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,1179 @@
1
+ = Matrix Class
2
+ :page-nav_order: 9
3
+ :page-parent: API Reference
4
+
5
+ == Purpose
6
+
7
+ The [`Matrix`](../../lib/postsvg/matrix.rb:6) class implements affine transformation matrices for PostScript coordinate transformations. It provides methods for translation, scaling, rotation, skewing, and matrix composition following the PostScript transformation model.
8
+
9
+ == References
10
+
11
+ * link:../index.adoc[Documentation Home]
12
+ * link:../api-reference.adoc[API Reference Overview]
13
+ * link:path-builder.adoc[PathBuilder Class]
14
+ * link:graphics-state.adoc[GraphicsState Class]
15
+ * link:interpreter.adoc[Interpreter Class]
16
+ * link:../architecture.adoc[Architecture Overview]
17
+
18
+ == Concepts
19
+
20
+ **Affine Transformation**:: A geometric transformation that preserves points, straight lines, and planes. Affine transformations include translation, scaling, rotation, reflection, and shearing.
21
+
22
+ **Transformation Matrix**:: A mathematical representation of affine transformations using a 3×3 matrix (simplified to 6 values in 2D).
23
+
24
+ **Matrix Composition**:: Combining multiple transformations by multiplying their matrices to produce a single composite transformation.
25
+
26
+ **PostScript Matrix Format**:: The 6-element array `[a b c d e f]` representing a transformation matrix where:
27
+ * `[a c e]` is the first row
28
+ * `[b d f]` is the second row
29
+ * `[0 0 1]` is the implicit third row
30
+
31
+ **Coordinate Space**:: The reference frame in which points are defined. Transformations convert points between different coordinate spaces.
32
+
33
+ == Class Overview
34
+
35
+ The [`Matrix`](../../lib/postsvg/matrix.rb:6) class is defined in [`lib/postsvg/matrix.rb`](../../lib/postsvg/matrix.rb:1).
36
+
37
+ **Responsibilities:**
38
+
39
+ * Represent 2D affine transformation matrices
40
+ * Perform matrix multiplication for transformation composition
41
+ * Apply transformations (translate, scale, rotate, skew)
42
+ * Transform points from one coordinate system to another
43
+ * Decompose matrices into component transformations
44
+ * Invert matrices for reverse transformations
45
+ * Detect identity matrices
46
+
47
+ **Mathematical Foundation:**
48
+
49
+ The matrix represents transformations using the form:
50
+
51
+ ```
52
+ ┌ ┐ ┌ ┐ ┌ ┐
53
+ │ x'│ │ a c e│ │x│
54
+ │ y'│ = │ b d f│ │y│
55
+ │ 1 │ │ 0 0 1│ │1│
56
+ └ ┘ └ ┘ └ ┘
57
+ ```
58
+
59
+ Where:
60
+ * `(x, y)` is the original point
61
+ * `(x', y')` is the transformed point
62
+ * `[a b c d e f]` are the matrix coefficients
63
+
64
+ **Transformation Equations:**
65
+
66
+ ```
67
+ x' = a*x + c*y + e
68
+ y' = b*x + d*y + f
69
+ ```
70
+
71
+ == Class Methods
72
+
73
+ === new
74
+
75
+ Create a new Matrix instance.
76
+
77
+ **Syntax:**
78
+
79
+ [source,ruby]
80
+ ----
81
+ matrix = Postsvg::Matrix.new(
82
+ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0
83
+ ) <1>
84
+ ----
85
+ <1> Initialize matrix with optional coefficients (defaults to identity)
86
+
87
+ **Where:**
88
+
89
+ `a`:: (Optional) Horizontal scaling/rotation component (default: 1)
90
+
91
+ `b`:: (Optional) Vertical skew/rotation component (default: 0)
92
+
93
+ `c`:: (Optional) Horizontal skew/rotation component (default: 0)
94
+
95
+ `d`:: (Optional) Vertical scaling/rotation component (default: 1)
96
+
97
+ `e`:: (Optional) Horizontal translation component (default: 0)
98
+
99
+ `f`:: (Optional) Vertical translation component (default: 0)
100
+
101
+ **Returns:**
102
+
103
+ New `Matrix` instance with specified coefficients
104
+
105
+ **Source:**
106
+
107
+ [`lib/postsvg/matrix.rb:9-16`](../../lib/postsvg/matrix.rb:9)
108
+
109
+ .Create identity matrix
110
+ [example]
111
+ ====
112
+ [source,ruby]
113
+ ----
114
+ require 'postsvg'
115
+
116
+ # Identity matrix (no transformation)
117
+ identity = Postsvg::Matrix.new
118
+ # Represents: [1 0 0 1 0 0]
119
+
120
+ puts identity.a # → 1
121
+ puts identity.b # → 0
122
+ puts identity.c # → 0
123
+ puts identity.d # → 1
124
+ puts identity.e # → 0
125
+ puts identity.f # → 0
126
+ ----
127
+ ====
128
+
129
+ .Create translation matrix
130
+ [example]
131
+ ====
132
+ [source,ruby]
133
+ ----
134
+ # Translate by (50, 100)
135
+ translate = Postsvg::Matrix.new(e: 50, f: 100)
136
+ # Represents: [1 0 0 1 50 100]
137
+
138
+ # Apply to point
139
+ result = translate.apply_point(10, 20)
140
+ puts "(#{result[:x]}, #{result[:y]})" # → "(60, 120)"
141
+ ----
142
+ ====
143
+
144
+ .Create scaling matrix
145
+ [example]
146
+ ====
147
+ [source,ruby]
148
+ ----
149
+ # Scale by 2x horizontally, 3x vertically
150
+ scale = Postsvg::Matrix.new(a: 2, d: 3)
151
+ # Represents: [2 0 0 3 0 0]
152
+
153
+ result = scale.apply_point(10, 20)
154
+ puts "(#{result[:x]}, #{result[:y]})" # → "(20, 60)"
155
+ ----
156
+ ====
157
+
158
+ .Create custom matrix
159
+ [example]
160
+ ====
161
+ [source,ruby]
162
+ ----
163
+ # Custom transformation matrix
164
+ custom = Postsvg::Matrix.new(
165
+ a: 2.0,
166
+ b: 0.5,
167
+ c: -0.5,
168
+ d: 2.0,
169
+ e: 100,
170
+ f: 50
171
+ )
172
+
173
+ # This matrix combines:
174
+ # - Scaling (a=2, d=2)
175
+ # - Rotation/skew (b=0.5, c=-0.5)
176
+ # - Translation (e=100, f=50)
177
+ ----
178
+ ====
179
+
180
+ == Instance Methods
181
+
182
+ === multiply
183
+
184
+ Multiply this matrix by another matrix (compose transformations).
185
+
186
+ **Syntax:**
187
+
188
+ [source,ruby]
189
+ ----
190
+ result = matrix1.multiply(matrix2) <1>
191
+ ----
192
+ <1> Compose transformations (matrix1 then matrix2)
193
+
194
+ **Where:**
195
+
196
+ `matrix2`:: Another `Matrix` instance to multiply with
197
+
198
+ **Returns:**
199
+
200
+ New `Matrix` instance representing the composed transformation
201
+
202
+ **Mathematical Operation:**
203
+
204
+ ```
205
+ Result = Matrix1 × Matrix2
206
+
207
+ ┌ ┐ ┌ ┐ ┌ ┐
208
+ │ a' c' e'│ │ a₁ c₁ e₁ │ │ a₂ c₂ e₂ │
209
+ │ b' d' f'│ = │ b₁ d₁ f₁ │ │ b₂ d₂ f₂ │
210
+ │ 0 0 1│ │ 0 0 1 │ │ 0 0 1 │
211
+ └ ┘ └ ┘ └ ┘
212
+ ```
213
+
214
+ **Source:**
215
+
216
+ [`lib/postsvg/matrix.rb:18-27`](../../lib/postsvg/matrix.rb:18)
217
+
218
+ .Combine transformations
219
+ [example]
220
+ ====
221
+ [source,ruby]
222
+ ----
223
+ # Translate then scale
224
+ translate = Postsvg::Matrix.new(e: 50, f: 50)
225
+ scale = Postsvg::Matrix.new(a: 2, d: 2)
226
+
227
+ # Order matters! translate THEN scale
228
+ combined = translate.multiply(scale)
229
+
230
+ # Apply to point (10, 10)
231
+ result = combined.apply_point(10, 10)
232
+ puts "(#{result[:x]}, #{result[:y]})"
233
+ # → "(120, 120)" = (10+50)*2, (10+50)*2
234
+ ----
235
+ ====
236
+
237
+ .Order matters in matrix multiplication
238
+ [example]
239
+ ====
240
+ [source,ruby]
241
+ ----
242
+ translate = Postsvg::Matrix.new(e: 50, f: 50)
243
+ scale = Postsvg::Matrix.new(a: 2, d: 2)
244
+
245
+ # Translate then scale
246
+ t_then_s = translate.multiply(scale)
247
+ result1 = t_then_s.apply_point(10, 10)
248
+ puts "Translate→Scale: (#{result1[:x]}, #{result1[:y]})"
249
+ # → "(120, 120)"
250
+
251
+ # Scale then translate
252
+ s_then_t = scale.multiply(translate)
253
+ result2 = s_then_t.apply_point(10, 10)
254
+ puts "Scale→Translate: (#{result2[:x]}, #{result2[:y]})"
255
+ # → "(70, 70)" = 10*2+50, 10*2+50
256
+
257
+ # Different results! Order matters
258
+ ----
259
+ ====
260
+
261
+ === translate
262
+
263
+ Create a new matrix representing translation.
264
+
265
+ **Syntax:**
266
+
267
+ [source,ruby]
268
+ ----
269
+ translated = matrix.translate(tx, ty) <1>
270
+ ----
271
+ <1> Apply translation to this matrix
272
+
273
+ **Where:**
274
+
275
+ `tx`:: Horizontal translation distance (Float or Integer)
276
+
277
+ `ty`:: Vertical translation distance (Float or Integer)
278
+
279
+ **Returns:**
280
+
281
+ New `Matrix` instance with translation applied
282
+
283
+ **Effect:**
284
+
285
+ Equivalent to multiplying by translation matrix `[1 0 0 1 tx ty]`
286
+
287
+ **Source:**
288
+
289
+ [`lib/postsvg/matrix.rb:29-31`](../../lib/postsvg/matrix.rb:29)
290
+
291
+ .Translate coordinates
292
+ [example]
293
+ ====
294
+ [source,ruby]
295
+ ----
296
+ matrix = Postsvg::Matrix.new
297
+ translated = matrix.translate(100, 50)
298
+
299
+ # Apply to origin
300
+ result = translated.apply_point(0, 0)
301
+ puts "(#{result[:x]}, #{result[:y]})" # → "(100, 50)"
302
+ ----
303
+ ====
304
+
305
+ .Chain translations
306
+ [example]
307
+ ====
308
+ [source,ruby]
309
+ ----
310
+ matrix = Postsvg::Matrix.new
311
+ .translate(50, 50) # Move to (50, 50)
312
+ .translate(30, 20) # Then move by (30, 20)
313
+
314
+ result = matrix.apply_point(0, 0)
315
+ puts "(#{result[:x]}, #{result[:y]})" # → "(80, 70)"
316
+ ----
317
+ ====
318
+
319
+ === scale
320
+
321
+ Create a new matrix representing scaling.
322
+
323
+ **Syntax:**
324
+
325
+ [source,ruby]
326
+ ----
327
+ scaled = matrix.scale(sx, sy) <1>
328
+ ----
329
+ <1> Apply scaling to this matrix
330
+
331
+ **Where:**
332
+
333
+ `sx`:: Horizontal scale factor (Float or Integer)
334
+
335
+ `sy`:: Vertical scale factor (Float or Integer)
336
+
337
+ **Returns:**
338
+
339
+ New `Matrix` instance with scaling applied
340
+
341
+ **Effect:**
342
+
343
+ Equivalent to multiplying by scaling matrix `[sx 0 0 sy 0 0]`
344
+
345
+ **Source:**
346
+
347
+ [`lib/postsvg/matrix.rb:33-35`](../../lib/postsvg/matrix.rb:33)
348
+
349
+ .Scale uniformly
350
+ [example]
351
+ ====
352
+ [source,ruby]
353
+ ----
354
+ matrix = Postsvg::Matrix.new
355
+ scaled = matrix.scale(2, 2) # Double size
356
+
357
+ result = scaled.apply_point(50, 50)
358
+ puts "(#{result[:x]}, #{result[:y]})" # → "(100, 100)"
359
+ ----
360
+ ====
361
+
362
+ .Scale non-uniformly
363
+ [example]
364
+ ====
365
+ [source,ruby]
366
+ ----
367
+ matrix = Postsvg::Matrix.new
368
+ stretched = matrix.scale(3, 1) # 3x wide, same height
369
+
370
+ result = stretched.apply_point(10, 20)
371
+ puts "(#{result[:x]}, #{result[:y]})" # → "(30, 20)"
372
+ ----
373
+ ====
374
+
375
+ .Flip horizontally
376
+ [example]
377
+ ====
378
+ [source,ruby]
379
+ ----
380
+ # Negative scale flips axis
381
+ flipped = Postsvg::Matrix.new.scale(-1, 1)
382
+
383
+ result = flipped.apply_point(50, 30)
384
+ puts "(#{result[:x]}, #{result[:y]})" # → "(-50, 30)"
385
+ ----
386
+ ====
387
+
388
+ === rotate
389
+
390
+ Create a new matrix representing rotation.
391
+
392
+ **Syntax:**
393
+
394
+ [source,ruby]
395
+ ----
396
+ rotated = matrix.rotate(degrees) <1>
397
+ ----
398
+ <1> Apply rotation to this matrix
399
+
400
+ **Where:**
401
+
402
+ `degrees`:: Rotation angle in degrees (Float or Integer)
403
+ * Positive: counter-clockwise rotation
404
+ * Negative: clockwise rotation
405
+
406
+ **Returns:**
407
+
408
+ New `Matrix` instance with rotation applied
409
+
410
+ **Effect:**
411
+
412
+ Equivalent to multiplying by rotation matrix:
413
+ ```
414
+ [cos(θ) sin(θ) 0]
415
+ [-sin(θ) cos(θ) 0]
416
+ [0 0 1]
417
+ ```
418
+
419
+ **Source:**
420
+
421
+ [`lib/postsvg/matrix.rb:37-45`](../../lib/postsvg/matrix.rb:37)
422
+
423
+ .Rotate 90 degrees
424
+ [example]
425
+ ====
426
+ [source,ruby]
427
+ ----
428
+ matrix = Postsvg::Matrix.new
429
+ rotated = matrix.rotate(90) # 90° counter-clockwise
430
+
431
+ result = rotated.apply_point(100, 0)
432
+ # 100 units along x-axis rotates to y-axis
433
+ puts "(#{result[:x].round(2)}, #{result[:y].round(2)})"
434
+ # → "(0.0, 100.0)"
435
+ ----
436
+ ====
437
+
438
+ .Rotate 45 degrees
439
+ [example]
440
+ ====
441
+ [source,ruby]
442
+ ----
443
+ rotated = Postsvg::Matrix.new.rotate(45)
444
+
445
+ result = rotated.apply_point(100, 0)
446
+ # √2/2 * 100 ≈ 70.71
447
+ puts "(#{result[:x].round(2)}, #{result[:y].round(2)})"
448
+ # → "(70.71, 70.71)"
449
+ ----
450
+ ====
451
+
452
+ .Clockwise rotation
453
+ [example]
454
+ ====
455
+ [source,ruby]
456
+ ----
457
+ # Negative angle = clockwise
458
+ clockwise = Postsvg::Matrix.new.rotate(-90)
459
+
460
+ result = clockwise.apply_point(100, 0)
461
+ puts "(#{result[:x].round(2)}, #{result[:y].round(2)})"
462
+ # → "(0.0, -100.0)"
463
+ ----
464
+ ====
465
+
466
+ === skew_x
467
+
468
+ Create a new matrix representing horizontal skew.
469
+
470
+ **Syntax:**
471
+
472
+ [source,ruby]
473
+ ----
474
+ skewed = matrix.skew_x(angle) <1>
475
+ ----
476
+ <1> Apply horizontal skew to this matrix
477
+
478
+ **Where:**
479
+
480
+ `angle`:: Skew angle in degrees (Float or Integer)
481
+
482
+ **Returns:**
483
+
484
+ New `Matrix` instance with horizontal skew applied
485
+
486
+ **Effect:**
487
+
488
+ Equivalent to multiplying by skew matrix `[1 0 tan(θ) 1 0 0]`
489
+
490
+ **Source:**
491
+
492
+ [`lib/postsvg/matrix.rb:47-50`](../../lib/postsvg/matrix.rb:47)
493
+
494
+ .Skew horizontally
495
+ [example]
496
+ ====
497
+ [source,ruby]
498
+ ----
499
+ skewed = Postsvg::Matrix.new.skew_x(30)
500
+
501
+ # Points at different y-values shift differently
502
+ result1 = skewed.apply_point(0, 0)
503
+ result2 = skewed.apply_point(0, 100)
504
+
505
+ puts "At y=0: (#{result1[:x].round(2)}, #{result1[:y]})"
506
+ # → "At y=0: (0.0, 0)"
507
+
508
+ puts "At y=100: (#{result2[:x].round(2)}, #{result2[:y]})"
509
+ # → "At y=100: (57.74, 100)" (shifted right)
510
+ ----
511
+ ====
512
+
513
+ === skew_y
514
+
515
+ Create a new matrix representing vertical skew.
516
+
517
+ **Syntax:**
518
+
519
+ [source,ruby]
520
+ ----
521
+ skewed = matrix.skew_y(angle) <1>
522
+ ----
523
+ <1> Apply vertical skew to this matrix
524
+
525
+ **Where:**
526
+
527
+ `angle`:: Skew angle in degrees (Float or Integer)
528
+
529
+ **Returns:**
530
+
531
+ New `Matrix` instance with vertical skew applied
532
+
533
+ **Effect:**
534
+
535
+ Equivalent to multiplying by skew matrix `[1 tan(θ) 0 1 0 0]`
536
+
537
+ **Source:**
538
+
539
+ [`lib/postsvg/matrix.rb:52-55`](../../lib/postsvg/matrix.rb:52)
540
+
541
+ .Skew vertically
542
+ [example]
543
+ ====
544
+ [source,ruby]
545
+ ----
546
+ skewed = Postsvg::Matrix.new.skew_y(30)
547
+
548
+ # Points at different x-values shift differently
549
+ result1 = skewed.apply_point(0, 0)
550
+ result2 = skewed.apply_point(100, 0)
551
+
552
+ puts "At x=0: (#{result1[:x]}, #{result1[:y].round(2)})"
553
+ # → "At x=0: (0, 0.0)"
554
+
555
+ puts "At x=100: (#{result2[:x]}, #{result2[:y].round(2)})"
556
+ # → "At x=100: (100, 57.74)" (shifted up)
557
+ ----
558
+ ====
559
+
560
+ === to_transform_string
561
+
562
+ Convert matrix to SVG transform string.
563
+
564
+ **Syntax:**
565
+
566
+ [source,ruby]
567
+ ----
568
+ transform_str = matrix.to_transform_string <1>
569
+ ----
570
+ <1> Generate SVG transform attribute value
571
+
572
+ **Returns:**
573
+
574
+ String in format `"matrix(a b c d e f)"`
575
+
576
+ **Source:**
577
+
578
+ [`lib/postsvg/matrix.rb:57-59`](../../lib/postsvg/matrix.rb:57)
579
+
580
+ .Generate SVG transform
581
+ [example]
582
+ ====
583
+ [source,ruby]
584
+ ----
585
+ matrix = Postsvg::Matrix.new
586
+ .translate(50, 100)
587
+ .rotate(45)
588
+ .scale(2, 2)
589
+
590
+ transform = matrix.to_transform_string
591
+ puts transform
592
+ # → "matrix(1.414... 1.414... -1.414... 1.414... 50 100)"
593
+
594
+ # Use in SVG
595
+ svg = %Q{<g transform="#{transform}">...</g>}
596
+ ----
597
+ ====
598
+
599
+ === apply_point
600
+
601
+ Transform a point using this matrix.
602
+
603
+ **Syntax:**
604
+
605
+ [source,ruby]
606
+ ----
607
+ result = matrix.apply_point(x, y) <1>
608
+ ----
609
+ <1> Apply transformation to point coordinates
610
+
611
+ **Where:**
612
+
613
+ `x`:: X-coordinate to transform (Float or Integer)
614
+
615
+ `y`:: Y-coordinate to transform (Float or Integer)
616
+
617
+ **Returns:**
618
+
619
+ Hash containing transformed coordinates:
620
+ * `:x` - Transformed x-coordinate
621
+ * `:y` - Transformed y-coordinate
622
+
623
+ **Transformation Formula:**
624
+
625
+ ```
626
+ x' = a*x + c*y + e
627
+ y' = b*x + d*y + f
628
+ ```
629
+
630
+ **Source:**
631
+
632
+ [`lib/postsvg/matrix.rb:61-66`](../../lib/postsvg/matrix.rb:61)
633
+
634
+ .Transform point
635
+ [example]
636
+ ====
637
+ [source,ruby]
638
+ ----
639
+ # Translate by (100, 50)
640
+ matrix = Postsvg::Matrix.new(e: 100, f: 50)
641
+
642
+ result = matrix.apply_point(10, 20)
643
+ puts "(#{result[:x]}, #{result[:y]})" # → "(110, 70)"
644
+ ----
645
+ ====
646
+
647
+ .Transform multiple points
648
+ [example]
649
+ ====
650
+ [source,ruby]
651
+ ----
652
+ matrix = Postsvg::Matrix.new
653
+ .translate(50, 50)
654
+ .rotate(90)
655
+ .scale(2, 2)
656
+
657
+ points = [[0, 0], [10, 0], [10, 10], [0, 10]]
658
+
659
+ transformed = points.map do |x, y|
660
+ result = matrix.apply_point(x, y)
661
+ [result[:x], result[:y]]
662
+ end
663
+
664
+ puts transformed.inspect
665
+ # Points of square transformed to new position/orientation/size
666
+ ----
667
+ ====
668
+
669
+ === decompose
670
+
671
+ Decompose matrix into component transformations.
672
+
673
+ **Syntax:**
674
+
675
+ [source,ruby]
676
+ ----
677
+ components = matrix.decompose <1>
678
+ ----
679
+ <1> Extract translation, scale, rotation, and skew
680
+
681
+ **Returns:**
682
+
683
+ Hash containing transformation components:
684
+ * `:translate` - Hash with `:x` and `:y` translation
685
+ * `:scale` - Hash with `:x` and `:y` scale factors
686
+ * `:rotate` - Rotation angle in degrees
687
+ * `:skew` - Hash with `:x` and `:y` skew angles in degrees
688
+
689
+ **Source:**
690
+
691
+ [`lib/postsvg/matrix.rb:68-121`](../../lib/postsvg/matrix.rb:68)
692
+
693
+ **Special Cases:**
694
+
695
+ * **Singular matrix** (determinant ≈ 0): Returns identity-like values
696
+ * **Degenerate matrix** (zero scale): Returns zero scale
697
+
698
+ .Decompose simple transformations
699
+ [example]
700
+ ====
701
+ [source,ruby]
702
+ ----
703
+ matrix = Postsvg::Matrix.new
704
+ .translate(100, 50)
705
+ .rotate(45)
706
+ .scale(2, 3)
707
+
708
+ components = matrix.decompose
709
+
710
+ puts "Translation: (#{components[:translate][:x]}, #{components[:translate][:y]})"
711
+ # → "Translation: (100, 50)"
712
+
713
+ puts "Scale: (#{components[:scale][:x].round(2)}, #{components[:scale][:y].round(2)})"
714
+ # → "Scale: (2.0, 3.0)"
715
+
716
+ puts "Rotation: #{components[:rotate].round(2)}°"
717
+ # → "Rotation: 45.0°"
718
+
719
+ puts "Skew: (#{components[:skew][:x].round(2)}, #{components[:skew][:y].round(2)})"
720
+ # → "Skew: (0.0, 0.0)"
721
+ ----
722
+ ====
723
+
724
+ .Analyze unknown matrix
725
+ [example]
726
+ ====
727
+ [source,ruby]
728
+ ----
729
+ # Matrix from external source
730
+ unknown = Postsvg::Matrix.new(
731
+ a: 1.5, b: 0.5,
732
+ c: -0.5, d: 1.5,
733
+ e: 200, f: 100
734
+ )
735
+
736
+ components = unknown.decompose
737
+
738
+ puts "This matrix performs:"
739
+ puts " - Translation: (#{components[:translate][:x]}, #{components[:translate][:y]})"
740
+ puts " - Scaling: #{components[:scale][:x].round(2)}x"
741
+ puts " - Rotation: #{components[:rotate].round(1)}°"
742
+ ----
743
+ ====
744
+
745
+ === invert
746
+
747
+ Compute the inverse of this matrix.
748
+
749
+ **Syntax:**
750
+
751
+ [source,ruby]
752
+ ----
753
+ inverse = matrix.invert <1>
754
+ ----
755
+ <1> Get inverse transformation matrix
756
+
757
+ **Returns:**
758
+
759
+ New `Matrix` instance representing the inverse transformation, or identity matrix if original is singular
760
+
761
+ **Mathematical Operation:**
762
+
763
+ For matrix M, finds M⁻¹ such that M × M⁻¹ = I (identity)
764
+
765
+ **Source:**
766
+
767
+ [`lib/postsvg/matrix.rb:123-138`](../../lib/postsvg/matrix.rb:123)
768
+
769
+ **Edge Cases:**
770
+
771
+ * **Singular matrix** (determinant = 0): Returns identity matrix
772
+ * **Near-singular** (|det| < 1e-10): Returns identity matrix
773
+
774
+ .Invert transformation
775
+ [example]
776
+ ====
777
+ [source,ruby]
778
+ ----
779
+ # Apply transformation
780
+ forward = Postsvg::Matrix.new
781
+ .translate(100, 50)
782
+ .rotate(30)
783
+ .scale(2, 2)
784
+
785
+ # Compute inverse
786
+ backward = forward.invert
787
+
788
+ # Apply both transformations
789
+ point = forward.apply_point(10, 20)
790
+ original = backward.apply_point(point[:x], point[:y])
791
+
792
+ puts "Original: (10, 20)"
793
+ puts "Transformed: (#{point[:x].round(2)}, #{point[:y].round(2)})"
794
+ puts "Back to original: (#{original[:x].round(2)}, #{original[:y].round(2)})"
795
+ # → Should be close to (10, 20)
796
+ ----
797
+ ====
798
+
799
+ .Undo transformations
800
+ [example]
801
+ ====
802
+ [source,ruby]
803
+ ----
804
+ # User applies transformations
805
+ user_transform = Postsvg::Matrix.new
806
+ .translate(150, 200)
807
+ .rotate(45)
808
+
809
+ # Get inverse to undo
810
+ undo = user_transform.invert
811
+
812
+ # Apply and undo
813
+ result1 = user_transform.apply_point(50, 50)
814
+ result2 = undo.apply_point(result1[:x], result1[:y])
815
+
816
+ puts "After transform: (#{result1[:x].round(2)}, #{result1[:y].round(2)})"
817
+ puts "After undo: (#{result2[:x].round(2)}, #{result2[:y].round(2)})"
818
+ # → Should return to (50, 50)
819
+ ----
820
+ ====
821
+
822
+ === identity?
823
+
824
+ Check if this matrix is the identity matrix.
825
+
826
+ **Syntax:**
827
+
828
+ [source,ruby]
829
+ ----
830
+ is_identity = matrix.identity? <1>
831
+ ----
832
+ <1> Test if matrix performs no transformation
833
+
834
+ **Returns:**
835
+
836
+ Boolean:
837
+ * `true` - Matrix is identity `[1 0 0 1 0 0]`
838
+ * `false` - Matrix performs some transformation
839
+
840
+ **Source:**
841
+
842
+ [`lib/postsvg/matrix.rb:140-142`](../../lib/postsvg/matrix.rb:140)
843
+
844
+ .Check for identity
845
+ [example]
846
+ ====
847
+ [source,ruby]
848
+ ----
849
+ identity = Postsvg::Matrix.new
850
+ puts identity.identity? # → true
851
+
852
+ translated = Postsvg::Matrix.new(e: 10, f: 10)
853
+ puts translated.identity? # → false
854
+
855
+ scaled = Postsvg::Matrix.new(a: 2, d: 2)
856
+ puts scaled.identity? # → false
857
+ ----
858
+ ====
859
+
860
+ .Optimize transformations
861
+ [example]
862
+ ====
863
+ [source,ruby]
864
+ ----
865
+ def apply_if_needed(matrix, point)
866
+ if matrix.identity?
867
+ # Skip transformation - no change needed
868
+ { x: point[0], y: point[1] }
869
+ else
870
+ matrix.apply_point(point[0], point[1])
871
+ end
872
+ end
873
+
874
+ # Usage
875
+ no_op = Postsvg::Matrix.new
876
+ transform = Postsvg::Matrix.new.scale(2, 2)
877
+
878
+ apply_if_needed(no_op, [10, 20]) # Fast path: no transformation
879
+ apply_if_needed(transform, [10, 20]) # Slow path: actual transformation
880
+ ----
881
+ ====
882
+
883
+ == Attributes
884
+
885
+ All matrix coefficients are publicly accessible via `attr_accessor`:
886
+
887
+ === a, b, c, d, e, f
888
+
889
+ Direct access to matrix coefficients.
890
+
891
+ **Syntax:**
892
+
893
+ [source,ruby]
894
+ ----
895
+ matrix.a = value # Horizontal scale/rotation
896
+ matrix.b = value # Vertical skew/rotation
897
+ matrix.c = value # Horizontal skew/rotation
898
+ matrix.d = value # Vertical scale/rotation
899
+ matrix.e = value # Horizontal translation
900
+ matrix.f = value # Vertical translation
901
+ ----
902
+
903
+ **Source:**
904
+
905
+ [`lib/postsvg/matrix.rb:7`](../../lib/postsvg/matrix.rb:7)
906
+
907
+ .Modify matrix coefficients
908
+ [example]
909
+ ====
910
+ [source,ruby]
911
+ ----
912
+ matrix = Postsvg::Matrix.new
913
+
914
+ # Set custom coefficients
915
+ matrix.a = 2.0 # Scale x by 2
916
+ matrix.d = 3.0 # Scale y by 3
917
+ matrix.e = 50 # Translate x by 50
918
+ matrix.f = 100 # Translate y by 100
919
+
920
+ result = matrix.apply_point(10, 10)
921
+ puts "(#{result[:x]}, #{result[:y]})" # → "(70, 130)"
922
+ # Calculation: (10*2 + 50, 10*3 + 100)
923
+ ----
924
+ ====
925
+
926
+ == Usage Patterns
927
+
928
+ === Pattern 1: Chained Transformations
929
+
930
+ [source,ruby]
931
+ ----
932
+ require 'postsvg'
933
+
934
+ # Build complex transformation via chaining
935
+ matrix = Postsvg::Matrix.new
936
+ .translate(200, 150) # Move to center
937
+ .rotate(45) # Rotate
938
+ .scale(2, 2) # Scale up
939
+ .translate(-50, -50) # Offset
940
+
941
+ # Apply to points
942
+ points = [[0, 0], [100, 0], [100, 100], [0, 100]]
943
+
944
+ transformed_points = points.map do |x, y|
945
+ result = matrix.apply_point(x, y)
946
+ [result[:x], result[:y]]
947
+ end
948
+ ----
949
+
950
+ === Pattern 2: Animation Interpolation
951
+
952
+ [source,ruby]
953
+ ----
954
+ require 'postsvg'
955
+
956
+ def interpolate_matrices(m1, m2, t)
957
+ # Linear interpolation between two matrices
958
+ # t = 0.0 → m1, t = 1.0 → m2
959
+
960
+ Postsvg::Matrix.new(
961
+ a: m1.a + (m2.a - m1.a) * t,
962
+ b: m1.b + (m2.b - m1.b) * t,
963
+ c: m1.c + (m2.c - m1.c) * t,
964
+ d: m1.d + (m2.d - m1.d) * t,
965
+ e: m1.e + (m2.e - m1.e) * t,
966
+ f: m1.f + (m2.f - m1.f) * t
967
+ )
968
+ end
969
+
970
+ # Animate from identity to transformed
971
+ start = Postsvg::Matrix.new
972
+ end_matrix = Postsvg::Matrix.new.translate(100, 100).rotate(90)
973
+
974
+ # Generate 10 frames
975
+ frames = (0..10).map do |i|
976
+ t = i / 10.0
977
+ interpolate_matrices(start, end_matrix, t)
978
+ end
979
+ ----
980
+
981
+ === Pattern 3: Coordinate System Conversion
982
+
983
+ [source,ruby]
984
+ ----
985
+ require 'postsvg'
986
+
987
+ class CoordinateConverter
988
+ def initialize(source_bounds, target_bounds)
989
+ # source_bounds: {x, y, width, height}
990
+ # target_bounds: {x, y, width, height}
991
+
992
+ sx = target_bounds[:width].to_f / source_bounds[:width]
993
+ sy = target_bounds[:height].to_f / source_bounds[:height]
994
+
995
+ @transform = Postsvg::Matrix.new
996
+ .translate(-source_bounds[:x], -source_bounds[:y])
997
+ .scale(sx, sy)
998
+ .translate(target_bounds[:x], target_bounds[:y])
999
+ end
1000
+
1001
+ def convert(x, y)
1002
+ @transform.apply_point(x, y)
1003
+ end
1004
+ end
1005
+
1006
+ # Convert from PostScript (0-612) to screen (0-800)
1007
+ converter = CoordinateConverter.new(
1008
+ { x: 0, y: 0, width: 612, height: 792 },
1009
+ { x: 0, y: 0, width: 800, height: 1000 }
1010
+ )
1011
+
1012
+ screen_pos = converter.convert(306, 396) # Center of page
1013
+ puts "(#{screen_pos[:x].round}, #{screen_pos[:y].round})"
1014
+ # → "(400, 500)" - Center of screen
1015
+ ----
1016
+
1017
+ === Pattern 4: Transform Accumulator
1018
+
1019
+ [source,ruby]
1020
+ ----
1021
+ require 'postsvg'
1022
+
1023
+ class TransformStack
1024
+ def initialize
1025
+ @stack = [Postsvg::Matrix.new] # Start with identity
1026
+ end
1027
+
1028
+ def push
1029
+ @stack.push(@stack.last.dup)
1030
+ end
1031
+
1032
+ def pop
1033
+ @stack.pop if @stack.length > 1
1034
+ end
1035
+
1036
+ def translate(tx, ty)
1037
+ @stack[-1] = @stack.last.translate(tx, ty)
1038
+ end
1039
+
1040
+ def rotate(degrees)
1041
+ @stack[-1] = @stack.last.rotate(degrees)
1042
+ end
1043
+
1044
+ def scale(sx, sy)
1045
+ @stack[-1] = @stack.last.scale(sx, sy)
1046
+ end
1047
+
1048
+ def current
1049
+ @stack.last
1050
+ end
1051
+
1052
+ def apply_point(x, y)
1053
+ current.apply_point(x, y)
1054
+ end
1055
+ end
1056
+
1057
+ # Usage (similar to PostScript gsave/grestore)
1058
+ transforms = TransformStack.new
1059
+
1060
+ transforms.translate(100, 100)
1061
+ transforms.push # Save state
1062
+ transforms.rotate(45)
1063
+ transforms.scale(2, 2)
1064
+ result1 = transforms.apply_point(10, 10)
1065
+ transforms.pop # Restore state
1066
+
1067
+ result2 = transforms.apply_point(10, 10)
1068
+
1069
+ puts "With rotation/scale: (#{result1[:x].round(2)}, #{result1[:y].round(2)})"
1070
+ puts "After restore: (#{result2[:x].round(2)}, #{result2[:y].round(2)})"
1071
+ ----
1072
+
1073
+ == Thread Safety
1074
+
1075
+ The `Matrix` class is **immutable in practice** - all transformation methods return NEW instances rather than modifying the original. This makes it thread-safe for reading, but shared mutable state (via direct attribute modification) is not thread-safe.
1076
+
1077
+ .Thread-safe usage
1078
+ [example]
1079
+ ====
1080
+ [source,ruby]
1081
+ ----
1082
+ # Safe: Immutable transformation methods
1083
+ shared_matrix = Postsvg::Matrix.new
1084
+
1085
+ threads = 10.times.map do
1086
+ Thread.new do
1087
+ # Each creates new matrices, doesn't modify shared_matrix
1088
+ rotated = shared_matrix.rotate(45)
1089
+ scaled = rotated.scale(2, 2)
1090
+ scaled.apply_point(100, 100)
1091
+ end
1092
+ end
1093
+
1094
+ results = threads.map(&:value)
1095
+
1096
+ # Unsafe: Direct attribute modification
1097
+ threads = 10.times.map do
1098
+ Thread.new do
1099
+ shared_matrix.a = rand(10) # RACE CONDITION!
1100
+ end
1101
+ end
1102
+ ----
1103
+ ====
1104
+
1105
+ **Best Practices:**
1106
+
1107
+ 1. Treat matrices as immutable
1108
+ 2. Use transformation methods (which return new instances)
1109
+ 3. Avoid direct attribute modification in concurrent code
1110
+ 4. Create new matrices per thread if needed
1111
+
1112
+ == Performance Considerations
1113
+
1114
+ **Time Complexity:**
1115
+
1116
+ * Matrix creation: O(1)
1117
+ * Transformation methods: O(1) - creates new matrix
1118
+ * `apply_point`: O(1) - simple arithmetic
1119
+ * `multiply`: O(1) - 6 multiplications + 6 additions
1120
+ * `decompose`: O(1) - trigonometric calculations
1121
+ * `invert`: O(1) - determinant and coefficient calculations
1122
+
1123
+ **Space Complexity:**
1124
+
1125
+ * Each matrix: 6 floating-point numbers (~48 bytes)
1126
+ * Negligible memory overhead
1127
+
1128
+ **Optimization Tips:**
1129
+
1130
+ 1. **Reuse matrices**: Store commonly used transformations
1131
+ 2. **Compose once**: Combine multiple transformations into single matrix
1132
+ 3. **Batch point transformations**: Apply same matrix to many points
1133
+ 4. **Cache decomposition**: Expensive, compute once if needed multiple times
1134
+
1135
+ .Performance example
1136
+ [example]
1137
+ ====
1138
+ [source,ruby]
1139
+ ----
1140
+ require 'postsvg'
1141
+ require 'benchmark'
1142
+
1143
+ # Create complex transformation once
1144
+ matrix = Postsvg::Matrix.new
1145
+ .translate(100, 100)
1146
+ .rotate(45)
1147
+ .scale(2, 2)
1148
+ .translate(-50, -50)
1149
+
1150
+ points = 10_000.times.map { [rand(1000), rand(1000)] }
1151
+
1152
+ # Fast: Single matrix, many points
1153
+ time = Benchmark.measure do
1154
+ points.each do |x, y|
1155
+ matrix.apply_point(x, y)
1156
+ end
1157
+ end
1158
+
1159
+ puts "Transformed #{points.length} points in #{'%.3f' % time.real}s"
1160
+ puts "Rate: #{(points.length / time.real).to_i} points/sec"
1161
+ ----
1162
+ ====
1163
+
1164
+ == Next Steps
1165
+
1166
+ * Learn about link:path-builder.adoc[PathBuilder] for using transforms with paths
1167
+ * Review link:graphics-state.adoc[GraphicsState] for transformation stack management
1168
+ * See link:interpreter.adoc[Interpreter] for PostScript transformation commands
1169
+ * Check link:../architecture.adoc[Architecture] for system design
1170
+
1171
+ == Bibliography
1172
+
1173
+ * link:path-builder.adoc[PathBuilder Documentation]
1174
+ * link:graphics-state.adoc[GraphicsState Documentation]
1175
+ * link:interpreter.adoc[Interpreter Documentation]
1176
+ * link:../architecture.adoc[Architecture Overview]
1177
+ * link:https://www.adobe.com/jp/print/postscript/pdfs/PLRM.pdf[PostScript Language Reference Manual]
1178
+ * link:https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform[MDN SVG Transform Reference]
1179
+ * link:https://en.wikipedia.org/wiki/Affine_transformation[Wikipedia: Affine Transformation]