Class: PerfectShape::Path

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

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, normalize_point_array

Methods inherited from Shape

#==, #bounding_box, #center_point, #center_x, #center_y, #height, #max_x, #max_y, #min_x, #min_y, #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



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

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.



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

def closed
  @closed
end

#shapesObject

Returns the value of attribute shapes.



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

def shapes
  @shapes
end

#winding_ruleObject

Returns the value of attribute winding_rule.



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

def winding_rule
  @winding_rule
end

Instance Method Details

#contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0) ⇒ 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



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

def contain?(x_or_point, y = nil, outline: false, distance_tolerance: 0)
  x, y = Point.normalize_point(x_or_point, y)
  return unless x && y
  
  if outline
    disconnected_shapes.any? {|shape| shape.contain?(x, y, outline: true, distance_tolerance: distance_tolerance) }
  else
    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
end

#disconnected_shapesObject

Disconnected shapes have their start point filled in so that each shape does not depend on the previous shape to determine its start point.

Also, if a point is followed by a non-point shape, it is removed since it is augmented to the following shape as its start point.

Lastly, if the path is closed, an extra shape is added to represent the line connecting the last point to the first



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/perfect_shape/path.rb', line 235

def disconnected_shapes
  initial_point = start_point = @shapes.first.to_a.map {|n| BigDecimal(n.to_s)}
  final_point = nil
  the_disconnected_shapes = @shapes.drop(1).map do |shape|
    case shape
    when Point
      disconnected_shape = Point.new(*shape.to_a)
      start_point = shape.to_a
      final_point = disconnected_shape.to_a
      nil
    when Array
      disconnected_shape = Point.new(*shape.map {|n| BigDecimal(n.to_s)})
      start_point = shape.map {|n| BigDecimal(n.to_s)}
      final_point = disconnected_shape.to_a
      nil
    when Line
      disconnected_shape = Line.new(points: [start_point.to_a, shape.points.last])
      start_point = shape.points.last.to_a
      final_point = disconnected_shape.points.last.to_a
      disconnected_shape
    when QuadraticBezierCurve
      disconnected_shape = QuadraticBezierCurve.new(points: [start_point.to_a] + shape.points)
      start_point = shape.points.last.to_a
      final_point = disconnected_shape.points.last.to_a
      disconnected_shape
    when CubicBezierCurve
      disconnected_shape = CubicBezierCurve.new(points: [start_point.to_a] + shape.points)
      start_point = shape.points.last.to_a
      final_point = disconnected_shape.points.last.to_a
      disconnected_shape
    end
  end
  the_disconnected_shapes << Line.new(points: [final_point, initial_point]) if closed?
  the_disconnected_shapes.compact
end

#drawing_typesObject



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

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.



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
222
223
224
# File 'lib/perfect_shape/path.rb', line 150

def point_crossings(x_or_point, y = nil)
  x, y = Point.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



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

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



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

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