@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/LICENSE +49 -0
- package/README.md +215 -0
- package/UPSTREAM.md +58 -0
- package/package.json +77 -0
- package/src/config.ts +50 -0
- package/src/core/attach.ts +117 -0
- package/src/core/catalogue.ts +245 -0
- package/src/core/driver.ts +1609 -0
- package/src/core/events.ts +539 -0
- package/src/core/hooks.ts +203 -0
- package/src/core/index.ts +86 -0
- package/src/core/loader.ts +210 -0
- package/src/core/loop.ts +210 -0
- package/src/core/portal.ts +236 -0
- package/src/core/props.ts +645 -0
- package/src/core/root.ts +652 -0
- package/src/core/store.ts +741 -0
- package/src/index.ts +86 -0
- package/src/intrinsics.ts +18 -0
- package/src/renderer.ts +12 -0
- package/src/testing.ts +259 -0
- package/src/web/Canvas.tsrx +295 -0
- package/src/web/Canvas.tsrx.d.ts +23 -0
- package/src/web/events.ts +79 -0
- package/src/web/measure.ts +96 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-renderer Three portals.
|
|
3
|
+
*
|
|
4
|
+
* Component ownership and Octane context stay at the authored call site. Only
|
|
5
|
+
* the universal host-placement domain is redirected to the borrowed Object3D.
|
|
6
|
+
*/
|
|
7
|
+
import * as THREE from 'three';
|
|
8
|
+
import {
|
|
9
|
+
createPortal as createUniversalPortal,
|
|
10
|
+
defineUniversalComponent,
|
|
11
|
+
universalComponent,
|
|
12
|
+
universalContext,
|
|
13
|
+
useContext,
|
|
14
|
+
useInsertionEffect,
|
|
15
|
+
useLayoutEffect,
|
|
16
|
+
useMemo,
|
|
17
|
+
useRef,
|
|
18
|
+
type UniversalRenderable,
|
|
19
|
+
} from 'octane/universal';
|
|
20
|
+
import {
|
|
21
|
+
createThreePortalTarget,
|
|
22
|
+
createThreePortalTargetBinding,
|
|
23
|
+
type ThreePortalTargetBinding,
|
|
24
|
+
} from './driver.js';
|
|
25
|
+
import type { ComputeFunction, EventManager } from './events.js';
|
|
26
|
+
import {
|
|
27
|
+
createPortalStore,
|
|
28
|
+
RootStoreContext,
|
|
29
|
+
updateCamera,
|
|
30
|
+
type RootState,
|
|
31
|
+
type RootStore,
|
|
32
|
+
type Size,
|
|
33
|
+
} from './store.js';
|
|
34
|
+
|
|
35
|
+
export type InjectState = Partial<
|
|
36
|
+
Omit<RootState, 'events'> & {
|
|
37
|
+
events?: {
|
|
38
|
+
enabled?: boolean;
|
|
39
|
+
priority?: number;
|
|
40
|
+
compute?: ComputeFunction;
|
|
41
|
+
connected?: unknown;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
>;
|
|
45
|
+
|
|
46
|
+
interface PortalProps {
|
|
47
|
+
readonly children: UniversalRenderable;
|
|
48
|
+
readonly container: THREE.Object3D;
|
|
49
|
+
readonly state?: InjectState;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface PortalOptions {
|
|
53
|
+
readonly events: InjectState['events'];
|
|
54
|
+
readonly size: Partial<Size> | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface PortalLayer {
|
|
58
|
+
readonly store: RootStore;
|
|
59
|
+
readonly target: ThreePortalTargetBinding;
|
|
60
|
+
readonly sync: (parent: RootState, options: PortalOptions) => void;
|
|
61
|
+
readonly commitCamera: (parent: RootState, options: PortalOptions) => void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const PORTAL_OPTIONS = Symbol('octane.three.portal.options');
|
|
65
|
+
const PORTAL_OPTIONS_COMMIT = Symbol('octane.three.portal.options-commit');
|
|
66
|
+
const PORTAL_LAYER = Symbol('octane.three.portal.layer');
|
|
67
|
+
const PORTAL_CAMERA_COMMIT = Symbol('octane.three.portal.camera-commit');
|
|
68
|
+
const PORTAL_SUBSCRIPTION = Symbol('octane.three.portal.subscription');
|
|
69
|
+
|
|
70
|
+
function getWorldMatrixSnapshot(object: THREE.Object3D): THREE.Matrix4 {
|
|
71
|
+
if (object.matrixWorldAutoUpdate === false) return object.matrixWorld.clone();
|
|
72
|
+
const local = object.matrixAutoUpdate
|
|
73
|
+
? new THREE.Matrix4().compose(object.position, object.quaternion, object.scale)
|
|
74
|
+
: object.matrix.clone();
|
|
75
|
+
return object.parent === null ? local : getWorldMatrixSnapshot(object.parent).multiply(local);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* `Viewport.getCurrentViewport` asks its camera for a world position, which in
|
|
80
|
+
* Three updates the camera and ancestor matrices. Derive against a detached
|
|
81
|
+
* snapshot so a render that is later rejected cannot mutate caller-owned hosts.
|
|
82
|
+
*/
|
|
83
|
+
function snapshotCamera<T extends THREE.Camera>(camera: T): T {
|
|
84
|
+
// Inheriting the read-only camera surface avoids Camera.clone(), which
|
|
85
|
+
// recursively copies children and JSON-serializes userData. Only the fields
|
|
86
|
+
// that getWorldPosition mutates need detached ownership here.
|
|
87
|
+
const snapshot = Object.create(camera) as T;
|
|
88
|
+
const world = getWorldMatrixSnapshot(camera);
|
|
89
|
+
snapshot.parent = null;
|
|
90
|
+
snapshot.matrixAutoUpdate = false;
|
|
91
|
+
snapshot.matrixWorldAutoUpdate = true;
|
|
92
|
+
snapshot.matrix = world.clone();
|
|
93
|
+
snapshot.matrixWorld = world.clone();
|
|
94
|
+
snapshot.matrixWorldInverse = camera.matrixWorldInverse.clone();
|
|
95
|
+
return snapshot;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function createLayer(
|
|
99
|
+
previousRoot: RootStore,
|
|
100
|
+
container: THREE.Object3D,
|
|
101
|
+
state: InjectState,
|
|
102
|
+
options: PortalOptions,
|
|
103
|
+
): PortalLayer {
|
|
104
|
+
const { events: _events, size: _size, ...rest } = state;
|
|
105
|
+
const raycaster = new THREE.Raycaster();
|
|
106
|
+
const pointer = new THREE.Vector2();
|
|
107
|
+
const target = createThreePortalTargetBinding(container);
|
|
108
|
+
const store = createPortalStore({
|
|
109
|
+
...previousRoot.getState(),
|
|
110
|
+
...rest,
|
|
111
|
+
// A managed portal target may be reconstructed without changing the
|
|
112
|
+
// authored target handle. The driver advances the shared binding to the
|
|
113
|
+
// accepted replacement, and subsequent parent-state mirrors retain it.
|
|
114
|
+
scene: target.current as THREE.Scene,
|
|
115
|
+
} as RootState);
|
|
116
|
+
|
|
117
|
+
const sync = (parent: RootState, currentOptions: PortalOptions) => {
|
|
118
|
+
store.setState((local) => {
|
|
119
|
+
let viewport: Partial<RootState['viewport']> | undefined;
|
|
120
|
+
if (local.camera != null && currentOptions.size !== undefined) {
|
|
121
|
+
const size = {
|
|
122
|
+
...parent.size,
|
|
123
|
+
...currentOptions.size,
|
|
124
|
+
} as Size;
|
|
125
|
+
viewport = parent.viewport.getCurrentViewport(
|
|
126
|
+
snapshotCamera(local.camera),
|
|
127
|
+
new THREE.Vector3(),
|
|
128
|
+
size,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
...parent,
|
|
133
|
+
...local,
|
|
134
|
+
scene: target.current as THREE.Scene,
|
|
135
|
+
raycaster,
|
|
136
|
+
pointer,
|
|
137
|
+
mouse: pointer,
|
|
138
|
+
previousRoot,
|
|
139
|
+
events: {
|
|
140
|
+
...parent.events,
|
|
141
|
+
...local.events,
|
|
142
|
+
...currentOptions.events,
|
|
143
|
+
} as EventManager<any>,
|
|
144
|
+
size: {
|
|
145
|
+
...parent.size,
|
|
146
|
+
...currentOptions.size,
|
|
147
|
+
},
|
|
148
|
+
viewport: {
|
|
149
|
+
...parent.viewport,
|
|
150
|
+
...viewport,
|
|
151
|
+
},
|
|
152
|
+
setEvents(events: Partial<EventManager<any>>) {
|
|
153
|
+
store.setState((value) => ({
|
|
154
|
+
events: { ...value.events, ...events },
|
|
155
|
+
}));
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
const commitCamera = (parent: RootState, currentOptions: PortalOptions) => {
|
|
161
|
+
const local = store.getState();
|
|
162
|
+
if (
|
|
163
|
+
local.camera == null ||
|
|
164
|
+
currentOptions.size === undefined ||
|
|
165
|
+
local.camera === parent.camera
|
|
166
|
+
) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
updateCamera(local.camera, {
|
|
170
|
+
...parent.size,
|
|
171
|
+
...currentOptions.size,
|
|
172
|
+
} as Size);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
sync(previousRoot.getState(), options);
|
|
176
|
+
return { store, target, sync, commitCamera };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const Portal = defineUniversalComponent<PortalProps>('three', (props) => {
|
|
180
|
+
if (props.container?.isObject3D !== true) {
|
|
181
|
+
throw new TypeError('@octanejs/three: createPortal target must be a Three Object3D.');
|
|
182
|
+
}
|
|
183
|
+
// Context lookup itself does not subscribe to state; portal children select
|
|
184
|
+
// from the enclave store they actually use.
|
|
185
|
+
const activeStore = useContext(RootStoreContext);
|
|
186
|
+
if (activeStore === null) {
|
|
187
|
+
throw new Error('R3F: createPortal can only be used within the Canvas component!');
|
|
188
|
+
}
|
|
189
|
+
const options = { events: props.state?.events, size: props.state?.size };
|
|
190
|
+
const optionsRef = useRef<PortalOptions>(options, PORTAL_OPTIONS);
|
|
191
|
+
useInsertionEffect(
|
|
192
|
+
() => {
|
|
193
|
+
optionsRef.current = options;
|
|
194
|
+
},
|
|
195
|
+
[options.events, options.size],
|
|
196
|
+
PORTAL_OPTIONS_COMMIT,
|
|
197
|
+
);
|
|
198
|
+
const layer = useMemo(
|
|
199
|
+
() => createLayer(activeStore, props.container, props.state ?? {}, options),
|
|
200
|
+
[activeStore, props.container],
|
|
201
|
+
PORTAL_LAYER,
|
|
202
|
+
);
|
|
203
|
+
useInsertionEffect(
|
|
204
|
+
() => {
|
|
205
|
+
layer.commitCamera(activeStore.getState(), optionsRef.current);
|
|
206
|
+
},
|
|
207
|
+
[activeStore, layer],
|
|
208
|
+
PORTAL_CAMERA_COMMIT,
|
|
209
|
+
);
|
|
210
|
+
useLayoutEffect(
|
|
211
|
+
() => {
|
|
212
|
+
return activeStore.subscribe((state) => {
|
|
213
|
+
layer.sync(state, optionsRef.current);
|
|
214
|
+
layer.commitCamera(state, optionsRef.current);
|
|
215
|
+
});
|
|
216
|
+
},
|
|
217
|
+
[activeStore, layer],
|
|
218
|
+
PORTAL_SUBSCRIPTION,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
return universalContext(RootStoreContext, layer.store, () =>
|
|
222
|
+
createUniversalPortal(
|
|
223
|
+
props.children,
|
|
224
|
+
createThreePortalTarget(props.container, layer.store, layer.target),
|
|
225
|
+
),
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
/** Render Three children into a borrowed Object3D with an isolated state layer. */
|
|
230
|
+
export function createPortal(
|
|
231
|
+
children: UniversalRenderable,
|
|
232
|
+
container: THREE.Object3D,
|
|
233
|
+
state?: InjectState,
|
|
234
|
+
): UniversalRenderable {
|
|
235
|
+
return universalComponent('three', Portal, { children, container, state });
|
|
236
|
+
}
|