Deckenstile: solide oder mehrschichtige Decke

Analog zu den Wandstilen erhalten Decken jetzt einen eigenen
Deckentyp mit Schichtaufbau (CeilingType, ceilingTypeId). Der
Schichtaufbau erscheint nur im Schnitt als gestapelte Bänder von OK
bis UK; der Grundriss bleibt eine flächige Poché wie bisher, da die
Schichtung von oben ohnehin nicht sichtbar ist. Auswahl solide/
mehrschichtig im Objektinfo-Panel wie bei Wänden, neuer
"Deckenstile"-Tab im Ressourcen-Manager mit Fugenlinien je Schicht.
This commit is contained in:
2026-07-03 22:09:02 +02:00
parent b1e7fdf8fc
commit d788cbb32b
13 changed files with 341 additions and 45 deletions
+13 -2
View File
@@ -27,6 +27,7 @@ import {
import { wallCorners } from "./model/geometry";
import { leftNormal, normalize, scale, sub } from "./model/geometry";
import type {
CeilingType,
Drawing2D,
DrawingLevel,
EdgeGrip,
@@ -1629,6 +1630,14 @@ export default function App() {
...p,
wallTypes: p.wallTypes.map((wt) => (wt.id === id ? { ...wt, ...patch } : wt)),
}));
// Deckenstile: immutable Patch eines Deckentyps — analog `patchWallType`.
const patchCeilingType = (id: string, patch: Partial<CeilingType>) =>
setProject((p) => ({
...p,
ceilingTypes: (p.ceilingTypes ?? []).map((ct) =>
ct.id === id ? { ...ct, ...patch } : ct,
),
}));
const importLineStyles = useStore((s) => s.importLineStyles);
const importHatches = useStore((s) => s.importHatches);
// Site-/Kontext-Schicht (siteSlice): Import/Entfernen/Gelände.
@@ -1915,6 +1924,7 @@ export default function App() {
onAddLineStyle: addLineStyle,
onDeleteLineStyle: deleteLineStyle,
onPatchWallType: patchWallType,
onPatchCeilingType: patchCeilingType,
onImportLineStyles: importLineStyles,
onImportHatches: importHatches,
selection,
@@ -1972,9 +1982,9 @@ export default function App() {
updateWall(selection.id, { top: anchor ?? undefined });
},
// ── Decken-Attribute (nur bei selektierter Decke) ──────────────────────
onSetCeilingType: (wallTypeId) => {
onSetCeilingType: (ceilingTypeId) => {
if (selection?.kind === "ceiling")
updateCeiling(selection.id, { wallTypeId, thickness: undefined });
updateCeiling(selection.id, { ceilingTypeId, thickness: undefined });
},
onSetCeilingThickness: (thickness) => {
if (selection?.kind === "ceiling")
@@ -2147,6 +2157,7 @@ export default function App() {
const resourceHandlers: ResourceManagerHandlers = {
onPatchComponent: patchComponent,
onPatchWallType: patchWallType,
onPatchCeilingType: patchCeilingType,
onAddComponent: addComponent,
onDeleteComponent: deleteComponent,
onPatchHatch: patchHatch,
+5
View File
@@ -427,6 +427,7 @@ export const de = {
"default.copySuffixLevel": "{name} Kopie",
"default.copySuffixLayer": "{name} KOPIE",
"default.wallTypeSingle": "Wand {thickness} m",
"default.ceilingTypeSingle": "Decke {thickness} m",
// ── Warnungen (Lösch-Schutz) ─────────────────────────────────────────────
"alert.layerInUse": "Ebene „{code} {name}\" wird von Bauteilen verwendet und kann nicht gelöscht werden.",
@@ -442,11 +443,15 @@ export const de = {
"resources.tab.hatches": "Schraffuren",
"resources.tab.lines": "Linien",
"resources.tab.wallStyles": "Wandstile",
"resources.tab.ceilingStyles": "Deckenstile",
"resources.tab.materials": "Materialien",
"resources.wallStyles.hint":
"Je Wandtyp die Schichtfugen (zwischen benachbarten Schichten) als wählbaren Linienstil. Ohne Zuweisung: Haarlinie 0.02 mm.",
"resources.wallStyles.empty": "Noch keine Wandtypen.",
"resources.ceilingStyles.hint":
"Je Deckentyp (liegender Schichtaufbau, oben → unten) die Schichtfugen als wählbaren Linienstil. Ohne Zuweisung: Haarlinie 0.02 mm.",
"resources.ceilingStyles.empty": "Noch keine Deckentypen.",
"resources.col.layer": "Schicht",
"resources.col.thickness": "Dicke (mm)",
"resources.col.joint": "Fuge",
+5
View File
@@ -424,6 +424,7 @@ export const en: Record<TranslationKey, string> = {
"default.copySuffixLevel": "{name} copy",
"default.copySuffixLayer": "{name} COPY",
"default.wallTypeSingle": "Wall {thickness} m",
"default.ceilingTypeSingle": "Ceiling {thickness} m",
// Alerts.
"alert.layerInUse": "Layer “{code} {name}” is used by components and cannot be deleted.",
@@ -439,11 +440,15 @@ export const en: Record<TranslationKey, string> = {
"resources.tab.hatches": "Hatches",
"resources.tab.lines": "Lines",
"resources.tab.wallStyles": "Wall styles",
"resources.tab.ceilingStyles": "Ceiling styles",
"resources.tab.materials": "Materials",
"resources.wallStyles.hint":
"Per wall type, the layer joints (between adjacent layers) as a selectable line style. Unassigned: 0.02 mm hairline.",
"resources.wallStyles.empty": "No wall types yet.",
"resources.ceilingStyles.hint":
"Per ceiling type (horizontal build-up, top → bottom), the layer joints as a selectable line style. Unassigned: 0.02 mm hairline.",
"resources.ceilingStyles.empty": "No ceiling types yet.",
"resources.col.layer": "Layer",
"resources.col.thickness": "Thickness (mm)",
"resources.col.joint": "Joint",
+23 -3
View File
@@ -122,6 +122,8 @@ export const sampleProject: Project = {
{ id: "render-int", name: "Innenputz", color: "#efece6", hatchId: "none", joinPriority: 10 },
// Für spätere T-Stöße: Beton als durchlaufender Backbone (höchste Priorität).
{ id: "concrete", name: "Beton", color: "#9aa0a6", hatchId: "sia-concrete", joinPriority: 100 },
// Zementestrich — oberste Schicht des mehrschichtigen Deckenaufbaus (Decken-Stile).
{ id: "screed", name: "Zementestrich", color: "#c9c2b3", hatchId: "diagonal", joinPriority: 30 },
],
wallTypes: [
{
@@ -137,6 +139,22 @@ export const sampleProject: Project = {
],
},
],
// Deckenstile (Decken-Aufbau-Typen) — das liegende Gegenstück der Wandstile.
// Schichten oben (OK) → unten (UK), analog zur Wand außen → innen.
ceilingTypes: [
{
id: "dg-massiv",
name: "Geschossdecke mehrschichtig",
layers: [
// Estrich (schwimmend) → Trittschalldämmung.
{ componentId: "screed", thickness: 0.06 },
// Innere Fuge der Dämmung trifft auf die Betondecke (massiv) → 0.13 mm.
{ componentId: "insulation", thickness: 0.04, jointLineStyleId: "joint-massive" },
// Betondecke (Rohdecke, tragend) — innerste/unterste Schicht.
{ componentId: "concrete", thickness: 0.2 },
],
},
],
// Oberste Schnitte: Geschosse + Schnitt/Ansicht + freie Zeichnung.
// baseElevation wird über recomputeFloorElevations gestapelt: EG=0, OG=2.6.
drawingLevels: recomputeFloorElevations([
@@ -246,8 +264,10 @@ export const sampleProject: Project = {
},
],
// 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).
// Grundriss (5 × 4 m). Kategorie „30 Decken", mehrschichtiger Deckenstil
// „dg-massiv" (Estrich + Trittschalldämmung + Betondecke, 30 cm), OK bündig
// mit dem EG-Wandkopf (baseElevation + floorHeight = 2.6 m). `wallTypeId`
// bleibt als LEGACY-Rückfall gesetzt (siehe `getCeilingType`).
ceilings: [
{
id: "C1",
@@ -261,7 +281,7 @@ export const sampleProject: Project = {
{ x: 0, y: 4 },
],
wallTypeId: "aw",
thickness: 0.25,
ceilingTypeId: "dg-massiv",
},
],
// Treppen — eine gerade Beispieltreppe im EG entlang der Ostwand, steigt ins OG
+51 -7
View File
@@ -144,6 +144,19 @@ export interface WallType {
layers: Layer[];
}
/**
* Ein Deckentyp (Deckenstil) = geordneter Schichtaufbau EINER Decke, liegend
* gestapelt (oben unten) das horizontale Gegenstück zum `WallType`. Nutzt
* denselben `Layer`-Typ (Bauteil + Dicke + optionaler Schichtfugen-Linienstil),
* damit Fuge/Schraffur-Auflösung identisch zur Wand bleiben. Eine einschichtige"
* Decke (SOLID) ist einfach ein Deckentyp mit genau einer Schicht.
*/
export interface CeilingType {
id: string;
name: string;
layers: Layer[];
}
// ── Parametrische Wände (Parametric Walls) ────────────────────────────────
// Parametrische Wand-Regeln generieren automatisch Wall[]-Arrays — analog zu
// FreeCAD BIM. Sie leben in Project.resources.parametricWalls[] und werden
@@ -462,8 +475,18 @@ export interface Ceiling {
* dupliziert (die letzte Kante läuft von outline[n-1] zu outline[0]).
*/
outline: Vec2[];
/** Verweis auf den (mehrschichtigen) Aufbau-Typ (wie WallType). */
/**
* LEGACY-Verweis auf einen Aufbau-Typ aus `wallTypes` (Rückwärtskompatibilität
* für Projekte von vor den dedizierten Deckenstilen). Nur wirksam, wenn
* `ceilingTypeId` fehlt.
*/
wallTypeId: string;
/**
* Verweis auf einen dedizierten Deckentyp (Deckenstil) aus `ceilingTypes`
* der reguläre Weg für SOLID (1 Schicht) und MEHRSCHICHTIG (>1 Schicht). Hat
* Vorrang vor `wallTypeId`, falls gesetzt und im Projekt auflösbar.
*/
ceilingTypeId?: string;
/** Optionale Übersteuerung der Gesamtdicke in Metern (sonst Typ-Dicke). */
thickness?: number;
/**
@@ -845,6 +868,13 @@ export interface Project {
/** Bauteil-Material-Bibliothek (Component Manager). */
components: Component[];
wallTypes: WallType[];
/**
* Deckentypen (Deckenstile) dediziert für Decken, analog `wallTypes`.
* Optional, damit bestehende Projekte/Tests ohne `ceilingTypes` gültig
* bleiben; Decken ohne `ceilingTypeId` lösen weiterhin über das LEGACY-Feld
* `Ceiling.wallTypeId` gegen `wallTypes` auf (siehe `getCeilingType`).
*/
ceilingTypes?: CeilingType[];
/** Oberste Schnitte: Geschosse + Schnitte/Ansichten. */
drawingLevels: DrawingLevel[];
/** Grafik-Kategorie-Baum (geschossübergreifend). */
@@ -898,21 +928,35 @@ export const getWallType = (project: Project, wall: Wall): WallType => {
return wt;
};
/** Liefert den Aufbau-Typ (WallType) einer Decke oder wirft. */
export const getCeilingType = (project: Project, ceiling: Ceiling): WallType => {
/**
* Liefert den Aufbau-Typ einer Decke oder wirft. Bevorzugt den dedizierten
* Deckentyp (`ceilingTypeId` `ceilingTypes`); fehlt er, fällt die Auflösung
* auf das LEGACY-Feld `wallTypeId` `wallTypes` zurück (Rückwärtskompatibilität
* mit Projekten von vor den Deckenstilen dort trug die Decke ihren Aufbau
* direkt über einen WallType).
*/
export const getCeilingType = (project: Project, ceiling: Ceiling): CeilingType | WallType => {
if (ceiling.ceilingTypeId) {
const ct = (project.ceilingTypes ?? []).find((t) => t.id === ceiling.ceilingTypeId);
if (ct) return ct;
}
const wt = project.wallTypes.find((t) => t.id === ceiling.wallTypeId);
if (!wt) throw new Error(`Unbekannter Deckentyp: ${ceiling.wallTypeId}`);
if (!wt) throw new Error(`Unbekannter Deckentyp: ${ceiling.ceilingTypeId ?? ceiling.wallTypeId}`);
return wt;
};
/**
* Gesamtdicke einer Decke (Meter): eine explizite `thickness`-Übersteuerung hat
* Vorrang, sonst die Summe der Schichtdicken ihres Aufbau-Typs.
* Vorrang, sonst die Summe der Schichtdicken ihres Aufbau-Typs (siehe
* `getCeilingType`).
*/
export const ceilingThickness = (project: Project, ceiling: Ceiling): number => {
if (ceiling.thickness != null && ceiling.thickness > 0) return ceiling.thickness;
const wt = project.wallTypes.find((t) => t.id === ceiling.wallTypeId);
return wt ? wallTypeThickness(wt) : 0.2;
try {
return wallTypeThickness(getCeilingType(project, ceiling));
} catch {
return 0.2;
}
};
/** Alle Öffnungen einer Wand (leere Liste, wenn keine oder `openings` fehlt). */
+6 -6
View File
@@ -510,7 +510,7 @@ function CeilingSection({
host: ReturnType<typeof usePanelHost>;
}) {
const isSingle = ceiling.singleLayer;
const multiPresets = ceiling.wallTypes.filter((wt) => wt.layerCount > 1);
const multiPresets = ceiling.ceilingTypes.filter((ct) => ct.layerCount > 1);
function chooseSingle() {
if (isSingle) return;
@@ -560,13 +560,13 @@ function CeilingSection({
<div className="objinfo-field">
<span className="objinfo-flabel">{t("objinfo.ceiling.preset")}</span>
<Dropdown
value={ceiling.wallTypeId}
value={ceiling.typeId}
onChange={(id) => host.onSetCeilingType(id)}
options={ceiling.wallTypes.map((wt) => ({
value: wt.id,
options={ceiling.ceilingTypes.map((ct) => ({
value: ct.id,
label: t("objinfo.wall.presetLabel", {
name: wt.name,
thickness: wt.thickness.toFixed(2),
name: ct.name,
thickness: ct.thickness.toFixed(2),
}),
}))}
title={t("objinfo.ceiling.preset")}
+1
View File
@@ -35,6 +35,7 @@ export function ResourcesPanel() {
onAddLineStyle: host.onAddLineStyle,
onDeleteLineStyle: host.onDeleteLineStyle,
onPatchWallType: host.onPatchWallType,
onPatchCeilingType: host.onPatchCeilingType,
onImportLineStyles: host.onImportLineStyles,
onImportHatches: host.onImportHatches,
};
+5 -2
View File
@@ -15,6 +15,7 @@ import { useContext } from "react";
import { PanelHostContext } from "./types";
import type { DisplayMode } from "./types";
import type {
CeilingType,
Component,
ContextObject,
DrawingLevel,
@@ -117,6 +118,8 @@ export interface PanelHostValue {
onDeleteLineStyle: (id: string) => void;
/** Wandstile: immutable Änderung eines Wandtyps (Schichtfugen-Stile). */
onPatchWallType: (id: string, patch: Partial<WallType>) => void;
/** Deckenstile: immutable Änderung eines Deckentyps (Schichtfugen-Stile). */
onPatchCeilingType: (id: string, patch: Partial<CeilingType>) => void;
/** Import fertiger (id-loser) Linienstile/Schraffuren (.lin/.pat). */
onImportLineStyles: (styles: Omit<LineStyle, "id">[]) => void;
onImportHatches: (hatches: Omit<HatchStyle, "id">[]) => void;
@@ -162,8 +165,8 @@ export interface PanelHostValue {
onSetWallTop: (anchor: VerticalAnchor | null) => void;
// ── Decken-Attribute (Object-Info-Panel; wirken NUR auf die selektierte Decke) ─
/** Weist der Decke einen Aufbau-Typ-Preset zu (wie WallType). */
onSetCeilingType: (wallTypeId: string) => void;
/** Weist der Decke einen dedizierten Deckentyp-Preset zu (Deckenstil). */
onSetCeilingType: (ceilingTypeId: string) => void;
/** Setzt die Gesamtdicke der Decke (Meter, thickness-Übersteuerung). */
onSetCeilingThickness: (thickness: number) => void;
/** Setzt/entfernt die OK-Bindung der Decke (`null` = Geschoss-Oberkante). */
+8 -3
View File
@@ -248,7 +248,7 @@ export type Primitive =
* {@link HatchRender} auf. Der Linienstil der Musterlinien wird, falls gesetzt,
* aus dem Line Manager nachgeschlagen; sonst gilt ein dünner Default.
*/
function resolveHatch(project: Project, hatchId: string, wallAngleDeg?: number): HatchRender {
export function resolveHatch(project: Project, hatchId: string, wallAngleDeg?: number): HatchRender {
const h = getHatch(project, hatchId);
const ls = h.lineStyleId ? getLineStyle(project, h.lineStyleId) : null;
// Wandbezogener Winkel: `HatchRender.angle` wird von allen Renderern in der
@@ -1195,8 +1195,13 @@ function addReferenceLine(out: Primitive[], wall: Wall, greyed: boolean): void {
* zur Wand-Schichtauflösung); die Umrissfarbe aus der optionalen Decken-Farbe
* bzw. dem Poché-Strich. Jedes Polygon trägt die `ceilingId` für die Auswahl.
*
* Im Detailgrad grob" entfällt die Schraffur (nur Fläche + Umriss); „mittel"/
* fein" zeigen die Component-Schraffur. `lwMm` ist die Kategorie-Strichstärke.
* BEWUSST FLÄCHIG, auch bei mehrschichtigem Deckentyp: anders als die Wand
* (deren Schichten im Grundriss als Querschnitt sichtbar sind) liegt der
* Schichtaufbau einer Decke in Z (oben unten) von oben unsichtbar. Die
* Schichtbänderung erscheint daher NUR im Schnitt (siehe `toSection.ts`), nicht
* hier. Im Detailgrad grob" entfällt die Schraffur (nur Fläche + Umriss);
* mittel"/„fein" zeigen die Component-Schraffur. `lwMm` ist die Kategorie-
* Strichstärke.
*/
function addCeilingPoche(
out: Primitive[],
+81 -11
View File
@@ -16,13 +16,12 @@
// v = absolute Höhe (world.y). Siehe src-tauri/render3d/src/section.rs.
import type { Ceiling, DrawingLevel, Project, Wall } from "../model/types";
import { openingsOfWall } from "../model/types";
import { getCeilingType, getComponent, openingsOfWall } from "../model/types";
import { ceilingVerticalExtent, wallVerticalExtent } from "../model/wall";
import { openingInterval, openingVerticalExtent } from "../geometry/opening";
import { projectToModel3d } from "./toWalls3d";
import { loadEngine3d } from "../engine/engine3d";
import type { SectionCutStyle } from "./generatePlan";
import { resolveCeilingSectionStyle, resolveWallSectionStyle } from "./generatePlan";
import { resolveCeilingSectionStyle, resolveHatch, resolveWallSectionStyle } from "./generatePlan";
import type { HatchRender } from "./generatePlan";
/** Bauteil-Referenz eines Cut-Polygons/einer Kante (Art + Eingabe-Index). */
@@ -209,21 +208,92 @@ function attachCutStyles(output: SectionOutput, project: Project): void {
if (output.cutPolygons.length === 0) return;
const walls = wallSegmentOwners(project);
const slabs = slabSegmentOwners(project);
const next: SectionCutPolygon[] = [];
for (const cp of output.cutPolygons) {
const ref = cp.component;
let style: SectionCutStyle | null = null;
if (ref.kind === "wall") {
const owner = walls[ref.index];
if (owner) style = resolveWallSectionStyle(project, owner);
} else {
const owner = slabs[ref.index];
if (owner) style = resolveCeilingSectionStyle(project, owner);
const style = owner ? resolveWallSectionStyle(project, owner) : null;
if (style) {
cp.fill = style.fill;
cp.hatch = style.hatch;
}
next.push(cp);
continue;
}
if (style) {
cp.fill = style.fill;
cp.hatch = style.hatch;
// Decke: bei mehrschichtigem Deckentyp wird das EINE geschnittene Rechteck
// (von der WASM-Schnitt-Extraktion) in liegende Teil-Rechtecke je Schicht
// aufgeteilt (oben/OK → unten/UK) — siehe `splitSlabLayers`. Nur im
// Schnitt sichtbar (im Grundriss bleibt die Decke bewusst flächig, siehe
// `addCeilingPoche`).
const owner = slabs[ref.index];
if (owner) {
const wt = getCeilingType(project, owner);
if (wt.layers.length > 1) {
next.push(...splitSlabLayers(cp, project, wt));
continue;
}
const style = resolveCeilingSectionStyle(project, owner);
if (style) {
cp.fill = style.fill;
cp.hatch = style.hatch;
}
}
next.push(cp);
}
output.cutPolygons = next;
}
/**
* Zerlegt das EINE geschnittene Deckenrechteck (`cp`, vier Ecken in u/v-Metern)
* in liegende Teil-Rechtecke je Schicht des Deckentyps `wt` das Schnitt-
* Gegenstück zur mehrschichtigen Wand-Poché (dort im Grundriss als Bänder quer
* zur Achse; hier im Schnitt als Bänder über die Höhe v, oben/OK unten/UK,
* exakt wie der liegende Aufbau physisch stapelt). Die Modell-Schichtdicken
* werden proportional auf die tatsächlich geschnittene Höhe skaliert (falls
* `Ceiling.thickness` die Typ-Summe übersteuert). Jedes Teil-Rechteck trägt
* Füllung + Schraffur seines Bauteils; die Fugen ergeben sich implizit aus den
* aneinanderstoßenden Rechtecken (jedes bekommt in `generateSectionPlan` die
* generische Schnitt-Umrisslinie).
*/
function splitSlabLayers(
cp: SectionCutPolygon,
project: Project,
wt: ReturnType<typeof getCeilingType>,
): SectionCutPolygon[] {
const totalT = wt.layers.reduce((s, l) => s + l.thickness, 0);
const us = cp.pts.map((p) => p[0]);
const vs = cp.pts.map((p) => p[1]);
const uMin = Math.min(...us);
const uMax = Math.max(...us);
const vMin = Math.min(...vs);
const vMax = Math.max(...vs); // vMax = OK (oben), vMin = UK (unten)
const vSpan = vMax - vMin;
if (totalT <= 1e-9 || vSpan <= 1e-9) return [cp];
const scale = vSpan / totalT;
const out: SectionCutPolygon[] = [];
let acc = 0; // kumulierte Dicke von OK (oben) nach UK (unten)
for (const layer of wt.layers) {
const comp = getComponent(project, layer.componentId);
const hatch = resolveHatch(project, comp.hatchId);
const vTop = vMax - acc * scale;
acc += layer.thickness;
const vBottom = vMax - acc * scale;
out.push({
component: cp.component,
color: cp.color,
pts: [
[uMin, vTop],
[uMax, vTop],
[uMax, vBottom],
[uMin, vBottom],
],
fill: hatch.pattern === "solid" ? "#000000" : "#ffffff",
hatch,
});
}
return out;
}
/**
+56 -5
View File
@@ -734,11 +734,7 @@ export function createProjectSlice(
setProject((p) => mapCeiling(p, id, (c) => ({ ...c, ...patch }))),
setCeilingThickness: (ceilingId, thickness) =>
setProject((p) =>
!isFinite(thickness) || thickness <= 0
? p
: mapCeiling(p, ceilingId, (c) => ({ ...c, thickness })),
),
setProject((p) => setCeilingThickness(p, ceilingId, thickness)),
// ── Öffnungs-Editieren ────────────────────────────────────────────────
updateOpening: (id, patch) =>
@@ -914,6 +910,61 @@ function setWallThickness(
};
}
/**
* Setzt die Gesamtdicke einer einschichtigen Decke über ihre Deckentyp-Schicht
* analog `setWallThickness`. Wenn der aktive Aufbau-Typ einschichtig UND ein
* DEDIZIERTER Deckentyp (`ceilingTypeId`) ist, der nur von dieser Decke genutzt
* wird, wird er direkt editiert. Sonst wird ein neuer, dedizierter
* einschichtiger Deckentyp (Klon der ersten Schicht des aktuellen Aufbaus)
* erzeugt und der Decke zugewiesen. Der geteilte LEGACY-Wandtyp
* (`Ceiling.wallTypeId`) wird dabei NIE mutiert er gehört den Wänden.
*/
function setCeilingThickness(
project: Project,
ceilingId: string,
thickness: number,
): Project {
if (!isFinite(thickness) || thickness <= 0) return project;
const ceiling = (project.ceilings ?? []).find((c) => c.id === ceilingId);
if (!ceiling) return project;
const activeId = ceiling.ceilingTypeId ?? ceiling.wallTypeId;
const ct =
(project.ceilingTypes ?? []).find((t) => t.id === ceiling.ceilingTypeId) ??
project.wallTypes.find((t) => t.id === ceiling.wallTypeId);
if (!ct || ct.layers.length === 0) return project;
const single = ct.layers.length === 1;
const usedByOthers = (project.ceilings ?? []).some(
(c) => c.id !== ceilingId && (c.ceilingTypeId ?? c.wallTypeId) === activeId,
);
if (single && !usedByOthers && ceiling.ceilingTypeId) {
// Direkt die Schichtdicke des (exklusiven, einschichtigen) Deckentyps setzen.
return {
...project,
ceilingTypes: (project.ceilingTypes ?? []).map((t) =>
t.id === ct.id ? { ...t, layers: [{ ...t.layers[0], thickness }] } : t,
),
};
}
// Dedizierten einschichtigen Deckentyp erzeugen (erste Schicht klonen) und
// der Decke zuweisen — damit die Dicke nur diese Decke betrifft.
const id = `ct-${Date.now()}`;
const clone = {
id,
name: t("default.ceilingTypeSingle", { thickness: thickness.toFixed(2) }),
layers: [{ ...ct.layers[0], thickness }],
};
return {
...project,
ceilingTypes: [...(project.ceilingTypes ?? []), clone],
ceilings: (project.ceilings ?? []).map((c) =>
c.id === ceilingId ? { ...c, ceilingTypeId: id, thickness: undefined } : c,
),
};
}
// ── Resize (Skalierung auf Zielmaß um einen Anker) ───────────────────────────
/**
+6 -5
View File
@@ -94,15 +94,16 @@ export interface WallInfo {
* einer Decke gesetzt). Anzeigefertige Werte + Listen für die Dropdowns.
*/
export interface CeilingInfo {
wallTypeId: string;
/** Effektiv aktiver Aufbau-Typ: `ceilingTypeId`, sonst LEGACY-`wallTypeId`. */
typeId: string;
/** Aktuelle Gesamtdicke (Meter). */
thickness: number;
/** Ob der aktuelle Aufbau-Typ einschichtig ist (1 Schicht). */
singleLayer: boolean;
/** Grundfläche der Decke (m²). */
area: number;
/** Verfügbare Aufbau-Typ-Presets (wie WallType). */
wallTypes: WallTypeChoice[];
/** Verfügbare Deckentyp-Presets (dediziert, `project.ceilingTypes`). */
ceilingTypes: WallTypeChoice[];
/** Geschoss, dem die Decke zugeordnet ist. */
floorId: string;
floorName: string;
@@ -358,11 +359,11 @@ function ceilingSelection(project: Project, ceiling: Ceiling): Selection {
const floor = project.drawingLevels.find((z) => z.id === ceiling.floorId);
const { zBottom, zTop } = ceilingVerticalExtent(project, ceiling);
const ceilingInfo: CeilingInfo = {
wallTypeId: ceiling.wallTypeId,
typeId: ceiling.ceilingTypeId ?? ceiling.wallTypeId,
thickness,
singleLayer: wt.layers.length <= 1,
area: ceilingArea(ceiling.outline),
wallTypes: project.wallTypes.map((t) => ({
ceilingTypes: (project.ceilingTypes ?? []).map((t) => ({
id: t.id,
name: t.name,
thickness: wallTypeThickness(t),
+81 -1
View File
@@ -18,6 +18,7 @@
import { useEffect, useRef, useState } from "react";
import type {
CeilingType,
Component,
ComponentMaterial,
HatchPattern,
@@ -1201,6 +1202,70 @@ function WallStylesTab({
);
}
/**
* Tab Deckenstile" das liegende Gegenstück zu {@link WallStylesTab}: pro
* Deckentyp (`project.ceilingTypes`) die Schichten mit Dicke + einem per-Fuge
* wählbaren Linienstil (Line Manager). Gleiche Tabelle/Sentinel-Logik wie bei
* den Wandstilen nur die Ressource (`ceilingTypes` statt `wallTypes`) und der
* Handler (`onPatchCeilingType`) unterscheiden sich.
*/
function CeilingStylesTab({
project,
onPatchCeilingType,
}: {
project: Project;
onPatchCeilingType: (id: string, patch: Partial<CeilingType>) => void;
}) {
const DEFAULT = "__default__";
const jointOptions = [
{ value: DEFAULT, label: t("resources.joint.default") },
...project.lineStyles.map((l) => ({ value: l.id, label: l.name })),
];
const setJoint = (ct: CeilingType, li: number, styleId: string | undefined) => {
const layers = ct.layers.map((l, i) =>
i === li ? { ...l, jointLineStyleId: styleId } : l,
);
onPatchCeilingType(ct.id, { layers });
};
const ceilingTypes = project.ceilingTypes ?? [];
return (
<div className="res-tab">
<div className="res-hint">{t("resources.ceilingStyles.hint")}</div>
{ceilingTypes.map((ct) => (
<div key={ct.id} className="res-wallstyle">
<div className="res-wallstyle-head">{ct.name}</div>
<ResTable columns={WALLSTYLE_COLUMNS} template={WALLSTYLE_TEMPLATE}>
{ct.layers.map((layer, li) => {
const comp = project.components.find((c) => c.id === layer.componentId);
const isInner = li === ct.layers.length - 1;
return (
<ResRow key={li}>
<ResCell emphasis>{comp ? comp.name : layer.componentId}</ResCell>
<ResCell align="right">{Math.round(layer.thickness * 1000)}</ResCell>
<ResCell>
{isInner ? (
<span className="res-joint-none"></span>
) : (
<SelectField
value={layer.jointLineStyleId ?? DEFAULT}
onChange={(v) => setJoint(ct, li, v === DEFAULT ? undefined : v)}
options={jointOptions}
/>
)}
</ResCell>
</ResRow>
);
})}
</ResTable>
</div>
))}
{ceilingTypes.length === 0 && (
<EmptyState text={t("resources.ceilingStyles.empty")} />
)}
</div>
);
}
/**
* Datei-Import-Schaltfläche (verstecktes <input type=file>). Liest die gewählte
* Datei als Text und reicht ihn nach oben. Für .lin/.pat-Import.
@@ -1449,13 +1514,20 @@ function EmptyState({ text }: { text: string }) {
// ── Manager-Schublade ──────────────────────────────────────────────────────
type TabId = "components" | "hatches" | "lines" | "wallStyles" | "materials";
type TabId =
| "components"
| "hatches"
| "lines"
| "wallStyles"
| "ceilingStyles"
| "materials";
const TABS: { id: TabId; labelKey: string }[] = [
{ id: "components", labelKey: "resources.tab.components" },
{ id: "hatches", labelKey: "resources.tab.hatches" },
{ id: "lines", labelKey: "resources.tab.lines" },
{ id: "wallStyles", labelKey: "resources.tab.wallStyles" },
{ id: "ceilingStyles", labelKey: "resources.tab.ceilingStyles" },
{ id: "materials", labelKey: "resources.tab.materials" },
];
@@ -1472,6 +1544,8 @@ export interface ResourceManagerHandlers {
onDeleteLineStyle: (id: string) => void;
/** Wandstile: immutable Änderung eines Wandtyps (z. B. Schichtfugen-Stile). */
onPatchWallType: (id: string, patch: Partial<WallType>) => void;
/** Deckenstile: immutable Änderung eines Deckentyps (z. B. Schichtfugen-Stile). */
onPatchCeilingType: (id: string, patch: Partial<CeilingType>) => void;
/** Import: fügt fertige (id-lose) Linienstile/Schraffuren hinzu (.lin/.pat). */
onImportLineStyles: (styles: Omit<LineStyle, "id">[]) => void;
onImportHatches: (hatches: Omit<HatchStyle, "id">[]) => void;
@@ -1564,6 +1638,12 @@ export function ResourceManager({
onPatchWallType={handlers.onPatchWallType}
/>
)}
{tab === "ceilingStyles" && (
<CeilingStylesTab
project={project}
onPatchCeilingType={handlers.onPatchCeilingType}
/>
)}
{tab === "materials" && <MaterialsTab />}
</div>
</aside>