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.
This commit is contained in:
2026-07-04 00:17:28 +02:00
parent bf3e8bf9fc
commit 87ebd91ba2
3 changed files with 178 additions and 15 deletions
+96 -3
View File
@@ -35,6 +35,20 @@ export interface LineStyle {
color: string; color: string;
/** Strichmuster in Millimetern; `null` = durchgezogen. */ /** Strichmuster in Millimetern; `null` = durchgezogen. */
dash: number[] | null; 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. */ /** Mögliche Schraffur-Muster im Plan/Schnitt. */
@@ -49,6 +63,30 @@ export type HatchPattern =
export interface HatchStyle { export interface HatchStyle {
id: string; id: string;
name: 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. */ /** Muster-Typ. */
pattern: HatchPattern; pattern: HatchPattern;
/** Grundmaßstab des Musters (1 = Standardteilung). */ /** Grundmaßstab des Musters (1 = Standardteilung). */
@@ -64,8 +102,12 @@ export interface HatchStyle {
*/ */
relativeToWall?: boolean; relativeToWall?: boolean;
/** /**
* Farbe der Musterlinien bzw. der Vollfüllung (`pattern==="solid"`). Bei * @deprecated Schraffuren tragen im Zielmodell KEINE eigene Farbe mehr — die
* `pattern==="none"` ungenutzt. * 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; color: string;
/** Optionaler Linienstil für die Musterlinien (Line Manager). */ /** Optionaler Linienstil für die Musterlinien (Line Manager). */
@@ -106,8 +148,24 @@ export interface ComponentMaterial {
export interface Component { export interface Component {
id: string; id: string;
name: 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; 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. */ /** Schnitt-Schraffur → Hatch Manager. Gilt, wo das Bauteil echt geschnitten ist. */
hatchId: string; hatchId: string;
/** /**
@@ -440,6 +498,17 @@ export interface Wall {
* nicht die Schicht-Füllfarben/Schraffuren. * nicht die Schicht-Füllfarben/Schraffuren.
*/ */
color?: string; 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 * Lage der Wandachse über die Dicke. Fehlt sie, gilt "center" (= heutiges
* Verhalten: Schichten symmetrisch T/2 … +T/2 um die Achse). * Verhalten: Schichten symmetrisch T/2 … +T/2 um die Achse).
@@ -501,6 +570,17 @@ export interface Ceiling {
* Kategorie-Farbe. * Kategorie-Farbe.
*/ */
color?: string; 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 * Vertikale Bindung der OBERKANTE (OK). Fehlt sie, sitzt die OK an der
* Oberkante des Geschosses (baseElevation + floorHeight). * Oberkante des Geschosses (baseElevation + floorHeight).
@@ -764,6 +844,19 @@ export interface Drawing2D {
lineStyleId?: string; lineStyleId?: string;
/** Optionale Schraffur für geschlossene Formen (Hatch Manager). */ /** Optionale Schraffur für geschlossene Formen (Hatch Manager). */
hatchId?: string; 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. */ /** Optionale explizite Strichfarbe; sonst Kategorie-Farbe. */
color?: string; color?: string;
/** /**
+79 -9
View File
@@ -113,12 +113,28 @@ export interface HatchRender {
pattern: HatchPattern; pattern: HatchPattern;
scale: number; scale: number;
angle: 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; color: string;
/** Linienstärke der Musterlinien in mm. */ /** Linienstärke der Musterlinien in mm. */
lineWeight: number; lineWeight: number;
/** Strichmuster der Musterlinien in mm; null = durchgezogen. */ /** Strichmuster der Musterlinien in mm; null = durchgezogen. */
dash: number[] | null; 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 * Löst eine HatchStyle-Ressource (per id) zu einer darstellungsfertigen
* {@link HatchRender} auf. Der Linienstil der Musterlinien wird, falls gesetzt, * {@link HatchRender} auf. Der Linienstil der Musterlinien wird, falls gesetzt,
* aus dem Line Manager nachgeschlagen; sonst gilt ein dünner Default. * 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 h = getHatch(project, hatchId);
const ls = h.lineStyleId ? getLineStyle(project, h.lineStyleId) : null; const ls = h.lineStyleId ? getLineStyle(project, h.lineStyleId) : null;
// Wandbezogener Winkel: `HatchRender.angle` wird von allen Renderern in der // 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, pattern: h.pattern,
scale: h.scale, scale: h.scale,
angle, angle,
color: h.color, // Vordergrund-Override zuerst, sonst der deprecated HatchStyle.color-Fallback.
color: foreground ?? h.color,
lineWeight: ls?.weight ?? 0.13, lineWeight: ls?.weight ?? 0.13,
dash: ls?.dash ?? null, 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 ──────────────────────────────────────────────────── // ── SIA-Poché-Füllung ────────────────────────────────────────────────────
// Die 2D-Poché einer (auch mehrschichtigen) Wand trägt NICHT die rohe Bauteil- // Die 2D-Poché einer (auch mehrschichtigen) Wand trägt NICHT die rohe Bauteil-
// Albedo (3D-Materialfarbe), sondern einen neutralen Hintergrund, auf dem die // 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; if (!best) return null;
const wallAngleDeg = const wallAngleDeg =
(Math.atan2(wall.end.y - wall.start.y, wall.end.x - wall.start.x) * 180) / Math.PI; (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. // Poché-Hintergrund neutral (weiß) statt Bauteil-Albedo; Vollmuster → schwarz.
const solid = hatch.pattern === "solid"; const solid = hatch.pattern === "solid";
return { return {
@@ -332,7 +390,10 @@ export function resolveCeilingSectionStyle(
const wt = getCeilingType(project, ceiling); const wt = getCeilingType(project, ceiling);
if (wt.layers.length === 0) return null; if (wt.layers.length === 0) return null;
const comp = getComponent(project, wt.layers[0].componentId); 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 { } catch {
return null; return null;
} }
@@ -726,12 +787,16 @@ function addDrawing2D(
out.push({ out.push({
kind: "polygon", kind: "polygon",
pts, 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 // Umriss zeichnen die separaten Linien-Primitive (mit Strichmuster); die
// Füllfläche selbst bleibt randlos, um doppelte Striche zu vermeiden. // Füllfläche selbst bleibt randlos, um doppelte Striche zu vermeiden.
stroke: "none", stroke: "none",
strokeWidthMm: 0, 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, greyed,
drawingId: d.id, drawingId: d.id,
}); });
@@ -916,7 +981,12 @@ function addWallPoche(
for (let li = 0; li < wt.layers.length; li++) { for (let li = 0; li < wt.layers.length; li++) {
const layer = wt.layers[li]; const layer = wt.layers[li];
const comp = getComponent(project, layer.componentId); 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 // SIA-Poché: neutraler Hintergrund (weiß) statt Bauteil-Albedo, die
// Schichtschraffur liegt darüber; Vollmuster ("solid") = schwarze Poché. // Schichtschraffur liegt darüber; Vollmuster ("solid") = schwarze Poché.
const layerSolid = layerHatch.pattern === "solid"; const layerSolid = layerHatch.pattern === "solid";
@@ -1228,7 +1298,7 @@ function addCeilingPoche(
// die Schraffur ganz (nur Fläche + Umriss). // die Schraffur ganz (nur Fläche + Umriss).
const hatch = const hatch =
detail !== "grob" && comp?.viewHatchId detail !== "grob" && comp?.viewHatchId
? resolveHatch(project, comp.viewHatchId) ? resolveHatch(project, comp.viewHatchId, undefined, resolveForeground(comp, ceiling.foreground))
: NO_HATCH; : NO_HATCH;
const solid = hatch.pattern === "solid"; const solid = hatch.pattern === "solid";
out.push({ out.push({
+3 -3
View File
@@ -21,7 +21,7 @@ import { ceilingVerticalExtent, wallVerticalExtent } from "../model/wall";
import { openingInterval, openingVerticalExtent } from "../geometry/opening"; import { openingInterval, openingVerticalExtent } from "../geometry/opening";
import { projectToModel3d } from "./toWalls3d"; import { projectToModel3d } from "./toWalls3d";
import { loadEngine3d } from "../engine/engine3d"; import { loadEngine3d } from "../engine/engine3d";
import { resolveCeilingSectionStyle, resolveHatch, resolveWallSectionStyle } from "./generatePlan"; import { resolveCeilingSectionStyle, resolveForeground, resolveHatch, resolveWallSectionStyle } from "./generatePlan";
import type { HatchRender } from "./generatePlan"; import type { HatchRender } from "./generatePlan";
/** Bauteil-Referenz eines Cut-Polygons/einer Kante (Art + Eingabe-Index). */ /** 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) let acc = 0; // kumulierte Dicke von OK (oben) nach UK (unten)
for (const layer of wt.layers) { for (const layer of wt.layers) {
const comp = getComponent(project, layer.componentId); 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; const vTop = vMax - acc * scale;
acc += layer.thickness; acc += layer.thickness;
const vBottom = vMax - acc * scale; const vBottom = vMax - acc * scale;
@@ -351,7 +351,7 @@ function splitWallLayers(
let acc = 0; // kumulierte Dicke von uMin (layers[0]) nach uMax let acc = 0; // kumulierte Dicke von uMin (layers[0]) nach uMax
for (const layer of wt.layers) { for (const layer of wt.layers) {
const comp = getComponent(project, layer.componentId); 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; const uLeft = uMin + acc * scale;
acc += layer.thickness; acc += layer.thickness;
const uRight = uMin + acc * scale; const uRight = uMin + acc * scale;