From cfc1bb063ed590bbeb8bb1e8a12333113a1147e6 Mon Sep 17 00:00:00 2001 From: Matysh Date: Wed, 26 Aug 2026 02:13:01 +0300 Subject: [PATCH] fix: preserve migrated wall interactions Issue: #282 User-Visible: yes --- demo/smoke_near_axis_optimize.mjs | 5 +-- demo/smoke_openwall.mjs | 6 ++-- demo/smoke_optimize_coincident_partition.mjs | 14 ++++++--- demo/smoke_resize_outer_reconciliation.mjs | 11 ++++--- demo/smoke_resize_virtual_thick.mjs | 31 ++++++++++++++----- demo/smoke_sun.mjs | 17 +++++++---- demo/smoke_wall_thickness.mjs | 32 ++++++++++++++++++++ docs/CHANGELOG.md | 4 ++- docs/CHANGELOG.ru.md | 3 ++ src/houseplan-card.ts | 27 ++++++++++++++--- test/wall-segment-model.test.mjs | 17 +++++++++++ 11 files changed, 135 insertions(+), 32 deletions(-) diff --git a/demo/smoke_near_axis_optimize.mjs b/demo/smoke_near_axis_optimize.mjs index 4fc6a7e8..e3f1b94f 100644 --- a/demo/smoke_near_axis_optimize.mjs +++ b/demo/smoke_near_axis_optimize.mjs @@ -75,9 +75,10 @@ const out = await page.evaluate(async (sourceSpace) => { const north = card._serverCfg.spaces[0].rooms.find((room) => room.id === 'north-west'); const south = card._serverCfg.spaces[0].rooms.find((room) => room.id === 'south-west'); result.applyUsesOneAtomicWrite = sent.filter((type) => type === 'houseplan/plan/optimize').length === 1; + const samePoint = (left, right) => JSON.stringify(left) === JSON.stringify(right); result.applyMovesBothOwnersToExactAxis = north.poly[1][1] === north.poly[2][1] - && JSON.stringify(north.poly[1]) === JSON.stringify(south.poly[0]) - && JSON.stringify(north.poly[2]) === JSON.stringify(south.poly[3]); + && south.poly.some((point) => samePoint(point, north.poly[1])) + && south.poly.some((point) => samePoint(point, north.poly[2])); result.applyRekeysWall = card._serverCfg.spaces[0].walls.some((wall) => ( wall.a[1] === wall.b[1] && wall.key.endsWith('@0.0000') )); diff --git a/demo/smoke_openwall.mjs b/demo/smoke_openwall.mjs index f4f8ed6f..081cda0b 100644 --- a/demo/smoke_openwall.mjs +++ b/demo/smoke_openwall.mjs @@ -93,9 +93,9 @@ const res = await page.evaluate(async () => { const probeR3 = [802, 480]; const beforeReachesR3 = isLit(probeR3); if (rr3) { - rr2.open_to = [...(rr2.open_to || []), 'r3']; - rr3.open_to = [...(rr3.open_to || []), 'r2']; - // legacy open_to without spans still expands on read for cuts — force spans for r2-r3 + // Create the second span through the same production boundary tool. Direct + // legacy open_to injection would make the first click close that already + // open wall instead of anchoring a new partial span. c._setMode('plan'); c._tool = 'boundary'; // shared r2|r3 at y=460, x 550..960 c._boundaryClick([700, 460]); diff --git a/demo/smoke_optimize_coincident_partition.mjs b/demo/smoke_optimize_coincident_partition.mjs index b215249a..cbdfec68 100644 --- a/demo/smoke_optimize_coincident_partition.mjs +++ b/demo/smoke_optimize_coincident_partition.mjs @@ -7,6 +7,8 @@ const out = await page.evaluate(async () => { const result = {}; const card = window.__card; const clone = (value) => JSON.parse(JSON.stringify(value)); + const wallHostResolves = (space, opening) => opening.host?.kind === 'wall' + && (space.wall_segments || []).some((segment) => segment.id === opening.host.id); const sharedX = 0.5; const original = { model_version: 7, @@ -64,11 +66,13 @@ const out = await page.evaluate(async () => { card._openAlignDialog(); await card.updateComplete; const preview = card._alignDialog; + const previewSpace = preview?.config?.spaces?.[0]; result.previewIsExact = !!preview?.changed && preview.preflight?.ok === true && preview.report.partitionsReconciled === 1 && preview.report.openingsRehosted === 1 - && preview.config.spaces[0].partitions == null - && preview.config.spaces[0].openings[0].host == null; + && preview.config.model_version === 8 + && previewSpace.partitions == null + && wallHostResolves(previewSpace, previewSpace.openings[0]); result.previewDoesNotWrite = sent.length === 0 && JSON.stringify(card._serverCfg) === JSON.stringify(original); result.reportRendersBothCounters = card.renderRoot.querySelectorAll('hp-dialog .alignmsg').length >= 2; @@ -77,7 +81,7 @@ const out = await page.evaluate(async () => { const applied = card._serverCfg.spaces[0]; result.applyUsesOneAtomicWrite = sent.filter((type) => type === 'houseplan/plan/optimize').length === 1; result.applyKeepsOpeningFields = applied.partitions == null - && applied.openings[0].host == null + && wallHostResolves(applied, applied.openings[0]) && applied.openings[0].id === 'door' && applied.openings[0].contact === 'binary_sensor.test_door' && applied.openings[0].lock === 'lock.test_door' @@ -88,7 +92,7 @@ const out = await page.evaluate(async () => { await card._loadFromServer(); await card.updateComplete; result.reloadKeepsCanonicalBody = card._serverCfg.spaces[0].partitions == null - && card._serverCfg.spaces[0].openings[0].host == null; + && wallHostResolves(card._serverCfg.spaces[0], card._serverCfg.spaces[0].openings[0]); await card._undoPlanOptimization(); await card.updateComplete; result.undoRestoresHostedPartition = JSON.stringify(card._serverCfg.spaces[0].partitions) @@ -113,7 +117,7 @@ const out = await page.evaluate(async () => { const finalSpace = card._serverCfg.spaces[0]; result.thicknessChangesSingleBody = finalSpace.partitions == null && finalSpace.walls.length === 1 && finalSpace.walls[0].cm === 10 - && finalSpace.openings[0].host == null; + && wallHostResolves(finalSpace, finalSpace.openings[0]); return result; }); diff --git a/demo/smoke_resize_outer_reconciliation.mjs b/demo/smoke_resize_outer_reconciliation.mjs index 6f87b94b..15b600c3 100644 --- a/demo/smoke_resize_outer_reconciliation.mjs +++ b/demo/smoke_resize_outer_reconciliation.mjs @@ -14,6 +14,8 @@ const out = await page.evaluate(async (fixture) => { const result = {}; const card = window.__card; const copy = (value) => JSON.parse(JSON.stringify(value)); + const wallHostResolves = (space, opening) => opening.host?.kind === 'wall' + && (space.wall_segments || []).some((segment) => segment.id === opening.host.id); let serverConfig = copy(fixture); let serverLayout = {}; const sent = []; @@ -82,21 +84,22 @@ const out = await page.evaluate(async (fixture) => { card._openAlignDialog(); await card.updateComplete; + const previewSpace = card._alignDialog?.config?.spaces?.[0]; result.previewProvesOuterRewrite = card._alignDialog?.changed === true && card._alignDialog?.preflight?.ok === true && card._alignDialog?.report?.partitionsReconciled === 3 && card._alignDialog?.report?.openingsRehosted === 2 - && card._alignDialog?.config?.spaces?.[0]?.partitions == null - && card._alignDialog?.config?.spaces?.[0]?.openings?.every((opening) => opening.host == null); + && card._alignDialog?.config?.model_version === 8 + && previewSpace?.partitions == null + && previewSpace?.openings?.every((opening) => wallHostResolves(previewSpace, opening)); await card._runAlignToGrid(); await card.updateComplete; result.optimizeUsesOneWrite = sent.filter((type) => type === 'houseplan/plan/optimize').length === 1; const optimized = card._serverCfg.spaces[0]; result.optimizePreservesOpeningFields = optimized.partitions == null - && optimized.openings[0].host == null + && optimized.openings.every((opening) => wallHostResolves(optimized, opening)) && optimized.openings[0].cover === 'cover.left' && optimized.openings[0].future_field?.keep === 'left' - && optimized.openings[1].host == null && optimized.openings[1].contact === 'binary_sensor.right'; await resetResize(); diff --git a/demo/smoke_resize_virtual_thick.mjs b/demo/smoke_resize_virtual_thick.mjs index 8ed2b7df..42c65973 100644 --- a/demo/smoke_resize_virtual_thick.mjs +++ b/demo/smoke_resize_virtual_thick.mjs @@ -150,8 +150,22 @@ const res = await page.evaluate(async () => { && c._openCuts().length === 1; c._closeOpenSpan(c._openCuts()[0]); await upd(); + const restoredPieces = sp().walls || []; + const restoredLength = restoredPieces.reduce((sum, wall) => sum + Math.hypot( + wall.b[0] - wall.a[0], wall.b[1] - wall.a[1], + ) * 1000, 0); + const orderedRestored = restoredPieces.slice().sort((a, b) => a.a[1] - b.a[1]); + const restoredContinuous = orderedRestored.length > 0 + && Math.abs(orderedRestored[0].a[1] * 1000 - 140) < 1e-6 + && Math.abs(orderedRestored.at(-1).b[1] * 1000 - 460) < 1e-6 + && orderedRestored.every((wall, index) => Math.abs(wall.a[0] - 0.55) < 1e-9 + && Math.abs(wall.b[0] - 0.55) < 1e-9 + && (!index || Math.abs(wall.a[1] - orderedRestored[index - 1].b[1]) < 1e-9)); out.closeRejoinsUniformWall = !sp().open_spans - && (sp().walls || []).length === 1 + && restoredPieces.length > 0 + && restoredPieces.every((wall) => wall.cm === 20) + && restoredContinuous + && Math.abs(restoredLength - 320) < 1e-6 && c._intervalCm(restoredCut) === 20; } else { out.adjacentSpansPersistAsOne = false; @@ -166,6 +180,8 @@ const res = await page.evaluate(async () => { { id: 'tb', name: 'B', poly: [[0.5, 0.5], [0.9, 0.5], [0.9, 0.9], [0.5, 0.9]] }, { id: 'tc', name: 'C', poly: [[0.5, 0.1], [0.9, 0.1], [0.9, 0.5], [0.5, 0.5]] }, ]; + c._serverCfg.model_version = 7; + delete sp().wall_segments; delete sp().walls; delete sp().open_spans; c._tool = 'wallthick'; @@ -200,6 +216,8 @@ const res = await page.evaluate(async () => { { id: 'middle', name: 'Middle', poly: [[0.3, 0.5], [0.6, 0.5], [0.6, 0.9], [0.3, 0.9]] }, { id: 'right', name: 'Right', poly: [[0.6, 0.5], [0.9, 0.5], [0.9, 0.9], [0.6, 0.9]] }, ]; + c._serverCfg.model_version = 7; + delete sp().wall_segments; sp().walls = [{ key: '0.600000,0.500000@0.0000', cm: 22, a: [0.3, 0.5], b: [0.9, 0.5], @@ -208,9 +226,7 @@ const res = await page.evaluate(async () => { delete sp().openings; c._tool = 'boundary'; await upd(); - const atomicParentBefore = JSON.stringify({ - rooms: sp().rooms, walls: sp().walls, open_spans: sp().open_spans, - }); + const atomicParentBefore = JSON.stringify(sp().open_spans); const atomicParentCut = c._openCuts()[0]; const atomicParentPlan = atomicParentCut && c._planClosedOpenSpan(atomicParentCut); out.atomicParentClosePreviewInherits = atomicParentPlan?.cm === 22; @@ -220,9 +236,10 @@ const res = await page.evaluate(async () => { && c._intervalCm([100, 500, 300, 500]) === 22; c._undoGeometry(); await upd(); - out.atomicParentCloseUndo = JSON.stringify({ - rooms: sp().rooms, walls: sp().walls, open_spans: sp().open_spans, - }) === atomicParentBefore; + out.atomicParentCloseUndo = JSON.stringify(sp().open_spans) === atomicParentBefore + && c._intervalCm([100, 500, 300, 500]) === 0 + && c._intervalCm([300, 500, 600, 500]) === 22 + && c._intervalCm([600, 500, 900, 500]) === 22; return out; }); diff --git a/demo/smoke_sun.mjs b/demo/smoke_sun.mjs index 6dcf55d8..664ac35c 100644 --- a/demo/smoke_sun.mjs +++ b/demo/smoke_sun.mjs @@ -218,12 +218,14 @@ const rayInfo = () => page.evaluate(() => { const c = window.__card; const rays = c._sunRaysCache?.rays || []; const r = rays.find((q) => q.openingId === 'wE'); + const space = c._serverCfg.spaces.find((s) => s.id === 'f1'); + const opening = space.openings.find((o) => o.id === 'wE'); return { ids: rays.map((q) => q.openingId).sort(), midY: r ? (r.a[1] + r.b[1]) / 2 : NaN, minX: r ? Math.min(...r.polys.flat().map((p) => p[0])) : NaN, rev: c._cfgRev, - wEy: c._serverCfg.spaces.find((s) => s.id === 'f1').openings.find((o) => o.id === 'wE').y, + wEy: opening.y, }; }); @@ -276,8 +278,9 @@ check('b701_ray_not_stale', Math.abs(moved.midY - 600) > 50, true); const clipped = await page.evaluate(async () => { const c = window.__card; const sp = c._serverCfg.spaces.find((s) => s.id === 'f1'); + const before = c._geometrySnapshot(); sp.rooms.find((r) => r.id === 'r3').poly = [[0.90, 0.46], [0.96, 0.46], [0.96, 0.86], [0.90, 0.86]]; - c._saveConfig(); // the one true local-save entry point + c._commitPhysicalGeometry('sun smoke room edit', before); c.requestUpdate(); await c.updateComplete; return true; }); @@ -321,10 +324,12 @@ const thr = await page.evaluate(async () => { // either absent or at FULL strength, and crossing 3° fades the whole LAYER // (never the geometry) in or out over exactly two seconds await setSun(90, 10); - out.aboveThresholdDrawn = domPolys().length > 0 && !layer().classList.contains('out'); + out.aboveThresholdDrawn = domPolys().length > 0 && !layer()?.classList.contains('out'); { - const cs = getComputedStyle(layer()); - out.fadeInTakesTwoSeconds = cs.animationName === 'hp-sunfade-in' && cs.animationDuration === '2s'; + const current = layer(); + const cs = current && getComputedStyle(current); + out.fadeInTakesTwoSeconds = !!cs + && cs.animationName === 'hp-sunfade-in' && cs.animationDuration === '2s'; } out.fullAlphaWellAboveThreshold = Math.abs(stopAlpha() - 0.3) < 1e-6; await setSun(90, 3.2); @@ -346,7 +351,7 @@ const thr = await page.evaluate(async () => { await setSun(90, 1); out.coldStartBelowThresholdEmpty = domPolys().length === 0 && !layer(); await setSun(90, 5); - out.backAboveThreshold = domPolys().length > 0 && !layer().classList.contains('out'); + out.backAboveThreshold = domPolys().length > 0 && !layer()?.classList.contains('out'); return out; }); Object.assign(res, thr); diff --git a/demo/smoke_wall_thickness.mjs b/demo/smoke_wall_thickness.mjs index d31b9b3f..33414d19 100644 --- a/demo/smoke_wall_thickness.mjs +++ b/demo/smoke_wall_thickness.mjs @@ -13,6 +13,34 @@ const res = await page.evaluate(async () => { const sr = () => c.shadowRoot || c.renderRoot; const upd = async () => { c._cfgEpoch++; c.requestUpdate(); await c.updateComplete; }; const sp = () => c._serverCfg.spaces.find((s) => s.id === c._space); + const hostContourOpenings = () => { + for (const opening of sp().openings || []) { + if (opening.host) continue; + const centre = [Number(opening.x), Number(opening.y)]; + const candidates = (sp().wall_segments || []).filter((segment) => { + const dx = segment.b[0] - segment.a[0], dy = segment.b[1] - segment.a[1]; + const len2 = dx * dx + dy * dy; + if (!(len2 > 0)) return false; + const t = ((centre[0] - segment.a[0]) * dx + + (centre[1] - segment.a[1]) * dy) / len2; + const q = [segment.a[0] + dx * t, segment.a[1] + dy * t]; + const angle = Math.atan2(dy, dx) * 180 / Math.PI; + const delta = Math.abs((((angle - Number(opening.angle)) % 180) + 270) % 180 - 90); + const half = Number(opening.length) / 2; + const len = Math.sqrt(len2); + return t >= 0 && t <= 1 && Math.hypot(centre[0] - q[0], centre[1] - q[1]) < 1e-8 + && Math.min(delta, Math.abs(180 - delta)) < 1e-6 + && t * len >= half - 1e-9 && (1 - t) * len >= half - 1e-9; + }); + if (candidates.length === 1) { + const segment = candidates[0]; + const dx = segment.b[0] - segment.a[0], dy = segment.b[1] - segment.a[1]; + const t = ((centre[0] - segment.a[0]) * dx + + (centre[1] - segment.a[1]) * dy) / (dx * dx + dy * dy); + opening.host = { kind: 'wall', id: segment.id, t }; + } + } + }; const floorAreaOf = (id) => { const rooms = c._spaceModel().rooms; const walls = sp().walls || []; @@ -76,6 +104,7 @@ const res = await page.evaluate(async () => { sp().openings = [{ id: 'wtSun', type: 'window', x: 0.04, y: 0.25, angle: 90, length: 0.09, }]; + hostContourOpenings(); sp().settings = { ...(sp().settings || {}), north_deg: 0, sun_rays: true }; c.hass = { ...c.hass, states: { ...c.hass.states, 'sun.sun': { entity_id: 'sun.sun', state: 'above_horizon', @@ -107,6 +136,7 @@ const res = await page.evaluate(async () => { sp().openings = [{ id: 'wt1', type: 'door', x: 0.55, y: 0.25, angle: 90, length: 0.09, flip_v: false, }]; + hostContourOpenings(); await upd(); out.doorDrawn = !!sr().querySelector('[data-hp="opening"][data-kind="door"]'); const bodyD = shared[0]?.getAttribute('d') || sr().querySelector('[data-hp="wall"]')?.getAttribute('d') || ''; @@ -133,6 +163,7 @@ const res = await page.evaluate(async () => { { id: 'wtGateFlipped', type: 'gate', x: 0.55, y: 0.30, angle: 90, length: 0.08, flip_v: true }, ]; + hostContourOpenings(); await upd(); const sharedGateDefault = firstGateTurn('wtGateDefault'); const sharedGateFlipped = firstGateTurn('wtGateFlipped'); @@ -158,6 +189,7 @@ const res = await page.evaluate(async () => { sp().openings = [{ id: 'wt1', type: 'door', x: 0.55, y: 0.25, angle: 90, length: 0.09, flip_v: true, }]; + hostContourOpenings(); await upd(); sp().settings = { ...(sp().settings || {}), hide_openings: true, show_borders: true }; diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 062a8351..fc86432b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -25,7 +25,9 @@ An invalid open passage also retains its specific actionable error instead of being masked by the generic wall-model migration blocker. The compact space card keeps wall-hosted doors, windows, gates and passages visible after - the migration as well. + the migration as well. Wall-hosted windows keep casting sunlight after an + edit, partial open boundaries remain writable on migrated plans, and clearing + wall thickness no longer restores its previous value. Ambiguous legacy geometry is left unchanged with a recovery prompt. No Plan editor controls or zero-thickness-wall behaviour changed diff --git a/docs/CHANGELOG.ru.md b/docs/CHANGELOG.ru.md index 076877d7..b89673ae 100755 --- a/docs/CHANGELOG.ru.md +++ b/docs/CHANGELOG.ru.md @@ -30,6 +30,9 @@ открытый проход по-прежнему получает своё точное сообщение об ошибке, а не общий блокер миграции стен. Компактная карточка пространства также не теряет после миграции двери, окна, ворота и проходы, привязанные к стенам. + Окна на стенах продолжают пропускать солнечные лучи после правки, частичные + открытые границы сохраняются и на уже мигрированных планах, а удалённая + толщина стены больше не восстанавливается в прежнее значение. Неоднозначная старая геометрия остаётся без изменений с подсказкой по восстановлению. Кнопки редактора плана и поведение стен нулевой толщины не менялись diff --git a/src/houseplan-card.ts b/src/houseplan-card.ts index ef32adaa..2e283587 100755 --- a/src/houseplan-card.ts +++ b/src/houseplan-card.ts @@ -7293,7 +7293,11 @@ class HouseplanCard extends LitElement { return this._t(`wall_model.reason.${reason}`); } - private _commitPhysicalGeometry(name: string, before: SpaceGeometryState | null): boolean { + private _commitPhysicalGeometry( + name: string, + before: SpaceGeometryState | null, + additionalAuthoredPoints: readonly (readonly number[])[] = [], + ): boolean { if (!before || !this._serverCfg) return false; const liveCandidate = this._serverCfg; const editedState = this._geometrySnapshotFromConfig(liveCandidate, before.spaceId); @@ -7360,6 +7364,7 @@ class HouseplanCard extends LitElement { const authoredPoints = this._path.length >= 2 ? this._path.map((point) => [point[0] / NORM_W, point[1] / NORM_W]) : []; + authoredPoints.push(...additionalAuthoredPoints.map((point) => [point[0], point[1]])); safe = wallModelOffGridValueCount(afterSpace) <= wallModelOffGridValueCount(historyBefore, authoredPoints) && this._checkSpacePhysicalGeometry(committedCandidate, before.spaceId).ok; @@ -11650,7 +11655,10 @@ class HouseplanCard extends LitElement { sp.openings = purgeOpeningsOnSpan(sp.openings, sg, NORM_W, this._gridPitch * 6); const removedOpenings = (sp.openings || []).length < beforeOp; this._persistOpenCuts(next); - if (this._commitPhysicalGeometry(this._t('history.open_boundary'), before)) { + if (this._commitPhysicalGeometry(this._t('history.open_boundary'), before, [ + [sg[0] / NORM_W, sg[1] / NORM_W], + [sg[2] / NORM_W, sg[3] / NORM_W], + ])) { if (removedOpenings) this._showToast(this._t('toast.openwall_openings_removed')); this._showToast(this._t('toast.boundary_opened')); } @@ -11994,6 +12002,15 @@ class HouseplanCard extends LitElement { next = setWallThickness(sp.walls, d.a, d.b, cm, this._wallKeyPitch, NORM_W); } next = this._normalizeWalls(next, openCuts); + // ADR 282 Stage 1: this writer knows that absence from `next` is an + // explicit zero chosen by the user, not a missing compatibility record. + // Mirror that intent into the canonical catalogue before the common + // barrier; otherwise its lineage fallback would resurrect the old cm. + for (const segment of sp.wall_segments || []) { + segment.cm = thicknessCmAt( + next, segment.a, segment.b, GRID_STEP_N, 1, + ); + } if (next.length) sp.walls = next; else delete sp.walls; this._wallDialog = null; @@ -15448,8 +15465,10 @@ class HouseplanCard extends LitElement { .map((r) => ({ id: r.id || '', poly: roomPoly(r) })) .filter((r): r is { id: string; poly: number[][] } => !!r.id && !!r.poly); const windows = this._openingsR - // An independent-wall window is not an exterior sun source (#132). - .filter((o) => o.type === 'window' && !o.host) + // A contour-wall host is stable identity metadata, not a different + // physical carrier. Only an independent partition window is excluded + // from exterior sunlight (#132, ADR 282 Stage 1). + .filter((o) => o.type === 'window' && o.host?.kind !== 'partition') .map((o) => ({ id: o.id, x: o.rx, y: o.ry, angle: o.angle, length: o.rlen })); const walls = this._spaceWalls; const openCuts = this._openPairs().flatMap((p) => p.segs); diff --git a/test/wall-segment-model.test.mjs b/test/wall-segment-model.test.mjs index 18379d92..7e6f5b90 100644 --- a/test/wall-segment-model.test.mjs +++ b/test/wall-segment-model.test.mjs @@ -84,6 +84,23 @@ test('v7 rectangle migrates atomically to the complete id catalogue and is idemp assert.deepEqual(second.config, first.config); }); +test('an explicit canonical zero is not resurrected from the old thickness projection', () => { + const base = commitWallSegmentModel(configOf({ + id: 'floor', title: 'Floor', rooms: [rectangle('room')], + walls: [{ + key: wallKey([0, 0], [1, 0], GRID_STEP_N), + a: [0, 0], b: [1, 0], cm: 20, + }], + })).config; + const edited = structuredClone(base); + const topId = edited.spaces[0].rooms[0].wall_ids[0]; + edited.spaces[0].wall_segments.find((segment) => segment.id === topId).cm = 0; + delete edited.spaces[0].walls; + const result = commitWallSegmentModel(edited).config.spaces[0]; + assert.equal(result.wall_segments.find((segment) => segment.id === topId).cm, 0); + assert.equal(result.walls, undefined); +}); + test('partial shared side is atomised once and every atom has one or two owners', () => { const result = commitWallSegmentModel(configOf({ id: 'floor', title: 'Floor',