mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
Owner's batch: - zoom now opens on what is DRAWN (rooms + 5% margin) for spaces with no background image; with one the image is the plan and still fits whole. A small plan on the square canvas no longer opens as a speck. - swiping between spaces, and the kiosk carousel, slide sideways; honours prefers-reduced-motion. - the room settings button reads 'Room settings' and lightens on hover. - 'curation' is filtering everywhere: UI strings, docs, code. Checked the yard while I was there: its drawing sits off-centre because it was drawn that way — before the migration x spanned 0.12..0.54 with 0.12 and 0.46 of margin. The migration added 0.1465 on each side, symmetrically. Content-fit zoom makes it moot anyway. From the v1.47.0 review: - HP-1470-02: the picker let you delete the plan you had just selected — it is not in the stored config yet, so the server rightly called it free, and the save then stored a url with no file. The button is disabled, and since two clients can do this in either order, config/set now verifies every internal plan url against the disk under the write lock and answers . External and legacy urls are not ours to police. - HP-1470-01: growth is bounded at the door rather than by deleting old files — that mistake cost real plans twice. check_quota refuses an upload that would push the store past 256 MB / 200 plans (1 GB / 1000 attachments) or leave less than 512 MB free. The plan list is capped at 60 newest with a total, and thumbnails load lazily. - HP-1470-03: picking a saved plan waited for nothing and stored a fallback ratio when the signature had not arrived — a square plan came out stretched. It waits for the signature, binds the result to the dialog that asked, and the dialog preview is signed too. - report §5: the last lifecycle comments still described age-based collection. Not released yet — the owner asked for a release once the batch is done.
109 lines
4.8 KiB
JavaScript
109 lines
4.8 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
NORM_W, spaceModels, roomBounds, roomCenter, defaultPositions, markerPos, labelPos, fitInSquare, contentBounds,
|
|
} from '../test-build/space-geometry.js';
|
|
|
|
const cfg = {
|
|
spaces: [{
|
|
id: 'f1', title: '1st', plan_aspect: 2, plan_url: '/plans/f1.svg', view_box: [0, 0, 1, 1],
|
|
rooms: [{ id: 'r1', name: 'Room', area: 'a1', poly: [[0.1, 0.1], [0.5, 0.1], [0.5, 0.5], [0.1, 0.5]] }],
|
|
}, {
|
|
id: 'yard', title: 'Yard', view_box: [0, 0, 1, 1], rooms: [],
|
|
}],
|
|
markers: [], settings: {},
|
|
};
|
|
|
|
test('spaceModels: the canvas is square; the image is centred by its own ratio', () => {
|
|
const m = spaceModels(cfg);
|
|
assert.equal(m.length, 2);
|
|
const f1 = m[0];
|
|
assert.deepEqual(f1.vb, [0, 0, 1000, 1000]);
|
|
assert.equal(f1.bg.href, '/plans/f1.svg');
|
|
// a plan twice as wide as it is tall: full width, half height, margins above
|
|
// and below — the canvas has no proportions of its own any more (v1.48.0)
|
|
assert.deepEqual(f1.bg, { href: '/plans/f1.svg', x: 0, y: 250, w: 1000, h: 500 });
|
|
assert.deepEqual(f1.rooms[0].poly, [[100, 100], [500, 100], [500, 500], [100, 500]]);
|
|
assert.equal(m[1].bg, null); // no plan_url
|
|
assert.equal(spaceModels(null).length, 0);
|
|
});
|
|
|
|
test('fitInSquare: wide gets top/bottom margins, tall gets side margins', () => {
|
|
assert.deepEqual(fitInSquare(2, 1000), { x: 0, y: 250, w: 1000, h: 500 });
|
|
assert.deepEqual(fitInSquare(0.5, 1000), { x: 250, y: 0, w: 500, h: 1000 });
|
|
assert.deepEqual(fitInSquare(1, 1000), { x: 0, y: 0, w: 1000, h: 1000 });
|
|
// unknown ratio (image not loaded yet, old config): assume square
|
|
for (const bad of [null, undefined, 0, -3, NaN, 'x']) {
|
|
assert.deepEqual(fitInSquare(bad, 1000), { x: 0, y: 0, w: 1000, h: 1000 });
|
|
}
|
|
});
|
|
|
|
test('roomBounds + roomCenter for a polygon', () => {
|
|
const r = spaceModels(cfg)[0].rooms[0];
|
|
assert.deepEqual(roomBounds(r), { x: 100, y: 100, w: 400, h: 400 });
|
|
assert.deepEqual(roomCenter(r), [300, 300]);
|
|
});
|
|
|
|
test('markerPos: saved layout → default grid → space centre', () => {
|
|
const model = spaceModels(cfg)[0];
|
|
const dev = { id: 'd1', space: 'f1', area: 'a1', entities: [] };
|
|
// saved layout (normalized) → render units
|
|
assert.deepEqual(
|
|
markerPos(dev, { d1: { s: 'f1', x: 0.2, y: 0.3 } }, cfg, {}, model),
|
|
{ x: 200, y: 300 }, // 0.2*1000, 0.3*1000
|
|
);
|
|
// default grid position (inside the room)
|
|
const defPos = defaultPositions([dev], model, 2.5);
|
|
assert.ok(defPos.d1);
|
|
assert.deepEqual(markerPos(dev, {}, cfg, defPos, model), defPos.d1);
|
|
const b = roomBounds(model.rooms[0]);
|
|
assert.ok(defPos.d1.x >= b.x && defPos.d1.x <= b.x + b.w && defPos.d1.y >= b.y && defPos.d1.y <= b.y + b.h);
|
|
// no layout, no defPos → space centre
|
|
assert.deepEqual(markerPos(dev, {}, cfg, {}, model), { x: 500, y: 500 });
|
|
});
|
|
|
|
test('labelPos: saved rl_<id> → render units; else room centre', () => {
|
|
const model = spaceModels(cfg)[0];
|
|
const r = model.rooms[0];
|
|
assert.deepEqual(labelPos(r, 'f1', { rl_r1: { s: 'f1', x: 0.3, y: 0.4 } }, cfg), { x: 300, y: 400 });
|
|
assert.deepEqual(labelPos(r, 'f1', {}, cfg), { x: 300, y: 300 }); // room centre
|
|
});
|
|
|
|
test('defaultPositions: several devices in one room are spread (declumped, distinct)', () => {
|
|
const model = spaceModels(cfg)[0];
|
|
const devs = [0, 1, 2, 3].map((i) => ({ id: 'd' + i, space: 'f1', area: 'a1', entities: [] }));
|
|
const pos = defaultPositions(devs, model, 2.5);
|
|
assert.equal(Object.keys(pos).length, 4);
|
|
const keys = Object.keys(pos);
|
|
for (let i = 0; i < keys.length; i++)
|
|
for (let j = i + 1; j < keys.length; j++) {
|
|
const a = pos[keys[i]], b = pos[keys[j]];
|
|
assert.ok(Math.hypot(a.x - b.x, a.y - b.y) > 1, 'positions distinct');
|
|
}
|
|
});
|
|
|
|
test('NORM_W is 1000', () => assert.equal(NORM_W, 1000));
|
|
|
|
test('contentBounds: fits what is drawn, with a 5% margin', () => {
|
|
const one = spaceModels({ spaces: [{
|
|
id: 's', view_box: [0, 0, 1, 1],
|
|
rooms: [{ id: 'r', poly: [[0.4, 0.4], [0.6, 0.4], [0.6, 0.6], [0.4, 0.6]] }],
|
|
}], markers: [] })[0];
|
|
// 200x200 render units in the middle of a 1000x1000 canvas, +5% of 200 each side
|
|
assert.deepEqual(contentBounds(one), { x: 390, y: 390, w: 220, h: 220 });
|
|
|
|
// rectangles count too, and the margin follows the LARGER side
|
|
const rect = spaceModels({ spaces: [{
|
|
id: 's', view_box: [0, 0, 1, 1],
|
|
rooms: [{ id: 'r', x: 0.1, y: 0.4, w: 0.6, h: 0.1 }],
|
|
}], markers: [] })[0];
|
|
const b = contentBounds(rect);
|
|
assert.equal(Math.round(b.w), 660); // 600 + 2 * 5% of 600
|
|
assert.equal(Math.round(b.h), 160); // 100 + the same absolute margin
|
|
assert.equal(Math.round(b.x), 70);
|
|
|
|
// nothing drawn → the caller keeps the whole canvas
|
|
const empty = spaceModels({ spaces: [{ id: 's', view_box: [0, 0, 1, 1], rooms: [] }], markers: [] })[0];
|
|
assert.equal(contentBounds(empty), null);
|
|
});
|