vector_salad 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/Gemfile +9 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +89 -0
  6. data/Rakefile +2 -0
  7. data/bin/vector_salad +72 -0
  8. data/examples/birthday.png +0 -0
  9. data/examples/birthday.rb +45 -0
  10. data/examples/birthday.svg +630 -0
  11. data/examples/boolean_operations.png +0 -0
  12. data/examples/boolean_operations.rb +23 -0
  13. data/examples/boolean_operations.svg +151 -0
  14. data/examples/bp_logo.png +0 -0
  15. data/examples/bp_logo.rb +18 -0
  16. data/examples/bp_logo.svg +25 -0
  17. data/examples/bunny_card.png +0 -0
  18. data/examples/bunny_card.rb +219 -0
  19. data/examples/bunny_card.svg +134 -0
  20. data/examples/chill.png +0 -0
  21. data/examples/chill.rb +16 -0
  22. data/examples/chill.svg +86 -0
  23. data/examples/circle_line_segments.png +0 -0
  24. data/examples/circle_line_segments.rb +11 -0
  25. data/examples/circle_line_segments.svg +28 -0
  26. data/examples/circles.png +0 -0
  27. data/examples/circles.rb +14 -0
  28. data/examples/circles.svg +11 -0
  29. data/examples/clip_operations.png +0 -0
  30. data/examples/clip_operations.rb +14 -0
  31. data/examples/clip_operations.svg +8 -0
  32. data/examples/cog_menu.png +0 -0
  33. data/examples/cog_menu.rb +32 -0
  34. data/examples/cog_menu.svg +37 -0
  35. data/examples/cubic_bezier_handles.png +0 -0
  36. data/examples/cubic_bezier_handles.rb +21 -0
  37. data/examples/cubic_bezier_handles.svg +14 -0
  38. data/examples/cubic_circle.png +0 -0
  39. data/examples/cubic_circle.rb +26 -0
  40. data/examples/cubic_circle.svg +29 -0
  41. data/examples/face.png +0 -0
  42. data/examples/face.rb +4 -0
  43. data/examples/face.svg +10 -0
  44. data/examples/flower.png +0 -0
  45. data/examples/flower.rb +23 -0
  46. data/examples/flower.svg +207 -0
  47. data/examples/fox.png +0 -0
  48. data/examples/fox.rb +110 -0
  49. data/examples/fox.svg +31 -0
  50. data/examples/fresh_vector_salad_gui.png +0 -0
  51. data/examples/galaxies.png +0 -0
  52. data/examples/galaxies.rb +60 -0
  53. data/examples/galaxies.svg +5806 -0
  54. data/examples/gold_stars.png +0 -0
  55. data/examples/gold_stars.rb +9 -0
  56. data/examples/gold_stars.svg +12 -0
  57. data/examples/paths.png +0 -0
  58. data/examples/paths.rb +87 -0
  59. data/examples/paths.svg +13 -0
  60. data/examples/pepsi_logo.png +0 -0
  61. data/examples/pepsi_logo.rb +21 -0
  62. data/examples/pepsi_logo.svg +10 -0
  63. data/examples/polygons.png +0 -0
  64. data/examples/polygons.rb +9 -0
  65. data/examples/polygons.svg +13 -0
  66. data/examples/quadratic_bezier_handle.png +0 -0
  67. data/examples/quadratic_bezier_handle.rb +18 -0
  68. data/examples/quadratic_bezier_handle.svg +13 -0
  69. data/examples/rects.png +0 -0
  70. data/examples/rects.rb +10 -0
  71. data/examples/rects.svg +11 -0
  72. data/examples/simple_path.png +0 -0
  73. data/examples/simple_path.rb +29 -0
  74. data/examples/simple_path.svg +8 -0
  75. data/examples/space.png +0 -0
  76. data/examples/space.rb +171 -0
  77. data/examples/space.svg +46453 -0
  78. data/examples/spiro_nodes.png +0 -0
  79. data/examples/spiro_nodes.rb +20 -0
  80. data/examples/spiro_nodes.svg +13 -0
  81. data/examples/squares.png +0 -0
  82. data/examples/squares.rb +14 -0
  83. data/examples/squares.svg +11 -0
  84. data/examples/stars.png +0 -0
  85. data/examples/stars.rb +3 -0
  86. data/examples/stars.svg +30006 -0
  87. data/examples/transforms.png +0 -0
  88. data/examples/transforms.rb +58 -0
  89. data/examples/transforms.svg +121 -0
  90. data/examples/triangles.png +0 -0
  91. data/examples/triangles.rb +8 -0
  92. data/examples/triangles.svg +9 -0
  93. data/lib/contracts_contracts.rb +32 -0
  94. data/lib/vector_salad.rb +5 -0
  95. data/lib/vector_salad/canvas.rb +27 -0
  96. data/lib/vector_salad/dsl.rb +41 -0
  97. data/lib/vector_salad/export_with_magic.rb +29 -0
  98. data/lib/vector_salad/exporters/base_exporter.rb +92 -0
  99. data/lib/vector_salad/exporters/svg_exporter.rb +174 -0
  100. data/lib/vector_salad/interpolate.rb +57 -0
  101. data/lib/vector_salad/magic.rb +17 -0
  102. data/lib/vector_salad/mixins/at.rb +28 -0
  103. data/lib/vector_salad/shape_proxy.rb +14 -0
  104. data/lib/vector_salad/standard_shapes/basic_shape.rb +29 -0
  105. data/lib/vector_salad/standard_shapes/circle.rb +64 -0
  106. data/lib/vector_salad/standard_shapes/clip.rb +51 -0
  107. data/lib/vector_salad/standard_shapes/custom.rb +28 -0
  108. data/lib/vector_salad/standard_shapes/difference.rb +28 -0
  109. data/lib/vector_salad/standard_shapes/exclusion.rb +28 -0
  110. data/lib/vector_salad/standard_shapes/flip.rb +24 -0
  111. data/lib/vector_salad/standard_shapes/hexagon.rb +15 -0
  112. data/lib/vector_salad/standard_shapes/intersection.rb +28 -0
  113. data/lib/vector_salad/standard_shapes/iso_tri.rb +39 -0
  114. data/lib/vector_salad/standard_shapes/jitter.rb +33 -0
  115. data/lib/vector_salad/standard_shapes/move.rb +24 -0
  116. data/lib/vector_salad/standard_shapes/multi_path.rb +82 -0
  117. data/lib/vector_salad/standard_shapes/n.rb +112 -0
  118. data/lib/vector_salad/standard_shapes/oval.rb +51 -0
  119. data/lib/vector_salad/standard_shapes/path.rb +249 -0
  120. data/lib/vector_salad/standard_shapes/pentagon.rb +15 -0
  121. data/lib/vector_salad/standard_shapes/polygon.rb +37 -0
  122. data/lib/vector_salad/standard_shapes/rect.rb +34 -0
  123. data/lib/vector_salad/standard_shapes/rotate.rb +24 -0
  124. data/lib/vector_salad/standard_shapes/scale.rb +34 -0
  125. data/lib/vector_salad/standard_shapes/square.rb +34 -0
  126. data/lib/vector_salad/standard_shapes/transform.rb +20 -0
  127. data/lib/vector_salad/standard_shapes/triangle.rb +15 -0
  128. data/lib/vector_salad/standard_shapes/union.rb +28 -0
  129. data/lib/vector_salad/version.rb +3 -0
  130. data/vector_salad.gemspec +34 -0
  131. metadata +262 -0
@@ -0,0 +1,33 @@
1
+ require 'vector_salad/standard_shapes/transform'
2
+
3
+ module VectorSalad
4
+ module StandardShapes
5
+ class Jitter < Transform
6
+ # Moves the contained shapes by the specified x and y amounts relatively.
7
+ #
8
+ # Examples:
9
+ #
10
+ # move(50, -10) do
11
+ # triangle(30, at: [50, -50])
12
+ # pentagon(40, at: [50, -100])
13
+ # end
14
+
15
+ # Jitter the position of nodes in a Path randomly.
16
+ # @param max The maximum offset
17
+ # @param min The minimum offset (default 0)
18
+ # @param fn The quantization number of sides
19
+ Contract Num, { min: Maybe[Num], fn: Maybe[Fixnum] } => Path
20
+ Contract Num, { min: Maybe[Num],
21
+ fn: Maybe[Fixnum],
22
+ canvas: VectorSalad::Canvas
23
+ }, Proc => Any
24
+ def initialize(max, min: 0, fn: nil, canvas:, **_options, &block)
25
+ instance_eval(&block) # inner_canvas is populated
26
+
27
+ @canvas.each do |shape|
28
+ canvas << shape.jitter(max, min: min, fn: fn)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ require "vector_salad/standard_shapes/transform"
2
+
3
+ module VectorSalad
4
+ module StandardShapes
5
+ class Move < Transform
6
+ # Moves the contained shapes by the specified x and y amounts relatively.
7
+ #
8
+ # Examples:
9
+ #
10
+ # move(50, -10) do
11
+ # triangle(30, at: [50, -50])
12
+ # pentagon(40, at: [50, -100])
13
+ # end
14
+ Contract Coord, Coord, { canvas: VectorSalad::Canvas }, Proc => Any
15
+ def initialize(x, y, canvas:, **_options, &block)
16
+ instance_eval(&block) # inner_canvas is populated
17
+
18
+ @canvas.each do |shape|
19
+ canvas << shape.move(x, y)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,82 @@
1
+ require 'vector_salad/standard_shapes/basic_shape'
2
+ require 'vector_salad/standard_shapes/n'
3
+ require 'vector_salad/interpolate'
4
+
5
+ module VectorSalad
6
+ module StandardShapes
7
+ class MultiPath < BasicShape
8
+ attr_reader :paths, :closed
9
+
10
+ # A MultiPath is a collection of Paths.
11
+ # It is mainly the result of {Clip} operations.
12
+ # See {Path} for details on constructing paths.
13
+ #
14
+ # Examples:
15
+ # new(
16
+ # Path.n([0,0], [0,300], [300,300], [300,0], [0,0]),
17
+ # Path.n([100,100], [200,100], [200,200], [100,200], [100,100])
18
+ # )
19
+ # new(
20
+ # [[0,0], [0,300], [300,300], [300,0], [0,0]],
21
+ # [[100,100], [200,100], [200,200], [100,200], [100,100]]
22
+ # )
23
+ Contract Args[Or[Array, Path]], { closed: Maybe[Bool] } => MultiPath
24
+ def initialize(*paths, closed: true, **options)
25
+ paths.each do |path|
26
+ path = path.instance_of?(Array) ? Path.new(*path) : path
27
+ @paths = Array(@paths) << path
28
+ end
29
+
30
+ @closed = closed
31
+ @options = options
32
+ self
33
+ end
34
+
35
+ # Move the shape relatively.
36
+ Contract Num, Num => MultiPath
37
+ def move(x, y)
38
+ each_send(:move, x, y)
39
+ end
40
+
41
+ # Rotates the MultiPath by the specified angle about the origin.
42
+ Contract Num => MultiPath
43
+ def rotate(angle)
44
+ each_send(:rotate, angle)
45
+ end
46
+
47
+ # Scale a MultiPath by multiplier about the origin.
48
+ Contract Num => MultiPath
49
+ def scale(multiplier)
50
+ each_send(:scale, multiplier)
51
+ end
52
+
53
+ def to_path
54
+ self
55
+ end
56
+
57
+ def to_cubic_path
58
+ self.class.new(*@paths.map(&:to_cubic_path), **@options)
59
+ end
60
+
61
+ def to_simple_path
62
+ self.class.new(*@paths.map(&:to_simple_path), **@options)
63
+ end
64
+
65
+ def to_multi_path
66
+ self
67
+ end
68
+
69
+ def to_a
70
+ @paths.map(&:to_a)
71
+ end
72
+
73
+ private
74
+
75
+ def each_send(method, *args, &block)
76
+ self.class.new(*@paths.map do |path|
77
+ path.send(method, *args, &block)
78
+ end, closed: @closed, **@options)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,112 @@
1
+ require "vector_salad/mixins/at"
2
+
3
+ module VectorSalad
4
+ module StandardShapes
5
+ class N
6
+ include VectorSalad::Mixins::At
7
+ include Contracts
8
+
9
+ attr_accessor :x, :y, :type
10
+
11
+ Contract nil, nil, :mirror => N
12
+ def initialize(x, y, type)
13
+ create(x, y, type)
14
+ self
15
+ end
16
+
17
+ # A node is the simplest primitive but useless on its own. Use nodes to
18
+ # build up a path (see Path). A node is a point in space with x and y
19
+ # coordinates. The coordinates must be nil if the node type is :mirror,
20
+ # else they must be numeric.
21
+ #
22
+ # A node also has a type, the simplest is :node for corners.
23
+ # To create node types other than :node see the shorthand class methods.
24
+ #
25
+ # Examples:
26
+ # new(50, 100)
27
+ # new(50, 100, :cubic)
28
+ # new(nil, nil, :mirror)
29
+ Contract Coord, Coord,
30
+ Maybe[*%i(node quadratic cubic g2 g4 left right)] => N
31
+ def initialize(x, y, type = :node)
32
+ create(x, y, type)
33
+ self
34
+ end
35
+
36
+ # Shorthand for calling `new` to create a node.
37
+ # The type defaults to :node for basic corner or line segment nodes.
38
+ # See the documentation for `new` for usage.
39
+ Contract Maybe[Coord], Maybe[Coord], Maybe[Symbol] => N
40
+ def self.n(x, y, type = :node)
41
+ new(x, y, type)
42
+ end
43
+
44
+ # Creates a :mirrored type node. It makes it easier to make smooth curves.
45
+ # The reflection is based on the cubic or quadratic node
46
+ # before the last standard :node so there must be one.
47
+ Contract None => N
48
+ def self.m
49
+ new(nil, nil, :mirror)
50
+ end
51
+
52
+ # Shorthand that creates a :cubic type bezier curve handle node.
53
+ # This "off curve" node isn't part of the path but distorts pulling it.
54
+ # Two :cubic nodes must come between two :node type nodes. E.g. n c c n.
55
+ # As the interaction of two cubic nodes distorts the line segment this
56
+ # can be quite difficult to imagine, see :quadratic or :spiro for
57
+ # easier alternatives to make curves.
58
+ Contract Coord, Coord => N
59
+ def self.c(x, y)
60
+ new(x, y, :cubic)
61
+ end
62
+
63
+ # Shorthand that creates a :quadratic type bezier curve handle node.
64
+ # This "off curve" node isn't part of the path but distorts pulling it.
65
+ # One :quadratic node must come between two :node type nodes. E.g. n q n.
66
+ # Only one quadratic node is needed to make a curve, however :spiro type
67
+ # nodes are even easier for making smooth curves.
68
+ Contract Coord, Coord => N
69
+ def self.q(x, y)
70
+ new(x, y, :quadratic)
71
+ end
72
+
73
+ # Shorthand that creates a smooth spiro :g2 node.
74
+ # Spiro nodes are "on curve" so the path bends through them, finding the
75
+ # smoothest possible route. They are perfect for making organic curves.
76
+ # There are :g2 and :g4 spiro node types;
77
+ # :g2 is most robust and a good all rounder.
78
+ Contract Coord, Coord => N
79
+ def self.s(x, y)
80
+ new(x, y, :g2)
81
+ end
82
+
83
+ # Shorthand that creates a smooth spiro :g4 node.
84
+ # There are :g2 and :g4 spiro node types;
85
+ # :g4 is smoothest but has longer distance effects and may sometimes fail.
86
+ Contract Coord, Coord => N
87
+ def self.g(x, y)
88
+ new(x, y, :g4)
89
+ end
90
+
91
+ # Shorthand that creates a :right spiro node.
92
+ # It joins a straight line segment on the left to a curve on the right.
93
+ Contract Coord, Coord => N
94
+ def self.r(x, y)
95
+ new(x, y, :right)
96
+ end
97
+
98
+ # Shorthand that creates a :left spiro node.
99
+ # It joins a straight line segment on the right to a curve on the left.
100
+ Contract Coord, Coord => N
101
+ def self.l(x, y)
102
+ new(x, y, :left)
103
+ end
104
+
105
+ private
106
+
107
+ def create(x, y, type)
108
+ @x, @y, @type = x, y, type
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,51 @@
1
+ require "vector_salad/standard_shapes/path"
2
+ require "vector_salad/standard_shapes/n"
3
+ require "vector_salad/mixins/at"
4
+
5
+ module VectorSalad
6
+ module StandardShapes
7
+ class Oval < BasicShape
8
+ include VectorSalad::Mixins::At
9
+ attr_reader :width, :height
10
+
11
+ # Create an oval.
12
+ #
13
+ # Examples:
14
+ # new(100, 200)
15
+ Contract Pos, Pos, {} => Oval
16
+ def initialize(width, height, **options)
17
+ @options = options
18
+ @width, @height = width, height
19
+ @x, @y = 0, 0
20
+ self
21
+ end
22
+
23
+ def to_path
24
+ # http://stackoverflow.com/a/13338311
25
+ # c = 4 * (Math.sqrt(2) - 1) / 3
26
+ # c = 0.5522847498307936
27
+ #
28
+ # http://spencermortensen.com/articles/bezier-circle/
29
+ c = 0.551915024494
30
+ dw = c * @width
31
+ dh = c * @height
32
+ Path.new(
33
+ N.n(@x + @width, @y),
34
+ N.c(@x + @width, @y + dh),
35
+ N.c(@x + dw, @y + @height),
36
+ N.n(@x, @y + @height),
37
+ N.c(@x - dw, @y + @height),
38
+ N.c(@x - @width, @y + dh),
39
+ N.n(@x - @width, @y),
40
+ N.c(@x - @width, @y - dh),
41
+ N.c(@x - dw, @y - @height),
42
+ N.n(@x, @y - @height),
43
+ N.c(@x + dw, @y - @height),
44
+ N.c(@x + @width, @y - dh),
45
+ N.n(@x + @width, @y),
46
+ @options
47
+ )
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,249 @@
1
+ require 'spiro'
2
+
3
+ require 'vector_salad/standard_shapes/basic_shape'
4
+ require 'vector_salad/standard_shapes/n'
5
+ require 'vector_salad/interpolate'
6
+
7
+ module VectorSalad
8
+ module StandardShapes
9
+ class Path < BasicShape
10
+ attr_reader :nodes, :closed
11
+
12
+ # The simplest shape primitive, all shapes can be represented as a Path.
13
+ # A path is made up of N nodes and these nodes can have different types
14
+ # (see N).
15
+ #
16
+ # Examples:
17
+ # new([0,0], [0,1], [1,1])
18
+ #
19
+ # @param nodes x,y coordinate arrays or N node instances
20
+ # @param closed whether the path is open or closed
21
+ Contract Args[Or[Array, N]], { closed: Maybe[Bool] } => Path
22
+ def initialize(*nodes, closed: true, **options)
23
+ @nodes = []
24
+ nodes.each_index do |i|
25
+ node = nodes[i].class == Array ? N.new(*nodes[i]) : nodes[i]
26
+ if i == 0 && ![:node, :g2, :g4, :left, :right].include?(node.type)
27
+ fail 'First node in a path must be :node or :spiro type.'
28
+ end
29
+ case node.type
30
+ when :cubic
31
+ unless nodes[i - 1].type == :node ||
32
+ (nodes[i - 2].type == :node && nodes[i - 1].type == :cubic)
33
+ fail ':cubic nodes must follow a :node and at most 1 other :cubic.'
34
+ end
35
+ when :quadratic
36
+ unless nodes[i - 1].type == :node
37
+ fail ':quadratic nodes must follow a :node.'
38
+ end
39
+ when :mirror
40
+ if nodes[i - 1].type == :node &&
41
+ (nodes[i - 2].type == :quadratic || nodes[i - 2].type == :cubic)
42
+ pivot = nodes[i - 1]
43
+ source = nodes[i - 2]
44
+
45
+ dx = pivot.x - source.x
46
+ dy = pivot.y - source.y
47
+ node[pivot.x + dx, pivot.y + dy]
48
+
49
+ node.type = source.type
50
+ else
51
+ fail ':reflect nodes must be preceeded by a :node with a
52
+ :quadratic or :cubic before that.'
53
+ end
54
+ when :node
55
+ end
56
+ @nodes << node
57
+ end
58
+
59
+ @closed = closed
60
+ @options = options
61
+ self
62
+ end
63
+
64
+ # Move the shape relatively.
65
+ Contract Num, Num => Path
66
+ def move(x, y)
67
+ Path.new(
68
+ *to_path.nodes.map do |node|
69
+ node.move(x, y)
70
+ end,
71
+ closed: @closed,
72
+ **@options
73
+ )
74
+ end
75
+
76
+ # Flips on the x axis.
77
+ Contract None => Path
78
+ def flip_x
79
+ flip(:x)
80
+ end
81
+
82
+ # Flips on the y axis.
83
+ Contract None => Path
84
+ def flip_y
85
+ flip(:y)
86
+ end
87
+
88
+ # Flips the path on the specified axis.
89
+ #
90
+ # Examples:
91
+ # flip(:x)
92
+ # flip(:y)
93
+ Contract Or[:x, :y] => Path
94
+ def flip(axis)
95
+ x = axis == :y ? -1 : 1
96
+ y = axis == :x ? -1 : 1
97
+
98
+ Path.new(
99
+ *to_path.nodes.map { |n| N.new(n.x * x, n.y * y, n.type) },
100
+ closed: @closed, **@options
101
+ )
102
+ end
103
+
104
+ # Rotates the Path by the specified angle about the origin.
105
+ # Examples:
106
+ # rotate(90)
107
+ # rotate(-45)
108
+ Contract Num => Path
109
+ def rotate(angle)
110
+ theta = angle / 180.0 * Math::PI
111
+
112
+ # http://stackoverflow.com/a/786508
113
+ # p'x = cos(theta) * (px-ox) - sin(theta) * (py-oy) + ox
114
+ # p'y = sin(theta) * (px-ox) + cos(theta) * (py-oy) + oy
115
+ Path.new(
116
+ *to_path.nodes.map do |n|
117
+ N.new(
118
+ Math.cos(theta) * n.x - Math.sin(theta) * n.y,
119
+ Math.sin(theta) * n.x + Math.cos(theta) * n.y,
120
+ n.type
121
+ )
122
+ end, closed: @closed, **@options
123
+ )
124
+ end
125
+
126
+ # Scale a Path by multiplier about the origin.
127
+ # Supply just 1 multiplier to scale evenly, or x and y multipliers
128
+ # to stretch or squash the axies.
129
+ # @param x_multiplier 1 is no change, 2 is double size, 0.5 is half, etc.
130
+ Contract Num, Maybe[Num] => Path
131
+ def scale(x_multiplier, y_multiplier = x_multiplier)
132
+ Path.new(
133
+ *to_path.nodes.map do |n|
134
+ N.new(
135
+ n.x * x_multiplier,
136
+ n.y * y_multiplier,
137
+ n.type
138
+ )
139
+ end, closed: @closed, **@options
140
+ )
141
+ end
142
+
143
+ # Jitter the position of nodes in a Path randomly.
144
+ # @param max The maximum offset
145
+ # @param min The minimum offset (default 0)
146
+ # @param fn The quantization number of sides
147
+ Contract Num, { min: Maybe[Num], fn: Maybe[Fixnum] } => Path
148
+ def jitter(max, min: 0, fn: nil)
149
+ Path.new(
150
+ *to_simple_path(fn).nodes.map do |n|
151
+ r = Random.rand(min..max)
152
+ a = Random.rand(0..Math::PI * 2)
153
+ x = r * Math.cos(a)
154
+ y = r * Math.sin(a)
155
+ n.move(x, y)
156
+ end, closed: @closed, **@options
157
+ )
158
+ end
159
+
160
+ def to_path
161
+ self
162
+ end
163
+
164
+ def to_bezier_path
165
+ path = to_path
166
+ spiro = false
167
+ flat_path = path.nodes.map do |n|
168
+ spiro = true if spiro || [:g2, :g4, :left, :right].include?(n.type)
169
+ [n.x, n.y, n.type]
170
+ end
171
+ if spiro
172
+ flat_spline_path = Spiro.spiros_to_splines(flat_path, @closed)
173
+ if flat_spline_path.nil?
174
+ fail 'Spiro failed, try different coordinates or using G2 nodes.'
175
+ else
176
+ path = Path.new(*flat_spline_path.map do |n|
177
+ N.new(n[0], n[1], n[2])
178
+ end, closed: @closed, **@options)
179
+ end
180
+ end
181
+ path
182
+ end
183
+
184
+ def to_cubic_path
185
+ path = to_bezier_path.nodes
186
+ cubic_path = []
187
+ quadratic_last = false
188
+ path.each_index do |i|
189
+ n = path[i]
190
+ if quadratic_last
191
+ n0 = path[i - 2]
192
+ q = path[i - 1]
193
+
194
+ # CP1 = QP0 + 2/3 * (QP1-QP0)
195
+ # CP2 = QP2 + 2/3 * (QP1-QP2)
196
+ third = 2 / 3.0
197
+ cubic_path << N.c(
198
+ n0.x + third * (q.x - n0.x),
199
+ n0.y + third * (q.y - n0.y)
200
+ )
201
+ cubic_path << N.c(
202
+ n.x + third * (q.x - n.x),
203
+ n.y + third * (q.y - n.y)
204
+ )
205
+ cubic_path << n
206
+
207
+ quadratic_last = false
208
+ elsif n.type == :quadratic
209
+ quadratic_last = true
210
+ else
211
+ cubic_path << n
212
+ end
213
+ end
214
+ Path.new(*cubic_path, closed: @closed, **@options)
215
+ end
216
+
217
+ def to_simple_path(*_)
218
+ # convert bezier curves and spiro splines
219
+ path = to_cubic_path.nodes
220
+
221
+ nodes = []
222
+ path.each_index do |i|
223
+ case path[i].type
224
+ when :node
225
+ if path[i - 1].type == :cubic
226
+ curve = path[i - 3..i].map(&:at)
227
+ nodes += VectorSalad::Interpolate.new.casteljau(curve)
228
+ else
229
+ nodes << path[i]
230
+ end
231
+ when :cubic
232
+ else
233
+ fail "Only :node and :cubic nodes in a path can be converted
234
+ to a simple path, was #{path[i].type}."
235
+ end
236
+ end
237
+ Path.new(*nodes, closed: @closed, **@options)
238
+ end
239
+
240
+ def to_multi_path
241
+ MultiPath.new(self)
242
+ end
243
+
244
+ def to_a
245
+ nodes.map(&:at)
246
+ end
247
+ end
248
+ end
249
+ end