diff --git a/src/geometry/roof.test.ts b/src/geometry/roof.test.ts index ebbcaae..3cb2cf2 100644 --- a/src/geometry/roof.test.ts +++ b/src/geometry/roof.test.ts @@ -154,4 +154,30 @@ describe("roofGeometry — Überstand", () => { const bb = roofBBox(g.eaves); expect(bb).toEqual({ x0: -0.5, y0: -0.5, x1: 6.5, y1: 4.5 }); }); + + it("getrennter Traufe/Ortgang-Überstand: First entlang X -> Ortgang weitet X, Traufe Y", () => { + // ridgeAxis "x": First entlang X -> Ortgang (Giebel) an den X-Enden, + // Traufe an den Y-Seiten. overhang(Traufe)=0.3, overhangGable(Ortgang)=0.8. + const g = roofGeometry( + roof({ shape: "sattel", ridgeAxis: "x", overhang: 0.3, overhangGable: 0.8 }), + E, + ); + const bb = roofBBox(g.eaves); + expect(bb).toEqual({ x0: -0.8, y0: -0.3, x1: 6.8, y1: 4.3 }); + }); + + it("getrennter Überstand: First entlang Y -> Ortgang weitet Y, Traufe X", () => { + const g = roofGeometry( + roof({ shape: "sattel", ridgeAxis: "y", overhang: 0.3, overhangGable: 0.8 }), + E, + ); + const bb = roofBBox(g.eaves); + expect(bb).toEqual({ x0: -0.3, y0: -0.8, x1: 6.3, y1: 4.8 }); + }); + + it("overhangGable fehlt -> ringsum gleich overhang (rückwärtskompatibel)", () => { + const g = roofGeometry(roof({ shape: "sattel", overhang: 0.4 }), E); + const bb = roofBBox(g.eaves); + expect(bb).toEqual({ x0: -0.4, y0: -0.4, x1: 6.4, y1: 4.4 }); + }); }); diff --git a/src/geometry/roof.ts b/src/geometry/roof.ts index 5a49624..722ec79 100644 --- a/src/geometry/roof.ts +++ b/src/geometry/roof.ts @@ -336,11 +336,16 @@ const swapLine = (l: [Vec2, Vec2]): [Vec2, Vec2] => [swap2(l[0]), swap2(l[1])]; */ export function roofGeometry(roof: Roof, eavesZ: number): RoofGeometry { const bb = roofBBox(roof.outline); - const o = Math.max(0, roof.overhang); - let x0 = bb.x0 - o; - let y0 = bb.y0 - o; - let x1 = bb.x1 + o; - let y1 = bb.y1 + o; + const oe = Math.max(0, roof.overhang); // Traufüberstand (senkrecht zum First) + const og = Math.max(0, roof.overhangGable ?? roof.overhang); // Ortgang (entlang First) + // Der First liegt entlang der `ridgeAxis`-Achse: dort wirkt der Ortgang-, quer + // dazu der Traufüberstand. In OUTLINE-Koordinaten je nach Firstrichtung mappen. + const ox = roof.ridgeAxis === "x" ? og : oe; + const oy = roof.ridgeAxis === "x" ? oe : og; + let x0 = bb.x0 - ox; + let y0 = bb.y0 - oy; + let x1 = bb.x1 + ox; + let y1 = bb.y1 + oy; const flip = roof.ridgeAxis === "y"; if (flip) { [x0, y0, x1, y1] = [y0, x0, y1, x1]; diff --git a/src/i18n/de.ts b/src/i18n/de.ts index fc83917..5974b09 100644 --- a/src/i18n/de.ts +++ b/src/i18n/de.ts @@ -309,7 +309,8 @@ export const de = { "objinfo.roof.mansardKnee": "Knicklage (% Spannweite)", "objinfo.roof.width": "Breite (m)", "objinfo.roof.depth": "Tiefe (m)", - "objinfo.roof.overhang": "Überstand (m)", + "objinfo.roof.overhang": "Überstand Traufe (m)", + "objinfo.roof.overhangGable": "Überstand Ortgang (m)", "objinfo.roof.thickness": "Dachdicke (m)", "objinfo.roof.baseElevation": "Traufhöhe (m)", "objinfo.roof.ridgeHeight": "Firsthöhe", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 59a8aaa..9c7f5bb 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -308,7 +308,8 @@ export const en: Record = { "objinfo.roof.mansardKnee": "Knee position (% span)", "objinfo.roof.width": "Width (m)", "objinfo.roof.depth": "Depth (m)", - "objinfo.roof.overhang": "Overhang (m)", + "objinfo.roof.overhang": "Eaves overhang (m)", + "objinfo.roof.overhangGable": "Rake overhang (m)", "objinfo.roof.thickness": "Roof thickness (m)", "objinfo.roof.baseElevation": "Eaves height (m)", "objinfo.roof.ridgeHeight": "Ridge height", diff --git a/src/model/types.ts b/src/model/types.ts index 49eb836..2e99853 100644 --- a/src/model/types.ts +++ b/src/model/types.ts @@ -1062,8 +1062,13 @@ export interface Roof { * gemessen von der Traufe. Kleiner ⇒ steiler Sockel kürzer; fehlt ⇒ 0.4. */ mansardKneeRatio?: number; - /** Dachüberstand an der Traufe (Meter, ringsum); 0 = bündig mit dem Umriss. */ + /** Dachüberstand an der TRAUFE (Meter, senkrecht zum First); 0 = bündig. */ overhang: number; + /** + * Dachüberstand am ORTGANG (Giebelseite, entlang des Firsts, Meter). Fehlt ⇒ + * gleich dem Traufüberstand {@link overhang} (rückwärtskompatibel = ringsum). + */ + overhangGable?: number; /** Firstrichtung (Sattel/Walm/Mansarde/Pult): entlang der X- oder Y-Achse. */ ridgeAxis: "x" | "y"; /** Traufhöhe (absolutes Z, Meter); fehlt ⇒ Oberkante des Geschosses. */ diff --git a/src/panels/ObjectInfoPanel.tsx b/src/panels/ObjectInfoPanel.tsx index ac7652f..7d57185 100644 --- a/src/panels/ObjectInfoPanel.tsx +++ b/src/panels/ObjectInfoPanel.tsx @@ -577,10 +577,18 @@ export function RoofSection({ }); })} - {/* Überstand + Dicke (Meter). */} + {/* Überstand: Traufe (senkrecht zum First) + Ortgang (Giebelseite). */} {numField("objinfo.roof.overhang", roof.overhang, 0.05, (v) => host.onSetRoofPatch({ overhang: Math.max(0, v) }), )} + {roof.shape !== "flach" && + roof.shape !== "zelt" && + numField( + "objinfo.roof.overhangGable", + roof.overhangGable ?? roof.overhang, + 0.05, + (v) => host.onSetRoofPatch({ overhangGable: Math.max(0, v) }), + )} {numField("objinfo.roof.thickness", roof.thickness, 0.02, (v) => host.onSetRoofPatch({ thickness: Math.max(0.01, v) }), )} diff --git a/src/state/selectionInfo.ts b/src/state/selectionInfo.ts index 6096a6a..98edc48 100644 --- a/src/state/selectionInfo.ts +++ b/src/state/selectionInfo.ts @@ -88,6 +88,8 @@ export interface RoofInfo { mansardKneeRatio?: number; /** Dachüberstand an der Traufe (Meter). */ overhang: number; + /** Dachüberstand am Ortgang (Giebelseite, Meter); fehlt ⇒ = Traufüberstand. */ + overhangGable?: number; /** Firstrichtung entlang X oder Y. */ ridgeAxis: "x" | "y"; /** Dachdicke (Meter). */ @@ -636,6 +638,7 @@ function roofSelection(project: Project, roof: Roof): Selection { mansardType: roof.mansardType, mansardKneeRatio: roof.mansardKneeRatio, overhang: roof.overhang, + overhangGable: roof.overhangGable, ridgeAxis: roof.ridgeAxis, thickness: roof.thickness, baseElevation: eavesZ,