/*************************************************************************** * Copyright (c) 2022 WandererFan * * * * This file is part of the FreeCAD CAx development system. * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library 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 Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this library; see the file COPYING.LIB. If not, * * write to the Free Software Foundation, Inc., 59 Temple Place, * * Suite 330, Boston, MA 02111-1307, USA * * * ***************************************************************************/ //DrawComplexSection processing overview //for Strategy = Offset, DCS is much the same as DVS //for Strategy = Aligned, there are many differences //execute // sectionExec(getShapeToCut()*) //sectionExec // makeSectionCut(baseShape) //makeSectionCut (separate thread) // note that the section cut is not required for Aligned strategy, // but it is useful for debugging // m_cuttingTool = makeCuttingTool* (DVSTool.brep) // m_cutPieces = (baseShape - m_cuttingTool) (DVSCutPieces.brep) //onSectionCutFinished // m_preparedShape = prepareShape(m_cutPieces)* - centered, scaled, rotated // geometryObject = DVP::buildGeometryObject(m_preparedShape) (HLR) //postHlrTasks // faceIntersections = findSectionPlaneIntersections // m_sectionTopoDSFaces = alignSectionFaces(faceIntersections) // m_tdSectionFaces = makeTDSectionFaces(m_sectionTopoDSFaces) //* for Aligned, we use a different ShapeToCut, as the standard one will // cause many coincident face problems later //* the cutting tool is built up from the profile, instead of the simple plane in DVS //* for Aligned, preparing the shape is much different than Offset or DVS // - most of the work is done in makeAlignedPieces // - for each segment of the profile, make a cutting tool, then get the boolean // intersection of the tool and the shape to cut // - align and distribute the intersections along an "effective" section plane // which is a flattened version of the profile #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "DrawComplexSection.h" #include "DrawUtil.h" #include "GeometryObject.h" #include "ShapeUtils.h" using namespace TechDraw; using namespace std; using DU = DrawUtil; //=========================================================================== // DrawComplexSection //=========================================================================== //NOLINTBEGIN PROPERTY_SOURCE(TechDraw::DrawComplexSection, TechDraw::DrawViewSection) const char* DrawComplexSection::ProjectionStrategyEnums[] = {"Offset", "Aligned", "NoParallel", nullptr}; //NOLINTEND DrawComplexSection::DrawComplexSection() : m_waitingForAlign(false) { static const char* fgroup = "Cutting Tool"; //NOLINTBEGIN ADD_PROPERTY_TYPE(CuttingToolWireObject, (nullptr), fgroup, App::Prop_None, "A sketch that describes the cutting tool"); CuttingToolWireObject.setScope(App::LinkScope::Global); ProjectionStrategy.setEnums(ProjectionStrategyEnums); ADD_PROPERTY_TYPE(ProjectionStrategy, ((long)0), fgroup, App::Prop_None, "Make a single cut, or use the profile in pieces"); //NOLINTEND } TopoDS_Shape DrawComplexSection::makeCuttingTool(double dMax) { TopoDS_Wire profileWire = makeProfileWire(); if (profileWire.IsNull()) { throw Base::RuntimeError("Can not make wire from cutting tool (1)"); } if (debugSection()) { BRepTools::Write(profileWire, "DCSmakeCuttingTool_profileWire.brep");//debug } // use "canBuild(profile, sectionnormal)" or validateProfileDirection? if (ProjectionStrategy.getValue() == 0) { // Offset. Warn if profile is not quite aligned with section normal. if // the profile and normal are misaligned, the check below for empty "solids" // will not be correct. constexpr double AngleThresholdDeg{5.0}; // bool isOK = validateOffsetProfile(profileWire, SectionNormal.getValue(), AngleThresholdDeg); } // TODO: this check should be in TaskComplexSection also. here it prevents a hard occ crash // if the sketch can't be made into an appropriate face/prism. if (CuttingToolWireObject.getValue()->isDerivedFrom(Base::Type::fromName("Sketcher::SketchObject"))) { if (!validateSketchNormal(CuttingToolWireObject.getValue())) { Base::Console().warning("cutting object not aligned with section normal in %s\n", Label.getValue()); } } if (BRep_Tool::IsClosed(profileWire)) { return makeCuttingToolFromClosedProfile(profileWire, dMax); } TopoDS_Shape cuttingTool = cuttingToolFromProfile(profileWire, dMax); if (debugSection()) { BRepTools::Write(cuttingTool, "DCSmakeCuttingTool_cuttingToo.brep");//debug } // save the tool face for shading/hatching of cut surface auto extrudeDir = Base::convertTo(getReferenceAxis()); gp_Vec extrudeVec = 2 * dMax * extrudeDir; m_toolFaceShape = BRepPrimAPI_MakePrism(profileWire, extrudeVec).Shape(); m_toolFaceShape = ShapeUtils::moveShape(m_toolFaceShape, Base::convertTo(extrudeDir) * -dMax); if (debugSection()) { BRepTools::Write(m_toolFaceShape, "DCSmakeCuttingTool_m_toolFaceShape.brep"); //debug } if (cuttingTool.ShapeType() == TopAbs_COMPSOLID || cuttingTool.ShapeType() == TopAbs_COMPOUND) { //Composite Solids do not cut well if they contain "solids" with no volume. This //happens if the profile has segments parallel to the extrude direction. //We need to disassemble it and only keep the real solids. return removeEmptyShapes(cuttingTool); } return cuttingTool; } TopoDS_Shape DrawComplexSection::getShapeToPrepare() const { if (ProjectionStrategy.getValue() == 0) { //Offset. Use regular section behaviour return DrawViewSection::getShapeToPrepare(); } //Aligned (or NoParallel) strategy return m_saveShape;//the original input shape } //get the shape ready for projection and cut surface finding TopoDS_Shape DrawComplexSection::prepareShape(const TopoDS_Shape& cutShape, double shapeSize) { if (ProjectionStrategy.getValue() == 0) { //Offset. Use regular section behaviour return DrawViewSection::prepareShape(cutShape, shapeSize); } //"Aligned" projection (Aligned Section) if (m_alignResult.IsNull()) { return {}; } // our shape is already centered "left/right" and "up/down" so we don't need to // center it here m_preparedShape = ShapeUtils::scaleShape(m_alignResult, getScale()); if (!DrawUtil::fpCompare(Rotation.getValue(), 0.0)) { m_preparedShape = ShapeUtils::rotateShape(m_preparedShape, getProjectionCS(), Rotation.getValue()); } if (debugSection()) { BRepTools::Write(m_preparedShape, "DCSprepareShape_preparedShape.brep"); //debug } return m_preparedShape; } void DrawComplexSection::makeSectionCut(const TopoDS_Shape& baseShape) { if (ProjectionStrategy.getValue() == 0) { //Offset. Use regular section behaviour return DrawViewSection::makeSectionCut(baseShape); } try { connectAlignWatcher = QObject::connect(&m_alignWatcher, &QFutureWatcherBase::finished, &m_alignWatcher, [this] { this->onSectionCutFinished(); }); // We create a lambda closure to hold a copy of baseShape. // This is important because this variable might be local to the calling // function and might get destructed before the parallel processing finishes. auto lambda = [this, baseShape]{this->makeAlignedPieces(baseShape);}; m_alignFuture = QtConcurrent::run(std::move(lambda)); m_alignWatcher.setFuture(m_alignFuture); waitingForAlign(true); } catch (...) { Base::Console().warning("%s failed to make alignedPieces\n", Label.getValue()); return; } return DrawViewSection::makeSectionCut(baseShape); } void DrawComplexSection::onSectionCutFinished() { if (m_cutFuture.isRunning() || //waitingForCut() m_alignFuture.isRunning()) {//waitingForAlign() //can not continue yet. return until the other thread ends return; } DrawViewSection::onSectionCutFinished(); QObject::disconnect(connectAlignWatcher); } //! for Aligned strategy, cut the rawShape by each segment of the tool profile and arrange the //! cut results in order. void DrawComplexSection::makeAlignedPieces(const TopoDS_Shape& rawShape) { if (!canBuild(getSectionCS(), CuttingToolWireObject.getValue())) { throw Base::RuntimeError("Profile is parallel to Section Normal"); } TopoDS_Wire profileWire = makeProfileWire(); if (profileWire.IsNull()) { throw Base::RuntimeError("ComplexSection failed to make profileWire"); } auto edgesAll = getUniqueEdges(profileWire); if (edgesAll.empty()) { // this is bad! throw Base::RuntimeError("Complex section: profile wire has no edges."); } // the reversers control left to right vs right to left (or top to bottom vs bottom to top) // arrangement of the cut pieces. double horizReverser{1.0}; double verticalReverser{1.0}; gp_Vec gProfileVec = makeProfileVector(profileWire); auto isProfileVertical = getReversers(gProfileVec, horizReverser, verticalReverser); // we should wind up with one entry per segment of the profile // this should be done with map and a response class std::vector pieces(edgesAll.size()); // results of cutting source with each segment's tool shape std::vector pieceXSizeAll(edgesAll.size()); //size in sectionCS.XDirection (width) std::vector pieceYSizeAll(edgesAll.size()); //size in sectionCS.YDirection (height) std::vector pieceZSizeAll(edgesAll.size()); //size in sectionCS.Direction (depth) std::vector pieceVerticalAll(edgesAll.size()); // displacement of piece in vertical direction auto uSectionNormal = SectionNormal.getValue(); uSectionNormal.Normalize(); auto uRotateAxis = getReferenceAxis(); uRotateAxis.Normalize(); // these normals will be pointing into a tool made from the profile. We want the normal pointing into // the remaining material shape, so we will reverse the normal at time of use. std::vector> faceNormals = getSegmentViewDirections(profileWire, uSectionNormal); // faceNormals are not in the same order as the faces(sometimes??). TopExp_Explorer expFaces(m_toolFaceShape, TopAbs_FACE); for (int iPiece = 0; expFaces.More(); expFaces.Next(), iPiece++) { TopoDS_Face face = TopoDS::Face(expFaces.Current()); if (!isFacePlanar(face)) { // TODO: continue blocks curved profile segments (which doesn't work right). continue; } // faceNormals point into toolShape. we want to intersect with remaining material, so reverse // the normal. std::pair segmentPair = findNormalForFace(face, faceNormals, edgesAll); int segmentIndex = segmentPair.first; Base::Vector3d segmentNormal = segmentPair.second * -1; segmentNormal.Normalize(); // always true for aligned, but not for no-parallel if (!showSegment(segmentNormal)) { continue; } double pieceVertical{0}; TopoDS_Shape rotatedPiece = cutAndRotatePiece(rawShape, face, segmentIndex, segmentNormal, pieceVertical); if (debugSection()) { stringstream ss; ss << "DCSmakeAlignedPieces_cutAndRotatedPiece" << segmentIndex << ".brep"; BRepTools::Write(rotatedPiece, ss.str().c_str());//debug } AlignedSizeResponse sizeResponse = getAlignedSize(rotatedPiece, segmentIndex); Base::Vector3d pieceSize = sizeResponse.pieceSize; pieceXSizeAll.at(segmentIndex) = pieceSize.x; // size in ProjectionCS. pieceYSizeAll.at(segmentIndex) = pieceSize.y; pieceZSizeAll.at(segmentIndex) = pieceSize.z; pieceVerticalAll.at(segmentIndex) = pieceVertical; if (debugSection()) { stringstream ss; ss << "DCSAlignedPiece" << segmentIndex << ".brep"; BRepTools::Write(sizeResponse.alignedPiece, ss.str().c_str());//debug } auto pieceOnPlane = movePieceToPaperPlane(sizeResponse.alignedPiece, sizeResponse.zMax); if (debugSection()) { stringstream ss; ss << "DCSpieceOnPlane" << segmentIndex << ".brep"; BRepTools::Write(pieceOnPlane, ss.str().c_str());//debug } // pieceOnPlane is on the paper plane, with piece centroid at the origin pieces.at(segmentIndex) = pieceOnPlane; } if (pieces.empty()) { m_alignResult = TopoDS_Compound(); Base::Console().message("DCS::makeAlignedPieces - no result\n"); return; } //space the pieces "horizontally" or "vertically" in OXYZ double movementReverser = isProfileVertical ? verticalReverser : horizReverser; auto movementAxis = gp_Vec(gp::OX().Direction()); auto alignmentAxis = gp_Vec(gp::OY().Direction().Reversed()); if (isProfileVertical) { movementAxis = gp_Vec(gp::OY().Direction()); alignmentAxis = gp_Vec(gp::OX().Direction()); } gp_Vec gMovementVector = movementAxis * movementReverser; size_t stopAt = pieces.size(); double cursorPosition = 0.0; for (size_t iPiece = 0; iPiece < stopAt; iPiece++) { double pieceSizeMoveDist = pieceXSizeAll.at(iPiece); if (isProfileVertical) { pieceSizeMoveDist = pieceYSizeAll.at(iPiece); } auto movedPiece = distributePiece(pieces.at(iPiece), pieceSizeMoveDist, pieceVerticalAll.at(iPiece), alignmentAxis, gMovementVector, cursorPosition); pieces.at(iPiece) = movedPiece; cursorPosition += pieceSizeMoveDist; if (debugSection()) { stringstream ss; ss << "DCSMovedPiece" << iPiece << ".brep"; BRepTools::Write(pieces.at(iPiece), ss.str().c_str());//debug } } //make a compound of the aligned pieces BRep_Builder builder; TopoDS_Compound comp; builder.MakeCompound(comp); for (auto& piece : pieces) { builder.Add(comp, piece); } //center the compound along SectionCS XDirection Base::Vector3d centerVector = Base::convertTo(gMovementVector) * cursorPosition / -2; TopoDS_Shape centeredCompound = ShapeUtils::moveShape(comp, centerVector); if (debugSection()) { BRepTools::Write(centeredCompound, "DCSmap40CenteredCompound.brep");//debug } // re-align our shape with the projection CS gp_Ax3 stdCS; //OXYZ gp_Trsf xPieceAlign; xPieceAlign.SetTransformation(getProjectionCS(), stdCS); BRepBuilderAPI_Transform mkTransAlign(centeredCompound, xPieceAlign); TopoDS_Shape alignedCompound = mkTransAlign.Shape(); if (debugSection()) { BRepTools::Write(alignedCompound, "DCSmap50AlignedCompound.brep");//debug } m_alignResult = alignedCompound; } //! tries to find the intersection faces of the cut shape and the cutting tool. //! Aligned complex sections need to intersect the final cut shape (which in this //! case is a compound of individual cuts) with the "effective" (flattened) section plane. //! Profiles containing curves need special handling (not implemented). TopoDS_Compound DrawComplexSection::findSectionPlaneIntersections(const TopoDS_Shape& shapeToIntersect) { if (shapeToIntersect.IsNull()) { // this shouldn't happen Base::Console().warning("DCS::findSectionPlaneInter - %s - cut shape is Null\n", getNameInDocument()); return {}; } if (ProjectionStrategy.getValue() == 0) {//Offset return singleToolIntersections(shapeToIntersect); } return alignedToolIntersections(shapeToIntersect); } //Intersect cutShape with each segment of the cutting tool TopoDS_Compound DrawComplexSection::singleToolIntersections(const TopoDS_Shape& cutShape) { App::DocumentObject* toolObj = CuttingToolWireObject.getValue(); if (!isLinearProfile(toolObj)) { //TODO: special handling required here // Base::Console().message("DCS::singleToolIntersection - profile has curves\n"); } BRep_Builder builder; TopoDS_Compound result; builder.MakeCompound(result); if (m_toolFaceShape.IsNull()) { return result; } TopExp_Explorer expFaces(cutShape, TopAbs_FACE); for (; expFaces.More(); expFaces.Next()) { TopoDS_Face face = TopoDS::Face(expFaces.Current()); if (!boxesIntersect(face, m_toolFaceShape)) { continue; } std::vector commonFaces = faceShapeIntersect(face, m_toolFaceShape); for (auto& cFace : commonFaces) { builder.Add(result, cFace); } } return result; } //Intersect cutShape with the effective (flattened paper plane) cutting plane to generate cut surface faces TopoDS_Compound DrawComplexSection::alignedToolIntersections(const TopoDS_Shape& cutShape) { BRep_Builder builder; TopoDS_Compound result; builder.MakeCompound(result); App::DocumentObject* toolObj = CuttingToolWireObject.getValue(); if (!isLinearProfile(toolObj)) { //TODO: special handling here? // Base::Console().message("DCS::alignedToolIntersection - profile has curves\n"); } gp_Pln effectivePlane = getSectionPlane(); //aligned result can be much wider than the shape itself, so we use an //infinite face. BRepBuilderAPI_MakeFace mkFace(effectivePlane, -Precision::Infinite(), Precision::Infinite(), -Precision::Infinite(), Precision::Infinite()); TopoDS_Face cuttingFace = mkFace.Face(); TopExp_Explorer expFaces(cutShape, TopAbs_FACE); for (; expFaces.More(); expFaces.Next()) { TopoDS_Face face = TopoDS::Face(expFaces.Current()); if (!boxesIntersect(face, cuttingFace)) { continue; } std::vector commonFaces = faceShapeIntersect(face, cuttingFace); for (auto& cFace : commonFaces) { builder.Add(result, cFace); } } if (debugSection()) { BRepTools::Write(result, "DCSmakeAlignedPieces_AlignedIntersectionResult.brep");//debug } return result; } TopoDS_Compound DrawComplexSection::alignSectionFaces(const TopoDS_Shape& faceIntersections) { if (ProjectionStrategy.getValue() == 0) { //Offset. Use regular section behaviour return DrawViewSection::alignSectionFaces(faceIntersections); } return TopoDS::Compound(mapToPage(faceIntersections)); } TopoDS_Shape DrawComplexSection::getShapeToIntersect() { if (ProjectionStrategy.getValue() == 0) {//Offset return DrawViewSection::getShapeToIntersect(); } //Aligned return m_preparedShape; } TopoDS_Shape DrawComplexSection::getShapeForDetail() const { if (ProjectionStrategy.getValue() == 0) {//Offset return DrawViewSection::getShapeForDetail(); } //Aligned return m_preparedShape; } TopoDS_Wire DrawComplexSection::makeProfileWire() const { App::DocumentObject* toolObj = CuttingToolWireObject.getValue(); return makeProfileWire(toolObj); } TopoDS_Wire DrawComplexSection::makeProfileWire(App::DocumentObject* toolObj) { if (!isProfileObject(toolObj)) { return {}; } TopoDS_Shape toolShape = Part::Feature::getShape(toolObj, Part::ShapeOption::ResolveLink | Part::ShapeOption::Transform); if (toolShape.IsNull()) { return {}; } TopoDS_Wire profileWire; if (toolShape.ShapeType() == TopAbs_WIRE) { profileWire = makeNoseToTailWire(toolShape); } else { //we have already checked that the shape is a wire or an edge in isProfileObject TopoDS_Edge edge = TopoDS::Edge(toolShape); profileWire = BRepBuilderAPI_MakeWire(edge).Wire(); } return profileWire; } gp_Vec DrawComplexSection::makeProfileVector(const TopoDS_Wire& profileWire) { auto ends = getWireEnds(profileWire); auto vec = ends.second - ends.first; vec.Normalize(); return Base::convertTo(vec); } //make drawable td geometry for section line BaseGeomPtrVector DrawComplexSection::makeSectionLineGeometry() { BaseGeomPtrVector result; auto* baseDvp = freecad_cast(BaseView.getValue()); if (baseDvp) { TopoDS_Wire lineWire = makeSectionLineWire(); // projectedWire will be Y inverted TopoDS_Shape projectedWire = GeometryObject::projectSimpleShape(lineWire, baseDvp->getProjectionCS()); TopExp_Explorer expEdges(projectedWire, TopAbs_EDGE); for (; expEdges.More(); expEdges.Next()) { BaseGeomPtr edge = BaseGeom::baseFactory(TopoDS::Edge(expEdges.Current())); result.push_back(edge); } } return result; } //get the end points of the section wire std::pair DrawComplexSection::sectionLineEnds() { std::pair result; TopoDS_Wire lineWire = makeSectionLineWire(); if (lineWire.IsNull()) { return result; } auto ends = getWireEnds(lineWire); Base::Vector3d first = ends.first; Base::Vector3d last = ends.second; auto* baseDvp = freecad_cast(BaseView.getValue()); if (baseDvp) { first = baseDvp->projectPoint(first); last = baseDvp->projectPoint(last); } result.first = first; result.second = last; return result; } //! get the directions (in 3d) of the section line arrows. //! the arrows on the section line are line of sight - from eye to preserved material. In a simple section, //! this is opposite to the section normal. In the complex section, we need the perpendicular direction most //! opposite to the SectionNormal. The arrows should be perpendicular to their profile segment. std::pair DrawComplexSection::sectionLineArrowDirs() { App::DocumentObject* toolObj = CuttingToolWireObject.getValue(); TopoDS_Wire profileWire = makeProfileWire(toolObj); if (profileWire.IsNull()) { throw Base::RuntimeError("Complex section profile wire is null"); } std::vector> segmentNormals = getSegmentViewDirections(profileWire, SectionNormal.getValue()); if (segmentNormals.empty()) { throw Base::RuntimeError("A complex section failed to create profile segment view directions"); } Base::Vector3d firstArrowDir = segmentNormals.front().second * -1; Base::Vector3d lastArrowDir = segmentNormals.back().second * -1; return { firstArrowDir, lastArrowDir }; } //! makes a representation of the 3d arrow directions on the base view's coord system std::pair DrawComplexSection::sectionLineArrowDirsMapped() { std::pair arrowDirsRaw = sectionLineArrowDirs(); TopoDS_Edge firstMapped = mapEdgeToBase(arrowDirsRaw.first); TopoDS_Edge lastMapped = mapEdgeToBase(arrowDirsRaw.second); std::pair arrowEnds = getSegmentEnds(firstMapped); Base::Vector3d firstDir = arrowEnds.second - arrowEnds.first; arrowEnds = getSegmentEnds(lastMapped); Base::Vector3d lastDir = arrowEnds.second - arrowEnds.first; return { firstDir, lastDir }; } //! find an axis for measuring rotation vs the line of sight Base::Vector3d DrawComplexSection::getReferenceAxis() const { Base::Vector3d rawDirection = getBaseDVP()->Direction.getValue(); rawDirection.Normalize(); return DU::closestBasisOriented(rawDirection); } //make a wire suitable for projection on a base view TopoDS_Wire DrawComplexSection::makeSectionLineWire() { TopoDS_Wire lineWire; App::DocumentObject* toolObj = CuttingToolWireObject.getValue(); auto* baseDvp = freecad_cast(BaseView.getValue()); if (baseDvp) { TopoDS_Shape toolShape = Part::Feature::getShape(toolObj, Part::ShapeOption::ResolveLink | Part::ShapeOption::Transform); if (toolShape.IsNull()) { // CuttingToolWireObject is likely still restoring and has no shape yet return {}; } Base::Vector3d centroid = baseDvp->getCurrentCentroid(); TopoDS_Shape sTrans = ShapeUtils::ShapeUtils::moveShape(toolShape, centroid * -1.0); TopoDS_Shape sScaled = ShapeUtils::scaleShape(sTrans, baseDvp->getScale()); //we don't mirror the scaled shape here as it will be mirrored by the projection if (sScaled.ShapeType() == TopAbs_WIRE) { lineWire = makeNoseToTailWire(sScaled); } else if (sScaled.ShapeType() == TopAbs_EDGE) { TopoDS_Edge edge = TopoDS::Edge(sScaled); lineWire = BRepBuilderAPI_MakeWire(edge).Wire(); } else { //probably can't happen as cut profile has been checked before this Base::Console().warning("DCS::makeSectionLineGeometry - profile is type: %d\n", static_cast(sScaled.ShapeType())); return {}; } } return lineWire; } //find the points where the section line changes direction, and the direction //of the profile before and after the point ChangePointVector DrawComplexSection::getChangePointsFromSectionLine() { ChangePointVector result; std::vector allPoints; auto* baseDvp = freecad_cast(BaseView.getValue()); if (baseDvp) { TopoDS_Wire lineWire = makeSectionLineWire(); // projectedWire will be Y inverted TopoDS_Shape projectedWire = GeometryObject::projectSimpleShape(lineWire, baseDvp->getProjectionCS()); if (projectedWire.IsNull()) { return result; } //get UNIQUE points along the projected profile TopExp_Explorer expVertex(projectedWire, TopAbs_VERTEX); gp_Pnt previousPoint(Precision::Infinite(), Precision::Infinite(), Precision::Infinite()); for (; expVertex.More(); expVertex.Next()) { TopoDS_Vertex vert = TopoDS::Vertex(expVertex.Current()); gp_Pnt gPoint = BRep_Tool::Pnt(vert); if (gPoint.IsEqual(previousPoint, 2 * EWTOLERANCE)) { continue; } allPoints.push_back(gPoint); previousPoint = gPoint; } //make the intermediate marks for (size_t iPoint = 1; iPoint < allPoints.size() - 1; iPoint++) { gp_Pnt location = allPoints.at(iPoint); gp_Dir preDir = gp_Dir(allPoints.at(iPoint - 1).XYZ() - allPoints.at(iPoint).XYZ()); gp_Dir postDir = gp_Dir(allPoints.at(iPoint + 1).XYZ() - allPoints.at(iPoint).XYZ()); ChangePoint point(location, preDir, postDir); result.push_back(point); } //make start and end marks gp_Pnt location0 = allPoints.at(0); gp_Pnt location1 = allPoints.at(1); gp_Dir postDir = gp_Dir(location1.XYZ() - location0.XYZ()); gp_Dir preDir = postDir.Reversed(); ChangePoint startPoint(location0, preDir, postDir); result.push_back(startPoint); location0 = allPoints.at(allPoints.size() - 1); location1 = allPoints.at(allPoints.size() - 2); preDir = gp_Dir(location0.XYZ() - location1.XYZ()); postDir = preDir.Reversed(); ChangePoint endPoint(location0, preDir, postDir); result.push_back(endPoint); } return result; } gp_Ax2 DrawComplexSection::getCSFromBase(const std::string& sectionName) const { App::DocumentObject* base = BaseView.getValue(); if (!base || !base->isDerivedFrom()) {//is second clause necessary? //if this DCS does not have a baseView, we must use the existing SectionCS return getSectionCS(); } return DrawViewSection::getCSFromBase(sectionName); } // check for profile segments that are almost, but not quite in the same direction // as the section normal direction. this often indicates a problem with the direction // being slightly wrong. see https://forum.freecad.org/viewtopic.php?t=79017&sid=612a62a60f5db955ee071a7aaa362dbb bool DrawComplexSection::validateOffsetProfile(const TopoDS_Wire& profile, Base::Vector3d direction, double angleThresholdDeg) const { constexpr double HalfCircleDegrees{180.0}; double angleThresholdRad = angleThresholdDeg * std::numbers::pi / HalfCircleDegrees; TopExp_Explorer explEdges(profile, TopAbs_EDGE); for (; explEdges.More(); explEdges.Next()) { std::pair segmentEnds = getSegmentEnds(TopoDS::Edge(explEdges.Current())); Base::Vector3d segmentDir = segmentEnds.second - segmentEnds.first; double angleRad = segmentDir.GetAngle(direction); if (angleRad < angleThresholdRad && angleRad > 0.0) { // profile segment is slightly skewed. possible bad SectionNormal? Base::Console().warning("%s profile is slightly skewed. Check SectionNormal low decimal places\n", getNameInDocument()); return false; } } return true; } // next two methods could be templated (??) to handle edge/wire std::pair DrawComplexSection::getSegmentEnds(const TopoDS_Edge& segment) { TopoDS_Vertex tvFirst; TopoDS_Vertex tvLast; TopExp::Vertices(segment, tvFirst, tvLast); gp_Pnt gpFirst = BRep_Tool::Pnt(tvFirst); gp_Pnt gpLast = BRep_Tool::Pnt(tvLast); std::pair result; result.first = Base::convertTo(gpFirst); result.second = Base::convertTo(gpLast); return result; } std::pair DrawComplexSection::getWireEnds(const TopoDS_Wire& wire) { TopoDS_Vertex tvFirst; TopoDS_Vertex tvLast; TopExp::Vertices(wire, tvFirst, tvLast); gp_Pnt gpFirst = BRep_Tool::Pnt(tvFirst); gp_Pnt gpLast = BRep_Tool::Pnt(tvLast); std::pair result; result.first = Base::convertTo(gpFirst); result.second = Base::convertTo(gpLast); return result; } //get the "effective" (flattened paper plane) section plane for Aligned and //the regular sectionPlane for Offset. gp_Pln DrawComplexSection::getSectionPlane() const { if (ProjectionStrategy.getValue() == 0) { //Offset. Use regular section behaviour return DrawViewSection::getSectionPlane(); } //"Aligned" projection (Aligned Section) //this is the same as DVS::getSectionPlane except that the plane origin is not the SectionOrigin Base::Vector3d vSectionNormal = SectionNormal.getValue(); gp_Dir gSectionNormal(vSectionNormal.x, vSectionNormal.y, vSectionNormal.z); gp_Pnt gOrigin(0.0, 0.0, 0.0); gp_Ax3 gPlaneCS(gOrigin, gSectionNormal); return {gPlaneCS}; } bool DrawComplexSection::isBaseValid() const { App::DocumentObject* base = BaseView.getValue(); if (!base) { //complex section is not based on an existing DVP return true; } if (!base->isDerivedFrom()) { //this is probably an error somewhere. the valid options are base = a DVP, //or no base return false; } //have a base and it is a DVP return true; } //if the profile is not nicely positioned within the vertical span of the shape, we might not overlap //the shape after extrusion. As long as the profile is within the extent of the shape in the //extrude direction we should be ok. bool DrawComplexSection::validateProfilePosition(const TopoDS_Wire& profileWire, const gp_Ax2& sectionCS) const { auto wireEnds = getWireEnds(profileWire); auto gpFirst = Base::convertTo(wireEnds.first); gp_Vec gProfileVector = makeProfileVector(profileWire); //since bounding boxes are aligned with the cardinal directions, we need to find //the appropriate direction to use when validating the profile position gp_Vec gSectionVector = getSectionCS().Direction().Reversed(); gp_Vec gExtrudeVector = gSectionVector.Crossed(gProfileVector); Base::Vector3d vClosestBasis = DrawUtil::closestBasis(gp_Dir(gExtrudeVector), sectionCS); auto gClosestBasis = gp_Dir(vClosestBasis.x, vClosestBasis.y, vClosestBasis.z); Bnd_Box shapeBox; shapeBox.SetGap(0.0); BRepBndLib::AddOptimal(m_saveShape, shapeBox); double xMin = 0, xMax = 0, yMin = 0, yMax = 0, zMin = 0, zMax = 0; //NOLINT shapeBox.Get(xMin, yMin, zMin, xMax, yMax, zMax); double spanLow = xMin; double spanHigh = xMax; double spanCheck = gpFirst.X(); if (gClosestBasis.IsParallel(sectionCS.YDirection(), Precision::Angular())) { spanLow = yMin; spanHigh = yMax; spanCheck = gpFirst.Y(); } else if (gClosestBasis.IsParallel(sectionCS.Direction(), Precision::Angular())) { spanLow = zMin; spanHigh = zMax; spanCheck = gpFirst.Z(); } if (spanLow > spanCheck || spanHigh < spanCheck) { //profile is not within span of shape return false; } //profile is within span of shape return true; } bool DrawComplexSection::showSegment(gp_Dir segmentNormal) const { if (ProjectionStrategy.getValue() < 2) { //Offset or Aligned are always true return true; } Base::Vector3d vSectionNormal = SectionNormal.getValue(); gp_Dir gSectionNormal(vSectionNormal.x, vSectionNormal.y, vSectionNormal.z); //segment normal is perpendicular to section normal, so segment is parallel to section normal, //and for ProjectionStrategy "NoParallel", we don't display these segments. return !DU::fpCompare(fabs(gSectionNormal.Dot(segmentNormal)), 0.0); } bool DrawComplexSection::showSegment(const Base::Vector3d& segmentNormal) const { return showSegment(Base::convertTo(segmentNormal)); } //Can we make a ComplexSection using this profile and sectionNormal? bool DrawComplexSection::canBuild(gp_Ax2 sectionCS, App::DocumentObject* profileObject) { if (!isProfileObject(profileObject)) { return false; } TopoDS_Shape shape = Part::Feature::getShape(profileObject, Part::ShapeOption::ResolveLink | Part::ShapeOption::Transform); if (BRep_Tool::IsClosed(shape)) { //closed profiles don't have a profile vector but should always make a section? return true; } // profile should be ortho to section normal, but this is a weak test gp_Vec gProfileVec = makeProfileVector(makeProfileWire(profileObject)); double dot = fabs(gProfileVec.Dot(sectionCS.Direction())); return !DU::fpCompare(dot, 1.0, EWTOLERANCE); } // general purpose geometry methods // returns the normal of the face. gp_Dir DrawComplexSection::getFaceNormal(TopoDS_Face& face) { BRepAdaptor_Surface adapt(face); double uParmFirst = adapt.FirstUParameter(); double uParmLast = adapt.LastUParameter(); double vParmFirst = adapt.FirstVParameter(); double vParmLast = adapt.LastVParameter(); double uMid = (uParmFirst + uParmLast) / 2; double vMid = (vParmFirst + vParmLast) / 2; constexpr double PropTolerance{0.01}; BRepLProp_SLProps prop(adapt, uMid, vMid, 1, PropTolerance); gp_Dir normalDir(0.0, 0.0, 1.0);//default if (prop.IsNormalDefined()) { normalDir = prop.Normal(); } return normalDir; } bool DrawComplexSection::boxesIntersect(TopoDS_Face& face, TopoDS_Shape& shape) { constexpr double OverlapTolerance{0.1}; Bnd_Box box0; Bnd_Box box1; BRepBndLib::Add(face, box0); box0.SetGap(OverlapTolerance);//generous BRepBndLib::Add(shape, box1); box1.SetGap(OverlapTolerance); return !box0.IsOut(box1); } TopoDS_Shape DrawComplexSection::shapeShapeIntersect(const TopoDS_Shape& shape0, const TopoDS_Shape& shape1) { FCBRepAlgoAPI_Common anOp; anOp.SetFuzzyValue(EWTOLERANCE); TopTools_ListOfShape anArg1; TopTools_ListOfShape anArg2; anArg1.Append(shape0); anArg2.Append(shape1); anOp.SetArguments(anArg1); anOp.SetTools(anArg2); anOp.Build(); TopoDS_Shape result = anOp.Shape();//always a compound if (isTrulyEmpty(result)) { return {}; } return result; } //find all the intersecting regions of face and shape std::vector DrawComplexSection::faceShapeIntersect(const TopoDS_Face& face, const TopoDS_Shape& shape) { TopoDS_Shape intersect = shapeShapeIntersect(face, shape); if (intersect.IsNull()) { return {}; } std::vector intersectFaceList; TopExp_Explorer expFaces(intersect, TopAbs_FACE); for (int i = 1; expFaces.More(); expFaces.Next(), i++) { intersectFaceList.push_back(TopoDS::Face(expFaces.Current())); } return intersectFaceList; } //! ensure that the edges in the output wire are in nose to tail order //! this doesn't need to be a wire, it could be just a shape? TopoDS_Wire DrawComplexSection::makeNoseToTailWire(const TopoDS_Shape& inShape) { if (inShape.IsNull()) { return {}; } std::list inList; TopExp_Explorer expEdges(inShape, TopAbs_EDGE); for (; expEdges.More(); expEdges.Next()) { TopoDS_Edge edge = TopoDS::Edge(expEdges.Current()); inList.push_back(edge); } std::list sortedList; if (inList.empty() || inList.size() == 1) { return {}; } sortedList = DrawUtil::sort_Edges(EWTOLERANCE, inList); BRepBuilderAPI_MakeWire mkWire; for (auto& edge : sortedList) { mkWire.Add(edge); } return mkWire.Wire(); } //static bool DrawComplexSection::isProfileObject(App::DocumentObject* obj) { //if the object's shape is a wire or an edge, then it can be a profile object TopoDS_Shape shape = Part::Feature::getShape(obj, Part::ShapeOption::ResolveLink | Part::ShapeOption::Transform); if (shape.IsNull()) { return false; } if (shape.ShapeType() == TopAbs_WIRE || shape.ShapeType() == TopAbs_EDGE) { return true; } //don't know what this is, but it isn't suitable as a profile return false; } bool DrawComplexSection::isMultiSegmentProfile(App::DocumentObject* obj) { //if the object's shape is a wire or an edge, then it can be a profile object TopoDS_Shape shape = Part::Feature::getShape(obj, Part::ShapeOption::ResolveLink | Part::ShapeOption::Transform); if (shape.IsNull()) { return false; } if (shape.ShapeType() == TopAbs_EDGE) { //only have 1 edge, can't be multisegment; return false; } if (shape.ShapeType() == TopAbs_WIRE) { std::vector edgesInWire; TopExp_Explorer expEdges(shape, TopAbs_EDGE); for (; expEdges.More(); expEdges.Next()) { TopoDS_Edge edge = TopoDS::Edge(expEdges.Current()); BRepAdaptor_Curve adapt(edge); if (adapt.GetType() == GeomAbs_Line) { edgesInWire.push_back(edge); } } if (edgesInWire.size() > 1) { return true; } } return false; } //check if the profile has curves in it bool DrawComplexSection::isLinearProfile(App::DocumentObject* obj) { TopoDS_Shape shape = Part::Feature::getShape(obj, Part::ShapeOption::ResolveLink | Part::ShapeOption::Transform); if (shape.IsNull()) { return false; } if (shape.ShapeType() == TopAbs_EDGE) { //only have 1 edge TopoDS_Edge edge = TopoDS::Edge(shape); BRepAdaptor_Curve adapt(edge); return (adapt.GetType() == GeomAbs_Line); } if (shape.ShapeType() == TopAbs_WIRE) { std::vector edgesInWire; TopExp_Explorer expEdges(shape, TopAbs_EDGE); for (; expEdges.More(); expEdges.Next()) { TopoDS_Edge edge = TopoDS::Edge(expEdges.Current()); BRepAdaptor_Curve adapt(edge); if (adapt.GetType() != GeomAbs_Line) { return false; } } //all the edges in the wire are lines return true; } //this shouldn't happen return false; } //a compound with no content is not considered IsNull by OCC. A more thorough check //is required. //https://dev.opencascade.org/content/compound-empty bool DrawComplexSection::isTrulyEmpty(const TopoDS_Shape& inShape) { bool hasContent = !inShape.IsNull() && TopoDS_Iterator(inShape).More(); return !hasContent; } TopoDS_Shape DrawComplexSection::removeEmptyShapes(const TopoDS_Shape& roughTool) { BRep_Builder builder; TopoDS_Compound comp; builder.MakeCompound(comp); TopExp_Explorer expSolids(roughTool, TopAbs_SOLID); for (; expSolids.More(); expSolids.Next()) { TopoDS_Solid solid = TopoDS::Solid(expSolids.Current()); GProp_GProps gprops; BRepGProp::VolumeProperties(solid, gprops); double volume = gprops.Mass(); if (volume > EWTOLERANCE) { builder.Add(comp, solid); } } return comp; } //! reversers determine if the cut pieces are arranged left to right or right to left (down to up/up to down) //! returns true if the profile vector is vertical. bool DrawComplexSection::getReversers(const gp_Vec& gProfileVec, double& horizReverser, double& verticalReverser) { bool isProfileVertical = true; auto sectionCS = getSectionCS(); auto sectionCSX = sectionCS.XDirection(); auto sectionCSY = sectionCS.YDirection(); auto verticalDot = gProfileVec.Dot(sectionCSY); if (DU::fpCompare(fabs(verticalDot), 0, EWTOLERANCE)) { //profile is +/- normal to Y. isProfileVertical = false; } horizReverser = 1.0; //profile vector points to right, so we move to right if (gProfileVec.Dot(sectionCSX) < 0.0) { //profileVec does not point towards stdX (right in paper space) horizReverser = -1.0; } verticalReverser = 1.0;//profile vector points to top, so we will move pieces upwards if (gProfileVec.Dot(sectionCSY) < 0.0) { //profileVec does not point towards stdY (up in paper space) verticalReverser = -1.0; } return isProfileVertical; } //! align a copy of the piece with OXYZ so we can use bounding box to get //! width, depth, height of the piece. We copy the piece so the transformation //! does not affect the original. AlignedSizeResponse DrawComplexSection::getAlignedSize(const TopoDS_Shape& pieceRotated, int iPiece) const { gp_Ax3 stdCS; //OXYZ BRepBuilderAPI_Copy BuilderPieceCopy(pieceRotated); TopoDS_Shape copyPieceRotatedShape = BuilderPieceCopy.Shape(); gp_Trsf xPieceAlign; xPieceAlign.SetTransformation(stdCS, getProjectionCS()); BRepBuilderAPI_Transform mkTransAlign(copyPieceRotatedShape, xPieceAlign); TopoDS_Shape pieceAligned = mkTransAlign.Shape(); // we may have shifted our piece off center, so we better recenter here gp_Trsf xPieceRecenter; gp_Vec rotatedCentroid = gp_Vec(ShapeUtils::findCentroid(pieceAligned).XYZ()); xPieceRecenter.SetTranslation(rotatedCentroid * -1.0); BRepBuilderAPI_Transform mkTransRecenter(pieceAligned, xPieceRecenter, true); pieceAligned = mkTransRecenter.Shape(); if (debugSection()) { stringstream ss; ss << "DCSDpieceAligned" << iPiece << ".brep"; BRepTools::Write(pieceAligned, ss.str().c_str());//debug ss.clear(); ss.str(std::string()); } Bnd_Box shapeBox; shapeBox.SetGap(0.0); BRepBndLib::AddOptimal(pieceAligned, shapeBox); double xMin = 0, xMax = 0, yMin = 0, yMax = 0, zMin = 0, zMax = 0; //NOLINT shapeBox.Get(xMin, yMin, zMin, xMax, yMax, zMax); double pieceXSize(xMax - xMin); double pieceYSize(yMax - yMin); double pieceZSize(zMax - zMin); Base::Vector3d pieceSize{pieceXSize, pieceYSize, pieceZSize}; return {pieceAligned, pieceSize, zMax}; } //! cut the rawShape with a tool derived from the segmentFace and align the result with the //! page plane. Also supplies the piece size. TopoDS_Shape DrawComplexSection::cutAndRotatePiece(const TopoDS_Shape& rawShape, const TopoDS_Face& segmentFace, int iPiece, // for debug only Base::Vector3d uOrientedSegmentNormal, double& pieceVertical) { auto segmentNormal = Base::convertTo(uOrientedSegmentNormal); auto rotateAxis = Base::convertTo(getReferenceAxis()); gp_Vec extrudeVec = segmentNormal * m_shapeSize; BRepPrimAPI_MakePrism mkPrism(segmentFace, extrudeVec); TopoDS_Shape segmentTool = mkPrism.Shape(); TopoDS_Shape intersect = shapeShapeIntersect(segmentTool, rawShape); if (intersect.IsNull()) { return {}; } if (debugSection()) { stringstream ss; ss << "DCSAintersect" << iPiece << ".brep"; BRepTools::Write(intersect, ss.str().c_str());//debug ss.clear(); ss.str(std::string()); } // move intersection shape to the origin so we can rotate it without worrying about // center of rotation. gp_Trsf xPieceCenter; gp_Vec pieceCentroid = gp_Vec(ShapeUtils::findCentroid(intersect).XYZ()); // save the amount we moved this piece in the vertical direction so we can // put it back in the right place later gp_Vec maskedVertical = DU::maskDirection(pieceCentroid, rotateAxis); maskedVertical = pieceCentroid - maskedVertical; pieceVertical = maskedVertical.X() + maskedVertical.Y() + maskedVertical.Z(); xPieceCenter.SetTranslation(pieceCentroid * -1.0); BRepBuilderAPI_Transform mkTransXLate(intersect, xPieceCenter, true); TopoDS_Shape pieceCentered = mkTransXLate.Shape(); if (debugSection()) { stringstream ss; ss << "DCSBpieceCentered" << iPiece << ".brep"; BRepTools::Write(pieceCentered, ss.str().c_str());//debug ss.clear(); ss.str(std::string()); } //rotate the intersection so interesting face is aligned with what will // become the paper plane. double faceAngle = gp_Vec(getSectionCS().Direction().Reversed()).AngleWithRef(segmentNormal, rotateAxis); gp_Ax1 faceAxis(gp_Pnt(0.0, 0.0, 0.0), rotateAxis); gp_Ax3 stdCS; gp_Ax3 pieceCS; pieceCS.Rotate(faceAxis, faceAngle); gp_Trsf xPieceRotate; xPieceRotate.SetTransformation(stdCS, pieceCS); BRepBuilderAPI_Transform mkTransRotate(pieceCentered, xPieceRotate, true); TopoDS_Shape pieceRotated = mkTransRotate.Shape(); return pieceRotated; } TopoDS_Shape DrawComplexSection::movePieceToPaperPlane(const TopoDS_Shape& piece, double sizeMax) { //now we need to move the piece so that the interesting face is coincident //with the paper plane (stdXY). This will be a move along stdZ by -zMax. gp_Vec toPaperPlane = gp::OZ().Direction().XYZ() * sizeMax * -1.0; gp_Trsf xPieceToPlane; xPieceToPlane.SetTranslation(toPaperPlane); BRepBuilderAPI_Transform mkTransDisplace(piece, xPieceToPlane, true); TopoDS_Shape pieceToPlane = mkTransDisplace.Shape(); // piece is on the paper plane, with piece centroid at the origin return pieceToPlane; } //! move the piece to its position across the page (X for a left right profile) TopoDS_Shape DrawComplexSection::distributePiece(const TopoDS_Shape& piece, double pieceSizeInDirection, double verticalDisplace, const gp_Vec& alignmentAxis, const gp_Vec& gMovementVector, double cursorPosition) { double pieceTotalDistanceToMove = cursorPosition + pieceSizeInDirection / 2; gp_Vec alignmentVector = alignmentAxis * verticalDisplace * -1; gp_Vec netDisplacementVector = gMovementVector * pieceTotalDistanceToMove + alignmentVector; gp_Trsf xPieceDistribute; xPieceDistribute.SetTranslation(netDisplacementVector); BRepBuilderAPI_Transform mkTransDistribute(piece, xPieceDistribute, true); auto distributedPiece = mkTransDistribute.Shape(); return distributedPiece; } // are these in the correct order? no guarantee. profile wires should be in nose to tail order std::vector DrawComplexSection::getUniqueEdges(const TopoDS_Wire& wireIn) { std::vector ret; TopTools_IndexedMapOfShape shapeMap; TopExp_Explorer Ex(wireIn, TopAbs_EDGE); while (Ex.More()) { shapeMap.Add(Ex.Current()); Ex.Next(); } for (Standard_Integer k = 1; k <= shapeMap.Extent(); k++) { const TopoDS_Shape& shape = shapeMap(k); auto edge = TopoDS::Edge(shape); ret.push_back(edge); } return ret; } //! Find the correct view directions for the profile segments by making a face from the profile and //! extruding it into a solid, then examining the faces of the solid. std::vector> DrawComplexSection::getSegmentViewDirections(const TopoDS_Wire& profileWire, Base::Vector3d sectionNormal) const { auto edgesAll = getUniqueEdges(profileWire); if (edgesAll.empty()) { // this is bad! throw Base::RuntimeError("Complex section: profile wire has no edges."); } // self-protection code should be elsewhere if (!checkSectionCS()) { // results will likely be incorrect // this message will show for every recompute of the complex section. Base::Console().warning("Coordinate system for ComplexSection is invalid. Check SectionNormal, Direction or XDirection.\n"); } auto profileVector = Base::convertTo(makeProfileVector(profileWire)); auto parallelDot = profileVector.Dot(sectionNormal); if (DU::fpCompare(std::fabs(parallelDot), 1, EWTOLERANCE)) { Base::Console().warning("Section normal is parallel to profile vector. Results may be incorrect.\n"); } TopoDS_Shape profileSolidTool = cuttingToolFromProfile(profileWire, m_shapeSize); // this should be the flattened profile? should not matter with disttoshape version of isonface. std::vector relocatedEdgesAll = getUniqueEdges(profileWire); if (debugSection()) { BRepTools::Write(profileSolidTool, "DCStoolFromProfile.brep");//debug } std::vector profileNormals; std::vector> normalKV; TopExp_Explorer expFaces(profileSolidTool, TopAbs_FACE); // are all these shenanigans necessary? // no guarantee of order from TopExp_Explorer?? Need to match faces to the profile segment that // generated it? for (int iFace = 0; expFaces.More(); expFaces.Next(), iFace++) { auto shape = expFaces.Current(); auto face = TopoDS::Face(shape); auto normal = Base::convertTo(getFaceNormal(face)); if (face.Orientation() == TopAbs_FORWARD) { // face on solid with Forward orientation will have a normal that points out of the // solid. With Reverse orientation the normal will point into the solid. We want the // direction into the solid. // "Face normal shows where body material is – it lies ‘behind' the face. In a correct solid body all face normals go ‘outward' (see below):" // https://opencascade.blogspot.com/2009/02/continued.html normal *= -1; } // skip top and bottom faces of the prism auto topOrBottomDot = std::fabs(normal.Dot(getReferenceAxis())); if (DU::fpCompare(topOrBottomDot, 1, EWTOLERANCE)) { continue; } if (!isFacePlanar(face)) { // TODO: continue blocks curved profile segments // ?? but also blocks "surface of extrusion" continue; } // this is an interesting face, get the normal and edge int iSegment{0}; for (auto& segment : edgesAll) { if (faceContainsEndpoints(segment, face)) { std::pair newEntry; newEntry.first = iSegment; newEntry.second = normal; normalKV.push_back(newEntry); break; } iSegment++; } } std::sort(normalKV.begin(), normalKV.end(), DrawComplexSection::normalLess); return normalKV; } //! true if the endpoints of edgeToMatch are vertexes of faceToSearch bool DrawComplexSection::faceContainsEndpoints(const TopoDS_Edge& edgeToMatch, const TopoDS_Face& faceToSearch) { std::pair edgeEnds = getSegmentEnds(edgeToMatch); bool matchedFirst = pointOnFace(edgeEnds.first, faceToSearch); bool matchedLast = pointOnFace(edgeEnds.second, faceToSearch); return matchedFirst && matchedLast; } //! extrudes a face made from a wire along the reference axis TopoDS_Shape DrawComplexSection::profileToSolid(const TopoDS_Wire& closedProfileWire, double dMax) const { BRepBuilderAPI_MakeFace mkFace(closedProfileWire); if (!mkFace.IsDone()) { throw Base::RuntimeError("Complex section could not create face from closed profile"); } auto extrudeVector = getReferenceAxis() * dMax * 2; BRepPrimAPI_MakePrism mkPrism(mkFace.Face(), Base::convertTo(extrudeVector)); auto profileSolid = mkPrism.Shape(); return profileSolid; } //! transform an edge to the base view's projection coordinate system TopoDS_Edge DrawComplexSection::mapEdgeToBase(const TopoDS_Edge& inEdge) { App::DocumentObject* baseObj = BaseView.getValue(); auto* baseDvp = freecad_cast(baseObj); BRepBuilderAPI_Copy BuilderEdgeCopy(inEdge); TopoDS_Edge edgeCopy = TopoDS::Edge(BuilderEdgeCopy.Shape()); gp_Ax3 stdCS; //OXYZ gp_Trsf xmapEdgeToBase; xmapEdgeToBase.SetTransformation(stdCS, baseDvp->getProjectionCS()); BRepBuilderAPI_Transform mkMappedEdge(edgeCopy, xmapEdgeToBase); TopoDS_Edge mappedEdge = TopoDS::Edge(mkMappedEdge.Shape()); return mappedEdge; } TopoDS_Edge DrawComplexSection::mapEdgeToBase(const Base::Vector3d& inVector) { if (inVector.Length() == 0) { throw Base::RuntimeError("Complex section received a request to map a null edge"); } gp_Pnt origin(0,0,0); gp_Pnt endPoint{Base::convertTo(inVector)}; TopoDS_Edge edgeToMap = BRepBuilderAPI_MakeEdge(origin, endPoint); return mapEdgeToBase(edgeToMap); } //! +/- same code as in Import::SketchExportHelper std::pair< Base::Vector3d, Base::Vector3d> DrawComplexSection::sketchNormalAndX(App::DocumentObject* sketchObj) { auto sketch = dynamic_cast(sketchObj); if (!sketch || !sketchObj->isDerivedFrom(Base::Type::fromName("Sketcher::SketchObject"))) { // should be a throw? up to the caller to enforce this?? return { Base::Vector3d(0,0,0), Base::Vector3d(0,0,0) }; } auto plm = sketch->Placement.getValue(); Base::Rotation rot = plm.getRotation(); Base::Vector3d stdZ {0.0, 0.0, 1.0}; Base::Vector3d sketchNormal; rot.multVec(stdZ, sketchNormal); Base::Vector3d stdX {1.0, 0.0, 0.0}; Base::Vector3d sketchX; rot.multVec(stdX, sketchX); return {sketchNormal, sketchX}; } //! true if sketch normal is +/- parallel to base view's projection direction. bool DrawComplexSection::validateSketchNormal(App::DocumentObject* sketchObject) const { if (!sketchObject || !sketchObject->isDerivedFrom(Base::Type::fromName("Sketcher::SketchObject"))) { return false; } std::pair normalX = sketchNormalAndX(sketchObject); auto* baseDvp = freecad_cast(BaseView.getValue()); double dot = std::fabs((normalX.first).Dot(baseDvp->Direction.getValue())); return DU::fpCompare(dot, 1, EWTOLERANCE); } // find the index of the profile segment that corresponds to a face in the cutting tool int DrawComplexSection::getSegmentIndex(const TopoDS_Face& face, const std::vector& edgesAll) { int iSegment{0}; for (auto& segment : edgesAll) { if (faceContainsEndpoints(segment, face)) { return iSegment; } iSegment++; } return -1; // not found! } //! find the normal for a face in a key-value pair of (index, normal). std::pair DrawComplexSection::findNormalForFace(const TopoDS_Face& face, const std::vector>& normalKV, const std::vector& segmentEdges) { size_t index = getSegmentIndex(face, segmentEdges); if (index < 0 || index >= segmentEdges.size()) { //NOLINT throw Base::RuntimeError("DCS::findNormalForFace - did not find normal for face!"); } for (auto& keyValue : normalKV) { if (static_cast(keyValue.first) == index) { return keyValue; } } throw Base::RuntimeError("DCS::findNormalForFace - no keyValue pair for segment!"); } bool DrawComplexSection::pointOnFace(Base::Vector3d point, const TopoDS_Face& face) { TopoDS_Vertex vert = BRepBuilderAPI_MakeVertex(Base::convertTo(point)); double dist = DU::simpleMinDist(vert, face); return (dist < EWTOLERANCE); } //! make a cutting tool from a closed shape. up to the caller to make sure the face is sensible. TopoDS_Shape DrawComplexSection::makeCuttingToolFromClosedProfile(const TopoDS_Wire& profileWire, double dMax) { TopoDS_Face toolFace; try { BRepBuilderAPI_MakeFace mkFace(profileWire); toolFace = mkFace.Face(); if (toolFace.IsNull()) { return {}; } } catch (...) { Base::Console().error("%s could not make tool from closed profile\n", Label.getValue()); return {}; } gp_Dir gpNormal = getFaceNormal(toolFace); auto extrudeDir = 2 * dMax * gpNormal; TopoDS_Shape prism = BRepPrimAPI_MakePrism(toolFace, extrudeDir).Shape(); prism = ShapeUtils::moveShape(prism, Base::convertTo(gpNormal) * -dMax); return prism; } bool DrawComplexSection::validateProfileAlignment(const TopoDS_Wire& profileWire) const { // use "canBuild(profile, sectionnormal)"? if (ProjectionStrategy.getValue() == 0) { // Offset. Warn if profile is not quite aligned with section normal. if // the profile and normal are misaligned, the check below for empty "solids" // will not be correct. // just a warning here, so don't fail on this constexpr double AngleThresholdDeg{5.0}; if (!validateOffsetProfile(profileWire, SectionNormal.getValue(), AngleThresholdDeg)) { Base::Console().warning("%s: profile and section normal are misaligned\n", Label.getValue()); } } // TODO: this check should be in TaskComplexSection also. In complex section, it prevents a hard occ crash // if the sketch can't be made into an appropriate face/prism. if (CuttingToolWireObject.getValue()->isDerivedFrom(Base::Type::fromName("Sketcher::SketchObject"))) { if (!validateSketchNormal(CuttingToolWireObject.getValue())) { Base::Console().error("%s: cutting object not aligned with section normal\n", Label.getValue()); return false; } } return true; } //! make a cutting tool from the profile and section normal. TopoDS_Shape DrawComplexSection::cuttingToolFromProfile(const TopoDS_Wire& inProfileWire, double dMax) const { TopoDS_Wire profileWireClosed = closeProfileForCut(inProfileWire, dMax); if (debugSection()) { BRepTools::Write(profileWireClosed, "DCSprofileWireClosed.brep"); //debug } TopoDS_Shape solid = profileToSolid(profileWireClosed, dMax); solid = ShapeUtils::moveShape(solid, getReferenceAxis() * -dMax); return solid; } TopoDS_Wire DrawComplexSection::closeProfileForCut(const TopoDS_Wire& profileWire, double dMax) const { // TODO: do these conversions gp_Pnt <-> Base::Vector3d <-> QPointF cause our problems with low // digits? auto* baseDvp = freecad_cast(BaseView.getValue()); TopoDS_Shape flatShape = GeometryObject::simpleProjection(profileWire, baseDvp->getProjectionCS()); TopoDS_Wire flatWire = makeNoseToTailWire(flatShape); if (debugSection()) { BRepTools::Write(flatWire, "DCSflatCloseWire.brep"); //debug } std::pair pvEnds = getWireEnds(flatWire); Base::Vector3d firstPWPoint = pvEnds.first; Base::Vector3d lastPWPoint = pvEnds.second; Base::Vector3d midPWPoint = (firstPWPoint + lastPWPoint) / 2; Base::Vector3d SNPoint = SectionNormal.getValue() * dMax; Base::Vector3d awayDirection = SNPoint - midPWPoint; // from midpoint to snpoint awayDirection.Normalize(); std::vector profileEdges = DU::shapeToVector(flatWire); TopoDS_Edge firstEdge = profileEdges.front(); std::pair edgeEnds = getSegmentEnds(firstEdge); Base::Vector3d firstExtendDir = edgeEnds.first - edgeEnds.second; firstExtendDir.Normalize(); Base::Vector3d firstExtendStartPoint = edgeEnds.second; double firstInternalDistance = (midPWPoint - firstExtendStartPoint).Length(); Base::Vector3d firstExtendEndPoint = firstExtendStartPoint + firstExtendDir * (dMax - firstInternalDistance); TopoDS_Edge firstReplacementEdge = BRepBuilderAPI_MakeEdge(Base::convertTo(firstExtendStartPoint), Base::convertTo(firstExtendEndPoint)); TopoDS_Edge lastEdge = profileEdges.back(); edgeEnds = getSegmentEnds(lastEdge); Base::Vector3d lastExtendDir = edgeEnds.second - edgeEnds.first; lastExtendDir.Normalize(); Base::Vector3d lastExtendStartPoint = edgeEnds.first; double lastInternalDistance = (midPWPoint - lastExtendStartPoint).Length(); Base::Vector3d lastExtendEndPoint = lastExtendStartPoint + lastExtendDir * (dMax - lastInternalDistance); TopoDS_Edge lastReplacementEdge = BRepBuilderAPI_MakeEdge(Base::convertTo(lastExtendStartPoint), Base::convertTo(lastExtendEndPoint)); Base::Vector3d pointOnArc = midPWPoint + awayDirection * dMax; Handle(Geom_TrimmedCurve) circleArc; try { GC_MakeArcOfCircle mkArc(Base::convertTo(lastExtendEndPoint), Base::convertTo(pointOnArc), Base::convertTo(firstExtendEndPoint)); circleArc = mkArc.Value(); if (!mkArc.IsDone()) { throw Base::RuntimeError("Complex section failed to create arc"); } } catch (...) { throw Base::RuntimeError("Complex section failed to create circular arc to close profile"); } TopoDS_Edge circleEdge = BRepBuilderAPI_MakeEdge(circleArc); // replace first and last edges and add circular arc std::vector oldProfileEdges = DU::shapeToVector(flatWire); std::vector newProfileEdges; newProfileEdges.emplace_back(firstReplacementEdge); if (oldProfileEdges.size() > 2) { newProfileEdges.insert(newProfileEdges.end(), oldProfileEdges.begin()+1, oldProfileEdges.end()-1); } newProfileEdges.emplace_back(lastReplacementEdge); newProfileEdges.emplace_back(circleEdge); BRepBuilderAPI_MakeWire mkWire; for (auto& edge : newProfileEdges) { mkWire.Add(edge); } return mkWire.Wire(); } bool DrawComplexSection::isFacePlanar(const TopoDS_Face& face) { BRepAdaptor_Surface adaptSurface(face); const GeomAdaptor_Surface& surf = adaptSurface.Surface(); Handle(Geom_Surface) hsurf = surf.Surface(); return GeomLib_IsPlanarSurface(hsurf).IsPlanar(); } //! compare 2 normal entries for sorting on segment index bool DrawComplexSection::normalLess(const std::pair& n1, const std::pair& n2) { return n1.first < n2.first; } // Python Drawing feature --------------------------------------------------------- namespace App { /// @cond DOXERR PROPERTY_SOURCE_TEMPLATE(TechDraw::DrawComplexSectionPython, TechDraw::DrawComplexSection) template<> const char* TechDraw::DrawComplexSectionPython::getViewProviderName() const { return "TechDrawGui::ViewProviderDrawingView"; } /// @endcond // explicit template instantiation template class TechDrawExport FeaturePythonT; }// namespace App