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.
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 / …),colorundlw(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,angleund dielineWeightder Musterlinien. Wird angewandt, wo ein Element keine eigene Schraffur aus einemComponentmitbringt (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:
- 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"). - 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. - 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
coloroderlwanpassen (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 → LineStyleverweist 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:
kindist additiv; bestehendeLineStyle-Daten setzen es per Migration ausdash(§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 nurlwje Code; die Umriss-Strichstärke kommt daraus, Farbe der Umrisse ist festPOCHE_STROKE.- Schraffur kommt ausschließlich aus dem
Componentder jeweiligen Schicht (resolveHatch(project, comp.hatchId)); die Ebenen-hatchwird 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 fixPOCHE_STROKE),strokeWidthMm: pen.lw * OUTLINE_DETAIL_FACTOR[detail],dash: pen.dash. → DasPrimitive„polygon" braucht ein optionalesdash?: number[] | null(Schichtfugen bleiben durchgezogen; nur die Umriss-Kontur nutztpen.dash). - Schichtfugen: behalten
POCHE_STROKEund ihre dünneLAYER_LINE_MM(interne Hilfslinien sind bewusst neutral, nicht stift-gefärbt). - Tür-Symbol / Referenzlinie:
clsbleibt, aberweightMmauspen.lw, und die PlanView darf die Stift-Farbe nutzen (door-leafetc. erhalten optional einstroke-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, heuteNO_HATCH): stattNO_HATCHnunlayerHatches.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 direktresolveLayerHatchals 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 aufundefined→ 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 bleibtctx.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:
-
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;
dashbleibt führend,kindist nur für das Dropdown.) -
Neutralen Basis-Linienstil sicherstellen — falls die Bibliothek noch keinen generischen „Standard"-Stift hat, einen
lineStylemit{ id: "ls-default", name: "Standard", kind: "solid", weight: <Ebenen-lw>, color: "#000", dash: null }anlegen. (Pro Ebene wird der Stift referenziert; die Ebenen-spezifischencolor/lwwandern in das Override, nicht in den Stil — so bleibt der Stil wiederverwendbar.) -
Pro LayerCategory
penbauen: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.
-
Pro LayerCategory
hatchbauen — aus dem altenhatch?: string:- War
hatchein gültigerHatchStyle.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).
- War
-
Deprecated-Felder belassen für eine Übergangsphase; nach Umstellung aller Lesepfade (
generatePlan,LayersPanel-Swatch, Clipboard) in einem zweiten Schrittcolor/lwausLayerCategoryund der altehatch: stringentfernen.
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/hatchintypes.ts.src/model/layerStyle.ts:PenRender,resolvePen,resolveLayerHatch,penMap,layerHatchMap;HatchRenderhierher ziehen + re-exportieren.migrateProject()(Schritte §7) + Aufruf beim Laden/Seed.npx tsc -bgrün.
Phase 2 — Generator umstellen.
generatePlannutztpenMap/layerHatchMapstattcategoryLwMap.Primitive-polygondash?, line/arcstroke?ergänzen;addWallPoche/addDoorSymbol-Signaturen aufPenRender+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ürkind: "layer"auf die PEN/HATCH-Sektionen erweitern (§6), mit Dropdowns überproject.lineStyles/project.hatches, Reset-Knöpfen, neuen i18n-Keys.layerClipboardauf{ pen, hatch }erweitern; Kopieren/Einfügen anpassen.
Phase 5 — Aufräumen.
- Deprecatete
color/lw/hatch: stringausLayerCategoryentfernen, 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.