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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +105 -0
- data/CLAUDE.md +173 -0
- data/Gemfile +2 -3
- data/README.adoc +456 -179
- data/Rakefile +100 -0
- data/TODO.roadmap/00-architecture.md +139 -0
- data/TODO.roadmap/01-autoload-migration.md +39 -0
- data/TODO.roadmap/02-isolate-dormant-code.md +51 -0
- data/TODO.roadmap/03-domain-model.md +66 -0
- data/TODO.roadmap/04-lexer.md +40 -0
- data/TODO.roadmap/05-parser.md +43 -0
- data/TODO.roadmap/06-graphics-state.md +45 -0
- data/TODO.roadmap/07-matrix-and-color.md +37 -0
- data/TODO.roadmap/08-svg-builder.md +51 -0
- data/TODO.roadmap/09-renderer.md +52 -0
- data/TODO.roadmap/10-visitor.md +58 -0
- data/TODO.roadmap/11-operator-coverage.md +103 -0
- data/TODO.roadmap/12-svg-domain.md +62 -0
- data/TODO.roadmap/13-translation-handlers.md +69 -0
- data/TODO.roadmap/14-ps-serializer.md +49 -0
- data/TODO.roadmap/15-cli-and-public-api.md +47 -0
- data/TODO.roadmap/16-specs.md +84 -0
- data/TODO.roadmap/17-docs-sync.md +47 -0
- data/TODO.roadmap/18-performance-and-determinism.md +47 -0
- data/TODO.roadmap/19-error-model.md +45 -0
- data/TODO.roadmap/20-font-and-text.md +46 -0
- data/TODO.roadmap/21-images.md +45 -0
- data/TODO.roadmap/22-forms-and-resources.md +25 -0
- data/TODO.roadmap/23-level2-level3.md +52 -0
- data/TODO.roadmap/24-ci-and-release.md +42 -0
- data/TODO.roadmap/README.md +77 -0
- data/docs/.gitignore +29 -0
- data/docs/CHANGELOG.md +114 -0
- data/docs/COMPLETE_DOCUMENTATION_STATUS.md +376 -0
- data/docs/DEPLOYMENT.md +456 -0
- data/docs/DEPLOYMENT_INSTRUCTIONS.md +229 -0
- data/docs/DOCUMENTATION_PLAN.md +425 -0
- data/docs/FINAL_SUMMARY.md +657 -0
- data/docs/Gemfile +15 -0
- data/docs/README.md +327 -0
- data/docs/_config.yml +99 -0
- data/docs/advanced-topics.adoc +370 -0
- data/docs/api-reference/colors.adoc +705 -0
- data/docs/api-reference/converter.adoc +699 -0
- data/docs/api-reference/execution-context.adoc +1210 -0
- data/docs/api-reference/graphics-state.adoc +1070 -0
- data/docs/api-reference/interpreter.adoc +810 -0
- data/docs/api-reference/matrix.adoc +1179 -0
- data/docs/api-reference/path-builder.adoc +1284 -0
- data/docs/api-reference/postsvg-module.adoc +388 -0
- data/docs/api-reference/svg-generator.adoc +891 -0
- data/docs/api-reference/tokenizer.adoc +925 -0
- data/docs/api-reference.adoc +221 -0
- data/docs/architecture/command-registry.adoc +1191 -0
- data/docs/architecture/conversion-pipeline.adoc +746 -0
- data/docs/architecture/design-decisions.adoc +999 -0
- data/docs/architecture/generator-stage.adoc +1115 -0
- data/docs/architecture/graphics-state-model.adoc +1089 -0
- data/docs/architecture/interpreter-stage.adoc +1125 -0
- data/docs/architecture/parser-stage.adoc +1051 -0
- data/docs/architecture.adoc +354 -0
- data/docs/cli-reference/batch-command.adoc +616 -0
- data/docs/cli-reference/check-command.adoc +677 -0
- data/docs/cli-reference/cli-options.adoc +802 -0
- data/docs/cli-reference/convert-command.adoc +462 -0
- data/docs/cli-reference/version-command.adoc +296 -0
- data/docs/cli-reference.adoc +317 -0
- data/docs/concepts/conversion-pipeline.adoc +903 -0
- data/docs/concepts/coordinate-systems.adoc +836 -0
- data/docs/concepts/graphics-state.adoc +861 -0
- data/docs/concepts/path-operations.adoc +1076 -0
- data/docs/concepts/postscript-language.adoc +859 -0
- data/docs/concepts/svg-generation.adoc +937 -0
- data/docs/concepts.adoc +198 -0
- data/docs/contributing.adoc +443 -0
- data/docs/development.adoc +420 -0
- data/docs/faq.adoc +493 -0
- data/docs/getting-started/basic-usage.adoc +538 -0
- data/docs/getting-started/common-workflows.adoc +577 -0
- data/docs/getting-started/first-conversion.adoc +492 -0
- data/docs/getting-started/installation.adoc +534 -0
- data/docs/getting-started.adoc +94 -0
- data/docs/index.adoc +248 -0
- data/docs/optimization.adoc +196 -0
- data/docs/ps2svg_compatibility.adoc +149 -0
- data/docs/quick-reference.adoc +453 -0
- data/docs/sitemap.adoc +337 -0
- data/docs/troubleshooting.adoc +486 -0
- data/docs/validation.adoc +772 -0
- data/exe/postsvg +1 -0
- data/lib/postsvg/cli.rb +104 -57
- data/lib/postsvg/color.rb +132 -0
- data/lib/postsvg/errors.rb +68 -3
- data/lib/postsvg/format_number.rb +22 -0
- data/lib/postsvg/graphics_context.rb +80 -0
- data/lib/postsvg/graphics_stack.rb +43 -0
- data/lib/postsvg/model/literals/array.rb +41 -0
- data/lib/postsvg/model/literals/dictionary.rb +34 -0
- data/lib/postsvg/model/literals/hex.rb +37 -0
- data/lib/postsvg/model/literals/name.rb +40 -0
- data/lib/postsvg/model/literals/number.rb +36 -0
- data/lib/postsvg/model/literals/procedure.rb +41 -0
- data/lib/postsvg/model/literals/string.rb +30 -0
- data/lib/postsvg/model/literals.rb +19 -0
- data/lib/postsvg/model/operator.rb +58 -0
- data/lib/postsvg/model/operators/arithmetic.rb +264 -0
- data/lib/postsvg/model/operators/boolean.rb +182 -0
- data/lib/postsvg/model/operators/color.rb +74 -0
- data/lib/postsvg/model/operators/container.rb +186 -0
- data/lib/postsvg/model/operators/control_flow.rb +119 -0
- data/lib/postsvg/model/operators/device.rb +21 -0
- data/lib/postsvg/model/operators/dictionary.rb +118 -0
- data/lib/postsvg/model/operators/font.rb +121 -0
- data/lib/postsvg/model/operators/graphics_state.rb +84 -0
- data/lib/postsvg/model/operators/painting.rb +29 -0
- data/lib/postsvg/model/operators/path.rb +169 -0
- data/lib/postsvg/model/operators/stack.rb +72 -0
- data/lib/postsvg/model/operators/transformations.rb +103 -0
- data/lib/postsvg/model/operators.rb +89 -0
- data/lib/postsvg/model/program.rb +68 -0
- data/lib/postsvg/model/token.rb +43 -0
- data/lib/postsvg/model.rb +17 -0
- data/lib/postsvg/options.rb +29 -0
- data/lib/postsvg/renderer.rb +85 -0
- data/lib/postsvg/serializer.rb +325 -0
- data/lib/postsvg/source/ast_builder.rb +308 -0
- data/lib/postsvg/source/lexer.rb +322 -0
- data/lib/postsvg/source/operand_stack.rb +55 -0
- data/lib/postsvg/source.rb +21 -0
- data/lib/postsvg/svg/attribute_parser.rb +45 -0
- data/lib/postsvg/svg/clip_path_registry.rb +44 -0
- data/lib/postsvg/svg/document.rb +22 -0
- data/lib/postsvg/svg/element.rb +84 -0
- data/lib/postsvg/svg/elements/circle.rb +36 -0
- data/lib/postsvg/svg/elements/clip_path.rb +26 -0
- data/lib/postsvg/svg/elements/defs.rb +24 -0
- data/lib/postsvg/svg/elements/ellipse.rb +38 -0
- data/lib/postsvg/svg/elements/group.rb +37 -0
- data/lib/postsvg/svg/elements/image.rb +35 -0
- data/lib/postsvg/svg/elements/line.rb +36 -0
- data/lib/postsvg/svg/elements/path.rb +32 -0
- data/lib/postsvg/svg/elements/polygon.rb +12 -0
- data/lib/postsvg/svg/elements/polyline.rb +32 -0
- data/lib/postsvg/svg/elements/rect.rb +44 -0
- data/lib/postsvg/svg/elements/svg.rb +39 -0
- data/lib/postsvg/svg/elements/text.rb +42 -0
- data/lib/postsvg/svg/elements.rb +31 -0
- data/lib/postsvg/svg/paint.rb +34 -0
- data/lib/postsvg/svg/parser.rb +39 -0
- data/lib/postsvg/svg/path_data/command.rb +27 -0
- data/lib/postsvg/svg/path_data/parser.rb +82 -0
- data/lib/postsvg/svg/path_data.rb +17 -0
- data/lib/postsvg/svg/stroke.rb +31 -0
- data/lib/postsvg/svg/transform_list.rb +59 -0
- data/lib/postsvg/svg.rb +22 -0
- data/lib/postsvg/svg_builder.rb +249 -0
- data/lib/postsvg/translation/arc_converter.rb +86 -0
- data/lib/postsvg/translation/bounding_box.rb +59 -0
- data/lib/postsvg/translation/context.rb +34 -0
- data/lib/postsvg/translation/handler_registry.rb +38 -0
- data/lib/postsvg/translation/handlers/circle_handler.rb +28 -0
- data/lib/postsvg/translation/handlers/clip_path_handler.rb +13 -0
- data/lib/postsvg/translation/handlers/defs_handler.rb +15 -0
- data/lib/postsvg/translation/handlers/ellipse_handler.rb +31 -0
- data/lib/postsvg/translation/handlers/group_handler.rb +21 -0
- data/lib/postsvg/translation/handlers/image_handler.rb +20 -0
- data/lib/postsvg/translation/handlers/line_handler.rb +23 -0
- data/lib/postsvg/translation/handlers/open_handler.rb +18 -0
- data/lib/postsvg/translation/handlers/path_handler.rb +356 -0
- data/lib/postsvg/translation/handlers/polygon_handler.rb +33 -0
- data/lib/postsvg/translation/handlers/polyline_handler.rb +31 -0
- data/lib/postsvg/translation/handlers/rect_handler.rb +27 -0
- data/lib/postsvg/translation/handlers/shared.rb +110 -0
- data/lib/postsvg/translation/handlers/svg_handler.rb +25 -0
- data/lib/postsvg/translation/handlers/text_handler.rb +56 -0
- data/lib/postsvg/translation/handlers.rb +25 -0
- data/lib/postsvg/translation/ps_renderer.rb +105 -0
- data/lib/postsvg/translation/record_emitter.rb +35 -0
- data/lib/postsvg/translation.rb +17 -0
- data/lib/postsvg/version.rb +1 -1
- data/lib/postsvg/visitors/ps_visitor/arithmetic.rb +125 -0
- data/lib/postsvg/visitors/ps_visitor/boolean.rb +105 -0
- data/lib/postsvg/visitors/ps_visitor/color.rb +53 -0
- data/lib/postsvg/visitors/ps_visitor/common.rb +66 -0
- data/lib/postsvg/visitors/ps_visitor/container.rb +164 -0
- data/lib/postsvg/visitors/ps_visitor/control_flow.rb +110 -0
- data/lib/postsvg/visitors/ps_visitor/device.rb +20 -0
- data/lib/postsvg/visitors/ps_visitor/dictionary.rb +89 -0
- data/lib/postsvg/visitors/ps_visitor/font.rb +93 -0
- data/lib/postsvg/visitors/ps_visitor/graphics_state.rb +55 -0
- data/lib/postsvg/visitors/ps_visitor/painting.rb +90 -0
- data/lib/postsvg/visitors/ps_visitor/path.rb +112 -0
- data/lib/postsvg/visitors/ps_visitor/stack.rb +47 -0
- data/lib/postsvg/visitors/ps_visitor/transformations.rb +101 -0
- data/lib/postsvg/visitors/ps_visitor.rb +208 -0
- data/lib/postsvg/visitors.rb +9 -0
- data/lib/postsvg.rb +93 -59
- data/lychee.toml +86 -0
- metadata +216 -11
- 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]
|