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,
|
||||
Vec2,
|
||||
Wall,
|
||||
WallType,
|
||||
} from "./model/types";
|
||||
import { generatePlan, generateSectionPlan } from "./plan/generatePlan";
|
||||
import type { CategoryDisplayResolver } from "./plan/generatePlan";
|
||||
@@ -1619,6 +1620,14 @@ export default function App() {
|
||||
const patchLineStyle = useStore((s) => s.patchLineStyle);
|
||||
const addLineStyle = useStore((s) => s.addLineStyle);
|
||||
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 importHatches = useStore((s) => s.importHatches);
|
||||
// Site-/Kontext-Schicht (siteSlice): Import/Entfernen/Gelände.
|
||||
@@ -1904,6 +1913,7 @@ export default function App() {
|
||||
onPatchLineStyle: patchLineStyle,
|
||||
onAddLineStyle: addLineStyle,
|
||||
onDeleteLineStyle: deleteLineStyle,
|
||||
onPatchWallType: patchWallType,
|
||||
onImportLineStyles: importLineStyles,
|
||||
onImportHatches: importHatches,
|
||||
selection,
|
||||
@@ -2135,6 +2145,7 @@ export default function App() {
|
||||
// Handler-Bündel für das schwebende Ressourcen-Fenster (ResourceManager).
|
||||
const resourceHandlers: ResourceManagerHandlers = {
|
||||
onPatchComponent: patchComponent,
|
||||
onPatchWallType: patchWallType,
|
||||
onAddComponent: addComponent,
|
||||
onDeleteComponent: deleteComponent,
|
||||
onPatchHatch: patchHatch,
|
||||
|
||||
@@ -438,8 +438,17 @@ export const de = {
|
||||
"resources.tab.components": "Bauteile",
|
||||
"resources.tab.hatches": "Schraffuren",
|
||||
"resources.tab.lines": "Linien",
|
||||
"resources.tab.wallStyles": "Wandstile",
|
||||
"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":
|
||||
"Kugel-Vorschau der eingebauten PBR-Materialbibliothek — durchsuchen und vergleichen. Zuweisen an ein Bauteil weiterhin über dessen Spalte „Material …\".",
|
||||
"resources.materials.searchPlaceholder": "Material suchen …",
|
||||
|
||||
@@ -435,8 +435,17 @@ export const en: Record<TranslationKey, string> = {
|
||||
"resources.tab.components": "Components",
|
||||
"resources.tab.hatches": "Hatches",
|
||||
"resources.tab.lines": "Lines",
|
||||
"resources.tab.wallStyles": "Wall styles",
|
||||
"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":
|
||||
"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 …",
|
||||
|
||||
@@ -30,6 +30,8 @@ export const sampleProject: Project = {
|
||||
{ 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).
|
||||
{ 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.
|
||||
// "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.
|
||||
layers: [
|
||||
{ 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: "render-int", thickness: 0.015 },
|
||||
],
|
||||
|
||||
@@ -128,6 +128,13 @@ export interface Layer {
|
||||
componentId: string;
|
||||
/** Schichtdicke in Metern. */
|
||||
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). */
|
||||
|
||||
@@ -34,6 +34,7 @@ export function ResourcesPanel() {
|
||||
onPatchLineStyle: host.onPatchLineStyle,
|
||||
onAddLineStyle: host.onAddLineStyle,
|
||||
onDeleteLineStyle: host.onDeleteLineStyle,
|
||||
onPatchWallType: host.onPatchWallType,
|
||||
onImportLineStyles: host.onImportLineStyles,
|
||||
onImportHatches: host.onImportHatches,
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@ import type {
|
||||
StairShape,
|
||||
VerticalAnchor,
|
||||
WallReferenceLine,
|
||||
WallType,
|
||||
} from "../model/types";
|
||||
import type { SnapSettings, ToolId } from "../tools/types";
|
||||
import type { Selection } from "../state/selectionInfo";
|
||||
@@ -114,6 +115,8 @@ export interface PanelHostValue {
|
||||
onPatchLineStyle: (id: string, patch: Partial<LineStyle>) => void;
|
||||
onAddLineStyle: () => 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). */
|
||||
onImportLineStyles: (styles: Omit<LineStyle, "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([]);
|
||||
});
|
||||
});
|
||||
|
||||
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),
|
||||
// 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-
|
||||
|
||||
@@ -1971,6 +1971,26 @@ body {
|
||||
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) ──────────────────────────────────
|
||||
Ein CSS-Grid trägt die Spalten (gridTemplateColumns wird je Tab inline
|
||||
gesetzt). Kopfzeile und Datenzeilen sind `display:contents`, damit ihre
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
HatchStyle,
|
||||
LineStyle,
|
||||
Project,
|
||||
WallType,
|
||||
} from "../model/types";
|
||||
import { PEN_WEIGHTS } from "../model/types";
|
||||
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 als Text und reicht ihn nach oben. Für .lin/.pat-Import.
|
||||
@@ -1377,12 +1449,13 @@ function EmptyState({ text }: { text: string }) {
|
||||
|
||||
// ── Manager-Schublade ──────────────────────────────────────────────────────
|
||||
|
||||
type TabId = "components" | "hatches" | "lines" | "materials";
|
||||
type TabId = "components" | "hatches" | "lines" | "wallStyles" | "materials";
|
||||
|
||||
const TABS: { id: TabId; labelKey: string }[] = [
|
||||
{ id: "components", labelKey: "resources.tab.components" },
|
||||
{ id: "hatches", labelKey: "resources.tab.hatches" },
|
||||
{ id: "lines", labelKey: "resources.tab.lines" },
|
||||
{ id: "wallStyles", labelKey: "resources.tab.wallStyles" },
|
||||
{ id: "materials", labelKey: "resources.tab.materials" },
|
||||
];
|
||||
|
||||
@@ -1397,6 +1470,8 @@ export interface ResourceManagerHandlers {
|
||||
onPatchLineStyle: (id: string, patch: Partial<LineStyle>) => void;
|
||||
onAddLineStyle: () => 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). */
|
||||
onImportLineStyles: (styles: Omit<LineStyle, "id">[]) => void;
|
||||
onImportHatches: (hatches: Omit<HatchStyle, "id">[]) => void;
|
||||
@@ -1483,6 +1558,12 @@ export function ResourceManager({
|
||||
onImportLineStyles={handlers.onImportLineStyles}
|
||||
/>
|
||||
)}
|
||||
{tab === "wallStyles" && (
|
||||
<WallStylesTab
|
||||
project={project}
|
||||
onPatchWallType={handlers.onPatchWallType}
|
||||
/>
|
||||
)}
|
||||
{tab === "materials" && <MaterialsTab />}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
Reference in New Issue
Block a user