From 37cd1a657a849b27ca5e78b231d7471163724c75 Mon Sep 17 00:00:00 2001
From: Furgo <148809153+furgo16@users.noreply.github.com>
Date: Thu, 26 Jun 2025 12:09:26 +0200
Subject: [PATCH 01/14] Import: DXF importer, refactor preferences UI
---
src/Mod/Draft/Resources/ui/preferences-dxf.ui | 774 +++++++++---------
src/Mod/Draft/importDXF.py | 81 +-
src/Mod/Import/App/dxf/ImpExpDxf.cpp | 271 +++---
src/Mod/Import/App/dxf/ImpExpDxf.h | 11 +-
4 files changed, 614 insertions(+), 523 deletions(-)
diff --git a/src/Mod/Draft/Resources/ui/preferences-dxf.ui b/src/Mod/Draft/Resources/ui/preferences-dxf.ui
index 45b348d707..c4a78612cd 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
-
+
+
+ false
+
+
+ Creates fully parametric Draft objects. Block definitions are imported as
+reusable objects (Part Compounds) and instances become `App::Link` objects,
+maintaining the block structure. Best for full integration with the Draft
+Workbench. (Legacy importer only)
- Some options are not yet available for the new importer
+ Editable draft objects (Highest fidelity, slowest)
+
+
+ DxfImportMode
+
+
+ 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
-
-
-
-
+
+
+ false
+
+
+ Creates parametric Part objects (e.g., Part::Line, Part::Circle). Block
+definitions are imported as reusable objects (Part Compounds) and instances
+become `App::Link` objects, maintaining the block structure. Best for
+script-based post-processing. (Not yet implemented)
+
+
+ Editable part primitives (High fidelity, slower)
+
+
+ DxfImportMode
+
+
+ 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
+
+
+ DxfImportMode
+
+
+ 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 viewing very large files with maximum performance.
+
+
+ Fused part shapes (Lowest fidelity, fastest)
+
+
+ DxfImportMode
+
+
+ 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 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
- 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 (legacy importer only)
+
+
+ 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::PrefDoubleSpinBox
- QDoubleSpinBox
+ Gui::PrefRadioButton
+ QRadioButton
- Gui::PrefRadioButton
- QRadioButton
+ Gui::PrefDoubleSpinBox
+ QDoubleSpinBox
-
- 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..39fcc9c3a9 100644
--- a/src/Mod/Draft/importDXF.py
+++ b/src/Mod/Draft/importDXF.py
@@ -4163,6 +4163,8 @@ def getViewDXF(view):
return block, insert
+# In src/Mod/Draft/importDXF.py
+
def readPreferences():
"""Read the preferences of the this module from the parameter database.
@@ -4184,6 +4186,7 @@ def readPreferences():
# 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
@@ -4193,28 +4196,66 @@ def readPreferences():
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")
+
+ # --- Read all feature and appearance toggles ---
+ # These are independent settings and can be read directly.
+ dxfDiscretizeCurves = params.get_param("DiscretizeEllipses", False)
+ dxfStarBlocks = params.get_param("dxfstarblocks", False)
+ dxfJoin = params.get_param("joingeometry", False)
+ dxfRenderPolylineWidth = params.get_param("renderPolylineWidth", False)
+ dxfImportTexts = params.get_param("dxftext", True)
+ dxfImportLayouts = params.get_param("dxflayout", False)
+ dxfImportPoints = params.get_param("dxfImportPoints", True)
+ dxfImportHatches = params.get_param("importDxfHatches", True)
+ dxfUseStandardSize = params.get_param("dxfStdSize", False)
+ dxfGetColors = params.get_param("dxfGetOriginalColors", True)
+ dxfUseDraftVisGroups = params.get_param("dxfUseDraftVisGroups", True)
+ dxfMakeFaceMode = params.get_param("MakeFaceMode", False)
+ dxfExportBlocks = params.get_param("dxfExportBlocks", True)
+ dxfScaling = params.get_param("dxfScaling", 1.0)
+
+ # These control which importer is used. The script should only proceed if
+ # the legacy importer is selected.
+ dxfUseLegacyImporter = params.get_param("dxfUseLegacyImporter", False)
+ dxfUseLegacyExporter = params.get_param("dxfUseLegacyExporter", False)
+
+ if not dxfUseLegacyImporter:
+ # If the legacy importer is called when not selected, exit.
+ # This prevents accidental execution.
+ return
+
+ # --- New, Centralized Logic for Structural Mode ---
+
+ # Read the legacy-specific override for sketch creation.
+ dxfCreateSketch = params.get_param("dxfCreateSketch", False)
+
+ if dxfCreateSketch:
+ # Sketch mode takes highest priority, other modes are irrelevant.
+ dxfCreatePart = False
+ dxfCreateDraft = False
+ dxfMakeBlocks = False
+ else:
+ # Not in sketch mode, so determine structure from DxfImportMode.
+ # This is where the new parameter is read, with its default value defined.
+ # 0=Draft, 1=Primitives, 2=Shapes, 3=Fused
+ import_mode = params.get_param("DxfImportMode", 2) # Default to "Individual part shapes"
+
+ if import_mode == 3: # Fused part shapes
+ dxfMakeBlocks = True # 'groupLayers' is the legacy equivalent
+ dxfCreatePart = False # In legacy, dxfMakeBlocks overrides these
+ dxfCreateDraft = False
+ elif import_mode == 0: # Editable draft objects
+ dxfMakeBlocks = False
+ dxfCreatePart = False
+ dxfCreateDraft = True
+ else: # Covers modes 1 (Primitives) and 2 (Shapes). Legacy maps both to "Simple part shapes"
+ dxfMakeBlocks = False
+ dxfCreatePart = True
+ dxfCreateDraft = False
+
+ # --- Other settings that are not checkboxes ---
dxfBrightBackground = isBrightBackground()
dxfDefaultColor = getColor()
- dxfExportBlocks = params.get_param("dxfExportBlocks")
- dxfScaling = params.get_param("dxfScaling")
class DxfImportReporter:
diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.cpp b/src/Mod/Import/App/dxf/ImpExpDxf.cpp
index 7a6add1c19..3319fe27a2 100644
--- a/src/Mod/Import/App/dxf/ImpExpDxf.cpp
+++ b/src/Mod/Import/App/dxf/ImpExpDxf.cpp
@@ -106,12 +106,23 @@ void ImpExpDxfRead::StartImport()
bool ImpExpDxfRead::ReadEntitiesSection()
{
+ // TODO: remove this once the unsupported modes have been implemented.
+ // Perform a one-time check for unsupported modes
+ if (m_importMode == ImportMode::EditableDraft) {
+ UnsupportedFeature("Import as 'Editable draft objects' is not yet implemented.");
+ // We can continue, and the switch statements below will do nothing,
+ // resulting in an empty import for geometry, which is correct behavior.
+ }
+ else if (m_importMode == ImportMode::EditablePrimitives) {
+ UnsupportedFeature("Import as 'Editable part primitives' is not yet implemented.");
+ }
+
// After parsing the BLOCKS section, compose all block definitions
// into FreeCAD objects before processing the ENTITIES section.
ComposeBlocks();
DrawingEntityCollector collector(*this);
- if (m_mergeOption < SingleShapes) {
+ if (m_importMode == ImportMode::FusedShapes) {
std::map> ShapesToCombine;
{
ShapeSavingEntityCollector savingCollector(*this, ShapesToCombine);
@@ -186,31 +197,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";
@@ -604,13 +602,24 @@ void ImpExpDxfRead::OnReadLine(const Base::Vector3d& start,
return;
}
- 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?
- return;
+ switch (m_importMode) {
+ case ImportMode::IndividualShapes:
+ case ImportMode::FusedShapes: {
+ gp_Pnt p0 = makePoint(start);
+ gp_Pnt p1 = makePoint(end);
+ // TODO: Really?? What about the people designing integrated circuits?
+ if (p0.IsEqual(p1, 1e-8)) {
+ return;
+ }
+ Collector->AddObject(BRepBuilderAPI_MakeEdge(p0, p1).Edge(), "Line");
+ break;
+ }
+ case ImportMode::EditableDraft:
+ case ImportMode::EditablePrimitives:
+ // Do nothing until these modes have been implemented, the one-time warning has already
+ // been issued.
+ break;
}
- Collector->AddObject(BRepBuilderAPI_MakeEdge(p0, p1).Edge(), "Line");
}
@@ -620,7 +629,19 @@ void ImpExpDxfRead::OnReadPoint(const Base::Vector3d& start)
return;
}
- Collector->AddObject(BRepBuilderAPI_MakeVertex(makePoint(start)).Vertex(), "Point");
+ switch (m_importMode) {
+ case ImportMode::IndividualShapes:
+ case ImportMode::FusedShapes: {
+ // For non-parametric modes, create a Part::Feature with a Vertex shape.
+ Collector->AddObject(BRepBuilderAPI_MakeVertex(makePoint(start)).Vertex(), "Point");
+ break;
+ }
+ case ImportMode::EditableDraft:
+ case ImportMode::EditablePrimitives:
+ // Do nothing until these modes have been implemented, the one-time warning has already
+ // been issued.
+ break;
+ }
}
@@ -634,19 +655,30 @@ void ImpExpDxfRead::OnReadArc(const Base::Vector3d& start,
return;
}
- gp_Pnt p0 = makePoint(start);
- gp_Pnt p1 = makePoint(end);
- gp_Dir up(0, 0, 1);
- if (!dir) {
- up = -up;
- }
- 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 {
- Base::Console().warning("ImpExpDxf - ignore degenerate arc of circle\n");
+ switch (m_importMode) {
+ case ImportMode::IndividualShapes:
+ case ImportMode::FusedShapes: {
+ gp_Pnt p0 = makePoint(start);
+ gp_Pnt p1 = makePoint(end);
+ gp_Dir up(0, 0, 1);
+ if (!dir) {
+ up.Reverse();
+ }
+ gp_Pnt pc = makePoint(center);
+ gp_Circ circle(gp_Ax2(pc, up), p0.Distance(pc));
+ if (circle.Radius() > 1e-9) {
+ Collector->AddObject(BRepBuilderAPI_MakeEdge(circle, p0, p1).Edge(), "Arc");
+ }
+ else {
+ Base::Console().warning("ImpExpDxf - ignore degenerate arc of circle\n");
+ }
+ break;
+ }
+ case ImportMode::EditableDraft:
+ case ImportMode::EditablePrimitives:
+ // Do nothing until these modes have been implemented, the one-time warning has already
+ // been issued.
+ break;
}
}
@@ -660,18 +692,27 @@ void ImpExpDxfRead::OnReadCircle(const Base::Vector3d& start,
return;
}
- gp_Pnt p0 = makePoint(start);
- gp_Dir up(0, 0, 1);
- if (!dir) {
- up = -up;
- }
- 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 {
- Base::Console().warning("ImpExpDxf - ignore degenerate circle\n");
+ switch (m_importMode) {
+ case ImportMode::IndividualShapes:
+ case ImportMode::FusedShapes: {
+ gp_Pnt p0 = makePoint(start);
+ gp_Dir up(0, 0, 1);
+ if (!dir) {
+ up.Reverse();
+ }
+ gp_Pnt pc = makePoint(center);
+ gp_Circ circle(gp_Ax2(pc, up), p0.Distance(pc));
+ if (circle.Radius() > 1e-9) {
+ Collector->AddObject(BRepBuilderAPI_MakeEdge(circle).Edge(), "Circle");
+ }
+ else {
+ Base::Console().warning("ImpExpDxf - ignore degenerate circle\n");
+ }
+ break;
+ }
+ case ImportMode::EditableDraft:
+ case ImportMode::EditablePrimitives:
+ break;
}
}
@@ -776,23 +817,32 @@ void ImpExpDxfRead::OnReadSpline(struct SplineData& sd)
return;
}
- try {
- Handle(Geom_BSplineCurve) geom;
- if (sd.control_points > 0) {
- geom = getSplineFromPolesAndKnots(sd);
- }
- else if (sd.fit_points > 0) {
- geom = getInterpolationSpline(sd);
- }
+ switch (m_importMode) {
+ case ImportMode::IndividualShapes:
+ case ImportMode::FusedShapes: {
+ try {
+ Handle(Geom_BSplineCurve) geom;
+ if (sd.control_points > 0) {
+ geom = getSplineFromPolesAndKnots(sd);
+ }
+ else if (sd.fit_points > 0) {
+ geom = getInterpolationSpline(sd);
+ }
- if (geom.IsNull()) {
- throw Standard_Failure();
- }
+ if (geom.IsNull()) {
+ throw Standard_Failure();
+ }
- Collector->AddObject(BRepBuilderAPI_MakeEdge(geom).Edge(), "Spline");
- }
- catch (const Standard_Failure&) {
- Base::Console().warning("ImpExpDxf - failed to create bspline\n");
+ Collector->AddObject(BRepBuilderAPI_MakeEdge(geom).Edge(), "Spline");
+ }
+ catch (const Standard_Failure&) {
+ Base::Console().warning("ImpExpDxf - failed to create bspline\n");
+ }
+ break;
+ }
+ case ImportMode::EditableDraft:
+ case ImportMode::EditablePrimitives:
+ break;
}
}
@@ -810,22 +860,30 @@ void ImpExpDxfRead::OnReadEllipse(const Base::Vector3d& center,
return;
}
- gp_Dir up(0, 0, 1);
- if (!dir) {
- up = -up;
- }
- 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 {
- Base::Console().warning("ImpExpDxf - ignore degenerate ellipse\n");
+ switch (m_importMode) {
+ case ImportMode::IndividualShapes:
+ case ImportMode::FusedShapes: {
+ gp_Dir up(0, 0, 1);
+ if (!dir) {
+ 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() > 1e-9) {
+ Collector->AddObject(BRepBuilderAPI_MakeEdge(ellipse).Edge(), "Ellipse");
+ }
+ else {
+ Base::Console().warning("ImpExpDxf - ignore degenerate ellipse\n");
+ }
+ break;
+ }
+ case ImportMode::EditableDraft:
+ case ImportMode::EditablePrimitives:
+ break;
}
}
-
void ImpExpDxfRead::OnReadText(const Base::Vector3d& point,
const double height,
const std::string& text,
@@ -901,10 +959,10 @@ void ImpExpDxfRead::OnReadDimension(const Base::Vector3d& start,
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
+ // 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)
@@ -941,6 +999,11 @@ void ImpExpDxfRead::OnReadPolyline(std::list& vertices, int flags)
return;
}
+ // Polyline explosion logic is complex and calls back to other OnRead... handlers.
+ // The mode switch should happen inside the final geometry creation handlers
+ // (OnReadLine, OnReadArc), so this function doesn't need its own switch statement.
+ // It simply acts as a dispatcher.
+
std::map> ShapesToCombine;
{
// TODO: Currently ExpandPolyline calls OnReadArc etc to generate the pieces, and these
@@ -948,20 +1011,19 @@ void ImpExpDxfRead::OnReadPolyline(std::list& vertices, int flags)
// 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.
+ // 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);
}
// Join the shapes.
if (!ShapesToCombine.empty()) {
- // TODO: If we want Draft objects and all segments are straight lines we can make a draft
- // wire.
+ // TODO: If we want Draft objects and all segments are straight lines we can make a
+ // draft wire.
CombineShapes(ShapesToCombine.begin()->second, "Polyline");
}
}
@@ -1013,12 +1075,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)
@@ -1043,9 +1105,9 @@ ImpExpDxfRead::MakeLayer(const std::string& name, ColorIndex_t color, std::strin
Py_False);
}
- // 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 +1117,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
}
@@ -1111,7 +1173,8 @@ 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.
+ // The object is already in the document, so we just need to style it and move it to a
+ // layer.
Reader.MoveToLayer(obj);
// Safely apply styles by checking the object's actual type
diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.h b/src/Mod/Import/App/dxf/ImpExpDxf.h
index 21b533d311..5a1855a651 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:
@@ -108,6 +117,7 @@ public:
void FinishImport() override;
private:
+ ImportMode m_importMode = ImportMode::IndividualShapes;
bool shouldSkipEntity() const
{
// This entity is in paper space, and the user setting says to ignore it.
@@ -370,7 +380,6 @@ protected:
private:
const EntityCollector* previousEntityCollector;
- const eEntityMergeType_t previousMmergeOption;
};
#endif
class BlockDefinitionCollector: public EntityCollector
From c238be2857677590e57df61f24c92daf5cb9aed7 Mon Sep 17 00:00:00 2001
From: Furgo <148809153+furgo16@users.noreply.github.com>
Date: Sun, 29 Jun 2025 08:53:08 +0200
Subject: [PATCH 02/14] Gui: add WaitCursor API
---
src/Gui/ApplicationPy.cpp | 26 ++++++++++++++++++++++++++
src/Gui/ApplicationPy.h | 3 +++
src/Gui/WaitCursor.cpp | 13 +++++++++++++
src/Gui/WaitCursor.h | 13 +++++++++++++
4 files changed, 55 insertions(+)
diff --git a/src/Gui/ApplicationPy.cpp b/src/Gui/ApplicationPy.cpp
index 4315e4f6c1..a71de70459 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 */
};
@@ -1813,3 +1819,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;
From 293ebf801c8c64c32ba95187f9cecce31e04f638 Mon Sep 17 00:00:00 2001
From: Furgo <148809153+furgo16@users.noreply.github.com>
Date: Sun, 29 Jun 2025 08:53:38 +0200
Subject: [PATCH 03/14] Import: DXF, add dedicated import dialog
---
src/Mod/Draft/CMakeLists.txt | 1 +
src/Mod/Draft/DxfImportDialog.py | 116 ++++++++++
src/Mod/Draft/Resources/Draft.qrc | 1 +
.../Resources/ui/preferences-dxf-import.ui | 154 +++++++++++++
src/Mod/Draft/importDXF.py | 213 ++++++++++++------
src/Mod/Import/App/dxf/ImpExpDxf.cpp | 30 +++
src/Mod/Import/App/dxf/ImpExpDxf.h | 2 +-
src/Mod/Import/Gui/AppImportGuiPy.cpp | 21 ++
8 files changed, 462 insertions(+), 76 deletions(-)
create mode 100644 src/Mod/Draft/DxfImportDialog.py
create mode 100644 src/Mod/Draft/Resources/ui/preferences-dxf-import.ui
diff --git a/src/Mod/Draft/CMakeLists.txt b/src/Mod/Draft/CMakeLists.txt
index 7e08b0c53e..f985c4350c 100644
--- a/src/Mod/Draft/CMakeLists.txt
+++ b/src/Mod/Draft/CMakeLists.txt
@@ -20,6 +20,7 @@ SET(Draft_SRCS_base
SET(Draft_import
importAirfoilDAT.py
importDXF.py
+ DxfImportDialog.py
importDWG.py
importOCA.py
importSVG.py
diff --git a/src/Mod/Draft/DxfImportDialog.py b/src/Mod/Draft/DxfImportDialog.py
new file mode 100644
index 0000000000..a1c7fec800
--- /dev/null
+++ b/src/Mod/Draft/DxfImportDialog.py
@@ -0,0 +1,116 @@
+import FreeCAD
+import FreeCADGui
+from PySide import QtCore, QtGui
+
+class DxfImportDialog:
+ """
+ A controller class that creates, manages, and shows the DXF import dialog.
+ """
+ def __init__(self, entity_counts, parent=None):
+ # Step 1: Load the UI from the resource file. This returns a new QDialog instance.
+ self.dialog = FreeCADGui.PySideUic.loadUi(":/ui/preferences-dxf-import.ui")
+
+ # Now, all widgets like "label_Summary" are attributes of self.dialog
+
+ self.entity_counts = entity_counts
+ self.total_entities = sum(entity_counts.values())
+
+ self.setup_ui()
+ self.connect_signals()
+ self.load_settings_and_set_initial_state()
+
+ def setup_ui(self):
+ """Perform initial UI setup."""
+ self.dialog.label_Summary.setText(f"File contains approximately {self.total_entities} geometric entities.")
+ self.dialog.label_Warning.hide()
+
+ def connect_signals(self):
+ """Connect signals from the dialog's widgets to our methods."""
+ buttonBox = self.dialog.findChild(QtGui.QDialogButtonBox, "buttonBox")
+ if buttonBox:
+ # Connect to our custom slots INSTEAD of the dialog's built-in ones
+ buttonBox.accepted.connect(self.on_accept)
+ buttonBox.rejected.connect(self.on_reject)
+ FreeCAD.Console.PrintLog("DxfImportDialog: OK and Cancel buttons connected.\n")
+ else:
+ FreeCAD.Console.PrintWarning("DxfImportDialog: Could not find buttonBox!\n")
+
+ self.dialog.radio_ImportAs_Draft.toggled.connect(self.update_warning_label)
+ self.dialog.radio_ImportAs_Primitives.toggled.connect(self.update_warning_label)
+ self.dialog.radio_ImportAs_Shapes.toggled.connect(self.update_warning_label)
+ self.dialog.radio_ImportAs_Fused.toggled.connect(self.update_warning_label)
+
+ def on_accept(self):
+ """Custom slot to debug the OK button click."""
+ FreeCAD.Console.PrintLog("DxfImportDialog: 'OK' button clicked. Calling self.dialog.accept().\n")
+ # Manually call the original slot
+ self.dialog.accept()
+ FreeCAD.Console.PrintLog("DxfImportDialog: self.dialog.accept() has been called.\n")
+
+ def on_reject(self):
+ """Custom slot to debug the Cancel button click."""
+ FreeCAD.Console.PrintLog("DxfImportDialog: 'Cancel' button clicked. Calling self.dialog.reject().\n")
+ # Manually call the original slot
+ self.dialog.reject()
+ FreeCAD.Console.PrintLog("DxfImportDialog: self.dialog.reject() has been called.\n")
+
+ def load_settings_and_set_initial_state(self):
+ """Load saved preferences and set the initial state of the dialog."""
+ hGrp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft")
+
+ mode = hGrp.GetInt("DxfImportMode", 2)
+
+ if mode == 0:
+ self.dialog.radio_ImportAs_Draft.setChecked(True)
+ elif mode == 1:
+ self.dialog.radio_ImportAs_Primitives.setChecked(True)
+ elif mode == 3:
+ self.dialog.radio_ImportAs_Fused.setChecked(True)
+ else:
+ self.dialog.radio_ImportAs_Shapes.setChecked(True)
+
+ is_legacy = hGrp.GetBool("dxfUseLegacyImporter", False)
+ if is_legacy:
+ self.dialog.radio_ImportAs_Primitives.setEnabled(False)
+ self.dialog.radio_ImportAs_Draft.setEnabled(True)
+ self.dialog.radio_ImportAs_Shapes.setEnabled(True)
+ self.dialog.radio_ImportAs_Fused.setEnabled(True)
+ else:
+ self.dialog.radio_ImportAs_Draft.setEnabled(False)
+ self.dialog.radio_ImportAs_Primitives.setEnabled(False)
+ self.dialog.radio_ImportAs_Shapes.setEnabled(True)
+ self.dialog.radio_ImportAs_Fused.setEnabled(True)
+
+ self.update_warning_label()
+
+ def update_warning_label(self):
+ """Updates the warning label based on selection and entity count."""
+ self.dialog.label_Warning.hide()
+ current_mode = self.get_selected_mode()
+
+ if self.total_entities > 5000 and (current_mode == 0 or current_mode == 1):
+ self.dialog.label_Warning.setText("Warning: Importing over 5000 entities as editable objects can be very slow.")
+ self.dialog.label_Warning.show()
+ elif self.total_entities > 20000 and current_mode == 2:
+ self.dialog.label_Warning.setText("Warning: Importing over 20,000 entities as individual shapes may be slow.")
+ self.dialog.label_Warning.show()
+
+ def exec_(self):
+ FreeCAD.Console.PrintLog("DxfImportDialog: Calling self.dialog.exec_()...\n")
+ result = self.dialog.exec_()
+ FreeCAD.Console.PrintLog("DxfImportDialog: self.dialog.exec_() returned with result: {}\n".format(result))
+ # QDialog.Accepted is usually 1, Rejected is 0.
+ FreeCAD.Console.PrintLog("(Note: QDialog.Accepted = {}, QDialog.Rejected = {})\n".format(QtGui.QDialog.Accepted, QtGui.QDialog.Rejected))
+ return result
+
+ def get_selected_mode(self):
+ """Return the integer value of the selected import mode."""
+ if self.dialog.radio_ImportAs_Draft.isChecked(): return 0
+ if self.dialog.radio_ImportAs_Primitives.isChecked(): return 1
+ if self.dialog.radio_ImportAs_Fused.isChecked(): return 3
+ if self.dialog.radio_ImportAs_Shapes.isChecked(): return 2
+ return 2
+
+ def get_show_dialog_again(self):
+ """Return True if the dialog should be shown next time."""
+ return not self.dialog.checkBox_ShowDialogAgain.isChecked()
diff --git a/src/Mod/Draft/Resources/Draft.qrc b/src/Mod/Draft/Resources/Draft.qrc
index 8cc66d2943..fa6211ca12 100644
--- a/src/Mod/Draft/Resources/Draft.qrc
+++ b/src/Mod/Draft/Resources/Draft.qrc
@@ -185,6 +185,7 @@
ui/preferences-draftvisual.ui
ui/preferences-dwg.ui
ui/preferences-dxf.ui
+ ui/preferences-dxf-import.ui
ui/preferences-oca.ui
ui/preferences-svg.ui
ui/TaskPanel_CircularArray.ui
diff --git a/src/Mod/Draft/Resources/ui/preferences-dxf-import.ui b/src/Mod/Draft/Resources/ui/preferences-dxf-import.ui
new file mode 100644
index 0000000000..fa66b241e8
--- /dev/null
+++ b/src/Mod/Draft/Resources/ui/preferences-dxf-import.ui
@@ -0,0 +1,154 @@
+
+
+ DxfImportDialog
+
+
+
+ 0
+ 0
+ 480
+ 280
+
+
+
+ DXF Import
+
+
+ -
+
+
+ Import as
+
+
+
-
+
+
+ Creates fully parametric Draft objects. Block definitions are imported as
+reusable objects (Part Compounds) and instances become `App::Link` objects,
+maintaining the block structure. Best for full integration with the Draft
+Workbench. (Legacy importer only)
+
+
+ Editable draft objects
+
+
+
+ -
+
+
+ Creates parametric Part objects (e.g., Part::Line, Part::Circle). Block
+definitions are imported as reusable objects (Part Compounds) and instances
+become `App::Link` objects, maintaining the block structure. Best for
+script-based post-processing. (Not yet implemented)
+
+
+ Editable part primitives
+
+
+
+ -
+
+
+ Creates a non-parametric shape for each DXF entity. Block definitions are
+imported as reusable objects (Part Compounds) and instances become `App::Link`
+objects, maintaining the block structure. Good for referencing and measuring.
+
+
+ Individual part shapes (recommended)
+
+
+
+ -
+
+
+ Merges all geometry per layer into a single, non-editable shape. Block
+structures are not preserved; their geometry becomes part of the layer's
+shape. Best for viewing very large files with maximum performance.
+
+
+ Fused part shapes (fastest)
+
+
+
+
+
+
+ -
+
+
+ QFrame::StyledPanel
+
+
+ QFrame::Sunken
+
+
+
+ 5
+
+
+ 5
+
+
+ 5
+
+
+ 5
+
+
-
+
+
+ File summary
+
+
+
+ -
+
+
+ color: #c00;
+
+
+ Warning
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 0
+
+
+
+
+ -
+
+
+ Do not show this dialog again
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py
index 39fcc9c3a9..cc67c5292b 100644
--- a/src/Mod/Draft/importDXF.py
+++ b/src/Mod/Draft/importDXF.py
@@ -69,6 +69,7 @@ from draftobjects.dimension import _Dimension
from draftutils import params
from draftutils import utils
from draftutils.utils import pyopen
+from PySide import QtCore, QtGui
gui = FreeCAD.GuiUp
draftui = None
@@ -2810,10 +2811,51 @@ def open(filename):
-----
Use local variables, not global variables.
"""
- readPreferences()
+ hGrp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft")
+ use_legacy = hGrp.GetBool("dxfUseLegacyImporter", False)
+
+ # The C++ layer (`Gui::Application::importFrom`) has set the WaitCursor.
+ # We must temporarily suspend it to show our interactive dialog.
+ try:
+ if gui:
+ FreeCADGui.suspendWaitCursor()
+
+ # --- Dialog Workflow ---
+ if gui and not use_legacy and hGrp.GetBool("dxfShowDialog", True):
+ try:
+ import ImportGui
+ # This C++ function will need to be created in a later step
+ entity_counts = ImportGui.preScanDxf(filename)
+ except Exception:
+ entity_counts = {}
+
+ from DxfImportDialog import DxfImportDialog
+ dlg = DxfImportDialog(entity_counts)
+
+ if dlg.exec_():
+ # User clicked OK, save settings from the dialog
+ hGrp.SetInt("DxfImportMode", dlg.get_selected_mode())
+ hGrp.SetBool("dxfShowDialog", dlg.get_show_dialog_again())
+ else:
+ # User clicked Cancel, abort the entire operation
+ FCC.PrintLog("DXF import cancelled by user.\n")
+ return
+ else:
+ # If we don't show the dialog, we still need to read preferences
+ # to ensure the correct backend logic is triggered.
+ readPreferences()
+
+ finally:
+ # --- CRITICAL: Always resume the wait state before returning to C++ ---
+ # This restores the wait cursor and event filter so the subsequent
+ # blocking C++ call behaves as expected within the C++ scope.
+ if gui:
+ FreeCADGui.resumeWaitCursor()
+
+ # --- Proceed with the blocking import logic ---
total_start_time = time.perf_counter()
- if dxfUseLegacyImporter:
+ if use_legacy:
getDXFlibs()
if dxfReader:
docname = os.path.splitext(os.path.basename(filename))[0]
@@ -2823,12 +2865,14 @@ def open(filename):
return doc
else:
errorDXFLib(gui)
- else:
+ return None
+ else: # Modern C++ Importer
docname = os.path.splitext(os.path.basename(filename))[0]
doc = FreeCAD.newDocument(docname)
doc.Label = docname
FreeCAD.setActiveDocument(doc.Name)
stats = None
+
if gui:
import ImportGui
stats = ImportGui.readDXF(filename)
@@ -2836,13 +2880,16 @@ def open(filename):
import Import
stats = Import.readDXF(filename)
+ Draft.convert_draft_texts()
+ doc.recompute()
+
total_end_time = time.perf_counter()
if stats:
+ # Report PROCESSING time only, not user dialog time.
reporter = DxfImportReporter(filename, stats, total_end_time - total_start_time)
reporter.report_to_console()
- Draft.convert_draft_texts() # convert annotations to Draft texts
- doc.recompute()
+ return doc
def insert(filename, docname):
@@ -2864,20 +2911,53 @@ def insert(filename, docname):
-----
Use local variables, not global variables.
"""
- readPreferences()
+ hGrp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft")
+ use_legacy = hGrp.GetBool("dxfUseLegacyImporter", False)
+
+ try:
+ if gui:
+ FreeCADGui.suspendWaitCursor()
+
+ # --- Dialog Workflow ---
+ if gui and not use_legacy and hGrp.GetBool("dxfShowDialog", True):
+ try:
+ import ImportGui
+ entity_counts = ImportGui.preScanDxf(filename)
+ except Exception:
+ entity_counts = {}
+
+ from DxfImportDialog import DxfImportDialog
+ dlg = DxfImportDialog(entity_counts)
+
+ if dlg.exec_():
+ hGrp.SetInt("DxfImportMode", dlg.get_selected_mode())
+ hGrp.SetBool("dxfShowDialog", dlg.get_show_dialog_again())
+ else:
+ FCC.PrintLog("DXF insert cancelled by user.\n")
+ return
+ else:
+ readPreferences()
+
+ finally:
+ if gui:
+ FreeCADGui.resumeWaitCursor()
+
+ # --- Proceed with the blocking insert logic ---
total_start_time = time.perf_counter()
+
try:
doc = FreeCAD.getDocument(docname)
except NameError:
doc = FreeCAD.newDocument(docname)
FreeCAD.setActiveDocument(docname)
- if dxfUseLegacyImporter:
+
+ if use_legacy:
getDXFlibs()
if dxfReader:
processdxf(doc, filename)
else:
errorDXFLib(gui)
- else:
+ else: # Modern C++ Importer
stats = None
if gui:
import ImportGui
@@ -2886,14 +2966,14 @@ def insert(filename, docname):
import Import
stats = Import.readDXF(filename)
+ Draft.convert_draft_texts()
+ doc.recompute()
+
total_end_time = time.perf_counter()
if stats:
reporter = DxfImportReporter(filename, stats, total_end_time - total_start_time)
reporter.report_to_console()
- Draft.convert_draft_texts() # convert annotations to Draft texts
- doc.recompute()
-
def getShapes(filename):
"""Read a DXF file, and return a list of shapes from its contents.
@@ -4183,81 +4263,64 @@ def readPreferences():
-----
Use local variables, not global variables.
"""
- # reading parameters
- if gui and params.get_param("dxfShowDialog"):
- FreeCADGui.showPreferencesByName("Import-Export", ":/ui/preferences-dxf.ui")
-
global dxfCreatePart, dxfCreateDraft, dxfCreateSketch
- global dxfDiscretizeCurves, dxfStarBlocks
- global dxfMakeBlocks, dxfJoin, dxfRenderPolylineWidth
- global dxfImportTexts, dxfImportLayouts
- global dxfImportPoints, dxfImportHatches, dxfUseStandardSize
- global dxfGetColors, dxfUseDraftVisGroups
- global dxfMakeFaceMode, dxfBrightBackground, dxfDefaultColor
- global dxfUseLegacyImporter, dxfExportBlocks, dxfScaling
- global dxfUseLegacyExporter
+ global dxfDiscretizeCurves, dxfStarBlocks, dxfMakeBlocks, dxfJoin, dxfRenderPolylineWidth
+ global dxfImportTexts, dxfImportLayouts, dxfImportPoints, dxfImportHatches, dxfUseStandardSize
+ global dxfGetColors, dxfUseDraftVisGroups, dxfMakeFaceMode, dxfBrightBackground, dxfDefaultColor
+ global dxfUseLegacyImporter, dxfExportBlocks, dxfScaling, dxfUseLegacyExporter
- # --- Read all feature and appearance toggles ---
- # These are independent settings and can be read directly.
- dxfDiscretizeCurves = params.get_param("DiscretizeEllipses", False)
- dxfStarBlocks = params.get_param("dxfstarblocks", False)
- dxfJoin = params.get_param("joingeometry", False)
- dxfRenderPolylineWidth = params.get_param("renderPolylineWidth", False)
- dxfImportTexts = params.get_param("dxftext", True)
- dxfImportLayouts = params.get_param("dxflayout", False)
- dxfImportPoints = params.get_param("dxfImportPoints", True)
- dxfImportHatches = params.get_param("importDxfHatches", True)
- dxfUseStandardSize = params.get_param("dxfStdSize", False)
- dxfGetColors = params.get_param("dxfGetOriginalColors", True)
- dxfUseDraftVisGroups = params.get_param("dxfUseDraftVisGroups", True)
- dxfMakeFaceMode = params.get_param("MakeFaceMode", False)
- dxfExportBlocks = params.get_param("dxfExportBlocks", True)
- dxfScaling = params.get_param("dxfScaling", 1.0)
+ # Use the direct C++ API via Python for all parameter access
+ hGrp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft")
- # These control which importer is used. The script should only proceed if
- # the legacy importer is selected.
- dxfUseLegacyImporter = params.get_param("dxfUseLegacyImporter", False)
- dxfUseLegacyExporter = params.get_param("dxfUseLegacyExporter", False)
+ dxfUseLegacyImporter = hGrp.GetBool("dxfUseLegacyImporter", False)
- if not dxfUseLegacyImporter:
- # If the legacy importer is called when not selected, exit.
- # This prevents accidental execution.
- return
+ # This logic is now only needed for the legacy importer.
+ # The modern importer reads its settings directly in C++.
+ if dxfUseLegacyImporter:
+ # Legacy override for sketch creation takes highest priority
+ dxfCreateSketch = hGrp.GetBool("dxfCreateSketch", False)
- # --- New, Centralized Logic for Structural Mode ---
-
- # Read the legacy-specific override for sketch creation.
- dxfCreateSketch = params.get_param("dxfCreateSketch", False)
-
- if dxfCreateSketch:
- # Sketch mode takes highest priority, other modes are irrelevant.
- dxfCreatePart = False
- dxfCreateDraft = False
- dxfMakeBlocks = False
- else:
- # Not in sketch mode, so determine structure from DxfImportMode.
- # This is where the new parameter is read, with its default value defined.
- # 0=Draft, 1=Primitives, 2=Shapes, 3=Fused
- import_mode = params.get_param("DxfImportMode", 2) # Default to "Individual part shapes"
-
- if import_mode == 3: # Fused part shapes
- dxfMakeBlocks = True # 'groupLayers' is the legacy equivalent
- dxfCreatePart = False # In legacy, dxfMakeBlocks overrides these
- dxfCreateDraft = False
- elif import_mode == 0: # Editable draft objects
- dxfMakeBlocks = False
+ if dxfCreateSketch:
dxfCreatePart = False
- dxfCreateDraft = True
- else: # Covers modes 1 (Primitives) and 2 (Shapes). Legacy maps both to "Simple part shapes"
- dxfMakeBlocks = False
- dxfCreatePart = True
dxfCreateDraft = False
+ dxfMakeBlocks = False
+ else:
+ # Read the new unified mode parameter and translate it to the old flags
+ # 0=Draft, 1=Primitives, 2=Shapes, 3=Fused
+ import_mode = hGrp.GetInt("DxfImportMode", 2) # Default to "Individual shapes"
+ if import_mode == 3: # Fused part shapes
+ dxfMakeBlocks = True
+ dxfCreatePart = False
+ dxfCreateDraft = False
+ elif import_mode == 0: # Editable draft objects
+ dxfMakeBlocks = False
+ dxfCreatePart = False
+ dxfCreateDraft = True
+ else: # Individual part shapes or Primitives
+ dxfMakeBlocks = False
+ dxfCreatePart = True
+ dxfCreateDraft = False
+
+ # The legacy importer still uses these global variables, so we read them all.
+ dxfDiscretizeCurves = hGrp.GetBool("DiscretizeEllipses", True)
+ dxfStarBlocks = hGrp.GetBool("dxfstarblocks", False)
+ dxfJoin = hGrp.GetBool("joingeometry", False)
+ dxfRenderPolylineWidth = hGrp.GetBool("renderPolylineWidth", False)
+ dxfImportTexts = hGrp.GetBool("dxftext", False)
+ dxfImportLayouts = hGrp.GetBool("dxflayout", False)
+ dxfImportPoints = hGrp.GetBool("dxfImportPoints", True)
+ dxfImportHatches = hGrp.GetBool("importDxfHatches", False)
+ dxfUseStandardSize = hGrp.GetBool("dxfStdSize", False)
+ dxfGetColors = hGrp.GetBool("dxfGetOriginalColors", True)
+ dxfUseDraftVisGroups = hGrp.GetBool("dxfUseDraftVisGroups", True)
+ dxfMakeFaceMode = hGrp.GetBool("MakeFaceMode", False)
+ dxfUseLegacyExporter = hGrp.GetBool("dxfUseLegacyExporter", False)
+ dxfExportBlocks = hGrp.GetBool("dxfExportBlocks", True)
+ dxfScaling = hGrp.GetFloat("dxfScaling", 1.0)
- # --- Other settings that are not checkboxes ---
dxfBrightBackground = isBrightBackground()
dxfDefaultColor = getColor()
-
class DxfImportReporter:
"""Formats and reports statistics from a DXF import process."""
def __init__(self, filename, stats_dict, total_time=0.0):
diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.cpp b/src/Mod/Import/App/dxf/ImpExpDxf.cpp
index 3319fe27a2..3ad107dc58 100644
--- a/src/Mod/Import/App/dxf/ImpExpDxf.cpp
+++ b/src/Mod/Import/App/dxf/ImpExpDxf.cpp
@@ -53,6 +53,7 @@
#include
#endif
+#include
#include
#include
#include
@@ -80,6 +81,35 @@ using namespace Import;
using BRepAdaptor_HCurve = BRepAdaptor_Curve;
#endif
+std::map ImpExpDxfRead::PreScan(const std::string& filepath)
+{
+ std::map counts;
+ std::ifstream ifs(filepath);
+ if (!ifs) {
+ // Could throw an exception or log an error
+ return counts;
+ }
+
+ std::string line;
+ bool next_is_entity_name = false;
+
+ while (std::getline(ifs, line)) {
+ // Simple trim for Windows-style carriage returns
+ if (!line.empty() && line.back() == '\r') {
+ line.pop_back();
+ }
+
+ if (next_is_entity_name) {
+ // The line after a " 0" group code is the entity type
+ counts[line]++;
+ next_is_entity_name = false;
+ }
+ else if (line == " 0") {
+ next_is_entity_name = true;
+ }
+ }
+ return counts;
+}
//******************************************************************************
// reading
diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.h b/src/Mod/Import/App/dxf/ImpExpDxf.h
index 5a1855a651..bdf38a8623 100644
--- a/src/Mod/Import/App/dxf/ImpExpDxf.h
+++ b/src/Mod/Import/App/dxf/ImpExpDxf.h
@@ -61,7 +61,7 @@ public:
{
Py_XDECREF(DraftModule);
}
-
+ static std::map PreScan(const std::string& filepath);
void StartImport() override;
Py::Object getStatsAsPyObject();
diff --git a/src/Mod/Import/Gui/AppImportGuiPy.cpp b/src/Mod/Import/Gui/AppImportGuiPy.cpp
index 0e4dc03c11..7654f1d4e7 100644
--- a/src/Mod/Import/Gui/AppImportGuiPy.cpp
+++ b/src/Mod/Import/Gui/AppImportGuiPy.cpp
@@ -94,6 +94,7 @@ public:
add_keyword_method("insert",
&Module::insert,
"insert(string,string) -- Insert the file into the given document.");
+ add_varargs_method("preScanDxf", &Module::preScanDxf, "preScanDxf(filepath) -> dict");
add_varargs_method("readDXF",
&Module::readDXF,
"readDXF(filename,[document,ignore_errors,option_source]): Imports a "
@@ -112,6 +113,26 @@ public:
}
private:
+ Py::Object preScanDxf(const Py::Tuple& args)
+ {
+ char* filepath_char = nullptr;
+ if (!PyArg_ParseTuple(args.ptr(), "et", "utf-8", &filepath_char)) {
+ throw Py::Exception();
+ }
+ std::string filepath(filepath_char);
+ PyMem_Free(filepath_char);
+
+#include
+
+ std::map counts = Import::ImpExpDxfRead::PreScan(filepath);
+
+ Py::Dict result;
+ for (const auto& pair : counts) {
+ result.setItem(Py::String(pair.first), Py::Long(pair.second));
+ }
+ return result;
+ }
+
Py::Object importOptions(const Py::Tuple& args)
{
char* Name {};
From 5951d759f8437b118ace1afde19e2bc159276e51 Mon Sep 17 00:00:00 2001
From: Furgo <148809153+furgo16@users.noreply.github.com>
Date: Sat, 28 Jun 2025 07:59:14 +0200
Subject: [PATCH 04/14] Import: DXF, first working version for import as Part
primitives
---
src/Mod/Draft/DxfImportDialog.py | 4 +-
src/Mod/Draft/Resources/ui/preferences-dxf.ui | 18 +-
src/Mod/Draft/importDXF.py | 405 +++++-----
src/Mod/Import/App/dxf/ImpExpDxf.cpp | 731 ++++++++++--------
src/Mod/Import/App/dxf/ImpExpDxf.h | 77 +-
5 files changed, 719 insertions(+), 516 deletions(-)
diff --git a/src/Mod/Draft/DxfImportDialog.py b/src/Mod/Draft/DxfImportDialog.py
index a1c7fec800..c260b6fd92 100644
--- a/src/Mod/Draft/DxfImportDialog.py
+++ b/src/Mod/Draft/DxfImportDialog.py
@@ -76,8 +76,8 @@ class DxfImportDialog:
self.dialog.radio_ImportAs_Shapes.setEnabled(True)
self.dialog.radio_ImportAs_Fused.setEnabled(True)
else:
- self.dialog.radio_ImportAs_Draft.setEnabled(False)
- self.dialog.radio_ImportAs_Primitives.setEnabled(False)
+ self.dialog.radio_ImportAs_Draft.setEnabled(True)
+ self.dialog.radio_ImportAs_Primitives.setEnabled(True)
self.dialog.radio_ImportAs_Shapes.setEnabled(True)
self.dialog.radio_ImportAs_Fused.setEnabled(True)
diff --git a/src/Mod/Draft/Resources/ui/preferences-dxf.ui b/src/Mod/Draft/Resources/ui/preferences-dxf.ui
index c4a78612cd..881e5f5e7c 100644
--- a/src/Mod/Draft/Resources/ui/preferences-dxf.ui
+++ b/src/Mod/Draft/Resources/ui/preferences-dxf.ui
@@ -111,19 +111,19 @@ the 'dxf_library' addon from the Addon Manager.
-
- false
+ 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. (Legacy importer only)
+Workbench.
Editable draft objects (Highest fidelity, slowest)
- DxfImportMode
+ dxfImportAsDraft
Mod/Draft
@@ -139,19 +139,19 @@ Workbench. (Legacy importer only)
-
- false
+ 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. (Not yet implemented)
+script-based post-processing and Part Workbench integration.
Editable part primitives (High fidelity, slower)
- DxfImportMode
+ dxfImportAsPrimitives
Mod/Draft
@@ -178,7 +178,7 @@ objects, maintaining the block structure. Good for referencing and measuring.true
- DxfImportMode
+ dxfImportAsShapes
Mod/Draft
@@ -196,13 +196,13 @@ objects, maintaining the block structure. Good for referencing and measuring.
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.
+shape. Best for importing and viewing very large files with maximum performance.
Fused part shapes (Lowest fidelity, fastest)
- DxfImportMode
+ dxfImportAsFused
Mod/Draft
diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py
index cc67c5292b..4e169752a7 100644
--- a/src/Mod/Draft/importDXF.py
+++ b/src/Mod/Draft/importDXF.py
@@ -2790,12 +2790,117 @@ 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()
+
+ if is_draft_mode:
+ # For Draft mode, we tell the C++ importer to create Part Primitives first.
+ hGrp.SetInt("DxfImportMode", 1)
+
+ # 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
+
+ # Restore the original mode setting if we changed it
+ if is_draft_mode:
+ hGrp.SetInt("DxfImportMode", 0)
+
+ # --- Post-processing step ---
+ if is_draft_mode and newly_created_objects:
+ post_process_to_draft(doc, newly_created_objects)
+
+ 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
+
+# --- REFACTORED open() and insert() functions ---
+
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
----------
@@ -2804,175 +2909,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.
"""
- hGrp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft")
- use_legacy = hGrp.GetBool("dxfUseLegacyImporter", False)
+ doc, stats, start_time, end_time = _import_dxf_file(filename, doc_name=None)
- # The C++ layer (`Gui::Application::importFrom`) has set the WaitCursor.
- # We must temporarily suspend it to show our interactive dialog.
- try:
- if gui:
- FreeCADGui.suspendWaitCursor()
-
- # --- Dialog Workflow ---
- if gui and not use_legacy and hGrp.GetBool("dxfShowDialog", True):
- try:
- import ImportGui
- # This C++ function will need to be created in a later step
- entity_counts = ImportGui.preScanDxf(filename)
- except Exception:
- entity_counts = {}
-
- from DxfImportDialog import DxfImportDialog
- dlg = DxfImportDialog(entity_counts)
-
- if dlg.exec_():
- # User clicked OK, save settings from the dialog
- hGrp.SetInt("DxfImportMode", dlg.get_selected_mode())
- hGrp.SetBool("dxfShowDialog", dlg.get_show_dialog_again())
- else:
- # User clicked Cancel, abort the entire operation
- FCC.PrintLog("DXF import cancelled by user.\n")
- return
- else:
- # If we don't show the dialog, we still need to read preferences
- # to ensure the correct backend logic is triggered.
- readPreferences()
-
- finally:
- # --- CRITICAL: Always resume the wait state before returning to C++ ---
- # This restores the wait cursor and event filter so the subsequent
- # blocking C++ call behaves as expected within the C++ scope.
- if gui:
- FreeCADGui.resumeWaitCursor()
-
- # --- Proceed with the blocking import logic ---
- total_start_time = time.perf_counter()
-
- if use_legacy:
- 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)
- return None
- else: # Modern C++ Importer
- docname = os.path.splitext(os.path.basename(filename))[0]
- doc = FreeCAD.newDocument(docname)
- doc.Label = docname
- FreeCAD.setActiveDocument(doc.Name)
- stats = None
-
- if gui:
- import ImportGui
- stats = ImportGui.readDXF(filename)
- else:
- import Import
- stats = Import.readDXF(filename)
-
- Draft.convert_draft_texts()
- doc.recompute()
-
- total_end_time = time.perf_counter()
- if stats:
- # Report PROCESSING time only, not user dialog time.
- reporter = DxfImportReporter(filename, stats, total_end_time - total_start_time)
- reporter.report_to_console()
-
- return doc
+ 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.
"""
- hGrp = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Draft")
- use_legacy = hGrp.GetBool("dxfUseLegacyImporter", False)
+ doc, stats, start_time, end_time = _import_dxf_file(filename, doc_name=docname)
- try:
- if gui:
- FreeCADGui.suspendWaitCursor()
+ if doc and stats:
+ reporter = DxfImportReporter(filename, stats, end_time - start_time)
+ reporter.report_to_console()
- # --- Dialog Workflow ---
- if gui and not use_legacy and hGrp.GetBool("dxfShowDialog", True):
- try:
- import ImportGui
- entity_counts = ImportGui.preScanDxf(filename)
- except Exception:
- entity_counts = {}
-
- from DxfImportDialog import DxfImportDialog
- dlg = DxfImportDialog(entity_counts)
-
- if dlg.exec_():
- hGrp.SetInt("DxfImportMode", dlg.get_selected_mode())
- hGrp.SetBool("dxfShowDialog", dlg.get_show_dialog_again())
- else:
- FCC.PrintLog("DXF insert cancelled by user.\n")
- return
- else:
- readPreferences()
-
- finally:
- if gui:
- FreeCADGui.resumeWaitCursor()
-
- # --- Proceed with the blocking insert logic ---
- total_start_time = time.perf_counter()
-
- try:
- doc = FreeCAD.getDocument(docname)
- except NameError:
- doc = FreeCAD.newDocument(docname)
- FreeCAD.setActiveDocument(docname)
-
- if use_legacy:
- getDXFlibs()
- if dxfReader:
- processdxf(doc, filename)
- else:
- errorDXFLib(gui)
- else: # Modern C++ Importer
- stats = None
- if gui:
- import ImportGui
- stats = ImportGui.readDXF(filename)
- else:
- import Import
- stats = Import.readDXF(filename)
-
- Draft.convert_draft_texts()
- doc.recompute()
-
- total_end_time = time.perf_counter()
- if stats:
- reporter = DxfImportReporter(filename, stats, total_end_time - total_start_time)
- reporter.report_to_console()
def getShapes(filename):
"""Read a DXF file, and return a list of shapes from its contents.
@@ -4274,32 +4242,46 @@ def readPreferences():
dxfUseLegacyImporter = hGrp.GetBool("dxfUseLegacyImporter", False)
- # This logic is now only needed for the legacy importer.
+ # 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:
+ if dxfCreateSketch: # dxfCreateSketch overrides the import mode for the legacy importer
dxfCreatePart = False
dxfCreateDraft = False
dxfMakeBlocks = False
- else:
- # Read the new unified mode parameter and translate it to the old flags
- # 0=Draft, 1=Primitives, 2=Shapes, 3=Fused
- import_mode = hGrp.GetInt("DxfImportMode", 2) # Default to "Individual shapes"
- if import_mode == 3: # Fused part shapes
- dxfMakeBlocks = True
- dxfCreatePart = False
- dxfCreateDraft = False
- elif import_mode == 0: # Editable draft objects
- dxfMakeBlocks = False
- dxfCreatePart = False
- dxfCreateDraft = True
- else: # Individual part shapes or Primitives
- dxfMakeBlocks = False
- dxfCreatePart = True
- dxfCreateDraft = False
+ # The '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)
@@ -4321,6 +4303,79 @@ def readPreferences():
dxfBrightBackground = isBrightBackground()
dxfDefaultColor = getColor()
+
+def post_process_to_draft(doc, new_objects):
+ """
+ Converts a list of newly created Part primitives and placeholders
+ into their corresponding Draft objects.
+ """
+ if not new_objects:
+ return
+
+ FCC.PrintMessage("Post-processing {} objects to Draft types...\n".format(len(new_objects)))
+
+ objects_to_delete = []
+
+ for obj in list(new_objects): # Iterate over a copy
+ if App.isdeleted(obj):
+ continue
+
+ if obj.isDerivedFrom("Part::Feature"):
+ # Handles Part::Vertex, Part::Line, Part::Circle, Part::Compound,
+ # and Part::Features containing Ellipses/Splines.
+ try:
+ Draft.upgrade([obj], delete=True)
+ except Exception as e:
+ FCC.PrintWarning("Could not upgrade {} to Draft object: {}\n".format(obj.Label, str(e)))
+
+ elif obj.isDerivedFrom("App::FeaturePython") and hasattr(obj, "DxfEntityType"):
+ # This is one of our placeholders
+ entity_type = obj.DxfEntityType
+
+ if entity_type == "DIMENSION":
+ try:
+ # 1. Create an empty Draft Dimension
+ dim = doc.addObject("App::FeaturePython", "Dimension")
+ Draft.Dimension(dim)
+ if gui:
+ from Draft import _ViewProviderDimension
+ _ViewProviderDimension(dim.ViewObject)
+
+ # 2. Copy properties directly from the placeholder
+ dim.Start = obj.Start
+ dim.End = obj.End
+ dim.Dimline = obj.Dimline
+ dim.Placement = obj.Placement
+
+ objects_to_delete.append(obj)
+ except Exception as e:
+ FCC.PrintWarning("Could not create Draft Dimension from {}: {}\n".format(obj.Label, str(e)))
+
+ elif entity_type == "TEXT":
+ try:
+ # 1. Create a Draft Text object
+ text_obj = Draft.make_text(obj.Text)
+
+ # 2. Copy properties
+ text_obj.Placement = obj.Placement
+ if gui:
+ # TEXTSCALING is a global defined at the top of importDXF.py
+ text_obj.ViewObject.FontSize = obj.DxfTextHeight * TEXTSCALING
+
+ objects_to_delete.append(obj)
+ except Exception as e:
+ FCC.PrintWarning("Could not create Draft Text from {}: {}\n".format(obj.Label, str(e)))
+
+ # Perform the deletion of placeholders after the loop
+ for obj in objects_to_delete:
+ try:
+ doc.removeObject(obj.Name)
+ except Exception:
+ pass
+
+ doc.recompute()
+
+
class DxfImportReporter:
"""Formats and reports statistics from a DXF import process."""
def __init__(self, filename, stats_dict, total_time=0.0):
diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.cpp b/src/Mod/Import/App/dxf/ImpExpDxf.cpp
index 3ad107dc58..5bb8c3f4cb 100644
--- a/src/Mod/Import/App/dxf/ImpExpDxf.cpp
+++ b/src/Mod/Import/App/dxf/ImpExpDxf.cpp
@@ -28,28 +28,37 @@
#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
#endif
@@ -66,10 +75,12 @@
#include
#include
#include
-#include
#include
#include
+#include
+#include
#include
+#include
#include
#include "ImpExpDxf.h"
@@ -81,6 +92,81 @@ 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);
+
+} // namespace
+
+namespace
+{
+
+// 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;
+}
+
+} // namespace
+
std::map ImpExpDxfRead::PreScan(const std::string& filepath)
{
std::map counts;
@@ -136,17 +222,6 @@ void ImpExpDxfRead::StartImport()
bool ImpExpDxfRead::ReadEntitiesSection()
{
- // TODO: remove this once the unsupported modes have been implemented.
- // Perform a one-time check for unsupported modes
- if (m_importMode == ImportMode::EditableDraft) {
- UnsupportedFeature("Import as 'Editable draft objects' is not yet implemented.");
- // We can continue, and the switch statements below will do nothing,
- // resulting in an empty import for geometry, which is correct behavior.
- }
- else if (m_importMode == ImportMode::EditablePrimitives) {
- UnsupportedFeature("Import as 'Editable part primitives' is not yet implemented.");
- }
-
// After parsing the BLOCKS section, compose all block definitions
// into FreeCAD objects before processing the ENTITIES section.
ComposeBlocks();
@@ -284,8 +359,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.
@@ -351,8 +428,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);
@@ -360,7 +436,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) {
@@ -387,96 +463,103 @@ 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;
+
+ for (const auto& builder : builderList) {
+ App::DocumentObject* newObject = nullptr;
+ switch (builder.type) {
+ case GeometryBuilder::PrimitiveType::None: {
+ auto* p = document->addObject("Shape");
+ p->Shape.setValue(builder.shape);
+ newObject = p;
+ 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: {
+ auto* p = document->addObject("Point");
+ TopoDS_Vertex v = TopoDS::Vertex(builder.shape);
+ gp_Pnt pnt = BRep_Tool::Pnt(v);
+ p->Placement.setValue(Base::Placement(Base::Vector3d(pnt.X(), pnt.Y(), pnt.Z()),
+ Base::Rotation()));
+ newObject = p;
+ 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::Line: {
+ newObject = createLinePrimitive(TopoDS::Edge(builder.shape), document, "Line");
+ 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; // Helper function failed
}
- }
- else if (shape.ShapeType() == TopAbs_VERTEX) {
- type_suffix = "Vertex";
- }
- else if (shape.ShapeType() == TopAbs_WIRE) {
- type_suffix = "Wire";
- }
- else if (shape.ShapeType() == TopAbs_FACE) {
- type_suffix = "Face";
- }
- else if (shape.ShapeType() == TopAbs_SHELL) {
- type_suffix = "Shell";
- }
- else if (shape.ShapeType() == TopAbs_SOLID) {
- type_suffix = "Solid";
- }
- else if (shape.ShapeType() == TopAbs_COMPOUND) {
- type_suffix = "Compound";
- }
- 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());
+ // For full circles, ensure the angles span the full 360 degrees
+ if (builder.type == GeometryBuilder::PrimitiveType::Circle) {
+ p->Angle1.setValue(0.0);
+ p->Angle2.setValue(360.0);
+ }
+ newObject = p;
+ break;
+ }
+ case GeometryBuilder::PrimitiveType::PolylineCompound: {
+ auto* p = document->addObject("Polyline");
+ std::vector segments;
+ TopExp_Explorer explorer(builder.shape, TopAbs_EDGE);
+ for (; explorer.More(); explorer.Next()) {
+ TopoDS_Edge edge = TopoDS::Edge(explorer.Current());
+ BRepAdaptor_Curve adaptor(edge);
+ App::DocumentObject* segment = nullptr;
+ if (adaptor.GetType() == GeomAbs_Line) {
+ segment = createLinePrimitive(edge, document, "Segment");
+ }
+ else if (adaptor.GetType() == GeomAbs_Circle) {
+ auto* arc = createCirclePrimitive(edge, document, "Arc");
+ segment = arc;
+ }
+ if (segment) {
+ IncrementCreatedObjectCount();
+ segment->Visibility.setValue(false);
+ ApplyGuiStyles(static_cast(segment));
+ segments.push_back(segment);
+ }
+ }
+ p->Links.setValues(segments);
+ newObject = p;
+ break;
+ }
+ case GeometryBuilder::PrimitiveType::Spline:
+ case GeometryBuilder::PrimitiveType::Ellipse:
+ default:
+ // Fallback for types without a specific primitive
+ auto* p = document->addObject("Shape");
+ p->Shape.setValue(builder.shape);
+ newObject = p;
+ break;
+ }
+
+ 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);
+ ApplyGuiStyles(static_cast(newObject));
+ childObjects.push_back(newObject);
}
}
}
- // 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.
@@ -487,7 +570,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()) {
@@ -612,8 +695,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
@@ -632,24 +714,17 @@ void ImpExpDxfRead::OnReadLine(const Base::Vector3d& start,
return;
}
- switch (m_importMode) {
- case ImportMode::IndividualShapes:
- case ImportMode::FusedShapes: {
- gp_Pnt p0 = makePoint(start);
- gp_Pnt p1 = makePoint(end);
- // TODO: Really?? What about the people designing integrated circuits?
- if (p0.IsEqual(p1, 1e-8)) {
- return;
- }
- Collector->AddObject(BRepBuilderAPI_MakeEdge(p0, p1).Edge(), "Line");
- break;
- }
- case ImportMode::EditableDraft:
- case ImportMode::EditablePrimitives:
- // Do nothing until these modes have been implemented, the one-time warning has already
- // been issued.
- break;
+ gp_Pnt p0 = makePoint(start);
+ gp_Pnt p1 = makePoint(end);
+ if (p0.IsEqual(p1, 1e-8)) {
+ return;
}
+ TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(p0, p1).Edge();
+ GeometryBuilder builder(edge);
+ if (m_importMode == ImportMode::EditablePrimitives) {
+ builder.type = GeometryBuilder::PrimitiveType::Line;
+ }
+ Collector->AddGeometry(builder);
}
@@ -658,20 +733,13 @@ void ImpExpDxfRead::OnReadPoint(const Base::Vector3d& start)
if (shouldSkipEntity()) {
return;
}
+ TopoDS_Vertex vertex = BRepBuilderAPI_MakeVertex(makePoint(start)).Vertex();
+ GeometryBuilder builder(vertex);
- switch (m_importMode) {
- case ImportMode::IndividualShapes:
- case ImportMode::FusedShapes: {
- // For non-parametric modes, create a Part::Feature with a Vertex shape.
- Collector->AddObject(BRepBuilderAPI_MakeVertex(makePoint(start)).Vertex(), "Point");
- break;
- }
- case ImportMode::EditableDraft:
- case ImportMode::EditablePrimitives:
- // Do nothing until these modes have been implemented, the one-time warning has already
- // been issued.
- break;
+ if (m_importMode == ImportMode::EditablePrimitives) {
+ builder.type = GeometryBuilder::PrimitiveType::Point;
}
+ Collector->AddGeometry(builder);
}
@@ -685,31 +753,25 @@ void ImpExpDxfRead::OnReadArc(const Base::Vector3d& start,
return;
}
- switch (m_importMode) {
- case ImportMode::IndividualShapes:
- case ImportMode::FusedShapes: {
- gp_Pnt p0 = makePoint(start);
- gp_Pnt p1 = makePoint(end);
- gp_Dir up(0, 0, 1);
- if (!dir) {
- up.Reverse();
- }
- gp_Pnt pc = makePoint(center);
- gp_Circ circle(gp_Ax2(pc, up), p0.Distance(pc));
- if (circle.Radius() > 1e-9) {
- Collector->AddObject(BRepBuilderAPI_MakeEdge(circle, p0, p1).Edge(), "Arc");
- }
- else {
- Base::Console().warning("ImpExpDxf - ignore degenerate arc of circle\n");
- }
- break;
- }
- case ImportMode::EditableDraft:
- case ImportMode::EditablePrimitives:
- // Do nothing until these modes have been implemented, the one-time warning has already
- // been issued.
- break;
+ gp_Pnt p0 = makePoint(start);
+ gp_Pnt p1 = makePoint(end);
+ gp_Dir up(0, 0, 1);
+ if (!dir) {
+ up.Reverse();
}
+ gp_Pnt pc = makePoint(center);
+ gp_Circ circle(gp_Ax2(pc, up), p0.Distance(pc));
+ 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);
+ if (m_importMode == ImportMode::EditablePrimitives) {
+ builder.type = GeometryBuilder::PrimitiveType::Arc;
+ }
+ Collector->AddGeometry(builder);
}
@@ -722,28 +784,24 @@ void ImpExpDxfRead::OnReadCircle(const Base::Vector3d& start,
return;
}
- switch (m_importMode) {
- case ImportMode::IndividualShapes:
- case ImportMode::FusedShapes: {
- gp_Pnt p0 = makePoint(start);
- gp_Dir up(0, 0, 1);
- if (!dir) {
- up.Reverse();
- }
- gp_Pnt pc = makePoint(center);
- gp_Circ circle(gp_Ax2(pc, up), p0.Distance(pc));
- if (circle.Radius() > 1e-9) {
- Collector->AddObject(BRepBuilderAPI_MakeEdge(circle).Edge(), "Circle");
- }
- else {
- Base::Console().warning("ImpExpDxf - ignore degenerate circle\n");
- }
- break;
- }
- case ImportMode::EditableDraft:
- case ImportMode::EditablePrimitives:
- break;
+ gp_Pnt p0 = makePoint(start);
+ gp_Dir up(0, 0, 1);
+ if (!dir) {
+ up.Reverse();
}
+ gp_Pnt pc = makePoint(center);
+ gp_Circ circle(gp_Ax2(pc, up), p0.Distance(pc));
+ if (circle.Radius() < 1e-9) {
+ Base::Console().warning("ImpExpDxf - ignore degenerate circle\n");
+ return;
+ }
+
+ TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(circle).Edge();
+ GeometryBuilder builder(edge);
+ if (m_importMode == ImportMode::EditablePrimitives) {
+ builder.type = GeometryBuilder::PrimitiveType::Circle;
+ }
+ Collector->AddGeometry(builder);
}
@@ -847,32 +905,25 @@ void ImpExpDxfRead::OnReadSpline(struct SplineData& sd)
return;
}
- switch (m_importMode) {
- case ImportMode::IndividualShapes:
- case ImportMode::FusedShapes: {
- try {
- Handle(Geom_BSplineCurve) geom;
- if (sd.control_points > 0) {
- geom = getSplineFromPolesAndKnots(sd);
- }
- else if (sd.fit_points > 0) {
- geom = getInterpolationSpline(sd);
- }
-
- if (geom.IsNull()) {
- throw Standard_Failure();
- }
-
- Collector->AddObject(BRepBuilderAPI_MakeEdge(geom).Edge(), "Spline");
- }
- catch (const Standard_Failure&) {
- Base::Console().warning("ImpExpDxf - failed to create bspline\n");
- }
- break;
+ try {
+ Handle(Geom_BSplineCurve) geom;
+ if (sd.control_points > 0) {
+ geom = getSplineFromPolesAndKnots(sd);
}
- case ImportMode::EditableDraft:
- case ImportMode::EditablePrimitives:
- break;
+ else if (sd.fit_points > 0) {
+ geom = getInterpolationSpline(sd);
+ }
+
+ if (!geom.IsNull()) {
+ GeometryBuilder builder(BRepBuilderAPI_MakeEdge(geom).Edge());
+ if (m_importMode == ImportMode::EditablePrimitives) {
+ builder.type = GeometryBuilder::PrimitiveType::Spline;
+ }
+ Collector->AddGeometry(builder);
+ }
+ }
+ catch (const Standard_Failure&) {
+ Base::Console().warning("ImpExpDxf - failed to create bspline\n");
}
}
@@ -890,28 +941,23 @@ void ImpExpDxfRead::OnReadEllipse(const Base::Vector3d& center,
return;
}
- switch (m_importMode) {
- case ImportMode::IndividualShapes:
- case ImportMode::FusedShapes: {
- gp_Dir up(0, 0, 1);
- if (!dir) {
- 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() > 1e-9) {
- Collector->AddObject(BRepBuilderAPI_MakeEdge(ellipse).Edge(), "Ellipse");
- }
- else {
- Base::Console().warning("ImpExpDxf - ignore degenerate ellipse\n");
- }
- break;
- }
- case ImportMode::EditableDraft:
- case ImportMode::EditablePrimitives:
- break;
+ gp_Dir up(0, 0, 1);
+ if (!dir) {
+ 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() < 1e-9) {
+ Base::Console().warning("ImpExpDxf - ignore degenerate ellipse\n");
+ return;
+ }
+
+ GeometryBuilder builder(BRepBuilderAPI_MakeEdge(ellipse).Edge());
+ if (m_importMode == ImportMode::EditablePrimitives) {
+ builder.type = GeometryBuilder::PrimitiveType::Ellipse;
+ }
+ Collector->AddGeometry(builder);
}
void ImpExpDxfRead::OnReadText(const Base::Vector3d& point,
@@ -919,41 +965,36 @@ void ImpExpDxfRead::OnReadText(const Base::Vector3d& point,
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");
}
}
@@ -979,82 +1020,150 @@ void ImpExpDxfRead::OnReadDimension(const Base::Vector3d& start,
const Base::Vector3d& point,
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::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;
}
- // Polyline explosion logic is complex and calls back to other OnRead... handlers.
- // The mode switch should happen inside the final geometry creation handlers
- // (OnReadLine, OnReadArc), so this function doesn't need its own switch statement.
- // It simply acts as a dispatcher.
-
- 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");
+
+ BRepBuilderAPI_MakeWire wireBuilder;
+ bool is_closed = ((flags & 1) != 0);
+ 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);
+ }
+ }
+
+ if (wireBuilder.IsDone()) {
+ TopoDS_Wire wire = wireBuilder.Wire();
+ GeometryBuilder builder(wire);
+
+ // For FusedShapes mode, we can create the object immediately.
+ // For other modes, we store the builder for later processing.
+ if (m_importMode == ImportMode::FusedShapes) {
+ Collector->AddObject(wire, "Polyline");
+ return;
+ }
+
+ if (m_importMode == ImportMode::EditablePrimitives) {
+ builder.type = GeometryBuilder::PrimitiveType::PolylineCompound;
+ }
+
+ Collector->AddGeometry(builder);
}
}
diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.h b/src/Mod/Import/App/dxf/ImpExpDxf.h
index bdf38a8623..002645f811 100644
--- a/src/Mod/Import/App/dxf/ImpExpDxf.h
+++ b/src/Mod/Import/App/dxf/ImpExpDxf.h
@@ -117,6 +117,29 @@ 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,
+ PolylineCompound
+ };
+
+ // 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
{
@@ -207,9 +230,7 @@ protected:
{}
const std::string Name;
const int Flags;
- std::map> Shapes;
- std::map>
- FeatureBuildersList;
+ std::map> GeometryBuilders;
std::map> Inserts;
};
@@ -259,6 +280,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.
@@ -288,6 +311,12 @@ protected:
{}
void AddObject(const TopoDS_Shape& shape, const char* nameBase) override;
+ void AddGeometry(const GeometryBuilder& builder) override
+ {
+ // In drawing mode, we create objects immediately based on the builder.
+ // For now, this just creates simple shapes. Primitives would need more logic here.
+ AddObject(builder.shape, "Shape");
+ }
void AddObject(App::DocumentObject* obj, const char* nameBase) override;
void AddObject(FeaturePythonBuilder shapeBuilder) override;
void AddInsert(const Base::Vector3d& point,
@@ -351,6 +380,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.
@@ -388,33 +422,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,
@@ -427,9 +468,7 @@ protected:
}
private:
- std::map>& ShapesList;
- std::map>&
- FeatureBuildersList;
+ std::map>& BuildersList;
std::map>& InsertsList;
};
@@ -507,4 +546,4 @@ protected:
} // namespace Import
-#endif // IMPEXPDXF_H
+#endif // IMPEXPDXFGUI_H
From a295f9dd5c6a2694764989237ffe96787ebb0ab1 Mon Sep 17 00:00:00 2001
From: Furgo <148809153+furgo16@users.noreply.github.com>
Date: Tue, 1 Jul 2025 01:09:53 +0200
Subject: [PATCH 05/14] Import: DXF, first working version of Draft objects
import
---
src/Mod/Draft/importDXF.py | 506 ++++++++++++++++++++-----
src/Mod/Import/App/dxf/ImpExpDxf.cpp | 532 +++++++++++++++++++--------
src/Mod/Import/App/dxf/ImpExpDxf.h | 15 +-
3 files changed, 809 insertions(+), 244 deletions(-)
diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py
index 4e169752a7..5c9fe5d1ba 100644
--- a/src/Mod/Draft/importDXF.py
+++ b/src/Mod/Draft/importDXF.py
@@ -79,6 +79,12 @@ if gui:
draftui = FreeCADGui.draftToolBar
except (AttributeError, NameError):
draftui = None
+ try:
+ from draftviewproviders.view_base import ViewProviderDraft
+ from draftviewproviders.view_wire import ViewProviderWire
+ except ImportError:
+ ViewProviderDraft = None
+ ViewProviderWire = None
from draftutils.translate import translate
from PySide import QtWidgets
else:
@@ -2849,10 +2855,6 @@ def _import_dxf_file(filename, doc_name=None):
# --- Core Import Execution ---
processing_start_time = time.perf_counter()
- if is_draft_mode:
- # For Draft mode, we tell the C++ importer to create Part Primitives first.
- hGrp.SetInt("DxfImportMode", 1)
-
# Take snapshot of objects before import
objects_before = set(doc.Objects)
@@ -2876,13 +2878,10 @@ def _import_dxf_file(filename, doc_name=None):
objects_after = set(doc.Objects)
newly_created_objects = objects_after - objects_before
- # Restore the original mode setting if we changed it
- if is_draft_mode:
- hGrp.SetInt("DxfImportMode", 0)
-
# --- Post-processing step ---
if is_draft_mode and newly_created_objects:
- post_process_to_draft(doc, newly_created_objects)
+ draft_postprocessor = DxfDraftPostProcessor(doc, newly_created_objects)
+ draft_postprocessor.run()
Draft.convert_draft_texts() # This is a general utility that should run for both importers
doc.recompute()
@@ -2892,7 +2891,6 @@ def _import_dxf_file(filename, doc_name=None):
# Return the results for the reporter
return doc, stats, processing_start_time, processing_end_time
-# --- REFACTORED open() and insert() functions ---
def open(filename):
"""Open a file and return a new document.
@@ -4211,8 +4209,6 @@ def getViewDXF(view):
return block, insert
-# In src/Mod/Draft/importDXF.py
-
def readPreferences():
"""Read the preferences of the this module from the parameter database.
@@ -4303,79 +4299,6 @@ def readPreferences():
dxfBrightBackground = isBrightBackground()
dxfDefaultColor = getColor()
-
-def post_process_to_draft(doc, new_objects):
- """
- Converts a list of newly created Part primitives and placeholders
- into their corresponding Draft objects.
- """
- if not new_objects:
- return
-
- FCC.PrintMessage("Post-processing {} objects to Draft types...\n".format(len(new_objects)))
-
- objects_to_delete = []
-
- for obj in list(new_objects): # Iterate over a copy
- if App.isdeleted(obj):
- continue
-
- if obj.isDerivedFrom("Part::Feature"):
- # Handles Part::Vertex, Part::Line, Part::Circle, Part::Compound,
- # and Part::Features containing Ellipses/Splines.
- try:
- Draft.upgrade([obj], delete=True)
- except Exception as e:
- FCC.PrintWarning("Could not upgrade {} to Draft object: {}\n".format(obj.Label, str(e)))
-
- elif obj.isDerivedFrom("App::FeaturePython") and hasattr(obj, "DxfEntityType"):
- # This is one of our placeholders
- entity_type = obj.DxfEntityType
-
- if entity_type == "DIMENSION":
- try:
- # 1. Create an empty Draft Dimension
- dim = doc.addObject("App::FeaturePython", "Dimension")
- Draft.Dimension(dim)
- if gui:
- from Draft import _ViewProviderDimension
- _ViewProviderDimension(dim.ViewObject)
-
- # 2. Copy properties directly from the placeholder
- dim.Start = obj.Start
- dim.End = obj.End
- dim.Dimline = obj.Dimline
- dim.Placement = obj.Placement
-
- objects_to_delete.append(obj)
- except Exception as e:
- FCC.PrintWarning("Could not create Draft Dimension from {}: {}\n".format(obj.Label, str(e)))
-
- elif entity_type == "TEXT":
- try:
- # 1. Create a Draft Text object
- text_obj = Draft.make_text(obj.Text)
-
- # 2. Copy properties
- text_obj.Placement = obj.Placement
- if gui:
- # TEXTSCALING is a global defined at the top of importDXF.py
- text_obj.ViewObject.FontSize = obj.DxfTextHeight * TEXTSCALING
-
- objects_to_delete.append(obj)
- except Exception as e:
- FCC.PrintWarning("Could not create Draft Text from {}: {}\n".format(obj.Label, str(e)))
-
- # Perform the deletion of placeholders after the loop
- for obj in objects_to_delete:
- try:
- doc.removeObject(obj.Name)
- except Exception:
- pass
-
- doc.recompute()
-
-
class DxfImportReporter:
"""Formats and reports statistics from a DXF import process."""
def __init__(self, filename, stats_dict, total_time=0.0):
@@ -4505,3 +4428,416 @@ class DxfImportReporter:
"""
output_string = self.to_console_string()
FCC.PrintMessage(output_string)
+
+
+def post_process_to_draft(doc, new_objects):
+ """
+ Entry point for the DXF post-processing workflow.
+ Instantiates and runs the DxfDraftPostProcessor.
+ """
+ processor = DxfDraftPostProcessor(doc, new_objects)
+ processor.run()
+
+
+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):
+ self.doc = doc
+ self.all_imported_objects = new_objects
+ 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() and child.isDerivedFrom("Part::Feature")
+ ]
+
+ 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"):
+ 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).
+ """
+ # 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.
+ 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")
+
+ FCC.PrintMessage(f"DEBUG: Final angles after assignment: {new_obj.FirstAngle.Value} deg to {new_obj.LastAngle.Value} deg\n")
+
+ # 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"
+
+ # --- 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
+
+ FCC.PrintMessage(f"DEBUG: Input is Part::Feature (ShapeType: {shape.ShapeType})\n")
+
+ # 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.
+ 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.
+ 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":
+ dim = self.doc.addObject("App::FeaturePython", "Dimension")
+ _Dimension(dim)
+ dim.addExtension("Part::AttachExtensionPython")
+ dim.Start = placeholder.Start
+ dim.End = placeholder.End
+ dim.Dimline = placeholder.Dimline
+ dim.Placement = placeholder.Placement
+ new_obj = dim
+ 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:
+ pass
+
+ 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(original_children)
+
+ # Process top-level geometry
+ for part_obj in top_geo:
+ self._create_and_parent_geometry(part_obj)
+ self.all_originals_to_delete.update(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 5bb8c3f4cb..ed01d65a2c 100644
--- a/src/Mod/Import/App/dxf/ImpExpDxf.cpp
+++ b/src/Mod/Import/App/dxf/ImpExpDxf.cpp
@@ -167,6 +167,137 @@ Part::Line* createLinePrimitive(const TopoDS_Edge& edge, App::Document* doc, con
} // 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();
+}
+
+void ImpExpDxfRead::CreateFlattenedPolyline(const TopoDS_Wire& wire, const char* name)
+{
+ auto* p = document->addObject(document->getUniqueObjectName(name).c_str());
+ p->Shape.setValue(wire);
+ Collector->AddObject(p, name);
+}
+
+void ImpExpDxfRead::CreateParametricPolyline(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);
+ ApplyGuiStyles(static_cast(segment));
+ segments.push_back(segment);
+ }
+ }
+ p->Links.setValues(segments);
+
+ Collector->AddObject(p, name);
+}
+
std::map ImpExpDxfRead::PreScan(const std::string& filepath)
{
std::map counts;
@@ -470,15 +601,14 @@ void ImpExpDxfRead::ComposeParametricBlock(const std::string& blockName,
// 5. Create and Link Primitive Geometry from the collected builders.
for (const auto& [attributes, builderList] : blockData.GeometryBuilders) {
- this->m_entityAttributes = attributes;
+ this->m_entityAttributes = attributes; // Set attributes for layer/color handling
for (const auto& builder : builderList) {
App::DocumentObject* newObject = nullptr;
switch (builder.type) {
- case GeometryBuilder::PrimitiveType::None: {
- auto* p = document->addObject("Shape");
- p->Shape.setValue(builder.shape);
- newObject = p;
+ // Existing cases for other primitives
+ case GeometryBuilder::PrimitiveType::Line: {
+ newObject = createLinePrimitive(TopoDS::Edge(builder.shape), document, "Line");
break;
}
case GeometryBuilder::PrimitiveType::Point: {
@@ -490,20 +620,14 @@ void ImpExpDxfRead::ComposeParametricBlock(const std::string& blockName,
newObject = p;
break;
}
- case GeometryBuilder::PrimitiveType::Line: {
- newObject = createLinePrimitive(TopoDS::Edge(builder.shape), document, "Line");
- 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; // Helper function failed
+ break;
}
-
- // For full circles, ensure the angles span the full 360 degrees
if (builder.type == GeometryBuilder::PrimitiveType::Circle) {
p->Angle1.setValue(0.0);
p->Angle2.setValue(360.0);
@@ -511,14 +635,43 @@ void ImpExpDxfRead::ComposeParametricBlock(const std::string& blockName,
newObject = p;
break;
}
- case GeometryBuilder::PrimitiveType::PolylineCompound: {
- auto* p = document->addObject("Polyline");
+ case GeometryBuilder::PrimitiveType::Ellipse: {
+ // Ellipses are generic Part::Feature as no Part primitive exists
+ auto* p = document->addObject("Ellipse");
+ p->Shape.setValue(builder.shape);
+ newObject = p;
+ break;
+ }
+ 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;
+ }
+ // NEW CASES FOR POLYLINES IN BLOCKS
+ case GeometryBuilder::PrimitiveType::PolylineFlattened: {
+ // This creates a simple Part::Feature wrapping the wire, which is standard for
+ // block children.
+ auto* p = document->addObject("Polyline");
+ p->Shape.setValue(TopoDS::Wire(builder.shape)); // Ensure it's a TopoDS_Wire
+ newObject = p;
+ break;
+ }
+ case GeometryBuilder::PrimitiveType::PolylineParametric: {
+ // REUSED ORIGINAL LOGIC for parametric polylines inside blocks.
+ // This creates a Part::Compound containing line/arc segments.
+ auto* p =
+ document->addObject("Polyline"); // Main polyline compound
std::vector segments;
- TopExp_Explorer explorer(builder.shape, TopAbs_EDGE);
+ TopExp_Explorer explorer(TopoDS::Wire(builder.shape),
+ TopAbs_EDGE); // Iterate edges of the wire
+
for (; explorer.More(); explorer.Next()) {
TopoDS_Edge edge = TopoDS::Edge(explorer.Current());
- BRepAdaptor_Curve adaptor(edge);
App::DocumentObject* segment = nullptr;
+ BRepAdaptor_Curve adaptor(edge);
+
if (adaptor.GetType() == GeomAbs_Line) {
segment = createLinePrimitive(edge, document, "Segment");
}
@@ -528,31 +681,38 @@ void ImpExpDxfRead::ComposeParametricBlock(const std::string& blockName,
}
if (segment) {
- IncrementCreatedObjectCount();
- segment->Visibility.setValue(false);
- ApplyGuiStyles(static_cast(segment));
+ // These segments are children of the polyline compound, not top-level
+ // block children.
+ IncrementCreatedObjectCount(); // Count this sub-object
+ segment->Visibility.setValue(false); // Sub-segments are usually hidden
+ // No layer/style needed here, inherited from the polyline compound or
+ // handled by block.
+ // ApplyGuiStyles(static_cast(segment));
segments.push_back(segment);
}
}
- p->Links.setValues(segments);
- newObject = p;
+ p->Links.setValues(segments); // Link segments to the polyline compound
+ newObject = p; // The polyline compound itself is the new object for the block
break;
}
- case GeometryBuilder::PrimitiveType::Spline:
- case GeometryBuilder::PrimitiveType::Ellipse:
- default:
- // Fallback for types without a specific primitive
+ case GeometryBuilder::PrimitiveType::None: // Default/fallback if not handled
+ default: {
+ // Generic shape, e.g., 3DFACE
auto* p = document->addObject("Shape");
p->Shape.setValue(builder.shape);
newObject = p;
break;
+ }
}
if (newObject) {
IncrementCreatedObjectCount();
- newObject->Visibility.setValue(false);
- ApplyGuiStyles(static_cast(newObject));
- childObjects.push_back(newObject);
+ 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
}
}
}
@@ -721,9 +881,22 @@ void ImpExpDxfRead::OnReadLine(const Base::Vector3d& start,
}
TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(p0, p1).Edge();
GeometryBuilder builder(edge);
- if (m_importMode == ImportMode::EditablePrimitives) {
- builder.type = GeometryBuilder::PrimitiveType::Line;
+
+ // 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::Line;
+ break;
}
+
Collector->AddGeometry(builder);
}
@@ -736,8 +909,15 @@ void ImpExpDxfRead::OnReadPoint(const Base::Vector3d& start)
TopoDS_Vertex vertex = BRepBuilderAPI_MakeVertex(makePoint(start)).Vertex();
GeometryBuilder builder(vertex);
- if (m_importMode == ImportMode::EditablePrimitives) {
- builder.type = GeometryBuilder::PrimitiveType::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);
}
@@ -767,9 +947,17 @@ void ImpExpDxfRead::OnReadArc(const Base::Vector3d& start,
}
TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(circle, p0, p1).Edge();
- GeometryBuilder builder(edge);
- if (m_importMode == ImportMode::EditablePrimitives) {
- builder.type = GeometryBuilder::PrimitiveType::Arc;
+ 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);
}
@@ -797,9 +985,17 @@ void ImpExpDxfRead::OnReadCircle(const Base::Vector3d& start,
}
TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(circle).Edge();
- GeometryBuilder builder(edge);
- if (m_importMode == ImportMode::EditablePrimitives) {
- builder.type = GeometryBuilder::PrimitiveType::Circle;
+ 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);
}
@@ -915,9 +1111,18 @@ void ImpExpDxfRead::OnReadSpline(struct SplineData& sd)
}
if (!geom.IsNull()) {
- GeometryBuilder builder(BRepBuilderAPI_MakeEdge(geom).Edge());
- if (m_importMode == ImportMode::EditablePrimitives) {
- builder.type = GeometryBuilder::PrimitiveType::Spline;
+ TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(geom).Edge();
+ GeometryBuilder builder(edge); // Instantiate builder once
+
+ 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);
}
@@ -953,9 +1158,18 @@ void ImpExpDxfRead::OnReadEllipse(const Base::Vector3d& center,
return;
}
- GeometryBuilder builder(BRepBuilderAPI_MakeEdge(ellipse).Edge());
- if (m_importMode == ImportMode::EditablePrimitives) {
- builder.type = GeometryBuilder::PrimitiveType::Ellipse;
+ TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(ellipse).Edge();
+ GeometryBuilder builder(edge); // Instantiate builder once
+
+ switch (m_importMode) {
+ case ImportMode::EditableDraft:
+ case ImportMode::EditablePrimitives:
+ builder.type = GeometryBuilder::PrimitiveType::Ellipse;
+ break;
+ case ImportMode::IndividualShapes:
+ case ImportMode::FusedShapes:
+ builder.type = GeometryBuilder::PrimitiveType::None; // Generic Part::Feature
+ break;
}
Collector->AddGeometry(builder);
}
@@ -1064,109 +1278,104 @@ void ImpExpDxfRead::OnReadPolyline(std::list& vertices, int flags)
return; // Not enough vertices for an open polyline
}
- BRepBuilderAPI_MakeWire wireBuilder;
- bool is_closed = ((flags & 1) != 0);
- 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++;
+ TopoDS_Wire wire = BuildWireFromPolyline(vertices, flags);
+ if (wire.IsNull()) {
+ return;
}
- 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);
- }
- }
-
- if (wireBuilder.IsDone()) {
- TopoDS_Wire wire = wireBuilder.Wire();
+ if (m_importMode == ImportMode::EditableDraft) {
GeometryBuilder builder(wire);
-
- // For FusedShapes mode, we can create the object immediately.
- // For other modes, we store the builder for later processing.
- if (m_importMode == ImportMode::FusedShapes) {
- Collector->AddObject(wire, "Polyline");
- return;
- }
-
- if (m_importMode == ImportMode::EditablePrimitives) {
- builder.type = GeometryBuilder::PrimitiveType::PolylineCompound;
- }
-
+ 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 = Reader.document->addObject(
+ Reader.document->getUniqueObjectName("Point").c_str());
+ if (newDocObj) {
+ TopoDS_Vertex v = TopoDS::Vertex(builder.shape);
+ gp_Pnt pnt = BRep_Tool::Pnt(v);
+ static_cast(newDocObj)->Placement.setValue(
+ Base::Placement(Base::Vector3d(pnt.X(), pnt.Y(), pnt.Z()), Base::Rotation()));
+ }
+ break;
+ }
+ case GeometryBuilder::PrimitiveType::Ellipse: {
+ newDocObj = Reader.document->addObject(
+ Reader.document->getUniqueObjectName("Ellipse").c_str());
+ if (newDocObj) {
+ static_cast(newDocObj)->Shape.setValue(builder.shape);
+ }
+ break;
+ }
+ case GeometryBuilder::PrimitiveType::Spline: {
+ newDocObj = Reader.document->addObject(
+ Reader.document->getUniqueObjectName("Spline").c_str());
+ if (newDocObj) {
+ static_cast(newDocObj)->Shape.setValue(builder.shape);
+ }
+ 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 = Reader.document->addObject(
+ Reader.document->getUniqueObjectName("Shape").c_str());
+ if (newDocObj) {
+ static_cast(newDocObj)->Shape.setValue(builder.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,
@@ -1238,10 +1447,14 @@ 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
@@ -1298,25 +1511,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);
}
@@ -1333,8 +1562,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 002645f811..15685f1d58 100644
--- a/src/Mod/Import/App/dxf/ImpExpDxf.h
+++ b/src/Mod/Import/App/dxf/ImpExpDxf.h
@@ -131,7 +131,8 @@ private:
Arc,
Ellipse,
Spline,
- PolylineCompound
+ 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.
@@ -176,6 +177,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
@@ -242,6 +247,7 @@ private:
App::DocumentObjectGroup* m_unreferencedBlocksGroup = nullptr;
App::Document* document;
std::string m_optionSource;
+ void _addOriginalLayerProperty(App::DocumentObject* obj);
protected:
friend class DrawingEntityCollector;
@@ -311,12 +317,7 @@ protected:
{}
void AddObject(const TopoDS_Shape& shape, const char* nameBase) override;
- void AddGeometry(const GeometryBuilder& builder) override
- {
- // In drawing mode, we create objects immediately based on the builder.
- // For now, this just creates simple shapes. Primitives would need more logic here.
- AddObject(builder.shape, "Shape");
- }
+ 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,
From 9f9104e182576487346efa14f0511010e44f0603 Mon Sep 17 00:00:00 2001
From: Furgo <148809153+furgo16@users.noreply.github.com>
Date: Tue, 8 Jul 2025 12:09:00 +0200
Subject: [PATCH 06/14] Import: DXF, make ellipses parametric
---
src/Mod/Draft/importDXF.py | 42 +++++++++++++++--
src/Mod/Import/App/dxf/ImpExpDxf.cpp | 70 ++++++++++++++++++++++------
2 files changed, 96 insertions(+), 16 deletions(-)
diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py
index 5c9fe5d1ba..a837cd56ac 100644
--- a/src/Mod/Draft/importDXF.py
+++ b/src/Mod/Draft/importDXF.py
@@ -4545,8 +4545,6 @@ class DxfDraftPostProcessor:
new_obj.FirstAngle = FreeCAD.Units.Quantity(start_angle, "deg")
new_obj.LastAngle = FreeCAD.Units.Quantity(end_angle, "deg")
- FCC.PrintMessage(f"DEBUG: Final angles after assignment: {new_obj.FirstAngle.Value} deg to {new_obj.LastAngle.Value} deg\n")
-
# 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)
@@ -4566,13 +4564,51 @@ class DxfDraftPostProcessor:
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
- FCC.PrintMessage(f"DEBUG: Input is Part::Feature (ShapeType: {shape.ShapeType})\n")
+ FCC.PrintMessage(f"DEBUG: {part_obj.Label} ({part_obj.Name}) is Part::Feature (ShapeType: {shape.ShapeType})\n")
# 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).
diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.cpp b/src/Mod/Import/App/dxf/ImpExpDxf.cpp
index ed01d65a2c..41371374d4 100644
--- a/src/Mod/Import/App/dxf/ImpExpDxf.cpp
+++ b/src/Mod/Import/App/dxf/ImpExpDxf.cpp
@@ -41,6 +41,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -97,12 +98,58 @@ 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);
+
} // 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)
{
@@ -636,10 +683,8 @@ void ImpExpDxfRead::ComposeParametricBlock(const std::string& blockName,
break;
}
case GeometryBuilder::PrimitiveType::Ellipse: {
- // Ellipses are generic Part::Feature as no Part primitive exists
- auto* p = document->addObject("Ellipse");
- p->Shape.setValue(builder.shape);
- newObject = p;
+ newObject =
+ createEllipsePrimitive(TopoDS::Edge(builder.shape), document, "Ellipse");
break;
}
case GeometryBuilder::PrimitiveType::Spline: {
@@ -649,7 +694,6 @@ void ImpExpDxfRead::ComposeParametricBlock(const std::string& blockName,
newObject = p;
break;
}
- // NEW CASES FOR POLYLINES IN BLOCKS
case GeometryBuilder::PrimitiveType::PolylineFlattened: {
// This creates a simple Part::Feature wrapping the wire, which is standard for
// block children.
@@ -659,7 +703,6 @@ void ImpExpDxfRead::ComposeParametricBlock(const std::string& blockName,
break;
}
case GeometryBuilder::PrimitiveType::PolylineParametric: {
- // REUSED ORIGINAL LOGIC for parametric polylines inside blocks.
// This creates a Part::Compound containing line/arc segments.
auto* p =
document->addObject("Polyline"); // Main polyline compound
@@ -1159,16 +1202,20 @@ void ImpExpDxfRead::OnReadEllipse(const Base::Vector3d& center,
}
TopoDS_Edge edge = BRepBuilderAPI_MakeEdge(ellipse).Edge();
- GeometryBuilder builder(edge); // Instantiate builder once
+ 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:
- builder.type = GeometryBuilder::PrimitiveType::None; // Generic Part::Feature
+ default:
+ // For other modes, create a generic shape (Part:Feature), which is the existing
+ // behavior.
+ builder.type = GeometryBuilder::PrimitiveType::None;
break;
}
Collector->AddGeometry(builder);
@@ -1332,11 +1379,8 @@ void ImpExpDxfRead::DrawingEntityCollector::AddGeometry(const GeometryBuilder& b
break;
}
case GeometryBuilder::PrimitiveType::Ellipse: {
- newDocObj = Reader.document->addObject(
- Reader.document->getUniqueObjectName("Ellipse").c_str());
- if (newDocObj) {
- static_cast(newDocObj)->Shape.setValue(builder.shape);
- }
+ newDocObj =
+ createEllipsePrimitive(TopoDS::Edge(builder.shape), Reader.document, "Ellipse");
break;
}
case GeometryBuilder::PrimitiveType::Spline: {
From d1e131863ba4ed56eb1ab4ea632e8c4987263883 Mon Sep 17 00:00:00 2001
From: Furgo <148809153+furgo16@users.noreply.github.com>
Date: Tue, 8 Jul 2025 14:42:53 +0200
Subject: [PATCH 07/14] Import: DXF, first working version of dimensions import
---
src/Mod/Draft/importDXF.py | 75 ++++++++++++++++++++++------
src/Mod/Import/App/dxf/ImpExpDxf.cpp | 17 ++++++-
src/Mod/Import/App/dxf/ImpExpDxf.h | 1 +
src/Mod/Import/App/dxf/dxf.cpp | 2 +-
src/Mod/Import/App/dxf/dxf.h | 1 +
5 files changed, 80 insertions(+), 16 deletions(-)
diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py
index a837cd56ac..44a32e9c5d 100644
--- a/src/Mod/Draft/importDXF.py
+++ b/src/Mod/Draft/importDXF.py
@@ -82,6 +82,7 @@ if gui:
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
@@ -2879,8 +2880,8 @@ def _import_dxf_file(filename, doc_name=None):
newly_created_objects = objects_after - objects_before
# --- Post-processing step ---
- if is_draft_mode and newly_created_objects:
- draft_postprocessor = DxfDraftPostProcessor(doc, newly_created_objects)
+ 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
@@ -4445,9 +4446,10 @@ class DxfDraftPostProcessor:
converting them into fully parametric Draft objects while preserving
the block and layer hierarchy.
"""
- def __init__(self, doc, new_objects):
+ 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 = []
@@ -4463,7 +4465,7 @@ class DxfDraftPostProcessor:
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() and child.isDerivedFrom("Part::Feature")
+ if child.isValid()
]
all_block_internal_objects_set = set()
@@ -4479,7 +4481,7 @@ class DxfDraftPostProcessor:
if obj.isDerivedFrom("App::FeaturePython") and hasattr(obj, "DxfEntityType"):
placeholders.append(obj)
- elif obj.isDerivedFrom("Part::Feature"):
+ elif obj.isDerivedFrom("Part::Feature") or obj.isDerivedFrom("App::Link"):
top_level_geometry.append(obj)
return block_definitions, top_level_geometry, placeholders
@@ -4490,6 +4492,10 @@ class DxfDraftPostProcessor:
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 \
@@ -4662,7 +4668,8 @@ class DxfDraftPostProcessor:
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.
- self.all_originals_to_delete.add(part_obj)
+ if new_obj is not part_obj:
+ self.all_originals_to_delete.add(part_obj)
return new_obj, obj_type_str
@@ -4716,14 +4723,51 @@ class DxfDraftPostProcessor:
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)
- dim.addExtension("Part::AttachExtensionPython")
- dim.Start = placeholder.Start
- dim.End = placeholder.End
- dim.Dimline = placeholder.Dimline
- dim.Placement = placeholder.Placement
+
+ 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
@@ -4850,12 +4894,15 @@ class DxfDraftPostProcessor:
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(original_children)
+ 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:
- self._create_and_parent_geometry(part_obj)
- self.all_originals_to_delete.update(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)
diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.cpp b/src/Mod/Import/App/dxf/ImpExpDxf.cpp
index 41371374d4..1c1316e142 100644
--- a/src/Mod/Import/App/dxf/ImpExpDxf.cpp
+++ b/src/Mod/Import/App/dxf/ImpExpDxf.cpp
@@ -1279,7 +1279,8 @@ 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() || !m_importAnnotations) {
return;
@@ -1304,6 +1305,20 @@ void ImpExpDxfRead::OnReadDimension(const Base::Vector3d& start,
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.
diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.h b/src/Mod/Import/App/dxf/ImpExpDxf.h
index 15685f1d58..0e7edde6e6 100644
--- a/src/Mod/Import/App/dxf/ImpExpDxf.h
+++ b/src/Mod/Import/App/dxf/ImpExpDxf.h
@@ -100,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;
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*/)
From cf548d0b8ec8e8817dcb65029498290bbb6f4935 Mon Sep 17 00:00:00 2001
From: Furgo <148809153+furgo16@users.noreply.github.com>
Date: Tue, 8 Jul 2025 12:32:22 +0200
Subject: [PATCH 08/14] Import: DXF, deduplicate Part primitives creation
Create helpers that can be reused when importing entities as top-level
geometry and as part of blocks
---
src/Mod/Import/App/dxf/ImpExpDxf.cpp | 145 ++++++++++++++-------------
src/Mod/Import/App/dxf/ImpExpDxf.h | 2 +
2 files changed, 78 insertions(+), 69 deletions(-)
diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.cpp b/src/Mod/Import/App/dxf/ImpExpDxf.cpp
index 1c1316e142..104202c564 100644
--- a/src/Mod/Import/App/dxf/ImpExpDxf.cpp
+++ b/src/Mod/Import/App/dxf/ImpExpDxf.cpp
@@ -100,7 +100,10 @@ Part::Circle* createCirclePrimitive(const TopoDS_Edge& edge, App::Document* doc,
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
@@ -212,6 +215,31 @@ Part::Line* createLinePrimitive(const TopoDS_Edge& edge, App::Document* doc, con
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)
@@ -306,14 +334,19 @@ TopoDS_Wire ImpExpDxfRead::BuildWireFromPolyline(std::list& vertices
return wireBuilder.Wire();
}
-void ImpExpDxfRead::CreateFlattenedPolyline(const TopoDS_Wire& wire, const char* name)
+Part::Feature* ImpExpDxfRead::createFlattenedPolylineFeature(const TopoDS_Wire& wire,
+ const char* name)
{
auto* p = document->addObject(document->getUniqueObjectName(name).c_str());
- p->Shape.setValue(wire);
- Collector->AddObject(p, name);
+ if (p) {
+ p->Shape.setValue(wire);
+ IncrementCreatedObjectCount();
+ }
+ return p;
}
-void ImpExpDxfRead::CreateParametricPolyline(const TopoDS_Wire& wire, const char* name)
+Part::Compound* ImpExpDxfRead::createParametricPolylineCompound(const TopoDS_Wire& wire,
+ const char* name)
{
auto* p = document->addObject(document->getUniqueObjectName(name).c_str());
IncrementCreatedObjectCount();
@@ -336,13 +369,37 @@ void ImpExpDxfRead::CreateParametricPolyline(const TopoDS_Wire& wire, const char
if (segment) {
IncrementCreatedObjectCount();
segment->Visibility.setValue(false);
- ApplyGuiStyles(static_cast(segment));
+ // We apply styles later, depending on the context
segments.push_back(segment);
}
}
p->Links.setValues(segments);
+ return p;
+}
- Collector->AddObject(p, name);
+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)
@@ -659,12 +716,8 @@ void ImpExpDxfRead::ComposeParametricBlock(const std::string& blockName,
break;
}
case GeometryBuilder::PrimitiveType::Point: {
- auto* p = document->addObject("Point");
- TopoDS_Vertex v = TopoDS::Vertex(builder.shape);
- gp_Pnt pnt = BRep_Tool::Pnt(v);
- p->Placement.setValue(Base::Placement(Base::Vector3d(pnt.X(), pnt.Y(), pnt.Z()),
- Base::Rotation()));
- newObject = p;
+ newObject =
+ createVertexPrimitive(TopoDS::Vertex(builder.shape), document, "Point");
break;
}
case GeometryBuilder::PrimitiveType::Circle:
@@ -697,53 +750,21 @@ void ImpExpDxfRead::ComposeParametricBlock(const std::string& blockName,
case GeometryBuilder::PrimitiveType::PolylineFlattened: {
// This creates a simple Part::Feature wrapping the wire, which is standard for
// block children.
- auto* p = document->addObject("Polyline");
- p->Shape.setValue(TopoDS::Wire(builder.shape)); // Ensure it's a TopoDS_Wire
- newObject = p;
+ newObject =
+ createFlattenedPolylineFeature(TopoDS::Wire(builder.shape), "Polyline");
break;
}
case GeometryBuilder::PrimitiveType::PolylineParametric: {
// This creates a Part::Compound containing line/arc segments.
- auto* p =
- document->addObject("Polyline"); // Main polyline compound
- std::vector segments;
- TopExp_Explorer explorer(TopoDS::Wire(builder.shape),
- TopAbs_EDGE); // Iterate edges of the wire
-
- 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) {
- auto* arc = createCirclePrimitive(edge, document, "Arc");
- segment = arc;
- }
-
- if (segment) {
- // These segments are children of the polyline compound, not top-level
- // block children.
- IncrementCreatedObjectCount(); // Count this sub-object
- segment->Visibility.setValue(false); // Sub-segments are usually hidden
- // No layer/style needed here, inherited from the polyline compound or
- // handled by block.
- // ApplyGuiStyles(static_cast(segment));
- segments.push_back(segment);
- }
- }
- p->Links.setValues(segments); // Link segments to the polyline compound
- newObject = p; // The polyline compound itself is the new object for the block
+ newObject =
+ createParametricPolylineCompound(TopoDS::Wire(builder.shape), "Polyline");
+ // No styling needed here, as the block's instance will control appearance.
break;
}
case GeometryBuilder::PrimitiveType::None: // Default/fallback if not handled
default: {
// Generic shape, e.g., 3DFACE
- auto* p = document->addObject("Shape");
- p->Shape.setValue(builder.shape);
- newObject = p;
+ newObject = createGenericShapeFeature(builder.shape, document, "Shape");
break;
}
}
@@ -1383,14 +1404,8 @@ void ImpExpDxfRead::DrawingEntityCollector::AddGeometry(const GeometryBuilder& b
break;
}
case GeometryBuilder::PrimitiveType::Point: {
- newDocObj = Reader.document->addObject(
- Reader.document->getUniqueObjectName("Point").c_str());
- if (newDocObj) {
- TopoDS_Vertex v = TopoDS::Vertex(builder.shape);
- gp_Pnt pnt = BRep_Tool::Pnt(v);
- static_cast(newDocObj)->Placement.setValue(
- Base::Placement(Base::Vector3d(pnt.X(), pnt.Y(), pnt.Z()), Base::Rotation()));
- }
+ newDocObj =
+ createVertexPrimitive(TopoDS::Vertex(builder.shape), Reader.document, "Point");
break;
}
case GeometryBuilder::PrimitiveType::Ellipse: {
@@ -1399,11 +1414,7 @@ void ImpExpDxfRead::DrawingEntityCollector::AddGeometry(const GeometryBuilder& b
break;
}
case GeometryBuilder::PrimitiveType::Spline: {
- newDocObj = Reader.document->addObject(
- Reader.document->getUniqueObjectName("Spline").c_str());
- if (newDocObj) {
- static_cast(newDocObj)->Shape.setValue(builder.shape);
- }
+ newDocObj = createGenericShapeFeature(builder.shape, Reader.document, "Spline");
break;
}
case GeometryBuilder::PrimitiveType::PolylineFlattened: {
@@ -1418,11 +1429,7 @@ void ImpExpDxfRead::DrawingEntityCollector::AddGeometry(const GeometryBuilder& b
}
case GeometryBuilder::PrimitiveType::None: // Fallback for generic shapes (e.g., 3DFACE)
default: {
- newDocObj = Reader.document->addObject(
- Reader.document->getUniqueObjectName("Shape").c_str());
- if (newDocObj) {
- static_cast(newDocObj)->Shape.setValue(builder.shape);
- }
+ newDocObj = createGenericShapeFeature(builder.shape, Reader.document, "Shape");
break;
}
}
diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.h b/src/Mod/Import/App/dxf/ImpExpDxf.h
index 0e7edde6e6..9054b253a7 100644
--- a/src/Mod/Import/App/dxf/ImpExpDxf.h
+++ b/src/Mod/Import/App/dxf/ImpExpDxf.h
@@ -162,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()
From 9b33975697e76fd86a378d8fddc5d83400e98610 Mon Sep 17 00:00:00 2001
From: Furgo <148809153+furgo16@users.noreply.github.com>
Date: Thu, 10 Jul 2025 12:20:03 +0200
Subject: [PATCH 09/14] Import: DXF, add suggestions and improve UI copy.
---
.../Resources/ui/preferences-dxf-import.ui | 14 +++----
src/Mod/Draft/Resources/ui/preferences-dxf.ui | 42 +++++++++----------
2 files changed, 28 insertions(+), 28 deletions(-)
diff --git a/src/Mod/Draft/Resources/ui/preferences-dxf-import.ui b/src/Mod/Draft/Resources/ui/preferences-dxf-import.ui
index fa66b241e8..a6b6b883e6 100644
--- a/src/Mod/Draft/Resources/ui/preferences-dxf-import.ui
+++ b/src/Mod/Draft/Resources/ui/preferences-dxf-import.ui
@@ -17,7 +17,7 @@
-
- Import as
+ Import As
-
@@ -26,10 +26,10 @@
Creates fully parametric Draft objects. Block definitions are imported as
reusable objects (Part Compounds) and instances become `App::Link` objects,
maintaining the block structure. Best for full integration with the Draft
-Workbench. (Legacy importer only)
+workbench.
- Editable draft objects
+ Editable Draft objects
@@ -39,10 +39,10 @@ Workbench. (Legacy importer only)
Creates parametric Part objects (e.g., Part::Line, Part::Circle). Block
definitions are imported as reusable objects (Part Compounds) and instances
become `App::Link` objects, maintaining the block structure. Best for
-script-based post-processing. (Not yet implemented)
+script-based post-processing.
- Editable part primitives
+ Editable Part primitives
@@ -54,7 +54,7 @@ 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)
+ Individual Part shapes (recommended)
@@ -66,7 +66,7 @@ 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)
+ Fused Part shapes (fastest)
diff --git a/src/Mod/Draft/Resources/ui/preferences-dxf.ui b/src/Mod/Draft/Resources/ui/preferences-dxf.ui
index 881e5f5e7c..27d1d024f7 100644
--- a/src/Mod/Draft/Resources/ui/preferences-dxf.ui
+++ b/src/Mod/Draft/Resources/ui/preferences-dxf.ui
@@ -105,7 +105,7 @@ the 'dxf_library' addon from the Addon Manager.
-
- Import as
+ Import As
-
@@ -117,10 +117,10 @@ the 'dxf_library' addon from the Addon Manager.
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.
+workbench.
- Editable draft objects (Highest fidelity, slowest)
+ Editable Draft objects (highest fidelity, slowest)
dxfImportAsDraft
@@ -145,10 +145,10 @@ Workbench.
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.
+script-based post-processing and Part workbench integration.
- Editable part primitives (High fidelity, slower)
+ Editable Part primitives (high fidelity, slower)
dxfImportAsPrimitives
@@ -172,7 +172,7 @@ 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)
+ Individual Part shapes (balanced, recommended)
true
@@ -199,7 +199,7 @@ 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)
+ Fused Part shapes (lowest fidelity, fastest)
dxfImportAsFused
@@ -221,7 +221,7 @@ shape. Best for importing and viewing very large files with maximum performance.
-
- Import settings
+ Import Settings
-
@@ -229,7 +229,7 @@ shape. Best for importing and viewing very large files with maximum performance.
-
- Global scaling factor:
+ Global scaling factor
@@ -250,7 +250,7 @@ shape. Best for importing and viewing very large files with maximum performance.
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
+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
@@ -276,7 +276,7 @@ in feet: 304.8
-
- Import:
+ Import
@@ -285,7 +285,7 @@ in feet: 304.8
-
- If checked, text, mtext, and dimension entities will be imported as Draft objects.
+ If checked, text, mtext, and dimension entities will be imported as Draft objects
Texts and dimensions
@@ -301,7 +301,7 @@ in feet: 304.8
-
- If checked, point entities will be imported.
+ If checked, point entities will be imported
Points
@@ -321,7 +321,7 @@ in feet: 304.8
If checked, entities from the paper space will also be imported. By default,
-only model space is imported.
+only model space is imported
Paper space objects
@@ -338,7 +338,7 @@ only model space is imported.
If checked, anonymous blocks (whose names begin with *) will also be imported.
-These are often used for hatches and dimensions.
+These are often used for hatches and dimensions
Anonymous blocks (*-blocks)
@@ -376,7 +376,7 @@ These are often used for hatches and dimensions.
-
- Appearance:
+ Appearance
@@ -386,7 +386,7 @@ These are often used for hatches and dimensions.
If checked, colors will be set as specified in the DXF file whenever
-possible. Otherwise, default FreeCAD colors are applied.
+possible. Otherwise, default FreeCAD colors are applied
Use colors from the DXF file
@@ -408,7 +408,7 @@ possible. Otherwise, default FreeCAD colors are applied.
false
- If checked, imported texts will get the standard Draft Text size, instead of
+ If checked, imported texts will get the standard Draft text size, instead of
the size defined in the DXF document. (Legacy importer only)
@@ -427,7 +427,7 @@ the size defined in the DXF document. (Legacy importer only)
-
- Advanced processing:
+ Advanced processing
@@ -480,10 +480,10 @@ representing that width. (Legacy importer only)
If checked, the legacy importer will attempt to create Sketcher objects
-instead of Draft or Part objects. This overrides the 'Import as' setting.
+instead of Draft or Part objects. This overrides the 'Import As' setting
- Create sketches (legacy importer only)
+ Create sketches
dxfCreateSketch
From df52665ae50f908ede13eada259a2e77a0585fe1 Mon Sep 17 00:00:00 2001
From: Furgo <148809153+furgo16@users.noreply.github.com>
Date: Thu, 10 Jul 2025 13:25:58 +0200
Subject: [PATCH 10/14] Import: DXF, fix typo in individual shapes import mode
---
src/Mod/Import/App/dxf/ImpExpDxf.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.cpp b/src/Mod/Import/App/dxf/ImpExpDxf.cpp
index 104202c564..62ab50ca6c 100644
--- a/src/Mod/Import/App/dxf/ImpExpDxf.cpp
+++ b/src/Mod/Import/App/dxf/ImpExpDxf.cpp
@@ -957,7 +957,7 @@ void ImpExpDxfRead::OnReadLine(const Base::Vector3d& start,
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::Line;
+ builder.type = GeometryBuilder::PrimitiveType::None;
break;
}
From 8ab525ddb9567ab2aa818141c54c2dd8a6e38ee5 Mon Sep 17 00:00:00 2001
From: Furgo <148809153+furgo16@users.noreply.github.com>
Date: Thu, 10 Jul 2025 14:16:51 +0200
Subject: [PATCH 11/14] Import: DXF, correctly transfer Draft.Line points to
make them editable
---
src/Mod/Draft/importDXF.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py
index 44a32e9c5d..b67a0f7fbf 100644
--- a/src/Mod/Draft/importDXF.py
+++ b/src/Mod/Draft/importDXF.py
@@ -4518,6 +4518,13 @@ class DxfDraftPostProcessor:
# 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"):
From f18b33565d01b7887a8453699b8df13fe931bcaf Mon Sep 17 00:00:00 2001
From: Furgo <148809153+furgo16@users.noreply.github.com>
Date: Thu, 10 Jul 2025 15:28:29 +0200
Subject: [PATCH 12/14] Import: DXF, make straight polylines Draft-editable
---
src/Mod/Draft/importDXF.py | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py
index b67a0f7fbf..8e7dd3b6b7 100644
--- a/src/Mod/Draft/importDXF.py
+++ b/src/Mod/Draft/importDXF.py
@@ -4629,6 +4629,24 @@ class DxfDraftPostProcessor:
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"
From 75d6cee9053af70ff3f3e4e1b94c19101825a48f Mon Sep 17 00:00:00 2001
From: Furgo <148809153+furgo16@users.noreply.github.com>
Date: Fri, 11 Jul 2025 00:00:32 +0200
Subject: [PATCH 13/14] Import: DXF, fix CodeQL errors
---
src/Mod/Draft/importDXF.py | 16 +++++-----------
1 file changed, 5 insertions(+), 11 deletions(-)
diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py
index 8e7dd3b6b7..21b8f03cf8 100644
--- a/src/Mod/Draft/importDXF.py
+++ b/src/Mod/Draft/importDXF.py
@@ -4431,15 +4431,6 @@ class DxfImportReporter:
FCC.PrintMessage(output_string)
-def post_process_to_draft(doc, new_objects):
- """
- Entry point for the DXF post-processing workflow.
- Instantiates and runs the DxfDraftPostProcessor.
- """
- processor = DxfDraftPostProcessor(doc, new_objects)
- processor.run()
-
-
class DxfDraftPostProcessor:
"""
Handles the post-processing of DXF files imported as Part objects,
@@ -4852,8 +4843,11 @@ class DxfDraftPostProcessor:
if group and not group.Group:
try:
self.doc.removeObject(group.Name)
- except Exception:
- pass
+ 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):
"""
From 66f92850dadbf1534ef88e817c120204182ac8d2 Mon Sep 17 00:00:00 2001
From: Furgo <148809153+furgo16@users.noreply.github.com>
Date: Fri, 11 Jul 2025 21:49:47 +0200
Subject: [PATCH 14/14] Remove debug print statement
---
src/Mod/Draft/importDXF.py | 2 --
1 file changed, 2 deletions(-)
diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py
index 21b8f03cf8..71fabd41d3 100644
--- a/src/Mod/Draft/importDXF.py
+++ b/src/Mod/Draft/importDXF.py
@@ -4612,8 +4612,6 @@ class DxfDraftPostProcessor:
if not shape.isValid():
return None, None
- FCC.PrintMessage(f"DEBUG: {part_obj.Label} ({part_obj.Name}) is Part::Feature (ShapeType: {shape.ShapeType})\n")
-
# 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.