Class: Sevgi::Geometry::Parallelogram

Inherits:
ParallelogramBase
  • Object
show all
Defined in:
lib/sevgi/geometry/elements/parallelogram.rb

Overview

Closed four-sided element whose opposite sides are equal and parallel. Every construction path rejects degenerate or unrelated side pairs; affine operations preserve the class while that invariant holds.

Examples:

Pair mathematical notation with English conveniences

Sevgi::Geometry::Parallelogram[[2, 0], [2, -90]] ==
  Sevgi::Geometry::Parallelogram.from_segments([2, 0], [2, -90])
points = [[0, 0], [2, 0], [2, 2], [0, 2]]
Sevgi::Geometry::Parallelogram.(*points) == Sevgi::Geometry::Parallelogram.from_points(*points)

Compare vertices with the axis-aligned bounding box

shape = Sevgi::Geometry::Parallelogram.([1, 1], [5, 1], [7, 4], [3, 4])
shape.C.deconstruct # => [7.0, 4.0]
shape.box.width     # => 6.0
shape.box.height    # => 3.0

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#ASevgi::Geometry::Point (readonly)

Returns first vertex.

Returns:



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
# File 'lib/sevgi/geometry/elements/parallelogram.rb', line 58

class Parallelogram < ParallelogramBase
  # Builds a parallelogram from adjacent base and side segments. Both segments originate at `position`; `base`
  # defines AB and `side` defines AD, regardless of their angles.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
  def self.[](base, side, position: Origin)
    base, side = Tuples[Segment, base, side]

    new_by_segments(base, side.reverse, base.reverse, side, position:)
  end

  # Builds a parallelogram from a base and bounding-height constraint. The constraint length is the target height;
  # its signed angle is retained as the direction of the derived side while the component magnitude determines that
  # side's non-negative length.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target height and side direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the height constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_height(base: [4, 0], constraint: [3, -90])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 3, angle: 90)
  #   base = Sevgi::Geometry::Segment[4, 0]
  #   Sevgi::Geometry::Parallelogram.new_by_height(base:, constraint:)
  def self.new_by_height(base:, constraint:, position: Origin)
    base = Tuple[Segment, base]
    constraint = Tuple[LengthAngle, constraint]

    height = constraint.length - base.y.abs
    angle = constraint.angle
    sine = F.sin(angle)
    Error.("Parallelogram height is smaller than its base span") if height.negative?
    Error.("Parallelogram height constraint must have a vertical component") if F.zero?(sine)

    self[base, Segment[height / sine.abs, angle], position:]
  end

  # Builds a parallelogram from a side and bounding-width constraint. The constraint length is the target width; its
  # signed angle is retained as the direction of the derived base while the component magnitude determines that
  # base's non-negative length.
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target width and base direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the width constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_width(side: [3, 90], constraint: [4, 180])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 4, angle: 0)
  #   side = Sevgi::Geometry::Segment[3, 90]
  #   Sevgi::Geometry::Parallelogram.new_by_width(side:, constraint:)
  def self.new_by_width(side:, constraint:, position: Origin)
    side = Tuple[Segment, side]
    constraint = Tuple[LengthAngle, constraint]

    width = constraint.length - side.x.abs
    angle = constraint.angle
    cosine = F.cos(angle)
    Error.("Parallelogram width is smaller than its side span") if width.negative?
    Error.("Parallelogram width constraint must have a horizontal component") if F.zero?(cosine)

    self[Segment[width / cosine.abs, angle], side, position:]
  end

  private

  def validate_geometry!
    a, b, c, d = segments
    valid = opposite?(a, c) && opposite?(b, d) && !F.zero?(cross(a, b))

    Error.("Parallelogram sides must be non-degenerate opposite pairs") unless valid
  end

  def cross(a, b) = (a.x * b.y) - (a.y * b.x)

  def opposite?(a, b) = F.zero?(a.x + b.x) && F.zero?(a.y + b.y)
end

#ABSevgi::Geometry::Line (readonly)

Returns side from A to B.

Returns:



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
# File 'lib/sevgi/geometry/elements/parallelogram.rb', line 58

class Parallelogram < ParallelogramBase
  # Builds a parallelogram from adjacent base and side segments. Both segments originate at `position`; `base`
  # defines AB and `side` defines AD, regardless of their angles.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
  def self.[](base, side, position: Origin)
    base, side = Tuples[Segment, base, side]

    new_by_segments(base, side.reverse, base.reverse, side, position:)
  end

  # Builds a parallelogram from a base and bounding-height constraint. The constraint length is the target height;
  # its signed angle is retained as the direction of the derived side while the component magnitude determines that
  # side's non-negative length.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target height and side direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the height constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_height(base: [4, 0], constraint: [3, -90])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 3, angle: 90)
  #   base = Sevgi::Geometry::Segment[4, 0]
  #   Sevgi::Geometry::Parallelogram.new_by_height(base:, constraint:)
  def self.new_by_height(base:, constraint:, position: Origin)
    base = Tuple[Segment, base]
    constraint = Tuple[LengthAngle, constraint]

    height = constraint.length - base.y.abs
    angle = constraint.angle
    sine = F.sin(angle)
    Error.("Parallelogram height is smaller than its base span") if height.negative?
    Error.("Parallelogram height constraint must have a vertical component") if F.zero?(sine)

    self[base, Segment[height / sine.abs, angle], position:]
  end

  # Builds a parallelogram from a side and bounding-width constraint. The constraint length is the target width; its
  # signed angle is retained as the direction of the derived base while the component magnitude determines that
  # base's non-negative length.
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target width and base direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the width constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_width(side: [3, 90], constraint: [4, 180])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 4, angle: 0)
  #   side = Sevgi::Geometry::Segment[3, 90]
  #   Sevgi::Geometry::Parallelogram.new_by_width(side:, constraint:)
  def self.new_by_width(side:, constraint:, position: Origin)
    side = Tuple[Segment, side]
    constraint = Tuple[LengthAngle, constraint]

    width = constraint.length - side.x.abs
    angle = constraint.angle
    cosine = F.cos(angle)
    Error.("Parallelogram width is smaller than its side span") if width.negative?
    Error.("Parallelogram width constraint must have a horizontal component") if F.zero?(cosine)

    self[Segment[width / cosine.abs, angle], side, position:]
  end

  private

  def validate_geometry!
    a, b, c, d = segments
    valid = opposite?(a, c) && opposite?(b, d) && !F.zero?(cross(a, b))

    Error.("Parallelogram sides must be non-degenerate opposite pairs") unless valid
  end

  def cross(a, b) = (a.x * b.y) - (a.y * b.x)

  def opposite?(a, b) = F.zero?(a.x + b.x) && F.zero?(a.y + b.y)
end

#BSevgi::Geometry::Point (readonly)

Returns second vertex.

Returns:



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
# File 'lib/sevgi/geometry/elements/parallelogram.rb', line 58

class Parallelogram < ParallelogramBase
  # Builds a parallelogram from adjacent base and side segments. Both segments originate at `position`; `base`
  # defines AB and `side` defines AD, regardless of their angles.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
  def self.[](base, side, position: Origin)
    base, side = Tuples[Segment, base, side]

    new_by_segments(base, side.reverse, base.reverse, side, position:)
  end

  # Builds a parallelogram from a base and bounding-height constraint. The constraint length is the target height;
  # its signed angle is retained as the direction of the derived side while the component magnitude determines that
  # side's non-negative length.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target height and side direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the height constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_height(base: [4, 0], constraint: [3, -90])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 3, angle: 90)
  #   base = Sevgi::Geometry::Segment[4, 0]
  #   Sevgi::Geometry::Parallelogram.new_by_height(base:, constraint:)
  def self.new_by_height(base:, constraint:, position: Origin)
    base = Tuple[Segment, base]
    constraint = Tuple[LengthAngle, constraint]

    height = constraint.length - base.y.abs
    angle = constraint.angle
    sine = F.sin(angle)
    Error.("Parallelogram height is smaller than its base span") if height.negative?
    Error.("Parallelogram height constraint must have a vertical component") if F.zero?(sine)

    self[base, Segment[height / sine.abs, angle], position:]
  end

  # Builds a parallelogram from a side and bounding-width constraint. The constraint length is the target width; its
  # signed angle is retained as the direction of the derived base while the component magnitude determines that
  # base's non-negative length.
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target width and base direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the width constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_width(side: [3, 90], constraint: [4, 180])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 4, angle: 0)
  #   side = Sevgi::Geometry::Segment[3, 90]
  #   Sevgi::Geometry::Parallelogram.new_by_width(side:, constraint:)
  def self.new_by_width(side:, constraint:, position: Origin)
    side = Tuple[Segment, side]
    constraint = Tuple[LengthAngle, constraint]

    width = constraint.length - side.x.abs
    angle = constraint.angle
    cosine = F.cos(angle)
    Error.("Parallelogram width is smaller than its side span") if width.negative?
    Error.("Parallelogram width constraint must have a horizontal component") if F.zero?(cosine)

    self[Segment[width / cosine.abs, angle], side, position:]
  end

  private

  def validate_geometry!
    a, b, c, d = segments
    valid = opposite?(a, c) && opposite?(b, d) && !F.zero?(cross(a, b))

    Error.("Parallelogram sides must be non-degenerate opposite pairs") unless valid
  end

  def cross(a, b) = (a.x * b.y) - (a.y * b.x)

  def opposite?(a, b) = F.zero?(a.x + b.x) && F.zero?(a.y + b.y)
end

#BCSevgi::Geometry::Line (readonly)

Returns side from B to C.

Returns:



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
# File 'lib/sevgi/geometry/elements/parallelogram.rb', line 58

class Parallelogram < ParallelogramBase
  # Builds a parallelogram from adjacent base and side segments. Both segments originate at `position`; `base`
  # defines AB and `side` defines AD, regardless of their angles.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
  def self.[](base, side, position: Origin)
    base, side = Tuples[Segment, base, side]

    new_by_segments(base, side.reverse, base.reverse, side, position:)
  end

  # Builds a parallelogram from a base and bounding-height constraint. The constraint length is the target height;
  # its signed angle is retained as the direction of the derived side while the component magnitude determines that
  # side's non-negative length.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target height and side direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the height constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_height(base: [4, 0], constraint: [3, -90])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 3, angle: 90)
  #   base = Sevgi::Geometry::Segment[4, 0]
  #   Sevgi::Geometry::Parallelogram.new_by_height(base:, constraint:)
  def self.new_by_height(base:, constraint:, position: Origin)
    base = Tuple[Segment, base]
    constraint = Tuple[LengthAngle, constraint]

    height = constraint.length - base.y.abs
    angle = constraint.angle
    sine = F.sin(angle)
    Error.("Parallelogram height is smaller than its base span") if height.negative?
    Error.("Parallelogram height constraint must have a vertical component") if F.zero?(sine)

    self[base, Segment[height / sine.abs, angle], position:]
  end

  # Builds a parallelogram from a side and bounding-width constraint. The constraint length is the target width; its
  # signed angle is retained as the direction of the derived base while the component magnitude determines that
  # base's non-negative length.
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target width and base direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the width constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_width(side: [3, 90], constraint: [4, 180])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 4, angle: 0)
  #   side = Sevgi::Geometry::Segment[3, 90]
  #   Sevgi::Geometry::Parallelogram.new_by_width(side:, constraint:)
  def self.new_by_width(side:, constraint:, position: Origin)
    side = Tuple[Segment, side]
    constraint = Tuple[LengthAngle, constraint]

    width = constraint.length - side.x.abs
    angle = constraint.angle
    cosine = F.cos(angle)
    Error.("Parallelogram width is smaller than its side span") if width.negative?
    Error.("Parallelogram width constraint must have a horizontal component") if F.zero?(cosine)

    self[Segment[width / cosine.abs, angle], side, position:]
  end

  private

  def validate_geometry!
    a, b, c, d = segments
    valid = opposite?(a, c) && opposite?(b, d) && !F.zero?(cross(a, b))

    Error.("Parallelogram sides must be non-degenerate opposite pairs") unless valid
  end

  def cross(a, b) = (a.x * b.y) - (a.y * b.x)

  def opposite?(a, b) = F.zero?(a.x + b.x) && F.zero?(a.y + b.y)
end

#CSevgi::Geometry::Point (readonly)

Returns third vertex.

Returns:



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
# File 'lib/sevgi/geometry/elements/parallelogram.rb', line 58

class Parallelogram < ParallelogramBase
  # Builds a parallelogram from adjacent base and side segments. Both segments originate at `position`; `base`
  # defines AB and `side` defines AD, regardless of their angles.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
  def self.[](base, side, position: Origin)
    base, side = Tuples[Segment, base, side]

    new_by_segments(base, side.reverse, base.reverse, side, position:)
  end

  # Builds a parallelogram from a base and bounding-height constraint. The constraint length is the target height;
  # its signed angle is retained as the direction of the derived side while the component magnitude determines that
  # side's non-negative length.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target height and side direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the height constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_height(base: [4, 0], constraint: [3, -90])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 3, angle: 90)
  #   base = Sevgi::Geometry::Segment[4, 0]
  #   Sevgi::Geometry::Parallelogram.new_by_height(base:, constraint:)
  def self.new_by_height(base:, constraint:, position: Origin)
    base = Tuple[Segment, base]
    constraint = Tuple[LengthAngle, constraint]

    height = constraint.length - base.y.abs
    angle = constraint.angle
    sine = F.sin(angle)
    Error.("Parallelogram height is smaller than its base span") if height.negative?
    Error.("Parallelogram height constraint must have a vertical component") if F.zero?(sine)

    self[base, Segment[height / sine.abs, angle], position:]
  end

  # Builds a parallelogram from a side and bounding-width constraint. The constraint length is the target width; its
  # signed angle is retained as the direction of the derived base while the component magnitude determines that
  # base's non-negative length.
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target width and base direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the width constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_width(side: [3, 90], constraint: [4, 180])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 4, angle: 0)
  #   side = Sevgi::Geometry::Segment[3, 90]
  #   Sevgi::Geometry::Parallelogram.new_by_width(side:, constraint:)
  def self.new_by_width(side:, constraint:, position: Origin)
    side = Tuple[Segment, side]
    constraint = Tuple[LengthAngle, constraint]

    width = constraint.length - side.x.abs
    angle = constraint.angle
    cosine = F.cos(angle)
    Error.("Parallelogram width is smaller than its side span") if width.negative?
    Error.("Parallelogram width constraint must have a horizontal component") if F.zero?(cosine)

    self[Segment[width / cosine.abs, angle], side, position:]
  end

  private

  def validate_geometry!
    a, b, c, d = segments
    valid = opposite?(a, c) && opposite?(b, d) && !F.zero?(cross(a, b))

    Error.("Parallelogram sides must be non-degenerate opposite pairs") unless valid
  end

  def cross(a, b) = (a.x * b.y) - (a.y * b.x)

  def opposite?(a, b) = F.zero?(a.x + b.x) && F.zero?(a.y + b.y)
end

#CDSevgi::Geometry::Line (readonly)

Returns side from C to D.

Returns:



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
# File 'lib/sevgi/geometry/elements/parallelogram.rb', line 58

class Parallelogram < ParallelogramBase
  # Builds a parallelogram from adjacent base and side segments. Both segments originate at `position`; `base`
  # defines AB and `side` defines AD, regardless of their angles.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
  def self.[](base, side, position: Origin)
    base, side = Tuples[Segment, base, side]

    new_by_segments(base, side.reverse, base.reverse, side, position:)
  end

  # Builds a parallelogram from a base and bounding-height constraint. The constraint length is the target height;
  # its signed angle is retained as the direction of the derived side while the component magnitude determines that
  # side's non-negative length.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target height and side direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the height constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_height(base: [4, 0], constraint: [3, -90])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 3, angle: 90)
  #   base = Sevgi::Geometry::Segment[4, 0]
  #   Sevgi::Geometry::Parallelogram.new_by_height(base:, constraint:)
  def self.new_by_height(base:, constraint:, position: Origin)
    base = Tuple[Segment, base]
    constraint = Tuple[LengthAngle, constraint]

    height = constraint.length - base.y.abs
    angle = constraint.angle
    sine = F.sin(angle)
    Error.("Parallelogram height is smaller than its base span") if height.negative?
    Error.("Parallelogram height constraint must have a vertical component") if F.zero?(sine)

    self[base, Segment[height / sine.abs, angle], position:]
  end

  # Builds a parallelogram from a side and bounding-width constraint. The constraint length is the target width; its
  # signed angle is retained as the direction of the derived base while the component magnitude determines that
  # base's non-negative length.
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target width and base direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the width constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_width(side: [3, 90], constraint: [4, 180])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 4, angle: 0)
  #   side = Sevgi::Geometry::Segment[3, 90]
  #   Sevgi::Geometry::Parallelogram.new_by_width(side:, constraint:)
  def self.new_by_width(side:, constraint:, position: Origin)
    side = Tuple[Segment, side]
    constraint = Tuple[LengthAngle, constraint]

    width = constraint.length - side.x.abs
    angle = constraint.angle
    cosine = F.cos(angle)
    Error.("Parallelogram width is smaller than its side span") if width.negative?
    Error.("Parallelogram width constraint must have a horizontal component") if F.zero?(cosine)

    self[Segment[width / cosine.abs, angle], side, position:]
  end

  private

  def validate_geometry!
    a, b, c, d = segments
    valid = opposite?(a, c) && opposite?(b, d) && !F.zero?(cross(a, b))

    Error.("Parallelogram sides must be non-degenerate opposite pairs") unless valid
  end

  def cross(a, b) = (a.x * b.y) - (a.y * b.x)

  def opposite?(a, b) = F.zero?(a.x + b.x) && F.zero?(a.y + b.y)
end

#DSevgi::Geometry::Point (readonly)

Returns fourth vertex.

Returns:



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
# File 'lib/sevgi/geometry/elements/parallelogram.rb', line 58

class Parallelogram < ParallelogramBase
  # Builds a parallelogram from adjacent base and side segments. Both segments originate at `position`; `base`
  # defines AB and `side` defines AD, regardless of their angles.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
  def self.[](base, side, position: Origin)
    base, side = Tuples[Segment, base, side]

    new_by_segments(base, side.reverse, base.reverse, side, position:)
  end

  # Builds a parallelogram from a base and bounding-height constraint. The constraint length is the target height;
  # its signed angle is retained as the direction of the derived side while the component magnitude determines that
  # side's non-negative length.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target height and side direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the height constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_height(base: [4, 0], constraint: [3, -90])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 3, angle: 90)
  #   base = Sevgi::Geometry::Segment[4, 0]
  #   Sevgi::Geometry::Parallelogram.new_by_height(base:, constraint:)
  def self.new_by_height(base:, constraint:, position: Origin)
    base = Tuple[Segment, base]
    constraint = Tuple[LengthAngle, constraint]

    height = constraint.length - base.y.abs
    angle = constraint.angle
    sine = F.sin(angle)
    Error.("Parallelogram height is smaller than its base span") if height.negative?
    Error.("Parallelogram height constraint must have a vertical component") if F.zero?(sine)

    self[base, Segment[height / sine.abs, angle], position:]
  end

  # Builds a parallelogram from a side and bounding-width constraint. The constraint length is the target width; its
  # signed angle is retained as the direction of the derived base while the component magnitude determines that
  # base's non-negative length.
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target width and base direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the width constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_width(side: [3, 90], constraint: [4, 180])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 4, angle: 0)
  #   side = Sevgi::Geometry::Segment[3, 90]
  #   Sevgi::Geometry::Parallelogram.new_by_width(side:, constraint:)
  def self.new_by_width(side:, constraint:, position: Origin)
    side = Tuple[Segment, side]
    constraint = Tuple[LengthAngle, constraint]

    width = constraint.length - side.x.abs
    angle = constraint.angle
    cosine = F.cos(angle)
    Error.("Parallelogram width is smaller than its side span") if width.negative?
    Error.("Parallelogram width constraint must have a horizontal component") if F.zero?(cosine)

    self[Segment[width / cosine.abs, angle], side, position:]
  end

  private

  def validate_geometry!
    a, b, c, d = segments
    valid = opposite?(a, c) && opposite?(b, d) && !F.zero?(cross(a, b))

    Error.("Parallelogram sides must be non-degenerate opposite pairs") unless valid
  end

  def cross(a, b) = (a.x * b.y) - (a.y * b.x)

  def opposite?(a, b) = F.zero?(a.x + b.x) && F.zero?(a.y + b.y)
end

#DASevgi::Geometry::Line (readonly)

Returns side from D to A.

Returns:



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
# File 'lib/sevgi/geometry/elements/parallelogram.rb', line 58

class Parallelogram < ParallelogramBase
  # Builds a parallelogram from adjacent base and side segments. Both segments originate at `position`; `base`
  # defines AB and `side` defines AD, regardless of their angles.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
  def self.[](base, side, position: Origin)
    base, side = Tuples[Segment, base, side]

    new_by_segments(base, side.reverse, base.reverse, side, position:)
  end

  # Builds a parallelogram from a base and bounding-height constraint. The constraint length is the target height;
  # its signed angle is retained as the direction of the derived side while the component magnitude determines that
  # side's non-negative length.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target height and side direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the height constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_height(base: [4, 0], constraint: [3, -90])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 3, angle: 90)
  #   base = Sevgi::Geometry::Segment[4, 0]
  #   Sevgi::Geometry::Parallelogram.new_by_height(base:, constraint:)
  def self.new_by_height(base:, constraint:, position: Origin)
    base = Tuple[Segment, base]
    constraint = Tuple[LengthAngle, constraint]

    height = constraint.length - base.y.abs
    angle = constraint.angle
    sine = F.sin(angle)
    Error.("Parallelogram height is smaller than its base span") if height.negative?
    Error.("Parallelogram height constraint must have a vertical component") if F.zero?(sine)

    self[base, Segment[height / sine.abs, angle], position:]
  end

  # Builds a parallelogram from a side and bounding-width constraint. The constraint length is the target width; its
  # signed angle is retained as the direction of the derived base while the component magnitude determines that
  # base's non-negative length.
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target width and base direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the width constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_width(side: [3, 90], constraint: [4, 180])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 4, angle: 0)
  #   side = Sevgi::Geometry::Segment[3, 90]
  #   Sevgi::Geometry::Parallelogram.new_by_width(side:, constraint:)
  def self.new_by_width(side:, constraint:, position: Origin)
    side = Tuple[Segment, side]
    constraint = Tuple[LengthAngle, constraint]

    width = constraint.length - side.x.abs
    angle = constraint.angle
    cosine = F.cos(angle)
    Error.("Parallelogram width is smaller than its side span") if width.negative?
    Error.("Parallelogram width constraint must have a horizontal component") if F.zero?(cosine)

    self[Segment[width / cosine.abs, angle], side, position:]
  end

  private

  def validate_geometry!
    a, b, c, d = segments
    valid = opposite?(a, c) && opposite?(b, d) && !F.zero?(cross(a, b))

    Error.("Parallelogram sides must be non-degenerate opposite pairs") unless valid
  end

  def cross(a, b) = (a.x * b.y) - (a.y * b.x)

  def opposite?(a, b) = F.zero?(a.x + b.x) && F.zero?(a.y + b.y)
end

Class Method Details

.[](base, side, position: Origin) ⇒ Sevgi::Geometry::Parallelogram

Builds a parallelogram from adjacent base and side segments. Both segments originate at position; base defines AB and side defines AD, regardless of their angles.

Parameters:

Returns:

Raises:



66
67
68
69
70
# File 'lib/sevgi/geometry/elements/parallelogram.rb', line 66

def self.[](base, side, position: Origin)
  base, side = Tuples[Segment, base, side]

  new_by_segments(base, side.reverse, base.reverse, side, position:)
end

.call(*points) ⇒ Sevgi::Geometry::Parallelogram

Builds a parallelogram from four boundary points.

Parameters:

Returns:

Raises:



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
# File 'lib/sevgi/geometry/elements/parallelogram.rb', line 58

class Parallelogram < ParallelogramBase
  # Builds a parallelogram from adjacent base and side segments. Both segments originate at `position`; `base`
  # defines AB and `side` defines AD, regardless of their angles.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
  def self.[](base, side, position: Origin)
    base, side = Tuples[Segment, base, side]

    new_by_segments(base, side.reverse, base.reverse, side, position:)
  end

  # Builds a parallelogram from a base and bounding-height constraint. The constraint length is the target height;
  # its signed angle is retained as the direction of the derived side while the component magnitude determines that
  # side's non-negative length.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target height and side direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the height constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_height(base: [4, 0], constraint: [3, -90])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 3, angle: 90)
  #   base = Sevgi::Geometry::Segment[4, 0]
  #   Sevgi::Geometry::Parallelogram.new_by_height(base:, constraint:)
  def self.new_by_height(base:, constraint:, position: Origin)
    base = Tuple[Segment, base]
    constraint = Tuple[LengthAngle, constraint]

    height = constraint.length - base.y.abs
    angle = constraint.angle
    sine = F.sin(angle)
    Error.("Parallelogram height is smaller than its base span") if height.negative?
    Error.("Parallelogram height constraint must have a vertical component") if F.zero?(sine)

    self[base, Segment[height / sine.abs, angle], position:]
  end

  # Builds a parallelogram from a side and bounding-width constraint. The constraint length is the target width; its
  # signed angle is retained as the direction of the derived base while the component magnitude determines that
  # base's non-negative length.
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target width and base direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the width constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_width(side: [3, 90], constraint: [4, 180])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 4, angle: 0)
  #   side = Sevgi::Geometry::Segment[3, 90]
  #   Sevgi::Geometry::Parallelogram.new_by_width(side:, constraint:)
  def self.new_by_width(side:, constraint:, position: Origin)
    side = Tuple[Segment, side]
    constraint = Tuple[LengthAngle, constraint]

    width = constraint.length - side.x.abs
    angle = constraint.angle
    cosine = F.cos(angle)
    Error.("Parallelogram width is smaller than its side span") if width.negative?
    Error.("Parallelogram width constraint must have a horizontal component") if F.zero?(cosine)

    self[Segment[width / cosine.abs, angle], side, position:]
  end

  private

  def validate_geometry!
    a, b, c, d = segments
    valid = opposite?(a, c) && opposite?(b, d) && !F.zero?(cross(a, b))

    Error.("Parallelogram sides must be non-degenerate opposite pairs") unless valid
  end

  def cross(a, b) = (a.x * b.y) - (a.y * b.x)

  def opposite?(a, b) = F.zero?(a.x + b.x) && F.zero?(a.y + b.y)
end

.from_points(*points) ⇒ Sevgi::Geometry::Parallelogram

Builds a parallelogram from four boundary points.

Parameters:

Returns:

Raises:



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
# File 'lib/sevgi/geometry/elements/parallelogram.rb', line 58

class Parallelogram < ParallelogramBase
  # Builds a parallelogram from adjacent base and side segments. Both segments originate at `position`; `base`
  # defines AB and `side` defines AD, regardless of their angles.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
  def self.[](base, side, position: Origin)
    base, side = Tuples[Segment, base, side]

    new_by_segments(base, side.reverse, base.reverse, side, position:)
  end

  # Builds a parallelogram from a base and bounding-height constraint. The constraint length is the target height;
  # its signed angle is retained as the direction of the derived side while the component magnitude determines that
  # side's non-negative length.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target height and side direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the height constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_height(base: [4, 0], constraint: [3, -90])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 3, angle: 90)
  #   base = Sevgi::Geometry::Segment[4, 0]
  #   Sevgi::Geometry::Parallelogram.new_by_height(base:, constraint:)
  def self.new_by_height(base:, constraint:, position: Origin)
    base = Tuple[Segment, base]
    constraint = Tuple[LengthAngle, constraint]

    height = constraint.length - base.y.abs
    angle = constraint.angle
    sine = F.sin(angle)
    Error.("Parallelogram height is smaller than its base span") if height.negative?
    Error.("Parallelogram height constraint must have a vertical component") if F.zero?(sine)

    self[base, Segment[height / sine.abs, angle], position:]
  end

  # Builds a parallelogram from a side and bounding-width constraint. The constraint length is the target width; its
  # signed angle is retained as the direction of the derived base while the component magnitude determines that
  # base's non-negative length.
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target width and base direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the width constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_width(side: [3, 90], constraint: [4, 180])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 4, angle: 0)
  #   side = Sevgi::Geometry::Segment[3, 90]
  #   Sevgi::Geometry::Parallelogram.new_by_width(side:, constraint:)
  def self.new_by_width(side:, constraint:, position: Origin)
    side = Tuple[Segment, side]
    constraint = Tuple[LengthAngle, constraint]

    width = constraint.length - side.x.abs
    angle = constraint.angle
    cosine = F.cos(angle)
    Error.("Parallelogram width is smaller than its side span") if width.negative?
    Error.("Parallelogram width constraint must have a horizontal component") if F.zero?(cosine)

    self[Segment[width / cosine.abs, angle], side, position:]
  end

  private

  def validate_geometry!
    a, b, c, d = segments
    valid = opposite?(a, c) && opposite?(b, d) && !F.zero?(cross(a, b))

    Error.("Parallelogram sides must be non-degenerate opposite pairs") unless valid
  end

  def cross(a, b) = (a.x * b.y) - (a.y * b.x)

  def opposite?(a, b) = F.zero?(a.x + b.x) && F.zero?(a.y + b.y)
end

.from_segments(base, side, position: Origin) ⇒ Sevgi::Geometry::Parallelogram

Builds a parallelogram from two adjacent segments and derives their opposites.

Parameters:

Returns:

Raises:



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
# File 'lib/sevgi/geometry/elements/parallelogram.rb', line 58

class Parallelogram < ParallelogramBase
  # Builds a parallelogram from adjacent base and side segments. Both segments originate at `position`; `base`
  # defines AB and `side` defines AD, regardless of their angles.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
  def self.[](base, side, position: Origin)
    base, side = Tuples[Segment, base, side]

    new_by_segments(base, side.reverse, base.reverse, side, position:)
  end

  # Builds a parallelogram from a base and bounding-height constraint. The constraint length is the target height;
  # its signed angle is retained as the direction of the derived side while the component magnitude determines that
  # side's non-negative length.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target height and side direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the height constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_height(base: [4, 0], constraint: [3, -90])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 3, angle: 90)
  #   base = Sevgi::Geometry::Segment[4, 0]
  #   Sevgi::Geometry::Parallelogram.new_by_height(base:, constraint:)
  def self.new_by_height(base:, constraint:, position: Origin)
    base = Tuple[Segment, base]
    constraint = Tuple[LengthAngle, constraint]

    height = constraint.length - base.y.abs
    angle = constraint.angle
    sine = F.sin(angle)
    Error.("Parallelogram height is smaller than its base span") if height.negative?
    Error.("Parallelogram height constraint must have a vertical component") if F.zero?(sine)

    self[base, Segment[height / sine.abs, angle], position:]
  end

  # Builds a parallelogram from a side and bounding-width constraint. The constraint length is the target width; its
  # signed angle is retained as the direction of the derived base while the component magnitude determines that
  # base's non-negative length.
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target width and base direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the width constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_width(side: [3, 90], constraint: [4, 180])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 4, angle: 0)
  #   side = Sevgi::Geometry::Segment[3, 90]
  #   Sevgi::Geometry::Parallelogram.new_by_width(side:, constraint:)
  def self.new_by_width(side:, constraint:, position: Origin)
    side = Tuple[Segment, side]
    constraint = Tuple[LengthAngle, constraint]

    width = constraint.length - side.x.abs
    angle = constraint.angle
    cosine = F.cos(angle)
    Error.("Parallelogram width is smaller than its side span") if width.negative?
    Error.("Parallelogram width constraint must have a horizontal component") if F.zero?(cosine)

    self[Segment[width / cosine.abs, angle], side, position:]
  end

  private

  def validate_geometry!
    a, b, c, d = segments
    valid = opposite?(a, c) && opposite?(b, d) && !F.zero?(cross(a, b))

    Error.("Parallelogram sides must be non-degenerate opposite pairs") unless valid
  end

  def cross(a, b) = (a.x * b.y) - (a.y * b.x)

  def opposite?(a, b) = F.zero?(a.x + b.x) && F.zero?(a.y + b.y)
end

.new_by_height(base:, constraint:, position: Origin) ⇒ Sevgi::Geometry::Parallelogram

Builds a parallelogram from a base and bounding-height constraint. The constraint length is the target height; its signed angle is retained as the direction of the derived side while the component magnitude determines that side's non-negative length.

Examples:

Use an array constraint

Sevgi::Geometry::Parallelogram.new_by_height(base: [4, 0], constraint: [3, -90])

Use a LengthAngle constraint

constraint = Sevgi::Geometry::LengthAngle.new(length: 3, angle: 90)
base = Sevgi::Geometry::Segment[4, 0]
Sevgi::Geometry::Parallelogram.new_by_height(base:, constraint:)

Parameters:

Returns:

Raises:



86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/sevgi/geometry/elements/parallelogram.rb', line 86

def self.new_by_height(base:, constraint:, position: Origin)
  base = Tuple[Segment, base]
  constraint = Tuple[LengthAngle, constraint]

  height = constraint.length - base.y.abs
  angle = constraint.angle
  sine = F.sin(angle)
  Error.("Parallelogram height is smaller than its base span") if height.negative?
  Error.("Parallelogram height constraint must have a vertical component") if F.zero?(sine)

  self[base, Segment[height / sine.abs, angle], position:]
end

.new_by_width(side:, constraint:, position: Origin) ⇒ Sevgi::Geometry::Parallelogram

Builds a parallelogram from a side and bounding-width constraint. The constraint length is the target width; its signed angle is retained as the direction of the derived base while the component magnitude determines that base's non-negative length.

Examples:

Use an array constraint

Sevgi::Geometry::Parallelogram.new_by_width(side: [3, 90], constraint: [4, 180])

Use a LengthAngle constraint

constraint = Sevgi::Geometry::LengthAngle.new(length: 4, angle: 0)
side = Sevgi::Geometry::Segment[3, 90]
Sevgi::Geometry::Parallelogram.new_by_width(side:, constraint:)

Parameters:

Returns:

Raises:



113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/sevgi/geometry/elements/parallelogram.rb', line 113

def self.new_by_width(side:, constraint:, position: Origin)
  side = Tuple[Segment, side]
  constraint = Tuple[LengthAngle, constraint]

  width = constraint.length - side.x.abs
  angle = constraint.angle
  cosine = F.cos(angle)
  Error.("Parallelogram width is smaller than its side span") if width.negative?
  Error.("Parallelogram width constraint must have a horizontal component") if F.zero?(cosine)

  self[Segment[width / cosine.abs, angle], side, position:]
end

Instance Method Details

#perimeterFloat

Returns the closed path perimeter.

Returns:

  • (Float)


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
# File 'lib/sevgi/geometry/elements/parallelogram.rb', line 58

class Parallelogram < ParallelogramBase
  # Builds a parallelogram from adjacent base and side segments. Both segments originate at `position`; `base`
  # defines AB and `side` defines AD, regardless of their angles.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when segments or position cannot be coerced
  def self.[](base, side, position: Origin)
    base, side = Tuples[Segment, base, side]

    new_by_segments(base, side.reverse, base.reverse, side, position:)
  end

  # Builds a parallelogram from a base and bounding-height constraint. The constraint length is the target height;
  # its signed angle is retained as the direction of the derived side while the component magnitude determines that
  # side's non-negative length.
  # @param base [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to B
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target height and side direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the height constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_height(base: [4, 0], constraint: [3, -90])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 3, angle: 90)
  #   base = Sevgi::Geometry::Segment[4, 0]
  #   Sevgi::Geometry::Parallelogram.new_by_height(base:, constraint:)
  def self.new_by_height(base:, constraint:, position: Origin)
    base = Tuple[Segment, base]
    constraint = Tuple[LengthAngle, constraint]

    height = constraint.length - base.y.abs
    angle = constraint.angle
    sine = F.sin(angle)
    Error.("Parallelogram height is smaller than its base span") if height.negative?
    Error.("Parallelogram height constraint must have a vertical component") if F.zero?(sine)

    self[base, Segment[height / sine.abs, angle], position:]
  end

  # Builds a parallelogram from a side and bounding-width constraint. The constraint length is the target width; its
  # signed angle is retained as the direction of the derived base while the component magnitude determines that
  # base's non-negative length.
  # @param side [Sevgi::Geometry::Segment, Array<Numeric>] segment from A to D
  # @param constraint [Sevgi::Geometry::LengthAngle, Array<Numeric>] target width and base direction
  # @param position [Sevgi::Geometry::Point, Array<Numeric>] starting point
  # @return [Sevgi::Geometry::Parallelogram]
  # @raise [Sevgi::Geometry::Error] when inputs cannot be coerced or the width constraint is infeasible
  # @example Use an array constraint
  #   Sevgi::Geometry::Parallelogram.new_by_width(side: [3, 90], constraint: [4, 180])
  # @example Use a LengthAngle constraint
  #   constraint = Sevgi::Geometry::LengthAngle.new(length: 4, angle: 0)
  #   side = Sevgi::Geometry::Segment[3, 90]
  #   Sevgi::Geometry::Parallelogram.new_by_width(side:, constraint:)
  def self.new_by_width(side:, constraint:, position: Origin)
    side = Tuple[Segment, side]
    constraint = Tuple[LengthAngle, constraint]

    width = constraint.length - side.x.abs
    angle = constraint.angle
    cosine = F.cos(angle)
    Error.("Parallelogram width is smaller than its side span") if width.negative?
    Error.("Parallelogram width constraint must have a horizontal component") if F.zero?(cosine)

    self[Segment[width / cosine.abs, angle], side, position:]
  end

  private

  def validate_geometry!
    a, b, c, d = segments
    valid = opposite?(a, c) && opposite?(b, d) && !F.zero?(cross(a, b))

    Error.("Parallelogram sides must be non-degenerate opposite pairs") unless valid
  end

  def cross(a, b) = (a.x * b.y) - (a.y * b.x)

  def opposite?(a, b) = F.zero?(a.x + b.x) && F.zero?(a.y + b.y)
end