SIA-Wandschraffuren: Beton-Kreuz, Backstein/Dämmung wandrelativ, hairline

- HatchStyle.relativeToWall: der Wandwinkel (aus wall.start/end) wird in
  addWallPoche und resolveWallSectionStyle auf hatch.angle addiert (Vorzeichen
  zur SVG-CW-Konvention passend), sodass die Schraffur der Wandachse folgt.
- sampleProject: sia-concrete (crosshatch 45° absolut), sia-brick (diagonal
  wandrelativ), sia-insulation (diagonal 90° wandrelativ, solide Haarlinie);
  Komponenten Beton/Backstein/Dämmung darauf umgehängt. Dichte Muster (scale
  0.5/0.5/0.45), hairline (Strichgewicht 0.05 → 0.6px-Boden).
- Poché-Hinterlegung: mehrschichtige Wände (und einschichtige) weiss, solid
  schwarz (Fill + Schraffurfarbe), konsistent in SVG- und glPlan-Fill; Mono
  behält Schwarz für solid. Gilt auch für den Schnitt-Cut.
- ResourceManager: Wandbezug-Schalter im Hatch-Manager. exportDxf: Dash-
  Splitting für gestrichelte Schraffuren.
This commit is contained in:
2026-07-03 19:47:03 +02:00
parent 6aaef8f46a
commit e9dc423a12
7 changed files with 187 additions and 32 deletions
+44 -1
View File
@@ -126,8 +126,13 @@ function emitPrimitive(
p.hatch.pattern !== "none" && p.hatch.pattern !== "none" &&
p.hatch.pattern !== "solid" p.hatch.pattern !== "solid"
) { ) {
const cycle = dashCycleMeters(p.hatch.dash);
for (const seg of hatchSegments(pts, p.hatch)) { for (const seg of hatchSegments(pts, p.hatch)) {
dxf.addLine(LAYER_HATCH, seg.a, seg.b); if (cycle) {
for (const d of dashSeg(seg, cycle)) dxf.addLine(LAYER_HATCH, d.a, d.b);
} else {
dxf.addLine(LAYER_HATCH, seg.a, seg.b);
}
} }
} }
break; break;
@@ -174,6 +179,44 @@ interface Seg {
b: Vec2; b: Vec2;
} }
// Strichmuster (`dash`, mm-Papier) → Zyklus in WELT-Metern. Gleiche Umrechnung
// wie der Bildschirm/GL-Pfad: 1 mm ≙ (1/0.13) Kachel-Einheiten, 1 Einheit = 1/90 m.
// `null` = durchgezogen (keine Zerlegung). Ungerade Musterlängen werden verdoppelt.
const DASH_MM_TO_M = (1 / 0.13) * (1 / 90);
function dashCycleMeters(dash: number[] | null): number[] | null {
if (!dash || dash.length === 0) return null;
const pat = dash.map((d) => Math.max(0, d) * DASH_MM_TO_M).filter((d) => d > 0);
if (pat.length === 0) return null;
return pat.length % 2 === 0 ? pat : pat.concat(pat);
}
// Zerlegt EINE Schraffurlinie in ihre durchgezogenen Teilstücke gemäß `cycle`
// ([an, aus, …], Meter). Die Phase startet je Linie neu — visuell identisch zum
// SVG-`<pattern>`, das das Muster je Kachellinie zurücksetzt.
function dashSeg(seg: Seg, cycle: number[]): Seg[] {
const out: Seg[] = [];
const len = Math.hypot(seg.b.x - seg.a.x, seg.b.y - seg.a.y);
if (len < 1e-9) return out;
const ux = (seg.b.x - seg.a.x) / len;
const uy = (seg.b.y - seg.a.y) / len;
let pos = 0;
let idx = 0;
let on = true;
while (pos < len - 1e-9) {
const step = Math.min(cycle[idx % cycle.length], len - pos);
if (on && step > 1e-9) {
out.push({
a: { x: seg.a.x + ux * pos, y: seg.a.y + uy * pos },
b: { x: seg.a.x + ux * (pos + step), y: seg.a.y + uy * (pos + step) },
});
}
pos += step;
idx++;
on = !on;
}
return out;
}
function hatchSegments( function hatchSegments(
poly: Vec2[], poly: Vec2[],
hatch: Extract<Primitive, { kind: "polygon" }>["hatch"], hatch: Extract<Primitive, { kind: "polygon" }>["hatch"],
+2
View File
@@ -510,6 +510,8 @@ export const de = {
"resources.col.pattern": "Muster", "resources.col.pattern": "Muster",
"resources.col.scale": "Maßstab", "resources.col.scale": "Maßstab",
"resources.col.angle": "Winkel °", "resources.col.angle": "Winkel °",
"resources.col.relativeToWall": "Wandbezug",
"resources.col.relativeToWall.hint": "Winkel relativ zur Wandachse — das Muster dreht mit der Wand mit",
"resources.col.lineStyle": "Linienstil", "resources.col.lineStyle": "Linienstil",
"resources.lineStyle.none": "— ohne —", "resources.lineStyle.none": "— ohne —",
"resources.delete.hatch": "Schraffur „{name}\" löschen", "resources.delete.hatch": "Schraffur „{name}\" löschen",
+2
View File
@@ -505,6 +505,8 @@ export const en: Record<TranslationKey, string> = {
"resources.col.pattern": "Pattern", "resources.col.pattern": "Pattern",
"resources.col.scale": "Scale", "resources.col.scale": "Scale",
"resources.col.angle": "Angle °", "resources.col.angle": "Angle °",
"resources.col.relativeToWall": "Wall-rel.",
"resources.col.relativeToWall.hint": "Angle relative to the wall axis — the pattern rotates with the wall",
"resources.col.lineStyle": "Line style", "resources.col.lineStyle": "Line style",
"resources.lineStyle.none": "— none —", "resources.lineStyle.none": "— none —",
"resources.delete.hatch": "Delete hatch “{name}”", "resources.delete.hatch": "Delete hatch “{name}”",
+46 -4
View File
@@ -24,6 +24,12 @@ export const sampleProject: Project = {
{ id: "thick", name: "Stark 0.5", weight: 0.5, color: "#0a0a0a", dash: null }, { id: "thick", name: "Stark 0.5", weight: 0.5, color: "#0a0a0a", dash: null },
// Schraffurlinie = schwarze Haarlinie (Grundeinstellung für alle Schraffuren). // Schraffurlinie = schwarze Haarlinie (Grundeinstellung für alle Schraffuren).
{ id: "hatch-line", name: "Schraffurlinie", weight: 0.13, color: "#1a1a1a", dash: null }, { id: "hatch-line", name: "Schraffurlinie", weight: 0.13, color: "#1a1a1a", dash: null },
// Feinste Haarlinie für die SIA-Schnittschraffuren (Beton/Backstein/Dämmung):
// Gewicht unter der Bildschirm-Untergrenze, damit die Musterlinien als dünnste
// Haarlinie (Stroke-Minimum) rendern.
{ id: "hatch-hair", name: "Schraffur-Haarlinie", weight: 0.05, color: "#1a1a1a", dash: null },
// Gestrichelte Haarlinie für die Dämmung (Striche quer durch die Wanddicke).
{ id: "hatch-dash", name: "Dämmungsstrich", weight: 0.05, color: "#1a1a1a", dash: [0.35, 0.3] },
], ],
// Schraffuren: color = Linien-/Vollfüllfarbe des Musters; angle in Grad. // Schraffuren: color = Linien-/Vollfüllfarbe des Musters; angle in Grad.
// "none" = keine Schraffur (nur Component-Füllung). // "none" = keine Schraffur (nur Component-Füllung).
@@ -67,17 +73,53 @@ export const sampleProject: Project = {
color: "#9aa0a6", color: "#9aa0a6",
lineStyleId: "thin", lineStyleId: "thin",
}, },
// ── SIA-Schnittschraffuren (Haarlinie) ──────────────────────────────────
// Dichte Teilung (scale < 1; Muster-Kachel = 8·scale) für den SIA-Look.
// Beton: Kreuzschraffur, absolut 45° (bildschirmfest, NICHT wandbezogen).
{
id: "sia-concrete",
name: "Beton (Kreuz)",
pattern: "crosshatch",
scale: 0.5,
angle: 45,
color: "#1a1a1a",
lineStyleId: "hatch-hair",
},
// Backstein: Diagonale, 45° RELATIV zur Wandachse (dreht mit der Wand mit).
{
id: "sia-brick",
name: "Backstein (Diagonal)",
pattern: "diagonal",
scale: 0.5,
angle: 45,
relativeToWall: true,
color: "#1a1a1a",
lineStyleId: "hatch-hair",
},
// Dämmung: DURCHGEZOGENE (nicht gestrichelte) Linien QUER durch die Wanddicke
// (senkrecht zur Wandachse), wandbezogen. Basiswinkel 0 = Musterlinie steht
// senkrecht zur Wandrichtung; durchgezogene Haarlinie, dichte Teilung.
{
id: "sia-insulation",
name: "Dämmung (Strich)",
pattern: "diagonal",
scale: 0.45,
angle: 0,
relativeToWall: true,
color: "#1a1a1a",
lineStyleId: "hatch-hair",
},
], ],
// Bauteil-Materialien: color = Poché-Füllung im Grundriss UND 3D-Diffusfarbe; // Bauteil-Materialien: color = Poché-Füllung im Grundriss UND 3D-Diffusfarbe;
// joinPriority höher = läuft am Stoß durch (für spätere T-Stöße). // joinPriority höher = läuft am Stoß durch (für spätere T-Stöße).
components: [ components: [
{ id: "render-ext", name: "Aussenputz", color: "#d8d2c7", hatchId: "none", joinPriority: 10 }, { id: "render-ext", name: "Aussenputz", color: "#d8d2c7", hatchId: "none", joinPriority: 10 },
// Weißer Grund unter der schwarzen Dämmungs-Haarlinie (Schraffur-Grundeinstellung). // Weißer Grund unter den durchgezogenen Dämmungs-Haarlinien (quer zur Wand).
{ id: "insulation", name: "Wärmedämmung", color: "#ffffff", hatchId: "insulation", joinPriority: 20 }, { id: "insulation", name: "Wärmedämmung", color: "#ffffff", hatchId: "sia-insulation", joinPriority: 20 },
{ id: "brick", name: "Backstein", color: "#8c5544", hatchId: "none", joinPriority: 50 }, { id: "brick", name: "Backstein", color: "#8c5544", hatchId: "sia-brick", joinPriority: 50 },
{ id: "render-int", name: "Innenputz", color: "#efece6", hatchId: "none", joinPriority: 10 }, { id: "render-int", name: "Innenputz", color: "#efece6", hatchId: "none", joinPriority: 10 },
// Für spätere T-Stöße: Beton als durchlaufender Backbone (höchste Priorität). // Für spätere T-Stöße: Beton als durchlaufender Backbone (höchste Priorität).
{ id: "concrete", name: "Beton", color: "#9aa0a6", hatchId: "solid-concrete", joinPriority: 100 }, { id: "concrete", name: "Beton", color: "#9aa0a6", hatchId: "sia-concrete", joinPriority: 100 },
], ],
wallTypes: [ wallTypes: [
{ {
+8
View File
@@ -55,6 +55,14 @@ export interface HatchStyle {
scale: number; scale: number;
/** Drehung des Musters in Grad. */ /** Drehung des Musters in Grad. */
angle: number; angle: number;
/**
* Wenn `true`, ist `angle` NICHT bildschirmfest, sondern relativ zur Achse der
* schraffierten Wand: das Muster dreht mit der Wandorientierung mit (z. B. eine
* Diagonale, die stets 45° zur Wand steht, oder Dämmungsstriche quer durch die
* Wanddicke). Nur die Wand-Poché wertet dies aus; ohne Wandkontext (z. B. Decke)
* degradiert es zu einem absoluten `angle`.
*/
relativeToWall?: boolean;
/** /**
* Farbe der Musterlinien bzw. der Vollfüllung (`pattern==="solid"`). Bei * Farbe der Musterlinien bzw. der Vollfüllung (`pattern==="solid"`). Bei
* `pattern==="none"` ungenutzt. * `pattern==="none"` ungenutzt.
+75 -26
View File
@@ -242,19 +242,40 @@ export type Primitive =
* {@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.
*/ */
function resolveHatch(project: Project, hatchId: string): HatchRender { function resolveHatch(project: Project, hatchId: string, wallAngleDeg?: number): 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
// SVG-Konvention (im Uhrzeigersinn, Bildschirm-Y nach unten) verstanden — der
// Modell-Renderer negiert ihn in `toModelAngleRad`. Die Wandachse `wallAngleDeg`
// ist dagegen ein MODELL-Winkel (atan2 der Richtung, gegen den Uhrzeigersinn,
// Y nach oben). Wegen der Y-Spiegelung entspricht die Wandrichtung am Bildschirm
// dem Winkel wallAngleDeg; das Muster folgt der Wand also, indem der
// Wandwinkel SUBTRAHIERT wird. Ohne Wandkontext (Decke) bleibt `angle` absolut.
const angle =
h.relativeToWall && wallAngleDeg != null ? h.angle - wallAngleDeg : h.angle;
return { return {
pattern: h.pattern, pattern: h.pattern,
scale: h.scale, scale: h.scale,
angle: h.angle, angle,
color: h.color, color: h.color,
lineWeight: ls?.weight ?? 0.13, lineWeight: ls?.weight ?? 0.13,
dash: ls?.dash ?? null, dash: ls?.dash ?? null,
}; };
} }
// ── 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
// Schraffur-Musterlinien liegen: Vollmuster ("solid") = reine SCHWARZE Poché
// (kein Linienmuster), jedes andere Muster (inkl. "none") = WEISSER Hintergrund.
const POCHE_FILL_WHITE = "#ffffff";
const POCHE_FILL_BLACK = "#000000";
/** Neutrale Poché-Füllfarbe für ein Muster: "solid" → schwarz, sonst weiß. */
function pocheFill(pattern: HatchPattern): string {
return pattern === "solid" ? POCHE_FILL_BLACK : POCHE_FILL_WHITE;
}
/** Füllfarbe + Schraffur eines aufgelösten Bauteils, fertig für eine Cut-Poché. */ /** Füllfarbe + Schraffur eines aufgelösten Bauteils, fertig für eine Cut-Poché. */
export interface SectionCutStyle { export interface SectionCutStyle {
fill: string; fill: string;
@@ -263,8 +284,8 @@ export interface SectionCutStyle {
/** /**
* Löst das tragende Bauteil (höchste `joinPriority`) einer Wand auf — dieselbe * Löst das tragende Bauteil (höchste `joinPriority`) einer Wand auf — dieselbe
* Regel wie {@link backboneColor} für die grobe Grundriss-Poché — und liefert * Regel wie {@link backbonePocheFill} für die grobe Grundriss-Poché — und liefert
* dessen Füllfarbe + echte Schraffur für die Schnitt-Poché. `null`, wenn Wandtyp * dessen Poché-Füllung + echte Schraffur für die Schnitt-Poché. `null`, wenn Wandtyp
* oder Bauteil nicht auflösbar sind (z. B. verwaistes `wallTypeId`); der * oder Bauteil nicht auflösbar sind (z. B. verwaistes `wallTypeId`); der
* Aufrufer fällt dann auf die generische Schnitt-Schraffur zurück. * Aufrufer fällt dann auf die generische Schnitt-Schraffur zurück.
*/ */
@@ -277,7 +298,15 @@ export function resolveWallSectionStyle(project: Project, wall: Wall): SectionCu
if (!best || comp.joinPriority > best.joinPriority) best = comp; if (!best || comp.joinPriority > best.joinPriority) best = comp;
} }
if (!best) return null; if (!best) return null;
return { fill: best.color, hatch: resolveHatch(project, best.hatchId) }; 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);
// Poché-Hintergrund neutral (weiß) statt Bauteil-Albedo; Vollmuster → schwarz.
const solid = hatch.pattern === "solid";
return {
fill: solid ? POCHE_FILL_BLACK : POCHE_FILL_WHITE,
hatch: solid ? { ...hatch, color: POCHE_FILL_BLACK } : hatch,
};
} catch { } catch {
return null; return null;
} }
@@ -528,17 +557,20 @@ export function generateSectionPlan(output: SectionOutput, mono = false): Plan {
}); });
const fill = cp.fill ?? rgbToHex(cp.color); const fill = cp.fill ?? rgbToHex(cp.color);
const hatch = cp.hatch ?? SECTION_HATCH; const hatch = cp.hatch ?? SECTION_HATCH;
// Mono: reines Weiss statt Bauteilfarbe, Schraffurfarbe → Tinte (nur // Mono: neutrale Poché — Weiss statt Bauteilfarbe, Schraffur-Musterlinien →
// Umriss + Schraffur-Musterlinien tragen; wie der toMono()-Postpass für // Tinte. Eine schwarze SIA-Vollpoché ("solid") BLEIBT jedoch schwarz
// die normale Poché, hier direkt inline, da Cut-Polygone kein "solid" // (Schwarz bleibt Schwarz, Weiss bleibt Weiss).
// führen). const solid = hatch.pattern === "solid";
const monoFill = solid ? POCHE_FILL_BLACK : "#ffffff";
const monoHatch =
solid ? hatch : hatch.pattern !== "none" ? { ...hatch, color: SECTION_INK } : hatch;
primitives.push({ primitives.push({
kind: "polygon", kind: "polygon",
pts, pts,
fill: mono ? "#ffffff" : fill, fill: mono ? monoFill : fill,
stroke: SECTION_INK, stroke: SECTION_INK,
strokeWidthMm: SECTION_CUT_OUTLINE_MM, strokeWidthMm: SECTION_CUT_OUTLINE_MM,
hatch: mono && hatch.pattern !== "none" ? { ...hatch, color: SECTION_INK } : hatch, hatch: mono ? monoHatch : hatch,
}); });
} }
@@ -619,14 +651,16 @@ function addContextContours(out: Primitive[], project: Project): void {
function toMono(primitives: Primitive[]): void { function toMono(primitives: Primitive[]): void {
for (const p of primitives) { for (const p of primitives) {
if (p.kind === "polygon") { if (p.kind === "polygon") {
// Vollfüllung „solid" deckte die Poché farbig — in mono auf Weiss setzen, // SIA-Poché in mono: Vollmuster „solid" ist eine SCHWARZE Poché und bleibt
// damit nur der Umriss trägt. Linien-/Kreuzschraffur bleibt, aber in Tinte. // schwarz (Schwarz bleibt Schwarz); jede andere Füllung wird weiß, die
// Linien-/Kreuzschraffur bleibt sichtbar, aber in Tinte.
if (p.hatch.pattern === "solid") { if (p.hatch.pattern === "solid") {
p.hatch = { ...p.hatch, color: "#ffffff" }; p.hatch = { ...p.hatch, color: POCHE_FILL_BLACK };
} else if (p.hatch.pattern !== "none") { p.fill = p.fill === "none" ? "none" : POCHE_FILL_BLACK;
p.hatch = { ...p.hatch, color: MONO_INK }; } else {
if (p.hatch.pattern !== "none") p.hatch = { ...p.hatch, color: MONO_INK };
p.fill = p.fill === "none" ? "none" : "#ffffff";
} }
p.fill = p.fill === "none" ? "none" : "#ffffff";
p.stroke = MONO_INK; p.stroke = MONO_INK;
} else if (p.kind === "line") { } else if (p.kind === "line") {
p.color = MONO_INK; p.color = MONO_INK;
@@ -801,6 +835,11 @@ function addWallPoche(
// GANZE Wand (alle Bänder) markiert und die PlanView die wallId nach oben // GANZE Wand (alle Bänder) markiert und die PlanView die wallId nach oben
// melden kann. // melden kann.
const wallId = wall.id; const wallId = wall.id;
// Achswinkel der Wand (Modell, Grad) — treibt wandbezogene Schraffuren
// ({@link resolveHatch}), damit z. B. eine Diagonale oder die Dämmungsstriche
// mit der Wandorientierung mitdrehen.
const wallAngleDeg =
(Math.atan2(wall.end.y - wall.start.y, wall.end.x - wall.start.x) * 180) / Math.PI;
// Strich-/Umrandungsfarbe der Wand: optionale Übersteuerung (wall.color), // Strich-/Umrandungsfarbe der Wand: optionale Übersteuerung (wall.color),
// sonst der Default-Poché-Strich. Betrifft NUR die Linienfarbe (Umriss + // sonst der Default-Poché-Strich. Betrifft NUR die Linienfarbe (Umriss +
// Schichtfugen), nicht die Schicht-Füllfarben/Schraffuren. // Schichtfugen), nicht die Schicht-Füllfarben/Schraffuren.
@@ -821,8 +860,9 @@ function addWallPoche(
const outlineMm = wallLwMm * OUTLINE_DETAIL_FACTOR[detail]; const outlineMm = wallLwMm * OUTLINE_DETAIL_FACTOR[detail];
const layerLineMm = LAYER_LINE_MM * LAYER_DETAIL_FACTOR[detail]; const layerLineMm = LAYER_LINE_MM * LAYER_DETAIL_FACTOR[detail];
// Vereinfachte Füllung (grob): Farbe des tragenden Bauteils (höchste Priorität). // Vereinfachte Füllung (grob): neutrale SIA-Poché des tragenden Bauteils
const simpleFill = backboneColor(project, wt); // (höchste Priorität) — weiß, bzw. schwarz falls dessen Muster "solid" ist.
const simpleFill = backbonePocheFill(project, wt);
// Kanten-Indizes der inneren Gehrungs-Stirnflächen (die Diagonalen am Knoten). // Kanten-Indizes der inneren Gehrungs-Stirnflächen (die Diagonalen am Knoten).
// clippedBand liefert [A.start, A.end, B.end, B.start] → Kante 1 (A.end→B.end) // clippedBand liefert [A.start, A.end, B.end, B.start] → Kante 1 (A.end→B.end)
@@ -868,13 +908,17 @@ function addWallPoche(
let off = refOff - total / 2; let off = refOff - total / 2;
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 layerHatch = resolveHatch(project, comp.hatchId, wallAngleDeg);
// SIA-Poché: neutraler Hintergrund (weiß) statt Bauteil-Albedo, die
// Schichtschraffur liegt darüber; Vollmuster ("solid") = schwarze Poché.
const layerSolid = layerHatch.pattern === "solid";
out.push({ out.push({
kind: "polygon", kind: "polygon",
pts: clippedBand(p1, p2, off, off + layer.thickness, startCut, endCut), pts: clippedBand(p1, p2, off, off + layer.thickness, startCut, endCut),
fill: comp.color, fill: layerSolid ? POCHE_FILL_BLACK : POCHE_FILL_WHITE,
stroke, stroke,
strokeWidthMm: layerLineMm, strokeWidthMm: layerLineMm,
hatch: resolveHatch(project, comp.hatchId), hatch: layerSolid ? { ...layerHatch, color: POCHE_FILL_BLACK } : layerHatch,
greyed, greyed,
// Die Diagonale am Knoten NICHT stricheln → keine 45°-Naht zwischen // Die Diagonale am Knoten NICHT stricheln → keine 45°-Naht zwischen
// gleichfarbigen Schichten der beiden Wände. // gleichfarbigen Schichten der beiden Wände.
@@ -900,15 +944,20 @@ function addWallPoche(
} }
} }
/** Farbe des tragenden Bauteils (höchste joinPriority) für die grobe Poché. */ /**
function backboneColor(project: Project, wt: Project["wallTypes"][number]): string { * Neutrale SIA-Poché-Füllung des tragenden Bauteils (höchste joinPriority) für
let best: { color: string; prio: number } | null = null; * die grobe Sammelfläche: weiß, bzw. schwarz falls dessen Schraffur "solid" ist.
* (Ersetzt die frühere rohe Bauteil-Albedo — im Plan zählt die Poché, nicht die
* 3D-Materialfarbe.)
*/
function backbonePocheFill(project: Project, wt: Project["wallTypes"][number]): string {
let best: { pattern: HatchPattern; prio: number } | null = null;
for (const layer of wt.layers) { for (const layer of wt.layers) {
const comp = getComponent(project, layer.componentId); const comp = getComponent(project, layer.componentId);
if (!best || comp.joinPriority > best.prio) if (!best || comp.joinPriority > best.prio)
best = { color: comp.color, prio: comp.joinPriority }; best = { pattern: getHatch(project, comp.hatchId).pattern, prio: comp.joinPriority };
} }
return best?.color ?? "#9aa0a6"; return best ? pocheFill(best.pattern) : POCHE_FILL_WHITE;
} }
/** /**
+10 -1
View File
@@ -860,12 +860,13 @@ const HATCH_COLUMNS: ResColumn[] = [
{ titleKey: "resources.col.pattern" }, { titleKey: "resources.col.pattern" },
{ titleKey: "resources.col.scale", align: "right" }, { titleKey: "resources.col.scale", align: "right" },
{ titleKey: "resources.col.angle", align: "right" }, { titleKey: "resources.col.angle", align: "right" },
{ titleKey: "resources.col.relativeToWall" },
{ titleKey: "resources.col.color" }, { titleKey: "resources.col.color" },
{ titleKey: "resources.col.lineStyle" }, { titleKey: "resources.col.lineStyle" },
{ titleKey: "" }, { titleKey: "" },
]; ];
const HATCH_TEMPLATE = const HATCH_TEMPLATE =
"minmax(110px, 1fr) minmax(120px, 0.9fr) 64px 60px auto minmax(120px, 0.9fr) 36px"; "minmax(110px, 1fr) minmax(120px, 0.9fr) 64px 60px 64px auto minmax(120px, 0.9fr) 36px";
function HatchRow({ function HatchRow({
hatch, hatch,
@@ -918,6 +919,14 @@ function HatchRow({
step={5} step={5}
/> />
</ResCell> </ResCell>
<ResCell>
<input
type="checkbox"
checked={!!hatch.relativeToWall}
onChange={(e) => onPatch({ relativeToWall: e.target.checked })}
title={t("resources.col.relativeToWall.hint")}
/>
</ResCell>
<ResCell> <ResCell>
<ColorField color={hatch.color} onChange={(color) => onPatch({ color })} /> <ColorField color={hatch.color} onChange={(color) => onPatch({ color })} />
</ResCell> </ResCell>