@driftengine/capture 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +29 -0
  3. package/README.md +68 -0
  4. package/dist/browserFrames.d.ts +31 -0
  5. package/dist/browserFrames.js +105 -0
  6. package/dist/collide.d.ts +59 -0
  7. package/dist/collide.js +92 -0
  8. package/dist/decimate.d.ts +42 -0
  9. package/dist/decimate.js +439 -0
  10. package/dist/delight.d.ts +42 -0
  11. package/dist/delight.js +351 -0
  12. package/dist/delightScene.d.ts +37 -0
  13. package/dist/delightScene.js +183 -0
  14. package/dist/depth.d.ts +44 -0
  15. package/dist/depth.js +53 -0
  16. package/dist/features.d.ts +33 -0
  17. package/dist/features.js +260 -0
  18. package/dist/frames.d.ts +47 -0
  19. package/dist/frames.js +87 -0
  20. package/dist/fusion.d.ts +77 -0
  21. package/dist/fusion.js +152 -0
  22. package/dist/gaussians/adam.d.ts +57 -0
  23. package/dist/gaussians/adam.js +88 -0
  24. package/dist/gaussians/depth.d.ts +8 -0
  25. package/dist/gaussians/depth.js +73 -0
  26. package/dist/gaussians/gradients.d.ts +36 -0
  27. package/dist/gaussians/gradients.js +228 -0
  28. package/dist/gaussians/harmonics.d.ts +46 -0
  29. package/dist/gaussians/harmonics.js +99 -0
  30. package/dist/gaussians/optimise.d.ts +50 -0
  31. package/dist/gaussians/optimise.js +125 -0
  32. package/dist/gaussians/parameters.d.ts +35 -0
  33. package/dist/gaussians/parameters.js +165 -0
  34. package/dist/gaussians/project.d.ts +72 -0
  35. package/dist/gaussians/project.js +137 -0
  36. package/dist/gaussians/projectGradient.d.ts +15 -0
  37. package/dist/gaussians/projectGradient.js +213 -0
  38. package/dist/gaussians/rasterise.d.ts +13 -0
  39. package/dist/gaussians/rasterise.js +65 -0
  40. package/dist/gaussians/refine.d.ts +36 -0
  41. package/dist/gaussians/refine.js +136 -0
  42. package/dist/gaussians/similarity.d.ts +25 -0
  43. package/dist/gaussians/similarity.js +80 -0
  44. package/dist/index.d.ts +101 -0
  45. package/dist/index.js +96 -0
  46. package/dist/marching.d.ts +38 -0
  47. package/dist/marching.js +258 -0
  48. package/dist/masks.d.ts +59 -0
  49. package/dist/masks.js +165 -0
  50. package/dist/math/dense.d.ts +68 -0
  51. package/dist/math/dense.js +294 -0
  52. package/dist/math/schur.d.ts +23 -0
  53. package/dist/math/schur.js +140 -0
  54. package/dist/models/batchNorm.d.ts +17 -0
  55. package/dist/models/batchNorm.js +24 -0
  56. package/dist/models/clipTokenizer.d.ts +40 -0
  57. package/dist/models/clipTokenizer.js +122 -0
  58. package/dist/models/clipTower.d.ts +27 -0
  59. package/dist/models/clipTower.js +45 -0
  60. package/dist/models/depthAnything2.d.ts +38 -0
  61. package/dist/models/depthAnything2.js +163 -0
  62. package/dist/models/depthAnything3.d.ts +26 -0
  63. package/dist/models/depthAnything3.js +171 -0
  64. package/dist/models/dualDpt.d.ts +19 -0
  65. package/dist/models/dualDpt.js +138 -0
  66. package/dist/models/hiera.d.ts +49 -0
  67. package/dist/models/hiera.js +120 -0
  68. package/dist/models/hieraAttention.d.ts +16 -0
  69. package/dist/models/hieraAttention.js +68 -0
  70. package/dist/models/miniature.d.ts +36 -0
  71. package/dist/models/miniature.js +216 -0
  72. package/dist/models/mobileSam.d.ts +41 -0
  73. package/dist/models/mobileSam.js +88 -0
  74. package/dist/models/owlv2.d.ts +49 -0
  75. package/dist/models/owlv2.js +131 -0
  76. package/dist/models/owlv2Detect.d.ts +25 -0
  77. package/dist/models/owlv2Detect.js +80 -0
  78. package/dist/models/owlv2Miniature.d.ts +27 -0
  79. package/dist/models/owlv2Miniature.js +87 -0
  80. package/dist/models/rope.d.ts +23 -0
  81. package/dist/models/rope.js +84 -0
  82. package/dist/models/sam21.d.ts +45 -0
  83. package/dist/models/sam21.js +126 -0
  84. package/dist/models/sam21Miniature.d.ts +37 -0
  85. package/dist/models/sam21Miniature.js +194 -0
  86. package/dist/models/sam21Video.d.ts +30 -0
  87. package/dist/models/sam21Video.js +101 -0
  88. package/dist/models/sam2Choice.d.ts +19 -0
  89. package/dist/models/sam2Choice.js +55 -0
  90. package/dist/models/sam2Decoder.d.ts +36 -0
  91. package/dist/models/sam2Decoder.js +108 -0
  92. package/dist/models/sam2Memories.d.ts +54 -0
  93. package/dist/models/sam2Memories.js +93 -0
  94. package/dist/models/sam2Memory.d.ts +30 -0
  95. package/dist/models/sam2Memory.js +136 -0
  96. package/dist/models/sam2Positions.d.ts +24 -0
  97. package/dist/models/sam2Positions.js +94 -0
  98. package/dist/models/sam2Tracker.d.ts +69 -0
  99. package/dist/models/sam2Tracker.js +172 -0
  100. package/dist/models/samDecoder.d.ts +45 -0
  101. package/dist/models/samDecoder.js +96 -0
  102. package/dist/models/samMiniature.d.ts +43 -0
  103. package/dist/models/samMiniature.js +161 -0
  104. package/dist/models/samPrompt.d.ts +57 -0
  105. package/dist/models/samPrompt.js +160 -0
  106. package/dist/models/seeded.d.ts +16 -0
  107. package/dist/models/seeded.js +48 -0
  108. package/dist/models/tinyVit.d.ts +38 -0
  109. package/dist/models/tinyVit.js +82 -0
  110. package/dist/models/twoWay.d.ts +46 -0
  111. package/dist/models/twoWay.js +67 -0
  112. package/dist/models/vit.d.ts +45 -0
  113. package/dist/models/vit.js +157 -0
  114. package/dist/models/windowAttention.d.ts +25 -0
  115. package/dist/models/windowAttention.js +104 -0
  116. package/dist/poses.d.ts +31 -0
  117. package/dist/poses.js +750 -0
  118. package/dist/prepare.d.ts +30 -0
  119. package/dist/prepare.js +238 -0
  120. package/dist/propose.d.ts +36 -0
  121. package/dist/propose.js +63 -0
  122. package/dist/resample.d.ts +39 -0
  123. package/dist/resample.js +165 -0
  124. package/dist/run.d.ts +3 -0
  125. package/dist/run.js +1 -0
  126. package/dist/scene.d.ts +104 -0
  127. package/dist/scene.js +109 -0
  128. package/dist/segment.d.ts +40 -0
  129. package/dist/segment.js +184 -0
  130. package/dist/testScene.d.ts +55 -0
  131. package/dist/testScene.js +214 -0
  132. package/dist/twoView.d.ts +22 -0
  133. package/dist/twoView.js +435 -0
  134. package/package.json +63 -0
  135. package/src/browserFrames.ts +145 -0
  136. package/src/collide.ts +128 -0
  137. package/src/decimate.ts +510 -0
  138. package/src/delight.ts +427 -0
  139. package/src/delightScene.ts +249 -0
  140. package/src/depth.ts +100 -0
  141. package/src/features.ts +302 -0
  142. package/src/frames.ts +111 -0
  143. package/src/fusion.ts +229 -0
  144. package/src/gaussians/adam.ts +131 -0
  145. package/src/gaussians/depth.ts +87 -0
  146. package/src/gaussians/gradients.ts +285 -0
  147. package/src/gaussians/harmonics.ts +122 -0
  148. package/src/gaussians/optimise.ts +191 -0
  149. package/src/gaussians/parameters.ts +186 -0
  150. package/src/gaussians/project.ts +207 -0
  151. package/src/gaussians/projectGradient.ts +234 -0
  152. package/src/gaussians/rasterise.ts +81 -0
  153. package/src/gaussians/refine.ts +176 -0
  154. package/src/gaussians/similarity.ts +87 -0
  155. package/src/index.ts +225 -0
  156. package/src/marching.ts +358 -0
  157. package/src/masks.ts +201 -0
  158. package/src/math/dense.ts +317 -0
  159. package/src/math/schur.ts +167 -0
  160. package/src/models/batchNorm.ts +57 -0
  161. package/src/models/clipTokenizer.ts +129 -0
  162. package/src/models/clipTower.ts +87 -0
  163. package/src/models/depthAnything2.ts +276 -0
  164. package/src/models/depthAnything3.ts +229 -0
  165. package/src/models/dualDpt.ts +233 -0
  166. package/src/models/hiera.ts +231 -0
  167. package/src/models/hieraAttention.ts +121 -0
  168. package/src/models/miniature.ts +241 -0
  169. package/src/models/mobileSam.ts +135 -0
  170. package/src/models/owlv2.ts +190 -0
  171. package/src/models/owlv2Detect.ts +116 -0
  172. package/src/models/owlv2Miniature.ts +108 -0
  173. package/src/models/rope.ts +111 -0
  174. package/src/models/sam21.ts +182 -0
  175. package/src/models/sam21Miniature.ts +225 -0
  176. package/src/models/sam21Video.ts +154 -0
  177. package/src/models/sam2Choice.ts +62 -0
  178. package/src/models/sam2Decoder.ts +218 -0
  179. package/src/models/sam2Memories.ts +165 -0
  180. package/src/models/sam2Memory.ts +247 -0
  181. package/src/models/sam2Positions.ts +106 -0
  182. package/src/models/sam2Tracker.ts +300 -0
  183. package/src/models/samDecoder.ts +225 -0
  184. package/src/models/samMiniature.ts +190 -0
  185. package/src/models/samPrompt.ts +227 -0
  186. package/src/models/seeded.ts +50 -0
  187. package/src/models/tinyVit.ts +178 -0
  188. package/src/models/twoWay.ts +122 -0
  189. package/src/models/vit.ts +265 -0
  190. package/src/models/windowAttention.ts +155 -0
  191. package/src/poses.ts +894 -0
  192. package/src/prepare.ts +306 -0
  193. package/src/propose.ts +96 -0
  194. package/src/resample.ts +197 -0
  195. package/src/run.ts +7 -0
  196. package/src/scene.ts +141 -0
  197. package/src/segment.ts +220 -0
  198. package/src/testScene.ts +283 -0
  199. package/src/twoView.ts +536 -0
@@ -0,0 +1,510 @@
1
+ /**
2
+ * A mesh brought down to a triangle budget, by collapsing the edges that cost the least.
3
+ *
4
+ * **What a marched surface hands over is far more triangles than its shape needs.** The volume is
5
+ * sampled on a lattice and every cell that the surface crosses emits triangles of about one cell
6
+ * across, so a flat wall costs as much as a folded one. Quadric error collapse is the standard
7
+ * answer and is the right one here: it measures each candidate collapse by the squared distance the
8
+ * surface would move, in metres, so what goes first is what changes the shape least — a wall
9
+ * flattens to a few triangles and a corner keeps its detail, without anybody choosing which.
10
+ *
11
+ * **The error is a quadric per vertex**, the sum over its faces of the squared distance to each
12
+ * face's plane. That is what makes the measure *accumulate*: after a collapse the survivor carries
13
+ * both vertices' planes, so the tenth collapse in a region is costed against the original surface
14
+ * rather than against the ninth's result. A greedy measure without it flattens a region one
15
+ * imperceptible step at a time until the shape is gone.
16
+ *
17
+ * **A border is held, and so is the facing.** An open capture's boundary is where the evidence
18
+ * stopped, and letting it creep inwards is losing the part of the scene that was seen. A collapse
19
+ * that would turn a face inside out is refused outright rather than costed, because the cost of an
20
+ * inverted face is not large — it is meaningless, and a mesh that collides inside-out is the
21
+ * failure `marching.ts` opens by describing.
22
+ */
23
+ import type { MeshData } from '@driftengine/drft';
24
+
25
+ export interface DecimateOptions {
26
+ /**
27
+ * How much a border edge resists being collapsed.
28
+ *
29
+ * The standard construction: each boundary edge contributes a plane through itself perpendicular
30
+ * to its own face, weighted by this, so moving along the border is cheap and moving off it is
31
+ * not. A hundred is enough that a border survives a decimation to a tenth and small enough that a
32
+ * long straight border still simplifies.
33
+ */
34
+ readonly borderWeight?: number;
35
+ }
36
+
37
+ const DEFAULT_BORDER = 100;
38
+
39
+ /**
40
+ * `mesh` simplified until it holds at most `budget` triangles.
41
+ *
42
+ * **Answers a mesh rather than filling one**, because neither the vertex count nor the triangle
43
+ * count is known before the collapses are made: a caller cannot size the arrays. A mesh already
44
+ * inside its budget is answered as it stands, copied, so a caller never has to ask which it got.
45
+ */
46
+ export function decimate(mesh: MeshData, budget: number, options: DecimateOptions = {}): MeshData {
47
+ const faceCount = mesh.indices.length / 3;
48
+ if (faceCount <= budget || budget <= 0) return copyOf(mesh);
49
+ const borderWeight = options.borderWeight ?? DEFAULT_BORDER;
50
+
51
+ const vertexCount = mesh.positions.length / 3;
52
+ const position = Float64Array.from(mesh.positions);
53
+ const faces = Int32Array.from(mesh.indices);
54
+ const faceAlive = new Uint8Array(faceCount).fill(1);
55
+ const vertexAlive = new Uint8Array(vertexCount).fill(1);
56
+ const facesOf: Set<number>[] = Array.from({ length: vertexCount }, () => new Set<number>());
57
+ for (let face = 0; face < faceCount; face += 1) {
58
+ for (let slot = 0; slot < 3; slot += 1) {
59
+ (facesOf[faces[face * 3 + slot] as number] as Set<number>).add(face);
60
+ }
61
+ }
62
+
63
+ /* Ten numbers a vertex: the symmetric 4 × 4 of the squared distance to its own planes. */
64
+ const quadrics = new Float64Array(vertexCount * 10);
65
+ const plane = new Float64Array(4);
66
+ for (let face = 0; face < faceCount; face += 1) {
67
+ if (!planeOf(position, faces, face, plane)) continue;
68
+ for (let slot = 0; slot < 3; slot += 1) {
69
+ addPlane(quadrics, faces[face * 3 + slot] as number, plane, 1);
70
+ }
71
+ }
72
+ /* And the borders, as a plane through each border edge at right angles to its own face. */
73
+ forEachBorderEdge(faces, faceAlive, faceCount, (a, b, face) => {
74
+ if (!planeOf(position, faces, face, plane)) return;
75
+ if (!borderPlane(position, a, b, plane)) return;
76
+ addPlane(quadrics, a, plane, borderWeight);
77
+ addPlane(quadrics, b, plane, borderWeight);
78
+ });
79
+
80
+ const heap = new CostHeap();
81
+ const version = new Map<number, number>();
82
+ const target = new Float64Array(3);
83
+ const pushEdge = (a: number, b: number): void => {
84
+ if (a === b) return;
85
+ const low = a < b ? a : b;
86
+ const high = a < b ? b : a;
87
+ const key = low * vertexCount + high;
88
+ const cost = collapseCost(quadrics, position, low, high, target);
89
+ const stamp = (version.get(key) ?? 0) + 1;
90
+ version.set(key, stamp);
91
+ heap.push(cost, key, stamp);
92
+ };
93
+ const edges = new Set<number>();
94
+ for (let face = 0; face < faceCount; face += 1) {
95
+ for (let slot = 0; slot < 3; slot += 1) {
96
+ const a = faces[face * 3 + slot] as number;
97
+ const b = faces[face * 3 + ((slot + 1) % 3)] as number;
98
+ const key = a < b ? a * vertexCount + b : b * vertexCount + a;
99
+ if (edges.has(key)) continue;
100
+ edges.add(key);
101
+ pushEdge(a, b);
102
+ }
103
+ }
104
+
105
+ let alive = faceCount;
106
+ while (alive > budget) {
107
+ const next = heap.pop();
108
+ if (next === null) break;
109
+ const key = next.key;
110
+ if ((version.get(key) ?? 0) !== next.stamp) continue;
111
+ const keep = Math.floor(key / vertexCount);
112
+ const drop = key % vertexCount;
113
+ if ((vertexAlive[keep] as number) === 0 || (vertexAlive[drop] as number) === 0) continue;
114
+ collapseCost(quadrics, position, keep, drop, target);
115
+ if (wouldFlip(position, faces, faceAlive, facesOf, keep, drop, target)) continue;
116
+
117
+ /* The faces that held both ends go; the rest move from the dropped vertex to the kept one. */
118
+ const kept = facesOf[keep] as Set<number>;
119
+ const dropped = facesOf[drop] as Set<number>;
120
+ for (const face of dropped) {
121
+ if (kept.has(face)) {
122
+ if ((faceAlive[face] as number) === 1) {
123
+ faceAlive[face] = 0;
124
+ alive -= 1;
125
+ }
126
+ continue;
127
+ }
128
+ for (let slot = 0; slot < 3; slot += 1) {
129
+ if ((faces[face * 3 + slot] as number) === drop) faces[face * 3 + slot] = keep;
130
+ }
131
+ kept.add(face);
132
+ }
133
+ for (const face of kept) {
134
+ if ((faceAlive[face] as number) === 0) kept.delete(face);
135
+ }
136
+ dropped.clear();
137
+ vertexAlive[drop] = 0;
138
+ for (let k = 0; k < 3; k += 1) position[keep * 3 + k] = target[k] as number;
139
+ for (let k = 0; k < 10; k += 1) {
140
+ quadrics[keep * 10 + k] =
141
+ (quadrics[keep * 10 + k] as number) + (quadrics[drop * 10 + k] as number);
142
+ }
143
+
144
+ /* Everything still touching the survivor has a new cost. */
145
+ const neighbours = new Set<number>();
146
+ for (const face of kept) {
147
+ for (let slot = 0; slot < 3; slot += 1) {
148
+ const vertex = faces[face * 3 + slot] as number;
149
+ if (vertex !== keep && (vertexAlive[vertex] as number) === 1) neighbours.add(vertex);
150
+ }
151
+ }
152
+ for (const vertex of neighbours) pushEdge(keep, vertex);
153
+ }
154
+
155
+ return rebuild(
156
+ position,
157
+ normalsFrom(position, faces, faceAlive, faceCount),
158
+ faces,
159
+ faceAlive,
160
+ faceCount,
161
+ mesh,
162
+ );
163
+ }
164
+
165
+ /** A face's plane, unit normal first, or false where the triangle has no area to have one. */
166
+ function planeOf(
167
+ position: Float64Array,
168
+ faces: Int32Array,
169
+ face: number,
170
+ out: Float64Array,
171
+ ): boolean {
172
+ const a = faces[face * 3] as number;
173
+ const b = faces[face * 3 + 1] as number;
174
+ const c = faces[face * 3 + 2] as number;
175
+ const ux = (position[b * 3] as number) - (position[a * 3] as number);
176
+ const uy = (position[b * 3 + 1] as number) - (position[a * 3 + 1] as number);
177
+ const uz = (position[b * 3 + 2] as number) - (position[a * 3 + 2] as number);
178
+ const vx = (position[c * 3] as number) - (position[a * 3] as number);
179
+ const vy = (position[c * 3 + 1] as number) - (position[a * 3 + 1] as number);
180
+ const vz = (position[c * 3 + 2] as number) - (position[a * 3 + 2] as number);
181
+ const nx = uy * vz - uz * vy;
182
+ const ny = uz * vx - ux * vz;
183
+ const nz = ux * vy - uy * vx;
184
+ const length = Math.sqrt(nx * nx + ny * ny + nz * nz);
185
+ if (!(length > 0)) return false;
186
+ out[0] = nx / length;
187
+ out[1] = ny / length;
188
+ out[2] = nz / length;
189
+ out[3] = -(
190
+ (out[0] as number) * (position[a * 3] as number) +
191
+ (out[1] as number) * (position[a * 3 + 1] as number) +
192
+ (out[2] as number) * (position[a * 3 + 2] as number)
193
+ );
194
+ return true;
195
+ }
196
+
197
+ /** The plane through a border edge at right angles to `plane`, replacing it in place. */
198
+ function borderPlane(position: Float64Array, a: number, b: number, plane: Float64Array): boolean {
199
+ const ex = (position[b * 3] as number) - (position[a * 3] as number);
200
+ const ey = (position[b * 3 + 1] as number) - (position[a * 3 + 1] as number);
201
+ const ez = (position[b * 3 + 2] as number) - (position[a * 3 + 2] as number);
202
+ const nx = ey * (plane[2] as number) - ez * (plane[1] as number);
203
+ const ny = ez * (plane[0] as number) - ex * (plane[2] as number);
204
+ const nz = ex * (plane[1] as number) - ey * (plane[0] as number);
205
+ const length = Math.sqrt(nx * nx + ny * ny + nz * nz);
206
+ if (!(length > 0)) return false;
207
+ plane[0] = nx / length;
208
+ plane[1] = ny / length;
209
+ plane[2] = nz / length;
210
+ plane[3] = -(
211
+ (plane[0] as number) * (position[a * 3] as number) +
212
+ (plane[1] as number) * (position[a * 3 + 1] as number) +
213
+ (plane[2] as number) * (position[a * 3 + 2] as number)
214
+ );
215
+ return true;
216
+ }
217
+
218
+ function addPlane(
219
+ quadrics: Float64Array,
220
+ vertex: number,
221
+ plane: Float64Array,
222
+ weight: number,
223
+ ): void {
224
+ const at = vertex * 10;
225
+ let slot = 0;
226
+ for (let row = 0; row < 4; row += 1) {
227
+ for (let column = row; column < 4; column += 1) {
228
+ quadrics[at + slot] =
229
+ (quadrics[at + slot] as number) +
230
+ weight * (plane[row] as number) * (plane[column] as number);
231
+ slot += 1;
232
+ }
233
+ }
234
+ }
235
+
236
+ /** `vᵀQv` for the quadric of a vertex, which is the squared distance to all its planes. */
237
+ function errorAt(quadrics: Float64Array, vertex: number, x: number, y: number, z: number): number {
238
+ const q = vertex * 10;
239
+ const v = [x, y, z, 1];
240
+ let total = 0;
241
+ let slot = 0;
242
+ for (let row = 0; row < 4; row += 1) {
243
+ for (let column = row; column < 4; column += 1) {
244
+ const term = (quadrics[q + slot] as number) * (v[row] as number) * (v[column] as number);
245
+ total += row === column ? term : 2 * term;
246
+ slot += 1;
247
+ }
248
+ }
249
+ return total;
250
+ }
251
+
252
+ /**
253
+ * What collapsing an edge costs, and where the survivor goes, into `out`.
254
+ *
255
+ * **Three candidates rather than the exact minimum**: both ends and the midpoint. Solving the
256
+ * quadric for its own minimum puts the vertex wherever the planes say, which for a nearly flat
257
+ * region is a badly conditioned system and an answer far outside the mesh. Choosing the best of
258
+ * three keeps every survivor on the surface it came from, which is what a collision mesh needs.
259
+ */
260
+ function collapseCost(
261
+ quadrics: Float64Array,
262
+ position: Float64Array,
263
+ keep: number,
264
+ drop: number,
265
+ out: Float64Array,
266
+ ): number {
267
+ const candidates = [
268
+ [
269
+ position[keep * 3] as number,
270
+ position[keep * 3 + 1] as number,
271
+ position[keep * 3 + 2] as number,
272
+ ],
273
+ [
274
+ position[drop * 3] as number,
275
+ position[drop * 3 + 1] as number,
276
+ position[drop * 3 + 2] as number,
277
+ ],
278
+ [
279
+ ((position[keep * 3] as number) + (position[drop * 3] as number)) / 2,
280
+ ((position[keep * 3 + 1] as number) + (position[drop * 3 + 1] as number)) / 2,
281
+ ((position[keep * 3 + 2] as number) + (position[drop * 3 + 2] as number)) / 2,
282
+ ],
283
+ ];
284
+ let best = Infinity;
285
+ for (const candidate of candidates) {
286
+ const [x, y, z] = candidate as [number, number, number];
287
+ const cost = errorAt(quadrics, keep, x, y, z) + errorAt(quadrics, drop, x, y, z);
288
+ if (cost < best) {
289
+ best = cost;
290
+ out[0] = x;
291
+ out[1] = y;
292
+ out[2] = z;
293
+ }
294
+ }
295
+ return best;
296
+ }
297
+
298
+ /** Would any face that survives the collapse end up facing the other way? */
299
+ function wouldFlip(
300
+ position: Float64Array,
301
+ faces: Int32Array,
302
+ faceAlive: Uint8Array,
303
+ facesOf: readonly Set<number>[],
304
+ keep: number,
305
+ drop: number,
306
+ target: Float64Array,
307
+ ): boolean {
308
+ const before = new Float64Array(4);
309
+ const after = new Float64Array(4);
310
+ const held = [0, 0, 0];
311
+ for (const vertex of [keep, drop]) {
312
+ for (const face of facesOf[vertex] as Set<number>) {
313
+ if ((faceAlive[face] as number) === 0) continue;
314
+ let touchesBoth = false;
315
+ for (let slot = 0; slot < 3; slot += 1) {
316
+ const other = faces[face * 3 + slot] as number;
317
+ if (other === (vertex === keep ? drop : keep)) touchesBoth = true;
318
+ }
319
+ /* A face holding both ends disappears in the collapse, so its facing is not at stake. */
320
+ if (touchesBoth) continue;
321
+ if (!planeOf(position, faces, face, before)) continue;
322
+ for (let slot = 0; slot < 3; slot += 1) {
323
+ const at = faces[face * 3 + slot] as number;
324
+ if (at !== vertex) continue;
325
+ for (let k = 0; k < 3; k += 1) {
326
+ held[k] = position[at * 3 + k] as number;
327
+ position[at * 3 + k] = target[k] as number;
328
+ }
329
+ const flat = !planeOf(position, faces, face, after);
330
+ for (let k = 0; k < 3; k += 1) position[at * 3 + k] = held[k] as number;
331
+ if (flat) return true;
332
+ const agree =
333
+ (before[0] as number) * (after[0] as number) +
334
+ (before[1] as number) * (after[1] as number) +
335
+ (before[2] as number) * (after[2] as number);
336
+ if (agree <= 0) return true;
337
+ }
338
+ }
339
+ }
340
+ return false;
341
+ }
342
+
343
+ /** Every edge used by exactly one live face, with that face. */
344
+ function forEachBorderEdge(
345
+ faces: Int32Array,
346
+ faceAlive: Uint8Array,
347
+ faceCount: number,
348
+ visit: (a: number, b: number, face: number) => void,
349
+ ): void {
350
+ const owner = new Map<string, { face: number; count: number }>();
351
+ for (let face = 0; face < faceCount; face += 1) {
352
+ if ((faceAlive[face] as number) === 0) continue;
353
+ for (let slot = 0; slot < 3; slot += 1) {
354
+ const a = faces[face * 3 + slot] as number;
355
+ const b = faces[face * 3 + ((slot + 1) % 3)] as number;
356
+ const key = a < b ? `${a}-${b}` : `${b}-${a}`;
357
+ const held = owner.get(key);
358
+ if (held === undefined) owner.set(key, { face, count: 1 });
359
+ else held.count += 1;
360
+ }
361
+ }
362
+ for (const [key, held] of owner) {
363
+ if (held.count !== 1) continue;
364
+ const [a, b] = key.split('-').map((value) => Number(value));
365
+ visit(a as number, b as number, held.face);
366
+ }
367
+ }
368
+
369
+ /** A normal a vertex, from the faces it is left with, so a collapsed mesh is shaded by itself. */
370
+ function normalsFrom(
371
+ position: Float64Array,
372
+ faces: Int32Array,
373
+ faceAlive: Uint8Array,
374
+ faceCount: number,
375
+ ): Float64Array {
376
+ const normals = new Float64Array(position.length);
377
+ const plane = new Float64Array(4);
378
+ for (let face = 0; face < faceCount; face += 1) {
379
+ if ((faceAlive[face] as number) === 0) continue;
380
+ if (!planeOf(position, faces, face, plane)) continue;
381
+ for (let slot = 0; slot < 3; slot += 1) {
382
+ const vertex = faces[face * 3 + slot] as number;
383
+ for (let k = 0; k < 3; k += 1) {
384
+ normals[vertex * 3 + k] = (normals[vertex * 3 + k] as number) + (plane[k] as number);
385
+ }
386
+ }
387
+ }
388
+ for (let vertex = 0; vertex < normals.length / 3; vertex += 1) {
389
+ const x = normals[vertex * 3] as number;
390
+ const y = normals[vertex * 3 + 1] as number;
391
+ const z = normals[vertex * 3 + 2] as number;
392
+ const length = Math.sqrt(x * x + y * y + z * z);
393
+ if (!(length > 0)) continue;
394
+ normals[vertex * 3] = x / length;
395
+ normals[vertex * 3 + 1] = y / length;
396
+ normals[vertex * 3 + 2] = z / length;
397
+ }
398
+ return normals;
399
+ }
400
+
401
+ /** The live faces and the vertices they still use, renumbered from nothing. */
402
+ function rebuild(
403
+ position: Float64Array,
404
+ normals: Float64Array,
405
+ faces: Int32Array,
406
+ faceAlive: Uint8Array,
407
+ faceCount: number,
408
+ source: MeshData,
409
+ ): MeshData {
410
+ const renumbered = new Map<number, number>();
411
+ const positions: number[] = [];
412
+ const outNormals: number[] = [];
413
+ const indices: number[] = [];
414
+ for (let face = 0; face < faceCount; face += 1) {
415
+ if ((faceAlive[face] as number) === 0) continue;
416
+ for (let slot = 0; slot < 3; slot += 1) {
417
+ const vertex = faces[face * 3 + slot] as number;
418
+ let at = renumbered.get(vertex);
419
+ if (at === undefined) {
420
+ at = positions.length / 3;
421
+ renumbered.set(vertex, at);
422
+ for (let k = 0; k < 3; k += 1) positions.push(position[vertex * 3 + k] as number);
423
+ for (let k = 0; k < 3; k += 1) outNormals.push(normals[vertex * 3 + k] as number);
424
+ }
425
+ indices.push(at);
426
+ }
427
+ }
428
+ return {
429
+ positions: Float32Array.from(positions),
430
+ normals: Float32Array.from(outNormals),
431
+ colors: new Float32Array(positions.length).fill(1),
432
+ /* One a vertex, against the vertices that survived rather than the ones that went in. */
433
+ emissive: new Float32Array(positions.length / 3),
434
+ indices: Uint32Array.from(indices),
435
+ };
436
+ }
437
+
438
+ function copyOf(mesh: MeshData): MeshData {
439
+ return {
440
+ positions: Float32Array.from(mesh.positions),
441
+ normals: Float32Array.from(mesh.normals),
442
+ colors: Float32Array.from(mesh.colors),
443
+ emissive: Float32Array.from(mesh.emissive),
444
+ indices: Uint32Array.from(mesh.indices),
445
+ };
446
+ }
447
+
448
+ /**
449
+ * The cheapest collapse first, with stale entries left in and skipped on the way out.
450
+ *
451
+ * A collapse changes the cost of every edge around it, and finding those entries to move would
452
+ * need a handle per edge. Pushing a new entry and letting the old one fall out is the standard
453
+ * trade: the heap holds more than it needs to and every pop is checked against the edge's current
454
+ * stamp. **Ties break by key**, so two collapses that cost the same happen in the same order every
455
+ * run — which is what makes a decimated capture reproducible.
456
+ */
457
+ class CostHeap {
458
+ private readonly costs: number[] = [];
459
+ private readonly keys: number[] = [];
460
+ private readonly stamps: number[] = [];
461
+
462
+ push(cost: number, key: number, stamp: number): void {
463
+ this.costs.push(cost);
464
+ this.keys.push(key);
465
+ this.stamps.push(stamp);
466
+ let at = this.costs.length - 1;
467
+ while (at > 0) {
468
+ const parent = (at - 1) >> 1;
469
+ if (!this.before(at, parent)) break;
470
+ this.swap(at, parent);
471
+ at = parent;
472
+ }
473
+ }
474
+
475
+ pop(): { key: number; stamp: number } | null {
476
+ if (this.costs.length === 0) return null;
477
+ const key = this.keys[0] as number;
478
+ const stamp = this.stamps[0] as number;
479
+ const last = this.costs.length - 1;
480
+ this.swap(0, last);
481
+ this.costs.pop();
482
+ this.keys.pop();
483
+ this.stamps.pop();
484
+ let at = 0;
485
+ for (;;) {
486
+ const left = at * 2 + 1;
487
+ const right = left + 1;
488
+ let best = at;
489
+ if (left < this.costs.length && this.before(left, best)) best = left;
490
+ if (right < this.costs.length && this.before(right, best)) best = right;
491
+ if (best === at) break;
492
+ this.swap(at, best);
493
+ at = best;
494
+ }
495
+ return { key, stamp };
496
+ }
497
+
498
+ private before(a: number, b: number): boolean {
499
+ const first = this.costs[a] as number;
500
+ const second = this.costs[b] as number;
501
+ if (first !== second) return first < second;
502
+ return (this.keys[a] as number) < (this.keys[b] as number);
503
+ }
504
+
505
+ private swap(a: number, b: number): void {
506
+ [this.costs[a], this.costs[b]] = [this.costs[b] as number, this.costs[a] as number];
507
+ [this.keys[a], this.keys[b]] = [this.keys[b] as number, this.keys[a] as number];
508
+ [this.stamps[a], this.stamps[b]] = [this.stamps[b] as number, this.stamps[a] as number];
509
+ }
510
+ }