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