Import: DXF, add dedicated import dialog

This commit is contained in:
Furgo
2025-06-29 08:53:38 +02:00
parent c238be2857
commit 293ebf801c
8 changed files with 462 additions and 76 deletions

View File

@@ -20,6 +20,7 @@ SET(Draft_SRCS_base
SET(Draft_import
importAirfoilDAT.py
importDXF.py
DxfImportDialog.py
importDWG.py
importOCA.py
importSVG.py

View File

@@ -0,0 +1,116 @@
import FreeCAD
import FreeCADGui
from PySide import QtCore, QtGui
class DxfImportDialog:
"""
A controller class that creates, manages, and shows the DXF import dialog.
"""
def __init__(self, entity_counts, parent=None):
# Step 1: Load the UI from the resource file. This returns a new QDialog instance.
self.dialog = FreeCADGui.PySideUic.loadUi(":/ui/preferences-dxf-import.ui")
# Now, all widgets like "label_Summary" are attributes of self.dialog
self.entity_counts = entity_counts
self.total_entities = sum(entity_counts.values())
self.setup_ui()
self.connect_signals()
self.load_settings_and_set_initial_state()
def setup_ui(self):
"""Perform initial UI setup."""
self.dialog.label_Summary.setText(f"File contains approximately {self.total_entities} geometric entities.")
self.dialog.label_Warning.hide()
def connect_signals(self):
"""Connect signals from the dialog's widgets to our methods."""
buttonBox = self.dialog.findChild(QtGui.QDialogButtonBox, "buttonBox")
if buttonBox:
# Connect to our custom slots INSTEAD of the dialog's built-in ones
buttonBox.accepted.connect(self.on_accept)
buttonBox.rejected.connect(self.on_reject)
FreeCAD.Console.PrintLog("DxfImportDialog: OK and Cancel buttons connected.\n")
else:
FreeCAD.Console.PrintWarning("DxfImportDialog: Could not find buttonBox!\n")
self.dialog.radio_ImportAs_Draft.toggled.connect(self.update_warning_label)
self.dialog.radio_ImportAs_Primitives.toggled.connect(self.update_warning_label)
self.dialog.radio_ImportAs_Shapes.toggled.connect(self.update_warning_label)
self.dialog.radio_ImportAs_Fused.toggled.connect(self.update_warning_label)
def on_accept(self):
"""Custom slot to debug the OK button click."""
FreeCAD.Console.PrintLog("DxfImportDialog: 'OK' button clicked. Calling self.dialog.accept().\n")
# Manually call the original slot
self.dialog.accept()
FreeCAD.Console.PrintLog("DxfImportDialog: self.dialog.accept() has been called.\n")
def on_reject(self):
"""Custom slot to debug the Cancel button click."""
FreeCAD.Console.PrintLog("DxfImportDialog: 'Cancel' button clicked. Calling self.dialog.reject().\n")
# Manually call the original slot
self.dialog.reject()
FreeCAD.Console.PrintLog("DxfImportDialog: self.dialog.reject() has been called.\n")
def load_settings_and_set_initial_state(self):
"""Load saved preferences and set the initial state of the dialog."""
hGrp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft")
mode = hGrp.GetInt("DxfImportMode", 2)
if mode == 0:
self.dialog.radio_ImportAs_Draft.setChecked(True)
elif mode == 1:
self.dialog.radio_ImportAs_Primitives.setChecked(True)
elif mode == 3:
self.dialog.radio_ImportAs_Fused.setChecked(True)
else:
self.dialog.radio_ImportAs_Shapes.setChecked(True)
is_legacy = hGrp.GetBool("dxfUseLegacyImporter", False)
if is_legacy:
self.dialog.radio_ImportAs_Primitives.setEnabled(False)
self.dialog.radio_ImportAs_Draft.setEnabled(True)
self.dialog.radio_ImportAs_Shapes.setEnabled(True)
self.dialog.radio_ImportAs_Fused.setEnabled(True)
else:
self.dialog.radio_ImportAs_Draft.setEnabled(False)
self.dialog.radio_ImportAs_Primitives.setEnabled(False)
self.dialog.radio_ImportAs_Shapes.setEnabled(True)
self.dialog.radio_ImportAs_Fused.setEnabled(True)
self.update_warning_label()
def update_warning_label(self):
"""Updates the warning label based on selection and entity count."""
self.dialog.label_Warning.hide()
current_mode = self.get_selected_mode()
if self.total_entities > 5000 and (current_mode == 0 or current_mode == 1):
self.dialog.label_Warning.setText("Warning: Importing over 5000 entities as editable objects can be very slow.")
self.dialog.label_Warning.show()
elif self.total_entities > 20000 and current_mode == 2:
self.dialog.label_Warning.setText("Warning: Importing over 20,000 entities as individual shapes may be slow.")
self.dialog.label_Warning.show()
def exec_(self):
FreeCAD.Console.PrintLog("DxfImportDialog: Calling self.dialog.exec_()...\n")
result = self.dialog.exec_()
FreeCAD.Console.PrintLog("DxfImportDialog: self.dialog.exec_() returned with result: {}\n".format(result))
# QDialog.Accepted is usually 1, Rejected is 0.
FreeCAD.Console.PrintLog("(Note: QDialog.Accepted = {}, QDialog.Rejected = {})\n".format(QtGui.QDialog.Accepted, QtGui.QDialog.Rejected))
return result
def get_selected_mode(self):
"""Return the integer value of the selected import mode."""
if self.dialog.radio_ImportAs_Draft.isChecked(): return 0
if self.dialog.radio_ImportAs_Primitives.isChecked(): return 1
if self.dialog.radio_ImportAs_Fused.isChecked(): return 3
if self.dialog.radio_ImportAs_Shapes.isChecked(): return 2
return 2
def get_show_dialog_again(self):
"""Return True if the dialog should be shown next time."""
return not self.dialog.checkBox_ShowDialogAgain.isChecked()

View File

@@ -185,6 +185,7 @@
<file>ui/preferences-draftvisual.ui</file>
<file>ui/preferences-dwg.ui</file>
<file>ui/preferences-dxf.ui</file>
<file>ui/preferences-dxf-import.ui</file>
<file>ui/preferences-oca.ui</file>
<file>ui/preferences-svg.ui</file>
<file>ui/TaskPanel_CircularArray.ui</file>

View File

@@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DxfImportDialog</class>
<widget class="QDialog" name="DxfImportDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>480</width>
<height>280</height>
</rect>
</property>
<property name="windowTitle">
<string>DXF Import</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="groupBox_ImportAs">
<property name="title">
<string>Import as</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_ImportAs">
<item>
<widget class="QRadioButton" name="radio_ImportAs_Draft">
<property name="toolTip">
<string>Creates fully parametric Draft objects. Block definitions are imported as
reusable objects (Part Compounds) and instances become `App::Link` objects,
maintaining the block structure. Best for full integration with the Draft
Workbench. (Legacy importer only)</string>
</property>
<property name="text">
<string>Editable draft objects</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_ImportAs_Primitives">
<property name="toolTip">
<string>Creates parametric Part objects (e.g., Part::Line, Part::Circle). Block
definitions are imported as reusable objects (Part Compounds) and instances
become `App::Link` objects, maintaining the block structure. Best for
script-based post-processing. (Not yet implemented)</string>
</property>
<property name="text">
<string>Editable part primitives</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_ImportAs_Shapes">
<property name="toolTip">
<string>Creates a non-parametric shape for each DXF entity. Block definitions are
imported as reusable objects (Part Compounds) and instances become `App::Link`
objects, maintaining the block structure. Good for referencing and measuring.</string>
</property>
<property name="text">
<string>Individual part shapes (recommended)</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radio_ImportAs_Fused">
<property name="toolTip">
<string>Merges all geometry per layer into a single, non-editable shape. Block
structures are not preserved; their geometry becomes part of the layer's
shape. Best for viewing very large files with maximum performance.</string>
</property>
<property name="text">
<string>Fused part shapes (fastest)</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_Summary">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_Summary">
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="label_Summary">
<property name="text">
<string>File summary</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_Warning">
<property name="styleSheet">
<string notr="true">color: #c00;</string>
</property>
<property name="text">
<string>Warning</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="checkBox_ShowDialogAgain">
<property name="text">
<string>Do not show this dialog again</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -69,6 +69,7 @@ from draftobjects.dimension import _Dimension
from draftutils import params
from draftutils import utils
from draftutils.utils import pyopen
from PySide import QtCore, QtGui
gui = FreeCAD.GuiUp
draftui = None
@@ -2810,10 +2811,51 @@ def open(filename):
-----
Use local variables, not global variables.
"""
readPreferences()
hGrp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft")
use_legacy = hGrp.GetBool("dxfUseLegacyImporter", False)
# The C++ layer (`Gui::Application::importFrom`) has set the WaitCursor.
# We must temporarily suspend it to show our interactive dialog.
try:
if gui:
FreeCADGui.suspendWaitCursor()
# --- Dialog Workflow ---
if gui and not use_legacy and hGrp.GetBool("dxfShowDialog", True):
try:
import ImportGui
# This C++ function will need to be created in a later step
entity_counts = ImportGui.preScanDxf(filename)
except Exception:
entity_counts = {}
from DxfImportDialog import DxfImportDialog
dlg = DxfImportDialog(entity_counts)
if dlg.exec_():
# User clicked OK, save settings from the dialog
hGrp.SetInt("DxfImportMode", dlg.get_selected_mode())
hGrp.SetBool("dxfShowDialog", dlg.get_show_dialog_again())
else:
# User clicked Cancel, abort the entire operation
FCC.PrintLog("DXF import cancelled by user.\n")
return
else:
# If we don't show the dialog, we still need to read preferences
# to ensure the correct backend logic is triggered.
readPreferences()
finally:
# --- CRITICAL: Always resume the wait state before returning to C++ ---
# This restores the wait cursor and event filter so the subsequent
# blocking C++ call behaves as expected within the C++ scope.
if gui:
FreeCADGui.resumeWaitCursor()
# --- Proceed with the blocking import logic ---
total_start_time = time.perf_counter()
if dxfUseLegacyImporter:
if use_legacy:
getDXFlibs()
if dxfReader:
docname = os.path.splitext(os.path.basename(filename))[0]
@@ -2823,12 +2865,14 @@ def open(filename):
return doc
else:
errorDXFLib(gui)
else:
return None
else: # Modern C++ Importer
docname = os.path.splitext(os.path.basename(filename))[0]
doc = FreeCAD.newDocument(docname)
doc.Label = docname
FreeCAD.setActiveDocument(doc.Name)
stats = None
if gui:
import ImportGui
stats = ImportGui.readDXF(filename)
@@ -2836,13 +2880,16 @@ def open(filename):
import Import
stats = Import.readDXF(filename)
Draft.convert_draft_texts()
doc.recompute()
total_end_time = time.perf_counter()
if stats:
# Report PROCESSING time only, not user dialog time.
reporter = DxfImportReporter(filename, stats, total_end_time - total_start_time)
reporter.report_to_console()
Draft.convert_draft_texts() # convert annotations to Draft texts
doc.recompute()
return doc
def insert(filename, docname):
@@ -2864,20 +2911,53 @@ def insert(filename, docname):
-----
Use local variables, not global variables.
"""
readPreferences()
hGrp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft")
use_legacy = hGrp.GetBool("dxfUseLegacyImporter", False)
try:
if gui:
FreeCADGui.suspendWaitCursor()
# --- Dialog Workflow ---
if gui and not use_legacy and hGrp.GetBool("dxfShowDialog", True):
try:
import ImportGui
entity_counts = ImportGui.preScanDxf(filename)
except Exception:
entity_counts = {}
from DxfImportDialog import DxfImportDialog
dlg = DxfImportDialog(entity_counts)
if dlg.exec_():
hGrp.SetInt("DxfImportMode", dlg.get_selected_mode())
hGrp.SetBool("dxfShowDialog", dlg.get_show_dialog_again())
else:
FCC.PrintLog("DXF insert cancelled by user.\n")
return
else:
readPreferences()
finally:
if gui:
FreeCADGui.resumeWaitCursor()
# --- Proceed with the blocking insert logic ---
total_start_time = time.perf_counter()
try:
doc = FreeCAD.getDocument(docname)
except NameError:
doc = FreeCAD.newDocument(docname)
FreeCAD.setActiveDocument(docname)
if dxfUseLegacyImporter:
if use_legacy:
getDXFlibs()
if dxfReader:
processdxf(doc, filename)
else:
errorDXFLib(gui)
else:
else: # Modern C++ Importer
stats = None
if gui:
import ImportGui
@@ -2886,14 +2966,14 @@ def insert(filename, docname):
import Import
stats = Import.readDXF(filename)
Draft.convert_draft_texts()
doc.recompute()
total_end_time = time.perf_counter()
if stats:
reporter = DxfImportReporter(filename, stats, total_end_time - total_start_time)
reporter.report_to_console()
Draft.convert_draft_texts() # convert annotations to Draft texts
doc.recompute()
def getShapes(filename):
"""Read a DXF file, and return a list of shapes from its contents.
@@ -4183,81 +4263,64 @@ def readPreferences():
-----
Use local variables, not global variables.
"""
# reading parameters
if gui and params.get_param("dxfShowDialog"):
FreeCADGui.showPreferencesByName("Import-Export", ":/ui/preferences-dxf.ui")
global dxfCreatePart, dxfCreateDraft, dxfCreateSketch
global dxfDiscretizeCurves, dxfStarBlocks
global dxfMakeBlocks, dxfJoin, dxfRenderPolylineWidth
global dxfImportTexts, dxfImportLayouts
global dxfImportPoints, dxfImportHatches, dxfUseStandardSize
global dxfGetColors, dxfUseDraftVisGroups
global dxfMakeFaceMode, dxfBrightBackground, dxfDefaultColor
global dxfUseLegacyImporter, dxfExportBlocks, dxfScaling
global dxfUseLegacyExporter
global dxfDiscretizeCurves, dxfStarBlocks, dxfMakeBlocks, dxfJoin, dxfRenderPolylineWidth
global dxfImportTexts, dxfImportLayouts, dxfImportPoints, dxfImportHatches, dxfUseStandardSize
global dxfGetColors, dxfUseDraftVisGroups, dxfMakeFaceMode, dxfBrightBackground, dxfDefaultColor
global dxfUseLegacyImporter, dxfExportBlocks, dxfScaling, dxfUseLegacyExporter
# --- Read all feature and appearance toggles ---
# These are independent settings and can be read directly.
dxfDiscretizeCurves = params.get_param("DiscretizeEllipses", False)
dxfStarBlocks = params.get_param("dxfstarblocks", False)
dxfJoin = params.get_param("joingeometry", False)
dxfRenderPolylineWidth = params.get_param("renderPolylineWidth", False)
dxfImportTexts = params.get_param("dxftext", True)
dxfImportLayouts = params.get_param("dxflayout", False)
dxfImportPoints = params.get_param("dxfImportPoints", True)
dxfImportHatches = params.get_param("importDxfHatches", True)
dxfUseStandardSize = params.get_param("dxfStdSize", False)
dxfGetColors = params.get_param("dxfGetOriginalColors", True)
dxfUseDraftVisGroups = params.get_param("dxfUseDraftVisGroups", True)
dxfMakeFaceMode = params.get_param("MakeFaceMode", False)
dxfExportBlocks = params.get_param("dxfExportBlocks", True)
dxfScaling = params.get_param("dxfScaling", 1.0)
# Use the direct C++ API via Python for all parameter access
hGrp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft")
# These control which importer is used. The script should only proceed if
# the legacy importer is selected.
dxfUseLegacyImporter = params.get_param("dxfUseLegacyImporter", False)
dxfUseLegacyExporter = params.get_param("dxfUseLegacyExporter", False)
dxfUseLegacyImporter = hGrp.GetBool("dxfUseLegacyImporter", False)
if not dxfUseLegacyImporter:
# If the legacy importer is called when not selected, exit.
# This prevents accidental execution.
return
# This logic is now only needed for the legacy importer.
# The modern importer reads its settings directly in C++.
if dxfUseLegacyImporter:
# Legacy override for sketch creation takes highest priority
dxfCreateSketch = hGrp.GetBool("dxfCreateSketch", False)
# --- New, Centralized Logic for Structural Mode ---
# Read the legacy-specific override for sketch creation.
dxfCreateSketch = params.get_param("dxfCreateSketch", False)
if dxfCreateSketch:
# Sketch mode takes highest priority, other modes are irrelevant.
dxfCreatePart = False
dxfCreateDraft = False
dxfMakeBlocks = False
else:
# Not in sketch mode, so determine structure from DxfImportMode.
# This is where the new parameter is read, with its default value defined.
# 0=Draft, 1=Primitives, 2=Shapes, 3=Fused
import_mode = params.get_param("DxfImportMode", 2) # Default to "Individual part shapes"
if import_mode == 3: # Fused part shapes
dxfMakeBlocks = True # 'groupLayers' is the legacy equivalent
dxfCreatePart = False # In legacy, dxfMakeBlocks overrides these
dxfCreateDraft = False
elif import_mode == 0: # Editable draft objects
dxfMakeBlocks = False
if dxfCreateSketch:
dxfCreatePart = False
dxfCreateDraft = True
else: # Covers modes 1 (Primitives) and 2 (Shapes). Legacy maps both to "Simple part shapes"
dxfMakeBlocks = False
dxfCreatePart = True
dxfCreateDraft = False
dxfMakeBlocks = False
else:
# Read the new unified mode parameter and translate it to the old flags
# 0=Draft, 1=Primitives, 2=Shapes, 3=Fused
import_mode = hGrp.GetInt("DxfImportMode", 2) # Default to "Individual shapes"
if import_mode == 3: # Fused part shapes
dxfMakeBlocks = True
dxfCreatePart = False
dxfCreateDraft = False
elif import_mode == 0: # Editable draft objects
dxfMakeBlocks = False
dxfCreatePart = False
dxfCreateDraft = True
else: # Individual part shapes or Primitives
dxfMakeBlocks = False
dxfCreatePart = True
dxfCreateDraft = False
# The legacy importer still uses these global variables, so we read them all.
dxfDiscretizeCurves = hGrp.GetBool("DiscretizeEllipses", True)
dxfStarBlocks = hGrp.GetBool("dxfstarblocks", False)
dxfJoin = hGrp.GetBool("joingeometry", False)
dxfRenderPolylineWidth = hGrp.GetBool("renderPolylineWidth", False)
dxfImportTexts = hGrp.GetBool("dxftext", False)
dxfImportLayouts = hGrp.GetBool("dxflayout", False)
dxfImportPoints = hGrp.GetBool("dxfImportPoints", True)
dxfImportHatches = hGrp.GetBool("importDxfHatches", False)
dxfUseStandardSize = hGrp.GetBool("dxfStdSize", False)
dxfGetColors = hGrp.GetBool("dxfGetOriginalColors", True)
dxfUseDraftVisGroups = hGrp.GetBool("dxfUseDraftVisGroups", True)
dxfMakeFaceMode = hGrp.GetBool("MakeFaceMode", False)
dxfUseLegacyExporter = hGrp.GetBool("dxfUseLegacyExporter", False)
dxfExportBlocks = hGrp.GetBool("dxfExportBlocks", True)
dxfScaling = hGrp.GetFloat("dxfScaling", 1.0)
# --- Other settings that are not checkboxes ---
dxfBrightBackground = isBrightBackground()
dxfDefaultColor = getColor()
class DxfImportReporter:
"""Formats and reports statistics from a DXF import process."""
def __init__(self, filename, stats_dict, total_time=0.0):

View File

@@ -53,6 +53,7 @@
#include <gp_Vec.hxx>
#endif
#include <fstream>
#include <App/Annotation.h>
#include <App/Application.h>
#include <App/Document.h>
@@ -80,6 +81,35 @@ using namespace Import;
using BRepAdaptor_HCurve = BRepAdaptor_Curve;
#endif
std::map<std::string, int> ImpExpDxfRead::PreScan(const std::string& filepath)
{
std::map<std::string, int> counts;
std::ifstream ifs(filepath);
if (!ifs) {
// Could throw an exception or log an error
return counts;
}
std::string line;
bool next_is_entity_name = false;
while (std::getline(ifs, line)) {
// Simple trim for Windows-style carriage returns
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
if (next_is_entity_name) {
// The line after a " 0" group code is the entity type
counts[line]++;
next_is_entity_name = false;
}
else if (line == " 0") {
next_is_entity_name = true;
}
}
return counts;
}
//******************************************************************************
// reading

View File

@@ -61,7 +61,7 @@ public:
{
Py_XDECREF(DraftModule);
}
static std::map<std::string, int> PreScan(const std::string& filepath);
void StartImport() override;
Py::Object getStatsAsPyObject();

View File

@@ -94,6 +94,7 @@ public:
add_keyword_method("insert",
&Module::insert,
"insert(string,string) -- Insert the file into the given document.");
add_varargs_method("preScanDxf", &Module::preScanDxf, "preScanDxf(filepath) -> dict");
add_varargs_method("readDXF",
&Module::readDXF,
"readDXF(filename,[document,ignore_errors,option_source]): Imports a "
@@ -112,6 +113,26 @@ public:
}
private:
Py::Object preScanDxf(const Py::Tuple& args)
{
char* filepath_char = nullptr;
if (!PyArg_ParseTuple(args.ptr(), "et", "utf-8", &filepath_char)) {
throw Py::Exception();
}
std::string filepath(filepath_char);
PyMem_Free(filepath_char);
#include <Mod/Import/App/dxf/ImpExpDxf.h>
std::map<std::string, int> counts = Import::ImpExpDxfRead::PreScan(filepath);
Py::Dict result;
for (const auto& pair : counts) {
result.setItem(Py::String(pair.first), Py::Long(pair.second));
}
return result;
}
Py::Object importOptions(const Py::Tuple& args)
{
char* Name {};