@threenative/core 0.1.0 → 0.3.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/dist/react.js ADDED
@@ -0,0 +1,635 @@
1
+ import { createElement } from 'react';
2
+ import ReactReconciler from 'react-reconciler';
3
+ import { DiscreteEventPriority, NoEventPriority } from 'react-reconciler/constants.js';
4
+ import { PlaneGeometry, Group, InstancedMesh, MeshBasicMaterial, Matrix4, Mesh, Color } from 'three';
5
+
6
+ // src/react.ts
7
+
8
+ // src/react-glyphs.ts
9
+ var GLYPH_WIDTH = 5;
10
+ var GLYPH_HEIGHT = 7;
11
+ var GLYPH_ADVANCE = 6;
12
+ var GLYPHS = {
13
+ " ": ".".repeat(GLYPH_WIDTH * GLYPH_HEIGHT),
14
+ "0": ".###.#...##..###.#.###..##...#.###.",
15
+ "1": "..#...##....#....#....#....#...###.",
16
+ "2": ".###.#...#....#...#...#...#...#####",
17
+ "3": "#####...#...#.....#.....##...#.###.",
18
+ "4": "...#...##..#.#.#..#.#####...#....#.",
19
+ "5": "######....####.....#....##...#.###.",
20
+ "6": "..##..#...#....####.#...##...#.###.",
21
+ "7": "#####....#...#...#...#....#....#...",
22
+ "8": ".###.#...##...#.###.#...##...#.###.",
23
+ "9": ".###.#...##...#.####....#...#..##..",
24
+ A: "..#...#.#.#...##...#######...##...#",
25
+ B: "####.#...##...#####.#...##...#####.",
26
+ C: ".#####....#....#....#....#.....####",
27
+ D: "###..#..#.#...##...##...##..#.###..",
28
+ E: "######....#....####.#....#....#####",
29
+ F: "######....#....####.#....#....#....",
30
+ G: ".###.#...##....#..###...##...#.###.",
31
+ H: "#...##...##...#######...##...##...#",
32
+ I: ".###...#....#....#....#....#...###.",
33
+ J: "..###...#....#....#....#.#..#..##..",
34
+ K: "#...##..#.#.#..##...#.#..#..#.#...#",
35
+ L: "#....#....#....#....#....#....#####",
36
+ M: "#...###.###.#.##.#.##...##...##...#",
37
+ N: "#...###..###..##.#.##..###..###...#",
38
+ O: ".###.#...##...##...##...##...#.###.",
39
+ P: "####.#...##...#####.#....#....#....",
40
+ Q: ".###.#...##...##...##.#.##..#..##.#",
41
+ R: "####.#...##...#####.#.#..#..#.#...#",
42
+ S: ".#####....#.....###.....#....#####.",
43
+ T: "#####..#....#....#....#....#....#..",
44
+ U: "#...##...##...##...##...##...#.###.",
45
+ V: "#...##...##...##...##...#.#.#...#..",
46
+ W: "#...##...##...##.#.##.#.###.###...#",
47
+ X: "#...##...#.#.#...#...#.#.#...##...#",
48
+ Y: "#...##...#.#.#...#....#....#....#..",
49
+ Z: "#####....#...#...#...#...#....#####",
50
+ ":": ".......#....#.........#....#.......",
51
+ ".": "...........................#....#..",
52
+ ",": "......................#....#...#...",
53
+ "-": "...............#####...............",
54
+ "+": ".......#....#..#####..#....#.......",
55
+ "=": "..........#####.....#####..........",
56
+ "/": "....#....#...#...#...#...#....#....",
57
+ "|": "..#....#....#....#....#....#....#..",
58
+ "!": "..#....#....#....#....#.........#..",
59
+ "?": ".###.#...#....#...#...#.........#..",
60
+ "'": "..#....#...........................",
61
+ "(": "...#...#...#....#....#.....#.....#.",
62
+ ")": ".#.....#.....#....#....#...#...#...",
63
+ "[": ".###..#....#....#....#....#....###.",
64
+ "]": ".###....#....#....#....#....#..###.",
65
+ "#": ".#.#..#.#.#####.#.#.#####.#.#..#.#.",
66
+ $: "..#...#####.#...###...#.#####...#..",
67
+ "%": "##..###.#....#...#...#....#.###..##",
68
+ "*": ".....#.#.#.###.#####.###.#.#.#.....",
69
+ "\xB7": ".................#.................",
70
+ "\xB0": ".##..#..#..##......................",
71
+ "<": "...#...#...#...#.....#.....#.....#.",
72
+ ">": ".#.....#.....#.....#...#...#...#...",
73
+ _: "..............................#####"
74
+ };
75
+ function glyphPixels(character) {
76
+ const art = GLYPHS[character] ?? GLYPHS[character.toUpperCase()];
77
+ if (art === void 0) return void 0;
78
+ const pixels = [];
79
+ for (let index = 0; index < GLYPH_WIDTH * GLYPH_HEIGHT; index += 1) {
80
+ if (art[index] !== "#") continue;
81
+ pixels.push([index % GLYPH_WIDTH, Math.floor(index / GLYPH_WIDTH)]);
82
+ }
83
+ return pixels;
84
+ }
85
+ function supportedGlyphs() {
86
+ return Object.keys(GLYPHS).join("");
87
+ }
88
+
89
+ // src/react-layout.ts
90
+ var STYLE_KEYS = /* @__PURE__ */ new Set([
91
+ "left",
92
+ "right",
93
+ "top",
94
+ "bottom",
95
+ "centerX",
96
+ "centerY",
97
+ "width",
98
+ "height",
99
+ "padding",
100
+ "direction",
101
+ "gap",
102
+ "align",
103
+ "background",
104
+ "color",
105
+ "opacity",
106
+ "fontSize",
107
+ "letterSpacing",
108
+ "textAlign",
109
+ "zIndex"
110
+ ]);
111
+ function assertKnownStyle(style, elementType) {
112
+ for (const key of Object.keys(style)) {
113
+ if (STYLE_KEYS.has(key)) continue;
114
+ throw new Error(
115
+ `TN_REACT_UNKNOWN_STYLE: <${elementType}> was given style '${key}', which the native overlay does not implement. Supported: ${[...STYLE_KEYS].join(", ")}.`
116
+ );
117
+ }
118
+ }
119
+ function supportedStyleKeys() {
120
+ return [...STYLE_KEYS];
121
+ }
122
+ var DEFAULT_FONT_SIZE = 14;
123
+ function measureText(text, fontSize, letterSpacing = 0) {
124
+ if (text.length === 0) return 0;
125
+ const cell = fontSize / GLYPH_HEIGHT;
126
+ const advance = GLYPH_ADVANCE * cell + letterSpacing;
127
+ return (text.length - 1) * advance + GLYPH_WIDTH * cell;
128
+ }
129
+ function intrinsicSize(node, fontSize) {
130
+ const { style } = node;
131
+ if (node.kind === "text") {
132
+ return {
133
+ width: style.width ?? measureText(node.text, fontSize, style.letterSpacing ?? 0),
134
+ height: style.height ?? fontSize
135
+ };
136
+ }
137
+ const padding = style.padding ?? 0;
138
+ const gap = style.gap ?? 0;
139
+ let contentWidth = 0;
140
+ let contentHeight = 0;
141
+ for (const [index, child] of node.children.entries()) {
142
+ const childFont = child.style.fontSize ?? fontSize;
143
+ const size = intrinsicSize(child, childFont);
144
+ if (style.direction === "row") {
145
+ contentWidth += size.width + (index > 0 ? gap : 0);
146
+ contentHeight = Math.max(contentHeight, size.height);
147
+ } else if (style.direction === "column") {
148
+ contentHeight += size.height + (index > 0 ? gap : 0);
149
+ contentWidth = Math.max(contentWidth, size.width);
150
+ } else {
151
+ contentWidth = Math.max(contentWidth, (child.style.left ?? 0) + size.width);
152
+ contentHeight = Math.max(contentHeight, (child.style.top ?? 0) + size.height);
153
+ }
154
+ }
155
+ return {
156
+ width: style.width ?? contentWidth + padding * 2,
157
+ height: style.height ?? contentHeight + padding * 2
158
+ };
159
+ }
160
+ function crossOffset(align, available, size) {
161
+ if (align === "center") return (available - size) / 2;
162
+ if (align === "end") return available - size;
163
+ return 0;
164
+ }
165
+ function place(node, content, size) {
166
+ const { style } = node;
167
+ let x;
168
+ if (style.centerX === true) x = content.x + (content.width - size.width) / 2;
169
+ else if (style.left !== void 0) x = content.x + style.left;
170
+ else if (style.right !== void 0) x = content.x + content.width - style.right - size.width;
171
+ else x = content.x;
172
+ let y;
173
+ if (style.centerY === true) y = content.y + (content.height - size.height) / 2;
174
+ else if (style.top !== void 0) y = content.y + style.top;
175
+ else if (style.bottom !== void 0) y = content.y + content.height - style.bottom - size.height;
176
+ else y = content.y;
177
+ node.box = { x, y, width: size.width, height: size.height };
178
+ }
179
+ function layoutTree(root, width, height) {
180
+ if (!(width > 0) || !(height > 0)) {
181
+ throw new Error(
182
+ `TN_REACT_LAYOUT_EMPTY_VIEWPORT: the overlay was laid out against ${width}x${height}. A zero-sized surface draws nothing and would look identical to a crash.`
183
+ );
184
+ }
185
+ root.box = { x: 0, y: 0, width, height };
186
+ root.resolvedFontSize = root.style.fontSize ?? DEFAULT_FONT_SIZE;
187
+ arrange(root, root.resolvedFontSize);
188
+ }
189
+ function arrange(node, fontSize) {
190
+ const { style } = node;
191
+ const padding = style.padding ?? 0;
192
+ const content = {
193
+ x: node.box.x + padding,
194
+ y: node.box.y + padding,
195
+ width: Math.max(0, node.box.width - padding * 2),
196
+ height: Math.max(0, node.box.height - padding * 2)
197
+ };
198
+ const gap = style.gap ?? 0;
199
+ let cursor = style.direction === "row" ? content.x : content.y;
200
+ for (const [index, child] of node.children.entries()) {
201
+ const childFont = child.style.fontSize ?? fontSize;
202
+ child.resolvedFontSize = childFont;
203
+ const size = intrinsicSize(child, childFont);
204
+ if (style.direction === "row") {
205
+ if (index > 0) cursor += gap;
206
+ child.box = {
207
+ x: cursor,
208
+ y: content.y + crossOffset(style.align, content.height, size.height),
209
+ width: size.width,
210
+ height: size.height
211
+ };
212
+ cursor += size.width;
213
+ } else if (style.direction === "column") {
214
+ if (index > 0) cursor += gap;
215
+ child.box = {
216
+ x: content.x + crossOffset(style.align, content.width, size.width),
217
+ y: cursor,
218
+ width: size.width,
219
+ height: size.height
220
+ };
221
+ cursor += size.height;
222
+ } else {
223
+ place(child, content, size);
224
+ }
225
+ arrange(child, childFont);
226
+ }
227
+ }
228
+ function textOffset(node) {
229
+ const runWidth = measureText(node.text, node.resolvedFontSize, node.style.letterSpacing ?? 0);
230
+ if (node.style.textAlign === "center") return (node.box.width - runWidth) / 2;
231
+ if (node.style.textAlign === "right") return node.box.width - runWidth;
232
+ return 0;
233
+ }
234
+
235
+ // src/react-host.ts
236
+ var VIEW_ELEMENT = "tn-view";
237
+ var TEXT_ELEMENT = "tn-text";
238
+ var UNIT_PLANE = new PlaneGeometry(1, 1);
239
+ var HOST_CONTEXT = {};
240
+ function isHost(node) {
241
+ return node.kind !== "raw";
242
+ }
243
+ function styleOf(props, type) {
244
+ const style = props.style ?? {};
245
+ if (typeof style !== "object")
246
+ throw new Error(`TN_REACT_BAD_STYLE: <${type}> style must be an object.`);
247
+ assertKnownStyle(style, type);
248
+ return style;
249
+ }
250
+ function makeNode(type, props) {
251
+ if (type !== VIEW_ELEMENT && type !== TEXT_ELEMENT) {
252
+ throw new Error(
253
+ `TN_REACT_UNKNOWN_ELEMENT: <${type}> has no native mapping. The native overlay renders <View> and <Text> only; a DOM tag such as <div> or <svg> cannot cross, because there is no DOM and no rasteriser on the native host.`
254
+ );
255
+ }
256
+ return {
257
+ kind: type === VIEW_ELEMENT ? "view" : "text",
258
+ props,
259
+ style: styleOf(props, type),
260
+ text: "",
261
+ nodes: [],
262
+ children: [],
263
+ box: { x: 0, y: 0, width: 0, height: 0 },
264
+ resolvedFontSize: 14
265
+ };
266
+ }
267
+ function reconcileChildren(node) {
268
+ const children = [];
269
+ let text = "";
270
+ for (const child of node.nodes) {
271
+ if (isHost(child)) children.push(child);
272
+ else text += child.text;
273
+ }
274
+ node.children = children;
275
+ node.text = text;
276
+ }
277
+ function toColor(value, fallback) {
278
+ if (value === void 0) return new Color(fallback);
279
+ return new Color(value);
280
+ }
281
+ function syncObject(node, parent, order) {
282
+ order.value += 1;
283
+ const renderOrder = 1e4 + order.value;
284
+ if (node.kind === "view") {
285
+ const background = node.style.background;
286
+ if (background === void 0) {
287
+ disposeObject(node);
288
+ } else {
289
+ const mesh = node.object ?? newQuad();
290
+ node.object = mesh;
291
+ const material = mesh.material;
292
+ material.color.copy(toColor(background, 16777215));
293
+ material.opacity = node.style.opacity ?? 1;
294
+ mesh.renderOrder = renderOrder;
295
+ mesh.scale.set(Math.max(node.box.width, 0), Math.max(node.box.height, 0), 1);
296
+ mesh.position.set(node.box.x + node.box.width / 2, -(node.box.y + node.box.height / 2), 0);
297
+ parent.add(mesh);
298
+ }
299
+ } else {
300
+ syncText(node, parent, renderOrder);
301
+ }
302
+ syncChildren(node.children, parent, order);
303
+ }
304
+ function syncChildren(children, parent, order) {
305
+ const sorted = children.map((child, index) => ({ child, index })).sort(
306
+ (left, right) => (left.child.style.zIndex ?? 0) - (right.child.style.zIndex ?? 0) || left.index - right.index
307
+ );
308
+ for (const { child } of sorted) syncObject(child, parent, order);
309
+ }
310
+ function newQuad() {
311
+ const mesh = new Mesh(
312
+ UNIT_PLANE,
313
+ new MeshBasicMaterial({ depthTest: false, depthWrite: false, transparent: true })
314
+ );
315
+ mesh.frustumCulled = false;
316
+ return mesh;
317
+ }
318
+ function syncText(node, parent, renderOrder) {
319
+ const cell = node.resolvedFontSize / GLYPH_HEIGHT;
320
+ const advance = 6 * cell + (node.style.letterSpacing ?? 0);
321
+ const runs = [];
322
+ const originX = node.box.x + textOffset(node);
323
+ for (const [index, character] of [...node.text].entries()) {
324
+ const pixels = glyphPixels(character);
325
+ if (pixels === void 0) {
326
+ throw new Error(
327
+ `TN_REACT_UNKNOWN_GLYPH: '${character}' is not in the overlay glyph set, so "${node.text}" would draw with a hole in it. Supported: ${supportedGlyphs()}`
328
+ );
329
+ }
330
+ for (const [px, py] of pixels) {
331
+ runs.push([originX + index * advance + px * cell, node.box.y + py * cell]);
332
+ }
333
+ }
334
+ const needed = Math.max(runs.length, 1);
335
+ let mesh = node.object;
336
+ if (mesh === void 0 || mesh.instanceMatrix.count < needed) {
337
+ disposeObject(node);
338
+ mesh = new InstancedMesh(
339
+ UNIT_PLANE,
340
+ new MeshBasicMaterial({ depthTest: false, depthWrite: false, transparent: true }),
341
+ // Headroom on purpose: a HUD number goes 30 -> 29 -> 28 all day, and reallocating whenever
342
+ // the pixel count ticks up by one would churn a buffer every second. Grow, never shrink.
343
+ Math.max(needed * 2, 256)
344
+ );
345
+ mesh.frustumCulled = false;
346
+ node.object = mesh;
347
+ }
348
+ const material = mesh.material;
349
+ material.color.copy(toColor(node.style.color, 16777215));
350
+ material.opacity = node.style.opacity ?? 1;
351
+ mesh.renderOrder = renderOrder;
352
+ const matrix = new Matrix4();
353
+ for (const [index, [x, y]] of runs.entries()) {
354
+ matrix.makeScale(cell, cell, 1);
355
+ matrix.setPosition(x + cell / 2, -(y + cell / 2), 0);
356
+ mesh.setMatrixAt(index, matrix);
357
+ }
358
+ mesh.count = runs.length;
359
+ mesh.instanceMatrix.needsUpdate = true;
360
+ parent.add(mesh);
361
+ }
362
+ function disposeObject(node) {
363
+ const object = node.object;
364
+ if (object === void 0) return;
365
+ object.removeFromParent();
366
+ const mesh = object;
367
+ mesh.material?.dispose();
368
+ node.object = void 0;
369
+ }
370
+ function disposeTree(node) {
371
+ disposeObject(node);
372
+ for (const child of node.children) disposeTree(child);
373
+ }
374
+ var currentPriority = NoEventPriority;
375
+ var config = {
376
+ supportsMutation: true,
377
+ supportsPersistence: false,
378
+ supportsHydration: false,
379
+ isPrimaryRenderer: false,
380
+ noTimeout: -1,
381
+ scheduleTimeout: setTimeout,
382
+ cancelTimeout: clearTimeout,
383
+ createInstance: (type, props) => makeNode(type, props),
384
+ createTextInstance: (text) => ({ kind: "raw", text }),
385
+ shouldSetTextContent: () => false,
386
+ getPublicInstance: (instance) => instance,
387
+ getRootHostContext: () => HOST_CONTEXT,
388
+ getChildHostContext: () => HOST_CONTEXT,
389
+ appendInitialChild: (parent, child) => attach(parent, child),
390
+ appendChild: (parent, child) => attach(parent, child),
391
+ appendChildToContainer: (container, child) => attach(container.root, child),
392
+ insertBefore: (parent, child, before) => insert(parent, child, before),
393
+ insertInContainerBefore: (container, child, before) => insert(container.root, child, before),
394
+ removeChild: (parent, child) => detach(parent, child),
395
+ removeChildFromContainer: (container, child) => detach(container.root, child),
396
+ clearContainer: (container) => {
397
+ for (const child of [...container.root.nodes]) detach(container.root, child);
398
+ },
399
+ finalizeInitialChildren: () => false,
400
+ commitUpdate: (instance, type, _prev, next) => {
401
+ instance.props = next;
402
+ instance.style = styleOf(next, type);
403
+ },
404
+ commitTextUpdate: (instance, _old, next) => {
405
+ instance.text = next;
406
+ if (instance.parent !== void 0) reconcileChildren(instance.parent);
407
+ },
408
+ prepareForCommit: () => null,
409
+ resetAfterCommit: (container) => container.invalidate(),
410
+ preparePortalMount: () => void 0,
411
+ detachDeletedInstance: () => void 0,
412
+ beforeActiveInstanceBlur: () => void 0,
413
+ afterActiveInstanceBlur: () => void 0,
414
+ prepareScopeUpdate: () => void 0,
415
+ getInstanceFromNode: () => null,
416
+ getInstanceFromScope: () => null,
417
+ setCurrentUpdatePriority: (priority) => {
418
+ currentPriority = priority;
419
+ },
420
+ getCurrentUpdatePriority: () => currentPriority,
421
+ // Discrete, not default, and deliberately so. A default-lane update is handed to React's own
422
+ // Scheduler, which reaches for `MessageChannel` and only falls back to `setTimeout`; on the
423
+ // native host that is one more browser global to depend on for a HUD that has to repaint inside
424
+ // a fixed-step frame anyway. Discrete puts the update on the sync lane, so `flushSyncWork()`
425
+ // from the game loop is all that is ever needed and the overlay never waits on a scheduler.
426
+ resolveUpdatePriority: () => currentPriority !== NoEventPriority ? currentPriority : DiscreteEventPriority,
427
+ getCurrentEventPriority: () => DiscreteEventPriority,
428
+ shouldAttemptEagerTransition: () => false,
429
+ requestPostPaintCallback: () => void 0,
430
+ maySuspendCommit: () => false,
431
+ preloadInstance: () => true,
432
+ startSuspendingCommit: () => void 0,
433
+ suspendInstance: () => void 0,
434
+ waitForCommitToBeReady: () => null,
435
+ resetFormInstance: () => void 0,
436
+ trackSchedulerEvent: () => void 0,
437
+ resolveEventType: () => null,
438
+ resolveEventTimeStamp: () => -1.1,
439
+ // biome-ignore lint/style/useNamingConvention: exact React reconciler host API name.
440
+ NotPendingTransition: null,
441
+ // biome-ignore lint/style/useNamingConvention: exact React reconciler host API name.
442
+ HostTransitionContext: {
443
+ $$typeof: /* @__PURE__ */ Symbol.for("react.context"),
444
+ // biome-ignore lint/style/useNamingConvention: exact React context API name.
445
+ Provider: null,
446
+ // biome-ignore lint/style/useNamingConvention: exact React context API name.
447
+ Consumer: null,
448
+ _currentValue: null,
449
+ _currentValue2: null,
450
+ _threadCount: 0
451
+ }
452
+ };
453
+ function attach(parent, child) {
454
+ if (!isHost(child)) child.parent = parent;
455
+ const existing = parent.nodes.indexOf(child);
456
+ if (existing >= 0) parent.nodes.splice(existing, 1);
457
+ parent.nodes.push(child);
458
+ reconcileChildren(parent);
459
+ }
460
+ function insert(parent, child, before) {
461
+ if (child === before) return;
462
+ if (!isHost(child)) child.parent = parent;
463
+ const existing = parent.nodes.indexOf(child);
464
+ if (existing >= 0) parent.nodes.splice(existing, 1);
465
+ const at = parent.nodes.indexOf(before);
466
+ parent.nodes.splice(at < 0 ? parent.nodes.length : at, 0, child);
467
+ reconcileChildren(parent);
468
+ }
469
+ function detach(parent, child) {
470
+ const at = parent.nodes.indexOf(child);
471
+ if (at >= 0) parent.nodes.splice(at, 1);
472
+ if (isHost(child)) disposeTree(child);
473
+ reconcileChildren(parent);
474
+ }
475
+ var reconciler = ReactReconciler(config);
476
+ function createReactOverlay(options) {
477
+ const { canvasLayer } = options;
478
+ const group = new Group();
479
+ group.name = "ThreeNativeReactOverlay";
480
+ canvasLayer.scene.add(group);
481
+ const root = {
482
+ kind: "view",
483
+ props: {},
484
+ style: {},
485
+ text: "",
486
+ nodes: [],
487
+ children: [],
488
+ box: { x: 0, y: 0, width: 0, height: 0 },
489
+ resolvedFontSize: 14
490
+ };
491
+ let dirty = true;
492
+ let lastWidth = 0;
493
+ let lastHeight = 0;
494
+ let objectCount = 0;
495
+ let commitCount = 0;
496
+ let lastCommitMs;
497
+ let failure;
498
+ let failureObject;
499
+ const container = {
500
+ root,
501
+ invalidate: () => {
502
+ dirty = true;
503
+ }
504
+ };
505
+ const report = (error) => {
506
+ const message = error instanceof Error ? error.message : String(error);
507
+ failure = { message, source: "react" };
508
+ dirty = true;
509
+ const named = new Error(`TN_REACT_OVERLAY_FAILED: ${message}`);
510
+ if (options.onError !== void 0) options.onError(named);
511
+ else console.error(named.message);
512
+ };
513
+ const fiberRoot = reconciler.createContainer(
514
+ container,
515
+ 0,
516
+ null,
517
+ false,
518
+ null,
519
+ "tn",
520
+ report,
521
+ report,
522
+ report,
523
+ null
524
+ );
525
+ const draw = () => {
526
+ const { camera } = canvasLayer;
527
+ const width = camera.right - camera.left;
528
+ const height = camera.top - camera.bottom;
529
+ if (!dirty && width === lastWidth && height === lastHeight) return;
530
+ lastWidth = width;
531
+ lastHeight = height;
532
+ dirty = false;
533
+ group.position.set(-width / 2, height / 2, 0);
534
+ try {
535
+ if (failure?.source === "react") {
536
+ showFailure(failure.message, width);
537
+ return;
538
+ }
539
+ layoutTree(root, width, height);
540
+ const order = { value: 0 };
541
+ syncChildren(root.children, group, order);
542
+ objectCount = order.value;
543
+ failure = void 0;
544
+ clearFailure();
545
+ } catch (error) {
546
+ const message = error instanceof Error ? error.message : String(error);
547
+ failure = { message, source: "draw" };
548
+ if (options.onError !== void 0) options.onError(new Error(message));
549
+ else console.error(message);
550
+ showFailure(message, width);
551
+ }
552
+ };
553
+ const clearFailure = () => {
554
+ if (failureObject !== void 0) {
555
+ failureObject.removeFromParent();
556
+ failureObject.material?.dispose();
557
+ failureObject = void 0;
558
+ }
559
+ for (const child of group.children) child.visible = true;
560
+ };
561
+ const showFailure = (message, width) => {
562
+ clearFailure();
563
+ for (const child of group.children) child.visible = false;
564
+ failureObject = drawFailure(group, message, width);
565
+ };
566
+ const stopResize = canvasLayer.onResize(() => {
567
+ dirty = true;
568
+ draw();
569
+ });
570
+ return {
571
+ render(element) {
572
+ failure = void 0;
573
+ reconciler.updateContainerSync(element, fiberRoot, null, null);
574
+ reconciler.flushSyncWork();
575
+ draw();
576
+ },
577
+ refresh() {
578
+ const startedAt = globalThis.performance?.now() ?? Date.now();
579
+ const wasDirty = dirty;
580
+ reconciler.flushSyncWork();
581
+ const committed = dirty && !wasDirty;
582
+ draw();
583
+ if (committed) {
584
+ commitCount += 1;
585
+ lastCommitMs = (globalThis.performance?.now() ?? Date.now()) - startedAt;
586
+ }
587
+ },
588
+ dispose() {
589
+ stopResize();
590
+ reconciler.updateContainerSync(null, fiberRoot, null, null);
591
+ reconciler.flushSyncWork();
592
+ clearFailure();
593
+ disposeTree(root);
594
+ group.removeFromParent();
595
+ },
596
+ get objectCount() {
597
+ return objectCount;
598
+ },
599
+ get commitCount() {
600
+ return commitCount;
601
+ },
602
+ get lastCommitMs() {
603
+ return lastCommitMs;
604
+ }
605
+ };
606
+ }
607
+ function drawFailure(group, message, width) {
608
+ const line = message.split(":")[0] ?? "TN_REACT_OVERLAY_FAILED";
609
+ const node = {
610
+ kind: "text",
611
+ props: {},
612
+ style: { color: "#ff5f4d", fontSize: 18 },
613
+ text: line.slice(0, 48).toUpperCase(),
614
+ nodes: [],
615
+ children: [],
616
+ box: { x: 12, y: 12, width: Math.max(width - 24, 1), height: 18 },
617
+ resolvedFontSize: 18
618
+ };
619
+ try {
620
+ syncText(node, group, 2e4);
621
+ return node.object;
622
+ } catch {
623
+ return void 0;
624
+ }
625
+ }
626
+
627
+ // src/react.ts
628
+ function View(props) {
629
+ return createElement(VIEW_ELEMENT, { style: props.style }, props.children);
630
+ }
631
+ function Text(props) {
632
+ return createElement(TEXT_ELEMENT, { style: props.style }, props.children);
633
+ }
634
+
635
+ export { TEXT_ELEMENT, Text, VIEW_ELEMENT, View, createReactOverlay, measureText, supportedGlyphs, supportedStyleKeys };