mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 20:29:00 +00:00
refactor(iso): drop the 2.5D overlay data nothing reads (#724)
After #714 the 2.5D overlay scene still carried what decides nothing: - src/iso-overlays.ts: IsoOverlayPlacement loses tether and grounding (always invisible) and raisedScene (always equal to visualScene); IsoOverlayOwner loses area; IsoOverlayPlacementInput loses hovered, focused, selected and filtersSupported, which the resolver ignored. IsoWallSilhouette and tetherGeometry go with them. - src/iso-scene-render.ts: the structural scene no longer projects wall silhouettes (isoWallSilhouettesOf and IsoSceneCacheEntry.wallSilhouettes) that served only as a cache key. The placement and render-scene caches are keyed by the wall geometry the scene is drawn with (IsoOverlaySceneInput. structure = scene.geometry): the structural LRU hands out the same object across zoom, stage resize and HA state, and a new one after any wall, room or opening edit. The resolveCollisions flag and its fit/live cache slots are gone: since #713 both held equal placements, and 2.5D renders only in View, where the fit probe and the live frame ask with the same devices, so they now read one snapshot. - src/houseplan-card.ts: the fit call passes no flag; the overlay scene gets structural.geometry. data-hp-iso-nudged stays the constant "false" read by the golden requireOneRise preflight, the live-touch smoke and the benchmark. Tests: iso-overlays pins the placement fields; iso-scene-render builds the structure with buildIsoWallGeometry, the #714 zoom/resize and #711 state tests stay, fit and live are asserted to share one snapshot, and two #724 AC2 tests run the production path (createIsoStructuralSource -> resolveIsoScene -> buildIsoOverlayRenderScene): a thicker wall with the same room rebuilds the scene (red with a key without walls, e.g. keyed by the room rows), and a room edit that moves the owner gives the new owner (red with a constant key). The silhouette-construction test goes with the construction. Mutants: #473 W2 (iso-placement-cache-survives-silhouette-change, id kept for history) now keys the placement cache by a constant instead of input.structure and its guard also runs the #724 AC2 tests; W6 patches the new structure line; the W5 description no longer speaks of a nudge. The isometric-contract regex checks the new key instead of the silhouette construction. docs/ISOMETRIC.md names the key. Live 2.5D output is unchanged: the 21 isometric golden scenes pass on the accepted baselines. Issue: #724 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:
+4
-1
@@ -244,7 +244,10 @@ may meet wall bodies and each other, and the overlapping pairs are the Flat ones
|
|||||||
exactly where Flat puts them, without a position correction. A device never
|
exactly where Flat puts them, without a position correction. A device never
|
||||||
moves because its Home Assistant state changed (#711 holds trivially: nothing
|
moves because its Home Assistant state changed (#711 holds trivially: nothing
|
||||||
is laid out). Neither zoom nor a stage resize is a layout event: a placement
|
is laid out). Neither zoom nor a stage resize is a layout event: a placement
|
||||||
depends only on the anchor, its owner room, the footprint and the rise.
|
depends only on the anchor, its owner room, the footprint and the rise. The
|
||||||
|
placement and render-scene caches are keyed by the wall geometry of the
|
||||||
|
structural scene (#724): a wall, room or opening edit starts them afresh, and
|
||||||
|
the fit envelope and the live frame read one snapshot.
|
||||||
|
|
||||||
History: #651 used to search a place for every marker (rigid same-room
|
History: #651 used to search a place for every marker (rigid same-room
|
||||||
clusters, a group collision resolver and a nudge of up to 48 CSS px towards the
|
clusters, a group collision resolver and a nudge of up to 48 CSS px towards the
|
||||||
|
|||||||
@@ -11085,21 +11085,22 @@ const MUTANT_DEFINITIONS = [
|
|||||||
{
|
{
|
||||||
id: 'iso-placement-cache-survives-silhouette-change',
|
id: 'iso-placement-cache-survives-silhouette-change',
|
||||||
guard: 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs '
|
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',
|
+ '&& node --test --test-name-pattern="#473 W2|#724 AC2" test/iso-scene-render.test.mjs',
|
||||||
because: 'placements are cached per wall-silhouette array identity; keying the cache by a '
|
because: 'placements are cached per structural wall geometry (#724; the wall-silhouette '
|
||||||
+ 'constant serves «near the wall» for a plan whose wall is gone (#473)',
|
+ 'array the id names was built only to be this key); keying the cache by a constant '
|
||||||
|
+ 'serves a placement, and with it the owner room, of a plan whose walls are gone (#473)',
|
||||||
patches: [{
|
patches: [{
|
||||||
file: 'src/iso-scene-render.ts',
|
file: 'src/iso-scene-render.ts',
|
||||||
find: ' let placements = isoOverlayPlacementCache.get(input.wallSilhouettes);',
|
find: ' let placements = isoOverlayPlacementCache.get(input.structure);',
|
||||||
replace: ' let placements = isoOverlayPlacementCache.get(ISO_PLACEMENT_CACHE_ANY);',
|
replace: ' let placements = isoOverlayPlacementCache.get(ISO_PLACEMENT_CACHE_ANY);',
|
||||||
}, {
|
}, {
|
||||||
file: 'src/iso-scene-render.ts',
|
file: 'src/iso-scene-render.ts',
|
||||||
find: ' isoOverlayPlacementCache.set(input.wallSilhouettes, placements);',
|
find: ' isoOverlayPlacementCache.set(input.structure, placements);',
|
||||||
replace: ' isoOverlayPlacementCache.set(ISO_PLACEMENT_CACHE_ANY, placements);',
|
replace: ' isoOverlayPlacementCache.set(ISO_PLACEMENT_CACHE_ANY, placements);',
|
||||||
}, {
|
}, {
|
||||||
file: 'src/iso-scene-render.ts',
|
file: 'src/iso-scene-render.ts',
|
||||||
find: 'export const ISO_OVERLAY_PLACEMENT_CACHE_LIMIT = 2048;',
|
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[] = [];',
|
replace: 'export const ISO_OVERLAY_PLACEMENT_CACHE_LIMIT = 2048;\nconst ISO_PLACEMENT_CACHE_ANY = {} as IsoWallGeometry;',
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
// #474: designer furniture artwork is a lazy chunk. Each protective contract
|
// #474: designer furniture artwork is a lazy chunk. Each protective contract
|
||||||
@@ -11395,12 +11396,13 @@ const MUTANT_DEFINITIONS = [
|
|||||||
{
|
{
|
||||||
id: 'stage3-w5-runtime-nudge-writes-storage',
|
id: 'stage3-w5-runtime-nudge-writes-storage',
|
||||||
guard: 'node demo/smoke_isometric_live_touch.mjs',
|
guard: 'node demo/smoke_isometric_live_touch.mjs',
|
||||||
because: 'W5: runtime nudge is presentation-only and must never write config, layout or '
|
because: 'W5: the 2.5D overlay scene (the wall-top rise of tiles and lock badges; nothing '
|
||||||
|
+ 'is nudged since #713) is presentation-only and must never write config, layout or '
|
||||||
+ 'browser storage (#160)',
|
+ 'browser storage (#160)',
|
||||||
patches: [{
|
patches: [{
|
||||||
file: 'src/houseplan-card.ts',
|
file: 'src/houseplan-card.ts',
|
||||||
find: ' return runtime.buildIsoOverlayRenderScene({',
|
find: ' return runtime.buildIsoOverlayRenderScene({',
|
||||||
replace: " localStorage.setItem('houseplan_stage3_nudge_mutant', space.id);\n"
|
replace: " localStorage.setItem('houseplan_stage3_overlay_mutant', space.id);\n"
|
||||||
+ ' return runtime.buildIsoOverlayRenderScene({',
|
+ ' return runtime.buildIsoOverlayRenderScene({',
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
@@ -11415,8 +11417,8 @@ const MUTANT_DEFINITIONS = [
|
|||||||
replace: ' if (!runtime || !layers || (!structural && disp.showBorders)) return null;',
|
replace: ' if (!runtime || !layers || (!structural && disp.showBorders)) return null;',
|
||||||
}, {
|
}, {
|
||||||
file: 'src/houseplan-card.ts',
|
file: 'src/houseplan-card.ts',
|
||||||
find: ' wallSilhouettes: structural.wallSilhouettes,',
|
find: ' structure: structural.geometry,',
|
||||||
replace: ' wallSilhouettes: structural?.wallSilhouettes ?? [],',
|
replace: " structure: structural?.geometry ?? { topPath: '', topFaces: [], sides: [], contactPath: '', edgeCount: 0 },",
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6012,7 +6012,7 @@ export class HouseplanCard extends LitElement {
|
|||||||
const overlays = this._isoOverlayScene(
|
const overlays = this._isoOverlayScene(
|
||||||
space, this._renderDevices.filter((device) => device.space === space.id && !device.hidden),
|
space, this._renderDevices.filter((device) => device.space === space.id && !device.hidden),
|
||||||
disp, runtime.resolveIsoDecorationLayers(disp), scene, iconPct, effectiveDeviceBaseSize(iconPct),
|
disp, runtime.resolveIsoDecorationLayers(disp), scene, iconPct, effectiveDeviceBaseSize(iconPct),
|
||||||
disp.showLqi ?? this._config?.show_signal ?? true, false);
|
disp.showLqi ?? this._config?.show_signal ?? true);
|
||||||
const envelope = overlays && runtime.resolveIsoOverlayFitEnvelope({
|
const envelope = overlays && runtime.resolveIsoOverlayFitEnvelope({
|
||||||
baseBounds: scene.frame, entries: overlays.entries, stageSize, targetView: (bounds) =>
|
baseBounds: scene.frame, entries: overlays.entries, stageSize, targetView: (bounds) =>
|
||||||
fitView([bounds.x, bounds.y, bounds.w, bounds.h], aspect) });
|
fitView([bounds.x, bounds.y, bounds.w, bounds.h], aspect) });
|
||||||
@@ -6067,14 +6067,12 @@ export class HouseplanCard extends LitElement {
|
|||||||
iconPct: number,
|
iconPct: number,
|
||||||
deviceBasePct: number,
|
deviceBasePct: number,
|
||||||
showLqi: boolean,
|
showLqi: boolean,
|
||||||
resolveCollisions = true,
|
|
||||||
): IsoOverlayRenderScene | null {
|
): IsoOverlayRenderScene | null {
|
||||||
const runtime = this._isoSceneRuntime;
|
const runtime = this._isoSceneRuntime;
|
||||||
if (!runtime || !layers?.structural || !structural) return null;
|
if (!runtime || !layers?.structural || !structural) return null;
|
||||||
return runtime.buildIsoOverlayRenderScene({
|
return runtime.buildIsoOverlayRenderScene({
|
||||||
space, devices: devs, openings: this._openingsR, display: disp,
|
space, devices: devs, openings: this._openingsR, display: disp,
|
||||||
wallSilhouettes: structural.wallSilhouettes,
|
structure: structural.geometry,
|
||||||
resolveCollisions,
|
|
||||||
iconPct, deviceBasePct, showLqi, cellCm: this._cellCm,
|
iconPct, deviceBasePct, showLqi, cellCm: this._cellCm,
|
||||||
kioskIconScale: this._mode === 'view' ? this._kioskScale.icon : 1,
|
kioskIconScale: this._mode === 'view' ? this._kioskScale.icon : 1,
|
||||||
kioskFontScale: this._mode === 'view' ? this._kioskScale.font : 1,
|
kioskFontScale: this._mode === 'view' ? this._kioskScale.font : 1,
|
||||||
|
|||||||
+7
-45
@@ -19,15 +19,8 @@ export interface IsoOverlayRoom {
|
|||||||
holes?: readonly (readonly PlanPoint[])[];
|
holes?: readonly (readonly PlanPoint[])[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A projected, canonical physical-wall surface. No union is done per marker. */
|
|
||||||
export interface IsoWallSilhouette {
|
|
||||||
outer: readonly ScenePoint[];
|
|
||||||
holes?: readonly (readonly ScenePoint[])[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IsoOverlayOwner {
|
export interface IsoOverlayOwner {
|
||||||
id: string;
|
id: string;
|
||||||
area: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IsoOverlayOwnerInput {
|
export interface IsoOverlayOwnerInput {
|
||||||
@@ -45,36 +38,22 @@ export interface IsoOverlayPlacementInput extends IsoOverlayOwnerInput {
|
|||||||
/** Half-size of the invisible floor-parallel footprint in plan units. */
|
/** Half-size of the invisible floor-parallel footprint in plan units. */
|
||||||
footprintHalfSize: PlanPoint;
|
footprintHalfSize: PlanPoint;
|
||||||
visualOffset?: number;
|
visualOffset?: number;
|
||||||
filtersSupported?: boolean;
|
|
||||||
hovered?: boolean;
|
|
||||||
focused?: boolean;
|
|
||||||
selected?: boolean;
|
|
||||||
camera?: IsoCamera;
|
camera?: IsoCamera;
|
||||||
/** Internal scene-builder fast path; arbitrary callers still resolve safely. */
|
/** Internal scene-builder fast path; arbitrary callers still resolve safely. */
|
||||||
ownerAlreadyResolved?: boolean;
|
ownerAlreadyResolved?: boolean;
|
||||||
resolvedOwner?: IsoOverlayOwner | null;
|
resolvedOwner?: IsoOverlayOwner | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IsoOverlayTetherGeometry {
|
|
||||||
from: ScenePoint;
|
|
||||||
to: ScenePoint;
|
|
||||||
visible: boolean;
|
|
||||||
length: number;
|
|
||||||
angleDeg: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IsoOverlayPlacement {
|
export interface IsoOverlayPlacement {
|
||||||
plane: IsoOverlayPlane;
|
plane: IsoOverlayPlane;
|
||||||
owner: IsoOverlayOwner | null;
|
owner: IsoOverlayOwner | null;
|
||||||
/** This is always the input logical point; the raised visual never mutates it. */
|
/** This is always the input logical point; the raised visual never mutates it. */
|
||||||
floorAnchor: PlanPoint;
|
floorAnchor: PlanPoint;
|
||||||
floorScene: ScenePoint;
|
floorScene: ScenePoint;
|
||||||
raisedScene: ScenePoint;
|
/** The screen-facing root: the floor anchor lifted by `visualOffset` (#713). */
|
||||||
visualScene: ScenePoint;
|
visualScene: ScenePoint;
|
||||||
/** Invisible fit footprint; never render it as a surface. */
|
/** Invisible fit footprint; never render it as a surface. */
|
||||||
footprint: readonly ScenePoint[];
|
footprint: readonly ScenePoint[];
|
||||||
grounding: { center: ScenePoint; visible: boolean };
|
|
||||||
tether: IsoOverlayTetherGeometry;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const EPS = 1e-9;
|
const EPS = 1e-9;
|
||||||
@@ -167,7 +146,7 @@ export function resolveIsoOverlayOwner(input: IsoOverlayOwnerInput): IsoOverlayO
|
|||||||
// a saved label may legitimately lie outside that room.
|
// a saved label may legitimately lie outside that room.
|
||||||
room = preferred;
|
room = preferred;
|
||||||
}
|
}
|
||||||
return room ? { id: room.id, area: roomArea(room) } : null;
|
return room ? { id: room.id } : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isoOverlayPlane(kind: IsoOverlayKind, showBorders: boolean): IsoOverlayPlane {
|
export function isoOverlayPlane(kind: IsoOverlayKind, showBorders: boolean): IsoOverlayPlane {
|
||||||
@@ -196,13 +175,6 @@ export function buildIsoFootprintPolygon(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function tetherGeometry(
|
|
||||||
from: ScenePoint, to: ScenePoint, visible: boolean,
|
|
||||||
): IsoOverlayTetherGeometry {
|
|
||||||
const dx = to[0] - from[0], dy = to[1] - from[1];
|
|
||||||
return { from, to, visible, length: Math.hypot(dx, dy), angleDeg: Math.atan2(dy, dx) * 180 / Math.PI };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve one overlay without mutating its saved coordinate. Since #713 a
|
* Resolve one overlay without mutating its saved coordinate. Since #713 a
|
||||||
* raised root is its floor anchor shifted straight up by `visualOffset`; #714
|
* raised root is its floor anchor shifted straight up by `visualOffset`; #714
|
||||||
@@ -220,30 +192,20 @@ export function resolveIsoOverlayPlacement(input: IsoOverlayPlacementInput): Iso
|
|||||||
const floorScene = projectPlanPoint(floorAnchor, 0, camera);
|
const floorScene = projectPlanPoint(floorAnchor, 0, camera);
|
||||||
const plane = isoOverlayPlane(input.kind, input.showBorders);
|
const plane = isoOverlayPlane(input.kind, input.showBorders);
|
||||||
if (plane === 'floor') {
|
if (plane === 'floor') {
|
||||||
return {
|
return { plane, owner: null, floorAnchor, floorScene, visualScene: floorScene, footprint: [] };
|
||||||
plane, owner: null, floorAnchor, floorScene, raisedScene: floorScene,
|
|
||||||
visualScene: floorScene, footprint: [],
|
|
||||||
grounding: { center: floorScene, visible: false },
|
|
||||||
tether: tetherGeometry(floorScene, floorScene, false),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The canonical floor anchor and footprint stay; the screen-facing content
|
// The canonical floor anchor and footprint stay; the screen-facing content
|
||||||
// stands `visualOffset` above the floor (#713: the wall top for tiles and
|
// stands `visualOffset` above the floor (#713: the wall top for tiles and
|
||||||
// lock badges, 0 for room names).
|
// lock badges, 0 for room names).
|
||||||
const raisedScene = projectPlanPoint(floorAnchor, visualOffset, camera);
|
const visualScene = projectPlanPoint(floorAnchor, visualOffset, camera);
|
||||||
const owner = input.ownerAlreadyResolved
|
const owner = input.ownerAlreadyResolved
|
||||||
? input.resolvedOwner ?? null
|
? input.resolvedOwner ?? null
|
||||||
: resolveIsoOverlayOwner(input);
|
: resolveIsoOverlayOwner(input);
|
||||||
const footprint = buildIsoFootprintPolygon(floorAnchor, input.footprintHalfSize,
|
const footprint = buildIsoFootprintPolygon(floorAnchor, input.footprintHalfSize,
|
||||||
visualOffset, camera);
|
visualOffset, camera);
|
||||||
// Ownership remains encoded by the immutable anchor and invisible bounded
|
// Ownership remains encoded by the immutable anchor and invisible bounded
|
||||||
// footprint. Stage 4 deliberately removes the visible ground dot and long
|
// footprint. Stage 4 removed the visible ground dot and long tether which made
|
||||||
// tether which made the architectural view look like a debug overlay.
|
// the architectural view look like a debug overlay; #724 removed their data.
|
||||||
const tetherVisible = false;
|
return { plane, owner, floorAnchor, floorScene, visualScene, footprint };
|
||||||
return {
|
|
||||||
plane, owner, floorAnchor, floorScene, raisedScene, visualScene: raisedScene, footprint,
|
|
||||||
grounding: { center: floorScene, visible: false },
|
|
||||||
tether: tetherGeometry(floorScene, raisedScene, tetherVisible),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-57
@@ -13,7 +13,7 @@ import { clampScale, islandsOf, roomPoly, type SpaceDisplay } from './logic';
|
|||||||
import {
|
import {
|
||||||
ISO_CAMERA, ISO_FLOOR_EDGE_HEIGHT,
|
ISO_CAMERA, ISO_FLOOR_EDGE_HEIGHT,
|
||||||
ISO_RAISED_OVERLAY_HEIGHT, ISO_WALL_HEIGHT,
|
ISO_RAISED_OVERLAY_HEIGHT, ISO_WALL_HEIGHT,
|
||||||
projectPlanPoint, projectedFrame, type PlanPoint, type ScenePoint,
|
projectedFrame, type PlanPoint, type ScenePoint,
|
||||||
} from './iso-projection';
|
} from './iso-projection';
|
||||||
import {
|
import {
|
||||||
buildIsoFloorGeometry, buildIsoWallGeometry, isoGeometryFingerprint,
|
buildIsoFloorGeometry, buildIsoWallGeometry, isoGeometryFingerprint,
|
||||||
@@ -28,7 +28,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
resolveIsoOverlayOwner, resolveIsoOverlayPlacement,
|
resolveIsoOverlayOwner, resolveIsoOverlayPlacement,
|
||||||
type IsoOverlayPlacement, type IsoOverlayRoom, type IsoRaisedOverlayKind,
|
type IsoOverlayPlacement, type IsoOverlayRoom, type IsoRaisedOverlayKind,
|
||||||
type IsoWallSilhouette,
|
|
||||||
} from './iso-overlays';
|
} from './iso-overlays';
|
||||||
import {
|
import {
|
||||||
floorFootprintGeometry, openingInnerFaceOffsetFromIndex,
|
floorFootprintGeometry, openingInnerFaceOffsetFromIndex,
|
||||||
@@ -376,7 +375,6 @@ export function isoRaisedOverlayHalfSize(input: IsoRaisedFootprintInput): PlanPo
|
|||||||
export type IsoSceneCacheEntry = {
|
export type IsoSceneCacheEntry = {
|
||||||
geometry: IsoWallGeometry;
|
geometry: IsoWallGeometry;
|
||||||
floor: IsoFloorGeometry;
|
floor: IsoFloorGeometry;
|
||||||
wallSilhouettes: readonly IsoWallSilhouette[];
|
|
||||||
openings: readonly IsoOpeningBasis[];
|
openings: readonly IsoOpeningBasis[];
|
||||||
openingSurfaces: readonly IsoOpeningRenderSurface[];
|
openingSurfaces: readonly IsoOpeningRenderSurface[];
|
||||||
};
|
};
|
||||||
@@ -568,27 +566,6 @@ export function createIsoStructuralSource(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const unknownArray = (value: unknown): readonly unknown[] => Array.isArray(value) ? value : [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Project a cached physical wall union once. Since #714 no overlay is tested
|
|
||||||
* against it; the frozen array is the structural identity the overlay caches key on.
|
|
||||||
*/
|
|
||||||
export function isoWallSilhouettesOf(geometry: unknown, height: number): IsoWallSilhouette[] {
|
|
||||||
const projectRing = (raw: unknown): ScenePoint[] => unknownArray(raw).flatMap((point) => {
|
|
||||||
if (!Array.isArray(point) || !Number.isFinite(point[0]) || !Number.isFinite(point[1])) return [];
|
|
||||||
return [projectPlanPoint([Number(point[0]), Number(point[1])], height)];
|
|
||||||
});
|
|
||||||
return unknownArray(geometry).flatMap((rawPolygon) => {
|
|
||||||
const polygon = unknownArray(rawPolygon);
|
|
||||||
if (!polygon.length) return [];
|
|
||||||
const outer = projectRing(polygon[0]);
|
|
||||||
if (outer.length < 3) return [];
|
|
||||||
const holes = polygon.slice(1).map(projectRing).filter((ring) => ring.length >= 3);
|
|
||||||
return [{ outer, holes } satisfies IsoWallSilhouette];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ResolveIsoSceneInput {
|
export interface ResolveIsoSceneInput {
|
||||||
source: IsoStructuralSource;
|
source: IsoStructuralSource;
|
||||||
cache: Map<string, IsoSceneCacheEntry>;
|
cache: Map<string, IsoSceneCacheEntry>;
|
||||||
@@ -604,15 +581,9 @@ export function resolveIsoScene(input: ResolveIsoSceneInput): IsoRenderScene {
|
|||||||
let value = cached.hit ? cached.value : undefined;
|
let value = cached.hit ? cached.value : undefined;
|
||||||
if (!value) {
|
if (!value) {
|
||||||
const structural = input.source.build();
|
const structural = input.source.build();
|
||||||
const geometry = buildIsoWallGeometry(structural.walls, ISO_CAMERA, wallHeight);
|
|
||||||
const wallTops = isoWallSilhouettesOf(structural.walls, wallHeight);
|
|
||||||
value = {
|
value = {
|
||||||
geometry,
|
geometry: buildIsoWallGeometry(structural.walls, ISO_CAMERA, wallHeight),
|
||||||
floor: buildIsoFloorGeometry(structural.floor, floorEdgeHeight),
|
floor: buildIsoFloorGeometry(structural.floor, floorEdgeHeight),
|
||||||
wallSilhouettes: Object.freeze([
|
|
||||||
...wallTops,
|
|
||||||
...geometry.sides.map((face) => ({ outer: face.points })),
|
|
||||||
]),
|
|
||||||
openings: structural.openings,
|
openings: structural.openings,
|
||||||
openingSurfaces: structural.openingSurfaces,
|
openingSurfaces: structural.openingSurfaces,
|
||||||
};
|
};
|
||||||
@@ -706,13 +677,14 @@ export interface IsoOverlaySceneInput {
|
|||||||
devices: readonly DevItem[];
|
devices: readonly DevItem[];
|
||||||
openings: readonly RenderOpening[];
|
openings: readonly RenderOpening[];
|
||||||
display: SpaceDisplay;
|
display: SpaceDisplay;
|
||||||
/** Identity of the structural scene; the placement caches are keyed by it. */
|
|
||||||
wallSilhouettes: readonly IsoWallSilhouette[];
|
|
||||||
/**
|
/**
|
||||||
* `false` for fit-envelope probes, which keep their own cache slot. Since
|
* The wall geometry of the structural scene the overlays stand in (#724). The
|
||||||
* #713 there is no collision search, so both slots hold equal placements.
|
* overlay caches are keyed by its identity: the structural LRU hands out the
|
||||||
|
* same object while only zoom, stage size or HA state change, and a new one
|
||||||
|
* after any wall, room or opening edit — so an edit never serves a placement,
|
||||||
|
* and with it an owner room, of a plan that is gone.
|
||||||
*/
|
*/
|
||||||
resolveCollisions?: boolean;
|
structure: IsoWallGeometry;
|
||||||
iconPct: number;
|
iconPct: number;
|
||||||
deviceBasePct: number;
|
deviceBasePct: number;
|
||||||
showLqi: boolean;
|
showLqi: boolean;
|
||||||
@@ -733,14 +705,13 @@ type IsoOverlayPlacementCacheEntry = { signature: string; placement: IsoOverlayP
|
|||||||
type IsoOverlayOwnerCacheEntry = { signature: string; owner: IsoOverlayPlacement['owner'] };
|
type IsoOverlayOwnerCacheEntry = { signature: string; owner: IsoOverlayPlacement['owner'] };
|
||||||
export const ISO_OVERLAY_PLACEMENT_CACHE_LIMIT = 2048;
|
export const ISO_OVERLAY_PLACEMENT_CACHE_LIMIT = 2048;
|
||||||
const isoOverlayPlacementCache = new WeakMap<
|
const isoOverlayPlacementCache = new WeakMap<
|
||||||
readonly IsoWallSilhouette[], Map<string, IsoOverlayPlacementCacheEntry>
|
IsoWallGeometry, Map<string, IsoOverlayPlacementCacheEntry>
|
||||||
>();
|
>();
|
||||||
const isoOverlayOwnerCache = new WeakMap<
|
const isoOverlayOwnerCache = new WeakMap<
|
||||||
readonly IsoOverlayRoomRow[], Map<string, IsoOverlayOwnerCacheEntry>
|
readonly IsoOverlayRoomRow[], Map<string, IsoOverlayOwnerCacheEntry>
|
||||||
>();
|
>();
|
||||||
const isoOverlayRenderSceneCache = new WeakMap<
|
/** One snapshot per structure: fit and the live frame share it (#724). */
|
||||||
readonly IsoWallSilhouette[], Map<'fit' | 'live', IsoOverlayRenderScene>
|
const isoOverlayRenderSceneCache = new WeakMap<IsoWallGeometry, IsoOverlayRenderScene>();
|
||||||
>();
|
|
||||||
|
|
||||||
function samePlacementMap<K>(
|
function samePlacementMap<K>(
|
||||||
previous: ReadonlyMap<K, IsoOverlayPlacement>, next: ReadonlyMap<K, IsoOverlayPlacement>,
|
previous: ReadonlyMap<K, IsoOverlayPlacement>, next: ReadonlyMap<K, IsoOverlayPlacement>,
|
||||||
@@ -795,10 +766,10 @@ export function buildIsoOverlayRenderScene(input: IsoOverlaySceneInput): IsoOver
|
|||||||
const roomPlacements = new Map<RoomCfg, IsoOverlayPlacement>();
|
const roomPlacements = new Map<RoomCfg, IsoOverlayPlacement>();
|
||||||
const locks = new Map<string, IsoOverlayPlacement>();
|
const locks = new Map<string, IsoOverlayPlacement>();
|
||||||
const entries: IsoOverlayRenderEntry[] = [];
|
const entries: IsoOverlayRenderEntry[] = [];
|
||||||
let placements = isoOverlayPlacementCache.get(input.wallSilhouettes);
|
let placements = isoOverlayPlacementCache.get(input.structure);
|
||||||
if (!placements) {
|
if (!placements) {
|
||||||
placements = new Map();
|
placements = new Map();
|
||||||
isoOverlayPlacementCache.set(input.wallSilhouettes, placements);
|
isoOverlayPlacementCache.set(input.structure, placements);
|
||||||
}
|
}
|
||||||
let owners = isoOverlayOwnerCache.get(roomRows);
|
let owners = isoOverlayOwnerCache.get(roomRows);
|
||||||
if (!owners) {
|
if (!owners) {
|
||||||
@@ -815,8 +786,7 @@ export function buildIsoOverlayRenderScene(input: IsoOverlaySceneInput): IsoOver
|
|||||||
preferredRoomId?: string | null,
|
preferredRoomId?: string | null,
|
||||||
): IsoOverlayPlacement => {
|
): IsoOverlayPlacement => {
|
||||||
const visualOffset = kind === 'room-label' ? 0 : wallHeight;
|
const visualOffset = kind === 'room-label' ? 0 : wallHeight;
|
||||||
const collisionMode = input.resolveCollisions === false ? 'fit' : 'live';
|
const cacheKey = `${kind}\u0000${id}`;
|
||||||
const cacheKey = `${collisionMode}\u0000${kind}\u0000${id}`;
|
|
||||||
// Nothing here depends on zoom or stage size, so a pinch, wheel or resize
|
// Nothing here depends on zoom or stage size, so a pinch, wheel or resize
|
||||||
// reuses the immutable placement and Lit keeps the raised SVG subtree.
|
// reuses the immutable placement and Lit keeps the raised SVG subtree.
|
||||||
const signature = [floorAnchor[0], floorAnchor[1],
|
const signature = [floorAnchor[0], floorAnchor[1],
|
||||||
@@ -825,16 +795,15 @@ export function buildIsoOverlayRenderScene(input: IsoOverlaySceneInput): IsoOver
|
|||||||
].join('|');
|
].join('|');
|
||||||
const cached = lruRead(placements!, cacheKey);
|
const cached = lruRead(placements!, cacheKey);
|
||||||
if (cached.hit && cached.value.signature === signature) return cached.value.placement;
|
if (cached.hit && cached.value.signature === signature) return cached.value.placement;
|
||||||
const ownerKey = `${kind}\u0000${id}`;
|
|
||||||
const ownerSignature = [floorAnchor[0], floorAnchor[1], preferredRoomId || ''].join('|');
|
const ownerSignature = [floorAnchor[0], floorAnchor[1], preferredRoomId || ''].join('|');
|
||||||
const cachedOwner = lruRead(owners!, ownerKey);
|
const cachedOwner = lruRead(owners!, cacheKey);
|
||||||
const owner = cachedOwner.hit && cachedOwner.value.signature === ownerSignature
|
const owner = cachedOwner.hit && cachedOwner.value.signature === ownerSignature
|
||||||
? cachedOwner.value.owner
|
? cachedOwner.value.owner
|
||||||
: resolveIsoOverlayOwner({
|
: resolveIsoOverlayOwner({
|
||||||
kind, floorAnchor, rooms, roomsValidated: true, preferredRoomId,
|
kind, floorAnchor, rooms, roomsValidated: true, preferredRoomId,
|
||||||
});
|
});
|
||||||
if (!cachedOwner.hit || cachedOwner.value.signature !== ownerSignature) {
|
if (!cachedOwner.hit || cachedOwner.value.signature !== ownerSignature) {
|
||||||
lruWrite(owners!, ownerKey, { signature: ownerSignature, owner },
|
lruWrite(owners!, cacheKey, { signature: ownerSignature, owner },
|
||||||
ISO_OVERLAY_PLACEMENT_CACHE_LIMIT);
|
ISO_OVERLAY_PLACEMENT_CACHE_LIMIT);
|
||||||
}
|
}
|
||||||
const placement = resolveIsoOverlayPlacement({
|
const placement = resolveIsoOverlayPlacement({
|
||||||
@@ -927,17 +896,11 @@ export function buildIsoOverlayRenderScene(input: IsoOverlaySceneInput): IsoOver
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const mode = input.resolveCollisions === false ? 'fit' : 'live';
|
const previous = isoOverlayRenderSceneCache.get(input.structure);
|
||||||
let renderScenes = isoOverlayRenderSceneCache.get(input.wallSilhouettes);
|
|
||||||
if (!renderScenes) {
|
|
||||||
renderScenes = new Map();
|
|
||||||
isoOverlayRenderSceneCache.set(input.wallSilhouettes, renderScenes);
|
|
||||||
}
|
|
||||||
const previous = renderScenes.get(mode);
|
|
||||||
const layoutSignature = entries.map((entry) => {
|
const layoutSignature = entries.map((entry) => {
|
||||||
const placement = entry.placement;
|
const placement = entry.placement;
|
||||||
return [entry.kind, entry.id, placement.floorAnchor[0], placement.floorAnchor[1],
|
return [entry.kind, entry.id, placement.floorAnchor[0], placement.floorAnchor[1],
|
||||||
placement.raisedScene[0], placement.raisedScene[1], placement.owner?.id || '',
|
placement.visualScene[0], placement.visualScene[1], placement.owner?.id || '',
|
||||||
layoutHalfSizeOf(entry)[0], layoutHalfSizeOf(entry)[1]].join('|');
|
layoutHalfSizeOf(entry)[0], layoutHalfSizeOf(entry)[1]].join('|');
|
||||||
}).sort().join('\u0001');
|
}).sort().join('\u0001');
|
||||||
if (previous?.layoutSignature === layoutSignature
|
if (previous?.layoutSignature === layoutSignature
|
||||||
@@ -960,7 +923,7 @@ export function buildIsoOverlayRenderScene(input: IsoOverlaySceneInput): IsoOver
|
|||||||
? { ...entry, screenHalfSize: next } : entry;
|
? { ...entry, screenHalfSize: next } : entry;
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
renderScenes.set(mode, refreshed);
|
isoOverlayRenderSceneCache.set(input.structure, refreshed);
|
||||||
return refreshed;
|
return refreshed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -983,7 +946,7 @@ export function buildIsoOverlayRenderScene(input: IsoOverlaySceneInput): IsoOver
|
|||||||
&& samePlacementMap(previous.devices, scene.devices)
|
&& samePlacementMap(previous.devices, scene.devices)
|
||||||
&& samePlacementMap(previous.rooms, scene.rooms)
|
&& samePlacementMap(previous.rooms, scene.rooms)
|
||||||
&& samePlacementMap(previous.locks, scene.locks)) return previous;
|
&& samePlacementMap(previous.locks, scene.locks)) return previous;
|
||||||
renderScenes.set(mode, scene);
|
isoOverlayRenderSceneCache.set(input.structure, scene);
|
||||||
return scene;
|
return scene;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-14
@@ -88,8 +88,8 @@ test('a degenerate or missing owner room still raises the overlay straight up',
|
|||||||
const result = placement(overrides);
|
const result = placement(overrides);
|
||||||
assert.equal(result.owner, null, name);
|
assert.equal(result.owner, null, name);
|
||||||
assert.equal(result.plane, 'raised', name);
|
assert.equal(result.plane, 'raised', name);
|
||||||
assert.deepEqual(result.visualScene, result.raisedScene, `${name}: no move without an owner`);
|
assert.deepEqual(result.visualScene,
|
||||||
assert.equal(result.tether.visible, false, name);
|
projectPlanPoint(result.floorAnchor, ISO_RAISED_OVERLAY_HEIGHT), `${name}: no move without an owner`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -106,31 +106,29 @@ test('footprint corners use the same affine camera on the raised plane', () => {
|
|||||||
'zero yaw keeps the screen-facing footprint aligned with the plan axes');
|
'zero yaw keeps the screen-facing footprint aligned with the plan axes');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// #724: what a placement carries is what someone reads — no invisible tether or
|
||||||
|
// grounding, no second raised point equal to the visual one, no owner area.
|
||||||
|
const PLACEMENT_FIELDS = ['floorAnchor', 'floorScene', 'footprint', 'owner', 'plane', 'visualScene'];
|
||||||
|
|
||||||
test('free low overlay separates the immutable floor anchor from its invisible footprint', () => {
|
test('free low overlay separates the immutable floor anchor from its invisible footprint', () => {
|
||||||
const normal = placement();
|
const normal = placement();
|
||||||
|
assert.deepEqual(Object.keys(normal).sort(), PLACEMENT_FIELDS);
|
||||||
assert.equal(normal.plane, 'raised');
|
assert.equal(normal.plane, 'raised');
|
||||||
|
assert.deepEqual(normal.owner, { id: 'room' });
|
||||||
assert.deepEqual(normal.floorAnchor, [50, 50]);
|
assert.deepEqual(normal.floorAnchor, [50, 50]);
|
||||||
assert.deepEqual(normal.floorScene, projectPlanPoint([50, 50], 0));
|
assert.deepEqual(normal.floorScene, projectPlanPoint([50, 50], 0));
|
||||||
assert.deepEqual(normal.raisedScene, projectPlanPoint([50, 50], ISO_RAISED_OVERLAY_HEIGHT));
|
assert.deepEqual(normal.visualScene, projectPlanPoint([50, 50], ISO_RAISED_OVERLAY_HEIGHT));
|
||||||
assert.deepEqual(normal.visualScene, normal.raisedScene);
|
|
||||||
assert.deepEqual(normal.footprint,
|
assert.deepEqual(normal.footprint,
|
||||||
buildIsoFootprintPolygon([50, 50], [4, 4], ISO_RAISED_OVERLAY_HEIGHT));
|
buildIsoFootprintPolygon([50, 50], [4, 4], ISO_RAISED_OVERLAY_HEIGHT));
|
||||||
assert.equal(normal.grounding.visible, false);
|
|
||||||
assert.equal(normal.tether.visible, false);
|
|
||||||
|
|
||||||
for (const state of ['hovered', 'focused', 'selected'])
|
|
||||||
assert.equal(placement({ [state]: true }).tether.visible, false, state);
|
|
||||||
assert.equal(placement({ filtersSupported: false }).grounding.visible, false,
|
|
||||||
'unsupported filters remove the soft grounding shadow only');
|
|
||||||
assert.deepEqual(placement({ visualOffset: 0 }).visualScene, normal.floorScene,
|
assert.deepEqual(placement({ visualOffset: 0 }).visualScene, normal.floorScene,
|
||||||
'a zero offset keeps the root on its floor point (#713 room names)');
|
'a zero offset keeps the root on its floor point (#713 room names)');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('show_borders:false is exact no-volume: floor anchor, no footprint/cues', () => {
|
test('show_borders:false is exact no-volume: floor anchor, no footprint/cues', () => {
|
||||||
const result = placement({ showBorders: false, hovered: true });
|
const result = placement({ showBorders: false });
|
||||||
|
assert.deepEqual(Object.keys(result).sort(), PLACEMENT_FIELDS);
|
||||||
assert.equal(result.plane, 'floor');
|
assert.equal(result.plane, 'floor');
|
||||||
|
assert.equal(result.owner, null);
|
||||||
assert.deepEqual(result.visualScene, result.floorScene);
|
assert.deepEqual(result.visualScene, result.floorScene);
|
||||||
assert.deepEqual(result.footprint, []);
|
assert.deepEqual(result.footprint, []);
|
||||||
assert.equal(result.grounding.visible, false);
|
|
||||||
assert.equal(result.tether.visible, false);
|
|
||||||
});
|
});
|
||||||
|
|||||||
+126
-47
@@ -36,6 +36,13 @@ import {
|
|||||||
projectPlanPoint,
|
projectPlanPoint,
|
||||||
} from '../test-build/iso-projection.js';
|
} from '../test-build/iso-projection.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* #724: the overlay caches are keyed by the structural wall geometry. Every
|
||||||
|
* fixture gets its own object, as a structural scene of its own would.
|
||||||
|
*/
|
||||||
|
const structureOf = (walls = []) => buildIsoWallGeometry(walls);
|
||||||
|
const wallRect = (x0, y0, x1, y1) => [[[[x0, y0], [x1, y0], [x1, y1], [x0, y1]]]];
|
||||||
|
|
||||||
const room = (id, x0, y0, x1, y1) => ({
|
const room = (id, x0, y0, x1, y1) => ({
|
||||||
id,
|
id,
|
||||||
poly: [[x0, y0], [x1, y0], [x1, y1], [x0, y1]],
|
poly: [[x0, y0], [x1, y0], [x1, y1], [x0, y1]],
|
||||||
@@ -121,7 +128,7 @@ test('room footprint treats wide ASCII glyphs conservatively', () => {
|
|||||||
test('overlay bounds use final screen footprint and canonical owner filtering', () => {
|
test('overlay bounds use final screen footprint and canonical owner filtering', () => {
|
||||||
const placement = (owner, center) => ({
|
const placement = (owner, center) => ({
|
||||||
owner: { id: owner }, floorScene: [center[0] - 5, center[1]],
|
owner: { id: owner }, floorScene: [center[0] - 5, center[1]],
|
||||||
raisedScene: center, visualScene: center,
|
visualScene: center,
|
||||||
footprint: [[center[0] - 2, center[1] - 1], [center[0] + 2, center[1] - 1],
|
footprint: [[center[0] - 2, center[1] - 1], [center[0] + 2, center[1] - 1],
|
||||||
[center[0] + 2, center[1] + 1], [center[0] - 2, center[1] + 1]],
|
[center[0] + 2, center[1] + 1], [center[0] - 2, center[1] + 1]],
|
||||||
});
|
});
|
||||||
@@ -139,7 +146,7 @@ test('#713 K8: overlay fit is the structure plus visible tiles, no #651 nudge re
|
|||||||
const entry = {
|
const entry = {
|
||||||
id: 'edge', kind: 'device', groundRadius: 1, screenHalfSize: [10, 8],
|
id: 'edge', kind: 'device', groundRadius: 1, screenHalfSize: [10, 8],
|
||||||
placement: {
|
placement: {
|
||||||
owner: { id: 'room' }, floorScene: [95, 50], raisedScene: [95, 50],
|
owner: { id: 'room' }, floorScene: [95, 50],
|
||||||
visualScene: [95, 50],
|
visualScene: [95, 50],
|
||||||
footprint: [[90, 46], [100, 46], [100, 54], [90, 54]],
|
footprint: [[90, 46], [100, 46], [100, 54], [90, 54]],
|
||||||
},
|
},
|
||||||
@@ -324,7 +331,7 @@ test('opening-lock scene keeps physical host ownership when spatial fallback poi
|
|||||||
view: { x: 0, y: 0, w: 100, h: 100 },
|
view: { x: 0, y: 0, w: 100, h: 100 },
|
||||||
display: { showNames: false, cardFontScale: 1 },
|
display: { showNames: false, cardFontScale: 1 },
|
||||||
layers: { shadows: true },
|
layers: { shadows: true },
|
||||||
wallSilhouettes: [],
|
structure: structureOf(),
|
||||||
iconPct: 100,
|
iconPct: 100,
|
||||||
deviceBasePct: 100,
|
deviceBasePct: 100,
|
||||||
showLqi: false,
|
showLqi: false,
|
||||||
@@ -365,7 +372,7 @@ test('opening lock without a canonical host owner never guesses from point conta
|
|||||||
view: { x: 0, y: 0, w: 100, h: 100 },
|
view: { x: 0, y: 0, w: 100, h: 100 },
|
||||||
display: { showNames: false, cardFontScale: 1 },
|
display: { showNames: false, cardFontScale: 1 },
|
||||||
layers: { shadows: true },
|
layers: { shadows: true },
|
||||||
wallSilhouettes: [{ outer: [[0, 45], [100, 45], [100, 55], [0, 55]] }],
|
structure: structureOf(wallRect(0, 45, 100, 55)),
|
||||||
iconPct: 100,
|
iconPct: 100,
|
||||||
deviceBasePct: 100,
|
deviceBasePct: 100,
|
||||||
showLqi: false,
|
showLqi: false,
|
||||||
@@ -386,18 +393,15 @@ test('opening lock without a canonical host owner never guesses from point conta
|
|||||||
// the badge simply stands on the wall-top plane above its anchor.
|
// the badge simply stands on the wall-top plane above its anchor.
|
||||||
assert.equal(placement?.plane, 'raised');
|
assert.equal(placement?.plane, 'raised');
|
||||||
assert.deepEqual(placement?.visualScene, projectPlanPoint(placement.floorAnchor, ISO_WALL_HEIGHT));
|
assert.deepEqual(placement?.visualScene, projectPlanPoint(placement.floorAnchor, ISO_WALL_HEIGHT));
|
||||||
assert.equal(placement?.tether.visible, false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Stage 4 reuses pure overlay placements and fit probes agree with the live scene', () => {
|
test('Stage 4 reuses pure overlay placements, and fit and the live frame share one snapshot', () => {
|
||||||
const owner = room('owner', 0, 0, 100, 100);
|
const owner = room('owner', 0, 0, 100, 100);
|
||||||
const space = {
|
const space = {
|
||||||
id: 'floor', title: 'Floor', cellCm: 5, vb: [0, 0, 100, 100], bg: null,
|
id: 'floor', title: 'Floor', cellCm: 5, vb: [0, 0, 100, 100], bg: null,
|
||||||
rooms: [owner], wall_segments: [], room_drafts: [], partitions: [], wall_columns: [],
|
rooms: [owner], wall_segments: [], room_drafts: [], partitions: [], wall_columns: [],
|
||||||
};
|
};
|
||||||
const wallSilhouettes = [{
|
const structure = structureOf(wallRect(-4, -10, 4, 110));
|
||||||
outer: buildIsoFootprintPolygon([0, 50], [4, 60], ISO_WALL_HEIGHT),
|
|
||||||
}];
|
|
||||||
const input = {
|
const input = {
|
||||||
space,
|
space,
|
||||||
devices: [{ id: 'device', space: 'floor', marker: { room_id: 'owner' } }],
|
devices: [{ id: 'device', space: 'floor', marker: { room_id: 'owner' } }],
|
||||||
@@ -406,7 +410,7 @@ test('Stage 4 reuses pure overlay placements and fit probes agree with the live
|
|||||||
referenceView: { x: 0, y: 0, w: 100, h: 100 },
|
referenceView: { x: 0, y: 0, w: 100, h: 100 },
|
||||||
display: { showNames: false, cardFontScale: 1 },
|
display: { showNames: false, cardFontScale: 1 },
|
||||||
layers: { structural: true, shadows: true },
|
layers: { structural: true, shadows: true },
|
||||||
wallSilhouettes,
|
structure,
|
||||||
iconPct: 3.4,
|
iconPct: 3.4,
|
||||||
deviceBasePct: 3.4,
|
deviceBasePct: 3.4,
|
||||||
showLqi: false,
|
showLqi: false,
|
||||||
@@ -442,9 +446,10 @@ test('Stage 4 reuses pure overlay placements and fit probes agree with the live
|
|||||||
const resized = buildIsoOverlayRenderScene({ ...input, stageSize: { width: 40, height: 40 } });
|
const resized = buildIsoOverlayRenderScene({ ...input, stageSize: { width: 40, height: 40 } });
|
||||||
assert.strictEqual(resized, live, '#714: a stage resize is not a layout event either');
|
assert.strictEqual(resized, live, '#714: a stage resize is not a layout event either');
|
||||||
|
|
||||||
const fit = buildIsoOverlayRenderScene({ ...input, resolveCollisions: false });
|
// #724: the fit envelope and the live frame ask with the same inputs and read
|
||||||
assert.deepEqual(fit.devices.get('device')?.visualScene, live.devices.get('device')?.visualScene,
|
// one snapshot — there is no search for them to differ by (#713).
|
||||||
'#713: fit and live placements agree — there is no search to differ by');
|
const fit = buildIsoOverlayRenderScene({ ...input });
|
||||||
|
assert.strictEqual(fit, live, 'fit and live share one render-scene snapshot');
|
||||||
|
|
||||||
const zoomed = buildIsoOverlayRenderScene({ ...input, view: { x: -10, y: -10, w: 120, h: 120 } });
|
const zoomed = buildIsoOverlayRenderScene({ ...input, view: { x: -10, y: -10, w: 120, h: 120 } });
|
||||||
assert.strictEqual(zoomed, live,
|
assert.strictEqual(zoomed, live,
|
||||||
@@ -462,7 +467,6 @@ test('render scene keeps coincident devices coincident, leaves labels alone and
|
|||||||
id: 'floor', title: 'Floor', cellCm: 5, vb: [0, 0, 400, 400], bg: null,
|
id: 'floor', title: 'Floor', cellCm: 5, vb: [0, 0, 400, 400], bg: null,
|
||||||
rooms: [owner], wall_segments: [], room_drafts: [], partitions: [], wall_columns: [],
|
rooms: [owner], wall_segments: [], room_drafts: [], partitions: [], wall_columns: [],
|
||||||
};
|
};
|
||||||
const walls = [];
|
|
||||||
const devices = ['b', 'a', 'c'].map((id) => ({
|
const devices = ['b', 'a', 'c'].map((id) => ({
|
||||||
id, space: 'floor', marker: { room_id: 'owner', x: 200, y: 200 },
|
id, space: 'floor', marker: { room_id: 'owner', x: 200, y: 200 },
|
||||||
}));
|
}));
|
||||||
@@ -471,7 +475,7 @@ test('render scene keeps coincident devices coincident, leaves labels alone and
|
|||||||
view: { x: 0, y: 0, w: 400, h: 400 },
|
view: { x: 0, y: 0, w: 400, h: 400 },
|
||||||
display: { showNames: true, cardFontScale: 1 },
|
display: { showNames: true, cardFontScale: 1 },
|
||||||
layers: { structural: true, shadows: true },
|
layers: { structural: true, shadows: true },
|
||||||
wallSilhouettes: walls,
|
structure: structureOf(),
|
||||||
iconPct: 3.4, deviceBasePct: 3.4, showLqi: false, cellCm: 5,
|
iconPct: 3.4, deviceBasePct: 3.4, showLqi: false, cellCm: 5,
|
||||||
kioskIconScale: 1, kioskFontScale: 1,
|
kioskIconScale: 1, kioskFontScale: 1,
|
||||||
stageSize: { width: 200, height: 200 },
|
stageSize: { width: 200, height: 200 },
|
||||||
@@ -505,9 +509,9 @@ test('render scene keeps coincident devices coincident, leaves labels alone and
|
|||||||
const permuted = buildIsoOverlayRenderScene({ ...input, devices: [...devices].reverse() });
|
const permuted = buildIsoOverlayRenderScene({ ...input, devices: [...devices].reverse() });
|
||||||
assert.strictEqual(permuted, scene,
|
assert.strictEqual(permuted, scene,
|
||||||
'HA registry permutations reuse the same immutable group layout snapshot');
|
'HA registry permutations reuse the same immutable group layout snapshot');
|
||||||
const fit = buildIsoOverlayRenderScene({ ...input, resolveCollisions: false });
|
const fit = buildIsoOverlayRenderScene({ ...input });
|
||||||
assert.deepEqual(fit.devices.get('a').visualScene, scene.devices.get('a').visualScene,
|
assert.strictEqual(fit.devices.get('a'), scene.devices.get('a'),
|
||||||
'fit probing and the live scene place the cluster identically');
|
'fit probing and the live scene read the same placement');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('#711 состояние устройства не двигает значки: раскладка не пересчитывается, границы видят бейдж', () => {
|
test('#711 состояние устройства не двигает значки: раскладка не пересчитывается, границы видят бейдж', () => {
|
||||||
@@ -516,7 +520,7 @@ test('#711 состояние устройства не двигает знач
|
|||||||
id: 'floor-710', title: 'Floor', cellCm: 5, vb: [0, 0, 400, 400], bg: null,
|
id: 'floor-710', title: 'Floor', cellCm: 5, vb: [0, 0, 400, 400], bg: null,
|
||||||
rooms: [owner], wall_segments: [], room_drafts: [], partitions: [], wall_columns: [],
|
rooms: [owner], wall_segments: [], room_drafts: [], partitions: [], wall_columns: [],
|
||||||
};
|
};
|
||||||
const wallSilhouettes = [{ outer: buildIsoFootprintPolygon([150, 200], [154, 260], ISO_WALL_HEIGHT) }];
|
const structure = structureOf(wallRect(146, 140, 154, 260));
|
||||||
const devices = ['lamp', 'plug', 'sensor'].map((id, index) => ({
|
const devices = ['lamp', 'plug', 'sensor'].map((id, index) => ({
|
||||||
id, space: 'floor-710', marker: { room_id: 'owner', x: 160 + index * 4, y: 200 },
|
id, space: 'floor-710', marker: { room_id: 'owner', x: 160 + index * 4, y: 200 },
|
||||||
}));
|
}));
|
||||||
@@ -533,7 +537,7 @@ test('#711 состояние устройства не двигает знач
|
|||||||
view: { x: 0, y: 0, w: 400, h: 400 },
|
view: { x: 0, y: 0, w: 400, h: 400 },
|
||||||
display: { showNames: false, cardFontScale: 1 },
|
display: { showNames: false, cardFontScale: 1 },
|
||||||
layers: { structural: true, shadows: true },
|
layers: { structural: true, shadows: true },
|
||||||
wallSilhouettes,
|
structure,
|
||||||
iconPct: 3.4, deviceBasePct: 3.4, showLqi: false, cellCm: 5,
|
iconPct: 3.4, deviceBasePct: 3.4, showLqi: false, cellCm: 5,
|
||||||
kioskIconScale: 1, kioskFontScale: 1,
|
kioskIconScale: 1, kioskFontScale: 1,
|
||||||
stageSize: { width: 200, height: 200 },
|
stageSize: { width: 200, height: 200 },
|
||||||
@@ -562,26 +566,6 @@ test('#711 состояние устройства не двигает знач
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('structural wall silhouettes reuse every visible side quad', () => {
|
|
||||||
const walls = [[[[45, 20], [55, 20], [55, 80], [45, 80]]]];
|
|
||||||
const scene = resolveIsoScene({
|
|
||||||
source: {
|
|
||||||
key: 'visible-side-silhouettes',
|
|
||||||
build: () => ({ walls, floor: walls, openings: [], openingSurfaces: [] }),
|
|
||||||
},
|
|
||||||
cache: new Map(),
|
|
||||||
cellCm: 5,
|
|
||||||
liveFrame: { x: 0, y: 0, w: 100, h: 100 },
|
|
||||||
});
|
|
||||||
assert.ok(scene.geometry.sides.length > 0, 'fixture exposes visible vertical wall faces');
|
|
||||||
assert.equal(scene.wallSilhouettes.length, 1 + scene.geometry.sides.length,
|
|
||||||
'the silhouette set contains the top footprint and every visible side');
|
|
||||||
scene.geometry.sides.forEach((face, index) => {
|
|
||||||
assert.strictEqual(scene.wallSilhouettes[index + 1].outer, face.points,
|
|
||||||
`visible side ${index} reuses its exact render quad`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('orphan hosted openings never become phantom Stage 4 volumes', () => {
|
test('orphan hosted openings never become phantom Stage 4 volumes', () => {
|
||||||
const base = {
|
const base = {
|
||||||
type: 'door', rx: 20, ry: 30, rlen: 40, angle: 0,
|
type: 'door', rx: 20, ry: 30, rlen: 40, angle: 0,
|
||||||
@@ -719,7 +703,7 @@ test('removed contact shadows are never read while ambient shadow capability rem
|
|||||||
display: { showBorders: true, hideOpenings: false },
|
display: { showBorders: true, hideOpenings: false },
|
||||||
scene: {
|
scene: {
|
||||||
key: 'solid-retry', geometry,
|
key: 'solid-retry', geometry,
|
||||||
floor: { footprintPath: '', sides: [] }, wallSilhouettes: [],
|
floor: { footprintPath: '', sides: [] },
|
||||||
openings: [], openingSurfaces: [], frame: { x: 0, y: 0, w: 100, h: 100 },
|
openings: [], openingSurfaces: [], frame: { x: 0, y: 0, w: 100, h: 100 },
|
||||||
},
|
},
|
||||||
openings: [], amountOf: () => 0, overlays: () => null, cellCm: 5,
|
openings: [], amountOf: () => 0, overlays: () => null, cellCm: 5,
|
||||||
@@ -846,7 +830,7 @@ const perfFixture = () => {
|
|||||||
id: 'floor', title: 'Floor', cellCm: 5, vb: [0, 0, 100, 100], bg: null,
|
id: 'floor', title: 'Floor', cellCm: 5, vb: [0, 0, 100, 100], bg: null,
|
||||||
rooms: [owner], wall_segments: [], room_drafts: [], partitions: [], wall_columns: [],
|
rooms: [owner], wall_segments: [], room_drafts: [], partitions: [], wall_columns: [],
|
||||||
};
|
};
|
||||||
const wallSilhouettes = [{ outer: buildIsoFootprintPolygon([0, 50], [4, 60], ISO_WALL_HEIGHT) }];
|
const structure = structureOf(wallRect(-4, -10, 4, 110));
|
||||||
const input = {
|
const input = {
|
||||||
space,
|
space,
|
||||||
devices: [{ id: 'device', space: 'floor', marker: { room_id: 'owner' } }],
|
devices: [{ id: 'device', space: 'floor', marker: { room_id: 'owner' } }],
|
||||||
@@ -855,7 +839,7 @@ const perfFixture = () => {
|
|||||||
referenceView: { x: 0, y: 0, w: 100, h: 100 },
|
referenceView: { x: 0, y: 0, w: 100, h: 100 },
|
||||||
display: { showNames: false, cardFontScale: 1 },
|
display: { showNames: false, cardFontScale: 1 },
|
||||||
layers: { structural: true, shadows: true },
|
layers: { structural: true, shadows: true },
|
||||||
wallSilhouettes,
|
structure,
|
||||||
iconPct: 3.4, deviceBasePct: 3.4, showLqi: false, cellCm: 5,
|
iconPct: 3.4, deviceBasePct: 3.4, showLqi: false, cellCm: 5,
|
||||||
kioskIconScale: 1, kioskFontScale: 1,
|
kioskIconScale: 1, kioskFontScale: 1,
|
||||||
stageSize: { width: 100, height: 100 },
|
stageSize: { width: 100, height: 100 },
|
||||||
@@ -870,7 +854,7 @@ const perfFixture = () => {
|
|||||||
openingEntityAvailable: () => false,
|
openingEntityAvailable: () => false,
|
||||||
openingWallIndex: () => ({ adjacencyEps: 0.1, edges: [] }),
|
openingWallIndex: () => ({ adjacencyEps: 0.1, edges: [] }),
|
||||||
};
|
};
|
||||||
return { input, wallSilhouettes };
|
return { input, structure };
|
||||||
};
|
};
|
||||||
|
|
||||||
test('#570 supersedes #473 W1: selection reuses the cue-free low placement', () => {
|
test('#570 supersedes #473 W1: selection reuses the cue-free low placement', () => {
|
||||||
@@ -885,15 +869,110 @@ test('#570 supersedes #473 W1: selection reuses the cue-free low placement', ()
|
|||||||
assert.strictEqual(again, selected, 'clearing selection keeps the same immutable placement');
|
assert.strictEqual(again, selected, 'clearing selection keeps the same immutable placement');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('#473 W2 after #713: walls no longer move a tile, the cache still follows the silhouette array', () => {
|
test('#473 W2 after #724: walls never move a tile, the cache follows the structural wall geometry', () => {
|
||||||
const { input } = perfFixture();
|
const { input } = perfFixture();
|
||||||
const withWall = buildIsoOverlayRenderScene(input).devices.get('device');
|
const withWall = buildIsoOverlayRenderScene(input).devices.get('device');
|
||||||
const noWalls = buildIsoOverlayRenderScene({ ...input, wallSilhouettes: [] }).devices.get('device');
|
const noWalls = buildIsoOverlayRenderScene({ ...input, structure: structureOf() }).devices.get('device');
|
||||||
assert.notStrictEqual(noWalls, withWall, 'a new geometry array is a new cache slot');
|
assert.notStrictEqual(noWalls, withWall, 'a new wall geometry is a new cache slot');
|
||||||
assert.deepEqual(noWalls.visualScene, withWall.visualScene,
|
assert.deepEqual(noWalls.visualScene, withWall.visualScene,
|
||||||
'the tile position does not depend on nearby walls');
|
'the tile position does not depend on nearby walls');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// #724 AC2: the overlay caches follow the wall geometry of the structural scene
|
||||||
|
// on the production path — createIsoStructuralSource → resolveIsoScene (the
|
||||||
|
// LRU) → buildIsoOverlayRenderScene({ structure: scene.geometry }).
|
||||||
|
const unhostedWalls = (poly, cm) => poly.map((a, index) => ({
|
||||||
|
key: wallKey(a, poly[(index + 1) % poly.length], 1), cm,
|
||||||
|
}));
|
||||||
|
const overlayInput = (space, structure, overrides = {}) => ({
|
||||||
|
space, structure, openings: [],
|
||||||
|
devices: [{ id: 'device', space: space.id }],
|
||||||
|
view: { x: 0, y: 0, w: 100, h: 100 },
|
||||||
|
display: { showNames: false, cardFontScale: 1 },
|
||||||
|
iconPct: 3.4, deviceBasePct: 3.4, showLqi: false, cellCm: 5,
|
||||||
|
kioskIconScale: 1, kioskFontScale: 1,
|
||||||
|
stageSize: { width: 100, height: 100 },
|
||||||
|
positionOf: () => ({ x: 50, 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: [] }),
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
test('#724 AC2: the overlay scene survives zoom, resize and HA state, and is rebuilt when a wall changes', () => {
|
||||||
|
const square = cacheRoom({ wall_ids: [] });
|
||||||
|
const cache = new Map();
|
||||||
|
const structural = (cm) => {
|
||||||
|
const input = structuralInput({ room: square, walls: unhostedWalls(square.poly, cm), coordinateScale: 1 });
|
||||||
|
return { space: input.space, scene: resolveIsoScene({
|
||||||
|
source: createIsoStructuralSource(input), cache, cellCm: 5, liveFrame: { x: 0, y: 0, w: 100, h: 100 },
|
||||||
|
}) };
|
||||||
|
};
|
||||||
|
const thin = structural(20);
|
||||||
|
assert.ok(thin.scene.geometry.sides.length > 0, 'fixture has real wall bodies');
|
||||||
|
const live = buildIsoOverlayRenderScene(overlayInput(thin.space, thin.scene.geometry));
|
||||||
|
|
||||||
|
// Same walls: the structural LRU hands out the same geometry, and neither
|
||||||
|
// zoom, a stage resize nor an HA state change is a layout event.
|
||||||
|
const again = structural(20);
|
||||||
|
assert.strictEqual(again.scene.geometry, thin.scene.geometry);
|
||||||
|
const zoomed = buildIsoOverlayRenderScene(overlayInput(again.space, again.scene.geometry, {
|
||||||
|
view: { x: 20, y: 20, w: 40, h: 40 }, stageSize: { width: 320, height: 180 },
|
||||||
|
}));
|
||||||
|
assert.strictEqual(zoomed, live, 'zoom and resize reuse the render scene');
|
||||||
|
const lit = buildIsoOverlayRenderScene(overlayInput(again.space, again.scene.geometry, {
|
||||||
|
presentationOf: () => ({
|
||||||
|
scale: 1, valueText: null, valueFullText: '', tempText: null, humText: null, lqiText: null,
|
||||||
|
valueBadge: { configured: true, enabled: true, text: '100 %', fullText: '100 %', position: 'right', tone: 'default' },
|
||||||
|
pulse: { animated: false, diameterScale: 1 },
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
assert.strictEqual(lit.devices.get('device'), live.devices.get('device'), 'HA state keeps the placement');
|
||||||
|
|
||||||
|
// A thicker wall with the same room: a new structure, so a new scene. A cache
|
||||||
|
// keyed without the walls would serve the placement of the old plan.
|
||||||
|
const thick = structural(30);
|
||||||
|
assert.notEqual(thick.scene.key, thin.scene.key);
|
||||||
|
assert.notStrictEqual(thick.scene.geometry, thin.scene.geometry);
|
||||||
|
const rebuilt = buildIsoOverlayRenderScene(overlayInput(thick.space, thick.scene.geometry));
|
||||||
|
assert.notStrictEqual(rebuilt, live, 'a wall edit rebuilds the overlay scene');
|
||||||
|
assert.notStrictEqual(rebuilt.devices.get('device'), live.devices.get('device'),
|
||||||
|
'and its placements');
|
||||||
|
assert.deepEqual(rebuilt.devices.get('device').visualScene, live.devices.get('device').visualScene,
|
||||||
|
'the tile itself stays where it was: walls never move it');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('#724 AC2: a room edit that changes the owner is a new structure — no owner of a plan that is gone', () => {
|
||||||
|
const cache = new Map();
|
||||||
|
const structural = (split) => {
|
||||||
|
const base = structuralInput();
|
||||||
|
const rooms = [
|
||||||
|
cacheRoom({ id: 'west', poly: [[0, 0], [split, 0], [split, 100], [0, 100]], wall_ids: [] }),
|
||||||
|
cacheRoom({ id: 'east', poly: [[split, 0], [200, 0], [200, 100], [split, 100]], wall_ids: [] }),
|
||||||
|
];
|
||||||
|
const space = { ...base.space, vb: [0, 0, 200, 100], rooms };
|
||||||
|
return { space, scene: resolveIsoScene({
|
||||||
|
source: createIsoStructuralSource({ ...base, space }), cache, cellCm: 5,
|
||||||
|
liveFrame: { x: 0, y: 0, w: 200, h: 100 },
|
||||||
|
}) };
|
||||||
|
};
|
||||||
|
const at = { positionOf: () => ({ x: 90, y: 50 }) };
|
||||||
|
const before = structural(100);
|
||||||
|
const west = buildIsoOverlayRenderScene(overlayInput(before.space, before.scene.geometry, at));
|
||||||
|
assert.equal(west.devices.get('device').owner?.id, 'west');
|
||||||
|
const after = structural(80);
|
||||||
|
assert.notEqual(after.scene.key, before.scene.key, 'room geometry is structural');
|
||||||
|
const east = buildIsoOverlayRenderScene(overlayInput(after.space, after.scene.geometry, at));
|
||||||
|
assert.equal(east.devices.get('device').owner?.id, 'east',
|
||||||
|
'the tile at the same point now belongs to the room that grew over it');
|
||||||
|
});
|
||||||
|
|
||||||
test('#713 AC3: live zoom never recomputes placement and every tile gets one straight-up shift', () => {
|
test('#713 AC3: live zoom never recomputes placement and every tile gets one straight-up shift', () => {
|
||||||
const { input } = perfFixture();
|
const { input } = perfFixture();
|
||||||
const lift = projectPlanPoint([0, 0], 0)[1] - projectPlanPoint([0, 0], ISO_WALL_HEIGHT)[1];
|
const lift = projectPlanPoint([0, 0], 0)[1] - projectPlanPoint([0, 0], ISO_WALL_HEIGHT)[1];
|
||||||
|
|||||||
@@ -197,7 +197,8 @@ test('#649 AC3 layout and collisions see the 2.5D tile size (× ISO_ICON_SCALE)'
|
|||||||
};
|
};
|
||||||
const scene = buildIsoOverlayRenderScene({
|
const scene = buildIsoOverlayRenderScene({
|
||||||
space, devices: [{ id: 'd1' }], openings: [], view: { x: 0, y: 0, w: 1000, h: 1000 },
|
space, devices: [{ id: 'd1' }], openings: [], view: { x: 0, y: 0, w: 1000, h: 1000 },
|
||||||
display: { showNames: false, cardFontScale: 1 }, layers: { shadows: true }, wallSilhouettes: [],
|
display: { showNames: false, cardFontScale: 1 }, layers: { shadows: true },
|
||||||
|
structure: { topPath: '', topFaces: [], sides: [], contactPath: '', edgeCount: 0 },
|
||||||
iconPct: 3, deviceBasePct: 3, showLqi: false, cellCm: 5, kioskIconScale: 1, kioskFontScale: 1,
|
iconPct: 3, deviceBasePct: 3, showLqi: false, cellCm: 5, kioskIconScale: 1, kioskFontScale: 1,
|
||||||
stageSize: { width: 1000, height: 1000 }, positionOf: () => ({ x: 500, y: 500 }),
|
stageSize: { width: 1000, height: 1000 }, positionOf: () => ({ x: 500, y: 500 }),
|
||||||
presentationOf: () => presentation, labelPositionOf: () => ({ x: 0, y: 0 }), labelScaleOf: () => 1,
|
presentationOf: () => presentation, labelPositionOf: () => ({ x: 0, y: 0 }), labelScaleOf: () => 1,
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ test('Stage 4 structural cache fingerprints geometry/camera/heights and excludes
|
|||||||
assert.match(sceneRender, /const floorEdgeHeight = gridVisualUnits\(ISO_FLOOR_EDGE_HEIGHT, input\.cellCm\)/);
|
assert.match(sceneRender, /const floorEdgeHeight = gridVisualUnits\(ISO_FLOOR_EDGE_HEIGHT, input\.cellCm\)/);
|
||||||
assert.match(sceneRender, /const raisedHeight = gridVisualUnits\(ISO_RAISED_OVERLAY_HEIGHT, input\.cellCm\)/);
|
assert.match(sceneRender, /const raisedHeight = gridVisualUnits\(ISO_RAISED_OVERLAY_HEIGHT, input\.cellCm\)/);
|
||||||
assert.match(sceneRender, /camera: ISO_CAMERA,[\s\S]*?wallHeight,[\s\S]*?raisedHeight,[\s\S]*?floorEdgeHeight,[\s\S]*?algorithm: 6/);
|
assert.match(sceneRender, /camera: ISO_CAMERA,[\s\S]*?wallHeight,[\s\S]*?raisedHeight,[\s\S]*?floorEdgeHeight,[\s\S]*?algorithm: 6/);
|
||||||
const source = section(sceneRender, 'export function createIsoStructuralSource', 'const unknownArray');
|
const source = section(sceneRender, 'export function createIsoStructuralSource', 'export interface ResolveIsoSceneInput');
|
||||||
const roomProjection = section(sceneRender,
|
const roomProjection = section(sceneRender,
|
||||||
'export function isoStructuralRoomGeometry', 'export type IsoStructuralOpeningHost');
|
'export function isoStructuralRoomGeometry', 'export type IsoStructuralOpeningHost');
|
||||||
for (const field of ['id', 'x', 'y', 'w', 'h', 'poly', 'wall_ids'])
|
for (const field of ['id', 'x', 'y', 'w', 'h', 'poly', 'wall_ids'])
|
||||||
@@ -204,8 +204,13 @@ test('Stage 4 structural cache fingerprints geometry/camera/heights and excludes
|
|||||||
assert.match(sceneRender, /projectIsoOpening\(basis, amountOf\(opening\)\)/);
|
assert.match(sceneRender, /projectIsoOpening\(basis, amountOf\(opening\)\)/);
|
||||||
assert.match(sceneRender, /lruWrite\(input\.cache, input\.source\.key, value, 8\)/);
|
assert.match(sceneRender, /lruWrite\(input\.cache, input\.source\.key, value, 8\)/);
|
||||||
assert.match(card, /data-hp-iso-structural-builds=\$\{iso \? this\._isoStructuralBuildCount : nothing\}/);
|
assert.match(card, /data-hp-iso-structural-builds=\$\{iso \? this\._isoStructuralBuildCount : nothing\}/);
|
||||||
assert.match(sceneRender, /const wallTops = isoWallSilhouettesOf\(structural\.walls, wallHeight\)/);
|
// #724: the overlay caches are keyed by the structural wall geometry itself —
|
||||||
assert.match(sceneRender, /wallSilhouettes:\s*Object\.freeze\(\[[\s\S]*?\.\.\.wallTops,[\s\S]*?\.\.\.geometry\.sides\.map\(\(face\) => \(\{ outer: face\.points \}\)\),[\s\S]*?\]\)/);
|
// no silhouette array is built only to be that key.
|
||||||
|
assert.match(sceneRender, /geometry: buildIsoWallGeometry\(structural\.walls, ISO_CAMERA, wallHeight\)/);
|
||||||
|
assert.match(sceneRender, /structure: IsoWallGeometry;/);
|
||||||
|
assert.match(sceneRender, /let placements = isoOverlayPlacementCache\.get\(input\.structure\)/);
|
||||||
|
assert.match(sceneRender, /const previous = isoOverlayRenderSceneCache\.get\(input\.structure\)/);
|
||||||
|
assert.doesNotMatch(sceneRender, /wallSilhouettes|isoWallSilhouettesOf|resolveCollisions/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('show_borders:false keeps the Flat floor plane and removes every volume cue', () => {
|
test('show_borders:false keeps the Flat floor plane and removes every volume cue', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user