partforge 0.23.0 → 0.24.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 +67 -7
- package/package.json +1 -1
- package/src/testing/measure.js +12 -2
- package/src/testing/mesh.js +25 -0
- package/src/testing/verify.js +12 -0
package/docs/AUTHORING-PARTS.md
CHANGED
|
@@ -35,6 +35,33 @@ supplies it via `framework/app.css`, imported by `mount`).
|
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
38
|
+
## Before geometry: state the engineering intent
|
|
39
|
+
|
|
40
|
+
For a decorative or low-consequence part, a short dimensional description may be
|
|
41
|
+
enough. For anything that mates with another object, carries load, or could cause harm
|
|
42
|
+
if it fails, write down the engineering intent **before** writing `build`:
|
|
43
|
+
|
|
44
|
+
- the coordinate frame, origin, and named datums;
|
|
45
|
+
- the allowed envelope and the interfaces that must align (mating faces, axes, hole
|
|
46
|
+
patterns, fits, clearances, and tolerances);
|
|
47
|
+
- the manufacturing process and material assumptions;
|
|
48
|
+
- load cases, support regions, intended load paths, and safety factors when structural
|
|
49
|
+
behavior matters;
|
|
50
|
+
- numbered acceptance claims with units and thresholds; and
|
|
51
|
+
- unresolved assumptions that need the user or an engineer to answer.
|
|
52
|
+
|
|
53
|
+
This may live in the task/specification, a companion design note, or comments next to
|
|
54
|
+
the part — partforge does not prescribe a blueprint schema yet. Do not silently invent
|
|
55
|
+
missing loads, material properties, tolerances, or safety factors. Ask, or record the
|
|
56
|
+
property as unverified.
|
|
57
|
+
|
|
58
|
+
Treat user/specification acceptance claims as **higher authority** than agent-authored
|
|
59
|
+
geometry and checks. An agent may add conservative checks, but must not delete a claim
|
|
60
|
+
or loosen its threshold merely to make a failing design pass; changing the contract
|
|
61
|
+
requires explicit approval.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
38
65
|
## The `PartDefinition` contract
|
|
39
66
|
|
|
40
67
|
A part is a default-exported object. Full shape (optional fields marked `?`):
|
|
@@ -744,7 +771,9 @@ empty/degenerate results; `holes` is the informative topology number.)
|
|
|
744
771
|
|
|
745
772
|
`render` writes one PNG per angle (`iso`, `front`, `top` by default; choose with
|
|
746
773
|
`--views iso,front`, output dir with `--out`) to `render/`. The view defaults to
|
|
747
|
-
the part's first declared view.
|
|
774
|
+
the part's first declared view. Treat renders as complementary evidence, not a ruler:
|
|
775
|
+
use several views for complex parts and the interactive viewer's cutaway for hidden
|
|
776
|
+
interfaces, but rely on `measure` / `verify` for dimensions, contact, and clearance.
|
|
748
777
|
|
|
749
778
|
The `measure` function is also exported for vitest (boot a Manifold kernel as in
|
|
750
779
|
"Testing a part", then `measure(kernel, part, "<view>")`):
|
|
@@ -797,6 +826,12 @@ JSON is pretty-printed across multiple lines) for robust machine parsing. With
|
|
|
797
826
|
so even if a later `verify` throw crashes the run the file is there — it just
|
|
798
827
|
lacks the `verify` key.
|
|
799
828
|
|
|
829
|
+
**Fresh-evidence rule.** A passing report is evidence only for the source, parameters,
|
|
830
|
+
view, backend, and framework version that produced it. Any relevant edit makes the old
|
|
831
|
+
result stale. Before reporting a part complete, run `measure` / `verify` again on the
|
|
832
|
+
current source and inspect current renders where visual requirements remain. Do not cite
|
|
833
|
+
a command that ran before the last geometry or expectation change as evidence.
|
|
834
|
+
|
|
800
835
|
**Part-authored hints.** Any `verify.expect` metric accepts `{ expr, hint }` in
|
|
801
836
|
place of a bare expression — use it to name the governing parameter:
|
|
802
837
|
|
|
@@ -813,8 +848,8 @@ verify: {
|
|
|
813
848
|
## Self-verification (the `verify` block)
|
|
814
849
|
|
|
815
850
|
A part can declare how it should be checked, co-located with its schema, so
|
|
816
|
-
`partforge measure` (and vitest) can
|
|
817
|
-
Add an optional top-level `verify` block:
|
|
851
|
+
`partforge measure` (and vitest) can enforce selected **geometric**, **assembly**, and
|
|
852
|
+
**DFM** properties. Add an optional top-level `verify` block:
|
|
818
853
|
|
|
819
854
|
```js
|
|
820
855
|
verify: {
|
|
@@ -833,13 +868,32 @@ verify: {
|
|
|
833
868
|
**What the profile gives you:** a hard **bed-fit** gate (the view bbox must fit `bed`)
|
|
834
869
|
and a **min-wall** warning. **What `expect` gives you:** per-sub-part assertions on the
|
|
835
870
|
facts `measure` already reports — `holes` (through-bores / genus), `volume`,
|
|
836
|
-
`surfaceArea`, `triangleCount`, `bbox`, `watertight`, `minWall
|
|
837
|
-
|
|
871
|
+
`surfaceArea`, `triangleCount`, `bbox`, `watertight`, `minWall`, `bounds` (per-sub-part
|
|
872
|
+
and aggregate axis-aligned `{min,max}` corner positions — where the geometry sits, vs
|
|
873
|
+
`bbox` which is only its size) and `centerOfMass` (`[x,y,z]`, the volume-weighted
|
|
874
|
+
centroid; `null` for a degenerate/zero-volume sub-part); and `_view` assertions `bbox`,
|
|
875
|
+
`volume`, `overlaps`, `centerOfMass`, `boundsMin`, `boundsMax`, plus the pair-wise
|
|
876
|
+
`contacts` / `clearance` below.
|
|
877
|
+
|
|
878
|
+
Passing these checks does **not** prove structural strength, fatigue life, stability,
|
|
879
|
+
manufacturing tolerance stack-up, regulatory compliance, or safe real-world use.
|
|
880
|
+
Load-bearing or safety-relevant parts need appropriate analytical/simulation evidence
|
|
881
|
+
(for example FEA with declared materials, loads, supports, and safety factors) plus
|
|
882
|
+
qualified human review. If no such evidence exists, say that physical performance is
|
|
883
|
+
unverified.
|
|
838
884
|
|
|
839
885
|
**Assertion DSL:** a bare number means equality (`holes: 1`); `">=n"`, `"<=n"`, `">n"`,
|
|
840
886
|
`"<n"`, or a range `"a..b"`; an optional unit suffix `mm`/`cm`/`mm3`/`cm3`; and for
|
|
841
|
-
`bbox`, a componentwise vector `"<=[x,y,z]"` /
|
|
842
|
-
The parser is strict — a malformed assertion
|
|
887
|
+
`bbox`, `centerOfMass`, `boundsMin`, `boundsMax`, a componentwise vector `"<=[x,y,z]"` /
|
|
888
|
+
`">=[x,y,z]"` where `*` skips an axis. The parser is strict — a malformed assertion
|
|
889
|
+
fails loudly.
|
|
890
|
+
|
|
891
|
+
```js
|
|
892
|
+
verify: { expect: {
|
|
893
|
+
stand: { boundsMin: ">=[0,0,0]", centerOfMass: "<=[*,*,25]" }, // sits in +octant, mass kept low
|
|
894
|
+
_view: { boundsMax: "<=[220,220,250]" }, // whole assembly fits the bed
|
|
895
|
+
} }
|
|
896
|
+
```
|
|
843
897
|
|
|
844
898
|
**Gates vs. warnings:** exact facts are **gates** (a failure sets a non-zero exit code);
|
|
845
899
|
`minWall` is computed (a ray/shot wall-thickness measurement) and reported as a
|
|
@@ -915,6 +969,12 @@ Checks run across the **default config plus every preset** (or your `cases` list
|
|
|
915
969
|
preset that changes only parameters no on-screen sub-part reads is deduplicated, so
|
|
916
970
|
coverage is cheap.
|
|
917
971
|
|
|
972
|
+
When an agent authors both geometry and `verify`, the check is useful feedback but not
|
|
973
|
+
an independent oracle. Preserve externally supplied acceptance claims verbatim (ideally
|
|
974
|
+
with stable IDs in the surrounding specification), and test boundary/tolerance cases in
|
|
975
|
+
addition to friendly defaults and presets. A repair should change the design, not relax
|
|
976
|
+
the requirement that exposed the failure.
|
|
977
|
+
|
|
918
978
|
---
|
|
919
979
|
|
|
920
980
|
## Fillet & chamfer (automatic OCCT backend)
|
package/package.json
CHANGED
package/src/testing/measure.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { buildView } from "./build.js";
|
|
2
2
|
import { assemblyOverlaps } from "../framework/assembly.js";
|
|
3
3
|
import { meshGaps, pairKey, CONTACT_EPS, GAP_THRESHOLD } from "./gaps.js";
|
|
4
|
-
import { bounds, meshArea } from "./mesh.js";
|
|
4
|
+
import { bounds, meshArea, meshCentroid } from "./mesh.js";
|
|
5
5
|
import { minWall } from "./min-wall.js";
|
|
6
6
|
|
|
7
7
|
const size = ({ min, max }) => [max[0] - min[0], max[1] - min[1], max[2] - min[2]];
|
|
@@ -26,6 +26,8 @@ export function measure(kernel, part, view = Object.keys(part.views)[0], params
|
|
|
26
26
|
return {
|
|
27
27
|
name,
|
|
28
28
|
bbox: size(b),
|
|
29
|
+
bounds: { min: b.min, max: b.max },
|
|
30
|
+
centerOfMass: meshCentroid(mesh.positions, mesh.indices),
|
|
29
31
|
volume: solid.volume(),
|
|
30
32
|
surfaceArea: meshArea(mesh.positions, mesh.indices),
|
|
31
33
|
triangleCount: mesh.triangles,
|
|
@@ -54,8 +56,16 @@ export function measure(kernel, part, view = Object.keys(part.views)[0], params
|
|
|
54
56
|
(g) => g.distance > CONTACT_EPS && g.distance < gapThreshold && !overlapping.has(pairKey(g.a, g.b)),
|
|
55
57
|
);
|
|
56
58
|
|
|
59
|
+
const ub = subparts.length ? unionBounds(subBounds) : { min: [0, 0, 0], max: [0, 0, 0] };
|
|
60
|
+
const weighted = subparts.filter((s) => s.centerOfMass !== null);
|
|
61
|
+
const totalVol = weighted.reduce((a, s) => a + s.volume, 0);
|
|
62
|
+
const aggCom = weighted.length && Math.abs(totalVol) > 1e-9
|
|
63
|
+
? [0, 1, 2].map((i) => weighted.reduce((a, s) => a + s.volume * s.centerOfMass[i], 0) / totalVol)
|
|
64
|
+
: null;
|
|
57
65
|
const aggregate = {
|
|
58
|
-
bbox:
|
|
66
|
+
bbox: size(ub),
|
|
67
|
+
bounds: { min: ub.min, max: ub.max },
|
|
68
|
+
centerOfMass: aggCom,
|
|
59
69
|
volume: subparts.reduce((a, s) => a + s.volume, 0),
|
|
60
70
|
surfaceArea: subparts.reduce((a, s) => a + s.surfaceArea, 0),
|
|
61
71
|
triangleCount: subparts.reduce((a, s) => a + s.triangleCount, 0),
|
package/src/testing/mesh.js
CHANGED
|
@@ -12,6 +12,31 @@ export function meshVolume(positions, indices) {
|
|
|
12
12
|
}
|
|
13
13
|
return Math.abs(V);
|
|
14
14
|
}
|
|
15
|
+
// Volume-weighted centroid (uniform-density center of mass) of a triangle mesh,
|
|
16
|
+
// via the same signed-tetrahedron decomposition as meshVolume. `indices` is
|
|
17
|
+
// optional: omit for a flat soup (3 verts/triangle). Returns [x,y,z], or null when
|
|
18
|
+
// the mesh encloses ~no volume (open/degenerate), where a centroid is undefined.
|
|
19
|
+
// Signed V (not abs) is used so the winding sign cancels in C/V.
|
|
20
|
+
export function meshCentroid(positions, indices) {
|
|
21
|
+
const n = indices ? indices.length : positions.length / 3;
|
|
22
|
+
let V = 0, cx = 0, cy = 0, cz = 0;
|
|
23
|
+
for (let i = 0; i < n; i += 3) {
|
|
24
|
+
const a = (indices ? indices[i] : i) * 3, b = (indices ? indices[i + 1] : i + 1) * 3, c = (indices ? indices[i + 2] : i + 2) * 3;
|
|
25
|
+
const ax = positions[a], ay = positions[a + 1], az = positions[a + 2];
|
|
26
|
+
const bx = positions[b], by = positions[b + 1], bz = positions[b + 2];
|
|
27
|
+
const dx = positions[c], dy = positions[c + 1], dz = positions[c + 2];
|
|
28
|
+
// signed volume of tetra (origin, a, b, c) = a · (b × c) / 6
|
|
29
|
+
const v = (ax * (by * dz - bz * dy) - ay * (bx * dz - bz * dx) + az * (bx * dy - by * dx)) / 6;
|
|
30
|
+
V += v;
|
|
31
|
+
// tetra centroid = (0 + a + b + c) / 4, weighted by its signed volume
|
|
32
|
+
cx += v * (ax + bx + dx) / 4;
|
|
33
|
+
cy += v * (ay + by + dy) / 4;
|
|
34
|
+
cz += v * (az + bz + dz) / 4;
|
|
35
|
+
}
|
|
36
|
+
if (Math.abs(V) < 1e-9) return null;
|
|
37
|
+
return [cx / V, cy / V, cz / V];
|
|
38
|
+
}
|
|
39
|
+
|
|
15
40
|
export function bboxSize(positions) {
|
|
16
41
|
const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
|
|
17
42
|
for (let i = 0; i < positions.length; i += 3) for (let a = 0; a < 3; a++) {
|
package/src/testing/verify.js
CHANGED
|
@@ -25,6 +25,12 @@ export const SUBPART_METRICS = {
|
|
|
25
25
|
hint: "triangle count is out of range — tessellation quality or feature count changed unexpectedly" },
|
|
26
26
|
bbox: { kind: "gate", extract: (s) => s.bbox,
|
|
27
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" },
|
|
28
34
|
minWall: { kind: "warn", extract: (s) => s.minWall,
|
|
29
35
|
hint: "thinnest wall is at the reported location — increase the governing wall/thickness parameter or reduce the intersecting feature's depth",
|
|
30
36
|
pattern: "minwall-sliver-triangles",
|
|
@@ -38,6 +44,12 @@ export const VIEW_METRICS = {
|
|
|
38
44
|
overlaps: { kind: "gate", extract: (r) => r.overlaps.length,
|
|
39
45
|
hint: "sub-parts interpenetrate near the reported location — adjust placement or add clearance in derive()",
|
|
40
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" },
|
|
41
53
|
};
|
|
42
54
|
|
|
43
55
|
// An expectation is a bare expression (string/number/boolean) or { expr, hint }.
|