Class: VectorSalad::StandardShapes::Path

Inherits:
BasicShape
  • Object
show all
Defined in:
lib/vector_salad/standard_shapes/path.rb,
lib/vector_salad/exporters/svg_exporter.rb

Overview

The simplest shape primitive, all shapes can be represented as a Path.

Instance Attribute Summary (collapse)

Instance Method Summary (collapse)

Constructor Details

- (Path) initialize(*nodes, closed: true, **options)

A path is made up of N nodes and these nodes can have different types (see N).

Examples:

new([0,0], [0,1], [1,1])

Parameters:

  • nodes (Args[Or[Array, N])

    x,y coordinate arrays or N node instances

  • closed ([])

    whether the path is open or closed

  • options ([])


22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/vector_salad/standard_shapes/path.rb', line 22

def initialize(*nodes, closed: true, **options)
  @nodes = []
  nodes.each_index do |i|
    node = nodes[i].class == Array ? N.new(*nodes[i]) : nodes[i]
    if i == 0 && ![:node, :g2, :g4, :left, :right].include?(node.type)
      fail "First node in a path must be :node or :spiro type."
    end
    case node.type
    when :cubic
      unless nodes[i - 1].type == :node ||
            (nodes[i - 2].type == :node && nodes[i - 1].type == :cubic)
        fail ":cubic node must follow a :node and at most 1 other :cubic."
      end
    when :quadratic
      unless nodes[i - 1].type == :node
        fail ":quadratic nodes must follow a :node."
      end
    when :mirror
      if nodes[i - 1].type == :node &&
         (nodes[i - 2].type == :quadratic || nodes[i - 2].type == :cubic)
        pivot = nodes[i - 1]
        source = nodes[i - 2]

        dx = pivot.x - source.x
        dy = pivot.y - source.y
        node.at = [pivot.x + dx, pivot.y + dy]

        node.type = source.type
      else
        fail ":reflect nodes must be preceeded by a :node with a
          :quadratic or :cubic before that."
      end
    when :node
    end
    @nodes << node
  end

  @closed = closed
  @options = options
  self
end

Instance Attribute Details

- (Object) closed (readonly)

Returns the value of attribute closed



11
12
13
# File 'lib/vector_salad/standard_shapes/path.rb', line 11

def closed
  @closed
end

- (Object) nodes (readonly)

Returns the value of attribute nodes



11
12
13
# File 'lib/vector_salad/standard_shapes/path.rb', line 11

def nodes
  @nodes
end

- (Object) options Originally defined in class BasicShape

Returns the value of attribute options

Instance Method Details

- (Path) [](x, y)

Move the path absolutely.

Parameters:

  • x (Coord)

    +Num; a coordinate+

  • y (Coord)

    +Num; a coordinate+

Returns:



66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/vector_salad/standard_shapes/path.rb', line 66

def [](x, y)
  # finds the top-left most point as if the path is in a bounding box
  top_left_point = nodes[0].at
  nodes.each do |node|
    top_left_point[0] = node.at[0] if node.at[0] < top_left_point[0]
    top_left_point[1] = node.at[1] if node.at[1] < top_left_point[1]
  end
  # finds the delta between the current top-left and new top-left coords
  dx = x - top_left_point[0]
  dy = y - top_left_point[1]
  # move path to the new delta
  move(dx, dy)
end

- (Path) flip(axis)

Flips the path on the specified axis.

Examples:

flip(:x)
flip(:y)

Parameters:

  • axis (Or[:x, :y])

Returns:



111
112
113
114
115
116
117
118
119
# File 'lib/vector_salad/standard_shapes/path.rb', line 111

def flip(axis)
  x = axis == :y ? -1 : 1
  y = axis == :x ? -1 : 1

  Path.new(
    *to_path.nodes.map { |n| N.new(n.x * x, n.y * y, n.type) },
    closed: @closed, **@options
  )
end

- (Path) flip_x

Flips on the x axis.

Returns:



94
95
96
# File 'lib/vector_salad/standard_shapes/path.rb', line 94

def flip_x
  flip(:x)
end

- (Path) flip_y

Flips on the y axis.

Returns:



100
101
102
# File 'lib/vector_salad/standard_shapes/path.rb', line 100

def flip_y
  flip(:y)
end

- (Path) jitter(max, min: 0, fn: nil)

Jitter the position of nodes in a Path randomly.

Parameters:

  • max (Coord)

    +Num; a coordinate+. The maximum offset

  • min ([])

    The minimum offset (default 0)

  • fn ([])

    The quantization number of sides

Returns:



169
170
171
172
173
174
175
176
177
178
179
# File 'lib/vector_salad/standard_shapes/path.rb', line 169

def jitter(max, min: 0, fn: nil)
  Path.new(
    *to_simple_path(fn).nodes.map do |n|
      r = Random.rand(min..max)
      a = Random.rand(0..Math::PI * 2)
      x = r * Math.cos(a)
      y = r * Math.sin(a)
      n.move(x, y)
    end, closed: @closed, **@options
  )
end

- (Path) move(x, y)

Move the path relatively.

Parameters:

  • x (Coord)

    +Num; a coordinate+

  • y (Coord)

    +Num; a coordinate+

Returns:



82
83
84
85
86
87
88
89
90
# File 'lib/vector_salad/standard_shapes/path.rb', line 82

def move(x, y)
  Path.new(
    *to_path.nodes.map do |node|
      node.move(x, y)
    end,
    closed: @closed,
    **@options
  )
end

- (Path) rotate(angle)

Rotates the Path by the specified angle about the origin.

Examples:

rotate(90)
rotate(-45)

Parameters:

  • angle (Coord)

    +Num; a coordinate+

Returns:



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/vector_salad/standard_shapes/path.rb', line 128

def rotate(angle)
  theta = angle / 180.0 * Math::PI

  # http://stackoverflow.com/a/786508
  # p'x = cos(theta) * (px-ox) - sin(theta) * (py-oy) + ox
  # p'y = sin(theta) * (px-ox) + cos(theta) * (py-oy) + oy
  Path.new(
    *to_path.nodes.map do |n|
      N.new(
        Math.cos(theta) * n.x - Math.sin(theta) * n.y,
        Math.sin(theta) * n.x + Math.cos(theta) * n.y,
        n.type
      )
    end, closed: @closed, **@options
  )
end

- (Path) scale(x_multiplier, y_multiplier = x_multiplier)

Scale a Path by multiplier about the origin. Supply just 1 multiplier to scale evenly, or x and y multipliers to stretch or squash the axies.

Parameters:

  • x_multiplier (Coord)

    +Num; a coordinate+. 1 is no change, 2 is double size, 0.5 is half, etc.

  • y_multiplier (Maybe[Coord]) (defaults to: x_multiplier)

Returns:



151
152
153
154
155
156
157
158
159
160
161
# File 'lib/vector_salad/standard_shapes/path.rb', line 151

def scale(x_multiplier, y_multiplier = x_multiplier)
  Path.new(
    *to_path.nodes.map do |n|
      N.new(
        n.x * x_multiplier,
        n.y * y_multiplier,
        n.type
      )
    end, closed: @closed, **@options
  )
end

- (Object) to_a

Return the nodes as an array of coordinates.



274
275
276
# File 'lib/vector_salad/standard_shapes/path.rb', line 274

def to_a
  nodes.map(&:at)
end

- (Object) to_bezier_path

Convert the complex path to a bezier path. This will convert any Spiro curve nodes into beziers.



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/vector_salad/standard_shapes/path.rb', line 188

def to_bezier_path
  path = to_path
  spiro = false
  flat_path = path.nodes.map do |n|
    spiro = true if spiro || [:g2, :g4, :left, :right].include?(n.type)
    [n.x, n.y, n.type]
  end
  if spiro
    flat_spline_path = Spiro.spiros_to_splines(flat_path, @closed)
    if flat_spline_path.nil?
      fail "Spiro failed, try different coordinates or using G2 nodes."
    else
      path = Path.new(*flat_spline_path.map do |n|
        N.new(n[0], n[1], n[2])
      end, closed: @closed, **@options)
    end
  end
  path
end

- (Object) to_cubic_path

Convert the path into a cubic bezier path (no quadratics). This will convert any Spiro curve nodes into beziers and any quadratic beziers into cubics.



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
# File 'lib/vector_salad/standard_shapes/path.rb', line 211

def to_cubic_path
  path = to_bezier_path.nodes
  cubic_path = []
  quadratic_last = false
  path.each_index do |i|
    n = path[i]
    if quadratic_last
      n0 = path[i - 2]
      q  = path[i - 1]

      # CP1 = QP0 + 2/3 * (QP1-QP0)
      # CP2 = QP2 + 2/3 * (QP1-QP2)
      third = 2 / 3.0
      cubic_path << N.c(
        n0.x + third * (q.x - n0.x),
        n0.y + third * (q.y - n0.y)
      )
      cubic_path << N.c(
        n.x + third * (q.x - n.x),
        n.y + third * (q.y - n.y)
      )
      cubic_path << n

      quadratic_last = false
    elsif n.type == :quadratic
      quadratic_last = true
    else
      cubic_path << n
    end
  end
  Path.new(*cubic_path, closed: @closed, **@options)
end

- (Object) to_multi_path

Wrap the path in a multi_path.



269
270
271
# File 'lib/vector_salad/standard_shapes/path.rb', line 269

def to_multi_path
  MultiPath.new(self)
end

- (Object) to_path

Convert the path to a path (it returns self)



182
183
184
# File 'lib/vector_salad/standard_shapes/path.rb', line 182

def to_path
  self
end

- (Object) to_simple_path(*_)

Flatten any curves in the path into many small straight line segments.



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/vector_salad/standard_shapes/path.rb', line 245

def to_simple_path(*_)
  # convert bezier curves and spiro splines
  path = to_cubic_path.nodes

  nodes = []
  path.each_index do |i|
    case path[i].type
    when :node
      if path[i - 1].type == :cubic
        curve = path[i - 3..i].map(&:at)
        nodes += VectorSalad::Interpolate.new.casteljau(curve)
      else
        nodes << path[i]
      end
    when :cubic
    else
      fail "Only :node and :cubic nodes in a path can be converted
        to a simple path, was #{path[i].type}."
    end
  end
  Path.new(*nodes, closed: @closed, **@options)
end

- (Object) to_svg

Export the shape to an svg string



37
38
39
40
41
42
43
# File 'lib/vector_salad/exporters/svg_exporter.rb', line 37

def to_svg
  svg = '<path d="'
  svg << to_svg_d_attribute
  svg << '"'
  svg << Exporters::SvgExporter.options(@options)
  svg << "/>"
end