DXF-Import: Platzierungsoption (relativ zu 0 ODER in Ansichtsmitte)
This commit is contained in:
+50
-1
@@ -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);
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user