Files
DOSSIER-STANDALONE/docs/design/layer-display-settings.md
karim ca859c4aa4 Browser-BIM (cad): semantisches Modell, abgeleitete 2D/3D-Sichten, Zeichenwerkzeuge
Standalone-Browser-Port von DOSSIER. Enthaelt das semantische Modell mit
Plan-/3D-Ableitung, Zeichen- und Editierwerkzeuge, Rhino-artiges Befehlssystem,
dockbares Panel-System, Resource-Manager, DXF/.lin/.pat-Import, i18n (de/en)
sowie Projektdokumentation und Probe-Harness.
2026-06-30 20:52:27 +02:00

499 lines
20 KiB
Markdown

# 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<LineKind, number[] | null> = {
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<code, …>` über den ganzen Baum, analog zur heutigen
`categoryLwMap`:
```ts
export function penMap(project: Project): Map<string, PenRender> {
const m = new Map<string, PenRender>();
for (const c of flattenCategories(project.layers)) m.set(c.code, resolvePen(project, c));
return m;
}
export function layerHatchMap(project: Project): Map<string, HatchRender | null> {
const m = new Map<string, HatchRender | null>();
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<LayerCategory>)` 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<LayerPen["override"]>) {
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: <Ebenen-lw>, 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:
<erste Hatch-id der Bibliothek> }` (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.