Download src/Mod/CAM/App/Area.cpp from AbdulElahGwaith/FreeCAD: direct link, hf CLI and curl.
- Browser
- Download file 139 kB
-
https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/CAM/App/Area.cpp
- Command line
-
hf download hf://AbdulElahGwaith/FreeCAD/src/Mod/CAM/App/Area.cpp
-
curl -L -o Area.cpp https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/CAM/App/Area.cpp
139 kB
| // SPDX-License-Identifier: LGPL-2.1-or-later | |
| /**************************************************************************** | |
| * Copyright (c) 2017 Zheng Lei (realthunder) <realthunder.dev@gmail.com> * | |
| * * | |
| * 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 * | |
| * * | |
| ****************************************************************************/ | |
| // From Boost 1.75 on the geometry component requires C++14 | |
| // FIXME: ISO C++11 requires at least one argument for the "..." in a variadic macro | |
| namespace bg = boost::geometry; | |
| namespace bgi = boost::geometry::index; | |
| using RParameters = bgi::linear<16>; | |
| BOOST_GEOMETRY_REGISTER_POINT_3D_GET_SET(gp_Pnt, double, bg::cs::cartesian, X, Y, Z, SetX, SetY, SetZ) | |
| FC_LOG_LEVEL_INIT("Path.Area", true, true) | |
| using namespace Path; | |
| CAreaParams::CAreaParams() | |
| : PARAM_INIT(PARAM_FNAME, AREA_PARAMS_CAREA) | |
| {} | |
| AreaParams::AreaParams() | |
| : PARAM_INIT(PARAM_FNAME, AREA_PARAMS_AREA) | |
| {} | |
| void AreaParams::dump(const char* msg) const | |
| { | |
| if (FC_LOG_INSTANCE.level() > FC_LOGLEVEL_TRACE) { | |
| std::ostringstream ss; | |
| ss << msg << '\n'; | |
| PARAM_FOREACH(AREA_PARAM_PRINT, AREA_PARAMS_AREA); | |
| FC_MSG(ss.str()); | |
| } | |
| } | |
| CAreaConfig::CAreaConfig(const CAreaParams& p, bool noFitArcs) | |
| { | |
| PARAM_FOREACH(AREA_CONF_SAVE_AND_APPLY, AREA_PARAMS_CAREA); | |
| // Arc fitting is lossy. We shall reduce the number of unnecessary fit | |
| if (noFitArcs) { | |
| CArea::set_fit_arcs(false); | |
| } | |
| } | |
| CAreaConfig::~CAreaConfig() | |
| { | |
| PARAM_FOREACH(AREA_CONF_RESTORE, AREA_PARAMS_CAREA); | |
| } | |
| ////////////////////////////////////////////////////////////////////////////// | |
| TYPESYSTEM_SOURCE(Path::Area, Base::BaseClass) | |
| bool Area::s_aborting; | |
| Area::Area(const AreaParams* params) | |
| : myParams(s_params) | |
| , myHaveFace(false) | |
| , myHaveSolid(false) | |
| , myShapeDone(false) | |
| , myProjecting(false) | |
| , mySkippedShapes(0) | |
| { | |
| if (params) { | |
| setParams(*params); | |
| } | |
| } | |
| Area::Area(const Area& other, bool deep_copy) | |
| : Base::BaseClass(other) | |
| , myShapes(other.myShapes) | |
| , myTrsf(other.myTrsf) | |
| , myParams(other.myParams) | |
| , myWorkPlane(other.myWorkPlane) | |
| , myHaveFace(other.myHaveFace) | |
| , myHaveSolid(other.myHaveSolid) | |
| , myShapeDone(false) | |
| , myProjecting(false) | |
| , mySkippedShapes(0) | |
| { | |
| if (!deep_copy || !other.isBuilt()) { | |
| return; | |
| } | |
| if (other.myArea) { | |
| myArea = std::make_unique<CArea>(*other.myArea); | |
| } | |
| myShapePlane = other.myShapePlane; | |
| myShape = other.myShape; | |
| myShapeDone = other.myShapeDone; | |
| mySections.reserve(other.mySections.size()); | |
| for (const shared_ptr<Area>& area : other.mySections) { | |
| mySections.push_back(make_shared<Area>(*area, true)); | |
| } | |
| } | |
| Area::~Area() | |
| { | |
| clean(); | |
| } | |
| void Area::setPlane(const TopoDS_Shape& shape) | |
| { | |
| clean(); | |
| if (shape.IsNull()) { | |
| myWorkPlane.Nullify(); | |
| return; | |
| } | |
| gp_Trsf trsf; | |
| TopoDS_Shape plane = findPlane(shape, trsf); | |
| if (plane.IsNull()) { | |
| throw Base::ValueError("shape is not planar"); | |
| } | |
| myWorkPlane = plane; | |
| myTrsf = trsf; | |
| } | |
| static bool getShapePlane(const TopoDS_Shape& shape, gp_Pln& pln) | |
| { | |
| if (shape.IsNull()) { | |
| return false; | |
| } | |
| if (shape.ShapeType() == TopAbs_FACE) { | |
| BRepAdaptor_Surface adapt(TopoDS::Face(shape)); | |
| if (adapt.GetType() != GeomAbs_Plane) { | |
| return false; | |
| } | |
| pln = adapt.Plane(); | |
| return true; | |
| } | |
| BRepLib_FindSurface finder(shape.Located(TopLoc_Location()), -1, Standard_True); | |
| if (!finder.Found()) { | |
| return false; | |
| } | |
| // TODO: It seemed that FindSurface disregard shape's | |
| // transformation SOMETIME, so we have to transformed the found | |
| // plane manually. Need to figure out WHY! | |
| // | |
| // ADD NOTE: Okay, one thing I find out that for face shape, this | |
| // FindSurface may produce plane at the wrong position, so use | |
| // adaptor to get the underlying surface plane directly (see | |
| // above). It remains to be seen that if FindSurface has the same | |
| // problem on wires | |
| pln = GeomAdaptor_Surface(finder.Surface()).Plane(); | |
| pln.Transform(shape.Location().Transformation()); | |
| return true; | |
| } | |
| bool Area::isCoplanar(const TopoDS_Shape& s1, const TopoDS_Shape& s2) | |
| { | |
| if (s1.IsNull() || s2.IsNull()) { | |
| return false; | |
| } | |
| if (s1.IsSame(s2)) { | |
| return true; | |
| } | |
| gp_Pln pln1, pln2; | |
| if (!getShapePlane(s1, pln1) || !getShapePlane(s2, pln2)) { | |
| return false; | |
| } | |
| return pln1.Position().IsCoplanar(pln2.Position(), Precision::Confusion(), Precision::Confusion()); | |
| } | |
| int Area::addShape( | |
| CArea& area, | |
| const TopoDS_Shape& shape, | |
| const gp_Trsf* trsf, | |
| double deflection, | |
| const TopoDS_Shape* plane, | |
| bool force_coplanar, | |
| CArea* areaOpen, | |
| bool to_edges, | |
| bool reorient | |
| ) | |
| { | |
| bool haveShape = false; | |
| int skipped = 0; | |
| for (TopExp_Explorer it(shape, TopAbs_FACE); it.More(); it.Next()) { | |
| haveShape = true; | |
| const TopoDS_Face& face = TopoDS::Face(it.Current()); | |
| if (plane && !isCoplanar(face, *plane)) { | |
| ++skipped; | |
| if (force_coplanar) { | |
| continue; | |
| } | |
| } | |
| for (TopExp_Explorer it(face, TopAbs_WIRE); it.More(); it.Next()) { | |
| addWire(area, TopoDS::Wire(it.Current()), trsf, deflection); | |
| } | |
| } | |
| if (haveShape) { | |
| return skipped; | |
| } | |
| CArea _area; | |
| CArea _areaOpen; | |
| for (TopExp_Explorer it(shape, TopAbs_WIRE); it.More(); it.Next()) { | |
| haveShape = true; | |
| const TopoDS_Wire& wire = TopoDS::Wire(it.Current()); | |
| if (plane && !isCoplanar(wire, *plane)) { | |
| ++skipped; | |
| if (force_coplanar) { | |
| continue; | |
| } | |
| } | |
| if (BRep_Tool::IsClosed(wire)) { | |
| addWire(_area, wire, trsf, deflection); | |
| } | |
| else if (to_edges) { | |
| for (TopExp_Explorer it(wire, TopAbs_EDGE); it.More(); it.Next()) { | |
| addWire( | |
| _areaOpen, | |
| BRepBuilderAPI_MakeWire(TopoDS::Edge(it.Current())).Wire(), | |
| trsf, | |
| deflection, | |
| true | |
| ); | |
| } | |
| } | |
| else { | |
| addWire(_areaOpen, wire, trsf, deflection); | |
| } | |
| } | |
| if (!haveShape) { | |
| for (TopExp_Explorer it(shape, TopAbs_EDGE); it.More(); it.Next()) { | |
| if (plane && !isCoplanar(it.Current(), *plane)) { | |
| ++skipped; | |
| if (force_coplanar) { | |
| continue; | |
| } | |
| } | |
| TopoDS_Wire wire = BRepBuilderAPI_MakeWire(TopoDS::Edge(it.Current())).Wire(); | |
| addWire(BRep_Tool::IsClosed(wire) ? _area : _areaOpen, wire, trsf, deflection); | |
| } | |
| } | |
| if (reorient) { | |
| _area.Reorder(); | |
| } | |
| area.m_curves.splice(area.m_curves.end(), _area.m_curves); | |
| if (areaOpen) { | |
| areaOpen->m_curves.splice(areaOpen->m_curves.end(), _areaOpen.m_curves); | |
| } | |
| else { | |
| area.m_curves.splice(area.m_curves.end(), _areaOpen.m_curves); | |
| } | |
| return skipped; | |
| } | |
| static std::vector<gp_Pnt> discretize(const TopoDS_Edge& edge, double deflection) | |
| { | |
| std::vector<gp_Pnt> ret; | |
| BRepAdaptor_Curve curve(edge); | |
| Standard_Real efirst, elast; | |
| efirst = curve.FirstParameter(); | |
| elast = curve.LastParameter(); | |
| bool reversed = (edge.Orientation() == TopAbs_REVERSED); | |
| // push the first point | |
| ret.push_back(curve.Value(reversed ? elast : efirst)); | |
| // NOTE: QuasiUniformDeflection has trouble with some B-Spline, see | |
| // https://forum.freecad.org/viewtopic.php?f=15&t=42628 | |
| // | |
| // GCPnts_QuasiUniformDeflection discretizer(curve, deflection, first, last); | |
| // | |
| GCPnts_UniformDeflection discretizer(curve, deflection, efirst, elast); | |
| if (!discretizer.IsDone()) { | |
| Standard_Failure::Raise("Curve discretization failed"); | |
| } | |
| if (discretizer.NbPoints() > 1) { | |
| int nbPoints = discretizer.NbPoints(); | |
| // strangely OCC discretizer points are one-based, not zero-based, why? | |
| if (reversed) { | |
| for (int i = nbPoints - 1; i >= 1; --i) { | |
| ret.push_back(discretizer.Value(i)); | |
| } | |
| } | |
| else { | |
| for (int i = 2; i <= nbPoints; i++) { | |
| ret.push_back(discretizer.Value(i)); | |
| } | |
| } | |
| } | |
| // push the last point | |
| ret.push_back(curve.Value(reversed ? efirst : elast)); | |
| return ret; | |
| } | |
| void Area::addWire(CArea& area, const TopoDS_Wire& wire, const gp_Trsf* trsf, double deflection, bool to_edges) | |
| { | |
| CCurve ccurve; | |
| BRepTools_WireExplorer xp(trsf ? TopoDS::Wire(wire.Moved(TopLoc_Location(*trsf))) : wire); | |
| if (!xp.More()) { | |
| AREA_TRACE("empty wire"); | |
| return; | |
| } | |
| gp_Pnt p = BRep_Tool::Pnt(xp.CurrentVertex()); | |
| ccurve.append(CVertex(Point(p.X(), p.Y()))); | |
| for (; xp.More(); xp.Next()) { | |
| const TopoDS_Edge& edge = TopoDS::Edge(xp.Current()); | |
| BRepAdaptor_Curve curve(edge); | |
| bool reversed = (xp.Current().Orientation() == TopAbs_REVERSED); | |
| p = curve.Value(reversed ? curve.FirstParameter() : curve.LastParameter()); | |
| switch (curve.GetType()) { | |
| case GeomAbs_Line: { | |
| ccurve.append(CVertex(Point(p.X(), p.Y()))); | |
| if (to_edges) { | |
| area.append(ccurve); | |
| ccurve.m_vertices.pop_front(); | |
| } | |
| break; | |
| } | |
| case GeomAbs_Circle: { | |
| double first = curve.FirstParameter(); | |
| double last = curve.LastParameter(); | |
| gp_Circ circle = curve.Circle(); | |
| gp_Dir dir = circle.Axis().Direction(); | |
| gp_Pnt center = circle.Location(); | |
| int type = dir.Z() < 0 ? -1 : 1; | |
| if (reversed) { | |
| type = -type; | |
| } | |
| if (fabs(first - last) > std::numbers::pi) { | |
| // Split arc(circle) larger than half circle. Because gcode | |
| // can't handle full circle? | |
| gp_Pnt mid = curve.Value((last - first) * 0.5 + first); | |
| ccurve.append( | |
| CVertex(type, Point(mid.X(), mid.Y()), Point(center.X(), center.Y())) | |
| ); | |
| } | |
| ccurve.append(CVertex(type, Point(p.X(), p.Y()), Point(center.X(), center.Y()))); | |
| if (to_edges) { | |
| ccurve.UnFitArcs(); | |
| CCurve c; | |
| c.append(ccurve.m_vertices.front()); | |
| auto it = ccurve.m_vertices.begin(); | |
| for (++it; it != ccurve.m_vertices.end(); ++it) { | |
| c.append(*it); | |
| area.append(c); | |
| c.m_vertices.pop_front(); | |
| } | |
| ccurve.m_vertices.clear(); | |
| ccurve.append(c.m_vertices.front()); | |
| } | |
| break; | |
| } | |
| default: { | |
| // Discretize all other type of curves | |
| const auto& pts = discretize(edge, deflection); | |
| for (size_t i = 1; i < pts.size(); ++i) { | |
| auto& pt = pts[i]; | |
| ccurve.append(CVertex(Point(pt.X(), pt.Y()))); | |
| if (to_edges) { | |
| area.append(ccurve); | |
| ccurve.m_vertices.pop_front(); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| if (!to_edges) { | |
| if (BRep_Tool::IsClosed(wire) && !ccurve.IsClosed()) { | |
| AREA_WARN("ccurve not closed"); | |
| ccurve.append(ccurve.m_vertices.front()); | |
| } | |
| area.move(std::move(ccurve)); | |
| } | |
| } | |
| void Area::clean(bool deleteShapes) | |
| { | |
| myShapeDone = false; | |
| mySections.clear(); | |
| myShape.Nullify(); | |
| myArea.reset(); | |
| myAreaOpen.reset(); | |
| myShapePlane.Nullify(); | |
| if (deleteShapes) { | |
| myShapes.clear(); | |
| myHaveFace = false; | |
| myHaveSolid = false; | |
| } | |
| } | |
| static inline ClipperLib::ClipType toClipperOp(short op) | |
| { | |
| switch (op) { | |
| case Area::OperationUnion: | |
| return ClipperLib::ctUnion; | |
| break; | |
| case Area::OperationDifference: | |
| return ClipperLib::ctDifference; | |
| break; | |
| case Area::OperationIntersection: | |
| return ClipperLib::ctIntersection; | |
| break; | |
| case Area::OperationXor: | |
| return ClipperLib::ctXor; | |
| break; | |
| default: | |
| throw Base::ValueError("invalid Operation"); | |
| } | |
| } | |
| void Area::add(const TopoDS_Shape& shape, short op) | |
| { | |
| if (shape.IsNull()) { | |
| throw Base::ValueError("null shape"); | |
| } | |
| if (op != OperationCompound) { | |
| toClipperOp(op); | |
| } | |
| bool haveSolid = TopExp_Explorer(shape, TopAbs_SOLID).More(); | |
| // TODO: shall we support Shells? | |
| if ((!haveSolid && myHaveSolid) || (haveSolid && !myHaveSolid && !myShapes.empty())) { | |
| throw Base::ValueError("mixing solid and planar shapes is not allowed"); | |
| } | |
| myHaveSolid = haveSolid; | |
| clean(); | |
| if (op != OperationCompound && myShapes.empty()) { | |
| op = OperationUnion; | |
| } | |
| myShapes.emplace_back(op, shape); | |
| } | |
| class ClearedAreaSegmentVisitor: public PathSegmentVisitor | |
| { | |
| private: | |
| CArea pathSegments; | |
| CArea holes; | |
| double maxZ; | |
| double radius; | |
| Base::BoundBox3d bbox; | |
| void point(const Base::Vector3d& p) | |
| { | |
| if (p.z <= maxZ) { | |
| if (bbox.MinX <= p.x && p.x <= bbox.MaxX && bbox.MinY <= p.y && p.y <= bbox.MaxY) { | |
| CCurve curve; | |
| curve.append(CVertex {{p.x + radius, p.y}}); | |
| curve.append(CVertex {1, {p.x - radius, p.y}, {p.x, p.y}}); | |
| curve.append(CVertex {1, {p.x + radius, p.y}, {p.x, p.y}}); | |
| holes.append(curve); | |
| } | |
| } | |
| } | |
| void line(const Base::Vector3d& last, const Base::Vector3d& next) | |
| { | |
| if (last.z <= maxZ && next.z <= maxZ) { | |
| Base::BoundBox2d segBox = {}; | |
| segBox.Add({last.x, last.y}); | |
| segBox.Add({next.x, next.y}); | |
| if (bbox.Intersect(segBox)) { | |
| CCurve curve; | |
| curve.append(CVertex {{last.x, last.y}}); | |
| curve.append(CVertex {{next.x, next.y}}); | |
| pathSegments.append(curve); | |
| } | |
| } | |
| } | |
| public: | |
| ClearedAreaSegmentVisitor(double maxZ, double radius, Base::BoundBox3d bbox) | |
| : maxZ(maxZ) | |
| , radius(radius) | |
| , bbox(bbox) | |
| { | |
| bbox.Enlarge(radius); | |
| } | |
| CArea getClearedArea() | |
| { | |
| CArea result {pathSegments}; | |
| result.Thicken(radius); | |
| result.Union(holes); | |
| return result; | |
| } | |
| void g0( | |
| int id, | |
| const Base::Vector3d& last, | |
| const Base::Vector3d& next, | |
| const std::deque<Base::Vector3d>& pts | |
| ) override | |
| { | |
| (void)id; | |
| (void)pts; | |
| line(last, next); | |
| } | |
| void g1( | |
| int id, | |
| const Base::Vector3d& last, | |
| const Base::Vector3d& next, | |
| const std::deque<Base::Vector3d>& pts | |
| ) override | |
| { | |
| (void)id; | |
| (void)pts; | |
| line(last, next); | |
| } | |
| void g23( | |
| int id, | |
| const Base::Vector3d& last, | |
| const Base::Vector3d& next, | |
| const std::deque<Base::Vector3d>& pts, | |
| const Base::Vector3d& center | |
| ) override | |
| { | |
| (void)id; | |
| (void)center; | |
| // Compute cw vs ccw | |
| const Base::Vector3d vdirect = next - last; | |
| const Base::Vector3d vstep = pts[0] - last; | |
| const bool ccw = vstep.x * vdirect.y - vstep.y * vdirect.x > 0; | |
| // Add an arc | |
| CCurve curve; | |
| curve.append(CVertex {{last.x, last.y}}); | |
| curve.append(CVertex {ccw ? 1 : -1, {next.x, next.y}, {center.x, center.y}}); | |
| pathSegments.append(curve); | |
| } | |
| void g8x( | |
| int id, | |
| const Base::Vector3d& last, | |
| const Base::Vector3d& next, | |
| const std::deque<Base::Vector3d>& pts, | |
| const std::deque<Base::Vector3d>& plist, | |
| const std::deque<Base::Vector3d>& qlist | |
| ) override | |
| { | |
| // (peck) drilling | |
| (void)id; | |
| (void)qlist; // pecks are always within the bounds of plist | |
| point(last); | |
| for (const auto p : pts) { | |
| point(p); | |
| } | |
| for (const auto p : plist) { | |
| point(p); | |
| } | |
| point(next); | |
| } | |
| void g38(int id, const Base::Vector3d& last, const Base::Vector3d& next) override | |
| { | |
| // probe operation; clears nothing | |
| (void)id; | |
| (void)last; | |
| (void)next; | |
| } | |
| }; | |
| std::shared_ptr<Area> Area::getClearedArea( | |
| const Toolpath* path, | |
| double diameter, | |
| double zmax, | |
| Base::BoundBox3d bbox | |
| ) | |
| { | |
| build(); | |
| // Precision losses in arc/segment conversions (multiples of Accuracy): | |
| // 2.3 in generation of gcode (see documentation in the implementation of CCurve::CheckForArc | |
| // (libarea/Curve.cpp) 1 in gcode arc to segment 1 in Thicken() cleared area 2 in getRestArea | |
| // target area offset in and back out Oversize cleared areas by buffer to smooth out imprecision | |
| // in arc/segment conversion. getRestArea() will compensate for this | |
| AreaParams params = myParams; | |
| params.Accuracy = myParams.Accuracy * .7 / 4; // 2.3 already encoded in gcode; 4 * .7/4 = 3 total | |
| params.SubjectFill = ClipperLib::pftNonZero; | |
| params.ClipFill = ClipperLib::pftNonZero; | |
| const double buffer = myParams.Accuracy * 3; | |
| // Do not fit arcs after these offsets; it introduces unnecessary approximation error, and all | |
| // off those arcs will be converted back to segments again for clipper differencing in | |
| // getRestArea anyway | |
| CAreaConfig conf(params, /*no_fit_arcs*/ true); | |
| ClearedAreaSegmentVisitor visitor(zmax, diameter / 2 + buffer, bbox); | |
| PathSegmentWalker walker(*path); | |
| walker.walk(visitor, Base::Vector3d(0, 0, zmax + 1)); | |
| std::shared_ptr<Area> clearedArea = make_shared<Area>(¶ms); | |
| clearedArea->myTrsf = {}; | |
| const CArea ca = visitor.getClearedArea(); | |
| if (ca.m_curves.size() > 0) { | |
| TopoDS_Shape clearedAreaShape = Area::toShape(ca, false); | |
| clearedArea->add(clearedAreaShape, OperationCompound); | |
| clearedArea->build(); | |
| } | |
| else { | |
| clearedArea->myArea = std::make_unique<CArea>(); | |
| clearedArea->myAreaOpen = std::make_unique<CArea>(); | |
| } | |
| return clearedArea; | |
| } | |
| std::shared_ptr<Area> Area::getRestArea(std::vector<std::shared_ptr<Area>> clearedAreas, double diameter) | |
| { | |
| build(); | |
| PARAM_ENUM_CONVERT(AREA_MY, PARAM_FNAME, PARAM_ENUM_EXCEPT, AREA_PARAMS_OFFSET_CONF); | |
| PARAM_ENUM_CONVERT(AREA_MY, PARAM_FNAME, PARAM_ENUM_EXCEPT, AREA_PARAMS_CLIPPER_FILL); | |
| // Precision losses in arc/segment conversions (multiples of Accuracy): | |
| // 2.3 in generation of gcode (see documentation in the implementation of CCurve::CheckForArc | |
| // (libarea/Curve.cpp) 1 in gcode arc to segment 1 in Thicken() cleared area 2 in getRestArea | |
| // target area offset in and back out Cleared area representations are oversized by buffer to | |
| // smooth out imprecision in arc/segment conversion. getRestArea() will compensate for this | |
| AreaParams params = myParams; | |
| params.Accuracy = myParams.Accuracy * .7 / 4; // 2.3 already encoded in gcode; 4 * .7/4 = 3 total | |
| const double buffer = myParams.Accuracy * 3; | |
| const double roundPrecision = params.Accuracy; | |
| // transform all clearedAreas into our workplane | |
| Area clearedAreasInPlane(¶ms); | |
| clearedAreasInPlane.myArea.reset(new CArea()); | |
| for (std::shared_ptr<Area> clearedArea : clearedAreas) { | |
| gp_Trsf trsf = clearedArea->myTrsf; | |
| trsf.Invert(); | |
| trsf.SetTranslationPart( | |
| gp_Vec { | |
| trsf.TranslationPart().X(), | |
| trsf.TranslationPart().Y(), | |
| -myTrsf.TranslationPart().Z() | |
| } | |
| ); // discard z-height of cleared workplane, set to myWorkPlane's height | |
| TopoDS_Shape clearedShape = Area::toShape(*clearedArea->myArea, false, &trsf); | |
| Area::addShape( | |
| *(clearedAreasInPlane.myArea), | |
| clearedShape, | |
| &myTrsf, | |
| .01 /*default value*/, | |
| &myWorkPlane | |
| ); | |
| } | |
| CArea clearable(*myArea); | |
| clearable.OffsetWithClipper(-diameter / 2, JoinType, EndType, params.MiterLimit, roundPrecision); | |
| clearable.OffsetWithClipper(diameter / 2, JoinType, EndType, params.MiterLimit, roundPrecision); | |
| // remaining = clearable - prevCleared | |
| CArea remaining(clearable); | |
| remaining.Clip( | |
| toClipperOp(Area::OperationDifference), | |
| &*(clearedAreasInPlane.myArea), | |
| SubjectFill, | |
| ClipFill | |
| ); | |
| // rest = intersect(clearable, offset(remaining, dTool)) | |
| // add buffer to dTool to compensate for oversizing in getClearedArea | |
| CArea restCArea(remaining); | |
| restCArea.OffsetWithClipper(diameter + buffer, JoinType, EndType, params.MiterLimit, roundPrecision); | |
| restCArea.Clip(toClipperOp(Area::OperationIntersection), &clearable, SubjectFill, ClipFill); | |
| if (restCArea.m_curves.size() == 0) { | |
| return {}; | |
| } | |
| std::shared_ptr<Area> restArea = make_shared<Area>(¶ms); | |
| gp_Trsf trsf(myTrsf.Inverted()); | |
| TopoDS_Shape restShape = Area::toShape(restCArea, false, &trsf); | |
| restArea->add(restShape, OperationCompound); | |
| return restArea; | |
| } | |
| TopoDS_Shape Area::toTopoShape() | |
| { | |
| build(); | |
| gp_Trsf trsf = myTrsf.Inverted(); | |
| return toShape(*myArea, false, &trsf); | |
| } | |
| void Area::setParams(const AreaParams& params) | |
| { | |
| // Validate all enum type of parameters | |
| PARAM_ENUM_CHECK(AREA_SRC, PARAM_ENUM_EXCEPT, AREA_PARAMS_CONF); | |
| if (params != myParams) { | |
| clean(); | |
| myParams = params; | |
| } | |
| } | |
| void Area::addToBuild(CArea& area, const TopoDS_Shape& shape) | |
| { | |
| if (myParams.Fill == FillAuto && !myHaveFace) { | |
| TopExp_Explorer it(shape, TopAbs_FACE); | |
| myHaveFace = it.More(); | |
| } | |
| TopoDS_Shape plane = getPlane(); | |
| CArea areaOpen; | |
| mySkippedShapes += addShape( | |
| area, | |
| shape, | |
| &myTrsf, | |
| myParams.Deflection, | |
| myParams.Coplanar == CoplanarNone ? nullptr : &plane, | |
| myHaveSolid || myParams.Coplanar == CoplanarForce, | |
| &areaOpen, | |
| myParams.OpenMode == OpenModeEdges, | |
| myParams.Reorient | |
| ); | |
| if (myProjecting) { | |
| // when projecting, we force all wires to be CCW in order to remove | |
| // inner holes | |
| for (auto& c : area.m_curves) { | |
| if (c.IsClosed() && c.IsClockwise()) { | |
| c.Reverse(); | |
| } | |
| } | |
| } | |
| if (!areaOpen.m_curves.empty()) { | |
| if (&area == myArea.get() || myParams.OpenMode == OpenModeNone) { | |
| myAreaOpen->m_curves.splice(myAreaOpen->m_curves.end(), areaOpen.m_curves); | |
| } | |
| else { | |
| AREA_WARN("open wires discarded in clipping shapes"); | |
| } | |
| } | |
| } | |
| static inline void getEndPoints(const TopoDS_Edge& e, gp_Pnt& p1, gp_Pnt& p2) | |
| { | |
| p1 = BRep_Tool::Pnt(TopExp::FirstVertex(e)); | |
| p2 = BRep_Tool::Pnt(TopExp::LastVertex(e)); | |
| } | |
| static inline void getEndPoints(const TopoDS_Wire& wire, gp_Pnt& p1, gp_Pnt& p2) | |
| { | |
| BRepTools_WireExplorer xp(wire); | |
| p1 = BRep_Tool::Pnt(TopoDS::Vertex(xp.CurrentVertex())); | |
| for (; xp.More(); xp.Next()) | |
| ; | |
| p2 = BRep_Tool::Pnt(TopoDS::Vertex(xp.CurrentVertex())); | |
| } | |
| // Toponaming integration note: there's a new class called WireJoiner in Mod/Part/App/ that has been | |
| // imported from RT's fork. It's an improved version of the following struct, therefore | |
| // probably at some point this struct should be replaced with the new imported class. | |
| // See https://github.com/realthunder/FreeCAD/blob/LinkStable/src/Mod/Part/App/WireJoiner.h for the | |
| // original implementation of the class and https://github.com/FreeCAD/FreeCAD/pull/12535 for the | |
| // import conversation. | |
| struct WireJoiner | |
| { | |
| using Box = bg::model::box<gp_Pnt>; | |
| static bool getBBox(const TopoDS_Edge& e, Box& box) | |
| { | |
| Bnd_Box bound; | |
| BRepBndLib::Add(e, bound); | |
| bound.SetGap(0.1); | |
| if (bound.IsVoid()) { | |
| if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { | |
| AREA_WARN("failed to get bound of edge"); | |
| } | |
| return false; | |
| } | |
| Standard_Real xMin, yMin, zMin, xMax, yMax, zMax; | |
| bound.Get(xMin, yMin, zMin, xMax, yMax, zMax); | |
| box = Box(gp_Pnt(xMin, yMin, zMin), gp_Pnt(xMax, yMax, zMax)); | |
| return true; | |
| } | |
| struct EdgeInfo | |
| { | |
| TopoDS_Edge edge; | |
| gp_Pnt p1; | |
| gp_Pnt p2; | |
| Box box; | |
| int iteration; | |
| int iStart[2]; // adjacent list index start for p1 and p2 | |
| int iEnd[2]; // adjacent list index end | |
| bool used; | |
| bool hasBox; | |
| EdgeInfo(const TopoDS_Edge& e, bool bbox) | |
| : edge(e) | |
| , hasBox(false) | |
| { | |
| getEndPoints(e, p1, p2); | |
| if (bbox) { | |
| hasBox = getBBox(e, box); | |
| } | |
| reset(); | |
| } | |
| EdgeInfo(const TopoDS_Edge& e, const gp_Pnt& pt1, const gp_Pnt& pt2, bool bbox) | |
| : edge(e) | |
| , p1(pt1) | |
| , p2(pt2) | |
| , hasBox(false) | |
| { | |
| if (bbox) { | |
| hasBox = getBBox(e, box); | |
| } | |
| reset(); | |
| } | |
| void reset() | |
| { | |
| iteration = 0; | |
| used = false; | |
| iStart[0] = iStart[1] = iEnd[0] = iEnd[1] = -1; | |
| } | |
| }; | |
| using Edges = std::list<EdgeInfo>; | |
| Edges edges; | |
| struct VertexInfo | |
| { | |
| Edges::iterator it; | |
| bool start; | |
| VertexInfo(Edges::iterator it, bool start) | |
| : it(it) | |
| , start(start) | |
| {} | |
| bool operator==(const VertexInfo& other) const | |
| { | |
| return it == other.it && start == other.start; | |
| } | |
| const gp_Pnt& pt() const | |
| { | |
| return start ? it->p1 : it->p2; | |
| } | |
| const gp_Pnt& ptOther() const | |
| { | |
| return start ? it->p2 : it->p1; | |
| } | |
| }; | |
| struct PntGetter | |
| { | |
| using result_type = const gp_Pnt&; | |
| result_type operator()(const VertexInfo& v) const | |
| { | |
| return v.pt(); | |
| } | |
| }; | |
| bgi::rtree<VertexInfo, RParameters, PntGetter> vmap; | |
| struct BoxGetter | |
| { | |
| using result_type = const Box&; | |
| result_type operator()(Edges::iterator it) const | |
| { | |
| return it->box; | |
| } | |
| }; | |
| bgi::rtree<Edges::iterator, RParameters, BoxGetter> boxMap; | |
| BRep_Builder builder; | |
| TopoDS_Compound comp; | |
| WireJoiner() | |
| { | |
| builder.MakeCompound(comp); | |
| } | |
| void remove(Edges::iterator it) | |
| { | |
| if (it->hasBox) { | |
| boxMap.remove(it); | |
| } | |
| vmap.remove(VertexInfo(it, true)); | |
| vmap.remove(VertexInfo(it, false)); | |
| edges.erase(it); | |
| } | |
| void add(Edges::iterator it) | |
| { | |
| vmap.insert(VertexInfo(it, true)); | |
| vmap.insert(VertexInfo(it, false)); | |
| if (it->hasBox) { | |
| boxMap.insert(it); | |
| } | |
| } | |
| void add(const TopoDS_Edge& e, bool bbox = false) | |
| { | |
| gp_Pnt p1, p2; | |
| getEndPoints(e, p1, p2); | |
| edges.emplace_front(e, p1, p2, bbox); | |
| add(edges.begin()); | |
| } | |
| void add(const TopoDS_Shape& shape, bool bbox = false) | |
| { | |
| for (TopExp_Explorer xp(shape, TopAbs_EDGE); xp.More(); xp.Next()) { | |
| add(TopoDS::Edge(xp.Current()), bbox); | |
| } | |
| } | |
| // This algorithm tries to join connected edges into wires | |
| // | |
| // tol*tol>Precision::SquareConfusion() can be used to join points that are | |
| // close but do not coincide with a line segment. The close points may be | |
| // the results of rounding issue. | |
| // | |
| void join(double tol) | |
| { | |
| tol = tol * tol; | |
| while (!edges.empty()) { | |
| auto it = edges.begin(); | |
| BRepBuilderAPI_MakeWire mkWire; | |
| mkWire.Add(it->edge); | |
| gp_Pnt pstart(it->p1), pend(it->p2); | |
| remove(it); | |
| bool done = false; | |
| for (int idx = 0; !done && idx < 2; ++idx) { | |
| while (!edges.empty()) { | |
| std::vector<VertexInfo> ret; | |
| ret.reserve(1); | |
| const gp_Pnt& pt = idx == 0 ? pstart : pend; | |
| vmap.query(bgi::nearest(pt, 1), std::back_inserter(ret)); | |
| assert(ret.size() == 1); | |
| double d = ret[0].pt().SquareDistance(pt); | |
| if (d > tol) { | |
| break; | |
| } | |
| const auto& info = *ret[0].it; | |
| bool start = ret[0].start; | |
| if (d > Precision::SquareConfusion()) { | |
| // insert a filling edge to solve the tolerance problem | |
| const gp_Pnt& pt = ret[idx].pt(); | |
| if (idx) { | |
| mkWire.Add(BRepBuilderAPI_MakeEdge(pend, pt).Edge()); | |
| } | |
| else { | |
| mkWire.Add(BRepBuilderAPI_MakeEdge(pt, pstart).Edge()); | |
| } | |
| } | |
| if (idx == 1 && start) { | |
| pend = info.p2; | |
| mkWire.Add(info.edge); | |
| } | |
| else if (idx == 0 && !start) { | |
| pstart = info.p1; | |
| mkWire.Add(info.edge); | |
| } | |
| else if (idx == 0 && start) { | |
| pstart = info.p2; | |
| mkWire.Add(TopoDS::Edge(info.edge.Reversed())); | |
| } | |
| else { | |
| pend = info.p1; | |
| mkWire.Add(TopoDS::Edge(info.edge.Reversed())); | |
| } | |
| remove(ret[0].it); | |
| if (pstart.SquareDistance(pend) <= Precision::SquareConfusion()) { | |
| done = true; | |
| break; | |
| } | |
| } | |
| } | |
| builder.Add(comp, mkWire.Wire()); | |
| } | |
| } | |
| // split any edges that are intersected by other edge's end point in the middle | |
| void splitEdges() | |
| { | |
| for (auto it = edges.begin(); it != edges.end();) { | |
| const auto& info = *it; | |
| if (!info.hasBox) { | |
| ++it; | |
| continue; | |
| } | |
| gp_Pnt pstart(info.p1), pend(info.p2); | |
| gp_Pnt pt; | |
| bool intersects = false; | |
| for (auto vit = boxMap.qbegin(bgi::intersects(info.box)); | |
| !intersects && vit != boxMap.qend(); | |
| ++vit) { | |
| const auto& other = *(*vit); | |
| if (info.edge.IsSame(other.edge)) { | |
| continue; | |
| } | |
| for (int i = 0; i < 2; ++i) { | |
| const gp_Pnt& p = i ? other.p1 : other.p2; | |
| if (pstart.SquareDistance(p) <= Precision::SquareConfusion() | |
| || pend.SquareDistance(p) <= Precision::SquareConfusion()) { | |
| continue; | |
| } | |
| BRepExtrema_DistShapeShape extss(BRepBuilderAPI_MakeVertex(p), info.edge); | |
| if (extss.IsDone() && extss.NbSolution()) { | |
| const gp_Pnt& pp = extss.PointOnShape2(1); | |
| if (pp.SquareDistance(p) <= Precision::SquareConfusion()) { | |
| pt = pp; | |
| intersects = true; | |
| break; | |
| } | |
| } | |
| else if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { | |
| AREA_WARN("BRepExtrema_DistShapeShape failed"); | |
| } | |
| } | |
| } | |
| if (!intersects) { | |
| ++it; | |
| continue; | |
| } | |
| Standard_Real first, last; | |
| Handle(Geom_Curve) curve = BRep_Tool::Curve(it->edge, first, last); | |
| bool reversed = pstart.SquareDistance(curve->Value(last)) <= Precision::SquareConfusion(); | |
| BRepBuilderAPI_MakeEdge mkEdge1, mkEdge2; | |
| if (reversed) { | |
| mkEdge1.Init(curve, pt, pstart); | |
| mkEdge2.Init(curve, pend, pt); | |
| } | |
| else { | |
| mkEdge1.Init(curve, pstart, pt); | |
| mkEdge2.Init(curve, pt, pend); | |
| } | |
| if (!mkEdge1.IsDone() || !mkEdge2.IsDone()) { | |
| if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { | |
| AREA_WARN( | |
| (reversed ? "reversed " : "") | |
| << "edge split failed " << AREA_XYZ(pstart) << ", " << AREA_XYZ(pt) << ", " | |
| << AREA_XYZ(pend) << ", " << ", err: " << mkEdge1.Error() << ", " | |
| << mkEdge2.Error() | |
| ); | |
| } | |
| ++it; | |
| continue; | |
| } | |
| Edges::iterator itNext = it; | |
| ++itNext; | |
| remove(it); | |
| add(it = edges.emplace(itNext, mkEdge1.Edge(), true)); | |
| add(edges.emplace(itNext, mkEdge2.Edge(), true)); | |
| } | |
| } | |
| // This algorithm tries to find a set of closed wires that includes as many | |
| // edges (added by calling add() ) as possible. One edge may be included | |
| // in more than one closed wires if it connects to more than one edges. | |
| int findClosedWires(double tol = Precision::Confusion()) | |
| { | |
| // Note on tolerance: It seems OCC projector sometimes mess up the | |
| // tolerance of edges which are supposed to be connected. So use a | |
| // lesser precision below, and call makeCleanWire to fix the tolerance | |
| std::vector<VertexInfo> adjacentList; | |
| std::set<EdgeInfo*> edgesToVisit; | |
| int count = 0; | |
| int skips = 0; | |
| for (auto& info : edges) { | |
| info.reset(); | |
| } | |
| FC_TIME_INIT(t); | |
| int rcount = 0; | |
| for (auto& info : edges) { | |
| if (BRep_Tool::IsClosed(info.edge)) { | |
| auto wire = BRepBuilderAPI_MakeWire(info.edge).Wire(); | |
| Area::showShape(wire, "closed"); | |
| builder.Add(comp, wire); | |
| ++count; | |
| continue; | |
| } | |
| gp_Pnt pt[2]; | |
| pt[0] = info.p1; | |
| pt[1] = info.p2; | |
| for (int i = 0; i < 2; ++i) { | |
| if (info.iStart[i] >= 0) { | |
| continue; | |
| } | |
| info.iEnd[i] = info.iStart[i] = (int)adjacentList.size(); | |
| // populate adjacent list | |
| constexpr int intMax = std::numeric_limits<int>::max(); | |
| for (auto vit = vmap.qbegin(bgi::nearest(pt[i], intMax)); vit != vmap.qend(); ++vit) { | |
| ++rcount; | |
| if (vit->pt().SquareDistance(pt[i]) > tol) { | |
| break; | |
| } | |
| auto& vinfo = *vit; | |
| // yes, we push ourself too, because other edges require | |
| // this info in the adjacent list. We'll do filtering later. | |
| adjacentList.push_back(vinfo); | |
| ++info.iEnd[i]; | |
| } | |
| // copy the adjacent indices to all connected edges | |
| for (int j = info.iStart[i]; j < info.iEnd[i]; ++j) { | |
| auto& other = adjacentList[j]; | |
| auto& otherInfo = *other.it; | |
| if (&otherInfo != &info) { | |
| int k = other.start ? 0 : 1; | |
| otherInfo.iStart[k] = info.iStart[i]; | |
| otherInfo.iEnd[k] = info.iEnd[i]; | |
| } | |
| } | |
| } | |
| if (info.iStart[0] != info.iEnd[0] && info.iStart[1] != info.iEnd[1]) { | |
| // add the edge only if it is connected to some other edges on | |
| // both ends | |
| edgesToVisit.insert(&info); | |
| } | |
| else { | |
| ++skips; | |
| } | |
| } | |
| FC_TIME_LOG(t, "rtree::nearest (" << rcount << ')'); | |
| struct StackInfo | |
| { | |
| size_t iStart; | |
| size_t iEnd; | |
| size_t iCurrent; | |
| explicit StackInfo(size_t idx) | |
| : iStart(idx) | |
| , iEnd(idx) | |
| , iCurrent(idx) | |
| {} | |
| }; | |
| std::vector<StackInfo> stack; | |
| std::vector<VertexInfo> vertexStack; | |
| for (int iteration = 1; !edgesToVisit.empty(); ++iteration) { | |
| EdgeInfo* currentInfo = *edgesToVisit.begin(); | |
| int currentIdx = 1; // used to tell whether search connection from the start(0) or end(1) | |
| TopoDS_Edge& e = currentInfo->edge; | |
| edgesToVisit.erase(edgesToVisit.begin()); | |
| gp_Pnt pstart = currentInfo->p1; | |
| gp_Pnt pend = currentInfo->p2; | |
| currentInfo->used = true; | |
| currentInfo->iteration = iteration; | |
| stack.clear(); | |
| vertexStack.clear(); | |
| // pstart and pend is the start and end vertex of the current wire | |
| while (true) { | |
| // push a new stack entry | |
| stack.emplace_back(vertexStack.size()); | |
| auto& r = stack.back(); | |
| // this loop is to find all edges connected to pend, and save them into stack.back() | |
| for (int i = currentInfo->iStart[currentIdx]; i < currentInfo->iEnd[currentIdx]; ++i) { | |
| auto& info = *adjacentList[i].it; | |
| if (info.iteration != iteration) { | |
| info.iteration = iteration; | |
| vertexStack.push_back(adjacentList[i]); | |
| ++r.iEnd; | |
| } | |
| } | |
| while (true) { | |
| auto& r = stack.back(); | |
| if (r.iCurrent < r.iEnd) { | |
| // now pick one edge from stack.back(), connect it to | |
| // pend, then extend pend | |
| auto& vinfo = vertexStack[r.iCurrent]; | |
| pend = vinfo.ptOther(); | |
| // update current edge info | |
| currentInfo = &(*vinfo.it); | |
| currentIdx = vinfo.start ? 1 : 0; | |
| break; | |
| } | |
| // if no edge left in stack.back(), then pop it, and try again | |
| vertexStack.erase(vertexStack.begin() + r.iStart, vertexStack.end()); | |
| stack.pop_back(); | |
| if (stack.empty()) { | |
| break; | |
| } | |
| ++stack.back().iCurrent; | |
| } | |
| if (stack.empty()) { | |
| // If stack is empty, it means this wire is open. Try a new | |
| // starting edge | |
| ++skips; | |
| break; | |
| } | |
| if (pstart.SquareDistance(pend) > tol) { | |
| // if the wire is not closed yet, continue search for the | |
| // next connected edge | |
| continue; | |
| } | |
| Handle(ShapeExtend_WireData) wireData = new ShapeExtend_WireData(); | |
| wireData->Add(e); | |
| for (auto& r : stack) { | |
| const auto& v = vertexStack[r.iCurrent]; | |
| auto& info = *v.it; | |
| if (v.start) { | |
| wireData->Add(info.edge); | |
| } | |
| else { | |
| wireData->Add(TopoDS::Edge(info.edge.Reversed())); | |
| } | |
| } | |
| // TechDraw even uses 0.1 as tolerance. Really? Why? | |
| TopoDS_Wire wire = makeCleanWire(wireData, 0.01); | |
| if (!BRep_Tool::IsClosed(wire)) { | |
| FC_WARN("failed to close some projection wire"); | |
| Area::showShape(wire, "failed"); | |
| ++skips; | |
| } | |
| else { | |
| for (auto& r : stack) { | |
| const auto& v = vertexStack[r.iCurrent]; | |
| auto& info = *v.it; | |
| if (!info.used) { | |
| info.used = true; | |
| edgesToVisit.erase(&info); | |
| } | |
| } | |
| Area::showShape(wire, "joined"); | |
| builder.Add(comp, wire); | |
| ++count; | |
| } | |
| break; | |
| } | |
| } | |
| FC_TIME_LOG(t, "found " << count << " closed wires, skipped " << skips << "edges. "); | |
| return skips; | |
| } | |
| //! make a clean wire with sorted, oriented, connected, etc edges | |
| // Copied from TechDraw::EdgeWalker | |
| static TopoDS_Wire makeCleanWire(Handle(ShapeExtend_WireData) wireData, double tol) | |
| { | |
| TopoDS_Wire result; | |
| BRepBuilderAPI_MakeWire mkWire; | |
| ShapeFix_ShapeTolerance sTol; | |
| Handle(ShapeFix_Wire) fixer = new ShapeFix_Wire; | |
| fixer->Load(wireData); | |
| fixer->Perform(); | |
| fixer->FixReorder(); | |
| fixer->SetMaxTolerance(tol); | |
| fixer->ClosedWireMode() = Standard_True; | |
| fixer->FixConnected(Precision::Confusion()); | |
| fixer->FixClosed(Precision::Confusion()); | |
| for (int i = 1; i <= wireData->NbEdges(); i++) { | |
| TopoDS_Edge edge = fixer->WireData()->Edge(i); | |
| sTol.SetTolerance(edge, tol, TopAbs_VERTEX); | |
| mkWire.Add(edge); | |
| } | |
| result = mkWire.Wire(); | |
| return result; | |
| } | |
| }; | |
| void Area::explode(const TopoDS_Shape& shape) | |
| { | |
| const TopoDS_Shape& plane = getPlane(); | |
| bool haveShape = false; | |
| for (TopExp_Explorer it(shape, TopAbs_FACE); it.More(); it.Next()) { | |
| haveShape = true; | |
| if (myParams.Coplanar != CoplanarNone && !isCoplanar(it.Current(), plane)) { | |
| ++mySkippedShapes; | |
| if (myParams.Coplanar == CoplanarForce) { | |
| continue; | |
| } | |
| } | |
| for (TopExp_Explorer itw(it.Current(), TopAbs_WIRE); itw.More(); itw.Next()) { | |
| for (BRepTools_WireExplorer xp(TopoDS::Wire(itw.Current())); xp.More(); xp.Next()) { | |
| addWire( | |
| *myArea, | |
| BRepBuilderAPI_MakeWire(TopoDS::Edge(xp.Current())).Wire(), | |
| &myTrsf, | |
| myParams.Deflection, | |
| true | |
| ); | |
| } | |
| } | |
| } | |
| if (haveShape) { | |
| return; | |
| } | |
| for (TopExp_Explorer it(shape, TopAbs_EDGE); it.More(); it.Next()) { | |
| if (myParams.Coplanar != CoplanarNone && !isCoplanar(it.Current(), plane)) { | |
| ++mySkippedShapes; | |
| if (myParams.Coplanar == CoplanarForce) { | |
| continue; | |
| } | |
| } | |
| addWire( | |
| *myArea, | |
| BRepBuilderAPI_MakeWire(TopoDS::Edge(it.Current())).Wire(), | |
| &myTrsf, | |
| myParams.Deflection, | |
| true | |
| ); | |
| } | |
| } | |
| void Area::showShape(const TopoDS_Shape& shape, const char* name, const char* fmt, ...) | |
| { | |
| if (FC_LOG_INSTANCE.level() > FC_LOGLEVEL_TRACE) { | |
| App::Document* pcDoc = App::GetApplication().getActiveDocument(); | |
| if (!pcDoc) { | |
| pcDoc = App::GetApplication().newDocument(); | |
| } | |
| char buf[256]; | |
| if (!name && fmt) { | |
| va_list args; | |
| va_start(args, fmt); | |
| vsnprintf(buf, sizeof(buf), fmt, args); | |
| va_end(args); | |
| name = buf; | |
| } | |
| Part::Feature* pcFeature = pcDoc->addObject<Part::Feature>(name); | |
| pcFeature->Shape.setValue(shape); | |
| } | |
| } | |
| template<class T> | |
| static void showShapes(const T& shapes, const char* name, const char* fmt = nullptr, ...) | |
| { | |
| if (FC_LOG_INSTANCE.level() > FC_LOGLEVEL_TRACE) { | |
| BRep_Builder builder; | |
| TopoDS_Compound comp; | |
| builder.MakeCompound(comp); | |
| for (auto& s : shapes) { | |
| if (s.IsNull()) { | |
| continue; | |
| } | |
| builder.Add(comp, s); | |
| } | |
| char buf[256]; | |
| if (!name && fmt) { | |
| va_list args; | |
| va_start(args, fmt); | |
| vsnprintf(buf, sizeof(buf), fmt, args); | |
| va_end(args); | |
| name = buf; | |
| } | |
| Area::showShape(comp, name); | |
| } | |
| } | |
| template<class Func> | |
| static int foreachSubshape( | |
| const TopoDS_Shape& shape, | |
| Func func, | |
| int type = TopAbs_FACE, | |
| bool groupOpenEdges = false | |
| ) | |
| { | |
| int res = -1; | |
| std::vector<TopoDS_Shape> openShapes; | |
| switch (type) { | |
| case TopAbs_SOLID: | |
| for (TopExp_Explorer it(shape, TopAbs_SOLID); it.More(); it.Next()) { | |
| res = TopAbs_SOLID; | |
| func(it.Current(), TopAbs_SOLID); | |
| } | |
| if (res >= 0) { | |
| break; | |
| } | |
| // fall through | |
| case TopAbs_FACE: | |
| for (TopExp_Explorer it(shape, TopAbs_FACE); it.More(); it.Next()) { | |
| res = TopAbs_FACE; | |
| func(it.Current(), TopAbs_FACE); | |
| } | |
| if (res >= 0) { | |
| break; | |
| } | |
| // fall through | |
| case TopAbs_WIRE: | |
| for (TopExp_Explorer it(shape, TopAbs_WIRE); it.More(); it.Next()) { | |
| res = TopAbs_WIRE; | |
| if (groupOpenEdges && !BRep_Tool::IsClosed(TopoDS::Wire(it.Current()))) { | |
| openShapes.push_back(it.Current()); | |
| } | |
| else { | |
| func(it.Current(), TopAbs_WIRE); | |
| } | |
| } | |
| if (res >= 0) { | |
| break; | |
| } | |
| // fall through | |
| default: | |
| for (TopExp_Explorer it(shape, TopAbs_EDGE); it.More(); it.Next()) { | |
| res = TopAbs_EDGE; | |
| if (groupOpenEdges) { | |
| TopoDS_Edge e = TopoDS::Edge(it.Current()); | |
| gp_Pnt p1, p2; | |
| getEndPoints(e, p1, p2); | |
| if (p1.SquareDistance(p2) > Precision::SquareConfusion()) { | |
| openShapes.push_back(it.Current()); | |
| continue; | |
| } | |
| } | |
| func(it.Current(), TopAbs_EDGE); | |
| } | |
| } | |
| if (openShapes.empty()) { | |
| return res; | |
| } | |
| BRep_Builder builder; | |
| TopoDS_Compound comp; | |
| builder.MakeCompound(comp); | |
| for (auto& s : openShapes) { | |
| builder.Add(comp, s); | |
| } | |
| func(comp, TopAbs_COMPOUND); | |
| return TopAbs_COMPOUND; | |
| } | |
| struct FindPlane | |
| { | |
| TopoDS_Shape& myPlaneShape; | |
| gp_Trsf& myTrsf; | |
| double& myZ; | |
| FindPlane(TopoDS_Shape& s, gp_Trsf& t, double& z) | |
| : myPlaneShape(s) | |
| , myTrsf(t) | |
| , myZ(z) | |
| {} | |
| void operator()(const TopoDS_Shape& shape, int) | |
| { | |
| gp_Trsf trsf; | |
| gp_Pln pln; | |
| if (!getShapePlane(shape, pln)) { | |
| return; | |
| } | |
| gp_Ax3 pos = pln.Position(); | |
| AREA_TRACE("plane pos " << AREA_XYZ(pos.Location()) << ", " << AREA_XYZ(pos.Direction())); | |
| // We only use right hand coordinate, hence gp_Ax2 instead of gp_Ax3 | |
| if (!pos.Direct()) { | |
| AREA_WARN("left hand coordinate"); | |
| pos = gp_Ax3(pos.Ax2()); | |
| } | |
| gp_Dir dir(pos.Direction()); | |
| // To make things more 'normalized', we force the plane to face positive | |
| // axis direction if it parallels to either X, Y or Z plane. | |
| bool x0 = fabs(dir.X()) < Precision::Confusion(); | |
| bool y0 = fabs(dir.Y()) < Precision::Confusion(); | |
| bool z0 = fabs(dir.Z()) < Precision::Confusion(); | |
| if (x0 && y0) { | |
| dir.SetZ(fabs(dir.Z())); | |
| } | |
| else if (x0 && z0) { | |
| dir.SetY(fabs(dir.Y())); | |
| } | |
| else if (y0 && z0) { | |
| dir.SetX(fabs(dir.X())); | |
| } | |
| pos.SetDirection(dir); | |
| trsf.SetTransformation(pos); | |
| if (x0 && y0) { | |
| TopExp_Explorer it(shape, TopAbs_VERTEX); | |
| const auto& pt = BRep_Tool::Pnt(TopoDS::Vertex(it.Current())); | |
| if (!myPlaneShape.IsNull() && myZ > pt.Z()) { | |
| return; | |
| } | |
| myZ = pt.Z(); | |
| } | |
| else if (!myPlaneShape.IsNull()) { | |
| return; | |
| } | |
| myPlaneShape = shape; | |
| myTrsf = trsf; | |
| AREA_TRACE("plane pos " << AREA_XYZ(pos.Location()) << ", " << AREA_XYZ(pos.Direction())); | |
| } | |
| }; | |
| TopoDS_Shape Area::findPlane(const TopoDS_Shape& shape, gp_Trsf& trsf) | |
| { | |
| TopoDS_Shape plane; | |
| double top_z; | |
| foreachSubshape(shape, FindPlane(plane, trsf, top_z)); | |
| return plane; | |
| } | |
| int Area::project( | |
| TopoDS_Shape& shape_out, | |
| const TopoDS_Shape& shape_in, | |
| const AreaParams* params, | |
| const TopoDS_Shape* work_plane | |
| ) | |
| { | |
| FC_TIME_INIT2(t, t1); | |
| Handle(HLRBRep_Algo) brep_hlr; | |
| gp_Dir dir(0, 0, 1); | |
| try { | |
| brep_hlr = new HLRBRep_Algo(); | |
| brep_hlr->Add(shape_in, 0); | |
| HLRAlgo_Projector projector(gp_Ax2(gp_Pnt(), dir)); | |
| brep_hlr->Projector(projector); | |
| brep_hlr->Update(); | |
| brep_hlr->Hide(); | |
| } | |
| catch (...) { | |
| AREA_ERR("error occurred while projecting shape"); | |
| return -1; | |
| } | |
| FC_TIME_LOG(t1, "HLRBrep_Algo"); | |
| WireJoiner joiner; | |
| try { | |
| TopoDS_Shape shape; | |
| HLRBRep_HLRToShape hlrToShape(brep_hlr); | |
| ADD_HLR_SHAPE(V); | |
| ADD_HLR_SHAPE(OutLineV); | |
| } | |
| catch (...) { | |
| AREA_ERR("error occurred while extracting edges"); | |
| return -1; | |
| } | |
| FC_TIME_LOG(t1, "WireJoiner init"); | |
| joiner.splitEdges(); | |
| FC_TIME_LOG(t1, "WireJoiner splitEdges"); | |
| for (const auto& v : joiner.edges) { | |
| showShape(v.edge, "split"); | |
| } | |
| double tolerance = params ? params->Tolerance : Precision::Confusion(); | |
| int skips = joiner.findClosedWires(tolerance); | |
| FC_TIME_LOG(t1, "WireJoiner findClosedWires"); | |
| showShape(joiner.comp, "pre_project"); | |
| Area area(params); | |
| area.myParams.SectionCount = 0; | |
| area.myParams.Offset = 0.0; | |
| area.myParams.PocketMode = 0; | |
| area.myParams.Explode = false; | |
| area.myParams.FitArcs = false; | |
| area.myParams.Reorient = false; | |
| area.myParams.Outline = true; | |
| area.myParams.Fill = TopExp_Explorer(shape_in, TopAbs_FACE).More() ? FillFace : FillNone; | |
| area.myParams.Coplanar = CoplanarNone; | |
| area.myProjecting = true; | |
| if (work_plane) { | |
| area.myWorkPlane = *work_plane; | |
| } | |
| area.add(joiner.comp, OperationUnion); | |
| const TopoDS_Shape& shape = area.getShape(); | |
| area.myParams.dump("project"); | |
| showShape(shape, "projected"); | |
| FC_TIME_LOG(t1, "Clipper wire union"); | |
| FC_TIME_LOG(t, "project total"); | |
| if (shape.IsNull()) { | |
| AREA_ERR("project failed"); | |
| return -1; | |
| } | |
| shape_out = shape; | |
| return skips; | |
| } | |
| std::vector<shared_ptr<Area>> Area::makeSections( | |
| PARAM_ARGS(PARAM_FARG, AREA_PARAMS_SECTION_EXTRA), | |
| const std::vector<double>& _heights, | |
| const TopoDS_Shape& section_plane | |
| ) | |
| { | |
| TopoDS_Shape plane; | |
| gp_Trsf trsf; | |
| if (!section_plane.IsNull()) { | |
| plane = findPlane(section_plane, trsf); | |
| } | |
| else { | |
| plane = getPlane(&trsf); | |
| } | |
| if (plane.IsNull()) { | |
| throw Base::ValueError("failed to obtain section plane"); | |
| } | |
| FC_TIME_INIT2(t, t1); | |
| TopLoc_Location loc(trsf); | |
| Bnd_Box bounds; | |
| for (const Shape& s : myShapes) { | |
| const TopoDS_Shape& shape = s.shape.Moved(loc); | |
| BRepBndLib::Add(shape, bounds, Standard_False); | |
| } | |
| bounds.SetGap(0.0); | |
| Standard_Real xMin, yMin, zMin, xMax, yMax, zMax; | |
| bounds.Get(xMin, yMin, zMin, xMax, yMax, zMax); | |
| AREA_TRACE( | |
| "section bounds X(" << xMin << ',' << xMax << "), Y(" << yMin << ',' << yMax << "), Z(" | |
| << zMin << ',' << zMax << ')' | |
| ); | |
| std::vector<double> heights; | |
| double tolerance = 0.0; | |
| if (_heights.empty()) { | |
| double z; | |
| double d = fabs(myParams.Stepdown); | |
| if (myParams.SectionCount > 1 && d < Precision::Confusion()) { | |
| throw Base::ValueError("invalid stepdown"); | |
| } | |
| if (mode == SectionModeBoundBox) { | |
| if (myParams.Stepdown > 0.0) { | |
| z = zMax - myParams.SectionOffset; | |
| } | |
| else { | |
| z = zMin + myParams.SectionOffset; | |
| } | |
| } | |
| else if (mode == SectionModeWorkplane) { | |
| // Because we've transformed the shapes using the work plane so | |
| // that the work plane is aligned with xy0 plane, the starting Z | |
| // value shall be 0 minus the given section offset. Note the | |
| // section offset is relative to the starting Z | |
| if (myParams.Stepdown > 0.0) { | |
| z = -myParams.SectionOffset; | |
| } | |
| else { | |
| z = myParams.SectionOffset; | |
| } | |
| } | |
| else { | |
| gp_Pnt pt(0, 0, myParams.SectionOffset); | |
| z = pt.Transformed(loc).Z(); | |
| } | |
| if (z > zMax) { | |
| z = zMax; | |
| } | |
| else if (z < zMin) { | |
| z = zMin; | |
| } | |
| double dz; | |
| if (myParams.Stepdown > 0.0) { | |
| dz = z - zMin; | |
| tolerance = myParams.SectionTolerance; | |
| } | |
| else { | |
| dz = zMax - z; | |
| tolerance = -myParams.SectionTolerance; | |
| } | |
| int count = myParams.SectionCount; | |
| if (count < 0 || count * d > dz) { | |
| count = floor(dz / d) + 1; | |
| } | |
| heights.reserve(count); | |
| for (int i = 0; i < count; ++i, z -= myParams.Stepdown) { | |
| double height = z - tolerance; | |
| if (z - zMin < myParams.SectionTolerance) { | |
| height = zMin + myParams.SectionTolerance; | |
| if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { | |
| AREA_WARN("hit bottom " << z << ',' << zMin << ',' << height); | |
| } | |
| heights.push_back(height); | |
| if (myParams.Stepdown > 0.0) { | |
| break; | |
| } | |
| } | |
| else if (zMax - z < myParams.SectionTolerance) { | |
| height = zMax - myParams.SectionTolerance; | |
| if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { | |
| AREA_WARN("hit top " << z << ',' << zMax << ',' << height); | |
| } | |
| heights.push_back(height); | |
| if (myParams.Stepdown < 0.0) { | |
| break; | |
| } | |
| } | |
| else { | |
| heights.push_back(height); | |
| } | |
| } | |
| } | |
| else { | |
| heights.reserve(_heights.size()); | |
| bool hitMax = false, hitMin = false; | |
| for (double z : _heights) { | |
| switch (mode) { | |
| case SectionModeAbsolute: { | |
| gp_Pnt pt(0, 0, z); | |
| z = pt.Transformed(loc).Z(); | |
| break; | |
| } | |
| case SectionModeBoundBox: | |
| z = zMax - z; | |
| break; | |
| case SectionModeWorkplane: | |
| z = -z; | |
| break; | |
| default: | |
| throw Base::ValueError("invalid section mode"); | |
| } | |
| if (z - zMin < myParams.SectionTolerance) { | |
| if (hitMin) { | |
| continue; | |
| } | |
| hitMin = true; | |
| double zNew = zMin + myParams.SectionTolerance; | |
| // Silence the warning if _heights is not empty | |
| if (_heights.empty() && FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) { | |
| AREA_WARN("hit bottom " << z << ',' << zMin << ',' << zNew); | |
| } | |
| z = zNew; | |
| } | |
| else if (zMax - z < myParams.SectionTolerance) { | |
| if (hitMax) { | |
| continue; | |
| } | |
| double zNew = zMax - myParams.SectionTolerance; | |
| AREA_WARN("hit top " << z << ',' << zMax << ',' << zNew); | |
| z = zNew; | |
| } | |
| heights.push_back(z); | |
| } | |
| } | |
| if (heights.empty()) { | |
| throw Base::ValueError("no sections"); | |
| } | |
| std::vector<shared_ptr<Area>> sections; | |
| sections.reserve(heights.size()); | |
| std::list<Shape> projectedShapes; | |
| if (project) { | |
| projectedShapes = getProjectedShapes(trsf, false); | |
| if (projectedShapes.empty()) { | |
| AREA_ERR("empty projection"); | |
| return sections; | |
| } | |
| } | |
| tolerance *= 2.0; | |
| bool can_retry = fabs(tolerance) > Precision::Confusion(); | |
| TopLoc_Location locInverse(loc.Inverted()); | |
| for (size_t i = 0; i < heights.size(); ++i) { | |
| double z = heights[i]; | |
| bool retried = !can_retry; | |
| while (true) { | |
| gp_Pln pln(gp_Pnt(0, 0, z), gp_Dir(0, 0, 1)); | |
| Standard_Real a, b, c, d; | |
| pln.Coefficients(a, b, c, d); | |
| BRepLib_MakeFace mkFace(pln, xMin, xMax, yMin, yMax); | |
| const TopoDS_Shape& face = mkFace.Face(); | |
| shared_ptr<Area> area(std::make_shared<Area>(&myParams)); | |
| area->myParams.Outline = false; | |
| area->setPlane(face.Moved(locInverse)); | |
| if (project) { | |
| for (const auto& s : projectedShapes) { | |
| gp_Trsf t; | |
| t.SetTranslation(gp_Vec(0, 0, -d)); | |
| TopLoc_Location wloc(t); | |
| area->add(s.shape.Moved(wloc).Moved(locInverse), s.op); | |
| } | |
| sections.push_back(area); | |
| break; | |
| } | |
| for (auto it = myShapes.begin(); it != myShapes.end(); ++it) { | |
| const auto& s = *it; | |
| BRep_Builder builder; | |
| TopoDS_Compound comp; | |
| builder.MakeCompound(comp); | |
| for (TopExp_Explorer xp(s.shape.Moved(loc), TopAbs_SOLID); xp.More(); xp.Next()) { | |
| showShape(xp.Current(), nullptr, "section_%zu_shape", i); | |
| std::list<TopoDS_Wire> wires; | |
| Part::CrossSection section(a, b, c, xp.Current()); | |
| Part::FuzzyHelper::withBooleanFuzzy(.0, [&]() { | |
| // Workaround for https://github.com/FreeCAD/FreeCAD/issues/17748 | |
| // needed to make finish pass work. | |
| // This fix might be better to move into Part::CrossSection but it is kept | |
| // here for now to be on the safe side. | |
| wires = section.slice(-d); | |
| }); | |
| showShapes(wires, nullptr, "section_%zu_wire", i); | |
| if (wires.empty()) { | |
| AREA_LOG("Section returns no wires"); | |
| continue; | |
| } | |
| // always try to make face to normalize wire orientation | |
| Part::FaceMakerBullseye mkFace; | |
| mkFace.setPlane(pln); | |
| for (const TopoDS_Wire& wire : wires) { | |
| if (BRep_Tool::IsClosed(wire)) { | |
| mkFace.addWire(wire); | |
| } | |
| } | |
| try { | |
| mkFace.Build(); | |
| const TopoDS_Shape& shape = mkFace.Shape(); | |
| if (shape.IsNull()) { | |
| AREA_WARN("FaceMakerBullseye return null shape on section"); | |
| } | |
| else { | |
| showShape(shape, nullptr, "section_%zu_face", i); | |
| for (auto it = wires.begin(), itNext = it; it != wires.end(); | |
| it = itNext) { | |
| ++itNext; | |
| if (BRep_Tool::IsClosed(*it)) { | |
| wires.erase(it); | |
| } | |
| } | |
| for (TopExp_Explorer xp( | |
| shape, | |
| myParams.Fill == FillNone ? TopAbs_WIRE : TopAbs_FACE | |
| ); | |
| xp.More(); | |
| xp.Next()) { | |
| builder.Add(comp, xp.Current()); | |
| } | |
| } | |
| } | |
| catch (Base::Exception& e) { | |
| AREA_WARN("FaceMakerBullseye failed on section: " << e.what()); | |
| } | |
| for (const TopoDS_Wire& wire : wires) { | |
| builder.Add(comp, wire); | |
| } | |
| } | |
| // Make sure the compound has at least one edge | |
| if (TopExp_Explorer(comp, TopAbs_EDGE).More()) { | |
| const TopoDS_Shape& shape = comp.Moved(locInverse); | |
| showShape(shape, nullptr, "section_%zu_result", i); | |
| area->add(shape, s.op); | |
| } | |
| else if (area->myShapes.empty()) { | |
| auto itNext = it; | |
| if (++itNext != myShapes.end() | |
| && (itNext->op == OperationIntersection || itNext->op == OperationDifference)) { | |
| break; | |
| } | |
| } | |
| } | |
| if (!area->myShapes.empty()) { | |
| sections.push_back(area); | |
| FC_TIME_LOG(t1, "makeSection " << z); | |
| showShape(area->getShape(), nullptr, "section_%zu_final", i); | |
| break; | |
| } | |
| if (retried) { | |
| AREA_WARN("Discard empty section"); | |
| break; | |
| } | |
| else { | |
| AREA_TRACE("retry section " << z << "->" << z + tolerance); | |
| z += tolerance; | |
| retried = true; | |
| } | |
| } | |
| } | |
| FC_TIME_LOG(t, "makeSection count: " << sections.size() << ", total"); | |
| return sections; | |
| } | |
| TopoDS_Shape Area::getPlane(gp_Trsf* trsf) | |
| { | |
| if (!myWorkPlane.IsNull()) { | |
| if (trsf) { | |
| *trsf = myTrsf; | |
| } | |
| return myWorkPlane; | |
| } | |
| if (myShapePlane.IsNull()) { | |
| if (myShapes.empty()) { | |
| throw Base::ValueError("no shape added"); | |
| } | |
| double top_z; | |
| for (auto& s : myShapes) { | |
| foreachSubshape(s.shape, FindPlane(myShapePlane, myTrsf, top_z)); | |
| } | |
| if (myShapePlane.IsNull()) { | |
| throw Base::ValueError("shapes are not planar"); | |
| } | |
| } | |
| if (trsf) { | |
| *trsf = myTrsf; | |
| } | |
| return myShapePlane; | |
| } | |
| bool Area::isBuilt() const | |
| { | |
| return (myArea || !mySections.empty()); | |
| } | |
| std::list<Area::Shape> Area::getProjectedShapes(const gp_Trsf& trsf, bool inverse) const | |
| { | |
| std::list<Shape> ret; | |
| TopLoc_Location loc(trsf); | |
| TopLoc_Location locInverse(loc.Inverted()); | |
| mySkippedShapes = 0; | |
| for (auto& s : myShapes) { | |
| TopoDS_Shape out; | |
| int skipped = Area::project(out, s.shape.Moved(loc), &myParams, &myWorkPlane); | |
| if (skipped < 0) { | |
| ++mySkippedShapes; | |
| continue; | |
| } | |
| else { | |
| mySkippedShapes += skipped; | |
| } | |
| if (!out.IsNull()) { | |
| ret.emplace_back(s.op, inverse ? out.Moved(locInverse) : out); | |
| } | |
| } | |
| if (mySkippedShapes) { | |
| AREA_WARN("skipped " << mySkippedShapes << " sub shapes during projection"); | |
| } | |
| return ret; | |
| } | |
| void Area::build() | |
| { | |
| if (isBuilt()) { | |
| return; | |
| } | |
| if (myShapes.empty()) { | |
| throw Base::ValueError("no shape added"); | |
| } | |
| PARAM_ENUM_CONVERT(AREA_MY, PARAM_FNAME, PARAM_ENUM_EXCEPT, AREA_PARAMS_CLIPPER_FILL); | |
| if (myHaveSolid && myParams.SectionCount) { | |
| mySections = makeSections(PARAM_FIELDS(AREA_MY, AREA_PARAMS_SECTION_EXTRA)); | |
| return; | |
| } | |
| FC_TIME_INIT(t); | |
| gp_Trsf trsf; | |
| getPlane(&trsf); | |
| try { | |
| myArea = std::make_unique<CArea>(); | |
| myAreaOpen = std::make_unique<CArea>(); | |
| CAreaConfig conf(myParams); | |
| CArea areaClip; | |
| mySkippedShapes = 0; | |
| short op = OperationUnion; | |
| bool pending = false; | |
| bool exploding = myParams.Explode; | |
| const auto& shapes = (myParams.Outline && !myProjecting) ? getProjectedShapes(trsf) | |
| : myShapes; | |
| for (const Shape& s : shapes) { | |
| if (exploding) { | |
| exploding = false; | |
| explode(s.shape); | |
| continue; | |
| } | |
| else if (op != s.op) { | |
| if (myParams.OpenMode != OpenModeNone) { | |
| myArea->m_curves.splice(myArea->m_curves.end(), myAreaOpen->m_curves); | |
| } | |
| pending = false; | |
| if (!areaClip.m_curves.empty()) { | |
| if (op == OperationCompound) { | |
| myArea->m_curves.splice(myArea->m_curves.end(), areaClip.m_curves); | |
| } | |
| else { | |
| myArea->Clip(toClipperOp(op), &areaClip, SubjectFill, ClipFill); | |
| areaClip.m_curves.clear(); | |
| } | |
| } | |
| op = s.op; | |
| } | |
| addToBuild(op == OperationUnion ? *myArea : areaClip, s.shape); | |
| pending = true; | |
| } | |
| if (mySkippedShapes && !myHaveSolid) { | |
| AREA_WARN( | |
| (myParams.Coplanar == CoplanarForce ? "Skipped " : "Found ") | |
| << mySkippedShapes << " non coplanar shapes" | |
| ); | |
| } | |
| if (pending) { | |
| if (myParams.OpenMode != OpenModeNone) { | |
| myArea->m_curves.splice(myArea->m_curves.end(), myAreaOpen->m_curves); | |
| } | |
| if (op == OperationCompound) { | |
| myArea->m_curves.splice(myArea->m_curves.end(), areaClip.m_curves); | |
| } | |
| else { | |
| myArea->Clip(toClipperOp(op), &areaClip, SubjectFill, ClipFill); | |
| } | |
| } | |
| myArea->m_curves.splice(myArea->m_curves.end(), myAreaOpen->m_curves); | |
| // Reassemble wires after explode | |
| if (myParams.Explode) { | |
| WireJoiner joiner; | |
| gp_Trsf trsf(myTrsf.Inverted()); | |
| for (const auto& c : myArea->m_curves) { | |
| auto wire = toShape(c, &trsf); | |
| if (!wire.IsNull()) { | |
| joiner.add(wire); | |
| } | |
| } | |
| joiner.join(Precision::Confusion()); | |
| Area area(&myParams); | |
| area.myParams.Explode = false; | |
| area.myParams.Coplanar = CoplanarNone; | |
| area.myWorkPlane = getPlane(&area.myTrsf); | |
| area.add(joiner.comp, OperationCompound); | |
| area.build(); | |
| myArea = std::move(area.myArea); | |
| } | |
| if (myParams.Outline) { | |
| myArea->Reorder(); | |
| for (auto it = myArea->m_curves.begin(), itNext = it; it != myArea->m_curves.end(); | |
| it = itNext) { | |
| ++itNext; | |
| auto& curve = *it; | |
| if (curve.IsClosed() && curve.IsClockwise()) { | |
| myArea->m_curves.erase(it); | |
| } | |
| } | |
| } | |
| FC_TIME_TRACE(t, "prepare"); | |
| } | |
| catch (...) { | |
| clean(); | |
| throw; | |
| } | |
| } | |
| TopoDS_Shape Area::toShape(CArea& area, short fill, int reorient) | |
| { | |
| gp_Trsf trsf(myTrsf.Inverted()); | |
| bool bFill; | |
| switch (fill) { | |
| case Area::FillAuto: | |
| bFill = myHaveFace; | |
| break; | |
| case Area::FillFace: | |
| bFill = true; | |
| break; | |
| default: | |
| bFill = false; | |
| } | |
| if (myParams.FitArcs) { | |
| if (&area == myArea.get()) { | |
| CArea copy(area); | |
| copy.FitArcs(); | |
| return toShape(copy, bFill, &trsf, reorient); | |
| } | |
| area.FitArcs(); | |
| } | |
| return toShape(area, bFill, &trsf, reorient); | |
| } | |
| TopoDS_Shape Area::getShape(int index) | |
| { | |
| build(); | |
| AREA_SECTION(getShape, index); | |
| if (myShapeDone) { | |
| return myShape; | |
| } | |
| if (!myArea) { | |
| return TopoDS_Shape(); | |
| } | |
| CAreaConfig conf(myParams); | |
| // if no offset, try pocket | |
| if (fabs(myParams.Offset) < Precision::Confusion()) { | |
| if (myParams.PocketMode == PocketModeNone) { | |
| myShape = toShape(*myArea, myParams.Fill); | |
| myShapeDone = true; | |
| return myShape; | |
| } | |
| myShape = makePocket(index, PARAM_FIELDS(AREA_MY, AREA_PARAMS_POCKET)); | |
| myShapeDone = true; | |
| return myShape; | |
| } | |
| // if no pocket, do offset or thicken | |
| if (myParams.PocketMode == PocketModeNone) { | |
| myShape = makeOffset(index, PARAM_FIELDS(AREA_MY, AREA_PARAMS_OFFSET)); | |
| myShapeDone = true; | |
| return myShape; | |
| } | |
| FC_TIME_INIT(t); | |
| // do offset first, then pocket the inner most offset shape | |
| std::list<shared_ptr<CArea>> areas; | |
| makeOffset(areas, PARAM_FIELDS(AREA_MY, AREA_PARAMS_OFFSET)); | |
| if (areas.empty()) { | |
| areas.push_back(make_shared<CArea>(*myArea)); | |
| } | |
| Area areaPocket(&myParams); | |
| bool front = true; | |
| if (areas.size() > 1) { | |
| double step = myParams.Stepover; | |
| if (fabs(step) < Precision::Confusion()) { | |
| step = myParams.Offset; | |
| } | |
| front = step > 0; | |
| } | |
| // for pocketing, we discard the outer most offset wire in order to achieve | |
| // the effect of offsetting shape first than pocket, where the actual offset | |
| // path is not wanted. For extra outline profiling, add extra_offset | |
| if (front) { | |
| areaPocket.add(toShape(*areas.front(), myParams.Fill)); | |
| areas.pop_back(); | |
| } | |
| else { | |
| areaPocket.add(toShape(*areas.back(), myParams.Fill)); | |
| areas.pop_front(); | |
| } | |
| BRep_Builder builder; | |
| TopoDS_Compound compound; | |
| builder.MakeCompound(compound); | |
| short fill = myParams.Thicken ? FillFace : FillNone; | |
| FC_TIME_INIT(t2); | |
| FC_DURATION_DECL_INIT(d); | |
| for (shared_ptr<CArea> area : areas) { | |
| if (myParams.Thicken) { | |
| area->Thicken(myParams.ToolRadius); | |
| FC_DURATION_PLUS(d, t2); | |
| } | |
| const TopoDS_Shape& shape = toShape(*area, fill); | |
| if (shape.IsNull()) { | |
| continue; | |
| } | |
| builder.Add(compound, shape); | |
| } | |
| if (myParams.Thicken) { | |
| FC_DURATION_LOG(d, "Thicken"); | |
| } | |
| // make sure the compound has at least one edge | |
| if (TopExp_Explorer(compound, TopAbs_EDGE).More()) { | |
| builder.Add(compound, areaPocket.makePocket(-1, PARAM_FIELDS(AREA_MY, AREA_PARAMS_POCKET))); | |
| myShape = compound; | |
| } | |
| myShapeDone = true; | |
| FC_TIME_LOG(t, "total"); | |
| return myShape; | |
| } | |
| TopoDS_Shape Area::makeOffset( | |
| int index, | |
| PARAM_ARGS(PARAM_FARG, AREA_PARAMS_OFFSET), | |
| int reorient, | |
| bool from_center | |
| ) | |
| { | |
| build(); | |
| AREA_SECTION(makeOffset, index, PARAM_FIELDS(PARAM_FARG, AREA_PARAMS_OFFSET), reorient, from_center); | |
| std::list<shared_ptr<CArea>> areas; | |
| makeOffset(areas, PARAM_FIELDS(PARAM_FARG, AREA_PARAMS_OFFSET), from_center); | |
| if (areas.empty()) { | |
| if (myParams.Thicken && myParams.ToolRadius > Precision::Confusion()) { | |
| CArea area(*myArea); | |
| FC_TIME_INIT(t); | |
| area.Thicken(myParams.ToolRadius); | |
| FC_TIME_LOG(t, "Thicken"); | |
| return toShape(area, FillFace, reorient); | |
| } | |
| return TopoDS_Shape(); | |
| } | |
| BRep_Builder builder; | |
| TopoDS_Compound compound; | |
| builder.MakeCompound(compound); | |
| FC_TIME_INIT(t); | |
| FC_DURATION_DECL_INIT(d); | |
| bool thicken = myParams.Thicken && myParams.ToolRadius > Precision::Confusion(); | |
| for (shared_ptr<CArea> area : areas) { | |
| short fill; | |
| if (thicken) { | |
| area->Thicken(myParams.ToolRadius); | |
| FC_DURATION_PLUS(d, t); | |
| fill = FillFace; | |
| } | |
| else if (areas.size() == 1) { | |
| fill = myParams.Fill; | |
| } | |
| else { | |
| fill = FillNone; | |
| } | |
| const TopoDS_Shape& shape = toShape(*area, fill, reorient); | |
| if (shape.IsNull()) { | |
| continue; | |
| } | |
| builder.Add(compound, shape); | |
| } | |
| if (thicken) { | |
| FC_DURATION_LOG(d, "Thicken"); | |
| } | |
| if (TopExp_Explorer(compound, TopAbs_EDGE).More()) { | |
| return TopoDS_Shape(std::move(compound)); | |
| } | |
| return TopoDS_Shape(); | |
| } | |
| void Area::makeOffset( | |
| list<shared_ptr<CArea>>& areas, | |
| PARAM_ARGS(PARAM_FARG, AREA_PARAMS_OFFSET), | |
| bool from_center | |
| ) | |
| { | |
| if (fabs(offset) < Precision::Confusion()) { | |
| return; | |
| } | |
| FC_TIME_INIT2(t, t1); | |
| long count = 1; | |
| if (extra_pass) { | |
| if (fabs(stepover) < Precision::Confusion()) { | |
| stepover = offset; | |
| } | |
| if (extra_pass > 0) { | |
| count += extra_pass; | |
| } | |
| else { | |
| if (stepover > 0 || offset > 0) { | |
| throw Base::ValueError("invalid extra count"); | |
| } | |
| // In this case, we loop until no outputs from clipper | |
| count = -1; | |
| } | |
| } | |
| PARAM_ENUM_CONVERT(AREA_MY, PARAM_FNAME, PARAM_ENUM_EXCEPT, AREA_PARAMS_OFFSET_CONF); | |
| PARAM_ENUM_CONVERT(AREA_MY, PARAM_FNAME, PARAM_ENUM_EXCEPT, AREA_PARAMS_CLIPPER_FILL); | |
| if (offset < 0) { | |
| if (count < 0) { | |
| if (!last_stepover) { | |
| last_stepover = offset * 0.5; | |
| } | |
| } | |
| else { | |
| last_stepover = 0; | |
| } | |
| } | |
| for (int i = 0; count < 0 || i < count; ++i, offset += stepover) { | |
| if (from_center) { | |
| areas.push_front(make_shared<CArea>()); | |
| } | |
| else { | |
| areas.push_back(make_shared<CArea>()); | |
| } | |
| CArea& area = from_center ? (*areas.front()) : (*areas.back()); | |
| CArea areaOpen; | |
| if (myParams.Algo == Area::Algolibarea) { | |
| for (const CCurve& c : myArea->m_curves) { | |
| if (c.IsClosed()) { | |
| area.append(c); | |
| } | |
| else { | |
| areaOpen.append(c); | |
| } | |
| } | |
| } | |
| else | |
| area = *myArea; | |
| switch (myParams.Algo) { | |
| case Area::Algolibarea: | |
| // libarea somehow fails offset without Reorder, but ClipperOffset | |
| // works okay. Don't know why | |
| area.Reorder(); | |
| area.Offset(-offset); | |
| if (areaOpen.m_curves.size()) { | |
| areaOpen.Thicken(offset); | |
| area.Clip(ClipperLib::ctUnion, &areaOpen, SubjectFill, ClipFill); | |
| } | |
| break; | |
| case Area::AlgoClipperOffset: | |
| area.OffsetWithClipper( | |
| offset, | |
| JoinType, | |
| EndType, | |
| myParams.MiterLimit, | |
| myParams.RoundPrecision | |
| ); | |
| break; | |
| } | |
| if (count > 1) { | |
| FC_TIME_LOG(t1, "makeOffset " << i << '/' << count); | |
| } | |
| if (area.m_curves.empty()) { | |
| if (from_center) { | |
| areas.pop_front(); | |
| } | |
| else { | |
| areas.pop_back(); | |
| } | |
| if (areas.empty()) { | |
| break; | |
| } | |
| if (last_stepover && last_stepover > stepover) { | |
| offset -= stepover; | |
| stepover = last_stepover; | |
| --i; | |
| continue; | |
| } | |
| return; | |
| } | |
| } | |
| FC_TIME_LOG(t, "makeOffset count: " << count); | |
| } | |
| TopoDS_Shape Area::makePocket(int index, PARAM_ARGS(PARAM_FARG, AREA_PARAMS_POCKET)) | |
| { | |
| if (tool_radius < Precision::Confusion()) { | |
| throw Base::ValueError("tool radius too small"); | |
| } | |
| if (stepover == 0.0) { | |
| stepover = tool_radius; | |
| } | |
| if (stepover < Precision::Confusion()) { | |
| throw Base::ValueError("stepover too small"); | |
| } | |
| if (mode == Area::PocketModeNone) { | |
| return TopoDS_Shape(); | |
| } | |
| build(); | |
| AREA_SECTION(makePocket, index, PARAM_FIELDS(PARAM_FARG, AREA_PARAMS_POCKET)); | |
| FC_TIME_INIT(t); | |
| bool done = false; | |
| if (index >= 0) { | |
| if (fabs(angle_shift) >= Precision::Confusion()) { | |
| angle += index * angle_shift; | |
| } | |
| if (fabs(shift) >= Precision::Confusion()) { | |
| shift *= index; | |
| } | |
| } | |
| if (angle < -360.0) { | |
| angle += ceil(fabs(angle) / 360.0) * 360.0; | |
| } | |
| else if (angle > 360.0) { | |
| angle -= floor(angle / 360.0) * 360.0; | |
| } | |
| else if (angle < 0.0) { | |
| angle += 360.0; | |
| } | |
| if (shift < -stepover) { | |
| shift += ceil(fabs(shift) / stepover) * stepover; | |
| } | |
| else if (shift > stepover) { | |
| shift -= floor(shift / stepover) * stepover; | |
| } | |
| else if (shift < 0.0) { | |
| shift += stepover; | |
| } | |
| CAreaConfig conf(myParams); | |
| CArea out; | |
| PocketMode pm; | |
| switch (mode) { | |
| case Area::PocketModeZigZag: | |
| pm = ZigZagPocketMode; | |
| break; | |
| case Area::PocketModeSpiral: | |
| pm = SpiralPocketMode; | |
| break; | |
| case Area::PocketModeOffset: { | |
| PARAM_DECLARE_INIT(PARAM_FNAME, AREA_PARAMS_OFFSET); | |
| Offset = -tool_radius - extra_offset - shift; | |
| ExtraPass = -1; | |
| Stepover = -stepover; | |
| LastStepover = -last_stepover; | |
| // make offset and make sure the loop is CW (i.e. inner wires) | |
| return makeOffset(index, PARAM_FIELDS(PARAM_FNAME, AREA_PARAMS_OFFSET), -1, from_center); | |
| } | |
| case Area::PocketModeZigZagOffset: | |
| pm = ZigZagThenSingleOffsetPocketMode; | |
| break; | |
| case Area::PocketModeLine: | |
| case Area::PocketModeGrid: | |
| case Area::PocketModeTriangle: { | |
| CBox2D box; | |
| myArea->GetBox(box); | |
| if (!box.m_valid) { | |
| throw Base::ValueError("failed to get bound box"); | |
| } | |
| double angles[4]; | |
| int count = 1; | |
| angles[0] = 0.0; | |
| if (mode == Area::PocketModeGrid) { | |
| angles[1] = 90.0; | |
| count = 2; | |
| if (shift < Precision::Confusion()) { | |
| count = 4; | |
| angles[2] = 180.0; | |
| angles[3] = 270.0; | |
| } | |
| } | |
| else if (mode == Area::PocketModeTriangle) { | |
| count = 3; | |
| angles[1] = 120; | |
| angles[2] = 240; | |
| } | |
| else { | |
| shift = 0.0; // Line pattern does not support shift | |
| } | |
| Point center(box.Centre()); | |
| double r = box.Radius() + stepover; | |
| if (extra_offset > 0) { | |
| r += extra_offset; | |
| } | |
| int steps = (int)ceil(r * 2.0 / stepover); | |
| for (int i = 0; i < count; ++i) { | |
| double a = angle + angles[i]; | |
| if (a > 360.0) { | |
| a -= 360.0; | |
| } | |
| double offset = -r + shift; | |
| for (int j = 0; j < steps; ++j, offset += stepover) { | |
| Point p1(-r, offset), p2(r, offset); | |
| if (a > Precision::Confusion()) { | |
| double r = Base::toRadians(a); | |
| p1.Rotate(r); | |
| p2.Rotate(r); | |
| } | |
| out.m_curves.emplace_back(); | |
| CCurve& curve = out.m_curves.back(); | |
| curve.m_vertices.emplace_back(p1 + center); | |
| curve.m_vertices.emplace_back(p2 + center); | |
| } | |
| } | |
| PARAM_ENUM_CONVERT(AREA_MY, PARAM_FNAME, PARAM_ENUM_EXCEPT, AREA_PARAMS_CLIPPER_FILL); | |
| PARAM_ENUM_CONVERT(AREA_MY, PARAM_FNAME, PARAM_ENUM_EXCEPT, AREA_PARAMS_OFFSET_CONF); | |
| auto area = *myArea; | |
| area.OffsetWithClipper( | |
| -tool_radius - extra_offset, | |
| JoinType, | |
| EndType, | |
| myParams.MiterLimit, | |
| myParams.RoundPrecision | |
| ); | |
| out.Clip(toClipperOp(OperationIntersection), &area, SubjectFill, ClipFill); | |
| done = true; | |
| break; | |
| } | |
| default: | |
| throw Base::ValueError("unknown pocket mode"); | |
| } | |
| if (!done) { | |
| CAreaPocketParams params(tool_radius, extra_offset, stepover, from_center, pm, angle); | |
| CArea in(*myArea); | |
| // MakePocketToolPath internally uses libarea Offset which somehow demands | |
| // reorder before input, otherwise nothing is shown. | |
| in.Reorder(); | |
| in.MakePocketToolpath(out.m_curves, params); | |
| } | |
| FC_TIME_LOG(t, "makePocket"); | |
| if (myParams.Thicken) { | |
| FC_TIME_INIT(t); | |
| out.Thicken(tool_radius); | |
| FC_TIME_LOG(t, "thicken"); | |
| return toShape(out, FillFace); | |
| } | |
| else { | |
| return toShape(out, FillNone); | |
| } | |
| } | |
| static inline bool IsLeft(const gp_Pnt& a, const gp_Pnt& b, const gp_Pnt& c) | |
| { | |
| return ((b.X() - a.X()) * (c.Y() - a.Y()) - (b.Y() - a.Y()) * (c.X() - a.X())) > 0; | |
| } | |
| TopoDS_Shape Area::toShape(const CCurve& _c, const gp_Trsf* trsf, int reorient) | |
| { | |
| Handle(TopTools_HSequenceOfShape) hEdges = new TopTools_HSequenceOfShape(); | |
| Handle(TopTools_HSequenceOfShape) hWires = new TopTools_HSequenceOfShape(); | |
| CCurve cReversed; | |
| if (reorient) { | |
| if (_c.IsClosed() | |
| && ((reorient > 0 && _c.IsClockwise()) || (reorient < 0 && !_c.IsClockwise()))) { | |
| cReversed = _c; | |
| cReversed.Reverse(); | |
| } | |
| else { | |
| reorient = 0; | |
| } | |
| } | |
| const CCurve& c = reorient ? cReversed : _c; | |
| TopoDS_Shape shape; | |
| gp_Pnt pstart, pt; | |
| bool first = true; | |
| for (const CVertex& v : c.m_vertices) { | |
| if (first) { | |
| first = false; | |
| pstart = pt = gp_Pnt(v.m_p.x, v.m_p.y, 0); | |
| continue; | |
| } | |
| gp_Pnt pnext(v.m_p.x, v.m_p.y, 0); | |
| if (pnext.SquareDistance(pt) <= Precision::SquareConfusion()) { | |
| continue; | |
| } | |
| if (v.m_type == 0) { | |
| auto edge = BRepBuilderAPI_MakeEdge(pt, pnext).Edge(); | |
| hEdges->Append(edge); | |
| } | |
| else { | |
| gp_Pnt center(v.m_c.x, v.m_c.y, 0); | |
| double r = center.Distance(pt); | |
| double r2 = center.Distance(pnext); | |
| bool fix_arc = fabs(r - r2) > Precision::Confusion(); | |
| while (true) { | |
| if (fix_arc) { | |
| double d = pt.Distance(pnext); | |
| double rr = r * r; | |
| double dd = d * d * 0.25; | |
| double q = rr <= dd ? 0 : sqrt(rr - dd); | |
| double x = (pt.X() + pnext.X()) * 0.5; | |
| double y = (pt.Y() + pnext.Y()) * 0.5; | |
| double dx = q * (pt.Y() - pnext.Y()) / d; | |
| double dy = q * (pnext.X() - pt.X()) / d; | |
| gp_Pnt newCenter(x + dx, y + dy, 0); | |
| if (IsLeft(pt, pnext, center) != IsLeft(pt, pnext, newCenter)) { | |
| newCenter.SetX(x - dx); | |
| newCenter.SetY(y - dy); | |
| } | |
| AREA_WARN( | |
| "Arc correction: " << r << ", " << r2 << ", center" << AREA_XYZ(center) | |
| << "->" << AREA_XYZ(newCenter) | |
| ); | |
| center = newCenter; | |
| } | |
| gp_Ax2 axis(center, gp_Dir(0, 0, v.m_type)); | |
| try { | |
| auto edge = BRepBuilderAPI_MakeEdge(gp_Circ(axis, r), pt, pnext).Edge(); | |
| hEdges->Append(edge); | |
| break; | |
| } | |
| catch (Standard_Failure& e) { | |
| if (!fix_arc) { | |
| fix_arc = true; | |
| AREA_WARN("OCC exception on making arc: " << e.GetMessageString()); | |
| } | |
| else { | |
| AREA_ERR("OCC exception on making arc: " << e.GetMessageString()); | |
| throw; | |
| } | |
| } | |
| } | |
| } | |
| pt = pnext; | |
| } | |
| ShapeAnalysis_FreeBounds::ConnectEdgesToWires(hEdges, Precision::Confusion(), Standard_False, hWires); | |
| if (!hWires->Length()) { | |
| return shape; | |
| } | |
| if (hWires->Length() == 1) { | |
| shape = hWires->Value(1); | |
| } | |
| else { | |
| BRep_Builder builder; | |
| TopoDS_Compound compound; | |
| builder.MakeCompound(compound); | |
| for (int i = 1; i <= hWires->Length(); ++i) { | |
| builder.Add(compound, hWires->Value(i)); | |
| } | |
| shape = compound; | |
| } | |
| if (trsf) { | |
| shape.Move(TopLoc_Location(*trsf)); | |
| } | |
| return shape; | |
| } | |
| TopoDS_Shape Area::toShape(const CArea& area, bool fill, const gp_Trsf* trsf, int reorient) | |
| { | |
| BRep_Builder builder; | |
| TopoDS_Compound compound; | |
| builder.MakeCompound(compound); | |
| for (const CCurve& c : area.m_curves) { | |
| const auto& wire = toShape(c, trsf, reorient); | |
| if (!wire.IsNull()) { | |
| builder.Add(compound, wire); | |
| } | |
| } | |
| TopExp_Explorer xp(compound, TopAbs_EDGE); | |
| if (!xp.More()) { | |
| return TopoDS_Shape(); | |
| } | |
| if (fill) { | |
| try { | |
| FC_TIME_INIT(t); | |
| Part::FaceMakerBullseye mkFace; | |
| if (trsf) { | |
| mkFace.setPlane(gp_Pln().Transformed(*trsf)); | |
| } | |
| for (TopExp_Explorer it(compound, TopAbs_WIRE); it.More(); it.Next()) { | |
| mkFace.addWire(TopoDS::Wire(it.Current())); | |
| } | |
| mkFace.Build(); | |
| if (mkFace.Shape().IsNull()) { | |
| AREA_WARN("FaceMakerBullseye returns null shape"); | |
| } | |
| FC_TIME_LOG(t, "makeFace"); | |
| return mkFace.Shape(); | |
| } | |
| catch (Base::Exception& e) { | |
| AREA_WARN("FaceMakerBullseye failed: " << e.what()); | |
| } | |
| } | |
| return TopoDS_Shape(std::move(compound)); | |
| } | |
| struct WireInfo | |
| { | |
| TopoDS_Wire wire; | |
| std::deque<gp_Pnt> points; | |
| gp_Pnt pt_end; | |
| bool isClosed; | |
| inline const gp_Pnt& pstart() const | |
| { | |
| return points.front(); | |
| } | |
| inline const gp_Pnt& pend() const | |
| { | |
| return isClosed ? pstart() : pt_end; | |
| } | |
| }; | |
| using Wires = std::list<WireInfo>; | |
| using RValue = std::pair<Wires::iterator, size_t>; | |
| struct RGetter | |
| { | |
| using result_type = const gp_Pnt&; | |
| result_type operator()(const RValue& v) const | |
| { | |
| return v.first->points[v.second]; | |
| } | |
| }; | |
| using RTree = bgi::rtree<RValue, RParameters, RGetter>; | |
| struct ShapeParams | |
| { | |
| double abscissa; | |
| int k; | |
| short orientation; | |
| short direction; | |
| FC_DURATION_DECLARE(qd); // rtree query duration | |
| FC_DURATION_DECLARE(bd); // rtree build duration | |
| FC_DURATION_DECLARE(rd); // rtree remove duration | |
| FC_DURATION_DECLARE(xd); // BRepExtrema_DistShapeShape duration | |
| ShapeParams(double _a, int _k, short o, short d) | |
| : abscissa(_a) | |
| , k(_k) | |
| , orientation(o) | |
| , direction(d) | |
| { | |
| FC_DURATION_INIT3(qd, bd, rd); | |
| FC_DURATION_INIT(xd); | |
| } | |
| }; | |
| bool operator<(const Wires::iterator& a, const Wires::iterator& b) | |
| { | |
| return &(*a) < &(*b); | |
| } | |
| using RResults = std::map<Wires::iterator, size_t>; | |
| struct GetWires | |
| { | |
| Wires& wires; | |
| RTree& rtree; | |
| ShapeParams& params; | |
| GetWires(std::list<WireInfo>& ws, RTree& rt, ShapeParams& rp) | |
| : wires(ws) | |
| , rtree(rt) | |
| , params(rp) | |
| {} | |
| void operator()(const TopoDS_Shape& shape, int type) | |
| { | |
| wires.emplace_back(); | |
| WireInfo& info = wires.back(); | |
| if (type == TopAbs_WIRE) { | |
| info.wire = TopoDS::Wire(shape); | |
| } | |
| else { | |
| info.wire = BRepBuilderAPI_MakeWire(TopoDS::Edge(shape)).Wire(); | |
| } | |
| info.isClosed = BRep_Tool::IsClosed(info.wire); | |
| if (info.isClosed && params.orientation == Area::OrientationReversed) { | |
| info.wire.Reverse(); | |
| } | |
| FC_TIME_INIT(t); | |
| if (params.abscissa < Precision::Confusion() || !info.isClosed) { | |
| gp_Pnt p1, p2; | |
| getEndPoints(info.wire, p1, p2); | |
| if (!info.isClosed && params.direction != Area::DirectionNone) { | |
| bool reverse = false; | |
| switch (params.direction) { | |
| case Area::DirectionXPositive: | |
| reverse = p1.X() > p2.X(); | |
| break; | |
| case Area::DirectionXNegative: | |
| reverse = p1.X() < p2.X(); | |
| break; | |
| case Area::DirectionYPositive: | |
| reverse = p1.Y() > p2.Y(); | |
| break; | |
| case Area::DirectionYNegative: | |
| reverse = p1.Y() < p2.Y(); | |
| break; | |
| case Area::DirectionZPositive: | |
| reverse = p1.Z() > p2.Z(); | |
| break; | |
| case Area::DirectionZNegative: | |
| reverse = p1.Z() < p2.Z(); | |
| break; | |
| } | |
| if (reverse) { | |
| info.wire.Reverse(); | |
| std::swap(p1, p2); | |
| } | |
| } | |
| // We don't add in-between vertices of an open wire, because we | |
| // haven't implemented open wire breaking yet. | |
| info.points.push_back(p1); | |
| if (!info.isClosed && params.direction == Area::DirectionNone) { | |
| info.points.push_back(p2); | |
| } | |
| info.pt_end = p2; | |
| } | |
| else { | |
| // For closed wires, we are can easily rebase the wire, so we | |
| // discretize the wires to spatial index it in order to accelerate | |
| // nearest point searching | |
| for (BRepTools_WireExplorer xp(info.wire); xp.More(); xp.Next()) { | |
| // push the head point | |
| info.points.push_back(BRep_Tool::Pnt(xp.CurrentVertex())); | |
| BRepAdaptor_Curve curve(xp.Current()); | |
| GCPnts_UniformAbscissa discretizer( | |
| curve, | |
| params.abscissa, | |
| curve.FirstParameter(), | |
| curve.LastParameter() | |
| ); | |
| if (discretizer.IsDone()) { | |
| int nbPoints = discretizer.NbPoints(); | |
| // OCC discretizer uses one-based index, so index one is | |
| // the first point. The tail point is added later, and the | |
| // head point is the tail of the previous edge. So We can | |
| // exclude the head and tail points, which is convenient | |
| // since we don't need to check the orientation of the | |
| // edge. | |
| for (int i = 2; i < nbPoints; i++) { | |
| info.points.push_back(curve.Value(discretizer.Parameter(i))); | |
| } | |
| } | |
| else { | |
| AREA_WARN("discretizer failed"); | |
| } | |
| } | |
| // no need to push the final tail point, since it's a closed wire | |
| // info.points.push_back(BRep_Tool::Pnt(xp.CurrentVertex())); | |
| } | |
| auto it = wires.end(); | |
| --it; | |
| for (size_t i = 0, count = it->points.size(); i < count; ++i) { | |
| rtree.insert(RValue(it, i)); | |
| } | |
| FC_DURATION_PLUS(params.bd, t); | |
| } | |
| }; | |
| struct ShapeInfo | |
| { | |
| gp_Pln myPln; | |
| Wires myWires; | |
| RTree myRTree; | |
| TopoDS_Shape myShape; | |
| gp_Pnt myBestPt; | |
| gp_Pnt myStartPt; | |
| Wires::iterator myBestWire; | |
| TopoDS_Shape mySupport; | |
| ShapeParams& myParams; | |
| Standard_Real myBestParameter; | |
| bool mySupportEdge; | |
| bool myPlanar; | |
| bool myRebase; | |
| bool myStart; | |
| ShapeInfo(const gp_Pln& pln, const TopoDS_Shape& shape, ShapeParams& params) | |
| : myPln(pln) | |
| , myShape(shape) | |
| , myStartPt(1e20, 1e20, 1e20) | |
| , myParams(params) | |
| , myBestParameter(0) | |
| , mySupportEdge(false) | |
| , myPlanar(true) | |
| , myRebase(false) | |
| , myStart(false) | |
| {} | |
| ShapeInfo(const TopoDS_Shape& shape, ShapeParams& params) | |
| : myShape(shape) | |
| , myStartPt(1e20, 1e20, 1e20) | |
| , myParams(params) | |
| , myBestParameter(0) | |
| , mySupportEdge(false) | |
| , myPlanar(false) | |
| , myRebase(false) | |
| , myStart(false) | |
| {} | |
| double nearest(const gp_Pnt& pt) | |
| { | |
| myStartPt = pt; | |
| if (myWires.empty()) { | |
| foreachSubshape(myShape, GetWires(myWires, myRTree, myParams), TopAbs_WIRE); | |
| } | |
| // Now find the true nearest point among the wires returned. Currently | |
| // only closed wire has a true nearest point, using OCC's | |
| // BRepExtrema_DistShapeShape. We don't do this on open wires, because | |
| // we haven't implemented wire breaking on open wire yet, and I doubt | |
| // its usefulness. | |
| RResults ret; | |
| { | |
| FC_TIME_INIT(t); | |
| myRTree.query(bgi::nearest(pt, myParams.k), bgi::inserter(ret)); | |
| FC_DURATION_PLUS(myParams.qd, t); | |
| } | |
| TopoDS_Shape v = BRepBuilderAPI_MakeVertex(pt); | |
| bool first = true; | |
| double best_d = 1e20; | |
| myBestWire = myWires.begin(); | |
| for (auto r : ret) { | |
| Wires::iterator it = r.first; | |
| const TopoDS_Shape& wire = it->wire; | |
| TopoDS_Shape support; | |
| bool support_edge; | |
| double d = 0; | |
| gp_Pnt p; | |
| bool done = false; | |
| bool is_start = false; | |
| if (BRep_Tool::IsClosed(wire)) { | |
| FC_TIME_INIT(t); | |
| BRepExtrema_DistShapeShape extss(v, wire); | |
| if (extss.IsDone() && extss.NbSolution()) { | |
| d = extss.Value(); | |
| d *= d; | |
| p = extss.PointOnShape2(1); | |
| support = extss.SupportOnShape2(1); | |
| support_edge = extss.SupportTypeShape2(1) == BRepExtrema_IsOnEdge; | |
| if (support_edge) { | |
| extss.ParOnEdgeS2(1, myBestParameter); | |
| } | |
| done = true; | |
| } | |
| else { | |
| AREA_WARN("BRepExtrema_DistShapeShape failed"); | |
| } | |
| FC_DURATION_PLUS(myParams.xd, t); | |
| } | |
| if (!done) { | |
| double d1 = pt.SquareDistance(it->pstart()); | |
| if (myParams.direction != Area::DirectionNone) { | |
| d = d1; | |
| p = it->pstart(); | |
| is_start = true; | |
| } | |
| else { | |
| double d2 = pt.SquareDistance(it->pend()); | |
| if (d1 < d2) { | |
| d = d1; | |
| p = it->pstart(); | |
| is_start = true; | |
| } | |
| else { | |
| d = d2; | |
| p = it->pend(); | |
| is_start = false; | |
| } | |
| } | |
| } | |
| if (!first && d >= best_d) { | |
| continue; | |
| } | |
| first = false; | |
| myBestPt = p; | |
| myBestWire = it; | |
| best_d = d; | |
| myRebase = done; | |
| myStart = is_start; | |
| if (done) { | |
| mySupport = support; | |
| mySupportEdge = support_edge; | |
| } | |
| } | |
| return best_d; | |
| } | |
| // Assumes nearest() has been called. Rebased the best wire | |
| // to begin with the best point. Currently only works with closed wire | |
| TopoDS_Shape rebaseWire(gp_Pnt& pend, double min_dist) | |
| { | |
| min_dist *= min_dist; | |
| BRepBuilderAPI_MakeWire mkWire; | |
| TopoDS_Shape estart; | |
| TopoDS_Edge eend; | |
| for (int state = 0; state < 3; ++state) { | |
| BRepTools_WireExplorer xp(TopoDS::Wire(myBestWire->wire)); | |
| gp_Pnt pprev(BRep_Tool::Pnt(xp.CurrentVertex())); | |
| // checking the case of bestpoint == wire start | |
| if (state == 0 && !mySupportEdge | |
| && pprev.SquareDistance(myBestPt) <= Precision::SquareConfusion()) { | |
| pend = myBestWire->pend(); | |
| return myBestWire->wire; | |
| } | |
| gp_Pnt pt; | |
| for (; xp.More(); xp.Next(), pprev = pt) { | |
| const auto& edge = xp.Current(); | |
| // state==2 means we are in second pass. estart marks the new | |
| // start of the wire. so seeing estart means we're done | |
| if (state == 2 && estart.IsEqual(edge)) { | |
| break; | |
| } | |
| // Edge split not working if using BRepAdaptor_Curve. | |
| // BRepBuilderAPI_MakeEdge always fails with | |
| // PointProjectionFailed. Why?? | |
| Standard_Real first, last; | |
| Handle(Geom_Curve) curve = BRep_Tool::Curve(edge, first, last); | |
| pt = curve->Value(last); | |
| bool reversed; | |
| if (pprev.SquareDistance(pt) <= Precision::SquareConfusion()) { | |
| reversed = true; | |
| pt = curve->Value(first); | |
| } | |
| else { | |
| reversed = false; | |
| } | |
| // state!=0 means we've found the new start of wire, now just | |
| // keep adding new edges | |
| if (state) { | |
| mkWire.Add(edge); | |
| continue; | |
| } | |
| // state==0 means we are looking for the new start | |
| if (mySupportEdge) { | |
| // if best point is on some edge, split the edge in half | |
| if (edge.IsEqual(mySupport)) { | |
| // to fix PointProjectionFailed. | |
| GeomAPI_ProjectPointOnCurve gpp; | |
| gpp.Init(myBestPt, curve); | |
| gpp.Perform(myBestPt); | |
| myBestPt = gpp.NearestPoint(); | |
| gpp.Perform(pprev); | |
| pprev = gpp.NearestPoint(); | |
| gpp.Perform(pt); | |
| pt = gpp.NearestPoint(); | |
| double d1 = pprev.SquareDistance(myBestPt); | |
| double d2 = pt.SquareDistance(myBestPt); | |
| if (d1 > min_dist && d2 > min_dist) { | |
| BRepBuilderAPI_MakeEdge mkEdge1, mkEdge2; | |
| if (reversed) { | |
| mkEdge1.Init(curve, myBestPt, pprev); | |
| mkEdge2.Init(curve, pt, myBestPt); | |
| } | |
| else { | |
| mkEdge1.Init(curve, pprev, myBestPt); | |
| mkEdge2.Init(curve, myBestPt, pt); | |
| } | |
| if (mkEdge1.IsDone() && mkEdge2.IsDone()) { | |
| if (reversed) { | |
| eend = TopoDS::Edge(mkEdge1.Edge().Reversed()); | |
| mkWire.Add(TopoDS::Edge(mkEdge2.Edge().Reversed())); | |
| } | |
| else { | |
| eend = mkEdge1.Edge(); | |
| mkWire.Add(mkEdge2.Edge()); | |
| } | |
| pend = myBestPt; | |
| estart = mySupport; | |
| state = 1; | |
| continue; | |
| } | |
| AREA_WARN( | |
| (reversed ? "reversed " : "") | |
| << "edge split failed " << AREA_XYZ(pprev) << ", " | |
| << AREA_XYZ(myBestPt) << ", " << AREA_XYZ(pt) << ", " << d1 << ", " | |
| << d2 << ", err: " << mkEdge1.Error() << ", " << mkEdge2.Error() | |
| ); | |
| } | |
| if (d1 < d2) { | |
| pend = pprev; | |
| estart = edge; | |
| state = 1; | |
| mkWire.Add(edge); | |
| } | |
| else { | |
| mySupportEdge = false; | |
| myBestPt = pt; | |
| continue; | |
| } | |
| } | |
| } | |
| else if (myBestPt.SquareDistance(pprev) <= Precision::SquareConfusion()) { | |
| pend = pprev; | |
| estart = edge; | |
| state = 1; | |
| mkWire.Add(edge); | |
| } | |
| } | |
| if (state == 0) { | |
| AREA_WARN("edge break point not found"); | |
| pend = myBestWire->pend(); | |
| return myBestWire->wire; | |
| } | |
| } | |
| if (!eend.IsNull()) { | |
| mkWire.Add(eend); | |
| } | |
| if (mkWire.IsDone()) { | |
| return mkWire.Wire(); | |
| } | |
| AREA_WARN("wire rebase failed"); | |
| pend = myBestWire->pend(); | |
| return myBestWire->wire; | |
| } | |
| std::list<TopoDS_Shape> sortWires( | |
| const gp_Pnt& pstart, | |
| gp_Pnt& pend, | |
| double min_dist, | |
| double max_dist, | |
| gp_Pnt* pentry | |
| ) | |
| { | |
| std::list<TopoDS_Shape> wires; | |
| if (myWires.empty() || pstart.SquareDistance(myStartPt) > Precision::SquareConfusion()) { | |
| nearest(pstart); | |
| if (myWires.empty()) { | |
| return wires; | |
| } | |
| } | |
| if (pentry) { | |
| *pentry = myBestPt; | |
| } | |
| if (min_dist < 0.01) { | |
| min_dist = 0.01; | |
| } | |
| while (true) { | |
| if (myRebase) { | |
| pend = myBestPt; | |
| wires.push_back(rebaseWire(pend, min_dist)); | |
| } | |
| else if (!myStart) { | |
| wires.push_back(myBestWire->wire.Reversed()); | |
| pend = myBestWire->pstart(); | |
| } | |
| else { | |
| wires.push_back(myBestWire->wire); | |
| pend = myBestWire->pend(); | |
| } | |
| FC_TIME_INIT(t); | |
| for (size_t i = 0, count = myBestWire->points.size(); i < count; ++i) { | |
| myRTree.remove(RValue(myBestWire, i)); | |
| } | |
| FC_DURATION_PLUS(myParams.rd, t); | |
| myWires.erase(myBestWire); | |
| if (myWires.empty()) { | |
| break; | |
| } | |
| double d = nearest(pend); | |
| if (max_dist > 0 && d > max_dist) { | |
| break; | |
| } | |
| } | |
| return wires; | |
| } | |
| }; | |
| struct ShapeInfoBuilder | |
| { | |
| std::list<ShapeInfo>& myList; | |
| gp_Trsf& myTrsf; | |
| short& myArcPlane; | |
| bool& myArcPlaneFound; | |
| ShapeParams& myParams; | |
| ShapeInfoBuilder( | |
| bool& plane_found, | |
| short& arc_plane, | |
| gp_Trsf& trsf, | |
| std::list<ShapeInfo>& list, | |
| ShapeParams& params | |
| ) | |
| : myList(list) | |
| , myTrsf(trsf) | |
| , myArcPlane(arc_plane) | |
| , myArcPlaneFound(plane_found) | |
| , myParams(params) | |
| {} | |
| void operator()(const TopoDS_Shape& shape, int type) | |
| { | |
| gp_Pln pln; | |
| if (!getShapePlane(shape, pln)) { | |
| myList.emplace_back(shape, myParams); | |
| return; | |
| } | |
| myList.emplace_back(pln, shape, myParams); | |
| if (myArcPlaneFound || myArcPlane == Area::ArcPlaneNone | |
| || myArcPlane == Area::ArcPlaneVariable) { | |
| return; | |
| } | |
| if (type == TopAbs_EDGE) { | |
| BRepAdaptor_Curve curve(TopoDS::Edge(shape)); | |
| if (curve.GetType() != GeomAbs_Circle) { | |
| return; | |
| } | |
| } | |
| else { | |
| bool found = false; | |
| for (TopExp_Explorer it(shape, TopAbs_EDGE); it.More(); it.Next()) { | |
| BRepAdaptor_Curve curve(TopoDS::Edge(it.Current())); | |
| if (curve.GetType() == GeomAbs_Circle) { | |
| found = true; | |
| break; | |
| } | |
| } | |
| if (!found) { | |
| return; | |
| } | |
| } | |
| gp_Ax3 pos = myList.back().myPln.Position(); | |
| if (!pos.Direct()) { | |
| pos = gp_Ax3(pos.Ax2()); | |
| } | |
| const gp_Dir& dir = pos.Direction(); | |
| gp_Ax3 dstPos; | |
| bool x0 = fabs(dir.X()) <= Precision::Confusion(); | |
| bool y0 = fabs(dir.Y()) <= Precision::Confusion(); | |
| bool z0 = fabs(dir.Z()) <= Precision::Confusion(); | |
| switch (myArcPlane) { | |
| case Area::ArcPlaneAuto: { | |
| if (x0 && y0) { | |
| AREA_TRACE("found arc plane XY"); | |
| myArcPlane = Area::ArcPlaneXY; | |
| } | |
| else if (x0 && z0) { | |
| AREA_TRACE("found arc plane ZX"); | |
| myArcPlane = Area::ArcPlaneZX; | |
| } | |
| else if (z0 && y0) { | |
| AREA_TRACE("found arc plane YZ"); | |
| myArcPlane = Area::ArcPlaneYZ; | |
| } | |
| else { | |
| myArcPlane = Area::ArcPlaneXY; | |
| dstPos = gp_Ax3(pos.Location(), gp_Dir(0, 0, 1)); | |
| break; | |
| } | |
| myArcPlaneFound = true; | |
| return; | |
| } | |
| case Area::ArcPlaneXY: | |
| if (x0 && y0) { | |
| myArcPlaneFound = true; | |
| return; | |
| } | |
| dstPos = gp_Ax3(pos.Location(), gp_Dir(0, 0, 1)); | |
| break; | |
| case Area::ArcPlaneZX: | |
| if (x0 && z0) { | |
| myArcPlaneFound = true; | |
| return; | |
| } | |
| dstPos = gp_Ax3(pos.Location(), gp_Dir(0, 1, 0)); | |
| break; | |
| case Area::ArcPlaneYZ: | |
| if (z0 && y0) { | |
| myArcPlaneFound = true; | |
| return; | |
| } | |
| dstPos = gp_Ax3(pos.Location(), gp_Dir(1, 0, 0)); | |
| break; | |
| default: | |
| return; | |
| } | |
| AREA_WARN("force arc plane " << AREA_XYZ(dir) << " to " << AREA_XYZ(dstPos.Direction())); | |
| myTrsf.SetTransformation(pos); | |
| gp_Trsf trsf; | |
| trsf.SetTransformation(dstPos); | |
| myTrsf.PreMultiply(trsf.Inverted()); | |
| myArcPlaneFound = true; | |
| } | |
| }; | |
| struct WireOrienter | |
| { | |
| std::list<TopoDS_Shape>& wires; | |
| const gp_Dir& dir; | |
| short orientation; | |
| short direction; | |
| WireOrienter(std::list<TopoDS_Shape>& ws, const gp_Dir& dir, short o, short d) | |
| : wires(ws) | |
| , dir(dir) | |
| , orientation(o) | |
| , direction(d) | |
| {} | |
| void operator()(const TopoDS_Shape& shape, int type) | |
| { | |
| if (type == TopAbs_WIRE) { | |
| wires.push_back(shape); | |
| } | |
| else { | |
| wires.push_back(BRepBuilderAPI_MakeWire(TopoDS::Edge(shape)).Wire()); | |
| } | |
| TopoDS_Shape& wire = wires.back(); | |
| if (BRep_Tool::IsClosed(wire)) { | |
| if (orientation == Area::OrientationReversed) { | |
| wire.Reverse(); | |
| } | |
| } | |
| else if (direction != Area::DirectionNone) { | |
| gp_Pnt p1, p2; | |
| getEndPoints(TopoDS::Wire(wire), p1, p2); | |
| bool reverse = false; | |
| switch (direction) { | |
| case Area::DirectionXPositive: | |
| reverse = p1.X() > p2.X(); | |
| break; | |
| case Area::DirectionXNegative: | |
| reverse = p1.X() < p2.X(); | |
| break; | |
| case Area::DirectionYPositive: | |
| reverse = p1.Y() > p2.Y(); | |
| break; | |
| case Area::DirectionYNegative: | |
| reverse = p1.Y() < p2.Y(); | |
| break; | |
| case Area::DirectionZPositive: | |
| reverse = p1.Z() > p2.Z(); | |
| break; | |
| case Area::DirectionZNegative: | |
| reverse = p1.Z() < p2.Z(); | |
| break; | |
| } | |
| if (reverse) { | |
| wire.Reverse(); | |
| } | |
| } | |
| } | |
| }; | |
| typedef Standard_Real (gp_Pnt::*AxisGetter)() const; | |
| typedef void (gp_Pnt::*AxisSetter)(Standard_Real); | |
| std::list<TopoDS_Shape> Area::sortWires( | |
| const std::list<TopoDS_Shape>& shapes, | |
| bool has_start, | |
| gp_Pnt* _pstart, | |
| gp_Pnt* _pend, | |
| double* stepdown_hint, | |
| short* _parc_plane, | |
| PARAM_ARGS(PARAM_FARG, AREA_PARAMS_SORT) | |
| ) | |
| { | |
| std::list<TopoDS_Shape> wires; | |
| if (shapes.empty()) { | |
| return wires; | |
| } | |
| AxisGetter getter; | |
| AxisSetter setter; | |
| switch (retract_axis) { | |
| case RetractAxisX: | |
| getter = &gp_Pnt::X; | |
| setter = &gp_Pnt::SetX; | |
| break; | |
| case RetractAxisY: | |
| getter = &gp_Pnt::Y; | |
| setter = &gp_Pnt::SetY; | |
| break; | |
| default: | |
| getter = &gp_Pnt::Z; | |
| setter = &gp_Pnt::SetZ; | |
| } | |
| if (sort_mode == SortModeGreedy && threshold < Precision::Confusion()) { | |
| AREA_WARN("Sort mode 'Greedy' requires a threshold value (suggestion: use the tool diameter)"); | |
| sort_mode = SortMode2D5; | |
| } | |
| short _arc_plane = ArcPlaneNone; | |
| short& arc_plane = _parc_plane ? *_parc_plane : _arc_plane; | |
| if (sort_mode == SortModeNone) { | |
| gp_Dir dir; | |
| switch (arc_plane) { | |
| case ArcPlaneYZ: | |
| dir = gp_Dir(1, 0, 0); | |
| break; | |
| case ArcPlaneZX: | |
| dir = gp_Dir(0, 1, 0); | |
| break; | |
| default: | |
| if (arc_plane != ArcPlaneXY) { | |
| AREA_WARN("Sort mode 'None' without a given arc plane, using XY plane"); | |
| } | |
| arc_plane = ArcPlaneXY; | |
| dir = gp_Dir(0, 0, 1); | |
| break; | |
| } | |
| for (auto& shape : shapes) { | |
| if (!shape.IsNull()) { | |
| foreachSubshape(shape, WireOrienter(wires, dir, orientation, direction), TopAbs_WIRE); | |
| } | |
| } | |
| return wires; | |
| } | |
| ShapeParams rparams(abscissa, nearest_k > 0 ? nearest_k : 1, orientation, direction); | |
| std::list<ShapeInfo> shape_list; | |
| FC_TIME_INIT2(t, t1); | |
| gp_Trsf trsf; | |
| bool arcPlaneFound = false; | |
| if (sort_mode == SortMode3D) { | |
| TopoDS_Builder builder; | |
| TopoDS_Compound comp; | |
| builder.MakeCompound(comp); | |
| for (auto& shape : shapes) { | |
| if (!shape.IsNull()) { | |
| builder.Add(comp, shape); | |
| } | |
| } | |
| TopExp_Explorer xp(comp, TopAbs_EDGE); | |
| if (xp.More()) { | |
| shape_list.emplace_back(comp, rparams); | |
| } | |
| } | |
| else { | |
| // first pass, find plane of each shape | |
| for (auto& shape : shapes) { | |
| // explode the shape | |
| if (!shape.IsNull()) { | |
| foreachSubshape( | |
| shape, | |
| ShapeInfoBuilder(arcPlaneFound, arc_plane, trsf, shape_list, rparams), | |
| TopAbs_FACE, | |
| true | |
| ); | |
| } | |
| } | |
| FC_TIME_LOG(t1, "plane finding"); | |
| } | |
| if (shape_list.empty()) { | |
| return wires; | |
| } | |
| Bnd_Box bounds; | |
| gp_Pnt pstart, pend; | |
| if (_pstart) { | |
| pstart = *_pstart; | |
| } | |
| bool use_bound = !has_start || !_pstart; | |
| // Second stage, group shape by its plane, and find overall boundary | |
| for (auto& info : shape_list) { | |
| if (arcPlaneFound) { | |
| info.myShape.Move(trsf); | |
| if (info.myPlanar) { | |
| info.myPln.Transform(trsf); | |
| } | |
| } | |
| BRepBndLib::Add(info.myShape, bounds, Standard_False); | |
| } | |
| if (use_bound || sort_mode == SortMode2D5 || sort_mode == SortModeGreedy) { | |
| for (auto itNext = shape_list.begin(), it = itNext; it != shape_list.end(); it = itNext) { | |
| ++itNext; | |
| if (!it->myPlanar) { | |
| continue; | |
| } | |
| TopoDS_Builder builder; | |
| TopoDS_Compound comp; | |
| builder.MakeCompound(comp); | |
| bool empty = true; | |
| for (auto itNext3 = itNext, itNext2 = itNext; itNext2 != shape_list.end(); | |
| itNext2 = itNext3) { | |
| ++itNext3; | |
| if (!itNext2->myPlanar | |
| || !it->myPln.Position().IsCoplanar( | |
| itNext2->myPln.Position(), | |
| Precision::Confusion(), | |
| Precision::Confusion() | |
| )) { | |
| continue; | |
| } | |
| if (itNext == itNext2) { | |
| ++itNext; | |
| } | |
| builder.Add(comp, itNext2->myShape); | |
| shape_list.erase(itNext2); | |
| empty = false; | |
| } | |
| if (!empty) { | |
| builder.Add(comp, it->myShape); | |
| it->myShape = comp; | |
| } | |
| } | |
| FC_TIME_LOG(t, "plane merging"); | |
| } | |
| bounds.SetGap(0.0); | |
| Standard_Real xMin, yMin, zMin, xMax, yMax, zMax; | |
| bounds.Get(xMin, yMin, zMin, xMax, yMax, zMax); | |
| AREA_TRACE( | |
| "bound (" << xMin << ", " << xMax << "), (" << yMin << ", " << yMax << "), (" << zMin | |
| << ", " << zMax << ')' | |
| ); | |
| if (use_bound) { | |
| pstart.SetCoord(xMax, yMax, zMax); | |
| if (_pstart) { | |
| *_pstart = pstart; | |
| } | |
| } | |
| else { | |
| switch (retract_axis) { | |
| case RetractAxisX: | |
| if (pstart.X() < xMax) { | |
| pstart.SetX(xMax); | |
| } | |
| break; | |
| case RetractAxisY: | |
| if (pstart.Y() < yMax) { | |
| pstart.SetY(yMax); | |
| } | |
| break; | |
| default: | |
| if (pstart.Z() < zMax) { | |
| pstart.SetZ(zMax); | |
| } | |
| } | |
| if (_pstart) { | |
| *_pstart = pstart; | |
| } | |
| } | |
| gp_Pln pln; | |
| double hint = 0.0; | |
| bool hint_first = true; | |
| auto current_it = shape_list.end(); | |
| double current_height = (pstart.*getter)(); | |
| double max_dist = sort_mode == SortModeGreedy ? threshold * threshold : 0; | |
| while (!shape_list.empty()) { | |
| AREA_TRACE("sorting " << shape_list.size() << ' ' << AREA_XYZ(pstart)); | |
| double best_d = std::numeric_limits<double>::max(); | |
| auto best_it = shape_list.begin(); | |
| for (auto it = best_it; it != shape_list.end(); ++it) { | |
| double d; | |
| gp_Pnt pt; | |
| if (it->myPlanar && current_it == shape_list.end()) { | |
| d = it->myPln.SquareDistance(pstart); | |
| } | |
| else { | |
| d = it->nearest(pstart); | |
| } | |
| if (d < best_d) { | |
| best_it = it; | |
| best_d = d; | |
| } | |
| } | |
| gp_Pnt pentry; | |
| if (sort_mode == SortModeGreedy) { | |
| // greedy sort will go down to the next layer even if the current | |
| // layer is not finished, as long as the path jump distance is | |
| // within the threshold. | |
| if (current_it == shape_list.end()) { | |
| current_it = best_it; | |
| current_height = (pstart.*getter)(); | |
| } | |
| else if (best_d > max_dist) { | |
| // If the path jumps beyond the threshold, bail out and go back | |
| // to the first unfinished layer. | |
| (pstart.*setter)(current_height); | |
| current_it->nearest(pstart); | |
| best_it = current_it; | |
| } | |
| } | |
| wires.splice(wires.end(), best_it->sortWires(pstart, pend, min_dist, max_dist, &pentry)); | |
| if (use_bound && _pstart) { | |
| use_bound = false; | |
| *_pstart = pentry; | |
| } | |
| if ((sort_mode == SortMode2D5 || sort_mode == SortMode3D) && stepdown_hint) { | |
| if (!best_it->myPlanar) { | |
| hint_first = true; | |
| } | |
| else if (hint_first) { | |
| hint_first = false; | |
| } | |
| else { | |
| // Calculate distance of two gp_pln. | |
| // | |
| // Can't use gp_pln.Distance(), because it only calculate | |
| // the distance if two plane are parallel. And it checks | |
| // parallelity using tolerance gp::Resolution() which is | |
| // defined as DBL_MIN (min double) in Standard_Real.hxx. | |
| // Really? Is that a bug? | |
| const gp_Pnt& P = pln.Position().Location(); | |
| const gp_Pnt& loc = best_it->myPln.Position().Location(); | |
| const gp_Dir& dir = best_it->myPln.Position().Direction(); | |
| double d | |
| = (dir.X() * (P.X() - loc.X()) + dir.Y() * (P.Y() - loc.Y()) | |
| + dir.Z() * (P.Z() - loc.Z())); | |
| if (d < 0) { | |
| d = -d; | |
| } | |
| if (d > hint) { | |
| hint = d; | |
| } | |
| } | |
| pln = best_it->myPln; | |
| } | |
| pstart = pend; | |
| if (best_it->myWires.empty()) { | |
| if (current_it == best_it) { | |
| current_it = shape_list.end(); | |
| } | |
| shape_list.erase(best_it); | |
| } | |
| } | |
| if (stepdown_hint && hint != 0.0) { | |
| *stepdown_hint = hint; | |
| } | |
| if (_pend) { | |
| *_pend = pend; | |
| } | |
| FC_DURATION_LOG(rparams.bd, "rtree build"); | |
| FC_DURATION_LOG(rparams.qd, "rtree query"); | |
| FC_DURATION_LOG(rparams.rd, "rtree clean"); | |
| FC_DURATION_LOG(rparams.xd, "BRepExtrema"); | |
| FC_TIME_LOG(t, "sortWires total"); | |
| return wires; | |
| } | |
| static inline void addParameter( | |
| bool verbose, | |
| Command& cmd, | |
| const char* name, | |
| double last, | |
| double next, | |
| bool relative = false | |
| ) | |
| { | |
| double d = next - last; | |
| if (verbose || fabs(d) > Precision::Confusion()) { | |
| cmd.Parameters[name] = relative ? d : next; | |
| } | |
| } | |
| static inline void addGCode( | |
| bool verbose, | |
| Toolpath& path, | |
| const gp_Pnt& last, | |
| const gp_Pnt& next, | |
| const char* name | |
| ) | |
| { | |
| Command cmd; | |
| cmd.Name = name; | |
| addParameter(verbose, cmd, "X", last.X(), next.X()); | |
| addParameter(verbose, cmd, "Y", last.Y(), next.Y()); | |
| addParameter(verbose, cmd, "Z", last.Z(), next.Z()); | |
| path.addCommand(cmd); | |
| return; | |
| } | |
| static inline void addG1( | |
| bool verbose, | |
| Toolpath& path, | |
| const gp_Pnt& last, | |
| const gp_Pnt& next, | |
| double f, | |
| double& last_f | |
| ) | |
| { | |
| addGCode(verbose, path, last, next, "G1"); | |
| if (f > Precision::Confusion()) { | |
| Command* cmd = path.getCommands().back(); | |
| addParameter(verbose, *cmd, "F", last_f, f); | |
| last_f = f; | |
| } | |
| return; | |
| } | |
| static void addG0( | |
| bool verbose, | |
| Toolpath& path, | |
| gp_Pnt last, | |
| const gp_Pnt& next, | |
| AxisSetter setter, | |
| double height | |
| ) | |
| { | |
| gp_Pnt pt(next); | |
| (pt.*setter)(height); | |
| if (!last.IsEqual(pt, Precision::Confusion())) { | |
| addGCode(verbose, path, last, pt, "G0"); | |
| } | |
| } | |
| static void addGArc( | |
| bool verbose, | |
| bool abs_center, | |
| Toolpath& path, | |
| const gp_Pnt& pstart, | |
| const gp_Pnt& pend, | |
| const gp_Pnt& center, | |
| bool clockwise, | |
| double f, | |
| double& last_f | |
| ) | |
| { | |
| Command cmd; | |
| cmd.Name = clockwise ? "G2" : "G3"; | |
| if (abs_center) { | |
| addParameter(verbose, cmd, "I", 0.0, center.X()); | |
| addParameter(verbose, cmd, "J", 0.0, center.Y()); | |
| addParameter(verbose, cmd, "K", 0.0, center.Z()); | |
| } | |
| else { | |
| addParameter(verbose, cmd, "I", pstart.X(), center.X(), true); | |
| addParameter(verbose, cmd, "J", pstart.Y(), center.Y(), true); | |
| addParameter(verbose, cmd, "K", pstart.Z(), center.Z(), true); | |
| } | |
| addParameter(verbose, cmd, "X", pstart.X(), pend.X()); | |
| addParameter(verbose, cmd, "Y", pstart.Y(), pend.Y()); | |
| addParameter(verbose, cmd, "Z", pstart.Z(), pend.Z()); | |
| if (f > Precision::Confusion()) { | |
| addParameter(verbose, cmd, "F", last_f, f); | |
| last_f = f; | |
| } | |
| path.addCommand(cmd); | |
| } | |
| static inline void addGCode(Toolpath& path, const char* name) | |
| { | |
| Command cmd; | |
| cmd.Name = name; | |
| path.addCommand(cmd); | |
| } | |
| void Area::setWireOrientation(TopoDS_Wire& wire, const gp_Dir& dir, bool wire_ccw) | |
| { | |
| // make a test face | |
| BRepBuilderAPI_MakeFace mkFace(wire, /*onlyplane=*/Standard_True); | |
| if (!mkFace.IsDone()) { | |
| AREA_WARN("setWireOrientation: failed to make test face"); | |
| return; | |
| } | |
| TopoDS_Face tmpFace = mkFace.Face(); | |
| // compare face surface normal with our plane's one | |
| BRepAdaptor_Surface surf(tmpFace); | |
| bool ccw = surf.Plane().Axis().Direction().Dot(dir) > 0; | |
| // unlikely, but just in case OCC decided to reverse our wire for the face... take that into | |
| // account! | |
| TopoDS_Iterator it(tmpFace, /*CumOri=*/Standard_False); | |
| ccw ^= it.Value().Orientation() != wire.Orientation(); | |
| if (ccw != wire_ccw) { | |
| wire.Reverse(); | |
| } | |
| } | |
| void Area::toPath( | |
| Toolpath& path, | |
| const std::list<TopoDS_Shape>& shapes, | |
| const gp_Pnt* _pstart, | |
| gp_Pnt* pend, | |
| PARAM_ARGS(PARAM_FARG, AREA_PARAMS_PATH) | |
| ) | |
| { | |
| std::list<TopoDS_Shape> wires; | |
| gp_Pnt pstart; | |
| if (_pstart) { | |
| pstart = *_pstart; | |
| } | |
| double stepdown_hint = 1.0; | |
| wires = sortWires( | |
| shapes, | |
| _pstart != nullptr, | |
| &pstart, | |
| pend, | |
| &stepdown_hint, | |
| PARAM_REF(PARAM_FARG, AREA_PARAMS_ARC_PLANE), | |
| PARAM_FIELDS(PARAM_FARG, AREA_PARAMS_SORT) | |
| ); | |
| if (wires.empty()) { | |
| return; | |
| } | |
| short currentArcPlane = arc_plane; | |
| if (preamble) { | |
| // absolute mode | |
| addGCode(path, "G90"); | |
| if (abs_center) { | |
| addGCode(path, "G90.1"); // absolute center for arc move | |
| } | |
| if (arc_plane == ArcPlaneZX) { | |
| addGCode(path, "G18"); | |
| } | |
| else if (arc_plane == ArcPlaneYZ) { | |
| addGCode(path, "G19"); | |
| } | |
| else { | |
| currentArcPlane = ArcPlaneXY; | |
| addGCode(path, "G17"); | |
| } | |
| } | |
| AxisGetter getter; | |
| AxisSetter setter; | |
| switch (retract_axis) { | |
| case RetractAxisX: | |
| getter = &gp_Pnt::X; | |
| setter = &gp_Pnt::SetX; | |
| break; | |
| case RetractAxisY: | |
| getter = &gp_Pnt::Y; | |
| setter = &gp_Pnt::SetY; | |
| break; | |
| default: | |
| getter = &gp_Pnt::Z; | |
| setter = &gp_Pnt::SetZ; | |
| } | |
| threshold = fabs(threshold); | |
| if (threshold < Precision::Confusion()) { | |
| threshold = Precision::Confusion(); | |
| } | |
| threshold *= threshold; | |
| // in case the user didn't specify feed start, sortWire() will choose one | |
| // based on the bound. We'll further adjust that according to resume height | |
| if (!_pstart || pstart.SquareDistance(*_pstart) > Precision::SquareConfusion()) { | |
| (pstart.*setter)(resume_height); | |
| } | |
| gp_Pnt plast, p; | |
| // initial vertical rapid pull up to retraction (or start Z height if higher) | |
| (p.*setter)(std::max(retraction, (pstart.*getter)())); | |
| addGCode(false, path, plast, p, "G0"); | |
| plast = p; | |
| p = pstart; | |
| // rapid horizontal move to start point | |
| gp_Pnt tmpPlast = plast; | |
| (tmpPlast.*setter)((p.*getter)()); | |
| if (_pstart && p.IsEqual(tmpPlast, Precision::Confusion())) { | |
| plast.SetCoord(10.0, 10.0, 10.0); | |
| (plast.*setter)(retraction); | |
| } | |
| (p.*setter)(retraction); | |
| addGCode(false, path, plast, p, "G0"); | |
| plast = p; | |
| bool first = true; | |
| bool arcWarned = false; | |
| double cur_f = 0.0; // current feed rate | |
| double nf = fabs(feedrate); // user specified normal move feed rate | |
| double vf = fabs(feedrate_v); // user specified vertical move feed rate | |
| if (vf < Precision::Confusion()) { | |
| vf = nf; | |
| } | |
| for (const TopoDS_Shape& wire : wires) { | |
| BRepTools_WireExplorer xp(TopoDS::Wire(wire)); | |
| p = BRep_Tool::Pnt(xp.CurrentVertex()); | |
| gp_Pnt pTmp(p), plastTmp(plast); | |
| // Assuming the stepdown direction is the same as retraction direction. | |
| // We don't want to count step down distance in stepdown direction, | |
| // because it is always safe to go in that direction in feed move | |
| // without getting bumped. | |
| (pTmp.*setter)(0.0); | |
| (plastTmp.*setter)(0.0); | |
| if (first) { | |
| // G0 to initial at retraction to handle if start point was set | |
| addG0(false, path, plast, p, setter, retraction); | |
| // rapid to plunge height | |
| addG0(false, path, plast, p, setter, resume_height); | |
| } | |
| else if (pTmp.SquareDistance(plastTmp) > threshold) { | |
| // raise to retraction height | |
| addG0(false, path, plast, plast, setter, retraction); | |
| // move to new location | |
| addG0(false, path, plast, p, setter, retraction); | |
| // lower to plunge height | |
| addG0(false, path, plast, p, setter, resume_height); | |
| } | |
| addG1(verbose, path, plast, p, vf, cur_f); | |
| plast = p; | |
| first = false; | |
| for (; xp.More(); xp.Next(), plast = p) { | |
| const auto& edge = xp.Current(); | |
| BRepAdaptor_Curve curve(edge); | |
| bool reversed = (edge.Orientation() == TopAbs_REVERSED); | |
| p = curve.Value(reversed ? curve.FirstParameter() : curve.LastParameter()); | |
| switch (curve.GetType()) { | |
| case GeomAbs_Line: { | |
| if (segmentation > Precision::Confusion()) { | |
| GCPnts_UniformAbscissa discretizer( | |
| curve, | |
| segmentation, | |
| curve.FirstParameter(), | |
| curve.LastParameter() | |
| ); | |
| if (discretizer.IsDone() && discretizer.NbPoints() > 2) { | |
| int nbPoints = discretizer.NbPoints(); | |
| if (reversed) { | |
| for (int i = nbPoints - 1; i >= 1; --i) { | |
| gp_Pnt pt = curve.Value(discretizer.Parameter(i)); | |
| addG1(verbose, path, plast, pt, nf, cur_f); | |
| plast = pt; | |
| } | |
| } | |
| else { | |
| for (int i = 2; i <= nbPoints; i++) { | |
| gp_Pnt pt = curve.Value(discretizer.Parameter(i)); | |
| addG1(verbose, path, plast, pt, nf, cur_f); | |
| plast = pt; | |
| } | |
| } | |
| break; | |
| } | |
| } | |
| addG1(verbose, path, plast, p, nf, cur_f); | |
| break; | |
| } | |
| case GeomAbs_Circle: { | |
| const gp_Circ& circle = curve.Circle(); | |
| const gp_Dir& dir = circle.Axis().Direction(); | |
| short arcPlane = ArcPlaneNone; | |
| bool clockwise; | |
| const char* cmd; | |
| if (fabs(dir.X()) < Precision::Confusion() | |
| && fabs(dir.Y()) < Precision::Confusion()) { | |
| clockwise = dir.Z() < 0; | |
| arcPlane = ArcPlaneXY; | |
| cmd = "G17"; | |
| } | |
| else if (fabs(dir.Z()) < Precision::Confusion() | |
| && fabs(dir.X()) < Precision::Confusion()) { | |
| clockwise = dir.Y() < 0; | |
| arcPlane = ArcPlaneZX; | |
| cmd = "G18"; | |
| } | |
| else if (fabs(dir.Y()) < Precision::Confusion() | |
| && fabs(dir.Z()) < Precision::Confusion()) { | |
| clockwise = dir.X() < 0; | |
| arcPlane = ArcPlaneYZ; | |
| cmd = "G19"; | |
| } | |
| if (arcPlane != ArcPlaneNone | |
| && (arcPlane == currentArcPlane || arc_plane == ArcPlaneVariable)) { | |
| if (arcPlane != currentArcPlane) { | |
| addGCode(path, cmd); | |
| } | |
| if (reversed) { | |
| clockwise = !clockwise; | |
| } | |
| gp_Pnt center = circle.Location(); | |
| double first = curve.FirstParameter(); | |
| double last = curve.LastParameter(); | |
| if (segmentation > Precision::Confusion()) { | |
| GCPnts_UniformAbscissa discretizer(curve, segmentation, first, last); | |
| if (discretizer.IsDone() && discretizer.NbPoints() > 2) { | |
| int nbPoints = discretizer.NbPoints(); | |
| if (reversed) { | |
| for (int i = nbPoints - 1; i >= 1; --i) { | |
| gp_Pnt pt = curve.Value(discretizer.Parameter(i)); | |
| addGArc( | |
| verbose, | |
| abs_center, | |
| path, | |
| plast, | |
| pt, | |
| center, | |
| clockwise, | |
| nf, | |
| cur_f | |
| ); | |
| plast = pt; | |
| } | |
| } | |
| else { | |
| for (int i = 2; i <= nbPoints; i++) { | |
| gp_Pnt pt = curve.Value(discretizer.Parameter(i)); | |
| addGArc( | |
| verbose, | |
| abs_center, | |
| path, | |
| plast, | |
| pt, | |
| center, | |
| clockwise, | |
| nf, | |
| cur_f | |
| ); | |
| plast = pt; | |
| } | |
| } | |
| break; | |
| } | |
| } | |
| if (fabs(first - last) > std::numbers::pi) { | |
| // Split arc(circle) larger than half circle. | |
| gp_Pnt mid = curve.Value((last - first) * 0.5 + first); | |
| addGArc(verbose, abs_center, path, plast, mid, center, clockwise, nf, cur_f); | |
| plast = mid; | |
| } | |
| addGArc(verbose, abs_center, path, plast, p, center, clockwise, nf, cur_f); | |
| break; | |
| } | |
| if (!arcWarned) { | |
| arcWarned = true; | |
| AREA_WARN("arc plane not aligned, force discretization"); | |
| } | |
| AREA_TRACE("arc discretize " << AREA_XYZ(dir)); | |
| } | |
| /* FALLTHRU */ | |
| default: { | |
| const auto& pts = discretize(edge, deflection); | |
| for (size_t i = 1; i < pts.size(); ++i) { | |
| auto& pt = pts[i]; | |
| addG1(verbose, path, plast, pt, nf, cur_f); | |
| plast = pt; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| void Area::abort(bool aborting) | |
| { | |
| s_aborting = aborting; | |
| } | |
| bool Area::aborting() | |
| { | |
| return s_aborting; | |
| } | |
| AreaStaticParams::AreaStaticParams() | |
| {} | |
| AreaStaticParams Area::s_params; | |
| void Area::setDefaultParams(const AreaStaticParams& params) | |
| { | |
| s_params = params; | |
| } | |
| const AreaStaticParams& Area::getDefaultParams() | |
| { | |
| return s_params; | |
| } | |