embedded-react 0.3.0 → 0.4.1

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.
@@ -22,16 +22,94 @@
22
22
  // shapes to path ops, bakes viewBox + <G> translate/scale into coordinates, and resolves inherited
23
23
  // paint. The opcodes below MUST stay in sync with er_scene.h.
24
24
 
25
- const VOP_SHAPE = 0;
26
- const VOP_MOVE = 1;
27
- const VOP_LINE = 2;
28
- const VOP_QUAD = 3;
29
- const VOP_CUBIC = 4;
30
- const VOP_ARC = 5;
31
- const VOP_CLOSE = 6;
25
+ // Op-tape vocabulary — exported so the build-time SVG baker (assets/bake-svg.mjs) can reuse it without
26
+ // duplicating the encoding (and without pulling its SVG-parser dependency into this runtime module).
27
+ export const VOP_SHAPE = 0;
28
+ export const VOP_MOVE = 1;
29
+ export const VOP_LINE = 2;
30
+ export const VOP_QUAD = 3;
31
+ export const VOP_CUBIC = 4;
32
+ export const VOP_ARC = 5;
33
+ export const VOP_CLOSE = 6;
34
+
35
+ export const CAP = {butt: 0, round: 1, square: 2};
36
+ export const JOIN = {miter: 0, round: 1, bevel: 2};
37
+
38
+ // Paint record width in the flat paint table: [fill, stroke, strokeWidth, miter, cap, join, fillRule,
39
+ // fillGrad, strokeGrad]. fillGrad/strokeGrad are 1-based indices into the gradient table (0 = solid). MUST
40
+ // match the bridge's VEC_PAINT_STRIDE (native_ui_bridge.c) and the AOT's paint emission.
41
+ export const PAINT_STRIDE = 9;
42
+
43
+ // Gradient table encoding — a gradient descriptor is { type, stops: [{color, offset}], ax, ay, bx, by, r }.
44
+ // type 1 = linear (axis (ax,ay)->(bx,by)), 2 = radial (centre (ax,ay), radius r). The flat float record is
45
+ // [type, stopCount, (color, offset) × GRAD_MAX_STOPS, ax, ay, bx, by, r]; MUST match the bridge's
46
+ // VEC_GRAD_STRIDE and the engine's ER_VGRAD_MAX_STOPS.
47
+ export const GRAD_MAX_STOPS = 8; // MUST match the engine's ER_VGRAD_MAX_STOPS (er_scene.h)
48
+ export const GRAD_STRIDE = 2 + GRAD_MAX_STOPS * 2 + 5; // 23
49
+ export const GRAD_LINEAR = 1;
50
+ export const GRAD_RADIAL = 2;
51
+ export const GRAD_CONIC = 3;
52
+
53
+ /** Linearly interpolates two straight-alpha ARGB8888 colors, per channel. */
54
+ function lerpArgb(c0, c1, t) {
55
+ const lp = sh => {
56
+ const a = (c0 >>> sh) & 0xff;
57
+ const b = (c1 >>> sh) & 0xff;
58
+ return Math.round(a + (b - a) * t) & 0xff;
59
+ };
60
+ return ((lp(24) << 24) | (lp(16) << 16) | (lp(8) << 8) | lp(0)) >>> 0;
61
+ }
32
62
 
33
- const CAP = { butt: 0, round: 1, square: 2 };
34
- const JOIN = { miter: 0, round: 1, bevel: 2 };
63
+ /** Gradient color at position t (stops ascending by offset; clamps to the endpoint colors). */
64
+ function evalStopsAt(stops, t) {
65
+ if (!stops.length) return 0;
66
+ if (t <= stops[0].offset) return stops[0].color >>> 0;
67
+ const last = stops[stops.length - 1];
68
+ if (t >= last.offset) return last.color >>> 0;
69
+ for (let i = 0; i < stops.length - 1; i++) {
70
+ const a = stops[i];
71
+ const b = stops[i + 1];
72
+ if (t <= b.offset) {
73
+ const span = b.offset - a.offset;
74
+ return lerpArgb(
75
+ a.color >>> 0,
76
+ b.color >>> 0,
77
+ span > 0 ? (t - a.offset) / span : 0,
78
+ );
79
+ }
80
+ }
81
+ return last.color >>> 0;
82
+ }
83
+
84
+ /** Reduces a stop list to at most `max` entries, resampling evenly across the stop range when it's over. */
85
+ function capStops(stops, max) {
86
+ if (stops.length <= max) return stops;
87
+ const lo = stops[0].offset;
88
+ const hi = stops[stops.length - 1].offset;
89
+ const out = [];
90
+ for (let i = 0; i < max; i++) {
91
+ const t = max === 1 ? lo : lo + (hi - lo) * (i / (max - 1));
92
+ out.push({color: evalStopsAt(stops, t), offset: t});
93
+ }
94
+ return out;
95
+ }
96
+
97
+ /** Encodes gradient descriptors into the flat float array NativeUI.setVectorOps takes (GRAD_STRIDE/gradient).
98
+ * A gradient with more than GRAD_MAX_STOPS stops is resampled down (not truncated) so it still ramps right. */
99
+ export function encodeVectorGradients(grads) {
100
+ const out = [];
101
+ if (!grads) return out;
102
+ for (const g of grads) {
103
+ const stops = capStops(g.stops || [], GRAD_MAX_STOPS);
104
+ out.push(g.type, stops.length);
105
+ for (let s = 0; s < GRAD_MAX_STOPS; s++) {
106
+ const st = stops[s];
107
+ out.push(st ? st.color >>> 0 : 0, st ? st.offset : 0);
108
+ }
109
+ out.push(g.ax || 0, g.ay || 0, g.bx || 0, g.by || 0, g.r || 0);
110
+ }
111
+ return out;
112
+ }
35
113
 
36
114
  // Minimal named-color set (extend as needed); everything else goes through hex/rgb parsing.
37
115
  const NAMED = {
@@ -70,7 +148,10 @@ function parseColorString(c) {
70
148
  if (s in NAMED) return NAMED[s] >>> 0;
71
149
  if (s[0] === '#') {
72
150
  s = s.slice(1);
73
- let r, g, b, a = 255;
151
+ let r,
152
+ g,
153
+ b,
154
+ a = 255;
74
155
  if (s.length === 3 || s.length === 4) {
75
156
  r = parseInt(s[0] + s[0], 16);
76
157
  g = parseInt(s[1] + s[1], 16);
@@ -89,7 +170,7 @@ function parseColorString(c) {
89
170
  if (s.startsWith('rgb')) {
90
171
  const m = s.match(/rgba?\(([^)]+)\)/);
91
172
  if (m) {
92
- const p = m[1].split(',').map((x) => x.trim());
173
+ const p = m[1].split(',').map(x => x.trim());
93
174
  const r = parseInt(p[0], 10) || 0;
94
175
  const g = parseInt(p[1], 10) || 0;
95
176
  const b = parseInt(p[2], 10) || 0;
@@ -114,7 +195,7 @@ function tokenizePath(d) {
114
195
  let cur = null;
115
196
  while ((m = re.exec(d)) !== null) {
116
197
  if (m[1]) {
117
- cur = { cmd: m[1], args: [] };
198
+ cur = {cmd: m[1], args: []};
118
199
  out.push(cur);
119
200
  } else if (cur) {
120
201
  cur.args.push(parseFloat(m[2]));
@@ -168,7 +249,12 @@ function arcToCubics(x0, y0, rx, ry, phiDeg, largeArc, sweep, x, y) {
168
249
  return a;
169
250
  };
170
251
  const theta1 = ang(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
171
- let dtheta = ang((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);
252
+ let dtheta = ang(
253
+ (x1p - cxp) / rx,
254
+ (y1p - cyp) / ry,
255
+ (-x1p - cxp) / rx,
256
+ (-y1p - cyp) / ry,
257
+ );
172
258
  if (!sweep && dtheta > 0) dtheta -= 2 * Math.PI;
173
259
  if (sweep && dtheta < 0) dtheta += 2 * Math.PI;
174
260
  const segs = Math.ceil(Math.abs(dtheta) / (Math.PI / 2));
@@ -191,7 +277,14 @@ function arcToCubics(x0, y0, rx, ry, phiDeg, largeArc, sweep, x, y) {
191
277
  const d1y = -rx * sinP * sin1 + ry * cosP * cos1;
192
278
  const d2x = -rx * cosP * sin2 - ry * sinP * cos2;
193
279
  const d2y = -rx * sinP * sin2 + ry * cosP * cos2;
194
- out.push([px + t * d1x, py + t * d1y, e2x - t * d2x, e2y - t * d2y, e2x, e2y]);
280
+ out.push([
281
+ px + t * d1x,
282
+ py + t * d1y,
283
+ e2x - t * d2x,
284
+ e2y - t * d2y,
285
+ e2x,
286
+ e2y,
287
+ ]);
195
288
  px = e2x;
196
289
  py = e2y;
197
290
  th = th2;
@@ -219,8 +312,8 @@ export function parsePath(d) {
219
312
  const a = tk.args;
220
313
  const U = C.toUpperCase();
221
314
  let i = 0;
222
- const rx = (v) => (rel ? cx + v : v);
223
- const ry = (v) => (rel ? cy + v : v);
315
+ const rx = v => (rel ? cx + v : v);
316
+ const ry = v => (rel ? cy + v : v);
224
317
  if (U === 'M') {
225
318
  // First pair is moveto; later pairs are implicit linetos.
226
319
  cx = rx(a[i++]);
@@ -313,7 +406,8 @@ export function parsePath(d) {
313
406
  const ex = rx(a[i++]);
314
407
  const ey = ry(a[i++]);
315
408
  const cubics = arcToCubics(cx, cy, arx, ary, rot, laf, swf, ex, ey);
316
- for (const c of cubics) ops.push(VOP_CUBIC, c[0], c[1], c[2], c[3], c[4], c[5]);
409
+ for (const c of cubics)
410
+ ops.push(VOP_CUBIC, c[0], c[1], c[2], c[3], c[4], c[5]);
317
411
  cx = ex;
318
412
  cy = ey;
319
413
  }
@@ -339,7 +433,19 @@ function circleOps(p) {
339
433
  const cy = num(p.cy, 0);
340
434
  const r = num(p.r, 0);
341
435
  // Start at angle 0, full circular arc.
342
- return [VOP_MOVE, cx + r, cy, VOP_ARC, cx, cy, r, 0, 2 * Math.PI, 0, VOP_CLOSE];
436
+ return [
437
+ VOP_MOVE,
438
+ cx + r,
439
+ cy,
440
+ VOP_ARC,
441
+ cx,
442
+ cy,
443
+ r,
444
+ 0,
445
+ 2 * Math.PI,
446
+ 0,
447
+ VOP_CLOSE,
448
+ ];
343
449
  }
344
450
 
345
451
  function ellipseOps(p) {
@@ -357,11 +463,32 @@ function rectOps(p) {
357
463
  const y = num(p.y, 0);
358
464
  const w = num(p.width, 0);
359
465
  const h = num(p.height, 0);
360
- return [VOP_MOVE, x, y, VOP_LINE, x + w, y, VOP_LINE, x + w, y + h, VOP_LINE, x, y + h, VOP_CLOSE];
466
+ return [
467
+ VOP_MOVE,
468
+ x,
469
+ y,
470
+ VOP_LINE,
471
+ x + w,
472
+ y,
473
+ VOP_LINE,
474
+ x + w,
475
+ y + h,
476
+ VOP_LINE,
477
+ x,
478
+ y + h,
479
+ VOP_CLOSE,
480
+ ];
361
481
  }
362
482
 
363
483
  function lineOps(p) {
364
- return [VOP_MOVE, num(p.x1, 0), num(p.y1, 0), VOP_LINE, num(p.x2, 0), num(p.y2, 0)];
484
+ return [
485
+ VOP_MOVE,
486
+ num(p.x1, 0),
487
+ num(p.y1, 0),
488
+ VOP_LINE,
489
+ num(p.x2, 0),
490
+ num(p.y2, 0),
491
+ ];
365
492
  }
366
493
 
367
494
  // Arc convenience: angles in DEGREES, clockwise from 12 o'clock (the gauge/dial convention). Emits a
@@ -371,7 +498,18 @@ function arcOpsCW(cx, cy, r, a0deg, a1deg) {
371
498
  // The engine's arc angle runs from +X (cos/sin); top-clockwise => subtract 90°.
372
499
  const a0 = ((a0deg - 90) * Math.PI) / 180;
373
500
  const a1 = ((a1deg - 90) * Math.PI) / 180;
374
- return [VOP_MOVE, cx + r * Math.cos(a0), cy + r * Math.sin(a0), VOP_ARC, cx, cy, r, a0, a1, 0];
501
+ return [
502
+ VOP_MOVE,
503
+ cx + r * Math.cos(a0),
504
+ cy + r * Math.sin(a0),
505
+ VOP_ARC,
506
+ cx,
507
+ cy,
508
+ r,
509
+ a0,
510
+ a1,
511
+ 0,
512
+ ];
375
513
  }
376
514
 
377
515
  // --- Imperative shapes -> op-tape (the fast path; no JSX, no React) -------------------------------
@@ -401,7 +539,7 @@ export function shapesToVector(shapes) {
401
539
  for (let si = 0; si < shapes.length; si++) {
402
540
  const s = shapes[si];
403
541
  const opStart = ops.length;
404
- const paintIndex = paints.length / 7;
542
+ const paintIndex = paints.length / PAINT_STRIDE;
405
543
  ops.push(VOP_SHAPE, paintIndex);
406
544
  if (s.arc) {
407
545
  const cx = s.arc[0];
@@ -409,18 +547,55 @@ export function shapesToVector(shapes) {
409
547
  const r = s.arc[2];
410
548
  const a0 = ((s.arc[3] - 90) * Math.PI) / 180;
411
549
  const a1 = ((s.arc[4] - 90) * Math.PI) / 180;
412
- ops.push(VOP_MOVE, cx + r * Math.cos(a0), cy + r * Math.sin(a0), VOP_ARC, cx, cy, r, a0, a1, 0);
550
+ ops.push(
551
+ VOP_MOVE,
552
+ cx + r * Math.cos(a0),
553
+ cy + r * Math.sin(a0),
554
+ VOP_ARC,
555
+ cx,
556
+ cy,
557
+ r,
558
+ a0,
559
+ a1,
560
+ 0,
561
+ );
413
562
  } else if (s.circle) {
414
563
  const cx = s.circle[0];
415
564
  const cy = s.circle[1];
416
565
  const r = s.circle[2];
417
- ops.push(VOP_MOVE, cx + r, cy, VOP_ARC, cx, cy, r, 0, 2 * Math.PI, 0, VOP_CLOSE);
566
+ ops.push(
567
+ VOP_MOVE,
568
+ cx + r,
569
+ cy,
570
+ VOP_ARC,
571
+ cx,
572
+ cy,
573
+ r,
574
+ 0,
575
+ 2 * Math.PI,
576
+ 0,
577
+ VOP_CLOSE,
578
+ );
418
579
  } else if (s.rect) {
419
580
  const x = s.rect[0];
420
581
  const y = s.rect[1];
421
582
  const w = s.rect[2];
422
583
  const h = s.rect[3];
423
- ops.push(VOP_MOVE, x, y, VOP_LINE, x + w, y, VOP_LINE, x + w, y + h, VOP_LINE, x, y + h, VOP_CLOSE);
584
+ ops.push(
585
+ VOP_MOVE,
586
+ x,
587
+ y,
588
+ VOP_LINE,
589
+ x + w,
590
+ y,
591
+ VOP_LINE,
592
+ x + w,
593
+ y + h,
594
+ VOP_LINE,
595
+ x,
596
+ y + h,
597
+ VOP_CLOSE,
598
+ );
424
599
  } else if (s.line) {
425
600
  ops.push(VOP_MOVE, s.line[0], s.line[1], VOP_LINE, s.line[2], s.line[3]);
426
601
  } else if (s.path) {
@@ -438,10 +613,12 @@ export function shapesToVector(shapes) {
438
613
  num(s.miter, 4),
439
614
  CAP[s.cap] ?? 0,
440
615
  JOIN[s.join] ?? 0,
441
- s.fillRule === 'evenodd' ? 1 : 0
616
+ s.fillRule === 'evenodd' ? 1 : 0,
617
+ 0, // fill_grad
618
+ 0, // stroke_grad — the imperative path has no gradients
442
619
  );
443
620
  }
444
- return { ops, paints };
621
+ return {ops, paints};
445
622
  }
446
623
 
447
624
  // --- Flatten an <Svg> subtree --------------------------------------------------------------------
@@ -457,12 +634,13 @@ const PAINT_DEFAULT = {
457
634
  };
458
635
 
459
636
  function mergePaint(base, props) {
460
- const out = { ...base };
461
- for (const k of Object.keys(PAINT_DEFAULT)) if (props[k] != null) out[k] = props[k];
637
+ const out = {...base};
638
+ for (const k of Object.keys(PAINT_DEFAULT))
639
+ if (props[k] != null) out[k] = props[k];
462
640
  return out;
463
641
  }
464
642
 
465
- /** Maps the resolved paint to the 7-number paint-table record [fill,stroke,w,miter,cap,join,rule]. */
643
+ /** Maps the resolved paint to the PAINT_STRIDE-number record [fill,stroke,w,miter,cap,join,rule,fillGrad]. */
466
644
  function paintRecord(paint, scale) {
467
645
  return [
468
646
  parseColor(paint.fill),
@@ -472,6 +650,8 @@ function paintRecord(paint, scale) {
472
650
  CAP[paint.strokeLinecap] ?? 0,
473
651
  JOIN[paint.strokeLinejoin] ?? 0,
474
652
  paint.fillRule === 'evenodd' ? 1 : 0,
653
+ 0, // fill_grad: inline <Svg> children don't reference gradients (baked <Svg source> does)
654
+ 0, // stroke_grad
475
655
  ];
476
656
  }
477
657
 
@@ -479,8 +659,8 @@ function paintRecord(paint, scale) {
479
659
  function transformOps(ops, T) {
480
660
  const out = [];
481
661
  let i = 0;
482
- const ax = (v) => v * T.sx + T.tx;
483
- const ay = (v) => v * T.sy + T.ty;
662
+ const ax = v => v * T.sx + T.tx;
663
+ const ay = v => v * T.sy + T.ty;
484
664
  while (i < ops.length) {
485
665
  const op = ops[i++];
486
666
  out.push(op);
@@ -489,10 +669,24 @@ function transformOps(ops, T) {
489
669
  } else if (op === VOP_QUAD) {
490
670
  out.push(ax(ops[i++]), ay(ops[i++]), ax(ops[i++]), ay(ops[i++]));
491
671
  } else if (op === VOP_CUBIC) {
492
- out.push(ax(ops[i++]), ay(ops[i++]), ax(ops[i++]), ay(ops[i++]), ax(ops[i++]), ay(ops[i++]));
672
+ out.push(
673
+ ax(ops[i++]),
674
+ ay(ops[i++]),
675
+ ax(ops[i++]),
676
+ ay(ops[i++]),
677
+ ax(ops[i++]),
678
+ ay(ops[i++]),
679
+ );
493
680
  } else if (op === VOP_ARC) {
494
681
  // center + radius scale (uniform scale assumed for arcs)
495
- out.push(ax(ops[i++]), ay(ops[i++]), ops[i++] * T.sx, ops[i++], ops[i++], ops[i++]);
682
+ out.push(
683
+ ax(ops[i++]),
684
+ ay(ops[i++]),
685
+ ops[i++] * T.sx,
686
+ ops[i++],
687
+ ops[i++],
688
+ ops[i++],
689
+ );
496
690
  }
497
691
  // VOP_CLOSE has no args
498
692
  }
@@ -518,13 +712,16 @@ export function flattenSvg(props) {
518
712
  const paints = [];
519
713
 
520
714
  // Root transform from viewBox vs width/height.
521
- let root = { sx: 1, sy: 1, tx: 0, ty: 0 };
715
+ let root = {sx: 1, sy: 1, tx: 0, ty: 0};
522
716
  if (props.viewBox && props.width && props.height) {
523
- const vb = String(props.viewBox).trim().split(/[\s,]+/).map(parseFloat);
717
+ const vb = String(props.viewBox)
718
+ .trim()
719
+ .split(/[\s,]+/)
720
+ .map(parseFloat);
524
721
  if (vb.length === 4 && vb[2] > 0 && vb[3] > 0) {
525
722
  const sx = num(props.width, vb[2]) / vb[2];
526
723
  const sy = num(props.height, vb[3]) / vb[3];
527
- root = { sx, sy, tx: -vb[0] * sx, ty: -vb[1] * sy };
724
+ root = {sx, sy, tx: -vb[0] * sx, ty: -vb[1] * sy};
528
725
  }
529
726
  }
530
727
 
@@ -538,7 +735,12 @@ export function flattenSvg(props) {
538
735
  const s = num(p.scale, 1);
539
736
  const gx = num(p.x ?? p.translateX, 0);
540
737
  const gy = num(p.y ?? p.translateY, 0);
541
- const childT = { sx: T.sx * s, sy: T.sy * s, tx: gx * T.sx + T.tx, ty: gy * T.sy + T.ty };
738
+ const childT = {
739
+ sx: T.sx * s,
740
+ sy: T.sy * s,
741
+ tx: gx * T.sx + T.tx,
742
+ ty: gy * T.sy + T.ty,
743
+ };
542
744
  walk(p.children, merged, childT);
543
745
  continue;
544
746
  }
@@ -549,10 +751,16 @@ export function flattenSvg(props) {
549
751
  else if (c.type === 'Rect') shapeOps = rectOps(p);
550
752
  else if (c.type === 'Line') shapeOps = lineOps(p);
551
753
  else if (c.type === 'Arc')
552
- shapeOps = arcOpsCW(num(p.cx, 0), num(p.cy, 0), num(p.r, 0), num(p.startAngle, 0), num(p.endAngle, 0));
754
+ shapeOps = arcOpsCW(
755
+ num(p.cx, 0),
756
+ num(p.cy, 0),
757
+ num(p.r, 0),
758
+ num(p.startAngle, 0),
759
+ num(p.endAngle, 0),
760
+ );
553
761
  if (!shapeOps || shapeOps.length === 0) continue;
554
762
 
555
- const paintIndex = paints.length / 7;
763
+ const paintIndex = paints.length / PAINT_STRIDE;
556
764
  const scale = (T.sx + T.sy) / 2; // stroke-width scale (uniform assumed)
557
765
  paints.push(...paintRecord(merged, scale));
558
766
  ops.push(VOP_SHAPE, paintIndex, ...transformOps(shapeOps, T));
@@ -560,5 +768,125 @@ export function flattenSvg(props) {
560
768
  };
561
769
 
562
770
  walk(props.children, PAINT_DEFAULT, root);
563
- return { ops, paints };
771
+ return {ops, paints};
772
+ }
773
+
774
+ // --- Imported SVG artifacts (<Svg source>) -------------------------------------------------------
775
+
776
+ /** Scales a full op-tape (with SHAPE headers) by (sx, sy): coordinates only, paint indices untouched. */
777
+ function scaleTape(ops, sx, sy) {
778
+ const out = [];
779
+ let i = 0;
780
+ while (i < ops.length) {
781
+ const op = ops[i++];
782
+ out.push(op);
783
+ if (op === VOP_SHAPE)
784
+ out.push(ops[i++]); // paint index — copy, do not scale
785
+ else if (op === VOP_MOVE || op === VOP_LINE)
786
+ out.push(ops[i++] * sx, ops[i++] * sy);
787
+ else if (op === VOP_QUAD)
788
+ out.push(ops[i++] * sx, ops[i++] * sy, ops[i++] * sx, ops[i++] * sy);
789
+ else if (op === VOP_CUBIC)
790
+ out.push(
791
+ ops[i++] * sx,
792
+ ops[i++] * sy,
793
+ ops[i++] * sx,
794
+ ops[i++] * sy,
795
+ ops[i++] * sx,
796
+ ops[i++] * sy,
797
+ );
798
+ else if (op === VOP_ARC)
799
+ out.push(
800
+ ops[i++] * sx,
801
+ ops[i++] * sy,
802
+ ops[i++] * sx,
803
+ ops[i++],
804
+ ops[i++],
805
+ ops[i++],
806
+ );
807
+ // VOP_CLOSE has no args.
808
+ }
809
+ return out;
810
+ }
811
+
812
+ /**
813
+ * Scales an imported vector artifact ({ops, paints, width, height} from the .svg baker) from its intrinsic
814
+ * size to a target box, also scaling stroke widths. Returns the original arrays untouched when no scaling
815
+ * is needed (the common <Svg source> case where the box equals the intrinsic size).
816
+ */
817
+ export function scaleVectorArtifact(art, targetW, targetH) {
818
+ const sx = art.width ? targetW / art.width : 1;
819
+ const sy = art.height ? targetH / art.height : 1;
820
+ const grads = art.gradients || [];
821
+ if (sx === 1 && sy === 1)
822
+ return {ops: art.ops, paints: art.paints, gradients: grads};
823
+ const sw = Math.sqrt(Math.abs(sx * sy)) || 1; // uniform stroke-width / radius scale
824
+ const paints = art.paints.slice();
825
+ for (let k = 2; k < paints.length; k += PAINT_STRIDE) paints[k] *= sw; // index 2 of each record = strokeWidth
826
+ // Gradient geometry lives in the same coordinate space as the path, so it scales with it (radius uniformly).
827
+ const gradients = grads.map(g => ({
828
+ ...g,
829
+ ax: (g.ax || 0) * sx,
830
+ ay: (g.ay || 0) * sy,
831
+ bx: (g.bx || 0) * sx,
832
+ by: (g.by || 0) * sy,
833
+ // Radial r is a length (scales); conic r is a start angle (invariant under uniform scale) — don't scale it.
834
+ r: g.type === GRAD_CONIC ? g.r || 0 : (g.r || 0) * sw,
835
+ }));
836
+ return {ops: scaleTape(art.ops, sx, sy), paints, gradients};
837
+ }
838
+
839
+ // --- Bridge-cap diagnostics ----------------------------------------------------------------------
840
+
841
+ let _warnedVecOps = false;
842
+ let _warnedVecPaints = false;
843
+ let _warnedVecGrads = false;
844
+
845
+ /**
846
+ * Warns (once per kind) when an <Svg>'s compiled geometry exceeds the native bridge's caps and will be
847
+ * SILENTLY TRUNCATED. The bridge (native_ui_bridge.c) copies the op-tape into a fixed buffer and caps the
848
+ * paint table; past the cap, geometry/shapes are dropped. The engine has its own pool diagnostics (on
849
+ * stderr, debug builds) for what happens further in; this surfaces the JS->engine boundary truncation in
850
+ * the developer's console. Pass the live caps from NativeUI.maxVectorOps / maxVectorPaints / maxVectorGrads
851
+ * (undefined on an older bridge => no-op).
852
+ *
853
+ * @param {number} opsLen Op-tape length (flat float count).
854
+ * @param {number} paintsLen Paint-table length (PAINT_STRIDE numbers per shape).
855
+ * @param {number} maxOps Bridge op-tape cap (NativeUI.maxVectorOps).
856
+ * @param {number} maxPaints Bridge paint cap, in SHAPES (NativeUI.maxVectorPaints).
857
+ * @param {number} [gradsLen] Gradient count (number of gradients referenced by the shapes).
858
+ * @param {number} [maxGrads] Bridge gradient cap (NativeUI.maxVectorGrads).
859
+ */
860
+ export function warnVectorCaps(
861
+ opsLen,
862
+ paintsLen,
863
+ maxOps,
864
+ maxPaints,
865
+ gradsLen,
866
+ maxGrads,
867
+ ) {
868
+ if (!_warnedVecOps && maxOps > 0 && opsLen > maxOps) {
869
+ _warnedVecOps = true;
870
+ console.warn(
871
+ `embedded-react: an <Svg> op-tape is too long (${opsLen} > ${maxOps} floats) and will be truncated — ` +
872
+ `the shape gets cut off. Simplify the path (fewer/coarser curves) or split it across <Svg> nodes; ` +
873
+ `raising the limit needs VEC_BRIDGE_MAX_OPS + ERUI_VECTOR_TAPE_MAX.`,
874
+ );
875
+ }
876
+ const shapes = (paintsLen / PAINT_STRIDE) | 0;
877
+ if (!_warnedVecPaints && maxPaints > 0 && shapes > maxPaints) {
878
+ _warnedVecPaints = true;
879
+ console.warn(
880
+ `embedded-react: an <Svg> has ${shapes} shapes (> ${maxPaints}) — the extra shapes won't render. ` +
881
+ `Split them across multiple <Svg> nodes; raising the limit needs VEC_BRIDGE_MAX_PAINTS + ERUI_VECTOR_PAINTS_MAX.`,
882
+ );
883
+ }
884
+ if (!_warnedVecGrads && maxGrads > 0 && gradsLen > maxGrads) {
885
+ _warnedVecGrads = true;
886
+ console.warn(
887
+ `embedded-react: an <Svg> references ${gradsLen} gradients (> ${maxGrads}) — the extra gradients are ` +
888
+ `dropped and shapes that use them fall back to solid fills/strokes. Reuse fewer distinct gradients across ` +
889
+ `shapes; raising the limit needs VEC_BRIDGE_MAX_GRADS + ERUI_VECTOR_GRADS_MAX.`,
890
+ );
891
+ }
564
892
  }
@@ -22,7 +22,7 @@
22
22
  //
23
23
  // Values must be JSON-serializable (numbers, strings, booleans, plain objects/arrays). The simulator
24
24
  // resets the persisted state when you press R (manual reload) or restart it.
25
- import { useState, useCallback } from 'react';
25
+ import {useState, useCallback} from 'react';
26
26
 
27
27
  /**
28
28
  * Drop-in useState that persists across simulator reloads, keyed by a stable string.
@@ -48,8 +48,8 @@ export function usePersistentState(key, initial) {
48
48
  });
49
49
 
50
50
  const set = useCallback(
51
- (next) => {
52
- setValue((prev) => {
51
+ next => {
52
+ setValue(prev => {
53
53
  const v = typeof next === 'function' ? next(prev) : next;
54
54
  const store = globalThis.__erPersist;
55
55
  if (store) {