mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 12:18:51 +00:00
fix: make plan visuals grid-scale invariant
Issue: #239 User-Visible: yes
This commit is contained in:
@@ -79,6 +79,24 @@ test('icon size: a percentage of the PLAN, exactly as before the canvas', () =>
|
||||
assert.equal(iconCqw(2.5, small, 1000, 2), 2 * iconCqw(2.5, small, 1000));
|
||||
});
|
||||
|
||||
test('icon size is invariant for physically equivalent plans at different grid scales', () => {
|
||||
const coarse = model({
|
||||
id: 'coarse', cell_cm: 5,
|
||||
rooms: [{ id: 'r', poly: [[0.1, 0.1], [0.9, 0.1], [0.9, 0.88], [0.1, 0.88]] }],
|
||||
});
|
||||
const fine = model({
|
||||
id: 'fine', cell_cm: 1, view_box: [0, 0, 5, 5],
|
||||
rooms: [{ id: 'r', poly: [[0.5, 0.5], [4.5, 0.5], [4.5, 4.4], [0.5, 4.4]] }],
|
||||
});
|
||||
assert.equal(iconUnit(coarse), NORM_W);
|
||||
assert.equal(iconUnit(fine), 5 * NORM_W);
|
||||
assert.equal(iconCqw(2.5, coarse, spaceFrame(coarse).w), iconCqw(2.5, fine, spaceFrame(fine).w));
|
||||
});
|
||||
|
||||
test('a legacy space without cell_cm keeps the historical five-centimetre read fallback', () => {
|
||||
assert.equal(model({ id: 'legacy' }).cellCm, 5);
|
||||
});
|
||||
|
||||
test('icon size: a plan drawn past the old square keeps its markers', () => {
|
||||
// rooms at 1.0..3.0 — 2 canvases wide, framed by ~2.2 canvases once the
|
||||
// frame is the content. With the old fixed NORM_W numerator the marker
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
GRID_IMPERIAL_CELL_CM,
|
||||
GRID_VISUAL_REFERENCE_CELL_CM,
|
||||
gridCellFieldToCm,
|
||||
gridCellFieldValue,
|
||||
gridVisualScale,
|
||||
gridVisualUnits,
|
||||
newSpaceCellCm,
|
||||
} from '../test-build/grid-scale.js';
|
||||
|
||||
test('visual scale preserves the 5 cm reference and follows physical equivalence', () => {
|
||||
assert.equal(GRID_VISUAL_REFERENCE_CELL_CM, 5);
|
||||
assert.equal(gridVisualScale(5), 1);
|
||||
for (const cellCm of [1, 2.54, 10, 25]) {
|
||||
assert.equal(gridVisualScale(cellCm), 5 / cellCm);
|
||||
assert.equal(gridVisualUnits(2.5, cellCm), 2.5 * (5 / cellCm));
|
||||
}
|
||||
});
|
||||
|
||||
test('invalid visual-scale inputs retain the legacy 5 cm appearance', () => {
|
||||
for (const value of [NaN, Infinity, -Infinity, 0, -1, undefined, null, '', '1', 'not-a-number']) {
|
||||
assert.equal(gridVisualScale(value), 1);
|
||||
}
|
||||
});
|
||||
|
||||
test('new-space defaults use one centimetre or exactly one inch', () => {
|
||||
assert.equal(newSpaceCellCm(false), 1);
|
||||
assert.equal(newSpaceCellCm(true), GRID_IMPERIAL_CELL_CM);
|
||||
assert.equal(GRID_IMPERIAL_CELL_CM, 2.54);
|
||||
});
|
||||
|
||||
test('imperial field projection is readable while canonical values stay available', () => {
|
||||
assert.equal(gridCellFieldValue(2.54, true), '1');
|
||||
assert.equal(gridCellFieldValue(1, false), '1');
|
||||
assert.equal(gridCellFieldValue(5, true), '1.968504');
|
||||
assert.equal(gridCellFieldToCm(1, true), 2.54);
|
||||
assert.equal(gridCellFieldToCm(1.25, true), 3.175);
|
||||
assert.equal(gridCellFieldToCm(1.25, false), 1.25);
|
||||
});
|
||||
@@ -41,13 +41,14 @@ test('all current floor/live layers remain in the one main scene', () => {
|
||||
test('structural cache includes opening flips and excludes live HA amount', () => {
|
||||
assert.match(card, /flipH: !!opening\.flip_h/);
|
||||
assert.match(card, /flipV: !!opening\.flip_v/);
|
||||
assert.match(card, /floorEdgeHeight: ISO_FLOOR_EDGE_HEIGHT, algorithm: 3/);
|
||||
assert.match(card, /const floorEdgeHeight = gridVisualUnits\(ISO_FLOOR_EDGE_HEIGHT, this\._cellCm\)/);
|
||||
assert.match(card, /floorEdgeHeight, algorithm: 3/);
|
||||
const source = card.slice(card.indexOf('private _isoSource()'), card.indexOf('private _isoSceneKey()'));
|
||||
assert.doesNotMatch(source, /_openingAmt|openingAmount|\.hass|matchMedia|CSS\.supports|theme|hover/);
|
||||
assert.match(card, /projectIsoOpening\(basis, this\._openingAmt\(opening\)\)/);
|
||||
assert.match(card, /isoLayers && !isoLayers\.floorSymbols/);
|
||||
assert.match(card, /if \(!this\._spaceDisplayForRender\(\)\.showBorders\)/);
|
||||
assert.match(card, /projectedFrame\(\{ rect: flat, wallHeight: ISO_WALL_HEIGHT \}\)/);
|
||||
assert.match(card, /wallHeight: gridVisualUnits\(ISO_WALL_HEIGHT, this\._cellCm\)/);
|
||||
assert.match(card, /viewBox=\$\{isoLayers\?\.structural[\s\S]*?: `\$\{floorView\.x\}/);
|
||||
assert.match(card, /preserveAspectRatio=\$\{isoLayers\?\.structural \|\| !iso \? 'xMidYMid meet' : 'none'\}/);
|
||||
assert.match(card, /transform=\$\{isoLayers\?\.structural \? isoFloorMatrixCss\(\) : nothing\}/);
|
||||
|
||||
@@ -56,7 +56,7 @@ test('placement preview adds passage-only cut geometry without changing saved sy
|
||||
const start = card.indexOf('private _renderOpeningPlacementPreview()');
|
||||
const preview = card.slice(start, card.indexOf('private _renderOpenings(', start));
|
||||
assert.match(preview, /candidate\.type === 'passage'/);
|
||||
assert.match(preview, /passagePlacementPreviewGeometry\(candidate, this\._gridPitch\)/);
|
||||
assert.match(preview, /passagePlacementPreviewGeometry\([\s\S]*gridVisualUnits\(this\._gridPitch, this\._cellCm\)/);
|
||||
assert.equal((preview.match(/class="passage-preview-cut"/g) || []).length, 1);
|
||||
assert.equal((preview.match(/class="passage-preview-boundary"/g) || []).length, 1,
|
||||
'one mapped template emits exactly two resolved boundary records');
|
||||
|
||||
@@ -25,13 +25,25 @@ const templateText = (value) => {
|
||||
|
||||
test('opening metrics expand hit and outline zones with a thick wall face', () => {
|
||||
assert.deepEqual(openingVisibleMetrics(spec()), {
|
||||
half: 50, jambHalf: 4, gateDepth: 0, outlineHalf: 16, hitHalf: 20,
|
||||
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: 28, hitHalf: 30,
|
||||
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 },
|
||||
|
||||
Reference in New Issue
Block a user