mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 04:09:17 +00:00
@@ -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')
|
||||
));
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
+11
-6
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
открытый проход по-прежнему получает своё точное сообщение об ошибке, а не
|
||||
общий блокер миграции стен. Компактная карточка пространства также не теряет
|
||||
после миграции двери, окна, ворота и проходы, привязанные к стенам.
|
||||
Окна на стенах продолжают пропускать солнечные лучи после правки, частичные
|
||||
открытые границы сохраняются и на уже мигрированных планах, а удалённая
|
||||
толщина стены больше не восстанавливается в прежнее значение.
|
||||
Неоднозначная старая геометрия остаётся без изменений с подсказкой по
|
||||
восстановлению. Кнопки редактора плана и поведение стен нулевой толщины не
|
||||
менялись
|
||||
|
||||
+23
-4
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user