@emasoft/svg-matrix 1.0.19 → 1.0.21
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 +256 -759
- package/bin/svg-matrix.js +171 -2
- package/bin/svglinter.cjs +1162 -0
- package/package.json +9 -3
- package/src/animation-optimization.js +394 -0
- package/src/animation-references.js +440 -0
- package/src/arc-length.js +940 -0
- package/src/bezier-analysis.js +1626 -0
- package/src/bezier-intersections.js +1369 -0
- package/src/clip-path-resolver.js +110 -2
- package/src/convert-path-data.js +583 -0
- package/src/css-specificity.js +443 -0
- package/src/douglas-peucker.js +356 -0
- package/src/flatten-pipeline.js +109 -4
- package/src/geometry-to-path.js +126 -16
- package/src/gjk-collision.js +840 -0
- package/src/index.js +175 -2
- package/src/off-canvas-detection.js +1222 -0
- package/src/path-analysis.js +1241 -0
- package/src/path-data-plugins.js +928 -0
- package/src/path-optimization.js +825 -0
- package/src/path-simplification.js +1140 -0
- package/src/polygon-clip.js +376 -99
- package/src/svg-boolean-ops.js +898 -0
- package/src/svg-collections.js +910 -0
- package/src/svg-parser.js +175 -16
- package/src/svg-rendering-context.js +627 -0
- package/src/svg-toolbox.js +7495 -0
- package/src/svg-validation-data.js +944 -0
- package/src/transform-decomposition.js +810 -0
- package/src/transform-optimization.js +936 -0
- package/src/use-symbol-resolver.js +75 -7
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Douglas-Peucker Path Simplification Algorithm
|
|
3
|
+
*
|
|
4
|
+
* Reduces the number of points in a polyline while preserving its shape.
|
|
5
|
+
* The algorithm recursively finds the point furthest from the line segment
|
|
6
|
+
* and keeps it if the distance exceeds the tolerance.
|
|
7
|
+
*
|
|
8
|
+
* Time complexity: O(n^2) worst case, O(n log n) average
|
|
9
|
+
*
|
|
10
|
+
* @module douglas-peucker
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Calculate perpendicular distance from a point to a line segment.
|
|
15
|
+
* @param {{x: number, y: number}} point - The point
|
|
16
|
+
* @param {{x: number, y: number}} lineStart - Line segment start
|
|
17
|
+
* @param {{x: number, y: number}} lineEnd - Line segment end
|
|
18
|
+
* @returns {number} Perpendicular distance
|
|
19
|
+
*/
|
|
20
|
+
export function perpendicularDistance(point, lineStart, lineEnd) {
|
|
21
|
+
const dx = lineEnd.x - lineStart.x;
|
|
22
|
+
const dy = lineEnd.y - lineStart.y;
|
|
23
|
+
|
|
24
|
+
// Handle degenerate case where line segment is a point
|
|
25
|
+
const lineLengthSq = dx * dx + dy * dy;
|
|
26
|
+
if (lineLengthSq < 1e-10) {
|
|
27
|
+
const pdx = point.x - lineStart.x;
|
|
28
|
+
const pdy = point.y - lineStart.y;
|
|
29
|
+
return Math.sqrt(pdx * pdx + pdy * pdy);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Calculate perpendicular distance using cross product formula
|
|
33
|
+
// |((y2-y1)*x0 - (x2-x1)*y0 + x2*y1 - y2*x1)| / sqrt((y2-y1)^2 + (x2-x1)^2)
|
|
34
|
+
const numerator = Math.abs(
|
|
35
|
+
dy * point.x - dx * point.y + lineEnd.x * lineStart.y - lineEnd.y * lineStart.x
|
|
36
|
+
);
|
|
37
|
+
const denominator = Math.sqrt(lineLengthSq);
|
|
38
|
+
|
|
39
|
+
return numerator / denominator;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Douglas-Peucker simplification algorithm (recursive implementation).
|
|
44
|
+
* @param {Array<{x: number, y: number}>} points - Array of points
|
|
45
|
+
* @param {number} tolerance - Maximum allowed deviation
|
|
46
|
+
* @returns {Array<{x: number, y: number}>} Simplified points
|
|
47
|
+
*/
|
|
48
|
+
export function douglasPeucker(points, tolerance) {
|
|
49
|
+
if (points.length <= 2) {
|
|
50
|
+
return points;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Find the point with maximum distance from the line between first and last
|
|
54
|
+
let maxDistance = 0;
|
|
55
|
+
let maxIndex = 0;
|
|
56
|
+
|
|
57
|
+
const first = points[0];
|
|
58
|
+
const last = points[points.length - 1];
|
|
59
|
+
|
|
60
|
+
for (let i = 1; i < points.length - 1; i++) {
|
|
61
|
+
const dist = perpendicularDistance(points[i], first, last);
|
|
62
|
+
if (dist > maxDistance) {
|
|
63
|
+
maxDistance = dist;
|
|
64
|
+
maxIndex = i;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// If max distance exceeds tolerance, recursively simplify both halves
|
|
69
|
+
if (maxDistance > tolerance) {
|
|
70
|
+
const leftHalf = douglasPeucker(points.slice(0, maxIndex + 1), tolerance);
|
|
71
|
+
const rightHalf = douglasPeucker(points.slice(maxIndex), tolerance);
|
|
72
|
+
|
|
73
|
+
// Combine results (remove duplicate point at junction)
|
|
74
|
+
return leftHalf.slice(0, -1).concat(rightHalf);
|
|
75
|
+
} else {
|
|
76
|
+
// All points are within tolerance, keep only endpoints
|
|
77
|
+
return [first, last];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Visvalingam-Whyatt simplification algorithm.
|
|
83
|
+
* Removes points based on the area of the triangle they form.
|
|
84
|
+
* Better for preserving overall shape character than Douglas-Peucker.
|
|
85
|
+
*
|
|
86
|
+
* Time complexity: O(n log n) with heap, O(n^2) without
|
|
87
|
+
*
|
|
88
|
+
* @param {Array<{x: number, y: number}>} points - Array of points
|
|
89
|
+
* @param {number} minArea - Minimum triangle area to keep
|
|
90
|
+
* @returns {Array<{x: number, y: number}>} Simplified points
|
|
91
|
+
*/
|
|
92
|
+
export function visvalingamWhyatt(points, minArea) {
|
|
93
|
+
if (points.length <= 2) {
|
|
94
|
+
return points;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Calculate triangle area for three points
|
|
98
|
+
const triangleArea = (p1, p2, p3) => {
|
|
99
|
+
return Math.abs(
|
|
100
|
+
(p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y)
|
|
101
|
+
) / 2;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Create a copy with area information
|
|
105
|
+
const pts = points.map((p, i) => ({ ...p, index: i }));
|
|
106
|
+
|
|
107
|
+
// Calculate initial areas
|
|
108
|
+
const areas = new Array(pts.length).fill(Infinity);
|
|
109
|
+
for (let i = 1; i < pts.length - 1; i++) {
|
|
110
|
+
areas[i] = triangleArea(pts[i - 1], pts[i], pts[i + 1]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Repeatedly remove the point with smallest area
|
|
114
|
+
const kept = new Array(pts.length).fill(true);
|
|
115
|
+
let remaining = pts.length;
|
|
116
|
+
|
|
117
|
+
while (remaining > 2) {
|
|
118
|
+
// Find point with minimum area
|
|
119
|
+
let minAreaValue = Infinity;
|
|
120
|
+
let minIndex = -1;
|
|
121
|
+
|
|
122
|
+
for (let i = 1; i < pts.length - 1; i++) {
|
|
123
|
+
if (kept[i] && areas[i] < minAreaValue) {
|
|
124
|
+
minAreaValue = areas[i];
|
|
125
|
+
minIndex = i;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Stop if minimum area exceeds threshold
|
|
130
|
+
if (minAreaValue >= minArea || minIndex === -1) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Remove this point
|
|
135
|
+
kept[minIndex] = false;
|
|
136
|
+
remaining--;
|
|
137
|
+
|
|
138
|
+
// Update areas of neighbors
|
|
139
|
+
let prevIndex = minIndex - 1;
|
|
140
|
+
while (prevIndex >= 0 && !kept[prevIndex]) prevIndex--;
|
|
141
|
+
|
|
142
|
+
let nextIndex = minIndex + 1;
|
|
143
|
+
while (nextIndex < pts.length && !kept[nextIndex]) nextIndex++;
|
|
144
|
+
|
|
145
|
+
if (prevIndex > 0) {
|
|
146
|
+
let prevPrevIndex = prevIndex - 1;
|
|
147
|
+
while (prevPrevIndex >= 0 && !kept[prevPrevIndex]) prevPrevIndex--;
|
|
148
|
+
if (prevPrevIndex >= 0 && nextIndex < pts.length) {
|
|
149
|
+
areas[prevIndex] = triangleArea(pts[prevPrevIndex], pts[prevIndex], pts[nextIndex]);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (nextIndex < pts.length - 1) {
|
|
154
|
+
let nextNextIndex = nextIndex + 1;
|
|
155
|
+
while (nextNextIndex < pts.length && !kept[nextNextIndex]) nextNextIndex++;
|
|
156
|
+
if (nextNextIndex < pts.length && prevIndex >= 0) {
|
|
157
|
+
areas[nextIndex] = triangleArea(pts[prevIndex], pts[nextIndex], pts[nextNextIndex]);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Collect kept points
|
|
163
|
+
return pts.filter((_, i) => kept[i]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Simplify a polyline using the specified algorithm.
|
|
168
|
+
* @param {Array<{x: number, y: number}>} points - Array of points
|
|
169
|
+
* @param {number} tolerance - Simplification tolerance
|
|
170
|
+
* @param {'douglas-peucker' | 'visvalingam'} algorithm - Algorithm to use
|
|
171
|
+
* @returns {Array<{x: number, y: number}>} Simplified points
|
|
172
|
+
*/
|
|
173
|
+
export function simplifyPolyline(points, tolerance, algorithm = 'douglas-peucker') {
|
|
174
|
+
if (algorithm === 'visvalingam') {
|
|
175
|
+
// For Visvalingam, tolerance is the minimum triangle area
|
|
176
|
+
return visvalingamWhyatt(points, tolerance * tolerance);
|
|
177
|
+
}
|
|
178
|
+
return douglasPeucker(points, tolerance);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Extract points from SVG path commands (L, l, H, h, V, v only).
|
|
183
|
+
* @param {Array<{command: string, args: number[]}>} commands - Path commands
|
|
184
|
+
* @returns {Array<{x: number, y: number}>} Extracted points
|
|
185
|
+
*/
|
|
186
|
+
export function extractPolylinePoints(commands) {
|
|
187
|
+
const points = [];
|
|
188
|
+
let cx = 0, cy = 0;
|
|
189
|
+
let startX = 0, startY = 0;
|
|
190
|
+
|
|
191
|
+
for (const { command, args } of commands) {
|
|
192
|
+
switch (command) {
|
|
193
|
+
case 'M':
|
|
194
|
+
cx = args[0]; cy = args[1];
|
|
195
|
+
startX = cx; startY = cy;
|
|
196
|
+
points.push({ x: cx, y: cy });
|
|
197
|
+
break;
|
|
198
|
+
case 'm':
|
|
199
|
+
cx += args[0]; cy += args[1];
|
|
200
|
+
startX = cx; startY = cy;
|
|
201
|
+
points.push({ x: cx, y: cy });
|
|
202
|
+
break;
|
|
203
|
+
case 'L':
|
|
204
|
+
cx = args[0]; cy = args[1];
|
|
205
|
+
points.push({ x: cx, y: cy });
|
|
206
|
+
break;
|
|
207
|
+
case 'l':
|
|
208
|
+
cx += args[0]; cy += args[1];
|
|
209
|
+
points.push({ x: cx, y: cy });
|
|
210
|
+
break;
|
|
211
|
+
case 'H':
|
|
212
|
+
cx = args[0];
|
|
213
|
+
points.push({ x: cx, y: cy });
|
|
214
|
+
break;
|
|
215
|
+
case 'h':
|
|
216
|
+
cx += args[0];
|
|
217
|
+
points.push({ x: cx, y: cy });
|
|
218
|
+
break;
|
|
219
|
+
case 'V':
|
|
220
|
+
cy = args[0];
|
|
221
|
+
points.push({ x: cx, y: cy });
|
|
222
|
+
break;
|
|
223
|
+
case 'v':
|
|
224
|
+
cy += args[0];
|
|
225
|
+
points.push({ x: cx, y: cy });
|
|
226
|
+
break;
|
|
227
|
+
case 'Z':
|
|
228
|
+
case 'z':
|
|
229
|
+
if (cx !== startX || cy !== startY) {
|
|
230
|
+
points.push({ x: startX, y: startY });
|
|
231
|
+
}
|
|
232
|
+
cx = startX; cy = startY;
|
|
233
|
+
break;
|
|
234
|
+
// For curves (C, S, Q, T, A), we just track the endpoint
|
|
235
|
+
case 'C':
|
|
236
|
+
cx = args[4]; cy = args[5]; break;
|
|
237
|
+
case 'c':
|
|
238
|
+
cx += args[4]; cy += args[5]; break;
|
|
239
|
+
case 'S': case 'Q':
|
|
240
|
+
cx = args[2]; cy = args[3]; break;
|
|
241
|
+
case 's': case 'q':
|
|
242
|
+
cx += args[2]; cy += args[3]; break;
|
|
243
|
+
case 'T':
|
|
244
|
+
cx = args[0]; cy = args[1]; break;
|
|
245
|
+
case 't':
|
|
246
|
+
cx += args[0]; cy += args[1]; break;
|
|
247
|
+
case 'A':
|
|
248
|
+
cx = args[5]; cy = args[6]; break;
|
|
249
|
+
case 'a':
|
|
250
|
+
cx += args[5]; cy += args[6]; break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return points;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Rebuild path commands from simplified polyline points.
|
|
259
|
+
* @param {Array<{x: number, y: number}>} points - Simplified points
|
|
260
|
+
* @param {boolean} closed - Whether the path is closed
|
|
261
|
+
* @returns {Array<{command: string, args: number[]}>} Path commands
|
|
262
|
+
*/
|
|
263
|
+
export function rebuildPathFromPoints(points, closed = false) {
|
|
264
|
+
if (points.length === 0) return [];
|
|
265
|
+
|
|
266
|
+
const commands = [];
|
|
267
|
+
|
|
268
|
+
// First point is M
|
|
269
|
+
commands.push({ command: 'M', args: [points[0].x, points[0].y] });
|
|
270
|
+
|
|
271
|
+
// Remaining points are L
|
|
272
|
+
for (let i = 1; i < points.length; i++) {
|
|
273
|
+
commands.push({ command: 'L', args: [points[i].x, points[i].y] });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (closed) {
|
|
277
|
+
commands.push({ command: 'Z', args: [] });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return commands;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Check if a path is a pure polyline (only M, L, H, V, Z commands).
|
|
285
|
+
* @param {Array<{command: string, args: number[]}>} commands - Path commands
|
|
286
|
+
* @returns {boolean} True if pure polyline
|
|
287
|
+
*/
|
|
288
|
+
export function isPurePolyline(commands) {
|
|
289
|
+
const polylineCommands = new Set(['M', 'm', 'L', 'l', 'H', 'h', 'V', 'v', 'Z', 'z']);
|
|
290
|
+
return commands.every(cmd => polylineCommands.has(cmd.command));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Simplify a path if it's a pure polyline.
|
|
295
|
+
* @param {Array<{command: string, args: number[]}>} commands - Path commands
|
|
296
|
+
* @param {number} tolerance - Simplification tolerance
|
|
297
|
+
* @param {string} algorithm - Algorithm to use
|
|
298
|
+
* @returns {{commands: Array<{command: string, args: number[]}>, simplified: boolean, originalPoints: number, simplifiedPoints: number}}
|
|
299
|
+
*/
|
|
300
|
+
export function simplifyPath(commands, tolerance, algorithm = 'douglas-peucker') {
|
|
301
|
+
if (!isPurePolyline(commands) || commands.length < 3) {
|
|
302
|
+
return {
|
|
303
|
+
commands,
|
|
304
|
+
simplified: false,
|
|
305
|
+
originalPoints: 0,
|
|
306
|
+
simplifiedPoints: 0
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const points = extractPolylinePoints(commands);
|
|
311
|
+
const originalCount = points.length;
|
|
312
|
+
|
|
313
|
+
if (originalCount < 3) {
|
|
314
|
+
return {
|
|
315
|
+
commands,
|
|
316
|
+
simplified: false,
|
|
317
|
+
originalPoints: originalCount,
|
|
318
|
+
simplifiedPoints: originalCount
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Check if path is closed
|
|
323
|
+
const isClosed = commands[commands.length - 1].command.toLowerCase() === 'z';
|
|
324
|
+
|
|
325
|
+
const simplifiedPoints = simplifyPolyline(points, tolerance, algorithm);
|
|
326
|
+
const simplifiedCount = simplifiedPoints.length;
|
|
327
|
+
|
|
328
|
+
if (simplifiedCount >= originalCount) {
|
|
329
|
+
return {
|
|
330
|
+
commands,
|
|
331
|
+
simplified: false,
|
|
332
|
+
originalPoints: originalCount,
|
|
333
|
+
simplifiedPoints: originalCount
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const newCommands = rebuildPathFromPoints(simplifiedPoints, isClosed);
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
commands: newCommands,
|
|
341
|
+
simplified: true,
|
|
342
|
+
originalPoints: originalCount,
|
|
343
|
+
simplifiedPoints: simplifiedCount
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export default {
|
|
348
|
+
perpendicularDistance,
|
|
349
|
+
douglasPeucker,
|
|
350
|
+
visvalingamWhyatt,
|
|
351
|
+
simplifyPolyline,
|
|
352
|
+
extractPolylinePoints,
|
|
353
|
+
rebuildPathFromPoints,
|
|
354
|
+
isPurePolyline,
|
|
355
|
+
simplifyPath
|
|
356
|
+
};
|
package/src/flatten-pipeline.js
CHANGED
|
@@ -339,6 +339,23 @@ function resolveAllPatterns(root, defsMap, opts) {
|
|
|
339
339
|
if (!patternEl || patternEl.tagName !== 'pattern') continue;
|
|
340
340
|
|
|
341
341
|
try {
|
|
342
|
+
// Check coordinate system units - skip if non-default
|
|
343
|
+
const patternUnits = patternEl.getAttribute('patternUnits') || 'objectBoundingBox';
|
|
344
|
+
const patternContentUnits = patternEl.getAttribute('patternContentUnits') || 'userSpaceOnUse';
|
|
345
|
+
const patternTransform = patternEl.getAttribute('patternTransform');
|
|
346
|
+
|
|
347
|
+
// PatternResolver handles these cases, but warn about complex patterns
|
|
348
|
+
// that might need special handling or cause issues
|
|
349
|
+
if (patternUnits !== 'objectBoundingBox') {
|
|
350
|
+
errors.push(`pattern ${refId}: non-default patternUnits="${patternUnits}" may cause rendering issues`);
|
|
351
|
+
}
|
|
352
|
+
if (patternContentUnits !== 'userSpaceOnUse') {
|
|
353
|
+
errors.push(`pattern ${refId}: non-default patternContentUnits="${patternContentUnits}" may cause rendering issues`);
|
|
354
|
+
}
|
|
355
|
+
if (patternTransform) {
|
|
356
|
+
errors.push(`pattern ${refId}: patternTransform present - complex transformation may cause rendering issues`);
|
|
357
|
+
}
|
|
358
|
+
|
|
342
359
|
// Get element bounding box (approximate from path data or attributes)
|
|
343
360
|
const bbox = getElementBBox(el);
|
|
344
361
|
if (!bbox) continue;
|
|
@@ -404,6 +421,20 @@ function resolveAllMasks(root, defsMap, opts) {
|
|
|
404
421
|
if (!maskEl || maskEl.tagName !== 'mask') continue;
|
|
405
422
|
|
|
406
423
|
try {
|
|
424
|
+
// Check coordinate system units
|
|
425
|
+
const maskUnits = maskEl.getAttribute('maskUnits') || 'objectBoundingBox';
|
|
426
|
+
const maskContentUnits = maskEl.getAttribute('maskContentUnits') || 'userSpaceOnUse';
|
|
427
|
+
|
|
428
|
+
// Default for mask is different from clipPath:
|
|
429
|
+
// maskUnits defaults to objectBoundingBox
|
|
430
|
+
// maskContentUnits defaults to userSpaceOnUse
|
|
431
|
+
if (maskUnits !== 'objectBoundingBox') {
|
|
432
|
+
errors.push(`mask ${refId}: non-default maskUnits="${maskUnits}" may cause rendering issues`);
|
|
433
|
+
}
|
|
434
|
+
if (maskContentUnits !== 'userSpaceOnUse') {
|
|
435
|
+
errors.push(`mask ${refId}: non-default maskContentUnits="${maskContentUnits}" may cause rendering issues`);
|
|
436
|
+
}
|
|
437
|
+
|
|
407
438
|
// Get element bounding box
|
|
408
439
|
const bbox = getElementBBox(el);
|
|
409
440
|
if (!bbox) continue;
|
|
@@ -479,6 +510,18 @@ function applyAllClipPaths(root, defsMap, opts, stats) {
|
|
|
479
510
|
if (!clipPathEl || clipPathEl.tagName !== 'clippath') continue;
|
|
480
511
|
|
|
481
512
|
try {
|
|
513
|
+
// Check coordinate system units
|
|
514
|
+
const clipPathUnits = clipPathEl.getAttribute('clipPathUnits') || 'userSpaceOnUse';
|
|
515
|
+
|
|
516
|
+
// userSpaceOnUse is the default and normal case for clipPath
|
|
517
|
+
// objectBoundingBox means coordinates are 0-1 relative to bounding box
|
|
518
|
+
if (clipPathUnits === 'objectBoundingBox') {
|
|
519
|
+
// This requires transforming clip coordinates based on target element's bbox
|
|
520
|
+
// which is complex - warn about it
|
|
521
|
+
errors.push(`clipPath ${refId}: objectBoundingBox units require bbox-relative coordinate transformation`);
|
|
522
|
+
// Note: We continue processing, but results may be incorrect
|
|
523
|
+
}
|
|
524
|
+
|
|
482
525
|
// Get element path data
|
|
483
526
|
const origPathData = getElementPathData(el, opts.precision);
|
|
484
527
|
if (!origPathData) continue;
|
|
@@ -1004,14 +1047,70 @@ function getElementBBox(el) {
|
|
|
1004
1047
|
/**
|
|
1005
1048
|
* Extract presentation attributes from element.
|
|
1006
1049
|
* @private
|
|
1050
|
+
*
|
|
1051
|
+
* CRITICAL: This function must include ALL SVG presentation attributes
|
|
1052
|
+
* that affect visual rendering. Missing attributes will cause SILENT
|
|
1053
|
+
* RENDERING BUGS when shapes are converted to paths.
|
|
1054
|
+
*
|
|
1055
|
+
* Categories:
|
|
1056
|
+
* - Stroke properties (width, caps, joins, dashes)
|
|
1057
|
+
* - Fill properties (opacity, rule)
|
|
1058
|
+
* - Clipping/Masking (clip-path, mask, filter) - NON-INHERITABLE but CRITICAL
|
|
1059
|
+
* - Marker properties (marker, marker-start/mid/end)
|
|
1060
|
+
* - Text properties (font, spacing, decoration)
|
|
1061
|
+
* - Rendering hints (shape-rendering, text-rendering, etc.)
|
|
1062
|
+
* - Visual effects (opacity, paint-order, vector-effect)
|
|
1007
1063
|
*/
|
|
1008
1064
|
function extractPresentationAttrs(el) {
|
|
1009
1065
|
const presentationAttrs = [
|
|
1010
|
-
|
|
1066
|
+
// Stroke properties
|
|
1067
|
+
'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin',
|
|
1011
1068
|
'stroke-dasharray', 'stroke-dashoffset', 'stroke-miterlimit', 'stroke-opacity',
|
|
1012
|
-
'
|
|
1013
|
-
|
|
1014
|
-
|
|
1069
|
+
'vector-effect', // Affects stroke rendering (non-scaling-stroke)
|
|
1070
|
+
|
|
1071
|
+
// Fill properties
|
|
1072
|
+
'fill', 'fill-opacity', 'fill-rule',
|
|
1073
|
+
|
|
1074
|
+
// CRITICAL: Non-inheritable but must be preserved on element
|
|
1075
|
+
'clip-path', // Clips geometry - MUST NOT BE LOST
|
|
1076
|
+
'mask', // Masks transparency - MUST NOT BE LOST
|
|
1077
|
+
'filter', // Visual effects - MUST NOT BE LOST
|
|
1078
|
+
'opacity', // Element opacity
|
|
1079
|
+
|
|
1080
|
+
// Clip/fill rules
|
|
1081
|
+
'clip-rule',
|
|
1082
|
+
|
|
1083
|
+
// Marker properties - arrows, dots, etc on paths
|
|
1084
|
+
'marker', // Shorthand for all markers
|
|
1085
|
+
'marker-start', // Start of path
|
|
1086
|
+
'marker-mid', // Vertices
|
|
1087
|
+
'marker-end', // End of path
|
|
1088
|
+
|
|
1089
|
+
// Visibility
|
|
1090
|
+
'visibility', 'display',
|
|
1091
|
+
|
|
1092
|
+
// Color
|
|
1093
|
+
'color',
|
|
1094
|
+
|
|
1095
|
+
// Text properties
|
|
1096
|
+
'font-family', 'font-size', 'font-weight', 'font-style',
|
|
1097
|
+
'text-anchor', 'dominant-baseline', 'alignment-baseline',
|
|
1098
|
+
'letter-spacing', 'word-spacing', 'text-decoration',
|
|
1099
|
+
|
|
1100
|
+
// Rendering hints
|
|
1101
|
+
'shape-rendering', 'text-rendering', 'image-rendering', 'color-rendering',
|
|
1102
|
+
|
|
1103
|
+
// Paint order (affects stroke/fill/marker rendering order)
|
|
1104
|
+
'paint-order',
|
|
1105
|
+
|
|
1106
|
+
// Event handling (visual feedback)
|
|
1107
|
+
'pointer-events', 'cursor',
|
|
1108
|
+
|
|
1109
|
+
// Preserve class and style for CSS targeting
|
|
1110
|
+
'class', 'style',
|
|
1111
|
+
|
|
1112
|
+
// ID must be preserved for references
|
|
1113
|
+
'id'
|
|
1015
1114
|
];
|
|
1016
1115
|
|
|
1017
1116
|
const attrs = {};
|
|
@@ -1027,6 +1126,10 @@ function extractPresentationAttrs(el) {
|
|
|
1027
1126
|
/**
|
|
1028
1127
|
* Get shape-specific attribute names.
|
|
1029
1128
|
* @private
|
|
1129
|
+
*
|
|
1130
|
+
* These are attributes specific to each shape element that should NOT be
|
|
1131
|
+
* copied when converting to a <path>. The geometry is encoded in the 'd'
|
|
1132
|
+
* attribute instead.
|
|
1030
1133
|
*/
|
|
1031
1134
|
function getShapeSpecificAttrs(tagName) {
|
|
1032
1135
|
const attrs = {
|
|
@@ -1036,6 +1139,8 @@ function getShapeSpecificAttrs(tagName) {
|
|
|
1036
1139
|
line: ['x1', 'y1', 'x2', 'y2'],
|
|
1037
1140
|
polyline: ['points'],
|
|
1038
1141
|
polygon: ['points'],
|
|
1142
|
+
// Image element has position/size attributes that don't apply to paths
|
|
1143
|
+
image: ['x', 'y', 'width', 'height', 'href', 'xlink:href', 'preserveAspectRatio'],
|
|
1039
1144
|
};
|
|
1040
1145
|
return attrs[tagName.toLowerCase()] || [];
|
|
1041
1146
|
}
|