@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.
- package/README.md +121 -0
- package/dist/animation.cjs.js +18 -3
- package/dist/animation.cjs.js.map +1 -1
- package/dist/animation.esm.js +18 -3
- package/dist/animation.esm.js.map +1 -1
- package/dist/core.cjs.js +18 -3
- package/dist/core.cjs.js.map +1 -1
- package/dist/core.esm.js +18 -3
- package/dist/core.esm.js.map +1 -1
- package/dist/host.cjs.js +18 -3
- package/dist/host.cjs.js.map +1 -1
- package/dist/host.d.ts +3 -5
- package/dist/host.esm.js +18 -3
- package/dist/host.esm.js.map +1 -1
- package/dist/index.cjs.js +18 -2535
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +3 -1020
- package/dist/index.esm.js +20 -2525
- package/dist/index.esm.js.map +1 -1
- package/dist/slot.cjs.js +1872 -61
- package/dist/slot.cjs.js.map +1 -1
- package/dist/slot.d.ts +555 -28
- package/dist/slot.esm.js +1857 -62
- package/dist/slot.esm.js.map +1 -1
- package/dist/vite.cjs.js +0 -5
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.esm.js +0 -5
- package/dist/vite.esm.js.map +1 -1
- package/package.json +2 -35
- package/src/animation/Tween.ts +14 -3
- package/src/host/index.ts +0 -1
- package/src/host/types.ts +0 -3
- package/src/index.ts +0 -28
- package/src/slot/anim/easing-map.ts +16 -2
- package/src/slot/cascade/TumbleController.ts +230 -0
- package/src/slot/config/ReelSystemConfig.ts +559 -0
- package/src/slot/config/presets.ts +222 -0
- package/src/slot/features/extra.ts +189 -0
- package/src/slot/features/index.ts +44 -0
- package/src/slot/features/symbols.ts +183 -0
- package/src/slot/features/types.ts +136 -0
- package/src/slot/features/wilds.ts +151 -0
- package/src/slot/grid/ReelGrid.ts +93 -24
- package/src/slot/grid/SymbolCell.ts +45 -11
- package/src/slot/index.ts +61 -2
- package/src/slot/motion/AnticipationController.ts +96 -0
- package/src/slot/motion/SpinEngine.ts +384 -0
- package/src/slot/system/ReelSystem.ts +295 -0
- package/src/vite/index.ts +0 -5
- package/dist/react-jsx.cjs.js +0 -3
- package/dist/react-jsx.cjs.js.map +0 -1
- package/dist/react-jsx.d.ts +0 -602
- package/dist/react-jsx.esm.js +0 -2
- package/dist/react-jsx.esm.js.map +0 -1
- package/dist/react.cjs.js +0 -3787
- package/dist/react.cjs.js.map +0 -1
- package/dist/react.d.ts +0 -1467
- package/dist/react.esm.js +0 -3771
- package/dist/react.esm.js.map +0 -1
- package/dist/ui.cjs.js +0 -2999
- package/dist/ui.cjs.js.map +0 -1
- package/dist/ui.d.ts +0 -1116
- package/dist/ui.esm.js +0 -2983
- package/dist/ui.esm.js.map +0 -1
- package/src/react/EngineContext.ts +0 -26
- package/src/react/ReactScene.ts +0 -88
- package/src/react/applyProps.ts +0 -271
- package/src/react/catalogue.ts +0 -17
- package/src/react/createPixiRoot.ts +0 -31
- package/src/react/extendAll.ts +0 -74
- package/src/react/hooks.ts +0 -46
- package/src/react/index.ts +0 -29
- package/src/react/jsx-runtime.ts +0 -346
- package/src/react/reconciler.ts +0 -338
- package/src/slot/freeSpins/FreeSpinsSession.ts +0 -40
- package/src/state/StateMachine.ts +0 -231
- package/src/state/index.ts +0 -1
- package/src/ui/BalanceDisplay.ts +0 -159
- package/src/ui/Button.ts +0 -304
- package/src/ui/FlexContainer.ts +0 -775
- package/src/ui/Label.ts +0 -124
- package/src/ui/LabelValue.ts +0 -122
- package/src/ui/Layout.ts +0 -291
- package/src/ui/Modal.ts +0 -170
- package/src/ui/Panel.ts +0 -204
- package/src/ui/ProgressBar.ts +0 -170
- package/src/ui/ScrollContainer.ts +0 -478
- package/src/ui/Slider.ts +0 -241
- package/src/ui/Toast.ts +0 -150
- package/src/ui/Toggle.ts +0 -201
- package/src/ui/WinDisplay.ts +0 -145
- package/src/ui/index.ts +0 -33
- package/src/ui/view.ts +0 -28
package/src/ui/FlexContainer.ts
DELETED
|
@@ -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
|
-
}
|