Schnitt: materialspezifische SIA-Schraffuren wiederhergestellt

de7a26b lieferte projectToModel3d pro SCHICHT eine eigene Voll-Box; der
Schnitt bekam damit segments*layers Cut-Polygone, waehrend
wallSegmentOwners weiter nur oeffnungsbasiert segmentiert -> Index-
Versatz, falscher Owner, splitWallLayers zerteilte bereits einschichtige
Baender erneut -> einheitliches Diagonal-Muster statt SIA-Muster.

Fix: Model3dOptions { layeredWalls } an projectToModel3d (Default true,
3D-Viewer unveraendert); computeSection ruft mit layeredWalls:false ->
eine Voll-Box pro Wand, Owner-Mapping wieder 1:1, splitWallLayers
erzeugt die per-Material-Baender (Daemmung-Haarlinien, Backstein-
Diagonale, Beton-Kreuz) wie im Grundriss. 216 Tests gruen.
This commit is contained in:
2026-07-04 14:44:36 +02:00
parent e095b51d15
commit 20cf870c4c
2 changed files with 40 additions and 11 deletions
+10 -4
View File
@@ -5,9 +5,11 @@
// 1. Aus einer Zeichnungsebene (DrawingLevel, kind "section"/"elevation") wird // 1. Aus einer Zeichnungsebene (DrawingLevel, kind "section"/"elevation") wird
// eine Schnittebene abgeleitet — die Grundriss-Schnittlinie `linePoints` // eine Schnittebene abgeleitet — die Grundriss-Schnittlinie `linePoints`
// plus `directionSign` (Blickrichtung). // plus `directionSign` (Blickrichtung).
// 2. Das Projekt wird über `projectToModel3d` (dieselbe Wand-/Decken-Zerlegung // 2. Das Projekt wird über `projectToModel3d({ layeredWalls: false })` zu
// wie der 3D-Viewport) zu Wänden + Decken geflacht und als JSON an // Wänden + Decken geflacht und als JSON an `cut_section_json` gereicht —
// `cut_section_json` gereicht. // pro Wand EINE Voll-Box (der 3D-Viewport bekommt dagegen je Materiallage
// eine Box). So bleiben die Cut-Polygone 1:1 zu `wallSegmentOwners`, und
// die per-Material-Schnitt-Schraffur macht `splitWallLayers` TS-seitig.
// 3. Das zurückgegebene `SectionOutput` (Cut-Polygone + sichtbare/verdeckte // 3. Das zurückgegebene `SectionOutput` (Cut-Polygone + sichtbare/verdeckte
// Kanten, alles in (u, v)-Metern) wird in `generateSectionPlan` // Kanten, alles in (u, v)-Metern) wird in `generateSectionPlan`
// (generatePlan.ts) zu Plan-Primitiven übersetzt. // (generatePlan.ts) zu Plan-Primitiven übersetzt.
@@ -624,7 +626,11 @@ export async function computeSection(
"render3d-WASM ohne cut_section_json — pkg3d neu bauen (npm run build:engine3d)", "render3d-WASM ohne cut_section_json — pkg3d neu bauen (npm run build:engine3d)",
); );
} }
const modelJson = JSON.stringify(projectToModel3d(project)); // Schnitt-Eingabe: EINE Voll-Box pro Wand (layeredWalls:false), damit die
// Cut-Polygone 1:1 zu `wallSegmentOwners` (Öffnungs-Segmentierung) liegen und
// `splitWallLayers` die materialspezifischen Schnitt-Bänder korrekt aus dem
// EINEN Wandrechteck erzeugt. Der 3D-Viewer bekommt weiter per-Schicht-Boxen.
const modelJson = JSON.stringify(projectToModel3d(project, { layeredWalls: false }));
const json = mod.cut_section_json( const json = mod.cut_section_json(
modelJson, modelJson,
plane.point[0], plane.point[0],
+30 -7
View File
@@ -185,13 +185,21 @@ interface WallBand {
* WallType / keine Lagen ⇒ EIN Vollkörper-Band über die volle Dicke im * WallType / keine Lagen ⇒ EIN Vollkörper-Band über die volle Dicke im
* neutralen Grundton (heutiges Fallback-Verhalten). * neutralen Grundton (heutiges Fallback-Verhalten).
*/ */
function resolveWallBands(project: Project, wall: Wall): WallBand[] { function resolveWallBands(project: Project, wall: Wall, layered = true): WallBand[] {
try { try {
const wt = getWallType(project, wall); const wt = getWallType(project, wall);
if (wt.layers.length > 0) { if (wt.layers.length > 0) {
// Gesamtdicke = Summe der Lagen (konsistent mit den Offsets); wenn diese // Gesamtdicke = Summe der Lagen (konsistent mit den Offsets); wenn diese
// von wallTypeThickness abwiche, gewinnt die Lagen-Summe (keine Lücken). // von wallTypeThickness abwiche, gewinnt die Lagen-Summe (keine Lücken).
const total = wt.layers.reduce((s, l) => s + l.thickness, 0); const total = wt.layers.reduce((s, l) => s + l.thickness, 0);
// Schnitt-Eingabe (layered=false): EINE Voll-Box über die ganze Dicke statt
// je Lage eine Box. So bleibt die Emission 1:1 Wand→Rechteck — passend zu
// `wallSegmentOwners`/`splitWallLayers` in toSection.ts, die die Schicht-
// Zerlegung TS-seitig aus diesem einen Rechteck machen. (Der 3D-Viewer ruft
// mit Default layered=true auf und bekommt unverändert die Schicht-Bänder.)
if (!layered) {
return [{ thickness: total, color: dominantLayerColor(wt.layers, project, WALL_RGB), offset: 0 }];
}
const bands: WallBand[] = []; const bands: WallBand[] = [];
let prefix = 0; let prefix = 0;
for (const l of wt.layers) { for (const l of wt.layers) {
@@ -415,8 +423,8 @@ function trimWallTopForCeilings(
} }
/** Emittiert die Teilquader EINER Wand (mit oder ohne Öffnungen). */ /** Emittiert die Teilquader EINER Wand (mit oder ohne Öffnungen). */
function emitWall(out: RWall[], project: Project, wall: Wall): void { function emitWall(out: RWall[], project: Project, wall: Wall, layered = true): void {
const bands = resolveWallBands(project, wall); const bands = resolveWallBands(project, wall, layered);
const { zBottom, zTop: rawZTop } = wallVerticalExtent(project, wall); const { zBottom, zTop: rawZTop } = wallVerticalExtent(project, wall);
const zTop = trimWallTopForCeilings(project, wall, zBottom, rawZTop); const zTop = trimWallTopForCeilings(project, wall, zBottom, rawZTop);
const axisLen = Math.hypot(wall.end.x - wall.start.x, wall.end.y - wall.start.y); const axisLen = Math.hypot(wall.end.x - wall.start.x, wall.end.y - wall.start.y);
@@ -608,16 +616,31 @@ function emitOpeningGlass(project: Project): RMesh[] {
* Nur die Wände (Rückwärts-Kompatibilität / bestehende Aufrufer). Zerlegt Wände * Nur die Wände (Rückwärts-Kompatibilität / bestehende Aufrufer). Zerlegt Wände
* mit Öffnungen in Teilquader. * mit Öffnungen in Teilquader.
*/ */
export function projectToWalls3d(project: Project): RWall[] { export function projectToWalls3d(project: Project, layered = true): RWall[] {
const out: RWall[] = []; const out: RWall[] = [];
for (const w of project.walls) emitWall(out, project, w); for (const w of project.walls) emitWall(out, project, w, layered);
return out; return out;
} }
/**
* Optionen für {@link projectToModel3d}. `layeredWalls` steuert, ob eine
* mehrschichtige Wand als eigene Box je Materiallage (Default, 3D-Viewer) oder
* als EINE Voll-Box über die ganze Dicke emittiert wird. Der Schnitt-Pfad
* (`computeSection`) setzt `false`, damit die Cut-Polygone 1:1 zur Wand liegen
* (die Schicht-Zerlegung macht dort `splitWallLayers` TS-seitig) — sonst
* verschöben die per-Lage vervielfachten Boxen die `ComponentRef.index`
* gegenüber `wallSegmentOwners` und die materialspezifische Schnitt-Schraffur
* ginge verloren.
*/
export interface Model3dOptions {
layeredWalls?: boolean;
}
/** Das volle 3D-Modell: Wände (mit Öffnungen) + Deckenplatten + Kontext-Meshes. */ /** Das volle 3D-Modell: Wände (mit Öffnungen) + Deckenplatten + Kontext-Meshes. */
export function projectToModel3d(project: Project): RModel3d { export function projectToModel3d(project: Project, opts: Model3dOptions = {}): RModel3d {
const layered = opts.layeredWalls ?? true;
return { return {
walls: projectToWalls3d(project), walls: projectToWalls3d(project, layered),
slabs: emitSlabs(project), slabs: emitSlabs(project),
// Kontext-Meshes (Terrain/Import) + Fenster-Glasscheiben (eigenes Mesh je // Kontext-Meshes (Terrain/Import) + Fenster-Glasscheiben (eigenes Mesh je
// Fensteröffnung) — pickGeometry/Highlight nutzen `meshes` bewusst NICHT, // Fensteröffnung) — pickGeometry/Highlight nutzen `meshes` bewusst NICHT,