Files
Art-gallery/server/__tests__/painting-column-clearance.test.js
Danila KhodjaefandCursor 13f6f088b6 Polish movement hall interiors: clear doors and bay columns, tone floors and windows.
Document hang keep-outs and period-module fixes; leave columns fixed while packing frames into gaps.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 12:02:10 +03:00

99 lines
4.1 KiB
JavaScript

/**
* Side-wall paintings must not hang in front of bay columns/pilasters.
* Columns stay fixed; the hang skips column keep-outs (and windows must too).
* node --test server/__tests__/painting-column-clearance.test.js
*/
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.join(__dirname, '..', '..');
const KEEP = fs.readFileSync(path.join(ROOT, 'client/src/utils/sideColumnKeepOuts.ts'), 'utf8');
const LAYOUT = fs.readFileSync(path.join(ROOT, 'client/src/utils/movementHallLayout.ts'), 'utf8');
const GEOMETRY = fs.readFileSync(path.join(ROOT, 'client/src/components/hall-details/geometry.ts'), 'utf8');
const GALLERY = fs.readFileSync(path.join(ROOT, 'client/src/components/VirtualGallery.tsx'), 'utf8');
/** Mirror of bayPositions — must stay in lockstep with sideColumnKeepOuts.ts / geometry.ts */
function bayPositions(depth, halfD, spacing = 4.2, max = 9) {
const count = Math.max(2, Math.min(max, Math.floor(depth / spacing)));
const step = depth / (count + 1);
return Array.from({ length: count }, (_, i) => -halfD + step * (i + 1));
}
function parseRhythmTable(src) {
const start = src.indexOf('SIDE_WALL_COLUMN_RHYTHMS');
assert.ok(start >= 0, 'SIDE_WALL_COLUMN_RHYTHMS export missing');
const bodyStart = src.indexOf('{', start);
const bodyEnd = src.indexOf('} as const', bodyStart);
assert.ok(bodyEnd > bodyStart);
const body = src.slice(bodyStart, bodyEnd + 1);
const rhythms = {};
for (const m of body.matchAll(
/['"]?([a-z0-9-]+)['"]?:\s*\{\s*spacing:\s*([\d.]+),\s*max:\s*(\d+),\s*halfClear:\s*([\d.]+)\s*\}/g
)) {
rhythms[m[1]] = { spacing: Number(m[2]), max: Number(m[3]), halfClear: Number(m[4]) };
}
return rhythms;
}
describe('Painting ↔ column clearance', () => {
it('exports bayPositions shared by geometry and keep-outs', () => {
assert.match(KEEP, /export function bayPositions/);
assert.match(GEOMETRY, /bayPositions/);
assert.match(GEOMETRY, /useBayZ[\s\S]*bayPositions/);
});
it('registers keep-outs for every mid-wall bay column style', () => {
const rhythms = parseRhythmTable(KEEP);
for (const kind of [
'gothic',
'roman',
'quattrocento',
'cinquecento',
'mannerist',
'neoclassical',
'gothic-revival',
'art-nouveau',
]) {
assert.ok(rhythms[kind], `missing rhythm for ${kind}`);
assert.ok(rhythms[kind].halfClear >= 0.18, `${kind} halfClear too small`);
}
});
it('layout packs side walls around column keep-outs and grows depth if needed', () => {
assert.match(LAYOUT, /sideColumnKeepOutIntervals/);
assert.match(LAYOUT, /depthForSideHang|sideWallGaps/);
assert.match(LAYOUT, /details\?:\s*MovementDetailKind/);
assert.match(LAYOUT, /buildAllMovementHallLayouts\(\s*paintings:\s*Painting\[\],\s*details\?/);
});
it('windows treat column keep-outs as occupied so they do not land on columns', () => {
const fn = LAYOUT.slice(LAYOUT.indexOf('export function computeSideWallWindows'));
assert.match(fn, /sideColumnKeepOutIntervals/);
assert.match(fn, /columnKeepOuts/);
});
it('VirtualGallery passes the movement detail kind into hall layout', () => {
assert.match(GALLERY, /buildAllMovementHallLayouts\(\s*paintings\s*,\s*interiorStyle\?\.details\s*\)/);
});
it('bay keep-out intervals cover each column centre for a typical hall depth', () => {
const rhythms = parseRhythmTable(KEEP);
const depth = 14;
const halfD = depth / 2;
for (const [kind, r] of Object.entries(rhythms)) {
const zs = bayPositions(depth, halfD, r.spacing, r.max);
assert.ok(zs.length >= 2, kind);
for (const z of zs) {
const lo = z - r.halfClear;
const hi = z + r.halfClear;
assert.ok(lo < z && z < hi, `${kind} keep-out must contain bay centre`);
// A typical frame half-width centred on the column would collide
const halfOuter = 0.35;
assert.ok(z - halfOuter < hi && z + halfOuter > lo, `${kind} frame would overlap column`);
}
}
});
});