Dach-Schichtlogik: RoofType/Layer[] wie Decken (3D mehrschichtig)
Kernthema der BIM-Tiefe (Designdoc docs/design/bim-elements-depth-study.md, P0):
Dächer bekommen einen mehrschichtigen Aufbau analog WallType/CeilingType.
- Modell: RoofType { layers: Layer[] }, Project.roofTypes, Roof.roofTypeId
(Roof.thickness bleibt einschichtiger Fallback). Resolver roofLayers()/
roofTotalThickness() — wiederverwendet den bestehenden Layer-Typ.
- 3D (emitRoofs): je Schicht ein eigenes Mesh mit Bauteilfarbe, entlang der
Flächennormalen unter die vorige gestapelt (Eindeckung aussen → Verkleidung
innen). Ohne roofTypeId einschichtiger thickness-Slab (Default-Dachfarbe).
- UI: Dachtyp-Dropdown im Dach-Panel (leer = einschichtig). Default-Dachtyp
'Steildach gedämmt' in sampleProject.
- +2 Tests (Schicht-Meshes/Stapelung; einschichtig = Slab+Giebel).
Offen (Designdoc): 2D-Schnitt-Poché der Dachschichten, RoofType-ResourceManager-
Tab, Traufe/Ortgang getrennt. 668/668 grün; 3D-Optik vom Nutzer in Tauri prüfen.
This commit is contained in:
@@ -300,6 +300,8 @@ export const de = {
|
|||||||
"objinfo.roof.ridgeAxis.y": "entlang Y",
|
"objinfo.roof.ridgeAxis.y": "entlang Y",
|
||||||
"objinfo.roof.pitch": "Neigung (°)",
|
"objinfo.roof.pitch": "Neigung (°)",
|
||||||
"objinfo.roof.pitchUpper": "Obere Neigung (°)",
|
"objinfo.roof.pitchUpper": "Obere Neigung (°)",
|
||||||
|
"objinfo.roof.type": "Dachtyp",
|
||||||
|
"objinfo.roof.type.single": "Einschichtig (Dicke)",
|
||||||
"objinfo.roof.mansardType": "Mansard-Art",
|
"objinfo.roof.mansardType": "Mansard-Art",
|
||||||
"objinfo.roof.mansard.giebel": "Giebel-Mansarde",
|
"objinfo.roof.mansard.giebel": "Giebel-Mansarde",
|
||||||
"objinfo.roof.mansard.walm": "Walm-Mansarde",
|
"objinfo.roof.mansard.walm": "Walm-Mansarde",
|
||||||
|
|||||||
@@ -299,6 +299,8 @@ export const en: Record<TranslationKey, string> = {
|
|||||||
"objinfo.roof.ridgeAxis.y": "along Y",
|
"objinfo.roof.ridgeAxis.y": "along Y",
|
||||||
"objinfo.roof.pitch": "Pitch (°)",
|
"objinfo.roof.pitch": "Pitch (°)",
|
||||||
"objinfo.roof.pitchUpper": "Upper pitch (°)",
|
"objinfo.roof.pitchUpper": "Upper pitch (°)",
|
||||||
|
"objinfo.roof.type": "Roof type",
|
||||||
|
"objinfo.roof.type.single": "Single layer (thickness)",
|
||||||
"objinfo.roof.mansardType": "Mansard type",
|
"objinfo.roof.mansardType": "Mansard type",
|
||||||
"objinfo.roof.mansard.giebel": "Gable mansard",
|
"objinfo.roof.mansard.giebel": "Gable mansard",
|
||||||
"objinfo.roof.mansard.walm": "Hip mansard",
|
"objinfo.roof.mansard.walm": "Hip mansard",
|
||||||
|
|||||||
@@ -202,6 +202,18 @@ export const sampleProject: Project = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
roofTypes: [
|
||||||
|
{
|
||||||
|
id: "roof-warm",
|
||||||
|
name: "Steildach gedämmt",
|
||||||
|
// Aussen (Eindeckung) → innen (Verkleidung); Bauteile aus der Bibliothek.
|
||||||
|
layers: [
|
||||||
|
{ componentId: "brick", thickness: 0.04 }, // Eindeckung (Ziegel, Platzhalter)
|
||||||
|
{ componentId: "insulation", thickness: 0.22, jointLineStyleId: "thin" }, // Sparren/Dämmung
|
||||||
|
{ componentId: "render-int", thickness: 0.0125 }, // Innenverkleidung (GK)
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
// Bauteil-Typen (Bibliothek) — Tür/Fenster/Treppe, analog Wand-/Deckenstile.
|
// Bauteil-Typen (Bibliothek) — Tür/Fenster/Treppe, analog Wand-/Deckenstile.
|
||||||
// Elemente referenzieren sie per `typeId`; ohne Referenz gilt Inline-Verhalten.
|
// Elemente referenzieren sie per `typeId`; ohne Referenz gilt Inline-Verhalten.
|
||||||
doorTypes: [
|
doorTypes: [
|
||||||
|
|||||||
+49
-1
@@ -276,6 +276,21 @@ export interface CeilingType {
|
|||||||
layers: Layer[];
|
layers: Layer[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein Dachtyp (Dachaufbau) = geordneter Schichtaufbau EINES Daches, senkrecht
|
||||||
|
* zur Dachfläche gestapelt (aussen/Eindeckung → innen/Verkleidung) — das
|
||||||
|
* geneigte Gegenstück zu `WallType`/`CeilingType`. Nutzt denselben `Layer`-Typ.
|
||||||
|
* Typische Folge: Eindeckung (Ziegel/Blech) → Konterlattung/Lattung → Unterdach
|
||||||
|
* → Sparren/Dämmung → Dampfbremse → Innenverkleidung. Ein Dach ohne `roofTypeId`
|
||||||
|
* fällt auf die einschichtige `Roof.thickness` zurück (rückwärtskompatibel).
|
||||||
|
*/
|
||||||
|
export interface RoofType {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
/** Aussen (Eindeckung) → innen (Verkleidung), analog `WallType.layers`. */
|
||||||
|
layers: Layer[];
|
||||||
|
}
|
||||||
|
|
||||||
// ── Bauteil-Typen: Tür / Fenster / Treppe ──────────────────────────────────
|
// ── Bauteil-Typen: Tür / Fenster / Treppe ──────────────────────────────────
|
||||||
// Wiederverwendbare Stile (Presets) für Türen, Fenster und Treppen — analog
|
// Wiederverwendbare Stile (Presets) für Türen, Fenster und Treppen — analog
|
||||||
// `WallType`/`CeilingType` (Bibliothek im Projekt, referenziert per `typeId`).
|
// `WallType`/`CeilingType` (Bibliothek im Projekt, referenziert per `typeId`).
|
||||||
@@ -1053,8 +1068,18 @@ export interface Roof {
|
|||||||
ridgeAxis: "x" | "y";
|
ridgeAxis: "x" | "y";
|
||||||
/** Traufhöhe (absolutes Z, Meter); fehlt ⇒ Oberkante des Geschosses. */
|
/** Traufhöhe (absolutes Z, Meter); fehlt ⇒ Oberkante des Geschosses. */
|
||||||
baseElevation?: number;
|
baseElevation?: number;
|
||||||
/** Dachdicke (Meter, senkrecht zur Fläche gemessen). */
|
/**
|
||||||
|
* Dachdicke (Meter, senkrecht zur Fläche) — Fallback/Override. Ohne
|
||||||
|
* {@link roofTypeId} ist es die einschichtige Gesamtdicke; mit `roofTypeId`
|
||||||
|
* bleibt es als reiner Fallback bestehen (die Schichtsumme des Typs gilt).
|
||||||
|
*/
|
||||||
thickness: number;
|
thickness: number;
|
||||||
|
/**
|
||||||
|
* Verweis auf einen Dachtyp (Dachaufbau) aus `Project.roofTypes` — mehrschichtig
|
||||||
|
* (Eindeckung/Lattung/Dämmung/Verkleidung), analog `Ceiling.ceilingTypeId`.
|
||||||
|
* Fehlt er, gilt die einschichtige {@link thickness}.
|
||||||
|
*/
|
||||||
|
roofTypeId?: string;
|
||||||
/** Optionale Übersteuerung der Strich-/Umrissfarbe; sonst Kategorie-Farbe. */
|
/** Optionale Übersteuerung der Strich-/Umrissfarbe; sonst Kategorie-Farbe. */
|
||||||
color?: string;
|
color?: string;
|
||||||
}
|
}
|
||||||
@@ -1950,6 +1975,11 @@ export interface Project {
|
|||||||
* `Ceiling.wallTypeId` gegen `wallTypes` auf (siehe `getCeilingType`).
|
* `Ceiling.wallTypeId` gegen `wallTypes` auf (siehe `getCeilingType`).
|
||||||
*/
|
*/
|
||||||
ceilingTypes?: CeilingType[];
|
ceilingTypes?: CeilingType[];
|
||||||
|
/**
|
||||||
|
* Dachtypen (Dachaufbauten) — mehrschichtig, analog `ceilingTypes`. Optional;
|
||||||
|
* Dächer ohne `roofTypeId` lösen über die einschichtige `Roof.thickness` auf.
|
||||||
|
*/
|
||||||
|
roofTypes?: RoofType[];
|
||||||
/**
|
/**
|
||||||
* Türtypen-Bibliothek (Türstile), analog `wallTypes`. Optional, damit
|
* Türtypen-Bibliothek (Türstile), analog `wallTypes`. Optional, damit
|
||||||
* bestehende Projekte/Tests ohne `doorTypes` gültig bleiben; Türen ohne
|
* bestehende Projekte/Tests ohne `doorTypes` gültig bleiben; Türen ohne
|
||||||
@@ -2110,6 +2140,24 @@ export const ceilingThickness = (project: Project, ceiling: Ceiling): number =>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schichtaufbau eines Daches (aussen/Eindeckung → innen/Verkleidung). Mit
|
||||||
|
* {@link Roof.roofTypeId} die Schichten des Dachtyps; sonst eine EINSCHICHTIGE
|
||||||
|
* synthetische Schicht aus {@link Roof.thickness} (leere `componentId` ⇒ der
|
||||||
|
* Renderer nutzt seine Default-Dachfarbe).
|
||||||
|
*/
|
||||||
|
export const roofLayers = (project: Project, roof: Roof): Layer[] => {
|
||||||
|
if (roof.roofTypeId) {
|
||||||
|
const rt = (project.roofTypes ?? []).find((t) => t.id === roof.roofTypeId);
|
||||||
|
if (rt && rt.layers.length > 0) return rt.layers;
|
||||||
|
}
|
||||||
|
return [{ componentId: "", thickness: Math.max(0, roof.thickness) }];
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Gesamtdicke eines Daches (Meter, senkrecht zur Fläche) = Summe der Schichten. */
|
||||||
|
export const roofTotalThickness = (project: Project, roof: Roof): number =>
|
||||||
|
roofLayers(project, roof).reduce((s, l) => s + Math.max(0, l.thickness), 0);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Löst den {@link DoorType} einer Tür-Öffnung auf, oder `undefined` (keine
|
* Löst den {@link DoorType} einer Tür-Öffnung auf, oder `undefined` (keine
|
||||||
* `typeId` bzw. unbekannt ⇒ Inline-Verhalten). Bewusst NICHT werfend — der
|
* `typeId` bzw. unbekannt ⇒ Inline-Verhalten). Bewusst NICHT werfend — der
|
||||||
|
|||||||
@@ -497,6 +497,21 @@ export function RoofSection({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Dachtyp (mehrschichtiger Aufbau) — leer = einschichtig (Dicke unten). */}
|
||||||
|
<div className="objinfo-field">
|
||||||
|
<span className="objinfo-flabel">{t("objinfo.roof.type")}</span>
|
||||||
|
<Dropdown
|
||||||
|
value={roof.roofTypeId ?? ""}
|
||||||
|
onChange={(v) => host.onSetRoofPatch({ roofTypeId: v || undefined })}
|
||||||
|
options={[
|
||||||
|
{ value: "", label: t("objinfo.roof.type.single") },
|
||||||
|
...(host.project.roofTypes ?? []).map((rt) => ({ value: rt.id, label: rt.name })),
|
||||||
|
]}
|
||||||
|
title={t("objinfo.roof.type")}
|
||||||
|
width={130}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Firstrichtung (X/Y) — nur bei Formen mit First relevant. */}
|
{/* Firstrichtung (X/Y) — nur bei Formen mit First relevant. */}
|
||||||
{roof.shape !== "flach" && roof.shape !== "zelt" && (
|
{roof.shape !== "flach" && roof.shape !== "zelt" && (
|
||||||
<div className="objinfo-field">
|
<div className="objinfo-field">
|
||||||
|
|||||||
+72
-15
@@ -1229,21 +1229,22 @@ describe("emitRoofs (Dachflächen + Giebel als Meshes)", () => {
|
|||||||
const project: Project = { ...sampleProject, roofs: [baseRoof] };
|
const project: Project = { ...sampleProject, roofs: [baseRoof] };
|
||||||
const { meshes } = projectToModel3d(project);
|
const { meshes } = projectToModel3d(project);
|
||||||
const roofMeshes = meshes.filter(isRoofMesh);
|
const roofMeshes = meshes.filter(isRoofMesh);
|
||||||
// Zwei Dachflächen (je ein Quad -> 2 Dreiecke) + zwei Giebel (je ein Dreieck)
|
// Einschichtiges Dach (thickness-Fallback): 1 Schicht-Slab-Mesh (Flächen) +
|
||||||
// landen in EINEM kombinierten Mesh (Flächen und Giebel bilden zusammen den
|
// 1 Giebel-Mesh (Endfüllung) = 2 Roof-Meshes (beide ROOF_RGB).
|
||||||
// geschlossenen Dachkörper).
|
expect(roofMeshes.length).toBe(2);
|
||||||
expect(roofMeshes.length).toBe(1);
|
for (const m of roofMeshes) {
|
||||||
const m = roofMeshes[0];
|
expect(m.positions.every((v) => Number.isFinite(v))).toBe(true);
|
||||||
expect(m.positions.every((v) => Number.isFinite(v))).toBe(true);
|
expect(m.indices.length % 3).toBe(0);
|
||||||
expect(m.indices.length % 3).toBe(0);
|
expect(m.indices.length).toBeGreaterThan(0);
|
||||||
expect(m.indices.length).toBeGreaterThan(0);
|
const vcount = m.positions.length / 3;
|
||||||
const vcount = m.positions.length / 3;
|
expect(m.indices.every((idx) => idx >= 0 && idx < vcount)).toBe(true);
|
||||||
expect(m.indices.every((idx) => idx >= 0 && idx < vcount)).toBe(true);
|
}
|
||||||
// Firsthöhe: eavesZ (baseElevation) + ridgeHeight aus roofGeometry.
|
// Firsthöhe: eavesZ (baseElevation) + ridgeHeight aus roofGeometry — über ALLE
|
||||||
|
// Roof-Meshes (Oberseite der Flächen bzw. Giebelspitze).
|
||||||
const g = roofGeometry(baseRoof, roofBaseElevation(project, baseRoof));
|
const g = roofGeometry(baseRoof, roofBaseElevation(project, baseRoof));
|
||||||
const expectedRidgeZ = roofBaseElevation(project, baseRoof) + g.ridgeHeight;
|
const expectedRidgeZ = roofBaseElevation(project, baseRoof) + g.ridgeHeight;
|
||||||
const heights = m.positions.filter((_, i) => i % 3 === 2);
|
const allHeights = roofMeshes.flatMap((m) => m.positions.filter((_, i) => i % 3 === 2));
|
||||||
expect(Math.max(...heights)).toBeCloseTo(expectedRidgeZ, 6);
|
expect(Math.max(...allHeights)).toBeCloseTo(expectedRidgeZ, 6);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Walmdach: vier Dachflächen (2 Quader + 2 Dreiecke) korrekt trianguliert", () => {
|
it("Walmdach: vier Dachflächen (2 Quader + 2 Dreiecke) korrekt trianguliert", () => {
|
||||||
@@ -1272,6 +1273,61 @@ describe("emitRoofs (Dachflächen + Giebel als Meshes)", () => {
|
|||||||
expect(Math.min(...heights)).toBeLessThan(3 - 0.2);
|
expect(Math.min(...heights)).toBeLessThan(3 - 0.2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Dach-Schichtaufbau (roofTypeId): je Schicht ein eigenes Mesh mit Bauteilfarbe, entlang der Normalen gestapelt", () => {
|
||||||
|
// Dachtyp mit zwei Schichten (Eindeckung + Sparren/Dämmung), eigene Farben.
|
||||||
|
const flatRoof: Roof = {
|
||||||
|
...baseRoof,
|
||||||
|
shape: "flach", // Flachdach: eine Fläche, keine Giebel -> reine Schicht-Meshes
|
||||||
|
roofTypeId: "rt-warm",
|
||||||
|
};
|
||||||
|
const project: Project = {
|
||||||
|
...sampleProject,
|
||||||
|
components: [
|
||||||
|
...sampleProject.components,
|
||||||
|
{
|
||||||
|
id: "comp-tile",
|
||||||
|
name: "Ziegel",
|
||||||
|
color: "#a03a2a",
|
||||||
|
hatchId: "",
|
||||||
|
joinPriority: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "comp-rafter",
|
||||||
|
name: "Sparren/Dämmung",
|
||||||
|
color: "#caa15a",
|
||||||
|
hatchId: "",
|
||||||
|
joinPriority: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
roofTypes: [
|
||||||
|
{
|
||||||
|
id: "rt-warm",
|
||||||
|
name: "Warmdach",
|
||||||
|
layers: [
|
||||||
|
{ componentId: "comp-tile", thickness: 0.05 },
|
||||||
|
{ componentId: "comp-rafter", thickness: 0.24 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
roofs: [flatRoof],
|
||||||
|
};
|
||||||
|
const meshes = projectToModel3d(project).meshes;
|
||||||
|
const tile = meshes.filter(
|
||||||
|
(m) => Math.abs(m.color[0] - 0xa0 / 255) < 1e-6 && Math.abs(m.color[1] - 0x3a / 255) < 1e-6,
|
||||||
|
);
|
||||||
|
const rafter = meshes.filter(
|
||||||
|
(m) => Math.abs(m.color[0] - 0xca / 255) < 1e-6 && Math.abs(m.color[1] - 0xa1 / 255) < 1e-6,
|
||||||
|
);
|
||||||
|
expect(tile.length).toBe(1); // Eindeckungs-Schicht als eigenes Mesh
|
||||||
|
expect(rafter.length).toBe(1); // Sparren-Schicht als eigenes Mesh
|
||||||
|
// Die Sparren-Schicht liegt UNTER der Eindeckung: ihre Oberkante = Traufe −
|
||||||
|
// Eindeckungsdicke. Flachdach-Oberseite bei baseElevation 3.
|
||||||
|
const topOf = (m: { positions: number[] }) =>
|
||||||
|
Math.max(...m.positions.filter((_, i) => i % 3 === 2));
|
||||||
|
expect(topOf(tile[0])).toBeCloseTo(3, 6);
|
||||||
|
expect(topOf(rafter[0])).toBeCloseTo(3 - 0.05, 6);
|
||||||
|
});
|
||||||
|
|
||||||
it("Projekt ohne Dächer emittiert keine Roof-Meshes (Rückwärtskompatibilität)", () => {
|
it("Projekt ohne Dächer emittiert keine Roof-Meshes (Rückwärtskompatibilität)", () => {
|
||||||
// Explizit leeres roofs-Feld -> keine Roof-Meshes.
|
// Explizit leeres roofs-Feld -> keine Roof-Meshes.
|
||||||
const project: Project = { ...sampleProject, roofs: [] };
|
const project: Project = { ...sampleProject, roofs: [] };
|
||||||
@@ -1280,8 +1336,9 @@ describe("emitRoofs (Dachflächen + Giebel als Meshes)", () => {
|
|||||||
const noField: Project = { ...sampleProject };
|
const noField: Project = { ...sampleProject };
|
||||||
delete (noField as { roofs?: unknown }).roofs;
|
delete (noField as { roofs?: unknown }).roofs;
|
||||||
expect(projectToModel3d(noField).meshes.filter(isRoofMesh).length).toBe(0);
|
expect(projectToModel3d(noField).meshes.filter(isRoofMesh).length).toBe(0);
|
||||||
// sampleProject enthält jetzt ein Demo-Satteldach (RF1) -> genau ein Roof-Mesh.
|
// sampleProject enthält ein Demo-Satteldach (RF1, einschichtig) -> 2 Roof-
|
||||||
expect(projectToModel3d(sampleProject).meshes.filter(isRoofMesh).length).toBe(1);
|
// Meshes: Schicht-Slab (Flächen) + Giebel-Endfüllung.
|
||||||
|
expect(projectToModel3d(sampleProject).meshes.filter(isRoofMesh).length).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+42
-10
@@ -45,6 +45,7 @@ import {
|
|||||||
getDoorType,
|
getDoorType,
|
||||||
getWindowType,
|
getWindowType,
|
||||||
glazingPanesOf,
|
glazingPanesOf,
|
||||||
|
roofLayers,
|
||||||
} from "../model/types";
|
} from "../model/types";
|
||||||
import { wallVerticalExtent, ceilingVerticalExtent, columnVerticalExtent, stairVerticalExtent } from "../model/wall";
|
import { wallVerticalExtent, ceilingVerticalExtent, columnVerticalExtent, stairVerticalExtent } from "../model/wall";
|
||||||
import { columnFootprint } from "../geometry/column";
|
import { columnFootprint } from "../geometry/column";
|
||||||
@@ -1729,16 +1730,47 @@ function emitRoofs(project: Project): RMesh[] {
|
|||||||
for (const roof of project.roofs ?? []) {
|
for (const roof of project.roofs ?? []) {
|
||||||
const eavesZ = roofBaseElevation(project, roof);
|
const eavesZ = roofBaseElevation(project, roof);
|
||||||
const g = roofGeometry(roof, eavesZ);
|
const g = roofGeometry(roof, eavesZ);
|
||||||
const color = roof.color ? hexToRgb(roof.color, ROOF_RGB) : ROOF_RGB;
|
const overrideColor = roof.color ? hexToRgb(roof.color, ROOF_RGB) : null;
|
||||||
const thickness = Math.max(0, roof.thickness);
|
// Schichtaufbau (aussen/Eindeckung → innen). Jede Schicht wird zu einem
|
||||||
const positions: number[] = [];
|
// eigenen RMesh mit ihrer Bauteilfarbe, entlang der Flächennormalen unter die
|
||||||
const indices: number[] = [];
|
// vorige gestapelt. Leere componentId (einschichtige thickness-Fallback) ⇒
|
||||||
// Dachflächen als solide Slabs (echte Dicke); Giebel bleiben dünne Füllung
|
// Default-Dachfarbe. Eine Roof.color-Übersteuerung gilt für alle Schichten.
|
||||||
// (schliessen die offenen Enden zur Attika hin).
|
const layers = roofLayers(project, roof);
|
||||||
for (const plane of g.planes) pushRoofSlab(positions, indices, plane.pts, thickness);
|
layers.reduce((offsetAbove, layer) => {
|
||||||
for (const gable of g.gables) pushFanTriangles(positions, indices, gable);
|
const t = Math.max(0, layer.thickness);
|
||||||
if (indices.length === 0) continue;
|
if (t <= EPS) return offsetAbove;
|
||||||
out.push({ positions, indices, kind: "extrusion", color });
|
const color =
|
||||||
|
overrideColor ??
|
||||||
|
(layer.componentId ? hexToRgb(getComponent(project, layer.componentId).color, ROOF_RGB) : ROOF_RGB);
|
||||||
|
const positions: number[] = [];
|
||||||
|
const indices: number[] = [];
|
||||||
|
for (const plane of g.planes) {
|
||||||
|
// Oberseite dieser Schicht = Dachfläche um `offsetAbove` entlang der
|
||||||
|
// Normalen nach innen versetzt; Slab-Dicke = Schichtdicke.
|
||||||
|
const nrm = planeNormalUp(plane.pts);
|
||||||
|
const top: RoofVec3[] = plane.pts.map((p) => [
|
||||||
|
p[0] - nrm[0] * offsetAbove,
|
||||||
|
p[1] - nrm[1] * offsetAbove,
|
||||||
|
p[2] - nrm[2] * offsetAbove,
|
||||||
|
]);
|
||||||
|
pushRoofSlab(positions, indices, top, t);
|
||||||
|
}
|
||||||
|
if (indices.length > 0) out.push({ positions, indices, kind: "extrusion", color });
|
||||||
|
return offsetAbove + t;
|
||||||
|
}, 0);
|
||||||
|
// Giebel (Endfüllung) als dünne Fläche, äusserste Schichtfarbe/Override.
|
||||||
|
if (g.gables.length > 0) {
|
||||||
|
const gp: number[] = [];
|
||||||
|
const gi: number[] = [];
|
||||||
|
for (const gable of g.gables) pushFanTriangles(gp, gi, gable);
|
||||||
|
if (gi.length > 0) {
|
||||||
|
const first = layers[0];
|
||||||
|
const gc =
|
||||||
|
overrideColor ??
|
||||||
|
(first?.componentId ? hexToRgb(getComponent(project, first.componentId).color, ROOF_RGB) : ROOF_RGB);
|
||||||
|
out.push({ positions: gp, indices: gi, kind: "extrusion", color: gc });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ export interface RoofInfo {
|
|||||||
pitchDeg: number;
|
pitchDeg: number;
|
||||||
/** Obere Neigung (Grad) — nur Mansarde relevant. */
|
/** Obere Neigung (Grad) — nur Mansarde relevant. */
|
||||||
pitchUpperDeg?: number;
|
pitchUpperDeg?: number;
|
||||||
|
/** Dachtyp-Verweis (mehrschichtiger Aufbau) oder undefined (einschichtig). */
|
||||||
|
roofTypeId?: string;
|
||||||
/** Mansarde-Untertyp (Giebel/Walm/Zelt) — nur Mansarde relevant. */
|
/** Mansarde-Untertyp (Giebel/Walm/Zelt) — nur Mansarde relevant. */
|
||||||
mansardType?: "giebel" | "walm" | "zelt";
|
mansardType?: "giebel" | "walm" | "zelt";
|
||||||
/** Mansarde-Knicklage (0..0.5 der halben Spannweite) — nur Mansarde relevant. */
|
/** Mansarde-Knicklage (0..0.5 der halben Spannweite) — nur Mansarde relevant. */
|
||||||
@@ -630,6 +632,7 @@ function roofSelection(project: Project, roof: Roof): Selection {
|
|||||||
shape: roof.shape,
|
shape: roof.shape,
|
||||||
pitchDeg: roof.pitchDeg,
|
pitchDeg: roof.pitchDeg,
|
||||||
pitchUpperDeg: roof.pitchUpperDeg,
|
pitchUpperDeg: roof.pitchUpperDeg,
|
||||||
|
roofTypeId: roof.roofTypeId,
|
||||||
mansardType: roof.mansardType,
|
mansardType: roof.mansardType,
|
||||||
mansardKneeRatio: roof.mansardKneeRatio,
|
mansardKneeRatio: roof.mansardKneeRatio,
|
||||||
overhang: roof.overhang,
|
overhang: roof.overhang,
|
||||||
|
|||||||
Reference in New Issue
Block a user