mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 15:09:30 +00:00
File diff suppressed because one or more lines are too long
@@ -151,6 +151,11 @@ export function prepareGoldenFixture(scenario) {
|
||||
});
|
||||
}
|
||||
if (scenario.junctionPatchResilience) {
|
||||
if (!Array.isArray(scenario.retainedWedgeProbe)
|
||||
|| scenario.retainedWedgeProbe.length !== 2
|
||||
|| !scenario.retainedWedgeProbe.every(Number.isFinite)) {
|
||||
throw new Error(`invalid golden retainedWedgeProbe: ${scenario.id}`);
|
||||
}
|
||||
fixture.config.spaces.push({
|
||||
...structuredClone(junctionPatchFixture),
|
||||
id: scenario.space,
|
||||
@@ -562,6 +567,18 @@ export async function prepareGoldenScenario(page, scenario) {
|
||||
throw new Error(`golden multi-wall bevel contract failed: ${scenario.id}`);
|
||||
}
|
||||
}
|
||||
if (scenario.retainedWedgeProbe) {
|
||||
const point = new DOMPoint(
|
||||
scenario.retainedWedgeProbe[0] * 1000,
|
||||
scenario.retainedWedgeProbe[1] * card._spaceH,
|
||||
);
|
||||
const wall = card.renderRoot.querySelector('[data-hp="wall"]');
|
||||
const papers = [...card.renderRoot.querySelectorAll('.hp-paper')];
|
||||
if (!wall?.isPointInFill?.(point)
|
||||
|| !papers.some((paper) => paper.isPointInFill?.(point))) {
|
||||
throw new Error(`golden retained T-junction wedge contract failed: ${scenario.id}`);
|
||||
}
|
||||
}
|
||||
if (scenario.wallKeyRoundtrip) {
|
||||
const { node, incidentArm } = scenario.wallKeyRoundtrip;
|
||||
const wall = card.renderRoot.querySelector('[data-hp="wall"]');
|
||||
|
||||
@@ -262,9 +262,11 @@ export const GOLDEN_SCENARIOS = Object.freeze([
|
||||
theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
|
||||
{ id: 'junction-patch-resilience-plan-dark', fixture: 'visual',
|
||||
space: 'golden-junction-patch-resilience', junctionPatchResilience: true, mode: 'plan',
|
||||
retainedWedgeProbe: [0.8955, 0.556],
|
||||
theme: 'dark', viewport: { width: 1180, height: 900 }, ...page },
|
||||
{ id: 'junction-patch-resilience-view-dark', fixture: 'visual',
|
||||
space: 'golden-junction-patch-resilience', junctionPatchResilience: true, mode: 'view',
|
||||
retainedWedgeProbe: [0.8955, 0.556],
|
||||
theme: 'dark', viewport: { width: 1000, height: 900 }, ...stage },
|
||||
{ id: 'multiwall-junction-bevel-view-dark', fixture: 'visual',
|
||||
space: 'golden-multiwall-junction', mode: 'view',
|
||||
|
||||
@@ -11,6 +11,22 @@ const result = await page.evaluate(async (spaceFixture) => {
|
||||
const out = {};
|
||||
const card = window.__card;
|
||||
const root = () => card.shadowRoot || card.renderRoot;
|
||||
const retainedWedgeProbe = [895.5, 556];
|
||||
const svgContains = (element, point = retainedWedgeProbe) =>
|
||||
!!element?.isPointInFill?.(new DOMPoint(point[0], point[1]));
|
||||
const ringContains = (ring, point) => {
|
||||
let inside = false;
|
||||
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
||||
const a = ring[i], b = ring[j];
|
||||
if (((a[1] > point[1]) !== (b[1] > point[1]))
|
||||
&& point[0] < ((b[0] - a[0]) * (point[1] - a[1]))
|
||||
/ (b[1] - a[1]) + a[0]) inside = !inside;
|
||||
}
|
||||
return inside;
|
||||
};
|
||||
const geometryContains = (geometry, point = retainedWedgeProbe) =>
|
||||
(geometry || []).some((polygon) => ringContains(polygon[0] || [], point)
|
||||
&& !(polygon.slice(1) || []).some((hole) => ringContains(hole, point)));
|
||||
const frame = () => new Promise((done) => requestAnimationFrame(() => requestAnimationFrame(done)));
|
||||
const settle = async () => {
|
||||
await card.updateComplete;
|
||||
@@ -62,6 +78,9 @@ const result = await page.evaluate(async (spaceFixture) => {
|
||||
const canonical = card._wallUnionGeometry();
|
||||
out.planKeepsMasonry = !!planD && !!planPath && canonical?.d === planD;
|
||||
out.paperKeepsFootprint = !!canonical?.paperD && !!root().querySelector('.hp-paper');
|
||||
out.planRetainsMeasuredWedge = svgContains(planPath);
|
||||
out.paperRetainsMeasuredWedge = [...root().querySelectorAll('.hp-paper')]
|
||||
.some((paper) => svgContains(paper));
|
||||
|
||||
const model = card._spaceModel();
|
||||
const polys = model.rooms.map((room) => ({ r: room, poly: room.poly }));
|
||||
@@ -69,6 +88,7 @@ const result = await page.evaluate(async (spaceFixture) => {
|
||||
out.lightAndSunKeepCanonicalMasonry = barriers.masonryGeometry.flat(2).length > 0
|
||||
&& barriers.occluders.length > 0
|
||||
&& !!barriers.fingerprint;
|
||||
out.lightAndSunRetainMeasuredWedge = geometryContains(barriers.masonryGeometry);
|
||||
const barrierFingerprint = barriers.fingerprint;
|
||||
const cacheBeforeState = card._wallUnionCache;
|
||||
|
||||
@@ -92,15 +112,26 @@ const result = await page.evaluate(async (spaceFixture) => {
|
||||
card._setMode('view');
|
||||
await update(false);
|
||||
out.viewMatchesPlan = root().querySelector('[data-hp="wall"]')?.getAttribute('d') === planD;
|
||||
out.viewRetainsMeasuredWedge = svgContains(root().querySelector('[data-hp="wall"]'));
|
||||
card._hoverRoom = { space: spaceFixture.id, room: model.rooms[0] };
|
||||
const hoverFloor = card._roomHoverPaths(model);
|
||||
out.cleanFloorConsumerStaysNonEmpty = !!hoverFloor?.fillD && !!hoverFloor.outlineD;
|
||||
out.cleanFloorExcludesMeasuredWedge = model.rooms.every((room) => {
|
||||
card._hoverRoom = { space: spaceFixture.id, room };
|
||||
const floor = card._roomHoverPaths(model);
|
||||
if (!floor?.fillD) return true;
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
path.setAttribute('d', floor.fillD);
|
||||
path.setAttribute('fill-rule', 'evenodd');
|
||||
return !svgContains(path);
|
||||
});
|
||||
card._hoverRoom = null;
|
||||
|
||||
const kioskBefore = card._config.kiosk;
|
||||
card._config.kiosk = true;
|
||||
await update(false);
|
||||
out.kioskMatchesPlan = root().querySelector('[data-hp="wall"]')?.getAttribute('d') === planD;
|
||||
out.kioskRetainsMeasuredWedge = svgContains(root().querySelector('[data-hp="wall"]'));
|
||||
card._config.kiosk = kioskBefore;
|
||||
await update(false);
|
||||
|
||||
@@ -124,6 +155,9 @@ const result = await page.evaluate(async (spaceFixture) => {
|
||||
await staticCard.updateComplete;
|
||||
out.staticMatchesPlan = staticCard.renderRoot
|
||||
?.querySelector('[data-hp="wall"]')?.getAttribute('d') === planD;
|
||||
out.staticRetainsMeasuredWedge = svgContains(
|
||||
staticCard.renderRoot?.querySelector('[data-hp="wall"]'),
|
||||
);
|
||||
staticCard.remove();
|
||||
|
||||
const labs = Object.freeze(['iso']);
|
||||
@@ -131,8 +165,10 @@ const result = await page.evaluate(async (spaceFixture) => {
|
||||
window.__hpLabs = labs;
|
||||
card._setProjection('iso');
|
||||
await update(false);
|
||||
const isoWalls = card._isoSource().build().walls;
|
||||
out.hiddenIsoKeepsMasonry = !!root().querySelector('[data-hp="iso-walls"]')
|
||||
&& card._isoSource().build().walls.flat(2).length > 0;
|
||||
&& isoWalls.flat(2).length > 0;
|
||||
out.hiddenIsoRetainsMeasuredWedge = geometryContains(isoWalls);
|
||||
|
||||
out.renderNeverWritesConfig = JSON.stringify(card._serverCfg.spaces[0]) === sourceBefore;
|
||||
return out;
|
||||
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
@@ -429,10 +429,12 @@ bevel bounded by `1.25 × H`; degree-2 joins keep the legacy `MITRE_LIMIT = 4`.
|
||||
The final bevel is applied to canonical masonry after its room/atomic/exterior
|
||||
union, preventing later boolean inputs from recreating the discarded spike.
|
||||
Canonical masonry replaces each affected local mask with complete physical ray
|
||||
strips clipped to the room union, retains overlap through the approved radius,
|
||||
and preserves the established full exterior cut outside the room union. This
|
||||
prevents the repair from deleting half an incident strip or changing a concave
|
||||
facade. Paper re-unions the room centre footprint after its facade cut.
|
||||
strips clipped to the bounded physical paper envelope, not just the room union,
|
||||
and retains overlap through the approved radius on both sides of the facade.
|
||||
Only the excessive portion beyond `1.25 × H` is removed. This prevents the
|
||||
repair from deleting an exterior half-strip into a white T-junction wedge while
|
||||
still rejecting the old unbounded spike. Paper applies that same bounded cut
|
||||
before re-unioning the room centre footprint (#261).
|
||||
`wallBodiesGeometry.roomGeom` caches this repaired room masonry before openings
|
||||
and independent bodies; clean-floor consumers subtract it from each source room
|
||||
and clip their fallback, so fill cannot escape the building or silently drop a
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Thick T-junctions no longer show white triangular gaps on the exterior side
|
||||
when three or more walls of different thickness meet. The bounded bevel now
|
||||
preserves valid masonry and paper up to the same limit on both sides of the
|
||||
room outline, consistently in Plan, View, kiosk, Static, hidden Iso and
|
||||
light/sun barriers, without restoring the former long mitre spike
|
||||
([#261](https://github.com/Matysh/houseplan-card/issues/261)).
|
||||
- Wall thickness now stays consistent across structural consumers when
|
||||
“Optimize plans” rewrites a compatibility key. Wall identity remains stable
|
||||
when an exact `1/240` grid endpoint is persisted with nine decimal places,
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
|
||||
## Не выпущено
|
||||
|
||||
- В толстых T-стыках больше не появляются белые треугольные щели с наружной
|
||||
стороны, когда сходятся три и более стены разной толщины. Ограниченная фаска
|
||||
теперь сохраняет допустимую кладку и подложку до одного предела по обе стороны
|
||||
контура комнаты — одинаково в Plan, View, kiosk, Static, скрытой изометрии и
|
||||
световых/солнечных барьерах — и не возвращает прежний длинный mitre-зуб
|
||||
([#261](https://github.com/Matysh/houseplan-card/issues/261)).
|
||||
- После перезаписи compatibility-key командой «Оптимизировать планы» толщина
|
||||
стены теперь остаётся согласованной у всех структурных потребителей.
|
||||
Идентификатор стены стабилен, когда точный узел сетки `1/240` сохраняется с
|
||||
|
||||
+20
-1
@@ -1677,7 +1677,8 @@ error-free report captured from the current source fingerprint; the entire set
|
||||
is validated before any reference is copied.
|
||||
|
||||
The matrix covers thick wall junctions, the full #197 multi-room
|
||||
virtual-junction resilience fixture in Plan and View, the #249 three-ray
|
||||
virtual-junction resilience fixture in Plan and View (including the #261
|
||||
measured exterior-wedge fill probe), the #249 three-ray
|
||||
unequal-thickness fixture with a semantic filled-node/empty-old-wedge gate,
|
||||
virtual/physical boundaries,
|
||||
partitions/columns, axis-aligned and 45° door/window/gate tunnels, hidden
|
||||
@@ -1708,6 +1709,14 @@ parity, paper and clean-floor presence, shared Glow/sun masonry, cache reuse on
|
||||
HA/theme ticks, no saved-config mutation, a filled node and the removed old
|
||||
spike. Full golden/smoke/performance remain pre-beta gates.
|
||||
|
||||
For #261, the anonymised #197 fixture also probes the real regression point
|
||||
`(895.5, 556)`: `roomGeom`, final masonry and paper must fill it, while every
|
||||
clean-floor contour must exclude it. The browser smoke repeats semantic point
|
||||
coverage in Plan, View, kiosk, Static, hidden Iso and light/sun masonry; the two
|
||||
existing #197 goldens require `SVGGeometryElement.isPointInFill()` at the same
|
||||
point. Mutation `multi-wall-paper-full-origin-cut` restores the faulty
|
||||
offset-origin cut and must make that regression test fail.
|
||||
|
||||
### Issue #73 baseline and implementation (2026-08-11)
|
||||
|
||||
The published v1.61.0-beta.6 exact SHA is the renderer baseline for #73: it
|
||||
@@ -2623,6 +2632,16 @@ require hands on real hardware — they remain for the human pass.
|
||||
[auto: test/wall-thickness.test.mjs +
|
||||
smoke_junction_patch_resilience + junction-patch-resilience golden
|
||||
scenarios].
|
||||
- [ ] **A bounded T-junction keeps its exterior half-wall (#261)**: in the
|
||||
anonymised #197 fixture the measured point `(895.5, 556)` is filled by
|
||||
room masonry, final masonry and paper, and excluded from clean floor.
|
||||
Plan, View, kiosk, Static, hidden Iso and light/sun barriers agree at that
|
||||
point, while the old excessive #249 spike remains absent
|
||||
[unit: test/wall-thickness.test.mjs; auto:
|
||||
smoke_junction_patch_resilience; golden:
|
||||
junction-patch-resilience-plan-dark +
|
||||
junction-patch-resilience-view-dark; mutation:
|
||||
multi-wall-paper-full-origin-cut].
|
||||
- [ ] **Openings cut the slab**: a door/window/gate on a thick wall leaves a gap in
|
||||
the body; the door swing is offset toward the inner face and gate leaves
|
||||
toward the exterior face; with
|
||||
|
||||
+3
-1
@@ -325,7 +325,9 @@ Other operations edit existing geometry:
|
||||
|
||||
Wall thickness is stored in real units. A room may have different thicknesses
|
||||
on different spans. Open wall branches and T-junctions are allowed; shared
|
||||
geometry remains joined without painted end caps.
|
||||
geometry remains joined without painted end caps. At a saved T-junction both
|
||||
physical half-walls stay solid: the bounded bevel removes only an excessive
|
||||
projecting corner and never leaves a white triangular gap.
|
||||
If Resize moves only part of a longer thick wall, the moved part keeps its
|
||||
thickness and follows the room while the remaining continuation stays in
|
||||
place. Openings on the moved part follow it in the same Undo/Redo operation.
|
||||
|
||||
@@ -389,7 +389,9 @@ Cancel и «Оставить стенами» ничего не исправля
|
||||
T-соединение входит в проходящую стену без видимого торца. Это действует уже у
|
||||
активного rubber-band до клика и сохраняется после клика, в View и на статичной
|
||||
карточке. Толщина каждого ранее поставленного отрезка остаётся своей; свободный
|
||||
конец незамкнутой стены остаётся плоским.
|
||||
конец незамкнутой стены остаётся плоским. У сохранённого T-стыка обе половины
|
||||
физической стены остаются сплошными: фаска убирает только чрезмерно выступающий
|
||||
угол и не оставляет белого треугольного клина.
|
||||
|
||||
Каждый законченный отрезок цепочки сохраняется сразу. Esc/Ctrl+Z удаляет
|
||||
последнюю точку. Pan, pinch и `pointercancel` ничего не завершают и не добавляют.
|
||||
|
||||
@@ -111,11 +111,13 @@ incident half-depth `H`; every excessive join is cut back with a straight local
|
||||
bevel and may not extend beyond `R = 1.25 × H`. Inside the room union, a bounded
|
||||
mask replaces the legacy ring with the complete finite ray strips, retains their
|
||||
overlap through `R`, and removes only the remaining excessive pairwise wedge.
|
||||
Outside the room union, the established full facade cut is preserved. This keeps
|
||||
the node centre and every arm area-connected without allowing an interior child
|
||||
mitre to change a concave facade. Ordinary two-ray corners retain the exact
|
||||
historical `MITRE_LIMIT = 4` contract. This is computed geometry only: saved room
|
||||
outlines and wall entries are not rewritten.
|
||||
The same bounded rule applies to the exterior half-wall and paper envelope:
|
||||
local ray strips are clipped to that physical envelope rather than the room
|
||||
centre, so a valid T-junction cannot become a white wedge. This keeps the node
|
||||
centre and every arm area-connected without allowing overlap beyond `R` or an
|
||||
interior child mitre to change a concave facade. Ordinary two-ray corners retain
|
||||
the exact historical `MITRE_LIMIT = 4` contract. This is computed geometry only:
|
||||
saved room outlines and wall entries are not rewritten (#261).
|
||||
|
||||
Clean-floor consumers subtract the cached, repaired canonical room masonry from
|
||||
their source room and take its outer component. The result is clipped to the
|
||||
@@ -194,7 +196,8 @@ valid masonry, paper, floor faces or light barriers for the whole space (#197).
|
||||
The same failure isolation covers degree-3+ repair: every node is rebuilt and
|
||||
committed independently inside its bounded mask. A malformed local candidate
|
||||
therefore keeps that node's previous body without reverting successful repairs
|
||||
at unrelated nodes.
|
||||
at unrelated nodes. The exterior paper uses the same `R`-bounded overlap as the
|
||||
masonry; it never restarts the cut at the offset origins (#261).
|
||||
|
||||
Runtime normalisation remains lossless for every positive exact thickness
|
||||
interval, regardless of its length. The explicit **Optimize plans** maintenance
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
сложность/риск 7/10
|
||||
- Issue: [#261](https://github.com/Matysh/houseplan-card/issues/261)
|
||||
- Ветка: `issue/261-white-wedges-root-cause`
|
||||
- Статус ТЗ: первая редакция, ожидает ревью
|
||||
- Статус ТЗ: реализовано, ожидает код-ревью
|
||||
|
||||
Канонические документы: `docs/SCOPE.md`, `docs/USER-GUIDE.ru.md`,
|
||||
`docs/ARCHITECTURE.md`, `docs/WALL-THICKNESS.md`,
|
||||
|
||||
@@ -218,6 +218,25 @@ export const MUTANTS = [
|
||||
replace: ' return v;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'multi-wall-paper-full-origin-cut',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="issue #197 keeps" '
|
||||
+ 'test/wall-thickness.test.mjs',
|
||||
because: 'the exterior paper must retain valid multi-wall overlap up to R; cutting from the '
|
||||
+ 'offset origins recreates the measured white T-junction wedge from #261',
|
||||
patches: [{
|
||||
file: 'src/wall-thickness.ts',
|
||||
find: ' for (const triangle of multiWallBevelTriangles(map)) {',
|
||||
replace: ' for (const triangle of multiWallBevelTrianglesAt(map, false)) {',
|
||||
}, {
|
||||
file: 'src/wall-thickness.ts',
|
||||
find: ' if (envelope) localInside = intersection(localInside, envelope);\n'
|
||||
+ ' else if (centre) localInside = intersection(localInside, centre);',
|
||||
replace: ' if (centre) localInside = intersection(localInside, centre);\n'
|
||||
+ ' else if (envelope) localInside = intersection(localInside, envelope);',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'wall-exact-span-fallback-disabled',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
|
||||
@@ -1996,10 +1996,9 @@ function multiWallBevelTrianglesAt(
|
||||
hit[0] - node.point[0], hit[1] - node.point[1],
|
||||
);
|
||||
if (!Number.isFinite(distance) || distance <= node.limit) continue;
|
||||
// Canonical masonry retains pairwise overlap up to R so ordinary
|
||||
// right-angle arms stay area-connected. The exterior paper uses the
|
||||
// offset origins instead: its job is to remove the complete facade
|
||||
// tooth, and the room centre is unioned back immediately afterwards.
|
||||
// Canonical masonry and its exterior paper retain pairwise overlap up
|
||||
// to R so ordinary right-angle arms stay area-connected. Starting the
|
||||
// cut at the offset origins removes valid exterior half-wall material.
|
||||
const advanceA = retainToLimit ? Math.sqrt(Math.max(
|
||||
0, node.limit * node.limit - a.halfDepth * a.halfDepth,
|
||||
)) : 0;
|
||||
@@ -2085,8 +2084,11 @@ function bevelMultiWallBody(
|
||||
]) as any);
|
||||
if (!local) continue;
|
||||
let localInside = intersection(local, closedRing(mask) as any);
|
||||
if (centre) localInside = intersection(localInside, centre);
|
||||
else if (envelope) localInside = intersection(localInside, envelope);
|
||||
// `envelope` is the bounded physical paper, including the exterior
|
||||
// half-walls. Clipping the repair to the room-centre union first drops
|
||||
// exactly the valid T-junction wedge this reconstruction must retain.
|
||||
if (envelope) localInside = intersection(localInside, envelope);
|
||||
else if (centre) localInside = intersection(localInside, centre);
|
||||
const outside = difference(boundedCurrent, closedRing(mask) as any);
|
||||
const preservedExterior = centre
|
||||
? difference(
|
||||
@@ -2111,7 +2113,7 @@ function bevelMultiWallPaper(
|
||||
map: MultiWallNodeMap,
|
||||
): any {
|
||||
let beveled = paper;
|
||||
for (const triangle of multiWallBevelTrianglesAt(map, false)) {
|
||||
for (const triangle of multiWallBevelTriangles(map)) {
|
||||
try {
|
||||
beveled = difference(beveled, closedRing(triangle) as any);
|
||||
} catch {
|
||||
|
||||
@@ -371,6 +371,7 @@ test('issue #197 golden keeps the complete junction fixture in Plan and View', (
|
||||
for (const scenario of scenarios) {
|
||||
assert.equal(scenario.theme, 'dark');
|
||||
assert.equal(scenario.junctionPatchResilience, true);
|
||||
assert.deepEqual(scenario.retainedWedgeProbe, [0.8955, 0.556]);
|
||||
const fixture = prepareGoldenFixture(scenario);
|
||||
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
|
||||
assert.ok(space);
|
||||
|
||||
@@ -1330,6 +1330,15 @@ test('issue #197 keeps the full masonry when one virtual-junction patch has ULP
|
||||
const intervals = wallIntervals(
|
||||
rooms, walls, cuts, pitch, fixture.cell_cm, GRID_PITCH, NORM_W,
|
||||
);
|
||||
const multiWallMap = buildMultiWallNodeMap(
|
||||
intervals, pitch * NORM_W * 0.04 * 4, NORM_W,
|
||||
);
|
||||
const affectedNode = multiWallMap.nodes.find((node) =>
|
||||
Math.hypot(node.point[0] - 887.5, node.point[1] - 550) < 1e-6);
|
||||
assert.equal(affectedNode?.rays.length, 3, 'the affected T-junction lost an incident ray');
|
||||
assert.equal(multiWallMap.nodes.some((node) =>
|
||||
Math.hypot(node.point[0] - 620.8333333333334, node.point[1] - 550) < 1e-6), false,
|
||||
'the zero-depth shared edge became a physical multi-wall node');
|
||||
const nodeCms = intervals
|
||||
.filter((iv) => Math.abs(iv.a[1] - 550) < 1e-6
|
||||
&& Math.abs(iv.b[1] - 550) < 1e-6)
|
||||
@@ -1369,11 +1378,42 @@ test('issue #197 keeps the full masonry when one virtual-junction patch has ULP
|
||||
assert.ok(geometry, 'one rejected junction patch must not erase the whole plan');
|
||||
assert.ok(geometry.geom.length > 0);
|
||||
assert.ok(geometry.paperGeom.length > 0);
|
||||
const retainedWedgeProbe = [895.5, 556];
|
||||
assertProbeInside(
|
||||
geometry.roomGeom, retainedWedgeProbe,
|
||||
'the room masonry lost the bounded exterior half-wall at the T-junction',
|
||||
);
|
||||
assertProbeInside(
|
||||
geometry.geom, retainedWedgeProbe,
|
||||
'the canonical masonry lost the bounded exterior half-wall at the T-junction',
|
||||
);
|
||||
assertProbeInside(
|
||||
geometry.paperGeom, retainedWedgeProbe,
|
||||
'the paper exposed the scene background through the bounded T-junction',
|
||||
);
|
||||
for (const ray of affectedNode.rays) assertProbeInside(
|
||||
geometry.roomGeom,
|
||||
[
|
||||
affectedNode.point[0] + ray.u[0] * affectedNode.limit * 0.5,
|
||||
affectedNode.point[1] + ray.u[1] * affectedNode.limit * 0.5,
|
||||
],
|
||||
'the repaired T-junction disconnected an incident physical ray',
|
||||
);
|
||||
for (const room of rooms) {
|
||||
const cleanFloor = innerContourForRoom(
|
||||
rooms, room.id, walls, cuts, pitch, fixture.cell_cm, GRID_PITCH, NORM_W,
|
||||
geometry.roomGeom,
|
||||
);
|
||||
if (cleanFloor) assertProbeOutside(
|
||||
closedGeometry(cleanFloor), retainedWedgeProbe,
|
||||
`room ${room.id} clean floor leaked into the retained T-junction wedge`,
|
||||
);
|
||||
}
|
||||
// #249 intentionally bevels degree-3+ nodes in this older fixture too.
|
||||
// #249 retains the physical multi-wall overlap up to R instead of reducing
|
||||
// right-angle arms to point contacts.
|
||||
closeTo(geometryArea(geometry.geom), 124512.89263371378, 1e-6);
|
||||
closeTo(geometryArea(geometry.paperGeom), 727248.4374999999, 1e-6);
|
||||
closeTo(geometryArea(geometry.geom), 124568.27047237023, 1e-6);
|
||||
closeTo(geometryArea(geometry.paperGeom), 727303.8153386558, 1e-6);
|
||||
assert.equal(
|
||||
JSON.stringify({ rooms, walls, cuts, openings, extraBodies }), before,
|
||||
'rendering mutated persisted input',
|
||||
|
||||
Reference in New Issue
Block a user