diff --git a/src/Mod/Path/App/Voronoi.h b/src/Mod/Path/App/Voronoi.h index 7be3cac3d7..d30651fbb1 100644 --- a/src/Mod/Path/App/Voronoi.h +++ b/src/Mod/Path/App/Voronoi.h @@ -58,6 +58,7 @@ namespace Path // types typedef double coordinate_type; + typedef boost::polygon::voronoi_vertex vertex_type; typedef boost::polygon::point_data point_type; typedef boost::polygon::segment_data segment_type; typedef boost::polygon::voronoi_diagram voronoi_diagram_type; diff --git a/src/Mod/Path/App/VoronoiCellPy.xml b/src/Mod/Path/App/VoronoiCellPy.xml index 6bce1f0b0c..dd57e44332 100644 --- a/src/Mod/Path/App/VoronoiCellPy.xml +++ b/src/Mod/Path/App/VoronoiCellPy.xml @@ -5,7 +5,7 @@ Name="VoronoiCellPy" Twin="VoronoiCell" TwinPointer="VoronoiCell" - Include="Mod/Path/App/Voronoi.h" + Include="Mod/Path/App/VoronoiCell.h" FatherInclude="Base/BaseClassPy.h" Namespace="Path" FatherNamespace="Base" diff --git a/src/Mod/Path/App/VoronoiCellPyImp.cpp b/src/Mod/Path/App/VoronoiCellPyImp.cpp index 113f248c0b..f64b722a24 100644 --- a/src/Mod/Path/App/VoronoiCellPyImp.cpp +++ b/src/Mod/Path/App/VoronoiCellPyImp.cpp @@ -27,19 +27,18 @@ # include #endif +#include "Base/Exception.h" +#include "Base/GeometryPyCXX.h" +#include "Base/PlacementPy.h" +#include "Base/Vector3D.h" +#include "Base/VectorPy.h" #include "Mod/Path/App/Voronoi.h" #include "Mod/Path/App/VoronoiCell.h" #include "Mod/Path/App/VoronoiCellPy.h" #include "Mod/Path/App/VoronoiEdge.h" #include "Mod/Path/App/VoronoiEdgePy.h" -#include -#include -#include -#include -#include -// files generated out of VoronoiCellPy.xml -#include "VoronoiCellPy.cpp" +#include "Mod/Path/App/VoronoiCellPy.cpp" using namespace Path; @@ -75,14 +74,14 @@ int VoronoiCellPy::PyInit(PyObject* args, PyObject* /*kwd*/) PyObject* VoronoiCellPy::richCompare(PyObject *lhs, PyObject *rhs, int op) { - PyObject *cmp = Py_False; + PyObject *cmp = (op == Py_EQ) ? Py_False : Py_True; if ( PyObject_TypeCheck(lhs, &VoronoiCellPy::Type) && PyObject_TypeCheck(rhs, &VoronoiCellPy::Type) - && op == Py_EQ) { + && (op == Py_EQ || op == Py_NE)) { const VoronoiCell *vl = static_cast(lhs)->getVoronoiCellPtr(); const VoronoiCell *vr = static_cast(rhs)->getVoronoiCellPtr(); if (vl->index == vr->index && vl->dia == vr->dia) { - cmp = Py_True; + cmp = (op == Py_EQ) ? Py_True : Py_False; } } Py_INCREF(cmp); diff --git a/src/Mod/Path/App/VoronoiEdgePy.xml b/src/Mod/Path/App/VoronoiEdgePy.xml index f6ade1eac4..bce837524d 100644 --- a/src/Mod/Path/App/VoronoiEdgePy.xml +++ b/src/Mod/Path/App/VoronoiEdgePy.xml @@ -5,7 +5,7 @@ Name="VoronoiEdgePy" Twin="VoronoiEdge" TwinPointer="VoronoiEdge" - Include="Mod/Path/App/Voronoi.h" + Include="Mod/Path/App/VoronoiEdge.h" FatherInclude="Base/BaseClassPy.h" Namespace="Path" FatherNamespace="Base" @@ -100,9 +100,9 @@ Returns true if edge goes through endpoint of the segment site - + - Returns a geom representation of the edge (line segment or arc of parabola) + Returns a shape for the edge diff --git a/src/Mod/Path/App/VoronoiEdgePyImp.cpp b/src/Mod/Path/App/VoronoiEdgePyImp.cpp index 451eca0220..cd347eee65 100644 --- a/src/Mod/Path/App/VoronoiEdgePyImp.cpp +++ b/src/Mod/Path/App/VoronoiEdgePyImp.cpp @@ -24,9 +24,17 @@ #ifndef _PreComp_ -# include +#include +#include +#include #endif +#include "Base/Exception.h" +#include "Base/Vector3D.h" +#include "Base/VectorPy.h" +#include "Mod/Part/App/ArcOfParabolaPy.h" +#include "Mod/Part/App/LineSegmentPy.h" +#include "Mod/Part/App/TopoShapeEdgePy.h" #include "Mod/Path/App/Voronoi.h" #include "Mod/Path/App/Voronoi.h" #include "Mod/Path/App/VoronoiCell.h" @@ -35,19 +43,148 @@ #include "Mod/Path/App/VoronoiEdgePy.h" #include "Mod/Path/App/VoronoiVertex.h" #include "Mod/Path/App/VoronoiVertexPy.h" -#include -#include -#include -#include -#include -#include -#include -// files generated out of VoronoiEdgePy.xml -#include "VoronoiEdgePy.cpp" +#include "Mod/Path/App/VoronoiEdgePy.cpp" using namespace Path; +namespace { + + Voronoi::point_type pointFromVertex(const Voronoi::vertex_type v) { + Voronoi::point_type pt; + pt.x(v.x()); + pt.y(v.y()); + return pt; + } + + Voronoi::point_type orthognalProjection(const Voronoi::point_type &point, const Voronoi::segment_type &segment) { + // move segment so it goes through the origin (s) + Voronoi::point_type offset; + { + offset.x(low(segment).x()); + offset.y(low(segment).y()); + } + Voronoi::point_type s; + { + s.x(high(segment).x() - offset.x()); + s.y(high(segment).y() - offset.y()); + } + // move point accordingly so it maintains it's relation to s (p) + Voronoi::point_type p; + { + p.x(point.x() - offset.x()); + p.y(point.y() - offset.y()); + } + // calculate the orthogonal projection of p onto s + // ((p dot s) / (s dot s)) * s (https://en.wikibooks.org/wiki/Linear_Algebra/Orthogonal_Projection_Onto_a_Line) + // and it back by original offset to get the projected point + double proj = (p.x() * s.x() + p.y() * s.y()) / (s.x() * s.x() + s.y() * s.y()); + Voronoi::point_type pt; + { + pt.x(offset.x() + proj * s.x()); + pt.y(offset.y() + proj * s.y()); + } + return pt; + } + + double length(const Voronoi::point_type &p) { + return sqrt(p.x() * p.x() + p.y() * p.y()); + } + + int sideOf(const Voronoi::point_type &p, const Voronoi::segment_type &s) { + Voronoi::coordinate_type dxp = p.x() - low(s).x(); + Voronoi::coordinate_type dyp = p.y() - low(s).y(); + Voronoi::coordinate_type dxs = high(s).x() - low(s).x(); + Voronoi::coordinate_type dys = high(s).y() - low(s).y(); + + double d = -dxs * dyp + dys * dxp; + if (d < 0) { + return -1; + } + if (d > 0) { + return +1; + } + return 0; + } + + template + double distanceBetween(const pt0_type &p0, const pt1_type &p1, double scale) { + Voronoi::point_type dist; + dist.x(p0.x() - p1.x()); + dist.y(p0.y() - p1.y()); + return length(dist) / scale; + } + + template + double signedDistanceBetween(const pt0_type &p0, const pt1_type &p1, double scale) { + if (length(p0) > length(p1)) { + return -distanceBetween(p0, p1, scale); + } + return distanceBetween(p0, p1, scale); + } + + + void addDistanceBetween(const Voronoi::diagram_type::vertex_type *v0, const Voronoi::point_type &p1, Py::List *list, double scale) { + if (v0) { + list->append(Py::Float(distanceBetween(*v0, p1, scale))); + } else { + Py_INCREF(Py_None); + list->append(Py::asObject(Py_None)); + } + } + + void addProjectedDistanceBetween(const Voronoi::diagram_type::vertex_type *v0, const Voronoi::segment_type &segment, Py::List *list, double scale) { + if (v0) { + Voronoi::point_type p0; + { + p0.x(v0->x()); + p0.y(v0->y()); + } + Voronoi::point_type p1 = orthognalProjection(p0, segment); + list->append(Py::Float(distanceBetween(*v0, p1, scale))); + } else { + Py_INCREF(Py_None); + list->append(Py::asObject(Py_None)); + } + } + + bool addDistancesToPoint(const VoronoiEdge *edge, Voronoi::point_type p, Py::List *list, double scale) { + addDistanceBetween(edge->ptr->vertex0(), p, list, scale); + addDistanceBetween(edge->ptr->vertex1(), p, list, scale); + return true; + } + + bool retrieveDistances(const VoronoiEdge *edge, Py::List *list) { + const Voronoi::diagram_type::cell_type *c0 = edge->ptr->cell(); + if (c0->contains_point()) { + return addDistancesToPoint(edge, edge->dia->retrievePoint(c0), list, edge->dia->getScale()); + } + const Voronoi::diagram_type::cell_type *c1 = edge->ptr->twin()->cell(); + if (c1->contains_point()) { + return addDistancesToPoint(edge, edge->dia->retrievePoint(c1), list, edge->dia->getScale()); + } + // at this point both cells are sourced from segments and it does not matter which one we use + Voronoi::segment_type segment = edge->dia->retrieveSegment(c0); + addProjectedDistanceBetween(edge->ptr->vertex0(), segment, list, edge->dia->getScale()); + addProjectedDistanceBetween(edge->ptr->vertex1(), segment, list, edge->dia->getScale()); + return false; + } + +} + +std::ostream& operator<<(std::ostream& os, const Voronoi::vertex_type &v) { + return os << '(' << v.x() << ", " << v.y() << ')'; +} + +std::ostream& operator<<(std::ostream& os, const Voronoi::point_type &p) { + return os << '(' << p.x() << ", " << p.y() << ')'; +} + +std::ostream& operator<<(std::ostream& os, const Voronoi::segment_type &s) { + return os << '<' << low(s) << ", " << high(s) << '>'; +} + + // returns a string which represents the object e.g. when printed in python std::string VoronoiEdgePy::representation(void) const { @@ -92,16 +229,14 @@ int VoronoiEdgePy::PyInit(PyObject* args, PyObject* /*kwd*/) PyObject* VoronoiEdgePy::richCompare(PyObject *lhs, PyObject *rhs, int op) { - PyObject *cmp = Py_False; + PyObject *cmp = (op == Py_EQ) ? Py_False : Py_True; if ( PyObject_TypeCheck(lhs, &VoronoiEdgePy::Type) && PyObject_TypeCheck(rhs, &VoronoiEdgePy::Type) - && op == Py_EQ) { + && (op == Py_EQ || op == Py_NE)) { const VoronoiEdge *vl = static_cast(lhs)->getVoronoiEdgePtr(); const VoronoiEdge *vr = static_cast(rhs)->getVoronoiEdgePtr(); - if (vl->index == vr->index && vl->dia == vr->dia) { - cmp = Py_True; - } else { - std::cerr << "VoronoiEdge==(" << vl->index << " != " << vr->index << " || " << (vl->dia == vr->dia) << ")" << std::endl; + if (vl->dia == vr->dia && vl->index == vr->index) { + cmp = (op == Py_EQ) ? Py_True : Py_False; } } Py_INCREF(cmp); @@ -259,43 +394,16 @@ PyObject* VoronoiEdgePy::isSecondary(PyObject *args) return chk; } -namespace { - Voronoi::point_type orthognalProjection(const Voronoi::point_type &point, const Voronoi::segment_type &segment) { - // move segment so it goes through the origin (s) - Voronoi::point_type offset; - { - offset.x(low(segment).x()); - offset.y(low(segment).y()); - } - Voronoi::point_type s; - { - s.x(high(segment).x() - offset.x()); - s.y(high(segment).y() - offset.y()); - } - // move point accordingly so it maintains it's relation to s (p) - Voronoi::point_type p; - { - p.x(point.x() - offset.x()); - p.y(point.y() - offset.y()); - } - // calculate the orthogonal projection of p onto s - // ((p dot s) / (s dot s)) * s (https://en.wikibooks.org/wiki/Linear_Algebra/Orthogonal_Projection_Onto_a_Line) - // and it back by original offset to get the projected point - double proj = (p.x() * s.x() + p.y() * s.y()) / (s.x() * s.x() + s.y() * s.y()); - Voronoi::point_type pt; - { - pt.x(offset.x() + proj * s.x()); - pt.y(offset.y() + proj * s.y()); - } - return pt; - } -} - -PyObject* VoronoiEdgePy::toGeom(PyObject *args) +PyObject* VoronoiEdgePy::toShape(PyObject *args) { - double z = 0.0; - if (!PyArg_ParseTuple(args, "|d", &z)) { - throw Py::RuntimeError("single argument of type double accepted"); + double z0 = 0.0; + double z1 = DBL_MAX; + int dbg = 0; + if (!PyArg_ParseTuple(args, "|ddp", &z0, &z1, &dbg)) { + throw Py::RuntimeError("no, one or two arguments of type double accepted"); + } + if (z1 == DBL_MAX) { + z1 = z0; } VoronoiEdge *e = getVoronoiEdgePtr(); if (e->isBound()) { @@ -305,8 +413,10 @@ PyObject* VoronoiEdgePy::toGeom(PyObject *args) auto v1 = e->ptr->vertex1(); if (v0 && v1) { auto p = new Part::GeomLineSegment; - p->setPoints(e->dia->scaledVector(*v0, z), e->dia->scaledVector(*v1, z)); - return new Part::LineSegmentPy(p); + p->setPoints(e->dia->scaledVector(*v0, z0), e->dia->scaledVector(*v1, z1)); + Handle(Geom_Curve) h = Handle(Geom_Curve)::DownCast(p->handle()); + BRepBuilderAPI_MakeEdge mkBuilder(h, h->FirstParameter(), h->LastParameter()); + return new Part::TopoShapeEdgePy(new Part::TopoShape(mkBuilder.Shape())); } } else { // infinite linear, need to clip somehow @@ -334,7 +444,7 @@ PyObject* VoronoiEdgePy::toGeom(PyObject *args) direction.y(dx); } } - double k = 10.0; // <-- need something smarter here + double k = 2.5; // <-- need something smarter here Voronoi::point_type begin; Voronoi::point_type end; if (e->ptr->vertex0()) { @@ -352,8 +462,10 @@ PyObject* VoronoiEdgePy::toGeom(PyObject *args) end.y(origin.y() + direction.y() * k); } auto p = new Part::GeomLineSegment; - p->setPoints(e->dia->scaledVector(begin, z), e->dia->scaledVector(end, z)); - return new Part::LineSegmentPy(p); + p->setPoints(e->dia->scaledVector(begin, z0), e->dia->scaledVector(end, z1)); + Handle(Geom_Curve) h = Handle(Geom_Curve)::DownCast(p->handle()); + BRepBuilderAPI_MakeEdge mkBuilder(h, h->FirstParameter(), h->LastParameter()); + return new Part::TopoShapeEdgePy(new Part::TopoShape(mkBuilder.Shape())); } } else { // parabolic curve, which is always formed by a point and an edge @@ -373,31 +485,103 @@ PyObject* VoronoiEdgePy::toGeom(PyObject *args) axis.x(point.x() - loc.x()); axis.y(point.y() - loc.y()); } - auto p = new Part::GeomParabola; + Voronoi::segment_type xaxis; { - p->setCenter(e->dia->scaledVector(point, z)); - p->setLocation(e->dia->scaledVector(loc, z)); - p->setAngleXU(atan2(axis.y(), axis.x())); - p->setFocal(sqrt(axis.x() * axis.x() + axis.y() * axis.y()) / e->dia->getScale()); + xaxis.low(point); + xaxis.high(loc); } - auto a = new Part::GeomArcOfParabola; - { - a->setHandle(Handle(Geom_Parabola)::DownCast(p->handle())); - // figure out the arc parameters - auto v0 = e->ptr->vertex0(); - auto v1 = e->ptr->vertex1(); - double param0 = 0; - double param1 = 0; - if (!p->closestParameter(e->dia->scaledVector(*v0, z), param0)) { - std::cerr << "closestParameter(v0) failed" << std::endl; - } - if (!p->closestParameter(e->dia->scaledVector(*v1, z), param1)) { - std::cerr << "closestParameter(v0) failed" << std::endl; - } - a->setRange(param0, param1, false); + // determine distances of the end points from the x-axis, those are the parameters for + // the arc of the parabola in the horizontal plane + auto pt0 = pointFromVertex(*e->ptr->vertex0()); + auto pt1 = pointFromVertex(*e->ptr->vertex1()); + Voronoi::point_type pt0x = orthognalProjection(pt0, xaxis); + Voronoi::point_type pt1x = orthognalProjection(pt1, xaxis); + double dist0 = distanceBetween(pt0, pt0x, e->dia->getScale()) * sideOf(pt0, xaxis); + double dist1 = distanceBetween(pt1, pt1x, e->dia->getScale()) * sideOf(pt1, xaxis); + if (dist1 < dist0) { + // if the parabola is traversed in the revere direction we need to use the points + // on the other side of the parabola - beauty of symmetric geometries + dist0 = -dist0; + dist1 = -dist1; } - return new Part::ArcOfParabolaPy(a); + + // at this point we have the direction of the x-axis and the two end points p0 and p1 + // which means we know the plane of the parabola + auto p0 = e->dia->scaledVector(pt0, z0); + auto p1 = e->dia->scaledVector(pt1, z1); + // we get a third point by moving p0 along the axis of the parabola + auto p_ = p0 + e->dia->scaledVector(axis, 0); + + // normal of the plane defined by those 3 points + auto norm = ((p_ - p0).Cross(p1 - p0)).Normalize(); + + // the next thing to figure out is the z level of the x-axis, + double zx = z0 - (dist0 / (dist0 - dist1)) * (z0 - z1); + + auto locn = e->dia->scaledVector(loc, zx); + auto xdir = e->dia->scaledVector(axis, zx); + + double focal; + if (z0 == z1) { + // focal length if parabola in the xy-plane is simply half the distance between the + // point and segment - aka the distance between point and location, aka the length of axis + focal = length(axis) / e->dia->getScale(); + } else { + // if the parabola is not in the xy-plane we need to find the + // (x,y) coordinates of a point on the parabola in the parabola's + // coordinate system. + // see: http://amsi.org.au/ESA_Senior_Years/SeniorTopic2/2a/2a_2content_10.html + // note that above website uses Y as the symmetry axis of the parabola whereas + // OCC uses X as the symmetry axis. The math below is in the website's system. + // We already know 2 points on the parabola (p0 and p1), we know their X values + // (dist0 and dist1) if the parabola is in the xy-plane, and we know their orthogonal + // projection onto the parabola's symmetry axis Y (pt0x and pt1x). The resulting Y + // values are the distance between the parabola's location (loc) and the respective + // orthogonal projection. Pythagoras gives us the X values, using the X from the + // xy-plane and the difference in z. + // Note that this calculation also gives correct results if the parabola is in + // the xy-plane (z0 == z1), it's just that above calculation is so much simpler. + double flenX0 = sqrt(dist0 * dist0 + (z0 - zx) * (z0 - zx)); + double flenX1 = sqrt(dist1 * dist1 + (zx - z1) * (zx - z1)); + double flenX; + double flenY; + // if one of the points is the location, we have to use the other to get sensible values + if (fabs(dist0) > fabs(dist1)) { + flenX = flenX0; + flenY = distanceBetween(loc, pt0x, e->dia->getScale()); + } else { + flenX = flenX1; + flenY = distanceBetween(loc, pt1x, e->dia->getScale()); + } + // parabola: (x - p)^2 = 4*focal*(y - q) | (p,q) ... location of parabola + focal = (flenX * flenX) / (4 * fabs(flenY)); + if (dbg) { + std::cerr << "segement" << segment << ", point" << point << std::endl; + std::cerr << " loc" << loc << ", axis" << axis << std::endl; + std::cerr << " dist0(" << dist0 << " : " << flenX0 << ", dist1(" << dist1 << " : " << flenX1 << ")" << std::endl; + std::cerr << " z(" << z0 << ", " << zx << ", " << z1 << ")" << std::endl; + } + // use new X values to set the parameters + dist0 = dist0 >= 0 ? flenX0 : -flenX0; + dist1 = dist1 >= 0 ? flenX1 : -flenX1; + } + + gp_Pnt pbLocn(locn.x, locn.y, locn.z); + gp_Dir pbNorm(norm.x, norm.y, norm.z); + gp_Dir pbXdir(xdir.x, xdir.y, 0); + + gp_Ax2 pb(pbLocn, pbNorm, pbXdir); + Handle(Geom_Parabola) parabola = new Geom_Parabola(pb, focal); + + auto arc = new Part::GeomArcOfParabola; + arc->setHandle(parabola); + arc->setRange(dist0, dist1, false); + + // get a shape for the parabola arc + Handle(Geom_Curve) h = Handle(Geom_Curve)::DownCast(arc->handle()); + BRepBuilderAPI_MakeEdge mkBuilder(h, h->FirstParameter(), h->LastParameter()); + return new Part::TopoShapeEdgePy(new Part::TopoShape(mkBuilder.Shape())); } } Py_INCREF(Py_None); @@ -405,61 +589,6 @@ PyObject* VoronoiEdgePy::toGeom(PyObject *args) } -namespace { - - double distanceBetween(const Voronoi::diagram_type::vertex_type &v0, const Voronoi::point_type &p1, double scale) { - double x = v0.x() - p1.x(); - double y = v0.y() - p1.y(); - return sqrt(x * x + y * y) / scale; - } - - void addDistanceBetween(const Voronoi::diagram_type::vertex_type *v0, const Voronoi::point_type &p1, Py::List *list, double scale) { - if (v0) { - list->append(Py::Float(distanceBetween(*v0, p1, scale))); - } else { - Py_INCREF(Py_None); - list->append(Py::asObject(Py_None)); - } - } - - void addProjectedDistanceBetween(const Voronoi::diagram_type::vertex_type *v0, const Voronoi::segment_type &segment, Py::List *list, double scale) { - if (v0) { - Voronoi::point_type p0; - { - p0.x(v0->x()); - p0.y(v0->y()); - } - Voronoi::point_type p1 = orthognalProjection(p0, segment); - list->append(Py::Float(distanceBetween(*v0, p1, scale))); - } else { - Py_INCREF(Py_None); - list->append(Py::asObject(Py_None)); - } - } - - bool addDistancesToPoint(const VoronoiEdge *edge, Voronoi::point_type p, Py::List *list, double scale) { - addDistanceBetween(edge->ptr->vertex0(), p, list, scale); - addDistanceBetween(edge->ptr->vertex1(), p, list, scale); - return true; - } - - bool retrieveDistances(const VoronoiEdge *edge, Py::List *list) { - const Voronoi::diagram_type::cell_type *c0 = edge->ptr->cell(); - if (c0->contains_point()) { - return addDistancesToPoint(edge, edge->dia->retrievePoint(c0), list, edge->dia->getScale()); - } - const Voronoi::diagram_type::cell_type *c1 = edge->ptr->twin()->cell(); - if (c1->contains_point()) { - return addDistancesToPoint(edge, edge->dia->retrievePoint(c1), list, edge->dia->getScale()); - } - // at this point both cells are sourced from segments and it does not matter which one we use - Voronoi::segment_type segment = edge->dia->retrieveSegment(c0); - addProjectedDistanceBetween(edge->ptr->vertex0(), segment, list, edge->dia->getScale()); - addProjectedDistanceBetween(edge->ptr->vertex1(), segment, list, edge->dia->getScale()); - return false; - } -} - PyObject* VoronoiEdgePy::getDistances(PyObject *args) { VoronoiEdge *e = getVoronoiEdgeFromPy(this, args); @@ -468,22 +597,6 @@ PyObject* VoronoiEdgePy::getDistances(PyObject *args) return Py::new_reference_to(list); } -std::ostream& operator<<(std::ostream &str, const Voronoi::point_type &p) { - return str << "[" << int(p.x()) << ", " << int(p.y()) << "]"; -} - -std::ostream& operator<<(std::ostream &str, const Voronoi::segment_type &s) { - return str << '<' << low(s) << '-' << high(s) << '>'; -} - -static bool pointsMatch(const Voronoi::point_type &p0, const Voronoi::point_type &p1) { - return long(p0.x()) == long(p1.x()) && long(p0.y()) == long(p1.y()); -} - -static void printCompare(const char *label, const Voronoi::point_type &p0, const Voronoi::point_type &p1) { - std::cerr << " " << label <<": " << pointsMatch(p1, p0) << pointsMatch(p0, p1) << " " << p0 << ' ' << p1 << std::endl; -} - PyObject* VoronoiEdgePy::getSegmentAngle(PyObject *args) { VoronoiEdge *e = getVoronoiEdgeFromPy(this, args); @@ -501,18 +614,7 @@ PyObject* VoronoiEdgePy::getSegmentAngle(PyObject *args) a += M_PI; } return Py::new_reference_to(Py::Float(a)); - } else { - std::cerr << "indices: " << std::endl; - std::cerr << " " << e->dia->segments[i0] << std::endl; - std::cerr << " " << e->dia->segments[i1] << std::endl; - std::cerr << " connected: " << e->dia->segmentsAreConnected(i0, i1) << std::endl; - printCompare("l/l", low(e->dia->segments[i0]), low(e->dia->segments[i1])); - printCompare("l/h", low(e->dia->segments[i0]), high(e->dia->segments[i1])); - printCompare("h/l", high(e->dia->segments[i0]), low(e->dia->segments[i1])); - printCompare("h/h", high(e->dia->segments[i0]), high(e->dia->segments[i1])); } - } else { - std::cerr << "constains_segment(" << e->ptr->cell()->contains_segment() << ", " << e->ptr->twin()->cell()->contains_segment() << ")" << std::endl; } Py_INCREF(Py_None); return Py_None; diff --git a/src/Mod/Path/App/VoronoiPyImp.cpp b/src/Mod/Path/App/VoronoiPyImp.cpp index 474d0e7da1..ed898be265 100644 --- a/src/Mod/Path/App/VoronoiPyImp.cpp +++ b/src/Mod/Path/App/VoronoiPyImp.cpp @@ -27,21 +27,20 @@ # include #endif +#include "Base/Exception.h" +#include "Base/GeometryPyCXX.h" +#include "Base/Vector3D.h" +#include "Base/VectorPy.h" #include "Mod/Path/App/Voronoi.h" #include "Mod/Path/App/VoronoiCell.h" +#include "Mod/Path/App/VoronoiCellPy.h" #include "Mod/Path/App/VoronoiEdge.h" +#include "Mod/Path/App/VoronoiEdgePy.h" +#include "Mod/Path/App/VoronoiPy.h" #include "Mod/Path/App/VoronoiVertex.h" -#include -#include -#include -#include +#include "Mod/Path/App/VoronoiVertexPy.h" -// files generated out of VoronoiPy.xml -#include "VoronoiPy.h" -#include "VoronoiPy.cpp" -#include "VoronoiCellPy.h" -#include "VoronoiEdgePy.h" -#include "VoronoiVertexPy.h" +#include "Mod/Path/App/VoronoiPy.cpp" using namespace Path; diff --git a/src/Mod/Path/App/VoronoiVertexPy.xml b/src/Mod/Path/App/VoronoiVertexPy.xml index 7cdd517b8b..b4e647b746 100644 --- a/src/Mod/Path/App/VoronoiVertexPy.xml +++ b/src/Mod/Path/App/VoronoiVertexPy.xml @@ -5,7 +5,7 @@ Name="VoronoiVertexPy" Twin="VoronoiVertex" TwinPointer="VoronoiVertex" - Include="Mod/Path/App/Voronoi.h" + Include="Mod/Path/App/VoronoiVertex.h" FatherInclude="Base/BaseClassPy.h" Namespace="Path" FatherNamespace="Base" @@ -46,9 +46,9 @@ - + - Returns a Vertex - or None if not possible + Returns a Vector - or None if not possible diff --git a/src/Mod/Path/App/VoronoiVertexPyImp.cpp b/src/Mod/Path/App/VoronoiVertexPyImp.cpp index 45e74a04b2..15d18f22ba 100644 --- a/src/Mod/Path/App/VoronoiVertexPyImp.cpp +++ b/src/Mod/Path/App/VoronoiVertexPyImp.cpp @@ -27,20 +27,19 @@ # include #endif -#include "Voronoi.h" -#include "VoronoiPy.h" -#include "VoronoiEdge.h" -#include "VoronoiEdgePy.h" -#include "VoronoiVertex.h" -#include "VoronoiVertexPy.h" -#include -#include -#include -#include -#include +#include "Base/Exception.h" +#include "Base/GeometryPyCXX.h" +#include "Base/PlacementPy.h" +#include "Base/Vector3D.h" +#include "Base/VectorPy.h" +#include "Mod/Path/App/Voronoi.h" +#include "Mod/Path/App/VoronoiEdge.h" +#include "Mod/Path/App/VoronoiEdgePy.h" +#include "Mod/Path/App/VoronoiPy.h" +#include "Mod/Path/App/VoronoiVertex.h" +#include "Mod/Path/App/VoronoiVertexPy.h" -// files generated out of VoronoiVertexPy.xml -#include "VoronoiVertexPy.cpp" +#include "Mod/Path/App/VoronoiVertexPy.cpp" using namespace Path; @@ -76,14 +75,14 @@ int VoronoiVertexPy::PyInit(PyObject* args, PyObject* /*kwd*/) PyObject* VoronoiVertexPy::richCompare(PyObject *lhs, PyObject *rhs, int op) { - PyObject *cmp = Py_False; + PyObject *cmp = (op == Py_EQ) ? Py_False : Py_True; if ( PyObject_TypeCheck(lhs, &VoronoiVertexPy::Type) && PyObject_TypeCheck(rhs, &VoronoiVertexPy::Type) - && op == Py_EQ) { + && (op == Py_EQ || op == Py_NE)) { const VoronoiVertex *vl = static_cast(lhs)->getVoronoiVertexPtr(); const VoronoiVertex *vr = static_cast(rhs)->getVoronoiVertexPtr(); if (vl->index == vr->index && vl->dia == vr->dia) { - cmp = Py_True; + cmp = (op == Py_EQ) ? Py_True : Py_False; } } Py_INCREF(cmp); @@ -151,7 +150,7 @@ Py::Object VoronoiVertexPy::getIncidentEdge() const { return Py::asObject(new VoronoiEdgePy(new VoronoiEdge(v->dia, v->ptr->incident_edge()))); } -PyObject* VoronoiVertexPy::toGeom(PyObject *args) +PyObject* VoronoiVertexPy::toPoint(PyObject *args) { double z = 0.0; if (!PyArg_ParseTuple(args, "|d", &z)) { diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 451226ae21..5b88d574de 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -198,6 +198,7 @@ SET(PathTests_SRCS PathTests/TestPathToolController.py PathTests/TestPathTooltable.py PathTests/TestPathUtil.py + PathTests/TestPathVoronoi.py PathTests/boxtest.fcstd PathTests/test_centroid_00.ngc PathTests/test_geomop.fcstd diff --git a/src/Mod/Path/PathScripts/PathVcarve.py b/src/Mod/Path/PathScripts/PathVcarve.py index 4c049c1cc0..ea8f5660b8 100644 --- a/src/Mod/Path/PathScripts/PathVcarve.py +++ b/src/Mod/Path/PathScripts/PathVcarve.py @@ -40,19 +40,15 @@ from PySide import QtCore __doc__ = "Class and implementation of Path Vcarve operation" -PRIMARY = 0 -EXTERIOR1 = 1 -EXTERIOR2 = 4 -TWIN = 2 -COLINEAR = 3 -SECONDARY = 5 - -if False: - PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) - PathLog.trackModule(PathLog.thisModule()) -else: - PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +PRIMARY = 0 +SECONDARY = 1 +EXTERIOR1 = 2 +EXTERIOR2 = 3 +COLINEAR = 4 +TWIN = 5 +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +#PathLog.trackModule(PathLog.thisModule()) # Qt translation handling def translate(context, text, disambig=None): @@ -60,7 +56,105 @@ def translate(context, text, disambig=None): VD = [] +Vertex = {} +_sorting = 'global' + +def _collectVoronoiWires(vd): + edges = [e for e in vd.Edges if e.Color == PRIMARY] + vertex = {} + for e in edges: + for v in e.Vertices: + i = v.Index + j = vertex.get(i, []) + j.append(e) + vertex[i] = j + Vertex.clear() + for v in vertex: + Vertex[v] = vertex[v] + + # knots are the start and end points of a wire + knots = [i for i in vertex if len(vertex[i]) == 1] + knots.extend([i for i in vertex if len(vertex[i]) > 2]) + if len(knots) == 0: + for i in vertex: + if len(vertex[i]) > 0: + knots.append(i) + break + + def consume(v, edge): + vertex[v] = [e for e in vertex[v] if e.Index != edge.Index] + return len(vertex[v]) == 0 + + def traverse(vStart, edge, edges): + if vStart == edge.Vertices[0].Index: + vEnd = edge.Vertices[1].Index + edges.append(edge) + else: + vEnd = edge.Vertices[0].Index + edges.append(edge.Twin) + + consume(vStart, edge) + if consume(vEnd, edge): + return None + return vEnd + + wires = [] + while knots: + we = [] + vFirst = knots[0] + vStart = vFirst + vLast = vFirst + if len(vertex[vStart]): + while not vStart is None: + vLast = vStart + edges = vertex[vStart] + if len(edges) > 0: + edge = edges[0] + vStart = traverse(vStart, edge, we) + else: + vStart = None + wires.append(we) + if len(vertex[vFirst]) == 0: + knots = [v for v in knots if v != vFirst] + if len(vertex[vLast]) == 0: + knots = [v for v in knots if v != vLast] + return wires + +def _sortVoronoiWires(wires, start = FreeCAD.Vector(0, 0, 0)): + def closestTo(start, point): + p = None + l = None + for i in point: + if l is None or l > start.distanceToPoint(point[i]): + l = start.distanceToPoint(point[i]) + p = i + return (p, l) + + + begin = {} + end = {} + + for i, w in enumerate(wires): + begin[i] = w[ 0].Vertices[0].toPoint() + end[i] = w[-1].Vertices[1].toPoint() + + result = [] + while begin: + (bIdx, bLen) = closestTo(start, begin) + (eIdx, eLen) = closestTo(start, end) + if bLen < eLen: + result.append(wires[bIdx]) + start = end[bIdx] + del begin[bIdx] + del end[bIdx] + else: + result.append([e.Twin for e in reversed(wires[eIdx])]) + start = begin[eIdx] + del begin[eIdx] + del end[eIdx] + + return result class ObjectVcarve(PathEngraveBase.ObjectOp): '''Proxy class for Vcarve operation.''' @@ -101,6 +195,30 @@ class ObjectVcarve(PathEngraveBase.ObjectOp): # upgrade ... self.setupAdditionalProperties(obj) + def _calculate_depth(self, MIC, zStart, zStop, zScale): + # given a maximum inscribed circle (MIC) and tool angle, + # return depth of cut relative to zStart. + depth = zStart - round(MIC / zScale, 4) + PathLog.debug('zStart value: {} depth: {}'.format(zStart, depth)) + return depth if depth > zStop else zStop + + def _getPartEdge(self, edge, zStart, zStop, zScale): + dist = edge.getDistances() + return edge.toShape(self._calculate_depth(dist[0], zStart, zStop, zScale), self._calculate_depth(dist[1], zStart, zStop, zScale)) + + def _getPartEdges(self, obj, vWire): + # pre-calculate the depth limits - pre-mature optimisation ;) + r = float(obj.ToolController.Tool.Diameter) / 2 + toolangle = obj.ToolController.Tool.CuttingEdgeAngle + zStart = self.model[0].Shape.BoundBox.ZMin + zStop = zStart - r / math.tan(math.radians(toolangle/2)) + zScale = 1.0 / math.tan(math.radians(toolangle / 2)) + + edges = [] + for e in vWire: + edges.append(self._getPartEdge(e, zStart, zStop, zScale)) + return edges + def buildPathMedial(self, obj, Faces): '''constructs a medial axis path using openvoronoi''' @@ -114,115 +232,24 @@ class ObjectVcarve(PathEngraveBase.ObjectOp): for i in range(len(pts)): vd.addSegment(ptv[i], ptv[i+1]) - def calculate_depth(MIC, baselevel=0): - # given a maximum inscribed circle (MIC) and tool angle, - # return depth of cut relative to baselevel. - - r = obj.ToolController.Tool.Diameter / 2 - toolangle = obj.ToolController.Tool.CuttingEdgeAngle - maxdepth = baselevel - r / math.tan(math.radians(toolangle/2)) - - d = baselevel - round(MIC / math.tan(math.radians(toolangle / 2)), 4) - PathLog.debug('baselevel value: {} depth: {}'.format(baselevel, d)) - return d if d <= maxdepth else maxdepth - - def getEdges(vd, color=[PRIMARY]): - if type(color) == int: - color = [color] - geomList = [] - bblevel = self.model[0].Shape.BoundBox.ZMin - for e in vd.Edges: - if e.Color not in color: - continue - if e.toGeom() is None: - continue - p1 = e.Vertices[0].toGeom(calculate_depth(e.getDistances()[0], bblevel)) - p2 = e.Vertices[-1].toGeom(calculate_depth(e.getDistances()[-1], bblevel)) - newedge = Part.Edge(Part.Vertex(p1), Part.Vertex(p2)) - - newedge.fixTolerance(obj.Tolerance, Part.Vertex) - geomList.append(newedge) - - return geomList - - def sortEm(mywire, unmatched): - remaining = [] - wireGrowing = False - - # end points of existing wire - wireverts = [mywire.Edges[0].valueAt(mywire.Edges[0].FirstParameter), - mywire.Edges[-1].valueAt(mywire.Edges[-1].LastParameter)] - - for i, candidate in enumerate(unmatched): - - # end points of candidate edge - cverts = [candidate.Edges[0].valueAt(candidate.Edges[0].FirstParameter), - candidate.Edges[-1].valueAt(candidate.Edges[-1].LastParameter)] - - # ignore short segments below tolerance level - if PathGeom.pointsCoincide(cverts[0], cverts[1], obj.Tolerance): - continue - - # iterate the combination of endpoints. If a match is found, - # make an edge from the common endpoint to the other end of - # the candidate wire. Add the edge to the wire and return it. - - # This generates a new edge rather than using the candidate to - # avoid vertexes with close but different vectors - for wvert in wireverts: - for idx, cvert in enumerate(cverts): - if PathGeom.pointsCoincide(wvert, cvert, obj.Tolerance): - wireGrowing = True - elist = mywire.Edges - otherIndex = int(not(idx)) - - newedge = Part.Edge(Part.Vertex(wvert), - Part.Vertex(cverts[otherIndex])) - - elist.append(newedge) - mywire = Part.Wire(Part.__sortEdges__(elist)) - remaining.extend(unmatched[i+1:]) - return mywire, remaining, wireGrowing - - # if not matched, add to remaining list to test later - remaining.append(candidate) - - return mywire, remaining, wireGrowing - - def getWires(candidateList): - - chains = [] - while len(candidateList) > 0: - cur_wire = Part.Wire(candidateList.pop(0)) - - wireGrowing = True - while wireGrowing: - cur_wire, candidateList, wireGrowing = sortEm(cur_wire, - candidateList) - - chains.append(cur_wire) - - return chains - - def cutWire(w): + def cutWire(edges): path = [] path.append(Path.Command("G0 Z{}".format(obj.SafeHeight.Value))) - e = w.Edges[0] + e = edges[0] p = e.valueAt(e.FirstParameter) path.append(Path.Command("G0 X{} Y{} Z{}".format(p.x, p.y, obj.SafeHeight.Value))) c = Path.Command("G1 X{} Y{} Z{} F{}".format(p.x, p.y, p.z, obj.ToolController.HorizFeed.Value)) path.append(c) - for e in w.Edges: + for e in edges: path.extend(PathGeom.cmdsForEdge(e, hSpeed=obj.ToolController.HorizFeed.Value)) return path VD.clear() - pathlist = [] - pathlist.append(Path.Command("(starting)")) + voronoiWires = [] for f in Faces: vd = Path.Voronoi() insert_many_wires(vd, f.Wires) @@ -233,17 +260,27 @@ class ObjectVcarve(PathEngraveBase.ObjectOp): e.Color = PRIMARY if e.isPrimary() else SECONDARY vd.colorExterior(EXTERIOR1) vd.colorExterior(EXTERIOR2, - lambda v: not f.isInside(v.toGeom(f.BoundBox.ZMin), + lambda v: not f.isInside(v.toPoint(f.BoundBox.ZMin), obj.Tolerance, True)) vd.colorColinear(COLINEAR, obj.Threshold) vd.colorTwins(TWIN) - edgelist = getEdges(vd) + wires = _collectVoronoiWires(vd); + if _sorting != 'global': + wires = _sortVoronoiWires(wires) + voronoiWires.extend(wires) + VD.append((f, vd, wires)) - for wire in getWires(edgelist): - pathlist.extend(cutWire(wire)) - VD.append((f, vd, getWires(edgelist))) + if _sorting == 'global': + voronoiWires = _sortVoronoiWires(voronoiWires) + pathlist = [] + pathlist.append(Path.Command("(starting)")) + for w in voronoiWires: + pWire = self._getPartEdges(obj, w) + if pWire: + wires.append(pWire) + pathlist.extend(cutWire(pWire)) self.commandlist = pathlist def opExecute(self, obj): @@ -283,12 +320,12 @@ class ObjectVcarve(PathEngraveBase.ObjectOp): PathLog.error(e) traceback.print_exc() PathLog.error(translate('PathVcarve', 'The Job Base Object has \ - no engraveable element. Engraving \ - operation will produce no output.')) +no engraveable element. Engraving \ +operation will produce no output.')) + raise e def opUpdateDepths(self, obj, ignoreErrors=False): - '''updateDepths(obj) ... engraving is always done at \ - the top most z-value''' + '''updateDepths(obj) ... engraving is always done at the top most z-value''' job = PathUtils.findParentJob(obj) self.opSetDefaultValues(obj, job) diff --git a/src/Mod/Path/PathTests/TestPathVoronoi.py b/src/Mod/Path/PathTests/TestPathVoronoi.py new file mode 100644 index 0000000000..451637d149 --- /dev/null +++ b/src/Mod/Path/PathTests/TestPathVoronoi.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2020 sliptonic * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU Lesser General Public License (LGPL) * +# * as published by the Free Software Foundation; either version 2 of * +# * the License, or (at your option) any later version. * +# * for detail see the LICENCE text file. * +# * * +# * This program 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 program; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import FreeCAD +import Part +import Path +import PathScripts.PathGeom as PathGeom +import PathTests.PathTestUtils as PathTestUtils +import sys + +vd = None + +def initVD(): + global vd + if vd is None: + pts = [(0,0), (3.5,0), (3.5,1), (1,1), (1,2), (2.5,2), (2.5,3), (1,3), (1,4), (3.5, 4), (3.5,5), (0,5)] + ptv = [FreeCAD.Vector(p[0], p[1]) for p in pts] + ptv.append(ptv[0]) + + vd = Path.Voronoi() + for i in range(len(pts)): + vd.addSegment(ptv[i], ptv[i+1]) + + vd.construct() + + for e in vd.Edges: + if sys.version_info.major > 2: + e.Color = 0 if e.isPrimary() else 1 + else: + e.Color = long(0) if e.isPrimary() else long(1) + + vd.colorExterior(2) + vd.colorColinear(3) + vd.colorTwins(4) + + +class TestPathVoronoi(PathTestUtils.PathTestBase): + + def setUp(self): + initVD() + + def test00(self): + '''Check vertex comparison''' + + self.assertTrue( vd.Vertices[0] == vd.Vertices[0]) + self.assertTrue( vd.Vertices[1] == vd.Vertices[1]) + self.assertTrue( vd.Vertices[0] != vd.Vertices[1]) + self.assertEqual( vd.Vertices[0], vd.Vertices[0]) + self.assertEqual( vd.Vertices[1], vd.Vertices[1]) + self.assertNotEqual(vd.Vertices[0], vd.Vertices[1]) + self.assertNotEqual(vd.Vertices[1], vd.Vertices[0]) + + def test10(self): + '''Check edge comparison''' + + self.assertTrue( vd.Edges[0] == vd.Edges[0]) + self.assertTrue( vd.Edges[1] == vd.Edges[1]) + self.assertTrue( vd.Edges[0] != vd.Edges[1]) + self.assertEqual( vd.Edges[0], vd.Edges[0]) + self.assertEqual( vd.Edges[1], vd.Edges[1]) + self.assertNotEqual(vd.Edges[0], vd.Edges[1]) + self.assertNotEqual(vd.Edges[1], vd.Edges[0]) + + + def test20(self): + '''Check cell comparison''' + + self.assertTrue( vd.Cells[0] == vd.Cells[0]) + self.assertTrue( vd.Cells[1] == vd.Cells[1]) + self.assertTrue( vd.Cells[0] != vd.Cells[1]) + self.assertEqual( vd.Cells[0], vd.Cells[0]) + self.assertEqual( vd.Cells[1], vd.Cells[1]) + self.assertNotEqual(vd.Cells[0], vd.Cells[1]) + self.assertNotEqual(vd.Cells[1], vd.Cells[0]) + + def test50(self): + '''Check toShape for linear edges''' + + edges = [e for e in vd.Edges if e.Color == 0 and e.isLinear()] + self.assertNotEqual(len(edges), 0) + e0 = edges[0] + + e = e0.toShape() + self.assertTrue(type(e.Curve) == Part.LineSegment or type(e.Curve) == Part.Line) + self.assertFalse(PathGeom.pointsCoincide(e.valueAt(e.FirstParameter), e.valueAt(e.LastParameter))) + self.assertRoughly(e.valueAt(e.FirstParameter).z, 0) + self.assertRoughly(e.valueAt(e.LastParameter).z, 0) + + def test51(self): + '''Check toShape for linear edges with set z''' + + edges = [e for e in vd.Edges if e.Color == 0 and e.isLinear()] + self.assertNotEqual(len(edges), 0) + e0 = edges[0] + + e = e0.toShape(13.7) + self.assertTrue(type(e.Curve) == Part.LineSegment or type(e.Curve) == Part.Line) + self.assertFalse(PathGeom.pointsCoincide(e.valueAt(e.FirstParameter), e.valueAt(e.LastParameter))) + self.assertRoughly(e.valueAt(e.FirstParameter).z, 13.7) + self.assertRoughly(e.valueAt(e.LastParameter).z, 13.7) + + def test52(self): + '''Check toShape for linear edges with varying z''' + + edges = [e for e in vd.Edges if e.Color == 0 and e.isLinear()] + self.assertNotEqual(len(edges), 0) + e0 = edges[0] + + e = e0.toShape(2.37, 5.14) + self.assertTrue(type(e.Curve) == Part.LineSegment or type(e.Curve) == Part.Line) + self.assertFalse(PathGeom.pointsCoincide(e.valueAt(e.FirstParameter), e.valueAt(e.LastParameter))) + self.assertRoughly(e.valueAt(e.FirstParameter).z, 2.37) + self.assertRoughly(e.valueAt(e.LastParameter).z, 5.14) + + def test60(self): + '''Check toShape for curved edges''' + + edges = [e for e in vd.Edges if e.Color == 0 and e.isCurved()] + self.assertNotEqual(len(edges), 0) + e0 = edges[0] + + e = e0.toShape() + self.assertTrue(type(e.Curve) == Part.Parabola or type(e.Curve) == Part.BSplineCurve) + self.assertFalse(PathGeom.pointsCoincide(e.valueAt(e.FirstParameter), e.valueAt(e.LastParameter))) + self.assertRoughly(e.valueAt(e.FirstParameter).z, 0) + self.assertRoughly(e.valueAt(e.LastParameter).z, 0) + + def test61(self): + '''Check toShape for curved edges with set z''' + + edges = [e for e in vd.Edges if e.Color == 0 and e.isCurved()] + self.assertNotEqual(len(edges), 0) + e0 = edges[0] + + e = e0.toShape(13.7) + self.assertTrue(type(e.Curve) == Part.Parabola or type(e.Curve) == Part.BSplineCurve) + self.assertFalse(PathGeom.pointsCoincide(e.valueAt(e.FirstParameter), e.valueAt(e.LastParameter))) + self.assertRoughly(e.valueAt(e.FirstParameter).z, 13.7) + self.assertRoughly(e.valueAt(e.LastParameter).z, 13.7) + + def test62(self): + '''Check toShape for curved edges with varying z''' + + edges = [e for e in vd.Edges if e.Color == 0 and e.isCurved()] + self.assertNotEqual(len(edges), 0) + e0 = edges[0] + + e = e0.toShape(2.37, 5.14) + self.assertTrue(type(e.Curve) == Part.Parabola or type(e.Curve) == Part.BSplineCurve) + self.assertFalse(PathGeom.pointsCoincide(e.valueAt(e.FirstParameter), e.valueAt(e.LastParameter))) + self.assertRoughly(e.valueAt(e.FirstParameter).z, 2.37) + self.assertRoughly(e.valueAt(e.LastParameter).z, 5.14) + diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index b5ac4e215c..147cf9a169 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -42,6 +42,7 @@ from PathTests.TestPathToolController import TestPathToolController from PathTests.TestPathSetupSheet import TestPathSetupSheet from PathTests.TestPathDeburr import TestPathDeburr from PathTests.TestPathHelix import TestPathHelix +from PathTests.TestPathVoronoi import TestPathVoronoi # dummy usage to get flake8 and lgtm quiet False if TestApp.__name__ else True @@ -62,4 +63,5 @@ False if TestPathDeburr.__name__ else True False if TestPathHelix.__name__ else True False if TestPathPreferences.__name__ else True False if TestPathToolBit.__name__ else True +False if TestPathVoronoi.__name__ else True