@codexo/exojs-react 0.15.2 → 0.16.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.
@@ -6,7 +6,7 @@ import type { Application } from '@codexo/exojs';
6
6
  *
7
7
  * @throws {Error} When no `<ExoCanvas>` ancestor is present.
8
8
  *
9
- * `app.frameCount` is a plain getter updated by the engine's own frame loop
9
+ * `app.frameCount` is a plain getter updated by the engine's own frame loop -
10
10
  * reading it here does not, on its own, make `HudOverlay` re-render. Pair it
11
11
  * with {@link import('./useSignal').useSignal} to subscribe to `app.onFrame`
12
12
  * and re-render on every dispatch.
@@ -20,4 +20,5 @@ import type { Application } from '@codexo/exojs';
20
20
  * }
21
21
  * ```
22
22
  */
23
- export declare function useExoApp(): Application;
23
+ export declare const useExoApp: () => Application;
24
+ //# sourceMappingURL=useExoApp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useExoApp.d.ts","sourceRoot":"","sources":["../../src/useExoApp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAIjD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,SAAS,QAAO,WAQ5B,CAAC"}
@@ -1,33 +1,33 @@
1
- import { useExoContext } from './ExoContext.js';
1
+ import { useExoContext } from "./ExoContext.js";
2
2
 
3
+ //#region src/useExoApp.ts
3
4
  /**
4
- * Returns the {@link Application} instance from the nearest {@link ExoCanvas}
5
- * ancestor. Throws an informative error when called outside of an
6
- * `<ExoCanvas>` tree.
7
- *
8
- * @throws {Error} When no `<ExoCanvas>` ancestor is present.
9
- *
10
- * `app.frameCount` is a plain getter updated by the engine's own frame loop
11
- * reading it here does not, on its own, make `HudOverlay` re-render. Pair it
12
- * with {@link import('./useSignal').useSignal} to subscribe to `app.onFrame`
13
- * and re-render on every dispatch.
14
- *
15
- * @example
16
- * ```tsx
17
- * function HudOverlay() {
18
- * const app = useExoApp();
19
- * const frameCount = useSignal(app.onFrame, () => app.frameCount);
20
- * return <span>Frame: {frameCount}</span>;
21
- * }
22
- * ```
23
- */
24
- function useExoApp() {
25
- const app = useExoContext();
26
- if (app === null) {
27
- throw new Error('useExoApp must be used inside an <ExoCanvas> component.');
28
- }
29
- return app;
30
- }
5
+ * Returns the {@link Application} instance from the nearest {@link ExoCanvas}
6
+ * ancestor. Throws an informative error when called outside of an
7
+ * `<ExoCanvas>` tree.
8
+ *
9
+ * @throws {Error} When no `<ExoCanvas>` ancestor is present.
10
+ *
11
+ * `app.frameCount` is a plain getter updated by the engine's own frame loop -
12
+ * reading it here does not, on its own, make `HudOverlay` re-render. Pair it
13
+ * with {@link import('./useSignal').useSignal} to subscribe to `app.onFrame`
14
+ * and re-render on every dispatch.
15
+ *
16
+ * @example
17
+ * ```tsx
18
+ * function HudOverlay() {
19
+ * const app = useExoApp();
20
+ * const frameCount = useSignal(app.onFrame, () => app.frameCount);
21
+ * return <span>Frame: {frameCount}</span>;
22
+ * }
23
+ * ```
24
+ */
25
+ const useExoApp = () => {
26
+ const app = useExoContext();
27
+ if (app === null) throw new Error("useExoApp must be used inside an <ExoCanvas> component.");
28
+ return app;
29
+ };
31
30
 
31
+ //#endregion
32
32
  export { useExoApp };
33
- //# sourceMappingURL=useExoApp.js.map
33
+ //# sourceMappingURL=useExoApp.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useExoApp.js","sources":["../../../src/useExoApp.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAIA;;;;;;;;;;;;;;;;;;;;AAoBG;SACa,SAAS,GAAA;AACvB,IAAA,MAAM,GAAG,GAAG,aAAa,EAAE;AAE3B,IAAA,IAAI,GAAG,KAAK,IAAI,EAAE;AAChB,QAAA,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC;IAC5E;AAEA,IAAA,OAAO,GAAG;AACZ;;;;"}
1
+ {"version":3,"file":"useExoApp.js","names":[],"sources":["../../src/useExoApp.ts"],"sourcesContent":["import type { Application } from '@codexo/exojs';\n\nimport { useExoContext } from './ExoContext';\n\n/**\n * Returns the {@link Application} instance from the nearest {@link ExoCanvas}\n * ancestor. Throws an informative error when called outside of an\n * `<ExoCanvas>` tree.\n *\n * @throws {Error} When no `<ExoCanvas>` ancestor is present.\n *\n * `app.frameCount` is a plain getter updated by the engine's own frame loop -\n * reading it here does not, on its own, make `HudOverlay` re-render. Pair it\n * with {@link import('./useSignal').useSignal} to subscribe to `app.onFrame`\n * and re-render on every dispatch.\n *\n * @example\n * ```tsx\n * function HudOverlay() {\n * const app = useExoApp();\n * const frameCount = useSignal(app.onFrame, () => app.frameCount);\n * return <span>Frame: {frameCount}</span>;\n * }\n * ```\n */\nexport const useExoApp = (): Application => {\n const app = useExoContext();\n\n if (app === null) {\n throw new Error('useExoApp must be used inside an <ExoCanvas> component.');\n }\n\n return app;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAyBA,MAAa,kBAA+B;CAC1C,MAAM,MAAM,cAAc;CAE1B,IAAI,QAAQ,MACV,MAAM,IAAI,MAAM,yDAAyD;CAG3E,OAAO;AACT"}
@@ -6,7 +6,7 @@ import { type Ref } from 'react';
6
6
  * Same as {@link ApplicationOptions} but the `canvas.element` and `canvas.mount`
7
7
  * fields are managed for you (the Application binds to the canvas the hook
8
8
  * references), so they are omitted. You may still pass
9
- * `canvas.width`/`height`/`sizingMode`/etc.
9
+ * `canvas.width`/`height`/`sizing`/etc.
10
10
  */
11
11
  export type ExoApplicationOptions = Omit<ApplicationOptions, 'canvas'> & {
12
12
  readonly canvas?: Omit<CanvasApplicationOptions, 'element' | 'mount'>;
@@ -25,7 +25,7 @@ export interface UseExoApplicationResult {
25
25
  /**
26
26
  * Creates and owns an ExoJS {@link Application}, binding it to a `<canvas>` you
27
27
  * render yourself and attach the returned `canvasRef` to. The hook renders no
28
- * DOM of its own you keep full control over the canvas element, its container,
28
+ * DOM of its own - you keep full control over the canvas element, its container,
29
29
  * and its styling.
30
30
  *
31
31
  * ```tsx
@@ -36,20 +36,24 @@ export interface UseExoApplicationResult {
36
36
  * ```
37
37
  *
38
38
  * **Reactivity model.** The Application is recreated only when an *identity*
39
- * option changes currently the render `backend` (you cannot hot-swap WebGL2 ↔
39
+ * option changes - currently the render `backend` (you cannot hot-swap WebGL2 ↔
40
40
  * WebGPU). All other supported options are applied *live* without tearing the
41
41
  * app down:
42
42
  *
43
43
  * - `canvas.width` / `canvas.height` → `app.resize(...)`
44
- * - `canvas.sizingMode` → `app.sizingMode`
45
44
  * - `clearColor` → `app.clearColor`
46
45
  *
47
46
  * Options without a live setter (e.g. `canvas.pixelRatio`, `seed`, `extensions`)
48
47
  * are captured at creation; change the `backend` or remount to apply them.
49
48
  *
50
- * Styling note: with the default `'fixed'` sizing mode the engine never touches
51
- * the canvas CSS, so you may style it freely. The `'fit'`/`'shrink'`/`'letterbox'`
52
- * modes manage `canvas.style` themselves don't fight them with a `style` prop.
49
+ * `canvas.sizing` is captured at creation too, deliberately: a policy is an
50
+ * object, so a new instance on every render would detach and re-attach the
51
+ * previous one each time. Assign `app.sizing` yourself to switch strategies.
52
+ *
53
+ * Styling note: with no `canvas.sizing` the engine keeps the canvas at the base
54
+ * resolution and writes only that size, so you may style it freely. A sizing
55
+ * policy owns the canvas's `width`/`height` styles - don't fight it with a
56
+ * `style` prop.
53
57
  *
54
58
  * @param options - Application options (the canvas element is the one you render).
55
59
  * @param onReady - Called once each time an Application is created.
@@ -57,4 +61,5 @@ export interface UseExoApplicationResult {
57
61
  * init/scene-load failures) while an Application exists. Re-subscribed
58
62
  * automatically whenever the Application is (re)created.
59
63
  */
60
- export declare function useExoApplication(options?: ExoApplicationOptions, onReady?: (app: Application) => void, onError?: (error: unknown) => void): UseExoApplicationResult;
64
+ export declare const useExoApplication: (options?: ExoApplicationOptions, onReady?: (app: Application) => void, onError?: (error: unknown) => void) => UseExoApplicationResult;
65
+ //# sourceMappingURL=useExoApplication.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useExoApplication.d.ts","sourceRoot":"","sources":["../../src/useExoApplication.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,KAAK,wBAAwB,EAAc,MAAM,eAAe,CAAC;AAChH,OAAO,EAAE,KAAK,GAAG,EAA+B,MAAM,OAAO,CAAC;AAE9D;;;;;;;GAOG;AACH,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,kBAAkB,EAAE,QAAQ,CAAC,GAAG;IACvE,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,wBAAwB,EAAE,SAAS,GAAG,OAAO,CAAC,CAAC;CACvE,CAAC;AAEF,iDAAiD;AACjD,MAAM,WAAW,uBAAuB;IACtC,qEAAqE;IACrE,QAAQ,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,iBAAiB,CAAC,CAAC;CAC5C;AAKD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,eAAO,MAAM,iBAAiB,GAC5B,UAAU,qBAAqB,EAC/B,UAAU,CAAC,GAAG,EAAE,WAAW,KAAK,IAAI,EACpC,UAAU,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,KACjC,uBAwFF,CAAC"}
@@ -1,124 +1,107 @@
1
- import { Application } from '@codexo/exojs';
2
- import { useRef, useState, useEffect } from 'react';
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { Application } from "@codexo/exojs";
3
3
 
4
+ //#region src/useExoApplication.ts
4
5
  /** Stable string key for the colour so the sync effect can depend on its value. */
5
- function colorKey(color) {
6
- return color === undefined ? undefined : `${color.r},${color.g},${color.b},${color.a}`;
7
- }
6
+ const colorKey = (color) => color === void 0 ? void 0 : `${color.r},${color.g},${color.b},${color.a}`;
8
7
  /**
9
- * Creates and owns an ExoJS {@link Application}, binding it to a `<canvas>` you
10
- * render yourself and attach the returned `canvasRef` to. The hook renders no
11
- * DOM of its own you keep full control over the canvas element, its container,
12
- * and its styling.
13
- *
14
- * ```tsx
15
- * function Game() {
16
- * const { app, canvasRef } = useExoApplication({ canvas: { width: 800, height: 600 } });
17
- * return <canvas ref={canvasRef} className="game" />;
18
- * }
19
- * ```
20
- *
21
- * **Reactivity model.** The Application is recreated only when an *identity*
22
- * option changes currently the render `backend` (you cannot hot-swap WebGL2 ↔
23
- * WebGPU). All other supported options are applied *live* without tearing the
24
- * app down:
25
- *
26
- * - `canvas.width` / `canvas.height` → `app.resize(...)`
27
- * - `canvas.sizingMode` → `app.sizingMode`
28
- * - `clearColor` → `app.clearColor`
29
- *
30
- * Options without a live setter (e.g. `canvas.pixelRatio`, `seed`, `extensions`)
31
- * are captured at creation; change the `backend` or remount to apply them.
32
- *
33
- * Styling note: with the default `'fixed'` sizing mode the engine never touches
34
- * the canvas CSS, so you may style it freely. The `'fit'`/`'shrink'`/`'letterbox'`
35
- * modes manage `canvas.style` themselves — don't fight them with a `style` prop.
36
- *
37
- * @param options - Application options (the canvas element is the one you render).
38
- * @param onReady - Called once each time an Application is created.
39
- * @param onError - Called for every {@link Application.onError} dispatch (async
40
- * init/scene-load failures) while an Application exists. Re-subscribed
41
- * automatically whenever the Application is (re)created.
42
- */
43
- function useExoApplication(options, onReady, onError) {
44
- const canvasRef = useRef(null);
45
- const [app, setApp] = useState(null);
46
- // Latest onReady without retriggering the lifecycle effect. Updated in an
47
- // effect (not during render) so the ref-write happens after commit.
48
- const onReadyRef = useRef(onReady);
49
- useEffect(() => {
50
- onReadyRef.current = onReady;
51
- });
52
- // Latest onError without retriggering the subscribe effect below.
53
- const onErrorRef = useRef(onError);
54
- useEffect(() => {
55
- onErrorRef.current = onError;
56
- });
57
- // Identity: only the backend type forces a full recreation.
58
- const backendKey = options?.backend?.type ?? 'auto';
59
- // ── Lifecycle: create on mount / recreate on backend change ───────────────
60
- useEffect(() => {
61
- const canvas = canvasRef.current;
62
- if (!canvas) {
63
- return;
64
- }
65
- // Bind to the React-rendered canvas. The engine never removes a canvas it
66
- // did not create (Application.destroy leaves it in the DOM), so React stays
67
- // the sole owner of the element's lifecycle.
68
- const application = new Application({
69
- ...options,
70
- canvas: { ...options?.canvas, element: canvas },
71
- });
72
- setApp(application);
73
- onReadyRef.current?.(application);
74
- return () => {
75
- application.destroy();
76
- setApp(null);
77
- };
78
- // Recreate only when the backend identity changes; live options are synced
79
- // by the effects below. `options` is intentionally read at (re)create time.
80
- // eslint-disable-next-line react-hooks/exhaustive-deps
81
- }, [backendKey]);
82
- // ── Live sync: error reporting ─────────────────────────────────────────────
83
- useEffect(() => {
84
- if (app === null) {
85
- return;
86
- }
87
- const handleError = (error) => {
88
- onErrorRef.current?.(error);
89
- };
90
- app.onError.add(handleError);
91
- return () => {
92
- app.onError.remove(handleError);
93
- };
94
- }, [app]);
95
- // ── Live sync: size ───────────────────────────────────────────────────────
96
- const width = options?.canvas?.width;
97
- const height = options?.canvas?.height;
98
- useEffect(() => {
99
- if (app !== null && width !== undefined && height !== undefined) {
100
- app.resize(width, height);
101
- }
102
- }, [app, width, height]);
103
- // ── Live sync: sizing mode ────────────────────────────────────────────────
104
- const sizingMode = options?.canvas?.sizingMode;
105
- useEffect(() => {
106
- if (app !== null && sizingMode !== undefined) {
107
- app.sizingMode = sizingMode;
108
- }
109
- }, [app, sizingMode]);
110
- // ── Live sync: clear colour ───────────────────────────────────────────────
111
- const clearColor = options?.clearColor;
112
- const clearKey = colorKey(clearColor);
113
- useEffect(() => {
114
- if (app !== null && clearColor !== undefined) {
115
- app.clearColor = clearColor;
116
- }
117
- // clearColor identity is unstable; depend on its value key instead.
118
- // eslint-disable-next-line react-hooks/exhaustive-deps
119
- }, [app, clearKey]);
120
- return { app, canvasRef };
121
- }
8
+ * Creates and owns an ExoJS {@link Application}, binding it to a `<canvas>` you
9
+ * render yourself and attach the returned `canvasRef` to. The hook renders no
10
+ * DOM of its own - you keep full control over the canvas element, its container,
11
+ * and its styling.
12
+ *
13
+ * ```tsx
14
+ * function Game() {
15
+ * const { app, canvasRef } = useExoApplication({ canvas: { width: 800, height: 600 } });
16
+ * return <canvas ref={canvasRef} className="game" />;
17
+ * }
18
+ * ```
19
+ *
20
+ * **Reactivity model.** The Application is recreated only when an *identity*
21
+ * option changes - currently the render `backend` (you cannot hot-swap WebGL2 ↔
22
+ * WebGPU). All other supported options are applied *live* without tearing the
23
+ * app down:
24
+ *
25
+ * - `canvas.width` / `canvas.height` → `app.resize(...)`
26
+ * - `clearColor` → `app.clearColor`
27
+ *
28
+ * Options without a live setter (e.g. `canvas.pixelRatio`, `seed`, `extensions`)
29
+ * are captured at creation; change the `backend` or remount to apply them.
30
+ *
31
+ * `canvas.sizing` is captured at creation too, deliberately: a policy is an
32
+ * object, so a new instance on every render would detach and re-attach the
33
+ * previous one each time. Assign `app.sizing` yourself to switch strategies.
34
+ *
35
+ * Styling note: with no `canvas.sizing` the engine keeps the canvas at the base
36
+ * resolution and writes only that size, so you may style it freely. A sizing
37
+ * policy owns the canvas's `width`/`height` styles - don't fight it with a
38
+ * `style` prop.
39
+ *
40
+ * @param options - Application options (the canvas element is the one you render).
41
+ * @param onReady - Called once each time an Application is created.
42
+ * @param onError - Called for every {@link Application.onError} dispatch (async
43
+ * init/scene-load failures) while an Application exists. Re-subscribed
44
+ * automatically whenever the Application is (re)created.
45
+ */
46
+ const useExoApplication = (options, onReady, onError) => {
47
+ const canvasRef = useRef(null);
48
+ const [app, setApp] = useState(null);
49
+ const onReadyRef = useRef(onReady);
50
+ useEffect(() => {
51
+ onReadyRef.current = onReady;
52
+ });
53
+ const onErrorRef = useRef(onError);
54
+ useEffect(() => {
55
+ onErrorRef.current = onError;
56
+ });
57
+ const backendKey = options?.backend?.type ?? "auto";
58
+ useEffect(() => {
59
+ const canvas = canvasRef.current;
60
+ if (!canvas) return;
61
+ const application = new Application({
62
+ ...options,
63
+ canvas: {
64
+ ...options?.canvas,
65
+ element: canvas
66
+ }
67
+ });
68
+ setApp(application);
69
+ onReadyRef.current?.(application);
70
+ return () => {
71
+ application.destroy();
72
+ setApp(null);
73
+ };
74
+ }, [backendKey]);
75
+ useEffect(() => {
76
+ if (app === null) return;
77
+ const handleError = (error) => {
78
+ onErrorRef.current?.(error);
79
+ };
80
+ app.onError.add(handleError);
81
+ return () => {
82
+ app.onError.remove(handleError);
83
+ };
84
+ }, [app]);
85
+ const width = options?.canvas?.width;
86
+ const height = options?.canvas?.height;
87
+ useEffect(() => {
88
+ if (app !== null && width !== void 0 && height !== void 0) app.resize(width, height);
89
+ }, [
90
+ app,
91
+ width,
92
+ height
93
+ ]);
94
+ const clearColor = options?.clearColor;
95
+ const clearKey = colorKey(clearColor);
96
+ useEffect(() => {
97
+ if (app !== null && clearColor !== void 0) app.clearColor = clearColor;
98
+ }, [app, clearKey]);
99
+ return {
100
+ app,
101
+ canvasRef
102
+ };
103
+ };
122
104
 
105
+ //#endregion
123
106
  export { useExoApplication };
124
- //# sourceMappingURL=useExoApplication.js.map
107
+ //# sourceMappingURL=useExoApplication.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useExoApplication.js","sources":["../../../src/useExoApplication.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useExoApplication.js","names":[],"sources":["../../src/useExoApplication.ts"],"sourcesContent":["import { Application, type ApplicationOptions, type CanvasApplicationOptions, type Color } from '@codexo/exojs';\nimport { type Ref, useEffect, useRef, useState } from 'react';\n\n/**\n * Options for {@link useExoApplication} / {@link import('./ExoCanvas').ExoCanvas}.\n *\n * Same as {@link ApplicationOptions} but the `canvas.element` and `canvas.mount`\n * fields are managed for you (the Application binds to the canvas the hook\n * references), so they are omitted. You may still pass\n * `canvas.width`/`height`/`sizing`/etc.\n */\nexport type ExoApplicationOptions = Omit<ApplicationOptions, 'canvas'> & {\n readonly canvas?: Omit<CanvasApplicationOptions, 'element' | 'mount'>;\n};\n\n/** Return value of {@link useExoApplication}. */\nexport interface UseExoApplicationResult {\n /** The Application instance, or `null` until it has been created. */\n readonly app: Application | null;\n /**\n * Attach this to the `<canvas>` element the Application should bind to. Typed\n * as `Ref` (not `RefObject`) so the same code type-checks against both\n * `@types/react` 18 and 19, whose `useRef`/`RefObject` nullability differ.\n */\n readonly canvasRef: Ref<HTMLCanvasElement>;\n}\n\n/** Stable string key for the colour so the sync effect can depend on its value. */\nconst colorKey = (color: Color | undefined): string | undefined => (color === undefined ? undefined : `${color.r},${color.g},${color.b},${color.a}`);\n\n/**\n * Creates and owns an ExoJS {@link Application}, binding it to a `<canvas>` you\n * render yourself and attach the returned `canvasRef` to. The hook renders no\n * DOM of its own - you keep full control over the canvas element, its container,\n * and its styling.\n *\n * ```tsx\n * function Game() {\n * const { app, canvasRef } = useExoApplication({ canvas: { width: 800, height: 600 } });\n * return <canvas ref={canvasRef} className=\"game\" />;\n * }\n * ```\n *\n * **Reactivity model.** The Application is recreated only when an *identity*\n * option changes - currently the render `backend` (you cannot hot-swap WebGL2 ↔\n * WebGPU). All other supported options are applied *live* without tearing the\n * app down:\n *\n * - `canvas.width` / `canvas.height` → `app.resize(...)`\n * - `clearColor` → `app.clearColor`\n *\n * Options without a live setter (e.g. `canvas.pixelRatio`, `seed`, `extensions`)\n * are captured at creation; change the `backend` or remount to apply them.\n *\n * `canvas.sizing` is captured at creation too, deliberately: a policy is an\n * object, so a new instance on every render would detach and re-attach the\n * previous one each time. Assign `app.sizing` yourself to switch strategies.\n *\n * Styling note: with no `canvas.sizing` the engine keeps the canvas at the base\n * resolution and writes only that size, so you may style it freely. A sizing\n * policy owns the canvas's `width`/`height` styles - don't fight it with a\n * `style` prop.\n *\n * @param options - Application options (the canvas element is the one you render).\n * @param onReady - Called once each time an Application is created.\n * @param onError - Called for every {@link Application.onError} dispatch (async\n * init/scene-load failures) while an Application exists. Re-subscribed\n * automatically whenever the Application is (re)created.\n */\nexport const useExoApplication = (\n options?: ExoApplicationOptions,\n onReady?: (app: Application) => void,\n onError?: (error: unknown) => void,\n): UseExoApplicationResult => {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const [app, setApp] = useState<Application | null>(null);\n\n // Latest onReady without retriggering the lifecycle effect. Updated in an\n // effect (not during render) so the ref-write happens after commit.\n const onReadyRef = useRef(onReady);\n useEffect(() => {\n onReadyRef.current = onReady;\n });\n\n // Latest onError without retriggering the subscribe effect below.\n const onErrorRef = useRef(onError);\n useEffect(() => {\n onErrorRef.current = onError;\n });\n\n // Identity: only the backend type forces a full recreation.\n const backendKey = options?.backend?.type ?? 'auto';\n\n // ── Lifecycle: create on mount / recreate on backend change ───────────────\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) {\n return;\n }\n\n // Bind to the React-rendered canvas. The engine never removes a canvas it\n // did not create (Application.destroy leaves it in the DOM), so React stays\n // the sole owner of the element's lifecycle.\n const application = new Application({\n ...options,\n canvas: { ...options?.canvas, element: canvas },\n });\n\n setApp(application);\n onReadyRef.current?.(application);\n\n return () => {\n // A React cleanup function cannot await, and teardown failures already\n // travel through the application's own error pipeline - so the Promise\n // is deliberately dropped rather than reported a second time here.\n void application.destroy();\n setApp(null);\n };\n // Recreate only when the backend identity changes; live options are synced\n // by the effects below. `options` is intentionally read at (re)create time.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [backendKey]);\n\n // ── Live sync: error reporting ─────────────────────────────────────────────\n useEffect(() => {\n if (app === null) {\n return;\n }\n\n const handleError = (error: Error): void => {\n onErrorRef.current?.(error);\n };\n\n app.onError.add(handleError);\n\n return () => {\n app.onError.remove(handleError);\n };\n }, [app]);\n\n // ── Live sync: size ───────────────────────────────────────────────────────\n const width = options?.canvas?.width;\n const height = options?.canvas?.height;\n useEffect(() => {\n if (app !== null && width !== undefined && height !== undefined) {\n app.resize(width, height);\n }\n }, [app, width, height]);\n\n // ── Live sync: clear colour ───────────────────────────────────────────────\n const clearColor = options?.clearColor;\n const clearKey = colorKey(clearColor);\n useEffect(() => {\n if (app !== null && clearColor !== undefined) {\n app.clearColor = clearColor;\n }\n // clearColor identity is unstable; depend on its value key instead.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [app, clearKey]);\n\n return { app, canvasRef };\n};\n"],"mappings":";;;;;AA4BA,MAAM,YAAY,UAAkD,UAAU,SAAY,SAAY,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,GAAG,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyChJ,MAAa,qBACX,SACA,SACA,YAC4B;CAC5B,MAAM,YAAY,OAA0B,IAAI;CAChD,MAAM,CAAC,KAAK,UAAU,SAA6B,IAAI;CAIvD,MAAM,aAAa,OAAO,OAAO;CACjC,gBAAgB;EACd,WAAW,UAAU;CACvB,CAAC;CAGD,MAAM,aAAa,OAAO,OAAO;CACjC,gBAAgB;EACd,WAAW,UAAU;CACvB,CAAC;CAGD,MAAM,aAAa,SAAS,SAAS,QAAQ;CAG7C,gBAAgB;EACd,MAAM,SAAS,UAAU;EACzB,IAAI,CAAC,QACH;EAMF,MAAM,cAAc,IAAI,YAAY;GAClC,GAAG;GACH,QAAQ;IAAE,GAAG,SAAS;IAAQ,SAAS;GAAO;EAChD,CAAC;EAED,OAAO,WAAW;EAClB,WAAW,UAAU,WAAW;EAEhC,aAAa;GAIX,AAAK,YAAY,QAAQ;GACzB,OAAO,IAAI;EACb;CAIF,GAAG,CAAC,UAAU,CAAC;CAGf,gBAAgB;EACd,IAAI,QAAQ,MACV;EAGF,MAAM,eAAe,UAAuB;GAC1C,WAAW,UAAU,KAAK;EAC5B;EAEA,IAAI,QAAQ,IAAI,WAAW;EAE3B,aAAa;GACX,IAAI,QAAQ,OAAO,WAAW;EAChC;CACF,GAAG,CAAC,GAAG,CAAC;CAGR,MAAM,QAAQ,SAAS,QAAQ;CAC/B,MAAM,SAAS,SAAS,QAAQ;CAChC,gBAAgB;EACd,IAAI,QAAQ,QAAQ,UAAU,UAAa,WAAW,QACpD,IAAI,OAAO,OAAO,MAAM;CAE5B,GAAG;EAAC;EAAK;EAAO;CAAM,CAAC;CAGvB,MAAM,aAAa,SAAS;CAC5B,MAAM,WAAW,SAAS,UAAU;CACpC,gBAAgB;EACd,IAAI,QAAQ,QAAQ,eAAe,QACjC,IAAI,aAAa;CAIrB,GAAG,CAAC,KAAK,QAAQ,CAAC;CAElB,OAAO;EAAE;EAAK;CAAU;AAC1B"}
@@ -1,24 +1,30 @@
1
1
  import { type Scene } from '@codexo/exojs';
2
2
  import { type DependencyList } from 'react';
3
3
  /**
4
- * Creates an instance of `SceneClass`, activates it on the ExoJS
5
- * {@link Application}, and returns it once the scene is live.
4
+ * Activates `SceneClass` on the ExoJS {@link Application} and returns the
5
+ * resulting instance once it is live. `SceneClass` must be registered in
6
+ * `ApplicationOptions.scenes` (passed to {@link import('./useExoApplication').useExoApplication}
7
+ * / {@link import('./ExoCanvas').ExoCanvas}) - unregistered targets reject in
8
+ * development builds.
6
9
  *
7
- * On first call (engine not yet started) this hook calls `app.start(scene)`,
10
+ * On first call (engine not yet started) this hook calls `app.start(SceneClass)`,
8
11
  * which initializes the render backend and begins the per-frame loop. On
9
- * subsequent dep-change remounts it calls `app.scene.setScene(scene)` to
10
- * switch scenes without restarting the engine.
12
+ * subsequent dep-change remounts it calls `app.scenes.change(SceneClass)` to
13
+ * switch scenes without restarting the engine, constructing a fresh instance.
11
14
  *
12
- * The scene is cleared (`setScene(null)`) when the component unmounts or
13
- * when `deps` change mirroring `useEffect` semantics.
15
+ * Effects that run while startup is still in flight - React StrictMode
16
+ * double-mounts every effect in development - join that `app.start()` call
17
+ * instead of racing a second navigation against it, and only activate
18
+ * `SceneClass` afterwards if startup did not already leave it active. A
19
+ * StrictMode double mount therefore activates the scene exactly once.
14
20
  *
15
- * A failure in `app.start()`/`app.scene.setScene()` (e.g. a scene's `onLoad`
21
+ * A failure in `app.start()`/`app.scenes.change()` (e.g. a scene's `load()`
16
22
  * rejects) is caught and routed to {@link Application.onError} rather than
17
- * left as an unhandled promise rejection subscribe via
23
+ * left as an unhandled promise rejection - subscribe via
18
24
  * `app.onError.add(...)` or the {@link import('./ExoCanvas').ExoCanvas}
19
25
  * `onError` prop to observe it.
20
26
  *
21
- * @param SceneClass - Constructor for the scene to instantiate.
27
+ * @param SceneClass - Constructor for the scene to activate.
22
28
  * @param deps - Extra deps that trigger scene replacement when changed, in
23
29
  * addition to the stable `app` reference (same semantics as `useEffect`).
24
30
  * @returns The active scene instance, or `null` while it is loading.
@@ -32,4 +38,5 @@ import { type DependencyList } from 'react';
32
38
  * }
33
39
  * ```
34
40
  */
35
- export declare function useScene<T extends Scene>(SceneClass: new () => T, deps?: DependencyList): T | null;
41
+ export declare const useScene: <T extends Scene>(SceneClass: new () => T, deps?: DependencyList) => T | null;
42
+ //# sourceMappingURL=useScene.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useScene.d.ts","sourceRoot":"","sources":["../../src/useScene.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,KAAK,EAAyB,MAAM,eAAe,CAAC;AACpF,OAAO,EAAE,KAAK,cAAc,EAA+B,MAAM,OAAO,CAAC;AAIzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,eAAO,MAAM,QAAQ,GAAI,CAAC,SAAS,KAAK,EAAE,YAAY,UAAU,CAAC,EAAE,OAAM,cAAmB,KAAG,CAAC,GAAG,IAkFlG,CAAC"}