sevgi-geometry 0.73.2 → 0.94.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,12 +2,39 @@
2
2
 
3
3
  module Sevgi
4
4
  module Geometry
5
+ # Validates finite real numeric inputs for geometry primitives.
6
+ # @api private
7
+ module Real
8
+ # Coerces a public numeric value to Float.
9
+ # @param field [Symbol, String] coordinate or component name
10
+ # @param value [Object] value to coerce
11
+ # @return [Float] finite float value
12
+ # @raise [Sevgi::Geometry::Error] when value is not a finite Numeric
13
+ def self.[](field, value)
14
+ unless value.is_a?(::Numeric)
15
+ Error.("Geometry #{field} must be a finite Numeric: #{value.inspect}")
16
+ end
17
+
18
+ value.to_f.tap do |number|
19
+ Error.("Geometry #{field} must be finite: #{value.inspect}") unless number.finite?
20
+ end
21
+ end
22
+ end
23
+
24
+ private_constant :Real
25
+
26
+ # Coerces array-like geometry inputs into typed tuple objects.
27
+ # @api private
5
28
  module Tuple
29
+ # Coerces an argument into the requested tuple class.
30
+ # @param klass [Class] tuple class such as {Point} or {Segment}
31
+ # @param arg [Array<Numeric>, Object] tuple instance or two-element numeric array
32
+ # @return [Object] tuple instance
33
+ # @raise [Sevgi::Geometry::Error] when arg cannot be coerced
6
34
  def self.[](klass, arg)
7
35
  case arg
8
36
  when ::Array
9
- Error.("Array must have 2 elements: #{arg}", arg) unless arg.size == 2
10
- Error.("Must be a numeric array: #{arg}") unless arg.all?(::Numeric)
37
+ Error.("Array must have 2 elements: #{arg.inspect}") unless arg.size == 2
11
38
 
12
39
  klass.send(:new, *arg)
13
40
  when klass
@@ -20,7 +47,14 @@ module Sevgi
20
47
 
21
48
  private_constant :Tuple
22
49
 
50
+ # Coerces multiple array-like geometry inputs into typed tuple objects.
51
+ # @api private
23
52
  module Tuples
53
+ # Coerces arguments into the requested tuple class.
54
+ # @param klass [Class] tuple class such as {Point} or {Segment}
55
+ # @param args [Array<Array<Numeric>, Object>] tuple instances or two-element numeric arrays
56
+ # @return [Array<Object>] tuple instances
57
+ # @raise [Sevgi::Geometry::Error] when any argument cannot be coerced
24
58
  def self.[](klass, *args) = args.map { Tuple[klass, it] }
25
59
  end
26
60
 
@@ -3,36 +3,64 @@
3
3
  module Sevgi
4
4
  module Geometry
5
5
  module Operation
6
+ # Alignment operation implementation.
6
7
  module Align
8
+ extend self
9
+
10
+ # Returns an element translated to align with another element.
11
+ # @param element [Sevgi::Geometry::Element] element to move
12
+ # @param other [Sevgi::Geometry::Element] reference element
13
+ # @param alignment [Symbol] one of :center, :left, :right, :top, or :bottom
14
+ # @return [Sevgi::Geometry::Element] translated element
15
+ # @raise [Sevgi::Geometry::Operation::OperationInapplicableError] when other is not a geometry element
16
+ # @raise [Sevgi::ArgumentError] when alignment is unknown
7
17
  def align(element, other, alignment = :center)
8
- alignment(element, other, alignment).apply(element)
18
+ offset = alignment(element, other, alignment)
19
+
20
+ element.translate(offset.x, offset.y)
9
21
  end
10
22
 
23
+ # Returns the offset needed to align one element with another.
24
+ # @param element [Sevgi::Geometry::Element] element to move
25
+ # @param other [Sevgi::Geometry::Element] reference element
26
+ # @param alignment [Symbol] one of :center, :left, :right, :top, or :bottom
27
+ # @return [Sevgi::Geometry::Point] translation offset
28
+ # @raise [Sevgi::Geometry::Operation::OperationInapplicableError] when other is not a geometry element
29
+ # @raise [Sevgi::ArgumentError] when alignment is unknown
11
30
  def alignment(element, other, alignment = :center)
31
+ OperationInapplicableError.("Not a Geometric Element: #{other}") unless other.is_a?(Element)
32
+
12
33
  this, that = element.box, other.box
13
34
 
14
35
  case alignment
15
36
  when :center
16
- Translation[(this.width - that.width) / 2.0, (this.height - that.height) / 2.0]
37
+ Point[
38
+ that.position.x + ((that.width - this.width) / 2.0) - this.position.x,
39
+ that.position.y + ((that.height - this.height) / 2.0) - this.position.y
40
+ ]
17
41
  when :left
18
- Translation[this.ne.x - that.ne.x, 0]
42
+ Point[that.position.x - this.position.x, 0]
19
43
  when :right
20
- Translation[this.sw.x - that.sw.x, 0]
44
+ Point[(that.position.x + that.width) - (this.position.x + this.width), 0]
21
45
  when :top
22
- Translation[0, this.ne.y - that.ne.y]
46
+ Point[0, that.position.y - this.position.y]
23
47
  when :bottom
24
- Translation[0, this.sw.y - that.sw.y]
48
+ Point[0, (that.position.y + that.height) - (this.position.y + this.height)]
25
49
  else
26
50
  ArgumentError.("No such type of alignment: #{alignment}")
27
51
  end
28
52
  end
29
53
 
30
- def applicable?(element)
31
- Translation.applicable?(element)
32
- end
54
+ # Reports whether the alignment handler can operate on an element.
55
+ # @api private
56
+ # @param element [Object] candidate element
57
+ # @return [Boolean]
58
+ def applicable?(element) = element.respond_to?(:translate)
33
59
  end
34
60
 
35
61
  register(Align, :align, :alignment)
62
+
63
+ private_constant :Align
36
64
  end
37
65
  end
38
66
  end
@@ -3,13 +3,30 @@
3
3
  module Sevgi
4
4
  module Geometry
5
5
  module Operation
6
+ # Sweep operation implementation.
6
7
  module Sweep
7
8
  extend self
8
9
 
10
+ # Default maximum number of sweep iterations.
9
11
  LIMIT = 1_000
10
12
 
11
- def sweep(element, initial:, direction:, step:, limit: LIMIT, &block)
12
- equation = Tuple[Point, initial].equation(direction)
13
+ # Sweeps parallel lines across a lined element in both directions.
14
+ #
15
+ # Generated lines are boundary-to-boundary interior spans. A single
16
+ # sweep position can produce multiple lines for closed concave elements.
17
+ # @param element [Sevgi::Geometry::Element::Lined] element to intersect
18
+ # @param initial [Sevgi::Geometry::Point, Array<Numeric>] point on the initial sweep line
19
+ # @param angle [Numeric] clockwise sweep line angle in degrees
20
+ # @param step [Numeric] signed distance between sweep lines
21
+ # @param limit [Integer] maximum iterations per direction
22
+ # @yield [lines] optional hook receiving the generated lines
23
+ # @yieldparam lines [Array<Sevgi::Geometry::Line>] generated sweep lines
24
+ # @yieldreturn [void]
25
+ # @return [Array<Sevgi::Geometry::Line>] generated sweep lines
26
+ # @raise [Sevgi::Geometry::Error] when initial cannot be coerced
27
+ # @raise [Sevgi::Geometry::Operation::OperationError] when iteration reaches the limit
28
+ def sweep(element, initial:, angle:, step:, limit: LIMIT, &block)
29
+ equation = Tuple[Point, initial].equation(angle)
13
30
 
14
31
  [
15
32
  *unisweep(element, equation.shift(-step), -step, limit:).reverse,
@@ -19,16 +36,41 @@ module Sevgi
19
36
  end
20
37
  end
21
38
 
22
- def sweep!(element, initial:, direction:, step:, limit: LIMIT, &block)
23
- sweep(element, initial:, direction:, step:, limit:) do |lines|
39
+ # Sweeps parallel lines across a lined element and requires at least one result.
40
+ #
41
+ # Generated lines are boundary-to-boundary interior spans. A single
42
+ # sweep position can produce multiple lines for closed concave elements.
43
+ # @param element [Sevgi::Geometry::Element::Lined] element to intersect
44
+ # @param initial [Sevgi::Geometry::Point, Array<Numeric>] point on the initial sweep line
45
+ # @param angle [Numeric] clockwise sweep line angle in degrees
46
+ # @param step [Numeric] signed distance between sweep lines
47
+ # @param limit [Integer] maximum iterations per direction
48
+ # @yield [lines] optional hook receiving the generated lines
49
+ # @yieldparam lines [Array<Sevgi::Geometry::Line>] generated sweep lines
50
+ # @yieldreturn [void]
51
+ # @return [Array<Sevgi::Geometry::Line>] generated sweep lines
52
+ # @raise [Sevgi::Geometry::Error] when initial cannot be coerced
53
+ # @raise [Sevgi::Geometry::Operation::OperationError] when no lines are found or iteration reaches the limit
54
+ def sweep!(element, initial:, angle:, step:, limit: LIMIT, &block)
55
+ sweep(element, initial:, angle:, step:, limit:) do |lines|
24
56
  if lines.empty?
25
- OperationError.("No lines found [initial: #{initial}, direction: #{direction} step: #{step}]")
57
+ OperationError.("No lines found [initial: #{initial}, angle: #{angle} step: #{step}]")
26
58
  end
27
59
 
28
60
  yield(lines) if block
29
61
  end
30
62
  end
31
63
 
64
+ # Sweeps parallel lines in one signed direction from an equation.
65
+ #
66
+ # Generated lines are boundary-to-boundary interior spans. A single
67
+ # sweep position can produce multiple lines for closed concave elements.
68
+ # @param element [Sevgi::Geometry::Element::Lined] element to intersect
69
+ # @param equation [Sevgi::Geometry::Equation] initial sweep equation
70
+ # @param step [Numeric] signed distance between sweep lines
71
+ # @param limit [Integer] maximum iterations
72
+ # @return [Array<Sevgi::Geometry::Line>] generated sweep lines
73
+ # @raise [Sevgi::Geometry::Operation::OperationError] when iteration reaches the limit
32
74
  def unisweep(element, equation, step, limit: LIMIT)
33
75
  lines = []
34
76
 
@@ -36,9 +78,7 @@ module Sevgi
36
78
  points = element.intersection(equation)
37
79
  return lines if points.empty?
38
80
 
39
- if points.size == 2 && !(line = Line.(*points)).ignorable?
40
- lines << line
41
- end
81
+ lines.concat(interior_lines(element, equation, points))
42
82
 
43
83
  equation = equation.shift(step)
44
84
  end
@@ -46,12 +86,50 @@ module Sevgi
46
86
  OperationError.("Loop limit reached: #{limit}")
47
87
  end
48
88
 
89
+ # Reports whether the sweep handler can operate on an element.
90
+ # @api private
91
+ # @param element [Object] candidate element
92
+ # @return [Boolean]
49
93
  def applicable?(element)
50
94
  element.respond_to?(:intersection)
51
95
  end
96
+
97
+ private
98
+
99
+ def interior_lines(element, equation, points)
100
+ if points.size == 2
101
+ line = simple_line(points)
102
+
103
+ return line ? [line] : []
104
+ end
105
+
106
+ sorted_points(equation, points).each_cons(2).filter_map do |starting, ending|
107
+ next unless element.inside?(midpoint(starting, ending))
108
+
109
+ simple_line([starting, ending])
110
+ end
111
+ end
112
+
113
+ def midpoint(starting, ending)
114
+ Point[(starting.x + ending.x) / 2.0, (starting.y + ending.y) / 2.0]
115
+ end
116
+
117
+ def simple_line(points)
118
+ line = Line.(*points)
119
+
120
+ line unless line.ignorable?
121
+ end
122
+
123
+ def sorted_points(equation, points)
124
+ points.sort_by do |point|
125
+ equation.is_a?(Equation::Linear::Vertical) ? [point.y, point.x] : [point.x, point.y]
126
+ end
127
+ end
52
128
  end
53
129
 
54
130
  register(Sweep, :sweep, :sweep!, :unisweep)
131
+
132
+ private_constant :Sweep
55
133
  end
56
134
  end
57
135
  end
@@ -2,14 +2,85 @@
2
2
 
3
3
  module Sevgi
4
4
  module Geometry
5
+ # Dispatches geometry operations to operation handler modules.
5
6
  module Operation
6
7
  extend self
7
8
 
9
+ # Raised when an operation starts but cannot complete.
8
10
  OperationError = Class.new(Error)
11
+
12
+ # Raised when an operation does not apply to the target element.
9
13
  OperationInapplicableError = Class.new(Error)
10
14
 
15
+ # @!parse
16
+ # class << self
17
+ # # Returns an element translated to align with another element.
18
+ # # @param element [Sevgi::Geometry::Element] element to move
19
+ # # @param other [Sevgi::Geometry::Element] reference element
20
+ # # @param alignment [Symbol] one of :center, :left, :right, :top, or :bottom
21
+ # # @return [Sevgi::Geometry::Element] translated element
22
+ # # @raise [Sevgi::Geometry::Operation::OperationInapplicableError] when an argument is not a geometry element
23
+ # # @raise [Sevgi::ArgumentError] when alignment is unknown
24
+ # def align(element, other, alignment = :center); end
25
+ #
26
+ # # Returns the offset needed to align one element with another.
27
+ # # @param element [Sevgi::Geometry::Element] element to move
28
+ # # @param other [Sevgi::Geometry::Element] reference element
29
+ # # @param alignment [Symbol] one of :center, :left, :right, :top, or :bottom
30
+ # # @return [Sevgi::Geometry::Point] translation offset
31
+ # # @raise [Sevgi::Geometry::Operation::OperationInapplicableError] when an argument is not a geometry element
32
+ # # @raise [Sevgi::ArgumentError] when alignment is unknown
33
+ # def alignment(element, other, alignment = :center); end
34
+ #
35
+ # # Sweeps parallel lines across a lined element in both directions.
36
+ # # @param element [Sevgi::Geometry::Element::Lined] element to intersect
37
+ # # @param initial [Sevgi::Geometry::Point, Array<Numeric>] point on the initial sweep line
38
+ # # @param angle [Numeric] clockwise sweep line angle in degrees
39
+ # # @param step [Numeric] signed distance between sweep lines
40
+ # # @param limit [Integer] maximum iterations per direction
41
+ # # @yield [lines] optional hook receiving the generated lines
42
+ # # @yieldparam lines [Array<Sevgi::Geometry::Line>] generated sweep lines
43
+ # # @yieldreturn [void]
44
+ # # @return [Array<Sevgi::Geometry::Line>] generated sweep lines
45
+ # # @raise [Sevgi::Geometry::Operation::OperationInapplicableError] when element is not sweepable
46
+ # # @raise [Sevgi::Geometry::Error] when initial cannot be coerced
47
+ # # @raise [Sevgi::Geometry::Operation::OperationError] when iteration reaches the limit
48
+ # def sweep(element, initial:, angle:, step:, limit: Sweep::LIMIT); end
49
+ #
50
+ # # Sweeps parallel lines across a lined element and requires at least one result.
51
+ # # @param element [Sevgi::Geometry::Element::Lined] element to intersect
52
+ # # @param initial [Sevgi::Geometry::Point, Array<Numeric>] point on the initial sweep line
53
+ # # @param angle [Numeric] clockwise sweep line angle in degrees
54
+ # # @param step [Numeric] signed distance between sweep lines
55
+ # # @param limit [Integer] maximum iterations per direction
56
+ # # @yield [lines] optional hook receiving the generated lines
57
+ # # @yieldparam lines [Array<Sevgi::Geometry::Line>] generated sweep lines
58
+ # # @yieldreturn [void]
59
+ # # @return [Array<Sevgi::Geometry::Line>] generated sweep lines
60
+ # # @raise [Sevgi::Geometry::Operation::OperationInapplicableError] when element is not sweepable
61
+ # # @raise [Sevgi::Geometry::Error] when initial cannot be coerced
62
+ # # @raise [Sevgi::Geometry::Operation::OperationError] when no lines are found or iteration reaches the limit
63
+ # def sweep!(element, initial:, angle:, step:, limit: Sweep::LIMIT); end
64
+ #
65
+ # # Sweeps parallel lines in one signed direction from an equation.
66
+ # # @param element [Sevgi::Geometry::Element::Lined] element to intersect
67
+ # # @param equation [Sevgi::Geometry::Equation] initial sweep equation
68
+ # # @param step [Numeric] signed distance between sweep lines
69
+ # # @param limit [Integer] maximum iterations
70
+ # # @return [Array<Sevgi::Geometry::Line>] generated sweep lines
71
+ # # @raise [Sevgi::Geometry::Operation::OperationInapplicableError] when element is not sweepable
72
+ # # @raise [Sevgi::Geometry::Operation::OperationError] when iteration reaches the limit
73
+ # def unisweep(element, equation, step, limit: Sweep::LIMIT); end
74
+ # end
75
+ # Registers one or more public operation methods.
76
+ # @api private
77
+ # @param handler [Module] operation handler module
78
+ # @param operations [Array<Symbol>] operation method names
79
+ # @return [Array<Symbol>] registered operation names
11
80
  def register(handler, *operations) = operations.each { |operation| def_operation(operation, handler) }
12
81
 
82
+ private :register
83
+
13
84
  private
14
85
 
15
86
  def def_operation(operation, handler)
@@ -2,80 +2,178 @@
2
2
 
3
3
  module Sevgi
4
4
  module Geometry
5
+ # Affine transforms shared by immutable geometry tuples.
5
6
  module Affinity
7
+ # Reflects a point across the selected axes.
8
+ #
9
+ # `x:` controls reflection across the x-axis, which negates y. `y:`
10
+ # controls reflection across the y-axis, which negates x.
11
+ # @param x [Boolean] reflect across the x-axis
12
+ # @param y [Boolean] reflect across the y-axis
13
+ # @return [Sevgi::Geometry::Point]
6
14
  def reflect(x: true, y: true) = with(x: (y ? -1 : 1) * self.x(), y: (x ? -1 : 1) * self.y())
7
15
 
8
- def reflect_x = with(y: -y)
9
-
10
- def reflect_y = with(x: -x)
11
-
16
+ # Rotates a point around the origin using screen-space degrees.
17
+ # @param a [Numeric] clockwise angle in degrees
18
+ # @return [Sevgi::Geometry::Point]
12
19
  def rotate(a) = with(x: (x * F.cos(a)) - (y * F.sin(a)), y: (x * F.sin(a)) + (y * F.cos(a)))
13
20
 
21
+ # Scales a point from the origin.
22
+ # @param sx [Numeric] x scale factor
23
+ # @param sy [Numeric, Sevgi::Undefined] y scale factor, defaulting to sx
24
+ # @return [Sevgi::Geometry::Point]
14
25
  def scale(sx, sy = Undefined) = with(x: sx * x, y: Undefined.default(sy, sx) * y)
15
26
 
16
- def skew(ax, ay = Undefined) = with(x: x + (y * F.tan(ax)), y: y + (x * F.tan(Undefined(ay, ax))))
27
+ # Skews a point from the origin.
28
+ # @param ax [Numeric] x-axis skew angle in degrees
29
+ # @param ay [Numeric, Sevgi::Undefined] y-axis skew angle in degrees, defaulting to ax
30
+ # @return [Sevgi::Geometry::Point]
31
+ def skew(ax, ay = Undefined) = with(x: x + (y * F.tan(ax)), y: y + (x * F.tan(Undefined.default(ay, ax))))
17
32
 
18
- def skew_x(a) = with(x: x + (y * f.tan(a)))
33
+ # Skews a point along x.
34
+ # @param a [Numeric] skew angle in degrees
35
+ # @return [Sevgi::Geometry::Point]
36
+ def skew_x(a) = with(x: x + (y * F.tan(a)))
19
37
 
38
+ # Skews a point along y.
39
+ # @param a [Numeric] skew angle in degrees
40
+ # @return [Sevgi::Geometry::Point]
20
41
  def skew_y(a) = with(y: y + (x * F.tan(a)))
21
42
 
43
+ # Translates a point.
44
+ # @param dx [Numeric] x offset
45
+ # @param dy [Numeric, Sevgi::Undefined] y offset, defaulting to dx
46
+ # @return [Sevgi::Geometry::Point]
22
47
  def translate(dx, dy = Undefined) = with(x: x + dx, y: y + Undefined.default(dy, dx))
23
48
  end
24
49
 
50
+ # Immutable point in SVG/screen coordinates.
51
+ #
52
+ # Use `Point[x, y]` to create a point from two coordinates.
25
53
  Point = Data.define(:x, :y) do
26
54
  include Comparable
27
55
  include Affinity
28
56
 
57
+ # @!attribute [r] x
58
+ # @return [Float] x coordinate
59
+ # @!attribute [r] y
60
+ # @return [Float] y coordinate
61
+
62
+ # Returns the screen-space angle from one point to another.
63
+ # @param starting [Sevgi::Geometry::Point, Array<Numeric>] start point
64
+ # @param ending [Sevgi::Geometry::Point, Array<Numeric>] end point
65
+ # @return [Float] clockwise angle in degrees
66
+ # @raise [Sevgi::Geometry::Error] when either point cannot be coerced
29
67
  def self.angle(starting, ending)
30
68
  starting, ending = Tuples[Point, starting, ending]
31
69
  F.atan2(ending.y - starting.y, ending.x - starting.x)
32
70
  end
33
71
 
72
+ # Compares two points with optional numeric precision.
73
+ # @param this [Sevgi::Geometry::Point, Array<Numeric>] first point
74
+ # @param that [Sevgi::Geometry::Point, Array<Numeric>] second point
75
+ # @param precision [Integer, nil] decimal precision, or nil for the current function default
76
+ # @return [Boolean]
77
+ # @raise [Sevgi::Geometry::Error] when either point cannot be coerced
34
78
  def self.eq?(this, that, precision: nil)
35
79
  this, that = Tuples[self, this, that]
36
80
  F.eq?(this.x, that.x, precision:) && F.eq?(this.y, that.y, precision:)
37
81
  end
38
82
 
83
+ # Returns the Euclidean distance between two points.
84
+ # @param starting [Sevgi::Geometry::Point, Array<Numeric>] start point
85
+ # @param ending [Sevgi::Geometry::Point, Array<Numeric>] end point
86
+ # @return [Float]
87
+ # @raise [Sevgi::Geometry::Error] when either point cannot be coerced
39
88
  def self.length(starting, ending)
40
89
  starting, ending = Tuples[Point, starting, ending]
41
90
  ::Math.sqrt(((starting.y - ending.y) ** 2) + ((starting.x - ending.x) ** 2))
42
91
  end
43
92
 
93
+ # Returns the origin point.
94
+ # @return [Sevgi::Geometry::Point]
44
95
  def self.origin
45
96
  new(x: 0.0, y: 0.0)
46
97
  end
47
98
 
48
- def initialize(x:, y:) = super(x: x.to_f, y: y.to_f)
49
-
99
+ # Creates a point.
100
+ # @param x [Numeric] x coordinate
101
+ # @param y [Numeric] y coordinate
102
+ # @return [void]
103
+ # @raise [Sevgi::Geometry::Error] when a coordinate is not a finite Numeric
104
+ def initialize(x:, y:) = super(x: Real[:x, x], y: Real[:y, y])
105
+
106
+ # Compares points by x, then y.
107
+ # @param other [Sevgi::Geometry::Point, Array<Numeric>] point to compare
108
+ # @return [Integer, nil]
109
+ # @raise [Sevgi::Geometry::Error] when other cannot be coerced
50
110
  def <=>(other) = (other = Tuple[Point, other]).nan? || nan? ? nil : deconstruct <=> other.deconstruct
51
111
 
112
+ # Reports whether this point is at or above another point in screen coordinates.
113
+ # @param other [Sevgi::Geometry::Point, Array<Numeric>] point to compare
114
+ # @return [Boolean]
115
+ # @raise [Sevgi::Geometry::Error] when other cannot be coerced
52
116
  def above?(other) = y <= Tuple[Point, other].y
53
117
 
118
+ # Returns a point rounded to precision.
119
+ # @param precision [Integer, nil] decimal precision, or nil for the current function default
120
+ # @return [Sevgi::Geometry::Point]
54
121
  def approx(precision = nil) = with(x: F.approx(x, precision), y: F.approx(y, precision))
55
122
 
123
+ # Reports whether this point is at or below another point in screen coordinates.
124
+ # @param other [Sevgi::Geometry::Point, Array<Numeric>] point to compare
125
+ # @return [Boolean]
126
+ # @raise [Sevgi::Geometry::Error] when other cannot be coerced
56
127
  def below?(other) = y >= Tuple[Point, other].y
57
128
 
129
+ # Compares this point with optional numeric precision.
130
+ # @param other [Sevgi::Geometry::Point, Array<Numeric>] point to compare
131
+ # @param precision [Integer, nil] decimal precision, or nil for the current function default
132
+ # @return [Boolean]
133
+ # @raise [Sevgi::Geometry::Error] when other cannot be coerced
58
134
  def eq?(other, precision: nil) = self.class.eq?(self, other, precision:)
59
135
 
136
+ # Reports strict point equality.
137
+ # @param other [Object] object to compare
138
+ # @return [Boolean]
60
139
  def eql?(other) = self.class == other.class && deconstruct == other.deconstruct
61
140
 
141
+ # Returns a hash compatible with strict equality.
142
+ # @return [Integer]
62
143
  def hash = [self.class, *deconstruct].hash
63
144
 
145
+ # Reports whether any coordinate is infinite.
146
+ # @return [Boolean]
64
147
  def infinite? = deconstruct.any?(&:infinite?)
65
148
 
149
+ # Reports whether this point is at or left of another point.
150
+ # @param other [Sevgi::Geometry::Point, Array<Numeric>] point to compare
151
+ # @return [Boolean]
152
+ # @raise [Sevgi::Geometry::Error] when other cannot be coerced
66
153
  def left?(other) = x <= Tuple[Point, other].x
67
154
 
155
+ # Reports whether any coordinate is NaN.
156
+ # @return [Boolean]
68
157
  def nan? = deconstruct.any?(&:nan?)
69
158
 
159
+ # Reports whether this point is at or right of another point.
160
+ # @param other [Sevgi::Geometry::Point, Array<Numeric>] point to compare
161
+ # @return [Boolean]
162
+ # @raise [Sevgi::Geometry::Error] when other cannot be coerced
70
163
  def right?(other) = x >= Tuple[Point, other].x
71
164
 
165
+ # Formats point coordinates for SVG coordinate-list attributes.
166
+ # @return [String]
72
167
  def to_cs = "#{F.approx(x)},#{F.approx(y)}"
73
168
 
169
+ # Formats the point for display.
170
+ # @return [String]
74
171
  def to_s = "(#{to_cs})"
75
172
 
76
173
  alias_method :==, :eql?
77
174
  end
78
175
 
176
+ # Origin point in SVG/screen coordinates.
79
177
  Origin = Point.origin
80
178
  end
81
179
  end