mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
perf: разбить вычитание лестниц по кадрам (#663)
Issue: #663 User-Visible: no
This commit is contained in:
@@ -96,6 +96,18 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: ' return source; // mutant: footprints no longer affect clean area\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'stairs-summary-area-clips-all-footprints-in-one-task',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test test/stairs.test.mjs',
|
||||
because: '#663 AC13: the maximum 250 stair footprints must be clipped in bounded slices; '
|
||||
+ 'one polyclip sweep blocks the summary panel main thread on slower clients.',
|
||||
patches: [{
|
||||
file: 'src/stairs.ts',
|
||||
find: ' batchSize = 24,\n',
|
||||
replace: ' batchSize = MAX_STAIRS_PER_SPACE, // mutant: one long main-thread clip\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'stairs-view-pan-opens-target-floor',
|
||||
guard: 'node demo/smoke_stairs.mjs',
|
||||
|
||||
@@ -247,3 +247,33 @@ export function geometryMinusStairs(
|
||||
return geometry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The summary panel calculates floor area after the first paint. A maximum
|
||||
* size space may contain 250 stairs; clipping all their footprints in one
|
||||
* polyclip call is observably one long main-thread task on slower clients.
|
||||
* Keep the synchronous helper above for ordinary room-sized callers, while
|
||||
* this iterator bounds each background slice and lets the scheduler yield.
|
||||
*/
|
||||
export function* geometryMinusStairsSteps(
|
||||
source: Geom,
|
||||
stairs: readonly Stair[] | null | undefined,
|
||||
scale = NORM_W,
|
||||
batchSize = 24,
|
||||
): Generator<void, Geom, void> {
|
||||
const footprints = stairList(stairs).map((stair) => stairFootprintGeometry(stair, scale));
|
||||
const size = Math.max(1, Math.floor(batchSize));
|
||||
let geometry = source;
|
||||
for (let index = 0; index < footprints.length; index += size) {
|
||||
const batch = footprints.slice(index, index + size);
|
||||
try {
|
||||
geometry = difference(geometry, ...batch);
|
||||
} catch {
|
||||
for (const footprint of batch) {
|
||||
try { geometry = difference(geometry, footprint); } catch { /* skip only the bad record */ }
|
||||
}
|
||||
}
|
||||
yield;
|
||||
}
|
||||
return geometry;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { innerContourForRoom, multiWallNodesForGeometry, wallBodiesGeometry } fr
|
||||
import type { Marker, ServerConfig, SpaceModel, SummaryPanelSource } from './types';
|
||||
import type { HaRegistrySnapshot } from './ha-binding-status';
|
||||
import type { SummaryHass } from './summary-panel-host';
|
||||
import { geometryMinusStairs } from './stairs';
|
||||
import { geometryMinusStairsSteps } from './stairs';
|
||||
|
||||
/** Count the unique real HA devices represented anywhere on the plan. */
|
||||
export function representedHaDeviceIds(input: {
|
||||
@@ -119,9 +119,16 @@ export function* cleanFloorAreaSteps(
|
||||
spaceFloor = unionGeometry(spaceFloor, clean);
|
||||
yield;
|
||||
}
|
||||
// Difference distributes over union. Subtracting the shared stair set once
|
||||
// avoids rooms x stairs polygon clips on dense plans (#663).
|
||||
spaceFloor = geometryMinusStairs(spaceFloor || [], space.stairs);
|
||||
// Difference distributes over union. Subtract the shared stair set once,
|
||||
// but in bounded batches: 250 footprints in one polyclip sweep becomes a
|
||||
// visible long task on slower clients (#663).
|
||||
const stairSteps = geometryMinusStairsSteps(spaceFloor || [], space.stairs);
|
||||
let stairStep = stairSteps.next();
|
||||
while (!stairStep.done) {
|
||||
yield;
|
||||
stairStep = stairSteps.next();
|
||||
}
|
||||
spaceFloor = stairStep.value;
|
||||
const cmPerUnit = prepared.cellCm / GRID_PITCH;
|
||||
total += geometryArea(spaceFloor) * cmPerUnit * cmPerUnit / 1e4;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
cachedStairRenderGeometry,
|
||||
floorAreaMinusStairs,
|
||||
geometryAreaMinusStairs,
|
||||
geometryMinusStairsSteps,
|
||||
isStair,
|
||||
stairRenderGeometry,
|
||||
} from '../test-build/stairs.js';
|
||||
@@ -128,6 +129,31 @@ test('#663 area removes only stair overlap and never becomes negative', () => {
|
||||
assert.equal(geometryAreaMinusStairs(source, [straight({ length: 2, width: 2 })]), 0);
|
||||
});
|
||||
|
||||
test('#663 dense stair subtraction yields between bounded polygon batches', () => {
|
||||
const source = [[[[0, 0], [1000, 0], [1000, 1000], [0, 1000], [0, 0]]]];
|
||||
const stairs = Array.from({ length: 5 }, (_, index) => straight({
|
||||
id: `stair-${index}`, x: 0.15 + index * 0.16, length: 0.1, width: 0.1,
|
||||
}));
|
||||
const steps = geometryMinusStairsSteps(source, stairs, 1000, 2);
|
||||
assert.equal(steps.next().done, false);
|
||||
assert.equal(steps.next().done, false);
|
||||
assert.equal(steps.next().done, false);
|
||||
const finished = steps.next();
|
||||
assert.equal(finished.done, true);
|
||||
assert.equal(geometryAreaMinusStairs(finished.value, []), 950_000);
|
||||
|
||||
const denseSteps = geometryMinusStairsSteps(source, Array.from({ length: 49 }, (_, index) => (
|
||||
straight({ id: `dense-${index}` })
|
||||
)));
|
||||
let slices = 0;
|
||||
let denseStep = denseSteps.next();
|
||||
while (!denseStep.done) {
|
||||
slices += 1;
|
||||
denseStep = denseSteps.next();
|
||||
}
|
||||
assert.equal(slices, 3, 'the default keeps a 49-stair calculation out of one main-thread task');
|
||||
});
|
||||
|
||||
test('#663 target states distinguish active, missing, self, deleted and fixed', () => {
|
||||
const spaces = new Set(['ground', 'upper']);
|
||||
assert.equal(stairTargetState(straight(), 'ground', spaces, false), 'active');
|
||||
|
||||
Reference in New Issue
Block a user