From 76a0d9ffe3c1c3010c964690b7e340b65de9b01d Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Thu, 12 Jun 2025 07:29:51 +0200 Subject: [PATCH 01/18] Import: DXF parser, add stats reporting structure --- src/Mod/Import/App/dxf/dxf.cpp | 9 +++++++++ src/Mod/Import/App/dxf/dxf.h | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/Mod/Import/App/dxf/dxf.cpp b/src/Mod/Import/App/dxf/dxf.cpp index 257fb5a165..b47c75a296 100644 --- a/src/Mod/Import/App/dxf/dxf.cpp +++ b/src/Mod/Import/App/dxf/dxf.cpp @@ -2342,6 +2342,7 @@ bool CDxfRead::SkipBlockContents() template void CDxfRead::UnsupportedFeature(const char* format, args&&... argValuess) { + m_stats.unsupportedFeaturesCount++; // NOLINTNEXTLINE(runtime/printf) std::string formattedMessage = fmt::sprintf(format, std::forward(argValuess)...); // We place these formatted messages in a map, count their occurrences and not their first @@ -2538,6 +2539,8 @@ bool CDxfRead::ReadVersion() m_version = RUnknown; } + m_stats.dxfVersion = m_record_data; + return ResolveEncoding(); } @@ -2606,6 +2609,9 @@ bool CDxfRead::ResolveEncoding() Py_DECREF(pyDecoder); Py_DECREF(pyUTF8Decoder); } + + m_stats.dxfEncoding = m_encoding; + return !m_encoding.empty(); } @@ -2759,6 +2765,9 @@ bool CDxfRead::ReadEntity() m_entityAttributes.m_paperSpace); // TODO: Ensure the stream is noboolalpha (for that // matter ensure the stream has the "C" locale SetupValueAttribute(eColor, m_entityAttributes.m_Color); + + m_stats.entityCounts[m_record_data]++; + // The entity record is already the current record and is already checked as a type 0 record if (IsObjectName("LINE")) { return ReadLine(); diff --git a/src/Mod/Import/App/dxf/dxf.h b/src/Mod/Import/App/dxf/dxf.h index c3da0d8fba..7af0b45a73 100644 --- a/src/Mod/Import/App/dxf/dxf.h +++ b/src/Mod/Import/App/dxf/dxf.h @@ -176,6 +176,18 @@ struct LWPolyDataOut point3D Extr; }; +// Statistics reporting structure +struct DxfImportStats +{ + double importTimeSeconds = 0.0; + std::string dxfVersion; + std::string dxfEncoding; + std::map entityCounts; + std::map importSettings; + int totalEntitiesCreated = 0; + int unsupportedFeaturesCount = 0; +}; + // "using" for enums is not supported by all platforms // https://stackoverflow.com/questions/41167119/how-to-fix-a-wsubobject-linkage-warning @@ -455,6 +467,7 @@ private: double m_unitScalingFactor = 0.0; protected: + DxfImportStats m_stats; // An additional scaling factor which can be modified before readDXF is called, and will be // incorporated into m_unitScalingFactor. void SetAdditionalScaling(double scaling) From 72ca1478e85b7a635abbd05b08933bc2fbd47607 Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Thu, 12 Jun 2025 07:35:50 +0200 Subject: [PATCH 02/18] Import: DXF importer, populate stats reporting structure --- src/Mod/Import/App/dxf/ImpExpDxf.cpp | 27 ++++++++++++++++++++++++++- src/Mod/Import/App/dxf/ImpExpDxf.h | 5 +++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.cpp b/src/Mod/Import/App/dxf/ImpExpDxf.cpp index 6b9279deaf..f05e6fed86 100644 --- a/src/Mod/Import/App/dxf/ImpExpDxf.cpp +++ b/src/Mod/Import/App/dxf/ImpExpDxf.cpp @@ -141,22 +141,32 @@ void ImpExpDxfRead::setOptions() { ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath(getOptionSource().c_str()); + m_stats.importSettings.clear(); + m_preserveLayers = hGrp->GetBool("dxfUseDraftVisGroups", true); + m_stats.importSettings["Use layers"] = m_preserveLayers ? "Yes" : "No"; + 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"; } // 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 @@ -164,12 +174,25 @@ void ImpExpDxfRead::setOptions() // 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. - SetAdditionalScaling(hGrp->GetFloat("dxfScaling", 1.0)); + bool joinGeometry = hGrp->GetBool("joingeometry", false); + m_stats.importSettings["Join geometry"] = joinGeometry ? "Yes" : "No"; + + double scaling = hGrp->GetFloat("dxfScaling", 1.0); + SetAdditionalScaling(scaling); + m_stats.importSettings["Manual scaling factor"] = std::to_string(scaling); m_importAnnotations = hGrp->GetBool("dxftext", false); + m_stats.importSettings["Import texts and dimensions"] = m_importAnnotations ? "Yes" : "No"; + m_importPoints = hGrp->GetBool("dxfImportPoints", true); + m_stats.importSettings["Import points"] = m_importPoints ? "Yes" : "No"; + m_importPaperSpaceEntities = hGrp->GetBool("dxflayout", false); + m_stats.importSettings["Import layout objects"] = m_importPaperSpaceEntities ? "Yes" : "No"; + m_importHiddenBlocks = hGrp->GetBool("dxfstarblocks", false); + m_stats.importSettings["Import hidden blocks"] = m_importHiddenBlocks ? "Yes" : "No"; + // TODO: There is currently no option for this: m_importFrozenLayers = // hGrp->GetBool("dxffrozenLayers", false); // TODO: There is currently no option for this: m_importHiddenLayers = @@ -771,6 +794,7 @@ std::string ImpExpDxfRead::Deformat(const char* text) 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); @@ -778,6 +802,7 @@ void ImpExpDxfRead::DrawingEntityCollector::AddObject(const TopoDS_Shape& shape, } void ImpExpDxfRead::DrawingEntityCollector::AddObject(FeaturePythonBuilder shapeBuilder) { + Reader.IncrementCreatedObjectCount(); App::FeaturePython* shape = shapeBuilder(Reader.OCSOrientationTransform); if (shape != nullptr) { Reader.MoveToLayer(shape); diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.h b/src/Mod/Import/App/dxf/ImpExpDxf.h index c486e6eb7d..989b62cf94 100644 --- a/src/Mod/Import/App/dxf/ImpExpDxf.h +++ b/src/Mod/Import/App/dxf/ImpExpDxf.h @@ -204,6 +204,11 @@ private: std::string m_optionSource; protected: + friend class DrawingEntityCollector; + void IncrementCreatedObjectCount() + { + m_stats.totalEntitiesCreated++; + } virtual void ApplyGuiStyles(Part::Feature* /*object*/) const {} virtual void ApplyGuiStyles(App::FeaturePython* /*object*/) const From 78b720fd355a6ed6dcf65f85e4f1cb5cc62ec032 Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Thu, 12 Jun 2025 09:09:59 +0200 Subject: [PATCH 03/18] Import: DXF importer, add Python bindings --- src/Mod/Import/App/AppImportPy.cpp | 1 + src/Mod/Import/App/dxf/ImpExpDxf.cpp | 24 ++++++++++++++++++++++++ src/Mod/Import/App/dxf/ImpExpDxf.h | 2 ++ src/Mod/Import/Gui/AppImportGuiPy.cpp | 2 +- 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Mod/Import/App/AppImportPy.cpp b/src/Mod/Import/App/AppImportPy.cpp index b4349587ef..91512a4a2e 100644 --- a/src/Mod/Import/App/AppImportPy.cpp +++ b/src/Mod/Import/App/AppImportPy.cpp @@ -415,6 +415,7 @@ private: dxf_file.setOptions(); dxf_file.DoRead(IgnoreErrors); pcDoc->recompute(); + return dxf_file.getStatsAsPyObject(); } catch (const Standard_Failure& e) { throw Py::RuntimeError(e.GetMessageString()); diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.cpp b/src/Mod/Import/App/dxf/ImpExpDxf.cpp index f05e6fed86..467543eb07 100644 --- a/src/Mod/Import/App/dxf/ImpExpDxf.cpp +++ b/src/Mod/Import/App/dxf/ImpExpDxf.cpp @@ -1372,3 +1372,27 @@ void ImpExpDxfWrite::exportDiametricDim(Base::Vector3d textLocn, arc2[2] = arcPoint2.z; writeDiametricDim(text, arc1, arc2, dimText); } + +Py::Object ImpExpDxfRead::getStatsAsPyObject() +{ + Py::Dict statsDict; + + statsDict.setItem("dxfVersion", Py::String(m_stats.dxfVersion)); + statsDict.setItem("dxfEncoding", Py::String(m_stats.dxfEncoding)); + statsDict.setItem("totalEntitiesCreated", Py::Long(m_stats.totalEntitiesCreated)); + statsDict.setItem("unsupportedFeaturesCount", Py::Long(m_stats.unsupportedFeaturesCount)); + + Py::Dict entityCountsDict; + for (const auto& pair : m_stats.entityCounts) { + entityCountsDict.setItem(pair.first.c_str(), Py::Long(pair.second)); + } + statsDict.setItem("entityCounts", entityCountsDict); + + Py::Dict importSettingsDict; + for (const auto& pair : m_stats.importSettings) { + importSettingsDict.setItem(pair.first.c_str(), Py::String(pair.second)); + } + statsDict.setItem("importSettings", importSettingsDict); + + return statsDict; +} diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.h b/src/Mod/Import/App/dxf/ImpExpDxf.h index 989b62cf94..50b8afa558 100644 --- a/src/Mod/Import/App/dxf/ImpExpDxf.h +++ b/src/Mod/Import/App/dxf/ImpExpDxf.h @@ -50,6 +50,8 @@ public: Py_XDECREF(DraftModule); } + Py::Object getStatsAsPyObject(); + bool ReadEntitiesSection() override; // CDxfRead's virtual functions diff --git a/src/Mod/Import/Gui/AppImportGuiPy.cpp b/src/Mod/Import/Gui/AppImportGuiPy.cpp index 35319202c6..700f439736 100644 --- a/src/Mod/Import/Gui/AppImportGuiPy.cpp +++ b/src/Mod/Import/Gui/AppImportGuiPy.cpp @@ -403,6 +403,7 @@ private: dxf_file.setOptions(); dxf_file.DoRead(IgnoreErrors); pcDoc->recompute(); + return dxf_file.getStatsAsPyObject(); } catch (const Standard_Failure& e) { throw Py::RuntimeError(e.GetMessageString()); @@ -410,7 +411,6 @@ private: catch (const Base::Exception& e) { throw Py::RuntimeError(e.what()); } - return Py::None(); } Py::Object exportOptions(const Py::Tuple& args) From d17db2ded1d4fb6793e4aeec25f493947418d6b9 Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Thu, 12 Jun 2025 09:20:28 +0200 Subject: [PATCH 04/18] Import: DXF Python frontend, implement stats reporter --- src/Mod/Draft/importDXF.py | 75 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py index 2f458b7ab6..191d04392b 100644 --- a/src/Mod/Draft/importDXF.py +++ b/src/Mod/Draft/importDXF.py @@ -2827,10 +2827,15 @@ def open(filename): FreeCAD.setActiveDocument(doc.Name) if gui: import ImportGui - ImportGui.readDXF(filename) + stats = ImportGui.readDXF(filename) else: import Import - Import.readDXF(filename) + stats = Import.readDXF(filename) + + if stats: + reporter = DxfImportReporter(stats) + reporter.report_to_console() + Draft.convert_draft_texts() # convert annotations to Draft texts doc.recompute() @@ -2869,10 +2874,15 @@ def insert(filename, docname): else: if gui: import ImportGui - ImportGui.readDXF(filename) + stats = ImportGui.readDXF(filename) else: import Import - Import.readDXF(filename) + stats = Import.readDXF(filename) + + if stats: + reporter = DxfImportReporter(stats) + reporter.report_to_console() + Draft.convert_draft_texts() # convert annotations to Draft texts doc.recompute() @@ -4197,3 +4207,60 @@ def readPreferences(): dxfDefaultColor = getColor() dxfExportBlocks = params.get_param("dxfExportBlocks") dxfScaling = params.get_param("dxfScaling") + + +class DxfImportReporter: + """Formats and reports statistics from a DXF import process.""" + def __init__(self, stats_dict): + self.stats = stats_dict + + def to_console_string(self): + """Formats the statistics into a human-readable string for console output.""" + if not self.stats: + return "DXF Import: No statistics were returned from the importer.\n" + + lines = ["\n--- DXF Import Summary ---"] + + # General Info + lines.append(f"DXF Version: {self.stats.get('dxfVersion', 'Unknown')}") + lines.append(f"File Encoding: {self.stats.get('dxfEncoding', 'Unknown')}") + # Timing will be 0.0 for now, but the line is ready for when it's fixed. + import_time = self.stats.get('importTimeSeconds', 0.0) + lines.append(f"Import Time: {import_time:.4f} seconds") + lines.append("") + + # Settings + lines.append("Import Settings:") + settings = self.stats.get('importSettings', {}) + if settings: + for key, value in sorted(settings.items()): + lines.append(f" - {key}: {value}") + else: + lines.append(" (No settings recorded)") + lines.append("") + + # Counts + lines.append("Entity Counts:") + total_read = 0 + entities = self.stats.get('entityCounts', {}) + if entities: + for key, value in sorted(entities.items()): + lines.append(f" - {key}: {value}") + total_read += value + lines.append("----------------------------") + lines.append(f" Total entities read: {total_read}") + else: + lines.append(" (No entities recorded)") + + lines.append(f"FreeCAD objects created: {self.stats.get('totalEntitiesCreated', 0)}") + lines.append(f"Unsupported features: {self.stats.get('unsupportedFeaturesCount', 0)}") + + lines.append("--- End of Summary ---\n") + return "\n".join(lines) + + def report_to_console(self): + """ + Prints the formatted statistics string to the FreeCAD console. + """ + output_string = self.to_console_string() + FCC.PrintMessage(output_string) From ea40128ea489c87fb76a1080cfa85fbd7e538e9e Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Wed, 18 Jun 2025 09:21:36 +0200 Subject: [PATCH 05/18] Import: DXF parser/importer, improve scale reporting Report additional information about user scaling value, scaling info source and resulting scale, including units. --- src/Mod/Import/App/dxf/ImpExpDxf.cpp | 4 ++ src/Mod/Import/App/dxf/dxf.cpp | 85 ++++++++++++++++++++++------ src/Mod/Import/App/dxf/dxf.h | 3 + 3 files changed, 75 insertions(+), 17 deletions(-) diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.cpp b/src/Mod/Import/App/dxf/ImpExpDxf.cpp index 467543eb07..5d48ce770e 100644 --- a/src/Mod/Import/App/dxf/ImpExpDxf.cpp +++ b/src/Mod/Import/App/dxf/ImpExpDxf.cpp @@ -1379,6 +1379,10 @@ Py::Object ImpExpDxfRead::getStatsAsPyObject() statsDict.setItem("dxfVersion", Py::String(m_stats.dxfVersion)); statsDict.setItem("dxfEncoding", Py::String(m_stats.dxfEncoding)); + statsDict.setItem("scalingSource", Py::String(m_stats.scalingSource)); + statsDict.setItem("fileUnits", Py::String(m_stats.fileUnits)); + statsDict.setItem("finalScalingFactor", Py::Float(m_stats.finalScalingFactor)); + statsDict.setItem("importTimeSeconds", Py::Float(m_stats.importTimeSeconds)); statsDict.setItem("totalEntitiesCreated", Py::Long(m_stats.totalEntitiesCreated)); statsDict.setItem("unsupportedFeaturesCount", Py::Long(m_stats.unsupportedFeaturesCount)); diff --git a/src/Mod/Import/App/dxf/dxf.cpp b/src/Mod/Import/App/dxf/dxf.cpp index b47c75a296..81117d3e36 100644 --- a/src/Mod/Import/App/dxf/dxf.cpp +++ b/src/Mod/Import/App/dxf/dxf.cpp @@ -26,6 +26,61 @@ using namespace std; + +namespace +{ + +std::string DxfUnitToString(DxfUnits::eDxfUnits_t unit) +{ + switch (unit) { + case DxfUnits::eInches: + return "Inches"; + case DxfUnits::eFeet: + return "Feet"; + case DxfUnits::eMiles: + return "Miles"; + case DxfUnits::eMillimeters: + return "Millimeters"; + case DxfUnits::eCentimeters: + return "Centimeters"; + case DxfUnits::eMeters: + return "Meters"; + case DxfUnits::eKilometers: + return "Kilometers"; + case DxfUnits::eMicroinches: + return "Microinches"; + case DxfUnits::eMils: + return "Mils"; + case DxfUnits::eYards: + return "Yards"; + case DxfUnits::eAngstroms: + return "Angstroms"; + case DxfUnits::eNanometers: + return "Nanometers"; + case DxfUnits::eMicrons: + return "Microns"; + case DxfUnits::eDecimeters: + return "Decimeters"; + case DxfUnits::eDekameters: + return "Dekameters"; + case DxfUnits::eHectometers: + return "Hectometers"; + case DxfUnits::eGigameters: + return "Gigameters"; + case DxfUnits::eAstronomicalUnits: + return "Astronomical Units"; + case DxfUnits::eLightYears: + return "Light Years"; + case DxfUnits::eParsecs: + return "Parsecs"; + case DxfUnits::eUnspecified: + default: + return "Unspecified"; + } +} + +} // namespace + static Base::Vector3d MakeVector3d(const double coordinates[3]) { // NOLINTNEXTLINE(readability/nolint) @@ -2818,13 +2873,11 @@ bool CDxfRead::ReadHeaderSection() if (m_record_type == eObjectType && IsObjectName("ENDSEC")) { if (m_unitScalingFactor == 0.0) { // Neither INSUNITS nor MEASUREMENT found, assume 1 DXF unit = 1mm - // TODO: Perhaps this default should depend on the current measuring units of the - // app. + // TODO: Perhaps this default should depend on the current project's unit system m_unitScalingFactor = m_additionalScaling; - ImportObservation("No INSUNITS or MEASUREMENT; setting scaling to 1 DXF unit = " - "%gmm based on DXF scaling option\n", - m_unitScalingFactor); + m_stats.fileUnits = "Unspecified (Defaulting to 1:1)"; } + m_stats.finalScalingFactor = m_unitScalingFactor; return true; } if (m_record_type != eVariableName) { @@ -2852,14 +2905,14 @@ bool CDxfRead::ReadVariable() if (!ParseValue(this, &varValue)) { ImportError("Failed to get integer from INSUNITS value '%s'\n", m_record_data); } - else if (auto units = DxfUnits::eDxfUnits_t(varValue); !DxfUnits::IsValid(units)) { - ImportError("Unknown value '%d' for INSUNITS\n", varValue); - } else { + auto units = DxfUnits::eDxfUnits_t(varValue); + if (!DxfUnits::IsValid(units)) { + units = DxfUnits::eUnspecified; + } m_unitScalingFactor = DxfUnits::Factor(units) * m_additionalScaling; - ImportObservation("Setting scaling to 1 DXF unit = %gmm based on INSUNITS and " - "DXF scaling option\n", - m_unitScalingFactor); + m_stats.scalingSource = "$INSUNITS"; + m_stats.fileUnits = DxfUnitToString(units); } return true; } @@ -2867,12 +2920,10 @@ bool CDxfRead::ReadVariable() get_next_record(); int varValue = 1; if (m_unitScalingFactor == 0.0 && ParseValue(this, &varValue)) { - m_unitScalingFactor = - DxfUnits::Factor(varValue != 0 ? DxfUnits::eMillimeters : DxfUnits::eInches) - * m_additionalScaling; - ImportObservation("Setting scaling to 1 DXF unit = %gmm based on MEASUREMENT and " - "DXF scaling option\n", - m_unitScalingFactor); + auto units = (varValue != 0 ? DxfUnits::eMillimeters : DxfUnits::eInches); + m_unitScalingFactor = DxfUnits::Factor(units) * m_additionalScaling; + m_stats.scalingSource = "$MEASUREMENT"; + m_stats.fileUnits = DxfUnitToString(units); } return true; } diff --git a/src/Mod/Import/App/dxf/dxf.h b/src/Mod/Import/App/dxf/dxf.h index 7af0b45a73..ac3d6b6f77 100644 --- a/src/Mod/Import/App/dxf/dxf.h +++ b/src/Mod/Import/App/dxf/dxf.h @@ -182,6 +182,9 @@ struct DxfImportStats double importTimeSeconds = 0.0; std::string dxfVersion; std::string dxfEncoding; + std::string scalingSource; + std::string fileUnits; + double finalScalingFactor = 1.0; std::map entityCounts; std::map importSettings; int totalEntitiesCreated = 0; From e8ce264507b38209ca597bd76d7fbb95e94cc252 Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Wed, 18 Jun 2025 09:22:03 +0200 Subject: [PATCH 06/18] Import: DXF parser/imported improve unsupported features count Classify unsupported features by type, and report on the breakdown. --- src/Mod/Import/App/dxf/ImpExpDxf.cpp | 7 ++++++- src/Mod/Import/App/dxf/dxf.cpp | 2 +- src/Mod/Import/App/dxf/dxf.h | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.cpp b/src/Mod/Import/App/dxf/ImpExpDxf.cpp index 5d48ce770e..b9adb639e1 100644 --- a/src/Mod/Import/App/dxf/ImpExpDxf.cpp +++ b/src/Mod/Import/App/dxf/ImpExpDxf.cpp @@ -1384,7 +1384,6 @@ Py::Object ImpExpDxfRead::getStatsAsPyObject() statsDict.setItem("finalScalingFactor", Py::Float(m_stats.finalScalingFactor)); statsDict.setItem("importTimeSeconds", Py::Float(m_stats.importTimeSeconds)); statsDict.setItem("totalEntitiesCreated", Py::Long(m_stats.totalEntitiesCreated)); - statsDict.setItem("unsupportedFeaturesCount", Py::Long(m_stats.unsupportedFeaturesCount)); Py::Dict entityCountsDict; for (const auto& pair : m_stats.entityCounts) { @@ -1398,5 +1397,11 @@ Py::Object ImpExpDxfRead::getStatsAsPyObject() } statsDict.setItem("importSettings", importSettingsDict); + Py::Dict unsupportedFeaturesDict; + for (const auto& pair : m_stats.unsupportedFeatures) { + unsupportedFeaturesDict.setItem(pair.first.c_str(), Py::Long(pair.second)); + } + statsDict.setItem("unsupportedFeatures", unsupportedFeaturesDict); + return statsDict; } diff --git a/src/Mod/Import/App/dxf/dxf.cpp b/src/Mod/Import/App/dxf/dxf.cpp index 81117d3e36..7028ff56a1 100644 --- a/src/Mod/Import/App/dxf/dxf.cpp +++ b/src/Mod/Import/App/dxf/dxf.cpp @@ -2397,9 +2397,9 @@ bool CDxfRead::SkipBlockContents() template void CDxfRead::UnsupportedFeature(const char* format, args&&... argValuess) { - m_stats.unsupportedFeaturesCount++; // NOLINTNEXTLINE(runtime/printf) std::string formattedMessage = fmt::sprintf(format, std::forward(argValuess)...); + m_stats.unsupportedFeatures[formattedMessage]++; // We place these formatted messages in a map, count their occurrences and not their first // occurrence. if (m_unsupportedFeaturesNoted[formattedMessage].first++ == 0) { diff --git a/src/Mod/Import/App/dxf/dxf.h b/src/Mod/Import/App/dxf/dxf.h index ac3d6b6f77..b6a04a1cad 100644 --- a/src/Mod/Import/App/dxf/dxf.h +++ b/src/Mod/Import/App/dxf/dxf.h @@ -187,8 +187,8 @@ struct DxfImportStats double finalScalingFactor = 1.0; std::map entityCounts; std::map importSettings; + std::map unsupportedFeatures; int totalEntitiesCreated = 0; - int unsupportedFeaturesCount = 0; }; From ceb72924c292aaf78542c7763daef2fdcde18bfb Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Sat, 14 Jun 2025 09:33:02 +0200 Subject: [PATCH 07/18] Import: DXF parser, disable now redundant unsupported entities reporting --- src/Mod/Import/App/dxf/dxf.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Mod/Import/App/dxf/dxf.cpp b/src/Mod/Import/App/dxf/dxf.cpp index 7028ff56a1..4be44063da 100644 --- a/src/Mod/Import/App/dxf/dxf.cpp +++ b/src/Mod/Import/App/dxf/dxf.cpp @@ -2400,11 +2400,12 @@ void CDxfRead::UnsupportedFeature(const char* format, args&&... argValuess) // NOLINTNEXTLINE(runtime/printf) std::string formattedMessage = fmt::sprintf(format, std::forward(argValuess)...); m_stats.unsupportedFeatures[formattedMessage]++; + // *** This message is now disabled here because we use the stats reporter instead *** // We place these formatted messages in a map, count their occurrences and not their first // occurrence. - if (m_unsupportedFeaturesNoted[formattedMessage].first++ == 0) { - m_unsupportedFeaturesNoted[formattedMessage].second = m_line; - } + // if (m_unsupportedFeaturesNoted[formattedMessage].first++ == 0) { + // m_unsupportedFeaturesNoted[formattedMessage].second = m_line; + // } } bool CDxfRead::get_next_record() From 77e404787183d7a3f7b220cd0c9b5627609fa2aa Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Sat, 14 Jun 2025 08:28:53 +0200 Subject: [PATCH 08/18] Import: DXF frontend, improve scale reporting --- src/Mod/Draft/importDXF.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py index 191d04392b..84e1c8ed33 100644 --- a/src/Mod/Draft/importDXF.py +++ b/src/Mod/Draft/importDXF.py @@ -4215,7 +4215,9 @@ class DxfImportReporter: self.stats = stats_dict def to_console_string(self): - """Formats the statistics into a human-readable string for console output.""" + """ + Formats the statistics into a human-readable string for console output. + """ if not self.stats: return "DXF Import: No statistics were returned from the importer.\n" @@ -4224,7 +4226,21 @@ class DxfImportReporter: # General Info lines.append(f"DXF Version: {self.stats.get('dxfVersion', 'Unknown')}") lines.append(f"File Encoding: {self.stats.get('dxfEncoding', 'Unknown')}") - # Timing will be 0.0 for now, but the line is ready for when it's fixed. + + # Scaling Info + file_units = self.stats.get('fileUnits', 'Not specified') + source = self.stats.get('scalingSource', '') + if source: + lines.append(f"File Units: {file_units} (from {source})") + else: + lines.append(f"File Units: {file_units}") + + manual_scaling = self.stats.get('importSettings', {}).get('Manual scaling factor', '1.0') + lines.append(f"Manual Scaling Factor: {manual_scaling}") + + final_scaling = self.stats.get('finalScalingFactor', 1.0) + lines.append(f"Final Scaling: 1 DXF unit = {final_scaling:.4f} mm") + import_time = self.stats.get('importTimeSeconds', 0.0) lines.append(f"Import Time: {import_time:.4f} seconds") lines.append("") @@ -4251,9 +4267,16 @@ class DxfImportReporter: lines.append(f" Total entities read: {total_read}") else: lines.append(" (No entities recorded)") - lines.append(f"FreeCAD objects created: {self.stats.get('totalEntitiesCreated', 0)}") - lines.append(f"Unsupported features: {self.stats.get('unsupportedFeaturesCount', 0)}") + lines.append("") + + lines.append("Unsupported Features:") + unsupported = self.stats.get('unsupportedFeatures', {}) + if unsupported: + for key, value in sorted(unsupported.items()): + lines.append(f" - {key}: {value} time(s)") + else: + lines.append(" (None)") lines.append("--- End of Summary ---\n") return "\n".join(lines) From f84ed59a43a4ce67ef6728c0deb056ef963ab85f Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Fri, 13 Jun 2025 04:07:39 +0200 Subject: [PATCH 09/18] Import: DXF backend, frontent; add time measurement --- src/Mod/Draft/importDXF.py | 27 ++++++++++++++++++++------- src/Mod/Import/App/AppImportPy.cpp | 8 +++++++- src/Mod/Import/App/dxf/dxf.h | 4 ++++ src/Mod/Import/Gui/AppImportGuiPy.cpp | 7 +++++++ 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py index 84e1c8ed33..edde131cdb 100644 --- a/src/Mod/Draft/importDXF.py +++ b/src/Mod/Draft/importDXF.py @@ -54,6 +54,7 @@ import sys import os import math import re +import time import FreeCAD import Part import Draft @@ -2810,6 +2811,8 @@ def open(filename): Use local variables, not global variables. """ readPreferences() + total_start_time = time.perf_counter() + if dxfUseLegacyImporter: getDXFlibs() if dxfReader: @@ -2825,6 +2828,7 @@ def open(filename): doc = FreeCAD.newDocument(docname) doc.Label = docname FreeCAD.setActiveDocument(doc.Name) + stats = None if gui: import ImportGui stats = ImportGui.readDXF(filename) @@ -2832,8 +2836,9 @@ def open(filename): import Import stats = Import.readDXF(filename) + total_end_time = time.perf_counter() if stats: - reporter = DxfImportReporter(stats) + reporter = DxfImportReporter(stats, total_end_time - total_start_time) reporter.report_to_console() Draft.convert_draft_texts() # convert annotations to Draft texts @@ -2860,6 +2865,7 @@ def insert(filename, docname): Use local variables, not global variables. """ readPreferences() + total_start_time = time.perf_counter() try: doc = FreeCAD.getDocument(docname) except NameError: @@ -2872,6 +2878,7 @@ def insert(filename, docname): else: errorDXFLib(gui) else: + stats = None if gui: import ImportGui stats = ImportGui.readDXF(filename) @@ -2879,8 +2886,9 @@ def insert(filename, docname): import Import stats = Import.readDXF(filename) + total_end_time = time.perf_counter() if stats: - reporter = DxfImportReporter(stats) + reporter = DxfImportReporter(stats, total_end_time - total_start_time) reporter.report_to_console() Draft.convert_draft_texts() # convert annotations to Draft texts @@ -4211,8 +4219,9 @@ def readPreferences(): class DxfImportReporter: """Formats and reports statistics from a DXF import process.""" - def __init__(self, stats_dict): + def __init__(self, stats_dict, total_time=0.0): self.stats = stats_dict + self.total_time = total_time def to_console_string(self): """ @@ -4223,11 +4232,11 @@ class DxfImportReporter: lines = ["\n--- DXF Import Summary ---"] - # General Info + # General info lines.append(f"DXF Version: {self.stats.get('dxfVersion', 'Unknown')}") lines.append(f"File Encoding: {self.stats.get('dxfEncoding', 'Unknown')}") - # Scaling Info + # Scaling info file_units = self.stats.get('fileUnits', 'Not specified') source = self.stats.get('scalingSource', '') if source: @@ -4240,9 +4249,13 @@ class DxfImportReporter: final_scaling = self.stats.get('finalScalingFactor', 1.0) lines.append(f"Final Scaling: 1 DXF unit = {final_scaling:.4f} mm") + lines.append("") - import_time = self.stats.get('importTimeSeconds', 0.0) - lines.append(f"Import Time: {import_time:.4f} seconds") + # Timing + lines.append("Performance:") + cpp_time = self.stats.get('importTimeSeconds', 0.0) + lines.append(f" - C++ Import Time: {cpp_time:.4f} seconds") + lines.append(f" - Total Import Time: {self.total_time:.4f} seconds") lines.append("") # Settings diff --git a/src/Mod/Import/App/AppImportPy.cpp b/src/Mod/Import/App/AppImportPy.cpp index 91512a4a2e..362700b83c 100644 --- a/src/Mod/Import/App/AppImportPy.cpp +++ b/src/Mod/Import/App/AppImportPy.cpp @@ -48,6 +48,7 @@ #endif #endif +#include #include "dxf/ImpExpDxf.h" #include "SketchExportHelper.h" #include @@ -413,7 +414,13 @@ private: ImpExpDxfRead dxf_file(EncodedName, pcDoc); dxf_file.setOptionSource(defaultOptions); dxf_file.setOptions(); + + auto startTime = std::chrono::high_resolution_clock::now(); dxf_file.DoRead(IgnoreErrors); + auto endTime = std::chrono::high_resolution_clock::now(); + std::chrono::duration elapsed = endTime - startTime; + dxf_file.setImportTime(elapsed.count()); + pcDoc->recompute(); return dxf_file.getStatsAsPyObject(); } @@ -423,7 +430,6 @@ private: catch (const Base::Exception& e) { throw Py::RuntimeError(e.what()); } - return Py::None(); } diff --git a/src/Mod/Import/App/dxf/dxf.h b/src/Mod/Import/App/dxf/dxf.h index b6a04a1cad..8bd97937d4 100644 --- a/src/Mod/Import/App/dxf/dxf.h +++ b/src/Mod/Import/App/dxf/dxf.h @@ -862,6 +862,10 @@ public: { return m_fail; } + void setImportTime(double seconds) + { + m_stats.importTimeSeconds = seconds; + } void DoRead(bool ignore_errors = false); // this reads the file and calls the following functions virtual void StartImport() diff --git a/src/Mod/Import/Gui/AppImportGuiPy.cpp b/src/Mod/Import/Gui/AppImportGuiPy.cpp index 700f439736..0e4dc03c11 100644 --- a/src/Mod/Import/Gui/AppImportGuiPy.cpp +++ b/src/Mod/Import/Gui/AppImportGuiPy.cpp @@ -46,6 +46,7 @@ #endif #endif +#include #include "ExportOCAFGui.h" #include "ImportOCAFGui.h" #include "OCAFBrowser.h" @@ -401,7 +402,13 @@ private: ImpExpDxfReadGui dxf_file(EncodedName, pcDoc); dxf_file.setOptionSource(defaultOptions); dxf_file.setOptions(); + + auto startTime = std::chrono::high_resolution_clock::now(); dxf_file.DoRead(IgnoreErrors); + auto endTime = std::chrono::high_resolution_clock::now(); + std::chrono::duration elapsed = endTime - startTime; + dxf_file.setImportTime(elapsed.count()); + pcDoc->recompute(); return dxf_file.getStatsAsPyObject(); } From d77cdf999b91b0da21cc8acf196b26a4850befa7 Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Fri, 13 Jun 2025 04:30:53 +0200 Subject: [PATCH 10/18] Import: DXF parser, add DXF block count --- src/Mod/Import/App/dxf/dxf.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Mod/Import/App/dxf/dxf.cpp b/src/Mod/Import/App/dxf/dxf.cpp index 4be44063da..942838bf4f 100644 --- a/src/Mod/Import/App/dxf/dxf.cpp +++ b/src/Mod/Import/App/dxf/dxf.cpp @@ -2349,6 +2349,7 @@ bool CDxfRead::ReadBlockInfo() int blockType = 0; std::string blockName; InitializeAttributes(); + m_stats.entityCounts["BLOCK"]++; // Both 2 and 3 are the block name. SetupStringAttribute(eName, blockName); SetupStringAttribute(eExtraText, blockName); From 9eaecf68030a89b65bebc30df91fd8595a41d486 Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Fri, 13 Jun 2025 04:35:23 +0200 Subject: [PATCH 11/18] Import: DXF parser, add entities in paperspace count --- src/Mod/Import/App/dxf/dxf.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Mod/Import/App/dxf/dxf.cpp b/src/Mod/Import/App/dxf/dxf.cpp index 942838bf4f..01d59a04d0 100644 --- a/src/Mod/Import/App/dxf/dxf.cpp +++ b/src/Mod/Import/App/dxf/dxf.cpp @@ -2002,6 +2002,9 @@ void CDxfRead::ProcessAllEntityAttributes() void CDxfRead::ResolveEntityAttributes() { m_entityAttributes.ResolveBylayerAttributes(*this); + if (m_entityAttributes.m_paperSpace) { + m_stats.entityCounts["ENTITIES_IN_PAPERSPACE"]++; + } // TODO: Look at the space and layer (hidden/frozen?) and options and return false if the entity // is not needed. // TODO: INSERT must not call this because an INSERT on a hidden layer should always be From 789ca45280aea100328b070518f902b3b3e8d15e Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Wed, 18 Jun 2025 09:47:05 +0200 Subject: [PATCH 12/18] Import: DXF importer, add code comments to C++ to Python stats passing function --- src/Mod/Import/App/dxf/ImpExpDxf.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.cpp b/src/Mod/Import/App/dxf/ImpExpDxf.cpp index b9adb639e1..534f2bcf4e 100644 --- a/src/Mod/Import/App/dxf/ImpExpDxf.cpp +++ b/src/Mod/Import/App/dxf/ImpExpDxf.cpp @@ -1375,8 +1375,10 @@ void ImpExpDxfWrite::exportDiametricDim(Base::Vector3d textLocn, Py::Object ImpExpDxfRead::getStatsAsPyObject() { + // Create a Python dictionary to hold all import statistics. Py::Dict statsDict; + // Populate the dictionary with general information about the import. statsDict.setItem("dxfVersion", Py::String(m_stats.dxfVersion)); statsDict.setItem("dxfEncoding", Py::String(m_stats.dxfEncoding)); statsDict.setItem("scalingSource", Py::String(m_stats.scalingSource)); @@ -1385,23 +1387,27 @@ Py::Object ImpExpDxfRead::getStatsAsPyObject() statsDict.setItem("importTimeSeconds", Py::Float(m_stats.importTimeSeconds)); statsDict.setItem("totalEntitiesCreated", Py::Long(m_stats.totalEntitiesCreated)); + // Create a nested dictionary for the counts of each DXF entity type read. Py::Dict entityCountsDict; for (const auto& pair : m_stats.entityCounts) { entityCountsDict.setItem(pair.first.c_str(), Py::Long(pair.second)); } statsDict.setItem("entityCounts", entityCountsDict); + // Create a nested dictionary for the import settings used for this session. Py::Dict importSettingsDict; for (const auto& pair : m_stats.importSettings) { importSettingsDict.setItem(pair.first.c_str(), Py::String(pair.second)); } statsDict.setItem("importSettings", importSettingsDict); + // Create a nested dictionary for any unsupported DXF features encountered. Py::Dict unsupportedFeaturesDict; for (const auto& pair : m_stats.unsupportedFeatures) { unsupportedFeaturesDict.setItem(pair.first.c_str(), Py::Long(pair.second)); } statsDict.setItem("unsupportedFeatures", unsupportedFeaturesDict); + // Return the fully populated statistics dictionary to the Python caller. return statsDict; } From 3f606177e3f65e723912fc84469d84e5e08c2ee1 Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Wed, 18 Jun 2025 09:54:59 +0200 Subject: [PATCH 13/18] Import: DXF parser, remove original unsupported features count and warning --- src/Mod/Import/App/dxf/dxf.cpp | 17 ----------------- src/Mod/Import/App/dxf/dxf.h | 1 - 2 files changed, 18 deletions(-) diff --git a/src/Mod/Import/App/dxf/dxf.cpp b/src/Mod/Import/App/dxf/dxf.cpp index 01d59a04d0..7afbb3c887 100644 --- a/src/Mod/Import/App/dxf/dxf.cpp +++ b/src/Mod/Import/App/dxf/dxf.cpp @@ -2404,12 +2404,6 @@ void CDxfRead::UnsupportedFeature(const char* format, args&&... argValuess) // NOLINTNEXTLINE(runtime/printf) std::string formattedMessage = fmt::sprintf(format, std::forward(argValuess)...); m_stats.unsupportedFeatures[formattedMessage]++; - // *** This message is now disabled here because we use the stats reporter instead *** - // We place these formatted messages in a map, count their occurrences and not their first - // occurrence. - // if (m_unsupportedFeaturesNoted[formattedMessage].first++ == 0) { - // m_unsupportedFeaturesNoted[formattedMessage].second = m_line; - // } } bool CDxfRead::get_next_record() @@ -2730,17 +2724,6 @@ void CDxfRead::DoRead(const bool ignore_errors /* = false */) } } FinishImport(); - - // Flush out any unsupported features messages - if (!m_unsupportedFeaturesNoted.empty()) { - ImportError("Unsupported DXF features:\n"); - for (auto& featureInfo : m_unsupportedFeaturesNoted) { - ImportError("%s: %d time(s) first at line %d\n", - featureInfo.first, - featureInfo.second.first, - featureInfo.second.second); - } - } } catch (const Base::Exception& e) { // This catches specific FreeCAD exceptions and re-throws them. diff --git a/src/Mod/Import/App/dxf/dxf.h b/src/Mod/Import/App/dxf/dxf.h index 8bd97937d4..caafca4989 100644 --- a/src/Mod/Import/App/dxf/dxf.h +++ b/src/Mod/Import/App/dxf/dxf.h @@ -716,7 +716,6 @@ protected: void UnsupportedFeature(const char* format, args&&... argValues); private: - std::map> m_unsupportedFeaturesNoted; std::string m_CodePage; // Code Page name from $DWGCODEPAGE or null if none/not read yet // The following was going to be python's canonical name for the encoding, but this is (a) not // easily found and (b) does not speed up finding the encoding object. From 89d2937afd822c8c1710865750861c44d989b185 Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Sun, 22 Jun 2025 13:11:00 +0200 Subject: [PATCH 14/18] Import: DXF parser, fix macOS linker issue --- src/Mod/Import/App/dxf/dxf.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Mod/Import/App/dxf/dxf.cpp b/src/Mod/Import/App/dxf/dxf.cpp index 7afbb3c887..b74284ad1f 100644 --- a/src/Mod/Import/App/dxf/dxf.cpp +++ b/src/Mod/Import/App/dxf/dxf.cpp @@ -3179,3 +3179,5 @@ Base::Color CDxfRead::ObjectColor(ColorIndex_t index) return result; } // NOLINTEND(cppcoreguidelines-avoid-magic-numbers, readability-magic-numbers) + +template void CDxfRead::UnsupportedFeature<>(const char*); From 318f205708440bc8bc17c429f0f584b9a9a4a2a4 Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Mon, 23 Jun 2025 23:01:39 +0200 Subject: [PATCH 15/18] Import: DXF backend/frontend, report more unsupported features info Now for unsupported features we report on line numbers and on the entity handle, in case that unsupported feature is a DXF entity. To avoid flooding the output, only a maximum of 5 instances are reported with details. The rest simply add up to the global count and are ellipsized in the report. Report output is now in sentence case. --- src/Mod/Draft/importDXF.py | 52 ++++++++++++++++++---------- src/Mod/Import/App/dxf/ImpExpDxf.cpp | 9 ++++- src/Mod/Import/App/dxf/dxf.cpp | 6 ++-- src/Mod/Import/App/dxf/dxf.h | 4 ++- 4 files changed, 49 insertions(+), 22 deletions(-) diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py index edde131cdb..3791e910ca 100644 --- a/src/Mod/Draft/importDXF.py +++ b/src/Mod/Draft/importDXF.py @@ -4228,38 +4228,38 @@ class DxfImportReporter: Formats the statistics into a human-readable string for console output. """ if not self.stats: - return "DXF Import: No statistics were returned from the importer.\n" + return "DXF Import: no statistics were returned from the importer.\n" - lines = ["\n--- DXF Import Summary ---"] + lines = ["\n--- DXF import summary ---"] # General info - lines.append(f"DXF Version: {self.stats.get('dxfVersion', 'Unknown')}") - lines.append(f"File Encoding: {self.stats.get('dxfEncoding', 'Unknown')}") + lines.append(f"DXF version: {self.stats.get('dxfVersion', 'Unknown')}") + lines.append(f"File encoding: {self.stats.get('dxfEncoding', 'Unknown')}") # Scaling info file_units = self.stats.get('fileUnits', 'Not specified') source = self.stats.get('scalingSource', '') if source: - lines.append(f"File Units: {file_units} (from {source})") + lines.append(f"File units: {file_units} (from {source})") else: - lines.append(f"File Units: {file_units}") + lines.append(f"File units: {file_units}") manual_scaling = self.stats.get('importSettings', {}).get('Manual scaling factor', '1.0') - lines.append(f"Manual Scaling Factor: {manual_scaling}") + lines.append(f"Manual scaling factor: {manual_scaling}") final_scaling = self.stats.get('finalScalingFactor', 1.0) - lines.append(f"Final Scaling: 1 DXF unit = {final_scaling:.4f} mm") + lines.append(f"Final scaling: 1 DXF unit = {final_scaling:.4f} mm") lines.append("") # Timing lines.append("Performance:") cpp_time = self.stats.get('importTimeSeconds', 0.0) - lines.append(f" - C++ Import Time: {cpp_time:.4f} seconds") - lines.append(f" - Total Import Time: {self.total_time:.4f} seconds") + lines.append(f" - C++ import time: {cpp_time:.4f} seconds") + lines.append(f" - Total import time: {self.total_time:.4f} seconds") lines.append("") # Settings - lines.append("Import Settings:") + lines.append("Import settings:") settings = self.stats.get('importSettings', {}) if settings: for key, value in sorted(settings.items()): @@ -4269,7 +4269,7 @@ class DxfImportReporter: lines.append("") # Counts - lines.append("Entity Counts:") + lines.append("Entity counts:") total_read = 0 entities = self.stats.get('entityCounts', {}) if entities: @@ -4283,15 +4283,31 @@ class DxfImportReporter: lines.append(f"FreeCAD objects created: {self.stats.get('totalEntitiesCreated', 0)}") lines.append("") - lines.append("Unsupported Features:") + lines.append("Import issues and unsupported features:") unsupported = self.stats.get('unsupportedFeatures', {}) if unsupported: - for key, value in sorted(unsupported.items()): - lines.append(f" - {key}: {value} time(s)") - else: - lines.append(" (None)") + for key, occurrences in sorted(unsupported.items()): + count = len(occurrences) + max_details_to_show = 5 - lines.append("--- End of Summary ---\n") + details_list = [] + for i, (line, handle) in enumerate(occurrences): + if i >= max_details_to_show: + break + if handle: + details_list.append(f"line {line} (handle {handle})") + else: + details_list.append(f"line {line} (no handle available)") + + details_str = ", ".join(details_list) + if count > max_details_to_show: + lines.append(f" - {key}: {count} time(s). Examples: {details_str}, ...") + else: + lines.append(f" - {key}: {count} time(s) at {details_str}") + else: + lines.append(" (none)") + + lines.append("--- End of summary ---\n") return "\n".join(lines) def report_to_console(self): diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.cpp b/src/Mod/Import/App/dxf/ImpExpDxf.cpp index 534f2bcf4e..815624f038 100644 --- a/src/Mod/Import/App/dxf/ImpExpDxf.cpp +++ b/src/Mod/Import/App/dxf/ImpExpDxf.cpp @@ -1404,7 +1404,14 @@ Py::Object ImpExpDxfRead::getStatsAsPyObject() // Create a nested dictionary for any unsupported DXF features encountered. Py::Dict unsupportedFeaturesDict; for (const auto& pair : m_stats.unsupportedFeatures) { - unsupportedFeaturesDict.setItem(pair.first.c_str(), Py::Long(pair.second)); + Py::List occurrencesList; + for (const auto& occurrence : pair.second) { + Py::Tuple infoTuple(2); + infoTuple.setItem(0, Py::Long(occurrence.first)); + infoTuple.setItem(1, Py::String(occurrence.second)); + occurrencesList.append(infoTuple); + } + unsupportedFeaturesDict.setItem(pair.first.c_str(), occurrencesList); } statsDict.setItem("unsupportedFeatures", unsupportedFeaturesDict); diff --git a/src/Mod/Import/App/dxf/dxf.cpp b/src/Mod/Import/App/dxf/dxf.cpp index b74284ad1f..41f1f064dc 100644 --- a/src/Mod/Import/App/dxf/dxf.cpp +++ b/src/Mod/Import/App/dxf/dxf.cpp @@ -2342,8 +2342,8 @@ bool CDxfRead::ReadDimension() bool CDxfRead::ReadUnknownEntity() { - UnsupportedFeature("Entity type '%s'", m_record_data); ProcessAllEntityAttributes(); + UnsupportedFeature("Entity type '%s'", m_record_data); return true; } @@ -2403,7 +2403,7 @@ void CDxfRead::UnsupportedFeature(const char* format, args&&... argValuess) { // NOLINTNEXTLINE(runtime/printf) std::string formattedMessage = fmt::sprintf(format, std::forward(argValuess)...); - m_stats.unsupportedFeatures[formattedMessage]++; + m_stats.unsupportedFeatures[formattedMessage].emplace_back(m_line, m_current_entity_handle); } bool CDxfRead::get_next_record() @@ -2798,6 +2798,8 @@ bool CDxfRead::ReadEntity() { InitializeAttributes(); m_entityAttributes.SetDefaults(); + m_current_entity_handle.clear(); + SetupStringAttribute(eHandle, m_current_entity_handle); EntityNormalVector.Set(0, 0, 1); Setup3DVectorAttribute(eExtrusionDirection, EntityNormalVector); SetupStringAttribute(eLinetypeName, m_entityAttributes.m_LineType); diff --git a/src/Mod/Import/App/dxf/dxf.h b/src/Mod/Import/App/dxf/dxf.h index caafca4989..1625822977 100644 --- a/src/Mod/Import/App/dxf/dxf.h +++ b/src/Mod/Import/App/dxf/dxf.h @@ -187,7 +187,7 @@ struct DxfImportStats double finalScalingFactor = 1.0; std::map entityCounts; std::map importSettings; - std::map unsupportedFeatures; + std::map>> unsupportedFeatures; int totalEntitiesCreated = 0; }; @@ -200,6 +200,7 @@ enum eDXFGroupCode_t ePrimaryText = 1, eName = 2, eExtraText = 3, + eHandle = 5, eLinetypeName = 6, eTextStyleName = 7, eLayerName = 8, @@ -462,6 +463,7 @@ private: bool m_not_eof = true; int m_line = 0; bool m_repeat_last_record = false; + std::string m_current_entity_handle; // The scaling from DXF units to millimetres. // This does not include the dxfScaling option From 41709fa3382454451f8b1f52a36918c046fc1c4f Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:19:30 +0200 Subject: [PATCH 16/18] Import: DXF parser, correctly pass line no. and handle --- src/Mod/Import/App/dxf/dxf.cpp | 7 +++++-- src/Mod/Import/App/dxf/dxf.h | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Mod/Import/App/dxf/dxf.cpp b/src/Mod/Import/App/dxf/dxf.cpp index 41f1f064dc..3e780d3e5b 100644 --- a/src/Mod/Import/App/dxf/dxf.cpp +++ b/src/Mod/Import/App/dxf/dxf.cpp @@ -2343,7 +2343,7 @@ bool CDxfRead::ReadDimension() bool CDxfRead::ReadUnknownEntity() { ProcessAllEntityAttributes(); - UnsupportedFeature("Entity type '%s'", m_record_data); + UnsupportedFeature("Entity type '%s'", m_current_entity_name.c_str()); return true; } @@ -2403,7 +2403,8 @@ void CDxfRead::UnsupportedFeature(const char* format, args&&... argValuess) { // NOLINTNEXTLINE(runtime/printf) std::string formattedMessage = fmt::sprintf(format, std::forward(argValuess)...); - m_stats.unsupportedFeatures[formattedMessage].emplace_back(m_line, m_current_entity_handle); + m_stats.unsupportedFeatures[formattedMessage].emplace_back(m_current_entity_line_number, + m_current_entity_handle); } bool CDxfRead::get_next_record() @@ -2796,6 +2797,8 @@ void CDxfRead::ProcessLayerReference(CDxfRead* object, void* target) } bool CDxfRead::ReadEntity() { + m_current_entity_line_number = m_line; + m_current_entity_name = m_record_data; InitializeAttributes(); m_entityAttributes.SetDefaults(); m_current_entity_handle.clear(); diff --git a/src/Mod/Import/App/dxf/dxf.h b/src/Mod/Import/App/dxf/dxf.h index 1625822977..92b3ce472c 100644 --- a/src/Mod/Import/App/dxf/dxf.h +++ b/src/Mod/Import/App/dxf/dxf.h @@ -463,6 +463,8 @@ private: bool m_not_eof = true; int m_line = 0; bool m_repeat_last_record = false; + int m_current_entity_line_number = 0; + std::string m_current_entity_name; std::string m_current_entity_handle; // The scaling from DXF units to millimetres. From 9ac905b140e63bb8b47f27ae3fe51ab8e151dd50 Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:19:46 +0200 Subject: [PATCH 17/18] Import: DXF reporter, add filename to report --- src/Mod/Draft/importDXF.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py index 3791e910ca..b6c38201f7 100644 --- a/src/Mod/Draft/importDXF.py +++ b/src/Mod/Draft/importDXF.py @@ -2838,7 +2838,7 @@ def open(filename): total_end_time = time.perf_counter() if stats: - reporter = DxfImportReporter(stats, total_end_time - total_start_time) + reporter = DxfImportReporter(filename, stats, total_end_time - total_start_time) reporter.report_to_console() Draft.convert_draft_texts() # convert annotations to Draft texts @@ -2888,7 +2888,7 @@ def insert(filename, docname): total_end_time = time.perf_counter() if stats: - reporter = DxfImportReporter(stats, total_end_time - total_start_time) + reporter = DxfImportReporter(filename, stats, total_end_time - total_start_time) reporter.report_to_console() Draft.convert_draft_texts() # convert annotations to Draft texts @@ -4219,7 +4219,8 @@ def readPreferences(): class DxfImportReporter: """Formats and reports statistics from a DXF import process.""" - def __init__(self, stats_dict, total_time=0.0): + def __init__(self, filename, stats_dict, total_time=0.0): + self.filename = filename self.stats = stats_dict self.total_time = total_time @@ -4231,6 +4232,7 @@ class DxfImportReporter: return "DXF Import: no statistics were returned from the importer.\n" lines = ["\n--- DXF import summary ---"] + lines.append(f"Import of file: '{self.filename}'\n") # General info lines.append(f"DXF version: {self.stats.get('dxfVersion', 'Unknown')}") @@ -4283,7 +4285,7 @@ class DxfImportReporter: lines.append(f"FreeCAD objects created: {self.stats.get('totalEntitiesCreated', 0)}") lines.append("") - lines.append("Import issues and unsupported features:") + lines.append("Unsupported features:") unsupported = self.stats.get('unsupportedFeatures', {}) if unsupported: for key, occurrences in sorted(unsupported.items()): From 0056f4d9a982e0cabcec91bd8a1419e1792299dc Mon Sep 17 00:00:00 2001 From: Furgo <148809153+furgo16@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:30:34 +0200 Subject: [PATCH 18/18] Import: DXF reporter, add unsupported indicator --- src/Mod/Draft/importDXF.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py index b6c38201f7..b6d94016f9 100644 --- a/src/Mod/Draft/importDXF.py +++ b/src/Mod/Draft/importDXF.py @@ -4273,10 +4273,23 @@ class DxfImportReporter: # Counts lines.append("Entity counts:") total_read = 0 + unsupported_keys = self.stats.get('unsupportedFeatures', {}).keys() + unsupported_entity_names = set() + for key in unsupported_keys: + # Extract the entity name from the key string, e.g., 'HATCH' from "Entity type 'HATCH'" + entity_name_match = re.search(r"\'(.*?)\'", key) + if entity_name_match: + unsupported_entity_names.add(entity_name_match.group(1)) + + has_unsupported_indicator = False entities = self.stats.get('entityCounts', {}) if entities: for key, value in sorted(entities.items()): - lines.append(f" - {key}: {value}") + indicator = "" + if key in unsupported_entity_names: + indicator = " (*)" + has_unsupported_indicator = True + lines.append(f" - {key}: {value}{indicator}") total_read += value lines.append("----------------------------") lines.append(f" Total entities read: {total_read}") @@ -4284,6 +4297,9 @@ class DxfImportReporter: lines.append(" (No entities recorded)") lines.append(f"FreeCAD objects created: {self.stats.get('totalEntitiesCreated', 0)}") lines.append("") + if has_unsupported_indicator: + lines.append("(*) Entity type not supported by importer.") + lines.append("") lines.append("Unsupported features:") unsupported = self.stats.get('unsupportedFeatures', {})