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

#at, #ignorable?, lined

Constructor Details

#initialize(&block) ⇒ Lined

Returns a new instance of Lined.



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

def initialize(&block)
  super()

  Error.("Constructor block required") unless block

  instance_exec(&block)

  @points ||= calculate_points_from_segments
  @segments ||= calculate_segments_from_points
  freeze_geometry!

  sanitize
end

Class Method Details

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

Builds an element from segments.

Parameters:

Returns:

Raises:



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

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



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

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:



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

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

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

Builds an element from points.

Parameters:

Returns:

Raises:



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

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

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

Builds an element from segments.

Parameters:

Returns:

Raises:



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

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:



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

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:



163
164
165
166
167
# File 'lib/sevgi/geometry/element.rb', line 163

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:



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

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:



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

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.



224
225
226
227
228
229
# File 'lib/sevgi/geometry/element.rb', line 224

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.



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

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:



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

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



236
237
238
# File 'lib/sevgi/geometry/element.rb', line 236

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

#eq?(other, precision: nil) ⇒ Boolean

Compares element points with optional numeric precision.

Parameters:

  • other (Object)

    object to compare

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

    decimal precision, or nil for the current function default

Returns:

  • (Boolean)


313
314
315
316
317
# File 'lib/sevgi/geometry/element.rb', line 313

def eq?(other, precision: nil)
  other.instance_of?(self.class) &&
    points.size == other.points.size &&
    points.zip(other.points).all? { |left, right| left.eq?(right, precision:) }
end

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

Reports strict element equality by class and exact points.

Parameters:

  • other (Object)

    object to compare

Returns:

  • (Boolean)


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

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

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

Returns immutable boundary equations for all lines.

Returns:



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

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

#hashInteger

Returns a hash compatible with strict equality.

Returns:

  • (Integer)


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

def hash = [self.class, *points].hash

#headSevgi::Geometry::Segment

Returns the first segment.



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

def head = @head ||= segments.first

#inside?(point) ⇒ Boolean

Reports whether a point is inside or on the boundary.

Open paths have no filled interior; for them this predicate is true only for points on the actual path boundary.

Parameters:

Returns:

  • (Boolean)

Raises:



404
405
406
407
408
409
410
# File 'lib/sevgi/geometry/element.rb', line 404

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

  return on?(point) if self.class.open?

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

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

Intersects the element boundary with an equation.

Boundary membership is tested on unrounded candidate points. precision: only rounds returned coordinates and controls duplicate collapse after membership has been accepted. When precision is nil, returned points use the current function precision.

Parameters:

  • equation (Sevgi::Geometry::Equation)

    equation to intersect with

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

    decimal precision for returned points, 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



347
348
349
350
351
352
# File 'lib/sevgi/geometry/element.rb', line 347

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

#linesArray<Sevgi::Geometry::Line>

Returns immutable boundary lines derived from segments and points.

Returns:



378
379
380
381
382
383
384
385
# File 'lib/sevgi/geometry/element.rb', line 378

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

#on?(point) ⇒ Boolean

Reports whether a point is on the boundary.

Parameters:

Returns:

  • (Boolean)

Raises:



416
417
418
419
420
# File 'lib/sevgi/geometry/element.rb', line 416

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:



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

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

#perimeterFloat

Returns the sum of segment lengths.

Returns:

  • (Float)


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

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

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

Returns immutable element points.

Parameters:

  • approximate (Boolean) (defaults to: false)

    true to round points with the current function precision

Returns:



243
244
245
# File 'lib/sevgi/geometry/element.rb', line 243

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

#positionSevgi::Geometry::Point

Returns the first point.



249
250
251
# File 'lib/sevgi/geometry/element.rb', line 249

def position
  @position ||= points.first
end

#reflect(x: true, y: true) ⇒ Sevgi::Geometry::Element::Lined

Returns an element reflected across the selected axes.

Parameters:

  • x (Boolean) (defaults to: true)

    reflect across the x-axis

  • y (Boolean) (defaults to: true)

    reflect across the y-axis

Returns:



301
302
303
304
305
# File 'lib/sevgi/geometry/element.rb', line 301

Geometry::Affinity.instance_methods.each do |transform|
  define_method(transform) do |*args, **kwargs, &block|
    self.class.new_by_points!(*points.map { it.public_send(transform, *args, **kwargs, &block) })
  end
end

#rotate(a) ⇒ Sevgi::Geometry::Element::Lined

Returns an element rotated around the origin.

Parameters:

  • a (Numeric)

    clockwise angle in degrees

Returns:



301
302
303
304
305
# File 'lib/sevgi/geometry/element.rb', line 301

Geometry::Affinity.instance_methods.each do |transform|
  define_method(transform) do |*args, **kwargs, &block|
    self.class.new_by_points!(*points.map { it.public_send(transform, *args, **kwargs, &block) })
  end
end

#scale(sx, sy = Undefined) ⇒ Sevgi::Geometry::Element::Lined

Returns an element scaled from the origin.

Parameters:

  • sx (Numeric)

    x scale factor

  • sy (Numeric, Sevgi::Undefined) (defaults to: Undefined)

    y scale factor, defaulting to sx

Returns:



301
302
303
304
305
# File 'lib/sevgi/geometry/element.rb', line 301

Geometry::Affinity.instance_methods.each do |transform|
  define_method(transform) do |*args, **kwargs, &block|
    self.class.new_by_points!(*points.map { it.public_send(transform, *args, **kwargs, &block) })
  end
end

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

Returns immutable element segments.

Parameters:

  • approximate (Boolean) (defaults to: false)

    true to round segments with the current function precision

Returns:



256
257
258
# File 'lib/sevgi/geometry/element.rb', line 256

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

#skew(ax, ay = Undefined) ⇒ Sevgi::Geometry::Element::Lined

Returns an element skewed from the origin.

Parameters:

  • ax (Numeric)

    x-axis skew angle in degrees

  • ay (Numeric, Sevgi::Undefined) (defaults to: Undefined)

    y-axis skew angle in degrees, defaulting to ax

Returns:



301
302
303
304
305
# File 'lib/sevgi/geometry/element.rb', line 301

Geometry::Affinity.instance_methods.each do |transform|
  define_method(transform) do |*args, **kwargs, &block|
    self.class.new_by_points!(*points.map { it.public_send(transform, *args, **kwargs, &block) })
  end
end

#skew_x(a) ⇒ Sevgi::Geometry::Element::Lined

Returns an element skewed along x.

Parameters:

  • a (Numeric)

    skew angle in degrees

Returns:



301
302
303
304
305
# File 'lib/sevgi/geometry/element.rb', line 301

Geometry::Affinity.instance_methods.each do |transform|
  define_method(transform) do |*args, **kwargs, &block|
    self.class.new_by_points!(*points.map { it.public_send(transform, *args, **kwargs, &block) })
  end
end

#skew_y(a) ⇒ Sevgi::Geometry::Element::Lined

Returns an element skewed along y.

Parameters:

  • a (Numeric)

    skew angle in degrees

Returns:



301
302
303
304
305
# File 'lib/sevgi/geometry/element.rb', line 301

Geometry::Affinity.instance_methods.each do |transform|
  define_method(transform) do |*args, **kwargs, &block|
    self.class.new_by_points!(*points.map { it.public_send(transform, *args, **kwargs, &block) })
  end
end

#tailSevgi::Geometry::Segment

Returns the last segment.



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

def tail = @tail ||= segments.last

#translate(dx, dy = Undefined) ⇒ Sevgi::Geometry::Element::Lined

Returns an element translated by offset.

Parameters:

  • dx (Numeric)

    x offset

  • dy (Numeric, Sevgi::Undefined) (defaults to: Undefined)

    y offset, defaulting to dx

Returns:



301
302
303
304
305
# File 'lib/sevgi/geometry/element.rb', line 301

Geometry::Affinity.instance_methods.each do |transform|
  define_method(transform) do |*args, **kwargs, &block|
    self.class.new_by_points!(*points.map { it.public_send(transform, *args, **kwargs, &block) })
  end
end