Files
houseplan-card/test/near-axis.test.mjs
T
2026-09-06 17:38:47 +03:00

146 lines
6.7 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import {
NEAR_AXIS_MAX_DEGREES,
NEAR_AXIS_MAX_SLOPE,
classifyNearAxisSegment,
repairNearAxisRoomWalls,
snapNearAxisEndpoint,
} from '../test-build/near-axis.js';
import { optimizePlans } from '../test-build/plan-optimizer.js';
import { MULTI_WALL_NEAR_ORTHOGONAL_MAX_DEGREES } from '../test-build/wall-thickness.js';
import { nearAxisProfile } from '../scripts/model-invariants.mjs';
import { readHouseplanProductionSource } from './houseplan-source.mjs';
const fixture = JSON.parse(readFileSync(
new URL('./fixtures/279-near-orthogonal-junction.json', import.meta.url), 'utf8',
));
const clone = (value) => JSON.parse(JSON.stringify(value));
const sourceOf = (file) => readFileSync(new URL(`../src/${file}`, import.meta.url), 'utf8');
test('#290 authoring, Optimize and renderer use one near-axis threshold source', () => {
const renderer = sourceOf('wall-thickness.ts');
const authoring = readHouseplanProductionSource();
const optimizer = sourceOf('plan-optimizer.ts');
assert.match(renderer, /import \{ NEAR_AXIS_MAX_DEGREES \} from '\.\/near-axis';/);
assert.match(renderer,
/export const MULTI_WALL_NEAR_ORTHOGONAL_MAX_DEGREES = NEAR_AXIS_MAX_DEGREES;/);
assert.match(authoring, /import \{ snapNearAxisEndpoint \} from '\.\/near-axis';/);
assert.match(authoring, /snapNearAxisEndpoint\(/);
assert.match(optimizer, /import \{ repairNearAxisRoomWalls \} from '\.\/near-axis';/);
assert.match(optimizer, /repairNearAxisRoomWalls\(space\)/);
});
test('#290 near-axis boundary is shared, inclusive and bounded', () => {
assert.equal(NEAR_AXIS_MAX_DEGREES, 0.25);
assert.equal(MULTI_WALL_NEAR_ORTHOGONAL_MAX_DEGREES, NEAR_AXIS_MAX_DEGREES);
assert.equal(classifyNearAxisSegment([0, 0], [316, 0]), null);
assert.equal(classifyNearAxisSegment([0, 0], [316, 1])?.axis, 'horizontal');
assert.equal(classifyNearAxisSegment([0, 0], [316, 2]), null);
assert.equal(classifyNearAxisSegment([0, 0], [1, NEAR_AXIS_MAX_SLOPE])?.axis, 'horizontal');
assert.equal(classifyNearAxisSegment([0, 0], [1, NEAR_AXIS_MAX_SLOPE * 1.000001]), null);
assert.equal(classifyNearAxisSegment([0, 0], [1, Math.tan(Math.PI / 6)]), null);
assert.equal(classifyNearAxisSegment([0, 0], [1, 316])?.axis, 'vertical');
assert.equal(classifyNearAxisSegment([1, 316], [0, 0])?.axis, 'vertical');
});
test('#290 Walls rule moves only the free endpoint', () => {
assert.deepEqual(snapNearAxisEndpoint([10, 20], [326, 21]), [326, 20]);
assert.deepEqual(snapNearAxisEndpoint([10, 20], [11, 336]), [10, 336]);
assert.deepEqual(snapNearAxisEndpoint([10, 20], [326, 22]), [326, 22]);
assert.deepEqual(snapNearAxisEndpoint([10, 20], [110, 120]), [110, 120]);
});
test('#290 repairs a duplicated 316x1 physical wall once without mutating input', () => {
const input = clone(fixture);
const before = JSON.stringify(input);
const result = repairNearAxisRoomWalls(input);
assert.equal(JSON.stringify(input), before);
assert.equal(result.report.wallsStraightened, 1);
assert.equal(result.report.wallsStraightenSkipped, 0);
assert.ok(Math.abs(result.report.maxStraightenShift - 1 / 240) < 1e-9);
const north = result.space.rooms.find((room) => room.id === 'north-west');
const south = result.space.rooms.find((room) => room.id === 'south-west');
assert.deepEqual(north.poly[1], south.poly[0]);
assert.deepEqual(north.poly[2], south.poly[3]);
assert.equal(north.poly[1][1], north.poly[2][1]);
});
test('#290 repairs ordinary independent walls but preserves true diagonals', () => {
const input = {
rooms: [],
partitions: [
{ id: 'former-draft-a', a: [0, 0], b: [316, 1], cm: 15 },
{ id: 'former-draft-b', a: [316, 1], b: [316, 20], cm: 15 },
{ id: 'near', a: [0, 100], b: [316, 101], cm: 15 },
{ id: 'diagonal', a: [0, 200], b: [100, 300], cm: 15 },
],
};
const result = repairNearAxisRoomWalls(input);
assert.equal(result.report.wallsStraightened, 2);
assert.equal(result.space.partitions[0].a[1], result.space.partitions[0].b[1]);
assert.deepEqual(result.space.partitions[1], input.partitions[1]);
assert.equal(result.space.partitions[2].a[1], result.space.partitions[2].b[1]);
assert.deepEqual(result.space.partitions[3], input.partitions[3]);
});
test('#290 skips a repair that would no longer fit a hosted opening', () => {
const input = {
rooms: [],
partitions: [{ id: 'host', a: [0, 0], b: [316, 1], cm: 15 }],
openings: [{
id: 'opening',
length: 316.001,
host: { kind: 'partition', id: 'host', t: 0.5 },
}],
};
const result = repairNearAxisRoomWalls(input);
assert.equal(result.report.wallsStraightened, 0);
assert.equal(result.report.wallsStraightenSkipped, 1);
assert.deepEqual(result.space.partitions, input.partitions);
});
test('#290 Optimize reports physical count, exact max centimetres and is idempotent', () => {
const config = { spaces: [{ id: 'floor', title: 'Floor', ...clone(fixture) }] };
const first = optimizePlans(config, {});
assert.equal(first.report.wallsStraightened, 1);
assert.equal(first.report.wallsStraightenSkipped, 0);
assert.ok(Math.abs(first.report.maxStraightenShiftCm - 1) < 1e-6);
assert.equal(first.report.maxStraightenSpace, 'floor');
assert.equal(first.changed, true);
const second = optimizePlans(first.config, first.layout);
assert.equal(second.changed, false);
assert.equal(second.report.wallsStraightened, 0);
assert.equal(second.report.maxStraightenShiftCm, 0);
});
test('#290 Optimize reduces the tracked real-plan near-axis profile and stays idempotent', () => {
for (const file of ['real-plan-first-floor.json', 'real-plan-second-floor.json']) {
const { space } = JSON.parse(readFileSync(
new URL(`./fixtures/${file}`, import.meta.url), 'utf8',
));
const config = { spaces: [clone(space)] };
const before = nearAxisProfile(config);
const first = optimizePlans(config, {});
const after = nearAxisProfile(first.config);
const second = optimizePlans(first.config, first.layout);
assert.equal(after.total, before.total - first.report.wallsStraightened, file);
assert.equal(after.total <= before.total, true, file);
assert.equal(second.report.wallsStraightened, 0, file);
assert.equal(nearAxisProfile(second.config).total, after.total, file);
}
const { space } = JSON.parse(readFileSync(
new URL('./fixtures/real-plan-second-floor.json', import.meta.url), 'utf8',
));
const config = { spaces: [clone(space)] };
const before = nearAxisProfile(config);
const repaired = optimizePlans(config, {});
assert.equal(before.total, 1);
assert.equal(repaired.report.wallsStraightened, 1);
assert.equal(nearAxisProfile(repaired.config).total, 0);
});