sevgi-geometry 0.73.2 → 0.93.1

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,80 +2,177 @@
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
 
99
+ # Creates a point.
100
+ # @param x [Numeric] x coordinate
101
+ # @param y [Numeric] y coordinate
102
+ # @return [void]
48
103
  def initialize(x:, y:) = super(x: x.to_f, y: y.to_f)
49
104
 
105
+ # Compares points by x, then y.
106
+ # @param other [Sevgi::Geometry::Point, Array<Numeric>] point to compare
107
+ # @return [Integer, nil]
108
+ # @raise [Sevgi::Geometry::Error] when other cannot be coerced
50
109
  def <=>(other) = (other = Tuple[Point, other]).nan? || nan? ? nil : deconstruct <=> other.deconstruct
51
110
 
111
+ # Reports whether this point is at or above another point in screen coordinates.
112
+ # @param other [Sevgi::Geometry::Point, Array<Numeric>] point to compare
113
+ # @return [Boolean]
114
+ # @raise [Sevgi::Geometry::Error] when other cannot be coerced
52
115
  def above?(other) = y <= Tuple[Point, other].y
53
116
 
117
+ # Returns a point rounded to precision.
118
+ # @param precision [Integer, nil] decimal precision, or nil for the current function default
119
+ # @return [Sevgi::Geometry::Point]
54
120
  def approx(precision = nil) = with(x: F.approx(x, precision), y: F.approx(y, precision))
55
121
 
122
+ # Reports whether this point is at or below another point in screen coordinates.
123
+ # @param other [Sevgi::Geometry::Point, Array<Numeric>] point to compare
124
+ # @return [Boolean]
125
+ # @raise [Sevgi::Geometry::Error] when other cannot be coerced
56
126
  def below?(other) = y >= Tuple[Point, other].y
57
127
 
128
+ # Compares this point with optional numeric precision.
129
+ # @param other [Sevgi::Geometry::Point, Array<Numeric>] point to compare
130
+ # @param precision [Integer, nil] decimal precision, or nil for the current function default
131
+ # @return [Boolean]
132
+ # @raise [Sevgi::Geometry::Error] when other cannot be coerced
58
133
  def eq?(other, precision: nil) = self.class.eq?(self, other, precision:)
59
134
 
135
+ # Reports strict point equality.
136
+ # @param other [Object] object to compare
137
+ # @return [Boolean]
60
138
  def eql?(other) = self.class == other.class && deconstruct == other.deconstruct
61
139
 
140
+ # Returns a hash compatible with strict equality.
141
+ # @return [Integer]
62
142
  def hash = [self.class, *deconstruct].hash
63
143
 
144
+ # Reports whether any coordinate is infinite.
145
+ # @return [Boolean]
64
146
  def infinite? = deconstruct.any?(&:infinite?)
65
147
 
148
+ # Reports whether this point is at or left of another point.
149
+ # @param other [Sevgi::Geometry::Point, Array<Numeric>] point to compare
150
+ # @return [Boolean]
151
+ # @raise [Sevgi::Geometry::Error] when other cannot be coerced
66
152
  def left?(other) = x <= Tuple[Point, other].x
67
153
 
154
+ # Reports whether any coordinate is NaN.
155
+ # @return [Boolean]
68
156
  def nan? = deconstruct.any?(&:nan?)
69
157
 
158
+ # Reports whether this point is at or right of another point.
159
+ # @param other [Sevgi::Geometry::Point, Array<Numeric>] point to compare
160
+ # @return [Boolean]
161
+ # @raise [Sevgi::Geometry::Error] when other cannot be coerced
70
162
  def right?(other) = x >= Tuple[Point, other].x
71
163
 
164
+ # Formats point coordinates for SVG coordinate-list attributes.
165
+ # @return [String]
72
166
  def to_cs = "#{F.approx(x)},#{F.approx(y)}"
73
167
 
168
+ # Formats the point for display.
169
+ # @return [String]
74
170
  def to_s = "(#{to_cs})"
75
171
 
76
172
  alias_method :==, :eql?
77
173
  end
78
174
 
175
+ # Origin point in SVG/screen coordinates.
79
176
  Origin = Point.origin
80
177
  end
81
178
  end
@@ -2,59 +2,133 @@
2
2
 
3
3
  module Sevgi
4
4
  module Geometry
5
+ # Immutable polar segment in SVG/screen coordinates.
6
+ #
7
+ # `length` is a distance and `angle` is a clockwise angle in degrees.
8
+ # Use `Segment[length, angle]` to create a segment from polar components.
5
9
  Segment = Data.define(:length, :angle) do
6
10
  include Comparable
7
11
 
12
+ # @!attribute [r] length
13
+ # @return [Float] segment length
14
+ # @!attribute [r] angle
15
+ # @return [Float] clockwise angle in degrees
16
+
17
+ # Creates a segment from start and end points.
18
+ # @param starting [Sevgi::Geometry::Point, Array<Numeric>] start point
19
+ # @param ending [Sevgi::Geometry::Point, Array<Numeric>] end point
20
+ # @return [Sevgi::Geometry::Segment]
21
+ # @raise [Sevgi::Geometry::Error] when either point cannot be coerced
8
22
  def self.call(starting, ending)
9
23
  starting, ending = Tuples[Point, starting, ending]
10
24
  self[Point.length(starting, ending), Point.angle(starting, ending)]
11
25
  end
12
26
 
27
+ # Compares two segments with optional numeric precision.
28
+ # @param this [Sevgi::Geometry::Segment, Array<Numeric>] first segment
29
+ # @param that [Sevgi::Geometry::Segment, Array<Numeric>] second segment
30
+ # @param precision [Integer, nil] decimal precision, or nil for the current function default
31
+ # @return [Boolean]
32
+ # @raise [Sevgi::Geometry::Error] when either segment cannot be coerced
13
33
  def self.eq?(this, that, precision: nil)
14
34
  this, that = Tuples[self, this, that]
15
35
  F.eq?(this.length, that.length, precision:) && F.eq?(this.angle, that.angle, precision:)
16
36
  end
17
37
 
18
- def self.horizontal!(length) = self[length, 180.0]
19
- def self.horizontal(length) = self[length, 0.0]
20
- def self.vertical!(length) = self[length, -90.0]
21
- def self.vertical(length) = self[length, 90.0]
38
+ # Returns a downward segment.
39
+ # @param length [Numeric] segment length
40
+ # @return [Sevgi::Geometry::Segment]
41
+ def self.downward(length) = self[length, 90.0]
42
+
43
+ # Returns a leftward segment.
44
+ # @param length [Numeric] segment length
45
+ # @return [Sevgi::Geometry::Segment]
46
+ def self.leftward(length) = self[length, 180.0]
47
+
48
+ # Returns a rightward segment.
49
+ # @param length [Numeric] segment length
50
+ # @return [Sevgi::Geometry::Segment]
51
+ def self.rightward(length) = self[length, 0.0]
52
+
53
+ # Returns an upward segment.
54
+ # @param length [Numeric] segment length
55
+ # @return [Sevgi::Geometry::Segment]
56
+ def self.upward(length) = self[length, -90.0]
57
+
58
+ class << self
59
+ # @return [Sevgi::Geometry::Segment]
60
+ alias_method :horizontal, :rightward
61
+ # @return [Sevgi::Geometry::Segment]
62
+ alias_method :vertical, :downward
63
+ end
22
64
 
65
+ # Creates a segment.
66
+ # @param length [Numeric] segment length
67
+ # @param angle [Numeric] clockwise angle in degrees
68
+ # @return [void]
23
69
  def initialize(length:, angle:) = super(length: length.to_f, angle: angle.to_f)
24
70
 
71
+ # Compares segments by length.
72
+ # @param other [Sevgi::Geometry::Segment, Array<Numeric>] segment to compare
73
+ # @return [Integer, nil]
74
+ # @raise [Sevgi::Geometry::Error] when other cannot be coerced
25
75
  def <=>(other) = (other = Tuple[Segment, other]).nan? || nan? ? nil : length <=> other.length
26
76
 
77
+ # Returns a segment rounded to precision.
78
+ # @param precision [Integer, nil] decimal precision, or nil for the current function default
79
+ # @return [Sevgi::Geometry::Segment]
27
80
  def approx(precision = nil) = with(length: F.approx(length, precision), angle: F.approx(angle, precision))
28
81
 
29
- def com = angle - 90.0
30
-
82
+ # Returns the endpoint reached from a starting point.
83
+ # @param starting [Sevgi::Geometry::Point, Array<Numeric>] start point
84
+ # @return [Sevgi::Geometry::Point]
85
+ # @raise [Sevgi::Geometry::Error] when starting cannot be coerced
31
86
  def ending(starting) = Tuple[Point, starting].translate(x, y)
32
87
 
88
+ # Compares this segment with optional numeric precision.
89
+ # @param other [Sevgi::Geometry::Segment, Array<Numeric>] segment to compare
90
+ # @param precision [Integer, nil] decimal precision, or nil for the current function default
91
+ # @return [Boolean]
92
+ # @raise [Sevgi::Geometry::Error] when other cannot be coerced
33
93
  def eq?(other, precision: nil) = self.class.eq?(self, other, precision:)
34
94
 
95
+ # Reports strict segment equality.
96
+ # @param other [Object] object to compare
97
+ # @return [Boolean]
35
98
  def eql?(other) = self.class == other.class && deconstruct == other.deconstruct
36
99
 
100
+ # Returns a hash compatible with strict equality.
101
+ # @return [Integer]
37
102
  def hash = [self.class, *deconstruct].hash
38
103
 
104
+ # Reports whether any component is infinite.
105
+ # @return [Boolean]
39
106
  def infinite? = deconstruct.any?(&:infinite?)
40
107
 
108
+ # Converts the segment into a line at a point.
109
+ # @param point [Sevgi::Geometry::Point, Array<Numeric>] line start point
110
+ # @return [Sevgi::Geometry::Line]
111
+ # @raise [Sevgi::Geometry::Error] when point cannot be coerced
41
112
  def line(point = Origin) = Line[length, angle, position: Tuple[Point, point]]
42
113
 
43
- def lx = x.abs
44
-
45
- def ly = y.abs
46
-
114
+ # Reports whether any component is NaN.
115
+ # @return [Boolean]
47
116
  def nan? = deconstruct.any?(&:nan?)
48
117
 
118
+ # Returns the opposite segment.
119
+ # @return [Sevgi::Geometry::Segment]
49
120
  def reverse = with(angle: angle + 180.0)
50
121
 
51
- def sup = angle - 180.0
52
-
122
+ # Returns the x component of the segment.
123
+ # @return [Float]
53
124
  def x = length * F.cos(angle)
54
125
 
126
+ # Returns the y component of the segment.
127
+ # @return [Float]
55
128
  def y = length * F.sin(angle)
56
129
  end
57
130
 
58
- LengthAngle = Data.define(:length, :angle)
131
+ # Lightweight polar value used where a plain length/angle tuple is enough.
132
+ Polar = Data.define(:length, :angle)
59
133
  end
60
134
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Sevgi
4
4
  module Geometry
5
- VERSION = "0.73.2"
5
+ # Component version.
6
+ VERSION = "0.93.1"
6
7
  end
7
8
  end
@@ -12,3 +12,12 @@ require_relative "geometry/equation"
12
12
  require_relative "geometry/operation"
13
13
 
14
14
  require_relative "geometry/version"
15
+
16
+ module Sevgi
17
+ # Screen-space geometry primitives used by Sevgi layout and drawing helpers.
18
+ #
19
+ # Coordinates follow SVG screen conventions: +x points right, +y points down,
20
+ # and positive angles turn clockwise.
21
+ module Geometry
22
+ end
23
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sevgi-geometry
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.73.2
4
+ version: 0.93.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Recai Oktaş
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.73.2
18
+ version: 0.93.1
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.73.2
25
+ version: 0.93.1
26
26
  description: Enhances the Sevgi toolkit with geometry objects and methods.
27
27
  email: roktas@gmail.com
28
28
  executables: []
@@ -36,11 +36,11 @@ files:
36
36
  - lib/sevgi/geometry/elements/curve.rb
37
37
  - lib/sevgi/geometry/elements/ellipse.rb
38
38
  - lib/sevgi/geometry/elements/line.rb
39
- - lib/sevgi/geometry/elements/parm.rb
39
+ - lib/sevgi/geometry/elements/parallelogram.rb
40
40
  - lib/sevgi/geometry/elements/polygon.rb
41
41
  - lib/sevgi/geometry/elements/polyline.rb
42
42
  - lib/sevgi/geometry/elements/rect.rb
43
- - lib/sevgi/geometry/elements/tri.rb
43
+ - lib/sevgi/geometry/elements/triangle.rb
44
44
  - lib/sevgi/geometry/equation.rb
45
45
  - lib/sevgi/geometry/equation/linear.rb
46
46
  - lib/sevgi/geometry/equation/quadratic.rb
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Parallelogram
4
-
5
- module Sevgi
6
- module Geometry
7
- class Parm < Element.lined(4)
8
- def self.[](horizontal, vertical, position: Origin)
9
- horizontal, vertical = Tuples[Segment, horizontal, vertical]
10
-
11
- new_by_segments(horizontal, vertical.reverse, horizontal.reverse, vertical, position:)
12
- end
13
-
14
- def self.new_by_height(horizontal:, tallness:, position: Origin)
15
- horizontal = Tuple[Segment, horizontal]
16
- tallness = Tuple[LengthAngle, tallness]
17
-
18
- height = tallness.length - horizontal.ly
19
- angle = tallness.angle
20
- length = height / F.sin(angle)
21
-
22
- self[horizontal, Segment[length, angle], position:]
23
- end
24
-
25
- def self.new_by_width(vertical:, wideness:, position: Origin)
26
- vertical = Tuple[Segment, vertical]
27
- wideness = Tuple[LengthAngle, wideness]
28
-
29
- width = wideness.length - vertical.lx
30
- angle = wideness.angle
31
- length = width / F.cos(angle)
32
-
33
- self[Segment[length, angle], vertical, position:]
34
- end
35
- end
36
- end
37
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sevgi
4
- module Geometry
5
- class Tri < Element.lined(3)
6
- def self.[](segment_a, segment_b, position: Origin)
7
- a, b = Tuples[Segment, segment_a, segment_b]
8
-
9
- length = ::Math.sqrt((a.length ** 2) + (b.length ** 2) - (2 * a.length * b.length * F.cos(b.sup - a.angle)))
10
- angle = b.sup + F.asin(a.length * F.sin(b.sup - a.angle) / length)
11
-
12
- c = Segment[length, angle]
13
-
14
- new_by_segments(a, b, c, position:)
15
- end
16
- end
17
- end
18
- end