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

Direct Known Subclasses

Circle

Instance Attribute Summary collapse

Attributes inherited from BasicShape

#options

Instance Method Summary collapse

Constructor Details

#initialize(*nodes, closed: true, **options) ⇒ Path

Returns a new instance of Path.



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 nodes 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[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

#closedObject (readonly)

Returns the value of attribute closed.



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

def closed
  @closed
end

#nodesObject (readonly)

Returns the value of attribute nodes.



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

def nodes
  @nodes
end

Instance Method Details

#flip(axis) ⇒ Object



94
95
96
97
98
99
100
101
102
# File 'lib/vector_salad/standard_shapes/path.rb', line 94

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

#flip_xObject



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

def flip_x
  flip(:x)
end

#flip_yObject



84
85
86
# File 'lib/vector_salad/standard_shapes/path.rb', line 84

def flip_y
  flip(:y)
end

#jitter(max, min: 0, fn: nil) ⇒ Object



148
149
150
151
152
153
154
155
156
157
158
# File 'lib/vector_salad/standard_shapes/path.rb', line 148

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

#move(x, y) ⇒ Object



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

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

#rotate(angle) ⇒ Object



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

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

#scale(x_multiplier, y_multiplier = x_multiplier) ⇒ Object



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

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

#to_aObject



244
245
246
# File 'lib/vector_salad/standard_shapes/path.rb', line 244

def to_a
  nodes.map(&:at)
end

#to_bezier_pathObject



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/vector_salad/standard_shapes/path.rb', line 164

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

#to_cubic_pathObject



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

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

#to_multi_pathObject



240
241
242
# File 'lib/vector_salad/standard_shapes/path.rb', line 240

def to_multi_path
  MultiPath.new(self)
end

#to_pathObject



160
161
162
# File 'lib/vector_salad/standard_shapes/path.rb', line 160

def to_path
  self
end

#to_simple_path(*_) ⇒ Object



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/vector_salad/standard_shapes/path.rb', line 217

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

#to_svgObject



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 << VectorSalad::Exporters::SvgExporter.options(@options)
  svg << '/>'
end

#to_svg_d_attributeObject



45
46
47
48
49
50
51
52
53
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/vector_salad/exporters/svg_exporter.rb', line 45

def to_svg_d_attribute
  nodes = to_bezier_path.nodes
  svg = ''
  svg << "M #{nodes[0].x} #{nodes[0].y}"

  nodes[1..-1].each_index do |j|
    i = j+1

    n = nodes[i]
    case n.type
    when :cubic
      if nodes[i-1].type == :node
        svg << " C #{n.x} #{n.y}"
      elsif nodes[i-2].type == :node && nodes[i-1].type == :cubic
        svg << ", #{n.x} #{n.y}"
      end
    when :quadratic
      svg << " Q #{n.x} #{n.y}"
    when :node
      if nodes[i-1].type == :cubic || nodes[i-1].type == :quadratic
        svg << ", #{n.x} #{n.y}"
      elsif nodes[i-1].type == :node
        svg << " L #{n.x} #{n.y}"
      end
    else
      raise "The SVG exporter doesn't support #{n.type} node type."
    end
  end

  svg << ' Z' if @closed
  svg
end