Fenster: Farbe/Strichstärke je Linien-Kategorie im Attribute-Panel einstellbar

Neue per-Öffnung-Übersteuerung (Opening.frameLine/sashLine/sillLineStyle,
je {color?, weight?}) für Blendrahmen+Stulp-/Laibungsblöcke, Flügelrahmen/
Sprossen und die Auf-/Untersicht-Sims-Andeutung separat. Fehlt eine
Übersteuerung, gilt weiterhin der bisherige Default (Opening.color/Haarlinie).
UI: drei neue Zeilen (Farbfeld + Strichstärke) im Objekt-Info-Panel bei
selektiertem Fenster.
This commit is contained in:
2026-07-12 18:45:28 +02:00
parent 441e4a319f
commit d2758efc8f
10 changed files with 212 additions and 10 deletions
+9
View File
@@ -3364,6 +3364,15 @@ export default function App() {
onSetOpeningLintelLines: (lintelLines) => { onSetOpeningLintelLines: (lintelLines) => {
if (selection?.kind === "opening") updateOpening(selection.id, { lintelLines }); if (selection?.kind === "opening") updateOpening(selection.id, { lintelLines });
}, },
onSetOpeningFrameLine: (v) => {
if (selection?.kind === "opening") updateOpening(selection.id, { frameLine: v });
},
onSetOpeningSashLine: (v) => {
if (selection?.kind === "opening") updateOpening(selection.id, { sashLine: v });
},
onSetOpeningSillLineStyle: (v) => {
if (selection?.kind === "opening") updateOpening(selection.id, { sillLineStyle: v });
},
// Typ-Zuweisung (Bibliothek): Tür-/Fenstertyp per typeId; leerer String löst // Typ-Zuweisung (Bibliothek): Tür-/Fenstertyp per typeId; leerer String löst
// die Zuweisung wieder (Inline-Verhalten). // die Zuweisung wieder (Inline-Verhalten).
onSetOpeningType: (typeId) => { onSetOpeningType: (typeId) => {
+4
View File
@@ -481,6 +481,10 @@ export const de = {
"objinfo.opening.lintelLines.innen": "Innen", "objinfo.opening.lintelLines.innen": "Innen",
"objinfo.opening.lintelLines.aussen": "Außen", "objinfo.opening.lintelLines.aussen": "Außen",
"objinfo.opening.lintelLines.beide": "Beide", "objinfo.opening.lintelLines.beide": "Beide",
"objinfo.opening.frameLine": "Rahmen",
"objinfo.opening.sashLine": "Fenster (Flügel/Sprossen)",
"objinfo.opening.sillLine": "Sims",
"objinfo.lineWeight": "Strichstärke (mm)",
// ── Treppen-Attribute (Object-Info-Panel) ─────────────────────────────── // ── Treppen-Attribute (Object-Info-Panel) ───────────────────────────────
"objinfo.stair.section": "Treppe", "objinfo.stair.section": "Treppe",
"objinfo.stair.shape": "Grundform", "objinfo.stair.shape": "Grundform",
+4
View File
@@ -480,6 +480,10 @@ export const en: Record<TranslationKey, string> = {
"objinfo.opening.lintelLines.innen": "Inside", "objinfo.opening.lintelLines.innen": "Inside",
"objinfo.opening.lintelLines.aussen": "Outside", "objinfo.opening.lintelLines.aussen": "Outside",
"objinfo.opening.lintelLines.beide": "Both", "objinfo.opening.lintelLines.beide": "Both",
"objinfo.opening.frameLine": "Frame",
"objinfo.opening.sashLine": "Window (sash/muntins)",
"objinfo.opening.sillLine": "Sill",
"objinfo.lineWeight": "Line weight (mm)",
"objinfo.stair.section": "Stair", "objinfo.stair.section": "Stair",
"objinfo.stair.shape": "Shape", "objinfo.stair.shape": "Shape",
"objinfo.stair.shape.straight": "Straight", "objinfo.stair.shape.straight": "Straight",
+25
View File
@@ -1218,6 +1218,31 @@ export interface Opening {
* Kategorie-Farbe. * Kategorie-Farbe.
*/ */
color?: string; color?: string;
/**
* Nur Fenster, Grundriss: Übersteuerung von Farbe/Strichstärke des
* Blendrahmens + Stulp-/Laibungsblöcke ({@link LineStyleOverride}).
* `undefined` ⇒ {@link Opening.color}, sonst Kategorie-Farbe/Haarlinie.
*/
frameLine?: LineStyleOverride;
/**
* Nur Fenster, Grundriss: Übersteuerung von Farbe/Strichstärke des
* Flügelrahmens/der Sprossen (window-sash/window-mullion).
*/
sashLine?: LineStyleOverride;
/**
* Nur Fenster, Grundriss: Übersteuerung von Farbe/Strichstärke der Auf-/
* Untersicht-Andeutung ({@link WindowType.sillLine}); nur wirksam, wenn der
* Fenstertyp dort eine Sims-Linie aktiviert hat.
*/
sillLineStyle?: LineStyleOverride;
}
/** Optionale Farb-/Strichstärke-Übersteuerung einer Linien-Kategorie im Grundriss. */
export interface LineStyleOverride {
/** Strichfarbe (Hex); fehlt ⇒ System-Default. */
color?: string;
/** Strichstärke in mm Papier; fehlt ⇒ System-Default (Haarlinie). */
weight?: number;
} }
/** /**
+76 -2
View File
@@ -15,6 +15,11 @@
// Texte über t(...). // Texte über t(...).
import { useState } from "react"; import { useState } from "react";
// Anzeige-Fallbacks für die Öffnungs-Linien-Übersteuerung (LineStyleRow) —
// dieselben Werte wie POCHE_STROKE/SYMBOL_HAIRLINE_MM in plan/generatePlan.ts
// (reine UI-Default-Anzeige, kein Re-Export nötig).
const OPENING_LINE_DEFAULT_COLOR = "#1a1a1a";
const OPENING_LINE_DEFAULT_WEIGHT = 0.02;
import { t } from "../i18n"; import { t } from "../i18n";
import type { TranslationKey } from "../i18n"; import type { TranslationKey } from "../i18n";
import { flattenCategories, formatM } from "../model/types"; import { flattenCategories, formatM } from "../model/types";
@@ -30,6 +35,7 @@ import type {
} from "../model/types"; } from "../model/types";
import { SIA_CATEGORIES, siaLabelFull } from "../geometry/roomArea"; import { SIA_CATEGORIES, siaLabelFull } from "../geometry/roomArea";
import { Dropdown } from "../ui/Dropdown"; import { Dropdown } from "../ui/Dropdown";
import { ColorHexField } from "../ui/ColorHexField";
import { usePanelHost } from "./host"; import { usePanelHost } from "./host";
import type { import type {
CeilingInfo, CeilingInfo,
@@ -986,6 +992,30 @@ export function OpeningSection({
min={1} min={1}
onCommit={(v) => host.onSetOpeningWingCount(Math.max(1, Math.min(4, Math.round(v))))} onCommit={(v) => host.onSetOpeningWingCount(Math.max(1, Math.min(4, Math.round(v))))}
/> />
{/* Grundriss-Linien je Kategorie: Rahmen (Blendrahmen+Stulp), Fenster
(Flügelrahmen/Sprossen), Sims (Auf-/Untersicht, nur wenn der
Fenstertyp sie aktiviert hat — s. WindowType.sillLine). */}
<LineStyleRow
label={t("objinfo.opening.frameLine")}
value={opening.frameLine}
fallbackColor={OPENING_LINE_DEFAULT_COLOR}
fallbackWeight={OPENING_LINE_DEFAULT_WEIGHT}
onChange={host.onSetOpeningFrameLine}
/>
<LineStyleRow
label={t("objinfo.opening.sashLine")}
value={opening.sashLine}
fallbackColor={OPENING_LINE_DEFAULT_COLOR}
fallbackWeight={OPENING_LINE_DEFAULT_WEIGHT}
onChange={host.onSetOpeningSashLine}
/>
<LineStyleRow
label={t("objinfo.opening.sillLine")}
value={opening.sillLineStyle}
fallbackColor={OPENING_LINE_DEFAULT_COLOR}
fallbackWeight={OPENING_LINE_DEFAULT_WEIGHT}
onChange={host.onSetOpeningSillLineStyle}
/>
</> </>
)} )}
@@ -1528,11 +1558,15 @@ function DimensionField({
value, value,
onCommit, onCommit,
min = 0, min = 0,
unit = "m",
step = 0.01,
}: { }: {
label: string; label: string;
value: number; value: number;
onCommit: (v: number) => void; onCommit: (v: number) => void;
min?: number; min?: number;
unit?: string;
step?: number;
}) { }) {
// Entwurf als String, damit Zwischenzustände (leer, „1.") tippbar sind. // Entwurf als String, damit Zwischenzustände (leer, „1.") tippbar sind.
// `null` = kein aktiver Entwurf → zeige den Host-Wert. // `null` = kein aktiver Entwurf → zeige den Host-Wert.
@@ -1553,7 +1587,7 @@ function DimensionField({
<input <input
className="objinfo-input" className="objinfo-input"
type="number" type="number"
step={0.01} step={step}
min={isFinite(min) ? min : undefined} min={isFinite(min) ? min : undefined}
value={shown} value={shown}
onChange={(e) => setDraft(e.target.value)} onChange={(e) => setDraft(e.target.value)}
@@ -1568,11 +1602,51 @@ function DimensionField({
} }
}} }}
/> />
<span className="objinfo-unit">m</span> <span className="objinfo-unit">{unit}</span>
</label> </label>
); );
} }
/** Farbe + Strichstärke (mm) einer Linien-Kategorie, mit "System"-Fallback-
* Anzeige (Override `undefined` ⇒ zeigt `fallbackColor`/`fallbackWeight`,
* Ändern setzt sofort einen expliziten Override). */
function LineStyleRow({
label,
value,
fallbackColor,
fallbackWeight,
onChange,
}: {
label: string;
value: { color?: string; weight?: number } | undefined;
fallbackColor: string;
fallbackWeight: number;
onChange: (v: { color?: string; weight?: number } | undefined) => void;
}) {
return (
<div className="objinfo-field">
<span className="objinfo-flabel">{label}</span>
<ColorHexField
value={value?.color ?? fallbackColor}
onChange={(color) => onChange({ ...value, color })}
/>
<input
className="objinfo-input objinfo-input-narrow"
type="number"
step={0.01}
min={0}
value={(value?.weight ?? fallbackWeight).toFixed(2)}
onChange={(e) => {
const w = Number(e.target.value);
if (isFinite(w) && w >= 0) onChange({ ...value, weight: w });
}}
title={t("objinfo.lineWeight")}
/>
<span className="objinfo-unit">mm</span>
</div>
);
}
// ── Dreh-Feld (Delta-Winkel in Grad) ───────────────────────────────────────── // ── Dreh-Feld (Delta-Winkel in Grad) ─────────────────────────────────────────
// Anders als DimensionField zeigt dieses Feld KEINEN persistenten Host-Wert — // Anders als DimensionField zeigt dieses Feld KEINEN persistenten Host-Wert —
// eine „aktuelle Rotation" gibt es für die Selektion generell nicht (Wand ist // eine „aktuelle Rotation" gibt es für die Selektion generell nicht (Wand ist
+6
View File
@@ -311,6 +311,12 @@ export interface PanelHostValue {
onSetOpeningDoorType: (doorType: "normal" | "wandoeffnung") => void; onSetOpeningDoorType: (doorType: "normal" | "wandoeffnung") => void;
/** Setzt die Sturzlinien-Darstellung der Tür. */ /** Setzt die Sturzlinien-Darstellung der Tür. */
onSetOpeningLintelLines: (lintelLines: "keine" | "innen" | "aussen" | "beide") => void; onSetOpeningLintelLines: (lintelLines: "keine" | "innen" | "aussen" | "beide") => void;
/** Übersteuert Farbe/Strichstärke des Blendrahmens + Stulp-/Laibungsblöcke. */
onSetOpeningFrameLine: (v: { color?: string; weight?: number } | undefined) => void;
/** Übersteuert Farbe/Strichstärke von Flügelrahmen/Sprossen. */
onSetOpeningSashLine: (v: { color?: string; weight?: number } | undefined) => void;
/** Übersteuert Farbe/Strichstärke der Auf-/Untersicht-Andeutung (Sims). */
onSetOpeningSillLineStyle: (v: { color?: string; weight?: number } | undefined) => void;
/** Weist der Öffnung einen Tür-/Fenstertyp (Bibliothek) zu; "" löst die Zuweisung. */ /** Weist der Öffnung einen Tür-/Fenstertyp (Bibliothek) zu; "" löst die Zuweisung. */
onSetOpeningType: (typeId: string) => void; onSetOpeningType: (typeId: string) => void;
/** /**
+21 -8
View File
@@ -2270,36 +2270,47 @@ function addOpeningSymbol(
out.push({ kind: "line", a: g[0], b: g[1], cls: "window-glass", weightMm: SYMBOL_HAIRLINE_MM, greyed, openingId: o.id }); out.push({ kind: "line", a: g[0], b: g[1], cls: "window-glass", weightMm: SYMBOL_HAIRLINE_MM, greyed, openingId: o.id });
} }
} else { } else {
// Linien-Kategorien einzeln übersteuerbar (Nutzer-Wunsch): Rahmen (Blend-
// rahmen-Kontur + Stulp-/Laibungsblöcke), Fenster (Flügelrahmen/Sprossen),
// Sims (Auf-/Untersicht-Andeutung, s. u.). `undefined` ⇒ bisheriges
// Verhalten (strokeColor/SYMBOL_HAIRLINE_MM).
const frameColor = o.frameLine?.color ?? strokeColor;
const frameWeight = o.frameLine?.weight ?? lwMm;
const frameHairline = o.frameLine?.weight ?? SYMBOL_HAIRLINE_MM;
const sashColor = o.sashLine?.color ?? strokeColor;
const sashWeight = o.sashLine?.weight ?? SYMBOL_HAIRLINE_MM;
// Blendrahmen-Rechteck (mittel + fein). // Blendrahmen-Rechteck (mittel + fein).
out.push({ out.push({
kind: "polygon", kind: "polygon",
pts: sym.frame, pts: sym.frame,
fill: "none", fill: "none",
stroke: strokeColor, stroke: frameColor,
strokeWidthMm: lwMm, strokeWidthMm: frameWeight,
hatch: NO_HATCH, hatch: NO_HATCH,
greyed, greyed,
openingId: o.id, openingId: o.id,
}); });
// Pfosten-/Flügel-Trennlinien. // Pfosten-/Flügel-Trennlinien (Alt-Pfad ohne Typ — zählt als "Fenster").
for (const [a, b] of sym.mullionLines) { for (const [a, b] of sym.mullionLines) {
out.push({ kind: "line", a, b, cls: "window-mullion", weightMm: SYMBOL_HAIRLINE_MM, color: strokeColor, greyed, openingId: o.id }); out.push({ kind: "line", a, b, cls: "window-mullion", weightMm: sashWeight, color: sashColor, greyed, openingId: o.id });
} }
// fein: Flügelrahmen (Flügelkontur) je öffenbarem Flügel — Schachtelung // fein: Flügelrahmen (Flügelkontur) je öffenbarem Flügel — Schachtelung
// Blendrahmen → Flügelrahmen → Glas (Feindetail, hebt fein klar von mittel ab). // Blendrahmen → Flügelrahmen → Glas (Feindetail, hebt fein klar von mittel ab).
if (detail === "fein") { if (detail === "fein") {
for (const pts of sym.sashFrames) { for (const pts of sym.sashFrames) {
for (let i = 0; i < pts.length; i++) { for (let i = 0; i < pts.length; i++) {
out.push({ kind: "line", a: pts[i], b: pts[(i + 1) % pts.length], cls: "window-sash", weightMm: SYMBOL_HAIRLINE_MM, color: strokeColor, greyed, openingId: o.id }); out.push({ kind: "line", a: pts[i], b: pts[(i + 1) % pts.length], cls: "window-sash", weightMm: sashWeight, color: sashColor, greyed, openingId: o.id });
} }
} }
} }
// Stulp-Mittelquadrat je Flügel-Junction (SIA-Signatur): mittel = EIN Quadrat, // Stulp-Mittelquadrat je Flügel-Junction (SIA-Signatur): mittel = EIN Quadrat,
// fein = ZWEI anliegende Rechtecke (die beiden Stulp-Stäbe der Flügel) — // fein = ZWEI anliegende Rechtecke (die beiden Stulp-Stäbe der Flügel) —
// bereits tiefenkorrekt (inset-/insetFace-bewusst) in windowSymbol berechnet. // bereits tiefenkorrekt (inset-/insetFace-bewusst) in windowSymbol berechnet.
// Zählt als "Rahmen" (Laibungs-/Stossblock des Blendrahmens).
for (const mark of sym.meetingMarks) { for (const mark of sym.meetingMarks) {
for (let i = 0; i < mark.length; i++) { for (let i = 0; i < mark.length; i++) {
out.push({ kind: "line", a: mark[i], b: mark[(i + 1) % mark.length], cls: "window-stulp", weightMm: SYMBOL_HAIRLINE_MM, color: strokeColor, greyed, openingId: o.id }); out.push({ kind: "line", a: mark[i], b: mark[(i + 1) % mark.length], cls: "window-stulp", weightMm: frameHairline, color: frameColor, greyed, openingId: o.id });
} }
} }
// Glaslinien (Anzahl je Detailgrad/Verglasung, s. glassCount). // Glaslinien (Anzahl je Detailgrad/Verglasung, s. glassCount).
@@ -2347,11 +2358,13 @@ function addOpeningSymbol(
if (detail !== "grob" && wt?.sillLine) { if (detail !== "grob" && wt?.sillLine) {
const { faces, view } = wt.sillLine; const { faces, view } = wt.sillLine;
const dash = view === "unter" ? OVERHEAD_DASH : [0.04, 0.08]; const dash = view === "unter" ? OVERHEAD_DASH : [0.04, 0.08];
const sillColor = o.sillLineStyle?.color ?? strokeColor;
const sillWeight = o.sillLineStyle?.weight ?? SYMBOL_HAIRLINE_MM;
if (faces === "aussen" || faces === "beide") { if (faces === "aussen" || faces === "beide") {
out.push({ kind: "line", a: sym.frame[0], b: sym.frame[1], cls: "window-sill", weightMm: SYMBOL_HAIRLINE_MM, dash, color: strokeColor, greyed, openingId: o.id }); out.push({ kind: "line", a: sym.frame[0], b: sym.frame[1], cls: "window-sill", weightMm: sillWeight, dash, color: sillColor, greyed, openingId: o.id });
} }
if (faces === "innen" || faces === "beide") { if (faces === "innen" || faces === "beide") {
out.push({ kind: "line", a: sym.frame[3], b: sym.frame[2], cls: "window-sill", weightMm: SYMBOL_HAIRLINE_MM, dash, color: strokeColor, greyed, openingId: o.id }); out.push({ kind: "line", a: sym.frame[3], b: sym.frame[2], cls: "window-sill", weightMm: sillWeight, dash, color: sillColor, greyed, openingId: o.id });
} }
} }
// Rollladen-/Sonnenschutzkasten (P0-Minimalfassung, reine 2D-Kontur): ein // Rollladen-/Sonnenschutzkasten (P0-Minimalfassung, reine 2D-Kontur): ein
@@ -324,6 +324,61 @@ describe("generatePlan — Auf-/Untersicht-Andeutung (WindowType.sillLine)", ()
}); });
}); });
describe("generatePlan — Linien-Übersteuerung je Kategorie (Opening.frameLine/sashLine/sillLineStyle)", () => {
const twoSashType: WindowType = {
...baseWindowType,
sashes: [
{ kind: "fluegel", autoWidth: true, opening: "dreh", hingeSide: "left" },
{ kind: "fluegel", autoWidth: true, opening: "dreh", hingeSide: "right" },
],
sillLine: { faces: "aussen", view: "auf" },
};
it("ohne Übersteuerung: Stulp-/Sash-/Sims-Linien nutzen den Default (Opening.color bzw. Haarlinie)", () => {
const p = project({ ...baseWindow, typeId: "wt1" }, [twoSashType]);
const stulp = linesOfClass(p, "window-stulp", "mittel");
expect(stulp.length).toBeGreaterThan(0);
for (const l of stulp) expect(l.kind === "line" && l.weightMm).toBeCloseTo(0.02, 6);
});
it("frameLine übersteuert Farbe+Stärke der Stulp-/Laibungsblöcke (window-stulp)", () => {
const p = project(
{ ...baseWindow, typeId: "wt1", frameLine: { color: "#ff0000", weight: 0.5 } },
[twoSashType],
);
const stulp = linesOfClass(p, "window-stulp", "mittel");
expect(stulp.length).toBeGreaterThan(0);
for (const l of stulp) {
expect(l.kind === "line" && l.color).toBe("#ff0000");
expect(l.kind === "line" && l.weightMm).toBeCloseTo(0.5, 6);
}
});
it("sashLine übersteuert Farbe+Stärke des Flügelrahmens (window-sash, fein)", () => {
const p = project(
{ ...baseWindow, typeId: "wt1", sashLine: { color: "#00ff00", weight: 0.3 } },
[twoSashType],
);
const sash = linesOfClass(p, "window-sash", "fein");
expect(sash.length).toBeGreaterThan(0);
for (const l of sash) {
expect(l.kind === "line" && l.color).toBe("#00ff00");
expect(l.kind === "line" && l.weightMm).toBeCloseTo(0.3, 6);
}
});
it("sillLineStyle übersteuert Farbe+Stärke der Auf-/Untersicht-Linie (window-sill)", () => {
const p = project(
{ ...baseWindow, typeId: "wt1", sillLineStyle: { color: "#0000ff", weight: 0.4 } },
[twoSashType],
);
const sill = linesOfClass(p, "window-sill", "mittel");
expect(sill.length).toBe(1);
expect(sill[0].kind === "line" && sill[0].color).toBe("#0000ff");
expect(sill[0].kind === "line" && sill[0].weightMm).toBeCloseTo(0.4, 6);
});
});
describe("generatePlan — Rollladenkasten shading (Item 3)", () => { describe("generatePlan — Rollladenkasten shading (Item 3)", () => {
const shadingLineCount = (p: Project) => linesOfClass(p, "window-shading").length; const shadingLineCount = (p: Project) => linesOfClass(p, "window-shading").length;
+7
View File
@@ -233,6 +233,10 @@ export interface OpeningInfo {
/** Aufgelöste absolute UK/OK (Meter). */ /** Aufgelöste absolute UK/OK (Meter). */
zBottom: number; zBottom: number;
zTop: number; zTop: number;
/** Nur Fenster: Grundriss-Linien-Übersteuerungen je Kategorie. */
frameLine?: { color?: string; weight?: number };
sashLine?: { color?: string; weight?: number };
sillLineStyle?: { color?: string; weight?: number };
} }
/** /**
@@ -723,6 +727,9 @@ function openingSelection(project: Project, o: Opening): Selection {
typeId: o.typeId, typeId: o.typeId,
zBottom: ext.zBottom, zBottom: ext.zBottom,
zTop: ext.zTop, zTop: ext.zTop,
frameLine: o.frameLine,
sashLine: o.sashLine,
sillLineStyle: o.sillLineStyle,
}; };
return { return {
kind: "opening", kind: "opening",
+5
View File
@@ -4267,6 +4267,11 @@ body {
color: var(--muted); color: var(--muted);
font-size: 11px; font-size: 11px;
} }
.objinfo-input-narrow {
flex: 0 0 auto;
width: 48px;
min-width: 48px;
}
/* ── Element-Abschnitte IM Attribute-Panel dem .attr-Grid-Look angleichen ────── /* ── Element-Abschnitte IM Attribute-Panel dem .attr-Grid-Look angleichen ──────
Die aus ObjectInfo verschobenen Sektionen (Wand/Decke/Öffnung/Treppe/Raum) Die aus ObjectInfo verschobenen Sektionen (Wand/Decke/Öffnung/Treppe/Raum)