Class: PerfectShape::Path

Inherits:
Shape
  • Object
show all
Includes:
MultiPoint
Defined in:
lib/perfect_shape/path.rb

Overview

Constant Summary collapse

SHAPE_TYPES =

Available class types for path shapes

[Array, PerfectShape::Point, PerfectShape::Line, PerfectShape::QuadraticBezierCurve, PerfectShape::CubicBezierCurve]
WINDING_RULES =

Available winding rules

[:wind_non_zero, :wind_even_odd]

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from MultiPoint

#max_x, #max_y, #min_x, #min_y

Methods inherited from Shape

#==, #bounding_box, #center_x, #center_y, #height, #max_x, #max_y, #min_x, #min_y, #normalize_point, #width

Constructor Details

#initialize(shapes: [], closed: false, winding_rule: :wind_non_zero) ⇒ Path

Constructs Path with winding rule, closed status, and shapes (must always start with PerfectShape::Point or Array of [x,y] coordinates) Shape class types can be any of SHAPE_TYPES: Array (x,y coordinates), PerfectShape::Point, PerfectShape::Line, PerfectShape::QuadraticBezierCurve, or PerfectShape::CubicBezierCurve winding_rule can be any of WINDING_RULES: :wind_non_zero (default) or :wind_even_odd closed can be true or false



49
50
51
52
53
# File 'lib/perfect_shape/path.rb', line 49

def initialize(shapes: [], closed: false, winding_rule: :wind_non_zero)
  self.closed = closed
  self.winding_rule = winding_rule
  self.shapes = shapes
end

Instance Attribute Details

#closedObject Also known as: closed?

Returns the value of attribute closed.



42
43
44
# File 'lib/perfect_shape/path.rb', line 42

def closed
  @closed
end

#shapesObject

Returns the value of attribute shapes.



42
43
44
# File 'lib/perfect_shape/path.rb', line 42

def shapes
  @shapes
end

#winding_ruleObject

Returns the value of attribute winding_rule.



41
42
43
# File 'lib/perfect_shape/path.rb', line 41

def winding_rule
  @winding_rule
end

Instance Method Details

#contain?(x_or_point, y = nil) ⇒ Boolean

Checks if path contains point (two-number Array or x, y args) using the Nonzero-Rule (aka Winding Number Algorithm): en.wikipedia.org/wiki/Nonzero-rule or using the Even-Odd Rule (aka Ray Casting Algorithm): en.wikipedia.org/wiki/Even%E2%80%93odd_rule

the path or false if the point lies outside of the path’s bounds.

Parameters:

  • x

    The X coordinate of the point to test.

  • y (defaults to: nil)

    The Y coordinate of the point to test.

Returns:

  • (Boolean)

    true if the point lies within the bound of



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/perfect_shape/path.rb', line 117

def contain?(x_or_point, y = nil)
  x, y = normalize_point(x_or_point, y)
  return unless x && y
  if (x * 0.0 + y * 0.0) == 0.0
    # N * 0.0 is 0.0 only if N is finite.
    # Here we know that both x and y are finite.
    return false if shapes.count < 2
    mask = winding_rule == :wind_non_zero ? -1 : 1
    (point_crossings(x, y) & mask) != 0
  else
    # Either x or y was infinite or NaN.
    # A NaN always produces a negative response to any test
    # and Infinity values cannot be "inside" any path so
    # they should return false as well.
    false
  end
end

#drawing_typesObject



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/perfect_shape/path.rb', line 83

def drawing_types
  the_drawing_shapes = @shapes.map do |shape|
    case shape
    when Point
      :move_to
    when Array
      :move_to
    when Line
      :line_to
    when QuadraticBezierCurve
      :quad_to
    when CubicBezierCurve
      :cubic_to
    end
  end
  the_drawing_shapes << :close if closed?
  the_drawing_shapes
end

#point_crossings(x_or_point, y = nil) ⇒ Object

Calculates the number of times the given path crosses the ray extending to the right from (x,y). If the point lies on a part of the path, then no crossings are counted for that intersection. +1 is added for each crossing where the Y coordinate is increasing -1 is added for each crossing where the Y coordinate is decreasing The return value is the sum of all crossings for every segment in the path. The path must start with a PerfectShape::Point (initial location) The caller must check for NaN values. The caller may also reject infinite values as well.



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
# File 'lib/perfect_shape/path.rb', line 146

def point_crossings(x_or_point, y = nil)
  x, y = normalize_point(x_or_point, y)
  return unless x && y
  return 0 if shapes.count == 0
  movx = movy = curx = cury = endx = endy = 0
  coords = points.flatten
  curx = movx = coords[0]
  cury = movy = coords[1]
  crossings = 0
  ci = 2
  1.upto(shapes.count - 1).each do |i|
    case drawing_types[i]
    when :move_to
      if cury != movy
        line = PerfectShape::Line.new(points: [[curx, cury], [movx, movy]])
        crossings += line.point_crossings(x, y)
      end
      movx = curx = coords[ci]
      ci += 1
      movy = cury = coords[ci]
      ci += 1
    when :line_to
      endx = coords[ci]
      ci += 1
      endy = coords[ci]
      ci += 1
      line = PerfectShape::Line.new(points: [[curx, cury], [endx, endy]])
      crossings += line.point_crossings(x, y)
      curx = endx;
      cury = endy;
    when :quad_to
      quad_ctrlx = coords[ci]
      ci += 1
      quad_ctrly = coords[ci]
      ci += 1
      endx = coords[ci]
      ci += 1
      endy = coords[ci]
      ci += 1
      quad = PerfectShape::QuadraticBezierCurve.new(points: [[curx, cury], [quad_ctrlx, quad_ctrly], [endx, endy]])
      crossings += quad.point_crossings(x, y)
      curx = endx;
      cury = endy;
    when :cubic_to
      cubic_ctrl1x = coords[ci]
      ci += 1
      cubic_ctrl1y = coords[ci]
      ci += 1
      cubic_ctrl2x = coords[ci]
      ci += 1
      cubic_ctrl2y = coords[ci]
      ci += 1
      endx = coords[ci]
      ci += 1
      endy = coords[ci]
      ci += 1
      cubic = PerfectShape::CubicBezierCurve.new(points: [[curx, cury], [cubic_ctrl1x, cubic_ctrl1y], [cubic_ctrl2x, cubic_ctrl2y], [endx, endy]])
      crossings += cubic.point_crossings(x, y)
      curx = endx;
      cury = endy;
    when :close
      if cury != movy
        line = PerfectShape::Line.new(points: [[curx, cury], [movx, movy]])
        crossings += line.point_crossings(x, y)
      end
      curx = movx
      cury = movy
    end
  end
  if cury != movy
    line = PerfectShape::Line.new(points: [[curx, cury], [movx, movy]])
    crossings += line.point_crossings(x, y)
  end
  crossings
end

#pointsObject



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/perfect_shape/path.rb', line 55

def points
  the_points = []
  @shapes.each do |shape|
    case shape
    when Point
      the_points << shape.to_a
    when Array
      the_points << shape.map {|n| BigDecimal(n.to_s)}
    when Line
      the_points << shape.points.last.to_a
    when QuadraticBezierCurve
      shape.points.each do |point|
        the_points << point.to_a
      end
    when CubicBezierCurve
      shape.points.each do |point|
        the_points << point.to_a
      end
    end
  end
  the_points << @shapes.first.to_a if closed?
  the_points
end

#points=(some_points) ⇒ Object



79
80
81
# File 'lib/perfect_shape/path.rb', line 79

def points=(some_points)
  raise "Cannot assign points directly! Must set shapes instead and points are calculated from them automatically."
end