Class: Sevgi::Geometry::Rect

Inherits:
RectBase
  • Object
show all
Defined in:
lib/sevgi/geometry/elements/rect.rb

Overview

Closed four-sided rectangle aligned to the screen axes. Affine operations return Rect while the result remains axis-aligned and widen to Parallelogram after rotation or skew changes that category. A Square similarly widens to Rect after unequal scaling.

Examples:

Inspect corners, sides, and containment

rect = Sevgi::Geometry::Rect[8, 4, position: [2, 3]]
rect.top_left.deconstruct # => [2.0, 3.0]
rect.right.length         # => 4.0
rect.inside?([5, 5])      # => true

Observe semantic widening after affine transforms

Sevgi::Geometry::Rect[8, 4].rotate(30).class # => Sevgi::Geometry::Parallelogram
Sevgi::Geometry::Square[4].scale(2, 1).class # => Sevgi::Geometry::Rect

Direct Known Subclasses

Square

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#ASevgi::Geometry::Point (readonly)

Returns top-left vertex.

Returns:



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/sevgi/geometry/elements/rect.rb', line 40

class Rect < RectBase
  # @overload [](width, height, position: Origin)
  #   Builds a rectangle from size and top-left position.
  #   @param width [Numeric] rectangle width
  #   @param height [Numeric] rectangle height
  #   @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  #   @return [Sevgi::Geometry::Rect]
  #   @raise [Sevgi::Geometry::Error] when position cannot be coerced or a dimension is negative
  # @example Mathematical notation and English convenience are equivalent
  #   Sevgi::Geometry::Rect[3, 4] == Sevgi::Geometry::Rect.from_size(3, 4)
  def self.[](width, height, position: Origin) = construct(width, height, position:)

  # Constructs a rectangle for canonical size notation.
  # @param width [Numeric] rectangle width
  # @param height [Numeric] rectangle height
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when position or a dimension is invalid
  # @api private
  def self.construct(width, height, position:)
    width = dimension!(:width, width)
    height = dimension!(:height, height)

    new_by_segments(
      Segment.rightward(width),
      Segment.downward(height),
      Segment.leftward(width),
      Segment.upward(height),
      position:
    )
  end

  # @overload call(top_left, bottom_right)
  #   Builds a rectangle from two opposite corners.
  #   @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
  #   @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
  #   @return [Sevgi::Geometry::Rect]
  #   @raise [Sevgi::Geometry::Error] when either point cannot be coerced
  # @example Mathematical notation and English convenience are equivalent
  #   Sevgi::Geometry::Rect.([0, 0], [3, 4]) == Sevgi::Geometry::Rect.from_corners([0, 0], [3, 4])
  def self.call(top_left, bottom_right)
    top_left, bottom_right = Tuples[Point, top_left, bottom_right]
    left, right = [top_left.x, bottom_right.x].minmax
    top, bottom = [top_left.y, bottom_right.y].minmax
    width, height = right - left, bottom - top

    if self <= Square
      Error.("Square corners must define equal dimensions") unless F.eq?(width, height)

      return self[width, position: [left, top]]
    end

    self[width, height, position: [left, top]]
  end

  # Builds a rectangle from two opposite corners.
  # @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
  # @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when either point cannot be coerced
  def self.from_corners(top_left, bottom_right) = call(top_left, bottom_right)

  # Builds a rectangle from size and top-left position.
  # @param width [Numeric] rectangle width
  # @param height [Numeric] rectangle height
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when position cannot be coerced or a dimension is negative
  def self.from_size(width, height, position: Origin) = self[width, height, position:]

  class << self
    private

    def affine(*points)
      left, right, top, bottom = bounds(points)
      width, height = right - left, bottom - top

      unless axis_aligned?(points, left, right, top, bottom)
        return Parallelogram.call(*points.first(4))
      end

      klass = self <= Square && F.eq?(width, height) ? Square : Rect
      return klass[width, position: [left, top]] if klass == Square

      klass[width, height, position: [left, top]]
    end

    def approximate(*points)
      new_by_points!(*points)
    rescue Error
      return Rect.send(:new_by_points!, *points) if self <= Square

      super
    end

    def axis_aligned?(points, left, right, top, bottom)
      expected = [[left, top], [right, top], [right, bottom], [left, bottom]]
      vertices = points.first(4)

      expected.all? { |corner| vertices.any? { it.eq?(Point[*corner]) } }
    end

    def bounds(points)
      vertices = points.first(4)
      [vertices.map(&:x).min, vertices.map(&:x).max, vertices.map(&:y).min, vertices.map(&:y).max]
    end

    def dimension!(name, value)
      value = Real[name, value]
      Error.("Rectangle #{name} cannot be negative") if value.negative?

      value
    end
  end

  private_class_method :construct, :from_points, :from_segments

  # Draws the rectangle into a graphics node.
  # @param node [Object] graphics node receiving the drawing command
  # @return [Object] graphics node command result
  def draw!(node, **) = node.rect(x: position.x, y: position.y, width: width, height: height, **)

  private :draw!

  # Returns rectangle height.
  # @return [Float]
  def height = @height ||= segments[1].length

  # Returns rectangle width.
  # @return [Float]
  def width = @width ||= segments[0].length

  # @!parse
  #   # Returns the top-left corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def top_left; end
  #
  #   # Returns the top-right corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def top_right; end
  #
  #   # Returns the bottom-right corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def bottom_right; end
  #
  #   # Returns the bottom-left corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def bottom_left; end
  %i[top_left top_right bottom_right bottom_left].each_with_index do |corner, i|
    define_method(corner) { points[i] }
  end

  # @!parse
  #   # Returns the top side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def top; end
  #
  #   # Returns the right side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def right; end
  #
  #   # Returns the bottom side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def bottom; end
  #
  #   # Returns the left side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def left; end
  %i[top right bottom left].each_with_index do |side, i|
    define_method(side) { lines[i] }
  end

  private

  def validate_geometry!
    left, right, top, bottom = self.class.send(:bounds, points)
    expected = [[left, top], [right, top], [right, bottom], [left, bottom], [left, top]]
    valid = points.zip(expected).all? { |point, pair| point.eq?(Point[*pair]) }

    Error.("Rectangle points must form an axis-aligned rectangle") unless valid
  end
end

#ABSevgi::Geometry::Line (readonly)

Returns top side.

Returns:



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/sevgi/geometry/elements/rect.rb', line 40

class Rect < RectBase
  # @overload [](width, height, position: Origin)
  #   Builds a rectangle from size and top-left position.
  #   @param width [Numeric] rectangle width
  #   @param height [Numeric] rectangle height
  #   @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  #   @return [Sevgi::Geometry::Rect]
  #   @raise [Sevgi::Geometry::Error] when position cannot be coerced or a dimension is negative
  # @example Mathematical notation and English convenience are equivalent
  #   Sevgi::Geometry::Rect[3, 4] == Sevgi::Geometry::Rect.from_size(3, 4)
  def self.[](width, height, position: Origin) = construct(width, height, position:)

  # Constructs a rectangle for canonical size notation.
  # @param width [Numeric] rectangle width
  # @param height [Numeric] rectangle height
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when position or a dimension is invalid
  # @api private
  def self.construct(width, height, position:)
    width = dimension!(:width, width)
    height = dimension!(:height, height)

    new_by_segments(
      Segment.rightward(width),
      Segment.downward(height),
      Segment.leftward(width),
      Segment.upward(height),
      position:
    )
  end

  # @overload call(top_left, bottom_right)
  #   Builds a rectangle from two opposite corners.
  #   @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
  #   @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
  #   @return [Sevgi::Geometry::Rect]
  #   @raise [Sevgi::Geometry::Error] when either point cannot be coerced
  # @example Mathematical notation and English convenience are equivalent
  #   Sevgi::Geometry::Rect.([0, 0], [3, 4]) == Sevgi::Geometry::Rect.from_corners([0, 0], [3, 4])
  def self.call(top_left, bottom_right)
    top_left, bottom_right = Tuples[Point, top_left, bottom_right]
    left, right = [top_left.x, bottom_right.x].minmax
    top, bottom = [top_left.y, bottom_right.y].minmax
    width, height = right - left, bottom - top

    if self <= Square
      Error.("Square corners must define equal dimensions") unless F.eq?(width, height)

      return self[width, position: [left, top]]
    end

    self[width, height, position: [left, top]]
  end

  # Builds a rectangle from two opposite corners.
  # @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
  # @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when either point cannot be coerced
  def self.from_corners(top_left, bottom_right) = call(top_left, bottom_right)

  # Builds a rectangle from size and top-left position.
  # @param width [Numeric] rectangle width
  # @param height [Numeric] rectangle height
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when position cannot be coerced or a dimension is negative
  def self.from_size(width, height, position: Origin) = self[width, height, position:]

  class << self
    private

    def affine(*points)
      left, right, top, bottom = bounds(points)
      width, height = right - left, bottom - top

      unless axis_aligned?(points, left, right, top, bottom)
        return Parallelogram.call(*points.first(4))
      end

      klass = self <= Square && F.eq?(width, height) ? Square : Rect
      return klass[width, position: [left, top]] if klass == Square

      klass[width, height, position: [left, top]]
    end

    def approximate(*points)
      new_by_points!(*points)
    rescue Error
      return Rect.send(:new_by_points!, *points) if self <= Square

      super
    end

    def axis_aligned?(points, left, right, top, bottom)
      expected = [[left, top], [right, top], [right, bottom], [left, bottom]]
      vertices = points.first(4)

      expected.all? { |corner| vertices.any? { it.eq?(Point[*corner]) } }
    end

    def bounds(points)
      vertices = points.first(4)
      [vertices.map(&:x).min, vertices.map(&:x).max, vertices.map(&:y).min, vertices.map(&:y).max]
    end

    def dimension!(name, value)
      value = Real[name, value]
      Error.("Rectangle #{name} cannot be negative") if value.negative?

      value
    end
  end

  private_class_method :construct, :from_points, :from_segments

  # Draws the rectangle into a graphics node.
  # @param node [Object] graphics node receiving the drawing command
  # @return [Object] graphics node command result
  def draw!(node, **) = node.rect(x: position.x, y: position.y, width: width, height: height, **)

  private :draw!

  # Returns rectangle height.
  # @return [Float]
  def height = @height ||= segments[1].length

  # Returns rectangle width.
  # @return [Float]
  def width = @width ||= segments[0].length

  # @!parse
  #   # Returns the top-left corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def top_left; end
  #
  #   # Returns the top-right corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def top_right; end
  #
  #   # Returns the bottom-right corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def bottom_right; end
  #
  #   # Returns the bottom-left corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def bottom_left; end
  %i[top_left top_right bottom_right bottom_left].each_with_index do |corner, i|
    define_method(corner) { points[i] }
  end

  # @!parse
  #   # Returns the top side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def top; end
  #
  #   # Returns the right side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def right; end
  #
  #   # Returns the bottom side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def bottom; end
  #
  #   # Returns the left side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def left; end
  %i[top right bottom left].each_with_index do |side, i|
    define_method(side) { lines[i] }
  end

  private

  def validate_geometry!
    left, right, top, bottom = self.class.send(:bounds, points)
    expected = [[left, top], [right, top], [right, bottom], [left, bottom], [left, top]]
    valid = points.zip(expected).all? { |point, pair| point.eq?(Point[*pair]) }

    Error.("Rectangle points must form an axis-aligned rectangle") unless valid
  end
end

#BSevgi::Geometry::Point (readonly)

Returns top-right vertex.

Returns:



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/sevgi/geometry/elements/rect.rb', line 40

class Rect < RectBase
  # @overload [](width, height, position: Origin)
  #   Builds a rectangle from size and top-left position.
  #   @param width [Numeric] rectangle width
  #   @param height [Numeric] rectangle height
  #   @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  #   @return [Sevgi::Geometry::Rect]
  #   @raise [Sevgi::Geometry::Error] when position cannot be coerced or a dimension is negative
  # @example Mathematical notation and English convenience are equivalent
  #   Sevgi::Geometry::Rect[3, 4] == Sevgi::Geometry::Rect.from_size(3, 4)
  def self.[](width, height, position: Origin) = construct(width, height, position:)

  # Constructs a rectangle for canonical size notation.
  # @param width [Numeric] rectangle width
  # @param height [Numeric] rectangle height
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when position or a dimension is invalid
  # @api private
  def self.construct(width, height, position:)
    width = dimension!(:width, width)
    height = dimension!(:height, height)

    new_by_segments(
      Segment.rightward(width),
      Segment.downward(height),
      Segment.leftward(width),
      Segment.upward(height),
      position:
    )
  end

  # @overload call(top_left, bottom_right)
  #   Builds a rectangle from two opposite corners.
  #   @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
  #   @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
  #   @return [Sevgi::Geometry::Rect]
  #   @raise [Sevgi::Geometry::Error] when either point cannot be coerced
  # @example Mathematical notation and English convenience are equivalent
  #   Sevgi::Geometry::Rect.([0, 0], [3, 4]) == Sevgi::Geometry::Rect.from_corners([0, 0], [3, 4])
  def self.call(top_left, bottom_right)
    top_left, bottom_right = Tuples[Point, top_left, bottom_right]
    left, right = [top_left.x, bottom_right.x].minmax
    top, bottom = [top_left.y, bottom_right.y].minmax
    width, height = right - left, bottom - top

    if self <= Square
      Error.("Square corners must define equal dimensions") unless F.eq?(width, height)

      return self[width, position: [left, top]]
    end

    self[width, height, position: [left, top]]
  end

  # Builds a rectangle from two opposite corners.
  # @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
  # @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when either point cannot be coerced
  def self.from_corners(top_left, bottom_right) = call(top_left, bottom_right)

  # Builds a rectangle from size and top-left position.
  # @param width [Numeric] rectangle width
  # @param height [Numeric] rectangle height
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when position cannot be coerced or a dimension is negative
  def self.from_size(width, height, position: Origin) = self[width, height, position:]

  class << self
    private

    def affine(*points)
      left, right, top, bottom = bounds(points)
      width, height = right - left, bottom - top

      unless axis_aligned?(points, left, right, top, bottom)
        return Parallelogram.call(*points.first(4))
      end

      klass = self <= Square && F.eq?(width, height) ? Square : Rect
      return klass[width, position: [left, top]] if klass == Square

      klass[width, height, position: [left, top]]
    end

    def approximate(*points)
      new_by_points!(*points)
    rescue Error
      return Rect.send(:new_by_points!, *points) if self <= Square

      super
    end

    def axis_aligned?(points, left, right, top, bottom)
      expected = [[left, top], [right, top], [right, bottom], [left, bottom]]
      vertices = points.first(4)

      expected.all? { |corner| vertices.any? { it.eq?(Point[*corner]) } }
    end

    def bounds(points)
      vertices = points.first(4)
      [vertices.map(&:x).min, vertices.map(&:x).max, vertices.map(&:y).min, vertices.map(&:y).max]
    end

    def dimension!(name, value)
      value = Real[name, value]
      Error.("Rectangle #{name} cannot be negative") if value.negative?

      value
    end
  end

  private_class_method :construct, :from_points, :from_segments

  # Draws the rectangle into a graphics node.
  # @param node [Object] graphics node receiving the drawing command
  # @return [Object] graphics node command result
  def draw!(node, **) = node.rect(x: position.x, y: position.y, width: width, height: height, **)

  private :draw!

  # Returns rectangle height.
  # @return [Float]
  def height = @height ||= segments[1].length

  # Returns rectangle width.
  # @return [Float]
  def width = @width ||= segments[0].length

  # @!parse
  #   # Returns the top-left corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def top_left; end
  #
  #   # Returns the top-right corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def top_right; end
  #
  #   # Returns the bottom-right corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def bottom_right; end
  #
  #   # Returns the bottom-left corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def bottom_left; end
  %i[top_left top_right bottom_right bottom_left].each_with_index do |corner, i|
    define_method(corner) { points[i] }
  end

  # @!parse
  #   # Returns the top side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def top; end
  #
  #   # Returns the right side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def right; end
  #
  #   # Returns the bottom side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def bottom; end
  #
  #   # Returns the left side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def left; end
  %i[top right bottom left].each_with_index do |side, i|
    define_method(side) { lines[i] }
  end

  private

  def validate_geometry!
    left, right, top, bottom = self.class.send(:bounds, points)
    expected = [[left, top], [right, top], [right, bottom], [left, bottom], [left, top]]
    valid = points.zip(expected).all? { |point, pair| point.eq?(Point[*pair]) }

    Error.("Rectangle points must form an axis-aligned rectangle") unless valid
  end
end

#BCSevgi::Geometry::Line (readonly)

Returns right side.

Returns:



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/sevgi/geometry/elements/rect.rb', line 40

class Rect < RectBase
  # @overload [](width, height, position: Origin)
  #   Builds a rectangle from size and top-left position.
  #   @param width [Numeric] rectangle width
  #   @param height [Numeric] rectangle height
  #   @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  #   @return [Sevgi::Geometry::Rect]
  #   @raise [Sevgi::Geometry::Error] when position cannot be coerced or a dimension is negative
  # @example Mathematical notation and English convenience are equivalent
  #   Sevgi::Geometry::Rect[3, 4] == Sevgi::Geometry::Rect.from_size(3, 4)
  def self.[](width, height, position: Origin) = construct(width, height, position:)

  # Constructs a rectangle for canonical size notation.
  # @param width [Numeric] rectangle width
  # @param height [Numeric] rectangle height
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when position or a dimension is invalid
  # @api private
  def self.construct(width, height, position:)
    width = dimension!(:width, width)
    height = dimension!(:height, height)

    new_by_segments(
      Segment.rightward(width),
      Segment.downward(height),
      Segment.leftward(width),
      Segment.upward(height),
      position:
    )
  end

  # @overload call(top_left, bottom_right)
  #   Builds a rectangle from two opposite corners.
  #   @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
  #   @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
  #   @return [Sevgi::Geometry::Rect]
  #   @raise [Sevgi::Geometry::Error] when either point cannot be coerced
  # @example Mathematical notation and English convenience are equivalent
  #   Sevgi::Geometry::Rect.([0, 0], [3, 4]) == Sevgi::Geometry::Rect.from_corners([0, 0], [3, 4])
  def self.call(top_left, bottom_right)
    top_left, bottom_right = Tuples[Point, top_left, bottom_right]
    left, right = [top_left.x, bottom_right.x].minmax
    top, bottom = [top_left.y, bottom_right.y].minmax
    width, height = right - left, bottom - top

    if self <= Square
      Error.("Square corners must define equal dimensions") unless F.eq?(width, height)

      return self[width, position: [left, top]]
    end

    self[width, height, position: [left, top]]
  end

  # Builds a rectangle from two opposite corners.
  # @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
  # @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when either point cannot be coerced
  def self.from_corners(top_left, bottom_right) = call(top_left, bottom_right)

  # Builds a rectangle from size and top-left position.
  # @param width [Numeric] rectangle width
  # @param height [Numeric] rectangle height
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when position cannot be coerced or a dimension is negative
  def self.from_size(width, height, position: Origin) = self[width, height, position:]

  class << self
    private

    def affine(*points)
      left, right, top, bottom = bounds(points)
      width, height = right - left, bottom - top

      unless axis_aligned?(points, left, right, top, bottom)
        return Parallelogram.call(*points.first(4))
      end

      klass = self <= Square && F.eq?(width, height) ? Square : Rect
      return klass[width, position: [left, top]] if klass == Square

      klass[width, height, position: [left, top]]
    end

    def approximate(*points)
      new_by_points!(*points)
    rescue Error
      return Rect.send(:new_by_points!, *points) if self <= Square

      super
    end

    def axis_aligned?(points, left, right, top, bottom)
      expected = [[left, top], [right, top], [right, bottom], [left, bottom]]
      vertices = points.first(4)

      expected.all? { |corner| vertices.any? { it.eq?(Point[*corner]) } }
    end

    def bounds(points)
      vertices = points.first(4)
      [vertices.map(&:x).min, vertices.map(&:x).max, vertices.map(&:y).min, vertices.map(&:y).max]
    end

    def dimension!(name, value)
      value = Real[name, value]
      Error.("Rectangle #{name} cannot be negative") if value.negative?

      value
    end
  end

  private_class_method :construct, :from_points, :from_segments

  # Draws the rectangle into a graphics node.
  # @param node [Object] graphics node receiving the drawing command
  # @return [Object] graphics node command result
  def draw!(node, **) = node.rect(x: position.x, y: position.y, width: width, height: height, **)

  private :draw!

  # Returns rectangle height.
  # @return [Float]
  def height = @height ||= segments[1].length

  # Returns rectangle width.
  # @return [Float]
  def width = @width ||= segments[0].length

  # @!parse
  #   # Returns the top-left corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def top_left; end
  #
  #   # Returns the top-right corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def top_right; end
  #
  #   # Returns the bottom-right corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def bottom_right; end
  #
  #   # Returns the bottom-left corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def bottom_left; end
  %i[top_left top_right bottom_right bottom_left].each_with_index do |corner, i|
    define_method(corner) { points[i] }
  end

  # @!parse
  #   # Returns the top side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def top; end
  #
  #   # Returns the right side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def right; end
  #
  #   # Returns the bottom side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def bottom; end
  #
  #   # Returns the left side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def left; end
  %i[top right bottom left].each_with_index do |side, i|
    define_method(side) { lines[i] }
  end

  private

  def validate_geometry!
    left, right, top, bottom = self.class.send(:bounds, points)
    expected = [[left, top], [right, top], [right, bottom], [left, bottom], [left, top]]
    valid = points.zip(expected).all? { |point, pair| point.eq?(Point[*pair]) }

    Error.("Rectangle points must form an axis-aligned rectangle") unless valid
  end
end

#CSevgi::Geometry::Point (readonly)

Returns bottom-right vertex.

Returns:



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/sevgi/geometry/elements/rect.rb', line 40

class Rect < RectBase
  # @overload [](width, height, position: Origin)
  #   Builds a rectangle from size and top-left position.
  #   @param width [Numeric] rectangle width
  #   @param height [Numeric] rectangle height
  #   @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  #   @return [Sevgi::Geometry::Rect]
  #   @raise [Sevgi::Geometry::Error] when position cannot be coerced or a dimension is negative
  # @example Mathematical notation and English convenience are equivalent
  #   Sevgi::Geometry::Rect[3, 4] == Sevgi::Geometry::Rect.from_size(3, 4)
  def self.[](width, height, position: Origin) = construct(width, height, position:)

  # Constructs a rectangle for canonical size notation.
  # @param width [Numeric] rectangle width
  # @param height [Numeric] rectangle height
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when position or a dimension is invalid
  # @api private
  def self.construct(width, height, position:)
    width = dimension!(:width, width)
    height = dimension!(:height, height)

    new_by_segments(
      Segment.rightward(width),
      Segment.downward(height),
      Segment.leftward(width),
      Segment.upward(height),
      position:
    )
  end

  # @overload call(top_left, bottom_right)
  #   Builds a rectangle from two opposite corners.
  #   @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
  #   @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
  #   @return [Sevgi::Geometry::Rect]
  #   @raise [Sevgi::Geometry::Error] when either point cannot be coerced
  # @example Mathematical notation and English convenience are equivalent
  #   Sevgi::Geometry::Rect.([0, 0], [3, 4]) == Sevgi::Geometry::Rect.from_corners([0, 0], [3, 4])
  def self.call(top_left, bottom_right)
    top_left, bottom_right = Tuples[Point, top_left, bottom_right]
    left, right = [top_left.x, bottom_right.x].minmax
    top, bottom = [top_left.y, bottom_right.y].minmax
    width, height = right - left, bottom - top

    if self <= Square
      Error.("Square corners must define equal dimensions") unless F.eq?(width, height)

      return self[width, position: [left, top]]
    end

    self[width, height, position: [left, top]]
  end

  # Builds a rectangle from two opposite corners.
  # @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
  # @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when either point cannot be coerced
  def self.from_corners(top_left, bottom_right) = call(top_left, bottom_right)

  # Builds a rectangle from size and top-left position.
  # @param width [Numeric] rectangle width
  # @param height [Numeric] rectangle height
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when position cannot be coerced or a dimension is negative
  def self.from_size(width, height, position: Origin) = self[width, height, position:]

  class << self
    private

    def affine(*points)
      left, right, top, bottom = bounds(points)
      width, height = right - left, bottom - top

      unless axis_aligned?(points, left, right, top, bottom)
        return Parallelogram.call(*points.first(4))
      end

      klass = self <= Square && F.eq?(width, height) ? Square : Rect
      return klass[width, position: [left, top]] if klass == Square

      klass[width, height, position: [left, top]]
    end

    def approximate(*points)
      new_by_points!(*points)
    rescue Error
      return Rect.send(:new_by_points!, *points) if self <= Square

      super
    end

    def axis_aligned?(points, left, right, top, bottom)
      expected = [[left, top], [right, top], [right, bottom], [left, bottom]]
      vertices = points.first(4)

      expected.all? { |corner| vertices.any? { it.eq?(Point[*corner]) } }
    end

    def bounds(points)
      vertices = points.first(4)
      [vertices.map(&:x).min, vertices.map(&:x).max, vertices.map(&:y).min, vertices.map(&:y).max]
    end

    def dimension!(name, value)
      value = Real[name, value]
      Error.("Rectangle #{name} cannot be negative") if value.negative?

      value
    end
  end

  private_class_method :construct, :from_points, :from_segments

  # Draws the rectangle into a graphics node.
  # @param node [Object] graphics node receiving the drawing command
  # @return [Object] graphics node command result
  def draw!(node, **) = node.rect(x: position.x, y: position.y, width: width, height: height, **)

  private :draw!

  # Returns rectangle height.
  # @return [Float]
  def height = @height ||= segments[1].length

  # Returns rectangle width.
  # @return [Float]
  def width = @width ||= segments[0].length

  # @!parse
  #   # Returns the top-left corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def top_left; end
  #
  #   # Returns the top-right corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def top_right; end
  #
  #   # Returns the bottom-right corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def bottom_right; end
  #
  #   # Returns the bottom-left corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def bottom_left; end
  %i[top_left top_right bottom_right bottom_left].each_with_index do |corner, i|
    define_method(corner) { points[i] }
  end

  # @!parse
  #   # Returns the top side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def top; end
  #
  #   # Returns the right side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def right; end
  #
  #   # Returns the bottom side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def bottom; end
  #
  #   # Returns the left side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def left; end
  %i[top right bottom left].each_with_index do |side, i|
    define_method(side) { lines[i] }
  end

  private

  def validate_geometry!
    left, right, top, bottom = self.class.send(:bounds, points)
    expected = [[left, top], [right, top], [right, bottom], [left, bottom], [left, top]]
    valid = points.zip(expected).all? { |point, pair| point.eq?(Point[*pair]) }

    Error.("Rectangle points must form an axis-aligned rectangle") unless valid
  end
end

#CDSevgi::Geometry::Line (readonly)

Returns bottom side.

Returns:



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/sevgi/geometry/elements/rect.rb', line 40

class Rect < RectBase
  # @overload [](width, height, position: Origin)
  #   Builds a rectangle from size and top-left position.
  #   @param width [Numeric] rectangle width
  #   @param height [Numeric] rectangle height
  #   @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  #   @return [Sevgi::Geometry::Rect]
  #   @raise [Sevgi::Geometry::Error] when position cannot be coerced or a dimension is negative
  # @example Mathematical notation and English convenience are equivalent
  #   Sevgi::Geometry::Rect[3, 4] == Sevgi::Geometry::Rect.from_size(3, 4)
  def self.[](width, height, position: Origin) = construct(width, height, position:)

  # Constructs a rectangle for canonical size notation.
  # @param width [Numeric] rectangle width
  # @param height [Numeric] rectangle height
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when position or a dimension is invalid
  # @api private
  def self.construct(width, height, position:)
    width = dimension!(:width, width)
    height = dimension!(:height, height)

    new_by_segments(
      Segment.rightward(width),
      Segment.downward(height),
      Segment.leftward(width),
      Segment.upward(height),
      position:
    )
  end

  # @overload call(top_left, bottom_right)
  #   Builds a rectangle from two opposite corners.
  #   @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
  #   @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
  #   @return [Sevgi::Geometry::Rect]
  #   @raise [Sevgi::Geometry::Error] when either point cannot be coerced
  # @example Mathematical notation and English convenience are equivalent
  #   Sevgi::Geometry::Rect.([0, 0], [3, 4]) == Sevgi::Geometry::Rect.from_corners([0, 0], [3, 4])
  def self.call(top_left, bottom_right)
    top_left, bottom_right = Tuples[Point, top_left, bottom_right]
    left, right = [top_left.x, bottom_right.x].minmax
    top, bottom = [top_left.y, bottom_right.y].minmax
    width, height = right - left, bottom - top

    if self <= Square
      Error.("Square corners must define equal dimensions") unless F.eq?(width, height)

      return self[width, position: [left, top]]
    end

    self[width, height, position: [left, top]]
  end

  # Builds a rectangle from two opposite corners.
  # @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
  # @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when either point cannot be coerced
  def self.from_corners(top_left, bottom_right) = call(top_left, bottom_right)

  # Builds a rectangle from size and top-left position.
  # @param width [Numeric] rectangle width
  # @param height [Numeric] rectangle height
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when position cannot be coerced or a dimension is negative
  def self.from_size(width, height, position: Origin) = self[width, height, position:]

  class << self
    private

    def affine(*points)
      left, right, top, bottom = bounds(points)
      width, height = right - left, bottom - top

      unless axis_aligned?(points, left, right, top, bottom)
        return Parallelogram.call(*points.first(4))
      end

      klass = self <= Square && F.eq?(width, height) ? Square : Rect
      return klass[width, position: [left, top]] if klass == Square

      klass[width, height, position: [left, top]]
    end

    def approximate(*points)
      new_by_points!(*points)
    rescue Error
      return Rect.send(:new_by_points!, *points) if self <= Square

      super
    end

    def axis_aligned?(points, left, right, top, bottom)
      expected = [[left, top], [right, top], [right, bottom], [left, bottom]]
      vertices = points.first(4)

      expected.all? { |corner| vertices.any? { it.eq?(Point[*corner]) } }
    end

    def bounds(points)
      vertices = points.first(4)
      [vertices.map(&:x).min, vertices.map(&:x).max, vertices.map(&:y).min, vertices.map(&:y).max]
    end

    def dimension!(name, value)
      value = Real[name, value]
      Error.("Rectangle #{name} cannot be negative") if value.negative?

      value
    end
  end

  private_class_method :construct, :from_points, :from_segments

  # Draws the rectangle into a graphics node.
  # @param node [Object] graphics node receiving the drawing command
  # @return [Object] graphics node command result
  def draw!(node, **) = node.rect(x: position.x, y: position.y, width: width, height: height, **)

  private :draw!

  # Returns rectangle height.
  # @return [Float]
  def height = @height ||= segments[1].length

  # Returns rectangle width.
  # @return [Float]
  def width = @width ||= segments[0].length

  # @!parse
  #   # Returns the top-left corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def top_left; end
  #
  #   # Returns the top-right corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def top_right; end
  #
  #   # Returns the bottom-right corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def bottom_right; end
  #
  #   # Returns the bottom-left corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def bottom_left; end
  %i[top_left top_right bottom_right bottom_left].each_with_index do |corner, i|
    define_method(corner) { points[i] }
  end

  # @!parse
  #   # Returns the top side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def top; end
  #
  #   # Returns the right side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def right; end
  #
  #   # Returns the bottom side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def bottom; end
  #
  #   # Returns the left side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def left; end
  %i[top right bottom left].each_with_index do |side, i|
    define_method(side) { lines[i] }
  end

  private

  def validate_geometry!
    left, right, top, bottom = self.class.send(:bounds, points)
    expected = [[left, top], [right, top], [right, bottom], [left, bottom], [left, top]]
    valid = points.zip(expected).all? { |point, pair| point.eq?(Point[*pair]) }

    Error.("Rectangle points must form an axis-aligned rectangle") unless valid
  end
end

#DSevgi::Geometry::Point (readonly)

Returns bottom-left vertex.

Returns:



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/sevgi/geometry/elements/rect.rb', line 40

class Rect < RectBase
  # @overload [](width, height, position: Origin)
  #   Builds a rectangle from size and top-left position.
  #   @param width [Numeric] rectangle width
  #   @param height [Numeric] rectangle height
  #   @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  #   @return [Sevgi::Geometry::Rect]
  #   @raise [Sevgi::Geometry::Error] when position cannot be coerced or a dimension is negative
  # @example Mathematical notation and English convenience are equivalent
  #   Sevgi::Geometry::Rect[3, 4] == Sevgi::Geometry::Rect.from_size(3, 4)
  def self.[](width, height, position: Origin) = construct(width, height, position:)

  # Constructs a rectangle for canonical size notation.
  # @param width [Numeric] rectangle width
  # @param height [Numeric] rectangle height
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when position or a dimension is invalid
  # @api private
  def self.construct(width, height, position:)
    width = dimension!(:width, width)
    height = dimension!(:height, height)

    new_by_segments(
      Segment.rightward(width),
      Segment.downward(height),
      Segment.leftward(width),
      Segment.upward(height),
      position:
    )
  end

  # @overload call(top_left, bottom_right)
  #   Builds a rectangle from two opposite corners.
  #   @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
  #   @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
  #   @return [Sevgi::Geometry::Rect]
  #   @raise [Sevgi::Geometry::Error] when either point cannot be coerced
  # @example Mathematical notation and English convenience are equivalent
  #   Sevgi::Geometry::Rect.([0, 0], [3, 4]) == Sevgi::Geometry::Rect.from_corners([0, 0], [3, 4])
  def self.call(top_left, bottom_right)
    top_left, bottom_right = Tuples[Point, top_left, bottom_right]
    left, right = [top_left.x, bottom_right.x].minmax
    top, bottom = [top_left.y, bottom_right.y].minmax
    width, height = right - left, bottom - top

    if self <= Square
      Error.("Square corners must define equal dimensions") unless F.eq?(width, height)

      return self[width, position: [left, top]]
    end

    self[width, height, position: [left, top]]
  end

  # Builds a rectangle from two opposite corners.
  # @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
  # @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when either point cannot be coerced
  def self.from_corners(top_left, bottom_right) = call(top_left, bottom_right)

  # Builds a rectangle from size and top-left position.
  # @param width [Numeric] rectangle width
  # @param height [Numeric] rectangle height
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when position cannot be coerced or a dimension is negative
  def self.from_size(width, height, position: Origin) = self[width, height, position:]

  class << self
    private

    def affine(*points)
      left, right, top, bottom = bounds(points)
      width, height = right - left, bottom - top

      unless axis_aligned?(points, left, right, top, bottom)
        return Parallelogram.call(*points.first(4))
      end

      klass = self <= Square && F.eq?(width, height) ? Square : Rect
      return klass[width, position: [left, top]] if klass == Square

      klass[width, height, position: [left, top]]
    end

    def approximate(*points)
      new_by_points!(*points)
    rescue Error
      return Rect.send(:new_by_points!, *points) if self <= Square

      super
    end

    def axis_aligned?(points, left, right, top, bottom)
      expected = [[left, top], [right, top], [right, bottom], [left, bottom]]
      vertices = points.first(4)

      expected.all? { |corner| vertices.any? { it.eq?(Point[*corner]) } }
    end

    def bounds(points)
      vertices = points.first(4)
      [vertices.map(&:x).min, vertices.map(&:x).max, vertices.map(&:y).min, vertices.map(&:y).max]
    end

    def dimension!(name, value)
      value = Real[name, value]
      Error.("Rectangle #{name} cannot be negative") if value.negative?

      value
    end
  end

  private_class_method :construct, :from_points, :from_segments

  # Draws the rectangle into a graphics node.
  # @param node [Object] graphics node receiving the drawing command
  # @return [Object] graphics node command result
  def draw!(node, **) = node.rect(x: position.x, y: position.y, width: width, height: height, **)

  private :draw!

  # Returns rectangle height.
  # @return [Float]
  def height = @height ||= segments[1].length

  # Returns rectangle width.
  # @return [Float]
  def width = @width ||= segments[0].length

  # @!parse
  #   # Returns the top-left corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def top_left; end
  #
  #   # Returns the top-right corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def top_right; end
  #
  #   # Returns the bottom-right corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def bottom_right; end
  #
  #   # Returns the bottom-left corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def bottom_left; end
  %i[top_left top_right bottom_right bottom_left].each_with_index do |corner, i|
    define_method(corner) { points[i] }
  end

  # @!parse
  #   # Returns the top side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def top; end
  #
  #   # Returns the right side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def right; end
  #
  #   # Returns the bottom side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def bottom; end
  #
  #   # Returns the left side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def left; end
  %i[top right bottom left].each_with_index do |side, i|
    define_method(side) { lines[i] }
  end

  private

  def validate_geometry!
    left, right, top, bottom = self.class.send(:bounds, points)
    expected = [[left, top], [right, top], [right, bottom], [left, bottom], [left, top]]
    valid = points.zip(expected).all? { |point, pair| point.eq?(Point[*pair]) }

    Error.("Rectangle points must form an axis-aligned rectangle") unless valid
  end
end

#DASevgi::Geometry::Line (readonly)

Returns left side.

Returns:



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/sevgi/geometry/elements/rect.rb', line 40

class Rect < RectBase
  # @overload [](width, height, position: Origin)
  #   Builds a rectangle from size and top-left position.
  #   @param width [Numeric] rectangle width
  #   @param height [Numeric] rectangle height
  #   @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  #   @return [Sevgi::Geometry::Rect]
  #   @raise [Sevgi::Geometry::Error] when position cannot be coerced or a dimension is negative
  # @example Mathematical notation and English convenience are equivalent
  #   Sevgi::Geometry::Rect[3, 4] == Sevgi::Geometry::Rect.from_size(3, 4)
  def self.[](width, height, position: Origin) = construct(width, height, position:)

  # Constructs a rectangle for canonical size notation.
  # @param width [Numeric] rectangle width
  # @param height [Numeric] rectangle height
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when position or a dimension is invalid
  # @api private
  def self.construct(width, height, position:)
    width = dimension!(:width, width)
    height = dimension!(:height, height)

    new_by_segments(
      Segment.rightward(width),
      Segment.downward(height),
      Segment.leftward(width),
      Segment.upward(height),
      position:
    )
  end

  # @overload call(top_left, bottom_right)
  #   Builds a rectangle from two opposite corners.
  #   @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
  #   @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
  #   @return [Sevgi::Geometry::Rect]
  #   @raise [Sevgi::Geometry::Error] when either point cannot be coerced
  # @example Mathematical notation and English convenience are equivalent
  #   Sevgi::Geometry::Rect.([0, 0], [3, 4]) == Sevgi::Geometry::Rect.from_corners([0, 0], [3, 4])
  def self.call(top_left, bottom_right)
    top_left, bottom_right = Tuples[Point, top_left, bottom_right]
    left, right = [top_left.x, bottom_right.x].minmax
    top, bottom = [top_left.y, bottom_right.y].minmax
    width, height = right - left, bottom - top

    if self <= Square
      Error.("Square corners must define equal dimensions") unless F.eq?(width, height)

      return self[width, position: [left, top]]
    end

    self[width, height, position: [left, top]]
  end

  # Builds a rectangle from two opposite corners.
  # @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
  # @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when either point cannot be coerced
  def self.from_corners(top_left, bottom_right) = call(top_left, bottom_right)

  # Builds a rectangle from size and top-left position.
  # @param width [Numeric] rectangle width
  # @param height [Numeric] rectangle height
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when position cannot be coerced or a dimension is negative
  def self.from_size(width, height, position: Origin) = self[width, height, position:]

  class << self
    private

    def affine(*points)
      left, right, top, bottom = bounds(points)
      width, height = right - left, bottom - top

      unless axis_aligned?(points, left, right, top, bottom)
        return Parallelogram.call(*points.first(4))
      end

      klass = self <= Square && F.eq?(width, height) ? Square : Rect
      return klass[width, position: [left, top]] if klass == Square

      klass[width, height, position: [left, top]]
    end

    def approximate(*points)
      new_by_points!(*points)
    rescue Error
      return Rect.send(:new_by_points!, *points) if self <= Square

      super
    end

    def axis_aligned?(points, left, right, top, bottom)
      expected = [[left, top], [right, top], [right, bottom], [left, bottom]]
      vertices = points.first(4)

      expected.all? { |corner| vertices.any? { it.eq?(Point[*corner]) } }
    end

    def bounds(points)
      vertices = points.first(4)
      [vertices.map(&:x).min, vertices.map(&:x).max, vertices.map(&:y).min, vertices.map(&:y).max]
    end

    def dimension!(name, value)
      value = Real[name, value]
      Error.("Rectangle #{name} cannot be negative") if value.negative?

      value
    end
  end

  private_class_method :construct, :from_points, :from_segments

  # Draws the rectangle into a graphics node.
  # @param node [Object] graphics node receiving the drawing command
  # @return [Object] graphics node command result
  def draw!(node, **) = node.rect(x: position.x, y: position.y, width: width, height: height, **)

  private :draw!

  # Returns rectangle height.
  # @return [Float]
  def height = @height ||= segments[1].length

  # Returns rectangle width.
  # @return [Float]
  def width = @width ||= segments[0].length

  # @!parse
  #   # Returns the top-left corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def top_left; end
  #
  #   # Returns the top-right corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def top_right; end
  #
  #   # Returns the bottom-right corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def bottom_right; end
  #
  #   # Returns the bottom-left corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def bottom_left; end
  %i[top_left top_right bottom_right bottom_left].each_with_index do |corner, i|
    define_method(corner) { points[i] }
  end

  # @!parse
  #   # Returns the top side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def top; end
  #
  #   # Returns the right side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def right; end
  #
  #   # Returns the bottom side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def bottom; end
  #
  #   # Returns the left side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def left; end
  %i[top right bottom left].each_with_index do |side, i|
    define_method(side) { lines[i] }
  end

  private

  def validate_geometry!
    left, right, top, bottom = self.class.send(:bounds, points)
    expected = [[left, top], [right, top], [right, bottom], [left, bottom], [left, top]]
    valid = points.zip(expected).all? { |point, pair| point.eq?(Point[*pair]) }

    Error.("Rectangle points must form an axis-aligned rectangle") unless valid
  end
end

Class Method Details

.[](width, height, position: Origin) ⇒ Sevgi::Geometry::Rect

Examples:

Mathematical notation and English convenience are equivalent

Sevgi::Geometry::Rect[3, 4] == Sevgi::Geometry::Rect.from_size(3, 4)

Builds a rectangle from size and top-left position.

Parameters:

  • width (Numeric)

    rectangle width

  • height (Numeric)

    rectangle height

  • position (Sevgi::Geometry::Point, Array<Numeric>) (defaults to: Origin)

    top-left position

Returns:

Raises:



50
# File 'lib/sevgi/geometry/elements/rect.rb', line 50

def self.[](width, height, position: Origin) = construct(width, height, position:)

.call(top_left, bottom_right) ⇒ Sevgi::Geometry::Rect

Examples:

Mathematical notation and English convenience are equivalent

Sevgi::Geometry::Rect.([0, 0], [3, 4]) == Sevgi::Geometry::Rect.from_corners([0, 0], [3, 4])

Builds a rectangle from two opposite corners.

Parameters:

Returns:

Raises:



80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/sevgi/geometry/elements/rect.rb', line 80

def self.call(top_left, bottom_right)
  top_left, bottom_right = Tuples[Point, top_left, bottom_right]
  left, right = [top_left.x, bottom_right.x].minmax
  top, bottom = [top_left.y, bottom_right.y].minmax
  width, height = right - left, bottom - top

  if self <= Square
    Error.("Square corners must define equal dimensions") unless F.eq?(width, height)

    return self[width, position: [left, top]]
  end

  self[width, height, position: [left, top]]
end

.from_corners(top_left, bottom_right) ⇒ Sevgi::Geometry::Rect

Builds a rectangle from two opposite corners.

Parameters:

Returns:

Raises:



100
# File 'lib/sevgi/geometry/elements/rect.rb', line 100

def self.from_corners(top_left, bottom_right) = call(top_left, bottom_right)

.from_size(width, height, position: Origin) ⇒ Sevgi::Geometry::Rect

Builds a rectangle from size and top-left position.

Parameters:

  • width (Numeric)

    rectangle width

  • height (Numeric)

    rectangle height

  • position (Sevgi::Geometry::Point, Array<Numeric>) (defaults to: Origin)

    top-left position

Returns:

Raises:



108
# File 'lib/sevgi/geometry/elements/rect.rb', line 108

def self.from_size(width, height, position: Origin) = self[width, height, position:]

Instance Method Details

#bottomSevgi::Geometry::Line

Returns the bottom side line.



208
209
210
# File 'lib/sevgi/geometry/elements/rect.rb', line 208

%i[top right bottom left].each_with_index do |side, i|
  define_method(side) { lines[i] }
end

#bottom_leftSevgi::Geometry::Point

Returns the bottom-left corner.



188
189
190
# File 'lib/sevgi/geometry/elements/rect.rb', line 188

%i[top_left top_right bottom_right bottom_left].each_with_index do |corner, i|
  define_method(corner) { points[i] }
end

#bottom_rightSevgi::Geometry::Point

Returns the bottom-right corner.



188
189
190
# File 'lib/sevgi/geometry/elements/rect.rb', line 188

%i[top_left top_right bottom_right bottom_left].each_with_index do |corner, i|
  define_method(corner) { points[i] }
end

#heightFloat

Returns rectangle height.

Returns:

  • (Float)


166
# File 'lib/sevgi/geometry/elements/rect.rb', line 166

def height = @height ||= segments[1].length

#leftSevgi::Geometry::Line

Returns the left side line.



208
209
210
# File 'lib/sevgi/geometry/elements/rect.rb', line 208

%i[top right bottom left].each_with_index do |side, i|
  define_method(side) { lines[i] }
end

#perimeterFloat

Returns the closed path perimeter.

Returns:

  • (Float)


40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/sevgi/geometry/elements/rect.rb', line 40

class Rect < RectBase
  # @overload [](width, height, position: Origin)
  #   Builds a rectangle from size and top-left position.
  #   @param width [Numeric] rectangle width
  #   @param height [Numeric] rectangle height
  #   @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  #   @return [Sevgi::Geometry::Rect]
  #   @raise [Sevgi::Geometry::Error] when position cannot be coerced or a dimension is negative
  # @example Mathematical notation and English convenience are equivalent
  #   Sevgi::Geometry::Rect[3, 4] == Sevgi::Geometry::Rect.from_size(3, 4)
  def self.[](width, height, position: Origin) = construct(width, height, position:)

  # Constructs a rectangle for canonical size notation.
  # @param width [Numeric] rectangle width
  # @param height [Numeric] rectangle height
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when position or a dimension is invalid
  # @api private
  def self.construct(width, height, position:)
    width = dimension!(:width, width)
    height = dimension!(:height, height)

    new_by_segments(
      Segment.rightward(width),
      Segment.downward(height),
      Segment.leftward(width),
      Segment.upward(height),
      position:
    )
  end

  # @overload call(top_left, bottom_right)
  #   Builds a rectangle from two opposite corners.
  #   @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
  #   @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
  #   @return [Sevgi::Geometry::Rect]
  #   @raise [Sevgi::Geometry::Error] when either point cannot be coerced
  # @example Mathematical notation and English convenience are equivalent
  #   Sevgi::Geometry::Rect.([0, 0], [3, 4]) == Sevgi::Geometry::Rect.from_corners([0, 0], [3, 4])
  def self.call(top_left, bottom_right)
    top_left, bottom_right = Tuples[Point, top_left, bottom_right]
    left, right = [top_left.x, bottom_right.x].minmax
    top, bottom = [top_left.y, bottom_right.y].minmax
    width, height = right - left, bottom - top

    if self <= Square
      Error.("Square corners must define equal dimensions") unless F.eq?(width, height)

      return self[width, position: [left, top]]
    end

    self[width, height, position: [left, top]]
  end

  # Builds a rectangle from two opposite corners.
  # @param top_left [Sevgi::Geometry::Point, Array<Numeric>] top-left corner
  # @param bottom_right [Sevgi::Geometry::Point, Array<Numeric>] bottom-right corner
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when either point cannot be coerced
  def self.from_corners(top_left, bottom_right) = call(top_left, bottom_right)

  # Builds a rectangle from size and top-left position.
  # @param width [Numeric] rectangle width
  # @param height [Numeric] rectangle height
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] top-left position
  # @return [Sevgi::Geometry::Rect]
  # @raise [Sevgi::Geometry::Error] when position cannot be coerced or a dimension is negative
  def self.from_size(width, height, position: Origin) = self[width, height, position:]

  class << self
    private

    def affine(*points)
      left, right, top, bottom = bounds(points)
      width, height = right - left, bottom - top

      unless axis_aligned?(points, left, right, top, bottom)
        return Parallelogram.call(*points.first(4))
      end

      klass = self <= Square && F.eq?(width, height) ? Square : Rect
      return klass[width, position: [left, top]] if klass == Square

      klass[width, height, position: [left, top]]
    end

    def approximate(*points)
      new_by_points!(*points)
    rescue Error
      return Rect.send(:new_by_points!, *points) if self <= Square

      super
    end

    def axis_aligned?(points, left, right, top, bottom)
      expected = [[left, top], [right, top], [right, bottom], [left, bottom]]
      vertices = points.first(4)

      expected.all? { |corner| vertices.any? { it.eq?(Point[*corner]) } }
    end

    def bounds(points)
      vertices = points.first(4)
      [vertices.map(&:x).min, vertices.map(&:x).max, vertices.map(&:y).min, vertices.map(&:y).max]
    end

    def dimension!(name, value)
      value = Real[name, value]
      Error.("Rectangle #{name} cannot be negative") if value.negative?

      value
    end
  end

  private_class_method :construct, :from_points, :from_segments

  # Draws the rectangle into a graphics node.
  # @param node [Object] graphics node receiving the drawing command
  # @return [Object] graphics node command result
  def draw!(node, **) = node.rect(x: position.x, y: position.y, width: width, height: height, **)

  private :draw!

  # Returns rectangle height.
  # @return [Float]
  def height = @height ||= segments[1].length

  # Returns rectangle width.
  # @return [Float]
  def width = @width ||= segments[0].length

  # @!parse
  #   # Returns the top-left corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def top_left; end
  #
  #   # Returns the top-right corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def top_right; end
  #
  #   # Returns the bottom-right corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def bottom_right; end
  #
  #   # Returns the bottom-left corner.
  #   # @return [Sevgi::Geometry::Point]
  #   def bottom_left; end
  %i[top_left top_right bottom_right bottom_left].each_with_index do |corner, i|
    define_method(corner) { points[i] }
  end

  # @!parse
  #   # Returns the top side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def top; end
  #
  #   # Returns the right side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def right; end
  #
  #   # Returns the bottom side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def bottom; end
  #
  #   # Returns the left side line.
  #   # @return [Sevgi::Geometry::Line]
  #   def left; end
  %i[top right bottom left].each_with_index do |side, i|
    define_method(side) { lines[i] }
  end

  private

  def validate_geometry!
    left, right, top, bottom = self.class.send(:bounds, points)
    expected = [[left, top], [right, top], [right, bottom], [left, bottom], [left, top]]
    valid = points.zip(expected).all? { |point, pair| point.eq?(Point[*pair]) }

    Error.("Rectangle points must form an axis-aligned rectangle") unless valid
  end
end

#rightSevgi::Geometry::Line

Returns the right side line.



208
209
210
# File 'lib/sevgi/geometry/elements/rect.rb', line 208

%i[top right bottom left].each_with_index do |side, i|
  define_method(side) { lines[i] }
end

#topSevgi::Geometry::Line

Returns the top side line.



208
209
210
# File 'lib/sevgi/geometry/elements/rect.rb', line 208

%i[top right bottom left].each_with_index do |side, i|
  define_method(side) { lines[i] }
end

#top_leftSevgi::Geometry::Point

Returns the top-left corner.



188
189
190
# File 'lib/sevgi/geometry/elements/rect.rb', line 188

%i[top_left top_right bottom_right bottom_left].each_with_index do |corner, i|
  define_method(corner) { points[i] }
end

#top_rightSevgi::Geometry::Point

Returns the top-right corner.



188
189
190
# File 'lib/sevgi/geometry/elements/rect.rb', line 188

%i[top_left top_right bottom_right bottom_left].each_with_index do |corner, i|
  define_method(corner) { points[i] }
end

#widthFloat

Returns rectangle width.

Returns:

  • (Float)


170
# File 'lib/sevgi/geometry/elements/rect.rb', line 170

def width = @width ||= segments[0].length