Merge pull request #22251 from furgo16/dxf-new-import-ui-and-more

Redesign DXF import UI, add Part primitives and Draft object import modes
This commit is contained in:
Yorik van Havre
2025-07-28 17:49:25 +02:00
committed by GitHub
15 changed files with 2237 additions and 767 deletions

View File

@@ -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;
}

View File

@@ -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
};

View File

@@ -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);
}

View File

@@ -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;

View File

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

View File

@@ -0,0 +1,116 @@
import FreeCAD
import FreeCADGui
from PySide import QtCore, QtGui
class DxfImportDialog:
"""
A controller class that creates, manages, and shows the DXF import dialog.
"""
def __init__(self, entity_counts, parent=None):
# Step 1: Load the UI from the resource file. This returns a new QDialog instance.
self.dialog = FreeCADGui.PySideUic.loadUi(":/ui/preferences-dxf-import.ui")
# Now, all widgets like "label_Summary" are attributes of self.dialog
self.entity_counts = entity_counts
self.total_entities = sum(entity_counts.values())
self.setup_ui()
self.connect_signals()
self.load_settings_and_set_initial_state()
def setup_ui(self):
"""Perform initial UI setup."""
self.dialog.label_Summary.setText(f"File contains approximately {self.total_entities} geometric entities.")
self.dialog.label_Warning.hide()
def connect_signals(self):
"""Connect signals from the dialog's widgets to our methods."""
buttonBox = self.dialog.findChild(QtGui.QDialogButtonBox, "buttonBox")
if buttonBox:
# Connect to our custom slots INSTEAD of the dialog's built-in ones
buttonBox.accepted.connect(self.on_accept)
buttonBox.rejected.connect(self.on_reject)
FreeCAD.Console.PrintLog("DxfImportDialog: OK and Cancel buttons connected.\n")
else:
FreeCAD.Console.PrintWarning("DxfImportDialog: Could not find buttonBox!\n")
self.dialog.radio_ImportAs_Draft.toggled.connect(self.update_warning_label)
self.dialog.radio_ImportAs_Primitives.toggled.connect(self.update_warning_label)
self.dialog.radio_ImportAs_Shapes.toggled.connect(self.update_warning_label)
self.dialog.radio_ImportAs_Fused.toggled.connect(self.update_warning_label)
def on_accept(self):
"""Custom slot to debug the OK button click."""
FreeCAD.Console.PrintLog("DxfImportDialog: 'OK' button clicked. Calling self.dialog.accept().\n")
# Manually call the original slot
self.dialog.accept()
FreeCAD.Console.PrintLog("DxfImportDialog: self.dialog.accept() has been called.\n")
def on_reject(self):
"""Custom slot to debug the Cancel button click."""
FreeCAD.Console.PrintLog("DxfImportDialog: 'Cancel' button clicked. Calling self.dialog.reject().\n")
# Manually call the original slot
self.dialog.reject()
FreeCAD.Console.PrintLog("DxfImportDialog: self.dialog.reject() has been called.\n")
def load_settings_and_set_initial_state(self):
"""Load saved preferences and set the initial state of the dialog."""
hGrp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft")
mode = hGrp.GetInt("DxfImportMode", 2)
if mode == 0:
self.dialog.radio_ImportAs_Draft.setChecked(True)
elif mode == 1:
self.dialog.radio_ImportAs_Primitives.setChecked(True)
elif mode == 3:
self.dialog.radio_ImportAs_Fused.setChecked(True)
else:
self.dialog.radio_ImportAs_Shapes.setChecked(True)
is_legacy = hGrp.GetBool("dxfUseLegacyImporter", False)
if is_legacy:
self.dialog.radio_ImportAs_Primitives.setEnabled(False)
self.dialog.radio_ImportAs_Draft.setEnabled(True)
self.dialog.radio_ImportAs_Shapes.setEnabled(True)
self.dialog.radio_ImportAs_Fused.setEnabled(True)
else:
self.dialog.radio_ImportAs_Draft.setEnabled(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()

View File

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

View File

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

View File

@@ -6,27 +6,28 @@
<rect>
<x>0</x>
<y>0</y>
<width>649</width>
<height>800</height>
<width>600</width>
<height>880</height>
</rect>
</property>
<property name="windowTitle">
<string>DXF</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<layout class="QVBoxLayout" name="verticalLayout_Main">
<item>
<widget class="QGroupBox" name="groupBox">
<widget class="QGroupBox" name="groupBox_General">
<property name="title">
<string>General options</string>
<string>General</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<layout class="QVBoxLayout" name="verticalLayout_General">
<item>
<widget class="Gui::PrefCheckBox" name="checkBox_dxfShowDialog">
<property name="toolTip">
<string>This preferences dialog will be shown when importing/ exporting DXF files</string>
<string>If checked, this preferences dialog will be shown each time you import or export
a DXF file.</string>
</property>
<property name="text">
<string>Show this dialog when importing and exporting</string>
<string>Show the importer dialog when importing a file</string>
</property>
<property name="checked">
<bool>true</bool>
@@ -42,14 +43,10 @@
<item>
<widget class="Gui::PrefCheckBox" name="checkBox_dxfUseLegacyImporter">
<property name="toolTip">
<string>Python importer is used, otherwise the newer C++ is used.
Note: C++ importer is faster, but is not as featureful yet</string>
<string>Use the legacy Python importer. This importer is more feature-complete but slower and requires an external library.</string>
</property>
<property name="text">
<string>Use legacy Python importer</string>
</property>
<property name="checked">
<bool>false</bool>
<string>Use legacy importer</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxfUseLegacyImporter</cstring>
@@ -62,11 +59,10 @@ Note: C++ importer is faster, but is not as featureful yet</string>
<item>
<widget class="Gui::PrefCheckBox" name="checkBox_dxfUseLegacyExporter">
<property name="toolTip">
<string>Python exporter is used, otherwise the newer C++ is used.
Note: C++ exporter is faster, but is not as featureful yet</string>
<string>Use the legacy Python exporter. This exporter is more feature-complete but slower and requires an external library.</string>
</property>
<property name="text">
<string>Use legacy Python exporter</string>
<string>Use legacy exporter</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxfUseLegacyExporter</cstring>
@@ -80,227 +76,160 @@ Note: C++ exporter is faster, but is not as featureful yet</string>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_1">
<widget class="QGroupBox" name="groupBox_AutoUpdate">
<property name="title">
<string>Automatic update (legacy importer/exporter only)</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_1">
<layout class="QVBoxLayout" name="verticalLayout_AutoUpdate">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_1">
<item>
<widget class="Gui::PrefCheckBox" name="checkBox_dxfAllowDownload">
<property name="toolTip">
<string>Allow FreeCAD to download the Python converter for DXF import and export.
You can also do this manually by installing the &quot;dxf_library&quot; workbench
from the Addon Manager.</string>
</property>
<property name="text">
<string>Allow FreeCAD to automatically download and update the DXF libraries</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxfAllowDownload</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
</widget>
</item>
</layout>
<widget class="Gui::PrefCheckBox" name="checkBox_dxfAllowDownload">
<property name="toolTip">
<string>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.</string>
</property>
<property name="text">
<string>Allow FreeCAD to automatically download and update the DXF libraries</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxfAllowDownload</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="GroupBox_2">
<widget class="QGroupBox" name="groupBox_ImportAs">
<property name="title">
<string>Import options</string>
<string>Import As</string>
</property>
<layout class="QVBoxLayout">
<property name="spacing">
<number>6</number>
</property>
<property name="leftMargin">
<number>9</number>
</property>
<property name="topMargin">
<number>9</number>
</property>
<property name="rightMargin">
<number>9</number>
</property>
<property name="bottomMargin">
<number>9</number>
</property>
<layout class="QVBoxLayout" name="verticalLayout_ImportAs">
<item>
<widget class="QLabel" name="label_ImporterMissing">
<property name="font">
<font>
<italic>true</italic>
</font>
<widget class="Gui::PrefRadioButton" name="radio_ImportAs_Draft">
<property name="enabled">
<bool>true</bool>
</property>
<property name="toolTip">
<string>Creates fully parametric Draft objects. Block definitions are imported as
reusable objects (Part Compounds) and instances become `App::Link` objects,
maintaining the block structure. Best for full integration with the Draft
workbench. </string>
</property>
<property name="text">
<string>Some options are not yet available for the new importer</string>
<string>Editable Draft objects (highest fidelity, slowest)</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxfImportAsDraft</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
<property name="prefRadioButtonGroup" stdset="0">
<string>DxfImportMode</string>
</property>
<property name="prefRadioButtonValue" stdset="0">
<number>0</number>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_Import">
<property name="text">
<string>Import</string>
</property>
</widget>
</item>
<item>
<widget class="Gui::PrefCheckBox" name="checkBox_dxftext">
<property name="toolTip">
<string>If unchecked, texts and mtexts won't be imported</string>
</property>
<property name="text">
<string>Texts and dimensions</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxftext</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
</widget>
</item>
<item>
<widget class="Gui::PrefCheckBox" name="checkBox_dxfImportPoints">
<property name="toolTip">
<string>If unchecked, points won't be imported</string>
</property>
<property name="text">
<string>points</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxfImportPoints</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
</widget>
</item>
<item>
<widget class="Gui::PrefCheckBox" name="checkBox_dxflayout">
<property name="toolTip">
<string>If checked, paper space objects will be imported too</string>
</property>
<property name="text">
<string>Layouts</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxflayout</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
</widget>
</item>
<item>
<widget class="Gui::PrefCheckBox" name="checkBox_dxfstarblocks">
<property name="toolTip">
<string>If you want the non-named blocks (beginning with a *) to be imported too</string>
</property>
<property name="text">
<string>*blocks</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxfstarblocks</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
</widget>
</item>
</layout>
<widget class="Gui::PrefRadioButton" name="radio_ImportAs_Primitives">
<property name="enabled">
<bool>true</bool>
</property>
<property name="toolTip">
<string>Creates parametric Part objects (e.g., Part::Line, Part::Circle). Block
definitions are imported as reusable objects (Part Compounds) and instances
become `App::Link` objects, maintaining the block structure. Best for
script-based post-processing and Part workbench integration.</string>
</property>
<property name="text">
<string>Editable Part primitives (high fidelity, slower)</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxfImportAsPrimitives</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
<property name="prefRadioButtonGroup" stdset="0">
<string>DxfImportMode</string>
</property>
<property name="prefRadioButtonValue" stdset="0">
<number>1</number>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_Create">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Create</string>
</property>
</widget>
</item>
<item>
<widget class="Gui::PrefRadioButton" name="radioButton_dxfCreatePart">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Only standard Part objects will be created (fastest)</string>
</property>
<property name="text">
<string>Simple Part shapes</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxfCreatePart</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
</widget>
</item>
<item>
<widget class="Gui::PrefRadioButton" name="radioButton_dxfCreateDraft">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Parametric Draft objects will be created whenever possible</string>
</property>
<property name="text">
<string>Draft objects</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxfCreateDraft</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
</widget>
</item>
<item>
<widget class="Gui::PrefRadioButton" name="radioButton_dxfCreateSketch">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Sketches will be created whenever possible</string>
</property>
<property name="text">
<string>Sketches</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxfCreateSketch</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
</widget>
</item>
</layout>
<widget class="Gui::PrefRadioButton" name="radio_ImportAs_Shapes">
<property name="toolTip">
<string>Creates a non-parametric shape for each DXF entity. Block definitions are
imported as reusable objects (Part Compounds) and instances become `App::Link`
objects, maintaining the block structure. Good for referencing and measuring.</string>
</property>
<property name="text">
<string>Individual Part shapes (balanced, recommended)</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxfImportAsShapes</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
<property name="prefRadioButtonGroup" stdset="0">
<string>DxfImportMode</string>
</property>
<property name="prefRadioButtonValue" stdset="0">
<number>2</number>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<widget class="Gui::PrefRadioButton" name="radio_ImportAs_Fused">
<property name="toolTip">
<string>Merges all geometry per layer into a single, non-editable shape. Block
structures are not preserved; their geometry becomes part of the layer's
shape. Best for importing and viewing very large files with maximum performance.</string>
</property>
<property name="text">
<string>Fused Part shapes (lowest fidelity, fastest)</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxfImportAsFused</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
<property name="prefRadioButtonGroup" stdset="0">
<string>DxfImportMode</string>
</property>
<property name="prefRadioButtonValue" stdset="0">
<number>3</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_ImportSettings">
<property name="title">
<string>Import Settings</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_ImportSettings">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_Scaling">
<item>
<widget class="QLabel" name="label_dxfScaling">
<property name="toolTip">
<string/>
</property>
<property name="text">
<string>Scale factor to apply to imported files</string>
<string>Global scaling factor</string>
</property>
</widget>
</item>
@@ -320,13 +249,13 @@ from the Addon Manager.</string>
<item>
<widget class="Gui::PrefDoubleSpinBox" name="spinBox_dxfScaling">
<property name="toolTip">
<string>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</string>
<string>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</string>
</property>
<property name="decimals">
<number>12</number>
<number>6</number>
</property>
<property name="maximum">
<double>999999.999998999992386</double>
@@ -345,16 +274,126 @@ Example: for files in millimeters: 1, in centimeters: 10,
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_ImportContent">
<property name="text">
<string>Import</string>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_Import">
<item row="0" column="0">
<widget class="Gui::PrefCheckBox" name="checkBox_dxftext">
<property name="toolTip">
<string>If checked, text, mtext, and dimension entities will be imported as Draft objects</string>
</property>
<property name="text">
<string>Texts and dimensions</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxftext</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="Gui::PrefCheckBox" name="checkBox_dxfImportPoints">
<property name="toolTip">
<string>If checked, point entities will be imported</string>
</property>
<property name="text">
<string>Points</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxfImportPoints</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="Gui::PrefCheckBox" name="checkBox_dxflayout">
<property name="toolTip">
<string>If checked, entities from the paper space will also be imported. By default,
only model space is imported</string>
</property>
<property name="text">
<string>Paper space objects</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxflayout</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="Gui::PrefCheckBox" name="checkBox_dxfstarblocks">
<property name="toolTip">
<string>If checked, anonymous blocks (whose names begin with *) will also be imported.
These are often used for hatches and dimensions</string>
</property>
<property name="text">
<string>Anonymous blocks (*-blocks)</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxfstarblocks</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="Gui::PrefCheckBox" name="checkBox_importDxfHatches">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>If checked, the boundaries of hatch objects will be imported as closed wires.
(Legacy importer only)</string>
</property>
<property name="text">
<string>Hatch boundaries</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>importDxfHatches</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_Appearance">
<property name="text">
<string>Appearance</string>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_Appearance">
<item row="0" column="0">
<widget class="Gui::PrefCheckBox" name="checkBox_dxfGetOriginalColors">
<property name="toolTip">
<string>Colors will set as specified in the DXF file whenever possible.
Otherwise default colors will be applied.</string>
<string>If checked, colors will be set as specified in the DXF file whenever
possible. Otherwise, default FreeCAD colors are applied</string>
</property>
<property name="text">
<string>Use colors from the DXF file</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxfGetOriginalColors</cstring>
</property>
@@ -363,63 +402,14 @@ Otherwise default colors will be applied.</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="Gui::PrefCheckBox" name="checkBox_joingeometry">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>FreeCAD will try to join coincident objects into wires.
Note that this can take a while!</string>
</property>
<property name="text">
<string>Join geometry</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>joingeometry</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="Gui::PrefCheckBox" name="checkBox_groupLayers">
<property name="toolTip">
<string>Objects from the same layers will be joined into Part Compounds,
turning the display faster, but making them less easily editable.</string>
</property>
<property name="text">
<string>Merge layer contents into blocks</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>groupLayers</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<item row="0" column="1">
<widget class="Gui::PrefCheckBox" name="checkBox_dxfStdSize">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Imported texts will get the standard Draft Text size,
instead of the size they have in the DXF document</string>
<string>If checked, imported texts will get the standard Draft text size, instead of
the size defined in the DXF document. (Legacy importer only)</string>
</property>
<property name="text">
<string>Use standard font size for texts</string>
@@ -435,61 +425,42 @@ instead of the size they have in the DXF document</string>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_9">
<item>
<widget class="Gui::PrefCheckBox" name="checkBox_dxfUseDraftVisGroups">
<property name="toolTip">
<string>If this is checked, DXF layers will be imported as Draft Layers</string>
</property>
<property name="text">
<string>Use layers</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxfUseDraftVisGroups</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
</widget>
</item>
</layout>
<widget class="QLabel" name="label_AdvancedProcessing">
<property name="text">
<string>Advanced processing</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_10">
<item>
<widget class="Gui::PrefCheckBox" name="checkBox_importDxfHatches">
<layout class="QGridLayout" name="gridLayout_Advanced">
<item row="0" column="0">
<widget class="Gui::PrefCheckBox" name="checkBox_joingeometry">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Hatches will be converted into simple wires</string>
<string>If checked, the legacy importer will attempt to join coincident geometric
objects into wires. This can be slow for large files. (Legacy importer only)</string>
</property>
<property name="text">
<string>Import hatch boundaries as wires</string>
<string>Join geometry</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>importDxfHatches</cstring>
<cstring>joingeometry</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_11">
<item>
<item row="0" column="1">
<widget class="Gui::PrefCheckBox" name="checkBox_renderPolylineWidth">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>If polylines have a width defined, they will be rendered
as closed wires with correct width</string>
<string>If checked, polylines that have a width property will be rendered as faces
representing that width. (Legacy importer only)</string>
</property>
<property name="text">
<string>Render polylines with width</string>
@@ -502,31 +473,39 @@ as closed wires with correct width</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="Gui::PrefCheckBox" name="checkBox_dxfCreateSketch">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>If checked, the legacy importer will attempt to create Sketcher objects
instead of Draft or Part objects. This overrides the 'Import As' setting</string>
</property>
<property name="text">
<string>Create sketches</string>
</property>
<property name="prefEntry" stdset="0">
<cstring>dxfCreateSketch</cstring>
</property>
<property name="prefPath" stdset="0">
<cstring>Mod/Draft</cstring>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<widget class="QGroupBox" name="groupBox_ExportOptions">
<property name="title">
<string>Export options</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<layout class="QVBoxLayout" name="verticalLayout_Export">
<item>
<widget class="QLabel" name="label_ExporterMissing">
<property name="font">
<font>
<italic>true</italic>
</font>
</property>
<property name="text">
<string>Some options are not yet available for the new exporter</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_12">
<layout class="QHBoxLayout" name="horizontalLayout_Discretize">
<item>
<widget class="Gui::PrefCheckBox" name="checkBox_DiscretizeEllipses">
<property name="toolTip">
@@ -598,7 +577,7 @@ If it is set to '0' the whole spline is treated as a straight segment.</string>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_13">
<layout class="QHBoxLayout" name="horizontalLayout_Export3D">
<item>
<widget class="Gui::PrefCheckBox" name="checkBox_dxfmesh">
<property name="enabled">
@@ -621,7 +600,7 @@ If it is set to '0' the whole spline is treated as a straight segment.</string>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_14">
<layout class="QHBoxLayout" name="horizontalLayout_ExportTechDraw">
<item>
<widget class="Gui::PrefCheckBox" name="checkBox_dxfExportBlocks">
<property name="toolTip">
@@ -645,7 +624,7 @@ This might fail for post DXF R12 templates.</string>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_15">
<layout class="QHBoxLayout" name="horizontalLayout_Project">
<item>
<widget class="Gui::PrefCheckBox" name="checkBox_dxfproject">
<property name="enabled">
@@ -671,21 +650,20 @@ This might fail for post DXF R12 templates.</string>
</widget>
</item>
<item>
<spacer name="verticalSpacer_1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>
<customwidget>
<class>Gui::PrefCheckBox</class>
@@ -693,82 +671,18 @@ This might fail for post DXF R12 templates.</string>
<header>Gui/PrefWidgets.h</header>
</customwidget>
<customwidget>
<class>Gui::PrefDoubleSpinBox</class>
<extends>QDoubleSpinBox</extends>
<class>Gui::PrefRadioButton</class>
<extends>QRadioButton</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
<customwidget>
<class>Gui::PrefRadioButton</class>
<extends>QRadioButton</extends>
<class>Gui::PrefDoubleSpinBox</class>
<extends>QDoubleSpinBox</extends>
<header>Gui/PrefWidgets.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>checkBox_dxfUseLegacyImporter</sender>
<signal>toggled(bool)</signal>
<receiver>label_Create</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
<connection>
<sender>checkBox_dxfUseLegacyImporter</sender>
<signal>toggled(bool)</signal>
<receiver>radioButton_dxfCreatePart</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
<connection>
<sender>checkBox_dxfUseLegacyImporter</sender>
<signal>toggled(bool)</signal>
<receiver>radioButton_dxfCreateDraft</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
<connection>
<sender>checkBox_dxfUseLegacyImporter</sender>
<signal>toggled(bool)</signal>
<receiver>radioButton_dxfCreateSketch</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
<connection>
<sender>checkBox_dxfUseLegacyImporter</sender>
<signal>toggled(bool)</signal>
@@ -785,6 +699,22 @@ This might fail for post DXF R12 templates.</string>
</hint>
</hints>
</connection>
<connection>
<sender>checkBox_dxfUseLegacyImporter</sender>
<signal>toggled(bool)</signal>
<receiver>checkBox_renderPolylineWidth</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
<connection>
<sender>checkBox_dxfUseLegacyImporter</sender>
<signal>toggled(bool)</signal>
@@ -820,7 +750,55 @@ This might fail for post DXF R12 templates.</string>
<connection>
<sender>checkBox_dxfUseLegacyImporter</sender>
<signal>toggled(bool)</signal>
<receiver>checkBox_renderPolylineWidth</receiver>
<receiver>checkBox_dxfCreateSketch</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
<connection>
<sender>checkBox_dxfCreateSketch</sender>
<signal>toggled(bool)</signal>
<receiver>groupBox_ImportAs</receiver>
<slot>setDisabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
<connection>
<sender>checkBox_dxfUseLegacyExporter</sender>
<signal>toggled(bool)</signal>
<receiver>checkBox_dxfmesh</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>20</x>
<y>20</y>
</hint>
<hint type="destinationlabel">
<x>20</x>
<y>20</y>
</hint>
</hints>
</connection>
<connection>
<sender>checkBox_dxfUseLegacyExporter</sender>
<signal>toggled(bool)</signal>
<receiver>checkBox_dxfproject</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">

View File

@@ -69,6 +69,7 @@ from draftobjects.dimension import _Dimension
from draftutils import params
from draftutils import utils
from draftutils.utils import pyopen
from PySide import QtCore, QtGui
gui = FreeCAD.GuiUp
draftui = None
@@ -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")

File diff suppressed because it is too large Load Diff

View File

@@ -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<std::string, int> 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<VertexInfo>& /*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<std::string>& composed);
void ComposeFlattenedBlock(const std::string& blockName, std::set<std::string>& 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<VertexInfo>& 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<CDxfRead::CommonEntityAttributes, std::list<TopoDS_Shape>> Shapes;
std::map<CDxfRead::CommonEntityAttributes, std::list<FeaturePythonBuilder>>
FeatureBuildersList;
std::map<CDxfRead::CommonEntityAttributes, std::list<GeometryBuilder>> GeometryBuilders;
std::map<CDxfRead::CommonEntityAttributes, std::list<Insert>> 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<CDxfRead::CommonEntityAttributes, std::list<TopoDS_Shape>>& shapesList,
std::map<CDxfRead::CommonEntityAttributes, std::list<FeaturePythonBuilder>>&
featureBuildersList,
std::map<CDxfRead::CommonEntityAttributes, std::list<GeometryBuilder>>& buildersList,
std::map<CDxfRead::CommonEntityAttributes, std::list<Block::Insert>>& 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<CDxfRead::CommonEntityAttributes, std::list<TopoDS_Shape>>& ShapesList;
std::map<CDxfRead::CommonEntityAttributes, std::list<FeaturePythonBuilder>>&
FeatureBuildersList;
std::map<CDxfRead::CommonEntityAttributes, std::list<GeometryBuilder>>& BuildersList;
std::map<CDxfRead::CommonEntityAttributes, std::list<Block::Insert>>& InsertsList;
};
@@ -498,4 +550,4 @@ protected:
} // namespace Import
#endif // IMPEXPDXF_H
#endif // IMPEXPDXFGUI_H

View File

@@ -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);

View File

@@ -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<VertexInfo>& /*vertices*/, int /*flags*/)

View File

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