No room border ends in a tooth: walls join round

Owner 2026-08-04: «углы границ комнат всё ещё с зубцами (фиксили для
декоративных линий, они теперь заканчиваются полукружьями, надо сделать
так же для границ комнат)».

A default miter join on a sharp room corner shoots a spike far past the
two walls that meet there, and flips to a flat bevel once the miter
limit clips it — both read as a tooth. Room borders now join ROUND:

* .room (polygon / evenodd path / rect) — one rule, so the plan view,
  the Plan editor and the static space-card all get it;
* .room-outline — a room with open boundaries draws its walls as
  separate M..L subpaths, so its corners are stroke ENDS: round caps
  close them the same way a round join closes a contour;
* .seg — the contour being drawn in the editor already had round caps,
  now it states the join too.

smoke_render_parity checks both renderers and the trimmed outline;
demo/shot_room_joins.mjs is the before/after still (a 45° apex).
This commit is contained in:
Matysh
2026-08-04 11:47:50 +03:00
parent 50ba443492
commit d21d532f10
5 changed files with 141 additions and 16 deletions
+45
View File
@@ -0,0 +1,45 @@
// Owner 2026-08-04: «углы границ комнат всё ещё с зубцами». A miter join on a
// sharp corner shoots a spike far past the walls that meet there; past the
// miter limit it flips to a flat bevel. Room borders now join round, like the
// decor lines already did. Two rooms make the defect obvious: an L (a concave
// 90° corner plus the convex ones) and a wedge with a ~45° apex — the angle
// where the miter spike is longest before the miter limit clips it to a bevel.
// Usage: node demo/shot_room_joins.mjs <outdir> <name>
import { launch } from './serve.mjs';
const outDir = process.argv[2] || '/tmp';
const name = process.argv[3] || 'room_joins';
const { page, browser } = await launch({ width: 900, height: 820 }, 6);
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.evaluate(async () => {
const c = window.__card;
const sp = c._serverCfg.spaces.find((s) => s.id === 'f1');
delete sp.plan_url;
sp.rooms = [
{ id: 'rL', name: '', area: 'living_room',
poly: [[0.06, 0.04], [0.52, 0.04], [0.52, 0.14], [0.94, 0.14], [0.94, 0.26], [0.06, 0.26]] },
{ id: 'rW', name: '', area: 'kitchen',
poly: [[0.20, 0.318], [0.90, 0.55], [0.20, 0.782]] },
];
sp.settings = { ...(sp.settings || {}), show_borders: true, show_names: false,
fill_mode: 'none', room_color: '#3ea6ff', room_opacity: 1 };
c._cfgEpoch = (c._cfgEpoch || 0) + 1;
c.requestUpdate();
await c.updateComplete;
});
await page.waitForTimeout(300);
// crop tight around the wedge's ~28° apex: at plan scale the whole stage hides
// a 5-unit spike, and the spike is the entire point of the shot
const box = await page.evaluate(() => {
const c = window.__card;
const svg = c.shadowRoot.querySelector('.stage svg');
const poly = [...svg.querySelectorAll('polygon.room')].pop();
const pts = poly.getAttribute('points').split(' ').map((s) => s.split(',').map(Number));
const apex = pts.reduce((m, p) => (p[0] > m[0] ? p : m), pts[0]);
const m = svg.getScreenCTM();
return { x: m.a * apex[0] + m.c * apex[1] + m.e, y: m.b * apex[0] + m.d * apex[1] + m.f };
});
await page.screenshot({ path: `${outDir}/${name}.png`,
clip: { x: box.x - 62, y: box.y - 30, width: 72, height: 60 } });
await browser.close();
console.log('shot written to ' + outDir + '/' + name + '.png');
+23
View File
@@ -47,6 +47,29 @@ const res = await page.evaluate(async () => {
out.parity = out.fullCardRoom0 === out.staticCardRoom0;
out.overrideRespected = out.staticCardRoom0 === 'transparent';
// Owner 2026-08-04: «углы границ комнат всё ещё с зубцами» — a miter join
// spikes far past a sharp corner (and bevels it flat once past the miter
// limit). Room borders join ROUND now, and BOTH renderers share the rule:
// the styles live in one cardStyles, and this is the smoke that keeps it so.
const joins = (root) => [...root.querySelectorAll('.room')]
.map((el) => getComputedStyle(el).strokeLinejoin);
const fullJoins = joins(main.shadowRoot || main.renderRoot);
const staticJoins = joins(card.renderRoot);
out.fullCardBordersRound = fullJoins.length > 0 && fullJoins.every((j) => j === 'round');
out.staticCardBordersRound = staticJoins.length > 0 && staticJoins.every((j) => j === 'round');
// the trimmed outline of a room with open boundaries is a set of separate
// subpaths: its CAPS are the corners, so they must be round as well
const f1cfg = cfg.spaces.find((s) => s.id === 'f1');
f1cfg.rooms[0].open_to = [f1cfg.rooms[1].id];
main._cfgEpoch++;
main.requestUpdate(); await main.updateComplete;
const outlines = [...(main.shadowRoot || main.renderRoot).querySelectorAll('.room-outline')];
out.trimmedOutlineDrawn = outlines.length > 0;
out.trimmedOutlineRound = outlines.every((el) => {
const cs = getComputedStyle(el);
return cs.strokeLinejoin === 'round' && cs.strokeLinecap === 'round';
});
return out;
});
// зафиксировано прогоном на v1.46.0 и сверено с кодом
File diff suppressed because one or more lines are too long
+27 -8
View File
File diff suppressed because one or more lines are too long
+19
View File
@@ -420,9 +420,19 @@ export const cardStyles = css`
.stage.daynight .hp-paperg {
filter: drop-shadow(0 2px 8px rgba(10, 16, 26, 0.28));
}
/* Owner 2026-08-04: «углы границ комнат всё ещё с зубцами». A miter join
on a 30-45° corner shoots a spike far past the wall (and flips to an
ugly bevel once past the miter limit) — the same defect the decor lines
had before they got round caps. Every room border, in EVERY renderer
that reuses these styles (plan view, plan editor, static space-card),
joins its walls with a ROUND join instead: the corner reads as the
stroke's own radius, never as a tooth. The linecap matters only for the
open outlines below, but it costs nothing to state it here. */
.room {
transition: 0.12s;
cursor: default; /* v1.40.1: rooms are not clickable — the label's link icon is */
stroke-linejoin: round;
stroke-linecap: round;
}
.room.overlay {
fill: transparent;
@@ -966,9 +976,15 @@ export const cardStyles = css`
.room.noedge {
stroke-opacity: 0 !important;
}
/* rooms with open boundaries draw their walls as separate M..L subpaths,
so a corner between two of them is two stroke ENDS meeting: round caps
fill it in the same way a round join fills a closed contour's corner
(owner 2026-08-04 — no teeth anywhere on a room border). */
.room-outline {
fill: none;
stroke-width: 2.5;
stroke-linejoin: round;
stroke-linecap: round;
pointer-events: none;
}
/* Plan editor: trimmed outlines use the markup blue */
@@ -1077,10 +1093,13 @@ export const cardStyles = css`
opacity: 0.5;
stroke-width: 0;
}
/* the contour being drawn in the Plan editor: each wall is its own <line>,
so the round cap IS the corner (matches the finished .room border) */
.seg {
stroke: var(--hp-accent);
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
}
.pathline {
stroke: #ffc14d;