diff --git a/src/App.tsx b/src/App.tsx index 4f87797..660ad18 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2968,7 +2968,12 @@ export default function App() { // ── Wand-Attribute (nur bei selektierter Wand) ───────────────────────── onSetWallReferenceLine: (ref) => { if (selection?.kind === "wall") - updateWall(selection.id, { referenceLine: ref }); + // Benannte Linie gewählt ⇒ freien Fugen-Versatz löschen (er übersteuert sonst). + updateWall(selection.id, { referenceLine: ref, referenceOffset: undefined }); + }, + onSetWallReferenceOffset: (offset) => { + if (selection?.kind === "wall") + updateWall(selection.id, { referenceOffset: offset ?? undefined }); }, onSetWallSliceTermination: (termination) => { if (selection?.kind === "wall") diff --git a/src/i18n/de.ts b/src/i18n/de.ts index 52d9f5a..3d91741 100644 --- a/src/i18n/de.ts +++ b/src/i18n/de.ts @@ -302,6 +302,7 @@ export const de = { "objinfo.wall.refLine.left": "Außen (links)", "objinfo.wall.refLine.center": "Mitte", "objinfo.wall.refLine.right": "Innen (rechts)", + "objinfo.wall.refLine.joint": "Fuge", "objinfo.wall.sliceTermination": "An Decke enden", "objinfo.wall.sliceTermination.both": "Aus", "objinfo.wall.sliceTermination.below": "Unten", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 0f8b189..029b8b3 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -301,6 +301,7 @@ export const en: Record = { "objinfo.wall.refLine.left": "Exterior (left)", "objinfo.wall.refLine.center": "Center", "objinfo.wall.refLine.right": "Interior (right)", + "objinfo.wall.refLine.joint": "Joint", "objinfo.wall.sliceTermination": "End at ceiling", "objinfo.wall.sliceTermination.both": "Off", "objinfo.wall.sliceTermination.below": "Below", diff --git a/src/model/types.ts b/src/model/types.ts index 156f8c0..4f269ca 100644 --- a/src/model/types.ts +++ b/src/model/types.ts @@ -827,6 +827,14 @@ export interface Wall { * Verhalten: Schichten symmetrisch −T/2 … +T/2 um die Achse). */ referenceLine?: WallReferenceLine; + /** + * Freier Referenz-Versatz der Achse entlang der +n-Normalen von der Wandmitte + * (Meter). Ist er gesetzt, ÜBERSTEUERT er {@link referenceLine} — genutzt, um + * bei mehrschichtigen Wänden eine SCHICHTTRENNLINIE (Fuge zwischen zwei + * Schichten) als Referenzlinie zu wählen. +T/2 = Aussenfläche, 0 = Mitte, + * −T/2 = Innenfläche; ein interner Fugenwert liegt dazwischen. + */ + referenceOffset?: number; /** * Vertikale Bindung der Unterkante (UK). Fehlt sie, sitzt die UK auf der * baseElevation des zugehörigen Geschosses (= heutiges Verhalten). diff --git a/src/model/wall.test.ts b/src/model/wall.test.ts index 04932f6..618af64 100644 --- a/src/model/wall.test.ts +++ b/src/model/wall.test.ts @@ -2,8 +2,8 @@ // ceilingVerticalExtent): reine Datenschicht, kein React/Store. import { describe, it, expect } from "vitest"; -import { ceilingVerticalExtent } from "./wall"; -import type { Ceiling, Project } from "./types"; +import { ceilingVerticalExtent, wallReferenceOffset } from "./wall"; +import type { Ceiling, Project, Wall } from "./types"; /** Minimalprojekt mit einem Geschoss (baseElevation 0, floorHeight 2.6) und * einem einschichtigen Wandtyp (0.2 m), den die Decke als Aufbau nutzt. */ @@ -113,3 +113,24 @@ describe("ceilingVerticalExtent", () => { expect(zBottom).toBeCloseTo(2.9); // baseElevation(og)=2.8 + offset(0.1) }); }); + +describe("wallReferenceOffset", () => { + const w = (extra: Partial): Wall => ({ referenceLine: "center", ...extra } as Wall); + const T = 0.4; // Gesamtdicke → half = 0.2 + + it("bildet die benannten Linien ab (aussen/mitte/innen)", () => { + expect(wallReferenceOffset(w({ referenceLine: "left" }), T)).toBeCloseTo(0.2, 6); + expect(wallReferenceOffset(w({ referenceLine: "center" }), T)).toBeCloseTo(0, 6); + expect(wallReferenceOffset(w({ referenceLine: "right" }), T)).toBeCloseTo(-0.2, 6); + }); + + it("übersteuert die Referenzlinie mit einem freien Fugen-Versatz", () => { + // Fuge z. B. nach einer 0.05-Aussenschale: offset = half − 0.05 = 0.15. + expect(wallReferenceOffset(w({ referenceLine: "left", referenceOffset: 0.15 }), T)).toBeCloseTo(0.15, 6); + }); + + it("klemmt den Fugen-Versatz auf ±halbe Dicke", () => { + expect(wallReferenceOffset(w({ referenceOffset: 5 }), T)).toBeCloseTo(0.2, 6); + expect(wallReferenceOffset(w({ referenceOffset: -5 }), T)).toBeCloseTo(-0.2, 6); + }); +}); diff --git a/src/model/wall.ts b/src/model/wall.ts index 114a86a..2c37f27 100644 --- a/src/model/wall.ts +++ b/src/model/wall.ts @@ -26,8 +26,14 @@ export function wallReferenceOffset( wall: Wall, totalThickness: number, ): number { - const ref = wall.referenceLine ?? "center"; const half = totalThickness / 2; + // Freier Fugen-Versatz (Schichttrennlinie als Referenz) übersteuert die + // benannte Referenzlinie. Auf die Wanddicke geklemmt, damit die Achse nie + // ausserhalb der Wand landet. + if (wall.referenceOffset != null) { + return Math.max(-half, Math.min(half, wall.referenceOffset)); + } + const ref = wall.referenceLine ?? "center"; if (ref === "left") return +half; if (ref === "right") return -half; return 0; diff --git a/src/panels/ObjectInfoPanel.tsx b/src/panels/ObjectInfoPanel.tsx index dff6b77..7516472 100644 --- a/src/panels/ObjectInfoPanel.tsx +++ b/src/panels/ObjectInfoPanel.tsx @@ -906,17 +906,29 @@ export function WallSection({
{t("objinfo.wall.section")}
- {/* Referenzlinie (wo die Achse über die Dicke liegt) — früher im ObjectInfo- - Kopf, jetzt hier bei den Wand-Attributen. */} + {/* Referenzlinie (wo die Achse über die Dicke liegt) — außen/mitte/innen ODER + bei mehrschichtigen Wänden eine SCHICHTTRENNLINIE (Fuge). Ein Fugenwert + wird als "off:" kodiert und übersteuert die benannte Linie. */}
{t("objinfo.wall.refLine")} host.onSetWallReferenceLine(v as WallReferenceLine)} + value={ + wall.referenceOffset != null + ? `off:${wall.referenceOffset.toFixed(4)}` + : wall.referenceLine + } + onChange={(v) => { + if (v.startsWith("off:")) host.onSetWallReferenceOffset(Number(v.slice(4))); + else host.onSetWallReferenceLine(v as WallReferenceLine); + }} options={[ { value: "left", label: t("objinfo.wall.refLine.left") }, { value: "center", label: t("objinfo.wall.refLine.center") }, { value: "right", label: t("objinfo.wall.refLine.right") }, + ...wall.layerBoundaries.map((b) => ({ + value: `off:${b.offset.toFixed(4)}`, + label: `${t("objinfo.wall.refLine.joint")} ${b.label}`, + })), ]} title={t("objinfo.wall.refLine")} width={120} diff --git a/src/panels/host.ts b/src/panels/host.ts index b279db5..e17f1fa 100644 --- a/src/panels/host.ts +++ b/src/panels/host.ts @@ -234,6 +234,11 @@ export interface PanelHostValue { // ── Wand-Attribute (Object-Info-Panel; wirken NUR auf die selektierte Wand) ─ /** Setzt die Lage der Wandachse über die Dicke (außen/mitte/innen). */ onSetWallReferenceLine: (ref: WallReferenceLine) => void; + /** + * Setzt einen freien Achsversatz (Schichttrennlinie als Referenz) oder löscht + * ihn (`null` → zurück zur benannten Referenzlinie). Meter entlang +n. + */ + onSetWallReferenceOffset: (offset: number | null) => void; /** * Setzt die Terminierungs-Regel am Deckenanschluss (Zuschnitt, NICHT Priorität): * "both" = heutiges Verhalten, "below"/"above" = die Wand endet an der Decke. diff --git a/src/state/selectionInfo.ts b/src/state/selectionInfo.ts index 0e82637..71667d3 100644 --- a/src/state/selectionInfo.ts +++ b/src/state/selectionInfo.ts @@ -14,6 +14,7 @@ import { ceilingThickness, flattenCategories, getCeilingType, + getComponent, getWallType, wallTypeLabel, wallTypeThickness, @@ -69,6 +70,16 @@ export interface FloorChoice { name: string; } +/** + * Eine wählbare Schichttrennlinie (Fuge zwischen zwei Schichten) als + * Referenzlinie einer mehrschichtigen Wand. `offset` ist der fertige + * Achsversatz entlang +n (Meter), `label` benennt die angrenzenden Schichten. + */ +export interface LayerBoundaryChoice { + offset: number; + label: string; +} + /** * Wand-spezifische Attribute für das Object-Info-Panel (nur bei Auswahl einer * einzelnen Wand gesetzt). Effektiv aufgelöste, anzeigefertige Werte plus die @@ -76,6 +87,14 @@ export interface FloorChoice { */ export interface WallInfo { referenceLine: WallReferenceLine; + /** Freier Achsversatz (Schichttrennlinie), falls gesetzt — sonst undefined. */ + referenceOffset?: number; + /** + * Interne Schichttrennlinien der Wand als wählbare Referenz (leer bei + * einschichtigen Wänden). Aussen-/Innenfläche stecken bereits in + * left/right — hier stehen nur die FUGEN dazwischen. + */ + layerBoundaries: LayerBoundaryChoice[]; /** Terminierungs-Regel am Deckenanschluss (Default "both" = heutiges Verhalten). */ sliceTermination: SliceTermination; wallTypeId: string; @@ -435,8 +454,26 @@ function wallSelection(project: Project, wall: Wall): Selection { .filter((o) => o.hostWallId === wall.id) .reduce((sum, o) => sum + o.width * o.height * thickness, 0); const netVolume = Math.max(0, grossVolume - openingVolume); + // Interne Schichttrennlinien als wählbare Referenz: von aussen (Schicht 0) + // nach innen aufsummiert; der Achsversatz +n = halbe Dicke − Fugenlage. + const half = thickness / 2; + const layerBoundaries: WallInfo["layerBoundaries"] = []; + let cum = 0; + const abbrevOf = (componentId: string): string => { + const c = getComponent(project, componentId); + return c ? c.abbrev ?? c.name : "?"; + }; + for (let i = 0; i < wt.layers.length - 1; i++) { + cum += wt.layers[i].thickness; + layerBoundaries.push({ + offset: half - cum, + label: `${abbrevOf(wt.layers[i].componentId)}|${abbrevOf(wt.layers[i + 1].componentId)}`, + }); + } const wallInfo: WallInfo = { referenceLine: wall.referenceLine ?? "center", + referenceOffset: wall.referenceOffset, + layerBoundaries, sliceTermination: wall.sliceTermination ?? "both", wallTypeId: wall.wallTypeId, thickness,