@energy8platform/game-engine 0.20.0 → 0.22.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 (93) hide show
  1. package/README.md +121 -0
  2. package/dist/animation.cjs.js +18 -3
  3. package/dist/animation.cjs.js.map +1 -1
  4. package/dist/animation.esm.js +18 -3
  5. package/dist/animation.esm.js.map +1 -1
  6. package/dist/core.cjs.js +18 -3
  7. package/dist/core.cjs.js.map +1 -1
  8. package/dist/core.esm.js +18 -3
  9. package/dist/core.esm.js.map +1 -1
  10. package/dist/host.cjs.js +18 -3
  11. package/dist/host.cjs.js.map +1 -1
  12. package/dist/host.d.ts +3 -5
  13. package/dist/host.esm.js +18 -3
  14. package/dist/host.esm.js.map +1 -1
  15. package/dist/index.cjs.js +18 -2535
  16. package/dist/index.cjs.js.map +1 -1
  17. package/dist/index.d.ts +3 -1020
  18. package/dist/index.esm.js +20 -2525
  19. package/dist/index.esm.js.map +1 -1
  20. package/dist/slot.cjs.js +1872 -61
  21. package/dist/slot.cjs.js.map +1 -1
  22. package/dist/slot.d.ts +555 -28
  23. package/dist/slot.esm.js +1857 -62
  24. package/dist/slot.esm.js.map +1 -1
  25. package/dist/vite.cjs.js +0 -5
  26. package/dist/vite.cjs.js.map +1 -1
  27. package/dist/vite.esm.js +0 -5
  28. package/dist/vite.esm.js.map +1 -1
  29. package/package.json +2 -35
  30. package/src/animation/Tween.ts +14 -3
  31. package/src/host/index.ts +0 -1
  32. package/src/host/types.ts +0 -3
  33. package/src/index.ts +0 -28
  34. package/src/slot/anim/easing-map.ts +16 -2
  35. package/src/slot/cascade/TumbleController.ts +230 -0
  36. package/src/slot/config/ReelSystemConfig.ts +559 -0
  37. package/src/slot/config/presets.ts +222 -0
  38. package/src/slot/features/extra.ts +189 -0
  39. package/src/slot/features/index.ts +44 -0
  40. package/src/slot/features/symbols.ts +183 -0
  41. package/src/slot/features/types.ts +136 -0
  42. package/src/slot/features/wilds.ts +151 -0
  43. package/src/slot/grid/ReelGrid.ts +93 -24
  44. package/src/slot/grid/SymbolCell.ts +45 -11
  45. package/src/slot/index.ts +61 -2
  46. package/src/slot/motion/AnticipationController.ts +96 -0
  47. package/src/slot/motion/SpinEngine.ts +384 -0
  48. package/src/slot/system/ReelSystem.ts +295 -0
  49. package/src/vite/index.ts +0 -5
  50. package/dist/react-jsx.cjs.js +0 -3
  51. package/dist/react-jsx.cjs.js.map +0 -1
  52. package/dist/react-jsx.d.ts +0 -602
  53. package/dist/react-jsx.esm.js +0 -2
  54. package/dist/react-jsx.esm.js.map +0 -1
  55. package/dist/react.cjs.js +0 -3787
  56. package/dist/react.cjs.js.map +0 -1
  57. package/dist/react.d.ts +0 -1467
  58. package/dist/react.esm.js +0 -3771
  59. package/dist/react.esm.js.map +0 -1
  60. package/dist/ui.cjs.js +0 -2999
  61. package/dist/ui.cjs.js.map +0 -1
  62. package/dist/ui.d.ts +0 -1116
  63. package/dist/ui.esm.js +0 -2983
  64. package/dist/ui.esm.js.map +0 -1
  65. package/src/react/EngineContext.ts +0 -26
  66. package/src/react/ReactScene.ts +0 -88
  67. package/src/react/applyProps.ts +0 -271
  68. package/src/react/catalogue.ts +0 -17
  69. package/src/react/createPixiRoot.ts +0 -31
  70. package/src/react/extendAll.ts +0 -74
  71. package/src/react/hooks.ts +0 -46
  72. package/src/react/index.ts +0 -29
  73. package/src/react/jsx-runtime.ts +0 -346
  74. package/src/react/reconciler.ts +0 -338
  75. package/src/slot/freeSpins/FreeSpinsSession.ts +0 -40
  76. package/src/state/StateMachine.ts +0 -231
  77. package/src/state/index.ts +0 -1
  78. package/src/ui/BalanceDisplay.ts +0 -159
  79. package/src/ui/Button.ts +0 -304
  80. package/src/ui/FlexContainer.ts +0 -775
  81. package/src/ui/Label.ts +0 -124
  82. package/src/ui/LabelValue.ts +0 -122
  83. package/src/ui/Layout.ts +0 -291
  84. package/src/ui/Modal.ts +0 -170
  85. package/src/ui/Panel.ts +0 -204
  86. package/src/ui/ProgressBar.ts +0 -170
  87. package/src/ui/ScrollContainer.ts +0 -478
  88. package/src/ui/Slider.ts +0 -241
  89. package/src/ui/Toast.ts +0 -150
  90. package/src/ui/Toggle.ts +0 -201
  91. package/src/ui/WinDisplay.ts +0 -145
  92. package/src/ui/index.ts +0 -33
  93. package/src/ui/view.ts +0 -28
@@ -1,775 +0,0 @@
1
- import { Container } from 'pixi.js';
2
-
3
- // ─── Types ───────────────────────────────────────────────
4
-
5
- export type FlexDirection = 'row' | 'column';
6
- export type JustifyContent = 'start' | 'center' | 'end' | 'space-between' | 'space-around';
7
- export type AlignItems = 'start' | 'center' | 'end' | 'stretch';
8
- export type AlignContent = 'start' | 'center' | 'end' | 'space-between' | 'stretch';
9
-
10
- export type AlignSelf = 'auto' | 'start' | 'center' | 'end' | 'stretch';
11
-
12
- export interface FlexItemConfig {
13
- /** Flex grow factor (0 = fixed size) */
14
- flexGrow?: number;
15
- /** Flex shrink factor (0 = don't shrink, default: 1) */
16
- flexShrink?: number;
17
- /** Explicit width override for layout calculations */
18
- layoutWidth?: number | string;
19
- /** Explicit height override for layout calculations */
20
- layoutHeight?: number | string;
21
- /** Override parent's alignItems for this child */
22
- alignSelf?: AlignSelf;
23
- /**
24
- * Exclude from flex layout (acts like position: absolute).
25
- *
26
- * Positioning props (`top`/`right`/`bottom`/`left`/`centerX`/`centerY`) describe
27
- * the **visual bounding rectangle**, so children with a centered internal
28
- * origin (Button, Label — `anchor = 0.5`) are positioned intuitively.
29
- * `left: 100` places the visible left edge at 100px, regardless of origin.
30
- */
31
- flexExclude?: boolean;
32
- /** Distance from top edge of parent content area to the child's visual top */
33
- top?: number;
34
- /** Distance from right edge of parent content area to the child's visual right */
35
- right?: number;
36
- /** Distance from bottom edge of parent content area to the child's visual bottom */
37
- bottom?: number;
38
- /** Distance from left edge of parent content area to the child's visual left */
39
- left?: number;
40
- /**
41
- * X coordinate of the child's visual center within parent content area.
42
- * Accepts pixels (`200`) or a percentage of parent content width (`'50%'`).
43
- * Takes precedence over `left`/`right`.
44
- */
45
- centerX?: number | string;
46
- /**
47
- * Y coordinate of the child's visual center within parent content area.
48
- * Accepts pixels or a percentage of parent content height (`'50%'`).
49
- * Takes precedence over `top`/`bottom`.
50
- */
51
- centerY?: number | string;
52
- }
53
-
54
- export interface FlexContainerConfig {
55
- /** Layout direction (default: 'row') */
56
- direction?: FlexDirection;
57
- /** Main-axis distribution (default: 'start') */
58
- justifyContent?: JustifyContent;
59
- /** Cross-axis alignment (default: 'start') */
60
- alignItems?: AlignItems;
61
- /** Gap between children in pixels (default: 0) */
62
- gap?: number;
63
- /** Padding [top, right, bottom, left] or single number (default: 0) */
64
- padding?: number | [number, number, number, number];
65
- /** Individual padding overrides (take priority over `padding`) */
66
- paddingTop?: number;
67
- paddingRight?: number;
68
- paddingBottom?: number;
69
- paddingLeft?: number;
70
- /** Enable wrapping to next line (default: false) */
71
- flexWrap?: boolean;
72
- /** Distribution of lines along the cross axis when wrapping (default: 'start') */
73
- alignContent?: AlignContent;
74
- /** Maximum width before wrapping (only with flexWrap) */
75
- maxWidth?: number;
76
- /** Maximum height before wrapping (only with flexWrap + column) */
77
- maxHeight?: number;
78
- /** Explicit container width — number in pixels, or string percentage (e.g. "50%") resolved against parent */
79
- width?: number | string;
80
- /** Explicit container height — number in pixels, or string percentage (e.g. "50%") resolved against parent */
81
- height?: number | string;
82
- }
83
-
84
- // ─── Helpers ─────────────────────────────────────────────
85
-
86
- function normalizePadding(p: number | [number, number, number, number]): [number, number, number, number] {
87
- return typeof p === 'number' ? [p, p, p, p] : p;
88
- }
89
-
90
- /** Resolve padding from config: individual props override the base `padding` value */
91
- function resolvePadding(config: FlexContainerConfig): [number, number, number, number] {
92
- const base = normalizePadding(config.padding ?? 0);
93
- return [
94
- config.paddingTop ?? base[0],
95
- config.paddingRight ?? base[1],
96
- config.paddingBottom ?? base[2],
97
- config.paddingLeft ?? base[3],
98
- ];
99
- }
100
-
101
- /** Resolve a dimension value — number passes through, "50%" resolves against reference */
102
- function resolveDimension(value: number | string | undefined, reference: number): number | undefined {
103
- if (value === undefined) return undefined;
104
- if (typeof value === 'number') return value;
105
- if (typeof value === 'string' && value.endsWith('%')) {
106
- const pct = parseFloat(value);
107
- if (!isNaN(pct) && reference > 0 && isFinite(reference)) return (pct / 100) * reference;
108
- }
109
- return undefined;
110
- }
111
-
112
- /** Measure a child's size and bounds offset for layout purposes */
113
- function measureChild(
114
- child: Container & { _flexConfig?: FlexItemConfig },
115
- parentContentW = 0,
116
- parentContentH = 0,
117
- ): { w: number; h: number; ox: number; oy: number } {
118
- const cfg = child._flexConfig;
119
- const resolvedLW = resolveDimension(cfg?.layoutWidth, parentContentW);
120
- const resolvedLH = resolveDimension(cfg?.layoutHeight, parentContentH);
121
-
122
- // FlexContainer children are top-left origin by construction, so `ox/oy = 0`
123
- // is correct and we can skip the `getLocalBounds()` call entirely.
124
- if (child instanceof FlexContainer) {
125
- const fc = child;
126
- const w = fc._explicitWidth > 0 ? fc._explicitWidth : (fc._computedWidth > 0 ? fc._computedWidth : undefined);
127
- const h = fc._explicitHeight > 0 ? fc._explicitHeight : (fc._computedHeight > 0 ? fc._computedHeight : undefined);
128
- if (w !== undefined && h !== undefined) {
129
- return { w: resolvedLW ?? w, h: resolvedLH ?? h, ox: 0, oy: 0 };
130
- }
131
- }
132
-
133
- // Use localBounds to get the true visual origin offset.
134
- // We take `ox/oy` from bounds unconditionally — a user-supplied `layoutWidth/Height`
135
- // overrides the SIZE used by flex (protection against pre-paint bounds of 0), but
136
- // it must NOT erase the center-anchor compensation: Button/Label views are drawn at
137
- // `(-w/2..w/2)` and their bounds.x = -w/2 is what lets `layoutLine` place the
138
- // visible rectangle where the layout intends.
139
- const bounds = child.getLocalBounds();
140
- return {
141
- w: resolvedLW ?? bounds.width,
142
- h: resolvedLH ?? bounds.height,
143
- ox: bounds.x,
144
- oy: bounds.y,
145
- };
146
- }
147
-
148
- // ─── Layout items within a single line ───────────────────
149
-
150
- interface LineItem {
151
- child: Container & { _flexConfig?: FlexItemConfig };
152
- w: number;
153
- h: number;
154
- /** Local bounds origin offset (x) — compensates for centered anchors */
155
- ox: number;
156
- /** Local bounds origin offset (y) */
157
- oy: number;
158
- }
159
-
160
- /**
161
- * Set a child's main or cross dimension.
162
- * For FlexContainer children, calls resize() to trigger internal relayout
163
- * instead of the PixiJS scale setter.
164
- */
165
- function setChildMainSize(child: Container, isRow: boolean, mainSize: number, item: LineItem): void {
166
- if (child instanceof FlexContainer) {
167
- const fc = child;
168
- fc.resize(
169
- isRow ? mainSize : fc._explicitWidth || fc._computedWidth,
170
- isRow ? fc._explicitHeight || fc._computedHeight : mainSize,
171
- );
172
- } else {
173
- if (isRow) {
174
- child.width = mainSize;
175
- } else {
176
- child.height = mainSize;
177
- }
178
- }
179
- if (isRow) item.w = mainSize;
180
- else item.h = mainSize;
181
- }
182
-
183
- function setChildCrossSize(child: Container, isRow: boolean, crossSize: number): void {
184
- if (child instanceof FlexContainer) {
185
- const fc = child;
186
- fc.resize(
187
- isRow ? fc._explicitWidth || fc._computedWidth : crossSize,
188
- isRow ? crossSize : fc._explicitHeight || fc._computedHeight,
189
- );
190
- } else {
191
- if (isRow) {
192
- child.height = crossSize;
193
- } else {
194
- child.width = crossSize;
195
- }
196
- }
197
- }
198
-
199
- function layoutLine(
200
- items: LineItem[],
201
- isRow: boolean,
202
- mainSize: number,
203
- justify: JustifyContent,
204
- align: AlignItems,
205
- gap: number,
206
- crossOffset: number,
207
- crossSize: number,
208
- ): void {
209
- if (items.length === 0) return;
210
-
211
- // Compute total fixed main size and flex grow total
212
- let totalFixed = 0;
213
- let totalGrow = 0;
214
- for (const item of items) {
215
- const grow = item.child._flexConfig?.flexGrow ?? 0;
216
- if (grow > 0) {
217
- totalGrow += grow;
218
- } else {
219
- totalFixed += isRow ? item.w : item.h;
220
- }
221
- }
222
-
223
- const totalGap = gap * (items.length - 1);
224
- const availableForFlex = Math.max(0, mainSize - totalFixed - totalGap);
225
-
226
- // Resolve flex sizes
227
- if (totalGrow > 0) {
228
- for (const item of items) {
229
- const grow = item.child._flexConfig?.flexGrow ?? 0;
230
- if (grow > 0) {
231
- const flexSize = (grow / totalGrow) * availableForFlex;
232
- setChildMainSize(item.child, isRow, flexSize, item);
233
- }
234
- }
235
- }
236
-
237
- // Shrink: if content overflows and mainSize is finite, shrink eligible items
238
- if (totalGrow === 0 && mainSize > 0) {
239
- const overflow = totalFixed + totalGap - mainSize;
240
- if (overflow > 0) {
241
- let totalShrinkable = 0;
242
- for (const item of items) {
243
- const shrink = item.child._flexConfig?.flexShrink ?? 1;
244
- if (shrink > 0) {
245
- totalShrinkable += isRow ? item.w : item.h;
246
- }
247
- }
248
- if (totalShrinkable > 0) {
249
- for (const item of items) {
250
- const shrink = item.child._flexConfig?.flexShrink ?? 1;
251
- if (shrink > 0) {
252
- const itemMain = isRow ? item.w : item.h;
253
- const reduction = overflow * (itemMain / totalShrinkable);
254
- const newSize = Math.max(0, itemMain - reduction);
255
- setChildMainSize(item.child, isRow, newSize, item);
256
- }
257
- }
258
- }
259
- }
260
- }
261
-
262
- // Calculate total main size after flex
263
- let totalMain = totalGap;
264
- for (const item of items) {
265
- totalMain += isRow ? item.w : item.h;
266
- }
267
-
268
- // Justify: compute starting offset and extra spacing
269
- let mainOffset = 0;
270
- let extraGap = 0;
271
-
272
- switch (justify) {
273
- case 'start':
274
- break;
275
- case 'center':
276
- mainOffset = Math.max(0, (mainSize - totalMain) / 2);
277
- break;
278
- case 'end':
279
- mainOffset = Math.max(0, mainSize - totalMain);
280
- break;
281
- case 'space-between':
282
- if (items.length > 1) {
283
- extraGap = Math.max(0, (mainSize - totalMain + totalGap) / (items.length - 1)) - gap;
284
- }
285
- break;
286
- case 'space-around':
287
- if (items.length > 0) {
288
- const totalSpace = Math.max(0, mainSize - totalMain + totalGap);
289
- const segment = totalSpace / items.length;
290
- mainOffset = segment / 2;
291
- extraGap = segment - gap;
292
- }
293
- break;
294
- }
295
-
296
- // Position each item
297
- let pos = mainOffset;
298
- for (const item of items) {
299
- const mainDim = isRow ? item.w : item.h;
300
- const crossDim = isRow ? item.h : item.w;
301
-
302
- // Cross-axis alignment (alignSelf overrides align)
303
- const effectiveAlign = (item.child._flexConfig?.alignSelf && item.child._flexConfig.alignSelf !== 'auto')
304
- ? item.child._flexConfig.alignSelf
305
- : align;
306
- let crossPos = crossOffset;
307
- switch (effectiveAlign) {
308
- case 'start':
309
- break;
310
- case 'center':
311
- crossPos += (crossSize - crossDim) / 2;
312
- break;
313
- case 'end':
314
- crossPos += crossSize - crossDim;
315
- break;
316
- case 'stretch':
317
- setChildCrossSize(item.child, isRow, crossSize);
318
- break;
319
- }
320
-
321
- // Compensate for local bounds offset (e.g. centered anchors)
322
- if (isRow) {
323
- item.child.x = pos - item.ox;
324
- item.child.y = crossPos - item.oy;
325
- } else {
326
- item.child.x = crossPos - item.ox;
327
- item.child.y = pos - item.oy;
328
- }
329
-
330
- pos += mainDim + gap + extraGap;
331
- }
332
- }
333
-
334
- // ─── FlexContainer ───────────────────────────────────────
335
-
336
- /**
337
- * Lightweight flexbox-like layout container for PixiJS.
338
- *
339
- * Supports row/column direction, justify/align, gap, padding, wrapping,
340
- * and flex-grow distribution. Zero external dependencies.
341
- *
342
- * @example
343
- * ```ts
344
- * const toolbar = new FlexContainer({
345
- * direction: 'row',
346
- * justifyContent: 'space-between',
347
- * alignItems: 'center',
348
- * gap: 16,
349
- * padding: 12,
350
- * });
351
- *
352
- * toolbar.addFlexChild(button1);
353
- * toolbar.addFlexChild(button2);
354
- * toolbar.resize(800, 60);
355
- * ```
356
- */
357
- export class FlexContainer extends Container {
358
- readonly __uiComponent = true as const;
359
-
360
- private _config: Required<Pick<FlexContainerConfig, 'direction' | 'justifyContent' | 'alignItems' | 'gap' | 'flexWrap' | 'alignContent'>>;
361
- private _padding: [number, number, number, number];
362
- private _maxWidth: number;
363
- private _maxHeight: number;
364
- /** @internal */ _explicitWidth: number;
365
- /** @internal */ _explicitHeight: number;
366
- /** @internal */ _computedWidth = 0;
367
- /** @internal */ _computedHeight = 0;
368
- /** @internal */ _availableWidth = 0;
369
- /** @internal */ _availableHeight = 0;
370
- /** @internal */ _rawWidth: number | string;
371
- /** @internal */ _rawHeight: number | string;
372
- private _layoutChildren: (Container & { _flexConfig?: FlexItemConfig })[] = [];
373
- private _layoutDirty = true;
374
- private _layoutSuspended = false;
375
-
376
- constructor(config: FlexContainerConfig = {}) {
377
- super();
378
-
379
- this._config = {
380
- direction: config.direction ?? 'row',
381
- justifyContent: config.justifyContent ?? 'start',
382
- alignItems: config.alignItems ?? 'start',
383
- gap: config.gap ?? 0,
384
- flexWrap: config.flexWrap ?? false,
385
- alignContent: config.alignContent ?? 'start',
386
- };
387
-
388
- this._padding = resolvePadding(config);
389
- this._maxWidth = config.maxWidth ?? Infinity;
390
- this._maxHeight = config.maxHeight ?? Infinity;
391
- this._rawWidth = config.width ?? 0;
392
- this._rawHeight = config.height ?? 0;
393
- this._explicitWidth = typeof this._rawWidth === 'number' ? this._rawWidth : 0;
394
- this._explicitHeight = typeof this._rawHeight === 'number' ? this._rawHeight : 0;
395
- }
396
-
397
- // ─── Public API ──────────────────────────────────────
398
-
399
- /** Add a child with optional flex config. Also registers in flex layout. */
400
- addFlexChild(child: Container, flexConfig?: FlexItemConfig): this {
401
- if (flexConfig) (child as any)._flexConfig = flexConfig;
402
- if (!this._layoutChildren.includes(child as any)) {
403
- this._layoutChildren.push(child as any);
404
- this._layoutDirty = true;
405
- }
406
- super.addChild(child);
407
- return this;
408
- }
409
-
410
- /** Remove a child from flex layout and display list */
411
- removeFlexChild(child: Container): this {
412
- const idx = this._layoutChildren.indexOf(child as any);
413
- if (idx !== -1) {
414
- this._layoutChildren.splice(idx, 1);
415
- this._layoutDirty = true;
416
- }
417
- super.removeChild(child);
418
- return this;
419
- }
420
-
421
- /** Remove all flex children */
422
- clearFlexChildren(): this {
423
- for (const child of this._layoutChildren) {
424
- super.removeChild(child);
425
- }
426
- this._layoutChildren.length = 0;
427
- this._layoutDirty = true;
428
- return this;
429
- }
430
-
431
- /**
432
- * Override addChild so children automatically participate in flex layout.
433
- * This enables declarative usage from React JSX.
434
- */
435
- override addChild<T extends Container>(...children: T[]): T {
436
- for (const child of children) {
437
- if (!this._layoutChildren.includes(child as any)) {
438
- this._layoutChildren.push(child as any);
439
- this._layoutDirty = true;
440
- }
441
- }
442
- const result = super.addChild(...children);
443
- if (this._layoutDirty && !this._layoutSuspended) this.updateLayout();
444
- return result;
445
- }
446
-
447
- override addChildAt<T extends Container>(child: T, index: number): T {
448
- if (!this._layoutChildren.includes(child as any)) {
449
- // Insert into layout children at matching position
450
- const layoutIndex = Math.min(index, this._layoutChildren.length);
451
- this._layoutChildren.splice(layoutIndex, 0, child as any);
452
- this._layoutDirty = true;
453
- }
454
- const result = super.addChildAt(child, index);
455
- if (this._layoutDirty && !this._layoutSuspended) this.updateLayout();
456
- return result;
457
- }
458
-
459
- override removeChild<T extends Container>(...children: T[]): T {
460
- for (const child of children) {
461
- const idx = this._layoutChildren.indexOf(child as any);
462
- if (idx !== -1) {
463
- this._layoutChildren.splice(idx, 1);
464
- this._layoutDirty = true;
465
- }
466
- }
467
- return super.removeChild(...children);
468
- }
469
-
470
- /** Get all flex layout children (read-only) */
471
- get flexChildren(): readonly Container[] {
472
- return this._layoutChildren;
473
- }
474
-
475
- /** Suspend automatic layout recalculation. Call resumeLayout() to flush. */
476
- suspendLayout(): void {
477
- this._layoutSuspended = true;
478
- }
479
-
480
- /** Resume automatic layout and flush if dirty. */
481
- resumeLayout(): void {
482
- this._layoutSuspended = false;
483
- if (this._layoutDirty) this.updateLayout();
484
- }
485
-
486
- /** Update the container size and recalculate layout */
487
- resize(width: number, height: number): void {
488
- this._explicitWidth = width;
489
- this._explicitHeight = height;
490
- this._layoutDirty = true;
491
- if (!this._layoutSuspended) this.updateLayout();
492
- }
493
-
494
- /** Update layout direction */
495
- setDirection(direction: FlexDirection): void {
496
- this._config.direction = direction;
497
- this._layoutDirty = true;
498
- }
499
-
500
- /** Update justifyContent */
501
- setJustifyContent(justify: JustifyContent): void {
502
- this._config.justifyContent = justify;
503
- this._layoutDirty = true;
504
- }
505
-
506
- /** Update alignItems */
507
- setAlignItems(align: AlignItems): void {
508
- this._config.alignItems = align;
509
- this._layoutDirty = true;
510
- }
511
-
512
- /** Update gap */
513
- setGap(gap: number): void {
514
- this._config.gap = gap;
515
- this._layoutDirty = true;
516
- }
517
-
518
- /** Update padding */
519
- setPadding(padding: number | [number, number, number, number]): void {
520
- this._padding = normalizePadding(padding);
521
- this._layoutDirty = true;
522
- }
523
-
524
- /**
525
- * Recalculate and apply layout positions for all children.
526
- * Called automatically by `resize()`. Call manually after
527
- * adding/removing children without resize.
528
- */
529
- updateLayout(): void {
530
- if (this._layoutSuspended) {
531
- this._layoutDirty = true;
532
- return;
533
- }
534
- this._layoutDirty = false;
535
- const { direction, justifyContent, alignItems, gap, flexWrap, alignContent } = this._config;
536
- const [pt, pr, pb, pl] = this._padding;
537
- const isRow = direction === 'row';
538
-
539
- // Resolve percentage width/height against parent's available space
540
- if (typeof this._rawWidth === 'string') {
541
- this._explicitWidth = resolveDimension(this._rawWidth, this._availableWidth) ?? 0;
542
- }
543
- if (typeof this._rawHeight === 'string') {
544
- this._explicitHeight = resolveDimension(this._rawHeight, this._availableHeight) ?? 0;
545
- }
546
-
547
- const contentW = this._explicitWidth > 0 ? this._explicitWidth - pl - pr : Infinity;
548
- const contentH = this._explicitHeight > 0 ? this._explicitHeight - pt - pb : Infinity;
549
- const mainLimit = isRow ? contentW : contentH;
550
- const crossLimit = isRow ? contentH : contentW;
551
-
552
- // Pass content area to measureChild for percentage resolution
553
- const pctRefW = contentW < Infinity ? contentW : 0;
554
- const pctRefH = contentH < Infinity ? contentH : 0;
555
-
556
- // Propagate available size to child FlexContainers and resolve their percentages
557
- for (const child of this._layoutChildren) {
558
- if (child instanceof FlexContainer) {
559
- const fc = child;
560
- fc._availableWidth = pctRefW;
561
- fc._availableHeight = pctRefH;
562
- // If child has percentage dimensions, trigger its layout to resolve them
563
- if (typeof fc._rawWidth === 'string' || typeof fc._rawHeight === 'string') {
564
- fc.updateLayout();
565
- }
566
- }
567
- }
568
-
569
- // Measure children (skip flexExclude — they position themselves)
570
- const measured: LineItem[] = [];
571
- for (const child of this._layoutChildren) {
572
- if (child._flexConfig?.flexExclude) continue;
573
- const { w, h, ox, oy } = measureChild(child, pctRefW, pctRefH);
574
- measured.push({ child, w, h, ox, oy });
575
- }
576
-
577
- // Split into lines (if wrapping)
578
- const lines: LineItem[][] = [];
579
- if (flexWrap && mainLimit < Infinity) {
580
- let currentLine: LineItem[] = [];
581
- let lineMain = 0;
582
-
583
- for (const item of measured) {
584
- const itemMain = isRow ? item.w : item.h;
585
- const wouldBe = lineMain + (currentLine.length > 0 ? gap : 0) + itemMain;
586
-
587
- if (currentLine.length > 0 && wouldBe > mainLimit) {
588
- lines.push(currentLine);
589
- currentLine = [item];
590
- lineMain = itemMain;
591
- } else {
592
- currentLine.push(item);
593
- lineMain = wouldBe;
594
- }
595
- }
596
- if (currentLine.length > 0) lines.push(currentLine);
597
- } else {
598
- lines.push(measured);
599
- }
600
-
601
- // Compute cross size per line
602
- const lineCrossSizes: number[] = lines.map((line) => {
603
- let maxCross = 0;
604
- for (const item of line) {
605
- const cross = isRow ? item.h : item.w;
606
- if (cross > maxCross) maxCross = cross;
607
- }
608
- return maxCross;
609
- });
610
-
611
- // Compute natural main size (for auto-sizing when no explicit size given)
612
- let naturalMainSize = 0;
613
- if (mainLimit === Infinity) {
614
- for (const line of lines) {
615
- let lineMain = 0;
616
- for (const item of line) {
617
- lineMain += isRow ? item.w : item.h;
618
- }
619
- lineMain += gap * Math.max(0, line.length - 1);
620
- naturalMainSize = Math.max(naturalMainSize, lineMain);
621
- }
622
- }
623
-
624
- // Effective main size: explicit if set, otherwise natural content size
625
- const effectiveMainSize = mainLimit < Infinity ? mainLimit : naturalMainSize;
626
-
627
- // Compute alignContent offsets for multi-line layouts
628
- const totalLinesCross = lineCrossSizes.reduce((s, v) => s + v, 0) + gap * Math.max(0, lines.length - 1);
629
- let acOffset = 0;
630
- let acExtraGap = 0;
631
- if (lines.length > 1 && crossLimit < Infinity) {
632
- const freeSpace = Math.max(0, crossLimit - totalLinesCross);
633
- switch (alignContent) {
634
- case 'center':
635
- acOffset = freeSpace / 2;
636
- break;
637
- case 'end':
638
- acOffset = freeSpace;
639
- break;
640
- case 'space-between':
641
- if (lines.length > 1) {
642
- acExtraGap = freeSpace / (lines.length - 1);
643
- }
644
- break;
645
- case 'stretch':
646
- if (lines.length > 0) {
647
- const extra = freeSpace / lines.length;
648
- for (let i = 0; i < lineCrossSizes.length; i++) {
649
- lineCrossSizes[i] += extra;
650
- }
651
- }
652
- break;
653
- // 'start' — no adjustment
654
- }
655
- }
656
-
657
- // Layout each line
658
- let crossOffset = (isRow ? pt : pl) + acOffset;
659
- for (let i = 0; i < lines.length; i++) {
660
- const line = lines[i];
661
- const lineCross = lineCrossSizes[i];
662
- const mainStart = isRow ? pl : pt;
663
-
664
- // Offset items by padding
665
- const tempItems = line.map((item) => ({ ...item }));
666
-
667
- // Cross size for alignment: use container cross size for single-line, line cross for multi-line
668
- const effectiveCross = lines.length === 1 && crossLimit < Infinity ? crossLimit : lineCross;
669
-
670
- layoutLine(
671
- tempItems,
672
- isRow,
673
- effectiveMainSize,
674
- mainLimit < Infinity ? justifyContent : 'start',
675
- alignItems,
676
- gap,
677
- crossOffset,
678
- effectiveCross,
679
- );
680
-
681
- // Apply main-axis padding offset
682
- for (const item of tempItems) {
683
- const origChild = line.find((l) => l.child === item.child)!;
684
- origChild.child.x = item.child.x + (isRow ? mainStart : 0);
685
- origChild.child.y = item.child.y + (isRow ? 0 : mainStart);
686
- }
687
-
688
- crossOffset += lineCross + gap + acExtraGap;
689
- }
690
-
691
- // Compute and store actual dimensions for measureChild() and getContentSize()
692
- let totalCrossNatural = 0;
693
- for (let i = 0; i < lineCrossSizes.length; i++) {
694
- totalCrossNatural += lineCrossSizes[i];
695
- if (i < lineCrossSizes.length - 1) totalCrossNatural += gap;
696
- }
697
-
698
- if (isRow) {
699
- this._computedWidth = this._explicitWidth > 0 ? this._explicitWidth : (pl + naturalMainSize + pr);
700
- this._computedHeight = this._explicitHeight > 0 ? this._explicitHeight : (pt + totalCrossNatural + pb);
701
- } else {
702
- this._computedWidth = this._explicitWidth > 0 ? this._explicitWidth : (pl + totalCrossNatural + pr);
703
- this._computedHeight = this._explicitHeight > 0 ? this._explicitHeight : (pt + naturalMainSize + pb);
704
- }
705
-
706
- // Position flexExclude children (absolute positioning).
707
- // All position props describe the visual (bounds) rectangle. We derive the
708
- // visual rectangle directly from `getLocalBounds()` rather than the
709
- // layout-config `w`/`h` returned by measureChild — those may differ from
710
- // actual bounds when the user pins `layoutWidth`/`layoutHeight` to a value
711
- // that doesn't match the child's real visual extent (e.g. a Button whose
712
- // text overflows its configured `width`). Mixing layout size with real
713
- // bounds-origin would shift the visual center off the requested point.
714
- // `centerX/centerY` take precedence over `left/right` / `top/bottom` on each axis.
715
- for (const child of this._layoutChildren) {
716
- if (!child._flexConfig?.flexExclude) continue;
717
- const cfg = child._flexConfig;
718
- const bounds = child.getLocalBounds();
719
- const cw = this._computedWidth;
720
- const ch = this._computedHeight;
721
-
722
- const centerX = resolveDimension(cfg.centerX, pctRefW);
723
- if (centerX !== undefined) child.x = centerX - bounds.x - bounds.width / 2;
724
- else if (cfg.left !== undefined) child.x = cfg.left - bounds.x;
725
- else if (cfg.right !== undefined) child.x = cw - cfg.right - bounds.x - bounds.width;
726
-
727
- const centerY = resolveDimension(cfg.centerY, pctRefH);
728
- if (centerY !== undefined) child.y = centerY - bounds.y - bounds.height / 2;
729
- else if (cfg.top !== undefined) child.y = cfg.top - bounds.y;
730
- else if (cfg.bottom !== undefined) child.y = ch - cfg.bottom - bounds.y - bounds.height;
731
- }
732
- }
733
-
734
- /** Computed content size (after layout) */
735
- getContentSize(): { width: number; height: number } {
736
- if (this._layoutDirty) this.updateLayout();
737
- return { width: this._computedWidth, height: this._computedHeight };
738
- }
739
-
740
- /** React reconciler update hook — applies changed config props */
741
- updateConfig(changed: Record<string, any>): void {
742
- if ('direction' in changed) this.setDirection(changed.direction);
743
- if ('justifyContent' in changed) this.setJustifyContent(changed.justifyContent);
744
- if ('alignItems' in changed) this.setAlignItems(changed.alignItems);
745
- if ('gap' in changed) this.setGap(changed.gap);
746
- if ('padding' in changed || 'paddingTop' in changed || 'paddingRight' in changed || 'paddingBottom' in changed || 'paddingLeft' in changed) {
747
- this._padding = resolvePadding(changed as FlexContainerConfig);
748
- this._layoutDirty = true;
749
- }
750
- if ('flexWrap' in changed) { this._config.flexWrap = changed.flexWrap; this._layoutDirty = true; }
751
- if ('alignContent' in changed) { this._config.alignContent = changed.alignContent; this._layoutDirty = true; }
752
- if ('width' in changed || 'height' in changed) {
753
- const w = changed.width ?? this._rawWidth;
754
- const h = changed.height ?? this._rawHeight;
755
- this._rawWidth = w;
756
- this._rawHeight = h;
757
- if (typeof w === 'number' && typeof h === 'number') {
758
- this.resize(w, h);
759
- } else {
760
- // Percentage — will resolve in updateLayout
761
- this._explicitWidth = typeof w === 'number' ? w : 0;
762
- this._explicitHeight = typeof h === 'number' ? h : 0;
763
- this._layoutDirty = true;
764
- if (!this._layoutSuspended) this.updateLayout();
765
- }
766
- return;
767
- }
768
- if (this._layoutDirty && !this._layoutSuspended) this.updateLayout();
769
- }
770
-
771
- override destroy(options?: boolean | { children?: boolean; texture?: boolean; textureSource?: boolean }): void {
772
- this._layoutChildren.length = 0;
773
- super.destroy(options);
774
- }
775
- }