axstack 0.36.1 → 0.38.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/README.md +5 -4
- package/bin/axstack.js +4 -18
- package/docs/getting-started.md +6 -6
- package/docs/host-operations.md +9 -5
- package/docs/installation.md +20 -41
- package/docs/workflows.md +12 -9
- package/package.json +1 -1
- package/profiles/presets/claude-only.json +3 -3
- package/profiles/presets/codex-only.json +3 -3
- package/profiles/presets/mixed.json +5 -4
- package/skills/axstack/references/automations.md +7 -2
- package/skills/axstack/references/lifecycle.md +2 -1
- package/skills/axstack/references/review-manager-prompt.md +44 -3
- package/skills/axstack/references/role-roster.md +13 -6
- package/skills/axstack/references/routing.md +2 -1
- package/skills/axstack/references/run-record.md +2 -0
- package/skills/axstack/references/ui-verification.md +6 -10
- package/skills/axstack-anatomy/ANATOMY.md +172 -0
- package/skills/axstack-anatomy/CHANGELOG.md +70 -0
- package/skills/axstack-anatomy/LICENSE +21 -0
- package/skills/axstack-anatomy/PROVENANCE.md +21 -0
- package/skills/axstack-anatomy/README.md +316 -0
- package/skills/axstack-anatomy/SKILL.md +74 -0
- package/skills/axstack-anatomy/SOURCE-PROOF.json +107 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/arcade-cabinet.css +44 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/build.mjs +714 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/live.js +597 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/parts.mjs +89 -0
- package/skills/axstack-anatomy/examples/arcade-cabinet/screen.mjs +114 -0
- package/skills/axstack-anatomy/examples/desk-computer/build.mjs +435 -0
- package/skills/axstack-anatomy/examples/dial-indicator/build.mjs +275 -0
- package/skills/axstack-anatomy/examples/raptor-engine/build.mjs +938 -0
- package/skills/axstack-anatomy/examples/raptor-engine/live.js +907 -0
- package/skills/axstack-anatomy/examples/raptor-engine/raptor.css +359 -0
- package/skills/axstack-anatomy/examples/ripple-tank/build.mjs +558 -0
- package/skills/axstack-anatomy/examples/ripple-tank/live.js +579 -0
- package/skills/axstack-anatomy/examples/ripple-tank/model.mjs +131 -0
- package/skills/axstack-anatomy/examples/ripple-tank/ripple.css +103 -0
- package/skills/axstack-anatomy/examples/spot-plate/art.tsx +110 -0
- package/skills/axstack-anatomy/examples/spot-plate/drops.ts +541 -0
- package/skills/axstack-anatomy/examples/spot-plate/fan.ts +84 -0
- package/skills/axstack-anatomy/examples/spot-plate/figure.tsx +25 -0
- package/skills/axstack-anatomy/examples/spot-plate/geometry.ts +280 -0
- package/skills/axstack-anatomy/examples/spot-plate/live.tsx +339 -0
- package/skills/axstack-anatomy/examples/spot-plate/rig.ts +230 -0
- package/skills/axstack-anatomy/examples/spot-plate/spot-plate.css +92 -0
- package/skills/axstack-anatomy/examples/test-rig/art.tsx +196 -0
- package/skills/axstack-anatomy/examples/test-rig/figure.tsx +23 -0
- package/skills/axstack-anatomy/examples/test-rig/geometry.ts +305 -0
- package/skills/axstack-anatomy/examples/test-rig/live.tsx +511 -0
- package/skills/axstack-anatomy/examples/test-rig/pose.ts +107 -0
- package/skills/axstack-anatomy/examples/test-rig/rig.ts +170 -0
- package/skills/axstack-anatomy/examples/test-rig/stretch.css +80 -0
- package/skills/axstack-anatomy/examples/test-rig/view.ts +99 -0
- package/skills/axstack-anatomy/examples/turning-dial/build.mjs +479 -0
- package/skills/axstack-anatomy/examples/turning-dial/dial.css +10 -0
- package/skills/axstack-anatomy/examples/turning-dial/live.js +342 -0
- package/skills/axstack-anatomy/examples/turning-dial/stress.mjs +269 -0
- package/skills/axstack-anatomy/kit/audit.mjs +1913 -0
- package/skills/axstack-anatomy/kit/canvas-painter.mjs +722 -0
- package/skills/axstack-anatomy/kit/gl-shared.mjs +184 -0
- package/skills/axstack-anatomy/kit/gl.mjs +184 -0
- package/skills/axstack-anatomy/kit/iso-kit.mjs +674 -0
- package/skills/axstack-anatomy/kit/iso-kit.ts +842 -0
- package/skills/axstack-anatomy/kit/iso.css +106 -0
- package/skills/axstack-anatomy/kit/lathe.mjs +1184 -0
- package/skills/axstack-anatomy/kit/react/draw.tsx +205 -0
- package/skills/axstack-anatomy/kit/react/live.tsx +140 -0
- package/skills/axstack-anatomy/kit/tube.mjs +306 -0
- package/skills/axstack-anatomy/kit/turn-audit.mjs +1096 -0
- package/skills/axstack-anatomy/kit/turn-build.mjs +1355 -0
- package/skills/axstack-anatomy/kit/turn-fixed.mjs +3800 -0
- package/skills/axstack-anatomy/kit/turn.mjs +3610 -0
- package/skills/axstack-anatomy/references/3d.md +352 -0
- package/skills/axstack-anatomy/references/craft.md +325 -0
- package/skills/axstack-anatomy/references/kit.md +419 -0
- package/skills/axstack-anatomy/references/motion.md +165 -0
- package/skills/axstack-anatomy/references/react.md +42 -0
- package/skills/axstack-anatomy/references/verify.md +351 -0
- package/skills/axstack-anatomy/references/walkthrough.md +75 -0
- package/skills/axstack-anatomy/references/webgl.md +473 -0
- package/skills/axstack-anatomy/scripts/audit-affine-bounds.test.mjs +54 -0
- package/skills/axstack-anatomy/scripts/audit-cache.test.mjs +37 -0
- package/skills/axstack-anatomy/scripts/audit-clearance-envelope.test.mjs +39 -0
- package/skills/axstack-anatomy/scripts/audit-empty-clipping.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/audit-order-convergence.test.mjs +55 -0
- package/skills/axstack-anatomy/scripts/audit-path-origin.test.mjs +26 -0
- package/skills/axstack-anatomy/scripts/audit-profile.test.mjs +41 -0
- package/skills/axstack-anatomy/scripts/audit-projection.test.mjs +36 -0
- package/skills/axstack-anatomy/scripts/audit-slide-clearance.test.mjs +58 -0
- package/skills/axstack-anatomy/scripts/audit-svg-attributes.test.mjs +46 -0
- package/skills/axstack-anatomy/scripts/audit-taper.test.mjs +29 -0
- package/skills/axstack-anatomy/scripts/capture-selector.test.mjs +131 -0
- package/skills/axstack-anatomy/scripts/capture.mjs +228 -0
- package/skills/axstack-anatomy/scripts/capture.test.mjs +42 -0
- package/skills/axstack-anatomy/scripts/chrome-port.mjs +36 -0
- package/skills/axstack-anatomy/scripts/chrome-startup.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/drive-actions.test.mjs +46 -0
- package/skills/axstack-anatomy/scripts/drive.mjs +302 -0
- package/skills/axstack-anatomy/scripts/drive.test.mjs +72 -0
- package/skills/axstack-anatomy/scripts/enter-key.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/fidelity-cut.test.mjs +54 -0
- package/skills/axstack-anatomy/scripts/fidelity-tube-coverage.test.mjs +40 -0
- package/skills/axstack-anatomy/scripts/inline-kit.mjs +26 -0
- package/skills/axstack-anatomy/scripts/line-ends.mjs +306 -0
- package/skills/axstack-anatomy/scripts/lines-arcs.test.mjs +50 -0
- package/skills/axstack-anatomy/scripts/lines-bezier.test.mjs +28 -0
- package/skills/axstack-anatomy/scripts/lines-round.test.mjs +56 -0
- package/skills/axstack-anatomy/scripts/lines-rounded-rect.test.mjs +32 -0
- package/skills/axstack-anatomy/scripts/lines.mjs +31 -0
- package/skills/axstack-anatomy/scripts/lines.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/navigation.test.mjs +55 -0
- package/skills/axstack-anatomy/scripts/orbit-sampling.test.mjs +50 -0
- package/skills/axstack-anatomy/scripts/react-dots-opacity.test.mjs +51 -0
- package/skills/axstack-anatomy/scripts/react-readout-first-write.test.mjs +37 -0
- package/skills/axstack-anatomy/scripts/react-solid-seamless.test.mjs +47 -0
- package/skills/axstack-anatomy/scripts/react-visibility-target.test.mjs +38 -0
- package/skills/axstack-anatomy/scripts/turn-bench.mjs +74 -0
- package/skills/axstack-anatomy/scripts/turn-check.mjs +641 -0
- package/skills/axstack-anatomy/scripts/turn-fidelity.mjs +413 -0
- package/skills/axstack-anatomy/scripts/turn-group-reference.test.mjs +40 -0
- package/skills/axstack-anatomy/scripts/turn-split-owner.test.mjs +62 -0
- package/skills/axstack-anatomy/theme.js +56 -0
- package/skills/axstack-explain/SKILL.md +28 -15
- package/skills/axstack-explain/references/delivery.md +22 -0
- package/skills/{axstack-diagram → axstack-explain}/references/fidelity.md +1 -7
- package/skills/axstack-explain/references/inline-pages.md +8 -8
- package/skills/axstack-explain/references/visual-qa.md +3 -5
- package/skills/axstack-explain/references/whiteboard-defense.md +59 -0
- package/skills/axstack-research/SKILL.md +18 -6
- package/src/archify-legacy.js +70 -0
- package/src/capabilities.js +1 -29
- package/src/installer.js +11 -31
- package/skills/axstack-diagram/SKILL.md +0 -28
- package/skills/axstack-diagram/references/archify.md +0 -76
- package/src/archify-pin.js +0 -5
- package/src/archify.js +0 -151
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import * as k from "../kit/iso-kit.mjs";
|
|
2
|
+
import * as G from "../kit/lathe.mjs";
|
|
3
|
+
import { tubePieces } from "../kit/tube.mjs";
|
|
4
|
+
import { TURN } from "../kit/turn.mjs";
|
|
5
|
+
|
|
6
|
+
const rootOf = (group) => {
|
|
7
|
+
let root = group;
|
|
8
|
+
while (root && root.parent) root = root.parent;
|
|
9
|
+
return root && root.kind === "turn" ? root : null;
|
|
10
|
+
};
|
|
11
|
+
const restValues = (T, theta) => Object.fromEntries(T.groups.map((g) => [g.name, g.kind === "turn" && !g.parent ? theta : g.kind === "free" ? null : 0]));
|
|
12
|
+
|
|
13
|
+
export function identity(T, P, angles, { plant = null } = {}) {
|
|
14
|
+
const scene = TURN.prepare(T.data, { unit: 1 });
|
|
15
|
+
const subpaths = (d) => {
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const [, body] of (d ?? "").matchAll(/M([^M]*)/g)) {
|
|
18
|
+
const nums = (body.match(/-?\d+(?:\.\d+)?(?:e-?\d+)?/g) ?? []).map(Number);
|
|
19
|
+
const pts = [];
|
|
20
|
+
for (let i = 0; i + 1 < nums.length; i += 2) pts.push([nums[i], nums[i + 1]]);
|
|
21
|
+
out.push({ pts, closed: /Z\s*$/.test(body) });
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
};
|
|
25
|
+
const segDist = (p, a, b) => {
|
|
26
|
+
const dx = b[0] - a[0];
|
|
27
|
+
const dy = b[1] - a[1];
|
|
28
|
+
const l = dx * dx + dy * dy;
|
|
29
|
+
const t = l ? Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / l)) : 0;
|
|
30
|
+
return Math.hypot(p[0] - a[0] - dx * t, p[1] - a[1] - dy * t);
|
|
31
|
+
};
|
|
32
|
+
const toPoly = (pts, closed) => (pts, closed);
|
|
33
|
+
void toPoly;
|
|
34
|
+
const distToPoly = (p, poly, closed = true) => {
|
|
35
|
+
let best = Infinity;
|
|
36
|
+
const n = poly.length;
|
|
37
|
+
for (let i = 0; i < (closed ? n : n - 1); i++) best = Math.min(best, segDist(p, poly[i], poly[(i + 1) % n]));
|
|
38
|
+
if (n === 1) best = Math.hypot(p[0] - poly[0][0], p[1] - poly[0][1]);
|
|
39
|
+
return best;
|
|
40
|
+
};
|
|
41
|
+
const oneWay = (A, B) => {
|
|
42
|
+
let worst = 0;
|
|
43
|
+
for (const p of A) {
|
|
44
|
+
let near = null;
|
|
45
|
+
let gap = Infinity;
|
|
46
|
+
for (const q of B) {
|
|
47
|
+
const g = Math.hypot(p[0] - q[0], p[1] - q[1]);
|
|
48
|
+
if (g < gap) {
|
|
49
|
+
gap = g;
|
|
50
|
+
near = q;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
worst = Math.max(worst, gap < 0.15 ? Math.max(Math.abs(p[0] - near[0]), Math.abs(p[1] - near[1])) : distToPoly(p, B));
|
|
54
|
+
}
|
|
55
|
+
return worst;
|
|
56
|
+
};
|
|
57
|
+
const hausdorff = (A, B) => Math.max(oneWay(A, B), oneWay(B, A));
|
|
58
|
+
const applyMatrix = (pts, t) => {
|
|
59
|
+
const v = (t.match(/-?\d+(?:\.\d+)?(?:e-?\d+)?/g) ?? []).map(Number);
|
|
60
|
+
return pts.map(([x, y]) => [v[0] * x + v[2] * y + v[4], v[1] * x + v[3] * y + v[5]]);
|
|
61
|
+
};
|
|
62
|
+
const levelOf = (score, bands) => {
|
|
63
|
+
let u = 0;
|
|
64
|
+
for (const t of bands) {
|
|
65
|
+
const x = Math.min(1, Math.max(0, (score - (t - 0.035)) / 0.07));
|
|
66
|
+
u += x * x * (3 - 2 * x);
|
|
67
|
+
}
|
|
68
|
+
return u;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
const results = { outline: 0, top: 0, faces: 0, compared: 0, skipped: 0, missing: 0, crease: 0 };
|
|
73
|
+
const worstAt = {};
|
|
74
|
+
for (const theta of angles) {
|
|
75
|
+
const cams = TURN.cams(scene, restValues(T, theta));
|
|
76
|
+
const Pp = { ...P, azimuth: P.azimuth - theta };
|
|
77
|
+
for (const part of T.parts) {
|
|
78
|
+
if (part.kind !== "prism" || !rootOf(part.group) || part.group.kind === "free") continue;
|
|
79
|
+
const planted = plant && part.name === plant;
|
|
80
|
+
const run = scene.runs[part.id];
|
|
81
|
+
const cam = part.group ? cams[part.group.index] : cams.world;
|
|
82
|
+
const f = TURN.emit(run, cam, TURN.frame(run));
|
|
83
|
+
let root = part.group;
|
|
84
|
+
while (root && root.parent) root = root.parent;
|
|
85
|
+
const pivot = root ? [root.origin[0], root.origin[1], 0] : [0, 0, 0];
|
|
86
|
+
const a = k.iso(pivot, P);
|
|
87
|
+
const b = k.iso(pivot, Pp);
|
|
88
|
+
const shift = [a[0] - b[0], a[1] - b[1]];
|
|
89
|
+
const sh = (pts) => pts.map(([x, y]) => [x + shift[0], y + shift[1]]);
|
|
90
|
+
const { F, ring, s0, s1 } = part.world;
|
|
91
|
+
const up = run.up;
|
|
92
|
+
const Pk = planted ? { ...Pp, azimuth: Pp.azimuth - 2 } : Pp;
|
|
93
|
+
const kit = up ? k.extrude(ring, s0, s1 - s0, Pk) : G.prismOf(F, ring, s0, s1, Pk);
|
|
94
|
+
const cut = Boolean(part.spec.cuts?.length);
|
|
95
|
+
const mine = subpaths(f.d[run.role[cut ? "fill" : "edge"]])[0].pts;
|
|
96
|
+
const theirs = sh(subpaths(kit.fill)[0].pts);
|
|
97
|
+
const visible = cut ? oneWay(subpaths(f.d[run.role.edge]).flatMap((path) => path.pts), theirs) : 0;
|
|
98
|
+
const ho = Math.max(hausdorff(mine, theirs), visible);
|
|
99
|
+
results.outline = Math.max(results.outline, ho);
|
|
100
|
+
if (ho > (worstAt[part.name]?.outline ?? -1)) worstAt[part.name] = { ...(worstAt[part.name] ?? {}), outline: ho, at: theta };
|
|
101
|
+
if (up) {
|
|
102
|
+
const topMine = applyMatrix(subpaths(part.planPath)[0].pts, f.t[run.role.top]);
|
|
103
|
+
const topKit = sh(subpaths(kit.top)[0].pts);
|
|
104
|
+
results.top = Math.max(results.top, hausdorff(topMine, topKit));
|
|
105
|
+
}
|
|
106
|
+
const kitFaces = kit.shades.map((d) => subpaths(d)).concat(up ? [] : [subpaths(kit.top)]);
|
|
107
|
+
const faceCount = up ? run.n : run.n + 2;
|
|
108
|
+
for (let i = 0; i < faceCount; i++) {
|
|
109
|
+
const role = run.role[`f${i}`];
|
|
110
|
+
const d = f.d[role];
|
|
111
|
+
if (!d) continue;
|
|
112
|
+
const q = f.q[role];
|
|
113
|
+
if (q % TURN.q !== 0) {
|
|
114
|
+
results.skipped++;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const tone = q / TURN.q;
|
|
118
|
+
const pts = subpaths(d)[0].pts;
|
|
119
|
+
let best = Infinity;
|
|
120
|
+
for (const sp of kitFaces[tone] ?? []) best = Math.min(best, hausdorff(pts, sh(sp.pts)));
|
|
121
|
+
if (!Number.isFinite(best)) {
|
|
122
|
+
results.missing++;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
results.compared++;
|
|
126
|
+
results.faces = Math.max(results.faces, best);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const limit = 0.06;
|
|
132
|
+
const pass = results.outline <= limit && results.top <= limit && results.faces <= limit && results.missing === 0;
|
|
133
|
+
const worst = Object.entries(worstAt).filter(([, w]) => w.outline > limit).map(([name, w]) => `${name} outline ${w.outline.toFixed(3)} at ${w.at}°`);
|
|
134
|
+
return { pass, text: `prisms at ${angles.length} angles: outline ${results.outline.toFixed(4)} px · top ${results.top.toFixed(4)} px · faces ${results.faces.toFixed(4)} px over ${results.compared} faces (${results.skipped} in crossfade windows, ${results.missing} with no kit face of that tone)`, worst };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function rounds(T, P, angles, { only = null } = {}) {
|
|
138
|
+
const scene = TURN.prepare(T.data, { unit: 1 });
|
|
139
|
+
const CELL = 0.1;
|
|
140
|
+
const subpaths = (d) => {
|
|
141
|
+
const out = [];
|
|
142
|
+
for (const [, body] of (d ?? "").matchAll(/M([^M]*)/g)) {
|
|
143
|
+
const nums = (body.match(/-?\d+(?:\.\d+)?(?:e-?\d+)?/g) ?? []).map(Number);
|
|
144
|
+
const pts = [];
|
|
145
|
+
for (let i = 0; i + 1 < nums.length; i += 2) pts.push([nums[i], nums[i + 1]]);
|
|
146
|
+
out.push(pts);
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
};
|
|
150
|
+
const segDist = (p, a, b) => {
|
|
151
|
+
const dx = b[0] - a[0];
|
|
152
|
+
const dy = b[1] - a[1];
|
|
153
|
+
const l = dx * dx + dy * dy;
|
|
154
|
+
const t = l ? Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / l)) : 0;
|
|
155
|
+
return Math.hypot(p[0] - a[0] - dx * t, p[1] - a[1] - dy * t);
|
|
156
|
+
};
|
|
157
|
+
const distToPoly = (p, poly) => {
|
|
158
|
+
let best = Infinity;
|
|
159
|
+
for (let i = 0; i < poly.length; i++) best = Math.min(best, segDist(p, poly[i], poly[(i + 1) % poly.length]));
|
|
160
|
+
return best;
|
|
161
|
+
};
|
|
162
|
+
const hausdorff = (A, B) => {
|
|
163
|
+
let worst = 0;
|
|
164
|
+
for (const p of A) worst = Math.max(worst, distToPoly(p, B));
|
|
165
|
+
for (const p of B) worst = Math.max(worst, distToPoly(p, A));
|
|
166
|
+
return worst;
|
|
167
|
+
};
|
|
168
|
+
function raster(layers, box, grow = 0) {
|
|
169
|
+
const [x0, y0, x1, y1] = box;
|
|
170
|
+
const w = Math.ceil((x1 - x0) / CELL) + 1;
|
|
171
|
+
const h = Math.ceil((y1 - y0) / CELL) + 1;
|
|
172
|
+
const out = new Int8Array(w * h).fill(-1);
|
|
173
|
+
for (const { tone, d } of layers) {
|
|
174
|
+
if (!d) continue;
|
|
175
|
+
const mask = new Uint8Array(w * h);
|
|
176
|
+
const polys = subpaths(d);
|
|
177
|
+
for (let row = 0; row < h; row++) {
|
|
178
|
+
const y = y0 + row * CELL + CELL / 2;
|
|
179
|
+
const hits = [];
|
|
180
|
+
for (const poly of polys)
|
|
181
|
+
for (let i = 0; i < poly.length; i++) {
|
|
182
|
+
const a = poly[i];
|
|
183
|
+
const b = poly[(i + 1) % poly.length];
|
|
184
|
+
if ((a[1] <= y && b[1] > y) || (b[1] <= y && a[1] > y)) {
|
|
185
|
+
const x = a[0] + ((y - a[1]) / (b[1] - a[1])) * (b[0] - a[0]);
|
|
186
|
+
hits.push([x, b[1] > a[1] ? 1 : -1]);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
hits.sort((p, q) => p[0] - q[0]);
|
|
190
|
+
let wind = 0;
|
|
191
|
+
for (let i = 0; i < hits.length - 1; i++) {
|
|
192
|
+
wind += hits[i][1];
|
|
193
|
+
if (!wind) continue;
|
|
194
|
+
const from = Math.max(0, Math.ceil((hits[i][0] - x0 - CELL / 2) / CELL));
|
|
195
|
+
const to = Math.min(w - 1, Math.floor((hits[i + 1][0] - x0 - CELL / 2) / CELL));
|
|
196
|
+
for (let c = from; c <= to; c++) mask[row * w + c] = 1;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const reach = Math.round(grow / CELL);
|
|
200
|
+
for (let row = 0; row < h; row++)
|
|
201
|
+
for (let c = 0; c < w; c++) {
|
|
202
|
+
let on = mask[row * w + c];
|
|
203
|
+
for (let dy = -reach; dy <= reach && !on; dy++)
|
|
204
|
+
for (let dx = -reach; dx <= reach && !on; dx++) {
|
|
205
|
+
if (dx * dx + dy * dy > reach * reach) continue;
|
|
206
|
+
const rr = row + dy;
|
|
207
|
+
const cc = c + dx;
|
|
208
|
+
if (rr >= 0 && cc >= 0 && rr < h && cc < w && mask[rr * w + cc]) on = 1;
|
|
209
|
+
}
|
|
210
|
+
if (on) out[row * w + c] = tone;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return { out, w, h };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const stats = { outline: 0, outlineFine: 0, bands: 0, cells: 0, mismatched: 0, parts: 0 };
|
|
217
|
+
const worst = [];
|
|
218
|
+
for (const theta of angles) {
|
|
219
|
+
const cams = TURN.cams(scene, restValues(T, theta));
|
|
220
|
+
const Pp = { ...P, azimuth: P.azimuth - theta };
|
|
221
|
+
const Pf = { ...Pp, origin: [Pp.origin[0] * 10, Pp.origin[1] * 10], scale: Pp.scale * 10 };
|
|
222
|
+
for (const part of T.parts) {
|
|
223
|
+
if ((part.kind !== "round" && part.kind !== "lathe") || (only && !only.test(part.name)) || !rootOf(part.group) || part.group.kind === "free") continue;
|
|
224
|
+
const run = scene.runs[part.id];
|
|
225
|
+
const cam = part.group ? cams[part.group.index] : cams.world;
|
|
226
|
+
const f = TURN.emit(run, cam, TURN.frame(run));
|
|
227
|
+
let root = part.group;
|
|
228
|
+
while (root && root.parent) root = root.parent;
|
|
229
|
+
const pivot = root ? [root.origin[0], root.origin[1], 0] : [0, 0, 0];
|
|
230
|
+
const a = k.iso(pivot, P);
|
|
231
|
+
const b = k.iso(pivot, Pp);
|
|
232
|
+
const shift = [a[0] - b[0], a[1] - b[1]];
|
|
233
|
+
let F, s0, s1, r0, r1, ends, profile;
|
|
234
|
+
if (part.kind === "lathe") {
|
|
235
|
+
const { curve, knots } = part.world;
|
|
236
|
+
F = part.world.F;
|
|
237
|
+
s0 = curve.s0;
|
|
238
|
+
s1 = curve.s1;
|
|
239
|
+
r0 = curve.radius(s0);
|
|
240
|
+
r1 = curve.radius(s1);
|
|
241
|
+
ends = ["flat", "flat"];
|
|
242
|
+
profile = [[s0, r0], ...knots.slice(1, -1).map((sv) => [sv, curve.radius(sv), 1]), [s1, r1]];
|
|
243
|
+
} else ({ F, s0, s1, r0, r1, ends, profile } = part.world);
|
|
244
|
+
const meridian = [...(ends[0] === "open" ? [] : [[profile[0][0], 0]]), ...profile.filter((p) => p[1] > 1e-9), ...(ends[1] === "open" ? [] : [[profile[profile.length - 1][0], 0]])];
|
|
245
|
+
const domed = ends.includes("dome");
|
|
246
|
+
const radius = (s) => {
|
|
247
|
+
if (ends[0] === "dome" && s < s0) return Math.sqrt(Math.max(0, r0 * r0 - (s - s0) ** 2));
|
|
248
|
+
if (ends[1] === "dome" && s > s1) return Math.sqrt(Math.max(0, r1 * r1 - (s - s1) ** 2));
|
|
249
|
+
return r0 + ((r1 - r0) * (Math.max(s0, Math.min(s1, s)) - s0)) / (s1 - s0);
|
|
250
|
+
};
|
|
251
|
+
const slope = (s) => {
|
|
252
|
+
if (ends[0] === "dome" && s < s0) return -(s - s0) / Math.max(1e-4, radius(s));
|
|
253
|
+
if (ends[1] === "dome" && s > s1) return -(s - s1) / Math.max(1e-4, radius(s));
|
|
254
|
+
return (r1 - r0) / (s1 - s0);
|
|
255
|
+
};
|
|
256
|
+
const lo = ends[0] === "dome" ? s0 - r0 : s0;
|
|
257
|
+
const hi = ends[1] === "dome" ? s1 + r1 : s1;
|
|
258
|
+
const knots = [];
|
|
259
|
+
for (let i = 0; i <= 64; i++) knots.push(lo + ((hi - lo) * i) / 64);
|
|
260
|
+
const analytic = [[lo, 0], ...knots.slice(1, -1).map((sv) => [sv, radius(sv), 1]), [hi, 0]];
|
|
261
|
+
if (ends[0] !== "dome") analytic.splice(1, 0, [lo, r0]);
|
|
262
|
+
if (ends[1] !== "dome") analytic.splice(analytic.length - 1, 0, [hi, r1]);
|
|
263
|
+
const lathed = part.kind === "lathe" ? { slope: part.world.curve.slope, radius: part.world.curve.radius } : null;
|
|
264
|
+
const kit = lathed ? G.lathe(meridian, F, Pp, { smooth: lathed }) : domed ? G.lathe(analytic, F, Pp, { smooth: { slope, radius } }) : G.lathe(meridian, F, Pp, { smooth: true });
|
|
265
|
+
const kitFine = lathed ? G.lathe(meridian, F, Pf, { smooth: lathed }) : domed ? G.lathe(analytic, F, Pf, { smooth: { slope, radius } }) : G.lathe(meridian, F, Pf, { smooth: true });
|
|
266
|
+
const sh = (pts) => pts.map(([x, y]) => [x + shift[0], y + shift[1]]);
|
|
267
|
+
const cut = Boolean(part.spec.cuts?.length);
|
|
268
|
+
const mine = subpaths(f.d[run.role[cut ? "fill" : "edge"]])[0];
|
|
269
|
+
const theirs = sh(subpaths(kit.fill)[0]);
|
|
270
|
+
const fineRef = subpaths(kitFine.fill)[0].map(([x, y]) => [x / 10 + shift[0], y / 10 + shift[1]]);
|
|
271
|
+
const ho = hausdorff(mine, theirs);
|
|
272
|
+
const visible = cut ? subpaths(f.d[run.role.edge]).flat().reduce((worst, point) => Math.max(worst, distToPoly(point, fineRef)), 0) : 0;
|
|
273
|
+
const hf = Math.max(hausdorff(mine, fineRef), visible);
|
|
274
|
+
stats.outline = Math.max(stats.outline, ho);
|
|
275
|
+
stats.outlineFine = Math.max(stats.outlineFine, hf);
|
|
276
|
+
stats.parts++;
|
|
277
|
+
const ours = [0, 1, 2, 3, 4].map((t) => ({ tone: t, d: f.d[run.role[`s${t}`]] }));
|
|
278
|
+
for (const end of [0, 1]) {
|
|
279
|
+
const role = run.role[`k${end}`];
|
|
280
|
+
if (role === undefined || !f.d[role]) continue;
|
|
281
|
+
const q = f.q[role];
|
|
282
|
+
ours.push({ tone: q % TURN.q === 0 ? q / TURN.q : 9, d: f.d[role] });
|
|
283
|
+
}
|
|
284
|
+
const theirsLayers = [...kitFine.shades.map((d, t) => ({ tone: t, d })), { tone: 4, d: kitFine.top }].map(({ tone, d }) => ({ tone, d: subpaths(d).map((poly) => "M" + poly.map(([x, y]) => `${x / 10 + shift[0]} ${y / 10 + shift[1]}`).join("L") + "Z").join("") }));
|
|
285
|
+
let x0 = Infinity;
|
|
286
|
+
let y0 = Infinity;
|
|
287
|
+
let x1 = -Infinity;
|
|
288
|
+
let y1 = -Infinity;
|
|
289
|
+
for (const [x, y] of mine) {
|
|
290
|
+
x0 = Math.min(x0, x);
|
|
291
|
+
y0 = Math.min(y0, y);
|
|
292
|
+
x1 = Math.max(x1, x);
|
|
293
|
+
y1 = Math.max(y1, y);
|
|
294
|
+
}
|
|
295
|
+
const box = [x0 - 1, y0 - 1, x1 + 1, y1 + 1];
|
|
296
|
+
const A = raster(ours, box, 0.2);
|
|
297
|
+
const B = raster(theirsLayers, box, 0.2);
|
|
298
|
+
const inside = raster([{ tone: 1, d: f.d[run.role.edge] }], box).out;
|
|
299
|
+
const capCells = raster([0, 1].map((end) => ({ tone: 1, d: run.role[`k${end}`] !== undefined ? f.d[run.role[`k${end}`]] : "" })), box, 0.6).out;
|
|
300
|
+
const capExempt = part.kind === "lathe";
|
|
301
|
+
const insideKit = raster([{ tone: 1, d: "M" + fineRef.map(([x, y]) => `${x} ${y}`).join("L") + "Z" }], box).out;
|
|
302
|
+
let worstGap = 0;
|
|
303
|
+
const reach = Math.ceil(0.5 / CELL) + 2;
|
|
304
|
+
for (let row = 0; row < A.h; row++)
|
|
305
|
+
for (let c = 0; c < A.w; c++) {
|
|
306
|
+
const i = row * A.w + c;
|
|
307
|
+
if (inside[i] < 0 || insideKit[i] < 0) continue;
|
|
308
|
+
const ta = A.out[i];
|
|
309
|
+
const tb = B.out[i];
|
|
310
|
+
if (ta === 9 || ta < 0 || tb < 0) continue;
|
|
311
|
+
if (capExempt && capCells[i] >= 0) continue;
|
|
312
|
+
stats.cells++;
|
|
313
|
+
if (ta === tb) continue;
|
|
314
|
+
let edge = false;
|
|
315
|
+
for (let dy = -3; dy <= 3 && !edge; dy++) for (let dx = -3; dx <= 3; dx++) if (inside[(row + dy) * A.w + c + dx] < 0) edge = true;
|
|
316
|
+
if (edge) continue;
|
|
317
|
+
stats.mismatched++;
|
|
318
|
+
let near = Infinity;
|
|
319
|
+
for (let dy = -reach; dy <= reach; dy++)
|
|
320
|
+
for (let dx = -reach; dx <= reach; dx++) {
|
|
321
|
+
const rr = row + dy;
|
|
322
|
+
const cc = c + dx;
|
|
323
|
+
if (rr < 0 || cc < 0 || rr >= A.h || cc >= A.w) continue;
|
|
324
|
+
if (B.out[rr * A.w + cc] === ta) near = Math.min(near, Math.hypot(dx, dy) * CELL);
|
|
325
|
+
}
|
|
326
|
+
worstGap = Math.max(worstGap, near);
|
|
327
|
+
}
|
|
328
|
+
stats.bands = Math.max(stats.bands, worstGap);
|
|
329
|
+
if (worstGap > 0.5 || hf > 0.05) worst.push({ part: part.name, theta, band: worstGap, outline: hf });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return { pass: stats.outlineFine <= 0.05 && stats.bands <= 0.5, text: `rounds: ${stats.parts} frames at ${angles.length} angles: outline vs kit ${stats.outline.toFixed(4)} px (kit rounds to 0.1), vs kit at 10x ${stats.outlineFine.toFixed(4)} px · band boundaries ≤ ${stats.bands.toFixed(2)} px apart (${stats.mismatched} of ${stats.cells} interior cells differ)`, worst: worst.sort((a, b) => b.band - a.band || b.outline - a.outline).slice(0, 12).map((w) => `${w.part} at ${w.theta}°: outline ${w.outline.toFixed(3)} px, band ${w.band.toFixed(2)} px`) };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function tubes(T, P, angles) {
|
|
337
|
+
const scene = TURN.prepare(T.data, { unit: 1 });
|
|
338
|
+
const subpaths = (d) => [...(d ?? "").matchAll(/M([^M]*)/g)].map(([, body]) => {
|
|
339
|
+
const nums = (body.match(/-?\d+(?:\.\d+)?(?:e-?\d+)?/g) ?? []).map(Number);
|
|
340
|
+
const pts = [];
|
|
341
|
+
for (let i = 0; i + 1 < nums.length; i += 2) pts.push([nums[i], nums[i + 1]]);
|
|
342
|
+
return pts;
|
|
343
|
+
});
|
|
344
|
+
const segDist = (p, a, b) => {
|
|
345
|
+
const dx = b[0] - a[0], dy = b[1] - a[1], l = dx * dx + dy * dy;
|
|
346
|
+
const t = l ? Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / l)) : 0;
|
|
347
|
+
return Math.hypot(p[0] - a[0] - dx * t, p[1] - a[1] - dy * t);
|
|
348
|
+
};
|
|
349
|
+
const compare = (mine, theirs) => {
|
|
350
|
+
const A = subpaths(mine);
|
|
351
|
+
const B = subpaths(theirs);
|
|
352
|
+
if (A.length !== B.length) return { error: Infinity, why: `subpaths ${A.length} vs ${B.length}` };
|
|
353
|
+
let worst = 0;
|
|
354
|
+
for (let i = 0; i < A.length; i++) {
|
|
355
|
+
if (A[i].length === B[i].length) for (let j = 0; j < A[i].length; j++) worst = Math.max(worst, Math.abs(A[i][j][0] - B[i][j][0]), Math.abs(A[i][j][1] - B[i][j][1]));
|
|
356
|
+
else {
|
|
357
|
+
for (const [from, to] of [[A[i], B[i]], [B[i], A[i]]]) for (const p of from) worst = Math.max(worst, Math.min(...to.slice(1).map((q, j) => segDist(p, to[j], q))));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return { error: worst };
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const stats = { body: 0, shine: 0, shade: 0, edges: 0, rings: 0, chunks: 0 };
|
|
364
|
+
const bad = [];
|
|
365
|
+
for (const theta of angles) {
|
|
366
|
+
const cams = TURN.cams(scene, restValues(T, theta));
|
|
367
|
+
const Pp = { ...P, azimuth: P.azimuth - theta };
|
|
368
|
+
const Pf = { ...Pp, origin: [Pp.origin[0] * 10, Pp.origin[1] * 10], scale: Pp.scale * 10 };
|
|
369
|
+
for (const route of T.routes) {
|
|
370
|
+
if (route.group && (!rootOf(route.group) || route.group.kind === "free")) continue;
|
|
371
|
+
const root = rootOf(route.group);
|
|
372
|
+
const pivot = root ? [root.origin[0], root.origin[1], 0] : [0, 0, 0];
|
|
373
|
+
const a = k.iso(pivot, P);
|
|
374
|
+
const b = k.iso(pivot, Pp);
|
|
375
|
+
const shift = [a[0] - b[0], a[1] - b[1]];
|
|
376
|
+
const kit = tubePieces(route.world, route.r, Pf, { maxLength: route.chunk, spacing: route.spacing, gaps: route.gaps, breaks: route.breaks, caps: route.caps, stripes: route.wide, rings: route.rings });
|
|
377
|
+
const scaled = (d) => subpaths(d).map((pts) => "M" + pts.map(([x, y]) => `${x / 10 + shift[0]} ${y / 10 + shift[1]}`).join("L")).join("");
|
|
378
|
+
const chunks = T.parts.filter((part) => part.route === route.name);
|
|
379
|
+
if (kit.length !== chunks.length) {
|
|
380
|
+
bad.push(`${route.name}: ${chunks.length} chunks vs kit ${kit.length}`);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
chunks.forEach((part, index) => {
|
|
384
|
+
const run = scene.runs[part.id];
|
|
385
|
+
const f = TURN.emit(run, route.group ? cams[route.group.index] : cams.world, TURN.frame(run));
|
|
386
|
+
const piece = kit[index];
|
|
387
|
+
stats.chunks++;
|
|
388
|
+
for (const key of ["body", "shine", "shade", "edges"]) {
|
|
389
|
+
if (run.role[key] === undefined) continue;
|
|
390
|
+
const { error, why } = compare(f.d[run.role[key]], scaled(piece[key]));
|
|
391
|
+
stats[key] = Math.max(stats[key], error);
|
|
392
|
+
if (error > 0.05) bad.push(`${part.name} ${key} at ${theta}°: ${why ?? error.toFixed(3)}`);
|
|
393
|
+
}
|
|
394
|
+
if (route.rings) {
|
|
395
|
+
const mine = run.roles.filter((role) => role.name.startsWith("r")).map((role) => f.d[run.role[role.name]]).join("");
|
|
396
|
+
const theirs = scaled(piece.rings.filter((ring) => ring.alpha * 32 >= 0.5).map((ring) => ring.d).join(""));
|
|
397
|
+
const A = subpaths(mine);
|
|
398
|
+
const B = subpaths(theirs);
|
|
399
|
+
let worst = A.length === B.length ? 0 : Infinity;
|
|
400
|
+
for (const seg of A) {
|
|
401
|
+
let best = Infinity;
|
|
402
|
+
for (const other of B) best = Math.min(best, Math.max(...seg.map((p, j) => Math.hypot(p[0] - other[j][0], p[1] - other[j][1]))));
|
|
403
|
+
worst = Math.max(worst, best);
|
|
404
|
+
}
|
|
405
|
+
stats.rings = Math.max(stats.rings, worst);
|
|
406
|
+
if (worst > 0.05) bad.push(`${part.name} rings at ${theta}°: ${A.length} vs ${B.length} segments, worst ${worst.toFixed(3)}`);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return { pass: !bad.length, text: `tubes: ${stats.chunks} chunk frames at ${angles.length} angles against tubePieces at 10x: body ${stats.body.toFixed(4)} · shine ${stats.shine.toFixed(4)} · shade ${stats.shade.toFixed(4)} · edges ${stats.edges.toFixed(4)} · rings ${stats.rings.toFixed(4)} px`, worst: bad.slice(0, 20) };
|
|
413
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { turning } from "../kit/turn-build.mjs";
|
|
4
|
+
import { identity } from "./turn-fidelity.mjs";
|
|
5
|
+
const P = { origin: [0, 0], scale: 1, azimuth: 45, elevation: 30 };
|
|
6
|
+
const fixture = () => { const T = turning(P); T.layer("parts"); return T; };
|
|
7
|
+
const prism = (T, group) => T.prism({ name: "pointer", group, plan: { x: 0, y: 0, w: 4, d: 2, r: 0 }, z: 0, h: 1 });
|
|
8
|
+
test("missing named part groups cannot silently pass as static geometry", () => {
|
|
9
|
+
const T = fixture(); T.group("dial", { turn: {} });
|
|
10
|
+
assert.throws(() => { prism(T, "dail"); T.build(); identity(T, P, [0, 90]); }, /turning: unknown group dail/);
|
|
11
|
+
assert.equal(T.parts.length, 0);
|
|
12
|
+
});
|
|
13
|
+
test("missing named parent groups cannot silently root nested motion", () => {
|
|
14
|
+
const T = fixture(); T.group("dial", { turn: {} });
|
|
15
|
+
assert.throws(() => T.group("slide", { parent: "dail", slide: { direction: [1, 0, 0], travel: 5 } }), /turning: unknown group dail/);
|
|
16
|
+
assert.equal(T.groups.length, 1);
|
|
17
|
+
});
|
|
18
|
+
test("duplicate group names cannot alias incompatible motion values", () => {
|
|
19
|
+
const T = fixture(); T.group("dial", { turn: {} });
|
|
20
|
+
assert.throws(() => T.group("dial", { slide: { direction: [1, 0, 0], travel: 5 } }), /turning: duplicate group dial/);
|
|
21
|
+
assert.equal(T.groups.length, 1);
|
|
22
|
+
});
|
|
23
|
+
test("existing named and object group references retain native fidelity", () => {
|
|
24
|
+
for (const named of [false, true]) {
|
|
25
|
+
const T = fixture(); const group = T.group("dial", { turn: {} });
|
|
26
|
+
const part = prism(T, named ? "dial" : group);
|
|
27
|
+
const child = T.group("slide", { parent: named ? "dial" : group, slide: { direction: [1, 0, 0], travel: 5 } });
|
|
28
|
+
assert.equal(part.group, group); assert.equal(child.parent, group);
|
|
29
|
+
T.build(); const result = identity(T, P, [0, 90]);
|
|
30
|
+
assert.equal(result.pass, true);
|
|
31
|
+
assert.match(result.text, /over [1-9]\d* faces/);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
test("omitted and explicit null groups keep supported static geometry", () => {
|
|
35
|
+
for (const group of [undefined, null]) {
|
|
36
|
+
const T = fixture(); const part = prism(T, group);
|
|
37
|
+
assert.equal(part.group, null);
|
|
38
|
+
T.build(); assert.equal(T.parts.length, 1);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import * as A from "../kit/audit.mjs";
|
|
4
|
+
import * as G from "../kit/lathe.mjs";
|
|
5
|
+
import { turning } from "../kit/turn-build.mjs";
|
|
6
|
+
import { TURN } from "../kit/turn.mjs";
|
|
7
|
+
import { posed } from "../kit/turn-audit.mjs";
|
|
8
|
+
function fixture(kind, owner = "pipe") {
|
|
9
|
+
const T = turning({ origin: [0, 0], scale: 1, azimuth: 45, elevation: 30 }); T.layer("all");
|
|
10
|
+
const group = T.group("assembly", { turn: { pivot: [5, 2] } });
|
|
11
|
+
const F = G.frameAlong([0, 0, 0], [0, 0, 1]);
|
|
12
|
+
const base = { name: "ferrule", owner, group, F, s0: 0, s1: 10 };
|
|
13
|
+
const part = kind === "prism" ? T.prism({ ...base, polygon: [[-2, -2], [2, -2], [2, 2], [-2, 2]] }) : T.round({ ...base, r: 2, ...(kind === "default round" ? {} : { fixed: false }) });
|
|
14
|
+
const tube = A.tube([[0, 0, -2], [0, 0, 12]], 0.5, { name: "pipe", owner: "pipe" });
|
|
15
|
+
return { T, part, tube };
|
|
16
|
+
}
|
|
17
|
+
for (const kind of ["prism", "round", "default round"]) test(`split ${kind} preserves explicit ownership and its pipe clearance`, () => {
|
|
18
|
+
const { T, part, tube } = fixture(kind);
|
|
19
|
+
assert.deepEqual(A.clearances([tube], part.shapes), []);
|
|
20
|
+
const halves = T.split(part, { n: [0, 0, 1], d: 5 });
|
|
21
|
+
assert.equal(halves.length, 2);
|
|
22
|
+
assert.ok(halves.every((half) => half.owner === "pipe" && half.shapes.every((shape) => shape.owner === "pipe")));
|
|
23
|
+
assert.deepEqual(A.clearances([tube], halves.flatMap((half) => half.shapes)), []);
|
|
24
|
+
T.build();
|
|
25
|
+
const scene = TURN.prepare(T.data, { unit: 1 });
|
|
26
|
+
for (const angle of [0, 45, 90, 180]) {
|
|
27
|
+
const cams = TURN.cams(scene, { assembly: angle });
|
|
28
|
+
const group = T.groups[0];
|
|
29
|
+
const pose = { R: cams[group.index].R, t: cams[group.index].t, origin: group.origin };
|
|
30
|
+
const shapes = halves.flatMap((half) => half.shapes.map((shape) => posed(shape, pose)));
|
|
31
|
+
const original = posed(part.shapes[0], pose);
|
|
32
|
+
assert.deepEqual(A.clearances([posed(tube, pose)], shapes), []);
|
|
33
|
+
for (const z of [2.5, 7.5]) {
|
|
34
|
+
const a = angle * Math.PI / 180;
|
|
35
|
+
const point = [5 - 4 * Math.cos(a) + 2 * Math.sin(a), 2 - 4 * Math.sin(a) - 2 * Math.cos(a), z];
|
|
36
|
+
const distance = Math.min(...shapes.map((shape) => A.sdf(shape, point)));
|
|
37
|
+
assert.ok(distance < 0);
|
|
38
|
+
assert.ok(Math.abs(distance - A.sdf(original, point)) < 1e-8);
|
|
39
|
+
}
|
|
40
|
+
for (const run of scene.runs) {
|
|
41
|
+
const frame = TURN.emit(run, cams[run.group], TURN.frame(run));
|
|
42
|
+
assert.ok(frame.d.some((path) => typeof path === "string" && path.length));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
test("an unrelated pipe still reports real penetration of split fittings", () => {
|
|
47
|
+
for (const kind of ["prism", "round", "default round"]) {
|
|
48
|
+
const { T, part, tube } = fixture(kind, "other");
|
|
49
|
+
const halves = T.split(part, { n: [0, 0, 1], d: 5 });
|
|
50
|
+
const failures = A.clearances([tube], halves.flatMap((half) => half.shapes));
|
|
51
|
+
assert.equal(failures.length, 2);
|
|
52
|
+
assert.ok(failures.every((failure) => failure.kind === "through" && failure.clearance < 0));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
test("unowned split parts retain normal collision checking", () => {
|
|
56
|
+
for (const kind of ["prism", "round", "default round"]) {
|
|
57
|
+
const { T, part, tube } = fixture(kind, null);
|
|
58
|
+
const halves = T.split(part, { n: [0, 0, 1], d: 5 });
|
|
59
|
+
assert.ok(halves.every((half) => !half.owner && half.shapes.every((shape) => !shape.owner)));
|
|
60
|
+
assert.equal(A.clearances([tube], halves.flatMap((half) => half.shapes)).length, 2);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Inject these exact bytes after the anatomy build's inline kit script.
|
|
2
|
+
function installAnatomyTheme({ doc = document, view = window, resolveColor } = {}) {
|
|
3
|
+
const body = doc.body;
|
|
4
|
+
const media = view.matchMedia('(prefers-color-scheme: dark)');
|
|
5
|
+
// Canvas delegates every supported CSS color syntax to the browser, in sRGB.
|
|
6
|
+
const color = resolveColor ?? ((value) => {
|
|
7
|
+
const canvas = doc.createElement('canvas');
|
|
8
|
+
canvas.width = canvas.height = 1;
|
|
9
|
+
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
10
|
+
context.fillStyle = value;
|
|
11
|
+
context.fillRect(0, 0, 1, 1);
|
|
12
|
+
return context.getImageData(0, 0, 1, 1).data;
|
|
13
|
+
});
|
|
14
|
+
if (!doc.getElementById('axstack-anatomy-theme')) {
|
|
15
|
+
const style = doc.createElement('style');
|
|
16
|
+
style.id = 'axstack-anatomy-theme';
|
|
17
|
+
style.textContent = `
|
|
18
|
+
body.iso[data-axstack-t3] { margin:0; padding:0; background:transparent;
|
|
19
|
+
--anatomy-page:transparent; --anatomy-mono:var(--font-mono);
|
|
20
|
+
color:var(--foreground); font-family:var(--font-sans); }
|
|
21
|
+
body.iso[data-axstack-t3] .iso-page { margin:0; padding:0; width:100%; max-width:none; min-height:0; background:none; }
|
|
22
|
+
body.iso[data-axstack-t3] .iso-plate { padding:16px; border-radius:var(--radius); }
|
|
23
|
+
`;
|
|
24
|
+
doc.head.appendChild(style);
|
|
25
|
+
}
|
|
26
|
+
const update = () => {
|
|
27
|
+
const background = view.getComputedStyle(doc.documentElement).getPropertyValue('--background').trim();
|
|
28
|
+
const marker = body.getAttribute('data-axstack-theme');
|
|
29
|
+
let dark = media.matches;
|
|
30
|
+
if (background) {
|
|
31
|
+
const rgba = color(background);
|
|
32
|
+
const alpha = rgba[3] / 255;
|
|
33
|
+
const channel = (index) => {
|
|
34
|
+
const value = (rgba[index] * alpha + 255 * (1 - alpha)) / 255;
|
|
35
|
+
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
36
|
+
};
|
|
37
|
+
dark = 0.2126 * channel(0) + 0.7152 * channel(1) + 0.0722 * channel(2) < 0.5;
|
|
38
|
+
if (body.getAttribute('data-axstack-t3') === null) body.setAttribute('data-axstack-t3', '');
|
|
39
|
+
} else body.removeAttribute('data-axstack-t3');
|
|
40
|
+
const theme = marker === 'dark' || marker === 'light' ? marker : dark ? 'dark' : 'light';
|
|
41
|
+
if (body.getAttribute('data-theme') !== theme) body.setAttribute('data-theme', theme);
|
|
42
|
+
// Override the kit's dark root scheme so transparent iframe pages follow the frame.
|
|
43
|
+
if (doc.documentElement.style.colorScheme !== theme) doc.documentElement.style.colorScheme = theme;
|
|
44
|
+
};
|
|
45
|
+
update();
|
|
46
|
+
media.addEventListener('change', update);
|
|
47
|
+
const observer = new view.MutationObserver(update);
|
|
48
|
+
observer.observe(doc.documentElement, {
|
|
49
|
+
attributes: true, attributeFilter: ['class', 'style', 'data-axstack-theme'] });
|
|
50
|
+
observer.observe(body, { attributes: true, attributeFilter: ['data-axstack-theme'] });
|
|
51
|
+
observer.observe(doc.head, { subtree: true, childList: true, characterData: true,
|
|
52
|
+
attributes: true, attributeFilter: ['media', 'disabled'] });
|
|
53
|
+
return () => { media.removeEventListener('change', update); observer.disconnect(); };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof document !== 'undefined') installAnatomyTheme();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: axstack-explain
|
|
3
|
-
description: When the user asks how, why, explain, or show, says they do not understand, a spec is being finalized, or you judge they need to understand something, use axstack-explain to show how it works and what exists, is missing, or remains unverified.
|
|
3
|
+
description: When the user asks how, why, explain, or show, asks to diagram, visualize, draw or defend, requests a whiteboard defense, says they do not understand, a spec is being finalized, or you judge they need to understand something, use axstack-explain to show how it works and what exists, is missing, or remains unverified.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Explain
|
|
@@ -12,6 +12,7 @@ Explain a system, change, current behavior, intended behavior, or implementation
|
|
|
12
12
|
gap from inspected evidence. This direct route needs no spec ceremony. Use
|
|
13
13
|
project documentation as evidence where relevant. It supersedes `axstack-docs`; if a stale install
|
|
14
14
|
exposes both skills, route the request here only.
|
|
15
|
+
For a stale `axstack-diagram` install, route requests here only.
|
|
15
16
|
|
|
16
17
|
Before acting, load [Standing contracts](../axstack/references/contracts.md),
|
|
17
18
|
then follow its required lifecycle and audit pointers. Explanation work has no
|
|
@@ -51,9 +52,21 @@ immediately before an actual profile dispatch.
|
|
|
51
52
|
|
|
52
53
|
## 2. Choose proportional output
|
|
53
54
|
|
|
54
|
-
Load [Diagram](../axstack-diagram/SKILL.md) for every diagram.
|
|
55
55
|
Explain owns evidence gathering, claim labels, the 700-word cap, dispatch, and delivery.
|
|
56
|
-
|
|
56
|
+
Choose the best visual for the concept: flowchart, sequence, state, architecture
|
|
57
|
+
map, timeline, comparison or bespoke SVG are examples, not a taxonomy.
|
|
58
|
+
Follow [Fidelity](references/fidelity.md) for every visual.
|
|
59
|
+
For whole-work understanding or defense, follow [Whiteboard defense](references/whiteboard-defense.md).
|
|
60
|
+
Dispatch every visual Explain delivers to `axstack-explainer` with a bounded
|
|
61
|
+
evidence brief, including inline pages, chat Mermaid and anatomy figures.
|
|
62
|
+
Plain-prose answers stay driver-written; other phases retain their own Mermaid
|
|
63
|
+
owners, and README/docs diagram edits go through Implement under these rules.
|
|
64
|
+
Use inline SVG or CSS inside inline pages. Never use Mermaid in inline pages.
|
|
65
|
+
Use Mermaid for chat, GitHub, and docs.
|
|
66
|
+
Include `accTitle` and `accDescr` in each Mermaid view.
|
|
67
|
+
Never use theme init in Mermaid. Split large Mermaid diagrams into views.
|
|
68
|
+
Use anatomy only on an explicit isometric, physical-object or anatomy request,
|
|
69
|
+
through [Anatomy](../axstack-anatomy/SKILL.md).
|
|
57
70
|
|
|
58
71
|
1. Keep the primary reader-facing explanation to a maximum of 700 words in
|
|
59
72
|
chat, HTML, and every other requested format. Preserve in that primary view
|
|
@@ -67,18 +80,13 @@ Diagram never calls explain.
|
|
|
67
80
|
maximum. If the draft is longer, compress repetition first and move only
|
|
68
81
|
supporting detail to a separate linked ticket or appendix. Essential answers
|
|
69
82
|
must not be hidden behind links, and evidence must not be silently discarded.
|
|
70
|
-
|
|
71
|
-
Each card has at most 40 words and cites its source, as defined in
|
|
72
|
-
[Archify](../axstack-diagram/references/archify.md).
|
|
73
|
-
Count node labels, headings, captions, and all non-card text.
|
|
83
|
+
Count node labels, headings, captions, and all page text.
|
|
74
84
|
2. Use chat only for an explicit chat request, an answer outside the description's conditions,
|
|
75
85
|
or inline-page fallback.
|
|
76
86
|
Use a compact diagram when useful.
|
|
77
|
-
For chat answers, never dispatch an agent when applying its Mermaid rules inline.
|
|
78
87
|
3. For the conditions in the description, create an inline page by default.
|
|
79
88
|
Honor an explicitly requested artifact format.
|
|
80
|
-
|
|
81
|
-
`axstack-explainer`. The reply adds only what the page omits.
|
|
89
|
+
The reply adds only what the page omits.
|
|
82
90
|
4. Profile IDs are presets, not availability proof. Before dispatch, follow the
|
|
83
91
|
launch sequence and preserve the configured model, mode, and effort. Report
|
|
84
92
|
an unavailable route; never substitute a model.
|
|
@@ -86,16 +94,18 @@ Diagram never calls explain.
|
|
|
86
94
|
## 3. Verify and deliver
|
|
87
95
|
|
|
88
96
|
1. For inline pages, follow [Inline pages](references/inline-pages.md).
|
|
89
|
-
For
|
|
97
|
+
For every explicitly requested HTML or visual artifact
|
|
90
98
|
outside inline T3 pages, follow [Visual QA](references/visual-qa.md).
|
|
91
99
|
2. Require independent `axstack-explainer-review` for consequential or complex
|
|
92
|
-
claims
|
|
100
|
+
claims or on request.
|
|
101
|
+
The `axstack-explainer-review` always reviews every visual at its exact final
|
|
102
|
+
bytes before delivery.
|
|
93
103
|
Consequential claims include gap or missing-claim reports, blast-radius
|
|
94
|
-
safety facts, and
|
|
104
|
+
safety facts, spec readbacks, and every whiteboard-defense explanation in chat too.
|
|
95
105
|
Bind it to the exact artifact identity.
|
|
96
106
|
Any byte change invalidates
|
|
97
107
|
that review and requires a fresh check. In `claude-only`, separate Sonnet
|
|
98
|
-
author high and reviewer high sessions are allowed for explanations as
|
|
108
|
+
author high and reviewer high sessions are allowed for explanations and every visual as
|
|
99
109
|
session independence only. This exception never permits same-model code
|
|
100
110
|
review or a cross-provider-independence claim.
|
|
101
111
|
3. Report source, tests, rendered observations, independent review, and
|
|
@@ -103,6 +113,9 @@ Diagram never calls explain.
|
|
|
103
113
|
4. For a public artifact, remove private paths, identifiers, prompts,
|
|
104
114
|
credentials, messages, and internal records. Local creation does not grant
|
|
105
115
|
authority to publish.
|
|
106
|
-
5.
|
|
116
|
+
5. Follow [Delivery](references/delivery.md) for fallback and repair holds.
|
|
117
|
+
Deliver the smallest useful explanation. Explanation causes no automatic
|
|
107
118
|
design and no automatic implementation; route later change work separately
|
|
108
119
|
only when the user requests it.
|
|
120
|
+
|
|
121
|
+
Ideas: [archify](https://github.com/tt-a1i/archify), [diagram-design](https://github.com/cathrynlavery/diagram-design), [visual-explainer](https://github.com/nicobailon/visual-explainer), [Matt Pocock](https://github.com/mattpocock/skills), and [poteto](https://github.com/poteto/how); guidance uses Axstack's own words.
|