Wandstile: Schichtfuge pro Fuge als LineStyle waehlbar + ResourceManager-Abteilung

Mehrschichtige Waende zeichnen die Trennlinie an jeder Materialfuge jetzt mit
einem pro Fuge waehlbaren LineStyle statt einer festen Haarlinie.

- Layer.jointLineStyleId (optional): LineStyle der Fuge an der Innenkante
  dieser Schicht; fehlt er, gilt die Default-Haarlinie (0.02) in Wandfarbe.
- generatePlan: Schicht-Baender tragen nur noch Fuellung + Schraffur (kein
  Band-Umriss); jede innere Fuge wird als eigene Linie mit ihrem LineStyle
  (Gewicht/Farbe/Dash, x Detailfaktor) gezeichnet. Wand-Umriss (0.35) und
  grob-Pfad unveraendert.
- ResourceManager: neue Abteilung "Wandstile" - je Wandtyp die Schichten
  mit Fugen-LineStyle-Dropdown.
- Sample: LineStyle "Schichtfuge 0.13"; der Daemmung-Backstein-Fuge (massiv/
  massiv) zugewiesen, Putzfugen bleiben Haarlinie.
- 2 Joint-Tests ergaenzt.
This commit is contained in:
2026-07-03 20:55:07 +02:00
parent 3a0652a337
commit bd80eb1c59
11 changed files with 211 additions and 9 deletions
+28
View File
@@ -114,3 +114,31 @@ describe("generatePlan — Wandecken-Gehrung", () => {
for (const p of wallPolys) expect(p.noStrokeEdges).toEqual([]);
});
});
describe("generatePlan — Schichtfugen als stilisierbare Linien", () => {
const jointLines = (proj: Project) =>
generatePlan(proj, "eg", visible, undefined, "mittel").primitives.filter(
(p): p is Extract<typeof p, { kind: "line" }> =>
p.kind === "line" && p.cls === "layer-joint",
);
it("zieht die innere Fuge als eigene Linie und übernimmt Gewicht/Farbe des LineStyle", () => {
const proj = cornerProject();
proj.walls = [proj.walls[0]]; // nur W1, zwei Schichten → genau eine innere Fuge
proj.wallTypes[0].layers[0].jointLineStyleId = "thin"; // weight 0.13, color #111
const joints = jointLines(proj);
expect(joints.length).toBe(1);
// LAYER_DETAIL_FACTOR["mittel"] == 1 → das LineStyle-Gewicht wirkt direkt.
expect(joints[0].weightMm).toBeCloseTo(0.13, 6);
expect(joints[0].color).toBe("#111");
expect(joints[0].dash ?? null).toBeNull();
});
it("fällt ohne Zuweisung auf die Haarlinie (0.02 mm) in Wandfarbe zurück", () => {
const proj = cornerProject();
proj.walls = [proj.walls[0]];
const joints = jointLines(proj);
expect(joints.length).toBe(1);
expect(joints[0].weightMm).toBeCloseTo(0.02, 6);
});
});
+37 -7
View File
@@ -80,7 +80,9 @@ const MONO_INK = "#111111";
// dpi = 96·devicePixelRatio in konstante Bildschirm-Pixel um (non-scaling-stroke),
// damit Linien beim Zoomen ihre Papiergröße behalten und der Massstab stimmt.
/** Haarlinie zwischen den Schichten (Schichtfugen) in mm — gleiche Stärke wie die Schraffur-Tinte. */
/** Default-Schichtfuge zwischen den Materialschichten in mm — Haarlinie (0.02),
* gleiche Stärke wie die Schraffur-Tinte. Pro Fuge im Wandstil überschreibbar
* (z. B. 0.13, wenn zwei massive Materialien aufeinandertreffen). */
const LAYER_LINE_MM = 0.02;
/** Haarlinie für Tür-/Fenstersymbole (Flügel, Bogen, Rahmen, Glas) in mm. */
const SYMBOL_HAIRLINE_MM = 0.02;
@@ -905,10 +907,12 @@ function addWallPoche(
// mittel/fein: Schichten von außen (-T/2) nach innen (+T/2) stapeln. Jede
// Schicht löst Bauteil (Füllfarbe) + Schraffur (Muster/Maßstab/Winkel/Farbe)
// auf; die Schichtfugen werden dünn gestrichen.
// auf. Die Bänder tragen NUR Füllung + Schraffur (kein eigener Umriss); jede
// innere Schichtfuge wird als EIGENSTÄNDIGE, stilisierbare Linie gezeichnet.
const bandJoinEdges = joinEdges(startCut, endCut);
let off = refOff - total / 2;
for (const layer of wt.layers) {
for (let li = 0; li < wt.layers.length; li++) {
const layer = wt.layers[li];
const comp = getComponent(project, layer.componentId);
const layerHatch = resolveHatch(project, comp.hatchId, wallAngleDeg);
// SIA-Poché: neutraler Hintergrund (weiß) statt Bauteil-Albedo, die
@@ -918,16 +922,42 @@ function addWallPoche(
kind: "polygon",
pts: clippedBand(p1, p2, off, off + layer.thickness, startCut, endCut),
fill: layerSolid ? POCHE_FILL_BLACK : POCHE_FILL_WHITE,
stroke,
strokeWidthMm: layerLineMm,
// Band ohne Umriss: keine Schichtfuge über die Bandkante, keine 45°-Naht
// am Knoten. Die Fugen kommen als separate `line`-Primitive (siehe unten).
stroke: "none",
strokeWidthMm: 0,
hatch: layerSolid ? { ...layerHatch, color: POCHE_FILL_BLACK } : layerHatch,
greyed,
// Die Diagonale am Knoten NICHT stricheln → keine 45°-Naht zwischen
// gleichfarbigen Schichten der beiden Wände.
// Für die Ecktests/Konsistenz beibehalten (bei stroke:"none" wirkungslos).
noStrokeEdges: bandJoinEdges,
wallId,
});
off += layer.thickness;
// Innere Schichtfuge (zwischen dieser und der nächst-inneren Schicht) als
// eigene Linie am kumulierten Offset `off`, mit dem Cut der Bänder geclippt.
// Stil aus layer.jointLineStyleId (Gewicht/Farbe/Strich); Rückfall = Haar-
// linie (LAYER_LINE_MM) in Wandfarbe. Das Gewicht folgt demselben Detail-
// Faktor wie die frühere Schichtfuge (`layerLineMm`), damit die Detailgrade
// sich unverändert verhalten. Die innerste Schicht hat keine innere Fuge.
if (li < wt.layers.length - 1) {
// clippedBand(p1, p2, off, off, …) liefert [edgeStart, edgeEnd, edgeEnd,
// edgeStart] → die zwei distinkten Endpunkte sind Index 0 (Start) und 1
// (Ende), beide am Cut abgeschnitten wie die Bandlängskante.
const jq = clippedBand(p1, p2, off, off, startCut, endCut);
const jls = layer.jointLineStyleId
? project.lineStyles.find((l) => l.id === layer.jointLineStyleId)
: undefined;
out.push({
kind: "line",
a: jq[0],
b: jq[1],
cls: "layer-joint",
weightMm: jls ? jls.weight * LAYER_DETAIL_FACTOR[detail] : layerLineMm,
color: jls ? jls.color : stroke,
dash: jls ? jls.dash : null,
greyed,
});
}
}
// Kräftige Wand-Umrisslinie über die volle Dicke (nur Kontur, keine Füllung),
// damit die Wand sich klar von den dünnen Schichtfugen abhebt. Die Gehrungs-