mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
fix: no jags on the edges of a degenerate apex (#329 §4)
Owner report: small serrations remained on the outer edges between the inner and the outer vertex. Measured on the fixture ring: two ~4 cm steps plus four micro-vertices at the tip. Their source was the inset contour's two-point bevel folding into a bow-tie, and the earlier half-plane clip of that fold, which left a 0.2 cm sliver the boolean union turned into steps. The inset now ends in ITS own mitre point at a degenerate apex — mirroring the sharp outer tip — so there is no fold to clip and no sliver to smear: the room ring is exactly three vertices, every side longer than the half depth. The clip helper and its cap plumbing are gone. The user-visible wording of this work already stands in both changelogs from the #329 entry. Issue: #329 User-Visible: no
This commit is contained in:
File diff suppressed because one or more lines are too long
Vendored
+115
-115
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
||||
"version": 1,
|
||||
"fixture": "synthetic-only",
|
||||
"chromium": "151.0.7922.34",
|
||||
"sourceFingerprint": "ab24211e8f36804dc9f508e4bae2446801cb332b91b5a75a3649a50f2376edc2",
|
||||
"sourceFingerprint": "41564f7e31f09f936503145b1d660462ee0bc4d757afaefc03d474766192c5e6",
|
||||
"captureScriptSha256": "ce2e9542fed9dade3085be87d16f69adb2ac8262893ad78ad966b1b9673f2983",
|
||||
"command": "npm run build && node demo/docs/capture.mjs",
|
||||
"scenarios": {
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "ab24211e8f36804dc9f508e4bae2446801cb332b91b5a75a3649a50f2376edc2",
|
||||
"sourceSha256": "41564f7e31f09f936503145b1d660462ee0bc4d757afaefc03d474766192c5e6",
|
||||
"imageSha256": "36223106c073f07d8cc3ecf8eaab37192ebb2687daba65c5c21047d0b7890de0"
|
||||
},
|
||||
"view-touch": {
|
||||
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "ab24211e8f36804dc9f508e4bae2446801cb332b91b5a75a3649a50f2376edc2",
|
||||
"sourceSha256": "41564f7e31f09f936503145b1d660462ee0bc4d757afaefc03d474766192c5e6",
|
||||
"imageSha256": "41e3ba67f8db0e98f26f484293af83ef937c369ca5ca6a59a3350d8954c906f4"
|
||||
},
|
||||
"space-create": {
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "ab24211e8f36804dc9f508e4bae2446801cb332b91b5a75a3649a50f2376edc2",
|
||||
"sourceSha256": "41564f7e31f09f936503145b1d660462ee0bc4d757afaefc03d474766192c5e6",
|
||||
"imageSha256": "0dc024726327f60f4a9ceaf3044381691f81f1090af81d1812e870f22d9343ba"
|
||||
},
|
||||
"room-contour-close": {
|
||||
@@ -47,7 +47,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "ab24211e8f36804dc9f508e4bae2446801cb332b91b5a75a3649a50f2376edc2",
|
||||
"sourceSha256": "41564f7e31f09f936503145b1d660462ee0bc4d757afaefc03d474766192c5e6",
|
||||
"imageSha256": "44cfaa95bd51e2cd628400e28db0ad8b2f0cd904385845bd402494f3f5c0d93c"
|
||||
},
|
||||
"plan-context-tray": {
|
||||
@@ -58,7 +58,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "ab24211e8f36804dc9f508e4bae2446801cb332b91b5a75a3649a50f2376edc2",
|
||||
"sourceSha256": "41564f7e31f09f936503145b1d660462ee0bc4d757afaefc03d474766192c5e6",
|
||||
"imageSha256": "e0662429b423fb74151b583dcc2c8635b001b637d03bbf7a6b16aec46399c3f8"
|
||||
},
|
||||
"device-editor": {
|
||||
@@ -69,7 +69,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "ab24211e8f36804dc9f508e4bae2446801cb332b91b5a75a3649a50f2376edc2",
|
||||
"sourceSha256": "41564f7e31f09f936503145b1d660462ee0bc4d757afaefc03d474766192c5e6",
|
||||
"imageSha256": "d0ffd31ce80bfde21ab75da356a5fc1af38246f2b301030880320620c228d89d"
|
||||
},
|
||||
"device-display-preview": {
|
||||
@@ -80,7 +80,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "ab24211e8f36804dc9f508e4bae2446801cb332b91b5a75a3649a50f2376edc2",
|
||||
"sourceSha256": "41564f7e31f09f936503145b1d660462ee0bc4d757afaefc03d474766192c5e6",
|
||||
"imageSha256": "2cdabae1f89c3286e4fac0ce30f757ee1690b707ab8a5488748b7cd420626160"
|
||||
},
|
||||
"background-editor": {
|
||||
@@ -91,7 +91,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "ab24211e8f36804dc9f508e4bae2446801cb332b91b5a75a3649a50f2376edc2",
|
||||
"sourceSha256": "41564f7e31f09f936503145b1d660462ee0bc4d757afaefc03d474766192c5e6",
|
||||
"imageSha256": "30147bb00a90eea7136b4cee30995f6e6a9217b5132f3e8d3ad7471413b1af8a"
|
||||
},
|
||||
"room-card": {
|
||||
@@ -102,7 +102,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "ab24211e8f36804dc9f508e4bae2446801cb332b91b5a75a3649a50f2376edc2",
|
||||
"sourceSha256": "41564f7e31f09f936503145b1d660462ee0bc4d757afaefc03d474766192c5e6",
|
||||
"imageSha256": "029a3e69ec647a8a370d99e6bb7f9225833c526739076022f6b52ba54bff30ea"
|
||||
},
|
||||
"device-info": {
|
||||
@@ -113,7 +113,7 @@
|
||||
},
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"sourceSha256": "ab24211e8f36804dc9f508e4bae2446801cb332b91b5a75a3649a50f2376edc2",
|
||||
"sourceSha256": "41564f7e31f09f936503145b1d660462ee0bc4d757afaefc03d474766192c5e6",
|
||||
"imageSha256": "dd492f53150b7149085daada5cce9eeae9bde9e7ea1d86679a54b3041f72f517"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Issue #329 — ограничения стыков стен и честная острая вершина
|
||||
|
||||
Статус: ревизия 4 (r1: M1/M2/L1; r2: M1 — AC7; поправка владельца 2026-08-27: §4 — острая вершина, не фаска). Решения владельца (чат, 2026-08-27, зафиксированы в issue):
|
||||
Статус: ревизия 5 (r1: M1/M2/L1; r2: M1 — AC7; поправки владельца 2026-08-27: §4 — острая вершина без фаски и без зазубрин на гранях). Решения владельца (чат, 2026-08-27, зафиксированы в issue):
|
||||
пять ограничений приняты; проверки действуют на записи, легаси читается как
|
||||
есть; для остаточных острых вершин легаси-планов рендер закрывает остриё
|
||||
честной фаской вместо «трезубца».
|
||||
@@ -77,9 +77,12 @@
|
||||
обеих стен) даёт **одну точку внешнего контура ровно в вершине плана**:
|
||||
ни двухточечной фаски, ни иглы mitre, уходящей на десятки сантиметров за
|
||||
стены, которые её образуют;
|
||||
- внутренний контур в этой зоне обрезается по линии смыкания внутренних
|
||||
граней, поэтому складка не вычитается и V-вырезов не возникает; кладка
|
||||
выше линии сплошная, штриховка непрерывна;
|
||||
- внутренний контур сходится в СВОЮ вершину (одна точка mitre) вместо
|
||||
двухточечной фаски, поэтому «бабочки» не возникает вовсе: вычитать нечего,
|
||||
V-вырезов нет, кладка выше сплошная, штриховка непрерывна;
|
||||
- на внешних гранях между внутренней и внешней вершиной не остаётся ступенек
|
||||
(вторая поправка владельца): кольцо комнаты — ровно треугольник, каждая
|
||||
сторона длиннее полутолщины, микро-вершин и «щепок» нет;
|
||||
- обычные и просто острые пары (#310, вершина вне зоны перекрытия) не
|
||||
меняются: их полный mitre сохраняется.
|
||||
|
||||
@@ -143,8 +146,10 @@ margin): правила П1–П5 применяются к элементам,
|
||||
вычета тел < 25 см², запись отклоняется по П5. Тот же треугольник со
|
||||
стороной 120 см (inradius ≈ 34.6 см) проходит. Юнит с этими числами.
|
||||
- **AC6 (§4).** Легаси-фикстура issue рендерится без «трезубца»: у вершины
|
||||
ровно одна точка внешнего контура, стоящая в вершине плана; ноль V-вырезов;
|
||||
golden-сцена + юнит на контуре.
|
||||
ровно одна точка внешнего контура, стоящая в вершине плана; внутренний
|
||||
контур — треугольник (ровно 3 точки, не «бабочка»); внешнее кольцо тела —
|
||||
ровно 3 различные вершины без ступенек и микро-сегментов; golden-сцена +
|
||||
юниты на контурах и на кольце.
|
||||
- **AC7a (Resize, канал §2).** Resize, приводящий к нарушению любого из
|
||||
П1–П5, упирается в последнюю допустимую позицию; при отсутствии
|
||||
безопасного шага ручка приглушена, и её объяснение (hover/focus/нажатие)
|
||||
|
||||
+12
-29
@@ -1670,6 +1670,14 @@ export function insetContour(
|
||||
out.push(hit);
|
||||
continue;
|
||||
}
|
||||
// #329: at a degenerate apex the two-point bevel folds the contour over
|
||||
// itself (a bow-tie), and subtracting that fold carved notches in the
|
||||
// masonry. The interior simply ends in ITS own apex — the single mitre
|
||||
// point — mirroring the sharp outer tip.
|
||||
if (isDegenerateApexCorner(poly, offsets, i)) {
|
||||
out.push(hit);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// bevel: two points, each edge's offset line stopped at the vertex offset
|
||||
if (oA > 0) out.push([poly[i][0] + nAx * oA, poly[i][1] + nAy * oA]);
|
||||
@@ -3768,11 +3776,7 @@ export function wallBodiesGeometry(
|
||||
const outC = outsetContour(pr.poly, pr.offsets, multiWallNodes);
|
||||
const inC = insetContour(pr.poly, pr.offsets, multiWallNodes);
|
||||
if (!outC) continue;
|
||||
// #329 §4: a legacy corner whose bodies overlap completely folds its
|
||||
// inset contour over itself; carry the clip regions with the ring.
|
||||
roomRings.push({
|
||||
outset: outC, inset: inC, apexCaps: degenerateApexCaps(pr.poly, pr.offsets),
|
||||
});
|
||||
roomRings.push({ outset: outC, inset: inC });
|
||||
}
|
||||
for (const body of extraBodies) {
|
||||
const xs = body.map((p) => p[0]), ys = body.map((p) => p[1]);
|
||||
@@ -3816,30 +3820,9 @@ export function wallBodiesGeometry(
|
||||
const bodyOf = (ring: typeof roomRings[number]): any => {
|
||||
const outset: any = closedRing(ring.outset);
|
||||
if (!ring.inset) return outset;
|
||||
let insetPoly = ring.inset;
|
||||
// #329 §4: at a degenerate apex the inset contour folds over itself
|
||||
// (a bow-tie); differencing that fold carved the V-notches. Clip the
|
||||
// fold away with the half-plane through the convergence point, so the
|
||||
// interior simply ends there and the masonry above stays solid.
|
||||
for (const cap of ring.apexCaps || []) {
|
||||
const clipped = clipPolygonOutsideCap(insetPoly, cap);
|
||||
if (clipped && clipped.length >= 3) insetPoly = clipped;
|
||||
}
|
||||
let hole: any = closedRing(insetPoly);
|
||||
// #329 §4 (owner correction 2026-08-27): at a degenerate apex the two
|
||||
// wall bodies overlap completely, and the inset contour folds over
|
||||
// itself there — that fold is what carved the "trident" of pikes and
|
||||
// V-notches. Clip the HOLE at the convergence point instead of cutting
|
||||
// the masonry: above it the wall is solid and the apex stays a normal
|
||||
// sharp point, exactly as a drawing would show it.
|
||||
for (const cap of ring.apexCaps || []) {
|
||||
try {
|
||||
const clipped = difference(hole, closedRing(cap) as any);
|
||||
if (clipped) hole = clipped;
|
||||
} catch {
|
||||
// Keep the hole we have; a failed clip must not lose the interior.
|
||||
}
|
||||
}
|
||||
// #329: the interior now ends in its own apex point (insetContour),
|
||||
// so the ring is a plain difference — no fold to clip, no slivers.
|
||||
const hole: any = closedRing(ring.inset);
|
||||
return difference(outset, hole);
|
||||
};
|
||||
corePhase = 'room-rings';
|
||||
|
||||
@@ -157,3 +157,31 @@ test('наследование считается по правилам, а не
|
||||
[{ rule: 'angle', subject: 'n', actual: 9, limit: 15 }], [],
|
||||
).length, 1);
|
||||
});
|
||||
|
||||
test('§4: у вырожденной вершины грани прямые — ни зазубрин, ни щепок', async () => {
|
||||
const { wallBodiesGeometry, insetContour, roomWallProfile } =
|
||||
await import('../test-build/wall-thickness.js');
|
||||
const room = {
|
||||
id: 'spike', name: 'Spike', area: null,
|
||||
poly: [[3.6417, 1.8333], [3.7167, 0.2417], [3.9083, 1.7667]],
|
||||
};
|
||||
const walls = room.poly.map((point, index) => ({
|
||||
key: `w${index}`, a: point, b: room.poly[(index + 1) % room.poly.length], cm: 15,
|
||||
}));
|
||||
const profile = roomWallProfile([room], room.id, walls, [], PITCH, CELL, PITCH, 1);
|
||||
// Внутренний контур сходится в СВОЮ вершину — четвёртой точки-складки нет.
|
||||
const inset = insetContour(profile.poly, profile.offsets, null);
|
||||
assert.equal(inset.length, 3, 'внутренний контур — треугольник, а не «бабочка»');
|
||||
|
||||
const geometry = wallBodiesGeometry([room], walls, [], [], PITCH, CELL, PITCH, 1);
|
||||
assert.equal(geometry.status, 'ok');
|
||||
const outer = geometry.roomGeom[0][0];
|
||||
const distinct = outer.slice(0, -1);
|
||||
assert.equal(distinct.length, 3, 'внешнее кольцо — треугольник без ступенек');
|
||||
// Никаких микро-вершин: каждая сторона длиннее полутолщины.
|
||||
for (let index = 0; index < distinct.length; index++) {
|
||||
const next = distinct[(index + 1) % distinct.length];
|
||||
const side = Math.hypot(next[0] - distinct[index][0], next[1] - distinct[index][1]);
|
||||
assert.ok(side > cm(7.5), `сторона ${index} не вырождена: ${side}`);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user