room settings button: the VISUAL centre for L-shaped rooms

The owner's kitchen-living room is L-shaped, and interiorPoint() only
promises 'somewhere inside' — the button sat near the seam, visibly
off-centre. poleOfInaccessibility() (largest inscribed circle, grid search
plus one refinement pass) puts it in the middle of the widest open space:
the slab of an L, the exact centre of a rectangle or square. Cached per
poly array in a WeakMap — the memoized model keeps the arrays stable, so
the search runs once per geometry, not per render.

Unit: square -> centre; thick-slab L -> mid-slab, always inside.
This commit is contained in:
Matysh
2026-07-29 13:55:03 +03:00
parent 0bb9282edd
commit 9eec856d50
5 changed files with 84 additions and 14 deletions
+13 -4
View File
@@ -15,7 +15,7 @@ import {
lqiColor, snapToGrid, samePoint, pointInPolygon, markerIdForBinding,
segmentCm, formatLength, roomEdges, roomPoly, pointStrictlyInside, roomsOverlap,
pointOnBoundary, mergeRooms, splitRoomPath, polygonArea, closestPointOnBoundary, pointStrictlyInside as ptInside, islandsOf, sharedBoundary, openZoneOf, distToSegment, outlineWithout, cutSegments, alignGuides, segmentAngle, is45, type AlignGuide, swipeTarget, clampScale, migratePdfUrls, roomFillModeOf, contentUrl,
snapToWall, openingAmount, interiorPoint,
snapToWall, openingAmount, interiorPoint, poleOfInaccessibility,
averageLqi, fitView, declump, safeUrl, resolveTapAction, floorsOf, type FloorInfo,
stateIcon, lightColorOf, isAlarmState, parseRoomRef, diffNewDevices, glowColorOf, doorSector, hasRoomBehind, controlsAction, isControllable,
spaceDisplayOf, roomFillStyle, fillColorsOf, DEFAULT_FILL_COLORS, type FillColors,
@@ -4505,13 +4505,22 @@ class HouseplanCard extends LitElement {
/** The room-settings button: detached from the (movable) label, always at
* the geometric CENTRE of the room, sized at 70% of a device icon and
* therefore zooming with the plan (owner's spec, 2026-07-29). */
private _gearPtCache = new WeakMap<number[][], number[]>();
private _renderRoomGear(
r: RoomCfg, space: SpaceModel, view: { x: number; y: number; w: number; h: number },
): TemplateResult | typeof nothing {
if (!r.id) return nothing;
const c = r.poly
? (interiorPoint(r.poly) || [r.poly[0][0], r.poly[0][1]])
: r.x != null && r.y != null ? [r.x + (r.w || 0) / 2, r.y + (r.h || 0) / 2] : null;
let c: number[] | null = null;
if (r.poly) {
// the VISUAL centre (largest inscribed circle) — interiorPoint only
// promises "inside", which sat visibly off-centre on an L-shaped room.
// The model is memoized, so the poly array is a stable cache key.
c = this._gearPtCache.get(r.poly) || null;
if (!c) { c = poleOfInaccessibility(r.poly); this._gearPtCache.set(r.poly, c); }
} else if (r.x != null && r.y != null) {
c = [r.x + (r.w || 0) / 2, r.y + (r.h || 0) / 2];
}
if (!c) return nothing;
const left = ((c[0] - view.x) / view.w) * 100;
const top = ((c[1] - view.y) / view.h) * 100;