@caperjs/core 0.2.4 → 0.3.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.
Files changed (172) hide show
  1. package/build/index.mjs +3 -1
  2. package/build/internal/discovery.mjs +1 -1
  3. package/build/internal/manifest.mjs +8 -3
  4. package/build/internal/validate.mjs +3 -1
  5. package/build/plugins/assetTypes.mjs +4 -4
  6. package/build/plugins/caperConfig.mjs +12 -5
  7. package/build/plugins/lists.mjs +29 -9
  8. package/cli/create.mjs +57 -6
  9. package/cli/create.test.mjs +36 -0
  10. package/cli/update.mjs +1 -1
  11. package/lib/{CaptionsPlugin-WPAcfAkx.js → CaptionsPlugin-BLbLl3WB.js} +3 -3
  12. package/lib/{CaptionsPlugin-WPAcfAkx.js.map → CaptionsPlugin-BLbLl3WB.js.map} +1 -1
  13. package/lib/{DataAdapter-DS-twowe.js → DataAdapter-aHSNM_26.js} +12 -7
  14. package/lib/DataAdapter-aHSNM_26.js.map +1 -0
  15. package/lib/{DebugRenderer-D1ryrRYx.js → DebugRenderer-8LjHtBzA.js} +2 -2
  16. package/lib/{DebugRenderer-D1ryrRYx.js.map → DebugRenderer-8LjHtBzA.js.map} +1 -1
  17. package/lib/{DevToolsPlugin-DH7zX7HU.js → DevToolsPlugin-Vrk6aOtS.js} +2 -2
  18. package/lib/{DevToolsPlugin-DH7zX7HU.js.map → DevToolsPlugin-Vrk6aOtS.js.map} +1 -1
  19. package/lib/{GSAPPlugin-BBtRjUqe.js → GSAPPlugin-Hm3F4zdg.js} +3 -3
  20. package/lib/{GSAPPlugin-BBtRjUqe.js.map → GSAPPlugin-Hm3F4zdg.js.map} +1 -1
  21. package/lib/{LayoutPlugin-Co6vcoV4.js → LayoutPlugin-Dpgeawfw.js} +4 -4
  22. package/lib/{LayoutPlugin-Co6vcoV4.js.map → LayoutPlugin-Dpgeawfw.js.map} +1 -1
  23. package/lib/{SpinePlugin-uN641Hmk.js → SpinePlugin-B08yAw1S.js} +690 -793
  24. package/lib/SpinePlugin-B08yAw1S.js.map +1 -0
  25. package/lib/{StatsPlugin-vZQRm6wh.js → StatsPlugin-D2Xa5j97.js} +5 -2
  26. package/lib/StatsPlugin-D2Xa5j97.js.map +1 -0
  27. package/lib/{VoiceOverPlugin-KlZdGj1G.js → VoiceOverPlugin-qtFlqpRn.js} +6 -5
  28. package/lib/VoiceOverPlugin-qtFlqpRn.js.map +1 -0
  29. package/lib/caper.mjs +23 -10
  30. package/lib/caper.mjs.map +1 -1
  31. package/lib/core/Application.d.ts +8 -3
  32. package/lib/core/Application.d.ts.map +1 -1
  33. package/lib/core/Application.test.d.ts +2 -0
  34. package/lib/core/Application.test.d.ts.map +1 -0
  35. package/lib/core/config.d.ts +6 -1
  36. package/lib/core/config.d.ts.map +1 -1
  37. package/lib/core/config.test.d.ts +2 -0
  38. package/lib/core/config.test.d.ts.map +1 -0
  39. package/lib/core/interfaces/IApplication.d.ts +6 -6
  40. package/lib/core/interfaces/IApplication.d.ts.map +1 -1
  41. package/lib/core/interfaces/ICoreSignals.d.ts +2 -1
  42. package/lib/core/interfaces/ICoreSignals.d.ts.map +1 -1
  43. package/lib/display/Camera.test.d.ts +2 -0
  44. package/lib/display/Camera.test.d.ts.map +1 -0
  45. package/lib/display/Entity.d.ts +2 -2
  46. package/lib/display/SceneTransition.test.d.ts +1 -0
  47. package/lib/display/SceneTransition.test.d.ts.map +1 -0
  48. package/lib/display/SpineAnimation.d.ts +3 -3
  49. package/lib/mixins/factory/methods.d.ts.map +1 -1
  50. package/lib/plugins/AssetsPlugin.d.ts.map +1 -1
  51. package/lib/plugins/AssetsPlugin.test.d.ts +2 -0
  52. package/lib/plugins/AssetsPlugin.test.d.ts.map +1 -0
  53. package/lib/plugins/DataAdapter.d.ts.map +1 -1
  54. package/lib/plugins/DataAdapter.test.d.ts +2 -0
  55. package/lib/plugins/DataAdapter.test.d.ts.map +1 -0
  56. package/lib/plugins/FullScreenPlugin.d.ts.map +1 -1
  57. package/lib/plugins/FullScreenPlugin.test.d.ts +2 -0
  58. package/lib/plugins/FullScreenPlugin.test.d.ts.map +1 -0
  59. package/lib/plugins/GSAPPlugin.d.ts +0 -2
  60. package/lib/plugins/GSAPPlugin.d.ts.map +1 -1
  61. package/lib/plugins/GSAPPlugin.test.d.ts +2 -0
  62. package/lib/plugins/GSAPPlugin.test.d.ts.map +1 -0
  63. package/lib/plugins/LookupPlugin.d.ts +6 -1
  64. package/lib/plugins/LookupPlugin.d.ts.map +1 -1
  65. package/lib/plugins/LookupPlugin.test.d.ts +2 -0
  66. package/lib/plugins/LookupPlugin.test.d.ts.map +1 -0
  67. package/lib/plugins/PopupManagerPlugin.d.ts.map +1 -1
  68. package/lib/plugins/PopupManagerPlugin.test.d.ts +1 -0
  69. package/lib/plugins/PopupManagerPlugin.test.d.ts.map +1 -0
  70. package/lib/plugins/SceneManagerPlugin.d.ts +1 -0
  71. package/lib/plugins/SceneManagerPlugin.d.ts.map +1 -1
  72. package/lib/plugins/SceneManagerPlugin.test.d.ts +2 -0
  73. package/lib/plugins/SceneManagerPlugin.test.d.ts.map +1 -0
  74. package/lib/plugins/StatsPlugin.d.ts +1 -0
  75. package/lib/plugins/StatsPlugin.d.ts.map +1 -1
  76. package/lib/plugins/WebEventsPlugin.d.ts.map +1 -1
  77. package/lib/plugins/actions/ActionsPlugin.test.d.ts +2 -0
  78. package/lib/plugins/actions/ActionsPlugin.test.d.ts.map +1 -0
  79. package/lib/plugins/audio/AudioChannel.d.ts.map +1 -1
  80. package/lib/plugins/audio/AudioManagerPlugin.d.ts.map +1 -1
  81. package/lib/plugins/audio/AudioManagerPlugin.test.d.ts +2 -0
  82. package/lib/plugins/audio/AudioManagerPlugin.test.d.ts.map +1 -0
  83. package/lib/plugins/audio/VoiceOverPlugin.d.ts.map +1 -1
  84. package/lib/plugins/captions/CaptionsPlugin.test.d.ts +2 -0
  85. package/lib/plugins/captions/CaptionsPlugin.test.d.ts.map +1 -0
  86. package/lib/plugins/focus/FocusManagerPlugin.d.ts.map +1 -1
  87. package/lib/plugins/focus/FocusManagerPlugin.test.d.ts +2 -0
  88. package/lib/plugins/focus/FocusManagerPlugin.test.d.ts.map +1 -0
  89. package/lib/plugins/input/AbstractControls.d.ts +5 -0
  90. package/lib/plugins/input/AbstractControls.d.ts.map +1 -1
  91. package/lib/plugins/input/Controls.test.d.ts +2 -0
  92. package/lib/plugins/input/Controls.test.d.ts.map +1 -0
  93. package/lib/plugins/input/keyboard/KeyboardContols.d.ts +5 -0
  94. package/lib/plugins/input/keyboard/KeyboardContols.d.ts.map +1 -1
  95. package/lib/plugins/input/touch/VirtualControls.d.ts +6 -0
  96. package/lib/plugins/input/touch/VirtualControls.d.ts.map +1 -1
  97. package/lib/plugins/input/touch/VirtualControls.test.d.ts +2 -0
  98. package/lib/plugins/input/touch/VirtualControls.test.d.ts.map +1 -0
  99. package/lib/{registries-BLrxALZ0.js → registries-m2vKTIih.js} +156 -101
  100. package/lib/registries-m2vKTIih.js.map +1 -0
  101. package/lib/store/Store.d.ts.map +1 -1
  102. package/lib/ui/Button.d.ts.map +1 -1
  103. package/lib/ui/UICanvas.d.ts +10 -1
  104. package/lib/ui/UICanvas.d.ts.map +1 -1
  105. package/lib/ui/UICanvas.test.d.ts +0 -1
  106. package/lib/ui/UICanvas.test.d.ts.map +1 -1
  107. package/lib/utils/color.d.ts.map +1 -1
  108. package/lib/utils/color.test.d.ts +2 -0
  109. package/lib/utils/color.test.d.ts.map +1 -0
  110. package/package.json +1 -1
  111. package/src/core/Application.test.ts +168 -0
  112. package/src/core/Application.ts +37 -27
  113. package/src/core/config.test.ts +79 -0
  114. package/src/core/config.ts +28 -6
  115. package/src/core/interfaces/IApplication.ts +6 -6
  116. package/src/core/interfaces/ICoreSignals.ts +2 -0
  117. package/src/display/Camera.test.ts +40 -0
  118. package/src/display/Camera.ts +3 -3
  119. package/src/display/Entity.ts +2 -2
  120. package/src/display/SceneTransition.test.ts +58 -0
  121. package/src/display/SceneTransition.ts +1 -1
  122. package/src/display/SpineAnimation.ts +4 -4
  123. package/src/mixins/factory/methods.ts +0 -1
  124. package/src/plugins/AssetsPlugin.test.ts +48 -0
  125. package/src/plugins/AssetsPlugin.ts +3 -1
  126. package/src/plugins/DataAdapter.test.ts +32 -0
  127. package/src/plugins/DataAdapter.ts +5 -1
  128. package/src/plugins/FullScreenPlugin.test.ts +48 -0
  129. package/src/plugins/FullScreenPlugin.ts +5 -1
  130. package/src/plugins/GSAPPlugin.test.ts +31 -0
  131. package/src/plugins/GSAPPlugin.ts +1 -4
  132. package/src/plugins/LookupPlugin.test.ts +42 -0
  133. package/src/plugins/LookupPlugin.ts +15 -3
  134. package/src/plugins/PopupManagerPlugin.test.ts +53 -0
  135. package/src/plugins/PopupManagerPlugin.ts +5 -3
  136. package/src/plugins/SceneManagerPlugin.test.ts +76 -0
  137. package/src/plugins/SceneManagerPlugin.ts +21 -11
  138. package/src/plugins/StatsPlugin.ts +8 -0
  139. package/src/plugins/WebEventsPlugin.test.ts +32 -0
  140. package/src/plugins/WebEventsPlugin.ts +2 -0
  141. package/src/plugins/actions/ActionsPlugin.test.ts +68 -0
  142. package/src/plugins/actions/ActionsPlugin.ts +5 -5
  143. package/src/plugins/audio/AudioChannel.test.ts +19 -0
  144. package/src/plugins/audio/AudioChannel.ts +8 -1
  145. package/src/plugins/audio/AudioManagerPlugin.test.ts +41 -0
  146. package/src/plugins/audio/AudioManagerPlugin.ts +3 -2
  147. package/src/plugins/audio/VoiceOverPlugin.ts +1 -0
  148. package/src/plugins/captions/CaptionsPlugin.test.ts +40 -0
  149. package/src/plugins/captions/CaptionsPlugin.ts +1 -1
  150. package/src/plugins/focus/FocusManagerPlugin.test.ts +71 -0
  151. package/src/plugins/focus/FocusManagerPlugin.ts +7 -0
  152. package/src/plugins/input/AbstractControls.ts +6 -0
  153. package/src/plugins/input/Controls.test.ts +149 -0
  154. package/src/plugins/input/keyboard/KeyboardContols.ts +29 -5
  155. package/src/plugins/input/touch/VirtualControls.test.ts +38 -0
  156. package/src/plugins/input/touch/VirtualControls.ts +44 -10
  157. package/src/plugins/spine/SpinePlugin.ts +2 -2
  158. package/src/store/Store.test.ts +15 -0
  159. package/src/store/Store.ts +4 -1
  160. package/src/ui/Button.test.ts +12 -0
  161. package/src/ui/Button.ts +1 -0
  162. package/src/ui/UICanvas.test.ts +109 -6
  163. package/src/ui/UICanvas.ts +41 -3
  164. package/src/utils/color.test.ts +16 -0
  165. package/src/utils/color.ts +2 -1
  166. package/src/utils/platform.ts +1 -1
  167. package/src/version.ts +1 -1
  168. package/lib/DataAdapter-DS-twowe.js.map +0 -1
  169. package/lib/SpinePlugin-uN641Hmk.js.map +0 -1
  170. package/lib/StatsPlugin-vZQRm6wh.js.map +0 -1
  171. package/lib/VoiceOverPlugin-KlZdGj1G.js.map +0 -1
  172. package/lib/registries-BLrxALZ0.js.map +0 -1
@@ -1,9 +1,10 @@
1
- import { bindAllMethods } from '../../../utils';
1
+ import { bindAllMethods, Logger } from '../../../utils';
2
2
  import type { ControlsActionMap, JoystickControlsScheme, TouchControlsMap, TouchControlsScheme } from '../types';
3
3
 
4
4
  import { JoystickDirection } from '..';
5
5
  import { Application } from '../../../core/Application';
6
6
  import { WithSignals } from '../../../mixins/signals';
7
+ import type { SignalConnection } from '../../../signals';
7
8
  import type { IButton, IJoystick } from '../../../ui';
8
9
  import type { Action } from '../../actions';
9
10
  import { AbstractControls } from '../AbstractControls';
@@ -20,6 +21,8 @@ export class VirtualControls extends WithSignals(AbstractControls) {
20
21
  private _activeButtonDownIds: Map<string, Action> = new Map();
21
22
  private _activeButtonUpIds: Map<string, Action> = new Map();
22
23
  private _combinationsMap: Map<string[], Action> = new Map();
24
+ private _buttonConnections: Map<IButton, SignalConnection[]> = new Map();
25
+ private _warnedMissingActions: Set<string> = new Set();
23
26
 
24
27
  constructor() {
25
28
  super();
@@ -44,12 +47,14 @@ export class VirtualControls extends WithSignals(AbstractControls) {
44
47
  if (!button || this._buttons.has(button)) {
45
48
  return;
46
49
  }
47
- this.addSignalConnection(
50
+ const connections = [
48
51
  button.onDown.connect(() => this._handleButtonDown(button)),
49
52
  button.onUp.connect(() => this._handleButtonUp(button)),
50
53
  button.onUpOutside.connect(() => this._handleButtonUp(button)),
51
54
  button.onDestroy.connect(() => this.removeButton(button)),
52
- );
55
+ ];
56
+ this.addSignalConnection(...connections);
57
+ this._buttonConnections.set(button, connections);
53
58
  this._buttons.add(button);
54
59
  }
55
60
 
@@ -57,10 +62,10 @@ export class VirtualControls extends WithSignals(AbstractControls) {
57
62
  if (!button || !this._buttons.has(button)) {
58
63
  return;
59
64
  }
60
- button.onDown.disconnect(() => this._handleButtonDown(button));
61
- button.onUp.disconnect(() => this._handleButtonUp(button));
62
- button.onUpOutside.disconnect(() => this._handleButtonUp(button));
63
- button.onDestroy.disconnect(() => this.removeButton(button));
65
+ // signals disconnect by identity — disconnect the connections made in addButton
66
+ this._buttonConnections.get(button)?.forEach((connection) => connection.disconnect());
67
+ this._buttonConnections.delete(button);
68
+ this._singleDownButtons.delete(button.id!);
64
69
  this._buttons.delete(button);
65
70
  }
66
71
 
@@ -69,7 +74,7 @@ export class VirtualControls extends WithSignals(AbstractControls) {
69
74
  this._buttonDownMap = scheme.down || {};
70
75
  this._buttonUpMap = scheme.up || {};
71
76
  this._joystickMap = scheme.joystick || {};
72
- this.app.signal.onActionContextChanged.connect(this._sortActions);
77
+ this.addSignalConnection(this.app.signal.onActionContextChanged.connect(this._sortActions));
73
78
  this._sortActions();
74
79
  }
75
80
 
@@ -77,13 +82,18 @@ export class VirtualControls extends WithSignals(AbstractControls) {
77
82
  this.app.ticker.add(this._update);
78
83
  }
79
84
 
85
+ public destroy(): void {
86
+ this.app.ticker.remove(this._update);
87
+ super.destroy();
88
+ }
89
+
80
90
  isActionActive(action: Action): boolean {
81
91
  const buttonAction = this.scheme['down']?.[action] ?? null;
82
92
  if (buttonAction) {
83
93
  if (Array.isArray(buttonAction)) {
84
- return this._combinationsMap.has(buttonAction);
94
+ return buttonAction.some((input) => this._isInputActive(input));
85
95
  } else {
86
- return this._singleDownButtons.has(buttonAction);
96
+ return this._isInputActive(buttonAction);
87
97
  }
88
98
  } else {
89
99
  const joystickAction = this.scheme['joystick']?.[action] ?? null;
@@ -98,6 +108,22 @@ export class VirtualControls extends WithSignals(AbstractControls) {
98
108
  return false;
99
109
  }
100
110
 
111
+ /** A single button id, or a `+` separated combination requiring every input to be down. */
112
+ private _isInputActive(input: string): boolean {
113
+ if (input.includes('+')) {
114
+ return input.split('+').every((id) => this._singleDownButtons.has(id) || this._joystick?.direction === id);
115
+ }
116
+ return this._singleDownButtons.has(input);
117
+ }
118
+
119
+ private _warnMissingAction(key: string) {
120
+ if (this._warnedMissingActions.has(key)) {
121
+ return;
122
+ }
123
+ this._warnedMissingActions.add(key);
124
+ Logger.warn(`VirtualControls: scheme references unknown action "${key}"`);
125
+ }
126
+
101
127
  private _sortActions() {
102
128
  const actions = this.app.actionsPlugin.getActions();
103
129
  this._combinations = [];
@@ -110,6 +136,10 @@ export class VirtualControls extends WithSignals(AbstractControls) {
110
136
  buttons.forEach((key) => {
111
137
  const item = this._buttonDownMap[key];
112
138
  const action = actions[key];
139
+ if (!action) {
140
+ this._warnMissingAction(key);
141
+ return;
142
+ }
113
143
  if (
114
144
  action.context !== '*' &&
115
145
  action.context !== this.app.actionContext &&
@@ -141,6 +171,10 @@ export class VirtualControls extends WithSignals(AbstractControls) {
141
171
  buttons.forEach((key) => {
142
172
  const item = this._buttonUpMap[key];
143
173
  const action = actions[key];
174
+ if (!action) {
175
+ this._warnMissingAction(key);
176
+ return;
177
+ }
144
178
  if (
145
179
  action.context !== '*' &&
146
180
  action.context !== this.app.actionContext &&
@@ -1,6 +1,6 @@
1
1
  import { extensions } from 'pixi.js';
2
2
  import { Plugin } from '../Plugin';
3
- import { Spine, spineLoaderExtension, SpinePipe, spineTextureAtlasLoader } from './pixi-spine';
3
+ import { Spine, spineLoaderExtension, spineTextureAtlasLoader } from './pixi-spine';
4
4
 
5
5
  export class SpinePlugin extends Plugin {
6
6
  public readonly id = 'SpinePlugin';
@@ -8,7 +8,7 @@ export class SpinePlugin extends Plugin {
8
8
  public async initialize() {
9
9
  extensions.add(spineTextureAtlasLoader);
10
10
  extensions.add(spineLoaderExtension);
11
- extensions.add(SpinePipe);
11
+ // SpinePipe and DarkTintBatcher register themselves at module scope in ./pixi-spine
12
12
  (window as any).Spine = Spine;
13
13
  }
14
14
  }
@@ -143,4 +143,19 @@ describe('Store', () => {
143
143
  expect(saveA).toHaveBeenCalledWith('k', 1);
144
144
  expect(saveB).toHaveBeenCalledWith('k', 1);
145
145
  });
146
+
147
+ it('save with "*" anywhere in an adapter array fans out to every storage-capable plugin', async () => {
148
+ const saveA = vi.fn(async () => 'a');
149
+ const saveB = vi.fn(async () => 'b');
150
+ const store = new Store().initialize(
151
+ makeApp([
152
+ { id: 'a', save: saveA, load: async () => undefined },
153
+ { id: 'b', save: saveB, load: async () => undefined },
154
+ { id: 'c' }, // not storage-capable → must be skipped
155
+ ]),
156
+ );
157
+ await store.save(['a', '*'] as never, 'k', 1);
158
+ expect(saveA).toHaveBeenCalledWith('k', 1);
159
+ expect(saveB).toHaveBeenCalledWith('k', 1);
160
+ });
146
161
  });
@@ -126,7 +126,10 @@ export class Store implements IStore {
126
126
  keys = adapterId as string[] | Partial<AdapterSaveConfig>[];
127
127
  }
128
128
 
129
- if ((keys[0] as string) === '*' || (keys[0] as Partial<AdapterSaveConfig>)?.adapterId === '*') {
129
+ const hasStarKey = keys.some(
130
+ (k) => (k as string) === '*' || (k as Partial<AdapterSaveConfig>)?.adapterId === '*',
131
+ );
132
+ if (hasStarKey) {
130
133
  keys = this.allAdapterIds();
131
134
  }
132
135
 
@@ -147,6 +147,18 @@ describe('Button press-state machine', () => {
147
147
  expect(onClick).toHaveBeenCalledTimes(1);
148
148
  });
149
149
 
150
+ it('dispatches a click action with no data without throwing, attaching the button to the payload', () => {
151
+ const button = makeButton();
152
+ (button as any).config.actions = { click: { id: 'foo' } };
153
+ const action = vi.fn();
154
+ Object.defineProperty(button, 'app', { value: { action }, configurable: true });
155
+
156
+ (button as any).handlePointerDown({ pointerId: 1 });
157
+ expect(() => (button as any).handleClick({ pointerId: 1 })).not.toThrow();
158
+
159
+ expect(action).toHaveBeenCalledWith('foo', { button });
160
+ });
161
+
150
162
  it('still clicks for a synthetic keyboard activation (no pointerId)', () => {
151
163
  const button = makeButton();
152
164
  const onClick = vi.fn();
package/src/ui/Button.ts CHANGED
@@ -461,6 +461,7 @@ export class Button extends _Button implements IButton {
461
461
  if (typeof action === 'function') {
462
462
  void action();
463
463
  } else {
464
+ action.data ??= {};
464
465
  if (!action.data.button) {
465
466
  action.data.button = this;
466
467
  }
@@ -1,12 +1,29 @@
1
- import { describe, expect, it, vi } from 'vitest';
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { Container as PIXIContainer } from 'pixi.js';
2
3
 
3
4
  // UICanvas transitively imports Application → Pixi display graph. Stub it — the
4
- // pure padding helper needs none of it.
5
- vi.mock('../core/Application', () => ({
6
- Application: { getInstance: () => ({}) },
7
- }));
5
+ // pure padding helper needs none of it. The "element reordering" describe block
6
+ // below constructs real UICanvas instances, which internally build FlexContainer
7
+ // and Container children; those classes read `Application.getInstance()` for
8
+ // their own `app` getter (independent of UICanvas's own `app` override), so the
9
+ // stub needs a minimal shape (`config.useLayout`) satisfying their constructors too.
10
+ vi.mock('../core/Application', async () => {
11
+ const { Signal } = await import('../signals');
12
+ const fakeApp = {
13
+ config: { useLayout: true },
14
+ renderer: { layout: undefined },
15
+ ticker: { addOnce: () => {} },
16
+ onResize: new Signal(),
17
+ };
18
+ return { Application: { getInstance: () => fakeApp } };
19
+ });
8
20
 
9
- import { computeEffectivePadding } from './UICanvas';
21
+ // Registers the factory method table `Factory()` (part of Container's mixin
22
+ // chain) reads from — must be imported before any display/ui class is
23
+ // constructed. See importOrder.containerFirst.test.ts.
24
+ import '../mixins/factory/const';
25
+ import { Signal } from '../signals';
26
+ import { computeEffectivePadding, UICanvas } from './UICanvas';
10
27
 
11
28
  const zero = { top: 0, right: 0, bottom: 0, left: 0 };
12
29
 
@@ -33,3 +50,89 @@ describe('computeEffectivePadding', () => {
33
50
  expect(computeEffectivePadding(base, safeArea).top).toBe(54);
34
51
  });
35
52
  });
53
+
54
+ describe('UICanvas element reordering', () => {
55
+ let app: any;
56
+ let originalAppDescriptor: PropertyDescriptor | undefined;
57
+
58
+ beforeEach(() => {
59
+ app = {
60
+ config: { useLayout: true },
61
+ size: { width: 800, height: 600 },
62
+ onResize: new Signal<() => void>(),
63
+ ticker: { addOnce: vi.fn() },
64
+ };
65
+ originalAppDescriptor = Object.getOwnPropertyDescriptor(UICanvas.prototype, 'app');
66
+ Object.defineProperty(UICanvas.prototype, 'app', { get: () => app, configurable: true });
67
+ });
68
+
69
+ afterEach(() => {
70
+ if (originalAppDescriptor) {
71
+ Object.defineProperty(UICanvas.prototype, 'app', originalAppDescriptor);
72
+ } else {
73
+ delete (UICanvas.prototype as any).app;
74
+ }
75
+ });
76
+
77
+ function addThree(canvas: UICanvas) {
78
+ const a = new PIXIContainer({ label: 'a' });
79
+ const b = new PIXIContainer({ label: 'b' });
80
+ const c = new PIXIContainer({ label: 'c' });
81
+ canvas.addElement(a, { align: 'top left' });
82
+ canvas.addElement(b, { align: 'top left' });
83
+ canvas.addElement(c, { align: 'top left' });
84
+ const container = (canvas as any)._childMap.get(a);
85
+ return { a, b, c, container };
86
+ }
87
+
88
+ it('reorderElement moves an element within its region', () => {
89
+ const canvas = new UICanvas({});
90
+ const { a, b, c, container } = addThree(canvas);
91
+
92
+ canvas.reorderElement(a, 2);
93
+
94
+ expect(container.children.indexOf(a)).toBeGreaterThanOrEqual(0);
95
+ expect([b, c, a].every((child, i) => container.children[i] === child)).toBe(true);
96
+ });
97
+
98
+ it('bringToFront moves an element to the last index in its region', () => {
99
+ const canvas = new UICanvas({});
100
+ const { a, b, c, container } = addThree(canvas);
101
+
102
+ canvas.bringToFront(a);
103
+
104
+ expect([b, c, a].every((child, i) => container.children[i] === child)).toBe(true);
105
+ });
106
+
107
+ it('sendToBack moves an element to the first index in its region', () => {
108
+ const canvas = new UICanvas({});
109
+ const { a, b, c, container } = addThree(canvas);
110
+
111
+ canvas.sendToBack(c);
112
+
113
+ expect([c, a, b].every((child, i) => container.children[i] === child)).toBe(true);
114
+ });
115
+
116
+ it('reorderElement throws when the child was never added via addElement', () => {
117
+ const canvas = new UICanvas({});
118
+ const neverAdded = new PIXIContainer({ label: 'never-added' });
119
+
120
+ expect(() => canvas.reorderElement(neverAdded, 0)).toThrow(/Cannot reorder element/);
121
+ expect(() => canvas.reorderElement(neverAdded, 0)).toThrow(/not added via addElement/);
122
+ });
123
+
124
+ it('bringToFront and sendToBack throw when the child was never added via addElement', () => {
125
+ const canvas = new UICanvas({});
126
+ const neverAdded = new PIXIContainer({ label: 'never-added' });
127
+
128
+ expect(() => canvas.bringToFront(neverAdded)).toThrow(/Cannot reorder element/);
129
+ expect(() => canvas.sendToBack(neverAdded)).toThrow(/Cannot reorder element/);
130
+ });
131
+
132
+ it('setChildIndex throws a guard error instead of the old Pixi passthrough', () => {
133
+ const canvas = new UICanvas({});
134
+ const { a } = addThree(canvas);
135
+
136
+ expect(() => canvas.setChildIndex(a, 0)).toThrow(/Do not call setChildIndex/);
137
+ });
138
+ });
@@ -490,9 +490,11 @@ export class UICanvas extends _UICanvas {
490
490
  /**
491
491
  * Sets the index of the child in the container
492
492
  */
493
- public setChildIndex = <U extends PIXIContainer>(child: U, index: number): void => {
494
- super.setChildIndex(child, index);
495
- this._updateLayout();
493
+ public setChildIndex = <U extends PIXIContainer>(_child: U, _index: number): void => {
494
+ throw new Error(
495
+ `UICanvas: Do not call setChildIndex() directly. Use reorderElement(child, index), bringToFront(child), or sendToBack(child) instead.\n` +
496
+ `Example: uiCanvas.reorderElement(myChild, 0)`,
497
+ );
496
498
  };
497
499
 
498
500
  /**
@@ -668,6 +670,42 @@ export class UICanvas extends _UICanvas {
668
670
  return child;
669
671
  }
670
672
 
673
+ /**
674
+ * Reorders an element within its region container, changing its position in
675
+ * the flex layout and paint order.
676
+ */
677
+ public reorderElement<U extends PIXIContainer>(child: U, index: number): U {
678
+ const container = this._childMap.get(child);
679
+ if (!container) {
680
+ throw new Error(
681
+ `UICanvas: Cannot reorder element — it was not added via addElement().\n` +
682
+ `Child label: "${child.label ?? '(unlabeled)'}"`,
683
+ );
684
+ }
685
+
686
+ container.setChildIndex(child, index);
687
+ this._updateLayout();
688
+ return child;
689
+ }
690
+
691
+ /** Moves an element to the last index (painted/laid-out last) within its region. */
692
+ public bringToFront<U extends PIXIContainer>(child: U): U {
693
+ const container = this._childMap.get(child);
694
+ if (!container) {
695
+ throw new Error(
696
+ `UICanvas: Cannot reorder element — it was not added via addElement().\n` +
697
+ `Child label: "${child.label ?? '(unlabeled)'}"`,
698
+ );
699
+ }
700
+
701
+ return this.reorderElement(child, container.children.length - 1);
702
+ }
703
+
704
+ /** Moves an element to the first index (painted/laid-out first) within its region. */
705
+ public sendToBack<U extends PIXIContainer>(child: U): U {
706
+ return this.reorderElement(child, 0);
707
+ }
708
+
671
709
  private _childAdded(child: PIXIContainer) {
672
710
  if (this.config.autoLayoutChildren) {
673
711
  if (!child.layout) {
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Color } from './color';
3
+
4
+ describe('Color.rgbToHexString', () => {
5
+ it('rounds fractional rgb components to a valid 2-digit hex pair', () => {
6
+ expect(Color.rgbToHexString(127.5)).toBe('80');
7
+ expect(Color.rgbToHexString(0.4)).toBe('00');
8
+ expect(Color.rgbToHexString(254.9)).toBe('ff');
9
+ });
10
+
11
+ it('produces a valid 6-digit hex string from fractional rgb components', () => {
12
+ const hex = Color.rgbToFullHexString(127.5, 0.4, 254.9);
13
+ expect(hex).toMatch(/^[0-9a-f]{6}$/);
14
+ expect(hex).toBe('8000ff');
15
+ });
16
+ });
@@ -64,7 +64,8 @@ export class Color {
64
64
  }
65
65
 
66
66
  public static rgbToHexString(pNumber: number): string {
67
- let hex = Number(pNumber).toString(16);
67
+ const clamped = Math.max(0, Math.min(255, Math.round(Number(pNumber))));
68
+ let hex = clamped.toString(16);
68
69
  if (hex.length < 2) {
69
70
  hex = '0' + hex;
70
71
  }
@@ -20,7 +20,7 @@ export const isRetina =
20
20
  */
21
21
  export const isTouch: boolean =
22
22
  typeof window !== 'undefined'
23
- ? 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator?.maxTouchPoints > 0
23
+ ? 'ontouchstart' in window || navigator.maxTouchPoints > 0
24
24
  : false;
25
25
 
26
26
  /**
package/src/version.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // This file is auto-generated during the build process.
2
- export const version: string = '0.2.4';
2
+ export const version: string = '0.3.0';
3
3
  export const pixiVersion: string = '8.19.0';
@@ -1 +0,0 @@
1
- {"version":3,"file":"DataAdapter-DS-twowe.js","names":[],"sources":["../src/plugins/DataAdapter.ts"],"sourcesContent":["import { Signal } from '../signals';\nimport { AppTypeOverrides, deepMerge, DeepPartial } from '../utils';\nimport { Plugin } from './Plugin';\n\nexport type DataSchema = {\n [key: string]: any;\n};\n\n/**\n * Interface for the options of the DataAdapter.\n */\nexport interface IDataAdapterOptions {\n initial: Partial<AppTypeOverrides['Data']>;\n /**\n * The namespace to use for the keys in local storage.\n */\n namespace: string;\n /**\n * Whether to override the data with the data from local storage.\n */\n overrideWithLocalStorage: boolean;\n /**\n * Whether to backup all data to local storage.\n */\n backupAll: boolean;\n /**\n * The keys to backup to local storage.\n */\n backupKeys: Array<keyof AppTypeOverrides['Data']>;\n}\n\nexport interface DataChangeSignalDetail {\n key?: any;\n value?: any;\n restore?: boolean;\n clear?: boolean;\n}\n\ntype DataAdapterType = AppTypeOverrides['Data'];\n\nexport interface IDataAdapter {\n get(): DataAdapterType;\n get<K extends keyof DataAdapterType>(key: K): DataAdapterType[K] | undefined;\n set<K extends keyof DataAdapterType>(key: K, data: DataAdapterType[K]): DataAdapterType;\n set(data: DeepPartial<DataAdapterType>, merge?: boolean): DataAdapterType;\n clear(): void;\n clear<K extends keyof DataAdapterType>(key: K): void;\n increment<\n K extends keyof DataAdapterType &\n { [P in keyof DataAdapterType]: DataAdapterType[P] extends number ? P : never }[keyof DataAdapterType],\n >(\n key: K,\n amount?: number,\n ): DataAdapterType[K];\n concat<\n K extends keyof DataAdapterType &\n { [P in keyof DataAdapterType]: DataAdapterType[P] extends any[] ? P : never }[keyof DataAdapterType],\n >(\n key: K,\n value: DataAdapterType[K] extends (infer E)[] ? E | E[] : never,\n ): DataAdapterType[K];\n append<\n K extends keyof DataAdapterType &\n { [P in keyof DataAdapterType]: DataAdapterType[P] extends string ? P : never }[keyof DataAdapterType],\n >(\n key: K,\n value: string,\n separator?: string,\n ): DataAdapterType[K];\n snapshot(): DataAdapterType;\n snapshot<K extends keyof DataAdapterType>(key?: K): DataAdapterType[K];\n onDataChange: Signal<(detail: DataChangeSignalDetail) => void>;\n clearOnResume?: boolean;\n}\n\nexport function defineData<T extends DataSchema>(data: T): T {\n return data;\n}\n\n/**\n * An in-memory game-data plugin with optional localStorage backup.\n *\n * Not `IStorageCapability` — its API (`get`/`set`/`increment`/...) is richer\n * than the generic save/load contract. Accessed directly via `app.data`.\n */\nexport class DataAdapter extends Plugin<Partial<IDataAdapterOptions>> implements IDataAdapter {\n public data: DataAdapterType;\n\n public onDataChange: Signal<(detail: DataChangeSignalDetail) => void> = new Signal();\n\n private backupKeys: Array<keyof DataAdapterType> = [];\n private backupAll: boolean = false;\n private namespace: string = '';\n private overrideWithLocalStorage: boolean = true;\n\n constructor(id: string = 'data') {\n super(id);\n }\n\n public getCoreSignals(): string[] {\n return ['onDataChange'];\n }\n\n destroy() {\n this.data = {} as DataAdapterType;\n }\n\n public initialize(options?: Partial<IDataAdapterOptions>): void {\n this.namespace = options?.namespace || this.app.appName;\n this.data = (options?.initial as DataAdapterType) || ({} as DataAdapterType);\n this.backupKeys = options?.backupKeys || [];\n this.backupAll = options?.backupAll || false;\n this.overrideWithLocalStorage = options?.overrideWithLocalStorage === false ? false : true;\n\n if (this.backupAll || this.backupKeys.length > 0) {\n if (this.overrideWithLocalStorage) {\n this.restoreFromLocalStorage(this.backupKeys);\n }\n this.backupToLocalStorage(this.backupKeys);\n }\n }\n\n set<K extends keyof DataAdapterType>(key: K, data: DataAdapterType[K]): DataAdapterType;\n set(data: DeepPartial<DataAdapterType>, merge?: boolean): DataAdapterType;\n set<K extends keyof DataAdapterType>(\n keyOrData: K | DeepPartial<DataAdapterType>,\n dataOrMerge?: DataAdapterType[K] | boolean,\n ): DataAdapterType {\n if (typeof keyOrData === 'string' || typeof keyOrData === 'number' || typeof keyOrData === 'symbol') {\n const key = keyOrData;\n const data = dataOrMerge as DataAdapterType[K];\n\n this.data[key] = data;\n if (this.backupAll || this.backupKeys.includes(key)) {\n this.backupToLocalStorage([key]);\n }\n this.onDataChange.emit({ key, value: data });\n return this.data;\n }\n\n const data = keyOrData as DeepPartial<DataAdapterType>;\n const merge = (dataOrMerge as boolean) ?? true;\n\n if (merge) {\n this.data = deepMerge({ ...this.data }, data);\n } else {\n this.data = data as DataAdapterType;\n }\n\n if (this.backupAll || this.backupKeys.length > 0) {\n this.backupToLocalStorage(this.backupKeys);\n }\n\n this.onDataChange.emit({\n key: Object.keys(data)?.length === 1 ? Object.keys(data)[0] : Object.keys(data),\n value: data[Object.keys(data)[0]] as DataAdapterType[keyof DataAdapterType],\n });\n\n return this.data;\n }\n\n get(): DataAdapterType;\n get<K extends keyof DataAdapterType>(key?: K): DataAdapterType[K] | undefined;\n get<K extends keyof DataAdapterType>(key?: K): DataAdapterType[K] | DataAdapterType | undefined {\n if (key === undefined) {\n return this.data;\n }\n return this.data[key];\n }\n\n increment<\n K extends keyof DataAdapterType &\n { [P in keyof DataAdapterType]: DataAdapterType[P] extends number ? P : never }[keyof DataAdapterType],\n >(key: K, amount: number = 1): DataAdapterType[K] {\n const currentValue = this.data[key] || 0;\n const newValue = currentValue + amount;\n this.set({ [key]: newValue } as DeepPartial<DataAdapterType>);\n return newValue as DataAdapterType[K];\n }\n\n concat<\n K extends keyof DataAdapterType &\n { [P in keyof DataAdapterType]: DataAdapterType[P] extends any[] ? P : never }[keyof DataAdapterType],\n >(key: K, value: DataAdapterType[K] extends (infer E)[] ? E | E[] : never): DataAdapterType[K] {\n const currentValue = this.data[key];\n const newValue = [...currentValue, ...(Array.isArray(value) ? value : [value])];\n this.set({ [key]: newValue } as DeepPartial<DataAdapterType>);\n return newValue as DataAdapterType[K];\n }\n\n append<\n K extends keyof DataAdapterType &\n { [P in keyof DataAdapterType]: DataAdapterType[P] extends string ? P : never }[keyof DataAdapterType],\n >(key: K, value: string, separator: string = ''): DataAdapterType[K] {\n const currentValue = this.data[key];\n let newValue = value;\n if ((currentValue as string)?.length > 0) {\n newValue = currentValue + separator + value;\n }\n this.set({ [key]: newValue } as DeepPartial<DataAdapterType>);\n return newValue as DataAdapterType[K];\n }\n\n clear(): void;\n clear<K extends keyof DataAdapterType>(key?: K): void {\n if (key === undefined) {\n this.data = {} as DataAdapterType;\n localStorage.clear();\n this.onDataChange.emit({ key, clear: true });\n } else {\n delete this.data[key];\n localStorage.removeItem(`${this.namespace}-${key as string}`);\n this.onDataChange.emit({ clear: true });\n }\n }\n\n snapshot(): DataAdapterType;\n snapshot<K extends keyof DataAdapterType>(key?: K): DataAdapterType[K];\n snapshot<K extends keyof DataAdapterType>(key?: K): DataAdapterType[K] | DataAdapterType {\n if (key === undefined) {\n return JSON.parse(JSON.stringify(this.data));\n }\n return JSON.parse(JSON.stringify(this.data[key]));\n }\n\n private backupToLocalStorage(keys?: Array<keyof DataAdapterType>): void {\n const dataToBackup =\n keys && keys?.length > 0 ? Object.fromEntries(keys.map((key) => [key, this.data[key]])) : this.data;\n\n for (const key in dataToBackup) {\n localStorage.setItem(`${this.namespace}-${key}`, JSON.stringify(dataToBackup[key]));\n }\n }\n\n private restoreFromLocalStorage(keys?: Array<keyof DataAdapterType>): void {\n const keysToRestore = keys && keys?.length > 0 ? keys : (Object.keys(this.data) as Array<keyof DataAdapterType>);\n keysToRestore.forEach((key) => {\n try {\n const loadedData = localStorage.getItem(`${this.namespace}-${String(key)}`);\n if (loadedData !== null) {\n try {\n const parsedData = JSON.parse(loadedData);\n this.data[key] = parsedData;\n } catch (parseError) {\n console.warn(\n `Failed to parse data for key \"${String(key)}\" in namespace \"${this.namespace}\". Using default value.`,\n );\n }\n }\n } catch (storageError) {\n console.error(\n `Error accessing localStorage for key \"${String(key)}\" in namespace \"${this.namespace}\":`,\n storageError,\n );\n }\n });\n this.onDataChange.emit({ restore: true });\n }\n}\n"],"mappings":"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"}