Class: Sevgi::Geometry::Element::Lined

Inherits:
Sevgi::Geometry::Element show all
Defined in:
lib/sevgi/geometry/element.rb

Overview

rubocop:disable Metrics/ClassLength Element whose boundary is represented by straight segments.

Constant Summary collapse

Open =

Open lined element base class.

Class.new(self) do
  # Draws the element as an SVG polyline.
  # @param node [Object] graphics node receiving the drawing command
  # @return [Object] graphics node command result
  def draw!(node, **) = node.polyline(points: points.map { it.deconstruct.join(",") }, **)
end
Close =

Closed lined element base class.

Class.new(self) do
  # Creates a closed element from points, appending the first point.
  # @param points [Array<Sevgi::Geometry::Point, Array<Numeric>>] boundary points
  # @return [Sevgi::Geometry::Element::Lined]
  # @raise [Sevgi::Geometry::Error] when any point cannot be coerced
  def self.new_by_points(*points) = super(*points, points.first)

  # Draws the element as an SVG polygon.
  # @param node [Object] graphics node receiving the drawing command
  # @return [Object] graphics node command result
  def draw!(node, **) = node.polygon(points: points.map { it.deconstruct.join(",") }, **)
end
SHORTCUTS =

Point shortcut names generated for fixed-size lined elements.

("A".."Z").to_a.freeze

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Sevgi::Geometry::Element

arced, #at, #ignorable?, lined, #translate

Constructor Details

#initialize(&block) ⇒ Lined

Returns a new instance of Lined.



204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/sevgi/geometry/element.rb', line 204

def initialize(&block)
  super()

  Error.("Constructor block required") unless block

  instance_exec(&block)

  @points ||= calculate_points_from_segments
  @segments ||= calculate_segments_from_points

  sanitize
end

Class Method Details

.[](*segments, position: Origin) ⇒ Sevgi::Geometry::Element::Lined

Builds an element from segments.

Parameters:

Returns:

Raises:



124
# File 'lib/sevgi/geometry/element.rb', line 124

def self.[](...) = from_segments(...)

.build(size = Undefined, open: false) ⇒ Class

Builds a concrete lined element class.

Parameters:

  • size (Integer, Sevgi::Undefined) (defaults to: Undefined)

    segment count for fixed-size elements, or Undefined for variable size

  • open (Boolean) (defaults to: false)

    true for an open path, false for a closed path

Returns:

  • (Class)

    lined element subclass



104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/sevgi/geometry/element.rb', line 104

def self.build(size = Undefined, open: false)
  Class.new(open ? Open : Close) do
    define_singleton_method(:close?) { !open }

    define_singleton_method(:open?) { open }

    define_singleton_method(:poly?) { size == Undefined }

    define_singleton_method(:size) { size }

    Lined.send(:define_shortcuts, self, size, open:) unless size == Undefined
  end
end

.call(*points) ⇒ Sevgi::Geometry::Element::Lined

Builds an element from points.

Parameters:

Returns:

Raises:



131
# File 'lib/sevgi/geometry/element.rb', line 131

def self.call(...) = from_points(...)

.from_points(*points) ⇒ Sevgi::Geometry::Element::Lined

Builds an element from points.

Parameters:

Returns:

Raises:



138
# File 'lib/sevgi/geometry/element.rb', line 138

def self.from_points(...) = new_by_points(...)

.from_segments(*segments, position: Origin) ⇒ Sevgi::Geometry::Element::Lined

Builds an element from segments.

Parameters:

Returns:

Raises:



146
# File 'lib/sevgi/geometry/element.rb', line 146

def self.from_segments(...) = new_by_segments(...)

.new_by_points(*points) ⇒ Sevgi::Geometry::Element::Lined

Builds an element from points, applying closed-path behavior where appropriate.

Parameters:

Returns:

Raises:



153
# File 'lib/sevgi/geometry/element.rb', line 153

def self.new_by_points(...) = new_by_points!(...)

.new_by_points!(*points) ⇒ Sevgi::Geometry::Element::Lined

Builds an element from an exact point path.

Closed classes require the closing point to be supplied by the caller.

Parameters:

Returns:

Raises:



161
162
163
164
165
# File 'lib/sevgi/geometry/element.rb', line 161

def self.new_by_points!(*points)
  new do
    @points = Tuples[Point, *points]
  end
end

.new_by_segments(*segments, position: Origin) ⇒ Sevgi::Geometry::Element::Lined

Builds an element from segments and a start position.

Parameters:

Returns:

Raises:



172
173
174
175
176
177
# File 'lib/sevgi/geometry/element.rb', line 172

def self.new_by_segments(*segments, position: Origin)
  new do
    @position = Tuple[Point, position]
    @segments = Tuples[Segment, *segments]
  end
end

Instance Method Details

#[](i) ⇒ Sevgi::Geometry::Line

Returns a line by index.

Parameters:

  • i (Integer)

    line index

Returns:

Raises:



305
# File 'lib/sevgi/geometry/element.rb', line 305

def [](i) = lines[i].tap { |line| Error.("No line exist for index: #{i}") unless line }

#approxSevgi::Geometry::Element::Lined

Returns an element with approximate points and segments.



221
222
223
224
225
226
# File 'lib/sevgi/geometry/element.rb', line 221

def approx
  points, segments = points(true), segments(true)
  self.class.send(:new) do
    @points, @segments = points, segments
  end
end

#boxSevgi::Geometry::Rect

Returns the bounding rectangle.



309
# File 'lib/sevgi/geometry/element.rb', line 309

def box = Rect.from_corners([(xs = points.map(&:x)).min, (ys = points.map(&:y)).min], [xs.max, ys.max])

#call(i) ⇒ Sevgi::Geometry::Point

Returns a point by index.

Parameters:

  • i (Integer)

    point index

Returns:

Raises:



315
# File 'lib/sevgi/geometry/element.rb', line 315

def call(i) = points[i].tap { Error.("No point exist for index: #{i}") unless it }

#draw(node, **attributes) ⇒ Object

Draws an approximate element into a graphics node.

Parameters:

  • node (Object)

    graphics node receiving the drawing command

  • attributes (Hash)

    drawing attributes

Returns:

  • (Object)

    graphics node command result



233
234
235
# File 'lib/sevgi/geometry/element.rb', line 233

def draw(...)
  approx.draw!(...)
end

#eql?(other) ⇒ Boolean Also known as: ==

Reports strict element equality by class and approximate points.

Parameters:

  • other (Object)

    object to compare

Returns:

  • (Boolean)


270
# File 'lib/sevgi/geometry/element.rb', line 270

def eql?(other) = self.class == other.class && points(true) == other.points(true)

#equationsArray<Sevgi::Geometry::Equation::Linear>

Returns boundary equations for all lines.



282
# File 'lib/sevgi/geometry/element.rb', line 282

def equations = @equations ||= lines.map(&:equation)

#hashInteger

Returns a hash compatible with strict equality.

Returns:

  • (Integer)


274
# File 'lib/sevgi/geometry/element.rb', line 274

def hash = [self.class, *points(true)].hash

#headSevgi::Geometry::Segment

Returns the first segment.



319
# File 'lib/sevgi/geometry/element.rb', line 319

def head = @head ||= segments.first

#inside?(point) ⇒ Boolean

Reports whether a point is inside or on the boundary.

Parameters:

Returns:

  • (Boolean)

Raises:



343
344
345
346
347
# File 'lib/sevgi/geometry/element.rb', line 343

def inside?(point)
  point = Tuple[Point, point]

  on?(point) || pnpoly(points, point)
end

#intersection(equation, precision: nil) ⇒ Array<Sevgi::Geometry::Point>

Intersects the element boundary with an equation.

Parameters:

  • equation (Sevgi::Geometry::Equation)

    equation to intersect with

  • precision (Integer, nil) (defaults to: nil)

    decimal precision, or nil for the current function default

Returns:

Raises:

  • (Sevgi::Geometry::Error)

    when equation is not an equation

  • (Sevgi::PanicError)

    when the equation combination is not implemented



290
291
292
293
294
295
296
297
# File 'lib/sevgi/geometry/element.rb', line 290

def intersection(equation, precision: nil)
  equations
    .map do |candidate|
      equation.intersect(candidate).map { |point| point.approx(precision) }.select { |point| on?(point) }
    end
    .flatten
    .uniq
end

#linesArray<Sevgi::Geometry::Line>

Returns boundary lines derived from segments and points.

Returns:



323
324
325
326
327
# File 'lib/sevgi/geometry/element.rb', line 323

def lines
  @lines ||= segments.zip(points[...segments.size]).map { |segment, position|
    segment.line(position)
  }
end

#on?(point) ⇒ Boolean

Reports whether a point is on the boundary.

Parameters:

Returns:

  • (Boolean)

Raises:



353
354
355
356
357
# File 'lib/sevgi/geometry/element.rb', line 353

def on?(point)
  point = Tuple[Point, point]

  lines.any? { it.over?(point) }
end

#outside?(point) ⇒ Boolean

Reports whether a point is outside the element boundary.

Parameters:

Returns:

  • (Boolean)

Raises:



363
# File 'lib/sevgi/geometry/element.rb', line 363

def outside?(point) = !inside?(point)

#perimeterFloat

Returns the sum of segment lengths.

Returns:

  • (Float)


331
# File 'lib/sevgi/geometry/element.rb', line 331

def perimeter = @perimeter ||= segments.sum(&:length)

#points(approximate = false) ⇒ Array<Sevgi::Geometry::Point>

Returns element points.

Parameters:

  • approximate (Boolean) (defaults to: false)

    true to round points with the current function precision

Returns:



240
241
242
# File 'lib/sevgi/geometry/element.rb', line 240

def points(approximate = false)
  approximate ? @points.map(&:approx) : @points
end

#positionSevgi::Geometry::Point

Returns the first point.



246
247
248
# File 'lib/sevgi/geometry/element.rb', line 246

def position
  @position ||= points.first
end

#segments(approximate = false) ⇒ Array<Sevgi::Geometry::Segment>

Returns element segments.

Parameters:

  • approximate (Boolean) (defaults to: false)

    true to round segments with the current function precision

Returns:



253
254
255
# File 'lib/sevgi/geometry/element.rb', line 253

def segments(approximate = false)
  approximate ? @segments.map(&:approx) : @segments
end

#tailSevgi::Geometry::Segment

Returns the last segment.



335
# File 'lib/sevgi/geometry/element.rb', line 335

def tail = @tail ||= segments.last