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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/LICENSE +5 -0
- data/README.md +37 -0
- data/lib/sevgi/geometry/element.rb +292 -46
- data/lib/sevgi/geometry/elements/line.rb +71 -15
- data/lib/sevgi/geometry/elements/parallelogram.rb +59 -0
- data/lib/sevgi/geometry/elements/polygon.rb +7 -1
- data/lib/sevgi/geometry/elements/polyline.rb +7 -1
- data/lib/sevgi/geometry/elements/rect.rb +88 -8
- data/lib/sevgi/geometry/elements/triangle.rb +47 -0
- data/lib/sevgi/geometry/equation/linear.rb +152 -9
- data/lib/sevgi/geometry/equation/quadratic.rb +10 -1
- data/lib/sevgi/geometry/equation.rb +50 -9
- data/lib/sevgi/geometry/errors.rb +1 -0
- data/lib/sevgi/geometry/internal.rb +36 -2
- data/lib/sevgi/geometry/operation/align.rb +37 -9
- data/lib/sevgi/geometry/operation/sweep.rb +86 -8
- data/lib/sevgi/geometry/operation.rb +71 -0
- data/lib/sevgi/geometry/point.rb +106 -8
- data/lib/sevgi/geometry/segment.rb +90 -15
- data/lib/sevgi/geometry/version.rb +2 -1
- data/lib/sevgi/geometry.rb +9 -0
- metadata +8 -9
- data/lib/sevgi/geometry/elements/circle.rb +0 -1
- data/lib/sevgi/geometry/elements/curve.rb +0 -1
- data/lib/sevgi/geometry/elements/ellipse.rb +0 -1
- data/lib/sevgi/geometry/elements/parm.rb +0 -37
- data/lib/sevgi/geometry/elements/tri.rb +0 -18
|
@@ -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}"
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
42
|
+
Point[that.position.x - this.position.x, 0]
|
|
19
43
|
when :right
|
|
20
|
-
|
|
44
|
+
Point[(that.position.x + that.width) - (this.position.x + this.width), 0]
|
|
21
45
|
when :top
|
|
22
|
-
|
|
46
|
+
Point[0, that.position.y - this.position.y]
|
|
23
47
|
when :bottom
|
|
24
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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},
|
|
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
|
-
|
|
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)
|
data/lib/sevgi/geometry/point.rb
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|