3D: Geschossdecken als extrudierte Polygone + hemisphärisches Licht

Deckenplatten (SlabInput) werden im render3d aus dem Grundriss-Umriss per
Ear-Clipping trianguliert und über die Deckendicke extrudiert (Deckel/Boden/
Mantel mit robust nach außen orientierten Normalen). Payload erweitert auf
{ walls, slabs } — blanke Wand-Arrays bleiben kompatibel. Beleuchtung auf
hemisphärisches Ambient (Himmel/Boden) + Directional-Sonne umgestellt, dezente
Kantenbetonung, hellerer Hintergrund (#f5f5f5). Beispiel-Geschossdecke im EG.
This commit is contained in:
2026-07-02 21:08:34 +02:00
parent 0189eed5ac
commit 31ac91b2a7
9 changed files with 532 additions and 82 deletions
+19 -2
View File
@@ -200,8 +200,25 @@ export const sampleProject: Project = {
sillHeight: 0.9,
},
],
// Decken (Slabs) — anfangs leer; werden mit dem Decken-Werkzeug gefüllt.
ceilings: [],
// Decken (Slabs) — Geschossdecke über dem EG (= Boden des OG). Umriss = EG-
// Grundriss (5 × 4 m). Kategorie „30 Decken", 25 cm dick, OK bündig mit dem
// EG-Wandkopf (baseElevation + floorHeight = 2.6 m).
ceilings: [
{
id: "C1",
type: "ceiling",
floorId: "eg",
categoryCode: "30",
outline: [
{ x: 0, y: 0 },
{ x: 5, y: 0 },
{ x: 5, y: 4 },
{ x: 0, y: 4 },
],
wallTypeId: "aw",
thickness: 0.25,
},
],
// Treppen — eine gerade Beispieltreppe im EG entlang der Ostwand, steigt ins OG
// (totalRise = Geschosshöhe). Kategorie „40 Treppen".
stairs: [
+2 -2
View File
@@ -12,7 +12,7 @@
import type { Plan } from "./generatePlan";
import type { Project } from "../model/types";
import { planToRenderScene } from "./toRenderScene";
import { projectToWalls3d } from "./toWalls3d";
import { projectToModel3d } from "./toWalls3d";
/** Sammel-Fenster: Modelländerungen werden gebündelt gepusht (nicht je Frame). */
const DEBOUNCE_MS = 200;
@@ -58,6 +58,6 @@ export function pushNativeWalls(project: Project): void {
if (!isTauri()) return;
clearTimeout(wallsTimer);
wallsTimer = setTimeout(() => {
void invokeNative("push_native_walls", { walls: projectToWalls3d(project) });
void invokeNative("push_native_walls", { walls: projectToModel3d(project) });
}, DEBOUNCE_MS);
}
+27 -15
View File
@@ -18,7 +18,7 @@
// Polygon-Platten (SlabInput) mitgegeben — render3d trianguliert den Umriss und
// zieht ihn über die Deckendicke hoch.
import type { Project, Wall, Opening } from "../model/types";
import type { Project, Wall } from "../model/types";
import { getWallType, wallTypeThickness, openingsOfWall } from "../model/types";
import { wallVerticalExtent, ceilingVerticalExtent } from "../model/wall";
import { openingInterval, openingVerticalExtent } from "../geometry/opening";
@@ -104,33 +104,45 @@ function emitWall(out: RWall[], project: Project, wall: Wall): void {
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) {
// Aussparungen der Wand als vereinheitlichte Cutouts sammeln:
// • Öffnungen (project.openings, Fenster mit Brüstung/Sturz),
// • Legacy-Türen (project.doors, sitzen am Boden, nur Sturz).
// Jeweils [from..to] auf die Achse geklemmt + vertikale Öffnungs-Ausdehnung.
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) items.push({ from: iv.from, to: iv.to, op });
if (!iv) continue;
const v = openingVerticalExtent(project, wall, op);
cutouts.push({ from: iv.from, to: iv.to, oBottom: v.zBottom, oTop: v.zTop });
}
items.sort((a, b) => a.from - b.from);
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 < EPS) continue;
// Tür sitzt am Boden (UK der Wand), reicht bis zur lichten Türhöhe.
const oTop = Math.min(zTop, zBottom + d.height);
cutouts.push({ from, to, oBottom: zBottom, oTop });
}
cutouts.sort((a, b) => a.from - b.from);
// Ohne Öffnungen: ein durchgehender Quader (wie bisher).
if (items.length === 0) {
// Ohne Aussparungen: ein durchgehender Quader (wie bisher).
if (cutouts.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.
// Wand entlang der Achse durchlaufen: volle Pfeiler zwischen den Aussparungen,
// Brüstung/Sturz im Öffnungsbereich. Überlappende Cutouts werden über `cursor`
// zusammengefasst.
let cursor = 0;
for (const { from, to, op } of items) {
for (const { from, to, oBottom, oTop } of cutouts) {
const segFrom = Math.max(cursor, from);
if (from > cursor) {
// Voller Wandpfeiler bis zur Öffnung.
// Voller Wandpfeiler bis zur Aussparung.
pushSegment(out, wall, cursor, from, zBottom, zTop, thickness);
}
if (to > segFrom) {
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);