render3d: Wandverschneidung an Ecken + sichtbare Schichten
mesh.rs berechnet die Ecken-Verschneidung jetzt selbst (portiert aus dem
Web-Kern computeJoins/clippedBand): Wandenden werden per Rundungs-Key
gruppiert, ueber Hoehen-Ueberlappung geclustert (gestapelte Geschosse mit
gleichem Grundriss verschneiden sich NICHT), und Zweier-Cluster ueber die
gemeinsame Gehrungslinie geschnitten. T-/X-Stoesse und freie Enden bleiben
stumpf, wie im Web-Kern. Endkappen-Normalen geometrisch aus dem Kreuzprodukt.
WallInput bekommt optional layers (WallLayer{thickness,color}, serde-default,
rueckwaertskompatibel): jede Schicht wird als eigenes Band ueber dieselbe
gehrte Grundflaeche extrudiert und materialgefaerbt. toWalls3d.ts loest die
WallType-Schichten je Wand auf (hexToRgb) und reicht sie durch.
39/39 Rust-Tests (7 neue: Gehrung, T-Stoss, freies Ende, Schichten, Kombi).
This commit is contained in:
+47
-7
@@ -17,15 +17,29 @@
|
||||
// DECKEN (Slabs): geschossgebundene Flächenbauteile werden als extrudierte
|
||||
// Polygon-Platten (SlabInput) mitgegeben — render3d trianguliert den Umriss und
|
||||
// zieht ihn über die Deckendicke hoch.
|
||||
//
|
||||
// SCHICHTEN (Putz/Dämmung/Mauerwerk/…): zusätzlich zur Gesamtdicke wird der
|
||||
// aufgelöste Schichtaufbau des WallType mitgegeben (`layers`) — je Schicht
|
||||
// Dicke + 3D-Farbe des zugehörigen Bauteil-Materials (Component). render3d
|
||||
// extrudiert daraus je Schicht ein eigenes Band quer zur Wanddicke (siehe
|
||||
// render3d::mesh); die Eck-Gehrung (Wandknoten-Verschneidung) berechnet
|
||||
// render3d selbständig aus der Wandkonnektivität, braucht also keine
|
||||
// zusätzlichen Felder von hier.
|
||||
|
||||
import type { Project, Wall } from "../model/types";
|
||||
import { getWallType, wallTypeThickness, openingsOfWall } from "../model/types";
|
||||
import { getComponent, getWallType, wallTypeThickness, openingsOfWall } from "../model/types";
|
||||
import { wallVerticalExtent, ceilingVerticalExtent } from "../model/wall";
|
||||
import { openingInterval, openingVerticalExtent } from "../geometry/opening";
|
||||
|
||||
export type RVec2 = [number, number];
|
||||
export type RRgb = [number, number, number];
|
||||
|
||||
/** Eine aufgelöste Wandschicht: Dicke + 3D-Albedo-Farbe (aus dem Component). */
|
||||
export interface RLayer {
|
||||
thickness: number;
|
||||
color: RRgb;
|
||||
}
|
||||
|
||||
export interface RWall {
|
||||
start: RVec2;
|
||||
end: RVec2;
|
||||
@@ -33,6 +47,8 @@ export interface RWall {
|
||||
height: number;
|
||||
baseElevation: number;
|
||||
color: RRgb;
|
||||
/** Aufgelöster Schichtaufbau (leer = kein WallType auflösbar, siehe emitWall). */
|
||||
layers: RLayer[];
|
||||
}
|
||||
|
||||
/** Eine extrudierte Deckenplatte: geschlossener Grundriss-Umriss + Z-Ausdehnung. */
|
||||
@@ -57,6 +73,18 @@ const SLAB_RGB: RRgb = [0.86, 0.86, 0.88];
|
||||
/** Ignoriere entartete vertikale Ausschnitte (Rundungsrauschen). */
|
||||
const EPS = 1e-4;
|
||||
|
||||
/**
|
||||
* Wandelt eine Hex-Farbe ("#rrggbb", Component-Farbe im Plan) in float-RGB
|
||||
* (0..1, render3d-Konvention) um. Ungültiges/fehlendes Format fällt auf den
|
||||
* neutralen Wand-Grundton zurück (nie ein hartes Fehlbild).
|
||||
*/
|
||||
function hexToRgb(hex: string): RRgb {
|
||||
const m = /^#?([0-9a-fA-F]{6})$/.exec(hex);
|
||||
if (!m) return WALL_RGB;
|
||||
const v = parseInt(m[1], 16);
|
||||
return [((v >> 16) & 0xff) / 255, ((v >> 8) & 0xff) / 255, (v & 0xff) / 255];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hängt einen Wand-Teilquader an: das Achsenstück [from..to] (Meter ab
|
||||
* Wand-Startpunkt) über den Höhenbereich [zBottom..zTop]. Entartete Stücke
|
||||
@@ -71,6 +99,7 @@ function pushSegment(
|
||||
zBottom: number,
|
||||
zTop: number,
|
||||
thickness: number,
|
||||
layers: RLayer[],
|
||||
): void {
|
||||
if (to - from <= EPS) return;
|
||||
if (zTop - zBottom <= EPS) return;
|
||||
@@ -89,16 +118,27 @@ function pushSegment(
|
||||
height: zTop - zBottom,
|
||||
baseElevation: zBottom,
|
||||
color: WALL_RGB,
|
||||
layers,
|
||||
});
|
||||
}
|
||||
|
||||
/** Emittiert die Teilquader EINER Wand (mit oder ohne Öffnungen). */
|
||||
function emitWall(out: RWall[], project: Project, wall: Wall): void {
|
||||
let thickness = 0.2;
|
||||
// Aufgelöster Schichtaufbau (Dicke + 3D-Farbe je Schicht des WallType) —
|
||||
// leer, wenn kein WallType auflösbar ist (render3d extrudiert dann wie
|
||||
// bisher einen Vollkörper aus `thickness`/`color`, siehe RWall-Moduldoc).
|
||||
let layers: RLayer[] = [];
|
||||
try {
|
||||
thickness = wallTypeThickness(getWallType(project, wall));
|
||||
const wt = getWallType(project, wall);
|
||||
thickness = wallTypeThickness(wt);
|
||||
layers = wt.layers.map((l) => ({
|
||||
thickness: l.thickness,
|
||||
color: hexToRgb(getComponent(project, l.componentId).color),
|
||||
}));
|
||||
} catch {
|
||||
thickness = 0.2;
|
||||
layers = [];
|
||||
}
|
||||
const { zBottom, zTop } = wallVerticalExtent(project, wall);
|
||||
const axisLen = Math.hypot(wall.end.x - wall.start.x, wall.end.y - wall.start.y);
|
||||
@@ -128,7 +168,7 @@ function emitWall(out: RWall[], project: Project, wall: Wall): void {
|
||||
|
||||
// Ohne Aussparungen: ein durchgehender Quader (wie bisher).
|
||||
if (cutouts.length === 0) {
|
||||
pushSegment(out, wall, 0, axisLen, zBottom, zTop, thickness);
|
||||
pushSegment(out, wall, 0, axisLen, zBottom, zTop, thickness, layers);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -140,23 +180,23 @@ function emitWall(out: RWall[], project: Project, wall: Wall): void {
|
||||
const segFrom = Math.max(cursor, from);
|
||||
if (from > cursor) {
|
||||
// Voller Wandpfeiler bis zur Aussparung.
|
||||
pushSegment(out, wall, cursor, from, zBottom, zTop, thickness);
|
||||
pushSegment(out, wall, cursor, from, zBottom, zTop, thickness, layers);
|
||||
}
|
||||
if (to > segFrom) {
|
||||
// Brüstung unter der Öffnung (bei Türen entfällt sie, da oBottom == zBottom).
|
||||
if (oBottom > zBottom + EPS) {
|
||||
pushSegment(out, wall, segFrom, to, zBottom, oBottom, thickness);
|
||||
pushSegment(out, wall, segFrom, to, zBottom, oBottom, thickness, layers);
|
||||
}
|
||||
// Sturz über der Öffnung (bis zum Wandkopf).
|
||||
if (oTop < zTop - EPS) {
|
||||
pushSegment(out, wall, segFrom, to, oTop, zTop, thickness);
|
||||
pushSegment(out, wall, segFrom, to, oTop, zTop, thickness, layers);
|
||||
}
|
||||
}
|
||||
cursor = Math.max(cursor, to);
|
||||
}
|
||||
// Restlicher Wandpfeiler bis zum Achsenende.
|
||||
if (cursor < axisLen) {
|
||||
pushSegment(out, wall, cursor, axisLen, zBottom, zTop, thickness);
|
||||
pushSegment(out, wall, cursor, axisLen, zBottom, zTop, thickness, layers);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user