@octanejs/three 0.1.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/src/index.ts ADDED
@@ -0,0 +1,86 @@
1
+ export {
2
+ addAfterEffect,
3
+ addEffect,
4
+ addTail,
5
+ advance,
6
+ applyProps,
7
+ buildGraph,
8
+ calculateDpr,
9
+ context,
10
+ createEvents,
11
+ createPortal,
12
+ createRoot,
13
+ dispose,
14
+ extend,
15
+ flushGlobalEffects,
16
+ getRootState,
17
+ invalidate,
18
+ unmountComponentAtNode,
19
+ useFrame,
20
+ useGraph,
21
+ useInstanceHandle,
22
+ useLoader,
23
+ useStore,
24
+ useThree,
25
+ } from './core/index.js';
26
+ export type {
27
+ Args,
28
+ Attach,
29
+ Catalogue,
30
+ ConstructorRepresentation,
31
+ EventProps,
32
+ Instance,
33
+ InstanceProps,
34
+ MathRepresentation,
35
+ MathType,
36
+ MathTypes,
37
+ PrimitiveProps,
38
+ RaycastableRepresentation,
39
+ ThreeElement,
40
+ ThreeElements,
41
+ ThreeInstanceProps,
42
+ ThreeKey,
43
+ ThreeRef,
44
+ ThreeToJSXElements,
45
+ } from './core/index.js';
46
+ export type {
47
+ Camera,
48
+ CameraProps,
49
+ ComputeFunction,
50
+ CanvasLike,
51
+ DefaultGLProps,
52
+ Dpr,
53
+ DomEvent,
54
+ EventHandlers,
55
+ EventManager,
56
+ Events,
57
+ FilterFunction,
58
+ Frameloop,
59
+ GLProps,
60
+ GlobalEffectType,
61
+ GlobalRenderCallback,
62
+ InternalState,
63
+ Intersection,
64
+ IntersectionEvent,
65
+ InjectState,
66
+ ObjectMap,
67
+ Extensions,
68
+ LoaderResult,
69
+ Performance,
70
+ RefObject,
71
+ RenderCallback,
72
+ Renderer,
73
+ RenderProps,
74
+ RootState,
75
+ RootStore,
76
+ Size,
77
+ Subscription,
78
+ PointerCaptureTarget,
79
+ ThreeEvent,
80
+ ThreeRoot,
81
+ Viewport,
82
+ XRManager,
83
+ } from './core/index.js';
84
+ export { Canvas } from './web/Canvas.tsrx';
85
+ export type { CanvasProps, CanvasRef, CanvasStyle } from './web/Canvas.tsrx';
86
+ export { createPointerEvents as events } from './web/events.js';
@@ -0,0 +1,18 @@
1
+ import type { ThreeElements as ThreeIntrinsicElements } from './core/catalogue.js';
2
+
3
+ export type {
4
+ Args,
5
+ Attach,
6
+ ConstructorRepresentation,
7
+ PrimitiveProps,
8
+ ThreeElement,
9
+ ThreeElements,
10
+ ThreeKey,
11
+ ThreeRef,
12
+ ThreeToJSXElements,
13
+ } from './core/catalogue.js';
14
+
15
+ /** TypeScript resolves this namespace through the intrinsic jsx-runtime export. */
16
+ export namespace JSX {
17
+ export interface IntrinsicElements extends ThreeIntrinsicElements {}
18
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Compiler-facing Three renderer ABI.
3
+ *
4
+ * Compiled `*.three.tsrx` modules import their host plans and hook helpers from
5
+ * this entry point. The Three driver remains package-owned while component
6
+ * execution, ownership, scheduling, refs, and effects stay in Octane.
7
+ */
8
+ export * from 'octane/universal';
9
+ export * from './core/index.js';
10
+ // Resolve the deliberate name overlap with the capability-gated universal
11
+ // primitive in favor of Three's R3F-shaped state-enclave adapter.
12
+ export { createPortal } from './core/portal.js';
package/src/testing.ts ADDED
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Deterministic, WebGL-free scene harness for `@octanejs/three` components.
3
+ *
4
+ * The harness deliberately drives the same configured root and frame loop as
5
+ * an application. It only replaces the rendering object with a small recorder,
6
+ * so host commits, hooks, global effects, and `useFrame` subscriptions retain
7
+ * their production behavior.
8
+ */
9
+ import type * as THREE from 'three';
10
+ import type { UniversalComponent } from 'octane/universal';
11
+ import {
12
+ dispatchThreeEvent,
13
+ getThreeEventListener,
14
+ getThreeEventStore,
15
+ runThreeEventScope,
16
+ } from './core/driver.js';
17
+ import { createRoot, type CanvasLike, type ThreeRoot } from './core/root.js';
18
+ import type { Renderer, RootStore } from './core/store.js';
19
+
20
+ const DEFAULT_WIDTH = 1280;
21
+ const DEFAULT_HEIGHT = 800;
22
+
23
+ export interface CreateThreeTestRendererOptions {
24
+ /** Logical canvas width. Defaults to 1280. */
25
+ readonly width?: number;
26
+ /** Logical canvas height. Defaults to 800. */
27
+ readonly height?: number;
28
+ }
29
+
30
+ /** Renderer recorder injected into a deterministic Three test root. */
31
+ export interface TestingRenderer extends Renderer {
32
+ /** Number of frames delivered through the root's real render loop. */
33
+ readonly frameCount: number;
34
+ /** Last scene submitted for rendering, or `null` before the first frame. */
35
+ readonly lastScene: THREE.Scene | null;
36
+ /** Last camera submitted for rendering, or `null` before the first frame. */
37
+ readonly lastCamera: THREE.Camera | null;
38
+ /** Whether root teardown disposed the recorder. */
39
+ readonly disposed: boolean;
40
+ }
41
+
42
+ /** Additional values copied onto a directly-fired testing event. */
43
+ export interface MockEventData {
44
+ [key: string]: unknown;
45
+ }
46
+
47
+ /** R3F test-renderer-compatible event payload produced by {@link fireEvent}. */
48
+ export interface MockSyntheticEvent extends MockEventData {
49
+ camera: THREE.Camera;
50
+ stopPropagation(): void;
51
+ target: object;
52
+ currentTarget: object;
53
+ sourceEvent: MockEventData;
54
+ }
55
+
56
+ export type FireEvent = (object: object, name: string, data?: MockEventData) => Promise<unknown>;
57
+
58
+ export interface ThreeTestRenderer {
59
+ readonly canvas: CanvasLike;
60
+ readonly scene: THREE.Scene;
61
+ readonly store: RootStore;
62
+ readonly root: ThreeRoot<CanvasLike>;
63
+ readonly renderer: TestingRenderer;
64
+ update<P>(component: UniversalComponent<P>, props: P): RootStore;
65
+ /** Directly invokes an event handler on a managed Three object. */
66
+ fireEvent: FireEvent;
67
+ /**
68
+ * Advances the configured `frameloop="never"` root by explicit deltas.
69
+ * A short delta array repeats its final value for the remaining frames.
70
+ */
71
+ advanceFrames(frames: number, delta?: number | readonly number[]): void;
72
+ unmount(): void;
73
+ }
74
+
75
+ class FrameRecorder implements TestingRenderer {
76
+ readonly domElement: CanvasLike;
77
+ readonly renderLists = { dispose() {} };
78
+ #frameCount = 0;
79
+ #lastScene: THREE.Scene | null = null;
80
+ #lastCamera: THREE.Camera | null = null;
81
+ #disposed = false;
82
+
83
+ constructor(canvas: CanvasLike) {
84
+ this.domElement = canvas;
85
+ }
86
+
87
+ get frameCount(): number {
88
+ return this.#frameCount;
89
+ }
90
+
91
+ get lastScene(): THREE.Scene | null {
92
+ return this.#lastScene;
93
+ }
94
+
95
+ get lastCamera(): THREE.Camera | null {
96
+ return this.#lastCamera;
97
+ }
98
+
99
+ get disposed(): boolean {
100
+ return this.#disposed;
101
+ }
102
+
103
+ render(scene: THREE.Scene, camera: THREE.Camera): void {
104
+ this.#frameCount++;
105
+ this.#lastScene = scene;
106
+ this.#lastCamera = camera;
107
+ }
108
+
109
+ setPixelRatio(_dpr: number): void {}
110
+
111
+ setSize(_width: number, _height: number, _updateStyle?: boolean): void {}
112
+
113
+ forceContextLoss(): void {}
114
+
115
+ dispose(): void {
116
+ this.#disposed = true;
117
+ }
118
+ }
119
+
120
+ function createCanvas(width: number, height: number): CanvasLike {
121
+ if (typeof document !== 'undefined' && typeof document.createElement === 'function') {
122
+ const canvas = document.createElement('canvas');
123
+ canvas.width = width;
124
+ canvas.height = height;
125
+ return canvas;
126
+ }
127
+ return { width, height, parentElement: null };
128
+ }
129
+
130
+ function assertFrameCount(frames: number): void {
131
+ if (!Number.isSafeInteger(frames) || frames < 0) {
132
+ throw new RangeError('@octanejs/three/testing: frames must be a non-negative integer.');
133
+ }
134
+ }
135
+
136
+ function deltaForFrame(delta: number | readonly number[], index: number): number {
137
+ const value = typeof delta === 'number' ? delta : (delta[index] ?? delta.at(-1) ?? 1);
138
+ if (!Number.isFinite(value)) {
139
+ throw new RangeError('@octanejs/three/testing: frame deltas must be finite numbers.');
140
+ }
141
+ return value;
142
+ }
143
+
144
+ function toEventHandlerName(name: string): string {
145
+ return name.startsWith('on') ? name : `on${name.charAt(0).toUpperCase()}${name.slice(1)}`;
146
+ }
147
+
148
+ function warnMissingEventHandler(name: string): void {
149
+ console.warn(
150
+ `Handler for ${name} was not found. You must pass event names in camelCase or name of the handler https://github.com/pmndrs/react-three-fiber/blob/master/packages/test-renderer/markdown/rttr.md#create-fireevent`,
151
+ );
152
+ }
153
+
154
+ async function dispatchTestingEvent(
155
+ object: object,
156
+ name: string,
157
+ data: MockEventData = {},
158
+ ): Promise<unknown> {
159
+ const handlerName = toEventHandlerName(name);
160
+ const listener = getThreeEventListener(object, handlerName);
161
+ const store = getThreeEventStore(object);
162
+ if (listener === undefined || store === undefined) {
163
+ warnMissingEventHandler(name);
164
+ return undefined;
165
+ }
166
+
167
+ const event = {
168
+ camera: store.getState().camera,
169
+ stopPropagation() {},
170
+ target: object,
171
+ currentTarget: object,
172
+ sourceEvent: data,
173
+ ...data,
174
+ } as MockSyntheticEvent;
175
+
176
+ const result = runThreeEventScope(store, listener.priority, () =>
177
+ dispatchThreeEvent(store, listener.id, event),
178
+ );
179
+ const resolved = await result;
180
+ // Continuous scopes schedule their commit in a microtask. Let that commit and
181
+ // any passive work it queues settle before the caller observes the scene.
182
+ await Promise.resolve();
183
+ return resolved;
184
+ }
185
+
186
+ /**
187
+ * Directly invoke the latest committed event listener for a managed Three object.
188
+ * Both `pointerDown` and `onPointerDown` naming forms are accepted.
189
+ */
190
+ export const fireEvent: FireEvent = dispatchTestingEvent;
191
+
192
+ /** Creates a configured, deterministic Three root without requesting WebGL. */
193
+ export async function createThreeTestRenderer<P>(
194
+ component: UniversalComponent<P>,
195
+ props: P,
196
+ options: CreateThreeTestRendererOptions = {},
197
+ ): Promise<ThreeTestRenderer> {
198
+ const width = options.width ?? DEFAULT_WIDTH;
199
+ const height = options.height ?? DEFAULT_HEIGHT;
200
+ const canvas = createCanvas(width, height);
201
+ const renderer = new FrameRecorder(canvas);
202
+ const root: ThreeRoot<CanvasLike> = createRoot(canvas);
203
+ let unmounted = false;
204
+
205
+ try {
206
+ await root.configure({
207
+ gl: renderer,
208
+ size: { width, height, top: 0, left: 0 },
209
+ dpr: 1,
210
+ frameloop: 'never',
211
+ });
212
+ root.render(component, props);
213
+ } catch (error) {
214
+ root.unmount();
215
+ throw error;
216
+ }
217
+
218
+ const store = root.store;
219
+ const scene = store.getState().scene;
220
+ const assertMounted = () => {
221
+ if (unmounted) {
222
+ throw new Error('@octanejs/three/testing: The test root has already been unmounted.');
223
+ }
224
+ };
225
+
226
+ return {
227
+ canvas,
228
+ scene,
229
+ store,
230
+ root,
231
+ renderer,
232
+ update<NextProps>(nextComponent: UniversalComponent<NextProps>, nextProps: NextProps) {
233
+ assertMounted();
234
+ return root.render(nextComponent, nextProps);
235
+ },
236
+ fireEvent(object, name, data) {
237
+ assertMounted();
238
+ return dispatchTestingEvent(object, name, data);
239
+ },
240
+ advanceFrames(frames, delta = 1) {
241
+ assertMounted();
242
+ assertFrameCount(frames);
243
+ const state = store.getState();
244
+ for (let index = 0; index < frames; index++) {
245
+ state.advance(state.clock.elapsedTime + deltaForFrame(delta, index));
246
+ }
247
+ },
248
+ unmount() {
249
+ if (unmounted) return;
250
+ unmounted = true;
251
+ root.unmount();
252
+ },
253
+ };
254
+ }
255
+
256
+ /** R3F test-renderer-compatible short name for the component-plus-props API. */
257
+ export const create = createThreeTestRenderer;
258
+
259
+ export default { create, fireEvent };
@@ -0,0 +1,295 @@
1
+ import { useInsertionEffect, useLayoutEffect, useRef, useState } from 'octane';
2
+ import { createUniversalHostBoundary } from 'octane/universal';
3
+ import type { RendererRegion } from 'octane/universal';
4
+ import type { ComputeFunction, DomEvent } from '../core/events.js';
5
+ import { createRoot, createThreeBoundaryMount } from '../core/root.js';
6
+ import type { RenderProps, ThreeBoundaryMount, ThreeRoot } from '../core/root.js';
7
+ import type { RootState, Size } from '../core/store.js';
8
+ import { createPointerEvents } from './events.js';
9
+ import { observeCanvasSize } from './measure.js';
10
+ import type { ResizeOptions } from './measure.js';
11
+
12
+ const ThreeBoundary = createUniversalHostBoundary('three');
13
+ const DEFAULT_RESIZE_OPTIONS: ResizeOptions = Object.freeze({
14
+ scroll: true,
15
+ debounce: Object.freeze({ scroll: 50, resize: 0 }),
16
+ });
17
+
18
+ export type CanvasStyle = Readonly<Record<string, string | number | null | undefined>>;
19
+ type EventPrefix = 'offset' | 'client' | 'page' | 'layer' | 'screen';
20
+ export type CanvasRef =
21
+ | { current: HTMLCanvasElement | null }
22
+ | ((canvas: HTMLCanvasElement | null) => void | (() => void))
23
+ | readonly CanvasRef[]
24
+ | null;
25
+
26
+ export interface CanvasProps extends Omit<RenderProps<HTMLCanvasElement>, 'size'>, Record<
27
+ string,
28
+ unknown
29
+ > {
30
+ /** Compiler-owned Three renderer region. Application JSX is lowered into this payload. */
31
+ children?: RendererRegion;
32
+ /** Fallback content rendered as native canvas fallback DOM. */
33
+ fallback?: unknown;
34
+ /** DOM measurement options compatible with the R3F Canvas surface. */
35
+ resize?: ResizeOptions;
36
+ /** Event subscription target. Defaults to the outer Canvas wrapper. */
37
+ eventSource?: HTMLElement | { current: HTMLElement | null };
38
+ /** Coordinate prefix used to normalize pointer positions. Defaults to `offset`. */
39
+ eventPrefix?: EventPrefix;
40
+ ref?: CanvasRef;
41
+ style?: CanvasStyle;
42
+ }
43
+
44
+ function sameSize(previous: Size | null, next: Size): boolean {
45
+ return previous !== null && previous.width === next.width && previous.height === next.height &&
46
+ previous.top === next.top &&
47
+ previous.left === next.left;
48
+ }
49
+
50
+ function configurationError(error: unknown): Error {
51
+ return error instanceof Error
52
+ ? error
53
+ : new Error(`@octanejs/three: Canvas configuration failed: ${String(error)}`);
54
+ }
55
+
56
+ interface CanvasEventBinding {
57
+ initialized: boolean;
58
+ restoreCompute?: ComputeFunction;
59
+ source?: CanvasProps['eventSource'];
60
+ target?: HTMLElement | null;
61
+ prefix?: EventPrefix;
62
+ prefixCompute?: ComputeFunction;
63
+ }
64
+
65
+ function resolveEventSource(
66
+ source: CanvasProps['eventSource'],
67
+ wrapper: HTMLDivElement | null,
68
+ ): HTMLElement | null {
69
+ if (source === undefined) return wrapper;
70
+ return 'current' in source ? source.current ?? wrapper : source;
71
+ }
72
+
73
+ function createPrefixCompute(prefix: EventPrefix): ComputeFunction {
74
+ return (event: DomEvent, state: RootState) => {
75
+ const x = event[`${prefix}X` as keyof DomEvent] as number;
76
+ const y = event[`${prefix}Y` as keyof DomEvent] as number;
77
+ state.pointer.set((x / state.size.width) * 2 - 1, -(y / state.size.height) * 2 + 1);
78
+ state.raycaster.setFromCamera(state.pointer, state.camera);
79
+ };
80
+ }
81
+
82
+ function bindCanvasEvents(
83
+ state: RootState,
84
+ source: CanvasProps['eventSource'],
85
+ wrapper: HTMLDivElement | null,
86
+ prefix: EventPrefix | undefined,
87
+ binding: CanvasEventBinding,
88
+ ): void {
89
+ let current = state.get();
90
+ const target = resolveEventSource(source, wrapper);
91
+ if (!binding.initialized || binding.source !== source || binding.target !== target) {
92
+ binding.source = source;
93
+ binding.target = target;
94
+ if (target === null) current.events.disconnect?.();
95
+ else if (current.events.connected !== target) current.events.connect?.(target);
96
+ current = state.get();
97
+ }
98
+
99
+ if (!binding.initialized) binding.initialized = true;
100
+ if (binding.prefix !== prefix) {
101
+ const previousPrefixCompute = binding.prefixCompute;
102
+ if (binding.prefix === undefined || current.events.compute !== previousPrefixCompute) {
103
+ binding.restoreCompute = current.events.compute;
104
+ }
105
+ binding.prefix = prefix;
106
+ if (prefix === undefined) {
107
+ binding.prefixCompute = undefined;
108
+ if (current.events.compute === previousPrefixCompute) {
109
+ current.setEvents({ compute: binding.restoreCompute });
110
+ }
111
+ } else {
112
+ const prefixCompute = createPrefixCompute(prefix);
113
+ binding.prefixCompute = prefixCompute;
114
+ current.setEvents({ compute: prefixCompute });
115
+ }
116
+ }
117
+ }
118
+
119
+ /** A measured DOM canvas whose authored children execute in the Three renderer. */
120
+ export function Canvas({
121
+ ref,
122
+ children,
123
+ fallback,
124
+ resize,
125
+ style,
126
+ gl,
127
+ events = createPointerEvents,
128
+ eventSource,
129
+ eventPrefix,
130
+ shadows,
131
+ linear,
132
+ flat,
133
+ legacy,
134
+ orthographic,
135
+ frameloop,
136
+ dpr,
137
+ performance,
138
+ raycaster,
139
+ camera,
140
+ scene,
141
+ onPointerMissed,
142
+ onCreated,
143
+ ...domProps
144
+ }: CanvasProps) {
145
+ const wrapperRef = useRef<HTMLDivElement | null>(null);
146
+ const canvasRef = useRef<HTMLCanvasElement | null>(null);
147
+ const rootRef = useRef<ThreeRoot<HTMLCanvasElement> | null>(null);
148
+ const eventSourceRef = useRef<CanvasProps['eventSource']>(eventSource);
149
+ const eventPrefixRef = useRef<EventPrefix | undefined>(eventPrefix);
150
+ const onCreatedRef = useRef(onCreated);
151
+ const onPointerMissedRef = useRef(onPointerMissed);
152
+ const eventBindingRef = useRef<CanvasEventBinding>({ initialized: false });
153
+ const createdHandlerRef = useRef<((state: RootState) => void) | null>(null);
154
+ const pointerMissedHandlerRef = useRef<((event: MouseEvent) => void) | null>(null);
155
+ const [size, setSize] = useState<Size | null>(null);
156
+ const [boundaryMount, setBoundaryMount] = useState<ThreeBoundaryMount | null>(null);
157
+ const [error, setError] = useState<Error | null>(null);
158
+ const eventTarget = resolveEventSource(eventSource, wrapperRef.current);
159
+ eventSourceRef.current = eventSource;
160
+ eventPrefixRef.current = eventPrefix;
161
+ onCreatedRef.current = onCreated;
162
+ onPointerMissedRef.current = onPointerMissed;
163
+ if (createdHandlerRef.current === null) {
164
+ createdHandlerRef.current = (state) => {
165
+ bindCanvasEvents(
166
+ state,
167
+ eventSourceRef.current,
168
+ wrapperRef.current,
169
+ eventPrefixRef.current,
170
+ eventBindingRef.current,
171
+ );
172
+ onCreatedRef.current?.(state.get());
173
+ };
174
+ }
175
+ if (pointerMissedHandlerRef.current === null) {
176
+ pointerMissedHandlerRef.current = (event) => onPointerMissedRef.current?.(event);
177
+ }
178
+
179
+ if (error !== null) throw error;
180
+
181
+ useLayoutEffect(() => {
182
+ const wrapper = wrapperRef.current;
183
+ if (wrapper === null) return;
184
+ return observeCanvasSize(wrapper, resize ?? DEFAULT_RESIZE_OPTIONS, (next) => {
185
+ setSize((previous) => (sameSize(previous, next) ? previous : next));
186
+ });
187
+ }, [resize]);
188
+
189
+ // A DOM Suspense or Activity boundary disconnects layout effects while its
190
+ // content is hidden. Root ownership must survive that retained state and
191
+ // end only when the Canvas is actually deleted.
192
+ useInsertionEffect(() => {
193
+ return () => {
194
+ const root = rootRef.current;
195
+ rootRef.current = null;
196
+ root?.unmount();
197
+ };
198
+ }, []);
199
+
200
+ useLayoutEffect(() => {
201
+ const canvas = canvasRef.current;
202
+ if (canvas === null || size === null || size.width <= 0 || size.height <= 0) return;
203
+ let cancelled = false;
204
+ let root = rootRef.current;
205
+ if (root === null) {
206
+ root = createRoot(canvas);
207
+ rootRef.current = root;
208
+ }
209
+ void root.configure({
210
+ gl,
211
+ events,
212
+ shadows,
213
+ linear,
214
+ flat,
215
+ legacy,
216
+ orthographic,
217
+ frameloop,
218
+ dpr,
219
+ performance,
220
+ raycaster,
221
+ camera,
222
+ scene,
223
+ onPointerMissed: pointerMissedHandlerRef.current,
224
+ onCreated: createdHandlerRef.current,
225
+ size,
226
+ }).then(() => {
227
+ if (cancelled) return;
228
+ setBoundaryMount(createThreeBoundaryMount(root));
229
+ }).catch((reason) => {
230
+ if (!cancelled) setError(configurationError(reason));
231
+ });
232
+ return () => {
233
+ cancelled = true;
234
+ };
235
+ }, [
236
+ size,
237
+ gl,
238
+ events,
239
+ shadows,
240
+ linear,
241
+ flat,
242
+ legacy,
243
+ orthographic,
244
+ frameloop,
245
+ dpr,
246
+ performance,
247
+ raycaster,
248
+ camera,
249
+ scene,
250
+ ]);
251
+
252
+ // A stable ref object can receive a new current target during commit. Recheck after every
253
+ // render; bindCanvasEvents avoids reconnecting when the resolved target is unchanged.
254
+ useLayoutEffect(() => {
255
+ const root = rootRef.current;
256
+ if (boundaryMount === null || root === null) return;
257
+ bindCanvasEvents(
258
+ root.store.getState(),
259
+ eventSource,
260
+ wrapperRef.current,
261
+ eventPrefix,
262
+ eventBindingRef.current,
263
+ );
264
+ }, null);
265
+
266
+ const renderedBoundaryMount: ThreeBoundaryMount | null =
267
+ boundaryMount === null
268
+ ? null
269
+ : {
270
+ ...boundaryMount,
271
+ props: { ...boundaryMount.props, region: children },
272
+ };
273
+
274
+ const wrapperStyle: CanvasStyle = {
275
+ position: 'relative',
276
+ width: '100%',
277
+ height: '100%',
278
+ overflow: 'hidden',
279
+ pointerEvents: eventTarget === wrapperRef.current ? 'auto' : 'none',
280
+ ...style,
281
+ };
282
+
283
+ return <div ref={wrapperRef} style={wrapperStyle} {...domProps}>
284
+ <div style={{ width: '100%', height: '100%' }}>
285
+ <canvas ref={[canvasRef, ref]} style={{ display: 'block' }}>{fallback}</canvas>
286
+ </div>
287
+ {renderedBoundaryMount === null
288
+ ? null
289
+ : <ThreeBoundary
290
+ root={renderedBoundaryMount.root}
291
+ component={renderedBoundaryMount.component}
292
+ props={renderedBoundaryMount.props}
293
+ />}
294
+ </div>;
295
+ }
@@ -0,0 +1,23 @@
1
+ import type { RendererRegion } from 'octane/universal';
2
+ import type { RenderProps } from '../core/root.js';
3
+ import type { ResizeOptions } from './measure.js';
4
+
5
+ export type CanvasStyle = Readonly<Record<string, string | number | null | undefined>>;
6
+ export type CanvasRef =
7
+ | { current: HTMLCanvasElement | null }
8
+ | ((canvas: HTMLCanvasElement | null) => void | (() => void))
9
+ | readonly CanvasRef[]
10
+ | null;
11
+
12
+ export interface CanvasProps
13
+ extends Omit<RenderProps<HTMLCanvasElement>, 'size'>, Record<string, unknown> {
14
+ children?: RendererRegion;
15
+ fallback?: unknown;
16
+ resize?: ResizeOptions;
17
+ eventSource?: HTMLElement | { current: HTMLElement | null };
18
+ eventPrefix?: 'offset' | 'client' | 'page' | 'layer' | 'screen';
19
+ ref?: CanvasRef;
20
+ style?: CanvasStyle;
21
+ }
22
+
23
+ export declare function Canvas(props: CanvasProps): unknown;