mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 06:59:46 +00:00
182 lines
6.3 KiB
TypeScript
182 lines
6.3 KiB
TypeScript
/** Physical wall faces used by the furniture magnet (#445). */
|
|
import {
|
|
inwardNormal, roomWallProfile,
|
|
type WallEntry,
|
|
} from './wall-thickness';
|
|
import { NORM_W } from './space-geometry';
|
|
import type { RoomCfg, SpaceModel } from './types';
|
|
|
|
export type FurnitureWallSurfaceOwner = 'room' | 'physical';
|
|
|
|
/**
|
|
* A finite surface on which furniture BACK may lie.
|
|
*
|
|
* Room faces are one-sided: `normal` points from masonry into the owning room.
|
|
* Independent partitions/columns are already physical polygons, so their faces
|
|
* are bidirectional (`normal = null`) and must never receive another offset.
|
|
*/
|
|
export interface FurnitureWallSurface {
|
|
a: [number, number];
|
|
b: [number, number];
|
|
axisA: [number, number];
|
|
axisB: [number, number];
|
|
normal: [number, number] | null;
|
|
owner: FurnitureWallSurfaceOwner;
|
|
stableId: string;
|
|
roomId?: string;
|
|
/** Present only for the paired faces of a non-zero exterior room wall. */
|
|
roomSide?: 'inside' | 'outside';
|
|
}
|
|
|
|
const finitePoint = (point: unknown): point is readonly [number, number] => (
|
|
Array.isArray(point) && point.length >= 2
|
|
&& Number.isFinite(point[0]) && Number.isFinite(point[1])
|
|
);
|
|
|
|
const segmentIdentity = (a: readonly number[], b: readonly number[]): string => {
|
|
const print = (point: readonly number[]) => `${point[0].toFixed(9)},${point[1].toFixed(9)}`;
|
|
const pa = print(a), pb = print(b);
|
|
return pa <= pb ? `${pa}|${pb}` : `${pb}|${pa}`;
|
|
};
|
|
|
|
/** Build every room-facing atomic surface, including zero/virtual intervals. */
|
|
export function roomFurnitureWallSurfaces(
|
|
rooms: RoomCfg[],
|
|
walls: WallEntry[] | null | undefined,
|
|
openCuts: number[][],
|
|
pitch: number,
|
|
cellCm: number,
|
|
gridPitch: number,
|
|
coordScale = 1,
|
|
): FurnitureWallSurface[] {
|
|
type RoomCandidate = FurnitureWallSurface & { atomId: string; half: number };
|
|
const candidates: RoomCandidate[] = [];
|
|
const ownersByAtom = new Map<string, Set<string>>();
|
|
for (const room of rooms || []) {
|
|
const roomId = typeof room?.id === 'string' ? room.id : '';
|
|
if (!roomId) continue;
|
|
const profile = roomWallProfile(
|
|
rooms, roomId, walls, openCuts, pitch, cellCm, gridPitch, coordScale,
|
|
);
|
|
if (!profile) continue;
|
|
for (let index = 0; index < profile.poly.length; index++) {
|
|
const rawA = profile.poly[index];
|
|
const rawB = profile.poly[(index + 1) % profile.poly.length];
|
|
if (!finitePoint(rawA) || !finitePoint(rawB)) continue;
|
|
const dx = rawB[0] - rawA[0], dy = rawB[1] - rawA[1];
|
|
if (!(Math.hypot(dx, dy) > 1e-9)) continue;
|
|
const normal = inwardNormal(profile.poly, index);
|
|
const rawHalf = Number(profile.offsets[index]);
|
|
const half = Number.isFinite(rawHalf) && rawHalf > 0 ? rawHalf : 0;
|
|
const axisA: [number, number] = [rawA[0], rawA[1]];
|
|
const axisB: [number, number] = [rawB[0], rawB[1]];
|
|
const atomId = segmentIdentity(axisA, axisB);
|
|
const owners = ownersByAtom.get(atomId) || new Set<string>();
|
|
owners.add(roomId);
|
|
ownersByAtom.set(atomId, owners);
|
|
candidates.push({
|
|
a: [axisA[0] + normal[0] * half, axisA[1] + normal[1] * half],
|
|
b: [axisB[0] + normal[0] * half, axisB[1] + normal[1] * half],
|
|
axisA,
|
|
axisB,
|
|
normal,
|
|
owner: 'room',
|
|
stableId: `room:${roomId}:${atomId}`,
|
|
roomId,
|
|
atomId,
|
|
half,
|
|
});
|
|
}
|
|
}
|
|
const out: FurnitureWallSurface[] = [];
|
|
for (const candidate of candidates) {
|
|
const { atomId, half, ...surface } = candidate;
|
|
const exterior = ownersByAtom.get(atomId)?.size === 1 && half > 1e-9;
|
|
if (!exterior) {
|
|
out.push(surface);
|
|
continue;
|
|
}
|
|
const normal = surface.normal!;
|
|
out.push({ ...surface, roomSide: 'inside', stableId: `${surface.stableId}:inside` });
|
|
out.push({
|
|
...surface,
|
|
a: [surface.axisA[0] - normal[0] * half, surface.axisA[1] - normal[1] * half],
|
|
b: [surface.axisB[0] - normal[0] * half, surface.axisB[1] - normal[1] * half],
|
|
normal: [-normal[0], -normal[1]],
|
|
roomSide: 'outside',
|
|
stableId: `${surface.stableId}:outside`,
|
|
});
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** Convert already-physical independent body faces without applying an offset. */
|
|
export function physicalFurnitureWallSurfaces(
|
|
bodies: readonly number[][][],
|
|
): FurnitureWallSurface[] {
|
|
const out: FurnitureWallSurface[] = [];
|
|
const seen = new Set<string>();
|
|
for (const body of bodies || []) {
|
|
if (!Array.isArray(body) || body.length < 2) continue;
|
|
for (let index = 0; index < body.length; index++) {
|
|
const rawA = body[index];
|
|
const rawB = body[(index + 1) % body.length];
|
|
if (!finitePoint(rawA) || !finitePoint(rawB)) continue;
|
|
if (!(Math.hypot(rawB[0] - rawA[0], rawB[1] - rawA[1]) > 1e-9)) continue;
|
|
const a: [number, number] = [rawA[0], rawA[1]];
|
|
const b: [number, number] = [rawB[0], rawB[1]];
|
|
const stableId = `physical:${segmentIdentity(a, b)}`;
|
|
if (seen.has(stableId)) continue;
|
|
seen.add(stableId);
|
|
out.push({
|
|
a, b,
|
|
axisA: [...a],
|
|
axisB: [...b],
|
|
normal: null,
|
|
owner: 'physical',
|
|
stableId,
|
|
});
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** Minimal runtime surface source; kept structural to avoid importing the editor. */
|
|
export interface FurnitureWallSurfaceSource {
|
|
_cfgEpoch: number;
|
|
_cellCm: number;
|
|
_gridPitch: number;
|
|
_wallKeyPitch: number;
|
|
_spaceWalls: WallEntry[];
|
|
_spaceModel: () => SpaceModel | undefined;
|
|
_openCuts: () => number[][];
|
|
_rawPhysicalBodiesR: () => number[][][];
|
|
}
|
|
|
|
const sourceCache = new WeakMap<object, {
|
|
key: string; value: readonly FurnitureWallSurface[];
|
|
}>();
|
|
|
|
/** One immutable candidate list per host geometry/config epoch. */
|
|
export function furnitureWallSurfacesFor(
|
|
source: FurnitureWallSurfaceSource,
|
|
): readonly FurnitureWallSurface[] {
|
|
const space = source._spaceModel();
|
|
if (!space) return [];
|
|
const key = [
|
|
space.id, source._cfgEpoch, source._cellCm,
|
|
source._gridPitch, source._wallKeyPitch,
|
|
].join('|');
|
|
const cached = sourceCache.get(source);
|
|
if (cached?.key === key) return cached.value;
|
|
const value = [
|
|
...roomFurnitureWallSurfaces(
|
|
space.rooms, source._spaceWalls, source._openCuts(),
|
|
source._wallKeyPitch, source._cellCm, source._gridPitch, NORM_W,
|
|
),
|
|
...physicalFurnitureWallSurfaces(source._rawPhysicalBodiesR()),
|
|
];
|
|
sourceCache.set(source, { key, value });
|
|
return value;
|
|
}
|