Class: PerfectShape::Path

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

Overview

Constant Summary collapse

SHAPE_TYPES =
[Array, Point, Line]
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, or PerfectShape::Line winding_rule can be any of WINDING_RULES: :wind_non_zero (default) or :wind_even_odd



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

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.



38
39
40
# File 'lib/perfect_shape/path.rb', line 38

def closed
  @closed
end

#shapesObject

Returns the value of attribute shapes.



38
39
40
# File 'lib/perfect_shape/path.rb', line 38

def shapes
  @shapes
end

#winding_ruleObject

Returns the value of attribute winding_rule.



37
38
39
# File 'lib/perfect_shape/path.rb', line 37

def winding_rule
  @winding_rule
end

Instance Method Details

#contain?(x_or_point, y = nil) ⇒ @code true

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

  • (@code true)

    if the point lies within the bound of



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/perfect_shape/path.rb', line 108

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).to_i & 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



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/perfect_shape/path.rb', line 75

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 # TODO
    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.



137
138
139
140
141
142
143
144
145
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
# File 'lib/perfect_shape/path.rb', line 137

def point_crossings(x_or_point, y = nil)
  x, y = normalize_point(x_or_point, y)
  return unless x && y
  return BigDecimal('0') if shapes.count == 0
  movx = movy = curx = cury = endx = endy = 0
  coords = points.flatten
  curx = movx = coords[0]
  cury = movy = coords[1]
  crossings = BigDecimal('0')
  ci = BigDecimal('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, 0)
        curx = endx;
        cury = endy;
#           when :cubic_to # TODO
#             crossings +=
#                 Curve.point_crossings_for_cubic(x, y,
#                                              curx, cury,
#                                              coords[ci++],
#                                              coords[ci++],
#                                              coords[ci++],
#                                              coords[ci++],
#                                              endx = coords[ci++],
#                                              endy = coords[ci++],
#                                              0);
#             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



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/perfect_shape/path.rb', line 50

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 # TODO
    end
  end
  the_points << @shapes.first.to_a if closed?
  the_points
end

#points=(some_points) ⇒ Object



71
72
73
# File 'lib/perfect_shape/path.rb', line 71

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