@energy8platform/game-engine 0.34.2 → 0.35.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/audio.cjs.js +114 -59
- package/dist/audio.cjs.js.map +1 -1
- package/dist/audio.d.ts +25 -0
- package/dist/audio.esm.js +114 -59
- package/dist/audio.esm.js.map +1 -1
- package/dist/core.cjs.js +222 -66
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.d.ts +25 -0
- package/dist/core.esm.js +223 -67
- package/dist/core.esm.js.map +1 -1
- package/dist/flow.cjs.js +246 -0
- package/dist/flow.cjs.js.map +1 -1
- package/dist/flow.d.ts +192 -33
- package/dist/flow.esm.js +238 -1
- package/dist/flow.esm.js.map +1 -1
- package/dist/host.cjs.js +343 -82
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.d.ts +82 -2
- package/dist/host.esm.js +344 -83
- package/dist/host.esm.js.map +1 -1
- package/dist/index.cjs.js +222 -66
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +72 -0
- package/dist/index.esm.js +223 -67
- package/dist/index.esm.js.map +1 -1
- package/dist/scene-devtools.cjs.js +529 -115
- package/dist/scene-devtools.cjs.js.map +1 -1
- package/dist/scene-devtools.d.ts +187 -34
- package/dist/scene-devtools.esm.js +529 -115
- package/dist/scene-devtools.esm.js.map +1 -1
- package/dist/scene.cjs.js +704 -46
- package/dist/scene.cjs.js.map +1 -1
- package/dist/scene.d.ts +228 -41
- package/dist/scene.esm.js +698 -47
- package/dist/scene.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/audio/AudioManager.ts +111 -53
- package/src/core/GameApplication.ts +47 -5
- package/src/host/buildConfig.ts +17 -4
- package/src/host/createSlotGame.ts +114 -12
- package/src/host/index.ts +3 -0
- package/src/host/types.ts +58 -0
- package/src/loading/LoadingScene.ts +76 -2
- package/src/loading/index.ts +6 -0
- package/src/types.ts +2 -0
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
import { Container, Graphics, Rectangle, Point } from 'pixi.js';
|
|
2
2
|
|
|
3
|
+
// Field schemas — the authoring contract for a node type's props.
|
|
4
|
+
//
|
|
5
|
+
// Before this existed the inspector inferred a control from the CURRENT value, which meant
|
|
6
|
+
// a prop that wasn't set yet had no row and no way to be added: a freshly added sprite had
|
|
7
|
+
// `props:{}` and therefore no way to choose a texture at all. It also had no notion of an
|
|
8
|
+
// enum, an asset or a node reference, and it round-tripped booleans through text.
|
|
9
|
+
//
|
|
10
|
+
// A schema is declared once by the contribution that owns the kind, and is used three ways:
|
|
11
|
+
// the inspector renders from it, validation can check against it, and the agent reads the
|
|
12
|
+
// same text as documentation (`doc`) — the plugin's docs and the agent's prompt stay one
|
|
13
|
+
// thing, per docs/slot-ide.md §6.2.
|
|
14
|
+
/** Shared field definitions, so every kind describes `alpha` the same way. */
|
|
15
|
+
function schemaFieldRows(schema, props) {
|
|
16
|
+
const rows = [];
|
|
17
|
+
if (!schema)
|
|
18
|
+
return rows;
|
|
19
|
+
const visit = (fields, prefix, container) => {
|
|
20
|
+
const entries = Object.entries(fields).sort((a, b) => (a[1].order ?? 1000) - (b[1].order ?? 1000) || a[0].localeCompare(b[0]));
|
|
21
|
+
for (const [key, field] of entries) {
|
|
22
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
23
|
+
const parent = container;
|
|
24
|
+
const value = parent && typeof parent === 'object' ? parent[key] : undefined;
|
|
25
|
+
if (field.kind === 'object' && field.fields) {
|
|
26
|
+
visit(field.fields, path, value);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
rows.push({ path, label: field.label ?? path, schema: field, value, unset: value === undefined });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
visit(schema, '', props);
|
|
34
|
+
return rows;
|
|
35
|
+
}
|
|
36
|
+
/** Props present on the node that the schema doesn't describe (plugin extras, hand-written). */
|
|
37
|
+
function extraPropKeys(schema, props) {
|
|
38
|
+
const known = new Set(Object.keys(schema ?? {}));
|
|
39
|
+
return Object.keys(props).filter((k) => !known.has(k));
|
|
40
|
+
}
|
|
41
|
+
|
|
3
42
|
// Pure helpers behind the scene inspector — kept renderer/DOM-free so the control
|
|
4
43
|
// inference and layout-field enumeration are unit-testable and reusable (the same
|
|
5
44
|
// mapping later feeds schema-driven autogen and the agent's docs).
|
|
@@ -35,7 +74,9 @@ function layoutFieldSpecs(rule) {
|
|
|
35
74
|
out.push(path === 'mode' ? { path, label, kind: 'readonly', value } : { path, label, kind: 'text', value });
|
|
36
75
|
}
|
|
37
76
|
else if (typeof value === 'boolean') {
|
|
38
|
-
|
|
77
|
+
// Must stay a real boolean: the engine tests rules with `=== false`
|
|
78
|
+
// (e.g. scaleWithGrid), so writing back the string "false" silently inverts behaviour.
|
|
79
|
+
out.push({ path, label, kind: 'boolean', value });
|
|
39
80
|
}
|
|
40
81
|
else if (Array.isArray(value)) {
|
|
41
82
|
value.forEach((item, i) => visit(item, `${path}.${i}`, `${label}[${i}]`));
|
|
@@ -105,6 +146,12 @@ const STYLE$2 = `
|
|
|
105
146
|
.e8si-json { display: block; width: calc(100% - 20px); margin: 2px 10px; min-height: 52px; font: 11px/1.4 ui-monospace, monospace; }
|
|
106
147
|
.e8si-apply { margin: 4px 10px; padding: 3px 10px; background: #18213d; color: #dfe6f5; border: 1px solid #2b3866; border-radius: 6px; cursor: pointer; }
|
|
107
148
|
.e8si-empty { padding: 10px; color: #7d8ab0; }
|
|
149
|
+
.e8si-unset label { opacity: 0.6; font-style: italic; }
|
|
150
|
+
.e8si-unset input, .e8si-unset select { opacity: 0.75; }
|
|
151
|
+
.e8si-clear { flex: none; background: none; border: 0; color: #55617f; cursor: pointer; font: inherit; padding: 0 2px; }
|
|
152
|
+
.e8si-clear:hover { color: #dfe6f5; }
|
|
153
|
+
.e8si-thumb { flex: none; width: 30px; height: 24px; object-fit: contain; border-radius: 4px;
|
|
154
|
+
background: repeating-conic-gradient(#161d33 0% 25%, #0d1428 0% 50%) 0 / 8px 8px; }
|
|
108
155
|
.e8si-badge { display: inline-block; padding: 0 6px; border: 1px solid #2b3866; border-radius: 8px; color: #9fb0d8; font-size: 10px; }
|
|
109
156
|
`;
|
|
110
157
|
function createSceneInspector(opts) {
|
|
@@ -271,12 +318,17 @@ function createSceneInspector(opts) {
|
|
|
271
318
|
}
|
|
272
319
|
else {
|
|
273
320
|
const input = document.createElement('input');
|
|
274
|
-
|
|
321
|
+
const isBool = spec.kind === 'boolean';
|
|
322
|
+
input.type = isBool ? 'checkbox' : spec.kind === 'number' ? 'number' : 'text';
|
|
275
323
|
if (spec.step !== undefined)
|
|
276
324
|
input.step = String(spec.step);
|
|
277
|
-
|
|
325
|
+
if (isBool)
|
|
326
|
+
input.checked = spec.value === true;
|
|
327
|
+
else
|
|
328
|
+
input.value = String(spec.value);
|
|
278
329
|
input.addEventListener('change', () => {
|
|
279
|
-
|
|
330
|
+
// Booleans must round-trip as booleans — the engine compares with `=== false`.
|
|
331
|
+
const raw = isBool ? input.checked : spec.kind === 'number' ? Number(input.value) : input.value;
|
|
280
332
|
if (spec.kind === 'number' && !Number.isFinite(raw))
|
|
281
333
|
return;
|
|
282
334
|
patchAndRefresh({
|
|
@@ -291,76 +343,225 @@ function createSceneInspector(opts) {
|
|
|
291
343
|
inspHost.appendChild(wrap);
|
|
292
344
|
}
|
|
293
345
|
}
|
|
294
|
-
// Props —
|
|
346
|
+
// Props — driven by the kind's schema, so EVERY authorable field has a row (including
|
|
347
|
+
// ones not set yet) with the right control: an asset picker for textures, a node picker
|
|
348
|
+
// for references, a select for enums. Props the schema doesn't know about still show up,
|
|
349
|
+
// inferred from their value, so hand-written and plugin extras stay editable.
|
|
295
350
|
const props = node.props ?? {};
|
|
296
|
-
const
|
|
297
|
-
|
|
351
|
+
const schemaKey = node.type === 'prefab' ? `prefab:${String(props.prefab ?? '')}` : node.type;
|
|
352
|
+
const schema = handle.schemas()[schemaKey];
|
|
353
|
+
const rows = schemaFieldRows(schema, props);
|
|
354
|
+
const extras = extraPropKeys(schema, props).filter((k) => !(node.type === 'prefab' && k === 'prefab'));
|
|
355
|
+
if (rows.length > 0 || extras.length > 0) {
|
|
298
356
|
const propsTitle = document.createElement('h3');
|
|
299
357
|
propsTitle.textContent = 'Props';
|
|
300
358
|
propsTitle.className = 'e8si-sec';
|
|
301
359
|
inspHost.appendChild(propsTitle);
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const area = document.createElement('textarea');
|
|
309
|
-
area.className = 'e8si-json';
|
|
310
|
-
area.value = JSON.stringify(value, null, 1);
|
|
311
|
-
const apply = document.createElement('button');
|
|
312
|
-
apply.className = 'e8si-apply';
|
|
313
|
-
apply.textContent = `apply ${key}`;
|
|
314
|
-
apply.addEventListener('click', () => {
|
|
315
|
-
try {
|
|
316
|
-
patchAndRefresh({ op: 'set-props', id, props: { [key]: JSON.parse(area.value) } });
|
|
317
|
-
}
|
|
318
|
-
catch {
|
|
319
|
-
apply.textContent = `apply ${key} — invalid JSON`;
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
inspHost.append(area, apply);
|
|
323
|
-
continue;
|
|
360
|
+
/** Write a (possibly nested) prop path. set-props merges at the top level only. */
|
|
361
|
+
const writePath = (path, value) => {
|
|
362
|
+
const parts = path.split('.');
|
|
363
|
+
if (parts.length === 1) {
|
|
364
|
+
patchAndRefresh({ op: 'set-props', id, props: { [path]: value } });
|
|
365
|
+
return;
|
|
324
366
|
}
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
wrap.appendChild(input);
|
|
367
|
+
const [head, ...rest] = parts;
|
|
368
|
+
const root = { ...(props[head] ?? {}) };
|
|
369
|
+
let cursor = root;
|
|
370
|
+
for (let i = 0; i < rest.length - 1; i++) {
|
|
371
|
+
cursor[rest[i]] = { ...(cursor[rest[i]] ?? {}) };
|
|
372
|
+
cursor = cursor[rest[i]];
|
|
332
373
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
374
|
+
cursor[rest[rest.length - 1]] = value;
|
|
375
|
+
patchAndRefresh({ op: 'set-props', id, props: { [head]: root } });
|
|
376
|
+
};
|
|
377
|
+
for (const row of rows) {
|
|
378
|
+
const wrap = field(row.label);
|
|
379
|
+
const label = wrap.querySelector('label');
|
|
380
|
+
if (label && row.schema.doc)
|
|
381
|
+
label.title = `${row.path} — ${row.schema.doc}`;
|
|
382
|
+
// An unset field shows its default, dimmed: "not set" must not read as "zero".
|
|
383
|
+
if (row.unset)
|
|
384
|
+
wrap.classList.add('e8si-unset');
|
|
385
|
+
const shown = row.unset ? row.schema.default : row.value;
|
|
386
|
+
const control = buildControl(row.schema, shown, (v) => writePath(row.path, v));
|
|
387
|
+
wrap.append(...control);
|
|
388
|
+
if (!row.unset && row.schema.default !== undefined) {
|
|
389
|
+
const clear = document.createElement('button');
|
|
390
|
+
clear.className = 'e8si-clear';
|
|
391
|
+
clear.textContent = '\u00d7';
|
|
392
|
+
clear.title = `Reset to the default (${JSON.stringify(row.schema.default)})`;
|
|
393
|
+
clear.addEventListener('click', () => writePath(row.path, undefined));
|
|
394
|
+
wrap.appendChild(clear);
|
|
347
395
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
396
|
+
inspHost.appendChild(wrap);
|
|
397
|
+
}
|
|
398
|
+
for (const key of extras)
|
|
399
|
+
renderInferred(key, props[key]);
|
|
400
|
+
}
|
|
401
|
+
/** Build the control(s) for one schema field. */
|
|
402
|
+
function buildControl(spec, value, commit) {
|
|
403
|
+
if (spec.kind === 'asset') {
|
|
404
|
+
const list = (opts.assets?.() ?? []).filter((a) => {
|
|
405
|
+
if (spec.accept === 'audio')
|
|
406
|
+
return a.kind === 'audio';
|
|
407
|
+
if (spec.accept === undefined)
|
|
408
|
+
return true;
|
|
409
|
+
return a.kind !== 'audio'; // image / spritesheet are both textures
|
|
410
|
+
});
|
|
411
|
+
const select = document.createElement('select');
|
|
412
|
+
const blank = document.createElement('option');
|
|
413
|
+
blank.value = '';
|
|
414
|
+
blank.textContent = list.length ? '\u2014 pick asset \u2014' : '(no asset manifest)';
|
|
415
|
+
select.appendChild(blank);
|
|
416
|
+
for (const a of list) {
|
|
417
|
+
const o = document.createElement('option');
|
|
418
|
+
o.value = a.alias;
|
|
419
|
+
o.textContent = a.alias;
|
|
420
|
+
select.appendChild(o);
|
|
421
|
+
}
|
|
422
|
+
const current = value === undefined ? '' : String(value);
|
|
423
|
+
if (current && !list.some((a) => a.alias === current)) {
|
|
424
|
+
const o = document.createElement('option');
|
|
425
|
+
o.value = current;
|
|
426
|
+
o.textContent = `${current} (missing)`;
|
|
427
|
+
select.appendChild(o);
|
|
428
|
+
}
|
|
429
|
+
select.value = current;
|
|
430
|
+
select.addEventListener('change', () => commit(select.value === '' ? undefined : select.value));
|
|
431
|
+
const out = [select];
|
|
432
|
+
const hit = list.find((a) => a.alias === current);
|
|
433
|
+
if (hit && opts.assetUrl) {
|
|
434
|
+
const img = document.createElement('img');
|
|
435
|
+
img.className = 'e8si-thumb';
|
|
436
|
+
img.src = opts.assetUrl(hit.path);
|
|
437
|
+
img.alt = current;
|
|
438
|
+
out.push(img);
|
|
439
|
+
}
|
|
440
|
+
return out;
|
|
441
|
+
}
|
|
442
|
+
if (spec.kind === 'nodeRef') {
|
|
443
|
+
const select = document.createElement('select');
|
|
444
|
+
const blank = document.createElement('option');
|
|
445
|
+
blank.value = '';
|
|
446
|
+
blank.textContent = '\u2014 pick node \u2014';
|
|
447
|
+
select.appendChild(blank);
|
|
448
|
+
const walk = (n) => {
|
|
449
|
+
if (n.id !== id && (!spec.ofType || n.type === spec.ofType)) {
|
|
450
|
+
const o = document.createElement('option');
|
|
451
|
+
o.value = n.id;
|
|
452
|
+
o.textContent = `${n.id} (${n.type})`;
|
|
453
|
+
select.appendChild(o);
|
|
454
|
+
}
|
|
455
|
+
n.children.forEach(walk);
|
|
456
|
+
};
|
|
457
|
+
walk(handle.tree());
|
|
458
|
+
select.value = value === undefined ? '' : String(value);
|
|
459
|
+
select.addEventListener('change', () => commit(select.value === '' ? undefined : select.value));
|
|
460
|
+
return [select];
|
|
461
|
+
}
|
|
462
|
+
if (spec.kind === 'enum') {
|
|
463
|
+
const select = document.createElement('select');
|
|
464
|
+
for (const opt of spec.options ?? []) {
|
|
465
|
+
const o = document.createElement('option');
|
|
466
|
+
o.value = opt;
|
|
467
|
+
o.textContent = opt;
|
|
468
|
+
select.appendChild(o);
|
|
359
469
|
}
|
|
470
|
+
select.value = value === undefined ? '' : String(value);
|
|
471
|
+
select.addEventListener('change', () => commit(select.value));
|
|
472
|
+
return [select];
|
|
473
|
+
}
|
|
474
|
+
if (spec.kind === 'boolean') {
|
|
475
|
+
const input = document.createElement('input');
|
|
476
|
+
input.type = 'checkbox';
|
|
477
|
+
input.checked = value === true;
|
|
478
|
+
input.addEventListener('change', () => commit(input.checked));
|
|
479
|
+
return [input];
|
|
480
|
+
}
|
|
481
|
+
if (spec.kind === 'color') {
|
|
482
|
+
const color = document.createElement('input');
|
|
483
|
+
color.type = 'color';
|
|
484
|
+
color.value = normalizeHex(value);
|
|
485
|
+
const text = document.createElement('input');
|
|
486
|
+
text.type = 'text';
|
|
487
|
+
text.value = value === undefined ? '' : String(value);
|
|
488
|
+
color.addEventListener('input', () => {
|
|
489
|
+
text.value = color.value;
|
|
490
|
+
commit(color.value);
|
|
491
|
+
});
|
|
492
|
+
text.addEventListener('change', () => commit(text.value));
|
|
493
|
+
return [color, text];
|
|
494
|
+
}
|
|
495
|
+
if (spec.kind === 'json') {
|
|
496
|
+
const area = document.createElement('textarea');
|
|
497
|
+
area.className = 'e8si-json';
|
|
498
|
+
area.value = value === undefined ? '' : JSON.stringify(value, null, 1);
|
|
499
|
+
area.addEventListener('change', () => {
|
|
500
|
+
const raw = area.value.trim();
|
|
501
|
+
if (raw === '')
|
|
502
|
+
return commit(undefined);
|
|
503
|
+
try {
|
|
504
|
+
commit(JSON.parse(raw));
|
|
505
|
+
area.style.borderColor = '';
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
area.style.borderColor = '#ff4d8d';
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
return [area];
|
|
512
|
+
}
|
|
513
|
+
const input = document.createElement('input');
|
|
514
|
+
input.type = spec.kind === 'number' ? 'number' : 'text';
|
|
515
|
+
if (spec.step !== undefined)
|
|
516
|
+
input.step = String(spec.step);
|
|
517
|
+
if (spec.min !== undefined)
|
|
518
|
+
input.min = String(spec.min);
|
|
519
|
+
if (spec.max !== undefined)
|
|
520
|
+
input.max = String(spec.max);
|
|
521
|
+
input.value = value === undefined ? '' : String(value);
|
|
522
|
+
input.addEventListener('change', () => {
|
|
523
|
+
if (input.value === '')
|
|
524
|
+
return commit(undefined);
|
|
525
|
+
const raw = spec.kind === 'number' ? Number(input.value) : input.value;
|
|
526
|
+
if (spec.kind === 'number' && !Number.isFinite(raw))
|
|
527
|
+
return;
|
|
528
|
+
commit(raw);
|
|
529
|
+
});
|
|
530
|
+
return [input];
|
|
531
|
+
}
|
|
532
|
+
/** Fallback for props the schema doesn't describe (plugin extras, hand-written keys). */
|
|
533
|
+
function renderInferred(key, value) {
|
|
534
|
+
const kind = propControlKind(value);
|
|
535
|
+
if (kind === 'json') {
|
|
536
|
+
const wrap = field(key);
|
|
360
537
|
inspHost.appendChild(wrap);
|
|
538
|
+
const area = document.createElement('textarea');
|
|
539
|
+
area.className = 'e8si-json';
|
|
540
|
+
area.value = JSON.stringify(value, null, 1);
|
|
541
|
+
const apply = document.createElement('button');
|
|
542
|
+
apply.className = 'e8si-apply';
|
|
543
|
+
apply.textContent = `apply ${key}`;
|
|
544
|
+
apply.addEventListener('click', () => {
|
|
545
|
+
try {
|
|
546
|
+
patchAndRefresh({ op: 'set-props', id, props: { [key]: JSON.parse(area.value) } });
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
apply.textContent = `apply ${key} — invalid JSON`;
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
inspHost.append(area, apply);
|
|
553
|
+
return;
|
|
361
554
|
}
|
|
555
|
+
const wrap = field(key);
|
|
556
|
+
const control = buildControl({ kind: kind === 'color' ? 'color' : kind }, value, (v) => patchAndRefresh({ op: 'set-props', id, props: { [key]: v } }));
|
|
557
|
+
wrap.append(...control);
|
|
558
|
+
inspHost.appendChild(wrap);
|
|
362
559
|
}
|
|
363
560
|
};
|
|
561
|
+
function normalizeHex(value) {
|
|
562
|
+
const s = typeof value === 'string' ? value : '';
|
|
563
|
+
return /^#[0-9a-f]{6}/i.test(s) ? s.slice(0, 7) : '#000000';
|
|
564
|
+
}
|
|
364
565
|
const select = (id) => {
|
|
365
566
|
selectedId = id;
|
|
366
567
|
renderTree();
|
|
@@ -860,7 +1061,10 @@ function createTransformGizmo(opts) {
|
|
|
860
1061
|
layer.addChildAt(body, 1);
|
|
861
1062
|
let nodeId = null;
|
|
862
1063
|
let drag = null;
|
|
863
|
-
|
|
1064
|
+
/** Press origin, to tell a click apart from a drag on pointerup. */
|
|
1065
|
+
let downAt = null;
|
|
1066
|
+
let moved = false;
|
|
1067
|
+
const DRAG_SLOP = 4; // px before a press counts as a drag
|
|
864
1068
|
const findDocNode = (id, node = handle.doc().root) => {
|
|
865
1069
|
if (node.id === id)
|
|
866
1070
|
return node;
|
|
@@ -872,18 +1076,13 @@ function createTransformGizmo(opts) {
|
|
|
872
1076
|
return undefined;
|
|
873
1077
|
};
|
|
874
1078
|
/**
|
|
875
|
-
* Effective layout rule + which override an edit should target
|
|
876
|
-
*
|
|
877
|
-
* otherwise the current orientation override, else base.
|
|
1079
|
+
* Effective layout rule + which override an edit should target: the current orientation's
|
|
1080
|
+
* override when one exists, else the base rule.
|
|
878
1081
|
*/
|
|
879
1082
|
const effectiveRule = (id) => {
|
|
880
1083
|
const node = findDocNode(id);
|
|
881
1084
|
if (!node)
|
|
882
1085
|
return { target: {} };
|
|
883
|
-
const mode = getMode();
|
|
884
|
-
if (mode && mode !== 'base') {
|
|
885
|
-
return { rule: node.modes?.[mode]?.layout ?? node.layout, target: { mode } };
|
|
886
|
-
}
|
|
887
1086
|
const o = getOrientation();
|
|
888
1087
|
const override = node.responsive?.[o]?.layout;
|
|
889
1088
|
return override ? { rule: override, target: { orientation: o } } : { rule: node.layout, target: {} };
|
|
@@ -904,6 +1103,9 @@ function createTransformGizmo(opts) {
|
|
|
904
1103
|
if (!box || !rule)
|
|
905
1104
|
return;
|
|
906
1105
|
const center = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
1106
|
+
downAt = { x: e.global.x, y: e.global.y, handle: h, alt: e.altKey };
|
|
1107
|
+
moved = false;
|
|
1108
|
+
opts.onGestureStart?.();
|
|
907
1109
|
drag = {
|
|
908
1110
|
handle: h,
|
|
909
1111
|
startBox: box,
|
|
@@ -917,6 +1119,11 @@ function createTransformGizmo(opts) {
|
|
|
917
1119
|
const onMove = (e) => {
|
|
918
1120
|
if (!drag || !nodeId)
|
|
919
1121
|
return;
|
|
1122
|
+
if (!moved && downAt && (Math.abs(e.global.x - downAt.x) > DRAG_SLOP || Math.abs(e.global.y - downAt.y) > DRAG_SLOP)) {
|
|
1123
|
+
moved = true;
|
|
1124
|
+
}
|
|
1125
|
+
if (!moved)
|
|
1126
|
+
return; // a press that has not travelled yet is still a potential click
|
|
920
1127
|
let rule;
|
|
921
1128
|
if (drag.handle === 'body') {
|
|
922
1129
|
const s = dpp(nodeId);
|
|
@@ -932,10 +1139,20 @@ function createTransformGizmo(opts) {
|
|
|
932
1139
|
const { factorX, factorY } = resizeFactors(drag.startBox, drag.handle, { x: e.global.x, y: e.global.y });
|
|
933
1140
|
rule = resizeRule(drag.baseRule, factorX, factorY);
|
|
934
1141
|
}
|
|
935
|
-
applyPatch({ op: 'set-layout', id: nodeId, layout: rule, orientation: drag.target.orientation
|
|
1142
|
+
applyPatch({ op: 'set-layout', id: nodeId, layout: rule, orientation: drag.target.orientation });
|
|
936
1143
|
};
|
|
937
1144
|
const onUp = () => {
|
|
1145
|
+
if (drag)
|
|
1146
|
+
opts.onGestureEnd?.();
|
|
938
1147
|
drag = null;
|
|
1148
|
+
// Body press that never moved = a selection click, not a move.
|
|
1149
|
+
if (downAt && !moved && downAt.handle === 'body') {
|
|
1150
|
+
const { x, y, alt } = downAt;
|
|
1151
|
+
downAt = null;
|
|
1152
|
+
opts.onBodyClick?.(x, y, alt);
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
downAt = null;
|
|
939
1156
|
};
|
|
940
1157
|
app.stage.on('pointermove', onMove);
|
|
941
1158
|
app.stage.on('pointerup', onUp);
|
|
@@ -978,6 +1195,24 @@ function createTransformGizmo(opts) {
|
|
|
978
1195
|
redraw();
|
|
979
1196
|
},
|
|
980
1197
|
refresh: redraw,
|
|
1198
|
+
nudge(dxScreen, dyScreen) {
|
|
1199
|
+
if (!nodeId)
|
|
1200
|
+
return;
|
|
1201
|
+
const { rule, target } = effectiveRule(nodeId);
|
|
1202
|
+
if (!rule)
|
|
1203
|
+
return;
|
|
1204
|
+
// Bracketed as a gesture so it lands on the undo stack as exactly one step.
|
|
1205
|
+
opts.onGestureStart?.();
|
|
1206
|
+
const s = dpp(nodeId);
|
|
1207
|
+
applyPatch({
|
|
1208
|
+
op: 'set-layout',
|
|
1209
|
+
id: nodeId,
|
|
1210
|
+
layout: nudgeRule(rule, dxScreen * s, dyScreen * s),
|
|
1211
|
+
orientation: target.orientation,
|
|
1212
|
+
});
|
|
1213
|
+
opts.onGestureEnd?.();
|
|
1214
|
+
redraw();
|
|
1215
|
+
},
|
|
981
1216
|
drag(h, from, to) {
|
|
982
1217
|
onDown(h, fakeEvent(from.x, from.y));
|
|
983
1218
|
onMove(fakeEvent(to.x, to.y));
|
|
@@ -1026,88 +1261,222 @@ function mountIdeBridge(opts) {
|
|
|
1026
1261
|
const post = (msg) => window.parent?.postMessage(msg, origin);
|
|
1027
1262
|
const emit = (event, payload) => post({ type: EVT, event, payload });
|
|
1028
1263
|
const pushSnapshot = () => emit('snapshot', snapshot());
|
|
1029
|
-
// Current authoring mode — the view the canvas shows AND the override edits target.
|
|
1030
|
-
let editMode = null;
|
|
1031
1264
|
const snapshot = () => ({
|
|
1032
1265
|
docId: handle.doc().id,
|
|
1033
1266
|
doc: handle.doc(),
|
|
1034
1267
|
tree: handle.tree(),
|
|
1035
1268
|
palette: handle.palette(),
|
|
1269
|
+
// Per-kind props schemas: the editor lives in another origin and cannot reach the
|
|
1270
|
+
// registry, so the authoring contract travels with the snapshot.
|
|
1271
|
+
schemas: handle.schemas(),
|
|
1036
1272
|
sceneFile: opts.sceneFile,
|
|
1037
|
-
modes: handle.doc().modes ?? [],
|
|
1038
|
-
mode: editMode,
|
|
1039
1273
|
selectedId,
|
|
1040
1274
|
});
|
|
1275
|
+
// Undo bookkeeping. Every applied patch reports the patch that undoes it; the editor
|
|
1276
|
+
// keeps the stack. A gizmo drag emits a patch per pointer frame, so while a gesture is in
|
|
1277
|
+
// flight we keep only the FIRST inverse — undoing a drag must rewind the whole gesture,
|
|
1278
|
+
// not one mouse-move.
|
|
1279
|
+
let gesture = null;
|
|
1280
|
+
const recordEdit = (result) => {
|
|
1281
|
+
if (result.ok !== true || !result.inverse)
|
|
1282
|
+
return;
|
|
1283
|
+
if (gesture) {
|
|
1284
|
+
gesture.inverse ??= result.inverse;
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
emit('edit', { inverse: result.inverse });
|
|
1288
|
+
};
|
|
1041
1289
|
// Transform gizmo (resize/rotate/move on canvas) — owns the selection visuals too, so
|
|
1042
1290
|
// no game writes selection code. Its patches round-trip a fresh snapshot to the IDE.
|
|
1043
1291
|
const gizmo = createTransformGizmo({
|
|
1044
1292
|
handle,
|
|
1045
1293
|
app,
|
|
1046
1294
|
getOrientation: opts.getOrientation,
|
|
1047
|
-
getMode: () => editMode,
|
|
1048
1295
|
designPerPixel: opts.designPerPixel,
|
|
1049
1296
|
applyPatch: (patch) => {
|
|
1050
|
-
handle.patch(patch);
|
|
1297
|
+
recordEdit(handle.patch(patch));
|
|
1051
1298
|
pushSnapshot();
|
|
1052
1299
|
},
|
|
1300
|
+
onGestureStart: () => {
|
|
1301
|
+
gesture = {};
|
|
1302
|
+
// Pixi dispatches to the gizmo's own graphics before our canvas listener runs; this
|
|
1303
|
+
// stops a handle press from also re-picking underneath the gizmo.
|
|
1304
|
+
gizmoBusy = true;
|
|
1305
|
+
setTimeout(() => {
|
|
1306
|
+
gizmoBusy = false;
|
|
1307
|
+
}, 0);
|
|
1308
|
+
},
|
|
1309
|
+
onGestureEnd: () => {
|
|
1310
|
+
const pending = gesture?.inverse;
|
|
1311
|
+
gesture = null;
|
|
1312
|
+
if (pending)
|
|
1313
|
+
emit('edit', { inverse: pending });
|
|
1314
|
+
},
|
|
1315
|
+
// A press inside the selection box that never became a drag is a click, not a move:
|
|
1316
|
+
// forward it so overlapping siblings and deeper children stay reachable.
|
|
1317
|
+
onBodyClick: (x, y, alt) => pickAt(x, y, alt),
|
|
1053
1318
|
});
|
|
1054
1319
|
const select = (id) => {
|
|
1055
1320
|
selectedId = id;
|
|
1056
1321
|
gizmo.attach(id);
|
|
1057
1322
|
emit('selected', { id });
|
|
1058
1323
|
};
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1324
|
+
/**
|
|
1325
|
+
* Canvas picking. Every node used to carry its own `hitArea` + a stopPropagation
|
|
1326
|
+
* pointerdown, which meant (a) the click rect went stale the moment anything resized,
|
|
1327
|
+
* and (b) you landed on whatever node happened to be deepest with no way to go up or
|
|
1328
|
+
* down. Picking is now computed at click time from live bounds, and yields the whole
|
|
1329
|
+
* ancestor chain under the cursor so the user can drill.
|
|
1330
|
+
*/
|
|
1331
|
+
const hitChain = (globalX, globalY) => {
|
|
1332
|
+
const rootId = handle.doc().root.id;
|
|
1333
|
+
let best = [];
|
|
1334
|
+
const walk = (node, ancestors) => {
|
|
1335
|
+
const view = handle.node(node.id);
|
|
1336
|
+
if (!view || !view.visible || view.alpha === 0)
|
|
1337
|
+
return;
|
|
1338
|
+
const b = view.getBounds();
|
|
1339
|
+
const inside = globalX >= b.x && globalX <= b.x + b.width && globalY >= b.y && globalY <= b.y + b.height;
|
|
1340
|
+
const chain = inside && node.id !== rootId ? [...ancestors, node.id] : ancestors;
|
|
1341
|
+
// Document order IS draw order (parents before children, siblings in order), so the
|
|
1342
|
+
// LAST hit is the one drawn on top. Preferring the deepest instead would make a node
|
|
1343
|
+
// added on top of the reels lose to the grid underneath it.
|
|
1344
|
+
if (inside && chain.length > 0)
|
|
1345
|
+
best = chain;
|
|
1346
|
+
for (const child of node.children ?? [])
|
|
1347
|
+
walk(child, chain);
|
|
1348
|
+
};
|
|
1349
|
+
walk(handle.doc().root, []);
|
|
1350
|
+
return best; // outermost → innermost
|
|
1351
|
+
};
|
|
1352
|
+
/** Where the last pick happened, so clicking the same spot again drills one level in. */
|
|
1353
|
+
let lastPick = null;
|
|
1354
|
+
const pickAt = (globalX, globalY, deepest = false) => {
|
|
1355
|
+
const chain = hitChain(globalX, globalY);
|
|
1356
|
+
if (chain.length === 0) {
|
|
1357
|
+
lastPick = null;
|
|
1358
|
+
select(null);
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
const sameSpot = lastPick !== null &&
|
|
1362
|
+
Math.abs(lastPick.x - globalX) < 5 &&
|
|
1363
|
+
Math.abs(lastPick.y - globalY) < 5 &&
|
|
1364
|
+
lastPick.chain.join() === chain.join();
|
|
1365
|
+
const index = deepest
|
|
1366
|
+
? chain.length - 1
|
|
1367
|
+
: sameSpot
|
|
1368
|
+
? Math.min(lastPick.index + 1, chain.length - 1)
|
|
1369
|
+
: 0;
|
|
1370
|
+
lastPick = { x: globalX, y: globalY, chain, index };
|
|
1371
|
+
select(chain[index]);
|
|
1372
|
+
};
|
|
1373
|
+
/** Select the parent of the current selection (Esc on the canvas, or the IDE's key). */
|
|
1374
|
+
const selectParent = () => {
|
|
1375
|
+
if (!selectedId)
|
|
1376
|
+
return;
|
|
1377
|
+
const rootId = handle.doc().root.id;
|
|
1378
|
+
const findParent = (node, parent) => {
|
|
1379
|
+
if (node.id === selectedId)
|
|
1380
|
+
return parent;
|
|
1381
|
+
for (const c of node.children ?? []) {
|
|
1382
|
+
const hit = findParent(c, node);
|
|
1383
|
+
if (hit)
|
|
1384
|
+
return hit;
|
|
1075
1385
|
}
|
|
1076
|
-
|
|
1386
|
+
return null;
|
|
1077
1387
|
};
|
|
1078
|
-
|
|
1388
|
+
const parent = findParent(handle.doc().root, null);
|
|
1389
|
+
if (parent && parent.id !== rootId) {
|
|
1390
|
+
// Keep drilling consistent: stepping out moves the cursor back up the same chain.
|
|
1391
|
+
if (lastPick)
|
|
1392
|
+
lastPick.index = Math.max(0, lastPick.chain.indexOf(parent.id));
|
|
1393
|
+
select(parent.id);
|
|
1394
|
+
}
|
|
1395
|
+
else {
|
|
1396
|
+
lastPick = null;
|
|
1397
|
+
select(null);
|
|
1398
|
+
}
|
|
1079
1399
|
};
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1400
|
+
// Picking listens on the CANVAS ELEMENT rather than the Pixi stage: a stage-level
|
|
1401
|
+
// `pointerdown` is only delivered when the stage itself wins the hit test, which depends
|
|
1402
|
+
// on event modes set elsewhere in the host — node-level handlers fired while the stage's
|
|
1403
|
+
// did not. The DOM event always arrives, and converting it is one rectangle of maths.
|
|
1404
|
+
const canvasEl = (app.canvas ?? app.view);
|
|
1405
|
+
/** True while the gizmo is handling this press (its own handles/body take priority). */
|
|
1406
|
+
let gizmoBusy = false;
|
|
1407
|
+
const onCanvasDown = (e) => {
|
|
1408
|
+
if (gizmoBusy)
|
|
1409
|
+
return;
|
|
1410
|
+
const rect = canvasEl.getBoundingClientRect();
|
|
1411
|
+
if (rect.width === 0 || rect.height === 0)
|
|
1412
|
+
return;
|
|
1413
|
+
// CSS pixels → renderer screen units (the space Pixi bounds are reported in).
|
|
1414
|
+
const x = ((e.clientX - rect.left) / rect.width) * app.screen.width;
|
|
1415
|
+
const y = ((e.clientY - rect.top) / rect.height) * app.screen.height;
|
|
1416
|
+
pickAt(x, y, e.altKey);
|
|
1417
|
+
};
|
|
1418
|
+
canvasEl.addEventListener('pointerdown', onCanvasDown);
|
|
1084
1419
|
const afterStructural = () => {
|
|
1085
1420
|
opts.relayout?.();
|
|
1086
|
-
wireHitAreas();
|
|
1087
1421
|
};
|
|
1088
1422
|
// ── RPC ───────────────────────────────────────────────────────────────────────
|
|
1089
1423
|
const methods = {
|
|
1090
1424
|
snapshot: () => snapshot(),
|
|
1091
1425
|
patch: (params) => {
|
|
1092
1426
|
const patch = params;
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1427
|
+
// A rejected patch used to warn into the game's console and still answer "ok" —
|
|
1428
|
+
// throw so the editor learns why nothing changed.
|
|
1429
|
+
const result = handle.patch(patch);
|
|
1430
|
+
if (result && result.ok === false)
|
|
1431
|
+
throw new Error(result.error);
|
|
1432
|
+
// Geometry may have moved (a swapped texture resizes a sprite just as a layout edit
|
|
1433
|
+
// does); picking reads live bounds, so only the host relayout hook is needed.
|
|
1434
|
+
afterStructural();
|
|
1435
|
+
// Broadcast too: patches can arrive from outside the editor (an agent driving the
|
|
1436
|
+
// bridge directly), and those used to leave the editor mirroring a stale doc.
|
|
1437
|
+
pushSnapshot();
|
|
1438
|
+
// The inverse rides back with the response rather than as an event, so the caller
|
|
1439
|
+
// records exactly the edits it made — no double-recording, no message ordering race.
|
|
1440
|
+
return { ...snapshot(), inverse: result.ok === true ? result.inverse : undefined };
|
|
1441
|
+
},
|
|
1442
|
+
/** The aliases `props.src` may hold — authoritative, unlike the public/assets file scan. */
|
|
1443
|
+
assetAliases: () => ({ aliases: opts.assetAliases?.() ?? null }),
|
|
1444
|
+
/**
|
|
1445
|
+
* Run a gizmo handle drag programmatically — the same path the mouse takes (down, move,
|
|
1446
|
+
* up), so tests and agents exercise gesture coalescing rather than a single patch.
|
|
1447
|
+
*/
|
|
1448
|
+
/** Arrow-key move of the selection, in screen px (one undo step per call). */
|
|
1449
|
+
nudge: (params) => {
|
|
1450
|
+
const p = params;
|
|
1451
|
+
gizmo.nudge(p.dx, p.dy);
|
|
1452
|
+
return { selectedId };
|
|
1453
|
+
},
|
|
1454
|
+
gizmoDrag: (params) => {
|
|
1455
|
+
const p = params;
|
|
1456
|
+
gizmo.drag(p.handle, p.from, p.to);
|
|
1457
|
+
return { ok: true };
|
|
1098
1458
|
},
|
|
1099
1459
|
select: (params) => {
|
|
1100
|
-
|
|
1460
|
+
const id = params.id;
|
|
1461
|
+
// A selection made in the tree restarts the drill cursor for the next canvas click.
|
|
1462
|
+
lastPick = null;
|
|
1463
|
+
select(id);
|
|
1101
1464
|
return { selectedId };
|
|
1102
1465
|
},
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1466
|
+
/** Step out one level (Esc) — the counterpart to click-again-to-drill-in. */
|
|
1467
|
+
selectParent: () => {
|
|
1468
|
+
selectParent();
|
|
1469
|
+
return { selectedId };
|
|
1470
|
+
},
|
|
1471
|
+
/**
|
|
1472
|
+
* Pick at a canvas point exactly as a click does — same drill-in cursor. Exposed so
|
|
1473
|
+
* tests and agents can select without synthesising pointer events (headless renderers
|
|
1474
|
+
* do not deliver them), and so the chain is inspectable.
|
|
1475
|
+
*/
|
|
1476
|
+
pick: (params) => {
|
|
1477
|
+
const p = params;
|
|
1478
|
+
pickAt(p.x, p.y, p.deepest === true);
|
|
1479
|
+
return { selectedId, chain: hitChain(p.x, p.y) };
|
|
1111
1480
|
},
|
|
1112
1481
|
save: async (params) => {
|
|
1113
1482
|
const file = params?.file ?? opts.sceneFile;
|
|
@@ -1158,7 +1527,9 @@ function mountIdeBridge(opts) {
|
|
|
1158
1527
|
file: f.file,
|
|
1159
1528
|
events: Object.keys(f.doc.on ?? {}),
|
|
1160
1529
|
cues: Object.keys(f.doc.cues ?? {}),
|
|
1161
|
-
|
|
1530
|
+
// Prefer what the runner actually accepts, with each kind's authoring schema; the
|
|
1531
|
+
// editor is cross-origin and cannot reach the registry itself.
|
|
1532
|
+
stepKinds: f.runner.stepKinds?.() ?? (f.stepKinds ?? []).map((kind) => ({ kind })),
|
|
1162
1533
|
ctx: f.ctx ?? [],
|
|
1163
1534
|
};
|
|
1164
1535
|
},
|
|
@@ -1206,12 +1577,54 @@ function mountIdeBridge(opts) {
|
|
|
1206
1577
|
requireFlow().runner.skip();
|
|
1207
1578
|
return { ok: true };
|
|
1208
1579
|
},
|
|
1580
|
+
// ── stage surface (multi-scene graph) — present only if opts.stage was given ─────
|
|
1581
|
+
stageMeta: () => {
|
|
1582
|
+
const s = opts.stage;
|
|
1583
|
+
if (!s)
|
|
1584
|
+
return { enabled: false };
|
|
1585
|
+
return { enabled: true, manifest: s.manifest, activeKey: s.activeKey, file: s.file, sceneParam: s.sceneParam ?? 'scene' };
|
|
1586
|
+
},
|
|
1587
|
+
stageSetManifest: (params) => {
|
|
1588
|
+
requireStage().manifest = params;
|
|
1589
|
+
return { ok: true };
|
|
1590
|
+
},
|
|
1591
|
+
stageSave: async () => {
|
|
1592
|
+
const s = requireStage();
|
|
1593
|
+
if (!s.file)
|
|
1594
|
+
throw new Error('no stage file configured');
|
|
1595
|
+
const res = await fetch('/__ide/write', {
|
|
1596
|
+
method: 'POST',
|
|
1597
|
+
headers: { 'content-type': 'application/json' },
|
|
1598
|
+
body: JSON.stringify({ file: s.file, content: s.manifest }),
|
|
1599
|
+
});
|
|
1600
|
+
if (!res.ok)
|
|
1601
|
+
throw new Error(await res.text());
|
|
1602
|
+
return { saved: s.file };
|
|
1603
|
+
},
|
|
1604
|
+
// Create a scene doc file (blank or a copy) so a new/duplicated scene has a real doc.
|
|
1605
|
+
stageCreateScene: async (params) => {
|
|
1606
|
+
const { file, from } = params;
|
|
1607
|
+
const content = from ?? { version: 1, id: file, design: handle.doc().design, root: { id: 'root', type: 'container', children: [] } };
|
|
1608
|
+
const res = await fetch('/__ide/write', {
|
|
1609
|
+
method: 'POST',
|
|
1610
|
+
headers: { 'content-type': 'application/json' },
|
|
1611
|
+
body: JSON.stringify({ file, content }),
|
|
1612
|
+
});
|
|
1613
|
+
if (!res.ok)
|
|
1614
|
+
throw new Error(await res.text());
|
|
1615
|
+
return { file };
|
|
1616
|
+
},
|
|
1209
1617
|
};
|
|
1210
1618
|
const requireFlow = () => {
|
|
1211
1619
|
if (!opts.flow)
|
|
1212
1620
|
throw new Error('this game exposed no flow surface');
|
|
1213
1621
|
return opts.flow;
|
|
1214
1622
|
};
|
|
1623
|
+
const requireStage = () => {
|
|
1624
|
+
if (!opts.stage)
|
|
1625
|
+
throw new Error('this game exposed no stage surface');
|
|
1626
|
+
return opts.stage;
|
|
1627
|
+
};
|
|
1215
1628
|
/** Topmost sprite-like node (props.src) whose laid-out bounds contain the point. */
|
|
1216
1629
|
const topSpriteAt = (x, y) => {
|
|
1217
1630
|
let best = null;
|
|
@@ -1260,13 +1673,14 @@ function mountIdeBridge(opts) {
|
|
|
1260
1673
|
};
|
|
1261
1674
|
window.addEventListener('message', onMessage);
|
|
1262
1675
|
// Announce readiness (the IDE may mount before or after the game boots).
|
|
1263
|
-
emit('ready', { docId: handle.doc().id, hasFlow: !!opts.flow });
|
|
1676
|
+
emit('ready', { docId: handle.doc().id, hasFlow: !!opts.flow, hasStage: !!opts.stage });
|
|
1264
1677
|
return {
|
|
1265
1678
|
refresh: pushSnapshot,
|
|
1266
1679
|
select,
|
|
1267
1680
|
gizmo,
|
|
1268
1681
|
destroy() {
|
|
1269
1682
|
window.removeEventListener('message', onMessage);
|
|
1683
|
+
canvasEl.removeEventListener('pointerdown', onCanvasDown);
|
|
1270
1684
|
gizmo.destroy();
|
|
1271
1685
|
},
|
|
1272
1686
|
};
|