fix: quantise the light aperture amount of moving covers (#366)

A gate or door bound to a position-reporting cover fed current_position
into the light-barrier signature at toFixed(3) precision: every percent of
movement produced a new fingerprint, a full physicalBodyParts recompute
and a recut — up to ~100 heavy passes per gate cycle, plus LRU churn.

The light pipeline now consumes one quantised amount
(OPENING_LIGHT_AMOUNT_QUANTUM = 0.05, exact 0 and 1 nodes) at the single
point that feeds BOTH the signature and the cut geometry, so the cache key
and the drawn aperture agree by construction and a full sweep costs at
most 21 recomputes. The door LEAF animation stays smooth — _openingAmt is
quantised only for the light pipeline, nowhere else. Binary contact doors
are byte-identical to the previous behaviour (pinned by unit).

Assumption recorded in the spec: the 5% visual step of the light cut is
indistinguishable on real plans; if field impressions disagree, the
quantum is a one-constant change (0.02 => <=51 recomputes) or the decision
falls back to a debounce. LIGHT.md §Caching documents the grid.

Issue: #366
User-Visible: yes
This commit is contained in:
Codex
2026-08-29 08:15:51 +00:00
committed by claude[bot]
parent 0d4010f87d
commit e758c3d9fb
34 changed files with 391 additions and 310 deletions
+4 -1
View File
@@ -22,6 +22,7 @@ import {
pointOnBoundary, mergeRooms, splitRoomPath, polygonArea, closestPointOnBoundary, pointStrictlyInside as ptInside, islandsOf, sharedBoundary, distToSegment, outlineWithout, cutSegments, alignGuides, segmentAngle, is45, isExact45Vector, type AlignGuide, swipeTarget, clampScale, migratePdfUrls, roomFillModeOf, roomGlowOf, contentUrl,
snapToWall, snapPointAlongPoly, openingAmount, openingShoulders, interiorPoint,
isInteriorLightOpeningType, openingLightApertureLength, openingLightStateSignature,
quantizeOpeningLightAmount,
openingEntityReferences, filterOpeningEntityCandidates,
poleOfInaccessibility, subst,
averageLqi, fitView, declump, safeUrl, floorsOf, type FloorInfo,
@@ -10375,7 +10376,9 @@ export class HouseplanCard extends LitElement {
const ny = Math.cos(rad);
const interior = onFloor([o.rx + nx * probe, o.ry + ny * probe])
&& onFloor([o.rx - nx * probe, o.ry - ny * probe]);
return interior ? [{ opening: o, amount: this._openingAmt(o) }] : [];
// #366: the light pipeline consumes the quantised amount — the door
// LEAF animation stays smooth (_openingAmt elsewhere), only light steps.
return interior ? [{ opening: o, amount: quantizeOpeningLightAmount(this._openingAmt(o)) }] : [];
});
const openingStateSignature = openingLightStateSignature(
passageStates.map(({ opening, amount }) => ({
+16
View File
@@ -342,6 +342,22 @@ export interface OpeningLightStateEntry {
amount: number;
}
/**
* #366: one grid for the light pipeline's aperture amount. A moving cover
* publishes current_position by the percent; unquantised it forces a full
* barrier recompute per tick (~100 per gate cycle). Both the signature AND
* the cut geometry consume the same quantised value, so the cache key and
* the drawn aperture agree by construction. 0 and 1 are exact grid nodes —
* binary contact doors are byte-identical to the pre-#366 behaviour.
*/
export const OPENING_LIGHT_AMOUNT_QUANTUM = 0.05;
export function quantizeOpeningLightAmount(amount: number): number {
const safe = Number.isFinite(amount) ? Math.max(0, Math.min(1, amount)) : 0;
return Math.min(1, Math.max(0,
Math.round(safe / OPENING_LIGHT_AMOUNT_QUANTUM) * OPENING_LIGHT_AMOUNT_QUANTUM));
}
/** State-only identity for interior light apertures. Structural geometry is
* fingerprinted separately, so unrelated HA updates cannot evict barriers. */
export function openingLightStateSignature(