meshfix-wasm 0.1.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/LICENSE +21 -0
- package/README.md +151 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +212 -0
- package/dist/issues.d.ts +2 -0
- package/dist/issues.js +76 -0
- package/dist/meshfix-core.js +2 -0
- package/dist/meshfix-core.wasm +0 -0
- package/dist/types.d.ts +139 -0
- package/dist/types.js +1 -0
- package/dist/worker-bridge.d.ts +14 -0
- package/dist/worker-bridge.js +71 -0
- package/dist/worker-client.d.ts +28 -0
- package/dist/worker-client.js +87 -0
- package/dist/worker-types.d.ts +26 -0
- package/dist/worker-types.js +1 -0
- package/dist/worker.d.ts +20 -0
- package/dist/worker.js +220 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Anthony Greco
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# meshfix-wasm
|
|
2
|
+
|
|
3
|
+
Client-side 3D mesh repair for the browser. Fixes broken STL, OBJ, and OFF files so they load cleanly in slicers like PrusaSlicer, Cura, and OrcaSlicer.
|
|
4
|
+
|
|
5
|
+
Built on [PMP Library](https://www.pmp-library.org/) compiled to WebAssembly.
|
|
6
|
+
|
|
7
|
+
See it in action at [www.justfixstl.com](https://www.justfixstl.com). All processing runs locally in the browser, no server, no uploads.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install meshfix-wasm
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { MeshFixWorker } from "meshfix-wasm";
|
|
19
|
+
|
|
20
|
+
// Initialize (loads WASM in a Web Worker)
|
|
21
|
+
const meshfix = await MeshFixWorker.init();
|
|
22
|
+
|
|
23
|
+
// Load an STL file
|
|
24
|
+
const buffer = await fetch("model.stl").then((r) => r.arrayBuffer());
|
|
25
|
+
|
|
26
|
+
// Analyze
|
|
27
|
+
const { analysis, issues } = await meshfix.analyzeDetailed(buffer);
|
|
28
|
+
console.log(`${analysis.vertexCount} vertices, ${analysis.faceCount} faces`);
|
|
29
|
+
console.log(`Issues: ${issues.map((i) => i.message).join(", ") || "none"}`);
|
|
30
|
+
|
|
31
|
+
// One-click repair
|
|
32
|
+
const result = await meshfix.repair();
|
|
33
|
+
console.log(`Vertices: ${result.verticesBefore} → ${result.verticesAfter}`);
|
|
34
|
+
console.log(`Faces: ${result.facesBefore} → ${result.facesAfter}`);
|
|
35
|
+
|
|
36
|
+
// Export repaired mesh
|
|
37
|
+
const repaired = await meshfix.exportMesh("stl");
|
|
38
|
+
const blob = new Blob([repaired], { type: "application/octet-stream" });
|
|
39
|
+
|
|
40
|
+
// Clean up
|
|
41
|
+
meshfix.dispose();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## API
|
|
45
|
+
|
|
46
|
+
### `MeshFixWorker` (recommended)
|
|
47
|
+
|
|
48
|
+
Runs all processing in a Web Worker so the UI stays responsive.
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const meshfix = await MeshFixWorker.init(options?);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Init options:**
|
|
55
|
+
|
|
56
|
+
| Option | Type | Description |
|
|
57
|
+
|--------|------|-------------|
|
|
58
|
+
| `workerUrl` | `string \| URL` | Custom worker script URL |
|
|
59
|
+
| `coreUrl` | `string` | Custom meshfix-core.js URL |
|
|
60
|
+
| `wasmUrl` | `string` | Custom .wasm file URL |
|
|
61
|
+
|
|
62
|
+
**Methods:**
|
|
63
|
+
|
|
64
|
+
| Method | Returns | Description |
|
|
65
|
+
|--------|---------|-------------|
|
|
66
|
+
| `analyze(buffer)` | `MeshStats` | Basic geometry stats |
|
|
67
|
+
| `analyzeDetailed(buffer)` | `{ analysis, issues }` | Full topology analysis with issue detection |
|
|
68
|
+
| `repair(options?, onProgress?)` | `RepairResult` | Auto-repair pipeline |
|
|
69
|
+
| `weldVertices(epsilon?)` | `WeldResult` | Merge duplicate vertices |
|
|
70
|
+
| `removeDegenerates(minArea?)` | `RemoveDegeneratesResult` | Remove zero-area and duplicate faces |
|
|
71
|
+
| `splitVertices()` | `SplitVerticesResult` | Fix non-manifold (bowtie) vertices |
|
|
72
|
+
| `fillHoles(maxEdges?)` | `FillHolesResult` | Fill boundary loops |
|
|
73
|
+
| `fixNormals()` | `FixNormalsResult` | Orient normals outward |
|
|
74
|
+
| `reanalyze()` | `{ analysis, issues }` | Re-analyze after modifications |
|
|
75
|
+
| `exportMesh(format?)` | `ArrayBuffer` | Export as `"stl"`, `"obj"`, or `"off"` |
|
|
76
|
+
| `toRenderData()` | `RenderData` | Get vertex/index buffers for 3D rendering |
|
|
77
|
+
| `dispose()` | `void` | Terminate worker and free memory |
|
|
78
|
+
|
|
79
|
+
### `MeshFix` (main thread)
|
|
80
|
+
|
|
81
|
+
Same API as `MeshFixWorker` but synchronous. Useful for debugging or environments without Web Workers.
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { MeshFix } from "meshfix-wasm";
|
|
85
|
+
const meshfix = await MeshFix.init();
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Repair Options
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
await meshfix.repair({
|
|
92
|
+
weldEpsilon: 1e-6, // vertex merge distance (default: 1e-6)
|
|
93
|
+
minArea: 1e-10, // degenerate face threshold (default: 1e-10)
|
|
94
|
+
maxHoleEdges: 100, // max hole size to fill (default: 100)
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Repair Pipeline
|
|
99
|
+
|
|
100
|
+
The `repair()` method runs these steps in order:
|
|
101
|
+
|
|
102
|
+
1. **Weld vertices** — merge duplicates within epsilon distance
|
|
103
|
+
2. **Remove degenerates** — delete zero-area and duplicate faces (skipped if mesh is already watertight)
|
|
104
|
+
3. **Split vertices** — fix non-manifold (bowtie) vertices
|
|
105
|
+
4. **Fill holes** — close boundary loops with fan triangulation
|
|
106
|
+
5. **Fix normals** — orient all faces outward using signed volume
|
|
107
|
+
|
|
108
|
+
### Progress Callbacks
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
await meshfix.repair({}, (event) => {
|
|
112
|
+
console.log(`Step ${event.stepIndex + 1}/${event.totalSteps}: ${event.step}`);
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Three.js Integration
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
const renderData = await meshfix.toRenderData();
|
|
120
|
+
|
|
121
|
+
const geometry = new THREE.BufferGeometry();
|
|
122
|
+
geometry.setAttribute("position", new THREE.BufferAttribute(renderData.positions, 3));
|
|
123
|
+
geometry.setAttribute("normal", new THREE.BufferAttribute(renderData.normals, 3));
|
|
124
|
+
geometry.setIndex(new THREE.BufferAttribute(renderData.indices, 1));
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The `renderData.faceFlags` field contains per-face bitmask flags for issue visualization:
|
|
128
|
+
|
|
129
|
+
| Flag | Bit | Meaning |
|
|
130
|
+
|------|-----|---------|
|
|
131
|
+
| `0x01` | Degenerate | Zero-area face |
|
|
132
|
+
| `0x02` | Duplicate | Identical to another face |
|
|
133
|
+
| `0x04` | Flipped | Normal points inward |
|
|
134
|
+
| `0x08` | Boundary | Face has an open edge |
|
|
135
|
+
| `0x10` | Non-manifold | Adjacent to a non-manifold vertex |
|
|
136
|
+
|
|
137
|
+
## Browser Support
|
|
138
|
+
|
|
139
|
+
Requires browsers with WebAssembly and Web Worker support:
|
|
140
|
+
- Chrome 57+
|
|
141
|
+
- Firefox 52+
|
|
142
|
+
- Safari 11+
|
|
143
|
+
- Edge 16+
|
|
144
|
+
|
|
145
|
+
## Acknowledgments
|
|
146
|
+
|
|
147
|
+
This library uses [PMP Library](https://www.pmp-library.org/) (MIT License) by the Polygon Mesh Processing Library developers and Computer Graphics Group, RWTH Aachen.
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
[MIT](LICENSE)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { MeshStats, MeshAnalysis, MeshIssue, MeshFixCoreModule, MeshAnalyzerInstance, WeldResult, RemoveDegeneratesResult, FixNormalsResult, FillHolesResult, SplitVerticesResult, RepairResult, RepairOptions, ProgressEvent, RenderData } from "./types.js";
|
|
2
|
+
export type { MeshStats, MeshAnalysis, MeshIssue, MeshFixCoreModule, MeshAnalyzerInstance, WeldResult, RemoveDegeneratesResult, FixNormalsResult, FillHolesResult, SplitVerticesResult, RepairResult, RepairOptions, ProgressEvent, RenderData };
|
|
3
|
+
export type { IssueType, IssueSeverity, RepairStep } from "./types.js";
|
|
4
|
+
export { buildIssues } from "./issues.js";
|
|
5
|
+
export { MeshFixWorker } from "./worker-client.js";
|
|
6
|
+
export type { WorkerInitOptions } from "./worker-types.js";
|
|
7
|
+
export type ExportFormat = "stl" | "obj" | "off";
|
|
8
|
+
export interface AnalysisResult {
|
|
9
|
+
analysis: MeshAnalysis;
|
|
10
|
+
issues: MeshIssue[];
|
|
11
|
+
}
|
|
12
|
+
export declare class MeshFix {
|
|
13
|
+
private module;
|
|
14
|
+
private analyzer;
|
|
15
|
+
private constructor();
|
|
16
|
+
static init(): Promise<MeshFix>;
|
|
17
|
+
analyze(data: ArrayBuffer): MeshStats;
|
|
18
|
+
analyzeTestShape(name: string): MeshStats;
|
|
19
|
+
analyzeDetailed(data: ArrayBuffer): AnalysisResult;
|
|
20
|
+
analyzeTestShapeDetailed(name: string): AnalysisResult;
|
|
21
|
+
weldVertices(epsilon?: number): WeldResult;
|
|
22
|
+
removeDegenerates(minArea?: number): RemoveDegeneratesResult;
|
|
23
|
+
fixNormals(): FixNormalsResult;
|
|
24
|
+
fillHoles(maxEdges?: number): FillHolesResult;
|
|
25
|
+
splitVertices(): SplitVerticesResult;
|
|
26
|
+
repair(options?: RepairOptions, onProgress?: (event: ProgressEvent) => void): RepairResult;
|
|
27
|
+
reanalyze(): AnalysisResult;
|
|
28
|
+
exportMesh(format?: ExportFormat): ArrayBuffer;
|
|
29
|
+
/** @deprecated Use exportMesh("stl") instead */
|
|
30
|
+
exportSTL(): ArrayBuffer;
|
|
31
|
+
toRenderData(): RenderData;
|
|
32
|
+
dispose(): void;
|
|
33
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { buildIssues } from "./issues.js";
|
|
2
|
+
export { buildIssues } from "./issues.js";
|
|
3
|
+
export { MeshFixWorker } from "./worker-client.js";
|
|
4
|
+
export class MeshFix {
|
|
5
|
+
constructor(module) {
|
|
6
|
+
this.module = module;
|
|
7
|
+
this.analyzer = new module.MeshAnalyzer();
|
|
8
|
+
}
|
|
9
|
+
static async init() {
|
|
10
|
+
const module = await createMeshFixCore();
|
|
11
|
+
return new MeshFix(module);
|
|
12
|
+
}
|
|
13
|
+
analyze(data) {
|
|
14
|
+
const uint8 = new Uint8Array(data);
|
|
15
|
+
const path = "/tmp/input.stl";
|
|
16
|
+
try {
|
|
17
|
+
this.module.FS.writeFile(path, uint8);
|
|
18
|
+
const ok = this.analyzer.loadFromFile(path);
|
|
19
|
+
if (!ok) {
|
|
20
|
+
throw new Error(this.analyzer.getLastError() || "Failed to load mesh");
|
|
21
|
+
}
|
|
22
|
+
return this.analyzer.getStats();
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
try {
|
|
26
|
+
this.module.FS.unlink(path);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// file may not exist if write failed
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
analyzeTestShape(name) {
|
|
34
|
+
const ok = this.analyzer.loadTestShape(name);
|
|
35
|
+
if (!ok) {
|
|
36
|
+
throw new Error(this.analyzer.getLastError() || "Failed to create test shape");
|
|
37
|
+
}
|
|
38
|
+
return this.analyzer.getStats();
|
|
39
|
+
}
|
|
40
|
+
analyzeDetailed(data) {
|
|
41
|
+
const uint8 = new Uint8Array(data);
|
|
42
|
+
const path = "/tmp/input.stl";
|
|
43
|
+
try {
|
|
44
|
+
this.module.FS.writeFile(path, uint8);
|
|
45
|
+
const ok = this.analyzer.loadFromFile(path);
|
|
46
|
+
if (!ok) {
|
|
47
|
+
throw new Error(this.analyzer.getLastError() || "Failed to load mesh");
|
|
48
|
+
}
|
|
49
|
+
const analysis = this.analyzer.getAnalysis();
|
|
50
|
+
return { analysis, issues: buildIssues(analysis) };
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
try {
|
|
54
|
+
this.module.FS.unlink(path);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// file may not exist if write failed
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
analyzeTestShapeDetailed(name) {
|
|
62
|
+
const ok = this.analyzer.loadTestShape(name);
|
|
63
|
+
if (!ok) {
|
|
64
|
+
throw new Error(this.analyzer.getLastError() || "Failed to create test shape");
|
|
65
|
+
}
|
|
66
|
+
const analysis = this.analyzer.getAnalysis();
|
|
67
|
+
return { analysis, issues: buildIssues(analysis) };
|
|
68
|
+
}
|
|
69
|
+
weldVertices(epsilon = 1e-6) {
|
|
70
|
+
if (!this.analyzer.isLoaded()) {
|
|
71
|
+
throw new Error("No mesh loaded");
|
|
72
|
+
}
|
|
73
|
+
return this.analyzer.weldVertices(epsilon);
|
|
74
|
+
}
|
|
75
|
+
removeDegenerates(minArea = 1e-10) {
|
|
76
|
+
if (!this.analyzer.isLoaded()) {
|
|
77
|
+
throw new Error("No mesh loaded");
|
|
78
|
+
}
|
|
79
|
+
return this.analyzer.removeDegenerates(minArea);
|
|
80
|
+
}
|
|
81
|
+
fixNormals() {
|
|
82
|
+
if (!this.analyzer.isLoaded()) {
|
|
83
|
+
throw new Error("No mesh loaded");
|
|
84
|
+
}
|
|
85
|
+
return this.analyzer.fixNormals();
|
|
86
|
+
}
|
|
87
|
+
fillHoles(maxEdges = 100) {
|
|
88
|
+
if (!this.analyzer.isLoaded()) {
|
|
89
|
+
throw new Error("No mesh loaded");
|
|
90
|
+
}
|
|
91
|
+
return this.analyzer.fillHoles(maxEdges);
|
|
92
|
+
}
|
|
93
|
+
splitVertices() {
|
|
94
|
+
if (!this.analyzer.isLoaded()) {
|
|
95
|
+
throw new Error("No mesh loaded");
|
|
96
|
+
}
|
|
97
|
+
return this.analyzer.splitVertices();
|
|
98
|
+
}
|
|
99
|
+
repair(options, onProgress) {
|
|
100
|
+
if (!this.analyzer.isLoaded()) {
|
|
101
|
+
throw new Error("No mesh loaded");
|
|
102
|
+
}
|
|
103
|
+
const weldEpsilon = options?.weldEpsilon ?? 1e-6;
|
|
104
|
+
const minArea = options?.minArea ?? 1e-10;
|
|
105
|
+
const maxHoleEdges = options?.maxHoleEdges ?? 100;
|
|
106
|
+
const totalSteps = 5;
|
|
107
|
+
const verticesBefore = this.analyzer.getVertexCount();
|
|
108
|
+
const facesBefore = this.analyzer.getFaceCount();
|
|
109
|
+
const progress = (step, stepIndex) => {
|
|
110
|
+
if (onProgress)
|
|
111
|
+
onProgress({ step, stepIndex, totalSteps });
|
|
112
|
+
};
|
|
113
|
+
progress("weld", 0);
|
|
114
|
+
const weld = this.analyzer.weldVertices(weldEpsilon);
|
|
115
|
+
// Only remove degenerates if mesh is not watertight — removing
|
|
116
|
+
// faces from a closed surface tears holes that may not fill cleanly.
|
|
117
|
+
const midAnalysis = this.analyzer.getAnalysis();
|
|
118
|
+
progress("removeDegenerates", 1);
|
|
119
|
+
const removeDegenerates = midAnalysis.isWatertight
|
|
120
|
+
? null
|
|
121
|
+
: this.analyzer.removeDegenerates(minArea);
|
|
122
|
+
progress("splitVertices", 2);
|
|
123
|
+
const splitVertices = this.analyzer.splitVertices();
|
|
124
|
+
progress("fillHoles", 3);
|
|
125
|
+
const fillHoles = this.analyzer.fillHoles(maxHoleEdges);
|
|
126
|
+
progress("fixNormals", 4);
|
|
127
|
+
const fixNormals = this.analyzer.fixNormals();
|
|
128
|
+
return {
|
|
129
|
+
weld,
|
|
130
|
+
removeDegenerates: removeDegenerates,
|
|
131
|
+
splitVertices,
|
|
132
|
+
fillHoles,
|
|
133
|
+
fixNormals,
|
|
134
|
+
verticesBefore,
|
|
135
|
+
verticesAfter: this.analyzer.getVertexCount(),
|
|
136
|
+
facesBefore,
|
|
137
|
+
facesAfter: this.analyzer.getFaceCount(),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
reanalyze() {
|
|
141
|
+
if (!this.analyzer.isLoaded()) {
|
|
142
|
+
throw new Error("No mesh loaded");
|
|
143
|
+
}
|
|
144
|
+
const analysis = this.analyzer.getAnalysis();
|
|
145
|
+
return { analysis, issues: buildIssues(analysis) };
|
|
146
|
+
}
|
|
147
|
+
exportMesh(format = "stl") {
|
|
148
|
+
if (!this.analyzer.isLoaded()) {
|
|
149
|
+
throw new Error("No mesh loaded");
|
|
150
|
+
}
|
|
151
|
+
const path = `/tmp/export.${format}`;
|
|
152
|
+
try {
|
|
153
|
+
const ok = this.analyzer.exportMesh(path);
|
|
154
|
+
if (!ok) {
|
|
155
|
+
throw new Error(this.analyzer.getLastError() || "Failed to export mesh");
|
|
156
|
+
}
|
|
157
|
+
const data = this.module.FS.readFile(path);
|
|
158
|
+
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
try {
|
|
162
|
+
this.module.FS.unlink(path);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// file may not exist if export failed
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** @deprecated Use exportMesh("stl") instead */
|
|
170
|
+
exportSTL() {
|
|
171
|
+
return this.exportMesh("stl");
|
|
172
|
+
}
|
|
173
|
+
toRenderData() {
|
|
174
|
+
if (!this.analyzer.isLoaded()) {
|
|
175
|
+
throw new Error("No mesh loaded");
|
|
176
|
+
}
|
|
177
|
+
const path = "/tmp/render.bin";
|
|
178
|
+
try {
|
|
179
|
+
const ok = this.analyzer.writeRenderData(path);
|
|
180
|
+
if (!ok) {
|
|
181
|
+
throw new Error(this.analyzer.getLastError() || "Failed to write render data");
|
|
182
|
+
}
|
|
183
|
+
const data = this.module.FS.readFile(path);
|
|
184
|
+
const buf = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
185
|
+
const view = new DataView(buf);
|
|
186
|
+
const vertexCount = view.getUint32(0, true);
|
|
187
|
+
const indexCount = view.getUint32(4, true);
|
|
188
|
+
const posOffset = 8;
|
|
189
|
+
const normOffset = posOffset + vertexCount * 3 * 4;
|
|
190
|
+
const idxOffset = normOffset + vertexCount * 3 * 4;
|
|
191
|
+
const flagsOffset = idxOffset + indexCount * 4;
|
|
192
|
+
const faceCount = indexCount / 3;
|
|
193
|
+
return {
|
|
194
|
+
positions: new Float32Array(buf, posOffset, vertexCount * 3),
|
|
195
|
+
normals: new Float32Array(buf, normOffset, vertexCount * 3),
|
|
196
|
+
indices: new Uint32Array(buf, idxOffset, indexCount),
|
|
197
|
+
faceFlags: new Uint8Array(buf, flagsOffset, faceCount),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
try {
|
|
202
|
+
this.module.FS.unlink(path);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// file may not exist if write failed
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
dispose() {
|
|
210
|
+
this.analyzer.delete();
|
|
211
|
+
}
|
|
212
|
+
}
|
package/dist/issues.d.ts
ADDED
package/dist/issues.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export function buildIssues(a) {
|
|
2
|
+
const issues = [];
|
|
3
|
+
if (a.nonManifoldVertexCount > 0) {
|
|
4
|
+
issues.push({
|
|
5
|
+
type: "non_manifold_vertices",
|
|
6
|
+
severity: "error",
|
|
7
|
+
count: a.nonManifoldVertexCount,
|
|
8
|
+
message: `${a.nonManifoldVertexCount} non-manifold vertex(es) found`,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
if (a.nonManifoldEdgeCount > 0) {
|
|
12
|
+
issues.push({
|
|
13
|
+
type: "non_manifold_edges",
|
|
14
|
+
severity: "error",
|
|
15
|
+
count: a.nonManifoldEdgeCount,
|
|
16
|
+
message: `${a.nonManifoldEdgeCount} non-manifold edge(s) detected (faces skipped during import)`,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
if (a.flippedNormalCount > 0) {
|
|
20
|
+
issues.push({
|
|
21
|
+
type: "flipped_normals",
|
|
22
|
+
severity: "warning",
|
|
23
|
+
count: a.flippedNormalCount,
|
|
24
|
+
message: `${a.flippedNormalCount} face(s) with flipped normals (inverted winding)`,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (a.degenerateTriangleCount > 0) {
|
|
28
|
+
issues.push({
|
|
29
|
+
type: "degenerate_triangles",
|
|
30
|
+
severity: "warning",
|
|
31
|
+
count: a.degenerateTriangleCount,
|
|
32
|
+
message: `${a.degenerateTriangleCount} degenerate triangle(s) with near-zero area`,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (a.holeCount > 0) {
|
|
36
|
+
issues.push({
|
|
37
|
+
type: "holes",
|
|
38
|
+
severity: "warning",
|
|
39
|
+
count: a.holeCount,
|
|
40
|
+
message: `${a.holeCount} hole(s) (boundary loops) found`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (a.duplicateFaceCount > 0) {
|
|
44
|
+
issues.push({
|
|
45
|
+
type: "duplicate_faces",
|
|
46
|
+
severity: "warning",
|
|
47
|
+
count: a.duplicateFaceCount,
|
|
48
|
+
message: `${a.duplicateFaceCount} duplicate face(s) found`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (a.isolatedVertexCount > 0) {
|
|
52
|
+
issues.push({
|
|
53
|
+
type: "isolated_vertices",
|
|
54
|
+
severity: "info",
|
|
55
|
+
count: a.isolatedVertexCount,
|
|
56
|
+
message: `${a.isolatedVertexCount} isolated vertex(es) not connected to any face`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (!a.isWatertight) {
|
|
60
|
+
issues.push({
|
|
61
|
+
type: "not_watertight",
|
|
62
|
+
severity: "info",
|
|
63
|
+
count: a.boundaryEdges,
|
|
64
|
+
message: `Mesh is not watertight (${a.boundaryEdges} boundary edges)`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (a.connectedComponents > 1) {
|
|
68
|
+
issues.push({
|
|
69
|
+
type: "multiple_components",
|
|
70
|
+
severity: "info",
|
|
71
|
+
count: a.connectedComponents,
|
|
72
|
+
message: `Mesh has ${a.connectedComponents} disconnected components`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return issues;
|
|
76
|
+
}
|