Files
houseplan-card/test/opening-symbol.test.mjs
T
2026-08-23 01:59:26 +03:00

130 lines
5.2 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
openingVisibleMetrics,
renderOpeningVisibleGeometry,
} from '../test-build/render/opening-symbol.js';
const spec = (patch = {}) => ({
type: 'door', length: 100, angle: 0, amount: 0,
flipH: false, flipV: false, base: '#000', tone: '#000',
cellCm: 10, gridPitch: 20,
face: { ox: 0, oy: 0, cm: 0, side: -1 },
...patch,
});
const templateText = (value) => {
if (value == null || value === false) return '';
if (typeof value === 'string' || typeof value === 'number') return String(value);
if (Array.isArray(value)) return value.map(templateText).join('');
if (!value.strings) return '';
return value.strings.reduce((text, part, index) => (
text + part + (index < value.values.length ? templateText(value.values[index]) : '')
), '');
};
test('opening metrics expand hit and outline zones with a thick wall face', () => {
assert.deepEqual(openingVisibleMetrics(spec()), {
half: 50, jambHalf: 2, gateDepth: 0, outlineHalf: 8, hitHalf: 10,
});
assert.deepEqual(openingVisibleMetrics(spec({ face: { ox: 0, oy: 20, cm: 20, side: 1 } })), {
half: 50, jambHalf: 20, gateDepth: 0, outlineHalf: 24, hitHalf: 25,
});
});
test('equivalent grid scales preserve visual padding without double-scaling physical jambs', () => {
const reference = openingVisibleMetrics(spec({
length: 100, cellCm: 5, face: { ox: 0, oy: 0, cm: 20, side: 1 },
}));
const detailed = openingVisibleMetrics(spec({
length: 500, cellCm: 1, face: { ox: 0, oy: 0, cm: 20, side: 1 },
}));
assert.equal(detailed.jambHalf, reference.jambHalf * 5);
assert.equal(detailed.outlineHalf, reference.outlineHalf * 5);
assert.equal(detailed.hitHalf, reference.hitHalf * 5);
});
test('shared renderer emits the expected visible symbol for every opening type', () => {
const windowText = templateText(renderOpeningVisibleGeometry(spec({
type: 'window', face: { ox: 0, oy: 20, cm: 20, side: 1 },
})));
const doorText = templateText(renderOpeningVisibleGeometry(spec({ type: 'door', amount: 1 })));
const gateText = templateText(renderOpeningVisibleGeometry(spec({ type: 'gate' })));
assert.match(windowText, /op-glass/);
assert.match(doorText, /op-leaf/);
assert.match(doorText, /op-arc/);
assert.match(doorText, /A 100 100/);
assert.match(doorText, /rotate\(-90deg\)/);
assert.equal((gateText.match(/op-leaf/g) || []).length, 2);
assert.ok(openingVisibleMetrics(spec({ type: 'gate' })).gateDepth > 0);
});
test('an open passage emits no visible symbol at all', () => {
const passageText = templateText(renderOpeningVisibleGeometry(spec({
type: 'passage', amount: 1, flipH: true, flipV: true,
face: { ox: 0, oy: 20, cm: 20, side: 1 },
})));
assert.equal(passageText, '');
});
test('shared renderer centres every flip while preserving opening direction', () => {
const centred = templateText(renderOpeningVisibleGeometry(spec({
type: 'window', amount: 0.5,
face: { ox: 0, oy: -20, cm: 20, side: -1 },
})));
assert.match(centred, /translate\(0 0\)/);
const flippedPositive = templateText(renderOpeningVisibleGeometry(spec({
type: 'window', amount: 0.5, flipH: true, flipV: true,
face: { ox: 0, oy: 20, cm: 20, side: 1 },
})));
const flippedNegative = templateText(renderOpeningVisibleGeometry(spec({
type: 'window', amount: 0.5, flipH: true, flipV: true,
face: { ox: 0, oy: -20, cm: 20, side: -1 },
})));
for (const text of [flippedPositive, flippedNegative]) {
assert.match(text, /scale\(-1 -1\)/);
assert.match(text, /translate\(0 0\)/);
assert.match(text, /rotate\(45deg\)/);
assert.match(text, /stroke-dashoffset="39\.269/);
assert.match(text, /op-glass/);
}
const doorDefault = templateText(renderOpeningVisibleGeometry(spec({
type: 'door', amount: 1, flipV: false,
face: { ox: 0, oy: 20, cm: 20, side: 1 },
})));
const doorFlipped = templateText(renderOpeningVisibleGeometry(spec({
type: 'door', amount: 1, flipV: true,
face: { ox: 0, oy: -20, cm: 20, side: -1 },
})));
assert.match(doorDefault, /scale\(1 1\)/);
assert.match(doorFlipped, /scale\(1 -1\)/);
assert.match(doorDefault, /translate\(0 0\)/);
assert.match(doorFlipped, /translate\(0 0\)/);
const gate = templateText(renderOpeningVisibleGeometry(spec({
type: 'gate', amount: 1, face: { ox: 0, oy: 20, cm: 20, side: 1 },
})));
const flippedGate = templateText(renderOpeningVisibleGeometry(spec({
type: 'gate', amount: 1, flipV: true,
face: { ox: 0, oy: -20, cm: 20, side: -1 },
})));
assert.match(gate, /translate\(0 0\)/);
assert.match(flippedGate, /translate\(0 0\)/);
assert.match(gate, /scale\(1 1\)/);
assert.match(flippedGate, /scale\(1 1\)/);
assert.match(gate, /rotate\(10deg\)/);
assert.match(gate, /rotate\(-10deg\)/);
assert.match(flippedGate, /rotate\(-10deg\)/);
assert.match(flippedGate, /rotate\(10deg\)/);
assert.equal(
gate.indexOf('rotate(10deg)') < gate.indexOf('rotate(-10deg)'), true,
'default gate turns its first leaf towards the resolved face',
);
assert.equal(
flippedGate.indexOf('rotate(-10deg)') < flippedGate.indexOf('rotate(10deg)'), true,
'flip_v reverses the first-leaf turn instead of cancelling in scaleY',
);
});