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