Grundriss: Decken-Umriss an Wand-Footprints clippen (Decke liegt darunter)

Die Decke ist ein Slab ueber der Schnittebene; ihre Umrisslinie soll dort, wo
eine Wand darueber steht, NICHT durch die Wand-Poche schlagen (glPlan zeichnet
alle Fuellungen, dann alle Linien -> Kontur landete sonst ueber den Waenden).
Die Fuellflaeche wird jetzt strokelos gezeichnet, der Umriss nur ueber die
Teilstuecke, die KEIN Wand-Footprint (OBB) verdeckt. Deckungsgleiche Decke
(Umriss = Wand-Mittellinien) -> Umriss entfaellt ganz; nur Ueberstaende bleiben.
This commit is contained in:
2026-07-04 23:26:00 +02:00
parent aa4205bf0b
commit c7e080f639
+120 -11
View File
@@ -44,6 +44,7 @@ import {
add,
along,
clippedBand,
dot,
leftNormal,
lineIntersect,
normalize,
@@ -574,6 +575,11 @@ export function generatePlan(
visibleCodes.has(c.categoryCode) &&
categoryDisplay(c.categoryCode).render,
);
// Wand-Footprints für das Verdecken der (darunterliegenden) Decken-Umrisse:
// die Decke liegt als Slab über der Schnittebene, ihre Umrisslinie soll dort,
// wo eine Wand darüber steht, NICHT durch die Wand-Poché schlagen. Nur nötig,
// wenn es überhaupt Decken gibt.
const wallFootprints = ceilings.length ? buildWallFootprints(project, walls) : [];
for (const ceiling of ceilings) {
const greyed = categoryDisplay(ceiling.categoryCode).greyed;
const category = catByCode.get(ceiling.categoryCode);
@@ -583,7 +589,7 @@ export function generatePlan(
category,
lwByCode.get(ceiling.categoryCode) ?? WALL_FALLBACK_MM,
);
addCeilingPoche(primitives, project, ceiling, greyed, detail, lwMm, category);
addCeilingPoche(primitives, project, ceiling, greyed, detail, lwMm, category, wallFootprints);
}
// Räume (SIA-416-Flächen) dieses Geschosses: transluzente Farbfüllung + Stempel
@@ -1077,6 +1083,88 @@ function subtractIntervals(
return parts;
}
/**
* Wand-Footprint als orientiertes Rechteck (OBB) für das Verdecken der
* darunterliegenden Decke im Grundriss: Achs-Startpunkt + Einheits-Richtung `u`
* und -Normale `m`, Halbdicke, Referenz-Querversatz (Wandachse↔Bandmitte) und
* Achslänge. Die u-Ausdehnung wird beim Test an beiden Enden um die Halbdicke
* verlängert, damit auch die Eck-Überlappe (Gehrungen) sicher abgedeckt sind.
*/
interface WallFootprint {
a: Vec2;
u: Vec2;
m: Vec2;
len: number;
half: number;
refOff: number;
}
/** Baut die Wand-Footprints (OBBs) aus den gerenderten Wänden. Nicht auflösbarer
* Wandtyp ⇒ Default-Dicke 0.2 m (wie der Vollkörper-Fallback im 3D-Emitter). */
function buildWallFootprints(project: Project, walls: Wall[]): WallFootprint[] {
const out: WallFootprint[] = [];
for (const w of walls) {
let t = 0.2;
try {
t = wallTypeThickness(getWallType(project, w));
} catch {
// Default-Dicke behalten.
}
const u = normalize(sub(w.end, w.start));
out.push({
a: w.start,
u,
m: leftNormal(u),
len: Math.hypot(w.end.x - w.start.x, w.end.y - w.start.y),
half: t / 2,
refOff: wallReferenceOffset(w, t),
});
}
return out;
}
/**
* Parametrisches Intervall [t0,t1] ⊆ [0,1], über das die Strecke `pa→pb`
* INNERHALB des Wand-Footprints liegt (also von der Wand verdeckt). `null`, wenn
* die Strecke den Footprint nicht schneidet. OBB-Test über zwei Slabs: Projektion
* auf die Achse `u` (Bereich [half, len+half]) und auf die Normale `m` (Bereich
* [refOffhalf, refOff+half]); das Schnitt-Intervall beider Slabs ∩ [0,1] ist die
* verdeckte Strecke.
*/
function segmentInsideWallBand(
pa: Vec2,
pb: Vec2,
fp: WallFootprint,
): [number, number] | null {
const d = sub(pb, pa);
const rel = sub(pa, fp.a);
const clipSlab = (
f0: number,
fd: number,
lo: number,
hi: number,
): [number, number] | null => {
if (Math.abs(fd) < 1e-12)
return f0 >= lo - CUTOUT_EPS && f0 <= hi + CUTOUT_EPS ? [0, 1] : null;
let ta = (lo - f0) / fd;
let tb = (hi - f0) / fd;
if (ta > tb) [ta, tb] = [tb, ta];
return [ta, tb];
};
const su = clipSlab(dot(rel, fp.u), dot(d, fp.u), -fp.half, fp.len + fp.half);
if (!su) return null;
const sm = clipSlab(
dot(rel, fp.m),
dot(d, fp.m),
fp.refOff - fp.half,
fp.refOff + fp.half,
);
if (!sm) return null;
const t0 = Math.max(0, su[0], sm[0]);
const t1 = Math.min(1, su[1], sm[1]);
return t1 > t0 + CUTOUT_EPS ? [t0, t1] : null;
}
function addWallPoche(
out: Primitive[],
project: Project,
@@ -1614,6 +1702,7 @@ function addCeilingPoche(
detail: DetailLevel,
lwMm: number,
category: LayerCategory | undefined,
wallFootprints: WallFootprint[],
): void {
const pts = ceiling.outline;
if (pts.length < 3) return;
@@ -1646,27 +1735,47 @@ function addCeilingPoche(
resolveForeground(comp, ceiling.foreground, category, ceiling.foregroundSource),
)
: NO_HATCH;
// Gefüllte Fläche OHNE eigenen Umriss (stroke:"none"): die Decke liegt unter
// der Wand-Poché; ihre Kante wird separat als verdeckungs-geclippte Linie
// gezeichnet (s. u.), damit sie nicht durch die darüberstehenden Wände schlägt.
// Trägt weiterhin die `ceilingId` für die Links-Klick-Auswahl der Fläche.
out.push({
kind: "polygon",
pts,
fill: comp ? pocheFill(hatch.pattern) : "none",
stroke,
strokeWidthMm: LAYER_LINE_MM * LAYER_DETAIL_FACTOR[detail],
stroke: "none",
strokeWidthMm: 0,
hatch,
greyed,
ceilingId,
});
// Kräftige Umrisslinie über die volle Decke (nur Kontur, keine Füllung).
// Kräftige Umrisslinie NUR dort, wo keine Wand die Decke verdeckt: jede
// Umriss-Kante gegen die Wand-Footprints clippen und die freien Teilstücke als
// Linien ausgeben. Liegt die Decke deckungsgleich unter den Wänden (Regelfall:
// Deckenumriss = Gebäude-/Wand-Mittellinien), entfällt der Umriss ganz; nur
// Überstände (Vordach/Balkon jenseits der Wand) bleiben sichtbar.
const n = pts.length;
for (let i = 0; i < n; i++) {
const a = pts[i];
const b = pts[(i + 1) % n];
const holes: Array<[number, number]> = [];
for (const fp of wallFootprints) {
const iv = segmentInsideWallBand(a, b, fp);
if (iv) holes.push(iv);
}
const d = sub(b, a);
for (const [t0, t1] of subtractIntervals(0, 1, holes)) {
out.push({
kind: "polygon",
pts,
fill: "none",
stroke,
strokeWidthMm: outlineMm,
hatch: NO_HATCH,
kind: "line",
a: add(a, scale(d, t0)),
b: add(a, scale(d, t1)),
cls: "ceiling-outline",
weightMm: outlineMm,
color: stroke,
greyed,
ceilingId,
});
}
}
}
/**