mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
committed by
claude[bot]
parent
d66cd2ebab
commit
f66cf8ad4d
File diff suppressed because one or more lines are too long
@@ -0,0 +1,149 @@
|
||||
/** Issue #138: a new room may close along one existing continuous solid wall interval. */
|
||||
import { launch, checkAll, finish } from './serve.mjs';
|
||||
|
||||
const { page, browser } = await launch({ width: 900, height: 820 }, 1);
|
||||
|
||||
const out = await page.evaluate(async () => {
|
||||
const result = {};
|
||||
const card = window.__card;
|
||||
const root = () => card.shadowRoot || card.renderRoot;
|
||||
const update = async () => { card.requestUpdate(); await card.updateComplete; };
|
||||
const clone = (value) => JSON.parse(JSON.stringify(value));
|
||||
const baseSpace = {
|
||||
id: 'autoclose', title: 'Autoclose', cell_cm: 5, view_box: [0, 0, 1, 0.7],
|
||||
rooms: [{
|
||||
id: 'existing', name: 'Existing', area: null,
|
||||
poly: [[0.1, 0.1], [0.5, 0.1], [0.5, 0.5], [0.1, 0.5]],
|
||||
}],
|
||||
walls: [{
|
||||
key: '0.500000,0.300000@1.5708',
|
||||
a: [0.5, 0.1], b: [0.5, 0.5], cm: 40,
|
||||
}],
|
||||
};
|
||||
const reset = async (spacePatch = {}) => {
|
||||
card._serverCfg = { spaces: [{ ...clone(baseSpace), ...clone(spacePatch) }], markers: [], settings: {} };
|
||||
card._layout = {};
|
||||
card._space = 'autoclose';
|
||||
card._modelCache = null;
|
||||
card._frame = null;
|
||||
card._cfgEpoch++;
|
||||
card._planSnapGeometryCache = null;
|
||||
card._path = [];
|
||||
card._activeDraftId = null;
|
||||
card._draftSegmentCms = [];
|
||||
card._closingWallCm = null;
|
||||
card._roomDialog = false;
|
||||
card._roomEditId = null;
|
||||
card._pendingSplit = null;
|
||||
card._toast = '';
|
||||
card._drawWallField = '15';
|
||||
card._setMode('plan');
|
||||
card._tool = 'draw';
|
||||
card._clearPlanSnapHover();
|
||||
await update();
|
||||
};
|
||||
|
||||
await reset();
|
||||
const stage = root().querySelector('.stage');
|
||||
const eventAt = (x, y, extra = {}) => {
|
||||
const rect = stage.getBoundingClientRect();
|
||||
const view = card._viewOr(card._baseVb());
|
||||
return new MouseEvent('click', {
|
||||
clientX: rect.left + ((x - view.x) / view.w) * rect.width,
|
||||
clientY: rect.top + ((y - view.y) / view.h) * rect.height,
|
||||
bubbles: true,
|
||||
...extra,
|
||||
});
|
||||
};
|
||||
const click = async (x, y, extra = {}) => {
|
||||
card._markupClick(eventAt(x, y, extra));
|
||||
await update();
|
||||
};
|
||||
const close = (a, b, epsilon = 1e-5) => Math.abs(a - b) <= epsilon;
|
||||
const currentDraft = () => card._curSpaceCfg.room_drafts?.find((draft) =>
|
||||
draft.id === card._activeDraftId);
|
||||
|
||||
// r1 regression guard: two points on the common wall are still only an open line.
|
||||
await click(500, 100);
|
||||
await click(500, 500);
|
||||
result.secondCommonWallPointStaysOpen = card._path.length === 2
|
||||
&& !card._roomDialog && !card._toast
|
||||
&& currentDraft()?.points.length === 2
|
||||
&& currentDraft()?.segments.length === 1;
|
||||
|
||||
// Endpoint-to-endpoint auto-close after the new room has enough vertices.
|
||||
await reset();
|
||||
await click(500, 100);
|
||||
await click(800, 100);
|
||||
await click(800, 500);
|
||||
await click(500, 500);
|
||||
const successfulDraft = clone(currentDraft());
|
||||
result.endpointAutoCloseOpensDialog = card._roomDialog
|
||||
&& card._path.length === 5
|
||||
&& close(card._path[0][0], card._path.at(-1)[0])
|
||||
&& close(card._path[0][1], card._path.at(-1)[1]);
|
||||
result.terminalSegmentPersistsBeforeDialog = successfulDraft?.points.length === 4
|
||||
&& successfulDraft?.segments.length === 3
|
||||
&& close(successfulDraft.points.at(-1)[0], 0.5)
|
||||
&& close(successfulDraft.points.at(-1)[1], 0.5)
|
||||
&& successfulDraft.segments.every((segment) => close(segment.cm, 15));
|
||||
result.roomIsNotCommittedBeforeSave = card._curSpaceCfg.rooms.length === 1;
|
||||
|
||||
card._roomDialogCancel();
|
||||
await update();
|
||||
result.cancelKeepsTerminalOpenDraft = !card._roomDialog
|
||||
&& card._path.length === 4
|
||||
&& !card._contourClosed
|
||||
&& JSON.stringify(currentDraft()) === JSON.stringify(successfulDraft)
|
||||
&& close(card._path.at(-1)[0], 500) && close(card._path.at(-1)[1], 500);
|
||||
|
||||
// Save promotes the same draft, keeps the existing shared thickness and creates no partition.
|
||||
await reset();
|
||||
await click(500, 100);
|
||||
await click(800, 100);
|
||||
await click(800, 500);
|
||||
await click(500, 500);
|
||||
card._nameSel = 'Adjacent';
|
||||
card._saveRoom();
|
||||
await update();
|
||||
const sharedWall = (card._curSpaceCfg.walls || []).find((wall) => wall.cm === 40
|
||||
&& wall.a && wall.b
|
||||
&& close(wall.a[0], 0.5) && close(wall.b[0], 0.5)
|
||||
&& close(Math.min(wall.a[1], wall.b[1]), 0.1)
|
||||
&& close(Math.max(wall.a[1], wall.b[1]), 0.5));
|
||||
result.savePromotesDraftWithoutPartition = card._curSpaceCfg.rooms.length === 2
|
||||
&& card._curSpaceCfg.rooms.some((room) => room.name === 'Adjacent')
|
||||
&& !card._curSpaceCfg.room_drafts?.length
|
||||
&& !card._curSpaceCfg.partitions?.length
|
||||
&& card._path.length === 0;
|
||||
result.sharedWallKeepsNeighbourThickness = !!sharedWall
|
||||
&& (card._curSpaceCfg.walls || []).filter((wall) => wall.cm === 15).length >= 3;
|
||||
|
||||
// A canonical cut splits the room wall, so the same endpoint pair no longer closes.
|
||||
await reset({ openings: [{
|
||||
id: 'door', type: 'door', x: 0.5, y: 0.3, angle: 90, length: 0.1,
|
||||
}] });
|
||||
await click(500, 100);
|
||||
await click(800, 100);
|
||||
await click(800, 500);
|
||||
await click(500, 500);
|
||||
result.openingCutPreventsAutoClose = !card._roomDialog
|
||||
&& card._path.length === 4
|
||||
&& currentDraft()?.points.length === 4;
|
||||
|
||||
// An eligible but self-intersecting prospective ring is consumed without partial write.
|
||||
await reset();
|
||||
await click(500, 100);
|
||||
await click(400, 300);
|
||||
await click(700, 300);
|
||||
const invalidDraftBefore = clone(currentDraft());
|
||||
await click(500, 500);
|
||||
result.invalidCloseHasNoPartialWrite = !card._roomDialog
|
||||
&& card._path.length === 3
|
||||
&& JSON.stringify(currentDraft()) === JSON.stringify(invalidDraftBefore)
|
||||
&& card._toast === card._t('toast.contour_cannot_close');
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
await finish(browser, checkAll(out));
|
||||
File diff suppressed because one or more lines are too long
Vendored
+65
-65
File diff suppressed because one or more lines are too long
@@ -414,6 +414,16 @@ field. Double click edits physical-object properties, pointer drag moves the
|
||||
whole object on the grid, and Delete removes only the selected object. The
|
||||
legacy root `space.segments` array is still stripped on every save.
|
||||
|
||||
Adjacent-room auto-close reuses the immutable architectural snap snapshot and
|
||||
requires the first and prospective terminal points to belong to one completed
|
||||
room's same solid segment after opening/open-span cuts. It runs only after the
|
||||
draft has two edges and after explicit first-point/Ctrl closure handling. The
|
||||
prospective polygon is checked for range, self-intersection and room overlap
|
||||
before `_path` or `room_drafts` changes; success persists the terminal segment
|
||||
and then opens the ordinary room dialog. Cancel therefore returns to the open
|
||||
draft at that terminal point, while Save lets the normal room/wall
|
||||
normalization preserve an existing shared wall's thickness.
|
||||
|
||||
While drawing, the length of the current segment follows the cursor (`_fmtLen` → `segmentCm`/
|
||||
`formatLength`): metres, or feet+inches when `hass.config.unit_system` is imperial. The scale is
|
||||
per-space `cell_cm` — cm represented by one grid cell (default 5, so 240 cells ≈ 12 m).
|
||||
|
||||
@@ -569,3 +569,13 @@ current anchor is excluded to prevent zero-length segments, while the first
|
||||
point of a valid room outline remains an explicit closure target. The static
|
||||
geometry is cached by structural editor state; pointer movement changes at
|
||||
most the single active candidate and never writes config, layout or storage.
|
||||
|
||||
For **Room outline**, the same completed-room provenance also enables adjacent
|
||||
auto-close. Once the draft already has two edges, a click that places its next
|
||||
point on the same uninterrupted solid room-wall segment as the first point is
|
||||
validated as the prospective polygon before any mutation. A valid click
|
||||
persists that terminal draft segment and opens the standard room dialog with
|
||||
the existing wall as the closing edge. A cut, a different edge, a saved draft
|
||||
or an independent partition is ineligible; an eligible but invalid polygon is
|
||||
reported without appending a point or segment. Explicit first-point and
|
||||
Ctrl/Cmd closure retain priority.
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- A new room outline that starts and ends on one uninterrupted solid interval
|
||||
of an existing room wall now closes automatically and opens the normal room
|
||||
dialog. Endpoints and wall-bound T-connection points work alike; openings,
|
||||
intentional gaps, different edges and a second point without enough sides do
|
||||
not trigger the shortcut ([#138](https://github.com/Matysh/houseplan-card/issues/138)).
|
||||
- Connected Room outline and Partition segments now form one seamless thick
|
||||
wall immediately, including the live rubber-band. Right and oblique corners
|
||||
use bounded mitre/bevel joins, T-connections have no visible butt face, each
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Новый контур комнаты, начатый и законченный на одном непрерывном сплошном
|
||||
участке стены существующей комнаты, теперь замыкается автоматически и
|
||||
открывает обычный диалог комнаты. Одинаково работают конечные узлы и точки
|
||||
T-соединения на линии; проёмы, намеренные разрывы, разные грани и второй клик
|
||||
без достаточного числа сторон быстрым замыканием не считаются
|
||||
([#138](https://github.com/Matysh/houseplan-card/issues/138)).
|
||||
- Соединённые отрезки «Контура комнаты» и «Перегородки» теперь сразу, включая
|
||||
активный rubber-band, образуют одну бесшовную толстую стену. Прямые и косые
|
||||
углы получают ограниченный mitre/bevel, T-соединение не показывает торец,
|
||||
|
||||
@@ -362,6 +362,18 @@ separately promised workflows:
|
||||
- [ ] Ctrl/Cmd+click closes the current endpoint back to the first point without
|
||||
adding another vertex. It requires at least two existing edges and refuses
|
||||
degenerate or self-intersecting closure [auto: smoke_editor_tabs]
|
||||
- [ ] Adjacent-room auto-close: after two drawn edges, ending on the same
|
||||
uninterrupted solid existing-room wall interval as the first point adds
|
||||
the terminal segment and opens the normal room dialog. Endpoints and
|
||||
wall-interior snap points both work; Save creates one room without an
|
||||
independent partition and preserves the shared wall thickness. Cancel
|
||||
restores the open draft ending at that point [auto: smoke_room_autoclose;
|
||||
unit: plan-snap-overlay.test.mjs]
|
||||
- [ ] Adjacent-room auto-close safety: a second point alone stays open; different
|
||||
room edges, draft/partition-only axes and points separated by an opening
|
||||
or open-span cut do not trigger it. An eligible self-intersecting contour
|
||||
shows the existing error and writes neither the terminal point nor segment
|
||||
[auto: smoke_room_autoclose; unit: plan-snap-overlay.test.mjs]
|
||||
- [ ] Room dialog: area list shows only unassigned areas; picking an area prefills the name
|
||||
- [ ] Room dialog uses the medium width and its body has no horizontal overflow;
|
||||
long options stay inside it at desktop and narrow widths [auto:
|
||||
|
||||
@@ -263,6 +263,12 @@ desktop: для точного рисования, Resize, модификато
|
||||
5. Замкните контур кликом по первой точке или `Ctrl` + кликом в любом месте.
|
||||
Быстрое замыкание доступно после двух нарисованных граней и не сработает,
|
||||
если замыкающая стена создаёт самопересечение.
|
||||
Если первая и текущая точки лежат на одном непрерывном сплошном участке
|
||||
стены существующей комнаты, текущий клик сам добавит последнюю грань,
|
||||
замкнёт контур общей стеной и откроет тот же диалог. Можно попадать как в
|
||||
конечный узел, так и в точку на линии. Проём или намеренный разрыв разделяет
|
||||
участок, разные грани не объединяются, а второй клик после единственной
|
||||
первой точки остаётся обычным незамкнутым отрезком.
|
||||
6. В диалоге задайте название, выберите свободную HA-зону либо оставьте в списке «— без зоны —», затем нажмите «Сохранить».
|
||||
|
||||
Во время рисования показываются длина сегмента и угол. Углы, кратные 45°, выделяются. Удерживайте `Shift`, чтобы зафиксировать текущий отрезок по ближайшему такому направлению; предпросмотр и поставленная точка останутся на сетке. Новая комната не может частично перекрывать другую, но полностью вложенная «островная» комната поддерживается.
|
||||
|
||||
+65
-23
@@ -132,7 +132,7 @@ import {
|
||||
physicalBodies, physicalBodySet,
|
||||
} from './physical-geometry';
|
||||
import {
|
||||
buildPlanSnapGeometry, resolvePlanSnap,
|
||||
buildPlanSnapGeometry, findSharedRoomSnapSegment, resolvePlanSnap,
|
||||
type PlanSnapCandidate, type PlanSnapGeometry, type PlanSnapSegment,
|
||||
} from './plan-snap-overlay';
|
||||
import {
|
||||
@@ -6424,27 +6424,33 @@ class HouseplanCard extends LitElement {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Validate and close the draft without using the closing click as a vertex. */
|
||||
private _closeRoomContour(showMinimumError = false): void {
|
||||
/** Validate one prospective room ring without mutating the live draft. */
|
||||
private _validateRoomContour(
|
||||
path: number[][], showMinimumError = false,
|
||||
): { closingCm: number } | null {
|
||||
// Three placed vertices mean two existing edges; the closing edge becomes
|
||||
// the third one. Anything shorter cannot enclose a room.
|
||||
if (this._path.length < 3) {
|
||||
if (path.length < 3) {
|
||||
if (showMinimumError) this._showToast(this._t('toast.contour_min_edges'));
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
const closingCm = this._drawWallCm;
|
||||
if (closingCm == null) { this._showPhysicalRange(100); return; }
|
||||
if (this._contourSelfIntersects(this._path) || polygonArea(this._path) <= 1e-6) {
|
||||
if (closingCm == null) { this._showPhysicalRange(100); return null; }
|
||||
if (this._contourSelfIntersects(path) || polygonArea(path) <= 1e-6) {
|
||||
this._showToast(this._t('toast.contour_cannot_close'));
|
||||
return; // keep the draft editable
|
||||
return null; // keep the draft editable
|
||||
}
|
||||
// A contour can enclose an existing room without any vertex inside it.
|
||||
const clash = this._overlapRoom(this._path);
|
||||
const clash = this._overlapRoom(path);
|
||||
if (clash) {
|
||||
this._showToast(this._t('toast.room_overlap', { name: clash.name || '' }));
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
this._path = [...this._path, [...this._path[0]]];
|
||||
return { closingCm };
|
||||
}
|
||||
|
||||
private _openRoomContourDialog(path: number[][], closingCm: number): void {
|
||||
this._path = [...path, [...path[0]]];
|
||||
this._closingWallCm = closingCm;
|
||||
this._clearPlanSnapHover();
|
||||
this._nameSel = '';
|
||||
@@ -6453,6 +6459,52 @@ class HouseplanCard extends LitElement {
|
||||
this._roomDialog = true;
|
||||
}
|
||||
|
||||
/** Validate and close the draft without using the closing click as a vertex. */
|
||||
private _closeRoomContour(showMinimumError = false): void {
|
||||
const valid = this._validateRoomContour(this._path, showMinimumError);
|
||||
if (!valid) return;
|
||||
this._openRoomContourDialog(this._path, valid.closingCm);
|
||||
}
|
||||
|
||||
/** The same append limits guard both an ordinary point and an auto-close terminal point. */
|
||||
private _canAppendRoomDraftPoint(): boolean {
|
||||
if (this._drawWallCm == null) { this._showPhysicalRange(100); return false; }
|
||||
if (this._path.length >= MAX_DRAFT_POINTS) {
|
||||
this._showToast(this._t('toast.physical_limit'));
|
||||
return false;
|
||||
}
|
||||
const spCfg = this._curSpaceCfg as any;
|
||||
const newDraft = !this._activeDraftId;
|
||||
if ((newDraft && (spCfg?.room_drafts || []).length >= MAX_ROOM_DRAFTS)
|
||||
|| this._draftSegmentCount(spCfg) >= MAX_DRAFT_SEGMENTS) {
|
||||
this._showToast(this._t('toast.physical_limit'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Consume a qualifying click whether it closes successfully or reports a validation error. */
|
||||
private _tryAutoCloseRoomContour(pt: number[]): boolean {
|
||||
// One existing point plus B is only a line. Preserve the ordinary second
|
||||
// click, symmetrically with the existing first-point closure gate.
|
||||
if (this._path.length < 2) return false;
|
||||
const shared = findSharedRoomSnapSegment(
|
||||
this._planSnapGeometrySnapshot().value,
|
||||
this._path[0],
|
||||
pt,
|
||||
this._gridPitch * 0.0002,
|
||||
);
|
||||
if (!shared) return false;
|
||||
if (!this._canAppendRoomDraftPoint()) return true;
|
||||
const prospective = [...this._path, pt];
|
||||
const valid = this._validateRoomContour(prospective);
|
||||
if (!valid) return true;
|
||||
this._path = prospective;
|
||||
this._persistActiveDraftSegment();
|
||||
this._openRoomContourDialog(this._path, valid.closingCm);
|
||||
return true;
|
||||
}
|
||||
|
||||
private _markupClick(ev: MouseEvent): void {
|
||||
if (this._vacFit) return; // the fit overlay owns all pointer input
|
||||
if (!this._markup) return;
|
||||
@@ -6555,23 +6607,13 @@ class HouseplanCard extends LitElement {
|
||||
this._closeRoomContour();
|
||||
return;
|
||||
}
|
||||
if (this._tryAutoCloseRoomContour(pt)) return;
|
||||
const join = this._draftEndAt(pt, this._activeDraftId || undefined);
|
||||
if (join) {
|
||||
this._mergeDraftEndpoint(join);
|
||||
return;
|
||||
}
|
||||
if (this._drawWallCm == null) { this._showPhysicalRange(100); return; }
|
||||
if (this._path.length >= MAX_DRAFT_POINTS) {
|
||||
this._showToast(this._t('toast.physical_limit'));
|
||||
return;
|
||||
}
|
||||
const spCfg = this._curSpaceCfg as any;
|
||||
const newDraft = !this._activeDraftId;
|
||||
if ((newDraft && (spCfg?.room_drafts || []).length >= MAX_ROOM_DRAFTS)
|
||||
|| this._draftSegmentCount(spCfg) >= MAX_DRAFT_SEGMENTS) {
|
||||
this._showToast(this._t('toast.physical_limit'));
|
||||
return;
|
||||
}
|
||||
if (!this._canAppendRoomDraftPoint()) return;
|
||||
this._path = [...this._path, pt];
|
||||
this._persistActiveDraftSegment();
|
||||
}
|
||||
|
||||
@@ -101,6 +101,10 @@ function segmentKey(source: SourceSegment, a: readonly number[], b: readonly num
|
||||
return `${sourceKey(source)}|${pointKey(ca)}|${pointKey(cb)}`;
|
||||
}
|
||||
|
||||
function sourceRank(kind: PlanSnapSourceKind): number {
|
||||
return kind === 'room' ? 0 : kind === 'draft' ? 1 : 2;
|
||||
}
|
||||
|
||||
function touches(point: readonly number[], segment: readonly number[], epsilon: number): boolean {
|
||||
return pointsEqual(point, [segment[0], segment[1]], epsilon)
|
||||
|| pointsEqual(point, [segment[2], segment[3]], epsilon);
|
||||
@@ -171,7 +175,10 @@ export function buildPlanSnapGeometry(options: BuildPlanSnapGeometryOptions): Pl
|
||||
sourceId: source.id,
|
||||
};
|
||||
const existing = segmentsByAxis.get(axisKey);
|
||||
if (!existing || candidate.key.localeCompare(existing.key) < 0) {
|
||||
if (!existing
|
||||
|| sourceRank(candidate.sourceKind) < sourceRank(existing.sourceKind)
|
||||
|| (sourceRank(candidate.sourceKind) === sourceRank(existing.sourceKind)
|
||||
&& candidate.key.localeCompare(existing.key) < 0)) {
|
||||
segmentsByAxis.set(axisKey, candidate);
|
||||
}
|
||||
}
|
||||
@@ -189,6 +196,39 @@ export function buildPlanSnapGeometry(options: BuildPlanSnapGeometryOptions): Pl
|
||||
};
|
||||
}
|
||||
|
||||
function pointOnSnapSegment(
|
||||
point: readonly number[], segment: PlanSnapSegment, epsilon: number,
|
||||
): boolean {
|
||||
const dx = segment.b[0] - segment.a[0];
|
||||
const dy = segment.b[1] - segment.a[1];
|
||||
const length = Math.hypot(dx, dy);
|
||||
if (!(length > epsilon)) return false;
|
||||
const ux = dx / length;
|
||||
const uy = dy / length;
|
||||
const px = point[0] - segment.a[0];
|
||||
const py = point[1] - segment.a[1];
|
||||
const along = px * ux + py * uy;
|
||||
const perpendicular = Math.abs(px * uy - py * ux);
|
||||
return perpendicular <= epsilon && along >= -epsilon && along <= length + epsilon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the stable completed-room solid interval that contains both points.
|
||||
* Because room openings and open spans are cut while the snapshot is built,
|
||||
* points on opposite sides of a gap can never share a returned segment.
|
||||
*/
|
||||
export function findSharedRoomSnapSegment(
|
||||
geometry: PlanSnapGeometry,
|
||||
a: readonly number[],
|
||||
b: readonly number[],
|
||||
epsilon = DEFAULT_EPSILON,
|
||||
): PlanSnapSegment | null {
|
||||
if (!finitePoint(a) || !finitePoint(b) || pointsEqual(a, b, epsilon)) return null;
|
||||
return geometry.segments.find((segment) => segment.sourceKind === 'room'
|
||||
&& pointOnSnapSegment(a, segment, epsilon)
|
||||
&& pointOnSnapSegment(b, segment, epsilon)) || null;
|
||||
}
|
||||
|
||||
function isExcluded(
|
||||
point: readonly number[],
|
||||
excluded: readonly (readonly number[])[],
|
||||
|
||||
@@ -2,6 +2,7 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
buildPlanSnapGeometry,
|
||||
findSharedRoomSnapSegment,
|
||||
resolvePlanSnap,
|
||||
} from '../test-build/plan-snap-overlay.js';
|
||||
|
||||
@@ -71,6 +72,51 @@ test('room cuts leave solid intervals but do not create cut-boundary endpoints',
|
||||
'an original endpoint remains when another solid wall still meets it');
|
||||
});
|
||||
|
||||
test('shared-room interval contains endpoints and interior wall-bound points only on one edge', () => {
|
||||
const geometry = buildPlanSnapGeometry({
|
||||
space: space({ rooms: [{ id: 'room', x: 0, y: 0, w: 100, h: 100 }] }),
|
||||
});
|
||||
assert.ok(findSharedRoomSnapSegment(geometry, [0, 0], [100, 0]));
|
||||
assert.ok(findSharedRoomSnapSegment(geometry, [0, 0], [40, 0]));
|
||||
assert.ok(findSharedRoomSnapSegment(geometry, [20, 0], [80, 0]));
|
||||
assert.equal(findSharedRoomSnapSegment(geometry, [0, 0], [100, 100]), null,
|
||||
'different room edges never imply an auto-closing wall');
|
||||
assert.equal(findSharedRoomSnapSegment(geometry, [40, 0], [40, 0]), null,
|
||||
'one point cannot define a closing interval');
|
||||
assert.equal(findSharedRoomSnapSegment(geometry, [-1, 0], [40, 0], 0.001), null,
|
||||
'collinearity outside the closed segment is insufficient');
|
||||
});
|
||||
|
||||
test('shared-room interval respects cuts and rejects draft or partition-only axes', () => {
|
||||
const cut = buildPlanSnapGeometry({
|
||||
space: space({ rooms: [{ id: 'room', x: 0, y: 0, w: 100, h: 100 }] }),
|
||||
roomCuts: [[40, 0, 60, 0]],
|
||||
});
|
||||
assert.ok(findSharedRoomSnapSegment(cut, [0, 0], [30, 0]));
|
||||
assert.equal(findSharedRoomSnapSegment(cut, [0, 0], [100, 0]), null,
|
||||
'opening or open-span cuts split eligibility');
|
||||
|
||||
const independent = buildPlanSnapGeometry({
|
||||
space: space({
|
||||
room_drafts: [{ id: 'draft', points: [[0, 10], [100, 10]], segments: [{ cm: 15 }] }],
|
||||
partitions: [{ id: 'partition', a: [0, 20], b: [100, 20], cm: 15 }],
|
||||
}),
|
||||
});
|
||||
assert.equal(findSharedRoomSnapSegment(independent, [0, 10], [100, 10]), null);
|
||||
assert.equal(findSharedRoomSnapSegment(independent, [0, 20], [100, 20]), null);
|
||||
});
|
||||
|
||||
test('a completed room remains the authority for a coincident deduplicated axis', () => {
|
||||
const geometry = buildPlanSnapGeometry({
|
||||
space: space({
|
||||
rooms: [{ id: 'room', x: 0, y: 0, w: 100, h: 100 }],
|
||||
partitions: [{ id: 'partition', a: [100, 0], b: [0, 0], cm: 15 }],
|
||||
}),
|
||||
});
|
||||
const shared = findSharedRoomSnapSegment(geometry, [0, 0], [100, 0]);
|
||||
assert.equal(shared?.sourceKind, 'room');
|
||||
});
|
||||
|
||||
test('endpoint wins over a closer line and tie resolution is stable', () => {
|
||||
const geometry = buildPlanSnapGeometry({
|
||||
space: space({
|
||||
|
||||
Reference in New Issue
Block a user