From 6288f755a8f171970f669659cb3994e4f2d3551d Mon Sep 17 00:00:00 2001 From: Karim Date: Sat, 4 Jul 2026 00:17:28 +0200 Subject: [PATCH] Schraffur/Linien: Typmodell-Fundament (additiv, backward-kompatibel) Vorbereitung fuer Vektor-/Bild-Schraffuren, Strich-/Zickzack-Linien und Vordergrund/Hintergrund-Farben ohne Verhaltensaenderung. Alle neuen Felder optional, Sample rendert identisch: - HatchStyle: kind (vector/image), lines (parallel/random), image (src/scaleX/scaleY/rotation); color deprecated, bleibt Resolve-Fallback. - LineStyle: kind (dash/zigzag), zigzag (amplitude/wavelength). - Component: foreground (Muster) + background (Fuellung); color bleibt Fallback und 3D-Albedo. - Wall/Ceiling/Drawing2D: foreground/background-Overrides (undefined = Nach System). - generatePlan: HatchRender um kind/lines/image erweitert; resolveForeground/ resolveBackground mit dokumentierter Reihenfolge (Override -> Component -> Alt-Farbe); toSection reicht Vordergrund je Schicht durch. --- src/model/types.ts | 99 ++++++++++++++++++++++++++++++++++++++-- src/plan/generatePlan.ts | 88 +++++++++++++++++++++++++++++++---- src/plan/toSection.ts | 6 +-- 3 files changed, 178 insertions(+), 15 deletions(-) diff --git a/src/model/types.ts b/src/model/types.ts index 2369b90..d6eacb2 100644 --- a/src/model/types.ts +++ b/src/model/types.ts @@ -35,6 +35,20 @@ export interface LineStyle { color: string; /** Strichmuster in Millimetern; `null` = durchgezogen. */ dash: number[] | null; + /** + * Linien-Typ (additiv, Default `undefined` ⇒ "dash"): + * • "dash" — gerade Linie mit `dash`-Strichmuster (bestehendes Verhalten). + * • "zigzag" — der Strich variiert in Y (Zickzack/Welle), Parameter in + * `zigzag`. Fehlt `kind`, gilt „dash"; die heutigen Renderer ignorieren + * `kind` und zeichnen weiterhin gerade Striche (Backward-Compat). + */ + kind?: "dash" | "zigzag"; + /** + * Parameter der Zickzack-/Wellen-Linie (nur `kind==="zigzag"`), beide in mm + * Papier: `amplitude` = Ausschlag quer zur Linie, `wavelength` = Periodenlänge + * entlang der Linie. Fehlt es, wird die Linie gerade gezeichnet. + */ + zigzag?: { amplitude: number; wavelength: number }; } /** Mögliche Schraffur-Muster im Plan/Schnitt. */ @@ -49,6 +63,30 @@ export type HatchPattern = export interface HatchStyle { id: string; name: string; + /** + * Schraffur-Typ (additiv, Default `undefined` ⇒ "vector"): + * • "vector" — Musterlinien (die bestehenden `pattern`/`scale`/`angle`/ + * `relativeToWall`/`lineStyleId` sind die Vektor-Parameter). Untermodus + * über `lines` (parallel vs. random). + * • "image" — ein Bild wird als Muster (Pattern-Fill) geladen; Parameter in + * `image` (Skalierung/Verzerrung/Rotation). + * Fehlt `kind`, gilt „vector"; heutige Renderer ignorieren `kind` und zeichnen + * weiterhin nach `pattern` (Backward-Compat). + */ + kind?: "vector" | "image"; + /** + * Untermodus einer Vektor-Schraffur (nur `kind` fehlt/"vector", Default + * `undefined` ⇒ "parallel"): + * • "parallel" — regelmäßige Teilung (bestehendes Verhalten). + * • "random" — zufällig verteilte Striche (z. B. Kies/Splitt). + */ + lines?: "parallel" | "random"; + /** + * Bild-Muster (nur `kind==="image"`). `src` = Data-URL oder Asset-Referenz; + * `scaleX`/`scaleY` = unabhängige Verzerrung in Breite/Höhe (L×B), `rotation` + * = Drehung in Grad. Bei `kind==="vector"` ungenutzt. + */ + image?: { src: string; scaleX: number; scaleY: number; rotation: number }; /** Muster-Typ. */ pattern: HatchPattern; /** Grundmaßstab des Musters (1 = Standardteilung). */ @@ -64,8 +102,12 @@ export interface HatchStyle { */ relativeToWall?: boolean; /** - * Farbe der Musterlinien bzw. der Vollfüllung (`pattern==="solid"`). Bei - * `pattern==="none"` ungenutzt. + * @deprecated Schraffuren tragen im Zielmodell KEINE eigene Farbe mehr — die + * Muster-/Vordergrundfarbe kommt vom Bauteil (`Component.foreground`) bzw. der + * Attribut-Überschreibung. Feld bleibt für die Backward-Compat-Auflösung als + * LETZTER Fallback erhalten (siehe Farb-Resolve-Reihenfolge in + * `plan/generatePlan.ts` → `resolveHatch`). Bei `pattern==="solid"` ist es die + * Vollfüllfarbe, bei `pattern==="none"` ungenutzt. */ color: string; /** Optionaler Linienstil für die Musterlinien (Line Manager). */ @@ -106,8 +148,24 @@ export interface ComponentMaterial { export interface Component { id: string; name: string; - /** Füllfarbe im Grundriss (Poché) und 3D-Diffusfarbe. */ + /** + * Füllfarbe im Grundriss (Poché) und 3D-Diffusfarbe. Bleibt bestehen; im + * Zielmodell (Vordergrund/Hintergrund, s. u.) dient sie als Fallback für + * `background`. Migrationsabsicht: neue Projekte setzen `foreground`/ + * `background`, Alt-Projekte fallen weiterhin auf `color` zurück. + */ color: string; + /** + * Vordergrundfarbe = Farbe der Muster-/Schraffurlinien (die Schraffur selbst + * trägt keine Farbe mehr). Optional; fehlt sie, greift die Resolve-Kette + * (Attribut-Override ?? Component.foreground ?? HatchStyle.color-Fallback). + */ + foreground?: string; + /** + * Hintergrundfarbe = Füllung (Poché). Optional; fehlt sie, gilt als Fallback + * `color`. Migrationsabsicht: `background` ⇐ `color`. + */ + background?: string; /** Schnitt-Schraffur → Hatch Manager. Gilt, wo das Bauteil echt geschnitten ist. */ hatchId: string; /** @@ -440,6 +498,17 @@ export interface Wall { * nicht die Schicht-Füllfarben/Schraffuren. */ color?: string; + /** + * Attribut-Override der Muster-/Schraffurfarbe (Vordergrund) DIESER Wand- + * Instanz. `undefined` = „Nach System" (erben → Component.foreground → + * HatchStyle.color-Fallback). Höchste Priorität in der Farb-Resolve-Kette. + */ + foreground?: string; + /** + * Attribut-Override der Füllfarbe (Hintergrund/Poché) DIESER Wand-Instanz. + * `undefined` = „Nach System" (erben → Component.background → Component.color). + */ + background?: string; /** * Lage der Wandachse über die Dicke. Fehlt sie, gilt "center" (= heutiges * Verhalten: Schichten symmetrisch −T/2 … +T/2 um die Achse). @@ -501,6 +570,17 @@ export interface Ceiling { * Kategorie-Farbe. */ color?: string; + /** + * Attribut-Override der Muster-/Schraffurfarbe (Vordergrund) DIESER Decken- + * Instanz. `undefined` = „Nach System" (erben → Component.foreground → + * HatchStyle.color-Fallback). + */ + foreground?: string; + /** + * Attribut-Override der Füllfarbe (Hintergrund/Poché) DIESER Decken-Instanz. + * `undefined` = „Nach System" (erben → Component.background → Component.color). + */ + background?: string; /** * Vertikale Bindung der OBERKANTE (OK). Fehlt sie, sitzt die OK an der * Oberkante des Geschosses (baseElevation + floorHeight). @@ -764,6 +844,19 @@ export interface Drawing2D { lineStyleId?: string; /** Optionale Schraffur für geschlossene Formen (Hatch Manager). */ hatchId?: string; + /** + * Attribut-Override der Muster-/Schraffurfarbe (Vordergrund) DIESER 2D-Form. + * `undefined` = „Nach System" (erben → HatchStyle.color-Fallback). Betrifft die + * Farbe der Schraffur-Musterlinien einer gefüllten Fläche (nicht den Umriss, + * der über `color`/`lineStyleId` läuft). + */ + foreground?: string; + /** + * Attribut-Override der Füllfarbe (Hintergrund) DIESER 2D-Form. `undefined` = + * „Nach System". Synonym/Nachfolger von `fillColor`; ist `background` gesetzt, + * hat es Vorrang vor `fillColor` (der Poché-Hintergrund der geschlossenen Form). + */ + background?: string; /** Optionale explizite Strichfarbe; sonst Kategorie-Farbe. */ color?: string; /** diff --git a/src/plan/generatePlan.ts b/src/plan/generatePlan.ts index 15b63b0..69361e1 100644 --- a/src/plan/generatePlan.ts +++ b/src/plan/generatePlan.ts @@ -113,12 +113,28 @@ export interface HatchRender { pattern: HatchPattern; scale: number; angle: number; - /** Farbe der Musterlinien bzw. der Vollfüllung (pattern==="solid"). */ + /** + * AUFGELÖSTE Farbe der Musterlinien bzw. der Vollfüllung (pattern==="solid"). + * Ergebnis der Farb-Resolve-Kette (siehe {@link resolveHatch}): Vordergrund = + * (Attribut-Override) ?? (Component.foreground) ?? (HatchStyle.color, Backward- + * Compat) ?? Default. Der heutige SVG-/GL-/DXF-Renderer liest weiterhin nur + * dieses Feld — die Kette ist zur Renderzeit bereits kollabiert. + */ color: string; /** Linienstärke der Musterlinien in mm. */ lineWeight: number; /** Strichmuster der Musterlinien in mm; null = durchgezogen. */ dash: number[] | null; + /** + * Schraffur-Typ (additiv). `undefined`/"vector" ⇒ Muster nach `pattern` (heutiges + * Verhalten); "image" ⇒ Bild-Muster (`image`). Bestehende Renderer ignorieren + * `kind` und zeichnen nach `pattern` → unbekannte kinds fallen auf heute zurück. + */ + kind?: "vector" | "image"; + /** Vektor-Untermodus (additiv). `undefined`/"parallel" ⇒ regelmäßig; "random" ⇒ zufällig. */ + lines?: "parallel" | "random"; + /** Bild-Muster-Parameter (nur `kind==="image"`); durchgereicht aus dem HatchStyle. */ + image?: { src: string; scaleX: number; scaleY: number; rotation: number }; } /** @@ -247,8 +263,21 @@ export type Primitive = * Löst eine HatchStyle-Ressource (per id) zu einer darstellungsfertigen * {@link HatchRender} auf. Der Linienstil der Musterlinien wird, falls gesetzt, * aus dem Line Manager nachgeschlagen; sonst gilt ein dünner Default. + * + * FARB-RESOLVE-REIHENFOLGE (Vordergrund = Muster-/Schraffurlinienfarbe): + * `foreground` (Parameter: bereits aufgelöste Attribut-Override ?? Component. + * foreground) ?? `HatchStyle.color` (deprecated Backward-Compat) . + * Der Aufrufer reicht den bereits kollabierten Vordergrund als `foreground` + * durch (siehe {@link resolveForeground}); ist er `undefined` (Sample: weder + * Attribut- noch Bauteil-Vordergrund gesetzt), greift der HatchStyle.color- + * Fallback → pixelidentisch zum bisherigen Verhalten. */ -export function resolveHatch(project: Project, hatchId: string, wallAngleDeg?: number): HatchRender { +export function resolveHatch( + project: Project, + hatchId: string, + wallAngleDeg?: number, + foreground?: string, +): 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 @@ -264,12 +293,36 @@ export function resolveHatch(project: Project, hatchId: string, wallAngleDeg?: n pattern: h.pattern, scale: h.scale, angle, - color: h.color, + // Vordergrund-Override zuerst, sonst der deprecated HatchStyle.color-Fallback. + color: foreground ?? h.color, lineWeight: ls?.weight ?? 0.13, dash: ls?.dash ?? null, + kind: h.kind, + lines: h.lines, + image: h.image, }; } +/** + * Kollabiert die Vordergrund-Farbkette (Muster-/Schraffurlinienfarbe) eines + * Bauteils zu einem einzelnen Wert für {@link resolveHatch}: + * (Attribut-Override `override`) ?? (`Component.foreground`) ?? `undefined`. + * `undefined` heißt „Nach System" — die Kette läuft dann in resolveHatch auf den + * HatchStyle.color-Backward-Compat-Fallback weiter (Sample-Verhalten). + */ +export function resolveForeground(comp: Component, override?: string): string | undefined { + return override ?? comp.foreground; +} + +/** + * Kollabiert die Hintergrund-/Füllfarbkette (Poché) eines Bauteils: + * (Attribut-Override `override`) ?? (`Component.background`) ?? `Component.color`. + * Immer definiert (endet spätestens bei `Component.color` = heutiges Verhalten). + */ +export function resolveBackground(comp: Component, override?: string): string { + return override ?? comp.background ?? comp.color; +} + // ── SIA-Poché-Füllung ──────────────────────────────────────────────────── // Die 2D-Poché einer (auch mehrschichtigen) Wand trägt NICHT die rohe Bauteil- // Albedo (3D-Materialfarbe), sondern einen neutralen Hintergrund, auf dem die @@ -306,7 +359,12 @@ export function resolveWallSectionStyle(project: Project, wall: Wall): SectionCu if (!best) return null; const wallAngleDeg = (Math.atan2(wall.end.y - wall.start.y, wall.end.x - wall.start.x) * 180) / Math.PI; - const hatch = resolveHatch(project, best.hatchId, wallAngleDeg); + const hatch = resolveHatch( + project, + best.hatchId, + wallAngleDeg, + resolveForeground(best, wall.foreground), + ); // Poché-Hintergrund neutral (weiß) statt Bauteil-Albedo; Vollmuster → schwarz. const solid = hatch.pattern === "solid"; return { @@ -332,7 +390,10 @@ export function resolveCeilingSectionStyle( const wt = getCeilingType(project, ceiling); if (wt.layers.length === 0) return null; const comp = getComponent(project, wt.layers[0].componentId); - return { fill: comp.color, hatch: resolveHatch(project, comp.hatchId) }; + return { + fill: resolveBackground(comp, ceiling.background), + hatch: resolveHatch(project, comp.hatchId, undefined, resolveForeground(comp, ceiling.foreground)), + }; } catch { return null; } @@ -726,12 +787,16 @@ function addDrawing2D( out.push({ kind: "polygon", pts, - fill: d.fillColor ?? "none", + // Hintergrund-Override hat Vorrang vor dem Alt-Feld fillColor (beide leer = + // transparente Fläche, nur Schraffur) — Sample bleibt identisch. + fill: d.background ?? d.fillColor ?? "none", // Umriss zeichnen die separaten Linien-Primitive (mit Strichmuster); die // Füllfläche selbst bleibt randlos, um doppelte Striche zu vermeiden. stroke: "none", strokeWidthMm: 0, - hatch: d.hatchId ? resolveHatch(project, d.hatchId) : NO_HATCH, + // Drawing2D trägt kein Bauteil → Vordergrund-Override direkt aus d.foreground + // (Attribut „Nach System" ⇒ undefined ⇒ HatchStyle.color-Fallback). + hatch: d.hatchId ? resolveHatch(project, d.hatchId, undefined, d.foreground) : NO_HATCH, greyed, drawingId: d.id, }); @@ -916,7 +981,12 @@ function addWallPoche( for (let li = 0; li < wt.layers.length; li++) { const layer = wt.layers[li]; const comp = getComponent(project, layer.componentId); - const layerHatch = resolveHatch(project, comp.hatchId, wallAngleDeg); + const layerHatch = resolveHatch( + project, + comp.hatchId, + wallAngleDeg, + resolveForeground(comp, wall.foreground), + ); // SIA-Poché: neutraler Hintergrund (weiß) statt Bauteil-Albedo, die // Schichtschraffur liegt darüber; Vollmuster ("solid") = schwarze Poché. const layerSolid = layerHatch.pattern === "solid"; @@ -1228,7 +1298,7 @@ function addCeilingPoche( // die Schraffur ganz (nur Fläche + Umriss). const hatch = detail !== "grob" && comp?.viewHatchId - ? resolveHatch(project, comp.viewHatchId) + ? resolveHatch(project, comp.viewHatchId, undefined, resolveForeground(comp, ceiling.foreground)) : NO_HATCH; const solid = hatch.pattern === "solid"; out.push({ diff --git a/src/plan/toSection.ts b/src/plan/toSection.ts index 64b8689..62a05da 100644 --- a/src/plan/toSection.ts +++ b/src/plan/toSection.ts @@ -21,7 +21,7 @@ import { ceilingVerticalExtent, wallVerticalExtent } from "../model/wall"; import { openingInterval, openingVerticalExtent } from "../geometry/opening"; import { projectToModel3d } from "./toWalls3d"; import { loadEngine3d } from "../engine/engine3d"; -import { resolveCeilingSectionStyle, resolveHatch, resolveWallSectionStyle } from "./generatePlan"; +import { resolveCeilingSectionStyle, resolveForeground, resolveHatch, resolveWallSectionStyle } from "./generatePlan"; import type { HatchRender } from "./generatePlan"; /** Bauteil-Referenz eines Cut-Polygons/einer Kante (Art + Eingabe-Index). */ @@ -288,7 +288,7 @@ function splitSlabLayers( 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 hatch = resolveHatch(project, comp.hatchId, undefined, resolveForeground(comp)); const vTop = vMax - acc * scale; acc += layer.thickness; const vBottom = vMax - acc * scale; @@ -351,7 +351,7 @@ function splitWallLayers( let acc = 0; // kumulierte Dicke von uMin (layers[0]) nach uMax for (const layer of wt.layers) { const comp = getComponent(project, layer.componentId); - const hatch = resolveHatch(project, comp.hatchId); + const hatch = resolveHatch(project, comp.hatchId, undefined, resolveForeground(comp)); const uLeft = uMin + acc * scale; acc += layer.thickness; const uRight = uMin + acc * scale;