DXF-Import: Platzierungsoption (relativ zu 0 ODER in Ansichtsmitte)

This commit is contained in:
2026-07-05 15:17:09 +02:00
parent 376aa67566
commit dd76ec89fd
5 changed files with 99 additions and 2 deletions
+50 -1
View File
@@ -226,6 +226,43 @@ function promptCount(): number | null {
return Number.isFinite(n) && n >= 1 ? n : 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() { export default function App() {
// Sprachwechsel abonnieren: re-rendert den gesamten Baum, wenn die Sprache // Sprachwechsel abonnieren: re-rendert den gesamten Baum, wenn die Sprache
// umgeschaltet wird (alle Kinder lesen denselben modulweiten Zustand über t). // umgeschaltet wird (alle Kinder lesen denselben modulweiten Zustand über t).
@@ -2136,12 +2173,24 @@ export default function App() {
decision.target.kind === "new" decision.target.kind === "new"
? addDrawingLevel(decision.target.name) ? addDrawingLevel(decision.target.name)
: decision.target.levelId; : decision.target.levelId;
const drawings = buildDrawings( let drawings = buildDrawings(
decision, decision,
levelId, levelId,
project, project,
activeCategoryCode, 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 (drawings.length) importDrawings(drawings);
if (decision.importMeshes && decision.parsed.meshes.length) { if (decision.importMeshes && decision.parsed.meshes.length) {
addContextObjects(decision.parsed.meshes); addContextObjects(decision.parsed.meshes);
+3
View File
@@ -791,6 +791,9 @@ export const de = {
"import.layers.heading": "Ebenen-Handling (DXF-Layer)", "import.layers.heading": "Ebenen-Handling (DXF-Layer)",
"import.layers.asCategories": "DXF-Layer als Kategorien übernehmen", "import.layers.asCategories": "DXF-Layer als Kategorien übernehmen",
"import.layers.toActive": "Alles auf die aktive Kategorie ({code})", "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.heading": "Meshes",
"import.mesh.asContext": "Meshes als Kontext-Geometrie (3D) importieren", "import.mesh.asContext": "Meshes als Kontext-Geometrie (3D) importieren",
"import.mesh.note": "Meshes sind keine 2D-Zeichnung — sie landen in der Kontext-Schicht.", "import.mesh.note": "Meshes sind keine 2D-Zeichnung — sie landen in der Kontext-Schicht.",
+3
View File
@@ -780,6 +780,9 @@ export const en: Record<TranslationKey, string> = {
"import.layers.heading": "Layer handling (DXF layers)", "import.layers.heading": "Layer handling (DXF layers)",
"import.layers.asCategories": "Keep DXF layers as categories", "import.layers.asCategories": "Keep DXF layers as categories",
"import.layers.toActive": "Everything onto the active category ({code})", "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.heading": "Meshes",
"import.mesh.asContext": "Import meshes as context geometry (3D)", "import.mesh.asContext": "Import meshes as context geometry (3D)",
"import.mesh.note": "Meshes are not 2D drawings — they go into the context layer.", "import.mesh.note": "Meshes are not 2D drawings — they go into the context layer.",
+7
View File
@@ -131,6 +131,8 @@ export interface PlanViewHandle {
applyScale: (n: number) => void; applyScale: (n: number) => void;
/** Aktuelle Pixel/Meter-Skala (für Snap-Toleranz der Command-Engine). */ /** Aktuelle Pixel/Meter-Skala (für Snap-Toleranz der Command-Engine). */
pxPerMeter: () => number; 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. */ /** Ergebnis eines Links-Klicks (Auswahl) an einer Modellposition. */
@@ -631,6 +633,11 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
if (n > 0) setPaperScale(n); if (n > 0) setPaperScale(n);
}, },
pxPerMeter: () => currentPxPerMeter(), 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 // eslint-disable-next-line react-hooks/exhaustive-deps
[], [],
+36 -1
View File
@@ -36,6 +36,12 @@ export interface ImportDecision {
categoryMode: CategoryMode; categoryMode: CategoryMode;
/** Ob die Meshes als Kontext-Geometrie übernommen werden. */ /** Ob die Meshes als Kontext-Geometrie übernommen werden. */
importMeshes: boolean; 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). */ /** Die geparsten Konturen-Sätze (für die spätere 2D-Konvertierung). */
parsed: DxfImportResult; parsed: DxfImportResult;
} }
@@ -127,6 +133,7 @@ export function ImportDialog({
layers.length > 0 ? "byLayer" : "active", layers.length > 0 ? "byLayer" : "active",
); );
const [importMeshes, setImportMeshes] = useState<boolean>(true); const [importMeshes, setImportMeshes] = useState<boolean>(true);
const [placement, setPlacement] = useState<"origin" | "viewCenter">("origin");
const canImport = const canImport =
!isDwg && !loading && (hasDrawings || (hasMeshes && importMeshes)); !isDwg && !loading && (hasDrawings || (hasMeshes && importMeshes));
@@ -137,7 +144,7 @@ export function ImportDialog({
targetKind === "existing" targetKind === "existing"
? ({ kind: "existing", levelId: existingLevelId } as const) ? ({ kind: "existing", levelId: existingLevelId } as const)
: ({ kind: "new", name: newName } as const); : ({ kind: "new", name: newName } as const);
onImport({ target, categoryMode, importMeshes, parsed }); onImport({ target, categoryMode, importMeshes, placement, parsed });
}; };
return ( return (
@@ -295,6 +302,34 @@ export function ImportDialog({
</section> </section>
)} )}
{/* Platzierung: an den DXF-Koordinaten (relativ zu 0) ODER in die
Mitte des aktuellen Ausschnitts geschoben. */}
{hasDrawings && (
<section className="imp-section">
<div className="imp-section-title">
{t("import.placement.heading")}
</div>
<label className="imp-radio">
<input
type="radio"
name="imp-place"
checked={placement === "origin"}
onChange={() => setPlacement("origin")}
/>
<span>{t("import.placement.origin")}</span>
</label>
<label className="imp-radio">
<input
type="radio"
name="imp-place"
checked={placement === "viewCenter"}
onChange={() => setPlacement("viewCenter")}
/>
<span>{t("import.placement.viewCenter")}</span>
</label>
</section>
)}
{/* Mesh-Behandlung. */} {/* Mesh-Behandlung. */}
{hasMeshes && ( {hasMeshes && (
<section className="imp-section"> <section className="imp-section">