@opsydyn/effect-three 0.0.0-stage → 0.2.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/CHANGELOG.md +69 -0
- package/LICENSE +21 -0
- package/README.md +189 -2
- package/dist/index-01kwfekm.js +5118 -0
- package/dist/index-01kwfekm.js.map +40 -0
- package/dist/index-4045wme0.js +129 -0
- package/dist/index-4045wme0.js.map +10 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1361 -0
- package/dist/index.js.map +14 -0
- package/dist/kernel/animation.d.ts +14 -0
- package/dist/kernel/animation.d.ts.map +1 -0
- package/dist/kernel/behaviours.d.ts +16 -0
- package/dist/kernel/behaviours.d.ts.map +1 -0
- package/dist/kernel/bodies.d.ts +60 -0
- package/dist/kernel/bodies.d.ts.map +1 -0
- package/dist/kernel/bodySources.d.ts +47 -0
- package/dist/kernel/bodySources.d.ts.map +1 -0
- package/dist/kernel/build.d.ts +564 -0
- package/dist/kernel/build.d.ts.map +1 -0
- package/dist/kernel/cameraMotion.d.ts +39 -0
- package/dist/kernel/cameraMotion.d.ts.map +1 -0
- package/dist/kernel/clock.d.ts +17 -0
- package/dist/kernel/clock.d.ts.map +1 -0
- package/dist/kernel/contactShadowPasses.d.ts +47 -0
- package/dist/kernel/contactShadowPasses.d.ts.map +1 -0
- package/dist/kernel/description.d.ts +1120 -0
- package/dist/kernel/description.d.ts.map +1 -0
- package/dist/kernel/diff.d.ts +90 -0
- package/dist/kernel/diff.d.ts.map +1 -0
- package/dist/kernel/environmentPresets.d.ts +30 -0
- package/dist/kernel/environmentPresets.d.ts.map +1 -0
- package/dist/kernel/equality.d.ts +64 -0
- package/dist/kernel/equality.d.ts.map +1 -0
- package/dist/kernel/frames.d.ts +55 -0
- package/dist/kernel/frames.d.ts.map +1 -0
- package/dist/kernel/index.d.ts +37 -0
- package/dist/kernel/index.d.ts.map +1 -0
- package/dist/kernel/index.js +335 -0
- package/dist/kernel/index.js.map +9 -0
- package/dist/kernel/instances.d.ts +26 -0
- package/dist/kernel/instances.d.ts.map +1 -0
- package/dist/kernel/labels.d.ts +51 -0
- package/dist/kernel/labels.d.ts.map +1 -0
- package/dist/kernel/loop.d.ts +49 -0
- package/dist/kernel/loop.d.ts.map +1 -0
- package/dist/kernel/materials.d.ts +24 -0
- package/dist/kernel/materials.d.ts.map +1 -0
- package/dist/kernel/motion.d.ts +36 -0
- package/dist/kernel/motion.d.ts.map +1 -0
- package/dist/kernel/orbit.d.ts +127 -0
- package/dist/kernel/orbit.d.ts.map +1 -0
- package/dist/kernel/overlays.d.ts +16 -0
- package/dist/kernel/overlays.d.ts.map +1 -0
- package/dist/kernel/performance.d.ts +41 -0
- package/dist/kernel/performance.d.ts.map +1 -0
- package/dist/kernel/physics.d.ts +166 -0
- package/dist/kernel/physics.d.ts.map +1 -0
- package/dist/kernel/physicsDebug.d.ts +18 -0
- package/dist/kernel/physicsDebug.d.ts.map +1 -0
- package/dist/kernel/pointer.d.ts +357 -0
- package/dist/kernel/pointer.d.ts.map +1 -0
- package/dist/kernel/renderKit.d.ts +49 -0
- package/dist/kernel/renderKit.d.ts.map +1 -0
- package/dist/kernel/renderer.d.ts +83 -0
- package/dist/kernel/renderer.d.ts.map +1 -0
- package/dist/kernel/resources.d.ts +112 -0
- package/dist/kernel/resources.d.ts.map +1 -0
- package/dist/kernel/sceneGraph.d.ts +155 -0
- package/dist/kernel/sceneGraph.d.ts.map +1 -0
- package/dist/kernel/sceneIndex.d.ts +90 -0
- package/dist/kernel/sceneIndex.d.ts.map +1 -0
- package/dist/kernel/stage.d.ts +87 -0
- package/dist/kernel/stage.d.ts.map +1 -0
- package/dist/kernel/textLayout.d.ts +29 -0
- package/dist/kernel/textLayout.d.ts.map +1 -0
- package/dist/kernel/tween.d.ts +35 -0
- package/dist/kernel/tween.d.ts.map +1 -0
- package/dist/kernel/xrInput.d.ts +107 -0
- package/dist/kernel/xrInput.d.ts.map +1 -0
- package/dist/kernel/xrLocomotion.d.ts +38 -0
- package/dist/kernel/xrLocomotion.d.ts.map +1 -0
- package/dist/runtime/FrameSource.d.ts +29 -0
- package/dist/runtime/FrameSource.d.ts.map +1 -0
- package/dist/runtime/ResourceRegistry.d.ts +18 -0
- package/dist/runtime/ResourceRegistry.d.ts.map +1 -0
- package/dist/runtime/SceneRuntime.d.ts +68 -0
- package/dist/runtime/SceneRuntime.d.ts.map +1 -0
- package/dist/runtime/SceneSchema.d.ts +2536 -0
- package/dist/runtime/SceneSchema.d.ts.map +1 -0
- package/dist/runtime/errors.d.ts +19 -0
- package/dist/runtime/errors.d.ts.map +1 -0
- package/dist/runtime/index.d.ts +11 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/webgl/contactShadows.d.ts +5 -0
- package/dist/webgl/contactShadows.d.ts.map +1 -0
- package/dist/webgl/index.d.ts +11 -0
- package/dist/webgl/index.d.ts.map +1 -0
- package/dist/webgl/index.js +239 -0
- package/dist/webgl/index.js.map +13 -0
- package/dist/webgl/shaderGrid.d.ts +27 -0
- package/dist/webgl/shaderGrid.d.ts.map +1 -0
- package/dist/webgl/sky.d.ts +4 -0
- package/dist/webgl/sky.d.ts.map +1 -0
- package/dist/webgpu/contactShadows.d.ts +5 -0
- package/dist/webgpu/contactShadows.d.ts.map +1 -0
- package/dist/webgpu/index.d.ts +10 -0
- package/dist/webgpu/index.d.ts.map +1 -0
- package/dist/webgpu/index.js +216 -0
- package/dist/webgpu/index.js.map +13 -0
- package/dist/webgpu/shaderGrid.d.ts +5 -0
- package/dist/webgpu/shaderGrid.d.ts.map +1 -0
- package/dist/webgpu/sky.d.ts +4 -0
- package/dist/webgpu/sky.d.ts.map +1 -0
- package/package.json +82 -4
- package/src/index.ts +11 -0
- package/src/kernel/animation.ts +24 -0
- package/src/kernel/behaviours.ts +36 -0
- package/src/kernel/bodies.ts +338 -0
- package/src/kernel/bodySources.ts +203 -0
- package/src/kernel/build.ts +1325 -0
- package/src/kernel/cameraMotion.ts +202 -0
- package/src/kernel/clock.ts +35 -0
- package/src/kernel/contactShadowPasses.ts +207 -0
- package/src/kernel/description.ts +1197 -0
- package/src/kernel/diff.ts +383 -0
- package/src/kernel/environmentPresets.ts +38 -0
- package/src/kernel/equality.ts +604 -0
- package/src/kernel/frames.ts +124 -0
- package/src/kernel/index.ts +274 -0
- package/src/kernel/instances.ts +97 -0
- package/src/kernel/labels.ts +54 -0
- package/src/kernel/loop.ts +248 -0
- package/src/kernel/materials.ts +258 -0
- package/src/kernel/motion.ts +75 -0
- package/src/kernel/orbit.ts +554 -0
- package/src/kernel/overlays.ts +130 -0
- package/src/kernel/performance.ts +101 -0
- package/src/kernel/physics.ts +361 -0
- package/src/kernel/physicsDebug.ts +68 -0
- package/src/kernel/pointer.ts +785 -0
- package/src/kernel/renderKit.ts +54 -0
- package/src/kernel/renderer.ts +136 -0
- package/src/kernel/resources.ts +504 -0
- package/src/kernel/sceneGraph.ts +1442 -0
- package/src/kernel/sceneIndex.ts +219 -0
- package/src/kernel/stage.ts +157 -0
- package/src/kernel/textLayout.ts +102 -0
- package/src/kernel/tween.ts +80 -0
- package/src/kernel/xrInput.ts +230 -0
- package/src/kernel/xrLocomotion.ts +72 -0
- package/src/runtime/FrameSource.ts +41 -0
- package/src/runtime/ResourceRegistry.ts +24 -0
- package/src/runtime/SceneRuntime.ts +229 -0
- package/src/runtime/SceneSchema.ts +983 -0
- package/src/runtime/errors.ts +18 -0
- package/src/runtime/index.ts +10 -0
- package/src/webgl/contactShadows.ts +67 -0
- package/src/webgl/index.ts +19 -0
- package/src/webgl/shaderGrid.ts +196 -0
- package/src/webgl/sky.ts +42 -0
- package/src/webgpu/contactShadows.ts +83 -0
- package/src/webgpu/index.ts +19 -0
- package/src/webgpu/shaderGrid.ts +158 -0
- package/src/webgpu/sky.ts +35 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orbit controls as data: pure functions from a camera and a gesture to the next camera.
|
|
3
|
+
* The camera stays in the application's Model; a `Gesture` Message carries the input and
|
|
4
|
+
* `orbit` applies it, so time travel, undo and persistence see every camera move.
|
|
5
|
+
*
|
|
6
|
+
* Behavioural reference: three.js `examples/jsm/controls/OrbitControls.js` at 0.186.1,
|
|
7
|
+
* the class Drei's `OrbitControls` wraps (`drei-main/src/core/OrbitControls.tsx` at
|
|
8
|
+
* `bf6f4ad`). As there:
|
|
9
|
+
*
|
|
10
|
+
* - The camera circles its `lookAt` point (the target) with world up `+y`.
|
|
11
|
+
* - Left drag rotates (with shift, control or meta: pans), middle drag dollies, right
|
|
12
|
+
* drag pans (with a modifier: rotates). One finger rotates; two pinch and pan.
|
|
13
|
+
* - A full drag across the height of the area turns the camera once round (`rotateSpeed`
|
|
14
|
+
* 1). Wheel and dolly steps scale the distance by `0.95 ^ (zoomSpeed * |delta| / 100)`;
|
|
15
|
+
* orthographic cameras change `zoom` instead.
|
|
16
|
+
* - Panning moves target and camera together, by the distance under the pointer.
|
|
17
|
+
* - Polar and azimuth limits, distance and zoom limits are applied after every step.
|
|
18
|
+
*
|
|
19
|
+
* Unlike three's controls these hold no state between calls; smoothing is the camera's
|
|
20
|
+
* own `damping`, which the scene applies to the rendered camera.
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
import {
|
|
25
|
+
MathUtils,
|
|
26
|
+
Matrix4,
|
|
27
|
+
OrthographicCamera,
|
|
28
|
+
PerspectiveCamera,
|
|
29
|
+
Plane,
|
|
30
|
+
Ray,
|
|
31
|
+
Spherical,
|
|
32
|
+
Vector3,
|
|
33
|
+
} from 'three';
|
|
34
|
+
|
|
35
|
+
import type { Camera, Vec3 } from './description.ts';
|
|
36
|
+
import type { Gesture, Modifiers } from './pointer.ts';
|
|
37
|
+
|
|
38
|
+
/** How `orbit` reads gestures: speeds and limits, as three.js's `OrbitControls`. */
|
|
39
|
+
export type OrbitOptions = {
|
|
40
|
+
readonly enableRotate: boolean;
|
|
41
|
+
readonly enableZoom: boolean;
|
|
42
|
+
readonly enablePan: boolean;
|
|
43
|
+
readonly rotateSpeed: number;
|
|
44
|
+
readonly zoomSpeed: number;
|
|
45
|
+
readonly panSpeed: number;
|
|
46
|
+
/** Pan in the screen's plane (`true`, the default) or along the ground (`false`). */
|
|
47
|
+
readonly screenSpacePanning: boolean;
|
|
48
|
+
/** Perspective cameras: the closest and furthest distance from the target. */
|
|
49
|
+
readonly minDistance: number;
|
|
50
|
+
readonly maxDistance: number;
|
|
51
|
+
/** Orthographic cameras: the smallest and largest `zoom`. */
|
|
52
|
+
readonly minZoom: number;
|
|
53
|
+
readonly maxZoom: number;
|
|
54
|
+
/** Radians from straight above (0) to straight below (π). */
|
|
55
|
+
readonly minPolarAngle: number;
|
|
56
|
+
readonly maxPolarAngle: number;
|
|
57
|
+
/** Radians around the vertical axis; infinite (the default) for no limit. */
|
|
58
|
+
readonly minAzimuthAngle: number;
|
|
59
|
+
readonly maxAzimuthAngle: number;
|
|
60
|
+
/** Dolly toward the point under the pointer rather than the target (`zoomToCursor`). */
|
|
61
|
+
readonly zoomToCursor: boolean;
|
|
62
|
+
/** Pixels an arrow key pans by, and turns an arrow key with a modifier makes. */
|
|
63
|
+
readonly keyPanSpeed: number;
|
|
64
|
+
readonly keyRotateSpeed: number;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** three.js's `OrbitControls` defaults. */
|
|
68
|
+
export const defaultOrbit: OrbitOptions = {
|
|
69
|
+
enableRotate: true,
|
|
70
|
+
enableZoom: true,
|
|
71
|
+
enablePan: true,
|
|
72
|
+
rotateSpeed: 1,
|
|
73
|
+
zoomSpeed: 1,
|
|
74
|
+
panSpeed: 1,
|
|
75
|
+
screenSpacePanning: true,
|
|
76
|
+
minDistance: 0,
|
|
77
|
+
maxDistance: Infinity,
|
|
78
|
+
minZoom: 0,
|
|
79
|
+
maxZoom: Infinity,
|
|
80
|
+
minPolarAngle: 0,
|
|
81
|
+
maxPolarAngle: Math.PI,
|
|
82
|
+
minAzimuthAngle: -Infinity,
|
|
83
|
+
maxAzimuthAngle: Infinity,
|
|
84
|
+
zoomToCursor: false,
|
|
85
|
+
keyPanSpeed: 7,
|
|
86
|
+
keyRotateSpeed: 1,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Orbit options: three.js's defaults with yours on top. */
|
|
90
|
+
export const orbitOptions = (options: Partial<OrbitOptions> = {}): OrbitOptions => ({
|
|
91
|
+
...defaultOrbit,
|
|
92
|
+
...options,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const twoPi = 2 * Math.PI;
|
|
96
|
+
|
|
97
|
+
// Scratch values, so a step allocates only the camera it returns.
|
|
98
|
+
const offset = new Vector3();
|
|
99
|
+
const spherical = new Spherical();
|
|
100
|
+
const target = new Vector3();
|
|
101
|
+
const position = new Vector3();
|
|
102
|
+
const axis = new Vector3();
|
|
103
|
+
const panOffset = new Vector3();
|
|
104
|
+
const basis = new Matrix4();
|
|
105
|
+
const worldUp = new Vector3(0, 1, 0);
|
|
106
|
+
|
|
107
|
+
const toVec3 = (vector: Vector3): Vec3 => [vector.x, vector.y, vector.z];
|
|
108
|
+
|
|
109
|
+
/** Clamps an azimuth to limits that may wrap past ±π (OrbitControls' `update`). */
|
|
110
|
+
const clampAzimuth = (theta: number, options: OrbitOptions): number => {
|
|
111
|
+
let min = options.minAzimuthAngle;
|
|
112
|
+
let max = options.maxAzimuthAngle;
|
|
113
|
+
if (!Number.isFinite(min) || !Number.isFinite(max)) return theta;
|
|
114
|
+
if (min < -Math.PI) min += twoPi;
|
|
115
|
+
else if (min > Math.PI) min -= twoPi;
|
|
116
|
+
if (max < -Math.PI) max += twoPi;
|
|
117
|
+
else if (max > Math.PI) max -= twoPi;
|
|
118
|
+
if (min <= max) return Math.max(min, Math.min(max, theta));
|
|
119
|
+
return theta > (min + max) / 2 ? Math.max(min, theta) : Math.min(max, theta);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const clampDistance = (distance: number, options: OrbitOptions): number =>
|
|
123
|
+
Math.max(options.minDistance, Math.min(options.maxDistance, distance));
|
|
124
|
+
|
|
125
|
+
/** Brings `spherical` within the limits, as every OrbitControls `update` does. */
|
|
126
|
+
const limit = (options: OrbitOptions): void => {
|
|
127
|
+
spherical.theta = clampAzimuth(spherical.theta, options);
|
|
128
|
+
spherical.phi = Math.max(options.minPolarAngle, Math.min(options.maxPolarAngle, spherical.phi));
|
|
129
|
+
spherical.makeSafe();
|
|
130
|
+
spherical.radius = clampDistance(spherical.radius, options);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Reads the camera's offset from its target into `spherical`, within the limits: a
|
|
135
|
+
* camera outside them starts from where the controls would first put it.
|
|
136
|
+
*/
|
|
137
|
+
const readSpherical = (camera: Camera, options: OrbitOptions): void => {
|
|
138
|
+
target.fromArray(camera.lookAt);
|
|
139
|
+
offset.fromArray(camera.position).sub(target);
|
|
140
|
+
spherical.setFromVector3(offset);
|
|
141
|
+
limit(options);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** The camera placed at `spherical` around `target`, within the limits, with `zoom`. */
|
|
145
|
+
const place = (camera: Camera, options: OrbitOptions, zoom: number): Camera => {
|
|
146
|
+
limit(options);
|
|
147
|
+
position.setFromSpherical(spherical).add(target);
|
|
148
|
+
const lookAt = toVec3(target);
|
|
149
|
+
return camera._tag === 'Perspective'
|
|
150
|
+
? { ...camera, position: toVec3(position), lookAt }
|
|
151
|
+
: {
|
|
152
|
+
...camera,
|
|
153
|
+
position: toVec3(position),
|
|
154
|
+
lookAt,
|
|
155
|
+
zoom: clampZoom(zoom, options),
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const clampZoom = (zoom: number, options: OrbitOptions): number =>
|
|
160
|
+
Math.max(options.minZoom, Math.min(options.maxZoom, zoom));
|
|
161
|
+
|
|
162
|
+
/** An orthographic camera's zoom within the limits; 1 for a perspective camera. */
|
|
163
|
+
const zoomOf = (camera: Camera, options: OrbitOptions): number =>
|
|
164
|
+
camera._tag === 'Orthographic' ? clampZoom(camera.zoom, options) : 1;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Turns the camera round its target by a drag of `deltaX`, `deltaY` CSS pixels over an
|
|
168
|
+
* area `height` pixels tall: a drag the full height turns it once round.
|
|
169
|
+
*/
|
|
170
|
+
export const rotateCamera = (
|
|
171
|
+
camera: Camera,
|
|
172
|
+
deltaX: number,
|
|
173
|
+
deltaY: number,
|
|
174
|
+
height: number,
|
|
175
|
+
options: OrbitOptions = defaultOrbit,
|
|
176
|
+
): Camera => {
|
|
177
|
+
readSpherical(camera, options);
|
|
178
|
+
const scale = (twoPi * options.rotateSpeed) / Math.max(1, height);
|
|
179
|
+
spherical.theta -= deltaX * scale;
|
|
180
|
+
spherical.phi -= deltaY * scale;
|
|
181
|
+
return place(camera, options, zoomOf(camera, options));
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Scales the camera's distance from its target by `scale` (below 1 moves closer). An
|
|
186
|
+
* orthographic camera keeps its distance and divides its `zoom` by `scale` instead.
|
|
187
|
+
*/
|
|
188
|
+
export const dollyCamera = (
|
|
189
|
+
camera: Camera,
|
|
190
|
+
scale: number,
|
|
191
|
+
options: OrbitOptions = defaultOrbit,
|
|
192
|
+
): Camera => {
|
|
193
|
+
readSpherical(camera, options);
|
|
194
|
+
if (camera._tag === 'Perspective') spherical.radius *= scale;
|
|
195
|
+
return place(camera, options, zoomOf(camera, options) / scale);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Moves the camera and its target together by a drag of `deltaX`, `deltaY` CSS pixels
|
|
200
|
+
* over an area `width` by `height`: the scene follows the pointer at the target's depth.
|
|
201
|
+
*/
|
|
202
|
+
export const panCamera = (
|
|
203
|
+
camera: Camera,
|
|
204
|
+
deltaX: number,
|
|
205
|
+
deltaY: number,
|
|
206
|
+
area: { readonly width: number; readonly height: number },
|
|
207
|
+
options: OrbitOptions = defaultOrbit,
|
|
208
|
+
): Camera => {
|
|
209
|
+
readSpherical(camera, options);
|
|
210
|
+
position.setFromSpherical(spherical).add(target);
|
|
211
|
+
basis.lookAt(position, target, worldUp);
|
|
212
|
+
const dx = deltaX * options.panSpeed;
|
|
213
|
+
const dy = deltaY * options.panSpeed;
|
|
214
|
+
// World units per CSS pixel at the target's depth.
|
|
215
|
+
const left =
|
|
216
|
+
camera._tag === 'Perspective'
|
|
217
|
+
? (2 * dx * spherical.radius * Math.tan((camera.fov / 2) * (Math.PI / 180))) /
|
|
218
|
+
Math.max(1, area.height)
|
|
219
|
+
: dx / zoomOf(camera, options);
|
|
220
|
+
const up =
|
|
221
|
+
camera._tag === 'Perspective'
|
|
222
|
+
? (2 * dy * spherical.radius * Math.tan((camera.fov / 2) * (Math.PI / 180))) /
|
|
223
|
+
Math.max(1, area.height)
|
|
224
|
+
: dy / zoomOf(camera, options);
|
|
225
|
+
panOffset.setFromMatrixColumn(basis, 0).multiplyScalar(-left);
|
|
226
|
+
if (options.screenSpacePanning) {
|
|
227
|
+
axis.setFromMatrixColumn(basis, 1);
|
|
228
|
+
} else {
|
|
229
|
+
axis.setFromMatrixColumn(basis, 0);
|
|
230
|
+
axis.crossVectors(worldUp, axis);
|
|
231
|
+
}
|
|
232
|
+
panOffset.addScaledVector(axis, up);
|
|
233
|
+
target.add(panOffset);
|
|
234
|
+
return place(camera, options, zoomOf(camera, options));
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/** Where on a gesture's area the pointer is: its size, and `x`, `y` from −1 to 1, y up. */
|
|
238
|
+
export type Cursor = {
|
|
239
|
+
readonly width: number;
|
|
240
|
+
readonly height: number;
|
|
241
|
+
readonly x: number;
|
|
242
|
+
readonly y: number;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/** OrbitControls' `_TILT_LIMIT`: steeper than 70° from up, a ground target is not moved. */
|
|
246
|
+
const tiltLimit = Math.cos(70 * MathUtils.DEG2RAD);
|
|
247
|
+
|
|
248
|
+
const pointer = new Vector3();
|
|
249
|
+
const before = new Vector3();
|
|
250
|
+
const forward = new Vector3();
|
|
251
|
+
const groundRay = new Ray();
|
|
252
|
+
const ground = new Plane();
|
|
253
|
+
|
|
254
|
+
/** A three.js camera placed as `spherical` around `target` puts it, fitted to `cursor`'s area. */
|
|
255
|
+
const hostCamera = (camera: Camera, cursor: Cursor, zoom: number) => {
|
|
256
|
+
const host =
|
|
257
|
+
camera._tag === 'Perspective'
|
|
258
|
+
? new PerspectiveCamera(
|
|
259
|
+
camera.fov,
|
|
260
|
+
cursor.width / Math.max(1, cursor.height),
|
|
261
|
+
camera.near,
|
|
262
|
+
camera.far,
|
|
263
|
+
)
|
|
264
|
+
: new OrthographicCamera(
|
|
265
|
+
cursor.width / -2,
|
|
266
|
+
cursor.width / 2,
|
|
267
|
+
cursor.height / 2,
|
|
268
|
+
cursor.height / -2,
|
|
269
|
+
camera.near,
|
|
270
|
+
camera.far,
|
|
271
|
+
);
|
|
272
|
+
if (host instanceof OrthographicCamera) host.zoom = zoom;
|
|
273
|
+
host.updateProjectionMatrix();
|
|
274
|
+
host.position.setFromSpherical(spherical).add(target);
|
|
275
|
+
host.lookAt(target);
|
|
276
|
+
host.updateMatrixWorld();
|
|
277
|
+
return host;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Dollies toward the point under the cursor by `scale` (below 1 moves closer), as
|
|
282
|
+
* OrbitControls does with `zoomToCursor`: a perspective camera moves along the pointer's
|
|
283
|
+
* ray; an orthographic one zooms and shifts so the point under the cursor stays there.
|
|
284
|
+
* The target then follows, `radius` ahead along the view (or on the ground, when panning
|
|
285
|
+
* along it and the view is not too steep). A middle-button drag aims where it was
|
|
286
|
+
* pressed: three 0.186's `_handleMouseDownDolly` passes `clientX` for both coordinates, a
|
|
287
|
+
* typo this does not copy.
|
|
288
|
+
*/
|
|
289
|
+
export const dollyCameraToward = (
|
|
290
|
+
camera: Camera,
|
|
291
|
+
scale: number,
|
|
292
|
+
cursor: Cursor,
|
|
293
|
+
options: OrbitOptions = defaultOrbit,
|
|
294
|
+
): Camera => {
|
|
295
|
+
readSpherical(camera, options);
|
|
296
|
+
const host = hostCamera(camera, cursor, zoomOf(camera, options));
|
|
297
|
+
let radius = spherical.radius;
|
|
298
|
+
if (host instanceof PerspectiveCamera) {
|
|
299
|
+
pointer.set(cursor.x, cursor.y, 1).unproject(host).sub(host.position).normalize();
|
|
300
|
+
const next = clampDistance(radius * scale, options);
|
|
301
|
+
host.position.addScaledVector(pointer, radius - next);
|
|
302
|
+
radius = next;
|
|
303
|
+
} else {
|
|
304
|
+
before.set(cursor.x, cursor.y, 0).unproject(host);
|
|
305
|
+
host.zoom = clampZoom(host.zoom / scale, options);
|
|
306
|
+
host.updateProjectionMatrix();
|
|
307
|
+
pointer.set(cursor.x, cursor.y, 0).unproject(host);
|
|
308
|
+
host.position.sub(pointer).add(before);
|
|
309
|
+
}
|
|
310
|
+
host.updateMatrixWorld();
|
|
311
|
+
forward.set(0, 0, -1).transformDirection(host.matrix);
|
|
312
|
+
if (options.screenSpacePanning) {
|
|
313
|
+
target.copy(forward).multiplyScalar(radius).add(host.position);
|
|
314
|
+
} else if (Math.abs(worldUp.dot(forward)) >= tiltLimit) {
|
|
315
|
+
groundRay.set(host.position, forward);
|
|
316
|
+
ground.setFromNormalAndCoplanarPoint(worldUp, target);
|
|
317
|
+
groundRay.intersectPlane(ground, target);
|
|
318
|
+
}
|
|
319
|
+
const lookAt = toVec3(target);
|
|
320
|
+
const placed = toVec3(host.position);
|
|
321
|
+
return camera._tag === 'Perspective'
|
|
322
|
+
? { ...camera, position: placed, lookAt }
|
|
323
|
+
: { ...camera, position: placed, lookAt, zoom: host.zoom };
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
/** `0.95 ^ (zoomSpeed * |delta| / 100)`: OrbitControls' `_getZoomScale`. */
|
|
327
|
+
const zoomScale = (delta: number, options: OrbitOptions): number =>
|
|
328
|
+
0.95 ** (options.zoomSpeed * Math.abs(delta * 0.01));
|
|
329
|
+
|
|
330
|
+
/** Dollies by `scale`, toward the cursor when the options ask for it. */
|
|
331
|
+
const dollyAt = (camera: Camera, scale: number, cursor: Cursor, options: OrbitOptions): Camera =>
|
|
332
|
+
options.zoomToCursor
|
|
333
|
+
? dollyCameraToward(camera, scale, cursor, options)
|
|
334
|
+
: dollyCamera(camera, scale, options);
|
|
335
|
+
|
|
336
|
+
/** A dolly by a wheel or vertical drag of `delta` pixels: up or forward moves closer. */
|
|
337
|
+
const dollyBy = (camera: Camera, delta: number, cursor: Cursor, options: OrbitOptions): Camera => {
|
|
338
|
+
if (delta === 0 || !options.enableZoom) return camera;
|
|
339
|
+
const scale = zoomScale(delta, options);
|
|
340
|
+
return dollyAt(camera, delta < 0 ? scale : 1 / scale, cursor, options);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const modified = (modifiers: Modifiers): boolean =>
|
|
344
|
+
modifiers.shift || modifiers.control || modifiers.meta;
|
|
345
|
+
|
|
346
|
+
type DragGesture = Extract<Gesture, { readonly _tag: 'Drag' }>;
|
|
347
|
+
|
|
348
|
+
const rotateBy = (camera: Camera, gesture: DragGesture, options: OrbitOptions): Camera =>
|
|
349
|
+
options.enableRotate
|
|
350
|
+
? rotateCamera(camera, gesture.deltaX, gesture.deltaY, gesture.height, options)
|
|
351
|
+
: camera;
|
|
352
|
+
|
|
353
|
+
const panBy = (
|
|
354
|
+
camera: Camera,
|
|
355
|
+
gesture: Extract<Gesture, { readonly deltaX: number }>,
|
|
356
|
+
options: OrbitOptions,
|
|
357
|
+
): Camera =>
|
|
358
|
+
options.enablePan ? panCamera(camera, gesture.deltaX, gesture.deltaY, gesture, options) : camera;
|
|
359
|
+
|
|
360
|
+
/** A drag by button: left rotates, middle dollies, right pans; a modifier swaps the two. */
|
|
361
|
+
const drag = (camera: Camera, gesture: DragGesture, options: OrbitOptions): Camera => {
|
|
362
|
+
switch (gesture.button) {
|
|
363
|
+
case 0:
|
|
364
|
+
return modified(gesture.modifiers)
|
|
365
|
+
? panBy(camera, gesture, options)
|
|
366
|
+
: rotateBy(camera, gesture, options);
|
|
367
|
+
case 1:
|
|
368
|
+
return dollyBy(camera, gesture.deltaY, gesture, options);
|
|
369
|
+
case 2:
|
|
370
|
+
return modified(gesture.modifiers)
|
|
371
|
+
? rotateBy(camera, gesture, options)
|
|
372
|
+
: panBy(camera, gesture, options);
|
|
373
|
+
default:
|
|
374
|
+
return camera;
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* The camera after one gesture, as three.js's `OrbitControls` would move it. Apply it in
|
|
380
|
+
* `update` and keep the result in the Model:
|
|
381
|
+
*
|
|
382
|
+
* ```ts
|
|
383
|
+
* GotGesture: ({ gesture }) => ({ model: { ...model, camera: orbit(model.camera, gesture) } })
|
|
384
|
+
* ```
|
|
385
|
+
*
|
|
386
|
+
* A wheel with control held (how trackpads report a pinch) dollies ten times as far.
|
|
387
|
+
*/
|
|
388
|
+
export const orbit = (
|
|
389
|
+
camera: Camera,
|
|
390
|
+
gesture: Gesture,
|
|
391
|
+
options: OrbitOptions = defaultOrbit,
|
|
392
|
+
): Camera => {
|
|
393
|
+
switch (gesture._tag) {
|
|
394
|
+
case 'Drag':
|
|
395
|
+
return drag(camera, gesture, options);
|
|
396
|
+
case 'Wheel':
|
|
397
|
+
return dollyBy(
|
|
398
|
+
camera,
|
|
399
|
+
gesture.modifiers.control ? gesture.deltaY * 10 : gesture.deltaY,
|
|
400
|
+
gesture,
|
|
401
|
+
options,
|
|
402
|
+
);
|
|
403
|
+
case 'Pinch': {
|
|
404
|
+
const dollied =
|
|
405
|
+
options.enableZoom && gesture.scale > 0
|
|
406
|
+
? dollyAt(camera, 1 / gesture.scale ** options.zoomSpeed, gesture, options)
|
|
407
|
+
: camera;
|
|
408
|
+
return panBy(dollied, gesture, options);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
/** The arrow keys OrbitControls listens to (`keys`). */
|
|
414
|
+
export type OrbitKey = 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight';
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* The camera after an arrow key, as OrbitControls' `_handleKeyDown`: up, down, left and
|
|
418
|
+
* right pan by `keyPanSpeed` pixels, or with shift, control or meta held turn by
|
|
419
|
+
* `keyRotateSpeed`. Map key presses to Messages (a Foldkit key subscription) and apply it.
|
|
420
|
+
*/
|
|
421
|
+
export const orbitKey = (
|
|
422
|
+
camera: Camera,
|
|
423
|
+
key: OrbitKey,
|
|
424
|
+
modifiers: Modifiers,
|
|
425
|
+
area: { readonly width: number; readonly height: number },
|
|
426
|
+
options: OrbitOptions = defaultOrbit,
|
|
427
|
+
): Camera => {
|
|
428
|
+
const turn = (twoPi * options.keyRotateSpeed) / Math.max(1, area.height);
|
|
429
|
+
const step = options.keyPanSpeed;
|
|
430
|
+
const rotating = modified(modifiers);
|
|
431
|
+
if (rotating && !options.enableRotate) return camera;
|
|
432
|
+
if (!rotating && !options.enablePan) return camera;
|
|
433
|
+
// A turn here is in radians; rotateCamera takes pixels over the area's height.
|
|
434
|
+
const turned = (left: number, up: number): Camera => {
|
|
435
|
+
readSpherical(camera, options);
|
|
436
|
+
spherical.theta -= left;
|
|
437
|
+
spherical.phi -= up;
|
|
438
|
+
return place(camera, options, zoomOf(camera, options));
|
|
439
|
+
};
|
|
440
|
+
switch (key) {
|
|
441
|
+
case 'ArrowUp':
|
|
442
|
+
return rotating ? turned(0, turn) : panCamera(camera, 0, step, area, options);
|
|
443
|
+
case 'ArrowDown':
|
|
444
|
+
return rotating ? turned(0, -turn) : panCamera(camera, 0, -step, area, options);
|
|
445
|
+
case 'ArrowLeft':
|
|
446
|
+
return rotating ? turned(turn, 0) : panCamera(camera, step, 0, area, options);
|
|
447
|
+
case 'ArrowRight':
|
|
448
|
+
return rotating ? turned(-turn, 0) : panCamera(camera, -step, 0, area, options);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
/** A world-space box, as `Measured` events report a node's. */
|
|
453
|
+
export type Box = { readonly min: Vec3; readonly max: Vec3 };
|
|
454
|
+
|
|
455
|
+
const corner = new Vector3();
|
|
456
|
+
const view = new Matrix4();
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* How far from a box's centre a camera must be to fit it, as Drei's `Bounds`
|
|
460
|
+
* (`src/core/Bounds.tsx` at `bf6f4ad`, `getSize`): the larger of the distances that fit
|
|
461
|
+
* its largest side vertically and horizontally, times `margin`.
|
|
462
|
+
*/
|
|
463
|
+
const fitDistance = (
|
|
464
|
+
camera: Camera,
|
|
465
|
+
box: Box,
|
|
466
|
+
area: { readonly width: number; readonly height: number },
|
|
467
|
+
margin: number,
|
|
468
|
+
): number => {
|
|
469
|
+
const largest = Math.max(
|
|
470
|
+
box.max[0] - box.min[0],
|
|
471
|
+
box.max[1] - box.min[1],
|
|
472
|
+
box.max[2] - box.min[2],
|
|
473
|
+
);
|
|
474
|
+
if (camera._tag === 'Orthographic') return margin * largest * 4;
|
|
475
|
+
const height = largest / (2 * Math.atan((Math.PI * camera.fov) / 360));
|
|
476
|
+
const width = height / (area.width / Math.max(1, area.height));
|
|
477
|
+
return margin * Math.max(height, width);
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const centreOf = (box: Box): Vec3 => [
|
|
481
|
+
(box.min[0] + box.max[0]) / 2,
|
|
482
|
+
(box.min[1] + box.max[1]) / 2,
|
|
483
|
+
(box.min[2] + box.max[2]) / 2,
|
|
484
|
+
];
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* The camera where turning by itself (`autoRotate`) took it, from an `AutoRotated`
|
|
488
|
+
* event: keep it in the Model before orbiting, so the gesture goes on from where the
|
|
489
|
+
* camera was shown.
|
|
490
|
+
*
|
|
491
|
+
* ```ts
|
|
492
|
+
* AutoRotated: ({ position }) => ({ model: { ...model, camera: autoRotated(model.camera, { position }) } })
|
|
493
|
+
* ```
|
|
494
|
+
*/
|
|
495
|
+
export const autoRotated = <C extends Camera>(
|
|
496
|
+
camera: C,
|
|
497
|
+
turned: { readonly position: Vec3 },
|
|
498
|
+
): C => ({ ...camera, position: turned.position });
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* The camera framing a box, as Drei's `Bounds` `fit` (margin 1.2 by default). A
|
|
502
|
+
* perspective camera keeps its direction and moves to the distance that fits the box,
|
|
503
|
+
* looking at its centre; an orthographic one keeps its place and zooms so the box's
|
|
504
|
+
* corners, seen from it, fit the area. Set the camera's `damping` to ease there.
|
|
505
|
+
*/
|
|
506
|
+
export const fitCamera = (
|
|
507
|
+
camera: Camera,
|
|
508
|
+
box: Box,
|
|
509
|
+
area: { readonly width: number; readonly height: number },
|
|
510
|
+
margin = 1.2,
|
|
511
|
+
): Camera => {
|
|
512
|
+
const centre = centreOf(box);
|
|
513
|
+
if (camera._tag === 'Orthographic') {
|
|
514
|
+
position.fromArray(camera.position);
|
|
515
|
+
target.fromArray(camera.lookAt);
|
|
516
|
+
view.lookAt(position, target, worldUp).setPosition(position).invert();
|
|
517
|
+
let halfWidth = 0;
|
|
518
|
+
let halfHeight = 0;
|
|
519
|
+
for (const x of [box.min[0], box.max[0]])
|
|
520
|
+
for (const y of [box.min[1], box.max[1]])
|
|
521
|
+
for (const z of [box.min[2], box.max[2]]) {
|
|
522
|
+
corner.set(x, y, z).applyMatrix4(view);
|
|
523
|
+
halfWidth = Math.max(halfWidth, Math.abs(corner.x));
|
|
524
|
+
halfHeight = Math.max(halfHeight, Math.abs(corner.y));
|
|
525
|
+
}
|
|
526
|
+
const zoom =
|
|
527
|
+
Math.min(
|
|
528
|
+
area.height / Math.max(1e-9, 2 * halfHeight),
|
|
529
|
+
area.width / Math.max(1e-9, 2 * halfWidth),
|
|
530
|
+
) / margin;
|
|
531
|
+
return { ...camera, zoom };
|
|
532
|
+
}
|
|
533
|
+
position.fromArray(camera.position);
|
|
534
|
+
target.fromArray(centre);
|
|
535
|
+
forward.copy(position).sub(target);
|
|
536
|
+
if (forward.lengthSq() === 0) forward.set(0, 0, 1);
|
|
537
|
+
forward.normalize();
|
|
538
|
+
position.copy(target).addScaledVector(forward, fitDistance(camera, box, area, margin));
|
|
539
|
+
return { ...camera, position: toVec3(position), lookAt: centre };
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Near and far planes that suit a box at its fitting distance, as Drei's `Bounds` `clip`:
|
|
544
|
+
* a hundredth of it and a hundred times it.
|
|
545
|
+
*/
|
|
546
|
+
export const clipCamera = (
|
|
547
|
+
camera: Camera,
|
|
548
|
+
box: Box,
|
|
549
|
+
area: { readonly width: number; readonly height: number },
|
|
550
|
+
margin = 1.2,
|
|
551
|
+
): Camera => {
|
|
552
|
+
const distance = fitDistance(camera, box, area, margin);
|
|
553
|
+
return { ...camera, near: distance / 100, far: distance * 100 };
|
|
554
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where each overlay's point is on the canvas, worked out for every frame the scene draws.
|
|
3
|
+
*
|
|
4
|
+
* Behavioural reference: Drei `src/web/Html.tsx` at `bf6f4ad`, without `transform`:
|
|
5
|
+
* `defaultCalculatePosition` (project to CSS pixels), `isObjectBehindCamera`,
|
|
6
|
+
* `isObjectVisible` (raycast occlusion against the scene), `objectScale` with
|
|
7
|
+
* `distanceFactor`, and `objectZIndex` over `zIndexRange`.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
type Camera,
|
|
13
|
+
type Object3D,
|
|
14
|
+
OrthographicCamera,
|
|
15
|
+
PerspectiveCamera,
|
|
16
|
+
Raycaster,
|
|
17
|
+
Vector2,
|
|
18
|
+
Vector3,
|
|
19
|
+
} from 'three';
|
|
20
|
+
|
|
21
|
+
import type { Overlay } from './description.ts';
|
|
22
|
+
import type { HostedView, SceneGraph } from './sceneGraph.ts';
|
|
23
|
+
|
|
24
|
+
/** Where to show one overlay's HTML, in CSS pixels from the canvas's top left. */
|
|
25
|
+
export type OverlayPlacement = {
|
|
26
|
+
readonly key: string;
|
|
27
|
+
readonly x: number;
|
|
28
|
+
readonly y: number;
|
|
29
|
+
/** 1, or the distance scale when the overlay has a `distanceFactor`. */
|
|
30
|
+
readonly scale: number;
|
|
31
|
+
readonly zIndex: number;
|
|
32
|
+
/** False behind the camera, or behind a mesh when the overlay occludes. */
|
|
33
|
+
readonly visible: boolean;
|
|
34
|
+
readonly center: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const point = new Vector3();
|
|
38
|
+
const cameraPosition = new Vector3();
|
|
39
|
+
const direction = new Vector3();
|
|
40
|
+
const toPoint = new Vector3();
|
|
41
|
+
const ndc = new Vector2();
|
|
42
|
+
const raycaster = new Raycaster();
|
|
43
|
+
|
|
44
|
+
/** The camera and area an overlay is shown through. */
|
|
45
|
+
type Lens = {
|
|
46
|
+
readonly camera: Camera;
|
|
47
|
+
readonly x: number;
|
|
48
|
+
readonly y: number;
|
|
49
|
+
readonly width: number;
|
|
50
|
+
readonly height: number;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const lensFor = (graph: SceneGraph, overlay: Overlay): Lens => {
|
|
54
|
+
const views = graph.views();
|
|
55
|
+
const view: HostedView | undefined =
|
|
56
|
+
overlay.view === null ? views[0] : views.find((candidate) => candidate.key === overlay.view);
|
|
57
|
+
if (view !== undefined) return { camera: view.camera, ...view.area };
|
|
58
|
+
const { width, height } = graph.viewport();
|
|
59
|
+
return { camera: graph.camera, x: 0, y: 0, width, height };
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** The overlay's point in world space: relative to its node, or as given. */
|
|
63
|
+
const worldPoint = (graph: SceneGraph, overlay: Overlay): Vector3 => {
|
|
64
|
+
point.fromArray(overlay.position);
|
|
65
|
+
const node: Object3D | undefined = overlay.node === null ? undefined : graph.object(overlay.node);
|
|
66
|
+
return node === undefined ? point : node.localToWorld(point);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** Drei's `isObjectBehindCamera`: more than 90° from where the camera looks. */
|
|
70
|
+
const behind = (camera: Camera, at: Vector3): boolean => {
|
|
71
|
+
cameraPosition.setFromMatrixPosition(camera.matrixWorld);
|
|
72
|
+
toPoint.copy(at).sub(cameraPosition);
|
|
73
|
+
return toPoint.angleTo(camera.getWorldDirection(direction)) > Math.PI / 2;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Drei's `isObjectVisible`: nothing in the scene is hit before the point. */
|
|
77
|
+
const unoccluded = (graph: SceneGraph, camera: Camera, at: Vector3): boolean => {
|
|
78
|
+
toPoint.copy(at).project(camera);
|
|
79
|
+
ndc.set(toPoint.x, toPoint.y);
|
|
80
|
+
raycaster.setFromCamera(ndc, camera);
|
|
81
|
+
const [nearest] = raycaster.intersectObjects(graph.scene.children, true);
|
|
82
|
+
return nearest === undefined || at.distanceTo(raycaster.ray.origin) < nearest.distance;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Drei's `objectScale`: one over the height of the view at the point's distance. */
|
|
86
|
+
const objectScale = (camera: Camera, at: Vector3): number => {
|
|
87
|
+
if (camera instanceof OrthographicCamera) return camera.zoom;
|
|
88
|
+
if (!(camera instanceof PerspectiveCamera)) return 1;
|
|
89
|
+
cameraPosition.setFromMatrixPosition(camera.matrixWorld);
|
|
90
|
+
const height = 2 * Math.tan((camera.fov * Math.PI) / 360) * at.distanceTo(cameraPosition);
|
|
91
|
+
return 1 / height;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** Drei's `objectZIndex`: linear in distance from near (`range[0]`) to far (`range[1]`). */
|
|
95
|
+
const zIndexOf = (camera: Camera, at: Vector3, range: readonly [number, number]): number => {
|
|
96
|
+
if (!(camera instanceof PerspectiveCamera || camera instanceof OrthographicCamera)) return 0;
|
|
97
|
+
cameraPosition.setFromMatrixPosition(camera.matrixWorld);
|
|
98
|
+
const slope = (range[1] - range[0]) / (camera.far - camera.near);
|
|
99
|
+
const offset = range[1] - slope * camera.far;
|
|
100
|
+
return Math.round(slope * at.distanceTo(cameraPosition) + offset);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const place = (graph: SceneGraph, overlay: Overlay): OverlayPlacement => {
|
|
104
|
+
const lens = lensFor(graph, overlay);
|
|
105
|
+
lens.camera.updateMatrixWorld();
|
|
106
|
+
const at = worldPoint(graph, overlay);
|
|
107
|
+
const visible =
|
|
108
|
+
!behind(lens.camera, at) && (!overlay.occlude || unoccluded(graph, lens.camera, at));
|
|
109
|
+
const scale =
|
|
110
|
+
overlay.distanceFactor === null ? 1 : objectScale(lens.camera, at) * overlay.distanceFactor;
|
|
111
|
+
const zIndex = zIndexOf(lens.camera, at, overlay.zIndexRange);
|
|
112
|
+
toPoint.copy(at).project(lens.camera);
|
|
113
|
+
return {
|
|
114
|
+
key: overlay.key,
|
|
115
|
+
x: lens.x + ((toPoint.x + 1) / 2) * lens.width,
|
|
116
|
+
y: lens.y + ((1 - toPoint.y) / 2) * lens.height,
|
|
117
|
+
scale,
|
|
118
|
+
zIndex,
|
|
119
|
+
visible,
|
|
120
|
+
center: overlay.center,
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** Places every overlay of the graph's scene, after its world matrices are current. */
|
|
125
|
+
export const placeOverlays = (graph: SceneGraph): ReadonlyArray<OverlayPlacement> => {
|
|
126
|
+
const overlays = graph.overlays();
|
|
127
|
+
if (overlays.length === 0) return [];
|
|
128
|
+
graph.scene.updateMatrixWorld();
|
|
129
|
+
return overlays.map((overlay) => place(graph, overlay));
|
|
130
|
+
};
|