mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 04:09:17 +00:00
test: prove coincident partition acceptance
Issue: #276 User-Visible: no
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,84 @@
|
||||
// #276: same-process incremental cost of coincident-partition reconciliation
|
||||
// inside the complete Optimize candidate pass on the deterministic large house.
|
||||
import { performance } from 'node:perf_hooks';
|
||||
|
||||
import { makeLargeHouseFixture, LARGE_HOUSE_COUNTS } from './fixtures/large-house.mjs';
|
||||
import { optimizePlans } from '../test-build/plan-optimizer.js';
|
||||
|
||||
const WARMUPS = 5;
|
||||
const SAMPLES = 40;
|
||||
const RELATIVE_OVERHEAD = 0.15;
|
||||
const ABSOLUTE_OVERHEAD_MS = 25;
|
||||
|
||||
const fixture = makeLargeHouseFixture();
|
||||
const emptyReconciliation = (rawSpace, _model, walls) => ({
|
||||
walls: walls || [],
|
||||
partitions: Array.isArray(rawSpace?.partitions) ? rawSpace.partitions : [],
|
||||
openings: Array.isArray(rawSpace?.openings) ? rawSpace.openings : [],
|
||||
partitionsReconciled: 0,
|
||||
openingsRehosted: 0,
|
||||
});
|
||||
const baseline = () => optimizePlans(fixture.config, {}, {}, {
|
||||
reconcileCoincidentPartitions: emptyReconciliation,
|
||||
});
|
||||
const candidate = () => optimizePlans(fixture.config, {});
|
||||
const timed = (operation) => {
|
||||
const start = performance.now();
|
||||
const result = operation();
|
||||
if (!result || !result.report) throw new Error('Optimize candidate returned no report');
|
||||
return performance.now() - start;
|
||||
};
|
||||
for (let index = 0; index < WARMUPS; index++) {
|
||||
baseline();
|
||||
candidate();
|
||||
}
|
||||
|
||||
const baselineTimes = [];
|
||||
const candidateTimes = [];
|
||||
const overheadTimes = [];
|
||||
for (let index = 0; index < SAMPLES; index++) {
|
||||
const candidateFirst = index % 2 === 1;
|
||||
const first = timed(candidateFirst ? candidate : baseline);
|
||||
const second = timed(candidateFirst ? baseline : candidate);
|
||||
const baselineMs = candidateFirst ? second : first;
|
||||
const candidateMs = candidateFirst ? first : second;
|
||||
baselineTimes.push(baselineMs);
|
||||
candidateTimes.push(candidateMs);
|
||||
overheadTimes.push(candidateMs - baselineMs);
|
||||
}
|
||||
|
||||
const quantile = (values, ratio) => {
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * ratio) - 1)];
|
||||
};
|
||||
const summary = (values) => ({
|
||||
min: Math.min(...values),
|
||||
median: quantile(values, 0.5),
|
||||
p95: quantile(values, 0.95),
|
||||
max: Math.max(...values),
|
||||
});
|
||||
const baselineSummary = summary(baselineTimes);
|
||||
const candidateSummary = summary(candidateTimes);
|
||||
const overheadSummary = summary(overheadTimes);
|
||||
const measuredOverheadP95 = Math.max(0, candidateSummary.p95 - baselineSummary.p95);
|
||||
const relativeP95 = measuredOverheadP95
|
||||
/ Math.max(baselineSummary.p95, Number.EPSILON);
|
||||
const pass = measuredOverheadP95 <= ABSOLUTE_OVERHEAD_MS
|
||||
&& relativeP95 <= RELATIVE_OVERHEAD;
|
||||
const report = {
|
||||
issue: 276,
|
||||
fixture: LARGE_HOUSE_COUNTS,
|
||||
warmups: WARMUPS,
|
||||
samples: SAMPLES,
|
||||
baseline: baselineSummary,
|
||||
candidate: candidateSummary,
|
||||
pairedOverhead: overheadSummary,
|
||||
budgets: {
|
||||
relativeOverhead: RELATIVE_OVERHEAD,
|
||||
absoluteOverheadMs: ABSOLUTE_OVERHEAD_MS,
|
||||
},
|
||||
measured: { overheadP95Ms: measuredOverheadP95, relativeP95 },
|
||||
pass,
|
||||
};
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
if (!pass) process.exitCode = 1;
|
||||
@@ -14,6 +14,9 @@ const orthogonalStripFixture = JSON.parse(readFileSync(
|
||||
const wallKeyRoundtripFixture = JSON.parse(readFileSync(
|
||||
new URL('../../test/fixtures/258-wall-key-roundtrip.json', import.meta.url), 'utf8',
|
||||
));
|
||||
const coincidentPartitionFixture = JSON.parse(readFileSync(
|
||||
new URL('../../test/fixtures/276-coincident-partition.json', import.meta.url), 'utf8',
|
||||
));
|
||||
|
||||
const fixtureFor = (scenario) => scenario.fixture === 'large'
|
||||
? makeLargeHouseFixture()
|
||||
@@ -67,6 +70,33 @@ async function stableEnvironment(page, scenario) {
|
||||
/** Apply every data-only scenario override before the fixture crosses into the browser. */
|
||||
export function prepareGoldenFixture(scenario) {
|
||||
const fixture = fixtureFor(scenario);
|
||||
if (scenario.coincidentPartition) {
|
||||
const state = scenario.coincidentPartition;
|
||||
if (!['before', 'thin', 'thick', 'virtual'].includes(state))
|
||||
throw new Error(`unknown coincidentPartition state: ${state}`);
|
||||
const space = structuredClone(coincidentPartitionFixture.spaces[0]);
|
||||
space.id = scenario.space;
|
||||
space.title = `Coincident partition ${state}`;
|
||||
space.settings = {
|
||||
fill_mode: 'none', show_borders: true, show_names: true,
|
||||
};
|
||||
if (state !== 'before') {
|
||||
delete space.partitions;
|
||||
space.walls[0].cm = state === 'thin' ? 10 : state === 'thick' ? 30 : 20;
|
||||
const opening = space.openings[0];
|
||||
delete opening.host;
|
||||
opening.x = 0.504166667;
|
||||
opening.y = 0.5;
|
||||
opening.angle = -90;
|
||||
if (state === 'virtual') {
|
||||
space.open_spans = [{
|
||||
a: [0.5041666666666667, 0.004166666666666667],
|
||||
b: [0.5041666666666667, 0.9958333333333333],
|
||||
}];
|
||||
}
|
||||
}
|
||||
fixture.config.spaces.push(space);
|
||||
}
|
||||
if (scenario.cornerSplitWall) {
|
||||
const stage = scenario.cornerSplitWall;
|
||||
if (!['before', 'thin', 'thick', 'zero-taper'].includes(stage))
|
||||
|
||||
+13
-1
@@ -1,7 +1,7 @@
|
||||
import { fixtureWallKey } from '../fixtures/visual-matrix.mjs';
|
||||
|
||||
/** Data-only HP-QA-01 capture matrix. Bump when framing or scenarios change. */
|
||||
export const GOLDEN_MATRIX_VERSION = 41;
|
||||
export const GOLDEN_MATRIX_VERSION = 42;
|
||||
|
||||
const stage = { capture: 'stage', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0005 } };
|
||||
const page = { capture: 'page', threshold: { maxChannelDelta: 10, maxDiffRatio: 0.0008 } };
|
||||
@@ -165,6 +165,18 @@ const decorLayerProbes = {
|
||||
};
|
||||
|
||||
export const GOLDEN_SCENARIOS = Object.freeze([
|
||||
{ id: 'coincident-partition-before-dark', fixture: 'visual',
|
||||
space: 'golden-coincident-partition', coincidentPartition: 'before',
|
||||
mode: 'view', theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
|
||||
{ id: 'coincident-partition-thin-dark', fixture: 'visual',
|
||||
space: 'golden-coincident-partition', coincidentPartition: 'thin',
|
||||
mode: 'view', theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
|
||||
{ id: 'coincident-partition-thick-dark', fixture: 'visual',
|
||||
space: 'golden-coincident-partition', coincidentPartition: 'thick',
|
||||
mode: 'view', theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
|
||||
{ id: 'coincident-partition-virtual-dark', fixture: 'visual',
|
||||
space: 'golden-coincident-partition', coincidentPartition: 'virtual',
|
||||
mode: 'view', theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
|
||||
{ id: 'split-corner-wall-before-dark', fixture: 'visual', space: 'golden-corner-split',
|
||||
cornerSplitWall: 'before', mode: 'view', theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
|
||||
{ id: 'split-corner-wall-thin-dark', fixture: 'visual', space: 'golden-corner-split',
|
||||
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
@@ -545,6 +545,11 @@ partial/composite matches, overlapping openings and adjacent independent
|
||||
bodies fail closed. The candidate then crosses the existing whole-plan
|
||||
geometry preflight and one atomic Optimize write/Undo boundary. No render or
|
||||
ordinary save path invokes this pass, so `PLAN_MODEL_VERSION` remains unchanged.
|
||||
`OptimizeDependencies` is a narrow test/benchmark seam: production uses the
|
||||
real helper, while the committed large-house benchmark substitutes a no-op to
|
||||
measure only this pass and the unit contract instruments its exact per-space
|
||||
call count. A source-ownership assertion fails if a render/pointer module ever
|
||||
imports the helper.
|
||||
|
||||
`boundary` is one contextual UI tool over the existing `open_spans` model.
|
||||
Before the first click, independent physical bodies block the room boundary
|
||||
|
||||
+12
-4
@@ -2815,15 +2815,23 @@ require hands on real hardware — they remain for the human pass.
|
||||
exact independent wall is removed, its hosted door becomes an ordinary
|
||||
opening at the same centre/angle with all bindings and unknown fields,
|
||||
and the wider centred thickness survives. Direction and room order do
|
||||
not matter; partial/extra/unknown/draft/column/opening-conflict cases are
|
||||
byte-preserved. Preview writes nothing, Apply uses one WS transaction,
|
||||
not matter; three non-overlapping hosted door/window/gate records are
|
||||
rehosted atomically, while a hosted-hosted overlap and all other
|
||||
partial/extra/unknown/draft/column/opening conflicts fail closed. Preview
|
||||
writes nothing, Apply uses one WS transaction,
|
||||
reload is idempotent, Undo restores the hosted form, and Boundary plus
|
||||
Thickness target the resulting shared wall [unit:
|
||||
Thickness target the resulting shared wall. Four targeted golden scenes
|
||||
retain the 5 cm offset and show before, 10 cm, 30 cm and virtual results;
|
||||
the paired large-house benchmark enforces p95 overhead ≤15% and ≤25 ms,
|
||||
while source ownership plus an injected counter keep the helper out of
|
||||
render/pointer paths [unit:
|
||||
test/coincident-partitions.test.mjs + test/plan-optimizer.test.mjs;
|
||||
auto: smoke_optimize_coincident_partition; mutations:
|
||||
`optimizer-coincident-opening-rehost-disabled`,
|
||||
`optimizer-coincident-partial-accepted`, existing
|
||||
`optimize-preflight-bypassed`].
|
||||
`optimize-preflight-bypassed`; performance:
|
||||
`npm run benchmark:coincident-partitions`; golden:
|
||||
`coincident-partition-{before,thin,thick,virtual}-dark`].
|
||||
- [ ] **Unit + backend**: inset/mitre/bevel, key from either end, degrade,
|
||||
rekey, cm↔inches; `walls` schema bounds
|
||||
[auto: test/wall-thickness.test.mjs + tests_backend/test_validation.py]
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 291 KiB After Width: | Height: | Size: 291 KiB |
@@ -2,7 +2,7 @@
|
||||
"version": 1,
|
||||
"fixture": "synthetic-only",
|
||||
"chromium": "151.0.7922.34",
|
||||
"sourceFingerprint": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
|
||||
"sourceFingerprint": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
|
||||
"captureScriptSha256": "ce2e9542fed9dade3085be87d16f69adb2ac8262893ad78ad966b1b9673f2983",
|
||||
"command": "npm run build && node demo/docs/capture.mjs",
|
||||
"scenarios": {
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
|
||||
"sourceSha256": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
|
||||
"imageSha256": "5ba555c1f46416ce17e40ef2b6ecd9eb6e1d0ce6b8d467acd0682af95bd36307"
|
||||
},
|
||||
"view-touch": {
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
|
||||
"sourceSha256": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
|
||||
"imageSha256": "deaae14893e66337b42e45ccfe87cf16f571cfc10f98da4250a6a6589a092a43"
|
||||
},
|
||||
"space-create": {
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
|
||||
"sourceSha256": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
|
||||
"imageSha256": "c33a7279165a4cec6fa6fadb6fd08cd967e082a17fe101ef442d27d36ae59b6b"
|
||||
},
|
||||
"room-contour-close": {
|
||||
@@ -47,7 +47,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
|
||||
"sourceSha256": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
|
||||
"imageSha256": "37f7e4f1d7601bc5c0fdfd38c6482a6af85c845c883283b9aba16d6457383355"
|
||||
},
|
||||
"plan-context-tray": {
|
||||
@@ -58,7 +58,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
|
||||
"sourceSha256": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
|
||||
"imageSha256": "b0398943d8a7c8215159d2d54b895c28feb1be8e303e65ca106ea5185c34644e"
|
||||
},
|
||||
"device-editor": {
|
||||
@@ -69,8 +69,8 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
|
||||
"imageSha256": "c3704489b36fd4177e761124fac032d8d09d6a94e3110f9a77d40ec110c1590b"
|
||||
"sourceSha256": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
|
||||
"imageSha256": "f24da9380b83b4e0087f81dadbf1ca45df0395558baa0441ebb8c4b14584c899"
|
||||
},
|
||||
"device-display-preview": {
|
||||
"file": "06-device-display-preview.png",
|
||||
@@ -80,7 +80,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
|
||||
"sourceSha256": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
|
||||
"imageSha256": "7241478b3fae2a5b1895f241bc50a65f1aaacce907d311cc87796a251c389a8b"
|
||||
},
|
||||
"background-editor": {
|
||||
@@ -91,7 +91,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
|
||||
"sourceSha256": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
|
||||
"imageSha256": "8241b09b0d821247a86eaadd6043a37dde29623c870d8a142bcd381c638a306d"
|
||||
},
|
||||
"room-card": {
|
||||
@@ -102,7 +102,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
|
||||
"sourceSha256": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
|
||||
"imageSha256": "029a3e69ec647a8a370d99e6bb7f9225833c526739076022f6b52ba54bff30ea"
|
||||
},
|
||||
"device-info": {
|
||||
@@ -113,7 +113,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "ceb125adfd9b21b62821936055562a2864e7d57007694c16169f7bc7ffa383b4",
|
||||
"sourceSha256": "06493179d9db0327626adb8a69fc7cdaf46e8e24f17dae066d93080e4ec0cedc",
|
||||
"imageSha256": "8e65ff2191ad1f1172848418c8381967ecff2cc831653a8f9c3ae11352a0e84b"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"benchmark:glow": "node demo/benchmark_glow.mjs",
|
||||
"benchmark:compare": "node demo/performance/compare.mjs",
|
||||
"benchmark:optimize-geometry-preflight": "node demo/benchmark_optimize_geometry_preflight.mjs",
|
||||
"benchmark:coincident-partitions": "node demo/benchmark_coincident_partitions.mjs",
|
||||
"golden:capture": "node demo/golden/run.mjs --mode=capture",
|
||||
"golden:verify": "node demo/golden/run.mjs --mode=verify",
|
||||
"continuity:screencast": "node demo/screencast_visual_continuity.mjs",
|
||||
|
||||
@@ -73,6 +73,11 @@ export interface OptimizeResult {
|
||||
changed: boolean;
|
||||
}
|
||||
|
||||
/** Test/benchmark seam for proving that structural maintenance stays inside Optimize. */
|
||||
export interface OptimizeDependencies {
|
||||
reconcileCoincidentPartitions?: typeof reconcileCoincidentPartitions;
|
||||
}
|
||||
|
||||
const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value));
|
||||
const own = (o: any, key: string): boolean => Object.prototype.hasOwnProperty.call(o, key);
|
||||
const clamp = (value: number, min: number, max: number): number => (
|
||||
@@ -393,7 +398,10 @@ export function optimizePlans(
|
||||
configIn: any,
|
||||
layoutIn: Record<string, any>,
|
||||
context: SpaceReferenceRepairContext = {},
|
||||
dependencies: OptimizeDependencies = {},
|
||||
): OptimizeResult {
|
||||
const reconcilePartitions = dependencies.reconcileCoincidentPartitions
|
||||
?? reconcileCoincidentPartitions;
|
||||
const references = repairSpaceReferences(configIn, layoutIn, context);
|
||||
const config = references.config;
|
||||
const original = JSON.stringify(configIn || {});
|
||||
@@ -545,7 +553,7 @@ export function optimizePlans(
|
||||
// preflight checks the candidate (#276).
|
||||
const reconciledModel = modelOf(space);
|
||||
if (reconciledModel) {
|
||||
const reconciled = reconcileCoincidentPartitions(
|
||||
const reconciled = reconcilePartitions(
|
||||
space, reconciledModel, space.walls || [], cuts,
|
||||
{
|
||||
pitch: GRID_STEP_N,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { join, relative } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { optimizePlans } from '../test-build/plan-optimizer.js';
|
||||
import { checkOptimizeGeometry } from '../test-build/plan-geometry-preflight.js';
|
||||
import { reconcileCoincidentPartitions } from '../test-build/coincident-partitions.js';
|
||||
import { GRID_PITCH, GRID_STEP_N as S } from '../test-build/space-geometry.js';
|
||||
import { wallIntervals } from '../test-build/wall-thickness.js';
|
||||
|
||||
@@ -68,6 +71,91 @@ test('issue 276 exact proof ignores endpoint direction and room order', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('issue 276 rehosts three non-overlapping door/window/gate openings atomically', () => {
|
||||
const input = clone(fixture);
|
||||
const space = input.spaces[0];
|
||||
space.openings = [
|
||||
{ id: 'hosted-door', type: 'door', x: 0, y: 0, angle: 0, length: 0.12,
|
||||
contact: 'binary_sensor.door', host: { kind: 'partition', id: 'redundant', t: 59 / 238 } },
|
||||
{ id: 'hosted-window', type: 'window', x: 0, y: 0, angle: 0, length: 0.12,
|
||||
cover: 'cover.window', host: { kind: 'partition', id: 'redundant', t: 0.50 } },
|
||||
{ id: 'hosted-gate', type: 'gate', x: 0, y: 0, angle: 0, length: 0.12,
|
||||
future_field: { keep: true }, host: { kind: 'partition', id: 'redundant', t: 179 / 238 } },
|
||||
];
|
||||
|
||||
const result = optimize(input);
|
||||
assert.equal(result.report.partitionsReconciled, 1);
|
||||
assert.equal(result.report.openingsRehosted, 3);
|
||||
const openings = result.config.spaces[0].openings;
|
||||
assert.deepEqual(openings.map((opening) => opening.id),
|
||||
['hosted-door', 'hosted-window', 'hosted-gate']);
|
||||
assert.deepEqual(openings.map((opening) => opening.type), ['door', 'window', 'gate']);
|
||||
assert.ok(openings.every((opening) => opening.host === undefined));
|
||||
assert.ok(openings.every((opening) => opening.x === 0.504166667));
|
||||
assert.ok(openings[0].y < openings[1].y && openings[1].y < openings[2].y);
|
||||
assert.equal(openings[0].contact, 'binary_sensor.door');
|
||||
assert.equal(openings[1].cover, 'cover.window');
|
||||
assert.deepEqual(openings[2].future_field, { keep: true });
|
||||
assert.equal(checkOptimizeGeometry(result.config).ok, true);
|
||||
|
||||
const second = optimize(result.config);
|
||||
assert.equal(second.changed, false);
|
||||
assert.equal(second.report.openingsRehosted, 0);
|
||||
assert.deepEqual(second.config, result.config);
|
||||
});
|
||||
|
||||
test('issue 276 fails closed when two hosted openings would overlap after rehost', () => {
|
||||
const input = clone(fixture);
|
||||
const space = input.spaces[0];
|
||||
space.openings = [
|
||||
{ id: 'overlap-door', type: 'door', x: 0, y: 0, angle: 0, length: 0.2,
|
||||
host: { kind: 'partition', id: 'redundant', t: 0.45 } },
|
||||
{ id: 'overlap-window', type: 'window', x: 0, y: 0, angle: 0, length: 0.2,
|
||||
host: { kind: 'partition', id: 'redundant', t: 0.55 } },
|
||||
];
|
||||
const result = optimize(input);
|
||||
assert.equal(result.report.partitionsReconciled, 0);
|
||||
assert.equal(result.report.openingsRehosted, 0);
|
||||
assert.equal(result.config.spaces[0].partitions.length, 1);
|
||||
assert.deepEqual(result.config.spaces[0].openings.map((opening) => opening.id),
|
||||
['overlap-door', 'overlap-window']);
|
||||
assert.ok(result.config.spaces[0].openings.every((opening) => opening.host?.id === 'redundant'));
|
||||
});
|
||||
|
||||
test('issue 276 reconciliation is owned by explicit Optimize and called once per valid space', () => {
|
||||
const input = clone(fixture);
|
||||
const secondSpace = clone(input.spaces[0]);
|
||||
secondSpace.id = 'offset-shared-wall-second';
|
||||
input.spaces.push(secondSpace);
|
||||
let calls = 0;
|
||||
const instrumented = (...args) => {
|
||||
calls++;
|
||||
return reconcileCoincidentPartitions(...args);
|
||||
};
|
||||
const result = optimizePlans(input, {}, {}, {
|
||||
reconcileCoincidentPartitions: instrumented,
|
||||
});
|
||||
assert.equal(calls, 2, 'one explicit pass per valid space in one Optimize candidate');
|
||||
assert.equal(result.report.partitionsReconciled, 2);
|
||||
|
||||
const sourceRoot = fileURLToPath(new URL('../src/', import.meta.url));
|
||||
const sourceFiles = [];
|
||||
const walk = (directory) => {
|
||||
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
||||
const path = join(directory, entry.name);
|
||||
if (entry.isDirectory()) walk(path);
|
||||
else if (entry.isFile() && entry.name.endsWith('.ts')) sourceFiles.push(path);
|
||||
}
|
||||
};
|
||||
walk(sourceRoot);
|
||||
const owners = sourceFiles
|
||||
.filter((path) => readFileSync(path, 'utf8').includes('reconcileCoincidentPartitions'))
|
||||
.map((path) => relative(sourceRoot, path).replaceAll('\\', '/'))
|
||||
.sort();
|
||||
assert.deepEqual(owners, ['coincident-partitions.ts', 'plan-optimizer.ts'],
|
||||
'render/pointer modules must not import or invoke the Optimize-only pass');
|
||||
});
|
||||
|
||||
test('issue 276 fails closed for an orphan host, overlap, draft, column and unknown partition data', () => {
|
||||
const variants = [];
|
||||
|
||||
|
||||
@@ -255,6 +255,36 @@ test('corner Split golden captures before, thin and thick facade states', () =>
|
||||
}
|
||||
});
|
||||
|
||||
test('issue 276 golden captures 5 cm offsets and hosted door before/after 10/30/virtual', () => {
|
||||
const scenarios = GOLDEN_SCENARIOS.filter((scenario) => scenario.coincidentPartition);
|
||||
assert.deepEqual(
|
||||
scenarios.map((scenario) => scenario.coincidentPartition),
|
||||
['before', 'thin', 'thick', 'virtual'],
|
||||
);
|
||||
for (const scenario of scenarios) {
|
||||
const fixture = prepareGoldenFixture(scenario);
|
||||
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
|
||||
assert.ok(space);
|
||||
assert.equal(space.cell_cm, 5);
|
||||
assert.ok(space.rooms[0].poly.some((point) => point[0] === 0.5
|
||||
&& point[1] === 0.004166666666666667), 'short 5 cm offset must be visible');
|
||||
assert.equal(space.openings.length, 1);
|
||||
if (scenario.coincidentPartition === 'before') {
|
||||
assert.equal(space.partitions.length, 1);
|
||||
assert.equal(space.openings[0].host.id, 'redundant');
|
||||
continue;
|
||||
}
|
||||
assert.equal(space.partitions, undefined);
|
||||
assert.equal(space.openings[0].host, undefined);
|
||||
assert.equal(space.openings[0].x, 0.504166667);
|
||||
if (scenario.coincidentPartition === 'virtual') {
|
||||
assert.equal(space.open_spans.length, 1);
|
||||
} else {
|
||||
assert.equal(space.walls[0].cm, scenario.coincidentPartition === 'thin' ? 10 : 30);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('filled opening golden has a pixel-level seam detector', () => {
|
||||
const scenario = GOLDEN_SCENARIOS.find((item) => item.id === 'openings-filled-tunnel-dark');
|
||||
assert.ok(scenario);
|
||||
@@ -322,7 +352,7 @@ test('sun-ray golden requires browser-painted light from a state-only sun entity
|
||||
assert.ok(scenario);
|
||||
const fixture = prepareGoldenFixture(scenario);
|
||||
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
|
||||
assert.equal(GOLDEN_MATRIX_VERSION, 41);
|
||||
assert.equal(GOLDEN_MATRIX_VERSION, 42);
|
||||
assert.equal(space.settings.sun_rays, true);
|
||||
assert.equal(scenario.northDeg, 90,
|
||||
'the sign-sensitive golden must keep a non-zero north direction');
|
||||
|
||||
Reference in New Issue
Block a user