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, PerfectShape::Arc, PerfectShape::Ellipse, PerfectShape::Circle, PerfectShape::Rectangle, PerfectShape::Square]
WINDING_RULES =

Available winding rules

[:wind_even_odd, :wind_non_zero]

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from MultiPoint

#first_point, #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_even_odd, line_to_complex_shapes: false) ⇒ Path

Constructs Path with winding rule, closed status, line_to_complex_shapes option, 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, PerfectShape::CubicBezierCurve PerfectShape::Arc, PerfectShape::Ellipse, or PerfectShape::Circle Complex shapes, meaning Arc, Ellipse, and Circle, are decomposed into basic path shapes, meaning Point, Line, QuadraticBezierCurve, and CubicBezierCurve. winding_rule can be any of WINDING_RULES: :wind_non_zero (default) or :wind_even_odd closed can be true or false (default) line_to_complex_shapes can be true or false (default), indicating whether to connect to complex shapes, meaning Arc, Ellipse, and Circle, with a line, or otherwise move to their start point instead.



58
59
60
61
62
63
# File 'lib/perfect_shape/path.rb', line 58

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

Instance Attribute Details

#closedObject Also known as: closed?

Returns the value of attribute closed.



46
47
48
# File 'lib/perfect_shape/path.rb', line 46

def closed
  @closed
end

#line_to_complex_shapesObject Also known as: line_to_complex_shapes?

Returns the value of attribute line_to_complex_shapes.



46
47
48
# File 'lib/perfect_shape/path.rb', line 46

def line_to_complex_shapes
  @line_to_complex_shapes
end

#shapesObject

Returns the value of attribute shapes.



46
47
48
# File 'lib/perfect_shape/path.rb', line 46

def shapes
  @shapes
end

#winding_ruleObject

Returns the value of attribute winding_rule.



45
46
47
# File 'lib/perfect_shape/path.rb', line 45

def winding_rule
  @winding_rule
end

Instance Method Details

#basic_shapesObject

Returns basic shapes (i.e. Point, Line, QuadraticBezierCurve, and CubicBezierCurve), decomposed from complex shapes like Arc, Ellipse, and Circle by calling their ‘#to_path_shapes` method



401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
# File 'lib/perfect_shape/path.rb', line 401

def basic_shapes
  the_shapes = []
  @shapes.each_with_index do |shape, i|
    if shape.respond_to?(:to_path_shapes)
      shape_basic_shapes = shape.to_path_shapes
      the_shapes << shape.first_point if i == 0
      if @line_to_complex_shapes
        first_basic_shape = shape_basic_shapes.shift
        new_first_basic_shape = PerfectShape::Line.new(points: [first_basic_shape.to_a])
        shape_basic_shapes.unshift(new_first_basic_shape)
      end
      the_shapes += shape_basic_shapes
    else
      the_shapes << shape
    end
  end
  the_shapes
end

#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



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/perfect_shape/path.rb', line 137

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 basic_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



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/perfect_shape/path.rb', line 256

def disconnected_shapes
  # TODO it seems basic_shapes.first should always return a point, but there is a case with CompositeShape that results in a line (shape) not point returned
  first_point = basic_shapes.first.is_a?(Array) ? basic_shapes.first : basic_shapes.first.first_point
  initial_point = start_point = first_point.map {|n| BigDecimal(n.to_s)}
  final_point = nil
  the_disconnected_shapes = basic_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



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/perfect_shape/path.rb', line 103

def drawing_types
  the_drawing_shapes = basic_shapes.each_with_index.flat_map do |shape, i|
    case shape
    when Point
      :move_to
    when Array
      :move_to
    when Line
      (i == 0) ? [:move_to, :line_to] : :line_to
    when QuadraticBezierCurve
      :quad_to
    when CubicBezierCurve
      :cubic_to
    end
  end
  the_drawing_shapes << :close if closed?
  the_drawing_shapes
end

#intersect?(rectangle) ⇒ Boolean

Returns:

  • (Boolean)


294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/perfect_shape/path.rb', line 294

def intersect?(rectangle)
  x = rectangle.x
  y = rectangle.y
  w = rectangle.width
  h = rectangle.height
  # [xy]+[wh] is NaN if any of those values are NaN,
  # or if adding the two together would produce NaN
  # by virtue of adding opposing Infinte values.
  # Since we need to add them below, their sum must
  # not be NaN.
  # We return false because NaN always produces a
  # negative response to tests
  return false if (x+w).nan? || (y+h).nan?
  return false if w <= 0 || h <= 0
  mask = winding_rule == :wind_non_zero ? -1 : 2
  crossings = rect_crossings(x, y, x+w, y+h)
  crossings == PerfectShape::Rectangle::RECT_INTERSECTS ||
    (crossings & mask) != 0
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.



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
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/perfect_shape/path.rb', line 171

def point_crossings(x_or_point, y = nil)
  x, y = Point.normalize_point(x_or_point, y)
  return unless x && y
  return 0 if basic_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(basic_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



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

def points
  the_points = []
  basic_shapes.each_with_index do |shape, i|
    case shape
    when Point
      the_points << shape.to_a
    when Array
      the_points << shape.map {|n| BigDecimal(n.to_s)}
    when Line
      if i == 0
        shape.points.each do |point|
          the_points << point.to_a
        end
      else
        the_points << shape.points.last.to_a
      end
    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
  if closed?
    first_basic_shape = basic_shapes.first
    closing_point = first_basic_shape.is_a?(Array) ? first_basic_shape : first_basic_shape.first_point
    the_points << closing_point
  end
  the_points
end

#points=(some_points) ⇒ Object



99
100
101
# File 'lib/perfect_shape/path.rb', line 99

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

#rect_crossings(rxmin, rymin, rxmax, rymax) ⇒ Object



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/perfect_shape/path.rb', line 314

def rect_crossings(rxmin, rymin, rxmax, rymax)
  numTypes = drawing_types.count
  return 0 if numTypes == 0
  coords = points.flatten
  curx = cury = movx = movy = endx = endy = nil
  curx = movx = coords[0]
  cury = movy = coords[1]
  crossings = 0
  ci = 2
  i = 1
  
  while crossings != PerfectShape::Rectangle::RECT_INTERSECTS && i < numTypes
    case drawing_types[i]
    when :move_to
      if curx != movx || cury != movy
        line = PerfectShape::Line.new(points: [curx, cury, movx, movy])
        crossings = line.rect_crossings(rxmin, rymin, rxmax, rymax, crossings)
      end
      # Count should always be a multiple of 2 here.
      # assert((crossings & 1) != 0)
      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.rect_crossings(rxmin, rymin, rxmax, rymax, crossings)
      curx = endx
      cury = endy
    when :quad_to
      cx = coords[ci]
      ci += 1
      cy = coords[ci]
      ci += 1
      endx = coords[ci]
      ci += 1
      endy = coords[ci]
      ci += 1
      quadratic_bezier_curve = PerfectShape::QuadraticBezierCurve.new(points: [curx, cury, cx, cy, endx, endy])
      crossings = quadratic_bezier_curve.rect_crossings(rxmin, rymin, rxmax, rymax, 0, crossings)
      curx = endx
      cury = endy
    when :cubic_to
      c1x = coords[ci]
      ci += 1
      c1y = coords[ci]
      ci += 1
      c2x = coords[ci]
      ci += 1
      c2y = coords[ci]
      ci += 1
      endx = coords[ci]
      ci += 1
      endy = coords[ci]
      ci += 1
      cubic_bezier_curve = PerfectShape::CubicBezierCurve.new(points: [curx, cury, c1x, c1y, c2x, c2y, endx, endy])
      crossings = cubic_bezier_curve.rect_crossings(rxmin, rymin, rxmax, rymax, 0, crossings)
      curx = endx
      cury = endy
    when :close
      if curx != movx || cury != movy
        line = PerfectShape::Line.new(points: [curx, cury, movx, movy])
        crossings = line.rect_crossings(rxmin, rymin, rxmax, rymax, crossings)
      end
      curx = movx
      cury = movy
      # Count should always be a multiple of 2 here.
      # assert((crossings & 1) != 0)
    end
    i += 1
  end
  if crossings != PerfectShape::Rectangle::RECT_INTERSECTS &&
    (curx != movx || cury != movy)
    line = PerfectShape::Line.new(points: [curx, cury, movx, movy])
    crossings = line.rect_crossings(rxmin, rymin, rxmax, rymax, crossings)
  end
  # Count should always be a multiple of 2 here.
  # assert((crossings & 1) != 0)
  crossings
end