@emasoft/svg-matrix 1.0.27 → 1.0.29
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 +325 -0
- package/bin/svg-matrix.js +994 -378
- package/bin/svglinter.cjs +4172 -433
- package/bin/svgm.js +744 -184
- package/package.json +16 -4
- package/src/animation-references.js +71 -52
- package/src/arc-length.js +160 -96
- package/src/bezier-analysis.js +257 -117
- package/src/bezier-intersections.js +411 -148
- package/src/browser-verify.js +240 -100
- package/src/clip-path-resolver.js +350 -142
- package/src/convert-path-data.js +279 -134
- package/src/css-specificity.js +78 -70
- package/src/flatten-pipeline.js +751 -263
- package/src/geometry-to-path.js +511 -182
- package/src/index.js +191 -46
- package/src/inkscape-support.js +404 -0
- package/src/marker-resolver.js +278 -164
- package/src/mask-resolver.js +209 -98
- package/src/matrix.js +147 -67
- package/src/mesh-gradient.js +187 -96
- package/src/off-canvas-detection.js +201 -104
- package/src/path-analysis.js +187 -107
- package/src/path-data-plugins.js +628 -167
- package/src/path-simplification.js +0 -1
- package/src/pattern-resolver.js +125 -88
- package/src/polygon-clip.js +111 -66
- package/src/svg-boolean-ops.js +194 -118
- package/src/svg-collections.js +48 -19
- package/src/svg-flatten.js +282 -164
- package/src/svg-parser.js +427 -200
- package/src/svg-rendering-context.js +147 -104
- package/src/svg-toolbox.js +16411 -3298
- package/src/svg2-polyfills.js +114 -245
- package/src/transform-decomposition.js +46 -41
- package/src/transform-optimization.js +89 -68
- package/src/transforms2d.js +49 -16
- package/src/transforms3d.js +58 -22
- package/src/use-symbol-resolver.js +150 -110
- package/src/vector.js +67 -15
- package/src/vendor/README.md +110 -0
- package/src/vendor/inkscape-hatch-polyfill.js +401 -0
- package/src/vendor/inkscape-hatch-polyfill.min.js +8 -0
- package/src/vendor/inkscape-mesh-polyfill.js +843 -0
- package/src/vendor/inkscape-mesh-polyfill.min.js +8 -0
- package/src/verification.js +288 -124
package/src/marker-resolver.js
CHANGED
|
@@ -15,14 +15,12 @@
|
|
|
15
15
|
* @module marker-resolver
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import Decimal from
|
|
19
|
-
import { Matrix } from
|
|
20
|
-
import * as Transforms2D from
|
|
18
|
+
import Decimal from "decimal.js";
|
|
19
|
+
import { Matrix } from "./matrix.js";
|
|
20
|
+
import * as Transforms2D from "./transforms2d.js";
|
|
21
21
|
|
|
22
22
|
Decimal.set({ precision: 80 });
|
|
23
23
|
|
|
24
|
-
const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
25
|
-
|
|
26
24
|
/**
|
|
27
25
|
* Parse an SVG marker element to extract all marker properties.
|
|
28
26
|
*
|
|
@@ -71,34 +69,38 @@ const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
|
71
69
|
*/
|
|
72
70
|
export function parseMarkerElement(markerElement) {
|
|
73
71
|
const data = {
|
|
74
|
-
id: markerElement.getAttribute(
|
|
75
|
-
markerWidth: parseFloat(markerElement.getAttribute(
|
|
76
|
-
markerHeight: parseFloat(markerElement.getAttribute(
|
|
77
|
-
refX: parseFloat(markerElement.getAttribute(
|
|
78
|
-
refY: parseFloat(markerElement.getAttribute(
|
|
79
|
-
orient: markerElement.getAttribute(
|
|
80
|
-
markerUnits: markerElement.getAttribute(
|
|
72
|
+
id: markerElement.getAttribute("id") || "",
|
|
73
|
+
markerWidth: parseFloat(markerElement.getAttribute("markerWidth") || "3"),
|
|
74
|
+
markerHeight: parseFloat(markerElement.getAttribute("markerHeight") || "3"),
|
|
75
|
+
refX: parseFloat(markerElement.getAttribute("refX") || "0"),
|
|
76
|
+
refY: parseFloat(markerElement.getAttribute("refY") || "0"),
|
|
77
|
+
orient: markerElement.getAttribute("orient") || "auto",
|
|
78
|
+
markerUnits: markerElement.getAttribute("markerUnits") || "strokeWidth",
|
|
81
79
|
viewBox: null,
|
|
82
|
-
preserveAspectRatio:
|
|
83
|
-
|
|
80
|
+
preserveAspectRatio:
|
|
81
|
+
markerElement.getAttribute("preserveAspectRatio") || "xMidYMid meet",
|
|
82
|
+
children: [],
|
|
84
83
|
};
|
|
85
84
|
|
|
86
85
|
// Parse viewBox if present
|
|
87
|
-
const viewBoxStr = markerElement.getAttribute(
|
|
86
|
+
const viewBoxStr = markerElement.getAttribute("viewBox");
|
|
88
87
|
if (viewBoxStr) {
|
|
89
|
-
const parts = viewBoxStr
|
|
88
|
+
const parts = viewBoxStr
|
|
89
|
+
.trim()
|
|
90
|
+
.split(/[\s,]+/)
|
|
91
|
+
.map(Number);
|
|
90
92
|
if (parts.length === 4) {
|
|
91
93
|
data.viewBox = {
|
|
92
94
|
x: parts[0],
|
|
93
95
|
y: parts[1],
|
|
94
96
|
width: parts[2],
|
|
95
|
-
height: parts[3]
|
|
97
|
+
height: parts[3],
|
|
96
98
|
};
|
|
97
99
|
}
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
// Parse orient attribute
|
|
101
|
-
if (data.orient !==
|
|
103
|
+
if (data.orient !== "auto" && data.orient !== "auto-start-reverse") {
|
|
102
104
|
// Parse as angle in degrees
|
|
103
105
|
const angle = parseFloat(data.orient);
|
|
104
106
|
if (!isNaN(angle)) {
|
|
@@ -124,44 +126,44 @@ export function parseMarkerChild(element) {
|
|
|
124
126
|
const tagName = element.tagName.toLowerCase();
|
|
125
127
|
const data = {
|
|
126
128
|
type: tagName,
|
|
127
|
-
id: element.getAttribute(
|
|
128
|
-
transform: element.getAttribute(
|
|
129
|
-
fill: element.getAttribute(
|
|
130
|
-
stroke: element.getAttribute(
|
|
131
|
-
strokeWidth: element.getAttribute(
|
|
132
|
-
opacity: element.getAttribute(
|
|
129
|
+
id: element.getAttribute("id") || null,
|
|
130
|
+
transform: element.getAttribute("transform") || null,
|
|
131
|
+
fill: element.getAttribute("fill") || null,
|
|
132
|
+
stroke: element.getAttribute("stroke") || null,
|
|
133
|
+
strokeWidth: element.getAttribute("stroke-width") || null,
|
|
134
|
+
opacity: element.getAttribute("opacity") || null,
|
|
133
135
|
};
|
|
134
136
|
|
|
135
137
|
switch (tagName) {
|
|
136
|
-
case
|
|
137
|
-
data.d = element.getAttribute(
|
|
138
|
+
case "path":
|
|
139
|
+
data.d = element.getAttribute("d") || "";
|
|
138
140
|
break;
|
|
139
|
-
case
|
|
140
|
-
data.x = parseFloat(element.getAttribute(
|
|
141
|
-
data.y = parseFloat(element.getAttribute(
|
|
142
|
-
data.width = parseFloat(element.getAttribute(
|
|
143
|
-
data.height = parseFloat(element.getAttribute(
|
|
141
|
+
case "rect":
|
|
142
|
+
data.x = parseFloat(element.getAttribute("x") || "0");
|
|
143
|
+
data.y = parseFloat(element.getAttribute("y") || "0");
|
|
144
|
+
data.width = parseFloat(element.getAttribute("width") || "0");
|
|
145
|
+
data.height = parseFloat(element.getAttribute("height") || "0");
|
|
144
146
|
break;
|
|
145
|
-
case
|
|
146
|
-
data.cx = parseFloat(element.getAttribute(
|
|
147
|
-
data.cy = parseFloat(element.getAttribute(
|
|
148
|
-
data.r = parseFloat(element.getAttribute(
|
|
147
|
+
case "circle":
|
|
148
|
+
data.cx = parseFloat(element.getAttribute("cx") || "0");
|
|
149
|
+
data.cy = parseFloat(element.getAttribute("cy") || "0");
|
|
150
|
+
data.r = parseFloat(element.getAttribute("r") || "0");
|
|
149
151
|
break;
|
|
150
|
-
case
|
|
151
|
-
data.cx = parseFloat(element.getAttribute(
|
|
152
|
-
data.cy = parseFloat(element.getAttribute(
|
|
153
|
-
data.rx = parseFloat(element.getAttribute(
|
|
154
|
-
data.ry = parseFloat(element.getAttribute(
|
|
152
|
+
case "ellipse":
|
|
153
|
+
data.cx = parseFloat(element.getAttribute("cx") || "0");
|
|
154
|
+
data.cy = parseFloat(element.getAttribute("cy") || "0");
|
|
155
|
+
data.rx = parseFloat(element.getAttribute("rx") || "0");
|
|
156
|
+
data.ry = parseFloat(element.getAttribute("ry") || "0");
|
|
155
157
|
break;
|
|
156
|
-
case
|
|
157
|
-
data.x1 = parseFloat(element.getAttribute(
|
|
158
|
-
data.y1 = parseFloat(element.getAttribute(
|
|
159
|
-
data.x2 = parseFloat(element.getAttribute(
|
|
160
|
-
data.y2 = parseFloat(element.getAttribute(
|
|
158
|
+
case "line":
|
|
159
|
+
data.x1 = parseFloat(element.getAttribute("x1") || "0");
|
|
160
|
+
data.y1 = parseFloat(element.getAttribute("y1") || "0");
|
|
161
|
+
data.x2 = parseFloat(element.getAttribute("x2") || "0");
|
|
162
|
+
data.y2 = parseFloat(element.getAttribute("y2") || "0");
|
|
161
163
|
break;
|
|
162
|
-
case
|
|
163
|
-
case
|
|
164
|
-
data.points = element.getAttribute(
|
|
164
|
+
case "polygon":
|
|
165
|
+
case "polyline":
|
|
166
|
+
data.points = element.getAttribute("points") || "";
|
|
165
167
|
break;
|
|
166
168
|
default:
|
|
167
169
|
// Store any additional attributes for unknown elements
|
|
@@ -202,8 +204,22 @@ export function parseMarkerChild(element) {
|
|
|
202
204
|
* const strokeWidth = 2;
|
|
203
205
|
* const transform = getMarkerTransform(markerDef, position, tangentAngle, strokeWidth, true);
|
|
204
206
|
*/
|
|
205
|
-
export function getMarkerTransform(
|
|
206
|
-
|
|
207
|
+
export function getMarkerTransform(
|
|
208
|
+
markerDef,
|
|
209
|
+
position,
|
|
210
|
+
tangentAngle,
|
|
211
|
+
strokeWidth = 1,
|
|
212
|
+
isStart = false,
|
|
213
|
+
) {
|
|
214
|
+
const {
|
|
215
|
+
markerWidth,
|
|
216
|
+
markerHeight,
|
|
217
|
+
refX,
|
|
218
|
+
refY,
|
|
219
|
+
orient,
|
|
220
|
+
markerUnits,
|
|
221
|
+
viewBox,
|
|
222
|
+
} = markerDef;
|
|
207
223
|
|
|
208
224
|
// Start with identity matrix
|
|
209
225
|
let transform = Matrix.identity(3);
|
|
@@ -214,11 +230,11 @@ export function getMarkerTransform(markerDef, position, tangentAngle, strokeWidt
|
|
|
214
230
|
|
|
215
231
|
// Step 2: Calculate rotation angle
|
|
216
232
|
let rotationAngle = 0;
|
|
217
|
-
if (orient ===
|
|
233
|
+
if (orient === "auto") {
|
|
218
234
|
rotationAngle = tangentAngle;
|
|
219
|
-
} else if (orient ===
|
|
235
|
+
} else if (orient === "auto-start-reverse" && isStart) {
|
|
220
236
|
rotationAngle = tangentAngle + Math.PI; // Add 180 degrees
|
|
221
|
-
} else if (typeof orient ===
|
|
237
|
+
} else if (typeof orient === "number") {
|
|
222
238
|
rotationAngle = orient * (Math.PI / 180); // Convert degrees to radians
|
|
223
239
|
}
|
|
224
240
|
|
|
@@ -231,7 +247,7 @@ export function getMarkerTransform(markerDef, position, tangentAngle, strokeWidt
|
|
|
231
247
|
// Step 3: Apply markerUnits scaling
|
|
232
248
|
let scaleX = 1;
|
|
233
249
|
let scaleY = 1;
|
|
234
|
-
if (markerUnits ===
|
|
250
|
+
if (markerUnits === "strokeWidth") {
|
|
235
251
|
scaleX = strokeWidth;
|
|
236
252
|
scaleY = strokeWidth;
|
|
237
253
|
}
|
|
@@ -316,8 +332,8 @@ export function getPathVertices(pathData) {
|
|
|
316
332
|
let currentY = 0;
|
|
317
333
|
let startX = 0;
|
|
318
334
|
let startY = 0;
|
|
319
|
-
let
|
|
320
|
-
let
|
|
335
|
+
let _lastControlX = 0;
|
|
336
|
+
let _lastControlY = 0;
|
|
321
337
|
|
|
322
338
|
// Parse path commands
|
|
323
339
|
const commands = parsePathCommands(pathData);
|
|
@@ -328,7 +344,7 @@ export function getPathVertices(pathData) {
|
|
|
328
344
|
const prevY = currentY;
|
|
329
345
|
|
|
330
346
|
switch (cmd.type) {
|
|
331
|
-
case
|
|
347
|
+
case "M": // moveto
|
|
332
348
|
currentX = cmd.x;
|
|
333
349
|
currentY = cmd.y;
|
|
334
350
|
startX = currentX;
|
|
@@ -347,11 +363,12 @@ export function getPathVertices(pathData) {
|
|
|
347
363
|
y: currentY,
|
|
348
364
|
tangentIn: 0,
|
|
349
365
|
tangentOut: 0,
|
|
350
|
-
index: vertices.length
|
|
366
|
+
index: vertices.length,
|
|
351
367
|
});
|
|
352
368
|
break;
|
|
353
369
|
|
|
354
|
-
case
|
|
370
|
+
case "L": {
|
|
371
|
+
// lineto
|
|
355
372
|
currentX = cmd.x;
|
|
356
373
|
currentY = cmd.y;
|
|
357
374
|
|
|
@@ -369,13 +386,15 @@ export function getPathVertices(pathData) {
|
|
|
369
386
|
y: currentY,
|
|
370
387
|
tangentIn: lineAngle,
|
|
371
388
|
tangentOut: lineAngle,
|
|
372
|
-
index: vertices.length
|
|
389
|
+
index: vertices.length,
|
|
373
390
|
});
|
|
374
391
|
break;
|
|
392
|
+
}
|
|
375
393
|
|
|
376
|
-
case
|
|
377
|
-
|
|
378
|
-
|
|
394
|
+
case "C": {
|
|
395
|
+
// cubic bezier
|
|
396
|
+
_lastControlX = cmd.x2;
|
|
397
|
+
_lastControlY = cmd.y2;
|
|
379
398
|
currentX = cmd.x;
|
|
380
399
|
currentY = cmd.y;
|
|
381
400
|
|
|
@@ -396,13 +415,15 @@ export function getPathVertices(pathData) {
|
|
|
396
415
|
y: currentY,
|
|
397
416
|
tangentIn: endTangent,
|
|
398
417
|
tangentOut: endTangent,
|
|
399
|
-
index: vertices.length
|
|
418
|
+
index: vertices.length,
|
|
400
419
|
});
|
|
401
420
|
break;
|
|
421
|
+
}
|
|
402
422
|
|
|
403
|
-
case
|
|
404
|
-
|
|
405
|
-
|
|
423
|
+
case "Q": {
|
|
424
|
+
// quadratic bezier
|
|
425
|
+
_lastControlX = cmd.x1;
|
|
426
|
+
_lastControlY = cmd.y1;
|
|
406
427
|
currentX = cmd.x;
|
|
407
428
|
currentY = cmd.y;
|
|
408
429
|
|
|
@@ -423,11 +444,13 @@ export function getPathVertices(pathData) {
|
|
|
423
444
|
y: currentY,
|
|
424
445
|
tangentIn: qEndTangent,
|
|
425
446
|
tangentOut: qEndTangent,
|
|
426
|
-
index: vertices.length
|
|
447
|
+
index: vertices.length,
|
|
427
448
|
});
|
|
428
449
|
break;
|
|
450
|
+
}
|
|
429
451
|
|
|
430
|
-
case
|
|
452
|
+
case "A": {
|
|
453
|
+
// arc
|
|
431
454
|
currentX = cmd.x;
|
|
432
455
|
currentY = cmd.y;
|
|
433
456
|
|
|
@@ -445,11 +468,13 @@ export function getPathVertices(pathData) {
|
|
|
445
468
|
y: currentY,
|
|
446
469
|
tangentIn: arcAngle,
|
|
447
470
|
tangentOut: arcAngle,
|
|
448
|
-
index: vertices.length
|
|
471
|
+
index: vertices.length,
|
|
449
472
|
});
|
|
450
473
|
break;
|
|
474
|
+
}
|
|
451
475
|
|
|
452
|
-
case
|
|
476
|
+
case "Z": {
|
|
477
|
+
// closepath
|
|
453
478
|
currentX = startX;
|
|
454
479
|
currentY = startY;
|
|
455
480
|
|
|
@@ -466,6 +491,7 @@ export function getPathVertices(pathData) {
|
|
|
466
491
|
vertices[0].tangentIn = closeAngle;
|
|
467
492
|
}
|
|
468
493
|
break;
|
|
494
|
+
}
|
|
469
495
|
}
|
|
470
496
|
}
|
|
471
497
|
|
|
@@ -483,11 +509,38 @@ export function parsePathCommands(pathData) {
|
|
|
483
509
|
|
|
484
510
|
// Normalize path data: add spaces around command letters
|
|
485
511
|
const normalized = pathData
|
|
486
|
-
.replace(/([MmLlHhVvCcSsQqTtAaZz])/g,
|
|
512
|
+
.replace(/([MmLlHhVvCcSsQqTtAaZz])/g, " $1 ")
|
|
487
513
|
.trim();
|
|
488
514
|
|
|
489
|
-
//
|
|
490
|
-
|
|
515
|
+
// FIX: Use regex to extract tokens (command letters and numbers)
|
|
516
|
+
// Handles implicit negative separators (e.g., "0.8-2.9" -> ["0.8", "-2.9"])
|
|
517
|
+
// Per W3C SVG spec, negative signs can act as delimiters without spaces
|
|
518
|
+
const commandRegex = /[MmLlHhVvCcSsQqTtAaZz]/;
|
|
519
|
+
const numRegex = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
|
|
520
|
+
const tokens = [];
|
|
521
|
+
|
|
522
|
+
let pos = 0;
|
|
523
|
+
while (pos < normalized.length) {
|
|
524
|
+
// Skip whitespace and commas
|
|
525
|
+
while (pos < normalized.length && /[\s,]/.test(normalized[pos])) pos++;
|
|
526
|
+
if (pos >= normalized.length) break;
|
|
527
|
+
|
|
528
|
+
// Check if it's a command letter
|
|
529
|
+
if (commandRegex.test(normalized[pos])) {
|
|
530
|
+
tokens.push(normalized[pos]);
|
|
531
|
+
pos++;
|
|
532
|
+
} else {
|
|
533
|
+
// Extract number
|
|
534
|
+
numRegex.lastIndex = pos;
|
|
535
|
+
const match = numRegex.exec(normalized);
|
|
536
|
+
if (match && match.index === pos) {
|
|
537
|
+
tokens.push(match[0]);
|
|
538
|
+
pos = numRegex.lastIndex;
|
|
539
|
+
} else {
|
|
540
|
+
pos++; // Skip invalid character
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
491
544
|
|
|
492
545
|
let i = 0;
|
|
493
546
|
let currentX = 0;
|
|
@@ -497,55 +550,64 @@ export function parsePathCommands(pathData) {
|
|
|
497
550
|
const cmdType = tokens[i];
|
|
498
551
|
|
|
499
552
|
switch (cmdType.toUpperCase()) {
|
|
500
|
-
case
|
|
553
|
+
case "M": {
|
|
554
|
+
// moveto
|
|
501
555
|
const mx = parseFloat(tokens[i + 1]);
|
|
502
556
|
const my = parseFloat(tokens[i + 2]);
|
|
503
557
|
commands.push({
|
|
504
|
-
type:
|
|
505
|
-
x: cmdType ===
|
|
506
|
-
y: cmdType ===
|
|
558
|
+
type: "M",
|
|
559
|
+
x: cmdType === "M" ? mx : currentX + mx,
|
|
560
|
+
y: cmdType === "M" ? my : currentY + my,
|
|
507
561
|
});
|
|
508
562
|
currentX = commands[commands.length - 1].x;
|
|
509
563
|
currentY = commands[commands.length - 1].y;
|
|
510
564
|
i += 3;
|
|
511
565
|
break;
|
|
566
|
+
}
|
|
512
567
|
|
|
513
|
-
case
|
|
568
|
+
case "L": {
|
|
569
|
+
// lineto
|
|
514
570
|
const lx = parseFloat(tokens[i + 1]);
|
|
515
571
|
const ly = parseFloat(tokens[i + 2]);
|
|
516
572
|
commands.push({
|
|
517
|
-
type:
|
|
518
|
-
x: cmdType ===
|
|
519
|
-
y: cmdType ===
|
|
573
|
+
type: "L",
|
|
574
|
+
x: cmdType === "L" ? lx : currentX + lx,
|
|
575
|
+
y: cmdType === "L" ? ly : currentY + ly,
|
|
520
576
|
});
|
|
521
577
|
currentX = commands[commands.length - 1].x;
|
|
522
578
|
currentY = commands[commands.length - 1].y;
|
|
523
579
|
i += 3;
|
|
524
580
|
break;
|
|
581
|
+
}
|
|
525
582
|
|
|
526
|
-
case
|
|
583
|
+
case "H": {
|
|
584
|
+
// horizontal lineto
|
|
527
585
|
const hx = parseFloat(tokens[i + 1]);
|
|
528
586
|
commands.push({
|
|
529
|
-
type:
|
|
530
|
-
x: cmdType ===
|
|
531
|
-
y: currentY
|
|
587
|
+
type: "L",
|
|
588
|
+
x: cmdType === "H" ? hx : currentX + hx,
|
|
589
|
+
y: currentY,
|
|
532
590
|
});
|
|
533
591
|
currentX = commands[commands.length - 1].x;
|
|
534
592
|
i += 2;
|
|
535
593
|
break;
|
|
594
|
+
}
|
|
536
595
|
|
|
537
|
-
case
|
|
596
|
+
case "V": {
|
|
597
|
+
// vertical lineto
|
|
538
598
|
const vy = parseFloat(tokens[i + 1]);
|
|
539
599
|
commands.push({
|
|
540
|
-
type:
|
|
600
|
+
type: "L",
|
|
541
601
|
x: currentX,
|
|
542
|
-
y: cmdType ===
|
|
602
|
+
y: cmdType === "V" ? vy : currentY + vy,
|
|
543
603
|
});
|
|
544
604
|
currentY = commands[commands.length - 1].y;
|
|
545
605
|
i += 2;
|
|
546
606
|
break;
|
|
607
|
+
}
|
|
547
608
|
|
|
548
|
-
case
|
|
609
|
+
case "C": {
|
|
610
|
+
// cubic bezier
|
|
549
611
|
const c1x = parseFloat(tokens[i + 1]);
|
|
550
612
|
const c1y = parseFloat(tokens[i + 2]);
|
|
551
613
|
const c2x = parseFloat(tokens[i + 3]);
|
|
@@ -553,61 +615,66 @@ export function parsePathCommands(pathData) {
|
|
|
553
615
|
const cx = parseFloat(tokens[i + 5]);
|
|
554
616
|
const cy = parseFloat(tokens[i + 6]);
|
|
555
617
|
commands.push({
|
|
556
|
-
type:
|
|
557
|
-
x1: cmdType ===
|
|
558
|
-
y1: cmdType ===
|
|
559
|
-
x2: cmdType ===
|
|
560
|
-
y2: cmdType ===
|
|
561
|
-
x: cmdType ===
|
|
562
|
-
y: cmdType ===
|
|
618
|
+
type: "C",
|
|
619
|
+
x1: cmdType === "C" ? c1x : currentX + c1x,
|
|
620
|
+
y1: cmdType === "C" ? c1y : currentY + c1y,
|
|
621
|
+
x2: cmdType === "C" ? c2x : currentX + c2x,
|
|
622
|
+
y2: cmdType === "C" ? c2y : currentY + c2y,
|
|
623
|
+
x: cmdType === "C" ? cx : currentX + cx,
|
|
624
|
+
y: cmdType === "C" ? cy : currentY + cy,
|
|
563
625
|
});
|
|
564
626
|
currentX = commands[commands.length - 1].x;
|
|
565
627
|
currentY = commands[commands.length - 1].y;
|
|
566
628
|
i += 7;
|
|
567
629
|
break;
|
|
630
|
+
}
|
|
568
631
|
|
|
569
|
-
case
|
|
632
|
+
case "Q": {
|
|
633
|
+
// quadratic bezier
|
|
570
634
|
const q1x = parseFloat(tokens[i + 1]);
|
|
571
635
|
const q1y = parseFloat(tokens[i + 2]);
|
|
572
636
|
const qx = parseFloat(tokens[i + 3]);
|
|
573
637
|
const qy = parseFloat(tokens[i + 4]);
|
|
574
638
|
commands.push({
|
|
575
|
-
type:
|
|
576
|
-
x1: cmdType ===
|
|
577
|
-
y1: cmdType ===
|
|
578
|
-
x: cmdType ===
|
|
579
|
-
y: cmdType ===
|
|
639
|
+
type: "Q",
|
|
640
|
+
x1: cmdType === "Q" ? q1x : currentX + q1x,
|
|
641
|
+
y1: cmdType === "Q" ? q1y : currentY + q1y,
|
|
642
|
+
x: cmdType === "Q" ? qx : currentX + qx,
|
|
643
|
+
y: cmdType === "Q" ? qy : currentY + qy,
|
|
580
644
|
});
|
|
581
645
|
currentX = commands[commands.length - 1].x;
|
|
582
646
|
currentY = commands[commands.length - 1].y;
|
|
583
647
|
i += 5;
|
|
584
648
|
break;
|
|
649
|
+
}
|
|
585
650
|
|
|
586
|
-
case
|
|
651
|
+
case "A": {
|
|
652
|
+
// arc
|
|
587
653
|
const rx = parseFloat(tokens[i + 1]);
|
|
588
654
|
const ry = parseFloat(tokens[i + 2]);
|
|
589
655
|
const xAxisRotation = parseFloat(tokens[i + 3]);
|
|
590
|
-
const largeArcFlag = parseInt(tokens[i + 4]);
|
|
591
|
-
const sweepFlag = parseInt(tokens[i + 5]);
|
|
656
|
+
const largeArcFlag = parseInt(tokens[i + 4], 10);
|
|
657
|
+
const sweepFlag = parseInt(tokens[i + 5], 10);
|
|
592
658
|
const ax = parseFloat(tokens[i + 6]);
|
|
593
659
|
const ay = parseFloat(tokens[i + 7]);
|
|
594
660
|
commands.push({
|
|
595
|
-
type:
|
|
661
|
+
type: "A",
|
|
596
662
|
rx,
|
|
597
663
|
ry,
|
|
598
664
|
xAxisRotation,
|
|
599
665
|
largeArcFlag,
|
|
600
666
|
sweepFlag,
|
|
601
|
-
x: cmdType ===
|
|
602
|
-
y: cmdType ===
|
|
667
|
+
x: cmdType === "A" ? ax : currentX + ax,
|
|
668
|
+
y: cmdType === "A" ? ay : currentY + ay,
|
|
603
669
|
});
|
|
604
670
|
currentX = commands[commands.length - 1].x;
|
|
605
671
|
currentY = commands[commands.length - 1].y;
|
|
606
672
|
i += 8;
|
|
607
673
|
break;
|
|
674
|
+
}
|
|
608
675
|
|
|
609
|
-
case
|
|
610
|
-
commands.push({ type:
|
|
676
|
+
case "Z": // closepath
|
|
677
|
+
commands.push({ type: "Z" });
|
|
611
678
|
i += 1;
|
|
612
679
|
break;
|
|
613
680
|
|
|
@@ -654,15 +721,17 @@ export function resolveMarkers(pathElement, defsMap) {
|
|
|
654
721
|
const instances = [];
|
|
655
722
|
|
|
656
723
|
// Get marker references
|
|
657
|
-
const markerStart = pathElement.getAttribute(
|
|
658
|
-
const markerMid = pathElement.getAttribute(
|
|
659
|
-
const markerEnd = pathElement.getAttribute(
|
|
724
|
+
const markerStart = pathElement.getAttribute("marker-start");
|
|
725
|
+
const markerMid = pathElement.getAttribute("marker-mid");
|
|
726
|
+
const markerEnd = pathElement.getAttribute("marker-end");
|
|
660
727
|
|
|
661
728
|
// Get stroke width for scaling
|
|
662
|
-
const strokeWidth = parseFloat(
|
|
729
|
+
const strokeWidth = parseFloat(
|
|
730
|
+
pathElement.getAttribute("stroke-width") || "1",
|
|
731
|
+
);
|
|
663
732
|
|
|
664
733
|
// Get path data and extract vertices
|
|
665
|
-
const pathData = pathElement.getAttribute(
|
|
734
|
+
const pathData = pathElement.getAttribute("d") || "";
|
|
666
735
|
const vertices = getPathVertices(pathData);
|
|
667
736
|
|
|
668
737
|
if (vertices.length === 0) {
|
|
@@ -683,14 +752,21 @@ export function resolveMarkers(pathElement, defsMap) {
|
|
|
683
752
|
|
|
684
753
|
if (markerDef && vertices.length > 0) {
|
|
685
754
|
const vertex = vertices[0];
|
|
686
|
-
const tangent =
|
|
755
|
+
const tangent =
|
|
756
|
+
vertices.length > 1 ? vertex.tangentOut : vertex.tangentIn;
|
|
687
757
|
|
|
688
758
|
instances.push({
|
|
689
759
|
markerDef,
|
|
690
760
|
position: { x: vertex.x, y: vertex.y },
|
|
691
|
-
transform: getMarkerTransform(
|
|
692
|
-
|
|
693
|
-
|
|
761
|
+
transform: getMarkerTransform(
|
|
762
|
+
markerDef,
|
|
763
|
+
{ x: vertex.x, y: vertex.y },
|
|
764
|
+
tangent,
|
|
765
|
+
strokeWidth,
|
|
766
|
+
true,
|
|
767
|
+
),
|
|
768
|
+
type: "start",
|
|
769
|
+
vertex,
|
|
694
770
|
});
|
|
695
771
|
}
|
|
696
772
|
}
|
|
@@ -709,9 +785,15 @@ export function resolveMarkers(pathElement, defsMap) {
|
|
|
709
785
|
instances.push({
|
|
710
786
|
markerDef,
|
|
711
787
|
position: { x: vertex.x, y: vertex.y },
|
|
712
|
-
transform: getMarkerTransform(
|
|
713
|
-
|
|
714
|
-
|
|
788
|
+
transform: getMarkerTransform(
|
|
789
|
+
markerDef,
|
|
790
|
+
{ x: vertex.x, y: vertex.y },
|
|
791
|
+
tangent,
|
|
792
|
+
strokeWidth,
|
|
793
|
+
false,
|
|
794
|
+
),
|
|
795
|
+
type: "mid",
|
|
796
|
+
vertex,
|
|
715
797
|
});
|
|
716
798
|
}
|
|
717
799
|
}
|
|
@@ -729,9 +811,15 @@ export function resolveMarkers(pathElement, defsMap) {
|
|
|
729
811
|
instances.push({
|
|
730
812
|
markerDef,
|
|
731
813
|
position: { x: vertex.x, y: vertex.y },
|
|
732
|
-
transform: getMarkerTransform(
|
|
733
|
-
|
|
734
|
-
|
|
814
|
+
transform: getMarkerTransform(
|
|
815
|
+
markerDef,
|
|
816
|
+
{ x: vertex.x, y: vertex.y },
|
|
817
|
+
tangent,
|
|
818
|
+
strokeWidth,
|
|
819
|
+
false,
|
|
820
|
+
),
|
|
821
|
+
type: "end",
|
|
822
|
+
vertex,
|
|
735
823
|
});
|
|
736
824
|
}
|
|
737
825
|
}
|
|
@@ -771,52 +859,58 @@ export function markerToPolygons(markerInstance, options = {}) {
|
|
|
771
859
|
let points = [];
|
|
772
860
|
|
|
773
861
|
switch (child.type) {
|
|
774
|
-
case
|
|
862
|
+
case "path":
|
|
775
863
|
// Parse path and convert to points
|
|
776
864
|
points = pathToPoints(child.d, curveSegments);
|
|
777
865
|
break;
|
|
778
866
|
|
|
779
|
-
case
|
|
867
|
+
case "rect":
|
|
780
868
|
// Convert rect to 4 corner points
|
|
781
869
|
points = [
|
|
782
870
|
{ x: child.x, y: child.y },
|
|
783
871
|
{ x: child.x + child.width, y: child.y },
|
|
784
872
|
{ x: child.x + child.width, y: child.y + child.height },
|
|
785
|
-
{ x: child.x, y: child.y + child.height }
|
|
873
|
+
{ x: child.x, y: child.y + child.height },
|
|
786
874
|
];
|
|
787
875
|
break;
|
|
788
876
|
|
|
789
|
-
case
|
|
877
|
+
case "circle":
|
|
790
878
|
// Approximate circle with polygon
|
|
791
879
|
points = circleToPoints(child.cx, child.cy, child.r, curveSegments * 4);
|
|
792
880
|
break;
|
|
793
881
|
|
|
794
|
-
case
|
|
882
|
+
case "ellipse":
|
|
795
883
|
// Approximate ellipse with polygon
|
|
796
|
-
points = ellipseToPoints(
|
|
884
|
+
points = ellipseToPoints(
|
|
885
|
+
child.cx,
|
|
886
|
+
child.cy,
|
|
887
|
+
child.rx,
|
|
888
|
+
child.ry,
|
|
889
|
+
curveSegments * 4,
|
|
890
|
+
);
|
|
797
891
|
break;
|
|
798
892
|
|
|
799
|
-
case
|
|
893
|
+
case "line":
|
|
800
894
|
points = [
|
|
801
895
|
{ x: child.x1, y: child.y1 },
|
|
802
|
-
{ x: child.x2, y: child.y2 }
|
|
896
|
+
{ x: child.x2, y: child.y2 },
|
|
803
897
|
];
|
|
804
898
|
break;
|
|
805
899
|
|
|
806
|
-
case
|
|
807
|
-
case
|
|
900
|
+
case "polygon":
|
|
901
|
+
case "polyline":
|
|
808
902
|
points = parsePoints(child.points);
|
|
809
903
|
break;
|
|
810
904
|
}
|
|
811
905
|
|
|
812
906
|
// Apply transform to all points
|
|
813
907
|
if (points.length > 0) {
|
|
814
|
-
const transformedPoints = points.map(p => {
|
|
908
|
+
const transformedPoints = points.map((p) => {
|
|
815
909
|
const result = Transforms2D.applyTransform(transform, p.x, p.y);
|
|
816
910
|
// result is an array [x, y] with Decimal values
|
|
817
911
|
return {
|
|
818
912
|
x: parseFloat(result[0].toFixed(precision)),
|
|
819
|
-
y: parseFloat(result[1].toFixed(precision))
|
|
913
|
+
y: parseFloat(result[1].toFixed(precision)),
|
|
820
914
|
};
|
|
821
915
|
});
|
|
822
916
|
|
|
@@ -842,55 +936,53 @@ export function pathToPoints(pathData, segments = 10) {
|
|
|
842
936
|
|
|
843
937
|
for (const cmd of commands) {
|
|
844
938
|
switch (cmd.type) {
|
|
845
|
-
case
|
|
939
|
+
case "M":
|
|
846
940
|
currentX = cmd.x;
|
|
847
941
|
currentY = cmd.y;
|
|
848
942
|
points.push({ x: currentX, y: currentY });
|
|
849
943
|
break;
|
|
850
944
|
|
|
851
|
-
case
|
|
945
|
+
case "L":
|
|
852
946
|
currentX = cmd.x;
|
|
853
947
|
currentY = cmd.y;
|
|
854
948
|
points.push({ x: currentX, y: currentY });
|
|
855
949
|
break;
|
|
856
950
|
|
|
857
|
-
case
|
|
951
|
+
case "C":
|
|
858
952
|
// Approximate cubic bezier with line segments
|
|
859
953
|
for (let i = 1; i <= segments; i++) {
|
|
860
954
|
const t = i / segments;
|
|
861
955
|
const t1 = 1 - t;
|
|
862
|
-
const x =
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
956
|
+
const x =
|
|
957
|
+
t1 * t1 * t1 * currentX +
|
|
958
|
+
3 * t1 * t1 * t * cmd.x1 +
|
|
959
|
+
3 * t1 * t * t * cmd.x2 +
|
|
960
|
+
t * t * t * cmd.x;
|
|
961
|
+
const y =
|
|
962
|
+
t1 * t1 * t1 * currentY +
|
|
963
|
+
3 * t1 * t1 * t * cmd.y1 +
|
|
964
|
+
3 * t1 * t * t * cmd.y2 +
|
|
965
|
+
t * t * t * cmd.y;
|
|
870
966
|
points.push({ x, y });
|
|
871
967
|
}
|
|
872
968
|
currentX = cmd.x;
|
|
873
969
|
currentY = cmd.y;
|
|
874
970
|
break;
|
|
875
971
|
|
|
876
|
-
case
|
|
972
|
+
case "Q":
|
|
877
973
|
// Approximate quadratic bezier with line segments
|
|
878
974
|
for (let i = 1; i <= segments; i++) {
|
|
879
975
|
const t = i / segments;
|
|
880
976
|
const t1 = 1 - t;
|
|
881
|
-
const x = t1 * t1 * currentX +
|
|
882
|
-
|
|
883
|
-
t * t * cmd.x;
|
|
884
|
-
const y = t1 * t1 * currentY +
|
|
885
|
-
2 * t1 * t * cmd.y1 +
|
|
886
|
-
t * t * cmd.y;
|
|
977
|
+
const x = t1 * t1 * currentX + 2 * t1 * t * cmd.x1 + t * t * cmd.x;
|
|
978
|
+
const y = t1 * t1 * currentY + 2 * t1 * t * cmd.y1 + t * t * cmd.y;
|
|
887
979
|
points.push({ x, y });
|
|
888
980
|
}
|
|
889
981
|
currentX = cmd.x;
|
|
890
982
|
currentY = cmd.y;
|
|
891
983
|
break;
|
|
892
984
|
|
|
893
|
-
case
|
|
985
|
+
case "A":
|
|
894
986
|
// Simplified arc approximation
|
|
895
987
|
currentX = cmd.x;
|
|
896
988
|
currentY = cmd.y;
|
|
@@ -917,7 +1009,7 @@ export function circleToPoints(cx, cy, r, segments = 32) {
|
|
|
917
1009
|
const angle = (i / segments) * 2 * Math.PI;
|
|
918
1010
|
points.push({
|
|
919
1011
|
x: cx + r * Math.cos(angle),
|
|
920
|
-
y: cy + r * Math.sin(angle)
|
|
1012
|
+
y: cy + r * Math.sin(angle),
|
|
921
1013
|
});
|
|
922
1014
|
}
|
|
923
1015
|
return points;
|
|
@@ -939,7 +1031,7 @@ export function ellipseToPoints(cx, cy, rx, ry, segments = 32) {
|
|
|
939
1031
|
const angle = (i / segments) * 2 * Math.PI;
|
|
940
1032
|
points.push({
|
|
941
1033
|
x: cx + rx * Math.cos(angle),
|
|
942
|
-
y: cy + ry * Math.sin(angle)
|
|
1034
|
+
y: cy + ry * Math.sin(angle),
|
|
943
1035
|
});
|
|
944
1036
|
}
|
|
945
1037
|
return points;
|
|
@@ -953,7 +1045,10 @@ export function ellipseToPoints(cx, cy, rx, ry, segments = 32) {
|
|
|
953
1045
|
*/
|
|
954
1046
|
export function parsePoints(pointsStr) {
|
|
955
1047
|
const points = [];
|
|
956
|
-
const coords = pointsStr
|
|
1048
|
+
const coords = pointsStr
|
|
1049
|
+
.trim()
|
|
1050
|
+
.split(/[\s,]+/)
|
|
1051
|
+
.map(Number);
|
|
957
1052
|
|
|
958
1053
|
for (let i = 0; i < coords.length - 1; i += 2) {
|
|
959
1054
|
points.push({ x: coords[i], y: coords[i + 1] });
|
|
@@ -989,18 +1084,37 @@ export function markersToPathData(markerInstances, precision = 2) {
|
|
|
989
1084
|
|
|
990
1085
|
// Start with M (moveto) command
|
|
991
1086
|
const first = polygon[0];
|
|
992
|
-
pathParts.push(
|
|
1087
|
+
pathParts.push(
|
|
1088
|
+
`M ${first.x.toFixed(precision)} ${first.y.toFixed(precision)}`,
|
|
1089
|
+
);
|
|
993
1090
|
|
|
994
1091
|
// Add L (lineto) commands for remaining points
|
|
995
1092
|
for (let i = 1; i < polygon.length; i++) {
|
|
996
1093
|
const pt = polygon[i];
|
|
997
|
-
pathParts.push(
|
|
1094
|
+
pathParts.push(
|
|
1095
|
+
`L ${pt.x.toFixed(precision)} ${pt.y.toFixed(precision)}`,
|
|
1096
|
+
);
|
|
998
1097
|
}
|
|
999
1098
|
|
|
1000
1099
|
// Close path
|
|
1001
|
-
pathParts.push(
|
|
1100
|
+
pathParts.push("Z");
|
|
1002
1101
|
}
|
|
1003
1102
|
}
|
|
1004
1103
|
|
|
1005
|
-
return pathParts.join(
|
|
1104
|
+
return pathParts.join(" ");
|
|
1006
1105
|
}
|
|
1106
|
+
|
|
1107
|
+
export default {
|
|
1108
|
+
parseMarkerElement,
|
|
1109
|
+
parseMarkerChild,
|
|
1110
|
+
getMarkerTransform,
|
|
1111
|
+
getPathVertices,
|
|
1112
|
+
parsePathCommands,
|
|
1113
|
+
resolveMarkers,
|
|
1114
|
+
markerToPolygons,
|
|
1115
|
+
pathToPoints,
|
|
1116
|
+
circleToPoints,
|
|
1117
|
+
ellipseToPoints,
|
|
1118
|
+
parsePoints,
|
|
1119
|
+
markersToPathData,
|
|
1120
|
+
};
|