Merge pull request #3978 from mlampert/feature/path-voronoi-vcarve-refactor

Path: Feature/path voronoi vcarve refactor
This commit is contained in:
sliptonic
2020-10-26 08:11:34 -05:00
committed by GitHub
12 changed files with 634 additions and 320 deletions

View File

@@ -58,6 +58,7 @@ namespace Path
// types
typedef double coordinate_type;
typedef boost::polygon::voronoi_vertex<double> vertex_type;
typedef boost::polygon::point_data<coordinate_type> point_type;
typedef boost::polygon::segment_data<coordinate_type> segment_type;
typedef boost::polygon::voronoi_diagram<double> voronoi_diagram_type;

View File

@@ -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"

View File

@@ -27,19 +27,18 @@
# include <boost/algorithm/string.hpp>
#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 <Base/Exception.h>
#include <Base/GeometryPyCXX.h>
#include <Base/PlacementPy.h>
#include <Base/Vector3D.h>
#include <Base/VectorPy.h>
// 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<VoronoiCellPy*>(lhs)->getVoronoiCellPtr();
const VoronoiCell *vr = static_cast<VoronoiCellPy*>(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);

View File

@@ -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 @@
<UserDocu>Returns true if edge goes through endpoint of the segment site</UserDocu>
</Documentation>
</Methode>
<Methode Name="toGeom" Const="true">
<Methode Name="toShape" Const="true">
<Documentation>
<UserDocu>Returns a geom representation of the edge (line segment or arc of parabola)</UserDocu>
<UserDocu>Returns a shape for the edge</UserDocu>
</Documentation>
</Methode>
<Methode Name="getDistances" Const="true">

View File

@@ -24,9 +24,17 @@
#ifndef _PreComp_
# include <boost/algorithm/string.hpp>
#include <boost/algorithm/string.hpp>
#include <BRepBuilderAPI_MakeEdge.hxx>
#include <Geom_Parabola.hxx>
#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 <Base/Exception.h>
#include <Base/GeometryPyCXX.h>
#include <Base/PlacementPy.h>
#include <Base/Vector3D.h>
#include <Base/VectorPy.h>
#include <Mod/Part/App/LineSegmentPy.h>
#include <Mod/Part/App/ArcOfParabolaPy.h>
// 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<typename pt0_type, typename pt1_type>
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<typename pt0_type, typename pt1_type>
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<VoronoiEdgePy*>(lhs)->getVoronoiEdgePtr();
const VoronoiEdge *vr = static_cast<VoronoiEdgePy*>(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;

View File

@@ -27,21 +27,20 @@
# include <boost/algorithm/string.hpp>
#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 <Base/Exception.h>
#include <Base/GeometryPyCXX.h>
#include <Base/Vector3D.h>
#include <Base/VectorPy.h>
#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;

View File

@@ -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 @@
</Documentation>
<Parameter Name="IncidentEdge" Type="Object"/>
</Attribute>
<Methode Name="toGeom" Const="true">
<Methode Name="toPoint" Const="true">
<Documentation>
<UserDocu>Returns a Vertex - or None if not possible</UserDocu>
<UserDocu>Returns a Vector - or None if not possible</UserDocu>
</Documentation>
</Methode>
</PythonExport>

View File

@@ -27,20 +27,19 @@
# include <boost/algorithm/string.hpp>
#endif
#include "Voronoi.h"
#include "VoronoiPy.h"
#include "VoronoiEdge.h"
#include "VoronoiEdgePy.h"
#include "VoronoiVertex.h"
#include "VoronoiVertexPy.h"
#include <Base/Exception.h>
#include <Base/GeometryPyCXX.h>
#include <Base/PlacementPy.h>
#include <Base/Vector3D.h>
#include <Base/VectorPy.h>
#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<VoronoiVertexPy*>(lhs)->getVoronoiVertexPtr();
const VoronoiVertex *vr = static_cast<VoronoiVertexPy*>(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)) {

View File

@@ -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

View File

@@ -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)

View File

@@ -0,0 +1,174 @@
# -*- coding: utf-8 -*-
# ***************************************************************************
# * *
# * Copyright (c) 2020 sliptonic <shopinthewoods@gmail.com> *
# * *
# * 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)

View File

@@ -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