@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,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mass properties of a triangle mesh, straight from its triangles (#508).
|
|
3
|
+
*
|
|
4
|
+
* Sewing an imported mesh into an OCCT solid only to ask the kernel for its
|
|
5
|
+
* volume took over a minute on a 57k-triangle STL, blocked every other call
|
|
6
|
+
* while it ran, and reported the answer as tessellated anyway. The divergence
|
|
7
|
+
* theorem gives the same numbers from the triangles in one pass.
|
|
8
|
+
*
|
|
9
|
+
* The mesh may hold several shells, and some of them may be voids. Each shell's
|
|
10
|
+
* triangles are first wound consistently with one another; whether the shell
|
|
11
|
+
* then counts as material or as a cavity is decided by how many other shells
|
|
12
|
+
* enclose it (even: material, odd: void), not by the file's winding, which
|
|
13
|
+
* imported meshes get wrong often enough that OCCT's path had to repair it.
|
|
14
|
+
* That reading needs the shells to nest; two bodies overlapping without one
|
|
15
|
+
* enclosing the other would have the overlap counted twice, so a mesh holding
|
|
16
|
+
* them gets no answer here (#529, see meshShellOverlap.ts).
|
|
17
|
+
*
|
|
18
|
+
* Only a mesh whose every shell is closed gets an answer. The signed volume of
|
|
19
|
+
* an open fragment depends on where the origin is, so there is no honest number
|
|
20
|
+
* to give for one; `null` sends the caller back to the kernel's sewing, which
|
|
21
|
+
* can close gaps this does not and refuses what it cannot close.
|
|
22
|
+
*
|
|
23
|
+
* Pure arithmetic, no kernel: safe to call from anywhere.
|
|
24
|
+
*/
|
|
25
|
+
import type { MeshData } from "./meshTransform.js";
|
|
26
|
+
export type MeshMassProperties = {
|
|
27
|
+
volume: number;
|
|
28
|
+
area: number;
|
|
29
|
+
centerOfMass: {
|
|
30
|
+
x: number;
|
|
31
|
+
y: number;
|
|
32
|
+
z: number;
|
|
33
|
+
};
|
|
34
|
+
bbox: {
|
|
35
|
+
xmin: number;
|
|
36
|
+
ymin: number;
|
|
37
|
+
zmin: number;
|
|
38
|
+
xmax: number;
|
|
39
|
+
ymax: number;
|
|
40
|
+
zmax: number;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* The mesh's mass properties, or `null` when its triangles do not bound a
|
|
45
|
+
* solid at any weld tolerance.
|
|
46
|
+
*
|
|
47
|
+
* Throws `MeshShellsOverlapError` when two of its closed bodies overlap without
|
|
48
|
+
* one enclosing the other (#529): adding them would count the overlap twice,
|
|
49
|
+
* and sewing them only for the kernel to refuse them held it for minutes on a
|
|
50
|
+
* large mesh, so the refusal comes from here.
|
|
51
|
+
*/
|
|
52
|
+
export declare function meshMassProperties(mesh: MeshData): MeshMassProperties | null;
|
|
53
|
+
/**
|
|
54
|
+
* Whether two of the mesh's closed shells overlap without one enclosing the
|
|
55
|
+
* other; `null` when the mesh does not close, so it has no shells to compare.
|
|
56
|
+
* Shells that only touch -- on a face, an edge or a corner -- do not overlap:
|
|
57
|
+
* what they share has no volume.
|
|
58
|
+
*/
|
|
59
|
+
export declare function meshShellsOverlap(mesh: MeshData): boolean | null;
|
|
60
|
+
/** One closed shell of a mesh, with its triangles wound as the mesh wound them. */
|
|
61
|
+
export type MeshSolidShell = {
|
|
62
|
+
/** Flat triangle corners in the mesh's own coordinates. */
|
|
63
|
+
positions: number[];
|
|
64
|
+
/** How many other shells enclose it: even is material, odd is a void. */
|
|
65
|
+
depth: number;
|
|
66
|
+
/**
|
|
67
|
+
* The volume it contributes to the mesh: positive for a body, negative for a
|
|
68
|
+
* void. Their sum is the mesh's own volume, which is what a solid assembled
|
|
69
|
+
* from the shells must come to if the nesting was read correctly.
|
|
70
|
+
*/
|
|
71
|
+
volume: number;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* The closed shells a mesh is made of, each with its nesting depth (#501).
|
|
75
|
+
*
|
|
76
|
+
* OCCT sews one shell into one solid; a mesh with a sealed internal void, or
|
|
77
|
+
* with several separate bodies, has more than one, and sewing them together
|
|
78
|
+
* fails. A caller builds each shell into a solid and puts them back together:
|
|
79
|
+
* material at even depth, voids at odd depth taken out of what encloses them.
|
|
80
|
+
* The winding is left alone, since the depth -- not the winding -- says which
|
|
81
|
+
* side is material, and the sewing orients each solid outward by itself.
|
|
82
|
+
*
|
|
83
|
+
* The shells are those of the first weld tolerance that closes the mesh, so
|
|
84
|
+
* their corners are the welded ones. Null unless every shell is closed and
|
|
85
|
+
* consistently orientable, because only then is the nesting well defined. A
|
|
86
|
+
* shell enclosing no volume is left out. Throws `MeshShellsOverlapError` when
|
|
87
|
+
* two shells overlap without nesting, since no nesting assembles them.
|
|
88
|
+
*/
|
|
89
|
+
export declare function meshSolidShells(mesh: MeshData): MeshSolidShell[] | null;
|
|
90
|
+
/** Flat triangle corners, as a stored mesh holds them, as `MeshData`. */
|
|
91
|
+
export declare function meshDataFromPositions(positions: readonly number[]): MeshData;
|
|
92
|
+
//# sourceMappingURL=meshMassProperties.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meshMassProperties.d.ts","sourceRoot":"","sources":["../src/meshMassProperties.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAGnD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAClD,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9F,CAAC;AAsBF;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,GAAG,kBAAkB,GAAG,IAAI,CAI5E;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,IAAI,CAGhE;AAED,mFAAmF;AACnF,MAAM,MAAM,cAAc,GAAG;IAC3B,2DAA2D;IAC3D,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,cAAc,EAAE,GAAG,IAAI,CAgBvE;AAED,yEAAyE;AACzE,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,QAAQ,CAY5E"}
|
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mass properties of a triangle mesh, straight from its triangles (#508).
|
|
3
|
+
*
|
|
4
|
+
* Sewing an imported mesh into an OCCT solid only to ask the kernel for its
|
|
5
|
+
* volume took over a minute on a 57k-triangle STL, blocked every other call
|
|
6
|
+
* while it ran, and reported the answer as tessellated anyway. The divergence
|
|
7
|
+
* theorem gives the same numbers from the triangles in one pass.
|
|
8
|
+
*
|
|
9
|
+
* The mesh may hold several shells, and some of them may be voids. Each shell's
|
|
10
|
+
* triangles are first wound consistently with one another; whether the shell
|
|
11
|
+
* then counts as material or as a cavity is decided by how many other shells
|
|
12
|
+
* enclose it (even: material, odd: void), not by the file's winding, which
|
|
13
|
+
* imported meshes get wrong often enough that OCCT's path had to repair it.
|
|
14
|
+
* That reading needs the shells to nest; two bodies overlapping without one
|
|
15
|
+
* enclosing the other would have the overlap counted twice, so a mesh holding
|
|
16
|
+
* them gets no answer here (#529, see meshShellOverlap.ts).
|
|
17
|
+
*
|
|
18
|
+
* Only a mesh whose every shell is closed gets an answer. The signed volume of
|
|
19
|
+
* an open fragment depends on where the origin is, so there is no honest number
|
|
20
|
+
* to give for one; `null` sends the caller back to the kernel's sewing, which
|
|
21
|
+
* can close gaps this does not and refuses what it cannot close.
|
|
22
|
+
*
|
|
23
|
+
* Pure arithmetic, no kernel: safe to call from anywhere.
|
|
24
|
+
*/
|
|
25
|
+
import { MeshShellsOverlapError, relateTouchingShells, shellPairKey } from "./meshShellOverlap.js";
|
|
26
|
+
/**
|
|
27
|
+
* Weld tolerances tried in turn, in mm, after a near-exact weld. They follow
|
|
28
|
+
* the sewing ladder `solidFromMeshPositions` uses, so a mesh whose corners miss
|
|
29
|
+
* by a micron -- as a mesh boolean's output can -- closes here as it did there.
|
|
30
|
+
*/
|
|
31
|
+
const WELD_LADDER_MM = [1e-5, 1e-4, 1e-3, 1e-2];
|
|
32
|
+
/**
|
|
33
|
+
* Two shells closer than this fraction of the mesh's largest coordinate touch
|
|
34
|
+
* rather than overlap or stand apart: sixteen float32 units in the last place.
|
|
35
|
+
*/
|
|
36
|
+
const FLOAT32_CONTACT = 16 * 2 ** -24;
|
|
37
|
+
/** A direction no axis-aligned or 45-degree feature lines up with, so a ray along it rarely grazes an edge. */
|
|
38
|
+
const RAY = (() => {
|
|
39
|
+
const d = [0.5773502691896258 + 0.1234567, 0.5773502691896258 + 0.0456789, 0.5773502691896258 - 0.0789123];
|
|
40
|
+
const length = Math.hypot(d[0], d[1], d[2]);
|
|
41
|
+
return [d[0] / length, d[1] / length, d[2] / length];
|
|
42
|
+
})();
|
|
43
|
+
/**
|
|
44
|
+
* The mesh's mass properties, or `null` when its triangles do not bound a
|
|
45
|
+
* solid at any weld tolerance.
|
|
46
|
+
*
|
|
47
|
+
* Throws `MeshShellsOverlapError` when two of its closed bodies overlap without
|
|
48
|
+
* one enclosing the other (#529): adding them would count the overlap twice,
|
|
49
|
+
* and sewing them only for the kernel to refuse them held it for minutes on a
|
|
50
|
+
* large mesh, so the refusal comes from here.
|
|
51
|
+
*/
|
|
52
|
+
export function meshMassProperties(mesh) {
|
|
53
|
+
const analysis = analyseMesh(mesh);
|
|
54
|
+
if (analysis?.overlapping)
|
|
55
|
+
throw new MeshShellsOverlapError();
|
|
56
|
+
return analysis ? analysis.properties : null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Whether two of the mesh's closed shells overlap without one enclosing the
|
|
60
|
+
* other; `null` when the mesh does not close, so it has no shells to compare.
|
|
61
|
+
* Shells that only touch -- on a face, an edge or a corner -- do not overlap:
|
|
62
|
+
* what they share has no volume.
|
|
63
|
+
*/
|
|
64
|
+
export function meshShellsOverlap(mesh) {
|
|
65
|
+
const analysis = analyseMesh(mesh);
|
|
66
|
+
return analysis ? analysis.overlapping : null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The closed shells a mesh is made of, each with its nesting depth (#501).
|
|
70
|
+
*
|
|
71
|
+
* OCCT sews one shell into one solid; a mesh with a sealed internal void, or
|
|
72
|
+
* with several separate bodies, has more than one, and sewing them together
|
|
73
|
+
* fails. A caller builds each shell into a solid and puts them back together:
|
|
74
|
+
* material at even depth, voids at odd depth taken out of what encloses them.
|
|
75
|
+
* The winding is left alone, since the depth -- not the winding -- says which
|
|
76
|
+
* side is material, and the sewing orients each solid outward by itself.
|
|
77
|
+
*
|
|
78
|
+
* The shells are those of the first weld tolerance that closes the mesh, so
|
|
79
|
+
* their corners are the welded ones. Null unless every shell is closed and
|
|
80
|
+
* consistently orientable, because only then is the nesting well defined. A
|
|
81
|
+
* shell enclosing no volume is left out. Throws `MeshShellsOverlapError` when
|
|
82
|
+
* two shells overlap without nesting, since no nesting assembles them.
|
|
83
|
+
*/
|
|
84
|
+
export function meshSolidShells(mesh) {
|
|
85
|
+
const analysis = analyseMesh(mesh);
|
|
86
|
+
if (!analysis)
|
|
87
|
+
return null;
|
|
88
|
+
if (analysis.overlapping)
|
|
89
|
+
throw new MeshShellsOverlapError();
|
|
90
|
+
const { shells, depth, contribution, corners, points, origin } = analysis;
|
|
91
|
+
return shells.flatMap((members, shell) => {
|
|
92
|
+
if (depth[shell] < 0)
|
|
93
|
+
return [];
|
|
94
|
+
const positions = [];
|
|
95
|
+
for (const t of members) {
|
|
96
|
+
for (let k = 0; k < 3; k += 1) {
|
|
97
|
+
const index = 3 * corners[3 * t + k];
|
|
98
|
+
positions.push(points[index] + origin[0], points[index + 1] + origin[1], points[index + 2] + origin[2]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return [{ positions, depth: depth[shell], volume: contribution[shell] }];
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/** Flat triangle corners, as a stored mesh holds them, as `MeshData`. */
|
|
105
|
+
export function meshDataFromPositions(positions) {
|
|
106
|
+
const vertices = [];
|
|
107
|
+
const faces = [];
|
|
108
|
+
for (let index = 0; index + 8 < positions.length; index += 9) {
|
|
109
|
+
const first = vertices.length;
|
|
110
|
+
for (let corner = 0; corner < 3; corner += 1) {
|
|
111
|
+
const at = index + corner * 3;
|
|
112
|
+
vertices.push([positions[at], positions[at + 1], positions[at + 2]]);
|
|
113
|
+
}
|
|
114
|
+
faces.push([first, first + 1, first + 2]);
|
|
115
|
+
}
|
|
116
|
+
return { name: "mesh", vertices, faces };
|
|
117
|
+
}
|
|
118
|
+
/** The mesh welded at the first tolerance that closes it, measured, with the shells it found. */
|
|
119
|
+
function analyseMesh(mesh) {
|
|
120
|
+
if (!mesh.vertices.length || !mesh.faces.length)
|
|
121
|
+
return null;
|
|
122
|
+
// Bounds, from every vertex a triangle uses.
|
|
123
|
+
let xmin = Infinity, ymin = Infinity, zmin = Infinity;
|
|
124
|
+
let xmax = -Infinity, ymax = -Infinity, zmax = -Infinity;
|
|
125
|
+
for (const face of mesh.faces) {
|
|
126
|
+
for (const index of face) {
|
|
127
|
+
const vertex = mesh.vertices[index];
|
|
128
|
+
if (!vertex)
|
|
129
|
+
continue;
|
|
130
|
+
const [x, y, z] = vertex;
|
|
131
|
+
if (x < xmin)
|
|
132
|
+
xmin = x;
|
|
133
|
+
if (x > xmax)
|
|
134
|
+
xmax = x;
|
|
135
|
+
if (y < ymin)
|
|
136
|
+
ymin = y;
|
|
137
|
+
if (y > ymax)
|
|
138
|
+
ymax = y;
|
|
139
|
+
if (z < zmin)
|
|
140
|
+
zmin = z;
|
|
141
|
+
if (z > zmax)
|
|
142
|
+
zmax = z;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!Number.isFinite(xmin + ymin + zmin + xmax + ymax + zmax))
|
|
146
|
+
return null;
|
|
147
|
+
const bbox = { xmin, ymin, zmin, xmax, ymax, zmax };
|
|
148
|
+
// Corners measured from the bounds' centre to keep the products small.
|
|
149
|
+
const origin = [(xmin + xmax) / 2, (ymin + ymax) / 2, (zmin + zmax) / 2];
|
|
150
|
+
let welded = cornerPoints(mesh, origin);
|
|
151
|
+
const tight = Math.max(1, Math.hypot(xmax - xmin, ymax - ymin, zmax - zmin)) * 1e-9;
|
|
152
|
+
// How far apart two shells may be and still touch. An STL stores float32
|
|
153
|
+
// corners, rounded by up to half a unit in the last place of the coordinate's
|
|
154
|
+
// own size -- about 6e-8 of it, 1e-5 mm at 200 mm -- so a part resting on
|
|
155
|
+
// another, once rotated or moved, sits that far above or below it: far more
|
|
156
|
+
// than the weld's near-exact step, and far less than any real overlap.
|
|
157
|
+
const reach = Math.max(Math.abs(xmin), Math.abs(xmax), Math.abs(ymin), Math.abs(ymax), Math.abs(zmin), Math.abs(zmax));
|
|
158
|
+
const contact = FLOAT32_CONTACT * Math.max(1, reach, Math.hypot(xmax - xmin, ymax - ymin, zmax - zmin));
|
|
159
|
+
// Each pass welds what the last one left, so it only sees distinct points.
|
|
160
|
+
for (const tolerance of [tight, ...WELD_LADDER_MM.filter((step) => step > tight)]) {
|
|
161
|
+
welded = compact(welded, weld(welded.points, tolerance));
|
|
162
|
+
const measured = measureWelded(welded, tolerance, Math.max(contact, tolerance * 10));
|
|
163
|
+
if (!measured)
|
|
164
|
+
continue;
|
|
165
|
+
const { volume, area, moment } = measured;
|
|
166
|
+
const properties = {
|
|
167
|
+
volume,
|
|
168
|
+
area,
|
|
169
|
+
centerOfMass: { x: origin[0] + moment[0] / volume, y: origin[1] + moment[1] / volume, z: origin[2] + moment[2] / volume },
|
|
170
|
+
bbox,
|
|
171
|
+
};
|
|
172
|
+
return { ...measured, properties, points: welded.points, origin };
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
function cornerPoints(mesh, origin) {
|
|
177
|
+
const points = new Float64Array(mesh.faces.length * 9);
|
|
178
|
+
const corners = new Int32Array(mesh.faces.length * 3);
|
|
179
|
+
mesh.faces.forEach((face, f) => {
|
|
180
|
+
for (let k = 0; k < 3; k += 1) {
|
|
181
|
+
const c = 3 * f + k;
|
|
182
|
+
const vertex = mesh.vertices[face[k]];
|
|
183
|
+
corners[c] = vertex ? c : -1;
|
|
184
|
+
if (!vertex)
|
|
185
|
+
continue;
|
|
186
|
+
points[3 * c] = vertex[0] - origin[0];
|
|
187
|
+
points[3 * c + 1] = vertex[1] - origin[1];
|
|
188
|
+
points[3 * c + 2] = vertex[2] - origin[2];
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
return { points, corners };
|
|
192
|
+
}
|
|
193
|
+
/** Keep only the points `into` maps to themselves, and point every corner at its survivor. */
|
|
194
|
+
function compact({ points, corners }, into) {
|
|
195
|
+
const renumbered = new Int32Array(into.length).fill(-1);
|
|
196
|
+
let kept = 0;
|
|
197
|
+
for (let p = 0; p < into.length; p += 1)
|
|
198
|
+
if (into[p] === p)
|
|
199
|
+
renumbered[p] = kept++;
|
|
200
|
+
const next = new Float64Array(kept * 3);
|
|
201
|
+
for (let p = 0; p < into.length; p += 1) {
|
|
202
|
+
if (renumbered[p] < 0)
|
|
203
|
+
continue;
|
|
204
|
+
next.set(points.subarray(3 * p, 3 * p + 3), 3 * renumbered[p]);
|
|
205
|
+
}
|
|
206
|
+
const remapped = corners.map((c) => (c < 0 ? -1 : renumbered[into[c]]));
|
|
207
|
+
return { points: next, corners: remapped };
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Merge points lying within `tolerance` of an earlier one on every axis.
|
|
211
|
+
*
|
|
212
|
+
* Points are bucketed into cells twice the tolerance wide, so the points within
|
|
213
|
+
* reach of one lie in the 2 x 2 x 2 cells on the side of its cell it sits
|
|
214
|
+
* nearest; checking those means two points straddling a cell boundary still
|
|
215
|
+
* merge, where rounding to a grid alone would keep them apart however close
|
|
216
|
+
* they are. Cells are keyed by a hash: a collision only puts two cells' points
|
|
217
|
+
* in one bucket, and the distance test still tells them apart.
|
|
218
|
+
*/
|
|
219
|
+
function weld(points, tolerance) {
|
|
220
|
+
const count = points.length / 3;
|
|
221
|
+
const into = new Int32Array(count);
|
|
222
|
+
const cells = new Map();
|
|
223
|
+
const size = 2 * tolerance;
|
|
224
|
+
const hash = (i, j, k) => Math.imul(i, 73856093) ^ Math.imul(j, 19349663) ^ Math.imul(k, 83492791);
|
|
225
|
+
for (let p = 0; p < count; p += 1) {
|
|
226
|
+
const x = points[3 * p], y = points[3 * p + 1], z = points[3 * p + 2];
|
|
227
|
+
const fx = x / size, fy = y / size, fz = z / size;
|
|
228
|
+
const i = Math.floor(fx), j = Math.floor(fy), k = Math.floor(fz);
|
|
229
|
+
const si = fx - i < 0.5 ? -1 : 1, sj = fy - j < 0.5 ? -1 : 1, sk = fz - k < 0.5 ? -1 : 1;
|
|
230
|
+
let found = -1;
|
|
231
|
+
search: for (let n = 0; n < 8; n += 1) {
|
|
232
|
+
const bucket = cells.get(hash(n & 1 ? i + si : i, n & 2 ? j + sj : j, n & 4 ? k + sk : k));
|
|
233
|
+
if (!bucket)
|
|
234
|
+
continue;
|
|
235
|
+
for (const q of bucket) {
|
|
236
|
+
if (Math.abs(points[3 * q] - x) <= tolerance && Math.abs(points[3 * q + 1] - y) <= tolerance && Math.abs(points[3 * q + 2] - z) <= tolerance) {
|
|
237
|
+
found = q;
|
|
238
|
+
break search;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (found >= 0) {
|
|
243
|
+
into[p] = into[found];
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
into[p] = p;
|
|
247
|
+
const key = hash(i, j, k);
|
|
248
|
+
const bucket = cells.get(key);
|
|
249
|
+
if (bucket)
|
|
250
|
+
bucket.push(p);
|
|
251
|
+
else
|
|
252
|
+
cells.set(key, [p]);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return into;
|
|
256
|
+
}
|
|
257
|
+
/** Volume, area and first moment of the welded mesh, or `null` when some shell is open or cannot be wound consistently. */
|
|
258
|
+
function measureWelded({ points, corners: raw }, tolerance, contact) {
|
|
259
|
+
// Triangles on the welded corners. A triangle with a repeated corner encloses
|
|
260
|
+
// and bounds nothing. A triangle the file repeats counts once, and two that
|
|
261
|
+
// coincide facing opposite ways -- the shared face of two parts stacked on
|
|
262
|
+
// each other -- cancel, which leaves the two parts' union.
|
|
263
|
+
const vertexCount = points.length / 3;
|
|
264
|
+
// Keyed by the two lowest corners; each entry lists [highest corner, net winding] pairs.
|
|
265
|
+
const tally = new Map();
|
|
266
|
+
for (let t = 0; t < raw.length; t += 3) {
|
|
267
|
+
if (raw[t] < 0 || raw[t + 1] < 0 || raw[t + 2] < 0)
|
|
268
|
+
continue;
|
|
269
|
+
const a = raw[t], b = raw[t + 1], c = raw[t + 2];
|
|
270
|
+
if (a === b || b === c || a === c)
|
|
271
|
+
continue;
|
|
272
|
+
// Rotate the smallest corner first; the other two then read ascending or not.
|
|
273
|
+
const [p, q, r] = a < b && a < c ? [a, b, c] : b < c ? [b, c, a] : [c, a, b];
|
|
274
|
+
const ascending = q < r;
|
|
275
|
+
const key = p * vertexCount + (ascending ? q : r);
|
|
276
|
+
const high = ascending ? r : q;
|
|
277
|
+
const entry = tally.get(key);
|
|
278
|
+
if (!entry) {
|
|
279
|
+
tally.set(key, [high, ascending ? 1 : -1]);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
let at = 0;
|
|
283
|
+
while (at < entry.length && entry[at] !== high)
|
|
284
|
+
at += 2;
|
|
285
|
+
if (at === entry.length)
|
|
286
|
+
entry.push(high, ascending ? 1 : -1);
|
|
287
|
+
else
|
|
288
|
+
entry[at + 1] += ascending ? 1 : -1;
|
|
289
|
+
}
|
|
290
|
+
const cornerList = [];
|
|
291
|
+
for (const [key, entry] of tally) {
|
|
292
|
+
const p = Math.floor(key / vertexCount), low = key - p * vertexCount;
|
|
293
|
+
for (let at = 0; at < entry.length; at += 2) {
|
|
294
|
+
if (entry[at + 1] > 0)
|
|
295
|
+
cornerList.push(p, low, entry[at]);
|
|
296
|
+
else if (entry[at + 1] < 0)
|
|
297
|
+
cornerList.push(p, entry[at], low);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const corners = Int32Array.from(cornerList);
|
|
301
|
+
const triangleCount = corners.length / 3;
|
|
302
|
+
if (!triangleCount)
|
|
303
|
+
return null;
|
|
304
|
+
const edgeKey = (a, b) => (a < b ? a * vertexCount + b : b * vertexCount + a);
|
|
305
|
+
// Which triangles meet along each edge.
|
|
306
|
+
const edgeTriangles = new Map();
|
|
307
|
+
for (let t = 0; t < triangleCount; t += 1) {
|
|
308
|
+
for (let k = 0; k < 3; k += 1) {
|
|
309
|
+
const key = edgeKey(corners[3 * t + k], corners[3 * t + ((k + 1) % 3)]);
|
|
310
|
+
const list = edgeTriangles.get(key);
|
|
311
|
+
if (list)
|
|
312
|
+
list.push(t);
|
|
313
|
+
else
|
|
314
|
+
edgeTriangles.set(key, [t]);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const runsForward = (t, a, b) => {
|
|
318
|
+
for (let k = 0; k < 3; k += 1) {
|
|
319
|
+
if (corners[3 * t + k] === a && corners[3 * t + ((k + 1) % 3)] === b)
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
return false;
|
|
323
|
+
};
|
|
324
|
+
// Shells: triangles joined across manifold edges, wound to agree with the
|
|
325
|
+
// first triangle of their shell. An edge shared by more than two triangles
|
|
326
|
+
// (two parts touching along it) joins nothing.
|
|
327
|
+
const shellOf = new Int32Array(triangleCount).fill(-1);
|
|
328
|
+
const flipped = new Uint8Array(triangleCount);
|
|
329
|
+
const shells = [];
|
|
330
|
+
for (let seed = 0; seed < triangleCount; seed += 1) {
|
|
331
|
+
if (shellOf[seed] >= 0)
|
|
332
|
+
continue;
|
|
333
|
+
const shell = shells.length;
|
|
334
|
+
const members = [seed];
|
|
335
|
+
shellOf[seed] = shell;
|
|
336
|
+
for (let cursor = 0; cursor < members.length; cursor += 1) {
|
|
337
|
+
const t = members[cursor];
|
|
338
|
+
for (let k = 0; k < 3; k += 1) {
|
|
339
|
+
let a = corners[3 * t + k], b = corners[3 * t + ((k + 1) % 3)];
|
|
340
|
+
if (flipped[t])
|
|
341
|
+
[a, b] = [b, a];
|
|
342
|
+
const sharing = edgeTriangles.get(edgeKey(a, b));
|
|
343
|
+
if (sharing.length !== 2)
|
|
344
|
+
continue;
|
|
345
|
+
const other = sharing[0] === t ? sharing[1] : sharing[0];
|
|
346
|
+
// Agreeing neighbours cross a shared edge in opposite directions.
|
|
347
|
+
const otherFlip = runsForward(other, a, b) ? 1 : 0;
|
|
348
|
+
if (shellOf[other] < 0) {
|
|
349
|
+
shellOf[other] = shell;
|
|
350
|
+
flipped[other] = otherFlip;
|
|
351
|
+
members.push(other);
|
|
352
|
+
}
|
|
353
|
+
else if (flipped[other] !== otherFlip) {
|
|
354
|
+
return null; // Not orientable.
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
shells.push(members);
|
|
359
|
+
}
|
|
360
|
+
// Every shell closed on its own: in its own winding, each edge it uses is
|
|
361
|
+
// crossed as often one way as the other. Checked before anything else, so an
|
|
362
|
+
// open mesh -- which can fall into one shell per triangle -- costs one pass.
|
|
363
|
+
for (const [key, sharing] of edgeTriangles) {
|
|
364
|
+
const a = Math.floor(key / vertexCount), b = key - a * vertexCount;
|
|
365
|
+
// [shell, net, shell, net, ...]: an edge rarely has more than one shell on it.
|
|
366
|
+
const nets = [];
|
|
367
|
+
for (const t of sharing) {
|
|
368
|
+
const direction = (runsForward(t, a, b) ? 1 : -1) * (flipped[t] ? -1 : 1);
|
|
369
|
+
let at = 0;
|
|
370
|
+
while (at < nets.length && nets[at] !== shellOf[t])
|
|
371
|
+
at += 2;
|
|
372
|
+
if (at === nets.length)
|
|
373
|
+
nets.push(shellOf[t], direction);
|
|
374
|
+
else
|
|
375
|
+
nets[at + 1] += direction;
|
|
376
|
+
}
|
|
377
|
+
for (let at = 1; at < nets.length; at += 2)
|
|
378
|
+
if (nets[at] !== 0)
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
// The triangle across each edge, where exactly two meet: the same joins the shells were made of.
|
|
382
|
+
const neighbours = new Int32Array(triangleCount * 3).fill(-1);
|
|
383
|
+
for (let t = 0; t < triangleCount; t += 1) {
|
|
384
|
+
for (let k = 0; k < 3; k += 1) {
|
|
385
|
+
const sharing = edgeTriangles.get(edgeKey(corners[3 * t + k], corners[3 * t + ((k + 1) % 3)]));
|
|
386
|
+
if (sharing.length === 2)
|
|
387
|
+
neighbours[3 * t + k] = sharing[0] === t ? sharing[1] : sharing[0];
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Per shell: signed volume, first moment, area and bounds, in its own winding.
|
|
391
|
+
const shellVolume = new Float64Array(shells.length);
|
|
392
|
+
const shellMoment = new Float64Array(shells.length * 3);
|
|
393
|
+
const shellBounds = new Float64Array(shells.length * 6);
|
|
394
|
+
let area = 0;
|
|
395
|
+
shells.forEach((members, shell) => {
|
|
396
|
+
let volume = 0, mx = 0, my = 0, mz = 0;
|
|
397
|
+
let bx0 = Infinity, by0 = Infinity, bz0 = Infinity, bx1 = -Infinity, by1 = -Infinity, bz1 = -Infinity;
|
|
398
|
+
for (const t of members) {
|
|
399
|
+
const ia = 3 * corners[3 * t];
|
|
400
|
+
let ib = 3 * corners[3 * t + 1];
|
|
401
|
+
let ic = 3 * corners[3 * t + 2];
|
|
402
|
+
if (flipped[t])
|
|
403
|
+
[ib, ic] = [ic, ib];
|
|
404
|
+
const ax = points[ia], ay = points[ia + 1], az = points[ia + 2];
|
|
405
|
+
const bx = points[ib], by = points[ib + 1], bz = points[ib + 2];
|
|
406
|
+
const cx = points[ic], cy = points[ic + 1], cz = points[ic + 2];
|
|
407
|
+
// Tetrahedron (origin, a, b, c): signed volume and centroid.
|
|
408
|
+
const v = (ax * (by * cz - bz * cy) - ay * (bx * cz - bz * cx) + az * (bx * cy - by * cx)) / 6;
|
|
409
|
+
volume += v;
|
|
410
|
+
mx += (v * (ax + bx + cx)) / 4;
|
|
411
|
+
my += (v * (ay + by + cy)) / 4;
|
|
412
|
+
mz += (v * (az + bz + cz)) / 4;
|
|
413
|
+
const ux = bx - ax, uy = by - ay, uz = bz - az;
|
|
414
|
+
const wx = cx - ax, wy = cy - ay, wz = cz - az;
|
|
415
|
+
area += Math.hypot(uy * wz - uz * wy, uz * wx - ux * wz, ux * wy - uy * wx) / 2;
|
|
416
|
+
bx0 = Math.min(bx0, ax, bx, cx);
|
|
417
|
+
bx1 = Math.max(bx1, ax, bx, cx);
|
|
418
|
+
by0 = Math.min(by0, ay, by, cy);
|
|
419
|
+
by1 = Math.max(by1, ay, by, cy);
|
|
420
|
+
bz0 = Math.min(bz0, az, bz, cz);
|
|
421
|
+
bz1 = Math.max(bz1, az, bz, cz);
|
|
422
|
+
}
|
|
423
|
+
shellVolume[shell] = volume;
|
|
424
|
+
shellMoment.set([mx, my, mz], shell * 3);
|
|
425
|
+
shellBounds.set([bx0, by0, bz0, bx1, by1, bz1], shell * 6);
|
|
426
|
+
});
|
|
427
|
+
// How many other shells enclose this one. Only a shell whose bounds hold all
|
|
428
|
+
// of this one's can, which rules out every pair that merely sits side by side
|
|
429
|
+
// or touches; for the rest, cast a ray from this shell's extreme corner along
|
|
430
|
+
// RAY, which leaves the shell itself at once, and count the crossings.
|
|
431
|
+
//
|
|
432
|
+
// Comparing every pair of shells' bounds would cost the square of the shell
|
|
433
|
+
// count, so shells are first entered in a coarse grid over every cell their
|
|
434
|
+
// bounds overlap. Any shell enclosing this one overlaps the cell this one's
|
|
435
|
+
// lowest corner is in, so only that cell's shells need comparing.
|
|
436
|
+
const slack = tolerance * 10;
|
|
437
|
+
// Shells whose surfaces meet: whether any overlap without nesting, and for
|
|
438
|
+
// the rest which encloses which, read from their surfaces -- a ray from a
|
|
439
|
+
// point where two shells touch cannot tell (#529).
|
|
440
|
+
const touching = shells.length > 1
|
|
441
|
+
? relateTouchingShells({ points, corners, shells, shellOf, neighbours, bounds: shellBounds }, contact)
|
|
442
|
+
: { overlapping: null, relations: new Map() };
|
|
443
|
+
const perAxis = Math.max(1, Math.min(64, Math.ceil(Math.cbrt(shells.length))));
|
|
444
|
+
const lows = [Infinity, Infinity, Infinity], highs = [-Infinity, -Infinity, -Infinity];
|
|
445
|
+
for (let b = 0; b < shellBounds.length; b += 6) {
|
|
446
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
447
|
+
lows[axis] = Math.min(lows[axis], shellBounds[b + axis]);
|
|
448
|
+
highs[axis] = Math.max(highs[axis], shellBounds[b + 3 + axis]);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const cellAlong = (axis, value) => {
|
|
452
|
+
const span = highs[axis] - lows[axis];
|
|
453
|
+
if (!(span > 0))
|
|
454
|
+
return 0;
|
|
455
|
+
return Math.max(0, Math.min(perAxis - 1, Math.floor(((value - lows[axis]) / span) * perAxis)));
|
|
456
|
+
};
|
|
457
|
+
const grid = new Map();
|
|
458
|
+
if (shells.length > 1) {
|
|
459
|
+
for (let shell = 0; shell < shells.length; shell += 1) {
|
|
460
|
+
const b = 6 * shell;
|
|
461
|
+
const i0 = cellAlong(0, shellBounds[b] - slack), i1 = cellAlong(0, shellBounds[b + 3] + slack);
|
|
462
|
+
const j0 = cellAlong(1, shellBounds[b + 1] - slack), j1 = cellAlong(1, shellBounds[b + 4] + slack);
|
|
463
|
+
const k0 = cellAlong(2, shellBounds[b + 2] - slack), k1 = cellAlong(2, shellBounds[b + 5] + slack);
|
|
464
|
+
for (let i = i0; i <= i1; i += 1) {
|
|
465
|
+
for (let j = j0; j <= j1; j += 1) {
|
|
466
|
+
for (let k = k0; k <= k1; k += 1) {
|
|
467
|
+
const key = i + perAxis * (j + perAxis * k);
|
|
468
|
+
const cell = grid.get(key);
|
|
469
|
+
if (cell)
|
|
470
|
+
cell.push(shell);
|
|
471
|
+
else
|
|
472
|
+
grid.set(key, [shell]);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const enclosingCount = (shell) => {
|
|
479
|
+
const s = 6 * shell;
|
|
480
|
+
let best = -Infinity, px = 0, py = 0, pz = 0;
|
|
481
|
+
let depth = 0;
|
|
482
|
+
const candidates = grid.get(cellAlong(0, shellBounds[s]) + perAxis * (cellAlong(1, shellBounds[s + 1]) + perAxis * cellAlong(2, shellBounds[s + 2]))) ?? [];
|
|
483
|
+
for (const other of candidates) {
|
|
484
|
+
if (other === shell)
|
|
485
|
+
continue;
|
|
486
|
+
const relation = touching.relations.get(shellPairKey(shell, other, shells.length));
|
|
487
|
+
if (relation) {
|
|
488
|
+
if (relation === (shell < other ? "first-inside" : "second-inside"))
|
|
489
|
+
depth += 1;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const b = 6 * other;
|
|
493
|
+
if (shellBounds[s] < shellBounds[b] - slack || shellBounds[s + 1] < shellBounds[b + 1] - slack || shellBounds[s + 2] < shellBounds[b + 2] - slack)
|
|
494
|
+
continue;
|
|
495
|
+
if (shellBounds[s + 3] > shellBounds[b + 3] + slack || shellBounds[s + 4] > shellBounds[b + 4] + slack || shellBounds[s + 5] > shellBounds[b + 5] + slack)
|
|
496
|
+
continue;
|
|
497
|
+
if (best === -Infinity) {
|
|
498
|
+
for (const t of shells[shell]) {
|
|
499
|
+
for (let k = 0; k < 3; k += 1) {
|
|
500
|
+
const i = 3 * corners[3 * t + k];
|
|
501
|
+
const reach = points[i] * RAY[0] + points[i + 1] * RAY[1] + points[i + 2] * RAY[2];
|
|
502
|
+
if (reach > best) {
|
|
503
|
+
best = reach;
|
|
504
|
+
px = points[i];
|
|
505
|
+
py = points[i + 1];
|
|
506
|
+
pz = points[i + 2];
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
let crossings = 0;
|
|
512
|
+
for (const t of shells[other]) {
|
|
513
|
+
if (rayHitsTriangle(px, py, pz, points, corners, t, slack))
|
|
514
|
+
crossings += 1;
|
|
515
|
+
}
|
|
516
|
+
if (crossings % 2 === 1)
|
|
517
|
+
depth += 1;
|
|
518
|
+
}
|
|
519
|
+
return depth;
|
|
520
|
+
};
|
|
521
|
+
let volume = 0;
|
|
522
|
+
const moment = [0, 0, 0];
|
|
523
|
+
// Nesting depth per shell, -1 for a shell that encloses nothing, and the
|
|
524
|
+
// signed volume each adds to the whole.
|
|
525
|
+
const depth = new Int32Array(shells.length).fill(-1);
|
|
526
|
+
const contribution = new Float64Array(shells.length);
|
|
527
|
+
for (let shell = 0; shell < shells.length; shell += 1) {
|
|
528
|
+
const own = shellVolume[shell];
|
|
529
|
+
if (own === 0)
|
|
530
|
+
continue;
|
|
531
|
+
depth[shell] = shells.length > 1 ? enclosingCount(shell) : 0;
|
|
532
|
+
const wanted = depth[shell] % 2 === 1 ? -1 : 1;
|
|
533
|
+
const sign = Math.sign(own) === wanted ? 1 : -1;
|
|
534
|
+
contribution[shell] = sign * own;
|
|
535
|
+
volume += sign * own;
|
|
536
|
+
moment[0] += sign * shellMoment[3 * shell];
|
|
537
|
+
moment[1] += sign * shellMoment[3 * shell + 1];
|
|
538
|
+
moment[2] += sign * shellMoment[3 * shell + 2];
|
|
539
|
+
}
|
|
540
|
+
if (!(volume > 0))
|
|
541
|
+
return null;
|
|
542
|
+
return { volume, area, moment, shells, depth, contribution, corners, overlapping: touching.overlapping !== null };
|
|
543
|
+
}
|
|
544
|
+
/** Möller-Trumbore: whether the ray from (px, py, pz) along RAY crosses triangle `t` beyond `minDistance`. */
|
|
545
|
+
function rayHitsTriangle(px, py, pz, points, corners, t, minDistance) {
|
|
546
|
+
const ia = 3 * corners[3 * t], ib = 3 * corners[3 * t + 1], ic = 3 * corners[3 * t + 2];
|
|
547
|
+
const ax = points[ia], ay = points[ia + 1], az = points[ia + 2];
|
|
548
|
+
const e1x = points[ib] - ax, e1y = points[ib + 1] - ay, e1z = points[ib + 2] - az;
|
|
549
|
+
const e2x = points[ic] - ax, e2y = points[ic + 1] - ay, e2z = points[ic + 2] - az;
|
|
550
|
+
const hx = RAY[1] * e2z - RAY[2] * e2y;
|
|
551
|
+
const hy = RAY[2] * e2x - RAY[0] * e2z;
|
|
552
|
+
const hz = RAY[0] * e2y - RAY[1] * e2x;
|
|
553
|
+
const det = e1x * hx + e1y * hy + e1z * hz;
|
|
554
|
+
if (Math.abs(det) < 1e-15)
|
|
555
|
+
return false;
|
|
556
|
+
const inv = 1 / det;
|
|
557
|
+
const sx = px - ax, sy = py - ay, sz = pz - az;
|
|
558
|
+
const u = (sx * hx + sy * hy + sz * hz) * inv;
|
|
559
|
+
if (u < 0 || u > 1)
|
|
560
|
+
return false;
|
|
561
|
+
const qx = sy * e1z - sz * e1y;
|
|
562
|
+
const qy = sz * e1x - sx * e1z;
|
|
563
|
+
const qz = sx * e1y - sy * e1x;
|
|
564
|
+
const v = (RAY[0] * qx + RAY[1] * qy + RAY[2] * qz) * inv;
|
|
565
|
+
if (v < 0 || u + v > 1)
|
|
566
|
+
return false;
|
|
567
|
+
const distance = (e2x * qx + e2y * qy + e2z * qz) * inv;
|
|
568
|
+
return distance > minDistance;
|
|
569
|
+
}
|
|
570
|
+
//# sourceMappingURL=meshMassProperties.js.map
|