mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 05:08:53 +00:00
perf: bound isometric overlay collision work
Issue: #160 User-Visible: no
This commit is contained in:
@@ -6119,7 +6119,7 @@ export class HouseplanCard extends LitElement {
|
||||
const overlays = this._isoOverlayScene(
|
||||
space, this._renderDevices.filter((device) => device.space === space.id && !device.hidden),
|
||||
baseView, disp, runtime.resolveIsoDecorationLayers(disp), scene, iconPct, effectiveDeviceBaseSize(iconPct),
|
||||
disp.showLqi ?? this._config?.show_signal ?? true);
|
||||
disp.showLqi ?? this._config?.show_signal ?? true, false);
|
||||
const envelope = overlays && runtime.resolveIsoOverlayFitEnvelope({
|
||||
baseBounds: scene.frame, entries: overlays.entries, stageSize, targetView: (bounds) =>
|
||||
fitView([bounds.x, bounds.y, bounds.w, bounds.h], aspect) });
|
||||
@@ -6179,12 +6179,14 @@ export class HouseplanCard extends LitElement {
|
||||
iconPct: number,
|
||||
deviceBasePct: number,
|
||||
showLqi: boolean,
|
||||
resolveCollisions = true,
|
||||
): IsoOverlayRenderScene | null {
|
||||
const runtime = this._isoSceneRuntime;
|
||||
if (!runtime || !layers?.structural || !structural) return null;
|
||||
return runtime.buildIsoOverlayRenderScene({
|
||||
space, devices: devs, openings: this._openingsR, view, display: disp, layers,
|
||||
wallSilhouettes: structural.wallSilhouettes,
|
||||
resolveCollisions,
|
||||
iconPct, deviceBasePct, showLqi, cellCm: this._cellCm,
|
||||
kioskIconScale: this._kiosk ? this._kioskScale.icon : 1,
|
||||
kioskFontScale: this._kiosk ? this._kioskScale.font : 1,
|
||||
|
||||
+48
-6
@@ -43,11 +43,15 @@ export interface IsoOverlayOwnerInput {
|
||||
rooms: readonly IsoOverlayRoom[];
|
||||
/** Device binding, room label owner, or the room selected by opening-host geometry. */
|
||||
preferredRoomId?: string | null;
|
||||
/** Internal fast path for room rows already normalised by isoOverlayRooms(). */
|
||||
roomsValidated?: boolean;
|
||||
}
|
||||
|
||||
export interface IsoOverlayPlacementInput extends IsoOverlayOwnerInput {
|
||||
showBorders: boolean;
|
||||
wallSilhouettes: readonly IsoWallSilhouette[];
|
||||
/** Internal fast path for silhouettes produced by the cached structural scene. */
|
||||
wallGeometryValidated?: boolean;
|
||||
/** Half-size of the floor-parallel plate in plan units. */
|
||||
plateHalfSize: PlanPoint;
|
||||
wallHeight?: number;
|
||||
@@ -225,7 +229,7 @@ const stableIdCompare = (a: string, b: string): number => a < b ? -1 : a > b ? 1
|
||||
|
||||
export function resolveIsoOverlayOwner(input: IsoOverlayOwnerInput): IsoOverlayOwner | null {
|
||||
if (!finitePoint(input.floorAnchor)) return null;
|
||||
const rooms = input.rooms.filter(validRoom);
|
||||
const rooms = input.roomsValidated ? input.rooms : input.rooms.filter(validRoom);
|
||||
const preferred = input.preferredRoomId
|
||||
? rooms.find((room) => room.id === input.preferredRoomId) || null
|
||||
: null;
|
||||
@@ -241,7 +245,12 @@ export function resolveIsoOverlayOwner(input: IsoOverlayOwnerInput): IsoOverlayO
|
||||
// a saved label may legitimately lie outside that room.
|
||||
room = preferred;
|
||||
}
|
||||
return room ? { id: room.id, area: roomArea(room), safePoint: isoRoomSafePoint(room) } : null;
|
||||
return room ? {
|
||||
id: room.id,
|
||||
area: roomArea(room),
|
||||
safePoint: input.roomsValidated && room.safePoint
|
||||
? [room.safePoint[0], room.safePoint[1]] : isoRoomSafePoint(room),
|
||||
} : null;
|
||||
}
|
||||
|
||||
export function isoOverlayPlane(kind: IsoOverlayKind, showBorders: boolean): IsoOverlayPlane {
|
||||
@@ -300,9 +309,39 @@ function pointInSilhouette(point: ScenePoint, silhouette: IsoWallSilhouette): bo
|
||||
return !(silhouette.holes || []).some((hole) => pointInRing(point, hole));
|
||||
}
|
||||
|
||||
type Bounds = readonly [minX: number, minY: number, maxX: number, maxY: number];
|
||||
const silhouetteBoundsCache = new WeakMap<IsoWallSilhouette, Bounds | null>();
|
||||
|
||||
function ringBounds(ring: readonly (readonly number[])[]): Bounds | null {
|
||||
if (!ring.length) return null;
|
||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||
for (const point of ring) {
|
||||
if (!finitePoint(point)) return null;
|
||||
minX = Math.min(minX, point[0]); minY = Math.min(minY, point[1]);
|
||||
maxX = Math.max(maxX, point[0]); maxY = Math.max(maxY, point[1]);
|
||||
}
|
||||
return [minX, minY, maxX, maxY];
|
||||
}
|
||||
|
||||
function silhouetteBounds(silhouette: IsoWallSilhouette, cache: boolean): Bounds | null {
|
||||
if (!cache) return ringBounds(silhouette.outer);
|
||||
if (silhouetteBoundsCache.has(silhouette)) return silhouetteBoundsCache.get(silhouette) ?? null;
|
||||
const bounds = ringBounds(silhouette.outer);
|
||||
silhouetteBoundsCache.set(silhouette, bounds);
|
||||
return bounds;
|
||||
}
|
||||
|
||||
function boundsNear(a: Bounds, b: Bounds, gap: number): boolean {
|
||||
return a[0] <= b[2] + gap && a[2] >= b[0] - gap
|
||||
&& a[1] <= b[3] + gap && a[3] >= b[1] - gap;
|
||||
}
|
||||
|
||||
function plateNearSilhouette(
|
||||
plate: readonly ScenePoint[], silhouette: IsoWallSilhouette, gap: number,
|
||||
plate: readonly ScenePoint[], plateBounds: Bounds,
|
||||
silhouette: IsoWallSilhouette, gap: number, cacheBounds: boolean,
|
||||
): boolean {
|
||||
const wallBounds = silhouetteBounds(silhouette, cacheBounds);
|
||||
if (!wallBounds || !boundsNear(plateBounds, wallBounds, gap)) return false;
|
||||
if (plate.some((point) => pointInSilhouette(point, silhouette))) return true;
|
||||
if (silhouette.outer.some((point) => pointInRing(point, plate))) return true;
|
||||
for (const wallRing of [silhouette.outer, ...(silhouette.holes || [])]) {
|
||||
@@ -380,12 +419,15 @@ export function resolveIsoOverlayPlacement(input: IsoOverlayPlacementInput): Iso
|
||||
const raisedHeight = wallHeight + visualOffset;
|
||||
const raisedScene = projectPlanPoint(floorAnchor, raisedHeight, camera);
|
||||
const owner = resolveIsoOverlayOwner(input);
|
||||
const geometryValid = input.wallSilhouettes.every(validSilhouette);
|
||||
const geometryValid = input.wallGeometryValidated ?? input.wallSilhouettes.every(validSilhouette);
|
||||
const gapUnits = safetyGap * unitsPerPixel;
|
||||
const basePlate = buildIsoPlatePolygon(floorAnchor, input.plateHalfSize,
|
||||
raisedHeight, camera);
|
||||
const isNear = (plate: readonly ScenePoint[]): boolean =>
|
||||
input.wallSilhouettes.some((wall) => plateNearSilhouette(plate, wall, gapUnits));
|
||||
const isNear = (plate: readonly ScenePoint[]): boolean => {
|
||||
const bounds = ringBounds(plate);
|
||||
return !!bounds && input.wallSilhouettes.some((wall) =>
|
||||
plateNearSilhouette(plate, bounds, wall, gapUnits, input.wallGeometryValidated === true));
|
||||
};
|
||||
const nearWallBefore = geometryValid ? isNear(basePlate) : true;
|
||||
|
||||
let distanceCss = 0;
|
||||
|
||||
+41
-17
@@ -685,6 +685,8 @@ export interface IsoOverlaySceneInput {
|
||||
display: SpaceDisplay;
|
||||
layers: IsoDecorationLayers;
|
||||
wallSilhouettes: readonly IsoWallSilhouette[];
|
||||
/** Fit-envelope probes need unnudged bounds only, not wall collision search. */
|
||||
resolveCollisions?: boolean;
|
||||
iconPct: number;
|
||||
deviceBasePct: number;
|
||||
showLqi: boolean;
|
||||
@@ -705,6 +707,11 @@ export interface IsoOverlaySceneInput {
|
||||
|
||||
type IsoOverlayRoomRow = { room: RoomCfg; overlayRoom: IsoOverlayRoom };
|
||||
const isoOverlayRoomCache = new WeakMap<readonly RoomCfg[], readonly IsoOverlayRoomRow[]>();
|
||||
type IsoOverlayPlacementCacheEntry = { signature: string; placement: IsoOverlayPlacement };
|
||||
export const ISO_OVERLAY_PLACEMENT_CACHE_LIMIT = 2048;
|
||||
const isoOverlayPlacementCache = new WeakMap<
|
||||
readonly IsoWallSilhouette[], Map<string, IsoOverlayPlacementCacheEntry>
|
||||
>();
|
||||
|
||||
/** Build legal island-room holes once per immutable room snapshot. */
|
||||
export function isoOverlayRooms(space: SpaceModel): readonly IsoOverlayRoomRow[] {
|
||||
@@ -740,6 +747,11 @@ export function buildIsoOverlayRenderScene(input: IsoOverlaySceneInput): IsoOver
|
||||
const roomPlacements = new Map<RoomCfg, IsoOverlayPlacement>();
|
||||
const locks = new Map<string, IsoOverlayPlacement>();
|
||||
const entries: IsoOverlayRenderEntry[] = [];
|
||||
let placements = isoOverlayPlacementCache.get(input.wallSilhouettes);
|
||||
if (!placements) {
|
||||
placements = new Map();
|
||||
isoOverlayPlacementCache.set(input.wallSilhouettes, placements);
|
||||
}
|
||||
const baseIconUnits = input.iconPct * iconUnit(input.space) * input.kioskIconScale / 100;
|
||||
const baseDeviceUnits = input.deviceBasePct * iconUnit(input.space) * input.kioskIconScale / 100;
|
||||
const place = (
|
||||
@@ -749,23 +761,35 @@ export function buildIsoOverlayRenderScene(input: IsoOverlaySceneInput): IsoOver
|
||||
plateHalfSize: PlanPoint,
|
||||
preferredRoomId?: string | null,
|
||||
selected = false,
|
||||
): IsoOverlayPlacement => resolveIsoOverlayPlacement({
|
||||
kind,
|
||||
floorAnchor,
|
||||
rooms,
|
||||
preferredRoomId,
|
||||
showBorders: true,
|
||||
wallSilhouettes: input.wallSilhouettes,
|
||||
plateHalfSize,
|
||||
wallHeight,
|
||||
visualOffset,
|
||||
sceneUnitsPerCssPixel: unitsPerPixel,
|
||||
filtersSupported: input.layers.shadows,
|
||||
// A persistent tether keeps ownership explicit without a second hover-only
|
||||
// render pipeline for the inert raised geometry.
|
||||
hovered: true,
|
||||
selected,
|
||||
});
|
||||
): 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,
|
||||
input.layers.shadows ? 1 : 0, selected ? 1 : 0].join('|');
|
||||
const cached = lruRead(placements!, cacheKey);
|
||||
if (cached.hit && cached.value.signature === signature) return cached.value.placement;
|
||||
const placement = resolveIsoOverlayPlacement({
|
||||
kind,
|
||||
floorAnchor,
|
||||
rooms, roomsValidated: true,
|
||||
preferredRoomId,
|
||||
showBorders: true,
|
||||
wallSilhouettes: input.resolveCollisions === false ? [] : input.wallSilhouettes,
|
||||
wallGeometryValidated: true,
|
||||
plateHalfSize,
|
||||
wallHeight,
|
||||
visualOffset,
|
||||
sceneUnitsPerCssPixel: unitsPerPixel,
|
||||
filtersSupported: input.layers.shadows,
|
||||
// A persistent tether keeps ownership explicit without a second hover-only
|
||||
// render pipeline for the inert raised geometry.
|
||||
hovered: true,
|
||||
selected,
|
||||
});
|
||||
lruWrite(placements!, cacheKey, { signature, placement }, ISO_OVERLAY_PLACEMENT_CACHE_LIMIT);
|
||||
return placement;
|
||||
};
|
||||
|
||||
for (const device of input.devices) {
|
||||
const pos = input.positionOf(device);
|
||||
|
||||
Reference in New Issue
Block a user