From 094d8b4504599590ea67934f462175712091e12c Mon Sep 17 00:00:00 2001 From: Karim Date: Fri, 3 Jul 2026 18:50:31 +0200 Subject: [PATCH] Schnitt: Cut-Polygone nutzen die echte Bauteil-Schraffur statt Albedo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die geschnittenen Flächen erhielten bisher die rohe Albedo-Farbe der Engine plus eine generische Diagonalschraffur. Jetzt löst attachCutStyles() jede Cut-Fläche über den index-parallelen ComponentRef auf ihr Quell-Bauteil auf: - toSection.ts: wallSegmentOwners()/slabSegmentOwners() bilden die exakte Segment-Zerlegung aus projectToModel3d nach (gleiche EPS-/Öffnungs-Logik) und liefern je emittiertem Segment das Quell-Wall/Ceiling — index-gleich zur WASM-Elementreihenfolge (per Test bewiesen: W1,W1,W1,W2). - generatePlan.ts: resolveWallSectionStyle/resolveCeilingSectionStyle wählen die Komponente mit höchster joinPriority (wie die Grundriss-Poché) bzw. die erste Deckenschicht und liefern fill+HatchRender über resolveHatch. generateSectionPlan nutzt diese, im Mono-Modus weiss/SECTION_INK. Fallback bei fehlendem Bauteil = bisheriges Verhalten (Albedo + generische Schraffur). Gates: tsc -b 0, build grün. --- src/plan/generatePlan.ts | 68 ++++++++++++++++++-- src/plan/toSection.ts | 132 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 192 insertions(+), 8 deletions(-) diff --git a/src/plan/generatePlan.ts b/src/plan/generatePlan.ts index 339d46c..9d1f2e9 100644 --- a/src/plan/generatePlan.ts +++ b/src/plan/generatePlan.ts @@ -7,6 +7,7 @@ import type { Ceiling, + Component, Drawing2D, HatchPattern, LayerCategory, @@ -254,6 +255,54 @@ function resolveHatch(project: Project, hatchId: string): HatchRender { }; } +/** Füllfarbe + Schraffur eines aufgelösten Bauteils, fertig für eine Cut-Poché. */ +export interface SectionCutStyle { + fill: string; + hatch: HatchRender; +} + +/** + * Löst das tragende Bauteil (höchste `joinPriority`) einer Wand auf — dieselbe + * Regel wie {@link backboneColor} für die grobe Grundriss-Poché — und liefert + * dessen Füllfarbe + echte Schraffur für die Schnitt-Poché. `null`, wenn Wandtyp + * oder Bauteil nicht auflösbar sind (z. B. verwaistes `wallTypeId`); der + * Aufrufer fällt dann auf die generische Schnitt-Schraffur zurück. + */ +export function resolveWallSectionStyle(project: Project, wall: Wall): SectionCutStyle | null { + try { + const wt = getWallType(project, wall); + let best: Component | null = null; + for (const layer of wt.layers) { + const comp = getComponent(project, layer.componentId); + if (!best || comp.joinPriority > best.joinPriority) best = comp; + } + if (!best) return null; + return { fill: best.color, hatch: resolveHatch(project, best.hatchId) }; + } catch { + return null; + } +} + +/** + * Löst das erste Bauteil des Deckentyps einer Decke auf — dieselbe Regel wie + * {@link addCeilingPoche} für die normale Deckenpoché — und liefert dessen + * Füllfarbe + echte Schraffur für die Schnitt-Poché. `null`, wenn Deckentyp + * oder Bauteil nicht auflösbar sind bzw. der Deckentyp keine Schicht trägt. + */ +export function resolveCeilingSectionStyle( + project: Project, + ceiling: Ceiling, +): SectionCutStyle | null { + try { + const wt = getCeilingType(project, ceiling); + if (wt.layers.length === 0) return null; + const comp = getComponent(project, wt.layers[0].componentId); + return { fill: comp.color, hatch: resolveHatch(project, comp.hatchId) }; + } catch { + return null; + } +} + export interface Plan { primitives: Primitive[]; bounds: { minX: number; minY: number; maxX: number; maxY: number }; @@ -465,22 +514,31 @@ export function generateSectionPlan(output: SectionOutput, mono = false): Plan { if (y > maxY) maxY = y; }; - // Cut-Polygone: gefüllte Poché-Fläche (Bauteilfarbe) + Schnitt-Schraffur + - // kräftige Umrisslinie — wie eine geschnittene Wand im Grundriss. + // Cut-Polygone: gefüllte Poché-Fläche + Schnitt-Schraffur + kräftige + // Umrisslinie — wie eine geschnittene Wand im Grundriss. `toSection.ts` + // (computeSection) löst je Cut-Polygon bereits das Quell-Bauteil auf und + // hängt dessen echte Füllfarbe/Schraffur an (`cp.fill`/`cp.hatch`); fehlt + // diese Auflösung (Fallback), greift die generische Schnitt-Schraffur auf + // Basis der rohen Albedo-Farbe aus dem 3D-Modell. for (const cp of output.cutPolygons) { if (cp.pts.length < 3) continue; const pts: Vec2[] = cp.pts.map(([u, v]) => { acc(u, v); return { x: u, y: v }; }); - // Mono: reines Weiss statt Bauteilfarbe (nur Umriss + Schraffur tragen). + const fill = cp.fill ?? rgbToHex(cp.color); + const hatch = cp.hatch ?? SECTION_HATCH; + // Mono: reines Weiss statt Bauteilfarbe, Schraffurfarbe → Tinte (nur + // Umriss + Schraffur-Musterlinien tragen; wie der toMono()-Postpass für + // die normale Poché, hier direkt inline, da Cut-Polygone kein "solid" + // führen). primitives.push({ kind: "polygon", pts, - fill: mono ? "#ffffff" : rgbToHex(cp.color), + fill: mono ? "#ffffff" : fill, stroke: SECTION_INK, strokeWidthMm: SECTION_CUT_OUTLINE_MM, - hatch: SECTION_HATCH, + hatch: mono && hatch.pattern !== "none" ? { ...hatch, color: SECTION_INK } : hatch, }); } diff --git a/src/plan/toSection.ts b/src/plan/toSection.ts index 8d3113f..b78ee6c 100644 --- a/src/plan/toSection.ts +++ b/src/plan/toSection.ts @@ -15,9 +15,15 @@ // Einheiten: METER. Koordinaten der Ausgabe: u = horizontal entlang der Ebene, // v = absolute Höhe (world.y). Siehe src-tauri/render3d/src/section.rs. -import type { DrawingLevel, Project } from "../model/types"; +import type { Ceiling, DrawingLevel, Project, Wall } from "../model/types"; +import { openingsOfWall } from "../model/types"; +import { ceilingVerticalExtent, wallVerticalExtent } from "../model/wall"; +import { openingInterval, openingVerticalExtent } from "../geometry/opening"; import { projectToModel3d } from "./toWalls3d"; import { loadEngine3d } from "../engine/engine3d"; +import type { SectionCutStyle } from "./generatePlan"; +import { resolveCeilingSectionStyle, resolveWallSectionStyle } from "./generatePlan"; +import type { HatchRender } from "./generatePlan"; /** Bauteil-Referenz eines Cut-Polygons/einer Kante (Art + Eingabe-Index). */ export interface SectionComponentRef { @@ -28,10 +34,19 @@ export interface SectionComponentRef { /** Ein geschnittenes Bauteil-Rechteck in (u, v)-Metern (Ring, implizit geschlossen). */ export interface SectionCutPolygon { component: SectionComponentRef; - /** Albedo-Farbe des Bauteils (RGB 0..1). */ + /** Albedo-Farbe des Bauteils (RGB 0..1) — Rohwert aus dem 3D-Modell (Fallback). */ color: [number, number, number]; /** Ring-Ecken (u, v). */ pts: Array<[number, number]>; + /** + * Echte Poché-Füllfarbe des geschnittenen Bauteils (tragende Wandschicht + * bzw. erste Deckenschicht), von `computeSection` aufgelöst. Fehlt, wenn das + * Quell-Bauteil nicht auflösbar war — der Aufrufer (generateSectionPlan) + * fällt dann auf `color` zurück. + */ + fill?: string; + /** Echte Schnitt-Schraffur des geschnittenen Bauteils, analog zu `fill`. */ + hatch?: HatchRender; } /** Ein projiziertes Liniensegment in (u, v)-Metern. */ @@ -102,6 +117,115 @@ export function sectionPlaneFromLevel(level: DrawingLevel): SectionPlaneSpec | n }; } +// ── Cut-Polygon → Quell-Bauteil (für die echte Hatch-Auflösung) ──────────── +// `cut_section_json` (section.rs) nummeriert jedes Cut-Polygon per +// `ComponentRef { kind, index }`, wobei `index` exakt die Position im +// `walls`/`slabs`-Array ist, das ihm als Modell-JSON übergeben wurde +// (`walls.iter().enumerate()` in section.rs) — also dieselbe Reihenfolge wie +// `projectToModel3d(project).walls`/`.slabs` (toWalls3d.ts). +// +// `projectToWalls3d` zerlegt EINE Wand mit Öffnungen in mehrere Teilquader +// (Pfeiler/Brüstung/Sturz) — die Emission ist also nicht 1:1 Wand→Element. +// Um einen `index` wieder auf seine Quellwand zurückzuführen, wird hier exakt +// dieselbe Segmentierungs-/Skip-Logik wie `emitWall`/`pushSegment` +// (toWalls3d.ts) repliziert — NICHT die Geometrie, nur welches Quellelement +// pro emittiertem Segment "besitzt". Bleibt diese Zählung nicht exakt parallel +// zu `emitWall`, verschieben sich die Indizes und die Hatch-Auflösung wird +// falsch (harmlos: der Aufrufer fällt dann pro Cut-Polygon auf die generische +// Schnitt-Schraffur zurück, siehe `resolveWallSectionStyle`/`generatePlan.ts`). +const OWNER_EPS = 1e-4; + +/** Liefert je emittiertem Wand-Teilquader (in Emissions-Reihenfolge) die Quellwand. */ +export function wallSegmentOwners(project: Project): Wall[] { + const owners: Wall[] = []; + const pushOwner = (wall: Wall, from: number, to: number, zBottom: number, zTop: number) => { + if (to - from <= OWNER_EPS) return; + if (zTop - zBottom <= OWNER_EPS) return; + owners.push(wall); + }; + for (const wall of project.walls) { + const { zBottom, zTop } = wallVerticalExtent(project, wall); + const axisLen = Math.hypot(wall.end.x - wall.start.x, wall.end.y - wall.start.y); + if (axisLen < 1e-9 || zTop - zBottom <= OWNER_EPS) continue; + + const cutouts: Array<{ from: number; to: number; oBottom: number; oTop: number }> = []; + for (const op of openingsOfWall(project, wall.id)) { + const iv = openingInterval(wall, op); + if (!iv) continue; + const v = openingVerticalExtent(project, wall, op); + cutouts.push({ from: iv.from, to: iv.to, oBottom: v.zBottom, oTop: v.zTop }); + } + for (const d of project.doors ?? []) { + if (d.hostWallId !== wall.id) continue; + const from = Math.max(0, Math.min(d.position, axisLen)); + const to = Math.max(from, Math.min(d.position + d.width, axisLen)); + if (to - from < OWNER_EPS) continue; + const oTop = Math.min(zTop, zBottom + d.height); + cutouts.push({ from, to, oBottom: zBottom, oTop }); + } + cutouts.sort((a, b) => a.from - b.from); + + if (cutouts.length === 0) { + pushOwner(wall, 0, axisLen, zBottom, zTop); + continue; + } + + let cursor = 0; + for (const { from, to, oBottom, oTop } of cutouts) { + const segFrom = Math.max(cursor, from); + if (from > cursor) pushOwner(wall, cursor, from, zBottom, zTop); + if (to > segFrom) { + if (oBottom > zBottom + OWNER_EPS) pushOwner(wall, segFrom, to, zBottom, oBottom); + if (oTop < zTop - OWNER_EPS) pushOwner(wall, segFrom, to, oTop, zTop); + } + cursor = Math.max(cursor, to); + } + if (cursor < axisLen) pushOwner(wall, cursor, axisLen, zBottom, zTop); + } + return owners; +} + +/** Liefert je emittierter Deckenplatte (in Emissions-Reihenfolge) die Quelldecke (1:1, keine Zerlegung). */ +export function slabSegmentOwners(project: Project): Ceiling[] { + const owners: Ceiling[] = []; + for (const c of project.ceilings ?? []) { + if (!c.outline || c.outline.length < 3) continue; + const { zBottom, zTop } = ceilingVerticalExtent(project, c); + if (zTop - zBottom <= OWNER_EPS) continue; + owners.push(c); + } + return owners; +} + +/** + * Hängt an jedes Cut-Polygon von `output` (in-place) die echte Poché-Füllfarbe + * + Schraffur des geschnittenen Bauteils an (`fill`/`hatch`), aufgelöst über + * die Wand-/Decken-Besitzer-Listen. Nicht auflösbare Referenzen (Index + * außerhalb der Besitzer-Liste, verwaistes `wallTypeId`/Bauteil) bleiben ohne + * `fill`/`hatch` — `generateSectionPlan` fällt dafür auf die generische + * Schnitt-Schraffur zurück. + */ +function attachCutStyles(output: SectionOutput, project: Project): void { + if (output.cutPolygons.length === 0) return; + const walls = wallSegmentOwners(project); + const slabs = slabSegmentOwners(project); + for (const cp of output.cutPolygons) { + const ref = cp.component; + let style: SectionCutStyle | null = null; + if (ref.kind === "wall") { + const owner = walls[ref.index]; + if (owner) style = resolveWallSectionStyle(project, owner); + } else { + const owner = slabs[ref.index]; + if (owner) style = resolveCeilingSectionStyle(project, owner); + } + if (style) { + cp.fill = style.fill; + cp.hatch = style.hatch; + } + } +} + /** * Berechnet den Schnitt/die Ansicht der Ebene `level` gegen das Modell `project` * über den WASM-Extraktor. Asynchron (das render3d-WASM-Modul wird lazy geladen). @@ -133,5 +257,7 @@ export async function computeSection( plane.normal[1], plane.normal[2], ); - return JSON.parse(json) as SectionOutput; + const output = JSON.parse(json) as SectionOutput; + attachCutStyles(output, project); + return output; }