// SPDX-License-Identifier: LGPL-2.1-or-later /**************************************************************************** * * * Copyright (c) 2024 wandererfan * * * * This file is part of FreeCAD. * * * * FreeCAD is free software: you can redistribute it and/or modify it * * under the terms of the GNU Lesser General Public License as * * published by the Free Software Foundation, either version 2.1 of the * * License, or (at your option) any later version. * * * * FreeCAD is distributed in the hope that it will be useful, but * * WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * Lesser General Public License for more details. * * * * You should have received a copy of the GNU Lesser General Public * * License along with FreeCAD. If not, see * * . * * * ***************************************************************************/ //! ShapeFinder is a class to obtain the located shape pointed at by a DocumentObject and a //! "new-style" long subelement name. It hides the complexities of obtaining the correct object //! and its placement. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ShapeFinder.h" using namespace Measure; //! ResolveResult is a class to hold the result of resolving a selection into the actual target //! object and traditional subElement name (Vertex1). ResolveResult::ResolveResult( const App::DocumentObject* realTarget, const std::string& shortSubName, const App::DocumentObject* targetParent ) : m_target(App::SubObjectT(realTarget, shortSubName.c_str())) , m_targetParent(App::DocumentObjectT(targetParent)) {} App::DocumentObject& ResolveResult::getTarget() const { return *(m_target.getObject()); } std::string ResolveResult::getShortSub() const { return m_target.getSubName(); } App::DocumentObject& ResolveResult::getTargetParent() const { return *(m_targetParent.getObject()); } //! returns the actual target object and subname pointed to by selectObj and selectLongSub (which //! is likely a result from getSelection or getSelectionEx) ResolveResult ShapeFinder::resolveSelection( const App::DocumentObject& selectObj, const std::string& selectLongSub ) { App::DocumentObject* targetParent {nullptr}; std::string childName {}; const char* subElement {nullptr}; App::DocumentObject* realTarget = selectObj.resolve(selectLongSub.c_str(), &targetParent, &childName, &subElement); auto shortSub = getLastTerm(selectLongSub); return {realTarget, shortSub, targetParent}; } //! returns the shape of rootObject+leafSub. Any transforms from objects in the path from rootObject //! to leafSub are applied to the shape. //! leafSub is typically obtained from Selection as it provides the appropriate longSubname. The //! leaf sub string can also be constructed by walking the tree. // TODO: to truly locate the shape, we need to consider attachments - see // ShapeExtractor::getShapesFromXRoot() // and ShapeFinder::getLinkAttachParent() TopoDS_Shape ShapeFinder::getLocatedShape(const App::DocumentObject& rootObject, const std::string& leafSub) { auto resolved = resolveSelection(rootObject, leafSub); auto target = &resolved.getTarget(); auto shortSub = resolved.getShortSub(); if (!target) { return {}; } TopoDS_Shape shape = Part::Feature::getShape( target, Part::ShapeOption::ResolveLink | Part::ShapeOption::Transform ); if (isShapeReallyNull(shape)) { return {}; } auto cleanSub = removeTnpInfo(leafSub); auto transform = getGlobalTransform(rootObject, cleanSub); shape = transformShape(shape, transform.first, transform.second); Part::TopoShape tShape {shape}; if (!shortSub.empty()) { return tShape.getSubTopoShape(shortSub.c_str()).getShape(); } return tShape.getShape(); } //! convenient version of previous method Part::TopoShape ShapeFinder::getLocatedTopoShape( const App::DocumentObject& rootObject, const std::string& leafSub ) { return {getLocatedShape(rootObject, leafSub)}; } //! traverse the tree from leafSub up to rootObject, obtaining placements along the way. Note that //! the placements will need to be applied in the reverse order (ie top down) of what is delivered //! in plm stack. leafSub is a dot separated longSubName which DOES NOT include rootObject. the //! result does not include rootObject's transform. void ShapeFinder::crawlPlacementChain( std::vector& plmStack, std::vector& scaleStack, const App::DocumentObject& rootObject, const std::string& leafSub ) { auto currentSub = leafSub; std::string previousSub {}; while (!currentSub.empty() && currentSub != previousSub) { auto resolved = resolveSelection(rootObject, currentSub); auto target = &resolved.getTarget(); if (!target) { return; } auto currentPlacement = getPlacement(target); auto currentScale = getScale(target); if (!currentPlacement.isIdentity() || !currentScale.isUnity()) { plmStack.push_back(currentPlacement); scaleStack.push_back(currentScale); } previousSub = currentSub; currentSub = pruneLastTerm(currentSub); } } //! return inShape with placement and scaler applied. If inShape contains any infinite subshapes //! (such as Datum planes), the infinite shapes will not be included in the result. TopoDS_Shape ShapeFinder::transformShape( TopoDS_Shape& inShape, const Base::Placement& placement, const Base::Matrix4D& scaler ) { if (isShapeReallyNull(inShape)) { return {}; } // we modify the parameter shape here. we don't claim to be const, but may be better to copy // the shape? Part::TopoShape tshape {inShape}; if (tshape.isInfinite()) { inShape = stripInfiniteShapes(inShape); } // copying the shape prevents "non-orthogonal GTrsf" errors in some versions // of OCC. Something to do with triangulation of shape?? // it may be that incremental mesh would work here too. BRepBuilderAPI_Copy copier(inShape); tshape = Part::TopoShape(copier.Shape()); if (tshape.isNull()) { return {}; } tshape.transformShape(scaler, true, true); tshape.setPlacement(placement); return tshape.getShape(); } //! this getter should work for any object, not just links Base::Placement ShapeFinder::getPlacement(const App::DocumentObject* root) { auto namedProperty = root->getPropertyByName("Placement"); auto placementProperty = dynamic_cast(namedProperty); if (namedProperty && placementProperty) { return placementProperty->getValue(); } return {}; } //! get root's scale property. If root is not a Link related object, then the identity matrrix will //! be returned. Base::Matrix4D ShapeFinder::getScale(const App::DocumentObject* root) { if (!isLinkLike(root)) { return {}; } Base::Matrix4D linkScale; auto namedProperty = root->getPropertyByName("ScaleVector"); auto scaleVectorProperty = dynamic_cast(namedProperty); if (scaleVectorProperty) { linkScale.scale(scaleVectorProperty->getValue()); } return linkScale; } //! there isn't convenient common ancestor for the members of the Link family. We use //! isLinkLike(obj) instead of obj->isDerivedFrom(). Some links have //! proxy objects and will not be detected by isDerivedFrom(). bool ShapeFinder::isLinkLike(const App::DocumentObject* obj) { if (!obj) { return false; } if (obj->isDerivedFrom() || obj->isDerivedFrom() || obj->isDerivedFrom()) { return true; } auto namedProperty = obj->getPropertyByName("LinkedObject"); auto linkedObjectProperty = dynamic_cast(namedProperty); if (linkedObjectProperty) { return true; } namedProperty = obj->getPropertyByName("ElementList"); auto elementListProperty = dynamic_cast(namedProperty); return elementListProperty != nullptr; } //! Infinite shapes can not be projected, so they need to be removed. inShape is usually a compound. //! Datum features (Axis, Plane and CS) are examples of infinite shapes. TopoDS_Shape ShapeFinder::stripInfiniteShapes(const TopoDS_Shape& inShape) { BRep_Builder builder; TopoDS_Compound comp; builder.MakeCompound(comp); TopoDS_Iterator it(inShape); for (; it.More(); it.Next()) { TopoDS_Shape shape = it.Value(); if (shape.ShapeType() < TopAbs_SOLID) { // look inside composite shapes shape = stripInfiniteShapes(shape); } else if (Part::TopoShape(shape).isInfinite()) { continue; } // simple shape & finite builder.Add(comp, shape); } return {std::move(comp)}; } //! check for shape is null or shape has no subshapes(vertex/edge/face/etc) //! this handles the case of an empty compound which is not IsNull, but has no //! content. // Note: the same code exists in TechDraw::ShapeUtils bool ShapeFinder::isShapeReallyNull(const TopoDS_Shape& shape) { // if the shape is null or it has no subshapes, then it is really null return shape.IsNull() || !TopoDS_Iterator(shape).More(); } //! Returns the net transformation of a path from rootObject to leafSub. rootObject's transform //! is included in the result. std::pair ShapeFinder::getGlobalTransform( const App::DocumentObject& rootObject, const std::string& leafSub ) { // we prune the last term if it is a vertex, edge or face std::string newSub = removeGeometryTerm(leafSub); std::vector plmStack; std::vector scaleStack; // get transforms below rootObject // Note: root object is provided by the caller and may or may not be a top level object crawlPlacementChain(plmStack, scaleStack, rootObject, newSub); auto pathTransform = sumTransforms(plmStack, scaleStack); // apply the placements in reverse order - top to bottom // should this be rootObject's local transform? auto rootTransform = getGlobalTransform(&rootObject); auto netPlm = rootTransform.first * pathTransform.first; auto netScale = rootTransform.second * pathTransform.second; return {netPlm, netScale}; } //! tries to get the global position and scale for a object with no information about the //! path through the tree from a root to cursor object. std::pair ShapeFinder::getGlobalTransform( const App::DocumentObject* cursorObject ) { if (!cursorObject) { return {}; } Base::Placement netPlm; Base::Matrix4D netScale = getScale(cursorObject); Base::Placement geoPlm; auto geoCursor = dynamic_cast(cursorObject); if (!isLinkLike(cursorObject) && geoCursor) { netPlm = geoCursor->globalPlacement(); return {netPlm, netScale}; } netPlm = getPlacement(cursorObject); return {netPlm, netScale}; } //! combine a series of placement & scale transforms. The input stacks are expected in leaf to root //! order, but the result is in the expected root to leaf order. std::pair ShapeFinder::sumTransforms( const std::vector& plmStack, const std::vector& scaleStack ) { Base::Placement netPlm; Base::Matrix4D netScale; auto itRevPlm = plmStack.rbegin(); for (; itRevPlm != plmStack.rend(); itRevPlm++) { netPlm *= *itRevPlm; } auto itRevScale = scaleStack.rbegin(); for (; itRevScale != scaleStack.rend(); itRevScale++) { netScale *= *itRevScale; } return {netPlm, netScale}; } //! get the parent to which attachObject is attached via Links (not regular Part::Attacher //! attachment) App::DocumentObject* ShapeFinder::getLinkAttachParent(const App::DocumentObject* attachedObject) { auto namedProperty = attachedObject->getPropertyByName("a1AttParent"); auto attachProperty = dynamic_cast(namedProperty); if (namedProperty && attachProperty) { return attachProperty->getValue(); } return {}; } //! debugging routine that returns a string representation of a placement. // TODO: this should be in Base::Placement? std::string ShapeFinder::PlacementAsString(const Base::Placement& inPlacement) { auto position = inPlacement.getPosition(); auto rotation = inPlacement.getRotation(); Base::Vector3d axis; double angle {0.0}; rotation.getValue(axis, angle); std::stringstream ss; ss << "pos: (" << position.x << ", " << position.y << ", " << position.z << ") axis: (" << axis.x << ", " << axis.y << ", " << axis.z << ") angle: " << Base::toDegrees(angle); return ss.str(); } //! debug routine. return readable form of TopLoc_Location from OCC std::string ShapeFinder::LocationAsString(const TopLoc_Location& location) { auto position = Base::Vector3d { location.Transformation().TranslationPart().X(), location.Transformation().TranslationPart().Y(), location.Transformation().TranslationPart().Z() }; gp_XYZ axisDir; double angle {0}; auto isRotation = location.Transformation().GetRotation(axisDir, angle); Base::Vector3d axis {axisDir.X(), axisDir.Y(), axisDir.Z()}; std::stringstream ss; ss << "isRotation: " << isRotation << " pos: (" << position.x << ", " << position.y << ", " << position.z << ") axis: (" << axisDir.X() << ", " << axisDir.Y() << ", " << axisDir.Z() << ") angle: " << Base::toDegrees(angle); return ss.str(); }