diff --git a/src/Gui/ApplicationPy.cpp b/src/Gui/ApplicationPy.cpp index 6419adc173..178531e472 100644 --- a/src/Gui/ApplicationPy.cpp +++ b/src/Gui/ApplicationPy.cpp @@ -430,6 +430,12 @@ PyMethodDef ApplicationPy::Methods[] = { "Remove all children from a group node.\n" "\n" "node : object"}, + {"suspendWaitCursor", (PyCFunction) ApplicationPy::sSuspendWaitCursor, METH_VARARGS, + "suspendWaitCursor() -> None\n\n" + "Temporarily suspends the application's wait cursor and event filter."}, + {"resumeWaitCursor", (PyCFunction) ApplicationPy::sResumeWaitCursor, METH_VARARGS, + "resumeWaitCursor() -> None\n\n" + "Resumes the application's wait cursor and event filter."}, {nullptr, nullptr, 0, nullptr} /* Sentinel */ }; @@ -1803,3 +1809,23 @@ PyObject* ApplicationPy::sSetUserEditMode(PyObject * /*self*/, PyObject *args) return Py::new_reference_to(Py::Boolean(ok)); } + +PyObject* ApplicationPy::sSuspendWaitCursor(PyObject * /*self*/, PyObject *args) +{ + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + WaitCursor::suspend(); + Py_RETURN_NONE; +} + +PyObject* ApplicationPy::sResumeWaitCursor(PyObject * /*self*/, PyObject *args) +{ + if (!PyArg_ParseTuple(args, "")) { + return nullptr; + } + + WaitCursor::resume(); + Py_RETURN_NONE; +} diff --git a/src/Gui/ApplicationPy.h b/src/Gui/ApplicationPy.h index 362cfcf88b..e12bf388d4 100644 --- a/src/Gui/ApplicationPy.h +++ b/src/Gui/ApplicationPy.h @@ -111,6 +111,9 @@ public: static PyObject* sGetUserEditMode (PyObject *self,PyObject *args); static PyObject* sSetUserEditMode (PyObject *self,PyObject *args); + static PyObject* sSuspendWaitCursor (PyObject *self, PyObject *args); + static PyObject* sResumeWaitCursor (PyObject *self, PyObject *args); + static PyMethodDef Methods[]; // clang-format on }; diff --git a/src/Gui/WaitCursor.cpp b/src/Gui/WaitCursor.cpp index 3d548b43b8..61000bb4de 100644 --- a/src/Gui/WaitCursor.cpp +++ b/src/Gui/WaitCursor.cpp @@ -189,3 +189,16 @@ void WaitCursor::setIgnoreEvents(FilterEventsFlags flags) { WaitCursorP::getInstance()->setIgnoreEvents(flags); } + +void WaitCursor::suspend() +{ + // Calling setBusy(false) will restore the cursor and remove the event filter. + WaitCursorP::getInstance()->setBusy(false); +} + +void WaitCursor::resume() +{ + // Calling setBusy(true) will set the wait cursor and reinstall the event filter. + // The WaitCursorP's internal state `isOn` correctly handles this call. + WaitCursorP::getInstance()->setBusy(true); +} \ No newline at end of file diff --git a/src/Gui/WaitCursor.h b/src/Gui/WaitCursor.h index 6031b55a36..6b6505accf 100644 --- a/src/Gui/WaitCursor.h +++ b/src/Gui/WaitCursor.h @@ -75,6 +75,19 @@ public: FilterEventsFlags ignoreEvents() const; void setIgnoreEvents(FilterEventsFlags flags = AllEvents); + /** + * @brief Suspends the wait cursor state by restoring the normal cursor + * and removing the event filter. To be used before showing an interactive + * dialog during a long operation. + */ + static void suspend(); + + /** + * @brief Resumes the wait cursor state by setting the wait cursor + * and reinstalling the event filter, if a WaitCursor is active. + */ + static void resume(); + private: FilterEventsFlags filter; static int instances; 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..c260b6fd92 --- /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(True) + self.dialog.radio_ImportAs_Primitives.setEnabled(True) + 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..a6b6b883e6 --- /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. + + + 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. + + + 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/Resources/ui/preferences-dxf.ui b/src/Mod/Draft/Resources/ui/preferences-dxf.ui index 45b348d707..27d1d024f7 100644 --- a/src/Mod/Draft/Resources/ui/preferences-dxf.ui +++ b/src/Mod/Draft/Resources/ui/preferences-dxf.ui @@ -6,27 +6,28 @@ 0 0 - 649 - 800 + 600 + 880 DXF - + - + - General options + General - + - This preferences dialog will be shown when importing/ exporting DXF files + If checked, this preferences dialog will be shown each time you import or export +a DXF file. - Show this dialog when importing and exporting + Show the importer dialog when importing a file true @@ -42,14 +43,10 @@ - Python importer is used, otherwise the newer C++ is used. -Note: C++ importer is faster, but is not as featureful yet + Use the legacy Python importer. This importer is more feature-complete but slower and requires an external library. - Use legacy Python importer - - - false + Use legacy importer dxfUseLegacyImporter @@ -62,11 +59,10 @@ Note: C++ importer is faster, but is not as featureful yet - Python exporter is used, otherwise the newer C++ is used. -Note: C++ exporter is faster, but is not as featureful yet + Use the legacy Python exporter. This exporter is more feature-complete but slower and requires an external library. - Use legacy Python exporter + Use legacy exporter dxfUseLegacyExporter @@ -80,227 +76,160 @@ Note: C++ exporter is faster, but is not as featureful yet - + Automatic update (legacy importer/exporter only) - + - - - - - Allow FreeCAD to download the Python converter for DXF import and export. -You can also do this manually by installing the "dxf_library" workbench -from the Addon Manager. - - - Allow FreeCAD to automatically download and update the DXF libraries - - - dxfAllowDownload - - - Mod/Draft - - - - + + + If checked, FreeCAD is allowed to download and update the Python libraries +required by the legacy importer. This can also be done manually by installing +the 'dxf_library' addon from the Addon Manager. + + + Allow FreeCAD to automatically download and update the DXF libraries + + + dxfAllowDownload + + + Mod/Draft + + - + - Import options + Import As - - - 6 - - - 9 - - - 9 - - - 9 - - - 9 - + - - - - true - + + + true + + + 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. - Some options are not yet available for the new importer + Editable Draft objects (highest fidelity, slowest) + + + dxfImportAsDraft + + + Mod/Draft + + + DxfImportMode + + + 0 - - - - - Import - - - - - - - If unchecked, texts and mtexts won't be imported - - - Texts and dimensions - - - dxftext - - - Mod/Draft - - - - - - - If unchecked, points won't be imported - - - points - - - dxfImportPoints - - - Mod/Draft - - - - - - - If checked, paper space objects will be imported too - - - Layouts - - - dxflayout - - - Mod/Draft - - - - - - - If you want the non-named blocks (beginning with a *) to be imported too - - - *blocks - - - dxfstarblocks - - - Mod/Draft - - - - + + + true + + + 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 and Part workbench integration. + + + Editable Part primitives (high fidelity, slower) + + + dxfImportAsPrimitives + + + Mod/Draft + + + DxfImportMode + + + 1 + + - - - - - false - - - Create - - - - - - - false - - - Only standard Part objects will be created (fastest) - - - Simple Part shapes - - - true - - - dxfCreatePart - - - Mod/Draft - - - - - - - false - - - Parametric Draft objects will be created whenever possible - - - Draft objects - - - dxfCreateDraft - - - Mod/Draft - - - - - - - false - - - Sketches will be created whenever possible - - - Sketches - - - dxfCreateSketch - - - Mod/Draft - - - - + + + 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 (balanced, recommended) + + + true + + + dxfImportAsShapes + + + Mod/Draft + + + DxfImportMode + + + 2 + + - + + + 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 importing and viewing very large files with maximum performance. + + + Fused Part shapes (lowest fidelity, fastest) + + + dxfImportAsFused + + + Mod/Draft + + + DxfImportMode + + + 3 + + + + + + + + + + Import Settings + + + + - - - - Scale factor to apply to imported files + Global scaling factor @@ -320,13 +249,13 @@ from the Addon Manager. - Scale factor to apply to DXF files on import. -The factor is the conversion between the unit of your DXF file and millimeters. -Example: for files in millimeters: 1, in centimeters: 10, - in meters: 1000, in inches: 25.4, in feet: 304.8 + Scale factor to apply to DXF files on import. The factor is the conversion +between the DXF file's unit and millimeters. Example: for files in +millimeters: 1, in centimeters: 10, in meters: 1000, in inches: 25.4, +in feet: 304.8 - 12 + 6 999999.999998999992386 @@ -345,16 +274,126 @@ Example: for files in millimeters: 1, in centimeters: 10, - - + + + Import + + + + + + + + + If checked, text, mtext, and dimension entities will be imported as Draft objects + + + Texts and dimensions + + + dxftext + + + Mod/Draft + + + + + + + If checked, point entities will be imported + + + Points + + + true + + + dxfImportPoints + + + Mod/Draft + + + + + + + If checked, entities from the paper space will also be imported. By default, +only model space is imported + + + Paper space objects + + + dxflayout + + + Mod/Draft + + + + + + + If checked, anonymous blocks (whose names begin with *) will also be imported. +These are often used for hatches and dimensions + + + Anonymous blocks (*-blocks) + + + dxfstarblocks + + + Mod/Draft + + + + + + + false + + + If checked, the boundaries of hatch objects will be imported as closed wires. +(Legacy importer only) + + + Hatch boundaries + + + importDxfHatches + + + Mod/Draft + + + + + + + + + Appearance + + + + + + - Colors will set as specified in the DXF file whenever possible. -Otherwise default colors will be applied. + If checked, colors will be set as specified in the DXF file whenever +possible. Otherwise, default FreeCAD colors are applied Use colors from the DXF file + + true + dxfGetOriginalColors @@ -363,63 +402,14 @@ Otherwise default colors will be applied. - - - - - - - - false - - - FreeCAD will try to join coincident objects into wires. -Note that this can take a while! - - - Join geometry - - - joingeometry - - - Mod/Draft - - - - - - - - - - - Objects from the same layers will be joined into Part Compounds, -turning the display faster, but making them less easily editable. - - - Merge layer contents into blocks - - - groupLayers - - - Mod/Draft - - - - - - - - + false - Imported texts will get the standard Draft Text size, -instead of the size they have in the DXF document + If checked, imported texts will get the standard Draft text size, instead of +the size defined in the DXF document. (Legacy importer only) Use standard font size for texts @@ -435,61 +425,42 @@ instead of the size they have in the DXF document - - - - - If this is checked, DXF layers will be imported as Draft Layers - - - Use layers - - - true - - - dxfUseDraftVisGroups - - - Mod/Draft - - - - + + + Advanced processing + + - - - + + + false - Hatches will be converted into simple wires + If checked, the legacy importer will attempt to join coincident geometric +objects into wires. This can be slow for large files. (Legacy importer only) - Import hatch boundaries as wires + Join geometry - importDxfHatches + joingeometry Mod/Draft - - - - - + false - If polylines have a width defined, they will be rendered -as closed wires with correct width + If checked, polylines that have a width property will be rendered as faces +representing that width. (Legacy importer only) Render polylines with width @@ -502,31 +473,39 @@ as closed wires with correct width + + + + false + + + If checked, the legacy importer will attempt to create Sketcher objects +instead of Draft or Part objects. This overrides the 'Import As' setting + + + Create sketches + + + dxfCreateSketch + + + Mod/Draft + + + - + Export options - + - - - - true - - - - Some options are not yet available for the new exporter - - - - - + @@ -598,7 +577,7 @@ If it is set to '0' the whole spline is treated as a straight segment. - + @@ -621,7 +600,7 @@ If it is set to '0' the whole spline is treated as a straight segment. - + @@ -645,7 +624,7 @@ This might fail for post DXF R12 templates. - + @@ -671,21 +650,20 @@ This might fail for post DXF R12 templates. - + Qt::Vertical 20 - 40 + 0 - Gui::PrefCheckBox @@ -693,82 +671,18 @@ This might fail for post DXF R12 templates.
Gui/PrefWidgets.h
- Gui::PrefDoubleSpinBox - QDoubleSpinBox + Gui::PrefRadioButton + QRadioButton
Gui/PrefWidgets.h
- Gui::PrefRadioButton - QRadioButton + Gui::PrefDoubleSpinBox + QDoubleSpinBox
Gui/PrefWidgets.h
- - checkBox_dxfUseLegacyImporter - toggled(bool) - label_Create - setEnabled(bool) - - - 20 - 20 - - - 20 - 20 - - - - - checkBox_dxfUseLegacyImporter - toggled(bool) - radioButton_dxfCreatePart - setEnabled(bool) - - - 20 - 20 - - - 20 - 20 - - - - - checkBox_dxfUseLegacyImporter - toggled(bool) - radioButton_dxfCreateDraft - setEnabled(bool) - - - 20 - 20 - - - 20 - 20 - - - - - checkBox_dxfUseLegacyImporter - toggled(bool) - radioButton_dxfCreateSketch - setEnabled(bool) - - - 20 - 20 - - - 20 - 20 - - - checkBox_dxfUseLegacyImporter toggled(bool) @@ -785,6 +699,22 @@ This might fail for post DXF R12 templates. + + checkBox_dxfUseLegacyImporter + toggled(bool) + checkBox_renderPolylineWidth + setEnabled(bool) + + + 20 + 20 + + + 20 + 20 + + + checkBox_dxfUseLegacyImporter toggled(bool) @@ -820,7 +750,55 @@ This might fail for post DXF R12 templates. checkBox_dxfUseLegacyImporter toggled(bool) - checkBox_renderPolylineWidth + checkBox_dxfCreateSketch + setEnabled(bool) + + + 20 + 20 + + + 20 + 20 + + + + + checkBox_dxfCreateSketch + toggled(bool) + groupBox_ImportAs + setDisabled(bool) + + + 20 + 20 + + + 20 + 20 + + + + + checkBox_dxfUseLegacyExporter + toggled(bool) + checkBox_dxfmesh + setEnabled(bool) + + + 20 + 20 + + + 20 + 20 + + + + + checkBox_dxfUseLegacyExporter + toggled(bool) + checkBox_dxfproject setEnabled(bool) diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py index 1325611448..71fabd41d3 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 @@ -78,6 +79,13 @@ if gui: draftui = FreeCADGui.draftToolBar except (AttributeError, NameError): draftui = None + try: + from draftviewproviders.view_base import ViewProviderDraft + from draftviewproviders.view_wire import ViewProviderWire + from draftviewproviders.view_dimension import ViewProviderLinearDimension + except ImportError: + ViewProviderDraft = None + ViewProviderWire = None from draftutils.translate import translate from PySide import QtWidgets else: @@ -2789,12 +2797,109 @@ def warn(dxfobject, num=None): badobjects.append(dxfobject) +def _import_dxf_file(filename, doc_name=None): + """ + Internal helper to handle the core logic for both open and insert. + """ + hGrp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + use_legacy = hGrp.GetBool("dxfUseLegacyImporter", False) + readPreferences() + + # --- Dialog Workflow --- + try: + if gui: + FreeCADGui.suspendWaitCursor() + + 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_(): + # Save the integer mode from the pop-up dialog. + hGrp.SetInt("DxfImportMode", dlg.get_selected_mode()) + + # Keep the main preferences booleans + # in sync with the choice just made in the pop-up dialog. + mode = dlg.get_selected_mode() + params.set_param("dxfImportAsDraft", mode == 0) + params.set_param("dxfImportAsPrimitives", mode == 1) + params.set_param("dxfImportAsShapes", mode == 2) + params.set_param("dxfImportAsFused", mode == 3) + hGrp.SetBool("dxfShowDialog", dlg.get_show_dialog_again()) + else: + return None, None, None, None # Return None to indicate cancellation + finally: + if gui: + FreeCADGui.resumeWaitCursor() + + import_mode = hGrp.GetInt("DxfImportMode", 2) + + # --- Document Handling --- + if doc_name: # INSERT operation + try: + doc = FreeCAD.getDocument(doc_name) + except NameError: + doc = FreeCAD.newDocument(doc_name) + FreeCAD.setActiveDocument(doc_name) + else: # OPEN operation + docname = os.path.splitext(os.path.basename(filename))[0] + doc = FreeCAD.newDocument(docname) + doc.Label = docname + FreeCAD.setActiveDocument(doc.Name) + + # --- Core Import Execution --- + processing_start_time = time.perf_counter() + + # Take snapshot of objects before import + objects_before = set(doc.Objects) + + stats = None # For C++ importer stats + if use_legacy: + getDXFlibs() + if dxfReader: + processdxf(doc, filename) + else: + errorDXFLib(gui) + return None, None + else: # Modern C++ Importer + if gui: + import ImportGui + stats = ImportGui.readDXF(filename) + else: + import Import + stats = Import.readDXF(filename) + + # Find the newly created objects + objects_after = set(doc.Objects) + newly_created_objects = objects_after - objects_before + + # --- Post-processing step --- + if not use_legacy and newly_created_objects: + draft_postprocessor = DxfDraftPostProcessor(doc, newly_created_objects, import_mode) + draft_postprocessor.run() + + Draft.convert_draft_texts() # This is a general utility that should run for both importers + doc.recompute() + + processing_end_time = time.perf_counter() + + # Return the results for the reporter + return doc, stats, processing_start_time, processing_end_time + + def open(filename): """Open a file and return a new document. - If the global variable `dxfUseLegacyImporter` exists, - it will process `filename` with `processdxf`. - Otherwise, it will use the `Import` module, `Import.readDXF(filename)`. + This function handles the import of a DXF file into a new document. + It shows an import dialog for the modern C++ importer if configured to do so. + It manages the import workflow, including pre-processing, calling the + correct backend (legacy or modern C++), and post-processing. Parameters ---------- @@ -2803,96 +2908,38 @@ def open(filename): Returns ------- - App::Document - The new document object with objects and shapes built from `filename`. - - To do - ----- - Use local variables, not global variables. + App::Document or None + The new document object with imported content, or None if the + operation was cancelled or failed. """ - readPreferences() - total_start_time = time.perf_counter() + doc, stats, start_time, end_time = _import_dxf_file(filename, doc_name=None) - if dxfUseLegacyImporter: - getDXFlibs() - if dxfReader: - docname = os.path.splitext(os.path.basename(filename))[0] - doc = FreeCAD.newDocument(docname) - doc.Label = docname - processdxf(doc, filename) - return doc - else: - errorDXFLib(gui) - else: - 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) - else: - import Import - stats = Import.readDXF(filename) - - 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() + if doc and stats: + reporter = DxfImportReporter(filename, stats, end_time - start_time) + reporter.report_to_console() + return doc def insert(filename, docname): """Import a file into the specified document. + This function handles the import of a DXF file into a specified document. + If the document does not exist, it will be created. It shows an import + dialog for the modern C++ importer if configured to do so. + Parameters ---------- filename : str The path to the file to import. - docname : str - The name of an `App::Document` instance into which - the objects and shapes from `filename` will be imported. - - If the document doesn't exist, it is created - and set as the active document. - - To do - ----- - Use local variables, not global variables. + The name of an App::Document instance to import the content into. """ - readPreferences() - total_start_time = time.perf_counter() - try: - doc = FreeCAD.getDocument(docname) - except NameError: - doc = FreeCAD.newDocument(docname) - FreeCAD.setActiveDocument(docname) - if dxfUseLegacyImporter: - getDXFlibs() - if dxfReader: - processdxf(doc, filename) - else: - errorDXFLib(gui) - else: - stats = None - if gui: - import ImportGui - stats = ImportGui.readDXF(filename) - else: - import Import - stats = Import.readDXF(filename) + doc, stats, start_time, end_time = _import_dxf_file(filename, doc_name=docname) - total_end_time = time.perf_counter() - if stats: - reporter = DxfImportReporter(filename, stats, total_end_time - total_start_time) - reporter.report_to_console() + if doc and stats: + reporter = DxfImportReporter(filename, stats, end_time - 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. @@ -4181,41 +4228,77 @@ 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 - dxfCreatePart = params.get_param("dxfCreatePart") - dxfCreateDraft = params.get_param("dxfCreateDraft") - dxfCreateSketch = params.get_param("dxfCreateSketch") - dxfDiscretizeCurves = params.get_param("DiscretizeEllipses") - dxfStarBlocks = params.get_param("dxfstarblocks") - dxfMakeBlocks = params.get_param("groupLayers") - dxfJoin = params.get_param("joingeometry") - dxfRenderPolylineWidth = params.get_param("renderPolylineWidth") - dxfImportTexts = params.get_param("dxftext") - dxfImportLayouts = params.get_param("dxflayout") - dxfImportPoints = params.get_param("dxfImportPoints") - dxfImportHatches = params.get_param("importDxfHatches") - dxfUseStandardSize = params.get_param("dxfStdSize") - dxfGetColors = params.get_param("dxfGetOriginalColors") - dxfUseDraftVisGroups = params.get_param("dxfUseDraftVisGroups") - dxfMakeFaceMode = params.get_param("MakeFaceMode") - dxfUseLegacyImporter = params.get_param("dxfUseLegacyImporter") - dxfUseLegacyExporter = params.get_param("dxfUseLegacyExporter") + global dxfDiscretizeCurves, dxfStarBlocks, dxfMakeBlocks, dxfJoin, dxfRenderPolylineWidth + global dxfImportTexts, dxfImportLayouts, dxfImportPoints, dxfImportHatches, dxfUseStandardSize + global dxfGetColors, dxfUseDraftVisGroups, dxfMakeFaceMode, dxfBrightBackground, dxfDefaultColor + global dxfUseLegacyImporter, dxfExportBlocks, dxfScaling, dxfUseLegacyExporter + + # Use the direct C++ API via Python for all parameter access + hGrp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft") + + dxfUseLegacyImporter = hGrp.GetBool("dxfUseLegacyImporter", False) + + # Synchronization Bridge (Booleans -> Integer) + # Read the boolean parameters from the main preferences dialog. Based on which one is true, set + # the single 'DxfImportMode' integer parameter that the C++ importer and legacy importer logic + # rely on. This ensures the setting from the main preferences is always respected at the start + # of an import. + if hGrp.GetBool("dxfImportAsDraft", False): + import_mode = 0 + elif hGrp.GetBool("dxfImportAsPrimitives", False): + import_mode = 1 + elif hGrp.GetBool("dxfImportAsFused", False): + import_mode = 3 + else: # Default to "Individual part shapes" + import_mode = 2 + hGrp.SetInt("DxfImportMode", import_mode) + + # The legacy importer logic now reads the unified import_mode integer. + # The modern importer reads its settings directly in C++. + if dxfUseLegacyImporter: + # Legacy override for sketch creation takes highest priority + dxfCreateSketch = hGrp.GetBool("dxfCreateSketch", False) + + if dxfCreateSketch: # dxfCreateSketch overrides the import mode for the legacy importer + dxfCreatePart = False + dxfCreateDraft = False + dxfMakeBlocks = False + # The 'import_mode' variable is now set by the UI synchronization bridge that runs just + # before this block. We now translate the existing 'import_mode' variable into the old + # flags. + elif import_mode == 0: # Editable draft objects + dxfMakeBlocks = False + dxfCreatePart = False + dxfCreateDraft = True + elif import_mode == 3: # Fused part shapes + dxfMakeBlocks = True + dxfCreatePart = False + dxfCreateDraft = False + else: # Individual part shapes or Primitives (modes 1 and 2) + 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) + dxfBrightBackground = isBrightBackground() dxfDefaultColor = getColor() - dxfExportBlocks = params.get_param("dxfExportBlocks") - dxfScaling = params.get_param("dxfScaling") - class DxfImportReporter: """Formats and reports statistics from a DXF import process.""" @@ -4346,3 +4429,515 @@ class DxfImportReporter: """ output_string = self.to_console_string() FCC.PrintMessage(output_string) + + +class DxfDraftPostProcessor: + """ + Handles the post-processing of DXF files imported as Part objects, + converting them into fully parametric Draft objects while preserving + the block and layer hierarchy. + """ + def __init__(self, doc, new_objects, import_mode): + self.doc = doc + self.all_imported_objects = new_objects + self.import_mode = import_mode + self.all_originals_to_delete = set() + self.newly_created_draft_objects = [] + + def _categorize_objects(self): + """ + Scans newly created objects from the C++ importer and categorizes them. + """ + block_definitions = {} + for group_name in ["_BlockDefinitions", "_UnreferencedBlocks"]: + block_group = self.doc.getObject(group_name) + if block_group: + for block_def_obj in block_group.Group: + if block_def_obj.isValid() and block_def_obj.isDerivedFrom("Part::Compound"): + block_definitions[block_def_obj] = [ + child for child in block_def_obj.Links + if child.isValid() + ] + + all_block_internal_objects_set = set() + for block_def, children in block_definitions.items(): + all_block_internal_objects_set.add(block_def) + all_block_internal_objects_set.update(children) + + top_level_geometry = [] + placeholders = [] + for obj in self.all_imported_objects: + if not obj.isValid() or obj in all_block_internal_objects_set: + continue + + if obj.isDerivedFrom("App::FeaturePython") and hasattr(obj, "DxfEntityType"): + placeholders.append(obj) + elif obj.isDerivedFrom("Part::Feature") or obj.isDerivedFrom("App::Link"): + top_level_geometry.append(obj) + + return block_definitions, top_level_geometry, placeholders + + def _create_draft_object_from_part(self, part_obj): + """ + Converts an intermediate Part object (from C++ importer) to a final Draft object, + ensuring correct underlying C++ object typing and property management. + Returns a tuple: (new_draft_object, type_string) or (None, None). + """ + if self.import_mode != 0: + # In non-Draft modes, do not convert geometry. Return it as is. + return part_obj, "KeptAsIs" + + # Skip invalid objects or objects that are block definitions themselves (their + # links/children will be converted) + if not part_obj.isValid() or \ + (part_obj.isDerivedFrom("Part::Compound") and hasattr(part_obj, "Links")): + return None, None + + new_obj = None + obj_type_str = None # Will be set based on converted type + + # Handle specific Part primitives (created directly by C++ importer as Part::Line, + # Part::Circle, Part::Vertex) These C++ primitives (Part::Line, Part::Circle) inherently + # have Shape and Placement. Part::Vertex is special, handled separately below. + if part_obj.isDerivedFrom("Part::Line"): + # Input `part_obj` is Part::Line. Create a Part::Part2DObjectPython as the + # Python-extensible base for Draft Line. Part::Part2DObjectPython (via Part::Feature) + # inherently has Shape and Placement, and supports .Proxy. + new_obj = self.doc.addObject("Part::Part2DObjectPython", + self.doc.getUniqueObjectName("Line")) + # Transfer the TopoDS_Shape from the original Part::Line to the new object's Shape + # property. + new_obj.Shape = part_obj.Shape + Draft.Wire(new_obj) # Attach the Python proxy. It will find Shape, Placement. + + # Manually transfer the parametric data from the Part::Line primitive + # to the new Draft.Wire's 'Points' property. + start_point = FreeCAD.Vector(part_obj.X1.Value, part_obj.Y1.Value, part_obj.Z1.Value) + end_point = FreeCAD.Vector(part_obj.X2.Value, part_obj.Y2.Value, part_obj.Z2.Value) + new_obj.Points = [start_point, end_point] + + obj_type_str = "Line" + + elif part_obj.isDerivedFrom("Part::Circle"): + # Input `part_obj` is Part::Circle. Create a Part::Part2DObjectPython. + new_obj = self.doc.addObject("Part::Part2DObjectPython", + self.doc.getUniqueObjectName("Circle")) + # Transfer the TopoDS_Shape from the original Part::Circle. This needs to happen + # *before* proxy attach. + new_obj.Shape = part_obj.Shape + + # Attach the Python proxy + # This call will add properties like Radius, FirstAngle, LastAngle to new_obj. + Draft.Circle(new_obj) + + # Transfer data *after* proxy attachment. + # Now that Draft.Circle(new_obj) has run and added the properties, we can assign values + # to them. + # Part::Circle has Radius, Angle1, Angle2 properties. + # Draft.Circle proxy uses FirstAngle and LastAngle instead of Angle1 and Angle2. + if hasattr(part_obj, 'Radius'): + new_obj.Radius = FreeCAD.Units.Quantity(part_obj.Radius.Value, "mm") + + # Calculate and transfer angles + if hasattr(part_obj, 'Angle1') and hasattr(part_obj, 'Angle2'): + start_angle, end_angle = self._get_canonical_angles( + part_obj.Angle1.Value, + part_obj.Angle2.Value, + part_obj.Radius.Value + ) + + new_obj.FirstAngle = FreeCAD.Units.Quantity(start_angle, "deg") + new_obj.LastAngle = FreeCAD.Units.Quantity(end_angle, "deg") + + # Determine the final object type string based on the canonical angles + is_full_circle = (abs(new_obj.FirstAngle.Value - 0.0) < 1e-7 and + abs(new_obj.LastAngle.Value - 360.0) < 1e-7) + + obj_type_str = "Circle" if is_full_circle else "Arc" + + + elif part_obj.isDerivedFrom("Part::Vertex"): # Input `part_obj` is Part::Vertex (C++ primitive for a point location). + # For Draft.Point, the proxy expects an App::FeaturePython base. + new_obj = self.doc.addObject("App::FeaturePython", self.doc.getUniqueObjectName("Point")) + new_obj.addExtension("Part::AttachExtensionPython") # Needed to provide Placement for App::FeaturePython. + # Transfer Placement explicitly from the original Part::Vertex. + if hasattr(part_obj, 'Placement'): + new_obj.Placement = part_obj.Placement + else: + new_obj.Placement = FreeCAD.Placement() + Draft.Point(new_obj) # Attach the Python proxy. + obj_type_str = "Point" + + elif part_obj.isDerivedFrom("Part::Ellipse"): + # Determine if it's a full ellipse or an arc + # The span check handles cases like (0, 360) or (-180, 180) + span = abs(part_obj.Angle2.Value - part_obj.Angle1.Value) + is_full_ellipse = abs(span % 360.0) < 1e-6 + + if is_full_ellipse: + # Create the C++ base object that has .Shape and .Placement. + new_obj = self.doc.addObject("Part::Part2DObjectPython", self.doc.getUniqueObjectName("Ellipse")) + + # Attach the parametric Draft.Ellipse Python proxy. + Draft.Ellipse(new_obj) + + # Transfer the parametric properties from the imported primitive to the new Draft + # object. The proxy will handle recomputing the shape. + new_obj.MajorRadius = part_obj.MajorRadius + new_obj.MinorRadius = part_obj.MinorRadius + new_obj.Placement = part_obj.Placement + + obj_type_str = "Ellipse" + else: + # Fallback for elliptical arcs. + + new_obj = self.doc.addObject("Part::Part2DObjectPython", self.doc.getUniqueObjectName("EllipticalArc")) + Draft.Wire(new_obj) # Attach proxy. + + # Re-create geometry at the origin using parametric properties. + # Convert degrees back to radians for the geometry kernel. + center_at_origin = FreeCAD.Vector(0, 0, 0) + geom = Part.Ellipse(center_at_origin, part_obj.MajorRadius.Value, part_obj.MinorRadius.Value) + shape_at_origin = geom.toShape(math.radians(part_obj.Angle1.Value), + math.radians(part_obj.Angle2.Value)) + + # Assign the un-transformed shape and the separate placement. + new_obj.Shape = shape_at_origin + new_obj.Placement = part_obj.Placement + obj_type_str = "Shape" + + # --- Handle generic Part::Feature objects (from C++ importer, wrapping TopoDS_Shapes like Wires, Splines, Ellipses) --- + elif part_obj.isDerivedFrom("Part::Feature"): # Input `part_obj` is a generic Part::Feature (from C++ importer). + shape = part_obj.Shape # This is the underlying TopoDS_Shape (Wire, Edge, Compound, Face etc.). + if not shape.isValid(): + return None, None + + # Determine specific Draft object type based on the ShapeType of the TopoDS_Shape. + if shape.ShapeType == "Wire": # If the TopoDS_Shape is a Wire (from DXF POLYLINE). + # Create a Part::Part2DObjectPython as the Python-extensible base for Draft Wire. + new_obj = self.doc.addObject("Part::Part2DObjectPython", self.doc.getUniqueObjectName("Wire")) + new_obj.Shape = shape # Transfer the TopoDS_Wire from the original Part::Feature. + Draft.Wire(new_obj) # Attach Python proxy. It will find Shape, Placement. + + # Check if all segments of the wire are straight lines. + # If so, we can safely populate the .Points property to make it parametric. + # Otherwise, we do nothing, leaving it as a non-parametric but geometrically correct shape. + is_all_lines = True + + for edge in shape.Edges: + if edge.Curve.TypeId == "Part::GeomLine": + continue # This is a straight segment + else: + is_all_lines = False + break # Found a curve, no need to check further + + if is_all_lines and shape.OrderedVertexes: + # All segments are straight, so we can make it an editable wire + points = [v.Point for v in shape.OrderedVertexes] + new_obj.Points = points + + new_obj.Closed = shape.isClosed() # Transfer specific properties expected by Draft.Wire. + obj_type_str = "Wire" + + # Fallback for other Part::Feature shapes (e.g., 3DFACE, SOLID, or unsupported Edge types). + else: # If the TopoDS_Shape is not a recognized primitive (e.g., Compound, Face, Solid). + # Wrap it in a Part::FeaturePython to allow Python property customization if needed. + new_obj = self.doc.addObject("Part::FeaturePython", self.doc.getUniqueObjectName("Shape")) + new_obj.addExtension("Part::AttachExtensionPython") # Add extension for Placement for App::FeaturePython. + new_obj.Shape = shape # Assign the TopoDS_Shape from the original Part::Feature. + # Explicitly set Placement for App::FeaturePython. + if hasattr(part_obj, 'Placement'): + new_obj.Placement = part_obj.Placement + else: + new_obj.Placement = FreeCAD.Placement() + # No specific Draft proxy for generic "Shape", but it's Python extensible. + obj_type_str = "Shape" + + # --- Handle App::Link objects (block instances from C++ importer) --- + elif part_obj.isDerivedFrom("App::Link"): # Input `part_obj` is an App::Link. + # App::Link objects are already suitable as a base for Draft.Clone/Array links. + # They natively have Placement and Link properties, and support .Proxy. + new_obj = part_obj # Reuse the object directly. + obj_type_str = "Link" + + # --- Handle App::FeaturePython placeholder objects (Text, Dimension from C++ importer) --- + elif part_obj.isDerivedFrom("App::FeaturePython"): # Input `part_obj` is an App::FeaturePython placeholder. + # These are specific placeholders the C++ importer created (`DxfEntityType` property). + # They are processed later in `_create_from_placeholders` to become proper Draft.Text/Dimension objects. + return None, None # Don't process them here; let the dedicated function handle them. + + # --- Final Common Steps for Newly Created Draft Objects --- + if new_obj: + new_obj.Label = part_obj.Label # Always transfer label. + + # If `new_obj` was freshly created (not `part_obj` reused), and `part_obj` had a Placement, + # ensure `new_obj`'s Placement is correctly set from `part_obj`. + # For `Part::*` types, Placement is set implicitly by the `addObject` call based on their `Shape`. + # For `App::FeaturePython` (like for Point and generic Shape fallback), explicit assignment is needed. + if new_obj is not part_obj: + if hasattr(part_obj, "Placement") and hasattr(new_obj, "Placement"): + new_obj.Placement = part_obj.Placement + elif not hasattr(new_obj, "Placement"): + # This should ideally not happen with the corrected logic above. + FCC.PrintWarning(f"Created object '{new_obj.Label}' of type '{obj_type_str}' does not have a 'Placement' property even after intended setup. This is unexpected.\n") + + # Add the original object (from C++ importer) to the list for deletion. + if new_obj is not part_obj: + self.all_originals_to_delete.add(part_obj) + + return new_obj, obj_type_str + + # If no conversion could be made (e.g., unsupported DXF entity not falling into a handled case), + # mark original for deletion and return None. + self.all_originals_to_delete.add(part_obj) + FCC.PrintWarning(f"DXF Post-Processor: Failed to convert object '{part_obj.Label}'. Discarding.\n") + return None, None + + def _parent_object_to_layer(self, new_obj, original_obj): + """Finds the correct layer from the original object and parents the new object to it.""" + if hasattr(original_obj, "OriginalLayer"): + layer_name = original_obj.OriginalLayer + + found_layers = self.doc.getObjectsByLabel(layer_name) + + layer_obj = None + if found_layers: + for l_obj in found_layers: + if Draft.get_type(l_obj) == 'Layer': + layer_obj = l_obj + break + + if layer_obj: + layer_obj.Proxy.addObject(layer_obj, new_obj) + else: + FCC.PrintWarning(f"DXF Post-Processor: Could not find a valid Draft Layer with label '{layer_name}' for object '{new_obj.Label}'.\n") + + def _create_and_parent_geometry(self, intermediate_obj): + """High-level helper to convert, name, and parent a single geometric object.""" + new_draft_obj, obj_type_str = self._create_draft_object_from_part(intermediate_obj) + if new_draft_obj: + label = intermediate_obj.Label + if not label or "__Feature" in label: + label = self.doc.getUniqueObjectName(obj_type_str) + new_draft_obj.Label = label + self._parent_object_to_layer(new_draft_obj, intermediate_obj) + self.newly_created_draft_objects.append(new_draft_obj) + else: + FCC.PrintWarning(f"DXF Post-Processor: Failed to convert object '{intermediate_obj.Label}'. Discarding.\n") + return new_draft_obj + + def _create_from_placeholders(self, placeholders): + """Creates final Draft objects from text/dimension placeholders.""" + if not placeholders: + return + + for placeholder in placeholders: + if not placeholder.isValid(): + continue + new_obj = None + try: + if placeholder.DxfEntityType == "DIMENSION": + # 1. Create the base object and attach the proxy, which adds the needed properties. + dim = self.doc.addObject("App::FeaturePython", "Dimension") + _Dimension(dim) + + if FreeCAD.GuiUp: + ViewProviderLinearDimension(dim.ViewObject) + + # 2. Get the transformation from the placeholder's Placement property. + plc = placeholder.Placement + + # 3. Transform the defining points from the placeholder's local coordinate system + # into the world coordinate system. + p_start = plc.multVec(placeholder.Start) + p_end = plc.multVec(placeholder.End) + p_dimline = plc.multVec(placeholder.Dimline) + + # 4. Assign these new, transformed points to the final dimension object. + dim.Start = p_start + dim.End = p_end + dim.Dimline = p_dimline + + # Do NOT try to set dim.Placement, as it does not exist. + + new_obj = dim + + # Check for and apply the dimension type (horizontal, vertical, etc.) + # This information is now plumbed through from the C++ importer. + if hasattr(placeholder, "DxfDimensionType"): + # The lower bits of the type flag define the dimension's nature. + # 0 = Rotated, Horizontal, or Vertical + # 1 = Aligned + # Other values are for angular, diameter, etc., not handled here. + dim_type = placeholder.DxfDimensionType & 0x0F + + # A type of 0 indicates that the dimension is projected. The + # projection direction is given by its rotation angle. + if dim_type == 0 and hasattr(placeholder, "DxfRotation"): + angle = placeholder.DxfRotation.Value # Angle is in radians + + # The Direction property on a Draft.Dimension controls its + # projection. Setting it here ensures the ViewProvider + # will draw it correctly as horizontal, vertical, or rotated. + direction_vector = FreeCAD.Vector(math.cos(angle), math.sin(angle), 0) + dim.Direction = direction_vector + + elif placeholder.DxfEntityType == "TEXT": + text_obj = Draft.make_text(placeholder.Text) + text_obj.Placement = placeholder.Placement + if FreeCAD.GuiUp: + text_obj.addProperty("App::PropertyFloat", "DxfTextHeight", "Internal") + text_obj.DxfTextHeight = placeholder.DxfTextHeight + new_obj = text_obj + + if new_obj: + new_obj.Label = placeholder.Label + self._parent_object_to_layer(new_obj, placeholder) + self.newly_created_draft_objects.append(new_obj) + except Exception as e: + FCC.PrintWarning(f"Could not create Draft object from placeholder '{placeholder.Label}': {e}\n") + + self.all_originals_to_delete.update(placeholders) + + def _apply_gui_styles(self): + """Attaches correct ViewProviders and styles to new Draft objects.""" + if not FreeCAD.GuiUp: + return + + # We style all newly created Draft objects, which are collected in this list. + # This now includes block children, top-level geometry, and placeholders. + all_objects_to_style = self.newly_created_draft_objects + + for obj in all_objects_to_style: + if obj.isValid() and hasattr(obj, "ViewObject") and hasattr(obj, "Proxy"): + try: + proxy_name = obj.Proxy.__class__.__name__ + if proxy_name in ("Wire", "Line"): + if ViewProviderWire: ViewProviderWire(obj.ViewObject) + elif proxy_name == "Circle": + if ViewProviderDraft: ViewProviderDraft(obj.ViewObject) + elif proxy_name == "Text": + if hasattr(obj, "DxfTextHeight"): + obj.ViewObject.FontSize = obj.DxfTextHeight * TEXTSCALING + except Exception as e: + FCC.PrintWarning(f"Failed to set ViewProvider for {obj.Name}: {e}\n") + + def _delete_objects_in_batch(self): + """Safely deletes all objects marked for removal.""" + if not self.all_originals_to_delete: + return + for obj in self.all_originals_to_delete: + if obj.isValid() and self.doc.getObject(obj.Name) is not None: + try: + if not obj.isDerivedFrom("App::DocumentObjectGroup") and not obj.isDerivedFrom("App::Link"): + self.doc.removeObject(obj.Name) + except Exception as e: + FCC.PrintWarning(f"Failed to delete object '{getattr(obj, 'Label', obj.Name)}': {e}\n") + + def _cleanup_organizational_groups(self): + """Removes empty organizational groups after processing.""" + for group_name in ["_BlockDefinitions", "_UnreferencedBlocks"]: + group = self.doc.getObject(group_name) + if group and not group.Group: + try: + self.doc.removeObject(group.Name) + except Exception as e: + FCC.PrintWarning( + "DXF Post-Processor: Could not remove temporary group " + f"'{group.Name}': {e}\n" + ) + + def _get_canonical_angles(self, start_angle_deg, end_angle_deg, radius_mm): + """ + Calculates canonical start and end angles for a Draft Arc/Circle that are + both geometrically equivalent to the input and syntactically valid for + FreeCAD's App::PropertyAngle, which constrains values to [-360, 360]. + + This is necessary because the C++ importer may provide angles outside this + range (e.g., end_angle > 360) to unambiguously define an arc's span and + distinguish between minor and major arcs. This function finds an + equivalent angle pair that respects the C++ constraints while preserving + the original geometry (span and direction). + """ + # Calculate the original angular span. + span = end_angle_deg - start_angle_deg + + # Handle degenerate and full-circle cases first. + # Case: A zero-radius, zero-span arc is a point. + if abs(radius_mm) < 1e-9 and abs(span) < 1e-9: + return 0.0, 0.0 + + # A span that is a multiple of 360 degrees is a full circle. + # Use a tolerance for floating point inaccuracies. + if abs(span % 360.0) < 1e-6 and abs(span) > 1e-7: + # Return the canonical representation for a full circle in Draft. + return 0.0, 360.0 + + # Normalize the start angle to a canonical [0, 360] range. + canonical_start = start_angle_deg % 360.0 + if canonical_start < 0: + canonical_start += 360.0 + + # Calculate the geometrically correct end angle based on the preserved span. + canonical_end = canonical_start + span + + # Find a valid representation within the [-360, 360] constraints. + # We can shift both start and end by multiples of 360 without changing the geometry. + # This "slides" the angular window until it fits within the allowed range. + # This handles cases where the calculated end > 360 or start is very negative. + while canonical_start > 360.0 or canonical_end > 360.0: + canonical_start -= 360.0 + canonical_end -= 360.0 + + while canonical_start < -360.0 or canonical_end < -360.0: + canonical_start += 360.0 + canonical_end += 360.0 + + # At this point, the pair (canonical_start, canonical_end) is both + # geometrically correct and should be valid for App::PropertyAngle. + return canonical_start, canonical_end + + def run(self): + """Executes the entire post-processing workflow.""" + FCC.PrintMessage("\n--- DXF DRAFT POST-PROCESSING ---\n") + if not self.all_imported_objects: + return + + self.doc.openTransaction("DXF Post-processing") + try: + block_defs, top_geo, placeholders = self._categorize_objects() + + # Process geometry inside block definitions + for block_def_obj, original_children in block_defs.items(): + new_draft_children = [self._create_and_parent_geometry(child) for child in original_children] + block_def_obj.Links = [obj for obj in new_draft_children if obj] + self.all_originals_to_delete.update(set(original_children) - set(new_draft_children)) + + # Process top-level geometry + converted_top_geo = [] + for part_obj in top_geo: + new_obj = self._create_and_parent_geometry(part_obj) + if new_obj: + converted_top_geo.append(new_obj) + self.all_originals_to_delete.update(set(top_geo) - set(converted_top_geo)) + + # Process placeholders like Text and Dimensions + self._create_from_placeholders(placeholders) + + # Perform all deletions at once + self._delete_objects_in_batch() + + except Exception as e: + self.doc.abortTransaction() + FCC.PrintError(f"Aborting DXF post-processing due to an error: {e}\n") + import traceback + traceback.print_exc() + return + finally: + self.doc.commitTransaction() + + self._apply_gui_styles() + self._cleanup_organizational_groups() + + self.doc.recompute() + FCC.PrintMessage("--- Draft post-processing finished. ---\n") diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.cpp b/src/Mod/Import/App/dxf/ImpExpDxf.cpp index 7a6add1c19..62ab50ca6c 100644 --- a/src/Mod/Import/App/dxf/ImpExpDxf.cpp +++ b/src/Mod/Import/App/dxf/ImpExpDxf.cpp @@ -28,31 +28,42 @@ #include #endif #include +#include #include #include #include +#include #include +#include +#include #include #include #include #include +#include +#include +#include #include #include +#include #include #include #include #include #include #include +#include #include #include #include #include #include #include +#include #include #endif +#include #include #include #include @@ -65,10 +76,12 @@ #include #include #include -#include #include #include +#include +#include #include +#include #include #include "ImpExpDxf.h" @@ -80,6 +93,344 @@ using namespace Import; using BRepAdaptor_HCurve = BRepAdaptor_Curve; #endif +namespace +{ + +Part::Circle* createCirclePrimitive(const TopoDS_Edge& edge, App::Document* doc, const char* name); +Part::Line* createLinePrimitive(const TopoDS_Edge& edge, App::Document* doc, const char* name); +Part::Ellipse* +createEllipsePrimitive(const TopoDS_Edge& edge, App::Document* doc, const char* name); +Part::Vertex* +createVertexPrimitive(const TopoDS_Vertex& vertex, App::Document* doc, const char* name); +Part::Feature* +createGenericShapeFeature(const TopoDS_Shape& shape, App::Document* doc, const char* name); + +} // namespace + +namespace +{ + +// Helper function to create and configure a Part::Ellipse primitive from a TopoDS_Edge +Part::Ellipse* createEllipsePrimitive(const TopoDS_Edge& edge, App::Document* doc, const char* name) +{ + auto* p = doc->addObject(name); + if (!p) { + return nullptr; + } + + TopLoc_Location loc; + Standard_Real first, last; + Handle(Geom_Curve) aCurve = BRep_Tool::Curve(edge, loc, first, last); + + if (aCurve->IsInstance(Geom_Ellipse::get_type_descriptor())) { + Handle(Geom_Ellipse) ellipse = Handle(Geom_Ellipse)::DownCast(aCurve); + + // Set parametric properties + p->MajorRadius.setValue(ellipse->MajorRadius()); + p->MinorRadius.setValue(ellipse->MinorRadius()); + + // The axis contains the full transformation (location and orientation). + // It's crucial to apply the TopLoc_Location transformation from the edge. + gp_Ax2 axis = ellipse->Position().Transformed(loc.Transformation()); + gp_Pnt center = axis.Location(); + gp_Dir xDir = axis.XDirection(); // Major Axis Direction + gp_Dir yDir = axis.YDirection(); // Minor Axis Direction + gp_Dir zDir = axis.Direction(); // Normal + + Base::Placement plc; + plc.setPosition(Base::Vector3d(center.X(), center.Y(), center.Z())); + plc.setRotation( + Base::Rotation::makeRotationByAxes(Base::Vector3d(xDir.X(), xDir.Y(), xDir.Z()), + Base::Vector3d(yDir.X(), yDir.Y(), yDir.Z()), + Base::Vector3d(zDir.X(), zDir.Y(), zDir.Z()))); + p->Placement.setValue(plc); + + // Set angles for arcs, converting from radians (OCC) to degrees (PropertyAngle) + BRep_Tool::Range(edge, first, last); + p->Angle1.setValue(Base::toDegrees(first)); + p->Angle2.setValue(Base::toDegrees(last)); + } + return p; +} + +// Helper function to create and configure a Part::Circle primitive from a TopoDS_Edge +Part::Circle* createCirclePrimitive(const TopoDS_Edge& edge, App::Document* doc, const char* name) +{ + auto* p = doc->addObject(name); + if (!p) { + return nullptr; + } + + TopLoc_Location loc; + Standard_Real first, last; + Handle(Geom_Curve) aCurve = BRep_Tool::Curve(edge, loc, first, last); + + if (aCurve->IsInstance(Geom_Circle::get_type_descriptor())) { + Handle(Geom_Circle) circle = Handle(Geom_Circle)::DownCast(aCurve); + p->Radius.setValue(circle->Radius()); + + // The axis contains the full transformation (location and orientation). + gp_Ax2 axis = circle->Position().Transformed(loc.Transformation()); + gp_Pnt center = axis.Location(); + gp_Dir xDir = axis.XDirection(); + gp_Dir yDir = axis.YDirection(); + gp_Dir zDir = axis.Direction(); + + Base::Placement plc; + plc.setPosition(Base::Vector3d(center.X(), center.Y(), center.Z())); + plc.setRotation( + Base::Rotation::makeRotationByAxes(Base::Vector3d(xDir.X(), xDir.Y(), xDir.Z()), + Base::Vector3d(yDir.X(), yDir.Y(), yDir.Z()), + Base::Vector3d(zDir.X(), zDir.Y(), zDir.Z()))); + p->Placement.setValue(plc); + + // Set angles for arcs + BRep_Tool::Range(edge, first, last); + p->Angle1.setValue(Base::toDegrees(first)); + p->Angle2.setValue(Base::toDegrees(last)); + } + return p; +} + +// Helper function to create and configure a Part::Line primitive from a TopoDS_Edge +Part::Line* createLinePrimitive(const TopoDS_Edge& edge, App::Document* doc, const char* name) +{ + auto* p = doc->addObject(name); + if (!p) { + return nullptr; + } + + TopoDS_Vertex v1, v2; + TopExp::Vertices(edge, v1, v2); + gp_Pnt p1 = BRep_Tool::Pnt(v1); + gp_Pnt p2 = BRep_Tool::Pnt(v2); + + p->X1.setValue(p1.X()); + p->Y1.setValue(p1.Y()); + p->Z1.setValue(p1.Z()); + p->X2.setValue(p2.X()); + p->Y2.setValue(p2.Y()); + p->Z2.setValue(p2.Z()); + + return p; +} + +// Helper function to create and configure a Part::Vertex primitive from a TopoDS_Vertex +Part::Vertex* +createVertexPrimitive(const TopoDS_Vertex& vertex, App::Document* doc, const char* name) +{ + auto* p = doc->addObject(name); + if (p) { + gp_Pnt pnt = BRep_Tool::Pnt(vertex); + p->X.setValue(pnt.X()); + p->Y.setValue(pnt.Y()); + p->Z.setValue(pnt.Z()); + } + return p; +} + +// Helper function to create a generic Part::Feature for any non-parametric shape +Part::Feature* +createGenericShapeFeature(const TopoDS_Shape& shape, App::Document* doc, const char* name) +{ + auto* p = doc->addObject(name); + if (p) { + p->Shape.setValue(shape); + } + return p; +} + +} // namespace + +TopoDS_Wire ImpExpDxfRead::BuildWireFromPolyline(std::list& vertices, int flags) +{ + BRepBuilderAPI_MakeWire wireBuilder; + bool is_closed = ((flags & 1) != 0); + if (vertices.empty()) { + return wireBuilder.Wire(); + } + + auto it = vertices.begin(); + auto prev_it = it++; + + while (it != vertices.end()) { + const VertexInfo& start_vertex = *prev_it; + const VertexInfo& end_vertex = *it; + TopoDS_Edge edge; + + if (start_vertex.bulge == 0.0) { + edge = BRepBuilderAPI_MakeEdge(makePoint(start_vertex.location), + makePoint(end_vertex.location)) + .Edge(); + } + else { + double cot = ((1.0 / start_vertex.bulge) - start_vertex.bulge) / 2.0; + double center_x = ((start_vertex.location.x + end_vertex.location.x) + - (end_vertex.location.y - start_vertex.location.y) * cot) + / 2.0; + double center_y = ((start_vertex.location.y + end_vertex.location.y) + + (end_vertex.location.x - start_vertex.location.x) * cot) + / 2.0; + double center_z = (start_vertex.location.z + end_vertex.location.z) / 2.0; + Base::Vector3d center(center_x, center_y, center_z); + + gp_Pnt p0 = makePoint(start_vertex.location); + gp_Pnt p1 = makePoint(end_vertex.location); + gp_Dir up(0, 0, 1); + if (start_vertex.bulge < 0) { + up.Reverse(); + } + gp_Pnt pc = makePoint(center); + gp_Circ circle(gp_Ax2(pc, up), p0.Distance(pc)); + if (circle.Radius() > 1e-9) { + edge = BRepBuilderAPI_MakeEdge(circle, p0, p1).Edge(); + } + } + + if (!edge.IsNull()) { + wireBuilder.Add(edge); + } + prev_it = it++; + } + + if (is_closed && vertices.size() > 1) { + const VertexInfo& start_vertex = vertices.back(); + const VertexInfo& end_vertex = vertices.front(); + TopoDS_Edge edge; + + if (start_vertex.bulge == 0.0) { + edge = BRepBuilderAPI_MakeEdge(makePoint(start_vertex.location), + makePoint(end_vertex.location)) + .Edge(); + } + else { + double cot = ((1.0 / start_vertex.bulge) - start_vertex.bulge) / 2.0; + double center_x = ((start_vertex.location.x + end_vertex.location.x) + - (end_vertex.location.y - start_vertex.location.y) * cot) + / 2.0; + double center_y = ((start_vertex.location.y + end_vertex.location.y) + + (end_vertex.location.x - start_vertex.location.x) * cot) + / 2.0; + double center_z = (start_vertex.location.z + end_vertex.location.z) / 2.0; + Base::Vector3d center(center_x, center_y, center_z); + + gp_Pnt p0 = makePoint(start_vertex.location); + gp_Pnt p1 = makePoint(end_vertex.location); + gp_Dir up(0, 0, 1); + if (start_vertex.bulge < 0) { + up.Reverse(); + } + gp_Pnt pc = makePoint(center); + gp_Circ circle(gp_Ax2(pc, up), p0.Distance(pc)); + if (circle.Radius() > 1e-9) { + edge = BRepBuilderAPI_MakeEdge(circle, p0, p1).Edge(); + } + } + if (!edge.IsNull()) { + wireBuilder.Add(edge); + } + } + + return wireBuilder.Wire(); +} + +Part::Feature* ImpExpDxfRead::createFlattenedPolylineFeature(const TopoDS_Wire& wire, + const char* name) +{ + auto* p = document->addObject(document->getUniqueObjectName(name).c_str()); + if (p) { + p->Shape.setValue(wire); + IncrementCreatedObjectCount(); + } + return p; +} + +Part::Compound* ImpExpDxfRead::createParametricPolylineCompound(const TopoDS_Wire& wire, + const char* name) +{ + auto* p = document->addObject(document->getUniqueObjectName(name).c_str()); + IncrementCreatedObjectCount(); + + std::vector segments; + TopExp_Explorer explorer(wire, TopAbs_EDGE); + + for (; explorer.More(); explorer.Next()) { + TopoDS_Edge edge = TopoDS::Edge(explorer.Current()); + App::DocumentObject* segment = nullptr; + BRepAdaptor_Curve adaptor(edge); + + if (adaptor.GetType() == GeomAbs_Line) { + segment = createLinePrimitive(edge, document, "Segment"); + } + else if (adaptor.GetType() == GeomAbs_Circle) { + segment = createCirclePrimitive(edge, document, "Arc"); + } + + if (segment) { + IncrementCreatedObjectCount(); + segment->Visibility.setValue(false); + // We apply styles later, depending on the context + segments.push_back(segment); + } + } + p->Links.setValues(segments); + return p; +} + +void ImpExpDxfRead::CreateFlattenedPolyline(const TopoDS_Wire& wire, const char* name) +{ + Part::Feature* p = createFlattenedPolylineFeature(wire, name); + + // Perform the context-specific action of adding it to the collector + if (p) { + Collector->AddObject(p, name); + } +} + +void ImpExpDxfRead::CreateParametricPolyline(const TopoDS_Wire& wire, const char* name) +{ + Part::Compound* p = createParametricPolylineCompound(wire, name); + + // Perform the context-specific actions (applying styles and adding to the document) + if (p) { + // Style the child segments + for (App::DocumentObject* segment : p->Links.getValues()) { + ApplyGuiStyles(static_cast(segment)); + } + // Add the final compound object to the document + Collector->AddObject(p, name); + } +} + +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 @@ -111,7 +462,7 @@ bool ImpExpDxfRead::ReadEntitiesSection() ComposeBlocks(); DrawingEntityCollector collector(*this); - if (m_mergeOption < SingleShapes) { + if (m_importMode == ImportMode::FusedShapes) { std::map> ShapesToCombine; { ShapeSavingEntityCollector savingCollector(*this, ShapesToCombine); @@ -186,31 +537,18 @@ void ImpExpDxfRead::setOptions() m_preserveColors = hGrp->GetBool("dxfGetOriginalColors", true); m_stats.importSettings["Use colors from the DXF file"] = m_preserveColors ? "Yes" : "No"; - // Default for creation type is to create draft objects. - // The radio-button structure of the options dialog should generally prevent this condition. - m_mergeOption = DraftObjects; - m_stats.importSettings["Merge option"] = "Create Draft objects"; // Default - if (hGrp->GetBool("groupLayers", true)) { - // Group all compatible objects together - m_mergeOption = MergeShapes; - m_stats.importSettings["Merge option"] = "Group layers into blocks"; - } - else if (hGrp->GetBool("dxfCreatePart", true)) { - // Create (non-draft) Shape objects when possible - m_mergeOption = SingleShapes; - m_stats.importSettings["Merge option"] = "Create Part shapes"; - } - else if (hGrp->GetBool("dxfCreateDraft", true)) { - // Create only Draft objects, making the result closest to drawn-from-scratch - m_mergeOption = DraftObjects; - m_stats.importSettings["Merge option"] = "Create Draft objects"; - } + // Read the new master import mode parameter, set the default. + int mode = hGrp->GetInt("DxfImportMode", static_cast(ImportMode::IndividualShapes)); + m_importMode = static_cast(mode); + // TODO: joingeometry should give an intermediate between MergeShapes and SingleShapes which // will merge shapes that happen to join end-to-end. As such it should be in the radio button // set, except that the legacy importer can do joining either for sketches or for shapes. What // this really means is there should be an "Import as sketch" checkbox, and only the // MergeShapes, JoinShapes, and SingleShapes radio buttons should be allowed, i.e. Draft Objects // would be ignored. + // Update: The "Join geometry" option is now a checkbox that is only enabled for the legacy + // importer. Whether the modern importer should support this is still up for debate. bool joinGeometry = hGrp->GetBool("joingeometry", false); m_stats.importSettings["Join geometry"] = joinGeometry ? "Yes" : "No"; @@ -256,8 +594,10 @@ void ImpExpDxfRead::ComposeFlattenedBlock(const std::string& blockName, std::list shapeCollection; // 4. Process primitive geometry. - for (const auto& [attributes, shapeList] : blockData.Shapes) { - shapeCollection.insert(shapeCollection.end(), shapeList.begin(), shapeList.end()); + for (const auto& [attributes, builderList] : blockData.GeometryBuilders) { + for (const auto& builder : builderList) { + shapeCollection.push_back(builder.shape); + } } // 5. Process nested inserts recursively. @@ -323,8 +663,7 @@ void ImpExpDxfRead::ComposeParametricBlock(const std::string& blockName, const Block& blockData = it->second; // 3. Create the master Part::Compound for this block definition. - std::string compName = "BLOCK_"; - compName += blockName; + std::string compName = "BLOCK_" + blockName; auto blockCompound = document->addObject( document->getUniqueObjectName(compName.c_str()).c_str()); m_blockDefinitionGroup->addObject(blockCompound); @@ -332,7 +671,7 @@ void ImpExpDxfRead::ComposeParametricBlock(const std::string& blockName, blockCompound->Visibility.setValue(false); this->m_blockDefinitions[blockName] = blockCompound; - std::vector linkedObjects; + std::vector childObjects; // 4. Recursively Compose and Link Nested Inserts. for (const auto& insertAttrPair : blockData.Inserts) { @@ -359,96 +698,92 @@ void ImpExpDxfRead::ComposeParametricBlock(const std::string& blockName, link->ScaleVector.setValue(nestedInsert.Scale); link->Visibility.setValue(false); IncrementCreatedObjectCount(); - linkedObjects.push_back(link); + childObjects.push_back(link); } } } - // 5. Create and Link Primitive Geometry. - // Iterate through each attribute group (e.g., each layer within the block). - for (const auto& [attributes, shapeList] : blockData.Shapes) { - // Then, iterate through each shape in that group and create a separate feature for it. - for (const auto& shape : shapeList) { - if (!shape.IsNull()) { - std::string cleanBlockLabel = blockName; - if (!cleanBlockLabel.empty() && std::isdigit(cleanBlockLabel[0])) { - // Workaround for FreeCAD's unique name generator, which prepends an underscore - // to names that start with a digit. We add our own prefix. - cleanBlockLabel.insert(0, "_"); + // 5. Create and Link Primitive Geometry from the collected builders. + for (const auto& [attributes, builderList] : blockData.GeometryBuilders) { + this->m_entityAttributes = attributes; // Set attributes for layer/color handling + + for (const auto& builder : builderList) { + App::DocumentObject* newObject = nullptr; + switch (builder.type) { + // Existing cases for other primitives + case GeometryBuilder::PrimitiveType::Line: { + newObject = createLinePrimitive(TopoDS::Edge(builder.shape), document, "Line"); + break; } - else if (!cleanBlockLabel.empty() && std::isdigit(cleanBlockLabel.back())) { - // Add a trailing underscore to prevent the unique name generator - // from incrementing the number in the block's name. - cleanBlockLabel += "_"; + case GeometryBuilder::PrimitiveType::Point: { + newObject = + createVertexPrimitive(TopoDS::Vertex(builder.shape), document, "Point"); + break; } - // Determine a more descriptive name for the primitive feature. - std::string type_suffix = "Shape"; - if (shape.ShapeType() == TopAbs_EDGE) { - BRepAdaptor_Curve adaptor(TopoDS::Edge(shape)); - switch (adaptor.GetType()) { - case GeomAbs_Line: - type_suffix = "Line"; - break; - case GeomAbs_Circle: - type_suffix = "Circle"; - break; - case GeomAbs_Ellipse: - type_suffix = "Ellipse"; - break; - case GeomAbs_BSplineCurve: - type_suffix = "BSpline"; - break; - case GeomAbs_BezierCurve: - type_suffix = "Bezier"; - break; - default: - type_suffix = "Edge"; - break; + case GeometryBuilder::PrimitiveType::Circle: + case GeometryBuilder::PrimitiveType::Arc: { + const char* name = + (builder.type == GeometryBuilder::PrimitiveType::Circle) ? "Circle" : "Arc"; + auto* p = createCirclePrimitive(TopoDS::Edge(builder.shape), document, name); + if (!p) { + break; } + if (builder.type == GeometryBuilder::PrimitiveType::Circle) { + p->Angle1.setValue(0.0); + p->Angle2.setValue(360.0); + } + newObject = p; + break; } - else if (shape.ShapeType() == TopAbs_VERTEX) { - type_suffix = "Vertex"; + case GeometryBuilder::PrimitiveType::Ellipse: { + newObject = + createEllipsePrimitive(TopoDS::Edge(builder.shape), document, "Ellipse"); + break; } - else if (shape.ShapeType() == TopAbs_WIRE) { - type_suffix = "Wire"; + case GeometryBuilder::PrimitiveType::Spline: { + // Splines are generic Part::Feature as no Part primitive exists + auto* p = document->addObject("Spline"); + p->Shape.setValue(builder.shape); + newObject = p; + break; } - else if (shape.ShapeType() == TopAbs_FACE) { - type_suffix = "Face"; + case GeometryBuilder::PrimitiveType::PolylineFlattened: { + // This creates a simple Part::Feature wrapping the wire, which is standard for + // block children. + newObject = + createFlattenedPolylineFeature(TopoDS::Wire(builder.shape), "Polyline"); + break; } - else if (shape.ShapeType() == TopAbs_SHELL) { - type_suffix = "Shell"; + case GeometryBuilder::PrimitiveType::PolylineParametric: { + // This creates a Part::Compound containing line/arc segments. + newObject = + createParametricPolylineCompound(TopoDS::Wire(builder.shape), "Polyline"); + // No styling needed here, as the block's instance will control appearance. + break; } - else if (shape.ShapeType() == TopAbs_SOLID) { - type_suffix = "Solid"; - } - else if (shape.ShapeType() == TopAbs_COMPOUND) { - type_suffix = "Compound"; + case GeometryBuilder::PrimitiveType::None: // Default/fallback if not handled + default: { + // Generic shape, e.g., 3DFACE + newObject = createGenericShapeFeature(builder.shape, document, "Shape"); + break; } + } - std::string primitive_base_label = cleanBlockLabel + "_" + type_suffix; - // Use getStandardObjectLabel to get a unique user-facing label (e.g., - // "block01_Line001") while keeping the internal object name clean. - auto geomFeature = document->addObject( - document->getStandardObjectLabel(primitive_base_label.c_str(), 3).c_str()); - + if (newObject) { IncrementCreatedObjectCount(); - geomFeature->Shape.setValue(shape); - geomFeature->Visibility.setValue(false); - - // Apply styling to this primitive feature using its original attributes. - this->m_entityAttributes = attributes; - this->ApplyGuiStyles(geomFeature); - - linkedObjects.push_back(geomFeature); + newObject->Visibility.setValue(false); // Children of blocks are hidden by default + // Layer and color are applied by the block itself (Part::Compound) or its children + // if overridden. + ApplyGuiStyles( + static_cast(newObject)); // Apply style to the child object + childObjects.push_back(newObject); // Add to the block's main children list } } } - // TODO: Add similar logic for blockData.FeatureBuildersList if needed. - // 6. Finalize the Part::Compound. - if (!linkedObjects.empty()) { - blockCompound->Links.setValues(linkedObjects); + if (!childObjects.empty()) { + blockCompound->Links.setValues(childObjects); } // 7. Mark this block as composed. @@ -459,7 +794,7 @@ void ImpExpDxfRead::ComposeBlocks() { std::set composedBlocks; - if (m_mergeOption == MergeShapes) { + if (m_importMode == ImportMode::FusedShapes) { // User wants flattened geometry for performance. for (const auto& pair : this->Blocks) { if (composedBlocks.find(pair.first) == composedBlocks.end()) { @@ -584,8 +919,7 @@ bool ImpExpDxfRead::OnReadBlock(const std::string& name, int flags) // The .emplace method is slightly more efficient here. auto& temporaryBlock = Blocks.emplace(std::make_pair(name, Block(name, flags))).first->second; BlockDefinitionCollector blockCollector(*this, - temporaryBlock.Shapes, - temporaryBlock.FeatureBuildersList, + temporaryBlock.GeometryBuilders, temporaryBlock.Inserts); if (!ReadBlockContents()) { return false; // Abort on parsing error @@ -606,11 +940,28 @@ void ImpExpDxfRead::OnReadLine(const Base::Vector3d& start, gp_Pnt p0 = makePoint(start); gp_Pnt p1 = makePoint(end); - if (p0.IsEqual(p1, 0.00000001)) { - // TODO: Really?? What about the people designing integrated circuits? + if (p0.IsEqual(p1, 1e-8)) { return; } - Collector->AddObject(BRepBuilderAPI_MakeEdge(p0, p1).Edge(), "Line"); + TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(p0, p1).Edge(); + GeometryBuilder builder(edge); + + // CORRECTED: Set PrimitiveType conditionally based on m_importMode + switch (m_importMode) { + case ImportMode::EditableDraft: + case ImportMode::EditablePrimitives: + // For these modes, we want a specific Part primitive (Part::Line) + builder.type = GeometryBuilder::PrimitiveType::Line; + break; + case ImportMode::IndividualShapes: + case ImportMode::FusedShapes: + // For these modes, we want a generic Part::Feature wrapping the TopoDS_Shape. + // PrimitiveType::None will lead to a generic Part::Feature in AddGeometry. + builder.type = GeometryBuilder::PrimitiveType::None; + break; + } + + Collector->AddGeometry(builder); } @@ -619,8 +970,20 @@ void ImpExpDxfRead::OnReadPoint(const Base::Vector3d& start) if (shouldSkipEntity()) { return; } + TopoDS_Vertex vertex = BRepBuilderAPI_MakeVertex(makePoint(start)).Vertex(); + GeometryBuilder builder(vertex); - Collector->AddObject(BRepBuilderAPI_MakeVertex(makePoint(start)).Vertex(), "Point"); + switch (m_importMode) { + case ImportMode::EditableDraft: + case ImportMode::EditablePrimitives: + builder.type = GeometryBuilder::PrimitiveType::Point; + break; + case ImportMode::IndividualShapes: + case ImportMode::FusedShapes: + builder.type = GeometryBuilder::PrimitiveType::None; // Generic Part::Feature + break; + } + Collector->AddGeometry(builder); } @@ -638,16 +1001,29 @@ void ImpExpDxfRead::OnReadArc(const Base::Vector3d& start, gp_Pnt p1 = makePoint(end); gp_Dir up(0, 0, 1); if (!dir) { - up = -up; + up.Reverse(); } gp_Pnt pc = makePoint(center); gp_Circ circle(gp_Ax2(pc, up), p0.Distance(pc)); - if (circle.Radius() > 0) { - Collector->AddObject(BRepBuilderAPI_MakeEdge(circle, p0, p1).Edge(), "Arc"); - } - else { + if (circle.Radius() < 1e-9) { Base::Console().warning("ImpExpDxf - ignore degenerate arc of circle\n"); + return; } + + TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(circle, p0, p1).Edge(); + GeometryBuilder builder(edge); // Instantiate builder once + + switch (m_importMode) { + case ImportMode::EditableDraft: + case ImportMode::EditablePrimitives: + builder.type = GeometryBuilder::PrimitiveType::Arc; + break; + case ImportMode::IndividualShapes: + case ImportMode::FusedShapes: + builder.type = GeometryBuilder::PrimitiveType::None; // Generic Part::Feature + break; + } + Collector->AddGeometry(builder); } @@ -663,16 +1039,29 @@ void ImpExpDxfRead::OnReadCircle(const Base::Vector3d& start, gp_Pnt p0 = makePoint(start); gp_Dir up(0, 0, 1); if (!dir) { - up = -up; + up.Reverse(); } gp_Pnt pc = makePoint(center); gp_Circ circle(gp_Ax2(pc, up), p0.Distance(pc)); - if (circle.Radius() > 0) { - Collector->AddObject(BRepBuilderAPI_MakeEdge(circle).Edge(), "Circle"); - } - else { + if (circle.Radius() < 1e-9) { Base::Console().warning("ImpExpDxf - ignore degenerate circle\n"); + return; } + + TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(circle).Edge(); + GeometryBuilder builder(edge); // Instantiate builder once + + switch (m_importMode) { + case ImportMode::EditableDraft: + case ImportMode::EditablePrimitives: + builder.type = GeometryBuilder::PrimitiveType::Circle; + break; + case ImportMode::IndividualShapes: + case ImportMode::FusedShapes: + builder.type = GeometryBuilder::PrimitiveType::None; // Generic Part::Feature + break; + } + Collector->AddGeometry(builder); } @@ -785,11 +1174,22 @@ void ImpExpDxfRead::OnReadSpline(struct SplineData& sd) geom = getInterpolationSpline(sd); } - if (geom.IsNull()) { - throw Standard_Failure(); - } + if (!geom.IsNull()) { + TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(geom).Edge(); + GeometryBuilder builder(edge); // Instantiate builder once - Collector->AddObject(BRepBuilderAPI_MakeEdge(geom).Edge(), "Spline"); + switch (m_importMode) { + case ImportMode::EditableDraft: + case ImportMode::EditablePrimitives: + builder.type = GeometryBuilder::PrimitiveType::Spline; + break; + case ImportMode::IndividualShapes: + case ImportMode::FusedShapes: + builder.type = GeometryBuilder::PrimitiveType::None; // Generic Part::Feature + break; + } + Collector->AddGeometry(builder); + } } catch (const Standard_Failure&) { Base::Console().warning("ImpExpDxf - failed to create bspline\n"); @@ -812,60 +1212,71 @@ void ImpExpDxfRead::OnReadEllipse(const Base::Vector3d& center, gp_Dir up(0, 0, 1); if (!dir) { - up = -up; + up.Reverse(); } gp_Pnt pc = makePoint(center); gp_Elips ellipse(gp_Ax2(pc, up), major_radius, minor_radius); ellipse.Rotate(gp_Ax1(pc, up), rotation); - if (ellipse.MinorRadius() > 0) { - Collector->AddObject(BRepBuilderAPI_MakeEdge(ellipse).Edge(), "Ellipse"); - } - else { + if (ellipse.MinorRadius() < 1e-9) { Base::Console().warning("ImpExpDxf - ignore degenerate ellipse\n"); + return; } -} + TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(ellipse).Edge(); + GeometryBuilder builder(edge); // Pass the shape to the builder + + switch (m_importMode) { + case ImportMode::EditableDraft: + case ImportMode::EditablePrimitives: + // Tag this geometry so the collector knows to create a Part::Ellipse primitive + builder.type = GeometryBuilder::PrimitiveType::Ellipse; + break; + case ImportMode::IndividualShapes: + case ImportMode::FusedShapes: + default: + // For other modes, create a generic shape (Part:Feature), which is the existing + // behavior. + builder.type = GeometryBuilder::PrimitiveType::None; + break; + } + Collector->AddGeometry(builder); +} void ImpExpDxfRead::OnReadText(const Base::Vector3d& point, const double height, const std::string& text, const double rotation) { - if (shouldSkipEntity()) { + if (shouldSkipEntity() || !m_importAnnotations) { return; } - // Note that our parameters do not contain all the information needed to properly orient the - // text. As a result the text will always appear on the XY plane - if (m_importAnnotations) { - auto makeText = [this, rotation, point, text, height]( - const Base::Matrix4D& transform) -> App::FeaturePython* { - PyObject* draftModule = getDraftModule(); - if (draftModule != nullptr) { - Base::Matrix4D localTransform; - localTransform.rotZ(Base::toRadians(rotation)); - localTransform.move(point); - PyObject* placement = - new Base::PlacementPy(Base::Placement(transform * localTransform)); - // returns a wrapped App::FeaturePython - auto builtText = dynamic_cast*>( - // NOLINTNEXTLINE(readability/nolint) - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast) - (Base::PyObjectBase*)PyObject_CallMethod(draftModule, - "make_text", - "sOif", - text.c_str(), - placement, - 0, - height)); - Py_DECREF(placement); - if (builtText != nullptr) { - return dynamic_cast(builtText->getDocumentObjectPtr()); - } - } - return nullptr; - }; - Collector->AddObject((FeaturePythonBuilder)makeText); + auto* p = static_cast(document->addObject("App::FeaturePython", "Text")); + if (p) { + p->addDynamicProperty("App::PropertyString", + "DxfEntityType", + "Internal", + "DXF entity type"); + static_cast(p->getPropertyByName("DxfEntityType"))->setValue("TEXT"); + + p->addDynamicProperty("App::PropertyStringList", "Text", "Data", "Text content"); + // Explicitly create the vector to resolve ambiguity + std::vector text_values = {text}; + static_cast(p->getPropertyByName("Text"))->setValues(text_values); + + p->addDynamicProperty("App::PropertyFloat", + "DxfTextHeight", + "Internal", + "Original text height"); + static_cast(p->getPropertyByName("DxfTextHeight"))->setValue(height); + + p->addDynamicProperty("App::PropertyPlacement", "Placement", "Base", "Object placement"); + Base::Placement pl; + pl.setPosition(point); + pl.setRotation(Base::Rotation(Base::Vector3d(0, 0, 1), Base::toRadians(rotation))); + static_cast(p->getPropertyByName("Placement"))->setValue(pl); + + Collector->AddObject(p, "Text"); } } @@ -889,83 +1300,148 @@ void ImpExpDxfRead::OnReadInsert(const Base::Vector3d& point, void ImpExpDxfRead::OnReadDimension(const Base::Vector3d& start, const Base::Vector3d& end, const Base::Vector3d& point, - double /*rotation*/) + int dimensionType, + double rotation) { - if (shouldSkipEntity()) { + if (shouldSkipEntity() || !m_importAnnotations) { return; } - if (m_importAnnotations) { - auto makeDimension = - [this, start, end, point](const Base::Matrix4D& transform) -> App::FeaturePython* { - PyObject* draftModule = getDraftModule(); - if (draftModule != nullptr) { - // TODO: Capture and apply OCSOrientationTransform to OCS coordinates - // Note, some of the locations in the DXF are OCS and some are UCS, but UCS doesn't - // mean UCS when in a block expansion, it means 'transform' - // So we want transform*vector for "UCS" coordinates and transform*ocdCapture*vector - // for "OCS" coordinates - // - // We implement the transform by mapping all the points from OCS to UCS - // TODO: Set the Normal property to transform*(0,0,1,0) - // TODO: Set the Direction property to transform*(the desired direction). - // By default this is parallel to (start-end). - PyObject* startPy = new Base::VectorPy(transform * start); - PyObject* endPy = new Base::VectorPy(transform * end); - PyObject* lineLocationPy = new Base::VectorPy(transform * point); - // returns a wrapped App::FeaturePython - auto builtDim = dynamic_cast*>( - // NOLINTNEXTLINE(readability/nolint) - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast) - (Base::PyObjectBase*)PyObject_CallMethod(draftModule, - "make_linear_dimension", - "OOO", - startPy, - endPy, - lineLocationPy)); - Py_DECREF(startPy); - Py_DECREF(endPy); - Py_DECREF(lineLocationPy); - if (builtDim != nullptr) { - return dynamic_cast(builtDim->getDocumentObjectPtr()); - } - } - return nullptr; - }; - Collector->AddObject((FeaturePythonBuilder)makeDimension); + auto* p = + static_cast(document->addObject("App::FeaturePython", "Dimension")); + if (p) { + p->addDynamicProperty("App::PropertyString", + "DxfEntityType", + "Internal", + "DXF entity type"); + static_cast(p->getPropertyByName("DxfEntityType")) + ->setValue("DIMENSION"); + + p->addDynamicProperty("App::PropertyVector", "Start", "Data", "Start point of dimension"); + static_cast(p->getPropertyByName("Start"))->setValue(start); + + p->addDynamicProperty("App::PropertyVector", "End", "Data", "End point of dimension"); + static_cast(p->getPropertyByName("End"))->setValue(end); + + p->addDynamicProperty("App::PropertyVector", "Dimline", "Data", "Point on dimension line"); + static_cast(p->getPropertyByName("Dimline"))->setValue(point); + + p->addDynamicProperty("App::PropertyInteger", + "DxfDimensionType", + "Internal", + "Original dimension type flag"); + static_cast(p->getPropertyByName("DxfDimensionType")) + ->setValue(dimensionType); + + p->addDynamicProperty("App::PropertyAngle", + "DxfRotation", + "Internal", + "Original dimension rotation"); + // rotation is already in radians from the caller + static_cast(p->getPropertyByName("DxfRotation"))->setValue(rotation); + + p->addDynamicProperty("App::PropertyPlacement", "Placement", "Base", "Object placement"); + Base::Placement pl; + // Correctly construct the rotation directly from the 4x4 matrix. + // The Base::Rotation constructor will extract the rotational part. + pl.setRotation(Base::Rotation(OCSOrientationTransform)); + static_cast(p->getPropertyByName("Placement"))->setValue(pl); + + Collector->AddObject(p, "Dimension"); } } + void ImpExpDxfRead::OnReadPolyline(std::list& vertices, int flags) { if (shouldSkipEntity()) { return; } - std::map> ShapesToCombine; - { - // TODO: Currently ExpandPolyline calls OnReadArc etc to generate the pieces, and these - // create TopoShape objects which ShapeSavingEntityCollector can gather up. - // Eventually when m_mergeOption being DraftObjects is implemented OnReadArc etc might - // generate Draft objects which ShapeSavingEntityCollector does not save. - // We need either a collector that collects everything (and we have to figure out - // how to join Draft objects) or we need to temporarily set m_mergeOption to SingleShapes - // if it is set to DraftObjects (and safely restore it on exceptions) - // A clean way would be to give the collector a "makeDraftObjects" property, - // and our special collector could give this the value 'false' whereas the main - // collector would base this on the option setting. - // Also ShapeSavingEntityCollector classifies by entityAttributes which is not needed here - // because they are constant throughout. - ShapeSavingEntityCollector savingCollector(*this, ShapesToCombine); - ExplodePolyline(vertices, flags); + if (vertices.size() < 2 && (flags & 1) == 0) { + return; // Not enough vertices for an open polyline } - // Join the shapes. - if (!ShapesToCombine.empty()) { - // TODO: If we want Draft objects and all segments are straight lines we can make a draft - // wire. - CombineShapes(ShapesToCombine.begin()->second, "Polyline"); + + TopoDS_Wire wire = BuildWireFromPolyline(vertices, flags); + if (wire.IsNull()) { + return; + } + + if (m_importMode == ImportMode::EditableDraft) { + GeometryBuilder builder(wire); + builder.type = GeometryBuilder::PrimitiveType::PolylineFlattened; + Collector->AddGeometry(builder); + } + else if (m_importMode == ImportMode::EditablePrimitives) { + GeometryBuilder builder(wire); + builder.type = GeometryBuilder::PrimitiveType::PolylineParametric; + Collector->AddGeometry(builder); + } + else { + Collector->AddObject(wire, "Polyline"); } } +void ImpExpDxfRead::DrawingEntityCollector::AddGeometry(const GeometryBuilder& builder) +{ + App::DocumentObject* newDocObj = nullptr; + + switch (builder.type) { + case GeometryBuilder::PrimitiveType::Line: { + newDocObj = createLinePrimitive(TopoDS::Edge(builder.shape), Reader.document, "Line"); + break; + } + case GeometryBuilder::PrimitiveType::Circle: { + auto* p = createCirclePrimitive(TopoDS::Edge(builder.shape), Reader.document, "Circle"); + if (p) { + p->Angle1.setValue(0.0); + p->Angle2.setValue(360.0); // Ensure it's a full circle if it's a circle entity + } + newDocObj = p; + break; + } + case GeometryBuilder::PrimitiveType::Arc: { + newDocObj = createCirclePrimitive(TopoDS::Edge(builder.shape), Reader.document, "Arc"); + break; + } + case GeometryBuilder::PrimitiveType::Point: { + newDocObj = + createVertexPrimitive(TopoDS::Vertex(builder.shape), Reader.document, "Point"); + break; + } + case GeometryBuilder::PrimitiveType::Ellipse: { + newDocObj = + createEllipsePrimitive(TopoDS::Edge(builder.shape), Reader.document, "Ellipse"); + break; + } + case GeometryBuilder::PrimitiveType::Spline: { + newDocObj = createGenericShapeFeature(builder.shape, Reader.document, "Spline"); + break; + } + case GeometryBuilder::PrimitiveType::PolylineFlattened: { + Reader.CreateFlattenedPolyline(TopoDS::Wire(builder.shape), "Polyline"); + newDocObj = nullptr; // Object handled by helper + break; + } + case GeometryBuilder::PrimitiveType::PolylineParametric: { + Reader.CreateParametricPolyline(TopoDS::Wire(builder.shape), "Polyline"); + newDocObj = nullptr; // Object handled by helper + break; + } + case GeometryBuilder::PrimitiveType::None: // Fallback for generic shapes (e.g., 3DFACE) + default: { + newDocObj = createGenericShapeFeature(builder.shape, Reader.document, "Shape"); + break; + } + } + + // Common post-creation steps for objects NOT handled by helper functions + if (newDocObj) { + Reader.IncrementCreatedObjectCount(); + Reader._addOriginalLayerProperty(newDocObj); + Reader.MoveToLayer(newDocObj); + Reader.ApplyGuiStyles(static_cast(newDocObj)); + } +} ImpExpDxfRead::Layer::Layer(const std::string& name, ColorIndex_t color, @@ -1013,12 +1489,12 @@ ImpExpDxfRead::MakeLayer(const std::string& name, ColorIndex_t color, std::strin PyObject* layer = nullptr; draftModule = getDraftModule(); if (draftModule != nullptr) { - // After the colours, I also want to pass the draw_style, but there is an intervening - // line-width parameter. It is easier to just pass that parameter's default value than - // to do the handstands to pass a named parameter. + // After the colours, I also want to pass the draw_style, but there is an + // intervening line-width parameter. It is easier to just pass that parameter's + // default value than to do the handstands to pass a named parameter. // TODO: Pass the appropriate draw_style (from "Solid" "Dashed" "Dotted" "DashDot") - // This needs an ObjectDrawStyleName analogous to ObjectColor but at the ImpExpDxfGui - // level. + // This needs an ObjectDrawStyleName analogous to ObjectColor but at the + // ImpExpDxfGui level. layer = // NOLINTNEXTLINE(readability/nolint) // NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast) @@ -1037,15 +1513,19 @@ ImpExpDxfRead::MakeLayer(const std::string& name, ColorIndex_t color, std::strin } auto result = new Layer(name, color, std::move(lineType), layer); if (result->DraftLayerView != Py_None) { - PyObject_SetAttrString(result->DraftLayerView, "OverrideLineColorChildren", Py_False); + // Get the correct boolean value based on the user's preference. + PyObject* overrideValue = m_preserveColors ? Py_True : Py_False; + PyObject_SetAttrString(result->DraftLayerView, + "OverrideLineColorChildren", + overrideValue); PyObject_SetAttrString(result->DraftLayerView, "OverrideShapeAppearanceChildren", - Py_False); + overrideValue); } - // We make our own layer class even if we could not make a layer. MoveToLayer will ignore - // such layers but we have to do this because it is not a polymorphic type so we can't tell - // what we pull out of m_entityAttributes.m_Layer. + // We make our own layer class even if we could not make a layer. MoveToLayer will + // ignore such layers but we have to do this because it is not a polymorphic type so we + // can't tell what we pull out of m_entityAttributes.m_Layer. return result; } return CDxfRead::MakeLayer(name, color, std::move(lineType)); @@ -1055,8 +1535,8 @@ void ImpExpDxfRead::MoveToLayer(App::DocumentObject* object) const if (m_preserveLayers) { static_cast(m_entityAttributes.m_Layer)->Contents.push_back(object); } - // TODO: else Hide the object if it is in a Hidden layer? That won't work because we've cleared - // out m_entityAttributes.m_Layer + // TODO: else Hide the object if it is in a Hidden layer? That won't work because we've + // cleared out m_entityAttributes.m_Layer } @@ -1097,24 +1577,41 @@ std::string ImpExpDxfRead::Deformat(const char* text) return ss.str(); } +void ImpExpDxfRead::_addOriginalLayerProperty(App::DocumentObject* obj) +{ + if (obj && m_entityAttributes.m_Layer) { + obj->addDynamicProperty("App::PropertyString", + "OriginalLayer", + "Internal", + "Layer name from the original DXF file.", + App::Property::Hidden); + static_cast(obj->getPropertyByName("OriginalLayer")) + ->setValue(m_entityAttributes.m_Layer->Name.c_str()); + } +} + void ImpExpDxfRead::DrawingEntityCollector::AddObject(const TopoDS_Shape& shape, const char* nameBase) { - Reader.IncrementCreatedObjectCount(); auto pcFeature = Reader.document->addObject(nameBase); - pcFeature->Shape.setValue(shape); - Reader.MoveToLayer(pcFeature); - Reader.ApplyGuiStyles(pcFeature); + + if (pcFeature) { + Reader.IncrementCreatedObjectCount(); + pcFeature->Shape.setValue(shape); + Reader._addOriginalLayerProperty(pcFeature); + Reader.MoveToLayer(pcFeature); + Reader.ApplyGuiStyles(pcFeature); + } } void ImpExpDxfRead::DrawingEntityCollector::AddObject(App::DocumentObject* obj, const char* /*nameBase*/) { - // This overload is for C++ created objects like App::Link - // The object is already in the document, so we just need to style it and move it to a layer. Reader.MoveToLayer(obj); + Reader._addOriginalLayerProperty(obj); - // Safely apply styles by checking the object's actual type + // Safely apply styles by checking the object's actual type (only for objects not replaced + // by Python) if (auto feature = dynamic_cast(obj)) { Reader.ApplyGuiStyles(feature); } @@ -1131,8 +1628,7 @@ void ImpExpDxfRead::DrawingEntityCollector::AddObject(FeaturePythonBuilder shape Reader.IncrementCreatedObjectCount(); App::FeaturePython* shape = shapeBuilder(Reader.OCSOrientationTransform); if (shape != nullptr) { - Reader.MoveToLayer(shape); - Reader.ApplyGuiStyles(shape); + Reader._addOriginalLayerProperty(shape); } } diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.h b/src/Mod/Import/App/dxf/ImpExpDxf.h index 21b533d311..9054b253a7 100644 --- a/src/Mod/Import/App/dxf/ImpExpDxf.h +++ b/src/Mod/Import/App/dxf/ImpExpDxf.h @@ -40,6 +40,15 @@ class BRepAdaptor_Curve; namespace Import { + +enum class ImportMode +{ + EditableDraft, + EditablePrimitives, + IndividualShapes, + FusedShapes +}; + class ImportExport ImpExpDxfRead: public CDxfRead { public: @@ -52,7 +61,7 @@ public: { Py_XDECREF(DraftModule); } - + static std::map PreScan(const std::string& filepath); void StartImport() override; Py::Object getStatsAsPyObject(); @@ -91,6 +100,7 @@ public: void OnReadDimension(const Base::Vector3d& start, const Base::Vector3d& end, const Base::Vector3d& point, + int dimensionType, double rotation) override; void OnReadPolyline(std::list& /*vertices*/, int flags) override; @@ -108,6 +118,31 @@ public: void FinishImport() override; private: + class GeometryBuilder + { + public: + // The type of primitive that a shape represents. 'None' is used for + // non-parametric modes. + enum class PrimitiveType + { + None, + Point, + Line, + Circle, + Arc, + Ellipse, + Spline, + PolylineFlattened, // Polyline imported as a simple Part::Feature with a TopoDS_Wire + PolylineParametric // Polyline imported as a Part::Compound of Part primitives + }; + + // The raw geometric shape. + TopoDS_Shape shape; + // The intended parametric type for the shape. + PrimitiveType type = PrimitiveType::None; + }; + + ImportMode m_importMode = ImportMode::IndividualShapes; bool shouldSkipEntity() const { // This entity is in paper space, and the user setting says to ignore it. @@ -127,6 +162,8 @@ private: void ComposeBlocks(); void ComposeParametricBlock(const std::string& blockName, std::set& composed); void ComposeFlattenedBlock(const std::string& blockName, std::set& composed); + Part::Compound* createParametricPolylineCompound(const TopoDS_Wire& wire, const char* name); + Part::Feature* createFlattenedPolylineFeature(const TopoDS_Wire& wire, const char* name); protected: PyObject* getDraftModule() @@ -143,6 +180,10 @@ protected: CDxfRead::Layer* MakeLayer(const std::string& name, ColorIndex_t color, std::string&& lineType) override; + TopoDS_Wire BuildWireFromPolyline(std::list& vertices, int flags); + void CreateFlattenedPolyline(const TopoDS_Wire& wire, const char* name); + void CreateParametricPolyline(const TopoDS_Wire& wire, const char* name); + // Overrides for layer management so we can record the layer objects in the FreeCAD drawing that // are associated with the layers in the DXF. class Layer: public CDxfRead::Layer @@ -197,9 +238,7 @@ protected: {} const std::string Name; const int Flags; - std::map> Shapes; - std::map> - FeatureBuildersList; + std::map> GeometryBuilders; std::map> Inserts; }; @@ -211,6 +250,7 @@ private: App::DocumentObjectGroup* m_unreferencedBlocksGroup = nullptr; App::Document* document; std::string m_optionSource; + void _addOriginalLayerProperty(App::DocumentObject* obj); protected: friend class DrawingEntityCollector; @@ -249,6 +289,8 @@ protected: // Called by OnReadXxxx functions to add Part objects virtual void AddObject(const TopoDS_Shape& shape, const char* nameBase) = 0; + // Generic method to add a new geometry builder + virtual void AddGeometry(const GeometryBuilder& builder) = 0; // Called by OnReadInsert to add App::Link or other C++-created objects virtual void AddObject(App::DocumentObject* obj, const char* nameBase) = 0; // Called by OnReadXxxx functions to add FeaturePython (draft) objects. @@ -278,6 +320,7 @@ protected: {} void AddObject(const TopoDS_Shape& shape, const char* nameBase) override; + void AddGeometry(const GeometryBuilder& builder) override; void AddObject(App::DocumentObject* obj, const char* nameBase) override; void AddObject(FeaturePythonBuilder shapeBuilder) override; void AddInsert(const Base::Vector3d& point, @@ -341,6 +384,11 @@ protected: ShapesList[Reader.m_entityAttributes].push_back(shape); } + void AddGeometry(const GeometryBuilder& builder) override + { + ShapesList[Reader.m_entityAttributes].push_back(builder.shape); + } + void AddObject(App::DocumentObject* obj, const char* nameBase) override { // A Link is not a shape to be merged, so pass to base class for standard handling. @@ -370,7 +418,6 @@ protected: private: const EntityCollector* previousEntityCollector; - const eEntityMergeType_t previousMmergeOption; }; #endif class BlockDefinitionCollector: public EntityCollector @@ -379,33 +426,40 @@ protected: public: BlockDefinitionCollector( ImpExpDxfRead& reader, - std::map>& shapesList, - std::map>& - featureBuildersList, + std::map>& buildersList, std::map>& insertsList) : EntityCollector(reader) - , ShapesList(shapesList) - , FeatureBuildersList(featureBuildersList) + , BuildersList(buildersList) , InsertsList(insertsList) {} // TODO: We will want AddAttributeDefinition as well. void AddObject(const TopoDS_Shape& shape, const char* /*nameBase*/) override { - ShapesList[Reader.m_entityAttributes].push_back(shape); - } - void AddObject(FeaturePythonBuilder shapeBuilder) override - { - FeatureBuildersList[Reader.m_entityAttributes].push_back(shapeBuilder); + // This path should no longer be taken, but is kept for compatibility. + BuildersList[Reader.m_entityAttributes].emplace_back(GeometryBuilder(shape)); } - void AddObject(App::DocumentObject* /*obj*/, const char* /*nameBase*/) override + void AddGeometry(const GeometryBuilder& builder) override + { + BuildersList[Reader.m_entityAttributes].push_back(builder); + } + + void AddObject(App::DocumentObject* /*obj*/, const char* nameBase) override { // This path should never be executed. Links and other fully-formed DocumentObjects // are created from INSERT entities, not as part of a BLOCK *definition*. If this // warning ever appears, it indicates a logic error in the importer. Reader.ImportError( - "Internal logic error: Attempted to add a DocumentObject to a block definition."); + "Internal logic error: Attempted to add a DocumentObject ('%s') to a block " + "definition.\n", + nameBase); + } + + void AddObject(FeaturePythonBuilder /*shapeBuilder*/) override + { + // This path is for Draft/FeaturePython objects and is not used by the + // primitives or shapes modes. } void AddInsert(const Base::Vector3d& point, @@ -418,9 +472,7 @@ protected: } private: - std::map>& ShapesList; - std::map>& - FeatureBuildersList; + std::map>& BuildersList; std::map>& InsertsList; }; @@ -498,4 +550,4 @@ protected: } // namespace Import -#endif // IMPEXPDXF_H +#endif // IMPEXPDXFGUI_H diff --git a/src/Mod/Import/App/dxf/dxf.cpp b/src/Mod/Import/App/dxf/dxf.cpp index 28be78f0b4..5511ab8feb 100644 --- a/src/Mod/Import/App/dxf/dxf.cpp +++ b/src/Mod/Import/App/dxf/dxf.cpp @@ -2331,7 +2331,7 @@ bool CDxfRead::ReadDimension() switch ((eDimensionType_t)dimensionType) { case eLinear: case eAligned: - OnReadDimension(start, end, linePosition, Base::toRadians(rotation)); + OnReadDimension(start, end, linePosition, dimensionType, Base::toRadians(rotation)); break; default: UnsupportedFeature("Dimension type '%d'", dimensionType); diff --git a/src/Mod/Import/App/dxf/dxf.h b/src/Mod/Import/App/dxf/dxf.h index dec4163db0..4f6a1f778a 100644 --- a/src/Mod/Import/App/dxf/dxf.h +++ b/src/Mod/Import/App/dxf/dxf.h @@ -938,6 +938,7 @@ public: virtual void OnReadDimension(const Base::Vector3d& /*start*/, const Base::Vector3d& /*end*/, const Base::Vector3d& /*point*/, + int /*dimensionType*/, double /*rotation*/) {} virtual void OnReadPolyline(std::list& /*vertices*/, int /*flags*/) 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 {};