@caperjs/core 0.3.0 → 0.4.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/build/assetpack.mjs +90 -0
- package/build/plugins/pruneFallbacks.mjs +25 -2
- package/lib/{CaptionsPlugin-BLbLl3WB.js → CaptionsPlugin-cSJj-R9M.js} +4 -4
- package/lib/{CaptionsPlugin-BLbLl3WB.js.map → CaptionsPlugin-cSJj-R9M.js.map} +1 -1
- package/lib/{DataAdapter-aHSNM_26.js → DataAdapter-D2tc6hx9.js} +5 -5
- package/lib/{DataAdapter-aHSNM_26.js.map → DataAdapter-D2tc6hx9.js.map} +1 -1
- package/lib/{DebugRenderer-8LjHtBzA.js → DebugRenderer-CGUKy6Fy.js} +2 -2
- package/lib/{DebugRenderer-8LjHtBzA.js.map → DebugRenderer-CGUKy6Fy.js.map} +1 -1
- package/lib/{DevToolsPlugin-Vrk6aOtS.js → DevToolsPlugin-B8GX3H9b.js} +2 -2
- package/lib/{DevToolsPlugin-Vrk6aOtS.js.map → DevToolsPlugin-B8GX3H9b.js.map} +1 -1
- package/lib/{GSAPPlugin-Hm3F4zdg.js → GSAPPlugin-Br9AjuTD.js} +5 -5
- package/lib/{GSAPPlugin-Hm3F4zdg.js.map → GSAPPlugin-Br9AjuTD.js.map} +1 -1
- package/lib/{LayoutPlugin-Dpgeawfw.js → LayoutPlugin-BX3OP7pw.js} +4 -4
- package/lib/{LayoutPlugin-Dpgeawfw.js.map → LayoutPlugin-BX3OP7pw.js.map} +1 -1
- package/lib/{SpinePlugin-B08yAw1S.js → SpinePlugin-C-JDdXpf.js} +2 -2
- package/lib/{SpinePlugin-B08yAw1S.js.map → SpinePlugin-C-JDdXpf.js.map} +1 -1
- package/lib/{StatsPlugin-D2Xa5j97.js → StatsPlugin-yJlJtoQ2.js} +7 -10
- package/lib/{StatsPlugin-D2Xa5j97.js.map → StatsPlugin-yJlJtoQ2.js.map} +1 -1
- package/lib/{VoiceOverPlugin-qtFlqpRn.js → VoiceOverPlugin-DjSZp4Ko.js} +4 -4
- package/lib/{VoiceOverPlugin-qtFlqpRn.js.map → VoiceOverPlugin-DjSZp4Ko.js.map} +1 -1
- package/lib/caper.mjs +156 -1927
- package/lib/caper.mjs.map +1 -1
- package/lib/const-C3sxeYhl.js +1746 -0
- package/lib/const-C3sxeYhl.js.map +1 -0
- package/lib/core/create.d.ts.map +1 -1
- package/lib/padding-1kHDpGlQ.js +41 -0
- package/lib/padding-1kHDpGlQ.js.map +1 -0
- package/lib/plugins/FullScreenPlugin.d.ts +0 -5
- package/lib/plugins/FullScreenPlugin.d.ts.map +1 -1
- package/lib/plugins/KeyboardPlugin.d.ts +0 -1
- package/lib/plugins/KeyboardPlugin.d.ts.map +1 -1
- package/lib/plugins/Plugin.d.ts +101 -2
- package/lib/plugins/Plugin.d.ts.map +1 -1
- package/lib/plugins/SceneManagerPlugin.d.ts +0 -1
- package/lib/plugins/SceneManagerPlugin.d.ts.map +1 -1
- package/lib/plugins/StatsPlugin.d.ts +0 -1
- package/lib/plugins/StatsPlugin.d.ts.map +1 -1
- package/lib/plugins/TimerPlugin.d.ts.map +1 -1
- package/lib/plugins/WebEventsPlugin.d.ts +0 -1
- package/lib/plugins/WebEventsPlugin.d.ts.map +1 -1
- package/lib/plugins/breakpoints/BreakpointPlugin.d.ts.map +1 -1
- package/lib/plugins/gesture/GesturePlugin.d.ts.map +1 -1
- package/lib/plugins/input/AbstractControls.d.ts +46 -7
- package/lib/plugins/input/AbstractControls.d.ts.map +1 -1
- package/lib/plugins/input/InputPlugin.d.ts.map +1 -1
- package/lib/plugins/input/controlsCore.d.ts +58 -0
- package/lib/plugins/input/controlsCore.d.ts.map +1 -0
- package/lib/plugins/input/controlsCore.test.d.ts +2 -0
- package/lib/plugins/input/controlsCore.test.d.ts.map +1 -0
- package/lib/plugins/input/keyboard/KeyboardControls.d.ts +15 -0
- package/lib/plugins/input/keyboard/KeyboardControls.d.ts.map +1 -0
- package/lib/plugins/input/keyboard/index.d.ts +1 -1
- package/lib/plugins/input/keyboard/index.d.ts.map +1 -1
- package/lib/plugins/input/touch/VirtualControls.d.ts +7 -19
- package/lib/plugins/input/touch/VirtualControls.d.ts.map +1 -1
- package/lib/{registries-m2vKTIih.js → registries-BfV-VF7a.js} +565 -552
- package/lib/registries-BfV-VF7a.js.map +1 -0
- package/package.json +1 -1
- package/src/core/create.ts +11 -0
- package/src/plugins/FullScreenPlugin.ts +5 -18
- package/src/plugins/KeyboardPlugin.ts +3 -10
- package/src/plugins/Plugin.test.ts +130 -2
- package/src/plugins/Plugin.ts +159 -3
- package/src/plugins/SceneManagerPlugin.ts +3 -8
- package/src/plugins/StatsPlugin.ts +3 -10
- package/src/plugins/TimerPlugin.ts +1 -2
- package/src/plugins/WebEventsPlugin.ts +9 -18
- package/src/plugins/breakpoints/BreakpointPlugin.ts +1 -2
- package/src/plugins/captions/CaptionsPlugin.ts +1 -1
- package/src/plugins/gesture/GesturePlugin.ts +4 -8
- package/src/plugins/input/AbstractControls.ts +103 -8
- package/src/plugins/input/Controls.test.ts +56 -0
- package/src/plugins/input/InputPlugin.ts +6 -14
- package/src/plugins/input/controlsCore.test.ts +151 -0
- package/src/plugins/input/controlsCore.ts +162 -0
- package/src/plugins/input/keyboard/KeyboardControls.ts +84 -0
- package/src/plugins/input/keyboard/index.ts +1 -1
- package/src/plugins/input/touch/VirtualControls.ts +41 -142
- package/src/version.ts +1 -1
- package/lib/plugins/input/keyboard/KeyboardContols.d.ts +0 -33
- package/lib/plugins/input/keyboard/KeyboardContols.d.ts.map +0 -1
- package/lib/registries-m2vKTIih.js.map +0 -1
- package/src/plugins/input/keyboard/KeyboardContols.ts +0 -206
package/package.json
CHANGED
package/src/core/create.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type RegisterSWOptions } from 'vite-plugin-pwa/types';
|
|
2
2
|
import { sayHello } from '../hello';
|
|
3
|
+
import { setDefaultFactoryMethods } from '../mixins/factory/defaults';
|
|
3
4
|
import type { PluginListItem } from '../plugins';
|
|
4
5
|
import type { AppTypeOverrides, SceneImportListItem } from '../utils';
|
|
5
6
|
import { triggerViteError } from '../utils/vite';
|
|
@@ -147,6 +148,16 @@ export async function create(
|
|
|
147
148
|
domElement: string | Window | HTMLElement = DEFAULT_GAME_CONTAINER_ID,
|
|
148
149
|
speak: boolean = true,
|
|
149
150
|
): Promise<App> {
|
|
151
|
+
// The package declares `sideEffects: false`, which lets an app's bundler drop
|
|
152
|
+
// const.ts's top-level registration from production builds. Re-registering here
|
|
153
|
+
// anchors the table to create(), which every app calls, so it can't be shaken.
|
|
154
|
+
// The import MUST stay dynamic: a static `core → const.ts` edge closes the
|
|
155
|
+
// factory/ui module cycle (see mixins/factory/defaults.ts and the
|
|
156
|
+
// importOrder.*.test.ts guards). By the time create() runs, nothing is
|
|
157
|
+
// mid-evaluation, so loading const.ts here is safe. Idempotent in dev/test,
|
|
158
|
+
// where the module side effect already registered the table.
|
|
159
|
+
const { defaultFactoryMethods } = await import('../mixins/factory/const');
|
|
160
|
+
setDefaultFactoryMethods(defaultFactoryMethods);
|
|
150
161
|
await documentReady();
|
|
151
162
|
checkWebGL();
|
|
152
163
|
if (speak) {
|
|
@@ -143,24 +143,11 @@ export class FullScreenPlugin extends Plugin implements IFullScreenPlugin {
|
|
|
143
143
|
* ```
|
|
144
144
|
*/
|
|
145
145
|
public initialize(): void {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Cleans up the plugin by removing all event listeners.
|
|
155
|
-
* Called automatically when the plugin is destroyed.
|
|
156
|
-
*/
|
|
157
|
-
public destroy() {
|
|
158
|
-
document.removeEventListener('fullscreenchange', this._onFullScreenChange);
|
|
159
|
-
document.removeEventListener('webkitfullscreenchange', this._onFullScreenChange);
|
|
160
|
-
document.removeEventListener('mozfullscreenchange', this._onFullScreenChange);
|
|
161
|
-
document.removeEventListener('msfullscreenchange', this._onFullScreenChange);
|
|
162
|
-
document.removeEventListener('fullscreenchange', this._onFullScreenChange);
|
|
163
|
-
super.destroy();
|
|
146
|
+
this.listen(document, 'fullscreenchange', this._onFullScreenChange);
|
|
147
|
+
this.listen(document, 'webkitfullscreenchange', this._onFullScreenChange);
|
|
148
|
+
this.listen(document, 'mozfullscreenchange', this._onFullScreenChange);
|
|
149
|
+
this.listen(document, 'msfullscreenchange', this._onFullScreenChange);
|
|
150
|
+
this.listen(document, 'fullscreenchange', this._onFullScreenChange);
|
|
164
151
|
}
|
|
165
152
|
|
|
166
153
|
/**
|
|
@@ -61,15 +61,8 @@ export class KeyboardPlugin extends Plugin implements IKeyboardPlugin {
|
|
|
61
61
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
62
62
|
public initialize(_options: any, _app: IApplication): void {
|
|
63
63
|
// track which keys are down
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
public destroy() {
|
|
69
|
-
document.removeEventListener('keydown', this._handleKeyDown);
|
|
70
|
-
document.removeEventListener('keyup', this._handleKeyUp);
|
|
71
|
-
document.removeEventListener('keydown', this._handleEvent);
|
|
72
|
-
document.removeEventListener('keyup', this._handleEvent);
|
|
64
|
+
this.listen(document, 'keydown', this._handleKeyDown as EventListener);
|
|
65
|
+
this.listen(document, 'keyup', this._handleKeyUp as EventListener);
|
|
73
66
|
}
|
|
74
67
|
|
|
75
68
|
public onKeyDown(key?: KeyboardKey): KeySignal {
|
|
@@ -145,7 +138,7 @@ export class KeyboardPlugin extends Plugin implements IKeyboardPlugin {
|
|
|
145
138
|
}
|
|
146
139
|
|
|
147
140
|
private _listen(eventType: KeyboardEventType): void {
|
|
148
|
-
|
|
141
|
+
this.listen(document, eventType, this._handleEvent as EventListener);
|
|
149
142
|
}
|
|
150
143
|
|
|
151
144
|
private _handleEvent(event: KeyboardEvent): void {
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
|
|
3
3
|
// Plugin.ts transitively imports Application → Pixi display graph. Stub it.
|
|
4
4
|
vi.mock('../core', () => ({
|
|
5
5
|
coreFunctionRegistry: {},
|
|
6
6
|
coreSignalRegistry: {},
|
|
7
7
|
}));
|
|
8
|
+
|
|
9
|
+
const { mockTicker } = vi.hoisted(() => ({
|
|
10
|
+
mockTicker: { add: vi.fn(), remove: vi.fn() },
|
|
11
|
+
}));
|
|
12
|
+
|
|
8
13
|
vi.mock('../core/Application', () => ({
|
|
9
|
-
Application: { getInstance: () => ({}) },
|
|
14
|
+
Application: { getInstance: () => ({ ticker: mockTicker }) },
|
|
10
15
|
}));
|
|
11
16
|
|
|
12
17
|
import { Signal } from '../signals';
|
|
18
|
+
import { Logger } from '../utils';
|
|
13
19
|
import { Plugin } from './Plugin';
|
|
14
20
|
|
|
15
21
|
class TestPlugin extends Plugin<{ foo: string }> {
|
|
@@ -71,3 +77,125 @@ describe('Plugin', () => {
|
|
|
71
77
|
await expect(p.postInitialize({} as never)).resolves.toBeUndefined();
|
|
72
78
|
});
|
|
73
79
|
});
|
|
80
|
+
|
|
81
|
+
describe('Plugin lifecycle cleanup', () => {
|
|
82
|
+
beforeEach(() => {
|
|
83
|
+
vi.restoreAllMocks();
|
|
84
|
+
mockTicker.add.mockClear();
|
|
85
|
+
mockTicker.remove.mockClear();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('runs disposers LIFO, and only once', () => {
|
|
89
|
+
const p = new TestPlugin();
|
|
90
|
+
const order: string[] = [];
|
|
91
|
+
p.addDisposer(
|
|
92
|
+
() => order.push('a'),
|
|
93
|
+
() => order.push('b'),
|
|
94
|
+
);
|
|
95
|
+
p.addDisposer(() => order.push('c'));
|
|
96
|
+
|
|
97
|
+
p.destroy();
|
|
98
|
+
expect(order).toEqual(['c', 'b', 'a']);
|
|
99
|
+
|
|
100
|
+
p.destroy();
|
|
101
|
+
expect(order).toEqual(['c', 'b', 'a']);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('destroy is idempotent for signal connections too', () => {
|
|
105
|
+
const p = new TestPlugin();
|
|
106
|
+
const s = new Signal<() => void>();
|
|
107
|
+
const handler = vi.fn();
|
|
108
|
+
p.addSignalConnection(s.connect(handler));
|
|
109
|
+
|
|
110
|
+
p.destroy();
|
|
111
|
+
expect(() => p.destroy()).not.toThrow();
|
|
112
|
+
s.emit();
|
|
113
|
+
expect(handler).not.toHaveBeenCalled();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('a throwing disposer does not stop the rest, and is reported', () => {
|
|
117
|
+
const errorSpy = vi.spyOn(Logger, 'error').mockImplementation(() => undefined);
|
|
118
|
+
const p = new TestPlugin();
|
|
119
|
+
const first = vi.fn();
|
|
120
|
+
const last = vi.fn();
|
|
121
|
+
|
|
122
|
+
p.addDisposer(first);
|
|
123
|
+
p.addDisposer(() => {
|
|
124
|
+
throw new Error('boom');
|
|
125
|
+
});
|
|
126
|
+
p.addDisposer(last);
|
|
127
|
+
|
|
128
|
+
expect(() => p.destroy()).not.toThrow();
|
|
129
|
+
expect(first).toHaveBeenCalledTimes(1);
|
|
130
|
+
expect(last).toHaveBeenCalledTimes(1);
|
|
131
|
+
expect(errorSpy).toHaveBeenCalledTimes(1);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('listen() attaches now and detaches on destroy', () => {
|
|
135
|
+
const p = new TestPlugin();
|
|
136
|
+
const target = document.createElement('div');
|
|
137
|
+
const handler = vi.fn();
|
|
138
|
+
|
|
139
|
+
p.listen(target, 'click', handler);
|
|
140
|
+
target.dispatchEvent(new Event('click'));
|
|
141
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
142
|
+
|
|
143
|
+
p.destroy();
|
|
144
|
+
target.dispatchEvent(new Event('click'));
|
|
145
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('listen() detaches capture-phase listeners with matching options', () => {
|
|
149
|
+
const p = new TestPlugin();
|
|
150
|
+
const parent = document.createElement('div');
|
|
151
|
+
const child = document.createElement('div');
|
|
152
|
+
parent.appendChild(child);
|
|
153
|
+
document.body.appendChild(parent);
|
|
154
|
+
const handler = vi.fn();
|
|
155
|
+
|
|
156
|
+
p.listen(parent, 'click', handler, true);
|
|
157
|
+
child.dispatchEvent(new Event('click', { bubbles: true }));
|
|
158
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
159
|
+
|
|
160
|
+
p.destroy();
|
|
161
|
+
child.dispatchEvent(new Event('click', { bubbles: true }));
|
|
162
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
163
|
+
|
|
164
|
+
parent.remove();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('listen() returns an early-removal fn that is safe to call twice', () => {
|
|
168
|
+
const p = new TestPlugin();
|
|
169
|
+
const target = document.createElement('div');
|
|
170
|
+
const handler = vi.fn();
|
|
171
|
+
|
|
172
|
+
const remove = p.listen(target, 'click', handler);
|
|
173
|
+
remove();
|
|
174
|
+
remove();
|
|
175
|
+
target.dispatchEvent(new Event('click'));
|
|
176
|
+
expect(handler).not.toHaveBeenCalled();
|
|
177
|
+
expect(() => p.destroy()).not.toThrow();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('addTickerCallback() adds to the app ticker and removes on destroy', () => {
|
|
181
|
+
const p = new TestPlugin();
|
|
182
|
+
const fn = vi.fn();
|
|
183
|
+
|
|
184
|
+
p.addTickerCallback(fn, p, 5);
|
|
185
|
+
expect(mockTicker.add).toHaveBeenCalledWith(fn, p, 5);
|
|
186
|
+
|
|
187
|
+
p.destroy();
|
|
188
|
+
expect(mockTicker.remove).toHaveBeenCalledWith(fn, p);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('addTickerCallback() returns an early-removal fn that only removes once', () => {
|
|
192
|
+
const p = new TestPlugin();
|
|
193
|
+
const fn = vi.fn();
|
|
194
|
+
|
|
195
|
+
const remove = p.addTickerCallback(fn);
|
|
196
|
+
remove();
|
|
197
|
+
remove();
|
|
198
|
+
p.destroy();
|
|
199
|
+
expect(mockTicker.remove).toHaveBeenCalledTimes(1);
|
|
200
|
+
});
|
|
201
|
+
});
|
package/src/plugins/Plugin.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import type { Ticker } from 'pixi.js';
|
|
1
2
|
import type { IApplication, ICoreFunctions, ICoreSignals } from '../core';
|
|
2
3
|
import { coreFunctionRegistry, coreSignalRegistry } from '../core';
|
|
3
4
|
import { Application } from '../core/Application';
|
|
4
5
|
import { SignalConnection, SignalConnections } from '../signals';
|
|
5
|
-
import { type AppTypeOverrides, bindAllMethods, ImportListItemModule } from '../utils';
|
|
6
|
+
import { type AppTypeOverrides, bindAllMethods, ImportListItemModule, Logger } from '../utils';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Public contract for a Caper plugin. A plugin is a long-lived object
|
|
@@ -25,7 +26,40 @@ import { type AppTypeOverrides, bindAllMethods, ImportListItemModule } from '../
|
|
|
25
26
|
* methods are called from scenes / other plugins / the app.
|
|
26
27
|
* 5. `destroy()` — called when the app shuts down. Tear down
|
|
27
28
|
* connections, disconnect signals, free resources. The base class
|
|
28
|
-
* already disconnects everything added via `addSignalConnection
|
|
29
|
+
* already disconnects everything added via `addSignalConnection`
|
|
30
|
+
* and runs everything registered via the cleanup primitives below.
|
|
31
|
+
*
|
|
32
|
+
* **Cleanup primitives.** Don't hand-roll matching add/remove pairs —
|
|
33
|
+
* register the resource through the base class and `destroy()` releases
|
|
34
|
+
* it for you. Overriding `destroy()` is still fine; just call
|
|
35
|
+
* `super.destroy()`.
|
|
36
|
+
*
|
|
37
|
+
* - `addSignalConnection(...)` — signal connections.
|
|
38
|
+
* - `listen(target, type, handler, options?)` — DOM listeners. Adds
|
|
39
|
+
* now, removes on destroy with the same capture/options semantics.
|
|
40
|
+
* - `addTickerCallback(fn, context?, priority?)` — ticker callbacks on
|
|
41
|
+
* `app.ticker`. Added now, removed on destroy.
|
|
42
|
+
* - `addDisposer(...fns)` — anything else (DOM nodes, timers, third
|
|
43
|
+
* party handles). Disposers run last-in-first-out on destroy, each
|
|
44
|
+
* isolated so one failure can't skip the others.
|
|
45
|
+
*
|
|
46
|
+
* `listen` and `addTickerCallback` also return a removal function, for
|
|
47
|
+
* the rarer case where you need to detach before the plugin dies. It's
|
|
48
|
+
* safe to call more than once.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* public initialize(): void {
|
|
53
|
+
* this.listen(window, 'resize', this._onResize);
|
|
54
|
+
* this.addTickerCallback(this._update);
|
|
55
|
+
* this.addDisposer(() => this._el.remove());
|
|
56
|
+
* }
|
|
57
|
+
* // no destroy() override needed — the base class cleans all three up
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* Listeners that get attached and detached repeatedly at runtime (an
|
|
61
|
+
* activate/deactivate cycle, say) should stay manual — the primitives
|
|
62
|
+
* are for resources whose lifetime matches the plugin's.
|
|
29
63
|
*
|
|
30
64
|
* Discovery: plugin classes under `src/plugins/` are auto-discovered
|
|
31
65
|
* by the Vite plugin if they default-export a class. Annotate with
|
|
@@ -79,7 +113,11 @@ export interface IPlugin<O = any> {
|
|
|
79
113
|
*/
|
|
80
114
|
postInitialize(_app: IApplication): Promise<void> | void;
|
|
81
115
|
|
|
82
|
-
/**
|
|
116
|
+
/**
|
|
117
|
+
* Tear down. Called on app shutdown. Runs every registered disposer
|
|
118
|
+
* (last-in-first-out) and disconnects every tracked signal connection.
|
|
119
|
+
* Safe to call more than once — the second call is a no-op.
|
|
120
|
+
*/
|
|
83
121
|
destroy(): void;
|
|
84
122
|
|
|
85
123
|
/**
|
|
@@ -91,6 +129,40 @@ export interface IPlugin<O = any> {
|
|
|
91
129
|
/** Disconnect every connection added via `addSignalConnection`. */
|
|
92
130
|
clearSignalConnections(): void;
|
|
93
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Register cleanup callbacks to run on `destroy`. Use for resources
|
|
134
|
+
* the other primitives don't cover — DOM nodes, timers, third-party
|
|
135
|
+
* handles.
|
|
136
|
+
*
|
|
137
|
+
* Disposers run last-in-first-out (so teardown mirrors setup), and each
|
|
138
|
+
* one is isolated: a throwing disposer is reported and the rest still
|
|
139
|
+
* run.
|
|
140
|
+
*/
|
|
141
|
+
addDisposer(...fns: Array<() => void>): void;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Add a DOM event listener now and remove it on `destroy`, with the
|
|
145
|
+
* same capture/options semantics it was added with.
|
|
146
|
+
*
|
|
147
|
+
* @returns A removal function, for the rarer case where the listener
|
|
148
|
+
* must come off before the plugin dies. Safe to call more than once.
|
|
149
|
+
*/
|
|
150
|
+
listen(
|
|
151
|
+
target: EventTarget,
|
|
152
|
+
type: string,
|
|
153
|
+
handler: EventListenerOrEventListenerObject,
|
|
154
|
+
options?: AddEventListenerOptions | boolean,
|
|
155
|
+
): () => void;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Add a callback to `app.ticker` now and remove it on `destroy`. The
|
|
159
|
+
* ticker is resolved when this is called, not at construction.
|
|
160
|
+
*
|
|
161
|
+
* @returns A removal function, for the rarer case where the callback
|
|
162
|
+
* must come off before the plugin dies. Safe to call more than once.
|
|
163
|
+
*/
|
|
164
|
+
addTickerCallback(fn: (ticker: Ticker) => void, context?: unknown, priority?: number): () => void;
|
|
165
|
+
|
|
94
166
|
/** Register methods this plugin exposes via the core function registry. */
|
|
95
167
|
registerCoreFunctions(): void;
|
|
96
168
|
|
|
@@ -117,6 +189,8 @@ export class Plugin<O = any> implements IPlugin<O> {
|
|
|
117
189
|
private static readonly __caper_method_binding_root = true;
|
|
118
190
|
// A collection of signal connections.
|
|
119
191
|
protected _signalConnections: SignalConnections = new SignalConnections();
|
|
192
|
+
// Cleanup callbacks registered via addDisposer / listen / addTickerCallback.
|
|
193
|
+
protected _disposers: Array<() => void> = [];
|
|
120
194
|
|
|
121
195
|
protected _options: O;
|
|
122
196
|
|
|
@@ -132,7 +206,25 @@ export class Plugin<O = any> implements IPlugin<O> {
|
|
|
132
206
|
return Application.getInstance();
|
|
133
207
|
}
|
|
134
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Tear down the plugin. Runs every registered disposer (last-in-first-out,
|
|
211
|
+
* each isolated so one failure can't skip the others), then disconnects
|
|
212
|
+
* every tracked signal connection. Idempotent.
|
|
213
|
+
*
|
|
214
|
+
* Subclasses may override this, but must call `super.destroy()`.
|
|
215
|
+
*/
|
|
135
216
|
public destroy(): void {
|
|
217
|
+
// swap the list out first, so a disposer that (indirectly) re-enters
|
|
218
|
+
// destroy can't run anything twice
|
|
219
|
+
const disposers = this._disposers;
|
|
220
|
+
this._disposers = [];
|
|
221
|
+
for (let i = disposers.length - 1; i >= 0; i--) {
|
|
222
|
+
try {
|
|
223
|
+
disposers[i]();
|
|
224
|
+
} catch (e) {
|
|
225
|
+
Logger.error(`Plugin "${this.id}" threw while running a disposer:`, e);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
136
228
|
this._signalConnections.disconnectAll();
|
|
137
229
|
}
|
|
138
230
|
|
|
@@ -164,6 +256,70 @@ export class Plugin<O = any> implements IPlugin<O> {
|
|
|
164
256
|
this._signalConnections.disconnectAll();
|
|
165
257
|
}
|
|
166
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Register cleanup callbacks to run on `destroy`, last-in-first-out.
|
|
261
|
+
* Use for resources the other primitives don't cover — DOM nodes,
|
|
262
|
+
* timers, third-party handles.
|
|
263
|
+
* @param fns - The cleanup callbacks to register.
|
|
264
|
+
*/
|
|
265
|
+
public addDisposer(...fns: Array<() => void>): void {
|
|
266
|
+
for (const fn of fns) {
|
|
267
|
+
this._disposers.push(fn);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Add a DOM event listener now and remove it on `destroy`, with the same
|
|
273
|
+
* capture/options semantics it was added with.
|
|
274
|
+
* @param target - The event target to listen on.
|
|
275
|
+
* @param type - The event type.
|
|
276
|
+
* @param handler - The listener.
|
|
277
|
+
* @param options - Passed through to both add and remove, so capture-phase
|
|
278
|
+
* listeners detach correctly.
|
|
279
|
+
* @returns A removal function for early detachment. Safe to call more than once.
|
|
280
|
+
*/
|
|
281
|
+
public listen(
|
|
282
|
+
target: EventTarget,
|
|
283
|
+
type: string,
|
|
284
|
+
handler: EventListenerOrEventListenerObject,
|
|
285
|
+
options?: AddEventListenerOptions | boolean,
|
|
286
|
+
): () => void {
|
|
287
|
+
target.addEventListener(type, handler, options);
|
|
288
|
+
let removed = false;
|
|
289
|
+
const remove = () => {
|
|
290
|
+
if (removed) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
removed = true;
|
|
294
|
+
target.removeEventListener(type, handler, options);
|
|
295
|
+
};
|
|
296
|
+
this.addDisposer(remove);
|
|
297
|
+
return remove;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Add a callback to `app.ticker` now and remove it on `destroy`. The ticker
|
|
302
|
+
* is resolved when this is called, not at construction.
|
|
303
|
+
* @param fn - The ticker callback.
|
|
304
|
+
* @param context - The context to invoke the callback with.
|
|
305
|
+
* @param priority - Pixi `UPDATE_PRIORITY` for the callback.
|
|
306
|
+
* @returns A removal function for early detachment. Safe to call more than once.
|
|
307
|
+
*/
|
|
308
|
+
public addTickerCallback(fn: (ticker: Ticker) => void, context?: unknown, priority?: number): () => void {
|
|
309
|
+
const ticker = this.app.ticker;
|
|
310
|
+
ticker.add(fn, context, priority);
|
|
311
|
+
let removed = false;
|
|
312
|
+
const remove = () => {
|
|
313
|
+
if (removed) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
removed = true;
|
|
317
|
+
ticker.remove(fn, context);
|
|
318
|
+
};
|
|
319
|
+
this.addDisposer(remove);
|
|
320
|
+
return remove;
|
|
321
|
+
}
|
|
322
|
+
|
|
167
323
|
/**
|
|
168
324
|
* @override
|
|
169
325
|
* @protected
|
|
@@ -139,12 +139,6 @@ export class SceneManagerPlugin extends Plugin implements ISceneManagerPlugin {
|
|
|
139
139
|
this._defaultLoadMethod = method;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
public destroy(): void {
|
|
143
|
-
window.removeEventListener('hashchange', this._onHashChange);
|
|
144
|
-
this._debugMenu?.parentElement?.removeChild(this._debugMenu);
|
|
145
|
-
super.destroy();
|
|
146
|
-
}
|
|
147
|
-
|
|
148
142
|
public async initialize(_options: any, app: IApplication): Promise<void> {
|
|
149
143
|
this._debugVisible =
|
|
150
144
|
this.app.config?.showSceneDebugMenu === true || (isDev && this.app.config?.showSceneDebugMenu !== false);
|
|
@@ -374,7 +368,7 @@ export class SceneManagerPlugin extends Plugin implements ISceneManagerPlugin {
|
|
|
374
368
|
}
|
|
375
369
|
|
|
376
370
|
private _listenForHashChange() {
|
|
377
|
-
|
|
371
|
+
this.listen(window, 'hashchange', this._onHashChange);
|
|
378
372
|
}
|
|
379
373
|
|
|
380
374
|
private _onHashChange() {
|
|
@@ -577,6 +571,7 @@ export class SceneManagerPlugin extends Plugin implements ISceneManagerPlugin {
|
|
|
577
571
|
this._debugMenu.appendChild(icon);
|
|
578
572
|
|
|
579
573
|
(Application.containerElement || document.body).appendChild(this._debugMenu);
|
|
574
|
+
this.addDisposer(() => this._debugMenu?.parentElement?.removeChild(this._debugMenu));
|
|
580
575
|
|
|
581
576
|
this._sceneSelect = document.createElement('select');
|
|
582
577
|
this._sceneSelect.style.cssText =
|
|
@@ -651,7 +646,7 @@ export class SceneManagerPlugin extends Plugin implements ISceneManagerPlugin {
|
|
|
651
646
|
|
|
652
647
|
this._debugMenu.appendChild(this._sceneSelect);
|
|
653
648
|
|
|
654
|
-
this._debugMenu
|
|
649
|
+
this.listen(this._debugMenu, 'change', (e: Event) => {
|
|
655
650
|
if (this._queue) {
|
|
656
651
|
e.preventDefault();
|
|
657
652
|
return;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { UPDATE_PRIORITY } from 'pixi.js';
|
|
2
2
|
import Stats from 'stats.js';
|
|
3
3
|
import { Application } from '../core/Application';
|
|
4
4
|
import { Plugin } from './Plugin';
|
|
@@ -12,6 +12,7 @@ export class StatsPlugin extends Plugin {
|
|
|
12
12
|
this.stats.dom.id = 'stats';
|
|
13
13
|
|
|
14
14
|
Application.containerElement?.appendChild(this.stats.dom);
|
|
15
|
+
this.addDisposer(() => this.stats.dom.remove());
|
|
15
16
|
|
|
16
17
|
this.stats.dom.style.position = 'absolute';
|
|
17
18
|
this.stats.dom.style.top = 'auto';
|
|
@@ -19,14 +20,6 @@ export class StatsPlugin extends Plugin {
|
|
|
19
20
|
this.stats.dom.style.right = '0';
|
|
20
21
|
this.stats.dom.style.left = 'auto';
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
public destroy(): void {
|
|
26
|
-
if (this.stats) {
|
|
27
|
-
Ticker.shared.remove(this.stats.update, this.stats);
|
|
28
|
-
this.stats.dom.remove();
|
|
29
|
-
}
|
|
30
|
-
super.destroy();
|
|
23
|
+
this.addTickerCallback(this.stats.update, this.stats, UPDATE_PRIORITY.UTILITY);
|
|
31
24
|
}
|
|
32
25
|
}
|
|
@@ -676,7 +676,7 @@ export class TimerPlugin extends Plugin implements ITimerPlugin {
|
|
|
676
676
|
|
|
677
677
|
public postInitialize() {
|
|
678
678
|
// Set up update loop for main thread timers only
|
|
679
|
-
this.
|
|
679
|
+
this.addTickerCallback(this.update);
|
|
680
680
|
|
|
681
681
|
// Initialize worker if needed
|
|
682
682
|
this.initWorker();
|
|
@@ -763,7 +763,6 @@ export class TimerPlugin extends Plugin implements ITimerPlugin {
|
|
|
763
763
|
}
|
|
764
764
|
|
|
765
765
|
public destroy(): void {
|
|
766
|
-
this.app.ticker.remove(this.update);
|
|
767
766
|
this.worker?.terminate();
|
|
768
767
|
this.worker = null;
|
|
769
768
|
this.timerCallbacks.clear();
|
|
@@ -44,26 +44,17 @@ export class WebEventsPlugin extends Plugin implements IWebEventsPlugin {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
public initialize(): void {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
this.listen(document, 'visibilitychange', this._onVisibilityChanged, false);
|
|
48
|
+
this.listen(window, 'pagehide', this._onPageHide, false);
|
|
49
|
+
this.listen(window, 'pageshow', this._onPageShow, false);
|
|
50
|
+
this.listen(window, 'resize', this._onResize);
|
|
51
|
+
this.listen(document, 'fullscreenchange', this._onResize);
|
|
52
|
+
this.listen(window, 'orientationchange', this._onOrientationChanged);
|
|
53
53
|
// installed PWAs (standalone/fullscreen display modes) can miss or mistime window.resize
|
|
54
54
|
// during launch transitions; visualViewport reports the settled size
|
|
55
|
-
window.visualViewport
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
public destroy() {
|
|
59
|
-
document.removeEventListener('visibilitychange', this._onVisibilityChanged, false);
|
|
60
|
-
window.removeEventListener('resize', this._onResize);
|
|
61
|
-
document.removeEventListener('fullscreenchange', this._onResize);
|
|
62
|
-
window.removeEventListener('pagehide', this._onPageHide, false);
|
|
63
|
-
window.removeEventListener('pageshow', this._onPageShow, false);
|
|
64
|
-
window.removeEventListener('orientationchange', this._onOrientationChanged);
|
|
65
|
-
window.visualViewport?.removeEventListener('resize', this._onVisualViewportResize);
|
|
66
|
-
super.destroy();
|
|
55
|
+
if (window.visualViewport) {
|
|
56
|
+
this.listen(window.visualViewport, 'resize', this._onVisualViewportResize);
|
|
57
|
+
}
|
|
67
58
|
}
|
|
68
59
|
|
|
69
60
|
protected getCoreSignals(): string[] {
|
|
@@ -150,7 +150,6 @@ export class BreakpointPlugin extends Plugin<BreakpointPluginOptions> implements
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
public destroy(): void {
|
|
153
|
-
this._pointerQuery?.removeEventListener('change', this._onPointerChange);
|
|
154
153
|
this._pointerQuery = null;
|
|
155
154
|
for (const signal of this._enter.values()) signal.disconnectAll();
|
|
156
155
|
for (const signal of this._leave.values()) signal.disconnectAll();
|
|
@@ -259,7 +258,7 @@ export class BreakpointPlugin extends Plugin<BreakpointPluginOptions> implements
|
|
|
259
258
|
}
|
|
260
259
|
this._pointerQuery = window.matchMedia('(pointer: coarse)');
|
|
261
260
|
this._pointer = this._pointerQuery.matches ? 'coarse' : 'fine';
|
|
262
|
-
this._pointerQuery
|
|
261
|
+
this.listen(this._pointerQuery, 'change', this._onPointerChange as EventListener);
|
|
263
262
|
}
|
|
264
263
|
|
|
265
264
|
private _signal(store: Map<string, Signal<() => void>>, name: string): Signal<() => void> {
|
|
@@ -298,7 +298,7 @@ export class CaptionsPlugin extends Plugin<CaptionsOptions> implements ICaptions
|
|
|
298
298
|
const RendererClass: CaptionRendererConstructor = this.options.renderer;
|
|
299
299
|
this.renderer = this.view.addChild(new RendererClass(this)) as CaptionsRenderer;
|
|
300
300
|
|
|
301
|
-
this.
|
|
301
|
+
this.addTickerCallback(this.update);
|
|
302
302
|
this.app.scenes.onSceneChangeStart.connect(this.stopAllCaptions);
|
|
303
303
|
}
|
|
304
304
|
|
|
@@ -74,23 +74,19 @@ export class GesturePlugin extends Plugin<GesturePluginOptions> implements IGest
|
|
|
74
74
|
canvas.style.touchAction = 'none';
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
this.listen(canvas, 'pointerdown', this._onPointerDown as EventListener);
|
|
78
|
+
this.listen(window, 'pointermove', this._onPointerMove as EventListener);
|
|
79
|
+
this.listen(window, 'pointerup', this._onPointerUp as EventListener);
|
|
80
|
+
this.listen(window, 'pointercancel', this._onPointerCancel as EventListener);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
public destroy(): void {
|
|
84
84
|
if (this._canvas) {
|
|
85
|
-
this._canvas.removeEventListener('pointerdown', this._onPointerDown);
|
|
86
85
|
if (this._options.preventDefault) {
|
|
87
86
|
this._canvas.style.touchAction = this._previousTouchAction;
|
|
88
87
|
}
|
|
89
88
|
this._canvas = null;
|
|
90
89
|
}
|
|
91
|
-
window.removeEventListener('pointermove', this._onPointerMove);
|
|
92
|
-
window.removeEventListener('pointerup', this._onPointerUp);
|
|
93
|
-
window.removeEventListener('pointercancel', this._onPointerCancel);
|
|
94
90
|
|
|
95
91
|
this._pointers.clear();
|
|
96
92
|
this._state = 'idle';
|