fix: close plan repair review findings

Issue: #228
User-Visible: yes
This commit is contained in:
Sergey Matyunin
2026-08-22 10:56:38 +03:00
parent 8fc3ec783b
commit 691cea074b
9 changed files with 91 additions and 24 deletions
File diff suppressed because one or more lines are too long
+29
View File
@@ -87,6 +87,35 @@ const out = await page.evaluate(async () => {
card._markupClick(clickAt(300, 300, true));
result.shiftBypassesExistingFaceOffer = card._path.length === 1 && !card._roomDialog;
card._cancelPath();
const largeGap = card._cmToUnits(3) / 1000;
const alternativeX = 0.1 + card._cmToUnits(1) / 1000;
await reset({ partitions: [
...ring(largeGap),
{ id: 'alternative', a: [alternativeX, 0.1], b: [alternativeX, 0.08], cm: 20 },
] });
card._markupClick(clickAt(300, 300));
result.ambiguousLargeGapUsesWallsFlow = card._path.length === 1
&& !card._roomDialog && !card._wallRepairDiagnostic && !card._toast;
await reset({
partitions: ring(card._cmToUnits(1.2) / 1000),
openings: [{
id: 'hosted-gap', type: 'door', x: 0.1, y: 0.3, angle: 90, length: 0.08,
host: { kind: 'partition', id: 'left', t: 0.5 },
}],
});
const hostedBefore = JSON.stringify(card._curSpaceCfg);
const hostedOffered = card._offerExistingWallFace([300, 300]);
card._nameSel = 'Must not repair';
card._saveRoom();
await update();
result.hostedOpeningBlocksRepair = hostedOffered
&& JSON.stringify(card._curSpaceCfg) === hostedBefore
&& !card._roomDialog && !card._wallFaceBatch
&& card._toast === card._t('toast.wall_repair_changed');
await reset({ partitions: ring() });
card._offerExistingWallFace([300, 300]);
card._nameSel = 'Existing face';
card._saveRoom();
File diff suppressed because one or more lines are too long
+3 -3
View File
File diff suppressed because one or more lines are too long
+4 -2
View File
@@ -7,8 +7,10 @@
and Shift accepts only an exact 45° ray endpoint or wall intersection. A
click inside an already closed empty wall contour can create its room; one
unambiguous gap up to 2 cm is repaired only together with that confirmed
room. Deleting a room now explicitly offers to keep its exclusive physical
walls or delete them, while shared walls and their openings survive
room; ambiguous wider gaps remain ordinary drawing, and a wall carrying a
hosted opening is never moved by repair. Deleting a room now explicitly
offers to keep its exclusive physical walls or delete them, while shared
walls and their openings survive
([#228](https://github.com/Matysh/houseplan-card/issues/228)).
- Resizing a room now reports the distance between wall faces, the number a tape
+5 -3
View File
@@ -13,9 +13,11 @@
неразличимые узлы просят увеличить масштаб, а `Shift` принимает только точную
конечную точку или пересечение на луче с шагом 45°. Клик внутри уже готового
пустого контура стен позволяет создать комнату; единственный однозначный
разрыв до 2 см исправляется только вместе с подтверждённой комнатой. При
удалении комнаты теперь можно явно оставить её физические стены либо удалить
их; общие стены и их проёмы сохраняются
разрыв до 2 см исправляется только вместе с подтверждённой комнатой. Более
широкий неоднозначный разрыв остаётся обычным рисованием, а стена с явно
привязанным проёмом repair-механикой не сдвигается. При удалении комнаты
теперь можно явно оставить её физические стены либо удалить их; общие стены
и их проёмы сохраняются
([#228](https://github.com/Matysh/houseplan-card/issues/228)).
- При изменении размера комнаты показываются внутренние размеры — расстояние от
+8 -9
View File
@@ -163,7 +163,9 @@ import {
type WallFaceGraph, type WallGraphFace, type WallGraphSourceSegment,
} from './wall-face-graph';
import { parameterOnPartition, planRoomDeletion } from './room-deletion';
import { planWallFaceRepair, type WallFaceRepairProposal } from './wall-face-repair';
import {
planWallFaceRepair, repairMovesHostedPartition, type WallFaceRepairProposal,
} from './wall-face-repair';
import {
LightSegment, polygonSegments, splitAtIntersections, visibilityPolygon,
} from './light-visibility';
@@ -7680,9 +7682,8 @@ class HouseplanCard extends LitElement {
gridStep: this._gridPitch,
epsilon,
});
if (diagnostic.kind !== 'none') {
this._wallRepairDiagnostic = diagnostic.kind === 'repair'
? diagnostic.proposal : diagnostic.proposals[0] || null;
if (diagnostic.kind === 'repair') {
this._wallRepairDiagnostic = diagnostic.proposal;
this._showToast(this._t('toast.wall_repair_too_large'));
return true;
}
@@ -12713,11 +12714,9 @@ class HouseplanCard extends LitElement {
if (distToSegment(proposal.to, [target.a[0], target.a[1], target.b[0], target.b[1]]) > epsilon) {
return false;
}
if (proposal.sourceKey.startsWith('static:partition|')) {
const id = proposal.sourceKey.slice('static:partition|'.length).split('|')[0];
if ((this._curSpaceCfg as any)?.openings?.some((opening: OpeningCfg) =>
opening.host?.kind === 'partition' && opening.host.id === id)) return false;
}
if (repairMovesHostedPartition(
proposal, (this._curSpaceCfg as any)?.openings || [],
)) return false;
return true;
}
+15
View File
@@ -26,6 +26,21 @@ export interface WallFaceRepairOptions {
requiredSourceKey?: string;
}
export interface WallRepairHostedOpening {
host?: { kind?: string; id?: string };
}
/** A hosted opening makes its persisted partition endpoint immovable. */
export function repairMovesHostedPartition(
proposal: Pick<WallFaceRepairProposal, 'sourceKey'>,
openings: readonly WallRepairHostedOpening[],
): boolean {
if (!proposal.sourceKey.startsWith('static:partition|')) return false;
const id = proposal.sourceKey.slice('static:partition|'.length).split('|')[0];
return !!id && openings.some((opening) =>
opening.host?.kind === 'partition' && opening.host.id === id);
}
const movable = (source: WallGraphSourceSegment): boolean => !source.key.startsWith('static:room|');
function projection(
+21 -1
View File
@@ -1,6 +1,8 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { planWallFaceRepair } from '../test-build/wall-face-repair.js';
import {
planWallFaceRepair, repairMovesHostedPartition,
} from '../test-build/wall-face-repair.js';
const ringWithGap = (gap) => [
{ a: [0, 0], b: [100, 0], key: 'static:partition|top|0' },
@@ -48,3 +50,21 @@ test('multiple valid closures fail closed instead of choosing record order', ()
});
assert.notEqual(result.kind, 'repair');
});
test('a hosted opening makes its partition an invalid repair mover', () => {
const proposal = {
sourceKey: 'static:partition|left|0', endpoint: 'b',
from: [0, 1.2], to: [0, 0], targetSourceKey: 'static:partition|top|0',
targetKind: 'endpoint', distance: 1.2,
};
assert.equal(repairMovesHostedPartition(proposal, [
{ host: { kind: 'partition', id: 'left' } },
]), true);
assert.equal(repairMovesHostedPartition(proposal, [
{ host: { kind: 'partition', id: 'right' } },
{ host: { kind: 'room', id: 'left' } },
]), false);
assert.equal(repairMovesHostedPartition({ ...proposal, sourceKey: 'active:session:0' }, [
{ host: { kind: 'partition', id: 'left' } },
]), false);
});