1 #include "GeometryEvaluator.h"
2 #include "Tree.h"
3 #include "GeometryCache.h"
4 #include "CGALCache.h"
5 #include "Polygon2d.h"
6 #include "module.h"
7 #include "ModuleInstantiation.h"
8 #include "state.h"
9 #include "offsetnode.h"
10 #include "transformnode.h"
11 #include "linearextrudenode.h"
12 #include "rotateextrudenode.h"
13 #include "csgnode.h"
14 #include "cgaladvnode.h"
15 #include "projectionnode.h"
16 #include "csgops.h"
17 #include "textnode.h"
18 #include "CGAL_Nef_polyhedron.h"
19 #include "cgalutils.h"
20 #include "rendernode.h"
21 #include "clipper-utils.h"
22 #include "polyset-utils.h"
23 #include "polyset.h"
24 #include "calc.h"
25 #include "printutils.h"
26 #include "svg.h"
27 #include "calc.h"
28 #include "dxfdata.h"
29 #include "degree_trig.h"
30 #include <ciso646> // C alternative tokens (xor)
31 #include <algorithm>
32 #include "boost-utils.h"
33
34 #pragma push_macro("NDEBUG")
35 #undef NDEBUG
36 #include <CGAL/convex_hull_2.h>
37 #include <CGAL/Point_2.h>
38 #pragma pop_macro("NDEBUG")
39
GeometryEvaluator(const class Tree & tree)40 GeometryEvaluator::GeometryEvaluator(const class Tree &tree):
41 tree(tree)
42 {
43 }
44
45 /*!
46 Set allownef to false to force the result to _not_ be a Nef polyhedron
47 */
evaluateGeometry(const AbstractNode & node,bool allownef)48 shared_ptr<const Geometry> GeometryEvaluator::evaluateGeometry(const AbstractNode &node,
49 bool allownef)
50 {
51 const std::string &key = this->tree.getIdString(node);
52 if (!GeometryCache::instance()->contains(key)) {
53 shared_ptr<const CGAL_Nef_polyhedron> N;
54 if (CGALCache::instance()->contains(key)) {
55 N = CGALCache::instance()->get(key);
56 }
57
58 // If not found in any caches, we need to evaluate the geometry
59 if (N) {
60 this->root = N;
61 }
62 else {
63 this->traverse(node);
64 }
65
66 if (!allownef) {
67 if (shared_ptr<const CGAL_Nef_polyhedron> N = dynamic_pointer_cast<const CGAL_Nef_polyhedron>(this->root)) {
68 PolySet *ps = new PolySet(3);
69 ps->setConvexity(N->getConvexity());
70 this->root.reset(ps);
71 if (!N->isEmpty()) {
72 bool err = CGALUtils::createPolySetFromNefPolyhedron3(*N->p3, *ps);
73 if (err) {
74 LOG(message_group::Error,Location::NONE,"","Nef->PolySet failed."); }
75 }
76 }
77
78 // We cannot render concave polygons, so tessellate any 3D PolySets
79 auto ps = dynamic_pointer_cast<const PolySet>(this->root);
80 if (ps && !ps->isEmpty()) {
81 // Since is_convex() doesn't handle non-planar faces, we need to tessellate
82 // also in the indeterminate state so we cannot just use a boolean comparison. See #1061
83 bool convex = bool(ps->convexValue()); // bool is true only if tribool is true, (not indeterminate and not false)
84 if (!convex) {
85 assert(ps->getDimension() == 3);
86 auto ps_tri = new PolySet(3, ps->convexValue());
87 ps_tri->setConvexity(ps->getConvexity());
88 PolysetUtils::tessellate_faces(*ps, *ps_tri);
89 this->root.reset(ps_tri);
90 }
91 }
92 }
93 smartCacheInsert(node, this->root);
94 return this->root;
95 }
96 return GeometryCache::instance()->get(key);
97 }
98
isValidDim(const Geometry::GeometryItem & item,unsigned int & dim) const99 bool GeometryEvaluator::isValidDim(const Geometry::GeometryItem &item, unsigned int &dim) const {
100 if (!item.first->modinst->isBackground() && item.second) {
101 if (!dim) dim = item.second->getDimension();
102 else if (dim != item.second->getDimension() && !item.second->isEmpty()) {
103 LOG(message_group::Warning,item.first->modinst->location(),this->tree.getDocumentPath(),"Mixing 2D and 3D objects is not supported");
104 return false;
105 }
106 }
107 return true;
108 }
109
applyToChildren(const AbstractNode & node,OpenSCADOperator op)110 GeometryEvaluator::ResultObject GeometryEvaluator::applyToChildren(const AbstractNode &node, OpenSCADOperator op)
111 {
112 CGALUtils::CGALErrorBehaviour behaviour{CGAL::THROW_EXCEPTION};
113
114 unsigned int dim = 0;
115 for(const auto &item : this->visitedchildren[node.index()]) {
116 if (!isValidDim(item, dim)) break;
117 }
118 if (dim == 2) return ResultObject(applyToChildren2D(node, op));
119 else if (dim == 3) return applyToChildren3D(node, op);
120 return ResultObject();
121 }
122
123 /*!
124 Applies the operator to all child nodes of the given node.
125
126 May return nullptr or any 3D Geometry object (can be either PolySet or CGAL_Nef_polyhedron)
127 */
applyToChildren3D(const AbstractNode & node,OpenSCADOperator op)128 GeometryEvaluator::ResultObject GeometryEvaluator::applyToChildren3D(const AbstractNode &node, OpenSCADOperator op)
129 {
130 Geometry::Geometries children = collectChildren3D(node);
131 if (children.size() == 0) return ResultObject();
132
133 if (op == OpenSCADOperator::HULL) {
134 PolySet *ps = new PolySet(3, true);
135
136 if (CGALUtils::applyHull(children, *ps)) {
137 return ps;
138 }
139
140 delete ps;
141 return ResultObject();
142 }
143
144 // Only one child -> this is a noop
145 if (children.size() == 1) return ResultObject(children.front().second);
146
147 switch(op) {
148 case OpenSCADOperator::MINKOWSKI:
149 {
150 Geometry::Geometries actualchildren;
151 for(const auto &item : children) {
152 if (item.second && !item.second->isEmpty()) actualchildren.push_back(item);
153 }
154 if (actualchildren.empty()) return ResultObject();
155 if (actualchildren.size() == 1) return ResultObject(actualchildren.front().second);
156 return ResultObject(CGALUtils::applyMinkowski(actualchildren));
157 break;
158 }
159 case OpenSCADOperator::UNION:
160 {
161 Geometry::Geometries actualchildren;
162 for(const auto &item : children) {
163 if (item.second && !item.second->isEmpty()) actualchildren.push_back(item);
164 }
165 if (actualchildren.empty()) return ResultObject();
166 if (actualchildren.size() == 1) return ResultObject(actualchildren.front().second);
167 return ResultObject(CGALUtils::applyUnion3D(actualchildren.begin(), actualchildren.end()));
168 break;
169 }
170 default:
171 {
172 return ResultObject(CGALUtils::applyOperator3D(children, op));
173 break;
174 }
175 }
176 }
177
178
179
180 /*!
181 Apply 2D hull.
182
183 May return an empty geometry but will not return nullptr.
184 */
applyHull2D(const AbstractNode & node)185 Polygon2d *GeometryEvaluator::applyHull2D(const AbstractNode &node)
186 {
187 std::vector<const Polygon2d *> children = collectChildren2D(node);
188 Polygon2d *geometry = new Polygon2d();
189
190 typedef CGAL::Point_2<CGAL::Cartesian<double>> CGALPoint2;
191 // Collect point cloud
192 std::list<CGALPoint2> points;
193 for(const auto &p : children) {
194 if (p) {
195 for(const auto &o : p->outlines()) {
196 for(const auto &v : o.vertices) {
197 points.push_back(CGALPoint2(v[0], v[1]));
198 }
199 }
200 }
201 }
202 if (points.size() > 0) {
203 // Apply hull
204 std::list<CGALPoint2> result;
205 CGAL::Failure_behaviour old_behaviour = CGAL::set_error_behaviour(CGAL::THROW_EXCEPTION);
206 try {
207 CGAL::convex_hull_2(points.begin(), points.end(), std::back_inserter(result));
208 // Construct Polygon2d
209 Outline2d outline;
210 for(const auto &p : result) {
211 outline.vertices.push_back(Vector2d(p[0], p[1]));
212 }
213 geometry->addOutline(outline);
214 }
215 catch (const CGAL::Failure_exception &e) {
216 LOG(message_group::Warning,Location::NONE,"","GeometryEvaluator::applyHull2D() during CGAL::convex_hull_2(): %1$s",e.what());
217 }
218 CGAL::set_error_behaviour(old_behaviour);
219 }
220 return geometry;
221 }
222
applyHull3D(const AbstractNode & node)223 Geometry *GeometryEvaluator::applyHull3D(const AbstractNode &node)
224 {
225 Geometry::Geometries children = collectChildren3D(node);
226
227 PolySet *P = new PolySet(3);
228 if (CGALUtils::applyHull(children, *P)) {
229 return P;
230 }
231 delete P;
232 return nullptr;
233 }
234
applyMinkowski2D(const AbstractNode & node)235 Polygon2d *GeometryEvaluator::applyMinkowski2D(const AbstractNode &node)
236 {
237 std::vector<const Polygon2d *> children = collectChildren2D(node);
238 if (!children.empty()) {
239 return ClipperUtils::applyMinkowski(children);
240 }
241 return nullptr;
242 }
243
244 /*!
245 Returns a list of Polygon2d children of the given node.
246 May return empty Polygon2d object, but not nullptr objects
247 */
collectChildren2D(const AbstractNode & node)248 std::vector<const class Polygon2d *> GeometryEvaluator::collectChildren2D(const AbstractNode &node)
249 {
250 std::vector<const Polygon2d *> children;
251 for(const auto &item : this->visitedchildren[node.index()]) {
252 const AbstractNode *chnode = item.first;
253 const shared_ptr<const Geometry> &chgeom = item.second;
254 if (chnode->modinst->isBackground()) continue;
255
256 // NB! We insert into the cache here to ensure that all children of
257 // a node is a valid object. If we inserted as we created them, the
258 // cache could have been modified before we reach this point due to a large
259 // sibling object.
260 smartCacheInsert(*chnode, chgeom);
261
262 if (chgeom) {
263 if (chgeom->getDimension() == 3) {
264 LOG(message_group::Warning, item.first->modinst->location(), this->tree.getDocumentPath(), "Ignoring 3D child object for 2D operation");
265 children.push_back(nullptr); // replace 3D geometry with empty geometry
266 } else {
267 if (chgeom->isEmpty()) {
268 children.push_back(nullptr);
269 } else {
270 const Polygon2d *polygons = dynamic_cast<const Polygon2d *>(chgeom.get());
271 assert(polygons);
272 children.push_back(polygons);
273 }
274 }
275 } else {
276 children.push_back(nullptr);
277 }
278 }
279 return children;
280 }
281
282 /*!
283 Since we can generate both Nef and non-Nef geometry, we need to insert it into
284 the appropriate cache.
285 This method inserts the geometry into the appropriate cache if it's not already cached.
286 */
smartCacheInsert(const AbstractNode & node,const shared_ptr<const Geometry> & geom)287 void GeometryEvaluator::smartCacheInsert(const AbstractNode &node,
288 const shared_ptr<const Geometry> &geom)
289 {
290 const std::string &key = this->tree.getIdString(node);
291
292 shared_ptr<const CGAL_Nef_polyhedron> N = dynamic_pointer_cast<const CGAL_Nef_polyhedron>(geom);
293 if (N) {
294 if (!CGALCache::instance()->contains(key)) CGALCache::instance()->insert(key, N);
295 }
296 else {
297 if (!GeometryCache::instance()->contains(key)) {
298 if (!GeometryCache::instance()->insert(key, geom)) {
299 LOG(message_group::Warning,Location::NONE,"","GeometryEvaluator: Node didn't fit into cache.");
300 }
301 }
302 }
303 }
304
isSmartCached(const AbstractNode & node)305 bool GeometryEvaluator::isSmartCached(const AbstractNode &node)
306 {
307 const std::string &key = this->tree.getIdString(node);
308 return (GeometryCache::instance()->contains(key) ||
309 CGALCache::instance()->contains(key));
310 }
311
smartCacheGet(const AbstractNode & node,bool preferNef)312 shared_ptr<const Geometry> GeometryEvaluator::smartCacheGet(const AbstractNode &node, bool preferNef)
313 {
314 const std::string &key = this->tree.getIdString(node);
315 shared_ptr<const Geometry> geom;
316 bool hasgeom = GeometryCache::instance()->contains(key);
317 bool hascgal = CGALCache::instance()->contains(key);
318 if (hascgal && (preferNef || !hasgeom)) geom = CGALCache::instance()->get(key);
319 else if (hasgeom) geom = GeometryCache::instance()->get(key);
320 return geom;
321 }
322
323 /*!
324 Returns a list of 3D Geometry children of the given node.
325 May return empty geometries, but not nullptr objects
326 */
collectChildren3D(const AbstractNode & node)327 Geometry::Geometries GeometryEvaluator::collectChildren3D(const AbstractNode &node)
328 {
329 Geometry::Geometries children;
330 for(const auto &item : this->visitedchildren[node.index()]) {
331 const AbstractNode *chnode = item.first;
332 const shared_ptr<const Geometry> &chgeom = item.second;
333 if (chnode->modinst->isBackground()) continue;
334
335 // NB! We insert into the cache here to ensure that all children of
336 // a node is a valid object. If we inserted as we created them, the
337 // cache could have been modified before we reach this point due to a large
338 // sibling object.
339 smartCacheInsert(*chnode, chgeom);
340
341 if (chgeom && chgeom->getDimension() == 2) {
342 LOG(message_group::Warning, item.first->modinst->location(), this->tree.getDocumentPath(), "Ignoring 2D child object for 3D operation");
343 children.push_back(std::make_pair(item.first, nullptr)); // replace 2D geometry with empty geometry
344 } else {
345 // Add children if geometry is 3D OR null/empty
346 children.push_back(item);
347 }
348 }
349 return children;
350 }
351 /*!
352
353 */
applyToChildren2D(const AbstractNode & node,OpenSCADOperator op)354 Polygon2d *GeometryEvaluator::applyToChildren2D(const AbstractNode &node, OpenSCADOperator op)
355 {
356 node.progress_report();
357 if (op == OpenSCADOperator::MINKOWSKI) {
358 return applyMinkowski2D(node);
359 }
360 else if (op == OpenSCADOperator::HULL) {
361 return applyHull2D(node);
362 }
363
364 std::vector<const Polygon2d *> children = collectChildren2D(node);
365
366 if (children.empty()) {
367 return nullptr;
368 }
369
370 if (children.size() == 1) {
371 if (children[0]) {
372 return new Polygon2d(*children[0]); // Copy
373 } else {
374 return nullptr;
375 }
376 }
377
378 ClipperLib::ClipType clipType;
379 switch (op) {
380 case OpenSCADOperator::UNION:
381 clipType = ClipperLib::ctUnion;
382 break;
383 case OpenSCADOperator::INTERSECTION:
384 clipType = ClipperLib::ctIntersection;
385 break;
386 case OpenSCADOperator::DIFFERENCE:
387 clipType = ClipperLib::ctDifference;
388 break;
389 default:
390 LOG(message_group::Error,Location::NONE,"","Unknown boolean operation %1$d",int(op));
391 return nullptr;
392 break;
393 }
394
395 return ClipperUtils::apply(children, clipType);
396 }
397
398 /*!
399 Adds ourself to our parent's list of traversed children.
400 Call this for _every_ node which affects output during traversal.
401 Usually, this should be called from the postfix stage, but for some nodes,
402 we defer traversal letting other components (e.g. CGAL) render the subgraph,
403 and we'll then call this from prefix and prune further traversal.
404
405 The added geometry can be nullptr if it wasn't possible to evaluate it.
406 */
addToParent(const State & state,const AbstractNode & node,const shared_ptr<const Geometry> & geom)407 void GeometryEvaluator::addToParent(const State &state,
408 const AbstractNode &node,
409 const shared_ptr<const Geometry> &geom)
410 {
411 this->visitedchildren.erase(node.index());
412 if (state.parent()) {
413 this->visitedchildren[state.parent()->index()].push_back(std::make_pair(&node, geom));
414 }
415 else {
416 // Root node
417 this->root = geom;
418 assert(this->visitedchildren.empty());
419 }
420 }
421
422 /*!
423 Custom nodes are handled here => implicit union
424 */
visit(State & state,const AbstractNode & node)425 Response GeometryEvaluator::visit(State &state, const AbstractNode &node)
426 {
427 if (state.isPrefix()) {
428 if (isSmartCached(node)) return Response::PruneTraversal;
429 state.setPreferNef(true); // Improve quality of CSG by avoiding conversion loss
430 }
431 if (state.isPostfix()) {
432 shared_ptr<const class Geometry> geom;
433 if (!isSmartCached(node)) {
434 geom = applyToChildren(node, OpenSCADOperator::UNION).constptr();
435 }
436 else {
437 geom = smartCacheGet(node, state.preferNef());
438 }
439 addToParent(state, node, geom);
440 node.progress_report();
441 }
442 return Response::ContinueTraversal;
443 }
444
445 /*!
446 Pass children to parent without touching them. Used by e.g. for loops
447 */
visit(State & state,const ListNode & node)448 Response GeometryEvaluator::visit(State &state, const ListNode &node)
449 {
450 if (state.parent()) {
451 if (state.isPrefix() && node.modinst->isBackground()) {
452 if (node.modinst->isBackground()) state.isBackground();
453 return Response::PruneTraversal;
454 }
455 if (state.isPostfix()) {
456 unsigned int dim = 0;
457 for(const auto &item : this->visitedchildren[node.index()]) {
458 if (!isValidDim(item, dim)) break;
459 const AbstractNode *chnode = item.first;
460 const shared_ptr<const Geometry> &chgeom = item.second;
461 addToParent(state, *chnode, chgeom);
462 }
463 this->visitedchildren.erase(node.index());
464 }
465 return Response::ContinueTraversal;
466 } else {
467 // Handle when a ListNode is given root modifier
468 return lazyEvaluateRootNode(state, node);
469 }
470 }
471
472 /*!
473 */
visit(State & state,const GroupNode & node)474 Response GeometryEvaluator::visit(State &state, const GroupNode &node)
475 {
476 return visit(state, (const AbstractNode &)node);
477 }
478
lazyEvaluateRootNode(State & state,const AbstractNode & node)479 Response GeometryEvaluator::lazyEvaluateRootNode(State &state, const AbstractNode& node) {
480 if (state.isPrefix()) {
481 if (node.modinst->isBackground()) {
482 state.isBackground();
483 return Response::PruneTraversal;
484 }
485 if (isSmartCached(node)) {
486 return Response::PruneTraversal;
487 }
488 }
489 if (state.isPostfix()) {
490 shared_ptr<const class Geometry> geom;
491
492 unsigned int dim = 0;
493 GeometryList::Geometries geometries;
494 for(const auto &item : this->visitedchildren[node.index()]) {
495 if (!isValidDim(item, dim)) break;
496 const AbstractNode *chnode = item.first;
497 const shared_ptr<const Geometry> &chgeom = item.second;
498 if (chnode->modinst->isBackground()) continue;
499 // NB! We insert into the cache here to ensure that all children of
500 // a node is a valid object. If we inserted as we created them, the
501 // cache could have been modified before we reach this point due to a large
502 // sibling object.
503 smartCacheInsert(*chnode, chgeom);
504 // Only use valid geometries
505 if (chgeom && !chgeom->isEmpty()) geometries.push_back(item);
506 }
507 if (geometries.size() == 1) geom = geometries.front().second;
508 else if (geometries.size() > 1) geom.reset(new GeometryList(geometries));
509
510 this->root = geom;
511 }
512 return Response::ContinueTraversal;
513 }
514
515 /*!
516 Root nodes are handled specially; they will flatten any child group
517 nodes to avoid doing an implicit top-level union.
518
519 NB! This is likely a temporary measure until a better implementation of
520 group nodes is in place.
521 */
visit(State & state,const RootNode & node)522 Response GeometryEvaluator::visit(State &state, const RootNode &node)
523 {
524 // If we didn't enable lazy unions, just union the top-level objects
525 if (!Feature::ExperimentalLazyUnion.is_enabled()) {
526 return visit(state, (const GroupNode &)node);
527 }
528 return lazyEvaluateRootNode(state, node);
529 }
530
visit(State & state,const OffsetNode & node)531 Response GeometryEvaluator::visit(State &state, const OffsetNode &node)
532 {
533 if (state.isPrefix() && isSmartCached(node)) return Response::PruneTraversal;
534 if (state.isPostfix()) {
535 shared_ptr<const Geometry> geom;
536 if (!isSmartCached(node)) {
537 const Geometry *geometry = applyToChildren2D(node, OpenSCADOperator::UNION);
538 if (geometry) {
539 const Polygon2d *polygon = dynamic_cast<const Polygon2d*>(geometry);
540 // ClipperLib documentation: The formula for the number of steps in a full
541 // circular arc is ... Pi / acos(1 - arc_tolerance / abs(delta))
542 double n = Calc::get_fragments_from_r(std::abs(node.delta), node.fn, node.fs, node.fa);
543 double arc_tolerance = std::abs(node.delta) * (1 - cos_degrees(180 / n));
544 const Polygon2d *result = ClipperUtils::applyOffset(*polygon, node.delta, node.join_type, node.miter_limit, arc_tolerance);
545 assert(result);
546 geom.reset(result);
547 delete geometry;
548 }
549 }
550 else {
551 geom = smartCacheGet(node, false);
552 }
553 addToParent(state, node, geom);
554 node.progress_report();
555 }
556 return Response::ContinueTraversal;
557 }
558
559 /*!
560 RenderNodes just pass on convexity
561 */
visit(State & state,const RenderNode & node)562 Response GeometryEvaluator::visit(State &state, const RenderNode &node)
563 {
564 if (state.isPrefix()) {
565 if (isSmartCached(node)) return Response::PruneTraversal;
566 state.setPreferNef(true); // Improve quality of CSG by avoiding conversion loss
567 }
568 if (state.isPostfix()) {
569 shared_ptr<const class Geometry> geom;
570 if (!isSmartCached(node)) {
571 ResultObject res = applyToChildren(node, OpenSCADOperator::UNION);
572
573 geom = res.constptr();
574 if (shared_ptr<const PolySet> ps = dynamic_pointer_cast<const PolySet>(geom)) {
575 // If we got a const object, make a copy
576 shared_ptr<PolySet> newps;
577 if (res.isConst()) newps.reset(new PolySet(*ps));
578 else newps = dynamic_pointer_cast<PolySet>(res.ptr());
579 newps->setConvexity(node.convexity);
580 geom = newps;
581 }
582 else if (shared_ptr<const CGAL_Nef_polyhedron> N = dynamic_pointer_cast<const CGAL_Nef_polyhedron>(geom)) {
583 // If we got a const object, make a copy
584 shared_ptr<CGAL_Nef_polyhedron> newN;
585 if (res.isConst()) newN.reset((CGAL_Nef_polyhedron*)N->copy());
586 else newN = dynamic_pointer_cast<CGAL_Nef_polyhedron>(res.ptr());
587 newN->setConvexity(node.convexity);
588 geom = newN;
589 }
590 }
591 else {
592 geom = smartCacheGet(node, state.preferNef());
593 }
594 node.progress_report();
595 addToParent(state, node, geom);
596 }
597 return Response::ContinueTraversal;
598 }
599
600 /*!
601 Leaf nodes can create their own geometry, so let them do that
602
603 input: None
604 output: PolySet or Polygon2d
605 */
visit(State & state,const LeafNode & node)606 Response GeometryEvaluator::visit(State &state, const LeafNode &node)
607 {
608 if (state.isPrefix()) {
609 shared_ptr<const Geometry> geom;
610 if (!isSmartCached(node)) {
611 const Geometry *geometry = node.createGeometry();
612 assert(geometry);
613 if (const Polygon2d *polygon = dynamic_cast<const Polygon2d*>(geometry)) {
614 if (!polygon->isSanitized()) {
615 Polygon2d *p = ClipperUtils::sanitize(*polygon);
616 delete geometry;
617 geometry = p;
618 }
619 }
620 geom.reset(geometry);
621 }
622 else geom = smartCacheGet(node, state.preferNef());
623 addToParent(state, node, geom);
624 node.progress_report();
625 }
626 return Response::PruneTraversal;
627 }
628
visit(State & state,const TextNode & node)629 Response GeometryEvaluator::visit(State &state, const TextNode &node)
630 {
631 if (state.isPrefix()) {
632 shared_ptr<const Geometry> geom;
633 if (!isSmartCached(node)) {
634 std::vector<const Geometry *> geometrylist = node.createGeometryList();
635 std::vector<const Polygon2d *> polygonlist;
636 for(const auto &geometry : geometrylist) {
637 const Polygon2d *polygon = dynamic_cast<const Polygon2d*>(geometry);
638 assert(polygon);
639 polygonlist.push_back(polygon);
640 }
641 geom.reset(ClipperUtils::apply(polygonlist, ClipperLib::ctUnion));
642 }
643 else geom = GeometryCache::instance()->get(this->tree.getIdString(node));
644 addToParent(state, node, geom);
645 node.progress_report();
646 }
647 return Response::PruneTraversal;
648 }
649
650
651 /*!
652 input: List of 2D or 3D objects (not mixed)
653 output: Polygon2d or 3D PolySet
654 operation:
655 o Perform csg op on children
656 */
visit(State & state,const CsgOpNode & node)657 Response GeometryEvaluator::visit(State &state, const CsgOpNode &node)
658 {
659 if (state.isPrefix()) {
660 if (isSmartCached(node)) return Response::PruneTraversal;
661 state.setPreferNef(true); // Improve quality of CSG by avoiding conversion loss
662 }
663 if (state.isPostfix()) {
664 shared_ptr<const Geometry> geom;
665 if (!isSmartCached(node)) {
666 geom = applyToChildren(node, node.type).constptr();
667 }
668 else {
669 geom = smartCacheGet(node, state.preferNef());
670 }
671 addToParent(state, node, geom);
672 node.progress_report();
673 }
674 return Response::ContinueTraversal;
675 }
676
677 /*!
678 input: List of 2D or 3D objects (not mixed)
679 output: Polygon2d or 3D PolySet
680 operation:
681 o Union all children
682 o Perform transform
683 */
visit(State & state,const TransformNode & node)684 Response GeometryEvaluator::visit(State &state, const TransformNode &node)
685 {
686 if (state.isPrefix() && isSmartCached(node)) return Response::PruneTraversal;
687 if (state.isPostfix()) {
688 shared_ptr<const class Geometry> geom;
689 if (!isSmartCached(node)) {
690 if (matrix_contains_infinity(node.matrix) || matrix_contains_nan(node.matrix)) {
691 // due to the way parse/eval works we can't currently distinguish between NaN and Inf
692 LOG(message_group::Warning,node.modinst->location(),this->tree.getDocumentPath(),"Transformation matrix contains Not-a-Number and/or Infinity - removing object.");
693 }
694 else {
695 // First union all children
696 ResultObject res = applyToChildren(node, OpenSCADOperator::UNION);
697 if ((geom = res.constptr())) {
698 if (geom->getDimension() == 2) {
699 shared_ptr<const Polygon2d> polygons = dynamic_pointer_cast<const Polygon2d>(geom);
700 assert(polygons);
701
702 // If we got a const object, make a copy
703 shared_ptr<Polygon2d> newpoly;
704 if (res.isConst()) newpoly.reset(new Polygon2d(*polygons));
705 else newpoly = dynamic_pointer_cast<Polygon2d>(res.ptr());
706
707 Transform2d mat2;
708 mat2.matrix() <<
709 node.matrix(0,0), node.matrix(0,1), node.matrix(0,3),
710 node.matrix(1,0), node.matrix(1,1), node.matrix(1,3),
711 node.matrix(3,0), node.matrix(3,1), node.matrix(3,3);
712 newpoly->transform(mat2);
713 // A 2D transformation may flip the winding order of a polygon.
714 // If that happens with a sanitized polygon, we need to reverse
715 // the winding order for it to be correct.
716 if (newpoly->isSanitized() && mat2.matrix().determinant() <= 0) {
717 geom.reset(ClipperUtils::sanitize(*newpoly));
718 }
719 }
720 else if (geom->getDimension() == 3) {
721 shared_ptr<const PolySet> ps = dynamic_pointer_cast<const PolySet>(geom);
722 if (ps) {
723 // If we got a const object, make a copy
724 shared_ptr<PolySet> newps;
725 if (res.isConst()) newps.reset(new PolySet(*ps));
726 else newps = dynamic_pointer_cast<PolySet>(res.ptr());
727 newps->transform(node.matrix);
728 geom = newps;
729 }
730 else {
731 shared_ptr<const CGAL_Nef_polyhedron> N = dynamic_pointer_cast<const CGAL_Nef_polyhedron>(geom);
732 assert(N);
733 // If we got a const object, make a copy
734 shared_ptr<CGAL_Nef_polyhedron> newN;
735 if (res.isConst()) newN.reset((CGAL_Nef_polyhedron*)N->copy());
736 else newN = dynamic_pointer_cast<CGAL_Nef_polyhedron>(res.ptr());
737 newN->transform(node.matrix);
738 geom = newN;
739 }
740 }
741 }
742 }
743 }
744 else {
745 geom = smartCacheGet(node, state.preferNef());
746 }
747 addToParent(state, node, geom);
748 node.progress_report();
749 }
750 return Response::ContinueTraversal;
751 }
752
translate_PolySet(PolySet & ps,const Vector3d & translation)753 static void translate_PolySet(PolySet &ps, const Vector3d &translation)
754 {
755 for(auto &p : ps.polygons) {
756 for(auto &v : p) {
757 v += translation;
758 }
759 }
760 }
761
762 /*
763 Compare Euclidean length of vectors
764 Return:
765 -1 : if v1 < v2
766 0 : if v1 ~= v2 (approximation to compoensate for floating point precision)
767 1 : if v1 > v2
768 */
sgn_vdiff(const Vector2d & v1,const Vector2d & v2)769 int sgn_vdiff(const Vector2d &v1, const Vector2d &v2) {
770 constexpr double ratio_threshold = 1e5; // 10ppm difference
771 double l1 = v1.norm();
772 double l2 = v2.norm();
773 // Compare the average and difference, to be independent of geometry scale.
774 // If the difference is within ratio_threshold of the avg, treat as equal.
775 double scale = (l1+l2);
776 double diff = 2*std::fabs(l1-l2)*ratio_threshold;
777 return diff > scale ? (l1 < l2 ? -1 : 1) : 0;
778 }
779
780 /*
781 Attempt to triangulate quads in an ideal way.
782 Each quad is composed of two adjacent outline vertices: (prev1, curr1)
783 and their corresponding transformed points one step up: (prev2, curr2).
784 Quads are triangulated across the shorter of the two diagonals, which works well in most cases.
785 However, when diagonals are equal length, decision may flip depending on other factors.
786 */
add_slice(PolySet * ps,const Polygon2d & poly,double rot1,double rot2,double h1,double h2,const Vector2d & scale1,const Vector2d & scale2)787 static void add_slice(PolySet *ps, const Polygon2d &poly,
788 double rot1, double rot2,
789 double h1, double h2,
790 const Vector2d &scale1,
791 const Vector2d &scale2)
792 {
793 Eigen::Affine2d trans1(Eigen::Scaling(scale1) * Eigen::Affine2d(rotate_degrees(-rot1)));
794 Eigen::Affine2d trans2(Eigen::Scaling(scale2) * Eigen::Affine2d(rotate_degrees(-rot2)));
795 Eigen::Affine2d trans_mid(Eigen::Scaling((scale1+scale2)/2) * Eigen::Affine2d(rotate_degrees(-(rot1+rot2)/2)));
796
797 bool is_straight = rot1==rot2 && scale1[0]==scale1[1] && scale2[0]==scale2[1];
798 bool any_zero = scale2[0] == 0 || scale2[1] == 0;
799 bool any_non_zero = scale2[0] != 0 || scale2[1] != 0;
800 // Not likely to matter, but when no twist (rot2 == rot1),
801 // setting back_twist true helps keep diagonals same as previous builds.
802 bool back_twist = rot2 <= rot1;
803
804 for(const auto &o : poly.outlines()) {
805 Vector2d prev1 = trans1 * o.vertices[0];
806 Vector2d prev2 = trans2 * o.vertices[0];
807
808 // For equal length diagonals, flip selected choice depending on direction of twist and
809 // whether the outline is negative (eg circle hole inside a larger circle).
810 // This was tested against circles with a single point touching the origin,
811 // and extruded with twist. Diagonal choice determined by whichever option
812 // matched the direction of diagonal for neighboring edges (which did not exhibit "equal" diagonals).
813 bool flip = ((!o.positive) xor (back_twist));
814
815 for (size_t i=1;i<=o.vertices.size();++i) {
816 Vector2d curr1 = trans1 * o.vertices[i % o.vertices.size()];
817 Vector2d curr2 = trans2 * o.vertices[i % o.vertices.size()];
818
819 int diff_sign = sgn_vdiff(prev1 - curr2, curr1 - prev2);
820 bool splitfirst = diff_sign == -1 || (diff_sign == 0 && !flip);
821
822 // Enable/Disable testing of 4-way split quads, with added midpoint.
823 // These look very nice when(and only when) diagonals are near equal.
824 // This typically happens when an edge is colinear with the origin.
825 #if 0
826 // Diagonals should be equal whenever an edge is co-linear with the origin (edge itself need not touch it)
827 if (!is_straight && diff_sign == 0) {
828 // Split into 4 triangles, with an added midpoint.
829 //Vector2d mid_prev = trans3 * (prev1 +curr1+curr2)/4;
830 Vector2d mid = trans_mid * (o.vertices[(i-1) % o.vertices.size()] + o.vertices[i % o.vertices.size()])/2;
831 double h_mid = (h1+h2)/2;
832 ps->append_poly();
833 ps->insert_vertex(prev1[0], prev1[1], h1);
834 ps->insert_vertex( mid[0], mid[1], h_mid);
835 ps->insert_vertex(curr1[0], curr1[1], h1);
836 ps->append_poly();
837 ps->insert_vertex(curr1[0], curr1[1], h1);
838 ps->insert_vertex( mid[0], mid[1], h_mid);
839 ps->insert_vertex(curr2[0], curr2[1], h2);
840 ps->append_poly();
841 ps->insert_vertex(curr2[0], curr2[1], h2);
842 ps->insert_vertex( mid[0], mid[1], h_mid);
843 ps->insert_vertex(prev2[0], prev2[1], h2);
844 ps->append_poly();
845 ps->insert_vertex(prev2[0], prev2[1], h2);
846 ps->insert_vertex( mid[0], mid[1], h_mid);
847 ps->insert_vertex(prev1[0], prev1[1], h1);
848 } else
849 #endif
850 // Split along shortest diagonal,
851 // unless at top for a 0-scaled axis (which can create 0 thickness "ears")
852 if (splitfirst xor any_zero) {
853 ps->append_poly();
854 ps->insert_vertex(prev1[0], prev1[1], h1);
855 ps->insert_vertex(curr2[0], curr2[1], h2);
856 ps->insert_vertex(curr1[0], curr1[1], h1);
857 if (!any_zero || (any_non_zero && prev2 != curr2)) {
858 ps->append_poly();
859 ps->insert_vertex(curr2[0], curr2[1], h2);
860 ps->insert_vertex(prev1[0], prev1[1], h1);
861 ps->insert_vertex(prev2[0], prev2[1], h2);
862 }
863 } else {
864 ps->append_poly();
865 ps->insert_vertex(prev1[0], prev1[1], h1);
866 ps->insert_vertex(prev2[0], prev2[1], h2);
867 ps->insert_vertex(curr1[0], curr1[1], h1);
868 if (!any_zero || (any_non_zero && prev2 != curr2)) {
869 ps->append_poly();
870 ps->insert_vertex(prev2[0], prev2[1], h2);
871 ps->insert_vertex(curr2[0], curr2[1], h2);
872 ps->insert_vertex(curr1[0], curr1[1], h1);
873 }
874 }
875 prev1 = curr1;
876 prev2 = curr2;
877 }
878 }
879 }
880
881 /*!
882 Input to extrude should be sanitized. This means non-intersecting, correct winding order
883 etc., the input coming from a library like Clipper.
884 */
extrudePolygon(const LinearExtrudeNode & node,const Polygon2d & poly)885 static Geometry *extrudePolygon(const LinearExtrudeNode &node, const Polygon2d &poly)
886 {
887 boost::tribool isConvex{poly.is_convex()};
888 // Twise or non-uniform scale makes convex polygons into unknown polyhedrons
889 if (isConvex && (node.twist != 0 || node.scale_x != node.scale_y)) isConvex = unknown;
890 PolySet *ps = new PolySet(3, isConvex);
891 ps->setConvexity(node.convexity);
892 if (node.height <= 0) return ps;
893
894 double h1, h2;
895
896 if (node.center) {
897 h1 = -node.height/2.0;
898 h2 = +node.height/2.0;
899 } else {
900 h1 = 0;
901 h2 = node.height;
902 }
903
904 PolySet *ps_bottom = poly.tessellate(); // bottom
905
906 // Flip vertex ordering for bottom polygon
907 for(auto &p : ps_bottom->polygons) {
908 std::reverse(p.begin(), p.end());
909 }
910 translate_PolySet(*ps_bottom, Vector3d(0,0,h1));
911
912 ps->append(*ps_bottom);
913 delete ps_bottom;
914 // If either scale components are 0, then top will be zero-area, so skip it.
915 if (node.scale_x > 0 && node.scale_y > 0) {
916 Polygon2d top_poly(poly);
917 Eigen::Affine2d trans(Eigen::Scaling(node.scale_x, node.scale_y) * Eigen::Affine2d(rotate_degrees(-node.twist)));
918 top_poly.transform(trans); // top
919 PolySet *ps_top = top_poly.tessellate();
920 translate_PolySet(*ps_top, Vector3d(0,0,h2));
921 ps->append(*ps_top);
922 delete ps_top;
923 }
924
925 size_t slices;
926 if (node.has_slices) {
927 slices = node.slices;
928 } else if (node.has_twist) {
929 double max_r1_sqr = 0; // r1 is before scaling
930 Vector2d scale(node.scale_x, node.scale_y);
931 for(const auto &o : poly.outlines())
932 for(const auto &v : o.vertices)
933 max_r1_sqr = fmax(max_r1_sqr, v.squaredNorm());
934 // Calculate Helical curve length for Twist with no Scaling
935 // **** Don't know how to handle twist with non-uniform scaling, ****
936 // **** so just use this straight helix calculation anyways. ****
937 if ((node.scale_x == 1.0 && node.scale_y == 1.0) || node.scale_x != node.scale_y) {
938 slices = (unsigned int)Calc::get_helix_slices(max_r1_sqr, node.height, node.twist, node.fn, node.fs, node.fa);
939 } else { // uniform scaling with twist, use conical helix calculation
940 slices = (unsigned int)Calc::get_conical_helix_slices(max_r1_sqr, node.height, node.twist, node.scale_x, node.fn, node.fs, node.fa);
941 }
942 } else if (node.scale_x != node.scale_y) {
943 // Non uniform scaling, w/o twist
944 double max_delta_sqr = 0; // delta from before/after scaling
945 Vector2d scale(node.scale_x, node.scale_y);
946 for(const auto &o : poly.outlines())
947 for(const auto &v : o.vertices)
948 max_delta_sqr = fmax(max_delta_sqr, (v-v.cwiseProduct(scale)).squaredNorm());
949 slices = Calc::get_diagonal_slices(max_delta_sqr, node.height, node.fn, node.fs);
950 } else {
951 // uniform or [1,1] scaling w/o twist needs only one slice
952 slices = 1;
953 }
954
955 for (unsigned int j = 0; j < slices; j++) {
956 double rot1 = node.twist*j / slices;
957 double rot2 = node.twist*(j+1) / slices;
958 double height1 = h1 + (h2-h1)*j / slices;
959 double height2 = h1 + (h2-h1)*(j+1) / slices;
960 Vector2d scale1(1 - (1-node.scale_x)*j / slices,
961 1 - (1-node.scale_y)*j / slices);
962 Vector2d scale2(1 - (1-node.scale_x)*(j+1) / slices,
963 1 - (1-node.scale_y)*(j+1) / slices);
964 add_slice(ps, poly, rot1, rot2, height1, height2, scale1, scale2);
965 }
966
967 return ps;
968 }
969
970 /*!
971 input: List of 2D objects
972 output: 3D PolySet
973 operation:
974 o Union all children
975 o Perform extrude
976 */
visit(State & state,const LinearExtrudeNode & node)977 Response GeometryEvaluator::visit(State &state, const LinearExtrudeNode &node)
978 {
979 if (state.isPrefix() && isSmartCached(node)) return Response::PruneTraversal;
980 if (state.isPostfix()) {
981 shared_ptr<const Geometry> geom;
982 if (!isSmartCached(node)) {
983 const Geometry *geometry = nullptr;
984 if (!node.filename.empty()) {
985 DxfData dxf(node.fn, node.fs, node.fa, node.filename, node.layername, node.origin_x, node.origin_y, node.scale_x);
986
987 Polygon2d *p2d = dxf.toPolygon2d();
988 if (p2d) geometry = ClipperUtils::sanitize(*p2d);
989 delete p2d;
990 }
991 else {
992 geometry = applyToChildren2D(node, OpenSCADOperator::UNION);
993 }
994 if (geometry) {
995 const Polygon2d *polygons = dynamic_cast<const Polygon2d*>(geometry);
996 Geometry *extruded = extrudePolygon(node, *polygons);
997 assert(extruded);
998 geom.reset(extruded);
999 delete geometry;
1000 }
1001 }
1002 else {
1003 geom = smartCacheGet(node, false);
1004 }
1005 addToParent(state, node, geom);
1006 node.progress_report();
1007 }
1008 return Response::ContinueTraversal;
1009 }
1010
fill_ring(std::vector<Vector3d> & ring,const Outline2d & o,double a,bool flip)1011 static void fill_ring(std::vector<Vector3d> &ring, const Outline2d &o, double a, bool flip)
1012 {
1013 if (flip) {
1014 unsigned int l = o.vertices.size()-1;
1015 for (unsigned int i=0 ; i<o.vertices.size(); ++i) {
1016 ring[i][0] = o.vertices[l-i][0] * sin_degrees(a);
1017 ring[i][1] = o.vertices[l-i][0] * cos_degrees(a);
1018 ring[i][2] = o.vertices[l-i][1];
1019 }
1020 } else {
1021 for (unsigned int i=0 ; i<o.vertices.size(); ++i) {
1022 ring[i][0] = o.vertices[i][0] * sin_degrees(a);
1023 ring[i][1] = o.vertices[i][0] * cos_degrees(a);
1024 ring[i][2] = o.vertices[i][1];
1025 }
1026 }
1027 }
1028
1029 /*!
1030 Input to extrude should be clean. This means non-intersecting, correct winding order
1031 etc., the input coming from a library like Clipper.
1032
1033 FIXME: We should handle some common corner cases better:
1034 o 2D polygon having an edge being on the Y axis:
1035 In this case, we don't need to generate geometry involving this edge as it
1036 will be an internal edge.
1037 o 2D polygon having a vertex touching the Y axis:
1038 This is more complex as the resulting geometry will (may?) be nonmanifold.
1039 In any case, the previous case is a specialization of this, so the following
1040 should be handled for both cases:
1041 Since the ring associated with this vertex will have a radius of zero, it will
1042 collapse to one vertex. Any quad using this ring will be collapsed to a triangle.
1043
1044 Currently, we generate a lot of zero-area triangles
1045
1046 */
rotatePolygon(const RotateExtrudeNode & node,const Polygon2d & poly)1047 static Geometry *rotatePolygon(const RotateExtrudeNode &node, const Polygon2d &poly)
1048 {
1049 if (node.angle == 0) return nullptr;
1050
1051 PolySet *ps = new PolySet(3);
1052 ps->setConvexity(node.convexity);
1053
1054 double min_x = 0;
1055 double max_x = 0;
1056 unsigned int fragments = 0;
1057 for(const auto &o : poly.outlines()) {
1058 for(const auto &v : o.vertices) {
1059 min_x = fmin(min_x, v[0]);
1060 max_x = fmax(max_x, v[0]);
1061
1062 if ((max_x - min_x) > max_x && (max_x - min_x) > fabs(min_x)) {
1063 LOG(message_group::Error,Location::NONE,"","all points for rotate_extrude() must have the same X coordinate sign (range is %1$.2f -> %2$.2f)",min_x,max_x);
1064 delete ps;
1065 return nullptr;
1066 }
1067 }
1068 }
1069 fragments = (unsigned int)fmax(Calc::get_fragments_from_r(max_x - min_x, node.fn, node.fs, node.fa) * std::abs(node.angle) / 360, 1);
1070
1071 bool flip_faces = (min_x >= 0 && node.angle > 0 && node.angle != 360) || (min_x < 0 && (node.angle < 0 || node.angle == 360));
1072
1073 if (node.angle != 360) {
1074 PolySet *ps_start = poly.tessellate(); // starting face
1075 Transform3d rot(angle_axis_degrees(90, Vector3d::UnitX()));
1076 ps_start->transform(rot);
1077 // Flip vertex ordering
1078 if (!flip_faces) {
1079 for(auto &p : ps_start->polygons) {
1080 std::reverse(p.begin(), p.end());
1081 }
1082 }
1083 ps->append(*ps_start);
1084 delete ps_start;
1085
1086 PolySet *ps_end = poly.tessellate();
1087 Transform3d rot2(angle_axis_degrees(node.angle, Vector3d::UnitZ()) * angle_axis_degrees(90, Vector3d::UnitX()));
1088 ps_end->transform(rot2);
1089 if (flip_faces) {
1090 for(auto &p : ps_end->polygons) {
1091 std::reverse(p.begin(), p.end());
1092 }
1093 }
1094 ps->append(*ps_end);
1095 delete ps_end;
1096 }
1097
1098 for(const auto &o : poly.outlines()) {
1099 std::vector<Vector3d> rings[2];
1100 rings[0].resize(o.vertices.size());
1101 rings[1].resize(o.vertices.size());
1102
1103 fill_ring(rings[0], o, (node.angle == 360) ? -90 : 90, flip_faces); // first ring
1104 for (unsigned int j = 0; j < fragments; ++j) {
1105 double a;
1106 if (node.angle == 360)
1107 a = -90 + ((j+1)%fragments) * 360.0 / fragments; // start on the -X axis, for legacy support
1108 else
1109 a = 90 - (j+1)* node.angle / fragments; // start on the X axis
1110 fill_ring(rings[(j+1)%2], o, a, flip_faces);
1111
1112 for (size_t i=0; i<o.vertices.size(); ++i) {
1113 ps->append_poly();
1114 ps->insert_vertex(rings[j%2][i]);
1115 ps->insert_vertex(rings[(j+1)%2][(i+1)%o.vertices.size()]);
1116 ps->insert_vertex(rings[j%2][(i+1)%o.vertices.size()]);
1117 ps->append_poly();
1118 ps->insert_vertex(rings[j%2][i]);
1119 ps->insert_vertex(rings[(j+1)%2][i]);
1120 ps->insert_vertex(rings[(j+1)%2][(i+1)%o.vertices.size()]);
1121 }
1122 }
1123 }
1124
1125 return ps;
1126 }
1127
1128 /*!
1129 input: List of 2D objects
1130 output: 3D PolySet
1131 operation:
1132 o Union all children
1133 o Perform extrude
1134 */
visit(State & state,const RotateExtrudeNode & node)1135 Response GeometryEvaluator::visit(State &state, const RotateExtrudeNode &node)
1136 {
1137 if (state.isPrefix() && isSmartCached(node)) return Response::PruneTraversal;
1138 if (state.isPostfix()) {
1139 shared_ptr<const Geometry> geom;
1140 if (!isSmartCached(node)) {
1141 const Geometry *geometry = nullptr;
1142 if (!node.filename.empty()) {
1143 DxfData dxf(node.fn, node.fs, node.fa, node.filename, node.layername, node.origin_x, node.origin_y, node.scale);
1144 Polygon2d *p2d = dxf.toPolygon2d();
1145 if (p2d) geometry = ClipperUtils::sanitize(*p2d);
1146 delete p2d;
1147 }
1148 else {
1149 geometry = applyToChildren2D(node, OpenSCADOperator::UNION);
1150 }
1151 if (geometry) {
1152 const Polygon2d *polygons = dynamic_cast<const Polygon2d*>(geometry);
1153 Geometry *rotated = rotatePolygon(node, *polygons);
1154 geom.reset(rotated);
1155 delete geometry;
1156 }
1157 }
1158 else {
1159 geom = smartCacheGet(node, false);
1160 }
1161 addToParent(state, node, geom);
1162 node.progress_report();
1163 }
1164 return Response::ContinueTraversal;
1165 }
1166
1167 /*!
1168 FIXME: Not in use
1169 */
visit(State &,const AbstractPolyNode &)1170 Response GeometryEvaluator::visit(State & /*state*/, const AbstractPolyNode & /*node*/)
1171 {
1172 assert(false);
1173 return Response::AbortTraversal;
1174 }
1175
1176 /*!
1177 input: List of 3D objects
1178 output: Polygon2d
1179 operation:
1180 o Union all children
1181 o Perform projection
1182 */
visit(State & state,const ProjectionNode & node)1183 Response GeometryEvaluator::visit(State &state, const ProjectionNode &node)
1184 {
1185 if (state.isPrefix() && isSmartCached(node)) return Response::PruneTraversal;
1186 if (state.isPostfix()) {
1187 shared_ptr<const class Geometry> geom;
1188 if (!isSmartCached(node)) {
1189
1190 if (!node.cut_mode) {
1191 ClipperLib::Clipper sumclipper;
1192 for(const auto &item : this->visitedchildren[node.index()]) {
1193 const AbstractNode *chnode = item.first;
1194 const shared_ptr<const Geometry> &chgeom = item.second;
1195 if (chnode->modinst->isBackground()) continue;
1196
1197 const Polygon2d *poly = nullptr;
1198
1199 // CGAL version of Geometry projection
1200 // Causes crashes in createNefPolyhedronFromGeometry() for this model:
1201 // projection(cut=false) {
1202 // cube(10);
1203 // difference() {
1204 // sphere(10);
1205 // cylinder(h=30, r=5, center=true);
1206 // }
1207 // }
1208 #if 0
1209 shared_ptr<const PolySet> chPS = dynamic_pointer_cast<const PolySet>(chgeom);
1210 const PolySet *ps2d = nullptr;
1211 shared_ptr<const CGAL_Nef_polyhedron> chN = dynamic_pointer_cast<const CGAL_Nef_polyhedron>(chgeom);
1212 if (chN) chPS.reset(chN->convertToPolyset());
1213 if (chPS) ps2d = PolysetUtils::flatten(*chPS);
1214 if (ps2d) {
1215 CGAL_Nef_polyhedron *N2d = CGALUtils::createNefPolyhedronFromGeometry(*ps2d);
1216 poly = N2d->convertToPolygon2d();
1217 }
1218 #endif
1219
1220 // Clipper version of Geometry projection
1221 // Clipper doesn't handle meshes very well.
1222 // It's better in V6 but not quite there. FIXME: stand-alone example.
1223 #if 1
1224 // project chgeom -> polygon2d
1225 shared_ptr<const PolySet> chPS = dynamic_pointer_cast<const PolySet>(chgeom);
1226 if (!chPS) {
1227 shared_ptr<const CGAL_Nef_polyhedron> chN = dynamic_pointer_cast<const CGAL_Nef_polyhedron>(chgeom);
1228 if (chN && !chN->isEmpty()) {
1229 PolySet *ps = new PolySet(3);
1230 bool err = CGALUtils::createPolySetFromNefPolyhedron3(*chN->p3, *ps);
1231 if (err) {
1232 LOG(message_group::Error,Location::NONE,"","Nef->PolySet failed");
1233 }
1234 else {
1235 chPS.reset(ps);
1236 }
1237 }
1238 }
1239 if (chPS) poly = PolysetUtils::project(*chPS);
1240 #endif
1241
1242 if (poly) {
1243 ClipperLib::Paths result = ClipperUtils::fromPolygon2d(*poly);
1244 // Using NonZero ensures that we don't create holes from polygons sharing
1245 // edges since we're unioning a mesh
1246 result = ClipperUtils::process(result,
1247 ClipperLib::ctUnion,
1248 ClipperLib::pftNonZero);
1249 // Add correctly winded polygons to the main clipper
1250 sumclipper.AddPaths(result, ClipperLib::ptSubject, true);
1251 }
1252
1253 delete poly;
1254 }
1255 ClipperLib::PolyTree sumresult;
1256 // This is key - without StrictlySimple, we tend to get self-intersecting results
1257 sumclipper.StrictlySimple(true);
1258 sumclipper.Execute(ClipperLib::ctUnion, sumresult, ClipperLib::pftNonZero, ClipperLib::pftNonZero);
1259 if (sumresult.Total() > 0) geom.reset(ClipperUtils::toPolygon2d(sumresult));
1260 }
1261 else {
1262 shared_ptr<const Geometry> newgeom = applyToChildren3D(node, OpenSCADOperator::UNION).constptr();
1263 if (newgeom) {
1264 shared_ptr<const CGAL_Nef_polyhedron> Nptr = dynamic_pointer_cast<const CGAL_Nef_polyhedron>(newgeom);
1265 if (!Nptr) {
1266 Nptr.reset(CGALUtils::createNefPolyhedronFromGeometry(*newgeom));
1267 }
1268 if (!Nptr->isEmpty()) {
1269 Polygon2d *poly = CGALUtils::project(*Nptr, node.cut_mode);
1270 if (poly) {
1271 poly->setConvexity(node.convexity);
1272 geom.reset(poly);
1273 }
1274 }
1275 }
1276 }
1277 }
1278 else {
1279 geom = smartCacheGet(node, false);
1280 }
1281 addToParent(state, node, geom);
1282 node.progress_report();
1283 }
1284 return Response::ContinueTraversal;
1285 }
1286
1287 /*!
1288 input: List of 2D or 3D objects (not mixed)
1289 output: any Geometry
1290 operation:
1291 o Perform cgal operation
1292 */
visit(State & state,const CgaladvNode & node)1293 Response GeometryEvaluator::visit(State &state, const CgaladvNode &node)
1294 {
1295 if (state.isPrefix() && isSmartCached(node)) return Response::PruneTraversal;
1296 if (state.isPostfix()) {
1297 shared_ptr<const Geometry> geom;
1298 if (!isSmartCached(node)) {
1299 switch (node.type) {
1300 case CgaladvType::MINKOWSKI: {
1301 ResultObject res = applyToChildren(node, OpenSCADOperator::MINKOWSKI);
1302 geom = res.constptr();
1303 // If we added convexity, we need to pass it on
1304 if (geom && geom->getConvexity() != node.convexity) {
1305 shared_ptr<Geometry> editablegeom;
1306 // If we got a const object, make a copy
1307 if (res.isConst()) editablegeom.reset(geom->copy());
1308 else editablegeom = res.ptr();
1309 geom = editablegeom;
1310 editablegeom->setConvexity(node.convexity);
1311 }
1312 break;
1313 }
1314 case CgaladvType::HULL: {
1315 geom = applyToChildren(node, OpenSCADOperator::HULL).constptr();
1316 break;
1317 }
1318 case CgaladvType::RESIZE: {
1319 ResultObject res = applyToChildren(node, OpenSCADOperator::UNION);
1320 geom = res.constptr();
1321 if (geom) {
1322 shared_ptr<Geometry> editablegeom;
1323 // If we got a const object, make a copy
1324 if (res.isConst()) editablegeom.reset(geom->copy());
1325 else editablegeom = res.ptr();
1326 if (editablegeom->getConvexity() != node.convexity) {
1327 editablegeom->setConvexity(node.convexity);
1328 }
1329 geom = editablegeom;
1330
1331 shared_ptr<CGAL_Nef_polyhedron> N = dynamic_pointer_cast<CGAL_Nef_polyhedron>(editablegeom);
1332 if (N) {
1333 N->resize(node.newsize, node.autosize);
1334 }
1335 else {
1336 shared_ptr<Polygon2d> poly = dynamic_pointer_cast<Polygon2d>(editablegeom);
1337 if (poly) {
1338 poly->resize(Vector2d(node.newsize[0], node.newsize[1]),
1339 Eigen::Matrix<bool,2,1>(node.autosize[0], node.autosize[1]));
1340 }
1341 else {
1342 shared_ptr<PolySet> ps = dynamic_pointer_cast<PolySet>(editablegeom);
1343 if (ps) {
1344 ps->resize(node.newsize, node.autosize);
1345 }
1346 else {
1347 assert(false);
1348 }
1349 }
1350 }
1351 }
1352 break;
1353 }
1354 default:
1355 assert(false && "not implemented");
1356 }
1357 }
1358 else {
1359 geom = smartCacheGet(node, state.preferNef());
1360 }
1361 addToParent(state, node, geom);
1362 node.progress_report();
1363 }
1364 return Response::ContinueTraversal;
1365 }
1366
visit(State & state,const AbstractIntersectionNode & node)1367 Response GeometryEvaluator::visit(State &state, const AbstractIntersectionNode &node)
1368 {
1369 if (state.isPrefix()) {
1370 if (isSmartCached(node)) return Response::PruneTraversal;
1371 state.setPreferNef(true); // Improve quality of CSG by avoiding conversion loss
1372 }
1373 if (state.isPostfix()) {
1374 shared_ptr<const class Geometry> geom;
1375 if (!isSmartCached(node)) {
1376 geom = applyToChildren(node, OpenSCADOperator::INTERSECTION).constptr();
1377 }
1378 else {
1379 geom = smartCacheGet(node, state.preferNef());
1380 }
1381 addToParent(state, node, geom);
1382 node.progress_report();
1383 }
1384 return Response::ContinueTraversal;
1385 }
1386