sevgi-geometry 0.95.0 → 1.0.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 +221 -2
- data/README.md +33 -8
- data/lib/sevgi/geometry/element.rb +320 -161
- data/lib/sevgi/geometry/elements/arc.rb +256 -0
- data/lib/sevgi/geometry/elements/circle.rb +28 -0
- data/lib/sevgi/geometry/elements/ellipse/affine.rb +72 -0
- data/lib/sevgi/geometry/elements/ellipse/length.rb +80 -0
- data/lib/sevgi/geometry/elements/ellipse.rb +269 -0
- data/lib/sevgi/geometry/elements/line.rb +68 -23
- data/lib/sevgi/geometry/elements/parallelogram.rb +108 -29
- data/lib/sevgi/geometry/elements/polygon.rb +41 -1
- data/lib/sevgi/geometry/elements/polyline.rb +45 -1
- data/lib/sevgi/geometry/elements/rect.rb +163 -29
- data/lib/sevgi/geometry/elements/triangle.rb +57 -11
- data/lib/sevgi/geometry/equation/linear.rb +131 -69
- data/lib/sevgi/geometry/equation/quadratic.rb +98 -16
- data/lib/sevgi/geometry/equation.rb +65 -24
- data/lib/sevgi/geometry/internal.rb +11 -3
- data/lib/sevgi/geometry/operation/align.rb +2 -4
- data/lib/sevgi/geometry/operation/box.rb +50 -0
- data/lib/sevgi/geometry/operation/sweep.rb +32 -18
- data/lib/sevgi/geometry/operation.rb +33 -18
- data/lib/sevgi/geometry/point.rb +126 -14
- data/lib/sevgi/geometry/predicate.rb +176 -0
- data/lib/sevgi/geometry/segment.rb +79 -14
- data/lib/sevgi/geometry/version.rb +1 -1
- data/lib/sevgi/geometry.rb +26 -2
- metadata +13 -6
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sevgi
|
|
4
|
+
module Geometry
|
|
5
|
+
module Operation
|
|
6
|
+
# Returns the smallest axis-aligned rectangle enclosing all elements.
|
|
7
|
+
#
|
|
8
|
+
# Each element contributes its existing {Sevgi::Geometry::Element#box},
|
|
9
|
+
# including zero-size boxes whose positions may still extend the result.
|
|
10
|
+
# @param elements [Array<Sevgi::Geometry::Element>] elements to enclose
|
|
11
|
+
# @return [Sevgi::Geometry::Rect] aggregate bounding rectangle
|
|
12
|
+
# @raise [Sevgi::ArgumentError] when no elements are given
|
|
13
|
+
# @raise [Sevgi::Geometry::Operation::OperationInapplicableError] when an argument is not a geometry element
|
|
14
|
+
# @example Enclose several geometry values
|
|
15
|
+
# a = Sevgi::Geometry::Rect[10, 5, position: [2, 3]]
|
|
16
|
+
# b = Sevgi::Geometry::Line.([-4, 8], [20, 12])
|
|
17
|
+
# Sevgi::Geometry::Operation.box(a, b) # => Rect spanning both elements
|
|
18
|
+
def box(*elements)
|
|
19
|
+
validate_box_elements(elements)
|
|
20
|
+
boxes = elements.map(&:box)
|
|
21
|
+
|
|
22
|
+
Rect.from_corners(minimum_corner(boxes), maximum_corner(boxes))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def maximum_corner(boxes)
|
|
28
|
+
[
|
|
29
|
+
boxes.map { it.position.x + it.width }.max,
|
|
30
|
+
boxes.map { it.position.y + it.height }.max
|
|
31
|
+
]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def minimum_corner(boxes)
|
|
35
|
+
[
|
|
36
|
+
boxes.map { it.position.x }.min,
|
|
37
|
+
boxes.map { it.position.y }.min
|
|
38
|
+
]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def validate_box_elements(elements)
|
|
42
|
+
ArgumentError.("At least one geometric element required") if elements.empty?
|
|
43
|
+
|
|
44
|
+
elements.each do |element|
|
|
45
|
+
OperationInapplicableError.("Not a Geometric Element: #{element}") unless element.is_a?(Element)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -4,17 +4,18 @@ module Sevgi
|
|
|
4
4
|
module Geometry
|
|
5
5
|
module Operation
|
|
6
6
|
# Sweep operation implementation.
|
|
7
|
+
# @api private
|
|
7
8
|
module Sweep
|
|
8
9
|
extend self
|
|
9
10
|
|
|
10
11
|
# Default maximum number of sweep iterations.
|
|
11
12
|
LIMIT = 1_000
|
|
12
13
|
|
|
13
|
-
# Sweeps parallel lines across a
|
|
14
|
+
# Sweeps parallel lines across a geometry element in both directions.
|
|
14
15
|
#
|
|
15
16
|
# 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
|
|
17
|
+
# sweep position can produce multiple lines for closed concave elements. Open paths produce no interior lines.
|
|
18
|
+
# @param element [Sevgi::Geometry::Element] element to intersect
|
|
18
19
|
# @param initial [Sevgi::Geometry::Point, Array<Numeric>] point on the initial sweep line
|
|
19
20
|
# @param angle [Numeric] clockwise sweep line angle in degrees
|
|
20
21
|
# @param step [Numeric] signed distance between sweep lines
|
|
@@ -23,9 +24,10 @@ module Sevgi
|
|
|
23
24
|
# @yieldparam lines [Array<Sevgi::Geometry::Line>] generated sweep lines
|
|
24
25
|
# @yieldreturn [void]
|
|
25
26
|
# @return [Array<Sevgi::Geometry::Line>] generated sweep lines
|
|
26
|
-
# @raise [Sevgi::Geometry::Error] when initial
|
|
27
|
+
# @raise [Sevgi::Geometry::Error] when initial, angle, step, or limit is invalid
|
|
27
28
|
# @raise [Sevgi::Geometry::Operation::OperationError] when iteration reaches the limit
|
|
28
29
|
def sweep(element, initial:, angle:, step:, limit: LIMIT, &block)
|
|
30
|
+
step = validate_arguments(step, limit)
|
|
29
31
|
equation = Tuple[Point, initial].equation(angle)
|
|
30
32
|
|
|
31
33
|
[
|
|
@@ -36,11 +38,11 @@ module Sevgi
|
|
|
36
38
|
end
|
|
37
39
|
end
|
|
38
40
|
|
|
39
|
-
# Sweeps parallel lines across a
|
|
41
|
+
# Sweeps parallel lines across a geometry element and requires at least one result.
|
|
40
42
|
#
|
|
41
43
|
# 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
|
|
44
|
+
# sweep position can produce multiple lines for closed concave elements. Open paths produce no interior lines.
|
|
45
|
+
# @param element [Sevgi::Geometry::Element] element to intersect
|
|
44
46
|
# @param initial [Sevgi::Geometry::Point, Array<Numeric>] point on the initial sweep line
|
|
45
47
|
# @param angle [Numeric] clockwise sweep line angle in degrees
|
|
46
48
|
# @param step [Numeric] signed distance between sweep lines
|
|
@@ -49,7 +51,7 @@ module Sevgi
|
|
|
49
51
|
# @yieldparam lines [Array<Sevgi::Geometry::Line>] generated sweep lines
|
|
50
52
|
# @yieldreturn [void]
|
|
51
53
|
# @return [Array<Sevgi::Geometry::Line>] generated sweep lines
|
|
52
|
-
# @raise [Sevgi::Geometry::Error] when initial
|
|
54
|
+
# @raise [Sevgi::Geometry::Error] when initial, angle, step, or limit is invalid
|
|
53
55
|
# @raise [Sevgi::Geometry::Operation::OperationError] when no lines are found or iteration reaches the limit
|
|
54
56
|
def sweep!(element, initial:, angle:, step:, limit: LIMIT, &block)
|
|
55
57
|
sweep(element, initial:, angle:, step:, limit:) do |lines|
|
|
@@ -64,14 +66,16 @@ module Sevgi
|
|
|
64
66
|
# Sweeps parallel lines in one signed direction from an equation.
|
|
65
67
|
#
|
|
66
68
|
# 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
|
|
69
|
+
# sweep position can produce multiple lines for closed concave elements. Open paths produce no interior lines.
|
|
70
|
+
# @param element [Sevgi::Geometry::Element] element to intersect
|
|
69
71
|
# @param equation [Sevgi::Geometry::Equation] initial sweep equation
|
|
70
72
|
# @param step [Numeric] signed distance between sweep lines
|
|
71
73
|
# @param limit [Integer] maximum iterations
|
|
72
74
|
# @return [Array<Sevgi::Geometry::Line>] generated sweep lines
|
|
75
|
+
# @raise [Sevgi::Geometry::Error] when step or limit is invalid
|
|
73
76
|
# @raise [Sevgi::Geometry::Operation::OperationError] when iteration reaches the limit
|
|
74
77
|
def unisweep(element, equation, step, limit: LIMIT)
|
|
78
|
+
step = validate_arguments(step, limit)
|
|
75
79
|
lines = []
|
|
76
80
|
|
|
77
81
|
limit.times do
|
|
@@ -86,36 +90,46 @@ module Sevgi
|
|
|
86
90
|
OperationError.("Loop limit reached: #{limit}")
|
|
87
91
|
end
|
|
88
92
|
|
|
93
|
+
private :unisweep
|
|
94
|
+
|
|
89
95
|
# Reports whether the sweep handler can operate on an element.
|
|
90
96
|
# @api private
|
|
91
97
|
# @param element [Object] candidate element
|
|
92
98
|
# @return [Boolean]
|
|
93
99
|
def applicable?(element)
|
|
94
|
-
element.respond_to?(:intersection)
|
|
100
|
+
element.respond_to?(:intersection) && element.respond_to?(:inside?) && element.respond_to?(:on?)
|
|
95
101
|
end
|
|
96
102
|
|
|
97
103
|
private
|
|
98
104
|
|
|
105
|
+
def validate_arguments(step, limit)
|
|
106
|
+
step = Real[:step, step]
|
|
107
|
+
Error.("Sweep step must be nonzero") if step.zero?
|
|
108
|
+
unless limit.is_a?(::Integer) && limit.positive?
|
|
109
|
+
Error.("Sweep limit must be a positive Integer: #{limit.inspect}")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
step
|
|
113
|
+
end
|
|
114
|
+
|
|
99
115
|
def interior_lines(element, equation, points)
|
|
100
|
-
return []
|
|
116
|
+
return [] unless element.closed?
|
|
101
117
|
|
|
102
118
|
if points.size == 2
|
|
119
|
+
return [] unless element.inside?(Point.midpoint(*points))
|
|
120
|
+
|
|
103
121
|
line = simple_line(points)
|
|
104
122
|
|
|
105
123
|
return line ? [line] : []
|
|
106
124
|
end
|
|
107
125
|
|
|
108
126
|
sorted_points(equation, points).each_cons(2).filter_map do |starting, ending|
|
|
109
|
-
next unless element.inside?(midpoint(starting, ending))
|
|
127
|
+
next unless element.inside?(Point.midpoint(starting, ending))
|
|
110
128
|
|
|
111
129
|
simple_line([starting, ending])
|
|
112
130
|
end
|
|
113
131
|
end
|
|
114
132
|
|
|
115
|
-
def midpoint(starting, ending)
|
|
116
|
-
Point[(starting.x + ending.x) / 2.0, (starting.y + ending.y) / 2.0]
|
|
117
|
-
end
|
|
118
|
-
|
|
119
133
|
def simple_line(points)
|
|
120
134
|
line = Line.(*points)
|
|
121
135
|
|
|
@@ -129,7 +143,7 @@ module Sevgi
|
|
|
129
143
|
end
|
|
130
144
|
end
|
|
131
145
|
|
|
132
|
-
register(Sweep, :sweep, :sweep
|
|
146
|
+
register(Sweep, :sweep, :sweep!)
|
|
133
147
|
|
|
134
148
|
private_constant :Sweep
|
|
135
149
|
end
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Geometry
|
|
5
|
-
#
|
|
5
|
+
# Stateless operations that relate or derive geometry values.
|
|
6
|
+
#
|
|
7
|
+
# `alignment` returns a translation offset, while `align` applies that
|
|
8
|
+
# offset to a copy. Center alignment works on both axes. Edge alignments
|
|
9
|
+
# change only the named axis and preserve the other coordinate. `sweep`
|
|
10
|
+
# derives boundary-to-boundary spans from a closed geometry element, and
|
|
11
|
+
# `sweep!` additionally requires at least one span.
|
|
6
12
|
module Operation
|
|
7
13
|
extend self
|
|
8
14
|
|
|
@@ -15,25 +21,37 @@ module Sevgi
|
|
|
15
21
|
# @!parse
|
|
16
22
|
# class << self
|
|
17
23
|
# # Returns an element translated to align with another element.
|
|
24
|
+
# # Center alignment changes both axes. Edge alignments change only the named axis.
|
|
18
25
|
# # @param element [Sevgi::Geometry::Element] element to move
|
|
19
26
|
# # @param other [Sevgi::Geometry::Element] reference element
|
|
20
27
|
# # @param alignment [Symbol] one of :center, :left, :right, :top, or :bottom
|
|
21
28
|
# # @return [Sevgi::Geometry::Element] translated element
|
|
22
29
|
# # @raise [Sevgi::Geometry::Operation::OperationInapplicableError] when an argument is not a geometry element
|
|
23
30
|
# # @raise [Sevgi::ArgumentError] when alignment is unknown
|
|
31
|
+
# # @example Center one rectangle inside another
|
|
32
|
+
# # inner = Sevgi::Geometry::Rect[4, 2]
|
|
33
|
+
# # outer = Sevgi::Geometry::Rect[20, 10, position: [5, 5]]
|
|
34
|
+
# # Sevgi::Geometry::Operation.align(inner, outer).position.deconstruct # => [13.0, 9.0]
|
|
24
35
|
# def align(element, other, alignment = :center); end
|
|
25
36
|
#
|
|
26
37
|
# # Returns the offset needed to align one element with another.
|
|
38
|
+
# # Center alignment includes both axes. Edge alignments return zero on the other axis.
|
|
27
39
|
# # @param element [Sevgi::Geometry::Element] element to move
|
|
28
40
|
# # @param other [Sevgi::Geometry::Element] reference element
|
|
29
41
|
# # @param alignment [Symbol] one of :center, :left, :right, :top, or :bottom
|
|
30
42
|
# # @return [Sevgi::Geometry::Point] translation offset
|
|
31
43
|
# # @raise [Sevgi::Geometry::Operation::OperationInapplicableError] when an argument is not a geometry element
|
|
32
44
|
# # @raise [Sevgi::ArgumentError] when alignment is unknown
|
|
45
|
+
# # @example Calculate an offset without moving the element
|
|
46
|
+
# # inner = Sevgi::Geometry::Rect[4, 2]
|
|
47
|
+
# # outer = Sevgi::Geometry::Rect[20, 10, position: [5, 5]]
|
|
48
|
+
# # Sevgi::Geometry::Operation.alignment(inner, outer, :bottom).approx.deconstruct # => [0.0, 13.0]
|
|
33
49
|
# def alignment(element, other, alignment = :center); end
|
|
34
50
|
#
|
|
35
|
-
# # Sweeps parallel lines across a
|
|
36
|
-
# #
|
|
51
|
+
# # Sweeps parallel lines across a geometry element in both directions.
|
|
52
|
+
# # `angle` is the direction of the returned lines. `step` is their signed perpendicular spacing.
|
|
53
|
+
# # Open paths yield no interior spans.
|
|
54
|
+
# # @param element [Sevgi::Geometry::Element] element to intersect
|
|
37
55
|
# # @param initial [Sevgi::Geometry::Point, Array<Numeric>] point on the initial sweep line
|
|
38
56
|
# # @param angle [Numeric] clockwise sweep line angle in degrees
|
|
39
57
|
# # @param step [Numeric] signed distance between sweep lines
|
|
@@ -43,12 +61,18 @@ module Sevgi
|
|
|
43
61
|
# # @yieldreturn [void]
|
|
44
62
|
# # @return [Array<Sevgi::Geometry::Line>] generated sweep lines
|
|
45
63
|
# # @raise [Sevgi::Geometry::Operation::OperationInapplicableError] when element is not sweepable
|
|
46
|
-
# # @raise [Sevgi::Geometry::Error] when initial
|
|
64
|
+
# # @raise [Sevgi::Geometry::Error] when initial, angle, step, or limit is invalid
|
|
47
65
|
# # @raise [Sevgi::Geometry::Operation::OperationError] when iteration reaches the limit
|
|
66
|
+
# # @example Generate horizontal spans through a rectangle
|
|
67
|
+
# # rect = Sevgi::Geometry::Rect[10, 6]
|
|
68
|
+
# # lines = Sevgi::Geometry::Operation.sweep(rect, initial: [0, 0], angle: 0, step: 2)
|
|
69
|
+
# # lines.size # => 4
|
|
70
|
+
# # lines.map(&:length).uniq # => [10.0]
|
|
48
71
|
# def sweep(element, initial:, angle:, step:, limit: Sweep::LIMIT); end
|
|
49
72
|
#
|
|
50
|
-
# # Sweeps parallel lines across a
|
|
51
|
-
# #
|
|
73
|
+
# # Sweeps parallel lines across a geometry element and requires at least one result.
|
|
74
|
+
# # It has the same geometry as {sweep}, but raises when the result is empty.
|
|
75
|
+
# # @param element [Sevgi::Geometry::Element] element to intersect
|
|
52
76
|
# # @param initial [Sevgi::Geometry::Point, Array<Numeric>] point on the initial sweep line
|
|
53
77
|
# # @param angle [Numeric] clockwise sweep line angle in degrees
|
|
54
78
|
# # @param step [Numeric] signed distance between sweep lines
|
|
@@ -58,19 +82,9 @@ module Sevgi
|
|
|
58
82
|
# # @yieldreturn [void]
|
|
59
83
|
# # @return [Array<Sevgi::Geometry::Line>] generated sweep lines
|
|
60
84
|
# # @raise [Sevgi::Geometry::Operation::OperationInapplicableError] when element is not sweepable
|
|
61
|
-
# # @raise [Sevgi::Geometry::Error] when initial
|
|
85
|
+
# # @raise [Sevgi::Geometry::Error] when initial, angle, step, or limit is invalid
|
|
62
86
|
# # @raise [Sevgi::Geometry::Operation::OperationError] when no lines are found or iteration reaches the limit
|
|
63
87
|
# 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
88
|
# end
|
|
75
89
|
# Registers one or more public operation methods.
|
|
76
90
|
# @api private
|
|
@@ -87,7 +101,7 @@ module Sevgi
|
|
|
87
101
|
define_singleton_method(operation) do |element, *args, **kwargs, &block|
|
|
88
102
|
OperationInapplicableError.("Not a Geometric Element: #{element}") unless element.is_a?(Element)
|
|
89
103
|
unless handler.applicable?(element)
|
|
90
|
-
OperationInapplicableError.("
|
|
104
|
+
OperationInapplicableError.("Operation not applicable to #{element}: #{handler}")
|
|
91
105
|
end
|
|
92
106
|
|
|
93
107
|
handler.public_send(operation, element, *args, **kwargs, &block)
|
|
@@ -98,4 +112,5 @@ module Sevgi
|
|
|
98
112
|
end
|
|
99
113
|
|
|
100
114
|
require_relative "operation/align"
|
|
115
|
+
require_relative "operation/box"
|
|
101
116
|
require_relative "operation/sweep"
|
data/lib/sevgi/geometry/point.rb
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Geometry
|
|
5
|
-
#
|
|
5
|
+
# Implementation owner and registry for public affine transformations on Point and lined elements.
|
|
6
|
+
# @api private
|
|
6
7
|
module Affinity
|
|
7
8
|
# Reflects a point across the selected axes.
|
|
8
9
|
#
|
|
@@ -11,45 +12,141 @@ module Sevgi
|
|
|
11
12
|
# @param x [Boolean] reflect across the x-axis
|
|
12
13
|
# @param y [Boolean] reflect across the y-axis
|
|
13
14
|
# @return [Sevgi::Geometry::Point]
|
|
14
|
-
|
|
15
|
+
# @raise [Sevgi::Geometry::Error] when a flag is not Boolean
|
|
16
|
+
def reflect(x: true, y: true)
|
|
17
|
+
Error.("Reflection x flag must be Boolean") unless x.equal?(true) || x.equal?(false)
|
|
18
|
+
Error.("Reflection y flag must be Boolean") unless y.equal?(true) || y.equal?(false)
|
|
19
|
+
|
|
20
|
+
with(x: (y ? -1 : 1) * self.x(), y: (x ? -1 : 1) * self.y())
|
|
21
|
+
end
|
|
15
22
|
|
|
16
23
|
# Rotates a point around the origin using screen-space degrees.
|
|
17
24
|
# @param a [Numeric] clockwise angle in degrees
|
|
18
25
|
# @return [Sevgi::Geometry::Point]
|
|
19
|
-
|
|
26
|
+
# @raise [Sevgi::Geometry::Error] when angle is not a finite real number
|
|
27
|
+
def rotate(a)
|
|
28
|
+
a = Real[:angle, a]
|
|
29
|
+
with(x: (x * F.cos(a)) - (y * F.sin(a)), y: (x * F.sin(a)) + (y * F.cos(a)))
|
|
30
|
+
end
|
|
20
31
|
|
|
21
32
|
# Scales a point from the origin.
|
|
22
33
|
# @param sx [Numeric] x scale factor
|
|
23
34
|
# @param sy [Numeric, Sevgi::Undefined] y scale factor, defaulting to sx
|
|
24
35
|
# @return [Sevgi::Geometry::Point]
|
|
25
|
-
|
|
36
|
+
# @raise [Sevgi::Geometry::Error] when a scale is not a finite real number
|
|
37
|
+
def scale(sx, sy = Undefined)
|
|
38
|
+
sx = Real[:sx, sx]
|
|
39
|
+
sy = Real[:sy, Undefined.default(sy, sx)]
|
|
40
|
+
with(x: sx * x, y: sy * y)
|
|
41
|
+
end
|
|
26
42
|
|
|
27
43
|
# Skews a point from the origin.
|
|
28
44
|
# @param ax [Numeric] x-axis skew angle in degrees
|
|
29
45
|
# @param ay [Numeric, Sevgi::Undefined] y-axis skew angle in degrees, defaulting to ax
|
|
30
46
|
# @return [Sevgi::Geometry::Point]
|
|
31
|
-
|
|
47
|
+
# @raise [Sevgi::Geometry::Error] when an angle is not a finite real number
|
|
48
|
+
def skew(ax, ay = Undefined)
|
|
49
|
+
ax = Real[:ax, ax]
|
|
50
|
+
ay = Real[:ay, Undefined.default(ay, ax)]
|
|
51
|
+
with(x: x + (y * F.tan(ax)), y: y + (x * F.tan(ay)))
|
|
52
|
+
end
|
|
32
53
|
|
|
33
54
|
# Skews a point along x.
|
|
34
55
|
# @param a [Numeric] skew angle in degrees
|
|
35
56
|
# @return [Sevgi::Geometry::Point]
|
|
36
|
-
|
|
57
|
+
# @raise [Sevgi::Geometry::Error] when angle is not a finite real number
|
|
58
|
+
def skew_x(a) = with(x: x + (y * F.tan(Real[:angle, a])))
|
|
37
59
|
|
|
38
60
|
# Skews a point along y.
|
|
39
61
|
# @param a [Numeric] skew angle in degrees
|
|
40
62
|
# @return [Sevgi::Geometry::Point]
|
|
41
|
-
|
|
63
|
+
# @raise [Sevgi::Geometry::Error] when angle is not a finite real number
|
|
64
|
+
def skew_y(a) = with(y: y + (x * F.tan(Real[:angle, a])))
|
|
42
65
|
|
|
43
66
|
# Translates a point.
|
|
44
67
|
# @param dx [Numeric] x offset
|
|
45
68
|
# @param dy [Numeric, Sevgi::Undefined] y offset, defaulting to dx
|
|
46
69
|
# @return [Sevgi::Geometry::Point]
|
|
47
|
-
|
|
70
|
+
# @raise [Sevgi::Geometry::Error] when an offset is not a finite real number
|
|
71
|
+
def translate(dx, dy = Undefined)
|
|
72
|
+
dx = Real[:dx, dx]
|
|
73
|
+
dy = Real[:dy, Undefined.default(dy, dx)]
|
|
74
|
+
with(x: x + dx, y: y + dy)
|
|
75
|
+
end
|
|
48
76
|
end
|
|
49
77
|
|
|
50
78
|
# Immutable point in SVG/screen coordinates.
|
|
51
79
|
#
|
|
52
|
-
# Use `Point[x, y]` to create a point from two coordinates.
|
|
80
|
+
# Use `Point[x, y]` to create a point from two coordinates. Public geometry
|
|
81
|
+
# methods that expect a point also accept `[x, y]`. Explicit Point values are
|
|
82
|
+
# most useful when a result will be transformed, compared, or reused.
|
|
83
|
+
# @example Measure and rotate a point in screen coordinates
|
|
84
|
+
# point = Sevgi::Geometry::Point[3, 4]
|
|
85
|
+
# Sevgi::Geometry::Point.length(Sevgi::Geometry::Origin, point) # => 5.0
|
|
86
|
+
# point.rotate(90).approx.deconstruct # => [-4.0, 3.0]
|
|
87
|
+
# @example Compare positions in screen coordinates
|
|
88
|
+
# upper = Sevgi::Geometry::Point[4, 2]
|
|
89
|
+
# lower = Sevgi::Geometry::Point[4, 8]
|
|
90
|
+
# upper.above?(lower) # => true
|
|
91
|
+
# upper.left?([6, 2]) # => true
|
|
92
|
+
# @see Sevgi::Geometry::Segment
|
|
93
|
+
# @!parse
|
|
94
|
+
# class Point
|
|
95
|
+
# # Creates a point from two coordinates.
|
|
96
|
+
# # @param x [Numeric] x coordinate
|
|
97
|
+
# # @param y [Numeric] y coordinate
|
|
98
|
+
# # @return [Sevgi::Geometry::Point]
|
|
99
|
+
# # @raise [Sevgi::Geometry::Error] when a coordinate is not a finite Numeric
|
|
100
|
+
# # @example Create a point with mathematical notation
|
|
101
|
+
# # Sevgi::Geometry::Point[3, 5]
|
|
102
|
+
# def self.[](x, y); end
|
|
103
|
+
#
|
|
104
|
+
# # Returns a point reflected across the selected axes.
|
|
105
|
+
# # @param x [Boolean] reflect across the x-axis
|
|
106
|
+
# # @param y [Boolean] reflect across the y-axis
|
|
107
|
+
# # @return [Sevgi::Geometry::Point]
|
|
108
|
+
# # @raise [Sevgi::Geometry::Error] when a flag is not Boolean
|
|
109
|
+
# def reflect(x: true, y: true); end
|
|
110
|
+
#
|
|
111
|
+
# # Returns a point rotated around the origin.
|
|
112
|
+
# # @param a [Numeric] clockwise angle in degrees
|
|
113
|
+
# # @return [Sevgi::Geometry::Point]
|
|
114
|
+
# # @raise [Sevgi::Geometry::Error] when angle is not a finite real number
|
|
115
|
+
# def rotate(a); end
|
|
116
|
+
#
|
|
117
|
+
# # Returns a point scaled from the origin.
|
|
118
|
+
# # @param sx [Numeric] x scale factor
|
|
119
|
+
# # @param sy [Numeric, Sevgi::Undefined] y scale factor, defaulting to sx
|
|
120
|
+
# # @return [Sevgi::Geometry::Point]
|
|
121
|
+
# # @raise [Sevgi::Geometry::Error] when a scale is not a finite real number
|
|
122
|
+
# def scale(sx, sy = Undefined); end
|
|
123
|
+
#
|
|
124
|
+
# # Returns a point skewed from the origin.
|
|
125
|
+
# # @param ax [Numeric] x-axis skew angle in degrees
|
|
126
|
+
# # @param ay [Numeric, Sevgi::Undefined] y-axis skew angle in degrees, defaulting to ax
|
|
127
|
+
# # @return [Sevgi::Geometry::Point]
|
|
128
|
+
# # @raise [Sevgi::Geometry::Error] when an angle is not a finite real number
|
|
129
|
+
# def skew(ax, ay = Undefined); end
|
|
130
|
+
#
|
|
131
|
+
# # Returns a point skewed along x.
|
|
132
|
+
# # @param a [Numeric] skew angle in degrees
|
|
133
|
+
# # @return [Sevgi::Geometry::Point]
|
|
134
|
+
# # @raise [Sevgi::Geometry::Error] when angle is not a finite real number
|
|
135
|
+
# def skew_x(a); end
|
|
136
|
+
#
|
|
137
|
+
# # Returns a point skewed along y.
|
|
138
|
+
# # @param a [Numeric] skew angle in degrees
|
|
139
|
+
# # @return [Sevgi::Geometry::Point]
|
|
140
|
+
# # @raise [Sevgi::Geometry::Error] when angle is not a finite real number
|
|
141
|
+
# def skew_y(a); end
|
|
142
|
+
#
|
|
143
|
+
# # Returns a translated point.
|
|
144
|
+
# # @param dx [Numeric] x offset
|
|
145
|
+
# # @param dy [Numeric, Sevgi::Undefined] y offset, defaulting to dx
|
|
146
|
+
# # @return [Sevgi::Geometry::Point]
|
|
147
|
+
# # @raise [Sevgi::Geometry::Error] when an offset is not a finite real number
|
|
148
|
+
# def translate(dx, dy = Undefined); end
|
|
149
|
+
# end
|
|
53
150
|
Point = Data.define(:x, :y) do
|
|
54
151
|
include Comparable
|
|
55
152
|
include Affinity
|
|
@@ -87,7 +184,17 @@ module Sevgi
|
|
|
87
184
|
# @raise [Sevgi::Geometry::Error] when either point cannot be coerced
|
|
88
185
|
def self.length(starting, ending)
|
|
89
186
|
starting, ending = Tuples[Point, starting, ending]
|
|
90
|
-
::Math.
|
|
187
|
+
::Math.hypot(starting.x - ending.x, starting.y - ending.y)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Returns the midpoint between two points.
|
|
191
|
+
# @param starting [Sevgi::Geometry::Point, Array<Numeric>] first point
|
|
192
|
+
# @param ending [Sevgi::Geometry::Point, Array<Numeric>] second point
|
|
193
|
+
# @return [Sevgi::Geometry::Point] midpoint
|
|
194
|
+
# @raise [Sevgi::Geometry::Error] when either point cannot be coerced
|
|
195
|
+
def self.midpoint(starting, ending)
|
|
196
|
+
starting, ending = Tuples[Point, starting, ending]
|
|
197
|
+
self[(starting.x / 2.0) + (ending.x / 2.0), (starting.y / 2.0) + (ending.y / 2.0)]
|
|
91
198
|
end
|
|
92
199
|
|
|
93
200
|
# Returns the origin point.
|
|
@@ -104,10 +211,13 @@ module Sevgi
|
|
|
104
211
|
def initialize(x:, y:) = super(x: Real[:x, x], y: Real[:y, y])
|
|
105
212
|
|
|
106
213
|
# Compares points by x, then y.
|
|
107
|
-
# @param other [
|
|
108
|
-
# @return [Integer, nil]
|
|
109
|
-
|
|
110
|
-
|
|
214
|
+
# @param other [Object] point or two-item coordinate array to compare
|
|
215
|
+
# @return [Integer, nil] comparison result, or nil when other is not a valid point value
|
|
216
|
+
def <=>(other)
|
|
217
|
+
deconstruct <=> Tuple[Point, other].deconstruct
|
|
218
|
+
rescue Error
|
|
219
|
+
nil
|
|
220
|
+
end
|
|
111
221
|
|
|
112
222
|
# Reports whether this point is at or above another point in screen coordinates.
|
|
113
223
|
# @param other [Sevgi::Geometry::Point, Array<Numeric>] point to compare
|
|
@@ -165,6 +275,8 @@ module Sevgi
|
|
|
165
275
|
alias_method :==, :eql?
|
|
166
276
|
end
|
|
167
277
|
|
|
278
|
+
private_constant :Affinity
|
|
279
|
+
|
|
168
280
|
# Origin point in SVG/screen coordinates.
|
|
169
281
|
Origin = Point.origin
|
|
170
282
|
end
|