diff --git a/src/Mod/Draft/importDXF.py b/src/Mod/Draft/importDXF.py index b6d94016f9..1325611448 100644 --- a/src/Mod/Draft/importDXF.py +++ b/src/Mod/Draft/importDXF.py @@ -4296,6 +4296,18 @@ class DxfImportReporter: else: lines.append(" (No entities recorded)") lines.append(f"FreeCAD objects created: {self.stats.get('totalEntitiesCreated', 0)}") + + lines.append("") + + # System Blocks + lines.append("System Blocks:") + system_blocks = self.stats.get('systemBlockCounts', {}) + if system_blocks: + for key, value in sorted(system_blocks.items()): + lines.append(f" - {key}: {value}") + else: + lines.append(" (None found or imported)") + lines.append("") if has_unsupported_indicator: lines.append("(*) Entity type not supported by importer.") diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.cpp b/src/Mod/Import/App/dxf/ImpExpDxf.cpp index 815624f038..7a6add1c19 100644 --- a/src/Mod/Import/App/dxf/ImpExpDxf.cpp +++ b/src/Mod/Import/App/dxf/ImpExpDxf.cpp @@ -56,6 +56,7 @@ #include #include #include +#include #include #include #include @@ -66,6 +67,9 @@ #include #include #include +#include +#include +#include #include "ImpExpDxf.h" @@ -87,8 +91,25 @@ ImpExpDxfRead::ImpExpDxfRead(const std::string& filepath, App::Document* pcDoc) setOptions(); } +void ImpExpDxfRead::StartImport() +{ + CDxfRead::StartImport(); + // Create a hidden group to store the base objects for block definitions + m_blockDefinitionGroup = static_cast( + document->addObject("App::DocumentObjectGroup", "_BlockDefinitions")); + m_blockDefinitionGroup->Visibility.setValue(false); + // Create a hidden group to store unreferenced blocks + m_unreferencedBlocksGroup = static_cast( + document->addObject("App::DocumentObjectGroup", "_UnreferencedBlocks")); + m_unreferencedBlocksGroup->Visibility.setValue(false); +} + bool ImpExpDxfRead::ReadEntitiesSection() { + // 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) { std::map> ShapesToCombine; @@ -137,6 +158,22 @@ void ImpExpDxfRead::CombineShapes(std::list& shapes, const char* n } } +TopoDS_Shape ImpExpDxfRead::CombineShapesToCompound(const std::list& shapes) const +{ + if (shapes.empty()) { + return TopoDS_Shape(); + } + BRep_Builder builder; + TopoDS_Compound comp; + builder.MakeCompound(comp); + for (const auto& sh : shapes) { + if (!sh.IsNull()) { + builder.Add(comp, sh); + } + } + return comp; +} + void ImpExpDxfRead::setOptions() { ParameterGrp::handle hGrp = @@ -199,31 +236,364 @@ void ImpExpDxfRead::setOptions() // hGrp->GetBool("dxfhiddenLayers", true); } -bool ImpExpDxfRead::OnReadBlock(const std::string& name, int flags) +void ImpExpDxfRead::ComposeFlattenedBlock(const std::string& blockName, + std::set& composed) { - if ((flags & 0x04) != 0) { - // Note that this doesn't mean there are not entities in the block. I don't - // know if the external reference can be cached because there are two other bits - // here, 0x10 and 0x20, that seem to handle "resolved" external references. - UnsupportedFeature("External (xref) BLOCK"); + // 1. Base Case: If already composed, do nothing. + if (composed.count(blockName)) { + return; } - else if (!m_importHiddenBlocks && (flags & 0x01) != 0) { - // It is an anonymous block used to build dimensions, hatches, etc so we don't need it - // and don't want to be complaining about unhandled entity types. - // Note that if it *is* for a hatch we could actually import it and use it to draw a hatch. + + // 2. Find the raw block data. + auto it = this->Blocks.find(blockName); + if (it == this->Blocks.end()) { + ImportError("Block '%s' is referenced but not defined. Skipping.", blockName.c_str()); + return; } - else if (Blocks.contains(name)) { - ImportError("Duplicate block name '%s'\n", name); + const Block& blockData = it->second; + + // 3. Collect all geometry shapes for this block. + std::list shapeCollection; + + // 4. Process primitive geometry. + for (const auto& [attributes, shapeList] : blockData.Shapes) { + shapeCollection.insert(shapeCollection.end(), shapeList.begin(), shapeList.end()); + } + + // 5. Process nested inserts recursively. + for (const auto& insertAttrPair : blockData.Inserts) { + for (const auto& nestedInsert : insertAttrPair.second) { + // Ensure the nested block is composed first. + ComposeFlattenedBlock(nestedInsert.Name, composed); + // Mark the nested block as referenced so it's not moved to the "Unreferenced" group. + m_referencedBlocks.insert(nestedInsert.Name); + + // Retrieve the final, flattened shape of the nested block. + auto shape_it = m_flattenedBlockShapes.find(nestedInsert.Name); + if (shape_it != m_flattenedBlockShapes.end()) { + if (!shape_it->second.IsNull()) { + // Use the Part::TopoShape wrapper to access the transformShape method. + Part::TopoShape nestedShape(shape_it->second); + // Apply the insert's transformation. + Base::Placement pl( + nestedInsert.Point, + Base::Rotation(Base::Vector3d(0, 0, 1), nestedInsert.Rotation)); + Base::Matrix4D transform = pl.toMatrix(); + transform.scale(nestedInsert.Scale); + nestedShape.transformShape(transform, true, true); // Use copy=true + shapeCollection.push_back(nestedShape.getShape()); + } + } + } + } + + // 6. Build the final merged shape. + TopoDS_Shape finalShape = CombineShapesToCompound(shapeCollection); + m_flattenedBlockShapes[blockName] = finalShape; // Cache the result. + + // 7. Create the final Part::Feature object. + if (!finalShape.IsNull()) { + std::string featureName = "BLOCK_" + blockName; + auto blockFeature = document->addObject( + document->getUniqueObjectName(featureName.c_str()).c_str()); + blockFeature->Shape.setValue(finalShape); + blockFeature->Visibility.setValue(false); + m_blockDefinitionGroup->addObject(blockFeature); + this->m_blockDefinitions[blockName] = blockFeature; + } + + // 8. Mark this block as composed. + composed.insert(blockName); +} + +void ImpExpDxfRead::ComposeParametricBlock(const std::string& blockName, + std::set& composed) +{ + // 1. Base Case: If this block has already been composed, we're done. + if (composed.count(blockName)) { + return; + } + + // 2. Find the raw block data from the parsing phase. + auto it = this->Blocks.find(blockName); + if (it == this->Blocks.end()) { + ImportError("Block '%s' is referenced but not defined. Skipping.", blockName.c_str()); + return; + } + const Block& blockData = it->second; + + // 3. Create the master Part::Compound for this block definition. + std::string compName = "BLOCK_"; + compName += blockName; + auto blockCompound = document->addObject( + document->getUniqueObjectName(compName.c_str()).c_str()); + m_blockDefinitionGroup->addObject(blockCompound); + IncrementCreatedObjectCount(); + blockCompound->Visibility.setValue(false); + this->m_blockDefinitions[blockName] = blockCompound; + + std::vector linkedObjects; + + // 4. Recursively Compose and Link Nested Inserts. + for (const auto& insertAttrPair : blockData.Inserts) { + for (const auto& nestedInsert : insertAttrPair.second) { + // Ensure the dependency is composed before we try to link to it. + ComposeParametricBlock(nestedInsert.Name, composed); + // Mark the nested block as referenced so it's not moved to the "Unreferenced" group. + m_referencedBlocks.insert(nestedInsert.Name); + + // Create the App::Link for this nested insert. + auto baseObjIt = m_blockDefinitions.find(nestedInsert.Name); + if (baseObjIt != m_blockDefinitions.end()) { + // The link's name should be based on the block it is inserting, not the parent. + std::string linkName = "Link_" + nestedInsert.Name; + auto link = document->addObject( + document->getUniqueObjectName(linkName.c_str()).c_str()); + link->setLink(-1, baseObjIt->second); + link->LinkTransform.setValue(false); + + // Apply placement and scale to the link itself. + Base::Placement pl(nestedInsert.Point, + Base::Rotation(Base::Vector3d(0, 0, 1), nestedInsert.Rotation)); + link->Placement.setValue(pl); + link->ScaleVector.setValue(nestedInsert.Scale); + link->Visibility.setValue(false); + IncrementCreatedObjectCount(); + linkedObjects.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, "_"); + } + 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 += "_"; + } + // 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; + } + } + 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()); + + 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); + } + } + } + + // TODO: Add similar logic for blockData.FeatureBuildersList if needed. + + // 6. Finalize the Part::Compound. + if (!linkedObjects.empty()) { + blockCompound->Links.setValues(linkedObjects); + } + + // 7. Mark this block as composed. + composed.insert(blockName); +} + +void ImpExpDxfRead::ComposeBlocks() +{ + std::set composedBlocks; + + if (m_mergeOption == MergeShapes) { + // User wants flattened geometry for performance. + for (const auto& pair : this->Blocks) { + if (composedBlocks.find(pair.first) == composedBlocks.end()) { + ComposeFlattenedBlock(pair.first, composedBlocks); + } + } } else { - Block& block = Blocks.insert(std::make_pair(name, Block(name, flags))).first->second; - BlockDefinitionCollector blockCollector(*this, - block.Shapes, - block.FeatureBuildersList, - block.Inserts); - return ReadBlockContents(); + // User wants a parametric, editable structure. + for (const auto& pair : this->Blocks) { + if (composedBlocks.find(pair.first) == composedBlocks.end()) { + ComposeParametricBlock(pair.first, composedBlocks); + } + } } - return SkipBlockContents(); +} + +void ImpExpDxfRead::FinishImport() +{ + // This function runs after all blocks have been parsed and composed. + // It sorts all created block definitions into two groups: those that are + // actively referenced in the drawing, and those that are not. + + std::vector referenced; + std::vector unreferenced; + + for (const auto& pair : m_blockDefinitions) { + const std::string& blockName = pair.first; + App::DocumentObject* blockObj = pair.second; + + bool is_referenced = (m_referencedBlocks.find(blockName) != m_referencedBlocks.end()); + + // A block is considered "referenced" if it was explicitly inserted + // or if it is an anonymous system block (e.g., for dimensions). + // All other named blocks are considered unreferenced if not found in the set. + if (is_referenced || (blockName.rfind('*', 0) == 0)) { + referenced.push_back(blockObj); + } + else { + unreferenced.push_back(blockObj); + } + } + + // Re-assign the group contents by setting the PropertyLinkList for each group. + // This correctly re-parents the objects in the document's dependency graph. + m_blockDefinitionGroup->Group.setValues(referenced); + m_unreferencedBlocksGroup->Group.setValues(unreferenced); + + // Final cleanup: If the unreferenced group is empty, remove it to avoid + // unnecessary clutter in the document tree. Otherwise, ensure it's hidden. + if (unreferenced.empty()) { + try { + document->removeObject(m_unreferencedBlocksGroup->getNameInDocument()); + } + catch (const Base::Exception& e) { + // It's not critical if removal fails, but we should log it. + e.reportException(); + } + } + else { + m_unreferencedBlocksGroup->Visibility.setValue(false); + } + + // If no blocks were defined in the file at all, remove the main definitions + // group as well to keep the document clean. + if (m_blockDefinitionGroup && m_blockDefinitionGroup->Group.getValues().empty()) { + try { + document->removeObject(m_blockDefinitionGroup->getNameInDocument()); + } + catch (const Base::Exception& e) { + e.reportException(); + } + } + + // call the base class implementation if it has one + CDxfRead::FinishImport(); +} + +bool ImpExpDxfRead::OnReadBlock(const std::string& name, int flags) +{ + // Step 1: Check for external references first. This is a critical check. + if ((flags & 0x04) != 0) { // Block is an Xref + UnsupportedFeature("External (xref) BLOCK"); + return SkipBlockContents(); + } + + // Step 2: Check if the block is anonymous/system. + bool isAnonymous = (name.find('*') == 0); + if (isAnonymous) { + if (name.size() > 1) { + char type = std::toupper(name[1]); + if (type == 'D') { + m_stats.systemBlockCounts["Dimension-related (*D)"]++; + } + else if (type == 'H' || type == 'X') { + m_stats.systemBlockCounts["Hatch-related (*H, *X)"]++; + } + else { + m_stats.systemBlockCounts["Other System Blocks"]++; + } + } + else { + m_stats.systemBlockCounts["Other System Blocks"]++; + } + + if (!m_importHiddenBlocks) { + return SkipBlockContents(); + } + } + else { + m_stats.entityCounts["BLOCK"]++; + } + + // Step 3: Check for duplicates to prevent errors. + if (this->Blocks.count(name)) { + ImportError("Duplicate block name '%s' found. Ignoring subsequent definition.", + name.c_str()); + return SkipBlockContents(); + } + + // Step 4: Use the temporary Block struct and Collector to parse all contents into memory. + // 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.Inserts); + if (!ReadBlockContents()) { + return false; // Abort on parsing error + } + + // That's it. The block is now parsed into this->Blocks. + // Composition will happen later in ComposeBlocks(). + return true; } void ImpExpDxfRead::OnReadLine(const Base::Vector3d& start, @@ -402,6 +772,10 @@ void ImpExpDxfRead::OnReadSpline(struct SplineData& sd) // Flags: // 1: Closed, 2: Periodic, 4: Rational, 8: Planar, 16: Linear + if (shouldSkipEntity()) { + return; + } + try { Handle(Geom_BSplineCurve) geom; if (sd.control_points > 0) { @@ -469,7 +843,7 @@ void ImpExpDxfRead::OnReadText(const Base::Vector3d& point, PyObject* draftModule = getDraftModule(); if (draftModule != nullptr) { Base::Matrix4D localTransform; - localTransform.rotZ(rotation); + localTransform.rotZ(Base::toRadians(rotation)); localTransform.move(point); PyObject* placement = new Base::PlacementPy(Base::Placement(transform * localTransform)); @@ -505,79 +879,11 @@ void ImpExpDxfRead::OnReadInsert(const Base::Vector3d& point, return; } + // Delegate the action to the currently active collector. + // If the BlockDefinitionCollector is active, it will just store the data. + // If the DrawingEntityCollector is active, it will create the App::Link. Collector->AddInsert(point, scale, name, rotation); } -void ImpExpDxfRead::ExpandInsert(const std::string& name, - const Base::Matrix4D& transform, - const Base::Vector3d& point, - double rotation, - const Base::Vector3d& scale) -{ - if (!Blocks.contains(name)) { - ImportError("Reference to undefined or external block '%s'\n", name); - return; - } - Block& block = Blocks.at(name); - // Apply the scaling, rotation, and move before the OCSEnttityTransform and place the result io - // BaseEntityTransform, - Base::Matrix4D localTransform; - localTransform.scale(scale.x, scale.y, scale.z); - localTransform.rotZ(rotation); - localTransform.move(point[0], point[1], point[2]); - localTransform = transform * localTransform; - CommonEntityAttributes mainAttributes = m_entityAttributes; - for (const auto& [attributes, shapes] : block.Shapes) { - // Put attributes into m_entityAttributes after using the latter to set byblock values in - // the former. - m_entityAttributes = attributes; - m_entityAttributes.ResolveByBlockAttributes(mainAttributes); - - for (const TopoDS_Shape& shape : shapes) { - // TODO???: See the comment in TopoShape::makeTransform regarding calling - // Moved(identityTransform) on the new shape - Collector->AddObject( - BRepBuilderAPI_Transform(shape, - Part::TopoShape::convert(localTransform), - Standard_True) - .Shape(), - "InsertPart"); // TODO: The collection should contain the nameBase to use - } - } - for (const auto& [attributes, featureBuilders] : block.FeatureBuildersList) { - // Put attributes into m_entityAttributes after using the latter to set byblock values in - // the former. - m_entityAttributes = attributes; - m_entityAttributes.ResolveByBlockAttributes(mainAttributes); - - for (const FeaturePythonBuilder& featureBuilder : featureBuilders) { - // TODO: Any non-identity transform from the original entity record needs to be applied - // before OCSEntityTransform (which includes this INSERT's transform followed by the - // transform for the INSERT's context (i.e. from an outeer INSERT) - // TODO: Perhaps pass a prefix ("Insert") to the builder to make the object name so - // Draft objects in a block get named similarly to Shapes. - App::FeaturePython* feature = featureBuilder(localTransform); - if (feature != nullptr) { - // Note that the featureBuilder has already placed this object in the drawing as a - // top-level object, so we don't have to add them but we must place it in its layer - // and set its gui styles - MoveToLayer(feature); - ApplyGuiStyles(feature); - } - } - } - for (const auto& [attributes, inserts] : block.Inserts) { - // Put attributes into m_entityAttributes after using the latter to set byblock values in - // the former. - m_entityAttributes = attributes; - m_entityAttributes.ResolveByBlockAttributes(mainAttributes); - - for (const Block::Insert& insert : inserts) { - // TODO: Apply the OCSOrientationTransform saved with the Insert statement to - // localTransform. (pass localTransform*insert.OCSDirectionTransform) - ExpandInsert(insert.Name, localTransform, insert.Point, insert.Rotation, insert.Scale); - } - } -} void ImpExpDxfRead::OnReadDimension(const Base::Vector3d& start, @@ -800,6 +1106,26 @@ void ImpExpDxfRead::DrawingEntityCollector::AddObject(const TopoDS_Shape& shape, 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); + + // Safely apply styles by checking the object's actual type + if (auto feature = dynamic_cast(obj)) { + Reader.ApplyGuiStyles(feature); + } + else if (auto pyFeature = dynamic_cast(obj)) { + Reader.ApplyGuiStyles(pyFeature); + } + else if (auto link = dynamic_cast(obj)) { + Reader.ApplyGuiStyles(link); + } +} + void ImpExpDxfRead::DrawingEntityCollector::AddObject(FeaturePythonBuilder shapeBuilder) { Reader.IncrementCreatedObjectCount(); @@ -1415,6 +1741,13 @@ Py::Object ImpExpDxfRead::getStatsAsPyObject() } statsDict.setItem("unsupportedFeatures", unsupportedFeaturesDict); + // Create a nested dictionary for the counts of system blocks encountered. + Py::Dict systemBlockCountsDict; + for (const auto& pair : m_stats.systemBlockCounts) { + systemBlockCountsDict.setItem(pair.first.c_str(), Py::Long(pair.second)); + } + statsDict.setItem("systemBlockCounts", systemBlockCountsDict); + // Return the fully populated statistics dictionary to the Python caller. return statsDict; } diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.h b/src/Mod/Import/App/dxf/ImpExpDxf.h index 50b8afa558..21b533d311 100644 --- a/src/Mod/Import/App/dxf/ImpExpDxf.h +++ b/src/Mod/Import/App/dxf/ImpExpDxf.h @@ -23,12 +23,15 @@ #ifndef IMPEXPDXF_H #define IMPEXPDXF_H +#include #include #include +#include #include #include #include +#include #include "dxf.h" @@ -50,6 +53,8 @@ public: Py_XDECREF(DraftModule); } + void StartImport() override; + Py::Object getStatsAsPyObject(); bool ReadEntitiesSection() override; @@ -83,14 +88,6 @@ public: const Base::Vector3d& scale, const std::string& name, double rotation) override; - // Expand a block reference; this should only happen when the collector draws to the document - // rather than saving things The transform should include the OCS Orientation transform for the - // insertion. - void ExpandInsert(const std::string& name, - const Base::Matrix4D& transform, - const Base::Vector3d& point, - double rotation, - const Base::Vector3d& scale); void OnReadDimension(const Base::Vector3d& start, const Base::Vector3d& end, const Base::Vector3d& point, @@ -108,6 +105,7 @@ public: m_optionSource = sourceName; } void setOptions(); + void FinishImport() override; private: bool shouldSkipEntity() const @@ -123,7 +121,12 @@ private: // Combine all the shapes in the given shapes collection into a single shape, and AddObject that // to the drawing. unref's all the shapes in the collection, possibly freeing them. void CombineShapes(std::list& shapes, const char* nameBase) const; + TopoDS_Shape CombineShapesToCompound(const std::list& shapes) const; PyObject* DraftModule = nullptr; + std::set m_referencedBlocks; + void ComposeBlocks(); + void ComposeParametricBlock(const std::string& blockName, std::set& composed); + void ComposeFlattenedBlock(const std::string& blockName, std::set& composed); protected: PyObject* getDraftModule() @@ -202,6 +205,10 @@ protected: private: std::map Blocks; + std::map m_flattenedBlockShapes; + std::map m_blockDefinitions; + App::DocumentObjectGroup* m_blockDefinitionGroup = nullptr; + App::DocumentObjectGroup* m_unreferencedBlocksGroup = nullptr; App::Document* document; std::string m_optionSource; @@ -213,6 +220,8 @@ protected: } virtual void ApplyGuiStyles(Part::Feature* /*object*/) const {} + virtual void ApplyGuiStyles(App::Link* /*object*/) const + {} virtual void ApplyGuiStyles(App::FeaturePython* /*object*/) const {} @@ -240,12 +249,15 @@ protected: // Called by OnReadXxxx functions to add Part objects virtual void AddObject(const TopoDS_Shape& shape, const char* nameBase) = 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. // Because we can't readily copy Draft objects, this method instead takes a builder which, // when called, creates and returns the object. virtual void AddObject(FeaturePythonBuilder shapeBuilder) = 0; // Called by OnReadInsert to either remember in a nested block or expand the block into the // drawing + // This method is now obsolete with the App::Link implementation virtual void AddInsert(const Base::Vector3d& point, const Base::Vector3d& scale, const std::string& name, @@ -266,13 +278,51 @@ protected: {} void AddObject(const TopoDS_Shape& shape, const char* nameBase) override; + void AddObject(App::DocumentObject* obj, const char* nameBase) override; void AddObject(FeaturePythonBuilder shapeBuilder) override; void AddInsert(const Base::Vector3d& point, const Base::Vector3d& scale, const std::string& name, double rotation) override { - Reader.ExpandInsert(name, Reader.OCSOrientationTransform, point, rotation, scale); + // This is the correct place to create top-level App::Link objects for INSERTs. + + // Find the base object from our map of stored block definitions. + auto it = Reader.m_blockDefinitions.find(name); + if (it == Reader.m_blockDefinitions.end()) { + return; + } + Reader.m_referencedBlocks.insert(name); + App::DocumentObject* baseObject = it->second; + + // Create a unique name for the link + std::string linkName = "Link_"; + std::string cleanName = name; + if (!cleanName.empty() && std::isdigit(cleanName.back())) { + // Add a trailing underscore to prevent the unique name generator + // from incrementing the number in the block's name. + cleanName += "_"; + } + linkName += cleanName; + linkName = Reader.document->getUniqueObjectName(linkName.c_str()); + + // Create the App::Link object directly in C++ + App::Link* link = Reader.document->addObject(linkName.c_str()); + Reader.IncrementCreatedObjectCount(); + if (!link) { + Reader.ImportError("Failed to create App::Link for block '%s'", name.c_str()); + return; + } + + // Configure the link + link->setLink(-1, baseObject); + link->LinkTransform.setValue(false); + link->Label.setValue(name.c_str()); + Base::Placement pl(point, Base::Rotation(Base::Vector3d(0, 0, 1), rotation)); + link->Placement.setValue(pl); + link->ScaleVector.setValue(scale); + + this->AddObject(link, "Link"); } }; class ShapeSavingEntityCollector: public DrawingEntityCollector @@ -291,6 +341,12 @@ protected: ShapesList[Reader.m_entityAttributes].push_back(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. + DrawingEntityCollector::AddObject(obj, nameBase); + } + private: std::map>& ShapesList; }; @@ -342,6 +398,16 @@ protected: { FeatureBuildersList[Reader.m_entityAttributes].push_back(shapeBuilder); } + + 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."); + } + void AddInsert(const Base::Vector3d& point, const Base::Vector3d& scale, const std::string& name, diff --git a/src/Mod/Import/App/dxf/dxf.cpp b/src/Mod/Import/App/dxf/dxf.cpp index 3e780d3e5b..28be78f0b4 100644 --- a/src/Mod/Import/App/dxf/dxf.cpp +++ b/src/Mod/Import/App/dxf/dxf.cpp @@ -2352,7 +2352,6 @@ 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); @@ -3162,7 +3161,10 @@ Base::Color CDxfRead::ObjectColor(ColorIndex_t index) result = wheel((index - 1) * 4, 0x00); } else if (index == 7) { - result = Base::Color(1, 1, 1); + // DXF color 7 is "black/white" and should adapt to the background. + // Since we cannot easily query the background theme from here, we will use a + // neutral mid-gray, which is visible on both light and dark themes. + result = Base::Color(0.5f, 0.5f, 0.5f); } else if (index == 8) { result = Base::Color(0.5, 0.5, 0.5); diff --git a/src/Mod/Import/App/dxf/dxf.h b/src/Mod/Import/App/dxf/dxf.h index 92b3ce472c..dec4163db0 100644 --- a/src/Mod/Import/App/dxf/dxf.h +++ b/src/Mod/Import/App/dxf/dxf.h @@ -188,6 +188,8 @@ struct DxfImportStats std::map entityCounts; std::map importSettings; std::map>> unsupportedFeatures; + std::map systemBlockCounts; + int totalEntitiesCreated = 0; }; diff --git a/src/Mod/Import/Gui/dxf/ImpExpDxfGui.cpp b/src/Mod/Import/Gui/dxf/ImpExpDxfGui.cpp index 704e1bbb3b..f6c2597425 100644 --- a/src/Mod/Import/Gui/dxf/ImpExpDxfGui.cpp +++ b/src/Mod/Import/Gui/dxf/ImpExpDxfGui.cpp @@ -52,9 +52,12 @@ #endif #include +#include + #include #include #include +#include #include #include "ImpExpDxfGui.h" @@ -77,6 +80,43 @@ void ImpExpDxfReadGui::ApplyGuiStyles(Part::Feature* object) const view->Transparency.setValue(0); } +void ImpExpDxfReadGui::ApplyGuiStyles(App::Link* object) const +{ + auto view = GuiDocument->getViewProvider(object); + + // The ViewProvider for an App::Link is a ViewProviderLink + auto* vpLink = dynamic_cast(view); + if (!vpLink) { + return; + } + + if (m_preserveColors) { + // The user wants to see colors from the DXF file. + // We style the link by setting its ViewProvider's properties directly, + // which is the same mechanism used for standard Part::Features. + Base::Color color = ObjectColor(m_entityAttributes.m_Color); + + // The ViewProviderLink does not have LineColor/PointColor properties itself, + // but setting them on the base ViewProvider seems to be respected by the renderer. + // If this does not work, the properties would need to be added to ViewProviderLink. + if (auto* prop = view->getPropertyByName("LineColor")) { + static_cast(prop)->setValue(color); + } + if (auto* prop = view->getPropertyByName("PointColor")) { + static_cast(prop)->setValue(color); + } + if (auto* prop = view->getPropertyByName("ShapeColor")) { + static_cast(prop)->setValue(color); + } + if (auto* prop = view->getPropertyByName("DrawStyle")) { + static_cast(prop)->setValue(GetDrawStyle()); + } + if (auto* prop = view->getPropertyByName("Transparency")) { + static_cast(prop)->setValue(0); + } + } +} + void ImpExpDxfReadGui::ApplyGuiStyles(App::FeaturePython* object) const { static Base::Type PropertyColorType = App::PropertyColor::getClassTypeId(); diff --git a/src/Mod/Import/Gui/dxf/ImpExpDxfGui.h b/src/Mod/Import/Gui/dxf/ImpExpDxfGui.h index 43531704b7..c2e16148c3 100644 --- a/src/Mod/Import/Gui/dxf/ImpExpDxfGui.h +++ b/src/Mod/Import/Gui/dxf/ImpExpDxfGui.h @@ -41,6 +41,7 @@ public: protected: void ApplyGuiStyles(Part::Feature* object) const override; + void ApplyGuiStyles(App::Link* object) const override; void ApplyGuiStyles(App::FeaturePython* object) const override; private: