mirror of
https://github.com/SoftFever/OrcaSlicer.git
synced 2025-07-25 15:44:12 -06:00
New custom backend for libnest2d using libslic3r types
Adapted to new clipper->eigen mod
This commit is contained in:
parent
7112ac61b6
commit
ad19ab219d
21 changed files with 656 additions and 765 deletions
|
@ -12,11 +12,8 @@ set(LIBNEST2D_SRCFILES
|
|||
include/libnest2d/placers/bottomleftplacer.hpp
|
||||
include/libnest2d/placers/nfpplacer.hpp
|
||||
include/libnest2d/selections/selection_boilerplate.hpp
|
||||
#include/libnest2d/selections/filler.hpp
|
||||
include/libnest2d/selections/firstfit.hpp
|
||||
#include/libnest2d/selections/djd_heuristic.hpp
|
||||
include/libnest2d/backends/clipper/geometries.hpp
|
||||
include/libnest2d/backends/clipper/clipper_polygon.hpp
|
||||
include/libnest2d/backends/libslic3r/geometries.hpp
|
||||
include/libnest2d/optimizers/nlopt/nlopt_boilerplate.hpp
|
||||
include/libnest2d/optimizers/nlopt/simplex.hpp
|
||||
include/libnest2d/optimizers/nlopt/subplex.hpp
|
||||
|
@ -27,5 +24,5 @@ set(LIBNEST2D_SRCFILES
|
|||
add_library(libnest2d STATIC ${LIBNEST2D_SRCFILES})
|
||||
|
||||
target_include_directories(libnest2d PUBLIC ${CMAKE_CURRENT_LIST_DIR}/include)
|
||||
target_link_libraries(libnest2d PUBLIC clipper NLopt::nlopt TBB::tbb Boost::boost)
|
||||
target_compile_definitions(libnest2d PUBLIC LIBNEST2D_THREADING_tbb LIBNEST2D_STATIC LIBNEST2D_OPTIMIZER_nlopt LIBNEST2D_GEOMETRIES_clipper)
|
||||
target_link_libraries(libnest2d PUBLIC NLopt::nlopt TBB::tbb Boost::boost libslic3r)
|
||||
target_compile_definitions(libnest2d PUBLIC LIBNEST2D_THREADING_tbb LIBNEST2D_STATIC LIBNEST2D_OPTIMIZER_nlopt LIBNEST2D_GEOMETRIES_libslic3r)
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
#ifndef CLIPPER_POLYGON_HPP
|
||||
#define CLIPPER_POLYGON_HPP
|
||||
|
||||
#include <clipper.hpp>
|
||||
|
||||
namespace ClipperLib {
|
||||
|
||||
struct Polygon {
|
||||
Path Contour;
|
||||
Paths Holes;
|
||||
|
||||
inline Polygon() = default;
|
||||
|
||||
inline explicit Polygon(const Path& cont): Contour(cont) {}
|
||||
// inline explicit Polygon(const Paths& holes):
|
||||
// Holes(holes) {}
|
||||
inline Polygon(const Path& cont, const Paths& holes):
|
||||
Contour(cont), Holes(holes) {}
|
||||
|
||||
inline explicit Polygon(Path&& cont): Contour(std::move(cont)) {}
|
||||
// inline explicit Polygon(Paths&& holes): Holes(std::move(holes)) {}
|
||||
inline Polygon(Path&& cont, Paths&& holes):
|
||||
Contour(std::move(cont)), Holes(std::move(holes)) {}
|
||||
};
|
||||
|
||||
#if 0
|
||||
inline IntPoint& operator +=(IntPoint& p, const IntPoint& pa ) {
|
||||
// This could be done with SIMD
|
||||
|
||||
p.x() += pa.x();
|
||||
p.y() += pa.y();
|
||||
return p;
|
||||
}
|
||||
|
||||
inline IntPoint operator+(const IntPoint& p1, const IntPoint& p2) {
|
||||
IntPoint ret = p1;
|
||||
ret += p2;
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline IntPoint& operator -=(IntPoint& p, const IntPoint& pa ) {
|
||||
p.x() -= pa.x();
|
||||
p.y() -= pa.y();
|
||||
return p;
|
||||
}
|
||||
|
||||
inline IntPoint operator -(const IntPoint& p ) {
|
||||
IntPoint ret = p;
|
||||
ret.x() = -ret.x();
|
||||
ret.y() = -ret.y();
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline IntPoint operator-(const IntPoint& p1, const IntPoint& p2) {
|
||||
IntPoint ret = p1;
|
||||
ret -= p2;
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline IntPoint& operator *=(IntPoint& p, const IntPoint& pa ) {
|
||||
p.x() *= pa.x();
|
||||
p.y() *= pa.y();
|
||||
return p;
|
||||
}
|
||||
|
||||
inline IntPoint operator*(const IntPoint& p1, const IntPoint& p2) {
|
||||
IntPoint ret = p1;
|
||||
ret *= p2;
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
#endif // CLIPPER_POLYGON_HPP
|
|
@ -1,356 +0,0 @@
|
|||
#ifndef CLIPPER_BACKEND_HPP
|
||||
#define CLIPPER_BACKEND_HPP
|
||||
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
|
||||
#include <libnest2d/geometry_traits.hpp>
|
||||
#include <libnest2d/geometry_traits_nfp.hpp>
|
||||
|
||||
#include "clipper_polygon.hpp"
|
||||
|
||||
namespace libnest2d {
|
||||
|
||||
// Aliases for convinience
|
||||
using PointImpl = ClipperLib::IntPoint;
|
||||
using PathImpl = ClipperLib::Path;
|
||||
using HoleStore = ClipperLib::Paths;
|
||||
using PolygonImpl = ClipperLib::Polygon;
|
||||
|
||||
template<> struct ShapeTag<PolygonImpl> { using Type = PolygonTag; };
|
||||
template<> struct ShapeTag<PathImpl> { using Type = PathTag; };
|
||||
template<> struct ShapeTag<PointImpl> { using Type = PointTag; };
|
||||
|
||||
// Type of coordinate units used by Clipper. Enough to specialize for point,
|
||||
// the rest of the types will work (Path, Polygon)
|
||||
template<> struct CoordType<PointImpl> {
|
||||
using Type = ClipperLib::cInt;
|
||||
static const constexpr ClipperLib::cInt MM_IN_COORDS = 1000000;
|
||||
};
|
||||
|
||||
// Enough to specialize for path, it will work for multishape and Polygon
|
||||
template<> struct PointType<PathImpl> { using Type = PointImpl; };
|
||||
|
||||
// This is crucial. CountourType refers to itself by default, so we don't have
|
||||
// to secialize for clipper Path. ContourType<PathImpl>::Type is PathImpl.
|
||||
template<> struct ContourType<PolygonImpl> { using Type = PathImpl; };
|
||||
|
||||
// The holes are contained in Clipper::Paths
|
||||
template<> struct HolesContainer<PolygonImpl> { using Type = ClipperLib::Paths; };
|
||||
|
||||
namespace pointlike {
|
||||
|
||||
// Tell libnest2d how to extract the X coord from a ClipperPoint object
|
||||
template<> inline ClipperLib::cInt x(const PointImpl& p)
|
||||
{
|
||||
return p.x();
|
||||
}
|
||||
|
||||
// Tell libnest2d how to extract the Y coord from a ClipperPoint object
|
||||
template<> inline ClipperLib::cInt y(const PointImpl& p)
|
||||
{
|
||||
return p.y();
|
||||
}
|
||||
|
||||
// Tell libnest2d how to extract the X coord from a ClipperPoint object
|
||||
template<> inline ClipperLib::cInt& x(PointImpl& p)
|
||||
{
|
||||
return p.x();
|
||||
}
|
||||
|
||||
// Tell libnest2d how to extract the Y coord from a ClipperPoint object
|
||||
template<> inline ClipperLib::cInt& y(PointImpl& p)
|
||||
{
|
||||
return p.y();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Using the libnest2d default area implementation
|
||||
#define DISABLE_BOOST_AREA
|
||||
|
||||
namespace shapelike {
|
||||
|
||||
template<>
|
||||
inline void offset(PolygonImpl& sh, TCoord<PointImpl> distance, const PolygonTag&)
|
||||
{
|
||||
#define DISABLE_BOOST_OFFSET
|
||||
|
||||
using ClipperLib::ClipperOffset;
|
||||
using ClipperLib::jtSquare;
|
||||
using ClipperLib::etClosedPolygon;
|
||||
using ClipperLib::Paths;
|
||||
|
||||
Paths result;
|
||||
|
||||
try {
|
||||
ClipperOffset offs;
|
||||
offs.AddPath(sh.Contour, jtSquare, etClosedPolygon);
|
||||
offs.AddPaths(sh.Holes, jtSquare, etClosedPolygon);
|
||||
offs.Execute(result, static_cast<double>(distance));
|
||||
} catch (ClipperLib::clipperException &) {
|
||||
throw GeometryException(GeomErr::OFFSET);
|
||||
}
|
||||
|
||||
// Offsetting reverts the orientation and also removes the last vertex
|
||||
// so boost will not have a closed polygon.
|
||||
|
||||
// we plan to replace contours
|
||||
sh.Holes.clear();
|
||||
|
||||
bool found_the_contour = false;
|
||||
for(auto& r : result) {
|
||||
if(ClipperLib::Orientation(r)) {
|
||||
// We don't like if the offsetting generates more than one contour
|
||||
// but throwing would be an overkill. Instead, we should warn the
|
||||
// caller about the inability to create correct geometries
|
||||
if(!found_the_contour) {
|
||||
sh.Contour = std::move(r);
|
||||
ClipperLib::ReversePath(sh.Contour);
|
||||
auto front_p = sh.Contour.front();
|
||||
sh.Contour.emplace_back(std::move(front_p));
|
||||
found_the_contour = true;
|
||||
} else {
|
||||
dout() << "Warning: offsetting result is invalid!";
|
||||
/* TODO warning */
|
||||
}
|
||||
} else {
|
||||
// TODO If there are multiple contours we can't be sure which hole
|
||||
// belongs to the first contour. (But in this case the situation is
|
||||
// bad enough to let it go...)
|
||||
sh.Holes.emplace_back(std::move(r));
|
||||
ClipperLib::ReversePath(sh.Holes.back());
|
||||
auto front_p = sh.Holes.back().front();
|
||||
sh.Holes.back().emplace_back(std::move(front_p));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void offset(PathImpl& sh, TCoord<PointImpl> distance, const PathTag&)
|
||||
{
|
||||
PolygonImpl p(std::move(sh));
|
||||
offset(p, distance, PolygonTag());
|
||||
sh = p.Contour;
|
||||
}
|
||||
|
||||
// Tell libnest2d how to make string out of a ClipperPolygon object
|
||||
template<> inline std::string toString(const PolygonImpl& sh)
|
||||
{
|
||||
std::stringstream ss;
|
||||
|
||||
ss << "Contour {\n";
|
||||
for(auto p : sh.Contour) {
|
||||
ss << "\t" << p.x() << " " << p.y() << "\n";
|
||||
}
|
||||
ss << "}\n";
|
||||
|
||||
for(auto& h : sh.Holes) {
|
||||
ss << "Holes {\n";
|
||||
for(auto p : h) {
|
||||
ss << "\t{\n";
|
||||
ss << "\t\t" << p.x() << " " << p.y() << "\n";
|
||||
ss << "\t}\n";
|
||||
}
|
||||
ss << "}\n";
|
||||
}
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
template<>
|
||||
inline PolygonImpl create(const PathImpl& path, const HoleStore& holes)
|
||||
{
|
||||
PolygonImpl p;
|
||||
p.Contour = path;
|
||||
p.Holes = holes;
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
template<> inline PolygonImpl create( PathImpl&& path, HoleStore&& holes) {
|
||||
PolygonImpl p;
|
||||
p.Contour.swap(path);
|
||||
p.Holes.swap(holes);
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
template<>
|
||||
inline const THolesContainer<PolygonImpl>& holes(const PolygonImpl& sh)
|
||||
{
|
||||
return sh.Holes;
|
||||
}
|
||||
|
||||
template<> inline THolesContainer<PolygonImpl>& holes(PolygonImpl& sh)
|
||||
{
|
||||
return sh.Holes;
|
||||
}
|
||||
|
||||
template<>
|
||||
inline TContour<PolygonImpl>& hole(PolygonImpl& sh, unsigned long idx)
|
||||
{
|
||||
return sh.Holes[idx];
|
||||
}
|
||||
|
||||
template<>
|
||||
inline const TContour<PolygonImpl>& hole(const PolygonImpl& sh,
|
||||
unsigned long idx)
|
||||
{
|
||||
return sh.Holes[idx];
|
||||
}
|
||||
|
||||
template<> inline size_t holeCount(const PolygonImpl& sh)
|
||||
{
|
||||
return sh.Holes.size();
|
||||
}
|
||||
|
||||
template<> inline PathImpl& contour(PolygonImpl& sh)
|
||||
{
|
||||
return sh.Contour;
|
||||
}
|
||||
|
||||
template<>
|
||||
inline const PathImpl& contour(const PolygonImpl& sh)
|
||||
{
|
||||
return sh.Contour;
|
||||
}
|
||||
|
||||
#define DISABLE_BOOST_TRANSLATE
|
||||
template<>
|
||||
inline void translate(PolygonImpl& sh, const PointImpl& offs)
|
||||
{
|
||||
for(auto& p : sh.Contour) { p += offs; }
|
||||
for(auto& hole : sh.Holes) for(auto& p : hole) { p += offs; }
|
||||
}
|
||||
|
||||
#define DISABLE_BOOST_ROTATE
|
||||
template<>
|
||||
inline void rotate(PolygonImpl& sh, const Radians& rads)
|
||||
{
|
||||
using Coord = TCoord<PointImpl>;
|
||||
|
||||
auto cosa = rads.cos();
|
||||
auto sina = rads.sin();
|
||||
|
||||
for(auto& p : sh.Contour) {
|
||||
p = {
|
||||
static_cast<Coord>(p.x() * cosa - p.y() * sina),
|
||||
static_cast<Coord>(p.x() * sina + p.y() * cosa)
|
||||
};
|
||||
}
|
||||
for(auto& hole : sh.Holes) for(auto& p : hole) {
|
||||
p = {
|
||||
static_cast<Coord>(p.x() * cosa - p.y() * sina),
|
||||
static_cast<Coord>(p.x() * sina + p.y() * cosa)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace shapelike
|
||||
|
||||
#define DISABLE_BOOST_NFP_MERGE
|
||||
inline TMultiShape<PolygonImpl> clipper_execute(
|
||||
ClipperLib::Clipper& clipper,
|
||||
ClipperLib::ClipType clipType,
|
||||
ClipperLib::PolyFillType subjFillType = ClipperLib::pftEvenOdd,
|
||||
ClipperLib::PolyFillType clipFillType = ClipperLib::pftEvenOdd)
|
||||
{
|
||||
TMultiShape<PolygonImpl> retv;
|
||||
|
||||
ClipperLib::PolyTree result;
|
||||
clipper.Execute(clipType, result, subjFillType, clipFillType);
|
||||
|
||||
retv.reserve(static_cast<size_t>(result.Total()));
|
||||
|
||||
std::function<void(ClipperLib::PolyNode*, PolygonImpl&)> processHole;
|
||||
|
||||
auto processPoly = [&retv, &processHole](ClipperLib::PolyNode *pptr) {
|
||||
PolygonImpl poly;
|
||||
poly.Contour.swap(pptr->Contour);
|
||||
|
||||
assert(!pptr->IsHole());
|
||||
|
||||
if(!poly.Contour.empty() ) {
|
||||
auto front_p = poly.Contour.front();
|
||||
auto &back_p = poly.Contour.back();
|
||||
if(front_p.x() != back_p.x() || front_p.y() != back_p.x())
|
||||
poly.Contour.emplace_back(front_p);
|
||||
}
|
||||
|
||||
for(auto h : pptr->Childs) { processHole(h, poly); }
|
||||
retv.push_back(poly);
|
||||
};
|
||||
|
||||
processHole = [&processPoly](ClipperLib::PolyNode *pptr, PolygonImpl& poly)
|
||||
{
|
||||
poly.Holes.emplace_back(std::move(pptr->Contour));
|
||||
|
||||
assert(pptr->IsHole());
|
||||
|
||||
if(!poly.Contour.empty() ) {
|
||||
auto front_p = poly.Contour.front();
|
||||
auto &back_p = poly.Contour.back();
|
||||
if(front_p.x() != back_p.x() || front_p.y() != back_p.x())
|
||||
poly.Contour.emplace_back(front_p);
|
||||
}
|
||||
|
||||
for(auto c : pptr->Childs) processPoly(c);
|
||||
};
|
||||
|
||||
auto traverse = [&processPoly] (ClipperLib::PolyNode *node)
|
||||
{
|
||||
for(auto ch : node->Childs) processPoly(ch);
|
||||
};
|
||||
|
||||
traverse(&result);
|
||||
|
||||
return retv;
|
||||
}
|
||||
|
||||
namespace nfp {
|
||||
|
||||
template<> inline TMultiShape<PolygonImpl>
|
||||
merge(const TMultiShape<PolygonImpl>& shapes)
|
||||
{
|
||||
ClipperLib::Clipper clipper(ClipperLib::ioReverseSolution);
|
||||
|
||||
bool closed = true;
|
||||
bool valid = true;
|
||||
|
||||
for(auto& path : shapes) {
|
||||
valid &= clipper.AddPath(path.Contour, ClipperLib::ptSubject, closed);
|
||||
|
||||
for(auto& h : path.Holes)
|
||||
valid &= clipper.AddPath(h, ClipperLib::ptSubject, closed);
|
||||
}
|
||||
|
||||
if(!valid) throw GeometryException(GeomErr::MERGE);
|
||||
|
||||
return clipper_execute(clipper, ClipperLib::ctUnion, ClipperLib::pftNegative);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#define DISABLE_BOOST_CONVEX_HULL
|
||||
|
||||
//#define DISABLE_BOOST_SERIALIZE
|
||||
//#define DISABLE_BOOST_UNSERIALIZE
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable: 4244)
|
||||
#pragma warning(disable: 4267)
|
||||
#endif
|
||||
// All other operators and algorithms are implemented with boost
|
||||
#include <libnest2d/utils/boost_alg.hpp>
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning(pop)
|
||||
#endif
|
||||
|
||||
#endif // CLIPPER_BACKEND_HPP
|
|
@ -0,0 +1,272 @@
|
|||
#ifndef CLIPPER_BACKEND_HPP
|
||||
#define CLIPPER_BACKEND_HPP
|
||||
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
|
||||
#include <libnest2d/geometry_traits.hpp>
|
||||
#include <libnest2d/geometry_traits_nfp.hpp>
|
||||
|
||||
#include <libslic3r/ExPolygon.hpp>
|
||||
#include <libslic3r/ClipperUtils.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
template<class T, class En = void> struct IsVec_ : public std::false_type {};
|
||||
|
||||
template<class T> struct IsVec_< Vec<2, T> >: public std::true_type {};
|
||||
|
||||
template<class T>
|
||||
static constexpr const bool IsVec = IsVec_<libnest2d::remove_cvref_t<T>>::value;
|
||||
|
||||
template<class T, class O> using VecOnly = std::enable_if_t<IsVec<T>, O>;
|
||||
|
||||
inline Point operator+(const Point& p1, const Point& p2) {
|
||||
Point ret = p1;
|
||||
ret += p2;
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline Point operator -(const Point& p ) {
|
||||
Point ret = p;
|
||||
ret.x() = -ret.x();
|
||||
ret.y() = -ret.y();
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline Point operator-(const Point& p1, const Point& p2) {
|
||||
Point ret = p1;
|
||||
ret -= p2;
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline Point& operator *=(Point& p, const Point& pa ) {
|
||||
p.x() *= pa.x();
|
||||
p.y() *= pa.y();
|
||||
return p;
|
||||
}
|
||||
|
||||
inline Point operator*(const Point& p1, const Point& p2) {
|
||||
Point ret = p1;
|
||||
ret *= p2;
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
namespace libnest2d {
|
||||
|
||||
template<class T> using Vec = Slic3r::Vec<2, T>;
|
||||
|
||||
// Aliases for convinience
|
||||
using PointImpl = Slic3r::Point;
|
||||
using PathImpl = Slic3r::Polygon;
|
||||
using HoleStore = Slic3r::Polygons;
|
||||
using PolygonImpl = Slic3r::ExPolygon;
|
||||
|
||||
template<> struct ShapeTag<Slic3r::Vec2crd> { using Type = PointTag; };
|
||||
template<> struct ShapeTag<Slic3r::Point> { using Type = PointTag; };
|
||||
|
||||
template<> struct ShapeTag<std::vector<Slic3r::Vec2crd>> { using Type = PathTag; };
|
||||
template<> struct ShapeTag<Slic3r::Polygon> { using Type = PathTag; };
|
||||
template<> struct ShapeTag<Slic3r::ExPolygon> { using Type = PolygonTag; };
|
||||
template<> struct ShapeTag<Slic3r::ExPolygons> { using Type = MultiPolygonTag; };
|
||||
|
||||
// Type of coordinate units used by Clipper. Enough to specialize for point,
|
||||
// the rest of the types will work (Path, Polygon)
|
||||
template<> struct CoordType<Slic3r::Point> {
|
||||
using Type = coord_t;
|
||||
static const constexpr coord_t MM_IN_COORDS = 1000000;
|
||||
};
|
||||
|
||||
template<> struct CoordType<Slic3r::Vec2crd> {
|
||||
using Type = coord_t;
|
||||
static const constexpr coord_t MM_IN_COORDS = 1000000;
|
||||
};
|
||||
|
||||
// Enough to specialize for path, it will work for multishape and Polygon
|
||||
template<> struct PointType<std::vector<Slic3r::Vec2crd>> { using Type = Slic3r::Vec2crd; };
|
||||
template<> struct PointType<Slic3r::Polygon> { using Type = Slic3r::Point; };
|
||||
template<> struct PointType<Slic3r::Points> { using Type = Slic3r::Point; };
|
||||
|
||||
// This is crucial. CountourType refers to itself by default, so we don't have
|
||||
// to secialize for clipper Path. ContourType<PathImpl>::Type is PathImpl.
|
||||
template<> struct ContourType<Slic3r::ExPolygon> { using Type = Slic3r::Polygon; };
|
||||
|
||||
// The holes are contained in Clipper::Paths
|
||||
template<> struct HolesContainer<Slic3r::ExPolygon> { using Type = Slic3r::Polygons; };
|
||||
|
||||
template<>
|
||||
struct OrientationType<Slic3r::Polygon> {
|
||||
static const constexpr Orientation Value = Orientation::COUNTER_CLOCKWISE;
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ClosureType<Slic3r::Polygon> {
|
||||
static const constexpr Closure Value = Closure::OPEN;
|
||||
};
|
||||
|
||||
template<> struct MultiShape<Slic3r::ExPolygon> { using Type = Slic3r::ExPolygons; };
|
||||
template<> struct ContourType<Slic3r::ExPolygons> { using Type = Slic3r::Polygon; };
|
||||
|
||||
// Using the libnest2d default area implementation
|
||||
#define DISABLE_BOOST_AREA
|
||||
|
||||
namespace shapelike {
|
||||
|
||||
template<>
|
||||
inline void offset(Slic3r::ExPolygon& sh, coord_t distance, const PolygonTag&)
|
||||
{
|
||||
#define DISABLE_BOOST_OFFSET
|
||||
auto res = Slic3r::offset_ex(sh, distance, ClipperLib::jtSquare);
|
||||
if (!res.empty()) sh = res.front();
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void offset(Slic3r::Polygon& sh, coord_t distance, const PathTag&)
|
||||
{
|
||||
auto res = Slic3r::offset(sh, distance, ClipperLib::jtSquare);
|
||||
if (!res.empty()) sh = res.front();
|
||||
}
|
||||
|
||||
// Tell libnest2d how to make string out of a ClipperPolygon object
|
||||
template<> inline std::string toString(const Slic3r::ExPolygon& sh)
|
||||
{
|
||||
std::stringstream ss;
|
||||
|
||||
ss << "Contour {\n";
|
||||
for(auto &p : sh.contour.points) {
|
||||
ss << "\t" << p.x() << " " << p.y() << "\n";
|
||||
}
|
||||
ss << "}\n";
|
||||
|
||||
for(auto& h : sh.holes) {
|
||||
ss << "Holes {\n";
|
||||
for(auto p : h.points) {
|
||||
ss << "\t{\n";
|
||||
ss << "\t\t" << p.x() << " " << p.y() << "\n";
|
||||
ss << "\t}\n";
|
||||
}
|
||||
ss << "}\n";
|
||||
}
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
template<>
|
||||
inline Slic3r::ExPolygon create(const Slic3r::Polygon& path, const Slic3r::Polygons& holes)
|
||||
{
|
||||
Slic3r::ExPolygon p;
|
||||
p.contour = path;
|
||||
p.holes = holes;
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
template<> inline Slic3r::ExPolygon create(Slic3r::Polygon&& path, Slic3r::Polygons&& holes) {
|
||||
Slic3r::ExPolygon p;
|
||||
p.contour.points.swap(path.points);
|
||||
p.holes.swap(holes);
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
template<>
|
||||
inline const THolesContainer<PolygonImpl>& holes(const Slic3r::ExPolygon& sh)
|
||||
{
|
||||
return sh.holes;
|
||||
}
|
||||
|
||||
template<> inline THolesContainer<PolygonImpl>& holes(Slic3r::ExPolygon& sh)
|
||||
{
|
||||
return sh.holes;
|
||||
}
|
||||
|
||||
template<>
|
||||
inline Slic3r::Polygon& hole(Slic3r::ExPolygon& sh, unsigned long idx)
|
||||
{
|
||||
return sh.holes[idx];
|
||||
}
|
||||
|
||||
template<>
|
||||
inline const Slic3r::Polygon& hole(const Slic3r::ExPolygon& sh, unsigned long idx)
|
||||
{
|
||||
return sh.holes[idx];
|
||||
}
|
||||
|
||||
template<> inline size_t holeCount(const Slic3r::ExPolygon& sh)
|
||||
{
|
||||
return sh.holes.size();
|
||||
}
|
||||
|
||||
template<> inline Slic3r::Polygon& contour(Slic3r::ExPolygon& sh)
|
||||
{
|
||||
return sh.contour;
|
||||
}
|
||||
|
||||
template<>
|
||||
inline const Slic3r::Polygon& contour(const Slic3r::ExPolygon& sh)
|
||||
{
|
||||
return sh.contour;
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void reserve(Slic3r::Polygon& p, size_t vertex_capacity, const PathTag&)
|
||||
{
|
||||
p.points.reserve(vertex_capacity);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void addVertex(Slic3r::Polygon& sh, const PathTag&, const Slic3r::Point &p)
|
||||
{
|
||||
sh.points.emplace_back(p);
|
||||
}
|
||||
|
||||
#define DISABLE_BOOST_TRANSLATE
|
||||
template<>
|
||||
inline void translate(Slic3r::ExPolygon& sh, const Slic3r::Point& offs)
|
||||
{
|
||||
sh.translate(offs);
|
||||
}
|
||||
|
||||
#define DISABLE_BOOST_ROTATE
|
||||
template<>
|
||||
inline void rotate(Slic3r::ExPolygon& sh, const Radians& rads)
|
||||
{
|
||||
sh.rotate(rads);
|
||||
}
|
||||
|
||||
} // namespace shapelike
|
||||
|
||||
namespace nfp {
|
||||
|
||||
#define DISABLE_BOOST_NFP_MERGE
|
||||
template<>
|
||||
inline TMultiShape<PolygonImpl> merge(const TMultiShape<PolygonImpl>& shapes)
|
||||
{
|
||||
return Slic3r::union_ex(shapes);
|
||||
}
|
||||
|
||||
} // namespace nfp
|
||||
} // namespace libnest2d
|
||||
|
||||
#define DISABLE_BOOST_CONVEX_HULL
|
||||
|
||||
//#define DISABLE_BOOST_SERIALIZE
|
||||
//#define DISABLE_BOOST_UNSERIALIZE
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable: 4244)
|
||||
#pragma warning(disable: 4267)
|
||||
#endif
|
||||
// All other operators and algorithms are implemented with boost
|
||||
#include <libnest2d/utils/boost_alg.hpp>
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning(pop)
|
||||
#endif
|
||||
|
||||
#endif // CLIPPER_BACKEND_HPP
|
|
@ -128,22 +128,32 @@ template<class S> struct ContourType<DefaultMultiShape<S>> {
|
|||
using Type = typename ContourType<S>::Type;
|
||||
};
|
||||
|
||||
enum class Orientation {
|
||||
CLOCKWISE,
|
||||
COUNTER_CLOCKWISE
|
||||
};
|
||||
enum class Orientation { CLOCKWISE, COUNTER_CLOCKWISE };
|
||||
|
||||
template<class S>
|
||||
struct OrientationType {
|
||||
|
||||
// Default Polygon orientation that the library expects
|
||||
static const Orientation Value = Orientation::CLOCKWISE;
|
||||
static const constexpr Orientation Value = Orientation::CLOCKWISE;
|
||||
};
|
||||
|
||||
template<class T> inline /*constexpr*/ bool is_clockwise() {
|
||||
template<class T> inline constexpr bool is_clockwise() {
|
||||
return OrientationType<TContour<T>>::Value == Orientation::CLOCKWISE;
|
||||
}
|
||||
|
||||
template<class T>
|
||||
inline const constexpr Orientation OrientationTypeV =
|
||||
OrientationType<TContour<T>>::Value;
|
||||
|
||||
enum class Closure { OPEN, CLOSED };
|
||||
|
||||
template<class S> struct ClosureType {
|
||||
static const constexpr Closure Value = Closure::CLOSED;
|
||||
};
|
||||
|
||||
template<class T>
|
||||
inline const constexpr Closure ClosureTypeV =
|
||||
ClosureType<TContour<T>>::Value;
|
||||
|
||||
/**
|
||||
* \brief A point pair base class for other point pairs (segment, box, ...).
|
||||
|
@ -587,9 +597,9 @@ inline void reserve(RawPath& p, size_t vertex_capacity, const PathTag&)
|
|||
}
|
||||
|
||||
template<class S, class...Args>
|
||||
inline void addVertex(S& sh, const PathTag&, Args...args)
|
||||
inline void addVertex(S& sh, const PathTag&, const TPoint<S> &p)
|
||||
{
|
||||
sh.emplace_back(std::forward<Args>(args)...);
|
||||
sh.emplace_back(p);
|
||||
}
|
||||
|
||||
template<class S, class Fn>
|
||||
|
@ -841,9 +851,9 @@ template<class P> auto rbegin(P& p) -> decltype(_backward(end(p)))
|
|||
return _backward(end(p));
|
||||
}
|
||||
|
||||
template<class P> auto rcbegin(const P& p) -> decltype(_backward(end(p)))
|
||||
template<class P> auto rcbegin(const P& p) -> decltype(_backward(cend(p)))
|
||||
{
|
||||
return _backward(end(p));
|
||||
return _backward(cend(p));
|
||||
}
|
||||
|
||||
template<class P> auto rend(P& p) -> decltype(_backward(begin(p)))
|
||||
|
@ -873,16 +883,16 @@ inline void reserve(T& sh, size_t vertex_capacity) {
|
|||
reserve(sh, vertex_capacity, Tag<T>());
|
||||
}
|
||||
|
||||
template<class S, class...Args>
|
||||
inline void addVertex(S& sh, const PolygonTag&, Args...args)
|
||||
template<class S>
|
||||
inline void addVertex(S& sh, const PolygonTag&, const TPoint<S> &p)
|
||||
{
|
||||
addVertex(contour(sh), PathTag(), std::forward<Args>(args)...);
|
||||
addVertex(contour(sh), PathTag(), p);
|
||||
}
|
||||
|
||||
template<class S, class...Args> // Tag dispatcher
|
||||
inline void addVertex(S& sh, Args...args)
|
||||
template<class S> // Tag dispatcher
|
||||
inline void addVertex(S& sh, const TPoint<S> &p)
|
||||
{
|
||||
addVertex(sh, Tag<S>(), std::forward<Args>(args)...);
|
||||
addVertex(sh, Tag<S>(), p);
|
||||
}
|
||||
|
||||
template<class S>
|
||||
|
|
|
@ -28,7 +28,7 @@ inline void buildPolygon(const EdgeList& edgelist,
|
|||
|
||||
auto& rsh = sl::contour(rpoly);
|
||||
|
||||
sl::reserve(rsh, 2*edgelist.size());
|
||||
sl::reserve(rsh, 2 * edgelist.size());
|
||||
|
||||
// Add the two vertices from the first edge into the final polygon.
|
||||
sl::addVertex(rsh, edgelist.front().first());
|
||||
|
@ -57,7 +57,6 @@ inline void buildPolygon(const EdgeList& edgelist,
|
|||
|
||||
tmp = std::next(tmp);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
template<class Container, class Iterator = typename Container::iterator>
|
||||
|
@ -214,15 +213,24 @@ inline NfpResult<RawShape> nfpConvexOnly(const RawShape& sh,
|
|||
// Reserve the needed memory
|
||||
edgelist.reserve(cap);
|
||||
sl::reserve(rsh, static_cast<unsigned long>(cap));
|
||||
auto add_edge = [&edgelist](const Vertex &v1, const Vertex &v2) {
|
||||
Edge e{v1, v2};
|
||||
if (e.sqlength() > 0)
|
||||
edgelist.emplace_back(e);
|
||||
};
|
||||
|
||||
{ // place all edges from sh into edgelist
|
||||
auto first = sl::cbegin(sh);
|
||||
auto next = std::next(first);
|
||||
|
||||
while(next != sl::cend(sh)) {
|
||||
edgelist.emplace_back(*(first), *(next));
|
||||
add_edge(*(first), *(next));
|
||||
|
||||
++first; ++next;
|
||||
}
|
||||
|
||||
if constexpr (ClosureTypeV<RawShape> == Closure::OPEN)
|
||||
add_edge(*sl::rcbegin(sh), *sl::cbegin(sh));
|
||||
}
|
||||
|
||||
{ // place all edges from other into edgelist
|
||||
|
@ -230,15 +238,19 @@ inline NfpResult<RawShape> nfpConvexOnly(const RawShape& sh,
|
|||
auto next = std::next(first);
|
||||
|
||||
while(next != sl::cend(other)) {
|
||||
edgelist.emplace_back(*(next), *(first));
|
||||
add_edge(*(next), *(first));
|
||||
|
||||
++first; ++next;
|
||||
}
|
||||
|
||||
if constexpr (ClosureTypeV<RawShape> == Closure::OPEN)
|
||||
add_edge(*sl::cbegin(other), *sl::rcbegin(other));
|
||||
}
|
||||
|
||||
std::sort(edgelist.begin(), edgelist.end(),
|
||||
[](const Edge& e1, const Edge& e2)
|
||||
std::sort(edgelist.begin(), edgelist.end(),
|
||||
[](const Edge& e1, const Edge& e2)
|
||||
{
|
||||
Vertex ax(1, 0); // Unit vector for the X axis
|
||||
const Vertex ax(1, 0); // Unit vector for the X axis
|
||||
|
||||
// get cectors from the edges
|
||||
Vertex p1 = e1.second() - e1.first();
|
||||
|
@ -284,12 +296,18 @@ inline NfpResult<RawShape> nfpConvexOnly(const RawShape& sh,
|
|||
// If Ratio is an actual rational type, there is no precision loss
|
||||
auto pcos1 = Ratio(lcos[0]) / lsq1 * sign * lcos[0];
|
||||
auto pcos2 = Ratio(lcos[1]) / lsq2 * sign * lcos[1];
|
||||
|
||||
return q[0] < 2 ? pcos1 < pcos2 : pcos1 > pcos2;
|
||||
|
||||
if constexpr (is_clockwise<RawShape>())
|
||||
return q[0] < 2 ? pcos1 < pcos2 : pcos1 > pcos2;
|
||||
else
|
||||
return q[0] < 2 ? pcos1 > pcos2 : pcos1 < pcos2;
|
||||
}
|
||||
|
||||
// If in different quadrants, compare the quadrant indices only.
|
||||
return q[0] > q[1];
|
||||
if constexpr (is_clockwise<RawShape>())
|
||||
return q[0] > q[1];
|
||||
else
|
||||
return q[0] < q[1];
|
||||
});
|
||||
|
||||
__nfp::buildPolygon(edgelist, rsh, top_nfp);
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
#include <libnest2d/backends/clipper/geometries.hpp>
|
||||
#endif
|
||||
|
||||
#ifdef LIBNEST2D_GEOMETRIES_libslic3r
|
||||
#include <libnest2d/backends/libslic3r/geometries.hpp>
|
||||
#endif
|
||||
|
||||
#ifdef LIBNEST2D_OPTIMIZER_nlopt
|
||||
// We include the stock optimizers for local and global optimization
|
||||
#include <libnest2d/optimizers/nlopt/subplex.hpp> // Local subplex for NfpPlacer
|
||||
|
|
|
@ -96,7 +96,7 @@ public:
|
|||
* @return The orientation type identifier for the _Item type.
|
||||
*/
|
||||
static BP2D_CONSTEXPR Orientation orientation() {
|
||||
return OrientationType<RawShape>::Value;
|
||||
return OrientationType<TContour<RawShape>>::Value;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -446,44 +446,32 @@ private:
|
|||
}
|
||||
};
|
||||
|
||||
template<class Sh> Sh create_rect(TCoord<Sh> width, TCoord<Sh> height)
|
||||
{
|
||||
auto sh = sl::create<Sh>(
|
||||
{{0, 0}, {0, height}, {width, height}, {width, 0}});
|
||||
|
||||
if constexpr (ClosureTypeV<Sh> == Closure::CLOSED)
|
||||
sl::addVertex(sh, {0, 0});
|
||||
|
||||
if constexpr (OrientationTypeV<Sh> == Orientation::COUNTER_CLOCKWISE)
|
||||
std::reverse(sl::begin(sh), sl::end(sh));
|
||||
|
||||
return sh;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Subclass of _Item for regular rectangle items.
|
||||
*/
|
||||
template<class RawShape>
|
||||
class _Rectangle: public _Item<RawShape> {
|
||||
using _Item<RawShape>::vertex;
|
||||
template<class Sh>
|
||||
class _Rectangle: public _Item<Sh> {
|
||||
using _Item<Sh>::vertex;
|
||||
using TO = Orientation;
|
||||
public:
|
||||
|
||||
using Unit = TCoord<TPoint<RawShape>>;
|
||||
using Unit = TCoord<Sh>;
|
||||
|
||||
template<TO o = OrientationType<RawShape>::Value>
|
||||
inline _Rectangle(Unit width, Unit height,
|
||||
// disable this ctor if o != CLOCKWISE
|
||||
enable_if_t< o == TO::CLOCKWISE, int> = 0 ):
|
||||
_Item<RawShape>( sl::create<RawShape>( {
|
||||
{0, 0},
|
||||
{0, height},
|
||||
{width, height},
|
||||
{width, 0},
|
||||
{0, 0}
|
||||
} ))
|
||||
{
|
||||
}
|
||||
|
||||
template<TO o = OrientationType<RawShape>::Value>
|
||||
inline _Rectangle(Unit width, Unit height,
|
||||
// disable this ctor if o != COUNTER_CLOCKWISE
|
||||
enable_if_t< o == TO::COUNTER_CLOCKWISE, int> = 0 ):
|
||||
_Item<RawShape>( sl::create<RawShape>( {
|
||||
{0, 0},
|
||||
{width, 0},
|
||||
{width, height},
|
||||
{0, height},
|
||||
{0, 0}
|
||||
} ))
|
||||
{
|
||||
}
|
||||
inline _Rectangle(Unit w, Unit h): _Item<Sh>{create_rect<Sh>(w, h)} {}
|
||||
|
||||
inline Unit width() const BP2D_NOEXCEPT {
|
||||
return getX(vertex(2));
|
||||
|
|
|
@ -365,44 +365,50 @@ protected:
|
|||
// the additional vertices for maintaning min object distance
|
||||
sl::reserve(rsh, finish-start+4);
|
||||
|
||||
/*auto addOthers = [&rsh, finish, start, &item](){
|
||||
auto addOthers_ = [&rsh, finish, start, &item](){
|
||||
for(size_t i = start+1; i < finish; i++)
|
||||
sl::addVertex(rsh, item.vertex(i));
|
||||
};*/
|
||||
};
|
||||
|
||||
auto reverseAddOthers = [&rsh, finish, start, &item](){
|
||||
auto reverseAddOthers_ = [&rsh, finish, start, &item](){
|
||||
for(auto i = finish-1; i > start; i--)
|
||||
sl::addVertex(rsh, item.vertex(
|
||||
static_cast<unsigned long>(i)));
|
||||
sl::addVertex(rsh, item.vertex(static_cast<unsigned long>(i)));
|
||||
};
|
||||
|
||||
auto addOthers = [&addOthers_, &reverseAddOthers_]() {
|
||||
if constexpr (!is_clockwise<RawShape>())
|
||||
addOthers_();
|
||||
else
|
||||
reverseAddOthers_();
|
||||
};
|
||||
|
||||
// Final polygon construction...
|
||||
|
||||
static_assert(OrientationType<RawShape>::Value ==
|
||||
Orientation::CLOCKWISE,
|
||||
"Counter clockwise toWallPoly() Unimplemented!");
|
||||
|
||||
// Clockwise polygon construction
|
||||
|
||||
sl::addVertex(rsh, topleft_vertex);
|
||||
|
||||
if(dir == Dir::LEFT) reverseAddOthers();
|
||||
if(dir == Dir::LEFT) addOthers();
|
||||
else {
|
||||
sl::addVertex(rsh, getX(topleft_vertex), 0);
|
||||
sl::addVertex(rsh, getX(bottomleft_vertex), 0);
|
||||
sl::addVertex(rsh, {getX(topleft_vertex), 0});
|
||||
sl::addVertex(rsh, {getX(bottomleft_vertex), 0});
|
||||
}
|
||||
|
||||
sl::addVertex(rsh, bottomleft_vertex);
|
||||
|
||||
if(dir == Dir::LEFT) {
|
||||
sl::addVertex(rsh, 0, getY(bottomleft_vertex));
|
||||
sl::addVertex(rsh, 0, getY(topleft_vertex));
|
||||
sl::addVertex(rsh, {0, getY(bottomleft_vertex)});
|
||||
sl::addVertex(rsh, {0, getY(topleft_vertex)});
|
||||
}
|
||||
else reverseAddOthers();
|
||||
else addOthers();
|
||||
|
||||
|
||||
// Close the polygon
|
||||
sl::addVertex(rsh, topleft_vertex);
|
||||
if constexpr (ClosureTypeV<RawShape> == Closure::CLOSED)
|
||||
sl::addVertex(rsh, topleft_vertex);
|
||||
|
||||
if constexpr (!is_clockwise<RawShape>())
|
||||
std::reverse(rsh.begin(), rsh.end());
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
|
@ -344,8 +344,7 @@ inline void correctNfpPosition(nfp::NfpResult<RawShape>& nfp,
|
|||
auto dtouch = touch_sh - touch_other;
|
||||
auto top_other = orbiter.rightmostTopVertex() + dtouch;
|
||||
auto dnfp = top_other - nfp.second; // nfp.second is the nfp reference point
|
||||
//FIXME the explicit type conversion ClipperLib::IntPoint()
|
||||
shapelike::translate(nfp.first, ClipperLib::IntPoint(dnfp));
|
||||
shapelike::translate(nfp.first, dnfp);
|
||||
}
|
||||
|
||||
template<class RawShape>
|
||||
|
@ -474,8 +473,7 @@ public:
|
|||
auto bbin = sl::boundingBox(bin);
|
||||
auto d = bbch.center() - bbin.center();
|
||||
auto chullcpy = chull;
|
||||
//FIXME the explicit type conversion ClipperLib::IntPoint()
|
||||
sl::translate(chullcpy, ClipperLib::IntPoint(d));
|
||||
sl::translate(chullcpy, d);
|
||||
return sl::isInside(chullcpy, bin) ? -1.0 : 1.0;
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
#pragma warning(pop)
|
||||
#endif
|
||||
// this should be removed to not confuse the compiler
|
||||
// #include <libnest2d.h>
|
||||
// #include "../libnest2d.hpp"
|
||||
|
||||
namespace bp2d {
|
||||
|
||||
|
@ -30,6 +30,10 @@ using libnest2d::PolygonImpl;
|
|||
using libnest2d::PathImpl;
|
||||
using libnest2d::Orientation;
|
||||
using libnest2d::OrientationType;
|
||||
using libnest2d::OrientationTypeV;
|
||||
using libnest2d::ClosureType;
|
||||
using libnest2d::Closure;
|
||||
using libnest2d::ClosureTypeV;
|
||||
using libnest2d::getX;
|
||||
using libnest2d::getY;
|
||||
using libnest2d::setX;
|
||||
|
@ -213,8 +217,15 @@ struct ToBoostOrienation<bp2d::Orientation::COUNTER_CLOCKWISE> {
|
|||
static const order_selector Value = counterclockwise;
|
||||
};
|
||||
|
||||
static const bp2d::Orientation RealOrientation =
|
||||
bp2d::OrientationType<bp2d::PolygonImpl>::Value;
|
||||
template<bp2d::Closure> struct ToBoostClosure {};
|
||||
|
||||
template<> struct ToBoostClosure<bp2d::Closure::OPEN> {
|
||||
static const constexpr closure_selector Value = closure_selector::open;
|
||||
};
|
||||
|
||||
template<> struct ToBoostClosure<bp2d::Closure::CLOSED> {
|
||||
static const constexpr closure_selector Value = closure_selector::closed;
|
||||
};
|
||||
|
||||
// Ring implementation /////////////////////////////////////////////////////////
|
||||
|
||||
|
@ -225,12 +236,13 @@ template<> struct tag<bp2d::PathImpl> {
|
|||
|
||||
template<> struct point_order<bp2d::PathImpl> {
|
||||
static const order_selector value =
|
||||
ToBoostOrienation<RealOrientation>::Value;
|
||||
ToBoostOrienation<bp2d::OrientationTypeV<bp2d::PathImpl>>::Value;
|
||||
};
|
||||
|
||||
// All our Paths should be closed for the bin packing application
|
||||
template<> struct closure<bp2d::PathImpl> {
|
||||
static const closure_selector value = closed;
|
||||
static const constexpr closure_selector value =
|
||||
ToBoostClosure< bp2d::ClosureTypeV<bp2d::PathImpl> >::Value;
|
||||
};
|
||||
|
||||
// Polygon implementation //////////////////////////////////////////////////////
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue