partforge 0.163.1 → 0.164.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/bin/cli.js +9 -5
- package/docs/AUTHORING-PARTS.md +199 -38
- package/docs/ERROR-PATTERNS.md +6 -0
- package/package.json +1 -1
- package/src/framework/geometry/kernel.js +1 -1
- package/src/framework/geometry/shape2d-sugar.js +12 -7
- 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
|
@@ -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
|
package/types/part.d.ts
CHANGED
|
@@ -484,9 +484,9 @@ export interface DfmProfile {
|
|
|
484
484
|
minWall?: number;
|
|
485
485
|
/**
|
|
486
486
|
* Steepest unsupported face the process prints cleanly, in degrees from
|
|
487
|
-
* vertical (45 on the FDM profiles; absent on resin).
|
|
488
|
-
*
|
|
489
|
-
*
|
|
487
|
+
* vertical (45 on the FDM profiles; absent on resin). The print process alone
|
|
488
|
+
* arms the check; `null` switches it off under a named base. Reported as a
|
|
489
|
+
* cost (needing supports), never a failure.
|
|
490
490
|
*/
|
|
491
491
|
overhang?: number | null;
|
|
492
492
|
/**
|
|
@@ -495,6 +495,8 @@ export interface DfmProfile {
|
|
|
495
495
|
* profile alone; `null` switches it off under a named base.
|
|
496
496
|
*/
|
|
497
497
|
bedContact?: number | null;
|
|
498
|
+
/** Percent of a printed piece's posed base area resting on the bed below which the footprint cost fires. */
|
|
499
|
+
footprint?: number | null;
|
|
498
500
|
/** Carried for a future gap check; not enforced yet. */
|
|
499
501
|
clearance?: number;
|
|
500
502
|
/**
|
|
@@ -562,8 +564,8 @@ export interface SubPartExpectations {
|
|
|
562
564
|
wall?: Expectation;
|
|
563
565
|
/**
|
|
564
566
|
* Unsupported downward-facing surface in mm² (faces steeper than the profile's
|
|
565
|
-
* `overhang` angle, bed at the sub-part's own lowest Z).
|
|
566
|
-
*
|
|
567
|
+
* `overhang` angle, bed at the sub-part's own lowest Z). Armed by the print
|
|
568
|
+
* process alone; reported as a cost (`verify.costs`), never a failure.
|
|
567
569
|
*/
|
|
568
570
|
overhangArea?: Expectation;
|
|
569
571
|
/**
|
|
@@ -572,6 +574,13 @@ export interface SubPartExpectations {
|
|
|
572
574
|
* Under an FDM profile every printed sub-part gets `>=50` by default; a gate.
|
|
573
575
|
*/
|
|
574
576
|
bedContactPct?: Expectation;
|
|
577
|
+
/**
|
|
578
|
+
* Share (0–100) of the sub-part's posed base area (its xy extent in the print pose)
|
|
579
|
+
* whose flat underside touches the bed — near 0 for a piece standing on a point, a
|
|
580
|
+
* line or a curl. Under an FDM profile every printed sub-part gets `>=5`; reported as
|
|
581
|
+
* a cost (`verify.costs`), never a failure; `accept: ["footprint"]` records a brim.
|
|
582
|
+
*/
|
|
583
|
+
footprintPct?: Expectation;
|
|
575
584
|
/** Symmetric-difference volume vs. the sub-part's declared `reference` import. */
|
|
576
585
|
refXorVolume?: Expectation;
|
|
577
586
|
/** Percent volume delta vs. the sub-part's declared `reference` import. */
|
|
@@ -640,12 +649,17 @@ export interface VerifyBlock<P = ResolvedParams, D = Derived> {
|
|
|
640
649
|
/** A named DFM profile or an inline one. */
|
|
641
650
|
process?: DfmProfileName | DfmProfile;
|
|
642
651
|
/**
|
|
643
|
-
* `"print"` declares
|
|
644
|
-
* bed at its own lowest Z
|
|
645
|
-
* check is armed. Omit it while a part is still being shaped or is bound for
|
|
646
|
-
* another process.
|
|
652
|
+
* `"print"` declares that `build()` is the print pose — Z up, each sub-part's
|
|
653
|
+
* bed at its own lowest Z. It does not arm any check; the print process does.
|
|
647
654
|
*/
|
|
648
655
|
orientation?: "print";
|
|
656
|
+
/**
|
|
657
|
+
* Costs the user accepted, by id. An accepted cost is still measured and
|
|
658
|
+
* reported (`accepted: true`), but no longer raised as something to fix. Only the
|
|
659
|
+
* process's own default check is marked accepted: a limit you wrote in `expect`
|
|
660
|
+
* (`overhangArea: "<=500"`) keeps its meaning and is never marked accepted.
|
|
661
|
+
*/
|
|
662
|
+
accept?: ("supports" | "footprint")[];
|
|
649
663
|
/** Which cases to check; default is `"defaults"` plus every preset name. */
|
|
650
664
|
cases?: string[];
|
|
651
665
|
/**
|
package/types/testing.d.ts
CHANGED
|
@@ -433,6 +433,14 @@ export interface SubPartFacts {
|
|
|
433
433
|
bedHoverGap: number | null;
|
|
434
434
|
/** Centroid of the largest hovering face; `null` when nothing hovers. */
|
|
435
435
|
bedHoverAt: number[] | null;
|
|
436
|
+
/**
|
|
437
|
+
* Share (0–100) of the piece's silhouette on the bed (its outline seen from below, in
|
|
438
|
+
* the print pose) that actually touches it — the footprint cost's reading, from the
|
|
439
|
+
* bed-contact pass; `null` when that pass did not run or the silhouette is empty.
|
|
440
|
+
*/
|
|
441
|
+
footprintPct: number | null;
|
|
442
|
+
/** The piece's lowest point, in display coordinates; `null` when not checked. */
|
|
443
|
+
footprintAt: number[] | null;
|
|
436
444
|
/**
|
|
437
445
|
* Present only when bed contact had to be read off the piece's export build (its
|
|
438
446
|
* print pose could not be read from place() alone) and that build failed.
|
|
@@ -521,6 +529,8 @@ export interface MeasureReport {
|
|
|
521
529
|
measuredOverhang: number | null;
|
|
522
530
|
/** Whether the bed-contact pass ran (the part is checked for it). */
|
|
523
531
|
measuredBedContact: boolean;
|
|
532
|
+
/** Whether the footprint was read — the same pass, so always `measuredBedContact`. */
|
|
533
|
+
measuredFootprint: boolean;
|
|
524
534
|
/**
|
|
525
535
|
* Present only in a view holding a sheet part: whether the printed sub-parts' print
|
|
526
536
|
* (export) poses were built for `printBbox`.
|
|
@@ -595,7 +605,7 @@ export function measure(
|
|
|
595
605
|
|
|
596
606
|
// --- verify -----------------------------------------------------------------
|
|
597
607
|
|
|
598
|
-
export type CheckStatus = "pass" | "fail" | "warn" | "skip";
|
|
608
|
+
export type CheckStatus = "pass" | "fail" | "warn" | "cost" | "skip";
|
|
599
609
|
|
|
600
610
|
export interface VerifyCheck {
|
|
601
611
|
/** `"part"` is the vacuous-verify notice; `"case"` the quick-lap "not measured" marker. */
|
|
@@ -603,8 +613,10 @@ export interface VerifyCheck {
|
|
|
603
613
|
/** The sub-part name, `"a×b"` for a pair check, or `null` for a scalar view metric. */
|
|
604
614
|
subpart: string | null;
|
|
605
615
|
metric: string;
|
|
606
|
-
/** `gate` failures set a non-zero exit code; `warn` never blocks. */
|
|
607
|
-
kind: "gate" | "warn";
|
|
616
|
+
/** `gate` failures set a non-zero exit code; `warn` never blocks; `cost` is a design choice with a price. */
|
|
617
|
+
kind: "gate" | "warn" | "cost";
|
|
618
|
+
/** On a `cost` check: whether the user listed its id in `verify.accept`. */
|
|
619
|
+
accepted?: boolean;
|
|
608
620
|
/** The asserted expression, stringified. */
|
|
609
621
|
expr: string;
|
|
610
622
|
actual: unknown;
|
|
@@ -651,6 +663,8 @@ export interface VerifyReport {
|
|
|
651
663
|
/** Every failing check, flattened, each tagged with its `case`. */
|
|
652
664
|
failures: Array<VerifyCheck & { case: string }>;
|
|
653
665
|
warnings: Array<VerifyCheck & { case: string }>;
|
|
666
|
+
/** Design choices with a price (supports, footprint); never failures. Accepted ones are flagged. */
|
|
667
|
+
costs: Array<VerifyCheck & { case: string }>;
|
|
654
668
|
/** Checks a quick lap could not evaluate. */
|
|
655
669
|
unevaluated: Array<VerifyCheck & { case: string }>;
|
|
656
670
|
/** Check instances the part or its profile declared, across cases (near-miss notices excluded). */
|