mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
fix: a room side and a live draft, seen in the right coordinates (#229 r1 H1,H2)
**High-1.** Комнаты хранятся в тех же координатах, что и перегородки: `roomPoly` отдаёт сырой полигон конфига. Обе обвязки делили его на `NORM_W` ещё раз, комната уезжала в область ~0.0001, и `junctionAt` не находил ни одного совпадения. Узел на T-стыке к середине стены комнаты — тот самый случай, ради которого ТЗ прошло два раунда ревью, — молча исчезал. Воспроизведено вызовом `optimizePlans`: `partitionsMerged === 1` там, где ожидается 0. **High-2.** Завершаемая цепочка к моменту слияния ещё лежит в `room_drafts`: каждый клик персистит её через `_persistActiveDraftSegment`, а удаляется черновик строкой ниже вызова слияния. Собственные концы цепочки считались чужим примыканием, и стык с существующей стеной не срастался. Активный черновик теперь исключается — ровно так же, как это делает `plan-snap-overlay` (`activeDraftId`). Дыры в тестах, которые это пропустили, закрыты по существу, а не заплаткой: - `demo/smoke_wall_chain_merge.mjs` рисует продолжение реальными кликами через `_markupClick`, а не присваиванием `_path`, — то есть исполняет тот путь, на котором дефект и жил. Клики задаются в координатах плана и переводятся через живой view box, иначе смок целится мимо только что нарисованной стены. - `test/plan-optimizer.test.mjs` получил комнату с примыканием к середине стороны: юниты модуля этого не ловили, потому что передают полигон уже в согласованном масштабе, минуя обвязку. - Мутанты `partition-merge-rescales-rooms` и `chain-merge-sees-own-draft` сторожат оба места: проверены применением патча, 1 и 2 падения. Issue: #229 User-Visible: no
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -44,9 +44,26 @@ const res = await page.evaluate(async () => {
|
||||
out.cornerKeepsBothRecords = partitionCount() - beforeCorner === 2;
|
||||
|
||||
// Drawing onto an existing wall merges with it — the seam belongs to the chain.
|
||||
// Real clicks, not an assigned `_path`: each click persists the chain into
|
||||
// `room_drafts`, and the merge used to mistake the chain's own ends for a
|
||||
// foreign junction (review CODE-REVIEW-229-r1, High-2).
|
||||
const stage = c.shadowRoot.querySelector('svg');
|
||||
// Clicks are given in plan coordinates and converted through the live view
|
||||
// box, so the smoke aims at the wall it just drew and not at a screen guess.
|
||||
const click = async (px, py) => {
|
||||
const rect = stage.getBoundingClientRect();
|
||||
const view = c._viewOr(c._baseVb());
|
||||
stage.dispatchEvent(new MouseEvent('click', {
|
||||
clientX: rect.left + ((px - view.x) / view.w) * rect.width,
|
||||
clientY: rect.top + ((py - view.y) / view.h) * rect.height,
|
||||
bubbles: true, composed: true,
|
||||
}));
|
||||
await settle();
|
||||
};
|
||||
const beforeTouch = partitionCount();
|
||||
c._path = [[560, 500], [700, 500]];
|
||||
c._draftSegmentCms = [];
|
||||
await click(560, 500);
|
||||
await click(700, 500);
|
||||
out.clicksPersistTheDraft = ((space().room_drafts || []).length) === 1;
|
||||
c._finishWallChain();
|
||||
await settle();
|
||||
out.chainMergesIntoTheWallItTouches = partitionCount() === beforeTouch;
|
||||
@@ -54,6 +71,7 @@ const res = await page.evaluate(async () => {
|
||||
(p) => Math.abs(p.a[1] * 1000 - 500) < 0.01 && Math.abs(p.b[1] * 1000 - 500) < 0.01,
|
||||
);
|
||||
out.touchedWallGrew = !!extended && Math.abs(extended.b[0] * 1000 - 700) < 0.01;
|
||||
out.draftIsGone = !(space().room_drafts || []).length;
|
||||
|
||||
return out;
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
Vendored
+3
-3
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"fixture": "synthetic-only",
|
||||
"sourceFingerprint": "d163d1ae7af291ebbcfe69419b5cf0bdcd20d1930ba79c72651b8c682451b7d2",
|
||||
"sourceFingerprint": "afa20fd81ac20e56571c5fc16e5f881b9fac8ead93f923b52d132c853f55cc48",
|
||||
"captureScriptSha256": "34f2219790d46efd8250e7a1bd829cb8fc0b0547e1260635fefa52407551b41b",
|
||||
"command": "npm run build && node demo/docs/capture.mjs",
|
||||
"scenarios": {
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "d163d1ae7af291ebbcfe69419b5cf0bdcd20d1930ba79c72651b8c682451b7d2",
|
||||
"sourceSha256": "afa20fd81ac20e56571c5fc16e5f881b9fac8ead93f923b52d132c853f55cc48",
|
||||
"imageSha256": "d7ce576f5a70b1977277e15985bb409f40f205f6016eeebc62f76a4394effdb4"
|
||||
},
|
||||
"view-touch": {
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "d163d1ae7af291ebbcfe69419b5cf0bdcd20d1930ba79c72651b8c682451b7d2",
|
||||
"sourceSha256": "afa20fd81ac20e56571c5fc16e5f881b9fac8ead93f923b52d132c853f55cc48",
|
||||
"imageSha256": "62c13b7f9dc6c0576735a727a1988bc35b9b28fc2a9d34e9a746a4dc3e01b28a"
|
||||
},
|
||||
"space-create": {
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "d163d1ae7af291ebbcfe69419b5cf0bdcd20d1930ba79c72651b8c682451b7d2",
|
||||
"sourceSha256": "afa20fd81ac20e56571c5fc16e5f881b9fac8ead93f923b52d132c853f55cc48",
|
||||
"imageSha256": "a73bb2301677752c7980712af554d40f3fd6077944249677245c8135d325b1b9"
|
||||
},
|
||||
"room-contour-close": {
|
||||
@@ -46,7 +46,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "d163d1ae7af291ebbcfe69419b5cf0bdcd20d1930ba79c72651b8c682451b7d2",
|
||||
"sourceSha256": "afa20fd81ac20e56571c5fc16e5f881b9fac8ead93f923b52d132c853f55cc48",
|
||||
"imageSha256": "8f234c7750809cbc75b3ce364dc2d3b55eebacd4b2b5e42b223d6e2cee9bb4ac"
|
||||
},
|
||||
"plan-context-tray": {
|
||||
@@ -57,7 +57,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "d163d1ae7af291ebbcfe69419b5cf0bdcd20d1930ba79c72651b8c682451b7d2",
|
||||
"sourceSha256": "afa20fd81ac20e56571c5fc16e5f881b9fac8ead93f923b52d132c853f55cc48",
|
||||
"imageSha256": "3eee486fe5cc226e8b3c19c4035ebb279087432a69959d24b7edabc59d848582"
|
||||
},
|
||||
"device-editor": {
|
||||
@@ -68,7 +68,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "d163d1ae7af291ebbcfe69419b5cf0bdcd20d1930ba79c72651b8c682451b7d2",
|
||||
"sourceSha256": "afa20fd81ac20e56571c5fc16e5f881b9fac8ead93f923b52d132c853f55cc48",
|
||||
"imageSha256": "a6360309a9a6125bdc2dfd411463d281af2f21a09b07e58eae1fc23d7371b8e5"
|
||||
},
|
||||
"device-display-preview": {
|
||||
@@ -79,7 +79,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "d163d1ae7af291ebbcfe69419b5cf0bdcd20d1930ba79c72651b8c682451b7d2",
|
||||
"sourceSha256": "afa20fd81ac20e56571c5fc16e5f881b9fac8ead93f923b52d132c853f55cc48",
|
||||
"imageSha256": "4476e223863379ef9dfd0a1c89fb4c169a1249268e733ac11e5a2eec8d90f8f5"
|
||||
},
|
||||
"background-editor": {
|
||||
@@ -90,7 +90,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "d163d1ae7af291ebbcfe69419b5cf0bdcd20d1930ba79c72651b8c682451b7d2",
|
||||
"sourceSha256": "afa20fd81ac20e56571c5fc16e5f881b9fac8ead93f923b52d132c853f55cc48",
|
||||
"imageSha256": "a83926911c2d6d468c81dfdf74be7e00cbbca9a02afade6e6b5644bdf4f46852"
|
||||
},
|
||||
"room-card": {
|
||||
@@ -101,7 +101,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "d163d1ae7af291ebbcfe69419b5cf0bdcd20d1930ba79c72651b8c682451b7d2",
|
||||
"sourceSha256": "afa20fd81ac20e56571c5fc16e5f881b9fac8ead93f923b52d132c853f55cc48",
|
||||
"imageSha256": "48a5685039a5e0de2a28190d857b193c86e2e63606e3c7a8c40fc9d2b190da4e"
|
||||
},
|
||||
"device-info": {
|
||||
@@ -112,7 +112,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "d163d1ae7af291ebbcfe69419b5cf0bdcd20d1930ba79c72651b8c682451b7d2",
|
||||
"sourceSha256": "afa20fd81ac20e56571c5fc16e5f881b9fac8ead93f923b52d132c853f55cc48",
|
||||
"imageSha256": "307b2ba224cd0b55c2e2936767a16cdda7af3ddffd292705f20c5a300e56c648"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -717,6 +717,31 @@ export const MUTANTS = [
|
||||
replace: ' .dev:not(.unavail):hover {',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'partition-merge-rescales-rooms',
|
||||
guard: 'node --test --test-name-pattern="issue 229" test/plan-optimizer.test.mjs',
|
||||
because: 'комнаты хранятся в тех же координатах, что перегородки: лишнее деление '
|
||||
+ 'уносит их в угол и примыкание к стене комнаты перестаёт находиться '
|
||||
+ '(CODE-REVIEW-229-r1, High-1)',
|
||||
patches: [{
|
||||
file: 'src/plan-optimizer.ts',
|
||||
find: ` .filter((poly: number[][] | null): poly is number[][] => !!poly),`,
|
||||
replace: ` .filter((poly: number[][] | null): poly is number[][] => !!poly)
|
||||
.map((poly: number[][]) => poly.map((p) => [p[0] / NORM_W, p[1] / NORM_W])),`,
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'chain-merge-sees-own-draft',
|
||||
guard: 'node demo/smoke_wall_chain_merge.mjs',
|
||||
because: 'завершаемая цепочка ещё лежит в room_drafts, и её собственные концы '
|
||||
+ 'нельзя принимать за чужое примыкание — иначе стык с существующей стеной '
|
||||
+ 'никогда не срастается (CODE-REVIEW-229-r1, High-2)',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: ' if (draft?.id && draft.id === this._activeDraftId) return [];',
|
||||
replace: ' void 0;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'partition-merge-disabled',
|
||||
guard: 'node demo/smoke_wall_chain_merge.mjs',
|
||||
|
||||
@@ -6553,12 +6553,17 @@ class HouseplanCard extends LitElement {
|
||||
pitch: GRID_STEP_N,
|
||||
seedIds,
|
||||
geometry: {
|
||||
// Same coordinates as the partitions — `roomPoly` returns the raw
|
||||
// config polygon (review CODE-REVIEW-229-r1, High-1).
|
||||
roomPolygons: rooms
|
||||
.map((room) => roomPoly(room))
|
||||
.filter((poly): poly is number[][] => !!poly)
|
||||
.map((poly) => poly.map((point) => [point[0] / NORM_W, point[1] / NORM_W])),
|
||||
.filter((poly): poly is number[][] => !!poly),
|
||||
columns: sp.wall_columns || [],
|
||||
// The chain being finished is still persisted as a draft at this
|
||||
// point, and its own ends must not pass for someone else's junction —
|
||||
// the same exclusion plan-snap-overlay makes (review r1, High-2).
|
||||
draftEnds: (sp.room_drafts || []).flatMap((draft: any) => {
|
||||
if (draft?.id && draft.id === this._activeDraftId) return [];
|
||||
const points = draft?.points || [];
|
||||
return points.length ? [points[0], points[points.length - 1]] : [];
|
||||
}),
|
||||
|
||||
@@ -503,10 +503,13 @@ export function optimizePlans(configIn: any, layoutIn: Record<string, any>): Opt
|
||||
const partitionMerge = mergeCollinearPartitions(space.partitions || [], {
|
||||
pitch: GRID_STEP_N,
|
||||
geometry: {
|
||||
// Rooms are stored in the same coordinates as partitions: `roomPoly`
|
||||
// hands back the raw config polygon, so scaling it here would push
|
||||
// every room into a corner and no junction would ever be found
|
||||
// (review CODE-REVIEW-229-r1, High-1).
|
||||
roomPolygons: (space.rooms || [])
|
||||
.map((room: any) => roomPoly(room))
|
||||
.filter((poly: number[][] | null): poly is number[][] => !!poly)
|
||||
.map((poly: number[][]) => poly.map((p) => [p[0] / NORM_W, p[1] / NORM_W])),
|
||||
.filter((poly: number[][] | null): poly is number[][] => !!poly),
|
||||
columns: space.wall_columns || [],
|
||||
draftEnds: (space.room_drafts || []).flatMap((draft: any) => {
|
||||
const points = draft?.points || [];
|
||||
|
||||
@@ -494,3 +494,23 @@ test('issue 229 Optimize rewrites the legacy projection when the merged wall tur
|
||||
assert.ok(Math.abs(door.x - 0.4) < 1e-9, `stale projection x: ${door.x}`);
|
||||
assert.equal(door.angle, 0, 'stale projection angle');
|
||||
});
|
||||
|
||||
test('issue 229 a node on the side of a room survives the sweep', () => {
|
||||
// The junction sits in the middle of the room's bottom side, not on a
|
||||
// corner — an ordinary T-junction, and the reason the node exists.
|
||||
// Regression for the rescaled room polygons of CODE-REVIEW-229-r1 High-1.
|
||||
const config = {
|
||||
spaces: [{
|
||||
id: 'f1', title: 'Floor', cell_cm: 5, view_box: [0, 0, 1, 1],
|
||||
rooms: [{ id: 'r1', x: 0.1, y: 0.1, w: 0.4, h: 0.4 }],
|
||||
partitions: [
|
||||
partition('p1', 0.1, 0.5, 0.3, 0.5),
|
||||
partition('p2', 0.3, 0.5, 0.5, 0.5),
|
||||
],
|
||||
}],
|
||||
markers: [], settings: {},
|
||||
};
|
||||
const result = optimizePlans(config, {});
|
||||
assert.equal(result.report.partitionsMerged, 0, 'the room side holds the node');
|
||||
assert.equal(result.config.spaces[0].partitions.length, 2);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user