3D-Wände mit Öffnungen: Türen/Fenster als Teilquader (Pfeiler+Brüstung+Sturz)
This commit is contained in:
@@ -50,8 +50,9 @@ export function pushNativeScene(plan: Plan): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debounced: die Wände des Projekts (alle Geschosse, gestapelt) als
|
* Debounced: das 3D-Modell des Projekts (alle Geschosse, gestapelt) — Wände mit
|
||||||
* render3d-WallInputs ans native 3D-Fenster schieben. No-op ohne Tauri.
|
* Öffnungs-Teilquadern plus Deckenplatten — ans native 3D-Fenster schieben.
|
||||||
|
* No-op ohne Tauri.
|
||||||
*/
|
*/
|
||||||
export function pushNativeWalls(project: Project): void {
|
export function pushNativeWalls(project: Project): void {
|
||||||
if (!isTauri()) return;
|
if (!isTauri()) return;
|
||||||
|
|||||||
+152
-19
@@ -6,10 +6,22 @@
|
|||||||
// Alle Geschosse werden einbezogen und über wallVerticalExtent korrekt
|
// Alle Geschosse werden einbezogen und über wallVerticalExtent korrekt
|
||||||
// gestapelt (EG baseElevation 0, OG 2.6 …), sodass das 3D-Fenster das ganze
|
// gestapelt (EG baseElevation 0, OG 2.6 …), sodass das 3D-Fenster das ganze
|
||||||
// Gebäude zeigt.
|
// Gebäude zeigt.
|
||||||
|
//
|
||||||
|
// ÖFFNUNGEN (Türen/Fenster): eine Wand mit Öffnungen wird NICHT als ein Quader
|
||||||
|
// emittiert, sondern in Teilquader entlang der Achse zerlegt:
|
||||||
|
// • volle Wandhöhe links/rechts der Öffnung (Pfeiler),
|
||||||
|
// • unter dem Fenster die Brüstung (UK..Sill),
|
||||||
|
// • über Tür/Fenster der Sturz (OpeningTop..Wandkopf).
|
||||||
|
// So entstehen sichtbare Aussparungen ohne dass render3d etwas davon wissen muss.
|
||||||
|
//
|
||||||
|
// DECKEN (Slabs): geschossgebundene Flächenbauteile werden als extrudierte
|
||||||
|
// Polygon-Platten (SlabInput) mitgegeben — render3d trianguliert den Umriss und
|
||||||
|
// zieht ihn über die Deckendicke hoch.
|
||||||
|
|
||||||
import type { Project } from "../model/types";
|
import type { Project, Wall, Opening } from "../model/types";
|
||||||
import { getWallType, wallTypeThickness } from "../model/types";
|
import { getWallType, wallTypeThickness, openingsOfWall } from "../model/types";
|
||||||
import { wallVerticalExtent } from "../model/wall";
|
import { wallVerticalExtent, ceilingVerticalExtent } from "../model/wall";
|
||||||
|
import { openingInterval, openingVerticalExtent } from "../geometry/opening";
|
||||||
|
|
||||||
export type RVec2 = [number, number];
|
export type RVec2 = [number, number];
|
||||||
export type RRgb = [number, number, number];
|
export type RRgb = [number, number, number];
|
||||||
@@ -23,28 +35,149 @@ export interface RWall {
|
|||||||
color: RRgb;
|
color: RRgb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Eine extrudierte Deckenplatte: geschlossener Grundriss-Umriss + Z-Ausdehnung. */
|
||||||
|
export interface RSlab {
|
||||||
|
outline: RVec2[];
|
||||||
|
zBottom: number;
|
||||||
|
zTop: number;
|
||||||
|
color: RRgb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Modell-Bündel für den nativen 3D-Renderer (Wände + Decken). */
|
||||||
|
export interface RModel3d {
|
||||||
|
walls: RWall[];
|
||||||
|
slabs: RSlab[];
|
||||||
|
}
|
||||||
|
|
||||||
/** Warmer, neutraler Wand-Grundton (wie render3d default). */
|
/** Warmer, neutraler Wand-Grundton (wie render3d default). */
|
||||||
const WALL_RGB: RRgb = [0.82, 0.8, 0.76];
|
const WALL_RGB: RRgb = [0.82, 0.8, 0.76];
|
||||||
|
/** Etwas hellerer, kühlerer Ton für Deckenplatten (heben sich von Wänden ab). */
|
||||||
|
const SLAB_RGB: RRgb = [0.86, 0.86, 0.88];
|
||||||
|
|
||||||
export function projectToWalls3d(project: Project): RWall[] {
|
/** Ignoriere entartete vertikale Ausschnitte (Rundungsrauschen). */
|
||||||
const out: RWall[] = [];
|
const EPS = 1e-4;
|
||||||
for (const w of project.walls) {
|
|
||||||
let thickness = 0.2;
|
/**
|
||||||
try {
|
* Hängt einen Wand-Teilquader an: das Achsenstück [from..to] (Meter ab
|
||||||
thickness = wallTypeThickness(getWallType(project, w));
|
* Wand-Startpunkt) über den Höhenbereich [zBottom..zTop]. Entartete Stücke
|
||||||
} catch {
|
* (Länge ≤ 0 oder Höhe ≤ 0) werden übersprungen — so überleben Öffnungen dicht
|
||||||
thickness = 0.2;
|
* an den Wandenden ohne Nullquader.
|
||||||
|
*/
|
||||||
|
function pushSegment(
|
||||||
|
out: RWall[],
|
||||||
|
wall: Wall,
|
||||||
|
from: number,
|
||||||
|
to: number,
|
||||||
|
zBottom: number,
|
||||||
|
zTop: number,
|
||||||
|
thickness: number,
|
||||||
|
): void {
|
||||||
|
if (to - from <= EPS) return;
|
||||||
|
if (zTop - zBottom <= EPS) return;
|
||||||
|
const dx = wall.end.x - wall.start.x;
|
||||||
|
const dy = wall.end.y - wall.start.y;
|
||||||
|
const len = Math.hypot(dx, dy);
|
||||||
|
if (len < 1e-9) return;
|
||||||
|
const ux = dx / len;
|
||||||
|
const uy = dy / len;
|
||||||
|
const p1: RVec2 = [wall.start.x + ux * from, wall.start.y + uy * from];
|
||||||
|
const p2: RVec2 = [wall.start.x + ux * to, wall.start.y + uy * to];
|
||||||
|
out.push({
|
||||||
|
start: p1,
|
||||||
|
end: p2,
|
||||||
|
thickness,
|
||||||
|
height: zTop - zBottom,
|
||||||
|
baseElevation: zBottom,
|
||||||
|
color: WALL_RGB,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Emittiert die Teilquader EINER Wand (mit oder ohne Öffnungen). */
|
||||||
|
function emitWall(out: RWall[], project: Project, wall: Wall): void {
|
||||||
|
let thickness = 0.2;
|
||||||
|
try {
|
||||||
|
thickness = wallTypeThickness(getWallType(project, wall));
|
||||||
|
} catch {
|
||||||
|
thickness = 0.2;
|
||||||
|
}
|
||||||
|
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 <= EPS) return;
|
||||||
|
|
||||||
|
// Öffnungen der Wand als [from..to]-Intervalle (auf die Achse geklemmt),
|
||||||
|
// nach Startlage sortiert.
|
||||||
|
const openings = openingsOfWall(project, wall.id);
|
||||||
|
const items: Array<{ from: number; to: number; op: Opening }> = [];
|
||||||
|
for (const op of openings) {
|
||||||
|
const iv = openingInterval(wall, op);
|
||||||
|
if (iv) items.push({ from: iv.from, to: iv.to, op });
|
||||||
|
}
|
||||||
|
items.sort((a, b) => a.from - b.from);
|
||||||
|
|
||||||
|
// Ohne Öffnungen: ein durchgehender Quader (wie bisher).
|
||||||
|
if (items.length === 0) {
|
||||||
|
pushSegment(out, wall, 0, axisLen, zBottom, zTop, thickness);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wand entlang der Achse durchlaufen: volle Pfeiler zwischen den Öffnungen,
|
||||||
|
// Brüstung/Sturz im Öffnungsbereich.
|
||||||
|
let cursor = 0;
|
||||||
|
for (const { from, to, op } of items) {
|
||||||
|
const segFrom = Math.max(cursor, from);
|
||||||
|
if (from > cursor) {
|
||||||
|
// Voller Wandpfeiler bis zur Öffnung.
|
||||||
|
pushSegment(out, wall, cursor, from, zBottom, zTop, thickness);
|
||||||
}
|
}
|
||||||
const { zBottom, zTop } = wallVerticalExtent(project, w);
|
if (to > segFrom) {
|
||||||
const height = Math.max(0.01, zTop - zBottom);
|
const { zBottom: oBottom, zTop: oTop } = openingVerticalExtent(project, wall, op);
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
// Sturz über der Öffnung (bis zum Wandkopf).
|
||||||
|
if (oTop < zTop - EPS) {
|
||||||
|
pushSegment(out, wall, segFrom, to, oTop, zTop, thickness);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor = Math.max(cursor, to);
|
||||||
|
}
|
||||||
|
// Restlicher Wandpfeiler bis zum Achsenende.
|
||||||
|
if (cursor < axisLen) {
|
||||||
|
pushSegment(out, wall, cursor, axisLen, zBottom, zTop, thickness);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Emittiert die Deckenplatten eines Projekts (falls vorhanden). */
|
||||||
|
function emitSlabs(project: Project): RSlab[] {
|
||||||
|
const out: RSlab[] = [];
|
||||||
|
for (const c of project.ceilings ?? []) {
|
||||||
|
if (!c.outline || c.outline.length < 3) continue;
|
||||||
|
// zBottom/zTop (und damit die Deckendicke) berechnet ceilingVerticalExtent
|
||||||
|
// bereits über ceilingThickness (respektiert Typ-Dicke + Übersteuerung).
|
||||||
|
const { zBottom, zTop } = ceilingVerticalExtent(project, c);
|
||||||
|
if (zTop - zBottom <= EPS) continue;
|
||||||
out.push({
|
out.push({
|
||||||
start: [w.start.x, w.start.y],
|
outline: c.outline.map((p) => [p.x, p.y] as RVec2),
|
||||||
end: [w.end.x, w.end.y],
|
zBottom,
|
||||||
thickness,
|
zTop,
|
||||||
height,
|
color: SLAB_RGB,
|
||||||
baseElevation: zBottom,
|
|
||||||
color: WALL_RGB,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nur die Wände (Rückwärts-Kompatibilität / bestehende Aufrufer). Zerlegt Wände
|
||||||
|
* mit Öffnungen in Teilquader.
|
||||||
|
*/
|
||||||
|
export function projectToWalls3d(project: Project): RWall[] {
|
||||||
|
const out: RWall[] = [];
|
||||||
|
for (const w of project.walls) emitWall(out, project, w);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Das volle 3D-Modell: Wände (mit Öffnungen) + Deckenplatten. */
|
||||||
|
export function projectToModel3d(project: Project): RModel3d {
|
||||||
|
return { walls: projectToWalls3d(project), slabs: emitSlabs(project) };
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user