@constructorfleet/sketchforge-engine 1.26.0 → 1.28.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 (200) hide show
  1. package/dist/cadAnalysis.d.ts +94 -0
  2. package/dist/cadAnalysis.d.ts.map +1 -0
  3. package/dist/cadAnalysis.js +157 -0
  4. package/dist/cadAnalysis.js.map +1 -0
  5. package/dist/cadBakeMetadata.d.ts +11 -2
  6. package/dist/cadBakeMetadata.d.ts.map +1 -1
  7. package/dist/cadBakeMetadata.js +9 -0
  8. package/dist/cadBakeMetadata.js.map +1 -1
  9. package/dist/cadProjection.d.ts.map +1 -1
  10. package/dist/cadProjection.js.map +1 -1
  11. package/dist/cadSection.d.ts.map +1 -1
  12. package/dist/cadShell.d.ts +143 -0
  13. package/dist/cadShell.d.ts.map +1 -0
  14. package/dist/cadShell.js +313 -0
  15. package/dist/cadShell.js.map +1 -0
  16. package/dist/cadSolids.d.ts.map +1 -1
  17. package/dist/cadSolids.js +18 -1
  18. package/dist/cadSolids.js.map +1 -1
  19. package/dist/cutBrepFrameMigration.d.ts +46 -0
  20. package/dist/cutBrepFrameMigration.d.ts.map +1 -0
  21. package/dist/cutBrepFrameMigration.js +105 -0
  22. package/dist/cutBrepFrameMigration.js.map +1 -0
  23. package/dist/cutterPadding.d.ts.map +1 -1
  24. package/dist/domain/cameraProjection.js.map +1 -1
  25. package/dist/domain/sketchforge.d.ts +31 -1
  26. package/dist/domain/sketchforge.d.ts.map +1 -1
  27. package/dist/editorHistory.d.ts.map +1 -1
  28. package/dist/editorHistory.js.map +1 -1
  29. package/dist/engine.d.ts +3 -3
  30. package/dist/engine.d.ts.map +1 -1
  31. package/dist/engine.js +156 -78
  32. package/dist/engine.js.map +1 -1
  33. package/dist/exactBooleanParts.d.ts +4 -2
  34. package/dist/exactBooleanParts.d.ts.map +1 -1
  35. package/dist/exactBooleanParts.js +7 -3
  36. package/dist/exactBooleanParts.js.map +1 -1
  37. package/dist/exactBooleanSolids.d.ts.map +1 -1
  38. package/dist/gearProfile.d.ts.map +1 -1
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/loftProfile.d.ts +1 -1
  44. package/dist/loftProfile.d.ts.map +1 -1
  45. package/dist/loftProfile.js.map +1 -1
  46. package/dist/meshEdgeProjection.d.ts +104 -0
  47. package/dist/meshEdgeProjection.d.ts.map +1 -0
  48. package/dist/meshEdgeProjection.js +439 -0
  49. package/dist/meshEdgeProjection.js.map +1 -0
  50. package/dist/meshImport.d.ts.map +1 -1
  51. package/dist/meshImport.js +2 -20
  52. package/dist/meshImport.js.map +1 -1
  53. package/dist/meshMassProperties.d.ts +92 -0
  54. package/dist/meshMassProperties.d.ts.map +1 -0
  55. package/dist/meshMassProperties.js +570 -0
  56. package/dist/meshMassProperties.js.map +1 -0
  57. package/dist/meshSection.d.ts +44 -0
  58. package/dist/meshSection.d.ts.map +1 -0
  59. package/dist/meshSection.js +154 -0
  60. package/dist/meshSection.js.map +1 -0
  61. package/dist/meshShellOverlap.d.ts +75 -0
  62. package/dist/meshShellOverlap.d.ts.map +1 -0
  63. package/dist/meshShellOverlap.js +797 -0
  64. package/dist/meshShellOverlap.js.map +1 -0
  65. package/dist/meshShellSolids.d.ts +44 -0
  66. package/dist/meshShellSolids.d.ts.map +1 -0
  67. package/dist/meshShellSolids.js +116 -0
  68. package/dist/meshShellSolids.js.map +1 -0
  69. package/dist/nodeSvgDomParser.d.ts +3 -0
  70. package/dist/nodeSvgDomParser.d.ts.map +1 -0
  71. package/dist/nodeSvgDomParser.js +53 -0
  72. package/dist/nodeSvgDomParser.js.map +1 -0
  73. package/dist/parametricRecompute.d.ts.map +1 -1
  74. package/dist/parametricRecompute.js +63 -10
  75. package/dist/parametricRecompute.js.map +1 -1
  76. package/dist/placementWorkplane.d.ts.map +1 -1
  77. package/dist/placementWorkplane.js.map +1 -1
  78. package/dist/projectAssets.d.ts +1 -1
  79. package/dist/projectStore.d.ts +1 -1
  80. package/dist/projectStore.d.ts.map +1 -1
  81. package/dist/projectStore.js +23 -2
  82. package/dist/projectStore.js.map +1 -1
  83. package/dist/revolveRequests.d.ts +57 -1
  84. package/dist/revolveRequests.d.ts.map +1 -1
  85. package/dist/revolveRequests.js +103 -4
  86. package/dist/revolveRequests.js.map +1 -1
  87. package/dist/sceneExport.d.ts +6 -0
  88. package/dist/sceneExport.d.ts.map +1 -1
  89. package/dist/sceneExport.js +6 -2
  90. package/dist/sceneExport.js.map +1 -1
  91. package/dist/serverCadInterference.d.ts +8 -45
  92. package/dist/serverCadInterference.d.ts.map +1 -1
  93. package/dist/serverCadInterference.js +21 -43
  94. package/dist/serverCadInterference.js.map +1 -1
  95. package/dist/serverCadOperations.d.ts +73 -4
  96. package/dist/serverCadOperations.d.ts.map +1 -1
  97. package/dist/serverCadOperations.js +356 -71
  98. package/dist/serverCadOperations.js.map +1 -1
  99. package/dist/serverCadProjection.d.ts +14 -1
  100. package/dist/serverCadProjection.d.ts.map +1 -1
  101. package/dist/serverCadProjection.js +135 -13
  102. package/dist/serverCadProjection.js.map +1 -1
  103. package/dist/serverCadProperties.d.ts +14 -7
  104. package/dist/serverCadProperties.d.ts.map +1 -1
  105. package/dist/serverCadProperties.js +60 -24
  106. package/dist/serverCadProperties.js.map +1 -1
  107. package/dist/serverCadSection.d.ts +21 -1
  108. package/dist/serverCadSection.d.ts.map +1 -1
  109. package/dist/serverCadSection.js +157 -43
  110. package/dist/serverCadSection.js.map +1 -1
  111. package/dist/serverCadShell.d.ts +7 -94
  112. package/dist/serverCadShell.d.ts.map +1 -1
  113. package/dist/serverCadShell.js +25 -272
  114. package/dist/serverCadShell.js.map +1 -1
  115. package/dist/serverCadThickness.d.ts +8 -8
  116. package/dist/serverCadThickness.d.ts.map +1 -1
  117. package/dist/serverCadThickness.js +15 -42
  118. package/dist/serverCadThickness.js.map +1 -1
  119. package/dist/serverShapeBooleans.d.ts +75 -0
  120. package/dist/serverShapeBooleans.d.ts.map +1 -1
  121. package/dist/serverShapeBooleans.js +251 -9
  122. package/dist/serverShapeBooleans.js.map +1 -1
  123. package/dist/serverShapeGroups.js.map +1 -1
  124. package/dist/serverStepExplode.d.ts +1 -2
  125. package/dist/serverStepExplode.d.ts.map +1 -1
  126. package/dist/serverStepExplode.js +15 -148
  127. package/dist/serverStepExplode.js.map +1 -1
  128. package/dist/shapeBounds.d.ts.map +1 -1
  129. package/dist/shapeCatalog.d.ts.map +1 -1
  130. package/dist/shapeCatalog.js +1 -0
  131. package/dist/shapeCatalog.js.map +1 -1
  132. package/dist/shapeRequests.d.ts +13 -0
  133. package/dist/shapeRequests.d.ts.map +1 -1
  134. package/dist/shapeRequests.js +14 -0
  135. package/dist/shapeRequests.js.map +1 -1
  136. package/dist/sketchCadProfile.d.ts +13 -0
  137. package/dist/sketchCadProfile.d.ts.map +1 -1
  138. package/dist/sketchCadProfile.js +108 -0
  139. package/dist/sketchCadProfile.js.map +1 -1
  140. package/dist/sketchCadSweep.js.map +1 -1
  141. package/dist/sketchEdgeSegmentLineage.d.ts +32 -0
  142. package/dist/sketchEdgeSegmentLineage.d.ts.map +1 -0
  143. package/dist/sketchEdgeSegmentLineage.js +49 -0
  144. package/dist/sketchEdgeSegmentLineage.js.map +1 -0
  145. package/dist/sketchEdgeSegmentLineageKernel.d.ts +83 -0
  146. package/dist/sketchEdgeSegmentLineageKernel.d.ts.map +1 -0
  147. package/dist/sketchEdgeSegmentLineageKernel.js +125 -0
  148. package/dist/sketchEdgeSegmentLineageKernel.js.map +1 -0
  149. package/dist/sketchFaceDraftKernel.d.ts +16 -9
  150. package/dist/sketchFaceDraftKernel.d.ts.map +1 -1
  151. package/dist/sketchFaceDraftKernel.js +19 -11
  152. package/dist/sketchFaceDraftKernel.js.map +1 -1
  153. package/dist/sketchRevolve.d.ts +13 -0
  154. package/dist/sketchRevolve.d.ts.map +1 -1
  155. package/dist/sketchRevolve.js +4 -1
  156. package/dist/sketchRevolve.js.map +1 -1
  157. package/dist/skfProject.d.ts +1 -1
  158. package/dist/skfProject.d.ts.map +1 -1
  159. package/dist/skfProject.js +63 -4
  160. package/dist/skfProject.js.map +1 -1
  161. package/dist/sphereTessellation.d.ts.map +1 -1
  162. package/dist/stepAssemblyExplode.d.ts +45 -0
  163. package/dist/stepAssemblyExplode.d.ts.map +1 -0
  164. package/dist/stepAssemblyExplode.js +195 -0
  165. package/dist/stepAssemblyExplode.js.map +1 -0
  166. package/dist/stepAssemblyHierarchy.js.map +1 -1
  167. package/dist/stepComponentMetadata.d.ts +1 -1
  168. package/dist/stepComponentMetadata.d.ts.map +1 -1
  169. package/dist/stepComponentMetadata.js +93 -39
  170. package/dist/stepComponentMetadata.js.map +1 -1
  171. package/dist/stepExport.d.ts +14 -1
  172. package/dist/stepExport.d.ts.map +1 -1
  173. package/dist/stepExport.js +81 -32
  174. package/dist/stepExport.js.map +1 -1
  175. package/dist/stepImportCommon.d.ts +4 -1
  176. package/dist/stepImportCommon.d.ts.map +1 -1
  177. package/dist/stepImportCommon.js +7 -6
  178. package/dist/stepImportCommon.js.map +1 -1
  179. package/dist/stlImport.js.map +1 -1
  180. package/dist/support/cadModifierTypes.d.ts +100 -1
  181. package/dist/support/cadModifierTypes.d.ts.map +1 -1
  182. package/dist/support/i18n/LocalizedError.d.ts.map +1 -1
  183. package/dist/support/i18n/locales/en/messages.json +42 -2
  184. package/dist/support/i18n/locales.d.ts.map +1 -1
  185. package/dist/support/lib/appTheme.d.ts.map +1 -1
  186. package/dist/support/lib/materialPresets.js.map +1 -1
  187. package/dist/support/lib/measurementUnits.d.ts.map +1 -1
  188. package/dist/support/lib/operator-configuration.json +13 -1
  189. package/dist/support/lib/operatorConfiguration.d.ts +5 -5
  190. package/dist/support/lib/workplaneGrid.js.map +1 -1
  191. package/dist/svgImport.d.ts +2 -0
  192. package/dist/svgImport.d.ts.map +1 -1
  193. package/dist/svgImport.js +7 -0
  194. package/dist/svgImport.js.map +1 -1
  195. package/dist/threadCatalog.js.map +1 -1
  196. package/dist/workplaneSettings.js.map +1 -1
  197. package/dist/workplaneShapes.d.ts +2 -2
  198. package/dist/workplaneShapes.d.ts.map +1 -1
  199. package/dist/workplaneShapes.js.map +1 -1
  200. package/package.json +7 -6
@@ -0,0 +1,797 @@
1
+ /**
2
+ * How the closed shells of a mesh sit relative to one another (#529).
3
+ *
4
+ * A mesh measured shell by shell -- bodies added, voids subtracted -- is only
5
+ * right when its shells nest: every pair is either apart or one wholly inside
6
+ * the other. Two bodies that overlap without either enclosing the other, as an
7
+ * imported STL nobody unioned can hold, would have the overlap counted twice.
8
+ *
9
+ * Only shells whose surfaces meet need any work. Two closed surfaces that do
10
+ * not meet are apart or nested, and ray parity from one of them says which.
11
+ * Where they do meet, one shell overlaps the other without nesting exactly when
12
+ * its surface has area strictly inside the other *and* area strictly outside
13
+ * it: a surface wholly inside (or on) another's closure is enclosed by it, and
14
+ * one wholly outside (or on) it is apart from it or encloses it.
15
+ *
16
+ * - Two faces crossing each other transversally prove both at once.
17
+ * - Shells that only touch -- along a face they share without sharing its
18
+ * triangles, an edge, or a corner -- have no area strictly on the far side,
19
+ * so they are apart or nested, and their volumes add or subtract exactly:
20
+ * what they share has no volume. Which of the two they are is read here from
21
+ * their surfaces, not from ray parity, because a ray cast from a point where
22
+ * two shells touch cannot tell inside from outside.
23
+ * - Bodies that overlap with no face crossing another -- side by side over a
24
+ * shared slab, every face of the overlap coplanar with one of theirs -- show
25
+ * it in their surfaces' sides: the touching triangles are sampled, and every
26
+ * connected patch of the rest is classified once.
27
+ *
28
+ * Shells whose surfaces never come within `slack` are left to the caller.
29
+ *
30
+ * A uniform grid finds which triangles of different shells come within `slack`
31
+ * of each other, and each shell that has to be classified against gets a grid
32
+ * of triangle columns along the ray, so neither step compares every triangle
33
+ * with every other. A shell is classified against another only where their
34
+ * bounds meet, so a plate with hundreds of parts resting on it costs each part
35
+ * its own neighbourhood, not the whole plate. Pure arithmetic, no kernel.
36
+ */
37
+ /**
38
+ * Thrown by the mesh measures when two closed bodies overlap without one
39
+ * enclosing the other. Callers on the server turn it into the exact path's
40
+ * refusal; it carries no kernel type so this module stays kernel-free.
41
+ */
42
+ export class MeshShellsOverlapError extends Error {
43
+ constructor() {
44
+ super("The mesh has closed bodies that overlap without one enclosing the other.");
45
+ this.name = "MeshShellsOverlapError";
46
+ }
47
+ }
48
+ /** The key `relateTouchingShells` files a pair of shells under. */
49
+ export function shellPairKey(a, b, shellCount) {
50
+ return a < b ? a * shellCount + b : b * shellCount + a;
51
+ }
52
+ /** A direction no axis-aligned or 45-degree feature lines up with. */
53
+ const RAY = (() => {
54
+ const d = [0.5773502691896258 + 0.0731, 0.5773502691896258 - 0.1123, 0.5773502691896258 + 0.0417];
55
+ const length = Math.hypot(d[0], d[1], d[2]);
56
+ return [d[0] / length, d[1] / length, d[2] / length];
57
+ })();
58
+ /** Two axes perpendicular to RAY, for projecting along it. */
59
+ const ACROSS = (() => {
60
+ const [dx, dy, dz] = RAY;
61
+ let ux = dy * 0 - dz * 1, uy = dz * 0 - dx * 0, uz = dx * 1 - dy * 0; // RAY x (0, 1, 0)
62
+ const length = Math.hypot(ux, uy, uz);
63
+ ux /= length;
64
+ uy /= length;
65
+ uz /= length;
66
+ const vx = dy * uz - dz * uy, vy = dz * ux - dx * uz, vz = dx * uy - dy * ux;
67
+ return [[ux, uy, uz], [vx, vy, vz]];
68
+ })();
69
+ /**
70
+ * Barycentric sample points of a touching triangle: the centroids of the 16
71
+ * triangles its edges split into quarters make, so a sample lands in any part
72
+ * of it that is more than a sliver.
73
+ */
74
+ const SAMPLES = (() => {
75
+ const n = 4;
76
+ const out = [];
77
+ for (let i = 0; i < n; i += 1) {
78
+ for (let j = 0; i + j < n; j += 1) {
79
+ out.push([(i + 1 / 3) / n, (j + 1 / 3) / n]);
80
+ if (i + j < n - 1)
81
+ out.push([(i + 2 / 3) / n, (j + 2 / 3) / n]);
82
+ }
83
+ }
84
+ // The centroid first: it settles most triangles in one query.
85
+ out.unshift([1 / 3, 1 / 3]);
86
+ return out;
87
+ })();
88
+ /** Bounds per axis may grow to this many grid cells, so a few huge triangles among tiny ones stay cheap. */
89
+ const MAX_CELLS_PER_AXIS = 128;
90
+ /**
91
+ * The relation of every pair of shells whose surfaces come within `slack` of
92
+ * each other, stopping at the first pair that overlaps without nesting.
93
+ */
94
+ export function relateTouchingShells(input, slack) {
95
+ const relations = new Map();
96
+ const shellCount = input.shells.length;
97
+ const partners = candidatePairs(input.bounds, shellCount, slack);
98
+ if (!partners.size)
99
+ return { overlapping: null, relations };
100
+ const grid = triangleGrid(input, partners, slack);
101
+ const contacts = new Map();
102
+ const tested = new Set();
103
+ const triangleCount = input.shellOf.length;
104
+ // Every pair of triangles from different shells sharing a cell. Each cell's
105
+ // triangles are grouped by shell first, so a fine shell filling a cell is
106
+ // never paired with itself.
107
+ const byShell = new Map();
108
+ for (const cell of grid.cells.values()) {
109
+ byShell.clear();
110
+ for (const t of cell) {
111
+ const shell = input.shellOf[t];
112
+ const list = byShell.get(shell);
113
+ if (list)
114
+ list.push(t);
115
+ else
116
+ byShell.set(shell, [t]);
117
+ }
118
+ if (byShell.size < 2)
119
+ continue;
120
+ // Ascending by shell, so the first of each pair is the lower-numbered shell.
121
+ const groups = [...byShell].sort((one, two) => one[0] - two[0]);
122
+ for (let x = 0; x < groups.length; x += 1) {
123
+ for (let y = x + 1; y < groups.length; y += 1) {
124
+ const [first, firstTriangles] = groups[x], [second, secondTriangles] = groups[y];
125
+ if (!partners.get(first)?.has(second))
126
+ continue;
127
+ const key = first * shellCount + second;
128
+ for (const s of firstTriangles) {
129
+ for (const t of secondTriangles) {
130
+ const pairKey = s * triangleCount + t;
131
+ if (tested.has(pairKey))
132
+ continue;
133
+ tested.add(pairKey);
134
+ const contact = triangleContact(input, s, t, slack);
135
+ if (contact === "none")
136
+ continue;
137
+ // Faces crossing each other: each shell has area on both sides of the other.
138
+ if (contact === "cross")
139
+ return { overlapping: [first, second], relations };
140
+ const entry = contacts.get(key) ?? { first: new Set(), second: new Set() };
141
+ entry.first.add(s);
142
+ entry.second.add(t);
143
+ contacts.set(key, entry);
144
+ }
145
+ }
146
+ }
147
+ }
148
+ }
149
+ const classifier = new SideClassifier(input, grid, slack);
150
+ for (const [key, { first: firstContacts, second: secondContacts }] of contacts) {
151
+ const first = Math.floor(key / shellCount), second = key - first * shellCount;
152
+ const firstSides = classifier.sides(first, second, firstContacts);
153
+ const secondSides = classifier.sides(second, first, secondContacts);
154
+ const relation = relationFrom(firstSides, secondSides);
155
+ relations.set(key, relation);
156
+ if (relation === "overlap")
157
+ return { overlapping: [first, second], relations };
158
+ }
159
+ return { overlapping: null, relations };
160
+ }
161
+ function relationFrom(first, second) {
162
+ if ((first.inside && first.outside) || (second.inside && second.outside))
163
+ return "overlap";
164
+ // A shell with no area off the other's surface -- the same body twice, in
165
+ // different triangles -- or two shells each inside the other: nothing honest
166
+ // to say, so the pair is not passed as nested.
167
+ if ((!first.inside && !first.outside) || (!second.inside && !second.outside))
168
+ return "overlap";
169
+ if (first.inside && second.inside)
170
+ return "overlap";
171
+ if (first.inside)
172
+ return "first-inside";
173
+ if (second.inside)
174
+ return "second-inside";
175
+ return "apart";
176
+ }
177
+ /** For each shell, the shells whose bounds come within `slack` of its own: sweep and prune along x. */
178
+ function candidatePairs(bounds, shellCount, slack) {
179
+ const partners = new Map();
180
+ if (shellCount < 2)
181
+ return partners;
182
+ const order = Array.from({ length: shellCount }, (_, shell) => shell).sort((p, q) => bounds[6 * p] - bounds[6 * q]);
183
+ for (let at = 0; at < order.length; at += 1) {
184
+ const a = order[at], ba = 6 * a;
185
+ for (let next = at + 1; next < order.length; next += 1) {
186
+ const b = order[next], bb = 6 * b;
187
+ if (bounds[bb] > bounds[ba + 3] + slack)
188
+ break;
189
+ if (bounds[bb + 1] > bounds[ba + 4] + slack || bounds[ba + 1] > bounds[bb + 4] + slack)
190
+ continue;
191
+ if (bounds[bb + 2] > bounds[ba + 5] + slack || bounds[ba + 2] > bounds[bb + 5] + slack)
192
+ continue;
193
+ if (bounds[bb + 3] < bounds[ba] - slack)
194
+ continue;
195
+ (partners.get(a) ?? partners.set(a, new Set()).get(a)).add(b);
196
+ (partners.get(b) ?? partners.set(b, new Set()).get(b)).add(a);
197
+ }
198
+ }
199
+ return partners;
200
+ }
201
+ /**
202
+ * Triangles of shells with a partner, each entered in every cell it passes
203
+ * within `slack` of. A triangle outside every partner's bounds cannot touch a
204
+ * partner, so it is left out.
205
+ */
206
+ function triangleGrid({ points, corners, shells, bounds }, partners, slack) {
207
+ const entered = [];
208
+ const box = new Float64Array(6);
209
+ let extent = 0;
210
+ const lows = [Infinity, Infinity, Infinity], highs = [-Infinity, -Infinity, -Infinity];
211
+ for (const [shell, others] of partners) {
212
+ const nearby = partnerBins(bounds, shell, others, slack);
213
+ for (const t of shells[shell]) {
214
+ triangleBox(points, corners, t, box);
215
+ let near = false;
216
+ for (const other of nearby.around(box)) {
217
+ const o = 6 * other;
218
+ if (box[0] <= bounds[o + 3] + slack && box[3] >= bounds[o] - slack
219
+ && box[1] <= bounds[o + 4] + slack && box[4] >= bounds[o + 1] - slack
220
+ && box[2] <= bounds[o + 5] + slack && box[5] >= bounds[o + 2] - slack) {
221
+ near = true;
222
+ break;
223
+ }
224
+ }
225
+ if (!near)
226
+ continue;
227
+ entered.push(t);
228
+ extent += Math.max(box[3] - box[0], box[4] - box[1], box[5] - box[2]);
229
+ for (let axis = 0; axis < 3; axis += 1) {
230
+ lows[axis] = Math.min(lows[axis], box[axis] - slack);
231
+ highs[axis] = Math.max(highs[axis], box[axis + 3] + slack);
232
+ }
233
+ }
234
+ }
235
+ const origin = [lows[0], lows[1], lows[2]];
236
+ const size = [1, 1, 1];
237
+ const count = [1, 1, 1];
238
+ const cells = new Map();
239
+ if (!entered.length)
240
+ return { origin, size, count, cells };
241
+ // Cells about as wide as an average triangle: each triangle lands in a few,
242
+ // and each cell holds a few.
243
+ const wanted = Math.max(extent / entered.length, slack * 4, Number.MIN_VALUE);
244
+ for (let axis = 0; axis < 3; axis += 1) {
245
+ const span = Math.max(highs[axis] - lows[axis], slack);
246
+ count[axis] = Math.max(1, Math.min(MAX_CELLS_PER_AXIS, Math.ceil(span / wanted)));
247
+ size[axis] = span / count[axis];
248
+ }
249
+ const half = [size[0] / 2, size[1] / 2, size[2] / 2];
250
+ for (const t of entered) {
251
+ triangleBox(points, corners, t, box);
252
+ const i0 = cellIndex(origin, size, count, 0, box[0] - slack), i1 = cellIndex(origin, size, count, 0, box[3] + slack);
253
+ const j0 = cellIndex(origin, size, count, 1, box[1] - slack), j1 = cellIndex(origin, size, count, 1, box[4] + slack);
254
+ const k0 = cellIndex(origin, size, count, 2, box[2] - slack), k1 = cellIndex(origin, size, count, 2, box[5] + slack);
255
+ const many = (i1 - i0 + 1) * (j1 - j0 + 1) * (k1 - k0 + 1) > 8;
256
+ const plane = many ? trianglePlane(points, corners, t) : null;
257
+ for (let i = i0; i <= i1; i += 1) {
258
+ for (let j = j0; j <= j1; j += 1) {
259
+ for (let k = k0; k <= k1; k += 1) {
260
+ if (plane) {
261
+ // A large triangle passes through only a sheet of the cells its bounds cover.
262
+ const cx = origin[0] + (i + 0.5) * size[0], cy = origin[1] + (j + 0.5) * size[1], cz = origin[2] + (k + 0.5) * size[2];
263
+ const [nx, ny, nz, d] = plane;
264
+ const reach = Math.abs(nx) * half[0] + Math.abs(ny) * half[1] + Math.abs(nz) * half[2] + slack;
265
+ if (Math.abs(nx * cx + ny * cy + nz * cz - d) > reach)
266
+ continue;
267
+ }
268
+ const key = i + count[0] * (j + count[1] * k);
269
+ const cell = cells.get(key);
270
+ if (cell)
271
+ cell.push(t);
272
+ else
273
+ cells.set(key, [t]);
274
+ }
275
+ }
276
+ }
277
+ }
278
+ return { origin, size, count, cells };
279
+ }
280
+ /**
281
+ * A shell's partners binned over its own bounds, so each of its triangles is
282
+ * compared with the few partners near it rather than all of them: a plate
283
+ * with hundreds of parts resting on it would otherwise cost its triangle count
284
+ * times theirs.
285
+ */
286
+ function partnerBins(bounds, shell, others, slack) {
287
+ const list = [...others];
288
+ const s = 6 * shell;
289
+ const perAxis = Math.max(1, Math.min(64, Math.ceil(Math.sqrt(list.length))));
290
+ const low = [bounds[s] - slack, bounds[s + 1] - slack, bounds[s + 2] - slack];
291
+ const span = [0, 1, 2].map((axis) => Math.max(bounds[s + 3 + axis] + slack - low[axis], Number.MIN_VALUE));
292
+ const bin = (axis, value) => Math.max(0, Math.min(perAxis - 1, Math.floor(((value - low[axis]) / span[axis]) * perAxis)));
293
+ const cells = new Map();
294
+ if (list.length > 4) {
295
+ for (const other of list) {
296
+ const o = 6 * other;
297
+ for (let i = bin(0, bounds[o] - slack); i <= bin(0, bounds[o + 3] + slack); i += 1) {
298
+ for (let j = bin(1, bounds[o + 1] - slack); j <= bin(1, bounds[o + 4] + slack); j += 1) {
299
+ for (let k = bin(2, bounds[o + 2] - slack); k <= bin(2, bounds[o + 5] + slack); k += 1) {
300
+ const key = i + perAxis * (j + perAxis * k);
301
+ const cell = cells.get(key);
302
+ if (cell)
303
+ cell.push(other);
304
+ else
305
+ cells.set(key, [other]);
306
+ }
307
+ }
308
+ }
309
+ }
310
+ }
311
+ return {
312
+ /** Partners whose bounds may meet `box`; a partner can be listed twice. */
313
+ around(box) {
314
+ if (list.length <= 4)
315
+ return list;
316
+ const found = [];
317
+ for (let i = bin(0, box[0]); i <= bin(0, box[3]); i += 1) {
318
+ for (let j = bin(1, box[1]); j <= bin(1, box[4]); j += 1) {
319
+ for (let k = bin(2, box[2]); k <= bin(2, box[5]); k += 1) {
320
+ const cell = cells.get(i + perAxis * (j + perAxis * k));
321
+ if (cell)
322
+ found.push(...cell);
323
+ }
324
+ }
325
+ }
326
+ return found;
327
+ },
328
+ };
329
+ }
330
+ function cellIndex(origin, size, count, axis, value) {
331
+ return Math.max(0, Math.min(count[axis] - 1, Math.floor((value - origin[axis]) / size[axis])));
332
+ }
333
+ function triangleBox(points, corners, t, out) {
334
+ out[0] = out[1] = out[2] = Infinity;
335
+ out[3] = out[4] = out[5] = -Infinity;
336
+ for (let k = 0; k < 3; k += 1) {
337
+ const p = 3 * corners[3 * t + k];
338
+ for (let axis = 0; axis < 3; axis += 1) {
339
+ out[axis] = Math.min(out[axis], points[p + axis]);
340
+ out[axis + 3] = Math.max(out[axis + 3], points[p + axis]);
341
+ }
342
+ }
343
+ }
344
+ /** Unit normal and offset of a triangle's plane, or `null` for a sliver. */
345
+ function trianglePlane(points, corners, t) {
346
+ const a = 3 * corners[3 * t], b = 3 * corners[3 * t + 1], c = 3 * corners[3 * t + 2];
347
+ const ux = points[b] - points[a], uy = points[b + 1] - points[a + 1], uz = points[b + 2] - points[a + 2];
348
+ const vx = points[c] - points[a], vy = points[c + 1] - points[a + 1], vz = points[c + 2] - points[a + 2];
349
+ let nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
350
+ const length = Math.hypot(nx, ny, nz);
351
+ if (!(length > 0))
352
+ return null;
353
+ nx /= length;
354
+ ny /= length;
355
+ nz /= length;
356
+ return [nx, ny, nz, nx * points[a] + ny * points[a + 1] + nz * points[a + 2]];
357
+ }
358
+ function corner(points, corners, t, k) {
359
+ const p = 3 * corners[3 * t + k];
360
+ return [points[p], points[p + 1], points[p + 2]];
361
+ }
362
+ const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
363
+ const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
364
+ const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
365
+ /**
366
+ * Whether two triangles of different shells cross, merely touch (come within
367
+ * `slack`), or stay apart.
368
+ */
369
+ function triangleContact({ points, corners }, s, t, slack) {
370
+ const p = [corner(points, corners, s, 0), corner(points, corners, s, 1), corner(points, corners, s, 2)];
371
+ const q = [corner(points, corners, t, 0), corner(points, corners, t, 1), corner(points, corners, t, 2)];
372
+ for (let axis = 0; axis < 3; axis += 1) {
373
+ if (Math.min(p[0][axis], p[1][axis], p[2][axis]) > Math.max(q[0][axis], q[1][axis], q[2][axis]) + slack)
374
+ return "none";
375
+ if (Math.min(q[0][axis], q[1][axis], q[2][axis]) > Math.max(p[0][axis], p[1][axis], p[2][axis]) + slack)
376
+ return "none";
377
+ }
378
+ for (let k = 0; k < 3; k += 1) {
379
+ if (edgeCrossesFace(p[k], p[(k + 1) % 3], q, slack) || edgeCrossesFace(q[k], q[(k + 1) % 3], p, slack))
380
+ return "cross";
381
+ }
382
+ const reach = slack * slack;
383
+ for (let k = 0; k < 3; k += 1) {
384
+ if (pointTriangleDistanceSq(p[k], q) <= reach || pointTriangleDistanceSq(q[k], p) <= reach)
385
+ return "touch";
386
+ for (let m = 0; m < 3; m += 1) {
387
+ if (segmentDistanceSq(p[k], p[(k + 1) % 3], q[m], q[(m + 1) % 3]) <= reach)
388
+ return "touch";
389
+ }
390
+ }
391
+ for (let k = 0; k < 3; k += 1) {
392
+ if (segmentMeetsTriangle(p[k], p[(k + 1) % 3], q) || segmentMeetsTriangle(q[k], q[(k + 1) % 3], p))
393
+ return "touch";
394
+ }
395
+ return "none";
396
+ }
397
+ /**
398
+ * Whether the edge from `e0` to `e1` passes through the face of `tri`
399
+ * transversally: its ends more than `slack` either side of the face's plane,
400
+ * and the point where it passes more than `slack` inside every edge of the
401
+ * face, where the face is flat. A surface crossing another there has area on
402
+ * both sides of it.
403
+ */
404
+ function edgeCrossesFace(e0, e1, tri, slack) {
405
+ const u = sub(tri[1], tri[0]), v = sub(tri[2], tri[0]);
406
+ const normal = cross(u, v);
407
+ const length = Math.hypot(...normal);
408
+ if (!(length > 0))
409
+ return false;
410
+ const n = [normal[0] / length, normal[1] / length, normal[2] / length];
411
+ const d0 = dot(n, sub(e0, tri[0])), d1 = dot(n, sub(e1, tri[0]));
412
+ if (!((d0 < -slack && d1 > slack) || (d0 > slack && d1 < -slack)))
413
+ return false;
414
+ const f = d0 / (d0 - d1);
415
+ const at = [e0[0] + (e1[0] - e0[0]) * f, e0[1] + (e1[1] - e0[1]) * f, e0[2] + (e1[2] - e0[2]) * f];
416
+ for (let k = 0; k < 3; k += 1) {
417
+ const a = tri[k], b = tri[(k + 1) % 3];
418
+ const edge = sub(b, a);
419
+ const edgeLength = Math.hypot(...edge);
420
+ if (!(edgeLength > 0))
421
+ return false;
422
+ if (dot(cross(edge, sub(at, a)), n) / edgeLength <= slack)
423
+ return false;
424
+ }
425
+ return true;
426
+ }
427
+ /** Whether the segment touches the triangle at all, by Möller-Trumbore over its length. */
428
+ function segmentMeetsTriangle(s0, s1, tri) {
429
+ const direction = sub(s1, s0);
430
+ const e1 = sub(tri[1], tri[0]), e2 = sub(tri[2], tri[0]);
431
+ const h = cross(direction, e2);
432
+ const det = dot(e1, h);
433
+ if (Math.abs(det) < 1e-300)
434
+ return false;
435
+ const inv = 1 / det;
436
+ const s = sub(s0, tri[0]);
437
+ const u = dot(s, h) * inv;
438
+ if (u < 0 || u > 1)
439
+ return false;
440
+ const q = cross(s, e1);
441
+ const v = dot(direction, q) * inv;
442
+ if (v < 0 || u + v > 1)
443
+ return false;
444
+ const along = dot(e2, q) * inv;
445
+ return along >= 0 && along <= 1;
446
+ }
447
+ /** Squared distance from a point to a triangle (Ericson, Real-Time Collision Detection, 5.1.5). */
448
+ function pointTriangleDistanceSq(p, [a, b, c]) {
449
+ const ab = sub(b, a), ac = sub(c, a), ap = sub(p, a);
450
+ const d1 = dot(ab, ap), d2 = dot(ac, ap);
451
+ let closest;
452
+ if (d1 <= 0 && d2 <= 0)
453
+ closest = a;
454
+ else {
455
+ const bp = sub(p, b);
456
+ const d3 = dot(ab, bp), d4 = dot(ac, bp);
457
+ if (d3 >= 0 && d4 <= d3)
458
+ closest = b;
459
+ else {
460
+ const vc = d1 * d4 - d3 * d2;
461
+ if (vc <= 0 && d1 >= 0 && d3 <= 0) {
462
+ const w = d1 / (d1 - d3);
463
+ closest = [a[0] + w * ab[0], a[1] + w * ab[1], a[2] + w * ab[2]];
464
+ }
465
+ else {
466
+ const cp = sub(p, c);
467
+ const d5 = dot(ab, cp), d6 = dot(ac, cp);
468
+ if (d6 >= 0 && d5 <= d6)
469
+ closest = c;
470
+ else {
471
+ const vb = d5 * d2 - d1 * d6;
472
+ if (vb <= 0 && d2 >= 0 && d6 <= 0) {
473
+ const w = d2 / (d2 - d6);
474
+ closest = [a[0] + w * ac[0], a[1] + w * ac[1], a[2] + w * ac[2]];
475
+ }
476
+ else {
477
+ const va = d3 * d6 - d5 * d4;
478
+ if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0) {
479
+ const w = (d4 - d3) / (d4 - d3 + (d5 - d6));
480
+ closest = [b[0] + w * (c[0] - b[0]), b[1] + w * (c[1] - b[1]), b[2] + w * (c[2] - b[2])];
481
+ }
482
+ else {
483
+ const denom = 1 / (va + vb + vc);
484
+ const v = vb * denom, w = vc * denom;
485
+ closest = [a[0] + ab[0] * v + ac[0] * w, a[1] + ab[1] * v + ac[1] * w, a[2] + ab[2] * v + ac[2] * w];
486
+ }
487
+ }
488
+ }
489
+ }
490
+ }
491
+ }
492
+ const d = sub(p, closest);
493
+ return dot(d, d);
494
+ }
495
+ /** Squared distance between two segments (Ericson, 5.1.9). */
496
+ function segmentDistanceSq(p1, q1, p2, q2) {
497
+ const d1 = sub(q1, p1), d2 = sub(q2, p2), r = sub(p1, p2);
498
+ const a = dot(d1, d1), e = dot(d2, d2), f = dot(d2, r);
499
+ let s, t;
500
+ if (a <= 0 && e <= 0) {
501
+ s = 0;
502
+ t = 0;
503
+ }
504
+ else if (a <= 0) {
505
+ s = 0;
506
+ t = clamp01(f / e);
507
+ }
508
+ else {
509
+ const c = dot(d1, r);
510
+ if (e <= 0) {
511
+ t = 0;
512
+ s = clamp01(-c / a);
513
+ }
514
+ else {
515
+ const b = dot(d1, d2);
516
+ const denom = a * e - b * b;
517
+ s = denom !== 0 ? clamp01((b * f - c * e) / denom) : 0;
518
+ t = (b * s + f) / e;
519
+ if (t < 0) {
520
+ t = 0;
521
+ s = clamp01(-c / a);
522
+ }
523
+ else if (t > 1) {
524
+ t = 1;
525
+ s = clamp01((b - c) / a);
526
+ }
527
+ }
528
+ }
529
+ const c1 = [p1[0] + d1[0] * s, p1[1] + d1[1] * s, p1[2] + d1[2] * s];
530
+ const c2 = [p2[0] + d2[0] * t, p2[1] + d2[1] * t, p2[2] + d2[2] * t];
531
+ const d = sub(c1, c2);
532
+ return dot(d, d);
533
+ }
534
+ const clamp01 = (value) => (value < 0 ? 0 : value > 1 ? 1 : value);
535
+ /** Which side of a shell's surface points of another shell lie on. */
536
+ class SideClassifier {
537
+ input;
538
+ grid;
539
+ slack;
540
+ columns = new Map();
541
+ visited;
542
+ stamp = 0;
543
+ constructor(input, grid, slack) {
544
+ this.input = input;
545
+ this.grid = grid;
546
+ this.slack = slack;
547
+ this.visited = new Int32Array(input.shellOf.length);
548
+ }
549
+ /**
550
+ * Whether `shell`'s surface has area strictly inside `other`, and strictly
551
+ * outside it. `touching` are its triangles that come within `slack` of
552
+ * `other`: each is sampled, and each connected patch of the rest -- which
553
+ * cannot pass from one side to the other without touching -- is classified
554
+ * from one of its triangles.
555
+ */
556
+ sides(shell, other, touching) {
557
+ const sides = { inside: false, outside: false };
558
+ const { points, corners, neighbours } = this.input;
559
+ const note = (inside) => {
560
+ if (inside === true)
561
+ sides.inside = true;
562
+ else if (inside === false)
563
+ sides.outside = true;
564
+ return sides.inside && sides.outside;
565
+ };
566
+ for (const t of touching) {
567
+ const a = corner(points, corners, t, 0), b = corner(points, corners, t, 1), c = corner(points, corners, t, 2);
568
+ for (const [u, v] of SAMPLES) {
569
+ const w = 1 - u - v;
570
+ const p = [w * a[0] + u * b[0] + v * c[0], w * a[1] + u * b[1] + v * c[1], w * a[2] + u * b[2] + v * c[2]];
571
+ if (this.beyondBounds(p, other)) {
572
+ if (note(false))
573
+ return sides;
574
+ continue;
575
+ }
576
+ if (this.nearSurface(p, other))
577
+ continue;
578
+ if (note(this.inside(p, other)))
579
+ return sides;
580
+ }
581
+ }
582
+ // Only the part of this shell within the other's bounds can be inside it.
583
+ // A corner beyond those bounds is outside, and so is every patch that
584
+ // reaches one; flooding stops there instead of crossing the whole shell,
585
+ // which made a plate with hundreds of parts resting on it quadratic.
586
+ const { bounds } = this.input;
587
+ const s = 6 * shell, o = 6 * other, slack = this.slack;
588
+ for (let axis = 0; axis < 3; axis += 1) {
589
+ if (bounds[s + axis] < bounds[o + axis] - slack || bounds[s + 3 + axis] > bounds[o + 3 + axis] + slack) {
590
+ if (note(false))
591
+ return sides;
592
+ break;
593
+ }
594
+ }
595
+ const region = this.trianglesNear(shell, other);
596
+ const regionStamp = this.stamp;
597
+ this.stamp += 1;
598
+ for (const seed of region) {
599
+ if (touching.has(seed) || this.visited[seed] === this.stamp)
600
+ continue;
601
+ const patch = [seed];
602
+ this.visited[seed] = this.stamp;
603
+ let leavesRegion = false;
604
+ for (let at = 0; at < patch.length && !leavesRegion; at += 1) {
605
+ for (let k = 0; k < 3; k += 1) {
606
+ const next = neighbours[3 * patch[at] + k];
607
+ if (next < 0 || touching.has(next) || this.visited[next] === this.stamp)
608
+ continue;
609
+ if (this.visited[next] !== regionStamp) {
610
+ leavesRegion = true;
611
+ break;
612
+ }
613
+ this.visited[next] = this.stamp;
614
+ patch.push(next);
615
+ }
616
+ }
617
+ if (leavesRegion) {
618
+ if (note(false))
619
+ return sides;
620
+ continue;
621
+ }
622
+ // One clean ray settles the patch; a ray grazing an edge is retried from another triangle.
623
+ for (const t of patch) {
624
+ const a = corner(points, corners, t, 0), b = corner(points, corners, t, 1), c = corner(points, corners, t, 2);
625
+ const inside = this.inside([(a[0] + b[0] + c[0]) / 3, (a[1] + b[1] + c[1]) / 3, (a[2] + b[2] + c[2]) / 3], other);
626
+ if (inside === null)
627
+ continue;
628
+ if (note(inside))
629
+ return sides;
630
+ break;
631
+ }
632
+ }
633
+ return sides;
634
+ }
635
+ /**
636
+ * The triangles of `shell` whose bounds meet `other`'s, found through the
637
+ * grid cells both shells' bounds share, and marked with a fresh stamp.
638
+ */
639
+ trianglesNear(shell, other) {
640
+ const { origin, size, count, cells } = this.grid;
641
+ const { points, corners, shellOf, bounds } = this.input;
642
+ const slack = this.slack, s = 6 * shell, o = 6 * other;
643
+ const low = [0, 1, 2].map((axis) => Math.max(bounds[s + axis], bounds[o + axis]) - slack);
644
+ const high = [0, 1, 2].map((axis) => Math.min(bounds[s + 3 + axis], bounds[o + 3 + axis]) + slack);
645
+ this.stamp += 1;
646
+ const found = [];
647
+ if (low[0] > high[0] || low[1] > high[1] || low[2] > high[2])
648
+ return found;
649
+ const box = new Float64Array(6);
650
+ const i0 = cellIndex(origin, size, count, 0, low[0]), i1 = cellIndex(origin, size, count, 0, high[0]);
651
+ const j0 = cellIndex(origin, size, count, 1, low[1]), j1 = cellIndex(origin, size, count, 1, high[1]);
652
+ const k0 = cellIndex(origin, size, count, 2, low[2]), k1 = cellIndex(origin, size, count, 2, high[2]);
653
+ for (let i = i0; i <= i1; i += 1) {
654
+ for (let j = j0; j <= j1; j += 1) {
655
+ for (let k = k0; k <= k1; k += 1) {
656
+ for (const t of cells.get(i + count[0] * (j + count[1] * k)) ?? []) {
657
+ if (shellOf[t] !== shell || this.visited[t] === this.stamp)
658
+ continue;
659
+ triangleBox(points, corners, t, box);
660
+ if (box[0] > high[0] || box[1] > high[1] || box[2] > high[2] || box[3] < low[0] || box[4] < low[1] || box[5] < low[2])
661
+ continue;
662
+ this.visited[t] = this.stamp;
663
+ found.push(t);
664
+ }
665
+ }
666
+ }
667
+ }
668
+ return found;
669
+ }
670
+ /** Whether `p` lies more than `slack` outside `shell`'s bounds, and so outside it. */
671
+ beyondBounds(p, shell) {
672
+ const b = 6 * shell, bounds = this.input.bounds, slack = this.slack;
673
+ for (let axis = 0; axis < 3; axis += 1) {
674
+ if (p[axis] < bounds[b + axis] - slack || p[axis] > bounds[b + 3 + axis] + slack)
675
+ return true;
676
+ }
677
+ return false;
678
+ }
679
+ /** Whether `p` lies within `slack` of `shell`'s surface. */
680
+ nearSurface(p, shell) {
681
+ const { origin, size, count, cells } = this.grid;
682
+ const { points, corners, shellOf } = this.input;
683
+ const reach = this.slack * this.slack;
684
+ const i0 = cellIndex(origin, size, count, 0, p[0] - this.slack), i1 = cellIndex(origin, size, count, 0, p[0] + this.slack);
685
+ const j0 = cellIndex(origin, size, count, 1, p[1] - this.slack), j1 = cellIndex(origin, size, count, 1, p[1] + this.slack);
686
+ const k0 = cellIndex(origin, size, count, 2, p[2] - this.slack), k1 = cellIndex(origin, size, count, 2, p[2] + this.slack);
687
+ for (let i = i0; i <= i1; i += 1) {
688
+ for (let j = j0; j <= j1; j += 1) {
689
+ for (let k = k0; k <= k1; k += 1) {
690
+ for (const t of cells.get(i + count[0] * (j + count[1] * k)) ?? []) {
691
+ if (shellOf[t] !== shell)
692
+ continue;
693
+ const tri = [corner(points, corners, t, 0), corner(points, corners, t, 1), corner(points, corners, t, 2)];
694
+ if (pointTriangleDistanceSq(p, tri) <= reach)
695
+ return true;
696
+ }
697
+ }
698
+ }
699
+ }
700
+ return false;
701
+ }
702
+ /** Ray parity of `p` against `shell`, or `null` when the ray grazes an edge and settles nothing. */
703
+ inside(p, shell) {
704
+ // Beyond the shell's bounds is outside it, with no ray to cast.
705
+ const b = 6 * shell, bounds = this.input.bounds;
706
+ for (let axis = 0; axis < 3; axis += 1) {
707
+ if (p[axis] < bounds[b + axis] || p[axis] > bounds[b + 3 + axis])
708
+ return false;
709
+ }
710
+ const column = this.columnsOf(shell);
711
+ const u = dot(p, ACROSS[0]), v = dot(p, ACROSS[1]);
712
+ const i = Math.floor((u - column.lowU) / column.size), j = Math.floor((v - column.lowV) / column.size);
713
+ if (i < 0 || j < 0 || i >= column.count || j >= column.count)
714
+ return false;
715
+ let crossings = 0;
716
+ for (const t of column.cells.get(i + column.count * j) ?? []) {
717
+ const hit = rayHit(p, this.input.points, this.input.corners, t);
718
+ if (hit === null)
719
+ return null;
720
+ if (hit)
721
+ crossings += 1;
722
+ }
723
+ return crossings % 2 === 1;
724
+ }
725
+ /** The shell's triangles binned by where they fall seen along RAY, built once per shell. */
726
+ columnsOf(shell) {
727
+ const known = this.columns.get(shell);
728
+ if (known)
729
+ return known;
730
+ const { points, corners } = this.input;
731
+ const triangles = this.input.shells[shell];
732
+ const projected = new Float64Array(triangles.length * 4);
733
+ let lowU = Infinity, lowV = Infinity, highU = -Infinity, highV = -Infinity;
734
+ triangles.forEach((t, index) => {
735
+ let u0 = Infinity, v0 = Infinity, u1 = -Infinity, v1 = -Infinity;
736
+ for (let k = 0; k < 3; k += 1) {
737
+ const c = corner(points, corners, t, k);
738
+ const u = dot(c, ACROSS[0]), v = dot(c, ACROSS[1]);
739
+ u0 = Math.min(u0, u);
740
+ u1 = Math.max(u1, u);
741
+ v0 = Math.min(v0, v);
742
+ v1 = Math.max(v1, v);
743
+ }
744
+ projected.set([u0, v0, u1, v1], 4 * index);
745
+ lowU = Math.min(lowU, u0);
746
+ highU = Math.max(highU, u1);
747
+ lowV = Math.min(lowV, v0);
748
+ highV = Math.max(highV, v1);
749
+ });
750
+ const count = Math.max(1, Math.min(1024, Math.ceil(Math.sqrt(triangles.length))));
751
+ const size = Math.max(highU - lowU, highV - lowV, Number.MIN_VALUE) / count * (1 + 1e-9);
752
+ const cells = new Map();
753
+ triangles.forEach((t, index) => {
754
+ const i0 = Math.floor((projected[4 * index] - lowU) / size), i1 = Math.min(count - 1, Math.floor((projected[4 * index + 2] - lowU) / size));
755
+ const j0 = Math.floor((projected[4 * index + 1] - lowV) / size), j1 = Math.min(count - 1, Math.floor((projected[4 * index + 3] - lowV) / size));
756
+ for (let i = i0; i <= i1; i += 1) {
757
+ for (let j = j0; j <= j1; j += 1) {
758
+ const key = i + count * j;
759
+ const cell = cells.get(key);
760
+ if (cell)
761
+ cell.push(t);
762
+ else
763
+ cells.set(key, [t]);
764
+ }
765
+ }
766
+ });
767
+ const grid = { lowU, lowV, size, count, cells };
768
+ this.columns.set(shell, grid);
769
+ return grid;
770
+ }
771
+ }
772
+ /** Relative margin inside which a ray is taken to graze a triangle's edge. */
773
+ const GRAZE = 1e-9;
774
+ /** Möller-Trumbore along RAY from `p`: a hit ahead of `p`, a miss, or `null` for a ray through an edge or corner. */
775
+ function rayHit(p, points, corners, t) {
776
+ const a = corner(points, corners, t, 0);
777
+ const e1 = sub(corner(points, corners, t, 1), a), e2 = sub(corner(points, corners, t, 2), a);
778
+ const h = cross(RAY, e2);
779
+ const det = dot(e1, h);
780
+ // Seen edge-on: a ray from off the surface cannot pass through it.
781
+ if (Math.abs(det) <= 1e-12 * Math.hypot(...e1) * Math.hypot(...e2))
782
+ return false;
783
+ const inv = 1 / det;
784
+ const s = sub(p, a);
785
+ const u = dot(s, h) * inv;
786
+ const q = cross(s, e1);
787
+ const v = dot(RAY, q) * inv;
788
+ if (u < -GRAZE || v < -GRAZE || u + v > 1 + GRAZE)
789
+ return false;
790
+ const distance = dot(e2, q) * inv;
791
+ if (distance <= 0)
792
+ return false;
793
+ if (u < GRAZE || v < GRAZE || u + v > 1 - GRAZE)
794
+ return null;
795
+ return true;
796
+ }
797
+ //# sourceMappingURL=meshShellOverlap.js.map