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

20 KiB

Design — Ebenen-Darstellung (Layer Display Settings)

Teil der Standalone-Architektur — siehe ../../ARCHITECTURE.md. Ressourcen/Stile: resources-graphics.md. Output/Pläne: plans-output.md. Kontextmenü/Inline-Editor: 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.

/** 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

/**
 * 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

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).

/** 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)

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:

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

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)

// 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:

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:

    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.