# Design — Ebenen-Darstellung (Layer Display Settings) > Teil der Standalone-Architektur — siehe [../../ARCHITECTURE.md](../../ARCHITECTURE.md). > Ressourcen/Stile: [resources-graphics.md](resources-graphics.md). Output/Pläne: > [plans-output.md](plans-output.md). Kontextmenü/Inline-Editor: > [context-menu.md](context-menu.md). Dieses Dokument spezifiziert die **per-Ebene Darstellungseinstellungen** auf der `LayerCategory` (Grafik-Kategorie) und den dazugehörigen Editor „Ebeneneinstellungen…", der aus dem Ebenen-Kontextmenü geöffnet wird. Heute trägt jede `LayerCategory` nur eine flache Strichstärke (`lw`), eine `color` und eine optionale `hatch` (ein freier String, der nirgends aufgelöst wird). Das reicht nicht: Eine Ebene soll — wie in Vectorworks/DOSSIER — einen vollständigen **Stift (PEN)** und eine vollständige **Standard-Schraffur (HATCH)** definieren, die beim Rendern angewandt werden. Bezeichner englisch, Prosa/UI-Text deutsch (CONVENTIONS.md). --- ## 1. Zielbild Jede Ebene definiert zwei Darstellungs-Aspekte, die in den Grundriss-Generator einfließen: - **PEN** — Linienstil der Ebene: `type` (durchgezogen / gestrichelt / …), `color` und `lw` (Strichstärke in mm Papier). Steuert alle Umriss-/Symbol-Linien der Elemente dieser Ebene (Wand-Umriss, Tür-Symbol, Referenzlinie). - **HATCH** — Standard-Schraffur der Ebene: `pattern`, `scale`, `angle` und die `lineWeight` der Musterlinien. Wird angewandt, wo ein Element keine eigene Schraffur aus einem `Component` mitbringt (z. B. einschichtige/„grob"-Flächen, reine 2D-Zeichnungsobjekte einer Ebene). Beides folgt dem Architektur-Prinzip: **Darstellung wird beim Rendern aufgelöst, nie in die Geometrie eingebacken.** --- ## 2. Reference vs. Inline — Entscheidung Es gibt drei Modelle, ein Datum für PEN/HATCH einer Ebene zu halten: 1. **Pure inline** — die Ebene trägt `{type,color,lw}` und `{pattern,scale,angle, lineWeight}` direkt. Einfach, aber: kein Wiederverwenden, kein zentrales Ändern; widerspricht der Ressourcen-Architektur (resources-graphics.md §1: „alles verweist per id, zentral änderbar"). 2. **Pure reference** — die Ebene trägt nur `lineStyleId` / `hatchId`. Konsistent, zentral, aber unflexibel: Eine Ebene kann z. B. nicht „den Stil X, aber in ihrer eigenen Farbe" wollen, ohne einen Klon-Stil anzulegen. 3. **Reference + optionale per-Ebene Overrides** (EMPFOHLEN) — die Ebene **verweist** auf eine `LineStyle`- bzw. `HatchStyle`-Ressource und darf **einzelne Felder lokal überschreiben**. Das ist exakt das DOSSIER/Vectorworks- Muster: ein Stil als Basis, regelbasierte/lokale Overrides obendrauf (resources-graphics.md, `overrides.py`). ### Empfehlung: Reference + optionale Overrides Begründung: - **Zentrale Pflege bleibt erhalten:** Ändert man den Linienstil „Wand stark" im Line Manager, ziehen alle Ebenen nach, die ihn referenzieren und das jeweilige Feld nicht überschreiben. - **Lokale Freiheit ohne Stil-Wildwuchs:** Eine Ebene kann punktuell `color` oder `lw` anpassen (häufigster Fall: gleiche Strichart, andere Farbe), ohne einen fast identischen Stil zu duplizieren. - **Migrationsfähig:** Die heutige flache `{color, lw}` der Ebene wird zu reinen Overrides über einem neutralen Basis-Stil — verlustfrei (siehe §6). - **Konsistent mit der bestehenden Kette:** `Component → Hatch → LineStyle` verweist bereits per id; Ebenen reihen sich nahtlos ein. Die Overrides sind **sparse**: nur gesetzte Felder überschreiben. Ein leeres Override-Objekt (oder `undefined`) bedeutet „komplett dem Stil folgen". --- ## 3. Datenmodell (TS) ### 3.1 LineStyle erweitern um `type` `LineStyle` trägt heute schon `weight`, `color`, `dash`. Wir machen die Strichart explizit benennbar (statt nur via `dash`-Array), damit der Editor ein sauberes Dropdown anbietet und `dash` daraus ableiten kann. ```ts /** Benannte Strichart eines Stifts (für UI-Dropdown). */ export type LineKind = "solid" | "dashed" | "dotted" | "dashdot"; /** mm-Strichmuster je Strichart (relativ zur Papier-mm). */ export const LINE_DASH: Record = { solid: null, dashed: [0.6, 0.4], dotted: [0.1, 0.25], dashdot: [0.6, 0.25, 0.1, 0.25], }; export interface LineStyle { id: string; name: string; /** NEU: benannte Strichart; `dash` wird daraus abgeleitet, falls nicht gesetzt. */ kind: LineKind; /** Strichstärke in Millimetern (≙ Rhino PlotWeight). */ weight: number; color: string; /** Strichmuster in mm; `null` = durchgezogen. Optional — sonst aus `kind`. */ dash: number[] | null; } ``` > Hinweis: `kind` ist additiv; bestehende `LineStyle`-Daten setzen es per Migration > aus `dash` (§6). ### 3.2 PEN- und HATCH-Override-Typen ```ts /** * Per-Ebene Stift (PEN). Verweist auf einen LineStyle; einzelne Felder dürfen * lokal überschrieben werden. Alle Override-Felder optional (sparse). */ export interface LayerPen { /** Basis-Linienstil (Line Manager). */ lineStyleId: string; /** Lokale Overrides — nur gesetzte Felder gewinnen. */ override?: { kind?: LineKind; color?: string; /** Strichstärke in mm Papier. */ lw?: number; }; } /** * Per-Ebene Standard-Schraffur (HATCH). Verweist auf einen HatchStyle; einzelne * Felder dürfen lokal überschrieben werden. `enabled=false` = Ebene hat keine * Default-Schraffur (Umriss-only). */ export interface LayerHatch { /** Aktiv? false = keine Default-Schraffur dieser Ebene. */ enabled: boolean; /** Basis-Schraffur (Hatch Manager). */ hatchId: string; /** Lokale Overrides — nur gesetzte Felder gewinnen. */ override?: { pattern?: HatchPattern; scale?: number; /** Drehung in Grad. */ angle?: number; color?: string; /** Strichstärke der Musterlinien in mm Papier. */ lineWeight?: number; }; } ``` ### 3.3 LayerCategory erweitern ```ts export interface LayerCategory { code: string; name: string; visible: boolean; locked: boolean; // ── NEU: vollständige Darstellung ────────────────────────────────────────── /** Stift der Ebene (PEN) — Linien aller Elemente dieser Ebene. */ pen: LayerPen; /** Standard-Schraffur der Ebene (HATCH). */ hatch: LayerHatch; /** Unterkategorien (Baum). */ children?: LayerCategory[]; // ── DEPRECATED (nur Übergang; siehe Migration §6) ────────────────────────── /** @deprecated → pen.override.color. */ color?: string; /** @deprecated → pen.override.lw. */ lw?: number; } ``` `color` und `lw` bleiben als optionale, deprecatete Felder bestehen, bis alle Lesepfade auf den Resolver (§4) umgestellt sind, und werden dann entfernt. Die Panel-Swatch (`LayersPanel`) liest künftig die **aufgelöste** Stift-Farbe. --- ## 4. Resolver — vom Modell zur Render-Entscheidung Der Resolver löst PEN/HATCH einer Ebene gegen die Ressourcen-Bibliotheken auf und wendet die Overrides an. Er ist die **einzige** Stelle, an der „Stil + Override" zusammenfließen; Generator und Panel rufen nur ihn. ### 4.1 Aufgelöste Render-Typen `HatchRender` existiert bereits in `generatePlan.ts`. Wir ergänzen ein paralleles `PenRender` und exportieren beide Resolver aus einem neuen Modul `src/model/layerStyle.ts` (damit Panel und Generator teilen). ```ts /** Aufgelöster Stift einer Ebene — alles, was die Linie zu zeichnen braucht. */ export interface PenRender { color: string; /** Strichstärke in mm Papier. */ lw: number; /** Strichmuster in mm Papier; null = durchgezogen. */ dash: number[] | null; } // HatchRender: bereits in generatePlan.ts definiert (pattern, scale, angle, // color, lineWeight, dash). Wird nach layerStyle.ts gezogen und re-exportiert. ``` ### 4.2 Resolver-Funktionen (Pseudocode) ```ts function resolvePen(project: Project, layer: LayerCategory): PenRender { const ls = getLineStyle(project, layer.pen.lineStyleId); // wirft, falls fehlend const o = layer.pen.override ?? {}; const kind = o.kind ?? ls.kind; return { color: o.color ?? ls.color, lw: o.lw ?? ls.weight, // Override-kind setzt das dash neu; sonst Stil-dash bzw. aus kind abgeleitet. dash: o.kind ? LINE_DASH[o.kind] : (ls.dash ?? LINE_DASH[ls.kind]), }; } function resolveLayerHatch(project: Project, layer: LayerCategory): HatchRender | null { if (!layer.hatch.enabled) return null; // Ebene ohne Default-Schraffur const h = getHatch(project, layer.hatch.hatchId); // wirft, falls fehlend const o = layer.hatch.override ?? {}; // Musterlinien-Stärke: Override > LineStyle der Schraffur > Default 0.13 mm. const baseLs = h.lineStyleId ? getLineStyle(project, h.lineStyleId) : null; return { pattern: o.pattern ?? h.pattern, scale: o.scale ?? h.scale, angle: o.angle ?? h.angle, color: o.color ?? h.color, lineWeight: o.lineWeight ?? baseLs?.weight ?? 0.13, dash: baseLs?.dash ?? null, }; } ``` Beide bauen eine `Map` über den ganzen Baum, analog zur heutigen `categoryLwMap`: ```ts export function penMap(project: Project): Map { const m = new Map(); for (const c of flattenCategories(project.layers)) m.set(c.code, resolvePen(project, c)); return m; } export function layerHatchMap(project: Project): Map { const m = new Map(); for (const c of flattenCategories(project.layers)) m.set(c.code, resolveLayerHatch(project, c)); return m; } ``` --- ## 5. Einfluss auf `generatePlan` Heute (generatePlan.ts): - `categoryLwMap(project.layers)` liefert nur `lw` je Code; die Umriss-Strichstärke kommt daraus, **Farbe** der Umrisse ist fest `POCHE_STROKE`. - Schraffur kommt ausschließlich aus dem `Component` der jeweiligen Schicht (`resolveHatch(project, comp.hatchId)`); die Ebenen-`hatch` wird **nicht** genutzt. Änderungen (minimal-invasiv, additiv): ### 5.1 Pens ersetzen `lwByCode` ```ts const pens = penMap(project); // statt categoryLwMap const layerHatches = layerHatchMap(project); … const pen = pens.get(wall.categoryCode) ?? FALLBACK_PEN; // {color, lw, dash} ``` `addWallPoche` und `addDoorSymbol` bekommen statt `wallLwMm: number` / `doorLwMm: number` jeweils das ganze `pen: PenRender`: - **Wand-Umrisslinie:** `stroke: pen.color` (statt fix `POCHE_STROKE`), `strokeWidthMm: pen.lw * OUTLINE_DETAIL_FACTOR[detail]`, `dash: pen.dash`. → Das `Primitive` „polygon" braucht ein optionales `dash?: number[] | null` (Schichtfugen bleiben durchgezogen; nur die Umriss-Kontur nutzt `pen.dash`). - **Schichtfugen:** behalten `POCHE_STROKE` und ihre dünne `LAYER_LINE_MM` (interne Hilfslinien sind bewusst neutral, nicht stift-gefärbt). - **Tür-Symbol / Referenzlinie:** `cls` bleibt, aber `weightMm` aus `pen.lw`, und die PlanView darf die Stift-Farbe nutzen (`door-leaf` etc. erhalten optional ein `stroke`-Feld am line/arc-Primitive; ansonsten greift die CSS-Klasse wie bisher). ### 5.2 Default-Schraffur der Ebene Die Ebenen-Schraffur greift dort, wo **keine Component-Schraffur** vorliegt: - **`detail === "grob"`** (eine Sammelfläche, heute `NO_HATCH`): statt `NO_HATCH` nun `layerHatches.get(wall.categoryCode) ?? NO_HATCH`. So bekommt die grobe Poché die Standard-Schraffur der Ebene (z. B. ein leichtes Diagonalmuster), falls die Ebene eine definiert; sonst bleibt sie ungeschraffiert. - **mittel/fein, mehrschichtig:** unverändert — die Component-Schraffur je Schicht hat Vorrang (spezifischer als die Ebene). Die Ebenen-Schraffur ist der *Fallback*, nicht der Default-Override. - **Reine 2D-Zeichnungsobjekte** (künftige `drawing`-Ebenen-Elemente ohne Component): nutzen direkt `resolveLayerHatch` als ihre Füllschraffur. Auflöse-Reihenfolge der Schraffur einer gezeichneten Fläche: ``` Component.hatch > LayerCategory.hatch (enabled) > keine Schraffur ``` ### 5.3 Geänderte Signaturen (Zusammenfassung) ```ts // vorher: addWallPoche(out, project, wall, doors, cuts, greyed, detail, wallLwMm) function addWallPoche(out, project, wall, doors, cuts, greyed, detail, pen: PenRender, layerHatch: HatchRender | null): void // vorher: addDoorSymbol(out, wall, door, greyed, detail, doorLwMm) function addDoorSymbol(out, wall, door, greyed, detail, pen: PenRender): void ``` `Primitive` (polygon) erhält optional `dash?: number[] | null`; line/arc erhalten optional `stroke?: string`, damit Pen-Farbe durchschlagen kann (CSS-Klasse bleibt Default). --- ## 6. Editor „Ebeneneinstellungen…" Geöffnet wie heute über `layerMenuItems → openLayerEditor(code)` → `setEditor({ kind: "layer", code, x, y })`. Der bestehende `InlineEditor`-Rahmen (dunkel, am Anker, Esc/Außenklick schließt) und die `EditorField`-Zeilen bleiben; der Inhalt wächst von 3 Feldern auf zwei kompakte Abschnitte **PEN** und **HATCH**. Da der Editor jetzt mehr Felder trägt, wird er als **kompakte Sektions-Form** gestaltet (zwei Gruppen mit Trenn-Überschrift), gemäß CONVENTIONS.md UI-Konventionen (saubere Form, keine wiederholten Beschriftungen, DOSSIER-Stil, alles via `t()`). ### 6.1 Aufbau ``` ┌ Ebene 20 ───────────────── × │ Name [ Wände ] │ │ ── Stift (PEN) ────────────── │ Linienstil [ Wand stark ▾ ] ← Dropdown über project.lineStyles │ Strichart [ durchgezogen ▾ ] ← override.kind (leer = "vom Stil") │ Farbe [■] [↺] ← override.color; ↺ = Override entfernen │ Stärke [ 0.35 ] mm [↺] ← override.lw │ │ ── Schraffur (HATCH) ──────── │ [✓] aktiv │ Schraffur [ Beton ▾ ] ← Dropdown über project.hatches │ Muster [ vom Stil ▾ ] ← override.pattern │ Maßstab [ 1.00 ] [↺] │ Drehung [ 45 ] ° [↺] │ Farbe [■] [↺] │ Linienst. [ 0.13 ] mm [↺] └────────────────────────────── ``` - **Override-Semantik im UI:** Jedes Override-Feld zeigt entweder „vom Stil" (Override leer → Platzhalter mit dem aufgelösten Stil-Wert als Hint) oder einen konkreten Wert. Ein kleiner **Reset-Knopf `↺`** je Override-Feld löscht das Override (setzt es zurück auf `undefined` → Feld folgt wieder dem Stil). - **Live, kein Bestätigen:** wie der heutige Editor — jede Änderung ruft sofort `patchCategory(code, patch)`. - **i18n:** alle Labels über `t()`. Neue Keys (Beispiele): `editor.pen`, `editor.lineStyle`, `editor.lineKind`, `editor.color`, `editor.lineWeight`, `editor.hatch`, `editor.hatchEnabled`, `editor.pattern`, `editor.scale`, `editor.rotation`, `editor.fromStyle`, `editor.resetOverride`. Strichart-/Muster-Werte: `lineKind.solid`, `lineKind.dashed`, …, `hatchPattern.solid`, `hatchPattern.diagonal`, … . Menü-Label bleibt `ctx.layerSettings`. ### 6.2 Patch-Helfer `patchCategory(code, patch: Partial)` bleibt die Schnittstelle. Für die verschachtelten Overrides nutzt der Editor schmale Helfer (im App-Scope), die sparse mergen und leere Overrides auf `undefined` kollabieren: ```ts function setPenOverride(cat: LayerCategory, patch: Partial) { const next = pruneEmpty({ ...cat.pen.override, ...patch }); patchCategory(cat.code, { pen: { ...cat.pen, override: next } }); } function setHatchOverride(cat, patch) { /* analog für cat.hatch.override */ } // pruneEmpty: entfernt undefined-Felder; gibt undefined zurück, wenn leer. ``` `setLineStyleId` / `setHatchId` setzen nur die Referenz; `hatch.enabled` ist ein Checkbox-Patch. ### 6.3 „Eigenschaften kopieren / einfügen" Der bestehende `layerClipboard` (heute `{ color, lw }`) wird auf die volle Darstellung erweitert: `{ pen, hatch }` (die Override-tragenden Strukturen, ohne `code/name/visible/locked`). „Kopieren" liest `{ pen, hatch }` der Quelle, „Einfügen" patcht sie auf das Ziel. So überträgt sich der komplette Stift + Schraffur einer Ebene auf eine andere. --- ## 7. Migration bestehender Beispieldaten Bestehende Projekte/Sample-Daten haben `LayerCategory { color, lw, hatch?: string }` und `LineStyle { weight, color, dash }` (ohne `kind`). Eine reine Lese-Zeit- Migration (`migrateProject(project)`), idempotent, beim Laden: 1. **LineStyle.kind ableiten** — aus `dash`: ``` dash == null || dash.length === 0 → "solid" sonst, wenn min(dash) sehr klein → "dotted" (heuristisch) sonst → "dashed" ``` (Eine genaue Zuordnung ist nicht nötig; `dash` bleibt führend, `kind` ist nur für das Dropdown.) 2. **Neutralen Basis-Linienstil sicherstellen** — falls die Bibliothek noch keinen generischen „Standard"-Stift hat, einen `lineStyle` mit `{ id: "ls-default", name: "Standard", kind: "solid", weight: , color: "#000", dash: null }` anlegen. (Pro Ebene wird der Stift referenziert; die Ebenen-spezifischen `color`/`lw` wandern in das **Override**, nicht in den Stil — so bleibt der Stil wiederverwendbar.) 3. **Pro LayerCategory `pen` bauen:** ```ts pen = { lineStyleId: "ls-default", override: pruneEmpty({ color: cat.color, lw: cat.lw }), } ``` Damit ist die Darstellung **pixelgenau wie vorher** (gleiche Farbe, gleiche lw), nur jetzt über die Resolver-Kette. 4. **Pro LayerCategory `hatch` bauen** — aus dem alten `hatch?: string`: - War `hatch` ein gültiger `HatchStyle.id` → `{ enabled: true, hatchId: hatch }`. - War es ein Pattern-Name oder leer/unbekannt → `{ enabled: false, hatchId: }` (Referenz muss existieren, aber inaktiv). So entsteht **keine** unbeabsichtigte Schraffur (Default heute: keine). 5. **Deprecated-Felder belassen** für eine Übergangsphase; nach Umstellung aller Lesepfade (`generatePlan`, `LayersPanel`-Swatch, Clipboard) in einem zweiten Schritt `color`/`lw` aus `LayerCategory` und der alte `hatch: string` entfernen. Migration ist **idempotent**: Liegt `pen`/`hatch` bereits vor, wird die Ebene unverändert durchgereicht. --- ## 8. Build-Plan (phasiert) **Phase 1 — Datenmodell & Resolver (keine UI-Sichtbarkeit).** - `LineKind` + `LINE_DASH`, `LineStyle.kind`, `LayerPen`, `LayerHatch`, `LayerCategory.pen/hatch` in `types.ts`. - `src/model/layerStyle.ts`: `PenRender`, `resolvePen`, `resolveLayerHatch`, `penMap`, `layerHatchMap`; `HatchRender` hierher ziehen + re-exportieren. - `migrateProject()` (Schritte §7) + Aufruf beim Laden/Seed. - `npx tsc -b` grün. **Phase 2 — Generator umstellen.** - `generatePlan` nutzt `penMap`/`layerHatchMap` statt `categoryLwMap`. - `Primitive`-polygon `dash?`, line/arc `stroke?` ergänzen; `addWallPoche`/ `addDoorSymbol`-Signaturen auf `PenRender` + `HatchRender|null`. - Ebenen-Default-Schraffur in „grob" und für Schicht-lose Flächen verdrahten. - Visuell prüfen via `node scripts/probe.mjs` (Geometrie unverändert, Farben/lw identisch zur Migration). **Phase 3 — Panel.** - `LayersPanel`-Swatch liest aufgelöste Stift-Farbe (`resolvePen(...).color`). **Phase 4 — Editor.** - `InlineEditor`-Inhalt für `kind: "layer"` auf die PEN/HATCH-Sektionen erweitern (§6), mit Dropdowns über `project.lineStyles` / `project.hatches`, Reset-Knöpfen, neuen i18n-Keys. - `layerClipboard` auf `{ pen, hatch }` erweitern; Kopieren/Einfügen anpassen. **Phase 5 — Aufräumen.** - Deprecatete `color`/`lw`/`hatch: string` aus `LayerCategory` entfernen, sobald kein Lesepfad sie mehr nutzt; Sample-Daten direkt im neuen Format ablegen. --- ## 9. Offene Punkte / bewusst nicht jetzt - **Pro-Geschoss-Overrides der Ebene** (eine Ebene anders je `DrawingLevel`): nicht in dieser Iteration; das Schema gilt geschossübergreifend (types.ts). Falls später nötig, als zweite Override-Ebene über demselben Resolver. - **Regelbasierte Overrides** (resources-graphics.md, `overrides.py`): orthogonal; würden nach der Ebenen-Auflösung greifen. - **Linienstil-Endkappen/Joins** und feinere Dash-Skalierung: bleiben in der PlanView (Darstellung), nicht im Modell.