@energy8platform/game-engine 0.33.6 → 0.33.8
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/flow.cjs.js +155 -86
- package/dist/flow.cjs.js.map +1 -1
- package/dist/flow.d.ts +92 -3
- package/dist/flow.esm.js +155 -86
- package/dist/flow.esm.js.map +1 -1
- package/dist/host.cjs.js +19 -8
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.esm.js +19 -8
- package/dist/host.esm.js.map +1 -1
- package/dist/scene-devtools.cjs.js +767 -3
- package/dist/scene-devtools.cjs.js.map +1 -1
- package/dist/scene-devtools.d.ts +256 -3
- package/dist/scene-devtools.esm.js +765 -4
- package/dist/scene-devtools.esm.js.map +1 -1
- package/dist/scene.cjs.js +1359 -21
- package/dist/scene.cjs.js.map +1 -1
- package/dist/scene.d.ts +608 -4
- package/dist/scene.esm.js +1347 -22
- package/dist/scene.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/host/balanceGate.ts +21 -10
- package/src/host/createSlotGame.ts +7 -5
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Container, Graphics, Rectangle } from 'pixi.js';
|
|
2
|
+
|
|
1
3
|
// Pure helpers behind the scene inspector — kept renderer/DOM-free so the control
|
|
2
4
|
// inference and layout-field enumeration are unit-testable and reusable (the same
|
|
3
5
|
// mapping later feeds schema-driven autogen and the agent's docs).
|
|
@@ -80,7 +82,7 @@ function setRulePath(rule, path, value) {
|
|
|
80
82
|
// a game's preview page, later a harness panel) mount it into any container element.
|
|
81
83
|
// Every edit leaves through a ScenePatch, so a mouse edit and an agent edit are the
|
|
82
84
|
// same operation on the same doc.
|
|
83
|
-
const STYLE = `
|
|
85
|
+
const STYLE$2 = `
|
|
84
86
|
.e8si { font: 12px/1.45 system-ui, sans-serif; color: #dfe6f5; display: flex; flex-direction: column; height: 100%; min-height: 0; }
|
|
85
87
|
.e8si h3 { margin: 0; padding: 8px 10px 6px; font-size: 11px; letter-spacing: 1px; text-transform: uppercase; color: #7d8ab0; }
|
|
86
88
|
.e8si-tree { overflow: auto; max-height: 42%; border-bottom: 1px solid #1c2540; padding-bottom: 6px; }
|
|
@@ -112,13 +114,17 @@ function createSceneInspector(opts) {
|
|
|
112
114
|
const root = document.createElement('div');
|
|
113
115
|
root.className = 'e8si';
|
|
114
116
|
const style = document.createElement('style');
|
|
115
|
-
style.textContent = STYLE;
|
|
117
|
+
style.textContent = STYLE$2;
|
|
116
118
|
root.appendChild(style);
|
|
119
|
+
const showTree = opts.showTree ?? true;
|
|
117
120
|
const treeHost = document.createElement('div');
|
|
118
121
|
treeHost.className = 'e8si-tree';
|
|
119
122
|
const inspHost = document.createElement('div');
|
|
120
123
|
inspHost.className = 'e8si-insp';
|
|
121
|
-
|
|
124
|
+
if (showTree)
|
|
125
|
+
root.append(treeHost, inspHost);
|
|
126
|
+
else
|
|
127
|
+
root.append(inspHost);
|
|
122
128
|
container.appendChild(root);
|
|
123
129
|
let selectedId = null;
|
|
124
130
|
// Edits from our own inputs schedule a refresh that must not rebuild the form under
|
|
@@ -149,6 +155,8 @@ function createSceneInspector(opts) {
|
|
|
149
155
|
};
|
|
150
156
|
// ── Outliner ────────────────────────────────────────────────────────────────
|
|
151
157
|
const renderTree = () => {
|
|
158
|
+
if (!showTree)
|
|
159
|
+
return;
|
|
152
160
|
treeHost.textContent = '';
|
|
153
161
|
const title = document.createElement('h3');
|
|
154
162
|
title.textContent = 'Scene';
|
|
@@ -213,6 +221,13 @@ function createSceneInspector(opts) {
|
|
|
213
221
|
meta.textContent = `${node.type}${node.name ? ` · ${node.name}` : ''}${node.anchorName ? ` · @${node.anchorName}` : ''}${node.visibleWhen ? ` · when: ${node.visibleWhen}` : ''}`;
|
|
214
222
|
head.append(idEl, meta);
|
|
215
223
|
inspHost.appendChild(head);
|
|
224
|
+
if (opts.onCalibrate && node.props?.inner && typeof node.props.inner.left === 'number') {
|
|
225
|
+
const btn = document.createElement('button');
|
|
226
|
+
btn.className = 'e8si-apply';
|
|
227
|
+
btn.textContent = '⊞ Calibrate inner (drag edges on canvas)';
|
|
228
|
+
btn.addEventListener('click', () => opts.onCalibrate(id));
|
|
229
|
+
inspHost.appendChild(btn);
|
|
230
|
+
}
|
|
216
231
|
// State selector (named prop variants).
|
|
217
232
|
const stateNames = Object.keys(node.states ?? {});
|
|
218
233
|
if (stateNames.length > 0) {
|
|
@@ -368,5 +383,751 @@ function createSceneInspector(opts) {
|
|
|
368
383
|
};
|
|
369
384
|
}
|
|
370
385
|
|
|
371
|
-
|
|
386
|
+
// Flow trace timeline — the visual half of the first-class Trace. Rows are step
|
|
387
|
+
// beats positioned proportionally on the run's time axis; the same data the agent
|
|
388
|
+
// asserts on, rendered for the human's timing eye ("фил" тайминга, §3.5).
|
|
389
|
+
const STYLE$1 = `
|
|
390
|
+
.e8ft { font: 11px/1.5 ui-monospace, monospace; color: #dfe6f5; }
|
|
391
|
+
.e8ft h3 { margin: 0; padding: 8px 10px 4px; font: 11px/1 system-ui, sans-serif; letter-spacing: 1px; text-transform: uppercase; color: #7d8ab0; }
|
|
392
|
+
.e8ft-meta { padding: 0 10px 6px; color: #8fa0c9; font-family: system-ui, sans-serif; }
|
|
393
|
+
.e8ft-row { display: flex; align-items: center; gap: 6px; padding: 1px 10px; white-space: nowrap; }
|
|
394
|
+
.e8ft-t { flex: 0 0 52px; text-align: right; color: #7d8ab0; }
|
|
395
|
+
.e8ft-track { flex: 1; position: relative; height: 12px; background: #0d1428; border-radius: 3px; overflow: hidden; }
|
|
396
|
+
.e8ft-dot { position: absolute; top: 1px; width: 10px; height: 10px; border-radius: 2px; }
|
|
397
|
+
.e8ft-label { flex: 0 0 46%; overflow: hidden; text-overflow: ellipsis; }
|
|
398
|
+
.e8ft-label .n { color: #8fa0c9; }
|
|
399
|
+
`;
|
|
400
|
+
const KIND_COLORS = {
|
|
401
|
+
tween: '#4dd0ff',
|
|
402
|
+
sound: '#ffd24a',
|
|
403
|
+
wait: '#55617f',
|
|
404
|
+
countUp: '#6ad55a',
|
|
405
|
+
setProps: '#c97fff',
|
|
406
|
+
setState: '#ff8a5a',
|
|
407
|
+
setVar: '#ff8a5a',
|
|
408
|
+
if: '#9b9bc9',
|
|
409
|
+
parallel: '#9b9bc9',
|
|
410
|
+
seq: '#9b9bc9',
|
|
411
|
+
code: '#ff4d8d',
|
|
412
|
+
};
|
|
413
|
+
const colorFor = (kind) => {
|
|
414
|
+
if (KIND_COLORS[kind])
|
|
415
|
+
return KIND_COLORS[kind];
|
|
416
|
+
let hash = 0;
|
|
417
|
+
for (const ch of kind)
|
|
418
|
+
hash = (hash * 31 + ch.charCodeAt(0)) % 360;
|
|
419
|
+
return `hsl(${hash} 70% 60%)`;
|
|
420
|
+
};
|
|
421
|
+
function createFlowTimeline(opts) {
|
|
422
|
+
const root = document.createElement('div');
|
|
423
|
+
root.className = 'e8ft';
|
|
424
|
+
const style = document.createElement('style');
|
|
425
|
+
style.textContent = STYLE$1;
|
|
426
|
+
root.appendChild(style);
|
|
427
|
+
const body = document.createElement('div');
|
|
428
|
+
root.appendChild(body);
|
|
429
|
+
opts.container.appendChild(root);
|
|
430
|
+
return {
|
|
431
|
+
show(trace) {
|
|
432
|
+
body.textContent = '';
|
|
433
|
+
const title = document.createElement('h3');
|
|
434
|
+
title.textContent = `Flow: ${trace.event}`;
|
|
435
|
+
const meta = document.createElement('div');
|
|
436
|
+
meta.className = 'e8ft-meta';
|
|
437
|
+
meta.textContent = `${trace.entries.length} steps · ${trace.durationMs}ms · turbo ×${trace.turbo}${trace.skipped ? ' · SKIPPED→settle' : ''}`;
|
|
438
|
+
body.append(title, meta);
|
|
439
|
+
if (opts.onScrub) {
|
|
440
|
+
const hint = document.createElement('div');
|
|
441
|
+
hint.className = 'e8ft-meta';
|
|
442
|
+
hint.textContent = 'click a beat to scrub the scene to that moment';
|
|
443
|
+
body.appendChild(hint);
|
|
444
|
+
}
|
|
445
|
+
const total = Math.max(1, trace.durationMs);
|
|
446
|
+
trace.entries.forEach((entry, index) => {
|
|
447
|
+
const row = document.createElement('div');
|
|
448
|
+
row.className = 'e8ft-row';
|
|
449
|
+
if (opts.onScrub) {
|
|
450
|
+
row.style.cursor = 'pointer';
|
|
451
|
+
row.addEventListener('click', () => opts.onScrub(trace.event, index));
|
|
452
|
+
row.addEventListener('mouseenter', () => (row.style.background = '#141c36'));
|
|
453
|
+
row.addEventListener('mouseleave', () => (row.style.background = ''));
|
|
454
|
+
}
|
|
455
|
+
const t = document.createElement('span');
|
|
456
|
+
t.className = 'e8ft-t';
|
|
457
|
+
t.textContent = `+${entry.t}`;
|
|
458
|
+
const track = document.createElement('span');
|
|
459
|
+
track.className = 'e8ft-track';
|
|
460
|
+
const dot = document.createElement('span');
|
|
461
|
+
dot.className = 'e8ft-dot';
|
|
462
|
+
dot.style.left = `${Math.min(98, (entry.t / total) * 100)}%`;
|
|
463
|
+
dot.style.background = colorFor(entry.do);
|
|
464
|
+
track.appendChild(dot);
|
|
465
|
+
const label = document.createElement('span');
|
|
466
|
+
label.className = 'e8ft-label';
|
|
467
|
+
label.innerHTML = '';
|
|
468
|
+
label.textContent = entry.do;
|
|
469
|
+
if (entry.node || entry.info) {
|
|
470
|
+
const n = document.createElement('span');
|
|
471
|
+
n.className = 'n';
|
|
472
|
+
n.textContent = ` ${entry.node ?? ''}${entry.info ? ` — ${entry.info}` : ''}`;
|
|
473
|
+
label.appendChild(n);
|
|
474
|
+
}
|
|
475
|
+
row.append(t, track, label);
|
|
476
|
+
body.appendChild(row);
|
|
477
|
+
});
|
|
478
|
+
},
|
|
479
|
+
clear() {
|
|
480
|
+
body.textContent = '';
|
|
481
|
+
},
|
|
482
|
+
destroy() {
|
|
483
|
+
root.remove();
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Flow doc editor — the low-code half of goal 3. Edits the LIVE FlowDoc object in place
|
|
489
|
+
// (the runner reads doc.on at fire-time, so the very next spin plays the edited flow);
|
|
490
|
+
// the host persists via its /__write route. Scalar fields get typed inputs, structured
|
|
491
|
+
// fields fall back to JSON — the same schema-less inference as the scene inspector,
|
|
492
|
+
// upgraded automatically once step contributions carry schemas.
|
|
493
|
+
const STYLE = `
|
|
494
|
+
.e8fe { font: 12px/1.5 system-ui, sans-serif; color: #dfe6f5; }
|
|
495
|
+
.e8fe h3 { margin: 0; padding: 8px 10px 4px; font-size: 11px; letter-spacing: 1px; text-transform: uppercase; color: #7d8ab0; }
|
|
496
|
+
.e8fe-ev { padding: 4px 10px 2px; color: #9fb0d8; font-weight: 700; }
|
|
497
|
+
.e8fe-step { display: flex; align-items: center; gap: 4px; padding: 1px 10px 1px 18px; cursor: pointer; white-space: nowrap; }
|
|
498
|
+
.e8fe-step:hover { background: #141c36; }
|
|
499
|
+
.e8fe-step.sel { background: #1d2b55; }
|
|
500
|
+
.e8fe-step .k { color: #4dd0ff; }
|
|
501
|
+
.e8fe-step .s { color: #8fa0c9; overflow: hidden; text-overflow: ellipsis; }
|
|
502
|
+
.e8fe-step .sp { flex: 1; }
|
|
503
|
+
.e8fe-step button { background: none; border: none; color: #7d8ab0; cursor: pointer; padding: 0 3px; font: inherit; }
|
|
504
|
+
.e8fe-step button:hover { color: #dfe6f5; }
|
|
505
|
+
.e8fe-form { padding: 2px 10px 6px 18px; border-left: 2px solid #1d2b55; margin-left: 14px; }
|
|
506
|
+
.e8fe-field { display: flex; align-items: center; gap: 6px; padding: 1px 0; }
|
|
507
|
+
.e8fe-field label { flex: 0 0 84px; color: #9fb0d8; }
|
|
508
|
+
.e8fe-field input, .e8fe-field textarea { flex: 1; min-width: 0; background: #0d1428; color: #dfe6f5; border: 1px solid #2b3866; border-radius: 6px; padding: 2px 6px; font: inherit; }
|
|
509
|
+
.e8fe-add { display: flex; gap: 6px; padding: 3px 10px 8px 18px; }
|
|
510
|
+
.e8fe-add select, .e8fe-add button { background: #18213d; color: #dfe6f5; border: 1px solid #2b3866; border-radius: 6px; padding: 2px 8px; font: inherit; cursor: pointer; }
|
|
511
|
+
`;
|
|
512
|
+
function summarize(step) {
|
|
513
|
+
const s = step;
|
|
514
|
+
const bits = [];
|
|
515
|
+
for (const key of ['node', 'cue', 'name', 'state', 'when', 'ref', 'ms', 'to']) {
|
|
516
|
+
if (s[key] !== undefined && typeof s[key] !== 'object')
|
|
517
|
+
bits.push(`${key}:${String(s[key])}`);
|
|
518
|
+
}
|
|
519
|
+
if (step.do === 'parallel')
|
|
520
|
+
bits.push(`${s.steps.length} tracks`);
|
|
521
|
+
if (step.do === 'seq' || step.do === 'if')
|
|
522
|
+
bits.push('…');
|
|
523
|
+
return bits.join(' ');
|
|
524
|
+
}
|
|
525
|
+
const DEFAULT_KINDS = ['wait', 'sound', 'tween', 'setState', 'setVar', 'setProps', 'countUp', 'if', 'parallel', 'seq', 'forEach', 'code'];
|
|
526
|
+
function createFlowEditor(opts) {
|
|
527
|
+
const { doc } = opts;
|
|
528
|
+
const root = document.createElement('div');
|
|
529
|
+
root.className = 'e8fe';
|
|
530
|
+
const style = document.createElement('style');
|
|
531
|
+
style.textContent = STYLE;
|
|
532
|
+
root.appendChild(style);
|
|
533
|
+
const body = document.createElement('div');
|
|
534
|
+
root.appendChild(body);
|
|
535
|
+
opts.container.appendChild(root);
|
|
536
|
+
let selected = null;
|
|
537
|
+
const changed = () => {
|
|
538
|
+
opts.onChange?.();
|
|
539
|
+
render();
|
|
540
|
+
};
|
|
541
|
+
const stepForm = (step) => {
|
|
542
|
+
const form = document.createElement('div');
|
|
543
|
+
form.className = 'e8fe-form';
|
|
544
|
+
const record = step;
|
|
545
|
+
for (const [key, value] of Object.entries(record)) {
|
|
546
|
+
if (key === 'do')
|
|
547
|
+
continue;
|
|
548
|
+
const field = document.createElement('div');
|
|
549
|
+
field.className = 'e8fe-field';
|
|
550
|
+
const label = document.createElement('label');
|
|
551
|
+
label.textContent = key;
|
|
552
|
+
field.appendChild(label);
|
|
553
|
+
const kind = propControlKind(value);
|
|
554
|
+
if (kind === 'json') {
|
|
555
|
+
const area = document.createElement('textarea');
|
|
556
|
+
area.rows = 3;
|
|
557
|
+
area.value = JSON.stringify(value);
|
|
558
|
+
area.addEventListener('change', () => {
|
|
559
|
+
try {
|
|
560
|
+
record[key] = JSON.parse(area.value);
|
|
561
|
+
changed();
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
area.style.borderColor = '#ff4d8d';
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
field.appendChild(area);
|
|
568
|
+
}
|
|
569
|
+
else if (kind === 'boolean') {
|
|
570
|
+
const input = document.createElement('input');
|
|
571
|
+
input.type = 'checkbox';
|
|
572
|
+
input.checked = value === true;
|
|
573
|
+
input.addEventListener('change', () => {
|
|
574
|
+
record[key] = input.checked;
|
|
575
|
+
changed();
|
|
576
|
+
});
|
|
577
|
+
field.appendChild(input);
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
const input = document.createElement('input');
|
|
581
|
+
input.type = kind === 'number' ? 'number' : 'text';
|
|
582
|
+
input.value = String(value);
|
|
583
|
+
input.addEventListener('change', () => {
|
|
584
|
+
const raw = kind === 'number' ? Number(input.value) : input.value;
|
|
585
|
+
if (kind === 'number' && !Number.isFinite(raw))
|
|
586
|
+
return;
|
|
587
|
+
record[key] = raw;
|
|
588
|
+
changed();
|
|
589
|
+
});
|
|
590
|
+
field.appendChild(input);
|
|
591
|
+
}
|
|
592
|
+
form.appendChild(field);
|
|
593
|
+
}
|
|
594
|
+
return form;
|
|
595
|
+
};
|
|
596
|
+
const render = () => {
|
|
597
|
+
body.textContent = '';
|
|
598
|
+
const title = document.createElement('h3');
|
|
599
|
+
title.textContent = 'Flow steps';
|
|
600
|
+
body.appendChild(title);
|
|
601
|
+
for (const [event, steps] of Object.entries(doc.on)) {
|
|
602
|
+
const header = document.createElement('div');
|
|
603
|
+
header.className = 'e8fe-ev';
|
|
604
|
+
header.textContent = `on ${event}`;
|
|
605
|
+
body.appendChild(header);
|
|
606
|
+
steps.forEach((step, index) => {
|
|
607
|
+
const row = document.createElement('div');
|
|
608
|
+
row.className = `e8fe-step${selected?.event === event && selected.index === index ? ' sel' : ''}`;
|
|
609
|
+
const k = document.createElement('span');
|
|
610
|
+
k.className = 'k';
|
|
611
|
+
k.textContent = step.do;
|
|
612
|
+
const s = document.createElement('span');
|
|
613
|
+
s.className = 's';
|
|
614
|
+
s.textContent = summarize(step);
|
|
615
|
+
const spacer = document.createElement('span');
|
|
616
|
+
spacer.className = 'sp';
|
|
617
|
+
row.append(k, s, spacer);
|
|
618
|
+
const button = (text, title, fn) => {
|
|
619
|
+
const b = document.createElement('button');
|
|
620
|
+
b.textContent = text;
|
|
621
|
+
b.title = title;
|
|
622
|
+
b.addEventListener('click', (e) => {
|
|
623
|
+
e.stopPropagation();
|
|
624
|
+
fn();
|
|
625
|
+
});
|
|
626
|
+
row.appendChild(b);
|
|
627
|
+
};
|
|
628
|
+
button('↑', 'move up', () => {
|
|
629
|
+
if (index === 0)
|
|
630
|
+
return;
|
|
631
|
+
[steps[index - 1], steps[index]] = [steps[index], steps[index - 1]];
|
|
632
|
+
changed();
|
|
633
|
+
});
|
|
634
|
+
button('↓', 'move down', () => {
|
|
635
|
+
if (index >= steps.length - 1)
|
|
636
|
+
return;
|
|
637
|
+
[steps[index + 1], steps[index]] = [steps[index], steps[index + 1]];
|
|
638
|
+
changed();
|
|
639
|
+
});
|
|
640
|
+
button('✕', 'delete step', () => {
|
|
641
|
+
steps.splice(index, 1);
|
|
642
|
+
if (selected?.event === event && selected.index === index)
|
|
643
|
+
selected = null;
|
|
644
|
+
changed();
|
|
645
|
+
});
|
|
646
|
+
row.addEventListener('click', () => {
|
|
647
|
+
selected = selected?.event === event && selected.index === index ? null : { event, index };
|
|
648
|
+
render();
|
|
649
|
+
});
|
|
650
|
+
body.appendChild(row);
|
|
651
|
+
if (selected?.event === event && selected.index === index)
|
|
652
|
+
body.appendChild(stepForm(step));
|
|
653
|
+
});
|
|
654
|
+
const add = document.createElement('div');
|
|
655
|
+
add.className = 'e8fe-add';
|
|
656
|
+
const select = document.createElement('select');
|
|
657
|
+
for (const kind of opts.kinds ?? DEFAULT_KINDS) {
|
|
658
|
+
const option = document.createElement('option');
|
|
659
|
+
option.value = kind;
|
|
660
|
+
option.textContent = kind;
|
|
661
|
+
select.appendChild(option);
|
|
662
|
+
}
|
|
663
|
+
const addBtn = document.createElement('button');
|
|
664
|
+
addBtn.textContent = '+ step';
|
|
665
|
+
addBtn.addEventListener('click', () => {
|
|
666
|
+
const kind = select.value;
|
|
667
|
+
const skeletons = {
|
|
668
|
+
wait: { do: 'wait', ms: 300 },
|
|
669
|
+
sound: { do: 'sound', cue: Object.keys(doc.cues ?? {})[0] ?? 'cue' },
|
|
670
|
+
tween: { do: 'tween', node: 'node', to: { alpha: 1 }, ms: 300 },
|
|
671
|
+
setState: { do: 'setState', node: 'node', state: null },
|
|
672
|
+
setVar: { do: 'setVar', name: 'mode', value: 'BASE' },
|
|
673
|
+
setProps: { do: 'setProps', node: 'node', props: {} },
|
|
674
|
+
countUp: { do: 'countUp', node: 'node', to: '$win', ms: 1000 },
|
|
675
|
+
if: { do: 'if', when: 'win >= 1', then: [], else: [] },
|
|
676
|
+
parallel: { do: 'parallel', steps: [[], []] },
|
|
677
|
+
seq: { do: 'seq', steps: [] },
|
|
678
|
+
forEach: { do: 'forEach', items: '$items', as: 'item', mode: 'parallel', staggerMs: 90, steps: [] },
|
|
679
|
+
code: { do: 'code', ref: 'ref' },
|
|
680
|
+
};
|
|
681
|
+
steps.push(skeletons[kind] ?? { do: kind });
|
|
682
|
+
changed();
|
|
683
|
+
});
|
|
684
|
+
add.append(select, addBtn);
|
|
685
|
+
body.appendChild(add);
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
render();
|
|
689
|
+
return {
|
|
690
|
+
refresh: render,
|
|
691
|
+
destroy() {
|
|
692
|
+
root.remove();
|
|
693
|
+
},
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Pure transform math for the canvas gizmo: turn a drag into a new LayoutRule, writing
|
|
698
|
+
// the RIGHT field for the rule's kind — the thing the user asked for ("resize on the
|
|
699
|
+
// canvas, not just the sidebar"). Resize maps to a rule's natural size lever:
|
|
700
|
+
// • widthFrac/heightFrac (viewport-fraction) or wFrac/hFrac (frame-fraction) — per-axis;
|
|
701
|
+
// • otherwise a uniform `scale` (absolute / cover / grid-cell / pin / plain fraction).
|
|
702
|
+
// The engine re-places the node by its own anchor after the lever changes, so the anchor
|
|
703
|
+
// stays pinned for free — no position juggling here. Rotation writes `rotation`.
|
|
704
|
+
/** Which size lever a rule exposes — decides how a resize drag is interpreted. */
|
|
705
|
+
function sizeLever(rule) {
|
|
706
|
+
if (rule.mode === 'viewport-fraction' && (rule.widthFrac !== undefined || rule.heightFrac !== undefined))
|
|
707
|
+
return 'wh-frac';
|
|
708
|
+
if (rule.mode === 'frame-fraction' && (rule.wFrac !== undefined || rule.hFrac !== undefined))
|
|
709
|
+
return 'wh-box';
|
|
710
|
+
if (rule.mode === 'frame-fraction')
|
|
711
|
+
return 'none'; // point placement inside a frame — no size
|
|
712
|
+
return 'scale';
|
|
713
|
+
}
|
|
714
|
+
const clampFactor = (f) => (Number.isFinite(f) && f > 0.01 ? f : 0.01);
|
|
715
|
+
const round4 = (n) => Math.round(n * 1e4) / 1e4;
|
|
716
|
+
/**
|
|
717
|
+
* Multiply a rule's size lever by (factorX, factorY). Corners pass equal factors (uniform);
|
|
718
|
+
* edges pass 1 on the untouched axis. Uniform levers (`scale`) use factorX.
|
|
719
|
+
*/
|
|
720
|
+
function resizeRule(rule, factorX, factorY) {
|
|
721
|
+
const fx = clampFactor(factorX);
|
|
722
|
+
const fy = clampFactor(factorY);
|
|
723
|
+
const next = JSON.parse(JSON.stringify(rule));
|
|
724
|
+
switch (sizeLever(rule)) {
|
|
725
|
+
case 'wh-frac': {
|
|
726
|
+
const r = next;
|
|
727
|
+
if (r.widthFrac !== undefined)
|
|
728
|
+
r.widthFrac = round4(r.widthFrac * fx);
|
|
729
|
+
if (r.heightFrac !== undefined)
|
|
730
|
+
r.heightFrac = round4(r.heightFrac * fy);
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
case 'wh-box': {
|
|
734
|
+
const r = next;
|
|
735
|
+
if (r.wFrac !== undefined)
|
|
736
|
+
r.wFrac = round4(r.wFrac * fx);
|
|
737
|
+
if (r.hFrac !== undefined)
|
|
738
|
+
r.hFrac = round4(r.hFrac * fy);
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
case 'scale': {
|
|
742
|
+
const r = next;
|
|
743
|
+
r.scale = round4((r.scale ?? 1) * fx);
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return next;
|
|
748
|
+
}
|
|
749
|
+
function setRuleRotation(rule, radians) {
|
|
750
|
+
const next = JSON.parse(JSON.stringify(rule));
|
|
751
|
+
next.rotation = round4(radians);
|
|
752
|
+
return next;
|
|
753
|
+
}
|
|
754
|
+
/** Nudge a rule's final pixel offset by a screen delta already converted to design units. */
|
|
755
|
+
function nudgeRule(rule, dxDesign, dyDesign) {
|
|
756
|
+
const next = JSON.parse(JSON.stringify(rule));
|
|
757
|
+
next.pxNudge = { x: Math.round((next.pxNudge?.x ?? 0) + dxDesign), y: Math.round((next.pxNudge?.y ?? 0) + dyDesign) };
|
|
758
|
+
return next;
|
|
759
|
+
}
|
|
760
|
+
/** Screen position of each handle for a bounding box. */
|
|
761
|
+
function handlePoint(box, id) {
|
|
762
|
+
const { x, y, width: w, height: h } = box;
|
|
763
|
+
const cx = x + w / 2;
|
|
764
|
+
const cy = y + h / 2;
|
|
765
|
+
switch (id) {
|
|
766
|
+
case 'nw': return { x, y };
|
|
767
|
+
case 'n': return { x: cx, y };
|
|
768
|
+
case 'ne': return { x: x + w, y };
|
|
769
|
+
case 'e': return { x: x + w, y: cy };
|
|
770
|
+
case 'se': return { x: x + w, y: y + h };
|
|
771
|
+
case 's': return { x: cx, y: y + h };
|
|
772
|
+
case 'sw': return { x, y: y + h };
|
|
773
|
+
case 'w': return { x, y: cy };
|
|
774
|
+
case 'rotate': return { x: cx, y: y - 28 };
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Resize factors from dragging a handle, using the OPPOSITE edge/corner as the fixed
|
|
779
|
+
* reference — the intuitive "grab a corner, drag out, it grows" feel.
|
|
780
|
+
*/
|
|
781
|
+
function resizeFactors(startBox, handle, pointer) {
|
|
782
|
+
const left = startBox.x;
|
|
783
|
+
const right = startBox.x + startBox.width;
|
|
784
|
+
const top = startBox.y;
|
|
785
|
+
const bottom = startBox.y + startBox.height;
|
|
786
|
+
const w = startBox.width || 1;
|
|
787
|
+
const h = startBox.height || 1;
|
|
788
|
+
const westX = () => Math.abs(pointer.x - right) / w; // dragging the west side, east fixed
|
|
789
|
+
const eastX = () => Math.abs(pointer.x - left) / w;
|
|
790
|
+
const northY = () => Math.abs(pointer.y - bottom) / h;
|
|
791
|
+
const southY = () => Math.abs(pointer.y - top) / h;
|
|
792
|
+
switch (handle) {
|
|
793
|
+
case 'w': return { factorX: westX(), factorY: 1 };
|
|
794
|
+
case 'e': return { factorX: eastX(), factorY: 1 };
|
|
795
|
+
case 'n': return { factorX: 1, factorY: northY() };
|
|
796
|
+
case 's': return { factorX: 1, factorY: southY() };
|
|
797
|
+
case 'nw': {
|
|
798
|
+
const f = uniform(westX(), northY());
|
|
799
|
+
return { factorX: f, factorY: f };
|
|
800
|
+
}
|
|
801
|
+
case 'ne': {
|
|
802
|
+
const f = uniform(eastX(), northY());
|
|
803
|
+
return { factorX: f, factorY: f };
|
|
804
|
+
}
|
|
805
|
+
case 'sw': {
|
|
806
|
+
const f = uniform(westX(), southY());
|
|
807
|
+
return { factorX: f, factorY: f };
|
|
808
|
+
}
|
|
809
|
+
case 'se': {
|
|
810
|
+
const f = uniform(eastX(), southY());
|
|
811
|
+
return { factorX: f, factorY: f };
|
|
812
|
+
}
|
|
813
|
+
default: return { factorX: 1, factorY: 1 };
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
/** Corner resize keeps aspect — use the diagonal ratio of the two per-axis factors. */
|
|
817
|
+
function uniform(a, b) {
|
|
818
|
+
return Math.sqrt(Math.max(0.0001, a) * Math.max(0.0001, b));
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Transform gizmo — the on-canvas resize / rotate / move manipulator for the selected
|
|
822
|
+
// node. Generalizes the frame calibrator: 8 resize handles + a rotate handle, and a body
|
|
823
|
+
// drag to move. Every gesture emits ONE set-layout ScenePatch (orientation-aware), so a
|
|
824
|
+
// mouse transform and an agent/inspector transform are the same operation. Live bounds are
|
|
825
|
+
// re-read each frame, so the gizmo follows the node as the engine relayouts.
|
|
826
|
+
const CORNERS = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
|
|
827
|
+
const HS = 9; // handle size
|
|
828
|
+
function createTransformGizmo(opts) {
|
|
829
|
+
const { handle, app } = opts;
|
|
830
|
+
const applyPatch = opts.applyPatch ?? handle.patch;
|
|
831
|
+
const getOrientation = opts.getOrientation ?? (() => 'landscape');
|
|
832
|
+
// pxNudge lives in the placement (parent/space) coordinate system, which maps to screen
|
|
833
|
+
// by the parent's world scale — so screen delta ÷ parent-world-scale = design units.
|
|
834
|
+
const dpp = (id) => {
|
|
835
|
+
if (opts.designPerPixel)
|
|
836
|
+
return opts.designPerPixel(id);
|
|
837
|
+
const parent = handle.node(id)?.parent;
|
|
838
|
+
const s = parent?.worldTransform?.a ?? 1;
|
|
839
|
+
return s !== 0 ? 1 / s : 1;
|
|
840
|
+
};
|
|
841
|
+
const layer = new Container();
|
|
842
|
+
layer.eventMode = 'static';
|
|
843
|
+
app.stage.addChild(layer);
|
|
844
|
+
const frame = new Graphics();
|
|
845
|
+
layer.addChild(frame);
|
|
846
|
+
const handleGfx = new Map();
|
|
847
|
+
for (const id of [...CORNERS, 'rotate']) {
|
|
848
|
+
const g = new Graphics();
|
|
849
|
+
g.eventMode = 'static';
|
|
850
|
+
g.cursor = id === 'rotate' ? 'grab' : cursorFor(id);
|
|
851
|
+
g.on('pointerdown', (e) => onDown(id, e));
|
|
852
|
+
handleGfx.set(id, g);
|
|
853
|
+
layer.addChild(g);
|
|
854
|
+
}
|
|
855
|
+
// A transparent body catcher for move-drags (below the handles).
|
|
856
|
+
const body = new Graphics();
|
|
857
|
+
body.eventMode = 'static';
|
|
858
|
+
body.cursor = 'move';
|
|
859
|
+
body.on('pointerdown', (e) => onDown('body', e));
|
|
860
|
+
layer.addChildAt(body, 1);
|
|
861
|
+
let nodeId = null;
|
|
862
|
+
let drag = null;
|
|
863
|
+
const findDocNode = (id, node = handle.doc().root) => {
|
|
864
|
+
if (node.id === id)
|
|
865
|
+
return node;
|
|
866
|
+
for (const child of node.children ?? []) {
|
|
867
|
+
const hit = findDocNode(id, child);
|
|
868
|
+
if (hit)
|
|
869
|
+
return hit;
|
|
870
|
+
}
|
|
871
|
+
return undefined;
|
|
872
|
+
};
|
|
873
|
+
/** Effective layout rule for the current orientation (base or portrait/landscape override). */
|
|
874
|
+
const effectiveRule = (id) => {
|
|
875
|
+
const node = findDocNode(id);
|
|
876
|
+
if (!node)
|
|
877
|
+
return {};
|
|
878
|
+
const o = getOrientation();
|
|
879
|
+
const override = node.responsive?.[o]?.layout;
|
|
880
|
+
return override ? { rule: override, orientation: o } : { rule: node.layout };
|
|
881
|
+
};
|
|
882
|
+
const boxOf = (id) => {
|
|
883
|
+
const view = handle.node(id);
|
|
884
|
+
if (!view || !view.visible)
|
|
885
|
+
return null;
|
|
886
|
+
const b = view.getBounds();
|
|
887
|
+
return { x: b.x, y: b.y, width: b.width, height: b.height };
|
|
888
|
+
};
|
|
889
|
+
const onDown = (h, e) => {
|
|
890
|
+
e.stopPropagation();
|
|
891
|
+
if (!nodeId)
|
|
892
|
+
return;
|
|
893
|
+
const box = boxOf(nodeId);
|
|
894
|
+
const { rule, orientation } = effectiveRule(nodeId);
|
|
895
|
+
if (!box || !rule)
|
|
896
|
+
return;
|
|
897
|
+
const center = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
898
|
+
drag = {
|
|
899
|
+
handle: h,
|
|
900
|
+
startBox: box,
|
|
901
|
+
baseRule: JSON.parse(JSON.stringify(rule)),
|
|
902
|
+
orientation,
|
|
903
|
+
start: { x: e.global.x, y: e.global.y },
|
|
904
|
+
center,
|
|
905
|
+
startAngle: Math.atan2(e.global.y - center.y, e.global.x - center.x),
|
|
906
|
+
};
|
|
907
|
+
};
|
|
908
|
+
const onMove = (e) => {
|
|
909
|
+
if (!drag || !nodeId)
|
|
910
|
+
return;
|
|
911
|
+
let rule;
|
|
912
|
+
if (drag.handle === 'body') {
|
|
913
|
+
const s = dpp(nodeId);
|
|
914
|
+
rule = nudgeRule(drag.baseRule, (e.global.x - drag.start.x) * s, (e.global.y - drag.start.y) * s);
|
|
915
|
+
}
|
|
916
|
+
else if (drag.handle === 'rotate') {
|
|
917
|
+
const angle = Math.atan2(e.global.y - drag.center.y, e.global.x - drag.center.x);
|
|
918
|
+
rule = setRuleRotation(drag.baseRule, (drag.baseRule.rotation ?? 0) + (angle - drag.startAngle));
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
if (sizeLever(drag.baseRule) === 'none')
|
|
922
|
+
return; // point-placed frame-fraction: nothing to size
|
|
923
|
+
const { factorX, factorY } = resizeFactors(drag.startBox, drag.handle, { x: e.global.x, y: e.global.y });
|
|
924
|
+
rule = resizeRule(drag.baseRule, factorX, factorY);
|
|
925
|
+
}
|
|
926
|
+
applyPatch({ op: 'set-layout', id: nodeId, layout: rule, orientation: drag.orientation });
|
|
927
|
+
};
|
|
928
|
+
const onUp = () => {
|
|
929
|
+
drag = null;
|
|
930
|
+
};
|
|
931
|
+
app.stage.on('pointermove', onMove);
|
|
932
|
+
app.stage.on('pointerup', onUp);
|
|
933
|
+
app.stage.on('pointerupoutside', onUp);
|
|
934
|
+
const redraw = () => {
|
|
935
|
+
const box = nodeId ? boxOf(nodeId) : null;
|
|
936
|
+
frame.clear();
|
|
937
|
+
body.clear();
|
|
938
|
+
if (!box) {
|
|
939
|
+
for (const g of handleGfx.values())
|
|
940
|
+
g.clear();
|
|
941
|
+
body.hitArea = null;
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
frame.rect(box.x, box.y, box.width, box.height).stroke({ color: 0x4dd0ff, width: 1.5, alpha: 0.9 });
|
|
945
|
+
// rotate stem
|
|
946
|
+
const rot = handlePoint(box, 'rotate');
|
|
947
|
+
frame.moveTo(box.x + box.width / 2, box.y).lineTo(rot.x, rot.y).stroke({ color: 0x4dd0ff, width: 1, alpha: 0.6 });
|
|
948
|
+
body.rect(box.x, box.y, box.width, box.height).fill({ color: 0xffffff, alpha: 0.001 });
|
|
949
|
+
body.hitArea = new Rectangle(box.x, box.y, box.width, box.height);
|
|
950
|
+
const sizable = sizeLever(nodeId ? effectiveRule(nodeId).rule ?? drag?.baseRule ?? {} : {}) !== 'none';
|
|
951
|
+
for (const id of [...CORNERS, 'rotate']) {
|
|
952
|
+
const g = handleGfx.get(id);
|
|
953
|
+
g.clear();
|
|
954
|
+
if (id !== 'rotate' && !sizable)
|
|
955
|
+
continue; // hide resize handles when the rule can't size
|
|
956
|
+
const p = handlePoint(box, id);
|
|
957
|
+
if (id === 'rotate')
|
|
958
|
+
g.circle(p.x, p.y, HS / 1.6).fill({ color: 0x4dd0ff }).stroke({ color: 0x06283d, width: 2 });
|
|
959
|
+
else
|
|
960
|
+
g.rect(p.x - HS / 2, p.y - HS / 2, HS, HS).fill({ color: 0x4dd0ff }).stroke({ color: 0x06283d, width: 2 });
|
|
961
|
+
g.hitArea = new Rectangle(p.x - HS, p.y - HS, HS * 2, HS * 2);
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
app.ticker.add(redraw);
|
|
965
|
+
const fakeEvent = (x, y) => ({ global: { x, y }, stopPropagation() { } });
|
|
966
|
+
return {
|
|
967
|
+
attach(id) {
|
|
968
|
+
nodeId = id;
|
|
969
|
+
redraw();
|
|
970
|
+
},
|
|
971
|
+
refresh: redraw,
|
|
972
|
+
drag(h, from, to) {
|
|
973
|
+
onDown(h, fakeEvent(from.x, from.y));
|
|
974
|
+
onMove(fakeEvent(to.x, to.y));
|
|
975
|
+
onUp();
|
|
976
|
+
},
|
|
977
|
+
destroy() {
|
|
978
|
+
app.ticker.remove(redraw);
|
|
979
|
+
app.stage.off('pointermove', onMove);
|
|
980
|
+
app.stage.off('pointerup', onUp);
|
|
981
|
+
app.stage.off('pointerupoutside', onUp);
|
|
982
|
+
layer.destroy({ children: true });
|
|
983
|
+
},
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
function cursorFor(id) {
|
|
987
|
+
switch (id) {
|
|
988
|
+
case 'nw':
|
|
989
|
+
case 'se': return 'nwse-resize';
|
|
990
|
+
case 'ne':
|
|
991
|
+
case 'sw': return 'nesw-resize';
|
|
992
|
+
case 'n':
|
|
993
|
+
case 's': return 'ns-resize';
|
|
994
|
+
case 'e':
|
|
995
|
+
case 'w': return 'ew-resize';
|
|
996
|
+
default: return 'default';
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// IDE bridge — the game-side half of the standalone editor. The game runs in an iframe;
|
|
1001
|
+
// the IDE app is the parent window. They speak a tiny postMessage RPC (`e8:ide:*`):
|
|
1002
|
+
// parent → child { type:'e8:ide:req', id, method, params }
|
|
1003
|
+
// child → parent { type:'e8:ide:res', id, ok, result | error }
|
|
1004
|
+
// child → parent { type:'e8:ide:evt', event, payload } (unsolicited: selection, ready)
|
|
1005
|
+
//
|
|
1006
|
+
// The whole editor is doc-driven: the IDE never reaches into Pixi — it reads a snapshot
|
|
1007
|
+
// (doc/tree/palette) and sends ScenePatches, exactly what the agent does. Canvas
|
|
1008
|
+
// click-to-select is owned here so no game writes selection code. Save goes through the
|
|
1009
|
+
// same-origin idePlugin `/__ide/write`.
|
|
1010
|
+
const REQ = 'e8:ide:req';
|
|
1011
|
+
const RES = 'e8:ide:res';
|
|
1012
|
+
const EVT = 'e8:ide:evt';
|
|
1013
|
+
function mountIdeBridge(opts) {
|
|
1014
|
+
const { handle, app } = opts;
|
|
1015
|
+
const origin = opts.targetOrigin ?? '*';
|
|
1016
|
+
let selectedId = null;
|
|
1017
|
+
const post = (msg) => window.parent?.postMessage(msg, origin);
|
|
1018
|
+
const emit = (event, payload) => post({ type: EVT, event, payload });
|
|
1019
|
+
const pushSnapshot = () => emit('snapshot', snapshot());
|
|
1020
|
+
const snapshot = () => ({
|
|
1021
|
+
docId: handle.doc().id,
|
|
1022
|
+
doc: handle.doc(),
|
|
1023
|
+
tree: handle.tree(),
|
|
1024
|
+
palette: handle.palette(),
|
|
1025
|
+
sceneFile: opts.sceneFile,
|
|
1026
|
+
selectedId,
|
|
1027
|
+
});
|
|
1028
|
+
// Transform gizmo (resize/rotate/move on canvas) — owns the selection visuals too, so
|
|
1029
|
+
// no game writes selection code. Its patches round-trip a fresh snapshot to the IDE.
|
|
1030
|
+
const gizmo = createTransformGizmo({
|
|
1031
|
+
handle,
|
|
1032
|
+
app,
|
|
1033
|
+
getOrientation: opts.getOrientation,
|
|
1034
|
+
designPerPixel: opts.designPerPixel,
|
|
1035
|
+
applyPatch: (patch) => {
|
|
1036
|
+
handle.patch(patch);
|
|
1037
|
+
pushSnapshot();
|
|
1038
|
+
},
|
|
1039
|
+
});
|
|
1040
|
+
const select = (id) => {
|
|
1041
|
+
selectedId = id;
|
|
1042
|
+
gizmo.attach(id);
|
|
1043
|
+
emit('selected', { id });
|
|
1044
|
+
};
|
|
1045
|
+
// Click any node on the canvas → select it (bubbles the id up to the IDE).
|
|
1046
|
+
const wireHitAreas = () => {
|
|
1047
|
+
const walk = (node) => {
|
|
1048
|
+
if (node.id !== handle.doc().root.id) {
|
|
1049
|
+
const view = handle.node(node.id);
|
|
1050
|
+
if (view) {
|
|
1051
|
+
view.eventMode = 'static';
|
|
1052
|
+
view.cursor = 'pointer';
|
|
1053
|
+
const lb = view.getLocalBounds();
|
|
1054
|
+
view.hitArea = new Rectangle(lb.x, lb.y, lb.width, lb.height);
|
|
1055
|
+
view.removeAllListeners('pointerdown');
|
|
1056
|
+
view.on('pointerdown', (e) => {
|
|
1057
|
+
e.stopPropagation();
|
|
1058
|
+
select(node.id);
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
node.children?.forEach(walk);
|
|
1063
|
+
};
|
|
1064
|
+
walk(handle.doc().root);
|
|
1065
|
+
};
|
|
1066
|
+
app.stage.eventMode = 'static';
|
|
1067
|
+
app.stage.hitArea = app.screen;
|
|
1068
|
+
app.stage.on('pointerdown', () => select(null));
|
|
1069
|
+
wireHitAreas();
|
|
1070
|
+
const afterStructural = () => {
|
|
1071
|
+
opts.relayout?.();
|
|
1072
|
+
wireHitAreas();
|
|
1073
|
+
};
|
|
1074
|
+
// ── RPC ───────────────────────────────────────────────────────────────────────
|
|
1075
|
+
const methods = {
|
|
1076
|
+
snapshot: () => snapshot(),
|
|
1077
|
+
patch: (params) => {
|
|
1078
|
+
const patch = params;
|
|
1079
|
+
handle.patch(patch);
|
|
1080
|
+
if (patch.op === 'add-node' || patch.op === 'remove-node' || patch.op === 'move-node' || patch.op === 'duplicate-node') {
|
|
1081
|
+
afterStructural();
|
|
1082
|
+
}
|
|
1083
|
+
return snapshot();
|
|
1084
|
+
},
|
|
1085
|
+
select: (params) => {
|
|
1086
|
+
select(params.id);
|
|
1087
|
+
return { selectedId };
|
|
1088
|
+
},
|
|
1089
|
+
save: async (params) => {
|
|
1090
|
+
const file = params?.file ?? opts.sceneFile;
|
|
1091
|
+
if (!file)
|
|
1092
|
+
throw new Error('no sceneFile configured');
|
|
1093
|
+
const res = await fetch('/__ide/write', {
|
|
1094
|
+
method: 'POST',
|
|
1095
|
+
headers: { 'content-type': 'application/json' },
|
|
1096
|
+
body: JSON.stringify({ file, content: handle.doc() }),
|
|
1097
|
+
});
|
|
1098
|
+
if (!res.ok)
|
|
1099
|
+
throw new Error(await res.text());
|
|
1100
|
+
return { saved: file };
|
|
1101
|
+
},
|
|
1102
|
+
};
|
|
1103
|
+
const onMessage = async (e) => {
|
|
1104
|
+
const data = e.data;
|
|
1105
|
+
if (data?.type !== REQ || typeof data.id !== 'number' || !data.method)
|
|
1106
|
+
return;
|
|
1107
|
+
const fn = methods[data.method];
|
|
1108
|
+
if (!fn)
|
|
1109
|
+
return post({ type: RES, id: data.id, ok: false, error: `unknown method "${data.method}"` });
|
|
1110
|
+
try {
|
|
1111
|
+
const result = await fn(data.params);
|
|
1112
|
+
post({ type: RES, id: data.id, ok: true, result });
|
|
1113
|
+
}
|
|
1114
|
+
catch (err) {
|
|
1115
|
+
post({ type: RES, id: data.id, ok: false, error: String(err?.message ?? err) });
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
window.addEventListener('message', onMessage);
|
|
1119
|
+
// Announce readiness (the IDE may mount before or after the game boots).
|
|
1120
|
+
emit('ready', { docId: handle.doc().id });
|
|
1121
|
+
return {
|
|
1122
|
+
refresh: pushSnapshot,
|
|
1123
|
+
select,
|
|
1124
|
+
gizmo,
|
|
1125
|
+
destroy() {
|
|
1126
|
+
window.removeEventListener('message', onMessage);
|
|
1127
|
+
gizmo.destroy();
|
|
1128
|
+
},
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
export { createFlowEditor, createFlowTimeline, createSceneInspector, layoutFieldSpecs, mountIdeBridge, propControlKind, setRulePath };
|
|
372
1133
|
//# sourceMappingURL=scene-devtools.esm.js.map
|