mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 12:49:56 +00:00
+49
-27
@@ -71,6 +71,8 @@ export interface IsoOverlayPlacementInput extends IsoOverlayOwnerInput {
|
||||
/** Internal scene-builder fast path; arbitrary callers still resolve safely. */
|
||||
ownerAlreadyResolved?: boolean;
|
||||
resolvedOwner?: IsoOverlayOwner | null;
|
||||
/** Internal zoom fast path. The hint is accepted only after an exact safety check. */
|
||||
nudgeHintCss?: number;
|
||||
}
|
||||
|
||||
export interface IsoOverlayTetherGeometry {
|
||||
@@ -440,6 +442,7 @@ export function resolveIsoOverlayPlacement(input: IsoOverlayPlacementInput): Iso
|
||||
|
||||
let distanceCss = 0;
|
||||
let nudgeScene: ScenePoint = [0, 0];
|
||||
let knownNearWallAfter: boolean | null = null;
|
||||
let capped = false;
|
||||
let status: IsoOverlayPlacement['status'] = geometryValid ? 'ok' : 'degraded';
|
||||
let reason: IsoOverlayPlacement['reason'] = geometryValid ? null : 'invalid-wall-geometry';
|
||||
@@ -501,38 +504,57 @@ export function resolveIsoOverlayPlacement(input: IsoOverlayPlacementInput): Iso
|
||||
plateNearSilhouette(plate, bounds, wall, gapUnits,
|
||||
input.wallGeometryValidated === true));
|
||||
};
|
||||
let clearAt: number | null = null;
|
||||
let previous = 0;
|
||||
let ownerBoundary = false;
|
||||
const samples = Math.ceil(searchLimitCss);
|
||||
for (let sample = 1; sample <= samples; sample++) {
|
||||
const candidate = Math.min(searchLimitCss, sample);
|
||||
const ratio = candidate * unitsPerPixel / length;
|
||||
const candidatePlan: PlanPoint = [
|
||||
const hint = input.nudgeHintCss;
|
||||
if (Number.isFinite(hint) && hint! >= 0 && hint! <= searchLimitCss) {
|
||||
const ratio = hint! * unitsPerPixel / length;
|
||||
const hintPlan: PlanPoint = [
|
||||
floorAnchor[0] + (owner.safePoint[0] - floorAnchor[0]) * ratio,
|
||||
floorAnchor[1] + (owner.safePoint[1] - floorAnchor[1]) * ratio,
|
||||
];
|
||||
if (!pathSafeToLimit
|
||||
&& (!ownerRoom || !segmentStrictlyInRoom(floorAnchor, candidatePlan, ownerRoom))) {
|
||||
ownerBoundary = true;
|
||||
break;
|
||||
if ((pathSafeToLimit
|
||||
|| !!ownerRoom && segmentStrictlyInRoom(floorAnchor, hintPlan, ownerRoom))
|
||||
&& !collidesAt(hint!)) {
|
||||
// A wheel step changes only the CSS/scene scale. Reusing the last
|
||||
// distance after proving it still clear avoids another 1 px scan
|
||||
// plus binary search for every raised marker.
|
||||
distanceCss = hint!;
|
||||
knownNearWallAfter = false;
|
||||
}
|
||||
if (!collidesAt(candidate)) { clearAt = candidate; break; }
|
||||
previous = candidate;
|
||||
}
|
||||
if (clearAt !== null) {
|
||||
let low = previous, high = clearAt;
|
||||
for (let iteration = 0; iteration < ISO_OVERLAY_NUDGE_SEARCH_ITERATIONS; iteration++) {
|
||||
const middle = (low + high) / 2;
|
||||
if (collidesAt(middle)) low = middle;
|
||||
else high = middle;
|
||||
if (knownNearWallAfter === null) {
|
||||
let clearAt: number | null = null;
|
||||
let previous = 0;
|
||||
let ownerBoundary = false;
|
||||
const samples = Math.ceil(searchLimitCss);
|
||||
for (let sample = 1; sample <= samples; sample++) {
|
||||
const candidate = Math.min(searchLimitCss, sample);
|
||||
const ratio = candidate * unitsPerPixel / length;
|
||||
const candidatePlan: PlanPoint = [
|
||||
floorAnchor[0] + (owner.safePoint[0] - floorAnchor[0]) * ratio,
|
||||
floorAnchor[1] + (owner.safePoint[1] - floorAnchor[1]) * ratio,
|
||||
];
|
||||
if (!pathSafeToLimit
|
||||
&& (!ownerRoom || !segmentStrictlyInRoom(floorAnchor, candidatePlan, ownerRoom))) {
|
||||
ownerBoundary = true;
|
||||
break;
|
||||
}
|
||||
if (!collidesAt(candidate)) { clearAt = candidate; break; }
|
||||
previous = candidate;
|
||||
}
|
||||
if (clearAt !== null) {
|
||||
let low = previous, high = clearAt;
|
||||
for (let iteration = 0; iteration < ISO_OVERLAY_NUDGE_SEARCH_ITERATIONS; iteration++) {
|
||||
const middle = (low + high) / 2;
|
||||
if (collidesAt(middle)) low = middle;
|
||||
else high = middle;
|
||||
}
|
||||
distanceCss = high;
|
||||
} else {
|
||||
distanceCss = previous;
|
||||
capped = true;
|
||||
status = 'degraded';
|
||||
reason = ownerBoundary ? 'owner-boundary' : 'nudge-cap';
|
||||
}
|
||||
distanceCss = high;
|
||||
} else {
|
||||
distanceCss = previous;
|
||||
capped = true;
|
||||
status = 'degraded';
|
||||
reason = ownerBoundary ? 'owner-boundary' : 'nudge-cap';
|
||||
}
|
||||
nudgeScene = [ux * distanceCss * unitsPerPixel, uy * distanceCss * unitsPerPixel];
|
||||
}
|
||||
@@ -542,7 +564,7 @@ export function resolveIsoOverlayPlacement(input: IsoOverlayPlacementInput): Iso
|
||||
const visualScene: ScenePoint = [raisedScene[0] + nudgeScene[0], raisedScene[1] + nudgeScene[1]];
|
||||
const plate = buildIsoPlatePolygon(floorAnchor, input.plateHalfSize,
|
||||
raisedHeight, camera, nudgeScene);
|
||||
const nearWallAfter = geometryValid ? isNear(plate) : true;
|
||||
const nearWallAfter = geometryValid ? knownNearWallAfter ?? isNear(plate) : true;
|
||||
const nudged = distanceCss > EPS;
|
||||
const tetherVisible = nudged || nearWallBefore || nearWallAfter
|
||||
|| !!input.hovered || !!input.focused || !!input.selected;
|
||||
|
||||
+28
-4
@@ -708,7 +708,12 @@ export interface IsoOverlaySceneInput {
|
||||
|
||||
type IsoOverlayRoomRow = { room: RoomCfg; overlayRoom: IsoOverlayRoom };
|
||||
const isoOverlayRoomCache = new WeakMap<readonly RoomCfg[], readonly IsoOverlayRoomRow[]>();
|
||||
type IsoOverlayPlacementCacheEntry = { signature: string; placement: IsoOverlayPlacement };
|
||||
type IsoOverlayPlacementCacheEntry = {
|
||||
signature: string;
|
||||
shapeSignature: string;
|
||||
unitsPerPixel: number;
|
||||
placement: IsoOverlayPlacement;
|
||||
};
|
||||
type IsoOverlayOwnerCacheEntry = { signature: string; owner: IsoOverlayPlacement['owner'] };
|
||||
export const ISO_OVERLAY_PLACEMENT_CACHE_LIMIT = 2048;
|
||||
const isoOverlayPlacementCache = new WeakMap<
|
||||
@@ -797,11 +802,26 @@ export function buildIsoOverlayRenderScene(input: IsoOverlaySceneInput): IsoOver
|
||||
): IsoOverlayPlacement => {
|
||||
const collisionMode = input.resolveCollisions === false ? 'fit' : 'live';
|
||||
const cacheKey = `${collisionMode}\u0000${kind}\u0000${id}`;
|
||||
const signature = [floorAnchor[0], floorAnchor[1], plateHalfSize[0], plateHalfSize[1],
|
||||
preferredRoomId || '', wallHeight, visualOffset, unitsPerPixel,
|
||||
const shapeSignature = [floorAnchor[0], floorAnchor[1], plateHalfSize[0], plateHalfSize[1],
|
||||
preferredRoomId || '', wallHeight, visualOffset,
|
||||
input.layers.shadows ? 1 : 0, selected ? 1 : 0].join('|');
|
||||
const signature = `${shapeSignature}|${unitsPerPixel}`;
|
||||
const cached = lruRead(placements!, cacheKey);
|
||||
if (cached.hit && cached.value.signature === signature) return cached.value.placement;
|
||||
if (cached.hit && cached.value.shapeSignature === shapeSignature
|
||||
&& unitsPerPixel <= cached.value.unitsPerPixel) {
|
||||
const previous = cached.value.placement;
|
||||
const distanceAtNewScale = previous.nudgeDistanceCss
|
||||
* cached.value.unitsPerPixel / unitsPerPixel;
|
||||
// Zooming in only shrinks the required scene-unit safety gap. An
|
||||
// unchanged plate that was clear therefore stays clear; an unchanged
|
||||
// non-near plate stays non-near. Reuse the immutable scene placement so
|
||||
// Lit also keeps the raised SVG subtree. Once its CSS displacement would
|
||||
// exceed the public cap, fall through to the exact resolver.
|
||||
if (!previous.nearWallBefore
|
||||
|| previous.cleared && distanceAtNewScale <= ISO_OVERLAY_MAX_NUDGE_CSS_PX)
|
||||
return previous;
|
||||
}
|
||||
const ownerKey = `${kind}\u0000${id}`;
|
||||
const ownerSignature = [floorAnchor[0], floorAnchor[1], preferredRoomId || ''].join('|');
|
||||
const cachedOwner = lruRead(owners!, ownerKey);
|
||||
@@ -821,6 +841,9 @@ export function buildIsoOverlayRenderScene(input: IsoOverlaySceneInput): IsoOver
|
||||
preferredRoomId,
|
||||
ownerAlreadyResolved: true,
|
||||
resolvedOwner: owner,
|
||||
nudgeHintCss: cached.hit && cached.value.shapeSignature === shapeSignature
|
||||
? cached.value.placement.nudgeDistanceCss * cached.value.unitsPerPixel / unitsPerPixel
|
||||
: undefined,
|
||||
showBorders: true,
|
||||
wallSilhouettes: input.resolveCollisions === false ? [] : input.wallSilhouettes,
|
||||
wallGeometryValidated: true,
|
||||
@@ -834,7 +857,8 @@ export function buildIsoOverlayRenderScene(input: IsoOverlaySceneInput): IsoOver
|
||||
hovered: true,
|
||||
selected,
|
||||
});
|
||||
lruWrite(placements!, cacheKey, { signature, placement }, ISO_OVERLAY_PLACEMENT_CACHE_LIMIT);
|
||||
lruWrite(placements!, cacheKey, { signature, shapeSignature, unitsPerPixel, placement },
|
||||
ISO_OVERLAY_PLACEMENT_CACHE_LIMIT);
|
||||
return placement;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user