diff --git a/src/Mod/Draft/CMakeLists.txt b/src/Mod/Draft/CMakeLists.txt index 7e08b0c53e..f985c4350c 100644 --- a/src/Mod/Draft/CMakeLists.txt +++ b/src/Mod/Draft/CMakeLists.txt @@ -20,6 +20,7 @@ SET(Draft_SRCS_base SET(Draft_import importAirfoilDAT.py importDXF.py + DxfImportDialog.py importDWG.py importOCA.py importSVG.py diff --git a/src/Mod/Draft/DxfImportDialog.py b/src/Mod/Draft/DxfImportDialog.py new file mode 100644 index 0000000000..a1c7fec800 --- /dev/null +++ b/src/Mod/Draft/DxfImportDialog.py @@ -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() diff --git a/src/Mod/Draft/Resources/Draft.qrc b/src/Mod/Draft/Resources/Draft.qrc index 8cc66d2943..fa6211ca12 100644 --- a/src/Mod/Draft/Resources/Draft.qrc +++ b/src/Mod/Draft/Resources/Draft.qrc @@ -185,6 +185,7 @@ ui/preferences-draftvisual.ui ui/preferences-dwg.ui ui/preferences-dxf.ui + ui/preferences-dxf-import.ui ui/preferences-oca.ui ui/preferences-svg.ui ui/TaskPanel_CircularArray.ui diff --git a/src/Mod/Draft/Resources/ui/preferences-dxf-import.ui b/src/Mod/Draft/Resources/ui/preferences-dxf-import.ui new file mode 100644 index 0000000000..fa66b241e8 --- /dev/null +++ b/src/Mod/Draft/Resources/ui/preferences-dxf-import.ui @@ -0,0 +1,154 @@ + + + DxfImportDialog + + + + 0 + 0 + 480 + 280 + + + + DXF Import + + + + + + Import as + + + + + + 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) + + + Editable draft objects + + + + + + + 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) + + + Editable part primitives + + + + + + + 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. + + + Individual part shapes (recommended) + + + + + + + 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. + + + Fused part shapes (fastest) + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Sunken + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + File summary + + + + + + + color: #c00; + + + Warning + + + true + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + Do not show this dialog again + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + \ No newline at end of file diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py index 39fcc9c3a9..cc67c5292b 100644 --- a/src/Mod/Draft/importDXF.py +++ b/src/Mod/Draft/importDXF.py @@ -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): diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.cpp b/src/Mod/Import/App/dxf/ImpExpDxf.cpp index 3319fe27a2..3ad107dc58 100644 --- a/src/Mod/Import/App/dxf/ImpExpDxf.cpp +++ b/src/Mod/Import/App/dxf/ImpExpDxf.cpp @@ -53,6 +53,7 @@ #include #endif +#include #include #include #include @@ -80,6 +81,35 @@ using namespace Import; using BRepAdaptor_HCurve = BRepAdaptor_Curve; #endif +std::map ImpExpDxfRead::PreScan(const std::string& filepath) +{ + std::map 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 diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.h b/src/Mod/Import/App/dxf/ImpExpDxf.h index 5a1855a651..bdf38a8623 100644 --- a/src/Mod/Import/App/dxf/ImpExpDxf.h +++ b/src/Mod/Import/App/dxf/ImpExpDxf.h @@ -61,7 +61,7 @@ public: { Py_XDECREF(DraftModule); } - + static std::map PreScan(const std::string& filepath); void StartImport() override; Py::Object getStatsAsPyObject(); diff --git a/src/Mod/Import/Gui/AppImportGuiPy.cpp b/src/Mod/Import/Gui/AppImportGuiPy.cpp index 0e4dc03c11..7654f1d4e7 100644 --- a/src/Mod/Import/Gui/AppImportGuiPy.cpp +++ b/src/Mod/Import/Gui/AppImportGuiPy.cpp @@ -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 + + std::map 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 {};