Files
houseplan-card/src/clean-floor.ts
T
Claude 47c245b5ba perf: вычитать лестницы из площади пола только по запросу (#669)
Путь чистого пола строят четыре места рендера, а площадь читают только
подсказка комнаты и PDF. cleanFloorForRoom больше не вычитает лестницы при
построении пути: площадь считается при первом чтении и хранится в том же
закэшированном объекте. geometryMinusStairs передаёт в polyclip только
контуры лестниц, чей габарит пересекает вычитаемую геометрию, — результат
тот же, а комната на этаже с 250 лестницами больше не прогоняет все 250.

Issue: #669
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 09:48:02 +03:00

67 lines
2.5 KiB
TypeScript

import { floorMinusBodies, geometryArea, polyclipPathD } from './physical-geometry';
import { geometryAreaMinusStairs } from './stairs';
import type { RoomCfg, SpaceModel } from './types';
import { lruRead, lruWrite } from './card-runtime';
import type { Geom } from 'polyclip-ts';
export type CleanFloorResult = {
floor: number[][];
geom: Geom | null;
path: string;
/** Clean-floor area minus stairs, computed on the first read (#669). */
readonly area: number;
};
type StairAreaFn = (source: Geom, stairs: SpaceModel['stairs'] | undefined) => number;
export function cleanFloorForRoom(input: {
room: RoomCfg;
floor: number[][];
space?: SpaceModel;
configEpoch: number;
resizePreview: boolean;
cache: Map<string, CleanFloorResult>;
physicalBodies(space: SpaceModel): number[][][];
/** Test seam (#669): the stair subtraction behind `area`. */
areaMinusStairs?: StairAreaFn;
}): CleanFloorResult {
const { room, floor, space } = input;
if (!space) return {
floor, geom: null, path: '', area: geometryArea([[[...floor, floor[0]]]]),
};
const roomKey = room.id || `#${space.rooms.indexOf(room)}`;
const key = `${space.id}|${input.configEpoch}|${roomKey}`;
if (!input.resizePreview) {
const cached = lruRead(input.cache, key);
if (cached.hit) return cached.value;
}
const xs = floor.map((point) => point[0]);
const ys = floor.map((point) => point[1]);
const box = [Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)];
const candidates = input.physicalBodies(space).filter((body) => {
const bx = body.map((point) => point[0]);
const by = body.map((point) => point[1]);
return Math.max(...bx) >= box[0] && Math.min(...bx) <= box[2]
&& Math.max(...by) >= box[1] && Math.min(...by) <= box[3];
});
const geom = candidates.length ? floorMinusBodies(floor, candidates) : null;
const subject = (geom || [[[...floor, floor[0]]]]) as Geom;
const stairs = space.stairs;
const areaMinusStairs: StairAreaFn = input.areaMinusStairs ?? geometryAreaMinusStairs;
let area: number | undefined;
const result: CleanFloorResult = {
floor,
geom,
path: geom ? polyclipPathD(geom) : '',
// #669: four render paths read only `path`; the room tooltip and the PDF
// read the area. Subtracting up to 250 stair footprints per room belongs
// to that first read, not to every render of the floor.
get area() {
if (area === undefined) area = areaMinusStairs(subject, stairs);
return area;
},
};
if (!input.resizePreview) lruWrite(input.cache, key, result, 600);
return result;
}