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/PrefWidgets.h
- Gui::PrefDoubleSpinBox - QDoubleSpinBox + Gui::PrefRadioButton + QRadioButton
Gui/PrefWidgets.h
- Gui::PrefRadioButton - QRadioButton + Gui::PrefDoubleSpinBox + QDoubleSpinBox
Gui/PrefWidgets.h
- - checkBox_dxfUseLegacyImporter - toggled(bool) - label_Create - setEnabled(bool) - - - 20 - 20 - - - 20 - 20 - - - - - checkBox_dxfUseLegacyImporter - toggled(bool) - radioButton_dxfCreatePart - setEnabled(bool) - - - 20 - 20 - - - 20 - 20 - - - - - checkBox_dxfUseLegacyImporter - toggled(bool) - radioButton_dxfCreateDraft - setEnabled(bool) - - - 20 - 20 - - - 20 - 20 - - - - - checkBox_dxfUseLegacyImporter - toggled(bool) - radioButton_dxfCreateSketch - setEnabled(bool) - - - 20 - 20 - - - 20 - 20 - - - checkBox_dxfUseLegacyImporter toggled(bool) @@ -785,6 +699,22 @@ This might fail for post DXF R12 templates. + + checkBox_dxfUseLegacyImporter + toggled(bool) + checkBox_renderPolylineWidth + setEnabled(bool) + + + 20 + 20 + + + 20 + 20 + + + checkBox_dxfUseLegacyImporter toggled(bool) @@ -820,7 +750,55 @@ This might fail for post DXF R12 templates. checkBox_dxfUseLegacyImporter toggled(bool) - checkBox_renderPolylineWidth + checkBox_dxfCreateSketch + setEnabled(bool) + + + 20 + 20 + + + 20 + 20 + + + + + checkBox_dxfCreateSketch + toggled(bool) + groupBox_ImportAs + setDisabled(bool) + + + 20 + 20 + + + 20 + 20 + + + + + checkBox_dxfUseLegacyExporter + toggled(bool) + checkBox_dxfmesh + setEnabled(bool) + + + 20 + 20 + + + 20 + 20 + + + + + checkBox_dxfUseLegacyExporter + toggled(bool) + checkBox_dxfproject setEnabled(bool) diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py index 1325611448..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.