@energy8platform/game-engine 0.12.0 → 0.14.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 +60 -9
- package/bin/simulate.ts +45 -5
- package/dist/index.cjs.js +215 -56
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +28 -6
- package/dist/index.esm.js +215 -56
- package/dist/index.esm.js.map +1 -1
- package/dist/lua.cjs.js +237 -0
- package/dist/lua.cjs.js.map +1 -1
- package/dist/lua.d.ts +59 -2
- package/dist/lua.esm.js +236 -2
- package/dist/lua.esm.js.map +1 -1
- package/dist/react.cjs.js +216 -57
- package/dist/react.cjs.js.map +1 -1
- package/dist/react.esm.js +216 -57
- package/dist/react.esm.js.map +1 -1
- package/dist/ui.cjs.js +215 -56
- package/dist/ui.cjs.js.map +1 -1
- package/dist/ui.d.ts +29 -7
- package/dist/ui.esm.js +215 -56
- package/dist/ui.esm.js.map +1 -1
- package/package.json +4 -2
- package/scripts/install-simulate.mjs +100 -0
- package/src/lua/NativeSimulationRunner.ts +367 -0
- package/src/lua/index.ts +7 -0
- package/src/react/jsx.d.ts +15 -3
- package/src/react/reconciler.ts +1 -1
- package/src/ui/FlexContainer.ts +241 -61
- package/src/ui/index.ts +1 -1
package/README.md
CHANGED
|
@@ -410,7 +410,7 @@ If no custom view is provided, components fall back to Graphics-based rendering
|
|
|
410
410
|
|
|
411
411
|
### FlexContainer
|
|
412
412
|
|
|
413
|
-
Lightweight flexbox-like layout container. Children added via `addChild()` automatically participate in flex layout.
|
|
413
|
+
Lightweight flexbox-like layout container. Children added via `addChild()` automatically participate in flex layout. Supports auto-sizing (no explicit `width`/`height` required), percentage dimensions, absolute positioning for excluded children, and multi-line alignment.
|
|
414
414
|
|
|
415
415
|
```typescript
|
|
416
416
|
const toolbar = new FlexContainer({
|
|
@@ -426,6 +426,32 @@ toolbar.addFlexChild(spacer, { flexGrow: 1 }); // with flex config
|
|
|
426
426
|
toolbar.resize(800, 60);
|
|
427
427
|
```
|
|
428
428
|
|
|
429
|
+
**FlexContainerConfig:**
|
|
430
|
+
|
|
431
|
+
| Property | Type | Default | Description |
|
|
432
|
+
| --- | --- | --- | --- |
|
|
433
|
+
| `direction` | `'row' \| 'column'` | `'row'` | Layout direction |
|
|
434
|
+
| `justifyContent` | `'start' \| 'center' \| 'end' \| 'space-between' \| 'space-around'` | `'start'` | Main-axis distribution |
|
|
435
|
+
| `alignItems` | `'start' \| 'center' \| 'end' \| 'stretch'` | `'start'` | Cross-axis alignment |
|
|
436
|
+
| `alignContent` | `'start' \| 'center' \| 'end' \| 'space-between' \| 'stretch'` | `'start'` | Multi-line cross-axis distribution (with `flexWrap`) |
|
|
437
|
+
| `gap` | `number` | `0` | Gap between children |
|
|
438
|
+
| `padding` | `number \| [top, right, bottom, left]` | `0` | Padding (shorthand) |
|
|
439
|
+
| `paddingTop/Right/Bottom/Left` | `number` | — | Individual padding overrides (take priority over `padding`) |
|
|
440
|
+
| `flexWrap` | `boolean` | `false` | Enable wrapping to next line |
|
|
441
|
+
| `width` | `number \| string` | — | Container width — pixels or `"50%"` relative to parent |
|
|
442
|
+
| `height` | `number \| string` | — | Container height — pixels or `"50%"` relative to parent |
|
|
443
|
+
|
|
444
|
+
**Auto-sizing:** When no explicit `width`/`height` is set, the container computes its size from content (`_computedWidth`/`_computedHeight`). Cross-axis `alignItems` works automatically (centers relative to tallest/widest child). Parent FlexContainers correctly measure auto-sized children.
|
|
445
|
+
|
|
446
|
+
**Percentage dimensions:** String values like `"100%"` or `"50%"` resolve against the parent FlexContainer's content area. Works for both container `width`/`height` and child `layoutWidth`/`layoutHeight`.
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
// Parent with explicit size, child fills 100% width and 50% height
|
|
450
|
+
const parent = new FlexContainer({ direction: 'column', width: 800, height: 600 });
|
|
451
|
+
const child = new FlexContainer({ width: '100%', height: '50%', direction: 'row' });
|
|
452
|
+
parent.addFlexChild(child); // child resolves to 800×300
|
|
453
|
+
```
|
|
454
|
+
|
|
429
455
|
**FlexItemConfig** — per-child options passed via `addFlexChild(child, config)` or JSX props:
|
|
430
456
|
|
|
431
457
|
| Property | Type | Default | Description |
|
|
@@ -434,16 +460,21 @@ toolbar.resize(800, 60);
|
|
|
434
460
|
| `flexShrink` | `number` | `1` | Flex shrink factor (`0` = don't shrink when content overflows) |
|
|
435
461
|
| `alignSelf` | `'auto' \| 'start' \| 'center' \| 'end' \| 'stretch'` | `'auto'` | Override parent's `alignItems` for this child |
|
|
436
462
|
| `flexExclude` | `boolean` | `false` | Exclude from flex layout (like `position: absolute`) |
|
|
437
|
-
| `layoutWidth` | `number` | — |
|
|
438
|
-
| `layoutHeight` | `number` | — |
|
|
463
|
+
| `layoutWidth` | `number \| string` | — | Width override — pixels or `"50%"` of parent content area |
|
|
464
|
+
| `layoutHeight` | `number \| string` | — | Height override — pixels or `"50%"` of parent content area |
|
|
465
|
+
| `top` | `number` | — | Absolute positioning (only with `flexExclude`) — distance from top edge |
|
|
466
|
+
| `right` | `number` | — | Absolute positioning — distance from right edge |
|
|
467
|
+
| `bottom` | `number` | — | Absolute positioning — distance from bottom edge |
|
|
468
|
+
| `left` | `number` | — | Absolute positioning — distance from left edge |
|
|
439
469
|
|
|
440
470
|
```typescript
|
|
441
471
|
// Fixed item that won't shrink + centered override
|
|
442
472
|
toolbar.addFlexChild(logo, { flexShrink: 0 });
|
|
443
473
|
toolbar.addFlexChild(badge, { alignSelf: 'center' });
|
|
444
474
|
|
|
445
|
-
//
|
|
446
|
-
toolbar.addFlexChild(
|
|
475
|
+
// Absolute positioning (like CSS position: absolute)
|
|
476
|
+
toolbar.addFlexChild(closeBtn, { flexExclude: true, top: 8, right: 8 });
|
|
477
|
+
toolbar.addFlexChild(background, { flexExclude: true, top: 0, left: 0 });
|
|
447
478
|
```
|
|
448
479
|
|
|
449
480
|
### Layout
|
|
@@ -906,10 +937,30 @@ All engine UI components are config-based: the reconciler passes JSX props as a
|
|
|
906
937
|
|
|
907
938
|
{/* Flex item props — work on any element inside <flexContainer> */}
|
|
908
939
|
<flexContainer direction="row" width={800} height={60}>
|
|
909
|
-
<graphics draw={drawBg} flexExclude />
|
|
910
|
-
<label text="Logo" flexShrink={0} />
|
|
911
|
-
<container flexGrow={1} />
|
|
912
|
-
<button text="Menu" alignSelf="center" />
|
|
940
|
+
<graphics draw={drawBg} flexExclude top={0} left={0} /> {/* absolute positioning */}
|
|
941
|
+
<label text="Logo" flexShrink={0} /> {/* won't shrink */}
|
|
942
|
+
<container flexGrow={1} /> {/* fills remaining space */}
|
|
943
|
+
<button text="Menu" alignSelf="center" /> {/* centered on cross-axis */}
|
|
944
|
+
<sprite texture="close" flexExclude top={4} right={4} /> {/* top-right corner */}
|
|
945
|
+
</flexContainer>
|
|
946
|
+
|
|
947
|
+
{/* Individual padding props */}
|
|
948
|
+
<flexContainer direction="column" paddingTop={20} paddingLeft={16} paddingRight={16}>
|
|
949
|
+
<label text="Content" />
|
|
950
|
+
</flexContainer>
|
|
951
|
+
|
|
952
|
+
{/* Percentage dimensions */}
|
|
953
|
+
<flexContainer direction="column" width={screen.width} height={screen.height}>
|
|
954
|
+
<flexContainer width="100%" height="50%" direction="row" alignItems="center">
|
|
955
|
+
<label text="Top half" />
|
|
956
|
+
</flexContainer>
|
|
957
|
+
</flexContainer>
|
|
958
|
+
|
|
959
|
+
{/* alignContent for wrapped lines */}
|
|
960
|
+
<flexContainer direction="row" flexWrap alignContent="center" width={400} height={400} gap={8}>
|
|
961
|
+
<button width={180} height={40} text="A" />
|
|
962
|
+
<button width={180} height={40} text="B" />
|
|
963
|
+
<button width={180} height={40} text="C" />
|
|
913
964
|
</flexContainer>
|
|
914
965
|
```
|
|
915
966
|
|
package/bin/simulate.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env npx tsx
|
|
2
2
|
import { SimulationRunner, formatSimulationResult } from '../src/lua/SimulationRunner';
|
|
3
3
|
import { ParallelSimulationRunner } from '../src/lua/ParallelSimulationRunner';
|
|
4
|
+
import { NativeSimulationRunner, findNativeBinary, formatNativeResult } from '../src/lua/NativeSimulationRunner';
|
|
4
5
|
import { cpus } from 'os';
|
|
5
|
-
import { resolve } from 'path';
|
|
6
|
+
import { resolve, dirname } from 'path';
|
|
6
7
|
|
|
7
8
|
// ─── Argument Parsing ───────────────────────────────────
|
|
8
9
|
|
|
@@ -10,9 +11,14 @@ function parseArgs(argv: string[]): Record<string, string> {
|
|
|
10
11
|
const args: Record<string, string> = {};
|
|
11
12
|
for (let i = 2; i < argv.length; i++) {
|
|
12
13
|
const arg = argv[i];
|
|
13
|
-
if (arg.startsWith('--')
|
|
14
|
+
if (arg.startsWith('--')) {
|
|
14
15
|
const key = arg.slice(2);
|
|
15
|
-
|
|
16
|
+
// Boolean flags (no value)
|
|
17
|
+
if (key === 'native' || key === 'js') {
|
|
18
|
+
args[key] = 'true';
|
|
19
|
+
} else if (i + 1 < argv.length) {
|
|
20
|
+
args[key] = argv[++i];
|
|
21
|
+
}
|
|
16
22
|
}
|
|
17
23
|
}
|
|
18
24
|
return args;
|
|
@@ -28,6 +34,8 @@ async function main() {
|
|
|
28
34
|
const action = args.action ?? 'spin';
|
|
29
35
|
const params = args.params ? JSON.parse(args.params) : undefined;
|
|
30
36
|
const workers = args.workers ? parseInt(args.workers, 10) : cpus().length;
|
|
37
|
+
const useNative = args.native === 'true';
|
|
38
|
+
const useJs = args.js === 'true';
|
|
31
39
|
|
|
32
40
|
// Load dev config
|
|
33
41
|
let config: any;
|
|
@@ -52,14 +60,46 @@ async function main() {
|
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
const gameId = config.gameDefinition.id ?? 'unknown';
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
|
|
64
|
+
// ─── Native binary detection ────────────────────────────
|
|
65
|
+
// Search in config dir first, then in the game-engine package root
|
|
66
|
+
const engineRoot = resolve(__dirname, '..');
|
|
67
|
+
const binaryPath = args.binary ?? (useJs ? null : (findNativeBinary(dirname(configPath)) ?? findNativeBinary(engineRoot)));
|
|
68
|
+
|
|
69
|
+
if (useNative && !binaryPath) {
|
|
70
|
+
console.error('Native simulation binary not found.');
|
|
71
|
+
console.error('Use --binary <path> or set SIMULATE_BINARY environment variable.');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
57
74
|
|
|
58
75
|
const onProgress = (completed: number, total: number) => {
|
|
59
76
|
const pct = Math.round((completed / total) * 100);
|
|
60
77
|
console.log(`Progress: ${completed.toLocaleString()}/${total.toLocaleString()} (${pct}%)`);
|
|
61
78
|
};
|
|
62
79
|
|
|
80
|
+
// ─── Native binary path ─────────────────────────────────
|
|
81
|
+
if (binaryPath) {
|
|
82
|
+
console.log(`Using native binary: ${binaryPath}`);
|
|
83
|
+
console.log(`Starting simulation for ${gameId} (${iterations.toLocaleString()} iterations, action: ${action})...`);
|
|
84
|
+
|
|
85
|
+
const runner = new NativeSimulationRunner({
|
|
86
|
+
binaryPath,
|
|
87
|
+
script: config.luaScript,
|
|
88
|
+
gameDefinition: config.gameDefinition,
|
|
89
|
+
iterations,
|
|
90
|
+
bet,
|
|
91
|
+
action,
|
|
92
|
+
params,
|
|
93
|
+
});
|
|
94
|
+
const result = await runner.run();
|
|
95
|
+
console.log(formatNativeResult(result));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─── JS simulation path ─────────────────────────────────
|
|
100
|
+
const useParallel = workers > 1;
|
|
101
|
+
console.log(`Starting simulation for ${gameId} (${iterations.toLocaleString()} iterations, action: ${action}, workers: ${useParallel ? workers : 1})...`);
|
|
102
|
+
|
|
63
103
|
let result;
|
|
64
104
|
|
|
65
105
|
if (useParallel) {
|
package/dist/index.cjs.js
CHANGED
|
@@ -2912,26 +2912,90 @@ class SpriteAnimation {
|
|
|
2912
2912
|
function normalizePadding(p) {
|
|
2913
2913
|
return typeof p === 'number' ? [p, p, p, p] : p;
|
|
2914
2914
|
}
|
|
2915
|
+
/** Resolve padding from config: individual props override the base `padding` value */
|
|
2916
|
+
function resolvePadding(config) {
|
|
2917
|
+
const base = normalizePadding(config.padding ?? 0);
|
|
2918
|
+
return [
|
|
2919
|
+
config.paddingTop ?? base[0],
|
|
2920
|
+
config.paddingRight ?? base[1],
|
|
2921
|
+
config.paddingBottom ?? base[2],
|
|
2922
|
+
config.paddingLeft ?? base[3],
|
|
2923
|
+
];
|
|
2924
|
+
}
|
|
2925
|
+
/** Resolve a dimension value — number passes through, "50%" resolves against reference */
|
|
2926
|
+
function resolveDimension(value, reference) {
|
|
2927
|
+
if (value === undefined)
|
|
2928
|
+
return undefined;
|
|
2929
|
+
if (typeof value === 'number')
|
|
2930
|
+
return value;
|
|
2931
|
+
if (typeof value === 'string' && value.endsWith('%')) {
|
|
2932
|
+
const pct = parseFloat(value);
|
|
2933
|
+
if (!isNaN(pct) && reference > 0 && isFinite(reference))
|
|
2934
|
+
return (pct / 100) * reference;
|
|
2935
|
+
}
|
|
2936
|
+
return undefined;
|
|
2937
|
+
}
|
|
2915
2938
|
/** Measure a child's size and bounds offset for layout purposes */
|
|
2916
|
-
function measureChild(child) {
|
|
2939
|
+
function measureChild(child, parentContentW = 0, parentContentH = 0) {
|
|
2917
2940
|
const cfg = child._flexConfig;
|
|
2918
|
-
|
|
2919
|
-
|
|
2941
|
+
const resolvedLW = resolveDimension(cfg?.layoutWidth, parentContentW);
|
|
2942
|
+
const resolvedLH = resolveDimension(cfg?.layoutHeight, parentContentH);
|
|
2943
|
+
if (resolvedLW !== undefined && resolvedLH !== undefined) {
|
|
2944
|
+
return { w: resolvedLW, h: resolvedLH, ox: 0, oy: 0 };
|
|
2920
2945
|
}
|
|
2921
|
-
// For FlexContainers, use their explicit
|
|
2946
|
+
// For FlexContainers, use their explicit or computed size
|
|
2922
2947
|
if (child instanceof FlexContainer) {
|
|
2923
2948
|
const fc = child;
|
|
2924
|
-
|
|
2925
|
-
|
|
2949
|
+
const w = fc._explicitWidth > 0 ? fc._explicitWidth : (fc._computedWidth > 0 ? fc._computedWidth : undefined);
|
|
2950
|
+
const h = fc._explicitHeight > 0 ? fc._explicitHeight : (fc._computedHeight > 0 ? fc._computedHeight : undefined);
|
|
2951
|
+
if (w !== undefined && h !== undefined) {
|
|
2952
|
+
return { w, h, ox: 0, oy: 0 };
|
|
2926
2953
|
}
|
|
2927
2954
|
}
|
|
2928
2955
|
// Use localBounds to get the true visual extent and origin offset.
|
|
2929
2956
|
// This handles children with non-zero anchors (e.g. Button, Label with centered text).
|
|
2930
2957
|
const bounds = child.getLocalBounds();
|
|
2931
|
-
const w =
|
|
2932
|
-
const h =
|
|
2958
|
+
const w = resolvedLW ?? bounds.width;
|
|
2959
|
+
const h = resolvedLH ?? bounds.height;
|
|
2933
2960
|
return { w, h, ox: bounds.x, oy: bounds.y };
|
|
2934
2961
|
}
|
|
2962
|
+
/**
|
|
2963
|
+
* Set a child's main or cross dimension.
|
|
2964
|
+
* For FlexContainer children, calls resize() to trigger internal relayout
|
|
2965
|
+
* instead of the PixiJS scale setter.
|
|
2966
|
+
*/
|
|
2967
|
+
function setChildMainSize(child, isRow, mainSize, item) {
|
|
2968
|
+
if (child instanceof FlexContainer) {
|
|
2969
|
+
const fc = child;
|
|
2970
|
+
fc.resize(isRow ? mainSize : fc._explicitWidth || fc._computedWidth, isRow ? fc._explicitHeight || fc._computedHeight : mainSize);
|
|
2971
|
+
}
|
|
2972
|
+
else {
|
|
2973
|
+
if (isRow) {
|
|
2974
|
+
child.width = mainSize;
|
|
2975
|
+
}
|
|
2976
|
+
else {
|
|
2977
|
+
child.height = mainSize;
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
if (isRow)
|
|
2981
|
+
item.w = mainSize;
|
|
2982
|
+
else
|
|
2983
|
+
item.h = mainSize;
|
|
2984
|
+
}
|
|
2985
|
+
function setChildCrossSize(child, isRow, crossSize) {
|
|
2986
|
+
if (child instanceof FlexContainer) {
|
|
2987
|
+
const fc = child;
|
|
2988
|
+
fc.resize(isRow ? fc._explicitWidth || fc._computedWidth : crossSize, isRow ? crossSize : fc._explicitHeight || fc._computedHeight);
|
|
2989
|
+
}
|
|
2990
|
+
else {
|
|
2991
|
+
if (isRow) {
|
|
2992
|
+
child.height = crossSize;
|
|
2993
|
+
}
|
|
2994
|
+
else {
|
|
2995
|
+
child.width = crossSize;
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2935
2999
|
function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, crossSize) {
|
|
2936
3000
|
if (items.length === 0)
|
|
2937
3001
|
return;
|
|
@@ -2955,14 +3019,7 @@ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, cr
|
|
|
2955
3019
|
const grow = item.child._flexConfig?.flexGrow ?? 0;
|
|
2956
3020
|
if (grow > 0) {
|
|
2957
3021
|
const flexSize = (grow / totalGrow) * availableForFlex;
|
|
2958
|
-
|
|
2959
|
-
item.w = flexSize;
|
|
2960
|
-
item.child.width = flexSize;
|
|
2961
|
-
}
|
|
2962
|
-
else {
|
|
2963
|
-
item.h = flexSize;
|
|
2964
|
-
item.child.height = flexSize;
|
|
2965
|
-
}
|
|
3022
|
+
setChildMainSize(item.child, isRow, flexSize, item);
|
|
2966
3023
|
}
|
|
2967
3024
|
}
|
|
2968
3025
|
}
|
|
@@ -2984,14 +3041,7 @@ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, cr
|
|
|
2984
3041
|
const itemMain = isRow ? item.w : item.h;
|
|
2985
3042
|
const reduction = overflow * (itemMain / totalShrinkable);
|
|
2986
3043
|
const newSize = Math.max(0, itemMain - reduction);
|
|
2987
|
-
|
|
2988
|
-
item.w = newSize;
|
|
2989
|
-
item.child.width = newSize;
|
|
2990
|
-
}
|
|
2991
|
-
else {
|
|
2992
|
-
item.h = newSize;
|
|
2993
|
-
item.child.height = newSize;
|
|
2994
|
-
}
|
|
3044
|
+
setChildMainSize(item.child, isRow, newSize, item);
|
|
2995
3045
|
}
|
|
2996
3046
|
}
|
|
2997
3047
|
}
|
|
@@ -3048,12 +3098,7 @@ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, cr
|
|
|
3048
3098
|
crossPos += crossSize - crossDim;
|
|
3049
3099
|
break;
|
|
3050
3100
|
case 'stretch':
|
|
3051
|
-
|
|
3052
|
-
item.child.height = crossSize;
|
|
3053
|
-
}
|
|
3054
|
-
else {
|
|
3055
|
-
item.child.width = crossSize;
|
|
3056
|
-
}
|
|
3101
|
+
setChildCrossSize(item.child, isRow, crossSize);
|
|
3057
3102
|
break;
|
|
3058
3103
|
}
|
|
3059
3104
|
// Compensate for local bounds offset (e.g. centered anchors)
|
|
@@ -3098,6 +3143,12 @@ class FlexContainer extends pixi_js.Container {
|
|
|
3098
3143
|
_maxHeight;
|
|
3099
3144
|
/** @internal */ _explicitWidth;
|
|
3100
3145
|
/** @internal */ _explicitHeight;
|
|
3146
|
+
/** @internal */ _computedWidth = 0;
|
|
3147
|
+
/** @internal */ _computedHeight = 0;
|
|
3148
|
+
/** @internal */ _availableWidth = 0;
|
|
3149
|
+
/** @internal */ _availableHeight = 0;
|
|
3150
|
+
/** @internal */ _rawWidth;
|
|
3151
|
+
/** @internal */ _rawHeight;
|
|
3101
3152
|
_layoutChildren = [];
|
|
3102
3153
|
_layoutDirty = true;
|
|
3103
3154
|
constructor(config = {}) {
|
|
@@ -3108,12 +3159,15 @@ class FlexContainer extends pixi_js.Container {
|
|
|
3108
3159
|
alignItems: config.alignItems ?? 'start',
|
|
3109
3160
|
gap: config.gap ?? 0,
|
|
3110
3161
|
flexWrap: config.flexWrap ?? false,
|
|
3162
|
+
alignContent: config.alignContent ?? 'start',
|
|
3111
3163
|
};
|
|
3112
|
-
this._padding =
|
|
3164
|
+
this._padding = resolvePadding(config);
|
|
3113
3165
|
this._maxWidth = config.maxWidth ?? Infinity;
|
|
3114
3166
|
this._maxHeight = config.maxHeight ?? Infinity;
|
|
3115
|
-
this.
|
|
3116
|
-
this.
|
|
3167
|
+
this._rawWidth = config.width ?? 0;
|
|
3168
|
+
this._rawHeight = config.height ?? 0;
|
|
3169
|
+
this._explicitWidth = typeof this._rawWidth === 'number' ? this._rawWidth : 0;
|
|
3170
|
+
this._explicitHeight = typeof this._rawHeight === 'number' ? this._rawHeight : 0;
|
|
3117
3171
|
}
|
|
3118
3172
|
// ─── Public API ──────────────────────────────────────
|
|
3119
3173
|
/** Add a child with optional flex config. Also registers in flex layout. */
|
|
@@ -3215,19 +3269,41 @@ class FlexContainer extends pixi_js.Container {
|
|
|
3215
3269
|
*/
|
|
3216
3270
|
updateLayout() {
|
|
3217
3271
|
this._layoutDirty = false;
|
|
3218
|
-
const { direction, justifyContent, alignItems, gap, flexWrap } = this._config;
|
|
3272
|
+
const { direction, justifyContent, alignItems, gap, flexWrap, alignContent } = this._config;
|
|
3219
3273
|
const [pt, pr, pb, pl] = this._padding;
|
|
3220
3274
|
const isRow = direction === 'row';
|
|
3275
|
+
// Resolve percentage width/height against parent's available space
|
|
3276
|
+
if (typeof this._rawWidth === 'string') {
|
|
3277
|
+
this._explicitWidth = resolveDimension(this._rawWidth, this._availableWidth) ?? 0;
|
|
3278
|
+
}
|
|
3279
|
+
if (typeof this._rawHeight === 'string') {
|
|
3280
|
+
this._explicitHeight = resolveDimension(this._rawHeight, this._availableHeight) ?? 0;
|
|
3281
|
+
}
|
|
3221
3282
|
const contentW = this._explicitWidth > 0 ? this._explicitWidth - pl - pr : Infinity;
|
|
3222
3283
|
const contentH = this._explicitHeight > 0 ? this._explicitHeight - pt - pb : Infinity;
|
|
3223
3284
|
const mainLimit = isRow ? contentW : contentH;
|
|
3224
3285
|
const crossLimit = isRow ? contentH : contentW;
|
|
3286
|
+
// Pass content area to measureChild for percentage resolution
|
|
3287
|
+
const pctRefW = contentW < Infinity ? contentW : 0;
|
|
3288
|
+
const pctRefH = contentH < Infinity ? contentH : 0;
|
|
3289
|
+
// Propagate available size to child FlexContainers and resolve their percentages
|
|
3290
|
+
for (const child of this._layoutChildren) {
|
|
3291
|
+
if (child instanceof FlexContainer) {
|
|
3292
|
+
const fc = child;
|
|
3293
|
+
fc._availableWidth = pctRefW;
|
|
3294
|
+
fc._availableHeight = pctRefH;
|
|
3295
|
+
// If child has percentage dimensions, trigger its layout to resolve them
|
|
3296
|
+
if (typeof fc._rawWidth === 'string' || typeof fc._rawHeight === 'string') {
|
|
3297
|
+
fc.updateLayout();
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3225
3301
|
// Measure children (skip flexExclude — they position themselves)
|
|
3226
3302
|
const measured = [];
|
|
3227
3303
|
for (const child of this._layoutChildren) {
|
|
3228
3304
|
if (child._flexConfig?.flexExclude)
|
|
3229
3305
|
continue;
|
|
3230
|
-
const { w, h, ox, oy } = measureChild(child);
|
|
3306
|
+
const { w, h, ox, oy } = measureChild(child, pctRefW, pctRefH);
|
|
3231
3307
|
measured.push({ child, w, h, ox, oy });
|
|
3232
3308
|
}
|
|
3233
3309
|
// Split into lines (if wrapping)
|
|
@@ -3264,42 +3340,106 @@ class FlexContainer extends pixi_js.Container {
|
|
|
3264
3340
|
}
|
|
3265
3341
|
return maxCross;
|
|
3266
3342
|
});
|
|
3343
|
+
// Compute natural main size (for auto-sizing when no explicit size given)
|
|
3344
|
+
let naturalMainSize = 0;
|
|
3345
|
+
if (mainLimit === Infinity) {
|
|
3346
|
+
for (const line of lines) {
|
|
3347
|
+
let lineMain = 0;
|
|
3348
|
+
for (const item of line) {
|
|
3349
|
+
lineMain += isRow ? item.w : item.h;
|
|
3350
|
+
}
|
|
3351
|
+
lineMain += gap * Math.max(0, line.length - 1);
|
|
3352
|
+
naturalMainSize = Math.max(naturalMainSize, lineMain);
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
// Effective main size: explicit if set, otherwise natural content size
|
|
3356
|
+
const effectiveMainSize = mainLimit < Infinity ? mainLimit : naturalMainSize;
|
|
3357
|
+
// Compute alignContent offsets for multi-line layouts
|
|
3358
|
+
const totalLinesCross = lineCrossSizes.reduce((s, v) => s + v, 0) + gap * Math.max(0, lines.length - 1);
|
|
3359
|
+
let acOffset = 0;
|
|
3360
|
+
let acExtraGap = 0;
|
|
3361
|
+
if (lines.length > 1 && crossLimit < Infinity) {
|
|
3362
|
+
const freeSpace = Math.max(0, crossLimit - totalLinesCross);
|
|
3363
|
+
switch (alignContent) {
|
|
3364
|
+
case 'center':
|
|
3365
|
+
acOffset = freeSpace / 2;
|
|
3366
|
+
break;
|
|
3367
|
+
case 'end':
|
|
3368
|
+
acOffset = freeSpace;
|
|
3369
|
+
break;
|
|
3370
|
+
case 'space-between':
|
|
3371
|
+
if (lines.length > 1) {
|
|
3372
|
+
acExtraGap = freeSpace / (lines.length - 1);
|
|
3373
|
+
}
|
|
3374
|
+
break;
|
|
3375
|
+
case 'stretch':
|
|
3376
|
+
if (lines.length > 0) {
|
|
3377
|
+
const extra = freeSpace / lines.length;
|
|
3378
|
+
for (let i = 0; i < lineCrossSizes.length; i++) {
|
|
3379
|
+
lineCrossSizes[i] += extra;
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
break;
|
|
3383
|
+
// 'start' — no adjustment
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3267
3386
|
// Layout each line
|
|
3268
|
-
let crossOffset = isRow ? pt : pl;
|
|
3387
|
+
let crossOffset = (isRow ? pt : pl) + acOffset;
|
|
3269
3388
|
for (let i = 0; i < lines.length; i++) {
|
|
3270
3389
|
const line = lines[i];
|
|
3271
3390
|
const lineCross = lineCrossSizes[i];
|
|
3272
3391
|
const mainStart = isRow ? pl : pt;
|
|
3273
3392
|
// Offset items by padding
|
|
3274
3393
|
const tempItems = line.map((item) => ({ ...item }));
|
|
3275
|
-
//
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
? crossLimit
|
|
3279
|
-
: (crossLimit < Infinity ? Math.min(lineCross, crossLimit) : lineCross);
|
|
3280
|
-
layoutLine(tempItems, isRow, mainLimit < Infinity ? mainLimit : 0, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, effectiveCross);
|
|
3394
|
+
// Cross size for alignment: use container cross size for single-line, line cross for multi-line
|
|
3395
|
+
const effectiveCross = lines.length === 1 && crossLimit < Infinity ? crossLimit : lineCross;
|
|
3396
|
+
layoutLine(tempItems, isRow, effectiveMainSize, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, effectiveCross);
|
|
3281
3397
|
// Apply main-axis padding offset
|
|
3282
3398
|
for (const item of tempItems) {
|
|
3283
3399
|
const origChild = line.find((l) => l.child === item.child);
|
|
3284
3400
|
origChild.child.x = item.child.x + (isRow ? mainStart : 0);
|
|
3285
3401
|
origChild.child.y = item.child.y + (isRow ? 0 : mainStart);
|
|
3286
3402
|
}
|
|
3287
|
-
crossOffset += lineCross + gap;
|
|
3403
|
+
crossOffset += lineCross + gap + acExtraGap;
|
|
3404
|
+
}
|
|
3405
|
+
// Compute and store actual dimensions for measureChild() and getContentSize()
|
|
3406
|
+
let totalCrossNatural = 0;
|
|
3407
|
+
for (let i = 0; i < lineCrossSizes.length; i++) {
|
|
3408
|
+
totalCrossNatural += lineCrossSizes[i];
|
|
3409
|
+
if (i < lineCrossSizes.length - 1)
|
|
3410
|
+
totalCrossNatural += gap;
|
|
3411
|
+
}
|
|
3412
|
+
if (isRow) {
|
|
3413
|
+
this._computedWidth = this._explicitWidth > 0 ? this._explicitWidth : (pl + naturalMainSize + pr);
|
|
3414
|
+
this._computedHeight = this._explicitHeight > 0 ? this._explicitHeight : (pt + totalCrossNatural + pb);
|
|
3415
|
+
}
|
|
3416
|
+
else {
|
|
3417
|
+
this._computedWidth = this._explicitWidth > 0 ? this._explicitWidth : (pl + totalCrossNatural + pr);
|
|
3418
|
+
this._computedHeight = this._explicitHeight > 0 ? this._explicitHeight : (pt + naturalMainSize + pb);
|
|
3419
|
+
}
|
|
3420
|
+
// Position flexExclude children (absolute positioning)
|
|
3421
|
+
for (const child of this._layoutChildren) {
|
|
3422
|
+
if (!child._flexConfig?.flexExclude)
|
|
3423
|
+
continue;
|
|
3424
|
+
const cfg = child._flexConfig;
|
|
3425
|
+
const { w, h } = measureChild(child, pctRefW, pctRefH);
|
|
3426
|
+
const cw = this._computedWidth;
|
|
3427
|
+
const ch = this._computedHeight;
|
|
3428
|
+
if (cfg.left !== undefined)
|
|
3429
|
+
child.x = cfg.left;
|
|
3430
|
+
else if (cfg.right !== undefined)
|
|
3431
|
+
child.x = cw - w - cfg.right;
|
|
3432
|
+
if (cfg.top !== undefined)
|
|
3433
|
+
child.y = cfg.top;
|
|
3434
|
+
else if (cfg.bottom !== undefined)
|
|
3435
|
+
child.y = ch - h - cfg.bottom;
|
|
3288
3436
|
}
|
|
3289
3437
|
}
|
|
3290
3438
|
/** Computed content size (after layout) */
|
|
3291
3439
|
getContentSize() {
|
|
3292
3440
|
if (this._layoutDirty)
|
|
3293
3441
|
this.updateLayout();
|
|
3294
|
-
|
|
3295
|
-
let maxY = 0;
|
|
3296
|
-
for (const child of this._layoutChildren) {
|
|
3297
|
-
const { w, h } = measureChild(child);
|
|
3298
|
-
maxX = Math.max(maxX, child.x + w);
|
|
3299
|
-
maxY = Math.max(maxY, child.y + h);
|
|
3300
|
-
}
|
|
3301
|
-
const [, pr, pb] = this._padding;
|
|
3302
|
-
return { width: maxX + pr, height: maxY + pb };
|
|
3442
|
+
return { width: this._computedWidth, height: this._computedHeight };
|
|
3303
3443
|
}
|
|
3304
3444
|
/** React reconciler update hook — applies changed config props */
|
|
3305
3445
|
updateConfig(changed) {
|
|
@@ -3311,15 +3451,34 @@ class FlexContainer extends pixi_js.Container {
|
|
|
3311
3451
|
this.setAlignItems(changed.alignItems);
|
|
3312
3452
|
if ('gap' in changed)
|
|
3313
3453
|
this.setGap(changed.gap);
|
|
3314
|
-
if ('padding' in changed)
|
|
3315
|
-
this.
|
|
3454
|
+
if ('padding' in changed || 'paddingTop' in changed || 'paddingRight' in changed || 'paddingBottom' in changed || 'paddingLeft' in changed) {
|
|
3455
|
+
this._padding = resolvePadding(changed);
|
|
3456
|
+
this._layoutDirty = true;
|
|
3457
|
+
}
|
|
3316
3458
|
if ('flexWrap' in changed) {
|
|
3317
3459
|
this._config.flexWrap = changed.flexWrap;
|
|
3318
3460
|
this._layoutDirty = true;
|
|
3319
3461
|
}
|
|
3462
|
+
if ('alignContent' in changed) {
|
|
3463
|
+
this._config.alignContent = changed.alignContent;
|
|
3464
|
+
this._layoutDirty = true;
|
|
3465
|
+
}
|
|
3320
3466
|
if ('width' in changed || 'height' in changed) {
|
|
3321
|
-
|
|
3322
|
-
|
|
3467
|
+
const w = changed.width ?? this._rawWidth;
|
|
3468
|
+
const h = changed.height ?? this._rawHeight;
|
|
3469
|
+
this._rawWidth = w;
|
|
3470
|
+
this._rawHeight = h;
|
|
3471
|
+
if (typeof w === 'number' && typeof h === 'number') {
|
|
3472
|
+
this.resize(w, h);
|
|
3473
|
+
}
|
|
3474
|
+
else {
|
|
3475
|
+
// Percentage — will resolve in updateLayout
|
|
3476
|
+
this._explicitWidth = typeof w === 'number' ? w : 0;
|
|
3477
|
+
this._explicitHeight = typeof h === 'number' ? h : 0;
|
|
3478
|
+
this._layoutDirty = true;
|
|
3479
|
+
this.updateLayout();
|
|
3480
|
+
}
|
|
3481
|
+
return;
|
|
3323
3482
|
}
|
|
3324
3483
|
if (this._layoutDirty)
|
|
3325
3484
|
this.updateLayout();
|