Skip to content

packages/three/src/lib/define-three-layer.ts

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

1 import { Raycaster, WebGLRenderer, type Object3D } from "three";
2 import {
3   defineRenderLayer,
4   type PibblRenderLayerContext,
5   type PibblRenderLayerTarget,
6 } from "@pibbl/core";
7 import { createDefaultPicker } from "./default-picker.js";
8 import { projectObjectBounds } from "./project-bounds.js";
9 import {
10   createThreeInputBridge,
11   type ThreeInputBridgeController,
12 } from "./three-input-bridge.js";
13 import type {
14   PibblThreeLayerContext,
15   PibblThreeLayerDefinition,
16   PibblThreeLayerResources,
17   PibblThreeTarget,
18 } from "./types.js";
19 
20 interface ThreeAdapterResources<
21   Props,
22   Resources extends PibblThreeLayerResources,
23 > {
24   readonly renderer: WebGLRenderer;
25   readonly input: ThreeInputBridgeController;
26   readonly threeContext: PibblThreeLayerContext;
27   readonly resources: Resources;
28   readonly raycaster: Raycaster;
29   props: Readonly<Props>;
30 }
31 
32 /**
33  * Defines a Pibbl external layer using ordinary Three.js scenes and resources. Each mount owns one
34  * transparent renderer; dispose must release application-created resources, and animation uses
35  * Pibbl's supplied frame and invalidation.
36  *
37  * @param definition - Resource lifecycle, rendering overrides, targets, and picking configuration.
38  * See {@link PibblThreeLayerDefinition} .
39  * @returns A Pibbl component mounting a persistent Three.js scene boundary with an adapter-owned
40  * renderer.
41  *
42  * @see {@link PibblThreeLayerDefinition}
43  * @see {@link PibblThreeLayerResources}
44  */
45 export function defineThreeLayer<
46   Props,
47   Resources extends PibblThreeLayerResources,
48 >(definition: PibblThreeLayerDefinition<Props, Resources>) {
49   const immutableDefinition = snapshotDefinition(definition);
50   return defineRenderLayer<
51     Props,
52     ThreeAdapterResources<Props, Resources>,
53     Object3D
54   >({
55     create(context, initialProps) {
56       const renderer = new WebGLRenderer({
57         ...immutableDefinition.renderer,
58         canvas: context.canvas,
59         alpha: true,
60       });
61       const input = createThreeInputBridge(context);
62       const threeContext = createThreeContext(context, renderer, input);
63       try {
64         const resources = immutableDefinition.create(
65           threeContext,
66           initialProps,
67         );
68         assertSynchronous(resources, "create");
69         assertThreeResources(resources);
70         input.finishCreate();
71         return {
72           renderer,
73           input,
74           threeContext,
75           resources,
76           raycaster: new Raycaster(),
77           props: initialProps,
78         };
79       } catch (error) {
80         try {
81           input.dispose();
82         } finally {
83           renderer.dispose();
84         }
85         throw error;
86       }
87     },
88     update(adapter, props) {
89       assertSynchronous(
90         immutableDefinition.update?.(
91           adapter.resources,
92           props,
93           adapter.threeContext,
94         ),
95         "update",
96       );
97       adapter.props = props;
98     },
99     resize(adapter, size) {
100       adapter.renderer.setPixelRatio(1);
101       adapter.renderer.setSize(size.backingWidth, size.backingHeight, false);
102       assertSynchronous(
103         immutableDefinition.resize?.(
104           adapter.resources,
105           size,
106           adapter.threeContext,
107         ),
108         "resize",
109       );
110     },
111     render(adapter, frame) {
112       if (immutableDefinition.render) {
113         assertSynchronous(
114           immutableDefinition.render(
115             adapter.resources,
116             frame,
117             adapter.threeContext,
118           ),
119           "render",
120         );
121       } else {
122         adapter.renderer.render(
123           adapter.resources.scene,
124           adapter.resources.camera,
125         );
126       }
127     },
128     dispose(adapter) {
129       try {
130         assertSynchronous(
131           immutableDefinition.dispose?.(
132             adapter.resources,
133             adapter.threeContext,
134           ),
135           "dispose",
136         );
137       } finally {
138         try {
139           adapter.input.dispose();
140         } finally {
141           adapter.renderer.dispose();
142         }
143       }
144     },
145     interaction: {
146       settle(adapter, props, size) {
147         const declaredTargets =
148           immutableDefinition.targets?.(adapter.resources, props) ?? [];
149         assertSynchronous(declaredTargets, "targets");
150         const declarations = Array.from(declaredTargets);
151         const targetMap = new Map<Object3D, PibblThreeTarget>();
152         const disabledRoots = new Set<Object3D>();
153         const targets: PibblRenderLayerTarget<Object3D>[] = [];
154         for (const declaration of declarations) {
155           targetMap.set(declaration.object, declaration);
156           if (declaration.pointerEvents === "none") {
157             disabledRoots.add(declaration.object);
158           }
159           const projectedBounds =
160             declaration.keyboardFocusable &&
161             !declaration.keyboardNavigationBounds
162               ? projectObjectBounds(
163                   declaration.object,
164                   adapter.resources.camera,
165                   size,
166                 )
167               : declaration.keyboardNavigationBounds;
168           targets.push({
169             target: declaration.object,
170             handlers: declaration.handlers,
171             pointerEvents: declaration.pointerEvents,
172             cursor: declaration.cursor,
173             keyboardFocusable:
174               declaration.keyboardFocusable && projectedBounds !== undefined,
175             keyboardNavigationBounds: projectedBounds,
176           });
177         }
178         let hitTest;
179         if (immutableDefinition.pick) {
180           const picker = immutableDefinition.pick(
181             adapter.resources,
182             adapter.threeContext,
183           );
184           assertSynchronous(picker, "pick");
185           hitTest = (point: Readonly<{ x: number; y: number }>) => {
186             const result = picker(point);
187             assertSynchronous(result, "picker");
188             return result;
189           };
190         } else {
191           hitTest = createDefaultPicker(
192             adapter.resources.scene,
193             adapter.resources.camera,
194             adapter.raycaster,
195             size,
196             targetMap,
197             disabledRoots,
198           );
199         }
200         return {
201           targets,
202           hitTest,
203           onInput: adapter.input.connected
204             ? (input) => adapter.input.deliver(input)
205             : undefined,
206         };
207       },
208     },
209   });
210 }
211 
212 function assertSynchronous(value: unknown, callback: string): void {
213   if (value && typeof (value as { then?: unknown }).then === "function") {
214     throw new TypeError(`defineThreeLayer ${callback}() must be synchronous.`);
215   }
216 }
217 
218 function createThreeContext(
219   context: Readonly<PibblRenderLayerContext>,
220   renderer: WebGLRenderer,
221   input: ThreeInputBridgeController,
222 ): PibblThreeLayerContext {
223   return Object.freeze({
224     canvas: context.canvas,
225     get size() {
226       return context.size;
227     },
228     invalidate: () => context.invalidate(),
229     renderer,
230     input,
231   });
232 }
233 
234 function assertThreeResources(
235   resources: PibblThreeLayerResources,
236 ): asserts resources is PibblThreeLayerResources {
237   if (!resources?.scene?.isScene || !resources?.camera?.isCamera) {
238     throw new TypeError(
239       "defineThreeLayer create() must return Three scene and camera resources.",
240     );
241   }
242 }
243 
244 function snapshotDefinition<Props, Resources extends PibblThreeLayerResources>(
245   definition: PibblThreeLayerDefinition<Props, Resources>,
246 ): PibblThreeLayerDefinition<Props, Resources> {
247   return Object.freeze({
248     renderer: definition.renderer
249       ? Object.freeze({ ...definition.renderer })
250       : undefined,
251     create: definition.create,
252     update: definition.update,
253     resize: definition.resize,
254     render: definition.render,
255     targets: definition.targets,
256     pick: definition.pick,
257     dispose: definition.dispose,
258   });
259 }
260 

Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.