postsvg 0.1.0 → 0.3.0

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 (201) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +105 -0
  3. data/CLAUDE.md +173 -0
  4. data/Gemfile +2 -3
  5. data/README.adoc +456 -179
  6. data/Rakefile +100 -0
  7. data/TODO.roadmap/00-architecture.md +139 -0
  8. data/TODO.roadmap/01-autoload-migration.md +39 -0
  9. data/TODO.roadmap/02-isolate-dormant-code.md +51 -0
  10. data/TODO.roadmap/03-domain-model.md +66 -0
  11. data/TODO.roadmap/04-lexer.md +40 -0
  12. data/TODO.roadmap/05-parser.md +43 -0
  13. data/TODO.roadmap/06-graphics-state.md +45 -0
  14. data/TODO.roadmap/07-matrix-and-color.md +37 -0
  15. data/TODO.roadmap/08-svg-builder.md +51 -0
  16. data/TODO.roadmap/09-renderer.md +52 -0
  17. data/TODO.roadmap/10-visitor.md +58 -0
  18. data/TODO.roadmap/11-operator-coverage.md +103 -0
  19. data/TODO.roadmap/12-svg-domain.md +62 -0
  20. data/TODO.roadmap/13-translation-handlers.md +69 -0
  21. data/TODO.roadmap/14-ps-serializer.md +49 -0
  22. data/TODO.roadmap/15-cli-and-public-api.md +47 -0
  23. data/TODO.roadmap/16-specs.md +84 -0
  24. data/TODO.roadmap/17-docs-sync.md +47 -0
  25. data/TODO.roadmap/18-performance-and-determinism.md +47 -0
  26. data/TODO.roadmap/19-error-model.md +45 -0
  27. data/TODO.roadmap/20-font-and-text.md +46 -0
  28. data/TODO.roadmap/21-images.md +45 -0
  29. data/TODO.roadmap/22-forms-and-resources.md +25 -0
  30. data/TODO.roadmap/23-level2-level3.md +52 -0
  31. data/TODO.roadmap/24-ci-and-release.md +42 -0
  32. data/TODO.roadmap/README.md +77 -0
  33. data/docs/.gitignore +29 -0
  34. data/docs/CHANGELOG.md +114 -0
  35. data/docs/COMPLETE_DOCUMENTATION_STATUS.md +376 -0
  36. data/docs/DEPLOYMENT.md +456 -0
  37. data/docs/DEPLOYMENT_INSTRUCTIONS.md +229 -0
  38. data/docs/DOCUMENTATION_PLAN.md +425 -0
  39. data/docs/FINAL_SUMMARY.md +657 -0
  40. data/docs/Gemfile +15 -0
  41. data/docs/README.md +327 -0
  42. data/docs/_config.yml +99 -0
  43. data/docs/advanced-topics.adoc +370 -0
  44. data/docs/api-reference/colors.adoc +705 -0
  45. data/docs/api-reference/converter.adoc +699 -0
  46. data/docs/api-reference/execution-context.adoc +1210 -0
  47. data/docs/api-reference/graphics-state.adoc +1070 -0
  48. data/docs/api-reference/interpreter.adoc +810 -0
  49. data/docs/api-reference/matrix.adoc +1179 -0
  50. data/docs/api-reference/path-builder.adoc +1284 -0
  51. data/docs/api-reference/postsvg-module.adoc +388 -0
  52. data/docs/api-reference/svg-generator.adoc +891 -0
  53. data/docs/api-reference/tokenizer.adoc +925 -0
  54. data/docs/api-reference.adoc +221 -0
  55. data/docs/architecture/command-registry.adoc +1191 -0
  56. data/docs/architecture/conversion-pipeline.adoc +746 -0
  57. data/docs/architecture/design-decisions.adoc +999 -0
  58. data/docs/architecture/generator-stage.adoc +1115 -0
  59. data/docs/architecture/graphics-state-model.adoc +1089 -0
  60. data/docs/architecture/interpreter-stage.adoc +1125 -0
  61. data/docs/architecture/parser-stage.adoc +1051 -0
  62. data/docs/architecture.adoc +354 -0
  63. data/docs/cli-reference/batch-command.adoc +616 -0
  64. data/docs/cli-reference/check-command.adoc +677 -0
  65. data/docs/cli-reference/cli-options.adoc +802 -0
  66. data/docs/cli-reference/convert-command.adoc +462 -0
  67. data/docs/cli-reference/version-command.adoc +296 -0
  68. data/docs/cli-reference.adoc +317 -0
  69. data/docs/concepts/conversion-pipeline.adoc +903 -0
  70. data/docs/concepts/coordinate-systems.adoc +836 -0
  71. data/docs/concepts/graphics-state.adoc +861 -0
  72. data/docs/concepts/path-operations.adoc +1076 -0
  73. data/docs/concepts/postscript-language.adoc +859 -0
  74. data/docs/concepts/svg-generation.adoc +937 -0
  75. data/docs/concepts.adoc +198 -0
  76. data/docs/contributing.adoc +443 -0
  77. data/docs/development.adoc +420 -0
  78. data/docs/faq.adoc +493 -0
  79. data/docs/getting-started/basic-usage.adoc +538 -0
  80. data/docs/getting-started/common-workflows.adoc +577 -0
  81. data/docs/getting-started/first-conversion.adoc +492 -0
  82. data/docs/getting-started/installation.adoc +534 -0
  83. data/docs/getting-started.adoc +94 -0
  84. data/docs/index.adoc +248 -0
  85. data/docs/optimization.adoc +196 -0
  86. data/docs/ps2svg_compatibility.adoc +149 -0
  87. data/docs/quick-reference.adoc +453 -0
  88. data/docs/sitemap.adoc +337 -0
  89. data/docs/troubleshooting.adoc +486 -0
  90. data/docs/validation.adoc +772 -0
  91. data/exe/postsvg +1 -0
  92. data/lib/postsvg/cli.rb +104 -57
  93. data/lib/postsvg/color.rb +132 -0
  94. data/lib/postsvg/errors.rb +68 -3
  95. data/lib/postsvg/format_number.rb +22 -0
  96. data/lib/postsvg/graphics_context.rb +80 -0
  97. data/lib/postsvg/graphics_stack.rb +43 -0
  98. data/lib/postsvg/model/literals/array.rb +41 -0
  99. data/lib/postsvg/model/literals/dictionary.rb +34 -0
  100. data/lib/postsvg/model/literals/hex.rb +37 -0
  101. data/lib/postsvg/model/literals/name.rb +40 -0
  102. data/lib/postsvg/model/literals/number.rb +36 -0
  103. data/lib/postsvg/model/literals/procedure.rb +41 -0
  104. data/lib/postsvg/model/literals/string.rb +30 -0
  105. data/lib/postsvg/model/literals.rb +19 -0
  106. data/lib/postsvg/model/operator.rb +58 -0
  107. data/lib/postsvg/model/operators/arithmetic.rb +264 -0
  108. data/lib/postsvg/model/operators/boolean.rb +182 -0
  109. data/lib/postsvg/model/operators/color.rb +74 -0
  110. data/lib/postsvg/model/operators/container.rb +186 -0
  111. data/lib/postsvg/model/operators/control_flow.rb +119 -0
  112. data/lib/postsvg/model/operators/device.rb +21 -0
  113. data/lib/postsvg/model/operators/dictionary.rb +118 -0
  114. data/lib/postsvg/model/operators/font.rb +121 -0
  115. data/lib/postsvg/model/operators/graphics_state.rb +84 -0
  116. data/lib/postsvg/model/operators/painting.rb +29 -0
  117. data/lib/postsvg/model/operators/path.rb +169 -0
  118. data/lib/postsvg/model/operators/stack.rb +72 -0
  119. data/lib/postsvg/model/operators/transformations.rb +103 -0
  120. data/lib/postsvg/model/operators.rb +89 -0
  121. data/lib/postsvg/model/program.rb +68 -0
  122. data/lib/postsvg/model/token.rb +43 -0
  123. data/lib/postsvg/model.rb +17 -0
  124. data/lib/postsvg/options.rb +29 -0
  125. data/lib/postsvg/renderer.rb +85 -0
  126. data/lib/postsvg/serializer.rb +325 -0
  127. data/lib/postsvg/source/ast_builder.rb +308 -0
  128. data/lib/postsvg/source/lexer.rb +322 -0
  129. data/lib/postsvg/source/operand_stack.rb +55 -0
  130. data/lib/postsvg/source.rb +21 -0
  131. data/lib/postsvg/svg/attribute_parser.rb +45 -0
  132. data/lib/postsvg/svg/clip_path_registry.rb +44 -0
  133. data/lib/postsvg/svg/document.rb +22 -0
  134. data/lib/postsvg/svg/element.rb +84 -0
  135. data/lib/postsvg/svg/elements/circle.rb +36 -0
  136. data/lib/postsvg/svg/elements/clip_path.rb +26 -0
  137. data/lib/postsvg/svg/elements/defs.rb +24 -0
  138. data/lib/postsvg/svg/elements/ellipse.rb +38 -0
  139. data/lib/postsvg/svg/elements/group.rb +37 -0
  140. data/lib/postsvg/svg/elements/image.rb +35 -0
  141. data/lib/postsvg/svg/elements/line.rb +36 -0
  142. data/lib/postsvg/svg/elements/path.rb +32 -0
  143. data/lib/postsvg/svg/elements/polygon.rb +12 -0
  144. data/lib/postsvg/svg/elements/polyline.rb +32 -0
  145. data/lib/postsvg/svg/elements/rect.rb +44 -0
  146. data/lib/postsvg/svg/elements/svg.rb +39 -0
  147. data/lib/postsvg/svg/elements/text.rb +42 -0
  148. data/lib/postsvg/svg/elements.rb +31 -0
  149. data/lib/postsvg/svg/paint.rb +34 -0
  150. data/lib/postsvg/svg/parser.rb +39 -0
  151. data/lib/postsvg/svg/path_data/command.rb +27 -0
  152. data/lib/postsvg/svg/path_data/parser.rb +82 -0
  153. data/lib/postsvg/svg/path_data.rb +17 -0
  154. data/lib/postsvg/svg/stroke.rb +31 -0
  155. data/lib/postsvg/svg/transform_list.rb +59 -0
  156. data/lib/postsvg/svg.rb +22 -0
  157. data/lib/postsvg/svg_builder.rb +249 -0
  158. data/lib/postsvg/translation/arc_converter.rb +86 -0
  159. data/lib/postsvg/translation/bounding_box.rb +59 -0
  160. data/lib/postsvg/translation/context.rb +34 -0
  161. data/lib/postsvg/translation/handler_registry.rb +38 -0
  162. data/lib/postsvg/translation/handlers/circle_handler.rb +28 -0
  163. data/lib/postsvg/translation/handlers/clip_path_handler.rb +13 -0
  164. data/lib/postsvg/translation/handlers/defs_handler.rb +15 -0
  165. data/lib/postsvg/translation/handlers/ellipse_handler.rb +31 -0
  166. data/lib/postsvg/translation/handlers/group_handler.rb +21 -0
  167. data/lib/postsvg/translation/handlers/image_handler.rb +20 -0
  168. data/lib/postsvg/translation/handlers/line_handler.rb +23 -0
  169. data/lib/postsvg/translation/handlers/open_handler.rb +18 -0
  170. data/lib/postsvg/translation/handlers/path_handler.rb +356 -0
  171. data/lib/postsvg/translation/handlers/polygon_handler.rb +33 -0
  172. data/lib/postsvg/translation/handlers/polyline_handler.rb +31 -0
  173. data/lib/postsvg/translation/handlers/rect_handler.rb +27 -0
  174. data/lib/postsvg/translation/handlers/shared.rb +110 -0
  175. data/lib/postsvg/translation/handlers/svg_handler.rb +25 -0
  176. data/lib/postsvg/translation/handlers/text_handler.rb +56 -0
  177. data/lib/postsvg/translation/handlers.rb +25 -0
  178. data/lib/postsvg/translation/ps_renderer.rb +105 -0
  179. data/lib/postsvg/translation/record_emitter.rb +35 -0
  180. data/lib/postsvg/translation.rb +17 -0
  181. data/lib/postsvg/version.rb +1 -1
  182. data/lib/postsvg/visitors/ps_visitor/arithmetic.rb +125 -0
  183. data/lib/postsvg/visitors/ps_visitor/boolean.rb +105 -0
  184. data/lib/postsvg/visitors/ps_visitor/color.rb +53 -0
  185. data/lib/postsvg/visitors/ps_visitor/common.rb +66 -0
  186. data/lib/postsvg/visitors/ps_visitor/container.rb +164 -0
  187. data/lib/postsvg/visitors/ps_visitor/control_flow.rb +110 -0
  188. data/lib/postsvg/visitors/ps_visitor/device.rb +20 -0
  189. data/lib/postsvg/visitors/ps_visitor/dictionary.rb +89 -0
  190. data/lib/postsvg/visitors/ps_visitor/font.rb +93 -0
  191. data/lib/postsvg/visitors/ps_visitor/graphics_state.rb +55 -0
  192. data/lib/postsvg/visitors/ps_visitor/painting.rb +90 -0
  193. data/lib/postsvg/visitors/ps_visitor/path.rb +112 -0
  194. data/lib/postsvg/visitors/ps_visitor/stack.rb +47 -0
  195. data/lib/postsvg/visitors/ps_visitor/transformations.rb +101 -0
  196. data/lib/postsvg/visitors/ps_visitor.rb +208 -0
  197. data/lib/postsvg/visitors.rb +9 -0
  198. data/lib/postsvg.rb +93 -59
  199. data/lychee.toml +86 -0
  200. metadata +216 -11
  201. data/postsvg.gemspec +0 -38
@@ -0,0 +1,1284 @@
1
+ = PathBuilder Class
2
+ :page-nav_order: 8
3
+ :page-parent: API Reference
4
+
5
+ == Purpose
6
+
7
+ The [`PathBuilder`](../../lib/postsvg/path_builder.rb:7) class constructs SVG path data strings from PostScript path operations. It provides a fluent interface for building complex SVG paths through incremental commands while maintaining proper formatting and coordinate tracking.
8
+
9
+ == References
10
+
11
+ * link:../index.adoc[Documentation Home]
12
+ * link:../api-reference.adoc[API Reference Overview]
13
+ * link:svg-generator.adoc[SvgGenerator Class]
14
+ * link:matrix.adoc[Matrix Class]
15
+ * link:interpreter.adoc[Interpreter Class]
16
+ * link:../architecture.adoc[Architecture Overview]
17
+
18
+ == Concepts
19
+
20
+ **SVG Path Data**:: A string representation of shapes using commands like `M` (moveto), `L` (lineto), `C` (curveto), and `Z` (closepath).
21
+
22
+ **Current Point**:: The position of the drawing cursor after the last path command, used as the starting point for the next command.
23
+
24
+ **Absolute vs Relative Commands**:: Absolute commands use fixed coordinates, while relative commands use offsets from the current point.
25
+
26
+ **Path Segment**:: An individual drawing command (move, line, curve, or close) that forms part of a complete path.
27
+
28
+ **Subpath**:: A continuous sequence of path segments that may be disconnected from other subpaths in the same path element.
29
+
30
+ == Class Overview
31
+
32
+ The [`PathBuilder`](../../lib/postsvg/path_builder.rb:7) class is defined in [`lib/postsvg/path_builder.rb`](../../lib/postsvg/path_builder.rb:1).
33
+
34
+ **Responsibilities:**
35
+
36
+ * Build SVG path data strings incrementally
37
+ * Track current drawing position
38
+ * Provide both absolute and relative path commands
39
+ * Format coordinates for clean SVG output
40
+ * Support path introspection and manipulation
41
+ * Enable path cloning and resetting
42
+
43
+ **Dependencies:**
44
+
45
+ * [`Matrix`](../../lib/postsvg/matrix.rb:5) - For coordinate transformations (imported but deprecated in favor of direct coordinate handling)
46
+
47
+ **Design Characteristics:**
48
+
49
+ * Fluent interface pattern for method chaining
50
+ * Stateful coordinate tracking
51
+ * Immutable path string parts for reliability
52
+ * Clean numeric formatting
53
+
54
+ == Class Methods
55
+
56
+ === new
57
+
58
+ Create a new PathBuilder instance.
59
+
60
+ **Syntax:**
61
+
62
+ [source,ruby]
63
+ ----
64
+ builder = Postsvg::PathBuilder.new <1>
65
+ ----
66
+ <1> Initialize an empty path builder
67
+
68
+ **Returns:**
69
+
70
+ New `PathBuilder` instance with:
71
+ * Empty path parts array
72
+ * Initial position at (0, 0)
73
+ * No last command
74
+
75
+ **Source:**
76
+
77
+ [`lib/postsvg/path_builder.rb:10-15`](../../lib/postsvg/path_builder.rb:10)
78
+
79
+ .Create path builder
80
+ [example]
81
+ ====
82
+ [source,ruby]
83
+ ----
84
+ require 'postsvg'
85
+
86
+ # Create builder
87
+ builder = Postsvg::PathBuilder.new
88
+
89
+ # Builder is ready to accept path commands
90
+ # Current position: (0, 0)
91
+ # Path data: "" (empty)
92
+ ----
93
+ ====
94
+
95
+ == Instance Methods
96
+
97
+ === move_to
98
+
99
+ Move to an absolute position without drawing.
100
+
101
+ **Syntax:**
102
+
103
+ [source,ruby]
104
+ ----
105
+ builder.move_to(x, y) <1>
106
+ ----
107
+ <1> Move drawing cursor to absolute coordinates
108
+
109
+ **Where:**
110
+
111
+ `x`:: X-coordinate in user units (Integer or Float)
112
+
113
+ `y`:: Y-coordinate in user units (Integer or Float)
114
+
115
+ **Returns:**
116
+
117
+ `nil` (modifies internal state)
118
+
119
+ **Side Effects:**
120
+
121
+ * Updates `@current_x` and `@current_y`
122
+ * Sets `@last_command` to `:move`
123
+ * Appends `"M x y"` to path parts
124
+
125
+ **Source:**
126
+
127
+ [`lib/postsvg/path_builder.rb:21-26`](../../lib/postsvg/path_builder.rb:21)
128
+
129
+ .Move to absolute position
130
+ [example]
131
+ ====
132
+ [source,ruby]
133
+ ----
134
+ builder = Postsvg::PathBuilder.new
135
+
136
+ builder.move_to(100, 50)
137
+ puts builder.to_path # → "M 100 50"
138
+
139
+ # Current position is now (100, 50)
140
+ ----
141
+ ====
142
+
143
+ .Start new subpath
144
+ [example]
145
+ ====
146
+ [source,ruby]
147
+ ----
148
+ builder = Postsvg::PathBuilder.new
149
+
150
+ # First subpath
151
+ builder.move_to(10, 10)
152
+ builder.line_to(90, 10)
153
+ builder.line_to(90, 90)
154
+ builder.close
155
+
156
+ # Second subpath
157
+ builder.move_to(20, 20)
158
+ builder.line_to(80, 20)
159
+ builder.line_to(80, 80)
160
+ builder.close
161
+
162
+ puts builder.to_path
163
+ # → "M 10 10 L 90 10 L 90 90 Z M 20 20 L 80 20 L 80 80 Z"
164
+ ----
165
+ ====
166
+
167
+ === move_to_rel
168
+
169
+ Move to a relative position without drawing.
170
+
171
+ **Syntax:**
172
+
173
+ [source,ruby]
174
+ ----
175
+ builder.move_to_rel(dx, dy) <1>
176
+ ----
177
+ <1> Move cursor relative to current position
178
+
179
+ **Where:**
180
+
181
+ `dx`:: X-offset from current position (Integer or Float)
182
+
183
+ `dy`:: Y-offset from current position (Integer or Float)
184
+
185
+ **Returns:**
186
+
187
+ `nil` (modifies internal state)
188
+
189
+ **Side Effects:**
190
+
191
+ * Sets `@last_command` to `:move`
192
+ * Appends `"m dx dy"` to path parts
193
+ * Does NOT update `@current_x` and `@current_y` (relies on SVG to track)
194
+
195
+ **Source:**
196
+
197
+ [`lib/postsvg/path_builder.rb:28-31`](../../lib/postsvg/path_builder.rb:28)
198
+
199
+ .Relative move
200
+ [example]
201
+ ====
202
+ [source,ruby]
203
+ ----
204
+ builder = Postsvg::PathBuilder.new
205
+
206
+ builder.move_to(50, 50) # Absolute: (50, 50)
207
+ builder.line_to(100, 50) # Absolute: (100, 50)
208
+ builder.move_to_rel(0, 50) # Relative: +50 in y
209
+
210
+ puts builder.to_path
211
+ # → "M 50 50 L 100 50 m 0 50"
212
+ ----
213
+ ====
214
+
215
+ === line_to
216
+
217
+ Draw a line to an absolute position.
218
+
219
+ **Syntax:**
220
+
221
+ [source,ruby]
222
+ ----
223
+ builder.line_to(x, y) <1>
224
+ ----
225
+ <1> Draw line from current position to absolute coordinates
226
+
227
+ **Where:**
228
+
229
+ `x`:: Target X-coordinate in user units (Integer or Float)
230
+
231
+ `y`:: Target Y-coordinate in user units (Integer or Float)
232
+
233
+ **Returns:**
234
+
235
+ `nil` (modifies internal state)
236
+
237
+ **Side Effects:**
238
+
239
+ * Updates `@current_x` and `@current_y` to target position
240
+ * Sets `@last_command` to `:line`
241
+ * Appends `"L x y"` to path parts
242
+
243
+ **Source:**
244
+
245
+ [`lib/postsvg/path_builder.rb:33-38`](../../lib/postsvg/path_builder.rb:33)
246
+
247
+ .Draw simple line
248
+ [example]
249
+ ====
250
+ [source,ruby]
251
+ ----
252
+ builder = Postsvg::PathBuilder.new
253
+
254
+ builder.move_to(0, 0)
255
+ builder.line_to(100, 0)
256
+ builder.line_to(100, 100)
257
+
258
+ puts builder.to_path
259
+ # → "M 0 0 L 100 0 L 100 100"
260
+ ----
261
+ ====
262
+
263
+ .Draw polyline
264
+ [example]
265
+ ====
266
+ [source,ruby]
267
+ ----
268
+ points = [[10, 10], [50, 30], [90, 10], [90, 90], [10, 90]]
269
+
270
+ builder = Postsvg::PathBuilder.new
271
+ builder.move_to(*points.first)
272
+
273
+ points[1..-1].each do |x, y|
274
+ builder.line_to(x, y)
275
+ end
276
+
277
+ puts builder.to_path
278
+ # → "M 10 10 L 50 30 L 90 10 L 90 90 L 10 90"
279
+ ----
280
+ ====
281
+
282
+ === line_to_rel
283
+
284
+ Draw a line to a relative position.
285
+
286
+ **Syntax:**
287
+
288
+ [source,ruby]
289
+ ----
290
+ builder.line_to_rel(dx, dy) <1>
291
+ ----
292
+ <1> Draw line using relative offset
293
+
294
+ **Where:**
295
+
296
+ `dx`:: X-offset from current position (Integer or Float)
297
+
298
+ `dy`:: Y-offset from current position (Integer or Float)
299
+
300
+ **Returns:**
301
+
302
+ `nil` (modifies internal state)
303
+
304
+ **Side Effects:**
305
+
306
+ * Sets `@last_command` to `:line`
307
+ * Appends `"l dx dy"` to path parts
308
+
309
+ **Source:**
310
+
311
+ [`lib/postsvg/path_builder.rb:40-43`](../../lib/postsvg/path_builder.rb:40)
312
+
313
+ .Draw with relative coordinates
314
+ [example]
315
+ ====
316
+ [source,ruby]
317
+ ----
318
+ builder = Postsvg::PathBuilder.new
319
+
320
+ builder.move_to(50, 50)
321
+ builder.line_to_rel(50, 0) # Right 50
322
+ builder.line_to_rel(0, 50) # Down 50
323
+ builder.line_to_rel(-50, 0) # Left 50
324
+ builder.line_to_rel(0, -50) # Up 50
325
+
326
+ puts builder.to_path
327
+ # → "M 50 50 l 50 0 l 0 50 l -50 0 l 0 -50"
328
+ # Creates a square using relative coordinates
329
+ ----
330
+ ====
331
+
332
+ === curve_to
333
+
334
+ Draw a cubic Bézier curve to an absolute position.
335
+
336
+ **Syntax:**
337
+
338
+ [source,ruby]
339
+ ----
340
+ builder.curve_to(x1, y1, x2, y2, x, y) <1>
341
+ ----
342
+ <1> Draw curve with two control points and end point
343
+
344
+ **Where:**
345
+
346
+ `x1`, `y1`:: First control point coordinates (Integer or Float)
347
+
348
+ `x2`, `y2`:: Second control point coordinates (Integer or Float)
349
+
350
+ `x`, `y`:: End point coordinates (Integer or Float)
351
+
352
+ **Returns:**
353
+
354
+ `nil` (modifies internal state)
355
+
356
+ **Side Effects:**
357
+
358
+ * Updates `@current_x` and `@current_y` to end point (x, y)
359
+ * Sets `@last_command` to `:curve`
360
+ * Appends `"C x1 y1 x2 y2 x y"` to path parts
361
+
362
+ **Source:**
363
+
364
+ [`lib/postsvg/path_builder.rb:45-52`](../../lib/postsvg/path_builder.rb:45)
365
+
366
+ .Draw simple curve
367
+ [example]
368
+ ====
369
+ [source,ruby]
370
+ ----
371
+ builder = Postsvg::PathBuilder.new
372
+
373
+ # S-curve
374
+ builder.move_to(10, 80)
375
+ builder.curve_to(
376
+ 40, 10, # First control point
377
+ 60, 10, # Second control point
378
+ 90, 80 # End point
379
+ )
380
+
381
+ puts builder.to_path
382
+ # → "M 10 80 C 40 10 60 10 90 80"
383
+ ----
384
+ ====
385
+
386
+ .Draw smooth wave
387
+ [example]
388
+ ====
389
+ [source,ruby]
390
+ ----
391
+ builder = Postsvg::PathBuilder.new
392
+ builder.move_to(0, 50)
393
+
394
+ # Multiple connected curves for wave pattern
395
+ [[20, 20, 40, 20, 50, 50],
396
+ [60, 80, 80, 80, 100, 50],
397
+ [120, 20, 140, 20, 150, 50]].each do |params|
398
+ builder.curve_to(*params)
399
+ end
400
+
401
+ puts builder.to_path
402
+ # → "M 0 50 C 20 20 40 20 50 50 C 60 80 80 80 100 50 C 120 20 140 20 150 50"
403
+ ----
404
+ ====
405
+
406
+ === curve_to_rel
407
+
408
+ Draw a cubic Bézier curve using relative coordinates.
409
+
410
+ **Syntax:**
411
+
412
+ [source,ruby]
413
+ ----
414
+ builder.curve_to_rel(dx1, dy1, dx2, dy2, dx, dy) <1>
415
+ ----
416
+ <1> Draw curve with relative control points and end point
417
+
418
+ **Where:**
419
+
420
+ `dx1`, `dy1`:: First control point offset from current position
421
+
422
+ `dx2`, `dy2`:: Second control point offset from current position
423
+
424
+ `dx`, `dy`:: End point offset from current position
425
+
426
+ **Returns:**
427
+
428
+ `nil` (modifies internal state)
429
+
430
+ **Source:**
431
+
432
+ [`lib/postsvg/path_builder.rb:54-59`](../../lib/postsvg/path_builder.rb:54)
433
+
434
+ .Relative curve drawing
435
+ [example]
436
+ ====
437
+ [source,ruby]
438
+ ----
439
+ builder = Postsvg::PathBuilder.new
440
+ builder.move_to(50, 50)
441
+
442
+ # Draw curve relative to current position
443
+ builder.curve_to_rel(
444
+ 10, -20, # First control: (60, 30)
445
+ 20, -20, # Second control: (70, 30)
446
+ 30, 0 # End: (80, 50)
447
+ )
448
+
449
+ puts builder.to_path
450
+ # → "M 50 50 c 10 -20 20 -20 30 0"
451
+ ----
452
+ ====
453
+
454
+ === ellipse_to
455
+
456
+ Draw an elliptical arc to an absolute position.
457
+
458
+ **Syntax:**
459
+
460
+ [source,ruby]
461
+ ----
462
+ builder.ellipse_to(rx, ry, rotation, large_arc, sweep, x, y) <1>
463
+ ----
464
+ <1> Draw elliptical arc with specified parameters
465
+
466
+ **Where:**
467
+
468
+ `rx`:: X-radius of the ellipse (Float)
469
+
470
+ `ry`:: Y-radius of the ellipse (Float)
471
+
472
+ `rotation`:: Rotation angle of the ellipse in degrees (Float)
473
+
474
+ `large_arc`:: Use large arc (0 or 1)
475
+ * `0` - Arc spans ≤ 180°
476
+ * `1` - Arc spans > 180°
477
+
478
+ `sweep`:: Direction of arc (0 or 1)
479
+ * `0` - Counter-clockwise
480
+ * `1` - Clockwise
481
+
482
+ `x`, `y`:: End point coordinates (Integer or Float)
483
+
484
+ **Returns:**
485
+
486
+ `nil` (modifies internal state)
487
+
488
+ **Side Effects:**
489
+
490
+ * Updates `@current_x` and `@current_y` to end point
491
+ * Sets `@last_command` to `:arc`
492
+ * Appends `"A rx ry rotation large_arc sweep x y"` to path parts
493
+
494
+ **Source:**
495
+
496
+ [`lib/postsvg/path_builder.rb:61-67`](../../lib/postsvg/path_builder.rb:61)
497
+
498
+ .Draw circular arc
499
+ [example]
500
+ ====
501
+ [source,ruby]
502
+ ----
503
+ builder = Postsvg::PathBuilder.new
504
+
505
+ # Draw semicircle
506
+ builder.move_to(10, 50)
507
+ builder.ellipse_to(
508
+ 40, 40, # Radii (same for circle)
509
+ 0, # No rotation
510
+ 0, # Small arc
511
+ 1, # Clockwise
512
+ 90, 50 # End point
513
+ )
514
+
515
+ puts builder.to_path
516
+ # → "M 10 50 A 40 40 0 0 1 90 50"
517
+ ----
518
+ ====
519
+
520
+ .Draw complex arc
521
+ [example]
522
+ ====
523
+ [source,ruby]
524
+ ----
525
+ builder = Postsvg::PathBuilder.new
526
+
527
+ # Draw rotated elliptical arc
528
+ builder.move_to(100, 100)
529
+ builder.ellipse_to(
530
+ 50, 30, # Ellipse radii
531
+ 45, # 45° rotation
532
+ 1, # Large arc (>180°)
533
+ 0, # Counter-clockwise
534
+ 200, 200 # End point
535
+ )
536
+
537
+ puts builder.to_path
538
+ # → "M 100 100 A 50 30 45 1 0 200 200"
539
+ ----
540
+ ====
541
+
542
+ === close
543
+
544
+ Close the current subpath by drawing a line to the starting point.
545
+
546
+ **Syntax:**
547
+
548
+ [source,ruby]
549
+ ----
550
+ builder.close <1>
551
+ ----
552
+ <1> Close current subpath
553
+
554
+ **Returns:**
555
+
556
+ `nil` (modifies internal state)
557
+
558
+ **Side Effects:**
559
+
560
+ * Sets `@last_command` to `:close`
561
+ * Appends `"Z"` to path parts
562
+ * Does not update current position (SVG handles this)
563
+
564
+ **Source:**
565
+
566
+ [`lib/postsvg/path_builder.rb:69-72`](../../lib/postsvg/path_builder.rb:69)
567
+
568
+ .Close simple shape
569
+ [example]
570
+ ====
571
+ [source,ruby]
572
+ ----
573
+ builder = Postsvg::PathBuilder.new
574
+
575
+ # Triangle
576
+ builder.move_to(50, 10)
577
+ builder.line_to(90, 90)
578
+ builder.line_to(10, 90)
579
+ builder.close # Completes triangle
580
+
581
+ puts builder.to_path
582
+ # → "M 50 10 L 90 90 L 10 90 Z"
583
+ ----
584
+ ====
585
+
586
+ .Multiple closed subpaths
587
+ [example]
588
+ ====
589
+ [source,ruby]
590
+ ----
591
+ builder = Postsvg::PathBuilder.new
592
+
593
+ # First shape
594
+ builder.move_to(10, 10)
595
+ builder.line_to(40, 10)
596
+ builder.line_to(40, 40)
597
+ builder.close
598
+
599
+ # Second shape
600
+ builder.move_to(60, 60)
601
+ builder.line_to(90, 60)
602
+ builder.line_to(90, 90)
603
+ builder.close
604
+
605
+ puts builder.to_path
606
+ # → "M 10 10 L 40 10 L 40 40 Z M 60 60 L 90 60 L 90 90 Z"
607
+ ----
608
+ ====
609
+
610
+ === to_path
611
+
612
+ Convert accumulated path commands to SVG path data string.
613
+
614
+ **Syntax:**
615
+
616
+ [source,ruby]
617
+ ----
618
+ path_data = builder.to_path <1>
619
+ ----
620
+ <1> Generate SVG path data string
621
+
622
+ **Returns:**
623
+
624
+ String containing space-separated path commands (e.g., `"M 10 10 L 50 50 Z"`)
625
+
626
+ **Source:**
627
+
628
+ [`lib/postsvg/path_builder.rb:74-76`](../../lib/postsvg/path_builder.rb:74)
629
+
630
+ .Generate path data
631
+ [example]
632
+ ====
633
+ [source,ruby]
634
+ ----
635
+ builder = Postsvg::PathBuilder.new
636
+ builder.move_to(0, 0)
637
+ builder.line_to(100, 100)
638
+
639
+ path_data = builder.to_path
640
+ puts path_data # → "M 0 0 L 100 100"
641
+
642
+ # Use in SVG
643
+ svg = <<~SVG
644
+ <svg xmlns="http://www.w3.org/2000/svg">
645
+ <path d="#{path_data}" stroke="black" fill="none"/>
646
+ </svg>
647
+ SVG
648
+ ----
649
+ ====
650
+
651
+ === length
652
+
653
+ Get the number of path command parts.
654
+
655
+ **Syntax:**
656
+
657
+ [source,ruby]
658
+ ----
659
+ count = builder.length <1>
660
+ ----
661
+ <1> Count path parts
662
+
663
+ **Returns:**
664
+
665
+ Integer count of path command parts
666
+
667
+ **Source:**
668
+
669
+ [`lib/postsvg/path_builder.rb:78-80`](../../lib/postsvg/path_builder.rb:78)
670
+
671
+ .Check path length
672
+ [example]
673
+ ====
674
+ [source,ruby]
675
+ ----
676
+ builder = Postsvg::PathBuilder.new
677
+
678
+ puts builder.length # → 0
679
+
680
+ builder.move_to(10, 10)
681
+ puts builder.length # → 1
682
+
683
+ builder.line_to(50, 50)
684
+ puts builder.length # → 2
685
+
686
+ builder.line_to(90, 10)
687
+ puts builder.length # → 3
688
+ ----
689
+ ====
690
+
691
+ === empty?
692
+
693
+ Check if the path is empty.
694
+
695
+ **Syntax:**
696
+
697
+ [source,ruby]
698
+ ----
699
+ is_empty = builder.empty? <1>
700
+ ----
701
+ <1> Check for empty path
702
+
703
+ **Returns:**
704
+
705
+ Boolean (`true` if no path commands, `false` otherwise)
706
+
707
+ **Source:**
708
+
709
+ [`lib/postsvg/path_builder.rb:82-84`](../../lib/postsvg/path_builder.rb:82)
710
+
711
+ .Check if empty
712
+ [example]
713
+ ====
714
+ [source,ruby]
715
+ ----
716
+ builder = Postsvg::PathBuilder.new
717
+
718
+ puts builder.empty? # → true
719
+
720
+ builder.move_to(10, 10)
721
+ puts builder.empty? # → false
722
+ ----
723
+ ====
724
+
725
+ === clear
726
+
727
+ Clear all path commands and reset state.
728
+
729
+ **Syntax:**
730
+
731
+ [source,ruby]
732
+ ----
733
+ builder.clear <1>
734
+ ----
735
+ <1> Reset path builder to initial state
736
+
737
+ **Returns:**
738
+
739
+ `nil`
740
+
741
+ **Side Effects:**
742
+
743
+ * Clears `@parts` array
744
+ * Resets `@last_command` to `nil`
745
+ * Resets `@current_x` and `@current_y` to 0.0
746
+
747
+ **Source:**
748
+
749
+ [`lib/postsvg/path_builder.rb:86-91`](../../lib/postsvg/path_builder.rb:86)
750
+
751
+ .Clear and reuse builder
752
+ [example]
753
+ ====
754
+ [source,ruby]
755
+ ----
756
+ builder = Postsvg::PathBuilder.new
757
+ builder.move_to(10, 10)
758
+ builder.line_to(50, 50)
759
+
760
+ puts builder.to_path # → "M 10 10 L 50 50"
761
+
762
+ # Clear for reuse
763
+ builder.clear
764
+ puts builder.empty? # → true
765
+ puts builder.to_path # → ""
766
+
767
+ # Build new path
768
+ builder.move_to(0, 0)
769
+ builder.line_to(100, 100)
770
+ puts builder.to_path # → "M 0 0 L 100 100"
771
+ ----
772
+ ====
773
+
774
+ === reset
775
+
776
+ Create a new PathBuilder instance.
777
+
778
+ **Syntax:**
779
+
780
+ [source,ruby]
781
+ ----
782
+ new_builder = builder.reset <1>
783
+ ----
784
+ <1> Create fresh PathBuilder
785
+
786
+ **Returns:**
787
+
788
+ New `PathBuilder` instance (does NOT modify original)
789
+
790
+ **Source:**
791
+
792
+ [`lib/postsvg/path_builder.rb:93-95`](../../lib/postsvg/path_builder.rb:93)
793
+
794
+ NOTE: This method creates a NEW instance rather than resetting the current one. Use `clear` to reset the current builder.
795
+
796
+ .Reset vs Clear
797
+ [example]
798
+ ====
799
+ [source,ruby]
800
+ ----
801
+ builder = Postsvg::PathBuilder.new
802
+ builder.move_to(10, 10)
803
+
804
+ # reset creates NEW instance
805
+ new_builder = builder.reset
806
+ puts builder.to_path # → "M 10 10" (unchanged)
807
+ puts new_builder.to_path # → "" (new empty builder)
808
+
809
+ # clear modifies current instance
810
+ builder.clear
811
+ puts builder.to_path # → "" (cleared)
812
+ ----
813
+ ====
814
+
815
+ === has_disconnected_subpath?
816
+
817
+ Check if path has a disconnected subpath.
818
+
819
+ **Syntax:**
820
+
821
+ [source,ruby]
822
+ ----
823
+ is_disconnected = builder.has_disconnected_subpath? <1>
824
+ ----
825
+ <1> Check for disconnected subpath
826
+
827
+ **Returns:**
828
+
829
+ Boolean:
830
+ * `true` - Path has data and last command is NOT move
831
+ * `false` - Path is empty OR last command is move
832
+
833
+ **Source:**
834
+
835
+ [`lib/postsvg/path_builder.rb:98-100`](../../lib/postsvg/path_builder.rb:98)
836
+
837
+ **Use Case:**
838
+
839
+ Detect when a new subpath should be started (typically after a paint operation).
840
+
841
+ .Check for disconnected subpath
842
+ [example]
843
+ ====
844
+ [source,ruby]
845
+ ----
846
+ builder = Postsvg::PathBuilder.new
847
+
848
+ puts builder.has_disconnected_subpath? # → false (empty)
849
+
850
+ builder.move_to(10, 10)
851
+ puts builder.has_disconnected_subpath? # → false (last is move)
852
+
853
+ builder.line_to(50, 50)
854
+ puts builder.has_disconnected_subpath? # → true (has data, last is line)
855
+
856
+ builder.move_to(100, 100)
857
+ puts builder.has_disconnected_subpath? # → false (last is move again)
858
+ ----
859
+ ====
860
+
861
+ === has_content?
862
+
863
+ Check if path has actual drawing commands (not just moveto).
864
+
865
+ **Syntax:**
866
+
867
+ [source,ruby]
868
+ ----
869
+ has_drawing = builder.has_content? <1>
870
+ ----
871
+ <1> Check for meaningful content
872
+
873
+ **Returns:**
874
+
875
+ Boolean:
876
+ * `true` - Path has more than one command part
877
+ * `false` - Path is empty or has only one command
878
+
879
+ **Source:**
880
+
881
+ [`lib/postsvg/path_builder.rb:103-105`](../../lib/postsvg/path_builder.rb:103)
882
+
883
+ .Check for content
884
+ [example]
885
+ ====
886
+ [source,ruby]
887
+ ----
888
+ builder = Postsvg::PathBuilder.new
889
+
890
+ puts builder.has_content? # → false (empty)
891
+
892
+ builder.move_to(10, 10)
893
+ puts builder.has_content? # → false (only move)
894
+
895
+ builder.line_to(50, 50)
896
+ puts builder.has_content? # → true (has drawing command)
897
+ ----
898
+ ====
899
+
900
+ === current_point
901
+
902
+ Get the current drawing position.
903
+
904
+ **Syntax:**
905
+
906
+ [source,ruby]
907
+ ----
908
+ x, y = builder.current_point <1>
909
+ ----
910
+ <1> Get current coordinates
911
+
912
+ **Returns:**
913
+
914
+ Array `[x, y]` containing current position coordinates (Float)
915
+
916
+ **Source:**
917
+
918
+ [`lib/postsvg/path_builder.rb:107-109`](../../lib/postsvg/path_builder.rb:107)
919
+
920
+ .Track current position
921
+ [example]
922
+ ====
923
+ [source,ruby]
924
+ ----
925
+ builder = Postsvg::PathBuilder.new
926
+
927
+ x, y = builder.current_point
928
+ puts "Start: (#{x}, #{y})" # → "Start: (0.0, 0.0)"
929
+
930
+ builder.move_to(50, 75)
931
+ x, y = builder.current_point
932
+ puts "After move: (#{x}, #{y})" # → "After move: (50, 75)"
933
+
934
+ builder.line_to(100, 125)
935
+ x, y = builder.current_point
936
+ puts "After line: (#{x}, #{y})" # → "After line: (100, 125)"
937
+ ----
938
+ ====
939
+
940
+ === dup
941
+
942
+ Create a deep copy of the PathBuilder.
943
+
944
+ **Syntax:**
945
+
946
+ [source,ruby]
947
+ ----
948
+ copy = builder.dup <1>
949
+ ----
950
+ <1> Duplicate path builder
951
+
952
+ **Returns:**
953
+
954
+ New `PathBuilder` instance with copied state:
955
+ * Duplicated `@parts` array
956
+ * Copied `@last_command`
957
+ * Copied `@current_x` and `@current_y`
958
+
959
+ **Source:**
960
+
961
+ [`lib/postsvg/path_builder.rb:111-118`](../../lib/postsvg/path_builder.rb:111)
962
+
963
+ .Duplicate builder
964
+ [example]
965
+ ====
966
+ [source,ruby]
967
+ ----
968
+ builder = Postsvg::PathBuilder.new
969
+ builder.move_to(10, 10)
970
+ builder.line_to(50, 50)
971
+
972
+ # Create independent copy
973
+ copy = builder.dup
974
+
975
+ # Modify copy
976
+ copy.line_to(90, 90)
977
+
978
+ puts builder.to_path # → "M 10 10 L 50 50"
979
+ puts copy.to_path # → "M 10 10 L 50 50 L 90 90"
980
+ ----
981
+ ====
982
+
983
+ == Attributes
984
+
985
+ === parts (read-only)
986
+
987
+ Access the array of path command parts.
988
+
989
+ **Syntax:**
990
+
991
+ [source,ruby]
992
+ ----
993
+ parts_array = builder.parts <1>
994
+ ----
995
+ <1> Get path parts array
996
+
997
+ **Returns:**
998
+
999
+ Array of strings, each representing one path command
1000
+
1001
+ **Source:**
1002
+
1003
+ [`lib/postsvg/path_builder.rb:8`](../../lib/postsvg/path_builder.rb:8)
1004
+
1005
+ .Inspect path parts
1006
+ [example]
1007
+ ====
1008
+ [source,ruby]
1009
+ ----
1010
+ builder = Postsvg::PathBuilder.new
1011
+ builder.move_to(10, 10)
1012
+ builder.line_to(50, 50)
1013
+ builder.close
1014
+
1015
+ puts builder.parts.inspect
1016
+ # → ["M 10 10", "L 50 50", "Z"]
1017
+
1018
+ puts builder.parts.join(" ")
1019
+ # → "M 10 10 L 50 50 Z" (same as to_path)
1020
+ ----
1021
+ ====
1022
+
1023
+ == Usage Patterns
1024
+
1025
+ === Pattern 1: Simple Shape Construction
1026
+
1027
+ [source,ruby]
1028
+ ----
1029
+ require 'postsvg'
1030
+
1031
+ def build_rectangle(x, y, width, height)
1032
+ builder = Postsvg::PathBuilder.new
1033
+
1034
+ builder.move_to(x, y)
1035
+ builder.line_to(x + width, y)
1036
+ builder.line_to(x + width, y + height)
1037
+ builder.line_to(x, y + height)
1038
+ builder.close
1039
+
1040
+ builder.to_path
1041
+ end
1042
+
1043
+ rect_path = build_rectangle(10, 10, 80, 60)
1044
+ puts rect_path # → "M 10 10 L 90 10 L 90 70 L 10 70 Z"
1045
+ ----
1046
+
1047
+ === Pattern 2: Reusable Builder
1048
+
1049
+ [source,ruby]
1050
+ ----
1051
+ require 'postsvg'
1052
+
1053
+ class ShapeBuilder
1054
+ def initialize
1055
+ @builder = Postsvg::PathBuilder.new
1056
+ end
1057
+
1058
+ def add_circle(cx, cy, r, segments = 16)
1059
+ @builder.move_to(cx + r, cy)
1060
+
1061
+ (1..segments).each do |i|
1062
+ angle = (i * 2 * Math::PI) / segments
1063
+ x = cx + r * Math.cos(angle)
1064
+ y = cy + r * Math.sin(angle)
1065
+ @builder.line_to(x, y)
1066
+ end
1067
+
1068
+ @builder.close
1069
+ self # Enable chaining
1070
+ end
1071
+
1072
+ def to_path
1073
+ @builder.to_path
1074
+ end
1075
+
1076
+ def clear
1077
+ @builder.clear
1078
+ self
1079
+ end
1080
+ end
1081
+
1082
+ # Usage
1083
+ shapes = ShapeBuilder.new
1084
+ shapes.add_circle(50, 50, 30)
1085
+ puts shapes.to_path
1086
+ ----
1087
+
1088
+ === Pattern 3: Incremental Path Construction
1089
+
1090
+ [source,ruby]
1091
+ ----
1092
+ require 'postsvg'
1093
+
1094
+ class IncrementalPath
1095
+ def initialize
1096
+ @builder = Postsvg::PathBuilder.new
1097
+ @started = false
1098
+ end
1099
+
1100
+ def add_point(x, y)
1101
+ if @started
1102
+ @builder.line_to(x, y)
1103
+ else
1104
+ @builder.move_to(x, y)
1105
+ @started = true
1106
+ end
1107
+ self
1108
+ end
1109
+
1110
+ def close_path
1111
+ @builder.close if @started
1112
+ @started = false
1113
+ self
1114
+ end
1115
+
1116
+ def to_path
1117
+ @builder.to_path
1118
+ end
1119
+ end
1120
+
1121
+ # Usage - build path from data points
1122
+ path = IncrementalPath.new
1123
+ [[10, 10], [50, 30], [90, 10], [70, 50]].each do |x, y|
1124
+ path.add_point(x, y)
1125
+ end
1126
+ path.close_path
1127
+
1128
+ puts path.to_path
1129
+ ----
1130
+
1131
+ === Pattern 4: Complex Shape Library
1132
+
1133
+ [source,ruby]
1134
+ ----
1135
+ require 'postsvg'
1136
+
1137
+ module Shapes
1138
+ def self.star(cx, cy, outer_r, inner_r, points = 5)
1139
+ builder = Postsvg::PathBuilder.new
1140
+
1141
+ points.times do |i|
1142
+ # Outer point
1143
+ angle = (i * 2 * Math::PI) / points - Math::PI / 2
1144
+ x = cx + outer_r * Math.cos(angle)
1145
+ y = cy + outer_r * Math.sin(angle)
1146
+
1147
+ if i == 0
1148
+ builder.move_to(x, y)
1149
+ else
1150
+ builder.line_to(x, y)
1151
+ end
1152
+
1153
+ # Inner point
1154
+ angle += Math::PI / points
1155
+ x = cx + inner_r * Math.cos(angle)
1156
+ y = cy + inner_r * Math.sin(angle)
1157
+ builder.line_to(x, y)
1158
+ end
1159
+
1160
+ builder.close
1161
+ builder.to_path
1162
+ end
1163
+
1164
+ def self.rounded_rectangle(x, y, w, h, r)
1165
+ builder = Postsvg::PathBuilder.new
1166
+
1167
+ builder.move_to(x + r, y)
1168
+ builder.line_to(x + w - r, y)
1169
+ builder.ellipse_to(r, r, 0, 0, 1, x + w, y + r)
1170
+ builder.line_to(x + w, y + h - r)
1171
+ builder.ellipse_to(r, r, 0, 0, 1, x + w - r, y + h)
1172
+ builder.line_to(x + r, y + h)
1173
+ builder.ellipse_to(r, r, 0, 0, 1, x, y + h - r)
1174
+ builder.line_to(x, y + r)
1175
+ builder.ellipse_to(r, r, 0, 0, 1, x + r, y)
1176
+ builder.close
1177
+
1178
+ builder.to_path
1179
+ end
1180
+ end
1181
+
1182
+ # Usage
1183
+ star_path = Shapes.star(100, 100, 50, 25, 5)
1184
+ rounded_rect = Shapes.rounded_rectangle(10, 10, 180, 80, 10)
1185
+ ----
1186
+
1187
+ == Thread Safety
1188
+
1189
+ The `PathBuilder` class is **not thread-safe** due to mutable internal state. Each thread should have its own instance.
1190
+
1191
+ .Correct multi-threaded usage
1192
+ [example]
1193
+ ====
1194
+ [source,ruby]
1195
+ ----
1196
+ # Bad: Sharing builder across threads
1197
+ builder = Postsvg::PathBuilder.new
1198
+ threads = shapes.map do |shape|
1199
+ Thread.new do
1200
+ shape.points.each { |x, y| builder.line_to(x, y) }
1201
+ end
1202
+ end
1203
+ # RACE CONDITION!
1204
+
1205
+ # Good: Each thread creates own builder
1206
+ threads = shapes.map do |shape|
1207
+ Thread.new do
1208
+ builder = Postsvg::PathBuilder.new
1209
+ builder.move_to(*shape.points.first)
1210
+ shape.points[1..-1].each { |x, y| builder.line_to(x, y) }
1211
+ builder.to_path
1212
+ end
1213
+ end
1214
+
1215
+ paths = threads.map(&:value)
1216
+ ----
1217
+ ====
1218
+
1219
+ == Performance Considerations
1220
+
1221
+ **Time Complexity:**
1222
+
1223
+ * Adding command: O(1) - appends to array
1224
+ * `to_path`: O(n) where n = number of parts (join operation)
1225
+ * `dup`: O(n) where n = number of parts (array duplication)
1226
+
1227
+ **Space Complexity:**
1228
+
1229
+ * Memory: O(n) where n = number of path commands
1230
+ * Each command stored as formatted string
1231
+
1232
+ **Optimization Tips:**
1233
+
1234
+ 1. **Reuse builders**: Use `clear` instead of creating new instances
1235
+ 2. **Minimize to_path calls**: Generate path data only when needed
1236
+ 3. **Use relative commands**: Can produce shorter path strings
1237
+ 4. **Avoid unnecessary precision**: num_fmt handles this automatically
1238
+
1239
+ .Performance measurement
1240
+ [example]
1241
+ ====
1242
+ [source,ruby]
1243
+ ----
1244
+ require 'postsvg'
1245
+ require 'benchmark'
1246
+
1247
+ # Test path building performance
1248
+ builder = Postsvg::PathBuilder.new
1249
+
1250
+ time = Benchmark.measure do
1251
+ 1000.times do |i|
1252
+ builder.move_to(i * 10, i * 5)
1253
+ builder.line_to(i * 10 + 50, i * 5 + 50)
1254
+ builder.close
1255
+ end
1256
+ end
1257
+
1258
+ puts "Built #{builder.length} commands in #{'%.3f' % time.real}s"
1259
+ puts "Rate: #{(builder.length / time.real).to_i} commands/sec"
1260
+
1261
+ gen_time = Benchmark.measure do
1262
+ @path = builder.to_path
1263
+ end
1264
+
1265
+ puts "Generated path in #{'%.3f' % gen_time.real}s"
1266
+ puts "Path length: #{@path.length} characters"
1267
+ ----
1268
+ ====
1269
+
1270
+ == Next Steps
1271
+
1272
+ * Learn about link:svg-generator.adoc[SvgGenerator] for complete SVG document generation
1273
+ * Review link:matrix.adoc[Matrix Class] for coordinate transformations
1274
+ * See link:interpreter.adoc[Interpreter] for PostScript execution
1275
+ * Check link:../architecture.adoc[Architecture] for system design
1276
+
1277
+ == Bibliography
1278
+
1279
+ * link:svg-generator.adoc[SvgGenerator Documentation]
1280
+ * link:matrix.adoc[Matrix Documentation]
1281
+ * link:interpreter.adoc[Interpreter Documentation]
1282
+ * link:../architecture.adoc[Architecture Overview]
1283
+ * link:https://www.w3.org/TR/SVG2/paths.html[W3C SVG Paths Specification]
1284
+ * link:https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths[MDN SVG Path Tutorial]