@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/react.esm.js CHANGED
@@ -205,26 +205,90 @@ function applyProps(instance, newProps, oldProps = {}) {
205
205
  function normalizePadding(p) {
206
206
  return typeof p === 'number' ? [p, p, p, p] : p;
207
207
  }
208
+ /** Resolve padding from config: individual props override the base `padding` value */
209
+ function resolvePadding(config) {
210
+ const base = normalizePadding(config.padding ?? 0);
211
+ return [
212
+ config.paddingTop ?? base[0],
213
+ config.paddingRight ?? base[1],
214
+ config.paddingBottom ?? base[2],
215
+ config.paddingLeft ?? base[3],
216
+ ];
217
+ }
218
+ /** Resolve a dimension value — number passes through, "50%" resolves against reference */
219
+ function resolveDimension(value, reference) {
220
+ if (value === undefined)
221
+ return undefined;
222
+ if (typeof value === 'number')
223
+ return value;
224
+ if (typeof value === 'string' && value.endsWith('%')) {
225
+ const pct = parseFloat(value);
226
+ if (!isNaN(pct) && reference > 0 && isFinite(reference))
227
+ return (pct / 100) * reference;
228
+ }
229
+ return undefined;
230
+ }
208
231
  /** Measure a child's size and bounds offset for layout purposes */
209
- function measureChild(child) {
232
+ function measureChild(child, parentContentW = 0, parentContentH = 0) {
210
233
  const cfg = child._flexConfig;
211
- if (cfg?.layoutWidth !== undefined && cfg?.layoutHeight !== undefined) {
212
- return { w: cfg.layoutWidth, h: cfg.layoutHeight, ox: 0, oy: 0 };
234
+ const resolvedLW = resolveDimension(cfg?.layoutWidth, parentContentW);
235
+ const resolvedLH = resolveDimension(cfg?.layoutHeight, parentContentH);
236
+ if (resolvedLW !== undefined && resolvedLH !== undefined) {
237
+ return { w: resolvedLW, h: resolvedLH, ox: 0, oy: 0 };
213
238
  }
214
- // For FlexContainers, use their explicit size if set
239
+ // For FlexContainers, use their explicit or computed size
215
240
  if (child instanceof FlexContainer) {
216
241
  const fc = child;
217
- if (fc._explicitWidth > 0 && fc._explicitHeight > 0) {
218
- return { w: fc._explicitWidth, h: fc._explicitHeight, ox: 0, oy: 0 };
242
+ const w = fc._explicitWidth > 0 ? fc._explicitWidth : (fc._computedWidth > 0 ? fc._computedWidth : undefined);
243
+ const h = fc._explicitHeight > 0 ? fc._explicitHeight : (fc._computedHeight > 0 ? fc._computedHeight : undefined);
244
+ if (w !== undefined && h !== undefined) {
245
+ return { w, h, ox: 0, oy: 0 };
219
246
  }
220
247
  }
221
248
  // Use localBounds to get the true visual extent and origin offset.
222
249
  // This handles children with non-zero anchors (e.g. Button, Label with centered text).
223
250
  const bounds = child.getLocalBounds();
224
- const w = cfg?.layoutWidth ?? bounds.width;
225
- const h = cfg?.layoutHeight ?? bounds.height;
251
+ const w = resolvedLW ?? bounds.width;
252
+ const h = resolvedLH ?? bounds.height;
226
253
  return { w, h, ox: bounds.x, oy: bounds.y };
227
254
  }
255
+ /**
256
+ * Set a child's main or cross dimension.
257
+ * For FlexContainer children, calls resize() to trigger internal relayout
258
+ * instead of the PixiJS scale setter.
259
+ */
260
+ function setChildMainSize(child, isRow, mainSize, item) {
261
+ if (child instanceof FlexContainer) {
262
+ const fc = child;
263
+ fc.resize(isRow ? mainSize : fc._explicitWidth || fc._computedWidth, isRow ? fc._explicitHeight || fc._computedHeight : mainSize);
264
+ }
265
+ else {
266
+ if (isRow) {
267
+ child.width = mainSize;
268
+ }
269
+ else {
270
+ child.height = mainSize;
271
+ }
272
+ }
273
+ if (isRow)
274
+ item.w = mainSize;
275
+ else
276
+ item.h = mainSize;
277
+ }
278
+ function setChildCrossSize(child, isRow, crossSize) {
279
+ if (child instanceof FlexContainer) {
280
+ const fc = child;
281
+ fc.resize(isRow ? fc._explicitWidth || fc._computedWidth : crossSize, isRow ? crossSize : fc._explicitHeight || fc._computedHeight);
282
+ }
283
+ else {
284
+ if (isRow) {
285
+ child.height = crossSize;
286
+ }
287
+ else {
288
+ child.width = crossSize;
289
+ }
290
+ }
291
+ }
228
292
  function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, crossSize) {
229
293
  if (items.length === 0)
230
294
  return;
@@ -248,14 +312,7 @@ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, cr
248
312
  const grow = item.child._flexConfig?.flexGrow ?? 0;
249
313
  if (grow > 0) {
250
314
  const flexSize = (grow / totalGrow) * availableForFlex;
251
- if (isRow) {
252
- item.w = flexSize;
253
- item.child.width = flexSize;
254
- }
255
- else {
256
- item.h = flexSize;
257
- item.child.height = flexSize;
258
- }
315
+ setChildMainSize(item.child, isRow, flexSize, item);
259
316
  }
260
317
  }
261
318
  }
@@ -277,14 +334,7 @@ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, cr
277
334
  const itemMain = isRow ? item.w : item.h;
278
335
  const reduction = overflow * (itemMain / totalShrinkable);
279
336
  const newSize = Math.max(0, itemMain - reduction);
280
- if (isRow) {
281
- item.w = newSize;
282
- item.child.width = newSize;
283
- }
284
- else {
285
- item.h = newSize;
286
- item.child.height = newSize;
287
- }
337
+ setChildMainSize(item.child, isRow, newSize, item);
288
338
  }
289
339
  }
290
340
  }
@@ -341,12 +391,7 @@ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, cr
341
391
  crossPos += crossSize - crossDim;
342
392
  break;
343
393
  case 'stretch':
344
- if (isRow) {
345
- item.child.height = crossSize;
346
- }
347
- else {
348
- item.child.width = crossSize;
349
- }
394
+ setChildCrossSize(item.child, isRow, crossSize);
350
395
  break;
351
396
  }
352
397
  // Compensate for local bounds offset (e.g. centered anchors)
@@ -391,6 +436,12 @@ class FlexContainer extends Container {
391
436
  _maxHeight;
392
437
  /** @internal */ _explicitWidth;
393
438
  /** @internal */ _explicitHeight;
439
+ /** @internal */ _computedWidth = 0;
440
+ /** @internal */ _computedHeight = 0;
441
+ /** @internal */ _availableWidth = 0;
442
+ /** @internal */ _availableHeight = 0;
443
+ /** @internal */ _rawWidth;
444
+ /** @internal */ _rawHeight;
394
445
  _layoutChildren = [];
395
446
  _layoutDirty = true;
396
447
  constructor(config = {}) {
@@ -401,12 +452,15 @@ class FlexContainer extends Container {
401
452
  alignItems: config.alignItems ?? 'start',
402
453
  gap: config.gap ?? 0,
403
454
  flexWrap: config.flexWrap ?? false,
455
+ alignContent: config.alignContent ?? 'start',
404
456
  };
405
- this._padding = normalizePadding(config.padding ?? 0);
457
+ this._padding = resolvePadding(config);
406
458
  this._maxWidth = config.maxWidth ?? Infinity;
407
459
  this._maxHeight = config.maxHeight ?? Infinity;
408
- this._explicitWidth = config.width ?? 0;
409
- this._explicitHeight = config.height ?? 0;
460
+ this._rawWidth = config.width ?? 0;
461
+ this._rawHeight = config.height ?? 0;
462
+ this._explicitWidth = typeof this._rawWidth === 'number' ? this._rawWidth : 0;
463
+ this._explicitHeight = typeof this._rawHeight === 'number' ? this._rawHeight : 0;
410
464
  }
411
465
  // ─── Public API ──────────────────────────────────────
412
466
  /** Add a child with optional flex config. Also registers in flex layout. */
@@ -508,19 +562,41 @@ class FlexContainer extends Container {
508
562
  */
509
563
  updateLayout() {
510
564
  this._layoutDirty = false;
511
- const { direction, justifyContent, alignItems, gap, flexWrap } = this._config;
565
+ const { direction, justifyContent, alignItems, gap, flexWrap, alignContent } = this._config;
512
566
  const [pt, pr, pb, pl] = this._padding;
513
567
  const isRow = direction === 'row';
568
+ // Resolve percentage width/height against parent's available space
569
+ if (typeof this._rawWidth === 'string') {
570
+ this._explicitWidth = resolveDimension(this._rawWidth, this._availableWidth) ?? 0;
571
+ }
572
+ if (typeof this._rawHeight === 'string') {
573
+ this._explicitHeight = resolveDimension(this._rawHeight, this._availableHeight) ?? 0;
574
+ }
514
575
  const contentW = this._explicitWidth > 0 ? this._explicitWidth - pl - pr : Infinity;
515
576
  const contentH = this._explicitHeight > 0 ? this._explicitHeight - pt - pb : Infinity;
516
577
  const mainLimit = isRow ? contentW : contentH;
517
578
  const crossLimit = isRow ? contentH : contentW;
579
+ // Pass content area to measureChild for percentage resolution
580
+ const pctRefW = contentW < Infinity ? contentW : 0;
581
+ const pctRefH = contentH < Infinity ? contentH : 0;
582
+ // Propagate available size to child FlexContainers and resolve their percentages
583
+ for (const child of this._layoutChildren) {
584
+ if (child instanceof FlexContainer) {
585
+ const fc = child;
586
+ fc._availableWidth = pctRefW;
587
+ fc._availableHeight = pctRefH;
588
+ // If child has percentage dimensions, trigger its layout to resolve them
589
+ if (typeof fc._rawWidth === 'string' || typeof fc._rawHeight === 'string') {
590
+ fc.updateLayout();
591
+ }
592
+ }
593
+ }
518
594
  // Measure children (skip flexExclude — they position themselves)
519
595
  const measured = [];
520
596
  for (const child of this._layoutChildren) {
521
597
  if (child._flexConfig?.flexExclude)
522
598
  continue;
523
- const { w, h, ox, oy } = measureChild(child);
599
+ const { w, h, ox, oy } = measureChild(child, pctRefW, pctRefH);
524
600
  measured.push({ child, w, h, ox, oy });
525
601
  }
526
602
  // Split into lines (if wrapping)
@@ -557,42 +633,106 @@ class FlexContainer extends Container {
557
633
  }
558
634
  return maxCross;
559
635
  });
636
+ // Compute natural main size (for auto-sizing when no explicit size given)
637
+ let naturalMainSize = 0;
638
+ if (mainLimit === Infinity) {
639
+ for (const line of lines) {
640
+ let lineMain = 0;
641
+ for (const item of line) {
642
+ lineMain += isRow ? item.w : item.h;
643
+ }
644
+ lineMain += gap * Math.max(0, line.length - 1);
645
+ naturalMainSize = Math.max(naturalMainSize, lineMain);
646
+ }
647
+ }
648
+ // Effective main size: explicit if set, otherwise natural content size
649
+ const effectiveMainSize = mainLimit < Infinity ? mainLimit : naturalMainSize;
650
+ // Compute alignContent offsets for multi-line layouts
651
+ const totalLinesCross = lineCrossSizes.reduce((s, v) => s + v, 0) + gap * Math.max(0, lines.length - 1);
652
+ let acOffset = 0;
653
+ let acExtraGap = 0;
654
+ if (lines.length > 1 && crossLimit < Infinity) {
655
+ const freeSpace = Math.max(0, crossLimit - totalLinesCross);
656
+ switch (alignContent) {
657
+ case 'center':
658
+ acOffset = freeSpace / 2;
659
+ break;
660
+ case 'end':
661
+ acOffset = freeSpace;
662
+ break;
663
+ case 'space-between':
664
+ if (lines.length > 1) {
665
+ acExtraGap = freeSpace / (lines.length - 1);
666
+ }
667
+ break;
668
+ case 'stretch':
669
+ if (lines.length > 0) {
670
+ const extra = freeSpace / lines.length;
671
+ for (let i = 0; i < lineCrossSizes.length; i++) {
672
+ lineCrossSizes[i] += extra;
673
+ }
674
+ }
675
+ break;
676
+ // 'start' — no adjustment
677
+ }
678
+ }
560
679
  // Layout each line
561
- let crossOffset = isRow ? pt : pl;
680
+ let crossOffset = (isRow ? pt : pl) + acOffset;
562
681
  for (let i = 0; i < lines.length; i++) {
563
682
  const line = lines[i];
564
683
  const lineCross = lineCrossSizes[i];
565
684
  const mainStart = isRow ? pl : pt;
566
685
  // Offset items by padding
567
686
  const tempItems = line.map((item) => ({ ...item }));
568
- // For single-line layouts, use the full available cross space for alignment;
569
- // for multi-line (wrapping), each line gets its own measured cross size.
570
- const effectiveCross = lines.length === 1 && crossLimit < Infinity
571
- ? crossLimit
572
- : (crossLimit < Infinity ? Math.min(lineCross, crossLimit) : lineCross);
573
- layoutLine(tempItems, isRow, mainLimit < Infinity ? mainLimit : 0, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, effectiveCross);
687
+ // Cross size for alignment: use container cross size for single-line, line cross for multi-line
688
+ const effectiveCross = lines.length === 1 && crossLimit < Infinity ? crossLimit : lineCross;
689
+ layoutLine(tempItems, isRow, effectiveMainSize, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, effectiveCross);
574
690
  // Apply main-axis padding offset
575
691
  for (const item of tempItems) {
576
692
  const origChild = line.find((l) => l.child === item.child);
577
693
  origChild.child.x = item.child.x + (isRow ? mainStart : 0);
578
694
  origChild.child.y = item.child.y + (isRow ? 0 : mainStart);
579
695
  }
580
- crossOffset += lineCross + gap;
696
+ crossOffset += lineCross + gap + acExtraGap;
697
+ }
698
+ // Compute and store actual dimensions for measureChild() and getContentSize()
699
+ let totalCrossNatural = 0;
700
+ for (let i = 0; i < lineCrossSizes.length; i++) {
701
+ totalCrossNatural += lineCrossSizes[i];
702
+ if (i < lineCrossSizes.length - 1)
703
+ totalCrossNatural += gap;
704
+ }
705
+ if (isRow) {
706
+ this._computedWidth = this._explicitWidth > 0 ? this._explicitWidth : (pl + naturalMainSize + pr);
707
+ this._computedHeight = this._explicitHeight > 0 ? this._explicitHeight : (pt + totalCrossNatural + pb);
708
+ }
709
+ else {
710
+ this._computedWidth = this._explicitWidth > 0 ? this._explicitWidth : (pl + totalCrossNatural + pr);
711
+ this._computedHeight = this._explicitHeight > 0 ? this._explicitHeight : (pt + naturalMainSize + pb);
712
+ }
713
+ // Position flexExclude children (absolute positioning)
714
+ for (const child of this._layoutChildren) {
715
+ if (!child._flexConfig?.flexExclude)
716
+ continue;
717
+ const cfg = child._flexConfig;
718
+ const { w, h } = measureChild(child, pctRefW, pctRefH);
719
+ const cw = this._computedWidth;
720
+ const ch = this._computedHeight;
721
+ if (cfg.left !== undefined)
722
+ child.x = cfg.left;
723
+ else if (cfg.right !== undefined)
724
+ child.x = cw - w - cfg.right;
725
+ if (cfg.top !== undefined)
726
+ child.y = cfg.top;
727
+ else if (cfg.bottom !== undefined)
728
+ child.y = ch - h - cfg.bottom;
581
729
  }
582
730
  }
583
731
  /** Computed content size (after layout) */
584
732
  getContentSize() {
585
733
  if (this._layoutDirty)
586
734
  this.updateLayout();
587
- let maxX = 0;
588
- let maxY = 0;
589
- for (const child of this._layoutChildren) {
590
- const { w, h } = measureChild(child);
591
- maxX = Math.max(maxX, child.x + w);
592
- maxY = Math.max(maxY, child.y + h);
593
- }
594
- const [, pr, pb] = this._padding;
595
- return { width: maxX + pr, height: maxY + pb };
735
+ return { width: this._computedWidth, height: this._computedHeight };
596
736
  }
597
737
  /** React reconciler update hook — applies changed config props */
598
738
  updateConfig(changed) {
@@ -604,15 +744,34 @@ class FlexContainer extends Container {
604
744
  this.setAlignItems(changed.alignItems);
605
745
  if ('gap' in changed)
606
746
  this.setGap(changed.gap);
607
- if ('padding' in changed)
608
- this.setPadding(changed.padding);
747
+ if ('padding' in changed || 'paddingTop' in changed || 'paddingRight' in changed || 'paddingBottom' in changed || 'paddingLeft' in changed) {
748
+ this._padding = resolvePadding(changed);
749
+ this._layoutDirty = true;
750
+ }
609
751
  if ('flexWrap' in changed) {
610
752
  this._config.flexWrap = changed.flexWrap;
611
753
  this._layoutDirty = true;
612
754
  }
755
+ if ('alignContent' in changed) {
756
+ this._config.alignContent = changed.alignContent;
757
+ this._layoutDirty = true;
758
+ }
613
759
  if ('width' in changed || 'height' in changed) {
614
- this.resize(changed.width ?? this._explicitWidth, changed.height ?? this._explicitHeight);
615
- return; // resize calls updateLayout
760
+ const w = changed.width ?? this._rawWidth;
761
+ const h = changed.height ?? this._rawHeight;
762
+ this._rawWidth = w;
763
+ this._rawHeight = h;
764
+ if (typeof w === 'number' && typeof h === 'number') {
765
+ this.resize(w, h);
766
+ }
767
+ else {
768
+ // Percentage — will resolve in updateLayout
769
+ this._explicitWidth = typeof w === 'number' ? w : 0;
770
+ this._explicitHeight = typeof h === 'number' ? h : 0;
771
+ this._layoutDirty = true;
772
+ this.updateLayout();
773
+ }
774
+ return;
616
775
  }
617
776
  if (this._layoutDirty)
618
777
  this.updateLayout();
@@ -624,7 +783,7 @@ class FlexContainer extends Container {
624
783
  }
625
784
 
626
785
  /** Flex item prop names that should be forwarded to _flexConfig on the child */
627
- const FLEX_ITEM_PROPS = ['flexGrow', 'flexShrink', 'layoutWidth', 'layoutHeight', 'alignSelf', 'flexExclude'];
786
+ const FLEX_ITEM_PROPS = ['flexGrow', 'flexShrink', 'layoutWidth', 'layoutHeight', 'alignSelf', 'flexExclude', 'top', 'right', 'bottom', 'left'];
628
787
  /** Extract FlexItemConfig from props if any flex item props are present */
629
788
  function extractFlexItemConfig(props) {
630
789
  let config;