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:
+11
@@ -34,6 +34,7 @@ import type {
|
|||||||
Project,
|
Project,
|
||||||
Vec2,
|
Vec2,
|
||||||
Wall,
|
Wall,
|
||||||
|
WallType,
|
||||||
} from "./model/types";
|
} from "./model/types";
|
||||||
import { generatePlan, generateSectionPlan } from "./plan/generatePlan";
|
import { generatePlan, generateSectionPlan } from "./plan/generatePlan";
|
||||||
import type { CategoryDisplayResolver } from "./plan/generatePlan";
|
import type { CategoryDisplayResolver } from "./plan/generatePlan";
|
||||||
@@ -1619,6 +1620,14 @@ export default function App() {
|
|||||||
const patchLineStyle = useStore((s) => s.patchLineStyle);
|
const patchLineStyle = useStore((s) => s.patchLineStyle);
|
||||||
const addLineStyle = useStore((s) => s.addLineStyle);
|
const addLineStyle = useStore((s) => s.addLineStyle);
|
||||||
const deleteLineStyle = useStore((s) => s.deleteLineStyle);
|
const deleteLineStyle = useStore((s) => s.deleteLineStyle);
|
||||||
|
// Wandstile: immutable Patch eines Wandtyps (z. B. Schichtfugen-Stile). Kein
|
||||||
|
// eigener Store-Slice nötig — läuft direkt über setProject wie die übrigen
|
||||||
|
// Ressourcen-Mutationen.
|
||||||
|
const patchWallType = (id: string, patch: Partial<WallType>) =>
|
||||||
|
setProject((p) => ({
|
||||||
|
...p,
|
||||||
|
wallTypes: p.wallTypes.map((wt) => (wt.id === id ? { ...wt, ...patch } : wt)),
|
||||||
|
}));
|
||||||
const importLineStyles = useStore((s) => s.importLineStyles);
|
const importLineStyles = useStore((s) => s.importLineStyles);
|
||||||
const importHatches = useStore((s) => s.importHatches);
|
const importHatches = useStore((s) => s.importHatches);
|
||||||
// Site-/Kontext-Schicht (siteSlice): Import/Entfernen/Gelände.
|
// Site-/Kontext-Schicht (siteSlice): Import/Entfernen/Gelände.
|
||||||
@@ -1904,6 +1913,7 @@ export default function App() {
|
|||||||
onPatchLineStyle: patchLineStyle,
|
onPatchLineStyle: patchLineStyle,
|
||||||
onAddLineStyle: addLineStyle,
|
onAddLineStyle: addLineStyle,
|
||||||
onDeleteLineStyle: deleteLineStyle,
|
onDeleteLineStyle: deleteLineStyle,
|
||||||
|
onPatchWallType: patchWallType,
|
||||||
onImportLineStyles: importLineStyles,
|
onImportLineStyles: importLineStyles,
|
||||||
onImportHatches: importHatches,
|
onImportHatches: importHatches,
|
||||||
selection,
|
selection,
|
||||||
@@ -2135,6 +2145,7 @@ export default function App() {
|
|||||||
// Handler-Bündel für das schwebende Ressourcen-Fenster (ResourceManager).
|
// Handler-Bündel für das schwebende Ressourcen-Fenster (ResourceManager).
|
||||||
const resourceHandlers: ResourceManagerHandlers = {
|
const resourceHandlers: ResourceManagerHandlers = {
|
||||||
onPatchComponent: patchComponent,
|
onPatchComponent: patchComponent,
|
||||||
|
onPatchWallType: patchWallType,
|
||||||
onAddComponent: addComponent,
|
onAddComponent: addComponent,
|
||||||
onDeleteComponent: deleteComponent,
|
onDeleteComponent: deleteComponent,
|
||||||
onPatchHatch: patchHatch,
|
onPatchHatch: patchHatch,
|
||||||
|
|||||||
@@ -438,8 +438,17 @@ export const de = {
|
|||||||
"resources.tab.components": "Bauteile",
|
"resources.tab.components": "Bauteile",
|
||||||
"resources.tab.hatches": "Schraffuren",
|
"resources.tab.hatches": "Schraffuren",
|
||||||
"resources.tab.lines": "Linien",
|
"resources.tab.lines": "Linien",
|
||||||
|
"resources.tab.wallStyles": "Wandstile",
|
||||||
"resources.tab.materials": "Materialien",
|
"resources.tab.materials": "Materialien",
|
||||||
|
|
||||||
|
"resources.wallStyles.hint":
|
||||||
|
"Je Wandtyp die Schichtfugen (zwischen benachbarten Schichten) als wählbaren Linienstil. Ohne Zuweisung: Haarlinie 0.02 mm.",
|
||||||
|
"resources.wallStyles.empty": "Noch keine Wandtypen.",
|
||||||
|
"resources.col.layer": "Schicht",
|
||||||
|
"resources.col.thickness": "Dicke (mm)",
|
||||||
|
"resources.col.joint": "Fuge",
|
||||||
|
"resources.joint.default": "— (Haarlinie)",
|
||||||
|
|
||||||
"resources.materials.hint":
|
"resources.materials.hint":
|
||||||
"Kugel-Vorschau der eingebauten PBR-Materialbibliothek — durchsuchen und vergleichen. Zuweisen an ein Bauteil weiterhin über dessen Spalte „Material …\".",
|
"Kugel-Vorschau der eingebauten PBR-Materialbibliothek — durchsuchen und vergleichen. Zuweisen an ein Bauteil weiterhin über dessen Spalte „Material …\".",
|
||||||
"resources.materials.searchPlaceholder": "Material suchen …",
|
"resources.materials.searchPlaceholder": "Material suchen …",
|
||||||
|
|||||||
@@ -435,8 +435,17 @@ export const en: Record<TranslationKey, string> = {
|
|||||||
"resources.tab.components": "Components",
|
"resources.tab.components": "Components",
|
||||||
"resources.tab.hatches": "Hatches",
|
"resources.tab.hatches": "Hatches",
|
||||||
"resources.tab.lines": "Lines",
|
"resources.tab.lines": "Lines",
|
||||||
|
"resources.tab.wallStyles": "Wall styles",
|
||||||
"resources.tab.materials": "Materials",
|
"resources.tab.materials": "Materials",
|
||||||
|
|
||||||
|
"resources.wallStyles.hint":
|
||||||
|
"Per wall type, the layer joints (between adjacent layers) as a selectable line style. Unassigned: 0.02 mm hairline.",
|
||||||
|
"resources.wallStyles.empty": "No wall types yet.",
|
||||||
|
"resources.col.layer": "Layer",
|
||||||
|
"resources.col.thickness": "Thickness (mm)",
|
||||||
|
"resources.col.joint": "Joint",
|
||||||
|
"resources.joint.default": "— (hairline)",
|
||||||
|
|
||||||
"resources.materials.hint":
|
"resources.materials.hint":
|
||||||
"Sphere preview of the built-in PBR material library — browse and compare. Assign to a component still via its “Material …” column.",
|
"Sphere preview of the built-in PBR material library — browse and compare. Assign to a component still via its “Material …” column.",
|
||||||
"resources.materials.searchPlaceholder": "Search materials …",
|
"resources.materials.searchPlaceholder": "Search materials …",
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ export const sampleProject: Project = {
|
|||||||
{ id: "hatch-hair", name: "Schraffur-Haarlinie", weight: 0.02, color: "#1a1a1a", dash: null },
|
{ id: "hatch-hair", name: "Schraffur-Haarlinie", weight: 0.02, color: "#1a1a1a", dash: null },
|
||||||
// Gestrichelte Haarlinie für die Dämmung (Striche quer durch die Wanddicke).
|
// Gestrichelte Haarlinie für die Dämmung (Striche quer durch die Wanddicke).
|
||||||
{ id: "hatch-dash", name: "Dämmungsstrich", weight: 0.02, color: "#1a1a1a", dash: [0.35, 0.3] },
|
{ id: "hatch-dash", name: "Dämmungsstrich", weight: 0.02, color: "#1a1a1a", dash: [0.35, 0.3] },
|
||||||
|
// Kräftige Schichtfuge, wo zwei massive Materialien aneinanderstoßen (Wandstile).
|
||||||
|
{ id: "joint-massive", name: "Schichtfuge 0.13", weight: 0.13, color: "#1a1a1a", dash: null },
|
||||||
],
|
],
|
||||||
// Schraffuren: color = Linien-/Vollfüllfarbe des Musters; angle in Grad.
|
// Schraffuren: color = Linien-/Vollfüllfarbe des Musters; angle in Grad.
|
||||||
// "none" = keine Schraffur (nur Component-Füllung).
|
// "none" = keine Schraffur (nur Component-Füllung).
|
||||||
@@ -128,7 +130,8 @@ export const sampleProject: Project = {
|
|||||||
// Schichten außen → innen; Priorität lebt jetzt am Component.
|
// Schichten außen → innen; Priorität lebt jetzt am Component.
|
||||||
layers: [
|
layers: [
|
||||||
{ componentId: "render-ext", thickness: 0.02 },
|
{ componentId: "render-ext", thickness: 0.02 },
|
||||||
{ componentId: "insulation", thickness: 0.16 },
|
// Innere Fuge der Dämmung trifft auf den Backstein (massiv↔massiv) → 0.13 mm.
|
||||||
|
{ componentId: "insulation", thickness: 0.16, jointLineStyleId: "joint-massive" },
|
||||||
{ componentId: "brick", thickness: 0.15 },
|
{ componentId: "brick", thickness: 0.15 },
|
||||||
{ componentId: "render-int", thickness: 0.015 },
|
{ componentId: "render-int", thickness: 0.015 },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -128,6 +128,13 @@ export interface Layer {
|
|||||||
componentId: string;
|
componentId: string;
|
||||||
/** Schichtdicke in Metern. */
|
/** Schichtdicke in Metern. */
|
||||||
thickness: number;
|
thickness: number;
|
||||||
|
/**
|
||||||
|
* Linienstil (LineStyle) der Schichtfuge an der INNEREN Kante DIESER Schicht —
|
||||||
|
* also der Fuge zwischen dieser und der nächst-inneren Schicht. Bei der
|
||||||
|
* innersten Schicht ungenutzt (ihre innere Kante ist der Wand-Innenumriss).
|
||||||
|
* Fehlt das Feld, wird die Standard-Haarlinie (0.02 mm) gezeichnet.
|
||||||
|
*/
|
||||||
|
jointLineStyleId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ein Wandtyp = geordneter Schichtaufbau (außen → innen). */
|
/** Ein Wandtyp = geordneter Schichtaufbau (außen → innen). */
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export function ResourcesPanel() {
|
|||||||
onPatchLineStyle: host.onPatchLineStyle,
|
onPatchLineStyle: host.onPatchLineStyle,
|
||||||
onAddLineStyle: host.onAddLineStyle,
|
onAddLineStyle: host.onAddLineStyle,
|
||||||
onDeleteLineStyle: host.onDeleteLineStyle,
|
onDeleteLineStyle: host.onDeleteLineStyle,
|
||||||
|
onPatchWallType: host.onPatchWallType,
|
||||||
onImportLineStyles: host.onImportLineStyles,
|
onImportLineStyles: host.onImportLineStyles,
|
||||||
onImportHatches: host.onImportHatches,
|
onImportHatches: host.onImportHatches,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import type {
|
|||||||
StairShape,
|
StairShape,
|
||||||
VerticalAnchor,
|
VerticalAnchor,
|
||||||
WallReferenceLine,
|
WallReferenceLine,
|
||||||
|
WallType,
|
||||||
} from "../model/types";
|
} from "../model/types";
|
||||||
import type { SnapSettings, ToolId } from "../tools/types";
|
import type { SnapSettings, ToolId } from "../tools/types";
|
||||||
import type { Selection } from "../state/selectionInfo";
|
import type { Selection } from "../state/selectionInfo";
|
||||||
@@ -114,6 +115,8 @@ export interface PanelHostValue {
|
|||||||
onPatchLineStyle: (id: string, patch: Partial<LineStyle>) => void;
|
onPatchLineStyle: (id: string, patch: Partial<LineStyle>) => void;
|
||||||
onAddLineStyle: () => void;
|
onAddLineStyle: () => void;
|
||||||
onDeleteLineStyle: (id: string) => void;
|
onDeleteLineStyle: (id: string) => void;
|
||||||
|
/** Wandstile: immutable Änderung eines Wandtyps (Schichtfugen-Stile). */
|
||||||
|
onPatchWallType: (id: string, patch: Partial<WallType>) => void;
|
||||||
/** Import fertiger (id-loser) Linienstile/Schraffuren (.lin/.pat). */
|
/** Import fertiger (id-loser) Linienstile/Schraffuren (.lin/.pat). */
|
||||||
onImportLineStyles: (styles: Omit<LineStyle, "id">[]) => void;
|
onImportLineStyles: (styles: Omit<LineStyle, "id">[]) => void;
|
||||||
onImportHatches: (hatches: Omit<HatchStyle, "id">[]) => void;
|
onImportHatches: (hatches: Omit<HatchStyle, "id">[]) => void;
|
||||||
|
|||||||
@@ -114,3 +114,31 @@ describe("generatePlan — Wandecken-Gehrung", () => {
|
|||||||
for (const p of wallPolys) expect(p.noStrokeEdges).toEqual([]);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -80,7 +80,9 @@ const MONO_INK = "#111111";
|
|||||||
// dpi = 96·devicePixelRatio in konstante Bildschirm-Pixel um (non-scaling-stroke),
|
// dpi = 96·devicePixelRatio in konstante Bildschirm-Pixel um (non-scaling-stroke),
|
||||||
// damit Linien beim Zoomen ihre Papiergröße behalten und der Massstab stimmt.
|
// 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;
|
const LAYER_LINE_MM = 0.02;
|
||||||
/** Haarlinie für Tür-/Fenstersymbole (Flügel, Bogen, Rahmen, Glas) in mm. */
|
/** Haarlinie für Tür-/Fenstersymbole (Flügel, Bogen, Rahmen, Glas) in mm. */
|
||||||
const SYMBOL_HAIRLINE_MM = 0.02;
|
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
|
// 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)
|
// 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);
|
const bandJoinEdges = joinEdges(startCut, endCut);
|
||||||
let off = refOff - total / 2;
|
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 comp = getComponent(project, layer.componentId);
|
||||||
const layerHatch = resolveHatch(project, comp.hatchId, wallAngleDeg);
|
const layerHatch = resolveHatch(project, comp.hatchId, wallAngleDeg);
|
||||||
// SIA-Poché: neutraler Hintergrund (weiß) statt Bauteil-Albedo, die
|
// SIA-Poché: neutraler Hintergrund (weiß) statt Bauteil-Albedo, die
|
||||||
@@ -918,16 +922,42 @@ function addWallPoche(
|
|||||||
kind: "polygon",
|
kind: "polygon",
|
||||||
pts: clippedBand(p1, p2, off, off + layer.thickness, startCut, endCut),
|
pts: clippedBand(p1, p2, off, off + layer.thickness, startCut, endCut),
|
||||||
fill: layerSolid ? POCHE_FILL_BLACK : POCHE_FILL_WHITE,
|
fill: layerSolid ? POCHE_FILL_BLACK : POCHE_FILL_WHITE,
|
||||||
stroke,
|
// Band ohne Umriss: keine Schichtfuge über die Bandkante, keine 45°-Naht
|
||||||
strokeWidthMm: layerLineMm,
|
// am Knoten. Die Fugen kommen als separate `line`-Primitive (siehe unten).
|
||||||
|
stroke: "none",
|
||||||
|
strokeWidthMm: 0,
|
||||||
hatch: layerSolid ? { ...layerHatch, color: POCHE_FILL_BLACK } : layerHatch,
|
hatch: layerSolid ? { ...layerHatch, color: POCHE_FILL_BLACK } : layerHatch,
|
||||||
greyed,
|
greyed,
|
||||||
// Die Diagonale am Knoten NICHT stricheln → keine 45°-Naht zwischen
|
// Für die Ecktests/Konsistenz beibehalten (bei stroke:"none" wirkungslos).
|
||||||
// gleichfarbigen Schichten der beiden Wände.
|
|
||||||
noStrokeEdges: bandJoinEdges,
|
noStrokeEdges: bandJoinEdges,
|
||||||
wallId,
|
wallId,
|
||||||
});
|
});
|
||||||
off += layer.thickness;
|
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),
|
// 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-
|
// damit die Wand sich klar von den dünnen Schichtfugen abhebt. Die Gehrungs-
|
||||||
|
|||||||
@@ -1971,6 +1971,26 @@ body {
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Wandstile: ein Block je Wandtyp (Überschrift + Schicht-/Fugen-Tabelle) ─── */
|
||||||
|
.res-wallstyle {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.res-wallstyle-head {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
padding: 2px 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Innerste Schicht: keine innere Fuge → gedämpfter Platzhalter statt Dropdown. */
|
||||||
|
.res-joint-none {
|
||||||
|
color: var(--muted);
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Tabelle (saubere Liste, CONVENTIONS.md) ──────────────────────────────────
|
/* ── Tabelle (saubere Liste, CONVENTIONS.md) ──────────────────────────────────
|
||||||
Ein CSS-Grid trägt die Spalten (gridTemplateColumns wird je Tab inline
|
Ein CSS-Grid trägt die Spalten (gridTemplateColumns wird je Tab inline
|
||||||
gesetzt). Kopfzeile und Datenzeilen sind `display:contents`, damit ihre
|
gesetzt). Kopfzeile und Datenzeilen sind `display:contents`, damit ihre
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import type {
|
|||||||
HatchStyle,
|
HatchStyle,
|
||||||
LineStyle,
|
LineStyle,
|
||||||
Project,
|
Project,
|
||||||
|
WallType,
|
||||||
} from "../model/types";
|
} from "../model/types";
|
||||||
import { PEN_WEIGHTS } from "../model/types";
|
import { PEN_WEIGHTS } from "../model/types";
|
||||||
import { parseLin } from "../io/linParser";
|
import { parseLin } from "../io/linParser";
|
||||||
@@ -1129,6 +1130,77 @@ function LinesTab({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Wandstile (Wall Styles) ────────────────────────────────────────────────
|
||||||
|
// Pro Wandtyp die geordneten Schichten (Bauteil + Dicke) und je INNERER Fuge
|
||||||
|
// (zwischen zwei aufeinanderfolgenden Schichten) ein LineStyle-Dropdown, das an
|
||||||
|
// layer[i].jointLineStyleId hängt (i = Schicht, deren innere Kante die Fuge ist).
|
||||||
|
// Die innerste Schicht hat keine innere Fuge (ihre Kante ist der Wand-Innenumriss).
|
||||||
|
|
||||||
|
// Spalten: Schicht | Dicke (mm) | Fuge (innere).
|
||||||
|
const WALLSTYLE_COLUMNS: ResColumn[] = [
|
||||||
|
{ titleKey: "resources.col.layer" },
|
||||||
|
{ titleKey: "resources.col.thickness", align: "right" },
|
||||||
|
{ titleKey: "resources.col.joint" },
|
||||||
|
];
|
||||||
|
const WALLSTYLE_TEMPLATE = "minmax(120px, 1fr) 72px minmax(140px, 1.2fr)";
|
||||||
|
|
||||||
|
function WallStylesTab({
|
||||||
|
project,
|
||||||
|
onPatchWallType,
|
||||||
|
}: {
|
||||||
|
project: Project;
|
||||||
|
onPatchWallType: (id: string, patch: Partial<WallType>) => void;
|
||||||
|
}) {
|
||||||
|
// Sentinel „— (Haarlinie)": kein Feld gesetzt → Standard-Haarlinie 0.02 mm.
|
||||||
|
const DEFAULT = "__default__";
|
||||||
|
const jointOptions = [
|
||||||
|
{ value: DEFAULT, label: t("resources.joint.default") },
|
||||||
|
...project.lineStyles.map((l) => ({ value: l.id, label: l.name })),
|
||||||
|
];
|
||||||
|
const setJoint = (wt: WallType, li: number, styleId: string | undefined) => {
|
||||||
|
const layers = wt.layers.map((l, i) =>
|
||||||
|
i === li ? { ...l, jointLineStyleId: styleId } : l,
|
||||||
|
);
|
||||||
|
onPatchWallType(wt.id, { layers });
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="res-tab">
|
||||||
|
<div className="res-hint">{t("resources.wallStyles.hint")}</div>
|
||||||
|
{project.wallTypes.map((wt) => (
|
||||||
|
<div key={wt.id} className="res-wallstyle">
|
||||||
|
<div className="res-wallstyle-head">{wt.name}</div>
|
||||||
|
<ResTable columns={WALLSTYLE_COLUMNS} template={WALLSTYLE_TEMPLATE}>
|
||||||
|
{wt.layers.map((layer, li) => {
|
||||||
|
const comp = project.components.find((c) => c.id === layer.componentId);
|
||||||
|
const isInner = li === wt.layers.length - 1;
|
||||||
|
return (
|
||||||
|
<ResRow key={li}>
|
||||||
|
<ResCell emphasis>{comp ? comp.name : layer.componentId}</ResCell>
|
||||||
|
<ResCell align="right">{Math.round(layer.thickness * 1000)}</ResCell>
|
||||||
|
<ResCell>
|
||||||
|
{isInner ? (
|
||||||
|
<span className="res-joint-none">–</span>
|
||||||
|
) : (
|
||||||
|
<SelectField
|
||||||
|
value={layer.jointLineStyleId ?? DEFAULT}
|
||||||
|
onChange={(v) => setJoint(wt, li, v === DEFAULT ? undefined : v)}
|
||||||
|
options={jointOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ResCell>
|
||||||
|
</ResRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ResTable>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{project.wallTypes.length === 0 && (
|
||||||
|
<EmptyState text={t("resources.wallStyles.empty")} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Datei-Import-Schaltfläche (verstecktes <input type=file>). Liest die gewählte
|
* Datei-Import-Schaltfläche (verstecktes <input type=file>). Liest die gewählte
|
||||||
* Datei als Text und reicht ihn nach oben. Für .lin/.pat-Import.
|
* Datei als Text und reicht ihn nach oben. Für .lin/.pat-Import.
|
||||||
@@ -1377,12 +1449,13 @@ function EmptyState({ text }: { text: string }) {
|
|||||||
|
|
||||||
// ── Manager-Schublade ──────────────────────────────────────────────────────
|
// ── Manager-Schublade ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
type TabId = "components" | "hatches" | "lines" | "materials";
|
type TabId = "components" | "hatches" | "lines" | "wallStyles" | "materials";
|
||||||
|
|
||||||
const TABS: { id: TabId; labelKey: string }[] = [
|
const TABS: { id: TabId; labelKey: string }[] = [
|
||||||
{ id: "components", labelKey: "resources.tab.components" },
|
{ id: "components", labelKey: "resources.tab.components" },
|
||||||
{ id: "hatches", labelKey: "resources.tab.hatches" },
|
{ id: "hatches", labelKey: "resources.tab.hatches" },
|
||||||
{ id: "lines", labelKey: "resources.tab.lines" },
|
{ id: "lines", labelKey: "resources.tab.lines" },
|
||||||
|
{ id: "wallStyles", labelKey: "resources.tab.wallStyles" },
|
||||||
{ id: "materials", labelKey: "resources.tab.materials" },
|
{ id: "materials", labelKey: "resources.tab.materials" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -1397,6 +1470,8 @@ export interface ResourceManagerHandlers {
|
|||||||
onPatchLineStyle: (id: string, patch: Partial<LineStyle>) => void;
|
onPatchLineStyle: (id: string, patch: Partial<LineStyle>) => void;
|
||||||
onAddLineStyle: () => void;
|
onAddLineStyle: () => void;
|
||||||
onDeleteLineStyle: (id: string) => void;
|
onDeleteLineStyle: (id: string) => void;
|
||||||
|
/** Wandstile: immutable Änderung eines Wandtyps (z. B. Schichtfugen-Stile). */
|
||||||
|
onPatchWallType: (id: string, patch: Partial<WallType>) => void;
|
||||||
/** Import: fügt fertige (id-lose) Linienstile/Schraffuren hinzu (.lin/.pat). */
|
/** Import: fügt fertige (id-lose) Linienstile/Schraffuren hinzu (.lin/.pat). */
|
||||||
onImportLineStyles: (styles: Omit<LineStyle, "id">[]) => void;
|
onImportLineStyles: (styles: Omit<LineStyle, "id">[]) => void;
|
||||||
onImportHatches: (hatches: Omit<HatchStyle, "id">[]) => void;
|
onImportHatches: (hatches: Omit<HatchStyle, "id">[]) => void;
|
||||||
@@ -1483,6 +1558,12 @@ export function ResourceManager({
|
|||||||
onImportLineStyles={handlers.onImportLineStyles}
|
onImportLineStyles={handlers.onImportLineStyles}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{tab === "wallStyles" && (
|
||||||
|
<WallStylesTab
|
||||||
|
project={project}
|
||||||
|
onPatchWallType={handlers.onPatchWallType}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{tab === "materials" && <MaterialsTab />}
|
{tab === "materials" && <MaterialsTab />}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
Reference in New Issue
Block a user