Class: Sevgi::Geometry::Ellipse

Inherits:
Sevgi::Geometry::Element::Arced show all
Defined in:
lib/sevgi/geometry/elements/ellipse.rb,
lib/sevgi/geometry/elements/ellipse/affine.rb,
lib/sevgi/geometry/elements/ellipse/length.rb

Overview

Immutable ellipse with positive radii, a center, and clockwise axis rotation. Parameter angles belong to the local ellipse axes, not to polar directions from the center.

Examples:

Select a finite boundary and inspect its endpoints

ellipse = Sevgi::Geometry::Ellipse[4, 2, position: [10, 20]]
ellipse.point(90).deconstruct # => [10.0, 22.0]
ellipse.arc(starting_angle: 0, extent: 90).ending == ellipse.point(90)

Direct Known Subclasses

Circle

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Sevgi::Geometry::Element::Arced

#eq?, #eql?, #hash, #outside?

Methods inherited from Element

#at, #closed?, #ignorable?, #intersection, lined

Constructor Details

#initialize(rx, ry, position:, rotation:) ⇒ void

Creates an ellipse. Use the bracket constructor.

Parameters:

  • rx (Numeric) —

    positive local x radius

  • ry (Numeric) —

    positive local y radius

  • position (Sevgi::Geometry::Point, Array<Numeric>) —

    center

  • rotation (Numeric) —

    clockwise rotation in degrees

Raises:



42
43
44
45
46
47
48
49
50
51
52
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 42

def initialize(rx, ry, position:, rotation:)
  super()
  @rx, @ry = [[:rx, rx], [:ry, ry]].map do |field, value|
    value = Real[field, value]
    Error.("Ellipse #{field} must be positive") unless value.positive?
    value
  end

  @position = Tuple[Point, position]
  @rotation = Real[:rotation, rotation]
end

Instance Attribute Details

#position ⇒ Sevgi::Geometry::Point (readonly) Also known as: center

Returns ellipse center.

Returns:



27
28
29
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 27

def position
  @position
end

#rotation ⇒ Float (readonly)

Returns clockwise rotation of the local axes in degrees.

Returns:

  • (Float) —

    clockwise rotation of the local axes in degrees



29
30
31
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 29

def rotation
  @rotation
end

#rx ⇒ Float (readonly)

Returns positive local x radius.

Returns:

  • (Float) —

    positive local x radius



31
32
33
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 31

def rx
  @rx
end

#ry ⇒ Float (readonly)

Returns positive local y radius.

Returns:

  • (Float) —

    positive local y radius



33
34
35
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 33

def ry
  @ry
end

Class Method Details

.[](rx, ry, position: Origin, rotation: 0) ⇒ Sevgi::Geometry::Ellipse

Builds an ellipse from local radii and its position.

Parameters:

  • rx (Numeric) —

    positive local x radius

  • ry (Numeric) —

    positive local y radius

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

    ellipse center

  • rotation (Numeric) (defaults to: 0) —

    clockwise rotation in degrees

Returns:

Raises:



21
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 21

def self.[](rx, ry, position: Origin, rotation: 0) = new(rx, ry, position:, rotation:)

Instance Method Details

#approx(precision = nil) ⇒ Sevgi::Geometry::Ellipse, Sevgi::Geometry::Circle

Rebuilds an ellipse from rounded canonical fields.

Parameters:

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

    decimal precision, or nil for the current function default

Returns:

Raises:

  • (Sevgi::Geometry::Error) —

    when rounding makes a radius zero

  • (Sevgi::ArgumentError) —

    when precision is invalid



59
60
61
62
63
64
65
66
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 59

def approx(precision = nil)
  rebuild(
    F.approx(rx, precision),
    F.approx(ry, precision),
    position.approx(precision),
    F.approx(rotation, precision)
  )
end

#arc(extent:, starting_angle: 0) ⇒ Sevgi::Geometry::Arc

Selects a finite, directed arc on this ellipse.

Parameters:

  • starting_angle (Numeric) (defaults to: 0) —

    local starting parameter angle in degrees

  • extent (Numeric) —

    signed angular extent strictly between -360 and 360 degrees

Returns:

Raises:



73
74
75
76
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 73

def arc(extent:, starting_angle: 0)
  parent = is_a?(Circle) ? Ellipse[rx, ry, position:] : self
  Arc.send(:new, parent, starting_angle:, extent:)
end

#box ⇒ Sevgi::Geometry::Rect

Returns the axis-aligned bounds without display rounding.



82
83
84
85
86
87
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 82

def box
  cosine, sine = F.cos(rotation), F.sin(rotation)
  dx = ::Math.hypot(rx * cosine, ry * sine)
  dy = ::Math.hypot(rx * sine, ry * cosine)
  Rect.from_corners([position.x - dx, position.y - dy], [position.x + dx, position.y + dy])
end

#circular? ⇒ Boolean

Reports whether the radii are exactly equal.

Returns:

  • (Boolean)


93
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 93

def circular? = rx == ry

#draw(node, **attributes) ⇒ Object

Draws an SVG ellipse using original geometric values.

Parameters:

  • node (Object) —

    graphics node receiving the element

  • attributes (Hash) —

    SVG attributes, including an optional outer transform

Returns:

  • (Object) —

    graphics command result



101
102
103
104
105
106
107
108
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 101

def draw(node, **attributes)
  unless rotation.zero?
    transform = "rotate(#{rotation} #{position.x} #{position.y})"
    attributes = attributes.merge(transform: [attributes[:transform], transform].compact.join(" "))
  end

  node.ellipse(cx: position.x, cy: position.y, rx:, ry:, **attributes)
end

#empty? ⇒ Boolean

Reports whether the ellipse has zero angular extent. A complete ellipse is never empty.

Returns:

  • (Boolean)


114
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 114

def empty? = false

#equation ⇒ Sevgi::Geometry::Equation::Quadratic

Returns the complete quadratic carrier.



118
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 118

def equation = @equation ||= Equation.quadratic(*coefficients, origin: position)

#equations ⇒ Array<Sevgi::Geometry::Equation::Quadratic>

Returns the immutable collection containing the complete carrier.



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

def equations = @equations ||= [equation].freeze

#inside?(point) ⇒ Boolean

Reports whether a point is inside or on the boundary.

Parameters:

Returns:

  • (Boolean)

Raises:



128
129
130
131
132
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 128

def inside?(point)
  point = Tuple[Point, point]
  local = local(point)
  ::Math.hypot(local.x / rx, local.y / ry) < 1.0 || on?(point)
end

#length ⇒ Float

Returns the perimeter with thread-independent numerical accuracy.

Returns:

  • (Float)

Raises:



137
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 137

def length = @length ||= arc_length(0, 360)

#on?(point) ⇒ Boolean

Reports whether a point matches its radial boundary reference at the current coordinate precision.

Parameters:

Returns:

  • (Boolean)

Raises:



143
144
145
146
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 143

def on?(point)
  point = Tuple[Point, point]
  point != position && point.eq?(self.point(parameter(point)))
end

#perimeter ⇒ Float

Returns the closed boundary length.

Returns:

  • (Float)


150
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 150

def perimeter = length

#point(angle) ⇒ Sevgi::Geometry::Point

Evaluates the boundary at a local parameter angle.

Parameters:

  • angle (Numeric) —

    clockwise local angle in degrees

Returns:

Raises:



156
157
158
159
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 156

def point(angle)
  angle = Real[:angle, angle] % 360.0
  Point[rx * F.cos(angle), ry * F.sin(angle)].rotate(rotation).translate(position.x, position.y)
end

#reflect(x: true, y: true) ⇒ Sevgi::Geometry::Ellipse, Sevgi::Geometry::Circle

Returns a reflected copy, preserving Circle where applicable.

Parameters:

  • x (Boolean) (defaults to: true) —

    reflect across the x-axis

  • y (Boolean) (defaults to: true) —

    reflect across the y-axis

Returns:

Raises:



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

def reflect(x: true, y: true) = affine(:reflect, x:, y:).first

#rotate(angle) ⇒ Sevgi::Geometry::Ellipse, Sevgi::Geometry::Circle

Rotates the center around the origin and the ellipse axes by the same angle.

Parameters:

  • angle (Numeric) —

    clockwise angle in degrees

Returns:

Raises:



172
173
174
175
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 172

def rotate(angle)
  angle = Real[:angle, angle]
  rebuild(rx, ry, position.rotate(angle), rotation + angle)
end

#scale(sx, sy = Undefined) ⇒ Sevgi::Geometry::Ellipse, Sevgi::Geometry::Circle

Scales the ellipse from the origin. Unequal factors can widen Circle to Ellipse.

Parameters:

  • sx (Numeric) —

    x scale factor

  • sy (Numeric, Sevgi::Undefined) (defaults to: Undefined) —

    y factor, defaulting to sx

Returns:

Raises:



184
185
186
187
188
189
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 184

def scale(sx, sy = Undefined)
  sx, sy = Real[:sx, sx], Real[:sy, Undefined.default(sy, sx)]
  return affine(:scale, sx, sy).first unless sx == sy

  rebuild(rx * sx.abs, ry * sy.abs, position.scale(sx, sy), rotation + (sx.negative? ? 180 : 0))
end

#skew(ax, ay = Undefined) ⇒ Sevgi::Geometry::Ellipse, Sevgi::Geometry::Circle

Skews the ellipse from the origin.

Parameters:

  • ax (Numeric) —

    x-axis skew angle in degrees

  • ay (Numeric, Sevgi::Undefined) (defaults to: Undefined) —

    y-axis angle, defaulting to ax

Returns:

Raises:



198
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 198

def skew(ax, ay = Undefined) = affine(:skew, ax, ay).first

#skew_x(angle) ⇒ Sevgi::Geometry::Ellipse, Sevgi::Geometry::Circle

Skews the ellipse along x.

Parameters:

  • angle (Numeric) —

    skew angle in degrees

Returns:

Raises:



204
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 204

def skew_x(angle) = affine(:skew_x, angle).first

#skew_y(angle) ⇒ Sevgi::Geometry::Ellipse, Sevgi::Geometry::Circle

Skews the ellipse along y.

Parameters:

  • angle (Numeric) —

    skew angle in degrees

Returns:

Raises:



210
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 210

def skew_y(angle) = affine(:skew_y, angle).first

#translate(dx, dy = Undefined) ⇒ Sevgi::Geometry::Ellipse, Sevgi::Geometry::Circle

Returns a translated copy.

Parameters:

  • dx (Numeric) —

    x offset

  • dy (Numeric, Sevgi::Undefined) (defaults to: Undefined) —

    y offset, defaulting to dx

Returns:

Raises:



217
# File 'lib/sevgi/geometry/elements/ellipse.rb', line 217

def translate(dx, dy = Undefined) = rebuild(rx, ry, position.translate(dx, dy), rotation)