Merge upstream main: cyclic print ordering, brim overlap fix, perimeter and support fixes

This commit is contained in:
Clifford Garwood
2026-07-31 01:42:48 -04:00
131 changed files with 54916 additions and 27918 deletions
+16 -1
View File
@@ -202,10 +202,25 @@ void AppConfig::set_defaults()
if (get("seq_top_layer_only").empty())
set("seq_top_layer_only", "1");
// ORCA: darken layers below the current one while scrubbing the preview (ported from preFlight)
// ORCA: darken the layers the preview layer slider is not scrubbed to
if (get("preview_dim_previous_layers").empty())
set_bool("preview_dim_previous_layers", false);
// ORCA: brightness of those dimmed layers, in percent. 0 = black, capped at 99 because
// 100 would render them unchanged, which is what disabling the option already does
if (get("preview_dim_previous_layers_brightness").empty())
set("preview_dim_previous_layers_brightness", "40");
else {
int brightness = 40;
try {
brightness = std::stoi(get("preview_dim_previous_layers_brightness"));
}
catch (...) {
brightness = 40;
}
set("preview_dim_previous_layers_brightness", std::to_string(std::max(0, std::min(brightness, 99))));
}
if (get("filaments_area_preferred_count").empty())
set("filaments_area_preferred_count", "10");
+14 -8
View File
@@ -32,15 +32,13 @@ static void append_and_translate(ExPolygons &dst, const ExPolygons &src, const P
for (; dst_idx < dst.size(); ++dst_idx)
dst[dst_idx].translate(instance_shift);
}
// BBS: generate brim area by objs
static void append_and_translate(ExPolygons& dst, const ExPolygons& src,
const PrintInstance& instance, size_t instance_idx, std::map<ObjectInstanceID, ExPolygons>& brimAreaMap) {
// Orca: Translate the brim area into print coordinates and store it per instance.
static void append_and_translate(const ExPolygons& src, const PrintInstance& instance,
size_t instance_idx, std::map<ObjectInstanceID, ExPolygons>& brimAreaMap) {
ExPolygons srcShifted = src;
Point instance_shift = instance.shift_without_plate_offset();
for (size_t src_idx = 0; src_idx < srcShifted.size(); ++src_idx)
srcShifted[src_idx].translate(instance_shift);
srcShifted = diff_ex(srcShifted, dst);
//expolygons_append(dst, temp2);
for (ExPolygon& expoly : srcShifted)
expoly.translate(instance_shift);
expolygons_append(brimAreaMap[{ instance.print_object->id(), instance_idx }], std::move(srcShifted));
}
@@ -572,7 +570,7 @@ static ExPolygons outer_inner_brim_area(const Print& print,
for (size_t instance_idx = 0; instance_idx < object->instances().size(); ++instance_idx) {
const PrintInstance& instance = object->instances()[instance_idx];
if (!brim_area_object.empty())
append_and_translate(brim_area, brim_area_object, instance, instance_idx, brimAreaMap);
append_and_translate(brim_area_object, instance, instance_idx, brimAreaMap);
append_and_translate(no_brim_area, no_brim_area_object, instance);
append_and_translate(holes, holes_object, instance);
append_and_translate(objectIslands, objectIsland, instance);
@@ -875,6 +873,14 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_
ExPolygons islands_area_ex = outer_inner_brim_area(print,
float(flow.scaled_spacing()), brimAreaMap, objPrintVec, printExtruders);
if (!print.config().combine_brims) {
ExPolygons claimed_area;
for (auto& [_, areas] : brimAreaMap) {
areas = diff_ex(areas, claimed_area);
expolygons_append(claimed_area, areas);
}
}
// BBS: Find boundingbox of the first layer
for (const ObjectID printObjID : print.print_object_ids()) {
BoundingBox bbx;
+2
View File
@@ -248,6 +248,8 @@ set(lisbslic3r_sources
GCode/Thumbnails.hpp
GCode/ToolOrdering.cpp
GCode/ToolOrdering.hpp
GCode/OrderingStrategies.cpp
GCode/OrderingStrategies.hpp
GCode/WipeTower2.cpp
GCode/WipeTower2.hpp
GCode/WipeTower.cpp
+1 -1
View File
@@ -78,7 +78,7 @@ public:
Standard_Boolean UserBreak() override { return should_stop.load(); }
void Show(const Message_ProgressScope&, const Standard_Boolean) override {
std::cout << "Progress: " << GetPosition() << "%" << std::endl;
std::cout << "Progress: " << std::fixed << std::setprecision(2) << 100.0 * GetPosition() << "%" << std::endl;
}
private:
std::atomic<bool>& should_stop;
+178 -44
View File
@@ -15,6 +15,7 @@
#include "GCode/Thumbnails.hpp"
#include "GCode/WipeTower.hpp"
#include "ShortestPath.hpp"
#include "GCode/OrderingStrategies.hpp"
#include "Print.hpp"
#include "Utils.hpp"
#include "ClipperUtils.hpp"
@@ -2841,6 +2842,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
m_role_based_fan_marker_layer.fill(-1);
m_fan_mover.release();
m_ordering_cache.clear();
m_writer.set_is_bbl_machine(is_bbl_printers);
@@ -3161,11 +3163,20 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// In non-sequential print, the printing extruders may have been modified by the extruder switches stored in Model::custom_gcode_per_print_z.
// Therefore initialize the printing extruders from there.
this->set_extruders(tool_ordering.all_extruders());
print_object_instances_ordering =
// By default, order object instances using a nearest neighbor search.
print.config().print_order == PrintOrder::Default ? chain_print_object_instances(print)
print_object_instances_ordering =
// By default, order object instances using nearest-neighbor chaining plus
// 2-opt and crossing-removal post-processing.
(print.config().print_order == PrintOrder::Default ? chain_print_object_instances(print)
// Snake: serpentine row traversal + 2-opt
: (print.config().print_order == PrintOrder::Snake ? chain_print_object_instances_snake(print)
// Best of all: run every strategy, pick the shortest total path
: (print.config().print_order == PrintOrder::BestOfStrategies ? chain_print_object_instances_best_of(print)
// Otherwise same order as the object list
: sort_object_instances_by_model_order(print);
: sort_object_instances_by_model_order(print))));
}
if (initial_extruder_id == (unsigned int)-1) {
// Nothing to print!
@@ -5658,7 +5669,9 @@ LayerResult GCode::process_layer(
//Calibration Layer-specific GCode
switch (print.calib_mode()) {
case CalibMode::Calib_PA_Tower: {
gcode += writer().set_pressure_advance(print.calib_params().start + static_cast<int>(print_z) * print.calib_params().step);
gcode += writer().set_pressure_advance(this->interpolate_value_across_layers(static_cast<float>(print.calib_params().start),
static_cast<float>(print.calib_params().end),
static_cast<float>(print.calib_params().step)));
break;
}
case CalibMode::Calib_Temp_Tower: {
@@ -5666,7 +5679,12 @@ LayerResult GCode::process_layer(
break;
}
case CalibMode::Calib_VFA_Tower: {
auto _speed = print.calib_params().start + std::floor(print_z / 5.0) * print.calib_params().step;
// Step the outer wall speed from start to end across the tower's layers. Plater::calib_VFA sizes the
// geometry so each speed step spans one visual block (a fixed number of layers), so the layer-based
// stepping stays aligned with the blocks regardless of nozzle size / layer height.
float _speed = this->interpolate_value_across_layers(static_cast<float>(print.calib_params().start),
static_cast<float>(print.calib_params().end),
static_cast<float>(print.calib_params().step));
m_calib_config.set_key_value("outer_wall_speed", new ConfigOptionFloatsNullable({std::round(_speed)}));
break;
}
@@ -6127,41 +6145,128 @@ LayerResult GCode::process_layer(
if (m_farthest_point_timelapse.enabled)
compute_farthest_point(layers, most_used_extruder, support_filaments);
std::map<unsigned int, std::vector<InstanceToPrint>> filament_to_print_instances;
// Per filament: instances to print, and the visit sequence over them. Island-level ordering
// may visit an instance more than once per layer; otherwise one visit per instance.
std::map<unsigned int, std::pair<std::vector<InstanceToPrint>, std::vector<InstanceVisit>>> filament_to_print_instances;
{
// Order individual islands rather than whole instances. Off for by-object sequencing,
// sequential printing, and the explicit AsObjectList order, which tour whole instances.
const bool island_level_ordering = print.config().print_sequence != PrintSequence::ByObject &&
single_object_instance_idx == size_t(-1) &&
print.config().print_order != PrintOrder::AsObjectList;
for (unsigned int filament_id : layer_tools.extruders) {
auto objects_by_extruder_it = by_extruder.find(filament_id);
if (objects_by_extruder_it == by_extruder.end()) continue;
auto &filament_plan = filament_to_print_instances[filament_id];
if (!island_level_ordering) {
// One visit per instance, printing all of its islands.
filament_plan.first = sort_print_object_instances(objects_by_extruder_it->second, layers, ordering, single_object_instance_idx);
filament_plan.second.reserve(filament_plan.first.size());
for (size_t i = 0; i < filament_plan.first.size(); ++i)
filament_plan.second.push_back({i, {}, true});
continue;
}
int plate_idx = print.get_plate_index();
Point wt_pos(print.config().wipe_tower_x.get_at(plate_idx), print.config().wipe_tower_y.get_at(plate_idx));
// Build the instances and one tour node per non-empty island (a single node for
// instances without chainable islands). Positions quantized to 1 mm so small
// centroid drift between layers still hits the tour cache below.
std::vector<GCode::ObjectByExtruder> &objects_by_extruder = objects_by_extruder_it->second;
std::vector<const PrintObject *> print_objects;
for (int obj_idx = 0; obj_idx < objects_by_extruder.size(); obj_idx++) {
auto &object_by_extruder = objects_by_extruder[obj_idx];
std::vector<InstanceToPrint> &instances = filament_plan.first;
std::vector<IslandOrderNode> nodes;
std::vector<size_t> node_instances;
auto quantize_to_mm = [](const Point &pt) -> Point {
const coord_t grid = coord_t(scale_(1.));
// Round to the nearest 1 mm symmetrically (integer division truncates toward
// zero, which would make the bucket straddling the origin twice as wide).
auto q = [grid](coord_t v) -> coord_t {
return ((v >= 0 ? v + grid / 2 : v - grid / 2) / grid) * grid;
};
return Point(q(pt.x()), q(pt.y()));
};
for (ObjectByExtruder &object_by_extruder : objects_by_extruder) {
if (object_by_extruder.islands.empty() && (object_by_extruder.support == nullptr || object_by_extruder.support->empty())) continue;
print_objects.push_back(print.get_object(obj_idx));
const size_t layer_id = &object_by_extruder - objects_by_extruder.data();
const PrintObject *print_object = layers[layer_id].original_object;
if (print_object == nullptr)
continue;
const Layer *obj_layer = layers[layer_id].object_layer;
std::vector<ObjectByExtruder::Island> &islands = object_by_extruder.islands;
const bool islands_chainable = obj_layer != nullptr && islands.size() == obj_layer->lslices.size() + 1;
for (size_t instance_id = 0; instance_id < print_object->instances().size(); ++instance_id) {
const size_t instance_idx = instances.size();
instances.emplace_back(object_by_extruder, layer_id, *print_object, instance_id,
print_object->instances()[instance_id].model_instance->get_labeled_id());
const Point &shift = print_object->instances()[instance_id].shift;
const size_t first_node = nodes.size();
if (islands_chainable)
for (size_t i = 0; i + 1 < islands.size(); ++i)
if (!islands[i].by_region.empty()) {
nodes.push_back({print_object->id(), instance_id, i,
quantize_to_mm(obj_layer->lslices[i].contour.centroid() + shift)});
node_instances.emplace_back(instance_idx);
}
if (nodes.size() == first_node) {
// No chainable islands: tour the whole instance as one stop.
nodes.push_back({print_object->id(), instance_id, size_t(-1), quantize_to_mm(shift)});
node_instances.emplace_back(instance_idx);
}
}
}
std::vector<const PrintInstance *> new_ordering = chain_print_object_instances(print_objects, &wt_pos);
std::reverse(new_ordering.begin(), new_ordering.end());
// Reuse the cached tour while this filament's island layout is unchanged.
auto &cache_entry = m_ordering_cache[filament_id];
if (!(cache_entry.first == nodes)) {
cache_entry.first = nodes;
Points node_points;
node_points.reserve(nodes.size());
for (const IslandOrderNode &node : nodes)
node_points.emplace_back(node.pos);
std::vector<size_t> tour = order_points_with_strategy(node_points, print.config().print_order, &wt_pos);
// Chained starting near the wipe tower, reversed so the layer ends near it.
std::reverse(tour.begin(), tour.end());
if (print.config().print_sequence == PrintSequence::ByObject) {
filament_to_print_instances[filament_id] = sort_print_object_instances(objects_by_extruder_it->second, layers, ordering, single_object_instance_idx);
} else {
// PrintSequence::ByLayer to use global ordering ( per object ordering ) if intra-layer order PrintOrder::AsObjectList is specified while keeping behaviour of PrintSequence::ByLayer
const std::vector<const PrintInstance*>* ordering_for_filament = (print.config().print_order == PrintOrder::AsObjectList && ordering != nullptr) ? ordering: &new_ordering;
filament_to_print_instances[filament_id] = sort_print_object_instances(objects_by_extruder_it->second, layers, ordering_for_filament, single_object_instance_idx);
// Group consecutive tour stops of the same instance into visits.
std::vector<InstanceVisit> visits;
std::vector<bool> instance_seen(instances.size(), false);
std::vector<int> last_visit_of_instance(instances.size(), -1);
for (size_t node_idx : tour) {
const size_t instance_idx = node_instances[node_idx];
if (visits.empty() || visits.back().instance_idx != instance_idx) {
visits.push_back({instance_idx, {}, !instance_seen[instance_idx]});
instance_seen[instance_idx] = true;
}
if (nodes[node_idx].island_idx != size_t(-1))
visits.back().islands.emplace_back(nodes[node_idx].island_idx);
last_visit_of_instance[instance_idx] = int(visits.size()) - 1;
}
// The trailing catch-all island has no geometry to chain by; append it to the
// instance's last visit.
for (size_t i = 0; i < instances.size(); ++i) {
if (last_visit_of_instance[i] < 0)
continue;
InstanceVisit &last_visit = visits[size_t(last_visit_of_instance[i])];
if (last_visit.islands.empty())
// A visit without explicit islands already prints everything.
continue;
std::vector<ObjectByExtruder::Island> &islands = instances[i].object_by_extruder.islands;
if (!islands.back().by_region.empty())
last_visit.islands.emplace_back(islands.size() - 1);
}
cache_entry.second = std::move(visits);
}
filament_plan.second = cache_entry.second;
}
}
std::set<size_t> layer_object_label_ids;
for (auto iter = filament_to_print_instances.begin(); iter != filament_to_print_instances.end(); ++iter) {
for (const InstanceToPrint &instance : iter->second) {
for (const InstanceToPrint &instance : iter->second.first) {
layer_object_label_ids.insert(instance.label_object_id);
}
}
@@ -6231,7 +6336,7 @@ LayerResult GCode::process_layer(
if (print.config().print_sequence == PrintSequence::ByLayer && m_enable_exclude_object && print.config().support_object_skip_flush.value) {
std::vector<size_t> filament_instances_id;
for (InstanceToPrint &instance : filament_to_print_instances[extruder_id]) filament_instances_id.emplace_back(instance.label_object_id);
for (InstanceToPrint &instance : filament_to_print_instances[extruder_id].first) filament_instances_id.emplace_back(instance.label_object_id);
m_filament_instances_code = _encode_label_ids_to_base64(filament_instances_id);
}
@@ -6312,7 +6417,9 @@ LayerResult GCode::process_layer(
if (layer_tools.has_wipe_tower && m_wipe_tower)
m_last_processor_extrusion_role = erWipeTower;
std::vector<InstanceToPrint> &instances_to_print = filament_to_print_instances[extruder_id];
auto &filament_plan = filament_to_print_instances[extruder_id];
std::vector<InstanceToPrint> &instances_to_print = filament_plan.first;
const std::vector<InstanceVisit> &instance_visits = filament_plan.second;
// We are almost ready to print. However, we must go through all the objects twice to print the overridden extrusions first (infill/perimeter wiping feature):
std::vector<ObjectByExtruder::Island::Region> by_region_per_copy_cache;
@@ -6320,10 +6427,11 @@ LayerResult GCode::process_layer(
if (is_anything_overridden && print_wipe_extrusions == 0)
gcode+="; PURGING FINISHED\n";
for (InstanceToPrint &instance_to_print : instances_to_print) {
for (const InstanceVisit &visit : instance_visits) {
InstanceToPrint &instance_to_print = instances_to_print[visit.instance_idx];
const auto& inst = instance_to_print.print_object.instances()[instance_to_print.instance_id];
const LayerToPrint &layer_to_print = layers[instance_to_print.layer_id];
if (print_wipe_extrusions == (is_anything_overridden ? 1 : 0)) {
if (visit.first_visit && print_wipe_extrusions == (is_anything_overridden ? 1 : 0)) {
gcode += generate_object_skirt_group(print, instance_to_print.print_object, instance_to_print.instance_id, layer_tools, layer, extruder_id);
gcode += generate_object_brim(print, instance_to_print.print_object, instance_to_print.instance_id, first_layer);
}
@@ -6376,7 +6484,7 @@ LayerResult GCode::process_layer(
m_avoid_crossing_perimeters.use_external_mp_once();
m_last_obj_copy = this_object_copy;
this->set_origin(unscale(offset));
if (instance_to_print.object_by_extruder.support != nullptr) {
if (visit.first_visit && instance_to_print.object_by_extruder.support != nullptr) {
m_layer = layers[instance_to_print.layer_id].support_layer;
m_object_layer_over_raft = false;
@@ -6410,9 +6518,42 @@ LayerResult GCode::process_layer(
m_layer = layer_to_print.layer();
m_object_layer_over_raft = object_layer_over_raft;
}
//FIXME order islands?
// Sequential tool path ordering of multiple parts within the same object, aka. perimeter tracking (#5511)
for (ObjectByExtruder::Island &island : instance_to_print.object_by_extruder.islands) {
// Island print order. Use the islands the tour assigned to this visit; if none,
// chain all islands nearest-neighbor from the current nozzle position (last_pos(),
// in this instance's frame after set_origin() above). Empty islands are skipped;
// the trailing catch-all island has no centroid to chain by and always goes last.
std::vector<ObjectByExtruder::Island> &islands = instance_to_print.object_by_extruder.islands;
std::vector<size_t> island_order = visit.islands;
if (island_order.empty()) {
island_order.reserve(islands.size());
if (layer_to_print.object_layer != nullptr && islands.size() == layer_to_print.object_layer->lslices.size() + 1) {
for (size_t i = 0; i + 1 < islands.size(); ++i)
if (!islands[i].by_region.empty())
island_order.emplace_back(i);
if (island_order.size() > 1) {
Points island_centroids;
island_centroids.reserve(island_order.size());
for (size_t i : island_order)
island_centroids.emplace_back(layer_to_print.object_layer->lslices[i].contour.centroid());
const Point start_near = this->last_pos();
std::vector<size_t> chain = chain_points(island_centroids, this->last_pos_defined() ? &start_near : nullptr);
std::vector<size_t> ordered;
ordered.reserve(island_order.size());
for (size_t k : chain)
ordered.emplace_back(island_order[k]);
island_order = std::move(ordered);
}
if (!islands.back().by_region.empty())
island_order.emplace_back(islands.size() - 1);
} else {
// Unexpected islands layout, keep the stored order.
for (size_t i = 0; i < islands.size(); ++i)
island_order.emplace_back(i);
}
}
for (size_t island_idx : island_order) {
ObjectByExtruder::Island &island = islands[island_idx];
const auto& by_region_specific = is_anything_overridden ? island.by_region_per_copy(by_region_per_copy_cache, static_cast<unsigned int>(instance_to_print.instance_id), extruder_id, print_wipe_extrusions != 0) : island.by_region;
// When starting a new object, use the external motion planner for the first travel move.
const Point& offset = instance_to_print.print_object.instances()[instance_to_print.instance_id].shift;
@@ -8377,29 +8518,22 @@ std::string GCode::extrusion_role_to_string_for_parser(const ExtrusionRole & rol
}
// Calculate the interpolated value for the current layer between start_value and end_value.
// Step will create equal layers steps from first to last value.
// Step > 0 splits the range into equal-width bands from first to last value (both inclusive).
// Step = 0 means gradual interpolation finishing at last value.
float GCode::interpolate_value_across_layers(float start_value, float end_value, float step) const
{
if (m_layer_index <= 1) {
return start_value;
}
else {
bool use_steps = step > 0.f;
if (use_steps) {
if (start_value > end_value) {
start_value += step;
} else {
end_value += step;
}
}
float ratio = m_layer_index / (m_layer_count - 1.f);
float value = start_value + ratio * (end_value - start_value);
if (use_steps) {
value = trunc(value / step) * step;
}
return value;
const float ratio = m_layer_index / (m_layer_count - 1.f);
if (step > 0.f) {
// Discrete equal-width bands. band is clamped to the last band so the result can't overshoot the range:
// at the top layer ratio * n_bands == n_bands, which would otherwise index one band past the end.
const int n_bands = std::lround(std::abs(end_value - start_value) / step) + 1;
const int band = std::min(n_bands - 1, static_cast<int>(ratio * n_bands));
return start_value + (end_value >= start_value ? 1.f : -1.f) * band * step;
}
return start_value + ratio * (end_value - start_value);
}
std::string encodeBase64(uint64_t value)
+34
View File
@@ -548,6 +548,40 @@ private:
// Cache for custom seam enforcers/blockers for each layer.
SeamPlacer m_seam_placer;
// One stop of the island-level tour: consecutive islands of a single instance. An instance
// can have several visits per layer when its islands are toured non-consecutively.
struct InstanceVisit
{
// Index into the per-filament InstanceToPrint vector.
size_t instance_idx;
// Islands to print, in order (indices into ObjectByExtruder::islands). Empty: print all
// islands, ordered at extrusion time.
std::vector<size_t> islands;
// First visit of this instance this layer; skirt, brim and support are emitted here.
bool first_visit;
};
// One node of the island-level tour, also used as cache key: identity plus quantized position.
struct IslandOrderNode
{
ObjectID object_id;
size_t instance_id;
// Index into ObjectByExtruder::islands, or size_t(-1) for an instance without chainable
// islands (e.g. support only), which is toured as a single stop.
size_t island_idx;
// Island centroid in G-code coordinates, quantized to 1 mm for cache stability.
Point pos;
bool operator==(const IslandOrderNode &rhs) const {
return object_id == rhs.object_id && instance_id == rhs.instance_id &&
island_idx == rhs.island_idx && pos == rhs.pos;
}
};
// Cache the per-filament island tour to avoid recomputing while the layer's island layout is
// unchanged. Key: filament_id. Value: {nodes the tour was computed from, resulting visits}.
std::map<unsigned int, std::pair<std::vector<IslandOrderNode>, std::vector<InstanceVisit>>>
m_ordering_cache;
ExtrusionQualityEstimator m_extrusion_quality_estimator;
+435
View File
@@ -0,0 +1,435 @@
// Print-object ordering strategies: implementation.
// Consolidates TSP post-processing, Snake, and Best-of-Strategies.
#include "OrderingStrategies.hpp"
#include "../Geometry.hpp"
#include "../ShortestPath.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <numeric>
#include <unordered_map>
#include <utility>
#include <vector>
namespace Slic3r {
/* ====================================================================
* TSP post-processing utilities
* ==================================================================== */
bool tsp_2opt_improve(std::vector<size_t>& path, const Points& centers, int max_passes)
{
size_t pn = path.size();
if (pn <= 2) return false;
// Pre-compute edge lengths once per pass to avoid redundant norm() calls.
auto recompute_edges = [&]() {
std::vector<double> el(pn);
for (size_t i = 0; i < pn; ++i) {
size_t ni = (i + 1) % pn;
el[i] = (centers[path[i]].cast<double>() - centers[path[ni]].cast<double>()).norm();
}
return el;
};
std::vector<double> el = recompute_edges();
// Pre-compute squared edge lengths for early rejection in the inner loop.
auto recompute_edges_sq = [&]() {
std::vector<double> elsq(pn);
for (size_t i = 0; i < pn; ++i) {
size_t ni = (i + 1) % pn;
elsq[i] = (centers[path[i]].cast<double>() - centers[path[ni]].cast<double>()).squaredNorm();
}
return elsq;
};
std::vector<double> elsq = recompute_edges_sq();
bool improved = false;
for (int pass = 0; max_passes <= 0 || pass < max_passes; ++pass) {
size_t best_i = pn, best_j = pn;
double best_gain = 0;
for (size_t i = 0; i < pn; ++i) {
const Vec2d& pi = centers[path[i]].cast<double>();
const Vec2d& p_in = centers[path[(i + 1) % pn]].cast<double>();
double d_i = el[i];
double d_i_sq = elsq[i];
for (size_t j = i + 2; j < pn; ++j) {
size_t j_next = (j + 1) % pn;
// Skip the swap that would reverse the entire cycle (removes both
// edges (0,1) and (pn-1,0), equivalent to traversing the cycle backwards).
if (i == 0 && j_next == 0) continue;
const Vec2d& pj = centers[path[j]].cast<double>();
const Vec2d& p_jn = centers[path[j_next]].cast<double>();
double d_j = el[j];
// Early rejection using squared distances (avoids 2 sqrt calls).
double new_a_sq = (pj - pi).squaredNorm();
double new_b_sq = (p_jn - p_in).squaredNorm();
if (new_a_sq >= d_i_sq && new_b_sq >= elsq[j]) continue;
double new_a = std::sqrt(new_a_sq);
double new_b = std::sqrt(new_b_sq);
double gain = d_i + d_j - new_a - new_b;
if (gain > best_gain) {
best_gain = gain;
best_i = i; best_j = j;
}
}
}
if (best_i == pn) break;
improved = true;
// Reverse the best swap segment
std::reverse(path.begin() + best_i + 1, path.begin() + best_j + 1);
// Recompute edge lengths after reversal
el = recompute_edges();
elsq = recompute_edges_sq();
}
return improved;
}
// Fast bounding-box overlap test (rejects most non-intersecting pairs).
static inline bool bboxes_overlap(const Point& a, const Point& b, const Point& c, const Point& d)
{
return !(std::max(a.x(), b.x()) < std::min(c.x(), d.x()) ||
std::max(c.x(), d.x()) < std::min(a.x(), b.x()) ||
std::max(a.y(), b.y()) < std::min(c.y(), d.y()) ||
std::max(c.y(), d.y()) < std::min(a.y(), b.y()));
}
bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers)
{
size_t pn = path.size();
if (pn <= 3) return false;
// Treat path as a cycle: include the closing edge (pn-1 -> 0), consistent with the other
// TSP helpers (2-opt, closing-edge rotation) that operate on the full cycle.
size_t n_edges = pn;
// Scan for first crossing; returns {i, j} or {npos, npos} if none.
auto find_crossing = [&]() -> std::pair<size_t, size_t> {
for (size_t i = 0; i < n_edges; ++i) {
const Point& ai = centers[path[i]];
const Point& bi = centers[path[(i + 1) % pn]];
for (size_t j = i + 2; j < n_edges; ++j) {
// Skip the (0, pn-1) pair: edges (0,1) and (pn-1,0) share node 0.
if (i == 0 && j == pn - 1) continue;
const Point& aj = centers[path[j]];
const Point& bj = centers[path[(j + 1) % pn]];
if (!bboxes_overlap(ai, bi, aj, bj)) continue;
if (Geometry::segments_intersect(ai, bi, aj, bj))
return {i, j};
}
}
return {std::numeric_limits<size_t>::max(), std::numeric_limits<size_t>::max()};
};
// Process crossings one at a time: find first, reverse it, restart scan.
// Cap iterations to prevent infinite loops on collinear/overlapping segments.
int max_iters = static_cast<int>(pn * pn);
bool improved = false;
while (max_iters-- > 0) {
auto [ci, cj] = find_crossing();
if (ci == std::numeric_limits<size_t>::max()) break;
improved = true;
std::reverse(path.begin() + ci + 1, path.begin() + cj + 1);
}
return improved;
}
void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers)
{
size_t pn = path.size();
size_t best_start = 0;
double best_closing2 = std::numeric_limits<double>::max();
for (size_t start = 0; start < pn; ++start) {
size_t last = (start + pn - 1) % pn;
double d2 = (centers[path[start]].cast<double>() - centers[path[last]].cast<double>()).squaredNorm();
if (d2 < best_closing2) { best_closing2 = d2; best_start = start; }
}
std::rotate(path.begin(), path.begin() + best_start, path.end());
}
/* ====================================================================
* Snake ordering
* ==================================================================== */
struct SnakeRow { double avg_y; std::vector<size_t> indices; };
// --- Row threshold computation ---
// Extract unique Y values and use the median gap between them to determine
// the row threshold.
static double compute_row_threshold(const std::vector<double>& sorted_ys,
double y_min, double y_max,
size_t n,
double fraction_of_y_range,
double min_threshold_um)
{
constexpr double MIN_GAP_FILTER = 1.0; // ignore sub-micron gaps (coord_t = 1/100mm)
// Extract unique Y values
std::vector<double> unique_ys;
unique_ys.reserve(sorted_ys.size());
unique_ys.push_back(sorted_ys[0]);
for (size_t i = 1; i < sorted_ys.size(); ++i) {
if (sorted_ys[i] - sorted_ys[i - 1] > MIN_GAP_FILTER)
unique_ys.push_back(sorted_ys[i]);
}
double fallback_threshold = (y_max - y_min) * fraction_of_y_range;
if (unique_ys.size() <= 1) {
return std::max(fallback_threshold, min_threshold_um);
}
// Compute gaps between consecutive unique Y values
std::vector<double> gaps;
gaps.reserve(unique_ys.size() - 1);
for (size_t i = 1; i < unique_ys.size(); ++i)
gaps.push_back(unique_ys[i] - unique_ys[i - 1]);
if (gaps.empty()) {
return std::max(fallback_threshold, min_threshold_um);
}
// Sort gaps to find the median
std::sort(gaps.begin(), gaps.end());
double median_gap = gaps[gaps.size() / 2];
double min_gap = gaps.front();
// Threshold: half the gap between consecutive unique Y values.
double threshold = (median_gap < min_gap * 1.5) ? min_gap * 0.5 : median_gap * 0.5;
bool has_row_structure;
if (unique_ys.size() * 2 <= n) {
has_row_structure = true;
} else {
// Single-column or sparse: uniform gaps indicate a deliberate grid
double max_gap = *std::max_element(gaps.begin(), gaps.end());
has_row_structure = (max_gap < min_gap * 2.0);
}
if (has_row_structure) {
// For grid-like data, use the gap-based threshold directly.
return threshold;
}
return std::max(fallback_threshold, min_threshold_um);
}
// --- Row grouping ---
// Bin points into rows by quantising Y / threshold
static std::vector<SnakeRow> group_into_rows(const Points& centers, double row_threshold)
{
size_t n = centers.size();
std::unordered_map<int64_t, std::vector<size_t>> row_map;
for (size_t i = 0; i < n; ++i) {
int64_t y_key = static_cast<int64_t>(std::floor(static_cast<double>(centers[i].y()) / row_threshold));
row_map[y_key].push_back(i);
}
std::vector<SnakeRow> rows;
rows.reserve(row_map.size());
for (auto& [key, indices] : row_map) {
double avg_y = std::accumulate(indices.begin(), indices.end(), 0.0,
[&](double acc, size_t idx) { return acc + static_cast<double>(centers[idx].y()); })
/ indices.size();
rows.push_back({avg_y, std::move(indices)});
}
std::sort(rows.begin(), rows.end(),
[](const SnakeRow& a, const SnakeRow& b) { return a.avg_y < b.avg_y; });
return rows;
}
// Sort each row by X and greedily pick the direction (left->right or right->left)
// that minimises the transition distance from the previous row's endpoint.
static std::vector<size_t> build_serpentine_path(const Points& centers,
std::vector<SnakeRow>& rows)
{
std::vector<size_t> path;
path.reserve(centers.size());
for (size_t ri = 0; ri < rows.size(); ++ri) {
auto& row = rows[ri].indices;
std::sort(row.begin(), row.end(),
[&](size_t a, size_t b) { return centers[a].x() < centers[b].x(); });
if (ri == 0) {
path.insert(path.end(), row.begin(), row.end());
} else {
const Point& prev_end = centers[path.back()];
double dist_to_left = (prev_end.cast<double>() - centers[row.front()].cast<double>()).squaredNorm();
double dist_to_right = (prev_end.cast<double>() - centers[row.back()].cast<double>()).squaredNorm();
if (dist_to_left <= dist_to_right)
path.insert(path.end(), row.begin(), row.end());
else
path.insert(path.end(), row.rbegin(), row.rend());
}
}
return path;
}
// Row-based serpentine traversal: detect rows, bin points, snake through them.
static std::vector<size_t> row_serpentine_path(const Points& centers,
double fraction_of_y_range = 0.02,
double min_threshold_um = 1e4)
{
if (centers.empty()) return {};
size_t n = centers.size();
// Collect and sort Y coordinates.
std::vector<double> sorted_ys;
sorted_ys.reserve(n);
for (const auto& p : centers) sorted_ys.push_back(static_cast<double>(p.y()));
std::sort(sorted_ys.begin(), sorted_ys.end());
auto [ymin, ymax] = std::minmax_element(sorted_ys.begin(), sorted_ys.end());
double y_min = *ymin, y_max = *ymax;
double row_threshold = compute_row_threshold(sorted_ys, y_min, y_max, n,
fraction_of_y_range, min_threshold_um);
auto rows = group_into_rows(centers, row_threshold);
return build_serpentine_path(centers, rows);
}
std::vector<size_t> snake_core(const Points& centers)
{
if (centers.empty()) return {};
std::vector<size_t> path = row_serpentine_path(centers);
for (int iter = 0; iter < 3; ++iter) {
bool improved = tsp_2opt_improve(path, centers);
improved |= tsp_remove_crossings(path, centers);
if (!improved) break;
}
return path;
}
std::vector<const PrintInstance*> chain_print_object_instances_snake(const std::vector<const PrintObject*>& print_objects, const Point* start_near)
{
return chain_instances_with_core(print_objects, start_near, snake_core);
}
std::vector<const PrintInstance*> chain_print_object_instances_snake(const Print& print)
{
return chain_print_object_instances_snake(print.objects().vector(), nullptr);
}
/* ====================================================================
* Best-of-strategies meta-strategy
* ==================================================================== */
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const std::vector<const PrintObject*>& print_objects, const Point* start_near)
{
if (print_objects.empty())
return {};
// Run all strategies.
std::vector<std::vector<const PrintInstance*>> candidates;
candidates.push_back(chain_print_object_instances(print_objects, start_near));
candidates.push_back(chain_print_object_instances_snake(print_objects, start_near));
// Compute metrics for each candidate.
struct Candidate { double total_len; double max_edge; };
std::vector<Candidate> metrics;
metrics.reserve(candidates.size());
for (size_t i = 0; i < candidates.size(); ++i) {
double total = 0.0;
double mx = 0.0;
for (size_t j = 0; j < candidates[i].size(); ++j) {
size_t k = (j + 1) % candidates[i].size();
double d = (candidates[i][j]->shift.cast<double>() - candidates[i][k]->shift.cast<double>()).norm();
total += d;
if (d > mx) mx = d;
}
metrics.push_back({total, mx});
}
// Pick shortest total path; tiebreak on smallest max edge.
auto best_it = std::min_element(metrics.begin(), metrics.end(),
[](const Candidate& a, const Candidate& b) {
return a.total_len < b.total_len ||
(a.total_len == b.total_len && a.max_edge < b.max_edge);
});
size_t best = static_cast<size_t>(std::distance(metrics.begin(), best_it));
return candidates[best];
}
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const Print& print)
{
return chain_print_object_instances_best_of(print.objects().vector(), nullptr);
}
/* ====================================================================
* Island-level ordering entry point
* ==================================================================== */
std::vector<size_t> order_points_with_strategy(const Points& points, PrintOrder print_order, const Point* start_near)
{
if (points.empty())
return {};
if (print_order != PrintOrder::Snake && print_order != PrintOrder::BestOfStrategies)
// Nearest neighbor + post-processing; honours start_near natively.
return chain_points_with_postprocessing(points, start_near);
auto run_snake = [&points, start_near]() {
std::vector<size_t> path = snake_core(points);
if (start_near != nullptr && !path.empty()) {
// Start the cycle at the point closest to start_near.
size_t best_start = 0;
double best_d2 = std::numeric_limits<double>::max();
for (size_t k = 0; k < points.size(); ++k) {
double d2 = (points[k].cast<double>() - start_near->cast<double>()).squaredNorm();
if (d2 < best_d2) { best_d2 = d2; best_start = k; }
}
auto it = std::find(path.begin(), path.end(), best_start);
if (it != path.begin() && it != path.end())
std::rotate(path.begin(), it, path.end());
} else {
tsp_rotate_minimize_closing(path, points);
}
return path;
};
if (print_order == PrintOrder::Snake)
return run_snake();
// Best-of: pick the shortest total cycle; tiebreak on smallest max edge.
std::vector<std::vector<size_t>> candidates;
candidates.emplace_back(chain_points_with_postprocessing(points, start_near));
candidates.emplace_back(run_snake());
size_t best = 0;
double best_len = std::numeric_limits<double>::max();
double best_edge = std::numeric_limits<double>::max();
for (size_t i = 0; i < candidates.size(); ++i) {
double len = tsp_cycle_path_length(candidates[i], points);
double edge = tsp_max_edge_length(candidates[i], points);
if (len < best_len || (len == best_len && edge < best_edge)) {
best_len = len; best_edge = edge; best = i;
}
}
return candidates[best];
}
} // namespace Slic3r
+148
View File
@@ -0,0 +1,148 @@
// Print-object ordering strategies and shared TSP post-processing utilities.
#ifndef slic3r_OrderingStrategies_hpp_
#define slic3r_OrderingStrategies_hpp_
#include "../libslic3r.h"
#include "../Point.hpp"
#ifndef SLIC3R_TEST_HARNESS
#include "../Print.hpp"
#endif
#include <algorithm>
#include <limits>
#include <utility>
#include <vector>
namespace Slic3r {
// --- Path improvement (operate on index vectors into `centers`) ---
// 2-opt improvement: reverses segments that reduce total cycle path length.
// Returns true if any improvement was made.
bool tsp_2opt_improve(std::vector<size_t>& path, const Points& centers, int max_passes = 10);
// Crossing removal: reverse any segment pair whose edges geometrically cross.
// Returns true if any crossing was removed.
bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers);
// Rotate the cycle so the closing edge (last -> first) is minimized.
void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers);
// Total Euclidean path length of a cycle (including closing edge).
inline double tsp_cycle_path_length(const std::vector<size_t>& path, const Points& centers)
{
if (path.size() < 2) return 0.0;
double total = 0.0;
for (size_t i = 0; i < path.size(); ++i) {
size_t next = (i + 1) % path.size();
total += (centers[path[i]].cast<double>() - centers[path[next]].cast<double>()).norm();
}
return total;
}
// Maximum edge length of a cycle (including closing edge).
inline double tsp_max_edge_length(const std::vector<size_t>& path, const Points& centers)
{
if (path.size() < 2) return 0.0;
double mx = 0.0;
for (size_t i = 0; i < path.size(); ++i) {
size_t next = (i + 1) % path.size();
double d = (centers[path[i]].cast<double>() - centers[path[next]].cast<double>()).norm();
if (d > mx) mx = d;
}
return mx;
}
#ifndef SLIC3R_TEST_HARNESS
// --- Wrapper boilerplate ---
// Collect instance centers from PrintObjects, optionally pre-rotate to honour
// start_near, call a core algorithm, and map the result back to PrintInstance*.
template<typename CoreFn>
std::vector<const PrintInstance*> chain_instances_with_core(
const std::vector<const PrintObject*>& print_objects,
const Point* start_near,
CoreFn&& core_fn)
{
Points instance_centers;
std::vector<std::pair<size_t, size_t>> instances;
for (size_t i = 0; i < print_objects.size(); ++i) {
const PrintObject& object = *print_objects[i];
for (size_t j = 0; j < object.instances().size(); ++j) {
instance_centers.emplace_back(object.instances()[j].shift);
instances.emplace_back(i, j);
}
}
if (instance_centers.empty()) return {};
// If start_near is provided, pre-rotate so closest point is first.
if (start_near != nullptr) {
size_t best_start = 0;
double best_d2 = std::numeric_limits<double>::max();
for (size_t k = 0; k < instance_centers.size(); ++k) {
double d2 = (instance_centers[k].cast<double>() - start_near->cast<double>()).squaredNorm();
if (d2 < best_d2) { best_d2 = d2; best_start = k; }
}
std::rotate(instance_centers.begin(), instance_centers.begin() + best_start, instance_centers.end());
std::rotate(instances.begin(), instances.begin() + best_start, instances.end());
}
auto path = core_fn(instance_centers);
// Rotate the cycle so the first element is the best starting point.
// When start_near is provided, pick the point closest to it (preserving
// the pre-rotation). Otherwise minimise the closing edge.
if (start_near != nullptr && !path.empty()) {
// Pre-rotation already put the closest point at index 0.
// Find where index 0 appears in the path and rotate it to the front.
auto it = std::find(path.begin(), path.end(), size_t(0));
if (it != path.begin())
std::rotate(path.begin(), it, path.end());
} else {
tsp_rotate_minimize_closing(path, instance_centers);
}
std::vector<const PrintInstance*> out;
out.reserve(path.size());
for (size_t step : path) {
out.emplace_back(&print_objects[instances[step].first]->instances()[instances[step].second]);
}
return out;
}
#endif // SLIC3R_TEST_HARNESS
// --- Core algorithms (operate on raw Points, return index permutations) ---
// Snake ordering: row grouping + serpentine traversal + post-processing.
std::vector<size_t> snake_core(const Points& centers);
#ifndef SLIC3R_TEST_HARNESS
// --- Production wrappers ---
// Snake ordering.
std::vector<const PrintInstance*> chain_print_object_instances_snake(const std::vector<const PrintObject*>& print_objects, const Point* start_near);
std::vector<const PrintInstance*> chain_print_object_instances_snake(const Print& print);
// Best-of-strategies: run all strategies and return the shortest result.
// Primary: shortest total path; secondary tiebreaker: smallest max edge.
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const std::vector<const PrintObject*>& print_objects, const Point* start_near);
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const Print& print);
// Order raw points with the selected strategy, returning an index permutation. Island-level
// counterpart of the chain_print_object_instances_* helpers. The returned cycle starts at the
// point closest to start_near; orders without a dedicated strategy use nearest-neighbor chaining.
std::vector<size_t> order_points_with_strategy(const Points& points, PrintOrder print_order, const Point* start_near);
#endif // SLIC3R_TEST_HARNESS
} // namespace Slic3r
#endif /* slic3r_OrderingStrategies_hpp_ */
+266 -34
View File
@@ -360,6 +360,14 @@ static ClipperLib_Z::Paths clip_extrusion(const ClipperLib_Z::Path& subject, con
return clipped_paths;
}
static double clipper_z_path_length(const ClipperLib_Z::Path &path)
{
double len = 0.;
for (size_t i = 1; i < path.size(); ++ i)
len += (Vec2d(double(path[i].x()), double(path[i].y())) - Vec2d(double(path[i - 1].x()), double(path[i - 1].y()))).norm();
return len;
}
struct PerimeterGeneratorArachneExtrusion
{
Arachne::ExtrusionLine* extrusion = nullptr;
@@ -571,17 +579,156 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
return extrusion_coll;
}
// ORCA: only_one_wall_top detects the top as "slice − upper", so a feature rising from the middle of a
// top surface becomes an enclosed hole that gets ringed with extra inner walls. Fill those holes back
// into the top. Only holes that are both covered by the upper layer (excludes bridges) and backed by
// solid material (excludes voids) are filled.
static ExPolygons fill_enclosed_top_feature_holes(const ExPolygons &top, const Polygons &covered_by_upper, const ExPolygons &solid)
// ORCA: only_one_wall_top acts on top surfaces, so without a top shell there is nothing for it to act on: zero top
// shell layers retype the top surfaces as internal, see LayerRegion::prepare_fill_surfaces(). A 0% top surface
// density does leave a top surface - just an unfilled one - so it does not disable the feature.
// ConfigManipulation::toggle_print_fff_options() hides the option under the same condition, so a profile that left
// it enabled does not act behind a hidden checkbox.
static bool has_top_shell_layers(const PrintRegionConfig &config)
{
ExPolygons filled = top;
for (ExPolygon &ex : filled)
ex.holes.clear();
const ExPolygons feature_holes = intersection_ex(intersection_ex(diff_ex(filled, top), covered_by_upper), solid);
return feature_holes.empty() ? top : union_ex(top, feature_holes);
return config.top_shell_layers.value > 0;
}
// ORCA: only_one_wall_first_layer thins the first layer to a single wall, the bottom counterpart of the above and
// gated the same way: zero bottom shell layers retype the bottom surfaces as internal, so that wall would ring
// sparse infill on the bed. The bottom surface density plays no part - an unfilled bottom surface is still a bottom
// surface, exactly as for the top - and it cannot reach zero anyway, being capped at a 10% minimum.
static bool has_bottom_shell_layers(const PrintRegionConfig &config)
{
return config.bottom_shell_layers.value > 0;
}
// ORCA: the inner walls are only given up when a top fill takes their space, and it has to actually reach it -
// a 0% top surface density leaves no fill at all, and without top_surface_expansion the fill never grows over
// them. Either way the original generation is kept (re-onion the not-top region), which is what users of
// only_one_wall_top alone have always got.
static bool top_fill_replaces_inner_walls(const PrintRegionConfig &config)
{
return has_top_shell_layers(config) && config.top_surface_density.value > 0 && config.top_surface_expansion.value > 0;
}
// ORCA: only_one_wall_top - cheap per-vertex classification of a wall against the top surface. Only Partial
// needs the geometry clipped or measured; a segment crossing the top with no vertex inside is rare enough to ignore.
enum class TopOverlap { None, Partial, Full };
static bool point_over_top(const Point &p, const ExPolygons &top_region, const BoundingBox &top_region_bbox)
{
if (! top_region_bbox.contains(p))
return false;
for (const ExPolygon &ex : top_region)
if (ex.contains(p, false))
return true;
return false;
}
static TopOverlap classify_over_top(const Points &pts, const ExPolygons &top_region, const BoundingBox &top_region_bbox)
{
size_t inside = 0;
for (const Point &p : pts)
if (point_over_top(p, top_region, top_region_bbox))
++ inside;
return inside == 0 ? TopOverlap::None : inside == pts.size() ? TopOverlap::Full : TopOverlap::Partial;
}
static TopOverlap classify_over_top(const Arachne::ExtrusionLine &el, const ExPolygons &top_region, const BoundingBox &top_region_bbox)
{
size_t inside = 0;
for (const Arachne::ExtrusionJunction &j : el.junctions)
if (point_over_top(j.p, top_region, top_region_bbox))
++ inside;
return inside == 0 ? TopOverlap::None : inside == el.junctions.size() ? TopOverlap::Full : TopOverlap::Partial;
}
// ORCA: only_one_wall_top for Arachne - cut out of the already generated inner walls the parts running over the top
// surface, so geometry that continues upward keeps its walls. A wall too short over the top to be worth slitting open
// is left whole, its footprint reported in kept_over_top for the caller to withhold from the top fill.
static void clip_inner_walls_over_top(std::vector<Arachne::VariableWidthLines> &inner_perimeters, const ExPolygons &top_region, coord_t perimeter_width, Polygons &kept_over_top)
{
const BoundingBox top_region_bbox = get_extents(top_region).inflated(SCALED_EPSILON);
auto covered_by = [](const Arachne::ExtrusionLine &el) {
Polyline centerline;
centerline.points.reserve(el.junctions.size());
coord_t width = 0;
for (const Arachne::ExtrusionJunction &j : el.junctions) {
centerline.points.emplace_back(j.p);
width = std::max(width, j.w);
}
return offset(centerline, float(width) / 2.f);
};
// Pull the cut back by half a wall width: the clip severs the centerline, but the bead's rounded end
// extends half a width past its endpoint and would otherwise overlap the top fill.
ClipperLib_Z::Paths top_paths_z;
for (const Polygon &poly : to_polygons(offset_ex(top_region, float(perimeter_width) / 2.f))) {
top_paths_z.emplace_back();
ClipperLib_Z::Path &out = top_paths_z.back();
out.reserve(poly.points.size());
for (const Point &pt : poly.points)
out.emplace_back(pt.x(), pt.y(), 0);
}
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters) {
Arachne::VariableWidthLines kept;
kept.reserve(inner_perimeter.size());
for (Arachne::ExtrusionLine &el : inner_perimeter) {
if (el.empty())
continue;
const TopOverlap overlap = classify_over_top(el, top_region, top_region_bbox);
if (overlap == TopOverlap::None) {
kept.emplace_back(std::move(el));
continue;
}
if (overlap == TopOverlap::Full)
continue; // the clip below would return nothing anyway
ClipperLib_Z::Path subject;
subject.reserve(el.size());
for (const Arachne::ExtrusionJunction &j : el.junctions)
subject.emplace_back(j.p.x(), j.p.y(), j.w);
ClipperLib_Z::Paths pieces = clip_extrusion(subject, top_paths_z, ClipperLib_Z::ctDifference);
// Clipper treats the subject as an open polyline, so it also cuts a closed loop at its (arbitrary)
// start vertex and may reverse pieces. Stitch pieces sharing an endpoint back together.
auto same_pt = [](const ClipperLib_Z::IntPoint &p, const ClipperLib_Z::IntPoint &q) {
return std::abs(p.x() - q.x()) <= SCALED_EPSILON && std::abs(p.y() - q.y()) <= SCALED_EPSILON;
};
for (size_t i = 0; i < pieces.size(); ++ i) {
for (size_t j = i + 1; j < pieces.size();) {
ClipperLib_Z::Path &a = pieces[i];
ClipperLib_Z::Path &b = pieces[j];
if (same_pt(a.front(), b.front()) || same_pt(a.front(), b.back()))
std::reverse(a.begin(), a.end());
if (same_pt(a.back(), b.back()))
std::reverse(b.begin(), b.end());
if (same_pt(a.back(), b.front())) {
a.insert(a.end(), b.begin() + 1, b.end());
pieces.erase(pieces.begin() + j);
j = i + 1; // the merged path has new endpoints, restart the scan
} else
++ j;
}
}
// If the clip removed next to nothing, keep the loop untouched instead of slitting it open. The
// half-width pull-back above already costs about one width per crossing, hence two widths.
double kept_length = 0.;
for (const ClipperLib_Z::Path &path : pieces)
kept_length += clipper_z_path_length(path);
if (clipper_z_path_length(subject) - kept_length < 2. * double(perimeter_width)) {
append(kept_over_top, covered_by(el));
kept.emplace_back(std::move(el));
continue;
}
for (const ClipperLib_Z::Path &path : pieces) {
Arachne::ExtrusionLine clipped(el.inset_idx, el.is_odd);
clipped.junctions.reserve(path.size());
for (const ClipperLib_Z::IntPoint &pt : path)
clipped.junctions.emplace_back(Point(pt.x(), pt.y()), coord_t(pt.z()), el.inset_idx);
// Discard tiny leftovers that would print as zits.
if (clipped.size() >= 2 && clipped.getLength() >= perimeter_width)
kept.emplace_back(std::move(clipped));
}
}
inner_perimeter = std::move(kept);
}
}
void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExPolygons &top_fills,
@@ -649,7 +796,6 @@ void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExP
ExPolygons delete_bridge = diff_ex(orig_polygons, bridge_checker, ApplySafetyOffset::Yes);
ExPolygons top_polygons = diff_ex(delete_bridge, upper_polygons_series_clipped, ApplySafetyOffset::Yes);
top_polygons = fill_enclosed_top_feature_holes(top_polygons, upper_polygons_series_clipped, orig_polygons);
// get the not-top surface, from the "real top" but enlarged by external_infill_margin (and the
// min_width_top_surface we removed a bit before)
@@ -1234,6 +1380,11 @@ void PerimeterGenerator::process_classic()
for (const Surface &surface : all_surfaces)
surface_exp.push_back(surface.expolygon);
std::vector<size_t> surface_order = chain_expolygons(surface_exp);
// ORCA: neither one-wall option has a surface to act on without the shell behind it, see
// has_top_shell_layers() / has_bottom_shell_layers(). Gated here so every use below - including the
// topmost and first layers - sees the same answer.
const bool only_one_wall_top = this->config->only_one_wall_top && has_top_shell_layers(*this->config);
const bool only_one_wall_first_layer = this->config->only_one_wall_first_layer && has_bottom_shell_layers(*this->config);
for (size_t order_idx = 0; order_idx < surface_order.size(); order_idx++) {
const Surface &surface = all_surfaces[surface_order[order_idx]];
// detect how many perimeters must be generated for this island
@@ -1241,16 +1392,23 @@ void PerimeterGenerator::process_classic()
int sparse_infill_density = this->config->sparse_infill_density.value;
if (this->config->alternate_extra_wall && this->layer_id % 2 == 1 && !m_spiral_vase && sparse_infill_density > 0) // add alternating extra wall
loop_number++;
if (this->layer_id == object_config->raft_layers && this->config->only_one_wall_first_layer)
if (this->layer_id == object_config->raft_layers && only_one_wall_first_layer)
loop_number = 0;
// Set the topmost layer to be one wall
if (loop_number > 0 && config->only_one_wall_top && this->upper_slices == nullptr)
if (loop_number > 0 && only_one_wall_top && this->upper_slices == nullptr)
loop_number = 0;
ExPolygons last = union_ex(surface.expolygon.simplify_p(surface_simplify_resolution));
ExPolygons gaps;
ExPolygons top_fills;
ExPolygons fill_clip;
// ORCA: only_one_wall_top, all empty unless this island has a top surface on this layer. See the
// post-onion reduction below: the region to keep clear of inner walls, the space freed by the dropped
// walls (goes to infill, not left as a void) and the space held by the kept ones (withheld from the fill).
ExPolygons one_wall_top_region;
ExPolygons one_wall_top_reclaimed;
Polygons one_wall_top_kept_bands;
bool apply_one_wall_top = false;
if (loop_number >= 0) {
// In case no perimeters are to be generated, loop_number will equal to -1.
std::vector<PerimeterGeneratorLoops> contours(loop_number+1); // depth => loops
@@ -1389,8 +1547,19 @@ void PerimeterGenerator::process_classic()
//BBS: refer to superslicer
//store surface for top infill if only_one_wall_top
if (i == 0 && i!=loop_number && config->only_one_wall_top && !surface.is_bridge() && this->upper_slices != NULL) {
this->split_top_surfaces(last, top_fills, last, fill_clip);
if (i == 0 && i!=loop_number && only_one_wall_top && !surface.is_bridge() && this->upper_slices != NULL) {
if (top_fill_replaces_inner_walls(*this->config)) {
// ORCA: take the top fill and the keep-out region but leave `last` as the real geometry,
// so the onion follows it and the walls over the top are reduced in one step below.
ExPolygons non_top_polygons;
this->split_top_surfaces(last, top_fills, non_top_polygons, fill_clip);
apply_one_wall_top = !top_fills.empty();
if (apply_one_wall_top)
one_wall_top_region = diff_ex(last, non_top_polygons);
} else {
// Onion the not-top region only, so the remaining walls stop at the top boundary.
this->split_top_surfaces(last, top_fills, last, fill_clip);
}
}
if (i == loop_number && (! has_gap_fill || this->config->sparse_infill_density.value == 0)) {
@@ -1400,6 +1569,46 @@ void PerimeterGenerator::process_classic()
}
}
// ORCA: only_one_wall_top reduction - drop the inner walls (depth > 0) running over the top surface and
// take that space back from the gaps, leaving the top with the outer wall and the top infill. Classic
// perimeters are closed loops, so a wall can only be kept or dropped whole; one that merely grazes the
// top (same tolerance as the Arachne clip) is kept and withheld from the top fill instead.
if (apply_one_wall_top) {
const BoundingBox top_region_bbox = get_extents(one_wall_top_region).inflated(SCALED_EPSILON);
const double grazing_tolerance = 2. * double(perimeter_width);
// The band a wall covers, taken around its centerline so the orientation of holes does not matter.
auto wall_band = [perimeter_spacing](const Polygon &poly) {
Polygon centerline = poly;
centerline.make_counter_clockwise();
return diff(offset(centerline, float(perimeter_spacing) / 2.f),
offset(centerline, -float(perimeter_spacing) / 2.f));
};
Polygons dropped_wall_bands;
auto reduce_over_top = [&](PerimeterGeneratorLoops &loops) {
loops.erase(std::remove_if(loops.begin(), loops.end(), [&](const PerimeterGeneratorLoop &loop) {
const TopOverlap overlap = classify_over_top(loop.polygon.points, one_wall_top_region, top_region_bbox);
if (overlap == TopOverlap::None)
return false;
// Only a wall straddling the boundary is worth measuring; a wall wholly over the top goes.
if (overlap == TopOverlap::Partial &&
total_length(intersection_pl(Polylines{ loop.polygon.split_at_first_point() }, one_wall_top_region)) < grazing_tolerance) {
append(one_wall_top_kept_bands, wall_band(loop.polygon));
return false;
}
append(dropped_wall_bands, wall_band(loop.polygon));
return true;
}), loops.end());
};
for (int d = 1; d <= loop_number; ++ d) {
reduce_over_top(contours[d]);
reduce_over_top(holes[d]);
}
if (! gaps.empty())
gaps = diff_ex(gaps, one_wall_top_region);
if (! dropped_wall_bands.empty())
one_wall_top_reclaimed = diff_ex(dropped_wall_bands, one_wall_top_region);
}
// nest loops: holes first
for (int d = 0; d <= loop_number; ++ d) {
PerimeterGeneratorLoops &holes_d = holes[d];
@@ -1634,7 +1843,10 @@ void PerimeterGenerator::process_classic()
and use zigzag). */
//FIXME Vojtech: This grows by a rounded extrusion width, not by line spacing,
// therefore it may cover the area, but no the volume.
last = diff_ex(last, gap_fill.polygons_covered_by_width(10.f));
Polygons gap_fill_covered = gap_fill.polygons_covered_by_width(10.f);
last = diff_ex(last, gap_fill_covered);
if (! one_wall_top_reclaimed.empty())
one_wall_top_reclaimed = diff_ex(one_wall_top_reclaimed, gap_fill_covered);
this->gap_fill->append(std::move(gap_fill.entities));
}
@@ -1679,9 +1891,15 @@ void PerimeterGenerator::process_classic()
// append infill areas to fill_surfaces
//if any top_fills, grow them by ext_perimeter_spacing/2 to have the real un-anchored fill
ExPolygons top_infill_exp = intersection_ex(fill_clip, offset_ex(top_fills, double(ext_perimeter_spacing / 2)));
// ORCA: only_one_wall_top - route the top fill around the walls kept despite grazing the top.
if (!one_wall_top_kept_bands.empty())
top_infill_exp = diff_ex(top_infill_exp, one_wall_top_kept_bands);
if (!top_fills.empty()) {
infill_exp = union_ex(infill_exp, offset_ex(top_infill_exp, double(top_infill_peri_overlap)));
}
// ORCA: only_one_wall_top - what the top fill does not cover of the dropped walls goes to infill.
if (!one_wall_top_reclaimed.empty())
infill_exp = union_ex(infill_exp, one_wall_top_reclaimed);
this->fill_surfaces->append(infill_exp, stInternal);
apply_extra_perimeters(infill_exp);
@@ -1700,6 +1918,8 @@ void PerimeterGenerator::process_classic()
double(-inset - infill_peri_overlap));
if (!top_fills.empty())
polyWithoutOverlap = union_ex(polyWithoutOverlap, top_infill_exp);
if (!one_wall_top_reclaimed.empty())
polyWithoutOverlap = union_ex(polyWithoutOverlap, one_wall_top_reclaimed);
this->fill_no_overlap->insert(this->fill_no_overlap->end(), polyWithoutOverlap.begin(), polyWithoutOverlap.end());
}
@@ -2138,6 +2358,11 @@ void PerimeterGenerator::process_arachne()
process_no_bridge(all_surfaces, perimeter_spacing, ext_perimeter_width);
// BBS: don't simplify too much which influence arc fitting when export gcode if arc_fitting is enabled
double surface_simplify_resolution = (print_config->enable_arc_fitting && !this->has_fuzzy_skin) ? 0.2 * m_scaled_resolution : m_scaled_resolution;
// ORCA: neither one-wall option has a surface to act on without the shell behind it, see
// has_top_shell_layers() / has_bottom_shell_layers(). Gated here so every use below - including the
// topmost and first layers - sees the same answer.
const bool only_one_wall_top = this->config->only_one_wall_top && has_top_shell_layers(*this->config);
const bool only_one_wall_first_layer = this->config->only_one_wall_first_layer && has_bottom_shell_layers(*this->config);
// we need to process each island separately because we might have different
// extra perimeters for each one
for (const Surface& surface : all_surfaces) {
@@ -2150,12 +2375,12 @@ void PerimeterGenerator::process_arachne()
// Set the bottommost layer to be one wall
const bool is_bottom_layer = (this->layer_id == object_config->raft_layers) ? true : false;
if (is_bottom_layer && this->config->only_one_wall_first_layer)
if (is_bottom_layer && only_one_wall_first_layer)
loop_number = 0;
// Orca: set the topmost layer to be one wall according to the config
const bool is_topmost_layer = (this->upper_slices == nullptr) ? true : false;
if (is_topmost_layer && loop_number > 0 && config->only_one_wall_top)
if (is_topmost_layer && loop_number > 0 && only_one_wall_top)
loop_number = 0;
auto apply_precise_outer_wall = config->precise_outer_wall && config->wall_sequence == WallSequence::InnerOuter;
@@ -2175,10 +2400,10 @@ void PerimeterGenerator::process_arachne()
//PS: One wall top surface for Arachne
ExPolygons top_expolygons;
// Calculate how many inner loops remain when TopSurfaces is selected.
const int inner_loop_number = (config->only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1;
const int inner_loop_number = (only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1;
// Set one perimeter when TopSurfaces is selected.
if (config->only_one_wall_top && loop_number > 0)
if (only_one_wall_top && loop_number > 0)
loop_number = 0;
Arachne::WallToolPathsParams input_params_tmp = input_params;
@@ -2209,7 +2434,6 @@ void PerimeterGenerator::process_arachne()
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, infill_contour_bbox);
top_expolygons = diff_ex(infill_contour, upper_slices_clipped);
top_expolygons = fill_enclosed_top_feature_holes(top_expolygons, upper_slices_clipped, infill_contour);
if (!top_expolygons.empty()) {
if (lower_slices != nullptr) {
@@ -2230,25 +2454,33 @@ void PerimeterGenerator::process_arachne()
// due to thin lines being generated
top_expolygons = offset2_ex(top_expolygons, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85));
// Get the not-top ExPolygons (including bridges) from current slices and expanded real top ExPolygons (without bridges).
const ExPolygons not_top_expolygons = diff_ex(infill_contour, top_expolygons);
// Get final top ExPolygons.
// Get final top ExPolygons (bridges were excluded above, so they stay walled).
top_expolygons = intersection_ex(top_expolygons, infill_contour);
const Polygons not_top_polygons = to_polygons(offset_ex(not_top_expolygons,wall_0_inset));
Arachne::WallToolPaths inner_wall_tool_paths(not_top_polygons, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp);
// ORCA: onion the real region (inside the outer wall) so the remaining walls follow the actual
// geometry, then cut away the parts over the top surface. Re-onioning the non-top complement
// instead - the fallback when there is no top fill - walls the top/non-top interface and rings
// top-surface islands with inner walls that don't exist when the feature is disabled.
const bool clip_walls_over_top = top_fill_replaces_inner_walls(*this->config);
const Polygons inner_region = to_polygons(offset_ex(clip_walls_over_top ? infill_contour
: diff_ex(infill_contour, top_expolygons),
wall_0_inset));
Arachne::WallToolPaths inner_wall_tool_paths(inner_region, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp);
std::vector<Arachne::VariableWidthLines> inner_perimeters = inner_wall_tool_paths.getToolPaths();
// Recalculate indexes of inner perimeters before merging them.
if (!perimeters.empty()) {
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters) {
if (inner_perimeter.empty())
continue;
if (clip_walls_over_top) {
Polygons kept_over_top;
clip_inner_walls_over_top(inner_perimeters, top_expolygons, perimeter_width, kept_over_top);
// Route the top fill around the walls kept despite grazing the top.
if (! kept_over_top.empty())
top_expolygons = diff_ex(top_expolygons, kept_over_top);
}
// Recalculate indexes of inner perimeters before merging them: they come after the single outer wall.
if (!perimeters.empty())
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters)
for (Arachne::ExtrusionLine &el : inner_perimeter)
++el.inset_idx;
}
}
perimeters.insert(perimeters.end(), inner_perimeters.begin(), inner_perimeters.end());
infill_contour = union_ex(top_expolygons, inner_wall_tool_paths.getInnerContour());
+22 -1
View File
@@ -332,6 +332,8 @@ CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrintSequence)
static t_config_enum_values s_keys_map_PrintOrder{
{ "default", int(PrintOrder::Default) },
{ "as_obj_list", int(PrintOrder::AsObjectList)},
{ "best_of", int(PrintOrder::BestOfStrategies)},
{ "snake", int(PrintOrder::Snake)},
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrintOrder)
@@ -2018,12 +2020,30 @@ void PrintConfigDef::init_fff_params()
def = this->add("print_order", coEnum);
def->label = L("Intra-layer order");
def->tooltip = L("Print order within a single layer.");
def->tooltip = L("Order in which object instances are visited within a single layer, which controls how much "
"travel is spent moving between them.\n\n"
"Default: nearest-neighbor chaining, refined with 2-opt and crossing removal. A good general "
"choice.\n"
"As object list: instances are printed in the same order as the object list, without any path "
"optimization. Use it when you need a predictable, manually controlled order.\n"
"Best of all (shortest path): every strategy is evaluated and the shortest one is used. The "
"object instance order is decided once for the whole print, while the ordering of individual "
"islands is decided per layer, so different layers may end up using different strategies. "
"Slightly slower to slice.\n"
"Snake: serpentine row-by-row traversal, refined with 2-opt. Well suited to regular grids of "
"many small parts.\n\n"
"With multiple filaments or tools in the same layer, minimizing tool changes takes priority: "
"objects are grouped by filament first and this setting only orders the instances within each "
"filament group, so the overall sequence may not look like the shortest path across the plate.");
def->enum_keys_map = &ConfigOptionEnum<PrintOrder>::get_enum_values();
def->enum_values.push_back("default");
def->enum_values.push_back("as_obj_list");
def->enum_values.push_back("best_of");
def->enum_values.push_back("snake");
def->enum_labels.push_back(L("Default"));
def->enum_labels.push_back(L("As object list"));
def->enum_labels.push_back(L("Best of all (shortest path)"));
def->enum_labels.push_back(L("Snake"));
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionEnum<PrintOrder>(PrintOrder::Default));
@@ -5648,6 +5668,7 @@ void PrintConfigDef::init_fff_params()
// Orca:
def = this->add("retract_after_wipe", coPercents);
def->label = L("Retract amount after wipe");
// xgettext:no-c-format, no-boost-format
def->tooltip = L("The length of fast retraction after wipe, relative to retraction length.\n"
"The value will be clamped by 100% minus the retract amount before the wipe value.");
def->sidetext = "%";
+2
View File
@@ -234,6 +234,8 @@ enum class PrintOrder
{
Default,
AsObjectList,
BestOfStrategies, // run all custom strategies, pick the shortest total path
Snake, // snake-like row traversal (back-and-forth) + 2-opt
Count,
};
+49 -34
View File
@@ -1364,7 +1364,6 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "infill_combination_max_layer_height"
|| opt_key == "bottom_shell_thickness"
|| opt_key == "top_shell_thickness"
|| opt_key == "top_surface_expansion"
|| opt_key == "top_surface_expansion_margin"
|| opt_key == "top_surface_expansion_direction"
|| opt_key == "minimum_sparse_infill_area"
@@ -1400,7 +1399,6 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "infill_anchor"
|| opt_key == "infill_anchor_max"
|| opt_key == "top_surface_line_width"
|| opt_key == "top_surface_density"
|| opt_key == "bottom_surface_density"
|| opt_key == "center_of_surface_pattern"
|| opt_key == "separated_infills"
@@ -1434,6 +1432,24 @@ bool PrintObject::invalidate_state_by_config_options(
is_approx(new_density->value, 0.) || is_approx(new_density->value, 100.))
steps.emplace_back(posPerimeters);
steps.emplace_back(posPrepareInfill);
} else if (opt_key == "top_surface_density") {
// ORCA: 0% means no top solid fill, which switches off both the top surface expansion and the wall
// removal over top surfaces. Only crossing zero matters; posPerimeters cascades to posPrepareInfill.
const auto *old_density = old_config.option<ConfigOptionPercent>(opt_key);
const auto *new_density = new_config.option<ConfigOptionPercent>(opt_key);
assert(old_density && new_density);
if (is_approx(old_density->value, 0.) || is_approx(new_density->value, 0.))
steps.emplace_back(posPerimeters);
steps.emplace_back(posInfill);
} else if (opt_key == "top_surface_expansion") {
// ORCA: without the expansion the top fill never reaches the space freed by only_one_wall_top, so the
// walls over top surfaces are kept. Only crossing zero matters; posPerimeters cascades to posPrepareInfill.
const auto *old_expansion = old_config.option<ConfigOptionFloat>(opt_key);
const auto *new_expansion = new_config.option<ConfigOptionFloat>(opt_key);
assert(old_expansion && new_expansion);
if (old_expansion->value <= 0. || new_expansion->value <= 0.)
steps.emplace_back(posPerimeters);
steps.emplace_back(posPrepareInfill);
} else if (opt_key == "internal_solid_infill_line_width") {
// This value is used for calculating perimeter - infill overlap, thus perimeters need to be recalculated.
steps.emplace_back(posPerimeters);
@@ -1760,51 +1776,50 @@ void PrintObject::detect_surfaces_type()
}
}
// ORCA: Expand the top surfaces outward by top_surface_expansion in every direction. This
// enlarges the top solid infill and, in particular, grows it over the covered material left
// by features rising from the middle of a top surface (filling holes and joining tops so the
// features rest on it). The expansion stays inside the section it belongs to: each connected
// solid island has its own outer wall, so the top is grown within each island separately and
// clipped to it - growing one island's top across the gap into another island (which may have
// no top surface, leaving a partially filled layer) is never allowed. The top infill sits
// inside the perimeters, so the margin is measured from the walls: the island is inset by the
// band the walls consume (outer wall + inner walls) plus the configured margin, making that
// value the real clearance between the expanded top and the walls (avoiding a hull line). The
// original top is unioned back in, so where it already sits within that band it is kept as-is.
// Never claims a bottom surface.
const double top_expansion = layerm->region().config().top_surface_expansion.value;
if (top_expansion > 0. && ! top.empty()) {
const double d = scale_(top_expansion);
const auto jt = Clipper2Lib::JoinType::Miter;
const ExPolygons T = union_ex(to_expolygons(top));
const int wall_loops = layerm->region().config().wall_loops.value;
// ORCA: Grow the top surfaces by top_surface_expansion, so the top solid infill also covers the
// material left by features rising from the middle of a top surface (filling the holes and
// joining the tops, so the features rest on solid infill). Each connected island is grown and
// clipped separately: growing one island's top across a gap into another - which may have no top
// surface at all, leaving a partially filled layer - is never allowed. The original top is
// unioned back in and bottom surfaces are never claimed, so this can only add area.
const PrintRegionConfig &region_config = layerm->region().config();
const double top_expansion = region_config.top_surface_expansion.value;
// Nothing to expand without a top fill: a 0% top surface density leaves the top layer with
// walls only, and zero top shell layers retypes it as internal in prepare_fill_surfaces().
if (top_expansion > 0. && region_config.top_shell_layers.value > 0 &&
region_config.top_surface_density.value > 0. && ! top.empty()) {
const double d = scale_(top_expansion);
const ExPolygons T = union_ex(to_expolygons(top));
// Walls are laid out on spacing, not width; and only_one_wall_top leaves a single wall over
// a top surface, which is exactly the situation handled here.
const int wall_loops = region_config.only_one_wall_top.value ? std::min(region_config.wall_loops.value, 1)
: region_config.wall_loops.value;
const double wall_band = wall_loops <= 0 ? 0. :
double(layerm->flow(frExternalPerimeter).scaled_width()) +
double(layerm->flow(frPerimeter).scaled_width()) * double(wall_loops - 1);
const double margin = scale_(layerm->region().config().top_surface_expansion_margin.value);
double(layerm->flow(frPerimeter).scaled_spacing()) * double(wall_loops - 1);
const double margin = scale_(region_config.top_surface_expansion_margin.value);
// minimum real top to act on: ignore anything thinner than ~2 top-infill lines
const float min_top = float(layerm->flow(frTopSolidInfill).scaled_width());
const auto direction = layerm->region().config().top_surface_expansion_direction.value;
const auto direction = region_config.top_surface_expansion_direction.value;
ExPolygons grown;
for (const ExPolygon &island : union_ex(layerm_slices_surfaces)) {
// The top infill only exists inside the perimeters, so seed and measure from the infill
// region (the island minus the wall band), not the raw slice. A section whose only
// exposed top lies in the wall band - i.e. a layer where the top is just the walls
// themselves - has no infill here and is skipped, instead of being flooded inward by
// the expansion. Thin slivers inside the infill region are dropped by the opening too.
// region (the island minus the wall band), not the raw slice: a section whose exposed top
// is just the walls themselves is then skipped instead of being flooded inward. Clip the
// layer's tops to the island first, to keep the boolean ops proportional to the island.
const ExPolygons infill_region = wall_band > 0. ? offset_ex(island, -float(wall_band)) : ExPolygons{ island };
const ExPolygons island_top = intersection_ex(T, infill_region);
const ExPolygons island_top = intersection_ex(
ClipperUtils::clip_clipper_polygons_with_subject_bbox(T, get_extents(island).inflated(SCALED_EPSILON)),
infill_region);
if (opening_ex(island_top, min_top).empty())
continue; // no real top infill in this section - never expand into it
// grow by d, then keep only the part allowed by the configured direction: inward fills
// the holes/gaps left by features (clip the growth back to the top's own filled outline,
// which leaves the outer edge fixed), outward grows the outer edge toward the walls (drop
// the growth that fell into the original holes), and inward+outward keeps both.
ExPolygons expanded = offset_ex_2(island_top, d, jt);
// Grow, then keep only what the configured direction allows, using the top's own filled
// outline (same outer edge, holes closed) to tell the two apart.
ExPolygons expanded = offset_ex_2(island_top, d, Clipper2Lib::JoinType::Miter);
if (direction != TopSurfaceExpansionDirection::InwardAndOutward) {
ExPolygons outline; // the top with its holes filled (same outer edge)
ExPolygons outline;
outline.reserve(island_top.size());
for (const ExPolygon &ex : island_top)
outline.emplace_back(ex.contour);
+25 -6
View File
@@ -10,6 +10,7 @@
#include "KDTreeIndirect.hpp"
#include "MutablePriorityQueue.hpp"
#include "Print.hpp"
#include "GCode/OrderingStrategies.hpp"
#include <cmath>
#include <cassert>
@@ -1103,7 +1104,7 @@ std::vector<size_t> chain_expolygons(const ExPolygons &input_exploy) {
return chain_points(points);
}
std::vector<size_t> chain_points(const Points &points, Point *start_near)
std::vector<size_t> chain_points(const Points &points, const Point *start_near)
{
auto segment_end_point = [&points](size_t idx, bool /* first_point */) -> const Point& { return points[idx]; };
std::vector<std::pair<size_t, bool>> ordered = chain_segments_greedy<Point, decltype(segment_end_point)>(segment_end_point, points.size(), start_near);
@@ -1111,9 +1112,26 @@ std::vector<size_t> chain_points(const Points &points, Point *start_near)
out.reserve(ordered.size());
for (auto &segment_and_reversal : ordered)
out.emplace_back(segment_and_reversal.first);
return out;
}
std::vector<size_t> chain_points_with_postprocessing(const Points &points, const Point *start_near)
{
std::vector<size_t> path = chain_points(points, start_near);
// Alternate 2-opt and crossing removal until convergence.
// 2-opt can create new crossings, and crossing removal can create new
// opportunities for 2-opt improvement. Break early if neither improves.
for (int iter = 0; iter < 3; ++iter) {
bool improved = tsp_2opt_improve(path, points);
improved |= tsp_remove_crossings(path, points);
if (!improved) break;
}
if (start_near == nullptr)
tsp_rotate_minimize_closing(path, points);
return path;
}
#ifndef NDEBUG
// #define DEBUG_SVG_OUTPUT
#endif /* NDEBUG */
@@ -2025,12 +2043,13 @@ std::vector<const PrintInstance*> chain_print_object_instances(const std::vector
instances.emplace_back(i, j);
}
}
auto segment_end_point = [&object_reference_points](size_t idx, bool /* first_point */) -> const Point& { return object_reference_points[idx]; };
std::vector<std::pair<size_t, bool>> ordered = chain_segments_greedy<Point, decltype(segment_end_point)>(segment_end_point, instances.size(), start_near);
// Order objects using nearest neighbor + post-processing (crossing removal + 2-opt).
std::vector<size_t> path = chain_points_with_postprocessing(object_reference_points, start_near);
std::vector<const PrintInstance*> out;
out.reserve(instances.size());
for (auto& segment_and_reversal : ordered) {
const std::pair<size_t, size_t>& inst = instances[segment_and_reversal.first];
out.reserve(path.size());
for (size_t idx : path) {
const std::pair<size_t, size_t>& inst = instances[idx];
out.emplace_back(&print_objects[inst.first]->instances()[inst.second]);
}
return out;
+3 -1
View File
@@ -15,7 +15,9 @@ namespace Slic3r {
using PolyNodes = std::vector<PolyNode*, PointsAllocator<PolyNode*>>;
}
std::vector<size_t> chain_points(const Points &points, Point *start_near = nullptr);
std::vector<size_t> chain_points(const Points &points, const Point *start_near = nullptr);
// Variant with post-processing (crossing removal + 2-opt) for object ordering.
std::vector<size_t> chain_points_with_postprocessing(const Points &points, const Point *start_near = nullptr);
std::vector<size_t> chain_expolygons(const ExPolygons &input_exploy);
std::vector<std::pair<size_t, bool>> chain_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near = nullptr);
+55 -27
View File
@@ -65,6 +65,15 @@ std::pair<SupportGeneratorLayersPtr, SupportGeneratorLayersPtr> generate_interfa
const bool smooth_supports = support_params.support_style != smsGrid;
SupportGeneratorLayersPtr &interface_layers = base_and_interface_layers.first;
SupportGeneratorLayersPtr &base_interface_layers = base_and_interface_layers.second;
// The user-facing interface layer counts include the contact layer. Internally,
// contact layers are generated separately, so only the remaining layers are
// projected into intermediate interface/base-interface layers here.
const size_t num_top_interface_layers = support_params.has_top_contacts ? support_params.num_top_interface_layers - 1 : 0;
const size_t num_bottom_interface_layers = support_params.has_bottom_contacts ? support_params.num_bottom_interface_layers - 1 : 0;
const size_t num_top_base_interface_layers = std::min(support_params.num_top_base_interface_layers, num_top_interface_layers);
const size_t num_bottom_base_interface_layers = std::min(support_params.num_bottom_base_interface_layers, num_bottom_interface_layers);
const size_t num_top_interface_layers_only = num_top_interface_layers - num_top_base_interface_layers;
const size_t num_bottom_interface_layers_only = num_bottom_interface_layers - num_bottom_base_interface_layers;
interface_layers.assign(intermediate_layers.size(), nullptr);
if (support_params.has_base_interfaces())
@@ -124,6 +133,8 @@ std::pair<SupportGeneratorLayersPtr, SupportGeneratorLayersPtr> generate_interfa
};
tbb::parallel_for(tbb::blocked_range<int>(0, int(intermediate_layers.size())),
[&bottom_contacts, &top_contacts, &top_interface_layers, &top_base_interface_layers, &intermediate_layers, &insert_layer, &support_params,
num_top_interface_layers, num_bottom_interface_layers, num_top_base_interface_layers, num_bottom_base_interface_layers,
num_top_interface_layers_only, num_bottom_interface_layers_only,
snug_supports, &interface_layers, &base_interface_layers](const tbb::blocked_range<int>& range) {
// Gather the top / bottom contact layers intersecting with num_interface_layers resp. num_interface_layers_only intermediate layers above / below
// this intermediate layer.
@@ -142,16 +153,16 @@ std::pair<SupportGeneratorLayersPtr, SupportGeneratorLayersPtr> generate_interfa
Polygons polygons_top_contact_projected_base;
Polygons polygons_bottom_contact_projected_interface;
Polygons polygons_bottom_contact_projected_base;
if (support_params.num_top_interface_layers > 0) {
if (num_top_interface_layers > 0) {
// Top Z coordinate of a slab, over which we are collecting the top / bottom contact surfaces
coordf_t top_z = intermediate_layers[std::min(num_intermediate - 1, idx_intermediate_layer + int(support_params.num_top_interface_layers) - 1)]->print_z;
coordf_t top_inteface_z = std::numeric_limits<coordf_t>::max();
if (support_params.num_top_base_interface_layers > 0)
coordf_t top_z = intermediate_layers[std::min(num_intermediate - 1, idx_intermediate_layer + int(num_top_interface_layers) - 1)]->print_z;
coordf_t top_interface_z = std::numeric_limits<coordf_t>::max();
if (num_top_base_interface_layers > 0)
// Some top base interface layers will be generated.
top_inteface_z = support_params.num_top_interface_layers_only() == 0 ?
top_interface_z = num_top_interface_layers_only == 0 ?
// Only base interface layers to generate.
- std::numeric_limits<coordf_t>::max() :
intermediate_layers[std::min(num_intermediate - 1, idx_intermediate_layer + int(support_params.num_top_interface_layers_only()) - 1)]->print_z;
intermediate_layers[std::min(num_intermediate - 1, idx_intermediate_layer + int(num_top_interface_layers_only) - 1)]->print_z;
// Move idx_top_contact_first up until above the current print_z.
idx_top_contact_first = idx_higher_or_equal(top_contacts, idx_top_contact_first, [&intermediate_layer](const SupportGeneratorLayer *layer){ return layer->print_z >= intermediate_layer.print_z; }); // - EPSILON
// Collect the top contact areas above this intermediate layer, below top_z.
@@ -160,22 +171,22 @@ std::pair<SupportGeneratorLayersPtr, SupportGeneratorLayersPtr> generate_interfa
//FIXME maybe this adds one interface layer in excess?
if (top_contact_layer.bottom_z - EPSILON > top_z)
break;
polygons_append(top_contact_layer.bottom_z - EPSILON > top_inteface_z ? polygons_top_contact_projected_base : polygons_top_contact_projected_interface,
polygons_append(top_contact_layer.bottom_z - EPSILON > top_interface_z ? polygons_top_contact_projected_base : polygons_top_contact_projected_interface,
// For snug supports, project the overhang polygons covering the whole overhang, so that they will merge without a gap with support polygons of the other layers.
// For grid supports, merging of support regions will be performed by the projection into grid.
snug_supports ? *top_contact_layer.overhang_polygons : top_contact_layer.polygons);
}
}
if (support_params.num_bottom_interface_layers > 0) {
if (num_bottom_interface_layers > 0) {
// Bottom Z coordinate of a slab, over which we are collecting the top / bottom contact surfaces
coordf_t bottom_z = intermediate_layers[std::max(0, idx_intermediate_layer - int(support_params.num_bottom_interface_layers) + 1)]->bottom_z;
coordf_t bottom_z = intermediate_layers[std::max(0, idx_intermediate_layer - int(num_bottom_interface_layers) + 1)]->bottom_z;
coordf_t bottom_interface_z = - std::numeric_limits<coordf_t>::max();
if (support_params.num_bottom_base_interface_layers > 0)
if (num_bottom_base_interface_layers > 0)
// Some bottom base interface layers will be generated.
bottom_interface_z = support_params.num_bottom_interface_layers_only() == 0 ?
bottom_interface_z = num_bottom_interface_layers_only == 0 ?
// Only base interface layers to generate.
std::numeric_limits<coordf_t>::max() :
intermediate_layers[std::max(0, idx_intermediate_layer - int(support_params.num_bottom_interface_layers_only()))]->bottom_z;
intermediate_layers[std::max(0, idx_intermediate_layer - int(num_bottom_interface_layers_only))]->bottom_z;
// Move idx_bottom_contact_first up until touching bottom_z.
idx_bottom_contact_first = idx_higher_or_equal(bottom_contacts, idx_bottom_contact_first, [bottom_z](const SupportGeneratorLayer *layer){ return layer->print_z >= bottom_z - EPSILON; });
// Collect the top contact areas above this intermediate layer, below top_z.
@@ -1563,13 +1574,17 @@ void generate_support_toolpaths(
// Pointer to the 1st layer interface filler.
auto filler_first_layer = filler_first_layer_ptr ? filler_first_layer_ptr.get() : filler_interface.get();
// Filler for the 1st layer interface, if different from filler_interface.
auto filler_raft_contact_ptr = std::unique_ptr<Fill>(range.begin() == n_raft_layers && config.support_interface_top_layers.value == 0 ?
const bool top_interfaces_enabled = support_params.num_top_interface_layers > 0;
const bool bottom_interfaces_enabled = support_params.num_bottom_interface_layers > 0;
const coordf_t base_interface_density = top_interfaces_enabled || !bottom_interfaces_enabled ?
support_params.top_interface_density : support_params.bottom_interface_density;
auto filler_raft_contact_ptr = std::unique_ptr<Fill>(range.begin() == n_raft_layers && !top_interfaces_enabled ?
Fill::new_from_type(support_params.raft_interface_fill_pattern) : nullptr);
// Pointer to the 1st layer interface filler.
auto filler_raft_contact = filler_raft_contact_ptr ? filler_raft_contact_ptr.get() : filler_interface.get();
// Filler for the base interface (to be used for soluble interface / non soluble base, to produce non soluble interface layer below soluble interface layer).
auto filler_base_interface = std::unique_ptr<Fill>(base_interface_layers.empty() ? nullptr :
Fill::new_from_type(support_params.top_interface_density > 0.95 || support_params.with_sheath ? ipRectilinear : ipSupportBase));
Fill::new_from_type(base_interface_density > 0.95 || support_params.with_sheath ? ipRectilinear : ipSupportBase));
auto filler_support = std::unique_ptr<Fill>(Fill::new_from_type(support_params.base_fill_pattern));
filler_interface->set_bounding_box(bbox_object);
if (filler_first_layer_ptr)
@@ -1583,10 +1598,7 @@ void generate_support_toolpaths(
{
SupportLayer &support_layer = *support_layers[support_layer_id];
LayerCache &layer_cache = layer_caches[support_layer_id];
const float support_interface_angle = (config.support_interface_pattern == smipRectilinearInterlaced) ?
support_params.raft_interface_angle(support_layer.interface_id()) :
((support_params.support_style == smsGrid || config.support_interface_pattern == smipRectilinear) ?
support_params.interface_angle : support_params.raft_interface_angle(support_layer.interface_id()));
const float support_interface_angle = support_params.support_interface_angle(support_layer.interface_id());
// Find polygons with the same print_z.
SupportGeneratorLayerExtruded &bottom_contact_layer = layer_cache.bottom_contact_layer;
@@ -1619,7 +1631,9 @@ void generate_support_toolpaths(
bool raft_layer = slicing_params.interface_raft_layers && top_contact_layer.layer && is_approx(top_contact_layer.layer->print_z, slicing_params.raft_contact_top_z);
// ORCA: Organic tree uses projected contacts to build the interface stack; avoid extra bottom-contact extrusion.
const bool organic_tree = support_params.support_style == SupportMaterialStyle::smsTreeOrganic;
if (config.support_interface_top_layers == 0) {
const bool top_interfaces = support_params.num_top_interface_layers > 0;
const bool bottom_interfaces = support_params.num_bottom_interface_layers > 0;
if (!top_interfaces) {
// If no top interface layers were requested, we treat the contact layer exactly as a generic base layer.
// Don't merge the raft contact layer though.
if (support_params.can_merge_support_regions && ! raft_layer) {
@@ -1642,15 +1656,29 @@ void generate_support_toolpaths(
if (top_contact_layer.could_merge(interface_layer) && ! raft_layer)
top_contact_layer.merge(std::move(interface_layer));
}
if ((config.support_interface_top_layers == 0 || config.support_interface_bottom_layers == 0) && support_params.can_merge_support_regions) {
if (!bottom_interfaces && support_params.can_merge_support_regions) {
if (base_layer.could_merge(bottom_contact_layer))
base_layer.merge(std::move(bottom_contact_layer));
else if (base_layer.empty() && ! bottom_contact_layer.empty() && ! bottom_contact_layer.layer->bridging)
base_layer = std::move(bottom_contact_layer);
} else if (bottom_contact_layer.could_merge(top_contact_layer) && ! raft_layer) {
top_contact_layer.merge(std::move(bottom_contact_layer));
if (top_interfaces && bottom_interfaces) {
top_contact_layer.merge(std::move(bottom_contact_layer));
} else if (bottom_interfaces) {
top_contact_layer.set_polygons_to_extrude(
diff(top_contact_layer.polygons_to_extrude(), bottom_contact_layer.polygons_to_extrude()));
} else {
bottom_contact_layer.set_polygons_to_extrude(
diff(bottom_contact_layer.polygons_to_extrude(), top_contact_layer.polygons_to_extrude()));
}
} else if (bottom_contact_layer.could_merge(interface_layer) && ! organic_tree) {
bottom_contact_layer.merge(std::move(interface_layer));
const bool interface_layer_is_bottom = interface_layer.layer->layer_type == SupporLayerType::BottomInterface;
if (bottom_interfaces && interface_layer_is_bottom) {
bottom_contact_layer.merge(std::move(interface_layer));
} else {
bottom_contact_layer.set_polygons_to_extrude(
diff(bottom_contact_layer.polygons_to_extrude(), interface_layer.polygons_to_extrude()));
}
}
// Orca: For organic trees the support-material regions are generated from
@@ -1730,12 +1758,12 @@ void generate_support_toolpaths(
interface_as_base ? ExtrusionRole::erSupportMaterial : ExtrusionRole::erSupportMaterialInterface, interface_flow);
}
};
const bool top_interfaces = support_params.num_top_interface_layers > 0;
const bool bottom_interfaces = top_interfaces && support_params.num_bottom_interface_layers > 0;
extrude_interface(top_contact_layer, raft_layer ? InterfaceLayerType::RaftContact : top_interfaces ? InterfaceLayerType::TopContact : InterfaceLayerType::InterfaceAsBase);
if (!organic_tree)
extrude_interface(bottom_contact_layer, bottom_interfaces ? InterfaceLayerType::BottomContact : InterfaceLayerType::InterfaceAsBase);
extrude_interface(interface_layer, top_interfaces ? InterfaceLayerType::Interface : InterfaceLayerType::InterfaceAsBase);
const bool interface_layer_enabled = !interface_layer.empty() &&
(interface_layer.layer->layer_type == SupporLayerType::BottomInterface ? bottom_interfaces : top_interfaces);
extrude_interface(interface_layer, interface_layer_enabled ? InterfaceLayerType::Interface : InterfaceLayerType::InterfaceAsBase);
// Base interface layers under soluble interfaces
if ( ! base_interface_layer.empty() && ! base_interface_layer.polygons_to_extrude().empty()) {
Fill *filler = filler_base_interface.get();
@@ -1745,7 +1773,7 @@ void generate_support_toolpaths(
Flow interface_flow = support_params.support_material_flow.with_height(float(base_interface_layer.layer->height));
filler->angle = support_interface_angle;
filler->spacing = support_params.support_material_interface_flow.spacing();
filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / support_params.top_interface_density));
filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / base_interface_density));
fill_expolygons_generate_paths(
// Destination
base_interface_layer.extrusions,
@@ -1753,7 +1781,7 @@ void generate_support_toolpaths(
// Regions to fill
union_safety_offset_ex(base_interface_layer.polygons_to_extrude()),
// Filler and its parameters
filler, float(support_params.top_interface_density),
filler, float(base_interface_density),
// Extrusion parameters
ExtrusionRole::erSupportMaterial, interface_flow);
}
+45 -15
View File
@@ -34,7 +34,7 @@ struct SupportParameters {
{
this->num_top_interface_layers = std::max(0, object_config.support_interface_top_layers.value);
this->num_bottom_interface_layers = number_of_support_interface_bottom_layers(object_config);
this->num_bottom_interface_layers = std::max(0, number_of_support_interface_bottom_layers(object_config));
this->has_top_contacts = num_top_interface_layers > 0;
this->has_bottom_contacts = num_bottom_interface_layers > 0;
// BBS: if support interface and support base do not use the same filament, add a base layer to improve their adhesion
@@ -46,15 +46,15 @@ struct SupportParameters {
if (non_soluble_base_top) { // ORCA: Try to support soluble dense interfaces with non-soluble dense interfaces.
this->num_top_base_interface_layers = size_t(std::min(int(num_top_interface_layers) / 2, 2));
} else {
this->num_top_base_interface_layers =
(different_support_interface_filament && this->zero_gap_interface_top) ? 1 : 0;
// Keep at least one configured layer on the interface filament.
this->num_top_base_interface_layers = different_support_interface_filament && num_top_interface_layers > 1 ? 1 : 0;
}
if (non_soluble_base_bottom) { // ORCA: Try to support soluble dense interfaces with non-soluble dense interfaces.
this->num_bottom_base_interface_layers = size_t(std::min(int(num_bottom_interface_layers) / 2, 2));
} else {
this->num_bottom_base_interface_layers =
(different_support_interface_filament && this->zero_gap_interface_bottom) ? 1 : 0;
// Keep at least one configured layer on the interface filament.
this->num_bottom_base_interface_layers = different_support_interface_filament && num_bottom_interface_layers > 1 ? 1 : 0;
}
}
this->first_layer_flow = Slic3r::support_material_1st_layer_flow(&object, float(slicing_params.first_print_layer_height));
@@ -74,7 +74,7 @@ struct SupportParameters {
for (auto layer : object.layers())
this->support_layer_height_min = std::min(this->support_layer_height_min, std::max(0.01, layer->height));
if (object_config.support_interface_top_layers.value == 0) {
if (this->num_top_interface_layers == 0 && this->num_bottom_interface_layers == 0) {
// No interface layers allowed, print everything with the base support pattern.
this->support_material_interface_flow = this->support_material_flow;
}
@@ -120,8 +120,8 @@ struct SupportParameters {
this->raft_interface_density = std::min(1., this->raft_interface_flow.spacing() / raft_interface_spacing);
this->support_spacing = object_config.support_base_pattern_spacing.value + this->support_material_flow.spacing();
this->support_density = std::min(1., this->support_material_flow.spacing() / this->support_spacing);
if (object_config.support_interface_top_layers.value == 0) {
// No interface layers allowed, print everything with the base support pattern.
if (this->num_top_interface_layers == 0) {
// No top interface layers allowed; keep unused top interface parameters aligned with base support.
this->top_interface_spacing = this->support_spacing;
this->top_interface_density = this->support_density;
}
@@ -133,16 +133,20 @@ struct SupportParameters {
this->support_density > 0.95 || this->with_sheath ? ipRectilinear : ipSupportBase;
this->interface_fill_pattern = (this->top_interface_density > 0.95 ? ipRectilinear : ipSupportBase);
this->raft_interface_fill_pattern = this->raft_interface_density > 0.95 ? ipRectilinear : ipSupportBase;
const coordf_t contact_interface_density = this->num_top_interface_layers > 0 ?
this->top_interface_density : this->bottom_interface_density;
const bool zero_gap_contact_interface = this->num_top_interface_layers > 0 ?
this->zero_gap_interface_top : this->zero_gap_interface_bottom;
if (object_config.support_interface_pattern == smipGrid)
this->contact_fill_pattern = ipGrid;
else if (object_config.support_interface_pattern == smipRectilinearInterlaced)
this->contact_fill_pattern = ipRectilinear;
else
this->contact_fill_pattern =
(object_config.support_interface_pattern == smipAuto && this->zero_gap_interface_top) ||
(object_config.support_interface_pattern == smipAuto && zero_gap_contact_interface) ||
object_config.support_interface_pattern == smipConcentric ?
ipConcentric :
(this->top_interface_density > 0.95 ? ipRectilinear : ipSupportBase);
(contact_interface_density > 0.95 ? ipRectilinear : ipSupportBase);
this->raft_angle_1st_layer = 0.f;
this->raft_angle_base = 0.f;
@@ -188,6 +192,7 @@ struct SupportParameters {
std::numeric_limits<double>::max();
support_style = object_config.support_style;
support_interface_pattern = object_config.support_interface_pattern;
if (support_style != smsDefault) {
if ((support_style == smsSnug || support_style == smsGrid) && is_tree(object_config.support_type)) support_style = smsDefault;
if ((support_style == smsTreeSlim || support_style == smsTreeStrong || support_style == smsTreeHybrid || support_style == smsTreeOrganic) &&
@@ -211,9 +216,9 @@ struct SupportParameters {
bool has_top_contacts;
// Is there at least a bottom contact layer extruded below support base?
bool has_bottom_contacts;
// Number of top interface layers without counting the contact layer.
// User-configured number of top interface layers, including the contact layer.
size_t num_top_interface_layers;
// Number of bottom interface layers without counting the contact layer.
// User-configured number of bottom interface layers, including the contact layer.
size_t num_bottom_interface_layers;
// Number of top base interface layers.
size_t num_top_base_interface_layers;
@@ -235,7 +240,7 @@ struct SupportParameters {
Flow support_material_interface_flow;
// Flow at the bottom interfaces and contacts.
Flow support_material_bottom_interface_flow;
// Flow at raft inteface & contact layers.
// Flow at raft interface & contact layers.
Flow raft_interface_flow;
coordf_t support_extrusion_width;
// Is merging of regions allowed? Could the interface & base support regions be printed with the same extruder?
@@ -262,6 +267,7 @@ struct SupportParameters {
// Density of the base support layers.
coordf_t support_density;
SupportMaterialStyle support_style = smsDefault;
SupportMaterialInterfacePattern support_interface_pattern = smipAuto;
// Pattern of the sparse infill including sparse raft layers.
InfillPattern base_fill_pattern;
@@ -280,9 +286,33 @@ struct SupportParameters {
float raft_angle_base;
float raft_angle_interface;
// Produce a raft interface angle for a given SupportLayer::interface_id()
// Produce a +/-45deg alternating raft interface angle for a given SupportLayer::interface_id().
float raft_interface_angle(size_t interface_id) const
{ return this->raft_angle_interface + ((interface_id & 1) ? float(- M_PI / 4.) : float(+ M_PI / 4.)); }
{ return this->raft_angle_interface + ((interface_id & 1) ? float(- M_PI_4) : float(+ M_PI_4)); }
// Produce support interface angle for a given SupportLayer::interface_id().
// Angle will be shifted/rotated based on interface pattern.
float support_interface_angle(size_t interface_id) const
{
float angle;
switch (this->support_interface_pattern) {
case SupportMaterialInterfacePattern::smipRectilinear:
angle = support_style == SupportMaterialStyle::smsSnug ? this->interface_angle - float(M_PI_4) : this->interface_angle;
break;
case SupportMaterialInterfacePattern::smipRectilinearInterlaced:
angle = this->interface_angle + ((interface_id & 1) ? float(M_PI_4) : float(-M_PI_4));
break;
case SupportMaterialInterfacePattern::smipGrid:
angle = this->base_angle;
break;
default:
angle = this->interface_angle;
break;
}
return angle;
}
bool independent_layer_height = false;
const double thresh_big_overhang = Slic3r::sqr(scale_(10));
+1 -1
View File
@@ -469,7 +469,7 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex
});
// 2) Sum over top / bottom ranges.
const bool processing_last_mesh = outline_idx == layer_outline_indices.size();
const bool processing_last_mesh = outline_idx == layer_outline_indices.back();
tbb::parallel_for(tbb::blocked_range<LayerIndex>(data.begin(), data.end()),
[&collision_areas_offsetted, &outlines, &machine_border = m_machine_border, &anti_overhang = m_anti_overhang, radius,
xy_distance, z_distance_bottom_layers, z_distance_top_layers, min_resolution = m_min_resolution, &data, processing_last_mesh, &throw_on_cancel]
+40 -54
View File
@@ -1511,7 +1511,9 @@ void TreeSupport::generate_toolpaths()
// ORCA: reset interface Fill state per area group to keep angles deterministic.
filler_interface->fixed_angle = false;
filler_interface->layer_id = size_t(-1);
filler_interface->angle = base_support_angle + M_PI_2; // default interface angle is perpendicular to support angle
filler_Roof1stLayer->fixed_angle = false;
filler_Roof1stLayer->layer_id = size_t(-1);
filler_interface->angle = m_support_params.support_interface_angle(area_group.interface_id);
if (area_group.type != SupportLayer::BaseType) {
// interface
if (layer_id == 0) {
@@ -1537,8 +1539,10 @@ void TreeSupport::generate_toolpaths()
fill_params.density = interface_density;
// Note: spacing means the separation between two lines as if they are tightly extruded
filler_Roof1stLayer->spacing = interface_flow.spacing();
filler_Roof1stLayer->angle = base_support_angle;
filler_Roof1stLayer->angle = m_support_params.support_interface_angle(area_group.interface_id);
fill_params.dont_sort = true;
filler_Roof1stLayer->fixed_angle = (m_object_config->support_interface_pattern == smipRectilinearInterlaced ||
m_object_config->support_interface_pattern == smipRectilinear);
Flow interface_base_flow = interface_as_base ? support_flow : interface_flow;
ExtrusionRole interface_role = interface_as_base ? erSupportMaterial : erSupportMaterialInterface;
// generate a perimeter first to support interface better
@@ -1556,18 +1560,11 @@ void TreeSupport::generate_toolpaths()
fill_params.density = bottom_interface_density;
filler_interface->spacing = interface_flow.spacing();
if (m_object_config->support_interface_pattern == smipGrid) {
filler_interface->angle = base_support_angle;
fill_params.dont_sort = true;
}
if (m_object_config->support_interface_pattern == smipRectilinearInterlaced) {
// ORCA: explicit 0/90 alternation for rectilinear interlaced interfaces.
filler_interface->fixed_angle = true;
filler_interface->angle = base_support_angle + ((area_group.interface_id & 1) * M_PI_2);
fill_params.dont_sort = true;
}
fill_params.dont_sort = (m_object_config->support_interface_pattern == smipGrid ||
m_object_config->support_interface_pattern == smipRectilinearInterlaced);
filler_interface->fixed_angle = (m_object_config->support_interface_pattern == smipRectilinearInterlaced ||
m_object_config->support_interface_pattern == smipRectilinear);
Flow interface_base_flow = interface_as_base ? support_flow : interface_flow;
ExtrusionRole interface_role = interface_as_base ? erSupportMaterial : erSupportMaterialInterface;
@@ -1579,17 +1576,11 @@ void TreeSupport::generate_toolpaths()
fill_params.density = interface_density;
filler_interface->spacing = interface_flow.spacing();
if (m_object_config->support_interface_pattern == smipGrid) {
filler_interface->angle = base_support_angle;
fill_params.dont_sort = true;
}
fill_params.dont_sort = (m_object_config->support_interface_pattern == smipGrid ||
m_object_config->support_interface_pattern == smipRectilinearInterlaced);
if (m_object_config->support_interface_pattern == smipRectilinearInterlaced) {
// ORCA: explicit 0/90 alternation for rectilinear interlaced interfaces.
filler_interface->fixed_angle = true;
filler_interface->angle = base_support_angle + ((area_group.interface_id & 1) * M_PI_2);
fill_params.dont_sort = true;
}
filler_interface->fixed_angle = (m_object_config->support_interface_pattern == smipRectilinearInterlaced ||
m_object_config->support_interface_pattern == smipRectilinear);
Flow interface_base_flow = interface_as_base ? support_flow : interface_flow;
ExtrusionRole interface_role = interface_as_base ? erSupportMaterial : erSupportMaterialInterface;
@@ -2014,6 +2005,9 @@ void TreeSupport::draw_circles()
// generate areas
const coordf_t layer_height = config.layer_height.value;
const size_t top_interface_layers = m_support_params.num_top_interface_layers;
const int top_base_interface_layers = std::min<int>(
int(m_support_params.num_top_base_interface_layers),
top_interface_layers > 0 ? int(top_interface_layers) - 1 : 0);
const size_t bottom_interface_layers = number_of_support_interface_bottom_layers(config);
const double nozzle_diameter = m_object->print()->config().nozzle_diameter.get_at(0);
const coordf_t line_width = config.get_abs_value("support_line_width", nozzle_diameter);
@@ -2054,12 +2048,14 @@ void TreeSupport::draw_circles()
ExPolygons& base_areas = ts_layer->base_areas;
ExPolygons& roof_areas = ts_layer->roof_areas;
ExPolygons roof_base_areas;
ExPolygons& roof_1st_layer = ts_layer->roof_1st_layer;
ExPolygons& floor_areas = ts_layer->floor_areas;
ExPolygons& roof_gap_areas = ts_layer->roof_gap_areas;
coordf_t max_layers_above_base = 0;
coordf_t max_layers_above_roof = 0;
coordf_t max_layers_above_roof1 = 0;
size_t first_base_roof_area = 0;
bool floor_interface_as_base = false;
bool has_circle_node = false;
bool need_extra_wall = false;
@@ -2094,8 +2090,6 @@ void TreeSupport::draw_circles()
break;
const SupportNode& node = *p_node;
// ORCA: Cap top interface height in mm based on per-node support layer height.
const coordf_t top_interface_height = coordf_t(top_interface_layers) * node.height;
ExPolygons area;
// Generate directly from overhang polygon if one of the following is true:
// 1) node is a normal part of hybrid support
@@ -2159,18 +2153,16 @@ void TreeSupport::draw_circles()
if (obj_layer_nr>0 && node.distance_to_top < 0)
append(roof_gap_areas, area);
// ORCA: Roof1stLayer must also fit inside the mm cap.
else if (obj_layer_nr > 0 && node.support_roof_layers_below == 1 &&
(node.dist_mm_to_top - this->top_z_distance) < top_interface_height + EPSILON && node.is_sharp_tail==false)
node.is_sharp_tail == false)
{
append(roof_1st_layer, area);
max_layers_above_roof1 = std::max(max_layers_above_roof1, node.dist_mm_to_top);
}
// ORCA: Roof layers must also fit inside the mm cap.
else if (obj_layer_nr > 0 && node.support_roof_layers_below > 1 &&
(node.dist_mm_to_top - this->top_z_distance) < top_interface_height + EPSILON && node.is_sharp_tail == false)
node.is_sharp_tail == false)
{
append(roof_areas, area);
append(node.support_roof_layers_below <= top_base_interface_layers ? roof_base_areas : roof_areas, area);
max_layers_above_roof = std::max(max_layers_above_roof, node.dist_mm_to_top);
}
else
@@ -2184,9 +2176,17 @@ void TreeSupport::draw_circles()
//m_object->print()->set_status(65, (boost::format( _u8L("Support: generate polygons at layer %d")) % layer_nr).str());
// join roof segments
roof_areas = diff_clipped(offset2_ex(roof_areas, line_width_scaled, -line_width_scaled), get_collision(false));
roof_areas = diff_clipped(closing_ex(roof_areas, line_width_scaled), get_collision(false));
roof_areas = intersection_ex(roof_areas, m_machine_border);
roof_1st_layer = diff_clipped(offset2_ex(roof_1st_layer, line_width_scaled, -line_width_scaled), get_collision(false));
roof_base_areas = diff_clipped(closing_ex(roof_base_areas, line_width_scaled), get_collision(false));
roof_base_areas = intersection_ex(roof_base_areas, m_machine_border);
if (!roof_base_areas.empty() && !roof_areas.empty())
roof_base_areas = diff_ex(roof_base_areas,
ClipperUtils::clip_clipper_polygons_with_subject_bbox(roof_areas, get_extents(roof_base_areas)));
first_base_roof_area = roof_areas.size();
append(roof_areas, std::move(roof_base_areas));
roof_1st_layer = diff_clipped(closing_ex(roof_1st_layer, line_width_scaled), get_collision(false));
// roof_1st_layer and roof_areas may intersect, so need to subtract roof_areas from roof_1st_layer
roof_1st_layer = diff_ex(roof_1st_layer, ClipperUtils::clip_clipper_polygons_with_subject_bbox(roof_areas,get_extents(roof_1st_layer)));
@@ -2366,9 +2366,11 @@ void TreeSupport::draw_circles()
area_groups.back().need_infill = overlaps({ expoly }, area_poly);
area_groups.back().need_extra_wall = need_extra_wall && !area_groups.back().need_infill;
}
for (auto& expoly : ts_layer->roof_areas) {
for (size_t roof_idx = 0; roof_idx < ts_layer->roof_areas.size(); ++roof_idx) {
auto &expoly = ts_layer->roof_areas[roof_idx];
//if (area(expoly) < SQ(scale_(1))) continue;
area_groups.emplace_back(&expoly, SupportLayer::RoofType, max_layers_above_roof);
area_groups.back().interface_as_base = roof_idx >= first_base_roof_area;
}
for (auto &expoly : ts_layer->floor_areas) {
//if (area(expoly) < SQ(scale_(1))) continue;
@@ -2378,6 +2380,7 @@ void TreeSupport::draw_circles()
for (auto &expoly : ts_layer->roof_1st_layer) {
//if (area(expoly) < SQ(scale_(1))) continue;
area_groups.emplace_back(&expoly, SupportLayer::Roof1stLayer, max_layers_above_roof1);
area_groups.back().interface_as_base = top_base_interface_layers > 0;
}
for (auto &area_group : area_groups) {
@@ -2406,7 +2409,6 @@ void TreeSupport::draw_circles()
}
});
// ORCA: normalize interface_id sequencing to follow printed interface layers only.
const int top_base_layers = int(m_support_params.num_top_base_interface_layers);
const bool interlaced = m_object_config->support_interface_pattern == smipRectilinearInterlaced;
int roof_interface_id = 0;
int floor_interface_id = 0;
@@ -2425,7 +2427,6 @@ void TreeSupport::draw_circles()
if (area_group.type == SupportLayer::RoofType || area_group.type == SupportLayer::Roof1stLayer) {
if (interlaced)
area_group.interface_id = roof_interface_id;
area_group.interface_as_base = top_base_layers > 0 && roof_interface_id < top_base_layers;
has_roof_interface = true;
} else if (area_group.type == SupportLayer::FloorType) {
if (interlaced)
@@ -2897,7 +2898,7 @@ void TreeSupport::drop_nodes()
node_parent->merged_neighbours.push_front(node_parent == p_node ? neighbour : p_node);
const bool to_buildplate = !is_inside_ex(get_collision(0, obj_layer_nr_next), next_position);
SupportNode* next_node = m_ts_data->create_node(next_position, node_parent->distance_to_top + 1, obj_layer_nr_next,
node_parent->support_roof_layers_below - (node_parent->distance_to_top > 0 ? 1 : 0),
node_parent->support_roof_layers_below - (node_parent->distance_to_top >= 0 ? 1 : 0),
to_buildplate, node_parent, print_z_next, height_next);
get_max_move_dist(next_node);
m_ts_data->m_mutex.lock();
@@ -2949,7 +2950,7 @@ void TreeSupport::drop_nodes()
for(auto& overhang:overhangs_next) {
Point next_pt = overhang.contour.centroid();
SupportNode *next_node = m_ts_data->create_node(next_pt, p_node->distance_to_top + 1, obj_layer_nr_next,
p_node->support_roof_layers_below - (p_node->distance_to_top > 0 ? 1 : 0),
p_node->support_roof_layers_below - (p_node->distance_to_top >= 0 ? 1 : 0),
to_buildplate, p_node, print_z_next, height_next);
next_node->max_move_dist = 0;
next_node->overhang = std::move(overhang);
@@ -3096,7 +3097,7 @@ void TreeSupport::drop_nodes()
auto next_collision = get_collision(0, obj_layer_nr_next);
const bool to_buildplate = !is_inside_ex(m_ts_data->m_layer_outlines[obj_layer_nr_next], next_layer_vertex);
SupportNode * next_node = m_ts_data->create_node(next_layer_vertex, node.distance_to_top + 1, obj_layer_nr_next,
node.support_roof_layers_below - (node.distance_to_top > 0 ? 1 : 0),
node.support_roof_layers_below - (node.distance_to_top >= 0 ? 1 : 0),
to_buildplate, p_node, print_z_next, height_next);
// don't increase radius if next node will collide partially with the object (STUDIO-7883)
to_outside = projection_onto(next_collision, next_node->position);
@@ -3376,21 +3377,6 @@ std::vector<LayerHeightData> TreeSupport::plan_layer_heights()
}
}
// ORCA: Recompute support_roof_layers_below from remaining interface height (independent heights).
const int top_layers = m_object->config().support_interface_top_layers.value;
if (m_support_params.independent_layer_height && top_layers > 0) {
const coordf_t interface_height_mm = coordf_t(top_layers) * m_slicing_params.layer_height;
for (int layer_nr = 0; layer_nr < contact_nodes.size(); layer_nr++) {
if (contact_nodes[layer_nr].empty()) continue;
for (SupportNode *node : contact_nodes[layer_nr]) {
if (node->height <= EPSILON) continue;
const coordf_t remaining_mm = interface_height_mm - (node->dist_mm_to_top - this->top_z_distance);
const int layers_fit = remaining_mm < -EPSILON ? 0 : int(std::floor((remaining_mm + EPSILON) / node->height));
node->support_roof_layers_below = std::min(layers_fit, top_layers);
}
}
}
// log layer_heights
for (size_t i = 0; i < layer_heights.size(); i++) {
//if (layer_heights[i].height > EPSILON)
@@ -3498,7 +3484,7 @@ void TreeSupport::generate_contact_points()
if (force_add || !already_inserted.count(hash_pos)) {
already_inserted.emplace(hash_pos);
bool to_buildplate = true;
size_t roof_layers = add_interface ? (support_roof_layers > 0 ? support_roof_layers - 1 : 0) : 0; // subtract 1 because the contact node itself counts as one layer
size_t roof_layers = add_interface ? support_roof_layers : 0;
// add a new node as a virtual node which acts as the invisible gap between support and object
// distance_to_top=-1: it's virtual
// print_z=object_layer->bottom_z: it directly contacts the bottom
+1 -1
View File
@@ -706,7 +706,7 @@ static std::optional<std::pair<Point, size_t>> polyline_sample_next_point_at_dis
filler->spacing = flow.spacing();
filler->angle = roof ?
//fixme support_layer.interface_id() instead of layer_idx
(support_params.interface_angle + (layer_idx & 1) ? float(- M_PI / 4.) : float(+ M_PI / 4.)) :
(support_params.interface_angle + ((layer_idx & 1) ? float(- M_PI_4) : float(+ M_PI_4))) :
support_params.base_angle;
// ORCA: use top-specific interface density after separating top/bottom settings.
+2 -2
View File
@@ -62,7 +62,7 @@ struct TreeSupportMeshGroupSettings {
this->support_line_width = support_material_flow(&print_object, config.layer_height).scaled_width();
this->support_roof_line_width = support_material_interface_flow(&print_object, config.layer_height).scaled_width();
const int bottom_interface_layers = number_of_support_interface_bottom_layers(config);
this->support_bottom_enable = config.support_interface_top_layers.value > 0 && bottom_interface_layers > 0;
this->support_bottom_enable = bottom_interface_layers > 0;
this->support_bottom_height = this->support_bottom_enable ?
bottom_interface_layers * this->layer_height :
0;
@@ -705,7 +705,7 @@ public:
SupportGeneratorLayersPtr& top_contacts_mutable() { return this->top_contacts; }
public:
// Insert the contact layer and some of the inteface and base interface layers below.
// Insert the contact layer and some of the interface and base interface layers below.
void add_roofs(std::vector<Polygons> &&new_roofs, const size_t insert_layer_idx)
{
if (! new_roofs.empty()) {
+5
View File
@@ -51,4 +51,9 @@
// Enable extension of tool position imgui dialog to show actual speed profile
#define ENABLE_ACTUAL_SPEED_DEBUG 1
// Disable layout inspector for public release
#if BBL_RELEASE_TO_PUBLIC
#define WXINSPECTOR_DISABLE
#endif
#endif // _prusaslicer_technologies_h_
+12
View File
@@ -42,10 +42,22 @@ struct Calib_Params
std::string shaper_type;
std::vector<double> accelerations;
std::vector<double> speeds;
// Resolved layer height for the VFA tower (0 = auto: nozzle_diameter / 2). Each speed block is a
// fixed number of layers tall, so this also determines the physical block height / tower height.
double vfa_layer_height = 0.0;
// Scale the calibration model to the nozzle diameter and set the layer height accordingly (temp tower / VFA).
// When false the 0.4 mm / 0.2 mm reference model is printed as-is.
bool nozzle_based_resize = true;
CalibMode mode;
};
// Number of printed layers per speed block in the VFA tower. The base model has 5 mm blocks designed
// for a 0.2 mm layer height (0.4 mm nozzle), i.e. 25 layers per block.
static constexpr int vfa_layers_per_block = 25;
static constexpr double vfa_base_block_height = 5.0;
static constexpr double vfa_base_nozzle_diameter = 0.4;
enum FlowRatioCalibrationType {
COMPLETE_CALIBRATION = 0,
FINE_CALIBRATION,