Class: Sevgi::Geometry::Segment

Inherits:
Data
  • Object
show all
Includes:
Comparable
Defined in:
lib/sevgi/geometry/segment.rb,
lib/sevgi/geometry/segment.rb

Overview

Immutable polar displacement in SVG/screen coordinates.

A Segment has no position: length is a distance and angle is a clockwise direction. Use #ending to apply it to a starting point or #line when a placed, finite line is required. Segment[length, angle] starts from polar components; Segment.(starting, ending) derives them from two points.

Examples:

Derive polar components from two points

segment = Sevgi::Geometry::Segment.([1, 2], [4, 6])
segment.length # => 5.0
segment.ending([1, 2]).deconstruct # => [4.0, 6.0]

Use a cardinal direction

Sevgi::Geometry::Segment.upward(3).ending([5, 5]).deconstruct # => [5.0, 2.0]

See Also:

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(length:, angle:) ⇒ void

Creates a segment.

Parameters:

  • length (Numeric)

    segment length

  • angle (Numeric)

    clockwise angle in degrees

Raises:



97
98
99
100
101
102
# File 'lib/sevgi/geometry/segment.rb', line 97

def initialize(length:, angle:)
  length = Real[:length, length]
  Error.("Segment length cannot be negative") if length.negative?

  super(length:, angle: Real[:angle, angle])
end

Instance Attribute Details

#angleFloat (readonly)

Returns clockwise angle in degrees.

Returns:

  • (Float)

    clockwise angle in degrees



30
31
32
# File 'lib/sevgi/geometry/segment.rb', line 30

def angle
  @angle
end

#lengthFloat (readonly)

Returns segment length.

Returns:

  • (Float)

    segment length



30
31
32
# File 'lib/sevgi/geometry/segment.rb', line 30

def length
  @length
end

Class Method Details

.[](length, angle) ⇒ Sevgi::Geometry::Segment

Creates a segment from polar components.

Examples:

Create a segment with mathematical notation

Sevgi::Geometry::Segment[5, 30]

Parameters:

  • length (Numeric)

    non-negative segment length

  • angle (Numeric)

    clockwise angle in degrees

Returns:

Raises:



30
31
32
33
34
35
36
37
38
39
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
# File 'lib/sevgi/geometry/segment.rb', line 30

Segment = Data.define(:length, :angle) do
  include Comparable

  # @!attribute [r] length
  #   @return [Float] segment length
  # @!attribute [r] angle
  #   @return [Float] clockwise angle in degrees

  # Creates a segment from start and end points.
  # @param starting [Sevgi::Geometry::Point, Array<Numeric>] start point
  # @param ending [Sevgi::Geometry::Point, Array<Numeric>] end point
  # @return [Sevgi::Geometry::Segment]
  # @raise [Sevgi::Geometry::Error] when either point cannot be coerced
  def self.call(starting, ending)
    starting, ending = Tuples[Point, starting, ending]
    self[Point.length(starting, ending), Point.angle(starting, ending)]
  end

  # Compares two segments with optional numeric precision.
  # @param this [Sevgi::Geometry::Segment, Array<Numeric>] first segment
  # @param that [Sevgi::Geometry::Segment, Array<Numeric>] second segment
  # @param precision [Integer, nil] decimal precision, or nil for the current function default
  # @return [Boolean]
  # @raise [Sevgi::Geometry::Error] when either segment cannot be coerced
  def self.eq?(this, that, precision: nil)
    this, that = Tuples[self, this, that]
    F.eq?(this.length, that.length, precision:) && F.eq?(this.angle, that.angle, precision:)
  end

  # Returns a downward segment.
  # @param length [Numeric] segment length
  # @return [Sevgi::Geometry::Segment]
  def self.downward(length) = self[length, 90.0]

  # Returns a leftward segment.
  # @param length [Numeric] segment length
  # @return [Sevgi::Geometry::Segment]
  def self.leftward(length) = self[length, 180.0]

  # Returns a rightward segment.
  # @param length [Numeric] segment length
  # @return [Sevgi::Geometry::Segment]
  def self.rightward(length) = self[length, 0.0]

  # Returns an upward segment.
  # @param length [Numeric] segment length
  # @return [Sevgi::Geometry::Segment]
  def self.upward(length) = self[length, -90.0]

  class << self
    # @overload horizontal(length)
    #   Returns a rightward segment.
    #   @param length [Numeric] segment length
    #   @return [Sevgi::Geometry::Segment]
    alias_method :horizontal, :rightward
    # @overload vertical(length)
    #   Returns a downward segment.
    #   @param length [Numeric] segment length
    #   @return [Sevgi::Geometry::Segment]
    alias_method :vertical, :downward
  end

  # Creates a segment.
  # @param length [Numeric] segment length
  # @param angle [Numeric] clockwise angle in degrees
  # @return [void]
  # @raise [Sevgi::Geometry::Error] when a component is not finite or length is negative
  def initialize(length:, angle:)
    length = Real[:length, length]
    Error.("Segment length cannot be negative") if length.negative?

    super(length:, angle: Real[:angle, angle])
  end

  # Compares segments by length.
  # @param other [Object] segment or two-item length/angle array to compare
  # @return [Integer, nil] comparison result, or nil when other is not a valid segment value
  def <=>(other)
    length <=> Tuple[Segment, other].length
  rescue Error
    nil
  end

  # Returns a segment rounded to precision.
  # @param precision [Integer, nil] decimal precision, or nil for the current function default
  # @return [Sevgi::Geometry::Segment]
  def approx(precision = nil) = with(length: F.approx(length, precision), angle: F.approx(angle, precision))

  # Returns the endpoint reached from a starting point.
  # @param starting [Sevgi::Geometry::Point, Array<Numeric>] start point
  # @return [Sevgi::Geometry::Point]
  # @raise [Sevgi::Geometry::Error] when starting cannot be coerced
  def ending(starting) = Tuple[Point, starting].translate(x, y)

  # Compares this segment with optional numeric precision.
  # @param other [Sevgi::Geometry::Segment, Array<Numeric>] segment to compare
  # @param precision [Integer, nil] decimal precision, or nil for the current function default
  # @return [Boolean]
  # @raise [Sevgi::Geometry::Error] when other cannot be coerced
  def eq?(other, precision: nil) = self.class.eq?(self, other, precision:)

  # Reports strict segment equality.
  # @param other [Object] object to compare
  # @return [Boolean]
  def eql?(other) = self.class == other.class && deconstruct == other.deconstruct

  # Returns a hash compatible with strict equality.
  # @return [Integer]
  def hash = [self.class, *deconstruct].hash

  # Converts the segment into a line at a point.
  # @param point [Sevgi::Geometry::Point, Array<Numeric>] line start point
  # @return [Sevgi::Geometry::Line]
  # @raise [Sevgi::Geometry::Error] when point cannot be coerced
  def line(point = Origin) = Line[length, angle, position: Tuple[Point, point]]

  # Returns the opposite segment.
  # @return [Sevgi::Geometry::Segment]
  def reverse = with(angle: angle + 180.0)

  # Returns the x component of the segment.
  # @return [Float]
  def x = length * F.cos(angle)

  # Returns the y component of the segment.
  # @return [Float]
  def y = length * F.sin(angle)
end

.call(starting, ending) ⇒ Sevgi::Geometry::Segment

Creates a segment from start and end points.

Parameters:

Returns:

Raises:



43
44
45
46
# File 'lib/sevgi/geometry/segment.rb', line 43

def self.call(starting, ending)
  starting, ending = Tuples[Point, starting, ending]
  self[Point.length(starting, ending), Point.angle(starting, ending)]
end

.downward(length) ⇒ Sevgi::Geometry::Segment Also known as: vertical

Returns a downward segment.

Parameters:

  • length (Numeric)

    segment length

Returns:



62
# File 'lib/sevgi/geometry/segment.rb', line 62

def self.downward(length) = self[length, 90.0]

.eq?(this, that, precision: nil) ⇒ Boolean

Compares two segments with optional numeric precision.

Parameters:

  • this (Sevgi::Geometry::Segment, Array<Numeric>)

    first segment

  • that (Sevgi::Geometry::Segment, Array<Numeric>)

    second segment

  • precision (Integer, nil) (defaults to: nil)

    decimal precision, or nil for the current function default

Returns:

  • (Boolean)

Raises:



54
55
56
57
# File 'lib/sevgi/geometry/segment.rb', line 54

def self.eq?(this, that, precision: nil)
  this, that = Tuples[self, this, that]
  F.eq?(this.length, that.length, precision:) && F.eq?(this.angle, that.angle, precision:)
end

.leftward(length) ⇒ Sevgi::Geometry::Segment

Returns a leftward segment.

Parameters:

  • length (Numeric)

    segment length

Returns:



67
# File 'lib/sevgi/geometry/segment.rb', line 67

def self.leftward(length) = self[length, 180.0]

.rightward(length) ⇒ Sevgi::Geometry::Segment Also known as: horizontal

Returns a rightward segment.

Parameters:

  • length (Numeric)

    segment length

Returns:



72
# File 'lib/sevgi/geometry/segment.rb', line 72

def self.rightward(length) = self[length, 0.0]

.upward(length) ⇒ Sevgi::Geometry::Segment

Returns an upward segment.

Parameters:

  • length (Numeric)

    segment length

Returns:



77
# File 'lib/sevgi/geometry/segment.rb', line 77

def self.upward(length) = self[length, -90.0]

Instance Method Details

#<=>(other) ⇒ Integer?

Compares segments by length.

Parameters:

  • other (Object)

    segment or two-item length/angle array to compare

Returns:

  • (Integer, nil)

    comparison result, or nil when other is not a valid segment value



107
108
109
110
111
# File 'lib/sevgi/geometry/segment.rb', line 107

def <=>(other)
  length <=> Tuple[Segment, other].length
rescue Error
  nil
end

#approx(precision = nil) ⇒ Sevgi::Geometry::Segment

Returns a segment rounded to precision.

Parameters:

  • precision (Integer, nil) (defaults to: nil)

    decimal precision, or nil for the current function default

Returns:



116
# File 'lib/sevgi/geometry/segment.rb', line 116

def approx(precision = nil) = with(length: F.approx(length, precision), angle: F.approx(angle, precision))

#ending(starting) ⇒ Sevgi::Geometry::Point

Returns the endpoint reached from a starting point.

Parameters:

Returns:

Raises:



122
# File 'lib/sevgi/geometry/segment.rb', line 122

def ending(starting) = Tuple[Point, starting].translate(x, y)

#eq?(other, precision: nil) ⇒ Boolean

Compares this segment with optional numeric precision.

Parameters:

  • other (Sevgi::Geometry::Segment, Array<Numeric>)

    segment to compare

  • precision (Integer, nil) (defaults to: nil)

    decimal precision, or nil for the current function default

Returns:

  • (Boolean)

Raises:



129
# File 'lib/sevgi/geometry/segment.rb', line 129

def eq?(other, precision: nil) = self.class.eq?(self, other, precision:)

#eql?(other) ⇒ Boolean

Reports strict segment equality.

Parameters:

  • other (Object)

    object to compare

Returns:

  • (Boolean)


134
# File 'lib/sevgi/geometry/segment.rb', line 134

def eql?(other) = self.class == other.class && deconstruct == other.deconstruct

#hashInteger

Returns a hash compatible with strict equality.

Returns:

  • (Integer)


138
# File 'lib/sevgi/geometry/segment.rb', line 138

def hash = [self.class, *deconstruct].hash

#line(point = Origin) ⇒ Sevgi::Geometry::Line

Converts the segment into a line at a point.

Parameters:

Returns:

Raises:



144
# File 'lib/sevgi/geometry/segment.rb', line 144

def line(point = Origin) = Line[length, angle, position: Tuple[Point, point]]

#reverseSevgi::Geometry::Segment

Returns the opposite segment.



148
# File 'lib/sevgi/geometry/segment.rb', line 148

def reverse = with(angle: angle + 180.0)

#xFloat

Returns the x component of the segment.

Returns:

  • (Float)


152
# File 'lib/sevgi/geometry/segment.rb', line 152

def x = length * F.cos(angle)

#yFloat

Returns the y component of the segment.

Returns:

  • (Float)


156
# File 'lib/sevgi/geometry/segment.rb', line 156

def y = length * F.sin(angle)