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