partforge 0.26.1 → 0.27.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.
@@ -0,0 +1,51 @@
1
+ // Metric registry: name → how to pull the value out of facts, whether a failure
2
+ // is a hard gate or a warning, and the diagnostics attached to a non-pass check:
3
+ // `hint` (required — the report contract promises one on every fail/warn),
4
+ // `pattern` (optional stable ERROR-PATTERNS.md#<id>), `locate` (optional
5
+ // [x,y,z] source). `manifoldOnly` facts are null on OCCT parts.
6
+ //
7
+ // This lives in framework/ rather than testing/ deliberately: the set of legal
8
+ // `verify.expect` metrics is part of the PartDefinition CONTRACT, which both the
9
+ // verify runner (testing) and the linter (partforge/lint) must agree on. Keeping
10
+ // it here lets the linter import the vocabulary without reaching measure.js or
11
+ // jobs.js, which pull in the geometry kernels. This module must stay import-free.
12
+ export const SUBPART_METRICS = {
13
+ holes: { kind: "gate", manifoldOnly: true, extract: (s) => s.holes,
14
+ hint: "genus is wrong — an unintended tunnel exists or an intended bore is blocked; make cut tools pierce fully (overcut past the faces)" },
15
+ watertight: { kind: "gate", manifoldOnly: true, extract: (s) => s.watertight,
16
+ hint: "a boolean produced an open shell — check for coplanar faces or a cut that exactly grazes a surface",
17
+ pattern: "boolean-not-watertight" },
18
+ volume: { kind: "gate", extract: (s) => s.volume,
19
+ hint: "solid volume is out of range — a feature is missing, doubled, or a governing parameter is mis-scaled" },
20
+ surfaceArea: { kind: "gate", extract: (s) => s.surfaceArea,
21
+ hint: "surface area is out of range — detail features (facets, ribs, textures) are missing or doubled" },
22
+ triangleCount: { kind: "gate", extract: (s) => s.triangleCount,
23
+ hint: "triangle count is out of range — tessellation quality or feature count changed unexpectedly" },
24
+ bbox: { kind: "gate", extract: (s) => s.bbox,
25
+ hint: "bounding box is out of range — check the governing dimensions and the part's orientation" },
26
+ centerOfMass: { kind: "gate", extract: (s) => s.centerOfMass,
27
+ hint: "center of mass is outside the expected region — mass is distributed differently than intended; check feature placement or a mis-scaled sub-part" },
28
+ boundsMin: { kind: "gate", extract: (s) => s.bounds?.min,
29
+ hint: "the low corner is out of range — the part is positioned or oriented differently than expected" },
30
+ boundsMax: { kind: "gate", extract: (s) => s.bounds?.max,
31
+ hint: "the high corner is out of range — the part is positioned or oriented differently than expected" },
32
+ minWall: { kind: "warn", extract: (s) => s.minWall,
33
+ hint: "thinnest wall is at the reported location — increase the governing wall/thickness parameter or reduce the intersecting feature's depth",
34
+ pattern: "minwall-sliver-triangles",
35
+ locate: (s) => s.minWallAt },
36
+ };
37
+ export const VIEW_METRICS = {
38
+ bbox: { kind: "gate", extract: (r) => r.aggregate.bbox,
39
+ hint: "the assembled view exceeds its size limit — shrink the assembly or pick a process with a larger bed" },
40
+ volume: { kind: "gate", extract: (r) => r.aggregate.volume,
41
+ hint: "total assembly volume is out of range — a sub-part is missing, doubled, or mis-scaled" },
42
+ overlaps: { kind: "gate", extract: (r) => r.overlaps.length,
43
+ hint: "sub-parts interpenetrate near the reported location — adjust placement or add clearance in derive()",
44
+ locate: (r) => r.overlaps[0]?.location ?? null },
45
+ centerOfMass: { kind: "gate", extract: (r) => r.aggregate.centerOfMass,
46
+ hint: "the assembly's center of mass is outside the expected region — a sub-part is mis-placed or mis-scaled" },
47
+ boundsMin: { kind: "gate", extract: (r) => r.aggregate.bounds?.min,
48
+ hint: "the assembly's low corner is out of range — check placement or orientation" },
49
+ boundsMax: { kind: "gate", extract: (r) => r.aggregate.bounds?.max,
50
+ hint: "the assembly's high corner is out of range — check placement or orientation" },
51
+ };
@@ -3,6 +3,7 @@
3
3
  // Each instance lazily imports only its own backend, so OCCT's ~11 MB WASM loads
4
4
  // only in the worker that needs it, and only on first use.
5
5
  import { handle } from "./jobs.js";
6
+ import { lintPart } from "../lint.js";
6
7
 
7
8
  async function manifoldKernels() {
8
9
  const [{ default: Module }, { createManifoldKernel }] = await Promise.all([
@@ -48,6 +49,14 @@ export function runWorker(part) {
48
49
  }
49
50
 
50
51
  self.onmessage = async (e) => {
52
+ // Lint is geometry-free by construction, so answer it before touching — or
53
+ // booting — a kernel. handle() in jobs.js takes an already-booted kernel, and
54
+ // the branches below await that boot, so routing lint through them would drag
55
+ // in OCCT's ~11 MB WASM to run a check that never calls the kernel at all.
56
+ if (e.data?.type === "lint") {
57
+ postMessage({ type: "lint-report", report: lintPart(part, { params: e.data.params }) });
58
+ return;
59
+ }
51
60
  let kernel;
52
61
  if (backend === "manifold") {
53
62
  await booting;
package/src/lint.js ADDED
@@ -0,0 +1,3 @@
1
+ // Public entry for `partforge/lint`. Deliberately separate from `partforge/testing`,
2
+ // whose entry pulls in the WASM kernels and cannot load in a browser sandbox.
3
+ export { lintPart, RULES } from "./framework/lint/index.js";
@@ -5,52 +5,11 @@ import { resolveProfile } from "./dfm-profiles.js";
5
5
  import { expandCases } from "./cases.js";
6
6
  import { subPartReadKeys, relevanceHash, RELEVANT_ALL } from "../framework/param-deps.js";
7
7
  import { resolveParams } from "../framework/jobs.js";
8
+ import { SUBPART_METRICS, VIEW_METRICS } from "../framework/verify-metrics.js";
8
9
 
9
- // Metric registry: name how to pull the value out of facts, whether a failure
10
- // is a hard gate or a warning, and the diagnostics attached to a non-pass check:
11
- // `hint` (required the report contract promises one on every fail/warn),
12
- // `pattern` (optional stable ERROR-PATTERNS.md#<id>), `locate` (optional
13
- // [x,y,z] source). `manifoldOnly` facts are null on OCCT parts.
14
- export const SUBPART_METRICS = {
15
- holes: { kind: "gate", manifoldOnly: true, extract: (s) => s.holes,
16
- hint: "genus is wrong — an unintended tunnel exists or an intended bore is blocked; make cut tools pierce fully (overcut past the faces)" },
17
- watertight: { kind: "gate", manifoldOnly: true, extract: (s) => s.watertight,
18
- hint: "a boolean produced an open shell — check for coplanar faces or a cut that exactly grazes a surface",
19
- pattern: "boolean-not-watertight" },
20
- volume: { kind: "gate", extract: (s) => s.volume,
21
- hint: "solid volume is out of range — a feature is missing, doubled, or a governing parameter is mis-scaled" },
22
- surfaceArea: { kind: "gate", extract: (s) => s.surfaceArea,
23
- hint: "surface area is out of range — detail features (facets, ribs, textures) are missing or doubled" },
24
- triangleCount: { kind: "gate", extract: (s) => s.triangleCount,
25
- hint: "triangle count is out of range — tessellation quality or feature count changed unexpectedly" },
26
- bbox: { kind: "gate", extract: (s) => s.bbox,
27
- hint: "bounding box is out of range — check the governing dimensions and the part's orientation" },
28
- centerOfMass: { kind: "gate", extract: (s) => s.centerOfMass,
29
- hint: "center of mass is outside the expected region — mass is distributed differently than intended; check feature placement or a mis-scaled sub-part" },
30
- boundsMin: { kind: "gate", extract: (s) => s.bounds?.min,
31
- hint: "the low corner is out of range — the part is positioned or oriented differently than expected" },
32
- boundsMax: { kind: "gate", extract: (s) => s.bounds?.max,
33
- hint: "the high corner is out of range — the part is positioned or oriented differently than expected" },
34
- minWall: { kind: "warn", extract: (s) => s.minWall,
35
- hint: "thinnest wall is at the reported location — increase the governing wall/thickness parameter or reduce the intersecting feature's depth",
36
- pattern: "minwall-sliver-triangles",
37
- locate: (s) => s.minWallAt },
38
- };
39
- export const VIEW_METRICS = {
40
- bbox: { kind: "gate", extract: (r) => r.aggregate.bbox,
41
- hint: "the assembled view exceeds its size limit — shrink the assembly or pick a process with a larger bed" },
42
- volume: { kind: "gate", extract: (r) => r.aggregate.volume,
43
- hint: "total assembly volume is out of range — a sub-part is missing, doubled, or mis-scaled" },
44
- overlaps: { kind: "gate", extract: (r) => r.overlaps.length,
45
- hint: "sub-parts interpenetrate near the reported location — adjust placement or add clearance in derive()",
46
- locate: (r) => r.overlaps[0]?.location ?? null },
47
- centerOfMass: { kind: "gate", extract: (r) => r.aggregate.centerOfMass,
48
- hint: "the assembly's center of mass is outside the expected region — a sub-part is mis-placed or mis-scaled" },
49
- boundsMin: { kind: "gate", extract: (r) => r.aggregate.bounds?.min,
50
- hint: "the assembly's low corner is out of range — check placement or orientation" },
51
- boundsMax: { kind: "gate", extract: (r) => r.aggregate.bounds?.max,
52
- hint: "the assembly's high corner is out of range — check placement or orientation" },
53
- };
10
+ // Re-exported for backwards compatibility: the registries moved to framework/ so
11
+ // the linter can read the metric vocabulary without importing a geometry kernel.
12
+ export { SUBPART_METRICS, VIEW_METRICS };
54
13
 
55
14
  // An expectation is a bare expression (string/number/boolean) or { expr, hint }.
56
15
  const normalizeExpectation = (spec) =>