openscad/src/GeometryEvaluator.cc

1125 lines
35 KiB
C++

#include "GeometryEvaluator.h"
#include "traverser.h"
#include "tree.h"
#include "GeometryCache.h"
#include "CGALCache.h"
#include "Polygon2d.h"
#include "module.h"
#include "state.h"
#include "transformnode.h"
#include "linearextrudenode.h"
#include "rotateextrudenode.h"
#include "csgnode.h"
#include "cgaladvnode.h"
#include "projectionnode.h"
#include "CGAL_Nef_polyhedron.h"
#include "cgalutils.h"
#include "rendernode.h"
#include "clipper-utils.h"
#include "polyset-utils.h"
#include "PolySet.h"
#include "openscad.h" // get_fragments_from_r()
#include "printutils.h"
#include "svg.h"
#include "dxfdata.h"
#include <algorithm>
#include <boost/foreach.hpp>
#include <CGAL/convex_hull_2.h>
GeometryEvaluator::GeometryEvaluator(const class Tree &tree):
tree(tree)
{
}
/*!
Factory method returning a Geometry from the given node. If the
node is already cached, the cached Geometry will be returned
otherwise a new Geometry will be created from the node. If cache is
true, the newly created Geometry will be cached.
FIXME: This looks redundant
*/
shared_ptr<const Geometry> GeometryEvaluator::getGeometry(const AbstractNode &node, bool cache)
{
std::string cacheid = this->tree.getIdString(node);
if (GeometryCache::instance()->contains(cacheid)) {
#ifdef DEBUG
// For cache debugging
PRINTB("GeometryCache hit: %s", cacheid.substr(0, 40));
#endif
return GeometryCache::instance()->get(cacheid);
}
shared_ptr<const Geometry> geom(this->evaluateGeometry(node, true));
if (cache) GeometryCache::instance()->insert(cacheid, geom);
return geom;
}
bool GeometryEvaluator::isCached(const AbstractNode &node) const
{
return GeometryCache::instance()->contains(this->tree.getIdString(node));
}
// FIXME: This doesn't insert into cache. Fix this here or in client code
/*!
Set allownef to false to force the result to _not_ be a Nef polyhedron
*/
shared_ptr<const Geometry> GeometryEvaluator::evaluateGeometry(const AbstractNode &node,
bool allownef)
{
if (!isCached(node)) {
Traverser trav(*this, node, Traverser::PRE_AND_POSTFIX);
trav.execute();
if (!allownef) {
shared_ptr<const CGAL_Nef_polyhedron> N = dynamic_pointer_cast<const CGAL_Nef_polyhedron>(this->root);
if (N) {
assert(N->getDimension() != 2); // FIXME: Remove 2D code
if (N->getDimension() == 2) this->root.reset(N->convertToPolygon2d());
else if (N->getDimension() == 3) this->root.reset(N->convertToPolyset());
else this->root.reset();
GeometryCache::instance()->insert(this->tree.getIdString(node), this->root);
}
}
return this->root;
}
return GeometryCache::instance()->get(this->tree.getIdString(node));
}
GeometryEvaluator::ResultObject GeometryEvaluator::applyToChildren(const AbstractNode &node, OpenSCADOperator op)
{
unsigned int dim = 0;
BOOST_FOREACH(const Geometry::ChildItem &item, this->visitedchildren[node.index()]) {
if (item.second) {
if (!dim) dim = item.second->getDimension();
else if (dim != item.second->getDimension()) {
PRINT("WARNING: Mixing 2D and 3D objects is not supported.");
break;
}
}
}
if (dim == 2) return ResultObject(applyToChildren2D(node, op));
else if (dim == 3) return applyToChildren3D(node, op);
return ResultObject();
}
/*!
Applies the operator to all child nodes of the given node.
May return NULL or any 3D Geometry object (can be either PolySet or CGAL_Nef_polyhedron)
*/
GeometryEvaluator::ResultObject GeometryEvaluator::applyToChildren3D(const AbstractNode &node, OpenSCADOperator op)
{
if (op == OPENSCAD_HULL) {
return ResultObject(applyHull3D(node));
}
Geometry::ChildList children = collectChildren3D(node);
if (children.size() == 0) return ResultObject();
// Only one child -> this is a noop
if (children.size() == 1) return ResultObject(children.front().second);
CGAL_Nef_polyhedron *N = new CGAL_Nef_polyhedron;
BOOST_FOREACH(const Geometry::ChildItem &item, children) {
const shared_ptr<const Geometry> &chgeom = item.second;
shared_ptr<const CGAL_Nef_polyhedron> chN;
if (!chgeom) {
chN.reset(new CGAL_Nef_polyhedron(3)); // Create null polyhedron
}
else {
chN = dynamic_pointer_cast<const CGAL_Nef_polyhedron>(chgeom);
if (!chN) {
const PolySet *chps = dynamic_cast<const PolySet*>(chgeom.get());
if (chps) chN.reset(createNefPolyhedronFromGeometry(*chps));
}
}
// Initialize N on first iteration with first expected geometric object
if (N->isNull() && !N->isEmpty()) *N = chN->copy();
else CGALUtils::applyBinaryOperator(*N, *chN, op);
item.first->progress_report();
}
/*
CGAL_Nef_polyhedron *N = new CGAL_Nef_polyhedron;
BOOST_FOREACH(const Geometry::ChildItem &item, this->visitedchildren[node.index()]) {
const AbstractNode *chnode = item.first;
const shared_ptr<const Geometry> &chgeom = item.second;
// FIXME: Don't use deep access to modinst members
if (chnode->modinst->isBackground()) continue;
shared_ptr<const CGAL_Nef_polyhedron> chN = dynamic_pointer_cast<const CGAL_Nef_polyhedron>(chgeom);
if (!chN) {
shared_ptr<const PolySet> chP = dynamic_pointer_cast<const PolySet>(chgeom);
if (chP) chN.reset(createNefPolyhedronFromGeometry(*chP));
}
// NB! We insert into the cache here to ensure that all children of
// a node is a valid object. If we inserted as we created them, the
// cache could have been modified before we reach this point due to a large
// sibling object.
smartCache(node, chN);
if (chgeom) {
if (chgeom->getDimension() == 3) {
// Initialize N on first iteration with first expected geometric object
if (N->isNull() && !N->isEmpty()) *N = chN->copy();
else CGALUtils::applyBinaryOperator(*N, *chN, op);
}
else {
// FIXME: Fix error message
PRINT("ERROR: this operation is not defined for 2D child objects!");
}
}
chnode->progress_report();
}
*/
return ResultObject(N);
}
Polygon2d *GeometryEvaluator::applyHull2D(const AbstractNode &node)
{
std::vector<const Polygon2d *> children = collectChildren2D(node);
Polygon2d *geometry = NULL;
// Collect point cloud
std::list<CGAL_Nef_polyhedron2::Point> points;
BOOST_FOREACH(const Polygon2d *p, children) {
BOOST_FOREACH(const Outline2d &o, p->outlines()) {
BOOST_FOREACH(const Vector2d &v, o) {
points.push_back(CGAL_Nef_polyhedron2::Point(v[0], v[1]));
}
}
}
if (points.size() > 0) {
// Apply hull
std::list<CGAL_Nef_polyhedron2::Point> result;
CGAL::convex_hull_2(points.begin(), points.end(), std::back_inserter(result));
// Construct Polygon2d
Outline2d outline;
BOOST_FOREACH(const CGAL_Nef_polyhedron2::Point &p, result) {
outline.push_back(Vector2d(CGAL::to_double(p[0]), CGAL::to_double(p[1])));
}
geometry = new Polygon2d();
geometry->addOutline(outline);
}
return geometry;
}
Geometry *GeometryEvaluator::applyHull3D(const AbstractNode &node)
{
Geometry::ChildList children = collectChildren3D(node);
CGAL_Polyhedron P;
if (CGALUtils::applyHull(children, P)) {
return new CGAL_Nef_polyhedron(new CGAL_Nef_polyhedron3(P));
}
return NULL;
}
void GeometryEvaluator::applyResize3D(CGAL_Nef_polyhedron &N,
const Vector3d &newsize,
const Eigen::Matrix<bool,3,1> &autosize)
{
assert(N.getDimension() != 2); // FIXME: Remove 2D code
// Based on resize() in Giles Bathgate's RapCAD (but not exactly)
if (N.isNull() || N.isEmpty()) return;
CGAL_Iso_cuboid_3 bb = bounding_box(*N.p3);
std::vector<NT3> scale, bbox_size;
for (int i=0;i<3;i++) {
scale.push_back(NT3(1));
bbox_size.push_back(bb.max_coord(i) - bb.min_coord(i));
}
int newsizemax_index = 0;
for (int i=0;i<N.getDimension();i++) {
if (newsize[i]) {
if (bbox_size[i] == NT3(0)) {
PRINT("WARNING: Resize in direction normal to flat object is not implemented");
return;
}
else {
scale[i] = NT3(newsize[i]) / bbox_size[i];
}
if (newsize[i] > newsize[newsizemax_index]) newsizemax_index = i;
}
}
NT3 autoscale = NT3(1);
if (newsize[newsizemax_index] != 0) {
autoscale = NT3(newsize[newsizemax_index]) / bbox_size[newsizemax_index];
}
for (int i=0;i<N.getDimension();i++) {
if (autosize[i] && newsize[i]==0) scale[i] = autoscale;
}
Eigen::Matrix4d t;
t << CGAL::to_double(scale[0]), 0, 0, 0,
0, CGAL::to_double(scale[1]), 0, 0,
0, 0, CGAL::to_double(scale[2]), 0,
0, 0, 0, 1;
N.transform(Transform3d(t));
return;
}
Polygon2d *GeometryEvaluator::applyMinkowski2D(const AbstractNode &node)
{
std::vector<const Polygon2d *> children = collectChildren2D(node);
if (children.size() > 0) {
bool first = false;
ClipperLib::Paths result = ClipperUtils::fromPolygon2d(*children[0]);
for (int i=1;i<children.size();i++) {
ClipperLib::Path &temp = result[0];
const Polygon2d *chgeom = children[i];
ClipperLib::Path shape = ClipperUtils::fromOutline2d(chgeom->outlines()[0]);
ClipperLib::MinkowskiSum(temp, shape, result, true);
}
// The results may contain holes due to ClipperLib failing to maintain
// solidity of minkowski results:
// https://sourceforge.net/p/polyclipping/discussion/1148419/thread/8488d4e8/
ClipperLib::Clipper clipper;
BOOST_FOREACH(ClipperLib::Path &p, result) {
if (ClipperLib::Orientation(p)) std::reverse(p.begin(), p.end());
clipper.AddPath(p, ClipperLib::ptSubject, true);
}
clipper.Execute(ClipperLib::ctUnion, result, ClipperLib::pftNonZero, ClipperLib::pftNonZero);
return ClipperUtils::toPolygon2d(result);
}
return NULL;
}
std::vector<const class Polygon2d *> GeometryEvaluator::collectChildren2D(const AbstractNode &node)
{
std::vector<const Polygon2d *> children;
BOOST_FOREACH(const Geometry::ChildItem &item, this->visitedchildren[node.index()]) {
const AbstractNode *chnode = item.first;
const shared_ptr<const Geometry> &chgeom = item.second;
// FIXME: Don't use deep access to modinst members
if (chnode->modinst->isBackground()) continue;
// NB! We insert into the cache here to ensure that all children of
// a node is a valid object. If we inserted as we created them, the
// cache could have been modified before we reach this point due to a large
// sibling object.
if (!isCached(*chnode)) {
GeometryCache::instance()->insert(this->tree.getIdString(*chnode), chgeom);
}
if (chgeom && chgeom->getDimension() == 2) {
const Polygon2d *polygons = dynamic_cast<const Polygon2d *>(chgeom.get());
assert(polygons);
children.push_back(polygons);
}
else {
PRINT("ERROR: Only 2D children are supported by this operation!");
}
}
return children;
}
void GeometryEvaluator::smartCache(const AbstractNode &node,
const shared_ptr<const Geometry> &geom)
{
// Since we can generate both Nef and non-Nef geometry, we need to insert it into
// the appropriate cache
shared_ptr<const CGAL_Nef_polyhedron> N = dynamic_pointer_cast<const CGAL_Nef_polyhedron>(geom);
if (N) {
if (!CGALCache::instance()->contains(this->tree.getIdString(node))) {
CGALCache::instance()->insert(this->tree.getIdString(node), N);
}
}
else {
if (!isCached(node)) {
if (!GeometryCache::instance()->insert(this->tree.getIdString(node), geom)) {
PRINT("WARNING: GeometryEvaluator: Root node didn't fit into cache");
}
}
}
}
Geometry::ChildList GeometryEvaluator::collectChildren3D(const AbstractNode &node)
{
Geometry::ChildList children;
BOOST_FOREACH(const Geometry::ChildItem &item, this->visitedchildren[node.index()]) {
const AbstractNode *chnode = item.first;
const shared_ptr<const Geometry> &chgeom = item.second;
// FIXME: Don't use deep access to modinst members
if (chnode->modinst->isBackground()) continue;
// NB! We insert into the cache here to ensure that all children of
// a node is a valid object. If we inserted as we created them, the
// cache could have been modified before we reach this point due to a large
// sibling object.
smartCache(*chnode, chgeom);
if (chgeom && chgeom->getDimension() == 3) {
children.push_back(item);
}
else {
PRINT("ERROR: Only 3D children are supported by this operation!");
shared_ptr<const Geometry> nullptr;
children.push_back(Geometry::ChildItem(item.first, nullptr));
}
}
return children;
}
/*!
*/
Polygon2d *GeometryEvaluator::applyToChildren2D(const AbstractNode &node, OpenSCADOperator op)
{
if (op == OPENSCAD_MINKOWSKI) {
return applyMinkowski2D(node);
}
else if (op == OPENSCAD_HULL) {
return applyHull2D(node);
}
ClipperLib::Clipper sumclipper;
bool first = true;
BOOST_FOREACH(const Geometry::ChildItem &item, this->visitedchildren[node.index()]) {
const AbstractNode *chnode = item.first;
const shared_ptr<const Geometry> &chgeom = item.second;
// FIXME: Don't use deep access to modinst members
if (chnode->modinst->isBackground()) continue;
// NB! We insert into the cache here to ensure that all children of
// a node is a valid object. If we inserted as we created them, the
// cache could have been modified before we reach this point due to a large
// sibling object.
if (!isCached(*chnode)) {
GeometryCache::instance()->insert(this->tree.getIdString(*chnode), chgeom);
}
if (chgeom) {
if (chgeom->getDimension() == 2) {
shared_ptr<const Polygon2d> polygons = dynamic_pointer_cast<const Polygon2d>(chgeom);
// FIXME: This will trigger on e.g. linear_extrude of minkowski sums.
assert(polygons);
// The first Clipper operation will sanitize the polygon, ensuring
// contours/holes have the correct winding order
ClipperLib::Paths result = ClipperUtils::fromPolygon2d(*polygons);
result = ClipperUtils::process(result,
ClipperLib::ctUnion,
ClipperLib::pftEvenOdd);
// Add correctly winded polygons to the main clipper
sumclipper.AddPaths(result, first ? ClipperLib::ptSubject : ClipperLib::ptClip, true);
}
else {
// FIXME: Wrong error message
PRINT("ERROR: linear_extrude() is not defined for 3D child objects!");
}
}
chnode->progress_report();
if (first) first = !first;
}
ClipperLib::ClipType clipType;
switch (op) {
case OPENSCAD_UNION:
clipType = ClipperLib::ctUnion;
break;
case OPENSCAD_INTERSECTION:
clipType = ClipperLib::ctIntersection;
break;
case OPENSCAD_DIFFERENCE:
clipType = ClipperLib::ctDifference;
break;
default:
PRINTB("Error: Unknown boolean operation %d", int(op));
return NULL;
break;
}
// Perform the main op
ClipperLib::Paths sumresult;
sumclipper.Execute(clipType, sumresult, ClipperLib::pftEvenOdd, ClipperLib::pftEvenOdd);
if (sumresult.size() == 0) return NULL;
// The returned result will have outlines ordered according to whether
// they're positive or negative: Positive outlines counter-clockwise and
// negative outlines clockwise.
// FIXME: We might want to introduce a flag in Polygon2d to signify this
return ClipperUtils::toPolygon2d(sumresult);
}
/*!
Adds ourself to out parent's list of traversed children.
Call this for _every_ node which affects output during traversal.
Usually, this should be called from the postfix stage, but for some nodes,
we defer traversal letting other components (e.g. CGAL) render the subgraph,
and we'll then call this from prefix and prune further traversal.
The added geometry can be NULL if it wasn't possible to evaluate it.
*/
void GeometryEvaluator::addToParent(const State &state,
const AbstractNode &node,
const shared_ptr<const Geometry> &geom)
{
this->visitedchildren.erase(node.index());
if (state.parent()) {
this->visitedchildren[state.parent()->index()].push_back(std::make_pair(&node, geom));
}
else {
// Root node, insert into cache
smartCache(node, geom);
this->root = geom;
}
}
/*!
Custom nodes are handled here => implicit union
*/
Response GeometryEvaluator::visit(State &state, const AbstractNode &node)
{
if (state.isPrefix() && isCached(node)) return PruneTraversal;
if (state.isPostfix()) {
shared_ptr<const class Geometry> geom;
if (!isCached(node)) {
geom = applyToChildren(node, OPENSCAD_UNION).constptr();
}
else {
geom = GeometryCache::instance()->get(this->tree.getIdString(node));
}
addToParent(state, node, geom);
}
return ContinueTraversal;
}
/*
FIXME: Where do we handle nodes which should be sent to CGAL?
if (state.isPrefix() && isCached(node)) return PruneTraversal;
if (state.isPostfix()) {
shared_ptr<const Geometry> geom;
if (!isCached(node)) {
CGAL_Nef_polyhedron N = this->cgalevaluator->evaluateCGALMesh(node);
CGALCache::instance()->insert(this->tree.getIdString(node), N);
PolySet *ps = NULL;
if (!N.isNull()) ps = N.convertToPolyset();
geom.reset(ps);
}
else {
geom = GeometryCache::instance()->get(this->tree.getIdString(node));
}
addToParent(state, node, geom);
}
return ContinueTraversal;
*/
/*!
Leaf nodes can create their own geometry, so let them do that
input: None
output: PolySet or Polygon2d
*/
Response GeometryEvaluator::visit(State &state, const LeafNode &node)
{
// FIXME: We should run the result of 2D geometry to Clipper to ensure
// correct winding order
if (state.isPrefix()) {
shared_ptr<const Geometry> geom;
if (!isCached(node)) {
const Geometry *geometry = node.createGeometry();
const Polygon2d *polygons = dynamic_cast<const Polygon2d*>(geometry);
if (polygons) {
ClipperLib::Paths result = ClipperUtils::fromPolygon2d(*polygons);
result = ClipperUtils::process(result,
ClipperLib::ctUnion,
ClipperLib::pftEvenOdd);
Polygon2d *p = ClipperUtils::toPolygon2d(result);
delete geometry;
geometry = p;
}
geom.reset(geometry);
}
else geom = GeometryCache::instance()->get(this->tree.getIdString(node));
addToParent(state, node, geom);
}
return PruneTraversal;
}
/*!
input: List of 2D or 3D objects (not mixed)
output: Polygon2d or 3D PolySet
operation:
o Perform csg op on children
*/
Response GeometryEvaluator::visit(State &state, const CsgNode &node)
{
if (state.isPrefix() && isCached(node)) return PruneTraversal;
if (state.isPostfix()) {
shared_ptr<const Geometry> geom;
if (!isCached(node)) {
geom = applyToChildren(node, node.type).constptr();
}
else {
geom = GeometryCache::instance()->get(this->tree.getIdString(node));
}
addToParent(state, node, geom);
}
return ContinueTraversal;
}
/*!
input: List of 2D or 3D objects (not mixed)
output: Polygon2d or 3D PolySet
operation:
o Union all children
o Perform transform
*/
Response GeometryEvaluator::visit(State &state, const TransformNode &node)
{
if (state.isPrefix() && isCached(node)) return PruneTraversal;
if (state.isPostfix()) {
shared_ptr<const class Geometry> geom;
if (!isCached(node)) {
if (matrix_contains_infinity(node.matrix) || matrix_contains_nan(node.matrix)) {
// due to the way parse/eval works we can't currently distinguish between NaN and Inf
PRINT("Warning: Transformation matrix contains Not-a-Number and/or Infinity - removing object.");
}
else {
// First union all children
ResultObject res = applyToChildren(node, OPENSCAD_UNION);
if ((geom = res.constptr())) {
if (geom->getDimension() == 2) {
shared_ptr<const Polygon2d> polygons = dynamic_pointer_cast<const Polygon2d>(geom);
assert(polygons);
// If we got a const object, make a copy
shared_ptr<Polygon2d> newpoly;
if (res.isConst()) newpoly.reset(new Polygon2d(*polygons));
else newpoly = dynamic_pointer_cast<Polygon2d>(res.ptr());
Transform2d mat2;
mat2.matrix() <<
node.matrix(0,0), node.matrix(0,1), node.matrix(0,3),
node.matrix(1,0), node.matrix(1,1), node.matrix(1,3),
node.matrix(3,0), node.matrix(3,1), node.matrix(3,3);
newpoly->transform(mat2);
geom = newpoly;
}
else if (geom->getDimension() == 3) {
shared_ptr<const PolySet> ps = dynamic_pointer_cast<const PolySet>(geom);
if (ps) {
// If we got a const object, make a copy
shared_ptr<PolySet> newps;
if (res.isConst()) newps.reset(new PolySet(*ps));
else newps = dynamic_pointer_cast<PolySet>(res.ptr());
newps->transform(node.matrix);
geom = newps;
}
else {
shared_ptr<const CGAL_Nef_polyhedron> N = dynamic_pointer_cast<const CGAL_Nef_polyhedron>(geom);
assert(N);
// If we got a const object, make a copy
shared_ptr<CGAL_Nef_polyhedron> newN;
if (res.isConst()) newN.reset(new CGAL_Nef_polyhedron(*N));
else newN = dynamic_pointer_cast<CGAL_Nef_polyhedron>(res.ptr());
newN->transform(node.matrix);
geom = newN;
}
}
}
}
}
else {
geom = GeometryCache::instance()->get(this->tree.getIdString(node));
}
addToParent(state, node, geom);
}
return ContinueTraversal;
}
static void translate_PolySet(PolySet &ps, const Vector3d &translation)
{
BOOST_FOREACH(PolySet::Polygon &p, ps.polygons) {
BOOST_FOREACH(Vector3d &v, p) {
v += translation;
}
}
}
static void add_slice(PolySet *ps, const Polygon2d &poly,
double rot1, double rot2,
double h1, double h2,
const Vector2d &scale1,
const Vector2d &scale2)
{
Eigen::Affine2d trans1(Eigen::Scaling(scale1) * Eigen::Rotation2D<double>(-rot1*M_PI/180));
Eigen::Affine2d trans2(Eigen::Scaling(scale2) * Eigen::Rotation2D<double>(-rot2*M_PI/180));
// FIXME: If scale2 == 0 we need to handle tessellation separately
bool splitfirst = sin(rot1 - rot2) >= 0.0;
BOOST_FOREACH(const Outline2d &o, poly.outlines()) {
Vector2d prev1 = trans1 * o[0];
Vector2d prev2 = trans2 * o[0];
for (size_t i=1;i<=o.size();i++) {
Vector2d curr1 = trans1 * o[i % o.size()];
Vector2d curr2 = trans2 * o[i % o.size()];
ps->append_poly();
if (splitfirst) {
ps->insert_vertex(prev1[0], prev1[1], h1);
ps->insert_vertex(curr2[0], curr2[1], h2);
ps->insert_vertex(curr1[0], curr1[1], h1);
if (scale2[0] > 0 || scale2[1] > 0) {
ps->append_poly();
ps->insert_vertex(curr2[0], curr2[1], h2);
ps->insert_vertex(prev1[0], prev1[1], h1);
ps->insert_vertex(prev2[0], prev2[1], h2);
}
}
else {
ps->insert_vertex(prev1[0], prev1[1], h1);
ps->insert_vertex(prev2[0], prev2[1], h2);
ps->insert_vertex(curr1[0], curr1[1], h1);
if (scale2[0] > 0 || scale2[1] > 0) {
ps->append_poly();
ps->insert_vertex(prev2[0], prev2[1], h2);
ps->insert_vertex(curr2[0], curr2[1], h2);
ps->insert_vertex(curr1[0], curr1[1], h1);
}
}
prev1 = curr1;
prev2 = curr2;
}
}
}
/*!
Input to extrude should be clean. This means non-intersecting, correct winding order
etc., the input coming from a library like Clipper.
*/
static Geometry *extrudePolygon(const LinearExtrudeNode &node, const Polygon2d &poly)
{
PolySet *ps = new PolySet();
ps->setConvexity(node.convexity);
if (node.height <= 0) return ps;
double h1, h2;
if (node.center) {
h1 = -node.height/2.0;
h2 = +node.height/2.0;
} else {
h1 = 0;
h2 = node.height;
}
PolySet *ps_bottom = poly.tessellate(); // bottom
// Flip vertex ordering for bottom polygon
BOOST_FOREACH(PolySet::Polygon &p, ps_bottom->polygons) {
std::reverse(p.begin(), p.end());
}
translate_PolySet(*ps_bottom, Vector3d(0,0,h1));
ps->append(*ps_bottom);
delete ps_bottom;
if (node.scale_x > 0 || node.scale_y > 0) {
Polygon2d top_poly(poly);
Eigen::Affine2d trans(Eigen::Scaling(node.scale_x, node.scale_y) *
Eigen::Rotation2D<double>(-node.twist*M_PI/180));
top_poly.transform(trans); // top
PolySet *ps_top = top_poly.tessellate();
translate_PolySet(*ps_top, Vector3d(0,0,h2));
ps->append(*ps_top);
delete ps_top;
}
size_t slices = node.has_twist ? node.slices : 1;
for (int j = 0; j < slices; j++) {
double rot1 = node.twist*j / slices;
double rot2 = node.twist*(j+1) / slices;
double height1 = h1 + (h2-h1)*j / slices;
double height2 = h1 + (h2-h1)*(j+1) / slices;
Vector2d scale1(1 - (1-node.scale_x)*j / slices,
1 - (1-node.scale_y)*j / slices);
Vector2d scale2(1 - (1-node.scale_x)*(j+1) / slices,
1 - (1-node.scale_y)*(j+1) / slices);
add_slice(ps, poly, rot1, rot2, height1, height2, scale1, scale2);
}
return ps;
}
/*!
input: List of 2D objects
output: 3D PolySet
operation:
o Union all children
o Perform extrude
*/
Response GeometryEvaluator::visit(State &state, const LinearExtrudeNode &node)
{
if (state.isPrefix() && isCached(node)) return PruneTraversal;
if (state.isPostfix()) {
shared_ptr<const Geometry> geom;
if (!isCached(node)) {
const Geometry *geometry;
if (!node.filename.empty()) {
DxfData dxf(node.fn, node.fs, node.fa, node.filename, node.layername, node.origin_x, node.origin_y, node.scale_x);
geometry = dxf.toPolygon2d();
}
else {
geometry = applyToChildren2D(node, OPENSCAD_UNION);
}
if (geometry) {
const Polygon2d *polygons = dynamic_cast<const Polygon2d*>(geometry);
Geometry *extruded = extrudePolygon(node, *polygons);
assert(extruded);
geom.reset(extruded);
delete geometry;
}
}
else {
geom = GeometryCache::instance()->get(this->tree.getIdString(node));
}
addToParent(state, node, geom);
}
return ContinueTraversal;
}
static void fill_ring(std::vector<Vector3d> &ring, const Outline2d &o, double a)
{
for (int i=0;i<o.size();i++) {
ring[i][0] = o[i][0] * sin(a);
ring[i][1] = o[i][0] * cos(a);
ring[i][2] = o[i][1];
}
}
/*!
Input to extrude should be clean. This means non-intersecting, correct winding order
etc., the input coming from a library like Clipper.
*/
static Geometry *rotatePolygon(const RotateExtrudeNode &node, const Polygon2d &poly)
{
PolySet *ps = new PolySet();
ps->setConvexity(node.convexity);
BOOST_FOREACH(const Outline2d &o, poly.outlines()) {
double min_x = 0;
double max_x = 0;
BOOST_FOREACH(const Vector2d &v, o) {
min_x = fmin(min_x, v[0]);
max_x = fmax(max_x, v[0]);
if ((max_x - min_x) > max_x && (max_x - min_x) > fabs(min_x)) {
PRINTB("ERROR: all points for rotate_extrude() must have the same X coordinate sign (range is %.2f -> %.2f)", min_x % max_x);
delete ps;
return NULL;
}
}
int fragments = get_fragments_from_r(max_x - min_x, node.fn, node.fs, node.fa);
std::vector<Vector3d> rings[2];
rings[0].reserve(o.size());
rings[1].reserve(o.size());
fill_ring(rings[0], o, -M_PI/2); // first ring
for (int j = 0; j <= fragments; j++) {
double a = ((j+1)*2*M_PI) / fragments - M_PI/2; // start on the X axis
fill_ring(rings[(j+1)%2], o, a);
for (size_t i=0;i<o.size();i++) {
ps->append_poly();
ps->insert_vertex(rings[j%2][i]);
ps->insert_vertex(rings[(j+1)%2][(i+1)%o.size()]);
ps->insert_vertex(rings[j%2][(i+1)%o.size()]);
ps->append_poly();
ps->insert_vertex(rings[j%2][i]);
ps->insert_vertex(rings[(j+1)%2][i]);
ps->insert_vertex(rings[(j+1)%2][(i+1)%o.size()]);
}
}
}
return ps;
}
/*!
input: List of 2D objects
output: 3D PolySet
operation:
o Union all children
o Perform extrude
*/
Response GeometryEvaluator::visit(State &state, const RotateExtrudeNode &node)
{
if (state.isPrefix() && isCached(node)) return PruneTraversal;
if (state.isPostfix()) {
shared_ptr<const Geometry> geom;
if (!isCached(node)) {
const Geometry *geometry;
if (!node.filename.empty()) {
DxfData dxf(node.fn, node.fs, node.fa, node.filename, node.layername, node.origin_x, node.origin_y, node.scale);
geometry = dxf.toPolygon2d();
}
else {
geometry = applyToChildren2D(node, OPENSCAD_UNION);
}
if (geometry) {
const Polygon2d *polygons = dynamic_cast<const Polygon2d*>(geometry);
Geometry *rotated = rotatePolygon(node, *polygons);
geom.reset(rotated);
delete geometry;
}
}
else {
geom = GeometryCache::instance()->get(this->tree.getIdString(node));
}
addToParent(state, node, geom);
}
return ContinueTraversal;
}
/*!
Handles non-leaf PolyNodes; projection
*/
Response GeometryEvaluator::visit(State &state, const AbstractPolyNode &node)
{
assert(false);
return AbortTraversal;
}
/*!
input: List of 3D objects
output: Polygon2d
operation:
o Union all children
o Perform projection
*/
Response GeometryEvaluator::visit(State &state, const ProjectionNode &node)
{
if (state.isPrefix() && isCached(node)) return PruneTraversal;
if (state.isPostfix()) {
shared_ptr<const class Geometry> geom;
if (!isCached(node)) {
if (!node.cut_mode) {
ClipperLib::Clipper sumclipper;
BOOST_FOREACH(const Geometry::ChildItem &item, this->visitedchildren[node.index()]) {
const AbstractNode *chnode = item.first;
const shared_ptr<const Geometry> &chgeom = item.second;
// FIXME: Don't use deep access to modinst members
if (chnode->modinst->isBackground()) continue;
const Polygon2d *poly = NULL;
// CGAL version of Geometry projection
// Causes crashes in createNefPolyhedronFromGeometry() for this model:
// projection(cut=false) {
// cube(10);
// difference() {
// sphere(10);
// cylinder(h=30, r=5, center=true);
// }
// }
#if 0
shared_ptr<const PolySet> chPS = dynamic_pointer_cast<const PolySet>(chgeom);
const PolySet *ps2d = NULL;
shared_ptr<const CGAL_Nef_polyhedron> chN = dynamic_pointer_cast<const CGAL_Nef_polyhedron>(chgeom);
if (chN) chPS.reset(chN->convertToPolyset());
if (chPS) ps2d = PolysetUtils::flatten(*chPS);
if (ps2d) {
CGAL_Nef_polyhedron *N2d = createNefPolyhedronFromGeometry(*ps2d);
poly = N2d->convertToPolygon2d();
}
#endif
// Clipper version of Geometry projection
// Clipper doesn't handle meshes very well.
// It's better in V6 but not quite there. FIXME: stand-alone example.
#if 1
// project chgeom -> polygon2d
shared_ptr<const PolySet> chPS = dynamic_pointer_cast<const PolySet>(chgeom);
if (!chPS) {
shared_ptr<const CGAL_Nef_polyhedron> chN = dynamic_pointer_cast<const CGAL_Nef_polyhedron>(chgeom);
if (chN) {
chPS.reset(chN->convertToPolyset());
}
}
if (chPS) poly = PolysetUtils::project(*chPS);
#endif
if (poly) {
ClipperLib::Paths result = ClipperUtils::fromPolygon2d(*poly);
// Using NonZero ensures that we don't create holes from polygons sharing
// edges since we're unioning a mesh
result = ClipperUtils::process(result,
ClipperLib::ctUnion,
ClipperLib::pftNonZero);
// Add correctly winded polygons to the main clipper
sumclipper.AddPaths(result, ClipperLib::ptSubject, true);
}
}
ClipperLib::Paths sumresult;
// This is key - without StrictlySimple, we tend to get self-intersecting results
sumclipper.StrictlySimple(true);
sumclipper.Execute(ClipperLib::ctUnion, sumresult, ClipperLib::pftNonZero, ClipperLib::pftNonZero);
if (sumresult.size() > 0) geom.reset(ClipperUtils::toPolygon2d(sumresult));
}
else {
shared_ptr<const Geometry> newgeom = applyToChildren3D(node, OPENSCAD_UNION).constptr();
if (newgeom) {
shared_ptr<const CGAL_Nef_polyhedron> Nptr = dynamic_pointer_cast<const CGAL_Nef_polyhedron>(newgeom);
if (!Nptr) {
Nptr.reset(createNefPolyhedronFromGeometry(*newgeom));
}
if (!Nptr->isNull()) {
CGAL_Nef_polyhedron nef_poly = CGALUtils::project(*Nptr, node.cut_mode);
Polygon2d *poly = nef_poly.convertToPolygon2d();
assert(poly);
poly->setConvexity(node.convexity);
geom.reset(poly);
}
}
}
}
else {
geom = GeometryCache::instance()->get(this->tree.getIdString(node));
}
addToParent(state, node, geom);
}
return ContinueTraversal;
}
/*!
input: List of 2D or 3D objects (not mixed)
output: PolySet (FIXME: implement Polygon2d)
operation:
o Perform cgal operation
*/
Response GeometryEvaluator::visit(State &state, const CgaladvNode &node)
{
if (state.isPrefix() && isCached(node)) return PruneTraversal;
if (state.isPostfix()) {
shared_ptr<const Geometry> geom;
if (!isCached(node)) {
switch (node.type) {
case MINKOWSKI: {
geom = applyToChildren(node, OPENSCAD_MINKOWSKI).constptr();
break;
}
case HULL: {
geom = applyToChildren(node, OPENSCAD_HULL).constptr();
break;
}
case RESIZE: {
ResultObject res = applyToChildren(node, OPENSCAD_UNION);
geom = res.constptr();
shared_ptr<const CGAL_Nef_polyhedron> N = dynamic_pointer_cast<const CGAL_Nef_polyhedron>(res.constptr());
if (N) {
// If we got a const object, make a copy
shared_ptr<CGAL_Nef_polyhedron> newN;
if (res.isConst()) newN.reset(new CGAL_Nef_polyhedron(*N));
else newN = dynamic_pointer_cast<CGAL_Nef_polyhedron>(res.ptr());
applyResize3D(*newN, node.newsize, node.autosize);
geom = newN;
}
else {
shared_ptr<const Polygon2d> poly = dynamic_pointer_cast<const Polygon2d>(res.constptr());
if (poly) {
// If we got a const object, make a copy
shared_ptr<Polygon2d> newpoly;
if (res.isConst()) newpoly.reset(new Polygon2d(*poly));
else newpoly = dynamic_pointer_cast<Polygon2d>(res.ptr());
newpoly->resize(Vector2d(node.newsize[0], node.newsize[1]),
Eigen::Matrix<bool,2,1>(node.autosize[0], node.autosize[1]));
}
else {
shared_ptr<const PolySet> ps = dynamic_pointer_cast<const PolySet>(res.constptr());
if (ps) {
// If we got a const object, make a copy
shared_ptr<PolySet> newps;
if (res.isConst()) newps.reset(new PolySet(*ps));
else newps = dynamic_pointer_cast<PolySet>(res.ptr());
newps->resize(node.newsize, node.autosize);
geom = newps;
}
else {
assert(false);
}
}
}
break;
}
default:
assert(false && "not implemented");
}
// MINKOWSKI
// 2D children -> Polygon2d, apply Clipper minkowski
// 3D children -> Nef, apply CGAL minkowski
// HULL
// 2D children -> Polygon2d (or really point clouds), apply 2D hull (CGAL)
// 3D children -> PolySet (or really point clouds), apply 2D hull (CGAL)
// RESIZE
// 2D children -> Polygon2d -> union -> apply resize
// 3D children -> PolySet -> union -> apply resize
}
else {
geom = GeometryCache::instance()->get(this->tree.getIdString(node));
}
addToParent(state, node, geom);
}
return ContinueTraversal;
}
/*!
input: List of 2D or 3D objects (not mixed)
output: PolySet (FIXME: implement Polygon2d?)
operation:
o Render to PolySet (using CGAL or Clipper)
*/
Response GeometryEvaluator::visit(State &state, const RenderNode &node)
{
if (state.isPrefix() && isCached(node)) return PruneTraversal;
if (state.isPostfix()) {
shared_ptr<const Geometry> geom;
if (!isCached(node)) {
geom = applyToChildren(node, OPENSCAD_UNION).constptr();
shared_ptr<const CGAL_Nef_polyhedron> N = dynamic_pointer_cast<const CGAL_Nef_polyhedron>(geom);
if (N) {
assert(N->getDimension() != 2); // FIXME: Remove 2D code
PolySet *ps = NULL;
if (!N->isNull()) {
if (N->getDimension() == 3 && !N->p3->is_simple()) {
PRINT("WARNING: Body of render() isn't valid 2-manifold!");
}
else {
ps = N->convertToPolyset();
if (ps) ps->setConvexity(node.convexity);
}
}
geom.reset(ps);
}
}
else {
geom = GeometryCache::instance()->get(this->tree.getIdString(node));
}
addToParent(state, node, geom);
}
return ContinueTraversal;
}