-
-
Notifications
You must be signed in to change notification settings - Fork 891
/
Copy pathTreeSupport.cpp
2460 lines (2207 loc) · 129 KB
/
TreeSupport.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright (c) 2023 UltiMaker
// CuraEngine is released under the terms of the AGPLv3 or higher
#include "TreeSupport.h"
#include <chrono>
#include <fstream>
#include <optional>
#include <stdio.h>
#include <string>
#include <thread>
#include <range/v3/view/drop_last.hpp>
#include <range/v3/view/enumerate.hpp>
#include <range/v3/view/iota.hpp>
#include <range/v3/view/reverse.hpp>
#include <scripta/logger.h>
#include <spdlog/spdlog.h>
#include "Application.h" //To get settings.
#include "TreeSupportTipGenerator.h"
#include "TreeSupportUtils.h"
#include "infill.h"
#include "infill/SierpinskiFillProvider.h"
#include "progress/Progress.h"
#include "settings/EnumSettings.h"
#include "support.h" //For precomputeCrossInfillTree
#include "utils/Simplify.h"
#include "utils/ThreadPool.h"
#include "utils/algorithm.h"
#include "utils/math.h" //For round_up_divide and PI.
#include "utils/polygonUtils.h" //For moveInside.
#include "utils/section_type.h"
namespace cura
{
TreeSupport::TreeSupport(const SliceDataStorage& storage)
{
size_t largest_printed_mesh_idx = 0;
for (const std::shared_ptr<SliceMeshStorage>& mesh_ptr : storage.meshes)
{
const auto& mesh = *mesh_ptr;
TreeSupportSettings::some_model_contains_thick_roof |= mesh.settings.get<coord_t>("support_roof_height") >= 2 * mesh.settings.get<coord_t>("layer_height");
TreeSupportSettings::has_to_rely_on_min_xy_dist_only |= mesh.settings.get<coord_t>("support_top_distance") == 0
|| mesh.settings.get<coord_t>("support_bottom_distance") == 0
|| mesh.settings.get<coord_t>("min_feature_size") < (FUDGE_LENGTH * 2);
}
// Group all meshes that can be processed together. NOTE this is different from mesh-groups!
// Only one setting object is needed per group, as different settings in the same group may only occur in the tip, which uses the original settings objects from the meshes.
for (auto [mesh_idx, mesh_ptr] : storage.meshes | ranges::views::enumerate)
{
SliceMeshStorage& mesh = *mesh_ptr;
const bool non_supportable_mesh = mesh.settings.get<bool>("infill_mesh") || mesh.settings.get<bool>("anti_overhang_mesh") || mesh.settings.get<bool>("support_mesh");
if (mesh.settings.get<ESupportStructure>("support_structure") != ESupportStructure::TREE || ! mesh.settings.get<bool>("support_enable") || non_supportable_mesh)
{
continue;
}
bool added = false;
TreeSupportSettings next_settings(mesh.settings);
for (auto& grouped_mesh : grouped_meshes)
{
if (next_settings == grouped_mesh.first)
{
added = true;
grouped_mesh.second.emplace_back(mesh_idx);
}
}
if (! added)
{
grouped_meshes.emplace_back(next_settings, std::vector<size_t>{ mesh_idx });
}
// no need to do this per mesh group as adaptive layers and raft setting are not setable per mesh.
if (storage.meshes[largest_printed_mesh_idx]->layers.back().printZ < mesh.layers.back().printZ)
{
largest_printed_mesh_idx = mesh_idx;
}
}
std::vector<coord_t> known_z(storage.meshes[largest_printed_mesh_idx]->layers.size());
for (auto [z, layer] : ranges::views::enumerate(storage.meshes[largest_printed_mesh_idx]->layers))
{
known_z[z] = layer.printZ;
}
for (auto& mesh : grouped_meshes)
{
mesh.first.setActualZ(known_z);
}
fake_roof_areas = std::vector<std::vector<FakeRoofArea>>(storage.support.supportLayers.size(), std::vector<FakeRoofArea>());
}
void TreeSupport::generateSupportAreas(SliceDataStorage& storage)
{
if (grouped_meshes.empty())
{
return;
}
if (storage.support.cross_fill_provider == nullptr)
{
AreaSupport::precomputeCrossInfillTree(storage);
}
// Process every mesh group. These groups can not be processed parallel, as the processing in each group is parallelized, and nested parallelization is disables and slow.
for (auto [counter, processing] : grouped_meshes | ranges::views::enumerate)
{
// process each combination of meshes
std::vector<std::set<TreeSupportElement*>> move_bounds(
storage.support.supportLayers
.size()); // Value is the area where support may be placed. As this is calculated in CreateLayerPathing it is saved and reused in drawAreas.
additional_required_support_area = std::vector<Shape>(storage.support.supportLayers.size(), Shape());
spdlog::info("Processing support tree mesh group {} of {} containing {} meshes.", counter + 1, grouped_meshes.size(), grouped_meshes[counter].second.size());
std::vector<Shape> exclude(storage.support.supportLayers.size());
auto t_start = std::chrono::high_resolution_clock::now();
// get all already existing support areas and exclude them
cura::parallel_for<coord_t>(
LayerIndex(0),
LayerIndex(storage.support.supportLayers.size()),
[&](const LayerIndex layer_idx)
{
Shape exlude_at_layer;
exlude_at_layer.push_back(storage.support.supportLayers[layer_idx].support_bottom);
exlude_at_layer.push_back(storage.support.supportLayers[layer_idx].support_roof);
for (auto part : storage.support.supportLayers[layer_idx].support_infill_parts)
{
exlude_at_layer.push_back(part.outline_);
}
exclude[layer_idx] = exlude_at_layer.unionPolygons();
scripta::log("tree_support_exclude", exclude[layer_idx], SectionType::SUPPORT, layer_idx);
});
config = processing.first; // This struct is used to easy retrieve setting. No other function except those in TreeModelVolumes and generateInitialAreas have knowledge of
// the existence of multiple meshes being processed.
progress_multiplier = 1.0 / double(grouped_meshes.size());
progress_offset = counter == 0 ? 0 : TREE_PROGRESS_TOTAL * (double(counter) * progress_multiplier);
volumes_ = TreeModelVolumes(
storage,
config.maximum_move_distance,
config.maximum_move_distance_slow,
config.support_line_width / 2,
processing.second.front(),
progress_multiplier,
progress_offset,
exclude);
// ### Precalculate avoidances, collision etc.
const LayerIndex max_required_layer = precalculate(storage, processing.second);
if (max_required_layer < 0)
{
spdlog::info("Support tree mesh group {} does not have any overhang. Skipping tree support generation for this support tree mesh group.", counter + 1);
continue; // If there is no overhang to support, skip these meshes
}
const auto t_precalc = std::chrono::high_resolution_clock::now();
// ### Place tips of the support tree
for (size_t mesh_idx : processing.second)
{
generateInitialAreas(*storage.meshes[mesh_idx], move_bounds, storage);
}
const auto t_gen = std::chrono::high_resolution_clock::now();
// ### Propagate the influence areas downwards.
createLayerPathing(move_bounds);
const auto t_path = std::chrono::high_resolution_clock::now();
// ### Set a point in each influence area
createNodesFromArea(move_bounds);
const auto t_place = std::chrono::high_resolution_clock::now();
// ### draw these points as circles
drawAreas(move_bounds, storage);
const auto t_draw = std::chrono::high_resolution_clock::now();
const auto dur_pre_gen = 0.001 * std::chrono::duration_cast<std::chrono::microseconds>(t_precalc - t_start).count();
const auto dur_gen = 0.001 * std::chrono::duration_cast<std::chrono::microseconds>(t_gen - t_precalc).count();
const auto dur_path = 0.001 * std::chrono::duration_cast<std::chrono::microseconds>(t_path - t_gen).count();
const auto dur_place = 0.001 * std::chrono::duration_cast<std::chrono::microseconds>(t_place - t_path).count();
const auto dur_draw = 0.001 * std::chrono::duration_cast<std::chrono::microseconds>(t_draw - t_place).count();
const auto dur_total = 0.001 * std::chrono::duration_cast<std::chrono::microseconds>(t_draw - t_start).count();
spdlog::info(
"Total time used creating Tree support for the currently grouped meshes: {} ms. Different subtasks:\n"
"Calculating Avoidance: {} ms Creating inital influence areas: {} ms Influence area creation: {} ms Placement of Points in InfluenceAreas: {} ms Drawing result as "
"support {} ms",
dur_total,
dur_pre_gen,
dur_gen,
dur_path,
dur_place,
dur_draw);
for (auto& layer : move_bounds)
{
for (auto elem : layer)
{
delete elem->area_;
delete elem;
}
}
}
storage.support.generated = true;
}
LayerIndex TreeSupport::precalculate(const SliceDataStorage& storage, std::vector<size_t> currently_processing_meshes)
{
// Calculate top most layer that is relevant for support.
LayerIndex max_layer = -1;
for (size_t mesh_idx : currently_processing_meshes)
{
const SliceMeshStorage& mesh = *storage.meshes[mesh_idx];
const coord_t layer_height = mesh.settings.get<coord_t>("layer_height");
const coord_t z_distance_top = mesh.settings.get<coord_t>("support_top_distance");
const size_t z_distance_top_layers = round_up_divide(z_distance_top,
layer_height) + 1; // Support must always be 1 layer below overhang.
if (mesh.overhang_areas.size() <= z_distance_top_layers)
{
continue;
}
for (const auto layer_idx : ranges::views::iota(1UL, mesh.overhang_areas.size() - z_distance_top_layers) | ranges::views::reverse)
{
// Look for max relevant layer.
const Shape& overhang = mesh.overhang_areas[layer_idx + z_distance_top_layers];
if (! overhang.empty())
{
if (layer_idx > max_layer) // iterates over multiple meshes
{
max_layer = 1 + layer_idx; // plus one to avoid problems if something is of by one
}
break;
}
}
}
// ### The actual precalculation happens in TreeModelVolumes.
if (max_layer >= 0)
{
volumes_.precalculate(max_layer);
}
return max_layer;
}
void TreeSupport::generateInitialAreas(const SliceMeshStorage& mesh, std::vector<std::set<TreeSupportElement*>>& move_bounds, SliceDataStorage& storage)
{
TreeSupportTipGenerator tip_gen(mesh, volumes_);
tip_gen.generateTips(storage, mesh, move_bounds, additional_required_support_area, fake_roof_areas);
}
void TreeSupport::mergeHelper(
std::map<TreeSupportElement, AABB>& reduced_aabb,
std::map<TreeSupportElement, AABB>& input_aabb,
const PropertyAreasUnordered& to_bp_areas,
const PropertyAreas& to_model_areas,
const PropertyAreas& influence_areas,
PropertyAreasUnordered& insert_bp_areas,
PropertyAreasUnordered& insert_model_areas,
PropertyAreasUnordered& insert_influence,
std::vector<TreeSupportElement>& erase,
const LayerIndex layer_idx)
{
const bool first_merge_iteration = reduced_aabb.empty(); // If this is the first iteration, all elements in input have to be merged with each other
const std::function<coord_t(size_t, double)> getRadiusFunction = [&](const size_t distance_to_top, const double buildplate_radius_increases)
{
return config.getRadius(distance_to_top, buildplate_radius_increases);
};
for (auto& influence : input_aabb)
{
bool merged = false;
AABB influence_aabb = influence.second;
for (const auto& reduced_check : reduced_aabb)
{
// As every area has to be checked for overlaps with other areas, some fast heuristic is needed to abort early if clearly possible
// This is so performance critical that using a map lookup instead of the direct access of the cached AABBs can have a surprisingly large performance impact
AABB aabb = reduced_check.second;
if (aabb.hit(influence_aabb))
{
if (! first_merge_iteration && input_aabb.count(reduced_check.first))
{
break; // Do not try to merge elements that already should have been merged. Done for potential performance improvement.
}
const bool merging_gracious_and_non_gracious = reduced_check.first.to_model_gracious_ != influence.first.to_model_gracious_;
// ^^^ We do not want to merge a gracious with a non gracious area as bad placement could negatively impact the dependability of the whole subtree.
const bool merging_to_bp = reduced_check.first.to_buildplate_ && influence.first.to_buildplate_;
const bool merging_min_and_regular_xy = reduced_check.first.use_min_xy_dist_ != influence.first.use_min_xy_dist_;
// ^^^ Could cause some issues with the increase of one area, as it is assumed that if the smaller is increased by the delta to the larger it is engulfed by it
// already.
// But because a different collision may be removed from the in drawArea generated circles, this assumption could be wrong.
const bool merging_different_range_limits = reduced_check.first.influence_area_limit_active_ && influence.first.influence_area_limit_active_
&& influence.first.influence_area_limit_range_ != reduced_check.first.influence_area_limit_range_;
coord_t increased_to_model_radius = 0;
size_t larger_to_model_dtt = 0;
if (! merging_to_bp)
{
const coord_t infl_radius = config.getRadius(influence.first); // Get the real radius increase as the user does not care for the collision model.
const coord_t redu_radius = config.getRadius(reduced_check.first);
if (reduced_check.first.to_buildplate_ != influence.first.to_buildplate_)
{
if (reduced_check.first.to_buildplate_)
{
if (infl_radius < redu_radius)
{
increased_to_model_radius = influence.first.increased_to_model_radius_ + redu_radius - infl_radius;
}
}
else
{
if (infl_radius > redu_radius)
{
increased_to_model_radius = reduced_check.first.increased_to_model_radius_ + infl_radius - redu_radius;
}
}
}
larger_to_model_dtt = std::max(influence.first.distance_to_top_, reduced_check.first.distance_to_top_);
}
// If a merge could place a stable branch on unstable ground, would be increasing the radius further than allowed to when merging to model and to_bp trees or
// would merge to model before it is known they will even been drawn the merge is skipped
if (merging_min_and_regular_xy || merging_gracious_and_non_gracious || increased_to_model_radius > config.max_to_model_radius_increase
|| (! merging_to_bp && larger_to_model_dtt < config.min_dtt_to_model && ! reduced_check.first.supports_roof_ && ! influence.first.supports_roof_)
|| merging_different_range_limits)
{
continue;
}
Shape relevant_infl;
Shape relevant_redu;
if (merging_to_bp)
{
relevant_infl = to_bp_areas.count(influence.first) ? to_bp_areas.at(influence.first)
: Shape(); // influence.first is a new element => not required to check if it was changed
relevant_redu = insert_bp_areas.count(reduced_check.first) ? insert_bp_areas[reduced_check.first]
: (to_bp_areas.count(reduced_check.first) ? to_bp_areas.at(reduced_check.first) : Shape());
}
else
{
relevant_infl = to_model_areas.count(influence.first) ? to_model_areas.at(influence.first) : Shape();
relevant_redu = insert_model_areas.count(reduced_check.first) ? insert_model_areas[reduced_check.first]
: (to_model_areas.count(reduced_check.first) ? to_model_areas.at(reduced_check.first) : Shape());
}
const bool red_bigger = config.getCollisionRadius(reduced_check.first) > config.getCollisionRadius(influence.first);
std::pair<TreeSupportElement, Shape> smaller_rad
= red_bigger ? std::pair<TreeSupportElement, Shape>(influence.first, relevant_infl) : std::pair<TreeSupportElement, Shape>(reduced_check.first, relevant_redu);
std::pair<TreeSupportElement, Shape> bigger_rad
= red_bigger ? std::pair<TreeSupportElement, Shape>(reduced_check.first, relevant_redu) : std::pair<TreeSupportElement, Shape>(influence.first, relevant_infl);
const coord_t real_radius_delta = std::abs(config.getRadius(bigger_rad.first) - config.getRadius(smaller_rad.first));
const coord_t smaller_collision_radius = config.getCollisionRadius(smaller_rad.first);
// the area of the bigger radius is used to ensure correct placement regarding the relevant avoidance, so if that would change an invalid area may be created
if (! bigger_rad.first.can_use_safe_radius_ && smaller_rad.first.can_use_safe_radius_)
{
continue;
}
// The bigger radius is used to verify that the area is still valid after the increase with the delta.
// If there were a point where the big influence area could be valid with can_use_safe_radius the element would already be can_use_safe_radius.
// The smaller radius, which gets increased by delta may reach into the area where use_min_xy_dist is no longer required.
bool use_min_radius = bigger_rad.first.use_min_xy_dist_ && smaller_rad.first.use_min_xy_dist_;
// The idea is that the influence area with the smaller collision radius is increased by the radius difference.
// If this area has any intersections with the influence area of the larger collision radius,
// a branch (of the larger collision radius) placed in this intersection, has already engulfed the branch of the smaller collision radius.
// Because of this a merge may happen even if the influence areas (that represent possible center points of branches) do not intersect yet.
// Remember that collision radius <= real radius as otherwise this assumption would be false.
const Shape small_rad_increased_by_big_minus_small = TreeSupportUtils::safeOffsetInc(
smaller_rad.second,
real_radius_delta,
volumes_.getCollision(smaller_collision_radius, layer_idx - 1, use_min_radius),
2 * (config.xy_distance + smaller_collision_radius - EPSILON), // Epsilon avoids possible rounding errors
0,
0,
config.support_line_distance / 2,
&config.simplifier);
Shape intersect = small_rad_increased_by_big_minus_small.intersection(bigger_rad.second);
if (intersect.area()
> 1) // dont use empty as a line is not empty, but for this use-case it very well may be (and would be one layer down as union does not keep lines)
{
// Check if the overlap is large enough (Small ares tend to attract rounding errors in clipper). While 25 was guessed as enough, i did not have reason to change
// it.
if (intersect.offset(-FUDGE_LENGTH / 2).area() <= 1)
{
continue;
}
// Do the actual merge now that the branches are confirmed to be able to intersect.
// Calculate which point is closest to the point of the last merge (or tip center if no merge above it has happened)
// Used at the end to estimate where to best place the branch on the bottom most layer
// Could be replaced with a random point inside the new area
Point2LL new_pos = reduced_check.first.next_position_;
if (! intersect.inside(new_pos, true))
{
PolygonUtils::moveInside(intersect, new_pos);
}
if (increased_to_model_radius == 0)
{
increased_to_model_radius = std::max(reduced_check.first.increased_to_model_radius_, influence.first.increased_to_model_radius_);
}
const TreeSupportElement key(
reduced_check.first,
influence.first,
layer_idx - 1,
new_pos,
increased_to_model_radius,
getRadiusFunction,
config.diameter_scale_bp_radius,
config.branch_radius,
config.diameter_angle_scale_factor);
const auto getIntersectInfluence = [&](const PropertyAreasUnordered& insert_infl, const PropertyAreas& infl_areas)
{
const Shape infl_small = insert_infl.count(smaller_rad.first) ? insert_infl.at(smaller_rad.first)
: (infl_areas.count(smaller_rad.first) ? infl_areas.at(smaller_rad.first) : Shape());
const Shape infl_big = insert_infl.count(bigger_rad.first) ? insert_infl.at(bigger_rad.first)
: (infl_areas.count(bigger_rad.first) ? infl_areas.at(bigger_rad.first) : Shape());
const Shape small_rad_increased_by_big_minus_small_infl = TreeSupportUtils::safeOffsetInc(
infl_small,
real_radius_delta,
volumes_.getCollision(smaller_collision_radius, layer_idx - 1, use_min_radius),
2 * (config.xy_distance + smaller_collision_radius - EPSILON),
0,
0,
config.support_line_distance / 2,
&config.simplifier);
return small_rad_increased_by_big_minus_small_infl.intersection(
infl_big); // If the one with the bigger radius with the lower radius removed overlaps we can merge.
};
Shape intersect_influence;
intersect_influence
= TreeSupportUtils::safeUnion(intersect, getIntersectInfluence(insert_influence, influence_areas)); // Rounding errors again. Do not ask me where or why.
Shape intersect_to_model;
if (merging_to_bp && config.support_rests_on_model)
{
intersect_to_model = getIntersectInfluence(insert_model_areas, to_model_areas);
intersect_influence = TreeSupportUtils::safeUnion(intersect_influence, intersect_to_model); // Still rounding errors.
}
// Remove the now merged elements from all buckets, as they do not exist anymore in their old form.
insert_bp_areas.erase(reduced_check.first);
insert_bp_areas.erase(influence.first);
insert_model_areas.erase(reduced_check.first);
insert_model_areas.erase(influence.first);
insert_influence.erase(reduced_check.first);
insert_influence.erase(influence.first);
(merging_to_bp ? insert_bp_areas : insert_model_areas).emplace(key, intersect);
if (merging_to_bp && config.support_rests_on_model)
{
insert_model_areas.emplace(key, intersect_to_model);
}
insert_influence.emplace(key, intersect_influence);
erase.emplace_back(reduced_check.first);
erase.emplace_back(influence.first);
const Shape merge
= intersect.unionPolygons(intersect_to_model).offset(config.getRadius(key), ClipperLib::jtRound).difference(volumes_.getCollision(0, layer_idx - 1));
// ^^^ Regular union should be preferable here as Polygons tend to only become smaller through rounding errors (smaller!=has smaller area as holes have a
// negative area.).
// And if this area disappears because of rounding errors, the only downside is that it can not merge again on this layer.
reduced_aabb.erase(reduced_check.first); // This invalidates reduced_check.
reduced_aabb.emplace(key, AABB(merge));
merged = true;
break;
}
}
}
if (! merged)
{
reduced_aabb[influence.first] = influence_aabb;
}
}
}
void TreeSupport::mergeInfluenceAreas(PropertyAreasUnordered& to_bp_areas, PropertyAreas& to_model_areas, PropertyAreas& influence_areas, LayerIndex layer_idx)
{
/*
* Idea behind this is that the calculation of merges can be accelerated a bit using divide and conquer:
* If two groups of areas are already merged, only all elements in group 2 have to be merged into group one.
* This can only accelerate by factor 2 (as half the work is merging the last two groups).
* The actual merge logic is found in mergeHelper. This function only manages parallelization of different mergeHelper calls.
*/
const size_t input_size = influence_areas.size();
size_t num_threads = std::max(size_t(1), size_t(std::thread::hardware_concurrency())); // For some reason hardware concurrency can return 0;
if (input_size == 0)
{
return;
}
constexpr int min_elements_per_bucket = 2;
// max_bucket_count is input_size/min_elements_per_bucket round down to the next 2^n.
// The rounding to 2^n is to ensure improved performance, as every iteration two buckets will be merged, halving the amount of buckets.
// If halving would cause an uneven count, e.g. 3 Then bucket 0 and 1 would have to be merged, and in the next iteration the last remaining buckets. This is assumed to not be
// optimal performance-wise.
const size_t max_bucket_count = std::pow(2, std::floor(std::log(round_up_divide(input_size, min_elements_per_bucket))));
int bucket_count = std::min(max_bucket_count, num_threads); // do not use more buckets than available threads.
// To achieve that every element in a bucket is already correctly merged with other elements in this bucket
// an extra empty bucket is created for each bucket, and the elements are merged into the empty one.
// Each thread will then process two buckets by merging all elements in the second bucket into the first one as mergeHelper will disable not trying to merge elements from the
// same bucket in this case.
std::vector<PropertyAreas> buckets_area(2 * bucket_count);
std::vector<std::map<TreeSupportElement, AABB>> buckets_aabb(2 * bucket_count);
size_t position = 0, counter = 0;
const size_t over_elements = input_size % bucket_count;
const size_t elements_per_step = input_size / bucket_count;
// Dplit the data in x parts to be able to divide and conquer.
// The first "over_elements" of buckets gets elements_per_step+1 elements
for (const auto& influence_area : influence_areas)
{
buckets_area[position * 2 + 1].emplace(influence_area.first, influence_area.second);
// ^^^ Only use every second bucket beginning with 1 as this makes the parallel call later easier as we assume everything in a bucket i%2==0 is already processed.
counter++;
if ((counter == elements_per_step && position >= over_elements) || counter > elements_per_step)
{
position++;
counter = 0;
}
}
// Precalculate the AABBs from the influence areas.
cura::parallel_for<size_t>(
0,
buckets_area.size() / 2,
[&](size_t idx) // +=2 as in the beginning only uneven buckets will be filled
{
idx = idx * 2 + 1; // this is eqivalent to a parallel for(size_t idx=1;idx<buckets_area.size(),idx+=2)
for (const std::pair<TreeSupportElement, Shape>& input_pair : buckets_area[idx])
{
AABB outer_support_wall_aabb = AABB(input_pair.second);
outer_support_wall_aabb.expand(config.getRadius(input_pair.first));
buckets_aabb[idx].emplace(input_pair.first, outer_support_wall_aabb);
}
});
while (buckets_area.size() > 1)
{
// Some temporary storage, of elements that have to be inserted or removed from the background storage. Only one per two buckets required
std::vector<PropertyAreasUnordered> insert_main(buckets_area.size() / 2);
std::vector<PropertyAreasUnordered> insert_secondary(buckets_area.size() / 2);
std::vector<PropertyAreasUnordered> insert_influence(buckets_area.size() / 2);
std::vector<std::vector<TreeSupportElement>> erase(buckets_area.size() / 2);
cura::parallel_for<size_t>(
0,
(coord_t)buckets_area.size() / 2,
[&](size_t bucket_pair_idx)
{
bucket_pair_idx *= 2; // this is eqivalent to a parallel for(size_t idx=0;idx<buckets_area.size()-1,idx+=2)
// Merge bucket_count adjacent to each other, merging uneven bucket numbers into even buckets
mergeHelper(
buckets_aabb[bucket_pair_idx],
buckets_aabb[bucket_pair_idx + 1],
to_bp_areas,
to_model_areas,
influence_areas,
insert_main[bucket_pair_idx / 2],
insert_secondary[bucket_pair_idx / 2],
insert_influence[bucket_pair_idx / 2],
erase[bucket_pair_idx / 2],
layer_idx);
buckets_area[bucket_pair_idx + 1].clear(); // clear now irrelevant max_bucket_count, and delete them later
buckets_aabb[bucket_pair_idx + 1].clear();
});
// Note the division in the limit of the loop!
for (const coord_t i : ranges::views::iota(0UL, buckets_area.size() / 2))
{
for (TreeSupportElement& del : erase[i])
{
to_bp_areas.erase(del);
to_model_areas.erase(del);
influence_areas.erase(del);
}
for (const std::pair<TreeSupportElement, Shape>& tup : insert_main[i])
{
to_bp_areas.emplace(tup);
}
for (const std::pair<TreeSupportElement, Shape>& tup : insert_secondary[i])
{
to_model_areas.emplace(tup);
}
for (const std::pair<TreeSupportElement, Shape>& tup : insert_influence[i])
{
influence_areas.emplace(tup);
}
}
const auto position_rem = std::remove_if(
buckets_area.begin(),
buckets_area.end(),
[&](const PropertyAreas x) mutable
{
return x.empty();
});
buckets_area.erase(position_rem, buckets_area.end());
const auto position_aabb = std::remove_if(
buckets_aabb.begin(),
buckets_aabb.end(),
[&](const std::map<TreeSupportElement, AABB> x) mutable
{
return x.empty();
});
buckets_aabb.erase(position_aabb, buckets_aabb.end());
}
}
std::optional<TreeSupportElement> TreeSupport::increaseSingleArea(
AreaIncreaseSettings settings,
LayerIndex layer_idx,
TreeSupportElement* parent,
const Shape& relevant_offset,
Shape& to_bp_data,
Shape& to_model_data,
Shape& increased,
const coord_t overspeed,
const bool mergelayer)
{
TreeSupportElement current_elem(parent); // Also increases DTT by one.
Shape check_layer_data;
if (settings.increase_radius_)
{
current_elem.effective_radius_height_ += 1;
}
coord_t radius = config.getCollisionRadius(current_elem);
if (settings.move_)
{
increased = relevant_offset;
if (overspeed > 0)
{
const coord_t safe_movement_distance = (current_elem.use_min_xy_dist_ ? config.xy_min_distance : config.xy_distance)
+ (std::min(config.z_distance_top_layers, config.z_distance_bottom_layers) > 0 ? config.min_feature_size : 0);
// The difference to ensure that the result not only conforms to wall_restriction, but collision/avoidance is done later.
// The higher last_safe_step_movement_distance comes exactly from the fact that the collision will be subtracted later.
increased = TreeSupportUtils::safeOffsetInc(
increased,
overspeed,
volumes_.getWallRestriction(config.getCollisionRadius(*parent), layer_idx, parent->use_min_xy_dist_),
safe_movement_distance,
safe_movement_distance + radius,
1,
config.support_line_distance / 2,
nullptr);
}
if (settings.no_error_ && settings.move_)
{
increased = config.simplifier.polygon(increased); // as ClipperLib::jtRound has to be used for offsets this simplify is VERY important for performance.
}
}
else // if no movement is done the areas keep parent area as no move == offset(0)
{
increased = *parent->area_;
}
if ((mergelayer || current_elem.to_buildplate_) && config.support_rest_preference == RestPreference::BUILDPLATE)
{
to_bp_data = TreeSupportUtils::safeUnion(increased.difference(volumes_.getAvoidance(radius, layer_idx - 1, settings.type_, false, settings.use_min_distance_)));
if (! current_elem.to_buildplate_ && to_bp_data.area() > 1) // mostly happening in the tip, but with merges one should check every time, just to be sure.
{
current_elem.to_buildplate_ = true; // sometimes nodes that can reach the buildplate are marked as cant reach, tainting subtrees. This corrects it.
spdlog::debug("Corrected taint leading to a wrong to model value on layer {} targeting {} with radius {}", layer_idx - 1, current_elem.target_height_, radius);
}
}
if (config.support_rests_on_model)
{
if (mergelayer || current_elem.to_model_gracious_)
{
to_model_data = TreeSupportUtils::safeUnion(increased.difference(volumes_.getAvoidance(radius, layer_idx - 1, settings.type_, true, settings.use_min_distance_)));
}
if (! current_elem.to_model_gracious_)
{
if (mergelayer && to_model_data.area() >= 1)
{
current_elem.to_model_gracious_ = true;
spdlog::debug("Corrected taint leading to a wrong non gracious value on layer {} targeting {} with radius {}", layer_idx - 1, current_elem.target_height_, radius);
}
else
{
to_model_data
= TreeSupportUtils::safeUnion(increased.difference(volumes_.getAvoidance(radius, layer_idx - 1, AvoidanceType::COLLISION, true, settings.use_min_distance_)));
}
}
}
check_layer_data = current_elem.to_buildplate_ ? to_bp_data : to_model_data;
if (settings.increase_radius_ && check_layer_data.area() > 1)
{
std::function<bool(coord_t)> validWithRadius = [&](coord_t next_radius)
{
if (volumes_.ceilRadius(next_radius, settings.use_min_distance_) <= volumes_.ceilRadius(radius, settings.use_min_distance_))
{
return true;
}
Shape to_bp_data_2;
if (current_elem.to_buildplate_)
{
// Regular union as output will not be used later => this area should always be a subset of the safeUnion one.
to_bp_data_2 = increased.difference(volumes_.getAvoidance(next_radius, layer_idx - 1, settings.type_, false, settings.use_min_distance_)).unionPolygons();
}
Shape to_model_data_2;
if (config.support_rests_on_model && ! current_elem.to_buildplate_)
{
to_model_data_2 = increased
.difference(volumes_.getAvoidance(
next_radius,
layer_idx - 1,
current_elem.to_model_gracious_ ? settings.type_ : AvoidanceType::COLLISION,
true,
settings.use_min_distance_))
.unionPolygons();
}
Shape check_layer_data_2 = current_elem.to_buildplate_ ? to_bp_data_2 : to_model_data_2;
return check_layer_data_2.area() > 1;
};
coord_t ceil_radius_before = volumes_.ceilRadius(radius, settings.use_min_distance_);
// If the Collision Radius is smaller than the actual radius, check if it can catch up without violating the avoidance.
if (config.getCollisionRadius(current_elem) < config.increase_radius_until_radius && config.getCollisionRadius(current_elem) < config.getRadius(current_elem))
{
coord_t target_radius = std::min(config.getRadius(current_elem), config.increase_radius_until_radius);
coord_t current_ceil_radius = volumes_.getRadiusNextCeil(radius, settings.use_min_distance_);
while (current_ceil_radius < target_radius && validWithRadius(volumes_.getRadiusNextCeil(current_ceil_radius + 1, settings.use_min_distance_)))
{
current_ceil_radius = volumes_.getRadiusNextCeil(current_ceil_radius + 1, settings.use_min_distance_);
}
size_t resulting_eff_dtt = current_elem.effective_radius_height_;
while (resulting_eff_dtt + 1 < current_elem.distance_to_top_
&& config.getRadius(resulting_eff_dtt + 1, current_elem.buildplate_radius_increases_) <= current_ceil_radius
&& config.getRadius(resulting_eff_dtt + 1, current_elem.buildplate_radius_increases_) <= config.getRadius(current_elem))
{
resulting_eff_dtt++;
}
current_elem.effective_radius_height_ = resulting_eff_dtt;
// If catchup is not possible, it is likely that there is a hole below. Assuming the branches are in some kind of bowl, the branches should still stay away from the
// wall of the bowl if possible.
if (config.getCollisionRadius(current_elem) < config.increase_radius_until_radius && config.getCollisionRadius(current_elem) < config.getRadius(current_elem))
{
Shape new_to_bp_data;
Shape new_to_model_data;
if (current_elem.to_buildplate_)
{
new_to_bp_data = to_bp_data.difference(volumes_.getCollision(config.getRadius(current_elem), layer_idx - 1, current_elem.use_min_xy_dist_));
if (new_to_bp_data.area() > EPSILON)
{
to_bp_data = new_to_bp_data;
}
}
if (config.support_rests_on_model && (! current_elem.to_buildplate_ || mergelayer))
{
new_to_model_data = to_model_data.difference(volumes_.getCollision(config.getRadius(current_elem), layer_idx - 1, current_elem.use_min_xy_dist_));
if (new_to_model_data.area() > EPSILON)
{
to_model_data = new_to_model_data;
}
}
}
}
radius = config.getCollisionRadius(current_elem);
const coord_t foot_radius_increase = config.branch_radius * (std::max(config.diameter_scale_bp_radius - config.diameter_angle_scale_factor, 0.0));
const double planned_foot_increase = std::min(1.0, double(config.recommendedMinRadius(layer_idx - 1) - config.getRadius(current_elem)) / foot_radius_increase);
// ^^^ Is nearly all of the time 1, but sometimes an increase of 1 could cause the radius to become bigger than recommendedMinRadius, which could cause the radius to become
// bigger than precalculated.
// If the support_rest_preference is GRACEFUL, increase buildplate_radius_increases anyway. This does ONLY affect the CollisionRadius, as the regular radius only includes
// the buildplate_radius_increases when the SupportElement is to_buildplate (which it can not be when support_rest_preference is GRACEFUL). If the branch later rests on the
// buildplate the to_buildplate flag will only need to be updated to ensure that the radius is also correctly increased. Downside is that the enlargement of the
// CollisionRadius can cause branches, that could rest on the model if the radius was not increased, to instead rest on the buildplate. A better way could be changing
// avoidance to model to not include the buildplate and then calculate avoidances by combining the to model avoidance without the radius increase with the to buildplate
// avoidance with the larger radius. This would require ensuring all requests for the avoidance would have to ensure that the correct hybrid avoidance is requested (which
// would only be relevant when support_rest_preference is GRACEFUL) Also unioning areas when an avoidance is requested may also have a relevant performance impact, so there
// can be an argument made that the current workaround is preferable.
const bool increase_bp_foot
= planned_foot_increase > 0 && (current_elem.to_buildplate_ || (current_elem.to_model_gracious_ && config.support_rest_preference == RestPreference::GRACEFUL));
if (increase_bp_foot && config.getRadius(current_elem) >= config.branch_radius && config.getRadius(current_elem) >= config.increase_radius_until_radius)
{
if (validWithRadius(config.getRadius(current_elem.effective_radius_height_, current_elem.buildplate_radius_increases_ + planned_foot_increase)))
{
current_elem.buildplate_radius_increases_ += planned_foot_increase;
radius = config.getCollisionRadius(current_elem);
}
}
if (ceil_radius_before != volumes_.ceilRadius(radius, settings.use_min_distance_))
{
if (current_elem.to_buildplate_)
{
to_bp_data = TreeSupportUtils::safeUnion(increased.difference(volumes_.getAvoidance(radius, layer_idx - 1, settings.type_, false, settings.use_min_distance_)));
}
if (config.support_rests_on_model && (! current_elem.to_buildplate_ || mergelayer))
{
to_model_data = TreeSupportUtils::safeUnion(increased.difference(
volumes_.getAvoidance(radius, layer_idx - 1, current_elem.to_model_gracious_ ? settings.type_ : AvoidanceType::COLLISION, true, settings.use_min_distance_)));
}
check_layer_data = current_elem.to_buildplate_ ? to_bp_data : to_model_data;
if (check_layer_data.area() < 1)
{
spdlog::error(
"Lost area by doing catch up from {} to radius {}",
ceil_radius_before,
volumes_.ceilRadius(config.getCollisionRadius(current_elem), settings.use_min_distance_));
}
}
}
if (current_elem.influence_area_limit_active_ && ! current_elem.use_min_xy_dist_ && check_layer_data.area() > 1
&& (current_elem.to_model_gracious_ || current_elem.distance_to_top_ <= config.min_dtt_to_model))
{
const coord_t max_radius_increase = std::max(
static_cast<coord_t>((config.branch_radius - config.min_radius) / config.tip_layers),
static_cast<coord_t>(
(config.branch_radius * config.diameter_angle_scale_factor)
+ config.branch_radius * (std::max(config.diameter_scale_bp_radius - config.diameter_angle_scale_factor, 0.0))));
bool limit_range_validated = false;
// Rounding errors in a while loop can cause non-termination, so better safe than sorry. See https://github.com/Ultimaker/Cura/issues/14133 for an example.
to_bp_data = TreeSupportUtils::safeUnion(to_bp_data);
to_model_data = TreeSupportUtils::safeUnion(to_model_data);
while (! limit_range_validated)
{
if (current_elem.to_buildplate_)
{
Shape limited_to_bp = to_bp_data.intersection((current_elem.influence_area_limit_area_));
if (limited_to_bp.area() > 1)
{
to_bp_data = limited_to_bp;
to_model_data = to_model_data.intersection((current_elem.influence_area_limit_area_));
limit_range_validated = true;
}
}
else
{
Shape limited_to_model_data = to_model_data.intersection((current_elem.influence_area_limit_area_));
if (limited_to_model_data.area() > 1)
{
to_bp_data = to_bp_data.intersection((current_elem.influence_area_limit_area_));
to_model_data = limited_to_model_data;
limit_range_validated = true;
}
}
if (! limit_range_validated)
{
const coord_t reach_increase = std::max(current_elem.influence_area_limit_range_ / 4, (config.maximum_move_distance + max_radius_increase));
current_elem.influence_area_limit_range_ += reach_increase;
current_elem.RecreateInfluenceLimitArea();
}
}
}
return check_layer_data.area() > 1 ? std::optional<TreeSupportElement>(current_elem) : std::optional<TreeSupportElement>();
}
void TreeSupport::increaseAreas(
PropertyAreasUnordered& to_bp_areas,
PropertyAreas& to_model_areas,
PropertyAreas& influence_areas,
std::vector<TreeSupportElement*>& bypass_merge_areas,
const std::vector<TreeSupportElement*>& last_layer,
const LayerIndex layer_idx,
const bool mergelayer)
{
std::mutex critical_sections;
cura::parallel_for<size_t>(
0,
last_layer.size(),
[&](const size_t idx)
{
TreeSupportElement* parent = last_layer[idx];
TreeSupportElement elem(parent); // Also increases dtt.
// Abstract representation of the model outline. If an influence area would move through it, it could teleport through a wall.
const Shape wall_restriction = volumes_.getWallRestriction(config.getCollisionRadius(*parent), layer_idx, parent->use_min_xy_dist_);
Shape to_bp_data;
Shape to_model_data;
coord_t radius = config.getCollisionRadius(elem);
// When the radius increases, the outer "support wall" of the branch will have been moved farther away from the center (as this is the definition of radius).
// As it is not specified that the support_tree_angle has to be one of the center of the branch,
// it is here seen as the smaller angle of the outer wall of the branch, to the outer wall of the same branch one layer above.
// As the branch may have become larger the distance between these 2 walls is smaller than the distance of the center points.
// These extra distance is added to the movement distance possible for this layer.
coord_t extra_speed = EPSILON; // The extra speed is added to both movement distances. Also move 5 microns faster than allowed to avoid rounding errors, this may cause
// issues at VERY VERY small layer heights.
coord_t extra_slow_speed = 0; // Only added to the slow movement distance.
const coord_t ceiled_parent_radius = volumes_.ceilRadius(config.getCollisionRadius(*parent), parent->use_min_xy_dist_);
const coord_t projected_radius_increased = config.getRadius(parent->effective_radius_height_ + 1, parent->buildplate_radius_increases_);
const coord_t projected_radius_delta = projected_radius_increased - config.getCollisionRadius(*parent);
// When z distance is more than one layer up and down the Collision used to calculate the wall restriction will always include the wall (and not just the
// xy_min_distance) of the layer above and below like this (d = blocked area because of z distance):
/*
* layer z+1:dddddiiiiiioooo
* layer z+0:xxxxxdddddddddd
* layer z-1:dddddxxxxxxxxxx
* For more detailed visualisation see calculateWallRestrictions
*/
const coord_t safe_movement_distance = (elem.use_min_xy_dist_ ? config.xy_min_distance : config.xy_distance)
+ (std::min(config.z_distance_top_layers, config.z_distance_bottom_layers) > 0 ? config.min_feature_size : 0);
if (ceiled_parent_radius == volumes_.ceilRadius(projected_radius_increased, parent->use_min_xy_dist_)
|| projected_radius_increased < config.increase_radius_until_radius)
{
// If it is guaranteed possible to increase the radius, the maximum movement speed can be increased, as it is assumed that the maximum movement speed is the one of
// the slower moving wall
extra_speed += projected_radius_delta;
}
else
{
// If a guaranteed radius increase is not possible, only increase the slow speed.
// Ensure that the slow movement distance can not become larger than the fast one.
extra_slow_speed += std::min(projected_radius_delta, (config.maximum_move_distance + extra_speed) - (config.maximum_move_distance_slow + extra_slow_speed));
}
if (config.layer_start_bp_radius > layer_idx
&& config.recommendedMinRadius(layer_idx - 1) < config.getRadius(elem.effective_radius_height_ + 1, elem.buildplate_radius_increases_))
{
// Can guarantee elephant foot radius increase.
if (ceiled_parent_radius
== volumes_.ceilRadius(config.getRadius(parent->effective_radius_height_ + 1, parent->buildplate_radius_increases_ + 1), parent->use_min_xy_dist_))
{
extra_speed += config.branch_radius * config.diameter_scale_bp_radius;
}
else
{
extra_slow_speed += std::min(
coord_t(config.branch_radius * config.diameter_scale_bp_radius),
config.maximum_move_distance - (config.maximum_move_distance_slow + extra_slow_speed));
}
}
const coord_t fast_speed = config.maximum_move_distance + extra_speed;
const coord_t slow_speed = config.maximum_move_distance_slow + extra_speed + extra_slow_speed;
Shape offset_slow;
Shape offset_fast;
bool add = false;
bool bypass_merge = false;
// Aliases for better readability.
constexpr bool increase_radius = true;
constexpr bool no_error = true;
constexpr bool use_min_radius = true;
constexpr bool move = true;
// Determine in which order configurations are checked if they result in a valid influence area. Check will stop if a valid area is found
std::deque<AreaIncreaseSettings> order;
std::function<void(const AreaIncreaseSettings&, bool)> insertSetting = [&](const AreaIncreaseSettings& settings, bool back)
{
if (std::find(order.begin(), order.end(), settings) == order.end())
{
if (back)
{
order.emplace_back(settings);
}
else
{
order.emplace_front(settings);
}
}
};