From fae4f6fcb6a47a19445712d8c490b9321ed20450 Mon Sep 17 00:00:00 2001 From: Karim Date: Fri, 3 Jul 2026 22:09:02 +0200 Subject: [PATCH] Deckenstile: solide oder mehrschichtige Decke MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/App.tsx | 15 +++++- src/i18n/de.ts | 5 ++ src/i18n/en.ts | 5 ++ src/model/sampleProject.ts | 26 ++++++++-- src/model/types.ts | 58 ++++++++++++++++++--- src/panels/ObjectInfoPanel.tsx | 12 ++--- src/panels/ResourcesPanel.tsx | 1 + src/panels/host.ts | 7 ++- src/plan/generatePlan.ts | 11 ++-- src/plan/toSection.ts | 92 ++++++++++++++++++++++++++++++---- src/state/projectSlice.ts | 61 ++++++++++++++++++++-- src/state/selectionInfo.ts | 11 ++-- src/ui/ResourceManager.tsx | 82 +++++++++++++++++++++++++++++- 13 files changed, 341 insertions(+), 45 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 4fa61b6..6180e37 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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) => + 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, diff --git a/src/i18n/de.ts b/src/i18n/de.ts index dc72ac9..69d2ed6 100644 --- a/src/i18n/de.ts +++ b/src/i18n/de.ts @@ -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", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index c0f2794..242af7b 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -424,6 +424,7 @@ export const en: Record = { "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 = { "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", diff --git a/src/model/sampleProject.ts b/src/model/sampleProject.ts index c406532..5f3c6aa 100644 --- a/src/model/sampleProject.ts +++ b/src/model/sampleProject.ts @@ -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 diff --git a/src/model/types.ts b/src/model/types.ts index f630c04..5c5753b 100644 --- a/src/model/types.ts +++ b/src/model/types.ts @@ -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). */ diff --git a/src/panels/ObjectInfoPanel.tsx b/src/panels/ObjectInfoPanel.tsx index 0880601..de46598 100644 --- a/src/panels/ObjectInfoPanel.tsx +++ b/src/panels/ObjectInfoPanel.tsx @@ -510,7 +510,7 @@ function CeilingSection({ host: ReturnType; }) { 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({
{t("objinfo.ceiling.preset")} 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")} diff --git a/src/panels/ResourcesPanel.tsx b/src/panels/ResourcesPanel.tsx index e73ec76..3f966ad 100644 --- a/src/panels/ResourcesPanel.tsx +++ b/src/panels/ResourcesPanel.tsx @@ -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, }; diff --git a/src/panels/host.ts b/src/panels/host.ts index 07e3eae..026475b 100644 --- a/src/panels/host.ts +++ b/src/panels/host.ts @@ -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) => void; + /** Deckenstile: immutable Änderung eines Deckentyps (Schichtfugen-Stile). */ + onPatchCeilingType: (id: string, patch: Partial) => void; /** Import fertiger (id-loser) Linienstile/Schraffuren (.lin/.pat). */ onImportLineStyles: (styles: Omit[]) => void; onImportHatches: (hatches: Omit[]) => 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). */ diff --git a/src/plan/generatePlan.ts b/src/plan/generatePlan.ts index 0a482aa..48388ba 100644 --- a/src/plan/generatePlan.ts +++ b/src/plan/generatePlan.ts @@ -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[], diff --git a/src/plan/toSection.ts b/src/plan/toSection.ts index b78ee6c..2513933 100644 --- a/src/plan/toSection.ts +++ b/src/plan/toSection.ts @@ -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, +): 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; } /** diff --git a/src/state/projectSlice.ts b/src/state/projectSlice.ts index f95146b..42eae50 100644 --- a/src/state/projectSlice.ts +++ b/src/state/projectSlice.ts @@ -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) ─────────────────────────── /** diff --git a/src/state/selectionInfo.ts b/src/state/selectionInfo.ts index 3717f33..7819493 100644 --- a/src/state/selectionInfo.ts +++ b/src/state/selectionInfo.ts @@ -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), diff --git a/src/ui/ResourceManager.tsx b/src/ui/ResourceManager.tsx index cd71d2b..ade9e3c 100644 --- a/src/ui/ResourceManager.tsx +++ b/src/ui/ResourceManager.tsx @@ -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) => 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 ( +
+
{t("resources.ceilingStyles.hint")}
+ {ceilingTypes.map((ct) => ( +
+
{ct.name}
+ + {ct.layers.map((layer, li) => { + const comp = project.components.find((c) => c.id === layer.componentId); + const isInner = li === ct.layers.length - 1; + return ( + + {comp ? comp.name : layer.componentId} + {Math.round(layer.thickness * 1000)} + + {isInner ? ( + + ) : ( + setJoint(ct, li, v === DEFAULT ? undefined : v)} + options={jointOptions} + /> + )} + + + ); + })} + +
+ ))} + {ceilingTypes.length === 0 && ( + + )} +
+ ); +} + /** * Datei-Import-Schaltfläche (verstecktes ). 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) => void; + /** Deckenstile: immutable Änderung eines Deckentyps (z. B. Schichtfugen-Stile). */ + onPatchCeilingType: (id: string, patch: Partial) => void; /** Import: fügt fertige (id-lose) Linienstile/Schraffuren hinzu (.lin/.pat). */ onImportLineStyles: (styles: Omit[]) => void; onImportHatches: (hatches: Omit[]) => void; @@ -1564,6 +1638,12 @@ export function ResourceManager({ onPatchWallType={handlers.onPatchWallType} /> )} + {tab === "ceilingStyles" && ( + + )} {tab === "materials" && }