@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
package/src/mesh-gradient.js
CHANGED
|
@@ -42,7 +42,15 @@ const SUBDIVISION_THRESHOLD = 2;
|
|
|
42
42
|
* const p = point("3.14159265358979323846", "2.71828182845904523536");
|
|
43
43
|
*/
|
|
44
44
|
export function point(x, y) {
|
|
45
|
-
|
|
45
|
+
if (x === null || x === undefined)
|
|
46
|
+
throw new Error("point: x parameter is required");
|
|
47
|
+
if (y === null || y === undefined)
|
|
48
|
+
throw new Error("point: y parameter is required");
|
|
49
|
+
const dx = D(x);
|
|
50
|
+
const dy = D(y);
|
|
51
|
+
if (!dx.isFinite()) throw new Error(`point: x must be finite, got ${x}`);
|
|
52
|
+
if (!dy.isFinite()) throw new Error(`point: y must be finite, got ${y}`);
|
|
53
|
+
return { x: dx, y: dy };
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
/**
|
|
@@ -66,12 +74,32 @@ export function point(x, y) {
|
|
|
66
74
|
* const blue = color(0, 0, 255, 128);
|
|
67
75
|
*/
|
|
68
76
|
export function color(r, g, b, a = 255) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
if (r === null || r === undefined)
|
|
78
|
+
throw new Error("color: r parameter is required");
|
|
79
|
+
if (g === null || g === undefined)
|
|
80
|
+
throw new Error("color: g parameter is required");
|
|
81
|
+
if (b === null || b === undefined)
|
|
82
|
+
throw new Error("color: b parameter is required");
|
|
83
|
+
if (!Number.isFinite(r)) throw new Error(`color: r must be finite, got ${r}`);
|
|
84
|
+
if (!Number.isFinite(g)) throw new Error(`color: g must be finite, got ${g}`);
|
|
85
|
+
if (!Number.isFinite(b)) throw new Error(`color: b must be finite, got ${b}`);
|
|
86
|
+
if (!Number.isFinite(a)) throw new Error(`color: a must be finite, got ${a}`);
|
|
87
|
+
|
|
88
|
+
const rVal = Math.round(r);
|
|
89
|
+
const gVal = Math.round(g);
|
|
90
|
+
const bVal = Math.round(b);
|
|
91
|
+
const aVal = Math.round(a);
|
|
92
|
+
|
|
93
|
+
if (rVal < 0 || rVal > 255)
|
|
94
|
+
throw new Error(`color: r must be 0-255, got ${rVal}`);
|
|
95
|
+
if (gVal < 0 || gVal > 255)
|
|
96
|
+
throw new Error(`color: g must be 0-255, got ${gVal}`);
|
|
97
|
+
if (bVal < 0 || bVal > 255)
|
|
98
|
+
throw new Error(`color: b must be 0-255, got ${bVal}`);
|
|
99
|
+
if (aVal < 0 || aVal > 255)
|
|
100
|
+
throw new Error(`color: a must be 0-255, got ${aVal}`);
|
|
101
|
+
|
|
102
|
+
return { r: rVal, g: gVal, b: bVal, a: aVal };
|
|
75
103
|
}
|
|
76
104
|
|
|
77
105
|
/**
|
|
@@ -105,20 +133,26 @@ export function color(r, g, b, a = 255) {
|
|
|
105
133
|
* // {r: 0, g: 255, b: 255, a: 255}
|
|
106
134
|
*/
|
|
107
135
|
export function parseColor(colorStr, opacity = 1) {
|
|
108
|
-
if (!colorStr) return color(0, 0, 0, 255);
|
|
136
|
+
if (!colorStr || typeof colorStr !== "string") return color(0, 0, 0, 255);
|
|
137
|
+
if (!Number.isFinite(opacity))
|
|
138
|
+
throw new Error(`parseColor: opacity must be finite, got ${opacity}`);
|
|
139
|
+
if (opacity < 0 || opacity > 1)
|
|
140
|
+
throw new Error(`parseColor: opacity must be 0-1, got ${opacity}`);
|
|
109
141
|
|
|
110
142
|
// Handle rgb() and rgba()
|
|
111
143
|
const rgbMatch = colorStr.match(
|
|
112
144
|
/rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/i,
|
|
113
145
|
);
|
|
114
146
|
if (rgbMatch) {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
);
|
|
147
|
+
const r = parseInt(rgbMatch[1], 10);
|
|
148
|
+
const g = parseInt(rgbMatch[2], 10);
|
|
149
|
+
const b = parseInt(rgbMatch[3], 10);
|
|
150
|
+
if (!Number.isFinite(r) || !Number.isFinite(g) || !Number.isFinite(b)) {
|
|
151
|
+
return color(0, 0, 0, 255 * opacity);
|
|
152
|
+
}
|
|
153
|
+
const a = rgbMatch[4] !== undefined ? parseFloat(rgbMatch[4]) : 1;
|
|
154
|
+
if (!Number.isFinite(a)) return color(r, g, b, 255 * opacity);
|
|
155
|
+
return color(r, g, b, Math.min(255, a * 255 * opacity));
|
|
122
156
|
}
|
|
123
157
|
|
|
124
158
|
// Handle hex colors
|
|
@@ -199,7 +233,18 @@ export function parseColor(colorStr, opacity = 1) {
|
|
|
199
233
|
* // {r: 100, g: 150, b: 200, a: 64}
|
|
200
234
|
*/
|
|
201
235
|
export function lerpColor(c1, c2, t) {
|
|
236
|
+
if (!c1 || !c2) throw new Error("lerpColor: c1 and c2 are required");
|
|
237
|
+
if (!("r" in c1 && "g" in c1 && "b" in c1 && "a" in c1))
|
|
238
|
+
throw new Error("lerpColor: c1 must have r, g, b, a properties");
|
|
239
|
+
if (!("r" in c2 && "g" in c2 && "b" in c2 && "a" in c2))
|
|
240
|
+
throw new Error("lerpColor: c2 must have r, g, b, a properties");
|
|
241
|
+
if (t === null || t === undefined)
|
|
242
|
+
throw new Error("lerpColor: t parameter is required");
|
|
243
|
+
|
|
202
244
|
const tNum = Number(t);
|
|
245
|
+
if (!Number.isFinite(tNum))
|
|
246
|
+
throw new Error(`lerpColor: t must be finite, got ${t}`);
|
|
247
|
+
|
|
203
248
|
const mt = 1 - tNum;
|
|
204
249
|
return color(
|
|
205
250
|
c1.r * mt + c2.r * tNum,
|
|
@@ -238,8 +283,28 @@ export function lerpColor(c1, c2, t) {
|
|
|
238
283
|
* // Average of all four corners
|
|
239
284
|
*/
|
|
240
285
|
export function bilinearColor(c00, c10, c01, c11, u, v) {
|
|
286
|
+
if (!c00 || !c10 || !c01 || !c11)
|
|
287
|
+
throw new Error("bilinearColor: all corner colors are required");
|
|
288
|
+
if (!("r" in c00 && "g" in c00 && "b" in c00 && "a" in c00))
|
|
289
|
+
throw new Error("bilinearColor: c00 must have r, g, b, a properties");
|
|
290
|
+
if (!("r" in c10 && "g" in c10 && "b" in c10 && "a" in c10))
|
|
291
|
+
throw new Error("bilinearColor: c10 must have r, g, b, a properties");
|
|
292
|
+
if (!("r" in c01 && "g" in c01 && "b" in c01 && "a" in c01))
|
|
293
|
+
throw new Error("bilinearColor: c01 must have r, g, b, a properties");
|
|
294
|
+
if (!("r" in c11 && "g" in c11 && "b" in c11 && "a" in c11))
|
|
295
|
+
throw new Error("bilinearColor: c11 must have r, g, b, a properties");
|
|
296
|
+
if (u === null || u === undefined)
|
|
297
|
+
throw new Error("bilinearColor: u parameter is required");
|
|
298
|
+
if (v === null || v === undefined)
|
|
299
|
+
throw new Error("bilinearColor: v parameter is required");
|
|
300
|
+
|
|
241
301
|
const uNum = Number(u);
|
|
242
302
|
const vNum = Number(v);
|
|
303
|
+
if (!Number.isFinite(uNum))
|
|
304
|
+
throw new Error(`bilinearColor: u must be finite, got ${u}`);
|
|
305
|
+
if (!Number.isFinite(vNum))
|
|
306
|
+
throw new Error(`bilinearColor: v must be finite, got ${v}`);
|
|
307
|
+
|
|
243
308
|
const mu = 1 - uNum;
|
|
244
309
|
const mv = 1 - vNum;
|
|
245
310
|
|
|
@@ -302,21 +367,38 @@ export function bilinearColor(c00, c10, c01, c11, u, v) {
|
|
|
302
367
|
* }
|
|
303
368
|
*/
|
|
304
369
|
export function evalCubicBezier(p0, p1, p2, p3, t) {
|
|
305
|
-
|
|
370
|
+
if (!p0 || !p1 || !p2 || !p3)
|
|
371
|
+
throw new Error("evalCubicBezier: all points are required");
|
|
372
|
+
if (!("x" in p0 && "y" in p0))
|
|
373
|
+
throw new Error("evalCubicBezier: p0 must have x, y properties");
|
|
374
|
+
if (!("x" in p1 && "y" in p1))
|
|
375
|
+
throw new Error("evalCubicBezier: p1 must have x, y properties");
|
|
376
|
+
if (!("x" in p2 && "y" in p2))
|
|
377
|
+
throw new Error("evalCubicBezier: p2 must have x, y properties");
|
|
378
|
+
if (!("x" in p3 && "y" in p3))
|
|
379
|
+
throw new Error("evalCubicBezier: p3 must have x, y properties");
|
|
380
|
+
if (t === null || t === undefined)
|
|
381
|
+
throw new Error("evalCubicBezier: t parameter is required");
|
|
382
|
+
|
|
383
|
+
const tDecimal = D(t);
|
|
384
|
+
if (!tDecimal.isFinite())
|
|
385
|
+
throw new Error(`evalCubicBezier: t must be finite, got ${t}`);
|
|
386
|
+
|
|
387
|
+
const mt = D(1).minus(tDecimal);
|
|
306
388
|
const mt2 = mt.mul(mt);
|
|
307
389
|
const mt3 = mt2.mul(mt);
|
|
308
|
-
const t2 =
|
|
309
|
-
const t3 = t2.mul(
|
|
390
|
+
const t2 = tDecimal.mul(tDecimal);
|
|
391
|
+
const t3 = t2.mul(tDecimal);
|
|
310
392
|
|
|
311
393
|
return {
|
|
312
394
|
x: mt3
|
|
313
395
|
.mul(p0.x)
|
|
314
|
-
.plus(D(3).mul(mt2).mul(
|
|
396
|
+
.plus(D(3).mul(mt2).mul(tDecimal).mul(p1.x))
|
|
315
397
|
.plus(D(3).mul(mt).mul(t2).mul(p2.x))
|
|
316
398
|
.plus(t3.mul(p3.x)),
|
|
317
399
|
y: mt3
|
|
318
400
|
.mul(p0.y)
|
|
319
|
-
.plus(D(3).mul(mt2).mul(
|
|
401
|
+
.plus(D(3).mul(mt2).mul(tDecimal).mul(p1.y))
|
|
320
402
|
.plus(D(3).mul(mt).mul(t2).mul(p2.y))
|
|
321
403
|
.plus(t3.mul(p3.y)),
|
|
322
404
|
};
|
|
@@ -358,7 +440,24 @@ export function evalCubicBezier(p0, p1, p2, p3, t) {
|
|
|
358
440
|
* }
|
|
359
441
|
*/
|
|
360
442
|
export function splitBezier(curve) {
|
|
443
|
+
if (!Array.isArray(curve))
|
|
444
|
+
throw new Error("splitBezier: curve must be an array");
|
|
445
|
+
if (curve.length !== 4)
|
|
446
|
+
throw new Error(
|
|
447
|
+
`splitBezier: curve must have exactly 4 points, got ${curve.length}`,
|
|
448
|
+
);
|
|
449
|
+
|
|
361
450
|
const [p0, p1, p2, p3] = curve;
|
|
451
|
+
if (!p0 || !p1 || !p2 || !p3)
|
|
452
|
+
throw new Error("splitBezier: all curve points must be defined");
|
|
453
|
+
if (!("x" in p0 && "y" in p0))
|
|
454
|
+
throw new Error("splitBezier: p0 must have x, y properties");
|
|
455
|
+
if (!("x" in p1 && "y" in p1))
|
|
456
|
+
throw new Error("splitBezier: p1 must have x, y properties");
|
|
457
|
+
if (!("x" in p2 && "y" in p2))
|
|
458
|
+
throw new Error("splitBezier: p2 must have x, y properties");
|
|
459
|
+
if (!("x" in p3 && "y" in p3))
|
|
460
|
+
throw new Error("splitBezier: p3 must have x, y properties");
|
|
362
461
|
|
|
363
462
|
// De Casteljau subdivision at t=0.5
|
|
364
463
|
const mid = (a, b) => ({
|
|
@@ -434,6 +533,55 @@ export class CoonsPatch {
|
|
|
434
533
|
* );
|
|
435
534
|
*/
|
|
436
535
|
constructor(top, right, bottom, left, colors) {
|
|
536
|
+
if (!Array.isArray(top) || top.length !== 4)
|
|
537
|
+
throw new Error("CoonsPatch: top must be an array of 4 points");
|
|
538
|
+
if (!Array.isArray(right) || right.length !== 4)
|
|
539
|
+
throw new Error("CoonsPatch: right must be an array of 4 points");
|
|
540
|
+
if (!Array.isArray(bottom) || bottom.length !== 4)
|
|
541
|
+
throw new Error("CoonsPatch: bottom must be an array of 4 points");
|
|
542
|
+
if (!Array.isArray(left) || left.length !== 4)
|
|
543
|
+
throw new Error("CoonsPatch: left must be an array of 4 points");
|
|
544
|
+
|
|
545
|
+
// Validate all points have x, y properties
|
|
546
|
+
for (let i = 0; i < 4; i++) {
|
|
547
|
+
if (!top[i] || !("x" in top[i]) || !("y" in top[i]))
|
|
548
|
+
throw new Error(`CoonsPatch: top[${i}] must have x, y properties`);
|
|
549
|
+
if (!right[i] || !("x" in right[i]) || !("y" in right[i]))
|
|
550
|
+
throw new Error(`CoonsPatch: right[${i}] must have x, y properties`);
|
|
551
|
+
if (!bottom[i] || !("x" in bottom[i]) || !("y" in bottom[i]))
|
|
552
|
+
throw new Error(`CoonsPatch: bottom[${i}] must have x, y properties`);
|
|
553
|
+
if (!left[i] || !("x" in left[i]) || !("y" in left[i]))
|
|
554
|
+
throw new Error(`CoonsPatch: left[${i}] must have x, y properties`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (
|
|
558
|
+
!Array.isArray(colors) ||
|
|
559
|
+
colors.length !== 2 ||
|
|
560
|
+
!Array.isArray(colors[0]) ||
|
|
561
|
+
colors[0].length !== 2 ||
|
|
562
|
+
!Array.isArray(colors[1]) ||
|
|
563
|
+
colors[1].length !== 2
|
|
564
|
+
) {
|
|
565
|
+
throw new Error("CoonsPatch: colors must be a 2x2 array");
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Validate all colors have r, g, b, a properties
|
|
569
|
+
for (let i = 0; i < 2; i++) {
|
|
570
|
+
for (let j = 0; j < 2; j++) {
|
|
571
|
+
if (
|
|
572
|
+
!colors[i][j] ||
|
|
573
|
+
!("r" in colors[i][j]) ||
|
|
574
|
+
!("g" in colors[i][j]) ||
|
|
575
|
+
!("b" in colors[i][j]) ||
|
|
576
|
+
!("a" in colors[i][j])
|
|
577
|
+
) {
|
|
578
|
+
throw new Error(
|
|
579
|
+
`CoonsPatch: colors[${i}][${j}] must have r, g, b, a properties`,
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
437
585
|
this.top = top;
|
|
438
586
|
this.right = right;
|
|
439
587
|
this.bottom = bottom;
|
|
@@ -482,11 +630,23 @@ export class CoonsPatch {
|
|
|
482
630
|
* }
|
|
483
631
|
*/
|
|
484
632
|
evaluate(u, v) {
|
|
633
|
+
if (u === null || u === undefined)
|
|
634
|
+
throw new Error("CoonsPatch.evaluate: u parameter is required");
|
|
635
|
+
if (v === null || v === undefined)
|
|
636
|
+
throw new Error("CoonsPatch.evaluate: v parameter is required");
|
|
637
|
+
|
|
638
|
+
const uDecimal = D(u);
|
|
639
|
+
const vDecimal = D(v);
|
|
640
|
+
if (!uDecimal.isFinite())
|
|
641
|
+
throw new Error(`CoonsPatch.evaluate: u must be finite, got ${u}`);
|
|
642
|
+
if (!vDecimal.isFinite())
|
|
643
|
+
throw new Error(`CoonsPatch.evaluate: v must be finite, got ${v}`);
|
|
644
|
+
|
|
485
645
|
// Boundary curves
|
|
486
|
-
const Lc = evalCubicBezier(...this.top,
|
|
487
|
-
const Ld = evalCubicBezier(...this.bottom,
|
|
488
|
-
const La = evalCubicBezier(...this.left,
|
|
489
|
-
const Lb = evalCubicBezier(...this.right,
|
|
646
|
+
const Lc = evalCubicBezier(...this.top, uDecimal); // L_c(u,0)
|
|
647
|
+
const Ld = evalCubicBezier(...this.bottom, uDecimal); // L_d(u,1)
|
|
648
|
+
const La = evalCubicBezier(...this.left, vDecimal); // L_a(0,v)
|
|
649
|
+
const Lb = evalCubicBezier(...this.right, vDecimal); // L_b(1,v)
|
|
490
650
|
|
|
491
651
|
// Corner points
|
|
492
652
|
const P00 = this.top[0];
|
|
@@ -496,30 +656,30 @@ export class CoonsPatch {
|
|
|
496
656
|
|
|
497
657
|
// Coons patch formula: S(u,v) = Lc(u) + Ld(u) - B(u,v)
|
|
498
658
|
// where B is bilinear interpolation of corners
|
|
499
|
-
const mu = D(1).minus(
|
|
500
|
-
const mv = D(1).minus(
|
|
659
|
+
const mu = D(1).minus(uDecimal);
|
|
660
|
+
const mv = D(1).minus(vDecimal);
|
|
501
661
|
|
|
502
662
|
// Ruled surface in u direction
|
|
503
|
-
const Sc_x = mv.mul(Lc.x).plus(
|
|
504
|
-
const Sc_y = mv.mul(Lc.y).plus(
|
|
663
|
+
const Sc_x = mv.mul(Lc.x).plus(vDecimal.mul(Ld.x));
|
|
664
|
+
const Sc_y = mv.mul(Lc.y).plus(vDecimal.mul(Ld.y));
|
|
505
665
|
|
|
506
666
|
// Ruled surface in v direction
|
|
507
|
-
const Sd_x = mu.mul(La.x).plus(
|
|
508
|
-
const Sd_y = mu.mul(La.y).plus(
|
|
667
|
+
const Sd_x = mu.mul(La.x).plus(uDecimal.mul(Lb.x));
|
|
668
|
+
const Sd_y = mu.mul(La.y).plus(uDecimal.mul(Lb.y));
|
|
509
669
|
|
|
510
670
|
// Bilinear interpolation of corners
|
|
511
671
|
const B_x = mu
|
|
512
672
|
.mul(mv)
|
|
513
673
|
.mul(P00.x)
|
|
514
|
-
.plus(
|
|
515
|
-
.plus(mu.mul(
|
|
516
|
-
.plus(
|
|
674
|
+
.plus(uDecimal.mul(mv).mul(P10.x))
|
|
675
|
+
.plus(mu.mul(vDecimal).mul(P01.x))
|
|
676
|
+
.plus(uDecimal.mul(vDecimal).mul(P11.x));
|
|
517
677
|
const B_y = mu
|
|
518
678
|
.mul(mv)
|
|
519
679
|
.mul(P00.y)
|
|
520
|
-
.plus(
|
|
521
|
-
.plus(mu.mul(
|
|
522
|
-
.plus(
|
|
680
|
+
.plus(uDecimal.mul(mv).mul(P10.y))
|
|
681
|
+
.plus(mu.mul(vDecimal).mul(P01.y))
|
|
682
|
+
.plus(uDecimal.mul(vDecimal).mul(P11.y));
|
|
523
683
|
|
|
524
684
|
// Coons formula: Sc + Sd - B
|
|
525
685
|
const pt = {
|
|
@@ -533,8 +693,8 @@ export class CoonsPatch {
|
|
|
533
693
|
this.colors[0][1],
|
|
534
694
|
this.colors[1][0],
|
|
535
695
|
this.colors[1][1],
|
|
536
|
-
|
|
537
|
-
|
|
696
|
+
uDecimal,
|
|
697
|
+
vDecimal,
|
|
538
698
|
);
|
|
539
699
|
|
|
540
700
|
return { point: pt, color: col };
|
|
@@ -723,12 +883,18 @@ export class CoonsPatch {
|
|
|
723
883
|
...this.bottom,
|
|
724
884
|
...this.left,
|
|
725
885
|
];
|
|
886
|
+
|
|
887
|
+
if (allPoints.length === 0)
|
|
888
|
+
throw new Error("CoonsPatch.getBBox: no points in patch");
|
|
889
|
+
|
|
726
890
|
let minX = allPoints[0].x,
|
|
727
891
|
maxX = allPoints[0].x;
|
|
728
892
|
let minY = allPoints[0].y,
|
|
729
893
|
maxY = allPoints[0].y;
|
|
730
894
|
|
|
731
895
|
for (const p of allPoints) {
|
|
896
|
+
if (!p || !("x" in p) || !("y" in p))
|
|
897
|
+
throw new Error("CoonsPatch.getBBox: invalid point in patch");
|
|
732
898
|
if (p.x.lt(minX)) minX = p.x;
|
|
733
899
|
if (p.x.gt(maxX)) maxX = p.x;
|
|
734
900
|
if (p.y.lt(minY)) minY = p.y;
|
|
@@ -797,71 +963,30 @@ export class CoonsPatch {
|
|
|
797
963
|
* console.log(`Parsed ${meshData.patches.length} patches`);
|
|
798
964
|
*/
|
|
799
965
|
export function parseMeshGradient(meshGradientDef) {
|
|
966
|
+
if (!meshGradientDef || typeof meshGradientDef !== "object") {
|
|
967
|
+
throw new Error("parseMeshGradient: meshGradientDef must be an object");
|
|
968
|
+
}
|
|
969
|
+
|
|
800
970
|
const x = D(meshGradientDef.x || 0);
|
|
801
971
|
const y = D(meshGradientDef.y || 0);
|
|
972
|
+
if (!x.isFinite())
|
|
973
|
+
throw new Error(
|
|
974
|
+
`parseMeshGradient: x must be finite, got ${meshGradientDef.x}`,
|
|
975
|
+
);
|
|
976
|
+
if (!y.isFinite())
|
|
977
|
+
throw new Error(
|
|
978
|
+
`parseMeshGradient: y must be finite, got ${meshGradientDef.y}`,
|
|
979
|
+
);
|
|
980
|
+
|
|
802
981
|
const type = meshGradientDef.type || "bilinear";
|
|
803
982
|
const gradientUnits = meshGradientDef.gradientUnits || "userSpaceOnUse";
|
|
804
983
|
const gradientTransform = meshGradientDef.gradientTransform || null;
|
|
805
984
|
|
|
806
985
|
const patches = [];
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
// Build the mesh grid
|
|
810
|
-
// nodes[row][col] = point, colors[row][col] = color
|
|
811
|
-
const nodes = [[point(x, y)]];
|
|
812
|
-
const colors = [[]];
|
|
813
|
-
|
|
814
|
-
const _currentX = x;
|
|
815
|
-
const _currentY = y;
|
|
816
|
-
|
|
817
|
-
for (let rowIdx = 0; rowIdx < meshRows.length; rowIdx++) {
|
|
818
|
-
const row = meshRows[rowIdx];
|
|
819
|
-
const meshPatches = row.meshpatches || [];
|
|
820
|
-
|
|
821
|
-
if (rowIdx > 0) {
|
|
822
|
-
nodes.push([]);
|
|
823
|
-
colors.push([]);
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
for (let colIdx = 0; colIdx < meshPatches.length; colIdx++) {
|
|
827
|
-
const patch = meshPatches[colIdx];
|
|
828
|
-
const stops = patch.stops || [];
|
|
829
|
-
|
|
830
|
-
// Each patch has up to 4 stops defining edges
|
|
831
|
-
for (let stopIdx = 0; stopIdx < stops.length; stopIdx++) {
|
|
832
|
-
const stop = stops[stopIdx];
|
|
833
|
-
const pathData = stop.path || "";
|
|
834
|
-
const stopColor = stop.color
|
|
835
|
-
? parseColor(stop.color, stop.opacity || 1)
|
|
836
|
-
: null;
|
|
837
|
-
|
|
838
|
-
// Parse path command (c/C/l/L for bezier/line)
|
|
839
|
-
const pathMatch = pathData.match(/^\s*([cClL])\s*(.*)/);
|
|
840
|
-
if (pathMatch) {
|
|
841
|
-
const cmd = pathMatch[1];
|
|
842
|
-
const _coords = pathMatch[2]
|
|
843
|
-
.trim()
|
|
844
|
-
.split(/[\s,]+/)
|
|
845
|
-
.map(Number);
|
|
846
|
-
|
|
847
|
-
if (cmd === "c" || cmd === "C") {
|
|
848
|
-
// Cubic bezier: c x1,y1 x2,y2 x3,y3 (relative)
|
|
849
|
-
// or C x1,y1 x2,y2 x3,y3 (absolute)
|
|
850
|
-
const _isRelative = cmd === "c";
|
|
851
|
-
// Store bezier control points for patch construction
|
|
852
|
-
} else if (cmd === "l" || cmd === "L") {
|
|
853
|
-
// Line: l dx,dy (relative) or L x,y (absolute)
|
|
854
|
-
const _isRelative = cmd === "l";
|
|
855
|
-
}
|
|
856
|
-
}
|
|
986
|
+
const _meshRows = meshGradientDef.meshrows || [];
|
|
857
987
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
// Corner colors
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
}
|
|
988
|
+
// NOTE: This is a stub implementation - full parsing logic needs to be added
|
|
989
|
+
// The current implementation only extracts metadata without building actual patches
|
|
865
990
|
|
|
866
991
|
return {
|
|
867
992
|
patches,
|
|
@@ -903,6 +1028,10 @@ export function parseMeshGradient(meshGradientDef) {
|
|
|
903
1028
|
* const imageData = rasterizeMeshGradient(meshData, 800, 600);
|
|
904
1029
|
*/
|
|
905
1030
|
export function parseMeshGradientElement(element) {
|
|
1031
|
+
if (!element || typeof element !== "object" || !element.getAttribute) {
|
|
1032
|
+
throw new Error("parseMeshGradientElement: element must be a DOM element");
|
|
1033
|
+
}
|
|
1034
|
+
|
|
906
1035
|
const data = {
|
|
907
1036
|
x: element.getAttribute("x") || "0",
|
|
908
1037
|
y: element.getAttribute("y") || "0",
|
|
@@ -926,10 +1055,13 @@ export function parseMeshGradientElement(element) {
|
|
|
926
1055
|
const colorMatch = style.match(/stop-color:\s*([^;]+)/);
|
|
927
1056
|
const opacityMatch = style.match(/stop-opacity:\s*([^;]+)/);
|
|
928
1057
|
|
|
1058
|
+
const opacityValue = opacityMatch ? parseFloat(opacityMatch[1]) : 1;
|
|
1059
|
+
const validOpacity = Number.isFinite(opacityValue) ? opacityValue : 1;
|
|
1060
|
+
|
|
929
1061
|
patchData.stops.push({
|
|
930
1062
|
path: stop.getAttribute("path") || "",
|
|
931
1063
|
color: colorMatch ? colorMatch[1].trim() : null,
|
|
932
|
-
opacity:
|
|
1064
|
+
opacity: validOpacity,
|
|
933
1065
|
});
|
|
934
1066
|
});
|
|
935
1067
|
|
|
@@ -984,13 +1116,30 @@ export function parseMeshGradientElement(element) {
|
|
|
984
1116
|
* const imageData = rasterizeMeshGradient(meshData, 1920, 1080, { samples: 32 });
|
|
985
1117
|
*/
|
|
986
1118
|
export function rasterizeMeshGradient(meshData, width, height, options = {}) {
|
|
1119
|
+
if (!meshData || typeof meshData !== "object") {
|
|
1120
|
+
throw new Error("rasterizeMeshGradient: meshData must be an object");
|
|
1121
|
+
}
|
|
1122
|
+
if (!Number.isInteger(width) || width <= 0) {
|
|
1123
|
+
throw new Error(
|
|
1124
|
+
`rasterizeMeshGradient: width must be a positive integer, got ${width}`,
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
if (!Number.isInteger(height) || height <= 0) {
|
|
1128
|
+
throw new Error(
|
|
1129
|
+
`rasterizeMeshGradient: height must be a positive integer, got ${height}`,
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
987
1133
|
const { samples = DEFAULT_PATCH_SAMPLES } = options;
|
|
988
1134
|
|
|
989
1135
|
// Create image data buffer
|
|
990
1136
|
const imageData = new Uint8ClampedArray(width * height * 4);
|
|
991
1137
|
|
|
992
1138
|
// For each patch, rasterize to the buffer
|
|
993
|
-
|
|
1139
|
+
const patches = meshData.patches || [];
|
|
1140
|
+
for (const patch of patches) {
|
|
1141
|
+
if (!patch)
|
|
1142
|
+
throw new Error("rasterizeMeshGradient: patch is null or undefined");
|
|
994
1143
|
rasterizePatch(patch, imageData, width, height, samples);
|
|
995
1144
|
}
|
|
996
1145
|
|
|
@@ -1012,14 +1161,37 @@ export function rasterizeMeshGradient(meshData, width, height, options = {}) {
|
|
|
1012
1161
|
* @param {number} height - Image height in pixels
|
|
1013
1162
|
* @param {number} samples - Subdivision control parameter (unused in adaptive mode)
|
|
1014
1163
|
*/
|
|
1015
|
-
function rasterizePatch(patch, imageData, width, height,
|
|
1164
|
+
function rasterizePatch(patch, imageData, width, height, _samples) {
|
|
1165
|
+
if (
|
|
1166
|
+
!patch ||
|
|
1167
|
+
typeof patch.isFlat !== "function" ||
|
|
1168
|
+
typeof patch.subdivide !== "function"
|
|
1169
|
+
) {
|
|
1170
|
+
throw new Error(
|
|
1171
|
+
"rasterizePatch: patch must be a valid CoonsPatch instance",
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
if (!imageData || !(imageData instanceof Uint8ClampedArray)) {
|
|
1175
|
+
throw new Error("rasterizePatch: imageData must be a Uint8ClampedArray");
|
|
1176
|
+
}
|
|
1177
|
+
if (!Number.isInteger(width) || width <= 0) {
|
|
1178
|
+
throw new Error(
|
|
1179
|
+
`rasterizePatch: width must be a positive integer, got ${width}`,
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
if (!Number.isInteger(height) || height <= 0) {
|
|
1183
|
+
throw new Error(
|
|
1184
|
+
`rasterizePatch: height must be a positive integer, got ${height}`,
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1016
1188
|
// Adaptive subdivision approach
|
|
1017
1189
|
const stack = [patch];
|
|
1018
1190
|
|
|
1019
1191
|
while (stack.length > 0) {
|
|
1020
1192
|
const currentPatch = stack.pop();
|
|
1021
1193
|
|
|
1022
|
-
if (currentPatch.isFlat()
|
|
1194
|
+
if (currentPatch.isFlat()) {
|
|
1023
1195
|
// Render as quad
|
|
1024
1196
|
renderPatchQuad(currentPatch, imageData, width, height);
|
|
1025
1197
|
} else {
|
|
@@ -1047,6 +1219,29 @@ function rasterizePatch(patch, imageData, width, height, samples) {
|
|
|
1047
1219
|
* @param {number} height - Image height in pixels
|
|
1048
1220
|
*/
|
|
1049
1221
|
function renderPatchQuad(patch, imageData, width, height) {
|
|
1222
|
+
if (
|
|
1223
|
+
!patch ||
|
|
1224
|
+
typeof patch.getBBox !== "function" ||
|
|
1225
|
+
typeof patch.evaluate !== "function"
|
|
1226
|
+
) {
|
|
1227
|
+
throw new Error(
|
|
1228
|
+
"renderPatchQuad: patch must be a valid CoonsPatch instance",
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
if (!imageData || !(imageData instanceof Uint8ClampedArray)) {
|
|
1232
|
+
throw new Error("renderPatchQuad: imageData must be a Uint8ClampedArray");
|
|
1233
|
+
}
|
|
1234
|
+
if (!Number.isInteger(width) || width <= 0) {
|
|
1235
|
+
throw new Error(
|
|
1236
|
+
`renderPatchQuad: width must be a positive integer, got ${width}`,
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
if (!Number.isInteger(height) || height <= 0) {
|
|
1240
|
+
throw new Error(
|
|
1241
|
+
`renderPatchQuad: height must be a positive integer, got ${height}`,
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1050
1245
|
const bbox = patch.getBBox();
|
|
1051
1246
|
const minX = Math.max(0, Math.floor(Number(bbox.minX)));
|
|
1052
1247
|
const maxX = Math.min(width - 1, Math.ceil(Number(bbox.maxX)));
|
|
@@ -1057,12 +1252,14 @@ function renderPatchQuad(patch, imageData, width, height) {
|
|
|
1057
1252
|
for (let y = minY; y <= maxY; y++) {
|
|
1058
1253
|
for (let x = minX; x <= maxX; x++) {
|
|
1059
1254
|
// Convert pixel to patch (u,v) coordinates
|
|
1255
|
+
const uDenom = bbox.maxX.minus(bbox.minX);
|
|
1256
|
+
const vDenom = bbox.maxY.minus(bbox.minY);
|
|
1060
1257
|
const u = D(x)
|
|
1061
1258
|
.minus(bbox.minX)
|
|
1062
|
-
.div(
|
|
1259
|
+
.div(uDenom.isZero() ? D(1) : uDenom);
|
|
1063
1260
|
const v = D(y)
|
|
1064
1261
|
.minus(bbox.minY)
|
|
1065
|
-
.div(
|
|
1262
|
+
.div(vDenom.isZero() ? D(1) : vDenom);
|
|
1066
1263
|
|
|
1067
1264
|
if (u.gte(0) && u.lte(1) && v.gte(0) && v.lte(1)) {
|
|
1068
1265
|
const { color: patchColor } = patch.evaluate(u, v);
|
|
@@ -1129,10 +1326,17 @@ function renderPatchQuad(patch, imageData, width, height) {
|
|
|
1129
1326
|
* console.log(`Generated ${polygons.length} polygons`);
|
|
1130
1327
|
*/
|
|
1131
1328
|
export function meshGradientToPolygons(meshData, options = {}) {
|
|
1329
|
+
if (!meshData || typeof meshData !== "object") {
|
|
1330
|
+
throw new Error("meshGradientToPolygons: meshData must be an object");
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1132
1333
|
const { subdivisions = 8 } = options;
|
|
1133
1334
|
const result = [];
|
|
1134
1335
|
|
|
1135
|
-
|
|
1336
|
+
const patches = meshData.patches || [];
|
|
1337
|
+
for (const patch of patches) {
|
|
1338
|
+
if (!patch)
|
|
1339
|
+
throw new Error("meshGradientToPolygons: patch is null or undefined");
|
|
1136
1340
|
const polys = patchToPolygons(patch, subdivisions);
|
|
1137
1341
|
result.push(...polys);
|
|
1138
1342
|
}
|
|
@@ -1153,6 +1357,17 @@ export function meshGradientToPolygons(meshData, options = {}) {
|
|
|
1153
1357
|
* @returns {Array<{polygon: Array, color: Object}>} Array of colored quads
|
|
1154
1358
|
*/
|
|
1155
1359
|
function patchToPolygons(patch, subdivisions) {
|
|
1360
|
+
if (!patch || typeof patch.evaluate !== "function") {
|
|
1361
|
+
throw new Error(
|
|
1362
|
+
"patchToPolygons: patch must be a valid CoonsPatch instance",
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
if (!Number.isInteger(subdivisions) || subdivisions <= 0) {
|
|
1366
|
+
throw new Error(
|
|
1367
|
+
`patchToPolygons: subdivisions must be a positive integer, got ${subdivisions}`,
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1156
1371
|
const result = [];
|
|
1157
1372
|
const step = D(1).div(subdivisions);
|
|
1158
1373
|
|
|
@@ -1234,6 +1449,13 @@ function patchToPolygons(patch, subdivisions) {
|
|
|
1234
1449
|
* const clipped = clipMeshGradient(meshData, viewport, { subdivisions: 32 });
|
|
1235
1450
|
*/
|
|
1236
1451
|
export function clipMeshGradient(meshData, clipPolygon, options = {}) {
|
|
1452
|
+
if (!meshData || typeof meshData !== "object") {
|
|
1453
|
+
throw new Error("clipMeshGradient: meshData must be an object");
|
|
1454
|
+
}
|
|
1455
|
+
if (!Array.isArray(clipPolygon)) {
|
|
1456
|
+
throw new Error("clipMeshGradient: clipPolygon must be an array");
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1237
1459
|
const { subdivisions = 16 } = options;
|
|
1238
1460
|
|
|
1239
1461
|
// First convert mesh to polygons
|
|
@@ -1243,6 +1465,8 @@ export function clipMeshGradient(meshData, clipPolygon, options = {}) {
|
|
|
1243
1465
|
const clippedPolygons = [];
|
|
1244
1466
|
|
|
1245
1467
|
for (const { polygon, color: polyColor } of meshPolygons) {
|
|
1468
|
+
if (!Array.isArray(polygon))
|
|
1469
|
+
throw new Error("clipMeshGradient: polygon must be an array");
|
|
1246
1470
|
const clipped = PolygonClip.polygonIntersection(polygon, clipPolygon);
|
|
1247
1471
|
|
|
1248
1472
|
for (const clippedPoly of clipped) {
|
|
@@ -1287,10 +1511,36 @@ export function clipMeshGradient(meshData, clipPolygon, options = {}) {
|
|
|
1287
1511
|
* ).join('\n');
|
|
1288
1512
|
*/
|
|
1289
1513
|
export function clippedMeshToSVG(clippedPolygons) {
|
|
1514
|
+
if (!Array.isArray(clippedPolygons)) {
|
|
1515
|
+
throw new Error("clippedMeshToSVG: clippedPolygons must be an array");
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1290
1518
|
return clippedPolygons.map(({ polygon, color: polyColor }) => {
|
|
1519
|
+
if (!Array.isArray(polygon))
|
|
1520
|
+
throw new Error("clippedMeshToSVG: polygon must be an array");
|
|
1521
|
+
if (
|
|
1522
|
+
!polyColor ||
|
|
1523
|
+
!(
|
|
1524
|
+
"r" in polyColor &&
|
|
1525
|
+
"g" in polyColor &&
|
|
1526
|
+
"b" in polyColor &&
|
|
1527
|
+
"a" in polyColor
|
|
1528
|
+
)
|
|
1529
|
+
) {
|
|
1530
|
+
throw new Error(
|
|
1531
|
+
"clippedMeshToSVG: polygon must have color with r, g, b, a properties",
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1291
1535
|
let pathData = "";
|
|
1292
1536
|
for (let i = 0; i < polygon.length; i++) {
|
|
1293
1537
|
const p = polygon[i];
|
|
1538
|
+
if (!p || !("x" in p && "y" in p)) {
|
|
1539
|
+
throw new Error(
|
|
1540
|
+
`clippedMeshToSVG: point at index ${i} must have x, y properties`,
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1294
1544
|
if (i === 0) {
|
|
1295
1545
|
pathData += `M ${Number(p.x).toFixed(6)} ${Number(p.y).toFixed(6)}`;
|
|
1296
1546
|
} else {
|