partforge 0.161.0 → 0.162.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
|
@@ -4375,12 +4375,16 @@ unverified.
|
|
|
4375
4375
|
|
|
4376
4376
|
**Assertion DSL:** a bare number means equality (`holes: 1`); `">=n"`, `"<=n"`, `">n"`,
|
|
4377
4377
|
`"<n"`, or a range `"a..b"`; an optional unit suffix `mm`/`cm`/`mm3`/`cm3`; and for
|
|
4378
|
-
`bbox`, `centerOfMass` and each side of `bounds`, a componentwise vector
|
|
4379
|
-
`"
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
with
|
|
4378
|
+
`bbox`, `centerOfMass` and each side of `bounds`, a componentwise vector — one-sided,
|
|
4379
|
+
`"<=[x,y,z]"` / `">=[x,y,z]"`, or two-sided with a range per axis,
|
|
4380
|
+
`"[28..32, 18..22, 9..11]"` (the bare comma form `"28..32,18..22,9..11"` is accepted
|
|
4381
|
+
too) — exactly 3 components, each a plain number in mm (or a range of them) or `*` to
|
|
4382
|
+
skip an axis. A bare `[40,40,10]` (meaning "exactly this size") is rejected, since a
|
|
4383
|
+
mesh's box shifts with tessellation and no tolerance would be honest for every part —
|
|
4384
|
+
pick `<=` for a limit, `>=` for a floor, or say what tolerance you mean with a range
|
|
4385
|
+
per axis (`"[39.5..40.5, 39.5..40.5, 9.8..10.2]"`; a zero-width `40..40` is an exact
|
|
4386
|
+
check with the parser's 1e-6 slack, and will fail on a chorded curve). `bounds` takes an
|
|
4387
|
+
object with `min` and/or `max`, each such a vector: `bounds: { min: ">=[0,0,0]", max:
|
|
4384
4388
|
"<=[220,220,250]" }`. The parser is strict — a malformed assertion fails loudly.
|
|
4385
4389
|
|
|
4386
4390
|
**A part whose typeface is a parameter needs band assertions, not points.** Glyph
|
package/package.json
CHANGED
|
@@ -160,6 +160,8 @@ function checkHeld(held, parts, path) {
|
|
|
160
160
|
// `verify-bad-expr`, not `verify-bad-pair-check`: a malformed key is a pair-naming
|
|
161
161
|
// problem, but a malformed value is an assertion-DSL problem, same as any other
|
|
162
162
|
// metric's expectation.
|
|
163
|
+
const DSL_HINT = "Use the assertion DSL: a bare value for equality, a comparison like `>=3`, a range like `2..5`, or a componentwise vector like `<=[60,60,60]` or `[28..32,18..22,9..11]` (with `*` to skip an axis).";
|
|
164
|
+
|
|
163
165
|
function checkClearanceExprs(clearance, path) {
|
|
164
166
|
if (clearance === undefined || clearance === null || typeof clearance !== "object") return [];
|
|
165
167
|
const out = [];
|
|
@@ -168,7 +170,7 @@ function checkClearanceExprs(clearance, path) {
|
|
|
168
170
|
catch (e) {
|
|
169
171
|
out.push(err("verify-bad-expr",
|
|
170
172
|
`the expectation for _view.clearance["${key}"] is not a valid assertion: ${e?.message || String(e)}`,
|
|
171
|
-
|
|
173
|
+
DSL_HINT,
|
|
172
174
|
`${path}[${JSON.stringify(key)}]`));
|
|
173
175
|
}
|
|
174
176
|
}
|
|
@@ -218,7 +220,7 @@ function checkProcessSpec(spec, path, valid, seen, depth = 0) {
|
|
|
218
220
|
// One expectation's well-formedness against its registry entry: it parses, and it has the form the
|
|
219
221
|
// metric reads — a range for `wall`, a componentwise vector for bbox/centerOfMass/the
|
|
220
222
|
// sides of bounds. A bare vector gets the two legal spellings back, in its own numbers.
|
|
221
|
-
const
|
|
223
|
+
const VECTOR_OPS = new Set(["vle", "vge", "vrange"]);
|
|
222
224
|
const capitalize = (t) => t.charAt(0).toUpperCase() + t.slice(1);
|
|
223
225
|
|
|
224
226
|
function checkExpr(target, metric, spec, reg) {
|
|
@@ -246,10 +248,24 @@ function checkExpr(target, metric, spec, reg) {
|
|
|
246
248
|
`Write ${metric} as \`"<min>..<max>"\` with min <= max, in mm.`,
|
|
247
249
|
path)];
|
|
248
250
|
}
|
|
249
|
-
if (form === "vector" && parsed.op !== "vle" && parsed.op !== "vge") {
|
|
251
|
+
if (form === "vector" && parsed.op !== "vle" && parsed.op !== "vge" && parsed.op !== "vrange") {
|
|
250
252
|
return [err("verify-bad-expr",
|
|
251
253
|
`the expectation for ${target}.${metric} must be a componentwise vector, got ${describe(expr)} — ${metric} is an [x,y,z] reading`,
|
|
252
|
-
`Write ${metric} as \`"<=[x,y,z]"
|
|
254
|
+
`Write ${metric} as \`"<=[x,y,z]"\`, \`">=[x,y,z]"\` or \`"[a..b,c..d,e..f]"\` (componentwise; * skips an axis).`,
|
|
255
|
+
path)];
|
|
256
|
+
}
|
|
257
|
+
// A vector on a scalar metric (`volume: "<=[1,2,3]"`) is not a parse error, and
|
|
258
|
+
// the evaluator would index a number and pass it vacuously — so it is caught here.
|
|
259
|
+
if (form !== "vector" && VECTOR_OPS.has(parsed.op)) {
|
|
260
|
+
return [err("verify-bad-expr",
|
|
261
|
+
`the expectation for ${target}.${metric} must be a single value, got ${describe(expr)} — ${metric} is one number, not an [x,y,z] reading`,
|
|
262
|
+
DSL_HINT,
|
|
263
|
+
path)];
|
|
264
|
+
}
|
|
265
|
+
if (parsed.op === "vrange" && parsed.vec.some((c) => c !== null && c[0] > c[1])) {
|
|
266
|
+
return [err("verify-bad-expr",
|
|
267
|
+
`the expectation for ${target}.${metric} must be a range vector with min <= max on every axis, got "${expr}"`,
|
|
268
|
+
`Write each axis of ${metric} as \`<min>..<max>\` with min <= max, in mm (or \`*\` to skip it).`,
|
|
253
269
|
path)];
|
|
254
270
|
}
|
|
255
271
|
return [];
|
|
@@ -25,7 +25,8 @@ const reVec = /^(>=|<=)\s*\[\s*(.+?)\s*\]$/;
|
|
|
25
25
|
// own numbers, which is a one-step fix. Returns that hint, or null when `expr` is
|
|
26
26
|
// not a bare vector. An exact vector is deliberately not offered: a mesh's box is
|
|
27
27
|
// tessellation-dependent (a curved face chords inside its true extent), so no
|
|
28
|
-
// tolerance would be honest for every part
|
|
28
|
+
// tolerance would be honest for every part — a writer who wants both sides
|
|
29
|
+
// declares the tolerance themselves with the per-axis range form below.
|
|
29
30
|
const reBareVec = /^(?:==?)?\s*\[(.*)\]$/;
|
|
30
31
|
const VEC_TAIL = "(componentwise; * skips an axis)";
|
|
31
32
|
|
|
@@ -36,6 +37,10 @@ export function bareVectorHint(expr) {
|
|
|
36
37
|
const m = expr.trim().match(reBareVec);
|
|
37
38
|
if (!m) return null;
|
|
38
39
|
comps = m[1].split(",").map((t) => t.trim());
|
|
40
|
+
// A component with `..` in it is a range-vector attempt (below), not a bare
|
|
41
|
+
// vector: its own parse error names the right fix, and the two-spelling hint
|
|
42
|
+
// here would send the writer back to a one-sided form.
|
|
43
|
+
if (comps.some((c) => c.includes(".."))) return null;
|
|
39
44
|
} else return null;
|
|
40
45
|
comps = comps.map((c) => c.replace(/\s*mm$/, ""));
|
|
41
46
|
const ok = comps.length === 3 && comps.every((c) => c === "*" || (c !== "" && Number.isFinite(Number(c))));
|
|
@@ -46,6 +51,33 @@ export function bareVectorHint(expr) {
|
|
|
46
51
|
|
|
47
52
|
const show = (expr) => (typeof expr === "string" ? `"${expr}"` : JSON.stringify(expr));
|
|
48
53
|
|
|
54
|
+
// The two-sided vector form: a RANGE per axis, `"[28..32, 18..22, 9..11]"`, with `*`
|
|
55
|
+
// skipping an axis. The brackets are optional because the bare comma form
|
|
56
|
+
// (`"28..32,18..22,9..11"`) is what a model produces when it reaches for this —
|
|
57
|
+
// three trials wrote it before the form existed and were told "unrecognized form".
|
|
58
|
+
// Components are plain numbers in mm, like the one-sided vectors: a unit suffix
|
|
59
|
+
// is refused rather than scaled. Returns null when `s` is not a range-vector
|
|
60
|
+
// attempt at all (no `..` inside a bracket pair or comma list), so the other
|
|
61
|
+
// forms get their turn; a malformed attempt throws its own error.
|
|
62
|
+
const reRangeComp = new RegExp(`^(${NUM})\\s*\\.\\.\\s*(${NUM})$`);
|
|
63
|
+
const RANGE_VEC_SHAPE = "exactly 3 components, each a range a..b in mm or *";
|
|
64
|
+
|
|
65
|
+
function rangeVector(s) {
|
|
66
|
+
const bracketed = s.match(/^\[(.*)\]$/);
|
|
67
|
+
const inner = bracketed ? bracketed[1] : s;
|
|
68
|
+
if (!inner.includes("..") || !inner.includes(",")) return null;
|
|
69
|
+
if (!bracketed && /[\[\]]/.test(s)) return null; // one bracket of a pair — not this form
|
|
70
|
+
const parts = inner.split(",").map((t) => t.trim());
|
|
71
|
+
if (parts.length !== 3) throw new Error(`assertion: a range vector takes ${RANGE_VEC_SHAPE}: "${s}"`);
|
|
72
|
+
const vec = parts.map((t) => {
|
|
73
|
+
if (t === "*") return null;
|
|
74
|
+
const m = t.match(reRangeComp);
|
|
75
|
+
if (!m) throw new Error(`assertion: a range vector takes ${RANGE_VEC_SHAPE}: "${s}"`);
|
|
76
|
+
return [toBase(m[1], undefined), toBase(m[2], undefined)];
|
|
77
|
+
});
|
|
78
|
+
return { op: "vrange", vec };
|
|
79
|
+
}
|
|
80
|
+
|
|
49
81
|
export function parseAssertion(expr) {
|
|
50
82
|
if (typeof expr === "number" || typeof expr === "boolean") return { op: "eq", value: expr };
|
|
51
83
|
const bare = bareVectorHint(expr);
|
|
@@ -53,6 +85,8 @@ export function parseAssertion(expr) {
|
|
|
53
85
|
if (typeof expr !== "string") throw new Error(`assertion: unsupported value ${JSON.stringify(expr)}`);
|
|
54
86
|
const s = expr.trim();
|
|
55
87
|
|
|
88
|
+
const vr = rangeVector(s);
|
|
89
|
+
if (vr) return vr;
|
|
56
90
|
const vec = s.match(reVec);
|
|
57
91
|
if (vec) {
|
|
58
92
|
const parts = vec[2].split(",").map((t) => t.trim());
|
|
@@ -76,6 +110,7 @@ export function parseAssertion(expr) {
|
|
|
76
110
|
const EPS = 1e-6;
|
|
77
111
|
const approxEq = (a, b) => Math.abs(a - b) <= EPS + EPS * Math.abs(b);
|
|
78
112
|
const fmtVec = (v) => "[" + v.map((x) => (x === null ? "*" : x)).join(",") + "]";
|
|
113
|
+
const fmtRangeVec = (v) => "[" + v.map((c) => (c === null ? "*" : `${c[0]}..${c[1]}`)).join(",") + "]";
|
|
79
114
|
|
|
80
115
|
export function evaluateAssertion(parsed, actual) {
|
|
81
116
|
switch (parsed.op) {
|
|
@@ -102,6 +137,15 @@ export function evaluateAssertion(parsed, actual) {
|
|
|
102
137
|
}
|
|
103
138
|
return { pass, message: `${fmtVec(actual)} ${ge ? ">=" : "<="} ${fmtVec(parsed.vec)}` };
|
|
104
139
|
}
|
|
140
|
+
case "vrange": {
|
|
141
|
+
let pass = true;
|
|
142
|
+
for (let i = 0; i < 3; i++) {
|
|
143
|
+
const band = parsed.vec[i];
|
|
144
|
+
if (band === null) continue;
|
|
145
|
+
if (actual[i] < band[0] - EPS || actual[i] > band[1] + EPS) pass = false;
|
|
146
|
+
}
|
|
147
|
+
return { pass, message: `${fmtVec(actual)} ${pass ? "in" : "out of"} ${fmtRangeVec(parsed.vec)}` };
|
|
148
|
+
}
|
|
105
149
|
default: throw new Error(`assertion: unknown op "${parsed.op}"`);
|
|
106
150
|
}
|
|
107
151
|
}
|
|
@@ -130,7 +130,7 @@ export const SUBPART_METRICS = {
|
|
|
130
130
|
hint: "the rebuild's symmetric difference vs its reference import is too large — compare the ghost overlay, then adjust the governing dimensions toward the measured reference" },
|
|
131
131
|
refVolumeDeltaPct: { kind: "gate", extract: (s) => s.deviation?.volumeDeltaPct ?? null,
|
|
132
132
|
hint: "rebuild volume differs from the reference import by more than the allowed percentage — a feature is missing, doubled, or mis-scaled vs the reference" },
|
|
133
|
-
refBboxDelta: { kind: "gate", extract: (s) => s.deviation?.bboxDelta ?? null,
|
|
133
|
+
refBboxDelta: { kind: "gate", form: "vector", extract: (s) => s.deviation?.bboxDelta ?? null,
|
|
134
134
|
hint: "the rebuild's bounding-box corners drift from the reference import — check overall dimensions and that the rebuild is aligned to the reference's coordinates" },
|
|
135
135
|
// Sheet parts (sheetPart(), partforge/geometry): each process's own checks. verify
|
|
136
136
|
// volunteers them on every sheet sub-part; an author may declare them like any other.
|