Schichttrennlinie als Wand-Referenzlinie (mehrschichtige Wände)

Bei mehrschichtigen Wänden lässt sich neben Aussen/Mitte/Innen jetzt auch
eine SCHICHTTRENNLINIE (Fuge zwischen zwei Schichten) als Referenzlinie
wählen — z. B. die Achse auf die Innenkante der Aussenschale legen.
- Neues Feld Wall.referenceOffset (freier Achsversatz entlang +n); wenn
  gesetzt, übersteuert es referenceLine. wallReferenceOffset bleibt die
  EINE Quelle → Grundriss, Schnitt und 3D erben das Verhalten automatisch.
- Object-Info-Panel: das Referenzlinien-Dropdown listet zusätzlich je
  interne Fuge einen Eintrag (Kürzel der angrenzenden Schichten); Auswahl
  setzt den Versatz, Aussen/Mitte/Innen löscht ihn wieder.
- selectionInfo berechnet die Fugen-Versätze (kumulierte Schichtdicken).
+3 Tests. tsc + vitest grün.
This commit is contained in:
2026-07-09 01:21:28 +02:00
parent 9a65900377
commit 938d6421f9
9 changed files with 104 additions and 8 deletions
+6 -1
View File
@@ -2968,7 +2968,12 @@ export default function App() {
// ── Wand-Attribute (nur bei selektierter Wand) ───────────────────────── // ── Wand-Attribute (nur bei selektierter Wand) ─────────────────────────
onSetWallReferenceLine: (ref) => { onSetWallReferenceLine: (ref) => {
if (selection?.kind === "wall") 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) => { onSetWallSliceTermination: (termination) => {
if (selection?.kind === "wall") if (selection?.kind === "wall")
+1
View File
@@ -302,6 +302,7 @@ export const de = {
"objinfo.wall.refLine.left": "Außen (links)", "objinfo.wall.refLine.left": "Außen (links)",
"objinfo.wall.refLine.center": "Mitte", "objinfo.wall.refLine.center": "Mitte",
"objinfo.wall.refLine.right": "Innen (rechts)", "objinfo.wall.refLine.right": "Innen (rechts)",
"objinfo.wall.refLine.joint": "Fuge",
"objinfo.wall.sliceTermination": "An Decke enden", "objinfo.wall.sliceTermination": "An Decke enden",
"objinfo.wall.sliceTermination.both": "Aus", "objinfo.wall.sliceTermination.both": "Aus",
"objinfo.wall.sliceTermination.below": "Unten", "objinfo.wall.sliceTermination.below": "Unten",
+1
View File
@@ -301,6 +301,7 @@ export const en: Record<TranslationKey, string> = {
"objinfo.wall.refLine.left": "Exterior (left)", "objinfo.wall.refLine.left": "Exterior (left)",
"objinfo.wall.refLine.center": "Center", "objinfo.wall.refLine.center": "Center",
"objinfo.wall.refLine.right": "Interior (right)", "objinfo.wall.refLine.right": "Interior (right)",
"objinfo.wall.refLine.joint": "Joint",
"objinfo.wall.sliceTermination": "End at ceiling", "objinfo.wall.sliceTermination": "End at ceiling",
"objinfo.wall.sliceTermination.both": "Off", "objinfo.wall.sliceTermination.both": "Off",
"objinfo.wall.sliceTermination.below": "Below", "objinfo.wall.sliceTermination.below": "Below",
+8
View File
@@ -827,6 +827,14 @@ export interface Wall {
* Verhalten: Schichten symmetrisch T/2 … +T/2 um die Achse). * Verhalten: Schichten symmetrisch T/2 … +T/2 um die Achse).
*/ */
referenceLine?: WallReferenceLine; 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 * Vertikale Bindung der Unterkante (UK). Fehlt sie, sitzt die UK auf der
* baseElevation des zugehörigen Geschosses (= heutiges Verhalten). * baseElevation des zugehörigen Geschosses (= heutiges Verhalten).
+23 -2
View File
@@ -2,8 +2,8 @@
// ceilingVerticalExtent): reine Datenschicht, kein React/Store. // ceilingVerticalExtent): reine Datenschicht, kein React/Store.
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vitest";
import { ceilingVerticalExtent } from "./wall"; import { ceilingVerticalExtent, wallReferenceOffset } from "./wall";
import type { Ceiling, Project } from "./types"; import type { Ceiling, Project, Wall } from "./types";
/** Minimalprojekt mit einem Geschoss (baseElevation 0, floorHeight 2.6) und /** Minimalprojekt mit einem Geschoss (baseElevation 0, floorHeight 2.6) und
* einem einschichtigen Wandtyp (0.2 m), den die Decke als Aufbau nutzt. */ * 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) expect(zBottom).toBeCloseTo(2.9); // baseElevation(og)=2.8 + offset(0.1)
}); });
}); });
describe("wallReferenceOffset", () => {
const w = (extra: Partial<Wall>): 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);
});
});
+7 -1
View File
@@ -26,8 +26,14 @@ export function wallReferenceOffset(
wall: Wall, wall: Wall,
totalThickness: number, totalThickness: number,
): number { ): number {
const ref = wall.referenceLine ?? "center";
const half = totalThickness / 2; 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 === "left") return +half;
if (ref === "right") return -half; if (ref === "right") return -half;
return 0; return 0;
+16 -4
View File
@@ -906,17 +906,29 @@ export function WallSection({
<div className="objinfo-sep" /> <div className="objinfo-sep" />
<div className="objinfo-section-label">{t("objinfo.wall.section")}</div> <div className="objinfo-section-label">{t("objinfo.wall.section")}</div>
{/* Referenzlinie (wo die Achse über die Dicke liegt) — früher im ObjectInfo- {/* Referenzlinie (wo die Achse über die Dicke liegt) — außen/mitte/innen ODER
Kopf, jetzt hier bei den Wand-Attributen. */} bei mehrschichtigen Wänden eine SCHICHTTRENNLINIE (Fuge). Ein Fugenwert
wird als "off:<offset>" kodiert und übersteuert die benannte Linie. */}
<div className="objinfo-field"> <div className="objinfo-field">
<span className="objinfo-flabel">{t("objinfo.wall.refLine")}</span> <span className="objinfo-flabel">{t("objinfo.wall.refLine")}</span>
<Dropdown <Dropdown
value={wall.referenceLine} value={
onChange={(v) => host.onSetWallReferenceLine(v as WallReferenceLine)} 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={[ options={[
{ value: "left", label: t("objinfo.wall.refLine.left") }, { value: "left", label: t("objinfo.wall.refLine.left") },
{ value: "center", label: t("objinfo.wall.refLine.center") }, { value: "center", label: t("objinfo.wall.refLine.center") },
{ value: "right", label: t("objinfo.wall.refLine.right") }, { 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")} title={t("objinfo.wall.refLine")}
width={120} width={120}
+5
View File
@@ -234,6 +234,11 @@ export interface PanelHostValue {
// ── Wand-Attribute (Object-Info-Panel; wirken NUR auf die selektierte Wand) ─ // ── Wand-Attribute (Object-Info-Panel; wirken NUR auf die selektierte Wand) ─
/** Setzt die Lage der Wandachse über die Dicke (außen/mitte/innen). */ /** Setzt die Lage der Wandachse über die Dicke (außen/mitte/innen). */
onSetWallReferenceLine: (ref: WallReferenceLine) => void; 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): * Setzt die Terminierungs-Regel am Deckenanschluss (Zuschnitt, NICHT Priorität):
* "both" = heutiges Verhalten, "below"/"above" = die Wand endet an der Decke. * "both" = heutiges Verhalten, "below"/"above" = die Wand endet an der Decke.
+37
View File
@@ -14,6 +14,7 @@ import {
ceilingThickness, ceilingThickness,
flattenCategories, flattenCategories,
getCeilingType, getCeilingType,
getComponent,
getWallType, getWallType,
wallTypeLabel, wallTypeLabel,
wallTypeThickness, wallTypeThickness,
@@ -69,6 +70,16 @@ export interface FloorChoice {
name: string; 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 * Wand-spezifische Attribute für das Object-Info-Panel (nur bei Auswahl einer
* einzelnen Wand gesetzt). Effektiv aufgelöste, anzeigefertige Werte plus die * einzelnen Wand gesetzt). Effektiv aufgelöste, anzeigefertige Werte plus die
@@ -76,6 +87,14 @@ export interface FloorChoice {
*/ */
export interface WallInfo { export interface WallInfo {
referenceLine: WallReferenceLine; 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). */ /** Terminierungs-Regel am Deckenanschluss (Default "both" = heutiges Verhalten). */
sliceTermination: SliceTermination; sliceTermination: SliceTermination;
wallTypeId: string; wallTypeId: string;
@@ -435,8 +454,26 @@ function wallSelection(project: Project, wall: Wall): Selection {
.filter((o) => o.hostWallId === wall.id) .filter((o) => o.hostWallId === wall.id)
.reduce((sum, o) => sum + o.width * o.height * thickness, 0); .reduce((sum, o) => sum + o.width * o.height * thickness, 0);
const netVolume = Math.max(0, grossVolume - openingVolume); 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 = { const wallInfo: WallInfo = {
referenceLine: wall.referenceLine ?? "center", referenceLine: wall.referenceLine ?? "center",
referenceOffset: wall.referenceOffset,
layerBoundaries,
sliceTermination: wall.sliceTermination ?? "both", sliceTermination: wall.sliceTermination ?? "both",
wallTypeId: wall.wallTypeId, wallTypeId: wall.wallTypeId,
thickness, thickness,