1 #include <libslic3r/SLA/SupportTreeBuildsteps.hpp>
2
3 #include <libslic3r/SLA/SpatIndex.hpp>
4 #include <libslic3r/Optimize/NLoptOptimizer.hpp>
5 #include <boost/log/trivial.hpp>
6
7 namespace Slic3r {
8 namespace sla {
9
10 using Slic3r::opt::initvals;
11 using Slic3r::opt::bounds;
12 using Slic3r::opt::StopCriteria;
13 using Slic3r::opt::Optimizer;
14 using Slic3r::opt::AlgNLoptSubplex;
15 using Slic3r::opt::AlgNLoptGenetic;
16
get_criteria(const SupportTreeConfig & cfg)17 StopCriteria get_criteria(const SupportTreeConfig &cfg)
18 {
19 return StopCriteria{}
20 .rel_score_diff(cfg.optimizer_rel_score_diff)
21 .max_iterations(cfg.optimizer_max_iterations);
22 }
23
24 template<class C, class Hit = IndexedMesh::hit_result>
min_hit(const C & hits)25 static Hit min_hit(const C &hits)
26 {
27 auto mit = std::min_element(hits.begin(), hits.end(),
28 [](const Hit &h1, const Hit &h2) {
29 return h1.distance() < h2.distance();
30 });
31
32 return *mit;
33 }
34
SupportTreeBuildsteps(SupportTreeBuilder & builder,const SupportableMesh & sm)35 SupportTreeBuildsteps::SupportTreeBuildsteps(SupportTreeBuilder & builder,
36 const SupportableMesh &sm)
37 : m_cfg(sm.cfg)
38 , m_mesh(sm.emesh)
39 , m_support_pts(sm.pts)
40 , m_support_nmls(sm.pts.size(), 3)
41 , m_builder(builder)
42 , m_points(sm.pts.size(), 3)
43 , m_thr(builder.ctl().cancelfn)
44 {
45 // Prepare the support points in Eigen/IGL format as well, we will use
46 // it mostly in this form.
47
48 long i = 0;
49 for (const SupportPoint &sp : m_support_pts) {
50 m_points.row(i)(X) = double(sp.pos(X));
51 m_points.row(i)(Y) = double(sp.pos(Y));
52 m_points.row(i)(Z) = double(sp.pos(Z));
53 ++i;
54 }
55 }
56
execute(SupportTreeBuilder & builder,const SupportableMesh & sm)57 bool SupportTreeBuildsteps::execute(SupportTreeBuilder & builder,
58 const SupportableMesh &sm)
59 {
60 if(sm.pts.empty()) return false;
61
62 builder.ground_level = sm.emesh.ground_level() - sm.cfg.object_elevation_mm;
63
64 SupportTreeBuildsteps alg(builder, sm);
65
66 // Let's define the individual steps of the processing. We can experiment
67 // later with the ordering and the dependencies between them.
68 enum Steps {
69 BEGIN,
70 FILTER,
71 PINHEADS,
72 CLASSIFY,
73 ROUTING_GROUND,
74 ROUTING_NONGROUND,
75 CASCADE_PILLARS,
76 MERGE_RESULT,
77 DONE,
78 ABORT,
79 NUM_STEPS
80 //...
81 };
82
83 // Collect the algorithm steps into a nice sequence
84 std::array<std::function<void()>, NUM_STEPS> program = {
85 [] () {
86 // Begin...
87 // Potentially clear up the shared data (not needed for now)
88 },
89
90 std::bind(&SupportTreeBuildsteps::filter, &alg),
91
92 std::bind(&SupportTreeBuildsteps::add_pinheads, &alg),
93
94 std::bind(&SupportTreeBuildsteps::classify, &alg),
95
96 std::bind(&SupportTreeBuildsteps::routing_to_ground, &alg),
97
98 std::bind(&SupportTreeBuildsteps::routing_to_model, &alg),
99
100 std::bind(&SupportTreeBuildsteps::interconnect_pillars, &alg),
101
102 std::bind(&SupportTreeBuildsteps::merge_result, &alg),
103
104 [] () {
105 // Done
106 },
107
108 [] () {
109 // Abort
110 }
111 };
112
113 Steps pc = BEGIN;
114
115 if(sm.cfg.ground_facing_only) {
116 program[ROUTING_NONGROUND] = []() {
117 BOOST_LOG_TRIVIAL(info)
118 << "Skipping model-facing supports as requested.";
119 };
120 }
121
122 // Let's define a simple automaton that will run our program.
123 auto progress = [&builder, &pc] () {
124 static const std::array<std::string, NUM_STEPS> stepstr {
125 "Starting",
126 "Filtering",
127 "Generate pinheads",
128 "Classification",
129 "Routing to ground",
130 "Routing supports to model surface",
131 "Interconnecting pillars",
132 "Merging support mesh",
133 "Done",
134 "Abort"
135 };
136
137 static const std::array<unsigned, NUM_STEPS> stepstate {
138 0,
139 10,
140 30,
141 50,
142 60,
143 70,
144 80,
145 99,
146 100,
147 0
148 };
149
150 if(builder.ctl().stopcondition()) pc = ABORT;
151
152 switch(pc) {
153 case BEGIN: pc = FILTER; break;
154 case FILTER: pc = PINHEADS; break;
155 case PINHEADS: pc = CLASSIFY; break;
156 case CLASSIFY: pc = ROUTING_GROUND; break;
157 case ROUTING_GROUND: pc = ROUTING_NONGROUND; break;
158 case ROUTING_NONGROUND: pc = CASCADE_PILLARS; break;
159 case CASCADE_PILLARS: pc = MERGE_RESULT; break;
160 case MERGE_RESULT: pc = DONE; break;
161 case DONE:
162 case ABORT: break;
163 default: ;
164 }
165
166 builder.ctl().statuscb(stepstate[pc], stepstr[pc]);
167 };
168
169 // Just here we run the computation...
170 while(pc < DONE) {
171 progress();
172 program[pc]();
173 }
174
175 return pc == ABORT;
176 }
177
pinhead_mesh_intersect(const Vec3d & s,const Vec3d & dir,double r_pin,double r_back,double width,double sd)178 IndexedMesh::hit_result SupportTreeBuildsteps::pinhead_mesh_intersect(
179 const Vec3d &s,
180 const Vec3d &dir,
181 double r_pin,
182 double r_back,
183 double width,
184 double sd)
185 {
186 static const size_t SAMPLES = 8;
187
188 // Move away slightly from the touching point to avoid raycasting on the
189 // inner surface of the mesh.
190
191 auto& m = m_mesh;
192 using HitResult = IndexedMesh::hit_result;
193
194 // Hit results
195 std::array<HitResult, SAMPLES> hits;
196
197 struct Rings {
198 double rpin;
199 double rback;
200 Vec3d spin;
201 Vec3d sback;
202 PointRing<SAMPLES> ring;
203
204 Vec3d backring(size_t idx) { return ring.get(idx, sback, rback); }
205 Vec3d pinring(size_t idx) { return ring.get(idx, spin, rpin); }
206 } rings {r_pin + sd, r_back + sd, s, s + width * dir, dir};
207
208 // We will shoot multiple rays from the head pinpoint in the direction
209 // of the pinhead robe (side) surface. The result will be the smallest
210 // hit distance.
211
212 ccr::for_each(size_t(0), hits.size(),
213 [&m, &rings, sd, &hits](size_t i) {
214
215 // Point on the circle on the pin sphere
216 Vec3d ps = rings.pinring(i);
217 // This is the point on the circle on the back sphere
218 Vec3d p = rings.backring(i);
219
220 auto &hit = hits[i];
221
222 // Point ps is not on mesh but can be inside or
223 // outside as well. This would cause many problems
224 // with ray-casting. To detect the position we will
225 // use the ray-casting result (which has an is_inside
226 // predicate).
227
228 Vec3d n = (p - ps).normalized();
229 auto q = m.query_ray_hit(ps + sd * n, n);
230
231 if (q.is_inside()) { // the hit is inside the model
232 if (q.distance() > rings.rpin) {
233 // If we are inside the model and the hit
234 // distance is bigger than our pin circle
235 // diameter, it probably indicates that the
236 // support point was already inside the
237 // model, or there is really no space
238 // around the point. We will assign a zero
239 // hit distance to these cases which will
240 // enforce the function return value to be
241 // an invalid ray with zero hit distance.
242 // (see min_element at the end)
243 hit = HitResult(0.0);
244 } else {
245 // re-cast the ray from the outside of the
246 // object. The starting point has an offset
247 // of 2*safety_distance because the
248 // original ray has also had an offset
249 auto q2 = m.query_ray_hit(ps + (q.distance() + 2 * sd) * n, n);
250 hit = q2;
251 }
252 } else
253 hit = q;
254 });
255
256 return min_hit(hits);
257 }
258
bridge_mesh_intersect(const Vec3d & src,const Vec3d & dir,double r,double sd)259 IndexedMesh::hit_result SupportTreeBuildsteps::bridge_mesh_intersect(
260 const Vec3d &src, const Vec3d &dir, double r, double sd)
261 {
262 static const size_t SAMPLES = 8;
263 PointRing<SAMPLES> ring{dir};
264
265 using Hit = IndexedMesh::hit_result;
266
267 // Hit results
268 std::array<Hit, SAMPLES> hits;
269
270 ccr::for_each(size_t(0), hits.size(),
271 [this, r, src, /*ins_check,*/ &ring, dir, sd, &hits] (size_t i)
272 {
273 Hit &hit = hits[i];
274
275 // Point on the circle on the pin sphere
276 Vec3d p = ring.get(i, src, r + sd);
277
278 auto hr = m_mesh.query_ray_hit(p + r * dir, dir);
279
280 if(/*ins_check && */hr.is_inside()) {
281 if(hr.distance() > 2 * r + sd) hit = Hit(0.0);
282 else {
283 // re-cast the ray from the outside of the object
284 hit = m_mesh.query_ray_hit(p + (hr.distance() + EPSILON) * dir, dir);
285 }
286 } else hit = hr;
287 });
288
289 return min_hit(hits);
290 }
291
interconnect(const Pillar & pillar,const Pillar & nextpillar)292 bool SupportTreeBuildsteps::interconnect(const Pillar &pillar,
293 const Pillar &nextpillar)
294 {
295 // We need to get the starting point of the zig-zag pattern. We have to
296 // be aware that the two head junctions are at different heights. We
297 // may start from the lowest junction and call it a day but this
298 // strategy would leave unconnected a lot of pillar duos where the
299 // shorter pillar is too short to start a new bridge but the taller
300 // pillar could still be bridged with the shorter one.
301 bool was_connected = false;
302
303 Vec3d supper = pillar.startpoint();
304 Vec3d slower = nextpillar.startpoint();
305 Vec3d eupper = pillar.endpoint();
306 Vec3d elower = nextpillar.endpoint();
307
308 double zmin = m_builder.ground_level + m_cfg.base_height_mm;
309 eupper(Z) = std::max(eupper(Z), zmin);
310 elower(Z) = std::max(elower(Z), zmin);
311
312 // The usable length of both pillars should be positive
313 if(slower(Z) - elower(Z) < 0) return false;
314 if(supper(Z) - eupper(Z) < 0) return false;
315
316 double pillar_dist = distance(Vec2d{slower(X), slower(Y)},
317 Vec2d{supper(X), supper(Y)});
318 double bridge_distance = pillar_dist / std::cos(-m_cfg.bridge_slope);
319 double zstep = pillar_dist * std::tan(-m_cfg.bridge_slope);
320
321 if(pillar_dist < 2 * m_cfg.head_back_radius_mm ||
322 pillar_dist > m_cfg.max_pillar_link_distance_mm) return false;
323
324 if(supper(Z) < slower(Z)) supper.swap(slower);
325 if(eupper(Z) < elower(Z)) eupper.swap(elower);
326
327 double startz = 0, endz = 0;
328
329 startz = slower(Z) - zstep < supper(Z) ? slower(Z) - zstep : slower(Z);
330 endz = eupper(Z) + zstep > elower(Z) ? eupper(Z) + zstep : eupper(Z);
331
332 if(slower(Z) - eupper(Z) < std::abs(zstep)) {
333 // no space for even one cross
334
335 // Get max available space
336 startz = std::min(supper(Z), slower(Z) - zstep);
337 endz = std::max(eupper(Z) + zstep, elower(Z));
338
339 // Align to center
340 double available_dist = (startz - endz);
341 double rounds = std::floor(available_dist / std::abs(zstep));
342 startz -= 0.5 * (available_dist - rounds * std::abs(zstep));
343 }
344
345 auto pcm = m_cfg.pillar_connection_mode;
346 bool docrosses =
347 pcm == PillarConnectionMode::cross ||
348 (pcm == PillarConnectionMode::dynamic &&
349 pillar_dist > 2*m_cfg.base_radius_mm);
350
351 // 'sj' means starting junction, 'ej' is the end junction of a bridge.
352 // They will be swapped in every iteration thus the zig-zag pattern.
353 // According to a config parameter, a second bridge may be added which
354 // results in a cross connection between the pillars.
355 Vec3d sj = supper, ej = slower; sj(Z) = startz; ej(Z) = sj(Z) + zstep;
356
357 // TODO: This is a workaround to not have a faulty last bridge
358 while(ej(Z) >= eupper(Z) /*endz*/) {
359 if(bridge_mesh_distance(sj, dirv(sj, ej), pillar.r) >= bridge_distance)
360 {
361 m_builder.add_crossbridge(sj, ej, pillar.r);
362 was_connected = true;
363 }
364
365 // double bridging: (crosses)
366 if(docrosses) {
367 Vec3d sjback(ej(X), ej(Y), sj(Z));
368 Vec3d ejback(sj(X), sj(Y), ej(Z));
369 if (sjback(Z) <= slower(Z) && ejback(Z) >= eupper(Z) &&
370 bridge_mesh_distance(sjback, dirv(sjback, ejback),
371 pillar.r) >= bridge_distance) {
372 // need to check collision for the cross stick
373 m_builder.add_crossbridge(sjback, ejback, pillar.r);
374 was_connected = true;
375 }
376 }
377
378 sj.swap(ej);
379 ej(Z) = sj(Z) + zstep;
380 }
381
382 return was_connected;
383 }
384
connect_to_nearpillar(const Head & head,long nearpillar_id)385 bool SupportTreeBuildsteps::connect_to_nearpillar(const Head &head,
386 long nearpillar_id)
387 {
388 auto nearpillar = [this, nearpillar_id]() -> const Pillar& {
389 return m_builder.pillar(nearpillar_id);
390 };
391
392 if (m_builder.bridgecount(nearpillar()) > m_cfg.max_bridges_on_pillar)
393 return false;
394
395 Vec3d headjp = head.junction_point();
396 Vec3d nearjp_u = nearpillar().startpoint();
397 Vec3d nearjp_l = nearpillar().endpoint();
398
399 double r = head.r_back_mm;
400 double d2d = distance(to_2d(headjp), to_2d(nearjp_u));
401 double d3d = distance(headjp, nearjp_u);
402
403 double hdiff = nearjp_u(Z) - headjp(Z);
404 double slope = std::atan2(hdiff, d2d);
405
406 Vec3d bridgestart = headjp;
407 Vec3d bridgeend = nearjp_u;
408 double max_len = r * m_cfg.max_bridge_length_mm / m_cfg.head_back_radius_mm;
409 double max_slope = m_cfg.bridge_slope;
410 double zdiff = 0.0;
411
412 // check the default situation if feasible for a bridge
413 if(d3d > max_len || slope > -max_slope) {
414 // not feasible to connect the two head junctions. We have to search
415 // for a suitable touch point.
416
417 double Zdown = headjp(Z) + d2d * std::tan(-max_slope);
418 Vec3d touchjp = bridgeend; touchjp(Z) = Zdown;
419 double D = distance(headjp, touchjp);
420 zdiff = Zdown - nearjp_u(Z);
421
422 if(zdiff > 0) {
423 Zdown -= zdiff;
424 bridgestart(Z) -= zdiff;
425 touchjp(Z) = Zdown;
426
427 double t = bridge_mesh_distance(headjp, DOWN, r);
428
429 // We can't insert a pillar under the source head to connect
430 // with the nearby pillar's starting junction
431 if(t < zdiff) return false;
432 }
433
434 if(Zdown <= nearjp_u(Z) && Zdown >= nearjp_l(Z) && D < max_len)
435 bridgeend(Z) = Zdown;
436 else
437 return false;
438 }
439
440 // There will be a minimum distance from the ground where the
441 // bridge is allowed to connect. This is an empiric value.
442 double minz = m_builder.ground_level + 4 * head.r_back_mm;
443 if(bridgeend(Z) < minz) return false;
444
445 double t = bridge_mesh_distance(bridgestart, dirv(bridgestart, bridgeend), r);
446
447 // Cannot insert the bridge. (further search might not worth the hassle)
448 if(t < distance(bridgestart, bridgeend)) return false;
449
450 std::lock_guard<ccr::BlockingMutex> lk(m_bridge_mutex);
451
452 if (m_builder.bridgecount(nearpillar()) < m_cfg.max_bridges_on_pillar) {
453 // A partial pillar is needed under the starting head.
454 if(zdiff > 0) {
455 m_builder.add_pillar(head.id, headjp.z() - bridgestart.z());
456 m_builder.add_junction(bridgestart, r);
457 m_builder.add_bridge(bridgestart, bridgeend, r);
458 } else {
459 m_builder.add_bridge(head.id, bridgeend);
460 }
461
462 m_builder.increment_bridges(nearpillar());
463 } else return false;
464
465 return true;
466 }
467
create_ground_pillar(const Vec3d & hjp,const Vec3d & sourcedir,double radius,long head_id)468 bool SupportTreeBuildsteps::create_ground_pillar(const Vec3d &hjp,
469 const Vec3d &sourcedir,
470 double radius,
471 long head_id)
472 {
473 Vec3d jp = hjp, endp = jp, dir = sourcedir;
474 long pillar_id = SupportTreeNode::ID_UNSET;
475 bool can_add_base = false, non_head = false;
476
477 double gndlvl = 0.; // The Z level where pedestals should be
478 double jp_gnd = 0.; // The lowest Z where a junction center can be
479 double gap_dist = 0.; // The gap distance between the model and the pad
480
481 auto to_floor = [&gndlvl](const Vec3d &p) { return Vec3d{p.x(), p.y(), gndlvl}; };
482
483 auto eval_limits = [this, &radius, &can_add_base, &gndlvl, &gap_dist, &jp_gnd]
484 (bool base_en = true)
485 {
486 can_add_base = base_en && radius >= m_cfg.head_back_radius_mm;
487 double base_r = can_add_base ? m_cfg.base_radius_mm : 0.;
488 gndlvl = m_builder.ground_level;
489 if (!can_add_base) gndlvl -= m_mesh.ground_level_offset();
490 jp_gnd = gndlvl + (can_add_base ? 0. : m_cfg.head_back_radius_mm);
491 gap_dist = m_cfg.pillar_base_safety_distance_mm + base_r + EPSILON;
492 };
493
494 eval_limits();
495
496 // We are dealing with a mini pillar that's potentially too long
497 if (radius < m_cfg.head_back_radius_mm && jp.z() - gndlvl > 20 * radius)
498 {
499 std::optional<DiffBridge> diffbr =
500 search_widening_path(jp, dir, radius, m_cfg.head_back_radius_mm);
501
502 if (diffbr && diffbr->endp.z() > jp_gnd) {
503 auto &br = m_builder.add_diffbridge(*diffbr);
504 if (head_id >= 0) m_builder.head(head_id).bridge_id = br.id;
505 endp = diffbr->endp;
506 radius = diffbr->end_r;
507 m_builder.add_junction(endp, radius);
508 non_head = true;
509 dir = diffbr->get_dir();
510 eval_limits();
511 } else return false;
512 }
513
514 if (m_cfg.object_elevation_mm < EPSILON)
515 {
516 // get a suitable direction for the corrector bridge. It is the
517 // original sourcedir's azimuth but the polar angle is saturated to the
518 // configured bridge slope.
519 auto [polar, azimuth] = dir_to_spheric(dir);
520 polar = PI - m_cfg.bridge_slope;
521 Vec3d d = spheric_to_dir(polar, azimuth).normalized();
522 double t = bridge_mesh_distance(endp, dir, radius);
523 double tmax = std::min(m_cfg.max_bridge_length_mm, t);
524 t = 0.;
525
526 double zd = endp.z() - jp_gnd;
527 double tmax2 = zd / std::sqrt(1 - m_cfg.bridge_slope * m_cfg.bridge_slope);
528 tmax = std::min(tmax, tmax2);
529
530 Vec3d nexp = endp;
531 double dlast = 0.;
532 while (((dlast = std::sqrt(m_mesh.squared_distance(to_floor(nexp)))) < gap_dist ||
533 !std::isinf(bridge_mesh_distance(nexp, DOWN, radius))) && t < tmax) {
534 t += radius;
535 nexp = endp + t * d;
536 }
537
538 if (dlast < gap_dist && can_add_base) {
539 nexp = endp;
540 t = 0.;
541 can_add_base = false;
542 eval_limits(can_add_base);
543
544 zd = endp.z() - jp_gnd;
545 tmax2 = zd / std::sqrt(1 - m_cfg.bridge_slope * m_cfg.bridge_slope);
546 tmax = std::min(tmax, tmax2);
547
548 while (((dlast = std::sqrt(m_mesh.squared_distance(to_floor(nexp)))) < gap_dist ||
549 !std::isinf(bridge_mesh_distance(nexp, DOWN, radius))) && t < tmax) {
550 t += radius;
551 nexp = endp + t * d;
552 }
553 }
554
555 // Could not find a path to avoid the pad gap
556 if (dlast < gap_dist) return false;
557
558 if (t > 0.) { // Need to make additional bridge
559 const Bridge& br = m_builder.add_bridge(endp, nexp, radius);
560 if (head_id >= 0) m_builder.head(head_id).bridge_id = br.id;
561
562 m_builder.add_junction(nexp, radius);
563 endp = nexp;
564 non_head = true;
565 }
566 }
567
568 Vec3d gp = to_floor(endp);
569 double h = endp.z() - gp.z();
570
571 pillar_id = head_id >= 0 && !non_head ? m_builder.add_pillar(head_id, h) :
572 m_builder.add_pillar(gp, h, radius);
573
574 if (can_add_base)
575 add_pillar_base(pillar_id);
576
577 if(pillar_id >= 0) // Save the pillar endpoint in the spatial index
578 m_pillar_index.guarded_insert(m_builder.pillar(pillar_id).endpt,
579 unsigned(pillar_id));
580
581 return true;
582 }
583
search_widening_path(const Vec3d & jp,const Vec3d & dir,double radius,double new_radius)584 std::optional<DiffBridge> SupportTreeBuildsteps::search_widening_path(
585 const Vec3d &jp, const Vec3d &dir, double radius, double new_radius)
586 {
587 double w = radius + 2 * m_cfg.head_back_radius_mm;
588 double stopval = w + jp.z() - m_builder.ground_level;
589 Optimizer<AlgNLoptSubplex> solver(get_criteria(m_cfg).stop_score(stopval));
590
591 auto [polar, azimuth] = dir_to_spheric(dir);
592
593 double fallback_ratio = radius / m_cfg.head_back_radius_mm;
594
595 auto oresult = solver.to_max().optimize(
596 [this, jp, radius, new_radius](const opt::Input<3> &input) {
597 auto &[plr, azm, t] = input;
598
599 auto d = spheric_to_dir(plr, azm).normalized();
600 double ret = pinhead_mesh_intersect(jp, d, radius, new_radius, t)
601 .distance();
602 double down = bridge_mesh_distance(jp + t * d, d, new_radius);
603
604 if (ret > t && std::isinf(down))
605 ret += jp.z() - m_builder.ground_level;
606
607 return ret;
608 },
609 initvals({polar, azimuth, w}), // start with what we have
610 bounds({
611 {PI - m_cfg.bridge_slope, PI}, // Must not exceed the slope limit
612 {-PI, PI}, // azimuth can be a full search
613 {radius + m_cfg.head_back_radius_mm,
614 fallback_ratio * m_cfg.max_bridge_length_mm}
615 }));
616
617 if (oresult.score >= stopval) {
618 polar = std::get<0>(oresult.optimum);
619 azimuth = std::get<1>(oresult.optimum);
620 double t = std::get<2>(oresult.optimum);
621 Vec3d endp = jp + t * spheric_to_dir(polar, azimuth);
622
623 return DiffBridge(jp, endp, radius, m_cfg.head_back_radius_mm);
624 }
625
626 return {};
627 }
628
filter()629 void SupportTreeBuildsteps::filter()
630 {
631 // Get the points that are too close to each other and keep only the
632 // first one
633 auto aliases = cluster(m_points, D_SP, 2);
634
635 PtIndices filtered_indices;
636 filtered_indices.reserve(aliases.size());
637 m_iheads.reserve(aliases.size());
638 m_iheadless.reserve(aliases.size());
639 for(auto& a : aliases) {
640 // Here we keep only the front point of the cluster.
641 filtered_indices.emplace_back(a.front());
642 }
643
644 // calculate the normals to the triangles for filtered points
645 auto nmls = sla::normals(m_points, m_mesh, m_cfg.head_front_radius_mm,
646 m_thr, filtered_indices);
647
648 // Not all of the support points have to be a valid position for
649 // support creation. The angle may be inappropriate or there may
650 // not be enough space for the pinhead. Filtering is applied for
651 // these reasons.
652
653 std::vector<Head> heads; heads.reserve(m_support_pts.size());
654 for (const SupportPoint &sp : m_support_pts) {
655 m_thr();
656 heads.emplace_back(
657 std::nan(""),
658 sp.head_front_radius,
659 0.,
660 m_cfg.head_penetration_mm,
661 Vec3d::Zero(), // dir
662 sp.pos.cast<double>() // displacement
663 );
664 }
665
666 std::function<void(unsigned, size_t, double)> filterfn;
667 filterfn = [this, &nmls, &heads, &filterfn](unsigned fidx, size_t i, double back_r) {
668 m_thr();
669
670 auto n = nmls.row(Eigen::Index(i));
671
672 // for all normals we generate the spherical coordinates and
673 // saturate the polar angle to 45 degrees from the bottom then
674 // convert back to standard coordinates to get the new normal.
675 // Then we just create a quaternion from the two normals
676 // (Quaternion::FromTwoVectors) and apply the rotation to the
677 // arrow head.
678
679 auto [polar, azimuth] = dir_to_spheric(n);
680
681 // skip if the tilt is not sane
682 if (polar < PI - m_cfg.normal_cutoff_angle) return;
683
684 // We saturate the polar angle to 3pi/4
685 polar = std::max(polar, PI - m_cfg.bridge_slope);
686
687 // save the head (pinpoint) position
688 Vec3d hp = m_points.row(fidx);
689
690 double lmin = m_cfg.head_width_mm, lmax = lmin;
691
692 if (back_r < m_cfg.head_back_radius_mm) {
693 lmin = 0., lmax = m_cfg.head_penetration_mm;
694 }
695
696 // The distance needed for a pinhead to not collide with model.
697 double w = lmin + 2 * back_r + 2 * m_cfg.head_front_radius_mm -
698 m_cfg.head_penetration_mm;
699
700 double pin_r = double(m_support_pts[fidx].head_front_radius);
701
702 // Reassemble the now corrected normal
703 auto nn = spheric_to_dir(polar, azimuth).normalized();
704
705 // check available distance
706 IndexedMesh::hit_result t = pinhead_mesh_intersect(hp, nn, pin_r,
707 back_r, w);
708
709 if (t.distance() < w) {
710 // Let's try to optimize this angle, there might be a
711 // viable normal that doesn't collide with the model
712 // geometry and its very close to the default.
713
714 Optimizer<AlgNLoptGenetic> solver(get_criteria(m_cfg));
715 solver.seed(0); // we want deterministic behavior
716
717 auto oresult = solver.to_max().optimize(
718 [this, pin_r, back_r, hp](const opt::Input<3> &input)
719 {
720 auto &[plr, azm, l] = input;
721
722 auto dir = spheric_to_dir(plr, azm).normalized();
723
724 return pinhead_mesh_intersect(
725 hp, dir, pin_r, back_r, l).distance();
726 },
727 initvals({polar, azimuth, (lmin + lmax) / 2.}), // start with what we have
728 bounds({
729 {PI - m_cfg.bridge_slope, PI}, // Must not exceed the slope limit
730 {-PI, PI}, // azimuth can be a full search
731 {lmin, lmax}
732 }));
733
734 if(oresult.score > w) {
735 polar = std::get<0>(oresult.optimum);
736 azimuth = std::get<1>(oresult.optimum);
737 nn = spheric_to_dir(polar, azimuth).normalized();
738 lmin = std::get<2>(oresult.optimum);
739 t = IndexedMesh::hit_result(oresult.score);
740 }
741 }
742
743 if (t.distance() > w && hp(Z) + w * nn(Z) >= m_builder.ground_level) {
744 Head &h = heads[fidx];
745 h.id = fidx; h.dir = nn; h.width_mm = lmin; h.r_back_mm = back_r;
746 } else if (back_r > m_cfg.head_fallback_radius_mm) {
747 filterfn(fidx, i, m_cfg.head_fallback_radius_mm);
748 }
749 };
750
751 ccr::for_each(size_t(0), filtered_indices.size(),
752 [this, &filterfn, &filtered_indices] (size_t i) {
753 filterfn(filtered_indices[i], i, m_cfg.head_back_radius_mm);
754 });
755
756 for (size_t i = 0; i < heads.size(); ++i)
757 if (heads[i].is_valid()) {
758 m_builder.add_head(i, heads[i]);
759 m_iheads.emplace_back(i);
760 }
761
762 m_thr();
763 }
764
add_pinheads()765 void SupportTreeBuildsteps::add_pinheads()
766 {
767 }
768
classify()769 void SupportTreeBuildsteps::classify()
770 {
771 // We should first get the heads that reach the ground directly
772 PtIndices ground_head_indices;
773 ground_head_indices.reserve(m_iheads.size());
774 m_iheads_onmodel.reserve(m_iheads.size());
775
776 // First we decide which heads reach the ground and can be full
777 // pillars and which shall be connected to the model surface (or
778 // search a suitable path around the surface that leads to the
779 // ground -- TODO)
780 for(unsigned i : m_iheads) {
781 m_thr();
782
783 Head &head = m_builder.head(i);
784 double r = head.r_back_mm;
785 Vec3d headjp = head.junction_point();
786
787 // collision check
788 auto hit = bridge_mesh_intersect(headjp, DOWN, r);
789
790 if(std::isinf(hit.distance())) ground_head_indices.emplace_back(i);
791 else if(m_cfg.ground_facing_only) head.invalidate();
792 else m_iheads_onmodel.emplace_back(i);
793
794 m_head_to_ground_scans[i] = hit;
795 }
796
797 // We want to search for clusters of points that are far enough
798 // from each other in the XY plane to not cross their pillar bases
799 // These clusters of support points will join in one pillar,
800 // possibly in their centroid support point.
801
802 auto pointfn = [this](unsigned i) {
803 return m_builder.head(i).junction_point();
804 };
805
806 auto predicate = [this](const PointIndexEl &e1,
807 const PointIndexEl &e2) {
808 double d2d = distance(to_2d(e1.first), to_2d(e2.first));
809 double d3d = distance(e1.first, e2.first);
810 return d2d < 2 * m_cfg.base_radius_mm
811 && d3d < m_cfg.max_bridge_length_mm;
812 };
813
814 m_pillar_clusters = cluster(ground_head_indices, pointfn, predicate,
815 m_cfg.max_bridges_on_pillar);
816 }
817
routing_to_ground()818 void SupportTreeBuildsteps::routing_to_ground()
819 {
820 ClusterEl cl_centroids;
821 cl_centroids.reserve(m_pillar_clusters.size());
822
823 for (auto &cl : m_pillar_clusters) {
824 m_thr();
825
826 // place all the centroid head positions into the index. We
827 // will query for alternative pillar positions. If a sidehead
828 // cannot connect to the cluster centroid, we have to search
829 // for another head with a full pillar. Also when there are two
830 // elements in the cluster, the centroid is arbitrary and the
831 // sidehead is allowed to connect to a nearby pillar to
832 // increase structural stability.
833
834 if (cl.empty()) continue;
835
836 // get the current cluster centroid
837 auto & thr = m_thr;
838 const auto &points = m_points;
839
840 long lcid = cluster_centroid(
841 cl, [&points](size_t idx) { return points.row(long(idx)); },
842 [thr](const Vec3d &p1, const Vec3d &p2) {
843 thr();
844 return distance(Vec2d(p1(X), p1(Y)), Vec2d(p2(X), p2(Y)));
845 });
846
847 assert(lcid >= 0);
848 unsigned hid = cl[size_t(lcid)]; // Head ID
849
850 cl_centroids.emplace_back(hid);
851
852 Head &h = m_builder.head(hid);
853
854 if (!create_ground_pillar(h.junction_point(), h.dir, h.r_back_mm, h.id)) {
855 BOOST_LOG_TRIVIAL(warning)
856 << "Pillar cannot be created for support point id: " << hid;
857 m_iheads_onmodel.emplace_back(h.id);
858 continue;
859 }
860 }
861
862 // now we will go through the clusters ones again and connect the
863 // sidepoints with the cluster centroid (which is a ground pillar)
864 // or a nearby pillar if the centroid is unreachable.
865 size_t ci = 0;
866 for (auto cl : m_pillar_clusters) {
867 m_thr();
868
869 auto cidx = cl_centroids[ci++];
870
871 auto q = m_pillar_index.query(m_builder.head(cidx).junction_point(), 1);
872 if (!q.empty()) {
873 long centerpillarID = q.front().second;
874 for (auto c : cl) {
875 m_thr();
876 if (c == cidx) continue;
877
878 auto &sidehead = m_builder.head(c);
879
880 if (!connect_to_nearpillar(sidehead, centerpillarID) &&
881 !search_pillar_and_connect(sidehead)) {
882 Vec3d pstart = sidehead.junction_point();
883 // Vec3d pend = Vec3d{pstart(X), pstart(Y), gndlvl};
884 // Could not find a pillar, create one
885 create_ground_pillar(pstart, sidehead.dir, sidehead.r_back_mm, sidehead.id);
886 }
887 }
888 }
889 }
890 }
891
connect_to_ground(Head & head,const Vec3d & dir)892 bool SupportTreeBuildsteps::connect_to_ground(Head &head, const Vec3d &dir)
893 {
894 auto hjp = head.junction_point();
895 double r = head.r_back_mm;
896 double t = bridge_mesh_distance(hjp, dir, head.r_back_mm);
897 double d = 0, tdown = 0;
898 t = std::min(t, m_cfg.max_bridge_length_mm * r / m_cfg.head_back_radius_mm);
899
900 while (d < t && !std::isinf(tdown = bridge_mesh_distance(hjp + d * dir, DOWN, r)))
901 d += r;
902
903 if(!std::isinf(tdown)) return false;
904
905 Vec3d endp = hjp + d * dir;
906 bool ret = false;
907
908 if ((ret = create_ground_pillar(endp, dir, head.r_back_mm))) {
909 m_builder.add_bridge(head.id, endp);
910 m_builder.add_junction(endp, head.r_back_mm);
911 }
912
913 return ret;
914 }
915
connect_to_ground(Head & head)916 bool SupportTreeBuildsteps::connect_to_ground(Head &head)
917 {
918 if (connect_to_ground(head, head.dir)) return true;
919
920 // Optimize bridge direction:
921 // Straight path failed so we will try to search for a suitable
922 // direction out of the cavity.
923 auto [polar, azimuth] = dir_to_spheric(head.dir);
924
925 Optimizer<AlgNLoptGenetic> solver(get_criteria(m_cfg).stop_score(1e6));
926 solver.seed(0); // we want deterministic behavior
927
928 double r_back = head.r_back_mm;
929 Vec3d hjp = head.junction_point();
930 auto oresult = solver.to_max().optimize(
931 [this, hjp, r_back](const opt::Input<2> &input) {
932 auto &[plr, azm] = input;
933 Vec3d n = spheric_to_dir(plr, azm).normalized();
934 return bridge_mesh_distance(hjp, n, r_back);
935 },
936 initvals({polar, azimuth}), // let's start with what we have
937 bounds({ {PI - m_cfg.bridge_slope, PI}, {-PI, PI} })
938 );
939
940 Vec3d bridgedir = spheric_to_dir(oresult.optimum).normalized();
941 return connect_to_ground(head, bridgedir);
942 }
943
connect_to_model_body(Head & head)944 bool SupportTreeBuildsteps::connect_to_model_body(Head &head)
945 {
946 if (head.id <= SupportTreeNode::ID_UNSET) return false;
947
948 auto it = m_head_to_ground_scans.find(unsigned(head.id));
949 if (it == m_head_to_ground_scans.end()) return false;
950
951 auto &hit = it->second;
952
953 if (!hit.is_hit()) {
954 // TODO scan for potential anchor points on model surface
955 return false;
956 }
957
958 Vec3d hjp = head.junction_point();
959 double zangle = std::asin(hit.direction()(Z));
960 zangle = std::max(zangle, PI/4);
961 double h = std::sin(zangle) * head.fullwidth();
962
963 // The width of the tail head that we would like to have...
964 h = std::min(hit.distance() - head.r_back_mm, h);
965
966 // If this is a mini pillar dont bother with the tail width, can be 0.
967 if (head.r_back_mm < m_cfg.head_back_radius_mm) h = std::max(h, 0.);
968 else if (h <= 0.) return false;
969
970 Vec3d endp{hjp(X), hjp(Y), hjp(Z) - hit.distance() + h};
971 auto center_hit = m_mesh.query_ray_hit(hjp, DOWN);
972
973 double hitdiff = center_hit.distance() - hit.distance();
974 Vec3d hitp = std::abs(hitdiff) < 2*head.r_back_mm?
975 center_hit.position() : hit.position();
976
977 long pillar_id = m_builder.add_pillar(head.id, hjp.z() - endp.z());
978 Pillar &pill = m_builder.pillar(pillar_id);
979
980 Vec3d taildir = endp - hitp;
981 double dist = (hitp - endp).norm() + m_cfg.head_penetration_mm;
982 double w = dist - 2 * head.r_pin_mm - head.r_back_mm;
983
984 if (w < 0.) {
985 BOOST_LOG_TRIVIAL(error) << "Pinhead width is negative!";
986 w = 0.;
987 }
988
989 m_builder.add_anchor(head.r_back_mm, head.r_pin_mm, w,
990 m_cfg.head_penetration_mm, taildir, hitp);
991
992 m_pillar_index.guarded_insert(pill.endpoint(), pill.id);
993
994 return true;
995 }
996
search_pillar_and_connect(const Head & source)997 bool SupportTreeBuildsteps::search_pillar_and_connect(const Head &source)
998 {
999 // Hope that a local copy takes less time than the whole search loop.
1000 // We also need to remove elements progressively from the copied index.
1001 PointIndex spindex = m_pillar_index.guarded_clone();
1002
1003 long nearest_id = SupportTreeNode::ID_UNSET;
1004
1005 Vec3d querypt = source.junction_point();
1006
1007 while(nearest_id < 0 && !spindex.empty()) { m_thr();
1008 // loop until a suitable head is not found
1009 // if there is a pillar closer than the cluster center
1010 // (this may happen as the clustering is not perfect)
1011 // than we will bridge to this closer pillar
1012
1013 Vec3d qp(querypt(X), querypt(Y), m_builder.ground_level);
1014 auto qres = spindex.nearest(qp, 1);
1015 if(qres.empty()) break;
1016
1017 auto ne = qres.front();
1018 nearest_id = ne.second;
1019
1020 if(nearest_id >= 0) {
1021 if (size_t(nearest_id) < m_builder.pillarcount()) {
1022 if(!connect_to_nearpillar(source, nearest_id) ||
1023 m_builder.pillar(nearest_id).r < source.r_back_mm) {
1024 nearest_id = SupportTreeNode::ID_UNSET; // continue searching
1025 spindex.remove(ne); // without the current pillar
1026 }
1027 }
1028 }
1029 }
1030
1031 return nearest_id >= 0;
1032 }
1033
routing_to_model()1034 void SupportTreeBuildsteps::routing_to_model()
1035 {
1036 // We need to check if there is an easy way out to the bed surface.
1037 // If it can be routed there with a bridge shorter than
1038 // min_bridge_distance.
1039
1040 ccr::for_each(m_iheads_onmodel.begin(), m_iheads_onmodel.end(),
1041 [this] (const unsigned idx) {
1042 m_thr();
1043
1044 auto& head = m_builder.head(idx);
1045
1046 // Search nearby pillar
1047 if (search_pillar_and_connect(head)) { return; }
1048
1049 // Cannot connect to nearby pillar. We will try to search for
1050 // a route to the ground.
1051 if (connect_to_ground(head)) { return; }
1052
1053 // No route to the ground, so connect to the model body as a last resort
1054 if (connect_to_model_body(head)) { return; }
1055
1056 // We have failed to route this head.
1057 BOOST_LOG_TRIVIAL(warning)
1058 << "Failed to route model facing support point. ID: " << idx;
1059
1060 head.invalidate();
1061 });
1062 }
1063
interconnect_pillars()1064 void SupportTreeBuildsteps::interconnect_pillars()
1065 {
1066 // Now comes the algorithm that connects pillars with each other.
1067 // Ideally every pillar should be connected with at least one of its
1068 // neighbors if that neighbor is within max_pillar_link_distance
1069
1070 // Pillars with height exceeding H1 will require at least one neighbor
1071 // to connect with. Height exceeding H2 require two neighbors.
1072 double H1 = m_cfg.max_solo_pillar_height_mm;
1073 double H2 = m_cfg.max_dual_pillar_height_mm;
1074 double d = m_cfg.max_pillar_link_distance_mm;
1075
1076 //A connection between two pillars only counts if the height ratio is
1077 // bigger than 50%
1078 double min_height_ratio = 0.5;
1079
1080 std::set<unsigned long> pairs;
1081
1082 // A function to connect one pillar with its neighbors. THe number of
1083 // neighbors is given in the configuration. This function if called
1084 // for every pillar in the pillar index. A pair of pillar will not
1085 // be connected multiple times this is ensured by the 'pairs' set which
1086 // remembers the processed pillar pairs
1087 auto cascadefn =
1088 [this, d, &pairs, min_height_ratio, H1] (const PointIndexEl& el)
1089 {
1090 Vec3d qp = el.first; // endpoint of the pillar
1091
1092 const Pillar& pillar = m_builder.pillar(el.second); // actual pillar
1093
1094 // Get the max number of neighbors a pillar should connect to
1095 unsigned neighbors = m_cfg.pillar_cascade_neighbors;
1096
1097 // connections are already enough for the pillar
1098 if(pillar.links >= neighbors) return;
1099
1100 double max_d = d * pillar.r / m_cfg.head_back_radius_mm;
1101 // Query all remaining points within reach
1102 auto qres = m_pillar_index.query([qp, max_d](const PointIndexEl& e){
1103 return distance(e.first, qp) < max_d;
1104 });
1105
1106 // sort the result by distance (have to check if this is needed)
1107 std::sort(qres.begin(), qres.end(),
1108 [qp](const PointIndexEl& e1, const PointIndexEl& e2){
1109 return distance(e1.first, qp) < distance(e2.first, qp);
1110 });
1111
1112 for(auto& re : qres) { // process the queried neighbors
1113
1114 if(re.second == el.second) continue; // Skip self
1115
1116 auto a = el.second, b = re.second;
1117
1118 // Get unique hash for the given pair (order doesn't matter)
1119 auto hashval = pairhash(a, b);
1120
1121 // Search for the pair amongst the remembered pairs
1122 if(pairs.find(hashval) != pairs.end()) continue;
1123
1124 const Pillar& neighborpillar = m_builder.pillar(re.second);
1125
1126 // this neighbor is occupied, skip
1127 if (neighborpillar.links >= neighbors) continue;
1128 if (neighborpillar.r < pillar.r) continue;
1129
1130 if(interconnect(pillar, neighborpillar)) {
1131 pairs.insert(hashval);
1132
1133 // If the interconnection length between the two pillars is
1134 // less than 50% of the longer pillar's height, don't count
1135 if(pillar.height < H1 ||
1136 neighborpillar.height / pillar.height > min_height_ratio)
1137 m_builder.increment_links(pillar);
1138
1139 if(neighborpillar.height < H1 ||
1140 pillar.height / neighborpillar.height > min_height_ratio)
1141 m_builder.increment_links(neighborpillar);
1142
1143 }
1144
1145 // connections are enough for one pillar
1146 if(pillar.links >= neighbors) break;
1147 }
1148 };
1149
1150 // Run the cascade for the pillars in the index
1151 m_pillar_index.foreach(cascadefn);
1152
1153 // We would be done here if we could allow some pillars to not be
1154 // connected with any neighbors. But this might leave the support tree
1155 // unprintable.
1156 //
1157 // The current solution is to insert additional pillars next to these
1158 // lonely pillars. One or even two additional pillar might get inserted
1159 // depending on the length of the lonely pillar.
1160
1161 size_t pillarcount = m_builder.pillarcount();
1162
1163 // Again, go through all pillars, this time in the whole support tree
1164 // not just the index.
1165 for(size_t pid = 0; pid < pillarcount; pid++) {
1166 auto pillar = [this, pid]() { return m_builder.pillar(pid); };
1167
1168 // Decide how many additional pillars will be needed:
1169
1170 unsigned needpillars = 0;
1171 if (pillar().bridges > m_cfg.max_bridges_on_pillar)
1172 needpillars = 3;
1173 else if (pillar().links < 2 && pillar().height > H2) {
1174 // Not enough neighbors to support this pillar
1175 needpillars = 2;
1176 } else if (pillar().links < 1 && pillar().height > H1) {
1177 // No neighbors could be found and the pillar is too long.
1178 needpillars = 1;
1179 }
1180
1181 needpillars = std::max(pillar().links, needpillars) - pillar().links;
1182 if (needpillars == 0) continue;
1183
1184 // Search for new pillar locations:
1185
1186 bool found = false;
1187 double alpha = 0; // goes to 2Pi
1188 double r = 2 * m_cfg.base_radius_mm;
1189 Vec3d pillarsp = pillar().startpoint();
1190
1191 // temp value for starting point detection
1192 Vec3d sp(pillarsp(X), pillarsp(Y), pillarsp(Z) - r);
1193
1194 // A vector of bool for placement feasbility
1195 std::vector<bool> canplace(needpillars, false);
1196 std::vector<Vec3d> spts(needpillars); // vector of starting points
1197
1198 double gnd = m_builder.ground_level;
1199 double min_dist = m_cfg.pillar_base_safety_distance_mm +
1200 m_cfg.base_radius_mm + EPSILON;
1201
1202 while(!found && alpha < 2*PI) {
1203 for (unsigned n = 0;
1204 n < needpillars && (!n || canplace[n - 1]);
1205 n++)
1206 {
1207 double a = alpha + n * PI / 3;
1208 Vec3d s = sp;
1209 s(X) += std::cos(a) * r;
1210 s(Y) += std::sin(a) * r;
1211 spts[n] = s;
1212
1213 // Check the path vertically down
1214 Vec3d check_from = s + Vec3d{0., 0., pillar().r};
1215 auto hr = bridge_mesh_intersect(check_from, DOWN, pillar().r);
1216 Vec3d gndsp{s(X), s(Y), gnd};
1217
1218 // If the path is clear, check for pillar base collisions
1219 canplace[n] = std::isinf(hr.distance()) &&
1220 std::sqrt(m_mesh.squared_distance(gndsp)) >
1221 min_dist;
1222 }
1223
1224 found = std::all_of(canplace.begin(), canplace.end(),
1225 [](bool v) { return v; });
1226
1227 // 20 angles will be tried...
1228 alpha += 0.1 * PI;
1229 }
1230
1231 std::vector<long> newpills;
1232 newpills.reserve(needpillars);
1233
1234 if (found)
1235 for (unsigned n = 0; n < needpillars; n++) {
1236 Vec3d s = spts[n];
1237 Pillar p(Vec3d{s.x(), s.y(), gnd}, s.z() - gnd, pillar().r);
1238
1239 if (interconnect(pillar(), p)) {
1240 Pillar &pp = m_builder.pillar(m_builder.add_pillar(p));
1241
1242 add_pillar_base(pp.id);
1243
1244 m_pillar_index.insert(pp.endpoint(), unsigned(pp.id));
1245
1246 m_builder.add_junction(s, pillar().r);
1247 double t = bridge_mesh_distance(pillarsp, dirv(pillarsp, s),
1248 pillar().r);
1249 if (distance(pillarsp, s) < t)
1250 m_builder.add_bridge(pillarsp, s, pillar().r);
1251
1252 if (pillar().endpoint()(Z) > m_builder.ground_level + pillar().r)
1253 m_builder.add_junction(pillar().endpoint(), pillar().r);
1254
1255 newpills.emplace_back(pp.id);
1256 m_builder.increment_links(pillar());
1257 m_builder.increment_links(pp);
1258 }
1259 }
1260
1261 if(!newpills.empty()) {
1262 for(auto it = newpills.begin(), nx = std::next(it);
1263 nx != newpills.end(); ++it, ++nx) {
1264 const Pillar& itpll = m_builder.pillar(*it);
1265 const Pillar& nxpll = m_builder.pillar(*nx);
1266 if(interconnect(itpll, nxpll)) {
1267 m_builder.increment_links(itpll);
1268 m_builder.increment_links(nxpll);
1269 }
1270 }
1271
1272 m_pillar_index.foreach(cascadefn);
1273 }
1274 }
1275 }
1276
1277 }} // namespace Slic3r::sla
1278