Skip to content
Source
Sign inSign up

mountDeviceViewer.ts

110 lines4.1 KB

1import { createPhoneInteraction } from "@t3tools/client-runtime/device/phone-interaction";
2import { observeResize } from "../../lib/observeResize";
3import { bindPhoneTrackpad } from "./phoneTrackpad";
4​
5type PhoneInteraction = ReturnType<typeof createPhoneInteraction>;
6type TrackpadPinch = NonNullable<Parameters<typeof bindPhoneTrackpad>[2]>;
7​
8interface MountableViewer {
9 readonly resize: (width: number, height: number, pixelRatio: number) => void;
10 readonly screenPoint: (
11 x: number,
12 y: number,
13 captured?: boolean,
14 ) => { x: number; y: number } | null;
15 readonly orbit: (deltaX: number, deltaY: number) => void;
16 readonly setInteractionActive: (active: boolean, mode: "touch" | "orbit") => void;
17 readonly resetPose: () => void;
18 readonly dispose: () => void;
20​
21export interface DeviceViewerReady {
22 /** Draws the latest decoded frame; registered as the stream's frame listener. */
23 readonly frame: () => void;
24 readonly pinch?: TrackpadPinch;
25 readonly dispose?: () => void;
27​
28/**
29 * Mounts a lazily loaded 3D device viewer on a canvas and returns its teardown, for use from
30 * the canvas ref callback. Owns host resizing, input cancellation, trackpad gestures, and
31 * disposal shared by every viewport.
32 */
33export function mountDeviceViewer<V extends MountableViewer>(
34 canvas: HTMLCanvasElement,
35 host: HTMLElement,
36 options: {
37 /** Resolves to the constructor, so a canvas unmounted while loading never builds a viewer. */
38 readonly load: () => Promise<() => V>;
39 readonly touch: (phase: "begin" | "move" | "end", point: { x: number; y: number }) => void;
40 readonly viewerRef: { current: V | null };
41 readonly interactionRef: { current: PhoneInteraction | null };
42 readonly onInputCancel: (cancel: (() => void) | null) => void;
43 readonly onResetReady: (reset: (() => void) | null) => void;
44 readonly onFrameListener: (listener: (() => void) | null) => void;
45 readonly onUnavailable: () => void;
46 readonly cancelInput?: (viewer: V) => void;
47 readonly ready: (context: {
48 readonly viewer: V;
49 readonly cancelTrackpad: () => void;
50 }) => DeviceViewerReady;
51 },
52) {
53 const { viewerRef, interactionRef } = options;
54 let disposed = false;
55 let trackpad: ReturnType<typeof bindPhoneTrackpad> | null = null;
56 let stopTrackpadEnd: (() => void) | undefined;
57 let mounted: DeviceViewerReady | null = null;
58 const resize = () => {
59 const { width, height } = host.getBoundingClientRect();
60 viewerRef.current?.resize(width, height, window.devicePixelRatio);
61 };
62 const stopObserving = observeResize(host, resize);
63 const blur = () => {
64 interactionRef.current?.end();
65 trackpad?.cancel();
66 if (viewerRef.current) options.cancelInput?.(viewerRef.current);
67 };
68 options.onInputCancel(blur);
69 window.addEventListener("blur", blur);
70 void options
71 .load()
72 .then((create) => {
73 if (disposed) return;
74 const viewer = create();
75 viewerRef.current = viewer;
76 options.onResetReady(viewer.resetPose);
77 const interaction = createPhoneInteraction({
78 screenPoint: (point, captured) => viewer.screenPoint(point.x, point.y, captured),
79 touch: options.touch,
80 orbit: viewer.orbit,
81 zoomBy: () => {},
82 onInteractionActive: viewer.setInteractionActive,
83 });
84 interactionRef.current = interaction;
85 resize();
86 mounted = options.ready({ viewer, cancelTrackpad: () => trackpad?.cancel() });
87 options.onFrameListener(mounted.frame);
88 mounted.frame();
89 trackpad = bindPhoneTrackpad(canvas, interaction, mounted.pinch);
90 stopTrackpadEnd = window.desktopBridge?.onTrackpadScrollEnd(() => trackpad?.endOrbit());
91 })
92 .catch(() => {
93 if (!disposed) options.onUnavailable();
94 });
95 return () => {
96 disposed = true;
97 stopTrackpadEnd?.();
98 trackpad?.dispose();
99 mounted?.dispose?.();
100 interactionRef.current?.end();
101 interactionRef.current = null;
102 options.onInputCancel(null);
103 options.onResetReady(null);
104 options.onFrameListener(null);
105 stopObserving();
106 window.removeEventListener("blur", blur);
107 viewerRef.current?.dispose();
108 viewerRef.current = null;
109 };