diff --git a/src/App.tsx b/src/App.tsx index 5267954..9fb0b6e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3364,6 +3364,15 @@ export default function App() { onSetOpeningLintelLines: (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 // die Zuweisung wieder (Inline-Verhalten). onSetOpeningType: (typeId) => { diff --git a/src/i18n/de.ts b/src/i18n/de.ts index f28bd59..cbb6f04 100644 --- a/src/i18n/de.ts +++ b/src/i18n/de.ts @@ -481,6 +481,10 @@ export const de = { "objinfo.opening.lintelLines.innen": "Innen", "objinfo.opening.lintelLines.aussen": "Außen", "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) ─────────────────────────────── "objinfo.stair.section": "Treppe", "objinfo.stair.shape": "Grundform", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 56d7a1c..80a3a26 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -480,6 +480,10 @@ export const en: Record = { "objinfo.opening.lintelLines.innen": "Inside", "objinfo.opening.lintelLines.aussen": "Outside", "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.shape": "Shape", "objinfo.stair.shape.straight": "Straight", diff --git a/src/model/types.ts b/src/model/types.ts index 9e1ccbb..680ca84 100644 --- a/src/model/types.ts +++ b/src/model/types.ts @@ -1218,6 +1218,31 @@ export interface Opening { * Kategorie-Farbe. */ 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; } /** diff --git a/src/panels/ObjectInfoPanel.tsx b/src/panels/ObjectInfoPanel.tsx index df8a7ad..ed80ff1 100644 --- a/src/panels/ObjectInfoPanel.tsx +++ b/src/panels/ObjectInfoPanel.tsx @@ -15,6 +15,11 @@ // Texte über t(...). 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 type { TranslationKey } from "../i18n"; import { flattenCategories, formatM } from "../model/types"; @@ -30,6 +35,7 @@ import type { } from "../model/types"; import { SIA_CATEGORIES, siaLabelFull } from "../geometry/roomArea"; import { Dropdown } from "../ui/Dropdown"; +import { ColorHexField } from "../ui/ColorHexField"; import { usePanelHost } from "./host"; import type { CeilingInfo, @@ -986,6 +992,30 @@ export function OpeningSection({ min={1} 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). */} + + + )} @@ -1528,11 +1558,15 @@ function DimensionField({ value, onCommit, min = 0, + unit = "m", + step = 0.01, }: { label: string; value: number; onCommit: (v: number) => void; min?: number; + unit?: string; + step?: number; }) { // Entwurf als String, damit Zwischenzustände (leer, „1.") tippbar sind. // `null` = kein aktiver Entwurf → zeige den Host-Wert. @@ -1553,7 +1587,7 @@ function DimensionField({ setDraft(e.target.value)} @@ -1568,11 +1602,51 @@ function DimensionField({ } }} /> - m + {unit} ); } +/** 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 ( +
+ {label} + onChange({ ...value, color })} + /> + { + const w = Number(e.target.value); + if (isFinite(w) && w >= 0) onChange({ ...value, weight: w }); + }} + title={t("objinfo.lineWeight")} + /> + mm +
+ ); +} + // ── Dreh-Feld (Delta-Winkel in Grad) ───────────────────────────────────────── // Anders als DimensionField zeigt dieses Feld KEINEN persistenten Host-Wert — // eine „aktuelle Rotation" gibt es für die Selektion generell nicht (Wand ist diff --git a/src/panels/host.ts b/src/panels/host.ts index 6f6f539..abb51b5 100644 --- a/src/panels/host.ts +++ b/src/panels/host.ts @@ -311,6 +311,12 @@ export interface PanelHostValue { onSetOpeningDoorType: (doorType: "normal" | "wandoeffnung") => void; /** Setzt die Sturzlinien-Darstellung der Tür. */ 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. */ onSetOpeningType: (typeId: string) => void; /** diff --git a/src/plan/generatePlan.ts b/src/plan/generatePlan.ts index fe3e5c9..c612875 100644 --- a/src/plan/generatePlan.ts +++ b/src/plan/generatePlan.ts @@ -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 }); } } 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). out.push({ kind: "polygon", pts: sym.frame, fill: "none", - stroke: strokeColor, - strokeWidthMm: lwMm, + stroke: frameColor, + strokeWidthMm: frameWeight, hatch: NO_HATCH, greyed, 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) { - 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 // Blendrahmen → Flügelrahmen → Glas (Feindetail, hebt fein klar von mittel ab). if (detail === "fein") { for (const pts of sym.sashFrames) { 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, // fein = ZWEI anliegende Rechtecke (die beiden Stulp-Stäbe der Flügel) — // bereits tiefenkorrekt (inset-/insetFace-bewusst) in windowSymbol berechnet. + // Zählt als "Rahmen" (Laibungs-/Stossblock des Blendrahmens). for (const mark of sym.meetingMarks) { 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). @@ -2347,11 +2358,13 @@ function addOpeningSymbol( if (detail !== "grob" && wt?.sillLine) { const { faces, view } = wt.sillLine; 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") { - 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") { - 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 diff --git a/src/plan/generatePlan.windowFields.test.ts b/src/plan/generatePlan.windowFields.test.ts index cc03fc2..28ecc27 100644 --- a/src/plan/generatePlan.windowFields.test.ts +++ b/src/plan/generatePlan.windowFields.test.ts @@ -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)", () => { const shadingLineCount = (p: Project) => linesOfClass(p, "window-shading").length; diff --git a/src/state/selectionInfo.ts b/src/state/selectionInfo.ts index 0b0b325..40604b6 100644 --- a/src/state/selectionInfo.ts +++ b/src/state/selectionInfo.ts @@ -233,6 +233,10 @@ export interface OpeningInfo { /** Aufgelöste absolute UK/OK (Meter). */ zBottom: 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, zBottom: ext.zBottom, zTop: ext.zTop, + frameLine: o.frameLine, + sashLine: o.sashLine, + sillLineStyle: o.sillLineStyle, }; return { kind: "opening", diff --git a/src/styles.css b/src/styles.css index d84b13f..0b0c756 100644 --- a/src/styles.css +++ b/src/styles.css @@ -4267,6 +4267,11 @@ body { color: var(--muted); 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 ────── Die aus ObjectInfo verschobenen Sektionen (Wand/Decke/Öffnung/Treppe/Raum)