@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/dist/ui.cjs.js CHANGED
@@ -27,26 +27,90 @@ function resolveView(input) {
27
27
  function normalizePadding(p) {
28
28
  return typeof p === 'number' ? [p, p, p, p] : p;
29
29
  }
30
+ /** Resolve padding from config: individual props override the base `padding` value */
31
+ function resolvePadding(config) {
32
+ const base = normalizePadding(config.padding ?? 0);
33
+ return [
34
+ config.paddingTop ?? base[0],
35
+ config.paddingRight ?? base[1],
36
+ config.paddingBottom ?? base[2],
37
+ config.paddingLeft ?? base[3],
38
+ ];
39
+ }
40
+ /** Resolve a dimension value — number passes through, "50%" resolves against reference */
41
+ function resolveDimension(value, reference) {
42
+ if (value === undefined)
43
+ return undefined;
44
+ if (typeof value === 'number')
45
+ return value;
46
+ if (typeof value === 'string' && value.endsWith('%')) {
47
+ const pct = parseFloat(value);
48
+ if (!isNaN(pct) && reference > 0 && isFinite(reference))
49
+ return (pct / 100) * reference;
50
+ }
51
+ return undefined;
52
+ }
30
53
  /** Measure a child's size and bounds offset for layout purposes */
31
- function measureChild(child) {
54
+ function measureChild(child, parentContentW = 0, parentContentH = 0) {
32
55
  const cfg = child._flexConfig;
33
- if (cfg?.layoutWidth !== undefined && cfg?.layoutHeight !== undefined) {
34
- return { w: cfg.layoutWidth, h: cfg.layoutHeight, ox: 0, oy: 0 };
56
+ const resolvedLW = resolveDimension(cfg?.layoutWidth, parentContentW);
57
+ const resolvedLH = resolveDimension(cfg?.layoutHeight, parentContentH);
58
+ if (resolvedLW !== undefined && resolvedLH !== undefined) {
59
+ return { w: resolvedLW, h: resolvedLH, ox: 0, oy: 0 };
35
60
  }
36
- // For FlexContainers, use their explicit size if set
61
+ // For FlexContainers, use their explicit or computed size
37
62
  if (child instanceof FlexContainer) {
38
63
  const fc = child;
39
- if (fc._explicitWidth > 0 && fc._explicitHeight > 0) {
40
- return { w: fc._explicitWidth, h: fc._explicitHeight, ox: 0, oy: 0 };
64
+ const w = fc._explicitWidth > 0 ? fc._explicitWidth : (fc._computedWidth > 0 ? fc._computedWidth : undefined);
65
+ const h = fc._explicitHeight > 0 ? fc._explicitHeight : (fc._computedHeight > 0 ? fc._computedHeight : undefined);
66
+ if (w !== undefined && h !== undefined) {
67
+ return { w, h, ox: 0, oy: 0 };
41
68
  }
42
69
  }
43
70
  // Use localBounds to get the true visual extent and origin offset.
44
71
  // This handles children with non-zero anchors (e.g. Button, Label with centered text).
45
72
  const bounds = child.getLocalBounds();
46
- const w = cfg?.layoutWidth ?? bounds.width;
47
- const h = cfg?.layoutHeight ?? bounds.height;
73
+ const w = resolvedLW ?? bounds.width;
74
+ const h = resolvedLH ?? bounds.height;
48
75
  return { w, h, ox: bounds.x, oy: bounds.y };
49
76
  }
77
+ /**
78
+ * Set a child's main or cross dimension.
79
+ * For FlexContainer children, calls resize() to trigger internal relayout
80
+ * instead of the PixiJS scale setter.
81
+ */
82
+ function setChildMainSize(child, isRow, mainSize, item) {
83
+ if (child instanceof FlexContainer) {
84
+ const fc = child;
85
+ fc.resize(isRow ? mainSize : fc._explicitWidth || fc._computedWidth, isRow ? fc._explicitHeight || fc._computedHeight : mainSize);
86
+ }
87
+ else {
88
+ if (isRow) {
89
+ child.width = mainSize;
90
+ }
91
+ else {
92
+ child.height = mainSize;
93
+ }
94
+ }
95
+ if (isRow)
96
+ item.w = mainSize;
97
+ else
98
+ item.h = mainSize;
99
+ }
100
+ function setChildCrossSize(child, isRow, crossSize) {
101
+ if (child instanceof FlexContainer) {
102
+ const fc = child;
103
+ fc.resize(isRow ? fc._explicitWidth || fc._computedWidth : crossSize, isRow ? crossSize : fc._explicitHeight || fc._computedHeight);
104
+ }
105
+ else {
106
+ if (isRow) {
107
+ child.height = crossSize;
108
+ }
109
+ else {
110
+ child.width = crossSize;
111
+ }
112
+ }
113
+ }
50
114
  function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, crossSize) {
51
115
  if (items.length === 0)
52
116
  return;
@@ -70,14 +134,7 @@ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, cr
70
134
  const grow = item.child._flexConfig?.flexGrow ?? 0;
71
135
  if (grow > 0) {
72
136
  const flexSize = (grow / totalGrow) * availableForFlex;
73
- if (isRow) {
74
- item.w = flexSize;
75
- item.child.width = flexSize;
76
- }
77
- else {
78
- item.h = flexSize;
79
- item.child.height = flexSize;
80
- }
137
+ setChildMainSize(item.child, isRow, flexSize, item);
81
138
  }
82
139
  }
83
140
  }
@@ -99,14 +156,7 @@ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, cr
99
156
  const itemMain = isRow ? item.w : item.h;
100
157
  const reduction = overflow * (itemMain / totalShrinkable);
101
158
  const newSize = Math.max(0, itemMain - reduction);
102
- if (isRow) {
103
- item.w = newSize;
104
- item.child.width = newSize;
105
- }
106
- else {
107
- item.h = newSize;
108
- item.child.height = newSize;
109
- }
159
+ setChildMainSize(item.child, isRow, newSize, item);
110
160
  }
111
161
  }
112
162
  }
@@ -163,12 +213,7 @@ function layoutLine(items, isRow, mainSize, justify, align, gap, crossOffset, cr
163
213
  crossPos += crossSize - crossDim;
164
214
  break;
165
215
  case 'stretch':
166
- if (isRow) {
167
- item.child.height = crossSize;
168
- }
169
- else {
170
- item.child.width = crossSize;
171
- }
216
+ setChildCrossSize(item.child, isRow, crossSize);
172
217
  break;
173
218
  }
174
219
  // Compensate for local bounds offset (e.g. centered anchors)
@@ -213,6 +258,12 @@ class FlexContainer extends pixi_js.Container {
213
258
  _maxHeight;
214
259
  /** @internal */ _explicitWidth;
215
260
  /** @internal */ _explicitHeight;
261
+ /** @internal */ _computedWidth = 0;
262
+ /** @internal */ _computedHeight = 0;
263
+ /** @internal */ _availableWidth = 0;
264
+ /** @internal */ _availableHeight = 0;
265
+ /** @internal */ _rawWidth;
266
+ /** @internal */ _rawHeight;
216
267
  _layoutChildren = [];
217
268
  _layoutDirty = true;
218
269
  constructor(config = {}) {
@@ -223,12 +274,15 @@ class FlexContainer extends pixi_js.Container {
223
274
  alignItems: config.alignItems ?? 'start',
224
275
  gap: config.gap ?? 0,
225
276
  flexWrap: config.flexWrap ?? false,
277
+ alignContent: config.alignContent ?? 'start',
226
278
  };
227
- this._padding = normalizePadding(config.padding ?? 0);
279
+ this._padding = resolvePadding(config);
228
280
  this._maxWidth = config.maxWidth ?? Infinity;
229
281
  this._maxHeight = config.maxHeight ?? Infinity;
230
- this._explicitWidth = config.width ?? 0;
231
- this._explicitHeight = config.height ?? 0;
282
+ this._rawWidth = config.width ?? 0;
283
+ this._rawHeight = config.height ?? 0;
284
+ this._explicitWidth = typeof this._rawWidth === 'number' ? this._rawWidth : 0;
285
+ this._explicitHeight = typeof this._rawHeight === 'number' ? this._rawHeight : 0;
232
286
  }
233
287
  // ─── Public API ──────────────────────────────────────
234
288
  /** Add a child with optional flex config. Also registers in flex layout. */
@@ -330,19 +384,41 @@ class FlexContainer extends pixi_js.Container {
330
384
  */
331
385
  updateLayout() {
332
386
  this._layoutDirty = false;
333
- const { direction, justifyContent, alignItems, gap, flexWrap } = this._config;
387
+ const { direction, justifyContent, alignItems, gap, flexWrap, alignContent } = this._config;
334
388
  const [pt, pr, pb, pl] = this._padding;
335
389
  const isRow = direction === 'row';
390
+ // Resolve percentage width/height against parent's available space
391
+ if (typeof this._rawWidth === 'string') {
392
+ this._explicitWidth = resolveDimension(this._rawWidth, this._availableWidth) ?? 0;
393
+ }
394
+ if (typeof this._rawHeight === 'string') {
395
+ this._explicitHeight = resolveDimension(this._rawHeight, this._availableHeight) ?? 0;
396
+ }
336
397
  const contentW = this._explicitWidth > 0 ? this._explicitWidth - pl - pr : Infinity;
337
398
  const contentH = this._explicitHeight > 0 ? this._explicitHeight - pt - pb : Infinity;
338
399
  const mainLimit = isRow ? contentW : contentH;
339
400
  const crossLimit = isRow ? contentH : contentW;
401
+ // Pass content area to measureChild for percentage resolution
402
+ const pctRefW = contentW < Infinity ? contentW : 0;
403
+ const pctRefH = contentH < Infinity ? contentH : 0;
404
+ // Propagate available size to child FlexContainers and resolve their percentages
405
+ for (const child of this._layoutChildren) {
406
+ if (child instanceof FlexContainer) {
407
+ const fc = child;
408
+ fc._availableWidth = pctRefW;
409
+ fc._availableHeight = pctRefH;
410
+ // If child has percentage dimensions, trigger its layout to resolve them
411
+ if (typeof fc._rawWidth === 'string' || typeof fc._rawHeight === 'string') {
412
+ fc.updateLayout();
413
+ }
414
+ }
415
+ }
340
416
  // Measure children (skip flexExclude — they position themselves)
341
417
  const measured = [];
342
418
  for (const child of this._layoutChildren) {
343
419
  if (child._flexConfig?.flexExclude)
344
420
  continue;
345
- const { w, h, ox, oy } = measureChild(child);
421
+ const { w, h, ox, oy } = measureChild(child, pctRefW, pctRefH);
346
422
  measured.push({ child, w, h, ox, oy });
347
423
  }
348
424
  // Split into lines (if wrapping)
@@ -379,42 +455,106 @@ class FlexContainer extends pixi_js.Container {
379
455
  }
380
456
  return maxCross;
381
457
  });
458
+ // Compute natural main size (for auto-sizing when no explicit size given)
459
+ let naturalMainSize = 0;
460
+ if (mainLimit === Infinity) {
461
+ for (const line of lines) {
462
+ let lineMain = 0;
463
+ for (const item of line) {
464
+ lineMain += isRow ? item.w : item.h;
465
+ }
466
+ lineMain += gap * Math.max(0, line.length - 1);
467
+ naturalMainSize = Math.max(naturalMainSize, lineMain);
468
+ }
469
+ }
470
+ // Effective main size: explicit if set, otherwise natural content size
471
+ const effectiveMainSize = mainLimit < Infinity ? mainLimit : naturalMainSize;
472
+ // Compute alignContent offsets for multi-line layouts
473
+ const totalLinesCross = lineCrossSizes.reduce((s, v) => s + v, 0) + gap * Math.max(0, lines.length - 1);
474
+ let acOffset = 0;
475
+ let acExtraGap = 0;
476
+ if (lines.length > 1 && crossLimit < Infinity) {
477
+ const freeSpace = Math.max(0, crossLimit - totalLinesCross);
478
+ switch (alignContent) {
479
+ case 'center':
480
+ acOffset = freeSpace / 2;
481
+ break;
482
+ case 'end':
483
+ acOffset = freeSpace;
484
+ break;
485
+ case 'space-between':
486
+ if (lines.length > 1) {
487
+ acExtraGap = freeSpace / (lines.length - 1);
488
+ }
489
+ break;
490
+ case 'stretch':
491
+ if (lines.length > 0) {
492
+ const extra = freeSpace / lines.length;
493
+ for (let i = 0; i < lineCrossSizes.length; i++) {
494
+ lineCrossSizes[i] += extra;
495
+ }
496
+ }
497
+ break;
498
+ // 'start' — no adjustment
499
+ }
500
+ }
382
501
  // Layout each line
383
- let crossOffset = isRow ? pt : pl;
502
+ let crossOffset = (isRow ? pt : pl) + acOffset;
384
503
  for (let i = 0; i < lines.length; i++) {
385
504
  const line = lines[i];
386
505
  const lineCross = lineCrossSizes[i];
387
506
  const mainStart = isRow ? pl : pt;
388
507
  // Offset items by padding
389
508
  const tempItems = line.map((item) => ({ ...item }));
390
- // For single-line layouts, use the full available cross space for alignment;
391
- // for multi-line (wrapping), each line gets its own measured cross size.
392
- const effectiveCross = lines.length === 1 && crossLimit < Infinity
393
- ? crossLimit
394
- : (crossLimit < Infinity ? Math.min(lineCross, crossLimit) : lineCross);
395
- layoutLine(tempItems, isRow, mainLimit < Infinity ? mainLimit : 0, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, effectiveCross);
509
+ // Cross size for alignment: use container cross size for single-line, line cross for multi-line
510
+ const effectiveCross = lines.length === 1 && crossLimit < Infinity ? crossLimit : lineCross;
511
+ layoutLine(tempItems, isRow, effectiveMainSize, mainLimit < Infinity ? justifyContent : 'start', alignItems, gap, crossOffset, effectiveCross);
396
512
  // Apply main-axis padding offset
397
513
  for (const item of tempItems) {
398
514
  const origChild = line.find((l) => l.child === item.child);
399
515
  origChild.child.x = item.child.x + (isRow ? mainStart : 0);
400
516
  origChild.child.y = item.child.y + (isRow ? 0 : mainStart);
401
517
  }
402
- crossOffset += lineCross + gap;
518
+ crossOffset += lineCross + gap + acExtraGap;
519
+ }
520
+ // Compute and store actual dimensions for measureChild() and getContentSize()
521
+ let totalCrossNatural = 0;
522
+ for (let i = 0; i < lineCrossSizes.length; i++) {
523
+ totalCrossNatural += lineCrossSizes[i];
524
+ if (i < lineCrossSizes.length - 1)
525
+ totalCrossNatural += gap;
526
+ }
527
+ if (isRow) {
528
+ this._computedWidth = this._explicitWidth > 0 ? this._explicitWidth : (pl + naturalMainSize + pr);
529
+ this._computedHeight = this._explicitHeight > 0 ? this._explicitHeight : (pt + totalCrossNatural + pb);
530
+ }
531
+ else {
532
+ this._computedWidth = this._explicitWidth > 0 ? this._explicitWidth : (pl + totalCrossNatural + pr);
533
+ this._computedHeight = this._explicitHeight > 0 ? this._explicitHeight : (pt + naturalMainSize + pb);
534
+ }
535
+ // Position flexExclude children (absolute positioning)
536
+ for (const child of this._layoutChildren) {
537
+ if (!child._flexConfig?.flexExclude)
538
+ continue;
539
+ const cfg = child._flexConfig;
540
+ const { w, h } = measureChild(child, pctRefW, pctRefH);
541
+ const cw = this._computedWidth;
542
+ const ch = this._computedHeight;
543
+ if (cfg.left !== undefined)
544
+ child.x = cfg.left;
545
+ else if (cfg.right !== undefined)
546
+ child.x = cw - w - cfg.right;
547
+ if (cfg.top !== undefined)
548
+ child.y = cfg.top;
549
+ else if (cfg.bottom !== undefined)
550
+ child.y = ch - h - cfg.bottom;
403
551
  }
404
552
  }
405
553
  /** Computed content size (after layout) */
406
554
  getContentSize() {
407
555
  if (this._layoutDirty)
408
556
  this.updateLayout();
409
- let maxX = 0;
410
- let maxY = 0;
411
- for (const child of this._layoutChildren) {
412
- const { w, h } = measureChild(child);
413
- maxX = Math.max(maxX, child.x + w);
414
- maxY = Math.max(maxY, child.y + h);
415
- }
416
- const [, pr, pb] = this._padding;
417
- return { width: maxX + pr, height: maxY + pb };
557
+ return { width: this._computedWidth, height: this._computedHeight };
418
558
  }
419
559
  /** React reconciler update hook — applies changed config props */
420
560
  updateConfig(changed) {
@@ -426,15 +566,34 @@ class FlexContainer extends pixi_js.Container {
426
566
  this.setAlignItems(changed.alignItems);
427
567
  if ('gap' in changed)
428
568
  this.setGap(changed.gap);
429
- if ('padding' in changed)
430
- this.setPadding(changed.padding);
569
+ if ('padding' in changed || 'paddingTop' in changed || 'paddingRight' in changed || 'paddingBottom' in changed || 'paddingLeft' in changed) {
570
+ this._padding = resolvePadding(changed);
571
+ this._layoutDirty = true;
572
+ }
431
573
  if ('flexWrap' in changed) {
432
574
  this._config.flexWrap = changed.flexWrap;
433
575
  this._layoutDirty = true;
434
576
  }
577
+ if ('alignContent' in changed) {
578
+ this._config.alignContent = changed.alignContent;
579
+ this._layoutDirty = true;
580
+ }
435
581
  if ('width' in changed || 'height' in changed) {
436
- this.resize(changed.width ?? this._explicitWidth, changed.height ?? this._explicitHeight);
437
- return; // resize calls updateLayout
582
+ const w = changed.width ?? this._rawWidth;
583
+ const h = changed.height ?? this._rawHeight;
584
+ this._rawWidth = w;
585
+ this._rawHeight = h;
586
+ if (typeof w === 'number' && typeof h === 'number') {
587
+ this.resize(w, h);
588
+ }
589
+ else {
590
+ // Percentage — will resolve in updateLayout
591
+ this._explicitWidth = typeof w === 'number' ? w : 0;
592
+ this._explicitHeight = typeof h === 'number' ? h : 0;
593
+ this._layoutDirty = true;
594
+ this.updateLayout();
595
+ }
596
+ return;
438
597
  }
439
598
  if (this._layoutDirty)
440
599
  this.updateLayout();