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:
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);
|
||||
});
|
||||
});
|
||||
@@ -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\)/);
|
||||
});
|
||||
});
|
||||
@@ -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*>/);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user