@emasoft/svg-matrix 1.0.28 → 1.0.30
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 +985 -378
- package/bin/svglinter.cjs +4172 -433
- package/bin/svgm.js +723 -180
- 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 +18 -7
- 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 +22 -18
- 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 +16381 -3370
- package/src/svg2-polyfills.js +93 -224
- 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
|
@@ -108,20 +108,20 @@
|
|
|
108
108
|
* @module off-canvas-detection
|
|
109
109
|
*/
|
|
110
110
|
|
|
111
|
-
import Decimal from
|
|
112
|
-
import { polygonsOverlap, point } from
|
|
111
|
+
import Decimal from "decimal.js";
|
|
112
|
+
import { polygonsOverlap, point } from "./gjk-collision.js";
|
|
113
113
|
|
|
114
114
|
// Set high precision for all calculations
|
|
115
115
|
Decimal.set({ precision: 80 });
|
|
116
116
|
|
|
117
117
|
// Helper to convert to Decimal
|
|
118
|
-
const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
118
|
+
const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
|
|
119
119
|
|
|
120
120
|
// Near-zero threshold for comparisons
|
|
121
|
-
const EPSILON = new Decimal(
|
|
121
|
+
const EPSILON = new Decimal("1e-40");
|
|
122
122
|
|
|
123
123
|
// Default tolerance for containment checks
|
|
124
|
-
const DEFAULT_TOLERANCE = new Decimal(
|
|
124
|
+
const DEFAULT_TOLERANCE = new Decimal("1e-10");
|
|
125
125
|
|
|
126
126
|
// Number of samples for path/curve verification
|
|
127
127
|
const VERIFICATION_SAMPLES = 100;
|
|
@@ -143,15 +143,20 @@ const VERIFICATION_SAMPLES = 100;
|
|
|
143
143
|
* @throws {Error} If viewBox string is invalid
|
|
144
144
|
*/
|
|
145
145
|
export function parseViewBox(viewBoxString) {
|
|
146
|
-
if (typeof viewBoxString !==
|
|
147
|
-
throw new Error(
|
|
146
|
+
if (typeof viewBoxString !== "string") {
|
|
147
|
+
throw new Error("ViewBox must be a string");
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
// Split on whitespace and/or commas
|
|
151
|
-
const parts = viewBoxString
|
|
151
|
+
const parts = viewBoxString
|
|
152
|
+
.trim()
|
|
153
|
+
.split(/[\s,]+/)
|
|
154
|
+
.filter((p) => p.length > 0);
|
|
152
155
|
|
|
153
156
|
if (parts.length !== 4) {
|
|
154
|
-
throw new Error(
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Invalid viewBox format: expected 4 values, got ${parts.length}`,
|
|
159
|
+
);
|
|
155
160
|
}
|
|
156
161
|
|
|
157
162
|
try {
|
|
@@ -162,12 +167,12 @@ export function parseViewBox(viewBoxString) {
|
|
|
162
167
|
|
|
163
168
|
// Validate positive dimensions
|
|
164
169
|
if (width.lessThanOrEqualTo(0) || height.lessThanOrEqualTo(0)) {
|
|
165
|
-
throw new Error(
|
|
170
|
+
throw new Error("ViewBox width and height must be positive");
|
|
166
171
|
}
|
|
167
172
|
|
|
168
173
|
// VERIFICATION: Reconstruct string and compare
|
|
169
174
|
const reconstructed = `${x.toString()} ${y.toString()} ${width.toString()} ${height.toString()}`;
|
|
170
|
-
const normalizedOriginal = parts.join(
|
|
175
|
+
const normalizedOriginal = parts.join(" ");
|
|
171
176
|
const verified = reconstructed === normalizedOriginal;
|
|
172
177
|
|
|
173
178
|
return { x, y, width, height, verified };
|
|
@@ -196,7 +201,7 @@ function createBBox(minX, minY, maxX, maxY) {
|
|
|
196
201
|
maxX,
|
|
197
202
|
maxY,
|
|
198
203
|
width: maxX.minus(minX),
|
|
199
|
-
height: maxY.minus(minY)
|
|
204
|
+
height: maxY.minus(minY),
|
|
200
205
|
};
|
|
201
206
|
}
|
|
202
207
|
|
|
@@ -208,12 +213,12 @@ function createBBox(minX, minY, maxX, maxY) {
|
|
|
208
213
|
* @param {Decimal} y - Point Y coordinate
|
|
209
214
|
* @returns {{minX: Decimal, minY: Decimal, maxX: Decimal, maxY: Decimal}}
|
|
210
215
|
*/
|
|
211
|
-
function
|
|
216
|
+
function _expandBBox(bbox, x, y) {
|
|
212
217
|
return {
|
|
213
218
|
minX: Decimal.min(bbox.minX, x),
|
|
214
219
|
minY: Decimal.min(bbox.minY, y),
|
|
215
220
|
maxX: Decimal.max(bbox.maxX, x),
|
|
216
|
-
maxY: Decimal.max(bbox.maxY, y)
|
|
221
|
+
maxY: Decimal.max(bbox.maxY, y),
|
|
217
222
|
};
|
|
218
223
|
}
|
|
219
224
|
|
|
@@ -243,12 +248,14 @@ function sampleCubicBezier(x0, y0, x1, y1, x2, y2, x3, y3, samples = 20) {
|
|
|
243
248
|
const oneMinusT2 = oneMinusT.mul(oneMinusT);
|
|
244
249
|
const oneMinusT3 = oneMinusT2.mul(oneMinusT);
|
|
245
250
|
|
|
246
|
-
const x = oneMinusT3
|
|
251
|
+
const x = oneMinusT3
|
|
252
|
+
.mul(x0)
|
|
247
253
|
.plus(D(3).mul(oneMinusT2).mul(t).mul(x1))
|
|
248
254
|
.plus(D(3).mul(oneMinusT).mul(t2).mul(x2))
|
|
249
255
|
.plus(t3.mul(x3));
|
|
250
256
|
|
|
251
|
-
const y = oneMinusT3
|
|
257
|
+
const y = oneMinusT3
|
|
258
|
+
.mul(y0)
|
|
252
259
|
.plus(D(3).mul(oneMinusT2).mul(t).mul(y1))
|
|
253
260
|
.plus(D(3).mul(oneMinusT).mul(t2).mul(y2))
|
|
254
261
|
.plus(t3.mul(y3));
|
|
@@ -280,11 +287,13 @@ function sampleQuadraticBezier(x0, y0, x1, y1, x2, y2, samples = 20) {
|
|
|
280
287
|
const oneMinusT2 = oneMinusT.mul(oneMinusT);
|
|
281
288
|
const t2 = t.mul(t);
|
|
282
289
|
|
|
283
|
-
const x = oneMinusT2
|
|
290
|
+
const x = oneMinusT2
|
|
291
|
+
.mul(x0)
|
|
284
292
|
.plus(D(2).mul(oneMinusT).mul(t).mul(x1))
|
|
285
293
|
.plus(t2.mul(x2));
|
|
286
294
|
|
|
287
|
-
const y = oneMinusT2
|
|
295
|
+
const y = oneMinusT2
|
|
296
|
+
.mul(y0)
|
|
288
297
|
.plus(D(2).mul(oneMinusT).mul(t).mul(y1))
|
|
289
298
|
.plus(t2.mul(y2));
|
|
290
299
|
|
|
@@ -302,10 +311,12 @@ function sampleQuadraticBezier(x0, y0, x1, y1, x2, y2, samples = 20) {
|
|
|
302
311
|
* @returns {boolean} True if point is inside or on boundary
|
|
303
312
|
*/
|
|
304
313
|
function pointInBBox(pt, bbox, tolerance = DEFAULT_TOLERANCE) {
|
|
305
|
-
return
|
|
314
|
+
return (
|
|
315
|
+
pt.x.greaterThanOrEqualTo(bbox.minX.minus(tolerance)) &&
|
|
306
316
|
pt.x.lessThanOrEqualTo(bbox.maxX.plus(tolerance)) &&
|
|
307
317
|
pt.y.greaterThanOrEqualTo(bbox.minY.minus(tolerance)) &&
|
|
308
|
-
pt.y.lessThanOrEqualTo(bbox.maxY.plus(tolerance))
|
|
318
|
+
pt.y.lessThanOrEqualTo(bbox.maxY.plus(tolerance))
|
|
319
|
+
);
|
|
309
320
|
}
|
|
310
321
|
|
|
311
322
|
// ============================================================================
|
|
@@ -326,7 +337,7 @@ function pointInBBox(pt, bbox, tolerance = DEFAULT_TOLERANCE) {
|
|
|
326
337
|
*/
|
|
327
338
|
export function pathBoundingBox(pathCommands) {
|
|
328
339
|
if (!pathCommands || pathCommands.length === 0) {
|
|
329
|
-
throw new Error(
|
|
340
|
+
throw new Error("Path commands array is empty");
|
|
330
341
|
}
|
|
331
342
|
|
|
332
343
|
let minX = D(Infinity);
|
|
@@ -352,7 +363,7 @@ export function pathBoundingBox(pathCommands) {
|
|
|
352
363
|
const isRelative = cmd.type === cmd.type.toLowerCase();
|
|
353
364
|
|
|
354
365
|
switch (type) {
|
|
355
|
-
case
|
|
366
|
+
case "M": // Move
|
|
356
367
|
{
|
|
357
368
|
// BUG 1 FIX: Handle relative coordinates
|
|
358
369
|
const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
|
|
@@ -370,11 +381,11 @@ export function pathBoundingBox(pathCommands) {
|
|
|
370
381
|
// BUG 3 FIX: Reset lastControl after non-curve command
|
|
371
382
|
lastControlX = currentX;
|
|
372
383
|
lastControlY = currentY;
|
|
373
|
-
lastCommand =
|
|
384
|
+
lastCommand = "M";
|
|
374
385
|
}
|
|
375
386
|
break;
|
|
376
387
|
|
|
377
|
-
case
|
|
388
|
+
case "L": // Line to
|
|
378
389
|
{
|
|
379
390
|
// BUG 1 FIX: Handle relative coordinates
|
|
380
391
|
const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
|
|
@@ -390,11 +401,11 @@ export function pathBoundingBox(pathCommands) {
|
|
|
390
401
|
// BUG 3 FIX: Reset lastControl after non-curve command
|
|
391
402
|
lastControlX = currentX;
|
|
392
403
|
lastControlY = currentY;
|
|
393
|
-
lastCommand =
|
|
404
|
+
lastCommand = "L";
|
|
394
405
|
}
|
|
395
406
|
break;
|
|
396
407
|
|
|
397
|
-
case
|
|
408
|
+
case "H": // Horizontal line
|
|
398
409
|
{
|
|
399
410
|
// BUG 1 FIX: Handle relative coordinates
|
|
400
411
|
const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
|
|
@@ -406,11 +417,11 @@ export function pathBoundingBox(pathCommands) {
|
|
|
406
417
|
// BUG 3 FIX: Reset lastControl after non-curve command
|
|
407
418
|
lastControlX = currentX;
|
|
408
419
|
lastControlY = currentY;
|
|
409
|
-
lastCommand =
|
|
420
|
+
lastCommand = "H";
|
|
410
421
|
}
|
|
411
422
|
break;
|
|
412
423
|
|
|
413
|
-
case
|
|
424
|
+
case "V": // Vertical line
|
|
414
425
|
{
|
|
415
426
|
// BUG 1 FIX: Handle relative coordinates
|
|
416
427
|
const y = isRelative ? currentY.plus(D(cmd.y)) : D(cmd.y);
|
|
@@ -422,11 +433,11 @@ export function pathBoundingBox(pathCommands) {
|
|
|
422
433
|
// BUG 3 FIX: Reset lastControl after non-curve command
|
|
423
434
|
lastControlX = currentX;
|
|
424
435
|
lastControlY = currentY;
|
|
425
|
-
lastCommand =
|
|
436
|
+
lastCommand = "V";
|
|
426
437
|
}
|
|
427
438
|
break;
|
|
428
439
|
|
|
429
|
-
case
|
|
440
|
+
case "C": // Cubic Bezier
|
|
430
441
|
{
|
|
431
442
|
// BUG 1 FIX: Handle relative coordinates
|
|
432
443
|
const x1 = isRelative ? currentX.plus(D(cmd.x1)) : D(cmd.x1);
|
|
@@ -437,7 +448,17 @@ export function pathBoundingBox(pathCommands) {
|
|
|
437
448
|
const y = isRelative ? currentY.plus(D(cmd.y)) : D(cmd.y);
|
|
438
449
|
|
|
439
450
|
// Sample curve points
|
|
440
|
-
const curvePoints = sampleCubicBezier(
|
|
451
|
+
const curvePoints = sampleCubicBezier(
|
|
452
|
+
currentX,
|
|
453
|
+
currentY,
|
|
454
|
+
x1,
|
|
455
|
+
y1,
|
|
456
|
+
x2,
|
|
457
|
+
y2,
|
|
458
|
+
x,
|
|
459
|
+
y,
|
|
460
|
+
20,
|
|
461
|
+
);
|
|
441
462
|
for (const pt of curvePoints) {
|
|
442
463
|
minX = Decimal.min(minX, pt.x);
|
|
443
464
|
minY = Decimal.min(minY, pt.y);
|
|
@@ -450,11 +471,11 @@ export function pathBoundingBox(pathCommands) {
|
|
|
450
471
|
lastControlY = y2;
|
|
451
472
|
currentX = x;
|
|
452
473
|
currentY = y;
|
|
453
|
-
lastCommand =
|
|
474
|
+
lastCommand = "C";
|
|
454
475
|
}
|
|
455
476
|
break;
|
|
456
477
|
|
|
457
|
-
case
|
|
478
|
+
case "S": // Smooth cubic Bezier
|
|
458
479
|
{
|
|
459
480
|
// BUG 1 FIX: Handle relative coordinates
|
|
460
481
|
const x2 = isRelative ? currentX.plus(D(cmd.x2)) : D(cmd.x2);
|
|
@@ -465,7 +486,7 @@ export function pathBoundingBox(pathCommands) {
|
|
|
465
486
|
// Reflect last control point
|
|
466
487
|
// BUG 3 FIX: lastControlX/Y are now always initialized, safe to use
|
|
467
488
|
let x1, y1;
|
|
468
|
-
if (lastCommand ===
|
|
489
|
+
if (lastCommand === "C" || lastCommand === "S") {
|
|
469
490
|
x1 = currentX.mul(2).minus(lastControlX);
|
|
470
491
|
y1 = currentY.mul(2).minus(lastControlY);
|
|
471
492
|
} else {
|
|
@@ -473,7 +494,17 @@ export function pathBoundingBox(pathCommands) {
|
|
|
473
494
|
y1 = currentY;
|
|
474
495
|
}
|
|
475
496
|
|
|
476
|
-
const curvePoints = sampleCubicBezier(
|
|
497
|
+
const curvePoints = sampleCubicBezier(
|
|
498
|
+
currentX,
|
|
499
|
+
currentY,
|
|
500
|
+
x1,
|
|
501
|
+
y1,
|
|
502
|
+
x2,
|
|
503
|
+
y2,
|
|
504
|
+
x,
|
|
505
|
+
y,
|
|
506
|
+
20,
|
|
507
|
+
);
|
|
477
508
|
for (const pt of curvePoints) {
|
|
478
509
|
minX = Decimal.min(minX, pt.x);
|
|
479
510
|
minY = Decimal.min(minY, pt.y);
|
|
@@ -486,11 +517,11 @@ export function pathBoundingBox(pathCommands) {
|
|
|
486
517
|
lastControlY = y2;
|
|
487
518
|
currentX = x;
|
|
488
519
|
currentY = y;
|
|
489
|
-
lastCommand =
|
|
520
|
+
lastCommand = "S";
|
|
490
521
|
}
|
|
491
522
|
break;
|
|
492
523
|
|
|
493
|
-
case
|
|
524
|
+
case "Q": // Quadratic Bezier
|
|
494
525
|
{
|
|
495
526
|
// BUG 1 FIX: Handle relative coordinates
|
|
496
527
|
const x1 = isRelative ? currentX.plus(D(cmd.x1)) : D(cmd.x1);
|
|
@@ -498,7 +529,15 @@ export function pathBoundingBox(pathCommands) {
|
|
|
498
529
|
const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
|
|
499
530
|
const y = isRelative ? currentY.plus(D(cmd.y)) : D(cmd.y);
|
|
500
531
|
|
|
501
|
-
const curvePoints = sampleQuadraticBezier(
|
|
532
|
+
const curvePoints = sampleQuadraticBezier(
|
|
533
|
+
currentX,
|
|
534
|
+
currentY,
|
|
535
|
+
x1,
|
|
536
|
+
y1,
|
|
537
|
+
x,
|
|
538
|
+
y,
|
|
539
|
+
20,
|
|
540
|
+
);
|
|
502
541
|
for (const pt of curvePoints) {
|
|
503
542
|
minX = Decimal.min(minX, pt.x);
|
|
504
543
|
minY = Decimal.min(minY, pt.y);
|
|
@@ -511,11 +550,11 @@ export function pathBoundingBox(pathCommands) {
|
|
|
511
550
|
lastControlY = y1;
|
|
512
551
|
currentX = x;
|
|
513
552
|
currentY = y;
|
|
514
|
-
lastCommand =
|
|
553
|
+
lastCommand = "Q";
|
|
515
554
|
}
|
|
516
555
|
break;
|
|
517
556
|
|
|
518
|
-
case
|
|
557
|
+
case "T": // Smooth quadratic Bezier
|
|
519
558
|
{
|
|
520
559
|
// BUG 1 FIX: Handle relative coordinates
|
|
521
560
|
const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
|
|
@@ -524,7 +563,7 @@ export function pathBoundingBox(pathCommands) {
|
|
|
524
563
|
// Reflect last control point
|
|
525
564
|
// BUG 3 FIX: lastControlX/Y are now always initialized, safe to use
|
|
526
565
|
let x1, y1;
|
|
527
|
-
if (lastCommand ===
|
|
566
|
+
if (lastCommand === "Q" || lastCommand === "T") {
|
|
528
567
|
x1 = currentX.mul(2).minus(lastControlX);
|
|
529
568
|
y1 = currentY.mul(2).minus(lastControlY);
|
|
530
569
|
} else {
|
|
@@ -532,7 +571,15 @@ export function pathBoundingBox(pathCommands) {
|
|
|
532
571
|
y1 = currentY;
|
|
533
572
|
}
|
|
534
573
|
|
|
535
|
-
const curvePoints = sampleQuadraticBezier(
|
|
574
|
+
const curvePoints = sampleQuadraticBezier(
|
|
575
|
+
currentX,
|
|
576
|
+
currentY,
|
|
577
|
+
x1,
|
|
578
|
+
y1,
|
|
579
|
+
x,
|
|
580
|
+
y,
|
|
581
|
+
20,
|
|
582
|
+
);
|
|
536
583
|
for (const pt of curvePoints) {
|
|
537
584
|
minX = Decimal.min(minX, pt.x);
|
|
538
585
|
minY = Decimal.min(minY, pt.y);
|
|
@@ -545,11 +592,11 @@ export function pathBoundingBox(pathCommands) {
|
|
|
545
592
|
lastControlY = y1;
|
|
546
593
|
currentX = x;
|
|
547
594
|
currentY = y;
|
|
548
|
-
lastCommand =
|
|
595
|
+
lastCommand = "T";
|
|
549
596
|
}
|
|
550
597
|
break;
|
|
551
598
|
|
|
552
|
-
case
|
|
599
|
+
case "A": // Arc (approximate with samples)
|
|
553
600
|
{
|
|
554
601
|
// BUG 4: Arc bounding box ignores actual arc geometry
|
|
555
602
|
// TODO: Implement proper arc-to-bezier conversion or calculate arc extrema
|
|
@@ -583,17 +630,17 @@ export function pathBoundingBox(pathCommands) {
|
|
|
583
630
|
// BUG 3 FIX: Reset lastControl after non-curve command
|
|
584
631
|
lastControlX = currentX;
|
|
585
632
|
lastControlY = currentY;
|
|
586
|
-
lastCommand =
|
|
633
|
+
lastCommand = "A";
|
|
587
634
|
}
|
|
588
635
|
break;
|
|
589
636
|
|
|
590
|
-
case
|
|
637
|
+
case "Z": // Close path
|
|
591
638
|
currentX = startX;
|
|
592
639
|
currentY = startY;
|
|
593
640
|
// BUG 3 FIX: Reset lastControl after non-curve command
|
|
594
641
|
lastControlX = currentX;
|
|
595
642
|
lastControlY = currentY;
|
|
596
|
-
lastCommand =
|
|
643
|
+
lastCommand = "Z";
|
|
597
644
|
break;
|
|
598
645
|
|
|
599
646
|
default:
|
|
@@ -601,8 +648,13 @@ export function pathBoundingBox(pathCommands) {
|
|
|
601
648
|
}
|
|
602
649
|
}
|
|
603
650
|
|
|
604
|
-
if (
|
|
605
|
-
|
|
651
|
+
if (
|
|
652
|
+
!minX.isFinite() ||
|
|
653
|
+
!minY.isFinite() ||
|
|
654
|
+
!maxX.isFinite() ||
|
|
655
|
+
!maxY.isFinite()
|
|
656
|
+
) {
|
|
657
|
+
throw new Error("Invalid bounding box: no valid coordinates found");
|
|
606
658
|
}
|
|
607
659
|
|
|
608
660
|
const bbox = createBBox(minX, minY, maxX, maxY);
|
|
@@ -636,7 +688,7 @@ export function pathBoundingBox(pathCommands) {
|
|
|
636
688
|
*/
|
|
637
689
|
export function shapeBoundingBox(shape) {
|
|
638
690
|
if (!shape || !shape.type) {
|
|
639
|
-
throw new Error(
|
|
691
|
+
throw new Error("Shape object must have a type property");
|
|
640
692
|
}
|
|
641
693
|
|
|
642
694
|
const type = shape.type.toLowerCase();
|
|
@@ -644,7 +696,7 @@ export function shapeBoundingBox(shape) {
|
|
|
644
696
|
let samplePoints = [];
|
|
645
697
|
|
|
646
698
|
switch (type) {
|
|
647
|
-
case
|
|
699
|
+
case "rect":
|
|
648
700
|
{
|
|
649
701
|
const x = D(shape.x);
|
|
650
702
|
const y = D(shape.y);
|
|
@@ -658,12 +710,12 @@ export function shapeBoundingBox(shape) {
|
|
|
658
710
|
{ x, y },
|
|
659
711
|
{ x: x.plus(width), y },
|
|
660
712
|
{ x: x.plus(width), y: y.plus(height) },
|
|
661
|
-
{ x, y: y.plus(height) }
|
|
713
|
+
{ x, y: y.plus(height) },
|
|
662
714
|
];
|
|
663
715
|
}
|
|
664
716
|
break;
|
|
665
717
|
|
|
666
|
-
case
|
|
718
|
+
case "circle":
|
|
667
719
|
{
|
|
668
720
|
const cx = D(shape.cx);
|
|
669
721
|
const cy = D(shape.cy);
|
|
@@ -683,7 +735,7 @@ export function shapeBoundingBox(shape) {
|
|
|
683
735
|
}
|
|
684
736
|
break;
|
|
685
737
|
|
|
686
|
-
case
|
|
738
|
+
case "ellipse":
|
|
687
739
|
{
|
|
688
740
|
const cx = D(shape.cx);
|
|
689
741
|
const cy = D(shape.cy);
|
|
@@ -704,7 +756,7 @@ export function shapeBoundingBox(shape) {
|
|
|
704
756
|
}
|
|
705
757
|
break;
|
|
706
758
|
|
|
707
|
-
case
|
|
759
|
+
case "line":
|
|
708
760
|
{
|
|
709
761
|
const x1 = D(shape.x1);
|
|
710
762
|
const y1 = D(shape.y1);
|
|
@@ -717,12 +769,15 @@ export function shapeBoundingBox(shape) {
|
|
|
717
769
|
const maxY = Decimal.max(y1, y2);
|
|
718
770
|
|
|
719
771
|
bbox = createBBox(minX, minY, maxX, maxY);
|
|
720
|
-
samplePoints = [
|
|
772
|
+
samplePoints = [
|
|
773
|
+
{ x: x1, y: y1 },
|
|
774
|
+
{ x: x2, y: y2 },
|
|
775
|
+
];
|
|
721
776
|
}
|
|
722
777
|
break;
|
|
723
778
|
|
|
724
|
-
case
|
|
725
|
-
case
|
|
779
|
+
case "polygon":
|
|
780
|
+
case "polyline":
|
|
726
781
|
{
|
|
727
782
|
if (!shape.points || shape.points.length === 0) {
|
|
728
783
|
throw new Error(`${type} must have points array`);
|
|
@@ -778,7 +833,7 @@ function bboxToPolygon(bbox) {
|
|
|
778
833
|
point(bbox.minX, bbox.minY),
|
|
779
834
|
point(bbox.maxX, bbox.minY),
|
|
780
835
|
point(bbox.maxX, bbox.maxY),
|
|
781
|
-
point(bbox.minX, bbox.maxY)
|
|
836
|
+
point(bbox.minX, bbox.maxY),
|
|
782
837
|
];
|
|
783
838
|
}
|
|
784
839
|
|
|
@@ -801,7 +856,7 @@ export function bboxIntersectsViewBox(bbox, viewBox) {
|
|
|
801
856
|
minX: viewBox.x,
|
|
802
857
|
minY: viewBox.y,
|
|
803
858
|
maxX: viewBox.x.plus(viewBox.width),
|
|
804
|
-
maxY: viewBox.y.plus(viewBox.height)
|
|
859
|
+
maxY: viewBox.y.plus(viewBox.height),
|
|
805
860
|
});
|
|
806
861
|
|
|
807
862
|
// Use GJK algorithm
|
|
@@ -809,7 +864,7 @@ export function bboxIntersectsViewBox(bbox, viewBox) {
|
|
|
809
864
|
|
|
810
865
|
return {
|
|
811
866
|
intersects: result.overlaps,
|
|
812
|
-
verified: result.verified
|
|
867
|
+
verified: result.verified,
|
|
813
868
|
};
|
|
814
869
|
}
|
|
815
870
|
|
|
@@ -840,7 +895,7 @@ export function isPathOffCanvas(pathCommands, viewBox) {
|
|
|
840
895
|
return {
|
|
841
896
|
offCanvas: false,
|
|
842
897
|
bbox,
|
|
843
|
-
verified: intersection.verified && bbox.verified
|
|
898
|
+
verified: intersection.verified && bbox.verified,
|
|
844
899
|
};
|
|
845
900
|
}
|
|
846
901
|
|
|
@@ -850,13 +905,13 @@ export function isPathOffCanvas(pathCommands, viewBox) {
|
|
|
850
905
|
minX: viewBox.x,
|
|
851
906
|
minY: viewBox.y,
|
|
852
907
|
maxX: viewBox.x.plus(viewBox.width),
|
|
853
|
-
maxY: viewBox.y.plus(viewBox.height)
|
|
908
|
+
maxY: viewBox.y.plus(viewBox.height),
|
|
854
909
|
};
|
|
855
910
|
|
|
856
911
|
// Sample a few points from the path to verify
|
|
857
912
|
let verified = true;
|
|
858
|
-
let
|
|
859
|
-
let
|
|
913
|
+
let _currentX = D(0);
|
|
914
|
+
let _currentY = D(0);
|
|
860
915
|
let sampleCount = 0;
|
|
861
916
|
const maxSamples = 10; // Limit verification samples
|
|
862
917
|
|
|
@@ -864,15 +919,15 @@ export function isPathOffCanvas(pathCommands, viewBox) {
|
|
|
864
919
|
if (sampleCount >= maxSamples) break;
|
|
865
920
|
|
|
866
921
|
const type = cmd.type.toUpperCase();
|
|
867
|
-
if (type ===
|
|
922
|
+
if (type === "M" || type === "L") {
|
|
868
923
|
const x = D(cmd.x);
|
|
869
924
|
const y = D(cmd.y);
|
|
870
925
|
if (pointInBBox({ x, y }, viewBoxBBox)) {
|
|
871
926
|
verified = false;
|
|
872
927
|
break;
|
|
873
928
|
}
|
|
874
|
-
|
|
875
|
-
|
|
929
|
+
_currentX = x;
|
|
930
|
+
_currentY = y;
|
|
876
931
|
sampleCount++;
|
|
877
932
|
}
|
|
878
933
|
}
|
|
@@ -880,7 +935,7 @@ export function isPathOffCanvas(pathCommands, viewBox) {
|
|
|
880
935
|
return {
|
|
881
936
|
offCanvas: true,
|
|
882
937
|
bbox,
|
|
883
|
-
verified: verified && bbox.verified
|
|
938
|
+
verified: verified && bbox.verified,
|
|
884
939
|
};
|
|
885
940
|
}
|
|
886
941
|
|
|
@@ -907,7 +962,7 @@ export function isShapeOffCanvas(shape, viewBox) {
|
|
|
907
962
|
return {
|
|
908
963
|
offCanvas: false,
|
|
909
964
|
bbox,
|
|
910
|
-
verified: intersection.verified && bbox.verified
|
|
965
|
+
verified: intersection.verified && bbox.verified,
|
|
911
966
|
};
|
|
912
967
|
}
|
|
913
968
|
|
|
@@ -915,7 +970,7 @@ export function isShapeOffCanvas(shape, viewBox) {
|
|
|
915
970
|
return {
|
|
916
971
|
offCanvas: true,
|
|
917
972
|
bbox,
|
|
918
|
-
verified: intersection.verified && bbox.verified
|
|
973
|
+
verified: intersection.verified && bbox.verified,
|
|
919
974
|
};
|
|
920
975
|
}
|
|
921
976
|
|
|
@@ -934,10 +989,10 @@ export function isShapeOffCanvas(shape, viewBox) {
|
|
|
934
989
|
function clipLine(p1, p2, bounds) {
|
|
935
990
|
// Cohen-Sutherland outcodes
|
|
936
991
|
const INSIDE = 0; // 0000
|
|
937
|
-
const LEFT = 1;
|
|
938
|
-
const RIGHT = 2;
|
|
992
|
+
const LEFT = 1; // 0001
|
|
993
|
+
const RIGHT = 2; // 0010
|
|
939
994
|
const BOTTOM = 4; // 0100
|
|
940
|
-
const TOP = 8;
|
|
995
|
+
const TOP = 8; // 1000
|
|
941
996
|
|
|
942
997
|
const computeOutcode = (x, y) => {
|
|
943
998
|
let code = INSIDE;
|
|
@@ -948,8 +1003,10 @@ function clipLine(p1, p2, bounds) {
|
|
|
948
1003
|
return code;
|
|
949
1004
|
};
|
|
950
1005
|
|
|
951
|
-
let x1 = p1.x,
|
|
952
|
-
|
|
1006
|
+
let x1 = p1.x,
|
|
1007
|
+
y1 = p1.y;
|
|
1008
|
+
let x2 = p2.x,
|
|
1009
|
+
y2 = p2.y;
|
|
953
1010
|
let outcode1 = computeOutcode(x1, y1);
|
|
954
1011
|
let outcode2 = computeOutcode(x2, y2);
|
|
955
1012
|
|
|
@@ -962,7 +1019,10 @@ function clipLine(p1, p2, bounds) {
|
|
|
962
1019
|
while (true) {
|
|
963
1020
|
if ((outcode1 | outcode2) === 0) {
|
|
964
1021
|
// Both points inside - accept
|
|
965
|
-
return [
|
|
1022
|
+
return [
|
|
1023
|
+
{ x: x1, y: y1 },
|
|
1024
|
+
{ x: x2, y: y2 },
|
|
1025
|
+
];
|
|
966
1026
|
} else if ((outcode1 & outcode2) !== 0) {
|
|
967
1027
|
// Both points outside same edge - reject
|
|
968
1028
|
return [];
|
|
@@ -1010,7 +1070,8 @@ function clipLine(p1, p2, bounds) {
|
|
|
1010
1070
|
} else if ((outcodeOut & RIGHT) !== 0) {
|
|
1011
1071
|
y = y1.plus(dy.mul(bounds.maxX.minus(x1)).div(dx));
|
|
1012
1072
|
x = bounds.maxX;
|
|
1013
|
-
} else {
|
|
1073
|
+
} else {
|
|
1074
|
+
// LEFT
|
|
1014
1075
|
y = y1.plus(dy.mul(bounds.minX.minus(x1)).div(dx));
|
|
1015
1076
|
x = bounds.minX;
|
|
1016
1077
|
}
|
|
@@ -1046,7 +1107,7 @@ export function clipPathToViewBox(pathCommands, viewBox) {
|
|
|
1046
1107
|
minX: viewBox.x,
|
|
1047
1108
|
minY: viewBox.y,
|
|
1048
1109
|
maxX: viewBox.x.plus(viewBox.width),
|
|
1049
|
-
maxY: viewBox.y.plus(viewBox.height)
|
|
1110
|
+
maxY: viewBox.y.plus(viewBox.height),
|
|
1050
1111
|
};
|
|
1051
1112
|
|
|
1052
1113
|
const clippedCommands = [];
|
|
@@ -1058,14 +1119,14 @@ export function clipPathToViewBox(pathCommands, viewBox) {
|
|
|
1058
1119
|
const type = cmd.type.toUpperCase();
|
|
1059
1120
|
|
|
1060
1121
|
switch (type) {
|
|
1061
|
-
case
|
|
1122
|
+
case "M": // Move
|
|
1062
1123
|
{
|
|
1063
1124
|
const x = D(cmd.x);
|
|
1064
1125
|
const y = D(cmd.y);
|
|
1065
1126
|
|
|
1066
1127
|
// Only add move if point is inside bounds
|
|
1067
1128
|
if (pointInBBox({ x, y }, bounds)) {
|
|
1068
|
-
clippedCommands.push({ type:
|
|
1129
|
+
clippedCommands.push({ type: "M", x, y });
|
|
1069
1130
|
pathStarted = true;
|
|
1070
1131
|
} else {
|
|
1071
1132
|
pathStarted = false;
|
|
@@ -1076,21 +1137,33 @@ export function clipPathToViewBox(pathCommands, viewBox) {
|
|
|
1076
1137
|
}
|
|
1077
1138
|
break;
|
|
1078
1139
|
|
|
1079
|
-
case
|
|
1140
|
+
case "L": // Line to
|
|
1080
1141
|
{
|
|
1081
1142
|
const x = D(cmd.x);
|
|
1082
1143
|
const y = D(cmd.y);
|
|
1083
1144
|
|
|
1084
1145
|
// Clip line segment
|
|
1085
|
-
const clipped = clipLine(
|
|
1146
|
+
const clipped = clipLine(
|
|
1147
|
+
{ x: currentX, y: currentY },
|
|
1148
|
+
{ x, y },
|
|
1149
|
+
bounds,
|
|
1150
|
+
);
|
|
1086
1151
|
|
|
1087
1152
|
if (clipped.length === 2) {
|
|
1088
1153
|
// Line segment visible after clipping
|
|
1089
1154
|
if (!pathStarted) {
|
|
1090
|
-
clippedCommands.push({
|
|
1155
|
+
clippedCommands.push({
|
|
1156
|
+
type: "M",
|
|
1157
|
+
x: clipped[0].x,
|
|
1158
|
+
y: clipped[0].y,
|
|
1159
|
+
});
|
|
1091
1160
|
pathStarted = true;
|
|
1092
1161
|
}
|
|
1093
|
-
clippedCommands.push({
|
|
1162
|
+
clippedCommands.push({
|
|
1163
|
+
type: "L",
|
|
1164
|
+
x: clipped[1].x,
|
|
1165
|
+
y: clipped[1].y,
|
|
1166
|
+
});
|
|
1094
1167
|
} else {
|
|
1095
1168
|
// Line segment completely clipped
|
|
1096
1169
|
pathStarted = false;
|
|
@@ -1101,17 +1174,29 @@ export function clipPathToViewBox(pathCommands, viewBox) {
|
|
|
1101
1174
|
}
|
|
1102
1175
|
break;
|
|
1103
1176
|
|
|
1104
|
-
case
|
|
1177
|
+
case "H": // Horizontal line
|
|
1105
1178
|
{
|
|
1106
1179
|
const x = D(cmd.x);
|
|
1107
|
-
const clipped = clipLine(
|
|
1180
|
+
const clipped = clipLine(
|
|
1181
|
+
{ x: currentX, y: currentY },
|
|
1182
|
+
{ x, y: currentY },
|
|
1183
|
+
bounds,
|
|
1184
|
+
);
|
|
1108
1185
|
|
|
1109
1186
|
if (clipped.length === 2) {
|
|
1110
1187
|
if (!pathStarted) {
|
|
1111
|
-
clippedCommands.push({
|
|
1188
|
+
clippedCommands.push({
|
|
1189
|
+
type: "M",
|
|
1190
|
+
x: clipped[0].x,
|
|
1191
|
+
y: clipped[0].y,
|
|
1192
|
+
});
|
|
1112
1193
|
pathStarted = true;
|
|
1113
1194
|
}
|
|
1114
|
-
clippedCommands.push({
|
|
1195
|
+
clippedCommands.push({
|
|
1196
|
+
type: "L",
|
|
1197
|
+
x: clipped[1].x,
|
|
1198
|
+
y: clipped[1].y,
|
|
1199
|
+
});
|
|
1115
1200
|
} else {
|
|
1116
1201
|
pathStarted = false;
|
|
1117
1202
|
}
|
|
@@ -1120,17 +1205,29 @@ export function clipPathToViewBox(pathCommands, viewBox) {
|
|
|
1120
1205
|
}
|
|
1121
1206
|
break;
|
|
1122
1207
|
|
|
1123
|
-
case
|
|
1208
|
+
case "V": // Vertical line
|
|
1124
1209
|
{
|
|
1125
1210
|
const y = D(cmd.y);
|
|
1126
|
-
const clipped = clipLine(
|
|
1211
|
+
const clipped = clipLine(
|
|
1212
|
+
{ x: currentX, y: currentY },
|
|
1213
|
+
{ x: currentX, y },
|
|
1214
|
+
bounds,
|
|
1215
|
+
);
|
|
1127
1216
|
|
|
1128
1217
|
if (clipped.length === 2) {
|
|
1129
1218
|
if (!pathStarted) {
|
|
1130
|
-
clippedCommands.push({
|
|
1219
|
+
clippedCommands.push({
|
|
1220
|
+
type: "M",
|
|
1221
|
+
x: clipped[0].x,
|
|
1222
|
+
y: clipped[0].y,
|
|
1223
|
+
});
|
|
1131
1224
|
pathStarted = true;
|
|
1132
1225
|
}
|
|
1133
|
-
clippedCommands.push({
|
|
1226
|
+
clippedCommands.push({
|
|
1227
|
+
type: "L",
|
|
1228
|
+
x: clipped[1].x,
|
|
1229
|
+
y: clipped[1].y,
|
|
1230
|
+
});
|
|
1134
1231
|
} else {
|
|
1135
1232
|
pathStarted = false;
|
|
1136
1233
|
}
|
|
@@ -1139,11 +1236,11 @@ export function clipPathToViewBox(pathCommands, viewBox) {
|
|
|
1139
1236
|
}
|
|
1140
1237
|
break;
|
|
1141
1238
|
|
|
1142
|
-
case
|
|
1143
|
-
case
|
|
1144
|
-
case
|
|
1145
|
-
case
|
|
1146
|
-
case
|
|
1239
|
+
case "C": // Cubic Bezier - sample as polyline
|
|
1240
|
+
case "S": // Smooth cubic - sample as polyline
|
|
1241
|
+
case "Q": // Quadratic - sample as polyline
|
|
1242
|
+
case "T": // Smooth quadratic - sample as polyline
|
|
1243
|
+
case "A": // Arc - sample as polyline
|
|
1147
1244
|
{
|
|
1148
1245
|
// For simplicity, just include the endpoint
|
|
1149
1246
|
// A full implementation would sample the curve
|
|
@@ -1152,10 +1249,10 @@ export function clipPathToViewBox(pathCommands, viewBox) {
|
|
|
1152
1249
|
|
|
1153
1250
|
if (pointInBBox({ x, y }, bounds)) {
|
|
1154
1251
|
if (!pathStarted) {
|
|
1155
|
-
clippedCommands.push({ type:
|
|
1252
|
+
clippedCommands.push({ type: "M", x, y });
|
|
1156
1253
|
pathStarted = true;
|
|
1157
1254
|
} else {
|
|
1158
|
-
clippedCommands.push({ type:
|
|
1255
|
+
clippedCommands.push({ type: "L", x, y });
|
|
1159
1256
|
}
|
|
1160
1257
|
} else {
|
|
1161
1258
|
pathStarted = false;
|
|
@@ -1166,9 +1263,9 @@ export function clipPathToViewBox(pathCommands, viewBox) {
|
|
|
1166
1263
|
}
|
|
1167
1264
|
break;
|
|
1168
1265
|
|
|
1169
|
-
case
|
|
1266
|
+
case "Z": // Close path
|
|
1170
1267
|
if (pathStarted) {
|
|
1171
|
-
clippedCommands.push({ type:
|
|
1268
|
+
clippedCommands.push({ type: "Z" });
|
|
1172
1269
|
}
|
|
1173
1270
|
pathStarted = false;
|
|
1174
1271
|
break;
|
|
@@ -1182,7 +1279,7 @@ export function clipPathToViewBox(pathCommands, viewBox) {
|
|
|
1182
1279
|
// VERIFICATION: Check that all points are within bounds
|
|
1183
1280
|
let verified = true;
|
|
1184
1281
|
for (const cmd of clippedCommands) {
|
|
1185
|
-
if (cmd.type ===
|
|
1282
|
+
if (cmd.type === "M" || cmd.type === "L") {
|
|
1186
1283
|
if (!pointInBBox({ x: cmd.x, y: cmd.y }, bounds)) {
|
|
1187
1284
|
verified = false;
|
|
1188
1285
|
break;
|
|
@@ -1218,5 +1315,5 @@ export default {
|
|
|
1218
1315
|
// Constants
|
|
1219
1316
|
EPSILON,
|
|
1220
1317
|
DEFAULT_TOLERANCE,
|
|
1221
|
-
VERIFICATION_SAMPLES
|
|
1318
|
+
VERIFICATION_SAMPLES,
|
|
1222
1319
|
};
|