mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
test: свидетели на перф-дельту #160 — кэш размещений, зум внутрь, AABB
User-Visible: no Issue: #473
This commit is contained in:
@@ -6462,6 +6462,63 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: ' if (!this.diagnosticsCache) return;\n if (true) return;\n',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'iso-placement-cache-ignores-selected',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="#473 W1" test/iso-scene-render.test.mjs',
|
||||
because: 'a selected plate must resolve its own placement; a signature without `selected` '
|
||||
+ 'hands back the unselected cached one — a stale picture with no failure (#473, perf delta of #160)',
|
||||
patches: [{
|
||||
file: 'src/iso-scene-render.ts',
|
||||
find: " input.layers.shadows ? 1 : 0, selected ? 1 : 0].join('|');",
|
||||
replace: " input.layers.shadows ? 1 : 0].join('|');",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'iso-placement-cache-survives-silhouette-change',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="#473 W2" test/iso-scene-render.test.mjs',
|
||||
because: 'placements are cached per wall-silhouette array identity; keying the cache by a '
|
||||
+ 'constant serves «near the wall» for a plan whose wall is gone (#473)',
|
||||
patches: [{
|
||||
file: 'src/iso-scene-render.ts',
|
||||
find: ' let placements = isoOverlayPlacementCache.get(input.wallSilhouettes);',
|
||||
replace: ' let placements = isoOverlayPlacementCache.get(ISO_PLACEMENT_CACHE_ANY);',
|
||||
}, {
|
||||
file: 'src/iso-scene-render.ts',
|
||||
find: ' isoOverlayPlacementCache.set(input.wallSilhouettes, placements);',
|
||||
replace: ' isoOverlayPlacementCache.set(ISO_PLACEMENT_CACHE_ANY, placements);',
|
||||
}, {
|
||||
file: 'src/iso-scene-render.ts',
|
||||
find: 'export const ISO_OVERLAY_PLACEMENT_CACHE_LIMIT = 2048;',
|
||||
replace: 'export const ISO_OVERLAY_PLACEMENT_CACHE_LIMIT = 2048;\nconst ISO_PLACEMENT_CACHE_ANY: readonly IsoWallSilhouette[] = [];',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'iso-zoom-in-reuses-near-wall-plate',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="#473 W3" test/iso-scene-render.test.mjs',
|
||||
because: 'zoom-in may reuse a placement only if the plate was never near a wall or was '
|
||||
+ 'cleared within the cap; dropping the first guard reuses a pinned plate at a scale where it '
|
||||
+ 'already cuts the wall (#473)',
|
||||
patches: [{
|
||||
file: 'src/iso-scene-render.ts',
|
||||
find: ' if (!previous.nearWallBefore',
|
||||
replace: ' if (true',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'iso-aabb-rejects-touching-wall',
|
||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
||||
+ '&& node --test --test-name-pattern="#473 W4" test/iso-scene-render.test.mjs',
|
||||
because: 'the AABB pre-check must keep every silhouette within the safety gap; a strict '
|
||||
+ 'overlap test drops walls the exact test would have caught and lets plates sit flush (#473)',
|
||||
patches: [{
|
||||
file: 'src/iso-overlays.ts',
|
||||
find: ' return a[0] <= b[2] + gap && a[2] >= b[0] - gap',
|
||||
replace: ' return a[0] <= b[2] && a[2] >= b[0]',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'stage3-w1-camera-rotation-reset',
|
||||
guard: 'node --test --test-name-pattern="exact fixed" test/iso-projection.test.mjs',
|
||||
|
||||
@@ -707,3 +707,118 @@ test('opening geometry policy is both fingerprinted and consumed by the structur
|
||||
const basis = changed.build().openings[0];
|
||||
assert.equal(basis.leafThickness, basis.wallHeight * policy.leafThicknessRatio);
|
||||
});
|
||||
|
||||
// #473. Перф-дельта #160 (f1b9bbf3..b3ca0ca2) ввела кэш размещений, кэш по
|
||||
// идентичности силуэтов, повторное использование при зуме внутрь и
|
||||
// AABB-отсечение — и ушла в бету по §11.4 без ревью и без единого свидетеля.
|
||||
// Двенадцать мутантов stage3-w* защищают картинку, ни один — эти механизмы.
|
||||
// Ошибка любого из них даёт УСТАРЕВШЕЕ размещение без единого падения.
|
||||
|
||||
const perfFixture = () => {
|
||||
const owner = room('owner', 0, 0, 100, 100);
|
||||
const space = {
|
||||
id: 'floor', title: 'Floor', cellCm: 5, vb: [0, 0, 100, 100], bg: null,
|
||||
rooms: [owner], wall_segments: [], room_drafts: [], partitions: [], wall_columns: [],
|
||||
};
|
||||
const wallSilhouettes = [{ outer: buildIsoPlatePolygon([0, 50], [4, 60], ISO_WALL_HEIGHT) }];
|
||||
const input = {
|
||||
space,
|
||||
devices: [{ id: 'device', space: 'floor', marker: { room_id: 'owner' } }],
|
||||
openings: [],
|
||||
view: { x: 0, y: 0, w: 100, h: 100 },
|
||||
display: { showNames: false, cardFontScale: 1 },
|
||||
layers: { structural: true, shadows: true },
|
||||
wallSilhouettes,
|
||||
iconPct: 3.4, deviceBasePct: 3.4, showLqi: false, cellCm: 5,
|
||||
kioskIconScale: 1, kioskFontScale: 1,
|
||||
stageSize: { width: 100, height: 100 },
|
||||
positionOf: () => ({ x: 5, y: 50 }),
|
||||
presentationOf: () => ({
|
||||
scale: 1, valueText: null, valueFullText: '', valueBadge: null,
|
||||
tempText: null, humText: null, lqiText: null,
|
||||
pulse: { animated: false, diameterScale: 1 },
|
||||
}),
|
||||
labelPositionOf: () => ({ x: 0, y: 0 }),
|
||||
labelScaleOf: () => 1,
|
||||
openingEntityAvailable: () => false,
|
||||
openingWallIndex: () => ({ adjacencyEps: 0.1, edges: [] }),
|
||||
};
|
||||
return { input, wallSilhouettes };
|
||||
};
|
||||
|
||||
test('#473 W1: выделение входит в подпись кэша размещений', () => {
|
||||
const { input } = perfFixture();
|
||||
const plain = buildIsoOverlayRenderScene(input).devices.get('device');
|
||||
const selected = buildIsoOverlayRenderScene({ ...input, selectedDeviceId: 'device' })
|
||||
.devices.get('device');
|
||||
// Подпись без `selected` вернула бы тот же кэшированный объект — плита
|
||||
// выделенного устройства осталась бы без своего размещения.
|
||||
assert.notStrictEqual(selected, plain, 'выделение обязано дать своё размещение, а не кэш');
|
||||
// Кэш держит одну запись на (режим, вид, id): снятие выделения пересчитывает
|
||||
// заново, и это правильно — подпись изменилась. Тождества с `plain` здесь
|
||||
// быть не должно, проверяется только отсутствие подмены.
|
||||
const again = buildIsoOverlayRenderScene(input).devices.get('device');
|
||||
assert.notStrictEqual(again, selected, 'снятие выделения не отдаёт выделенное размещение');
|
||||
});
|
||||
|
||||
test('#473 W2: кэш размещений привязан к идентичности массива силуэтов', () => {
|
||||
const { input } = perfFixture();
|
||||
const withWall = buildIsoOverlayRenderScene(input).devices.get('device');
|
||||
assert.equal(withWall.nearWallBefore, true, 'фикстура: стена рядом с плитой');
|
||||
// Новая геометрия — новый массив. Кэш, ключуемый константой, отдал бы
|
||||
// размещение «у стены» для плана, в котором стены больше нет.
|
||||
const noWalls = buildIsoOverlayRenderScene({ ...input, wallSilhouettes: [] }).devices.get('device');
|
||||
assert.equal(noWalls.nearWallBefore, false, 'без стен плита не у стены');
|
||||
assert.notStrictEqual(noWalls, withWall);
|
||||
});
|
||||
|
||||
test('#473 W3: при зуме внутрь плита у стены не переиспользуется вслепую', () => {
|
||||
const { input } = perfFixture();
|
||||
// Далёкая плита: зум внутрь переиспользует доказанно безопасное размещение.
|
||||
const far = { ...input, positionOf: () => ({ x: 60, y: 20 }) };
|
||||
const farLive = buildIsoOverlayRenderScene(far).devices.get('device');
|
||||
assert.equal(farLive.nearWallBefore, false);
|
||||
const farZoomed = buildIsoOverlayRenderScene({ ...far, view: { x: 0, y: 0, w: 80, h: 80 } })
|
||||
.devices.get('device');
|
||||
assert.strictEqual(farZoomed, farLive, 'не у стены — переиспользуется');
|
||||
// Плита у стены, которую не удалось очистить: зум внутрь обязан идти в
|
||||
// точный резолвер. Гард `!nearWallBefore || cleared && …` без первой
|
||||
// половины переиспользовал бы её на масштабе, где она уже режет стену.
|
||||
const pinned = {
|
||||
...input,
|
||||
// упор со всех сторон: узкая комната не даёт места для nudge
|
||||
space: { ...input.space, rooms: [room('owner', 0, 44, 12, 56)] },
|
||||
positionOf: () => ({ x: 5, y: 50 }),
|
||||
};
|
||||
const pinnedLive = buildIsoOverlayRenderScene(pinned).devices.get('device');
|
||||
assert.equal(pinnedLive.nearWallBefore, true);
|
||||
if (pinnedLive.cleared) {
|
||||
// Фикстура не смогла создать неочищенную плиту — тест обязан сказать об
|
||||
// этом честно, а не пройти молча (правило после #426).
|
||||
assert.fail('фикстура «у стены, не очищена» не построилась: cleared=true');
|
||||
}
|
||||
const pinnedZoomed = buildIsoOverlayRenderScene({ ...pinned, view: { x: 0, y: 0, w: 80, h: 80 } })
|
||||
.devices.get('device');
|
||||
assert.notStrictEqual(pinnedZoomed, pinnedLive, 'у стены и не очищена — точный резолвер, не кэш');
|
||||
});
|
||||
|
||||
test('#473 W4: AABB-отсечение учитывает зазор безопасности', () => {
|
||||
// Плита, чьи границы не пересекают границы стены, но лежат внутри gap,
|
||||
// обязана считаться «у стены». Отсечение без зазора отбросило бы силуэт
|
||||
// до точного теста, и плита легла бы вплотную к стене.
|
||||
const wall = { outer: buildIsoPlatePolygon([0, 50], [4, 60], ISO_WALL_HEIGHT) };
|
||||
const owner = { id: 'owner', outer: [[0, 0], [100, 0], [100, 100], [0, 100]], holes: [], safePoint: [50, 50] };
|
||||
const placeAt = (x, safetyGapCssPx) => resolveIsoOverlayPlacement({
|
||||
kind: 'device', floorAnchor: [x, 50], plateHalfSize: [2, 2],
|
||||
rooms: [owner], roomsValidated: true, preferredRoomId: 'owner',
|
||||
ownerAlreadyResolved: true, resolvedOwner: { room: owner, safePoint: [50, 50] },
|
||||
showBorders: true, wallSilhouettes: [wall], wallGeometryValidated: true,
|
||||
unitsPerPixel: 1, safetyGapCssPx, wallHeight: ISO_WALL_HEIGHT, visualOffset: 0,
|
||||
});
|
||||
// без зазора плита на расстоянии 2 единиц от стены — не у стены
|
||||
const noGap = placeAt(4 + 2 + 2, 0);
|
||||
assert.equal(noGap.nearWallBefore, false, 'контроль: без зазора не у стены');
|
||||
// с зазором 4 — у стены, хотя AABB не пересекаются
|
||||
const withGap = placeAt(4 + 2 + 2, 4);
|
||||
assert.equal(withGap.nearWallBefore, true, 'в пределах зазора — у стены');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user