diff --git a/src/App.tsx b/src/App.tsx index b67d3e9..b769544 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -226,6 +226,43 @@ function promptCount(): number | null { return Number.isFinite(n) && n >= 1 ? n : null; } +/** Umriss (bbox) aller Drawing2D-Geometrien in Modell-Metern (null = leer). */ +function drawingsBBox( + drawings: Drawing2D[], +): { minX: number; minY: number; maxX: number; maxY: number } | null { + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + const acc = (x: number, y: number) => { + minX = Math.min(minX, x); minY = Math.min(minY, y); + maxX = Math.max(maxX, x); maxY = Math.max(maxY, y); + }; + for (const d of drawings) { + const g = d.geom; + if (g.shape === "line") { acc(g.a.x, g.a.y); acc(g.b.x, g.b.y); } + else if (g.shape === "polyline") for (const p of g.pts) acc(p.x, p.y); + else if (g.shape === "rect") { acc(g.min.x, g.min.y); acc(g.max.x, g.max.y); } + else if (g.shape === "circle" || g.shape === "arc") { + acc(g.center.x - g.r, g.center.y - g.r); + acc(g.center.x + g.r, g.center.y + g.r); + } else if (g.shape === "text") acc(g.at.x, g.at.y); + } + return Number.isFinite(minX) ? { minX, minY, maxX, maxY } : null; +} + +/** Ein Drawing2D um (dx,dy) in Modell-Metern verschieben (alle Formen). */ +function translateDrawing(d: Drawing2D, dx: number, dy: number): Drawing2D { + const mv = (p: Vec2): Vec2 => ({ x: p.x + dx, y: p.y + dy }); + const g = d.geom; + switch (g.shape) { + case "line": return { ...d, geom: { ...g, a: mv(g.a), b: mv(g.b) } }; + case "polyline": return { ...d, geom: { ...g, pts: g.pts.map(mv) } }; + case "rect": return { ...d, geom: { ...g, min: mv(g.min), max: mv(g.max) } }; + case "circle": return { ...d, geom: { ...g, center: mv(g.center) } }; + case "arc": return { ...d, geom: { ...g, center: mv(g.center) } }; + case "text": return { ...d, geom: { ...g, at: mv(g.at) } }; + default: return d; + } +} + export default function App() { // Sprachwechsel abonnieren: re-rendert den gesamten Baum, wenn die Sprache // umgeschaltet wird (alle Kinder lesen denselben modulweiten Zustand über t). @@ -2136,12 +2173,24 @@ export default function App() { decision.target.kind === "new" ? addDrawingLevel(decision.target.name) : decision.target.levelId; - const drawings = buildDrawings( + let drawings = buildDrawings( decision, levelId, project, activeCategoryCode, ); + // Platzierung „viewCenter": den Gesamt-Umriss so verschieben, dass seine + // Mitte auf die Mitte des aktuellen Ausschnitts fällt (sonst bleibt alles an + // den DXF-Koordinaten relativ zum Nullpunkt). + if (decision.placement === "viewCenter" && drawings.length) { + const center = planViewRef.current?.viewCenterModel(); + const bb = drawingsBBox(drawings); + if (center && bb) { + const dx = center.x - (bb.minX + bb.maxX) / 2; + const dy = center.y - (bb.minY + bb.maxY) / 2; + drawings = drawings.map((d) => translateDrawing(d, dx, dy)); + } + } if (drawings.length) importDrawings(drawings); if (decision.importMeshes && decision.parsed.meshes.length) { addContextObjects(decision.parsed.meshes); diff --git a/src/i18n/de.ts b/src/i18n/de.ts index b096488..7324497 100644 --- a/src/i18n/de.ts +++ b/src/i18n/de.ts @@ -791,6 +791,9 @@ export const de = { "import.layers.heading": "Ebenen-Handling (DXF-Layer)", "import.layers.asCategories": "DXF-Layer als Kategorien übernehmen", "import.layers.toActive": "Alles auf die aktive Kategorie ({code})", + "import.placement.heading": "Platzierung", + "import.placement.origin": "An DXF-Koordinaten (relativ zum Nullpunkt)", + "import.placement.viewCenter": "In die Mitte der aktuellen Ansicht", "import.mesh.heading": "Meshes", "import.mesh.asContext": "Meshes als Kontext-Geometrie (3D) importieren", "import.mesh.note": "Meshes sind keine 2D-Zeichnung — sie landen in der Kontext-Schicht.", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 14573e3..d1940dc 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -780,6 +780,9 @@ export const en: Record = { "import.layers.heading": "Layer handling (DXF layers)", "import.layers.asCategories": "Keep DXF layers as categories", "import.layers.toActive": "Everything onto the active category ({code})", + "import.placement.heading": "Placement", + "import.placement.origin": "At DXF coordinates (relative to origin)", + "import.placement.viewCenter": "Centered in the current view", "import.mesh.heading": "Meshes", "import.mesh.asContext": "Import meshes as context geometry (3D)", "import.mesh.note": "Meshes are not 2D drawings — they go into the context layer.", diff --git a/src/plan/PlanView.tsx b/src/plan/PlanView.tsx index 85a834b..0cf702d 100644 --- a/src/plan/PlanView.tsx +++ b/src/plan/PlanView.tsx @@ -131,6 +131,8 @@ export interface PlanViewHandle { applyScale: (n: number) => void; /** Aktuelle Pixel/Meter-Skala (für Snap-Toleranz der Command-Engine). */ pxPerMeter: () => number; + /** Mitte des aktuellen Ausschnitts in Modell-Metern (z. B. für Import-Platzierung). */ + viewCenterModel: () => Vec2; } /** Ergebnis eines Links-Klicks (Auswahl) an einer Modellposition. */ @@ -631,6 +633,11 @@ export const PlanView = forwardRef( if (n > 0) setPaperScale(n); }, pxPerMeter: () => currentPxPerMeter(), + viewCenterModel: () => { + // Mitte der frischen viewBox (aus Ref) → Modell (inverse toScreen). + const v = viewRef.current; + return { x: (v.x + v.w / 2) / PX_PER_M, y: -(v.y + v.h / 2) / PX_PER_M }; + }, }), // eslint-disable-next-line react-hooks/exhaustive-deps [], diff --git a/src/ui/ImportDialog.tsx b/src/ui/ImportDialog.tsx index 8e9a638..aad1f19 100644 --- a/src/ui/ImportDialog.tsx +++ b/src/ui/ImportDialog.tsx @@ -36,6 +36,12 @@ export interface ImportDecision { categoryMode: CategoryMode; /** Ob die Meshes als Kontext-Geometrie übernommen werden. */ importMeshes: boolean; + /** + * Platzierung der importierten Geometrie: `origin` = 1:1 an den DXF-Koordinaten + * (relativ zum Nullpunkt); `viewCenter` = der Gesamt-Umriss wird in die Mitte + * des aktuellen Ausschnitts geschoben. + */ + placement: "origin" | "viewCenter"; /** Die geparsten Konturen-Sätze (für die spätere 2D-Konvertierung). */ parsed: DxfImportResult; } @@ -127,6 +133,7 @@ export function ImportDialog({ layers.length > 0 ? "byLayer" : "active", ); const [importMeshes, setImportMeshes] = useState(true); + const [placement, setPlacement] = useState<"origin" | "viewCenter">("origin"); const canImport = !isDwg && !loading && (hasDrawings || (hasMeshes && importMeshes)); @@ -137,7 +144,7 @@ export function ImportDialog({ targetKind === "existing" ? ({ kind: "existing", levelId: existingLevelId } as const) : ({ kind: "new", name: newName } as const); - onImport({ target, categoryMode, importMeshes, parsed }); + onImport({ target, categoryMode, importMeshes, placement, parsed }); }; return ( @@ -295,6 +302,34 @@ export function ImportDialog({ )} + {/* Platzierung: an den DXF-Koordinaten (relativ zu 0) ODER in die + Mitte des aktuellen Ausschnitts geschoben. */} + {hasDrawings && ( +
+
+ {t("import.placement.heading")} +
+ + +
+ )} + {/* Mesh-Behandlung. */} {hasMeshes && (