ResourceManager: Wandstile + Deckenstile im Master-Detail-Layout
Wand- und Deckenstile bekommen dasselbe Master-Detail wie Schraffuren/Linien: Liste links mit Querschnitt-Thumbnail + Name, Detail rechts mit grossem Querschnitt und Aufbau-Editor (Schichten: Bauteil/Dicke/Fugenlinie, hinzufuegen/entfernen/umordnen), Inline-Rename, Trash-Delete, Neu anlegen. Neuer WallTypeSwatch (hatchPreview) zeichnet die geschichtete Wand/Decke als proportionale Baender mit den Bauteil-Schnittschraffuren. Add/Delete-Handler (addWallType/deleteWallType/addCeilingType/deleteCeilingType, In-Use-Schutz) in App.tsx + host ergaenzt.
This commit is contained in:
+55
@@ -1706,6 +1706,53 @@ export default function App() {
|
|||||||
ct.id === id ? { ...ct, ...patch } : ct,
|
ct.id === id ? { ...ct, ...patch } : ct,
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
// Neuen Wandtyp anlegen: eine Default-Schicht (erstes Bauteil, 200 mm).
|
||||||
|
const addWallType = () =>
|
||||||
|
setProject((p) => {
|
||||||
|
const componentId = p.components[0]?.id ?? "";
|
||||||
|
const wallType: WallType = {
|
||||||
|
id: `wt-${Date.now()}`,
|
||||||
|
name: t("default.wallTypeName"),
|
||||||
|
layers: [{ componentId, thickness: 0.2 }],
|
||||||
|
};
|
||||||
|
return { ...p, wallTypes: [...p.wallTypes, wallType] };
|
||||||
|
});
|
||||||
|
// Wandtyp löschen — geschützt, solange ihn eine Wand verwendet.
|
||||||
|
const deleteWallType = (id: string) =>
|
||||||
|
setProject((p) => {
|
||||||
|
const used = p.walls.some((w) => w.wallTypeId === id);
|
||||||
|
if (used) {
|
||||||
|
const name = p.wallTypes.find((wt) => wt.id === id)?.name ?? id;
|
||||||
|
window.alert(t("alert.wallTypeInUse", { name }));
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
return { ...p, wallTypes: p.wallTypes.filter((wt) => wt.id !== id) };
|
||||||
|
});
|
||||||
|
// Neuen Deckentyp anlegen — analog `addWallType` (150 mm Default).
|
||||||
|
const addCeilingType = () =>
|
||||||
|
setProject((p) => {
|
||||||
|
const componentId = p.components[0]?.id ?? "";
|
||||||
|
const ceilingType: CeilingType = {
|
||||||
|
id: `ct-${Date.now()}`,
|
||||||
|
name: t("default.ceilingTypeName"),
|
||||||
|
layers: [{ componentId, thickness: 0.15 }],
|
||||||
|
};
|
||||||
|
return { ...p, ceilingTypes: [...(p.ceilingTypes ?? []), ceilingType] };
|
||||||
|
});
|
||||||
|
// Deckentyp löschen — geschützt, solange ihn eine Decke verwendet.
|
||||||
|
const deleteCeilingType = (id: string) =>
|
||||||
|
setProject((p) => {
|
||||||
|
const used = (p.ceilings ?? []).some((c) => c.ceilingTypeId === id);
|
||||||
|
if (used) {
|
||||||
|
const name = (p.ceilingTypes ?? []).find((ct) => ct.id === id)?.name ?? id;
|
||||||
|
window.alert(t("alert.ceilingTypeInUse", { name }));
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...p,
|
||||||
|
ceilingTypes: (p.ceilingTypes ?? []).filter((ct) => ct.id !== id),
|
||||||
|
};
|
||||||
|
});
|
||||||
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.
|
||||||
@@ -1996,7 +2043,11 @@ export default function App() {
|
|||||||
onAddLineStyle: addLineStyle,
|
onAddLineStyle: addLineStyle,
|
||||||
onDeleteLineStyle: deleteLineStyle,
|
onDeleteLineStyle: deleteLineStyle,
|
||||||
onPatchWallType: patchWallType,
|
onPatchWallType: patchWallType,
|
||||||
|
onAddWallType: addWallType,
|
||||||
|
onDeleteWallType: deleteWallType,
|
||||||
onPatchCeilingType: patchCeilingType,
|
onPatchCeilingType: patchCeilingType,
|
||||||
|
onAddCeilingType: addCeilingType,
|
||||||
|
onDeleteCeilingType: deleteCeilingType,
|
||||||
onImportLineStyles: importLineStyles,
|
onImportLineStyles: importLineStyles,
|
||||||
onImportHatches: importHatches,
|
onImportHatches: importHatches,
|
||||||
selection,
|
selection,
|
||||||
@@ -2247,7 +2298,11 @@ export default function App() {
|
|||||||
const resourceHandlers: ResourceManagerHandlers = {
|
const resourceHandlers: ResourceManagerHandlers = {
|
||||||
onPatchComponent: patchComponent,
|
onPatchComponent: patchComponent,
|
||||||
onPatchWallType: patchWallType,
|
onPatchWallType: patchWallType,
|
||||||
|
onAddWallType: addWallType,
|
||||||
|
onDeleteWallType: deleteWallType,
|
||||||
onPatchCeilingType: patchCeilingType,
|
onPatchCeilingType: patchCeilingType,
|
||||||
|
onAddCeilingType: addCeilingType,
|
||||||
|
onDeleteCeilingType: deleteCeilingType,
|
||||||
onAddComponent: addComponent,
|
onAddComponent: addComponent,
|
||||||
onDeleteComponent: deleteComponent,
|
onDeleteComponent: deleteComponent,
|
||||||
onPatchHatch: patchHatch,
|
onPatchHatch: patchHatch,
|
||||||
|
|||||||
@@ -446,12 +446,16 @@ export const de = {
|
|||||||
"default.copySuffixLayer": "{name} KOPIE",
|
"default.copySuffixLayer": "{name} KOPIE",
|
||||||
"default.wallTypeSingle": "Wand {thickness} m",
|
"default.wallTypeSingle": "Wand {thickness} m",
|
||||||
"default.ceilingTypeSingle": "Decke {thickness} m",
|
"default.ceilingTypeSingle": "Decke {thickness} m",
|
||||||
|
"default.wallTypeName": "Neuer Wandtyp",
|
||||||
|
"default.ceilingTypeName": "Neuer Deckentyp",
|
||||||
|
|
||||||
// ── Warnungen (Lösch-Schutz) ─────────────────────────────────────────────
|
// ── Warnungen (Lösch-Schutz) ─────────────────────────────────────────────
|
||||||
"alert.layerInUse": "Ebene „{code} {name}\" wird von Bauteilen verwendet und kann nicht gelöscht werden.",
|
"alert.layerInUse": "Ebene „{code} {name}\" wird von Bauteilen verwendet und kann nicht gelöscht werden.",
|
||||||
"alert.componentInUse": "Bauteil „{name}\" wird von einem Wandtyp verwendet und kann nicht gelöscht werden.",
|
"alert.componentInUse": "Bauteil „{name}\" wird von einem Wandtyp verwendet und kann nicht gelöscht werden.",
|
||||||
"alert.hatchInUse": "Schraffur „{name}\" wird von einem Bauteil verwendet und kann nicht gelöscht werden.",
|
"alert.hatchInUse": "Schraffur „{name}\" wird von einem Bauteil verwendet und kann nicht gelöscht werden.",
|
||||||
"alert.lineStyleInUse": "Linienstil „{name}\" wird von einer Schraffur verwendet und kann nicht gelöscht werden.",
|
"alert.lineStyleInUse": "Linienstil „{name}\" wird von einer Schraffur verwendet und kann nicht gelöscht werden.",
|
||||||
|
"alert.wallTypeInUse": "Wandtyp „{name}\" wird von einer Wand verwendet und kann nicht gelöscht werden.",
|
||||||
|
"alert.ceilingTypeInUse": "Deckentyp „{name}\" wird von einer Decke verwendet und kann nicht gelöscht werden.",
|
||||||
|
|
||||||
// ── Ressourcen-Manager ───────────────────────────────────────────────────
|
// ── Ressourcen-Manager ───────────────────────────────────────────────────
|
||||||
"resources.title": "Ressourcen",
|
"resources.title": "Ressourcen",
|
||||||
@@ -470,10 +474,17 @@ export const de = {
|
|||||||
"resources.ceilingStyles.hint":
|
"resources.ceilingStyles.hint":
|
||||||
"Je Deckentyp (liegender Schichtaufbau, oben → unten) die Schichtfugen als wählbaren Linienstil. Ohne Zuweisung: Haarlinie 0.02 mm.",
|
"Je Deckentyp (liegender Schichtaufbau, oben → unten) die Schichtfugen als wählbaren Linienstil. Ohne Zuweisung: Haarlinie 0.02 mm.",
|
||||||
"resources.ceilingStyles.empty": "Noch keine Deckentypen.",
|
"resources.ceilingStyles.empty": "Noch keine Deckentypen.",
|
||||||
|
"resources.wallStyles.placeholder": "Wandtyp",
|
||||||
|
"resources.ceilingStyles.placeholder": "Deckentyp",
|
||||||
"resources.col.layer": "Schicht",
|
"resources.col.layer": "Schicht",
|
||||||
|
"resources.col.component": "Bauteil",
|
||||||
"resources.col.thickness": "Dicke (mm)",
|
"resources.col.thickness": "Dicke (mm)",
|
||||||
"resources.col.joint": "Fuge",
|
"resources.col.joint": "Fuge",
|
||||||
"resources.joint.default": "— (Haarlinie)",
|
"resources.joint.default": "— (Haarlinie)",
|
||||||
|
"resources.layer.add": "Schicht hinzufügen",
|
||||||
|
"resources.layer.remove": "Schicht entfernen",
|
||||||
|
"resources.layer.moveUp": "Schicht nach oben",
|
||||||
|
"resources.layer.moveDown": "Schicht nach unten",
|
||||||
|
|
||||||
"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 …\".",
|
||||||
@@ -556,6 +567,8 @@ export const de = {
|
|||||||
"resources.col.lineStyle": "Linienstil",
|
"resources.col.lineStyle": "Linienstil",
|
||||||
"resources.lineStyle.none": "— ohne —",
|
"resources.lineStyle.none": "— ohne —",
|
||||||
"resources.delete.hatch": "Schraffur „{name}\" löschen",
|
"resources.delete.hatch": "Schraffur „{name}\" löschen",
|
||||||
|
"resources.delete.wallType": "Wandtyp „{name}\" löschen",
|
||||||
|
"resources.delete.ceilingType": "Deckentyp „{name}\" löschen",
|
||||||
|
|
||||||
// Schraffur-/Linien-Typ (Master-Detail-Editor).
|
// Schraffur-/Linien-Typ (Master-Detail-Editor).
|
||||||
"resources.field.type": "Typ",
|
"resources.field.type": "Typ",
|
||||||
|
|||||||
@@ -443,12 +443,16 @@ export const en: Record<TranslationKey, string> = {
|
|||||||
"default.copySuffixLayer": "{name} COPY",
|
"default.copySuffixLayer": "{name} COPY",
|
||||||
"default.wallTypeSingle": "Wall {thickness} m",
|
"default.wallTypeSingle": "Wall {thickness} m",
|
||||||
"default.ceilingTypeSingle": "Ceiling {thickness} m",
|
"default.ceilingTypeSingle": "Ceiling {thickness} m",
|
||||||
|
"default.wallTypeName": "New wall type",
|
||||||
|
"default.ceilingTypeName": "New ceiling type",
|
||||||
|
|
||||||
// Alerts.
|
// Alerts.
|
||||||
"alert.layerInUse": "Layer “{code} {name}” is used by components and cannot be deleted.",
|
"alert.layerInUse": "Layer “{code} {name}” is used by components and cannot be deleted.",
|
||||||
"alert.componentInUse": "Component “{name}” is used by a wall type and cannot be deleted.",
|
"alert.componentInUse": "Component “{name}” is used by a wall type and cannot be deleted.",
|
||||||
"alert.hatchInUse": "Hatch “{name}” is used by a component and cannot be deleted.",
|
"alert.hatchInUse": "Hatch “{name}” is used by a component and cannot be deleted.",
|
||||||
"alert.lineStyleInUse": "Line style “{name}” is used by a hatch and cannot be deleted.",
|
"alert.lineStyleInUse": "Line style “{name}” is used by a hatch and cannot be deleted.",
|
||||||
|
"alert.wallTypeInUse": "Wall type “{name}” is used by a wall and cannot be deleted.",
|
||||||
|
"alert.ceilingTypeInUse": "Ceiling type “{name}” is used by a ceiling and cannot be deleted.",
|
||||||
|
|
||||||
// Resource manager.
|
// Resource manager.
|
||||||
"resources.title": "Resources",
|
"resources.title": "Resources",
|
||||||
@@ -467,10 +471,17 @@ export const en: Record<TranslationKey, string> = {
|
|||||||
"resources.ceilingStyles.hint":
|
"resources.ceilingStyles.hint":
|
||||||
"Per ceiling type (horizontal build-up, top → bottom), the layer joints as a selectable line style. Unassigned: 0.02 mm hairline.",
|
"Per ceiling type (horizontal build-up, top → bottom), the layer joints as a selectable line style. Unassigned: 0.02 mm hairline.",
|
||||||
"resources.ceilingStyles.empty": "No ceiling types yet.",
|
"resources.ceilingStyles.empty": "No ceiling types yet.",
|
||||||
|
"resources.wallStyles.placeholder": "Wall type",
|
||||||
|
"resources.ceilingStyles.placeholder": "Ceiling type",
|
||||||
"resources.col.layer": "Layer",
|
"resources.col.layer": "Layer",
|
||||||
|
"resources.col.component": "Component",
|
||||||
"resources.col.thickness": "Thickness (mm)",
|
"resources.col.thickness": "Thickness (mm)",
|
||||||
"resources.col.joint": "Joint",
|
"resources.col.joint": "Joint",
|
||||||
"resources.joint.default": "— (hairline)",
|
"resources.joint.default": "— (hairline)",
|
||||||
|
"resources.layer.add": "Add layer",
|
||||||
|
"resources.layer.remove": "Remove layer",
|
||||||
|
"resources.layer.moveUp": "Move layer up",
|
||||||
|
"resources.layer.moveDown": "Move layer down",
|
||||||
|
|
||||||
"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.",
|
||||||
@@ -551,6 +562,8 @@ export const en: Record<TranslationKey, string> = {
|
|||||||
"resources.col.lineStyle": "Line style",
|
"resources.col.lineStyle": "Line style",
|
||||||
"resources.lineStyle.none": "— none —",
|
"resources.lineStyle.none": "— none —",
|
||||||
"resources.delete.hatch": "Delete hatch “{name}”",
|
"resources.delete.hatch": "Delete hatch “{name}”",
|
||||||
|
"resources.delete.wallType": "Delete wall type “{name}”",
|
||||||
|
"resources.delete.ceilingType": "Delete ceiling type “{name}”",
|
||||||
|
|
||||||
// Hatch/line type (master-detail editor).
|
// Hatch/line type (master-detail editor).
|
||||||
"resources.field.type": "Type",
|
"resources.field.type": "Type",
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ export function ResourcesPanel() {
|
|||||||
onAddLineStyle: host.onAddLineStyle,
|
onAddLineStyle: host.onAddLineStyle,
|
||||||
onDeleteLineStyle: host.onDeleteLineStyle,
|
onDeleteLineStyle: host.onDeleteLineStyle,
|
||||||
onPatchWallType: host.onPatchWallType,
|
onPatchWallType: host.onPatchWallType,
|
||||||
|
onAddWallType: host.onAddWallType,
|
||||||
|
onDeleteWallType: host.onDeleteWallType,
|
||||||
onPatchCeilingType: host.onPatchCeilingType,
|
onPatchCeilingType: host.onPatchCeilingType,
|
||||||
|
onAddCeilingType: host.onAddCeilingType,
|
||||||
|
onDeleteCeilingType: host.onDeleteCeilingType,
|
||||||
onImportLineStyles: host.onImportLineStyles,
|
onImportLineStyles: host.onImportLineStyles,
|
||||||
onImportHatches: host.onImportHatches,
|
onImportHatches: host.onImportHatches,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -120,8 +120,12 @@ export interface PanelHostValue {
|
|||||||
onDeleteLineStyle: (id: string) => void;
|
onDeleteLineStyle: (id: string) => void;
|
||||||
/** Wandstile: immutable Änderung eines Wandtyps (Schichtfugen-Stile). */
|
/** Wandstile: immutable Änderung eines Wandtyps (Schichtfugen-Stile). */
|
||||||
onPatchWallType: (id: string, patch: Partial<WallType>) => void;
|
onPatchWallType: (id: string, patch: Partial<WallType>) => void;
|
||||||
|
onAddWallType: () => void;
|
||||||
|
onDeleteWallType: (id: string) => void;
|
||||||
/** Deckenstile: immutable Änderung eines Deckentyps (Schichtfugen-Stile). */
|
/** Deckenstile: immutable Änderung eines Deckentyps (Schichtfugen-Stile). */
|
||||||
onPatchCeilingType: (id: string, patch: Partial<CeilingType>) => void;
|
onPatchCeilingType: (id: string, patch: Partial<CeilingType>) => void;
|
||||||
|
onAddCeilingType: () => void;
|
||||||
|
onDeleteCeilingType: (id: string) => 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;
|
||||||
|
|||||||
@@ -2095,6 +2095,68 @@ body {
|
|||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Wand-/Deckenstil-Aufbau (Master-Detail) ────────────────────────────── */
|
||||||
|
/* Der Querschnitt-Swatch (WallTypeSwatch) ist frei skalierbar. */
|
||||||
|
.res-wallswatch {
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Aufbau-Editor: die Schichten gestapelt, darunter „Schicht hinzufügen". */
|
||||||
|
.res-layers {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.res-layer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--input);
|
||||||
|
}
|
||||||
|
.res-layer-move {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.res-layer-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 16px;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 5px;
|
||||||
|
background: var(--panel-2);
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 9px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s, border-color 0.12s;
|
||||||
|
}
|
||||||
|
.res-layer-btn:hover:not(:disabled) {
|
||||||
|
background: var(--accent-dim);
|
||||||
|
border-color: var(--accent-border);
|
||||||
|
}
|
||||||
|
.res-layer-btn:disabled {
|
||||||
|
opacity: 0.35;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.res-layer-fields {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.res-layer-fields .res-field {
|
||||||
|
grid-template-columns: 76px 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── 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
|
||||||
|
|||||||
+298
-105
@@ -24,6 +24,7 @@ import type {
|
|||||||
ComponentMaterial,
|
ComponentMaterial,
|
||||||
HatchPattern,
|
HatchPattern,
|
||||||
HatchStyle,
|
HatchStyle,
|
||||||
|
Layer,
|
||||||
LineStyle,
|
LineStyle,
|
||||||
Project,
|
Project,
|
||||||
WallType,
|
WallType,
|
||||||
@@ -43,7 +44,8 @@ import {
|
|||||||
} from "../materials/ambientcg";
|
} from "../materials/ambientcg";
|
||||||
import { requestMaterialPreview, cancelMaterialPreview } from "../materials/spherePreview";
|
import { requestMaterialPreview, cancelMaterialPreview } from "../materials/spherePreview";
|
||||||
import { EyeIcon } from "./EyeIcon";
|
import { EyeIcon } from "./EyeIcon";
|
||||||
import { HatchSwatch, LineSwatch } from "./hatchPreview";
|
import { HatchSwatch, LineSwatch, WallTypeSwatch } from "./hatchPreview";
|
||||||
|
import type { SwatchLayer } from "./hatchPreview";
|
||||||
import {
|
import {
|
||||||
segmentsToDash,
|
segmentsToDash,
|
||||||
dashToSegments,
|
dashToSegments,
|
||||||
@@ -1498,26 +1500,54 @@ function LinesTab({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Wandstile (Wall Styles) ────────────────────────────────────────────────
|
// ── Wandstile & Deckenstile (geschichteter Aufbau) ─────────────────────────
|
||||||
// Pro Wandtyp die geordneten Schichten (Bauteil + Dicke) und je INNERER Fuge
|
// Beide Ressourcen teilen Struktur (WallType/CeilingType = {id,name,layers[]})
|
||||||
// (zwischen zwei aufeinanderfolgenden Schichten) ein LineStyle-Dropdown, das an
|
// und Editor: ein Master-Detail-Layout wie bei Schraffuren/Linien. Liste links
|
||||||
// layer[i].jointLineStyleId hängt (i = Schicht, deren innere Kante die Fuge ist).
|
// = je Typ eine Zeile mit kleiner Querschnitt-Vorschau + Name; Detail rechts =
|
||||||
// Die innerste Schicht hat keine innere Fuge (ihre Kante ist der Wand-Innenumriss).
|
// Aufbau-Editor (Schichten stapeln: Bauteil/Dicke/Fugenlinie, hinzufügen/
|
||||||
|
// entfernen/umordnen) + großer Live-Querschnitt. Ein gemeinsames Detail-Panel
|
||||||
|
// {@link LayeredStyleDetail}, parametrisiert über die Orientierung (Wand:
|
||||||
|
// Bänder nebeneinander / Decke: liegend gestapelt) und die jeweiligen Handler.
|
||||||
|
//
|
||||||
|
// Fugen: `layer[i].jointLineStyleId` ist der Linienstil der INNEREN Kante der
|
||||||
|
// Schicht i (Fuge zur nächst-inneren Schicht). Die innerste Schicht hat keine
|
||||||
|
// innere Fuge (ihre Kante ist der Innenumriss) → dort kein Dropdown.
|
||||||
|
|
||||||
// Spalten: Schicht | Dicke (mm) | Fuge (innere).
|
/** Wand- und Deckentyp teilen dieselbe Form (`{id,name,layers}`). */
|
||||||
const WALLSTYLE_COLUMNS: ResColumn[] = [
|
type LayeredStyle = WallType | CeilingType;
|
||||||
{ 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({
|
/**
|
||||||
|
* Baut die Querschnitt-Bänder (eine je Schicht) aus den Bauteil-Schnitt-
|
||||||
|
* schraffuren: Schicht → Bauteil → dessen `hatchId` → HatchStyle. Fehlt das
|
||||||
|
* Bauteil/die Schraffur, bleibt das Band leer (weiß).
|
||||||
|
*/
|
||||||
|
function swatchLayersOf(style: LayeredStyle, project: Project): SwatchLayer[] {
|
||||||
|
return style.layers.map((l) => {
|
||||||
|
const comp = project.components.find((c) => c.id === l.componentId);
|
||||||
|
const hatch = comp
|
||||||
|
? project.hatches.find((hh) => hh.id === comp.hatchId)
|
||||||
|
: undefined;
|
||||||
|
return { hatch, thickness: l.thickness };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Detail-Panel EINES Wand-/Deckentyps (rechte Seite des Master-Detail-Layouts). */
|
||||||
|
function LayeredStyleDetail({
|
||||||
|
style,
|
||||||
project,
|
project,
|
||||||
onPatchWallType,
|
orientation,
|
||||||
|
onPatch,
|
||||||
|
onDelete,
|
||||||
|
deleteTitle,
|
||||||
|
namePlaceholder,
|
||||||
}: {
|
}: {
|
||||||
|
style: LayeredStyle;
|
||||||
project: Project;
|
project: Project;
|
||||||
onPatchWallType: (id: string, patch: Partial<WallType>) => void;
|
orientation: "wall" | "ceiling";
|
||||||
|
onPatch: (patch: Partial<LayeredStyle>) => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
deleteTitle: string;
|
||||||
|
namePlaceholder: string;
|
||||||
}) {
|
}) {
|
||||||
// Sentinel „— (Haarlinie)": kein Feld gesetzt → Standard-Haarlinie 0.02 mm.
|
// Sentinel „— (Haarlinie)": kein Feld gesetzt → Standard-Haarlinie 0.02 mm.
|
||||||
const DEFAULT = "__default__";
|
const DEFAULT = "__default__";
|
||||||
@@ -1525,111 +1555,266 @@ function WallStylesTab({
|
|||||||
{ value: DEFAULT, label: t("resources.joint.default") },
|
{ value: DEFAULT, label: t("resources.joint.default") },
|
||||||
...project.lineStyles.map((l) => ({ value: l.id, label: l.name })),
|
...project.lineStyles.map((l) => ({ value: l.id, label: l.name })),
|
||||||
];
|
];
|
||||||
const setJoint = (wt: WallType, li: number, styleId: string | undefined) => {
|
const compOptions =
|
||||||
const layers = wt.layers.map((l, i) =>
|
project.components.length > 0
|
||||||
i === li ? { ...l, jointLineStyleId: styleId } : l,
|
? project.components.map((c) => ({ value: c.id, label: c.name }))
|
||||||
);
|
: [{ value: "", label: "—" }];
|
||||||
onPatchWallType(wt.id, { layers });
|
|
||||||
|
const layers = style.layers;
|
||||||
|
const setLayers = (next: Layer[]) => onPatch({ layers: next });
|
||||||
|
const patchLayer = (i: number, patch: Partial<Layer>) =>
|
||||||
|
setLayers(layers.map((l, k) => (k === i ? { ...l, ...patch } : l)));
|
||||||
|
const addLayer = () =>
|
||||||
|
setLayers([
|
||||||
|
...layers,
|
||||||
|
{ componentId: project.components[0]?.id ?? "", thickness: 0.1 },
|
||||||
|
]);
|
||||||
|
const removeLayer = (i: number) => {
|
||||||
|
if (layers.length <= 1) return;
|
||||||
|
setLayers(layers.filter((_, k) => k !== i));
|
||||||
};
|
};
|
||||||
|
const moveLayer = (i: number, dir: -1 | 1) => {
|
||||||
|
const j = i + dir;
|
||||||
|
if (j < 0 || j >= layers.length) return;
|
||||||
|
const next = layers.slice();
|
||||||
|
[next[i], next[j]] = [next[j], next[i]];
|
||||||
|
setLayers(next);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="res-tab">
|
<div className="res-md-detail-inner">
|
||||||
<div className="res-hint">{t("resources.wallStyles.hint")}</div>
|
<div className="res-md-head">
|
||||||
{project.wallTypes.map((wt) => (
|
<input
|
||||||
<div key={wt.id} className="res-wallstyle">
|
className="res-input res-md-rename"
|
||||||
<div className="res-wallstyle-head">{wt.name}</div>
|
value={style.name}
|
||||||
<ResTable columns={WALLSTYLE_COLUMNS} template={WALLSTYLE_TEMPLATE}>
|
placeholder={namePlaceholder}
|
||||||
{wt.layers.map((layer, li) => {
|
onChange={(e) => onPatch({ name: e.target.value })}
|
||||||
const comp = project.components.find((c) => c.id === layer.componentId);
|
/>
|
||||||
const isInner = li === wt.layers.length - 1;
|
<DeleteButton onClick={onDelete} title={deleteTitle} />
|
||||||
return (
|
</div>
|
||||||
<ResRow key={li}>
|
|
||||||
<ResCell emphasis>{comp ? comp.name : layer.componentId}</ResCell>
|
<div className="res-md-preview">
|
||||||
<ResCell align="right">{Math.round(layer.thickness * 1000)}</ResCell>
|
<WallTypeSwatch
|
||||||
<ResCell>
|
layers={swatchLayersOf(style, project)}
|
||||||
{isInner ? (
|
width={orientation === "wall" ? 240 : 132}
|
||||||
<span className="res-joint-none">–</span>
|
height={orientation === "wall" ? 96 : 132}
|
||||||
) : (
|
orientation={orientation}
|
||||||
<SelectField
|
/>
|
||||||
value={layer.jointLineStyleId ?? DEFAULT}
|
</div>
|
||||||
onChange={(v) => setJoint(wt, li, v === DEFAULT ? undefined : v)}
|
|
||||||
options={jointOptions}
|
<div className="res-layers">
|
||||||
/>
|
{layers.map((layer, i) => {
|
||||||
)}
|
const isInner = i === layers.length - 1;
|
||||||
</ResCell>
|
return (
|
||||||
</ResRow>
|
<div key={i} className="res-layer">
|
||||||
);
|
<div className="res-layer-move">
|
||||||
})}
|
<button
|
||||||
</ResTable>
|
type="button"
|
||||||
</div>
|
className="res-layer-btn"
|
||||||
))}
|
title={t("resources.layer.moveUp")}
|
||||||
{project.wallTypes.length === 0 && (
|
disabled={i === 0}
|
||||||
<EmptyState text={t("resources.wallStyles.empty")} />
|
onClick={() => moveLayer(i, -1)}
|
||||||
)}
|
>
|
||||||
|
▲
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="res-layer-btn"
|
||||||
|
title={t("resources.layer.moveDown")}
|
||||||
|
disabled={isInner}
|
||||||
|
onClick={() => moveLayer(i, 1)}
|
||||||
|
>
|
||||||
|
▼
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="res-layer-fields">
|
||||||
|
<FieldRow label={t("resources.col.component")}>
|
||||||
|
<SelectField
|
||||||
|
value={layer.componentId}
|
||||||
|
onChange={(componentId) => patchLayer(i, { componentId })}
|
||||||
|
options={compOptions}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
|
<FieldRow label={t("resources.col.thickness")}>
|
||||||
|
<NumberField
|
||||||
|
value={Math.round(layer.thickness * 1000)}
|
||||||
|
onChange={(mm) =>
|
||||||
|
patchLayer(i, { thickness: Math.max(0, mm) / 1000 })
|
||||||
|
}
|
||||||
|
step={5}
|
||||||
|
min={0}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
|
<FieldRow label={t("resources.col.joint")}>
|
||||||
|
{isInner ? (
|
||||||
|
<span className="res-joint-none">–</span>
|
||||||
|
) : (
|
||||||
|
<SelectField
|
||||||
|
value={layer.jointLineStyleId ?? DEFAULT}
|
||||||
|
onChange={(v) =>
|
||||||
|
patchLayer(i, {
|
||||||
|
jointLineStyleId: v === DEFAULT ? undefined : v,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
options={jointOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</FieldRow>
|
||||||
|
</div>
|
||||||
|
<DeleteButton
|
||||||
|
onClick={() => removeLayer(i)}
|
||||||
|
title={t("resources.layer.remove")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<button type="button" className="res-add" onClick={addLayer}>
|
||||||
|
<span className="res-add-plus">+</span> {t("resources.layer.add")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tab „Deckenstile" — das liegende Gegenstück zu {@link WallStylesTab}: pro
|
* Gemeinsamer Master-Detail-Tab-Rumpf für Wand-/Deckenstile: Liste links
|
||||||
* Deckentyp (`project.ceilingTypes`) die Schichten mit Dicke + einem per-Fuge
|
* (Querschnitt-Vorschau + Name, +-Neu in der Fusszeile), Detail rechts über
|
||||||
* wählbaren Linienstil (Line Manager). Gleiche Tabelle/Sentinel-Logik wie bei
|
* {@link LayeredStyleDetail}. Parametrisiert über die Ressource + Orientierung.
|
||||||
* den Wandstilen — nur die Ressource (`ceilingTypes` statt `wallTypes`) und der
|
*/
|
||||||
* Handler (`onPatchCeilingType`) unterscheiden sich.
|
function LayeredStylesTab({
|
||||||
|
project,
|
||||||
|
types,
|
||||||
|
orientation,
|
||||||
|
hintKey,
|
||||||
|
emptyKey,
|
||||||
|
placeholderKey,
|
||||||
|
deleteKeyPrefix,
|
||||||
|
onPatch,
|
||||||
|
onAdd,
|
||||||
|
onDelete,
|
||||||
|
}: {
|
||||||
|
project: Project;
|
||||||
|
types: LayeredStyle[];
|
||||||
|
orientation: "wall" | "ceiling";
|
||||||
|
hintKey: string;
|
||||||
|
emptyKey: string;
|
||||||
|
placeholderKey: string;
|
||||||
|
deleteKeyPrefix: string;
|
||||||
|
onPatch: (id: string, patch: Partial<LayeredStyle>) => void;
|
||||||
|
onAdd: () => void;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
|
}) {
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
const selected = types.find((s) => s.id === selectedId) ?? types[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="res-tab">
|
||||||
|
<div className="res-hint">{t(hintKey)}</div>
|
||||||
|
<div className="res-md">
|
||||||
|
<div className="res-md-list">
|
||||||
|
{types.map((s) => (
|
||||||
|
<button
|
||||||
|
key={s.id}
|
||||||
|
className={`res-md-row${selected?.id === s.id ? " active" : ""}`}
|
||||||
|
onClick={() => setSelectedId(s.id)}
|
||||||
|
>
|
||||||
|
<span className="res-md-row-thumb">
|
||||||
|
<WallTypeSwatch
|
||||||
|
layers={swatchLayersOf(s, project)}
|
||||||
|
width={46}
|
||||||
|
height={28}
|
||||||
|
orientation={orientation}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="res-md-row-name">{s.name}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{types.length === 0 && (
|
||||||
|
<div className="res-md-empty">{t(emptyKey)}</div>
|
||||||
|
)}
|
||||||
|
<div className="res-md-listfoot">
|
||||||
|
<button className="res-add" onClick={onAdd}>
|
||||||
|
<span className="res-add-plus">+</span> {t("resources.add")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="res-md-detail">
|
||||||
|
{selected ? (
|
||||||
|
<LayeredStyleDetail
|
||||||
|
key={selected.id}
|
||||||
|
style={selected}
|
||||||
|
project={project}
|
||||||
|
orientation={orientation}
|
||||||
|
onPatch={(patch) => onPatch(selected.id, patch)}
|
||||||
|
onDelete={() => onDelete(selected.id)}
|
||||||
|
deleteTitle={t(deleteKeyPrefix, { name: selected.name })}
|
||||||
|
namePlaceholder={t(placeholderKey)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="res-md-empty">{t(emptyKey)}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function WallStylesTab({
|
||||||
|
project,
|
||||||
|
onPatchWallType,
|
||||||
|
onAddWallType,
|
||||||
|
onDeleteWallType,
|
||||||
|
}: {
|
||||||
|
project: Project;
|
||||||
|
onPatchWallType: (id: string, patch: Partial<WallType>) => void;
|
||||||
|
onAddWallType: () => void;
|
||||||
|
onDeleteWallType: (id: string) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<LayeredStylesTab
|
||||||
|
project={project}
|
||||||
|
types={project.wallTypes}
|
||||||
|
orientation="wall"
|
||||||
|
hintKey="resources.wallStyles.hint"
|
||||||
|
emptyKey="resources.wallStyles.empty"
|
||||||
|
placeholderKey="resources.wallStyles.placeholder"
|
||||||
|
deleteKeyPrefix="resources.delete.wallType"
|
||||||
|
onPatch={onPatchWallType}
|
||||||
|
onAdd={onAddWallType}
|
||||||
|
onDelete={onDeleteWallType}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tab „Deckenstile" — das liegende Gegenstück zu {@link WallStylesTab}: nutzt
|
||||||
|
* denselben {@link LayeredStylesTab}-Rumpf, nur die Ressource (`ceilingTypes`),
|
||||||
|
* die Orientierung (gestapelt) und die Handler unterscheiden sich.
|
||||||
*/
|
*/
|
||||||
function CeilingStylesTab({
|
function CeilingStylesTab({
|
||||||
project,
|
project,
|
||||||
onPatchCeilingType,
|
onPatchCeilingType,
|
||||||
|
onAddCeilingType,
|
||||||
|
onDeleteCeilingType,
|
||||||
}: {
|
}: {
|
||||||
project: Project;
|
project: Project;
|
||||||
onPatchCeilingType: (id: string, patch: Partial<CeilingType>) => void;
|
onPatchCeilingType: (id: string, patch: Partial<CeilingType>) => void;
|
||||||
|
onAddCeilingType: () => void;
|
||||||
|
onDeleteCeilingType: (id: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const DEFAULT = "__default__";
|
|
||||||
const jointOptions = [
|
|
||||||
{ value: DEFAULT, label: t("resources.joint.default") },
|
|
||||||
...project.lineStyles.map((l) => ({ value: l.id, label: l.name })),
|
|
||||||
];
|
|
||||||
const setJoint = (ct: CeilingType, li: number, styleId: string | undefined) => {
|
|
||||||
const layers = ct.layers.map((l, i) =>
|
|
||||||
i === li ? { ...l, jointLineStyleId: styleId } : l,
|
|
||||||
);
|
|
||||||
onPatchCeilingType(ct.id, { layers });
|
|
||||||
};
|
|
||||||
const ceilingTypes = project.ceilingTypes ?? [];
|
|
||||||
return (
|
return (
|
||||||
<div className="res-tab">
|
<LayeredStylesTab
|
||||||
<div className="res-hint">{t("resources.ceilingStyles.hint")}</div>
|
project={project}
|
||||||
{ceilingTypes.map((ct) => (
|
types={project.ceilingTypes ?? []}
|
||||||
<div key={ct.id} className="res-wallstyle">
|
orientation="ceiling"
|
||||||
<div className="res-wallstyle-head">{ct.name}</div>
|
hintKey="resources.ceilingStyles.hint"
|
||||||
<ResTable columns={WALLSTYLE_COLUMNS} template={WALLSTYLE_TEMPLATE}>
|
emptyKey="resources.ceilingStyles.empty"
|
||||||
{ct.layers.map((layer, li) => {
|
placeholderKey="resources.ceilingStyles.placeholder"
|
||||||
const comp = project.components.find((c) => c.id === layer.componentId);
|
deleteKeyPrefix="resources.delete.ceilingType"
|
||||||
const isInner = li === ct.layers.length - 1;
|
onPatch={onPatchCeilingType}
|
||||||
return (
|
onAdd={onAddCeilingType}
|
||||||
<ResRow key={li}>
|
onDelete={onDeleteCeilingType}
|
||||||
<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(ct, li, v === DEFAULT ? undefined : v)}
|
|
||||||
options={jointOptions}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ResCell>
|
|
||||||
</ResRow>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ResTable>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{ceilingTypes.length === 0 && (
|
|
||||||
<EmptyState text={t("resources.ceilingStyles.empty")} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1911,8 +2096,12 @@ export interface ResourceManagerHandlers {
|
|||||||
onDeleteLineStyle: (id: string) => void;
|
onDeleteLineStyle: (id: string) => void;
|
||||||
/** Wandstile: immutable Änderung eines Wandtyps (z. B. Schichtfugen-Stile). */
|
/** Wandstile: immutable Änderung eines Wandtyps (z. B. Schichtfugen-Stile). */
|
||||||
onPatchWallType: (id: string, patch: Partial<WallType>) => void;
|
onPatchWallType: (id: string, patch: Partial<WallType>) => void;
|
||||||
|
onAddWallType: () => void;
|
||||||
|
onDeleteWallType: (id: string) => void;
|
||||||
/** Deckenstile: immutable Änderung eines Deckentyps (z. B. Schichtfugen-Stile). */
|
/** Deckenstile: immutable Änderung eines Deckentyps (z. B. Schichtfugen-Stile). */
|
||||||
onPatchCeilingType: (id: string, patch: Partial<CeilingType>) => void;
|
onPatchCeilingType: (id: string, patch: Partial<CeilingType>) => void;
|
||||||
|
onAddCeilingType: () => void;
|
||||||
|
onDeleteCeilingType: (id: string) => 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;
|
||||||
@@ -2097,12 +2286,16 @@ export function ResourceManager({
|
|||||||
<WallStylesTab
|
<WallStylesTab
|
||||||
project={project}
|
project={project}
|
||||||
onPatchWallType={handlers.onPatchWallType}
|
onPatchWallType={handlers.onPatchWallType}
|
||||||
|
onAddWallType={handlers.onAddWallType}
|
||||||
|
onDeleteWallType={handlers.onDeleteWallType}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{tab === "ceilingStyles" && (
|
{tab === "ceilingStyles" && (
|
||||||
<CeilingStylesTab
|
<CeilingStylesTab
|
||||||
project={project}
|
project={project}
|
||||||
onPatchCeilingType={handlers.onPatchCeilingType}
|
onPatchCeilingType={handlers.onPatchCeilingType}
|
||||||
|
onAddCeilingType={handlers.onAddCeilingType}
|
||||||
|
onDeleteCeilingType={handlers.onDeleteCeilingType}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{tab === "materials" && <MaterialsTab />}
|
{tab === "materials" && <MaterialsTab />}
|
||||||
|
|||||||
+163
-20
@@ -70,25 +70,14 @@ function parallelLines(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Live-SVG-Swatch einer Schraffur. Rendert:
|
* Reine Muster-Füllung EINER Schraffur als SVG-Knoten für eine Box `w`×`h`
|
||||||
* • Bild-Schraffur (`kind==="image"`) als gekachelten <pattern>/<image>-Fill
|
* (ohne Rahmen/Clip — die übernimmt der Aufrufer). Ausgelagert aus
|
||||||
* mit unabhängiger Skalierung (scaleX/scaleY) und Rotation.
|
* {@link HatchSwatch}, damit auch der geschichtete {@link WallTypeSwatch} pro
|
||||||
* • Vektor-Schraffur nach `pattern`: solid/none/diagonal/crosshatch/insulation.
|
* Band DASSELBE Muster zeichnet. `uid` macht interne <pattern>-IDs eindeutig
|
||||||
* Untermodus `lines==="random"` verteilt kurze Striche zufällig (Kies/Splitt).
|
* (mehrere Bänder je SVG).
|
||||||
* `scale` verdichtet/streckt das Muster, `angle` dreht es.
|
|
||||||
*/
|
*/
|
||||||
export function HatchSwatch({
|
function hatchInner(hatch: HatchStyle, w: number, h: number, uid: string): React.ReactNode {
|
||||||
hatch,
|
const patId = `hsp_${uid}`;
|
||||||
size = 34,
|
|
||||||
}: {
|
|
||||||
hatch: HatchStyle;
|
|
||||||
size?: number;
|
|
||||||
}) {
|
|
||||||
const rawId = useId().replace(/[^a-zA-Z0-9_-]/g, "");
|
|
||||||
const clipId = `hsc_${rawId}`;
|
|
||||||
const patId = `hsp_${rawId}`;
|
|
||||||
const w = size;
|
|
||||||
const h = size;
|
|
||||||
|
|
||||||
// Grundabstand der Musterlinien (px), über den Maßstab moduliert.
|
// Grundabstand der Musterlinien (px), über den Maßstab moduliert.
|
||||||
const scale = hatch.scale > 0 ? hatch.scale : 1;
|
const scale = hatch.scale > 0 ? hatch.scale : 1;
|
||||||
@@ -101,7 +90,7 @@ export function HatchSwatch({
|
|||||||
// Bild-Kachel: Basiskachel skaliert mit scaleX/scaleY, gedreht via
|
// Bild-Kachel: Basiskachel skaliert mit scaleX/scaleY, gedreht via
|
||||||
// patternTransform. Fehlt die Quelle, zeigen wir einen dezenten Rahmen.
|
// patternTransform. Fehlt die Quelle, zeigen wir einen dezenten Rahmen.
|
||||||
const src = hatch.image?.src;
|
const src = hatch.image?.src;
|
||||||
const base = Math.max(6, size * 0.6);
|
const base = Math.max(6, Math.min(w, h) * 0.6);
|
||||||
const tileW = Math.max(2, base * (hatch.image?.scaleX ?? 1));
|
const tileW = Math.max(2, base * (hatch.image?.scaleX ?? 1));
|
||||||
const tileH = Math.max(2, base * (hatch.image?.scaleY ?? 1));
|
const tileH = Math.max(2, base * (hatch.image?.scaleY ?? 1));
|
||||||
const rot = hatch.image?.rotation ?? 0;
|
const rot = hatch.image?.rotation ?? 0;
|
||||||
@@ -217,6 +206,29 @@ export function HatchSwatch({
|
|||||||
inner = parallelLines(w, h, angle, spacing, 0.8, "d");
|
inner = parallelLines(w, h, angle, spacing, 0.8, "d");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Live-SVG-Swatch einer Schraffur. Rendert:
|
||||||
|
* • Bild-Schraffur (`kind==="image"`) als gekachelten <pattern>/<image>-Fill
|
||||||
|
* mit unabhängiger Skalierung (scaleX/scaleY) und Rotation.
|
||||||
|
* • Vektor-Schraffur nach `pattern`: solid/none/diagonal/crosshatch/insulation.
|
||||||
|
* Untermodus `lines==="random"` verteilt kurze Striche zufällig (Kies/Splitt).
|
||||||
|
* `scale` verdichtet/streckt das Muster, `angle` dreht es.
|
||||||
|
*/
|
||||||
|
export function HatchSwatch({
|
||||||
|
hatch,
|
||||||
|
size = 34,
|
||||||
|
}: {
|
||||||
|
hatch: HatchStyle;
|
||||||
|
size?: number;
|
||||||
|
}) {
|
||||||
|
const rawId = useId().replace(/[^a-zA-Z0-9_-]/g, "");
|
||||||
|
const clipId = `hsc_${rawId}`;
|
||||||
|
const w = size;
|
||||||
|
const h = size;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
className="res-swatch"
|
className="res-swatch"
|
||||||
@@ -240,7 +252,138 @@ export function HatchSwatch({
|
|||||||
stroke="var(--border)"
|
stroke="var(--border)"
|
||||||
strokeWidth={1}
|
strokeWidth={1}
|
||||||
/>
|
/>
|
||||||
<g clipPath={`url(#${clipId})`}>{inner}</g>
|
<g clipPath={`url(#${clipId})`}>{hatchInner(hatch, w, h, rawId)}</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eine Schicht für den geschichteten Wand-/Decken-Querschnitt: ihre
|
||||||
|
* Schnittschraffur (oder `undefined` = leer/weiß) und die relative Dicke
|
||||||
|
* (proportional zur Bandbreite). Farbe kommt wie sonst über `currentColor`.
|
||||||
|
*/
|
||||||
|
export interface SwatchLayer {
|
||||||
|
hatch: HatchStyle | undefined;
|
||||||
|
thickness: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Live-SVG-Querschnitt EINES Wand-/Deckentyps: die Schichten als aneinander-
|
||||||
|
* liegende Bänder, proportional zur Dicke, jedes Band mit der Schnittschraffur
|
||||||
|
* seines Bauteils. `orientation="wall"` legt die Bänder NEBENEINANDER (Blick
|
||||||
|
* entlang der Wand — Schichten quer über die Dicke), `"ceiling"` STAPELT sie
|
||||||
|
* (liegender Deckenaufbau, oben → innen). Dünne Trennlinien markieren die Fugen;
|
||||||
|
* ein gerundeter Rahmen fasst das Ganze wie die übrigen Swatches ein.
|
||||||
|
*/
|
||||||
|
export function WallTypeSwatch({
|
||||||
|
layers,
|
||||||
|
width = 128,
|
||||||
|
height = 64,
|
||||||
|
orientation = "wall",
|
||||||
|
}: {
|
||||||
|
layers: SwatchLayer[];
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
orientation?: "wall" | "ceiling";
|
||||||
|
}) {
|
||||||
|
const rawId = useId().replace(/[^a-zA-Z0-9_-]/g, "");
|
||||||
|
const clipId = `wtc_${rawId}`;
|
||||||
|
const w = width;
|
||||||
|
const h = height;
|
||||||
|
|
||||||
|
// Gewichte: 0-dicke Schichten dünn, aber sichtbar. Proportional zur Summe.
|
||||||
|
const weights = layers.map((l) => (l.thickness > 0 ? l.thickness : 0.01));
|
||||||
|
const total = weights.reduce((a, b) => a + b, 0) || 1;
|
||||||
|
const along = orientation === "wall" ? w : h;
|
||||||
|
|
||||||
|
// Bänder entlang der Stapelachse aufsummieren (px-Offsets).
|
||||||
|
let acc = 0;
|
||||||
|
const bands = layers.map((l, i) => {
|
||||||
|
const len = (weights[i] / total) * along;
|
||||||
|
const start = acc;
|
||||||
|
acc += len;
|
||||||
|
const bx = orientation === "wall" ? start : 0;
|
||||||
|
const by = orientation === "wall" ? 0 : start;
|
||||||
|
const bw = orientation === "wall" ? len : w;
|
||||||
|
const bh = orientation === "wall" ? h : len;
|
||||||
|
return { hatch: l.hatch, bx, by, bw, bh, start, len };
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className="res-swatch res-wallswatch"
|
||||||
|
width={w}
|
||||||
|
height={h}
|
||||||
|
viewBox={`0 0 ${w} ${h}`}
|
||||||
|
style={{ display: "block" }}
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<clipPath id={clipId}>
|
||||||
|
<rect x={0} y={0} width={w} height={h} rx={3} />
|
||||||
|
</clipPath>
|
||||||
|
{bands.map((b, i) => (
|
||||||
|
<clipPath key={i} id={`${clipId}_b${i}`}>
|
||||||
|
<rect x={0} y={0} width={b.bw} height={b.bh} />
|
||||||
|
</clipPath>
|
||||||
|
))}
|
||||||
|
</defs>
|
||||||
|
<rect
|
||||||
|
x={0.5}
|
||||||
|
y={0.5}
|
||||||
|
width={w - 1}
|
||||||
|
height={h - 1}
|
||||||
|
rx={3}
|
||||||
|
fill="var(--input)"
|
||||||
|
stroke="var(--border)"
|
||||||
|
strokeWidth={1}
|
||||||
|
/>
|
||||||
|
<g clipPath={`url(#${clipId})`}>
|
||||||
|
{bands.map((b, i) => (
|
||||||
|
<g
|
||||||
|
key={i}
|
||||||
|
transform={`translate(${b.bx.toFixed(2)},${b.by.toFixed(2)})`}
|
||||||
|
clipPath={`url(#${clipId}_b${i})`}
|
||||||
|
>
|
||||||
|
{b.hatch && hatchInner(b.hatch, b.bw, b.bh, `${rawId}_${i}`)}
|
||||||
|
</g>
|
||||||
|
))}
|
||||||
|
{/* Fugen: dünne Trennlinie an jeder INNEREN Bandgrenze. */}
|
||||||
|
{bands.slice(1).map((b, i) =>
|
||||||
|
orientation === "wall" ? (
|
||||||
|
<line
|
||||||
|
key={i}
|
||||||
|
x1={b.start}
|
||||||
|
y1={0}
|
||||||
|
x2={b.start}
|
||||||
|
y2={h}
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={0.6}
|
||||||
|
opacity={0.5}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<line
|
||||||
|
key={i}
|
||||||
|
x1={0}
|
||||||
|
y1={b.start}
|
||||||
|
x2={w}
|
||||||
|
y2={b.start}
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={0.6}
|
||||||
|
opacity={0.5}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</g>
|
||||||
|
<rect
|
||||||
|
x={0.5}
|
||||||
|
y={0.5}
|
||||||
|
width={w - 1}
|
||||||
|
height={h - 1}
|
||||||
|
rx={3}
|
||||||
|
fill="none"
|
||||||
|
stroke="var(--border)"
|
||||||
|
strokeWidth={1}
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user