4;){const e=t.findIndex(t=>!t.live);if(e<0)break;clearTimeout(t[e].evict),t.splice(e,1)}}_warmScheduleEvict(t,e){if(clearTimeout(t.evict),!(t.dlg||t.vp&&"view"!==t.vp.mode))return;const i=t.freed,s=t.owner;t.evict=window.setTimeout(()=>{if(t.evict=0,t.freed!==i||t.owner!==s)return;var n;t.dlg=null,t.vp=(n=t.vp)&&"view"!==n.mode?{...n,mode:"view",zoom:n.snap?.space===n.space?n.snap.zoom:n.zoom,view:null,snap:null,tool:"draw",decorTool:"select",showHidden:!1,selId:null,rszSel:null,decorSel:null}:n,t.frameFingerprint="";const o=fm.get(e);if(!t.live&&o&&o.length>1){const e=o.indexOf(t);e>=0&&o.splice(e,1)}},bm+250)}_warmAdoptViewport(t){const e=this._warmVp;if(!e||this._warmSlot?.path!==location.pathname)return void(this._warmVp=null);const i=this._fixedFloorState();if(this._hashApplied||!this._model.find(t=>t.id===e.space)||"valid"===i.kind&&i.id!==e.space||this._hasFixedFloor&&"valid"!==i.kind)return void(this._warmVp=null);this._commitSpace(e.space,!0),this._navApplied=!0,this._adoptMode("view"!==e.mode&&this._canEdit&&!t.kiosk?e.mode:"view"),this._pendingNavMode="view"===e.mode||this._canEdit||t.kiosk?null:e.mode,this._zoom=e.zoom;const s=this._effectiveProjection(),n=s===e.projection&&this._labsIso===e.activeLabsIso;if(this._view=n&&e.view?{...e.view}:null,this._viewModeSnap=n&&e.snap?{...e.snap}:null,!n&&e.logicalCenter){const t="iso"===s?xg([e.logicalCenter.x,e.logicalCenter.y],0):[e.logicalCenter.x,e.logicalCenter.y];this._applyView(e.zoom,t[0],t[1])}var o;this._tool="openwall"===(o=e.tool)||"closewall"===o?"boundary":"opening"===(o=function(t){return"partition"===t?"draw":t}(o))?"draw":"string"==typeof o&&Rm.has(o)?o:"draw",this._decorTool=e.decorTool,this._showHidden=e.showHidden,this._showFar!==e.showFar&&(this._showFar=e.showFar,this._frame=null),this._selId=e.selId,this._rszSel=e.rszSel,this._decorSel=e.decorSel,this._warmVpArmed=!0}_warmPatch(t,e=!1){if(this._config?.kiosk)return;const i=ym(this._config);if(!this._warmSlot||this._warmKey===i){if(!this._warmSlot){if(!e)return;const t=this.parentNode;this._warmKey=i,this._warmSlot={owner:this._warmGen,path:location.pathname,place:t?new WeakRef(t):null,idx:this._warmIdx(t),live:!0,hdrH:this._hdrH,stageH:0,vp:null,frameFingerprint:"",devices:null,dlg:null,freed:0,evict:0};const s=fm.get(i)||[];for(s.push(this._warmSlot),fm.set(i,s),this._warmTrim(s);fm.size>8;){const t=fm.keys().next().value;if(void 0===t||t===i)break;for(const e of fm.get(t)||[])clearTimeout(e.evict);fm.delete(t)}}Object.assign(this._warmSlot,t)}}_warmViewportState(){const t=this._effectiveProjection();return{space:this._space,mode:this._mode,projection:t,activeLabsIso:this._labsIso,logicalCenter:this._logicalViewCenter(t),zoom:this._zoom,view:this._view?{...this._view}:null,snap:this._viewModeSnap?{...this._viewModeSnap}:null,tool:this._tool,decorTool:this._decorTool,showHidden:this._showHidden,showFar:this._showFar,selId:this._selId,rszSel:this._rszSel,decorSel:this._decorSel}}_warmDialogState(){const t=(t,e)=>({kind:t,space:this._space,mode:this._mode,data:e});return this._tapConfirm||this._alignDialog||this._mergeDialog||this._importDialog||this._backupExportDialog||this._backupImportDialog?null:this._openingInfo?t("openingInfo",this._openingInfo.id):this._infoCard?t("info",this._infoCard.id):this._rulesDialog?this._rulesDialog.busy?null:t("rules",this._rulesDialog):this._settingsDialog?this._settingsDialog.busy?null:t("settings",this._settingsDialog):this._markerDialog?this._markerDialog.busy?null:t("marker",this._markerDialog):this._openingDialog?t("opening",this._openingDialog):this._backdropDialog?t("backdrop",this._backdropDialog):this._decorShapeDialog?t("decorShape",this._decorShapeDialog):this._decorTextDialog?t("decorText",this._decorTextDialog):this._roomDialog?t("room",{editId:this._roomEditId,fill:this._roomFill,customFill:this._roomCustomFill,tempSrc:this._roomTempSrc,humSrc:this._roomHumSrc,srcOpen:this._roomSrcOpen,srcFilter:this._roomSrcFilter,nameScale:this._roomNameScale,labelScale:this._roomLabelScale,areaSel:this._areaSel,nameSel:this._nameSel,pendingSplit:this._pendingSplit,wallFaceBatch:this._wallFaceBatch,path:this._path}):this._spaceDialog?this._spaceDialog.busy?null:t("space",this._spaceDialog):null}_warmSnapshot(){if(this._booting||this._config?.kiosk||"steady"!==this._continuity.state)return;const t={vp:this._warmViewportState(),frameFingerprint:this._continuity.frameFingerprint,devices:this._devices};if(this._warmRevivePending||(t.dlg=this._warmDialogState()),this.isConnected&&this._warmSlot?.owner===this._warmGen){const e=this.parentNode;t.place=e?new WeakRef(e):null,t.idx=this._warmIdx(e)}this._warmPatch(t)}_warmReviveDialog(){this._warmRevivePending=!1;const t=this._warmSlot;if(this._warmReviveTimer=void 0,!t||!t.dlg)return;const e=t.dlg,i=t.freed;if(t.dlg=null,t.freed=0,clearTimeout(t.evict),t.evict=0,i&&!(Date.now()-i>bm)&&e.space===this._space&&e.mode===this._mode){switch(e.kind){case"space":this._spaceDialog={...e.data,busy:!1,savedBusy:!1};break;case"marker":this._markerDialog={...e.data,busy:!1};break;case"settings":this._settingsDialog={...e.data,busy:!1};break;case"rules":this._rulesDialog={...e.data,busy:!1};break;case"opening":this._openingDialog={...e.data};break;case"backdrop":this._backdropDialog={...e.data};break;case"decorShape":this._decorShapeDialog={...e.data};break;case"decorText":{this._decorTextDialog={...e.data};const t=String(this._decorTextDialog?.text??"").length;this._decorTextSelection={start:t,end:t};break}case"room":{const t=e.data;this._roomEditId=t.editId,this._roomFill=t.fill,this._roomCustomFill=t.customFill||null,this._roomTempSrc=t.tempSrc,this._roomHumSrc=t.humSrc,this._roomSrcOpen=t.srcOpen,this._roomSrcFilter=t.srcFilter,this._roomNameScale=t.nameScale,this._roomLabelScale=t.labelScale,this._areaSel=t.areaSel,this._nameSel=t.nameSel,this._pendingSplit=t.pendingSplit,this._wallFaceBatch=t.wallFaceBatch||null,this._path=t.path,this._wallFaceBatch&&(this._activeDraftId=this._wallFaceBatch.activeDraftId,this._draftSegmentCms=[...this._wallFaceBatch.activeCms]),this._roomDialog=!0;break}case"info":{const t=this._devices.find(t=>t.id===e.data);t&&(this._infoCard=t);break}case"openingInfo":{const t=(this._curSpaceCfg?.openings||[]).find(t=>t.id===e.data);t&&(this._openingInfo=t);break}}this.requestUpdate()}}_cacheSnapshot(){if(this._serverCfg)try{this._cfgContentFingerprint=Ih(this._serverCfg),this._layoutContentFingerprint=Ih(this._layout),this._virtualLights=function(t,e,i){if(t.configRev===i)return t;const s=new Set((Array.isArray(e?.markers)?e.markers:[]).filter(yn).map(t=>t.id));return{...t,configRev:i,off:new Set([...t.off].filter(t=>s.has(t)))}}(this._virtualLights,this._serverCfg,this._cfgRev),localStorage.setItem(km,JSON.stringify({config:this._serverCfg,rev:this._cfgRev,config_fingerprint:this._cfgContentFingerprint,layout:this._layout,layout_rev:this._layoutRev,layout_fingerprint:this._layoutContentFingerprint,virtual_lights:fn(this._virtualLights)}))}catch{}}_beginContinuityCandidate(t,e,i="plan"){return this._booting&&!this._continuity.hasCompleteFrame?this._continuity.token:(this._continuityDataReady=e,this._continuityPaintToken=-1,this._stagedDeviceSnapshotToken=-1,this._resumeSettling=!0,this._continuity.beginCandidate(t,i))}_continuityStageValid(){const t=this._stageEl;return!!t&&t.clientWidth>0&&t.clientHeight>0}_continuityAssetsReady(){if(!this._model.length)return!1;const t=this._model.length?this._spaceModel():null;return!t?.bg?.href||this._signer.isReady(this.hass,t.bg.href)}_initialSpaceSelection(t,e=this._loadOk){const i=this._fixedFloorState(t,e);return"valid"===i.kind?{id:i.id,source:"fixed"}:this._hasFixedFloor?{id:null,source:"none"}:function(t){const e=new Set(t.spaceIds.filter(t=>!!t));if(!e.size)return{id:null,source:"none"};const i=[["hash",!1===t.acceptHash?null:t.hashSpace],["current",t.preserveCurrent?t.currentSpace:null],["saved",t.savedSpace],["default",t.defaultSpace],["first",t.spaceIds[0]]];for(const[t,s]of i)if(s&&e.has(s))return{id:s,source:t};return{id:null,source:"none"}}({spaceIds:t.map(t=>t.id),hashSpace:this._hashSpace(),acceptHash:!this._hashApplied,currentSpace:this._space,preserveCurrent:this._hashApplied||this._navApplied||this._warmVpArmed,savedSpace:this._savedNav()?.space,defaultSpace:this._config?.default_floor})}_adoptInitialSpace(t,e=this._loadOk){const i=this._initialSpaceSelection(t,e);return i.id?(this._commitSpace(i.id,!0),"hash"===i.source&&(this._hashApplied=!0),"saved"===i.source&&(this._navApplied=!0),i):i}_candidateBackdrop(t,e=this._space){const i=xc(t),s=this._fixedFloorState(i,!0);if(this._hasFixedFloor&&"valid"!==s.kind)return"";const n=this._initialSpaceSelection(i,!0).id||(i.some(t=>t.id===e)?e:i[0]?.id);return i.find(t=>t.id===n)?.bg?.href||""}_visualFrameFingerprint(){const t=this._stageEl,e=t?[t.clientWidth,t.clientHeight]:[0,0];return Ah([this._cfgRev,this._cfgContentFingerprint||Ih(this._serverCfg),this._layoutRev,this._layoutContentFingerprint||Ih(this._layout),this._space,this._mode,this._view,e,this._glowScreenBlend?"screen":"normal",this.hass?.themes?.darkMode??this.hass?.themes?.default_theme??""])}_settleContinuityFrame(){if(this._booting||!this._continuityStageValid())return;if(!this._continuity.hasCompleteFrame&&"steady"===this._continuity.state)return void(this._continuityAssetsReady()&&(this._renderSnapshotAt=Date.now(),this._continuity.markCompleteFrame(this._visualFrameFingerprint())));if(!this._continuityDataReady)return;if(!["holding","offline-stale","overlay-pending","overlay-visible","candidate-ready"].includes(this._continuity.state))return;const t=this._continuity.token;if(this._candidateDeviceSnapshot&&this._candidateDeviceSnapshot!==this._visibleDeviceSnapshot&&this._stagedDeviceSnapshotToken!==t)return this._stagedDeviceSnapshotToken=t,void this.requestUpdate();this._continuityPaintToken!==t&&(this._continuityPaintToken=t,this._continuity.candidateReady(t)&&this._continuity.commitAfterPaint(t,{updateComplete:()=>this.updateComplete,stageValid:()=>this.isConnected&&this._continuityStageValid(),assetsReady:()=>this._continuityAssetsReady(),frameFingerprint:()=>this._visualFrameFingerprint()}).then(e=>{e&&t===this._continuity.token?(this._resumeSettling=!1,this._renderSnapshotAt=Date.now(),this._candidateDeviceSnapshot&&(this._visibleDeviceSnapshot=this._candidateDeviceSnapshot),this._candidateDeviceSnapshot=null,this._stagedDeviceSnapshotToken=-1,this._warmSnapshot()):t===this._continuity.token&&(this._continuityPaintToken=-1,this._stagedDeviceSnapshotToken=-1,this._candidateDeviceSnapshot=null,this.requestUpdate())}))}_onBackdropLoaded(t,e){this._signer.markLoaded(this.hass,t,e),this._continuity.note("asset-ready"),this._continuityPaintToken=-1,"steady"!==this._continuity.state&&this.requestUpdate()}_renderRecoveryOverlay(){if(!this._continuity.overlayVisible&&"recovery-error"!==this._continuity.state)return G;const t="connection"===this._continuity.recoveryReason;return W`t.stopPropagation()}
@click=${t=>t.stopPropagation()}
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 10495184..5b1e79bf 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -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
diff --git a/docs/TESTING.md b/docs/TESTING.md
index 04c931d9..96a3dff9 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -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]
diff --git a/docs/images/06-device-editor.png b/docs/images/06-device-editor.png
index 51af34aa..3f56ab27 100644
Binary files a/docs/images/06-device-editor.png and b/docs/images/06-device-editor.png differ
diff --git a/docs/images/screenshots.json b/docs/images/screenshots.json
index 4d51d348..23bfdfd5 100644
--- a/docs/images/screenshots.json
+++ b/docs/images/screenshots.json
@@ -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"
}
}
diff --git a/package.json b/package.json
index 99a3ee38..773b2c6f 100755
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/plan-optimizer.ts b/src/plan-optimizer.ts
index d0773a89..960aec80 100644
--- a/src/plan-optimizer.ts
+++ b/src/plan-optimizer.ts
@@ -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 = (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,
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,
diff --git a/test/coincident-partitions.test.mjs b/test/coincident-partitions.test.mjs
index 82c455cf..41de46fb 100644
--- a/test/coincident-partitions.test.mjs
+++ b/test/coincident-partitions.test.mjs
@@ -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 = [];
diff --git a/test/golden-matrix.test.mjs b/test/golden-matrix.test.mjs
index 29cadd94..f6c56178 100644
--- a/test/golden-matrix.test.mjs
+++ b/test/golden-matrix.test.mjs
@@ -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');