@emasoft/svg-matrix 1.0.30 → 1.0.31
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/bin/svg-matrix.js +310 -61
- package/bin/svglinter.cjs +102 -3
- package/bin/svgm.js +236 -27
- package/package.json +1 -1
- package/src/animation-optimization.js +137 -17
- package/src/animation-references.js +123 -6
- package/src/arc-length.js +213 -4
- package/src/bezier-analysis.js +217 -21
- package/src/bezier-intersections.js +275 -12
- package/src/browser-verify.js +237 -4
- package/src/clip-path-resolver.js +168 -0
- package/src/convert-path-data.js +479 -28
- package/src/css-specificity.js +73 -10
- package/src/douglas-peucker.js +219 -2
- package/src/flatten-pipeline.js +284 -26
- package/src/geometry-to-path.js +250 -25
- package/src/gjk-collision.js +236 -33
- package/src/index.js +261 -3
- package/src/inkscape-support.js +86 -28
- package/src/logger.js +48 -3
- package/src/marker-resolver.js +278 -74
- package/src/mask-resolver.js +265 -66
- package/src/matrix.js +44 -5
- package/src/mesh-gradient.js +352 -102
- package/src/off-canvas-detection.js +382 -13
- package/src/path-analysis.js +192 -18
- package/src/path-data-plugins.js +309 -5
- package/src/path-optimization.js +129 -5
- package/src/path-simplification.js +188 -32
- package/src/pattern-resolver.js +454 -106
- package/src/polygon-clip.js +324 -1
- package/src/svg-boolean-ops.js +226 -9
- package/src/svg-collections.js +7 -5
- package/src/svg-flatten.js +386 -62
- package/src/svg-parser.js +179 -8
- package/src/svg-rendering-context.js +235 -6
- package/src/svg-toolbox.js +45 -8
- package/src/svg2-polyfills.js +40 -10
- package/src/transform-decomposition.js +258 -32
- package/src/transform-optimization.js +259 -13
- package/src/transforms2d.js +82 -9
- package/src/transforms3d.js +62 -10
- package/src/use-symbol-resolver.js +286 -42
- package/src/vector.js +64 -8
- package/src/verification.js +392 -1
|
@@ -193,15 +193,41 @@ export function parseViewBox(viewBoxString) {
|
|
|
193
193
|
* @param {Decimal} maxX - Maximum X coordinate
|
|
194
194
|
* @param {Decimal} maxY - Maximum Y coordinate
|
|
195
195
|
* @returns {{minX: Decimal, minY: Decimal, maxX: Decimal, maxY: Decimal, width: Decimal, height: Decimal}}
|
|
196
|
+
* @throws {Error} If parameters are invalid or bounds are inverted
|
|
196
197
|
*/
|
|
197
198
|
function createBBox(minX, minY, maxX, maxY) {
|
|
199
|
+
// Validate all parameters exist and are Decimal-convertible (WHY: prevent null/undefined crashes)
|
|
200
|
+
if (minX == null || minY == null || maxX == null || maxY == null) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
"createBBox: all parameters (minX, minY, maxX, maxY) must be provided",
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Convert to Decimal (WHY: ensure consistent type)
|
|
207
|
+
const dMinX = D(minX);
|
|
208
|
+
const dMinY = D(minY);
|
|
209
|
+
const dMaxX = D(maxX);
|
|
210
|
+
const dMaxY = D(maxY);
|
|
211
|
+
|
|
212
|
+
// Validate bounds are not inverted (WHY: catch logic errors early)
|
|
213
|
+
if (dMaxX.lessThan(dMinX)) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`createBBox: maxX (${dMaxX}) must be >= minX (${dMinX})`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (dMaxY.lessThan(dMinY)) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
`createBBox: maxY (${dMaxY}) must be >= minY (${dMinY})`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
198
224
|
return {
|
|
199
|
-
minX,
|
|
200
|
-
minY,
|
|
201
|
-
maxX,
|
|
202
|
-
maxY,
|
|
203
|
-
width:
|
|
204
|
-
height:
|
|
225
|
+
minX: dMinX,
|
|
226
|
+
minY: dMinY,
|
|
227
|
+
maxX: dMaxX,
|
|
228
|
+
maxY: dMaxY,
|
|
229
|
+
width: dMaxX.minus(dMinX),
|
|
230
|
+
height: dMaxY.minus(dMinY),
|
|
205
231
|
};
|
|
206
232
|
}
|
|
207
233
|
|
|
@@ -212,13 +238,34 @@ function createBBox(minX, minY, maxX, maxY) {
|
|
|
212
238
|
* @param {Decimal} x - Point X coordinate
|
|
213
239
|
* @param {Decimal} y - Point Y coordinate
|
|
214
240
|
* @returns {{minX: Decimal, minY: Decimal, maxX: Decimal, maxY: Decimal}}
|
|
241
|
+
* @throws {Error} If bbox or coordinates are invalid
|
|
215
242
|
*/
|
|
216
243
|
function _expandBBox(bbox, x, y) {
|
|
244
|
+
// Validate bbox parameter (WHY: prevent null dereference)
|
|
245
|
+
if (!bbox || typeof bbox !== "object") {
|
|
246
|
+
throw new Error("_expandBBox: bbox must be a bounding box object");
|
|
247
|
+
}
|
|
248
|
+
if (
|
|
249
|
+
bbox.minX == null ||
|
|
250
|
+
bbox.minY == null ||
|
|
251
|
+
bbox.maxX == null ||
|
|
252
|
+
bbox.maxY == null
|
|
253
|
+
) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
"_expandBBox: bbox must have minX, minY, maxX, maxY properties",
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Validate coordinates (WHY: prevent null/undefined crashes)
|
|
260
|
+
if (x == null || y == null) {
|
|
261
|
+
throw new Error("_expandBBox: x and y coordinates must be provided");
|
|
262
|
+
}
|
|
263
|
+
|
|
217
264
|
return {
|
|
218
|
-
minX: Decimal.min(bbox.minX, x),
|
|
219
|
-
minY: Decimal.min(bbox.minY, y),
|
|
220
|
-
maxX: Decimal.max(bbox.maxX, x),
|
|
221
|
-
maxY: Decimal.max(bbox.maxY, y),
|
|
265
|
+
minX: Decimal.min(bbox.minX, D(x)),
|
|
266
|
+
minY: Decimal.min(bbox.minY, D(y)),
|
|
267
|
+
maxX: Decimal.max(bbox.maxX, D(x)),
|
|
268
|
+
maxY: Decimal.max(bbox.maxY, D(y)),
|
|
222
269
|
};
|
|
223
270
|
}
|
|
224
271
|
|
|
@@ -235,8 +282,30 @@ function _expandBBox(bbox, x, y) {
|
|
|
235
282
|
* @param {Decimal} y3 - End Y
|
|
236
283
|
* @param {number} samples - Number of samples
|
|
237
284
|
* @returns {Array<{x: Decimal, y: Decimal}>} Sample points
|
|
285
|
+
* @throws {Error} If parameters are invalid or samples is zero/negative
|
|
238
286
|
*/
|
|
239
287
|
function sampleCubicBezier(x0, y0, x1, y1, x2, y2, x3, y3, samples = 20) {
|
|
288
|
+
// Validate all parameters exist (WHY: prevent null/undefined crashes)
|
|
289
|
+
if (
|
|
290
|
+
x0 == null ||
|
|
291
|
+
y0 == null ||
|
|
292
|
+
x1 == null ||
|
|
293
|
+
y1 == null ||
|
|
294
|
+
x2 == null ||
|
|
295
|
+
y2 == null ||
|
|
296
|
+
x3 == null ||
|
|
297
|
+
y3 == null
|
|
298
|
+
) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
"sampleCubicBezier: all coordinate parameters must be provided",
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Validate samples parameter (WHY: prevent division by zero)
|
|
305
|
+
if (samples == null || samples <= 0) {
|
|
306
|
+
throw new Error("sampleCubicBezier: samples must be a positive number");
|
|
307
|
+
}
|
|
308
|
+
|
|
240
309
|
const points = [];
|
|
241
310
|
for (let i = 0; i <= samples; i++) {
|
|
242
311
|
const t = D(i).div(samples);
|
|
@@ -276,8 +345,28 @@ function sampleCubicBezier(x0, y0, x1, y1, x2, y2, x3, y3, samples = 20) {
|
|
|
276
345
|
* @param {Decimal} y2 - End Y
|
|
277
346
|
* @param {number} samples - Number of samples
|
|
278
347
|
* @returns {Array<{x: Decimal, y: Decimal}>} Sample points
|
|
348
|
+
* @throws {Error} If parameters are invalid or samples is zero/negative
|
|
279
349
|
*/
|
|
280
350
|
function sampleQuadraticBezier(x0, y0, x1, y1, x2, y2, samples = 20) {
|
|
351
|
+
// Validate all parameters exist (WHY: prevent null/undefined crashes)
|
|
352
|
+
if (
|
|
353
|
+
x0 == null ||
|
|
354
|
+
y0 == null ||
|
|
355
|
+
x1 == null ||
|
|
356
|
+
y1 == null ||
|
|
357
|
+
x2 == null ||
|
|
358
|
+
y2 == null
|
|
359
|
+
) {
|
|
360
|
+
throw new Error(
|
|
361
|
+
"sampleQuadraticBezier: all coordinate parameters must be provided",
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Validate samples parameter (WHY: prevent division by zero)
|
|
366
|
+
if (samples == null || samples <= 0) {
|
|
367
|
+
throw new Error("sampleQuadraticBezier: samples must be a positive number");
|
|
368
|
+
}
|
|
369
|
+
|
|
281
370
|
const points = [];
|
|
282
371
|
for (let i = 0; i <= samples; i++) {
|
|
283
372
|
const t = D(i).div(samples);
|
|
@@ -309,8 +398,29 @@ function sampleQuadraticBezier(x0, y0, x1, y1, x2, y2, samples = 20) {
|
|
|
309
398
|
* @param {{minX: Decimal, minY: Decimal, maxX: Decimal, maxY: Decimal}} bbox - Bounding box
|
|
310
399
|
* @param {Decimal} tolerance - Tolerance for boundary checks
|
|
311
400
|
* @returns {boolean} True if point is inside or on boundary
|
|
401
|
+
* @throws {Error} If pt or bbox are invalid
|
|
312
402
|
*/
|
|
313
403
|
function pointInBBox(pt, bbox, tolerance = DEFAULT_TOLERANCE) {
|
|
404
|
+
// Validate point parameter (WHY: prevent null dereference)
|
|
405
|
+
if (!pt || typeof pt !== "object" || pt.x == null || pt.y == null) {
|
|
406
|
+
throw new Error("pointInBBox: pt must be an object with x and y properties");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Validate bbox parameter (WHY: prevent null dereference)
|
|
410
|
+
if (!bbox || typeof bbox !== "object") {
|
|
411
|
+
throw new Error("pointInBBox: bbox must be a bounding box object");
|
|
412
|
+
}
|
|
413
|
+
if (
|
|
414
|
+
bbox.minX == null ||
|
|
415
|
+
bbox.minY == null ||
|
|
416
|
+
bbox.maxX == null ||
|
|
417
|
+
bbox.maxY == null
|
|
418
|
+
) {
|
|
419
|
+
throw new Error(
|
|
420
|
+
"pointInBBox: bbox must have minX, minY, maxX, maxY properties",
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
314
424
|
return (
|
|
315
425
|
pt.x.greaterThanOrEqualTo(bbox.minX.minus(tolerance)) &&
|
|
316
426
|
pt.x.lessThanOrEqualTo(bbox.maxX.plus(tolerance)) &&
|
|
@@ -358,6 +468,13 @@ export function pathBoundingBox(pathCommands) {
|
|
|
358
468
|
const samplePoints = [];
|
|
359
469
|
|
|
360
470
|
for (const cmd of pathCommands) {
|
|
471
|
+
// Validate command object and type property (WHY: prevent null dereference and type errors)
|
|
472
|
+
if (!cmd || typeof cmd !== "object" || !cmd.type || typeof cmd.type !== "string") {
|
|
473
|
+
throw new Error(
|
|
474
|
+
"pathBoundingBox: each command must be an object with a string type property",
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
361
478
|
const type = cmd.type.toUpperCase();
|
|
362
479
|
// BUG 1 FIX: Check if command is relative (lowercase) or absolute (uppercase)
|
|
363
480
|
const isRelative = cmd.type === cmd.type.toLowerCase();
|
|
@@ -365,6 +482,11 @@ export function pathBoundingBox(pathCommands) {
|
|
|
365
482
|
switch (type) {
|
|
366
483
|
case "M": // Move
|
|
367
484
|
{
|
|
485
|
+
// Validate required properties (WHY: prevent accessing undefined properties)
|
|
486
|
+
if (cmd.x == null || cmd.y == null) {
|
|
487
|
+
throw new Error("pathBoundingBox: M command requires x and y properties");
|
|
488
|
+
}
|
|
489
|
+
|
|
368
490
|
// BUG 1 FIX: Handle relative coordinates
|
|
369
491
|
const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
|
|
370
492
|
const y = isRelative ? currentY.plus(D(cmd.y)) : D(cmd.y);
|
|
@@ -387,6 +509,11 @@ export function pathBoundingBox(pathCommands) {
|
|
|
387
509
|
|
|
388
510
|
case "L": // Line to
|
|
389
511
|
{
|
|
512
|
+
// Validate required properties (WHY: prevent accessing undefined properties)
|
|
513
|
+
if (cmd.x == null || cmd.y == null) {
|
|
514
|
+
throw new Error("pathBoundingBox: L command requires x and y properties");
|
|
515
|
+
}
|
|
516
|
+
|
|
390
517
|
// BUG 1 FIX: Handle relative coordinates
|
|
391
518
|
const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
|
|
392
519
|
const y = isRelative ? currentY.plus(D(cmd.y)) : D(cmd.y);
|
|
@@ -407,6 +534,11 @@ export function pathBoundingBox(pathCommands) {
|
|
|
407
534
|
|
|
408
535
|
case "H": // Horizontal line
|
|
409
536
|
{
|
|
537
|
+
// Validate required properties (WHY: prevent accessing undefined properties)
|
|
538
|
+
if (cmd.x == null) {
|
|
539
|
+
throw new Error("pathBoundingBox: H command requires x property");
|
|
540
|
+
}
|
|
541
|
+
|
|
410
542
|
// BUG 1 FIX: Handle relative coordinates
|
|
411
543
|
const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
|
|
412
544
|
|
|
@@ -423,6 +555,11 @@ export function pathBoundingBox(pathCommands) {
|
|
|
423
555
|
|
|
424
556
|
case "V": // Vertical line
|
|
425
557
|
{
|
|
558
|
+
// Validate required properties (WHY: prevent accessing undefined properties)
|
|
559
|
+
if (cmd.y == null) {
|
|
560
|
+
throw new Error("pathBoundingBox: V command requires y property");
|
|
561
|
+
}
|
|
562
|
+
|
|
426
563
|
// BUG 1 FIX: Handle relative coordinates
|
|
427
564
|
const y = isRelative ? currentY.plus(D(cmd.y)) : D(cmd.y);
|
|
428
565
|
|
|
@@ -439,6 +576,20 @@ export function pathBoundingBox(pathCommands) {
|
|
|
439
576
|
|
|
440
577
|
case "C": // Cubic Bezier
|
|
441
578
|
{
|
|
579
|
+
// Validate required properties (WHY: prevent accessing undefined properties)
|
|
580
|
+
if (
|
|
581
|
+
cmd.x1 == null ||
|
|
582
|
+
cmd.y1 == null ||
|
|
583
|
+
cmd.x2 == null ||
|
|
584
|
+
cmd.y2 == null ||
|
|
585
|
+
cmd.x == null ||
|
|
586
|
+
cmd.y == null
|
|
587
|
+
) {
|
|
588
|
+
throw new Error(
|
|
589
|
+
"pathBoundingBox: C command requires x1, y1, x2, y2, x, y properties",
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
442
593
|
// BUG 1 FIX: Handle relative coordinates
|
|
443
594
|
const x1 = isRelative ? currentX.plus(D(cmd.x1)) : D(cmd.x1);
|
|
444
595
|
const y1 = isRelative ? currentY.plus(D(cmd.y1)) : D(cmd.y1);
|
|
@@ -477,6 +628,13 @@ export function pathBoundingBox(pathCommands) {
|
|
|
477
628
|
|
|
478
629
|
case "S": // Smooth cubic Bezier
|
|
479
630
|
{
|
|
631
|
+
// Validate required properties (WHY: prevent accessing undefined properties)
|
|
632
|
+
if (cmd.x2 == null || cmd.y2 == null || cmd.x == null || cmd.y == null) {
|
|
633
|
+
throw new Error(
|
|
634
|
+
"pathBoundingBox: S command requires x2, y2, x, y properties",
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
480
638
|
// BUG 1 FIX: Handle relative coordinates
|
|
481
639
|
const x2 = isRelative ? currentX.plus(D(cmd.x2)) : D(cmd.x2);
|
|
482
640
|
const y2 = isRelative ? currentY.plus(D(cmd.y2)) : D(cmd.y2);
|
|
@@ -523,6 +681,13 @@ export function pathBoundingBox(pathCommands) {
|
|
|
523
681
|
|
|
524
682
|
case "Q": // Quadratic Bezier
|
|
525
683
|
{
|
|
684
|
+
// Validate required properties (WHY: prevent accessing undefined properties)
|
|
685
|
+
if (cmd.x1 == null || cmd.y1 == null || cmd.x == null || cmd.y == null) {
|
|
686
|
+
throw new Error(
|
|
687
|
+
"pathBoundingBox: Q command requires x1, y1, x, y properties",
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
526
691
|
// BUG 1 FIX: Handle relative coordinates
|
|
527
692
|
const x1 = isRelative ? currentX.plus(D(cmd.x1)) : D(cmd.x1);
|
|
528
693
|
const y1 = isRelative ? currentY.plus(D(cmd.y1)) : D(cmd.y1);
|
|
@@ -556,6 +721,11 @@ export function pathBoundingBox(pathCommands) {
|
|
|
556
721
|
|
|
557
722
|
case "T": // Smooth quadratic Bezier
|
|
558
723
|
{
|
|
724
|
+
// Validate required properties (WHY: prevent accessing undefined properties)
|
|
725
|
+
if (cmd.x == null || cmd.y == null) {
|
|
726
|
+
throw new Error("pathBoundingBox: T command requires x and y properties");
|
|
727
|
+
}
|
|
728
|
+
|
|
559
729
|
// BUG 1 FIX: Handle relative coordinates
|
|
560
730
|
const x = isRelative ? currentX.plus(D(cmd.x)) : D(cmd.x);
|
|
561
731
|
const y = isRelative ? currentY.plus(D(cmd.y)) : D(cmd.y);
|
|
@@ -598,6 +768,11 @@ export function pathBoundingBox(pathCommands) {
|
|
|
598
768
|
|
|
599
769
|
case "A": // Arc (approximate with samples)
|
|
600
770
|
{
|
|
771
|
+
// Validate required properties (WHY: prevent accessing undefined properties)
|
|
772
|
+
if (cmd.x == null || cmd.y == null) {
|
|
773
|
+
throw new Error("pathBoundingBox: A command requires x and y properties");
|
|
774
|
+
}
|
|
775
|
+
|
|
601
776
|
// BUG 4: Arc bounding box ignores actual arc geometry
|
|
602
777
|
// TODO: Implement proper arc-to-bezier conversion or calculate arc extrema
|
|
603
778
|
// Current implementation only samples linearly between endpoints, which
|
|
@@ -698,6 +873,18 @@ export function shapeBoundingBox(shape) {
|
|
|
698
873
|
switch (type) {
|
|
699
874
|
case "rect":
|
|
700
875
|
{
|
|
876
|
+
// Validate required properties (WHY: prevent accessing undefined properties)
|
|
877
|
+
if (
|
|
878
|
+
shape.x == null ||
|
|
879
|
+
shape.y == null ||
|
|
880
|
+
shape.width == null ||
|
|
881
|
+
shape.height == null
|
|
882
|
+
) {
|
|
883
|
+
throw new Error(
|
|
884
|
+
"shapeBoundingBox: rect requires x, y, width, height properties",
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
|
|
701
888
|
const x = D(shape.x);
|
|
702
889
|
const y = D(shape.y);
|
|
703
890
|
const width = D(shape.width);
|
|
@@ -717,6 +904,13 @@ export function shapeBoundingBox(shape) {
|
|
|
717
904
|
|
|
718
905
|
case "circle":
|
|
719
906
|
{
|
|
907
|
+
// Validate required properties (WHY: prevent accessing undefined properties)
|
|
908
|
+
if (shape.cx == null || shape.cy == null || shape.r == null) {
|
|
909
|
+
throw new Error(
|
|
910
|
+
"shapeBoundingBox: circle requires cx, cy, r properties",
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
|
|
720
914
|
const cx = D(shape.cx);
|
|
721
915
|
const cy = D(shape.cy);
|
|
722
916
|
const r = D(shape.r);
|
|
@@ -737,6 +931,18 @@ export function shapeBoundingBox(shape) {
|
|
|
737
931
|
|
|
738
932
|
case "ellipse":
|
|
739
933
|
{
|
|
934
|
+
// Validate required properties (WHY: prevent accessing undefined properties)
|
|
935
|
+
if (
|
|
936
|
+
shape.cx == null ||
|
|
937
|
+
shape.cy == null ||
|
|
938
|
+
shape.rx == null ||
|
|
939
|
+
shape.ry == null
|
|
940
|
+
) {
|
|
941
|
+
throw new Error(
|
|
942
|
+
"shapeBoundingBox: ellipse requires cx, cy, rx, ry properties",
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
|
|
740
946
|
const cx = D(shape.cx);
|
|
741
947
|
const cy = D(shape.cy);
|
|
742
948
|
const rx = D(shape.rx);
|
|
@@ -758,6 +964,18 @@ export function shapeBoundingBox(shape) {
|
|
|
758
964
|
|
|
759
965
|
case "line":
|
|
760
966
|
{
|
|
967
|
+
// Validate required properties (WHY: prevent accessing undefined properties)
|
|
968
|
+
if (
|
|
969
|
+
shape.x1 == null ||
|
|
970
|
+
shape.y1 == null ||
|
|
971
|
+
shape.x2 == null ||
|
|
972
|
+
shape.y2 == null
|
|
973
|
+
) {
|
|
974
|
+
throw new Error(
|
|
975
|
+
"shapeBoundingBox: line requires x1, y1, x2, y2 properties",
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
|
|
761
979
|
const x1 = D(shape.x1);
|
|
762
980
|
const y1 = D(shape.y1);
|
|
763
981
|
const x2 = D(shape.x2);
|
|
@@ -789,6 +1007,13 @@ export function shapeBoundingBox(shape) {
|
|
|
789
1007
|
let maxY = D(-Infinity);
|
|
790
1008
|
|
|
791
1009
|
for (const pt of shape.points) {
|
|
1010
|
+
// Validate point has x and y properties (WHY: prevent accessing undefined properties)
|
|
1011
|
+
if (!pt || typeof pt !== "object" || pt.x == null || pt.y == null) {
|
|
1012
|
+
throw new Error(
|
|
1013
|
+
`shapeBoundingBox: ${type} points must have x and y properties`,
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
792
1017
|
const x = D(pt.x);
|
|
793
1018
|
const y = D(pt.y);
|
|
794
1019
|
minX = Decimal.min(minX, x);
|
|
@@ -827,8 +1052,24 @@ export function shapeBoundingBox(shape) {
|
|
|
827
1052
|
*
|
|
828
1053
|
* @param {{minX: Decimal, minY: Decimal, maxX: Decimal, maxY: Decimal}} bbox - Bounding box
|
|
829
1054
|
* @returns {Array<{x: Decimal, y: Decimal}>} Polygon vertices (counter-clockwise)
|
|
1055
|
+
* @throws {Error} If bbox is invalid
|
|
830
1056
|
*/
|
|
831
1057
|
function bboxToPolygon(bbox) {
|
|
1058
|
+
// Validate bbox parameter (WHY: prevent null dereference)
|
|
1059
|
+
if (!bbox || typeof bbox !== "object") {
|
|
1060
|
+
throw new Error("bboxToPolygon: bbox must be a bounding box object");
|
|
1061
|
+
}
|
|
1062
|
+
if (
|
|
1063
|
+
bbox.minX == null ||
|
|
1064
|
+
bbox.minY == null ||
|
|
1065
|
+
bbox.maxX == null ||
|
|
1066
|
+
bbox.maxY == null
|
|
1067
|
+
) {
|
|
1068
|
+
throw new Error(
|
|
1069
|
+
"bboxToPolygon: bbox must have minX, minY, maxX, maxY properties",
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
832
1073
|
return [
|
|
833
1074
|
point(bbox.minX, bbox.minY),
|
|
834
1075
|
point(bbox.maxX, bbox.minY),
|
|
@@ -848,9 +1089,25 @@ function bboxToPolygon(bbox) {
|
|
|
848
1089
|
* @param {{minX: Decimal, minY: Decimal, maxX: Decimal, maxY: Decimal}} bbox - Bounding box to test
|
|
849
1090
|
* @param {{x: Decimal, y: Decimal, width: Decimal, height: Decimal}} viewBox - ViewBox object
|
|
850
1091
|
* @returns {{intersects: boolean, verified: boolean}}
|
|
1092
|
+
* @throws {Error} If bbox or viewBox are invalid
|
|
851
1093
|
*/
|
|
852
1094
|
export function bboxIntersectsViewBox(bbox, viewBox) {
|
|
853
|
-
//
|
|
1095
|
+
// Validate viewBox parameter (WHY: prevent null dereference)
|
|
1096
|
+
if (!viewBox || typeof viewBox !== "object") {
|
|
1097
|
+
throw new Error("bboxIntersectsViewBox: viewBox must be an object");
|
|
1098
|
+
}
|
|
1099
|
+
if (
|
|
1100
|
+
viewBox.x == null ||
|
|
1101
|
+
viewBox.y == null ||
|
|
1102
|
+
viewBox.width == null ||
|
|
1103
|
+
viewBox.height == null
|
|
1104
|
+
) {
|
|
1105
|
+
throw new Error(
|
|
1106
|
+
"bboxIntersectsViewBox: viewBox must have x, y, width, height properties",
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Convert both to polygons (bboxToPolygon validates bbox)
|
|
854
1111
|
const bboxPoly = bboxToPolygon(bbox);
|
|
855
1112
|
const viewBoxPoly = bboxToPolygon({
|
|
856
1113
|
minX: viewBox.x,
|
|
@@ -882,9 +1139,25 @@ export function bboxIntersectsViewBox(bbox, viewBox) {
|
|
|
882
1139
|
* @param {Array<Object>} pathCommands - Array of path command objects
|
|
883
1140
|
* @param {{x: Decimal, y: Decimal, width: Decimal, height: Decimal}} viewBox - ViewBox object
|
|
884
1141
|
* @returns {{offCanvas: boolean, bbox: Object, verified: boolean}}
|
|
1142
|
+
* @throws {Error} If parameters are invalid
|
|
885
1143
|
*/
|
|
886
1144
|
export function isPathOffCanvas(pathCommands, viewBox) {
|
|
887
|
-
//
|
|
1145
|
+
// Validate viewBox parameter (WHY: prevent null dereference)
|
|
1146
|
+
if (!viewBox || typeof viewBox !== "object") {
|
|
1147
|
+
throw new Error("isPathOffCanvas: viewBox must be an object");
|
|
1148
|
+
}
|
|
1149
|
+
if (
|
|
1150
|
+
viewBox.x == null ||
|
|
1151
|
+
viewBox.y == null ||
|
|
1152
|
+
viewBox.width == null ||
|
|
1153
|
+
viewBox.height == null
|
|
1154
|
+
) {
|
|
1155
|
+
throw new Error(
|
|
1156
|
+
"isPathOffCanvas: viewBox must have x, y, width, height properties",
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Calculate path bounding box (pathBoundingBox validates pathCommands)
|
|
888
1161
|
const bbox = pathBoundingBox(pathCommands);
|
|
889
1162
|
|
|
890
1163
|
// Check intersection
|
|
@@ -918,8 +1191,14 @@ export function isPathOffCanvas(pathCommands, viewBox) {
|
|
|
918
1191
|
for (const cmd of pathCommands) {
|
|
919
1192
|
if (sampleCount >= maxSamples) break;
|
|
920
1193
|
|
|
1194
|
+
// Validate command has type (WHY: prevent null dereference)
|
|
1195
|
+
if (!cmd || !cmd.type) continue;
|
|
1196
|
+
|
|
921
1197
|
const type = cmd.type.toUpperCase();
|
|
922
1198
|
if (type === "M" || type === "L") {
|
|
1199
|
+
// Skip if properties missing (WHY: avoid crashes during verification)
|
|
1200
|
+
if (cmd.x == null || cmd.y == null) continue;
|
|
1201
|
+
|
|
923
1202
|
const x = D(cmd.x);
|
|
924
1203
|
const y = D(cmd.y);
|
|
925
1204
|
if (pointInBBox({ x, y }, viewBoxBBox)) {
|
|
@@ -949,9 +1228,25 @@ export function isPathOffCanvas(pathCommands, viewBox) {
|
|
|
949
1228
|
* @param {Object} shape - Shape object with type and properties
|
|
950
1229
|
* @param {{x: Decimal, y: Decimal, width: Decimal, height: Decimal}} viewBox - ViewBox object
|
|
951
1230
|
* @returns {{offCanvas: boolean, bbox: Object, verified: boolean}}
|
|
1231
|
+
* @throws {Error} If parameters are invalid
|
|
952
1232
|
*/
|
|
953
1233
|
export function isShapeOffCanvas(shape, viewBox) {
|
|
954
|
-
//
|
|
1234
|
+
// Validate viewBox parameter (WHY: prevent null dereference)
|
|
1235
|
+
if (!viewBox || typeof viewBox !== "object") {
|
|
1236
|
+
throw new Error("isShapeOffCanvas: viewBox must be an object");
|
|
1237
|
+
}
|
|
1238
|
+
if (
|
|
1239
|
+
viewBox.x == null ||
|
|
1240
|
+
viewBox.y == null ||
|
|
1241
|
+
viewBox.width == null ||
|
|
1242
|
+
viewBox.height == null
|
|
1243
|
+
) {
|
|
1244
|
+
throw new Error(
|
|
1245
|
+
"isShapeOffCanvas: viewBox must have x, y, width, height properties",
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// Calculate shape bounding box (shapeBoundingBox validates shape)
|
|
955
1250
|
const bbox = shapeBoundingBox(shape);
|
|
956
1251
|
|
|
957
1252
|
// Check intersection
|
|
@@ -985,8 +1280,31 @@ export function isShapeOffCanvas(shape, viewBox) {
|
|
|
985
1280
|
* @param {{x: Decimal, y: Decimal}} p2 - Line segment end
|
|
986
1281
|
* @param {{minX: Decimal, minY: Decimal, maxX: Decimal, maxY: Decimal}} bounds - Clipping bounds
|
|
987
1282
|
* @returns {Array<{x: Decimal, y: Decimal}>} Clipped segment endpoints (empty if completely outside)
|
|
1283
|
+
* @throws {Error} If parameters are invalid
|
|
988
1284
|
*/
|
|
989
1285
|
function clipLine(p1, p2, bounds) {
|
|
1286
|
+
// Validate points (WHY: prevent null dereference)
|
|
1287
|
+
if (!p1 || typeof p1 !== "object" || p1.x == null || p1.y == null) {
|
|
1288
|
+
throw new Error("clipLine: p1 must be an object with x and y properties");
|
|
1289
|
+
}
|
|
1290
|
+
if (!p2 || typeof p2 !== "object" || p2.x == null || p2.y == null) {
|
|
1291
|
+
throw new Error("clipLine: p2 must be an object with x and y properties");
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// Validate bounds (WHY: prevent null dereference)
|
|
1295
|
+
if (!bounds || typeof bounds !== "object") {
|
|
1296
|
+
throw new Error("clipLine: bounds must be a bounding box object");
|
|
1297
|
+
}
|
|
1298
|
+
if (
|
|
1299
|
+
bounds.minX == null ||
|
|
1300
|
+
bounds.minY == null ||
|
|
1301
|
+
bounds.maxX == null ||
|
|
1302
|
+
bounds.maxY == null
|
|
1303
|
+
) {
|
|
1304
|
+
throw new Error(
|
|
1305
|
+
"clipLine: bounds must have minX, minY, maxX, maxY properties",
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
990
1308
|
// Cohen-Sutherland outcodes
|
|
991
1309
|
const INSIDE = 0; // 0000
|
|
992
1310
|
const LEFT = 1; // 0001
|
|
@@ -1101,8 +1419,29 @@ function clipLine(p1, p2, bounds) {
|
|
|
1101
1419
|
* @param {Array<Object>} pathCommands - Array of path command objects
|
|
1102
1420
|
* @param {{x: Decimal, y: Decimal, width: Decimal, height: Decimal}} viewBox - ViewBox object
|
|
1103
1421
|
* @returns {{commands: Array<Object>, verified: boolean}}
|
|
1422
|
+
* @throws {Error} If parameters are invalid
|
|
1104
1423
|
*/
|
|
1105
1424
|
export function clipPathToViewBox(pathCommands, viewBox) {
|
|
1425
|
+
// Validate pathCommands (WHY: prevent null dereference)
|
|
1426
|
+
if (!Array.isArray(pathCommands)) {
|
|
1427
|
+
throw new Error("clipPathToViewBox: pathCommands must be an array");
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// Validate viewBox parameter (WHY: prevent null dereference)
|
|
1431
|
+
if (!viewBox || typeof viewBox !== "object") {
|
|
1432
|
+
throw new Error("clipPathToViewBox: viewBox must be an object");
|
|
1433
|
+
}
|
|
1434
|
+
if (
|
|
1435
|
+
viewBox.x == null ||
|
|
1436
|
+
viewBox.y == null ||
|
|
1437
|
+
viewBox.width == null ||
|
|
1438
|
+
viewBox.height == null
|
|
1439
|
+
) {
|
|
1440
|
+
throw new Error(
|
|
1441
|
+
"clipPathToViewBox: viewBox must have x, y, width, height properties",
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1106
1445
|
const bounds = {
|
|
1107
1446
|
minX: viewBox.x,
|
|
1108
1447
|
minY: viewBox.y,
|
|
@@ -1116,11 +1455,21 @@ export function clipPathToViewBox(pathCommands, viewBox) {
|
|
|
1116
1455
|
let pathStarted = false;
|
|
1117
1456
|
|
|
1118
1457
|
for (const cmd of pathCommands) {
|
|
1458
|
+
// Validate command object and type (WHY: prevent null dereference)
|
|
1459
|
+
if (!cmd || typeof cmd !== "object" || !cmd.type || typeof cmd.type !== "string") {
|
|
1460
|
+
continue; // Skip invalid commands during clipping (WHY: graceful degradation)
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1119
1463
|
const type = cmd.type.toUpperCase();
|
|
1120
1464
|
|
|
1121
1465
|
switch (type) {
|
|
1122
1466
|
case "M": // Move
|
|
1123
1467
|
{
|
|
1468
|
+
// Skip if properties missing (WHY: graceful degradation during clipping)
|
|
1469
|
+
if (cmd.x == null || cmd.y == null) {
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1124
1473
|
const x = D(cmd.x);
|
|
1125
1474
|
const y = D(cmd.y);
|
|
1126
1475
|
|
|
@@ -1139,6 +1488,11 @@ export function clipPathToViewBox(pathCommands, viewBox) {
|
|
|
1139
1488
|
|
|
1140
1489
|
case "L": // Line to
|
|
1141
1490
|
{
|
|
1491
|
+
// Skip if properties missing (WHY: graceful degradation during clipping)
|
|
1492
|
+
if (cmd.x == null || cmd.y == null) {
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1142
1496
|
const x = D(cmd.x);
|
|
1143
1497
|
const y = D(cmd.y);
|
|
1144
1498
|
|
|
@@ -1176,6 +1530,11 @@ export function clipPathToViewBox(pathCommands, viewBox) {
|
|
|
1176
1530
|
|
|
1177
1531
|
case "H": // Horizontal line
|
|
1178
1532
|
{
|
|
1533
|
+
// Skip if properties missing (WHY: graceful degradation during clipping)
|
|
1534
|
+
if (cmd.x == null) {
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1179
1538
|
const x = D(cmd.x);
|
|
1180
1539
|
const clipped = clipLine(
|
|
1181
1540
|
{ x: currentX, y: currentY },
|
|
@@ -1207,6 +1566,11 @@ export function clipPathToViewBox(pathCommands, viewBox) {
|
|
|
1207
1566
|
|
|
1208
1567
|
case "V": // Vertical line
|
|
1209
1568
|
{
|
|
1569
|
+
// Skip if properties missing (WHY: graceful degradation during clipping)
|
|
1570
|
+
if (cmd.y == null) {
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1210
1574
|
const y = D(cmd.y);
|
|
1211
1575
|
const clipped = clipLine(
|
|
1212
1576
|
{ x: currentX, y: currentY },
|
|
@@ -1242,6 +1606,11 @@ export function clipPathToViewBox(pathCommands, viewBox) {
|
|
|
1242
1606
|
case "T": // Smooth quadratic - sample as polyline
|
|
1243
1607
|
case "A": // Arc - sample as polyline
|
|
1244
1608
|
{
|
|
1609
|
+
// Skip if properties missing (WHY: graceful degradation during clipping)
|
|
1610
|
+
if (cmd.x == null || cmd.y == null) {
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1245
1614
|
// For simplicity, just include the endpoint
|
|
1246
1615
|
// A full implementation would sample the curve
|
|
1247
1616
|
const x = D(cmd.x);
|