@datamoc/mw_games 0.36.1 → 0.38.0

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