diff --git a/src/App.tsx b/src/App.tsx index aca8fae..40e2aed 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1856,6 +1856,13 @@ export default function App() { } if (e.key !== "Delete" && e.key !== "Backspace") return; if (activeTransform) return; // während einer Transformation nicht löschen + // Schnittlinie: Entf löscht NUR ihre Linie (linePoints), die Ebene bleibt. + if (selectedSectionLineId) { + patchLevel(selectedSectionLineId, { linePoints: undefined }); + setSelectedSectionLineId(null); + e.preventDefault(); + return; + } if ( selectedDrawingIds.length || selectedWallIds.length || @@ -1918,6 +1925,7 @@ export default function App() { selectedExtrudedSolidIds, selectedColumnIds, selectedRoofIds, + selectedSectionLineId, project.walls, project.drawings2d, project.ceilings, @@ -2711,6 +2719,21 @@ export default function App() { ], ); + // Gewählte Schnitt-/Ansichtslinie (DrawingLevel) — separat von `selection`, da + // eine Schnittlinie KEIN Projekt-Bauteil ist. Speist die Schnittlinien-Sektion + // des Object-Info-Panels. + const sectionLine = useMemo( + () => + selectedSectionLineId + ? project.drawingLevels.find( + (z) => + z.id === selectedSectionLineId && + (z.kind === "section" || z.kind === "elevation"), + ) ?? null + : null, + [project.drawingLevels, selectedSectionLineId], + ); + // ── DXF/DWG-Import (Dialog) ─────────────────────────────────────────────── // Eine Datei (Button ODER Drop) öffnet den modalen Import-Dialog. DXF wird als // Text geparst, DWG als ArrayBuffer über LibreDWG-WASM (lazy, asynchron); beide @@ -3323,6 +3346,14 @@ export default function App() { onSetRoofPatch: (patch) => { if (selection?.kind === "roof") updateRoof(selection.id, patch); }, + // ── Schnitt-/Ansichtslinie (nur bei selektierter Linie) ──────────────── + sectionLine, + onSetSectionLinePatch: (patch) => { + if (selectedSectionLineId) patchLevel(selectedSectionLineId, patch); + }, + onDeleteSectionLine: () => { + if (selectedSectionLineId) patchLevel(selectedSectionLineId, { linePoints: undefined }); + }, // ── Treppen-Attribute (nur bei selektierter Treppe) ──────────────────── onSetStairShape: (shape) => { if (selection?.kind === "stair") updateStair(selection.id, { shape }); @@ -3853,6 +3884,8 @@ export default function App() { detail, scaleDenominator, selection, + sectionLine, + selectedSectionLineId, selectedViewSnapshotId, ]); diff --git a/src/i18n/de.ts b/src/i18n/de.ts index d3f64ae..2132cbe 100644 --- a/src/i18n/de.ts +++ b/src/i18n/de.ts @@ -503,6 +503,18 @@ export const de = { "objinfo.room.refFloor": "Referenzgeschoss", "objinfo.room.editStamp": "Stempel bearbeiten …", // ── Extrusions-Attribute (truck-Integration, Object-Info-Panel) ───────── + "objinfo.sectionLine.section": "Schnittlinie", + "objinfo.sectionLine.kind.section": "Schnitt", + "objinfo.sectionLine.kind.elevation": "Ansicht", + "objinfo.sectionLine.name": "Name", + "objinfo.sectionLine.flip": "Blickrichtung umkehren", + "objinfo.sectionLine.startX": "Start X", + "objinfo.sectionLine.startY": "Start Y", + "objinfo.sectionLine.endX": "Ende X", + "objinfo.sectionLine.endY": "Ende Y", + "objinfo.sectionLine.depth": "Schnitt-Tiefe (m)", + "objinfo.sectionLine.depthHint": "leer = unbegrenzt", + "objinfo.sectionLine.delete": "Schnittlinie löschen", "objinfo.extrudedSolid.section": "Extrusion", "objinfo.extrudedSolid.height": "Höhe", "objinfo.extrudedSolid.taper": "Verjüngung", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index fe572e6..6a3ad62 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -500,6 +500,18 @@ export const en: Record = { "objinfo.room.refFloor": "Reference floor", "objinfo.room.editStamp": "Edit stamp …", // ── Extruded solid attributes (truck integration, object info panel) ──── + "objinfo.sectionLine.section": "Section line", + "objinfo.sectionLine.kind.section": "Section", + "objinfo.sectionLine.kind.elevation": "Elevation", + "objinfo.sectionLine.name": "Name", + "objinfo.sectionLine.flip": "Flip view direction", + "objinfo.sectionLine.startX": "Start X", + "objinfo.sectionLine.startY": "Start Y", + "objinfo.sectionLine.endX": "End X", + "objinfo.sectionLine.endY": "End Y", + "objinfo.sectionLine.depth": "Section depth (m)", + "objinfo.sectionLine.depthHint": "empty = unlimited", + "objinfo.sectionLine.delete": "Delete section line", "objinfo.extrudedSolid.section": "Extrusion", "objinfo.extrudedSolid.height": "Height", "objinfo.extrudedSolid.taper": "Taper", diff --git a/src/panels/AttributesPanel.tsx b/src/panels/AttributesPanel.tsx index be63416..8215c19 100644 --- a/src/panels/AttributesPanel.tsx +++ b/src/panels/AttributesPanel.tsx @@ -42,6 +42,7 @@ import { RoomSection, ExtrudedSolidSection, ColumnSection, + SectionLineSection, } from "./ObjectInfoPanel"; /** UI-Zustand des 3-Optionen-Quellen-Dropdowns (nicht 1:1 das Modell-Feld: @@ -92,10 +93,15 @@ export function AttributesPanel() { // Leerzustand: der Typ-Picker (falls ein Bauteil-Werkzeug aktiv ist), sonst ein // kompakter Hinweis. if (sel === null) { + // Schnitt-/Ansichtslinie ist kein Projekt-Bauteil (kein `selection`) — sie + // kommt über host.sectionLine und wird hier im „Leerzustand" bearbeitet. return (
{typeSection} - {!typeSection &&
{t("attr.empty")}
} + {host.sectionLine && } + {!typeSection && !host.sectionLine && ( +
{t("attr.empty")}
+ )}
); } diff --git a/src/panels/ObjectInfoPanel.tsx b/src/panels/ObjectInfoPanel.tsx index 5ffacd0..0f6dd99 100644 --- a/src/panels/ObjectInfoPanel.tsx +++ b/src/panels/ObjectInfoPanel.tsx @@ -19,10 +19,12 @@ import { t } from "../i18n"; import type { TranslationKey } from "../i18n"; import { flattenCategories, formatM } from "../model/types"; import type { + DrawingLevel, RoofShape, SiaCategory, SliceTermination, StairShape, + Vec2, VerticalAnchor, WallReferenceLine, } from "../model/types"; @@ -314,6 +316,118 @@ export function RoomSection({ ); } +// ── Schnitt-/Ansichtslinien-Abschnitt (Object-Info; nur die gewählte Linie) ── +// Name (editierbar), Blickrichtung umkehren (directionSign-Flip), Endpunkt- +// Koordinaten (editierbar) + Schnitt-Tiefe; Löschen setzt linePoints zurück +// (die Ebene bleibt bestehen). Eine Schnittlinie ist KEIN Projekt-Bauteil, +// darum kommt sie über `host.sectionLine` (nicht über `selection`). +export function SectionLineSection({ + level, + host, +}: { + level: DrawingLevel; + host: ReturnType; +}) { + const line = level.linePoints; + const kindLabel = + level.kind === "elevation" + ? t("objinfo.sectionLine.kind.elevation") + : t("objinfo.sectionLine.kind.section"); + const patchPoint = (which: 0 | 1, axis: "x" | "y", value: number) => { + if (!line) return; + const next: [Vec2, Vec2] = [{ ...line[0] }, { ...line[1] }]; + next[which] = { ...next[which], [axis]: value }; + host.onSetSectionLinePatch({ linePoints: next }); + }; + const coordField = (labelKey: TranslationKey, value: number, on: (v: number) => void) => ( +
+ {t(labelKey)} + { + const v = Number(e.target.value); + if (Number.isFinite(v)) on(v); + }} + title={t(labelKey)} + /> +
+ ); + return ( + <> +
+
+ {t("objinfo.sectionLine.section")} · {kindLabel} +
+ + {/* Name (editierbar). */} +
+ {t("objinfo.sectionLine.name")} + host.onSetSectionLinePatch({ name: e.target.value })} + title={t("objinfo.sectionLine.name")} + /> +
+ + {/* Blickrichtung umkehren (directionSign-Flip). */} + + + {/* Endpunkt-Koordinaten (nur wenn eine Linie gesetzt ist). */} + {line && ( + <> + {coordField("objinfo.sectionLine.startX", line[0].x, (v) => patchPoint(0, "x", v))} + {coordField("objinfo.sectionLine.startY", line[0].y, (v) => patchPoint(0, "y", v))} + {coordField("objinfo.sectionLine.endX", line[1].x, (v) => patchPoint(1, "x", v))} + {coordField("objinfo.sectionLine.endY", line[1].y, (v) => patchPoint(1, "y", v))} + + )} + + {/* Schnitt-Tiefe (leer = unbegrenzt). */} +
+ {t("objinfo.sectionLine.depth")} + { + const raw = e.target.value.trim(); + if (raw === "") { + host.onSetSectionLinePatch({ depth: undefined }); + return; + } + const v = Number(raw); + if (Number.isFinite(v) && v > 0) host.onSetSectionLinePatch({ depth: v }); + }} + title={t("objinfo.sectionLine.depth")} + /> +
+ + {/* Löschen: linePoints zurücksetzen (die Ebene bleibt bestehen). */} + + + ); +} + // ── Extrusions-Attribut-Abschnitt (truck-Integration) ─────────────────────── // Höhe editierbar (löst Re-Extrusion aus); Grundfläche + Geschoss read-only. export function ExtrudedSolidSection({ diff --git a/src/panels/host.ts b/src/panels/host.ts index e5b046b..6f6f539 100644 --- a/src/panels/host.ts +++ b/src/panels/host.ts @@ -330,6 +330,25 @@ export interface PanelHostValue { */ onSetRoofPatch: (patch: Partial) => void; + // ── Schnitt-/Ansichtslinie (Object-Info-Panel; nur die selektierte Linie) ── + /** + * Die aktuell gewählte Schnitt-/Ansichtsebene (DrawingLevel, kind + * "section"/"elevation"), oder `null`. Das Object-Info-Panel zeigt ihre + * Attribute (Name, Blickrichtung, Endpunkte, Tiefe), auch wenn `selection` + * null ist (eine Schnittlinie ist KEIN Projekt-Bauteil). + */ + sectionLine: DrawingLevel | null; + /** + * Immutable Änderung einer Schnitt-/Ansichtslinie (Name/directionSign/ + * linePoints/depth) — wirkt NUR auf die selektierte Linie. + */ + onSetSectionLinePatch: (patch: Partial) => void; + /** + * Löscht die Schnittlinie der gewählten Ebene (`linePoints` = undefined); die + * Ebene selbst bleibt bestehen (der Schnitt zeigt dann wieder den Hinweis). + */ + onDeleteSectionLine: () => void; + // ── Treppen-Attribute (Object-Info-Panel; nur die selektierte Treppe) ─── /** Setzt die Grundform (gerade/L/Wendel). */ onSetStairShape: (shape: StairShape) => void;