partforge 0.141.0 → 0.142.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/docs/AUTHORING-PARTS.md
CHANGED
|
@@ -103,8 +103,8 @@ export default {
|
|
|
103
103
|
- `place(solid, ctx)` is an optional escape hatch for parts whose **display pose differs
|
|
104
104
|
from their export pose** (e.g. positioning a sub-part in an assembly). `ctx.purpose` is
|
|
105
105
|
`"display"` or `"export"`; `ctx.view` is the active view. Default is identity, so simple
|
|
106
|
-
parts omit it.
|
|
107
|
-
|
|
106
|
+
parts omit it. Display placement may depend on `view`: the viewer re-poses each
|
|
107
|
+
sub-part when the tab changes, so one sub-part can sit differently in two views.
|
|
108
108
|
The viewer applies the display pose as a matrix over the canonical mesh, so `build()` may
|
|
109
109
|
query geometry freely; only `place()` has to stay a rigid motion of its argument, reading
|
|
110
110
|
`p` and `d`, for a pose-only param to play at frame rate and for layer lines to stay on
|
|
@@ -3721,12 +3721,11 @@ affect `ok`, `measure`, or `--strict`.
|
|
|
3721
3721
|
|
|
3722
3722
|
**Place invariants**, found by running the geometry-free place probe (the same
|
|
3723
3723
|
one animation's `animation-track-rebuilds` uses) against each sub-part's
|
|
3724
|
-
`place()`, even when `build()` queries the solid — `
|
|
3725
|
-
|
|
3726
|
-
`place
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
place rules and earns the `animation-track-rebuilds` note.
|
|
3724
|
+
`place()`, even when `build()` queries the solid — `place-not-rigid` (display vs.
|
|
3725
|
+
export placement may differ only by a rigid motion — translate/rotate — never a
|
|
3726
|
+
reshape; an error). A `place()` the probe cannot read (it queries the solid or
|
|
3727
|
+
passes a function) stays silent for the place rule and earns the
|
|
3728
|
+
`animation-track-rebuilds` note.
|
|
3730
3729
|
|
|
3731
3730
|
**Appearance** (all warnings) — `unknown-material` (a `display.material` the
|
|
3732
3731
|
library does not know; the viewer draws it as if it named none — a PLA print
|
|
@@ -3994,7 +3993,9 @@ a **min-wall** warning, and — only for a part that also declares
|
|
|
3994
3993
|
`orientation: "print"` — an **overhang** warning: `overhangArea` is the mm² of
|
|
3995
3994
|
downward-facing surface steeper than the profile's `overhang` angle (45° from vertical
|
|
3996
3995
|
on the FDM profiles; resin carries none, since it prints on supports), measured per
|
|
3997
|
-
sub-part
|
|
3996
|
+
sub-part in its print (export) pose — not as the view shows it — with the bed at that
|
|
3997
|
+
pose's own lowest Z, and warned past 1 mm²; the reported location is where the face sits
|
|
3998
|
+
in the view. The
|
|
3998
3999
|
opt-in is deliberate: a profile says what a process can print, the orientation key
|
|
3999
4000
|
says this part is laid out for it, and a part still being shaped, or one bound for a
|
|
4000
4001
|
different process, should not be nagged about its underside. Writing your own
|
|
@@ -4429,8 +4430,6 @@ symptom first** — it maps error text → cause → fix. The invariants, one li
|
|
|
4429
4430
|
232 and a box is 12, and every one is a boolean operand — so prefer instancing one
|
|
4430
4431
|
union of a row over a chain of per-feature booleans, and drop counts the print
|
|
4431
4432
|
cannot show.
|
|
4432
|
-
- **Display placement is view-independent**; only `place(..., { purpose: "export" })` may
|
|
4433
|
-
depend on `view` ([view-dependent-display-place](ERROR-PATTERNS.md#view-dependent-display-place)).
|
|
4434
4433
|
- **Keep geometry backend-agnostic** (kernel calls only); only STEP requires OCCT
|
|
4435
4434
|
([probe-routed-to-occt](ERROR-PATTERNS.md#probe-routed-to-occt),
|
|
4436
4435
|
[occt-holes-watertight-na](ERROR-PATTERNS.md#occt-holes-watertight-na)).
|
package/docs/ERROR-PATTERNS.md
CHANGED
|
@@ -138,12 +138,6 @@ Variant literals under this entry: `extrude: unknown bevel option`, `extrude: be
|
|
|
138
138
|
- **Cause:** OCCT and Manifold WASM must not boot in the same process.
|
|
139
139
|
- **Fix:** Keep OCCT-booting tests in their own files (vitest isolates per file) and boot via `bootOcctKernel()` in a `beforeAll`. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "Testing a part".
|
|
140
140
|
|
|
141
|
-
## view-dependent-display-place
|
|
142
|
-
|
|
143
|
-
- **Symptom:** A sub-part renders correctly in one view but appears misplaced (usually in its other-view pose) after switching views.
|
|
144
|
-
- **Cause:** A `place` that depends on `ctx.view` for `purpose: "display"` — display meshes are built once per sub-part and cached across views.
|
|
145
|
-
- **Fix:** Make display placement view-independent; only `place(..., { purpose: "export" })` may branch on `view`. The rule sees a `place()` behind any `build()`, including one that queries the solid; a `place()` it cannot read stays silent. See [AUTHORING-PARTS.md](AUTHORING-PARTS.md) § "The `PartDefinition` contract".
|
|
146
|
-
|
|
147
141
|
## place-not-rigid
|
|
148
142
|
|
|
149
143
|
- **Symptom:** The exported/printed part is a mirror image of — or a different size than — the same part shown in the assembly/display view. Nothing throws: the preview looks right and only the STL/STEP is wrong, or vice-versa.
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
// Group 6 — the
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
1
|
+
// Group 6 — the place() invariant (docs/AUTHORING-PARTS.md "Display vs export
|
|
2
|
+
// placement"), promoted from a doc-only convention to lint because the animation
|
|
3
|
+
// system leans on place() for every pose-only track: display vs export may differ
|
|
4
|
+
// only by a rigid motion (translate/rotate). A second rule, view-dependent-display-
|
|
5
|
+
// place, forbade a display pose that varies by view; it was retired in 0.142 — since
|
|
6
|
+
// 0.141 the viewer meshes the canonical build once (the mesh cache stamps the view)
|
|
7
|
+
// and re-poses it on every tab switch, so the stale-geometry hazard it named is gone,
|
|
8
|
+
// and as a lint ERROR it refused to load the one-piece-per-sub-part shape a per-view
|
|
9
|
+
// layout needs (partforge-cloud feedback #161/#162).
|
|
10
|
+
// The check runs the place-scope pose probe: place() alone, on a canonical token,
|
|
8
11
|
// so build() may query freely without hiding a finding. A READABLE result carries
|
|
9
12
|
// `baseHash` (CANONICAL for translate/rotate only, another hash when place()
|
|
10
13
|
// reshapes) and `pose`; a place() the probe cannot read (it queries the solid or
|
|
@@ -26,35 +29,10 @@ function viewNames(part, view, p) {
|
|
|
26
29
|
.map(([name]) => name);
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
const poseKey = (pose) => JSON.stringify(pose);
|
|
30
32
|
const readable = (probe) => probe.baseHash !== undefined;
|
|
31
33
|
const probePlace = (sp, ctx) => probeSubPartPose(sp, ctx, { scope: "place" });
|
|
32
34
|
|
|
33
35
|
export const PLACE_RULES = [
|
|
34
|
-
{
|
|
35
|
-
id: "view-dependent-display-place",
|
|
36
|
-
run: ({ part, p, d }) => {
|
|
37
|
-
const out = [];
|
|
38
|
-
const views = Object.keys(isPlainObject(part?.views) ? part.views : {});
|
|
39
|
-
if (views.length < 2) return out;
|
|
40
|
-
for (const [name, sp] of Object.entries(isPlainObject(part?.parts) ? part.parts : {})) {
|
|
41
|
-
const inViews = views.filter((v) => viewNames(part, v, p).includes(name));
|
|
42
|
-
if (inViews.length < 2) continue;
|
|
43
|
-
const probes = inViews.map((view) => probePlace(sp, { view, purpose: "display", p, d }));
|
|
44
|
-
if (!probes.every(readable)) continue;
|
|
45
|
-
const first = probes[0];
|
|
46
|
-
const differs = probes.some((x) => x.baseHash !== first.baseHash || poseKey(x.pose) !== poseKey(first.pose));
|
|
47
|
-
if (differs) {
|
|
48
|
-
out.push(err("view-dependent-display-place",
|
|
49
|
-
`sub-part "${name}" display placement differs between views (${inViews.join(", ")})`,
|
|
50
|
-
"Display meshes are built once per sub-part and cached across views, so a view-dependent display pose shows stale geometry after a tab switch. Only `place(..., { purpose: \"export\" })` may vary; keep the display branch view-independent.",
|
|
51
|
-
`parts.${name}.place`,
|
|
52
|
-
"view-dependent-display-place"));
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return out;
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
36
|
{
|
|
59
37
|
id: "place-not-rigid",
|
|
60
38
|
run: ({ part, p, d }) => {
|
|
@@ -24,3 +24,21 @@ export function printFrameMatrix(subPart, { view, p, d, frame = "posed" }) {
|
|
|
24
24
|
if (!disp.trusted || !exp.trusted || disp.baseHash !== exp.baseHash) return [...IDENTITY];
|
|
25
25
|
return poseDelta(exp.pose, disp.pose);
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
const isIdentity = (m) => m.every((v, i) => Math.abs(v - IDENTITY[i]) < 1e-12);
|
|
29
|
+
|
|
30
|
+
// The map from a DISPLAY-posed solid to the same piece in its print (export) pose, for
|
|
31
|
+
// the oracle's overhang reading — or null when the two poses are the same (no place(),
|
|
32
|
+
// or a place() that moves nothing between purposes) or cannot be read. Place-scope
|
|
33
|
+
// probes first: both poses are place(build), so E · D⁻¹ needs only place() to be
|
|
34
|
+
// readable, and a build that queries its solid cannot make it untrusted. A place()
|
|
35
|
+
// that only the full-scope probe reads falls back to printFrameMatrix's posed branch.
|
|
36
|
+
export function displayToPrintMatrix(subPart, { view, p, d }) {
|
|
37
|
+
if (!subPart?.place) return null;
|
|
38
|
+
const disp = probeSubPartPose(subPart, { view, purpose: "display", p, d }, { scope: "place" });
|
|
39
|
+
const exp = probeSubPartPose(subPart, { view, purpose: "export", p, d }, { scope: "place" });
|
|
40
|
+
const m = disp.trusted && exp.trusted
|
|
41
|
+
? poseDelta(exp.pose, disp.pose)
|
|
42
|
+
: printFrameMatrix(subPart, { view, p, d, frame: "posed" });
|
|
43
|
+
return isIdentity(m) ? null : m;
|
|
44
|
+
}
|
|
@@ -4,7 +4,8 @@ import { assemblyOverlaps } from "../assembly.js";
|
|
|
4
4
|
import { resolveParams, buildPosed } from "../part-model.js";
|
|
5
5
|
import { newReadSink, expandReads } from "../read-recorder.js";
|
|
6
6
|
import { probeSubPartPose } from "../pose-probe-core.js";
|
|
7
|
-
import { composePose } from "../geometry/pose.js";
|
|
7
|
+
import { composePose, invertRigid, transformPositions } from "../geometry/pose.js";
|
|
8
|
+
import { displayToPrintMatrix } from "../materials/print-frame.js";
|
|
8
9
|
import { isSheetPart, sheetMeta, SHEET_CHECK_BUDGET_MS, MARK_DEPTH } from "../sheet/constants.js";
|
|
9
10
|
import { resolveSheet } from "../sheet/resolve.js";
|
|
10
11
|
import { processFor } from "../process/registry.js";
|
|
@@ -128,6 +129,21 @@ const transformPoint = (m, [x, y, z]) => [
|
|
|
128
129
|
m[2] * x + m[6] * y + m[10] * z + m[14],
|
|
129
130
|
];
|
|
130
131
|
|
|
132
|
+
// Overhang of one printed sub-part in its PRINT pose. measure builds the display pose,
|
|
133
|
+
// and a piece shown assembled (a lid closed over its box) is not how it prints: judging
|
|
134
|
+
// its underside there flagged the wrong faces and missed the real ones. The display mesh
|
|
135
|
+
// is carried into the export pose by a rigid matrix — no second build — and the bed is
|
|
136
|
+
// that pose's own lowest Z; `at` comes back in display coordinates, where every other
|
|
137
|
+
// finding of this view sits. An unreadable or identical pose measures as shown.
|
|
138
|
+
function printPoseOverhang(mesh, sp, { view, p, d, maxAngle, displayBedZ }) {
|
|
139
|
+
const m = p ? displayToPrintMatrix(sp, { view, p, d }) : null;
|
|
140
|
+
if (!m) return overhang(mesh, { maxAngle, bedZ: displayBedZ });
|
|
141
|
+
const positions = Float32Array.from(mesh.positions);
|
|
142
|
+
transformPositions(positions, m);
|
|
143
|
+
const out = overhang({ positions, indices: mesh.indices }, { maxAngle });
|
|
144
|
+
return out?.at ? { ...out, at: transformPoint(invertRigid(m), out.at) } : out;
|
|
145
|
+
}
|
|
146
|
+
|
|
131
147
|
// One sheet row's facts, within `budget.left` ms of 2-D work, which it spends (see
|
|
132
148
|
// measure()). A 2-D location is lifted through the sub-part's DISPLAY pose, as the geometry-free
|
|
133
149
|
// probe records it, at mid-thickness; a pose the probe cannot trust (a place() that
|
|
@@ -268,6 +284,10 @@ export function measure(kernel, part, view = Object.keys(part.views)[0], params
|
|
|
268
284
|
// declaration order). A view without a sheet part measures exactly as it always has.
|
|
269
285
|
const sheetView = built.some(({ name }) => isSheetPart(part.parts[name]));
|
|
270
286
|
const sheetParams = sheetView ? resolveParams(part, params, undefined, reads) : null;
|
|
287
|
+
// The overhang reading poses each piece for print (printPoseOverhang), which reads the
|
|
288
|
+
// resolved params through place() — recorded into the same sink, since the reading
|
|
289
|
+
// depends on them.
|
|
290
|
+
const poseParams = overhangAngle != null ? (sheetParams ?? resolveParams(part, params, undefined, reads)) : null;
|
|
271
291
|
// The print-pose sizes are read by one check, the process bed (verify passes whether
|
|
272
292
|
// its profile has one; alone, measure asks the part's own profile), and each costs a
|
|
273
293
|
// second build of the part — so they are built only for a bed.
|
|
@@ -286,14 +306,14 @@ export function measure(kernel, part, view = Object.keys(part.views)[0], params
|
|
|
286
306
|
? minWall(mesh, { bvh: cachedBVH(mesh, bvhCache), maxSamples: minWallSamples, band: wallBands[name] ?? null })
|
|
287
307
|
: null;
|
|
288
308
|
// One pass over the triangles, no index — cheap enough for every lap. The bed
|
|
289
|
-
// is this sub-part's own lowest Z
|
|
290
|
-
// that is never printed (`exportable: false` — a reference ghost, a
|
|
291
|
-
// slab, a placeholder) is not judged.
|
|
292
|
-
//
|
|
293
|
-
// flat beside its base) is a known gap, stated in the authoring docs.
|
|
294
|
-
// A sheet part is laser-cut, never printed: no overhang reading for it either.
|
|
309
|
+
// is this sub-part's own lowest Z in its PRINT pose (printPoseOverhang). A
|
|
310
|
+
// sub-part that is never printed (`exportable: false` — a reference ghost, a
|
|
311
|
+
// probe slab, a placeholder) is not judged. A sheet part is laser-cut, never
|
|
312
|
+
// printed: no overhang reading for it either.
|
|
295
313
|
const printed = part.parts[name]?.exportable !== false && !sheet;
|
|
296
|
-
const oh = overhangAngle != null && printed
|
|
314
|
+
const oh = overhangAngle != null && printed
|
|
315
|
+
? printPoseOverhang(mesh, part.parts[name], { view, ...poseParams, maxAngle: overhangAngle, displayBedZ: b.min[2] })
|
|
316
|
+
: null;
|
|
297
317
|
const vol = solid.volume();
|
|
298
318
|
// Deviation-from-reference: only for a sub-part that declares `reference:
|
|
299
319
|
// "<import name>"` (Task 12 — the gate that holds a parametric rebuild to
|