perf: reuse structural room contours

Issue: #322
User-Visible: no
This commit is contained in:
Matysh
2026-08-27 12:58:02 +03:00
parent 0c3aacdd34
commit 298030d47e
4 changed files with 141 additions and 101 deletions
File diff suppressed because one or more lines are too long
+34 -34
View File
File diff suppressed because one or more lines are too long
+30 -33
View File
@@ -1617,6 +1617,7 @@ class HouseplanCard extends LitElement {
private _cleanFloorCache = new Map<string, { private _cleanFloorCache = new Map<string, {
floor: number[][]; geom: any; path: string; area: number; floor: number[][]; geom: any; path: string; area: number;
}>(); }>();
private _innerContourCache = new Map<string, number[][] | null>();
private _glowClipCache = new Map<string, GlowClipGeometry | null>(); private _glowClipCache = new Map<string, GlowClipGeometry | null>();
/** Redacted, bounded dedupe for numerical floor fallbacks (#218). */ /** Redacted, bounded dedupe for numerical floor fallbacks (#218). */
private _glowGeometryWarnings = new Set<string>(); private _glowGeometryWarnings = new Set<string>();
@@ -11590,6 +11591,26 @@ class HouseplanCard extends LitElement {
).map((b) => [b.a[0], b.a[1], b.b[0], b.b[1]]); ).map((b) => [b.a[0], b.a[1], b.b[0], b.b[1]]);
} }
/** Structural room face shared by every fill surface in the same frame. */
private _innerRoomContour(
space: SpaceModel,
roomId: string,
openCuts: number[][] = this._openCuts(),
roomWalls = this._wallUnionGeometry()?.roomGeom,
): number[][] | null {
const cutsKey = openCuts.map((cut) => cut.join(',')).join(';');
const key = `${space.id}|${this._cfgEpoch}|${roomId}|${cutsKey}`;
const cached = lruRead(this._innerContourCache, key);
if (cached.hit) return cached.value;
const value = innerContourForRoom(
space.rooms, roomId, this._spaceWalls, openCuts,
this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W,
roomWalls,
);
lruWrite(this._innerContourCache, key, value, 600);
return value;
}
/** /**
* The ATOMIC wall stretch under the cursor for the wall-thickness tool. * The ATOMIC wall stretch under the cursor for the wall-thickness tool.
* *
@@ -12024,11 +12045,7 @@ class HouseplanCard extends LitElement {
const ownPoly = polys.get(room) || null; const ownPoly = polys.get(room) || null;
if (!fill || !ownPoly) return nothing; if (!fill || !ownPoly) return nothing;
const floor = this._spaceWalls.length && room.id const floor = this._spaceWalls.length && room.id
? (innerContourForRoom( ? (this._innerRoomContour(space, room.id, openCuts, roomWalls) || ownPoly)
space.rooms, room.id, this._spaceWalls, openCuts,
this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W,
roomWalls,
) || ownPoly)
: ownPoly; : ownPoly;
const otherPolys = space.rooms const otherPolys = space.rooms
.filter((other) => other !== room) .filter((other) => other !== room)
@@ -12229,11 +12246,7 @@ class HouseplanCard extends LitElement {
const walls = this._spaceWalls; const walls = this._spaceWalls;
const roomWalls = this._wallUnionGeometry()?.roomGeom; const roomWalls = this._wallUnionGeometry()?.roomGeom;
const floor = walls.length && room.id const floor = walls.length && room.id
? (innerContourForRoom( ? (this._innerRoomContour(space, room.id, allOpenCuts, roomWalls) || poly)
space.rooms, room.id, walls, allOpenCuts,
this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W,
roomWalls,
) || poly)
: poly; : poly;
const contours: { axis: number[][]; face: number[][] }[] = [{ axis: poly, face: floor }]; const contours: { axis: number[][]; face: number[][] }[] = [{ axis: poly, face: floor }];
for (const island of islandPolys) { for (const island of islandPolys) {
@@ -15301,11 +15314,7 @@ class HouseplanCard extends LitElement {
const roomWalls = this._wallUnionGeometry()?.roomGeom; const roomWalls = this._wallUnionGeometry()?.roomGeom;
if (walls.length) { if (walls.length) {
for (const r of rooms) { for (const r of rooms) {
const inn = innerContourForRoom( const inn = this._innerRoomContour(space, r.id, openCuts, roomWalls);
space.rooms, r.id, walls, openCuts,
this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W,
roomWalls,
);
if (inn) innerByRoom[r.id] = inn; if (inn) innerByRoom[r.id] = inn;
} }
for (const o of windows) { for (const o of windows) {
@@ -16691,11 +16700,7 @@ class HouseplanCard extends LitElement {
const roomWalls = this._wallUnionGeometry()?.roomGeom; const roomWalls = this._wallUnionGeometry()?.roomGeom;
const enabledClip = enabled.length === polys.length ? null : enabled.map(({ r, poly }) => { const enabledClip = enabled.length === polys.length ? null : enabled.map(({ r, poly }) => {
const floorPoly = walls.length && r.id const floorPoly = walls.length && r.id
? (innerContourForRoom( ? (this._innerRoomContour(space, r.id, openCuts, roomWalls) || poly)
space.rooms, r.id, walls, openCuts,
this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W,
roomWalls,
) || poly)
: poly; : poly;
const clean = this._cleanFloor(r, floorPoly, space).path; const clean = this._cleanFloor(r, floorPoly, space).path;
const holes = islandsOf( const holes = islandsOf(
@@ -17638,6 +17643,8 @@ class HouseplanCard extends LitElement {
// audit L1: hoisted out of the per-room map — these depend on the // audit L1: hoisted out of the per-room map — these depend on the
// config, not on entity state, and were recomputed per room. // config, not on entity state, and were recomputed per room.
const allZeroCuts = this._openCuts(); const allZeroCuts = this._openCuts();
const allThickCuts = this._thickWallCuts();
const roomWallGeometry = this._wallUnionGeometry()?.roomGeom;
const polyCache = new Map<any, number[][] | null>(); const polyCache = new Map<any, number[][] | null>();
const polyOf = (rr: any) => { const polyOf = (rr: any) => {
if (!polyCache.has(rr)) polyCache.set(rr, roomPoly(rr)); if (!polyCache.has(rr)) polyCache.set(rr, roomPoly(rr));
@@ -17694,18 +17701,13 @@ class HouseplanCard extends LitElement {
) <= this._gridPitch * 0.08); ) <= this._gridPitch * 0.08);
}) })
: []; : [];
const thickCuts = !isPicked ? this._thickWallCuts() : []; const thickCuts = !isPicked ? allThickCuts : [];
const edgeCuts = zeroCuts.concat(thickCuts); const edgeCuts = zeroCuts.concat(thickCuts);
if (edgeCuts.length) cls += ' noedge'; if (edgeCuts.length) cls += ' noedge';
// island rooms punch holes in their parent's fill (evenodd) // island rooms punch holes in their parent's fill (evenodd)
const walls = this._spaceWalls; const walls = this._spaceWalls;
const fillPoly = (walls.length && r.id && myPoly) const fillPoly = (walls.length && r.id && myPoly)
? (innerContourForRoom( ? (this._innerRoomContour(space, r.id, allZeroCuts, roomWallGeometry) || myPoly)
space.rooms, r.id, walls,
allZeroCuts,
this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W,
this._wallUnionGeometry()?.roomGeom,
) || myPoly)
: myPoly; : myPoly;
const holes = fillPoly ? islandsOf(fillPoly, otherPolys(r)) : []; const holes = fillPoly ? islandsOf(fillPoly, otherPolys(r)) : [];
const pathD = (pts: number[][]) => const pathD = (pts: number[][]) =>
@@ -18879,12 +18881,7 @@ class HouseplanCard extends LitElement {
if (!space) return null; if (!space) return null;
const walls = this._spaceWalls; const walls = this._spaceWalls;
const floor = walls.length && r.id const floor = walls.length && r.id
? (innerContourForRoom( ? (this._innerRoomContour(space, r.id) || poly)
space.rooms, r.id, walls,
this._openCuts(),
this._wallKeyPitch, this._cellCm, this._gridPitch, NORM_W,
this._wallUnionGeometry()?.roomGeom,
) || poly)
: poly; : poly;
const clean = this._cleanFloor(r, floor); const clean = this._cleanFloor(r, floor);
const cmPerUnit = this._cellCm / this._gridPitch; const cmPerUnit = this._cellCm / this._gridPitch;
+43
View File
@@ -64,3 +64,46 @@ test('contract accepts recent optional fields only when their runtime type is va
/invalid private API types: _cleanFloorCache:map/, /invalid private API types: _cleanFloorCache:map/,
); );
}); });
test('view render resolves structural wall cuts once per frame, not once per room', () => {
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const start = source.indexOf('const allZeroCuts = this._openCuts();');
const end = source.indexOf('${this._renderOpeningTunnelFills(space, roomFills)}', start);
assert.ok(start >= 0 && end > start, 'view room-render block is present');
const roomRender = source.slice(start, end);
assert.match(roomRender, /const allThickCuts = this\._thickWallCuts\(\);/);
assert.match(roomRender, /const roomWallGeometry = this\._wallUnionGeometry\(\)\?\.roomGeom;/);
assert.equal(
[...roomRender.matchAll(/this\._thickWallCuts\(\)/g)].length,
1,
'the structural resolver must stay outside the room map',
);
assert.match(roomRender, /const thickCuts = !isPicked \? allThickCuts : \[\];/);
assert.equal(
[...roomRender.matchAll(/this\._wallUnionGeometry\(\)/g)].length,
1,
'the wall union lookup must stay outside the room map',
);
});
test('room inner faces are structurally cached and shared by both fill layers', () => {
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
const helperStart = source.indexOf('private _innerRoomContour(');
const helperEnd = source.indexOf('\n /**', helperStart);
assert.ok(helperStart >= 0 && helperEnd > helperStart, 'inner-contour cache helper is present');
const helper = source.slice(helperStart, helperEnd);
assert.match(helper, /lruRead\(this\._innerContourCache, key\)/);
assert.match(helper, /lruWrite\(this\._innerContourCache, key, value, 600\)/);
const glowStart = source.indexOf('private _renderGlowBaseRooms(');
const glowEnd = source.indexOf('\n private _renderWallBodies(', glowStart);
const glowBase = source.slice(glowStart, glowEnd);
assert.equal([...glowBase.matchAll(/innerContourForRoom\(/g)].length, 0);
assert.match(glowBase, /this\._innerRoomContour\(space, room\.id, openCuts, roomWalls\)/);
const viewStart = source.indexOf('const allZeroCuts = this._openCuts();');
const viewEnd = source.indexOf('${this._renderOpeningTunnelFills(space, roomFills)}', viewStart);
const viewRooms = source.slice(viewStart, viewEnd);
assert.equal([...viewRooms.matchAll(/innerContourForRoom\(/g)].length, 0);
assert.match(viewRooms, /this\._innerRoomContour\(space, r\.id, allZeroCuts, roomWallGeometry\)/);
});