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:
@@ -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) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -480,6 +480,10 @@ export const en: Record<TranslationKey, string> = {
|
||||
"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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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). */}
|
||||
<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,
|
||||
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({
|
||||
<input
|
||||
className="objinfo-input"
|
||||
type="number"
|
||||
step={0.01}
|
||||
step={step}
|
||||
min={isFinite(min) ? min : undefined}
|
||||
value={shown}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
/** 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) ─────────────────────────────────────────
|
||||
// Anders als DimensionField zeigt dieses Feld KEINEN persistenten Host-Wert —
|
||||
// eine „aktuelle Rotation" gibt es für die Selektion generell nicht (Wand ist
|
||||
|
||||
@@ -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;
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user