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
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sevgi
|
|
4
|
+
module Geometry
|
|
5
|
+
# Generated superclass for Parallelogram.
|
|
6
|
+
# @api private
|
|
7
|
+
ParallelogramBase = Element.lined(4)
|
|
8
|
+
private_constant :ParallelogramBase
|
|
9
|
+
|
|
10
|
+
# Closed four-sided element built from horizontal and vertical segments.
|
|
11
|
+
class Parallelogram < ParallelogramBase
|
|
12
|
+
# Builds a parallelogram from adjacent horizontal and vertical segments.
|
|
13
|
+
# @param horizontal [Sevgi::Geometry::Segment, Array<Numeric>] horizontal segment
|
|
14
|
+
# @param vertical [Sevgi::Geometry::Segment, Array<Numeric>] vertical segment
|
|
15
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
16
|
+
# @return [Sevgi::Geometry::Parallelogram]
|
|
17
|
+
# @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
|
|
18
|
+
def self.[](horizontal, vertical, position: Origin)
|
|
19
|
+
horizontal, vertical = Tuples[Segment, horizontal, vertical]
|
|
20
|
+
|
|
21
|
+
new_by_segments(horizontal, vertical.reverse, horizontal.reverse, vertical, position:)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Builds a parallelogram from a horizontal segment and tallness constraint.
|
|
25
|
+
# @param horizontal [Sevgi::Geometry::Segment, Array<Numeric>] horizontal segment
|
|
26
|
+
# @param tallness [Sevgi::Geometry::Polar, Array<Numeric>] target tallness as length and angle
|
|
27
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
28
|
+
# @return [Sevgi::Geometry::Parallelogram]
|
|
29
|
+
# @raise [Sevgi::Geometry::Error] when inputs cannot be coerced
|
|
30
|
+
def self.new_by_height(horizontal:, tallness:, position: Origin)
|
|
31
|
+
horizontal = Tuple[Segment, horizontal]
|
|
32
|
+
tallness = Tuple[Polar, tallness]
|
|
33
|
+
|
|
34
|
+
height = tallness.length - horizontal.y.abs
|
|
35
|
+
angle = tallness.angle
|
|
36
|
+
length = height / F.sin(angle)
|
|
37
|
+
|
|
38
|
+
self[horizontal, Segment[length, angle], position:]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Builds a parallelogram from a vertical segment and wideness constraint.
|
|
42
|
+
# @param vertical [Sevgi::Geometry::Segment, Array<Numeric>] vertical segment
|
|
43
|
+
# @param wideness [Sevgi::Geometry::Polar, Array<Numeric>] target wideness as length and angle
|
|
44
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
45
|
+
# @return [Sevgi::Geometry::Parallelogram]
|
|
46
|
+
# @raise [Sevgi::Geometry::Error] when inputs cannot be coerced
|
|
47
|
+
def self.new_by_width(vertical:, wideness:, position: Origin)
|
|
48
|
+
vertical = Tuple[Segment, vertical]
|
|
49
|
+
wideness = Tuple[Polar, wideness]
|
|
50
|
+
|
|
51
|
+
width = wideness.length - vertical.x.abs
|
|
52
|
+
angle = wideness.angle
|
|
53
|
+
length = width / F.cos(angle)
|
|
54
|
+
|
|
55
|
+
self[Segment[length, angle], vertical, position:]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Geometry
|
|
5
|
-
|
|
5
|
+
# Generated superclass for Polygon.
|
|
6
|
+
# @api private
|
|
7
|
+
PolygonBase = Element.lined
|
|
8
|
+
private_constant :PolygonBase
|
|
9
|
+
|
|
10
|
+
# Variable-size closed lined element.
|
|
11
|
+
class Polygon < PolygonBase
|
|
6
12
|
end
|
|
7
13
|
end
|
|
8
14
|
end
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Geometry
|
|
5
|
-
|
|
5
|
+
# Generated superclass for Polyline.
|
|
6
|
+
# @api private
|
|
7
|
+
PolylineBase = Element.lined(open: true)
|
|
8
|
+
private_constant :PolylineBase
|
|
9
|
+
|
|
10
|
+
# Variable-size open lined element.
|
|
11
|
+
class Polyline < PolylineBase
|
|
6
12
|
end
|
|
7
13
|
end
|
|
8
14
|
end
|
|
@@ -2,8 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Geometry
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
# Generated superclass for Rect.
|
|
6
|
+
# @api private
|
|
7
|
+
RectBase = Element.lined(4)
|
|
8
|
+
private_constant :RectBase
|
|
9
|
+
|
|
10
|
+
# Closed four-sided rectangle aligned to the screen axes.
|
|
11
|
+
class Rect < RectBase
|
|
12
|
+
# @overload [](width, height, position: Origin)
|
|
13
|
+
# Builds a rectangle from size and top-left position.
|
|
14
|
+
# @param width [Numeric] rectangle width
|
|
15
|
+
# @param height [Numeric] rectangle height
|
|
16
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
|
|
17
|
+
# @return [Sevgi::Geometry::Rect]
|
|
18
|
+
# @raise [Sevgi::Geometry::Error] when position cannot be coerced
|
|
19
|
+
def self.[](...) = from_size(...)
|
|
20
|
+
|
|
21
|
+
# @overload call(top_left, bottom_right)
|
|
22
|
+
# Builds a rectangle from two opposite corners.
|
|
23
|
+
# @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
|
|
24
|
+
# @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
|
|
25
|
+
# @return [Sevgi::Geometry::Rect]
|
|
26
|
+
# @raise [Sevgi::Geometry::Error] when either point cannot be coerced
|
|
27
|
+
def self.call(...) = from_corners(...)
|
|
28
|
+
|
|
29
|
+
# Builds a rectangle from two opposite corners.
|
|
30
|
+
# @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
|
|
31
|
+
# @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
|
|
32
|
+
# @return [Sevgi::Geometry::Rect]
|
|
33
|
+
# @raise [Sevgi::Geometry::Error] when either point cannot be coerced
|
|
34
|
+
def self.from_corners(top_left, bottom_right)
|
|
7
35
|
top_left, bottom_right = Tuples[Point, top_left, bottom_right]
|
|
8
36
|
width = (bottom_right.x - top_left.x).abs
|
|
9
37
|
|
|
@@ -15,35 +43,87 @@ module Sevgi
|
|
|
15
43
|
)
|
|
16
44
|
end
|
|
17
45
|
|
|
18
|
-
|
|
46
|
+
# Builds a rectangle from size and top-left position.
|
|
47
|
+
# @param width [Numeric] rectangle width
|
|
48
|
+
# @param height [Numeric] rectangle height
|
|
49
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
|
|
50
|
+
# @return [Sevgi::Geometry::Rect]
|
|
51
|
+
# @raise [Sevgi::Geometry::Error] when position cannot be coerced
|
|
52
|
+
def self.from_size(width, height, position: Origin)
|
|
19
53
|
new_by_segments(
|
|
20
|
-
Segment.
|
|
21
|
-
Segment.
|
|
22
|
-
Segment.
|
|
23
|
-
Segment.
|
|
54
|
+
Segment.rightward(width),
|
|
55
|
+
Segment.downward(height),
|
|
56
|
+
Segment.leftward(width),
|
|
57
|
+
Segment.upward(height),
|
|
24
58
|
position:
|
|
25
59
|
)
|
|
26
60
|
end
|
|
27
61
|
|
|
62
|
+
# Draws the rectangle into a graphics node.
|
|
63
|
+
# @param node [Object] graphics node receiving the drawing command
|
|
64
|
+
# @return [Object] graphics node command result
|
|
28
65
|
def draw!(node, **) = node.rect(x: position.x, y: position.y, width: width, height: height, **)
|
|
29
66
|
|
|
67
|
+
# Returns rectangle height.
|
|
68
|
+
# @return [Float]
|
|
30
69
|
def height = @height ||= segments[1].length
|
|
31
70
|
|
|
71
|
+
# Returns rectangle width.
|
|
72
|
+
# @return [Float]
|
|
32
73
|
def width = @width ||= segments[0].length
|
|
33
74
|
|
|
75
|
+
# @!parse
|
|
76
|
+
# # Returns the top-left corner.
|
|
77
|
+
# # @return [Sevgi::Geometry::Point]
|
|
78
|
+
# def top_left; end
|
|
79
|
+
#
|
|
80
|
+
# # Returns the top-right corner.
|
|
81
|
+
# # @return [Sevgi::Geometry::Point]
|
|
82
|
+
# def top_right; end
|
|
83
|
+
#
|
|
84
|
+
# # Returns the bottom-right corner.
|
|
85
|
+
# # @return [Sevgi::Geometry::Point]
|
|
86
|
+
# def bottom_right; end
|
|
87
|
+
#
|
|
88
|
+
# # Returns the bottom-left corner.
|
|
89
|
+
# # @return [Sevgi::Geometry::Point]
|
|
90
|
+
# def bottom_left; end
|
|
34
91
|
%i[top_left top_right bottom_right bottom_left].each_with_index do |corner, i|
|
|
35
92
|
define_method(corner) { points[i] }
|
|
36
93
|
end
|
|
37
94
|
|
|
95
|
+
# @!parse
|
|
96
|
+
# # Returns the top side line.
|
|
97
|
+
# # @return [Sevgi::Geometry::Line]
|
|
98
|
+
# def top; end
|
|
99
|
+
#
|
|
100
|
+
# # Returns the right side line.
|
|
101
|
+
# # @return [Sevgi::Geometry::Line]
|
|
102
|
+
# def right; end
|
|
103
|
+
#
|
|
104
|
+
# # Returns the bottom side line.
|
|
105
|
+
# # @return [Sevgi::Geometry::Line]
|
|
106
|
+
# def bottom; end
|
|
107
|
+
#
|
|
108
|
+
# # Returns the left side line.
|
|
109
|
+
# # @return [Sevgi::Geometry::Line]
|
|
110
|
+
# def left; end
|
|
38
111
|
%i[top right bottom left].each_with_index do |side, i|
|
|
39
112
|
define_method(side) { lines[i] }
|
|
40
113
|
end
|
|
41
114
|
end
|
|
42
115
|
|
|
116
|
+
# Rectangle with equal width and height.
|
|
43
117
|
class Square < Rect
|
|
118
|
+
# @return [Float] side length
|
|
44
119
|
alias length width
|
|
45
120
|
|
|
46
|
-
|
|
121
|
+
# Builds a square from side length and top-left position.
|
|
122
|
+
# @param length [Numeric] side length
|
|
123
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
|
|
124
|
+
# @return [Sevgi::Geometry::Square]
|
|
125
|
+
# @raise [Sevgi::Geometry::Error] when position cannot be coerced
|
|
126
|
+
def self.[](length, position: Origin) = from_size(length, length, position:)
|
|
47
127
|
end
|
|
48
128
|
end
|
|
49
129
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sevgi
|
|
4
|
+
module Geometry
|
|
5
|
+
# Generated superclass for Triangle.
|
|
6
|
+
# @api private
|
|
7
|
+
TriangleBase = Element.lined(3)
|
|
8
|
+
private_constant :TriangleBase
|
|
9
|
+
|
|
10
|
+
# Closed three-sided element built from two non-collinear adjacent segments.
|
|
11
|
+
class Triangle < TriangleBase
|
|
12
|
+
# Builds a triangle from two adjacent segments.
|
|
13
|
+
#
|
|
14
|
+
# The closing segment is the direct vector from the end of `segment_b`
|
|
15
|
+
# back to `position`. Segment order controls orientation; reversing the
|
|
16
|
+
# inputs returns the corresponding opposite orientation. Zero-length or
|
|
17
|
+
# collinear inputs are rejected using the current numeric precision.
|
|
18
|
+
# @param segment_a [Sevgi::Geometry::Segment, Array<Numeric>] first segment
|
|
19
|
+
# @param segment_b [Sevgi::Geometry::Segment, Array<Numeric>] second segment
|
|
20
|
+
# @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
|
|
21
|
+
# @return [Sevgi::Geometry::Triangle]
|
|
22
|
+
# @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced, or the segments are degenerate
|
|
23
|
+
def self.[](segment_a, segment_b, position: Origin)
|
|
24
|
+
a, b = Tuples[Segment, segment_a, segment_b]
|
|
25
|
+
|
|
26
|
+
validate!(a, b)
|
|
27
|
+
new_by_segments(a, b, closing_segment(a, b), position:)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.closing_segment(a, b)
|
|
31
|
+
Segment.(b.ending(a.ending(Origin)), Origin)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.cross(a, b) = (a.x * b.y) - (a.y * b.x)
|
|
35
|
+
|
|
36
|
+
def self.validate!(a, b)
|
|
37
|
+
if F.zero?(a.length) ||
|
|
38
|
+
F.zero?(b.length) ||
|
|
39
|
+
F.zero?(cross(a, b))
|
|
40
|
+
Error.("Triangle segments must form a non-degenerate triangle")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private_class_method :closing_segment, :cross, :validate!
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -3,31 +3,79 @@
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Geometry
|
|
5
5
|
class Equation
|
|
6
|
+
# Base class for linear equations.
|
|
6
7
|
class Linear < Equation
|
|
8
|
+
# Non-axis-aligned linear equation in `y = slope * x + intercept` form.
|
|
7
9
|
class Diagonal < Linear
|
|
10
|
+
# @!attribute [r] slope
|
|
11
|
+
# @return [Float] line slope
|
|
12
|
+
# @!attribute [r] intercept
|
|
13
|
+
# @return [Float] y-intercept
|
|
8
14
|
attr_reader :slope, :intercept
|
|
9
15
|
|
|
16
|
+
# Creates a diagonal linear equation.
|
|
17
|
+
# @param slope [Numeric] line slope
|
|
18
|
+
# @param intercept [Numeric] y-intercept
|
|
19
|
+
# @return [void]
|
|
20
|
+
# @raise [Sevgi::Geometry::Error] when a coefficient is not a finite Numeric
|
|
10
21
|
def initialize(slope:, intercept:)
|
|
11
22
|
super()
|
|
12
23
|
|
|
13
|
-
@slope = slope
|
|
14
|
-
@intercept = intercept
|
|
24
|
+
@slope = Real[:slope, slope]
|
|
25
|
+
@intercept = Real[:intercept, intercept]
|
|
15
26
|
end
|
|
16
27
|
|
|
28
|
+
# Returns an equation rounded to precision.
|
|
29
|
+
# @param precision [Integer, nil] decimal precision, or nil for the current function default
|
|
30
|
+
# @return [Sevgi::Geometry::Equation::Linear::Diagonal]
|
|
17
31
|
def approx(precision = nil)
|
|
18
32
|
self.class.new(slope: F.approx(slope, precision), intercept: F.approx(intercept, precision))
|
|
19
33
|
end
|
|
20
34
|
|
|
35
|
+
# Reports strict equation equality.
|
|
36
|
+
# @param other [Object] object to compare
|
|
37
|
+
# @return [Boolean]
|
|
21
38
|
def eql?(other) = self.class == other.class && [slope, intercept] == [other.slope, other.intercept]
|
|
22
39
|
|
|
40
|
+
# Returns a hash compatible with strict equality.
|
|
41
|
+
# @return [Integer]
|
|
23
42
|
def hash = [self.class, slope, intercept].hash
|
|
24
43
|
|
|
25
|
-
|
|
44
|
+
# Reports whether a point is on the left side of the line in screen coordinates.
|
|
45
|
+
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
46
|
+
# @return [Boolean]
|
|
47
|
+
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
48
|
+
def left?(point)
|
|
49
|
+
point = Tuple[Point, point]
|
|
26
50
|
|
|
27
|
-
|
|
51
|
+
F.gt?(point.y, y(point.x))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Reports whether a point is on the line.
|
|
55
|
+
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
56
|
+
# @return [Boolean]
|
|
57
|
+
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
58
|
+
def on?(point)
|
|
59
|
+
point = Tuple[Point, point]
|
|
60
|
+
|
|
61
|
+
F.eq?(point.y, y(point.x))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Reports whether a point is on the right side of the line in screen coordinates.
|
|
65
|
+
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
66
|
+
# @return [Boolean]
|
|
67
|
+
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
68
|
+
def right?(point)
|
|
69
|
+
point = Tuple[Point, point]
|
|
28
70
|
|
|
29
|
-
|
|
71
|
+
F.lt?(point.y, y(point.x))
|
|
72
|
+
end
|
|
30
73
|
|
|
74
|
+
# Returns a parallel equation shifted by a signed perpendicular offset.
|
|
75
|
+
# @param distance [Numeric, nil] signed perpendicular offset
|
|
76
|
+
# @param dx [Numeric, nil] explicit x translation
|
|
77
|
+
# @param dy [Numeric, nil] explicit y translation
|
|
78
|
+
# @return [Sevgi::Geometry::Equation::Linear::Diagonal]
|
|
31
79
|
def shift(distance = nil, dx: nil, dy: nil)
|
|
32
80
|
dx ||= 0.0
|
|
33
81
|
dy ||= 0.0
|
|
@@ -40,6 +88,8 @@ module Sevgi
|
|
|
40
88
|
Diagonal.new(slope:, intercept: intercept - (slope * dx) + dy)
|
|
41
89
|
end
|
|
42
90
|
|
|
91
|
+
# Formats the equation for display.
|
|
92
|
+
# @return [String]
|
|
43
93
|
def to_s
|
|
44
94
|
strings = []
|
|
45
95
|
|
|
@@ -49,41 +99,134 @@ module Sevgi
|
|
|
49
99
|
"Linear<y = #{strings.join(intercept.positive? ? " + " : " - ")}>"
|
|
50
100
|
end
|
|
51
101
|
|
|
102
|
+
# Evaluates x for a y coordinate.
|
|
103
|
+
# @param y [Numeric] y coordinate
|
|
104
|
+
# @return [Float]
|
|
52
105
|
def x(y) = (y - intercept) / slope
|
|
53
106
|
|
|
107
|
+
# Evaluates y for an x coordinate.
|
|
108
|
+
# @param x [Numeric] x coordinate
|
|
109
|
+
# @return [Float]
|
|
54
110
|
def y(x) = (slope * x) + intercept
|
|
55
111
|
|
|
56
112
|
alias == eql?
|
|
57
113
|
end
|
|
58
114
|
|
|
115
|
+
# Horizontal linear equation in `y = c` form.
|
|
59
116
|
class Horizontal < Diagonal
|
|
117
|
+
# Creates a horizontal equation.
|
|
118
|
+
# @param c [Numeric] y coordinate
|
|
119
|
+
# @return [void]
|
|
120
|
+
# @raise [Sevgi::Geometry::Error] when c is not a finite Numeric
|
|
60
121
|
def initialize(c) = super(slope: 0.0, intercept: c)
|
|
122
|
+
|
|
123
|
+
# Returns an equation rounded to precision.
|
|
124
|
+
# @param precision [Integer, nil] decimal precision, or nil for the current function default
|
|
125
|
+
# @return [Sevgi::Geometry::Equation::Linear::Horizontal]
|
|
126
|
+
def approx(precision = nil) = self.class.new(F.approx(intercept, precision))
|
|
127
|
+
|
|
128
|
+
# Returns a parallel horizontal equation shifted by offsets.
|
|
129
|
+
#
|
|
130
|
+
# A positive signed distance shifts upward in screen coordinates.
|
|
131
|
+
# @param distance [Numeric, nil] signed perpendicular offset
|
|
132
|
+
# @param dx [Numeric, nil] accepted for signature compatibility and ignored
|
|
133
|
+
# @param dy [Numeric, nil] explicit y translation
|
|
134
|
+
# @return [Sevgi::Geometry::Equation::Linear::Horizontal]
|
|
135
|
+
def shift(distance = nil, dx: nil, dy: nil)
|
|
136
|
+
_dx = dx
|
|
137
|
+
|
|
138
|
+
self.class.new(intercept + (dy || 0.0) - (distance || 0.0))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Formats the equation for display.
|
|
142
|
+
# @return [String]
|
|
143
|
+
def to_s = "Linear<y = #{F.approx(intercept)}>"
|
|
61
144
|
end
|
|
62
145
|
|
|
146
|
+
# Vertical linear equation in `x = c` form.
|
|
63
147
|
class Vertical < Linear
|
|
148
|
+
# Creates a vertical equation.
|
|
149
|
+
# @param c [Numeric] x coordinate
|
|
150
|
+
# @return [void]
|
|
151
|
+
# @raise [Sevgi::Geometry::Error] when c is not a finite Numeric
|
|
64
152
|
def initialize(c)
|
|
65
153
|
super()
|
|
66
154
|
|
|
67
|
-
@x = c
|
|
155
|
+
@x = Real[:x, c]
|
|
68
156
|
end
|
|
69
157
|
|
|
70
|
-
|
|
158
|
+
# Returns an equation rounded to precision.
|
|
159
|
+
# @param precision [Integer, nil] decimal precision, or nil for the current function default
|
|
160
|
+
# @return [Sevgi::Geometry::Equation::Linear::Vertical]
|
|
161
|
+
def approx(precision = nil) = self.class.new(F.approx(x, precision))
|
|
71
162
|
|
|
72
|
-
|
|
163
|
+
# Reports strict equation equality.
|
|
164
|
+
# @param other [Object] object to compare
|
|
165
|
+
# @return [Boolean]
|
|
166
|
+
def eql?(other) = self.class == other.class && x == other.x
|
|
73
167
|
|
|
74
|
-
|
|
168
|
+
# Returns a hash compatible with strict equality.
|
|
169
|
+
# @return [Integer]
|
|
170
|
+
def hash = [self.class, x].hash
|
|
75
171
|
|
|
172
|
+
# Reports whether a point is on the left side of the line.
|
|
173
|
+
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
174
|
+
# @return [Boolean]
|
|
175
|
+
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
176
|
+
def left?(point)
|
|
177
|
+
point = Tuple[Point, point]
|
|
178
|
+
|
|
179
|
+
F.lt?(point.x, x(point.y))
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Reports whether a point is on the line.
|
|
183
|
+
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
184
|
+
# @return [Boolean]
|
|
185
|
+
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
186
|
+
def on?(point)
|
|
187
|
+
point = Tuple[Point, point]
|
|
188
|
+
|
|
189
|
+
F.eq?(point.x, x(point.y))
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Reports whether a point is on the right side of the line.
|
|
193
|
+
# @param point [Sevgi::Geometry::Point, Array<Numeric>] point to test
|
|
194
|
+
# @return [Boolean]
|
|
195
|
+
# @raise [Sevgi::Geometry::Error] when point cannot be coerced
|
|
196
|
+
def right?(point)
|
|
197
|
+
point = Tuple[Point, point]
|
|
198
|
+
|
|
199
|
+
F.gt?(point.x, x(point.y))
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Returns a parallel vertical equation shifted by offsets.
|
|
203
|
+
#
|
|
204
|
+
# A positive signed distance shifts right in screen coordinates.
|
|
205
|
+
# @param distance [Numeric, nil] signed perpendicular offset
|
|
206
|
+
# @param dx [Numeric, nil] explicit x translation
|
|
207
|
+
# @param dy [Numeric, nil] accepted for signature compatibility and ignored
|
|
208
|
+
# @return [Sevgi::Geometry::Equation::Linear::Vertical]
|
|
76
209
|
def shift(distance = nil, dx: nil, dy: nil)
|
|
77
210
|
_dy = dy
|
|
78
211
|
|
|
79
212
|
self.class.new(x + (distance || 0.0) + (dx || 0.0))
|
|
80
213
|
end
|
|
81
214
|
|
|
215
|
+
# Formats the equation for display.
|
|
216
|
+
# @return [String]
|
|
82
217
|
def to_s = "Linear<x = #{F.approx(x)}>"
|
|
83
218
|
|
|
219
|
+
# Evaluates x for a y coordinate.
|
|
220
|
+
# @param _ [Numeric, nil] ignored y coordinate
|
|
221
|
+
# @return [Float]
|
|
84
222
|
def x(_ = nil) = @x
|
|
85
223
|
|
|
224
|
+
# Evaluates y for an x coordinate.
|
|
225
|
+
# @param _ [Numeric, nil] ignored x coordinate
|
|
226
|
+
# @return [Float] positive infinity because a vertical line has no single y
|
|
86
227
|
def y(_ = nil) = ::Float::INFINITY
|
|
228
|
+
|
|
229
|
+
alias == eql?
|
|
87
230
|
end
|
|
88
231
|
end
|
|
89
232
|
end
|
|
@@ -3,14 +3,23 @@
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Geometry
|
|
5
5
|
class Equation
|
|
6
|
+
# Base class for quadratic equations.
|
|
6
7
|
class Quadratic < Equation
|
|
7
8
|
end
|
|
8
9
|
end
|
|
9
10
|
|
|
11
|
+
# Reserved circle element support.
|
|
12
|
+
# @api private
|
|
10
13
|
class Circle
|
|
14
|
+
# Returns the circle equation.
|
|
15
|
+
# @abstract Circle equation support is not implemented yet.
|
|
16
|
+
# @return [Sevgi::Geometry::Equation::Quadratic]
|
|
17
|
+
# @raise [Sevgi::PanicError] until circle equation support is implemented
|
|
11
18
|
def equation
|
|
12
|
-
|
|
19
|
+
PanicError.("#{self.class}#equation must be implemented")
|
|
13
20
|
end
|
|
14
21
|
end
|
|
22
|
+
|
|
23
|
+
private_constant :Circle
|
|
15
24
|
end
|
|
16
25
|
end
|
|
@@ -2,13 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Geometry
|
|
5
|
+
# Base class for geometric equations that can intersect with each other.
|
|
5
6
|
class Equation
|
|
7
|
+
# Builds a non-axis-aligned linear equation.
|
|
8
|
+
# @param slope [Numeric] line slope
|
|
9
|
+
# @param intercept [Numeric] y-intercept
|
|
10
|
+
# @return [Sevgi::Geometry::Equation::Linear::Diagonal]
|
|
11
|
+
# @raise [Sevgi::Geometry::Error] when a coefficient is not a finite Numeric
|
|
6
12
|
def self.diagonal(slope:, intercept:) = Linear::Diagonal.new(slope:, intercept:)
|
|
7
13
|
|
|
14
|
+
# Builds a horizontal linear equation.
|
|
15
|
+
# @param const [Numeric] y coordinate
|
|
16
|
+
# @return [Sevgi::Geometry::Equation::Linear::Horizontal]
|
|
17
|
+
# @raise [Sevgi::Geometry::Error] when const is not a finite Numeric
|
|
8
18
|
def self.horizontal(const) = Linear::Horizontal.new(const)
|
|
9
19
|
|
|
20
|
+
# Builds a vertical linear equation.
|
|
21
|
+
# @param const [Numeric] x coordinate
|
|
22
|
+
# @return [Sevgi::Geometry::Equation::Linear::Vertical]
|
|
23
|
+
# @raise [Sevgi::Geometry::Error] when const is not a finite Numeric
|
|
10
24
|
def self.vertical(const) = Linear::Vertical.new(const)
|
|
11
25
|
|
|
26
|
+
# Intersects this equation with another equation.
|
|
27
|
+
# @param other [Sevgi::Geometry::Equation] equation to intersect with
|
|
28
|
+
# @return [Array<Sevgi::Geometry::Point>] intersection points
|
|
29
|
+
# @raise [Sevgi::Geometry::Error] when other is not an equation
|
|
30
|
+
# @raise [Sevgi::PanicError] when the equation combination is not implemented
|
|
12
31
|
def intersect(other)
|
|
13
32
|
Error.("Must be an equation: #{other}") unless other.is_a?(Equation)
|
|
14
33
|
|
|
@@ -20,41 +39,61 @@ module Sevgi
|
|
|
20
39
|
in [Quadratic, Quadratic]
|
|
21
40
|
quadratic_vs_quadratic(other)
|
|
22
41
|
else
|
|
23
|
-
|
|
42
|
+
PanicError.("Intersection not implemented: #{self.class} / #{other.class}")
|
|
24
43
|
end
|
|
25
44
|
|
|
26
45
|
Array(points)
|
|
27
46
|
end
|
|
28
47
|
|
|
29
|
-
|
|
48
|
+
# Evaluates y for an x coordinate.
|
|
49
|
+
# @abstract Subclasses implement equation-specific mapping.
|
|
50
|
+
# @param _x [Numeric] x coordinate
|
|
51
|
+
# @return [Float]
|
|
52
|
+
# @raise [Sevgi::PanicError] when a subclass does not implement y
|
|
53
|
+
def y(_x, ...) = PanicError.("#{self.class}#y must be implemented")
|
|
30
54
|
|
|
31
55
|
private
|
|
32
56
|
|
|
33
57
|
def linear_vs_linear(other)
|
|
34
58
|
case [self, other]
|
|
35
59
|
in [Linear::Diagonal, Linear::Diagonal]
|
|
36
|
-
|
|
60
|
+
diagonal_vs_diagonal(self, other)
|
|
37
61
|
in [Linear::Diagonal, Linear::Vertical]
|
|
38
|
-
|
|
62
|
+
diagonal_vs_vertical(self, other)
|
|
39
63
|
in [Linear::Vertical, Linear::Diagonal]
|
|
40
|
-
|
|
64
|
+
diagonal_vs_vertical(other, self)
|
|
41
65
|
in [Linear::Vertical, Linear::Vertical]
|
|
42
|
-
|
|
66
|
+
nil
|
|
43
67
|
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def diagonal_vs_diagonal(left, right)
|
|
71
|
+
return nil if F.eq?(left.slope, right.slope)
|
|
72
|
+
|
|
73
|
+
x = (right.intercept - left.intercept) / (left.slope - right.slope)
|
|
74
|
+
|
|
75
|
+
Point[x, left.y(x)]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def diagonal_vs_vertical(diagonal, vertical)
|
|
79
|
+
x = vertical.x
|
|
44
80
|
|
|
45
|
-
Point[x, y]
|
|
81
|
+
Point[x, diagonal.y(x)]
|
|
46
82
|
end
|
|
47
83
|
|
|
48
84
|
def linear_vs_quadratic(...)
|
|
49
|
-
|
|
85
|
+
PanicError.("Linear/quadratic intersection must be implemented")
|
|
50
86
|
end
|
|
51
87
|
|
|
52
88
|
def quadratic_vs_quadratic(...)
|
|
53
|
-
|
|
89
|
+
PanicError.("Quadratic/quadratic intersection must be implemented")
|
|
54
90
|
end
|
|
55
91
|
end
|
|
56
92
|
|
|
57
93
|
class Point
|
|
94
|
+
# Returns the linear equation passing through this point at an angle.
|
|
95
|
+
# @param angle [Numeric] clockwise angle in degrees
|
|
96
|
+
# @return [Sevgi::Geometry::Equation::Linear]
|
|
58
97
|
def equation(angle)
|
|
59
98
|
return Equation.horizontal(y) if F.zero?(angle % 180.0)
|
|
60
99
|
return Equation.vertical(x) if F.zero?(angle % 90.0)
|
|
@@ -64,6 +103,8 @@ module Sevgi
|
|
|
64
103
|
end
|
|
65
104
|
|
|
66
105
|
class Line
|
|
106
|
+
# Returns the linear equation containing this line.
|
|
107
|
+
# @return [Sevgi::Geometry::Equation::Linear]
|
|
67
108
|
def equation = position.equation(angle)
|
|
68
109
|
end
|
|
69
110
|
|