diff --git a/src/i18n/de.ts b/src/i18n/de.ts index 62c4959..f28bd59 100644 --- a/src/i18n/de.ts +++ b/src/i18n/de.ts @@ -409,6 +409,12 @@ export const de = { "oed.f.transomHeight": "Oberlichthöhe (m)", "oed.f.mullionRows": "Kämpfer-/Sprossenzeilen", "oed.f.mullionCols": "Sprossen-Spalten", + "oed.f.sillLineFaces": "Auf-/Untersicht-Linie", + "oed.f.sillLineView": "Blickrichtung", + "oed.sillLine.off": "Keine", + "oed.sillLine.beide": "Beide", + "oed.sillLine.auf": "Aufsicht (von oben)", + "oed.sillLine.unter": "Untersicht (von unten)", "oed.kind.dreh": "Dreh", "oed.kind.kipp": "Kipp", "oed.kind.drehkipp": "Dreh-Kipp", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 673a225..56d7a1c 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -409,6 +409,12 @@ export const en: Record = { "oed.f.transomHeight": "Transom height (m)", "oed.f.mullionRows": "Transom/muntin rows", "oed.f.mullionCols": "Muntin columns", + "oed.f.sillLineFaces": "Top-/bottom-view line", + "oed.f.sillLineView": "View direction", + "oed.sillLine.off": "None", + "oed.sillLine.beide": "Both", + "oed.sillLine.auf": "Top view (from above)", + "oed.sillLine.unter": "Bottom view (from below)", "oed.kind.dreh": "Casement", "oed.kind.kipp": "Tilt", "oed.kind.drehkipp": "Turn-tilt", diff --git a/src/model/types.ts b/src/model/types.ts index fdf6b27..9e1ccbb 100644 --- a/src/model/types.ts +++ b/src/model/types.ts @@ -458,6 +458,15 @@ export interface WindowType { * (Meter), abgeteilt durch einen Kämpfer. 0/fehlt ⇒ kein Oberlicht. */ transomHeight?: number; + /** + * Auf-/Untersicht-Andeutung im Grundriss (mittel/fein): eine Referenzlinie an + * der gewählten Fläche(n) — sinnvoll, wenn die Öffnung die Schnittebene NICHT + * regulär durchschneidet (z. B. bodentiefes Fenster oder hoch sitzendes + * Oberlicht). `faces` wählt aussen/innen/beide, `view` die Blickrichtung: + * "auf" (Aufsicht von oben, dünn) oder "unter" (Untersicht von unten, + * gestrichelt wie eine überkopf liegende Kante). Fehlt/undefined ⇒ keine Linie. + */ + sillLine?: { faces: "aussen" | "innen" | "beide"; view: "auf" | "unter" }; /** Default-Brüstungshöhe über Wand-UK (Meter). */ defaultSillHeight: number; /** Default-Lichtbreite (Meter). */ diff --git a/src/plan/generatePlan.ts b/src/plan/generatePlan.ts index d43f704..c11f22d 100644 --- a/src/plan/generatePlan.ts +++ b/src/plan/generatePlan.ts @@ -2349,10 +2349,21 @@ function addOpeningSymbol( } } } - // Die frühere Brüstungslinie (Wandachse, unabhängig von der Rahmenposition) - // ist entfernt (Nutzer-Feedback: falsch positioniert, Dash rendert nicht - // sichtbar) — eine korrekte Andeutung kommt als Teil der geplanten Auf-/ - // Untersicht-Option (aussen/innen, je nach Einbaulage) neu. + // Auf-/Untersicht-Andeutung ({@link WindowType.sillLine}): eine Referenzlinie + // an der Rahmen-Aussen-/Innenkante (NICHT der Wandachse — ersetzt die frühere, + // falsch positionierte Brüstungslinie), nur wenn der Typ es explizit wählt. + // "auf" (Aufsicht von oben, dünn gepunktet) vs. "unter" (Untersicht von unten, + // gestrichelt wie die übrigen Überkopf-Projektionen). + if (detail !== "grob" && wt?.sillLine) { + const { faces, view } = wt.sillLine; + const dash = view === "unter" ? OVERHEAD_DASH : [0.04, 0.08]; + 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 }); + } + 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 }); + } + } // Rollladen-/Sonnenschutzkasten (P0-Minimalfassung, reine 2D-Kontur): ein // gestricheltes Rechteck ÜBER der Öffnung, ausserhalb der Wand-Aussenfläche — // Breite = `shading.box.width` (0/fehlt ⇒ Öffnungsbreite), die zweite diff --git a/src/plan/generatePlan.windowFields.test.ts b/src/plan/generatePlan.windowFields.test.ts index 1aff624..cc03fc2 100644 --- a/src/plan/generatePlan.windowFields.test.ts +++ b/src/plan/generatePlan.windowFields.test.ts @@ -287,6 +287,43 @@ describe("generatePlan — Fensterbank (window-sill) folgt der eingezogenen Rahm }); }); +describe("generatePlan — Auf-/Untersicht-Andeutung (WindowType.sillLine)", () => { + // "mittel" isoliert die sillLine-Linien sauber von der (fein-only) Fenster- + // bank-Box (dieselbe cls, andere Bedingung) — bei mittel kann Letztere nicht + // feuern, jede window-sill-Linie hier stammt also von sillLine. + it("fehlt sillLine -> keine window-sill-Linie", () => { + const p = project({ ...baseWindow, typeId: "wt1" }, [baseWindowType]); + expect(linesOfClass(p, "window-sill", "mittel").length).toBe(0); + }); + + it("faces: 'aussen' -> genau EINE Linie, an der Rahmen-Aussenkante", () => { + const wt: WindowType = { ...baseWindowType, sillLine: { faces: "aussen", view: "auf" } }; + const p = project({ ...baseWindow, typeId: "wt1" }, [wt]); + const lines = linesOfClass(p, "window-sill", "mittel"); + expect(lines.length).toBe(1); + }); + + it("faces: 'beide' -> ZWEI Linien (aussen + innen)", () => { + const wt: WindowType = { ...baseWindowType, sillLine: { faces: "beide", view: "auf" } }; + const p = project({ ...baseWindow, typeId: "wt1" }, [wt]); + expect(linesOfClass(p, "window-sill", "mittel").length).toBe(2); + }); + + it("bei 'grob' nie sichtbar, unabhängig von faces", () => { + const wt: WindowType = { ...baseWindowType, sillLine: { faces: "beide", view: "auf" } }; + const p = project({ ...baseWindow, typeId: "wt1" }, [wt]); + expect(linesOfClass(p, "window-sill", "grob").length).toBe(0); + }); + + it("view: 'unter' nutzt die Überkopf-Strichelung (gestrichelt wie sonstige Überkopf-Projektionen)", () => { + const wt: WindowType = { ...baseWindowType, sillLine: { faces: "aussen", view: "unter" } }; + const p = project({ ...baseWindow, typeId: "wt1" }, [wt]); + const lines = linesOfClass(p, "window-sill", "mittel"); + expect(lines.length).toBe(1); + expect(lines[0].kind === "line" && lines[0].dash).toEqual([0.4, 0.25]); + }); +}); + describe("generatePlan — Rollladenkasten shading (Item 3)", () => { const shadingLineCount = (p: Project) => linesOfClass(p, "window-shading").length; diff --git a/src/ui/OpeningEditorDialog.tsx b/src/ui/OpeningEditorDialog.tsx index 21a9876..c5679d9 100644 --- a/src/ui/OpeningEditorDialog.tsx +++ b/src/ui/OpeningEditorDialog.tsx @@ -348,6 +348,35 @@ function BasisPanel({ min={0} onCommit={(n) => patchWt({ insetFromFace: n })} /> + + patchWt({ + sillLine: + v === "off" + ? undefined + : { faces: v as "aussen" | "innen" | "beide", view: wt.sillLine?.view ?? "auf" }, + }) + } + /> + {wt.sillLine && ( + patchWt({ sillLine: { faces: wt.sillLine!.faces, view: v } })} + /> + )} ); }