mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-06 14:39:22 +00:00
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
This commit is contained in:
+18
-3
@@ -8,9 +8,12 @@ export type CleanFloorResult = {
|
||||
floor: number[][];
|
||||
geom: Geom | null;
|
||||
path: string;
|
||||
area: number;
|
||||
/** 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[][];
|
||||
@@ -19,6 +22,8 @@ export function cleanFloorForRoom(input: {
|
||||
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 {
|
||||
@@ -40,11 +45,21 @@ export function cleanFloorForRoom(input: {
|
||||
&& Math.max(...by) >= box[1] && Math.min(...by) <= box[3];
|
||||
});
|
||||
const geom = candidates.length ? floorMinusBodies(floor, candidates) : null;
|
||||
const result = {
|
||||
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) : '',
|
||||
area: geometryAreaMinusStairs(geom || [[[...floor, floor[0]]]], space.stairs),
|
||||
// #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;
|
||||
|
||||
+43
-1
@@ -234,7 +234,7 @@ export function geometryAreaMinusStairs(
|
||||
export function geometryMinusStairs(
|
||||
source: Geom, stairs: readonly Stair[] | null | undefined, scale = NORM_W,
|
||||
): Geom {
|
||||
const footprints = stairList(stairs).map((stair) => stairFootprintGeometry(stair, scale));
|
||||
const footprints = stairFootprintsTouching(source, stairs, scale);
|
||||
if (!footprints.length) return source;
|
||||
try {
|
||||
// A single sweep avoids repeatedly rebuilding the same subject for dense floors.
|
||||
@@ -249,6 +249,48 @@ export function geometryMinusStairs(
|
||||
}
|
||||
}
|
||||
|
||||
type Bounds = readonly [number, number, number, number];
|
||||
|
||||
function geometryBounds(geometry: Geom): Bounds | null {
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
const visit = (value: unknown): void => {
|
||||
if (!Array.isArray(value)) return;
|
||||
if (typeof value[0] === 'number') {
|
||||
const x = value[0];
|
||||
const y = value[1] as number;
|
||||
if (x < minX) minX = x;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (y > maxY) maxY = y;
|
||||
return;
|
||||
}
|
||||
for (const item of value) visit(item);
|
||||
};
|
||||
visit(geometry);
|
||||
return minX <= maxX && minY <= maxY ? [minX, minY, maxX, maxY] : null;
|
||||
}
|
||||
|
||||
const boundsOverlap = (a: Bounds, b: Bounds | null): boolean => !!b
|
||||
&& a[0] <= b[2] && b[0] <= a[2] && a[1] <= b[3] && b[1] <= a[3];
|
||||
|
||||
/**
|
||||
* #669: only a footprint whose bounds overlap the subject can change the
|
||||
* difference. A room on a maximum-size floor otherwise sends all 250 stair
|
||||
* footprints into one polyclip sweep, and the room count multiplies it.
|
||||
*/
|
||||
export function stairFootprintsTouching(
|
||||
source: Geom, stairs: readonly Stair[] | null | undefined, scale = NORM_W,
|
||||
): Geom[] {
|
||||
const bounds = geometryBounds(source);
|
||||
if (!bounds) return [];
|
||||
return stairList(stairs)
|
||||
.map((stair) => stairFootprintGeometry(stair, scale))
|
||||
.filter((footprint) => boundsOverlap(bounds, geometryBounds(footprint)));
|
||||
}
|
||||
|
||||
/**
|
||||
* The summary panel calculates floor area after the first paint. A maximum
|
||||
* size space may contain 250 stairs; clipping all their footprints in one
|
||||
|
||||
Reference in New Issue
Block a user