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>
This commit is contained in:
Danila KhodjaefandCursor committed 2026-09-15 12:02:10 +03:00
1 parent 1b760faf0d
commit 13f6f088b6
25 files changed
+1147 -318

No files matched your search

@@ -0,0 +1,46 @@
/**
* Abstract Expressionism loft must read as an open painter's loft, not a barred cell.
* node --test server/__tests__/abex-loft-interior.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 STYLES = fs.readFileSync(path.join(ROOT, 'client/src/data/movement-interior-styles.ts'), 'utf8');
const DETAILS = fs.readFileSync(path.join(ROOT, 'client/src/components/hall-details/modernism.tsx'), 'utf8');
function loftStyleBlock() {
const start = STYLES.indexOf("'abstract-expressionist-loft'");
assert.ok(start >= 0, 'abstract-expressionist-loft style must exist');
const next = STYLES.indexOf('mk(', start + 1);
return STYLES.slice(start, next > start ? next : undefined);
}
function loftDetailsBlock() {
const start = DETAILS.indexOf('export function LoftDetails');
assert.ok(start >= 0, 'LoftDetails must exist');
const next = DETAILS.indexOf('export function ', start + 1);
return DETAILS.slice(start, next > start ? next : undefined);
}
describe('Abstract Expressionism loft — open studio, no prison vibe', () => {
it('does not use factory windows (iron-bar grid)', () => {
const block = loftStyleBlock();
assert.doesNotMatch(block, /style: 'factory'/);
assert.match(block, /style: 'picture'|style: 'clerestory'|style: 'skylight'/);
});
it('uses open painted / warm loft surfaces, not dingy cell brick-only', () => {
const block = loftStyleBlock();
assert.match(block, /north-light|painter|studio|loft/i);
assert.doesNotMatch(block, /north-light factory windows/);
});
it('LoftDetails has no cast-iron columns or sprinkler prison pipe', () => {
const block = loftDetailsBlock();
assert.doesNotMatch(block, /Cast-iron|Sprinkler main|cast-iron/);
assert.match(block, /timber|wood|joist|painter|studio/i);
});
});
+34
View File
@@ -0,0 +1,34 @@
/**
* Baroque upper-wall field panels must be framed velvet, not solid gilt slabs
* that wash out beside windows.
* node --test server/__tests__/baroque-panels.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 DETAILS = fs.readFileSync(path.join(ROOT, 'client/src/components/hall-details/courtly.tsx'), 'utf8');
function baroqueBlock() {
const start = DETAILS.indexOf('export function BaroqueStateDetails');
assert.ok(start >= 0, 'BaroqueStateDetails must exist');
const next = DETAILS.indexOf('export function ', start + 1);
return DETAILS.slice(start, next > start ? next : undefined);
}
describe('Baroque field panels beside windows', () => {
it('does not draw a solid gilt slab covering the velvet field', () => {
const block = baroqueBlock();
assert.doesNotMatch(block, /boxGeometry args=\{\[0\.04, 1\.12, w \+ 0\.12\]\}/);
assert.match(block, /velvet|field/i);
assert.match(block, /frame|rail|moulding/i);
});
it('keeps a generous clearance around window openings', () => {
const block = baroqueBlock();
assert.match(block, /halfClear = fieldW \/ 2 \+ rail \+ 0\.35/);
assert.match(block, /\(mid, halfClear\)/);
});
});
+54
View File
@@ -0,0 +1,54 @@
/**
* Dada movement hall must read as a modern cabaret, not a barred cell.
* node --test server/__tests__/dada-interior.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 STYLES = fs.readFileSync(path.join(ROOT, 'client/src/data/movement-interior-styles.ts'), 'utf8');
const WINDOWS = fs.readFileSync(path.join(ROOT, 'client/src/components/GalleryWindows.tsx'), 'utf8');
const DETAILS = fs.readFileSync(path.join(ROOT, 'client/src/components/hall-details/modernism.tsx'), 'utf8');
function dadaSalonBlock() {
const start = STYLES.indexOf("'dada-salon'");
assert.ok(start >= 0, 'dada-salon style must exist');
const next = STYLES.indexOf('mk(', start + 1);
return STYLES.slice(start, next > start ? next : undefined);
}
function dadaDetailsBlock() {
const start = DETAILS.indexOf('export function DadaDetails');
assert.ok(start >= 0, 'DadaDetails must exist');
const next = DETAILS.indexOf('export function ', start + 1);
return DETAILS.slice(start, next > start ? next : undefined);
}
describe('Dada interior — modern cabaret, no prison vibe', () => {
it('declares a picture window style (open glass, no iron grid)', () => {
assert.match(STYLES, /\| 'picture'/);
assert.match(WINDOWS, /style === 'picture'/);
});
it('does not use factory or gothic-lancet windows (barred / cell look)', () => {
const block = dadaSalonBlock();
assert.doesNotMatch(block, /style: 'factory'/);
assert.doesNotMatch(block, /style: 'gothic-lancet'/);
assert.match(block, /style: 'picture'/);
});
it('uses modern painted surfaces instead of dingy brick cell walls', () => {
const block = dadaSalonBlock();
assert.doesNotMatch(block, /wallSide: 'brick'/);
assert.match(block, /painted-flat|painted-emulsion/);
assert.match(block, /Modern|cabaret|collage/i);
});
it('DadaDetails has no service pipe / iron-bar geometry', () => {
const block = dadaDetailsBlock();
assert.doesNotMatch(block, /Service pipe|cylinderGeometry/);
assert.match(block, /collage|cabaret|panel/i);
});
});
@@ -0,0 +1,38 @@
/**
* Low wall mouldings must not run through exit doors on the end walls.
* node --test server/__tests__/doorway-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 PRIMITIVES = fs.readFileSync(path.join(ROOT, 'client/src/components/hall-details/primitives.tsx'), 'utf8');
const GEOMETRY = fs.readFileSync(path.join(ROOT, 'client/src/components/hall-details/geometry.ts'), 'utf8');
const COURTLY = fs.readFileSync(path.join(ROOT, 'client/src/components/hall-details/courtly.tsx'), 'utf8');
function flemishBlock() {
const start = COURTLY.indexOf('export function FlemishDetails');
assert.ok(start >= 0);
const next = COURTLY.indexOf('export function ', start + 1);
return COURTLY.slice(start, next > start ? next : undefined);
}
describe('Doorway clearance for wall mouldings', () => {
it('exports a shared door half-clearance used by WallBand', () => {
assert.match(GEOMETRY, /DOOR_CLEAR_HALF\s*=\s*1\.45/);
assert.match(PRIMITIVES, /DOOR_CLEAR_HALF/);
});
it('WallBand splits back/front bands that sit in the door height', () => {
assert.match(PRIMITIVES, /crossesDoor/);
assert.match(PRIMITIVES, /DOOR_CLEAR_HALF/);
assert.match(PRIMITIVES, /leftX|rightX/);
});
it('Flemish linenfold ribs skip the back-wall doorway', () => {
const block = flemishBlock();
assert.match(block, /DOOR_CLEAR_HALF|Math\.abs\(x\)\s*>/);
});
});
+57
View File
@@ -0,0 +1,57 @@
/**
* No movement hall floor tint may blow out as near-white.
* node --test server/__tests__/floor-brightness.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 STYLES = fs.readFileSync(path.join(ROOT, 'client/src/data/movement-interior-styles.ts'), 'utf8');
/** Soft ceiling — mid stone / warm wood, not gallery flood. */
const MAX_FLOOR_LUMINANCE = 0.58;
function luminance(hex) {
const n = parseInt(hex.slice(1), 16);
const r = (n >> 16) & 255;
const g = (n >> 8) & 255;
const b = n & 255;
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
}
function floorTints() {
// tints objects inside mk(): wall/ceiling/floor hex triples (optional wallSide)
const re =
/\{\s*wall:\s*'#[^']+',(?:\s*wallSide:\s*'#[^']+',)?\s*ceiling:\s*'#[^']+',\s*floor:\s*'(#[0-9a-fA-F]{6})'/g;
const out = [];
let m;
while ((m = re.exec(STYLES))) out.push(m[1]);
return out;
}
describe('Movement floors — no blown-out brightness', () => {
it('every authored floor tint stays at or below mid-stone luminance', () => {
const floors = floorTints();
assert.ok(floors.length >= 20, `expected many floor tints, got ${floors.length}`);
const offenders = floors
.map((hex) => ({ hex, L: luminance(hex) }))
.filter((f) => f.L > MAX_FLOOR_LUMINANCE);
assert.deepEqual(
offenders,
[],
offenders.map((f) => `${f.hex} L=${f.L.toFixed(3)}`).join(', ')
);
});
it('Baroque floor is subdued warm marble, not near-white', () => {
const start = STYLES.indexOf("'baroque-palace'");
assert.ok(start >= 0);
const block = STYLES.slice(start, STYLES.indexOf('mk(', start + 1));
const m = block.match(/ceiling:\s*'#[^']+',\s*floor:\s*'(#[0-9a-fA-F]{6})'/);
assert.ok(m);
assert.ok(luminance(m[1]) <= MAX_FLOOR_LUMINANCE);
assert.doesNotMatch(block, /floor: '#ece8e0'/);
});
});
@@ -0,0 +1,40 @@
/**
* High Renaissance (cinquecento) must not flood the hall with tiny contrast
* tiles or gold ring ornaments in every coffer.
* node --test server/__tests__/high-renaissance-interior.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 STYLES = fs.readFileSync(path.join(ROOT, 'client/src/data/movement-interior-styles.ts'), 'utf8');
const DETAILS = fs.readFileSync(path.join(ROOT, 'client/src/components/hall-details/classical.tsx'), 'utf8');
function styleBlock() {
const start = STYLES.indexOf("'high-renaissance-palazzo'");
assert.ok(start >= 0);
return STYLES.slice(start, STYLES.indexOf('mk(', start + 1));
}
function cinquecentoBlock() {
const start = DETAILS.indexOf('export function CinquecentoDetails');
assert.ok(start >= 0);
const next = DETAILS.indexOf('export function ', start + 1);
return DETAILS.slice(start, next > start ? next : DETAILS.length);
}
describe('High Renaissance interior — quieter surfaces', () => {
it('does not use high-contrast marble-checker flooring', () => {
const block = styleBlock();
assert.doesNotMatch(block, /floor: 'marble-checker'/);
assert.match(block, /floor: 'marble-veined-carrara'|floor: 'terrazzo'|floor: 'parquet/);
});
it('Cinquecento coffers have no gold rosette rings', () => {
const block = cinquecentoBlock();
assert.doesNotMatch(block, /rosette=\{trim\}/);
assert.match(block, /CofferedCeiling/);
});
});
@@ -0,0 +1,35 @@
/**
* Mannerist herm pilasters must read as clean architecture, not flat white strips
* with floating edge artifacts.
* node --test server/__tests__/mannerist-columns.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 DETAILS = fs.readFileSync(path.join(ROOT, 'client/src/components/hall-details/courtly.tsx'), 'utf8');
function manneristBlock() {
const start = DETAILS.indexOf('export function ManneristDetails');
assert.ok(start >= 0, 'ManneristDetails must exist');
const next = DETAILS.indexOf('export function ', start + 1);
return DETAILS.slice(start, next > start ? next : undefined);
}
describe('Mannerist herm columns', () => {
it('does not use the glitchy thin shaft + offset rustication + torus capital', () => {
const block = manneristBlock();
assert.doesNotMatch(block, /boxGeometry args=\{\[0\.09, 3\.8, 0\.3\]\}/);
assert.doesNotMatch(block, /position=\{\[side \* 0\.03, y, 0\]\}/);
assert.doesNotMatch(block, /torusGeometry args=\{\[0\.14, 0\.055/);
});
it('builds herm pilasters with clearance and a proper capital', () => {
const block = manneristBlock();
assert.match(block, /herm|pilaster/i);
assert.match(block, /clearLeft|clearRight|useWallClearance/);
assert.match(block, /capital|console|abacus/i);
});
});
@@ -0,0 +1,51 @@
/**
* Neoclassical museum floor must not blow out as near-white marble.
* node --test server/__tests__/neoclassical-interior.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 STYLES = fs.readFileSync(path.join(ROOT, 'client/src/data/movement-interior-styles.ts'), 'utf8');
function neoclassicalBlock() {
const start = STYLES.indexOf("'neoclassical-museum'");
assert.ok(start >= 0, 'neoclassical-museum style must exist');
const next = STYLES.indexOf('mk(', start + 1);
return STYLES.slice(start, next > start ? next : undefined);
}
function floorTintHex(block) {
const m = block.match(/floor:\s*'#[0-9a-fA-F]{6}'/);
assert.ok(m, 'floor tint hex must be present in tints object');
// Prefer the tints line: `{ wall: '...', ceiling: '...', floor: '#......', trim: '...' }`
const tints = block.match(/\{\s*wall:\s*'#[^']+',\s*ceiling:\s*'#[^']+',\s*floor:\s*'(#[0-9a-fA-F]{6})'/);
assert.ok(tints, 'tints.floor must be a hex color');
return tints[1];
}
function luminance(hex) {
const n = parseInt(hex.slice(1), 16);
const r = (n >> 16) & 255;
const g = (n >> 8) & 255;
const b = n & 255;
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
}
describe('Neoclassical interior — subdued stone floor', () => {
it('floor tint is mid stone grey, not near-white', () => {
const block = neoclassicalBlock();
const hex = floorTintHex(block);
assert.ok(luminance(hex) < 0.62, `floor luminance ${luminance(hex).toFixed(2)} for ${hex} is still too bright`);
assert.doesNotMatch(block, /floor: '#eceae6'/);
});
it('skylight is not overdriven for the marble floor', () => {
const block = neoclassicalBlock();
const sky = block.match(/style: 'skylight',\s*lightColor: '[^']+',\s*lightIntensity:\s*([0-9.]+)/);
assert.ok(sky, 'skylight must exist');
assert.ok(Number(sky[1]) <= 3.6, `skylight intensity ${sky[1]} is still too strong`);
});
});
@@ -0,0 +1,34 @@
/**
* No movement hall may use factory (iron-bar grid) windows — they read as prison cells.
* node --test server/__tests__/no-factory-windows.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 STYLES = fs.readFileSync(path.join(ROOT, 'client/src/data/movement-interior-styles.ts'), 'utf8');
function styleBlock(id) {
const start = STYLES.indexOf(`'${id}'`);
assert.ok(start >= 0, `${id} must exist`);
const next = STYLES.indexOf('mk(', start + 1);
return STYLES.slice(start, next > start ? next : undefined);
}
describe('Movement interiors — no factory / iron-bar windows', () => {
it('no authored movement style uses factory windows', () => {
// Only count style: 'factory' assignments inside BY_MOVEMENT_ID mk() blocks,
// not the GalleryWindowStyle union member.
const uses = [...STYLES.matchAll(/style:\s*'factory'/g)];
assert.equal(uses.length, 0, `found ${uses.length} factory window assignment(s)`);
});
it('Futurism uses open picture or clerestory daylight, not bars', () => {
const block = styleBlock('futurist-loft');
assert.doesNotMatch(block, /style: 'factory'/);
assert.match(block, /style: 'picture'|style: 'ribbon'|style: 'clerestory'|style: 'skylight'/);
assert.match(block, /open|ribbon|picture|glass/i);
});
});
@@ -0,0 +1,98 @@
/**
* 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`);
}
}
});
});
@@ -0,0 +1,35 @@
/**
* Surrealist hall must not place a freestanding decorative door in a corner.
* node --test server/__tests__/surrealist-interior.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 DETAILS = fs.readFileSync(path.join(ROOT, 'client/src/components/hall-details/modernism.tsx'), 'utf8');
const COURTLY = fs.readFileSync(path.join(ROOT, 'client/src/components/hall-details/courtly.tsx'), 'utf8');
const CLASSICAL = fs.readFileSync(path.join(ROOT, 'client/src/components/hall-details/classical.tsx'), 'utf8');
const NINETEENTH = fs.readFileSync(path.join(ROOT, 'client/src/components/hall-details/nineteenth.tsx'), 'utf8');
function surrealistBlock() {
const start = DETAILS.indexOf('export function SurrealistDetails');
assert.ok(start >= 0, 'SurrealistDetails must exist');
const next = DETAILS.indexOf('export function ', start + 1);
return DETAILS.slice(start, next > start ? next : undefined);
}
describe('Surrealist interior — no corner door', () => {
it('does not place a freestanding door in the corner', () => {
const block = surrealistBlock();
assert.doesNotMatch(block, /door that leads nowhere|standing in the corner pocket/i);
assert.doesNotMatch(block, /boxGeometry args=\{\[0\.72, 2\.1/);
assert.doesNotMatch(block, /boxGeometry args=\{\[0\.84, 2\.24/);
});
it('no other period module places a freestanding corner door prop', () => {
const sources = [DETAILS, COURTLY, CLASSICAL, NINETEENTH].join('\n');
assert.doesNotMatch(sources, /door that leads nowhere|leads nowhere|false door|freestanding door/i);
});
});