@emasoft/svg-matrix 1.0.4 → 1.0.6
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.
- package/README.md +341 -304
- package/package.json +19 -1
- package/src/browser-verify.js +463 -0
- package/src/clip-path-resolver.js +759 -0
- package/src/geometry-to-path.js +348 -0
- package/src/index.js +413 -6
- package/src/marker-resolver.js +1006 -0
- package/src/mask-resolver.js +1407 -0
- package/src/mesh-gradient.js +1215 -0
- package/src/pattern-resolver.js +844 -0
- package/src/polygon-clip.js +1491 -0
- package/src/svg-flatten.js +1615 -76
- package/src/text-to-path.js +820 -0
- package/src/transforms2d.js +493 -37
- package/src/transforms3d.js +418 -47
- package/src/use-symbol-resolver.js +1126 -0
- package/samples/test.svg +0 -39
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Resolver Module - Flatten SVG pattern elements
|
|
3
|
+
*
|
|
4
|
+
* Resolves SVG pattern elements by expanding pattern tiles into
|
|
5
|
+
* concrete geometry with Decimal.js precision.
|
|
6
|
+
*
|
|
7
|
+
* Supports:
|
|
8
|
+
* - patternUnits (userSpaceOnUse, objectBoundingBox)
|
|
9
|
+
* - patternContentUnits (userSpaceOnUse, objectBoundingBox)
|
|
10
|
+
* - patternTransform
|
|
11
|
+
* - viewBox on patterns
|
|
12
|
+
* - Nested patterns (pattern referencing another pattern via href)
|
|
13
|
+
*
|
|
14
|
+
* @module pattern-resolver
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import Decimal from 'decimal.js';
|
|
18
|
+
import { Matrix } from './matrix.js';
|
|
19
|
+
import * as Transforms2D from './transforms2d.js';
|
|
20
|
+
import * as PolygonClip from './polygon-clip.js';
|
|
21
|
+
import * as ClipPathResolver from './clip-path-resolver.js';
|
|
22
|
+
|
|
23
|
+
Decimal.set({ precision: 80 });
|
|
24
|
+
|
|
25
|
+
const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse pattern element to structured data
|
|
29
|
+
*
|
|
30
|
+
* Extracts all relevant attributes from an SVG `<pattern>` element and its children,
|
|
31
|
+
* preparing them for pattern resolution. Handles nested patterns via href references.
|
|
32
|
+
*
|
|
33
|
+
* SVG Pattern Concepts:
|
|
34
|
+
* - **patternUnits**: Defines coordinate system for x, y, width, height
|
|
35
|
+
* - 'objectBoundingBox' (default): Values are fractions (0-1) of target element bbox
|
|
36
|
+
* - 'userSpaceOnUse': Values are absolute coordinates in user space
|
|
37
|
+
*
|
|
38
|
+
* - **patternContentUnits**: Defines coordinate system for pattern children
|
|
39
|
+
* - 'userSpaceOnUse' (default): Children use absolute coordinates
|
|
40
|
+
* - 'objectBoundingBox': Children coordinates scaled by target bbox
|
|
41
|
+
*
|
|
42
|
+
* - **patternTransform**: Additional transformation applied to the pattern
|
|
43
|
+
*
|
|
44
|
+
* - **viewBox**: Establishes coordinate system for pattern content, allowing
|
|
45
|
+
* scaling and aspect ratio control independent of pattern tile size
|
|
46
|
+
*
|
|
47
|
+
* @param {Element} patternElement - SVG pattern DOM element to parse
|
|
48
|
+
* @returns {Object} Parsed pattern data containing:
|
|
49
|
+
* - id {string}: Pattern element ID
|
|
50
|
+
* - patternUnits {string}: 'objectBoundingBox' or 'userSpaceOnUse'
|
|
51
|
+
* - patternContentUnits {string}: 'objectBoundingBox' or 'userSpaceOnUse'
|
|
52
|
+
* - patternTransform {string|null}: Transform attribute string
|
|
53
|
+
* - x {number}: Pattern tile x offset (default 0)
|
|
54
|
+
* - y {number}: Pattern tile y offset (default 0)
|
|
55
|
+
* - width {number}: Pattern tile width
|
|
56
|
+
* - height {number}: Pattern tile height
|
|
57
|
+
* - viewBox {string|null}: ViewBox attribute string
|
|
58
|
+
* - viewBoxParsed {Object|undefined}: Parsed viewBox {x, y, width, height}
|
|
59
|
+
* - preserveAspectRatio {string}: Aspect ratio preservation mode
|
|
60
|
+
* - href {string|null}: Reference to another pattern element
|
|
61
|
+
* - children {Array}: Array of parsed child element data
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* const patternEl = document.getElementById('myPattern');
|
|
65
|
+
* const data = parsePatternElement(patternEl);
|
|
66
|
+
* // {
|
|
67
|
+
* // id: 'myPattern',
|
|
68
|
+
* // patternUnits: 'objectBoundingBox',
|
|
69
|
+
* // x: 0, y: 0, width: 0.2, height: 0.2,
|
|
70
|
+
* // children: [{ type: 'rect', fill: 'blue', ... }]
|
|
71
|
+
* // }
|
|
72
|
+
*/
|
|
73
|
+
export function parsePatternElement(patternElement) {
|
|
74
|
+
const data = {
|
|
75
|
+
id: patternElement.getAttribute('id') || '',
|
|
76
|
+
patternUnits: patternElement.getAttribute('patternUnits') || 'objectBoundingBox',
|
|
77
|
+
patternContentUnits: patternElement.getAttribute('patternContentUnits') || 'userSpaceOnUse',
|
|
78
|
+
patternTransform: patternElement.getAttribute('patternTransform') || null,
|
|
79
|
+
x: patternElement.getAttribute('x'),
|
|
80
|
+
y: patternElement.getAttribute('y'),
|
|
81
|
+
width: patternElement.getAttribute('width'),
|
|
82
|
+
height: patternElement.getAttribute('height'),
|
|
83
|
+
viewBox: patternElement.getAttribute('viewBox') || null,
|
|
84
|
+
preserveAspectRatio: patternElement.getAttribute('preserveAspectRatio') || 'xMidYMid meet',
|
|
85
|
+
href: patternElement.getAttribute('href') ||
|
|
86
|
+
patternElement.getAttribute('xlink:href') || null,
|
|
87
|
+
children: []
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Parse numeric values
|
|
91
|
+
data.x = data.x !== null ? parseFloat(data.x) : 0;
|
|
92
|
+
data.y = data.y !== null ? parseFloat(data.y) : 0;
|
|
93
|
+
data.width = data.width !== null ? parseFloat(data.width) : 0;
|
|
94
|
+
data.height = data.height !== null ? parseFloat(data.height) : 0;
|
|
95
|
+
|
|
96
|
+
// Parse viewBox if present
|
|
97
|
+
if (data.viewBox) {
|
|
98
|
+
const parts = data.viewBox.trim().split(/[\s,]+/).map(Number);
|
|
99
|
+
if (parts.length === 4) {
|
|
100
|
+
data.viewBoxParsed = {
|
|
101
|
+
x: parts[0],
|
|
102
|
+
y: parts[1],
|
|
103
|
+
width: parts[2],
|
|
104
|
+
height: parts[3]
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Parse child elements
|
|
110
|
+
for (const child of patternElement.children) {
|
|
111
|
+
const tagName = child.tagName.toLowerCase();
|
|
112
|
+
const childData = {
|
|
113
|
+
type: tagName,
|
|
114
|
+
fill: child.getAttribute('fill') || 'black',
|
|
115
|
+
stroke: child.getAttribute('stroke') || 'none',
|
|
116
|
+
strokeWidth: parseFloat(child.getAttribute('stroke-width') || '1'),
|
|
117
|
+
opacity: parseFloat(child.getAttribute('opacity') || '1'),
|
|
118
|
+
transform: child.getAttribute('transform') || null
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Parse shape-specific attributes
|
|
122
|
+
switch (tagName) {
|
|
123
|
+
case 'rect':
|
|
124
|
+
childData.x = parseFloat(child.getAttribute('x') || '0');
|
|
125
|
+
childData.y = parseFloat(child.getAttribute('y') || '0');
|
|
126
|
+
childData.width = parseFloat(child.getAttribute('width') || '0');
|
|
127
|
+
childData.height = parseFloat(child.getAttribute('height') || '0');
|
|
128
|
+
childData.rx = parseFloat(child.getAttribute('rx') || '0');
|
|
129
|
+
childData.ry = parseFloat(child.getAttribute('ry') || '0');
|
|
130
|
+
break;
|
|
131
|
+
case 'circle':
|
|
132
|
+
childData.cx = parseFloat(child.getAttribute('cx') || '0');
|
|
133
|
+
childData.cy = parseFloat(child.getAttribute('cy') || '0');
|
|
134
|
+
childData.r = parseFloat(child.getAttribute('r') || '0');
|
|
135
|
+
break;
|
|
136
|
+
case 'ellipse':
|
|
137
|
+
childData.cx = parseFloat(child.getAttribute('cx') || '0');
|
|
138
|
+
childData.cy = parseFloat(child.getAttribute('cy') || '0');
|
|
139
|
+
childData.rx = parseFloat(child.getAttribute('rx') || '0');
|
|
140
|
+
childData.ry = parseFloat(child.getAttribute('ry') || '0');
|
|
141
|
+
break;
|
|
142
|
+
case 'path':
|
|
143
|
+
childData.d = child.getAttribute('d') || '';
|
|
144
|
+
break;
|
|
145
|
+
case 'polygon':
|
|
146
|
+
childData.points = child.getAttribute('points') || '';
|
|
147
|
+
break;
|
|
148
|
+
case 'polyline':
|
|
149
|
+
childData.points = child.getAttribute('points') || '';
|
|
150
|
+
break;
|
|
151
|
+
case 'line':
|
|
152
|
+
childData.x1 = parseFloat(child.getAttribute('x1') || '0');
|
|
153
|
+
childData.y1 = parseFloat(child.getAttribute('y1') || '0');
|
|
154
|
+
childData.x2 = parseFloat(child.getAttribute('x2') || '0');
|
|
155
|
+
childData.y2 = parseFloat(child.getAttribute('y2') || '0');
|
|
156
|
+
break;
|
|
157
|
+
case 'use':
|
|
158
|
+
childData.href = child.getAttribute('href') ||
|
|
159
|
+
child.getAttribute('xlink:href') || '';
|
|
160
|
+
childData.x = parseFloat(child.getAttribute('x') || '0');
|
|
161
|
+
childData.y = parseFloat(child.getAttribute('y') || '0');
|
|
162
|
+
break;
|
|
163
|
+
case 'g':
|
|
164
|
+
// Groups can contain nested shapes
|
|
165
|
+
childData.children = [];
|
|
166
|
+
for (const gc of child.children) {
|
|
167
|
+
childData.children.push({
|
|
168
|
+
type: gc.tagName.toLowerCase(),
|
|
169
|
+
fill: gc.getAttribute('fill') || 'inherit'
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
data.children.push(childData);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return data;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Calculate the pattern tile dimensions in user space
|
|
183
|
+
*
|
|
184
|
+
* Converts pattern tile coordinates from patternUnits space to absolute user space
|
|
185
|
+
* coordinates. The tile defines the repeating unit of the pattern.
|
|
186
|
+
*
|
|
187
|
+
* Pattern Tiling Mechanics:
|
|
188
|
+
* - Pattern tiles repeat infinitely in both x and y directions
|
|
189
|
+
* - The tile origin (x, y) defines where the first tile is positioned
|
|
190
|
+
* - Tiles are placed at (x + n*width, y + m*height) for all integers n, m
|
|
191
|
+
* - When patternUnits='objectBoundingBox', dimensions are fractions of target bbox
|
|
192
|
+
* - When patternUnits='userSpaceOnUse', dimensions are absolute coordinates
|
|
193
|
+
*
|
|
194
|
+
* @param {Object} patternData - Parsed pattern data from parsePatternElement()
|
|
195
|
+
* @param {Object} patternData.patternUnits - 'objectBoundingBox' or 'userSpaceOnUse'
|
|
196
|
+
* @param {number} patternData.x - Pattern tile x offset
|
|
197
|
+
* @param {number} patternData.y - Pattern tile y offset
|
|
198
|
+
* @param {number} patternData.width - Pattern tile width
|
|
199
|
+
* @param {number} patternData.height - Pattern tile height
|
|
200
|
+
* @param {Object} targetBBox - Target element bounding box in user space
|
|
201
|
+
* @param {number} targetBBox.x - Bounding box x coordinate
|
|
202
|
+
* @param {number} targetBBox.y - Bounding box y coordinate
|
|
203
|
+
* @param {number} targetBBox.width - Bounding box width
|
|
204
|
+
* @param {number} targetBBox.height - Bounding box height
|
|
205
|
+
* @returns {Object} Tile dimensions in user space containing:
|
|
206
|
+
* - x {Decimal}: Tile x origin in user space
|
|
207
|
+
* - y {Decimal}: Tile y origin in user space
|
|
208
|
+
* - width {Decimal}: Tile width in user space
|
|
209
|
+
* - height {Decimal}: Tile height in user space
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* // objectBoundingBox pattern (default)
|
|
213
|
+
* const data = { patternUnits: 'objectBoundingBox', x: 0, y: 0, width: 0.25, height: 0.25 };
|
|
214
|
+
* const bbox = { x: 100, y: 200, width: 400, height: 300 };
|
|
215
|
+
* const tile = getPatternTile(data, bbox);
|
|
216
|
+
* // Result: { x: 100, y: 200, width: 100, height: 75 }
|
|
217
|
+
* // Tile is 25% of target width and height
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* // userSpaceOnUse pattern
|
|
221
|
+
* const data = { patternUnits: 'userSpaceOnUse', x: 10, y: 20, width: 50, height: 50 };
|
|
222
|
+
* const bbox = { x: 100, y: 200, width: 400, height: 300 };
|
|
223
|
+
* const tile = getPatternTile(data, bbox);
|
|
224
|
+
* // Result: { x: 10, y: 20, width: 50, height: 50 }
|
|
225
|
+
* // Tile uses absolute coordinates, bbox is ignored
|
|
226
|
+
*/
|
|
227
|
+
export function getPatternTile(patternData, targetBBox) {
|
|
228
|
+
if (patternData.patternUnits === 'objectBoundingBox') {
|
|
229
|
+
// Dimensions are fractions of target bbox
|
|
230
|
+
return {
|
|
231
|
+
x: D(targetBBox.x).plus(D(patternData.x).mul(targetBBox.width)),
|
|
232
|
+
y: D(targetBBox.y).plus(D(patternData.y).mul(targetBBox.height)),
|
|
233
|
+
width: D(patternData.width).mul(targetBBox.width),
|
|
234
|
+
height: D(patternData.height).mul(targetBBox.height)
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// userSpaceOnUse - use values directly
|
|
239
|
+
return {
|
|
240
|
+
x: D(patternData.x),
|
|
241
|
+
y: D(patternData.y),
|
|
242
|
+
width: D(patternData.width),
|
|
243
|
+
height: D(patternData.height)
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Calculate transform matrix for pattern content
|
|
249
|
+
*
|
|
250
|
+
* Builds the transformation matrix that positions and scales pattern children
|
|
251
|
+
* within each pattern tile. Handles viewBox scaling and patternContentUnits.
|
|
252
|
+
*
|
|
253
|
+
* Transform Chain:
|
|
254
|
+
* 1. If viewBox is present: scale and translate to fit viewBox into tile
|
|
255
|
+
* 2. If patternContentUnits='objectBoundingBox': scale by target bbox dimensions
|
|
256
|
+
*
|
|
257
|
+
* ViewBox Handling:
|
|
258
|
+
* - viewBox establishes a coordinate system independent of tile size
|
|
259
|
+
* - Content in viewBox coordinates is scaled to fit tile dimensions
|
|
260
|
+
* - preserveAspectRatio controls scaling (default 'xMidYMid meet' uses uniform scale)
|
|
261
|
+
* - Content is centered within tile when aspect ratios differ
|
|
262
|
+
*
|
|
263
|
+
* @param {Object} patternData - Parsed pattern data from parsePatternElement()
|
|
264
|
+
* @param {Object} patternData.patternContentUnits - 'objectBoundingBox' or 'userSpaceOnUse'
|
|
265
|
+
* @param {Object} [patternData.viewBoxParsed] - Parsed viewBox {x, y, width, height}
|
|
266
|
+
* @param {string} [patternData.preserveAspectRatio] - Aspect ratio mode (default 'xMidYMid meet')
|
|
267
|
+
* @param {Object} tile - Pattern tile dimensions from getPatternTile()
|
|
268
|
+
* @param {Decimal} tile.x - Tile x origin
|
|
269
|
+
* @param {Decimal} tile.y - Tile y origin
|
|
270
|
+
* @param {Decimal} tile.width - Tile width
|
|
271
|
+
* @param {Decimal} tile.height - Tile height
|
|
272
|
+
* @param {Object} targetBBox - Target element bounding box in user space
|
|
273
|
+
* @param {number} targetBBox.x - Bounding box x coordinate
|
|
274
|
+
* @param {number} targetBBox.y - Bounding box y coordinate
|
|
275
|
+
* @param {number} targetBBox.width - Bounding box width
|
|
276
|
+
* @param {number} targetBBox.height - Bounding box height
|
|
277
|
+
* @returns {Matrix} 3x3 transformation matrix for pattern content
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* // Pattern with viewBox
|
|
281
|
+
* const data = {
|
|
282
|
+
* patternContentUnits: 'userSpaceOnUse',
|
|
283
|
+
* viewBoxParsed: { x: 0, y: 0, width: 100, height: 100 },
|
|
284
|
+
* preserveAspectRatio: 'xMidYMid meet'
|
|
285
|
+
* };
|
|
286
|
+
* const tile = { x: D(0), y: D(0), width: D(50), height: D(50) };
|
|
287
|
+
* const bbox = { x: 0, y: 0, width: 200, height: 200 };
|
|
288
|
+
* const M = getPatternContentTransform(data, tile, bbox);
|
|
289
|
+
* // M scales viewBox (100x100) to fit tile (50x50), scale = 0.5
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* // Pattern with objectBoundingBox content units
|
|
293
|
+
* const data = {
|
|
294
|
+
* patternContentUnits: 'objectBoundingBox',
|
|
295
|
+
* viewBoxParsed: null
|
|
296
|
+
* };
|
|
297
|
+
* const tile = { x: D(0), y: D(0), width: D(100), height: D(100) };
|
|
298
|
+
* const bbox = { x: 50, y: 50, width: 400, height: 300 };
|
|
299
|
+
* const M = getPatternContentTransform(data, tile, bbox);
|
|
300
|
+
* // M translates to bbox origin and scales by bbox dimensions
|
|
301
|
+
*/
|
|
302
|
+
export function getPatternContentTransform(patternData, tile, targetBBox) {
|
|
303
|
+
let M = Matrix.identity(3);
|
|
304
|
+
|
|
305
|
+
// Apply viewBox transform if present
|
|
306
|
+
if (patternData.viewBoxParsed) {
|
|
307
|
+
const vb = patternData.viewBoxParsed;
|
|
308
|
+
const tileWidth = Number(tile.width);
|
|
309
|
+
const tileHeight = Number(tile.height);
|
|
310
|
+
|
|
311
|
+
// Scale from viewBox to tile
|
|
312
|
+
const scaleX = tileWidth / vb.width;
|
|
313
|
+
const scaleY = tileHeight / vb.height;
|
|
314
|
+
|
|
315
|
+
// For 'xMidYMid meet', use uniform scale
|
|
316
|
+
const scale = Math.min(scaleX, scaleY);
|
|
317
|
+
|
|
318
|
+
// Center the content
|
|
319
|
+
const offsetX = (tileWidth - vb.width * scale) / 2;
|
|
320
|
+
const offsetY = (tileHeight - vb.height * scale) / 2;
|
|
321
|
+
|
|
322
|
+
M = M.mul(Transforms2D.translation(offsetX - vb.x * scale, offsetY - vb.y * scale));
|
|
323
|
+
M = M.mul(Transforms2D.scale(scale, scale));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Apply objectBoundingBox scaling if needed
|
|
327
|
+
if (patternData.patternContentUnits === 'objectBoundingBox') {
|
|
328
|
+
M = M.mul(Transforms2D.translation(targetBBox.x, targetBBox.y));
|
|
329
|
+
M = M.mul(Transforms2D.scale(targetBBox.width, targetBBox.height));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return M;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Convert pattern child element to polygon representation
|
|
337
|
+
*
|
|
338
|
+
* Transforms a pattern's child shape (rect, circle, path, etc.) into a polygon
|
|
339
|
+
* by applying the appropriate transformation matrix. Uses sampling for curves.
|
|
340
|
+
*
|
|
341
|
+
* @param {Object} child - Pattern child element data from parsePatternElement()
|
|
342
|
+
* @param {string} child.type - Element type ('rect', 'circle', 'ellipse', 'path', etc.)
|
|
343
|
+
* @param {string} [child.transform] - Element's transform attribute
|
|
344
|
+
* @param {Matrix} [transform=null] - Additional transform matrix to apply (e.g., tile position + content transform)
|
|
345
|
+
* @param {number} [samples=20] - Number of samples for approximating curves as line segments
|
|
346
|
+
* @returns {Array<{x: number, y: number}>} Array of polygon vertices in user space
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* const child = {
|
|
350
|
+
* type: 'rect',
|
|
351
|
+
* x: 0, y: 0,
|
|
352
|
+
* width: 10, height: 10,
|
|
353
|
+
* fill: 'red'
|
|
354
|
+
* };
|
|
355
|
+
* const M = Transforms2D.translation(100, 50);
|
|
356
|
+
* const polygon = patternChildToPolygon(child, M);
|
|
357
|
+
* // Returns rectangle vertices translated to (100, 50)
|
|
358
|
+
*/
|
|
359
|
+
export function patternChildToPolygon(child, transform = null, samples = 20) {
|
|
360
|
+
// Create element-like object for ClipPathResolver
|
|
361
|
+
const element = {
|
|
362
|
+
type: child.type,
|
|
363
|
+
...child,
|
|
364
|
+
transform: child.transform
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// Get polygon using ClipPathResolver
|
|
368
|
+
let polygon = ClipPathResolver.shapeToPolygon(element, null, samples);
|
|
369
|
+
|
|
370
|
+
// Apply additional transform if provided
|
|
371
|
+
if (transform && polygon.length > 0) {
|
|
372
|
+
polygon = polygon.map(p => {
|
|
373
|
+
const [x, y] = Transforms2D.applyTransform(transform, p.x, p.y);
|
|
374
|
+
return { x, y };
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return polygon;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Generate pattern tile positions that cover a bounding box
|
|
383
|
+
*
|
|
384
|
+
* Calculates all tile origin positions needed to cover the target area.
|
|
385
|
+
* Tiles are arranged in a grid, with each tile positioned at:
|
|
386
|
+
* (tile.x + i * tile.width, tile.y + j * tile.height)
|
|
387
|
+
* for integer indices i, j.
|
|
388
|
+
*
|
|
389
|
+
* Pattern Tile Generation:
|
|
390
|
+
* - Determines which tile indices (i, j) are needed to cover coverBBox
|
|
391
|
+
* - Generates positions for all tiles that overlap or touch the bbox
|
|
392
|
+
* - Returns array of tile origins in user space coordinates
|
|
393
|
+
* - Used by resolvePattern() to instantiate pattern content across target
|
|
394
|
+
*
|
|
395
|
+
* @param {Object} tile - Pattern tile dimensions from getPatternTile()
|
|
396
|
+
* @param {Decimal} tile.x - Tile x origin
|
|
397
|
+
* @param {Decimal} tile.y - Tile y origin
|
|
398
|
+
* @param {Decimal} tile.width - Tile width (must be > 0)
|
|
399
|
+
* @param {Decimal} tile.height - Tile height (must be > 0)
|
|
400
|
+
* @param {Object} coverBBox - Bounding box area to cover with tiles
|
|
401
|
+
* @param {number} coverBBox.x - Area x coordinate
|
|
402
|
+
* @param {number} coverBBox.y - Area y coordinate
|
|
403
|
+
* @param {number} coverBBox.width - Area width
|
|
404
|
+
* @param {number} coverBBox.height - Area height
|
|
405
|
+
* @returns {Array<{x: Decimal, y: Decimal}>} Array of tile origin positions in user space
|
|
406
|
+
*
|
|
407
|
+
* @example
|
|
408
|
+
* const tile = { x: D(0), y: D(0), width: D(50), height: D(50) };
|
|
409
|
+
* const bbox = { x: 25, y: 25, width: 150, height: 100 };
|
|
410
|
+
* const positions = getTilePositions(tile, bbox);
|
|
411
|
+
* // Returns positions for tiles covering the bbox:
|
|
412
|
+
* // [{ x: 0, y: 0 }, { x: 50, y: 0 }, { x: 100, y: 0 }, { x: 150, y: 0 },
|
|
413
|
+
* // { x: 0, y: 50 }, { x: 50, y: 50 }, { x: 100, y: 50 }, { x: 150, y: 50 }]
|
|
414
|
+
*/
|
|
415
|
+
export function getTilePositions(tile, coverBBox) {
|
|
416
|
+
const positions = [];
|
|
417
|
+
|
|
418
|
+
const tileX = Number(tile.x);
|
|
419
|
+
const tileY = Number(tile.y);
|
|
420
|
+
const tileW = Number(tile.width);
|
|
421
|
+
const tileH = Number(tile.height);
|
|
422
|
+
|
|
423
|
+
if (tileW <= 0 || tileH <= 0) return positions;
|
|
424
|
+
|
|
425
|
+
// Calculate start and end indices
|
|
426
|
+
const startI = Math.floor((coverBBox.x - tileX) / tileW);
|
|
427
|
+
const endI = Math.ceil((coverBBox.x + coverBBox.width - tileX) / tileW);
|
|
428
|
+
const startJ = Math.floor((coverBBox.y - tileY) / tileH);
|
|
429
|
+
const endJ = Math.ceil((coverBBox.y + coverBBox.height - tileY) / tileH);
|
|
430
|
+
|
|
431
|
+
for (let i = startI; i < endI; i++) {
|
|
432
|
+
for (let j = startJ; j < endJ; j++) {
|
|
433
|
+
positions.push({
|
|
434
|
+
x: D(tileX).plus(D(tileW).mul(i)),
|
|
435
|
+
y: D(tileY).plus(D(tileH).mul(j))
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return positions;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Resolve pattern to set of polygons with associated styles
|
|
445
|
+
*
|
|
446
|
+
* Expands a pattern definition into concrete geometry by:
|
|
447
|
+
* 1. Computing tile dimensions based on patternUnits
|
|
448
|
+
* 2. Generating tile positions to cover the target area
|
|
449
|
+
* 3. For each tile, transforming pattern children to polygons
|
|
450
|
+
* 4. Returning all polygons with their fill, stroke, and opacity
|
|
451
|
+
*
|
|
452
|
+
* This is the main pattern resolution function that converts the declarative
|
|
453
|
+
* SVG pattern into explicit polygon geometry suitable for rendering or further
|
|
454
|
+
* processing (e.g., clipping against target shapes).
|
|
455
|
+
*
|
|
456
|
+
* @param {Object} patternData - Parsed pattern data from parsePatternElement()
|
|
457
|
+
* @param {Array} patternData.children - Array of child shape data
|
|
458
|
+
* @param {Object} targetBBox - Target element bounding box to fill with pattern
|
|
459
|
+
* @param {number} targetBBox.x - Bounding box x coordinate
|
|
460
|
+
* @param {number} targetBBox.y - Bounding box y coordinate
|
|
461
|
+
* @param {number} targetBBox.width - Bounding box width
|
|
462
|
+
* @param {number} targetBBox.height - Bounding box height
|
|
463
|
+
* @param {Object} [options={}] - Resolution options
|
|
464
|
+
* @param {number} [options.samples=20] - Number of samples for curve approximation
|
|
465
|
+
* @param {number} [options.maxTiles=1000] - Maximum number of tiles to generate (performance limit)
|
|
466
|
+
* @returns {Array<Object>} Array of styled polygons, each containing:
|
|
467
|
+
* - polygon {Array<{x, y}>}: Polygon vertices in user space
|
|
468
|
+
* - fill {string}: Fill color (e.g., 'red', '#ff0000')
|
|
469
|
+
* - stroke {string}: Stroke color
|
|
470
|
+
* - strokeWidth {number}: Stroke width
|
|
471
|
+
* - opacity {number}: Opacity value (0-1)
|
|
472
|
+
*
|
|
473
|
+
* @example
|
|
474
|
+
* const patternData = parsePatternElement(patternEl);
|
|
475
|
+
* const targetBBox = { x: 0, y: 0, width: 200, height: 200 };
|
|
476
|
+
* const polygons = resolvePattern(patternData, targetBBox);
|
|
477
|
+
* // Returns array of polygons representing the pattern content
|
|
478
|
+
* // Each polygon can be rendered with its associated fill and stroke
|
|
479
|
+
*/
|
|
480
|
+
export function resolvePattern(patternData, targetBBox, options = {}) {
|
|
481
|
+
const { samples = 20, maxTiles = 1000 } = options;
|
|
482
|
+
const result = [];
|
|
483
|
+
|
|
484
|
+
// Get tile dimensions
|
|
485
|
+
const tile = getPatternTile(patternData, targetBBox);
|
|
486
|
+
|
|
487
|
+
if (Number(tile.width) <= 0 || Number(tile.height) <= 0) {
|
|
488
|
+
return result;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Get content transform
|
|
492
|
+
const contentTransform = getPatternContentTransform(patternData, tile, targetBBox);
|
|
493
|
+
|
|
494
|
+
// Get tile positions
|
|
495
|
+
const positions = getTilePositions(tile, targetBBox);
|
|
496
|
+
|
|
497
|
+
// Limit number of tiles
|
|
498
|
+
const limitedPositions = positions.slice(0, maxTiles);
|
|
499
|
+
|
|
500
|
+
// Convert each child in each tile
|
|
501
|
+
for (const pos of limitedPositions) {
|
|
502
|
+
const tileTranslate = Transforms2D.translation(pos.x, pos.y);
|
|
503
|
+
|
|
504
|
+
for (const child of patternData.children) {
|
|
505
|
+
// Combine transforms: tile position + content transform + child transform
|
|
506
|
+
let M = tileTranslate.mul(contentTransform);
|
|
507
|
+
|
|
508
|
+
let polygon = patternChildToPolygon(child, M, samples);
|
|
509
|
+
|
|
510
|
+
if (polygon.length >= 3) {
|
|
511
|
+
result.push({
|
|
512
|
+
polygon,
|
|
513
|
+
fill: child.fill,
|
|
514
|
+
stroke: child.stroke,
|
|
515
|
+
strokeWidth: child.strokeWidth,
|
|
516
|
+
opacity: child.opacity
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return result;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Apply pattern fill to a target polygon
|
|
527
|
+
*
|
|
528
|
+
* Resolves the pattern and clips it against the target polygon, returning only
|
|
529
|
+
* the visible portions of the pattern. This is the key function for applying
|
|
530
|
+
* patterns to arbitrary shapes.
|
|
531
|
+
*
|
|
532
|
+
* Pattern Application Process:
|
|
533
|
+
* 1. Resolve pattern to polygons using resolvePattern()
|
|
534
|
+
* 2. For each pattern polygon, compute intersection with target polygon
|
|
535
|
+
* 3. Return only the intersecting portions with their styles
|
|
536
|
+
* 4. Results can be directly rendered to visualize the pattern fill
|
|
537
|
+
*
|
|
538
|
+
* @param {Array<{x: number, y: number}>} targetPolygon - Target polygon vertices to fill
|
|
539
|
+
* @param {Object} patternData - Parsed pattern data from parsePatternElement()
|
|
540
|
+
* @param {Object} targetBBox - Target element bounding box
|
|
541
|
+
* @param {number} targetBBox.x - Bounding box x coordinate
|
|
542
|
+
* @param {number} targetBBox.y - Bounding box y coordinate
|
|
543
|
+
* @param {number} targetBBox.width - Bounding box width
|
|
544
|
+
* @param {number} targetBBox.height - Bounding box height
|
|
545
|
+
* @param {Object} [options={}] - Resolution options (passed to resolvePattern)
|
|
546
|
+
* @param {number} [options.samples=20] - Curve sampling resolution
|
|
547
|
+
* @param {number} [options.maxTiles=1000] - Maximum tiles to generate
|
|
548
|
+
* @returns {Array<Object>} Array of clipped polygons, each containing:
|
|
549
|
+
* - polygon {Array<{x, y}>}: Clipped polygon vertices (intersection with target)
|
|
550
|
+
* - fill {string}: Fill color
|
|
551
|
+
* - opacity {number}: Opacity value
|
|
552
|
+
*
|
|
553
|
+
* @example
|
|
554
|
+
* // Apply striped pattern to a circle
|
|
555
|
+
* const circlePolygon = [...]; // Circle approximated as polygon
|
|
556
|
+
* const patternData = parsePatternElement(stripesPatternEl);
|
|
557
|
+
* const bbox = { x: 0, y: 0, width: 100, height: 100 };
|
|
558
|
+
* const clippedParts = applyPattern(circlePolygon, patternData, bbox);
|
|
559
|
+
* // Returns only the stripe portions visible within the circle
|
|
560
|
+
*/
|
|
561
|
+
export function applyPattern(targetPolygon, patternData, targetBBox, options = {}) {
|
|
562
|
+
const patternPolygons = resolvePattern(patternData, targetBBox, options);
|
|
563
|
+
const result = [];
|
|
564
|
+
|
|
565
|
+
for (const { polygon, fill, opacity } of patternPolygons) {
|
|
566
|
+
if (opacity <= 0) continue;
|
|
567
|
+
|
|
568
|
+
const intersection = PolygonClip.polygonIntersection(targetPolygon, polygon);
|
|
569
|
+
|
|
570
|
+
for (const clippedPoly of intersection) {
|
|
571
|
+
if (clippedPoly.length >= 3) {
|
|
572
|
+
result.push({
|
|
573
|
+
polygon: clippedPoly,
|
|
574
|
+
fill,
|
|
575
|
+
opacity
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return result;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Flatten pattern to single combined polygon (union of all shapes)
|
|
586
|
+
*
|
|
587
|
+
* Combines all pattern content into a single unified polygon by computing
|
|
588
|
+
* the union of all shapes. Useful for creating clip paths from patterns or
|
|
589
|
+
* for simplified pattern representations.
|
|
590
|
+
*
|
|
591
|
+
* @param {Object} patternData - Parsed pattern data from parsePatternElement()
|
|
592
|
+
* @param {Object} targetBBox - Target element bounding box
|
|
593
|
+
* @param {number} targetBBox.x - Bounding box x coordinate
|
|
594
|
+
* @param {number} targetBBox.y - Bounding box y coordinate
|
|
595
|
+
* @param {number} targetBBox.width - Bounding box width
|
|
596
|
+
* @param {number} targetBBox.height - Bounding box height
|
|
597
|
+
* @param {Object} [options={}] - Resolution options (passed to resolvePattern)
|
|
598
|
+
* @param {number} [options.samples=20] - Curve sampling resolution
|
|
599
|
+
* @param {number} [options.maxTiles=1000] - Maximum tiles to generate
|
|
600
|
+
* @returns {Array<{x: number, y: number}>} Combined polygon vertices representing union of all pattern shapes
|
|
601
|
+
*
|
|
602
|
+
* @example
|
|
603
|
+
* // Create a clip path from a dot pattern
|
|
604
|
+
* const dotPatternData = parsePatternElement(dotPatternEl);
|
|
605
|
+
* const bbox = { x: 0, y: 0, width: 200, height: 200 };
|
|
606
|
+
* const clipPolygon = patternToClipPath(dotPatternData, bbox);
|
|
607
|
+
* // Returns single polygon that is the union of all dots in the pattern
|
|
608
|
+
*/
|
|
609
|
+
export function patternToClipPath(patternData, targetBBox, options = {}) {
|
|
610
|
+
const patternPolygons = resolvePattern(patternData, targetBBox, options);
|
|
611
|
+
|
|
612
|
+
// Union all polygons
|
|
613
|
+
let result = [];
|
|
614
|
+
|
|
615
|
+
for (const { polygon, opacity } of patternPolygons) {
|
|
616
|
+
if (opacity > 0 && polygon.length >= 3) {
|
|
617
|
+
if (result.length === 0) {
|
|
618
|
+
result = polygon;
|
|
619
|
+
} else {
|
|
620
|
+
const unionResult = PolygonClip.polygonUnion(result, polygon);
|
|
621
|
+
if (unionResult.length > 0 && unionResult[0].length >= 3) {
|
|
622
|
+
result = unionResult[0];
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return result;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Generate SVG path data for pattern content
|
|
633
|
+
*
|
|
634
|
+
* Converts the flattened pattern polygon into SVG path data string that can
|
|
635
|
+
* be used in a `<path>` element's `d` attribute.
|
|
636
|
+
*
|
|
637
|
+
* @param {Object} patternData - Parsed pattern data from parsePatternElement()
|
|
638
|
+
* @param {Object} targetBBox - Target element bounding box
|
|
639
|
+
* @param {number} targetBBox.x - Bounding box x coordinate
|
|
640
|
+
* @param {number} targetBBox.y - Bounding box y coordinate
|
|
641
|
+
* @param {number} targetBBox.width - Bounding box width
|
|
642
|
+
* @param {number} targetBBox.height - Bounding box height
|
|
643
|
+
* @param {Object} [options={}] - Resolution options (passed to patternToClipPath)
|
|
644
|
+
* @param {number} [options.samples=20] - Curve sampling resolution
|
|
645
|
+
* @param {number} [options.maxTiles=1000] - Maximum tiles to generate
|
|
646
|
+
* @returns {string} SVG path data string (e.g., "M 0 0 L 10 0 L 10 10 Z")
|
|
647
|
+
*
|
|
648
|
+
* @example
|
|
649
|
+
* const patternData = parsePatternElement(patternEl);
|
|
650
|
+
* const bbox = { x: 0, y: 0, width: 100, height: 100 };
|
|
651
|
+
* const pathData = patternToPathData(patternData, bbox);
|
|
652
|
+
* // Returns: "M 0.000000 0.000000 L 10.000000 0.000000 L 10.000000 10.000000 Z"
|
|
653
|
+
* // Can be used in: <path d={pathData} />
|
|
654
|
+
*/
|
|
655
|
+
export function patternToPathData(patternData, targetBBox, options = {}) {
|
|
656
|
+
const polygon = patternToClipPath(patternData, targetBBox, options);
|
|
657
|
+
|
|
658
|
+
if (polygon.length < 3) return '';
|
|
659
|
+
|
|
660
|
+
let d = '';
|
|
661
|
+
for (let i = 0; i < polygon.length; i++) {
|
|
662
|
+
const p = polygon[i];
|
|
663
|
+
const x = Number(p.x).toFixed(6);
|
|
664
|
+
const y = Number(p.y).toFixed(6);
|
|
665
|
+
d += i === 0 ? `M ${x} ${y}` : ` L ${x} ${y}`;
|
|
666
|
+
}
|
|
667
|
+
d += ' Z';
|
|
668
|
+
|
|
669
|
+
return d;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Calculate pattern tile count for a given area
|
|
674
|
+
*
|
|
675
|
+
* Computes how many tiles (columns and rows) are needed to cover the target
|
|
676
|
+
* bounding box. Useful for estimating pattern complexity and performance.
|
|
677
|
+
*
|
|
678
|
+
* @param {Object} patternData - Parsed pattern data from parsePatternElement()
|
|
679
|
+
* @param {Object} targetBBox - Target element bounding box to cover
|
|
680
|
+
* @param {number} targetBBox.width - Bounding box width
|
|
681
|
+
* @param {number} targetBBox.height - Bounding box height
|
|
682
|
+
* @returns {Object} Tile count information:
|
|
683
|
+
* - columns {number}: Number of tile columns needed
|
|
684
|
+
* - rows {number}: Number of tile rows needed
|
|
685
|
+
* - total {number}: Total number of tiles (columns * rows)
|
|
686
|
+
*
|
|
687
|
+
* @example
|
|
688
|
+
* const patternData = { width: 0.25, height: 0.25, patternUnits: 'objectBoundingBox' };
|
|
689
|
+
* const bbox = { x: 0, y: 0, width: 400, height: 300 };
|
|
690
|
+
* const tileCount = getPatternTileCount(patternData, bbox);
|
|
691
|
+
* // With 0.25 fraction: tile is 100x75, needs 4 columns and 4 rows
|
|
692
|
+
* // Result: { columns: 4, rows: 4, total: 16 }
|
|
693
|
+
*/
|
|
694
|
+
export function getPatternTileCount(patternData, targetBBox) {
|
|
695
|
+
const tile = getPatternTile(patternData, targetBBox);
|
|
696
|
+
|
|
697
|
+
const tileW = Number(tile.width);
|
|
698
|
+
const tileH = Number(tile.height);
|
|
699
|
+
|
|
700
|
+
if (tileW <= 0 || tileH <= 0) {
|
|
701
|
+
return { columns: 0, rows: 0, total: 0 };
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const columns = Math.ceil(targetBBox.width / tileW);
|
|
705
|
+
const rows = Math.ceil(targetBBox.height / tileH);
|
|
706
|
+
|
|
707
|
+
return {
|
|
708
|
+
columns,
|
|
709
|
+
rows,
|
|
710
|
+
total: columns * rows
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Get bounding box of pattern content
|
|
716
|
+
*
|
|
717
|
+
* Calculates the bounding box that encloses all child shapes within the pattern
|
|
718
|
+
* definition. Analyzes only the intrinsic geometry of children, not their
|
|
719
|
+
* positions after tile/transform application.
|
|
720
|
+
*
|
|
721
|
+
* Supported shape types: rect, circle, ellipse, line
|
|
722
|
+
* (Other shapes like path, polygon return no bbox contribution)
|
|
723
|
+
*
|
|
724
|
+
* @param {Object} patternData - Parsed pattern data from parsePatternElement()
|
|
725
|
+
* @param {Array} patternData.children - Array of child element data
|
|
726
|
+
* @returns {Object} Bounding box of pattern content:
|
|
727
|
+
* - x {number}: Left edge
|
|
728
|
+
* - y {number}: Top edge
|
|
729
|
+
* - width {number}: Width
|
|
730
|
+
* - height {number}: Height
|
|
731
|
+
* Returns {x: 0, y: 0, width: 0, height: 0} if no children have computable bbox
|
|
732
|
+
*
|
|
733
|
+
* @example
|
|
734
|
+
* const patternData = {
|
|
735
|
+
* children: [
|
|
736
|
+
* { type: 'rect', x: 10, y: 20, width: 50, height: 30 },
|
|
737
|
+
* { type: 'circle', cx: 80, cy: 50, r: 10 }
|
|
738
|
+
* ]
|
|
739
|
+
* };
|
|
740
|
+
* const bbox = getPatternContentBBox(patternData);
|
|
741
|
+
* // Result: { x: 10, y: 20, width: 80, height: 40 }
|
|
742
|
+
* // (from x:10 to x:90, y:20 to y:60)
|
|
743
|
+
*/
|
|
744
|
+
export function getPatternContentBBox(patternData) {
|
|
745
|
+
let minX = Infinity;
|
|
746
|
+
let minY = Infinity;
|
|
747
|
+
let maxX = -Infinity;
|
|
748
|
+
let maxY = -Infinity;
|
|
749
|
+
|
|
750
|
+
for (const child of patternData.children) {
|
|
751
|
+
let childBBox = null;
|
|
752
|
+
|
|
753
|
+
switch (child.type) {
|
|
754
|
+
case 'rect':
|
|
755
|
+
childBBox = {
|
|
756
|
+
x: child.x,
|
|
757
|
+
y: child.y,
|
|
758
|
+
width: child.width,
|
|
759
|
+
height: child.height
|
|
760
|
+
};
|
|
761
|
+
break;
|
|
762
|
+
case 'circle':
|
|
763
|
+
childBBox = {
|
|
764
|
+
x: child.cx - child.r,
|
|
765
|
+
y: child.cy - child.r,
|
|
766
|
+
width: child.r * 2,
|
|
767
|
+
height: child.r * 2
|
|
768
|
+
};
|
|
769
|
+
break;
|
|
770
|
+
case 'ellipse':
|
|
771
|
+
childBBox = {
|
|
772
|
+
x: child.cx - child.rx,
|
|
773
|
+
y: child.cy - child.ry,
|
|
774
|
+
width: child.rx * 2,
|
|
775
|
+
height: child.ry * 2
|
|
776
|
+
};
|
|
777
|
+
break;
|
|
778
|
+
case 'line':
|
|
779
|
+
childBBox = {
|
|
780
|
+
x: Math.min(child.x1, child.x2),
|
|
781
|
+
y: Math.min(child.y1, child.y2),
|
|
782
|
+
width: Math.abs(child.x2 - child.x1),
|
|
783
|
+
height: Math.abs(child.y2 - child.y1)
|
|
784
|
+
};
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (childBBox) {
|
|
789
|
+
minX = Math.min(minX, childBBox.x);
|
|
790
|
+
minY = Math.min(minY, childBBox.y);
|
|
791
|
+
maxX = Math.max(maxX, childBBox.x + childBBox.width);
|
|
792
|
+
maxY = Math.max(maxY, childBBox.y + childBBox.height);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (minX === Infinity) {
|
|
797
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
return {
|
|
801
|
+
x: minX,
|
|
802
|
+
y: minY,
|
|
803
|
+
width: maxX - minX,
|
|
804
|
+
height: maxY - minY
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Parse patternTransform attribute
|
|
810
|
+
*
|
|
811
|
+
* Converts an SVG transform string (e.g., "rotate(45) translate(10, 20)")
|
|
812
|
+
* into a 3x3 transformation matrix. The patternTransform attribute allows
|
|
813
|
+
* additional transformations to be applied to the entire pattern coordinate system.
|
|
814
|
+
*
|
|
815
|
+
* PatternTransform Attribute:
|
|
816
|
+
* - Applied to the pattern coordinate system before patternUnits scaling
|
|
817
|
+
* - Can contain any SVG transform functions: translate, rotate, scale, skewX, skewY, matrix
|
|
818
|
+
* - Useful for rotating, scaling, or skewing entire patterns
|
|
819
|
+
* - Composed right-to-left (last transform applied first in coordinate chain)
|
|
820
|
+
*
|
|
821
|
+
* @param {string|null} transformStr - SVG transform attribute string
|
|
822
|
+
* Examples: "rotate(45)", "translate(10, 20) scale(2)", "matrix(1,0,0,1,0,0)"
|
|
823
|
+
* @returns {Matrix} 3x3 transformation matrix (identity matrix if transformStr is null/empty)
|
|
824
|
+
*
|
|
825
|
+
* @example
|
|
826
|
+
* const M = parsePatternTransform("rotate(45)");
|
|
827
|
+
* // Returns 3x3 matrix representing 45-degree rotation
|
|
828
|
+
*
|
|
829
|
+
* @example
|
|
830
|
+
* const M = parsePatternTransform("translate(100, 50) scale(2)");
|
|
831
|
+
* // Returns matrix that first scales by 2, then translates by (100, 50)
|
|
832
|
+
*
|
|
833
|
+
* @example
|
|
834
|
+
* const M = parsePatternTransform(null);
|
|
835
|
+
* // Returns identity matrix (no transformation)
|
|
836
|
+
*/
|
|
837
|
+
export function parsePatternTransform(transformStr) {
|
|
838
|
+
if (!transformStr) {
|
|
839
|
+
return Matrix.identity(3);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Use ClipPathResolver's transform parser
|
|
843
|
+
return ClipPathResolver.parseTransform(transformStr);
|
|
844
|
+
}
|