mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 11:49:16 +00:00
v1.59.0-beta.8: audit follow-ups and inner-corner sun rays
This commit is contained in:
@@ -49,13 +49,13 @@ import {
|
||||
wallEdgeBodies, wallBodiesUnionPath, paperRoomShapesWithWalls,
|
||||
innerContourForRoom, openingInnerFaceOffset, applyWallThicknessToNewRoom,
|
||||
drawWallPreviewD, DRAW_WALL_DEFAULT_CM, wallIntervals, normalizeWallIntervals,
|
||||
intervalCmAt, type WallEntry,
|
||||
intervalCmAt, wallBodyNeedsSolid, type WallEntry,
|
||||
} from './wall-thickness';
|
||||
import {
|
||||
resolveOpenCuts, hitSharedWall, hitOuterWall, hitOpenSpan, snapOpenPoint,
|
||||
clampToEdgeEnds, jointsOnEdge, cutsToSpanEntries, syncOpenToFromCuts,
|
||||
applyThicknessOnClose, purgeOpeningsOnSpan,
|
||||
pointOnOpenCut, removeCut, rekeyOpenSpansAfterMove, degradeOpenSpans, clipOpenSpansToShared,
|
||||
pointOnOpenCut, removeCut, rekeyOpenSpansAfterMove, clipOpenSpansToShared,
|
||||
sanitizeOpenSpans, entryToSeg,
|
||||
type OpenSpanEntry,
|
||||
} from './open-spans';
|
||||
@@ -85,7 +85,7 @@ import {
|
||||
import { alignAllToGrid, type AlignReport } from './align-grid';
|
||||
import { langOf, t, type I18nKey } from './i18n';
|
||||
|
||||
const CARD_VERSION = '1.59.0-beta.7';
|
||||
const CARD_VERSION = '1.59.0-beta.8';
|
||||
/** HP-1552 boot-veil timing (AUD-1552-02). The veil holds for at least
|
||||
* BOOT_MIN_MS; every stage-height change restarts a BOOT_QUIET_MS
|
||||
* trailing-quiescence requirement (chrome still settling near the cap
|
||||
@@ -5086,7 +5086,6 @@ class HouseplanCard extends LitElement {
|
||||
// first-save migration the spec asks for).
|
||||
spans = cutsToSpanEntries(resolveOpenCuts(rooms, null, NORM_W, eps), NORM_W);
|
||||
}
|
||||
spans = degradeOpenSpans(spans, rooms, NORM_W, eps);
|
||||
spans = clipOpenSpansToShared(spans, rooms, NORM_W, eps);
|
||||
this._persistOpenCuts(spans.map((e) => entryToSeg(e, NORM_W)));
|
||||
}
|
||||
@@ -5437,7 +5436,7 @@ class HouseplanCard extends LitElement {
|
||||
const v = this._viewOr(this._baseVb());
|
||||
const px = stage && stage.clientWidth && v.w ? stage.clientWidth / v.w : 1;
|
||||
const stroke = disp?.color || 'var(--hp-muted)';
|
||||
const solid = united.depthUnits * px < 3;
|
||||
const solid = wallBodyNeedsSolid(united.depthUnits, px);
|
||||
const wf = this._fillColors.wall_fill;
|
||||
// Fill colour UNDER the hatch (owner 2026-08-05): both, never one instead
|
||||
// of the other. When the body is thinner than ~3px on screen the hatch
|
||||
@@ -7052,11 +7051,11 @@ class HouseplanCard extends LitElement {
|
||||
// side of a wall.
|
||||
//
|
||||
// DEV-EB173-01: the axis runs along the wall's INWARD NORMAL, from the
|
||||
// window line inward, and is `r.depth` = `len·cos` long — NOT along the
|
||||
// ray. For parallel rays the distance travelled from the glass is an
|
||||
// room-side opening face inward, and is `r.depth` = `len·cos` long — NOT
|
||||
// along the ray. For parallel rays, distance from the source span is an
|
||||
// affine function of the point, so its iso-alpha lines are parallel to the
|
||||
// wall; with this axis every point `source + dir·u` lands at offset
|
||||
// `u/len`. Whole pane at peak alpha, identical fade distance along every
|
||||
// `u/len`. Whole inner opening at peak alpha, identical fade distance along every
|
||||
// ray, and the parallelogram's far edge exactly on the gradient's end.
|
||||
//
|
||||
// THE RIM (owner 2026-08-04, docs/SUN.md «The rim»): a 1 px black hairline
|
||||
|
||||
+40
-14
@@ -126,9 +126,15 @@ export function snapOpenPoint(
|
||||
return [ax + dx * u, ay + dy * u];
|
||||
}
|
||||
|
||||
/** All shared-boundary segments between rooms (render units). */
|
||||
export function allSharedSegs(rooms: any[], eps: number): number[][] {
|
||||
const out: number[][] = [];
|
||||
interface SharedSeg {
|
||||
seg: number[];
|
||||
/** Room-pair identity. Adjacent pieces from different pairs must stay split. */
|
||||
pair: string;
|
||||
}
|
||||
|
||||
/** Shared-boundary segments with their room-pair semantics (render units). */
|
||||
function sharedSegsWithPairs(rooms: any[], eps: number): SharedSeg[] {
|
||||
const out: SharedSeg[] = [];
|
||||
const list = (rooms || []).filter((r) => r?.id);
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const pa = roomPoly(list[i]);
|
||||
@@ -136,12 +142,17 @@ export function allSharedSegs(rooms: any[], eps: number): number[][] {
|
||||
for (let j = i + 1; j < list.length; j++) {
|
||||
const pb = roomPoly(list[j]);
|
||||
if (!pb) continue;
|
||||
for (const sg of sharedBoundary(pa, pb, eps)) out.push(sg);
|
||||
for (const sg of sharedBoundary(pa, pb, eps)) out.push({ seg: sg, pair: `${i}:${j}` });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** All shared-boundary segments between rooms (render units). */
|
||||
export function allSharedSegs(rooms: any[], eps: number): number[][] {
|
||||
return sharedSegsWithPairs(rooms, eps).map((x) => x.seg);
|
||||
}
|
||||
|
||||
/** Find shared edge under a point; returns rooms + the atomic shared segment. */
|
||||
export function hitSharedWall(
|
||||
raw: number[],
|
||||
@@ -471,7 +482,10 @@ export function degradeOpenSpans(
|
||||
|
||||
/**
|
||||
* Project each open span onto the current shared-boundary geometry and clip
|
||||
* to the overlap. Spans that no longer overlap any shared stretch are dropped.
|
||||
* to every overlap. Spans that no longer overlap any shared stretch are dropped.
|
||||
* A Split may turn one continuous physical boundary into adjacent stretches
|
||||
* owned by different room pairs. Those pieces must remain separate so their
|
||||
* midpoints derive the correct `open_to` links (AUD-159B7-01).
|
||||
* Prevents "solid outline + dashed open" after resize when a span drifts off
|
||||
* the true shared edge.
|
||||
*/
|
||||
@@ -483,7 +497,7 @@ export function clipOpenSpansToShared(
|
||||
): OpenSpanEntry[] {
|
||||
const clean = sanitizeOpenSpans(spans);
|
||||
if (!clean.length) return [];
|
||||
const shared = allSharedSegs(roomsModel, eps);
|
||||
const shared = sharedSegsWithPairs(roomsModel, eps);
|
||||
if (!shared.length) return [];
|
||||
const out: OpenSpanEntry[] = [];
|
||||
const minLen = Math.max(eps * 4, 1e-6);
|
||||
@@ -494,8 +508,8 @@ export function clipOpenSpansToShared(
|
||||
const aLen = Math.hypot(adx, ady);
|
||||
if (aLen < minLen) continue;
|
||||
const ux = adx / aLen, uy = ady / aLen;
|
||||
let best: { lo: number; hi: number } | null = null;
|
||||
for (const sh of shared) {
|
||||
const byPair = new Map<string, { lo: number; hi: number }[]>();
|
||||
for (const { seg: sh, pair } of shared) {
|
||||
// both endpoints of `sh` must lie on the line of sg
|
||||
const d1 = Math.abs((sh[0] - ax) * uy - (sh[1] - ay) * ux);
|
||||
const d2 = Math.abs((sh[2] - ax) * uy - (sh[3] - ay) * ux);
|
||||
@@ -505,13 +519,25 @@ export function clipOpenSpansToShared(
|
||||
const lo = Math.max(0, Math.min(t1, t2));
|
||||
const hi = Math.min(aLen, Math.max(t1, t2));
|
||||
if (hi - lo < minLen) continue;
|
||||
if (!best || hi - lo > best.hi - best.lo) best = { lo, hi };
|
||||
const ranges = byPair.get(pair) || [];
|
||||
ranges.push({ lo, hi });
|
||||
byPair.set(pair, ranges);
|
||||
}
|
||||
for (const ranges of byPair.values()) {
|
||||
ranges.sort((a, b) => a.lo - b.lo || a.hi - b.hi);
|
||||
const merged: { lo: number; hi: number }[] = [];
|
||||
for (const range of ranges) {
|
||||
const tail = merged[merged.length - 1];
|
||||
if (tail && range.lo <= tail.hi + minLen) tail.hi = Math.max(tail.hi, range.hi);
|
||||
else merged.push({ ...range });
|
||||
}
|
||||
for (const range of merged) {
|
||||
const na = [ax + ux * range.lo, ay + uy * range.lo];
|
||||
const nb = [ax + ux * range.hi, ay + uy * range.hi];
|
||||
if (Math.hypot(nb[0] - na[0], nb[1] - na[1]) < minLen) continue;
|
||||
out.push(spanToEntry(na, nb, coordScale));
|
||||
}
|
||||
}
|
||||
if (!best) continue;
|
||||
const na = [ax + ux * best.lo, ay + uy * best.lo];
|
||||
const nb = [ax + ux * best.hi, ay + uy * best.hi];
|
||||
if (Math.hypot(nb[0] - na[0], nb[1] - na[1]) < minLen) continue;
|
||||
out.push(spanToEntry(na, nb, coordScale));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@ class HouseplanSpaceCard extends LitElement {
|
||||
private _snap: HpConfigSnapshot | null = null;
|
||||
private _loading = false;
|
||||
private _unsub?: () => void;
|
||||
private _stageWidth = 0;
|
||||
private _stageObserver?: ResizeObserver;
|
||||
private _observedStage?: HTMLElement;
|
||||
|
||||
static properties = {
|
||||
hass: { attribute: false },
|
||||
@@ -82,6 +85,9 @@ class HouseplanSpaceCard extends LitElement {
|
||||
public disconnectedCallback(): void {
|
||||
this._unsub?.();
|
||||
this._unsub = undefined;
|
||||
this._stageObserver?.disconnect();
|
||||
this._stageObserver = undefined;
|
||||
this._observedStage = undefined;
|
||||
this._signer.dispose();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
@@ -92,6 +98,27 @@ class HouseplanSpaceCard extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(): void {
|
||||
const stage = this.renderRoot.querySelector<HTMLElement>('.hp-static-stage') || undefined;
|
||||
if (stage === this._observedStage) return;
|
||||
this._stageObserver?.disconnect();
|
||||
this._observedStage = stage;
|
||||
if (!stage) {
|
||||
this._stageObserver = undefined;
|
||||
return;
|
||||
}
|
||||
const measure = () => {
|
||||
const width = stage.clientWidth;
|
||||
if (width > 0 && Math.abs(width - this._stageWidth) > 0.5) {
|
||||
this._stageWidth = width;
|
||||
this.requestUpdate();
|
||||
}
|
||||
};
|
||||
this._stageObserver = new ResizeObserver(measure);
|
||||
this._stageObserver.observe(stage);
|
||||
measure();
|
||||
}
|
||||
|
||||
private _loadedOnce = false;
|
||||
private async _load(): Promise<void> {
|
||||
if (!this.hass || this._loading) return;
|
||||
@@ -152,6 +179,7 @@ class HouseplanSpaceCard extends LitElement {
|
||||
layout: this._snap?.layout || {},
|
||||
spaceId,
|
||||
iconSize: this._config.icon_size,
|
||||
stageWidth: this._stageWidth,
|
||||
lang: this._lang,
|
||||
// resolved at render time: a url baked in earlier would be the unsigned one
|
||||
displayUrl: (raw) => this._signer.display(this.hass, raw),
|
||||
@@ -228,6 +256,9 @@ class HouseplanSpaceCard extends LitElement {
|
||||
stroke-width: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
.wallbody.solid {
|
||||
fill: none;
|
||||
}
|
||||
.hp-static-stage .devlayer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
+8
-2
@@ -9,7 +9,9 @@
|
||||
import { html, svg, nothing, type TemplateResult } from 'lit';
|
||||
import { buildDevices, areaLqi, areaLights, areaTemp } from './devices';
|
||||
import { spaceDisplayOf, roomFillStyle, fillColorsOf, roomFillModeOf, stageBgOf, paperRoomShapes } from './logic';
|
||||
import { wallBodiesUnionPath, paperRoomShapesWithWalls, type WallEntry } from './wall-thickness';
|
||||
import {
|
||||
wallBodiesUnionPath, paperRoomShapesWithWalls, wallBodyNeedsSolid, type WallEntry,
|
||||
} from './wall-thickness';
|
||||
import { DEFAULT_ICON_RULES, compileIconRules, EXCLUDED_DOMAINS } from './rules';
|
||||
import { t, type Lang } from './i18n';
|
||||
import { bgModeOf, northDegOf, sunStateOf, dayPhase } from './sun';
|
||||
@@ -28,6 +30,8 @@ export interface StaticRenderOpts {
|
||||
layout: Layout;
|
||||
spaceId: string;
|
||||
iconSize?: number;
|
||||
/** Measured CSS width of the static stage, used for screen-depth policies. */
|
||||
stageWidth?: number;
|
||||
lang: Lang;
|
||||
/**
|
||||
* Resolve a stored content url to what the DOM may actually request — the
|
||||
@@ -207,6 +211,8 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
|
||||
const wallUnion = walls.length && disp.showBorders
|
||||
? wallBodiesUnionPath(space.rooms, walls, [], [], GRID_STEP_N, cellCm, GRID_PITCH, NORM_W)
|
||||
: null;
|
||||
const pxPerUnit = o.stageWidth && vb[2] ? o.stageWidth / vb[2] : 1;
|
||||
const solidWall = !!wallUnion && wallBodyNeedsSolid(wallUnion.depthUnits, pxPerUnit);
|
||||
|
||||
return html`
|
||||
<div class="hp-static-stage" style="aspect-ratio:${vb[2]}/${vb[3]}${stageBg ? ';background:' + stageBg : ''};--wall-fill:${colors.wall_fill.c};--wall-fill-op:${colors.wall_fill.a}">
|
||||
@@ -229,7 +235,7 @@ export function renderSpaceStatic(o: StaticRenderOpts): TemplateResult | null {
|
||||
${wallUnion
|
||||
? svg`<g class="wallbodies" style="--room-stroke:${disp.color}">
|
||||
<path class="wallbody-fill" d="${wallUnion.d}"></path>
|
||||
<path class="wallbody" data-hp="wall" data-id="union" data-kind="union"
|
||||
<path class="wallbody ${solidWall ? 'solid' : ''}" data-hp="wall" data-id="union" data-kind="union"
|
||||
d="${wallUnion.d}"></path>
|
||||
</g>`
|
||||
: nothing}
|
||||
|
||||
+25
-29
@@ -158,17 +158,17 @@ export function rayLength(elevation: number): number {
|
||||
|
||||
/**
|
||||
* The unclipped wedge: the window span a-b extruded along `dir` by the SAME
|
||||
* `len` at both ends. An honest parallelogram — every ray through the glass
|
||||
* `len` at both ends. An honest parallelogram — every ray through the opening
|
||||
* travels exactly the wedge's reach, so the promised "30 % shorter" holds for
|
||||
* each side of every wedge, at any sun angle.
|
||||
*
|
||||
* Its far edge is parallel to the WALL, and that is not a compromise: it is
|
||||
* the iso-alpha line of the gradient the card actually draws. For parallel
|
||||
* rays the distance travelled from the glass is `depth / cos`, an affine
|
||||
* rays the distance travelled from the source span is `depth / cos`, an affine
|
||||
* function of the point whose level sets are lines PARALLEL TO THE WALL, so
|
||||
* the fade must run along the wall's NORMAL (see `SunRay.normal/depth` and
|
||||
* docs/SUN.md), not along `dir`. With that axis all three invariants hold at
|
||||
* once: the whole pane of glass sits at offset 0 (peak alpha end to end), the
|
||||
* once: the whole source span sits at offset 0 (peak alpha end to end), the
|
||||
* alpha at any point depends only on how far its own ray has travelled, and
|
||||
* the wedge's far edge coincides with the gradient's end — a bright kerb is
|
||||
* impossible by construction.
|
||||
@@ -216,7 +216,7 @@ export interface SunRay {
|
||||
roomId: string;
|
||||
/** Clipped wedge outline(s), render units. */
|
||||
polys: number[][][];
|
||||
/** Window span endpoints (the bright end of the gradient). */
|
||||
/** Room-side opening corners (the bright end of the gradient). */
|
||||
a: number[];
|
||||
b: number[];
|
||||
/** Direction the light travels (AWAY from the sun), unit vector. */
|
||||
@@ -225,7 +225,7 @@ export interface SunRay {
|
||||
len: number;
|
||||
/**
|
||||
* INWARD wall normal (unit) — the axis of the fade. The distance a point
|
||||
* has travelled from the glass is the same affine function of the point as
|
||||
* has travelled from the source span is the same affine function of the point as
|
||||
* its depth under the wall, so the gradient's iso-alpha lines are parallel
|
||||
* to the wall and its axis is this normal (docs/SUN.md, DEV-EB173-01).
|
||||
*/
|
||||
@@ -233,7 +233,7 @@ export interface SunRay {
|
||||
/**
|
||||
* Length of that axis: `len · (dir·normal)` — the perpendicular depth a ray
|
||||
* reaches after travelling `len`. A point `source + dir·u` therefore lands
|
||||
* at offset `u/len`: the glass is all at 0, the far edge all at 1.
|
||||
* at offset `u/len`: the source span is all at 0, the far edge all at 1.
|
||||
*/
|
||||
depth: number;
|
||||
}
|
||||
@@ -245,8 +245,9 @@ export interface SunRay {
|
||||
* NOT considered (documented limit).
|
||||
*
|
||||
* `innerByRoom` (optional): when wall thickness is set, clip wedges to each
|
||||
* room's inner contour and narrow the lit width through the opening tunnel
|
||||
* (`max(0, L − d·tan(|α|))`, docs/WALL-THICKNESS.md §5).
|
||||
* room's inner contour. `wallDepthByOpening` moves the full window span from
|
||||
* the wall centreline to its room-side face, so the two side rays start at the
|
||||
* opening's two inner corners (docs/WALL-THICKNESS.md §5).
|
||||
*/
|
||||
export function computeSunRays(
|
||||
rooms: SunRoom[],
|
||||
@@ -270,28 +271,23 @@ export function computeSunRays(
|
||||
if (!room) continue;
|
||||
const clipPoly = (innerByRoom && innerByRoom[info.roomId]) || room.poly;
|
||||
const rad = (w.angle * Math.PI) / 180;
|
||||
let half = w.length / 2;
|
||||
const d = wallDepthByOpening?.[w.id] || 0;
|
||||
if (d > 0) {
|
||||
// tunnel narrowing: α = angle between ray and inward normal
|
||||
const inward: [number, number] = [-info.normal[0], -info.normal[1]];
|
||||
const cos = Math.max(-1, Math.min(1, away[0] * inward[0] + away[1] * inward[1]));
|
||||
const sin = Math.sqrt(Math.max(0, 1 - cos * cos));
|
||||
const tan = cos > 1e-9 ? sin / cos : 1e6;
|
||||
const eff = Math.max(0, w.length - d * tan);
|
||||
half = eff / 2;
|
||||
if (!(eff > 0)) continue;
|
||||
}
|
||||
const half = w.length / 2;
|
||||
const normal: [number, number] = [-info.normal[0], -info.normal[1]];
|
||||
const d = Math.max(0, wallDepthByOpening?.[w.id] || 0);
|
||||
// A wall grows ±½ from its centreline. Start the whole light span on the
|
||||
// room-side face: its endpoints are the two inner corners of the opening,
|
||||
// independent of the sun's incidence angle.
|
||||
const sourceX = w.x + normal[0] * d / 2;
|
||||
const sourceY = w.y + normal[1] * d / 2;
|
||||
const hx = Math.cos(rad) * half;
|
||||
const hy = Math.sin(rad) * half;
|
||||
const a = [w.x - hx, w.y - hy];
|
||||
const b = [w.x + hx, w.y + hy];
|
||||
const a = [sourceX - hx, sourceY - hy];
|
||||
const b = [sourceX + hx, sourceY + hy];
|
||||
const len = k * w.length;
|
||||
const polys = clipToRoom(rayQuad(a, b, away, len), clipPoly);
|
||||
if (!polys.length) continue;
|
||||
// inward normal + how deep the ray gets: cos of the incidence angle,
|
||||
// which windowLit() has already found to be above RAY_MIN_COS
|
||||
const normal: [number, number] = [-info.normal[0], -info.normal[1]];
|
||||
const cos = away[0] * normal[0] + away[1] * normal[1];
|
||||
out.push({ openingId: w.id, roomId: info.roomId, polys, a, b, dir: away, len,
|
||||
normal, depth: len * cos });
|
||||
@@ -356,13 +352,13 @@ export const RAY_FADE_END = 0.85;
|
||||
|
||||
/**
|
||||
* Gradient stops along the shaft: `[offset 0..1, share of the peak alpha]`.
|
||||
* Convex ease-out — bright at the glass, half gone by a third of the way,
|
||||
* Convex ease-out — bright at the inner opening, half gone by a third of the way,
|
||||
* a whisper at two thirds, nothing from RAY_FADE_END on. Consumed by the card
|
||||
* as SVG <stop>s over the FULL wedge length, so the geometry and the gradient
|
||||
* always describe the same shaft (docs/SUN.md).
|
||||
*
|
||||
* This gradient is the ONLY thing that dissolves a wedge: the falloff runs
|
||||
* along the ray, from the glass inward, and the sides of the shaft keep the
|
||||
* along the ray, from the inner opening inward, and the sides of the shaft keep the
|
||||
* hard edge light actually has (owner 2026-08-04: «не надо размывать их
|
||||
* боковые грани»). No blur is involved anywhere.
|
||||
*/
|
||||
@@ -394,8 +390,8 @@ export function rayStops(): [number, number][] {
|
||||
*
|
||||
* Contract (docs/SUN.md, «The rim»):
|
||||
*
|
||||
* - only the two SIDE edges — the ones running from the ends of the window
|
||||
* along `dir`. Never the glass edge (a-b) and never the far edge: those are
|
||||
* - only the two SIDE edges — the ones running from the inner opening corners
|
||||
* along `dir`. Never the source edge (a-b) and never the far edge: those are
|
||||
* not boundaries of the beam, they are its source and its end;
|
||||
* - one screen pixel at any zoom (`vector-effect: non-scaling-stroke`);
|
||||
* - black, and it dies EXACTLY with the fill: same gradient axis (the wall's
|
||||
@@ -407,7 +403,7 @@ export function rayStops(): [number, number][] {
|
||||
*/
|
||||
|
||||
/**
|
||||
* Peak rim opacity at the glass, before cloud cover. Visually tuned on the
|
||||
* Peak rim opacity at the inner opening, before cloud cover. Visually tuned on the
|
||||
* demo rig at both extremes: it has to make the shaft legible on white paper
|
||||
* (the whole point) yet not read as an ink outline over the dark glow canvas.
|
||||
* Below ~0.3 the line disappears on paper at kiosk scale; above ~0.5 it turns
|
||||
@@ -456,7 +452,7 @@ export function rayRimEdges(ray: SunRay, eps = 1e-4): number[][][] {
|
||||
for (let i = 0; i < poly.length; i++) {
|
||||
const p = poly[i];
|
||||
const q = poly[(i + 1) % poly.length];
|
||||
// off the side's line? then this boundary edge is the glass, the far
|
||||
// off the side's line? then this boundary edge is the source, the far
|
||||
// edge, or a wall the room cut the wedge with — not a side of the beam
|
||||
if (Math.abs((p[0] - src[0]) * nx + (p[1] - src[1]) * ny) > eps) continue;
|
||||
if (Math.abs((q[0] - src[0]) * nx + (q[1] - src[1]) * ny) > eps) continue;
|
||||
|
||||
@@ -18,12 +18,21 @@ export const WALL_MIN_CM = 1;
|
||||
export const WALL_MAX_CM = 100;
|
||||
/** Default thickness offered in the Draw toolbar (docs/WALL-THICKNESS.md §6). */
|
||||
export const DRAW_WALL_DEFAULT_CM = 15;
|
||||
/** Below this screen depth the diagonal hatch becomes visual noise. */
|
||||
export const WALL_HATCH_MIN_PX = 3;
|
||||
|
||||
/** Mitre spikes longer than this × thickness fall back to a bevel. */
|
||||
export const MITRE_LIMIT = 4;
|
||||
|
||||
// ------------------------------- units --------------------------------------
|
||||
|
||||
/** Shared full/static render policy for the thin-on-screen fallback. */
|
||||
export function wallBodyNeedsSolid(depthUnits: number, pxPerUnit: number): boolean {
|
||||
return Number.isFinite(depthUnits) && depthUnits > 0
|
||||
&& Number.isFinite(pxPerUnit) && pxPerUnit > 0
|
||||
&& depthUnits * pxPerUnit < WALL_HATCH_MIN_PX;
|
||||
}
|
||||
|
||||
export function clampWallCm(cm: number): number {
|
||||
if (!Number.isFinite(cm)) return WALL_MIN_CM;
|
||||
return Math.max(WALL_MIN_CM, Math.min(WALL_MAX_CM, cm));
|
||||
|
||||
Reference in New Issue
Block a user