partforge 0.163.1 → 0.164.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/bin/cli.js +9 -5
- package/docs/AUTHORING-PARTS.md +195 -36
- package/docs/ERROR-PATTERNS.md +6 -0
- package/package.json +1 -1
- package/src/framework/lint/rules-verify.js +35 -2
- package/src/framework/oracle/bed-contact.js +94 -12
- package/src/framework/oracle/dfm-profiles.js +42 -51
- package/src/framework/oracle/gates.js +7 -1
- package/src/framework/oracle/measure.js +40 -15
- package/src/framework/oracle/verify.js +44 -20
- package/src/framework/process/fdm/descriptor.js +48 -0
- package/src/framework/process/registry.js +30 -0
- package/src/framework/process/resin/descriptor.js +19 -0
- package/src/framework/verify-metrics.js +17 -6
- package/types/part.d.ts +23 -9
- package/types/testing.d.ts +17 -3
|
@@ -1,41 +1,7 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
// (a warn, armed only by the part — see overhangAngleFor; resin prints on
|
|
6
|
-
// supports, so it carries none); `bedContact` is the least share (%) of a printed
|
|
7
|
-
// piece's near-bed flat underside that must rest on the bed (a GATE, armed by the
|
|
8
|
-
// profile alone — see bedContactFor; resin prints on supports, so it carries none);
|
|
9
|
-
// `clearance` mm is carried for a future gap check (not enforced yet).
|
|
10
|
-
//
|
|
11
|
-
// `fits`, `holeUndersize` and `bridge` are the numbers an author (or the cloud
|
|
12
|
-
// agent) copies into derive() for a mating feature; nothing in this repo reads
|
|
13
|
-
// them yet. They are guide defaults from the FDM design guides, not measurements:
|
|
14
|
-
// a calibrated printer overrides them through an inline `{ base, fits: {...} }`
|
|
15
|
-
// profile, which REPLACES the fits table whole. `fits` are diametral offsets in
|
|
16
|
-
// mm (negative = interference): press (a pin that never comes out), push (a cap
|
|
17
|
-
// pressed on by hand), slip (a lid that lifts off), moving (a hinge pin or axle),
|
|
18
|
-
// printInPlace (parts printed already assembled). `holeUndersize` is added to a
|
|
19
|
-
// horizontal bore's diameter, since the top of a printed arch sags inward.
|
|
20
|
-
// `bridge` is the longest flat span the process prints unsupported; resin prints
|
|
21
|
-
// on supports, so it carries none.
|
|
22
|
-
export const PROFILES = {
|
|
23
|
-
"fdm-pla": {
|
|
24
|
-
bed: [220, 220, 250], minWall: 1.2, clearance: 0.2, overhang: 45, bedContact: 50,
|
|
25
|
-
fits: { press: -0.1, push: 0.05, slip: 0.25, moving: 0.35, printInPlace: 0.45 },
|
|
26
|
-
holeUndersize: 0.25, bridge: 10,
|
|
27
|
-
},
|
|
28
|
-
"fdm-petg": {
|
|
29
|
-
bed: [220, 220, 250], minWall: 1.5, clearance: 0.3, overhang: 45, bedContact: 50,
|
|
30
|
-
fits: { press: -0.1, push: 0.05, slip: 0.3, moving: 0.4, printInPlace: 0.5 },
|
|
31
|
-
holeUndersize: 0.3, bridge: 10,
|
|
32
|
-
},
|
|
33
|
-
"resin": {
|
|
34
|
-
bed: [120, 68, 160], minWall: 0.6, clearance: 0.1,
|
|
35
|
-
fits: { press: -0.05, push: 0.05, slip: 0.1, moving: 0.15, printInPlace: 0.2 },
|
|
36
|
-
holeUndersize: 0.05,
|
|
37
|
-
},
|
|
38
|
-
};
|
|
1
|
+
// The profiles live on the print process descriptors (process/fdm, process/resin);
|
|
2
|
+
// PROFILES is the registry's merged map, re-exported so every import keeps working.
|
|
3
|
+
import { PRINT_PROFILES as PROFILES } from "../process/registry.js";
|
|
4
|
+
export { PROFILES };
|
|
39
5
|
|
|
40
6
|
export function resolveProfile(spec) {
|
|
41
7
|
if (typeof spec === "string") {
|
|
@@ -62,11 +28,11 @@ export const ORIENTATIONS = ["print"];
|
|
|
62
28
|
export const DEFAULT_OVERHANG_ANGLE = 45;
|
|
63
29
|
|
|
64
30
|
// Which overhang angle a part is checked against, or null when it is not checked.
|
|
65
|
-
// Two ways in
|
|
66
|
-
//
|
|
67
|
-
// not
|
|
68
|
-
//
|
|
69
|
-
//
|
|
31
|
+
// Two ways in:
|
|
32
|
+
// - the print process alone: a profile carrying `overhang` arms it (a profile
|
|
33
|
+
// without one, resin, checks nothing). The reading is a COST, not a warning, so a
|
|
34
|
+
// part still being shaped is told what supports would cost, never nagged;
|
|
35
|
+
// `verify.orientation` no longer takes part in the decision;
|
|
70
36
|
// - an `overhangArea` expectation the author wrote themselves, in any case's
|
|
71
37
|
// `expect` (`expanded`, from gates.js's expandExpectations) — measured against
|
|
72
38
|
// the profile's angle, or DEFAULT_OVERHANG_ANGLE when the profile has none,
|
|
@@ -82,10 +48,10 @@ export function overhangAngleFor(part, process, { expanded = [] } = {}) {
|
|
|
82
48
|
const spec = process ?? part?.verify?.process;
|
|
83
49
|
const fromProfile = spec ? resolveProfile(spec).overhang : undefined;
|
|
84
50
|
const angle = typeof fromProfile === "number" && Number.isFinite(fromProfile) ? fromProfile : null;
|
|
85
|
-
if (
|
|
51
|
+
if (angle != null) return angle;
|
|
86
52
|
const asserted = expanded.some(({ expect }) =>
|
|
87
53
|
Object.entries(expect ?? {}).some(([name, o]) => name !== "_view" && o && typeof o === "object" && "overhangArea" in o));
|
|
88
|
-
return asserted ?
|
|
54
|
+
return asserted ? DEFAULT_OVERHANG_ANGLE : null;
|
|
89
55
|
}
|
|
90
56
|
|
|
91
57
|
// The `bedContactPct` floor an author's own expectation or `orientation: "print"` arms
|
|
@@ -93,12 +59,11 @@ export function overhangAngleFor(part, process, { expanded = [] } = {}) {
|
|
|
93
59
|
export const DEFAULT_BED_CONTACT = 50;
|
|
94
60
|
|
|
95
61
|
// The least share of a printed piece's near-bed flat underside that must touch the bed
|
|
96
|
-
// (oracle/bed-contact.js), or null when it is not checked.
|
|
97
|
-
// PROFILE ALONE arms it
|
|
98
|
-
//
|
|
99
|
-
// piece
|
|
100
|
-
//
|
|
101
|
-
// profile is checked from its first build. Three ways in:
|
|
62
|
+
// (oracle/bed-contact.js), or null when it is not checked. Like overhangAngleFor the
|
|
63
|
+
// PROFILE ALONE arms it, but as a GATE, not a cost: a flat underside hovering a fraction
|
|
64
|
+
// of a millimetre above the piece's lowest point is a defect in whatever orientation the
|
|
65
|
+
// piece is exported — every exporter and slicer lays it on that lowest point — so a part
|
|
66
|
+
// carrying an FDM profile is checked from its first build. Three ways in:
|
|
102
67
|
// - a profile carrying `bedContact` (the FDM profiles; resin carries none);
|
|
103
68
|
// - `verify.orientation: "print"` with NO profile. A named profile decides alone:
|
|
104
69
|
// resin prints on supports, so orientation must not arm it there (overhangAngleFor's
|
|
@@ -118,3 +83,29 @@ export function bedContactFor(part, process, { expanded = [] } = {}) {
|
|
|
118
83
|
Object.entries(expect ?? {}).some(([name, o]) => name !== "_view" && o && typeof o === "object" && "bedContactPct" in o));
|
|
119
84
|
return asserted ? DEFAULT_BED_CONTACT : null;
|
|
120
85
|
}
|
|
86
|
+
|
|
87
|
+
// The `footprintPct` floor an author's own expectation arms the measurement against
|
|
88
|
+
// when the profile names none (a resin part, or no profile at all).
|
|
89
|
+
export const DEFAULT_FOOTPRINT = 10;
|
|
90
|
+
|
|
91
|
+
// The least share of a printed piece's silhouette on the bed that must actually touch
|
|
92
|
+
// it (the footprint, read by bed contact's pass — measure.js `footprintPct`), or null when it
|
|
93
|
+
// is not checked. A COST, like overhang: a piece standing on a point, a line or a curl
|
|
94
|
+
// prints with a brim or a raft, which the user may well choose. Two ways in:
|
|
95
|
+
// - a profile carrying `footprint` (the FDM profiles; resin carries none, since it
|
|
96
|
+
// prints on supports), and `{ base: "fdm-pla", footprint: null }` stays off;
|
|
97
|
+
// - a `footprintPct` expectation the author wrote themselves, in any case's `expect`.
|
|
98
|
+
// `verify.orientation` takes no part (overhangAngleFor's rule), but an invalid value
|
|
99
|
+
// still throws, as it does there.
|
|
100
|
+
export function footprintFor(part, process, { expanded = [] } = {}) {
|
|
101
|
+
const orientation = part?.verify?.orientation;
|
|
102
|
+
if (orientation != null && !ORIENTATIONS.includes(orientation)) {
|
|
103
|
+
throw new Error(`unknown verify.orientation: ${JSON.stringify(orientation)} (known: ${ORIENTATIONS.join(", ")})`);
|
|
104
|
+
}
|
|
105
|
+
const spec = process ?? part?.verify?.process;
|
|
106
|
+
const fromProfile = spec ? resolveProfile(spec).footprint : undefined;
|
|
107
|
+
if (typeof fromProfile === "number" && Number.isFinite(fromProfile)) return fromProfile;
|
|
108
|
+
const asserted = expanded.some(({ expect }) =>
|
|
109
|
+
Object.entries(expect ?? {}).some(([name, o]) => name !== "_view" && o && typeof o === "object" && "footprintPct" in o));
|
|
110
|
+
return asserted ? DEFAULT_FOOTPRINT : null;
|
|
111
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// conservative direction, since that is the full-resolution behaviour every part
|
|
13
13
|
// had before budgets existed. The real error surfaces from verify, which is where
|
|
14
14
|
// a reader can act on it.
|
|
15
|
-
import { resolveProfile, overhangAngleFor, bedContactFor } from "./dfm-profiles.js";
|
|
15
|
+
import { resolveProfile, overhangAngleFor, bedContactFor, footprintFor } from "./dfm-profiles.js";
|
|
16
16
|
import { expandCases } from "./cases.js";
|
|
17
17
|
import { resolveParams } from "../part-model.js";
|
|
18
18
|
import { parseAssertion } from "./assert-dsl.js";
|
|
@@ -56,6 +56,12 @@ export function partBedContact(part, { process, expanded } = {}) {
|
|
|
56
56
|
try { return bedContactFor(part, process, { expanded: expanded ?? expandExpectations(part) }); } catch { return null; }
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// The footprint floor a part is checked against, or null when not checked
|
|
60
|
+
// (dfm-profiles.js footprintFor holds the rule). Total, like partBedContact.
|
|
61
|
+
export function partFootprint(part, { process, expanded } = {}) {
|
|
62
|
+
try { return footprintFor(part, process, { expanded: expanded ?? expandExpectations(part) }); } catch { return null; }
|
|
63
|
+
}
|
|
64
|
+
|
|
59
65
|
export function partGatesMinWall(part, { process, expanded } = {}) {
|
|
60
66
|
try {
|
|
61
67
|
const spec = process ?? part?.verify?.process;
|
|
@@ -14,7 +14,7 @@ import { bounds, meshArea, meshCentroid } from "./mesh.js";
|
|
|
14
14
|
import { minWall, DIAGNOSTIC_SAMPLES } from "./min-wall.js";
|
|
15
15
|
import { overhang } from "./overhang.js";
|
|
16
16
|
import { bedContact } from "./bed-contact.js";
|
|
17
|
-
import { partBedContact, partGatesMinWall, partHasBed, partOverhangAngle, partWallBands } from "./gates.js";
|
|
17
|
+
import { partBedContact, partFootprint, partGatesMinWall, partHasBed, partOverhangAngle, partWallBands } from "./gates.js";
|
|
18
18
|
import { summarizeContours } from "./shape-probe.js";
|
|
19
19
|
|
|
20
20
|
const size = ({ min, max }) => [max[0] - min[0], max[1] - min[1], max[2] - min[2]];
|
|
@@ -133,15 +133,15 @@ const transformPoint = (m, [x, y, z]) => [
|
|
|
133
133
|
// Bed contact read off the piece's real EXPORT build, for a sub-part whose print pose
|
|
134
134
|
// the probes could not read (a place() that queries its solid): judging the display
|
|
135
135
|
// mesh there would pass a bad print orientation or fail a good one. One extra build,
|
|
136
|
-
// in its own cache round like printPoseBboxes, only for such a piece. `at`
|
|
137
|
-
// with the pose unreadable there is no honest way back to
|
|
138
|
-
// export pose that does not build answers `{ error }`, which
|
|
139
|
-
// unevaluated rather than guessing.
|
|
136
|
+
// in its own cache round like printPoseBboxes, only for such a piece. `at` and
|
|
137
|
+
// `lowestAt` are withheld: with the pose unreadable there is no honest way back to
|
|
138
|
+
// display coordinates. An export pose that does not build answers `{ error }`, which
|
|
139
|
+
// verify reports as unevaluated rather than guessing.
|
|
140
140
|
function exportPoseBedContact(kernel, part, name, view, { p, d }) {
|
|
141
141
|
kernel.beginSubPart?.(`oracle:print:${view}`);
|
|
142
142
|
try {
|
|
143
143
|
const out = bedContact(buildPosed(kernel, part, name, { purpose: "export", view, p, d }).toMesh());
|
|
144
|
-
return out ? { ...out, at: null } : out;
|
|
144
|
+
return out ? { ...out, at: null, lowestAt: null } : out;
|
|
145
145
|
} catch (e) {
|
|
146
146
|
return { error: String(e?.message || e).slice(0, 200) };
|
|
147
147
|
} finally { kernel.endSubPart?.(); }
|
|
@@ -153,8 +153,9 @@ function exportPoseBedContact(kernel, part, name, view, { p, d }) {
|
|
|
153
153
|
// box) is not how it prints: judging its underside there flagged the wrong faces and
|
|
154
154
|
// missed the real ones. The display mesh is carried into the export pose by a rigid
|
|
155
155
|
// matrix — no second build, and one copy shared by both readings — and the bed is that
|
|
156
|
-
// pose's own lowest Z; each `at` comes back in display
|
|
157
|
-
// finding of this view sits. An unreadable or identical
|
|
156
|
+
// pose's own lowest Z; each `at` (and bed contact's `lowestAt`) comes back in display
|
|
157
|
+
// coordinates, where every other finding of this view sits. An unreadable or identical
|
|
158
|
+
// pose measures as shown.
|
|
158
159
|
// `m` is displayToPrintMatrix's answer, resolved by the caller: the measure memo
|
|
159
160
|
// keys on it, since the pose comes from place() and is not part of the solid's hash.
|
|
160
161
|
function printPoseUnderside(mesh, m, { maxAngle, bed, displayBedZ }) {
|
|
@@ -163,7 +164,15 @@ function printPoseUnderside(mesh, m, { maxAngle, bed, displayBedZ }) {
|
|
|
163
164
|
posed = { positions: Float32Array.from(mesh.positions), indices: mesh.indices };
|
|
164
165
|
transformPositions(posed.positions, m);
|
|
165
166
|
}
|
|
166
|
-
const back = (out) =>
|
|
167
|
+
const back = (out) => {
|
|
168
|
+
if (!m || !out) return out;
|
|
169
|
+
const inv = invertRigid(m);
|
|
170
|
+
return {
|
|
171
|
+
...out,
|
|
172
|
+
...(out.at ? { at: transformPoint(inv, out.at) } : {}),
|
|
173
|
+
...(out.lowestAt ? { lowestAt: transformPoint(inv, out.lowestAt) } : {}),
|
|
174
|
+
};
|
|
175
|
+
};
|
|
167
176
|
return {
|
|
168
177
|
oh: maxAngle != null ? back(posed ? overhang(posed, { maxAngle }) : overhang(mesh, { maxAngle, bedZ: displayBedZ })) : null,
|
|
169
178
|
bc: bed ? back(posed ? bedContact(posed) : bedContact(mesh, { bedZ: displayBedZ })) : null,
|
|
@@ -293,9 +302,10 @@ export function measure(kernel, part, view = Object.keys(part.views)[0], params
|
|
|
293
302
|
// Declared wall bands, per sub-part, for exactly these params (Task 5). Resolved
|
|
294
303
|
// once per measure; a bad declaration throws here, which is a measure error.
|
|
295
304
|
const wallBands = opts.minWall ? partWallBands(part, params, reads) : {};
|
|
296
|
-
// Overhang is measured
|
|
297
|
-
// overhangAngleFor
|
|
298
|
-
//
|
|
305
|
+
// Overhang is measured for a part whose print process arms it (dfm-profiles.js
|
|
306
|
+
// overhangAngleFor: a profile carrying `overhang`, or the author's own
|
|
307
|
+
// `overhangArea` expectation) — everything else reads null. verify hands the angle
|
|
308
|
+
// it resolved in (a `process` override changes it); `null` there is an explicit
|
|
299
309
|
// "not checked", not a fallback. The angle used is stamped on the result as
|
|
300
310
|
// `measuredOverhang`, and verify refuses a seed whose stamp disagrees with the
|
|
301
311
|
// angle it needs — the min-wall superset rule's counterpart, since a reading
|
|
@@ -304,8 +314,11 @@ export function measure(kernel, part, view = Object.keys(part.views)[0], params
|
|
|
304
314
|
// Bed contact is measured for a part whose profile (or orientation, or its own
|
|
305
315
|
// expectation) arms it — dfm-profiles.js bedContactFor. Unlike overhang the reading
|
|
306
316
|
// does not depend on the threshold, so the stamp is a boolean: `measuredBedContact`.
|
|
307
|
-
// verify passes its own answer (a `process` override can arm or disarm it).
|
|
308
|
-
|
|
317
|
+
// verify passes its own answer (a `process` override can arm or disarm it). The same
|
|
318
|
+
// pass reads the footprint cost (dfm-profiles.js footprintFor), so a part whose
|
|
319
|
+
// footprint alone is checked arms it too — here and in verify — and one stamp, and
|
|
320
|
+
// one memo-key bit, covers both readings.
|
|
321
|
+
const bedArmed = (opts.bedContact !== undefined ? opts.bedContact : (partBedContact(part) ?? partFootprint(part))) != null;
|
|
309
322
|
const underside = overhangAngle != null || bedArmed;
|
|
310
323
|
const subBounds = [];
|
|
311
324
|
// Sheet parts: a view holding one sizes each printed sub-part in its print pose
|
|
@@ -341,7 +354,7 @@ export function measure(kernel, part, view = Object.keys(part.views)[0], params
|
|
|
341
354
|
const printMatrix = printPose?.matrix ?? null;
|
|
342
355
|
// A pose the probes could not read is not the identity: bed contact then reads the
|
|
343
356
|
// export build itself (exportPoseBedContact). Overhang keeps its display-pose
|
|
344
|
-
// fallback — a
|
|
357
|
+
// fallback — a cost, and unchanged here.
|
|
345
358
|
const exportBed = bedArmed && printPose && !printPose.trusted;
|
|
346
359
|
// Memo key = the inputs this sub-part's facts actually depend on, besides its
|
|
347
360
|
// own geometry hash: whether min-wall ran, at what sample budget and against
|
|
@@ -447,6 +460,16 @@ export function measure(kernel, part, view = Object.keys(part.views)[0], params
|
|
|
447
460
|
bedHoverArea: bc ? bc.hoverArea : null,
|
|
448
461
|
bedHoverGap: bc?.gap ?? null,
|
|
449
462
|
bedHoverAt: bc?.at ?? null,
|
|
463
|
+
// The footprint (same pass): the share of the piece's silhouette on the bed that
|
|
464
|
+
// actually touches it, in percent (clamped at 100: the silhouette is a
|
|
465
|
+
// raster, a cell or so off along its edges) — near 0 for a
|
|
466
|
+
// piece standing on a point, a line or a curl, which bedContactPct passes since
|
|
467
|
+
// nothing flat hovers, and near 100 for anything that stands on its whole
|
|
468
|
+
// underside, however little of its bounding box that fills (a tube on its rim, a
|
|
469
|
+
// frame, a diagonal bar). Null when not measured or the silhouette is empty. Located at
|
|
470
|
+
// the piece's lowest point, in display coordinates.
|
|
471
|
+
footprintPct: bc && bc.downArea > 0 ? Math.min(100, (100 * bc.contactArea) / bc.downArea) : null,
|
|
472
|
+
footprintAt: bc?.lowestAt ?? null,
|
|
450
473
|
...(exportBc?.error ? { bedContactError: exportBc.error } : {}),
|
|
451
474
|
// The process's 2-D facts on a sheet part (sheetRowFacts), null on every other.
|
|
452
475
|
sheet: sheet
|
|
@@ -537,6 +560,8 @@ export function measure(kernel, part, view = Object.keys(part.views)[0], params
|
|
|
537
560
|
measuredOverhang: overhangAngle ?? null,
|
|
538
561
|
// Whether the bed-contact pass ran — the same kind of stamp, read by the same gate.
|
|
539
562
|
measuredBedContact: bedArmed,
|
|
563
|
+
// The footprint is read by the same pass, so the same stamp.
|
|
564
|
+
measuredFootprint: bedArmed,
|
|
540
565
|
// Companion stamp to measuredMinWall, and read the same way: whether the pass
|
|
541
566
|
// ran, said by the pass itself rather than claimed by whoever holds the result.
|
|
542
567
|
measuredGaps,
|
|
@@ -2,7 +2,7 @@ import { parseAssertion, evaluateAssertion } from "./assert-dsl.js";
|
|
|
2
2
|
import { measure as defaultMeasure } from "./measure.js";
|
|
3
3
|
import { pairKey, CONTACT_EPS } from "./gaps.js";
|
|
4
4
|
import { heldCheck } from "./held.js";
|
|
5
|
-
import { resolveProfile, overhangAngleFor, bedContactFor } from "./dfm-profiles.js";
|
|
5
|
+
import { resolveProfile, overhangAngleFor, bedContactFor, footprintFor } from "./dfm-profiles.js";
|
|
6
6
|
import { expandExpectations, partGatesMinWall } from "./gates.js";
|
|
7
7
|
import { relevanceHash } from "../param-deps.js";
|
|
8
8
|
import { SUBPART_METRICS, VIEW_METRICS, sidesSpecError } from "../verify-metrics.js";
|
|
@@ -121,12 +121,13 @@ function pairGapChecks(facts, { contacts, clearance }, subPartNames) {
|
|
|
121
121
|
return checks;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
function check(scope, subpart, metric, spec, registry, factsObj) {
|
|
124
|
+
function check(scope, subpart, metric, spec, registry, factsObj, accept = []) {
|
|
125
125
|
const reg = registry[metric];
|
|
126
126
|
if (!reg) throw new Error(`unknown ${scope} metric "${metric}"${subpart ? ` on sub-part "${subpart}"` : ""}`);
|
|
127
127
|
const { expr, hint: partHint } = normalizeExpectation(spec);
|
|
128
128
|
const actual = reg.extract(factsObj);
|
|
129
|
-
const base = { scope, subpart, metric, kind: reg.kind, expr: String(expr)
|
|
129
|
+
const base = { scope, subpart, metric, kind: reg.kind, expr: String(expr),
|
|
130
|
+
...(reg.kind === "cost" ? { accepted: accept.includes(reg.costId) } : {}) };
|
|
130
131
|
if (actual === null || actual === undefined) {
|
|
131
132
|
if (reg.manifoldOnly) return { ...base, actual, status: "skip", pass: null, message: "n/a (OCCT backend)" };
|
|
132
133
|
if (metric === "minWall") {
|
|
@@ -156,7 +157,7 @@ function check(scope, subpart, metric, spec, registry, factsObj) {
|
|
|
156
157
|
return { ...base, actual, status: "skip", pass: null, message: "unavailable" };
|
|
157
158
|
}
|
|
158
159
|
const { pass, message } = evaluateAssertion(parseAssertion(expr), actual);
|
|
159
|
-
const status = pass ? "pass" : reg.kind === "warn" ? "warn" : "fail";
|
|
160
|
+
const status = pass ? "pass" : reg.kind === "warn" ? "warn" : reg.kind === "cost" ? "cost" : "fail";
|
|
160
161
|
const out = { ...base, actual, status, pass, message };
|
|
161
162
|
// A measurement caveat rides along whatever the verdict — a min-wall reading
|
|
162
163
|
// taken from a sample still passed, but the reader should know it was a sample.
|
|
@@ -178,14 +179,14 @@ function check(scope, subpart, metric, spec, registry, factsObj) {
|
|
|
178
179
|
// (`bounds: { min, max }` → `bounds.min`, `bounds.max`), each judged by `check()` against
|
|
179
180
|
// its side's own registry entry. A malformed compound value throws, as a malformed
|
|
180
181
|
// expression does.
|
|
181
|
-
function checksFor(scope, subpart, metric, spec, registry, factsObj) {
|
|
182
|
+
function checksFor(scope, subpart, metric, spec, registry, factsObj, accept = []) {
|
|
182
183
|
const reg = registry[metric];
|
|
183
|
-
if (!reg?.sides) return [check(scope, subpart, metric, spec, registry, factsObj)];
|
|
184
|
+
if (!reg?.sides) return [check(scope, subpart, metric, spec, registry, factsObj, accept)];
|
|
184
185
|
const bad = sidesSpecError(metric, reg, spec);
|
|
185
186
|
if (bad) throw new Error(`${scope === "view" ? "_view" : subpart}: ${bad}`);
|
|
186
187
|
return Object.entries(spec).map(([side, sideSpec]) => {
|
|
187
188
|
const name = `${metric}.${side}`;
|
|
188
|
-
return check(scope, subpart, name, sideSpec, { [name]: reg.sides[side] }, factsObj);
|
|
189
|
+
return check(scope, subpart, name, sideSpec, { [name]: reg.sides[side] }, factsObj, accept);
|
|
189
190
|
});
|
|
190
191
|
}
|
|
191
192
|
|
|
@@ -214,8 +215,8 @@ const SHEET_BUDGET_HINT = "The 2-D sheet checks did not fit their time budget
|
|
|
214
215
|
// The print checks: measure() casts no rays and runs no overhang pass on a sheet part.
|
|
215
216
|
// Declared on one, each is a skip — declared, never evaluated, so a sheet-only forge
|
|
216
217
|
// that declares only these has no verdict rather than a vacuous pass.
|
|
217
|
-
const PRINT_ONLY = new Set(["minWall", "wall", "overhangArea", "bedContactPct"]);
|
|
218
|
-
const PRINT_ONLY_HINT = "minWall, wall, overhangArea and
|
|
218
|
+
const PRINT_ONLY = new Set(["minWall", "wall", "overhangArea", "bedContactPct", "footprintPct"]);
|
|
219
|
+
const PRINT_ONLY_HINT = "minWall, wall, overhangArea, bedContactPct and footprintPct are print checks, and a sheet part is cut, not printed: nothing measures them here. For a laser part declare sheetBridge (the narrowest web or finger) or sheetGap (the narrowest hole or slot) instead.";
|
|
219
220
|
|
|
220
221
|
// A sheet reading the geometry engine could not take (SheetFacts.readErrors): a
|
|
221
222
|
// volunteered check warns with the reason; a declared one warns with it too, and is
|
|
@@ -268,12 +269,16 @@ function sheetRowChecks(s, declaredExp) {
|
|
|
268
269
|
}
|
|
269
270
|
|
|
270
271
|
// Pure policy: profile rules + per-part expect → checks for one case's facts.
|
|
271
|
-
// `overhang` is the angle the part
|
|
272
|
-
// or null/undefined: only then does the profile's overhang rule apply, as a
|
|
273
|
-
//
|
|
272
|
+
// `overhang` is the angle the part's process arms (dfm-profiles.js overhangAngleFor),
|
|
273
|
+
// or null/undefined: only then does the profile's overhang rule apply, as a 1 mm²
|
|
274
|
+
// cost — a chamfer sitting exactly on the angle sheds slivers.
|
|
274
275
|
// `bedContact` is the floor the part is checked against (dfm-profiles.js bedContactFor),
|
|
275
276
|
// or null/undefined: only then does every PRINTED sub-part get `bedContactPct >= floor`.
|
|
276
|
-
|
|
277
|
+
// `footprint` the same for the footprint cost (dfm-profiles.js footprintFor):
|
|
278
|
+
// `footprintPct >= floor` on every printed sub-part.
|
|
279
|
+
// `accept` the cost ids the user accepted (`verify.accept`), stamped on each cost the
|
|
280
|
+
// process volunteered (never on one the author wrote in `expect`).
|
|
281
|
+
export function evaluateCase(facts, { profile, expect, subPartNames, exportableNames, overhang = null, bedContact = null, footprint = null, accept = [] }) {
|
|
277
282
|
const checks = [];
|
|
278
283
|
// contacts/clearance/held are pair-derived, not scalar view metrics — peel
|
|
279
284
|
// them off before the registry loop. held.js explains the exportable bound.
|
|
@@ -301,6 +306,7 @@ export function evaluateCase(facts, { profile, expect, subPartNames, exportableN
|
|
|
301
306
|
// is withheld rather than read as "unavailable" — which would count as answered.
|
|
302
307
|
const overhangSkipped = overhang != null && (facts.measuredOverhang ?? null) === null;
|
|
303
308
|
const bedContactSkipped = bedContact != null && facts.measuredBedContact !== true;
|
|
309
|
+
const footprintSkipped = footprint != null && facts.measuredFootprint !== true;
|
|
304
310
|
// A piece that is never printed (`exportable: false`) is not judged for bed contact;
|
|
305
311
|
// the floor is applied to the rest. Without the vocabulary, every row is printed.
|
|
306
312
|
const printedNames = exportableNames ? new Set(exportableNames) : null;
|
|
@@ -319,9 +325,14 @@ export function evaluateCase(facts, { profile, expect, subPartNames, exportableN
|
|
|
319
325
|
...(profile?.minWall != null ? { minWall: `>=${profile.minWall}` } : {}),
|
|
320
326
|
...(overhang != null ? { overhangArea: "<=1" } : {}),
|
|
321
327
|
...(bedContact != null && (!printedNames || printedNames.has(s.name)) ? { bedContactPct: `>=${bedContact}` } : {}),
|
|
328
|
+
...(footprint != null && (!printedNames || printedNames.has(s.name)) ? { footprintPct: `>=${footprint}` } : {}),
|
|
322
329
|
...(expect?.[s.name] ?? {}),
|
|
323
330
|
};
|
|
324
|
-
|
|
331
|
+
// `accept` covers only the process's own default check: a cost metric the author
|
|
332
|
+
// wrote in `expect` is the user's own limit and keeps its meaning, never accepted.
|
|
333
|
+
const authored = expect?.[s.name] ?? {};
|
|
334
|
+
const acceptFor = (metric) => (Object.hasOwn(authored, metric) ? [] : accept);
|
|
335
|
+
for (const [metric, expr] of Object.entries(merged)) for (const c of checksFor("subpart", s.name, metric, expr, SUBPART_METRICS, s, acceptFor(metric))) {
|
|
325
336
|
if (minWallSkipped && (metric === "minWall" || metric === "wall") && c.actual == null) {
|
|
326
337
|
checks.push({ ...c, unevaluated: true, message: "not measured (quick check)",
|
|
327
338
|
hint: `re-run this check without \`quick\` to measure ${metric === "wall" ? "the wall band" : "min wall"}` });
|
|
@@ -332,10 +343,11 @@ export function evaluateCase(facts, { profile, expect, subPartNames, exportableN
|
|
|
332
343
|
hint: "re-run this check without `quick` to measure overhang" });
|
|
333
344
|
continue;
|
|
334
345
|
}
|
|
335
|
-
|
|
346
|
+
// The footprint is read by bed contact's pass, so it is withheld the same ways.
|
|
347
|
+
if ((metric === "bedContactPct" || metric === "footprintPct") && c.actual == null && s.bedContactError) {
|
|
336
348
|
checks.push({ ...c, status: "skip", pass: null, unevaluated: true,
|
|
337
349
|
message: `not measured: its print (export) pose did not build — ${s.bedContactError}`,
|
|
338
|
-
hint: "bed contact is read in the print pose, and this piece's could not be posed by its place() alone, so its export build was read instead — and that build failed. Fix the export pose; the check then runs
|
|
350
|
+
hint: `${metric === "footprintPct" ? "the footprint" : "bed contact"} is read in the print pose, and this piece's could not be posed by its place() alone, so its export build was read instead — and that build failed. Fix the export pose; the check then runs.` });
|
|
339
351
|
continue;
|
|
340
352
|
}
|
|
341
353
|
if (bedContactSkipped && metric === "bedContactPct" && c.actual == null) {
|
|
@@ -343,6 +355,11 @@ export function evaluateCase(facts, { profile, expect, subPartNames, exportableN
|
|
|
343
355
|
hint: "re-run this check without `quick` to measure bed contact" });
|
|
344
356
|
continue;
|
|
345
357
|
}
|
|
358
|
+
if (footprintSkipped && metric === "footprintPct" && c.actual == null) {
|
|
359
|
+
checks.push({ ...c, unevaluated: true, message: "not measured (quick check)",
|
|
360
|
+
hint: "re-run this check without `quick` to measure the footprint" });
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
346
363
|
checks.push(c);
|
|
347
364
|
}
|
|
348
365
|
}
|
|
@@ -372,6 +389,8 @@ export function verify(kernel, part, { process, view, measureFn = defaultMeasure
|
|
|
372
389
|
// Throws on a bad `verify.orientation`, the same loudness as a bad profile name.
|
|
373
390
|
const overhangAngle = overhangAngleFor(part, process, { expanded });
|
|
374
391
|
const bedContactFloor = bedContactFor(part, process, { expanded });
|
|
392
|
+
const footprintFloor = footprintFor(part, process, { expanded });
|
|
393
|
+
const accept = Array.isArray(part?.verify?.accept) ? part.verify.accept : [];
|
|
375
394
|
// Reuse rule (spec §4): case B may reuse a measurement taken at params A when
|
|
376
395
|
// B agrees with A on every param that measurement READ (recorded by measure()
|
|
377
396
|
// itself). Sound for the same reason the mesh cache is: a deterministic build
|
|
@@ -437,7 +456,10 @@ export function verify(kernel, part, { process, view, measureFn = defaultMeasure
|
|
|
437
456
|
const overhangMatches = (seed?.result?.measuredOverhang ?? null) === (overhangAngle ?? null);
|
|
438
457
|
// Bed contact's half: a seed that skipped the pass cannot answer an armed gate. The
|
|
439
458
|
// reverse (a seed that ran it, for a run that does not need it) is a free superset.
|
|
440
|
-
|
|
459
|
+
// The footprint cost rides the same pass, so either floor needs it.
|
|
460
|
+
// measure() reads `bedContact` only as "run the pass", so either floor arms it.
|
|
461
|
+
const armsBedPass = bedContactFloor ?? footprintFloor;
|
|
462
|
+
const bedContactMatches = armsBedPass == null || seed?.result?.measuredBedContact === true;
|
|
441
463
|
// The print-pose half (views holding a sheet part): a seed measured without the print
|
|
442
464
|
// sizes cannot answer a bed, so it is admitted only when this run needs no bed.
|
|
443
465
|
const needPrintBboxes = profile?.bed != null;
|
|
@@ -456,7 +478,7 @@ export function verify(kernel, part, { process, view, measureFn = defaultMeasure
|
|
|
456
478
|
// for every case buys nothing. (A seed measured WITH probes is a superset in
|
|
457
479
|
// the same way a min-wall seed is: the extra key is simply never read here.)
|
|
458
480
|
const result = measureFn(kernel, part, view, params,
|
|
459
|
-
{ minWall: needMinWall, probes: false, overhang: overhangAngle, bedContact:
|
|
481
|
+
{ minWall: needMinWall, probes: false, overhang: overhangAngle, bedContact: armsBedPass, printBboxes: needPrintBboxes });
|
|
460
482
|
entries.push({ params: fp, result });
|
|
461
483
|
return result;
|
|
462
484
|
};
|
|
@@ -477,7 +499,7 @@ export function verify(kernel, part, { process, view, measureFn = defaultMeasure
|
|
|
477
499
|
}];
|
|
478
500
|
const caseResults = expanded.map(({ name, params, expect }) => {
|
|
479
501
|
const facts = measureCase(params);
|
|
480
|
-
return { name, params, checks: facts ? evaluateCase(facts, { profile, expect, subPartNames, exportableNames, overhang: overhangAngle, bedContact: bedContactFloor }) : notMeasured(name) };
|
|
502
|
+
return { name, params, checks: facts ? evaluateCase(facts, { profile, expect, subPartNames, exportableNames, overhang: overhangAngle, bedContact: bedContactFloor, footprint: footprintFloor, accept }) : notMeasured(name) };
|
|
481
503
|
});
|
|
482
504
|
// VACUOUS VERIFY. A part that declares nothing — no profile, an empty or absent
|
|
483
505
|
// `expect` — used to come back `ok: true` with zero checks, and every reader
|
|
@@ -512,11 +534,12 @@ export function verify(kernel, part, { process, view, measureFn = defaultMeasure
|
|
|
512
534
|
declared++;
|
|
513
535
|
if (c.status !== "skip" && !c.unevaluated) evaluated++;
|
|
514
536
|
}
|
|
515
|
-
const declaresAnything = profile != null || overhangAngle != null || bedContactFloor != null
|
|
537
|
+
const declaresAnything = profile != null || overhangAngle != null || bedContactFloor != null || footprintFloor != null
|
|
516
538
|
|| expanded.some(({ expect }) => Object.values(expect ?? {}).some((o) => o && typeof o === "object" && Object.keys(o).length > 0));
|
|
517
539
|
const failures = all.filter((c) => c.status === "fail");
|
|
518
540
|
const unevaluated = all.filter((c) => c.unevaluated);
|
|
519
541
|
const warnings = all.filter((c) => c.status === "warn");
|
|
542
|
+
const costs = all.filter((c) => c.status === "cost");
|
|
520
543
|
if (evaluated === 0 && unevaluated.length === 0) {
|
|
521
544
|
warnings.push({ case: null, scope: "part", subpart: null, metric: "expectations", kind: "warn", expr: "evaluated",
|
|
522
545
|
actual: 0, status: "warn", pass: null,
|
|
@@ -534,6 +557,7 @@ export function verify(kernel, part, { process, view, measureFn = defaultMeasure
|
|
|
534
557
|
cases: caseResults,
|
|
535
558
|
failures,
|
|
536
559
|
warnings,
|
|
560
|
+
costs,
|
|
537
561
|
unevaluated,
|
|
538
562
|
declared,
|
|
539
563
|
evaluated,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Reusable design-for-manufacturing process profiles — a manufacturing technique
|
|
2
|
+
// as DATA: which checks apply and at what thresholds. `bed` is the build volume
|
|
3
|
+
// [x,y,z] in mm (a hard bbox-fit gate); `minWall` mm (a warn); `overhang` is the
|
|
4
|
+
// steepest unsupported face the process prints cleanly, in degrees from vertical
|
|
5
|
+
// (a cost — supports — armed by the profile alone, see overhangAngleFor; resin
|
|
6
|
+
// prints on supports, so it carries none); `bedContact` is the least share (%) of a printed
|
|
7
|
+
// piece's near-bed flat underside that must rest on the bed (a GATE, armed by the
|
|
8
|
+
// profile alone — see bedContactFor; resin prints on supports, so it carries none);
|
|
9
|
+
// `clearance` mm is carried for a future gap check (not enforced yet).
|
|
10
|
+
//
|
|
11
|
+
// `fits`, `holeUndersize` and `bridge` are the numbers an author (or the cloud
|
|
12
|
+
// agent) copies into derive() for a mating feature; nothing in this repo reads
|
|
13
|
+
// them yet. They are guide defaults from the FDM design guides, not measurements:
|
|
14
|
+
// a calibrated printer overrides them through an inline `{ base, fits: {...} }`
|
|
15
|
+
// profile, which REPLACES the fits table whole. `fits` are diametral offsets in
|
|
16
|
+
// mm (negative = interference): press (a pin that never comes out), push (a cap
|
|
17
|
+
// pressed on by hand), slip (a lid that lifts off), moving (a hinge pin or axle),
|
|
18
|
+
// printInPlace (parts printed already assembled). `holeUndersize` is added to a
|
|
19
|
+
// horizontal bore's diameter, since the top of a printed arch sags inward.
|
|
20
|
+
// `bridge` is the longest flat span the process prints unsupported; resin prints
|
|
21
|
+
// on supports, so it carries none.
|
|
22
|
+
//
|
|
23
|
+
// `footprint` is the percent of a printed piece's posed base area resting on the
|
|
24
|
+
// bed below which the footprint cost fires.
|
|
25
|
+
//
|
|
26
|
+
// Import-free, like every process descriptor: lint and the oracle read it through
|
|
27
|
+
// process/registry.js.
|
|
28
|
+
export const FDM = {
|
|
29
|
+
id: "fdm",
|
|
30
|
+
label: "FDM 3D printing",
|
|
31
|
+
stock: "filament",
|
|
32
|
+
docId: "printed-parts",
|
|
33
|
+
profiles: {
|
|
34
|
+
"fdm-pla": {
|
|
35
|
+
bed: [220, 220, 250], minWall: 1.2, clearance: 0.2, overhang: 45, bedContact: 50, footprint: 10,
|
|
36
|
+
fits: { press: -0.1, push: 0.05, slip: 0.25, moving: 0.35, printInPlace: 0.45 },
|
|
37
|
+
holeUndersize: 0.25, bridge: 10,
|
|
38
|
+
},
|
|
39
|
+
"fdm-petg": {
|
|
40
|
+
bed: [220, 220, 250], minWall: 1.5, clearance: 0.3, overhang: 45, bedContact: 50, footprint: 10,
|
|
41
|
+
fits: { press: -0.1, push: 0.05, slip: 0.3, moving: 0.4, printInPlace: 0.5 },
|
|
42
|
+
holeUndersize: 0.3, bridge: 10,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
costs: { supports: "overhangArea", footprint: "footprintPct" },
|
|
46
|
+
metrics: {},
|
|
47
|
+
lintRules: [],
|
|
48
|
+
};
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import { isSheetPart } from "../sheet/constants.js";
|
|
8
8
|
import { LASER } from "./laser/descriptor.js";
|
|
9
9
|
import { ROUTER } from "./cnc-router/descriptor.js";
|
|
10
|
+
import { FDM } from "./fdm/descriptor.js";
|
|
11
|
+
import { RESIN } from "./resin/descriptor.js";
|
|
10
12
|
|
|
11
13
|
export const PROCESSES = [LASER, ROUTER];
|
|
12
14
|
export const PROCESS_IDS = PROCESSES.map((d) => d.id);
|
|
@@ -23,3 +25,31 @@ export const SHEET_METRICS = Object.assign({}, ...PROCESSES.map((d) => d.metrics
|
|
|
23
25
|
|
|
24
26
|
// Every process's own lint rules; lint/rules-sheet.js appends them to SHEET_RULES.
|
|
25
27
|
export const PROCESS_LINT_RULES = PROCESSES.flatMap((d) => d.lintRules);
|
|
28
|
+
|
|
29
|
+
// Print processes are their own list: PROCESSES stays the SHEET list (exporters,
|
|
30
|
+
// destinations, SHEET_METRICS and sheet/part.js's PROCESS_IDS check all read it), so
|
|
31
|
+
// fdm and resin never pass as a sheet part's process.
|
|
32
|
+
export const PRINT_PROCESSES = [FDM, RESIN];
|
|
33
|
+
export const PRINT_PROCESS_IDS = PRINT_PROCESSES.map((d) => d.id);
|
|
34
|
+
export const ALL_PROCESSES = [...PROCESSES, ...PRINT_PROCESSES];
|
|
35
|
+
|
|
36
|
+
export const printProcessById = (id) => PRINT_PROCESSES.find((d) => d.id === id) ?? null;
|
|
37
|
+
|
|
38
|
+
// The DFM profiles of every print process as one map, keyed by profile id.
|
|
39
|
+
export const PRINT_PROFILES = Object.freeze(Object.assign({}, ...PRINT_PROCESSES.map((d) => d.profiles)));
|
|
40
|
+
|
|
41
|
+
// The print process owning a profile spec: a profile id, or an inline object whose
|
|
42
|
+
// `base` (recursively) names one. Anything else is null.
|
|
43
|
+
export function printProcessFor(spec) {
|
|
44
|
+
let id = spec;
|
|
45
|
+
// A cyclic inline profile (`p.base = p`) is malformed, not a loop to follow: lint calls
|
|
46
|
+
// this on user-authored parts, and a hang there takes the editor with it.
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
while (id && typeof id === "object") {
|
|
49
|
+
if (seen.has(id)) return null;
|
|
50
|
+
seen.add(id);
|
|
51
|
+
id = id.base;
|
|
52
|
+
}
|
|
53
|
+
if (typeof id !== "string") return null;
|
|
54
|
+
return PRINT_PROCESSES.find((d) => Object.hasOwn(d.profiles, id)) ?? null;
|
|
55
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Resin printing as DATA: see process/fdm/descriptor.js for what each profile field
|
|
2
|
+
// means. Resin prints on supports, so it carries no overhang, bedContact, footprint
|
|
3
|
+
// or bridge, and no costs. Import-free, like every process descriptor.
|
|
4
|
+
export const RESIN = {
|
|
5
|
+
id: "resin",
|
|
6
|
+
label: "Resin 3D printing",
|
|
7
|
+
stock: "resin",
|
|
8
|
+
docId: "printed-parts",
|
|
9
|
+
profiles: {
|
|
10
|
+
"resin": {
|
|
11
|
+
bed: [120, 68, 160], minWall: 0.6, clearance: 0.1,
|
|
12
|
+
fits: { press: -0.05, push: 0.05, slip: 0.1, moving: 0.15, printInPlace: 0.2 },
|
|
13
|
+
holeUndersize: 0.05,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
costs: {},
|
|
17
|
+
metrics: {},
|
|
18
|
+
lintRules: [],
|
|
19
|
+
};
|
|
@@ -100,12 +100,14 @@ export const SUBPART_METRICS = {
|
|
|
100
100
|
? `no reading from the ${sampled} of ${total} triangles sampled — not a clean bill of health; a thin spot may exist between samples`
|
|
101
101
|
: `sampled ${sampled} of ${total} triangles — an upper bound; a thinner spot may exist between samples`;
|
|
102
102
|
} },
|
|
103
|
-
// Unsupported downward-facing area (oracle/overhang.js), in mm². Measured
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
// A
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
// Unsupported downward-facing area (oracle/overhang.js), in mm². Measured for a
|
|
104
|
+
// part whose print process carries an `overhang` angle (the process alone arms it)
|
|
105
|
+
// or that asserts it; elsewhere `extract` returns null and check() skips it.
|
|
106
|
+
// A cost, never a gate or a warning: needing supports is a design choice with a
|
|
107
|
+
// price (a bridge reads as a ceiling and cannot be told apart), so it is listed
|
|
108
|
+
// under `costs` and the user may accept it (`verify.accept: ["supports"]`).
|
|
109
|
+
overhangArea: { kind: "cost", costId: "supports", extract: (s) => s.overhangArea,
|
|
110
|
+
hint: "unsupported downward-facing surface at the reported location — reorient the part so the face is vertical or on the bed, chamfer it to the process's overhang angle, split it into a bridged or supported feature; when the user chooses supports instead, record it with verify.accept: [\"supports\"]",
|
|
109
111
|
locate: (s) => s.overhangAt,
|
|
110
112
|
note: (s) => (s.overhangAngle != null ? `steepest unsupported face ${s.overhangAngle.toFixed(1)}° from vertical` : null) },
|
|
111
113
|
// How much of a printed piece's near-bed flat underside rests on the bed
|
|
@@ -121,6 +123,15 @@ export const SUBPART_METRICS = {
|
|
|
121
123
|
note: (s) => (s.bedHoverGap != null
|
|
122
124
|
? `${s.bedHoverArea.toFixed(1)} mm² of flat underside sits ${s.bedHoverGap.toFixed(2)} mm above the bed; ${s.bedContactArea.toFixed(1)} mm² touches it`
|
|
123
125
|
: null) },
|
|
126
|
+
// The footprint (measure.js, from bed contact's pass): the flat contact area over the
|
|
127
|
+
// piece's silhouette on the bed, in percent. Measured where bed contact is, plus where the
|
|
128
|
+
// profile carries `footprint` or the author declares it. A COST, like overhangArea: a
|
|
129
|
+
// piece on a point or a line prints with a brim or a raft, which the user may choose
|
|
130
|
+
// (`accept: ["footprint"]`); bedContactPct passes such a piece, since nothing hovers.
|
|
131
|
+
footprintPct: { kind: "cost", costId: "footprint", extract: (s) => s.footprintPct,
|
|
132
|
+
hint: "the share of this piece's silhouette on the bed (its outline in the print pose) that actually touches it is small — it stands on a point, a line or a curve, which reads near zero, so the first layer has almost nothing to hold on to and the piece can tip or pull loose. Lay it on its flat face, add a flat foot or pad at its lowest point, or print it with a brim (accept: [\"footprint\"] records that choice)",
|
|
133
|
+
pattern: "piece-stands-on-nothing",
|
|
134
|
+
locate: (s) => s.footprintAt },
|
|
124
135
|
// `s.deviation` (measure.js) exists only for a sub-part declaring `reference:
|
|
125
136
|
// "<import name>"`; on every other sub-part `extract` returns null, which
|
|
126
137
|
// `check()` already reports as status "skip" rather than a fail — the
|