@opsydyn/effect-three 0.3.0 → 0.6.4
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 +56 -0
- package/README.md +15 -1
- package/dist/{index-8cf8t6by.js → index-re3rrfwd.js} +2935 -104
- package/dist/index-re3rrfwd.js.map +56 -0
- package/dist/index-ttwy0znv.js +145 -0
- package/dist/index-ttwy0znv.js.map +12 -0
- package/dist/index.js +557 -147
- package/dist/index.js.map +8 -6
- package/dist/kernel/bodies.d.ts +20 -1
- package/dist/kernel/bodies.d.ts.map +1 -1
- package/dist/kernel/boxSelect.d.ts +22 -0
- package/dist/kernel/boxSelect.d.ts.map +1 -0
- package/dist/kernel/build.d.ts +165 -7
- package/dist/kernel/build.d.ts.map +1 -1
- package/dist/kernel/cameraShake.d.ts.map +1 -1
- package/dist/kernel/describeScene.d.ts +50 -0
- package/dist/kernel/describeScene.d.ts.map +1 -0
- package/dist/kernel/description.d.ts +223 -1
- package/dist/kernel/description.d.ts.map +1 -1
- package/dist/kernel/diff.d.ts +15 -0
- package/dist/kernel/diff.d.ts.map +1 -1
- package/dist/kernel/equality.d.ts +11 -1
- package/dist/kernel/equality.d.ts.map +1 -1
- package/dist/kernel/gizmo.d.ts +69 -0
- package/dist/kernel/gizmo.d.ts.map +1 -0
- package/dist/kernel/history.d.ts +61 -0
- package/dist/kernel/history.d.ts.map +1 -0
- package/dist/kernel/index.d.ts +19 -6
- package/dist/kernel/index.d.ts.map +1 -1
- package/dist/kernel/index.js +124 -3
- package/dist/kernel/index.js.map +1 -1
- package/dist/kernel/inspect.d.ts +85 -0
- package/dist/kernel/inspect.d.ts.map +1 -0
- package/dist/kernel/lineGeometry.d.ts +25 -0
- package/dist/kernel/lineGeometry.d.ts.map +1 -0
- package/dist/kernel/lockstep.d.ts +114 -0
- package/dist/kernel/lockstep.d.ts.map +1 -0
- package/dist/kernel/loop.d.ts.map +1 -1
- package/dist/kernel/materials.d.ts.map +1 -1
- package/dist/kernel/paintOrder.d.ts +53 -0
- package/dist/kernel/paintOrder.d.ts.map +1 -0
- package/dist/kernel/physics.d.ts +39 -0
- package/dist/kernel/physics.d.ts.map +1 -1
- package/dist/kernel/pivot.d.ts +54 -0
- package/dist/kernel/pivot.d.ts.map +1 -0
- package/dist/kernel/pointBuffers.d.ts +17 -0
- package/dist/kernel/pointBuffers.d.ts.map +1 -0
- package/dist/kernel/pointer.d.ts +31 -1
- package/dist/kernel/pointer.d.ts.map +1 -1
- package/dist/kernel/preview.d.ts +32 -0
- package/dist/kernel/preview.d.ts.map +1 -0
- package/dist/kernel/renderKit.d.ts +21 -1
- package/dist/kernel/renderKit.d.ts.map +1 -1
- package/dist/kernel/renderer.d.ts +6 -0
- package/dist/kernel/renderer.d.ts.map +1 -1
- package/dist/kernel/sceneChanges.d.ts +87 -0
- package/dist/kernel/sceneChanges.d.ts.map +1 -0
- package/dist/kernel/sceneGraph.d.ts +9 -1
- package/dist/kernel/sceneGraph.d.ts.map +1 -1
- package/dist/kernel/sceneIndex.d.ts +5 -0
- package/dist/kernel/sceneIndex.d.ts.map +1 -1
- package/dist/kernel/seeded.d.ts +9 -0
- package/dist/kernel/seeded.d.ts.map +1 -0
- package/dist/kernel/shots.d.ts +26 -0
- package/dist/kernel/shots.d.ts.map +1 -0
- package/dist/kernel/tape.d.ts +38 -0
- package/dist/kernel/tape.d.ts.map +1 -0
- package/dist/kernel/timeline.d.ts +60 -0
- package/dist/kernel/timeline.d.ts.map +1 -0
- package/dist/kernel/trail.d.ts +31 -0
- package/dist/kernel/trail.d.ts.map +1 -0
- package/dist/kernel/viewportGizmo.d.ts +25 -0
- package/dist/kernel/viewportGizmo.d.ts.map +1 -0
- package/dist/runtime/Inspector.d.ts +20 -0
- package/dist/runtime/Inspector.d.ts.map +1 -0
- package/dist/runtime/SceneFile.d.ts +462 -0
- package/dist/runtime/SceneFile.d.ts.map +1 -0
- package/dist/runtime/SceneSchema.d.ts +404 -0
- package/dist/runtime/SceneSchema.d.ts.map +1 -1
- package/dist/runtime/errors.d.ts +9 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/index.d.ts +3 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/webgl/index.d.ts.map +1 -1
- package/dist/webgl/index.js +228 -3
- package/dist/webgl/index.js.map +6 -4
- package/dist/webgl/line.d.ts +4 -0
- package/dist/webgl/line.d.ts.map +1 -0
- package/dist/webgl/points.d.ts +4 -0
- package/dist/webgl/points.d.ts.map +1 -0
- package/dist/webgpu/index.d.ts.map +1 -1
- package/dist/webgpu/index.js +206 -26
- package/dist/webgpu/index.js.map +6 -4
- package/dist/webgpu/line.d.ts +4 -0
- package/dist/webgpu/line.d.ts.map +1 -0
- package/dist/webgpu/points.d.ts +4 -0
- package/dist/webgpu/points.d.ts.map +1 -0
- package/package.json +4 -2
- package/src/kernel/bodies.ts +299 -17
- package/src/kernel/boxSelect.ts +73 -0
- package/src/kernel/build.ts +374 -1
- package/src/kernel/cameraShake.ts +3 -13
- package/src/kernel/describeScene.ts +224 -0
- package/src/kernel/description.ts +243 -0
- package/src/kernel/diff.ts +39 -0
- package/src/kernel/equality.ts +123 -0
- package/src/kernel/gizmo.ts +478 -0
- package/src/kernel/history.ts +118 -0
- package/src/kernel/index.ts +110 -1
- package/src/kernel/inspect.ts +214 -0
- package/src/kernel/lineGeometry.ts +75 -0
- package/src/kernel/lockstep.ts +237 -0
- package/src/kernel/loop.ts +7 -0
- package/src/kernel/materials.ts +3 -0
- package/src/kernel/paintOrder.ts +274 -0
- package/src/kernel/physics.ts +86 -1
- package/src/kernel/pivot.ts +418 -0
- package/src/kernel/pointBuffers.ts +115 -0
- package/src/kernel/pointer.ts +98 -17
- package/src/kernel/preview.ts +467 -0
- package/src/kernel/renderKit.ts +23 -1
- package/src/kernel/renderer.ts +6 -0
- package/src/kernel/sceneChanges.ts +265 -0
- package/src/kernel/sceneGraph.ts +354 -11
- package/src/kernel/sceneIndex.ts +8 -0
- package/src/kernel/seeded.ts +18 -0
- package/src/kernel/shots.ts +167 -0
- package/src/kernel/tape.ts +124 -0
- package/src/kernel/timeline.ts +93 -0
- package/src/kernel/trail.ts +170 -0
- package/src/kernel/viewportGizmo.ts +194 -0
- package/src/runtime/Inspector.ts +43 -0
- package/src/runtime/SceneFile.ts +46 -0
- package/src/runtime/SceneSchema.ts +319 -0
- package/src/runtime/errors.ts +6 -0
- package/src/runtime/index.ts +3 -1
- package/src/webgl/index.ts +4 -0
- package/src/webgl/line.ts +66 -0
- package/src/webgl/points.ts +205 -0
- package/src/webgpu/index.ts +4 -0
- package/src/webgpu/line.ts +67 -0
- package/src/webgpu/points.ts +193 -0
- package/dist/index-8cf8t6by.js.map +0 -41
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pivot controls as data, after Drei's `PivotControls` (`bf6f4ad`,
|
|
3
|
+
* `src/web/pivotControls/`): every handle at once, turned with the object and sized in
|
|
4
|
+
* scene units — an arrow along each axis, a square sliding in each plane, a quarter arc
|
|
5
|
+
* turning round each axis, and a sphere scaling along each. Drei's listen to the pointer
|
|
6
|
+
* and move a group themselves; here `pivotNodes` describes the handles from the Model, a
|
|
7
|
+
* press on one (`pressPivot`, with the point the press hit) and the drag rays after it
|
|
8
|
+
* (`dragPivot`) return the next placement, and the Model keeps both.
|
|
9
|
+
*
|
|
10
|
+
* The maths is Drei's: an arrow and a sphere follow the point on their axis nearest the
|
|
11
|
+
* ray (`calculateOffset`); a square and an arc follow where the ray's line meets their
|
|
12
|
+
* plane, a square by its offset along the plane's two axes, an arc by the angle turned
|
|
13
|
+
* about the object (`calculateAngle`); a sphere scales by 2 to the power of a fifth of
|
|
14
|
+
* the distance dragged, in handle sizes. Snapping is Drei's shift: ten degrees, and
|
|
15
|
+
* scales to a tenth.
|
|
16
|
+
*
|
|
17
|
+
* Placements are in the object's parent's space, which is taken to be the world, as for
|
|
18
|
+
* `gizmoNodes`.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
import { Euler, Quaternion, Vector3 } from 'three';
|
|
23
|
+
|
|
24
|
+
import { basic, cone, cylinder, fixed, group, mesh, plane, sphere } from './build.ts';
|
|
25
|
+
import type { SceneNode, Vec3 } from './description.ts';
|
|
26
|
+
import type { Placement } from './gizmo.ts';
|
|
27
|
+
import type { Ray } from './pointer.ts';
|
|
28
|
+
|
|
29
|
+
type Axis = 'x' | 'y' | 'z';
|
|
30
|
+
|
|
31
|
+
/** A handle: what it does, and the axis it is on (a square's and an arc's normal). */
|
|
32
|
+
export type PivotHandle = `${'arrow' | 'slider' | 'rotator' | 'sphere'}-${Axis}`;
|
|
33
|
+
|
|
34
|
+
/** A drag in progress: the handle, the placement it began at and the frame it moves in. */
|
|
35
|
+
export type PivotGrab = {
|
|
36
|
+
readonly handle: PivotHandle;
|
|
37
|
+
readonly start: Placement;
|
|
38
|
+
/** Where the press hit the handle. */
|
|
39
|
+
readonly point: Vec3;
|
|
40
|
+
/** The handle's directions in the world: its axis, or its plane's two and normal. */
|
|
41
|
+
readonly e1: Vec3;
|
|
42
|
+
readonly e2: Vec3;
|
|
43
|
+
readonly normal: Vec3;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type Pivot = {
|
|
47
|
+
/** The handles' size in scene units: an arrow's length (Drei's `scale`, 1). */
|
|
48
|
+
readonly scale: number;
|
|
49
|
+
/** Whether the object hides the handles behind it (Drei's `depthTest`, true). */
|
|
50
|
+
readonly depthTest: boolean;
|
|
51
|
+
/** Leave out kinds of handle, as Drei's `disable*`. */
|
|
52
|
+
readonly disableAxes: boolean;
|
|
53
|
+
readonly disableSliders: boolean;
|
|
54
|
+
readonly disableRotations: boolean;
|
|
55
|
+
readonly disableScaling: boolean;
|
|
56
|
+
/** The handle under the pointer, drawn in Drei's hovered yellow. */
|
|
57
|
+
readonly hovered: PivotHandle | null;
|
|
58
|
+
readonly grab: PivotGrab | null;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Pivot controls with Drei's defaults: size 1, depth tested, every handle shown. */
|
|
62
|
+
export const pivot = (options: Partial<Omit<Pivot, 'hovered' | 'grab'>> = {}): Pivot => ({
|
|
63
|
+
scale: options.scale ?? 1,
|
|
64
|
+
depthTest: options.depthTest ?? true,
|
|
65
|
+
disableAxes: options.disableAxes ?? false,
|
|
66
|
+
disableSliders: options.disableSliders ?? false,
|
|
67
|
+
disableRotations: options.disableRotations ?? false,
|
|
68
|
+
disableScaling: options.disableScaling ?? false,
|
|
69
|
+
hovered: null,
|
|
70
|
+
grab: null,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const unit: Record<Axis, Vector3> = {
|
|
74
|
+
x: new Vector3(1, 0, 0),
|
|
75
|
+
y: new Vector3(0, 1, 0),
|
|
76
|
+
z: new Vector3(0, 0, 1),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Each plane's two directions, by its normal, in Drei's order. */
|
|
80
|
+
const planeOf: Record<Axis, readonly [Axis, Axis]> = {
|
|
81
|
+
z: ['x', 'y'],
|
|
82
|
+
y: ['z', 'x'],
|
|
83
|
+
x: ['y', 'z'],
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const turnOf = (rotation: Vec3): Quaternion =>
|
|
87
|
+
new Quaternion().setFromEuler(new Euler(rotation[0], rotation[1], rotation[2]));
|
|
88
|
+
|
|
89
|
+
const toVec3 = (vector: Vector3): Vec3 => [vector.x, vector.y, vector.z];
|
|
90
|
+
|
|
91
|
+
const kindOf = (handle: PivotHandle) =>
|
|
92
|
+
handle.split('-')[0] as 'arrow' | 'slider' | 'rotator' | 'sphere';
|
|
93
|
+
const axisOf = (handle: PivotHandle): Axis => handle.split('-')[1] as Axis;
|
|
94
|
+
|
|
95
|
+
/** The handle's frame in the world: turned with the object, as Drei's group is. */
|
|
96
|
+
const frameOf = (handle: PivotHandle, placement: Placement) => {
|
|
97
|
+
const turn = turnOf(placement.rotation);
|
|
98
|
+
const axis = axisOf(handle);
|
|
99
|
+
const [first, second] = planeOf[axis];
|
|
100
|
+
const kind = kindOf(handle);
|
|
101
|
+
const flat = kind === 'slider' || kind === 'rotator';
|
|
102
|
+
const e1 = (flat ? unit[first] : unit[axis]).clone().applyQuaternion(turn);
|
|
103
|
+
const e2 = (flat ? unit[second] : unit[axis]).clone().applyQuaternion(turn);
|
|
104
|
+
return { e1, e2, normal: e1.clone().cross(e2).normalize() };
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** A press on `handle` at `point`, with the object at `placement`: takes hold. */
|
|
108
|
+
export const pressPivot = (
|
|
109
|
+
state: Pivot,
|
|
110
|
+
placement: Placement,
|
|
111
|
+
handle: PivotHandle,
|
|
112
|
+
point: Vec3,
|
|
113
|
+
): Pivot => {
|
|
114
|
+
const { e1, e2, normal } = frameOf(handle, placement);
|
|
115
|
+
return {
|
|
116
|
+
...state,
|
|
117
|
+
grab: {
|
|
118
|
+
handle,
|
|
119
|
+
start: placement,
|
|
120
|
+
point,
|
|
121
|
+
e1: toVec3(e1),
|
|
122
|
+
e2: toVec3(e2),
|
|
123
|
+
normal: toVec3(kindOf(handle) === 'arrow' || kindOf(handle) === 'sphere' ? e1 : normal),
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export const releasePivot = (state: Pivot): Pivot => ({ ...state, grab: null });
|
|
129
|
+
|
|
130
|
+
export const hoverPivot = (state: Pivot, handle: PivotHandle | null): Pivot =>
|
|
131
|
+
state.hovered === handle ? state : { ...state, hovered: handle };
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Drei's `calculateOffset`: how far along `direction` from `point` the line nearest the
|
|
135
|
+
* ray is, solving for the closest points of the two lines.
|
|
136
|
+
*/
|
|
137
|
+
const offsetAlong = (point: Vector3, direction: Vector3, ray: Ray): number => {
|
|
138
|
+
const origin = new Vector3(...ray.origin);
|
|
139
|
+
const toward = new Vector3(...ray.direction);
|
|
140
|
+
const e1 = direction.dot(direction);
|
|
141
|
+
const e2 = direction.dot(point) - direction.dot(origin);
|
|
142
|
+
const e3 = direction.dot(toward);
|
|
143
|
+
if (e3 === 0) return -e2 / e1;
|
|
144
|
+
const v1 = toward
|
|
145
|
+
.clone()
|
|
146
|
+
.multiplyScalar(e1 / e3)
|
|
147
|
+
.sub(direction);
|
|
148
|
+
const v2 = toward
|
|
149
|
+
.clone()
|
|
150
|
+
.multiplyScalar(e2 / e3)
|
|
151
|
+
.add(origin)
|
|
152
|
+
.sub(point);
|
|
153
|
+
return -v1.dot(v2) / v1.dot(v1);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** Where the ray's line meets the plane through `at` with `normal`, if it does. */
|
|
157
|
+
const meetPlane = (ray: Ray, at: Vector3, normal: Vector3): Vector3 | undefined => {
|
|
158
|
+
const origin = new Vector3(...ray.origin);
|
|
159
|
+
const toward = new Vector3(...ray.direction);
|
|
160
|
+
const facing = normal.dot(toward);
|
|
161
|
+
if (facing === 0) return undefined;
|
|
162
|
+
return origin.addScaledVector(toward, normal.dot(at.clone().sub(origin)) / facing);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/** Drei's `calculateAngle`: from the press to `to`, round `at`, in the plane of e1 and e2. */
|
|
166
|
+
const angleBetween = (from: Vector3, to: Vector3, at: Vector3, e1: Vector3, e2: Vector3) => {
|
|
167
|
+
const a = from.clone().sub(at);
|
|
168
|
+
const b = to.clone().sub(at);
|
|
169
|
+
return Math.atan2(b.dot(e2), b.dot(e1)) - Math.atan2(a.dot(e2), a.dot(e1));
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const vectorsOf = (grab: PivotGrab) => ({
|
|
173
|
+
point: new Vector3(...grab.point),
|
|
174
|
+
e1: new Vector3(...grab.e1),
|
|
175
|
+
e2: new Vector3(...grab.e2),
|
|
176
|
+
normal: new Vector3(...grab.normal),
|
|
177
|
+
at: new Vector3(...grab.start.position),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const moved = (start: Placement, by: Vector3): Placement => ({
|
|
181
|
+
...start,
|
|
182
|
+
position: toVec3(new Vector3(...start.position).add(by)),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const arrowDrag = (grab: PivotGrab, ray: Ray): Placement => {
|
|
186
|
+
const { point, e1 } = vectorsOf(grab);
|
|
187
|
+
return moved(grab.start, e1.clone().multiplyScalar(offsetAlong(point, e1, ray)));
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const sliderDrag = (grab: PivotGrab, ray: Ray): Placement => {
|
|
191
|
+
const { point, e1, e2, normal, at } = vectorsOf(grab);
|
|
192
|
+
const meet = meetPlane(ray, at, normal);
|
|
193
|
+
if (meet === undefined) return grab.start;
|
|
194
|
+
const offset = meet.sub(point);
|
|
195
|
+
return moved(
|
|
196
|
+
grab.start,
|
|
197
|
+
e1.multiplyScalar(offset.dot(e1)).add(e2.multiplyScalar(offset.dot(e2))),
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const tenDegrees = Math.PI / 18;
|
|
202
|
+
|
|
203
|
+
const rotatorDrag = (grab: PivotGrab, ray: Ray, snap: boolean): Placement => {
|
|
204
|
+
const { point, e1, e2, normal, at } = vectorsOf(grab);
|
|
205
|
+
const meet = meetPlane(ray, at, normal);
|
|
206
|
+
if (meet === undefined) return grab.start;
|
|
207
|
+
const turned = angleBetween(point, meet, at, e1, e2);
|
|
208
|
+
const angle = snap ? Math.round(turned / tenDegrees) * tenDegrees : turned;
|
|
209
|
+
const turn = new Quaternion()
|
|
210
|
+
.setFromAxisAngle(normal, angle)
|
|
211
|
+
.multiply(turnOf(grab.start.rotation));
|
|
212
|
+
const euler = new Euler().setFromQuaternion(turn.normalize());
|
|
213
|
+
return { ...grab.start, rotation: [euler.x, euler.y, euler.z] };
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const sphereDrag = (state: Pivot, grab: PivotGrab, ray: Ray, snap: boolean): Placement => {
|
|
217
|
+
const { point, e1 } = vectorsOf(grab);
|
|
218
|
+
const grown = 2 ** ((offsetAlong(point, e1, ray) / state.scale) * 0.2);
|
|
219
|
+
// Drei never lets a scale fall below 1e-5, which would break the object's matrix.
|
|
220
|
+
const factor = Math.max(snap ? Math.round(grown * 10) / 10 : grown, 1e-5);
|
|
221
|
+
const index = { x: 0, y: 1, z: 2 }[axisOf(grab.handle)];
|
|
222
|
+
const scale: Vec3 = [
|
|
223
|
+
grab.start.scale[0] * (index === 0 ? factor : 1),
|
|
224
|
+
grab.start.scale[1] * (index === 1 ? factor : 1),
|
|
225
|
+
grab.start.scale[2] * (index === 2 ? factor : 1),
|
|
226
|
+
];
|
|
227
|
+
return { ...grab.start, scale };
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The placement a drag ray gives, or `undefined` while nothing is held. `snap` rounds
|
|
232
|
+
* turns to ten degrees and scales to tenths, as Drei's shift does.
|
|
233
|
+
*/
|
|
234
|
+
export const dragPivot = (
|
|
235
|
+
state: Pivot,
|
|
236
|
+
ray: Ray,
|
|
237
|
+
options: { readonly snap?: boolean } = {},
|
|
238
|
+
): Placement | undefined => {
|
|
239
|
+
const grab = state.grab;
|
|
240
|
+
if (grab === null) return undefined;
|
|
241
|
+
const snap = options.snap ?? false;
|
|
242
|
+
const kind = kindOf(grab.handle);
|
|
243
|
+
if (kind === 'arrow') return arrowDrag(grab, ray);
|
|
244
|
+
if (kind === 'slider') return sliderDrag(grab, ray);
|
|
245
|
+
if (kind === 'rotator') return rotatorDrag(grab, ray, snap);
|
|
246
|
+
return sphereDrag(state, grab, ray, snap);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// DRAWING
|
|
250
|
+
|
|
251
|
+
/** Drei's `axisColors` and `hoveredColor`. */
|
|
252
|
+
const colours: Record<Axis, number> = { x: 0xff2060, y: 0x20df80, z: 0x2080ff };
|
|
253
|
+
const hoveredColour = 0xffff40;
|
|
254
|
+
|
|
255
|
+
const handles = ['arrow', 'slider', 'rotator', 'sphere'] as const;
|
|
256
|
+
|
|
257
|
+
/** The handle a hit names, if it is one of this pivot's. */
|
|
258
|
+
export const pivotHandleOf = (key: string, hit: string): PivotHandle | undefined => {
|
|
259
|
+
const [owner, name] = hit.split(':');
|
|
260
|
+
if (owner !== key) return undefined;
|
|
261
|
+
return handles
|
|
262
|
+
.flatMap((kind) => (['x', 'y', 'z'] as const).map((axis): PivotHandle => `${kind}-${axis}`))
|
|
263
|
+
.find((handle) => handle === name);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const lookOf = (state: Pivot, handle: PivotHandle) =>
|
|
267
|
+
basic({
|
|
268
|
+
color:
|
|
269
|
+
state.grab?.handle === handle || state.hovered === handle
|
|
270
|
+
? hoveredColour
|
|
271
|
+
: colours[axisOf(handle)],
|
|
272
|
+
depthTest: state.depthTest,
|
|
273
|
+
side: 'double',
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* What a hit shape is drawn with: nothing to see, never hiding anything, still picked
|
|
278
|
+
* (Drei's arrows carry an invisible cylinder the same way).
|
|
279
|
+
*/
|
|
280
|
+
const unseen = basic({ opacity: 0, transparent: true, depthTest: false });
|
|
281
|
+
|
|
282
|
+
/** Turns +y onto each axis, for the arrows, stems and spheres, which are drawn along y. */
|
|
283
|
+
const pointAlong: Record<Axis, Vec3> = {
|
|
284
|
+
x: [0, 0, -Math.PI / 2],
|
|
285
|
+
y: [0, 0, 0],
|
|
286
|
+
z: [Math.PI / 2, 0, 0],
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const along = (axis: Axis, distance: number): Vec3 => {
|
|
290
|
+
const { x, y, z } = unit[axis];
|
|
291
|
+
return [x * distance, y * distance, z * distance];
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/** Drei's `AxisArrow`: a stem of 4/5 of the size and a cone of 1/5, 1/20 wide. */
|
|
295
|
+
const arrowNodes = (key: string, state: Pivot, axis: Axis): ReadonlyArray<SceneNode> => {
|
|
296
|
+
const handle: PivotHandle = `arrow-${axis}`;
|
|
297
|
+
const size = state.scale;
|
|
298
|
+
const coneLength = size / 5;
|
|
299
|
+
const stem = size - coneLength;
|
|
300
|
+
const material = lookOf(state, handle);
|
|
301
|
+
return [
|
|
302
|
+
mesh({
|
|
303
|
+
key: `${key}:${handle}:stem`,
|
|
304
|
+
position: along(axis, stem / 2),
|
|
305
|
+
rotation: fixed(pointAlong[axis]),
|
|
306
|
+
geometry: cylinder({ radiusTop: size / 80, radiusBottom: size / 80, height: stem }),
|
|
307
|
+
material,
|
|
308
|
+
}),
|
|
309
|
+
// Drei's hit cylinder: 1.4 cone widths round, the arrow's whole length.
|
|
310
|
+
mesh({
|
|
311
|
+
key: `${key}:${handle}:hit`,
|
|
312
|
+
position: along(axis, size / 2),
|
|
313
|
+
rotation: fixed(pointAlong[axis]),
|
|
314
|
+
geometry: cylinder({ radiusTop: size * 0.07, radiusBottom: size * 0.07, height: size }),
|
|
315
|
+
material: unseen,
|
|
316
|
+
interactive: true,
|
|
317
|
+
}),
|
|
318
|
+
mesh({
|
|
319
|
+
key: `${key}:${handle}:cone`,
|
|
320
|
+
position: along(axis, stem + coneLength / 2),
|
|
321
|
+
rotation: fixed(pointAlong[axis]),
|
|
322
|
+
geometry: cone({ radius: size / 20, height: coneLength, radialSegments: 24 }),
|
|
323
|
+
material,
|
|
324
|
+
interactive: true,
|
|
325
|
+
}),
|
|
326
|
+
];
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
/** A rotation onto the plane of an axis's slider and arc: x onto its first, y its second. */
|
|
330
|
+
const ontoPlane: Record<Axis, Vec3> = {
|
|
331
|
+
z: [0, 0, 0],
|
|
332
|
+
y: [-Math.PI / 2, 0, -Math.PI / 2],
|
|
333
|
+
x: [Math.PI / 2, Math.PI / 2, 0],
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/** Drei's `PlaneSlider`: a square 0.225 of the size, its middle 1.7/7 out along both. */
|
|
337
|
+
const sliderNode = (key: string, state: Pivot, axis: Axis): SceneNode => {
|
|
338
|
+
const handle: PivotHandle = `slider-${axis}`;
|
|
339
|
+
const out = (state.scale / 7) * 1.7;
|
|
340
|
+
const side = state.scale * 0.225;
|
|
341
|
+
return group({
|
|
342
|
+
key: `${key}:${handle}`,
|
|
343
|
+
rotation: fixed(ontoPlane[axis]),
|
|
344
|
+
children: [
|
|
345
|
+
mesh({
|
|
346
|
+
key: `${key}:${handle}:square`,
|
|
347
|
+
position: [out, out, 0],
|
|
348
|
+
geometry: plane({ width: side, height: side }),
|
|
349
|
+
material: lookOf(state, handle),
|
|
350
|
+
interactive: true,
|
|
351
|
+
}),
|
|
352
|
+
],
|
|
353
|
+
});
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const arcSegments = 8;
|
|
357
|
+
|
|
358
|
+
/** Drei's `AxisRotator`: a quarter circle of radius 0.65 of the size, from its first to its second. */
|
|
359
|
+
const rotatorNode = (key: string, state: Pivot, axis: Axis): SceneNode => {
|
|
360
|
+
const handle: PivotHandle = `rotator-${axis}`;
|
|
361
|
+
const radius = state.scale * 0.65;
|
|
362
|
+
const step = Math.PI / 2 / arcSegments;
|
|
363
|
+
const chord = 2 * radius * Math.sin(step / 2);
|
|
364
|
+
// A cylinder lies along y: each turned to the arc's tangent at its middle. The thin one
|
|
365
|
+
// shows; a thick, unseen one round it is what the pointer finds, as Drei's line is
|
|
366
|
+
// picked a few pixels wide.
|
|
367
|
+
const segment = (index: number, part: 'line' | 'hit'): SceneNode => {
|
|
368
|
+
const middle = step * (index + 0.5);
|
|
369
|
+
const thickness = part === 'line' ? radius / 50 : state.scale * 0.03;
|
|
370
|
+
return mesh({
|
|
371
|
+
key: `${key}:${handle}:${part}:${index}`,
|
|
372
|
+
position: [Math.cos(middle) * radius, Math.sin(middle) * radius, 0],
|
|
373
|
+
rotation: fixed([0, 0, middle]),
|
|
374
|
+
geometry: cylinder({ radiusTop: thickness, radiusBottom: thickness, height: chord }),
|
|
375
|
+
material: part === 'line' ? lookOf(state, handle) : unseen,
|
|
376
|
+
interactive: part === 'hit',
|
|
377
|
+
});
|
|
378
|
+
};
|
|
379
|
+
const indices = Array.from({ length: arcSegments }, (_, index) => index);
|
|
380
|
+
return group({
|
|
381
|
+
key: `${key}:${handle}`,
|
|
382
|
+
rotation: fixed(ontoPlane[axis]),
|
|
383
|
+
children: [
|
|
384
|
+
...indices.map((index) => segment(index, 'line')),
|
|
385
|
+
...indices.map((index) => segment(index, 'hit')),
|
|
386
|
+
],
|
|
387
|
+
});
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
/** Drei's `ScalingSphere`: a ball 1/22.5 of the size, 1.2 sizes out along its axis. */
|
|
391
|
+
const sphereNode = (key: string, state: Pivot, axis: Axis): SceneNode =>
|
|
392
|
+
mesh({
|
|
393
|
+
key: `${key}:sphere-${axis}`,
|
|
394
|
+
position: along(axis, state.scale * 1.2),
|
|
395
|
+
geometry: sphere({ radius: state.scale / 22.5, widthSegments: 12, heightSegments: 12 }),
|
|
396
|
+
material: lookOf(state, `sphere-${axis}`),
|
|
397
|
+
interactive: true,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const axesOf: ReadonlyArray<Axis> = ['x', 'y', 'z'];
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* The handles for `placement`: a group at the object, turned with it (and not scaled),
|
|
404
|
+
* its handles keyed `${key}:arrow-x`, `${key}:slider-z`, `${key}:rotator-y`,
|
|
405
|
+
* `${key}:sphere-x` and so on, each pickable.
|
|
406
|
+
*/
|
|
407
|
+
export const pivotNodes = (key: string, state: Pivot, placement: Placement): SceneNode =>
|
|
408
|
+
group({
|
|
409
|
+
key,
|
|
410
|
+
position: placement.position,
|
|
411
|
+
rotation: fixed(placement.rotation),
|
|
412
|
+
children: [
|
|
413
|
+
...(state.disableAxes ? [] : axesOf.flatMap((axis) => arrowNodes(key, state, axis))),
|
|
414
|
+
...(state.disableSliders ? [] : axesOf.map((axis) => sliderNode(key, state, axis))),
|
|
415
|
+
...(state.disableRotations ? [] : axesOf.map((axis) => rotatorNode(key, state, axis))),
|
|
416
|
+
...(state.disableScaling ? [] : axesOf.map((axis) => sphereNode(key, state, axis))),
|
|
417
|
+
],
|
|
418
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The buffers a render kit draws points from, made from their look: given points as
|
|
3
|
+
* they are, Drei's sparkles and stars scattered as Drei scatters them (`Sparkles.tsx`,
|
|
4
|
+
* `Stars.tsx` at `bf6f4ad`) but from a seed, so the same look gives the same buffers.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import { Color, Spherical, Vector3 } from 'three';
|
|
9
|
+
|
|
10
|
+
import type { PointLook } from './description.ts';
|
|
11
|
+
import { seededRandom } from './seeded.ts';
|
|
12
|
+
|
|
13
|
+
/** Per-point attributes: three numbers a point for positions, colours and noise. */
|
|
14
|
+
export type PointBuffers = {
|
|
15
|
+
readonly count: number;
|
|
16
|
+
readonly positions: Float32Array;
|
|
17
|
+
readonly colors: Float32Array;
|
|
18
|
+
readonly sizes: Float32Array;
|
|
19
|
+
/** Sparkles only: each mote's drift speed, opacity and noise. */
|
|
20
|
+
readonly speeds: Float32Array;
|
|
21
|
+
readonly opacities: Float32Array;
|
|
22
|
+
readonly noise: Float32Array;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const linear = new Color();
|
|
26
|
+
|
|
27
|
+
const dots = (look: Extract<PointLook, { readonly _tag: 'Dots' }>): PointBuffers => {
|
|
28
|
+
const count = look.positions.length;
|
|
29
|
+
const colors = new Float32Array(count * 3);
|
|
30
|
+
for (let point = 0; point < count; point++) {
|
|
31
|
+
linear.setHex(look.colors?.[point] ?? look.color).toArray(colors, point * 3);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
count,
|
|
35
|
+
positions: Float32Array.from(look.positions.flat()),
|
|
36
|
+
colors,
|
|
37
|
+
sizes: Float32Array.from({ length: count }, (_, point) => look.sizes?.[point] ?? 1),
|
|
38
|
+
speeds: new Float32Array(0),
|
|
39
|
+
opacities: new Float32Array(0),
|
|
40
|
+
noise: new Float32Array(0),
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Drei's `Sparkles`: positions spread through the box, random sizes unless given. */
|
|
45
|
+
const sparkles = (look: Extract<PointLook, { readonly _tag: 'Sparkles' }>): PointBuffers => {
|
|
46
|
+
const random = seededRandom(look.seed);
|
|
47
|
+
// three's `MathUtils.randFloatSpread`, as Drei spreads them, from the seed.
|
|
48
|
+
const spread = (range: number) => range * (0.5 - random());
|
|
49
|
+
const { count } = look;
|
|
50
|
+
const positions = new Float32Array(count * 3);
|
|
51
|
+
for (let point = 0; point < count; point++) {
|
|
52
|
+
positions[point * 3] = spread(look.scale[0]);
|
|
53
|
+
positions[point * 3 + 1] = spread(look.scale[1]);
|
|
54
|
+
positions[point * 3 + 2] = spread(look.scale[2]);
|
|
55
|
+
}
|
|
56
|
+
const colors = new Float32Array(count * 3);
|
|
57
|
+
linear.setHex(look.color);
|
|
58
|
+
for (let point = 0; point < count; point++) linear.toArray(colors, point * 3);
|
|
59
|
+
const noise = new Float32Array(count * 3);
|
|
60
|
+
for (let point = 0; point < count; point++) noise.set(look.noise, point * 3);
|
|
61
|
+
return {
|
|
62
|
+
count,
|
|
63
|
+
positions,
|
|
64
|
+
colors,
|
|
65
|
+
sizes: Float32Array.from({ length: count }, () => look.size ?? random()),
|
|
66
|
+
speeds: new Float32Array(count).fill(look.speed),
|
|
67
|
+
opacities: new Float32Array(count).fill(look.opacity),
|
|
68
|
+
noise,
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Drei's `Stars`: a shell of stars, nearer ones later, hues round the colour wheel. */
|
|
73
|
+
const stars = (look: Extract<PointLook, { readonly _tag: 'Stars' }>): PointBuffers => {
|
|
74
|
+
const random = seededRandom(look.seed);
|
|
75
|
+
const { count } = look;
|
|
76
|
+
const positions = new Float32Array(count * 3);
|
|
77
|
+
const colors = new Float32Array(count * 3);
|
|
78
|
+
const sizes = Float32Array.from({ length: count }, () => (0.5 + 0.5 * random()) * look.factor);
|
|
79
|
+
const place = new Vector3();
|
|
80
|
+
const spherical = new Spherical();
|
|
81
|
+
const increment = look.depth / count;
|
|
82
|
+
let radius = look.radius + look.depth;
|
|
83
|
+
for (let star = 0; star < count; star++) {
|
|
84
|
+
radius -= increment * random();
|
|
85
|
+
place.setFromSpherical(
|
|
86
|
+
spherical.set(radius, Math.acos(1 - random() * 2), random() * 2 * Math.PI),
|
|
87
|
+
);
|
|
88
|
+
place.toArray(positions, star * 3);
|
|
89
|
+
linear.setHSL(star / count, look.saturation, 0.9).toArray(colors, star * 3);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
count,
|
|
93
|
+
positions,
|
|
94
|
+
colors,
|
|
95
|
+
sizes,
|
|
96
|
+
speeds: new Float32Array(0),
|
|
97
|
+
opacities: new Float32Array(0),
|
|
98
|
+
noise: new Float32Array(0),
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** The buffers for a look. */
|
|
103
|
+
export const pointBuffers = (look: PointLook): PointBuffers => {
|
|
104
|
+
switch (look._tag) {
|
|
105
|
+
case 'Dots':
|
|
106
|
+
return dots(look);
|
|
107
|
+
case 'Sparkles':
|
|
108
|
+
return sparkles(look);
|
|
109
|
+
case 'Stars':
|
|
110
|
+
return stars(look);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** Whether a look moves on its own: sparkles drift and stars twinkle in scene time. */
|
|
115
|
+
export const animatedLook = (look: PointLook): boolean => look._tag !== 'Dots';
|