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

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

Overview

Element whose boundary is represented by straight segments.

The same path is available as immutable #points, #segments, and #lines collections. Closed shapes repeat their first point at the end; open paths do not. Only closed shapes have a filled interior, so inside? on an open path is equivalent to testing its boundary.

Examples:

Inspect the interchangeable point, segment, and line views

rect = Sevgi::Geometry::Rect[8, 4]
rect.points.size   # => 5
rect.segments.size # => 4
rect.lines.size    # => 4

See Also:

Instance Method Summary collapse

Methods inherited from Sevgi::Geometry::Element

#at, #ignorable?, lined

Constructor Details

#initialize { ... } ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Creates a lined element from a geometry-definition block.

Yields:

  • evaluates point or segment definitions in the new element

Yield Returns:

  • (Object)

    ignored block result

Raises:



280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/sevgi/geometry/element.rb', line 280

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
  validate_geometry!
end

Instance Method Details

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

Returns a line by index.

Parameters:

  • i (Integer)

    line index

Returns:

Raises:



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

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

#approx(precision = nil) ⇒ Sevgi::Geometry::Element::Lined

Returns an element rebuilt from rounded boundary points.

Segments are derived from the rounded points so both representations describe the same path. When rounding breaks a concrete shape invariant, the result widens to a less specific Rect, Polygon, or Polyline rather than retaining a misleading concrete class.

Parameters:

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

    decimal precision, or nil for the current function default

Returns:

Raises:

  • (Sevgi::ArgumentError)

    when precision is invalid



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

def approx(precision = nil) = self.class.send(:approximate, *rounded_points(precision))

#boxSevgi::Geometry::Rect

Returns the bounding rectangle.



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

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:



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

def call(i) = points[i].tap { Error.("No point exists 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



312
313
314
# File 'lib/sevgi/geometry/element.rb', line 312

def draw(...)
  approx.send(: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)


400
401
402
403
404
# File 'lib/sevgi/geometry/element.rb', line 400

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)


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

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

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

Returns immutable boundary equations for all lines.

Returns:



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

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

#hashInteger

Returns a hash compatible with strict equality.

Returns:

  • (Integer)


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

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

#headSevgi::Geometry::Segment

Returns the first segment.



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

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.

Examples:

Compare closed and open path containment

rect = Sevgi::Geometry::Rect[8, 4]
line = Sevgi::Geometry::Line.([0, 0], [8, 0])
rect.inside?([4, 2]) # => true
line.inside?([4, 2]) # => false
line.inside?([4, 0]) # => true

Parameters:

Returns:

  • (Boolean)

Raises:



502
503
504
505
506
507
508
# File 'lib/sevgi/geometry/element.rb', line 502

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

  return on?(point) unless self.class.send(:close?)

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

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

Intersects the element boundary with an equation.

Precision is applied consistently to boundary-membership tolerance, returned-coordinate rounding, and duplicate collapse. A nil precision uses the current thread's function precision for all three stages.

Examples:

Intersect a rectangle with a vertical line

rect = Sevgi::Geometry::Rect[8, 4]
axis = Sevgi::Geometry::Equation.vertical(3)
rect.intersection(axis).map(&:deconstruct) # => [[3.0, 0.0], [3.0, 4.0]]

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



436
437
438
439
440
441
442
443
444
# File 'lib/sevgi/geometry/element.rb', line 436

def intersection(equation, precision: nil)
  Error.("Must be an equation: #{equation}") unless equation.is_a?(Equation)

  points = equations.flat_map do |candidate|
    equation.intersect(candidate).select { |point| boundary_point?(point, precision) }
  end

  points.map { |point| point.approx(precision) }.uniq
end

#lengthFloat

Returns the total path length.

Returns:

  • (Float)


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

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

#linesArray<Sevgi::Geometry::Line>

Returns immutable boundary lines derived from segments and points.

Returns:



470
471
472
473
474
475
476
477
# File 'lib/sevgi/geometry/element.rb', line 470

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:



514
515
516
517
518
# File 'lib/sevgi/geometry/element.rb', line 514

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:



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

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

#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:



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

def points(approximate = false)
  approximate ? rounded_points(nil) : @points
end

#positionSevgi::Geometry::Point

Returns the first point.



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

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:

Raises:



387
388
389
390
391
392
# File 'lib/sevgi/geometry/element.rb', line 387

Affinity.public_instance_methods(false).each do |transform|
  define_method(transform) do |*args, **kwargs, &block|
    transformed = points.map { it.public_send(transform, *args, **kwargs, &block) }
    self.class.send(:affine, *transformed)
  end
end

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

Returns an element rotated around the origin.

Parameters:

  • a (Numeric)

    clockwise angle in degrees

Returns:

Raises:



387
388
389
390
391
392
# File 'lib/sevgi/geometry/element.rb', line 387

Affinity.public_instance_methods(false).each do |transform|
  define_method(transform) do |*args, **kwargs, &block|
    transformed = points.map { it.public_send(transform, *args, **kwargs, &block) }
    self.class.send(:affine, *transformed)
  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:

Raises:



387
388
389
390
391
392
# File 'lib/sevgi/geometry/element.rb', line 387

Affinity.public_instance_methods(false).each do |transform|
  define_method(transform) do |*args, **kwargs, &block|
    transformed = points.map { it.public_send(transform, *args, **kwargs, &block) }
    self.class.send(:affine, *transformed)
  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:



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

def segments(approximate = false)
  approximate ? rounded_segments(nil) : @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:

Raises:



387
388
389
390
391
392
# File 'lib/sevgi/geometry/element.rb', line 387

Affinity.public_instance_methods(false).each do |transform|
  define_method(transform) do |*args, **kwargs, &block|
    transformed = points.map { it.public_send(transform, *args, **kwargs, &block) }
    self.class.send(:affine, *transformed)
  end
end

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

Returns an element skewed along x.

Parameters:

  • a (Numeric)

    skew angle in degrees

Returns:

Raises:



387
388
389
390
391
392
# File 'lib/sevgi/geometry/element.rb', line 387

Affinity.public_instance_methods(false).each do |transform|
  define_method(transform) do |*args, **kwargs, &block|
    transformed = points.map { it.public_send(transform, *args, **kwargs, &block) }
    self.class.send(:affine, *transformed)
  end
end

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

Returns an element skewed along y.

Parameters:

  • a (Numeric)

    skew angle in degrees

Returns:

Raises:



387
388
389
390
391
392
# File 'lib/sevgi/geometry/element.rb', line 387

Affinity.public_instance_methods(false).each do |transform|
  define_method(transform) do |*args, **kwargs, &block|
    transformed = points.map { it.public_send(transform, *args, **kwargs, &block) }
    self.class.send(:affine, *transformed)
  end
end

#tailSevgi::Geometry::Segment

Returns the last segment.



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

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:

Raises:



387
388
389
390
391
392
# File 'lib/sevgi/geometry/element.rb', line 387

Affinity.public_instance_methods(false).each do |transform|
  define_method(transform) do |*args, **kwargs, &block|
    transformed = points.map { it.public_send(transform, *args, **kwargs, &block) }
    self.class.send(:affine, *transformed)
  end
end