partforge 0.36.0 → 0.36.1
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/docs/KERNEL-CONTRACT.md
CHANGED
|
@@ -222,7 +222,7 @@ Normative signatures: `kernel.js`'s `@typedef Solid`.
|
|
|
222
222
|
| `genus()` / `isEmpty()` | Optional (`SOLID_OPTIONAL_OPS`): mesh-topology queries — through-hole count / no-geometry test. The mesh backend provides them; OCCT has no cheap equivalent. |
|
|
223
223
|
| `toMesh({quality?})` | Render mesh: `{positions, normals, indices?, triangles, edges?, featureIds?, features?}`. `indices` optional (a backend may emit soup or indexed); `normals` may be empty (`length 0`) to delegate creasing to the viewer; `edges` (feature-line segments) and the feature fields are optional metadata. |
|
|
224
224
|
| `toSTL({quality?})` | `Promise<ArrayBuffer>`, binary STL, outward CCW winding. Stored facet normals may be zero — slicers recompute them (the mesh backend happens to write them). |
|
|
225
|
-
| `toIndexedMesh()` | `{positions, indices}` indexed mesh (3MF path). |
|
|
225
|
+
| `toIndexedMesh({quality?})` | `{positions, indices}` indexed mesh (3MF path); defaults to `"print"` like `toSTL`. Coincident vertices need NOT be welded — the 3MF writer welds, because that format reads topology from the indices rather than re-stitching soup by position the way an STL consumer does. |
|
|
226
226
|
| `fillet(r)` · `fillet({r, edges?})` / `chamfer(d)` · `chamfer({d, edges?})` / `shell({t, open})` | B-rep class (core throws `KernelCapabilityError`). Scalar `fillet(3)`/`chamfer(1)` acts on all edges; the options form adds an `edges` selector. `shell` hollows inward, keeping outer dimensions; `open` (face selector) is required. |
|
|
227
227
|
|
|
228
228
|
`quality` (`"preview"` | `"print"`) is **advisory**: it trades tessellation density for
|
package/package.json
CHANGED
|
@@ -34,7 +34,7 @@ const MESH = { preview: { tolerance: 0.1, angularTolerance: 0.5 }, print: { tole
|
|
|
34
34
|
|
|
35
35
|
export function createOcctKernel(replicad) {
|
|
36
36
|
const { makeCylinder, makeBox, makeCircle, makeHelix, assembleWire, genericSweep,
|
|
37
|
-
|
|
37
|
+
loft, draw, exportSTEP, measureVolume, makeSphere, makeLine, Plane } = replicad;
|
|
38
38
|
|
|
39
39
|
// Fillet/chamfer/shell failure recovery (skip-on-failure, chamfer binary search) —
|
|
40
40
|
// see occt-repair.js for the policies and why they differ per op.
|
|
@@ -127,8 +127,12 @@ export function createOcctKernel(replicad) {
|
|
|
127
127
|
const key = h("cutAll", hash, tools.map((t) => t._hash));
|
|
128
128
|
return cached(key, () => {
|
|
129
129
|
const a = mat(), bs = tools.map((t) => t._mat());
|
|
130
|
+
if (bs.length === 0) return wrap(a._s.clone(), cloneLabels(a._labels), key);
|
|
131
|
+
const fusedTools = bs
|
|
132
|
+
.slice(1)
|
|
133
|
+
.reduce((acc, b) => acc.fuse(b._s.clone()), bs[0]._s.clone());
|
|
130
134
|
return wrap(
|
|
131
|
-
a._s.clone().cut(
|
|
135
|
+
a._s.clone().cut(fusedTools),
|
|
132
136
|
[...cloneLabels(a._labels), ...bs.flatMap((b) => cloneLabels(b._labels))],
|
|
133
137
|
key,
|
|
134
138
|
);
|
|
@@ -210,8 +214,10 @@ export function createOcctKernel(replicad) {
|
|
|
210
214
|
});
|
|
211
215
|
},
|
|
212
216
|
volume: () => measureVolume(mat()._s),
|
|
213
|
-
|
|
214
|
-
|
|
217
|
+
// Same default as toSTL: an export is an export, so a .3mf must not ship a
|
|
218
|
+
// coarser tessellation than the .stl of the same solid would.
|
|
219
|
+
toIndexedMesh: ({ quality = "print" } = {}) => {
|
|
220
|
+
const base = baseMesh(quality);
|
|
215
221
|
return { positions: posedPositions(base), indices: Uint32Array.from(base.indices) };
|
|
216
222
|
},
|
|
217
223
|
});
|
|
@@ -19,6 +19,54 @@ const RELS =
|
|
|
19
19
|
const xmlEsc = (s) => String(s).replace(/[<>&"]/g, (c) => ({ "<": "<", ">": ">", "&": "&", '"': """ }[c]));
|
|
20
20
|
const r = (x) => +x.toFixed(4); // 0.1 µm precision — finer than any printer, much smaller XML
|
|
21
21
|
|
|
22
|
+
// Weld coincident vertices and drop triangles that collapse to zero area.
|
|
23
|
+
//
|
|
24
|
+
// This is what keeps a .3mf manifold, and it has no STL equivalent: STL is vertex
|
|
25
|
+
// soup, so a consumer re-stitches triangles by POSITION and never sees how the
|
|
26
|
+
// mesh was indexed. 3MF reads topology from the indices instead, so two triangles
|
|
27
|
+
// that meet along an edge must literally cite the same two vertex ids — otherwise
|
|
28
|
+
// each counts the shared edge as its own boundary and slicers report the solid as
|
|
29
|
+
// non-manifold. The OCCT backend triangulates every B-rep face independently and
|
|
30
|
+
// concatenates the results, so it hands us one copy of each seam vertex PER
|
|
31
|
+
// adjacent face; welding here is what closes that mesh. (Manifold's own output is
|
|
32
|
+
// already welded, so for that backend this is a no-op beyond the copy.)
|
|
33
|
+
//
|
|
34
|
+
// Welding uses `r` — exactly the precision the file is written at — so any two
|
|
35
|
+
// vertices that would print identical coordinates always collapse to one id. A
|
|
36
|
+
// looser tolerance would move geometry; a tighter one would leave split vertices
|
|
37
|
+
// sitting at coordinates the file cannot tell apart.
|
|
38
|
+
function weld(positions, indices) {
|
|
39
|
+
const idOf = new Map(); // "x,y,z" → canonical vertex id
|
|
40
|
+
const coord = []; // canonical id → rounded x,y,z (flat)
|
|
41
|
+
const canon = new Uint32Array(positions.length / 3);
|
|
42
|
+
for (let i = 0, v = 0; i < positions.length; i += 3, v++) {
|
|
43
|
+
const x = r(positions[i]), y = r(positions[i + 1]), z = r(positions[i + 2]);
|
|
44
|
+
const key = `${x},${y},${z}`;
|
|
45
|
+
let id = idOf.get(key);
|
|
46
|
+
if (id === undefined) { id = coord.length / 3; idOf.set(key, id); coord.push(x, y, z); }
|
|
47
|
+
canon[v] = id;
|
|
48
|
+
}
|
|
49
|
+
// Emit vertices in order of first use by a surviving triangle, so a vertex left
|
|
50
|
+
// behind by a dropped degenerate never ships as an unreferenced <vertex>.
|
|
51
|
+
const emitted = new Map(); // canonical id → written vertex index
|
|
52
|
+
const verts = [], tris = [];
|
|
53
|
+
const emit = (id) => {
|
|
54
|
+
let at = emitted.get(id);
|
|
55
|
+
if (at === undefined) {
|
|
56
|
+
at = verts.length / 3;
|
|
57
|
+
emitted.set(id, at);
|
|
58
|
+
verts.push(coord[id * 3], coord[id * 3 + 1], coord[id * 3 + 2]);
|
|
59
|
+
}
|
|
60
|
+
return at;
|
|
61
|
+
};
|
|
62
|
+
for (let k = 0; k < indices.length; k += 3) {
|
|
63
|
+
const a = canon[indices[k]], b = canon[indices[k + 1]], c = canon[indices[k + 2]];
|
|
64
|
+
if (a === b || b === c || a === c) continue; // zero area at print precision — carries no surface
|
|
65
|
+
tris.push(emit(a), emit(b), emit(c));
|
|
66
|
+
}
|
|
67
|
+
return { verts, tris };
|
|
68
|
+
}
|
|
69
|
+
|
|
22
70
|
// parts: [{ name, positions: Float32Array (x,y,z per vertex), indices: Uint32Array (3 per triangle) }]
|
|
23
71
|
// → ArrayBuffer of the .3mf zip (millimetre units; one <object> + <build> item per part).
|
|
24
72
|
export function meshTo3MF(parts) {
|
|
@@ -29,10 +77,9 @@ export function meshTo3MF(parts) {
|
|
|
29
77
|
];
|
|
30
78
|
parts.forEach((p, i) => {
|
|
31
79
|
out.push(`<object id="${i + 1}" type="model" name="${xmlEsc(p.name)}"><mesh><vertices>`);
|
|
32
|
-
const v = p.positions;
|
|
33
|
-
for (let k = 0; k < v.length; k += 3) out.push(`<vertex x="${
|
|
80
|
+
const { verts: v, tris: t } = weld(p.positions, p.indices);
|
|
81
|
+
for (let k = 0; k < v.length; k += 3) out.push(`<vertex x="${v[k]}" y="${v[k + 1]}" z="${v[k + 2]}"/>`);
|
|
34
82
|
out.push("</vertices><triangles>");
|
|
35
|
-
const t = p.indices;
|
|
36
83
|
for (let k = 0; k < t.length; k += 3) out.push(`<triangle v1="${t[k]}" v2="${t[k + 1]}" v3="${t[k + 2]}"/>`);
|
|
37
84
|
out.push("</triangles></mesh></object>");
|
|
38
85
|
});
|
package/src/framework/jobs.js
CHANGED
|
@@ -136,7 +136,7 @@ export async function handle(kernel, part, msg, post, opts = {}) {
|
|
|
136
136
|
if (names.length === 0) throw new Error("no exportable parts selected");
|
|
137
137
|
const meshes = names.map((name) => {
|
|
138
138
|
onProgress(`building ${label(name)}`);
|
|
139
|
-
const { positions, indices } = posed(name, "export", onProgress).toIndexedMesh();
|
|
139
|
+
const { positions, indices } = posed(name, "export", onProgress).toIndexedMesh({ quality: msg.quality ?? "print" });
|
|
140
140
|
return { name: exportName(name), positions, indices };
|
|
141
141
|
});
|
|
142
142
|
onProgress("writing 3MF file");
|