From 0dcfe945053e0fc5c0502f4fc13d2fcd18ebaea2 Mon Sep 17 00:00:00 2001 From: Kevin Martin Date: Wed, 10 Jan 2024 09:07:03 -0500 Subject: [PATCH] Handle all combos of "group into blocks" "use DXF colors" "use layers" Fixes #11873 this was the primary goal of these changes Fixes (partially) #11874 the parts of a polyline are combined as a compound object (shape) but it would be preferable for them to be made into a wire (if possible) Fixes #11872 Objects in a block definition are now kept separately until the block is inserted, in which case the inserted objects are subject to all the other options regarding combining. Fixes (partially, review required) #11871 Text and dimensions are now kept as part of the block definition and are placed in the drawing when the block is inserted but this code has not been extensively tested. Affects #11875, custom types are not made, but the labels now reflect the object types rather than all being "Shapennn" This leaves the importer options handling in a bit of a mess that needs cleanup eventually, but this can be a new issue. This includes some importer flags that have no corresponding options (e.g. import frozen layers), some flags not yet implemented, some flags not yet even declared in the code because their implementation is further off (import hatch outlines), and some suggested future options (import SOLIDs as faces) Centralize the calculation of the OCS for entities as they're read from the DXF. Most of the entities don't use this yet, but some of them roll their own crude Normal Vector handling. Because the new code takes priority over the old for reading the normal vector, such code will always see (0, 0, 1) as the extrusion direction. --- src/Mod/Import/App/dxf/ImpExpDxf.cpp | 488 +++++++++++++++++------- src/Mod/Import/App/dxf/ImpExpDxf.h | 258 ++++++++++++- src/Mod/Import/App/dxf/dxf.cpp | 231 +++++++---- src/Mod/Import/App/dxf/dxf.h | 258 +++++++++++-- src/Mod/Import/Gui/dxf/ImpExpDxfGui.cpp | 71 +++- src/Mod/Import/Gui/dxf/ImpExpDxfGui.h | 7 +- 6 files changed, 1041 insertions(+), 272 deletions(-) diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.cpp b/src/Mod/Import/App/dxf/ImpExpDxf.cpp index 9ade2e2d2e..dbe6882acc 100644 --- a/src/Mod/Import/App/dxf/ImpExpDxf.cpp +++ b/src/Mod/Import/App/dxf/ImpExpDxf.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -81,20 +82,128 @@ using BRepAdaptor_HCurve = BRepAdaptor_Curve; ImpExpDxfRead::ImpExpDxfRead(const std::string& filepath, App::Document* pcDoc) : CDxfRead(filepath) , document(pcDoc) - , optionGroupLayers(false) - , optionImportAnnotations(true) { setOptionSource("User parameter:BaseApp/Preferences/Mod/Draft"); setOptions(); } +bool ImpExpDxfRead::ReadEntitiesSection() +{ + DrawingEntityCollector collector(*this); + if (m_mergeOption < SingleShapes) { + std::map> ShapesToCombine; + { + ShapeSavingEntityCollector savingCollector(*this, ShapesToCombine); + if (!CDxfRead::ReadEntitiesSection()) { + return false; + } + } + + // Merge the contents of ShapesToCombine and AddObject the result(s) + // TODO: We do end-to-end joining or complete merging as selected by the options. + for (auto& shapeSet : ShapesToCombine) { + m_entityAttributes = shapeSet.first; + CombineShapes(shapeSet.second, "Compound"); + } + } + else { + if (!CDxfRead::ReadEntitiesSection()) { + return false; + } + } + if (m_preserveLayers) { + // Hide the Hidden layers if possible (if GUI exists) + // We do this now rather than when the layer is created so all objects + // within the layers also become hidden. + for (auto& layerEntry : Layers) { + auto layer = (Layer*)layerEntry.second; + if (layer->DraftLayerView != nullptr && layer->Hidden) { + PyObject_CallMethod(layer->DraftLayerView, "hide", nullptr); + } + } + } + return true; +} + +void ImpExpDxfRead::CombineShapes(std::list& shapes, const char* nameBase) const +{ + BRep_Builder builder; + TopoDS_Compound comp; + builder.MakeCompound(comp); + for (const auto& sh : shapes) { + if (!sh.IsNull()) { + builder.Add(comp, sh); + } + } + if (!comp.IsNull()) { + Collector->AddObject(comp, nameBase); + } +} + void ImpExpDxfRead::setOptions() { ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath(getOptionSource().c_str()); - optionGroupLayers = hGrp->GetBool("groupLayers", false); - optionImportAnnotations = hGrp->GetBool("dxftext", false); + m_preserveLayers = hGrp->GetBool("dxfUseDraftVisGroups", true); + m_preserveColors = hGrp->GetBool("dxfGetOriginalColors", true); + // 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; + if (hGrp->GetBool("groupLayers", true)) { + // Group all compatible objects together + m_mergeOption = MergeShapes; + } + else if (hGrp->GetBool("dxfCreatePart", true)) { + // Create (non-draft) Shape objects when possible + m_mergeOption = SingleShapes; + } + else if (hGrp->GetBool("dxfCreateDraft", true)) { + // Create only Draft objects, making the result closest to drawn-from-scratch + m_mergeOption = DraftObjects; + } + // 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. SetAdditionalScaling(hGrp->GetFloat("dxfScaling", 1.0)); + + m_importAnnotations = hGrp->GetBool("dxftext", false); + m_importPoints = hGrp->GetBool("dxfImportPoints", true); + m_importPaperSpaceEntities = hGrp->GetBool("dxflayout", false); + m_importHiddenBlocks = hGrp->GetBool("dxfstarblocks", false); + // TODO: There is currently no option for this: m_importFrozenLayers = + // hGrp->GetBool("dxffrozenLayers", false); + // TODO: There is currently no option for this: m_importHiddenLayers = + // hGrp->GetBool("dxfhiddenLayers", true); +} + +bool ImpExpDxfRead::OnReadBlock(const std::string& name, int flags) +{ + 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"); + } + 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. + } + else if (Blocks.count(name) > 0) { + ImportError("Duplicate block name '%s'\n", name); + } + 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(); + } + return SkipBlockContents(); } void ImpExpDxfRead::OnReadLine(const Base::Vector3d& start, @@ -107,17 +216,13 @@ void ImpExpDxfRead::OnReadLine(const Base::Vector3d& start, // TODO: Really?? What about the people designing integrated circuits? return; } - BRepBuilderAPI_MakeEdge makeEdge(p0, p1); - TopoDS_Edge edge = makeEdge.Edge(); - AddObject(new Part::TopoShape(edge)); + Collector->AddObject(BRepBuilderAPI_MakeEdge(p0, p1).Edge(), "Line"); } void ImpExpDxfRead::OnReadPoint(const Base::Vector3d& start) { - BRepBuilderAPI_MakeVertex makeVertex(makePoint(start)); - TopoDS_Vertex vertex = makeVertex.Vertex(); - AddObject(new Part::TopoShape(vertex)); + Collector->AddObject(BRepBuilderAPI_MakeVertex(makePoint(start)).Vertex(), "Point"); } @@ -136,9 +241,7 @@ void ImpExpDxfRead::OnReadArc(const Base::Vector3d& start, gp_Pnt pc = makePoint(center); gp_Circ circle(gp_Ax2(pc, up), p0.Distance(pc)); if (circle.Radius() > 0) { - BRepBuilderAPI_MakeEdge makeEdge(circle, p0, p1); - TopoDS_Edge edge = makeEdge.Edge(); - AddObject(new Part::TopoShape(edge)); + Collector->AddObject(BRepBuilderAPI_MakeEdge(circle, p0, p1).Edge(), "Arc"); } else { Base::Console().Warning("ImpExpDxf - ignore degenerate arc of circle\n"); @@ -159,9 +262,7 @@ void ImpExpDxfRead::OnReadCircle(const Base::Vector3d& start, gp_Pnt pc = makePoint(center); gp_Circ circle(gp_Ax2(pc, up), p0.Distance(pc)); if (circle.Radius() > 0) { - BRepBuilderAPI_MakeEdge makeEdge(circle); - TopoDS_Edge edge = makeEdge.Edge(); - AddObject(new Part::TopoShape(edge)); + Collector->AddObject(BRepBuilderAPI_MakeEdge(circle).Edge(), "Circle"); } else { Base::Console().Warning("ImpExpDxf - ignore degenerate circle\n"); @@ -278,9 +379,7 @@ void ImpExpDxfRead::OnReadSpline(struct SplineData& sd) throw Standard_Failure(); } - BRepBuilderAPI_MakeEdge makeEdge(geom); - TopoDS_Edge edge = makeEdge.Edge(); - AddObject(new Part::TopoShape(edge)); + Collector->AddObject(BRepBuilderAPI_MakeEdge(geom).Edge(), "Spline"); } catch (const Standard_Failure&) { Base::Console().Warning("ImpExpDxf - failed to create bspline\n"); @@ -305,9 +404,7 @@ void ImpExpDxfRead::OnReadEllipse(const Base::Vector3d& center, gp_Elips ellipse(gp_Ax2(pc, up), major_radius, minor_radius); ellipse.Rotate(gp_Ax1(pc, up), rotation); if (ellipse.MinorRadius() > 0) { - BRepBuilderAPI_MakeEdge makeEdge(ellipse); - TopoDS_Edge edge = makeEdge.Edge(); - AddObject(new Part::TopoShape(edge)); + Collector->AddObject(BRepBuilderAPI_MakeEdge(ellipse).Edge(), "Ellipse"); } else { Base::Console().Warning("ImpExpDxf - ignore degenerate ellipse\n"); @@ -322,13 +419,15 @@ void ImpExpDxfRead::OnReadText(const Base::Vector3d& point, { // 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 (optionImportAnnotations) { - if (LayerName().rfind("BLOCKS", 0) != 0) { - PyObject* draftModule = nullptr; - Base::Rotation rot(Base::Vector3d(0, 0, 1), rotation); - PyObject* placement = new Base::PlacementPy(Base::Placement(point, rot)); - draftModule = PyImport_ImportModule("Draft"); + if (m_importAnnotations) { + auto makeText = [=](const Base::Matrix4D& transform) -> App::FeaturePython* { + PyObject* draftModule = getDraftModule(); if (draftModule != nullptr) { + Base::Matrix4D localTransform; + localTransform.rotZ(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) @@ -340,18 +439,14 @@ void ImpExpDxfRead::OnReadText(const Base::Vector3d& point, placement, 0, height)); + Py_DECREF(placement); if (builtText != nullptr) { - ApplyGuiStyles( - dynamic_cast(builtText->getDocumentObjectPtr())); + return dynamic_cast(builtText->getDocumentObjectPtr()); } } - // We own all the return values so we must release them. - Py_DECREF(placement); - Py_XDECREF(draftModule); - } - else { - UnsupportedFeature("TEXT or MTEXT in BLOCK definition"); - } + return nullptr; + }; + Collector->AddObject((FeaturePythonBuilder)makeText); } } @@ -361,37 +456,78 @@ void ImpExpDxfRead::OnReadInsert(const Base::Vector3d& point, const std::string& name, double rotation) { - // std::cout << "Inserting block " << name << " rotation " << rotation << " pos " << point[0] << - // "," << point[1] << "," << point[2] << " scale " << scale[0] << "," << scale[1] << "," << - // scale[2] << std::endl; - std::string prefix = "BLOCKS "; - prefix += name; - prefix += " "; - auto checkScale = [=](double scale) { - return scale != 0.0 ? scale : 1.0; - }; - for (const auto& layer : layers) { - if (layer.first.substr(0, prefix.size()) == prefix) { - BRep_Builder builder; - TopoDS_Compound comp; - builder.MakeCompound(comp); - for (const auto& shape : layer.second) { - const TopoDS_Shape& sh = shape->getShape(); - if (!sh.IsNull()) { - builder.Add(comp, sh); - } - } - if (!comp.IsNull()) { - auto pcomp = new Part::TopoShape(comp); - Base::Matrix4D mat; - mat.scale(checkScale(scale[0]), checkScale(scale[1]), checkScale(scale[2])); - mat.rotZ(rotation); - mat.move(point[0], point[1], point[2]); - pcomp->transformShape(mat, true); - AddObject(pcomp); + 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.count(name) == 0) { + 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); + } + } } @@ -400,53 +536,160 @@ void ImpExpDxfRead::OnReadDimension(const Base::Vector3d& start, const Base::Vector3d& point, double /*rotation*/) { - if (optionImportAnnotations) { - PyObject* draftModule = nullptr; - PyObject* startPy = new Base::VectorPy(start); - PyObject* endPy = new Base::VectorPy(end); - PyObject* lineLocationPy = new Base::VectorPy(point); - draftModule = PyImport_ImportModule("Draft"); - if (draftModule != nullptr) { - // 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)); - if (builtDim != nullptr) { - ApplyGuiStyles(dynamic_cast(builtDim->getDocumentObjectPtr())); + if (m_importAnnotations) { + auto makeDimension = [=](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()); + } } - } - // We own all the return values so we must release them. - Py_DECREF(startPy); - Py_DECREF(endPy); - Py_DECREF(lineLocationPy); - Py_XDECREF(draftModule); + return nullptr; + }; + Collector->AddObject((FeaturePythonBuilder)makeDimension); + } +} +void ImpExpDxfRead::OnReadPolyline(std::list& vertices, int flags) +{ + 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); + } + // 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"); } } -void ImpExpDxfRead::AddObject(Part::TopoShape* shape) +ImpExpDxfRead::Layer::Layer(const std::string& name, + ColorIndex_t color, + std::string&& lineType, + PyObject* drawingLayer) + : CDxfRead::Layer(name, color, std::move(lineType)) + , DraftLayer(drawingLayer) + , DraftLayerView(drawingLayer == nullptr ? nullptr + : PyObject_GetAttrString(drawingLayer, "ViewObject")) +{} +ImpExpDxfRead::Layer::~Layer() { - std::vector vec; - std::string destinationLayerName(LayerName()); - if (layers.count(destinationLayerName) != 0) { - vec = layers[destinationLayerName]; + Py_XDECREF(DraftLayer); + Py_XDECREF(DraftLayerView); +} + +CDxfRead::Layer* +ImpExpDxfRead::MakeLayer(const std::string& name, ColorIndex_t color, std::string&& lineType) +{ + if (m_preserveLayers) { + // Hidden layers are implemented in the wrapup code after the entire file has been read. + App::Color appColor = ObjectColor(color); + PyObject* draftModule = nullptr; + PyObject* layer = nullptr; + draftModule = PyImport_ImportModule("Draft"); + 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. + // TODO: Pass the appropriate draw_style (from "Solid" "Dashed" "Dotted" "DashDot") + // This needs an ObjectDrawStyleName analogous to ObjectColor but at the ImpExpDxfGui + // level. + layer = + // NOLINTNEXTLINE(readability/nolint) + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast) + (Base::PyObjectBase*)PyObject_CallMethod(draftModule, + "make_layer", + "s(fff)(fff)fs", + name.c_str(), + appColor.r, + appColor.g, + appColor.b, + appColor.r, + appColor.g, + appColor.b, + 2.0, + "Solid"); + Py_DECREF(draftModule); + } + auto result = new Layer(name, color, std::move(lineType), layer); + if (result->DraftLayerView != nullptr) { + PyObject_SetAttrString(result->DraftLayerView, "OverrideLineColorChildren", Py_False); + PyObject_SetAttrString(result->DraftLayerView, "OverrideShapeColorChildren", 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. + return result; } - vec.push_back(shape); - layers[destinationLayerName] = vec; - if (!optionGroupLayers) { - if (destinationLayerName.rfind("BLOCKS", 0) != 0) { - auto pcFeature = - dynamic_cast(document->addObject("Part::Feature", "Shape")); - pcFeature->Shape.setValue(shape->getShape()); - ApplyGuiStyles(pcFeature); + return CDxfRead::MakeLayer(name, color, std::move(lineType)); +} +void ImpExpDxfRead::MoveToLayer(App::DocumentObject* object) const +{ + if (m_preserveLayers) { + static PyObject* addObjectName = + PyUnicode_FromString("addObject"); // This never gets freed, we always have a reference + auto layer = static_cast(m_entityAttributes.m_Layer); + if (layer->DraftLayer != nullptr) { + // We have to move the object to layer->DraftLayer + // The DraftLayer will have a Proxy attribute which has a addObject attribute which we + // call with (draftLayer, draftObject) Checking from python, the layer is a + // App::FeaturePython, and its Proxy is a draftobjects.layer.Layer + PyObject* proxy = PyObject_GetAttrString(layer->DraftLayer, "Proxy"); + if (proxy != nullptr) { + // TODO: De we have to check if the method exists? The legacy importer does. + PyObject_CallMethodObjArgs(proxy, + addObjectName, + layer->DraftLayer, + object->getPyObject(), + nullptr); + Py_DECREF(proxy); + return; + } } } + // 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 } @@ -487,32 +730,21 @@ std::string ImpExpDxfRead::Deformat(const char* text) return ss.str(); } - -void ImpExpDxfRead::AddGraphics() const +void ImpExpDxfRead::DrawingEntityCollector::AddObject(const TopoDS_Shape& shape, + const char* nameBase) { - if (optionGroupLayers) { - for (const auto& layer : layers) { - BRep_Builder builder; - TopoDS_Compound comp; - builder.MakeCompound(comp); - std::string k = layer.first; - if (k == "0") { // FreeCAD doesn't like an object name being '0'... - k = "LAYER_0"; - } - if (k.rfind("BLOCKS", 0) != 0) { - for (const auto& shape : layer.second) { - const TopoDS_Shape& sh = shape->getShape(); - if (!sh.IsNull()) { - builder.Add(comp, sh); - } - } - if (!comp.IsNull()) { - auto pcFeature = dynamic_cast( - document->addObject("Part::Feature", k.c_str())); - pcFeature->Shape.setValue(comp); - } - } - } + auto pcFeature = + dynamic_cast(Reader.document->addObject("Part::Feature", nameBase)); + pcFeature->Shape.setValue(shape); + Reader.MoveToLayer(pcFeature); + Reader.ApplyGuiStyles(pcFeature); +} +void ImpExpDxfRead::DrawingEntityCollector::AddObject(FeaturePythonBuilder shapeBuilder) +{ + App::FeaturePython* shape = shapeBuilder(Reader.OCSOrientationTransform); + if (shape != nullptr) { + Reader.MoveToLayer(shape); + Reader.ApplyGuiStyles(shape); } } diff --git a/src/Mod/Import/App/dxf/ImpExpDxf.h b/src/Mod/Import/App/dxf/ImpExpDxf.h index 349c0d77c7..d219e0f99f 100644 --- a/src/Mod/Import/App/dxf/ImpExpDxf.h +++ b/src/Mod/Import/App/dxf/ImpExpDxf.h @@ -26,6 +26,7 @@ #include #include +#include #include #include @@ -40,8 +41,19 @@ class ImportExport ImpExpDxfRead: public CDxfRead { public: ImpExpDxfRead(const std::string& filepath, App::Document* pcDoc); + ImpExpDxfRead(const ImpExpDxfRead&) = delete; + ImpExpDxfRead(ImpExpDxfRead&&) = delete; + void operator=(const ImpExpDxfRead&) = delete; + void operator=(ImpExpDxfRead&&) = delete; + ~ImpExpDxfRead() override + { + Py_XDECREF(DraftModule); + } + + bool ReadEntitiesSection() override; // CDxfRead's virtual functions + bool OnReadBlock(const std::string& name, int flags) override; void OnReadLine(const Base::Vector3d& start, const Base::Vector3d& end, bool hidden) override; void OnReadPoint(const Base::Vector3d& start) override; void OnReadText(const Base::Vector3d& point, @@ -69,14 +81,20 @@ 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, double rotation) override; - void AddGraphics() const override; + void OnReadPolyline(std::list& /*vertices*/, int flags) override; - // FreeCAD-specific functions - void AddObject(Part::TopoShape* shape); // Called by OnRead functions to add Part objects std::string Deformat(const char* text); // Removes DXF formatting from texts std::string getOptionSource() @@ -94,17 +112,235 @@ private: { return {point3d.x, point3d.y, point3d.z}; } + void MoveToLayer(App::DocumentObject* object) const; + // 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; + PyObject* DraftModule = nullptr; protected: - virtual void ApplyGuiStyles(Part::Feature* /*object*/) - {} - virtual void ApplyGuiStyles(App::FeaturePython* /*object*/) - {} + PyObject* getDraftModule() + { + if (DraftModule == nullptr) { + DraftModule = PyImport_ImportModule("Draft"); + } + return DraftModule; + } + CDxfRead::Layer* + MakeLayer(const std::string& name, ColorIndex_t color, std::string&& lineType) override; + + // 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 + { + public: + Layer(const std::string& name, + ColorIndex_t color, + std::string&& lineType, + PyObject* drawingLayer); + Layer(const Layer&) = delete; + Layer(Layer&&) = delete; + void operator=(const Layer&) = delete; + void operator=(Layer&&) = delete; + ~Layer() override; + PyObject* const DraftLayer; + PyObject* const DraftLayerView; + }; + + using FeaturePythonBuilder = + std::function; + // Block management + class Block + { + public: + struct Insert + { + const Base::Vector3d Point; + const Base::Vector3d Scale; + const std::string Name; + const double Rotation; + + // NOLINTNEXTLINE(readability/nolint) + // NOLINTNEXTLINE(modernize-pass-by-value) Pass by value adds unwarranted complexity + Insert(const std::string& Name, + const Base::Vector3d& Point, + double Rotation, + const Base::Vector3d& Scale) + : Point(Point) + , Scale(Scale) + , Name(Name) + , Rotation(Rotation) + {} + }; + // NOLINTNEXTLINE(readability/nolint) + // NOLINTNEXTLINE(modernize-pass-by-value) Pass by value adds unwarranted complexity + Block(const std::string& name, int flags) + : Name(name) + , Flags(flags) + {} + const std::string Name; + const int Flags; + std::map> Shapes; + std::map> + FeatureBuildersList; + std::map> Inserts; + }; + +private: + std::map Blocks; App::Document* document; - bool optionGroupLayers; - bool optionImportAnnotations; - std::map> layers; std::string m_optionSource; + +protected: + virtual void ApplyGuiStyles(Part::Feature* /*object*/) const + {} + virtual void ApplyGuiStyles(App::FeaturePython* /*object*/) const + {} + + // Gathering of created entities + class EntityCollector + { + public: + explicit EntityCollector(ImpExpDxfRead& reader) + : Reader(reader) + , previousCollector(reader.Collector) + { + Reader.Collector = this; + } + EntityCollector(const EntityCollector&) = delete; + EntityCollector(EntityCollector&&) = delete; + void operator=(const EntityCollector&) = delete; + void operator=(EntityCollector&&) = delete; + + virtual ~EntityCollector() + { + if (Reader.Collector == this) { + Reader.Collector = previousCollector; + } + } + + // Called by OnReadXxxx functions to add Part objects + virtual void AddObject(const TopoDS_Shape& shape, 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 + virtual void AddInsert(const Base::Vector3d& point, + const Base::Vector3d& scale, + const std::string& name, + double rotation) = 0; + + protected: + ImpExpDxfRead& Reader; + + private: + EntityCollector* const previousCollector; + }; + class DrawingEntityCollector: public EntityCollector + { + // This collector places all objects into the drawing + public: + explicit DrawingEntityCollector(ImpExpDxfRead& reader) + : EntityCollector(reader) + {} + + void AddObject(const TopoDS_Shape& shape, 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); + } + }; + class ShapeSavingEntityCollector: public DrawingEntityCollector + { + // This places draft objects into the drawing but stashes away Shapes. + public: + ShapeSavingEntityCollector( + ImpExpDxfRead& reader, + std::map>& shapesList) + : DrawingEntityCollector(reader) + , ShapesList(shapesList) + {} + + void AddObject(const TopoDS_Shape& shape, const char* /*nameBase*/) override + { + ShapesList[Reader.m_entityAttributes].push_back(shape); + } + + private: + std::map>& ShapesList; + }; +#ifdef LATER + class PolylineEntityCollector: public CombiningDrawingEntityCollector + { + // This places draft objects into the drawing but stashes away Shapes. + public: + explicit PolylineEntityCollector(CDxfRead& reader) + : CombiningDrawingEntityCollector(reader) + , previousMmergeOption(reader.m_mergeOption) + { + // TODO: We also have to temporarily shift from DraftObjects to some other mode so the + // pieces of the polyline some through as shapes and not Draft objects, or maybe the + // polyline builder should not call OnArcRead and OnLineRead to do their dirty work. + } + ~PolylineEntityCollector(); + + void AddObject(Part::TopoShape* shape) override; + void AddObject(App::FeaturePython* shape) override; + + private: + const EntityCollector* previousEntityCollector; + const eEntityMergeType_t previousMmergeOption; + }; +#endif + class BlockDefinitionCollector: public EntityCollector + { + // Collect all the entities plus their entityAttrubutes into given collections. + public: + BlockDefinitionCollector( + ImpExpDxfRead& reader, + std::map>& shapesList, + std::map>& + featureBuildersList, + std::map>& insertsList) + : EntityCollector(reader) + , ShapesList(shapesList) + , FeatureBuildersList(featureBuildersList) + , 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); + } + void AddInsert(const Base::Vector3d& point, + const Base::Vector3d& scale, + const std::string& name, + double rotation) override + { + InsertsList[Reader.m_entityAttributes].push_back( + Block::Insert(name, point, rotation, scale)); + } + + private: + std::map>& ShapesList; + std::map>& + FeatureBuildersList; + std::map>& InsertsList; + }; + +private: + EntityCollector* Collector = nullptr; }; class ImportExport ImpExpDxfWrite: public CDxfWrite @@ -122,7 +358,7 @@ public: { return m_optionSource; } - void setOptionSource(std::string s) + void setOptionSource(const std::string& s) { m_optionSource = s; } diff --git a/src/Mod/Import/App/dxf/dxf.cpp b/src/Mod/Import/App/dxf/dxf.cpp index 5d9920620a..7597adef98 100644 --- a/src/Mod/Import/App/dxf/dxf.cpp +++ b/src/Mod/Import/App/dxf/dxf.cpp @@ -1,4 +1,4 @@ -// dxf.cpp +// dxf.cpp // Copyright (c) 2009, Dan Heeks // This program is released under the BSD license. See the file COPYING for details. // modified 2018 wandererfan @@ -1813,8 +1813,16 @@ CDxfRead::CDxfRead(const std::string& filepath) CDxfRead::~CDxfRead() { delete m_ifs; + // Delete the Layer objects which are referenced by pointer from the Layers table. + for (auto& pair : Layers) { + delete pair.second; + } } +// Static member initializers +const std::string CDxfRead::LineTypeByLayer("BYLAYER"); // NOLINT(runtime/string) +const std::string CDxfRead::LineTypeByBlock("BYBLOCK"); // NOLINT(runtime/string) +const std::string CDxfRead::DefaultLineType("CONTINUOUS"); // NOLINT(runtime/string) // // Setup for ProcessCommonEntityAttribute @@ -1842,6 +1850,12 @@ void CDxfRead::SetupScaledDoubleIntoList(eDXFGroupCode_t x_record_type, list