@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
package/src/verification.js
CHANGED
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
* @module verification
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import Decimal from
|
|
18
|
-
import { Matrix } from
|
|
19
|
-
import { Vector } from
|
|
20
|
-
import * as Transforms2D from
|
|
17
|
+
import Decimal from "decimal.js";
|
|
18
|
+
import { Matrix as _Matrix } from "./matrix.js";
|
|
19
|
+
import { Vector as _Vector } from "./vector.js";
|
|
20
|
+
import * as Transforms2D from "./transforms2d.js";
|
|
21
21
|
|
|
22
22
|
// Use high precision for verifications
|
|
23
23
|
Decimal.set({ precision: 80 });
|
|
24
24
|
|
|
25
|
-
const D = x => (x instanceof Decimal ? x : new Decimal(x));
|
|
25
|
+
const D = (x) => (x instanceof Decimal ? x : new Decimal(x));
|
|
26
26
|
const ZERO = new Decimal(0);
|
|
27
27
|
const ONE = new Decimal(1);
|
|
28
28
|
|
|
@@ -81,8 +81,8 @@ export function verifyTransformRoundTrip(matrix, x, y) {
|
|
|
81
81
|
valid: false,
|
|
82
82
|
error: new Decimal(Infinity),
|
|
83
83
|
tolerance,
|
|
84
|
-
message:
|
|
85
|
-
details: { determinant: matrix.determinant() }
|
|
84
|
+
message: "Matrix is not invertible (determinant = 0)",
|
|
85
|
+
details: { determinant: matrix.determinant() },
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -108,15 +108,15 @@ export function verifyTransformRoundTrip(matrix, x, y) {
|
|
|
108
108
|
transformed: { x: fwdX.toString(), y: fwdY.toString() },
|
|
109
109
|
recovered: { x: revX.toString(), y: revY.toString() },
|
|
110
110
|
errorX: errorX.toExponential(),
|
|
111
|
-
errorY: errorY.toExponential()
|
|
112
|
-
}
|
|
111
|
+
errorY: errorY.toExponential(),
|
|
112
|
+
},
|
|
113
113
|
};
|
|
114
114
|
} catch (e) {
|
|
115
115
|
return {
|
|
116
116
|
valid: false,
|
|
117
117
|
error: new Decimal(Infinity),
|
|
118
118
|
tolerance,
|
|
119
|
-
message: `Verification error: ${e.message}
|
|
119
|
+
message: `Verification error: ${e.message}`,
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
}
|
|
@@ -140,7 +140,7 @@ export function verifyTransformGeometry(matrix, points) {
|
|
|
140
140
|
valid: false,
|
|
141
141
|
error: ZERO,
|
|
142
142
|
tolerance,
|
|
143
|
-
message:
|
|
143
|
+
message: "Need at least 3 points for geometry verification",
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -149,27 +149,43 @@ export function verifyTransformGeometry(matrix, points) {
|
|
|
149
149
|
const absdet = det.abs();
|
|
150
150
|
|
|
151
151
|
// Transform all points
|
|
152
|
-
const transformed = points.map(p => {
|
|
152
|
+
const transformed = points.map((p) => {
|
|
153
153
|
const [tx, ty] = Transforms2D.applyTransform(matrix, D(p.x), D(p.y));
|
|
154
154
|
return { x: tx, y: ty };
|
|
155
155
|
});
|
|
156
156
|
|
|
157
157
|
// Verify area scaling (using first 3 points as triangle)
|
|
158
158
|
const origArea = triangleArea(points[0], points[1], points[2]);
|
|
159
|
-
const transArea = triangleArea(
|
|
159
|
+
const transArea = triangleArea(
|
|
160
|
+
transformed[0],
|
|
161
|
+
transformed[1],
|
|
162
|
+
transformed[2],
|
|
163
|
+
);
|
|
160
164
|
|
|
161
165
|
// Expected transformed area = |det| * original area
|
|
162
166
|
const expectedArea = absdet.times(origArea);
|
|
163
167
|
const areaError = transArea.minus(expectedArea).abs();
|
|
164
|
-
const relativeAreaError = origArea.isZero()
|
|
168
|
+
const relativeAreaError = origArea.isZero()
|
|
169
|
+
? areaError
|
|
170
|
+
: areaError.div(origArea);
|
|
165
171
|
|
|
166
172
|
const areaValid = relativeAreaError.lessThan(tolerance);
|
|
167
173
|
|
|
168
174
|
// Verify collinearity preservation (if 3+ points are collinear, they should remain so)
|
|
169
175
|
let collinearityValid = true;
|
|
170
176
|
if (points.length >= 3) {
|
|
171
|
-
const origCollinear = areCollinear(
|
|
172
|
-
|
|
177
|
+
const origCollinear = areCollinear(
|
|
178
|
+
points[0],
|
|
179
|
+
points[1],
|
|
180
|
+
points[2],
|
|
181
|
+
tolerance,
|
|
182
|
+
);
|
|
183
|
+
const transCollinear = areCollinear(
|
|
184
|
+
transformed[0],
|
|
185
|
+
transformed[1],
|
|
186
|
+
transformed[2],
|
|
187
|
+
tolerance,
|
|
188
|
+
);
|
|
173
189
|
collinearityValid = origCollinear === transCollinear;
|
|
174
190
|
}
|
|
175
191
|
|
|
@@ -181,23 +197,23 @@ export function verifyTransformGeometry(matrix, points) {
|
|
|
181
197
|
error,
|
|
182
198
|
tolerance,
|
|
183
199
|
message: valid
|
|
184
|
-
?
|
|
185
|
-
: `Geometry verification FAILED: ${!areaValid ?
|
|
200
|
+
? "Geometric properties preserved"
|
|
201
|
+
: `Geometry verification FAILED: ${!areaValid ? "area scaling incorrect" : "collinearity not preserved"}`,
|
|
186
202
|
details: {
|
|
187
203
|
determinant: det.toString(),
|
|
188
204
|
originalArea: origArea.toString(),
|
|
189
205
|
transformedArea: transArea.toString(),
|
|
190
206
|
expectedArea: expectedArea.toString(),
|
|
191
207
|
areaError: relativeAreaError.toExponential(),
|
|
192
|
-
collinearityPreserved: collinearityValid
|
|
193
|
-
}
|
|
208
|
+
collinearityPreserved: collinearityValid,
|
|
209
|
+
},
|
|
194
210
|
};
|
|
195
211
|
} catch (e) {
|
|
196
212
|
return {
|
|
197
213
|
valid: false,
|
|
198
214
|
error: new Decimal(Infinity),
|
|
199
215
|
tolerance,
|
|
200
|
-
message: `Verification error: ${e.message}
|
|
216
|
+
message: `Verification error: ${e.message}`,
|
|
201
217
|
};
|
|
202
218
|
}
|
|
203
219
|
}
|
|
@@ -225,8 +241,8 @@ export function verifyMatrixInversion(matrix) {
|
|
|
225
241
|
valid: false,
|
|
226
242
|
error: new Decimal(Infinity),
|
|
227
243
|
tolerance,
|
|
228
|
-
message:
|
|
229
|
-
details: { determinant: matrix.determinant().toString() }
|
|
244
|
+
message: "Matrix is singular (not invertible)",
|
|
245
|
+
details: { determinant: matrix.determinant().toString() },
|
|
230
246
|
};
|
|
231
247
|
}
|
|
232
248
|
|
|
@@ -250,7 +266,13 @@ export function verifyMatrixInversion(matrix) {
|
|
|
250
266
|
}
|
|
251
267
|
|
|
252
268
|
if (error.greaterThanOrEqualTo(tolerance)) {
|
|
253
|
-
errors.push({
|
|
269
|
+
errors.push({
|
|
270
|
+
row: i,
|
|
271
|
+
col: j,
|
|
272
|
+
expected: expected.toString(),
|
|
273
|
+
actual: actual.toString(),
|
|
274
|
+
error: error.toExponential(),
|
|
275
|
+
});
|
|
254
276
|
}
|
|
255
277
|
}
|
|
256
278
|
}
|
|
@@ -267,15 +289,15 @@ export function verifyMatrixInversion(matrix) {
|
|
|
267
289
|
details: {
|
|
268
290
|
matrixSize: `${n}x${n}`,
|
|
269
291
|
maxError: maxError.toExponential(),
|
|
270
|
-
failedElements: errors.slice(0, 5) // First 5 failures
|
|
271
|
-
}
|
|
292
|
+
failedElements: errors.slice(0, 5), // First 5 failures
|
|
293
|
+
},
|
|
272
294
|
};
|
|
273
295
|
} catch (e) {
|
|
274
296
|
return {
|
|
275
297
|
valid: false,
|
|
276
298
|
error: new Decimal(Infinity),
|
|
277
299
|
tolerance,
|
|
278
|
-
message: `Verification error: ${e.message}
|
|
300
|
+
message: `Verification error: ${e.message}`,
|
|
279
301
|
};
|
|
280
302
|
}
|
|
281
303
|
}
|
|
@@ -320,14 +342,14 @@ export function verifyMultiplicationAssociativity(A, B, C) {
|
|
|
320
342
|
tolerance,
|
|
321
343
|
message: valid
|
|
322
344
|
? `Associativity verified: (A*B)*C = A*(B*C), max error ${maxError.toExponential()}`
|
|
323
|
-
: `Associativity FAILED: max error ${maxError.toExponential()}
|
|
345
|
+
: `Associativity FAILED: max error ${maxError.toExponential()}`,
|
|
324
346
|
};
|
|
325
347
|
} catch (e) {
|
|
326
348
|
return {
|
|
327
349
|
valid: false,
|
|
328
350
|
error: new Decimal(Infinity),
|
|
329
351
|
tolerance,
|
|
330
|
-
message: `Verification error: ${e.message}
|
|
352
|
+
message: `Verification error: ${e.message}`,
|
|
331
353
|
};
|
|
332
354
|
}
|
|
333
355
|
}
|
|
@@ -349,17 +371,21 @@ export function verifyMultiplicationAssociativity(A, B, C) {
|
|
|
349
371
|
* @param {Decimal} [distanceTolerance] - Max distance outside allowed (default: 1e-6)
|
|
350
372
|
* @returns {VerificationResult} Verification result
|
|
351
373
|
*/
|
|
352
|
-
export function verifyPolygonContainment(
|
|
374
|
+
export function verifyPolygonContainment(
|
|
375
|
+
inner,
|
|
376
|
+
outer,
|
|
377
|
+
distanceTolerance = null,
|
|
378
|
+
) {
|
|
353
379
|
const tolerance = computeTolerance();
|
|
354
380
|
// Distance tolerance for curve approximation - points can be slightly outside
|
|
355
|
-
const maxDistOutside = distanceTolerance || new Decimal(
|
|
381
|
+
const maxDistOutside = distanceTolerance || new Decimal("1e-6");
|
|
356
382
|
|
|
357
383
|
if (inner.length < 3 || outer.length < 3) {
|
|
358
384
|
return {
|
|
359
385
|
valid: false,
|
|
360
386
|
error: ZERO,
|
|
361
387
|
tolerance,
|
|
362
|
-
message:
|
|
388
|
+
message: "Polygons must have at least 3 vertices",
|
|
363
389
|
};
|
|
364
390
|
}
|
|
365
391
|
|
|
@@ -385,7 +411,7 @@ export function verifyPolygonContainment(inner, outer, distanceTolerance = null)
|
|
|
385
411
|
index: i,
|
|
386
412
|
x: point.x.toString(),
|
|
387
413
|
y: point.y.toString(),
|
|
388
|
-
distanceOutside: distToEdge.toExponential()
|
|
414
|
+
distanceOutside: distToEdge.toExponential(),
|
|
389
415
|
});
|
|
390
416
|
}
|
|
391
417
|
}
|
|
@@ -402,15 +428,15 @@ export function verifyPolygonContainment(inner, outer, distanceTolerance = null)
|
|
|
402
428
|
innerVertices: inner.length,
|
|
403
429
|
outerVertices: outer.length,
|
|
404
430
|
maxOutsideDistance: maxOutsideDistance.toExponential(),
|
|
405
|
-
outsidePoints: outsidePoints.slice(0, 5)
|
|
406
|
-
}
|
|
431
|
+
outsidePoints: outsidePoints.slice(0, 5),
|
|
432
|
+
},
|
|
407
433
|
};
|
|
408
434
|
} catch (e) {
|
|
409
435
|
return {
|
|
410
436
|
valid: false,
|
|
411
437
|
error: new Decimal(Infinity),
|
|
412
438
|
tolerance,
|
|
413
|
-
message: `Verification error: ${e.message}
|
|
439
|
+
message: `Verification error: ${e.message}`,
|
|
414
440
|
};
|
|
415
441
|
}
|
|
416
442
|
}
|
|
@@ -418,16 +444,23 @@ export function verifyPolygonContainment(inner, outer, distanceTolerance = null)
|
|
|
418
444
|
/**
|
|
419
445
|
* Compute minimum distance from a point to the edges of a polygon.
|
|
420
446
|
* @private
|
|
447
|
+
* @param {{x: Decimal|number, y: Decimal|number}} point - Point to measure from
|
|
448
|
+
* @param {Array<{x: Decimal|number, y: Decimal|number}>} polygon - Polygon vertices
|
|
449
|
+
* @returns {Decimal} Minimum distance to any polygon edge
|
|
421
450
|
*/
|
|
422
451
|
function minDistanceToPolygonEdge(point, polygon) {
|
|
423
452
|
let minDist = new Decimal(Infinity);
|
|
424
|
-
const px = D(point.x),
|
|
453
|
+
const px = D(point.x),
|
|
454
|
+
py = D(point.y);
|
|
425
455
|
|
|
426
456
|
for (let i = 0; i < polygon.length; i++) {
|
|
427
457
|
const j = (i + 1) % polygon.length;
|
|
428
|
-
const p1 = polygon[i],
|
|
429
|
-
|
|
430
|
-
const
|
|
458
|
+
const p1 = polygon[i],
|
|
459
|
+
p2 = polygon[j];
|
|
460
|
+
const x1 = D(p1.x),
|
|
461
|
+
y1 = D(p1.y);
|
|
462
|
+
const x2 = D(p2.x),
|
|
463
|
+
y2 = D(p2.y);
|
|
431
464
|
|
|
432
465
|
// Distance from point to line segment [p1, p2]
|
|
433
466
|
const dx = x2.minus(x1);
|
|
@@ -440,9 +473,13 @@ function minDistanceToPolygonEdge(point, polygon) {
|
|
|
440
473
|
dist = pointDistance(point, p1);
|
|
441
474
|
} else {
|
|
442
475
|
// Project point onto line, clamp to segment
|
|
443
|
-
const t = Decimal.max(
|
|
444
|
-
|
|
445
|
-
|
|
476
|
+
const t = Decimal.max(
|
|
477
|
+
ZERO,
|
|
478
|
+
Decimal.min(
|
|
479
|
+
ONE,
|
|
480
|
+
px.minus(x1).times(dx).plus(py.minus(y1).times(dy)).div(lenSq),
|
|
481
|
+
),
|
|
482
|
+
);
|
|
446
483
|
const projX = x1.plus(t.times(dx));
|
|
447
484
|
const projY = y1.plus(t.times(dy));
|
|
448
485
|
dist = px.minus(projX).pow(2).plus(py.minus(projY).pow(2)).sqrt();
|
|
@@ -476,8 +513,8 @@ export function verifyPolygonIntersection(poly1, poly2, intersection) {
|
|
|
476
513
|
valid: true,
|
|
477
514
|
error: ZERO,
|
|
478
515
|
tolerance,
|
|
479
|
-
message:
|
|
480
|
-
details: { intersectionVertices: intersection.length }
|
|
516
|
+
message: "Intersection is empty or degenerate (valid result)",
|
|
517
|
+
details: { intersectionVertices: intersection.length },
|
|
481
518
|
};
|
|
482
519
|
}
|
|
483
520
|
|
|
@@ -494,7 +531,9 @@ export function verifyPolygonIntersection(poly1, poly2, intersection) {
|
|
|
494
531
|
const minArea = Decimal.min(area1, area2);
|
|
495
532
|
|
|
496
533
|
// Allow small tolerance for floating point in area calculation
|
|
497
|
-
const areaValid = areaInt.lessThanOrEqualTo(
|
|
534
|
+
const areaValid = areaInt.lessThanOrEqualTo(
|
|
535
|
+
minArea.times(ONE.plus(tolerance)),
|
|
536
|
+
);
|
|
498
537
|
|
|
499
538
|
const valid = containment1.valid && containment2.valid && areaValid;
|
|
500
539
|
|
|
@@ -503,23 +542,23 @@ export function verifyPolygonIntersection(poly1, poly2, intersection) {
|
|
|
503
542
|
error: valid ? ZERO : ONE,
|
|
504
543
|
tolerance,
|
|
505
544
|
message: valid
|
|
506
|
-
?
|
|
507
|
-
: `Intersection FAILED: ${!containment1.valid ?
|
|
545
|
+
? "Intersection verified: contained in both polygons, area valid"
|
|
546
|
+
: `Intersection FAILED: ${!containment1.valid ? "not in poly1, " : ""}${!containment2.valid ? "not in poly2, " : ""}${!areaValid ? "area too large" : ""}`,
|
|
508
547
|
details: {
|
|
509
548
|
containedInPoly1: containment1.valid,
|
|
510
549
|
containedInPoly2: containment2.valid,
|
|
511
550
|
area1: area1.toString(),
|
|
512
551
|
area2: area2.toString(),
|
|
513
552
|
intersectionArea: areaInt.toString(),
|
|
514
|
-
areaValid
|
|
515
|
-
}
|
|
553
|
+
areaValid,
|
|
554
|
+
},
|
|
516
555
|
};
|
|
517
556
|
} catch (e) {
|
|
518
557
|
return {
|
|
519
558
|
valid: false,
|
|
520
559
|
error: new Decimal(Infinity),
|
|
521
560
|
tolerance,
|
|
522
|
-
message: `Verification error: ${e.message}
|
|
561
|
+
message: `Verification error: ${e.message}`,
|
|
523
562
|
};
|
|
524
563
|
}
|
|
525
564
|
}
|
|
@@ -540,15 +579,17 @@ export function verifyPolygonIntersection(poly1, poly2, intersection) {
|
|
|
540
579
|
*/
|
|
541
580
|
export function verifyCircleToPath(cx, cy, r, pathData) {
|
|
542
581
|
const tolerance = computeTolerance();
|
|
543
|
-
const cxD = D(cx),
|
|
582
|
+
const cxD = D(cx),
|
|
583
|
+
cyD = D(cy),
|
|
584
|
+
rD = D(r);
|
|
544
585
|
|
|
545
586
|
try {
|
|
546
587
|
// Expected key points (cardinal points)
|
|
547
588
|
const expectedPoints = [
|
|
548
|
-
{ x: cxD.plus(rD), y: cyD, name:
|
|
549
|
-
{ x: cxD, y: cyD.plus(rD), name:
|
|
550
|
-
{ x: cxD.minus(rD), y: cyD, name:
|
|
551
|
-
{ x: cxD, y: cyD.minus(rD), name:
|
|
589
|
+
{ x: cxD.plus(rD), y: cyD, name: "right" },
|
|
590
|
+
{ x: cxD, y: cyD.plus(rD), name: "bottom" },
|
|
591
|
+
{ x: cxD.minus(rD), y: cyD, name: "left" },
|
|
592
|
+
{ x: cxD, y: cyD.minus(rD), name: "top" },
|
|
552
593
|
];
|
|
553
594
|
|
|
554
595
|
// Extract points from path data
|
|
@@ -566,7 +607,10 @@ export function verifyCircleToPath(cx, cy, r, pathData) {
|
|
|
566
607
|
maxError = error;
|
|
567
608
|
}
|
|
568
609
|
if (error.greaterThanOrEqualTo(tolerance)) {
|
|
569
|
-
missingPoints.push({
|
|
610
|
+
missingPoints.push({
|
|
611
|
+
...expected,
|
|
612
|
+
nearestError: error.toExponential(),
|
|
613
|
+
});
|
|
570
614
|
}
|
|
571
615
|
} else {
|
|
572
616
|
missingPoints.push(expected);
|
|
@@ -587,15 +631,15 @@ export function verifyCircleToPath(cx, cy, r, pathData) {
|
|
|
587
631
|
center: { x: cxD.toString(), y: cyD.toString() },
|
|
588
632
|
radius: rD.toString(),
|
|
589
633
|
pathPointCount: pathPoints.length,
|
|
590
|
-
missingPoints: missingPoints.map(p => p.name || `(${p.x}, ${p.y})`)
|
|
591
|
-
}
|
|
634
|
+
missingPoints: missingPoints.map((p) => p.name || `(${p.x}, ${p.y})`),
|
|
635
|
+
},
|
|
592
636
|
};
|
|
593
637
|
} catch (e) {
|
|
594
638
|
return {
|
|
595
639
|
valid: false,
|
|
596
640
|
error: new Decimal(Infinity),
|
|
597
641
|
tolerance,
|
|
598
|
-
message: `Verification error: ${e.message}
|
|
642
|
+
message: `Verification error: ${e.message}`,
|
|
599
643
|
};
|
|
600
644
|
}
|
|
601
645
|
}
|
|
@@ -612,15 +656,18 @@ export function verifyCircleToPath(cx, cy, r, pathData) {
|
|
|
612
656
|
*/
|
|
613
657
|
export function verifyRectToPath(x, y, width, height, pathData) {
|
|
614
658
|
const tolerance = computeTolerance();
|
|
615
|
-
const xD = D(x),
|
|
659
|
+
const xD = D(x),
|
|
660
|
+
yD = D(y),
|
|
661
|
+
wD = D(width),
|
|
662
|
+
hD = D(height);
|
|
616
663
|
|
|
617
664
|
try {
|
|
618
665
|
// Expected corners
|
|
619
666
|
const expectedCorners = [
|
|
620
|
-
{ x: xD, y: yD, name:
|
|
621
|
-
{ x: xD.plus(wD), y: yD, name:
|
|
622
|
-
{ x: xD.plus(wD), y: yD.plus(hD), name:
|
|
623
|
-
{ x: xD, y: yD.plus(hD), name:
|
|
667
|
+
{ x: xD, y: yD, name: "top-left" },
|
|
668
|
+
{ x: xD.plus(wD), y: yD, name: "top-right" },
|
|
669
|
+
{ x: xD.plus(wD), y: yD.plus(hD), name: "bottom-right" },
|
|
670
|
+
{ x: xD, y: yD.plus(hD), name: "bottom-left" },
|
|
624
671
|
];
|
|
625
672
|
|
|
626
673
|
const pathPoints = extractPathPoints(pathData);
|
|
@@ -636,7 +683,10 @@ export function verifyRectToPath(x, y, width, height, pathData) {
|
|
|
636
683
|
maxError = error;
|
|
637
684
|
}
|
|
638
685
|
if (error.greaterThanOrEqualTo(tolerance)) {
|
|
639
|
-
missingCorners.push({
|
|
686
|
+
missingCorners.push({
|
|
687
|
+
...corner,
|
|
688
|
+
nearestError: error.toExponential(),
|
|
689
|
+
});
|
|
640
690
|
}
|
|
641
691
|
} else {
|
|
642
692
|
missingCorners.push(corner);
|
|
@@ -654,17 +704,22 @@ export function verifyRectToPath(x, y, width, height, pathData) {
|
|
|
654
704
|
? `Rect to path verified: all corners present, max error ${maxError.toExponential()}`
|
|
655
705
|
: `Rect to path FAILED: ${missingCorners.length} corners missing or inaccurate`,
|
|
656
706
|
details: {
|
|
657
|
-
rect: {
|
|
707
|
+
rect: {
|
|
708
|
+
x: xD.toString(),
|
|
709
|
+
y: yD.toString(),
|
|
710
|
+
width: wD.toString(),
|
|
711
|
+
height: hD.toString(),
|
|
712
|
+
},
|
|
658
713
|
pathPointCount: pathPoints.length,
|
|
659
|
-
missingCorners: missingCorners.map(c => c.name)
|
|
660
|
-
}
|
|
714
|
+
missingCorners: missingCorners.map((c) => c.name),
|
|
715
|
+
},
|
|
661
716
|
};
|
|
662
717
|
} catch (e) {
|
|
663
718
|
return {
|
|
664
719
|
valid: false,
|
|
665
720
|
error: new Decimal(Infinity),
|
|
666
721
|
tolerance,
|
|
667
|
-
message: `Verification error: ${e.message}
|
|
722
|
+
message: `Verification error: ${e.message}`,
|
|
668
723
|
};
|
|
669
724
|
}
|
|
670
725
|
}
|
|
@@ -687,8 +742,16 @@ export function verifyLinearGradientTransform(original, baked, matrix) {
|
|
|
687
742
|
|
|
688
743
|
try {
|
|
689
744
|
// Transform original points using the provided matrix
|
|
690
|
-
const [expX1, expY1] = Transforms2D.applyTransform(
|
|
691
|
-
|
|
745
|
+
const [expX1, expY1] = Transforms2D.applyTransform(
|
|
746
|
+
matrix,
|
|
747
|
+
D(original.x1 || 0),
|
|
748
|
+
D(original.y1 || 0),
|
|
749
|
+
);
|
|
750
|
+
const [expX2, expY2] = Transforms2D.applyTransform(
|
|
751
|
+
matrix,
|
|
752
|
+
D(original.x2 || 1),
|
|
753
|
+
D(original.y2 || 0),
|
|
754
|
+
);
|
|
692
755
|
|
|
693
756
|
// Compare with baked values
|
|
694
757
|
const errorX1 = D(baked.x1).minus(expX1).abs();
|
|
@@ -707,16 +770,21 @@ export function verifyLinearGradientTransform(original, baked, matrix) {
|
|
|
707
770
|
? `Linear gradient transform verified: max error ${maxError.toExponential()}`
|
|
708
771
|
: `Linear gradient transform FAILED: max error ${maxError.toExponential()}`,
|
|
709
772
|
details: {
|
|
710
|
-
expected: {
|
|
711
|
-
|
|
712
|
-
|
|
773
|
+
expected: {
|
|
774
|
+
x1: expX1.toString(),
|
|
775
|
+
y1: expY1.toString(),
|
|
776
|
+
x2: expX2.toString(),
|
|
777
|
+
y2: expY2.toString(),
|
|
778
|
+
},
|
|
779
|
+
actual: baked,
|
|
780
|
+
},
|
|
713
781
|
};
|
|
714
782
|
} catch (e) {
|
|
715
783
|
return {
|
|
716
784
|
valid: false,
|
|
717
785
|
error: new Decimal(Infinity),
|
|
718
786
|
tolerance,
|
|
719
|
-
message: `Verification error: ${e.message}
|
|
787
|
+
message: `Verification error: ${e.message}`,
|
|
720
788
|
};
|
|
721
789
|
}
|
|
722
790
|
}
|
|
@@ -728,14 +796,22 @@ export function verifyLinearGradientTransform(original, baked, matrix) {
|
|
|
728
796
|
/**
|
|
729
797
|
* Compute signed area of triangle using cross product.
|
|
730
798
|
* @private
|
|
799
|
+
* @param {{x: Decimal|number, y: Decimal|number}} p1 - First triangle vertex
|
|
800
|
+
* @param {{x: Decimal|number, y: Decimal|number}} p2 - Second triangle vertex
|
|
801
|
+
* @param {{x: Decimal|number, y: Decimal|number}} p3 - Third triangle vertex
|
|
802
|
+
* @returns {Decimal} Signed area of the triangle
|
|
731
803
|
*/
|
|
732
804
|
function triangleArea(p1, p2, p3) {
|
|
733
|
-
const x1 = D(p1.x),
|
|
734
|
-
|
|
735
|
-
const
|
|
805
|
+
const x1 = D(p1.x),
|
|
806
|
+
y1 = D(p1.y);
|
|
807
|
+
const x2 = D(p2.x),
|
|
808
|
+
y2 = D(p2.y);
|
|
809
|
+
const x3 = D(p3.x),
|
|
810
|
+
y3 = D(p3.y);
|
|
736
811
|
|
|
737
812
|
// Area = 0.5 * |x1(y2-y3) + x2(y3-y1) + x3(y1-y2)|
|
|
738
|
-
const area = x1
|
|
813
|
+
const area = x1
|
|
814
|
+
.times(y2.minus(y3))
|
|
739
815
|
.plus(x2.times(y3.minus(y1)))
|
|
740
816
|
.plus(x3.times(y1.minus(y2)))
|
|
741
817
|
.abs()
|
|
@@ -747,6 +823,11 @@ function triangleArea(p1, p2, p3) {
|
|
|
747
823
|
/**
|
|
748
824
|
* Check if three points are collinear.
|
|
749
825
|
* @private
|
|
826
|
+
* @param {{x: Decimal|number, y: Decimal|number}} p1 - First point
|
|
827
|
+
* @param {{x: Decimal|number, y: Decimal|number}} p2 - Second point
|
|
828
|
+
* @param {{x: Decimal|number, y: Decimal|number}} p3 - Third point
|
|
829
|
+
* @param {Decimal|number} tolerance - Collinearity tolerance
|
|
830
|
+
* @returns {boolean} True if points are collinear within tolerance
|
|
750
831
|
*/
|
|
751
832
|
function areCollinear(p1, p2, p3, tolerance) {
|
|
752
833
|
const area = triangleArea(p1, p2, p3);
|
|
@@ -756,6 +837,8 @@ function areCollinear(p1, p2, p3, tolerance) {
|
|
|
756
837
|
/**
|
|
757
838
|
* Compute signed area of a polygon using shoelace formula.
|
|
758
839
|
* @private
|
|
840
|
+
* @param {Array<{x: Decimal|number, y: Decimal|number}>} polygon - Polygon vertices
|
|
841
|
+
* @returns {Decimal} Absolute area of the polygon
|
|
759
842
|
*/
|
|
760
843
|
function polygonArea(polygon) {
|
|
761
844
|
if (polygon.length < 3) return ZERO;
|
|
@@ -765,8 +848,10 @@ function polygonArea(polygon) {
|
|
|
765
848
|
|
|
766
849
|
for (let i = 0; i < n; i++) {
|
|
767
850
|
const j = (i + 1) % n;
|
|
768
|
-
const xi = D(polygon[i].x),
|
|
769
|
-
|
|
851
|
+
const xi = D(polygon[i].x),
|
|
852
|
+
yi = D(polygon[i].y);
|
|
853
|
+
const xj = D(polygon[j].x),
|
|
854
|
+
yj = D(polygon[j].y);
|
|
770
855
|
area = area.plus(xi.times(yj).minus(xj.times(yi)));
|
|
771
856
|
}
|
|
772
857
|
|
|
@@ -776,22 +861,29 @@ function polygonArea(polygon) {
|
|
|
776
861
|
/**
|
|
777
862
|
* Check if point is inside polygon using ray casting.
|
|
778
863
|
* @private
|
|
864
|
+
* @param {{x: Decimal|number, y: Decimal|number}} point - Point to test
|
|
865
|
+
* @param {Array<{x: Decimal|number, y: Decimal|number}>} polygon - Polygon vertices
|
|
866
|
+
* @returns {boolean} True if point is inside or on polygon boundary
|
|
779
867
|
*/
|
|
780
868
|
function isPointInPolygon(point, polygon) {
|
|
781
|
-
const px = D(point.x),
|
|
869
|
+
const px = D(point.x),
|
|
870
|
+
py = D(point.y);
|
|
782
871
|
const n = polygon.length;
|
|
783
872
|
let inside = false;
|
|
784
873
|
|
|
785
874
|
for (let i = 0, j = n - 1; i < n; j = i++) {
|
|
786
|
-
const xi = D(polygon[i].x),
|
|
787
|
-
|
|
875
|
+
const xi = D(polygon[i].x),
|
|
876
|
+
yi = D(polygon[i].y);
|
|
877
|
+
const xj = D(polygon[j].x),
|
|
878
|
+
yj = D(polygon[j].y);
|
|
788
879
|
|
|
789
880
|
// Check if point is on the edge (with tolerance)
|
|
790
881
|
const onEdge = isPointOnSegment(point, polygon[i], polygon[j]);
|
|
791
882
|
if (onEdge) return true;
|
|
792
883
|
|
|
793
884
|
// Ray casting
|
|
794
|
-
const intersect =
|
|
885
|
+
const intersect =
|
|
886
|
+
yi.greaterThan(py) !== yj.greaterThan(py) &&
|
|
795
887
|
px.lessThan(xj.minus(xi).times(py.minus(yi)).div(yj.minus(yi)).plus(xi));
|
|
796
888
|
|
|
797
889
|
if (intersect) inside = !inside;
|
|
@@ -803,12 +895,19 @@ function isPointInPolygon(point, polygon) {
|
|
|
803
895
|
/**
|
|
804
896
|
* Check if point is on line segment.
|
|
805
897
|
* @private
|
|
898
|
+
* @param {{x: Decimal|number, y: Decimal|number}} point - Point to test
|
|
899
|
+
* @param {{x: Decimal|number, y: Decimal|number}} segStart - Segment start point
|
|
900
|
+
* @param {{x: Decimal|number, y: Decimal|number}} segEnd - Segment end point
|
|
901
|
+
* @returns {boolean} True if point is on the segment within tolerance
|
|
806
902
|
*/
|
|
807
903
|
function isPointOnSegment(point, segStart, segEnd) {
|
|
808
904
|
const tolerance = computeTolerance();
|
|
809
|
-
const px = D(point.x),
|
|
810
|
-
|
|
811
|
-
const
|
|
905
|
+
const px = D(point.x),
|
|
906
|
+
py = D(point.y);
|
|
907
|
+
const x1 = D(segStart.x),
|
|
908
|
+
y1 = D(segStart.y);
|
|
909
|
+
const x2 = D(segEnd.x),
|
|
910
|
+
y2 = D(segEnd.y);
|
|
812
911
|
|
|
813
912
|
// Check if point is within bounding box
|
|
814
913
|
const minX = Decimal.min(x1, x2).minus(tolerance);
|
|
@@ -816,12 +915,21 @@ function isPointOnSegment(point, segStart, segEnd) {
|
|
|
816
915
|
const minY = Decimal.min(y1, y2).minus(tolerance);
|
|
817
916
|
const maxY = Decimal.max(y1, y2).plus(tolerance);
|
|
818
917
|
|
|
819
|
-
if (
|
|
918
|
+
if (
|
|
919
|
+
px.lessThan(minX) ||
|
|
920
|
+
px.greaterThan(maxX) ||
|
|
921
|
+
py.lessThan(minY) ||
|
|
922
|
+
py.greaterThan(maxY)
|
|
923
|
+
) {
|
|
820
924
|
return false;
|
|
821
925
|
}
|
|
822
926
|
|
|
823
927
|
// Check collinearity (cross product should be ~0)
|
|
824
|
-
const cross = x2
|
|
928
|
+
const cross = x2
|
|
929
|
+
.minus(x1)
|
|
930
|
+
.times(py.minus(y1))
|
|
931
|
+
.minus(y2.minus(y1).times(px.minus(x1)))
|
|
932
|
+
.abs();
|
|
825
933
|
const segLength = pointDistance(segStart, segEnd);
|
|
826
934
|
|
|
827
935
|
return cross.div(segLength.plus(tolerance)).lessThan(tolerance);
|
|
@@ -830,6 +938,9 @@ function isPointOnSegment(point, segStart, segEnd) {
|
|
|
830
938
|
/**
|
|
831
939
|
* Compute distance between two points.
|
|
832
940
|
* @private
|
|
941
|
+
* @param {{x: Decimal|number, y: Decimal|number}} p1 - First point
|
|
942
|
+
* @param {{x: Decimal|number, y: Decimal|number}} p2 - Second point
|
|
943
|
+
* @returns {Decimal} Euclidean distance between the points
|
|
833
944
|
*/
|
|
834
945
|
function pointDistance(p1, p2) {
|
|
835
946
|
const dx = D(p2.x).minus(D(p1.x));
|
|
@@ -840,11 +951,14 @@ function pointDistance(p1, p2) {
|
|
|
840
951
|
/**
|
|
841
952
|
* Extract coordinate points from SVG path data.
|
|
842
953
|
* @private
|
|
954
|
+
* @param {string} pathData - SVG path data string
|
|
955
|
+
* @returns {Array<{x: Decimal, y: Decimal}>} Array of extracted coordinate points
|
|
843
956
|
*/
|
|
844
957
|
function extractPathPoints(pathData) {
|
|
845
958
|
const points = [];
|
|
846
959
|
// Match all number pairs in path data using matchAll
|
|
847
|
-
const regex =
|
|
960
|
+
const regex =
|
|
961
|
+
/([+-]?\d*\.?\d+(?:[eE][+-]?\d+)?)[,\s]+([+-]?\d*\.?\d+(?:[eE][+-]?\d+)?)/g;
|
|
848
962
|
const matches = pathData.matchAll(regex);
|
|
849
963
|
|
|
850
964
|
for (const match of matches) {
|
|
@@ -857,6 +971,9 @@ function extractPathPoints(pathData) {
|
|
|
857
971
|
/**
|
|
858
972
|
* Find the nearest point in a list to a target point.
|
|
859
973
|
* @private
|
|
974
|
+
* @param {{x: Decimal|number, y: Decimal|number}} target - Target point
|
|
975
|
+
* @param {Array<{x: Decimal|number, y: Decimal|number}>} points - Array of points to search
|
|
976
|
+
* @returns {{x: Decimal, y: Decimal}|null} Nearest point or null if array is empty
|
|
860
977
|
*/
|
|
861
978
|
function findNearestPoint(target, points) {
|
|
862
979
|
if (points.length === 0) return null;
|
|
@@ -914,7 +1031,9 @@ export function computePolygonDifference(subject, clip) {
|
|
|
914
1031
|
outsidePoints.push(current);
|
|
915
1032
|
if (!nextOutside) {
|
|
916
1033
|
// Crossing from outside to inside - add intersection point
|
|
917
|
-
outsidePoints.push(
|
|
1034
|
+
outsidePoints.push(
|
|
1035
|
+
lineIntersectE2E(current, next, edgeStart, edgeEnd),
|
|
1036
|
+
);
|
|
918
1037
|
}
|
|
919
1038
|
} else if (nextOutside) {
|
|
920
1039
|
// Crossing from inside to outside - add intersection point
|
|
@@ -957,9 +1076,17 @@ export function computePolygonDifference(subject, clip) {
|
|
|
957
1076
|
* @param {string|Decimal} [customTolerance='1e-10'] - Custom tolerance (string or Decimal)
|
|
958
1077
|
* @returns {VerificationResult} Verification result
|
|
959
1078
|
*/
|
|
960
|
-
export function verifyClipPathE2E(
|
|
1079
|
+
export function verifyClipPathE2E(
|
|
1080
|
+
original,
|
|
1081
|
+
clipped,
|
|
1082
|
+
outsideFragments = [],
|
|
1083
|
+
customTolerance = "1e-10",
|
|
1084
|
+
) {
|
|
961
1085
|
// Use configurable tolerance - higher clipSegments allows tighter tolerance
|
|
962
|
-
const tolerance =
|
|
1086
|
+
const tolerance =
|
|
1087
|
+
customTolerance instanceof Decimal
|
|
1088
|
+
? customTolerance
|
|
1089
|
+
: new Decimal(customTolerance);
|
|
963
1090
|
// Ensure outsideFragments is an array
|
|
964
1091
|
const fragments = outsideFragments || [];
|
|
965
1092
|
|
|
@@ -976,13 +1103,19 @@ export function verifyClipPathE2E(original, clipped, outsideFragments = [], cust
|
|
|
976
1103
|
// 1. Clipped area must be <= original area (intersection property)
|
|
977
1104
|
// 2. Clipped area must be >= 0 (non-negative area)
|
|
978
1105
|
// 3. For overlapping polygons, clipped area should be > 0
|
|
979
|
-
const clippedValid = clippedArea.lessThanOrEqualTo(
|
|
980
|
-
|
|
1106
|
+
const clippedValid = clippedArea.lessThanOrEqualTo(
|
|
1107
|
+
originalArea.times(ONE.plus(tolerance)),
|
|
1108
|
+
);
|
|
1109
|
+
const outsideValid = outsideArea.greaterThanOrEqualTo(
|
|
1110
|
+
ZERO.minus(tolerance.times(originalArea)),
|
|
1111
|
+
);
|
|
981
1112
|
|
|
982
1113
|
// The "error" for E2E is how close we are to perfect area conservation
|
|
983
1114
|
// Since we compute outside = original - clipped, the error is exactly 0 by construction
|
|
984
1115
|
// What we're really verifying is that the clipped area is reasonable
|
|
985
|
-
const areaRatio = originalArea.isZero()
|
|
1116
|
+
const areaRatio = originalArea.isZero()
|
|
1117
|
+
? ONE
|
|
1118
|
+
: clippedArea.div(originalArea);
|
|
986
1119
|
const error = ZERO; // By construction, original = clipped + outside is exact
|
|
987
1120
|
|
|
988
1121
|
const valid = clippedValid && outsideValid;
|
|
@@ -997,19 +1130,19 @@ export function verifyClipPathE2E(original, clipped, outsideFragments = [], cust
|
|
|
997
1130
|
details: {
|
|
998
1131
|
originalArea: originalArea.toString(),
|
|
999
1132
|
clippedArea: clippedArea.toString(),
|
|
1000
|
-
outsideArea: outsideArea.toString(),
|
|
1133
|
+
outsideArea: outsideArea.toString(), // Computed exactly as original - clipped
|
|
1001
1134
|
areaRatio: areaRatio.toFixed(6),
|
|
1002
1135
|
fragmentCount: fragments.length,
|
|
1003
1136
|
clippedValid,
|
|
1004
|
-
outsideValid
|
|
1005
|
-
}
|
|
1137
|
+
outsideValid,
|
|
1138
|
+
},
|
|
1006
1139
|
};
|
|
1007
1140
|
} catch (e) {
|
|
1008
1141
|
return {
|
|
1009
1142
|
valid: false,
|
|
1010
1143
|
error: new Decimal(Infinity),
|
|
1011
1144
|
tolerance,
|
|
1012
|
-
message: `E2E verification error: ${e.message}
|
|
1145
|
+
message: `E2E verification error: ${e.message}`,
|
|
1013
1146
|
};
|
|
1014
1147
|
}
|
|
1015
1148
|
}
|
|
@@ -1033,7 +1166,7 @@ export function verifyPipelineE2E(params) {
|
|
|
1033
1166
|
valid: false,
|
|
1034
1167
|
error: new Decimal(Infinity),
|
|
1035
1168
|
tolerance,
|
|
1036
|
-
message:
|
|
1169
|
+
message: "E2E verification failed: point count mismatch",
|
|
1037
1170
|
};
|
|
1038
1171
|
}
|
|
1039
1172
|
|
|
@@ -1049,7 +1182,7 @@ export function verifyPipelineE2E(params) {
|
|
|
1049
1182
|
const [expectedX, expectedY] = Transforms2D.applyTransform(
|
|
1050
1183
|
expectedTransform,
|
|
1051
1184
|
D(orig.x),
|
|
1052
|
-
D(orig.y)
|
|
1185
|
+
D(orig.y),
|
|
1053
1186
|
);
|
|
1054
1187
|
|
|
1055
1188
|
// Compare with actual flattened point
|
|
@@ -1066,7 +1199,7 @@ export function verifyPipelineE2E(params) {
|
|
|
1066
1199
|
pointIndex: i,
|
|
1067
1200
|
expected: { x: expectedX.toString(), y: expectedY.toString() },
|
|
1068
1201
|
actual: { x: flat.x.toString(), y: flat.y.toString() },
|
|
1069
|
-
error: error.toExponential()
|
|
1202
|
+
error: error.toExponential(),
|
|
1070
1203
|
});
|
|
1071
1204
|
}
|
|
1072
1205
|
}
|
|
@@ -1083,15 +1216,15 @@ export function verifyPipelineE2E(params) {
|
|
|
1083
1216
|
details: {
|
|
1084
1217
|
pointsChecked: originalPoints.length,
|
|
1085
1218
|
maxError: maxError.toExponential(),
|
|
1086
|
-
failedPoints: errors.slice(0, 5)
|
|
1087
|
-
}
|
|
1219
|
+
failedPoints: errors.slice(0, 5),
|
|
1220
|
+
},
|
|
1088
1221
|
};
|
|
1089
1222
|
} catch (e) {
|
|
1090
1223
|
return {
|
|
1091
1224
|
valid: false,
|
|
1092
1225
|
error: new Decimal(Infinity),
|
|
1093
1226
|
tolerance,
|
|
1094
|
-
message: `E2E verification error: ${e.message}
|
|
1227
|
+
message: `E2E verification error: ${e.message}`,
|
|
1095
1228
|
};
|
|
1096
1229
|
}
|
|
1097
1230
|
}
|
|
@@ -1116,7 +1249,9 @@ export function verifyPolygonUnionArea(polygons, expectedArea) {
|
|
|
1116
1249
|
}
|
|
1117
1250
|
|
|
1118
1251
|
const error = totalArea.minus(D(expectedArea)).abs();
|
|
1119
|
-
const relativeError = D(expectedArea).isZero()
|
|
1252
|
+
const relativeError = D(expectedArea).isZero()
|
|
1253
|
+
? error
|
|
1254
|
+
: error.div(D(expectedArea));
|
|
1120
1255
|
const valid = relativeError.lessThan(tolerance);
|
|
1121
1256
|
|
|
1122
1257
|
return {
|
|
@@ -1129,20 +1264,28 @@ export function verifyPolygonUnionArea(polygons, expectedArea) {
|
|
|
1129
1264
|
details: {
|
|
1130
1265
|
totalArea: totalArea.toString(),
|
|
1131
1266
|
expectedArea: expectedArea.toString(),
|
|
1132
|
-
polygonCount: polygons.length
|
|
1133
|
-
}
|
|
1267
|
+
polygonCount: polygons.length,
|
|
1268
|
+
},
|
|
1134
1269
|
};
|
|
1135
1270
|
} catch (e) {
|
|
1136
1271
|
return {
|
|
1137
1272
|
valid: false,
|
|
1138
1273
|
error: new Decimal(Infinity),
|
|
1139
1274
|
tolerance,
|
|
1140
|
-
message: `Union verification error: ${e.message}
|
|
1275
|
+
message: `Union verification error: ${e.message}`,
|
|
1141
1276
|
};
|
|
1142
1277
|
}
|
|
1143
1278
|
}
|
|
1144
1279
|
|
|
1145
|
-
|
|
1280
|
+
/**
|
|
1281
|
+
* Check if point is inside edge (for difference computation).
|
|
1282
|
+
* E2E helper function.
|
|
1283
|
+
* @private
|
|
1284
|
+
* @param {{x: Decimal|number, y: Decimal|number}} point - Point to test
|
|
1285
|
+
* @param {{x: Decimal|number, y: Decimal|number}} edgeStart - Edge start point
|
|
1286
|
+
* @param {{x: Decimal|number, y: Decimal|number}} edgeEnd - Edge end point
|
|
1287
|
+
* @returns {boolean} True if point is on the inside side of the edge
|
|
1288
|
+
*/
|
|
1146
1289
|
function isInsideEdgeE2E(point, edgeStart, edgeEnd) {
|
|
1147
1290
|
const px = D(point.x).toNumber();
|
|
1148
1291
|
const py = D(point.y).toNumber();
|
|
@@ -1154,12 +1297,25 @@ function isInsideEdgeE2E(point, edgeStart, edgeEnd) {
|
|
|
1154
1297
|
return (ex - sx) * (py - sy) - (ey - sy) * (px - sx) >= 0;
|
|
1155
1298
|
}
|
|
1156
1299
|
|
|
1157
|
-
|
|
1300
|
+
/**
|
|
1301
|
+
* Compute line intersection for difference computation.
|
|
1302
|
+
* E2E helper function.
|
|
1303
|
+
* @private
|
|
1304
|
+
* @param {{x: Decimal|number, y: Decimal|number}} p1 - First point on line 1
|
|
1305
|
+
* @param {{x: Decimal|number, y: Decimal|number}} p2 - Second point on line 1
|
|
1306
|
+
* @param {{x: Decimal|number, y: Decimal|number}} p3 - First point on line 2
|
|
1307
|
+
* @param {{x: Decimal|number, y: Decimal|number}} p4 - Second point on line 2
|
|
1308
|
+
* @returns {{x: Decimal, y: Decimal}} Intersection point
|
|
1309
|
+
*/
|
|
1158
1310
|
function lineIntersectE2E(p1, p2, p3, p4) {
|
|
1159
|
-
const x1 = D(p1.x).toNumber(),
|
|
1160
|
-
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1311
|
+
const x1 = D(p1.x).toNumber(),
|
|
1312
|
+
y1 = D(p1.y).toNumber();
|
|
1313
|
+
const x2 = D(p2.x).toNumber(),
|
|
1314
|
+
y2 = D(p2.y).toNumber();
|
|
1315
|
+
const x3 = D(p3.x).toNumber(),
|
|
1316
|
+
y3 = D(p3.y).toNumber();
|
|
1317
|
+
const x4 = D(p4.x).toNumber(),
|
|
1318
|
+
y4 = D(p4.y).toNumber();
|
|
1163
1319
|
|
|
1164
1320
|
const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
|
|
1165
1321
|
if (Math.abs(denom) < 1e-10) {
|
|
@@ -1170,7 +1326,7 @@ function lineIntersectE2E(p1, p2, p3, p4) {
|
|
|
1170
1326
|
|
|
1171
1327
|
return {
|
|
1172
1328
|
x: D(x1 + t * (x2 - x1)),
|
|
1173
|
-
y: D(y1 + t * (y2 - y1))
|
|
1329
|
+
y: D(y1 + t * (y2 - y1)),
|
|
1174
1330
|
};
|
|
1175
1331
|
}
|
|
1176
1332
|
|
|
@@ -1189,29 +1345,37 @@ function lineIntersectE2E(p1, p2, p3, p4) {
|
|
|
1189
1345
|
* @returns {Object} Comprehensive verification report
|
|
1190
1346
|
*/
|
|
1191
1347
|
export function verifyPathTransformation(params) {
|
|
1192
|
-
const {
|
|
1348
|
+
const {
|
|
1349
|
+
matrix,
|
|
1350
|
+
originalPath: _originalPath,
|
|
1351
|
+
transformedPath: _transformedPath,
|
|
1352
|
+
testPoints = [],
|
|
1353
|
+
} = params;
|
|
1193
1354
|
const results = {
|
|
1194
1355
|
allPassed: true,
|
|
1195
|
-
verifications: []
|
|
1356
|
+
verifications: [],
|
|
1196
1357
|
};
|
|
1197
1358
|
|
|
1198
1359
|
// Verify matrix is valid
|
|
1199
1360
|
const invResult = verifyMatrixInversion(matrix);
|
|
1200
|
-
results.verifications.push({ name:
|
|
1361
|
+
results.verifications.push({ name: "Matrix Inversion", ...invResult });
|
|
1201
1362
|
if (!invResult.valid) results.allPassed = false;
|
|
1202
1363
|
|
|
1203
1364
|
// Verify round-trip for test points
|
|
1204
1365
|
for (let i = 0; i < testPoints.length; i++) {
|
|
1205
1366
|
const pt = testPoints[i];
|
|
1206
1367
|
const rtResult = verifyTransformRoundTrip(matrix, pt.x, pt.y);
|
|
1207
|
-
results.verifications.push({
|
|
1368
|
+
results.verifications.push({
|
|
1369
|
+
name: `Round-trip Point ${i + 1}`,
|
|
1370
|
+
...rtResult,
|
|
1371
|
+
});
|
|
1208
1372
|
if (!rtResult.valid) results.allPassed = false;
|
|
1209
1373
|
}
|
|
1210
1374
|
|
|
1211
1375
|
// Verify geometry preservation
|
|
1212
1376
|
if (testPoints.length >= 3) {
|
|
1213
1377
|
const geoResult = verifyTransformGeometry(matrix, testPoints);
|
|
1214
|
-
results.verifications.push({ name:
|
|
1378
|
+
results.verifications.push({ name: "Geometry Preservation", ...geoResult });
|
|
1215
1379
|
if (!geoResult.valid) results.allPassed = false;
|
|
1216
1380
|
}
|
|
1217
1381
|
|