@opsydyn/effect-three 0.4.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 +41 -0
- package/README.md +4 -2
- package/dist/{index-h3vf50fn.js → index-re3rrfwd.js} +880 -79
- package/dist/index-re3rrfwd.js.map +56 -0
- package/dist/index.js +71 -2
- package/dist/index.js.map +3 -3
- package/dist/kernel/bodies.d.ts +20 -1
- package/dist/kernel/bodies.d.ts.map +1 -1
- package/dist/kernel/build.d.ts +24 -2
- package/dist/kernel/build.d.ts.map +1 -1
- package/dist/kernel/description.d.ts +68 -0
- package/dist/kernel/description.d.ts.map +1 -1
- package/dist/kernel/equality.d.ts +3 -1
- package/dist/kernel/equality.d.ts.map +1 -1
- package/dist/kernel/index.d.ts +7 -5
- package/dist/kernel/index.d.ts.map +1 -1
- package/dist/kernel/index.js +20 -2
- package/dist/kernel/index.js.map +1 -1
- package/dist/kernel/loop.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/pointer.d.ts +19 -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/sceneGraph.d.ts +3 -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/timeline.d.ts +60 -0
- package/dist/kernel/timeline.d.ts.map +1 -0
- package/dist/runtime/SceneFile.d.ts +14 -0
- package/dist/runtime/SceneFile.d.ts.map +1 -1
- package/dist/runtime/SceneSchema.d.ts +86 -0
- package/dist/runtime/SceneSchema.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/kernel/bodies.ts +299 -17
- package/src/kernel/build.ts +42 -1
- package/src/kernel/description.ts +68 -0
- package/src/kernel/equality.ts +22 -0
- package/src/kernel/index.ts +13 -1
- package/src/kernel/loop.ts +2 -0
- package/src/kernel/paintOrder.ts +274 -0
- package/src/kernel/physics.ts +86 -1
- package/src/kernel/pointer.ts +43 -5
- package/src/kernel/preview.ts +467 -0
- package/src/kernel/sceneGraph.ts +35 -3
- package/src/kernel/sceneIndex.ts +8 -0
- package/src/kernel/timeline.ts +93 -0
- package/src/runtime/SceneSchema.ts +72 -0
- package/dist/index-h3vf50fn.js.map +0 -53
package/src/kernel/index.ts
CHANGED
|
@@ -48,6 +48,10 @@ export {
|
|
|
48
48
|
xrMesh,
|
|
49
49
|
xrMeshGeometryId,
|
|
50
50
|
physics,
|
|
51
|
+
modelAsset,
|
|
52
|
+
textureAsset,
|
|
53
|
+
environmentAsset,
|
|
54
|
+
fontAsset,
|
|
51
55
|
cuboidCollider,
|
|
52
56
|
ballCollider,
|
|
53
57
|
capsuleCollider,
|
|
@@ -149,6 +153,7 @@ export {
|
|
|
149
153
|
overlaysEqual,
|
|
150
154
|
xrEquals,
|
|
151
155
|
physicsEquals,
|
|
156
|
+
assetsEqual,
|
|
152
157
|
skyEquals,
|
|
153
158
|
viewsEqual,
|
|
154
159
|
vec3Equals,
|
|
@@ -207,8 +212,12 @@ export {
|
|
|
207
212
|
type PhysicsJoint,
|
|
208
213
|
type PhysicsWorld,
|
|
209
214
|
type Quat,
|
|
215
|
+
type Received,
|
|
216
|
+
type RestoredWorld,
|
|
217
|
+
snapshotBytes,
|
|
218
|
+
snapshotText,
|
|
210
219
|
} from './physics.ts';
|
|
211
|
-
export type { BodyCollision, BodyContactForce } from './bodies.ts';
|
|
220
|
+
export type { BodyCollision, BodyContactForce, PhysicsReached } from './bodies.ts';
|
|
212
221
|
export type {
|
|
213
222
|
ContactShadowPasses,
|
|
214
223
|
HostedLine,
|
|
@@ -255,6 +264,7 @@ export {
|
|
|
255
264
|
type Picker,
|
|
256
265
|
type PlaneEquation,
|
|
257
266
|
pointOnPlane,
|
|
267
|
+
type PointerDevice,
|
|
258
268
|
type PointerInput,
|
|
259
269
|
type PointerTracker,
|
|
260
270
|
type Ray,
|
|
@@ -282,6 +292,7 @@ export {
|
|
|
282
292
|
type SceneGraph,
|
|
283
293
|
type SceneGraphOptions,
|
|
284
294
|
} from './sceneGraph.ts';
|
|
295
|
+
export { type PreviewOptions, type PreviewResult, previewSvg } from './preview.ts';
|
|
285
296
|
export {
|
|
286
297
|
type DescriptionError,
|
|
287
298
|
emptyIndex,
|
|
@@ -310,6 +321,7 @@ export {
|
|
|
310
321
|
export { type BoxCorner, boxSelect, type CanvasSize, type Selectable } from './boxSelect.ts';
|
|
311
322
|
export * as History from './history.ts';
|
|
312
323
|
export * as Lockstep from './lockstep.ts';
|
|
324
|
+
export * as Timeline from './timeline.ts';
|
|
313
325
|
export {
|
|
314
326
|
describeScene,
|
|
315
327
|
formatSummary,
|
package/src/kernel/loop.ts
CHANGED
|
@@ -203,6 +203,8 @@ export const createSceneLoop = (options: SceneLoopOptions): SceneLoop => {
|
|
|
203
203
|
for (const event of tracker.frame()) emit(event);
|
|
204
204
|
for (const collision of graph.collisions()) emit({ _tag: 'Collision', ...collision });
|
|
205
205
|
for (const force of graph.contactForces()) emit({ _tag: 'ContactForce', ...force });
|
|
206
|
+
// After the collisions of the steps that led there, so a Model has them all first.
|
|
207
|
+
for (const reached of graph.physicsReached()) emit({ _tag: 'PhysicsReached', ...reached });
|
|
206
208
|
// Settings change only through reconcile, which replaces the value: compare by reference.
|
|
207
209
|
const settings = graph.renderSettings();
|
|
208
210
|
if (settings !== configured) {
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The order to paint a still in, far to near from one eye, exactly: a binary space
|
|
3
|
+
* partition (Fuchs, Kedem and Naylor, 1980) of what is drawn, where sorting by depth
|
|
4
|
+
* would paint a floor over what stands on it. It partitions objects rather than faces: an
|
|
5
|
+
* object none of whose faces can hide another (any convex solid, its back faces culled)
|
|
6
|
+
* is painted as it is, and is told which side of a plane it lies by its bounding sphere
|
|
7
|
+
* before its faces are looked at, so a scene of many objects stays quick. Splitting
|
|
8
|
+
* planes are chosen to cut few objects and to share them out evenly.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { Sphere, Vector3 } from 'three';
|
|
13
|
+
|
|
14
|
+
/** A flat convex polygon in the world, and what it is painted as. */
|
|
15
|
+
export type Facet<T> = {
|
|
16
|
+
readonly _tag: 'Facet';
|
|
17
|
+
readonly points: ReadonlyArray<Vector3>;
|
|
18
|
+
readonly item: T;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Something without area (a line, a point) at a place in the world. */
|
|
22
|
+
export type Mark<T> = { readonly _tag: 'Mark'; readonly at: Vector3; readonly item: T };
|
|
23
|
+
|
|
24
|
+
/** What one object draws, and whether its facets may be painted in any order. */
|
|
25
|
+
export type Unit<T> = {
|
|
26
|
+
readonly facets: ReadonlyArray<Facet<T>>;
|
|
27
|
+
readonly marks: ReadonlyArray<Mark<T>>;
|
|
28
|
+
readonly bounds: Sphere;
|
|
29
|
+
readonly selfOrdered: boolean;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** A plane, `normal · p = constant`. */
|
|
33
|
+
export type Plane = { readonly normal: Vector3; readonly constant: number };
|
|
34
|
+
|
|
35
|
+
/** How far from a plane, in world units a unit from the origin, a point counts as on it. */
|
|
36
|
+
const onPlane = 1e-6;
|
|
37
|
+
|
|
38
|
+
/** How many facets are weighed as each partition's plane. */
|
|
39
|
+
const candidates = 8;
|
|
40
|
+
|
|
41
|
+
/** How much worse a cut object is than an object on the fuller side. */
|
|
42
|
+
const splitCost = 8;
|
|
43
|
+
|
|
44
|
+
const planeOf = (points: ReadonlyArray<Vector3>): Plane | undefined => {
|
|
45
|
+
const [a, b, c] = points;
|
|
46
|
+
if (a === undefined || b === undefined || c === undefined) return undefined;
|
|
47
|
+
const normal = new Vector3().subVectors(b, a).cross(new Vector3().subVectors(c, a));
|
|
48
|
+
if (normal.lengthSq() === 0) return undefined;
|
|
49
|
+
normal.normalize();
|
|
50
|
+
return { normal, constant: normal.dot(a) };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const side = (plane: Plane, point: Vector3): number => plane.normal.dot(point) - plane.constant;
|
|
54
|
+
|
|
55
|
+
const toleranceAt = (points: ReadonlyArray<Vector3>): number =>
|
|
56
|
+
onPlane * Math.max(1, ...points.map((point) => point.length()));
|
|
57
|
+
|
|
58
|
+
type Parts<T> = {
|
|
59
|
+
readonly front?: Facet<T>;
|
|
60
|
+
readonly back?: Facet<T>;
|
|
61
|
+
readonly on?: Facet<T>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** A facet split by a plane: what lies in front of it, behind it, and in it. */
|
|
65
|
+
export const split = <T>(plane: Plane, facet: Facet<T>): Parts<T> => {
|
|
66
|
+
const sides = facet.points.map((point) => side(plane, point));
|
|
67
|
+
const tolerance = toleranceAt(facet.points);
|
|
68
|
+
if (sides.every((s) => Math.abs(s) <= tolerance)) return { on: facet };
|
|
69
|
+
if (sides.every((s) => s >= -tolerance)) return { front: facet };
|
|
70
|
+
if (sides.every((s) => s <= tolerance)) return { back: facet };
|
|
71
|
+
const front: Array<Vector3> = [];
|
|
72
|
+
const back: Array<Vector3> = [];
|
|
73
|
+
facet.points.forEach((point, i) => {
|
|
74
|
+
const next = facet.points[(i + 1) % facet.points.length] ?? point;
|
|
75
|
+
const here = sides[i] ?? 0;
|
|
76
|
+
const there = sides[(i + 1) % sides.length] ?? 0;
|
|
77
|
+
if (here >= -tolerance) front.push(point);
|
|
78
|
+
if (here <= tolerance) back.push(point);
|
|
79
|
+
if ((here > tolerance && there < -tolerance) || (here < -tolerance && there > tolerance)) {
|
|
80
|
+
const crossing = point.clone().lerp(next, here / (here - there));
|
|
81
|
+
front.push(crossing);
|
|
82
|
+
back.push(crossing.clone());
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
...(front.length >= 3 ? { front: { ...facet, points: front } } : {}),
|
|
87
|
+
...(back.length >= 3 ? { back: { ...facet, points: back } } : {}),
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Whether the eye is in front of `facet` and all of `facets` lie on or behind its plane. */
|
|
92
|
+
function hidesNone<T>(facet: Facet<T>, facets: ReadonlyArray<Facet<T>>, eye: Vector3): boolean {
|
|
93
|
+
const plane = planeOf(facet.points);
|
|
94
|
+
if (plane === undefined) return true;
|
|
95
|
+
const tolerance = toleranceAt(facet.points);
|
|
96
|
+
return (
|
|
97
|
+
side(plane, eye) > tolerance &&
|
|
98
|
+
facets.every(
|
|
99
|
+
(other) => other === facet || other.points.every((point) => side(plane, point) <= tolerance),
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Gathers what one object draws. Its facets may be painted in any order when the eye is
|
|
106
|
+
* in front of each and every other lies on or behind each one's plane: then none comes
|
|
107
|
+
* between the eye and another.
|
|
108
|
+
*/
|
|
109
|
+
export const unitOf = <T>(
|
|
110
|
+
facets: ReadonlyArray<Facet<T>>,
|
|
111
|
+
marks: ReadonlyArray<Mark<T>>,
|
|
112
|
+
eye: Vector3,
|
|
113
|
+
): Unit<T> => {
|
|
114
|
+
const points = [...facets.flatMap((facet) => facet.points), ...marks.map((mark) => mark.at)];
|
|
115
|
+
const bounds = new Sphere().setFromPoints(points);
|
|
116
|
+
const selfOrdered = facets.every((facet) => hidesNone(facet, facets, eye));
|
|
117
|
+
return { facets, marks, bounds, selfOrdered };
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
type Partition<T> = {
|
|
121
|
+
readonly front: ReadonlyArray<Unit<T>>;
|
|
122
|
+
readonly back: ReadonlyArray<Unit<T>>;
|
|
123
|
+
readonly on: ReadonlyArray<Facet<T>>;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const sizeOf = <T>(unit: Unit<T>): number => unit.facets.length + unit.marks.length;
|
|
127
|
+
|
|
128
|
+
/** Which side of `plane` a unit lies, by its bounds: `0` when they cross it. */
|
|
129
|
+
const sideOf = <T>(plane: Plane, unit: Unit<T>): number => {
|
|
130
|
+
const distance = side(plane, unit.bounds.center);
|
|
131
|
+
const reach = unit.bounds.radius * (1 + onPlane) + onPlane;
|
|
132
|
+
return distance > reach ? 1 : distance < -reach ? -1 : 0;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** Units cut in two by `plane`, and how many facets and marks fall either side. */
|
|
136
|
+
const weigh = <T>(plane: Plane, units: ReadonlyArray<Unit<T>>): number => {
|
|
137
|
+
let cut = 0;
|
|
138
|
+
let front = 0;
|
|
139
|
+
let back = 0;
|
|
140
|
+
for (const unit of units) {
|
|
141
|
+
const whole = sideOf(plane, unit);
|
|
142
|
+
if (whole > 0) front += sizeOf(unit);
|
|
143
|
+
else if (whole < 0) back += sizeOf(unit);
|
|
144
|
+
else {
|
|
145
|
+
const parts = unit.facets.map((facet) => split(plane, facet));
|
|
146
|
+
const ahead = parts.filter((part) => part.front !== undefined).length;
|
|
147
|
+
const behind = parts.filter((part) => part.back !== undefined).length;
|
|
148
|
+
if (ahead > 0 && behind > 0) cut++;
|
|
149
|
+
front += ahead;
|
|
150
|
+
back += behind;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return splitCost * cut + Math.abs(front - back);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const partition = <T>(plane: Plane, units: ReadonlyArray<Unit<T>>): Partition<T> => {
|
|
157
|
+
const front: Array<Unit<T>> = [];
|
|
158
|
+
const back: Array<Unit<T>> = [];
|
|
159
|
+
const on: Array<Facet<T>> = [];
|
|
160
|
+
for (const unit of units) {
|
|
161
|
+
const whole = sideOf(plane, unit);
|
|
162
|
+
if (whole > 0) front.push(unit);
|
|
163
|
+
else if (whole < 0) back.push(unit);
|
|
164
|
+
else {
|
|
165
|
+
const ahead: Array<Facet<T>> = [];
|
|
166
|
+
const behind: Array<Facet<T>> = [];
|
|
167
|
+
for (const facet of unit.facets) {
|
|
168
|
+
const parts = split(plane, facet);
|
|
169
|
+
if (parts.front !== undefined) ahead.push(parts.front);
|
|
170
|
+
if (parts.back !== undefined) behind.push(parts.back);
|
|
171
|
+
if (parts.on !== undefined) on.push(parts.on);
|
|
172
|
+
}
|
|
173
|
+
const marksAhead = unit.marks.filter((mark) => side(plane, mark.at) >= 0);
|
|
174
|
+
const marksBehind = unit.marks.filter((mark) => side(plane, mark.at) < 0);
|
|
175
|
+
// A part of an object is bounded by the whole, and keeps its order.
|
|
176
|
+
if (ahead.length + marksAhead.length > 0)
|
|
177
|
+
front.push({ ...unit, facets: ahead, marks: marksAhead });
|
|
178
|
+
if (behind.length + marksBehind.length > 0)
|
|
179
|
+
back.push({ ...unit, facets: behind, marks: marksBehind });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { front, back, on };
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/** The plane of one of a few facets spread over the units: the one that weighs least. */
|
|
186
|
+
const planeFor = <T>(units: ReadonlyArray<Unit<T>>): Plane | undefined => {
|
|
187
|
+
const withFacets = units.filter((unit) => unit.facets.length > 0);
|
|
188
|
+
const count = Math.min(candidates, withFacets.length);
|
|
189
|
+
let best: Plane | undefined;
|
|
190
|
+
let lightest = Number.POSITIVE_INFINITY;
|
|
191
|
+
for (let i = 0; i < count; i++) {
|
|
192
|
+
const unit = withFacets[Math.floor((i * withFacets.length) / count)];
|
|
193
|
+
const facet = unit?.facets[Math.floor((i * (unit?.facets.length ?? 0)) / count)];
|
|
194
|
+
const plane = facet === undefined ? undefined : planeOf(facet.points);
|
|
195
|
+
if (plane === undefined) continue;
|
|
196
|
+
const weight = weigh(plane, units);
|
|
197
|
+
if (weight < lightest) {
|
|
198
|
+
lightest = weight;
|
|
199
|
+
best = plane;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (best !== undefined) return best;
|
|
203
|
+
// Every one weighed has no area: any facet that has one.
|
|
204
|
+
for (const unit of withFacets) {
|
|
205
|
+
for (const facet of unit.facets) {
|
|
206
|
+
const plane = planeOf(facet.points);
|
|
207
|
+
if (plane !== undefined) return plane;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const distanceFrom =
|
|
214
|
+
(eye: Vector3) =>
|
|
215
|
+
<T>(mark: Mark<T>): number =>
|
|
216
|
+
mark.at.distanceToSquared(eye);
|
|
217
|
+
|
|
218
|
+
/** Each facet of a unit as a unit of its own, and each mark: for a unit that is not self-ordered. */
|
|
219
|
+
const apart = <T>(unit: Unit<T>): ReadonlyArray<Unit<T>> => [
|
|
220
|
+
...unit.facets.map((facet) => ({
|
|
221
|
+
facets: [facet],
|
|
222
|
+
marks: [],
|
|
223
|
+
bounds: new Sphere().setFromPoints([...facet.points]),
|
|
224
|
+
selfOrdered: true,
|
|
225
|
+
})),
|
|
226
|
+
...unit.marks.map((mark) => ({
|
|
227
|
+
facets: [],
|
|
228
|
+
marks: [mark],
|
|
229
|
+
bounds: new Sphere(mark.at.clone(), 0),
|
|
230
|
+
selfOrdered: true,
|
|
231
|
+
})),
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
/** Appends what `units` draw to `into`, farthest from `eye` first. */
|
|
235
|
+
const paint = <T>(
|
|
236
|
+
units: ReadonlyArray<Unit<T>>,
|
|
237
|
+
eye: Vector3,
|
|
238
|
+
into: Array<Facet<T> | Mark<T>>,
|
|
239
|
+
): void => {
|
|
240
|
+
const [only] = units;
|
|
241
|
+
if (only !== undefined && units.length === 1) {
|
|
242
|
+
if (only.selfOrdered && only.marks.length === 0) {
|
|
243
|
+
into.push(...only.facets);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (only.facets.length === 0) {
|
|
247
|
+
into.push(...only.marks.toSorted((a, b) => distanceFrom(eye)(b) - distanceFrom(eye)(a)));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
paint(apart(only), eye, into);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const plane = planeFor(units);
|
|
254
|
+
if (plane === undefined) {
|
|
255
|
+
const marks = units.flatMap((unit) => unit.marks);
|
|
256
|
+
into.push(...marks.toSorted((a, b) => distanceFrom(eye)(b) - distanceFrom(eye)(a)));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const { front, back, on } = partition(plane, units);
|
|
260
|
+
const facing = side(plane, eye) >= 0;
|
|
261
|
+
paint(facing ? back : front, eye, into);
|
|
262
|
+
into.push(...on);
|
|
263
|
+
paint(facing ? front : back, eye, into);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/** What `units` draw, in the order to paint it seen from `eye`: farthest first. */
|
|
267
|
+
export const paintOrder = <T>(
|
|
268
|
+
units: ReadonlyArray<Unit<T>>,
|
|
269
|
+
eye: Vector3,
|
|
270
|
+
): ReadonlyArray<Facet<T> | Mark<T>> => {
|
|
271
|
+
const into: Array<Facet<T> | Mark<T>> = [];
|
|
272
|
+
paint(units, eye, into);
|
|
273
|
+
return into;
|
|
274
|
+
};
|
package/src/kernel/physics.ts
CHANGED
|
@@ -145,10 +145,95 @@ export type PhysicsWorld = {
|
|
|
145
145
|
/** Whether any dynamic body is still moving (not asleep). */
|
|
146
146
|
readonly awake: () => boolean;
|
|
147
147
|
readonly dispose: () => void;
|
|
148
|
+
/**
|
|
149
|
+
* For a world that does what it is asked elsewhere (in a worker, ADR-017): the
|
|
150
|
+
* collisions that have come back since the last call, and whether anything it was asked
|
|
151
|
+
* is still to come back. Until it has, `pose`, `contactForces` and `debugLines` are as
|
|
152
|
+
* of what has, and `step` returns no collisions. The scene graph takes it every frame,
|
|
153
|
+
* and reports a world on `until` as there only once nothing is pending, so its checksum
|
|
154
|
+
* is of the engine's own poses. Absent for a world that does what it is asked at once.
|
|
155
|
+
*/
|
|
156
|
+
readonly received?: () => Received;
|
|
157
|
+
/**
|
|
158
|
+
* The world as it is between steps, for `PhysicsEngine.restoreWorld` to make again:
|
|
159
|
+
* every body, collider and joint, and whatever the engine carries from step to step.
|
|
160
|
+
* A world working elsewhere gives `undefined` until it has the snapshot it asked for
|
|
161
|
+
* (`received` is pending meanwhile). Absent for an engine that cannot.
|
|
162
|
+
*/
|
|
163
|
+
readonly snapshot?: () => Uint8Array | undefined;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/** What a world stepping elsewhere has sent back (`PhysicsWorld.received`). */
|
|
167
|
+
export type Received = {
|
|
168
|
+
readonly collisions: ReadonlyArray<Collision>;
|
|
169
|
+
readonly pending: boolean;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* A world made from a snapshot, and what it holds already: the ids of its bodies and of
|
|
174
|
+
* its joints, which the scene graph takes on rather than adding again.
|
|
175
|
+
*/
|
|
176
|
+
export type RestoredWorld = {
|
|
177
|
+
readonly world: PhysicsWorld;
|
|
178
|
+
readonly bodies: ReadonlyArray<string>;
|
|
179
|
+
readonly joints: ReadonlyArray<string>;
|
|
148
180
|
};
|
|
149
181
|
|
|
150
182
|
/** Makes physics worlds. Provide one to the scene graph (in Foldkit, the environment). */
|
|
151
|
-
export type PhysicsEngine = {
|
|
183
|
+
export type PhysicsEngine = {
|
|
184
|
+
readonly createWorld: (physics: Physics) => PhysicsWorld;
|
|
185
|
+
/**
|
|
186
|
+
* Makes a world from a snapshot one of this engine's worlds took (`snapshot`), as it
|
|
187
|
+
* was then, for `physics({ from })`. Absent for an engine that cannot.
|
|
188
|
+
*/
|
|
189
|
+
readonly restoreWorld?: (physics: Physics, snapshot: Uint8Array) => RestoredWorld;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/** `count` characters from `first` on. */
|
|
193
|
+
const run = (first: string, count: number): ReadonlyArray<string> =>
|
|
194
|
+
Array.from({ length: count }, (_, i) => String.fromCharCode(first.charCodeAt(0) + i));
|
|
195
|
+
|
|
196
|
+
/** Base64's digits, in order. */
|
|
197
|
+
const digits: ReadonlyArray<string> = [...run('A', 26), ...run('a', 26), ...run('0', 10), '+', '/'];
|
|
198
|
+
const digitValues = new Map(digits.map((char, i) => [char, i]));
|
|
199
|
+
const digit = (value: number): string => digits[value] ?? '';
|
|
200
|
+
|
|
201
|
+
/** A snapshot as text (base64), as `PhysicsReached` carries it and a message can. */
|
|
202
|
+
export const snapshotText = (bytes: Uint8Array): string => {
|
|
203
|
+
const out: Array<string> = [];
|
|
204
|
+
for (let at = 0; at < bytes.length; at += 3) {
|
|
205
|
+
const a = bytes[at] ?? 0;
|
|
206
|
+
const b = bytes[at + 1] ?? 0;
|
|
207
|
+
const c = bytes[at + 2] ?? 0;
|
|
208
|
+
const left = bytes.length - at;
|
|
209
|
+
out.push(
|
|
210
|
+
digit(a >> 2),
|
|
211
|
+
digit(((a & 3) << 4) | (b >> 4)),
|
|
212
|
+
left > 1 ? digit(((b & 15) << 2) | (c >> 6)) : '=',
|
|
213
|
+
left > 2 ? digit(c & 63) : '=',
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return out.join('');
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/** The bytes of a snapshot written by `snapshotText`. */
|
|
220
|
+
export const snapshotBytes = (text: string): Uint8Array => {
|
|
221
|
+
const digits = text.replace(/=+$/, '');
|
|
222
|
+
const bytes = new Uint8Array(Math.floor((digits.length * 3) / 4));
|
|
223
|
+
let bits = 0;
|
|
224
|
+
let held = 0;
|
|
225
|
+
let at = 0;
|
|
226
|
+
for (const char of digits) {
|
|
227
|
+
bits = (bits << 6) | (digitValues.get(char) ?? 0);
|
|
228
|
+
held += 6;
|
|
229
|
+
if (held >= 8) {
|
|
230
|
+
held -= 8;
|
|
231
|
+
bytes[at] = (bits >> held) & 255;
|
|
232
|
+
at += 1;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return bytes;
|
|
236
|
+
};
|
|
152
237
|
|
|
153
238
|
const toVec3 = (vector: Vector3): Vec3 => [vector.x, vector.y, vector.z];
|
|
154
239
|
|
package/src/kernel/pointer.ts
CHANGED
|
@@ -26,10 +26,15 @@ export type Modifiers = {
|
|
|
26
26
|
readonly alt: boolean;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
/** What pressed: `PointerEvent.pointerType`. A finger is less steady than a mouse or pen. */
|
|
30
|
+
export type PointerDevice = 'mouse' | 'pen' | 'touch';
|
|
31
|
+
|
|
29
32
|
/**
|
|
30
33
|
* Pointer input in normalised device coordinates. `button` follows `MouseEvent.button`.
|
|
31
34
|
* `pointer` is the input's `PointerEvent.pointerId` (0 when absent): with two pointers
|
|
32
|
-
* pressed (two fingers), the second only takes part in pinches.
|
|
35
|
+
* pressed (two fingers), the second only takes part in pinches. A press's `device` is
|
|
36
|
+
* what pressed (a mouse when absent); it decides how far a press may travel and still be
|
|
37
|
+
* a click on nothing.
|
|
33
38
|
*/
|
|
34
39
|
export type PointerInput =
|
|
35
40
|
| {
|
|
@@ -45,6 +50,7 @@ export type PointerInput =
|
|
|
45
50
|
readonly button: number;
|
|
46
51
|
readonly pointer?: number;
|
|
47
52
|
readonly modifiers?: Modifiers;
|
|
53
|
+
readonly device?: PointerDevice;
|
|
48
54
|
}
|
|
49
55
|
| {
|
|
50
56
|
readonly _tag: 'PointerUp';
|
|
@@ -285,6 +291,19 @@ export type SceneEvent =
|
|
|
285
291
|
readonly maxForceDirection: Vec3;
|
|
286
292
|
readonly maxForceMagnitude: number;
|
|
287
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* A world on `physics({ until })` reached that step and waits there: its run, its
|
|
296
|
+
* step count, and a checksum of every body's pose, which peers running the same inputs
|
|
297
|
+
* share (ADR-014). It comes after the collisions of the steps that led there. At
|
|
298
|
+
* `physics({ snapshotAt })` it carries a snapshot of the world, to start another `from`.
|
|
299
|
+
*/
|
|
300
|
+
| {
|
|
301
|
+
readonly _tag: 'PhysicsReached';
|
|
302
|
+
readonly run: number;
|
|
303
|
+
readonly step: number;
|
|
304
|
+
readonly checksum: string;
|
|
305
|
+
readonly snapshot?: string;
|
|
306
|
+
}
|
|
288
307
|
/**
|
|
289
308
|
* A measured label's laid-out size changed (`scene({ measure })`, after its glyphs are
|
|
290
309
|
* laid out): in its own units, before its scale, for sizing what goes behind it, as
|
|
@@ -530,14 +549,24 @@ export type PointerTracker = {
|
|
|
530
549
|
|
|
531
550
|
const none: ReadonlyArray<SceneEvent> = [];
|
|
532
551
|
|
|
533
|
-
/**
|
|
534
|
-
|
|
552
|
+
/**
|
|
553
|
+
* How far, in CSS pixels, a press on nothing may travel and still be a miss rather than a
|
|
554
|
+
* drag. React Three Fiber allows 2 (`events.ts`, `delta <= 2`, at pinned `d74622e`), but
|
|
555
|
+
* measures it on the browser's `click`, which a browser only synthesises from a tap that
|
|
556
|
+
* kept within its own, larger touch slop. This kernel works from pointer down and up, so
|
|
557
|
+
* it gives a finger its own allowance: a steady tap wanders a few pixels, a drag far more.
|
|
558
|
+
*/
|
|
559
|
+
const missTolerances = { mouse: 2, pen: 2, touch: 10 } as const satisfies Record<
|
|
560
|
+
PointerDevice,
|
|
561
|
+
number
|
|
562
|
+
>;
|
|
535
563
|
|
|
536
564
|
type Press = {
|
|
537
565
|
readonly key: string | undefined;
|
|
538
566
|
readonly button: number;
|
|
539
567
|
readonly x: number;
|
|
540
568
|
readonly y: number;
|
|
569
|
+
readonly device: PointerDevice;
|
|
541
570
|
};
|
|
542
571
|
|
|
543
572
|
/** The press a gesture began with: its button, modifiers, mesh and region. */
|
|
@@ -599,7 +628,10 @@ export const createPointerTracker = (picker: Picker): PointerTracker => {
|
|
|
599
628
|
}
|
|
600
629
|
return;
|
|
601
630
|
}
|
|
602
|
-
if (
|
|
631
|
+
if (
|
|
632
|
+
from?.button === event.button &&
|
|
633
|
+
travel(from, event.x, event.y) <= missTolerances[from.device]
|
|
634
|
+
) {
|
|
603
635
|
events.push({ _tag: 'PointerMissed', button: event.button });
|
|
604
636
|
}
|
|
605
637
|
};
|
|
@@ -781,7 +813,13 @@ export const createPointerTracker = (picker: Picker): PointerTracker => {
|
|
|
781
813
|
moveGesture(id, event, events);
|
|
782
814
|
break;
|
|
783
815
|
case 'PointerDown':
|
|
784
|
-
press = {
|
|
816
|
+
press = {
|
|
817
|
+
key: hit?.key,
|
|
818
|
+
button: event.button,
|
|
819
|
+
x: event.x,
|
|
820
|
+
y: event.y,
|
|
821
|
+
device: event.device ?? 'mouse',
|
|
822
|
+
};
|
|
785
823
|
pressing = true;
|
|
786
824
|
dragged = false;
|
|
787
825
|
if (hit !== undefined) events.push({ _tag: 'PointerDown', button: event.button, ...hit });
|