@datamoc/mw_games 0.37.0 → 0.39.1

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.
Files changed (113) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +26 -0
  3. package/api-surface.json +650 -2
  4. package/dist/assets/SvgParts.d.ts +31 -0
  5. package/dist/assets/SvgParts.d.ts.map +1 -0
  6. package/dist/assets/SvgParts.js +54 -0
  7. package/dist/assets/SvgParts.js.map +1 -0
  8. package/dist/assets/SvgSource.d.ts +3 -0
  9. package/dist/assets/SvgSource.d.ts.map +1 -0
  10. package/dist/assets/SvgSource.js +22 -0
  11. package/dist/assets/SvgSource.js.map +1 -0
  12. package/dist/assets/index.d.ts +2 -0
  13. package/dist/assets/index.d.ts.map +1 -1
  14. package/dist/assets/index.js +1 -0
  15. package/dist/assets/index.js.map +1 -1
  16. package/dist/assets/svg.d.ts.map +1 -1
  17. package/dist/assets/svg.js +9 -5
  18. package/dist/assets/svg.js.map +1 -1
  19. package/dist/mw_games.global.js +110 -110
  20. package/dist/mwl/presentation.d.ts +70 -0
  21. package/dist/mwl/presentation.d.ts.map +1 -0
  22. package/dist/mwl/presentation.js +419 -0
  23. package/dist/mwl/presentation.js.map +1 -0
  24. package/dist/physics/PhysicsWorld.d.ts +49 -17
  25. package/dist/physics/PhysicsWorld.d.ts.map +1 -1
  26. package/dist/physics/PhysicsWorld.js +297 -59
  27. package/dist/physics/PhysicsWorld.js.map +1 -1
  28. package/dist/physics/XpbdWorld.d.ts +1 -1
  29. package/dist/physics/XpbdWorld.js +1 -1
  30. package/dist/physics/cloth/index.d.ts +98 -0
  31. package/dist/physics/cloth/index.d.ts.map +1 -0
  32. package/dist/physics/cloth/index.js +207 -0
  33. package/dist/physics/cloth/index.js.map +1 -0
  34. package/dist/physics/index.d.ts +4 -0
  35. package/dist/physics/index.d.ts.map +1 -1
  36. package/dist/physics/index.js +2 -0
  37. package/dist/physics/index.js.map +1 -1
  38. package/dist/physics/spring/index.d.ts +90 -0
  39. package/dist/physics/spring/index.d.ts.map +1 -0
  40. package/dist/physics/spring/index.js +97 -0
  41. package/dist/physics/spring/index.js.map +1 -0
  42. package/dist/skeleton/index.d.ts +94 -13
  43. package/dist/skeleton/index.d.ts.map +1 -1
  44. package/dist/skeleton/index.js +192 -73
  45. package/dist/skeleton/index.js.map +1 -1
  46. package/dist/two-d/presentation/MwlPresentation.d.ts +110 -0
  47. package/dist/two-d/presentation/MwlPresentation.d.ts.map +1 -0
  48. package/dist/two-d/presentation/MwlPresentation.js +755 -0
  49. package/dist/two-d/presentation/MwlPresentation.js.map +1 -0
  50. package/dist/two-d/presentation/index.d.ts +3 -0
  51. package/dist/two-d/presentation/index.d.ts.map +1 -0
  52. package/dist/two-d/presentation/index.js +2 -0
  53. package/dist/two-d/presentation/index.js.map +1 -0
  54. package/dist/two-d/render/ClothSprite.d.ts +83 -0
  55. package/dist/two-d/render/ClothSprite.d.ts.map +1 -0
  56. package/dist/two-d/render/ClothSprite.js +105 -0
  57. package/dist/two-d/render/ClothSprite.js.map +1 -0
  58. package/dist/two-d/render/HazeLayer.d.ts +112 -0
  59. package/dist/two-d/render/HazeLayer.d.ts.map +1 -0
  60. package/dist/two-d/render/HazeLayer.js +185 -0
  61. package/dist/two-d/render/HazeLayer.js.map +1 -0
  62. package/dist/two-d/render/NeonGlow.d.ts +100 -0
  63. package/dist/two-d/render/NeonGlow.d.ts.map +1 -0
  64. package/dist/two-d/render/NeonGlow.js +183 -0
  65. package/dist/two-d/render/NeonGlow.js.map +1 -0
  66. package/dist/two-d/render/SkeletalSprite.d.ts +5 -2
  67. package/dist/two-d/render/SkeletalSprite.d.ts.map +1 -1
  68. package/dist/two-d/render/SkeletalSprite.js +27 -3
  69. package/dist/two-d/render/SkeletalSprite.js.map +1 -1
  70. package/dist/two-d/render/SpringSprite.d.ts +40 -0
  71. package/dist/two-d/render/SpringSprite.d.ts.map +1 -0
  72. package/dist/two-d/render/SpringSprite.js +39 -0
  73. package/dist/two-d/render/SpringSprite.js.map +1 -0
  74. package/dist/two-d/render/SpriteDeform.d.ts +156 -0
  75. package/dist/two-d/render/SpriteDeform.d.ts.map +1 -0
  76. package/dist/two-d/render/SpriteDeform.js +278 -0
  77. package/dist/two-d/render/SpriteDeform.js.map +1 -0
  78. package/dist/two-d/render/SvgSkeleton.d.ts +54 -0
  79. package/dist/two-d/render/SvgSkeleton.d.ts.map +1 -0
  80. package/dist/two-d/render/SvgSkeleton.js +112 -0
  81. package/dist/two-d/render/SvgSkeleton.js.map +1 -0
  82. package/dist/two-d/render/index.d.ts +12 -0
  83. package/dist/two-d/render/index.d.ts.map +1 -1
  84. package/dist/two-d/render/index.js +6 -0
  85. package/dist/two-d/render/index.js.map +1 -1
  86. package/dist/two-d/ui/Bar.d.ts +3 -1
  87. package/dist/two-d/ui/Bar.d.ts.map +1 -1
  88. package/dist/two-d/ui/Bar.js +5 -0
  89. package/dist/two-d/ui/Bar.js.map +1 -1
  90. package/dist/two-d/ui/Button.d.ts +12 -0
  91. package/dist/two-d/ui/Button.d.ts.map +1 -1
  92. package/dist/two-d/ui/Button.js +16 -4
  93. package/dist/two-d/ui/Button.js.map +1 -1
  94. package/dist/two-d/ui/Window.d.ts +10 -0
  95. package/dist/two-d/ui/Window.d.ts.map +1 -1
  96. package/dist/two-d/ui/Window.js +17 -6
  97. package/dist/two-d/ui/Window.js.map +1 -1
  98. package/dist/two-d/ui/index.d.ts +2 -0
  99. package/dist/two-d/ui/index.d.ts.map +1 -1
  100. package/dist/two-d/ui/index.js.map +1 -1
  101. package/dist/version.d.ts +1 -1
  102. package/dist/version.js +1 -1
  103. package/package.json +24 -7
  104. package/sbom.cdx.json +6 -58
  105. package/tools/asset-report.d.mts +37 -0
  106. package/tools/asset-report.mjs +157 -0
  107. package/tools/compile-resources.d.mts +2 -0
  108. package/tools/compile-resources.mjs +23 -2
  109. package/tools/emit-page.d.mts +2 -0
  110. package/tools/emit-page.mjs +11 -1
  111. package/tools/verify-scenarios.d.mts +15 -0
  112. package/tools/verify-scenarios.mjs +129 -0
  113. package/tools/vite.mjs +4 -1
@@ -0,0 +1,755 @@
1
+ import { Container, Graphics, Sprite } from 'pixi.js';
2
+ import * as Input from '../../core/Input.js';
3
+ import { texture } from '../../assets/loader.js';
4
+ import { Camera } from '../render/Camera.js';
5
+ import { Button } from '../ui/Button.js';
6
+ import { Label } from '../ui/Label.js';
7
+ import { Bar } from '../ui/Bar.js';
8
+ import { Window } from '../ui/Window.js';
9
+ import { WindowStack } from '../ui/WindowStack.js';
10
+ import { Grid, flowLayout, resolveAnchor } from '../ui/Layout.js';
11
+ import { setTheme, theme } from '../ui/theme.js';
12
+ /** Fallback widget sizes, shared by construction and layout so the two cannot drift. */
13
+ const DEFAULT_BUTTON_WIDTH = 160;
14
+ const DEFAULT_BUTTON_HEIGHT = 32;
15
+ const DEFAULT_BAR_HEIGHT = 12;
16
+ const DEFAULT_WINDOW_WIDTH = 300;
17
+ const DEFAULT_WINDOW_HEIGHT = 200;
18
+ /**
19
+ * Instantiates compiled MWL presentation using existing Pixi widgets. The controller owns state
20
+ * and actions; context switches restyle the same views without rebuilding procedural adapters.
21
+ * Kept off the root barrel so a game pays for this runtime only when it imports the subpath.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * import { MwlPresentation } from '@datamoc/mw_games/two-d/presentation';
26
+ * import type { CompiledPresentation } from '@datamoc/mw_games/mwl/presentation';
27
+ * import { Node2D } from '@datamoc/mw_games/two-d/render';
28
+ * declare const data: CompiledPresentation;
29
+ * const ui = new MwlPresentation(data, 'game', { parent: new Node2D(), state: () => ({}), actions: {} });
30
+ * ui.resize(800, 600);
31
+ * ui.update(1 / 60);
32
+ * ui.destroy();
33
+ * ```
34
+ */
35
+ export class MwlPresentation {
36
+ constructor(data, sceneId, options) {
37
+ this.root = new Container();
38
+ this.nodes = new Map();
39
+ this.roots = [];
40
+ this.appliedTheme = null;
41
+ this.context = '';
42
+ this.tokens = {};
43
+ this.viewport = { width: 0, height: 0 };
44
+ this.focused = null;
45
+ this.destroyed = false;
46
+ this.state = {};
47
+ this.inputListener = (action) => this.handleAction(action);
48
+ this.data = data;
49
+ this.options = options;
50
+ if (!Object.hasOwn(data.scenes, sceneId))
51
+ throw new Error(`unknown presentation scene "${sceneId}"`);
52
+ const scene = data.scenes[sceneId];
53
+ for (const action of data.manifest.actions)
54
+ if (!Object.hasOwn(options.actions, action))
55
+ throw new Error(`missing presentation action "${action}"`);
56
+ for (const adapter of data.manifest.adapters ?? [])
57
+ if (!options.adapters || !Object.hasOwn(options.adapters, adapter))
58
+ throw new Error(`missing presentation adapter "${adapter}"`);
59
+ this.baseTheme = scene.theme;
60
+ this.previousTheme = theme();
61
+ const cameraNode = scene.children.find((node) => node.kind === 'scene_camera');
62
+ this.cameraProperties = cameraNode?.properties ?? {};
63
+ this.cameraPath = cameraNode ? `${sceneId}/${cameraNode.id}` : null;
64
+ this.camera = new Camera({ zoom: Number(cameraNode?.properties.zoom ?? 1) });
65
+ this.camera.snapTo(Number(cameraNode?.properties.x ?? 0), Number(cameraNode?.properties.y ?? 0));
66
+ this.windows = new WindowStack();
67
+ this.root.addChild(this.camera.world);
68
+ options.parent.addChild(this.root);
69
+ try {
70
+ this.applyTheme();
71
+ for (const node of scene.children) {
72
+ if (node.kind === 'scene_camera')
73
+ continue;
74
+ const parent = node.kind === 'scene_layer' && node.properties.space === 'world' ? this.camera.world : this.root;
75
+ this.roots.push(this.createRecord(node, `${sceneId}/${node.id}`, parent));
76
+ }
77
+ this.root.addChild(this.windows);
78
+ Input.onAction.add(this.inputListener);
79
+ this.update();
80
+ this.unsubscribe = options.subscribe?.(() => this.update());
81
+ }
82
+ catch (error) {
83
+ this.destroy();
84
+ throw error;
85
+ }
86
+ }
87
+ /** Looks up an instance by its compiled path; repeats add their stable key to the path. */
88
+ get(id) {
89
+ if (!this.destroyed && id === this.cameraPath)
90
+ return this.camera.world;
91
+ return this.nodes.get(id)?.view ?? null;
92
+ }
93
+ get focusId() {
94
+ return this.focused?.path ?? null;
95
+ }
96
+ get activeContext() {
97
+ return this.context;
98
+ }
99
+ get palette() {
100
+ return this.tokens;
101
+ }
102
+ setContext(context = '') {
103
+ this.assertAlive();
104
+ if (context && !Object.hasOwn(this.data.contexts, context))
105
+ throw new Error(`unknown presentation context "${context}"`);
106
+ if (context === this.context)
107
+ return;
108
+ this.context = context;
109
+ this.applyTheme();
110
+ for (const node of this.nodes.values())
111
+ node.values = {};
112
+ this.update();
113
+ }
114
+ applyTheme() {
115
+ const context = this.data.contexts[this.context];
116
+ const definition = this.data.themes[context?.theme ?? this.baseTheme];
117
+ this.tokens = { ...definition.tokens, ...context?.tokens };
118
+ const props = definition.properties;
119
+ setTheme({
120
+ ...this.previousTheme,
121
+ padding: Number(props.padding ?? this.previousTheme.padding),
122
+ spacing: Number(props.spacing ?? this.previousTheme.spacing),
123
+ font: {
124
+ ...this.previousTheme.font,
125
+ family: props.font_family ?? this.previousTheme.font.family,
126
+ size: Number(props.font_size ?? this.previousTheme.font.size),
127
+ },
128
+ color: {
129
+ ...this.previousTheme.color,
130
+ panelFill: this.tokens.panel,
131
+ text: this.tokens.text,
132
+ textHighlight: this.tokens.accent,
133
+ selection: this.tokens.selection,
134
+ },
135
+ });
136
+ this.appliedTheme = theme();
137
+ }
138
+ update(dt = 0) {
139
+ this.assertAlive();
140
+ this.state = this.options.state();
141
+ let changed = false;
142
+ for (const node of this.roots)
143
+ changed = this.refresh(node) || changed;
144
+ if (this.root.children[this.root.children.length - 1] !== this.windows)
145
+ this.root.setChildIndex(this.windows, this.root.children.length - 1);
146
+ if (changed)
147
+ this.layout();
148
+ for (const node of this.nodes.values())
149
+ node.adapter?.update?.(dt, this.state);
150
+ this.camera.update(dt);
151
+ this.windows.update(dt);
152
+ const window = this.windows.top
153
+ ? [...this.nodes.values()].find((node) => node.view === this.windows.top)
154
+ : undefined;
155
+ const candidates = this.focusable(window);
156
+ if (!this.focused || !candidates.includes(this.focused))
157
+ this.setFocus(candidates[0] ?? null);
158
+ }
159
+ resize(width, height) {
160
+ this.assertAlive();
161
+ if (![width, height].every((size) => Number.isFinite(size) && size >= 0))
162
+ throw new Error('presentation viewport must be finite and nonnegative');
163
+ this.viewport = { width, height };
164
+ this.camera.setViewport(width, height);
165
+ if (this.cameraProperties.fit_width)
166
+ this.camera.fit(Number(this.cameraProperties.fit_width), Number(this.cameraProperties.fit_height));
167
+ this.windows.setViewport(width, height);
168
+ this.layout();
169
+ }
170
+ createRecord(definition, path, parent, item) {
171
+ const node = {
172
+ definition,
173
+ path,
174
+ parent,
175
+ item,
176
+ view: null,
177
+ children: [],
178
+ properties: {},
179
+ values: {},
180
+ width: 0,
181
+ height: 0,
182
+ state: 'idle',
183
+ closed: false,
184
+ cleanups: [],
185
+ };
186
+ if (this.nodes.has(path))
187
+ throw new Error(`duplicate presentation path "${path}"`);
188
+ this.nodes.set(path, node);
189
+ return node;
190
+ }
191
+ read(node, name, fallback) {
192
+ const binding = node.definition.properties[`${name}_binding`];
193
+ if (!binding)
194
+ return node.definition.properties[name] === undefined
195
+ ? fallback
196
+ : this.coerce(node.definition.properties[name], typeof fallback);
197
+ const value = binding.startsWith('item.') ? node.item?.[binding.slice(5)] : this.state[binding];
198
+ if (typeof value !== typeof fallback || (typeof value === 'number' && !Number.isFinite(value)))
199
+ throw new Error(`${node.path}: invalid binding "${binding}"`);
200
+ return value;
201
+ }
202
+ coerce(value, type) {
203
+ if (type === 'number')
204
+ return Number(value);
205
+ if (type === 'boolean')
206
+ return value === 'true';
207
+ return value;
208
+ }
209
+ resolveProperties(node) {
210
+ const base = node.definition.properties;
211
+ return {
212
+ ...base,
213
+ ...this.data.contexts[this.context]?.overrides[node.definition.source],
214
+ ...this.data.variants[base.variant],
215
+ ...node.definition.states[node.state],
216
+ };
217
+ }
218
+ refresh(node) {
219
+ const visible = this.read(node, 'visible', true);
220
+ if (!visible)
221
+ node.closed = false;
222
+ if (node.definition.kind === 'ui_window' && (!visible || node.closed)) {
223
+ const changed = Boolean(node.view);
224
+ if (node.view)
225
+ this.unmount(node);
226
+ return changed;
227
+ }
228
+ let changed = false;
229
+ if (!node.view) {
230
+ this.mount(node);
231
+ changed = true;
232
+ }
233
+ const view = this.viewOf(node);
234
+ if (view.visible !== visible) {
235
+ view.visible = visible;
236
+ changed = true;
237
+ }
238
+ if (view instanceof Button)
239
+ changed = this.refreshButton(node, view) || changed;
240
+ const properties = this.resolveProperties(node);
241
+ if (!equalProperties(node.properties, properties)) {
242
+ node.properties = properties;
243
+ changed = true;
244
+ }
245
+ const values = this.readValues(node, properties, visible);
246
+ if (!equalProperties(node.values, values)) {
247
+ node.values = values;
248
+ changed = true;
249
+ }
250
+ if (changed)
251
+ this.applyProperties(node);
252
+ if (node.definition.kind === 'ui_repeat')
253
+ changed = this.reconcile(node) || changed;
254
+ for (const child of node.children) {
255
+ child.item = node.item ?? child.item;
256
+ changed = this.refresh(child) || changed;
257
+ }
258
+ return changed;
259
+ }
260
+ refreshButton(node, view) {
261
+ let changed = false;
262
+ const disabled = this.read(node, 'disabled', false);
263
+ const selected = this.read(node, 'selected', false);
264
+ if (disabled !== view.disabled) {
265
+ view.setDisabled(disabled);
266
+ changed = true;
267
+ }
268
+ if (selected !== view.selected) {
269
+ view.setSelected(selected);
270
+ changed = true;
271
+ }
272
+ if (disabled)
273
+ node.state = 'disabled';
274
+ else if (selected)
275
+ node.state = 'selected';
276
+ else if (node === this.focused)
277
+ node.state = 'focused';
278
+ else if (['disabled', 'selected', 'focused'].includes(node.state))
279
+ node.state = 'idle';
280
+ return changed;
281
+ }
282
+ readValues(node, properties, visible) {
283
+ const view = this.viewOf(node);
284
+ const values = { visible, alpha: Number(properties.alpha ?? 1) };
285
+ if (view instanceof Button || view instanceof Label)
286
+ values.text = this.read(node, 'text', '');
287
+ if (view instanceof Window)
288
+ values.title = this.read(node, 'title', '');
289
+ if (view instanceof Bar) {
290
+ values.value = this.read(node, 'value', 1);
291
+ values.max = this.read(node, 'max', 1);
292
+ values.color = this.read(node, 'color', this.token(properties.color_token, 'accent'));
293
+ }
294
+ return values;
295
+ }
296
+ viewOf(node) {
297
+ const view = node.view;
298
+ if (!view)
299
+ throw new Error(`${node.path}: missing view`);
300
+ return view;
301
+ }
302
+ mount(node) {
303
+ const props = this.resolveProperties(node);
304
+ const width = Number(props.width ?? DEFAULT_BUTTON_WIDTH), height = Number(props.height ?? DEFAULT_BUTTON_HEIGHT);
305
+ switch (node.definition.kind) {
306
+ case 'ui_label':
307
+ node.view = new Label({ text: '', color: this.token(props.color_token, 'text') });
308
+ break;
309
+ case 'ui_button':
310
+ node.view = new Button({ width, height, text: '' });
311
+ break;
312
+ case 'ui_bar':
313
+ node.view = new Bar({ width, height: Number(props.height ?? DEFAULT_BAR_HEIGHT) });
314
+ break;
315
+ case 'ui_window':
316
+ node.view = new Window({
317
+ width: Number(props.width ?? DEFAULT_WINDOW_WIDTH),
318
+ height: Number(props.height ?? DEFAULT_WINDOW_HEIGHT),
319
+ title: '',
320
+ modal: props.modal !== 'false',
321
+ closable: props.closable !== 'false',
322
+ closeButton: true,
323
+ blocker: true,
324
+ anchor: (props.anchor ?? 'center'),
325
+ });
326
+ break;
327
+ default: {
328
+ if (props.adapter) {
329
+ const adapters = this.options.adapters;
330
+ const create = adapters && Object.hasOwn(adapters, props.adapter) ? adapters[props.adapter] : undefined;
331
+ if (!create)
332
+ throw new Error(`${node.path}: missing presentation adapter "${props.adapter}"`);
333
+ node.adapter = create({ tokens: this.tokens, properties: props });
334
+ node.view = node.adapter.view;
335
+ }
336
+ else
337
+ node.view = new Container();
338
+ }
339
+ }
340
+ node.parent.addChild(node.view);
341
+ const childParent = node.view instanceof Window ? node.view.content : node.view;
342
+ if (node.definition.kind !== 'ui_repeat')
343
+ for (const child of node.definition.children)
344
+ node.children.push(this.createRecord(child, `${node.path}/${child.id}`, childParent, node.item));
345
+ if (node.view instanceof Button)
346
+ this.wireButton(node, node.view);
347
+ if (node.view instanceof Window)
348
+ this.wireWindow(node, node.view);
349
+ }
350
+ wireButton(node, button) {
351
+ const click = () => {
352
+ this.setFocus(node);
353
+ this.invoke(node.definition.properties.action, node.item);
354
+ if (!this.destroyed)
355
+ this.update();
356
+ };
357
+ button.onClick.add(click);
358
+ node.cleanups.push(() => button.onClick.remove(click));
359
+ for (const [event, state] of [
360
+ ['pointerover', 'hover'],
361
+ ['pointerout', 'idle'],
362
+ ['pointerdown', 'pressed'],
363
+ ['pointerup', 'hover'],
364
+ ]) {
365
+ const listener = () => {
366
+ node.state = state;
367
+ this.update();
368
+ };
369
+ button.on(event, listener);
370
+ node.cleanups.push(() => button.off(event, listener));
371
+ }
372
+ }
373
+ wireWindow(node, window) {
374
+ window.delegate = { handleAction: (action) => this.navigate(action, node) };
375
+ const close = () => {
376
+ node.closed = true;
377
+ this.unmount(node, false);
378
+ this.invoke(node.definition.properties.close_action, node.item);
379
+ };
380
+ window.onClose.add(close);
381
+ node.cleanups.push(() => window.onClose.remove(close));
382
+ this.windows.push(window);
383
+ }
384
+ reconcile(node) {
385
+ const p = node.definition.properties;
386
+ const items = this.state[p.items_binding];
387
+ if (!Array.isArray(items))
388
+ throw new Error(`${node.path}: collection "${p.items_binding}" must be an array`);
389
+ const keys = new Set();
390
+ for (const item of items) {
391
+ const key = item[p.key];
392
+ if ((typeof key !== 'string' && typeof key !== 'number') ||
393
+ (typeof key === 'number' && !Number.isFinite(key)))
394
+ throw new Error(`${node.path}: invalid repeat key`);
395
+ const path = `${node.path}/${encodeURIComponent(String(key))}`;
396
+ if (keys.has(path))
397
+ throw new Error(`${node.path}: duplicate repeat key "${key}"`);
398
+ keys.add(path);
399
+ }
400
+ const existing = new Map(node.children.map((child) => [child.path.slice(0, child.path.lastIndexOf('/')), child]));
401
+ const next = [];
402
+ let changed = false;
403
+ for (const item of items) {
404
+ const prefix = `${node.path}/${encodeURIComponent(String(item[p.key]))}`;
405
+ const template = node.definition.children[0];
406
+ const child = existing.get(prefix) ??
407
+ this.createRecord(template, `${prefix}/${template.id}`, this.viewOf(node), item);
408
+ if (!existing.has(prefix))
409
+ changed = true;
410
+ existing.delete(prefix);
411
+ child.item = item;
412
+ next.push(child);
413
+ }
414
+ for (const child of existing.values()) {
415
+ this.dispose(child);
416
+ changed = true;
417
+ }
418
+ if (next.some((child, i) => child !== node.children[i]))
419
+ changed = true;
420
+ node.children = next;
421
+ return changed;
422
+ }
423
+ token(name, fallback) {
424
+ return this.tokens[name ?? fallback];
425
+ }
426
+ applyProperties(node) {
427
+ const view = this.viewOf(node), props = node.properties, values = node.values;
428
+ view.alpha = values.alpha;
429
+ if (node.definition.kind === 'scene_layer' && !node.adapter) {
430
+ if (props.image) {
431
+ if (!node.image) {
432
+ node.image = new Sprite(texture(props.image));
433
+ view.addChildAt(node.image, 0);
434
+ }
435
+ else
436
+ node.image.texture = texture(props.image);
437
+ }
438
+ else {
439
+ node.image?.destroy();
440
+ node.image = undefined;
441
+ }
442
+ }
443
+ if (['ui_container', 'ui_row', 'ui_column', 'ui_grid', 'ui_mount', 'ui_repeat'].includes(node.definition.kind)) {
444
+ if (props.background_token && !node.background) {
445
+ node.background = new Graphics();
446
+ view.addChildAt(node.background, 0);
447
+ }
448
+ else if (!props.background_token && node.background) {
449
+ node.background.destroy();
450
+ node.background = undefined;
451
+ }
452
+ }
453
+ const translate = this.options.translate ?? ((text) => text);
454
+ if (view instanceof Label) {
455
+ view.setText(translate(values.text));
456
+ view.setColor(this.token(props.color_token, 'text'));
457
+ }
458
+ if (view instanceof Button) {
459
+ view.setText(translate(values.text));
460
+ view.setColors({
461
+ text: this.token(props.color_token, 'text'),
462
+ disabledText: this.token(props.color_token, 'text'),
463
+ background: this.token(props.background_token, 'panel'),
464
+ border: this.token(props.border_token, 'accent'),
465
+ selection: node.definition.states[node.state]?.background_token
466
+ ? this.token(props.background_token, 'selection')
467
+ : this.tokens.selection,
468
+ });
469
+ }
470
+ if (view instanceof Bar) {
471
+ view.setValue(values.value, values.max);
472
+ view.setColor(values.color);
473
+ view.setBackground(this.token(props.background_token, 'panel'));
474
+ }
475
+ if (view instanceof Window) {
476
+ view.setTitle(translate(values.title));
477
+ view.setColors({
478
+ background: this.token(props.background_token, 'panel'),
479
+ border: this.token(props.border_token, 'accent'),
480
+ title: this.token(props.color_token, 'accent'),
481
+ });
482
+ }
483
+ node.adapter?.restyle?.({ tokens: this.tokens, properties: props });
484
+ }
485
+ isVisible(node) {
486
+ return Boolean(node.view?.visible);
487
+ }
488
+ layout() {
489
+ const bounds = { x: 0, y: 0, ...this.viewport };
490
+ for (const node of this.roots)
491
+ this.layoutNode(node, node.parent === this.camera.world
492
+ ? { ...bounds, width: this.camera.view.width, height: this.camera.view.height }
493
+ : bounds);
494
+ this.windows.setViewport(this.viewport.width, this.viewport.height);
495
+ }
496
+ layoutNode(node, bounds) {
497
+ const view = node.view;
498
+ if (!view || !view.visible)
499
+ return;
500
+ const { width, height: measured } = this.measureNode(node, view, bounds);
501
+ const props = node.properties;
502
+ const padding = Number(props.padding ?? 0), gap = Number(props.gap ?? 0);
503
+ const inner = {
504
+ x: 0,
505
+ y: 0,
506
+ width: Math.max(0, width - padding * 2),
507
+ height: Math.max(0, measured - padding * 2),
508
+ };
509
+ if (view instanceof Window) {
510
+ inner.width = view.contentWidth;
511
+ inner.height = view.contentHeight;
512
+ }
513
+ const visible = node.children.filter((child) => this.isVisible(child));
514
+ for (const child of visible)
515
+ this.layoutNode(child, inner);
516
+ const auto = this.placeChildren(node, visible, inner, padding, gap);
517
+ const height = auto ?? measured;
518
+ node.width = width;
519
+ node.height = height;
520
+ this.paintNode(node, width, height);
521
+ this.anchorNode(node, view, bounds, width, height);
522
+ node.adapter?.resize?.(width, height);
523
+ }
524
+ measureNode(node, view, bounds) {
525
+ const props = node.properties;
526
+ const defaultWidth = view instanceof Button || view instanceof Bar ? DEFAULT_BUTTON_WIDTH : bounds.width;
527
+ let width = Math.min(Number(props.width ?? defaultWidth), bounds.width);
528
+ let height = Number(props.height ?? this.defaultHeight(view, bounds));
529
+ if (view instanceof Window) {
530
+ width = Math.max(0, Math.min(Number(props.width ?? DEFAULT_WINDOW_WIDTH), this.viewport.width - theme().padding * 2));
531
+ height = Math.max(0, Math.min(Number(props.height ?? DEFAULT_WINDOW_HEIGHT), this.viewport.height - theme().padding * 2));
532
+ view.resize(width, height);
533
+ }
534
+ else if (view instanceof Button || view instanceof Bar)
535
+ view.resize(width, height);
536
+ else if (view instanceof Label) {
537
+ view.setWrapWidth(width);
538
+ height = view.height;
539
+ }
540
+ return { width, height };
541
+ }
542
+ defaultHeight(view, bounds) {
543
+ if (view instanceof Button)
544
+ return DEFAULT_BUTTON_HEIGHT;
545
+ if (view instanceof Bar)
546
+ return DEFAULT_BAR_HEIGHT;
547
+ if (view instanceof Label)
548
+ return view.height;
549
+ return bounds.height;
550
+ }
551
+ /** Positions visible children; returns the content height when the node sizes to fit. */
552
+ placeChildren(node, visible, inner, padding, gap) {
553
+ const props = node.properties;
554
+ const kind = node.definition.kind;
555
+ if (['ui_row', 'ui_repeat'].includes(kind)) {
556
+ const total = visible.reduce((sum, child) => sum + child.width, 0) + gap * Math.max(0, visible.length - 1);
557
+ const result = flowLayout(visible, kind === 'ui_row' && props.wrap === 'false' ? Math.max(total, inner.width) : inner.width, { gap, align: props.align });
558
+ visible.forEach((child, i) => {
559
+ child.view.position.set(padding + result.positions[i].x, padding + result.positions[i].y);
560
+ });
561
+ return props.height === undefined ? result.height + padding * 2 : undefined;
562
+ }
563
+ if (kind === 'ui_column') {
564
+ let y = padding;
565
+ for (const child of visible) {
566
+ child.view.position.set(padding, y);
567
+ y += child.height + gap;
568
+ }
569
+ return props.height === undefined ? Math.max(0, y - gap + padding) : undefined;
570
+ }
571
+ if (kind === 'ui_grid') {
572
+ const columns = Number(props.columns), rows = Math.ceil(visible.length / columns);
573
+ const grid = new Grid({
574
+ columns: Array.from({ length: columns }, () => ({ grow: 1 })),
575
+ rows: Array.from({ length: rows }, () => ({
576
+ size: Math.max(0, ...visible.map((child) => child.height)),
577
+ })),
578
+ gap,
579
+ });
580
+ visible.forEach((child, i) => {
581
+ const cell = grid.rect(Math.floor(i / columns), i % columns, inner);
582
+ this.layoutNode(child, { ...cell, x: cell.x + padding, y: cell.y + padding });
583
+ });
584
+ if (props.height === undefined)
585
+ return (grid.rowSizes(inner.height).reduce((sum, size) => sum + size, 0) +
586
+ gap * Math.max(0, rows - 1) +
587
+ padding * 2);
588
+ return undefined;
589
+ }
590
+ if (['ui_container', 'ui_mount'].includes(kind) && props.height === undefined)
591
+ return Math.max(0, ...visible.map((child) => child.view.y + child.height)) + padding * 2;
592
+ return undefined;
593
+ }
594
+ paintNode(node, width, height) {
595
+ const props = node.properties;
596
+ if (node.background) {
597
+ node.background.clear().rect(0, 0, width, height).fill(this.token(props.background_token, 'panel'));
598
+ if (props.border_token)
599
+ node.background.stroke({ color: this.token(props.border_token, 'accent'), width: 1 });
600
+ }
601
+ }
602
+ anchorNode(node, view, bounds, width, height) {
603
+ const props = node.properties;
604
+ if (node.parent === this.camera.world)
605
+ view.position.set(Number(props.x ?? 0), Number(props.y ?? 0));
606
+ else if (!(view instanceof Window)) {
607
+ const placed = resolveAnchor({
608
+ anchor: (props.anchor ?? 'top-left'),
609
+ width,
610
+ height,
611
+ offsetX: Number(props.x ?? 0),
612
+ offsetY: Number(props.y ?? 0),
613
+ }, bounds);
614
+ view.position.set(placed.x, placed.y);
615
+ }
616
+ }
617
+ invoke(action, item) {
618
+ if (action)
619
+ this.options.actions[action](item);
620
+ }
621
+ isFocusable(node) {
622
+ if (!(node.view instanceof Button) || node.view.disabled)
623
+ return false;
624
+ for (let view = node.view; view; view = view.parent)
625
+ if (!view.visible)
626
+ return false;
627
+ return true;
628
+ }
629
+ focusable(window) {
630
+ const result = [];
631
+ const visit = (node) => {
632
+ if (this.isFocusable(node))
633
+ result.push(node);
634
+ for (const child of node.children)
635
+ visit(child);
636
+ };
637
+ for (const node of window ? window.children : this.roots)
638
+ visit(node);
639
+ return result;
640
+ }
641
+ setFocus(node) {
642
+ if (node === this.focused)
643
+ return;
644
+ if (this.focused?.state === 'focused')
645
+ this.focused.state = 'idle';
646
+ this.focused = node;
647
+ if (node && node.state === 'idle')
648
+ node.state = 'focused';
649
+ }
650
+ navigate(action, window) {
651
+ const nodes = this.focusable(window);
652
+ if (!nodes.length)
653
+ return false;
654
+ let index = nodes.indexOf(this.focused);
655
+ if (['up', 'left', 'down', 'right', 'next'].includes(action)) {
656
+ const delta = ['up', 'left'].includes(action) ? -1 : 1;
657
+ index = index < 0 ? 0 : (index + delta + nodes.length) % nodes.length;
658
+ this.setFocus(nodes[index]);
659
+ this.update();
660
+ return true;
661
+ }
662
+ if (action === 'confirm') {
663
+ const node = nodes[Math.max(0, index)];
664
+ this.setFocus(node);
665
+ node.view.onClick.dispatch();
666
+ this.update();
667
+ return true;
668
+ }
669
+ return false;
670
+ }
671
+ /** Offers modal windows input first, then the screen-space buttons. */
672
+ handleAction(action) {
673
+ if (this.destroyed)
674
+ return false;
675
+ if (this.windows.top)
676
+ return this.windows.handleAction(action) || this.windows.blocksWorld;
677
+ return this.navigate(action);
678
+ }
679
+ unmount(node, destroyView = true) {
680
+ const children = node.children;
681
+ const cleanups = node.cleanups;
682
+ const adapter = node.adapter;
683
+ const view = node.view;
684
+ node.children = [];
685
+ node.cleanups = [];
686
+ node.adapter = undefined;
687
+ node.background = undefined;
688
+ node.image = undefined;
689
+ node.view = null;
690
+ node.values = {};
691
+ node.properties = {};
692
+ cleanupAll([
693
+ ...children.map((child) => () => this.dispose(child)),
694
+ ...cleanups,
695
+ () => adapter?.destroy?.(),
696
+ () => {
697
+ if (!destroyView || !view || view.destroyed)
698
+ return;
699
+ if (view instanceof Window)
700
+ view.close();
701
+ else {
702
+ view.parent?.removeChild(view);
703
+ view.destroy({ children: true });
704
+ }
705
+ },
706
+ ]);
707
+ }
708
+ dispose(node) {
709
+ this.nodes.delete(node.path);
710
+ this.unmount(node);
711
+ }
712
+ assertAlive() {
713
+ if (this.destroyed)
714
+ throw new Error('presentation is destroyed');
715
+ }
716
+ destroy() {
717
+ if (this.destroyed)
718
+ return;
719
+ this.destroyed = true;
720
+ const unsubscribe = this.unsubscribe;
721
+ this.unsubscribe = undefined;
722
+ Input.onAction.remove(this.inputListener);
723
+ cleanupAll([
724
+ () => unsubscribe?.(),
725
+ ...this.roots.map((node) => () => this.dispose(node)),
726
+ () => {
727
+ this.nodes.clear();
728
+ this.focused = null;
729
+ this.root.parent?.removeChild(this.root);
730
+ this.root.destroy({ children: true });
731
+ },
732
+ () => {
733
+ if (theme() === this.appliedTheme)
734
+ setTheme(this.previousTheme);
735
+ },
736
+ ]);
737
+ }
738
+ }
739
+ function cleanupAll(tasks) {
740
+ const errors = [];
741
+ for (const task of tasks) {
742
+ try {
743
+ task();
744
+ }
745
+ catch (error) {
746
+ errors.push(error);
747
+ }
748
+ }
749
+ if (errors.length)
750
+ throw new AggregateError(errors, 'presentation cleanup failed');
751
+ }
752
+ function equalProperties(a, b) {
753
+ return Object.keys(a).length === Object.keys(b).length && Object.keys(b).every((key) => a[key] === b[key]);
754
+ }
755
+ //# sourceMappingURL=MwlPresentation.js.map