Compare commits

..
Author SHA1 Message Date
SoftFever 3880f2c58e Bump version to 2.5.0-alpha 2026-10-16 18:52:11 +08:00
22 changed files with 184 additions and 759 deletions

No files matched your search

-13
View File
@@ -288,19 +288,6 @@ if (APPLE)
# Type"); Ninja/-Wall does not, and under -Werror it fails Xcode builds on code CI accepts.
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_64_TO_32_BIT_CONVERSION "NO")
# The Xcode generator ignores CMAKE_<LANG>_COMPILER_LAUNCHER, so hand the launcher to
# Xcode's own Compiler Launcher setting, which wraps every clang compile (C, C++,
# Objective-C++ and the precompiled header) with one program: the C++ launcher wins when
# both are set. The setting takes a single program path, not a CMake launcher list.
# Xcode versions without it ignore it and build uncached.
if (CMAKE_GENERATOR STREQUAL "Xcode")
if (CMAKE_CXX_COMPILER_LAUNCHER)
set(CMAKE_XCODE_ATTRIBUTE_C_COMPILER_LAUNCHER "${CMAKE_CXX_COMPILER_LAUNCHER}")
elseif (CMAKE_C_COMPILER_LAUNCHER)
set(CMAKE_XCODE_ATTRIBUTE_C_COMPILER_LAUNCHER "${CMAKE_C_COMPILER_LAUNCHER}")
endif ()
endif ()
message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}")
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(CMAKE_INSTALL_RPATH "$ORIGIN")
-50
View File
@@ -111,54 +111,6 @@ else
export CMAKE_POLICY_COMPAT=""
fi
# Compiler cache, as in build_linux.sh. The program comes from CMAKE_CCACHE, else from a
# launcher already set in the environment (the CI build sets ccache there), else sccache or
# ccache from PATH; one program serves both languages. Each run applies its own choice to the
# build directories, replacing a launcher an earlier run or a hand-run configure recorded.
CMAKE_CCACHE_ARGS=()
CCACHE_VAR=""
for var in CMAKE_CCACHE CMAKE_CXX_COMPILER_LAUNCHER CMAKE_C_COMPILER_LAUNCHER; do
if [ -n "${!var:-}" ]; then
CCACHE_VAR=$var
break
fi
done
if [ -n "$CCACHE_VAR" ]; then
CCACHE_PROGRAM=${!CCACHE_VAR}
echo "Checking ${CCACHE_VAR} environment variable for compiler cache program..."
CMAKE_CCACHE=$(command -v "${CCACHE_PROGRAM}") || {
echo "${CCACHE_VAR} environment variable is set to '${CCACHE_PROGRAM}' but it was not found in PATH."
CMAKE_CCACHE=""
}
elif command -v sccache >/dev/null 2>&1 ; then
CMAKE_CCACHE=$(command -v sccache)
elif command -v ccache >/dev/null 2>&1 ; then
CMAKE_CCACHE=$(command -v ccache)
fi
if [ -n "${CMAKE_CCACHE}" ] ; then
echo "${CMAKE_CCACHE} found, enabling compiler caching..."
CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER="${CMAKE_CCACHE}" -DCMAKE_CXX_COMPILER_LAUNCHER="${CMAKE_CCACHE}")
# The same ccache settings as the CI build. Without this sloppiness ccache refuses every
# compile that uses the precompiled header, which is most of OrcaSlicer, and skips headers
# a checkout has just written. It is added to the sloppiness already configured, and
# CCACHE_SLOPPINESS or CCACHE_(NO)DEPEND set in the environment win.
if [[ "$(basename "${CMAKE_CCACHE}")" == ccache ]] ; then
if [[ -z "${CCACHE_SLOPPINESS+x}" ]] ; then
CCACHE_SLOPPINESS=$("${CMAKE_CCACHE}" --get-config sloppiness 2>/dev/null || true)
export CCACHE_SLOPPINESS="${CCACHE_SLOPPINESS:+${CCACHE_SLOPPINESS},}pch_defines,time_macros,include_file_mtime,include_file_ctime"
fi
# Hash the includes the compiler reports instead of preprocessing every miss.
if [[ -z "${CCACHE_DEPEND+x}${CCACHE_NODEPEND+x}" ]] ; then
export CCACHE_DEPEND=1
fi
fi
else
echo "Note: ccache or sccache are not found. Install either of them for faster rebuilds."
# Clear a launcher recorded by an earlier configure. The entries must be set empty rather
# than removed: CMake fills a missing launcher entry from the environment.
CMAKE_CCACHE_ARGS=(-DCMAKE_C_COMPILER_LAUNCHER:STRING= -DCMAKE_CXX_COMPILER_LAUNCHER:STRING=)
fi
echo "Build params:"
echo " - ARCH: $ARCH"
echo " - BUILD_CONFIG: $BUILD_CONFIG"
@@ -210,7 +162,6 @@ function build_deps() {
-DCMAKE_OSX_ARCHITECTURES:STRING="${_ARCH}" \
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
${CMAKE_POLICY_COMPAT}
fi
cmake --build . --config "$BUILD_CONFIG" --target deps
@@ -313,7 +264,6 @@ function build_slicer() {
-DCMAKE_OSX_ARCHITECTURES="${_ARCH}" \
-DCMAKE_OSX_DEPLOYMENT_TARGET="${OSX_DEPLOYMENT_TARGET}" \
-DCMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH}" \
"${CMAKE_CCACHE_ARGS[@]}" \
${CMAKE_POLICY_COMPAT}
fi
cmake --build . --config "$BUILD_CONFIG" --target "$SLICER_BUILD_TARGET"
@@ -45,10 +45,28 @@
<color type="primary" scheme_preference="dark">#00695C</color>
</branding>
<releases>
<release version="2.5.0-dev" date="2026-06-22" type="development">
<url type="details">https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds</url>
<release version="2.5.0-alpha" date="2026-10-14" type="development">
<url type="details">https://www.orcaslicer.com/wiki/releases/release_2_5_0_alpha</url>
<description>
<p>See the release page for detailed changelog.</p>
<p>See the release page for detailed changelog of OrcaSlicer 2.5.0-alpha release.</p>
</description>
</release>
<release version="2.4.2" date="2026-07-26">
<url type="details">https://www.orcaslicer.com/wiki/releases/release_2_4_2</url>
<description>
<p>See the release page for detailed changelog of OrcaSlicer 2.4.2 release.</p>
</description>
</release>
<release version="2.4.1" date="2026-06-26">
<url type="details">https://www.orcaslicer.com/wiki/releases/release_2_4_1</url>
<description>
<p>See the release page for detailed changelog of OrcaSlicer 2.4.1 release.</p>
</description>
</release>
<release version="2.4.0" date="2026-06-20">
<url type="details">https://www.orcaslicer.com/wiki/releases/release_2_4_0</url>
<description>
<p>See the release page for detailed changelog of OrcaSlicer 2.4.0 release.</p>
</description>
</release>
<release version="2.3.2" date="2025-03-23">
+7 -3
View File
@@ -812,8 +812,7 @@ bool WallToolPaths::removeEmptyToolPaths(std::vector<VariableWidthLines> &toolpa
* Get the order constraints of the insets when printing walls per region / hole.
* Each returned pair consists of adjacent wall lines where the left has an inset_idx one lower than the right.
*
* Odd walls follow the same order as even ones, so that the centre line of a narrow wall is an inner wall
* like any other when printing inner to outer.
* Odd walls should always go after their enclosing wall polygons.
*
* \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one.
*/
@@ -887,7 +886,12 @@ WallToolPaths::ExtrusionLineSet WallToolPaths::getRegionOrder(const std::vector<
continue; // not directly adjacent
if (!shorter_then(loc_here - lineloc_nearby.j.p, (lineloc_here.j.w + lineloc_nearby.j.w) / 2 * diagonal_extension))
continue; // points are too far away from each other
if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) {
if (here->is_odd || nearby->is_odd) {
if (here->is_odd && !nearby->is_odd && nearby->inset_idx < here->inset_idx)
order_requirements.emplace(std::make_pair(nearby, here));
if (nearby->is_odd && !here->is_odd && here->inset_idx < nearby->inset_idx)
order_requirements.emplace(std::make_pair(here, nearby));
} else if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) {
order_requirements.emplace(std::make_pair(nearby, here));
} else {
assert((nearby->inset_idx > here->inset_idx) == outer_to_inner);
-10
View File
@@ -3985,16 +3985,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
this->placeholder_parser().set("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder)));
this->placeholder_parser().set("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder)));
this->placeholder_parser().set("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
// The final print statistics are known only after the whole G-code is generated, so they are filled in during post-processing too.
// The running extruded_* totals are left alone; profiles use them as numbers.
this->placeholder_parser().set("used_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
this->placeholder_parser().set("print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder)));
this->placeholder_parser().set("normal_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder)));
this->placeholder_parser().set("silent_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Silent_Print_Time_Placeholder)));
this->placeholder_parser().set("total_weight", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Weight_Placeholder)));
this->placeholder_parser().set("total_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Cost_Placeholder)));
this->placeholder_parser().set("total_wipe_tower_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Cost_Placeholder)));
this->placeholder_parser().set("total_wipe_tower_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Filament_Placeholder)));
// Sync variant-mapped params into placeholder_parser before processing start gcode
update_placeholder_parser_with_variant_params();
+71 -47
View File
@@ -120,13 +120,7 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags = {
"@PRINT_TIME_HOUR@",
"@PRINT_TIME_MINUTE@",
"@PRINT_TIME_SEC@",
"@USED_FILAMENT_LENGTH@",
"@NORMAL_PRINT_TIME@",
"@SILENT_PRINT_TIME@",
"@TOTAL_WEIGHT@",
"@TOTAL_COST@",
"@TOTAL_WIPE_TOWER_COST@",
"@TOTAL_WIPE_TOWER_FILAMENT@"
"@USED_FILAMENT_LENGTH@"
};
const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
@@ -153,13 +147,7 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
"@PRINT_TIME_HOUR@",
"@PRINT_TIME_MINUTE@",
"@PRINT_TIME_SEC@",
"@USED_FILAMENT_LENGTH@",
"@NORMAL_PRINT_TIME@",
"@SILENT_PRINT_TIME@",
"@TOTAL_WEIGHT@",
"@TOTAL_COST@",
"@TOTAL_WIPE_TOWER_COST@",
"@TOTAL_WIPE_TOWER_FILAMENT@"
"@USED_FILAMENT_LENGTH@"
};
@@ -1276,22 +1264,19 @@ void GCodeProcessor::run_post_process()
return ret;
};
// Process inline placeholders (print_time_*, used_filament_length and the print statistics known only after export)
// Process inline placeholders (print_time_total_sec, print_time_day, print_time_hour, print_time_minute, print_time_sec and used_filament_length)
auto process_inline_placeholders = [&](std::string& gcode_line) {
bool processed = false;
// Every inline placeholder contains '@', so a line without one has nothing to replace.
if (gcode_line.find('@') == std::string::npos)
return processed;
auto replace_tag = [&gcode_line, &processed](ETags tag, const std::string& value) {
const std::string& placeholder = reserved_tag(tag);
size_t pos = gcode_line.find(placeholder);
while (pos != std::string::npos) {
gcode_line.replace(pos, placeholder.length(), value);
processed = true;
pos = gcode_line.find(placeholder, pos + value.length());
}
};
const std::string& print_time_total_placeholder = reserved_tag(ETags::Print_Time_Total_Sec_Placeholder);
const std::string& print_time_day_placeholder = reserved_tag(ETags::Print_Time_Day_Placeholder);
const std::string& print_time_hour_placeholder = reserved_tag(ETags::Print_Time_Hour_Placeholder);
const std::string& print_time_minute_placeholder = reserved_tag(ETags::Print_Time_Minute_Placeholder);
const std::string& print_time_sec_placeholder = reserved_tag(ETags::Print_Time_Sec_Placeholder);
const std::string& used_filament_placeholder = reserved_tag(ETags::Used_Filament_Length_Placeholder);
double print_time_total_sec = m_time_processor.machines[static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal)].time;
if (print_time_total_sec < 0.0)
@@ -1304,31 +1289,70 @@ void GCodeProcessor::run_post_process()
int print_time_minute = (day_remainder_seconds % 3600) / 60;
int print_time_sec = day_remainder_seconds % 60;
double total_filament_mm = 0.0;
for (const auto& mm : filament_mm) {
total_filament_mm += mm;
// Replace print_time_total_sec
size_t pos = gcode_line.find(print_time_total_placeholder);
while (pos != std::string::npos) {
char buf[64];
sprintf(buf, "%.2f", print_time_total_sec);
gcode_line.replace(pos, print_time_total_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_total_placeholder, pos + strlen(buf));
}
replace_tag(ETags::Print_Time_Total_Sec_Placeholder, Slic3r::float_to_string_decimal_point(print_time_total_sec, 2));
replace_tag(ETags::Print_Time_Day_Placeholder, std::to_string(print_time_day));
replace_tag(ETags::Print_Time_Hour_Placeholder, std::to_string(print_time_hour));
replace_tag(ETags::Print_Time_Minute_Placeholder, std::to_string(print_time_minute));
replace_tag(ETags::Print_Time_Sec_Placeholder, std::to_string(print_time_sec));
replace_tag(ETags::Used_Filament_Length_Placeholder,
Slic3r::float_to_string_decimal_point(total_filament_mm / 1000.0, 2)); // Convert mm to m
// Same values as the print statistics that filename_format gets (PrintStatistics::config(), filled in by
// update_print_estimated_stats() in GCode.cpp), which runs only after this post-processing.
replace_tag(ETags::Normal_Print_Time_Placeholder, get_time_dhm(PrintEstimatedStatistics::ETimeMode::Normal));
replace_tag(ETags::Silent_Print_Time_Placeholder, is_stealth_time_estimator_enabled() ?
get_time_dhm(PrintEstimatedStatistics::ETimeMode::Stealth) :
short_time("N/A"));
const double total_cost = filament_total_cost + m_print->config().time_cost.value * print_time_total_sec / 3600.0;
replace_tag(ETags::Total_Weight_Placeholder, Slic3r::float_to_string_decimal_point(filament_total_g, 2));
replace_tag(ETags::Total_Cost_Placeholder, Slic3r::float_to_string_decimal_point(total_cost, 2));
replace_tag(ETags::Total_Wipe_Tower_Cost_Placeholder,
Slic3r::float_to_string_decimal_point(m_print->print_statistics().total_wipe_tower_cost, 2));
replace_tag(ETags::Total_Wipe_Tower_Filament_Placeholder,
Slic3r::float_to_string_decimal_point(m_print->print_statistics().total_wipe_tower_filament, 2));
// Replace print_time_day
pos = gcode_line.find(print_time_day_placeholder);
while (pos != std::string::npos) {
char buf[64];
sprintf(buf, "%d", print_time_day);
gcode_line.replace(pos, print_time_day_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_day_placeholder, pos + strlen(buf));
}
// Replace print_time_hour
pos = gcode_line.find(print_time_hour_placeholder);
while (pos != std::string::npos) {
char buf[64];
sprintf(buf, "%d", print_time_hour);
gcode_line.replace(pos, print_time_hour_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_hour_placeholder, pos + strlen(buf));
}
// Replace print_time_minute
pos = gcode_line.find(print_time_minute_placeholder);
while (pos != std::string::npos) {
char buf[64];
sprintf(buf, "%d", print_time_minute);
gcode_line.replace(pos, print_time_minute_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_minute_placeholder, pos + strlen(buf));
}
// Replace print_time_sec
pos = gcode_line.find(print_time_sec_placeholder);
while (pos != std::string::npos) {
char buf[64];
sprintf(buf, "%d", print_time_sec);
gcode_line.replace(pos, print_time_sec_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_sec_placeholder, pos + strlen(buf));
}
// Replace used_filament_length
pos = gcode_line.find(used_filament_placeholder);
while (pos != std::string::npos) {
double total_filament_mm = 0.0;
for (const auto& mm : filament_mm) {
total_filament_mm += mm;
}
double used_filament_length = total_filament_mm / 1000.0; // Convert mm to m
char buf[64];
sprintf(buf, "%.2f", used_filament_length);
gcode_line.replace(pos, used_filament_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(used_filament_placeholder, pos + strlen(buf));
}
return processed;
};
-6
View File
@@ -601,12 +601,6 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
Print_Time_Minute_Placeholder,
Print_Time_Sec_Placeholder,
Used_Filament_Length_Placeholder,
Normal_Print_Time_Placeholder,
Silent_Print_Time_Placeholder,
Total_Weight_Placeholder,
Total_Cost_Placeholder,
Total_Wipe_Tower_Cost_Placeholder,
Total_Wipe_Tower_Filament_Placeholder,
};
static const std::string& reserved_tag(ETags tag) { return s_IsBBLPrinter ? Reserved_Tags[static_cast<unsigned char>(tag)] : Reserved_Tags_compatible[static_cast<unsigned char>(tag)]; }
+24 -65
View File
@@ -62,12 +62,7 @@ public:
// Depth in the hierarchy. External perimeter has depth = 0. An external perimeter could be both a contour and a hole.
unsigned short depth;
// ORCA: an external hole perimeter touching the next outer wall all around, with nothing between them.
// E.g. a 1mm tube wall with 0.5mm walls: 2 lines, the contour's outer wall and the hole's.
bool is_thin_wall_hole = false;
// ORCA: an external hole perimeter with no room for an inner wall beside it, at most gap fill. Every thin wall
// hole is one, and so is e.g. a 1.5mm tube wall with 0.5mm walls: 3 lines, the two outer walls and gap fill
// between them. A 2mm tube wall has room for an inner wall a side and is not. Only set for Inner/Outer/Inner.
bool is_narrow_wall_hole = false;
// Children contour, may be both CCW and CW oriented (outer contours or holes).
std::vector<PerimeterGeneratorLoop> children;
@@ -577,8 +572,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
extrusion_coll.append(std::move(extrusion_loop));
// Orca: Reverse the order of paths for thin wall holes. We define thin wall hole as a hole with only one perimeter.
const bool thin_wall_hole = !pg_extrusion.is_contour && pg_extrusions.size() == 2;
if (thin_wall_hole && perimeter_generator.config->wall_sequence != WallSequence::OuterInner &&
!perimeter_generator.outer_walls_first_for_brim())
if (thin_wall_hole && perimeter_generator.config->wall_sequence != WallSequence::OuterInner)
std::reverse(extrusion_coll.entities.begin(), extrusion_coll.entities.end());
}
else {
@@ -1696,24 +1690,13 @@ void PerimeterGenerator::process_classic()
last = std::move(offsets);
// ORCA: a thin wall hole has no room for gap fill or an inner wall anywhere beside its outer wall, a
// narrow wall hole none for an inner wall, the room the next loop is offset by below.
// ORCA: a thin wall hole has no room for gap fill or an inner wall anywhere beside its outer wall.
if (i == 0 && ! holes[0].empty()) {
auto reach = [&last](double room) {
return to_polygons(offset2_ex(last, -float(room), float(room) + float(SCALED_EPSILON)));
};
auto out_of_reach = [](const Polygon &hole, const Polygons &reach) {
return intersection_pl(Polylines{ hole.split_at_first_point() },
ClipperUtils::clip_clipper_polygons_with_subject_bbox(reach, get_extents(hole).inflated(SCALED_EPSILON))).empty();
};
// Only Inner/Outer/Inner orders narrow wall holes apart.
const bool narrow_holes_ordered = this->config->wall_sequence == WallSequence::InnerOuterInner;
const Polygons gap_fill_reach = reach(0.5 * (ext_perimeter_spacing2 + gap_fill_min_width));
const Polygons inner_wall_reach = narrow_holes_ordered ? reach(ext_perimeter_spacing2 + min_spacing / 2.) : Polygons();
for (PerimeterGeneratorLoop &hole : holes[0]) {
hole.is_thin_wall_hole = out_of_reach(hole.polygon, gap_fill_reach);
hole.is_narrow_wall_hole = narrow_holes_ordered && out_of_reach(hole.polygon, inner_wall_reach);
}
const float room = float(0.5 * (ext_perimeter_spacing2 + gap_fill_min_width));
const Polygons reach = to_polygons(offset2_ex(last, -room, room + float(SCALED_EPSILON)));
for (PerimeterGeneratorLoop &hole : holes[0])
hole.is_thin_wall_hole = intersection_pl(Polylines{ hole.polygon.split_at_first_point() },
ClipperUtils::clip_clipper_polygons_with_subject_bbox(reach, get_extents(hole.polygon).inflated(SCALED_EPSILON))).empty();
}
//BBS: refer to superslicer
@@ -1852,16 +1835,18 @@ void PerimeterGenerator::process_classic()
// if brim will be printed, reverse the order of perimeters so that
// we continue inwards after having finished the brim
// TODO: add test for perimeter order
bool is_outer_wall_first = this->config->wall_sequence == WallSequence::OuterInner;
// Orca: a contour with only a narrow wall hole beside it has nothing to sandwich, and prints the hole first,
// then the contour, then the gap fill between them, as Arachne does.
const PerimeterGeneratorLoops &island_loops = contours.front();
const bool narrow_wall_hole_island = island_loops.size() == 1 && island_loops.front().children.size() == 1 &&
island_loops.front().children.front().is_narrow_wall_hole;
if (is_outer_wall_first || this->outer_walls_first_for_brim())
if (is_outer_wall_first ||
//BBS: always print outer wall first when there indeed has brim.
// btLeadingEdgeOnly is an outer brim too (a belt brim at the part's first contact).
(this->layer_id == 0 &&
(this->object_config->brim_type == BrimType::btOuterOnly ||
this->object_config->brim_type == BrimType::btLeadingEdgeOnly) &&
this->object_config->brim_width.value > 0))
entities.reverse();
// Orca: sandwich mode. Apply after 1st layer.
else if ((this->config->wall_sequence == WallSequence::InnerOuterInner) && layer_id > 0 && !narrow_wall_hole_island){
else if ((this->config->wall_sequence == WallSequence::InnerOuterInner) && layer_id > 0){
entities.reverse(); // reverse all entities - order them from external to internal
if(entities.entities.size()>2){ // 3 walls minimum needed to do inner outer inner ordering
int position = 0; // index to run the re-ordering for multiple external perimeters in a single island.
@@ -2427,25 +2412,11 @@ std::vector<PerimeterGeneratorArachneExtrusion> reorderPerimetersByProximity(std
// Function to reorder perimeters starting from a given reference index
auto reorderFromReference = [&](int referenceIndex) {
// Find first level touching perimeters. The odd centre line of a wall too narrow for two inner walls is shared
// by the outer walls on both sides, so those join this island and are printed before it too, holes first.
std::vector<int> outerIndices{referenceIndex};
std::vector<int> firstLevelTouchingIndices;
for (size_t next = 0; next < outerIndices.size(); ++next)
for (int idx : findAllTouchingPerimeters(entities, {outerIndices[next]}, threshold_external, threshold_internal, 1)) {
if (std::find(firstLevelTouchingIndices.begin(), firstLevelTouchingIndices.end(), idx) != firstLevelTouchingIndices.end())
continue;
firstLevelTouchingIndices.push_back(idx);
if (!entities[idx].extrusion->is_odd)
continue;
for (size_t i = 0; i < entities.size(); ++i)
if (entities[i].extrusion->inset_idx == 0 && includedIndices.count(int(i)) == 0 &&
std::find(outerIndices.begin(), outerIndices.end(), int(i)) == outerIndices.end() &&
arachne_lines_touch(*entities[i].extrusion, *entities[idx].extrusion, double(threshold_external)))
outerIndices.push_back(int(i));
}
std::stable_partition(outerIndices.begin(), outerIndices.end(),
[&entities](int idx) { return entities[idx].extrusion->is_closed && !entities[idx].is_contour; });
std::unordered_set<int> firstLevelIndices;
firstLevelIndices.insert(referenceIndex);
// Find first level touching perimeters
std::vector<int> firstLevelTouchingIndices = findAllTouchingPerimeters(entities, firstLevelIndices, threshold_external, threshold_internal, 1);
// Bring the largest first level perimeter to the front
// The longest first neighbour is most likely the dominant proximate perimeter
// hence printing it immediately after the external perimeter should speed things up
@@ -2456,10 +2427,8 @@ std::vector<PerimeterGeneratorArachneExtrusion> reorderPerimetersByProximity(std
std::iter_swap(maxIt, firstLevelTouchingIndices.end() - 1);
}
// Insert first level perimeters into reordered list
for (int idx : outerIndices) {
reordered.push_back(entities[idx]);
includedIndices.insert(idx);
}
reordered.push_back(entities[referenceIndex]);
includedIndices.insert(referenceIndex);
for (int idx : firstLevelTouchingIndices) {
if (includedIndices.count(idx) == 0) {
@@ -2788,7 +2757,7 @@ void PerimeterGenerator::process_arachne()
if (layer_id == 0){ // disable inner outer inner algorithm after the first layer
is_outer_wall_first =
this->config->wall_sequence == WallSequence::OuterInner || this->outer_walls_first_for_brim();
this->config->wall_sequence == WallSequence::OuterInner;
}
if (is_outer_wall_first) {
start_perimeter = 0;
@@ -3073,16 +3042,6 @@ ExPolygons PerimeterGenerator::printable_slices(const ExPolygons &slices) const
return min_width > EPSILON ? opening_ex(slices, float(scale_(min_width / 2.))) : slices;
}
bool PerimeterGenerator::outer_walls_first_for_brim() const
{
return layer_id == 0 && brim_puts_outer_walls_first(object_config->brim_type.value, object_config->brim_width.value);
}
bool brim_puts_outer_walls_first(BrimType brim_type, double brim_width)
{
return brim_width > 0 && (brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btLeadingEdgeOnly);
}
std::vector<Polygons> PerimeterGenerator::generate_lower_polygons_series(float width)
{
float nozzle_diameter = print_config->nozzle_diameter.get_at(config->outer_wall_filament_id - 1);
-6
View File
@@ -164,8 +164,6 @@ public:
Polygons lower_slices_polygons() const { return m_lower_slices_polygons; }
// ORCA: the slices less the slivers the wall generator prints nothing for, so they never count as support.
ExPolygons printable_slices(const ExPolygons &slices) const;
// ORCA: whether this layer prints its outer walls first for the brim, see brim_puts_outer_walls_first().
bool outer_walls_first_for_brim() const;
private:
std::vector<Polygons> generate_lower_polygons_series(float width);
@@ -185,10 +183,6 @@ private:
Polygons m_lower_slices_polygons;
};
// ORCA: a brim against the outer walls makes the first layer print them first, whatever the wall sequence, so the
// brim runs straight on into the outer wall. Auto, ear and painted brims may leave the outer walls bare.
bool brim_puts_outer_walls_first(BrimType brim_type, double brim_width);
}
#endif
-1
View File
@@ -2293,7 +2293,6 @@ void PresetCollection::load_project_embedded_presets(std::vector<Preset*>& proje
BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent for config %1%!")%preset->file;
continue;
}
extend_default_config_length(config, false, {});
preset->config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
//preset->config.apply(std::move(config));
Preset::normalize(preset->config);
+1 -8
View File
@@ -756,14 +756,7 @@ public:
Preset& get_edited_preset() { return m_edited_preset; }
const Preset& get_edited_preset() const { return m_edited_preset; }
// The root of the selected preset's inherits chain, or the selected preset itself when
// the chain names a preset that is not loaded (e.g. a vendor that is not installed).
const Preset& get_selected_preset_base() const
{
const Preset &selected = m_presets[m_idx_selected];
const Preset *base = get_preset_base(selected);
return base ? *base : selected;
}
const Preset& get_selected_preset_base() const { return *get_preset_base(m_presets[m_idx_selected]); }
// Return the last saved preset.
// const Preset& get_saved_preset() const { return m_saved_preset; }
+9 -8
View File
@@ -17,7 +17,6 @@
#include "I18N.hpp"
#include "Layer.hpp"
#include "MutablePolygon.hpp"
#include "PerimeterGenerator.hpp"
#include "PrintBase.hpp"
#include "PrintConfig.hpp"
#include "SLA/IndexedMesh.hpp"
@@ -1465,13 +1464,15 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "top_surface_speed") {
// Brim is printed below supports, support invalidates brim and skirt.
steps.emplace_back(posSupportMaterial);
if (opt_key == "brim_type" || opt_key == "brim_width") {
// Regenerate the walls when the brim starts or stops putting the first layer's outer walls first.
auto outer_walls_first = [](const ConfigOptionResolver &config) {
return brim_puts_outer_walls_first(config.option<ConfigOptionEnum<BrimType>>("brim_type")->value,
config.option<ConfigOptionFloat>("brim_width")->value);
};
if (outer_walls_first(old_config) != outer_walls_first(new_config))
if (opt_key == "brim_type") {
const auto* old_brim_type = old_config.option<ConfigOptionEnum<BrimType>>(opt_key);
const auto* new_brim_type = new_config.option<ConfigOptionEnum<BrimType>>(opt_key);
//BBS: When switch to manual brim, the object must have brim, then re-generate perimeter
//to make the wall order of first layer to be outer-first
// btLeadingEdgeOnly is printed as an outer brim (Brim.cpp, BeltBrim.cpp), so it
// takes part in the same first-layer wall order rule.
if (old_brim_type->value == btOuterOnly || new_brim_type->value == btOuterOnly ||
old_brim_type->value == btLeadingEdgeOnly || new_brim_type->value == btLeadingEdgeOnly)
steps.emplace_back(posPerimeters);
}
} else if (
+8 -5
View File
@@ -707,10 +707,8 @@ void tree_supports_generate_paths(
false);
}
if (eec) {
if (!eec->empty()) {
std::reverse(eec->entities.begin(), eec->entities.end());
dst.emplace_back(eec.release());
}
std::reverse(eec->entities.begin(), eec->entities.end());
dst.emplace_back(eec.release());
}
}
}
@@ -1827,7 +1825,12 @@ void generate_support_toolpaths(
no_sort = true;
} else if (support_params.support_style == SupportMaterialStyle::smsTreeOrganic &&
(config.support_base_pattern == smpNone || config.support_base_pattern == smpDefault)) {
tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow, support_params);
// Orca: A special case for the hollow Organic supports
// Orca: If the tree supports are too tall, use a double wall to make it stronger
SupportParameters support_params2 = support_params;
if (support_layer.print_z > 100.0)
support_params2.tree_branch_diameter_double_wall_area_scaled = 0.1;
tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow, support_params2);
done = true;
}
if (! done)
+22 -10
View File
@@ -2149,9 +2149,7 @@ void TreeSupport::draw_circles()
const bool with_lightning_infill = m_support_params.base_fill_pattern == ipLightning;
coordf_t support_extrusion_width = m_support_params.support_extrusion_width;
const float tree_brim_width = config.tree_support_brim_width.value;
// Reinforce branches moving more than half a support-line width between layers.
// Square the threshold for comparison with the squared XY movement below.
const double max_move_sq = sqr(double(scale_(m_object_config->support_line_width / 2)));
// Belt floor: the first object layer is not on a flat bed — it rests on the
// tilted, moving belt. So the first-object-layer adhesion features (the tree
// support brim, the hybrid first-layer base expansion) must be suppressed:
@@ -2203,7 +2201,7 @@ void TreeSupport::draw_circles()
size_t first_base_roof_area = 0;
bool floor_interface_as_base = false;
bool has_circle_node = false;
ExPolygons extra_wall_areas;
bool need_extra_wall = false;
ExPolygons collision_sharp_tails;
ExPolygons collision_base;
auto get_collision = [&](bool sharp_tail) -> ExPolygons &{
@@ -2281,6 +2279,7 @@ void TreeSupport::draw_circles()
// area = diff_clipped({ ExPolygon(circle) }, get_collision(node.is_sharp_tail && node.distance_to_top <= 0));
if (!area.empty()) has_circle_node = true;
if (node.need_extra_wall) need_extra_wall = true;
// merge overhang to get a smoother interface surface
// Do not merge when buildplate_only is on, because some underneath nodes may have been deleted.
@@ -2295,10 +2294,6 @@ void TreeSupport::draw_circles()
}
}
// Use the Pythagorean theorem to compare movement magnitude (dx² + dy²) without a square root.
if (node.is_processed && node.movement.cast<double>().squaredNorm() > max_move_sq)
append(extra_wall_areas, area);
if (obj_layer_nr>0 && node.distance_to_top < 0)
append(roof_gap_areas, area);
else if (obj_layer_nr > 0 && node.support_roof_layers_below == 1 &&
@@ -2535,8 +2530,7 @@ void TreeSupport::draw_circles()
//if (area(expoly) < SQ(scale_(1))) continue;
area_groups.emplace_back(&expoly, SupportLayer::BaseType, max_layers_above_base);
area_groups.back().need_infill = overlaps({ expoly }, area_poly);
area_groups.back().need_extra_wall = !area_groups.back().need_infill &&
!extra_wall_areas.empty() && overlaps({expoly}, extra_wall_areas);
area_groups.back().need_extra_wall = need_extra_wall && !area_groups.back().need_infill;
}
for (size_t roof_idx = 0; roof_idx < ts_layer->roof_areas.size(); ++roof_idx) {
auto &expoly = ts_layer->roof_areas[roof_idx];
@@ -3439,6 +3433,11 @@ void TreeSupport::smooth_nodes()
}
}
float max_move = scale_(m_object_config->support_line_width / 2);
// if the branch is very tall, the tip also needs extra wall
float thresh_tall_branch = 100;
float thresh_dist_to_top = 30;
for (int layer_nr = 0; layer_nr< contact_nodes.size(); layer_nr++) {
std::vector<SupportNode *> &curr_layer_nodes = contact_nodes[layer_nr];
if (curr_layer_nodes.empty()) continue;
@@ -3448,17 +3447,20 @@ void TreeSupport::smooth_nodes()
std::vector<double> radii;
std::vector<SupportNode *> branch;
SupportNode * p_node = node;
float total_height = 0;
// add a fixed head if it's not a polygon node, see STUDIO-4403
// Polygon node can't be added because the move distance might be huge, making the nodes in between jump and dangling
if (node->child && node->child->type!=ePolygon) {
pts.push_back(p_node->child->position);
radii.push_back(p_node->child->radius);
branch.push_back(p_node->child);
total_height += p_node->child->height;
}
do {
pts.push_back(p_node->position);
radii.push_back(p_node->radius);
branch.push_back(p_node);
total_height += p_node->height;
p_node = p_node->parent;
} while (p_node && !p_node->is_processed);
if (pts.size() < 3) continue;
@@ -3477,6 +3479,9 @@ void TreeSupport::smooth_nodes()
branch[i]->radius = radii1[i];
branch[i]->movement = (pts[i + 1] - pts[i - 1]) / 2;
branch[i]->is_processed = true;
if (branch[i]->parents.size() > 1 || (branch[i]->movement.x() > max_move || branch[i]->movement.y() > max_move) ||
(total_height > thresh_tall_branch && branch[i]->dist_mm_to_top < thresh_dist_to_top))
branch[i]->need_extra_wall = true;
BOOST_LOG_TRIVIAL(info) << "smooth_nodes: layer_nr=" << layer_nr << ", i=" << i << ", pt=" << pt << ", movement=" << branch[i]->movement << ", radius=" << branch[i]->radius;
}
}
@@ -3484,6 +3489,13 @@ void TreeSupport::smooth_nodes()
std::swap(pts, pts1);
std::swap(radii, radii1);
}
else {
// interpolate need_extra_wall in the end
for (size_t i = 1; i < branch.size() - 1; i++) {
if (branch[i - 1]->need_extra_wall && branch[i + 1]->need_extra_wall)
branch[i]->need_extra_wall = true;
}
}
}
}
}
+1
View File
@@ -135,6 +135,7 @@ struct SupportNode
TreeNodeType type = eCircle;
bool is_corner = false;
bool is_processed = false;
bool need_extra_wall = false;
bool is_sharp_tail = false;
bool valid = true;
ExPolygon overhang; // when type==ePolygon, set this value to get original overhang area
+2 -2
View File
@@ -3151,7 +3151,7 @@ void CreatePrinterPresetDialog::set_current_visible_printer()
m_printer_name_to_preset.clear();
for (const Preset &printer_preset : printer_presets) {
if (!printer_preset.is_visible) continue;
if (preset_bundle->printers.get_preset_base(printer_preset) != &printer_preset) continue;
if (preset_bundle->printers.get_preset_base(printer_preset)->name != printer_preset.name) continue;
if (auto printer_model = dynamic_cast<ConfigOptionString*>(const_cast<Preset&>(printer_preset).config.option("printer_model", false))) {
if (m_printer_name_to_preset.find(printer_model->value) == m_printer_name_to_preset.end()) {
printer_choice.push_back(from_u8(printer_model->value));
@@ -4962,7 +4962,7 @@ void CreatePresetForPrinterDialog::get_visible_printer_and_compatible_filament_p
if (filament_preset.is_default || !filament_preset.is_compatible || filament_preset.is_project_embedded) continue;
ConfigOptionStrings *filament_types;
const Preset * filament_preset_base = m_preset_bundle->filaments.get_preset_base(filament_preset);
if (filament_preset_base == &filament_preset || filament_preset_base == nullptr) {
if (filament_preset_base == &filament_preset) {
filament_types = dynamic_cast<ConfigOptionStrings *>(const_cast<Preset *>(&filament_preset)->config.option("filament_type"));
} else {
filament_types = dynamic_cast<ConfigOptionStrings *>(const_cast<Preset *>(filament_preset_base)->config.option("filament_type"));
+17 -29
View File
@@ -474,14 +474,17 @@ void Selection::add_curr_plate()
if (!m_valid)
return;
std::vector<unsigned int> volume_idxs = get_volume_idxs_from_plate(wxGetApp().plater()->get_partplate_list().get_curr_plate_index());
if (volume_idxs.empty())
return;
wxGetApp().plater()->take_snapshot(std::string("Selection-Add Curr Plate All!"));
m_mode = Instance;
clear();
do_add_volumes(volume_idxs);
PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
for (int obj_idx = 0; obj_idx < m_model->objects.size(); obj_idx++) {
if (plate && plate->contain_instance_totally(obj_idx, 0)) {
std::vector<unsigned int> volume_idxs = get_volume_idxs_from_object(obj_idx);
do_add_volumes(volume_idxs);
}
}
update_type();
this->set_bounding_boxes_dirty();
@@ -508,14 +511,20 @@ void Selection::remove_curr_plate()
if (!m_valid)
return;
std::vector<unsigned int> volume_idxs = get_volume_idxs_from_plate(wxGetApp().plater()->get_partplate_list().get_curr_plate_index());
if (volume_idxs.empty())
PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate();
if (plate->empty())
return;
wxGetApp().plater()->take_snapshot(std::string("Selection-Delete Curr Plate All"));
m_mode = Instance;
clear();
do_add_volumes(volume_idxs);
for (int obj_idx = 0; obj_idx < m_model->objects.size(); obj_idx++) {
if (plate && plate->contain_instance(obj_idx, 0)) {
std::vector<unsigned int> volume_idxs = get_volume_idxs_from_object(obj_idx);
do_add_volumes(volume_idxs);
}
}
update_type();
this->set_bounding_boxes_dirty();
@@ -2261,27 +2270,6 @@ std::vector<unsigned int> Selection::get_volume_idxs_from_volume(unsigned int ob
return idxs;
}
std::vector<unsigned int> Selection::get_volume_idxs_from_plate(const int plate_idx) const
{
PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_plate(plate_idx);
std::vector<unsigned int> idxs;
if (!plate)
return idxs;
for (int obj_idx = 0; obj_idx < (int)m_model->objects.size(); obj_idx++) {
const ModelObject* obj = m_model->objects[obj_idx];
for (int inst_idx = 0; inst_idx < (int)obj->instances.size(); inst_idx++) {
// on this plate or overlapping it in some way (fully inside, oversized or partly outside)
if (plate->contain_instance(obj_idx, inst_idx) || plate->intersect_instance(obj_idx, inst_idx)) {
std::vector<unsigned int> inst_idxs = get_volume_idxs_from_instance(obj_idx, inst_idx);
idxs.insert(idxs.end(), inst_idxs.begin(), inst_idxs.end());
}
}
}
return idxs;
}
std::vector<unsigned int> Selection::get_missing_volume_idxs_from(const std::vector<unsigned int>& volume_idxs) const
{
std::vector<unsigned int> idxs;
-2
View File
@@ -409,8 +409,6 @@ public:
std::vector<unsigned int> get_volume_idxs_from_instance(unsigned int object_idx, unsigned int instance_idx) const;
// returns the idx of the volume corresponding to the volume with the given idxs
std::vector<unsigned int> get_volume_idxs_from_volume(unsigned int object_idx, unsigned int instance_idx, unsigned int volume_idx) const;
// returns the list of idxs of the volumes of all instances on the given plate or overlapping it. (fully inside, oversized or partly outside)
std::vector<unsigned int> get_volume_idxs_from_plate(const int plate_idx) const;
// returns the list of idxs of the volumes contained in the selection but not in the given list
std::vector<unsigned int> get_missing_volume_idxs_from(const std::vector<unsigned int>& volume_idxs) const;
// returns the list of idxs of the volumes contained in the given list but not in the selection
-382
View File
@@ -22,7 +22,6 @@
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/Polyline.hpp"
#include <limits>
#include <string>
@@ -995,384 +994,3 @@ TEST_CASE("A hole whose outer wall touches the contour's runs with it", "[Perime
CHECK(layers[i].holes_ccw.front() == ccw);
}
}
namespace {
// The outer radius of round_tube().
const double tube_radius = 11.5;
// Every setting the tube walls below are measured against. With 0.5mm walls a 1mm tube wall holds only the two
// outer walls, touching, and every further 0.5mm makes room for one more inner wall.
DynamicPrintConfig tube_config(const char *wall_generator, int wall_loops)
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "wall_generator", wall_generator },
{ "wall_loops", wall_loops },
{ "wall_sequence", "inner wall/outer wall" },
{ "wall_direction", "ccw" },
{ "layer_height", 0.2 },
{ "initial_layer_print_height", 0.2 },
{ "nozzle_diameter", "0.4" },
{ "outer_wall_line_width", 0.5 },
{ "inner_wall_line_width", 0.5 },
{ "precise_outer_wall", true },
{ "detect_thin_wall", false },
{ "only_one_wall_top", false },
{ "only_one_wall_first_layer", false },
{ "brim_type", "no_brim" },
{ "detect_overhang_wall", true },
{ "overhang_reverse", false },
{ "overhang_reverse_threshold", "0%" },
});
return config;
}
// A 2mm tall round tube whose wall is `wall` mm thick.
Print &round_tube(Print &print, Model &model, const DynamicPrintConfig &config, double wall)
{
ModelObject *object = model.add_object();
object->name = "round_tube.stl";
object->add_volume(make_cylinder(tube_radius, 2.), ModelVolumeType::MODEL_PART, false);
object->add_volume(make_cylinder(tube_radius - wall, 2.), ModelVolumeType::NEGATIVE_VOLUME, false);
object->add_instance();
object->ensure_on_bed();
print.auto_assign_extruders(object);
print.apply(model, config);
print.validate();
print.set_status_silent();
print.process();
return print;
}
// The side of the tube wall a wall loop lies on. The centre is the middle of the tube wall, where Arachne lays the
// inner wall both sides share when the tube wall is too narrow for a pair of them.
enum class TubeSide { Contour, Hole, Centre };
struct TubeLoop {
TubeSide side;
int inset;
bool ccw;
};
// The wall loops of every layer of round_tube() in extrusion order.
std::vector<std::vector<TubeLoop>> tube_loops(const Print &print, double wall)
{
const double centre = tube_radius - 0.5 * wall;
std::vector<std::vector<TubeLoop>> out;
for (const Layer *layer : print.objects().front()->layers()) {
const Vec2d axis = get_extents(layer->lslices).center().cast<double>();
std::vector<TubeLoop> &loops = out.emplace_back();
for (const LayerRegion *region : layer->regions())
for (const ExtrusionEntity *entity : region->perimeters.flatten().entities) {
if (! entity->is_loop())
continue;
const ExtrusionLoop *loop = static_cast<const ExtrusionLoop*>(entity);
const Polygon polygon = loop->polygon();
double radius = 0.;
for (const Point &p : polygon.points)
radius += unscaled<double>((p.cast<double>() - axis).norm());
radius /= double(polygon.points.size());
const TubeSide side = radius > centre + 0.1 ? TubeSide::Contour : radius < centre - 0.1 ? TubeSide::Hole : TubeSide::Centre;
loops.push_back({ side, loop->inset_idx, polygon.is_counter_clockwise() });
}
}
return out;
}
// The insets one side of a wall is printed in, given its deepest inset: inside out for Inner/Outer, outside in for
// Outer/Inner, and for Inner/Outer/Inner the third wall onwards inside out, then the outer wall, then the first inner
// wall. Inner/Outer/Inner prints its first layer Inner/Outer, and outside in when there is no third wall.
std::vector<int> wall_sequence_insets(const std::string &wall_sequence, bool first_layer, int deepest)
{
std::vector<int> insets;
if (wall_sequence == "inner wall/outer wall" || (wall_sequence == "inner-outer-inner wall" && first_layer)) {
for (int inset = deepest; inset >= 0; -- inset)
insets.push_back(inset);
} else if (wall_sequence == "outer wall/inner wall" || deepest < 2) {
for (int inset = 0; inset <= deepest; ++ inset)
insets.push_back(inset);
} else {
for (int inset = deepest; inset >= 2; -- inset)
insets.push_back(inset);
insets.push_back(0);
insets.push_back(1);
}
return insets;
}
// Whether a wall of round_tube() runs with the contour: all but the walls of the hole, and those too when the hole
// is a thin wall hole, its outer wall touching the contour's with nothing between them.
bool runs_with_contour(const TubeLoop &loop, bool thin_wall_hole)
{
return loop.side != TubeSide::Hole || thin_wall_hole;
}
// The insets of the walls on one side of a round_tube() layer, the shared centre line included, in extrusion order.
std::vector<int> side_insets(const std::vector<TubeLoop> &loops, TubeSide side)
{
std::vector<int> insets;
for (const TubeLoop &loop : loops)
if (loop.side == side || loop.side == TubeSide::Centre)
insets.push_back(loop.inset);
return insets;
}
// The walls of a round_tube() layer in extrusion order, as side and inset: "c0 c1 h0 h1" is the contour's outer wall,
// then its first inner wall, then the same for the hole, and "m" marks the wall in the middle both sides share.
std::string wall_order(const std::vector<TubeLoop> &loops)
{
std::string out;
for (const TubeLoop &loop : loops)
out += std::string(out.empty() ? "" : " ") + (loop.side == TubeSide::Contour ? "c" : loop.side == TubeSide::Hole ? "h" : "m") +
std::to_string(loop.inset);
return out;
}
} // namespace
// Each side of a tube wall prints its walls in the order the wall sequence asks for, from two walls a side up to
// four. The inner wall Arachne lays in the middle of a wall too narrow for a pair of them is an inner wall of both
// sides; Classic fills that middle with gap fill instead, so it has one wall less a side there.
TEST_CASE("Each side of a tube wall prints its walls in the wall sequence order", "[Perimeters]")
{
struct TubeWall {
double wall;
int wall_loops;
// Walls a side, a shared inner wall included.
int arachne_walls;
int classic_walls;
};
const TubeWall tube = GENERATE(values<TubeWall>({
{ 1.5, 3, 2, 1 }, { 2.0, 2, 2, 2 }, { 2.5, 3, 3, 2 }, { 3.0, 3, 3, 3 }, { 4.0, 4, 4, 4 } }));
const char *wall_generator = GENERATE("classic", "arachne");
const std::string wall_sequence = GENERATE("inner wall/outer wall", "outer wall/inner wall", "inner-outer-inner wall");
CAPTURE(tube.wall, tube.wall_loops, wall_generator, wall_sequence);
DynamicPrintConfig config = tube_config(wall_generator, tube.wall_loops);
config.set_deserialize_strict("wall_sequence", wall_sequence);
Print print;
Model model;
round_tube(print, model, config, tube.wall);
const std::vector<std::vector<TubeLoop>> layers = tube_loops(print, tube.wall);
// 2mm at 0.2mm layers.
REQUIRE(layers.size() == 10);
const int walls = std::string(wall_generator) == "arachne" ? tube.arachne_walls : tube.classic_walls;
for (size_t i = 0; i < layers.size(); ++ i) {
for (const TubeSide side : { TubeSide::Contour, TubeSide::Hole }) {
const bool contour_side = side == TubeSide::Contour;
CAPTURE(i, contour_side);
const std::vector<int> insets = side_insets(layers[i], side);
REQUIRE(int(insets.size()) == walls);
CHECK(insets == wall_sequence_insets(wall_sequence, i == 0, walls - 1));
}
}
}
// The outer wall of a thin wall hole touches the contour's with nothing between them. The hole is printed first, so
// the contour leans on it, unless the wall sequence puts the outer walls first.
TEST_CASE("A thin wall hole is printed before the contour unless the outer walls go first", "[Perimeters]")
{
const char *wall_generator = GENERATE("classic", "arachne");
const std::string wall_sequence = GENERATE("inner wall/outer wall", "outer wall/inner wall", "inner-outer-inner wall");
CAPTURE(wall_generator, wall_sequence);
DynamicPrintConfig config = tube_config(wall_generator, 3);
config.set_deserialize_strict("wall_sequence", wall_sequence);
Print print;
Model model;
round_tube(print, model, config, 1.);
const std::vector<std::vector<TubeLoop>> layers = tube_loops(print, 1.);
REQUIRE(layers.size() == 10);
const TubeSide first = wall_sequence == "outer wall/inner wall" ? TubeSide::Contour : TubeSide::Hole;
for (size_t i = 0; i < layers.size(); ++ i) {
CAPTURE(i);
REQUIRE(layers[i].size() == 2);
REQUIRE(layers[i][0].side != layers[i][1].side);
CHECK(layers[i].front().side == first);
}
}
// A 1.5mm tube wall leaves Inner/Outer/Inner no third wall to sandwich, only the inner wall Arachne lays in the middle
// for both sides, or Classic's gap fill. Past the first layer it prints the hole first, then the contour, then the
// middle.
TEST_CASE("Inner/Outer/Inner prints the hole of a narrow tube wall first and the middle last", "[Perimeters]")
{
const char *wall_generator = GENERATE("classic", "arachne");
CAPTURE(wall_generator);
DynamicPrintConfig config = tube_config(wall_generator, 3);
config.set_deserialize_strict("wall_sequence", "inner-outer-inner wall");
Print print;
Model model;
round_tube(print, model, config, 1.5);
const std::vector<std::vector<TubeLoop>> layers = tube_loops(print, 1.5);
REQUIRE(layers.size() == 10);
// Classic's gap fill is not a wall loop, and goes after the walls.
const std::string expected = std::string(wall_generator) == "arachne" ? "h0 c0 m1" : "h0 c0";
for (size_t i = 1; i < layers.size(); ++ i) {
CAPTURE(i);
CHECK(wall_order(layers[i]) == expected);
}
}
// The contour's walls run in the wall direction and the hole's against it, except that the walls of a thin wall
// hole run with the contour, as does the inner wall Arachne shares between both sides of a 1.5mm tube wall.
TEST_CASE("Tube walls run in the wall direction and hole walls against it", "[Perimeters]")
{
const double wall = GENERATE(1.0, 1.5, 2.0, 3.0);
const char *wall_generator = GENERATE("classic", "arachne");
const std::string wall_direction = GENERATE("ccw", "cw");
CAPTURE(wall, wall_generator, wall_direction);
DynamicPrintConfig config = tube_config(wall_generator, 3);
config.set_deserialize_strict("wall_direction", wall_direction);
Print print;
Model model;
round_tube(print, model, config, wall);
const std::vector<std::vector<TubeLoop>> layers = tube_loops(print, wall);
REQUIRE(layers.size() == 10);
const bool thin_wall_hole = wall == 1.;
for (size_t i = 0; i < layers.size(); ++ i) {
CAPTURE(i);
REQUIRE(std::any_of(layers[i].begin(), layers[i].end(), [](const TubeLoop &loop) { return loop.side == TubeSide::Contour; }));
REQUIRE(std::any_of(layers[i].begin(), layers[i].end(), [](const TubeLoop &loop) { return loop.side == TubeSide::Hole; }));
for (const TubeLoop &loop : layers[i]) {
CAPTURE(int(loop.side), loop.inset);
CHECK(loop.ccw == (runs_with_contour(loop, thin_wall_hole) == (wall_direction == "ccw")));
}
}
}
// Reverse on even with no threshold reverses every wall of every other layer, or only the inner walls when it is
// restricted to them, and leaves the order of the walls alone.
TEST_CASE("Reverse on even reverses the tube walls of every other layer", "[Perimeters]")
{
const double wall = GENERATE(1.0, 1.5, 3.0);
const char *wall_generator = GENERATE("classic", "arachne");
const bool reverse_internal_only = GENERATE(false, true);
CAPTURE(wall, wall_generator, reverse_internal_only);
auto slice = [&](bool overhang_reverse) {
DynamicPrintConfig config = tube_config(wall_generator, 3);
config.set_deserialize_strict({
{ "overhang_reverse", overhang_reverse },
{ "overhang_reverse_internal_only", reverse_internal_only },
});
Print print;
Model model;
round_tube(print, model, config, wall);
return tube_loops(print, wall);
};
const std::vector<std::vector<TubeLoop>> plain = slice(false);
const std::vector<std::vector<TubeLoop>> reversed = slice(true);
REQUIRE(plain.size() == 10);
REQUIRE(reversed.size() == plain.size());
for (size_t i = 0; i < plain.size(); ++ i) {
CAPTURE(i);
REQUIRE(reversed[i].size() == plain[i].size());
for (size_t j = 0; j < plain[i].size(); ++ j) {
CAPTURE(j);
CHECK(reversed[i][j].side == plain[i][j].side);
CHECK(reversed[i][j].inset == plain[i][j].inset);
const bool flips = i % 2 == 1 && (! reverse_internal_only || plain[i][j].inset > 0);
CHECK(reversed[i][j].ccw == (plain[i][j].ccw != flips));
}
}
}
namespace {
DynamicPrintConfig brim_tube_config(const char *wall_generator, const std::string &wall_sequence, const char *brim_type, double brim_width)
{
DynamicPrintConfig config = tube_config(wall_generator, 3);
config.set_deserialize_strict({
{ "wall_sequence", wall_sequence },
{ "brim_type", brim_type },
{ "brim_width", brim_width },
});
return config;
}
} // namespace
// A brim against the outer walls makes the first layer print them first, whatever the wall sequence, so the brim runs
// straight on into the outer wall: each side of the tube wall outside in, and the contour before a thin wall hole.
// Brims that may leave the outer walls bare keep the wall sequence, and so do the layers above.
TEST_CASE("A brim against the outer walls makes the first layer print them first", "[Perimeters]")
{
struct Brim {
const char *type;
double width;
bool outer_walls_first;
};
const Brim brim = GENERATE(values<Brim>({
{ "no_brim", 5., false }, { "inner_only", 5., false }, { "auto_brim", 5., false }, { "outer_only", 0., false },
{ "outer_only", 5., true }, { "outer_and_inner", 5., true }, { "leading_edge_only", 5., true } }));
const double wall = GENERATE(1.0, 3.0);
const char *wall_generator = GENERATE("classic", "arachne");
const std::string wall_sequence = GENERATE("inner wall/outer wall", "inner-outer-inner wall");
CAPTURE(brim.type, brim.width, wall, wall_generator, wall_sequence);
Print print;
Model model;
round_tube(print, model, brim_tube_config(wall_generator, wall_sequence, brim.type, brim.width), wall);
const std::vector<std::vector<TubeLoop>> layers = tube_loops(print, wall);
REQUIRE(layers.size() == 10);
// The thin wall hole has its outer wall a side, the 3mm tube wall three walls a side.
const size_t walls = wall == 1. ? 1 : 3;
for (const size_t i : { size_t(0), size_t(1) }) {
const bool outer_walls_first = brim.outer_walls_first && i == 0;
for (const TubeSide side : { TubeSide::Contour, TubeSide::Hole }) {
const bool contour_side = side == TubeSide::Contour;
CAPTURE(i, contour_side);
const std::vector<int> insets = side_insets(layers[i], side);
REQUIRE(insets.size() == walls);
CHECK(insets == wall_sequence_insets(outer_walls_first ? "outer wall/inner wall" : wall_sequence, i == 0, int(walls) - 1));
}
if (wall == 1.)
CHECK(layers[i].front().side == (outer_walls_first ? TubeSide::Contour : TubeSide::Hole));
}
}
// Changing the brim of a sliced print regenerates the walls when the first layer's wall order changes with it, so
// the print gets the walls a slice from scratch with the new brim gets.
TEST_CASE("Changing the brim updates the wall order of the first layer", "[Perimeters]")
{
struct BrimChange {
const char *from_type;
double from_width;
const char *to_type;
double to_width;
};
const BrimChange change = GENERATE(values<BrimChange>({
{ "outer_only", 0., "outer_only", 5. }, { "outer_only", 5., "outer_only", 0. },
{ "no_brim", 5., "outer_and_inner", 5. }, { "outer_and_inner", 5., "inner_only", 5. } }));
const char *wall_generator = GENERATE("classic", "arachne");
CAPTURE(change.from_type, change.from_width, change.to_type, change.to_width, wall_generator);
const std::string wall_sequence = "inner wall/outer wall";
Print print;
Model model;
round_tube(print, model, brim_tube_config(wall_generator, wall_sequence, change.from_type, change.from_width), 3.);
const std::string before = wall_order(tube_loops(print, 3.).front());
print.apply(model, brim_tube_config(wall_generator, wall_sequence, change.to_type, change.to_width));
print.process();
const std::string after = wall_order(tube_loops(print, 3.).front());
Print fresh_print;
Model fresh_model;
round_tube(fresh_print, fresh_model, brim_tube_config(wall_generator, wall_sequence, change.to_type, change.to_width), 3.);
const std::string fresh = wall_order(tube_loops(fresh_print, 3.).front());
// The change does turn the first layer around.
REQUIRE(before != fresh);
CHECK(after == fresh);
}
-59
View File
@@ -735,65 +735,6 @@ TEST_CASE("Custom G-code placeholders are substituted", "[Print]")
CHECK(per_layer.find(";Layer:399 ") != std::string::npos);
}
// The final print statistics are known only after the whole G-code is generated, yet every custom G-code,
// the file header included, gets the same values as filename_format.
TEST_CASE("Print statistics in custom G-code match the final print statistics", "[Print][Regression]")
{
const std::string gcode_key = GENERATE(as<std::string>{}, "file_start_gcode", "machine_start_gcode", "machine_end_gcode");
// Bambu printers use another tag table and G-code layout.
const bool is_bbl = GENERATE(false, true);
CAPTURE(gcode_key, is_bbl);
const std::vector<std::string> number_keys = { "total_weight", "used_filament", "total_cost", "total_wipe_tower_cost",
"total_wipe_tower_filament" };
const std::vector<std::string> time_keys = { "print_time", "normal_print_time", "silent_print_time" };
std::string templ;
for (const std::vector<std::string> *keys : { &number_keys, &time_keys })
for (const std::string &key : *keys)
templ += "; stat " + key + "={" + key + "}\n";
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ gcode_key, templ },
{ "filament_density", "1.25" },
{ "filament_cost", "20" },
{ "time_cost", "2" },
});
Print print;
Model model;
init_print({ cube(20) }, print, model, config);
print.is_BBL_printer() = is_bbl;
std::string gcode;
REQUIRE_NOTHROW(gcode = Slic3r::Test::gcode(print));
// Match whole lines only: the config block repeats the template on a single line, and Bambu printers
// write that block before the start G-code.
const std::string lines = "\n" + gcode;
const auto value_of = [&lines](const std::string &key) {
const std::string prefix = "\n; stat " + key + "=";
const size_t start = lines.find(prefix);
REQUIRE(start != std::string::npos);
const size_t begin = start + prefix.size();
return lines.substr(begin, lines.find('\n', begin) - begin);
};
// The same statistics filename_format is expanded with.
DynamicConfig stats = print.print_statistics().config();
// A 20mm cube with filament_density 1.25 weighs something, unlike the running extruded_weight_total in the header.
CHECK(stats.opt_float("total_weight") > 0.);
for (const std::string &key : number_keys) {
CAPTURE(key);
const std::string value = value_of(key);
REQUIRE(value.find('@') == std::string::npos);
// Written with two decimals.
CHECK_THAT(std::stod(value), Catch::Matchers::WithinAbs(stats.opt_float(key), 0.006));
}
for (const std::string &key : time_keys) {
CAPTURE(key);
CHECK(value_of(key) == stats.opt_string(key));
}
}
TEST_CASE("export_gcode writes G-code without a result pointer", "[Print][export_gcode]")
{
Print print;
@@ -985,37 +985,6 @@ TEST_CASE("A user filament synced to the cloud and back keeps its values and uns
}
}
TEST_CASE("A project filament storing one value for a per-variant key applies it to every variant", "[Preset][Bundle][Regression]")
{
PresetCollection filaments(Preset::TYPE_FILAMENT, Preset::filament_options(),
static_cast<const PrintRegionConfig &>(FullPrintConfig::defaults()));
Preset &parent = add_inmemory_preset(filaments, "Parent Filament");
parent.config.set_key_value("filament_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive High Flow"}));
parent.config.set_key_value("adaptive_pressure_advance_model", new ConfigOptionStrings({"parent standard", "parent high flow"}));
parent.config.set_deserialize_strict("pressure_advance", "0.02,0.03");
parent.is_system = true;
// A filament saved before these keys became per-variant lists both variants but stores each key once.
const std::string model = "0.025,15.6,5000";
DynamicPrintConfig config;
config.set_key_value(BBL_JSON_KEY_INHERITS, new ConfigOptionString("Parent Filament"));
config.set_key_value("filament_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive High Flow"}));
config.set_key_value("adaptive_pressure_advance_model", new ConfigOptionStrings({model}));
config.set_deserialize_strict("pressure_advance", "0.016");
auto embedded = std::make_unique<Preset>(Preset::TYPE_FILAMENT, "Project Filament", false);
embedded->config = std::move(config);
embedded->is_project_embedded = true;
std::vector<Preset *> project_presets{embedded.get()};
PresetsConfigSubstitutions substitutions;
filaments.load_project_embedded_presets(project_presets, PRESET_FILAMENT_NAME, substitutions, ForwardCompatibilitySubstitutionRule::Disable);
const Preset *loaded = filaments.find_preset("Project Filament");
REQUIRE(loaded != nullptr);
CHECK(loaded->config.option<ConfigOptionStrings>("adaptive_pressure_advance_model")->values == std::vector<std::string>{model, model});
check_double_vector(loaded->config.option<ConfigOptionFloats>("pressure_advance")->values, {0.016, 0.016});
}
TEST_CASE("Repeated loads of a user preset directory produce the same presets", "[Preset][Bundle]")
{
ScopedTemporaryDir temp_dir;
@@ -1219,24 +1188,6 @@ TEST_CASE("find_preset2 auto-matches removed Generic vendor profiles to the libr
CHECK(bundle.filaments.find_preset2("BrandX Generic PETG", /*auto_match=*/true) == nullptr);
}
TEST_CASE("The selected preset is its own base when its parent is not loaded", "[Preset]")
{
PresetCollection printers(Preset::TYPE_PRINTER, Preset::printer_options(),
static_cast<const PrintRegionConfig &>(FullPrintConfig::defaults()));
add_inmemory_preset(printers, "Printer");
add_inmemory_preset(printers, "User Printer", "Printer");
add_inmemory_preset(printers, "Orphan Printer", "Uninstalled Vendor Printer");
printers.select_preset_by_name("User Printer", true);
REQUIRE(printers.get_selected_preset_name() == "User Printer");
CHECK(printers.get_selected_preset_base().name == "Printer");
printers.select_preset_by_name("Orphan Printer", true);
REQUIRE(printers.get_selected_preset_name() == "Orphan Printer");
REQUIRE(printers.get_preset_base(printers.get_selected_preset()) == nullptr);
CHECK(printers.get_selected_preset_base().name == "Orphan Printer");
}
TEST_CASE("Renamed parent is normalized into a loaded preset's inherits", "[Preset][Rename]")
{
ScopedTemporaryDir temp_dir;
+1 -1
View File
@@ -7,7 +7,7 @@ set(SLIC3R_APP_KEY "OrcaSlicer")
if(NOT DEFINED BBL_INTERNAL_TESTING)
set(BBL_INTERNAL_TESTING "0")
endif()
set(SoftFever_VERSION "2.5.0-dev")
set(SoftFever_VERSION "2.5.0-alpha")
string(REGEX MATCH "^([0-9]+)\\.([0-9]+)\\.([0-9]+)"
SoftFever_VERSION_MATCH ${SoftFever_VERSION})
set(ORCA_VERSION_MAJOR ${CMAKE_MATCH_1})