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