mirror of
https://github.com/gittrahan/support-fins.git
synced 2026-10-19 02:10:52 -05:00
web/orient.js drives the whole left rail (the suggestion ranking, the strength arrow, the layer posture) and had no tests. Adds pure, offline invariants so a change to a verdict or a solver can't drift unnoticed: - layerVerdict buckets tall→weak / flat→strong / on-side→mixed, and the posture-ONLY return is pinned (locks the drop of the always-on layer note so a future edit can't quietly re-add it); - loadAlignment reads a pull good/mixed/poor by how much it crosses the layers, with the good/mixed cut at 60° off in-plane and a zero-vector guard; - suggestOrientations turns a part saved tilted (a baked-in 60° overhang) back to its support-free flat pose and ranks it first, sorted + well-formed; - suggestStrengthPose lays an axial pull into the layer plane on a SEATED pose (never the needle-tower the guard exists to reject) and declines an already in-plane load. 43 pass (30 existing + 13 new): deno test --allow-read tests/
117 lines
6.0 KiB
JavaScript
117 lines
6.0 KiB
JavaScript
// Orientation + strength logic (web/orient.js). Pure functions in, verdicts and
|
|
// ranked poses out -- no DOM, so they test cleanly here. Covers the three things
|
|
// the rail leans on: the layer-posture bucket, the load-alignment verdict, and the
|
|
// two solvers (minimise-support suggestions, and the load-driven strength pose).
|
|
|
|
import { WEB, assert, assertClose, blockTopo, tiltedBlockTopo, rotX } from './_util.js';
|
|
|
|
const { suggestOrientations, suggestStrengthPose, layerVerdict, loadAlignment } =
|
|
await import(`${WEB}orient.js`);
|
|
const { analyze } = await import(`${WEB}overhangs.js`);
|
|
|
|
const IDENT = rotX(0); // as-loaded pose
|
|
|
|
// ------------------------------------------------------------ layerVerdict
|
|
|
|
Deno.test('layerVerdict: a tall part reads weak (long axis up the layers)', () => {
|
|
assert(layerVerdict({ x: 10, y: 10, z: 100 }).posture === 'weak');
|
|
});
|
|
|
|
Deno.test('layerVerdict: a flat slab reads strong (long spans along the layers)', () => {
|
|
assert(layerVerdict({ x: 100, y: 80, z: 5 }).posture === 'strong');
|
|
});
|
|
|
|
Deno.test('layerVerdict: an on-its-side part reads mixed', () => {
|
|
// dims sorted [50,70,100]; z=70 is neither the tallest span nor the shortest.
|
|
assert(layerVerdict({ x: 100, y: 50, z: 70 }).posture === 'mixed');
|
|
});
|
|
|
|
Deno.test('layerVerdict: returns posture ONLY -- the always-on text note was dropped', () => {
|
|
// Locks the simplification: the note field is gone, posture is the whole API now,
|
|
// and the one remaining consumer (the strength caveat) reads posture, not note.
|
|
const v = layerVerdict({ x: 100, y: 80, z: 5 });
|
|
assert(v.note === undefined, 'layerVerdict should no longer return a display note');
|
|
assert(Object.keys(v).length === 1, `expected just {posture}, got ${Object.keys(v)}`);
|
|
});
|
|
|
|
// ------------------------------------------------------------ loadAlignment
|
|
|
|
Deno.test('loadAlignment: a pull straight up the build axis is poor (across the layers)', () => {
|
|
assert(loadAlignment([0, 0, 1]).quality === 'poor');
|
|
});
|
|
|
|
Deno.test('loadAlignment: a pull in the layer plane is good (along the layers)', () => {
|
|
assert(loadAlignment([1, 0, 0]).quality === 'good');
|
|
});
|
|
|
|
Deno.test('loadAlignment: a 45-degree pull is mixed', () => {
|
|
const al = loadAlignment([1, 0, 1]);
|
|
assert(al.quality === 'mixed');
|
|
assertClose(al.cross, Math.SQRT1_2, 1e-9, 'cross should be |z|/|d| = 1/sqrt(2)');
|
|
});
|
|
|
|
Deno.test('loadAlignment: the good/mixed boundary sits at 60 degrees from the plate', () => {
|
|
// cross = 0.5 is exactly 60deg off in-plane and must still count as good (<= 0.5).
|
|
assert(loadAlignment([Math.sqrt(3), 0, 1]).quality === 'good');
|
|
});
|
|
|
|
Deno.test('loadAlignment: a zero-length direction is null, not a crash', () => {
|
|
assert(loadAlignment([0, 0, 0]) === null);
|
|
});
|
|
|
|
// ------------------------------------------------------------ suggestOrientations
|
|
|
|
Deno.test('suggestOrientations: finds the flat pose for a part saved tilted', () => {
|
|
// A block whose vertices were tilted 60deg and saved that way: as loaded it has a
|
|
// broad downward overhang, but rotating it back flat needs no support at all. The
|
|
// suggester must surface that pose and rank it on top.
|
|
const topo = tiltedBlockTopo(0, 60, 0, 40, 0, 12, 60);
|
|
const asLoaded = analyze(topo, 45, IDENT);
|
|
assert(asLoaded.overArea > 1, 'the tilted-as-saved block should have an overhang as loaded');
|
|
|
|
const { candidates, confidence } = suggestOrientations(topo, { top: 3, threshold: 45 });
|
|
assert(candidates.length >= 1, 'expected at least one candidate');
|
|
assert(['high', 'medium', 'low', 'none'].includes(confidence), `bad confidence ${confidence}`);
|
|
|
|
const best = candidates[0];
|
|
assert(best.overArea < asLoaded.overArea, 'the best pose should reduce overhang area');
|
|
assert(best.overArea < 1, `the best pose should be ~support-free, got ${best.overArea} mm^2`);
|
|
assert(best.regions === 0, `the best pose should have no overhang regions, got ${best.regions}`);
|
|
});
|
|
|
|
Deno.test('suggestOrientations: candidates are ranked best-first and well-formed', () => {
|
|
const topo = tiltedBlockTopo(0, 60, 0, 40, 0, 12, 60);
|
|
const { candidates } = suggestOrientations(topo, { top: 3, threshold: 45 });
|
|
for (let i = 1; i < candidates.length; i++) {
|
|
assert(candidates[i - 1].score <= candidates[i].score, 'candidates must be sorted by score');
|
|
}
|
|
for (const c of candidates) {
|
|
assert(Array.isArray(c.rot) && c.rot.length === 9, 'rot must be a 3x3 (9-element) matrix');
|
|
assert(typeof c.seating === 'string', 'each candidate reports a seating kind');
|
|
assert(c.size && typeof c.size.z === 'number', 'each candidate carries its full seated size');
|
|
}
|
|
});
|
|
|
|
// ------------------------------------------------------------ suggestStrengthPose
|
|
|
|
Deno.test('suggestStrengthPose: lays the load into the layer plane, on a seated pose', () => {
|
|
// A tall square post pulled along its own long axis. As loaded the pull runs
|
|
// straight up the build layers (weak); the fix is to lay the post on its side so
|
|
// the pull runs along the layers -- and that pose still sits flat on the bed.
|
|
const topo = blockTopo(0, 20, 0, 20, 0, 100);
|
|
const pose = suggestStrengthPose(topo, [0, 0, 1], { threshold: 45 });
|
|
assert(pose, 'a strictly better printable pose should exist for an axial pull on a post');
|
|
assert(pose.bedArea >= 1, 'the recommended pose must actually sit on the bed, not balance on a tip');
|
|
assert(pose.cross < 0.5, `the load should end up in-plane (cross ${pose.cross})`);
|
|
assert(pose.quality === 'good', `expected a good alignment verdict, got ${pose.quality}`);
|
|
});
|
|
|
|
Deno.test('suggestStrengthPose: an already in-plane load needs no turn (returns null)', () => {
|
|
// Load already runs across the footprint of a flat slab -- in the layer plane
|
|
// whichever seated pose you pick -- so there's no stronger printable pose to offer.
|
|
const topo = blockTopo(0, 100, 0, 80, 0, 8);
|
|
const pose = suggestStrengthPose(topo, [1, 0, 0], { threshold: 45 });
|
|
// Either nothing beats the flat pose (null), or the best it finds is still in-plane.
|
|
assert(pose === null || pose.cross < 0.5, 'must not turn a flat slab into a worse-aligned pose');
|
|
});
|