diff --git a/src/App.tsx b/src/App.tsx index 5d36c96..8b711dd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3076,6 +3076,59 @@ export default function App() { if (!sel2) return; commitTransformOn(sel2, "move", [{ x: 0, y: 0 }, { x: dx, y: dy }], "move", 1); }, + // Objektinfo-Kopf: Klasse (Grafik-Kategorie) + Zeichnungsebene ändern + // (VW-Muster „Klasse"/„Ebene"). Je Elementart der passende Store-Weg. + onSetSelectionCategory: (code) => { + if (!selection || !code) return; + const id = selection.id; + switch (selection.kind) { + case "wall": updateWall(id, { categoryCode: code }); break; + case "ceiling": updateCeiling(id, { categoryCode: code }); break; + case "roof": updateRoof(id, { categoryCode: code }); break; + case "opening": updateOpening(id, { categoryCode: code }); break; + case "stair": updateStair(id, { categoryCode: code }); break; + case "room": updateRoom(id, { categoryCode: code }); break; + case "drawing2d": updateDrawing2D(id, { categoryCode: code }); break; + case "column": + setProject((p) => ({ + ...p, + columns: (p.columns ?? []).map((c) => + c.id === id ? { ...c, categoryCode: code } : c, + ), + })); + break; + default: break; // extrudedSolid/door: keine Kategorie am Element + } + }, + onSetSelectionLevel: (levelId) => { + if (!selection || !levelId) return; + const id = selection.id; + switch (selection.kind) { + case "wall": updateWall(id, { floorId: levelId }); break; + case "ceiling": updateCeiling(id, { floorId: levelId }); break; + case "roof": updateRoof(id, { floorId: levelId }); break; + case "stair": updateStair(id, { floorId: levelId }); break; + case "room": updateRoom(id, { floorId: levelId }); break; + case "drawing2d": updateDrawing2D(id, { levelId }); break; + case "extrudedSolid": + setProject((p) => ({ + ...p, + extrudedSolids: (p.extrudedSolids ?? []).map((s) => + s.id === id ? { ...s, levelId } : s, + ), + })); + break; + case "column": + setProject((p) => ({ + ...p, + columns: (p.columns ?? []).map((c) => + c.id === id ? { ...c, floorId: levelId } : c, + ), + })); + break; + default: break; // opening: Ebene kommt aus der Wirts-Wand + } + }, onRotateSelectionAround: (cx, cy, deg) => { if (!selection) return; if (!isFinite(deg) || deg === 0) return; diff --git a/src/i18n/de.ts b/src/i18n/de.ts index 5974b09..c6ddf8a 100644 --- a/src/i18n/de.ts +++ b/src/i18n/de.ts @@ -266,6 +266,8 @@ export const de = { // Objekt-Info-Palette (ObjectInfoPanel) "objinfo.title": "Objekt-Info", "objinfo.empty": "Kein Objekt gewählt", + "objinfo.category": "Klasse", + "objinfo.level": "Ebene", "objinfo.refpoint": "Bezugspunkt", "objinfo.setRefpoint": "Bezugspunkt wählen", "objinfo.x": "X", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 9c7f5bb..7f6c69f 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -265,6 +265,8 @@ export const en: Record = { // Objekt-Info-Palette (ObjectInfoPanel) "objinfo.title": "Object Info", "objinfo.empty": "No object selected", + "objinfo.category": "Class", + "objinfo.level": "Layer", "objinfo.refpoint": "Reference point", "objinfo.setRefpoint": "Set reference point", "objinfo.x": "X", diff --git a/src/panels/ObjectInfoPanel.tsx b/src/panels/ObjectInfoPanel.tsx index 7d57185..f6cdf09 100644 --- a/src/panels/ObjectInfoPanel.tsx +++ b/src/panels/ObjectInfoPanel.tsx @@ -17,7 +17,7 @@ import { useState } from "react"; import { t } from "../i18n"; import type { TranslationKey } from "../i18n"; -import { formatM } from "../model/types"; +import { flattenCategories, formatM } from "../model/types"; import type { RoofShape, SiaCategory, @@ -110,13 +110,43 @@ export function ObjectInfoPanel() { return (
{m && } - {/* Kopfzeile: Typ + Kategorie. Die element-spezifischen Attribut-Abschnitte - (Wand/Decke/Öffnung/Treppe/Raum) liegen jetzt im Attribute-Panel; - ObjectInfo trägt nur noch Bezugspunkt + Masse. */} + {/* Kopfzeile: Typ + editierbare Klasse (Grafik-Kategorie) + Zeichnungsebene + (VW-Muster „Klasse"/„Ebene"). Die element-spezifischen Attribut- + Abschnitte (Wand/Decke/Öffnung/Treppe/Raum) liegen im Attribute-Panel. */}
{t(`objinfo.kind.${sel.kind}`)} - {sel.categoryCode}
+ {sel.categoryCode !== "" && ( +
+ {t("objinfo.category")} + host.onSetSelectionCategory(v)} + options={flattenCategories(host.project.layers).map((c) => ({ + value: c.code, + label: `${c.code} ${c.name}`, + }))} + title={t("objinfo.category")} + width={150} + /> +
+ )} + {sel.floorId != null && ( +
+ {t("objinfo.level")} + host.onSetSelectionLevel(v)} + options={host.project.drawingLevels + // Geschosse + die aktuelle Ebene (2D-Zeichnungen können auch auf + // Schnitt-/Zeichnungs-Ebenen liegen — die darf nicht verschwinden). + .filter((z) => z.kind === "floor" || z.id === sel.floorId) + .map((z) => ({ value: z.id, label: z.name }))} + title={t("objinfo.level")} + width={150} + /> +
+ )}
diff --git a/src/panels/host.ts b/src/panels/host.ts index 047202f..4372cd9 100644 --- a/src/panels/host.ts +++ b/src/panels/host.ts @@ -214,6 +214,18 @@ export interface PanelHostValue { * diese Elementarten nicht) ein No-op. */ onMoveSelectionBy: (dx: number, dy: number) => void; + /** + * Setzt die Grafik-Kategorie (Klasse/Ebene) des selektierten Elements — + * editierbar im Objektinfo-Kopf (VW: „Klasse"). Wirkt auf alle Elementarten + * mit `categoryCode`. + */ + onSetSelectionCategory: (code: string) => void; + /** + * Verschiebt das selektierte Element auf eine andere Zeichnungsebene + * (Geschoss) — editierbar im Objektinfo-Kopf (VW: „Ebene"). No-op bei + * Öffnungen (deren Ebene kommt aus der Wirts-Wand). + */ + onSetSelectionLevel: (levelId: string) => void; /** * Dreht die Selektion um `deg` Grad um den Weltpunkt (cx,cy) — generischer * Rotate über das Transform-System; deckt dieselben Selektionsarten ab wie diff --git a/src/state/selectionInfo.ts b/src/state/selectionInfo.ts index 98edc48..b59fde9 100644 --- a/src/state/selectionInfo.ts +++ b/src/state/selectionInfo.ts @@ -345,6 +345,12 @@ export interface Selection { id: string; /** Grafik-Kategorie (Ebene) des Elements. */ categoryCode: string; + /** + * Zeichnungsebene (Geschoss) des Elements — editierbar im Objektinfo-Kopf + * (VW: „Ebene"). Bei Öffnungen `undefined` (die Ebene kommt aus der + * Wirts-Wand, ein Verschieben auf eine andere Ebene ergäbe keinen Sinn). + */ + floorId?: string; /** Effektiv aufgelöste Strich-/Umrandungsfarbe (hex). */ color: string; /** Effektiv aufgelöste Strichstärke in Millimetern. */ @@ -550,6 +556,7 @@ function wallSelection(project: Project, wall: Wall): Selection { kind: "wall", id: wall.id, categoryCode: wall.categoryCode, + floorId: wall.floorId, color, weightMm, fillHatchId: wall.hatchId, @@ -604,6 +611,7 @@ function ceilingSelection(project: Project, ceiling: Ceiling): Selection { kind: "ceiling", id: ceiling.id, categoryCode: ceiling.categoryCode, + floorId: ceiling.floorId, color, weightMm, fillHatchId: ceiling.hatchId, @@ -653,6 +661,7 @@ function roofSelection(project: Project, roof: Roof): Selection { kind: "roof", id: roof.id, categoryCode: roof.categoryCode, + floorId: roof.floorId, color, weightMm, closed: true, @@ -751,6 +760,7 @@ function stairSelection(project: Project, s: Stair): Selection { kind: "stair", id: s.id, categoryCode: s.categoryCode, + floorId: s.floorId, color, weightMm, fillHatchId: undefined, @@ -791,6 +801,7 @@ function roomSelection(project: Project, r: Room): Selection { kind: "room", id: r.id, categoryCode: r.categoryCode, + floorId: r.floorId, color, weightMm, fillHatchId: undefined, @@ -829,6 +840,7 @@ function extrudedSolidSelection(project: Project, s: ExtrudedSolid): Selection { kind: "extrudedSolid", id: s.id, categoryCode: "", + floorId: s.levelId, color: EXTRUSION_COLOR, weightMm: WALL_FALLBACK_MM, fillHatchId: null, @@ -872,6 +884,7 @@ function columnSelection(project: Project, col: Column): Selection { kind: "column", id: col.id, categoryCode: col.categoryCode, + floorId: col.floorId, color: col.color ?? category?.color ?? COLUMN_COLOR, weightMm: WALL_FALLBACK_MM, bbox: { minX, minY, maxX, maxY }, @@ -900,6 +913,7 @@ function drawingSelection(project: Project, d: Drawing2D): Selection { kind: "drawing2d", id: d.id, categoryCode: d.categoryCode, + floorId: d.levelId, color, weightMm, fillHatchId: closed ? d.hatchId ?? null : null,