@energy8platform/game-engine 0.33.6 → 0.33.7
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/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/dist/scene.cjs.js
CHANGED
|
@@ -272,22 +272,47 @@ function evalVisibleWhen(expr, vars) {
|
|
|
272
272
|
// schema (validation + inspector autogen) and an `agentDoc` (the plugin's documentation
|
|
273
273
|
// and the agent's prompt are the same text).
|
|
274
274
|
/** Merge built-ins with plugin contributions; later plugins may override earlier kinds. */
|
|
275
|
-
function createSceneRegistry(plugins = [], builtins = []) {
|
|
275
|
+
function createSceneRegistry(plugins = [], builtins = [], builtinFilters = []) {
|
|
276
276
|
const nodeTypes = new Map();
|
|
277
277
|
const prefabs = new Map();
|
|
278
|
+
const filterKinds = new Map();
|
|
278
279
|
for (const contribution of builtins)
|
|
279
280
|
nodeTypes.set(contribution.kind, contribution);
|
|
281
|
+
for (const contribution of builtinFilters)
|
|
282
|
+
filterKinds.set(contribution.kind, contribution);
|
|
280
283
|
for (const plugin of plugins) {
|
|
281
284
|
for (const contribution of plugin.nodeTypes ?? [])
|
|
282
285
|
nodeTypes.set(contribution.kind, contribution);
|
|
283
286
|
for (const prefab of plugin.prefabs ?? [])
|
|
284
287
|
prefabs.set(prefab.name, prefab);
|
|
288
|
+
for (const contribution of plugin.filterKinds ?? [])
|
|
289
|
+
filterKinds.set(contribution.kind, contribution);
|
|
285
290
|
}
|
|
286
291
|
return {
|
|
287
292
|
nodeType: (kind) => nodeTypes.get(kind),
|
|
288
293
|
prefab: (name) => prefabs.get(name),
|
|
294
|
+
filterKind: (kind) => filterKinds.get(kind),
|
|
289
295
|
kinds: () => [...nodeTypes.keys()],
|
|
290
296
|
prefabNames: () => [...prefabs.keys()],
|
|
297
|
+
filterKindNames: () => [...filterKinds.keys()],
|
|
298
|
+
palette() {
|
|
299
|
+
const entries = [];
|
|
300
|
+
for (const c of nodeTypes.values()) {
|
|
301
|
+
if (!c.defaults)
|
|
302
|
+
continue; // hide dispatch-only kinds (prefab)
|
|
303
|
+
entries.push({ kind: c.kind, label: c.defaults.label ?? c.kind, isPrefab: false, agentDoc: c.agentDoc, defaults: c.defaults });
|
|
304
|
+
}
|
|
305
|
+
for (const p of prefabs.values()) {
|
|
306
|
+
entries.push({
|
|
307
|
+
kind: p.name,
|
|
308
|
+
label: p.defaults?.label ?? p.name,
|
|
309
|
+
isPrefab: true,
|
|
310
|
+
agentDoc: p.agentDoc,
|
|
311
|
+
defaults: p.defaults ?? {},
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return entries;
|
|
315
|
+
},
|
|
291
316
|
};
|
|
292
317
|
}
|
|
293
318
|
|
|
@@ -2925,13 +2950,14 @@ function emptyBoard(cfg) {
|
|
|
2925
2950
|
// Built-in node contributions: the core vocabulary every scene doc can rely on.
|
|
2926
2951
|
// Everything else (ropes, meters, particles, game HUDs) arrives as a ScenePlugin through
|
|
2927
2952
|
// the same contribution shape — built-ins get no special powers.
|
|
2928
|
-
const num = (v, fallback) => (typeof v === 'number' && Number.isFinite(v) ? v : fallback);
|
|
2953
|
+
const num$1 = (v, fallback) => (typeof v === 'number' && Number.isFinite(v) ? v : fallback);
|
|
2929
2954
|
const str = (v, fallback) => (typeof v === 'string' ? v : fallback);
|
|
2930
2955
|
// ── container / layer ───────────────────────────────────────────────────────────────
|
|
2931
2956
|
function containerContribution(kind) {
|
|
2932
2957
|
return {
|
|
2933
2958
|
kind,
|
|
2934
2959
|
agentDoc: 'Grouping node; order of children = z-order. Keep it un-laid-out (identity) unless you move a whole group. With props.space {width,height} it becomes a nested coordinate space: descendants lay out inside that fixed design space and its own rule places the space (cover → the diorama idiom). Natural size = space size, else 0×0.',
|
|
2960
|
+
defaults: { label: kind === 'layer' ? 'Layer' : 'Container', props: {} },
|
|
2935
2961
|
create(node) {
|
|
2936
2962
|
const view = new pixi_js.Container();
|
|
2937
2963
|
return {
|
|
@@ -2943,7 +2969,7 @@ function containerContribution(kind) {
|
|
|
2943
2969
|
: { width: 0, height: 0 };
|
|
2944
2970
|
},
|
|
2945
2971
|
applyProps(props) {
|
|
2946
|
-
view.alpha = num(props.alpha, 1);
|
|
2972
|
+
view.alpha = num$1(props.alpha, 1);
|
|
2947
2973
|
},
|
|
2948
2974
|
};
|
|
2949
2975
|
},
|
|
@@ -2953,18 +2979,23 @@ function containerContribution(kind) {
|
|
|
2953
2979
|
const rectContribution = {
|
|
2954
2980
|
kind: 'rect',
|
|
2955
2981
|
agentDoc: 'Solid rounded rectangle: plates behind text, dimmers, and mask targets (reference its id from another node\'s `mask`). Props: width, height, fill, alpha, radius, stroke {color,width}.',
|
|
2982
|
+
defaults: {
|
|
2983
|
+
label: 'Rectangle',
|
|
2984
|
+
props: { width: 200, height: 120, fill: '#3355aa', radius: 12 },
|
|
2985
|
+
layout: { mode: 'viewport-fraction', xFrac: 0.5, yFrac: 0.5, anchor: [0.5, 0.5] },
|
|
2986
|
+
},
|
|
2956
2987
|
create(node) {
|
|
2957
2988
|
const g = new pixi_js.Graphics();
|
|
2958
2989
|
let size = { width: 0, height: 0 };
|
|
2959
2990
|
const draw = (props) => {
|
|
2960
|
-
const width = num(props.width, 100);
|
|
2961
|
-
const height = num(props.height, 100);
|
|
2962
|
-
const radius = num(props.radius, 0);
|
|
2991
|
+
const width = num$1(props.width, 100);
|
|
2992
|
+
const height = num$1(props.height, 100);
|
|
2993
|
+
const radius = num$1(props.radius, 0);
|
|
2963
2994
|
size = { width, height };
|
|
2964
2995
|
g.clear();
|
|
2965
2996
|
g.roundRect(0, 0, width, height, radius).fill({
|
|
2966
2997
|
color: str(props.fill, '#ffffff'),
|
|
2967
|
-
alpha: num(props.alpha, 1),
|
|
2998
|
+
alpha: num$1(props.alpha, 1),
|
|
2968
2999
|
});
|
|
2969
3000
|
const stroke = props.stroke;
|
|
2970
3001
|
if (stroke) {
|
|
@@ -3004,7 +3035,7 @@ function spriteInstance(node, ctx) {
|
|
|
3004
3035
|
// Visual props go on the wrapper (the node's view) so tint/alpha are observable and
|
|
3005
3036
|
// patchable on the node itself; the inner sprite only carries texture + flip.
|
|
3006
3037
|
wrapper.tint = p.tint ?? 0xffffff;
|
|
3007
|
-
wrapper.alpha = num(p.alpha, 1);
|
|
3038
|
+
wrapper.alpha = num$1(p.alpha, 1);
|
|
3008
3039
|
const flip = p.flipX === true;
|
|
3009
3040
|
sprite.scale.x = flip ? -1 : 1;
|
|
3010
3041
|
sprite.position.x = flip ? sprite.texture.width : 0;
|
|
@@ -3019,6 +3050,11 @@ function spriteInstance(node, ctx) {
|
|
|
3019
3050
|
const spriteContribution = {
|
|
3020
3051
|
kind: 'sprite',
|
|
3021
3052
|
agentDoc: 'Static art. Props: src (texture alias), tint, alpha. Natural size = texture size; use layout to place/scale.',
|
|
3053
|
+
defaults: {
|
|
3054
|
+
label: 'Sprite',
|
|
3055
|
+
props: {},
|
|
3056
|
+
layout: { mode: 'viewport-fraction', xFrac: 0.5, yFrac: 0.5, anchor: [0.5, 0.5] },
|
|
3057
|
+
},
|
|
3022
3058
|
create: spriteInstance,
|
|
3023
3059
|
};
|
|
3024
3060
|
const reelFrameContribution = {
|
|
@@ -3030,17 +3066,22 @@ const reelFrameContribution = {
|
|
|
3030
3066
|
const textContribution = {
|
|
3031
3067
|
kind: 'text',
|
|
3032
3068
|
agentDoc: 'Vector text (labels, logos-as-type). Props: text, fontSize, fill, fontFamily, fontWeight, align, letterSpacing. For rolling win numbers prefer a bitmapNumber/prefab, not text.',
|
|
3069
|
+
defaults: {
|
|
3070
|
+
label: 'Text',
|
|
3071
|
+
props: { text: 'Text', fontSize: 48, fill: '#ffffff', fontWeight: 'bold' },
|
|
3072
|
+
layout: { mode: 'viewport-fraction', xFrac: 0.5, yFrac: 0.5, anchor: [0.5, 0.5] },
|
|
3073
|
+
},
|
|
3033
3074
|
create(node) {
|
|
3034
3075
|
const props = node.props ?? {};
|
|
3035
3076
|
const view = new pixi_js.Text({
|
|
3036
3077
|
text: str(props.text, ''),
|
|
3037
3078
|
style: {
|
|
3038
3079
|
fontFamily: str(props.fontFamily, 'Arial, sans-serif'),
|
|
3039
|
-
fontSize: num(props.fontSize, 32),
|
|
3080
|
+
fontSize: num$1(props.fontSize, 32),
|
|
3040
3081
|
fill: props.fill ?? '#ffffff',
|
|
3041
3082
|
fontWeight: props.fontWeight ?? 'normal',
|
|
3042
3083
|
align: props.align ?? 'left',
|
|
3043
|
-
letterSpacing: num(props.letterSpacing, 0),
|
|
3084
|
+
letterSpacing: num$1(props.letterSpacing, 0),
|
|
3044
3085
|
},
|
|
3045
3086
|
});
|
|
3046
3087
|
return {
|
|
@@ -3050,9 +3091,9 @@ const textContribution = {
|
|
|
3050
3091
|
if (p.text !== undefined)
|
|
3051
3092
|
view.text = str(p.text, '');
|
|
3052
3093
|
view.style.fill = p.fill ?? '#ffffff';
|
|
3053
|
-
view.style.fontSize = num(p.fontSize, 32);
|
|
3054
|
-
view.style.letterSpacing = num(p.letterSpacing, 0);
|
|
3055
|
-
view.alpha = num(p.alpha, 1);
|
|
3094
|
+
view.style.fontSize = num$1(p.fontSize, 32);
|
|
3095
|
+
view.style.letterSpacing = num$1(p.letterSpacing, 0);
|
|
3096
|
+
view.alpha = num$1(p.alpha, 1);
|
|
3056
3097
|
},
|
|
3057
3098
|
};
|
|
3058
3099
|
},
|
|
@@ -3068,8 +3109,8 @@ const animatedSpriteContribution = {
|
|
|
3068
3109
|
const apply = (p) => {
|
|
3069
3110
|
const sheet = p.sheet;
|
|
3070
3111
|
const alias = str(sheet?.alias, '');
|
|
3071
|
-
const cols = num(sheet?.cols, 1);
|
|
3072
|
-
const rows = num(sheet?.rows, 1);
|
|
3112
|
+
const cols = num$1(sheet?.cols, 1);
|
|
3113
|
+
const rows = num$1(sheet?.rows, 1);
|
|
3073
3114
|
const full = alias ? ctx.texture(alias) : pixi_js.Texture.EMPTY;
|
|
3074
3115
|
const frames = [];
|
|
3075
3116
|
if (full !== pixi_js.Texture.EMPTY && cols > 0 && rows > 0) {
|
|
@@ -3083,14 +3124,14 @@ const animatedSpriteContribution = {
|
|
|
3083
3124
|
}
|
|
3084
3125
|
animated?.destroy();
|
|
3085
3126
|
animated = new pixi_js.AnimatedSprite(frames.length ? frames : [pixi_js.Texture.EMPTY]);
|
|
3086
|
-
animated.animationSpeed = num(p.fps, 12) / 60;
|
|
3127
|
+
animated.animationSpeed = num$1(p.fps, 12) / 60;
|
|
3087
3128
|
animated.loop = true;
|
|
3088
|
-
const frame = Math.min(num(p.frame, 0), animated.totalFrames - 1);
|
|
3129
|
+
const frame = Math.min(num$1(p.frame, 0), animated.totalFrames - 1);
|
|
3089
3130
|
if (p.playing === true)
|
|
3090
3131
|
animated.gotoAndPlay(Math.max(0, frame));
|
|
3091
3132
|
else
|
|
3092
3133
|
animated.gotoAndStop(Math.max(0, frame));
|
|
3093
|
-
animated.alpha = num(p.alpha, 1);
|
|
3134
|
+
animated.alpha = num$1(p.alpha, 1);
|
|
3094
3135
|
view.removeChildren();
|
|
3095
3136
|
view.addChild(animated);
|
|
3096
3137
|
size = { width: animated.textures.length ? animated.textures[0].width : 0, height: animated.textures.length ? animated.textures[0].height : 0 };
|
|
@@ -3176,6 +3217,19 @@ const prefabContribution = {
|
|
|
3176
3217
|
return prefab.create(node.props ?? {}, ctx);
|
|
3177
3218
|
},
|
|
3178
3219
|
};
|
|
3220
|
+
/** Core filter kinds; heavier looks (bloom etc.) arrive as plugins carrying their deps. */
|
|
3221
|
+
const BUILTIN_FILTER_KINDS = [
|
|
3222
|
+
{
|
|
3223
|
+
kind: 'blur',
|
|
3224
|
+
agentDoc: 'Gaussian blur: {kind:"blur", strength, quality}. Shadow/glow layers idiom.',
|
|
3225
|
+
create(spec) {
|
|
3226
|
+
return new pixi_js.BlurFilter({
|
|
3227
|
+
strength: typeof spec.strength === 'number' ? spec.strength : 4,
|
|
3228
|
+
quality: typeof spec.quality === 'number' ? spec.quality : 3,
|
|
3229
|
+
});
|
|
3230
|
+
},
|
|
3231
|
+
},
|
|
3232
|
+
];
|
|
3179
3233
|
const BUILTIN_NODE_TYPES = [
|
|
3180
3234
|
containerContribution('container'),
|
|
3181
3235
|
containerContribution('layer'),
|
|
@@ -3267,7 +3321,7 @@ function validateSceneDoc(doc, registry) {
|
|
|
3267
3321
|
const MAX_LAYOUT_PASSES = 5;
|
|
3268
3322
|
function createSceneFromDoc(doc, opts = {}) {
|
|
3269
3323
|
const log = opts.log ?? ((msg) => console.warn(`[scene] ${msg}`));
|
|
3270
|
-
const registry = createSceneRegistry(opts.plugins ?? [], BUILTIN_NODE_TYPES);
|
|
3324
|
+
const registry = createSceneRegistry(opts.plugins ?? [], BUILTIN_NODE_TYPES, BUILTIN_FILTER_KINDS);
|
|
3271
3325
|
const errors = validateSceneDoc(doc, registry);
|
|
3272
3326
|
if (errors.length > 0) {
|
|
3273
3327
|
const detail = errors.map((e) => (e.nodeId ? `[${e.nodeId}] ${e.message}` : e.message)).join('\n ');
|
|
@@ -3302,6 +3356,27 @@ function createSceneFromDoc(doc, opts = {}) {
|
|
|
3302
3356
|
throw new Error(`scene node "${node.id}" (${node.type}) failed to create: ${err.message}`);
|
|
3303
3357
|
}
|
|
3304
3358
|
parentView.addChild(instance.view);
|
|
3359
|
+
if (node.filters?.length) {
|
|
3360
|
+
const filters = node.filters
|
|
3361
|
+
.map((spec) => {
|
|
3362
|
+
const contribution = registry.filterKind(spec.kind);
|
|
3363
|
+
if (!contribution) {
|
|
3364
|
+
warnOnce(`filter:${spec.kind}`, `unknown filter kind "${spec.kind}" on "${node.id}" — no plugin contributes it`);
|
|
3365
|
+
return null;
|
|
3366
|
+
}
|
|
3367
|
+
try {
|
|
3368
|
+
return contribution.create(spec);
|
|
3369
|
+
}
|
|
3370
|
+
catch (err) {
|
|
3371
|
+
// Filters need a GPU context — degrade gracefully in headless runs.
|
|
3372
|
+
warnOnce(`filter-fail:${spec.kind}`, `filter "${spec.kind}" failed to create: ${err.message}`);
|
|
3373
|
+
return null;
|
|
3374
|
+
}
|
|
3375
|
+
})
|
|
3376
|
+
.filter((f) => f !== null);
|
|
3377
|
+
if (filters.length)
|
|
3378
|
+
instance.view.filters = filters;
|
|
3379
|
+
}
|
|
3305
3380
|
const runtime = {
|
|
3306
3381
|
node,
|
|
3307
3382
|
instance,
|
|
@@ -3470,8 +3545,15 @@ function createSceneFromDoc(doc, opts = {}) {
|
|
|
3470
3545
|
const eff = effectiveNode(runtime.node, orientation, runtime.state);
|
|
3471
3546
|
const natural = runtime.instance.measure();
|
|
3472
3547
|
const rule = eff.layout;
|
|
3548
|
+
// `space:'viewport'` resolves against the live viewport even inside a nested
|
|
3549
|
+
// space, then converts into the space's coordinates (screen-pin at a space depth).
|
|
3550
|
+
const viewportSpace = rule?.space === 'viewport' && frame !== null;
|
|
3551
|
+
if (viewportSpace && !frame.bounds) {
|
|
3552
|
+
next.push(runtime); // need the space root's global rect first
|
|
3553
|
+
continue;
|
|
3554
|
+
}
|
|
3473
3555
|
const resolution = rule
|
|
3474
|
-
? resolveLayoutRule(rule, natural, layoutCtx(frame))
|
|
3556
|
+
? resolveLayoutRule(rule, natural, layoutCtx(viewportSpace ? null : frame))
|
|
3475
3557
|
: {
|
|
3476
3558
|
placement: {
|
|
3477
3559
|
x: parentOriginX,
|
|
@@ -3486,7 +3568,21 @@ function createSceneFromDoc(doc, opts = {}) {
|
|
|
3486
3568
|
next.push(runtime);
|
|
3487
3569
|
continue;
|
|
3488
3570
|
}
|
|
3489
|
-
|
|
3571
|
+
let p = resolution.placement;
|
|
3572
|
+
if (viewportSpace) {
|
|
3573
|
+
if (frameOf(frame) !== null) {
|
|
3574
|
+
warnOnce(`vspace:${runtime.node.id}`, `space:'viewport' inside a nested space is unsupported ("${runtime.node.id}")`);
|
|
3575
|
+
}
|
|
3576
|
+
else {
|
|
3577
|
+
p = {
|
|
3578
|
+
...p,
|
|
3579
|
+
x: (p.x - frame.bounds.x) / frame.worldScaleX,
|
|
3580
|
+
y: (p.y - frame.bounds.y) / frame.worldScaleY,
|
|
3581
|
+
scaleX: p.scaleX / frame.worldScaleX,
|
|
3582
|
+
scaleY: p.scaleY / frame.worldScaleY,
|
|
3583
|
+
};
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3490
3586
|
const view = runtime.instance.view;
|
|
3491
3587
|
view.position.set((p.x - parentOriginX) / parentScaleX, (p.y - parentOriginY) / parentScaleY);
|
|
3492
3588
|
view.scale.set(p.scaleX / parentScaleX, p.scaleY / parentScaleY);
|
|
@@ -3544,6 +3640,61 @@ function createSceneFromDoc(doc, opts = {}) {
|
|
|
3544
3640
|
}
|
|
3545
3641
|
return undefined;
|
|
3546
3642
|
};
|
|
3643
|
+
/** Locate a node's parent doc-node + its index in that parent's children. */
|
|
3644
|
+
const locate = (id) => {
|
|
3645
|
+
const walk = (node) => {
|
|
3646
|
+
const kids = node.children ?? [];
|
|
3647
|
+
for (let i = 0; i < kids.length; i++) {
|
|
3648
|
+
if (kids[i].id === id)
|
|
3649
|
+
return { parent: node, index: i };
|
|
3650
|
+
const hit = walk(kids[i]);
|
|
3651
|
+
if (hit)
|
|
3652
|
+
return hit;
|
|
3653
|
+
}
|
|
3654
|
+
return undefined;
|
|
3655
|
+
};
|
|
3656
|
+
return walk(doc.root);
|
|
3657
|
+
};
|
|
3658
|
+
const collectIds = (node, out = []) => {
|
|
3659
|
+
out.push(node.id);
|
|
3660
|
+
for (const child of node.children ?? [])
|
|
3661
|
+
collectIds(child, out);
|
|
3662
|
+
return out;
|
|
3663
|
+
};
|
|
3664
|
+
/** Deep-clone a subtree, suffixing every id/anchorName so it stays unique in the doc. */
|
|
3665
|
+
const cloneWithSuffix = (node, suffix) => {
|
|
3666
|
+
const clone = JSON.parse(JSON.stringify(node));
|
|
3667
|
+
const remap = (n) => {
|
|
3668
|
+
n.id = `${n.id}${suffix}`;
|
|
3669
|
+
if (n.anchorName)
|
|
3670
|
+
n.anchorName = `${n.anchorName}${suffix}`;
|
|
3671
|
+
n.children?.forEach(remap);
|
|
3672
|
+
};
|
|
3673
|
+
remap(clone);
|
|
3674
|
+
return clone;
|
|
3675
|
+
};
|
|
3676
|
+
/** Build a freshly-inserted subtree under an existing parent runtime, at a child index. */
|
|
3677
|
+
const buildInto = (node, parentRuntime, index) => {
|
|
3678
|
+
const before = parentRuntime.instance.view.children.length;
|
|
3679
|
+
build(node, parentRuntime, parentRuntime.instance.view);
|
|
3680
|
+
// build appended to the end — move the new view to the requested child index.
|
|
3681
|
+
const view = runtimes.get(node.id).instance.view;
|
|
3682
|
+
const clampedIndex = Math.max(0, Math.min(index, before));
|
|
3683
|
+
parentRuntime.instance.view.setChildIndex(view, clampedIndex);
|
|
3684
|
+
};
|
|
3685
|
+
const destroyRuntimeTree = (id) => {
|
|
3686
|
+
const runtime = runtimes.get(id);
|
|
3687
|
+
if (!runtime)
|
|
3688
|
+
return;
|
|
3689
|
+
for (const descendantId of collectIds(runtime.node)) {
|
|
3690
|
+
const r = runtimes.get(descendantId);
|
|
3691
|
+
if (r) {
|
|
3692
|
+
r.instance.destroy?.();
|
|
3693
|
+
runtimes.delete(descendantId);
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
runtime.instance.view.removeFromParent();
|
|
3697
|
+
};
|
|
3547
3698
|
const setVar = (name, value) => {
|
|
3548
3699
|
vars[name] = value;
|
|
3549
3700
|
relayout();
|
|
@@ -3587,6 +3738,72 @@ function createSceneFromDoc(doc, opts = {}) {
|
|
|
3587
3738
|
return setState(patch.id, patch.state);
|
|
3588
3739
|
case 'set-var':
|
|
3589
3740
|
return setVar(patch.name, patch.value);
|
|
3741
|
+
case 'add-node': {
|
|
3742
|
+
const parentRuntime = runtimes.get(patch.parent);
|
|
3743
|
+
if (!parentRuntime)
|
|
3744
|
+
return warnOnce(`add:${patch.parent}`, `add-node: unknown parent "${patch.parent}"`);
|
|
3745
|
+
const clash = collectIds(patch.node).find((id) => runtimes.has(id));
|
|
3746
|
+
if (clash)
|
|
3747
|
+
return warnOnce(`add-clash:${clash}`, `add-node: id "${clash}" already exists`);
|
|
3748
|
+
if (!registry.nodeType(patch.node.type)) {
|
|
3749
|
+
return warnOnce(`add-kind:${patch.node.type}`, `add-node: unknown type "${patch.node.type}"`);
|
|
3750
|
+
}
|
|
3751
|
+
(parentRuntime.node.children ??= []).splice(patch.index ?? parentRuntime.node.children.length, 0, patch.node);
|
|
3752
|
+
buildInto(patch.node, parentRuntime, patch.index ?? Number.MAX_SAFE_INTEGER);
|
|
3753
|
+
wireMasks();
|
|
3754
|
+
break;
|
|
3755
|
+
}
|
|
3756
|
+
case 'remove-node': {
|
|
3757
|
+
if (patch.id === doc.root.id)
|
|
3758
|
+
return warnOnce('rm-root', 'remove-node: cannot remove the root');
|
|
3759
|
+
const loc = locate(patch.id);
|
|
3760
|
+
if (!loc)
|
|
3761
|
+
return warnOnce(`rm:${patch.id}`, `remove-node: unknown node "${patch.id}"`);
|
|
3762
|
+
destroyRuntimeTree(patch.id);
|
|
3763
|
+
loc.parent.children.splice(loc.index, 1);
|
|
3764
|
+
wireMasks();
|
|
3765
|
+
break;
|
|
3766
|
+
}
|
|
3767
|
+
case 'move-node': {
|
|
3768
|
+
if (patch.id === doc.root.id)
|
|
3769
|
+
return warnOnce('mv-root', 'move-node: cannot move the root');
|
|
3770
|
+
const loc = locate(patch.id);
|
|
3771
|
+
const source = findNode(doc.root, patch.id);
|
|
3772
|
+
const parentRuntime = runtimes.get(patch.parent);
|
|
3773
|
+
if (!loc || !source)
|
|
3774
|
+
return warnOnce(`mv:${patch.id}`, `move-node: unknown node "${patch.id}"`);
|
|
3775
|
+
if (!parentRuntime)
|
|
3776
|
+
return warnOnce(`mv-p:${patch.parent}`, `move-node: unknown parent "${patch.parent}"`);
|
|
3777
|
+
if (collectIds(source).includes(patch.parent)) {
|
|
3778
|
+
return warnOnce(`mv-cycle:${patch.id}`, `move-node: cannot move "${patch.id}" into itself/its descendant "${patch.parent}"`);
|
|
3779
|
+
}
|
|
3780
|
+
// `index` is the FINAL position in the destination's children (post-removal).
|
|
3781
|
+
const [node] = loc.parent.children.splice(loc.index, 1);
|
|
3782
|
+
destroyRuntimeTree(patch.id); // rebuild the moved subtree under the new parent
|
|
3783
|
+
const dest = (parentRuntime.node.children ??= []);
|
|
3784
|
+
const targetIndex = Math.max(0, Math.min(patch.index ?? dest.length, dest.length));
|
|
3785
|
+
dest.splice(targetIndex, 0, node);
|
|
3786
|
+
buildInto(node, parentRuntime, targetIndex);
|
|
3787
|
+
wireMasks();
|
|
3788
|
+
break;
|
|
3789
|
+
}
|
|
3790
|
+
case 'duplicate-node': {
|
|
3791
|
+
if (patch.id === doc.root.id)
|
|
3792
|
+
return warnOnce('dup-root', 'duplicate-node: cannot duplicate the root');
|
|
3793
|
+
const loc = locate(patch.id);
|
|
3794
|
+
const source = findNode(doc.root, patch.id);
|
|
3795
|
+
if (!loc || !source)
|
|
3796
|
+
return warnOnce(`dup:${patch.id}`, `duplicate-node: unknown node "${patch.id}"`);
|
|
3797
|
+
let suffix = '-copy';
|
|
3798
|
+
while (collectIds(source).some((id) => runtimes.has(`${id}${suffix}`)))
|
|
3799
|
+
suffix += '2';
|
|
3800
|
+
const clone = cloneWithSuffix(source, suffix);
|
|
3801
|
+
const parentRuntime = runtimes.get(loc.parent.id);
|
|
3802
|
+
loc.parent.children.splice(loc.index + 1, 0, clone);
|
|
3803
|
+
buildInto(clone, parentRuntime, loc.index + 1);
|
|
3804
|
+
wireMasks();
|
|
3805
|
+
break;
|
|
3806
|
+
}
|
|
3590
3807
|
}
|
|
3591
3808
|
relayout();
|
|
3592
3809
|
};
|
|
@@ -3607,6 +3824,12 @@ function createSceneFromDoc(doc, opts = {}) {
|
|
|
3607
3824
|
setState,
|
|
3608
3825
|
patch: applyPatch,
|
|
3609
3826
|
doc: () => doc,
|
|
3827
|
+
palette: () => registry.palette(),
|
|
3828
|
+
resetProps() {
|
|
3829
|
+
for (const runtime of runtimes.values())
|
|
3830
|
+
runtime.appliedProps = 'stale';
|
|
3831
|
+
relayout();
|
|
3832
|
+
},
|
|
3610
3833
|
destroy() {
|
|
3611
3834
|
for (const runtime of runtimes.values())
|
|
3612
3835
|
runtime.instance.destroy?.();
|
|
@@ -3616,15 +3839,1130 @@ function createSceneFromDoc(doc, opts = {}) {
|
|
|
3616
3839
|
};
|
|
3617
3840
|
}
|
|
3618
3841
|
|
|
3842
|
+
// Inner-hollow calibrator gizmo: drag the four edges of a frame's declared hollow
|
|
3843
|
+
// (props.inner fractions) directly on the canvas. Every drag is a set-props ScenePatch,
|
|
3844
|
+
// so the grid bound with frame-fraction use:'inner' re-fits LIVE while you drag — this
|
|
3845
|
+
// replaces the hand-measured FRAME_INNER/FRAME_CUT constants every studied game carries.
|
|
3846
|
+
const MIN_FRAC = 0.05;
|
|
3847
|
+
const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
|
|
3848
|
+
/** Pure edge-drag math: `frac` is the pointer position normalized to the frame rect axis. */
|
|
3849
|
+
function dragInnerEdge(inner, edge, frac) {
|
|
3850
|
+
const right = inner.left + inner.width;
|
|
3851
|
+
const bottom = inner.top + inner.height;
|
|
3852
|
+
switch (edge) {
|
|
3853
|
+
case 'left': {
|
|
3854
|
+
const left = clamp(frac, 0, right - MIN_FRAC);
|
|
3855
|
+
return { ...inner, left, width: right - left };
|
|
3856
|
+
}
|
|
3857
|
+
case 'right': {
|
|
3858
|
+
const r = clamp(frac, inner.left + MIN_FRAC, 1);
|
|
3859
|
+
return { ...inner, width: r - inner.left };
|
|
3860
|
+
}
|
|
3861
|
+
case 'top': {
|
|
3862
|
+
const top = clamp(frac, 0, bottom - MIN_FRAC);
|
|
3863
|
+
return { ...inner, top, height: bottom - top };
|
|
3864
|
+
}
|
|
3865
|
+
case 'bottom': {
|
|
3866
|
+
const b = clamp(frac, inner.top + MIN_FRAC, 1);
|
|
3867
|
+
return { ...inner, height: b - inner.top };
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
const HANDLE = 14;
|
|
3872
|
+
function createInnerCalibrator(opts) {
|
|
3873
|
+
const { handle, nodeId } = opts;
|
|
3874
|
+
const applyPatch = opts.applyPatch ?? handle.patch;
|
|
3875
|
+
const view = new pixi_js.Container();
|
|
3876
|
+
const g = new pixi_js.Graphics();
|
|
3877
|
+
view.addChild(g);
|
|
3878
|
+
const handles = new Map();
|
|
3879
|
+
for (const edge of ['left', 'right', 'top', 'bottom']) {
|
|
3880
|
+
const h = new pixi_js.Graphics();
|
|
3881
|
+
h.eventMode = 'static';
|
|
3882
|
+
h.cursor = edge === 'left' || edge === 'right' ? 'ew-resize' : 'ns-resize';
|
|
3883
|
+
h.on('pointerdown', (e) => {
|
|
3884
|
+
e.stopPropagation();
|
|
3885
|
+
dragging = edge;
|
|
3886
|
+
// Capture moves everywhere only WHILE dragging — otherwise stay click-through.
|
|
3887
|
+
view.hitArea = new pixi_js.Rectangle(-1e6, -1e6, 2e6, 2e6);
|
|
3888
|
+
});
|
|
3889
|
+
handles.set(edge, h);
|
|
3890
|
+
view.addChild(h);
|
|
3891
|
+
}
|
|
3892
|
+
let dragging = null;
|
|
3893
|
+
const docInner = () => {
|
|
3894
|
+
const walk = (n) => {
|
|
3895
|
+
if (n.id === nodeId)
|
|
3896
|
+
return n;
|
|
3897
|
+
for (const c of n.children ?? []) {
|
|
3898
|
+
const hit = walk(c);
|
|
3899
|
+
if (hit)
|
|
3900
|
+
return hit;
|
|
3901
|
+
}
|
|
3902
|
+
return undefined;
|
|
3903
|
+
};
|
|
3904
|
+
const inner = walk(handle.doc().root)?.props?.inner;
|
|
3905
|
+
return inner && typeof inner.left === 'number' ? inner : undefined;
|
|
3906
|
+
};
|
|
3907
|
+
const frameBounds = () => {
|
|
3908
|
+
const target = handle.node(nodeId);
|
|
3909
|
+
if (!target)
|
|
3910
|
+
return undefined;
|
|
3911
|
+
const b = target.getBounds();
|
|
3912
|
+
return new pixi_js.Rectangle(b.x, b.y, b.width, b.height);
|
|
3913
|
+
};
|
|
3914
|
+
const refresh = () => {
|
|
3915
|
+
g.clear();
|
|
3916
|
+
const bounds = frameBounds();
|
|
3917
|
+
const inner = docInner();
|
|
3918
|
+
if (!bounds || !inner) {
|
|
3919
|
+
for (const h of handles.values())
|
|
3920
|
+
h.clear();
|
|
3921
|
+
return;
|
|
3922
|
+
}
|
|
3923
|
+
const rx = bounds.x + inner.left * bounds.width;
|
|
3924
|
+
const ry = bounds.y + inner.top * bounds.height;
|
|
3925
|
+
const rw = inner.width * bounds.width;
|
|
3926
|
+
const rh = inner.height * bounds.height;
|
|
3927
|
+
g.rect(bounds.x, bounds.y, bounds.width, bounds.height).stroke({ color: 0xffa02f, width: 1, alpha: 0.5 });
|
|
3928
|
+
g.rect(rx, ry, rw, rh).fill({ color: 0x4dd0ff, alpha: 0.08 });
|
|
3929
|
+
g.rect(rx, ry, rw, rh).stroke({ color: 0x4dd0ff, width: 2, alpha: 0.9 });
|
|
3930
|
+
const mid = {
|
|
3931
|
+
left: [rx, ry + rh / 2],
|
|
3932
|
+
right: [rx + rw, ry + rh / 2],
|
|
3933
|
+
top: [rx + rw / 2, ry],
|
|
3934
|
+
bottom: [rx + rw / 2, ry + rh],
|
|
3935
|
+
};
|
|
3936
|
+
for (const [edge, h] of handles) {
|
|
3937
|
+
const [cx, cy] = mid[edge];
|
|
3938
|
+
h.clear();
|
|
3939
|
+
h.rect(cx - HANDLE / 2, cy - HANDLE / 2, HANDLE, HANDLE).fill({ color: 0x4dd0ff });
|
|
3940
|
+
h.rect(cx - HANDLE / 2, cy - HANDLE / 2, HANDLE, HANDLE).stroke({ color: 0x06283d, width: 2 });
|
|
3941
|
+
h.hitArea = new pixi_js.Rectangle(cx - HANDLE, cy - HANDLE, HANDLE * 2, HANDLE * 2);
|
|
3942
|
+
}
|
|
3943
|
+
};
|
|
3944
|
+
view.eventMode = 'static';
|
|
3945
|
+
view.on('pointermove', (e) => {
|
|
3946
|
+
if (!dragging)
|
|
3947
|
+
return;
|
|
3948
|
+
const bounds = frameBounds();
|
|
3949
|
+
const inner = docInner();
|
|
3950
|
+
if (!bounds || !inner)
|
|
3951
|
+
return;
|
|
3952
|
+
const frac = dragging === 'left' || dragging === 'right'
|
|
3953
|
+
? (e.global.x - bounds.x) / bounds.width
|
|
3954
|
+
: (e.global.y - bounds.y) / bounds.height;
|
|
3955
|
+
const next = dragInnerEdge(inner, dragging, Math.round(frac * 1000) / 1000);
|
|
3956
|
+
applyPatch({ op: 'set-props', id: nodeId, props: { inner: next } });
|
|
3957
|
+
opts.onChange?.(next);
|
|
3958
|
+
refresh();
|
|
3959
|
+
});
|
|
3960
|
+
const endDrag = () => {
|
|
3961
|
+
dragging = null;
|
|
3962
|
+
view.hitArea = null;
|
|
3963
|
+
};
|
|
3964
|
+
view.on('pointerup', endDrag);
|
|
3965
|
+
view.on('pointerupoutside', endDrag);
|
|
3966
|
+
refresh();
|
|
3967
|
+
return {
|
|
3968
|
+
view,
|
|
3969
|
+
refresh,
|
|
3970
|
+
destroy() {
|
|
3971
|
+
view.destroy({ children: true });
|
|
3972
|
+
},
|
|
3973
|
+
};
|
|
3974
|
+
}
|
|
3975
|
+
|
|
3976
|
+
// Pure transform math for the canvas gizmo: turn a drag into a new LayoutRule, writing
|
|
3977
|
+
// the RIGHT field for the rule's kind — the thing the user asked for ("resize on the
|
|
3978
|
+
// canvas, not just the sidebar"). Resize maps to a rule's natural size lever:
|
|
3979
|
+
// • widthFrac/heightFrac (viewport-fraction) or wFrac/hFrac (frame-fraction) — per-axis;
|
|
3980
|
+
// • otherwise a uniform `scale` (absolute / cover / grid-cell / pin / plain fraction).
|
|
3981
|
+
// The engine re-places the node by its own anchor after the lever changes, so the anchor
|
|
3982
|
+
// stays pinned for free — no position juggling here. Rotation writes `rotation`.
|
|
3983
|
+
/** Which size lever a rule exposes — decides how a resize drag is interpreted. */
|
|
3984
|
+
function sizeLever(rule) {
|
|
3985
|
+
if (rule.mode === 'viewport-fraction' && (rule.widthFrac !== undefined || rule.heightFrac !== undefined))
|
|
3986
|
+
return 'wh-frac';
|
|
3987
|
+
if (rule.mode === 'frame-fraction' && (rule.wFrac !== undefined || rule.hFrac !== undefined))
|
|
3988
|
+
return 'wh-box';
|
|
3989
|
+
if (rule.mode === 'frame-fraction')
|
|
3990
|
+
return 'none'; // point placement inside a frame — no size
|
|
3991
|
+
return 'scale';
|
|
3992
|
+
}
|
|
3993
|
+
const clampFactor = (f) => (Number.isFinite(f) && f > 0.01 ? f : 0.01);
|
|
3994
|
+
const round4 = (n) => Math.round(n * 1e4) / 1e4;
|
|
3995
|
+
/**
|
|
3996
|
+
* Multiply a rule's size lever by (factorX, factorY). Corners pass equal factors (uniform);
|
|
3997
|
+
* edges pass 1 on the untouched axis. Uniform levers (`scale`) use factorX.
|
|
3998
|
+
*/
|
|
3999
|
+
function resizeRule(rule, factorX, factorY) {
|
|
4000
|
+
const fx = clampFactor(factorX);
|
|
4001
|
+
const fy = clampFactor(factorY);
|
|
4002
|
+
const next = JSON.parse(JSON.stringify(rule));
|
|
4003
|
+
switch (sizeLever(rule)) {
|
|
4004
|
+
case 'wh-frac': {
|
|
4005
|
+
const r = next;
|
|
4006
|
+
if (r.widthFrac !== undefined)
|
|
4007
|
+
r.widthFrac = round4(r.widthFrac * fx);
|
|
4008
|
+
if (r.heightFrac !== undefined)
|
|
4009
|
+
r.heightFrac = round4(r.heightFrac * fy);
|
|
4010
|
+
break;
|
|
4011
|
+
}
|
|
4012
|
+
case 'wh-box': {
|
|
4013
|
+
const r = next;
|
|
4014
|
+
if (r.wFrac !== undefined)
|
|
4015
|
+
r.wFrac = round4(r.wFrac * fx);
|
|
4016
|
+
if (r.hFrac !== undefined)
|
|
4017
|
+
r.hFrac = round4(r.hFrac * fy);
|
|
4018
|
+
break;
|
|
4019
|
+
}
|
|
4020
|
+
case 'scale': {
|
|
4021
|
+
const r = next;
|
|
4022
|
+
r.scale = round4((r.scale ?? 1) * fx);
|
|
4023
|
+
break;
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
return next;
|
|
4027
|
+
}
|
|
4028
|
+
function setRuleRotation(rule, radians) {
|
|
4029
|
+
const next = JSON.parse(JSON.stringify(rule));
|
|
4030
|
+
next.rotation = round4(radians);
|
|
4031
|
+
return next;
|
|
4032
|
+
}
|
|
4033
|
+
/** Nudge a rule's final pixel offset by a screen delta already converted to design units. */
|
|
4034
|
+
function nudgeRule(rule, dxDesign, dyDesign) {
|
|
4035
|
+
const next = JSON.parse(JSON.stringify(rule));
|
|
4036
|
+
next.pxNudge = { x: Math.round((next.pxNudge?.x ?? 0) + dxDesign), y: Math.round((next.pxNudge?.y ?? 0) + dyDesign) };
|
|
4037
|
+
return next;
|
|
4038
|
+
}
|
|
4039
|
+
/** Screen position of each handle for a bounding box. */
|
|
4040
|
+
function handlePoint(box, id) {
|
|
4041
|
+
const { x, y, width: w, height: h } = box;
|
|
4042
|
+
const cx = x + w / 2;
|
|
4043
|
+
const cy = y + h / 2;
|
|
4044
|
+
switch (id) {
|
|
4045
|
+
case 'nw': return { x, y };
|
|
4046
|
+
case 'n': return { x: cx, y };
|
|
4047
|
+
case 'ne': return { x: x + w, y };
|
|
4048
|
+
case 'e': return { x: x + w, y: cy };
|
|
4049
|
+
case 'se': return { x: x + w, y: y + h };
|
|
4050
|
+
case 's': return { x: cx, y: y + h };
|
|
4051
|
+
case 'sw': return { x, y: y + h };
|
|
4052
|
+
case 'w': return { x, y: cy };
|
|
4053
|
+
case 'rotate': return { x: cx, y: y - 28 };
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
/**
|
|
4057
|
+
* Resize factors from dragging a handle, using the OPPOSITE edge/corner as the fixed
|
|
4058
|
+
* reference — the intuitive "grab a corner, drag out, it grows" feel.
|
|
4059
|
+
*/
|
|
4060
|
+
function resizeFactors(startBox, handle, pointer) {
|
|
4061
|
+
const left = startBox.x;
|
|
4062
|
+
const right = startBox.x + startBox.width;
|
|
4063
|
+
const top = startBox.y;
|
|
4064
|
+
const bottom = startBox.y + startBox.height;
|
|
4065
|
+
const w = startBox.width || 1;
|
|
4066
|
+
const h = startBox.height || 1;
|
|
4067
|
+
const westX = () => Math.abs(pointer.x - right) / w; // dragging the west side, east fixed
|
|
4068
|
+
const eastX = () => Math.abs(pointer.x - left) / w;
|
|
4069
|
+
const northY = () => Math.abs(pointer.y - bottom) / h;
|
|
4070
|
+
const southY = () => Math.abs(pointer.y - top) / h;
|
|
4071
|
+
switch (handle) {
|
|
4072
|
+
case 'w': return { factorX: westX(), factorY: 1 };
|
|
4073
|
+
case 'e': return { factorX: eastX(), factorY: 1 };
|
|
4074
|
+
case 'n': return { factorX: 1, factorY: northY() };
|
|
4075
|
+
case 's': return { factorX: 1, factorY: southY() };
|
|
4076
|
+
case 'nw': {
|
|
4077
|
+
const f = uniform(westX(), northY());
|
|
4078
|
+
return { factorX: f, factorY: f };
|
|
4079
|
+
}
|
|
4080
|
+
case 'ne': {
|
|
4081
|
+
const f = uniform(eastX(), northY());
|
|
4082
|
+
return { factorX: f, factorY: f };
|
|
4083
|
+
}
|
|
4084
|
+
case 'sw': {
|
|
4085
|
+
const f = uniform(westX(), southY());
|
|
4086
|
+
return { factorX: f, factorY: f };
|
|
4087
|
+
}
|
|
4088
|
+
case 'se': {
|
|
4089
|
+
const f = uniform(eastX(), southY());
|
|
4090
|
+
return { factorX: f, factorY: f };
|
|
4091
|
+
}
|
|
4092
|
+
default: return { factorX: 1, factorY: 1 };
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
/** Corner resize keeps aspect — use the diagonal ratio of the two per-axis factors. */
|
|
4096
|
+
function uniform(a, b) {
|
|
4097
|
+
return Math.sqrt(Math.max(0.0001, a) * Math.max(0.0001, b));
|
|
4098
|
+
}
|
|
4099
|
+
|
|
4100
|
+
// Transform gizmo — the on-canvas resize / rotate / move manipulator for the selected
|
|
4101
|
+
// node. Generalizes the frame calibrator: 8 resize handles + a rotate handle, and a body
|
|
4102
|
+
// drag to move. Every gesture emits ONE set-layout ScenePatch (orientation-aware), so a
|
|
4103
|
+
// mouse transform and an agent/inspector transform are the same operation. Live bounds are
|
|
4104
|
+
// re-read each frame, so the gizmo follows the node as the engine relayouts.
|
|
4105
|
+
const CORNERS = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
|
|
4106
|
+
const HS = 9; // handle size
|
|
4107
|
+
function createTransformGizmo(opts) {
|
|
4108
|
+
const { handle, app } = opts;
|
|
4109
|
+
const applyPatch = opts.applyPatch ?? handle.patch;
|
|
4110
|
+
const getOrientation = opts.getOrientation ?? (() => 'landscape');
|
|
4111
|
+
// pxNudge lives in the placement (parent/space) coordinate system, which maps to screen
|
|
4112
|
+
// by the parent's world scale — so screen delta ÷ parent-world-scale = design units.
|
|
4113
|
+
const dpp = (id) => {
|
|
4114
|
+
if (opts.designPerPixel)
|
|
4115
|
+
return opts.designPerPixel(id);
|
|
4116
|
+
const parent = handle.node(id)?.parent;
|
|
4117
|
+
const s = parent?.worldTransform?.a ?? 1;
|
|
4118
|
+
return s !== 0 ? 1 / s : 1;
|
|
4119
|
+
};
|
|
4120
|
+
const layer = new pixi_js.Container();
|
|
4121
|
+
layer.eventMode = 'static';
|
|
4122
|
+
app.stage.addChild(layer);
|
|
4123
|
+
const frame = new pixi_js.Graphics();
|
|
4124
|
+
layer.addChild(frame);
|
|
4125
|
+
const handleGfx = new Map();
|
|
4126
|
+
for (const id of [...CORNERS, 'rotate']) {
|
|
4127
|
+
const g = new pixi_js.Graphics();
|
|
4128
|
+
g.eventMode = 'static';
|
|
4129
|
+
g.cursor = id === 'rotate' ? 'grab' : cursorFor(id);
|
|
4130
|
+
g.on('pointerdown', (e) => onDown(id, e));
|
|
4131
|
+
handleGfx.set(id, g);
|
|
4132
|
+
layer.addChild(g);
|
|
4133
|
+
}
|
|
4134
|
+
// A transparent body catcher for move-drags (below the handles).
|
|
4135
|
+
const body = new pixi_js.Graphics();
|
|
4136
|
+
body.eventMode = 'static';
|
|
4137
|
+
body.cursor = 'move';
|
|
4138
|
+
body.on('pointerdown', (e) => onDown('body', e));
|
|
4139
|
+
layer.addChildAt(body, 1);
|
|
4140
|
+
let nodeId = null;
|
|
4141
|
+
let drag = null;
|
|
4142
|
+
const findDocNode = (id, node = handle.doc().root) => {
|
|
4143
|
+
if (node.id === id)
|
|
4144
|
+
return node;
|
|
4145
|
+
for (const child of node.children ?? []) {
|
|
4146
|
+
const hit = findDocNode(id, child);
|
|
4147
|
+
if (hit)
|
|
4148
|
+
return hit;
|
|
4149
|
+
}
|
|
4150
|
+
return undefined;
|
|
4151
|
+
};
|
|
4152
|
+
/** Effective layout rule for the current orientation (base or portrait/landscape override). */
|
|
4153
|
+
const effectiveRule = (id) => {
|
|
4154
|
+
const node = findDocNode(id);
|
|
4155
|
+
if (!node)
|
|
4156
|
+
return {};
|
|
4157
|
+
const o = getOrientation();
|
|
4158
|
+
const override = node.responsive?.[o]?.layout;
|
|
4159
|
+
return override ? { rule: override, orientation: o } : { rule: node.layout };
|
|
4160
|
+
};
|
|
4161
|
+
const boxOf = (id) => {
|
|
4162
|
+
const view = handle.node(id);
|
|
4163
|
+
if (!view || !view.visible)
|
|
4164
|
+
return null;
|
|
4165
|
+
const b = view.getBounds();
|
|
4166
|
+
return { x: b.x, y: b.y, width: b.width, height: b.height };
|
|
4167
|
+
};
|
|
4168
|
+
const onDown = (h, e) => {
|
|
4169
|
+
e.stopPropagation();
|
|
4170
|
+
if (!nodeId)
|
|
4171
|
+
return;
|
|
4172
|
+
const box = boxOf(nodeId);
|
|
4173
|
+
const { rule, orientation } = effectiveRule(nodeId);
|
|
4174
|
+
if (!box || !rule)
|
|
4175
|
+
return;
|
|
4176
|
+
const center = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
4177
|
+
drag = {
|
|
4178
|
+
handle: h,
|
|
4179
|
+
startBox: box,
|
|
4180
|
+
baseRule: JSON.parse(JSON.stringify(rule)),
|
|
4181
|
+
orientation,
|
|
4182
|
+
start: { x: e.global.x, y: e.global.y },
|
|
4183
|
+
center,
|
|
4184
|
+
startAngle: Math.atan2(e.global.y - center.y, e.global.x - center.x),
|
|
4185
|
+
};
|
|
4186
|
+
};
|
|
4187
|
+
const onMove = (e) => {
|
|
4188
|
+
if (!drag || !nodeId)
|
|
4189
|
+
return;
|
|
4190
|
+
let rule;
|
|
4191
|
+
if (drag.handle === 'body') {
|
|
4192
|
+
const s = dpp(nodeId);
|
|
4193
|
+
rule = nudgeRule(drag.baseRule, (e.global.x - drag.start.x) * s, (e.global.y - drag.start.y) * s);
|
|
4194
|
+
}
|
|
4195
|
+
else if (drag.handle === 'rotate') {
|
|
4196
|
+
const angle = Math.atan2(e.global.y - drag.center.y, e.global.x - drag.center.x);
|
|
4197
|
+
rule = setRuleRotation(drag.baseRule, (drag.baseRule.rotation ?? 0) + (angle - drag.startAngle));
|
|
4198
|
+
}
|
|
4199
|
+
else {
|
|
4200
|
+
if (sizeLever(drag.baseRule) === 'none')
|
|
4201
|
+
return; // point-placed frame-fraction: nothing to size
|
|
4202
|
+
const { factorX, factorY } = resizeFactors(drag.startBox, drag.handle, { x: e.global.x, y: e.global.y });
|
|
4203
|
+
rule = resizeRule(drag.baseRule, factorX, factorY);
|
|
4204
|
+
}
|
|
4205
|
+
applyPatch({ op: 'set-layout', id: nodeId, layout: rule, orientation: drag.orientation });
|
|
4206
|
+
};
|
|
4207
|
+
const onUp = () => {
|
|
4208
|
+
drag = null;
|
|
4209
|
+
};
|
|
4210
|
+
app.stage.on('pointermove', onMove);
|
|
4211
|
+
app.stage.on('pointerup', onUp);
|
|
4212
|
+
app.stage.on('pointerupoutside', onUp);
|
|
4213
|
+
const redraw = () => {
|
|
4214
|
+
const box = nodeId ? boxOf(nodeId) : null;
|
|
4215
|
+
frame.clear();
|
|
4216
|
+
body.clear();
|
|
4217
|
+
if (!box) {
|
|
4218
|
+
for (const g of handleGfx.values())
|
|
4219
|
+
g.clear();
|
|
4220
|
+
body.hitArea = null;
|
|
4221
|
+
return;
|
|
4222
|
+
}
|
|
4223
|
+
frame.rect(box.x, box.y, box.width, box.height).stroke({ color: 0x4dd0ff, width: 1.5, alpha: 0.9 });
|
|
4224
|
+
// rotate stem
|
|
4225
|
+
const rot = handlePoint(box, 'rotate');
|
|
4226
|
+
frame.moveTo(box.x + box.width / 2, box.y).lineTo(rot.x, rot.y).stroke({ color: 0x4dd0ff, width: 1, alpha: 0.6 });
|
|
4227
|
+
body.rect(box.x, box.y, box.width, box.height).fill({ color: 0xffffff, alpha: 0.001 });
|
|
4228
|
+
body.hitArea = new pixi_js.Rectangle(box.x, box.y, box.width, box.height);
|
|
4229
|
+
const sizable = sizeLever(nodeId ? effectiveRule(nodeId).rule ?? drag?.baseRule ?? {} : {}) !== 'none';
|
|
4230
|
+
for (const id of [...CORNERS, 'rotate']) {
|
|
4231
|
+
const g = handleGfx.get(id);
|
|
4232
|
+
g.clear();
|
|
4233
|
+
if (id !== 'rotate' && !sizable)
|
|
4234
|
+
continue; // hide resize handles when the rule can't size
|
|
4235
|
+
const p = handlePoint(box, id);
|
|
4236
|
+
if (id === 'rotate')
|
|
4237
|
+
g.circle(p.x, p.y, HS / 1.6).fill({ color: 0x4dd0ff }).stroke({ color: 0x06283d, width: 2 });
|
|
4238
|
+
else
|
|
4239
|
+
g.rect(p.x - HS / 2, p.y - HS / 2, HS, HS).fill({ color: 0x4dd0ff }).stroke({ color: 0x06283d, width: 2 });
|
|
4240
|
+
g.hitArea = new pixi_js.Rectangle(p.x - HS, p.y - HS, HS * 2, HS * 2);
|
|
4241
|
+
}
|
|
4242
|
+
};
|
|
4243
|
+
app.ticker.add(redraw);
|
|
4244
|
+
const fakeEvent = (x, y) => ({ global: { x, y }, stopPropagation() { } });
|
|
4245
|
+
return {
|
|
4246
|
+
attach(id) {
|
|
4247
|
+
nodeId = id;
|
|
4248
|
+
redraw();
|
|
4249
|
+
},
|
|
4250
|
+
refresh: redraw,
|
|
4251
|
+
drag(h, from, to) {
|
|
4252
|
+
onDown(h, fakeEvent(from.x, from.y));
|
|
4253
|
+
onMove(fakeEvent(to.x, to.y));
|
|
4254
|
+
onUp();
|
|
4255
|
+
},
|
|
4256
|
+
destroy() {
|
|
4257
|
+
app.ticker.remove(redraw);
|
|
4258
|
+
app.stage.off('pointermove', onMove);
|
|
4259
|
+
app.stage.off('pointerup', onUp);
|
|
4260
|
+
app.stage.off('pointerupoutside', onUp);
|
|
4261
|
+
layer.destroy({ children: true });
|
|
4262
|
+
},
|
|
4263
|
+
};
|
|
4264
|
+
}
|
|
4265
|
+
function cursorFor(id) {
|
|
4266
|
+
switch (id) {
|
|
4267
|
+
case 'nw':
|
|
4268
|
+
case 'se': return 'nwse-resize';
|
|
4269
|
+
case 'ne':
|
|
4270
|
+
case 'sw': return 'nesw-resize';
|
|
4271
|
+
case 'n':
|
|
4272
|
+
case 's': return 'ns-resize';
|
|
4273
|
+
case 'e':
|
|
4274
|
+
case 'w': return 'ew-resize';
|
|
4275
|
+
default: return 'default';
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
|
|
4279
|
+
/**
|
|
4280
|
+
* Base class for all scenes.
|
|
4281
|
+
* Provides a root PixiJS Container and lifecycle hooks.
|
|
4282
|
+
*
|
|
4283
|
+
* @example
|
|
4284
|
+
* ```ts
|
|
4285
|
+
* class MenuScene extends Scene {
|
|
4286
|
+
* async onEnter() {
|
|
4287
|
+
* const bg = Sprite.from('menu-bg');
|
|
4288
|
+
* this.container.addChild(bg);
|
|
4289
|
+
* }
|
|
4290
|
+
*
|
|
4291
|
+
* onUpdate(dt: number) {
|
|
4292
|
+
* // per-frame logic
|
|
4293
|
+
* }
|
|
4294
|
+
*
|
|
4295
|
+
* onResize(width: number, height: number) {
|
|
4296
|
+
* // reposition UI
|
|
4297
|
+
* }
|
|
4298
|
+
* }
|
|
4299
|
+
* ```
|
|
4300
|
+
*/
|
|
4301
|
+
class Scene {
|
|
4302
|
+
container;
|
|
4303
|
+
constructor() {
|
|
4304
|
+
this.container = new pixi_js.Container();
|
|
4305
|
+
this.container.label = this.constructor.name;
|
|
4306
|
+
}
|
|
4307
|
+
}
|
|
4308
|
+
|
|
4309
|
+
// Flow step registry — the same contribution shape as scene node types (§6.2): kind +
|
|
4310
|
+
// runtime executor + schema/agentDoc for tooling. Core steps are built-ins registered
|
|
4311
|
+
// through it; games and plugins add their own `do` kinds without touching the runner.
|
|
4312
|
+
function createFlowStepRegistry(plugins = [], builtins = []) {
|
|
4313
|
+
const steps = new Map();
|
|
4314
|
+
for (const contribution of builtins)
|
|
4315
|
+
steps.set(contribution.kind, contribution);
|
|
4316
|
+
for (const plugin of plugins) {
|
|
4317
|
+
for (const contribution of plugin.steps ?? [])
|
|
4318
|
+
steps.set(contribution.kind, contribution);
|
|
4319
|
+
}
|
|
4320
|
+
return {
|
|
4321
|
+
step: (kind) => steps.get(kind),
|
|
4322
|
+
kinds: () => [...steps.keys()],
|
|
4323
|
+
};
|
|
4324
|
+
}
|
|
4325
|
+
|
|
4326
|
+
// Built-in flow steps. Presentation writes (tween/setProps/countUp) target live
|
|
4327
|
+
// instances/views — never the scene doc (the doc stays the persistable SSOT); runtime
|
|
4328
|
+
// state changes (setVar/setState) go through the scene handle like any agent patch.
|
|
4329
|
+
const num = (v, fallback) => (typeof v === 'number' && Number.isFinite(v) ? v : fallback);
|
|
4330
|
+
function formatValue(value, format) {
|
|
4331
|
+
const rounded = Math.round(value);
|
|
4332
|
+
if (format === 'space')
|
|
4333
|
+
return String(rounded).replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
|
4334
|
+
return String(rounded);
|
|
4335
|
+
}
|
|
4336
|
+
const tweenStep = {
|
|
4337
|
+
kind: 'tween',
|
|
4338
|
+
agentDoc: 'Animate numeric view properties of a scene node (alpha, x, y, rotation; scale animates both axes). Transient — the doc is untouched, and any relayout (setVar/setState/resize) re-asserts layout-owned props (x/y/scale/rotation) from the doc. Persist a move via a set-layout patch, not a tween. Turbo divides ms; skip jumps to the end values.',
|
|
4339
|
+
async run(raw, rt) {
|
|
4340
|
+
const step = raw;
|
|
4341
|
+
const view = rt.view(step.node);
|
|
4342
|
+
if (!view)
|
|
4343
|
+
return rt.log(`tween: unknown node "${step.node}"`);
|
|
4344
|
+
const to = {};
|
|
4345
|
+
for (const [key, value] of Object.entries(step.to))
|
|
4346
|
+
to[key] = num(rt.resolve(value), 0);
|
|
4347
|
+
const applyFinal = () => {
|
|
4348
|
+
for (const [key, value] of Object.entries(to)) {
|
|
4349
|
+
if (key === 'scale')
|
|
4350
|
+
view.scale.set(value);
|
|
4351
|
+
else
|
|
4352
|
+
view[key] = value;
|
|
4353
|
+
}
|
|
4354
|
+
};
|
|
4355
|
+
if (rt.skipped || rt.instant)
|
|
4356
|
+
return applyFinal();
|
|
4357
|
+
const { scale, ...rest } = to;
|
|
4358
|
+
const jobs = [];
|
|
4359
|
+
if (Object.keys(rest).length > 0) {
|
|
4360
|
+
jobs.push(Tween.to(view, rest, step.ms / rt.turbo, easingByName(step.ease)));
|
|
4361
|
+
}
|
|
4362
|
+
if (scale !== undefined) {
|
|
4363
|
+
jobs.push(Tween.to(view.scale, { x: scale, y: scale }, step.ms / rt.turbo, easingByName(step.ease)));
|
|
4364
|
+
}
|
|
4365
|
+
// A skip during flight must land on the end state: race the tween against skip-release.
|
|
4366
|
+
await Promise.race([Promise.all(jobs), rt.wait(step.ms + 50)]);
|
|
4367
|
+
if (rt.skipped) {
|
|
4368
|
+
Tween.killTweensOf(view);
|
|
4369
|
+
Tween.killTweensOf(view.scale);
|
|
4370
|
+
applyFinal();
|
|
4371
|
+
}
|
|
4372
|
+
},
|
|
4373
|
+
};
|
|
4374
|
+
const soundStep = {
|
|
4375
|
+
kind: 'sound',
|
|
4376
|
+
agentDoc: 'Play/stop a cue from flow.cues (rotation/jitter handled by the runner). Never reference audio files directly.',
|
|
4377
|
+
run(raw, rt) {
|
|
4378
|
+
const step = raw;
|
|
4379
|
+
rt.playCue(step.cue, step.action ?? 'play');
|
|
4380
|
+
},
|
|
4381
|
+
};
|
|
4382
|
+
const setStateStep = {
|
|
4383
|
+
kind: 'setState',
|
|
4384
|
+
agentDoc: "Switch a node's named state (frame anticipation/bonus look). Same operation the inspector and agent use.",
|
|
4385
|
+
run(raw, rt) {
|
|
4386
|
+
const step = raw;
|
|
4387
|
+
rt.scene.setState(step.node, step.state);
|
|
4388
|
+
},
|
|
4389
|
+
};
|
|
4390
|
+
const setVarStep = {
|
|
4391
|
+
kind: 'setVar',
|
|
4392
|
+
agentDoc: "Set a runtime var driving visibleWhen (e.g. mode). Use for mode transitions ('setVar mode free_spins').",
|
|
4393
|
+
run(raw, rt) {
|
|
4394
|
+
const step = raw;
|
|
4395
|
+
rt.scene.setVar(step.name, rt.resolve(step.value));
|
|
4396
|
+
},
|
|
4397
|
+
};
|
|
4398
|
+
const setPropsStep = {
|
|
4399
|
+
kind: 'setProps',
|
|
4400
|
+
agentDoc: 'Transient prop write on the live instance (badge values, board swaps during presentation). The scene doc is NOT modified.',
|
|
4401
|
+
run(raw, rt) {
|
|
4402
|
+
const step = raw;
|
|
4403
|
+
const instance = rt.scene.instance(step.node);
|
|
4404
|
+
if (!instance?.applyProps)
|
|
4405
|
+
return rt.log(`setProps: node "${step.node}" has no applyProps`);
|
|
4406
|
+
const props = {};
|
|
4407
|
+
for (const [key, value] of Object.entries(step.props))
|
|
4408
|
+
props[key] = rt.resolve(value);
|
|
4409
|
+
const node = findDocNode(rt, step.node);
|
|
4410
|
+
instance.applyProps({ ...(node?.props ?? {}), ...props });
|
|
4411
|
+
},
|
|
4412
|
+
};
|
|
4413
|
+
function findDocNode(rt, id) {
|
|
4414
|
+
const walk = (n) => {
|
|
4415
|
+
if (n.id === id)
|
|
4416
|
+
return n;
|
|
4417
|
+
for (const child of n.children ?? []) {
|
|
4418
|
+
const hit = walk(child);
|
|
4419
|
+
if (hit)
|
|
4420
|
+
return hit;
|
|
4421
|
+
}
|
|
4422
|
+
return undefined;
|
|
4423
|
+
};
|
|
4424
|
+
return walk(rt.scene.doc().root);
|
|
4425
|
+
}
|
|
4426
|
+
const countUpStep = {
|
|
4427
|
+
kind: 'countUp',
|
|
4428
|
+
agentDoc: "Animated number roll on an instance prop (default 'value' — badge prefabs). `to` may be '$win' (ctx ref). Turbo shortens, skip jumps to the final value.",
|
|
4429
|
+
async run(raw, rt) {
|
|
4430
|
+
const step = raw;
|
|
4431
|
+
const instance = rt.scene.instance(step.node);
|
|
4432
|
+
if (!instance?.applyProps)
|
|
4433
|
+
return rt.log(`countUp: node "${step.node}" has no applyProps`);
|
|
4434
|
+
const node = findDocNode(rt, step.node);
|
|
4435
|
+
const prop = step.prop ?? 'value';
|
|
4436
|
+
const from = num(rt.resolve(step.from), 0);
|
|
4437
|
+
const to = num(rt.resolve(step.to), 0);
|
|
4438
|
+
const ms = num(step.ms, 1000) / rt.turbo;
|
|
4439
|
+
const apply = (value) => instance.applyProps({ ...(node?.props ?? {}), [prop]: formatValue(value, step.format) });
|
|
4440
|
+
if (rt.skipped || rt.instant || ms <= 0)
|
|
4441
|
+
return apply(to);
|
|
4442
|
+
const start = performance.now();
|
|
4443
|
+
while (!rt.skipped) {
|
|
4444
|
+
const k = Math.min(1, (performance.now() - start) / ms);
|
|
4445
|
+
apply(from + (to - from) * (1 - Math.pow(1 - k, 2)));
|
|
4446
|
+
if (k >= 1)
|
|
4447
|
+
return;
|
|
4448
|
+
await rt.wait(16);
|
|
4449
|
+
}
|
|
4450
|
+
apply(to);
|
|
4451
|
+
},
|
|
4452
|
+
};
|
|
4453
|
+
const waitStep = {
|
|
4454
|
+
kind: 'wait',
|
|
4455
|
+
agentDoc: "Pause: {ms} (turbo-scaled) or {until:'tap'} (released by tap or skip). Prefer explicit waits over baking delays into tweens.",
|
|
4456
|
+
async run(raw, rt) {
|
|
4457
|
+
const step = raw;
|
|
4458
|
+
if (step.until === 'tap')
|
|
4459
|
+
return rt.waitTap();
|
|
4460
|
+
return rt.wait(num(step.ms, 0));
|
|
4461
|
+
},
|
|
4462
|
+
};
|
|
4463
|
+
const ifStep = {
|
|
4464
|
+
kind: 'if',
|
|
4465
|
+
agentDoc: "Branch on the fire() ctx: {when:'win >= 100', then:[…], else:[…]}. Bare name = truthy check.",
|
|
4466
|
+
async run(raw, rt) {
|
|
4467
|
+
const step = raw;
|
|
4468
|
+
await rt.run(rt.when(step.when) ? step.then : (step.else ?? []));
|
|
4469
|
+
},
|
|
4470
|
+
};
|
|
4471
|
+
const parallelStep = {
|
|
4472
|
+
kind: 'parallel',
|
|
4473
|
+
agentDoc: 'Run tracks concurrently and await them all: {steps: [[…], […]]}. Each track is an independent sequence.',
|
|
4474
|
+
async run(raw, rt) {
|
|
4475
|
+
const step = raw;
|
|
4476
|
+
await Promise.all(step.steps.map((track) => rt.run(track)));
|
|
4477
|
+
},
|
|
4478
|
+
};
|
|
4479
|
+
const seqStep = {
|
|
4480
|
+
kind: 'seq',
|
|
4481
|
+
agentDoc: 'Nested sequence (grouping inside parallel tracks).',
|
|
4482
|
+
async run(raw, rt) {
|
|
4483
|
+
const step = raw;
|
|
4484
|
+
await rt.run(step.steps);
|
|
4485
|
+
},
|
|
4486
|
+
};
|
|
4487
|
+
const forEachStep = {
|
|
4488
|
+
kind: 'forEach',
|
|
4489
|
+
agentDoc: "Fan out over a ctx collection: {items:'$jars', as:'jar', mode:'parallel', staggerMs:90, steps:[…]}. Nested steps read `$jar` / `$jarIndex`. THE primitive for staggered jar flights, per-cell transmutes, cascade histories — validated on 3 games.",
|
|
4490
|
+
async run(raw, rt) {
|
|
4491
|
+
const step = raw;
|
|
4492
|
+
const items = rt.resolve(step.items);
|
|
4493
|
+
if (!Array.isArray(items))
|
|
4494
|
+
return rt.log(`forEach: items did not resolve to an array (${String(step.items)})`);
|
|
4495
|
+
const as = step.as ?? 'item';
|
|
4496
|
+
const stagger = num(step.staggerMs, 0);
|
|
4497
|
+
const runtimes = items.map((item, index) => rt.child({ [as]: item, [`${as}Index`]: index }));
|
|
4498
|
+
if ((step.mode ?? 'sequential') === 'parallel') {
|
|
4499
|
+
await Promise.all(runtimes.map(async (child, index) => {
|
|
4500
|
+
if (stagger > 0 && index > 0)
|
|
4501
|
+
await rt.wait(stagger * index);
|
|
4502
|
+
await child.run(step.steps);
|
|
4503
|
+
}));
|
|
4504
|
+
}
|
|
4505
|
+
else {
|
|
4506
|
+
for (let index = 0; index < runtimes.length; index++) {
|
|
4507
|
+
if (stagger > 0 && index > 0)
|
|
4508
|
+
await rt.wait(stagger);
|
|
4509
|
+
await runtimes[index].run(step.steps);
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
},
|
|
4513
|
+
};
|
|
4514
|
+
const codeStep = {
|
|
4515
|
+
kind: 'code',
|
|
4516
|
+
agentDoc: 'Escape hatch: named choreography registered in createFlowRunner({code}). Use when the step vocabulary genuinely cannot express it — then consider a plugin step.',
|
|
4517
|
+
async run(raw, rt) {
|
|
4518
|
+
const step = raw;
|
|
4519
|
+
const handler = rt.codeRef(step.ref);
|
|
4520
|
+
if (!handler)
|
|
4521
|
+
return rt.log(`code: unknown ref "${step.ref}"`);
|
|
4522
|
+
await handler(rt, step.args ?? {});
|
|
4523
|
+
},
|
|
4524
|
+
};
|
|
4525
|
+
const BUILTIN_FLOW_STEPS = [
|
|
4526
|
+
tweenStep,
|
|
4527
|
+
soundStep,
|
|
4528
|
+
setStateStep,
|
|
4529
|
+
setVarStep,
|
|
4530
|
+
setPropsStep,
|
|
4531
|
+
countUpStep,
|
|
4532
|
+
waitStep,
|
|
4533
|
+
ifStep,
|
|
4534
|
+
parallelStep,
|
|
4535
|
+
seqStep,
|
|
4536
|
+
forEachStep,
|
|
4537
|
+
codeStep,
|
|
4538
|
+
];
|
|
4539
|
+
|
|
4540
|
+
// Flow-IR interpreter: createFlowRunner(flowDoc, { scene, … }).fire(event, ctx) executes
|
|
4541
|
+
// the event's steps and returns a Trace.
|
|
4542
|
+
//
|
|
4543
|
+
// Skip semantics (first-class, docs/slot-ide.md §1.7): skip() does NOT cancel a run — it
|
|
4544
|
+
// makes the remaining steps complete in zero time. Waits resolve instantly, tweens and
|
|
4545
|
+
// count-ups jump to their end values, taps release. The flow always reaches its settled
|
|
4546
|
+
// state; three of the studied games lacked exactly this. Turbo divides every duration.
|
|
4547
|
+
const noopAudio = { play: () => { }, stop: () => { } };
|
|
4548
|
+
function validateFlowDoc(doc, registry) {
|
|
4549
|
+
const errors = [];
|
|
4550
|
+
if (doc.version !== 1)
|
|
4551
|
+
errors.push(`unsupported flow doc version ${String(doc.version)}`);
|
|
4552
|
+
const visit = (step, path) => {
|
|
4553
|
+
if (!registry.step(step.do)) {
|
|
4554
|
+
errors.push(`${path}: unknown step "${step.do}" — no plugin contributes it. Registered: ${registry.kinds().join(', ')}`);
|
|
4555
|
+
return;
|
|
4556
|
+
}
|
|
4557
|
+
if (step.do === 'if') {
|
|
4558
|
+
step.then.forEach((s, i) => visit(s, `${path}.then[${i}]`));
|
|
4559
|
+
(step.else ?? []).forEach((s, i) => visit(s, `${path}.else[${i}]`));
|
|
4560
|
+
}
|
|
4561
|
+
else if (step.do === 'parallel') {
|
|
4562
|
+
step.steps.forEach((track, ti) => track.forEach((s, i) => visit(s, `${path}[${ti}][${i}]`)));
|
|
4563
|
+
}
|
|
4564
|
+
else if (step.do === 'seq' || step.do === 'forEach') {
|
|
4565
|
+
step.steps.forEach((s, i) => visit(s, `${path}.steps[${i}]`));
|
|
4566
|
+
}
|
|
4567
|
+
else if (step.do === 'sound') {
|
|
4568
|
+
const cue = step.cue;
|
|
4569
|
+
if (!doc.cues?.[cue])
|
|
4570
|
+
errors.push(`${path}: sound cue "${cue}" is not declared in flow.cues`);
|
|
4571
|
+
}
|
|
4572
|
+
};
|
|
4573
|
+
for (const [event, steps] of Object.entries(doc.on ?? {})) {
|
|
4574
|
+
steps.forEach((step, i) => visit(step, `on.${event}[${i}]`));
|
|
4575
|
+
}
|
|
4576
|
+
return errors;
|
|
4577
|
+
}
|
|
4578
|
+
function createFlowRunner(doc, opts) {
|
|
4579
|
+
const log = opts.log ?? ((msg) => console.warn(`[flow] ${msg}`));
|
|
4580
|
+
const registry = createFlowStepRegistry(opts.plugins ?? [], BUILTIN_FLOW_STEPS);
|
|
4581
|
+
const errors = validateFlowDoc(doc, registry);
|
|
4582
|
+
if (errors.length > 0)
|
|
4583
|
+
throw new Error(`flow doc "${doc.id}" is invalid:\n ${errors.join('\n ')}`);
|
|
4584
|
+
const audio = opts.audio ?? noopAudio;
|
|
4585
|
+
let turbo = 1;
|
|
4586
|
+
const rotation = new Map();
|
|
4587
|
+
const tapWaiters = new Set();
|
|
4588
|
+
const active = new Set();
|
|
4589
|
+
const lastCtx = new Map();
|
|
4590
|
+
const resolveCue = (name) => {
|
|
4591
|
+
const def = doc.cues?.[name];
|
|
4592
|
+
if (!def)
|
|
4593
|
+
return undefined;
|
|
4594
|
+
const sources = Array.isArray(def.src) ? def.src : [def.src];
|
|
4595
|
+
let index = 0;
|
|
4596
|
+
if (def.rotate && sources.length > 1) {
|
|
4597
|
+
index = (rotation.get(name) ?? 0) % sources.length;
|
|
4598
|
+
rotation.set(name, index + 1);
|
|
4599
|
+
}
|
|
4600
|
+
const jitter = def.jitter ?? 0;
|
|
4601
|
+
return {
|
|
4602
|
+
cue: name,
|
|
4603
|
+
src: sources[index],
|
|
4604
|
+
channel: def.channel ?? 'sfx',
|
|
4605
|
+
loop: def.loop ?? false,
|
|
4606
|
+
rate: jitter ? 1 + (Math.random() * 2 - 1) * jitter : 1,
|
|
4607
|
+
volume: def.volume ?? 1,
|
|
4608
|
+
};
|
|
4609
|
+
};
|
|
4610
|
+
const fire = async (event, ctx = {}, fireOpts) => {
|
|
4611
|
+
const steps = doc.on[event];
|
|
4612
|
+
const start = performance.now();
|
|
4613
|
+
const entries = [];
|
|
4614
|
+
const replay = fireOpts?.replay;
|
|
4615
|
+
const run = {
|
|
4616
|
+
skipped: false,
|
|
4617
|
+
instant: !!replay,
|
|
4618
|
+
haltAfter: replay?.untilEntry ?? -1,
|
|
4619
|
+
halted: false,
|
|
4620
|
+
releases: new Set(),
|
|
4621
|
+
};
|
|
4622
|
+
active.add(run);
|
|
4623
|
+
if (!replay)
|
|
4624
|
+
lastCtx.set(event, ctx);
|
|
4625
|
+
const trace = (entry) => {
|
|
4626
|
+
const full = { t: Math.round(performance.now() - start), ...entry };
|
|
4627
|
+
entries.push(full);
|
|
4628
|
+
if (run.haltAfter >= 0 && entries.length - 1 >= run.haltAfter)
|
|
4629
|
+
run.halted = true;
|
|
4630
|
+
if (!run.instant)
|
|
4631
|
+
opts.onTrace?.(event, full);
|
|
4632
|
+
};
|
|
4633
|
+
const makeRuntime = (ctxLocal) => {
|
|
4634
|
+
const rtLocal = {
|
|
4635
|
+
scene: opts.scene,
|
|
4636
|
+
ctx: ctxLocal,
|
|
4637
|
+
audio,
|
|
4638
|
+
get skipped() {
|
|
4639
|
+
return run.skipped;
|
|
4640
|
+
},
|
|
4641
|
+
get instant() {
|
|
4642
|
+
return run.instant;
|
|
4643
|
+
},
|
|
4644
|
+
get turbo() {
|
|
4645
|
+
return turbo;
|
|
4646
|
+
},
|
|
4647
|
+
wait(ms) {
|
|
4648
|
+
// Instant runs still yield a macrotask — a plugin polling `while(!done) await wait()`
|
|
4649
|
+
// must not starve the event loop the animation it awaits runs on.
|
|
4650
|
+
if (run.instant)
|
|
4651
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
4652
|
+
if (run.skipped || ms <= 0)
|
|
4653
|
+
return Promise.resolve();
|
|
4654
|
+
return new Promise((resolve) => {
|
|
4655
|
+
const timer = setTimeout(() => {
|
|
4656
|
+
run.releases.delete(release);
|
|
4657
|
+
resolve();
|
|
4658
|
+
}, ms / turbo);
|
|
4659
|
+
const release = () => {
|
|
4660
|
+
clearTimeout(timer);
|
|
4661
|
+
resolve();
|
|
4662
|
+
};
|
|
4663
|
+
run.releases.add(release);
|
|
4664
|
+
});
|
|
4665
|
+
},
|
|
4666
|
+
waitTap() {
|
|
4667
|
+
if (run.skipped || run.instant)
|
|
4668
|
+
return Promise.resolve();
|
|
4669
|
+
return new Promise((resolve) => {
|
|
4670
|
+
const release = () => {
|
|
4671
|
+
tapWaiters.delete(release);
|
|
4672
|
+
run.releases.delete(release);
|
|
4673
|
+
resolve();
|
|
4674
|
+
};
|
|
4675
|
+
tapWaiters.add(release);
|
|
4676
|
+
run.releases.add(release);
|
|
4677
|
+
});
|
|
4678
|
+
},
|
|
4679
|
+
async run(list) {
|
|
4680
|
+
for (const step of list) {
|
|
4681
|
+
if (run.halted)
|
|
4682
|
+
return;
|
|
4683
|
+
const contribution = registry.step(step.do);
|
|
4684
|
+
if (!contribution)
|
|
4685
|
+
continue; // validated; defensive
|
|
4686
|
+
trace({
|
|
4687
|
+
do: step.do,
|
|
4688
|
+
node: typeof step.node === 'string' ? step.node : undefined,
|
|
4689
|
+
});
|
|
4690
|
+
// trace() may flip `halted` on THIS entry — the halting step still executes
|
|
4691
|
+
// (scrub = "the state right after step N"); the loop stops before the next one.
|
|
4692
|
+
await contribution.run(step, rtLocal);
|
|
4693
|
+
}
|
|
4694
|
+
},
|
|
4695
|
+
resolve(value) {
|
|
4696
|
+
if (typeof value === 'string' && value.startsWith('$')) {
|
|
4697
|
+
return value
|
|
4698
|
+
.slice(1)
|
|
4699
|
+
.split('.')
|
|
4700
|
+
.reduce((acc, key) => acc?.[key], ctxLocal);
|
|
4701
|
+
}
|
|
4702
|
+
return value;
|
|
4703
|
+
},
|
|
4704
|
+
when(expr) {
|
|
4705
|
+
const bare = /^\s*([A-Za-z_$][\w$]*)\s*$/.exec(expr);
|
|
4706
|
+
if (bare)
|
|
4707
|
+
return Boolean(ctxLocal[bare[1]]);
|
|
4708
|
+
const result = evalVisibleWhen(expr, ctxLocal);
|
|
4709
|
+
if (result === undefined) {
|
|
4710
|
+
log(`unsupported when "${expr}" — treated as false`);
|
|
4711
|
+
return false;
|
|
4712
|
+
}
|
|
4713
|
+
return result;
|
|
4714
|
+
},
|
|
4715
|
+
child: (ctxPatch) => makeRuntime({ ...ctxLocal, ...ctxPatch }),
|
|
4716
|
+
trace,
|
|
4717
|
+
view: (id) => opts.scene.node(id),
|
|
4718
|
+
log,
|
|
4719
|
+
codeRef: (ref) => opts.code?.[ref],
|
|
4720
|
+
playCue(name, action) {
|
|
4721
|
+
if (run.instant)
|
|
4722
|
+
return; // scrub replays stay silent
|
|
4723
|
+
if (action === 'stop')
|
|
4724
|
+
return audio.stop(name);
|
|
4725
|
+
const resolved = resolveCue(name);
|
|
4726
|
+
if (resolved)
|
|
4727
|
+
audio.play(resolved);
|
|
4728
|
+
},
|
|
4729
|
+
};
|
|
4730
|
+
return rtLocal;
|
|
4731
|
+
};
|
|
4732
|
+
const rt = makeRuntime(ctx);
|
|
4733
|
+
try {
|
|
4734
|
+
if (!steps)
|
|
4735
|
+
log(`fire("${event}") — no steps declared`);
|
|
4736
|
+
else
|
|
4737
|
+
await rt.run(steps);
|
|
4738
|
+
}
|
|
4739
|
+
finally {
|
|
4740
|
+
active.delete(run);
|
|
4741
|
+
}
|
|
4742
|
+
return {
|
|
4743
|
+
event,
|
|
4744
|
+
turbo,
|
|
4745
|
+
skipped: run.skipped,
|
|
4746
|
+
...(replay ? { haltedAtEntry: Math.min(run.haltAfter, entries.length - 1) } : {}),
|
|
4747
|
+
durationMs: Math.round(performance.now() - start),
|
|
4748
|
+
entries,
|
|
4749
|
+
};
|
|
4750
|
+
};
|
|
4751
|
+
return {
|
|
4752
|
+
fire,
|
|
4753
|
+
replay(event, untilEntry) {
|
|
4754
|
+
const ctx = lastCtx.get(event);
|
|
4755
|
+
if (!ctx)
|
|
4756
|
+
log(`replay("${event}") before any fire — running with an empty ctx`);
|
|
4757
|
+
// Scrub = doc baseline + steps 0..N. Without the reset, leftovers of the previous
|
|
4758
|
+
// full run (steps > N) would bleed into the pose.
|
|
4759
|
+
opts.scene.resetProps();
|
|
4760
|
+
return fire(event, ctx ?? {}, { replay: { untilEntry } });
|
|
4761
|
+
},
|
|
4762
|
+
skip() {
|
|
4763
|
+
for (const run of active) {
|
|
4764
|
+
run.skipped = true;
|
|
4765
|
+
for (const release of [...run.releases])
|
|
4766
|
+
release();
|
|
4767
|
+
run.releases.clear();
|
|
4768
|
+
}
|
|
4769
|
+
},
|
|
4770
|
+
setTurbo(factor) {
|
|
4771
|
+
turbo = Math.max(0.1, factor);
|
|
4772
|
+
},
|
|
4773
|
+
tap() {
|
|
4774
|
+
for (const release of [...tapWaiters])
|
|
4775
|
+
release();
|
|
4776
|
+
},
|
|
4777
|
+
events: () => Object.keys(doc.on ?? {}),
|
|
4778
|
+
destroy() {
|
|
4779
|
+
this.skip();
|
|
4780
|
+
active.clear();
|
|
4781
|
+
},
|
|
4782
|
+
};
|
|
4783
|
+
}
|
|
4784
|
+
|
|
4785
|
+
// DocScene — the host bridge: a Scene + SlotSceneController whose presentation is
|
|
4786
|
+
// entirely doc-driven. It builds the display tree from scene.json, lays out on the
|
|
4787
|
+
// host's design-unit resize, and translates every host lifecycle hook into a flow-IR
|
|
4788
|
+
// event (serialized, so spinStart/result/enterMode never interleave). Sound cues route
|
|
4789
|
+
// to the REAL SceneApi.audio. With this class a game's `scenes` list entry becomes
|
|
4790
|
+
// { key: 'game', scene: createDocScene({ scene: sceneJson, flow: flowJson, … }) }
|
|
4791
|
+
// and the game ships no GameScene code for whatever the docs express.
|
|
4792
|
+
/** Build the createSlotGame `scenes` list from a StageDoc — every entry a DocScene. */
|
|
4793
|
+
function buildDocScenes(stage, shared) {
|
|
4794
|
+
return stage.scenes.map((entry) => ({
|
|
4795
|
+
key: entry.key,
|
|
4796
|
+
scene: new DocScene({ ...shared, scene: entry.scene, flow: entry.flow, advanceOnTap: entry.advanceOnTap }),
|
|
4797
|
+
skipOnReplay: entry.skipOnReplay,
|
|
4798
|
+
}));
|
|
4799
|
+
}
|
|
4800
|
+
/** Cue channel 'music' routes to playMusic/stopMusic; everything else to play(). */
|
|
4801
|
+
function sceneAudioAdapter(api, log) {
|
|
4802
|
+
const musicCues = new Set();
|
|
4803
|
+
return {
|
|
4804
|
+
play(cue) {
|
|
4805
|
+
const audio = api()?.audio;
|
|
4806
|
+
if (!audio)
|
|
4807
|
+
return log(`audio cue "${cue.cue}" before onCreate — dropped`);
|
|
4808
|
+
if (cue.channel === 'music') {
|
|
4809
|
+
musicCues.add(cue.cue);
|
|
4810
|
+
audio.playMusic(cue.src);
|
|
4811
|
+
}
|
|
4812
|
+
else {
|
|
4813
|
+
audio.play(cue.src, { loop: cue.loop, speed: cue.rate, volume: cue.volume });
|
|
4814
|
+
}
|
|
4815
|
+
},
|
|
4816
|
+
stop(cueName) {
|
|
4817
|
+
const audio = api()?.audio;
|
|
4818
|
+
if (!audio)
|
|
4819
|
+
return;
|
|
4820
|
+
if (musicCues.has(cueName))
|
|
4821
|
+
audio.stopMusic();
|
|
4822
|
+
// Looped sfx stop is not addressable through SceneAudio yet — flows should prefer
|
|
4823
|
+
// short one-shots for sfx and music channel for anything long-lived.
|
|
4824
|
+
},
|
|
4825
|
+
};
|
|
4826
|
+
}
|
|
4827
|
+
class DocScene extends Scene {
|
|
4828
|
+
opts;
|
|
4829
|
+
api = null;
|
|
4830
|
+
handle_ = null;
|
|
4831
|
+
runner_ = null;
|
|
4832
|
+
chain = Promise.resolve();
|
|
4833
|
+
lastSize = { width: 0, height: 0 };
|
|
4834
|
+
log;
|
|
4835
|
+
constructor(opts) {
|
|
4836
|
+
super();
|
|
4837
|
+
this.opts = opts;
|
|
4838
|
+
this.log = opts.log ?? ((m) => console.warn(`[doc-scene] ${m}`));
|
|
4839
|
+
}
|
|
4840
|
+
/** The live scene handle (inspector/agent attachment). Null before onEnter. */
|
|
4841
|
+
get scene() {
|
|
4842
|
+
return this.handle_;
|
|
4843
|
+
}
|
|
4844
|
+
get flow() {
|
|
4845
|
+
return this.runner_;
|
|
4846
|
+
}
|
|
4847
|
+
// ── Scene lifecycle ────────────────────────────────────────────────────────
|
|
4848
|
+
onEnter(data) {
|
|
4849
|
+
const goto = data?.goto;
|
|
4850
|
+
const sceneOpts = {
|
|
4851
|
+
plugins: this.opts.plugins,
|
|
4852
|
+
resolveSymbol: this.opts.resolveSymbol,
|
|
4853
|
+
vars: { mode: 'BASE', ...(this.opts.vars ?? {}) },
|
|
4854
|
+
log: this.log,
|
|
4855
|
+
};
|
|
4856
|
+
if (this.opts.texture)
|
|
4857
|
+
sceneOpts.texture = this.opts.texture;
|
|
4858
|
+
this.handle_ = createSceneFromDoc(this.opts.scene, sceneOpts);
|
|
4859
|
+
this.container.addChild(this.handle_.view);
|
|
4860
|
+
if (this.lastSize.width > 0)
|
|
4861
|
+
this.handle_.layout(this.lastSize.width, this.lastSize.height);
|
|
4862
|
+
if (this.opts.flow) {
|
|
4863
|
+
this.runner_ = createFlowRunner(this.opts.flow, {
|
|
4864
|
+
scene: this.handle_,
|
|
4865
|
+
plugins: this.opts.flowPlugins,
|
|
4866
|
+
code: this.opts.code,
|
|
4867
|
+
audio: sceneAudioAdapter(() => this.api, this.log),
|
|
4868
|
+
onTrace: this.opts.onTrace,
|
|
4869
|
+
log: this.log,
|
|
4870
|
+
});
|
|
4871
|
+
}
|
|
4872
|
+
// Taps feed `wait until:'tap'` flows; with advanceOnTap they also switch scenes.
|
|
4873
|
+
this.container.eventMode = 'static';
|
|
4874
|
+
this.container.on('pointertap', () => {
|
|
4875
|
+
this.runner_?.tap();
|
|
4876
|
+
if (this.opts.advanceOnTap)
|
|
4877
|
+
goto?.(this.opts.advanceOnTap);
|
|
4878
|
+
});
|
|
4879
|
+
if (this.opts.flow?.on['enter'])
|
|
4880
|
+
void this.fire('enter');
|
|
4881
|
+
}
|
|
4882
|
+
onResize(width, height) {
|
|
4883
|
+
this.lastSize = { width, height };
|
|
4884
|
+
this.handle_?.layout(width, height);
|
|
4885
|
+
this.container.hitArea = { contains: (x, y) => x >= 0 && y >= 0 && x <= width && y <= height };
|
|
4886
|
+
}
|
|
4887
|
+
onDestroy() {
|
|
4888
|
+
this.runner_?.destroy();
|
|
4889
|
+
this.handle_?.destroy();
|
|
4890
|
+
this.runner_ = null;
|
|
4891
|
+
this.handle_ = null;
|
|
4892
|
+
}
|
|
4893
|
+
// ── SlotSceneController → flow events (serialized) ─────────────────────────
|
|
4894
|
+
fire(event, ctx) {
|
|
4895
|
+
const next = this.chain.then(() => this.runner_?.fire(event, ctx)).catch((err) => {
|
|
4896
|
+
this.log(`flow "${event}" failed: ${String(err)}`);
|
|
4897
|
+
});
|
|
4898
|
+
this.chain = next;
|
|
4899
|
+
return next;
|
|
4900
|
+
}
|
|
4901
|
+
ctxOf(result, ctx) {
|
|
4902
|
+
if (this.opts.resultCtx)
|
|
4903
|
+
return this.opts.resultCtx(result, ctx);
|
|
4904
|
+
return {
|
|
4905
|
+
...result,
|
|
4906
|
+
win: result.totalWin,
|
|
4907
|
+
mode: ctx.mode,
|
|
4908
|
+
action: ctx.action,
|
|
4909
|
+
bet: ctx.bet,
|
|
4910
|
+
};
|
|
4911
|
+
}
|
|
4912
|
+
onCreate(api) {
|
|
4913
|
+
this.api = api;
|
|
4914
|
+
this.runner_?.setTurbo((this.opts.turboFactor ?? ((l) => 1 + l))(api.turbo));
|
|
4915
|
+
}
|
|
4916
|
+
onSpinStart() {
|
|
4917
|
+
void this.fire('spinStart');
|
|
4918
|
+
}
|
|
4919
|
+
async onSpin(result, ctx) {
|
|
4920
|
+
await this.fire('result', this.ctxOf(result, ctx));
|
|
4921
|
+
}
|
|
4922
|
+
async onEnterMode(result, ctx) {
|
|
4923
|
+
this.handle_?.setVar('mode', ctx.mode);
|
|
4924
|
+
await this.fire('enterMode', this.ctxOf(result, ctx));
|
|
4925
|
+
}
|
|
4926
|
+
async onExitMode(result, ctx) {
|
|
4927
|
+
await this.fire('exitMode', this.ctxOf(result, ctx));
|
|
4928
|
+
this.handle_?.setVar('mode', 'BASE');
|
|
4929
|
+
}
|
|
4930
|
+
onSpinEnd(result, ctx) {
|
|
4931
|
+
void this.fire('spinEnd', this.ctxOf(result, ctx));
|
|
4932
|
+
}
|
|
4933
|
+
onSkip() {
|
|
4934
|
+
this.runner_?.skip();
|
|
4935
|
+
}
|
|
4936
|
+
onTurboChanged(level) {
|
|
4937
|
+
this.runner_?.setTurbo((this.opts.turboFactor ?? ((l) => 1 + l))(level));
|
|
4938
|
+
}
|
|
4939
|
+
}
|
|
4940
|
+
function createDocScene(opts) {
|
|
4941
|
+
return new DocScene(opts);
|
|
4942
|
+
}
|
|
4943
|
+
|
|
4944
|
+
exports.BUILTIN_FILTER_KINDS = BUILTIN_FILTER_KINDS;
|
|
3619
4945
|
exports.BUILTIN_NODE_TYPES = BUILTIN_NODE_TYPES;
|
|
4946
|
+
exports.DocScene = DocScene;
|
|
4947
|
+
exports.buildDocScenes = buildDocScenes;
|
|
4948
|
+
exports.createDocScene = createDocScene;
|
|
4949
|
+
exports.createInnerCalibrator = createInnerCalibrator;
|
|
3620
4950
|
exports.createSceneFromDoc = createSceneFromDoc;
|
|
3621
4951
|
exports.createSceneRegistry = createSceneRegistry;
|
|
4952
|
+
exports.createTransformGizmo = createTransformGizmo;
|
|
3622
4953
|
exports.dependencyOf = dependencyOf;
|
|
4954
|
+
exports.dragInnerEdge = dragInnerEdge;
|
|
3623
4955
|
exports.edgePoint = edgePoint;
|
|
3624
4956
|
exports.effectiveNode = effectiveNode;
|
|
3625
4957
|
exports.evalVisibleWhen = evalVisibleWhen;
|
|
4958
|
+
exports.handlePoint = handlePoint;
|
|
4959
|
+
exports.nudgeRule = nudgeRule;
|
|
3626
4960
|
exports.orientationOf = orientationOf;
|
|
3627
4961
|
exports.placementBounds = placementBounds;
|
|
4962
|
+
exports.resizeFactors = resizeFactors;
|
|
4963
|
+
exports.resizeRule = resizeRule;
|
|
3628
4964
|
exports.resolveLayoutRule = resolveLayoutRule;
|
|
4965
|
+
exports.setRuleRotation = setRuleRotation;
|
|
4966
|
+
exports.sizeLever = sizeLever;
|
|
3629
4967
|
exports.validateSceneDoc = validateSceneDoc;
|
|
3630
4968
|
//# sourceMappingURL=scene.cjs.js.map
|