packages/three/src/lib/define-three-layer.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
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 version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.