mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
@@ -5,6 +5,8 @@ import { readFileSync } from 'node:fs';
|
||||
import {
|
||||
draftLiveCandidateSpace, draftLiveSeed, isSingleDraftAppend,
|
||||
} from '../test-build/draft-live-preflight.js';
|
||||
import { increasedViolations, junctionLimitViolations } from '../test-build/junction-limits.js';
|
||||
import { GRID_STEP_N } from '../test-build/space-geometry.js';
|
||||
|
||||
const room = (id, x, y, size = 0.1) => ({
|
||||
id,
|
||||
@@ -17,6 +19,45 @@ const wallSegments = (one) => one.poly.map((a, index) => ({
|
||||
owners: [one.id],
|
||||
}));
|
||||
|
||||
const cmUnits = (value) => value / 5 * GRID_STEP_N;
|
||||
|
||||
const limitSegments = (space) => [
|
||||
...(space.wall_segments || []),
|
||||
...(space.partitions || []),
|
||||
...(space.room_drafts || []).flatMap((draft) =>
|
||||
(draft.segments || []).map((segment, index) => ({
|
||||
id: segment.id,
|
||||
a: draft.points[index], b: draft.points[index + 1], cm: segment.cm,
|
||||
}))),
|
||||
];
|
||||
|
||||
const introducedRules = (previous, candidate) => increasedViolations(
|
||||
junctionLimitViolations(
|
||||
{ model_version: 9, spaces: [candidate] }, candidate.id,
|
||||
limitSegments(candidate), { status: 'lightweight' },
|
||||
),
|
||||
junctionLimitViolations(
|
||||
{ model_version: 9, spaces: [previous] }, previous.id,
|
||||
limitSegments(previous), { status: 'lightweight' },
|
||||
),
|
||||
).map((item) => item.rule).sort();
|
||||
|
||||
const paritySpace = (partitions, seedLengthCm = 40) => ({
|
||||
id: 'space', cell_cm: 5,
|
||||
rooms: [], walls: [], wall_segments: [], openings: [], open_spans: [],
|
||||
wall_columns: [],
|
||||
// Valid remote work makes the full input genuinely larger than the local
|
||||
// projection without adding an inherited limit violation.
|
||||
partitions: [
|
||||
...partitions,
|
||||
{ id: 'remote', a: [2, 2], b: [2 + cmUnits(40), 2], cm: 0 },
|
||||
],
|
||||
room_drafts: [{
|
||||
id: 'active', points: [[0, 0], [cmUnits(seedLengthCm), 0]],
|
||||
segments: [{ id: 'seed', cm: 0 }],
|
||||
}],
|
||||
});
|
||||
|
||||
const activeDraft = {
|
||||
id: 'active',
|
||||
points: [[0.05, 0.05], [0.1, 0.05], [0.1, 0.1], [0.2, 0.1]],
|
||||
@@ -140,6 +181,54 @@ test('#461 remote geometry growth does not grow the selected component', () => {
|
||||
assert.deepEqual(selectedEighty.space.partitions.map((item) => item.id), ['near']);
|
||||
});
|
||||
|
||||
test('#461 local and full junction verdicts agree at every limit boundary', () => {
|
||||
const ray = (id, angleDeg, lengthCm = 40) => {
|
||||
const angle = angleDeg * Math.PI / 180;
|
||||
return {
|
||||
id, a: [0, 0],
|
||||
b: [cmUnits(lengthCm) * Math.cos(angle), cmUnits(lengthCm) * Math.sin(angle)],
|
||||
cm: 0,
|
||||
};
|
||||
};
|
||||
const valenceRays = (total) => Array.from({ length: total - 1 }, (_, index) =>
|
||||
ray(`ray-${total}-${index}`, (index + 1) * 360 / total));
|
||||
const cases = [
|
||||
{ name: '14 degree junction is rejected', partitions: [ray('angle-14', 14)],
|
||||
expected: ['angle'] },
|
||||
{ name: '15 degree junction is accepted', partitions: [ray('angle-15', 15)],
|
||||
expected: [] },
|
||||
{ name: '19 cm segment is rejected', partitions: [], seedLengthCm: 19,
|
||||
expected: ['length'] },
|
||||
{ name: '20 cm segment is accepted', partitions: [], seedLengthCm: 20,
|
||||
expected: [] },
|
||||
{ name: '4 cm node clearance is rejected', partitions: [{
|
||||
id: 'distance-4',
|
||||
a: [cmUnits(4), cmUnits(4)], b: [cmUnits(4), cmUnits(44)], cm: 0,
|
||||
}], expected: ['distance'] },
|
||||
{ name: '5 cm node clearance is accepted', partitions: [{
|
||||
id: 'distance-5',
|
||||
a: [cmUnits(5), cmUnits(5)], b: [cmUnits(5), cmUnits(45)], cm: 0,
|
||||
}], expected: [] },
|
||||
{ name: 'valence 6 is accepted', partitions: valenceRays(6), expected: [] },
|
||||
{ name: 'valence 7 is rejected', partitions: valenceRays(7), expected: ['valence'] },
|
||||
];
|
||||
|
||||
for (const scenario of cases) {
|
||||
const candidate = paritySpace(scenario.partitions, scenario.seedLengthCm);
|
||||
const previous = { ...candidate, room_drafts: [] };
|
||||
const seed = draftLiveSeed(candidate, 'active');
|
||||
const localPrevious = draftLiveCandidateSpace(previous, seed)?.space;
|
||||
const localCandidate = draftLiveCandidateSpace(candidate, seed)?.space;
|
||||
assert.ok(seed && localPrevious && localCandidate, scenario.name);
|
||||
const full = introducedRules(previous, candidate);
|
||||
const local = introducedRules(localPrevious, localCandidate);
|
||||
assert.deepEqual(local, full, `${scenario.name}: local verdict must equal full verdict`);
|
||||
assert.deepEqual(full, scenario.expected, `${scenario.name}: boundary verdict`);
|
||||
assert.equal(localCandidate.partitions.some((item) => item.id === 'remote'), false,
|
||||
`${scenario.name}: parity must be proved on a bounded projection`);
|
||||
}
|
||||
});
|
||||
|
||||
test('#461 projector closes a collinear run and keeps every ray at its junctions', () => {
|
||||
const space = {
|
||||
id: 'space', cell_cm: 5, rooms: [], walls: [], wall_segments: [],
|
||||
|
||||
Reference in New Issue
Block a user