@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.
Files changed (143) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +15 -1
  3. package/dist/{index-8cf8t6by.js → index-re3rrfwd.js} +2935 -104
  4. package/dist/index-re3rrfwd.js.map +56 -0
  5. package/dist/index-ttwy0znv.js +145 -0
  6. package/dist/index-ttwy0znv.js.map +12 -0
  7. package/dist/index.js +557 -147
  8. package/dist/index.js.map +8 -6
  9. package/dist/kernel/bodies.d.ts +20 -1
  10. package/dist/kernel/bodies.d.ts.map +1 -1
  11. package/dist/kernel/boxSelect.d.ts +22 -0
  12. package/dist/kernel/boxSelect.d.ts.map +1 -0
  13. package/dist/kernel/build.d.ts +165 -7
  14. package/dist/kernel/build.d.ts.map +1 -1
  15. package/dist/kernel/cameraShake.d.ts.map +1 -1
  16. package/dist/kernel/describeScene.d.ts +50 -0
  17. package/dist/kernel/describeScene.d.ts.map +1 -0
  18. package/dist/kernel/description.d.ts +223 -1
  19. package/dist/kernel/description.d.ts.map +1 -1
  20. package/dist/kernel/diff.d.ts +15 -0
  21. package/dist/kernel/diff.d.ts.map +1 -1
  22. package/dist/kernel/equality.d.ts +11 -1
  23. package/dist/kernel/equality.d.ts.map +1 -1
  24. package/dist/kernel/gizmo.d.ts +69 -0
  25. package/dist/kernel/gizmo.d.ts.map +1 -0
  26. package/dist/kernel/history.d.ts +61 -0
  27. package/dist/kernel/history.d.ts.map +1 -0
  28. package/dist/kernel/index.d.ts +19 -6
  29. package/dist/kernel/index.d.ts.map +1 -1
  30. package/dist/kernel/index.js +124 -3
  31. package/dist/kernel/index.js.map +1 -1
  32. package/dist/kernel/inspect.d.ts +85 -0
  33. package/dist/kernel/inspect.d.ts.map +1 -0
  34. package/dist/kernel/lineGeometry.d.ts +25 -0
  35. package/dist/kernel/lineGeometry.d.ts.map +1 -0
  36. package/dist/kernel/lockstep.d.ts +114 -0
  37. package/dist/kernel/lockstep.d.ts.map +1 -0
  38. package/dist/kernel/loop.d.ts.map +1 -1
  39. package/dist/kernel/materials.d.ts.map +1 -1
  40. package/dist/kernel/paintOrder.d.ts +53 -0
  41. package/dist/kernel/paintOrder.d.ts.map +1 -0
  42. package/dist/kernel/physics.d.ts +39 -0
  43. package/dist/kernel/physics.d.ts.map +1 -1
  44. package/dist/kernel/pivot.d.ts +54 -0
  45. package/dist/kernel/pivot.d.ts.map +1 -0
  46. package/dist/kernel/pointBuffers.d.ts +17 -0
  47. package/dist/kernel/pointBuffers.d.ts.map +1 -0
  48. package/dist/kernel/pointer.d.ts +31 -1
  49. package/dist/kernel/pointer.d.ts.map +1 -1
  50. package/dist/kernel/preview.d.ts +32 -0
  51. package/dist/kernel/preview.d.ts.map +1 -0
  52. package/dist/kernel/renderKit.d.ts +21 -1
  53. package/dist/kernel/renderKit.d.ts.map +1 -1
  54. package/dist/kernel/renderer.d.ts +6 -0
  55. package/dist/kernel/renderer.d.ts.map +1 -1
  56. package/dist/kernel/sceneChanges.d.ts +87 -0
  57. package/dist/kernel/sceneChanges.d.ts.map +1 -0
  58. package/dist/kernel/sceneGraph.d.ts +9 -1
  59. package/dist/kernel/sceneGraph.d.ts.map +1 -1
  60. package/dist/kernel/sceneIndex.d.ts +5 -0
  61. package/dist/kernel/sceneIndex.d.ts.map +1 -1
  62. package/dist/kernel/seeded.d.ts +9 -0
  63. package/dist/kernel/seeded.d.ts.map +1 -0
  64. package/dist/kernel/shots.d.ts +26 -0
  65. package/dist/kernel/shots.d.ts.map +1 -0
  66. package/dist/kernel/tape.d.ts +38 -0
  67. package/dist/kernel/tape.d.ts.map +1 -0
  68. package/dist/kernel/timeline.d.ts +60 -0
  69. package/dist/kernel/timeline.d.ts.map +1 -0
  70. package/dist/kernel/trail.d.ts +31 -0
  71. package/dist/kernel/trail.d.ts.map +1 -0
  72. package/dist/kernel/viewportGizmo.d.ts +25 -0
  73. package/dist/kernel/viewportGizmo.d.ts.map +1 -0
  74. package/dist/runtime/Inspector.d.ts +20 -0
  75. package/dist/runtime/Inspector.d.ts.map +1 -0
  76. package/dist/runtime/SceneFile.d.ts +462 -0
  77. package/dist/runtime/SceneFile.d.ts.map +1 -0
  78. package/dist/runtime/SceneSchema.d.ts +404 -0
  79. package/dist/runtime/SceneSchema.d.ts.map +1 -1
  80. package/dist/runtime/errors.d.ts +9 -0
  81. package/dist/runtime/errors.d.ts.map +1 -1
  82. package/dist/runtime/index.d.ts +3 -1
  83. package/dist/runtime/index.d.ts.map +1 -1
  84. package/dist/webgl/index.d.ts.map +1 -1
  85. package/dist/webgl/index.js +228 -3
  86. package/dist/webgl/index.js.map +6 -4
  87. package/dist/webgl/line.d.ts +4 -0
  88. package/dist/webgl/line.d.ts.map +1 -0
  89. package/dist/webgl/points.d.ts +4 -0
  90. package/dist/webgl/points.d.ts.map +1 -0
  91. package/dist/webgpu/index.d.ts.map +1 -1
  92. package/dist/webgpu/index.js +206 -26
  93. package/dist/webgpu/index.js.map +6 -4
  94. package/dist/webgpu/line.d.ts +4 -0
  95. package/dist/webgpu/line.d.ts.map +1 -0
  96. package/dist/webgpu/points.d.ts +4 -0
  97. package/dist/webgpu/points.d.ts.map +1 -0
  98. package/package.json +4 -2
  99. package/src/kernel/bodies.ts +299 -17
  100. package/src/kernel/boxSelect.ts +73 -0
  101. package/src/kernel/build.ts +374 -1
  102. package/src/kernel/cameraShake.ts +3 -13
  103. package/src/kernel/describeScene.ts +224 -0
  104. package/src/kernel/description.ts +243 -0
  105. package/src/kernel/diff.ts +39 -0
  106. package/src/kernel/equality.ts +123 -0
  107. package/src/kernel/gizmo.ts +478 -0
  108. package/src/kernel/history.ts +118 -0
  109. package/src/kernel/index.ts +110 -1
  110. package/src/kernel/inspect.ts +214 -0
  111. package/src/kernel/lineGeometry.ts +75 -0
  112. package/src/kernel/lockstep.ts +237 -0
  113. package/src/kernel/loop.ts +7 -0
  114. package/src/kernel/materials.ts +3 -0
  115. package/src/kernel/paintOrder.ts +274 -0
  116. package/src/kernel/physics.ts +86 -1
  117. package/src/kernel/pivot.ts +418 -0
  118. package/src/kernel/pointBuffers.ts +115 -0
  119. package/src/kernel/pointer.ts +98 -17
  120. package/src/kernel/preview.ts +467 -0
  121. package/src/kernel/renderKit.ts +23 -1
  122. package/src/kernel/renderer.ts +6 -0
  123. package/src/kernel/sceneChanges.ts +265 -0
  124. package/src/kernel/sceneGraph.ts +354 -11
  125. package/src/kernel/sceneIndex.ts +8 -0
  126. package/src/kernel/seeded.ts +18 -0
  127. package/src/kernel/shots.ts +167 -0
  128. package/src/kernel/tape.ts +124 -0
  129. package/src/kernel/timeline.ts +93 -0
  130. package/src/kernel/trail.ts +170 -0
  131. package/src/kernel/viewportGizmo.ts +194 -0
  132. package/src/runtime/Inspector.ts +43 -0
  133. package/src/runtime/SceneFile.ts +46 -0
  134. package/src/runtime/SceneSchema.ts +319 -0
  135. package/src/runtime/errors.ts +6 -0
  136. package/src/runtime/index.ts +3 -1
  137. package/src/webgl/index.ts +4 -0
  138. package/src/webgl/line.ts +66 -0
  139. package/src/webgl/points.ts +205 -0
  140. package/src/webgpu/index.ts +4 -0
  141. package/src/webgpu/line.ts +67 -0
  142. package/src/webgpu/points.ts +193 -0
  143. package/dist/index-8cf8t6by.js.map +0 -41
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Lockstep, as data (roadmap, "Beyond parity" 2): peers that run the same `update` stay
3
+ * in the same state by applying the same inputs in the same order. Time is cut into
4
+ * turns; every member sends one packet for each turn (empty when it did nothing), and a
5
+ * turn applies only once every member's packet for it has arrived, its inputs in peer
6
+ * order. Any transport that delivers each packet to every peer will do: a
7
+ * `BroadcastChannel` between tabs, a socket relay between machines.
8
+ *
9
+ * Membership changes in step too. The leader (the member whose id sorts first) admits
10
+ * a newcomer with an entry in its own packet; when that turn applies, everywhere at
11
+ * once, the newcomer becomes a member `lead + 1` turns later, and the leader sends it
12
+ * the state as it is then (`welcomeDue`), from which it carries on. A member that leaves
13
+ * says which turn it sent last (`leave`); one that goes quiet is dropped by the leader
14
+ * from the turn everyone is waiting on, which no one can have applied.
15
+ *
16
+ * Pure functions over a value, like the rest of the kernel: an app keeps the `Lockstep`
17
+ * in its Model, sends what `send` gives it, passes what arrives to `receive`, and
18
+ * applies what `advance` returns. Use it as a namespace: `Lockstep.make(self)`.
19
+ *
20
+ * @module
21
+ */
22
+
23
+ /** What a turn carries: an app's input, or the leader admitting a newcomer. */
24
+ export type Entry<I> =
25
+ | { readonly _tag: 'Input'; readonly input: I }
26
+ | { readonly _tag: 'Admit'; readonly peer: string };
27
+
28
+ /** One member's packet for one turn. */
29
+ export type Packet<I> = {
30
+ readonly peer: string;
31
+ readonly turn: number;
32
+ readonly entries: ReadonlyArray<Entry<I>>;
33
+ };
34
+
35
+ /** A member: the first turn it sends for, and the turn it stopped before, once it has. */
36
+ export type Member = {
37
+ readonly peer: string;
38
+ readonly from: number;
39
+ readonly until: number | null;
40
+ };
41
+
42
+ export type Lockstep<I> = {
43
+ readonly self: string;
44
+ readonly members: ReadonlyArray<Member>;
45
+ /** The next turn to apply. */
46
+ readonly turn: number;
47
+ /** The next turn this peer sends for. */
48
+ readonly sent: number;
49
+ /** How many turns ahead of the applied one a peer may send: inputs wait this long. */
50
+ readonly lead: number;
51
+ /** Entries waiting for this peer's next packet. */
52
+ readonly pending: ReadonlyArray<Entry<I>>;
53
+ /** Packets for turns not yet applied, this peer's own among them. */
54
+ readonly received: ReadonlyArray<Packet<I>>;
55
+ };
56
+
57
+ /** An input applied: the turn, and the peer it came from. */
58
+ export type Applied<I> = { readonly turn: number; readonly peer: string; readonly input: I };
59
+
60
+ const sortedPeers = (members: ReadonlyArray<Member>): ReadonlyArray<Member> =>
61
+ members.toSorted((a, b) => (a.peer < b.peer ? -1 : a.peer > b.peer ? 1 : 0));
62
+
63
+ const memberAt = (member: Member, turn: number): boolean =>
64
+ member.from <= turn && (member.until === null || turn < member.until);
65
+
66
+ /** The members a turn waits for, in the order their inputs apply. */
67
+ export const membersAt = <I>(state: Lockstep<I>, turn: number): ReadonlyArray<string> =>
68
+ sortedPeers(state.members.filter((member) => memberAt(member, turn))).map(
69
+ (member) => member.peer,
70
+ );
71
+
72
+ /** The member who admits and drops others at `turn`: the one whose id sorts first. */
73
+ export const leaderAt = <I>(state: Lockstep<I>, turn: number): string | undefined =>
74
+ membersAt(state, turn)[0];
75
+
76
+ /** A session of one: this peer alone, from turn 0. */
77
+ export const make = <I>(self: string, lead = 2): Lockstep<I> => ({
78
+ self,
79
+ members: [{ peer: self, from: 0, until: null }],
80
+ turn: 0,
81
+ sent: 0,
82
+ lead,
83
+ pending: [],
84
+ received: [],
85
+ });
86
+
87
+ /**
88
+ * A newcomer's session, from the leader's welcome: the members and the turn it starts
89
+ * at, with whatever packets it heard while it waited.
90
+ */
91
+ export const join = <I>(
92
+ self: string,
93
+ welcome: {
94
+ readonly turn: number;
95
+ readonly members: ReadonlyArray<Member>;
96
+ readonly lead: number;
97
+ },
98
+ heard: ReadonlyArray<Packet<I>> = [],
99
+ ): Lockstep<I> => ({
100
+ self,
101
+ members: welcome.members,
102
+ turn: welcome.turn,
103
+ sent: welcome.turn,
104
+ lead: welcome.lead,
105
+ pending: [],
106
+ received: heard.filter((packet) => packet.turn >= welcome.turn),
107
+ });
108
+
109
+ /** `input` waits for this peer's next packet. */
110
+ export const submit = <I>(state: Lockstep<I>, input: I): Lockstep<I> => ({
111
+ ...state,
112
+ pending: [...state.pending, { _tag: 'Input', input }],
113
+ });
114
+
115
+ const known = <I>(state: Lockstep<I>, peer: string): boolean =>
116
+ state.members.some((member) => member.peer === peer && member.until === null) ||
117
+ state.pending.some((entry) => entry._tag === 'Admit' && entry.peer === peer);
118
+
119
+ /** The leader asks for `peer` to be admitted; anyone else, or a known peer, changes nothing. */
120
+ export const admit = <I>(state: Lockstep<I>, peer: string): Lockstep<I> =>
121
+ leaderAt(state, state.sent) !== state.self || known(state, peer)
122
+ ? state
123
+ : { ...state, pending: [...state.pending, { _tag: 'Admit', peer }] };
124
+
125
+ const hasPacket = <I>(state: Lockstep<I>, peer: string, turn: number): boolean =>
126
+ state.received.some((packet) => packet.peer === peer && packet.turn === turn);
127
+
128
+ /**
129
+ * A packet arriving: kept until its turn applies. Ones for turns already applied, from
130
+ * peers not members then, and second copies are dropped.
131
+ */
132
+ export const receive = <I>(state: Lockstep<I>, packet: Packet<I>): Lockstep<I> => {
133
+ if (packet.turn < state.turn || hasPacket(state, packet.peer, packet.turn)) return state;
134
+ const member = state.members.find((each) => each.peer === packet.peer);
135
+ if (member !== undefined && !memberAt(member, packet.turn)) return state;
136
+ return { ...state, received: [...state.received, packet] };
137
+ };
138
+
139
+ /**
140
+ * This peer's packet for its next turn, with what is pending, when it is a member then
141
+ * and not more than `lead` turns ahead; it keeps its own copy.
142
+ */
143
+ export const send = <I>(
144
+ state: Lockstep<I>,
145
+ ): { readonly state: Lockstep<I>; readonly packet: Packet<I> | undefined } => {
146
+ const member = state.members.find((each) => each.peer === state.self);
147
+ const ahead = state.sent >= state.turn + state.lead;
148
+ if (member === undefined || !memberAt(member, state.sent) || ahead) {
149
+ return { state, packet: undefined };
150
+ }
151
+ const packet: Packet<I> = { peer: state.self, turn: state.sent, entries: state.pending };
152
+ return {
153
+ state: { ...state, sent: state.sent + 1, pending: [], received: [...state.received, packet] },
154
+ packet,
155
+ };
156
+ };
157
+
158
+ /** Peers whose packet for the next turn has not arrived: what the session waits on. */
159
+ export const waitingOn = <I>(state: Lockstep<I>): ReadonlyArray<string> =>
160
+ membersAt(state, state.turn).filter((peer) => !hasPacket(state, peer, state.turn));
161
+
162
+ /** `peer`'s packet for `turn`, as a list of none or one. */
163
+ const packetsOf = <I>(state: Lockstep<I>, peer: string, turn: number): ReadonlyArray<Packet<I>> =>
164
+ state.received.filter((packet) => packet.peer === peer && packet.turn === turn);
165
+
166
+ /** One turn applied, its admissions made members `lead + 1` turns on. */
167
+ const applyTurn = <I>(
168
+ state: Lockstep<I>,
169
+ ): { state: Lockstep<I>; applied: ReadonlyArray<Applied<I>> } => {
170
+ const turn = state.turn;
171
+ const packets = membersAt(state, turn).flatMap((peer) => packetsOf(state, peer, turn));
172
+ const entries = packets.flatMap((packet) =>
173
+ packet.entries.map((entry) => ({ peer: packet.peer, entry })),
174
+ );
175
+ const admitted = entries.flatMap(({ entry }) =>
176
+ entry._tag === 'Admit' && !state.members.some((member) => member.peer === entry.peer)
177
+ ? [{ peer: entry.peer, from: turn + state.lead + 1, until: null }]
178
+ : [],
179
+ );
180
+ const applied = entries.flatMap(({ peer, entry }) =>
181
+ entry._tag === 'Input' ? [{ turn, peer, input: entry.input }] : [],
182
+ );
183
+ return {
184
+ state: {
185
+ ...state,
186
+ turn: turn + 1,
187
+ members: [...state.members, ...admitted],
188
+ received: state.received.filter((packet) => packet.turn > turn),
189
+ },
190
+ applied,
191
+ };
192
+ };
193
+
194
+ /**
195
+ * Every turn whose packets have all arrived, applied in order: the new state, and the
196
+ * inputs to apply to the app, oldest first and in peer order within a turn.
197
+ */
198
+ export const advance = <I>(
199
+ state: Lockstep<I>,
200
+ ): { readonly state: Lockstep<I>; readonly applied: ReadonlyArray<Applied<I>> } => {
201
+ let current = state;
202
+ const applied: Array<Applied<I>> = [];
203
+ // A turn with no members waits forever rather than passing unheard.
204
+ while (membersAt(current, current.turn).length > 0 && waitingOn(current).length === 0) {
205
+ const next = applyTurn(current);
206
+ current = next.state;
207
+ applied.push(...next.applied);
208
+ }
209
+ return { state: current, applied };
210
+ };
211
+
212
+ /**
213
+ * Newcomers this peer, as leader, should welcome now: those who become members at the
214
+ * turn about to apply, once every turn before it has (the state is then theirs to start
215
+ * from). Each is due once: at the moment `before` (the previous turn) is passed.
216
+ */
217
+ export const welcomeDue = <I>(before: number, state: Lockstep<I>): ReadonlyArray<string> =>
218
+ state.members
219
+ .filter((member) => member.from > before && member.from <= state.turn)
220
+ // The leader of the turn before the newcomer counts: never the newcomer itself, whose
221
+ // id may sort first.
222
+ .filter((member) => leaderAt(state, member.from - 1) === state.self)
223
+ .map((member) => member.peer);
224
+
225
+ /** This peer leaving: the turn it sent last, for the others to stop waiting after. */
226
+ export const leave = <I>(state: Lockstep<I>): number => state.sent - 1;
227
+
228
+ /** `peer` stops being a member from `until`; its packets from then are dropped. */
229
+ export const remove = <I>(state: Lockstep<I>, peer: string, until: number): Lockstep<I> => ({
230
+ ...state,
231
+ members: state.members.map((member) =>
232
+ member.peer === peer && (member.until === null || until < member.until)
233
+ ? { ...member, until }
234
+ : member,
235
+ ),
236
+ received: state.received.filter((packet) => packet.peer !== peer || packet.turn < until),
237
+ });
@@ -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) {
@@ -215,6 +217,11 @@ export const createSceneLoop = (options: SceneLoopOptions): SceneLoop => {
215
217
  const views = graph.views();
216
218
  if (views.length === 0) renderer.render(graph.scene, graph.camera);
217
219
  for (const view of views) renderer.render(graph.scene, view.camera, view.area);
220
+ const hud = graph.hud();
221
+ if (hud !== undefined && renderer.renderOver !== undefined) {
222
+ renderer.renderOver(hud.graph.scene, hud.graph.camera);
223
+ hud.graph.drawn();
224
+ }
218
225
  graph.drawn();
219
226
  measure();
220
227
  const overlays = graph.overlays();
@@ -108,6 +108,9 @@ const applyBasic = (
108
108
  textures: Textures,
109
109
  ): void => {
110
110
  material.color.setHex(description.color);
111
+ // Drawn over everything when not depth-tested, and leaving no depth behind.
112
+ material.depthTest = description.depthTest;
113
+ material.depthWrite = description.depthTest;
111
114
  const mapChanged = applyMap(material, description.map, textures);
112
115
  if (applySurface(material, description) || mapChanged) material.needsUpdate = true;
113
116
  };
@@ -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
+ };
@@ -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 = { readonly createWorld: (physics: Physics) => PhysicsWorld };
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