embedded-react 0.1.0
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/LICENSE +201 -0
- package/NOTICE +58 -0
- package/README.md +224 -0
- package/aot/compile.mjs +3066 -0
- package/aot/screenshot-smoke.mjs +110 -0
- package/aot/style-map.mjs +248 -0
- package/assets/bake-font.mjs +190 -0
- package/assets/bake-image.mjs +50 -0
- package/assets/build-builtin-font.mjs +51 -0
- package/assets/emit-c.mjs +187 -0
- package/assets/emit-container.mjs +121 -0
- package/assets/emit-pack.mjs +128 -0
- package/assets/index.mjs +72 -0
- package/assets/rasterize.mjs +169 -0
- package/build.mjs +136 -0
- package/pack-container.mjs +161 -0
- package/package.json +79 -0
- package/persist-transform.mjs +106 -0
- package/src/embedded-react/Animated.js +352 -0
- package/src/embedded-react/AppRegistry.js +49 -0
- package/src/embedded-react/Easing.js +39 -0
- package/src/embedded-react/LayoutAnimation.js +45 -0
- package/src/embedded-react/Platform.js +26 -0
- package/src/embedded-react/StyleSheet.js +36 -0
- package/src/embedded-react/components.js +44 -0
- package/src/embedded-react/imperative.js +68 -0
- package/src/embedded-react/index.js +52 -0
- package/src/embedded-react/layout-anim-config.js +91 -0
- package/src/embedded-react/split-style.js +58 -0
- package/src/embedded-react/svg-ops.js +564 -0
- package/src/embedded-react/usePersistentState.js +69 -0
- package/src/host-config.js +196 -0
- package/src/native-ui.js +24 -0
- package/src/props.js +183 -0
- package/src/renderer.js +57 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Cory Lamming
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Pure SVG -> engine op-tape compiler. No engine/React imports, so it is unit-testable in isolation.
|
|
18
|
+
//
|
|
19
|
+
// The engine's ER_NODE_VECTOR node takes a flat float "op-tape" plus a flat paint table (see the
|
|
20
|
+
// ER_VOP_* contract in er_scene.h). This module compiles a declarative <Svg> subtree (Path/Circle/
|
|
21
|
+
// Rect/Line/Ellipse/G elements) into that form: it parses SVG `d` path data, converts primitive
|
|
22
|
+
// shapes to path ops, bakes viewBox + <G> translate/scale into coordinates, and resolves inherited
|
|
23
|
+
// paint. The opcodes below MUST stay in sync with er_scene.h.
|
|
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;
|
|
32
|
+
|
|
33
|
+
const CAP = { butt: 0, round: 1, square: 2 };
|
|
34
|
+
const JOIN = { miter: 0, round: 1, bevel: 2 };
|
|
35
|
+
|
|
36
|
+
// Minimal named-color set (extend as needed); everything else goes through hex/rgb parsing.
|
|
37
|
+
const NAMED = {
|
|
38
|
+
none: 0,
|
|
39
|
+
transparent: 0,
|
|
40
|
+
black: 0xff000000,
|
|
41
|
+
white: 0xffffffff,
|
|
42
|
+
red: 0xffff0000,
|
|
43
|
+
green: 0xff008000,
|
|
44
|
+
blue: 0xff0000ff,
|
|
45
|
+
gray: 0xff808080,
|
|
46
|
+
grey: 0xff808080,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Cache string->uint32 results: a theme reuses a handful of color strings, so on a continuous drag
|
|
50
|
+
// (which recompiles the dial every move) this turns repeated hex parsing into a Map lookup.
|
|
51
|
+
const _colorCache = new Map();
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parses a CSS color string to a uint32 ARGB8888. Returns 0 (fully transparent) for none/transparent/
|
|
55
|
+
* null, so the engine treats it as "no fill"/"no stroke". Accepts #rgb, #rgba, #rrggbb, #rrggbbaa,
|
|
56
|
+
* rgb()/rgba(), and the small named set above. String results are memoised (see _colorCache).
|
|
57
|
+
*/
|
|
58
|
+
export function parseColor(c) {
|
|
59
|
+
if (c == null || c === false) return 0;
|
|
60
|
+
if (typeof c === 'number') return c >>> 0;
|
|
61
|
+
const cached = _colorCache.get(c);
|
|
62
|
+
if (cached !== undefined) return cached;
|
|
63
|
+
const v = parseColorString(c);
|
|
64
|
+
_colorCache.set(c, v);
|
|
65
|
+
return v;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseColorString(c) {
|
|
69
|
+
let s = String(c).trim().toLowerCase();
|
|
70
|
+
if (s in NAMED) return NAMED[s] >>> 0;
|
|
71
|
+
if (s[0] === '#') {
|
|
72
|
+
s = s.slice(1);
|
|
73
|
+
let r, g, b, a = 255;
|
|
74
|
+
if (s.length === 3 || s.length === 4) {
|
|
75
|
+
r = parseInt(s[0] + s[0], 16);
|
|
76
|
+
g = parseInt(s[1] + s[1], 16);
|
|
77
|
+
b = parseInt(s[2] + s[2], 16);
|
|
78
|
+
if (s.length === 4) a = parseInt(s[3] + s[3], 16);
|
|
79
|
+
} else if (s.length === 6 || s.length === 8) {
|
|
80
|
+
r = parseInt(s.slice(0, 2), 16);
|
|
81
|
+
g = parseInt(s.slice(2, 4), 16);
|
|
82
|
+
b = parseInt(s.slice(4, 6), 16);
|
|
83
|
+
if (s.length === 8) a = parseInt(s.slice(6, 8), 16);
|
|
84
|
+
} else {
|
|
85
|
+
return 0xff000000;
|
|
86
|
+
}
|
|
87
|
+
return ((a << 24) | (r << 16) | (g << 8) | b) >>> 0;
|
|
88
|
+
}
|
|
89
|
+
if (s.startsWith('rgb')) {
|
|
90
|
+
const m = s.match(/rgba?\(([^)]+)\)/);
|
|
91
|
+
if (m) {
|
|
92
|
+
const p = m[1].split(',').map((x) => x.trim());
|
|
93
|
+
const r = parseInt(p[0], 10) || 0;
|
|
94
|
+
const g = parseInt(p[1], 10) || 0;
|
|
95
|
+
const b = parseInt(p[2], 10) || 0;
|
|
96
|
+
const a = p[3] != null ? Math.round(parseFloat(p[3]) * 255) : 255;
|
|
97
|
+
return ((a << 24) | (r << 16) | (g << 8) | b) >>> 0;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return 0xff000000; // unknown -> opaque black (SVG-ish default)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// --- SVG `d` path-data parser -------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
// Hoisted so the pattern is compiled once at module load, not on every parsePath call. The /g lastIndex is reset below.
|
|
106
|
+
const PATH_RE = /([MmLlHhVvCcSsQqTtAaZz])|(-?\d*\.?\d+(?:[eE][-+]?\d+)?)/g;
|
|
107
|
+
|
|
108
|
+
/** Tokenizes a `d` string into {cmd, args[]} groups (one per command letter). */
|
|
109
|
+
function tokenizePath(d) {
|
|
110
|
+
const out = [];
|
|
111
|
+
const re = PATH_RE;
|
|
112
|
+
re.lastIndex = 0;
|
|
113
|
+
let m;
|
|
114
|
+
let cur = null;
|
|
115
|
+
while ((m = re.exec(d)) !== null) {
|
|
116
|
+
if (m[1]) {
|
|
117
|
+
cur = { cmd: m[1], args: [] };
|
|
118
|
+
out.push(cur);
|
|
119
|
+
} else if (cur) {
|
|
120
|
+
cur.args.push(parseFloat(m[2]));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Converts an SVG elliptical arc (endpoint form) to a list of cubic bezier segments. */
|
|
127
|
+
function arcToCubics(x0, y0, rx, ry, phiDeg, largeArc, sweep, x, y) {
|
|
128
|
+
const out = [];
|
|
129
|
+
if (rx === 0 || ry === 0) {
|
|
130
|
+
out.push([x0, y0, x, y, x, y]); // degenerate -> straight line as a cubic
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
rx = Math.abs(rx);
|
|
134
|
+
ry = Math.abs(ry);
|
|
135
|
+
const phi = (phiDeg * Math.PI) / 180;
|
|
136
|
+
const cosP = Math.cos(phi);
|
|
137
|
+
const sinP = Math.sin(phi);
|
|
138
|
+
const dx = (x0 - x) / 2;
|
|
139
|
+
const dy = (y0 - y) / 2;
|
|
140
|
+
const x1p = cosP * dx + sinP * dy;
|
|
141
|
+
const y1p = -sinP * dx + cosP * dy;
|
|
142
|
+
let rxs = rx * rx;
|
|
143
|
+
let rys = ry * ry;
|
|
144
|
+
const x1ps = x1p * x1p;
|
|
145
|
+
const y1ps = y1p * y1p;
|
|
146
|
+
// Correct out-of-range radii.
|
|
147
|
+
const lam = x1ps / rxs + y1ps / rys;
|
|
148
|
+
if (lam > 1) {
|
|
149
|
+
const s = Math.sqrt(lam);
|
|
150
|
+
rx *= s;
|
|
151
|
+
ry *= s;
|
|
152
|
+
rxs = rx * rx;
|
|
153
|
+
rys = ry * ry;
|
|
154
|
+
}
|
|
155
|
+
let sign = largeArc !== sweep ? 1 : -1;
|
|
156
|
+
let num = rxs * rys - rxs * y1ps - rys * x1ps;
|
|
157
|
+
if (num < 0) num = 0;
|
|
158
|
+
const co = sign * Math.sqrt(num / (rxs * y1ps + rys * x1ps || 1));
|
|
159
|
+
const cxp = (co * rx * y1p) / ry;
|
|
160
|
+
const cyp = (-co * ry * x1p) / rx;
|
|
161
|
+
const cx = cosP * cxp - sinP * cyp + (x0 + x) / 2;
|
|
162
|
+
const cy = sinP * cxp + cosP * cyp + (y0 + y) / 2;
|
|
163
|
+
const ang = (ux, uy, vx, vy) => {
|
|
164
|
+
const dot = ux * vx + uy * vy;
|
|
165
|
+
const len = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy)) || 1;
|
|
166
|
+
let a = Math.acos(Math.max(-1, Math.min(1, dot / len)));
|
|
167
|
+
if (ux * vy - uy * vx < 0) a = -a;
|
|
168
|
+
return a;
|
|
169
|
+
};
|
|
170
|
+
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);
|
|
172
|
+
if (!sweep && dtheta > 0) dtheta -= 2 * Math.PI;
|
|
173
|
+
if (sweep && dtheta < 0) dtheta += 2 * Math.PI;
|
|
174
|
+
const segs = Math.ceil(Math.abs(dtheta) / (Math.PI / 2));
|
|
175
|
+
const delta = dtheta / segs;
|
|
176
|
+
const t = (4 / 3) * Math.tan(delta / 4);
|
|
177
|
+
let th = theta1;
|
|
178
|
+
let px = x0;
|
|
179
|
+
let py = y0;
|
|
180
|
+
for (let i = 0; i < segs; i++) {
|
|
181
|
+
const th2 = th + delta;
|
|
182
|
+
const cos1 = Math.cos(th);
|
|
183
|
+
const sin1 = Math.sin(th);
|
|
184
|
+
const cos2 = Math.cos(th2);
|
|
185
|
+
const sin2 = Math.sin(th2);
|
|
186
|
+
const e1x = cx + rx * cosP * cos1 - ry * sinP * sin1;
|
|
187
|
+
const e1y = cy + rx * sinP * cos1 + ry * cosP * sin1;
|
|
188
|
+
const e2x = cx + rx * cosP * cos2 - ry * sinP * sin2;
|
|
189
|
+
const e2y = cy + rx * sinP * cos2 + ry * cosP * sin2;
|
|
190
|
+
const d1x = -rx * cosP * sin1 - ry * sinP * cos1;
|
|
191
|
+
const d1y = -rx * sinP * sin1 + ry * cosP * cos1;
|
|
192
|
+
const d2x = -rx * cosP * sin2 - ry * sinP * cos2;
|
|
193
|
+
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]);
|
|
195
|
+
px = e2x;
|
|
196
|
+
py = e2y;
|
|
197
|
+
th = th2;
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Parses an SVG `d` string into engine path ops (a flat number array, NO leading SHAPE op). Supports
|
|
204
|
+
* M/L/H/V/C/S/Q/T/A/Z and their relative (lowercase) forms; arcs are converted to cubics.
|
|
205
|
+
*/
|
|
206
|
+
export function parsePath(d) {
|
|
207
|
+
const ops = [];
|
|
208
|
+
const toks = tokenizePath(d);
|
|
209
|
+
let cx = 0;
|
|
210
|
+
let cy = 0;
|
|
211
|
+
let sx = 0;
|
|
212
|
+
let sy = 0; // subpath start
|
|
213
|
+
let prevCtrlX = 0;
|
|
214
|
+
let prevCtrlY = 0;
|
|
215
|
+
let prevCmd = '';
|
|
216
|
+
for (const tk of toks) {
|
|
217
|
+
const C = tk.cmd;
|
|
218
|
+
const rel = C >= 'a' && C <= 'z';
|
|
219
|
+
const a = tk.args;
|
|
220
|
+
const U = C.toUpperCase();
|
|
221
|
+
let i = 0;
|
|
222
|
+
const rx = (v) => (rel ? cx + v : v);
|
|
223
|
+
const ry = (v) => (rel ? cy + v : v);
|
|
224
|
+
if (U === 'M') {
|
|
225
|
+
// First pair is moveto; later pairs are implicit linetos.
|
|
226
|
+
cx = rx(a[i++]);
|
|
227
|
+
cy = ry(a[i++]);
|
|
228
|
+
sx = cx;
|
|
229
|
+
sy = cy;
|
|
230
|
+
ops.push(VOP_MOVE, cx, cy);
|
|
231
|
+
while (i + 1 < a.length) {
|
|
232
|
+
cx = rel ? cx + a[i++] : a[i++];
|
|
233
|
+
cy = rel ? cy + a[i++] : a[i++];
|
|
234
|
+
ops.push(VOP_LINE, cx, cy);
|
|
235
|
+
}
|
|
236
|
+
} else if (U === 'L') {
|
|
237
|
+
while (i + 1 < a.length) {
|
|
238
|
+
cx = rel ? cx + a[i++] : a[i++];
|
|
239
|
+
cy = rel ? cy + a[i++] : a[i++];
|
|
240
|
+
ops.push(VOP_LINE, cx, cy);
|
|
241
|
+
}
|
|
242
|
+
} else if (U === 'H') {
|
|
243
|
+
while (i < a.length) {
|
|
244
|
+
cx = rel ? cx + a[i++] : a[i++];
|
|
245
|
+
ops.push(VOP_LINE, cx, cy);
|
|
246
|
+
}
|
|
247
|
+
} else if (U === 'V') {
|
|
248
|
+
while (i < a.length) {
|
|
249
|
+
cy = rel ? cy + a[i++] : a[i++];
|
|
250
|
+
ops.push(VOP_LINE, cx, cy);
|
|
251
|
+
}
|
|
252
|
+
} else if (U === 'C') {
|
|
253
|
+
while (i + 5 < a.length) {
|
|
254
|
+
const c1x = rx(a[i++]);
|
|
255
|
+
const c1y = ry(a[i++]);
|
|
256
|
+
const c2x = rx(a[i++]);
|
|
257
|
+
const c2y = ry(a[i++]);
|
|
258
|
+
const ex = rx(a[i++]);
|
|
259
|
+
const ey = ry(a[i++]);
|
|
260
|
+
ops.push(VOP_CUBIC, c1x, c1y, c2x, c2y, ex, ey);
|
|
261
|
+
prevCtrlX = c2x;
|
|
262
|
+
prevCtrlY = c2y;
|
|
263
|
+
cx = ex;
|
|
264
|
+
cy = ey;
|
|
265
|
+
}
|
|
266
|
+
} else if (U === 'S') {
|
|
267
|
+
while (i + 3 < a.length) {
|
|
268
|
+
const refl = prevCmd === 'C' || prevCmd === 'S';
|
|
269
|
+
const c1x = refl ? 2 * cx - prevCtrlX : cx;
|
|
270
|
+
const c1y = refl ? 2 * cy - prevCtrlY : cy;
|
|
271
|
+
const c2x = rx(a[i++]);
|
|
272
|
+
const c2y = ry(a[i++]);
|
|
273
|
+
const ex = rx(a[i++]);
|
|
274
|
+
const ey = ry(a[i++]);
|
|
275
|
+
ops.push(VOP_CUBIC, c1x, c1y, c2x, c2y, ex, ey);
|
|
276
|
+
prevCtrlX = c2x;
|
|
277
|
+
prevCtrlY = c2y;
|
|
278
|
+
cx = ex;
|
|
279
|
+
cy = ey;
|
|
280
|
+
}
|
|
281
|
+
} else if (U === 'Q') {
|
|
282
|
+
while (i + 3 < a.length) {
|
|
283
|
+
const qx = rx(a[i++]);
|
|
284
|
+
const qy = ry(a[i++]);
|
|
285
|
+
const ex = rx(a[i++]);
|
|
286
|
+
const ey = ry(a[i++]);
|
|
287
|
+
ops.push(VOP_QUAD, qx, qy, ex, ey);
|
|
288
|
+
prevCtrlX = qx;
|
|
289
|
+
prevCtrlY = qy;
|
|
290
|
+
cx = ex;
|
|
291
|
+
cy = ey;
|
|
292
|
+
}
|
|
293
|
+
} else if (U === 'T') {
|
|
294
|
+
while (i + 1 < a.length) {
|
|
295
|
+
const refl = prevCmd === 'Q' || prevCmd === 'T';
|
|
296
|
+
const qx = refl ? 2 * cx - prevCtrlX : cx;
|
|
297
|
+
const qy = refl ? 2 * cy - prevCtrlY : cy;
|
|
298
|
+
const ex = rx(a[i++]);
|
|
299
|
+
const ey = ry(a[i++]);
|
|
300
|
+
ops.push(VOP_QUAD, qx, qy, ex, ey);
|
|
301
|
+
prevCtrlX = qx;
|
|
302
|
+
prevCtrlY = qy;
|
|
303
|
+
cx = ex;
|
|
304
|
+
cy = ey;
|
|
305
|
+
}
|
|
306
|
+
} else if (U === 'A') {
|
|
307
|
+
while (i + 6 < a.length) {
|
|
308
|
+
const arx = a[i++];
|
|
309
|
+
const ary = a[i++];
|
|
310
|
+
const rot = a[i++];
|
|
311
|
+
const laf = a[i++];
|
|
312
|
+
const swf = a[i++];
|
|
313
|
+
const ex = rx(a[i++]);
|
|
314
|
+
const ey = ry(a[i++]);
|
|
315
|
+
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]);
|
|
317
|
+
cx = ex;
|
|
318
|
+
cy = ey;
|
|
319
|
+
}
|
|
320
|
+
} else if (U === 'Z') {
|
|
321
|
+
ops.push(VOP_CLOSE);
|
|
322
|
+
cx = sx;
|
|
323
|
+
cy = sy;
|
|
324
|
+
}
|
|
325
|
+
prevCmd = U;
|
|
326
|
+
}
|
|
327
|
+
return ops;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// --- Primitive shapes -> path ops ----------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
function num(v, dflt) {
|
|
333
|
+
const n = typeof v === 'number' ? v : parseFloat(v);
|
|
334
|
+
return isNaN(n) ? dflt : n;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function circleOps(p) {
|
|
338
|
+
const cx = num(p.cx, 0);
|
|
339
|
+
const cy = num(p.cy, 0);
|
|
340
|
+
const r = num(p.r, 0);
|
|
341
|
+
// 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];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function ellipseOps(p) {
|
|
346
|
+
// Approximate via an arc-free path: four cubic beziers (kappa) — reuse parsePath by emitting an A path.
|
|
347
|
+
const cx = num(p.cx, 0);
|
|
348
|
+
const cy = num(p.cy, 0);
|
|
349
|
+
const rx = num(p.rx, 0);
|
|
350
|
+
const ry = num(p.ry, 0);
|
|
351
|
+
const d = `M ${cx - rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx + rx} ${cy} A ${rx} ${ry} 0 1 0 ${cx - rx} ${cy} Z`;
|
|
352
|
+
return parsePath(d);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function rectOps(p) {
|
|
356
|
+
const x = num(p.x, 0);
|
|
357
|
+
const y = num(p.y, 0);
|
|
358
|
+
const w = num(p.width, 0);
|
|
359
|
+
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];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
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)];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Arc convenience: angles in DEGREES, clockwise from 12 o'clock (the gauge/dial convention). Emits a
|
|
368
|
+
// native VOP_ARC (no d-string regex / bezier conversion), so it is cheap enough to rebuild every drag
|
|
369
|
+
// frame — the key to fast imperative updates.
|
|
370
|
+
function arcOpsCW(cx, cy, r, a0deg, a1deg) {
|
|
371
|
+
// The engine's arc angle runs from +X (cos/sin); top-clockwise => subtract 90°.
|
|
372
|
+
const a0 = ((a0deg - 90) * Math.PI) / 180;
|
|
373
|
+
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];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// --- Imperative shapes -> op-tape (the fast path; no JSX, no React) -------------------------------
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Compiles a flat array of primitive shape descriptors into {ops, paints} for NativeUI.setVectorOps.
|
|
381
|
+
* Each descriptor names one primitive plus paint fields, e.g.:
|
|
382
|
+
* { arc: [cx, cy, r, startDeg, endDeg], stroke: '#f4a261', strokeWidth: 14, cap: 'round' }
|
|
383
|
+
* { circle: [cx, cy, r], fill: '#16202f', stroke: '#f4a261', strokeWidth: 3 }
|
|
384
|
+
* { line: [x1, y1, x2, y2], stroke, strokeWidth }
|
|
385
|
+
* { rect: [x, y, w, h], fill }
|
|
386
|
+
* { path: 'M..A..', stroke } // d-string supported but slower (regex + bezier)
|
|
387
|
+
* Arc angles are degrees, clockwise from 12 o'clock. This avoids the d-string parser entirely, so it's
|
|
388
|
+
* cheap enough to call on every pointer move (the imperative drag path).
|
|
389
|
+
*/
|
|
390
|
+
// Reused output buffers — shapesToVector is on the drag hot path and its result is consumed
|
|
391
|
+
// synchronously by NativeUI.setVectorOps, so growing fresh arrays (plus per-shape arrays + spreads)
|
|
392
|
+
// every move is pure GC. Push directly into these instead.
|
|
393
|
+
const _vecOps = [];
|
|
394
|
+
const _vecPaints = [];
|
|
395
|
+
|
|
396
|
+
export function shapesToVector(shapes) {
|
|
397
|
+
const ops = _vecOps;
|
|
398
|
+
const paints = _vecPaints;
|
|
399
|
+
ops.length = 0;
|
|
400
|
+
paints.length = 0;
|
|
401
|
+
for (let si = 0; si < shapes.length; si++) {
|
|
402
|
+
const s = shapes[si];
|
|
403
|
+
const opStart = ops.length;
|
|
404
|
+
const paintIndex = paints.length / 7;
|
|
405
|
+
ops.push(VOP_SHAPE, paintIndex);
|
|
406
|
+
if (s.arc) {
|
|
407
|
+
const cx = s.arc[0];
|
|
408
|
+
const cy = s.arc[1];
|
|
409
|
+
const r = s.arc[2];
|
|
410
|
+
const a0 = ((s.arc[3] - 90) * Math.PI) / 180;
|
|
411
|
+
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);
|
|
413
|
+
} else if (s.circle) {
|
|
414
|
+
const cx = s.circle[0];
|
|
415
|
+
const cy = s.circle[1];
|
|
416
|
+
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);
|
|
418
|
+
} else if (s.rect) {
|
|
419
|
+
const x = s.rect[0];
|
|
420
|
+
const y = s.rect[1];
|
|
421
|
+
const w = s.rect[2];
|
|
422
|
+
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);
|
|
424
|
+
} else if (s.line) {
|
|
425
|
+
ops.push(VOP_MOVE, s.line[0], s.line[1], VOP_LINE, s.line[2], s.line[3]);
|
|
426
|
+
} else if (s.path) {
|
|
427
|
+
const so = parsePath(s.path);
|
|
428
|
+
for (let k = 0; k < so.length; k++) ops.push(so[k]);
|
|
429
|
+
}
|
|
430
|
+
if (ops.length <= opStart + 2) {
|
|
431
|
+
ops.length = opStart; // nothing emitted — roll back the SHAPE header
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
paints.push(
|
|
435
|
+
parseColor(s.fill),
|
|
436
|
+
parseColor(s.stroke),
|
|
437
|
+
num(s.strokeWidth, 1),
|
|
438
|
+
num(s.miter, 4),
|
|
439
|
+
CAP[s.cap] ?? 0,
|
|
440
|
+
JOIN[s.join] ?? 0,
|
|
441
|
+
s.fillRule === 'evenodd' ? 1 : 0
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
return { ops, paints };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// --- Flatten an <Svg> subtree --------------------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
const PAINT_DEFAULT = {
|
|
450
|
+
fill: 'black',
|
|
451
|
+
stroke: 'none',
|
|
452
|
+
strokeWidth: 1,
|
|
453
|
+
strokeLinecap: 'butt',
|
|
454
|
+
strokeLinejoin: 'miter',
|
|
455
|
+
strokeMiterlimit: 4,
|
|
456
|
+
fillRule: 'nonzero',
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
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];
|
|
462
|
+
return out;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Maps the resolved paint to the 7-number paint-table record [fill,stroke,w,miter,cap,join,rule]. */
|
|
466
|
+
function paintRecord(paint, scale) {
|
|
467
|
+
return [
|
|
468
|
+
parseColor(paint.fill),
|
|
469
|
+
parseColor(paint.stroke),
|
|
470
|
+
num(paint.strokeWidth, 1) * scale,
|
|
471
|
+
num(paint.strokeMiterlimit, 4),
|
|
472
|
+
CAP[paint.strokeLinecap] ?? 0,
|
|
473
|
+
JOIN[paint.strokeLinejoin] ?? 0,
|
|
474
|
+
paint.fillRule === 'evenodd' ? 1 : 0,
|
|
475
|
+
];
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Applies a {sx,sy,tx,ty} transform in place to a path-op array (no leading SHAPE op). */
|
|
479
|
+
function transformOps(ops, T) {
|
|
480
|
+
const out = [];
|
|
481
|
+
let i = 0;
|
|
482
|
+
const ax = (v) => v * T.sx + T.tx;
|
|
483
|
+
const ay = (v) => v * T.sy + T.ty;
|
|
484
|
+
while (i < ops.length) {
|
|
485
|
+
const op = ops[i++];
|
|
486
|
+
out.push(op);
|
|
487
|
+
if (op === VOP_MOVE || op === VOP_LINE) {
|
|
488
|
+
out.push(ax(ops[i++]), ay(ops[i++]));
|
|
489
|
+
} else if (op === VOP_QUAD) {
|
|
490
|
+
out.push(ax(ops[i++]), ay(ops[i++]), ax(ops[i++]), ay(ops[i++]));
|
|
491
|
+
} 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++]));
|
|
493
|
+
} else if (op === VOP_ARC) {
|
|
494
|
+
// 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++]);
|
|
496
|
+
}
|
|
497
|
+
// VOP_CLOSE has no args
|
|
498
|
+
}
|
|
499
|
+
return out;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function asArray(children) {
|
|
503
|
+
if (children == null || children === false || children === true) return [];
|
|
504
|
+
return Array.isArray(children) ? children : [children];
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function isElement(c) {
|
|
508
|
+
return c && typeof c === 'object' && c.type != null && c.props != null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Flattens an <Svg> element's props into {ops, paints} flat number arrays for NativeUI.setVectorOps.
|
|
513
|
+
* Handles Path/Circle/Ellipse/Rect/Line shapes, <G> grouping (inherited paint + translate/scale), and
|
|
514
|
+
* a root viewBox -> width/height scale. Returns empty arrays when there is nothing to draw.
|
|
515
|
+
*/
|
|
516
|
+
export function flattenSvg(props) {
|
|
517
|
+
const ops = [];
|
|
518
|
+
const paints = [];
|
|
519
|
+
|
|
520
|
+
// Root transform from viewBox vs width/height.
|
|
521
|
+
let root = { sx: 1, sy: 1, tx: 0, ty: 0 };
|
|
522
|
+
if (props.viewBox && props.width && props.height) {
|
|
523
|
+
const vb = String(props.viewBox).trim().split(/[\s,]+/).map(parseFloat);
|
|
524
|
+
if (vb.length === 4 && vb[2] > 0 && vb[3] > 0) {
|
|
525
|
+
const sx = num(props.width, vb[2]) / vb[2];
|
|
526
|
+
const sy = num(props.height, vb[3]) / vb[3];
|
|
527
|
+
root = { sx, sy, tx: -vb[0] * sx, ty: -vb[1] * sy };
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const walk = (child, paint, T) => {
|
|
532
|
+
for (const c of asArray(child)) {
|
|
533
|
+
if (!isElement(c)) continue;
|
|
534
|
+
const p = c.props || {};
|
|
535
|
+
const merged = mergePaint(paint, p);
|
|
536
|
+
if (c.type === 'G') {
|
|
537
|
+
// Compose a translate/scale for the group, then recurse.
|
|
538
|
+
const s = num(p.scale, 1);
|
|
539
|
+
const gx = num(p.x ?? p.translateX, 0);
|
|
540
|
+
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 };
|
|
542
|
+
walk(p.children, merged, childT);
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
let shapeOps = null;
|
|
546
|
+
if (c.type === 'Path' && p.d) shapeOps = parsePath(p.d);
|
|
547
|
+
else if (c.type === 'Circle') shapeOps = circleOps(p);
|
|
548
|
+
else if (c.type === 'Ellipse') shapeOps = ellipseOps(p);
|
|
549
|
+
else if (c.type === 'Rect') shapeOps = rectOps(p);
|
|
550
|
+
else if (c.type === 'Line') shapeOps = lineOps(p);
|
|
551
|
+
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));
|
|
553
|
+
if (!shapeOps || shapeOps.length === 0) continue;
|
|
554
|
+
|
|
555
|
+
const paintIndex = paints.length / 7;
|
|
556
|
+
const scale = (T.sx + T.sy) / 2; // stroke-width scale (uniform assumed)
|
|
557
|
+
paints.push(...paintRecord(merged, scale));
|
|
558
|
+
ops.push(VOP_SHAPE, paintIndex, ...transformOps(shapeOps, T));
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
walk(props.children, PAINT_DEFAULT, root);
|
|
563
|
+
return { ops, paints };
|
|
564
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 Cory Lamming
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// usePersistentState — a useState that survives the simulator's hot reload (see /SIMULATOR.md, Phase
|
|
18
|
+
// 3). The simulator's build transform (persist-transform.mjs) rewrites the app's plain `useState`
|
|
19
|
+
// into this helper, so persistence is transparent — you normally never call this directly; it's also
|
|
20
|
+
// exported for explicit use. Outside the simulator (e.g., on a device, where the host doesn't install
|
|
21
|
+
// `__erPersist`) it is exactly useState, so the same app code runs everywhere.
|
|
22
|
+
//
|
|
23
|
+
// Values must be JSON-serializable (numbers, strings, booleans, plain objects/arrays). The simulator
|
|
24
|
+
// resets the persisted state when you press R (manual reload) or restart it.
|
|
25
|
+
import { useState, useCallback } from 'react';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Drop-in useState that persists across simulator reloads, keyed by a stable string.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} key Stable identity for this piece of state (unique within the app).
|
|
31
|
+
* @param {*|Function} initial Initial value (or a lazy initializer), used when nothing is persisted.
|
|
32
|
+
* @returns {[*, Function]} [value, setValue] — same shape as useState.
|
|
33
|
+
*/
|
|
34
|
+
export function usePersistentState(key, initial) {
|
|
35
|
+
const [value, setValue] = useState(() => {
|
|
36
|
+
const store = globalThis.__erPersist;
|
|
37
|
+
if (store) {
|
|
38
|
+
const stored = store.get(key);
|
|
39
|
+
if (stored !== undefined) {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(stored);
|
|
42
|
+
} catch {
|
|
43
|
+
/* corrupt/incompatible — fall back to initial */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return typeof initial === 'function' ? initial() : initial;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const set = useCallback(
|
|
51
|
+
(next) => {
|
|
52
|
+
setValue((prev) => {
|
|
53
|
+
const v = typeof next === 'function' ? next(prev) : next;
|
|
54
|
+
const store = globalThis.__erPersist;
|
|
55
|
+
if (store) {
|
|
56
|
+
try {
|
|
57
|
+
store.set(key, JSON.stringify(v));
|
|
58
|
+
} catch {
|
|
59
|
+
/* unserializable value — skip persistence, keep the in-memory state */
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return v;
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
[key],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return [value, set];
|
|
69
|
+
}
|