fix: preserve authored wall paths across identity barrier

Issue: #282
User-Visible: yes
This commit is contained in:
Matysh
2026-08-26 02:21:15 +03:00
parent 71ac4a6639
commit 07402d64cc
5 changed files with 28 additions and 5 deletions
+4 -1
View File
@@ -19,7 +19,10 @@
atomically on their first structural edit or through **Optimize plans**;
Resize, Undo/Redo and repeated edits preserve the same identities without
accumulating off-grid coordinates. The backend Optimize transaction also
enforces this upgrade when invoked directly. Ambiguous legacy geometry is
enforces this upgrade when invoked directly. An authored wall chain remains
valid when it becomes a room or independent walls, while a rejected
transaction stores no partial result and no longer reports false success.
Ambiguous legacy geometry is
left unchanged with a recovery prompt. No Plan editor controls or
zero-thickness-wall behaviour changed
([#282](https://github.com/Matysh/houseplan-card/issues/282)).
+3 -1
View File
@@ -24,7 +24,9 @@
изменении или через **«Оптимизировать планы»**; Resize, отмена/повтор и
последующие правки сохраняют те же ID без накопления координат вне сетки;
серверная транзакция Optimize также гарантирует это обновление при прямом
вызове.
вызове. Принятая пользователем цепочка остаётся допустимой при преобразовании
в комнату или независимые стены, а отклонённая транзакция не записывает
частичный результат и не показывает ложное сообщение об успехе.
Неоднозначная старая геометрия остаётся без изменений с подсказкой по
восстановлению. Кнопки редактора плана и поведение стен нулевой толщины не
менялись
+10 -2
View File
@@ -7353,7 +7353,15 @@ class HouseplanCard extends LitElement {
let safe = false;
try {
const afterSpace = committedCandidate.spaces.find((space: any) => space.id === before.spaceId);
safe = wallModelOffGridValueCount(afterSpace) <= wallModelOffGridValueCount(historyBefore)
// A completed point of the active, grid-snapped chain is authored input,
// even before its first draft record exists in `before`. Include that
// transient carrier in the growth baseline; later conversion from draft
// to room/partition must not count the same coordinate as newly derived.
const authoredPoints = this._path.length >= 2
? this._path.map((point) => [point[0] / NORM_W, point[1] / NORM_W])
: [];
safe = wallModelOffGridValueCount(afterSpace)
<= wallModelOffGridValueCount(historyBefore, authoredPoints)
&& this._checkSpacePhysicalGeometry(committedCandidate, before.spaceId).ok;
} catch {
safe = false;
@@ -13692,7 +13700,7 @@ class HouseplanCard extends LitElement {
else delete sp.walls;
}
this._commitPhysicalGeometry(this._t('history.wall_face_batch'), before);
if (!this._commitPhysicalGeometry(this._t('history.wall_face_batch'), before)) return;
delete this._resumeDraftBySpace[this._space];
this._path = [];
this._activeDraftId = null;
+6 -1
View File
@@ -80,7 +80,9 @@ const canonicalSpan = (a: number[], b: number[]): [Point, Point] => (
const lengthOf = (a: number[], b: number[]): number => Math.hypot(b[0] - a[0], b[1] - a[1]);
/** Unique authored/derived contour coordinates that are materially off-grid. */
export const wallModelOffGridValueCount = (space: any): number => {
export const wallModelOffGridValueCount = (
space: any, additionalPoints: readonly (readonly number[])[] = [],
): number => {
const values = new Set<string>();
const inspect = (value: unknown): void => {
const number = Number(value);
@@ -101,6 +103,9 @@ export const wallModelOffGridValueCount = (space: any): number => {
inspect(segment?.b?.[0]); inspect(segment?.b?.[1]);
}
}
for (const additionalPoint of additionalPoints) {
inspect(additionalPoint?.[0]); inspect(additionalPoint?.[1]);
}
return values.size;
};
const projectT = (p: number[], a: number[], b: number[]): number => {
+5
View File
@@ -144,6 +144,11 @@ test('off-grid contour guard counts values once across compatibility projections
wall_segments: [{ id: 'wall', a: [point, 0], b: [point, 1], cm: 20 }],
walls: [{ key: 'legacy', a: [point, 0], b: [point, 1], cm: 20 }],
}), 1);
assert.equal(
wallModelOffGridValueCount({ rooms: [] }, [[point, 0], [point, 1]]),
1,
'a transient authored path contributes its coordinates to the baseline once',
);
});
test('room openings acquire a stable wall host while partition hosts stay untouched', () => {