perf: reuse wall union across projection toggles

Issue: #89
User-Visible: no
This commit is contained in:
Sergey Matyunin
2026-08-13 17:56:46 +03:00
parent 3270e039d8
commit 2576d9d2fd
6 changed files with 577 additions and 531 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+174 -174
View File
File diff suppressed because one or more lines are too long
+24 -6
View File
@@ -55,7 +55,8 @@ import {
import {
degradeWalls, rekeyWallsAfterMove,
setWallThickness, setWallThicknessForRoom, cmToField, wallCmToUnits,
wallEdgeBodies, wallBodiesGeometry, wallBodiesUnionPath, paperRoomShapesWithWalls,
wallEdgeBodies, wallBodiesGeometry, wallBodiesPathFromGeometry, wallBodiesUnionPath,
paperRoomShapesWithWalls,
innerContourForRoom, roomWallProfile, outsetContour,
openingInnerFaceOffsetFromIndex, openingTunnelGeometriesFromIndex,
openingWallIndex as buildOpeningWallIndex, applyWallThicknessToNewRoom,
@@ -4418,7 +4419,15 @@ class HouseplanCard extends LitElement {
}
/** The rectangle "fit to screen" fits — always the content (docs/CANVAS.md). */
private _isoSource(): { key: string; build: () => any } {
private _wallUnionKey(): string {
return `${this._space}|${this._cfgEpoch}|${this._spaceModel().rooms.length}`;
}
private _isoSource(): {
key: string;
wallKey: string;
build: () => { geom: any; depthUnits: number };
} {
const space = this._spaceModel();
const walls = this._spaceWalls;
const openCuts = this._openPairs().flatMap((pair) => pair.segs);
@@ -4436,6 +4445,7 @@ class HouseplanCard extends LitElement {
})}`;
return {
key,
wallKey: this._wallUnionKey(),
build: () => {
const extras = physicalBodies(space, this._cellCm, this._gridPitch);
const united = walls.length || extras.length
@@ -4443,9 +4453,9 @@ class HouseplanCard extends LitElement {
space.rooms, walls, openCuts, openings,
this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W, extras,
)
: { geom: [] };
: { geom: [], depthUnits: 0 };
if (!united) throw new Error('wall boolean geometry failed');
return united.geom;
return united;
},
};
}
@@ -4461,7 +4471,15 @@ class HouseplanCard extends LitElement {
if (cached) return { key: source.key, ...cached };
const flat = this._frameOf().rect;
const frame = projectedFrame({ rect: flat, wallHeight: ISO_WALL_HEIGHT });
const geometry = buildIsoWallGeometry(source.build());
const united = source.build();
const geometry = buildIsoWallGeometry(united.geom);
// Flat and iso are two projections of the same canonical union. Seed the
// existing one-entry flat cache while that union is already in hand so a
// Flat -> Volumetric toggle does not repeat the polygon boolean pass.
this._wallUnionCache = {
key: source.wallKey,
value: wallBodiesPathFromGeometry(united),
};
const value = { geometry, frame };
lruWrite(this._isoGeometryCache, source.key, value, 8);
return { key: source.key, ...value };
@@ -10018,7 +10036,7 @@ class HouseplanCard extends LitElement {
angle: Number(o.angle) || 0,
length: (Number(o.length) > 0 ? Number(o.length) : 0.9) * NORM_W,
}));
const unionKey = `${this._space}|${this._cfgEpoch}|${this._spaceModel().rooms.length}`;
const unionKey = this._wallUnionKey();
if (!this._wallUnionCache || this._wallUnionCache.key !== unionKey) {
this._wallUnionCache = {
key: unionKey,
+16 -2
View File
@@ -1225,6 +1225,20 @@ function polyclipToPathD(geom: any): string {
return d;
}
/**
* Project an already computed canonical wall union into the flat SVG path.
* Isometric rendering and the ordinary flat wall layer share the same boolean
* result; keeping this conversion separate prevents a projection toggle from
* repeating the expensive polygon union merely to obtain its path string.
*/
export function wallBodiesPathFromGeometry(
united: { geom: any; depthUnits: number } | null,
): { d: string; depthUnits: number; fillRule: 'evenodd' } | null {
if (!united) return null;
const d = polyclipToPathD(united.geom);
return d ? { d, depthUnits: united.depthUnits, fillRule: 'evenodd' } : null;
}
/**
* Mitre patches at an endpoint where a virtual stretch meets real walls that
* belong to different room contours.
@@ -1462,8 +1476,8 @@ export function wallBodiesUnionPath(
const united = wallBodiesGeometry(
rooms, walls, openCuts, openings, pitch, cellCm, gridPitch, coordScale, extraBodies,
);
const d = united ? polyclipToPathD(united.geom) : '';
if (united && d) return { d, depthUnits: united.depthUnits, fillRule: 'evenodd' };
const projected = wallBodiesPathFromGeometry(united);
if (projected) return projected;
if (united) return null; // successful empty result: do not resurrect raw rings
// fall back to evenodd rings concatenated
const rings = wallBodyRings(rooms, walls, openCuts, pitch, cellCm, gridPitch, coordScale);
+15 -1
View File
@@ -6,7 +6,8 @@ import {
setWallThickness, setWallThicknessForRoom, applyWallThicknessToNewRoom,
drawWallPreviewD, DRAW_WALL_DEFAULT_CM, clampWallCm, cmToField, fieldToCm,
wallCmToUnits, insetContour, inwardNormal, edgeKinds, wallEdgeBodies,
wallBodyRings, wallBodiesUnionPath, innerContourForRoom,
wallBodyRings, wallBodiesGeometry, wallBodiesPathFromGeometry, wallBodiesUnionPath,
innerContourForRoom,
paperRoomShapesWithWalls, WALL_MIN_CM, WALL_MAX_CM, MITRE_LIMIT,
atomicPolyForRoom, insetOffsetsForRoom, wallIntervals, materializeWallIntervals,
normalizeWallIntervals,
@@ -669,6 +670,19 @@ test('wallBodiesUnionPath: single fully-thick room keeps a floor hole', () => {
assert.ok((united.d.match(/M/g) || []).length >= 2, united.d);
});
test('wallBodiesPathFromGeometry reuses the canonical union without changing its flat path', () => {
const room = { id: 'shared', poly: [[100, 100], [300, 100], [300, 300], [100, 300]] };
const walls = applyWallThicknessToNewRoom([], [room], 'shared', 15, 0.01, [], 1000);
const geometry = wallBodiesGeometry(
[room], walls, [], [], 0.01, cellCm, GRID_PITCH, 1000,
);
const reused = wallBodiesPathFromGeometry(geometry);
const direct = wallBodiesUnionPath(
[room], walls, [], [], 0.01, cellCm, GRID_PITCH, 1000,
);
assert.deepEqual(reused, direct);
});
test('wallBodiesUnionPath: a parent floor never erases a nested room wall', () => {
const scale = 1000;
const rooms = [