@energy8platform/game-engine 0.13.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/dist/index.d.ts CHANGED
@@ -1265,6 +1265,7 @@ declare class SpriteAnimation {
1265
1265
  type FlexDirection = 'row' | 'column';
1266
1266
  type JustifyContent = 'start' | 'center' | 'end' | 'space-between' | 'space-around';
1267
1267
  type AlignItems = 'start' | 'center' | 'end' | 'stretch';
1268
+ type AlignContent = 'start' | 'center' | 'end' | 'space-between' | 'stretch';
1268
1269
  type AlignSelf = 'auto' | 'start' | 'center' | 'end' | 'stretch';
1269
1270
  interface FlexItemConfig {
1270
1271
  /** Flex grow factor (0 = fixed size) */
@@ -1272,13 +1273,21 @@ interface FlexItemConfig {
1272
1273
  /** Flex shrink factor (0 = don't shrink, default: 1) */
1273
1274
  flexShrink?: number;
1274
1275
  /** Explicit width override for layout calculations */
1275
- layoutWidth?: number;
1276
+ layoutWidth?: number | string;
1276
1277
  /** Explicit height override for layout calculations */
1277
- layoutHeight?: number;
1278
+ layoutHeight?: number | string;
1278
1279
  /** Override parent's alignItems for this child */
1279
1280
  alignSelf?: AlignSelf;
1280
1281
  /** Exclude from flex layout (acts like position: absolute) */
1281
1282
  flexExclude?: boolean;
1283
+ /** Absolute positioning for flexExclude children (distance from top edge) */
1284
+ top?: number;
1285
+ /** Absolute positioning for flexExclude children (distance from right edge) */
1286
+ right?: number;
1287
+ /** Absolute positioning for flexExclude children (distance from bottom edge) */
1288
+ bottom?: number;
1289
+ /** Absolute positioning for flexExclude children (distance from left edge) */
1290
+ left?: number;
1282
1291
  }
1283
1292
  interface FlexContainerConfig {
1284
1293
  /** Layout direction (default: 'row') */
@@ -1291,16 +1300,23 @@ interface FlexContainerConfig {
1291
1300
  gap?: number;
1292
1301
  /** Padding [top, right, bottom, left] or single number (default: 0) */
1293
1302
  padding?: number | [number, number, number, number];
1303
+ /** Individual padding overrides (take priority over `padding`) */
1304
+ paddingTop?: number;
1305
+ paddingRight?: number;
1306
+ paddingBottom?: number;
1307
+ paddingLeft?: number;
1294
1308
  /** Enable wrapping to next line (default: false) */
1295
1309
  flexWrap?: boolean;
1310
+ /** Distribution of lines along the cross axis when wrapping (default: 'start') */
1311
+ alignContent?: AlignContent;
1296
1312
  /** Maximum width before wrapping (only with flexWrap) */
1297
1313
  maxWidth?: number;
1298
1314
  /** Maximum height before wrapping (only with flexWrap + column) */
1299
1315
  maxHeight?: number;
1300
- /** Explicit container width (used for cross-axis alignment/stretch) */
1301
- width?: number;
1302
- /** Explicit container height (used for cross-axis alignment/stretch) */
1303
- height?: number;
1316
+ /** Explicit container width number in pixels, or string percentage (e.g. "50%") resolved against parent */
1317
+ width?: number | string;
1318
+ /** Explicit container height number in pixels, or string percentage (e.g. "50%") resolved against parent */
1319
+ height?: number | string;
1304
1320
  }
1305
1321
  /**
1306
1322
  * Lightweight flexbox-like layout container for PixiJS.
@@ -1331,6 +1347,12 @@ declare class FlexContainer extends Container {
1331
1347
  private _maxHeight;
1332
1348
  /** @internal */ _explicitWidth: number;
1333
1349
  /** @internal */ _explicitHeight: number;
1350
+ /** @internal */ _computedWidth: number;
1351
+ /** @internal */ _computedHeight: number;
1352
+ /** @internal */ _availableWidth: number;
1353
+ /** @internal */ _availableHeight: number;
1354
+ /** @internal */ _rawWidth: number | string;
1355
+ /** @internal */ _rawHeight: number | string;
1334
1356
  private _layoutChildren;
1335
1357
  private _layoutDirty;
1336
1358
  constructor(config?: FlexContainerConfig);
package/dist/index.esm.js CHANGED
@@ -2911,26 +2911,90 @@ class SpriteAnimation {
2911
2911
  function normalizePadding(p) {
2912
2912
  return typeof p === 'number' ? [p, p, p, p] : p;
2913
2913
  }
2914
+ /** Resolve padding from config: individual props override the base `padding` value */
2915
+ function resolvePadding(config) {
2916
+ const base = normalizePadding(config.padding ?? 0);
2917
+ return [
2918
+ config.paddingTop ?? base[0],
2919
+ config.paddingRight ?? base[1],
2920
+ config.paddingBottom ?? base[2],
2921
+ config.paddingLeft ?? base[3],
2922
+ ];
2923
+ }
2924
+ /** Resolve a dimension value — number passes through, "50%" resolves against reference */
2925
+ function resolveDimension(value, reference) {
2926
+ if (value === undefined)
2927
+ return undefined;
2928
+ if (typeof value === 'number')
2929
+ return value;
2930
+ if (typeof value === 'string' && value.endsWith('%')) {
2931
+ const pct = parseFloat(value);
2932
+ if (!isNaN(pct) && reference > 0 && isFinite(reference))
2933
+ return (pct / 100) * reference;
2934
+ }
2935
+ return undefined;
2936
+ }
2914
2937
  /** Measure a child's size and bounds offset for layout purposes */
2915
- function measureChild(child) {
2938
+ function measureChild(child, parentContentW = 0, parentContentH = 0) {
2916
2939
  const cfg = child._flexConfig;
2917
- if (cfg?.layoutWidth !== undefined && cfg?.layoutHeight !== undefined) {
2918
- return { w: cfg.layoutWidth, h: cfg.layoutHeight, ox: 0, oy: 0 };
2940
+ const resolvedLW = resolveDimension(cfg?.layoutWidth, parentContentW);
2941
+ const resolvedLH = resolveDimension(cfg?.layoutHeight, parentContentH);
2942
+ if (resolvedLW !== undefined && resolvedLH !== undefined) {
2943
+ return { w: resolvedLW, h: resolvedLH, ox: 0, oy: 0 };
2919
2944
  }
2920
- // For FlexContainers, use their explicit size if set
2945
+ // For FlexContainers, use their explicit or computed size
2921
2946
  if (child instanceof FlexContainer) {
2922
2947
  const fc = child;
2923
- if (fc._explicitWidth > 0 && fc._explicitHeight > 0) {
2924
- return { w: fc._explicitWidth, h: fc._explicitHeight, ox: 0, oy: 0 };
2948
+ const w = fc._explicitWidth > 0 ? fc._explicitWidth : (fc._computedWidth > 0 ? fc._computedWidth : undefined);
2949
+ const h = fc._explicitHeight > 0 ? fc._explicitHeight : (fc._computedHeight > 0 ? fc._computedHeight : undefined);
2950
+ if (w !== undefined && h !== undefined) {
2951
+ return { w, h, ox: 0, oy: 0 };
2925
2952
  }
2926
2953
  }
2927
2954
  // Use localBounds to get the true visual extent and origin offset.
2928
2955
  // This handles children with non-zero anchors (e.g. Button, Label with centered text).
2929
2956
  const bounds = child.getLocalBounds();
2930
- const w = cfg?.layoutWidth ?? bounds.width;
2931
- const h = cfg?.layoutHeight ?? bounds.height;
2957
+ const w = resolvedLW ?? bounds.width;
2958
+ const h = resolvedLH ?? bounds.height;
2932
2959
  return { w, h, ox: bounds.x, oy: bounds.y };
2933
2960
  }
2961
+ /**
2962
+ * Set a child's main or cross dimension.
2963
+ * For FlexContainer children, calls resize() to trigger internal relayout
2964
+ * instead of the PixiJS scale setter.
2965
+ */
2966
+ function setChildMainSize(child, isRow, mainSize, item) {
2967
+ if (child instanceof FlexContainer) {
2968
+ const fc = child;
2969
+ fc.resize(isRow ? mainSize : fc._explicitWidth || fc._computedWidth, isRow ? fc._explicitHeight || fc._computedHeight : mainSize);
2970
+ }
2971
+ else {
2972
+ if (isRow) {
2973
+ child.width = mainSize;
2974
+ }
2975
+ else {
2976
+ child.height = mainSize;
2977
+ }
2978
+ }
2979
+ if (isRow)
2980
+ item.w = mainSize;
2981
+ else
2982
+ item.h = mainSize;
2983
+ }
2984
+ function setChildCrossSize(child, isRow, crossSize) {
2985
+ if (child instanceof FlexContainer) {
2986
+ const fc = child;
2987
+ fc.resize(isRow ? fc._explicitWidth || fc._computedWidth : crossSize, isRow ? crossSize : fc._explicitHeight || fc._computedHeight);
2988
+ }
2989
+ else {
2990
+ if (isRow) {
2991
+ child.height = crossSize;
2992
+ }
2993
+ else {
2994
+ child.width = crossSize;
2995
+ }
2996
+ }
2997
+ }
2934
2998
  function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, crossSize) {
2935
2999
  if (items.length === 0)
2936
3000
  return;
@@ -2954,14 +3018,7 @@ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, cr
2954
3018
  const grow = item.child._flexConfig?.flexGrow ?? 0;
2955
3019
  if (grow > 0) {
2956
3020
  const flexSize = (grow / totalGrow) * availableForFlex;
2957
- if (isRow) {
2958
- item.w = flexSize;
2959
- item.child.width = flexSize;
2960
- }
2961
- else {
2962
- item.h = flexSize;
2963
- item.child.height = flexSize;
2964
- }
3021
+ setChildMainSize(item.child, isRow, flexSize, item);
2965
3022
  }
2966
3023
  }
2967
3024
  }
@@ -2983,14 +3040,7 @@ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, cr
2983
3040
  const itemMain = isRow ? item.w : item.h;
2984
3041
  const reduction = overflow * (itemMain / totalShrinkable);
2985
3042
  const newSize = Math.max(0, itemMain - reduction);
2986
- if (isRow) {
2987
- item.w = newSize;
2988
- item.child.width = newSize;
2989
- }
2990
- else {
2991
- item.h = newSize;
2992
- item.child.height = newSize;
2993
- }
3043
+ setChildMainSize(item.child, isRow, newSize, item);
2994
3044
  }
2995
3045
  }
2996
3046
  }
@@ -3047,12 +3097,7 @@ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, cr
3047
3097
  crossPos += crossSize - crossDim;
3048
3098
  break;
3049
3099
  case 'stretch':
3050
- if (isRow) {
3051
- item.child.height = crossSize;
3052
- }
3053
- else {
3054
- item.child.width = crossSize;
3055
- }
3100
+ setChildCrossSize(item.child, isRow, crossSize);
3056
3101
  break;
3057
3102
  }
3058
3103
  // Compensate for local bounds offset (e.g. centered anchors)
@@ -3097,6 +3142,12 @@ class FlexContainer extends Container {
3097
3142
  _maxHeight;
3098
3143
  /** @internal */ _explicitWidth;
3099
3144
  /** @internal */ _explicitHeight;
3145
+ /** @internal */ _computedWidth = 0;
3146
+ /** @internal */ _computedHeight = 0;
3147
+ /** @internal */ _availableWidth = 0;
3148
+ /** @internal */ _availableHeight = 0;
3149
+ /** @internal */ _rawWidth;
3150
+ /** @internal */ _rawHeight;
3100
3151
  _layoutChildren = [];
3101
3152
  _layoutDirty = true;
3102
3153
  constructor(config = {}) {
@@ -3107,12 +3158,15 @@ class FlexContainer extends Container {
3107
3158
  alignItems: config.alignItems ?? 'start',
3108
3159
  gap: config.gap ?? 0,
3109
3160
  flexWrap: config.flexWrap ?? false,
3161
+ alignContent: config.alignContent ?? 'start',
3110
3162
  };
3111
- this._padding = normalizePadding(config.padding ?? 0);
3163
+ this._padding = resolvePadding(config);
3112
3164
  this._maxWidth = config.maxWidth ?? Infinity;
3113
3165
  this._maxHeight = config.maxHeight ?? Infinity;
3114
- this._explicitWidth = config.width ?? 0;
3115
- this._explicitHeight = config.height ?? 0;
3166
+ this._rawWidth = config.width ?? 0;
3167
+ this._rawHeight = config.height ?? 0;
3168
+ this._explicitWidth = typeof this._rawWidth === 'number' ? this._rawWidth : 0;
3169
+ this._explicitHeight = typeof this._rawHeight === 'number' ? this._rawHeight : 0;
3116
3170
  }
3117
3171
  // ─── Public API ──────────────────────────────────────
3118
3172
  /** Add a child with optional flex config. Also registers in flex layout. */
@@ -3214,19 +3268,41 @@ class FlexContainer extends Container {
3214
3268
  */
3215
3269
  updateLayout() {
3216
3270
  this._layoutDirty = false;
3217
- const { direction, justifyContent, alignItems, gap, flexWrap } = this._config;
3271
+ const { direction, justifyContent, alignItems, gap, flexWrap, alignContent } = this._config;
3218
3272
  const [pt, pr, pb, pl] = this._padding;
3219
3273
  const isRow = direction === 'row';
3274
+ // Resolve percentage width/height against parent's available space
3275
+ if (typeof this._rawWidth === 'string') {
3276
+ this._explicitWidth = resolveDimension(this._rawWidth, this._availableWidth) ?? 0;
3277
+ }
3278
+ if (typeof this._rawHeight === 'string') {
3279
+ this._explicitHeight = resolveDimension(this._rawHeight, this._availableHeight) ?? 0;
3280
+ }
3220
3281
  const contentW = this._explicitWidth > 0 ? this._explicitWidth - pl - pr : Infinity;
3221
3282
  const contentH = this._explicitHeight > 0 ? this._explicitHeight - pt - pb : Infinity;
3222
3283
  const mainLimit = isRow ? contentW : contentH;
3223
3284
  const crossLimit = isRow ? contentH : contentW;
3285
+ // Pass content area to measureChild for percentage resolution
3286
+ const pctRefW = contentW < Infinity ? contentW : 0;
3287
+ const pctRefH = contentH < Infinity ? contentH : 0;
3288
+ // Propagate available size to child FlexContainers and resolve their percentages
3289
+ for (const child of this._layoutChildren) {
3290
+ if (child instanceof FlexContainer) {
3291
+ const fc = child;
3292
+ fc._availableWidth = pctRefW;
3293
+ fc._availableHeight = pctRefH;
3294
+ // If child has percentage dimensions, trigger its layout to resolve them
3295
+ if (typeof fc._rawWidth === 'string' || typeof fc._rawHeight === 'string') {
3296
+ fc.updateLayout();
3297
+ }
3298
+ }
3299
+ }
3224
3300
  // Measure children (skip flexExclude — they position themselves)
3225
3301
  const measured = [];
3226
3302
  for (const child of this._layoutChildren) {
3227
3303
  if (child._flexConfig?.flexExclude)
3228
3304
  continue;
3229
- const { w, h, ox, oy } = measureChild(child);
3305
+ const { w, h, ox, oy } = measureChild(child, pctRefW, pctRefH);
3230
3306
  measured.push({ child, w, h, ox, oy });
3231
3307
  }
3232
3308
  // Split into lines (if wrapping)
@@ -3263,42 +3339,106 @@ class FlexContainer extends Container {
3263
3339
  }
3264
3340
  return maxCross;
3265
3341
  });
3342
+ // Compute natural main size (for auto-sizing when no explicit size given)
3343
+ let naturalMainSize = 0;
3344
+ if (mainLimit === Infinity) {
3345
+ for (const line of lines) {
3346
+ let lineMain = 0;
3347
+ for (const item of line) {
3348
+ lineMain += isRow ? item.w : item.h;
3349
+ }
3350
+ lineMain += gap * Math.max(0, line.length - 1);
3351
+ naturalMainSize = Math.max(naturalMainSize, lineMain);
3352
+ }
3353
+ }
3354
+ // Effective main size: explicit if set, otherwise natural content size
3355
+ const effectiveMainSize = mainLimit < Infinity ? mainLimit : naturalMainSize;
3356
+ // Compute alignContent offsets for multi-line layouts
3357
+ const totalLinesCross = lineCrossSizes.reduce((s, v) => s + v, 0) + gap * Math.max(0, lines.length - 1);
3358
+ let acOffset = 0;
3359
+ let acExtraGap = 0;
3360
+ if (lines.length > 1 && crossLimit < Infinity) {
3361
+ const freeSpace = Math.max(0, crossLimit - totalLinesCross);
3362
+ switch (alignContent) {
3363
+ case 'center':
3364
+ acOffset = freeSpace / 2;
3365
+ break;
3366
+ case 'end':
3367
+ acOffset = freeSpace;
3368
+ break;
3369
+ case 'space-between':
3370
+ if (lines.length > 1) {
3371
+ acExtraGap = freeSpace / (lines.length - 1);
3372
+ }
3373
+ break;
3374
+ case 'stretch':
3375
+ if (lines.length > 0) {
3376
+ const extra = freeSpace / lines.length;
3377
+ for (let i = 0; i < lineCrossSizes.length; i++) {
3378
+ lineCrossSizes[i] += extra;
3379
+ }
3380
+ }
3381
+ break;
3382
+ // 'start' — no adjustment
3383
+ }
3384
+ }
3266
3385
  // Layout each line
3267
- let crossOffset = isRow ? pt : pl;
3386
+ let crossOffset = (isRow ? pt : pl) + acOffset;
3268
3387
  for (let i = 0; i < lines.length; i++) {
3269
3388
  const line = lines[i];
3270
3389
  const lineCross = lineCrossSizes[i];
3271
3390
  const mainStart = isRow ? pl : pt;
3272
3391
  // Offset items by padding
3273
3392
  const tempItems = line.map((item) => ({ ...item }));
3274
- // For single-line layouts, use the full available cross space for alignment;
3275
- // for multi-line (wrapping), each line gets its own measured cross size.
3276
- const effectiveCross = lines.length === 1 && crossLimit < Infinity
3277
- ? crossLimit
3278
- : (crossLimit < Infinity ? Math.min(lineCross, crossLimit) : lineCross);
3279
- layoutLine(tempItems, isRow, mainLimit < Infinity ? mainLimit : 0, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, effectiveCross);
3393
+ // Cross size for alignment: use container cross size for single-line, line cross for multi-line
3394
+ const effectiveCross = lines.length === 1 && crossLimit < Infinity ? crossLimit : lineCross;
3395
+ layoutLine(tempItems, isRow, effectiveMainSize, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, effectiveCross);
3280
3396
  // Apply main-axis padding offset
3281
3397
  for (const item of tempItems) {
3282
3398
  const origChild = line.find((l) => l.child === item.child);
3283
3399
  origChild.child.x = item.child.x + (isRow ? mainStart : 0);
3284
3400
  origChild.child.y = item.child.y + (isRow ? 0 : mainStart);
3285
3401
  }
3286
- crossOffset += lineCross + gap;
3402
+ crossOffset += lineCross + gap + acExtraGap;
3403
+ }
3404
+ // Compute and store actual dimensions for measureChild() and getContentSize()
3405
+ let totalCrossNatural = 0;
3406
+ for (let i = 0; i < lineCrossSizes.length; i++) {
3407
+ totalCrossNatural += lineCrossSizes[i];
3408
+ if (i < lineCrossSizes.length - 1)
3409
+ totalCrossNatural += gap;
3410
+ }
3411
+ if (isRow) {
3412
+ this._computedWidth = this._explicitWidth > 0 ? this._explicitWidth : (pl + naturalMainSize + pr);
3413
+ this._computedHeight = this._explicitHeight > 0 ? this._explicitHeight : (pt + totalCrossNatural + pb);
3414
+ }
3415
+ else {
3416
+ this._computedWidth = this._explicitWidth > 0 ? this._explicitWidth : (pl + totalCrossNatural + pr);
3417
+ this._computedHeight = this._explicitHeight > 0 ? this._explicitHeight : (pt + naturalMainSize + pb);
3418
+ }
3419
+ // Position flexExclude children (absolute positioning)
3420
+ for (const child of this._layoutChildren) {
3421
+ if (!child._flexConfig?.flexExclude)
3422
+ continue;
3423
+ const cfg = child._flexConfig;
3424
+ const { w, h } = measureChild(child, pctRefW, pctRefH);
3425
+ const cw = this._computedWidth;
3426
+ const ch = this._computedHeight;
3427
+ if (cfg.left !== undefined)
3428
+ child.x = cfg.left;
3429
+ else if (cfg.right !== undefined)
3430
+ child.x = cw - w - cfg.right;
3431
+ if (cfg.top !== undefined)
3432
+ child.y = cfg.top;
3433
+ else if (cfg.bottom !== undefined)
3434
+ child.y = ch - h - cfg.bottom;
3287
3435
  }
3288
3436
  }
3289
3437
  /** Computed content size (after layout) */
3290
3438
  getContentSize() {
3291
3439
  if (this._layoutDirty)
3292
3440
  this.updateLayout();
3293
- let maxX = 0;
3294
- let maxY = 0;
3295
- for (const child of this._layoutChildren) {
3296
- const { w, h } = measureChild(child);
3297
- maxX = Math.max(maxX, child.x + w);
3298
- maxY = Math.max(maxY, child.y + h);
3299
- }
3300
- const [, pr, pb] = this._padding;
3301
- return { width: maxX + pr, height: maxY + pb };
3441
+ return { width: this._computedWidth, height: this._computedHeight };
3302
3442
  }
3303
3443
  /** React reconciler update hook — applies changed config props */
3304
3444
  updateConfig(changed) {
@@ -3310,15 +3450,34 @@ class FlexContainer extends Container {
3310
3450
  this.setAlignItems(changed.alignItems);
3311
3451
  if ('gap' in changed)
3312
3452
  this.setGap(changed.gap);
3313
- if ('padding' in changed)
3314
- this.setPadding(changed.padding);
3453
+ if ('padding' in changed || 'paddingTop' in changed || 'paddingRight' in changed || 'paddingBottom' in changed || 'paddingLeft' in changed) {
3454
+ this._padding = resolvePadding(changed);
3455
+ this._layoutDirty = true;
3456
+ }
3315
3457
  if ('flexWrap' in changed) {
3316
3458
  this._config.flexWrap = changed.flexWrap;
3317
3459
  this._layoutDirty = true;
3318
3460
  }
3461
+ if ('alignContent' in changed) {
3462
+ this._config.alignContent = changed.alignContent;
3463
+ this._layoutDirty = true;
3464
+ }
3319
3465
  if ('width' in changed || 'height' in changed) {
3320
- this.resize(changed.width ?? this._explicitWidth, changed.height ?? this._explicitHeight);
3321
- return; // resize calls updateLayout
3466
+ const w = changed.width ?? this._rawWidth;
3467
+ const h = changed.height ?? this._rawHeight;
3468
+ this._rawWidth = w;
3469
+ this._rawHeight = h;
3470
+ if (typeof w === 'number' && typeof h === 'number') {
3471
+ this.resize(w, h);
3472
+ }
3473
+ else {
3474
+ // Percentage — will resolve in updateLayout
3475
+ this._explicitWidth = typeof w === 'number' ? w : 0;
3476
+ this._explicitHeight = typeof h === 'number' ? h : 0;
3477
+ this._layoutDirty = true;
3478
+ this.updateLayout();
3479
+ }
3480
+ return;
3322
3481
  }
3323
3482
  if (this._layoutDirty)
3324
3483
  this.updateLayout();