@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.
- package/dist/cadAnalysis.d.ts +94 -0
- package/dist/cadAnalysis.d.ts.map +1 -0
- package/dist/cadAnalysis.js +157 -0
- package/dist/cadAnalysis.js.map +1 -0
- package/dist/cadBakeMetadata.d.ts +11 -2
- package/dist/cadBakeMetadata.d.ts.map +1 -1
- package/dist/cadBakeMetadata.js +9 -0
- package/dist/cadBakeMetadata.js.map +1 -1
- package/dist/cadProjection.d.ts.map +1 -1
- package/dist/cadProjection.js.map +1 -1
- package/dist/cadSection.d.ts.map +1 -1
- package/dist/cadShell.d.ts +143 -0
- package/dist/cadShell.d.ts.map +1 -0
- package/dist/cadShell.js +313 -0
- package/dist/cadShell.js.map +1 -0
- package/dist/cadSolids.d.ts.map +1 -1
- package/dist/cadSolids.js +18 -1
- package/dist/cadSolids.js.map +1 -1
- package/dist/cutBrepFrameMigration.d.ts +46 -0
- package/dist/cutBrepFrameMigration.d.ts.map +1 -0
- package/dist/cutBrepFrameMigration.js +105 -0
- package/dist/cutBrepFrameMigration.js.map +1 -0
- package/dist/cutterPadding.d.ts.map +1 -1
- package/dist/domain/cameraProjection.js.map +1 -1
- package/dist/domain/sketchforge.d.ts +31 -1
- package/dist/domain/sketchforge.d.ts.map +1 -1
- package/dist/editorHistory.d.ts.map +1 -1
- package/dist/editorHistory.js.map +1 -1
- package/dist/engine.d.ts +3 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +156 -78
- package/dist/engine.js.map +1 -1
- package/dist/exactBooleanParts.d.ts +4 -2
- package/dist/exactBooleanParts.d.ts.map +1 -1
- package/dist/exactBooleanParts.js +7 -3
- package/dist/exactBooleanParts.js.map +1 -1
- package/dist/exactBooleanSolids.d.ts.map +1 -1
- package/dist/gearProfile.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/loftProfile.d.ts +1 -1
- package/dist/loftProfile.d.ts.map +1 -1
- package/dist/loftProfile.js.map +1 -1
- package/dist/meshEdgeProjection.d.ts +104 -0
- package/dist/meshEdgeProjection.d.ts.map +1 -0
- package/dist/meshEdgeProjection.js +439 -0
- package/dist/meshEdgeProjection.js.map +1 -0
- package/dist/meshImport.d.ts.map +1 -1
- package/dist/meshImport.js +2 -20
- package/dist/meshImport.js.map +1 -1
- package/dist/meshMassProperties.d.ts +92 -0
- package/dist/meshMassProperties.d.ts.map +1 -0
- package/dist/meshMassProperties.js +570 -0
- package/dist/meshMassProperties.js.map +1 -0
- package/dist/meshSection.d.ts +44 -0
- package/dist/meshSection.d.ts.map +1 -0
- package/dist/meshSection.js +154 -0
- package/dist/meshSection.js.map +1 -0
- package/dist/meshShellOverlap.d.ts +75 -0
- package/dist/meshShellOverlap.d.ts.map +1 -0
- package/dist/meshShellOverlap.js +797 -0
- package/dist/meshShellOverlap.js.map +1 -0
- package/dist/meshShellSolids.d.ts +44 -0
- package/dist/meshShellSolids.d.ts.map +1 -0
- package/dist/meshShellSolids.js +116 -0
- package/dist/meshShellSolids.js.map +1 -0
- package/dist/nodeSvgDomParser.d.ts +3 -0
- package/dist/nodeSvgDomParser.d.ts.map +1 -0
- package/dist/nodeSvgDomParser.js +53 -0
- package/dist/nodeSvgDomParser.js.map +1 -0
- package/dist/parametricRecompute.d.ts.map +1 -1
- package/dist/parametricRecompute.js +63 -10
- package/dist/parametricRecompute.js.map +1 -1
- package/dist/placementWorkplane.d.ts.map +1 -1
- package/dist/placementWorkplane.js.map +1 -1
- package/dist/projectAssets.d.ts +1 -1
- package/dist/projectStore.d.ts +1 -1
- package/dist/projectStore.d.ts.map +1 -1
- package/dist/projectStore.js +23 -2
- package/dist/projectStore.js.map +1 -1
- package/dist/revolveRequests.d.ts +57 -1
- package/dist/revolveRequests.d.ts.map +1 -1
- package/dist/revolveRequests.js +103 -4
- package/dist/revolveRequests.js.map +1 -1
- package/dist/sceneExport.d.ts +6 -0
- package/dist/sceneExport.d.ts.map +1 -1
- package/dist/sceneExport.js +6 -2
- package/dist/sceneExport.js.map +1 -1
- package/dist/serverCadInterference.d.ts +8 -45
- package/dist/serverCadInterference.d.ts.map +1 -1
- package/dist/serverCadInterference.js +21 -43
- package/dist/serverCadInterference.js.map +1 -1
- package/dist/serverCadOperations.d.ts +73 -4
- package/dist/serverCadOperations.d.ts.map +1 -1
- package/dist/serverCadOperations.js +356 -71
- package/dist/serverCadOperations.js.map +1 -1
- package/dist/serverCadProjection.d.ts +14 -1
- package/dist/serverCadProjection.d.ts.map +1 -1
- package/dist/serverCadProjection.js +135 -13
- package/dist/serverCadProjection.js.map +1 -1
- package/dist/serverCadProperties.d.ts +14 -7
- package/dist/serverCadProperties.d.ts.map +1 -1
- package/dist/serverCadProperties.js +60 -24
- package/dist/serverCadProperties.js.map +1 -1
- package/dist/serverCadSection.d.ts +21 -1
- package/dist/serverCadSection.d.ts.map +1 -1
- package/dist/serverCadSection.js +157 -43
- package/dist/serverCadSection.js.map +1 -1
- package/dist/serverCadShell.d.ts +7 -94
- package/dist/serverCadShell.d.ts.map +1 -1
- package/dist/serverCadShell.js +25 -272
- package/dist/serverCadShell.js.map +1 -1
- package/dist/serverCadThickness.d.ts +8 -8
- package/dist/serverCadThickness.d.ts.map +1 -1
- package/dist/serverCadThickness.js +15 -42
- package/dist/serverCadThickness.js.map +1 -1
- package/dist/serverShapeBooleans.d.ts +75 -0
- package/dist/serverShapeBooleans.d.ts.map +1 -1
- package/dist/serverShapeBooleans.js +251 -9
- package/dist/serverShapeBooleans.js.map +1 -1
- package/dist/serverShapeGroups.js.map +1 -1
- package/dist/serverStepExplode.d.ts +1 -2
- package/dist/serverStepExplode.d.ts.map +1 -1
- package/dist/serverStepExplode.js +15 -148
- package/dist/serverStepExplode.js.map +1 -1
- package/dist/shapeBounds.d.ts.map +1 -1
- package/dist/shapeCatalog.d.ts.map +1 -1
- package/dist/shapeCatalog.js +1 -0
- package/dist/shapeCatalog.js.map +1 -1
- package/dist/shapeRequests.d.ts +13 -0
- package/dist/shapeRequests.d.ts.map +1 -1
- package/dist/shapeRequests.js +14 -0
- package/dist/shapeRequests.js.map +1 -1
- package/dist/sketchCadProfile.d.ts +13 -0
- package/dist/sketchCadProfile.d.ts.map +1 -1
- package/dist/sketchCadProfile.js +108 -0
- package/dist/sketchCadProfile.js.map +1 -1
- package/dist/sketchCadSweep.js.map +1 -1
- package/dist/sketchEdgeSegmentLineage.d.ts +32 -0
- package/dist/sketchEdgeSegmentLineage.d.ts.map +1 -0
- package/dist/sketchEdgeSegmentLineage.js +49 -0
- package/dist/sketchEdgeSegmentLineage.js.map +1 -0
- package/dist/sketchEdgeSegmentLineageKernel.d.ts +83 -0
- package/dist/sketchEdgeSegmentLineageKernel.d.ts.map +1 -0
- package/dist/sketchEdgeSegmentLineageKernel.js +125 -0
- package/dist/sketchEdgeSegmentLineageKernel.js.map +1 -0
- package/dist/sketchFaceDraftKernel.d.ts +16 -9
- package/dist/sketchFaceDraftKernel.d.ts.map +1 -1
- package/dist/sketchFaceDraftKernel.js +19 -11
- package/dist/sketchFaceDraftKernel.js.map +1 -1
- package/dist/sketchRevolve.d.ts +13 -0
- package/dist/sketchRevolve.d.ts.map +1 -1
- package/dist/sketchRevolve.js +4 -1
- package/dist/sketchRevolve.js.map +1 -1
- package/dist/skfProject.d.ts +1 -1
- package/dist/skfProject.d.ts.map +1 -1
- package/dist/skfProject.js +63 -4
- package/dist/skfProject.js.map +1 -1
- package/dist/sphereTessellation.d.ts.map +1 -1
- package/dist/stepAssemblyExplode.d.ts +45 -0
- package/dist/stepAssemblyExplode.d.ts.map +1 -0
- package/dist/stepAssemblyExplode.js +195 -0
- package/dist/stepAssemblyExplode.js.map +1 -0
- package/dist/stepAssemblyHierarchy.js.map +1 -1
- package/dist/stepComponentMetadata.d.ts +1 -1
- package/dist/stepComponentMetadata.d.ts.map +1 -1
- package/dist/stepComponentMetadata.js +93 -39
- package/dist/stepComponentMetadata.js.map +1 -1
- package/dist/stepExport.d.ts +14 -1
- package/dist/stepExport.d.ts.map +1 -1
- package/dist/stepExport.js +81 -32
- package/dist/stepExport.js.map +1 -1
- package/dist/stepImportCommon.d.ts +4 -1
- package/dist/stepImportCommon.d.ts.map +1 -1
- package/dist/stepImportCommon.js +7 -6
- package/dist/stepImportCommon.js.map +1 -1
- package/dist/stlImport.js.map +1 -1
- package/dist/support/cadModifierTypes.d.ts +100 -1
- package/dist/support/cadModifierTypes.d.ts.map +1 -1
- package/dist/support/i18n/LocalizedError.d.ts.map +1 -1
- package/dist/support/i18n/locales/en/messages.json +42 -2
- package/dist/support/i18n/locales.d.ts.map +1 -1
- package/dist/support/lib/appTheme.d.ts.map +1 -1
- package/dist/support/lib/materialPresets.js.map +1 -1
- package/dist/support/lib/measurementUnits.d.ts.map +1 -1
- package/dist/support/lib/operator-configuration.json +13 -1
- package/dist/support/lib/operatorConfiguration.d.ts +5 -5
- package/dist/support/lib/workplaneGrid.js.map +1 -1
- package/dist/svgImport.d.ts +2 -0
- package/dist/svgImport.d.ts.map +1 -1
- package/dist/svgImport.js +7 -0
- package/dist/svgImport.js.map +1 -1
- package/dist/threadCatalog.js.map +1 -1
- package/dist/workplaneSettings.js.map +1 -1
- package/dist/workplaneShapes.d.ts +2 -2
- package/dist/workplaneShapes.d.ts.map +1 -1
- package/dist/workplaneShapes.js.map +1 -1
- 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
|