Merge branch 'main' into feat/plugin-lifecycle-evts

This commit is contained in:
Ian Chua
2026-09-18 21:35:09 +08:00
committed by GitHub
7433 changed files with 274917 additions and 159438 deletions
+27 -2
View File
@@ -33,7 +33,8 @@ if (SLIC3R_GUI)
set (wxWidgets_CONFIG_OPTIONS "--toolkit=gtk${SLIC3R_GTK}")
find_package(wxWidgets 3.3 REQUIRED COMPONENTS base core adv html gl aui net media webview)
else ()
find_package(wxWidgets 3.3 CONFIG REQUIRED COMPONENTS html adv gl core base webview aui net media)
# propgrid is required by wxInspector.
find_package(wxWidgets 3.3 CONFIG REQUIRED COMPONENTS html adv gl core base webview aui net media propgrid)
endif ()
if(UNIX)
@@ -75,7 +76,7 @@ if (SLIC3R_GUI)
list(FILTER wxWidgets_LIBRARIES EXCLUDE REGEX expat)
list(APPEND wxWidgets_LIBRARIES ${EXPAT_LIBRARIES})
endif ()
# This is an issue in the new wxWidgets cmake build, doesn't deal with librt
find_library(LIBRT rt)
if(LIBRT)
@@ -90,6 +91,16 @@ if (SLIC3R_GUI)
# list(REMOVE_ITEM wxWidgets_LIBRARIES oleacc)
find_package(wxInspector REQUIRED)
# wxInspector's exported interface names the release wxWidgets import
# libraries, which a Debug build cannot link. wx is linked above instead.
get_target_property(_wxinspector_interface wxInspector::wxInspector INTERFACE_LINK_LIBRARIES)
if (_wxinspector_interface)
list(FILTER _wxinspector_interface EXCLUDE REGEX "wx(base|msw)3[0-9]u[_.]")
set_target_properties(wxInspector::wxInspector PROPERTIES
INTERFACE_LINK_LIBRARIES "${_wxinspector_interface}")
endif ()
list(APPEND wxWidgets_LIBRARIES "wxInspector::wxInspector")
message(STATUS "wx libs: ${wxWidgets_LIBRARIES}")
@@ -294,6 +305,16 @@ if (WIN32)
endif()
else ()
if (NOT APPLE)
set(output_sos_Release "")
set(output_sos_Debug "")
add_custom_target(OrcaSlicerSosCopy ALL DEPENDS OrcaSlicer)
if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
orcaslicer_copy_sos(OrcaSlicerSosCopy "Debug" "d" output_sos_Debug)
else()
orcaslicer_copy_sos(OrcaSlicerSosCopy "Release" "" output_sos_Release)
endif()
endif()
if (APPLE AND NOT CMAKE_MACOSX_BUNDLE)
# On OSX, the name of the binary matches the name of the Application.
add_custom_command(TARGET OrcaSlicer POST_BUILD
@@ -372,5 +393,9 @@ if (WIN32)
install(FILES ${output_dlls_${build_type}} DESTINATION ".")
install(DIRECTORY "${CMAKE_PREFIX_PATH}/libpython/" DESTINATION "python")
else ()
if (APPLE)
else()
install(FILES ${output_sos_${build_type}} DESTINATION "${CMAKE_INSTALL_PREFIX}")
endif()
install(TARGETS OrcaSlicer RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR})
endif ()
+752 -84
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -297,7 +297,7 @@ int wmain(int argc, wchar_t **argv)
// printf("Loading Slic3r library: %S\n", path_to_slic3r);
HINSTANCE hInstance_Slic3r = LoadLibraryExW(path_to_slic3r, nullptr, 0);
if (hInstance_Slic3r == nullptr) {
printf("OrcaSlicer.dll was not loaded, error=%d\n", GetLastError());
printf("OrcaSlicer.dll was not loaded, error=%lu\n", GetLastError());
return -1;
}
+2 -1
View File
@@ -9,6 +9,7 @@
#include <boost/format.hpp>
#include <mutex>
#include "git_commit_hash.h"
#include "libslic3r_version.h"
static std::string g_log_folder;
@@ -39,7 +40,7 @@ CBaseException::CBaseException(HANDLE hProcess, WORD wPID, LPCTSTR lpSymbolPath,
output_file->open(log_filename, std::ios::out | std::ios::app);
// Output app build info in crash log so we could look for the correct PDB files
OutputString(_T("%s\n\n"), _T(SLIC3R_APP_NAME " " SoftFever_VERSION " Build " GIT_COMMIT_HASH));
OutputString(_T("%s\n\n"), _T(SLIC3R_APP_NAME " " SoftFever_VERSION " Build " GIT_COMMIT_HASH GIT_COMMIT_SUFFIX));
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
#pragma once
#include <boost/nowide/cstdio.hpp>
#include <boost/nowide/fstream.hpp>
#include "stackwalker.h"
#include "StackWalker.h"
#include <eh.h>
class CBaseException : public CStackWalker
+47 -2
View File
@@ -8,7 +8,11 @@
#define NANOSVGRAST_IMPLEMENTATION
#include "nanosvg/nanosvgrast.h"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/GCode.hpp"
#include "libslic3r/GCode/WipeTower.hpp"
#include "libslic3r/GCode/WipeTowerEstimate.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/PresetBundle.hpp"
@@ -116,15 +120,45 @@ Vec2d printable_area_center(const DynamicPrintConfig &cfg)
return 0.5 * (lo + hi);
}
// Put the prime tower where the GUI and CLI would before slicing. The config default (x 15, y 220)
// lies off any bed shallower than the tower, and generation rejects an off-plate tower instead of
// exporting it. Beside the centred cube, clear of the edge exclusion strips some beds carry, then
// pulled inside the printable outline by the tower's own estimated footprint, with a few mm of
// clearance so the conflict checker never sees the two touch.
void place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d &center)
{
const auto *area = cfg.option<ConfigOptionPoints>("printable_area");
if (area == nullptr || area->values.size() < 3)
return;
const WipeTowerFootprint footprint = estimate_wipe_tower_footprint(cfg, resolve_wipe_tower_type(cfg), {0, 1}, cfg.opt_float("layer_height"), 10.);
if (footprint.depth < EPSILON)
return;
const double margin = WIPE_TOWER_MARGIN + footprint.brim_width;
// The position is the tower's own origin; a rotated tower extends from it in another
// direction, so place the rotated box's extents rather than the origin.
Slic3r::Polygon box({Point::new_scale(0., 0.), Point::new_scale(footprint.width, 0.), Point::new_scale(footprint.width, footprint.depth), Point::new_scale(0., footprint.depth)});
box.rotate(Geometry::deg2rad(cfg.opt_float("wipe_tower_rotation_angle")));
const BoundingBox local = get_extents(box);
const Vec2d lo = unscale(local.min);
const Vec2d size = unscale(local.max) - lo;
Vec2d pos(center.x() + 5. + margin + 5. - lo.x(), center.y() - size.y() / 2. - lo.y());
box.translate(Point::new_scale(pos.x(), pos.y()));
const Vec2f move = WipeTower::move_box_inside_polygon(get_extents(box), Polygons{Polygon::new_scale(area->values)}, scaled<coord_t>(margin));
pos += move.cast<double>();
cfg.option<ConfigOptionFloats>("wipe_tower_x", true)->values = {pos.x()};
cfg.option<ConfigOptionFloats>("wipe_tower_y", true)->values = {pos.y()};
}
// Slice one centered cube that switches from filament 1 to filament 2 partway up, so exactly one
// filament change fires, then export. The change drives the printer's own change_filament_gcode: on a
// single-nozzle machine it rides the AMS prime tower (append_tcr), on a multi-nozzle machine it routes
// through the nozzle swap (set_extruder / append_tcr2) - the engine picks the path from the printer's
// topology, so one model covers both. An undefined placeholder in any shipped custom g-code throws
// Slic3r::PlaceholderParserError from export.
std::string slice_two_color_cube_and_export(const DynamicPrintConfig &cfg, bool is_bbl)
std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl)
{
const Vec2d center = printable_area_center(cfg);
place_wipe_tower(cfg, center);
TriangleMesh m = make_cube(10, 10, 10);
m.translate(float(center.x() - 5.), float(center.y() - 5.), 0.f);
@@ -175,6 +209,17 @@ void select_printer_default_presets(PresetBundle &bundle)
if (const auto *def_fil = printer_preset.config.option<ConfigOptionStrings>("default_filament_profile");
def_fil != nullptr && !def_fil->values.empty())
bundle.filaments.select_preset_by_name(def_fil->values.front(), /*force=*/true);
// Re-seed the per-slot filament list from that selection, or the sweep's result depends on the
// printer sliced before it. Once there are 2+ slots, full_config() builds the filament config from
// filament_presets and ignores the selected preset (PresetBundle::full_fff_config), while
// update_compatible() only replaces a slot that has gone *incompatible* - and when it does, it ranks
// the outgoing preset's alias, then its filament type, above the printer's own default. The sweep
// grows every printer to 2 slots and update_multi_material_filament_presets() never shrinks them, so
// a material picked up on the first printer rides the whole run. With all vendors loaded the first
// printer inherits a TPU (the load-time pick is whichever filament sorts first), the type match
// re-resolves it to "Generic TPU @System", and its alias then pins every later printer to that
// vendor's own "Generic TPU @..." - which the BBL dual-nozzle profiles rightly refuse to group.
bundle.filament_presets.assign(1, bundle.filaments.get_selected_preset_name());
}
// The vendor/printer currently being sliced, stamped onto every engine log record by the sink below so
@@ -381,7 +426,7 @@ int main(int argc, char* argv[])
("generate_presets,g", po::value<bool>()->default_value(false), "Generate user presets for mock test")
("slice,s", po::bool_switch()->default_value(false), "Slice a two-colour cube through every printer to expand all custom g-code (catches placeholder/flow errors that static checks miss). Off unless this flag is present.")
("outdir,o", po::value<std::string>()->default_value(""), "With -s, also save each printer's g-code to this folder (as <vendor>__<printer>.gcode) for manual inspection. Optional.")
("check_filament_subtypes,f", po::bool_switch()->default_value(false), "Also flag printers with duplicate (ambiguous) filament subtypes. Off unless this flag is present.")
("check_filament_subtypes,f", po::bool_switch()->default_value(true), "Also flag printers with duplicate (ambiguous) filament subtypes. Off unless this flag is present.")
("log_level,l", po::value<int>()->default_value(2), "Log level. Optional, default is 2 (warning). Higher values produce more detailed logs.");
// clang-format on
+2 -2
View File
@@ -364,7 +364,7 @@ void CStackWalker::GetModuleInformation(LPMODULE_INFO pmi)
if (dwInfoSize > 0)
{
LPVOID lpData = new byte[dwInfoSize];
byte *lpData = new byte[dwInfoSize];
ZeroMemory(lpData, dwInfoSize * sizeof(byte));
if (GetFileVersionInfo(pmi->szModulePath, dwHandle, dwInfoSize, lpData) > 0 )
@@ -425,7 +425,7 @@ LPSTACKINFO CStackWalker::StackWalker(HANDLE hThread, const CONTEXT* context)
else
c = *context;
STACKFRAME64 sf = {0};
STACKFRAME64 sf = {};
DWORD imageType;
//intel X86
+16 -2
View File
@@ -23,7 +23,8 @@ int main(int argc, char* argv[])
#else
("path,p", po::value<std::string>()->default_value("../../../resources/profiles"), "Path to profiles directory")
#endif
("log_level,l", po::value<int>()->default_value(2), "Log level (0=trace, 2=info, 4=error)");
("log_level,l", po::value<int>()->default_value(2), "Log level (0=trace, 2=info, 4=error)")
("vendor,v", po::value<std::string>()->default_value(""), "Vendor name. Optional; generate the cache for this vendor only (the Orca filament library is always included as the inheritance base). All vendors if not specified.");
// clang-format on
po::variables_map vm;
@@ -38,6 +39,7 @@ int main(int argc, char* argv[])
const std::string profiles_path = vm["path"].as<std::string>();
const int log_level = vm["log_level"].as<int>();
const std::string vendor = vm["vendor"].as<std::string>();
if (!fs::exists(profiles_path) || !fs::is_directory(profiles_path)) {
std::cerr << "Error: '" << profiles_path << "' is not a valid directory\n";
@@ -59,8 +61,12 @@ int main(int argc, char* argv[])
preset_bundle->set_is_validation_mode(true);
preset_bundle->set_default_suppressed(true);
preset_bundle->set_generate_vendor_caches(true);
// Empty == every vendor. Otherwise only this vendor (plus the always-loaded
// Orca filament library) is parsed, so only its <vendor>.opc is written.
preset_bundle->set_vendor_to_validate(vendor);
std::cout << "Loading system presets from: " << profiles_path << "\n";
std::cout << "Loading system presets from: " << profiles_path
<< (vendor.empty() ? "" : " (vendor: " + vendor + ")") << "\n";
try {
// In validation mode data_dir() is the profiles directory set above, so the
@@ -71,6 +77,14 @@ int main(int argc, char* argv[])
return 1;
}
// A specific vendor must have produced its own cache; the always-loaded
// filament library alone would otherwise mask a misspelt or removed name.
if (!vendor.empty() && !fs::exists(fs::path(profiles_path) / (vendor + ".opc"))) {
std::cerr << "No cache was generated for vendor '" << vendor << "' under " << profiles_path
<< " - check the vendor name.\n";
return 1;
}
size_t cache_count = 0;
for (auto& entry : fs::directory_iterator(profiles_path))
if (boost::iends_with(entry.path().string(), ".opc"))
@@ -83,6 +83,10 @@ copy_shared_object_to_dir() {
src_real="$(readlink -f "$src")"
dst_name="$(basename "$src_real")"
mkdir -p "$dst_dir"
if [ "$src_real" = "$dst_dir/$dst_name" ]; then
# Already bundled; the dependency resolved from the bundle directory.
return 0
fi
cp -fL "$src_real" "$dst_dir/$dst_name"
if [ -L "$src" ]; then
@@ -96,12 +100,23 @@ copy_shared_object_to_dir() {
}
bundle_dependency_closure() {
local dst_dir="$1"
local dst_dir
dst_dir="$(cd -- "$1" && pwd)"
shift
local -a queue=("$@")
local target dep dep_real copied_path
local target dep dep_real dep_key copied_path
declare -A seen=()
# Dependencies are resolved with ldd, which only searches the default
# loader path. Deps-built shared libraries (e.g. the FFmpeg stack) are not
# installed there and carry no RUNPATH of their own, so once copied into
# the bundle ldd can no longer resolve one sibling from another
# (libavcodec -> libavutil) and reports it as missing. Extend the loader
# path with the bundle directory plus the source directories of files
# already bundled, so every library that was resolved once keeps resolving
# for its own dependencies. The audit script does the same
# (scripts/check_appimage_libs.sh).
local -a search_dirs=("$dst_dir")
while [ ${#queue[@]} -gt 0 ]; do
target="${queue[0]}"
@@ -122,17 +137,24 @@ bundle_dependency_closure() {
continue
fi
if [ -n "${seen[$dep_real]}" ]; then
# Key dedup on the bundled file rather than the source path: once
# ldd resolves a library from the bundle directory (via the
# LD_LIBRARY_PATH above) its path is a dst_dir path, which differs
# from the source path the first resolution returned. Keying on
# the source path would re-copy the file onto itself.
dep_key="$dst_dir/$(basename "$dep_real")"
if [ -n "${seen[$dep_key]}" ]; then
continue
fi
seen[$dep_real]=1
seen[$dep_key]=1
copy_shared_object_to_dir "$dep" "$dst_dir"
search_dirs+=("$(dirname "$dep_real")")
copied_path="$dst_dir/$(basename "$dep_real")"
if [ -e "$copied_path" ]; then
queue+=("$copied_path")
fi
done < <(appimage_list_direct_dependencies "$target")
done < <(LD_LIBRARY_PATH="$(IFS=:; printf '%s' "${search_dirs[*]}")${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" appimage_list_direct_dependencies "$target")
done
}
+1 -1
View File
@@ -229,7 +229,7 @@ public:
m_bbox(bbox.min - Point(SCALED_EPSILON, SCALED_EPSILON), bbox.max + Point(SCALED_EPSILON, SCALED_EPSILON)) {}
size_t idx() const { return m_idx; }
const BoundingBox& bbox() const { return m_bbox; }
Point centroid() const { return (m_bbox.min() + m_bbox.max() / 2); }
Point centroid() const { return (m_bbox.min() + m_bbox.max()) / 2; }
private:
size_t m_idx;
BoundingBox m_bbox;
+1 -1
View File
@@ -49,7 +49,7 @@ SplittedLine split_line(const PathType& path, const ExPolygons& clip, bool close
// Convert the input path into an open ZPath
ClipperZUtils::ZPath p;
p.reserve(path.size() + closed ? 1 : 0);
p.reserve(path.size() + (closed ? 1 : 0));
ClipperLib_Z::cInt z = 0;
for (const auto& point : path) {
p.emplace_back(point.x(), point.y(), z);
+38 -1
View File
@@ -42,6 +42,9 @@ namespace Slic3r {
static const std::string VERSION_CHECK_URL = "https://check-version.orcaslicer.com/latest";
static const std::string PROFILE_UPDATE_URL = "https://check-version.orcaslicer.com/profile";
constexpr const char* CONFIG_ORCA_UPDATER_URL = "orca_updater_url";
static const std::string MODELS_STR = "models";
const std::string AppConfig::SECTION_FILAMENTS = "filaments";
@@ -310,6 +313,26 @@ void AppConfig::set_defaults()
if (get("zoom_to_mouse").empty())
set_bool("zoom_to_mouse", false);
#ifdef SLIC3R_CAD
// Experimental parametric Design tab. Off by default: the tab is not created at all
// until this is turned on, so nothing it builds reaches an unsuspecting user.
if (get("enable_cad_feature").empty())
set_bool("enable_cad_feature", false);
// Auto-weld sketch endpoints within kSketchJoinTol when building closed loops.
// Default ON: it is what the ~90% case wants; OFF makes the kernel demand an exact
// joint. The GUI pushes it into SketchEngine via set_sketch_auto_close().
if (get("auto_close_sketch_loops").empty())
set_bool("auto_close_sketch_loops", true);
// Design tab: draw a mate connector as a face rather than as the abstract disc + roll
// quadrant. Defaults ON — face orientation is hardwired perception, so the roll and the
// verse read without being learned, which no abstract glyph achieves. Turning it off
// restores the conventional CAD representation for users who expect it (x0kd).
if (get("design_connector_face_glyph").empty())
set_bool("design_connector_face_glyph", true);
#endif
//#ifdef SUPPORT_SHOW_HINTS
if (get("show_hints").empty())
set_bool("show_hints", false);
@@ -635,6 +658,11 @@ void AppConfig::set_defaults()
set_bool("use_printer_agents", false);
}
if (get("enable_ota").empty())
{
set_bool("enable_ota", false);
}
// Remove legacy window positions/sizes
erase("app", "main_frame_maximized");
erase("app", "main_frame_pos");
@@ -1440,6 +1468,7 @@ void AppConfig::set_mouse_device(const std::string& name, double translation_spe
it->second["invert_yaw"] = invert_yaw ? "1" : "0";
it->second["invert_pitch"] = invert_pitch ? "1" : "0";
it->second["invert_roll"] = invert_roll ? "1" : "0";
m_dirty = true;
}
std::vector<std::string> AppConfig::get_mouse_device_names() const
@@ -1814,7 +1843,10 @@ std::string AppConfig::version_check_url() const
std::string AppConfig::profile_update_url() const
{
return PROFILE_UPDATE_URL;
std::string orca_updater_url = get(CONFIG_ORCA_UPDATER_URL);
if (orca_updater_url.empty())
return PROFILE_UPDATE_URL;
return orca_updater_url;
}
bool AppConfig::exists()
@@ -1822,4 +1854,9 @@ bool AppConfig::exists()
return boost::filesystem::exists(config_path());
}
std::string AppConfig::load_if_exists()
{
return boost::filesystem::exists(loading_path()) ? load() : std::string();
}
}; // namespace Slic3r
+3 -1
View File
@@ -113,8 +113,10 @@ public:
void set_defaults();
// Load the slic3r.ini from a user profile directory (or a datadir, if configured).
// return error string or empty strinf
// Return an error string, or an empty string on success.
std::string load();
// Treat a missing config as default state; otherwise load it normally.
std::string load_if_exists();
// Store the slic3r.ini into a user profile directory (or a datadir, if configured).
void save();
+6 -6
View File
@@ -57,24 +57,24 @@ void ArcFitter::do_arc_fitting(const Points& points, std::vector<PathFittingData
//BBS: can be fit as arc, then save arc data temperarily
last_arc = target_arc;
if (back_index == points.size() - 1) {
result.emplace_back(std::move(PathFittingData{ front_index,
result.emplace_back(PathFittingData{ front_index,
back_index,
last_arc.direction == ArcDirection::Arc_Dir_CCW ? EMovePathType::Arc_move_ccw : EMovePathType::Arc_move_cw,
last_arc }));
last_arc });
front_index = back_index;
}
} else {
if (back_index - front_index > 2) {
//BBS: althought current point_stack can't be fit as arc,
//but previous must can be fit if removing the top in stack, so save last arc
result.emplace_back(std::move(PathFittingData{ front_index,
result.emplace_back(PathFittingData{ front_index,
back_index - 1,
last_arc.direction == ArcDirection::Arc_Dir_CCW ? EMovePathType::Arc_move_ccw : EMovePathType::Arc_move_cw,
last_arc }));
last_arc });
} else {
//BBS: save the first segment as line move when 3 point-line can't be fit as arc move
if (result.empty() || result.back().path_type != EMovePathType::Linear_move)
result.emplace_back(std::move(PathFittingData{front_index, front_index + 1, EMovePathType::Linear_move, ArcSegment()}));
result.emplace_back(PathFittingData{front_index, front_index + 1, EMovePathType::Linear_move, ArcSegment()});
else if(result.back().path_type == EMovePathType::Linear_move)
result.back().end_point_index = front_index + 1;
}
@@ -87,7 +87,7 @@ void ArcFitter::do_arc_fitting(const Points& points, std::vector<PathFittingData
//BBS: handle the remain data
if (front_index != back_index) {
if (result.empty() || result.back().path_type != EMovePathType::Linear_move)
result.emplace_back(std::move(PathFittingData{front_index, back_index, EMovePathType::Linear_move, ArcSegment()}));
result.emplace_back(PathFittingData{front_index, back_index, EMovePathType::Linear_move, ArcSegment()});
else if (result.back().path_type == EMovePathType::Linear_move)
result.back().end_point_index = back_index;
}
+1 -1
View File
@@ -797,7 +797,7 @@ public:
}
});
m_pck.unfitIndicator([this](std::string name) {
m_pck.unfitIndicator([](std::string name) {
BOOST_LOG_TRIVIAL(debug) << "arrange progress: " + name;
});
@@ -1,6 +1,7 @@
#include "BlacklistedLibraryCheck.hpp"
#include <cstdio>
#include <boost/filesystem/path.hpp>
#include <boost/nowide/convert.hpp>
#ifdef WIN32
+7 -3
View File
@@ -13,7 +13,6 @@ BuildVolume::BuildVolume(const std::vector<Vec2d> &printable_area, const double
: m_bed_shape(printable_area), m_max_print_height(printable_height), m_extruder_shapes(extruder_areas), m_extruder_printable_height(extruder_printable_heights)
{
assert(printable_height >= 0);
//assert(extruder_printable_heights.size() == extruder_areas.size());
m_polygon = Polygon::new_scale(printable_area);
assert(m_polygon.is_counter_clockwise());
@@ -86,6 +85,9 @@ BuildVolume::BuildVolume(const std::vector<Vec2d> &printable_area, const double
m_shared_volume.data[2] = m_bboxf.max.x();
m_shared_volume.data[3] = m_bboxf.max.y();
m_shared_volume.zs[1] = m_bboxf.max.z();
if (extruder_printable_heights.size() < m_extruder_shapes.size())
BOOST_LOG_TRIVIAL(warning) << boost::format("extruder_printable_height has only %1% entries but extruder_printable_area has %2%, falling back to the bed printable_height for the missing ones")
% extruder_printable_heights.size() % m_extruder_shapes.size();
for (unsigned int index = 0; index < m_extruder_shapes.size(); index++)
{
std::vector<Vec2d>& extruder_shape = m_extruder_shapes[index];
@@ -100,7 +102,9 @@ BuildVolume::BuildVolume(const std::vector<Vec2d> &printable_area, const double
return;
}
if ((extruder_shape == printable_area)&&(extruder_printable_heights[index] == printable_height)) {
const double extruder_height = index < extruder_printable_heights.size() ? extruder_printable_heights[index] : printable_height;
if ((extruder_shape == printable_area)&&(extruder_height == printable_height)) {
extruder_volume.same_with_bed = true;
extruder_volume.type = m_type;
extruder_volume.bbox = m_bbox;
@@ -113,7 +117,7 @@ BuildVolume::BuildVolume(const std::vector<Vec2d> &printable_area, const double
double poly_area = poly.area();
extruder_volume.bbox = get_extents(poly);
BoundingBoxf temp_bboxf = get_extents(extruder_shape);
extruder_volume.bboxf = BoundingBoxf3{ to_3d(temp_bboxf.min, 0.), to_3d(temp_bboxf.max, extruder_printable_heights[index]) };
extruder_volume.bboxf = BoundingBoxf3{ to_3d(temp_bboxf.min, 0.), to_3d(temp_bboxf.max, extruder_height) };
if (extruder_shape.size() >= 4 && std::abs((poly_area - double(extruder_volume.bbox.size().x()) * double(extruder_volume.bbox.size().y()))) < sqr(SCALED_EPSILON))
{
File diff suppressed because it is too large Load Diff
+776
View File
@@ -0,0 +1,776 @@
#ifndef slic3r_CadDocument_hpp_
#define slic3r_CadDocument_hpp_
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/CAD/SketchEngine.hpp"
#include "libslic3r/CAD/GeometryEngine.hpp" // FaceGroup
#include "libslic3r/Color.hpp" // ColorRGBA (per-body display colour override)
#include <TopoDS_Shape.hxx>
#include <TopoDS_Wire.hxx>
#include <cereal/cereal.hpp>
#include <cereal/types/vector.hpp>
#include <cereal/types/string.hpp>
#include <map>
#include <cereal/types/map.hpp>
#include <string>
#include <vector>
#include <utility>
namespace Slic3r {
enum class CadFeatureType { Sketch, Extrude, Fillet, Chamfer, Hole, Thread, Shell, Revolve, Sweep, Pattern, Plane, Loft, Draft, Import, Boolean, Cut, Mirror, Axis, CoordSys, Helix, Transform, Thicken, Project, DeleteFace, Rib, SurfaceExtrude, SurfaceRevolve, ThickenSurface, SurfaceOffset, SurfaceLoft, SurfaceFill, Mate };
enum class SketchShape { Rectangle, Circle };
enum class PlaneType { Offset, Angle, Midplane, Tangent, TwoEdges, Coincident };
enum class AxisType { TwoPoints, FaceNormal, CylinderCenterline, PlaneIntersection, AlongEdge };
enum class CoordSysType { PointWorld, FaceAndDirection };
enum class BooleanMode { New, Add, Cut, Intersect };
enum class ExtrudeEnd { Blind, Symmetric, TwoSided, ThroughAll, UpToFace, UpToVertex };
// Serialize a TopoDS_Shape to/from a BRep string (declared before CadFeature so its
// inline cereal save()/load() can resolve these non-dependent calls).
std::string brep_to_string(const TopoDS_Shape& s);
TopoDS_Shape brep_from_string(const std::string& d);
struct CadFeature {
CadFeatureType type{CadFeatureType::Sketch};
std::string name;
bool enabled{true};
// Sketch params (centered on the plane origin)
SketchShape shape{SketchShape::Rectangle};
SketchPlane plane{SketchPlane::XY()};
double width{20};
double height{20};
double radius{10};
// Real 2D sketch geometry (Onshape-style). When non-empty this takes
// precedence over the shape/width/height/radius enum path in build_sketch_wire.
SketchProfile profile;
// Onshape-style multi-entity sketch geometry. When non-empty this takes
// precedence over both `profile` and the shape-enum path in build_sketch_wire.
std::vector<SketchEntity> entities;
// 2D geometric constraints on `profile` (point indices). Solved in place.
std::vector<SketchConstraintDef> constraints;
// Onshape-style constraints on `entities` (Fase 4.2). Solved in place against
// entity endpoints. Used when `entities` is non-empty (the legacy `constraints`
// vector applies only to the `profile` path).
std::vector<SketchEntityConstraintDef> entity_constraints;
// Imported rigid 2D art (Text glyphs / SVG vector paths) as filled regions.
// Each region: contour[0] = outer loop, contour[1..] = holes; points in
// plane (u,v) millimetres. Rendered as a sketch overlay and extruded via a
// faces-with-holes path (SketchEngine::make_extrude_regions) — deliberately
// NOT solver entities, so imported art contributes zero DoF and never
// pollutes the constraint solver / DoF readout. When non-empty it takes
// precedence over the entities/profile/shape paths in the Extrude case.
std::vector<std::vector<std::vector<Vec2d>>> imported_regions;
// Imported rigid 3D B-rep solid (STEP). When the feature type is Import this carries
// the OCCT shape verbatim — it is adopted as a base body in route_feature (no parametric
// recipe). Downstream face/edge features (fillet/chamfer/cut/shell/...) act on it like any
// other body. TopoDS_Shape is a cheap handle, so copying it through recompute/checkpoint
// snapshots is cheap. In-session only for now (no BRep serialization yet).
TopoDS_Shape imported_solid;
// Non-destructive placement transform for imported_regions (Text/SVG),
// applied at display + extrude time as
// p -> (p.x*import_scale_x + import_offset.x, p.y*import_scale_y + import_offset.y).
// Lets the art be moved / enlarged / stretched (independent X/Y) repeatedly
// without re-vectorising. Identity = no change.
Vec2d import_offset{0, 0};
double import_scale_x{1.0};
double import_scale_y{1.0};
// Text/SVG dropped ONTO a solid face (centred on it): the extrude then defaults to an
// inward Cut (engraving) targeting `import_face_body`. False = free art on a plane.
bool import_on_face{false};
int import_face_body{-1};
// Extrude params
int sketch_ref{-1}; // index into features[] of the consumed sketch
double distance{10};
bool symmetric{false};
BooleanMode mode{BooleanMode::New};
ExtrudeEnd extrude_end{ExtrudeEnd::Blind};
double distance2{0}; // second-side depth for TwoSided
double taper_deg{0}; // draft angle (C4-part2)
bool flip{false}; // reverse the extrude direction (negate plane normal)
int up_to_face{-1}; // target solid-face id for UpToFace (C4-part2)
int extrude_src_face{-1}; // global face id on the current body to extrude as a profile; -1 = use sketch wire
Vec3d up_to_point{0,0,0}; // target for UpToVertex (C4-part2)
// Multi-body target: which body (index into CadDocument::bodies) this feature acts on.
// -1 = auto (last body). A New extrude appends a fresh body; Add/Cut/Intersect, dress-up,
// hole and face-extrude(non-New) mutate bodies[target]; face-extrude reads its source
// face from bodies[target] too. The source-face owner for face-extrude lives here.
int target_body{-1};
// Dress-up params (Fillet/Chamfer) — applied to the current body in order
double dressup_size{1.0}; // fillet radius or chamfer distance
FaceGroup face_group{FaceGroup::All};
int dressup_edge{-1}; // global edge id for edge-targeted fillet/chamfer; -1 = use face_group
// Hole params (positioned circular cut into the current body)
double hole_diameter{5};
double hole_depth{10};
bool hole_through{true}; // true = symmetric through-cut, ignores hole_depth
double hole_x{0}; // position on the plane (plane u/x axis)
double hole_y{0}; // position on the plane (plane v/y axis)
// Hole standards library (extends the plain bore above).
// hole_style: 0 = simple, 1 = counterbore, 2 = countersink.
int hole_style{0};
double hole_cbore_diameter{0}; // counterbore cylinder diameter (mm), style==1
double hole_cbore_depth{0}; // counterbore depth from the entry face (mm), style==1
double hole_csink_diameter{0}; // countersink major diameter at entry face (mm), style==2
double hole_csink_angle{90}; // countersink included angle (degrees), style==2
std::string hole_standard; // provenance only, e.g. "M6" / "1/4-20"; not used by geometry
// Thread params (helical thread about the plane normal at a positioned point)
double thread_radius{5}; // nominal cylinder radius
double thread_pitch{2}; // axial advance per turn
double thread_height{10}; // total axial length
double thread_depth{1}; // radial crest depth of the thread profile
bool thread_internal{false}; // false = external threaded rod (New body);
// true = tapped bore cut into the current body
double thread_x{0}; // axis position on the plane (u/x axis)
double thread_y{0}; // axis position on the plane (v/y axis)
// Shell params (hollow the current body to a wall thickness, removing one open face)
double shell_thickness{2}; // wall thickness (inward offset)
int shell_face{-1}; // global face id to remove (open the shell); -1 = none
// Draft params (taper a single solid face about a neutral plane = body bbox bottom, pull +Z)
int draft_face{-1}; // global face id to draft; -1 = none
double draft_angle{5}; // draft angle in degrees (signed: + leans the face inward)
// Revolve params (sweep a profile about an in-plane axis through the plane origin).
// Reuses sketch_ref / entities (profile), flip (direction), mode (boolean) and
// target_body. revolve_axis: 0 = plane X axis, 1 = plane Y axis.
double revolve_angle{360}; // sweep angle in degrees (1..360)
int revolve_axis{0}; // 0 = plane X, 1 = plane Y
// Sweep: profile carried by sketch_ref / entities (like Extrude); the spine is a
// second Sketch referenced by sweep_path_ref (an open or closed wire). Reuses
// mode (boolean) and target_body.
int sweep_path_ref{-1}; // index into features[] of the path Sketch
// Loft: build a solid through 2+ closed profile Sketches (loft_profile_refs, in
// order, each on its own plane). loft_ruled=false → smooth sections, true → ruled.
// Reuses mode (boolean) and target_body.
std::vector<int> loft_profile_refs; // ordered indices into features[] of profile Sketches
bool loft_ruled{false};
// Pattern: replicate the target body, copies fused into it. pattern_circular=false
// → linear (pattern_count instances spaced pattern_spacing along plane axis
// pattern_dir: 0=X, 1=Y); true → circular (pattern_count instances over
// pattern_angle° total about the plane normal through the plane origin, so a seed
// offset from the origin orbits the axis). Reuses target_body + plane.
bool pattern_circular{false};
int pattern_count{3}; // total instances incl. the seed (>=1)
double pattern_spacing{20}; // linear step (mm)
int pattern_dir{0}; // linear direction: 0 = plane X, 1 = plane Y
double pattern_angle{360}; // circular total angle (degrees)
// Pattern along a curve: when pattern_curve_sketch >= 0 this mode takes precedence over
// linear/circular. Copies are placed at equal-parameter points along entity
// pattern_curve_entity of sketch pattern_curve_sketch, translated by (P_i - P_0).
int pattern_curve_sketch{-1}; // feature index of the Sketch holding the guide curve
int pattern_curve_entity{-1}; // entity index of the guide curve within that sketch
// Parametric bindings: field-member-name -> expression string. On recompute() each entry
// is evaluated against the document variables and written into the named numeric field
// BEFORE geometry runs. Empty (the common case) means the feature uses its literal fields.
std::map<std::string, std::string> expr;
// Datum/reference plane: a derived SketchPlane the document offers as a selectable
// sketch plane (no solid). plane_base selects the reference (0=XY,1=XZ,2=YZ, or 3+N
// = the Nth earlier datum plane); plane_offset shifts along the base normal;
// plane_angle tilts plane_angle° about the base axis plane_axis (0=base X, 1=base Y).
int plane_base{0};
double plane_offset{20};
double plane_angle_tilt{0}; // degrees (named *_tilt to avoid clash w/ revolve)
int plane_axis{0}; // tilt axis: 0 = base X, 1 = base Y
PlaneType plane_type{PlaneType::Offset};
int plane_face_body{-1};
int plane_face{-1};
int plane_face2_body{-1};
int plane_face2{-1};
int plane_edge_body{-1};
int plane_edge{-1};
int plane_edge2_body{-1};
int plane_edge2{-1};
double plane_u_size{60};
double plane_v_size{60};
// Boolean: combine two EXISTING bodies. `mode` reuses BooleanMode (Add = union,
// Cut = subtract tool from target, Intersect = keep overlap; New unused). `target_body`
// is the body that survives (result written back to it); `bool_tool_body` is the other
// operand, consumed (erased) unless `bool_keep_tool`. `bool_tolerance` = OCCT fuzzy value
// (0 = exact). Per-face merge: when both bool_target_face/bool_tool_face are set, the tool
// is first snapped so those two faces are coincident (gap closed within bool_tolerance),
// then the boolean welds them and coplanar faces are unified into one clean face.
int bool_tool_body{-1};
bool bool_keep_tool{false};
double bool_tolerance{0.0};
int bool_target_face{-1}; // global face id on the target body to mate (-1 = none)
int bool_tool_face{-1}; // global face id on the tool body to mate (-1 = none)
// Cut: split one target body with a plane, keeping the upper half, lower half, or both.
// Reuses `plane` for the cut plane and `target_body` for which body is cut.
double cut_offset{0.0}; // offset along the cut-plane normal (mm)
bool cut_flip{false}; // flip the normal => swaps which side is "upper"
bool cut_keep_upper{true}; // keep the +normal half
bool cut_keep_lower{false}; // keep the -normal half (both => split into two bodies)
// Mirror: reflect a body about a plane. Reuses `plane` (mirror plane, as Cut does),
// `target_body` (body to mirror), and `mode` (New = separate mirrored copy,
// Add = fuse the mirror back into the source). mirror_keep_original decides whether
// the source body survives when mode is New.
bool mirror_keep_original{true};
// Datum axis: reference line (no solid). Construction params stored; resolve_datum_axes()
// computes the world-space origin + unit direction on demand.
AxisType axis_type{AxisType::TwoPoints};
Vec3d axis_p1{0, 0, 0};
Vec3d axis_p2{0, 0, 10};
int axis_body{-1};
int axis_face{-1};
int axis_edge{-1};
int axis_plane_a{-1};
int axis_plane_b{-1};
// Datum coordinate system (no solid). Stored as point + two orthonormal axes.
CoordSysType coordsys_type{CoordSysType::PointWorld};
Vec3d coordsys_point{0, 0, 0};
int coordsys_body{-1};
int coordsys_face{-1};
int coordsys_edge{-1};
Vec3d coordsys_x_hint{1, 0, 0};
// Fingerprint of the face this connector was bound to, for drift detection. -1 = not yet
// recorded (an old recipe, or a connector that has never resolved).
//
// Surface TYPE and EDGE COUNT specifically, because they survive every legitimate edit:
// Transform moves the body, Draft tilts the face, a dimension change resizes it, and none
// of those change either value. Centroid, area and normal all fail that test — see the
// issue. The cost is that a slide from one planar 4-edge face to another planar 4-edge face
// is invisible; a detector that never cries wolf is worth more here than a total one.
int coordsys_face_kind{-1}; // GeomAbs_SurfaceType as int
int coordsys_face_edges{-1}; // number of edges bounding the face
// Helix curve params (consumed as a sweep path to build springs/coils/augers).
// Axis = plane normal through plane origin. pitch = axial rise per full turn.
// left_handed flips the winding direction. taper_deg != 0 gives a conical helix.
double helix_radius{10};
double helix_pitch{5};
double helix_height{20};
bool helix_left_handed{false};
double helix_taper_deg{0};
// Transform feature: rigid move/rotate of an existing body. Rotation is applied
// first (about xf_axis through xf_pivot), then the translation.
Vec3d xf_translate{0, 0, 0};
Vec3d xf_axis{0, 0, 1};
Vec3d xf_pivot{0, 0, 0};
double xf_angle_deg{0};
bool xf_copy{false}; // true: keep the original, append the moved copy as a new body
// Thicken feature: offset one face of an existing body into a new thin solid body.
// The face belongs to `target_body`; the offset runs along the face normal.
int thicken_face{-1}; // global face id on the target body; -1 = invalid
double thicken_thickness{2}; // wall thickness (always used as |value|)
bool thicken_flip{false}; // true: offset against the face normal
// Cut-by-face: when cut_face >= 0, apply_cut derives the cut plane from this face
// (via SketchPlane::from_face) instead of the base `plane`. cut_offset / cut_flip
// still apply along the derived normal.
int cut_face_body{-1}; // body owning the face; -1 = the target body
int cut_face{-1}; // global face id to cut along; -1 = use `plane`
// Project feature: convert edges of an existing solid into sketch entities on `plane`.
int project_source_body{-1}; // body owning the edges; -1 = last body
std::vector<int> project_edges; // global edge ids to project; empty => use project_face
int project_face{-1}; // if project_edges empty, project every edge of this face
// Direct edit: faces to remove (global face indices into target_body's shape),
// healed via BRepAlgoAPI_Defeaturing.
std::vector<int> delete_faces;
// Rib: a thin wall grown from an open sketch line, fused to the body.
int rib_sketch_ref{-1}; // feature index of the Sketch holding the profile
int rib_entity{-1}; // index of the open Line entity within that sketch
double rib_thickness{2}; // wall thickness (mm), centred on the line
double rib_depth{10}; // extrude distance along the sketch-plane normal (mm)
// --- Mate (assembly) ---
// 0 Fastened — all 6 DOF fixed: B's frame is driven onto A's exactly.
// 1 Planar — z axes aligned, normal distance set to mate_offset; in-plane position free.
// 2 Revolute — axes collinear, position on the axis fixed; rotation about z free.
// 3 Slider — orientation fully fixed, perpendicular position fixed; axial slide free.
// 4 Cylindrical— axes collinear, perpendicular fixed; both spin and axial slide free.
// A "free" DOF is preserved from the body's current placement, not zeroed.
int mate_kind{0};
int mate_cs_a{-1}; // feature index of the FIXED CoordSys (mate connector A)
int mate_cs_b{-1}; // feature index of the CoordSys on the body that MOVES
double mate_offset{0}; // translation along A's z, mm
double mate_angle{0}; // rotation about A's z, degrees
bool mate_flip{false}; // oppose the two z axes (face-to-face)
template<class Archive>
void save(Archive& ar) const {
std::string brep = (type == CadFeatureType::Import) ? brep_to_string(imported_solid) : std::string();
ar(type, name, enabled, shape, plane, width, height, radius,
profile, entities, constraints, entity_constraints, imported_regions,
import_offset, import_scale_x, import_scale_y, import_on_face, import_face_body,
sketch_ref, distance, symmetric, mode, extrude_end, distance2, taper_deg, flip,
up_to_face, extrude_src_face, up_to_point, target_body,
dressup_size, face_group, dressup_edge,
hole_diameter, hole_depth, hole_through, hole_x, hole_y,
thread_radius, thread_pitch, thread_height, thread_depth, thread_internal, thread_x, thread_y,
shell_thickness, shell_face,
draft_face, draft_angle,
revolve_angle, revolve_axis,
sweep_path_ref, loft_profile_refs, loft_ruled,
pattern_circular, pattern_count, pattern_spacing, pattern_dir, pattern_angle,
plane_base, plane_offset, plane_angle_tilt, plane_axis,
bool_tool_body, bool_keep_tool, bool_tolerance, bool_target_face, bool_tool_face,
cut_offset, cut_flip, cut_keep_upper, cut_keep_lower,
brep,
plane_type, plane_face_body, plane_face, plane_face2_body, plane_face2,
plane_edge_body, plane_edge, plane_edge2_body, plane_edge2, plane_u_size, plane_v_size,
mirror_keep_original,
axis_type, axis_p1, axis_p2, axis_body, axis_face, axis_edge, axis_plane_a, axis_plane_b,
coordsys_type, coordsys_point, coordsys_body, coordsys_face, coordsys_edge, coordsys_x_hint,
helix_radius, helix_pitch, helix_height, helix_left_handed, helix_taper_deg,
xf_translate, xf_axis, xf_pivot, xf_angle_deg, xf_copy,
thicken_face, thicken_thickness, thicken_flip,
cut_face_body, cut_face,
project_source_body, project_edges, project_face,
delete_faces,
hole_style, hole_cbore_diameter, hole_cbore_depth,
hole_csink_diameter, hole_csink_angle, hole_standard,
rib_sketch_ref, rib_entity, rib_thickness, rib_depth,
pattern_curve_sketch, pattern_curve_entity,
expr,
mate_kind, mate_cs_a, mate_cs_b, mate_offset, mate_angle, mate_flip,
coordsys_face_kind, coordsys_face_edges);
}
template<class Archive>
void load(Archive& ar) {
std::string brep;
ar(type, name, enabled, shape, plane, width, height, radius,
profile, entities, constraints, entity_constraints, imported_regions,
import_offset, import_scale_x, import_scale_y, import_on_face, import_face_body,
sketch_ref, distance, symmetric, mode, extrude_end, distance2, taper_deg, flip,
up_to_face, extrude_src_face, up_to_point, target_body,
dressup_size, face_group, dressup_edge,
hole_diameter, hole_depth, hole_through, hole_x, hole_y,
thread_radius, thread_pitch, thread_height, thread_depth, thread_internal, thread_x, thread_y,
shell_thickness, shell_face,
draft_face, draft_angle,
revolve_angle, revolve_axis,
sweep_path_ref, loft_profile_refs, loft_ruled,
pattern_circular, pattern_count, pattern_spacing, pattern_dir, pattern_angle,
plane_base, plane_offset, plane_angle_tilt, plane_axis,
bool_tool_body, bool_keep_tool, bool_tolerance, bool_target_face, bool_tool_face,
cut_offset, cut_flip, cut_keep_upper, cut_keep_lower,
brep,
plane_type, plane_face_body, plane_face, plane_face2_body, plane_face2,
plane_edge_body, plane_edge, plane_edge2_body, plane_edge2, plane_u_size, plane_v_size,
mirror_keep_original,
axis_type, axis_p1, axis_p2, axis_body, axis_face, axis_edge, axis_plane_a, axis_plane_b,
coordsys_type, coordsys_point, coordsys_body, coordsys_face, coordsys_edge, coordsys_x_hint,
helix_radius, helix_pitch, helix_height, helix_left_handed, helix_taper_deg,
xf_translate, xf_axis, xf_pivot, xf_angle_deg, xf_copy,
thicken_face, thicken_thickness, thicken_flip,
cut_face_body, cut_face,
project_source_body, project_edges, project_face,
delete_faces,
hole_style, hole_cbore_diameter, hole_cbore_depth,
hole_csink_diameter, hole_csink_angle, hole_standard,
rib_sketch_ref, rib_entity, rib_thickness, rib_depth,
pattern_curve_sketch, pattern_curve_entity,
expr,
mate_kind, mate_cs_a, mate_cs_b, mate_offset, mate_angle, mate_flip,
coordsys_face_kind, coordsys_face_edges);
imported_solid = brep_from_string(brep);
}
};
// Serialize a TopoDS_Shape to/from a BRep string for cereal persistence.
std::string brep_to_string(const TopoDS_Shape& s);
TopoDS_Shape brep_from_string(const std::string& d);
// One independent solid in a multi-body document.
struct CadBody {
TopoDS_Shape shape;
std::string name;
// The name the USER gave this body. A body is NOT its first feature: an Extrude, a Cut and
// a Fillet all land on the same body, so renaming `source_feature` renames one operation in
// the history, not the object — which is exactly the bug this field exists to end. `name`
// above is the DERIVED label (the maker's name, restamped every recompute) and stays that;
// this one is set only by a rename, carried across recompute() by body index, and written
// into the recipe so it survives save/load.
bool has_user_name{false};
std::string user_name;
// Per-body display colour override (Color tool). When has_color is false the GUI
// falls back to the auto body-index palette. Carried across recompute() by body index.
bool has_color{false};
ColorRGBA color;
// Index into `features` of the feature that CREATED this body, or -1. A body is a
// recomputed result, so without this there is no way back to its maker and "delete this
// body" cannot be expressed at all — the GUI could only answer "select the FEATURE that
// created this body". Stamped in one place, the recompute loop; see the note there for
// why a single "still unset?" test is sufficient and stays correct for new feature types.
int source_feature{-1};
};
// OCCT-only feature tree backing the Design tab. No GUI dependencies (lives in libslic3r).
class CadDocument {
public:
std::vector<CadFeature> features;
// Named document variables: name -> expression. Evaluated topologically each recompute();
// an expression may reference other variables. Feature `expr` bindings resolve against these.
std::map<std::string, std::string> variables;
// Multi-body result of the last replay. A "New" extrude appends a body; other ops
// mutate a target body. Empty after a failed/empty recompute.
std::vector<CadBody> bodies;
TopoDS_Shape body; // compound of all bodies (1 body => that body) — display/compat
TriangleMesh display_mesh; // tessellation of all bodies, concatenated (picking)
std::vector<TriangleMesh> display_body_meshes; // one mesh per body, in `bodies` order (per-body color)
std::vector<int> display_tri_face; // per-triangle face id WITHIN its source body
std::vector<int> display_tri_body; // per-triangle source body index (into bodies)
std::string error; // last recompute error ("" = ok)
// Mate diagnostics, refilled by every recompute(). Non-fatal by design: the
// document still evaluates — this only names what the user should look at.
// .first = index of the offending Mate feature, .second = human-readable reason.
// NOT "over-constraint" — this kernel has no solver, so there is no DOF analysis
// behind these; they are graph facts about which mate drives which body.
std::vector<std::pair<int, std::string>> mate_conflicts;
// Modeling origin: the world point the default XY/XZ/YZ planes pass through. The GUI sets this
// to the bed centre so sketches/datums land in the middle of the bed (not the bed corner =
// world 0). Not serialized — the GUI re-applies it from the live bed on every tab show.
Vec3d modeling_origin{Vec3d::Zero()};
// Tessellation quality, matched to Orca's OWN STEP importer (Format/STEP.hpp defaults:
// linear 0.003, angular 0.5 rad) so a body modelled here reaches the screen at the same
// density as the identical body imported through Prepare. It was 0.01 linear — 3.3x coarser
// than anything else in the app, which is why curved faces read as faceted next to an
// imported part. Angular already matched. Same BRepMesh_IncrementalMesh call, same GLVolume
// path, same shaders: the renderer was never the difference, the mesh fed to it was.
double linear_deflection{0.003};
double angular_deflection{0.5};
int add_sketch(SketchShape shape, const SketchPlane& plane,
double width, double height, double radius,
const std::string& name);
int add_sketch_profile(const SketchProfile& profile, const SketchPlane& plane,
const std::string& name);
// Onshape-style multi-entity sketch: stores the entity list verbatim. When
// non-empty it takes precedence over profile/enum in build_sketch_wire.
int add_sketch_entities(const std::vector<SketchEntity>& entities,
const SketchPlane& plane, const std::string& name,
const std::vector<SketchEntityConstraintDef>& constraints = {});
// Project edges of source_body onto plane, producing a sketch feature whose
// entities are (re)derived on every recompute.
int add_project_edges(int source_body, const std::vector<int>& edge_ids, int face,
const SketchPlane& plane, const std::string& name);
// Onshape's "Use" / SolidWorks' "Convert Entities": project a body's edges onto the plane of
// an EXISTING sketch feature and append them to that sketch as CONSTRUCTION entities, so new
// geometry can be constrained to them. Returns the number of entities appended, or -1 if the
// sketch or body reference is invalid. Unlike add_project_edges this creates no feature: the
// references become part of the sketch that borrows them.
int project_edges_into_sketch(int sketch_feature, int source_body,
const std::vector<int>& edge_ids, int face);
// Append a bridging BSpline entity connecting endpoint `end_a` of entity `ent_a` to
// endpoint `end_b` of entity `ent_b`, both within sketch feature `sketch_ref`. Returns
// the new entity's index within that sketch's entities vector. Non-parametric: computed
// once from the current endpoints (does not auto-follow later solver moves).
int add_bridge(int sketch_ref, int ent_a, int end_a, int ent_b, int end_b,
const std::string& name);
// Solve features[index]'s sketch constraints, writing solved coordinates back
// into its profile.points. No-op (returns true) if the feature has no
// constraints. Returns false if index is invalid / not a Sketch / solve fails.
bool solve_sketch_feature(int index);
int add_extrude(int sketch_ref, double distance, bool symmetric,
BooleanMode mode, const std::string& name);
// Extrude a single loop given directly as entities (sketch_ref = -1, plane carried).
int add_extrude_entities(const std::vector<SketchEntity>& entities,
const SketchPlane& plane, double distance, bool symmetric,
BooleanMode mode, const std::string& name);
// Extrude an existing solid FACE (global face id on the body) as the profile.
int add_extrude_face(int src_face, double distance, bool symmetric,
BooleanMode mode, const std::string& name);
int add_fillet(double radius, FaceGroup faces, const std::string& name);
int add_fillet(double radius, int edge_id, const std::string& name);
int add_chamfer(double distance, FaceGroup faces, const std::string& name);
int add_chamfer(double distance, int edge_id, const std::string& name);
int add_hole(double diameter, double depth, bool through,
double x, double y, const SketchPlane& plane,
const std::string& name);
int add_hole_styled(double diameter, double depth, bool through,
double x, double y, const SketchPlane& plane, int style,
double cbore_diameter, double cbore_depth,
double csink_diameter, double csink_angle,
const std::string& standard, const std::string& name);
int add_hole_standard(const std::string& designation, int style, bool through,
double depth, double x, double y,
const SketchPlane& plane, const std::string& name);
int add_thread(double radius, double pitch, double height, double depth,
bool internal, double x, double y, const SketchPlane& plane,
const std::string& name);
int add_revolve(int sketch_ref, double angle, int axis, bool flip,
BooleanMode mode, const std::string& name);
// Self-contained revolve of a single loop given directly as entities (sketch_ref=-1).
int add_revolve_entities(const std::vector<SketchEntity>& entities,
const SketchPlane& plane, double angle, int axis, bool flip,
BooleanMode mode, const std::string& name);
// Sweep the profile Sketch (profile_sketch_ref) along the path Sketch (path_sketch_ref).
int add_pattern(bool circular, int count, double spacing, int dir,
double angle_deg, int target_body, const std::string& name);
// Pattern `count` copies of `target` along entity `curve_entity` of sketch `curve_sketch`.
int add_pattern_on_curve(int count, int curve_sketch, int curve_entity, int target,
const std::string& name);
int add_sweep(int profile_sketch_ref, int path_sketch_ref, BooleanMode mode,
const std::string& name);
// Loft through the ordered profile Sketches (each a closed wire on its own plane).
int add_loft(const std::vector<int>& profile_refs, bool ruled, BooleanMode mode,
const std::string& name);
// Skin 2+ profile sketches open (no end caps) -> a sheet body.
int add_surface_loft(const std::vector<int>& profile_refs, bool ruled, const std::string& name);
// Fill sketch sketch_ref's closed boundary wire with a smooth face -> a one-face sheet body.
int add_surface_fill(int sketch_ref, const std::string& name);
int add_shell(double thickness, int face, int target_body, const std::string& name);
// Grow a thin rib wall (thickness, depth) from the open Line entity `entity` inside sketch
// feature `sketch_ref`, fused to `target_body`. Returns the new feature index.
int add_rib(int sketch_ref, int entity, double thickness, double depth,
int target_body, const std::string& name);
int add_draft(double angle, int face, int target_body, const std::string& name);
// Boolean between two existing bodies. op reuses BooleanMode (Add=union, Cut=subtract,
// Intersect=common; New invalid). target survives, tool is consumed unless keep_tool.
// tolerance = OCCT fuzzy value; target_face/tool_face (-1 = none) drive the per-face snap+merge.
int add_boolean(BooleanMode op, int target_body, int tool_body, bool keep_tool,
double tolerance, int target_face, int tool_face, const std::string& name);
// Plane Cut (Onshape split-by-plane): trim target_body by the plane (origin offset along
// its normal by `offset`, normal flipped iff `flip`). keep_upper/keep_lower select the
// +normal / -normal half; both => the body is split into two coexisting bodies.
int add_cut(const SketchPlane& plane, double offset, bool flip,
bool keep_upper, bool keep_lower, int target_body, const std::string& name);
// Split target_body along the plane of face `face` (owned by face_body, -1 = target).
// keep_upper/keep_lower select which half survives; both => split into two bodies.
int add_split_by_face(int target_body, int face_body, int face,
bool keep_upper, bool keep_lower, const std::string& name);
int add_mirror(const SketchPlane& plane, int target_body, BooleanMode mode,
const std::string& name);
// Rigid body transform: rotate `angle_deg` about `axis` through `pivot`, then translate.
// copy=true keeps the source body and appends the transformed one as a new body.
int add_transform(int target_body, const Vec3d& translate, const Vec3d& axis,
const Vec3d& pivot, double angle_deg, bool copy, const std::string& name);
// Offset face `face` of `target_body` by `thickness` along its normal, producing a new
// thin solid appended as a new body. flip=true offsets against the normal.
int add_thicken(int target_body, int face, double thickness, bool flip, const std::string& name);
// Thicken an entire SHEET body's shell into a solid.
int add_thicken_surface(int target_body, double thickness, bool flip, const std::string& name);
// Offset a SHEET body's shell by a signed distance, producing another SHEET body.
int add_surface_offset(int target_body, double offset, const std::string& name);
int add_delete_face(int target_body, const std::vector<int>& faces,
const std::string& name);
int add_surface_extrude(int sketch_ref, double distance, const std::string& name);
int add_surface_revolve(int sketch_ref, double angle_deg, int axis, const std::string& name);
// Datum plane: derived from base (0=XY/1=XZ/2=YZ/3+N=Nth earlier datum), offset
// along its normal, optional tilt about a base axis. Produces no solid.
int add_plane(int base, double offset, double angle_tilt, int axis,
const std::string& name);
// Datum axis: construction method axis_type determines which ref fields are read.
int add_axis(AxisType axis_type, const std::string& name);
// Datum coordinate system.
int add_coordsys(CoordSysType type, const Vec3d& point, const std::string& name);
int add_mate(int kind, int cs_a, int cs_b, double offset, double angle_deg, bool flip,
const std::string& name);
// Which mate types apply to a connector pair, as reported to the viewport palette.
struct MateOption {
int kind{0}; // 0..4, the five mate types in CadDocument.hpp:308-314
bool viable{true};
std::string reason; // empty when viable; why not, when not
};
// ALWAYS all five entries, ALWAYS in kind order. Never filtered: the caller dims what is
// not viable rather than hiding it, so the list must be stable in length and order between
// calls. Pure query over existing data — records nothing, mutates nothing.
std::vector<MateOption> mate_options(int cs_a, int cs_b) const;
int add_helix(const SketchPlane& plane, double radius, double pitch, double height,
bool left_handed, double taper_deg, const std::string& name);
// Build the helix wire from a Helix feature's params (exposed for tests).
TopoDS_Wire build_helix_wire(const CadFeature& f, std::string& err) const;
// Every datum plane currently in the recipe, in feature order, as (name, plane).
// Used by the GUI to populate plane pickers (after the 3 base planes).
std::vector<std::pair<std::string, SketchPlane>> resolve_datum_planes() const;
// World-space sketch plane lying on a body's PLANAR face, so a face picked in the viewport can
// be sketched on directly — no datum plane in between and nothing to choose from a list.
// Returns false when the indices don't resolve or the face isn't planar (a cylinder or a fillet
// has no single plane, and guessing one from a mid-parameter normal would silently sketch on a
// tangent). Same derivation the Coincident datum method uses, shared so the two cannot drift.
bool plane_of_face(int body_idx, int face_idx, SketchPlane& out) const;
// Resolved datum axes in feature order. axis_err is non-empty if construction failed.
struct DatumAxis { std::string name; Vec3d origin{0,0,0}; Vec3d direction{0,0,1};
std::string error; };
std::vector<DatumAxis> resolve_datum_axes() const;
// Resolved datum coordinate systems. X/Y unit, orthonormal (Z = X.cross(Y)).
struct DatumCoordSys { std::string name; Vec3d origin{0,0,0}; Vec3d x{1,0,0};
Vec3d y{0,1,0}; std::string error; };
std::vector<DatumCoordSys> resolve_datum_coordsys() const;
void clear();
bool recompute(); // replay features -> body + display_mesh; false on error
// CadRecipe serialization contract:
// - v1 blobs are deliberately not loadable; there is no migration path by design
// - append fields ONLY at the end of save/load, never reorder (golden fixture enforces this)
// Bumped every time the bodies are rebuilt, i.e. every time the face and edge MAPS change.
// Global face/edge ids are indices into TopExp::MapShapes and mean nothing across a rebuild,
// so any caller holding an id from an earlier state is holding a wrong one. This is the
// handle that lets it find out instead of silently addressing the wrong edge.
//
// Session-scoped and deliberately NOT serialized: an id is only meaningful within the run
// that produced it, so persisting the counter would imply a promise across loads that the
// ids themselves cannot keep.
uint64_t topo_generation{1};
// v5: every feature is length-framed, so a reader can stop early on an older file and skip
// the tail of a newer one. This is the LAST version that has to break anything — from here a
// new field only needs appending to save/load, with no bump and no orphaned projects.
// v6: no wire-format change — the bytes are v5's, and both are read by the same framed path.
// The stamp advances only to put a project-container change on the record; the 3MF backends
// own that story. v4 still opens through the pre-framing flat path.
static constexpr uint32_t ORCA_CAD_RECIPE_VERSION = 6;
std::string serialize_recipe() const;
bool deserialize_recipe(const std::string& blob);
// Export every body to a STEP file as native B-rep (not mesh). body_xforms is the
// per-body display transform (Move gizmo); when supplied the bodies are written at
// those positions so the STEP matches what Commit ships. false + err on failure.
bool export_step(const std::string& path,
const std::vector<Transform3d>& body_xforms,
std::string& err) const;
GeometryEngine::MassProps body_mass_properties(int body_index) const;
// One overlapping pair of solid bodies. Indices are into `bodies`, a_ < b_.
struct Interference { int body_a{-1}; int body_b{-1}; double volume{0}; };
// Every pair of solid bodies whose intersection encloses more than min_volume (mm^3).
// Reports only — mutates nothing, so mates and placements are unaffected by calling it.
// Sheet bodies are skipped: an intersection involving one encloses no volume.
std::vector<Interference> check_interference(double min_volume = 1e-6) const;
// ponytail: derived from the OCCT shape type; no stored flag, bodies aren't serialized anyway.
static bool is_sheet_shape(const TopoDS_Shape& s); // true if TopExp finds no TopAbs_SOLID
// Undo/redo of the feature recipe (Onshape-style Ctrl+Z). The caller marks a
// user-action boundary by calling checkpoint() BEFORE the mutation(s) for that
// action (add/delete/move/replace, or a direct features edit). undo()/redo() then
// restore the snapshot and recompute(). Because everything else (bodies/meshes/
// body) is derived by recompute(), snapshotting `features` alone is a complete,
// exact history; one checkpoint == one Ctrl+Z step.
void checkpoint(); // snapshot `features` for undo + invalidate redo
// Drop the most recent checkpoint. For a mutation that took a checkpoint, then failed
// and restored the pre-mutation state itself (the constraint paths reject an
// over-constrained addition this way): the snapshot now describes a state identical to
// the current one, and leaving it turns the next Ctrl+Z into a press that does nothing.
void abandon_checkpoint();
bool can_undo() const { return !m_undo.empty(); }
bool can_redo() const { return !m_redo.empty(); }
size_t undo_depth() const { return m_undo.size(); }
size_t redo_depth() const { return m_redo.size(); }
bool undo(); // restore the previous feature list + recompute(); false if no history
bool redo(); // re-apply the most recently undone change; false if none
// Feature-tree editing (Onshape-style). All are transactional: they snapshot
// features, mutate, recompute(), and roll back to the snapshot (re-recomputing)
// if the result is invalid — so a failed edit never leaves a broken body.
//
// remove_feature: erase features[index]; deleting a Sketch cascades to the
// Extrude(s) that consume it; surviving sketch_ref indices are remapped.
// move_feature: shift features[index] by delta (-1 up / +1 down), clamped;
// sketch_ref indices of the two swapped slots are remapped.
// replace_feature: overwrite features[index] with `edited` (its name and, for
// an Extrude, its sketch_ref are preserved from the original).
bool remove_feature(int index);
bool move_feature(int index, int delta);
bool replace_feature(int index, const CadFeature& edited);
// replace_sketch_extrude: a box is two linked features (Sketch + Extrude);
// overwrite both slots from one `edited` candidate (sketch params ->
// features[sketch_idx], extrude params -> features[extrude_idx]), keeping
// each slot's name/type and the sketch_ref link. Transactional like above.
bool replace_sketch_extrude(int sketch_idx, int extrude_idx, const CadFeature& edited);
// Apply ONE candidate feature on top of the current committed body and
// tessellate the result into out_mesh, WITHOUT modifying features/body/
// display_mesh. Returns false (with err set) if the candidate is invalid.
// Used by the Design tab to show a translucent ghost before Confirm.
bool preview(const CadFeature& candidate, TriangleMesh& out_mesh, std::string& err) const;
// Same, but also returns the per-body meshes (in `bodies` order; the candidate may append
// one), so the GUI can apply its display-only per-body Move transforms to the ghost and keep
// it overlaid on the moved body instead of floating back at the untransformed origin.
bool preview(const CadFeature& candidate, TriangleMesh& out_mesh,
std::vector<TriangleMesh>& out_body_meshes, std::string& err) const;
private:
TopoDS_Wire build_sketch_wire(const CadFeature& sketch, bool closed_only = false) const;
// The planar region an Extrude sweeps: the sketch's outer loop with its inner loops as
// holes. Falls back to a face over build_sketch_wire() for the legacy profile/shape paths,
// which have no concept of a second loop.
TopoDS_Face build_sketch_face(const CadFeature& sketch) const;
// Apply a single feature to (result, have_body), throwing std::runtime_error on
// failure. `context` is the body whose faces/edges the feature reads (face-extrude
// source, up-to-face target, dress-up, hole) — it differs from `result` only when the
// feature builds a NEW body from an existing one (face-extrude New). Shared by route.
void apply_feature(TopoDS_Shape& result, bool& have_body,
const TopoDS_Shape& context, const CadFeature& f) const;
// Route one feature into the bodies list: resolve its target body, decide whether it
// starts a new body (empty list, or an Extrude with mode New) vs mutates an existing
// one, then apply_feature. Shared by recompute() (replay all) and preview() (candidate).
void route_feature(std::vector<CadBody>& bodies, const CadFeature& f) const;
// Boolean between two existing bodies: resolve target + tool, optionally snap the tool so
// the picked faces mate, run the OCCT op (with fuzzy tolerance), write the result back to the
// target and erase the consumed tool. Mutates the bodies vector directly (unlike apply_feature,
// which works on a single result shape). Throws std::runtime_error on a failed op.
void apply_boolean(std::vector<CadBody>& bodies, const CadFeature& f) const;
void apply_cut(std::vector<CadBody>& bodies, const CadFeature& f) const;
void apply_mirror(std::vector<CadBody>& bodies, const CadFeature& f) const;
void apply_transform(std::vector<CadBody>& bodies, const CadFeature& f) const;
void apply_thicken(std::vector<CadBody>& bodies, const CadFeature& f) const;
void apply_thicken_surface(std::vector<CadBody>& bodies, const CadFeature& f) const;
void apply_surface_offset(std::vector<CadBody>& bodies, const CadFeature& f) const;
void apply_project(const std::vector<CadBody>& bodies, CadFeature& f) const;
static DatumCoordSys datum_frame(const std::vector<CadBody>& bodies, const CadFeature& f);
void apply_mate(std::vector<CadBody>& bodies, const CadFeature& f) const;
void detect_mate_conflicts(); // refills mate_conflicts from the feature list alone
// Undo/redo stacks of recipe snapshots. checkpoint() pushes onto m_undo and clears
// m_redo; undo()/redo() shuffle the current state between them. Capped so a long
// session can't grow unbounded.
//
// The snapshot MUST carry `variables` as well as `features`: a caller that sets a bad
// variable, sees recompute() fail and calls undo() to roll it back would otherwise be
// left with the bad variable still in the document, so every later recompute fails —
// the exact corruption the checkpoint/undo pattern exists to prevent. Not serialized,
// so this changes no on-disk format.
struct Snapshot {
std::vector<CadFeature> features;
std::map<std::string, std::string> variables;
};
std::vector<Snapshot> m_undo;
std::vector<Snapshot> m_redo;
static constexpr size_t k_undo_cap = 200;
};
} // namespace Slic3r
#endif // slic3r_CadDocument_hpp_
+711
View File
@@ -0,0 +1,711 @@
#include "libslic3r/CAD/GeometryEngine.hpp"
#include <BRepMesh_IncrementalMesh.hxx>
#include <BRep_Tool.hxx>
#include <BRepAdaptor_Surface.hxx>
#include <BRepLProp_SLProps.hxx>
#include <gp_Cylinder.hxx>
#include <BRepFilletAPI_MakeFillet.hxx>
#include <BRepFilletAPI_MakeChamfer.hxx>
#include <stdexcept>
#include <TopExp_Explorer.hxx>
#include <TopoDS.hxx>
#include <TopoDS_Face.hxx>
#include <TopoDS_Edge.hxx>
#include <TopExp.hxx>
#include <TopTools.hxx>
#include <TopTools_IndexedMapOfShape.hxx>
#include <Poly_Triangulation.hxx>
#include <gp_Ax2.hxx>
#include <gp_Dir.hxx>
#include <gp_Pnt.hxx>
#include <BRepGProp.hxx>
#include <GProp_GProps.hxx>
#include <GeomLProp_SLProps.hxx>
#include <BRepAdaptor_Curve.hxx>
#include <gp_Circ.hxx>
#include <GCPnts_TangentialDeflection.hxx>
#include <STEPControl_Reader.hxx>
#include <IFSelect_ReturnStatus.hxx>
#include <Standard_Failure.hxx>
#include <BRepExtrema_DistShapeShape.hxx>
#include <BRepBuilderAPI_MakeVertex.hxx>
#include <BRepBuilderAPI_MakeEdge.hxx>
#include <BRepBuilderAPI_MakeWire.hxx>
#include <BRepBuilderAPI_MakeFace.hxx>
#include <BRepBuilderAPI_MakeSolid.hxx>
#include <BRep_Builder.hxx>
#include <TopoDS_Shell.hxx>
#include <TopoDS_Vertex.hxx>
#include <ShapeUpgrade_UnifySameDomain.hxx>
#include <array>
#include <map>
#include <cmath>
namespace Slic3r {
// ---- STEP import (B-rep, not mesh) ----
std::vector<TopoDS_Shape> GeometryEngine::read_step_solids(const std::string& path, std::string& err)
{
err.clear();
std::vector<TopoDS_Shape> out;
try {
STEPControl_Reader reader;
if (reader.ReadFile(path.c_str()) != IFSelect_RetDone) {
err = "cannot read STEP file";
return out;
}
reader.TransferRoots();
const TopoDS_Shape shape = reader.OneShape();
if (shape.IsNull()) { err = "STEP file has no geometry"; return out; }
// One body per top-level solid; fall back to the whole shape (shells/faces) if none.
for (TopExp_Explorer ex(shape, TopAbs_SOLID); ex.More(); ex.Next())
out.push_back(ex.Current());
if (out.empty())
out.push_back(shape);
} catch (const Standard_Failure& e) {
err = e.GetMessageString() ? e.GetMessageString() : "OCCT failed to read STEP";
out.clear();
}
return out;
}
// ---- Mesh -> B-rep (faceted, shared topology by construction) ----
//
// Port of mesh2step's brep_build.py. Two properties are load-bearing and easy to lose:
//
// 1. The edge cache is keyed on the UNORDERED vertex-index pair, and a triangle that walks
// the edge backwards (i > j) gets edge.Reversed(). Consistently-wound meshes (STL/OBJ/3MF
// all are) walk every shared edge in opposite directions from its two adjacent triangles,
// so this reversal is exactly what leaves the faces coherently outward-oriented.
// 2. Degeneracy is split in two, deliberately. A triangle is dropped as sub-resolution noise
// only if its longest edge is below `tolerance` (an absolute floor), while sliver rejection
// is scale-INDEPENDENT (area < 1e-9 * longest_edge^2). Folding the two together under one
// `area < tolerance^2` test rejects legitimate thin CAD slivers whenever tolerance is coarse
// relative to them, turning a watertight input into a falsely-open shell — a real regression
// mesh2step hit on a 62k-triangle mechanical part.
TopoDS_Shape GeometryEngine::mesh_to_brep(const indexed_triangle_set& its,
double tolerance,
double merge_angle_deg,
MeshBrepStats& stats)
{
stats = MeshBrepStats{};
stats.input_tris = int(its.indices.size());
if (tolerance <= 0.0)
throw std::runtime_error("mesh_to_brep: tolerance must be > 0");
if (its.indices.empty())
throw std::runtime_error("mesh_to_brep: mesh has no triangles");
// 1. Tolerance-quantized vertex dedup. A merged vertex keeps the exact coordinates of the
// first input occurrence — vertices are grouped by a cell, never snapped onto its grid.
std::map<std::array<long long, 3>, int> cell_to_new;
std::vector<int> old_to_new(its.vertices.size(), -1);
std::vector<Vec3d> verts;
verts.reserve(its.vertices.size());
for (size_t i = 0; i < its.vertices.size(); ++i) {
const Vec3d p = its.vertices[i].cast<double>();
const std::array<long long, 3> cell{ (long long) std::llround(p.x() / tolerance),
(long long) std::llround(p.y() / tolerance),
(long long) std::llround(p.z() / tolerance) };
auto ins = cell_to_new.emplace(cell, int(verts.size()));
if (ins.second)
verts.push_back(p);
old_to_new[i] = ins.first->second;
}
// 2. Reject degenerate triangles (see the two-part rule in the comment above).
std::vector<Vec3i32> tris;
tris.reserve(its.indices.size());
for (const Vec3i32& t : its.indices) {
const int a = old_to_new[t(0)], b = old_to_new[t(1)], c = old_to_new[t(2)];
if (a == b || b == c || a == c) { ++stats.degenerate_collapsed; continue; }
const Vec3d& pa = verts[a]; const Vec3d& pb = verts[b]; const Vec3d& pc = verts[c];
const double e0 = (pb - pa).norm(), e1 = (pc - pb).norm(), e2 = (pa - pc).norm();
const double longest = std::max(e0, std::max(e1, e2));
if (longest < tolerance) { ++stats.degenerate_collapsed; continue; }
const double area = 0.5 * (pb - pa).cross(pc - pa).norm();
if (area < 1e-9 * longest * longest) { ++stats.degenerate_sliver; continue; }
tris.emplace_back(a, b, c);
}
stats.kept_tris = int(tris.size());
if (tris.empty())
throw std::runtime_error("mesh_to_brep: every triangle was rejected as degenerate "
"(try a smaller tolerance)");
// 3. One face per triangle, sharing vertices and edges through the caches.
std::vector<TopoDS_Vertex> vertex_cache(verts.size());
std::vector<bool> vertex_made(verts.size(), false);
auto get_vertex = [&](int i) -> const TopoDS_Vertex& {
if (!vertex_made[i]) {
const Vec3d& p = verts[i];
vertex_cache[i] = BRepBuilderAPI_MakeVertex(gp_Pnt(p.x(), p.y(), p.z())).Vertex();
vertex_made[i] = true;
}
return vertex_cache[i];
};
std::map<std::pair<int, int>, TopoDS_Edge> edge_cache;
std::map<std::pair<int, int>, int> edge_usage;
auto get_edge = [&](int i, int j) -> TopoDS_Edge {
const std::pair<int, int> key = (i < j) ? std::make_pair(i, j) : std::make_pair(j, i);
++edge_usage[key];
auto it = edge_cache.find(key);
if (it == edge_cache.end())
it = edge_cache.emplace(key,
BRepBuilderAPI_MakeEdge(get_vertex(key.first), get_vertex(key.second)).Edge()).first;
return (i > j) ? TopoDS::Edge(it->second.Reversed()) : it->second;
};
BRep_Builder builder;
TopoDS_Shell shell;
builder.MakeShell(shell);
for (const Vec3i32& t : tris) {
try {
BRepBuilderAPI_MakeWire mk_wire(get_edge(t(0), t(1)), get_edge(t(1), t(2)), get_edge(t(2), t(0)));
if (!mk_wire.IsDone()) { ++stats.faces_failed; continue; }
BRepBuilderAPI_MakeFace mk_face(mk_wire.Wire());
if (!mk_face.IsDone()) { ++stats.faces_failed; continue; }
builder.Add(shell, mk_face.Face());
++stats.faces_built;
} catch (const Standard_Failure&) {
++stats.faces_failed;
}
}
// 4. Watertightness falls straight out of the usage counts the cache already gathered.
for (const auto& kv : edge_usage) {
if (kv.second == 1) ++stats.boundary_edges;
else if (kv.second >= 3) ++stats.nonmanifold_edges;
}
stats.unique_edges = int(edge_usage.size());
stats.watertight = stats.boundary_edges == 0 && stats.nonmanifold_edges == 0 && stats.unique_edges > 0;
TopoDS_Shape shape = shell;
if (stats.watertight && stats.faces_built > 0) {
BRepBuilderAPI_MakeSolid mk_solid(shell);
if (mk_solid.IsDone()) {
TopoDS_Solid solid = mk_solid.Solid();
GProp_GProps props;
BRepGProp::VolumeProperties(solid, props);
double vol = props.Mass();
if (vol < 0.0) { // inward-wound input
solid = TopoDS::Solid(solid.Reversed());
vol = -vol;
}
if (vol > 0.0) {
shape = solid;
stats.is_solid = true;
stats.volume = vol;
}
}
}
// 5. Optional coplanar merge. Faceted output is one planar face per triangle — exact, but
// you cannot meaningfully fillet or extrude a face that IS a single triangle. Merging
// coplanar neighbours is what turns the import into something the face/edge tools can
// actually operate on (a 12-triangle cube collapses to its 6 real faces).
if (merge_angle_deg > 0.0) {
try {
ShapeUpgrade_UnifySameDomain unifier(shape, true, true, true);
unifier.SetAngularTolerance(merge_angle_deg * M_PI / 180.0);
unifier.SetLinearTolerance(tolerance);
unifier.Build();
const TopoDS_Shape merged = unifier.Shape();
if (!merged.IsNull())
shape = merged;
} catch (const Standard_Failure&) {
// Merging is an optimisation, not a correctness step: keep the exact faceted shape.
}
}
stats.faces_final = face_count(shape);
return shape;
}
// ---- Primitive creation ----
TopoDS_Solid GeometryEngine::make_primitive(const PrimitiveParams& params)
{
switch (params.type) {
case PrimitiveType::Box:
return BRepPrimAPI_MakeBox(gp_Pnt(-params.box_w/2, -params.box_d/2, 0),
params.box_w, params.box_d, params.box_h).Solid();
case PrimitiveType::Cylinder:
return BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(0,0,0), gp_Dir(0,0,1)),
params.cyl_radius, params.cyl_height).Solid();
case PrimitiveType::Sphere:
return BRepPrimAPI_MakeSphere(gp_Pnt(0,0,params.sph_radius), params.sph_radius).Solid();
case PrimitiveType::Cone:
return BRepPrimAPI_MakeCone(gp_Ax2(gp_Pnt(0,0,0), gp_Dir(0,0,1)),
params.cone_r1, params.cone_r2, params.cone_height).Solid();
case PrimitiveType::Torus:
return BRepPrimAPI_MakeTorus(gp_Ax2(gp_Pnt(0,0,params.torus_r2), gp_Dir(0,0,1)),
params.torus_r1, params.torus_r2).Solid();
default:
return BRepPrimAPI_MakeBox(gp_Pnt(-10,-10,0), 20,20,20).Solid();
}
}
// ---- Face classification ----
FaceGroup GeometryEngine::classify_face(const TopoDS_Face& face, const TopoDS_Shape& /*solid*/)
{
try {
BRepAdaptor_Surface surf(face);
if (surf.GetType() == GeomAbs_Plane) {
// Sample normal at center UV
double u = (surf.FirstUParameter() + surf.LastUParameter()) / 2.0;
double v = (surf.FirstVParameter() + surf.LastVParameter()) / 2.0;
gp_Pnt pt; gp_Vec du, dv;
surf.D1(u, v, pt, du, dv);
gp_Dir n = du.Crossed(dv);
if (face.Orientation() == TopAbs_REVERSED) n.Reverse();
if (n.Z() > 0.7) return FaceGroup::Top;
if (n.Z() < -0.7) return FaceGroup::Bottom;
return FaceGroup::Lateral;
}
} catch (...) {}
return FaceGroup::Lateral;
}
// ---- Edge collection ----
std::vector<TopoDS_Edge> GeometryEngine::collect_edges(const TopoDS_Shape& solid, FaceGroup target)
{
std::vector<TopoDS_Edge> result;
if (target == FaceGroup::All) {
for (TopExp_Explorer exp(solid, TopAbs_EDGE); exp.More(); exp.Next())
result.push_back(TopoDS::Edge(exp.Current()));
return result;
}
// Build edge-to-face map once
TopTools_IndexedDataMapOfShapeListOfShape edgeFaceMap;
TopExp::MapShapesAndAncestors(solid, TopAbs_EDGE, TopAbs_FACE, edgeFaceMap);
for (TopExp_Explorer edgeExp(solid, TopAbs_EDGE); edgeExp.More(); edgeExp.Next()) {
const TopoDS_Edge& edge = TopoDS::Edge(edgeExp.Current());
if (!edgeFaceMap.Contains(edge)) continue;
const TopTools_ListOfShape& faces = edgeFaceMap.FindFromKey(edge);
bool include = false;
for (auto it = faces.begin(); it != faces.end(); ++it) {
FaceGroup fg = classify_face(TopoDS::Face(*it), solid);
if (target == FaceGroup::Top && fg == FaceGroup::Top) { include = true; break; }
if (target == FaceGroup::Bottom && fg == FaceGroup::Bottom) { include = true; break; }
if (target == FaceGroup::Lateral && fg == FaceGroup::Lateral) { include = true; break; }
}
if (!include && target == FaceGroup::Top) {
for (auto it = faces.begin(); it != faces.end(); ++it) {
if (classify_face(TopoDS::Face(*it), solid) == FaceGroup::Top) { include = true; break; }
}
}
if (!include && target == FaceGroup::Bottom) {
for (auto it = faces.begin(); it != faces.end(); ++it) {
if (classify_face(TopoDS::Face(*it), solid) == FaceGroup::Bottom) { include = true; break; }
}
}
if (target == FaceGroup::Lateral && !include) {
int lateralCount = 0;
for (auto it = faces.begin(); it != faces.end(); ++it) {
if (classify_face(TopoDS::Face(*it), solid) == FaceGroup::Lateral) ++lateralCount;
}
if (lateralCount >= 2) include = true;
}
if (include) result.push_back(edge);
}
return result;
}
// ---- Fillet/Chamfer ----
TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radius, FaceGroup faces)
{
if (radius <= 0.001) return solid;
std::vector<TopoDS_Edge> edges = collect_edges(solid, faces);
if (edges.empty()) return solid;
BRepFilletAPI_MakeFillet fillet(solid);
for (const auto& edge : edges)
fillet.Add(radius, edge);
fillet.Build();
// A too-large radius (e.g. >= half the smallest spanned dimension) makes the
// operation degenerate; OCCT leaves IsDone() false. Report it instead of
// silently returning the unfilleted solid (which reads as a false success).
if (!fillet.IsDone()) throw std::runtime_error("fillet radius too large for this geometry");
return fillet.Shape();
}
TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double distance, FaceGroup faces)
{
if (distance <= 0.001) return solid;
std::vector<TopoDS_Edge> edges = collect_edges(solid, faces);
if (edges.empty()) return solid;
BRepFilletAPI_MakeChamfer chamfer(solid);
for (const auto& edge : edges)
chamfer.Add(distance, edge); // symmetric chamfer
chamfer.Build();
if (!chamfer.IsDone()) throw std::runtime_error("chamfer distance too large for this geometry");
return chamfer.Shape();
}
TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radius, int edge_id)
{
if (radius <= 0.001) return solid;
TopoDS_Edge edge = edge_by_index(solid, edge_id);
if (edge.IsNull()) throw std::runtime_error("apply_fillet: invalid edge id");
BRepFilletAPI_MakeFillet mk(solid);
mk.Add(radius, edge);
mk.Build();
if (!mk.IsDone()) throw std::runtime_error("apply_fillet: OCCT fillet failed");
return mk.Shape();
}
TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double distance, int edge_id)
{
if (distance <= 0.001) return solid;
TopoDS_Edge edge = edge_by_index(solid, edge_id);
if (edge.IsNull()) throw std::runtime_error("apply_chamfer: invalid edge id");
BRepFilletAPI_MakeChamfer mk(solid);
mk.Add(distance, edge);
mk.Build();
if (!mk.IsDone()) throw std::runtime_error("apply_chamfer: OCCT chamfer failed");
return mk.Shape();
}
// ---- Tessellation ----
TriangleMesh GeometryEngine::tessellate(const TopoDS_Shape& shape,
double linear_deflection,
double angular_deflection)
{
BRepMesh_IncrementalMesh mesh(shape, linear_deflection, false, angular_deflection, true);
int nbNodes = 0, nbTri = 0;
for (TopExp_Explorer exp(shape, TopAbs_FACE); exp.More(); exp.Next()) {
TopLoc_Location loc;
Handle(Poly_Triangulation) tri = BRep_Tool::Triangulation(TopoDS::Face(exp.Current()), loc);
if (!tri.IsNull()) { nbNodes += tri->NbNodes(); nbTri += tri->NbTriangles(); }
}
if (nbTri == 0 || nbNodes == 0) return TriangleMesh{};
stl_file stl;
stl.stats.type = inmemory;
stl.stats.number_of_facets = (uint32_t)nbTri;
stl.stats.original_num_facets = stl.stats.number_of_facets;
stl_allocate(&stl);
std::vector<Vec3f> pts; pts.reserve(nbNodes);
int ndOff = 0, trOff = 0;
for (TopExp_Explorer exp(shape, TopAbs_FACE); exp.More(); exp.Next()) {
const TopoDS_Shape& F = exp.Current();
TopLoc_Location loc;
Handle(Poly_Triangulation) tri = BRep_Tool::Triangulation(TopoDS::Face(F), loc);
if (tri.IsNull()) continue;
gp_Trsf T = loc.Transformation();
for (int i = 1; i <= tri->NbNodes(); ++i) {
gp_Pnt p = tri->Node(i); p.Transform(T);
pts.emplace_back(Vec3f(p.X(), p.Y(), p.Z()));
}
auto orient = exp.Current().Orientation();
int ids[3];
for (int i = 1; i <= tri->NbTriangles(); ++i) {
Poly_Triangle t = tri->Triangle(i); t.Get(ids[0], ids[1], ids[2]);
if (orient == TopAbs_REVERSED) std::swap(ids[1], ids[2]);
stl_facet f;
f.vertex[0] = pts[ids[0]+ndOff-1].cast<float>();
f.vertex[1] = pts[ids[1]+ndOff-1].cast<float>();
f.vertex[2] = pts[ids[2]+ndOff-1].cast<float>();
f.extra[0]=0; f.extra[1]=0;
stl_normal n; stl_calculate_normal(n,&f); stl_normalize_vector(n);
f.normal=n; stl.facet_start[trOff+i-1]=f;
}
ndOff += tri->NbNodes(); trOff += tri->NbTriangles();
}
TriangleMesh result; result.from_stl(stl); return result;
}
GeometryEngine::Deviation
GeometryEngine::surface_deviation(const TopoDS_Shape& candidate,
const TopoDS_Shape& reference,
double linear_deflection)
{
Deviation d;
if (candidate.IsNull() || reference.IsNull()) return d;
TriangleMesh mesh = tessellate(candidate, linear_deflection, 0.5);
const auto& verts = mesh.its.vertices;
if (verts.empty()) return d;
double sum = 0.0, sumsq = 0.0;
int n = 0;
for (const auto& v : verts) {
gp_Pnt p(v.x(), v.y(), v.z());
BRepExtrema_DistShapeShape dss(BRepBuilderAPI_MakeVertex(p).Vertex(), reference);
if (!dss.IsDone() || dss.NbSolution() < 1) continue;
double dist = dss.Value();
d.max_mm = std::max(d.max_mm, dist);
sum += dist; sumsq += dist * dist; ++n;
}
d.sample_count = n;
if (n > 0) { d.mean_mm = sum / n; d.rms_mm = std::sqrt(sumsq / n); }
return d;
}
GeometryEngine::MassProps GeometryEngine::mass_properties(const TopoDS_Shape& shape)
{
MassProps p;
if (shape.IsNull()) return p;
try {
// A sheet body (open shell, no solid) encloses nothing, and BRepGProp::VolumeProperties
// integrates the divergence theorem over whatever faces exist — on an open shell that is
// not a volume at all. It came back as 96000 with an inertia diagonal of
// [-4.2e7, -4.2e7, -6.9e7] for a 60x60x40 four-walled box: negative principal moments,
// which no real body can have. The old code then hid the only obvious tell by taking
// std::abs() of the mass. Report the honest answer instead — surface area is still
// meaningful, so this is not a failure, just not a solid.
p.is_solid = TopExp_Explorer(shape, TopAbs_SOLID).More();
if (!p.is_solid) {
GProp_GProps sonly;
BRepGProp::SurfaceProperties(shape, sonly);
p.surface_area = sonly.Mass();
p.valid = true; // the area IS trustworthy; volume/inertia stay zero
return p;
}
GProp_GProps vprops;
BRepGProp::VolumeProperties(shape, vprops);
double mass = vprops.Mass();
if (std::abs(mass) < 1e-30) return p;
p.volume = std::abs(mass);
p.center_of_mass = Vec3d(vprops.CentreOfMass().X(), vprops.CentreOfMass().Y(), vprops.CentreOfMass().Z());
gp_Mat mat = vprops.MatrixOfInertia();
p.inertia = {{
mat(1,1), mat(1,2), mat(1,3),
mat(2,1), mat(2,2), mat(2,3),
mat(3,1), mat(3,2), mat(3,3),
}};
GProp_GProps sprops;
BRepGProp::SurfaceProperties(shape, sprops);
p.surface_area = sprops.Mass();
p.valid = true;
} catch (const Standard_Failure&) {
// leave valid = false
}
return p;
}
std::string GeometryEngine::primitive_name(PrimitiveType type)
{
switch (type) {
case PrimitiveType::Box: return "Box";
case PrimitiveType::Cylinder: return "Cylinder";
case PrimitiveType::Sphere: return "Sphere";
case PrimitiveType::Cone: return "Cone";
case PrimitiveType::Torus: return "Torus";
default: return "Unknown";
}
}
// ---- Topology accessors ----
int GeometryEngine::face_count(const TopoDS_Shape& shape)
{
int n = 0;
for (TopExp_Explorer e(shape, TopAbs_FACE); e.More(); e.Next())
++n;
return n;
}
TopoDS_Face GeometryEngine::face_by_index(const TopoDS_Shape& shape, int index)
{
if (index < 0) return TopoDS_Face();
int ordinal = 0;
for (TopExp_Explorer e(shape, TopAbs_FACE); e.More(); e.Next()) {
if (ordinal == index)
return TopoDS::Face(e.Current());
++ordinal;
}
return TopoDS_Face();
}
std::vector<TopoDS_Face> GeometryEngine::faces_of(const TopoDS_Shape& shape)
{
std::vector<TopoDS_Face> out;
for (TopExp_Explorer e(shape, TopAbs_FACE); e.More(); e.Next())
out.push_back(TopoDS::Face(e.Current())); // same order as face_by_index
return out;
}
std::vector<TopoDS_Edge> GeometryEngine::edges_of(const TopoDS_Shape& shape)
{
TopTools_IndexedMapOfShape map;
TopExp::MapShapes(shape, TopAbs_EDGE, map); // same order as edge_by_index
std::vector<TopoDS_Edge> out;
out.reserve(map.Extent());
for (int i = 1; i <= map.Extent(); ++i)
out.push_back(TopoDS::Edge(map(i)));
return out;
}
std::vector<TopoDS_Edge> GeometryEngine::edges_of_face(const TopoDS_Face& face)
{
std::vector<TopoDS_Edge> result;
TopTools_IndexedMapOfShape map;
TopExp::MapShapes(face, TopAbs_EDGE, map);
for (int i = 1; i <= map.Extent(); ++i)
result.push_back(TopoDS::Edge(map(i)));
return result;
}
std::vector<Vec3d> GeometryEngine::sample_edge_world(const TopoDS_Edge& edge, double chord_tol)
{
if (BRep_Tool::Degenerated(edge))
return {};
BRepAdaptor_Curve curve(edge);
GCPnts_TangentialDeflection disc(curve, 0.1, chord_tol);
std::vector<Vec3d> pts;
if (disc.NbPoints() >= 2) {
for (int i = 1; i <= disc.NbPoints(); ++i) {
gp_Pnt p = disc.Value(i);
pts.emplace_back(p.X(), p.Y(), p.Z());
}
} else {
gp_Pnt p0 = curve.Value(curve.FirstParameter());
gp_Pnt p1 = curve.Value(curve.LastParameter());
pts.emplace_back(p0.X(), p0.Y(), p0.Z());
pts.emplace_back(p1.X(), p1.Y(), p1.Z());
}
return pts;
}
Vec3d GeometryEngine::face_centroid_world(const TopoDS_Face& face)
{
GProp_GProps props;
BRepGProp::SurfaceProperties(face, props);
gp_Pnt c = props.CentreOfMass();
return Vec3d(c.X(), c.Y(), c.Z());
}
Vec3d GeometryEngine::face_normal_world(const TopoDS_Face& face)
{
BRepAdaptor_Surface surf(face);
const double u = 0.5 * (surf.FirstUParameter() + surf.LastUParameter());
const double v = 0.5 * (surf.FirstVParameter() + surf.LastVParameter());
BRepLProp_SLProps props(surf, u, v, 1, 1e-6);
gp_Dir n(0.0, 0.0, 1.0);
if (props.IsNormalDefined()) n = props.Normal();
if (face.Orientation() == TopAbs_REVERSED) n.Reverse(); // outward (account for face winding)
return Vec3d(n.X(), n.Y(), n.Z());
}
GeometryEngine::CylinderFace GeometryEngine::cylinder_of_face(const TopoDS_Face& face)
{
CylinderFace cf;
if (face.IsNull()) return cf;
BRepAdaptor_Surface surf(face);
if (surf.GetType() != GeomAbs_Cylinder) return cf;
const gp_Cylinder cyl = surf.Cylinder();
const gp_Ax1 ax = cyl.Axis();
const Vec3d axis(ax.Direction().X(), ax.Direction().Y(), ax.Direction().Z());
const Vec3d apt (ax.Location().X(), ax.Location().Y(), ax.Location().Z());
cf.radius = cyl.Radius();
// Axial extent: V is the axial parameter on a cylinder; bound the face's two ends and
// order them so `axis` points base -> top.
const double umid = 0.5 * (surf.FirstUParameter() + surf.LastUParameter());
const gp_Pnt e0 = surf.Value(umid, surf.FirstVParameter());
const gp_Pnt e1 = surf.Value(umid, surf.LastVParameter());
double t0 = (Vec3d(e0.X(), e0.Y(), e0.Z()) - apt).dot(axis);
double t1 = (Vec3d(e1.X(), e1.Y(), e1.Z()) - apt).dot(axis);
if (t1 < t0) std::swap(t0, t1);
cf.base = apt + axis * t0;
cf.axis = axis;
cf.height = t1 - t0;
// Internal (bore) vs external: compare the face's outward normal at its centre to the
// outward radial direction. A bore's normal points toward the axis (dot < 0).
const gp_Pnt sp = surf.Value(umid, 0.5 * (surf.FirstVParameter() + surf.LastVParameter()));
const Vec3d S(sp.X(), sp.Y(), sp.Z());
const Vec3d axpt = cf.base + axis * (S - cf.base).dot(axis);
const Vec3d radial = (S - axpt).normalized();
cf.internal = face_normal_world(face).dot(radial) < 0.0;
cf.ok = true;
return cf;
}
GeometryEngine::CylinderFace GeometryEngine::circle_of_edge(const TopoDS_Edge& edge)
{
CylinderFace cf;
if (edge.IsNull()) return cf;
BRepAdaptor_Curve curve(edge);
if (curve.GetType() != GeomAbs_Circle) return cf;
const gp_Circ c = curve.Circle();
const gp_Ax1 ax = c.Axis();
cf.base = Vec3d(c.Location().X(), c.Location().Y(), c.Location().Z());
cf.axis = Vec3d(ax.Direction().X(), ax.Direction().Y(), ax.Direction().Z());
cf.radius = c.Radius();
cf.height = 0.0; // an edge carries no axial extent; the card keeps the current length
cf.internal = false; // ambiguous from an edge alone — default external, user can toggle
cf.ok = true;
return cf;
}
bool GeometryEngine::face_plane_bounds(const TopoDS_Face& face, const Vec3d& origin,
const Vec3d& x_axis, const Vec3d& y_axis,
double& umin, double& umax, double& vmin, double& vmax)
{
umin = vmin = 1e30; umax = vmax = -1e30;
bool any = false;
for (TopExp_Explorer ex(face, TopAbs_VERTEX); ex.More(); ex.Next()) {
const gp_Pnt p = BRep_Tool::Pnt(TopoDS::Vertex(ex.Current()));
const Vec3d P(p.X(), p.Y(), p.Z());
const double u = (P - origin).dot(x_axis);
const double v = (P - origin).dot(y_axis);
umin = std::min(umin, u); umax = std::max(umax, u);
vmin = std::min(vmin, v); vmax = std::max(vmax, v);
any = true;
}
return any;
}
int GeometryEngine::edge_count(const TopoDS_Shape& shape)
{
TopTools_IndexedMapOfShape map;
TopExp::MapShapes(shape, TopAbs_EDGE, map);
return map.Extent();
}
TopoDS_Edge GeometryEngine::edge_by_index(const TopoDS_Shape& shape, int index)
{
TopTools_IndexedMapOfShape map;
TopExp::MapShapes(shape, TopAbs_EDGE, map);
if (index < 0 || index >= map.Extent())
return TopoDS_Edge();
return TopoDS::Edge(map(index + 1));
}
int GeometryEngine::edge_index_of(const TopoDS_Shape& shape, const TopoDS_Edge& edge)
{
TopTools_IndexedMapOfShape map;
TopExp::MapShapes(shape, TopAbs_EDGE, map);
int idx = map.FindIndex(edge);
return (idx > 0) ? (idx - 1) : -1;
}
} // namespace Slic3r
+183
View File
@@ -0,0 +1,183 @@
#ifndef slic3r_GeometryEngine_hpp_
#define slic3r_GeometryEngine_hpp_
#include "libslic3r/TriangleMesh.hpp"
#include <BRepPrimAPI_MakeBox.hxx>
#include <BRepPrimAPI_MakeCylinder.hxx>
#include <BRepPrimAPI_MakeSphere.hxx>
#include <BRepPrimAPI_MakeCone.hxx>
#include <BRepPrimAPI_MakeTorus.hxx>
#include <gp_Ax2.hxx>
#include <TopoDS_Solid.hxx>
#include <TopoDS_Face.hxx>
#include <TopoDS_Edge.hxx>
#include <vector>
#include <string>
namespace Slic3r {
enum class PrimitiveType { Box, Cylinder, Sphere, Cone, Torus, COUNT };
enum class DressUpType { Fillet, Chamfer };
enum class FaceGroup { Top, Bottom, Lateral, All };
struct PrimitiveParams {
PrimitiveType type{PrimitiveType::Box};
double box_w{20}, box_h{20}, box_d{20};
double cyl_radius{10}, cyl_height{20};
double sph_radius{10};
double cone_r1{10}, cone_r2{5}, cone_height{20};
double torus_r1{10}, torus_r2{3};
// Dress-up
bool dressup_enabled{false};
DressUpType dressup_type{DressUpType::Fillet};
FaceGroup dressup_faces{FaceGroup::All};
double dressup_radius{1.0}; // fillet radius
double dressup_chamfer_dist{1.0}; // chamfer distance (symmetric)
// Mesh quality
double linear_deflection{0.01};
double angular_deflection{0.5};
template<class Archive>
void serialize(Archive& ar) {
ar(type, box_w, box_h, box_d, cyl_radius, cyl_height, sph_radius,
cone_r1, cone_r2, cone_height, torus_r1, torus_r2,
dressup_enabled, dressup_type, dressup_faces, dressup_radius, dressup_chamfer_dist,
linear_deflection, angular_deflection);
}
};
class GeometryEngine
{
public:
static TopoDS_Solid make_primitive(const PrimitiveParams& params);
// Read a STEP file into its top-level solids (one TopoDS_Shape per solid; falls back to
// the whole shape if it contains no closed solids). Reuses OCCT's STEPControl_Reader,
// already linked via Format/STEP.cpp — no new dependency. err is set on failure (empty result).
static std::vector<TopoDS_Shape> read_step_solids(const std::string& path, std::string& err);
// Triangle mesh -> B-rep solid. Native port of mesh2step
// (github.com/tommasobbianchi/mesh2step): vertices and edges are SHARED across triangles
// at construction time (vertex cache by deduped index, edge cache by unordered index pair),
// so there is no BRepBuilderAPI_Sewing pass to reconstruct topology afterwards — which is
// both faster and what makes watertightness fall out of the edge-usage counts for free.
// Runs in-process on the OCCT kernel libslic3r already links: no STEP file is written or
// re-read (a faceted STEP of a 62k-triangle mesh is ~149 MB and takes OCCT's reader >300 s
// to parse back, so routing the Design tab through a file would hang the GUI).
struct MeshBrepStats {
int input_tris{0};
int kept_tris{0};
int degenerate_collapsed{0}; // <3 distinct vertices after tolerance quantization
int degenerate_sliver{0}; // 3 distinct vertices but near-collinear
int faces_built{0};
int faces_failed{0};
int unique_edges{0};
int boundary_edges{0}; // used by exactly 1 triangle -> open shell
int nonmanifold_edges{0}; // used by >=3 triangles
bool watertight{false}; // every edge used exactly twice
bool is_solid{false}; // watertight AND MakeSolid gave a positive volume
double volume{0.0};
int faces_final{0}; // after the optional coplanar merge
};
// tolerance: spatial quantization cell used ONLY for vertex dedup and as the
// sub-resolution floor below which a triangle is noise. Never a sew tolerance.
// merge_angle_deg > 0: run ShapeUpgrade_UnifySameDomain to merge coplanar neighbours into
// single faces (a 12-triangle cube -> 6 pickable faces). This is what makes the imported
// body editable with the face/edge tools; <= 0 keeps the exact one-face-per-triangle form.
// Never wraps a non-watertight shell as a fake solid: an open mesh comes back as a shell,
// with the reason (boundary / non-manifold edge counts) reported in stats.
static TopoDS_Shape mesh_to_brep(const indexed_triangle_set& its,
double tolerance,
double merge_angle_deg,
MeshBrepStats& stats);
struct MassProps {
double volume{0.0};
double surface_area{0.0};
Vec3d center_of_mass{Vec3d::Zero()};
std::array<double, 9> inertia{};
bool valid{false};
// False for a sheet body (an open shell with no solid). Volume and inertia are then
// meaningless and are reported as zero; surface_area stays meaningful. See the .cpp.
bool is_solid{false};
};
static MassProps mass_properties(const TopoDS_Shape& shape);
struct Deviation { double max_mm{0}; double mean_mm{0}; double rms_mm{0}; int sample_count{0}; };
static Deviation surface_deviation(const TopoDS_Shape& candidate,
const TopoDS_Shape& reference,
double linear_deflection = 0.5);
static TopoDS_Shape apply_fillet(const TopoDS_Shape& solid, double radius,
FaceGroup faces = FaceGroup::All);
static TopoDS_Shape apply_fillet(const TopoDS_Shape& solid, double radius,
int edge_id);
static TopoDS_Shape apply_chamfer(const TopoDS_Shape& solid, double distance,
FaceGroup faces = FaceGroup::All);
static TopoDS_Shape apply_chamfer(const TopoDS_Shape& solid, double distance,
int edge_id);
static TriangleMesh tessellate(const TopoDS_Shape& shape,
double linear_deflection = 0.01,
double angular_deflection = 0.5);
static std::string primitive_name(PrimitiveType type);
// Topology accessors for in-viewport face/edge picking (Design tab). Face index is the
// TopExp_Explorer(shape, TopAbs_FACE) ordinal — identical to SketchEngine::tessellate's
// per-triangle face id, so a picked triangle's id maps back to a face here.
static TopoDS_Face face_by_index(const TopoDS_Shape& shape, int index); // null if out of range
static int face_count(const TopoDS_Shape& shape);
// Bulk enumeration in the SAME order as face_by_index / edge_by_index, so ids are
// interchangeable. Walking a body with the _by_index accessors is quadratic (each call
// rescans the shape — edge_by_index even rebuilds the whole indexed map), which cost
// ~15 s on a 4.7k-face imported solid; enumerate once instead.
static std::vector<TopoDS_Face> faces_of(const TopoDS_Shape& shape);
static std::vector<TopoDS_Edge> edges_of(const TopoDS_Shape& shape);
static std::vector<TopoDS_Edge> edges_of_face(const TopoDS_Face& face);
// Centre of mass (world) of a face — used to compute the extrude length for "up to face".
static Vec3d face_centroid_world(const TopoDS_Face& face);
// Outward unit normal of a face at its UV midpoint (orientation-aware) — for the shell gizmo.
static Vec3d face_normal_world(const TopoDS_Face& face);
// Sample an edge into a world-space polyline (>=2 pts) for pick-distance + highlight.
static std::vector<Vec3d> sample_edge_world(const TopoDS_Edge& edge, double chord_tol = 0.05);
// 0-based edge index into TopExp::MapShapes(shape, TopAbs_EDGE, map).
static int edge_count(const TopoDS_Shape& shape);
static TopoDS_Edge edge_by_index(const TopoDS_Shape& shape, int index);
static int edge_index_of(const TopoDS_Shape& shape, const TopoDS_Edge& edge);
// Analysis of a cylindrical face for the Thread tool (a hole bore or a cylinder's lateral
// surface): axis (base at the lower axial end + unit direction), radius, axial extent, and
// whether it is a bore (face normal points toward the axis = internal thread). ok=false if
// the face is not a cylinder.
struct CylinderFace {
bool ok{false};
Vec3d base{0, 0, 0};
Vec3d axis{0, 0, 1};
double radius{0};
double height{0};
bool internal{false};
};
static CylinderFace cylinder_of_face(const TopoDS_Face& face);
// Circular edge (a cylinder's perimeter): base = circle centre, axis = circle normal,
// radius = circle radius, height = 0 (unknown from an edge), internal = false. ok=false if
// the edge is not a circle. Lets the Thread tool be driven by a picked circular rim.
static CylinderFace circle_of_edge(const TopoDS_Edge& edge);
// Plane-coordinate (u,v) bounding box of a face's vertices, measured from `origin` along
// `x_axis`/`y_axis`. Lets the Hole tool dimension the hole from the face SIDES (umin/vmin =
// two adjacent edges) instead of from the centre. Returns false if the face has no vertices.
static bool face_plane_bounds(const TopoDS_Face& face, const Vec3d& origin,
const Vec3d& x_axis, const Vec3d& y_axis,
double& umin, double& umax, double& vmin, double& vmax);
private:
static std::vector<TopoDS_Edge> collect_edges(const TopoDS_Shape& solid, FaceGroup faces);
static FaceGroup classify_face(const TopoDS_Face& face, const TopoDS_Shape& solid);
};
} // namespace Slic3r
#endif // slic3r_GeometryEngine_hpp_
+307
View File
@@ -0,0 +1,307 @@
#include "libslic3r/CAD/SketchConstraints.hpp"
#include <Eigen/Dense>
#include <cmath>
namespace Slic3r {
int SketchConstraints::add_point(double x, double y)
{
m_vars.push_back(x);
m_vars.push_back(y);
return static_cast<int>(m_vars.size() / 2) - 1;
}
void SketchConstraints::set_point(int id, double x, double y)
{
size_t idx = 2 * id;
m_vars[idx] = x;
m_vars[idx + 1] = y;
}
Vec2d SketchConstraints::get_point(int id) const
{
size_t idx = 2 * id;
return Vec2d(m_vars[idx], m_vars[idx + 1]);
}
int SketchConstraints::point_count() const
{
return static_cast<int>(m_vars.size() / 2);
}
void SketchConstraints::fix_point(int id)
{
size_t idx = 2 * id;
Con c;
c.type = FIX_POINT;
c.a = id;
c.b = c.c = c.d = 0;
c.k0 = m_vars[idx];
c.k1 = m_vars[idx + 1];
m_cons.push_back(c);
}
void SketchConstraints::coincident(int a, int b)
{
Con c;
c.type = COINCIDENT;
c.a = a; c.b = b; c.c = c.d = 0;
c.k0 = c.k1 = 0;
m_cons.push_back(c);
}
void SketchConstraints::horizontal(int a, int b)
{
Con c;
c.type = HORIZONTAL;
c.a = a; c.b = b; c.c = c.d = 0;
c.k0 = c.k1 = 0;
m_cons.push_back(c);
}
void SketchConstraints::vertical(int a, int b)
{
Con c;
c.type = VERTICAL;
c.a = a; c.b = b; c.c = c.d = 0;
c.k0 = c.k1 = 0;
m_cons.push_back(c);
}
void SketchConstraints::distance(int a, int b, double d)
{
Con c;
c.type = DISTANCE;
c.a = a; c.b = b; c.c = c.d = 0;
c.k0 = d; c.k1 = 0;
m_cons.push_back(c);
}
void SketchConstraints::lock_x(int id, double x)
{
Con c;
c.type = LOCK_X;
c.a = id;
c.b = c.c = c.d = 0;
c.k0 = x; c.k1 = 0;
m_cons.push_back(c);
}
void SketchConstraints::lock_y(int id, double y)
{
Con c;
c.type = LOCK_Y;
c.a = id;
c.b = c.c = c.d = 0;
c.k0 = y; c.k1 = 0;
m_cons.push_back(c);
}
void SketchConstraints::equal_length(int a, int b, int c, int d)
{
Con con;
con.type = EQUAL_LENGTH;
con.a = a; con.b = b; con.c = c; con.d = d;
con.k0 = con.k1 = 0;
m_cons.push_back(con);
}
void SketchConstraints::parallel(int a, int b, int c, int d)
{
Con con;
con.type = PARALLEL;
con.a = a; con.b = b; con.c = c; con.d = d;
con.k0 = con.k1 = 0;
m_cons.push_back(con);
}
void SketchConstraints::perpendicular(int a, int b, int c, int d)
{
Con con;
con.type = PERPENDICULAR;
con.a = a; con.b = b; con.c = c; con.d = d;
con.k0 = con.k1 = 0;
m_cons.push_back(con);
}
void SketchConstraints::midpoint(int m, int a, int b)
{
Con con;
con.type = MIDPOINT;
con.a = m; con.b = a; con.c = b; con.d = -1;
con.k0 = con.k1 = 0;
m_cons.push_back(con);
}
void SketchConstraints::symmetric(int a, int b, int c, int d)
{
Con con;
con.type = SYMMETRIC;
con.a = a; con.b = b; con.c = c; con.d = d;
con.k0 = con.k1 = 0;
m_cons.push_back(con);
}
void SketchConstraints::angle(int a, int b, int c, int d, double radians)
{
Con con;
con.type = ANGLE;
con.a = a; con.b = b; con.c = c; con.d = d;
con.k0 = radians; con.k1 = 0;
m_cons.push_back(con);
}
void SketchConstraints::point_line_distance(int p, int a, int b, double dist)
{
Con con;
con.type = PT_LINE_DIST;
con.a = p; con.b = a; con.c = b; con.d = -1;
con.k0 = dist; con.k1 = 0;
m_cons.push_back(con);
}
Eigen::VectorXd SketchConstraints::residuals(const std::vector<double>& v) const
{
auto X = [&](int i) { return v[2 * i]; };
auto Y = [&](int i) { return v[2 * i + 1]; };
std::vector<double> res;
for (const auto& c : m_cons) {
switch (c.type) {
case FIX_POINT:
res.push_back(X(c.a) - c.k0);
res.push_back(Y(c.a) - c.k1);
break;
case COINCIDENT:
res.push_back(X(c.a) - X(c.b));
res.push_back(Y(c.a) - Y(c.b));
break;
case HORIZONTAL:
res.push_back(Y(c.a) - Y(c.b));
break;
case VERTICAL:
res.push_back(X(c.a) - X(c.b));
break;
case DISTANCE:
res.push_back(std::hypot(X(c.a) - X(c.b), Y(c.a) - Y(c.b)) - c.k0);
break;
case LOCK_X:
res.push_back(X(c.a) - c.k0);
break;
case LOCK_Y:
res.push_back(Y(c.a) - c.k0);
break;
case EQUAL_LENGTH:
res.push_back(std::hypot(X(c.a) - X(c.b), Y(c.a) - Y(c.b)) -
std::hypot(X(c.c) - X(c.d), Y(c.c) - Y(c.d)));
break;
case PARALLEL:
res.push_back((X(c.b) - X(c.a)) * (Y(c.d) - Y(c.c)) -
(Y(c.b) - Y(c.a)) * (X(c.d) - X(c.c)));
break;
case PERPENDICULAR:
res.push_back((X(c.b) - X(c.a)) * (X(c.d) - X(c.c)) +
(Y(c.b) - Y(c.a)) * (Y(c.d) - Y(c.c)));
break;
case MIDPOINT:
res.push_back(X(c.a) - 0.5 * (X(c.b) + X(c.c)));
res.push_back(Y(c.a) - 0.5 * (Y(c.b) + Y(c.c)));
break;
case SYMMETRIC: {
const double abx = X(c.b) - X(c.a), aby = Y(c.b) - Y(c.a);
const double cdx = X(c.d) - X(c.c), cdy = Y(c.d) - Y(c.c);
res.push_back(abx * cdx + aby * cdy);
const double mx = 0.5 * (X(c.a) + X(c.b));
const double my = 0.5 * (Y(c.a) + Y(c.b));
res.push_back((mx - X(c.c)) * cdy - (my - Y(c.c)) * cdx);
break;
}
case ANGLE: {
const double ux = X(c.b) - X(c.a), uy = Y(c.b) - Y(c.a);
const double wx = X(c.d) - X(c.c), wy = Y(c.d) - Y(c.c);
const double cross = ux * wy - uy * wx;
const double dot = ux * wx + uy * wy;
res.push_back(std::atan2(cross, dot) - c.k0);
break;
}
case PT_LINE_DIST: {
const double bx = X(c.b), by = Y(c.b);
const double cx = X(c.c), cy = Y(c.c);
const double L = std::hypot(cx - bx, cy - by);
const double num = (X(c.a) - bx) * (cy - by) - (Y(c.a) - by) * (cx - bx);
res.push_back((L > 1e-12 ? std::abs(num) / L : 0.0) - c.k0);
break;
}
}
}
Eigen::VectorXd r(static_cast<Eigen::Index>(res.size()));
for (size_t i = 0; i < res.size(); ++i)
r(static_cast<Eigen::Index>(i)) = res[i];
return r;
}
Eigen::MatrixXd SketchConstraints::jacobian(const std::vector<double>& v) const
{
int m = static_cast<int>(residuals(v).size());
int n = static_cast<int>(v.size());
Eigen::MatrixXd J(m, n);
const double eps = 1e-7;
std::vector<double> vp = v;
std::vector<double> vm = v;
for (int j = 0; j < n; ++j) {
vp[j] = v[j] + eps;
vm[j] = v[j] - eps;
Eigen::VectorXd rp = residuals(vp);
Eigen::VectorXd rm = residuals(vm);
vp[j] = v[j];
vm[j] = v[j];
J.col(j) = (rp - rm) / (2.0 * eps);
}
return J;
}
bool SketchConstraints::solve(int max_iter, double tol)
{
if (m_cons.empty()) return true;
double lambda = 1e-3;
Eigen::VectorXd r = residuals(m_vars);
for (int it = 0; it < max_iter; ++it) {
double rn = r.norm();
if (rn < tol) return true;
Eigen::MatrixXd J = jacobian(m_vars);
Eigen::MatrixXd A = J.transpose() * J;
Eigen::VectorXd g = J.transpose() * r;
bool stepped = false;
for (int t = 0; t < 12; ++t) {
Eigen::MatrixXd Ad = A;
for (int i = 0; i < Ad.rows(); ++i)
Ad(i, i) += lambda * (1.0 + Ad(i, i));
Eigen::VectorXd dx = Ad.ldlt().solve(-g);
std::vector<double> cand = m_vars;
for (size_t i = 0; i < cand.size(); ++i)
cand[i] += dx[static_cast<Eigen::Index>(i)];
Eigen::VectorXd rc = residuals(cand);
if (rc.norm() < rn) {
m_vars = cand;
r = rc;
lambda = std::max(lambda * 0.4, 1e-12);
stepped = true;
break;
}
lambda *= 3.0;
}
if (!stepped) break;
}
return r.norm() < tol * 100;
}
double SketchConstraints::residual_norm() const
{
return residuals(m_vars).norm();
}
} // namespace Slic3r
+68
View File
@@ -0,0 +1,68 @@
#ifndef slic3r_SketchConstraints_hpp_
#define slic3r_SketchConstraints_hpp_
#include "libslic3r/Point.hpp"
#include <vector>
#include <Eigen/Dense>
namespace Slic3r {
class SketchConstraints {
public:
int add_point(double x, double y);
void set_point(int id, double x, double y);
Vec2d get_point(int id) const;
int point_count() const;
void fix_point(int id);
void coincident(int a, int b);
void horizontal(int a, int b);
void vertical(int a, int b);
void distance(int a, int b, double d);
void lock_x(int id, double x);
void lock_y(int id, double y);
void equal_length(int a, int b, int c, int d);
void parallel(int a, int b, int c, int d);
void perpendicular(int a, int b, int c, int d);
void midpoint(int m, int a, int b);
void symmetric(int a, int b, int c, int d);
void angle(int a, int b, int c, int d, double radians);
void point_line_distance(int p, int a, int b, double dist);
bool solve(int max_iter = 200, double tol = 1e-10);
double residual_norm() const;
private:
std::vector<double> m_vars;
enum ConType : int {
FIX_POINT = 0,
COINCIDENT,
HORIZONTAL,
VERTICAL,
DISTANCE,
LOCK_X,
LOCK_Y,
EQUAL_LENGTH,
PARALLEL,
PERPENDICULAR,
MIDPOINT,
SYMMETRIC,
ANGLE,
PT_LINE_DIST
};
struct Con {
int type;
int a, b, c, d;
double k0, k1;
};
std::vector<Con> m_cons;
Eigen::VectorXd residuals(const std::vector<double>& v) const;
Eigen::MatrixXd jacobian(const std::vector<double>& v) const;
};
} // namespace Slic3r
#endif // slic3r_SketchConstraints_hpp_
File diff suppressed because it is too large Load Diff
+373
View File
@@ -0,0 +1,373 @@
#ifndef slic3r_SketchEngine_hpp_
#define slic3r_SketchEngine_hpp_
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/CAD/GeometryEngine.hpp"
#include <gp_Pln.hxx>
#include <gp_Ax3.hxx>
#include <TopoDS_Wire.hxx>
#include <TopoDS_Shape.hxx>
#include <TopoDS_Face.hxx>
#include <vector>
#include <utility>
namespace Slic3r {
struct SketchSegment {
enum Type { Line, Arc, Circle, Rectangle, Polygon };
Type type{Line};
Vec2d p0{0,0}, p1{0,0};
Vec2d center{0,0};
double radius{0}, start_angle{0}, end_angle{0};
std::vector<Vec2d> points;
template<class Archive>
void serialize(Archive& ar) { ar(type, p0, p1, center, radius, start_angle, end_angle, points); }
};
struct SketchEntity {
enum class Type { Line, Arc, Circle, Point, Ellipse, EllipseArc, BSpline };
Type type{Type::Line};
Vec2d p0{0,0}; // Line: start; Arc/EllipseArc: start; Circle/Point/Ellipse: center; BSpline: first pole
Vec2d p1{0,0}; // Line: end; Arc/EllipseArc: end; (unused for Circle/Point/Ellipse); BSpline: last pole
Vec2d center{0,0}; // Arc/Circle/Ellipse(Arc) center
double radius{0}; // Circle/Arc radius; Ellipse(Arc): semi-major axis (a)
double start_angle{0}; // Arc sweep start; Ellipse(Arc): parametric start angle (radians)
double end_angle{0}; // Arc sweep end; Ellipse(Arc): parametric end angle
bool construction{false};
double rminor{0}; // Ellipse(Arc): semi-minor axis (b)
double rotation{0}; // Ellipse(Arc): major-axis angle phi (radians, about center)
std::vector<Vec2d> ctrl; // BSpline: control points (poles); p0/p1 mirror first/last pole
template<class Archive>
void serialize(Archive& ar) {
// Append-only: rminor/rotation added for Ellipse(Arc) (P2 Tier-B.1); ctrl for BSpline (B.2).
ar(type, p0, p1, center, radius, start_angle, end_angle, construction, rminor, rotation, ctrl);
}
};
struct SketchPlane {
Vec3d origin{0,0,0};
Vec3d normal{0,0,1};
Vec3d x_axis{1,0,0};
Vec3d y_axis{0,1,0};
gp_Pln to_occt() const;
static SketchPlane from_face(const TopoDS_Face& face);
static SketchPlane XY() { return {}; }
static SketchPlane XZ() { return {{0,0,0}, {0,1,0}, {1,0,0}, {0,0,1}}; }
static SketchPlane YZ() { return {{0,0,0}, {1,0,0}, {0,1,0}, {0,0,1}}; }
Vec2d project(const Vec3d& ray_origin, const Vec3d& ray_dir) const;
Vec3d to_world(const Vec2d& pt) const;
template<class Archive>
void serialize(Archive& ar) { ar(origin, normal, x_axis, y_axis); }
};
struct SketchProfile {
std::vector<Vec2d> points;
bool closed{false};
bool is_closed(double tolerance = 0.5) const;
bool try_close(double tolerance = 0.5);
void clear() { points.clear(); closed = false; }
TopoDS_Wire to_occt_wire(const SketchPlane& plane) const;
template<class Archive>
void serialize(Archive& ar) { ar(points, closed); }
};
// Two sketch endpoints this close are ONE joint. Shared deliberately by the viewport
// (region_loops / connected_loop / open-end detection) and by the kernel
// (entities_to_wires): the viewport is what shades a region closed and offers it for
// extrude, so the kernel MUST be able to build every loop the viewport shades. When
// these two numbers disagreed the viewport promised a closed region at 1e-3 and the
// kernel refused it at 1e-4, which extruded a solid the user never drew.
// Nothing legitimate in a mm-scale sketch is 1 um apart.
inline constexpr double kSketchJoinTol = 1e-3; // mm
// Effective sketch joint tolerance. ONE value for the viewport (region_loops /
// loop_report / connected_loop) and the kernel (entities_to_wires): if these ever
// disagree again, the viewport shades a region closed that the kernel refuses to
// build, which is how a sketch got extruded into the wrong solid. The GUI pushes
// the "auto_close_sketch_loops" preference in via set_sketch_auto_close(); the
// kernel defaults to ON so headless/kernel-only callers keep welding.
double sketch_join_tol();
void set_sketch_auto_close(bool on);
enum class SketchConstraintType {
Fix, Coincident, Horizontal, Vertical, Distance,
LockX, LockY, EqualLength, Parallel, Perpendicular,
Concentric,
Tangent, Midpoint, Symmetric, Angle,
Radius, Diameter,
PointOnLine, // a point lies on a line (or at signed perpendicular distance `value`)
PointOnObject, // a point lies on an entity edge (line -> PT_ON_LINE, circle -> PT_ON_CIRCLE)
// Append-only: cereal serializes this enum positionally as its underlying int, so
// inserting anywhere but the end reinterprets every constraint in every saved recipe.
EqualRadius,
Collinear,
DistanceX, // |dx| between two points, projected onto the sketch X axis
DistanceY, // |dy| between two points, projected onto the sketch Y axis
SymmetricAboutY, // mirror across the sketch's vertical axis (x = 0); axis is implicit
SymmetricAboutX // mirror across the sketch's horizontal axis (y = 0); axis is implicit
};
// Constraint on a SketchProfile, referencing profile point indices (a,b,c,d).
// `value` carries the target for Distance/LockX/LockY (ignored otherwise).
struct SketchConstraintDef {
SketchConstraintType type{SketchConstraintType::Coincident};
int a{-1}, b{-1}, c{-1}, d{-1};
double value{0.0};
template<class Archive> void serialize(Archive& ar) { ar(type, a, b, c, d, value); }
};
// Which point of an entity a constraint reference names.
// P0 = SketchEntity::p0 (Line start / Point position)
// P1 = SketchEntity::p1 (Line end)
// Center = SketchEntity::center (Arc/Circle center)
enum class SketchPointRole { P0, P1, Center };
// Constraint on coexisting SketchEntity objects (Fase 4.2). Each reference is an
// (entity index, point role) pair. Point-form constraints
// (Fix/Coincident/Horizontal/Vertical/Distance/LockX/LockY) use refs A and B as
// individual points. Segment-form constraints (Parallel/Perpendicular/EqualLength)
// use entity indices `ea`/`eb` as whole line segments (their P0->P1); roles are
// ignored for those. `value` carries the target for Distance/LockX/LockY.
struct SketchEntityConstraintDef {
SketchConstraintType type{SketchConstraintType::Coincident};
int ea{-1}, eb{-1}; // entity indices
SketchPointRole ra{SketchPointRole::P0}; // role within ea
SketchPointRole rb{SketchPointRole::P0}; // role within eb
double value{0.0};
int ec{-1}; // third entity ref (Symmetric axis)
SketchPointRole rc{SketchPointRole::P0}; // role within ec
template<class Archive> void serialize(Archive& ar) { ar(type, ea, eb, ra, rb, value, ec, rc); }
};
// Implicit references every sketch has, addressable from a constraint's ea/eb/ec without
// existing as SketchEntity objects. NEGATIVE so they cannot collide with an entity index;
// -1 is already "unset" and stays that way. Values are serialized inside existing int
// fields, so they are append-only in spirit: never renumber these.
constexpr int kSketchRefOrigin = -2; // the sketch origin point (0,0)
constexpr int kSketchRefAxisX = -3; // the sketch X axis, through the origin, +X
constexpr int kSketchRefAxisY = -4; // the sketch Y axis, through the origin, +Y
inline bool is_sketch_ref(int ei) { return ei <= kSketchRefOrigin; }
// How many real endpoints a type exposes, and which roles they are. p1 is UNUSED for
// Circle/Point/Ellipse (SketchEntity::p1 above) and reads (0,0) — walking {P0,P1} blindly
// over those invents a phantom endpoint at the origin, which for a pair of Points always
// wins a closest-pair search at distance 0 and binds a role the solver silently refuses.
int sketch_entity_ends(const SketchEntity& e, std::pair<SketchPointRole, Vec2d> out[2]);
bool sketch_closest_ends(const SketchEntity& A, const SketchEntity& B,
SketchPointRole& ra, SketchPointRole& rb, Vec2d& pa, Vec2d& pb);
// Why an entity-constraint pick is refused. The caller maps a reason to a localized string;
// the planner itself stays translation-free.
enum class ConstraintReject {
None, NeedOneEntity, NeedTwoEntities, NeedALine, NeedTwoLines,
NeedTwoRounds, NeedTangentPair, NeedJoinablePoints, NeedMeasurablePoints,
// The following are not in the GUI's current switch but are the faithful outcomes of
// its remaining branches; they need a reason too or the caller cannot tell them apart.
NeedPointAndLine, // Midpoint: one Point + one Line
NeedTwoPointsOrLines, // Symmetric / SymmetricAboutX/Y: two Points or two Lines
NeedAxisLine, // Symmetric: e2 must be a Line to act as the axis
NeedRound, // Radius/Diameter: a Circle or Arc
Unsupported // entity-constraint path has no binding for this type
};
struct ConstraintPlan {
enum class Kind { Reject, Apply, AskValue };
Kind kind{Kind::Reject};
ConstraintReject reason{ConstraintReject::None};
// Apply/AskValue only: the defs to commit. One element for every ordinary type, TWO for
// Symmetric/SymmetricAboutX/Y on two lines (P0/P0 and P1/P1), matching the GUI's builds.
std::vector<SketchEntityConstraintDef> defs{};
double prefill{0.0}; // AskValue only: the value to show pre-filled
};
// Pure: no wx, no translation, no UI. The caller maps `reason` to a localized string.
// e2 is the axis-line pick Symmetric needs (def.ec); every other type ignores it.
ConstraintPlan plan_entity_constraint(const std::vector<SketchEntity>& ents,
int e0, int e1, int e2, SketchConstraintType type);
// Solve a bare entity list in place against entity-form constraints. Shared by
// CadDocument::solve_sketch_feature (committed features) and the in-session GUI
// sketch tool (live solving as dimensions/constraints are added). Returns true on
// convergence; an empty constraint list is a no-op that returns true.
bool solve_sketch_entities(std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints);
struct SketchParams {
// Extrude/Revolve
double extrude_len{10}; bool extrude_sym{false}; double extrude_taper{0};
double revolve_deg{360};
bool is_pocket{false}; // cut into selected object instead of new
// Dress-up
bool dressup_enabled{false};
DressUpType dressup_type{DressUpType::Fillet};
FaceGroup dressup_faces{FaceGroup::All};
double dressup_radius{1.0};
double dressup_chamfer_dist{1.0};
// Mesh
double linear_deflection{0.01};
template<class Archive>
void serialize(Archive& ar) {
ar(extrude_len, extrude_sym, extrude_taper, revolve_deg, is_pocket,
dressup_enabled, dressup_type, dressup_faces, dressup_radius, dressup_chamfer_dist,
linear_deflection);
}
};
class SketchEngine
{
public:
static TopoDS_Shape make_extrude(const TopoDS_Wire& wire, const SketchPlane& plane,
double length, bool symmetric = false, double taper_deg = 0.0);
static TopoDS_Shape make_extrude(const TopoDS_Face& face, const SketchPlane& plane,
double length, bool symmetric = false, double taper_deg = 0.0);
// Asymmetric two-sided prism: extrude the wire's face by `up` along +normal and `down`
// along -normal, fused into one solid. up/down are non-negative magnitudes.
// Tapered (draft) extrude of a planar wire: the top profile is the base wire offset in its
// plane by length*tan(taper_deg), lofted from base to top. Falls back to a straight prism on
// any failure (self-intersecting offset / loft error). taper_deg>0 widens the top.
static TopoDS_Shape make_extrude_taper(const TopoDS_Wire& wire, const SketchPlane& plane,
double length, double taper_deg);
static TopoDS_Shape make_extrude_two_sided(const TopoDS_Wire& wire, const SketchPlane& plane,
double up, double down);
static TopoDS_Shape make_extrude_two_sided(const TopoDS_Face& face, const SketchPlane& plane,
double up, double down);
static TopoDS_Shape make_extrude_face(const TopoDS_Face& face, const SketchPlane& plane,
double length, bool symmetric = false, double taper_deg = 0.0);
// Extrude a set of imported rigid regions (Text/SVG). Each region is
// contour[0]=outer loop + contour[1..]=hole loops, in plane (u,v) mm. Builds
// one planar face-with-holes per region, extrudes it, and fuses all region
// solids into a single shape. Empty/degenerate contours are skipped.
static TopoDS_Shape make_extrude_regions(
const std::vector<std::vector<std::vector<Vec2d>>>& regions,
const SketchPlane& plane, double length, bool symmetric = false);
// Revolve a planar profile wire about an axis lying in the sketch plane and
// passing through the plane origin: axis_sel 0 = plane X axis, 1 = plane Y axis.
// A negative angle_deg sweeps the opposite direction (Flip). The profile must
// lie to one side of the axis (Onshape rule); a straddling profile self-intersects.
static TopoDS_Shape make_revolve(const TopoDS_Wire& wire, const SketchPlane& plane,
double angle_deg = 360.0, int axis_sel = 0);
// Sweep a planar profile wire along a path (spine) wire. The profile is turned
// into a face and swept with BRepOffsetAPI_MakePipe, which keeps the profile
// perpendicular to the spine along its length. The path may be open or closed;
// for a clean solid the path's first point should sit on/near the profile plane.
static TopoDS_Shape make_sweep(const TopoDS_Wire& profile, const TopoDS_Wire& path);
// Loft a solid through 2+ closed profile wires (each on its own plane), in the
// given order. ruled=true => straight (ruled) sections; false => smooth (C2).
static TopoDS_Shape make_loft(const std::vector<TopoDS_Wire>& profiles, bool ruled);
// Skin `profiles` WITHOUT end caps -> an open shell (sheet). Same as make_loft but the
// ThruSections solid flag is false. // ponytail: a sibling instead of a bool param, so no
// existing call site changes.
static TopoDS_Shape make_loft_surface(const std::vector<TopoDS_Wire>& profiles, bool ruled);
static TopoDS_Shape make_pocket(const TopoDS_Wire& wire, const SketchPlane& plane,
const TopoDS_Shape& target, double depth);
static TriangleMesh tessellate(const TopoDS_Shape& shape,
double linear_deflection = 0.01,
double angular_deflection = 0.5);
static TriangleMesh tessellate(const TopoDS_Shape& shape,
std::vector<int>& tri_face,
double linear_deflection = 0.01,
double angular_deflection = 0.5);
static TopoDS_Wire entities_to_wire(const std::vector<SketchEntity>& entities,
const SketchPlane& plane,
bool closed_only = false);
// Every loop the sketch holds, in the order each loop's FIRST entity appears in
// `entities`. A Circle or Ellipse is a loop on its own; Line/Arc/EllipseArc/BSpline
// entities are grouped into loops by shared endpoints. An OPEN chain is returned too —
// a sweep path is legitimately open, so open-ness is not an error here — unless
// `closed_only` is true, in which case an open chain is DISCARDED (skipped, not an
// error). Empty vector = nothing usable; the caller decides whether that is an error.
static std::vector<TopoDS_Wire> entities_to_wires(const std::vector<SketchEntity>& entities,
const SketchPlane& plane,
bool closed_only = false);
// A planar face from a set of coplanar loops: the largest-area loop is the outer boundary
// and every other loop is a hole in it. Throws std::runtime_error with a message naming the
// problem when the loops do not describe one such region.
static TopoDS_Face wires_to_face(const std::vector<TopoDS_Wire>& wires,
const SketchPlane& plane);
static std::vector<SketchEntity> mirror_entities(
const std::vector<SketchEntity>& src, const Vec2d& a, const Vec2d& b);
// Offset a sketch by `d`, PRESERVING CHAINS. Entities joined by shared endpoints are
// offset together and their seams repaired (miter join), so a closed profile comes back
// closed and can still be extruded; per-entity offsetting cannot do that. Sign convention:
// +d moves each curve to the LEFT of its direction of travel, which for a CCW closed loop
// is inward. Ellipses and splines are not offset (a parallel of either is not the same
// kind of curve) and are dropped from the result.
static std::vector<SketchEntity> offset_entities(
const std::vector<SketchEntity>& src, double d);
// Rigid-transform array. Returns the (count-1) copies for instance i=1..count-1
// (the originals in `src` are NOT included). Each copy i is `src` rigidly
// transformed by: rotate by i*angle_step about `pivot`, then translate by i*step.
// Rectangular/linear array: angle_step = 0, step = spacing*direction (pivot unused).
// Polar array: step = (0,0), angle_step = sweep/count, pivot = centre.
// Orientation-preserving, so arc/ellipse parametric angles shift by i*angle_step.
static std::vector<SketchEntity> array_entities(
const std::vector<SketchEntity>& src, int count,
const Vec2d& step, double angle_step, const Vec2d& pivot);
// General affine transform (move / rotate / scale), applied IN PLACE: returns
// the SAME entities (same count and order), each mapped by
// p -> pivot + scale * R(angle) * (p - pivot) + move
// (radii scale by |scale|; arc/ellipse parametric/rotation angles shift by
// `angle`). Unlike array_entities this mutates the subjects rather than adding
// copies. Move: angle=0, scale=1. Rotate-in-place: move=(0,0), scale=1,
// pivot=centroid. Scale: angle=0.
static std::vector<SketchEntity> transform_entities(
const std::vector<SketchEntity>& src,
const Vec2d& move, double angle, double scale, const Vec2d& pivot);
static bool fillet_lines(const SketchEntity& a, const SketchEntity& b, double r,
SketchEntity& a_out, SketchEntity& b_out, SketchEntity& arc_out);
// Symmetric chamfer between two lines meeting at a corner: trims each line back
// by setback distance `d` from the shared corner and returns the connecting
// straight segment (seg_out) in place of the corner. a_out/b_out are the trimmed
// lines; seg_out goes seg_out.p0 (on a) -> seg_out.p1 (on b). False if the lines
// are parallel or `d` overruns either line.
static bool chamfer_lines(const SketchEntity& a, const SketchEntity& b, double d,
SketchEntity& a_out, SketchEntity& b_out, SketchEntity& seg_out);
static bool trim_entity(SketchEntity& e, const std::vector<SketchEntity>& others,
const Vec2d& pick);
static bool extend_entity(SketchEntity& e, const std::vector<SketchEntity>& others,
const Vec2d& pick);
// Build a cubic-Bezier G1 bridge (as a BSpline entity, 4 poles) connecting endpoint
// `a_end` of `a` to endpoint `b_end` of `b` (0 = start/p0 side, 1 = end/p1 side).
// Tangent-continuous with both entities where the endpoint tangent is defined.
static SketchEntity make_bridge(const SketchEntity& a, int a_end,
const SketchEntity& b, int b_end);
};
// Free endpoints of a sketch: the sketch-space points where a chain fails to close.
// Same weld tolerance as the wire build, so it can never contradict it.
std::vector<Vec2d> sketch_open_ends(const std::vector<SketchEntity>&, const SketchPlane&);
} // namespace Slic3r
#endif // slic3r_SketchEngine_hpp_
+145
View File
@@ -0,0 +1,145 @@
#include "libslic3r/CAD/SketchImport.hpp"
#include "libslic3r/Emboss.hpp"
#include "libslic3r/NSVGUtils.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/TextConfiguration.hpp" // FontProp
#include "libslic3r/libslic3r.h" // SCALING_FACTOR
#include "libslic3r/Utils.hpp" // resources_dir
#include <algorithm>
#include <limits>
namespace Slic3r {
// Convert one ExPolygon (outer contour + CW holes) into an ImportRegion,
// mapping each integer Point to plane (u,v) mm via `to_mm`.
template<class ToMm>
static ImportRegion expoly_to_region(const ExPolygon& ex, ToMm to_mm)
{
auto contour_pts = [&](const Polygon& poly) {
std::vector<Vec2d> c;
c.reserve(poly.points.size());
for (const Point& p : poly.points)
c.push_back(to_mm(p));
return c;
};
ImportRegion region;
region.push_back(contour_pts(ex.contour));
for (const Polygon& h : ex.holes)
region.push_back(contour_pts(h));
return region;
}
// Shift all regions so their common bounding-box centre sits on the origin
// (Onshape/typical CAD insert places imported art centred on the sketch).
static void center_regions(ImportRegions& regs)
{
double lo_x = std::numeric_limits<double>::max();
double lo_y = std::numeric_limits<double>::max();
double hi_x = -std::numeric_limits<double>::max();
double hi_y = -std::numeric_limits<double>::max();
bool any = false;
for (const auto& region : regs)
for (const auto& contour : region)
for (const Vec2d& p : contour) {
lo_x = std::min(lo_x, p.x()); hi_x = std::max(hi_x, p.x());
lo_y = std::min(lo_y, p.y()); hi_y = std::max(hi_y, p.y());
any = true;
}
if (!any) return;
const Vec2d c(0.5 * (lo_x + hi_x), 0.5 * (lo_y + hi_y));
for (auto& region : regs)
for (auto& contour : region)
for (Vec2d& p : contour)
p -= c;
}
static std::string default_font_path()
{
return resources_dir() + "/fonts/HarmonyOS_Sans_SC_Regular.ttf";
}
ImportRegions text_to_regions(const std::string& utf8, double size_mm,
const std::string& font_path)
{
if (utf8.empty() || size_mm <= 0.0)
return {};
const std::string path = font_path.empty() ? default_font_path() : font_path;
std::unique_ptr<Emboss::FontFile> ff = Emboss::create_font_file(path.c_str());
if (!ff)
return {};
Emboss::FontFileWithCache fwc(std::move(ff));
if (!fwc.has_value())
return {};
FontProp prop(static_cast<float>(size_mm)); // per_glyph=false
HealedExPolygons healed = Emboss::text2shapes(fwc, utf8.c_str(), prop);
if (healed.expolygons.empty())
return {};
// Shape points are integers scaled by 1/SHAPE_SCALE in font units;
// get_text_shape_scale collapses (size_in_mm / unit_per_em) * SHAPE_SCALE
// into a single mm-per-shape-unit factor. FreeType y is up already.
const double s = Emboss::get_text_shape_scale(prop, *fwc.font_file);
auto to_mm = [s](const Point& p) { return Vec2d(p.x() * s, p.y() * s); };
ImportRegions regs;
regs.reserve(healed.expolygons.size());
for (const ExPolygon& ex : healed.expolygons)
regs.push_back(expoly_to_region(ex, to_mm));
center_regions(regs);
return regs;
}
ImportRegions svg_to_regions(const std::string& svg_path, double scale)
{
if (svg_path.empty() || scale <= 0.0)
return {};
NSVGimage_ptr image = nsvgParseFromFile(svg_path, "mm", 96.0f);
if (!image)
return {};
// A filled shape that also carries a stroke would import the stroke as a
// thick outline band wrapped around the fill (the reported "too large line
// width"). For CAD import the fill silhouette is what's wanted, so drop the
// stroke on any shape that has a fill; stroke-only line art is kept.
for (NSVGshape* s = image->shapes; s != nullptr; s = s->next)
if (s->fill.type != NSVG_PAINT_NONE)
s->stroke.type = NSVG_PAINT_NONE;
// tesselation tolerance is in image (mm) scale; 0.3 mm keeps curves smooth
// without exploding the contour count. is_y_negative (default) flips SVG's
// y-down to the sketch's y-up.
NSVGLineParams param(0.3);
ExPolygonsWithIds ids = create_shape_with_ids(*image, param);
// NSVG points are integers scaled by 1/SCALING_FACTOR (param.scale default):
// mm = point * SCALING_FACTOR, then the user scale factor.
const double s = SCALING_FACTOR * scale;
auto to_mm = [s](const Point& p) { return Vec2d(p.x() * s, p.y() * s); };
ImportRegions regs;
for (const ExPolygonsWithId& w : ids)
for (const ExPolygon& ex : w.expoly)
regs.push_back(expoly_to_region(ex, to_mm));
center_regions(regs);
return regs;
}
ImportRegions transform_regions(const ImportRegions& src, const Vec2d& offset,
double scale_x, double scale_y)
{
ImportRegions out = src;
for (auto& region : out)
for (auto& contour : region)
for (Vec2d& p : contour)
p = Vec2d(p.x() * scale_x + offset.x(), p.y() * scale_y + offset.y());
return out;
}
} // namespace Slic3r
+37
View File
@@ -0,0 +1,37 @@
#ifndef slic3r_SketchImport_hpp_
#define slic3r_SketchImport_hpp_
#include "libslic3r/Point.hpp" // Vec2d
#include <string>
#include <vector>
namespace Slic3r {
// A rigid imported region: contour[0] = outer loop, contour[1..] = holes;
// points in plane (u,v) millimetres. The nested vector type matches
// CadFeature::imported_regions exactly, so results assign directly.
using ImportRegion = std::vector<std::vector<Vec2d>>;
using ImportRegions = std::vector<ImportRegion>;
// Vectorize UTF-8 text into filled regions (mm), centred on the origin.
// `size_mm` is the cap/line height. `font_path` empty -> a bundled default
// font (resources/fonts). Returns an empty vector on any failure.
ImportRegions text_to_regions(const std::string& utf8, double size_mm,
const std::string& font_path = std::string());
// Parse an SVG file's filled paths into regions (mm), centred on the origin.
// `scale` multiplies the authored size (1.0 = as authored). Returns an empty
// vector on any failure.
ImportRegions svg_to_regions(const std::string& svg_path, double scale = 1.0);
// Apply an axis-aligned placement transform to regions:
// p -> ( p.x * scale_x + offset.x, p.y * scale_y + offset.y )
// Used to move / enlarge / stretch imported art non-destructively (the
// feature keeps the centred source regions + this transform).
ImportRegions transform_regions(const ImportRegions& src, const Vec2d& offset,
double scale_x, double scale_y);
} // namespace Slic3r
#endif // slic3r_SketchImport_hpp_
+234
View File
@@ -0,0 +1,234 @@
#include "libslic3r/CAD/SketchInference.hpp"
#include <algorithm>
#include <cmath>
namespace Slic3r {
// Candidate target collected during the scan; we keep the closest within each
// priority tier and resolve ties by tier then distance.
namespace {
struct Cand {
InferenceSnap::Kind kind{InferenceSnap::Kind::None};
int entity{-1};
SketchPointRole role{SketchPointRole::P0};
Vec2d point{0, 0};
double dist{0.0};
};
// Lower number = higher priority.
int tier(InferenceSnap::Kind k)
{
switch (k) {
case InferenceSnap::Kind::Endpoint: return 0;
case InferenceSnap::Kind::Center: return 1;
case InferenceSnap::Kind::Origin: return 2;
case InferenceSnap::Kind::Midpoint: return 3;
case InferenceSnap::Kind::OnEdge: return 4;
default: return 9;
}
}
} // namespace
InferenceSnap infer_point_snap(const std::vector<SketchEntity>& entities,
const Vec2d& query, double tol,
bool include_origin)
{
Cand best;
best.kind = InferenceSnap::Kind::None;
best.point = query;
auto offer = [&](InferenceSnap::Kind k, int ent, SketchPointRole r, const Vec2d& q) {
const double d = (q - query).norm();
if (d > tol) return;
const bool better = (best.kind == InferenceSnap::Kind::None) ||
(tier(k) < tier(best.kind)) ||
(tier(k) == tier(best.kind) && d < best.dist);
if (better) { best.kind = k; best.entity = ent; best.role = r; best.point = q; best.dist = d; }
};
for (size_t i = 0; i < entities.size(); ++i) {
const SketchEntity& e = entities[i];
const int ei = int(i);
switch (e.type) {
case SketchEntity::Type::Line: {
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P0, e.p0);
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P1, e.p1);
offer(InferenceSnap::Kind::Midpoint, ei, SketchPointRole::P0, 0.5 * (e.p0 + e.p1));
// Projection onto the segment interior (PointOnObject candidate).
const Vec2d d = e.p1 - e.p0;
const double L2 = d.squaredNorm();
if (L2 > 1e-12) {
double t = (query - e.p0).dot(d) / L2;
if (t > 0.02 && t < 0.98)
offer(InferenceSnap::Kind::OnEdge, ei, SketchPointRole::P0, e.p0 + t * d);
}
break;
}
case SketchEntity::Type::Arc: {
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P0, e.p0);
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P1, e.p1);
offer(InferenceSnap::Kind::Center, ei, SketchPointRole::Center, e.center);
// Mid-arc point, so an arc is as snappable in its middle as a line is.
const double am = 0.5 * (e.start_angle + e.end_angle);
offer(InferenceSnap::Kind::Midpoint, ei, SketchPointRole::P0,
Vec2d(e.center.x() + e.radius * std::cos(am),
e.center.y() + e.radius * std::sin(am)));
break;
}
case SketchEntity::Type::Circle: {
offer(InferenceSnap::Kind::Center, ei, SketchPointRole::Center, e.center);
// Nearest point on the circle rim (PointOnObject candidate).
const Vec2d v = query - e.center;
const double n = v.norm();
if (n > 1e-9 && e.radius > 1e-9)
offer(InferenceSnap::Kind::OnEdge, ei, SketchPointRole::Center,
e.center + v * (e.radius / n));
break;
}
case SketchEntity::Type::Point:
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P0, e.p0);
break;
case SketchEntity::Type::EllipseArc:
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P0, e.p0);
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P1, e.p1);
offer(InferenceSnap::Kind::Center, ei, SketchPointRole::Center, e.center);
break;
case SketchEntity::Type::Ellipse:
offer(InferenceSnap::Kind::Center, ei, SketchPointRole::Center, e.center);
break;
case SketchEntity::Type::BSpline:
// Endpoints (first/last pole) snap for loop closure.
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P0, e.p0);
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P1, e.p1);
break;
}
}
if (include_origin)
offer(InferenceSnap::Kind::Origin, -1, SketchPointRole::P0, Vec2d(0, 0));
InferenceSnap r;
r.kind = best.kind; r.entity = best.entity; r.role = best.role; r.point = best.point;
return r;
}
std::optional<SketchConstraintType>
infer_axis_constraint(const Vec2d& anchor, const Vec2d& tip, double ang_tol_rad)
{
const Vec2d d = tip - anchor;
if (d.squaredNorm() < 1e-12) return std::nullopt;
const double ang = std::atan2(std::abs(d.y()), std::abs(d.x())); // 0=horizontal, pi/2=vertical
if (ang <= ang_tol_rad) return SketchConstraintType::Horizontal;
if (ang >= M_PI / 2.0 - ang_tol_rad) return SketchConstraintType::Vertical;
return std::nullopt;
}
// Unsigned angle between two (unnormalized) direction vectors, in [0, pi]. 0 = same
// direction, pi = opposite, pi/2 = perpendicular. Inputs must be non-degenerate.
// static: this is a file-local helper, not part of the module's interface -- at namespace
// scope with external linkage it would be a link-time collision waiting to happen.
static double unsigned_angle(const Vec2d& a, const Vec2d& b)
{
const double cross = a.x() * b.y() - a.y() * b.x();
const double dot = a.x() * b.x() + a.y() * b.y();
return std::atan2(std::abs(cross), dot);
}
std::vector<SketchEntityConstraintDef>
infer_relations(const std::vector<SketchEntity>& entities, int new_ei,
double ang_tol_rad, double len_tol_frac)
{
std::vector<SketchEntityConstraintDef> out;
if (new_ei <= 0 || new_ei >= int(entities.size())) return out;
// AT MOST ONE constraint per rule per new entity, not one per PAIR. Without this the
// function is quadratic in the sketch: a drawing with 200 equal holes yields ~20000
// EqualRadius candidates, the batch is rejected as over-constrained, and the caller's
// one-at-a-time fallback then runs a solve per constraint. Measured 2026-08-31: that
// pinned the app at 95% of a core with the MCP socket unresponsive -- the same failure
// the axes batch above already carries a warning about. Keep the best candidate only.
int best_ang_j = -1, best_rad_j = -1, best_tan_j = -1;
double best_ang_err = 1e30, best_rad_err = 1e30, best_tan_err = 1e30;
SketchConstraintType best_ang_type = SketchConstraintType::Parallel;
const SketchEntity& n = entities[new_ei];
const bool n_line = n.type == SketchEntity::Type::Line;
const bool n_curve = n.type == SketchEntity::Type::Arc || n.type == SketchEntity::Type::Circle;
if (!n_line && !n_curve) return out; // not a Line / Arc / Circle
if (n_line && (n.p1 - n.p0).squaredNorm() < 1e-18) return out; // degenerate
if (n_curve && n.radius < 1e-9) return out;
for (int j = 0; j < new_ei; ++j) {
const SketchEntity& o = entities[j];
const bool o_line = o.type == SketchEntity::Type::Line;
const bool o_curve = o.type == SketchEntity::Type::Arc || o.type == SketchEntity::Type::Circle;
if (!o_line && !o_curve) continue;
if (o_line && (o.p1 - o.p0).squaredNorm() < 1e-18) continue;
if (o_curve && o.radius < 1e-9) continue;
if (n_line && o_line) {
// R1 — parallel / perpendicular, restricted to CONNECTED lines. Connection is
// what keeps this from firing on every distant line that is roughly parallel.
const bool connected = (n.p0 - o.p0).squaredNorm() <= 1e-14 ||
(n.p0 - o.p1).squaredNorm() <= 1e-14 ||
(n.p1 - o.p0).squaredNorm() <= 1e-14 ||
(n.p1 - o.p1).squaredNorm() <= 1e-14;
if (!connected) continue;
const double ang = unsigned_angle(n.p1 - n.p0, o.p1 - o.p0);
const double par_err = std::min(ang, M_PI - ang);
const double per_err = std::abs(ang - M_PI / 2.0);
if (par_err <= ang_tol_rad && par_err < best_ang_err) {
best_ang_err = par_err; best_ang_j = j;
best_ang_type = SketchConstraintType::Parallel;
} else if (per_err <= ang_tol_rad && per_err < best_ang_err) {
best_ang_err = per_err; best_ang_j = j;
best_ang_type = SketchConstraintType::Perpendicular;
}
} else if (n_curve && o_curve) {
// R2 — equal radius between circles / arcs, relative to the larger.
const double larger = n.radius > o.radius ? n.radius : o.radius;
const double err = std::abs(n.radius - o.radius) / larger;
if (err <= len_tol_frac && err < best_rad_err) { best_rad_err = err; best_rad_j = j; }
} else {
// R3 — tangent where a line meets a circle / arc at a shared endpoint, and only
// when the line is ALREADY perpendicular to the radius at that point.
const SketchEntity& ln = n_line ? n : o;
const SketchEntity& cv = n_line ? o : n;
const Vec2d ldir = ln.p1 - ln.p0;
bool tangent = false;
const Vec2d le[2] = { ln.p0, ln.p1 };
for (int k = 0; k < 2 && !tangent; ++k) {
if (cv.type == SketchEntity::Type::Arc) {
const Vec2d ce[2] = { cv.p0, cv.p1 };
for (int m = 0; m < 2; ++m) {
if ((le[k] - ce[m]).squaredNorm() > 1e-14) continue;
const Vec2d r = ce[m] - cv.center;
if (r.squaredNorm() < 1e-18) continue;
tangent = std::abs(unsigned_angle(ldir, r) - M_PI / 2.0) <= ang_tol_rad;
if (tangent) break;
}
} else { // Circle: shared point is a line endpoint on the rim.
const Vec2d r = le[k] - cv.center;
if (std::abs(r.norm() - cv.radius) > 1e-7) continue;
if (r.squaredNorm() < 1e-18) continue;
tangent = std::abs(unsigned_angle(ldir, r) - M_PI / 2.0) <= ang_tol_rad;
}
}
if (tangent && best_tan_err > 0.0) { best_tan_err = 0.0; best_tan_j = j; }
}
}
auto emit = [&](SketchConstraintType t, int j) {
if (j < 0) return;
SketchEntityConstraintDef c;
c.type = t; c.ea = j; c.eb = new_ei;
out.push_back(c);
};
emit(best_ang_type, best_ang_j); // R1
emit(SketchConstraintType::EqualRadius, best_rad_j); // R2
emit(SketchConstraintType::Tangent, best_tan_j); // R3
return out;
}
} // namespace Slic3r
+55
View File
@@ -0,0 +1,55 @@
#ifndef slic3r_SketchInference_hpp_
#define slic3r_SketchInference_hpp_
#include "libslic3r/CAD/SketchEngine.hpp"
#include <vector>
#include <optional>
#include <cmath>
namespace Slic3r {
// Result of snapping a free cursor point onto the most relevant inference target
// among the committed sketch entities and the sketch origin. This is the backbone
// that lets geometry self-constrain as it is drawn: the GUI records the returned
// target at click time and, once the entity it belongs to exists, emits the
// matching constraint (Coincident onto an endpoint/centre, Fix onto the origin,
// PointOnObject onto an edge) so the relation survives a re-solve.
struct InferenceSnap {
enum class Kind { None, Endpoint, Center, Midpoint, OnEdge, Origin };
Kind kind{Kind::None};
int entity{-1}; // hit entity index (-1 = origin/none)
SketchPointRole role{SketchPointRole::P0}; // which point of `entity` (Endpoint/Center)
Vec2d point{0, 0}; // snapped coordinate (== query when None)
bool snapped() const { return kind != Kind::None; }
};
// Snap `query` onto the best inference target within `tol` plane units. Priority,
// highest first: Endpoint, Center, Origin, Midpoint, OnEdge. Construction entities
// participate (you constrain to them too). Returns {None, query} when nothing is in
// range. Pure — no GUI / GL dependencies, so it is unit-testable in libslic3r.
InferenceSnap infer_point_snap(const std::vector<SketchEntity>& entities,
const Vec2d& query, double tol,
bool include_origin = true);
// Relational inference for an in-progress segment anchor->tip. If its direction is
// within `ang_tol_rad` of an axis, returns Horizontal or Vertical (the constraint to
// auto-emit on the committed segment); std::nullopt otherwise. Degenerate (near-zero
// length) segments return nullopt.
std::optional<SketchConstraintType>
infer_axis_constraint(const Vec2d& anchor, const Vec2d& tip, double ang_tol_rad = 3.0 * M_PI / 180.0);
// Relational constraints to auto-emit for a newly drawn entity `new_ei` against the
// entities already in the sketch. Pure, no GUI/GL dependencies, unit-testable.
//
// Deliberately conservative: every rule requires the relation to be ALREADY TRUE within
// tolerance, so an inferred constraint never moves geometry the user drew — it only pins a
// relation that is visibly there. Returns an empty vector when nothing qualifies.
std::vector<SketchEntityConstraintDef>
infer_relations(const std::vector<SketchEntity>& entities, int new_ei,
double ang_tol_rad = 2.0 * M_PI / 180.0,
double len_tol_frac = 0.01);
} // namespace Slic3r
#endif // slic3r_SketchInference_hpp_
+555
View File
@@ -0,0 +1,555 @@
#include "libslic3r/CAD/SketchSolver.hpp"
#include <slvs.h>
#include <cmath>
#include <cstring>
#include <functional>
#include <map>
#include <unordered_map>
namespace Slic3r {
using CT = SketchConstraintType;
using Role = SketchPointRole;
namespace {
constexpr Slvs_hGroup G_FIXED = 1; // workplane / reference: held constant
constexpr Slvs_hGroup G_SK = 2; // sketch geometry: the group we solve
// Per-entity slvs handles. p0/p1/center are point2d entity handles; prim is the
// line/arc/circle entity; rparam is the circle radius param.
struct Slots {
Slvs_hEntity prim{0}, p0{0}, p1{0}, center{0};
Slvs_hParam rparam{0};
std::vector<Slvs_hEntity> pts; // BSpline control points (point2d handles)
};
struct Build {
std::vector<Slvs_Param> params;
std::vector<Slvs_Entity> ents;
std::vector<Slvs_Constraint> cons;
Slvs_hParam ph{0};
Slvs_hEntity eh{0};
Slvs_hConstraint ch{0};
Slvs_hEntity wp{0}, normal{0};
Slvs_hParam P(Slvs_hGroup g, double v) { params.push_back(Slvs_MakeParam(++ph, g, v)); return ph; }
Slvs_hEntity E(Slvs_Entity e) { ents.push_back(e); return e.h; }
Slvs_hEntity pt2d(Slvs_hGroup g, double u, double v)
{ return E(Slvs_MakePoint2d(++eh, g, wp, P(g, u), P(g, v))); }
// Generic constraint (entityC unused by Slvs_MakeConstraint — set it manually below).
void C(int type, double val, Slvs_hEntity ptA, Slvs_hEntity ptB,
Slvs_hEntity eA, Slvs_hEntity eB, Slvs_hEntity eC = 0, int other = 0)
{
Slvs_Constraint c = Slvs_MakeConstraint(++ch, G_SK, type, wp, val, ptA, ptB, eA, eB);
c.entityC = eC;
c.other = other;
cons.push_back(c);
}
};
inline int role_idx(Role r) { return int(r); }
} // namespace
static SketchSolveResult solve_system(std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints,
int dragged_ei, Role dragged_role)
{
SketchSolveResult out;
if (constraints.empty()) { out.ok = true; out.dof = -1; return out; }
Build b;
// ---- Fixed 2D XY workplane (origin at 0,0,0; identity normal) -------------------
Slvs_hEntity origin = b.E(Slvs_MakePoint3d(++b.eh, G_FIXED,
b.P(G_FIXED, 0.0), b.P(G_FIXED, 0.0), b.P(G_FIXED, 0.0)));
double qw, qx, qy, qz;
Slvs_MakeQuaternion(1, 0, 0, 0, 1, 0, &qw, &qx, &qy, &qz);
b.normal = b.E(Slvs_MakeNormal3d(++b.eh, G_FIXED,
b.P(G_FIXED, qw), b.P(G_FIXED, qx), b.P(G_FIXED, qy), b.P(G_FIXED, qz)));
b.wp = b.E(Slvs_MakeWorkplane(++b.eh, G_FIXED, origin, b.normal));
// Unit direction references for the axis-projected distance constraints. Both live in
// G_FIXED, so they are held constant and add no DOF to the system.
// libslvs defines a LINE_SEGMENT's direction as point[0] - point[1] (entity.cpp
// VectorGetExprs), so the unit vector's head is listed first to yield +X / +Y.
const Slvs_hEntity dir_x [[maybe_unused]] = b.E(Slvs_MakeLineSegment(++b.eh, G_FIXED, b.wp,
b.pt2d(G_FIXED, 1.0, 0.0), b.pt2d(G_FIXED, 0.0, 0.0)));
const Slvs_hEntity dir_y [[maybe_unused]] = b.E(Slvs_MakeLineSegment(++b.eh, G_FIXED, b.wp,
b.pt2d(G_FIXED, 0.0, 1.0), b.pt2d(G_FIXED, 0.0, 0.0)));
// Implicit sketch references (origin, X axis, Y axis), addressable by the negative
// sentinels in SketchEngine.hpp. G_FIXED: held constant, zero added DOF. The axis lines
// are built head-first so their direction reads +X / +Y, matching dir_x / dir_y.
const Slvs_hEntity ref_origin_pt = b.pt2d(G_FIXED, 0.0, 0.0);
const Slvs_hEntity ref_axis_x = b.E(Slvs_MakeLineSegment(++b.eh, G_FIXED, b.wp,
b.pt2d(G_FIXED, 1.0, 0.0), ref_origin_pt));
const Slvs_hEntity ref_axis_y = b.E(Slvs_MakeLineSegment(++b.eh, G_FIXED, b.wp,
b.pt2d(G_FIXED, 0.0, 1.0), ref_origin_pt));
// ---- Entities -------------------------------------------------------------------
std::vector<Slots> slot(entities.size());
for (size_t i = 0; i < entities.size(); ++i) {
const SketchEntity& e = entities[i];
Slots s;
switch (e.type) {
case SketchEntity::Type::Line:
s.p0 = b.pt2d(G_SK, e.p0.x(), e.p0.y());
s.p1 = b.pt2d(G_SK, e.p1.x(), e.p1.y());
s.prim = b.E(Slvs_MakeLineSegment(++b.eh, G_SK, b.wp, s.p0, s.p1));
break;
case SketchEntity::Type::Point:
s.p0 = b.pt2d(G_SK, e.p0.x(), e.p0.y());
break;
case SketchEntity::Type::Circle: {
s.center = b.pt2d(G_SK, e.center.x(), e.center.y());
s.p0 = s.center; // p0 mirrors centre for circles
s.rparam = b.P(G_SK, e.radius > 1e-9 ? e.radius : 1.0);
Slvs_hEntity dist = b.E(Slvs_MakeDistance(++b.eh, G_SK, b.wp, s.rparam));
s.prim = b.E(Slvs_MakeCircle(++b.eh, G_SK, b.wp, s.center, b.normal, dist));
break;
}
case SketchEntity::Type::Arc:
s.center = b.pt2d(G_SK, e.center.x(), e.center.y());
s.p0 = b.pt2d(G_SK, e.p0.x(), e.p0.y()); // start
s.p1 = b.pt2d(G_SK, e.p1.x(), e.p1.y()); // end
s.prim = b.E(Slvs_MakeArcOfCircle(++b.eh, G_SK, b.wp, b.normal, s.center, s.p0, s.p1));
break;
// libslvs has no conic entity (scope note): register the ellipse's defining
// points only (center + arc endpoints) so center/endpoint constraints solve;
// the a/b/phi shape params pass through unsolved.
case SketchEntity::Type::Ellipse:
s.center = b.pt2d(G_SK, e.center.x(), e.center.y());
s.p0 = s.center; // p0 mirrors centre (circle convention)
break;
case SketchEntity::Type::EllipseArc:
s.center = b.pt2d(G_SK, e.center.x(), e.center.y());
s.p0 = b.pt2d(G_SK, e.p0.x(), e.p0.y()); // start
s.p1 = b.pt2d(G_SK, e.p1.x(), e.p1.y()); // end
break;
// No native slvs curve for an arbitrary-degree spline: register the control
// poles as point2d so endpoints (and any pole-targeted constraint) solve. The
// OCCT curve is rebuilt from the solved poles. p0/p1 mirror first/last pole so
// Coincident at the spline ends closes loops just like a Line.
case SketchEntity::Type::BSpline:
s.pts.reserve(e.ctrl.size());
for (const Vec2d& cp : e.ctrl)
s.pts.push_back(b.pt2d(G_SK, cp.x(), cp.y()));
if (!s.pts.empty()) { s.p0 = s.pts.front(); s.p1 = s.pts.back(); }
break;
}
slot[i] = s;
}
auto valid = [&](int ei) { return ei >= 0 && ei < int(entities.size()); };
auto ptOf = [&](int ei, Role r) -> Slvs_hEntity {
if (ei == kSketchRefOrigin) return ref_origin_pt;
if (ei == kSketchRefAxisX || ei == kSketchRefAxisY) return ref_origin_pt; // axes pass through it
if (!valid(ei)) return 0;
const Slots& s = slot[ei];
switch (r) {
case Role::P0: return s.p0;
case Role::P1: return s.p1;
case Role::Center: return s.center ? s.center : s.p0;
}
return 0;
};
auto primOf = [&](int ei) -> Slvs_hEntity {
if (ei == kSketchRefAxisX) return ref_axis_x;
if (ei == kSketchRefAxisY) return ref_axis_y;
return valid(ei) ? slot[ei].prim : 0; // origin has no prim: it is a point
};
auto coordOf = [&](int ei, Role r) -> Vec2d {
if (is_sketch_ref(ei)) return Vec2d(0, 0); // all three pass through the origin
if (!valid(ei)) return Vec2d(0, 0);
const SketchEntity& e = entities[ei];
switch (r) { case Role::P0: return e.p0; case Role::P1: return e.p1; case Role::Center: return e.center; }
return e.p0;
};
// A fixed reference point at (x,y) — used to pin coordinates (Fix / LockX / LockY).
auto fixedRef = [&](double x, double y) -> Slvs_hEntity { return b.pt2d(G_FIXED, x, y); };
// ---- Constraints ----------------------------------------------------------------
for (const auto& c : constraints) {
// Robustness: never feed libslvs a null handle. A constraint that references an
// entity which produced no solver primitive (Point/Ellipse/EllipseArc/BSpline get
// no `prim`) or no point for the requested role would make Slvs FindById abort the
// whole process. Skip such a constraint instead of crashing.
bool ref_ok = true;
switch (c.type) {
case CT::Coincident: case CT::Horizontal: case CT::Vertical: case CT::Distance:
ref_ok = ptOf(c.ea, c.ra) && ptOf(c.eb, c.rb); break;
case CT::DistanceX:
case CT::DistanceY:
ref_ok = ptOf(c.ea, c.ra) && ptOf(c.eb, c.rb); break;
case CT::Concentric:
ref_ok = ptOf(c.ea, Role::Center) && ptOf(c.eb, Role::Center); break;
case CT::Fix: case CT::LockX: case CT::LockY:
ref_ok = ptOf(c.ea, c.ra) != 0; break;
case CT::EqualLength: case CT::Parallel: case CT::Perpendicular:
case CT::Angle: case CT::Tangent:
ref_ok = primOf(c.ea) && primOf(c.eb); break;
case CT::Radius: case CT::Diameter:
ref_ok = primOf(c.ea) != 0; break;
case CT::Midpoint:
ref_ok = ptOf(c.ea, c.ra) && primOf(c.eb); break;
case CT::Symmetric:
ref_ok = ptOf(c.ea, c.ra) && ptOf(c.eb, c.rb) && primOf(c.ec); break;
case CT::SymmetricAboutY: case CT::SymmetricAboutX:
ref_ok = ptOf(c.ea, c.ra) && ptOf(c.eb, c.rb); break;
case CT::PointOnLine: case CT::PointOnObject:
ref_ok = ptOf(c.ea, c.ra) && primOf(c.eb); break;
case CT::EqualRadius:
case CT::Collinear:
ref_ok = primOf(c.ea) && primOf(c.eb); break;
}
if (!ref_ok) continue;
switch (c.type) {
case CT::Coincident:
b.C(SLVS_C_POINTS_COINCIDENT, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), 0, 0);
break;
case CT::Concentric:
b.C(SLVS_C_POINTS_COINCIDENT, 0, ptOf(c.ea, Role::Center), ptOf(c.eb, Role::Center), 0, 0);
break;
case CT::Horizontal:
b.C(SLVS_C_HORIZONTAL, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), 0, 0);
break;
case CT::Vertical:
b.C(SLVS_C_VERTICAL, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), 0, 0);
break;
case CT::Distance:
b.C(SLVS_C_PT_PT_DISTANCE, c.value, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), 0, 0);
break;
case CT::DistanceX:
// Distance between the two points measured along X only: project the vector
// between them onto the fixed unit X direction.
b.C(SLVS_C_PROJ_PT_DISTANCE, c.value, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), dir_x, 0);
break;
case CT::DistanceY:
b.C(SLVS_C_PROJ_PT_DISTANCE, c.value, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), dir_y, 0);
break;
case CT::Fix: {
const Vec2d p = coordOf(c.ea, c.ra);
b.C(SLVS_C_POINTS_COINCIDENT, 0, ptOf(c.ea, c.ra), fixedRef(p.x(), p.y()), 0, 0);
break;
}
case CT::LockX: {
const Vec2d p = coordOf(c.ea, c.ra);
b.C(SLVS_C_VERTICAL, 0, ptOf(c.ea, c.ra), fixedRef(c.value, p.y()), 0, 0);
break;
}
case CT::LockY: {
const Vec2d p = coordOf(c.ea, c.ra);
b.C(SLVS_C_HORIZONTAL, 0, ptOf(c.ea, c.ra), fixedRef(p.x(), c.value), 0, 0);
break;
}
case CT::EqualLength:
b.C(SLVS_C_EQUAL_LENGTH_LINES, 0, 0, 0, primOf(c.ea), primOf(c.eb));
break;
case CT::Parallel:
b.C(SLVS_C_PARALLEL, 0, 0, 0, primOf(c.ea), primOf(c.eb));
break;
case CT::Perpendicular:
b.C(SLVS_C_PERPENDICULAR, 0, 0, 0, primOf(c.ea), primOf(c.eb));
break;
case CT::Midpoint:
b.C(SLVS_C_AT_MIDPOINT, 0, ptOf(c.ea, c.ra), 0, primOf(c.eb), 0);
break;
case CT::Symmetric:
// ptA, ptB symmetric about the axis line (ec).
b.C(SLVS_C_SYMMETRIC_LINE, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), primOf(c.ec), 0);
break;
case CT::SymmetricAboutY:
b.C(SLVS_C_SYMMETRIC_LINE, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), primOf(kSketchRefAxisY), 0);
break;
case CT::SymmetricAboutX:
b.C(SLVS_C_SYMMETRIC_LINE, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), primOf(kSketchRefAxisX), 0);
break;
case CT::Angle:
// model stores radians; slvs angle is in degrees.
b.C(SLVS_C_ANGLE, c.value * 180.0 / M_PI, 0, 0, primOf(c.ea), primOf(c.eb));
break;
case CT::Radius:
b.C(SLVS_C_DIAMETER, 2.0 * c.value, 0, 0, primOf(c.ea), 0);
break;
case CT::Diameter:
b.C(SLVS_C_DIAMETER, c.value, 0, 0, primOf(c.ea), 0);
break;
case CT::Tangent: {
const bool aCurve = valid(c.ea) && entities[c.ea].type != SketchEntity::Type::Line;
const bool bCurve = valid(c.eb) && entities[c.eb].type != SketchEntity::Type::Line;
if (aCurve && bCurve)
b.C(SLVS_C_CURVE_CURVE_TANGENT, 0, 0, 0, primOf(c.ea), primOf(c.eb));
else {
const int ci = aCurve ? c.ea : c.eb; // the curve
const int li = aCurve ? c.eb : c.ea; // the line
if (valid(ci) && entities[ci].type == SketchEntity::Type::Circle) {
// A FULL circle cannot use SLVS_C_ARC_LINE_TANGENT. That constraint reads
// arc->point[1] / point[2] — the arc's endpoints (see constrainteq.cpp,
// Type::ARC_LINE_TANGENT) — and a circle entity only has point[0], its
// centre. The zero handles send FindById into "Cannot find handle", which
// ABORTS the process rather than failing the solve, taking every later test
// with it. It is also the wrong equation for a circle: it only makes the
// line perpendicular to the radius AT AN ENDPOINT that does not exist.
//
// For a circle, tangency is exactly "the centre sits one radius away from
// the line", which slvs expresses directly.
//
// ponytail: the radius is captured here rather than tied as a variable —
// the C API takes a constant distance and offers no way to reference the
// circle's radius parameter. Exact whenever the radius is fixed or simply
// not being changed by another constraint in the same solve; if some other
// constraint drives the radius, re-solving restores tangency. Tying them
// would need an auxiliary point constrained onto both circle and line.
b.C(SLVS_C_PT_LINE_DISTANCE, entities[ci].radius,
ptOf(ci, Role::Center), 0, primOf(li), 0);
} else {
b.C(SLVS_C_ARC_LINE_TANGENT, 0, 0, 0, primOf(ci), primOf(li));
}
}
break;
}
case CT::PointOnLine:
if (std::abs(c.value) < 1e-9)
b.C(SLVS_C_PT_ON_LINE, 0, ptOf(c.ea, c.ra), 0, primOf(c.eb), 0);
else
b.C(SLVS_C_PT_LINE_DISTANCE, std::abs(c.value), ptOf(c.ea, c.ra), 0, primOf(c.eb), 0);
break;
case CT::PointOnObject:
// Point (ea,ra) lies on entity edge eb: a circle rim -> PT_ON_CIRCLE,
// otherwise the segment line -> PT_ON_LINE.
if (valid(c.eb) && entities[c.eb].type == SketchEntity::Type::Circle)
b.C(SLVS_C_PT_ON_CIRCLE, 0, ptOf(c.ea, c.ra), 0, primOf(c.eb), 0);
else
b.C(SLVS_C_PT_ON_LINE, 0, ptOf(c.ea, c.ra), 0, primOf(c.eb), 0);
break;
case CT::EqualRadius:
b.C(SLVS_C_EQUAL_RADIUS, 0, 0, 0, primOf(c.ea), primOf(c.eb));
break;
case CT::Collinear:
// libslvs has no collinear code. Two lines are collinear iff they are parallel
// AND a point of one lies on the other's infinite line — emit both.
b.C(SLVS_C_PARALLEL, 0, 0, 0, primOf(c.ea), primOf(c.eb));
// Point-on-infinite-line via PT_LINE_DISTANCE=0 rather than PT_ON_LINE: the
// latter creates an internal `valP` param that this port's Slvs_Solve leaves at
// 0 in the working set (ModifyToSatisfy only updates SK.param), so an already
// collinear pair drifts. PT_LINE_DISTANCE=0 is the same condition with no extra
// parameter, so an already-satisfied solve is a clean no-op.
b.C(SLVS_C_PT_LINE_DISTANCE, 0, ptOf(c.eb, Role::P0), 0, primOf(c.ea), 0);
break;
}
}
// ---- Solve ----------------------------------------------------------------------
Slvs_System sys;
std::memset(&sys, 0, sizeof(sys));
sys.param = b.params.data(); sys.params = int(b.params.size());
sys.entity = b.ents.data(); sys.entities = int(b.ents.size());
sys.constraint = b.cons.data(); sys.constraints = int(b.cons.size());
std::vector<Slvs_hConstraint> failed(b.cons.size() + 1, 0);
sys.failed = failed.data();
sys.faileds = int(failed.size());
sys.calculateFaileds = 1;
// Drag pin: feed the dragged point's two params into sys.dragged[] so the solver
// favours keeping that point at the cursor and re-solves the rest around it.
if (dragged_ei >= 0) {
const Slvs_hEntity h = ptOf(dragged_ei, dragged_role);
for (const Slvs_Entity& en : b.ents)
if (en.h == h) { sys.dragged[0] = en.param[0]; sys.dragged[1] = en.param[1]; break; }
}
Slvs_Solve(&sys, G_SK);
out.result = sys.result;
out.dof = sys.dof;
out.ok = (sys.result == SLVS_RESULT_OKAY);
// Map solved param handles -> values, then read points back.
std::unordered_map<Slvs_hParam, double> pv;
pv.reserve(sys.params * 2);
for (int i = 0; i < sys.params; ++i) pv[sys.param[i].h] = sys.param[i].val;
std::unordered_map<Slvs_hEntity, const Slvs_Entity*> byH;
byH.reserve(sys.entities * 2);
for (int i = 0; i < sys.entities; ++i) byH[sys.entity[i].h] = &sys.entity[i];
auto coord = [&](Slvs_hEntity h) -> Vec2d {
auto it = byH.find(h);
if (it == byH.end()) return Vec2d(0, 0);
return Vec2d(pv[it->second->param[0]], pv[it->second->param[1]]);
};
// Map failed constraint handles back to indices into `constraints`.
if (!out.ok && sys.faileds > 0) {
std::unordered_map<Slvs_hConstraint, int> chToIdx;
// constraint handles were assigned in order starting after the fixed group; the
// i-th sketch constraint in b.cons has handle = its position. Rebuild by scanning.
for (size_t k = 0; k < b.cons.size(); ++k) chToIdx[b.cons[k].h] = int(k);
for (int i = 0; i < sys.faileds; ++i) {
auto it = chToIdx.find(failed[i]);
if (it != chToIdx.end() && it->second < int(constraints.size()))
out.bad.push_back(it->second);
}
}
// ---- Read solved geometry back --------------------------------------------------
// ONLY on success. A failed solve leaves libslvs' params holding its last Newton
// iterate — geometry that satisfies nothing and is usually wildly deformed. Writing
// that back made every rejected attempt destructive: the caller rolls the constraints
// back, but the sketch it rolls back to is already wreckage, so the next attempt starts
// from the corpse. The fillet degrade ladder hit this on every corner — rung 1 (a
// tangent on each leg) is legitimately over-constrained against the legs' own H/V, and
// its wreckage then failed rungs 2 and 3, which solve cleanly on their own. The arc
// ended up with no constraints at all and the solver snapped the corner shut. pl5.
if (!out.ok) return out;
for (size_t i = 0; i < entities.size(); ++i) {
SketchEntity& e = entities[i];
const Slots& s = slot[i];
if (s.p0) e.p0 = coord(s.p0);
if (s.p1) e.p1 = coord(s.p1);
if (s.center) e.center = coord(s.center);
if (e.type == SketchEntity::Type::BSpline) {
for (size_t k = 0; k < s.pts.size() && k < e.ctrl.size(); ++k)
e.ctrl[k] = coord(s.pts[k]);
if (!e.ctrl.empty()) { e.p0 = e.ctrl.front(); e.p1 = e.ctrl.back(); }
} else if (e.type == SketchEntity::Type::Circle) {
if (s.rparam) { auto it = pv.find(s.rparam); if (it != pv.end()) e.radius = it->second; }
e.p0 = e.center;
} else if (e.type == SketchEntity::Type::Arc && s.center) {
// Reflow arc angles from solved centre + endpoints, preserving sweep sign.
const double old_sweep = e.end_angle - e.start_angle;
const double ns = std::atan2(e.p0.y() - e.center.y(), e.p0.x() - e.center.x());
const double ne = std::atan2(e.p1.y() - e.center.y(), e.p1.x() - e.center.x());
double sweep = ne - ns;
const double TWO_PI = 2.0 * M_PI;
while (sweep <= -TWO_PI) sweep += TWO_PI;
while (sweep >= TWO_PI) sweep -= TWO_PI;
if (old_sweep >= 0.0 && sweep < 0.0) sweep += TWO_PI;
if (old_sweep < 0.0 && sweep > 0.0) sweep -= TWO_PI;
e.start_angle = ns;
e.end_angle = ns + sweep;
e.radius = 0.5 * ((e.p0 - e.center).norm() + (e.p1 - e.center).norm());
}
}
return out;
}
// libslvs carries a COMPILE-TIME ceiling: solvespace.h declares `enum { MAX_UNKNOWNS = 1024 }`
// and sizes the System's param and equation arrays with it. solve_system() hands the solver every
// entity in the sketch, constrained or not, at 2 params per point — so a sketch of about 480 lines
// is the last one that fits, and the very next one comes back TOO_MANY_UNKNOWNS.
//
// What that did, before this: DesignSketchTool::try_add_constraints rolls the whole batch back
// when the solve fails, so the auto-constraint pass over a large sketch dropped EVERY constraint
// it had just inferred. Measured on the rig — 480 lines: 960 constraints, dof 480. 520 lines:
// 0 constraints, dof unknown. Nothing was said, and from there on no dimension and no constraint
// could ever be applied to that sketch, because each attempt re-solved the same oversized system
// and was rejected in turn. A typed length simply did nothing.
//
// Constraints only couple entities that SHARE a point, so a sketch is naturally a set of
// independent systems — a plate with 300 cut-outs is 301 little problems, not one big one.
// Solving them separately keeps every one of them far under the ceiling AND is faster, since the
// solver's work is superlinear in system size.
//
// The whole system is still tried FIRST, and this runs only on TOO_MANY_UNKNOWNS, so every sketch
// that fits today keeps its exact current behaviour, including its reported degrees of freedom.
// A genuinely over-constrained sketch still fails: the conflict lives inside one component and
// that component still rejects it.
static SketchSolveResult solve_partitioned(std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints,
int dragged_ei, Role dragged_role)
{
const int n = int(entities.size());
std::vector<int> parent(n);
for (int i = 0; i < n; ++i) parent[i] = i;
std::function<int(int)> find = [&](int a) {
while (parent[a] != a) { parent[a] = parent[parent[a]]; a = parent[a]; }
return a;
};
auto unite = [&](int a, int b) {
if (a < 0 || b < 0 || a >= n || b >= n) return;
a = find(a); b = find(b);
if (a != b) parent[a] = b;
};
for (const auto& c : constraints) { unite(c.ea, c.eb); unite(c.ea, c.ec); }
// Group the constraints by the component they belong to.
std::map<int, std::vector<int>> groups;
for (size_t i = 0; i < constraints.size(); ++i) {
const int a = constraints[i].ea;
if (a < 0 || a >= n) continue;
groups[find(a)].push_back(int(i));
}
SketchSolveResult out;
out.ok = true;
out.dof = 0;
// Solve into COPIES and commit only if every component succeeded. The contract callers rely
// on is all-or-nothing — try_add_constraints rolls the batch back and expects the geometry it
// rolls back to be untouched — and partial writes would break it.
std::vector<std::pair<std::vector<int>, std::vector<SketchEntity>>> solved;
for (const auto& [root, cidx] : groups) {
std::vector<int> ents; // global indices, in order
std::map<int, int> local; // global -> local
auto take = [&](int e) {
if (e < 0 || e >= n || local.count(e)) return;
local[e] = int(ents.size());
ents.push_back(e);
};
for (int ci : cidx) { take(constraints[ci].ea); take(constraints[ci].eb); take(constraints[ci].ec); }
std::vector<SketchEntity> sub;
sub.reserve(ents.size());
for (int e : ents) sub.push_back(entities[e]);
std::vector<SketchEntityConstraintDef> subc;
subc.reserve(cidx.size());
for (int ci : cidx) {
SketchEntityConstraintDef d = constraints[ci];
auto map1 = [&](int& e) { e = (e >= 0 && local.count(e)) ? local[e] : -1; };
map1(d.ea); map1(d.eb); map1(d.ec);
subc.push_back(d);
}
const int sub_drag = (dragged_ei >= 0 && local.count(dragged_ei)) ? local[dragged_ei] : -1;
SketchSolveResult r = solve_system(sub, subc, sub_drag, dragged_role);
if (!r.ok) {
out.ok = false;
out.result = r.result;
for (int bi : r.bad)
if (bi >= 0 && bi < int(cidx.size())) out.bad.push_back(cidx[bi]);
}
if (r.dof > 0) out.dof += r.dof;
solved.emplace_back(std::move(ents), std::move(sub));
}
if (!out.ok) return out;
for (auto& [ents, sub] : solved)
for (size_t k = 0; k < ents.size(); ++k) entities[ents[k]] = sub[k];
return out;
}
static SketchSolveResult solve_impl(std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints,
int dragged_ei, Role dragged_role)
{
SketchSolveResult out = solve_system(entities, constraints, dragged_ei, dragged_role);
if (out.ok || out.result != SLVS_RESULT_TOO_MANY_UNKNOWNS) return out;
return solve_partitioned(entities, constraints, dragged_ei, dragged_role);
}
SketchSolveResult sketch_solve(std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints)
{
return solve_impl(entities, constraints, -1, Role::P0);
}
SketchSolveResult sketch_solve_drag(std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints,
int dragged_ei, SketchPointRole dragged_role)
{
return solve_impl(entities, constraints, dragged_ei, dragged_role);
}
} // namespace Slic3r
+37
View File
@@ -0,0 +1,37 @@
#ifndef slic3r_SketchSolver_hpp_
#define slic3r_SketchSolver_hpp_
// Bridge from the Design tab's SketchEntity / SketchEntityConstraintDef model onto the
// vendored SolveSpace constraint solver (src/libslic3r/slvs, libslvs). Replaces the
// hand-rolled SketchConstraints: full constraint set, real DoF counting, and
// over-constrained (bad-constraint) detection. Solves on a fixed 2D XY workplane.
#include "libslic3r/CAD/SketchEngine.hpp"
#include <vector>
namespace Slic3r {
struct SketchSolveResult {
bool ok{false}; // solver converged & consistent
int dof{-1}; // remaining degrees of freedom (>0 under-constrained)
int result{0}; // raw SLVS_RESULT_* code
std::vector<int> bad; // indices (into `constraints`) of conflicting constraints
};
// Solve `constraints` over `entities` in place (writes solved coordinates back into the
// entities; arc angles are reflowed preserving sweep direction). No-op success when
// `constraints` is empty.
SketchSolveResult sketch_solve(std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints);
// Drag-aware solve: pins the (dragged_ei, dragged_role) point's parameters via the
// solver's `dragged[]` priority list so the solver keeps that point where the cursor
// placed it (caller must have moved it first) and moves the OTHER free geometry to
// re-satisfy the constraints. dragged_ei < 0 behaves identically to sketch_solve.
SketchSolveResult sketch_solve_drag(std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints,
int dragged_ei, SketchPointRole dragged_role);
} // namespace Slic3r
#endif
+95
View File
@@ -0,0 +1,95 @@
#include "libslic3r/CAD/ThreadStandards.hpp"
namespace Slic3r {
// Imperial helpers: convert nominal inch diameter / threads-per-inch to mm.
static constexpr double IN = 25.4;
static inline double tpi_pitch(double tpi) { return IN / tpi; }
const std::vector<ThreadSpec>& thread_standards()
{
using S = ThreadSpec::Series;
static const std::vector<ThreadSpec> table = {
// --- ISO metric, coarse pitch (ISO 261 preferred series) ---
{"M1", 1.0, 0.25, S::MetricCoarse},
{"M1.2", 1.2, 0.25, S::MetricCoarse},
{"M1.6", 1.6, 0.35, S::MetricCoarse},
{"M2", 2.0, 0.40, S::MetricCoarse},
{"M2.5", 2.5, 0.45, S::MetricCoarse},
{"M3", 3.0, 0.50, S::MetricCoarse},
{"M4", 4.0, 0.70, S::MetricCoarse},
{"M5", 5.0, 0.80, S::MetricCoarse},
{"M6", 6.0, 1.00, S::MetricCoarse},
{"M8", 8.0, 1.25, S::MetricCoarse},
{"M10", 10.0, 1.50, S::MetricCoarse},
{"M12", 12.0, 1.75, S::MetricCoarse},
{"M14", 14.0, 2.00, S::MetricCoarse},
{"M16", 16.0, 2.00, S::MetricCoarse},
{"M20", 20.0, 2.50, S::MetricCoarse},
{"M24", 24.0, 3.00, S::MetricCoarse},
{"M30", 30.0, 3.50, S::MetricCoarse},
{"M36", 36.0, 4.00, S::MetricCoarse},
{"M42", 42.0, 4.50, S::MetricCoarse},
{"M48", 48.0, 5.00, S::MetricCoarse},
{"M56", 56.0, 5.50, S::MetricCoarse},
{"M64", 64.0, 6.00, S::MetricCoarse},
// --- ISO metric, common fine pitches (ISO 261 fine series) ---
{"M8x1", 8.0, 1.00, S::MetricFine},
{"M10x1.25", 10.0, 1.25, S::MetricFine},
{"M10x1", 10.0, 1.00, S::MetricFine},
{"M12x1.5", 12.0, 1.50, S::MetricFine},
{"M12x1.25", 12.0, 1.25, S::MetricFine},
{"M16x1.5", 16.0, 1.50, S::MetricFine},
{"M20x1.5", 20.0, 1.50, S::MetricFine},
{"M24x2", 24.0, 2.00, S::MetricFine},
// --- Unified National Coarse (UTS / ASME B1.1) ---
{"#1-64 UNC", 0.073 * IN, tpi_pitch(64), S::UNC},
{"#2-56 UNC", 0.086 * IN, tpi_pitch(56), S::UNC},
{"#3-48 UNC", 0.099 * IN, tpi_pitch(48), S::UNC},
{"#4-40 UNC", 0.112 * IN, tpi_pitch(40), S::UNC},
{"#5-40 UNC", 0.125 * IN, tpi_pitch(40), S::UNC},
{"#6-32 UNC", 0.138 * IN, tpi_pitch(32), S::UNC},
{"#8-32 UNC", 0.164 * IN, tpi_pitch(32), S::UNC},
{"#10-24 UNC", 0.190 * IN, tpi_pitch(24), S::UNC},
{"#12-24 UNC", 0.216 * IN, tpi_pitch(24), S::UNC},
{"1/4-20 UNC", 0.250 * IN, tpi_pitch(20), S::UNC},
{"5/16-18 UNC", 0.3125 * IN, tpi_pitch(18), S::UNC},
{"3/8-16 UNC", 0.375 * IN, tpi_pitch(16), S::UNC},
{"7/16-14 UNC", 0.4375 * IN, tpi_pitch(14), S::UNC},
{"1/2-13 UNC", 0.500 * IN, tpi_pitch(13), S::UNC},
{"9/16-12 UNC", 0.5625 * IN, tpi_pitch(12), S::UNC},
{"5/8-11 UNC", 0.625 * IN, tpi_pitch(11), S::UNC},
{"3/4-10 UNC", 0.750 * IN, tpi_pitch(10), S::UNC},
{"7/8-9 UNC", 0.875 * IN, tpi_pitch(9), S::UNC},
{"1-8 UNC", 1.000 * IN, tpi_pitch(8), S::UNC},
// --- Unified National Fine (UTS / ASME B1.1) ---
{"#2-64 UNF", 0.086 * IN, tpi_pitch(64), S::UNF},
{"#4-48 UNF", 0.112 * IN, tpi_pitch(48), S::UNF},
{"#6-40 UNF", 0.138 * IN, tpi_pitch(40), S::UNF},
{"#8-36 UNF", 0.164 * IN, tpi_pitch(36), S::UNF},
{"#10-32 UNF", 0.190 * IN, tpi_pitch(32), S::UNF},
{"1/4-28 UNF", 0.250 * IN, tpi_pitch(28), S::UNF},
{"5/16-24 UNF", 0.3125 * IN, tpi_pitch(24), S::UNF},
{"3/8-24 UNF", 0.375 * IN, tpi_pitch(24), S::UNF},
{"7/16-20 UNF", 0.4375 * IN, tpi_pitch(20), S::UNF},
{"1/2-20 UNF", 0.500 * IN, tpi_pitch(20), S::UNF},
{"9/16-18 UNF", 0.5625 * IN, tpi_pitch(18), S::UNF},
{"5/8-18 UNF", 0.625 * IN, tpi_pitch(18), S::UNF},
{"3/4-16 UNF", 0.750 * IN, tpi_pitch(16), S::UNF},
{"1-12 UNF", 1.000 * IN, tpi_pitch(12), S::UNF},
};
return table;
}
const ThreadSpec* find_thread_standard(const std::string& name)
{
for (const ThreadSpec& s : thread_standards())
if (s.name == name)
return &s;
return nullptr;
}
} // namespace Slic3r
+39
View File
@@ -0,0 +1,39 @@
#ifndef slic3r_ThreadStandards_hpp_
#define slic3r_ThreadStandards_hpp_
#include <string>
#include <vector>
namespace Slic3r {
// Canonical mechanical thread specifications (ISO metric + Unified imperial).
// All dimensions are stored in millimetres so the CAD kernel can consume them
// directly. The profile is the common 60deg V shared by ISO 261/965 and ASME
// B1.1 (UTS), so the cut/ridge depth used by the Design-tab Thread tool is the
// basic external thread height h = 0.6134 * pitch, and the internal (tapped)
// minor diameter is D1 = D - 1.0825 * pitch (= D - 2*5H/8).
struct ThreadSpec {
enum class Series { MetricCoarse, MetricFine, UNC, UNF };
std::string name; // designation, e.g. "M6", "1/4-20 UNC"
double major_diameter_mm; // nominal (crest) diameter
double pitch_mm; // axial advance per turn
Series series;
// 60deg basic external thread height (radial crest-to-root engagement).
double thread_depth_mm() const { return 0.6134 * pitch_mm; }
// Internal/tapped minor (tap-drill) diameter for the same nominal thread.
double minor_diameter_mm() const { return major_diameter_mm - 1.0825 * pitch_mm; }
bool imperial() const { return series == Series::UNC || series == Series::UNF; }
};
// Full ordered table (metric coarse, metric fine, UNC, UNF) for GUI listing.
const std::vector<ThreadSpec>& thread_standards();
// Exact case-sensitive designation lookup; nullptr if not a known standard.
const ThreadSpec* find_thread_standard(const std::string& name);
} // namespace Slic3r
#endif
+79
View File
@@ -21,6 +21,11 @@ endif()
option(BUILD_SHARED_LIBS "Build shared libs" OFF)
option(USE_SLIC3R_CONSOLE_LOG "Enable console logging in RelWithDebInfo builds" OFF)
# SolveSpace constraint solver (2D sketch solver backbone), built in deps/SLVS.
if (SLIC3R_CAD)
find_package(SLVS REQUIRED)
endif ()
set(lisbslic3r_sources
AABBMesh.cpp
AABBMesh.hpp
@@ -147,6 +152,8 @@ set(lisbslic3r_sources
Fill/FillBase.hpp
Fill/FillConcentric.cpp
Fill/FillConcentric.hpp
Fill/FillSpiralInset.cpp
Fill/FillSpiralInset.hpp
Fill/FillConcentricInternal.cpp
Fill/FillConcentricInternal.hpp
Fill/FillCornerSmoothing.cpp
@@ -258,6 +265,8 @@ set(lisbslic3r_sources
GCode/SmallAreaInfillFlowCompensator.hpp
GCode/SpiralVase.cpp
GCode/SpiralVase.hpp
GCode/WipePathHelpers.cpp
GCode/WipePathHelpers.hpp
GCode/ThumbnailData.cpp
GCode/ThumbnailData.hpp
GCode/Thumbnails.cpp
@@ -270,6 +279,8 @@ set(lisbslic3r_sources
GCode/WipeTower2.hpp
GCode/WipeTower.cpp
GCode/WipeTower.hpp
GCode/WipeTowerEstimate.cpp
GCode/WipeTowerEstimate.hpp
GCodeWriter.cpp
GCodeWriter.hpp
Geometry/ArcWelder.hpp
@@ -298,6 +309,8 @@ set(lisbslic3r_sources
Layer.cpp
Layer.hpp
LayerRegion.cpp
LayOnFace.cpp
LayOnFace.hpp
libslic3r.cpp
libslic3r.h
Line.cpp
@@ -368,6 +381,8 @@ set(lisbslic3r_sources
Preset.hpp
PrincipalComponents2D.cpp
PrincipalComponents2D.hpp
PublishSettings.cpp
PublishSettings.hpp
PrintApply.cpp
PrintBase.cpp
PrintBase.hpp
@@ -498,6 +513,29 @@ set(lisbslic3r_sources
FlushVolPredictor.cpp
)
# Parametric Design/CAD kernel. Needs OCCT's ModelingAlgorithms module and the
# vendored SolveSpace solver; both are pulled in only when SLIC3R_CAD is ON.
if (SLIC3R_CAD)
list(APPEND lisbslic3r_sources
CAD/GeometryEngine.cpp
CAD/GeometryEngine.hpp
CAD/SketchEngine.cpp
CAD/SketchEngine.hpp
CAD/SketchConstraints.cpp
CAD/SketchConstraints.hpp
CAD/SketchSolver.cpp
CAD/SketchSolver.hpp
CAD/SketchInference.cpp
CAD/SketchInference.hpp
CAD/SketchImport.cpp
CAD/SketchImport.hpp
CAD/CadDocument.cpp
CAD/CadDocument.hpp
CAD/ThreadStandards.cpp
CAD/ThreadStandards.hpp
)
endif ()
if (APPLE)
list(APPEND lisbslic3r_sources
MacUtils.mm
@@ -552,6 +590,12 @@ if (_opts)
target_compile_options(libslic3r_cgal PRIVATE "${_opts_bad}")
endif()
if (IS_CLANG_CL)
# CGAL passes /fp:strict /fp:except-. clang-cl reports the second as overriding part of
# the first; the settings cc1 receives are the same ones MSVC produces from that pair.
target_compile_options(libslic3r_cgal PRIVATE -Wno-overriding-option)
endif ()
target_link_libraries(libslic3r_cgal PRIVATE ${_cgal_tgt} admesh libigl mcut boost_libs)
if (MSVC AND "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") # 32 bit MSVC workaround
@@ -603,6 +647,30 @@ set(OCCT_LIBS
TKMath
TKernel
)
# The CAD kernel is the only consumer of OCCT's ModelingAlgorithms module: TKFillet
# (BRepFilletAPI), TKOffset (BRepOffsetAPI) and TKBool, which the other two need.
#
# PREPEND, never append: this list is single-pass static link order, dependents before
# dependencies note TKernel, which everything needs, is deliberately last. TKOffset
# references BRepAlgo_Loop, which TKBool defines, so TKOffset must come BEFORE TKBool.
# Appending put it after, and a strictly single-pass linker (the Flatpak build) failed with
# libTKOffset.a(BRepOffset_MakeLoops.cxx.o): undefined reference to
# `BRepAlgo_Loop::BRepAlgo_Loop()'
# while the ordinary Linux, macOS and Windows links resolved it anyway. Use set() rather
# than list(PREPEND), which needs CMake 3.15 and this project supports 3.13.
if (SLIC3R_CAD)
set(OCCT_LIBS TKFillet TKOffset TKBool ${OCCT_LIBS})
# deps is configured separately, so its SLIC3R_CAD can differ from ours. The module is
# all-or-nothing, so one absent toolkit proves it; fail here rather than at link time.
if (NOT TARGET TKFillet)
message(FATAL_ERROR
"SLIC3R_CAD is ON, but the OpenCASCADE in ${CMAKE_PREFIX_PATH} was built without "
"BUILD_MODULE_ModelingAlgorithms. Rebuild the dependencies with -DSLIC3R_CAD=ON, "
"or configure this project with -DSLIC3R_CAD=OFF.")
endif ()
endif ()
# Published for the Windows packaging step in the top-level CMakeLists.txt.
set(OCCT_LIBS "${OCCT_LIBS}" CACHE INTERNAL "OCCT toolkits linked by libslic3r")
target_link_libraries(libslic3r
PUBLIC
@@ -655,6 +723,10 @@ if (TARGET OpenVDB::openvdb)
target_link_libraries(libslic3r PRIVATE OpenVDB::openvdb)
endif()
if (SLIC3R_CAD)
target_link_libraries(libslic3r PUBLIC SLVS::slvs)
endif ()
if(WIN32)
target_link_libraries(libslic3r PRIVATE Psapi.lib bcrypt.lib)
endif()
@@ -663,6 +735,13 @@ if(SLIC3R_PROFILE)
target_link_libraries(libslic3r PRIVATE Shiny)
endif()
if (WIN32)
# Public, since BlacklistedLibraryCheck.hpp includes windows.h. Empty
# WIN32_LEAN_AND_MEAN matches the sources that define it themselves; bare
# NOMINMAX matches the one libigl already passes.
target_compile_definitions(libslic3r PUBLIC "WIN32_LEAN_AND_MEAN=" "NOMINMAX")
endif ()
if (SLIC3R_PCH AND NOT SLIC3R_SYNTAXONLY)
add_precompiled_header(libslic3r pchheader.hpp FORCEINCLUDE)
endif ()
+8 -8
View File
@@ -13,8 +13,8 @@ Slic3r::Polylines Paths64_to_polylines(const Clipper2Lib::Paths64& in)
Slic3r::Points points;
points.reserve(path64.size());
for (const Clipper2Lib::Point64& point64 : path64)
points.emplace_back(std::move(Slic3r::Point(point64.x, point64.y)));
out.emplace_back(std::move(Slic3r::Polyline(points)));
points.emplace_back(Slic3r::Point(point64.x, point64.y));
out.emplace_back(Slic3r::Polyline(points));
}
return out;
}
@@ -29,7 +29,7 @@ Clipper2Lib::Paths64 Slic3rPoints_to_Paths64(const Container& in)
Clipper2Lib::Path64 path;
path.reserve(item.size());
for (const Slic3r::Point& point : item.points)
path.emplace_back(std::move(Clipper2Lib::Point64(point.x(), point.y())));
path.emplace_back(Clipper2Lib::Point64(point.x(), point.y()));
out.emplace_back(std::move(path));
}
return out;
@@ -44,7 +44,7 @@ Points Path64ToPoints(const Clipper2Lib::Path64& path64)
{
Points points;
points.reserve(path64.size());
for (const Clipper2Lib::Point64 &point64 : path64) points.emplace_back(std::move(Slic3r::Point(point64.x, point64.y)));
for (const Clipper2Lib::Point64 &point64 : path64) points.emplace_back(Slic3r::Point(point64.x, point64.y));
return points;
}
@@ -99,7 +99,7 @@ Clipper2Lib::Paths64 Slic3rPolygons_to_Paths64(const Polygons &in)
for (const Polygon &poly : in) {
Clipper2Lib::Path64 path;
path.reserve(poly.points.size());
for (const Slic3r::Point &point : poly.points) path.emplace_back(std::move(Clipper2Lib::Point64(point.x(), point.y())));
for (const Slic3r::Point &point : poly.points) path.emplace_back(Clipper2Lib::Point64(point.x(), point.y()));
out.emplace_back(std::move(path));
}
return out;
@@ -114,7 +114,7 @@ Clipper2Lib::Paths64 Slic3rExPolygons_to_Paths64(const ExPolygons& in)
const auto &poly = expolygon.contour_or_hole(i);
Clipper2Lib::Path64 path;
path.reserve(poly.points.size());
for (const Slic3r::Point &point : poly.points) path.emplace_back(std::move(Clipper2Lib::Point64(point.x(), point.y())));
for (const Slic3r::Point &point : poly.points) path.emplace_back(Clipper2Lib::Point64(point.x(), point.y()));
out.emplace_back(std::move(path));
}
}
@@ -134,8 +134,8 @@ Polylines _clipper2_pl_open(Clipper2Lib::ClipType clipType, const Slic3r::Polyli
Slic3r::Polylines out;
out.reserve(solution.size() + solution_open.size());
polylines_append(out, std::move(Paths64_to_polylines(solution)));
polylines_append(out, std::move(Paths64_to_polylines(solution_open)));
polylines_append(out, Paths64_to_polylines(solution));
polylines_append(out, Paths64_to_polylines(solution_open));
return out;
}
+66 -11
View File
@@ -7,6 +7,7 @@
#include <algorithm>
#include <assert.h>
#include <fstream>
#include <sstream>
#include <iostream>
#include <iomanip>
#include <regex>
@@ -1049,7 +1050,8 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex
std::vector<std::string>& different_settings = this->option<ConfigOptionStrings>("different_settings_to_system", true)->values;
size_t size = different_settings.size();
if (size == 0) {
size = this->option<ConfigOptionStrings>("filament_settings_id")->values.size() + 2;
const auto *filament_ids = this->option<ConfigOptionStrings>("filament_settings_id");
size = (filament_ids ? filament_ids->values.size() : 0) + 2;
different_settings.resize(size);
}
@@ -1514,6 +1516,19 @@ std::optional<PluginCapabilityRef> parse_capability_ref(const std::string& value
//BBS: add json support
void ConfigBase::save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const
{
// Serialize first: if that throws (invalid UTF-8), the existing file stays untouched.
std::ostringstream ss;
this->save_to_json(ss, name, from, version);
boost::nowide::ofstream c;
c.open(file, std::ios::out | std::ios::trunc);
c << ss.str();
c.close();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
}
void ConfigBase::save_to_json(std::ostream &os, const std::string &name, const std::string &from, const std::string &version, bool replace_invalid_utf8) const
{
json j;
//record the headers
@@ -1560,12 +1575,7 @@ void ConfigBase::save_to_json(const std::string &file, const std::string &name,
j["plugins"] = unique_refs;
}
boost::nowide::ofstream c;
c.open(file, std::ios::out | std::ios::trunc);
c << j.dump(1, '\t') << std::endl;
c.close();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
os << j.dump(1, '\t', false, replace_invalid_utf8 ? json::error_handler_t::replace : json::error_handler_t::strict) << std::endl;
}
void ConfigBase::save(const std::string &file) const
@@ -1715,6 +1725,36 @@ const ConfigOption* DynamicConfig::optptr(const t_config_option_key &opt_key) co
return (it == options.end()) ? nullptr : it->second.get();
}
// ConfigOptionBool(s)::deserialize only understands "1" and "0", but scripts commonly spell CLI
// flags as --opt=true or --opt=no. Map the usual spellings onto what deserialize() accepts, per
// comma-separated item so vector options keep working, and pass anything else through unchanged
// so a genuine typo is still reported as invalid.
static std::string normalize_cli_bool_value(const std::string &value)
{
static const char* true_values[] = { "1", "true", "yes", "on", "enabled" };
static const char* false_values[] = { "0", "false", "no", "off", "disabled" };
auto matches = [](const std::string &item, const char* const* candidates, size_t count) {
return std::any_of(candidates, candidates + count, [&item](const char* candidate) { return boost::iequals(item, candidate); });
};
std::string normalized;
std::istringstream is(value);
std::string item;
while (std::getline(is, item, ',')) {
boost::trim(item);
if (! normalized.empty())
normalized += ",";
if (matches(item, true_values, std::size(true_values)))
normalized += "1";
else if (matches(item, false_values, std::size(false_values)))
normalized += "0";
else
normalized += item;
}
return normalized;
}
bool DynamicConfig::read_cli(int argc, const char* const argv[], t_config_option_keys* extra, t_config_option_keys* keys)
{
// cache the CLI option => opt_key mapping
@@ -1812,17 +1852,32 @@ bool DynamicConfig::read_cli(int argc, const char* const argv[], t_config_option
// to the end of the value.
if (opt_base->type() == coBools && value.empty())
static_cast<ConfigOptionBools*>(opt_base)->values.push_back(!no);
else
else {
// Deserialize any other vector value (ConfigOptionInts, Floats, Percents, Points) the same way
// they get deserialized from an .ini file. For ConfigOptionStrings, that means that the C-style unescape
// will be applied for values enclosed in quotes, while values non-enclosed in quotes are left to be
// unescaped by the calling shell.
opt_vector->deserialize(value, true);
const std::string vector_value = opt_base->type() == coBools ? normalize_cli_bool_value(value) : value;
bool deserialized = false;
try {
deserialized = opt_vector->deserialize(vector_value, true);
} catch (const std::exception &ex) {
// e.g. "nil" deserialized into a non-nullable vector option throws instead of
// returning false - treat that the same as any other invalid value here.
deserialized = false;
}
if (! deserialized) {
boost::nowide::cerr << "Invalid value for option --" << token.c_str() << std::endl;
return false;
}
}
} else if (opt_base->type() == coBool) {
if (value.empty())
static_cast<ConfigOptionBool*>(opt_base)->value = !no;
else
opt_base->deserialize(value);
else if (! opt_base->deserialize(normalize_cli_bool_value(value))) {
boost::nowide::cerr << "Invalid value for option --" << token.c_str() << std::endl;
return false;
}
} else if (opt_base->type() == coString) {
// Do not unescape single string values, the unescaping is left to the calling shell.
static_cast<ConfigOptionString*>(opt_base)->value = value;
+15
View File
@@ -1006,6 +1006,7 @@ public:
int getInt() const override { return this->value; }
void setInt(int val) override { this->value = val; }
ConfigOption* clone() const override { return new ConfigOptionInt(*this); }
using ConfigOptionSingle<int>::operator==;
bool operator==(const ConfigOptionInt &rhs) const throw() { return this->value == rhs.value; }
std::string serialize() const override
@@ -1048,6 +1049,7 @@ public:
ConfigOptionType type() const override { return static_type(); }
ConfigOption* clone() const override { return new ConfigOptionIntsTempl(*this); }
ConfigOptionIntsTempl& operator= (const ConfigOption *opt) { this->set(opt); return *this; }
using ConfigOptionVector<int>::operator==;
bool operator==(const ConfigOptionIntsTempl &rhs) const throw() { return this->values == rhs.values; }
bool operator< (const ConfigOptionIntsTempl &rhs) const throw() { return this->values < rhs.values; }
// Could a special "nil" value be stored inside the vector, indicating undefined value?
@@ -1137,6 +1139,7 @@ public:
ConfigOptionType type() const override { return static_type(); }
ConfigOption* clone() const override { return new ConfigOptionString(*this); }
ConfigOptionString& operator=(const ConfigOption *opt) { this->set(opt); return *this; }
using ConfigOptionSingle<std::string>::operator==;
bool operator==(const ConfigOptionString &rhs) const throw() { return this->value == rhs.value; }
bool operator< (const ConfigOptionString &rhs) const throw() { return this->value < rhs.value; }
bool empty() const { return this->value.empty(); }
@@ -1171,6 +1174,7 @@ public:
ConfigOptionType type() const override { return static_type(); }
ConfigOption* clone() const override { return new ConfigOptionStrings(*this); }
ConfigOptionStrings& operator=(const ConfigOption *opt) { this->set(opt); return *this; }
using ConfigOptionVector<std::string>::operator==;
bool operator==(const ConfigOptionStrings &rhs) const throw() { return this->values == rhs.values; }
bool operator< (const ConfigOptionStrings &rhs) const throw() { return this->values < rhs.values; }
bool is_nil(size_t) const override { return false; }
@@ -1215,6 +1219,7 @@ public:
ConfigOptionType type() const override { return static_type(); }
ConfigOption* clone() const override { return new ConfigOptionPercent(*this); }
ConfigOptionPercent& operator= (const ConfigOption *opt) { this->set(opt); return *this; }
using ConfigOptionFloat::operator==;
bool operator==(const ConfigOptionPercent &rhs) const throw() { return this->value == rhs.value; }
bool operator< (const ConfigOptionPercent &rhs) const throw() { return this->value < rhs.value; }
@@ -1257,6 +1262,7 @@ public:
ConfigOptionType type() const override { return static_type(); }
ConfigOption* clone() const override { return new ConfigOptionPercentsTempl(*this); }
ConfigOptionPercentsTempl& operator=(const ConfigOption *opt) { this->set(opt); return *this; }
using ConfigOptionFloatsTempl<NULLABLE>::operator==;
bool operator==(const ConfigOptionPercentsTempl &rhs) const throw() { return ConfigOptionFloatsTempl<NULLABLE>::vectors_equal(this->values, rhs.values); }
bool operator< (const ConfigOptionPercentsTempl &rhs) const throw() { return ConfigOptionFloatsTempl<NULLABLE>::vectors_lower(this->values, rhs.values); }
@@ -1502,6 +1508,7 @@ public:
ConfigOptionType type() const override { return static_type(); }
ConfigOption* clone() const override { return new ConfigOptionPoint(*this); }
ConfigOptionPoint& operator=(const ConfigOption *opt) { this->set(opt); return *this; }
using ConfigOptionSingle<Vec2d>::operator==;
bool operator==(const ConfigOptionPoint &rhs) const throw() { return this->value == rhs.value; }
bool operator< (const ConfigOptionPoint &rhs) const throw() { return this->value < rhs.value; }
@@ -1539,6 +1546,7 @@ public:
ConfigOptionType type() const override { return static_type(); }
ConfigOption* clone() const override { return new ConfigOptionPoints(*this); }
ConfigOptionPoints& operator= (const ConfigOption *opt) { this->set(opt); return *this; }
using ConfigOptionVector<Vec2d>::operator==;
bool operator==(const ConfigOptionPoints &rhs) const throw() { return this->values == rhs.values; }
bool operator< (const ConfigOptionPoints &rhs) const throw()
{ return std::lexicographical_compare(this->values.begin(), this->values.end(), rhs.values.begin(), rhs.values.end(), [](const auto &l, const auto &r){ return l < r; }); }
@@ -1617,6 +1625,7 @@ public:
ConfigOptionType type() const override { return static_type(); }
ConfigOption* clone() const override { return new ConfigOptionPoint3(*this); }
ConfigOptionPoint3& operator=(const ConfigOption *opt) { this->set(opt); return *this; }
using ConfigOptionSingle<Vec3d>::operator==;
bool operator==(const ConfigOptionPoint3 &rhs) const throw() { return this->value == rhs.value; }
bool operator< (const ConfigOptionPoint3 &rhs) const throw()
{ return this->value.x() < rhs.value.x() || (this->value.x() == rhs.value.x() && (this->value.y() < rhs.value.y() || (this->value.y() == rhs.value.y() && this->value.z() < rhs.value.z()))); }
@@ -1860,6 +1869,7 @@ public:
bool getBool() const override { return this->value; }
ConfigOption* clone() const override { return new ConfigOptionBool(*this); }
ConfigOptionBool& operator=(const ConfigOption *opt) { this->set(opt); return *this; }
using ConfigOptionSingle<bool>::operator==;
bool operator==(const ConfigOptionBool &rhs) const throw() { return this->value == rhs.value; }
bool operator< (const ConfigOptionBool &rhs) const throw() { return int(this->value) < int(rhs.value); }
@@ -1911,6 +1921,7 @@ public:
ConfigOptionType type() const override { return static_type(); }
ConfigOption* clone() const override { return new ConfigOptionBoolsTempl(*this); }
ConfigOptionBoolsTempl& operator=(const ConfigOption *opt) { this->set(opt); return *this; }
using ConfigOptionVector<unsigned char>::operator==;
bool operator==(const ConfigOptionBoolsTempl &rhs) const throw() { return this->values == rhs.values; }
bool operator< (const ConfigOptionBoolsTempl &rhs) const throw() { return this->values < rhs.values; }
// Could a special "nil" value be stored inside the vector, indicating undefined value?
@@ -2163,6 +2174,7 @@ public:
ConfigOptionEnumsGenericTempl& operator= (const ConfigOption* opt) { this->set(opt); return *this; }
bool operator< (const ConfigOptionInts& rhs) const throw() { return this->values < rhs.values; }
using ConfigOptionInts::operator==;
bool operator==(const ConfigOptionInts& rhs) const
{
if (rhs.type() != this->type())
@@ -2813,6 +2825,9 @@ public:
//BBS: add json support
void save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const;
// Same document, written to a stream. Invalid UTF-8 in a string value throws nlohmann's type_error unless
// replace_invalid_utf8 is set, which writes U+FFFD instead (for callers such as stdout with no handler).
void save_to_json(std::ostream &os, const std::string &name, const std::string &from, const std::string &version, bool replace_invalid_utf8 = false) const;
// Rebuild the in-memory "plugins" manifest (the "name;uuid;capability" references the plugin
// dispatchers consume) from the plugin-backed options via the registered resolver. save_to_json()
+5 -5
View File
@@ -968,10 +968,10 @@ EmbossStyles Emboss::get_font_list_by_register() {
}
// TODO: Fix global function
bool CALLBACK EnumFamCallBack(LPLOGFONT lplf,
LPNEWTEXTMETRIC lpntm,
DWORD FontType,
LPVOID aFontList)
int CALLBACK EnumFamCallBack(const LOGFONT *lplf,
const TEXTMETRIC *lpntm,
DWORD FontType,
LPARAM aFontList)
{
std::vector<std::wstring> *fontList =
(std::vector<std::wstring> *) (aFontList);
@@ -988,7 +988,7 @@ EmbossStyles Emboss::get_font_list_by_enumeration() {
HDC hDC = GetDC(NULL);
std::vector<std::wstring> font_names;
EnumFontFamilies(hDC, (LPCTSTR) NULL, (FONTENUMPROC) EnumFamCallBack,
EnumFontFamilies(hDC, (LPCTSTR) NULL, EnumFamCallBack,
(LPARAM) &font_names);
EmbossStyles font_list;
+4
View File
@@ -454,6 +454,10 @@ class ExtrusionLoop : public ExtrusionEntity
{
public:
ExtrusionPaths paths;
// ORCA: Set on a loop extruded entirely in mid air and out of reach of the layer below: it has
// nothing to lean on until this layer is bridged, so the G-code writer holds it back until the
// infill is down. See defer_unsupported_loops() in PerimeterGenerator.cpp.
bool print_after_infill = false;
ExtrusionLoop(ExtrusionLoopRole role = elrDefault) : m_loop_role(role) {}
ExtrusionLoop(const ExtrusionPaths &paths, ExtrusionLoopRole role = elrDefault) : paths(paths), m_loop_role(role) {}
+10 -7
View File
@@ -342,7 +342,7 @@ void fuzzy_polyline(Points& poly, bool closed, coordf_t slice_z, const FuzzySkin
}
// Thanks Cura developers for this function.
void fuzzy_extrusion_line(Arachne::ExtrusionJunctions& ext_lines, coordf_t slice_z, const FuzzySkinConfig& cfg, bool closed)
void fuzzy_extrusion_line(Arachne::ExtrusionJunctions& ext_lines, coordf_t slice_z, coordf_t layer_height, const FuzzySkinConfig& cfg, bool closed)
{
if (cfg.noise_type == NoiseType::Ripple) {
@@ -356,7 +356,9 @@ void fuzzy_extrusion_line(Arachne::ExtrusionJunctions& ext_lines, coordf_t slice
const double min_dist_between_points = cfg.point_distance * 3. / 4.; // hardcoded: the point distance may vary between 3/4 and 5/4 the supplied value
const double range_random_point_dist = cfg.point_distance / 2.;
const double min_extrusion_width = 0.01; // workaround for many print options. Need overwrite formula with the layer height parameter. The width must more than >>> layer_height * (1 - 0.25 * PI) * 1.05 <<< (last num is the coeff of overlay error case)
// ExtrusionJunction::w is a scaled coord_t, so this floor must be scaled too.
// Flow::rounded_rectangle_extrusion_spacing() requires width > height * (1 - 0.25 * PI); keep 5% above it.
const double min_extrusion_width = scaled<double>(layer_height * (1. - 0.25 * M_PI) * 1.05);
double dist_left_over = random_value() * (min_dist_between_points / 2.); // the distance to be traversed on the line before making the first new point
auto* p0 = &ext_lines.front();
@@ -685,12 +687,13 @@ Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perim
void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerator& perimeter_generator, const bool is_contour, const bool closed)
{
const auto slice_z = perimeter_generator.slice_z;
const auto layer_height = perimeter_generator.layer_height;
const auto& regions = perimeter_generator.regions_by_fuzzify;
if (regions.size() == 1) { // optimization
const auto& config = regions.begin()->first;
const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, extrusion->inset_idx, is_contour);
if (fuzzify)
fuzzy_extrusion_line(extrusion->junctions, slice_z, config, closed);
fuzzy_extrusion_line(extrusion->junctions, slice_z, perimeter_generator.layer_height, config, closed);
} else {
// Merge regions that produce identical fuzzy effects (differ only in type).
// When the style (e.g. External) and a painted region (All) both fuzzify this loop
@@ -701,7 +704,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
// Fast path: single merged region — apply directly without splitting
if (merged_regions.size() == 1 && merged_regions.front().expolygons.empty()) {
fuzzy_extrusion_line(extrusion->junctions, slice_z, *merged_regions.front().config, closed);
fuzzy_extrusion_line(extrusion->junctions, slice_z, perimeter_generator.layer_height, *merged_regions.front().config, closed);
return;
}
@@ -761,7 +764,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
// Fuzzy splitted extrusion
if (std::all_of(splitted.begin(), splitted.end(), [](const Algorithm::SplitLineJunction& j) { return j.clipped; })) {
// The entire polygon is fuzzified
fuzzy_extrusion_line(extrusion->junctions, slice_z, *r.config, closed);
fuzzy_extrusion_line(extrusion->junctions, slice_z, perimeter_generator.layer_height, *r.config, closed);
continue;
} else {
const auto current_ext = extrusion->junctions;
@@ -769,12 +772,12 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
segment.reserve(current_ext.size());
extrusion->junctions.clear();
const auto fuzzy_current_segment = [&segment, &extrusion, &r, slice_z]() {
const auto fuzzy_current_segment = [&segment, &extrusion, &r, slice_z, layer_height]() {
// Orca: non fuzzy points to isolate fuzzy region
const auto front = segment.front();
const auto back = segment.back();
fuzzy_extrusion_line(segment, slice_z, *r.config, false);
fuzzy_extrusion_line(segment, slice_z, layer_height, *r.config, false);
// Orca: only add non fuzzy point if it's not in the extrusion closing point.
if (!extrusion->junctions.empty() && extrusion->junctions.front().p != front.p) {
extrusion->junctions.push_back(front);
@@ -9,7 +9,7 @@ namespace Slic3r::Feature::FuzzySkin {
void fuzzy_polyline(Points& poly, bool closed, coordf_t slice_z, const FuzzySkinConfig& cfg);
void fuzzy_extrusion_line(Arachne::ExtrusionJunctions& ext_lines, coordf_t slice_z, const FuzzySkinConfig& cfg, bool closed = true);
void fuzzy_extrusion_line(Arachne::ExtrusionJunctions& ext_lines, coordf_t slice_z, coordf_t layer_height, const FuzzySkinConfig& cfg, bool closed = true);
void group_region_by_fuzzify(PerimeterGenerator& g);
+63 -54
View File
@@ -11,7 +11,7 @@
#include "AABBTreeLines.hpp"
#include "ExtrusionEntity.hpp"
#include "FillBase.hpp"
#include "Fill.hpp"
#include "FillRectilinear.hpp"
#include "FillLightning.hpp"
#include "FillConcentricInternal.hpp"
@@ -950,7 +950,7 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
params.extruder = region_config.internal_solid_filament_id;
// Orca: forced fill order applies only to top/bottom surfaces filled with a
// center-based pattern; everything else stays at Default to keep batching together.
if (params.pattern == ipConcentric || params.pattern == ipArchimedeanChords || params.pattern == ipOctagramSpiral) {
if (params.pattern == ipConcentric || params.pattern == ipSpiralInset || params.pattern == ipArchimedeanChords || params.pattern == ipOctagramSpiral) {
if (params.extrusion_role == erTopSolidInfill)
params.fill_order = region_config.top_surface_fill_order.value;
else if (params.extrusion_role == erBottomSurface)
@@ -1234,6 +1234,33 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
return surface_fills;
}
// Orca: Anchors and printed infill must share the same body origin. Keep the choice
// here so per-model surface centering and separated sparse infill cannot drift apart.
static BoundingBox infill_bounding_box(const Layer &layer, const SurfaceFill &fill, const ExPolygon &expoly, BoundingBox bbox)
{
const auto &params = fill.params;
const auto &config = layer.regions()[fill.region_id]->region().config();
const bool external = params.extrusion_role == erTopSolidInfill || params.extrusion_role == erBottomSurface;
const bool per_model = external && params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model &&
(params.pattern == ipArchimedeanChords || params.pattern == ipOctagramSpiral);
const bool separate = !external && params.separated_infills &&
(is_separable_infill_pattern(params.pattern) || !config.solid_infill_rotate_template.value.empty() ||
!config.sparse_infill_rotate_template.value.empty());
if (per_model || separate) {
double best_overlap = 0.;
for (size_t i = 0; i < layer.lslices.size() && i < layer.lslices_separated_component_bboxes.size(); ++i) {
const double overlap = area(intersection_ex(layer.lslices[i], expoly));
if (overlap > best_overlap) {
best_overlap = overlap;
const Point center = layer.lslices_separated_component_bboxes[i].center();
bbox = layer.object()->bounding_box();
bbox.translate(center.x(), center.y());
}
}
}
return bbox;
}
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
void export_group_fills_to_svg(const char *path, const std::vector<SurfaceFill> &fills)
{
@@ -1332,7 +1359,8 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
params.anchor_length = surface_fill.params.anchor_length;
params.anchor_length_max = surface_fill.params.anchor_length_max;
params.resolution = resolution;
params.use_arachne = surface_fill.params.pattern == ipConcentric || surface_fill.params.pattern == ipConcentricInternal;
params.use_arachne = surface_fill.params.pattern == ipConcentric || surface_fill.params.pattern == ipSpiralInset ||
surface_fill.params.pattern == ipConcentricInternal;
params.layer_height = layerm->layer()->height;
params.lateral_lattice_angle_1 = surface_fill.params.lateral_lattice_angle_1;
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
@@ -1352,19 +1380,9 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
// Orca: Checking the filling of a centered surface by drawing for each model parts
bool is_top_or_bottom = params.extrusion_role == erTopSolidInfill || params.extrusion_role == erBottomSurface;
bool is_centered_infill = surface_fill.params.pattern == ipArchimedeanChords || surface_fill.params.pattern == ipOctagramSpiral;
if (is_top_or_bottom) {
params.center_of_surface_pattern = surface_fill.params.center_of_surface_pattern; // Orca: center of surface pattern
}
// Orca: Each_Model centers the pattern on each model part's bbox; Each_Surface / Each_Assembly
// fall through to the default (whole-object) bounding box below.
bool is_per_model_center = is_top_or_bottom && params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model && is_centered_infill;
bool is_separate_infill = !is_top_or_bottom && surface_fill.params.separated_infills &&
(
is_separable_infill_pattern(surface_fill.params.pattern) ||
params.config->solid_infill_rotate_template != "" ||
params.config->sparse_infill_rotate_template != "" );
if( surface_fill.params.pattern == ipLockedZag ) {
params.locked_zag = true;
params.infill_lock_depth = surface_fill.params.infill_lock_depth;
@@ -1388,34 +1406,8 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
params.can_reverse = false;
for (ExPolygon& expoly : surface_fill.expolygons) {
// Orca: separate infill / per-model pattern centering.
//
// Center the pattern on each connected body of the object independently, so every piece
// is filled exactly as if it were sliced on its own: touching/overlapping parts merge
// into one body sharing a center, while separate parts and disconnected islands (even
// interleaved-but-not-touching ones, e.g. chain links) each get their own. The body each
// island belongs to, and its full bounding box, were resolved in 3D by PrintObject::
// infill() (lslices_separated_component_bboxes, aligned with this layer's lslices). We
// match this fill region to the island it overlaps most, then re-use the whole-object
// bounding box (origin-centered — identical extent to the default, so coverage and cost
// are unchanged) re-centered on that body.
if (is_per_model_center || is_separate_infill) {
double best_overlap = 0.;
BoundingBox best_component;
for (size_t r = 0; r < this->lslices.size() && r < this->lslices_separated_component_bboxes.size(); ++ r) {
const double overlap = area(intersection_ex(this->lslices[r], expoly));
if (overlap > best_overlap) {
best_overlap = overlap;
best_component = this->lslices_separated_component_bboxes[r];
}
}
if (best_component.defined) {
const Point c = best_component.center();
BoundingBox part_bbox = bbox; // origin-centered, whole-object extent (from above)
part_bbox.translate(c.x(), c.y()); // re-center on this body
f->set_bounding_box(part_bbox);
}
} // - End: separate infill / per-model pattern centering
// Orca: Reuse the body origin used for bridge anchoring, resetting it for each surface.
f->set_bounding_box(infill_bounding_box(*this, surface_fill, expoly, bbox));
f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
if (params.symmetric_infill_y_axis) {
@@ -1515,6 +1507,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
case ipCubic:
case ipLine:
case ipConcentric:
case ipSpiralInset:
case ipHoneycomb:
case ipLateralHoneycomb:
case ip3DHoneycomb:
@@ -1581,8 +1574,14 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
params.multiline = surface_fill.params.multiline;
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
params.smooth_factor = surface_fill.params.smooth_factor;
// Orca: Match make_fills() when choosing the origin of plane-path patterns.
// Without the sparse extrusion role, the filler uses each surface's bounds
// instead of the object's bounds, so bridge anchors shift away from printed infill.
params.extrusion_role = surface_fill.params.extrusion_role;
for (ExPolygon &expoly : surface_fill.expolygons) {
// Orca: Match the per-body origin of make_fills() before generating physical anchors.
f->set_bounding_box(infill_bounding_box(*this, surface_fill, expoly, bbox));
// Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon.
f->spacing = surface_fill.params.spacing;
surface_fill.surface.expolygon = std::move(expoly);
@@ -1596,6 +1595,25 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
return sparse_infill_polylines;
}
// Returns the filament id (1-based) the region is ironed with, or -1 when the
// region is not ironed. AllSolid always irons. TopSurfaces and TopmostOnly need
// either some top shells or, in spiral mode, more than one bottom shell, and
// TopmostOnly additionally needs the layer to be the topmost one.
int Layer::choose_ironing_extruder(const PrintRegionConfig &cfg,
bool spiral_mode,
bool is_topmost_layer)
{
if (cfg.ironing_type == IroningType::NoIroning)
return -1;
const bool gate = (cfg.ironing_type == IroningType::AllSolid)
|| ((cfg.top_shell_layers > 0 || (spiral_mode && cfg.bottom_shell_layers > 1))
&& (cfg.ironing_type == IroningType::TopSurfaces
|| (cfg.ironing_type == IroningType::TopmostOnly && is_topmost_layer)));
if (!gate)
return -1;
return cfg.top_surface_filament_id;
}
// Create ironing extrusions over top surfaces.
void Layer::make_ironing()
{
@@ -1665,19 +1683,10 @@ void Layer::make_ironing()
if (! layerm->slices.empty()) {
IroningParams ironing_params;
const PrintRegionConfig &config = layerm->region().config();
if (config.ironing_type != IroningType::NoIroning &&
(config.ironing_type == IroningType::AllSolid ||
((config.top_shell_layers > 0 || (this->object()->print()->config().spiral_mode && config.bottom_shell_layers > 1)) &&
(config.ironing_type == IroningType::TopSurfaces ||
(config.ironing_type == IroningType::TopmostOnly && layerm->layer()->upper_layer == nullptr))))) {
if (config.outer_wall_filament_id == config.top_surface_filament_id || config.wall_loops == 0) {
// Iron the whole face.
ironing_params.extruder = config.top_surface_filament_id;
} else {
// Iron just the infill.
ironing_params.extruder = config.top_surface_filament_id;
}
}
ironing_params.extruder = Layer::choose_ironing_extruder(
config,
/*spiral_mode=*/this->object()->print()->config().spiral_mode,
/*is_topmost_layer=*/layerm->layer()->upper_layer == nullptr);
if (ironing_params.extruder != -1) {
//TODO just_infill is currently not used.
ironing_params.just_infill = false;
+6
View File
@@ -14,6 +14,12 @@ namespace Slic3r {
class ExtrusionEntityCollection;
class LayerRegion;
class PrintObject;
// Orca: Share the layer rotation calculation between infill generation and internal
// bridge angle selection so both interpret rotation templates in the same way.
double calculate_infill_rotation_angle(const PrintObject *object, size_t layer_id,
const double &fixed_infill_angle, const std::string &template_string);
// An interface class to Perl, aggregating an instance of a Fill and a FillData.
class Filler
+2 -2
View File
@@ -1395,8 +1395,8 @@ void Filler::_fill_surface_single(
}
#endif /* ADAPTIVE_CUBIC_INFILL_DEBUG_OUTPUT */
const auto hook_length = coordf_t(std::min<float>(std::numeric_limits<coord_t>::max(), scale_(params.anchor_length)));
const auto hook_length_max = coordf_t(std::min<float>(std::numeric_limits<coord_t>::max(), scale_(params.anchor_length_max)));
const auto hook_length = coordf_t(scale_(params.anchor_length));
const auto hook_length_max = coordf_t(scale_(params.anchor_length_max));
Polylines all_polylines_with_hooks = all_polylines.size() > 1 ? connect_lines_using_hooks(std::move(all_polylines), expolygon, this->spacing, hook_length, hook_length_max) : std::move(all_polylines);
+7 -3
View File
@@ -15,6 +15,7 @@
#include "FillBase.hpp"
#include "FillConcentric.hpp"
#include "FillSpiralInset.hpp"
#include "FillHoneycomb.hpp"
#include "Fill3DHoneycomb.hpp"
#include "FillGyroid.hpp"
@@ -41,6 +42,7 @@ Fill* Fill::new_from_type(const InfillPattern type)
{
switch (type) {
case ipConcentric: return new FillConcentric();
case ipSpiralInset: return new FillSpiralInset();
case ipHoneycomb: return new FillHoneycomb();
case ipLateralHoneycomb: return new FillLateralHoneycomb();
case ip3DHoneycomb: return new Fill3DHoneycomb();
@@ -2465,9 +2467,11 @@ void Fill::connect_base_support(Polylines &&infill_ordered, const std::vector<co
#endif // INFILL_DEBUG_OUTPUT
const std::vector<SupportArcCost> arches = evaluate_support_arches(infill_ordered, graph, spacing, params);
static const double cost_low = line_spacing * 1.3;
static const double cost_high = line_spacing * 2.;
static const double cost_veryhigh = line_spacing * 3.;
// Must not be static: line_spacing varies per call (base vs interface fills differ),
// and a static here would fix these to whichever call ran first, order depending on thread count.
const double cost_low = line_spacing * 1.3;
const double cost_high = line_spacing * 2.;
const double cost_veryhigh = line_spacing * 3.;
{
std::vector<const SupportArcCost*> selected;
+12 -14
View File
@@ -2395,12 +2395,7 @@ static std::vector<MonotonicRegionLink> chain_monotonic_regions(
}
// Probability (unnormalized) of traversing a link between two monotonic regions.
auto path_probability = [
#ifndef __APPLE__
// clang complains when capturing constexpr constants.
pheromone_alpha, pheromone_beta
#endif // __APPLE__
](AntPath &path) {
auto path_probability = [](AntPath &path) {
return pow(path.pheromone, pheromone_alpha) * pow(path.visibility, pheromone_beta);
};
@@ -3095,10 +3090,11 @@ bool FillRectilinear::fill_surface_trapezoidal(
case 0: // Grid / Trapezoidal
{
// Generate a non-crossing trapezoidal pattern to avoid overextrusion at intersections when `multiline > 1`.
// P2--P3
// / \
// P0_P1/ \P4_
//
/*
* P2--P3
* / \
* P0_P1/ \P4_
*/
// P0xP1x=P4xP0x=d1/2
// P2xP3x=d1
// P1yP2y=P2yP3y=d2
@@ -3176,10 +3172,12 @@ bool FillRectilinear::fill_surface_trapezoidal(
case 1: // Triangular
{
// Generate a non-crossing trapezoidal pattern with a base line below.
// P1-P2
// / \
// P0/ \P3_P4
// ----------------
/*
* P1-P2
* / \
* P0/ \P3_P4
* ----------------
*/
// P1xP2x=P3xP4x=d2
// P0yP1y=P2yP3y=h-2d1
//
+426
View File
@@ -0,0 +1,426 @@
#include "../ClipperUtils.hpp"
#include "../ExPolygon.hpp"
#include "../Surface.hpp"
#include "../VariableWidth.hpp"
#include "Arachne/WallToolPaths.hpp"
#include "FillSpiralInset.hpp"
#include <algorithm>
#include <cmath>
#include <functional>
namespace Slic3r {
// Index of the corner the spiral should start at. Every following loop is split at the point nearest
// the end of the one before it, so this choice propagates inwards and decides where the whole spiral
// hands over from ring to ring. A tight corner is the worst place for it: there the next ring
// retreats along the bisector by spacing/sin(angle), so the spiral has to strike out several spacings
// to reach it instead of stepping across to a ring running parallel one spacing away.
//
// A right angle is taken first when the loop has one. It clips cleanly, since the trimming below
// scales with 1/sin(angle) and so is at its shortest and least sensitive there, and it holds its
// shape as the loop is offset inwards, which keeps the handover in the same place ring after ring.
// Failing that the widest corner is the flattest stretch on offer, which is the next best handover.
// A straight point is no corner at all and only turns up as an artefact of the offsetting, so it is
// skipped.
static int find_spiral_start_corner(const Polygon& loop)
{
const size_t n = loop.points.size();
if (n < 3)
return 0;
// cos(85 deg): a corner within five degrees of square counts as a right angle.
static const double right_angle_cos = 0.08716;
// cos(179 deg): anything flatter than this counts as a straight point rather than a corner.
static const double straight_cos = -0.99985;
// Only convex corners qualify. A reflex corner spans the same angle between its two edges but
// bulges the other way, so the next ring in steps away from it along the bisector instead of
// hugging it, and starting there hands over across a long diagonal on every single ring. Loops
// arrive counter-clockwise, in which case a convex corner turns left, but check the winding
// rather than trust it. A closed loop always has at least one convex corner.
const double convex_turn = loop.is_counter_clockwise() ? 1.0 : -1.0;
double best_right_cos = right_angle_cos;
int best_right = -1;
double best_wide_cos = 1.0;
int best_wide = -1;
for (size_t i = 0; i < n; ++i) {
const Point& p_prev = loop.points[(i - 1 + n) % n];
const Point& p = loop.points[i];
const Point& p_next = loop.points[(i + 1) % n];
Vec2d e_in = (p - p_prev).cast<double>();
Vec2d e_out = (p_next - p).cast<double>();
double len1 = e_in.norm();
double len2 = e_out.norm();
if (len1 < 1e-6 || len2 < 1e-6)
continue;
if (convex_turn * (e_in.x() * e_out.y() - e_in.y() * e_out.x()) <= 0.0)
continue;
// Cosine of the angle the two edges span at the corner: 1 at a spike, 0 square, -1 straight.
double cos_val = -e_in.dot(e_out) / (len1 * len2);
if (std::abs(cos_val) < best_right_cos) {
best_right_cos = std::abs(cos_val);
best_right = int(i);
}
if (cos_val > straight_cos && cos_val < best_wide_cos) {
best_wide_cos = cos_val;
best_wide = int(i);
}
}
if (best_right >= 0)
return best_right;
// A loop smooth enough to have no corner at all, a circle say, hands over equally well anywhere.
return best_wide < 0 ? 0 : best_wide;
}
// Length to trim off the end of a loop so that it does not overlap the start of the next one.
// The theoretical gap is distance/sin(alpha), alpha being the angle between the last segment of the
// loop and the first segment of the next one.
static double loop_clip_length(const Polyline& loop_path, const double gap)
{
const Point& p_prev = loop_path.points[loop_path.points.size() - 2];
const Point& p_last = loop_path.points.back();
const Point& p_next = loop_path.points[1];
Vec2d v1 = (p_last - p_prev).cast<double>();
Vec2d v2 = (p_next - p_last).cast<double>();
if (v1.norm() < 1e-6 || v2.norm() < 1e-6)
return gap;
double alpha = std::atan2(std::abs(v1.x() * v2.y() - v1.y() * v2.x()), v1.dot(v2));
// Outside 45deg < alpha < 120deg the 1/sin(alpha) term would clip far too much, so fall back to the plain gap.
return (alpha > M_PI / 4 && alpha < 2 * M_PI / 3) ? gap / std::sin(alpha) : gap;
}
// The chaining below drives two kinds of loop: the plain offset polygons of the classic path, and
// Arachne's variable width walls. These are the only four steps that differ between them. Widths run
// two per segment, so every point added or removed takes a pair with it.
static Polyline open_loop(const Polygon& loop, int start_index) { return loop.split_at_index(start_index); }
static ThickPolyline open_loop(const Arachne::ExtrusionLine& loop, int start_index)
{
ThickPolyline path = Arachne::to_thick_polyline(loop);
// start_at_index() rotates a closed path, and wants it closed with a matching width at both ends.
if (path.points.front() != path.points.back()) {
const coordf_t w_first = path.width.front(), w_last = path.width.back();
path.points.emplace_back(path.points.front());
path.width.emplace_back(w_last);
path.width.emplace_back(w_first);
}
path.start_at_index(start_index);
return path;
}
static void clip_path_end(Polyline& path, double distance) { path.clip_end(distance); }
static void clip_path_end(ThickPolyline& path, double distance)
{
// Polyline::clip_end() knows nothing about the widths, so walk back trimming the two together.
while (distance > 0 && path.points.size() >= 2) {
const Point last = path.points.back();
const coordf_t w_end = path.width.back();
path.points.pop_back();
path.width.pop_back();
const coordf_t w_start = path.width.back();
path.width.pop_back();
const Vec2d v = (path.points.back() - last).cast<double>();
const double len = v.norm();
if (len > distance) {
const double t = distance / len;
path.points.emplace_back((last.cast<double>() + v * t).cast<coord_t>());
path.width.emplace_back(w_start);
path.width.emplace_back(w_start + (w_end - w_start) * (1.0 - t));
return;
}
distance -= len;
}
path.clear();
}
static void append_path(Polyline& dst, Polyline&& src) { dst.append(std::move(src)); }
static void append_path(ThickPolyline& dst, ThickPolyline&& src)
{
if (dst.empty()) {
dst = std::move(src);
return;
}
if (dst.points.back() == src.points.front()) {
// Carrying straight on from the same point, so there is no run across to give a width to.
src.points.erase(src.points.begin());
src.width.erase(src.width.begin(), src.width.begin() + 2);
} else {
// The run across to the next loop tapers between the two ends it joins.
const coordf_t w_from = dst.width.back(), w_to = src.width.front();
dst.width.emplace_back(w_from);
dst.width.emplace_back(w_to);
}
append(dst.points, std::move(src.points));
append(dst.width, std::move(src.width));
}
// The classic loops all carry the same width, so the innermost one of an island can still ring an
// unfilled pin hole, which the spiral plugs by running into the middle. Arachne's walls widen to take
// up whatever is left over, so there is nothing there to plug and the stub would only double back
// over the wall that just filled it.
static bool leaves_a_centre_hole(const Polygon&) { return true; }
static bool leaves_a_centre_hole(const Arachne::ExtrusionLine&) { return false; }
static void append_path_point(Polyline& path, const Point& point) { path.points.emplace_back(point); }
static void append_path_point(ThickPolyline& path, const Point& point)
{
const coordf_t w = path.width.back();
path.points.emplace_back(point);
path.width.emplace_back(w);
path.width.emplace_back(w);
}
// Chain the loops of one surface into as few continuous spirals as its shape allows. The loops arrive
// ordered outside in, depth first, each paired with its outline in loop_outlines; every decision here
// is made on those outlines, so the two kinds of loop take exactly the same route.
template<class LoopType, class PathType>
static std::vector<PathType> generate_spiral_insets(const FillParams& params,
const std::vector<const LoopType*>& loops,
const Polygons& loop_outlines,
const coord_t distance,
const ExPolygon& original_expoly)
{
std::vector<PathType> output;
PathType spiral;
Point current_pos(0, 0);
// Index into loops of the innermost loop appended to the spiral currently being built.
int innermost_loop = -1;
// Whether the spiral can run straight from one point to the other. The run across is extruded,
// not travelled, so it has to be a genuine step over to the ring alongside:
// - up to a ring spacing and a half it cannot leave the material, and needs no check at all,
// which covers all but a few of the loops;
// - beyond that it is tested against the surface, which catches the points that are close in a
// straight line but separated by a hole or a notch;
// - past four spacings it is refused outright. A handover does stretch at a corner, where the
// next ring retreats along the bisector by spacing/sin(angle), but four spacings is already a
// fifteen degree wedge, and down a wedge that tight the run across would trace the bisector,
// which is where the tail is filled from anyway. Anything longer is a traverse across the
// surface that prints over what it crosses. Breaking the spiral leaves the G-code to travel it.
const double free_hop = 1.5 * double(distance);
const double max_hop = 4.0 * double(distance);
auto reachable = [&](const Point& from, const Point& to) {
const double hop = from.distance_to(to);
if (hop > max_hop)
return false;
return hop <= free_hop || original_expoly.contains(Line(from, to));
};
// The centre point plugs the pin hole left in the middle of an island, it is not meant to
// traverse it, so it is only worth adding when the innermost loop has shrunk to about a ring.
const double max_center_stub = 2.0 * double(distance);
// Emit the spiral built so far as one path and start over on a fresh island.
auto flush_spiral = [&]() {
if (spiral.empty())
return;
// Run into the middle of the innermost loop so the island's centre is filled instead of being
// left as a pin hole. Only where there is a hole to fill: the loop has to still enclose open
// space once its own bead is accounted for, or the stub just runs back over that bead. And
// the point has to sit inside the loop and be reachable, or it runs off across the surface.
if (innermost_loop >= 0 && leaves_a_centre_hole(*loops[innermost_loop])) {
const Polygon& innermost = loop_outlines[innermost_loop];
const Point centroid = innermost.centroid();
if (!offset(innermost, -float(0.5 * double(distance))).empty() && centroid != spiral.last_point() &&
spiral.last_point().distance_to(centroid) <= max_center_stub && innermost.contains(centroid) &&
reachable(spiral.last_point(), centroid))
append_path_point(spiral, centroid);
}
output.emplace_back(std::move(spiral));
spiral.clear();
innermost_loop = -1;
current_pos = Point(0, 0);
};
for (size_t i = 0; i < loops.size(); ++i) {
const Polygon& outline = loop_outlines[i];
if (outline.points.empty())
continue;
// The loop is opened into a path with the split point repeated at both ends, so a usable one
// has at least 3 points. Both kinds of loop share the outline's indices, hence its start point.
PathType loop_path = open_loop(*loops[i], spiral.empty() ? find_spiral_start_corner(outline) :
current_pos.nearest_point_index(outline.points));
if (loop_path.size() < 3)
continue;
// Island jumping: the loops are ordered by their nesting, depth first, so the next one
// continues the current spiral exactly when it lies inside the one just laid down. Distance
// cannot stand in for that test: at a sharp corner the next ring retreats along the bisector
// by spacing/sin(angle), which leaves it several spacings away while still being the very
// next ring in, and the spiral would break off at every spike.
const bool same_island = innermost_loop >= 0 && loop_outlines[innermost_loop].contains(loop_path.points.front());
if (!spiral.empty() && (!same_island || !reachable(spiral.last_point(), loop_path.points.front()))) {
flush_spiral();
loop_path = open_loop(*loops[i], find_spiral_start_corner(outline));
if (loop_path.size() < 3)
continue;
}
// Clip the end of the loop to leave room for the run into the next one. The last loop of the
// surface has no successor, so it only gives up half of the gap.
clip_path_end(loop_path, loop_clip_length(loop_path, (i + 1 == loops.size() ? 0.5 : 1.0) * double(distance)));
// Clipping empties the path when the loop is shorter than the clipping length, which happens
// on the degenerate slivers that offsetting leaves behind. Such a loop carries no extrusion.
if (loop_path.size() < 2)
continue;
append_path(spiral, std::move(loop_path));
innermost_loop = int(i);
current_pos = spiral.last_point();
}
flush_spiral();
// An outward fill order runs every spiral from its centre to its outer edge, innermost island first.
if (params.fill_order != SurfaceFillOrder::Inward) {
for (PathType& path : output)
path.reverse();
std::reverse(output.begin(), output.end());
}
return output;
}
void FillSpiralInset::_fill_surface_single(const FillParams& params,
unsigned int thickness_layers,
const std::pair<float, Point>& direction,
ExPolygon expolygon,
Polylines& polylines_out)
{
BoundingBox bounding_box = expolygon.contour.bounding_box();
coord_t min_spacing = scale_(this->spacing);
coord_t distance = coord_t(min_spacing / params.density);
if (params.density > 0.9999f && !params.dont_adjust) {
distance = this->_adjust_solid_spacing(bounding_box.size()(0), distance);
this->spacing = unscale<double>(distance);
}
Polygons loops = to_polygons(expolygon);
ExPolygons last{std::move(expolygon)};
while (!last.empty()) {
last = offset2_ex(last, -(distance + min_spacing / 2), +min_spacing / 2);
append(loops, to_polygons(last));
}
// Orders the loops outside in, depth first, which is the order the chaining below expects.
loops = union_pt_chained_outside_in(loops);
std::vector<const Polygon*> loop_refs;
loop_refs.reserve(loops.size());
for (const Polygon& loop : loops)
loop_refs.emplace_back(&loop);
Polylines spiral_result = generate_spiral_insets<Polygon, Polyline>(params, loop_refs, loops, distance, expolygon);
append(polylines_out, spiral_result);
}
void FillSpiralInset::_fill_surface_single(const FillParams& params,
unsigned int thickness_layers,
const std::pair<float, Point>& direction,
ExPolygon expolygon,
ThickPolylines& thick_polylines_out)
{
assert(params.use_arachne);
assert(this->print_config != nullptr && this->print_object_config != nullptr);
// Only a solid surface is worth the variable width walls; a sparse one falls back to plain loops.
if (params.density <= 0.9999f || params.dont_adjust) {
Polylines polylines;
this->_fill_surface_single(params, thickness_layers, direction, expolygon, polylines);
append(thick_polylines_out, to_thick_polylines(std::move(polylines), scaled<coord_t>(this->spacing)));
return;
}
// no rotation is supported for this infill pattern
Point bbox_size = expolygon.contour.bounding_box().size();
coord_t min_spacing = scaled<coord_t>(this->spacing);
coord_t loops_count = std::max(bbox_size.x(), bbox_size.y()) / min_spacing + 1;
Polygons polygons = offset(expolygon, float(min_spacing) / 2.f);
double min_nozzle_diameter = *std::min_element(print_config->nozzle_diameter.values.begin(), print_config->nozzle_diameter.values.end());
Arachne::WallToolPathsParams input_params;
input_params.min_bead_width = 0.85 * min_nozzle_diameter;
input_params.min_feature_size = 0.25 * min_nozzle_diameter;
input_params.wall_transition_length = 1.0 * min_nozzle_diameter;
input_params.wall_transition_angle = 10;
input_params.wall_transition_filter_deviation = 0.25 * min_nozzle_diameter;
input_params.wall_distribution_count = 1;
Arachne::WallToolPaths wallToolPaths(polygons, min_spacing, min_spacing, loops_count, 0, params.layer_height, input_params);
std::vector<Arachne::VariableWidthLines> walls_by_inset = wallToolPaths.getToolPaths();
// Open walls are the thin features Arachne fits between the closed ones. They cannot join a
// spiral, so they go out as they are; leaving them behind is what would put the gaps back.
std::vector<const Arachne::ExtrusionLine*> walls;
Polygons wall_outlines;
ThickPolylines open_walls;
for (const Arachne::VariableWidthLines& inset : walls_by_inset)
for (const Arachne::ExtrusionLine& wall : inset) {
if (wall.empty())
continue;
if (wall.is_closed) {
walls.emplace_back(&wall);
wall_outlines.emplace_back(wall.toPolygon());
} else {
open_walls.emplace_back(Arachne::to_thick_polyline(wall));
}
}
// Arachne hands the walls back grouped by inset, which is not their nesting: around a hole the
// wall of a given inset lies inside the wall of that same inset around the contour. Nest them by
// containment instead, so the spiral follows one island all the way in before starting the next,
// the same order union_pt_chained_outside_in gives the classic path above.
const size_t wall_count = walls.size();
std::vector<int> nesting_depth(wall_count, 0), parent(wall_count, -1);
std::vector<char> inside(wall_count * wall_count, 0);
for (size_t i = 0; i < wall_count; ++i)
for (size_t j = 0; j < wall_count; ++j)
if (i != j && wall_outlines[j].contains(walls[i]->junctions.front().p)) {
inside[i * wall_count + j] = 1;
++nesting_depth[i];
}
// The innermost of the walls containing this one, which is the deepest of them, is its parent.
for (size_t i = 0; i < wall_count; ++i)
for (size_t j = 0; j < wall_count; ++j)
if (inside[i * wall_count + j] && (parent[i] < 0 || nesting_depth[parent[i]] < nesting_depth[j]))
parent[i] = int(j);
std::vector<const Arachne::ExtrusionLine*> ordered;
Polygons outlines;
ordered.reserve(wall_count);
outlines.reserve(wall_count);
std::function<void(int)> descend = [&](int idx) {
ordered.emplace_back(walls[idx]);
outlines.emplace_back(wall_outlines[idx]);
for (size_t k = 0; k < wall_count; ++k)
if (parent[k] == idx)
descend(int(k));
};
for (size_t i = 0; i < wall_count; ++i)
if (parent[i] < 0)
descend(int(i));
ThickPolylines spiral_result =
generate_spiral_insets<Arachne::ExtrusionLine, ThickPolyline>(params, ordered, outlines, min_spacing, expolygon);
append(thick_polylines_out, std::move(spiral_result));
append(thick_polylines_out, std::move(open_walls));
}
} // namespace Slic3r
+37
View File
@@ -0,0 +1,37 @@
#ifndef slic3r_FillSpiralInset_hpp_
#define slic3r_FillSpiralInset_hpp_
#include "FillBase.hpp"
namespace Slic3r {
class FillSpiralInset : public Fill
{
public:
~FillSpiralInset() override = default;
bool is_self_crossing() override { return false; }
protected:
Fill* clone() const override { return new FillSpiralInset(*this); };
void _fill_surface_single(
const FillParams &params,
unsigned int thickness_layers,
const std::pair<float, Point> &direction,
ExPolygon expolygon,
Polylines &polylines_out) override;
// Orca: solid surfaces are filled with Arachne's variable width walls, which widen to take up
// whatever the fixed width loops above would have left over as gaps.
void _fill_surface_single(
const FillParams &params,
unsigned int thickness_layers,
const std::pair<float, Point> &direction,
ExPolygon expolygon,
ThickPolylines &thick_polylines_out) override;
bool no_sort() const override { return true; }
};
} // namespace Slic3r
#endif // slic3r_FillSpiralInset_hpp_
+2 -2
View File
@@ -40,8 +40,8 @@ static float DeltaHS_BBS(float h1, float s1, float v1, float h2, float s2, float
return std::min(1.2f, dxy);
}
FlushVolCalculator::FlushVolCalculator(int min, int max, int flush_dataset, float multiplier)
:m_min_flush_vol(min), m_max_flush_vol(max), m_multiplier(multiplier), m_flush_dataset(flush_dataset)
FlushVolCalculator::FlushVolCalculator(int min, int max, int flush_dataset)
:m_min_flush_vol(min), m_max_flush_vol(max), m_flush_dataset(flush_dataset)
{
}
+1 -2
View File
@@ -15,7 +15,7 @@ extern const int g_max_flush_volume;
class FlushVolCalculator
{
public:
FlushVolCalculator(int min, int max, int flush_dataset, float multiplier = 1.0f);
FlushVolCalculator(int min, int max, int flush_dataset);
~FlushVolCalculator()
{
}
@@ -32,7 +32,6 @@ public:
private:
int m_min_flush_vol;
int m_max_flush_vol;
float m_multiplier;
int m_flush_dataset;
};
+3 -2
View File
@@ -10,6 +10,7 @@
#include <string>
#include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp>
#ifdef _WIN32
#define DIR_SEPARATOR '\\'
@@ -55,9 +56,9 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
boost::filesystem::path temp_mtl_path(mtl_file);
mtl_path = temp_mtl_path;
}
auto _mtl_path = mtl_name_is_path ? mtl_abs_path.string().c_str() : mtl_path.string().c_str();
const std::string _mtl_path = (mtl_name_is_path ? mtl_abs_path : mtl_path).string();
if (boost::filesystem::exists(mtl_name_is_path ? mtl_abs_path : mtl_path)) {
if (!ObjParser::mtlparse(_mtl_path, mtl_data)) {
if (!ObjParser::mtlparse(_mtl_path.c_str(), mtl_data)) {
BOOST_LOG_TRIVIAL(error) << "load_obj:load_mtl: failed to parse " << _mtl_path;
message = _L("load mtl in obj: failed to parse");
return false;
+22 -12
View File
@@ -111,14 +111,19 @@ bool StepPreProcessor::isUtf8File(const char* path)
bool StepPreProcessor::isUtf8(const std::string str)
{
size_t num = 0;
int i = 0;
size_t i = 0;
while (i < str.length()) {
if ((str[i] & 0x80) == 0x00) {
const unsigned char lead = static_cast<unsigned char>(str[i]);
if ((lead & 0x80) == 0x00) {
i++;
} else if ((num = preNum(str[i])) > 2) {
// preNum() counts the leading 1 bits, and a multi-byte sequence is 2 to 4
// bytes long, so anything outside that range is not a lead byte.
} else if ((num = preNum(lead)) >= 2 && num <= 4) {
if (i + num > str.length())
return false;
i++;
for (int j = 0; j < num - 1; j++) {
if ((str[i] & 0xc0) != 0x80)
for (size_t j = 0; j < num - 1; j++) {
if ((static_cast<unsigned char>(str[i]) & 0xc0) != 0x80)
return false;
i++;
}
@@ -132,15 +137,20 @@ bool StepPreProcessor::isUtf8(const std::string str)
bool StepPreProcessor::isGBK(const std::string str) {
size_t i = 0;
while (i < str.length()) {
if (str[i] <= 0x7f) {
// char is signed here, so every byte compares <= 0x7f unless widened first.
const unsigned char lead = static_cast<unsigned char>(str[i]);
if (lead <= 0x7f) {
i++;
continue;
} else {
if (str[i] >= 0x81 &&
str[i] <= 0xfe &&
str[i + 1] >= 0x40 &&
str[i + 1] <= 0xfe &&
str[i + 1] != 0xf7) {
if (i + 1 >= str.length())
return false;
const unsigned char trail = static_cast<unsigned char>(str[i + 1]);
if (lead >= 0x81 &&
lead <= 0xfe &&
trail >= 0x40 &&
trail <= 0xfe &&
trail != 0xf7) {
i += 2;
continue;
}
@@ -586,7 +596,7 @@ Step::Step_Status Step::mesh(Model* model,
for (Standard_Integer aNodeIter = 1; aNodeIter <= aTriangulation->NbNodes(); ++aNodeIter) {
gp_Pnt aPnt = aTriangulation->Node(aNodeIter);
aPnt.Transform(aTrsf);
points.emplace_back(std::move(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z())));
points.emplace_back(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z()));
}
// BBS: copy triangles
const TopAbs_Orientation anOrientation = anExpSF.Current().Orientation();
+216 -54
View File
@@ -102,45 +102,6 @@ struct ZipUnicodePathExtraField
}
};
// Validate that a relative file path does not escape the root directory via path traversal.
static bool is_path_within_root(const std::string& file_path, const boost::filesystem::path& root)
{
if (file_path.empty())
return false;
boost::filesystem::path p(file_path);
if (p.is_absolute())
return false;
// Reject any path component that is ".."
for (const auto& component : p) {
if (component == "..")
return false;
}
// Resolve the full path and verify it starts with the canonical root (also catches symlink escapes)
try {
boost::filesystem::path full_path = root / p;
boost::filesystem::path canonical_root = boost::filesystem::weakly_canonical(root);
boost::filesystem::path canonical_full = boost::filesystem::weakly_canonical(full_path);
auto root_str = canonical_root.string();
auto full_str = canonical_full.string();
if (full_str.length() < root_str.length())
return false;
if (full_str.compare(0, root_str.length(), root_str) != 0)
return false;
// Ensure it's a proper prefix (not just a substring of a longer directory name)
if (full_str.length() > root_str.length() &&
full_str[root_str.length()] != boost::filesystem::path::preferred_separator)
return false;
} catch (const boost::filesystem::filesystem_error&) {
return false;
}
return true;
}
// VERSION NUMBERS
// 0 : .3mf, files saved by older slic3r or other applications. No version definition in them.
// 1 : Introduction of 3mf versioning. No other change in data saved into 3mf files.
@@ -214,6 +175,10 @@ const std::string BBS_MODEL_CONFIG_RELS_FILE = "Metadata/_rels/model_settings.co
const std::string SLICE_INFO_CONFIG_FILE = "Metadata/slice_info.config";
const std::string FILAMENT_SEQUENCE_FILE = "Metadata/filament_sequence.json";
const std::string BBS_LAYER_HEIGHTS_PROFILE_FILE = "Metadata/layer_heights_profile.txt";
const std::string ORCA_CAD_RECIPE_FILE = "Metadata/orca_cad.bin";
// Read-only: the recipe entry's pre-rename name. A reader that knows only the new one drops the
// feature tree of every project written before the move, without a word. Never written.
const std::string LEGACY_CAD_RECIPE_FILE = "Metadata/SnapOrca_cad.bin";
const std::string LAYER_CONFIG_RANGES_FILE = "Metadata/layer_config_ranges.xml";
const std::string BRIM_EAR_POINTS_FILE = "Metadata/brim_ear_points.txt";
/*const std::string SLA_SUPPORT_POINTS_FILE = "Metadata/Slic3r_PE_sla_support_points.txt";
@@ -681,6 +646,11 @@ bool bbs_is_valid_object_type(const std::string& type)
namespace Slic3r {
bool is_published_3mf_flag(const std::string &value)
{
return value == "1";
}
void PlateData::parse_filament_info(GCodeProcessorResult *result)
{
if (!result) return;
@@ -1217,6 +1187,20 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
// add backup & restore logic
bool _load_model_from_file(std::string filename, Model& model, PlateDataPtrs& plate_data_list, std::vector<Preset*>& project_presets, DynamicPrintConfig& config, ConfigSubstitutionContext& config_substitutions, Import3mfProgressFn proFn = nullptr,
BBLProject* project = nullptr, int plate_id = 0);
// A minimal published 3MF carries no slicer tags (any tag would make old receivers show
// a baked-in, wrong "old version" popup on their geometry-only fallback), so it
// classifies as From_Other. It is still a fully structured OrcaSlicer file though:
// identified by its own metadata, it keeps BBS-grade geometry handling (no instance
// splitting, no transform baking, no renaming) in this build. Old receivers without the
// publish feature don't know the metadata and take their third-party geometry path.
// Reads the parse-time metadata: the model XML carries it before its resources, while
// m_model->model_info is only filled in after the whole XML has been parsed.
bool _is_published_3mf() const {
const auto it = this->model_info.metadata_items.find(ORCA_PUBLISHED_TAG);
return it != this->model_info.metadata_items.end() && is_published_3mf_flag(it->second);
}
bool _is_svg_shape_file(const std::string &filename) const;
bool _extract_from_archive(mz_zip_archive& archive, std::string const & path, std::function<bool (mz_zip_archive& archive, const mz_zip_archive_file_stat& stat)>, bool restore = false);
bool _extract_xml_from_archive(mz_zip_archive& archive, std::string const & path, XML_StartElementHandler start_handler, XML_EndElementHandler end_handler);
@@ -1970,6 +1954,15 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
// extract slic3r print config file
_extract_project_config_from_archive(archive, stat, config, config_substitutions, model);
}
else if (boost::algorithm::iequals(name, ORCA_CAD_RECIPE_FILE)
|| boost::algorithm::iequals(name, LEGACY_CAD_RECIPE_FILE)) {
// Restore the editable CAD recipe (optional; absent in non-CAD projects).
if (stat.m_uncomp_size > 0) {
std::string buf((size_t)stat.m_uncomp_size, '\0');
if (mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, buf.data(), buf.size(), 0))
model.cad_recipe = std::move(buf);
}
}
else if (boost::algorithm::iequals(name, CUT_INFORMATION_FILE)) {
// extract object cut info
_extract_cut_information_from_archive(archive, stat, config_substitutions);
@@ -2041,7 +2034,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
lock.close();
if (!m_is_bbl_3mf) {
if (!m_is_bbl_3mf && !_is_published_3mf()) {
// if the 3mf was not produced by OrcaSlicer and there is more than one instance,
// split the object in as many objects as instances
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", found 3mf from other vendor, split as instance");
@@ -3605,7 +3598,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
m_index_paths.insert({ object.first.second, object.first.first});
}
if (!m_is_bbl_3mf) {
if (!m_is_bbl_3mf && !_is_published_3mf()) {
// if the 3mf was not produced by OrcaSlicer and there is only one object,
// set the object name to match the filename
if (m_model->objects.size() == 1)
@@ -5328,7 +5321,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
TriangleMesh triangle_mesh(std::move(its), volume_data.mesh_stats);
if (!m_is_bbl_3mf) {
if (!m_is_bbl_3mf && !_is_published_3mf()) {
// if the 3mf was not produced by OrcaSlicer and there is only one instance,
// bake the transformation into the geometry to allow the reload from disk command
// to work properly
@@ -5974,6 +5967,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
bool m_save_gcode { false }; // whether to save gcode for normal save
bool m_skip_model { false }; // skip model when exporting .gcode.3mf
bool m_skip_auxiliary { false }; // skip normal axuiliary files
bool m_minimal_published { false }; // published 3MF: omit the project config, the embedded preset files and the slicer tags
bool m_use_loaded_id { false }; // whether to use loaded id for identify_id
bool m_share_mesh { false }; // whether to share mesh between objects
std::string m_thumbnail_middle = PRINTER_THUMBNAIL_MIDDLE_FILE;
@@ -6027,6 +6021,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
bool _add_mesh_to_object_stream(std::function<bool(std::string &, bool)> const &flush, ObjectData const &object_data) const;
bool _add_build_to_model_stream(std::stringstream& stream, const BuildItemsList& build_items) const;
bool _add_layer_height_profile_file_to_archive(mz_zip_archive& archive, Model& model);
bool _add_cad_recipe_file_to_archive(mz_zip_archive& archive, Model& model);
bool _add_layer_config_ranges_file_to_archive(mz_zip_archive& archive, Model& model);
bool _add_brim_ear_points_file_to_archive(mz_zip_archive& archive, Model& model);
bool _add_sla_support_points_file_to_archive(mz_zip_archive& archive, Model& model);
@@ -6073,6 +6068,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
m_skip_auxiliary = store_params.strategy & SaveStrategy::SkipAuxiliary;
m_share_mesh = store_params.strategy & SaveStrategy::ShareMesh;
m_from_backup_save = store_params.strategy & SaveStrategy::Backup;
m_minimal_published = store_params.strategy & SaveStrategy::MinimalPublished;
m_use_loaded_id = store_params.strategy & SaveStrategy::UseLoadedId;
@@ -6422,6 +6418,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return false;
}
if (!_add_cad_recipe_file_to_archive(archive, model)) {
close_zip_writer(&archive);
return false;
}
// BBS progress point
/*BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format("export 3mf EXPORT_STAGE_ADD_LAYER_RANGE\n");
if (proFn) {
@@ -6482,7 +6483,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
// Adds slic3r print config file ("Metadata/Slic3r_PE.config").
// This file contains the content of FullPrintConfig / SLAFullPrintConfig.
if (config != nullptr) {
// Omitted for minimal published 3MF: OrcaSlicer versions without the publish feature
// then fall back to importing the geometry only, and new versions read the published
// payload from the model metadata instead.
if (config != nullptr && !m_minimal_published) {
// BBS: change to json format
// if (!_add_print_config_file_to_archive(archive, *config)) {
if (!_add_project_config_file_to_archive(archive, *config, model)) { return false; }
@@ -6495,8 +6499,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (cb_cancel) return false;
}
// BBS: add project config
if (project_presets.size() > 0) {
// BBS: add project config (omitted for minimal published 3MF)
if (!m_minimal_published && project_presets.size() > 0) {
// BBS: add project embedded preset files
_add_project_embedded_presets_to_archive(archive, model, project_presets);
@@ -6968,10 +6972,31 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
// Orca: PRIVACY: do not store creation & modification date in 3mf
metadata_item_map[BBL_CREATION_DATE_TAG] = "";
metadata_item_map[BBL_MODIFICATION_TAG] = "";
// Orca: Write the BambuStudio compatibility version string using SLIC3R_VERSION
metadata_item_map[BBL_APPLICATION_TAG] = (boost::format("%1%-%2%") % "BambuStudio" % SLIC3R_VERSION).str();
// Orca: Write the BambuStudio compatibility version string using SLIC3R_VERSION.
// A minimal published 3MF writes no slicer tags at all: any tag would route old
// receivers onto a geometry-only fallback whose baked-in popup misreports the
// file ("old OrcaSlicer version" / "BambuStudio"), while tag-less files classify
// as From_Other and import the geometry silently.
if (m_minimal_published) {
// metadata_item_map is seeded from the input file's metadata_items above, so a
// project opened from a regular Orca/BBS 3MF still carries the slicer-identifying
// tags it came with. Erase every one of them - not just the two most common -
// so a published 3MF is fully tag-less: old receivers classify it as From_Other
// and import the geometry silently instead of showing a baked-in "old version"
// popup, and no version marker survives to seed a later re-save.
metadata_item_map.erase(BBL_APPLICATION_TAG);
metadata_item_map.erase(ORCASLICER_TAG);
metadata_item_map.erase(BBS_3MF_VERSION);
metadata_item_map.erase(BBS_3MF_VERSION1);
} else {
metadata_item_map[BBL_APPLICATION_TAG] = (boost::format("%1%-%2%") % "BambuStudio" % SLIC3R_VERSION).str();
}
}
metadata_item_map[BBS_3MF_VERSION] = std::to_string(VERSION_BBS_3MF);
// The Bambu 3MF version marker is part of the slicer identity: omit it for a minimal
// published file along with the tags erased above (skipping the overwrite alone would
// leave the value the source file seeded into metadata_item_map).
if (!m_minimal_published)
metadata_item_map[BBS_3MF_VERSION] = std::to_string(VERSION_BBS_3MF);
if (!model.mk_name.empty()) {
metadata_item_map[BBL_MAKERLAB_TAG] = xml_escape(model.mk_name);
@@ -6994,7 +7019,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
BOOST_LOG_TRIVIAL(info) << "bbs_3mf: save key= " << item.first << ", value = " << item.second;
stream << " <" << METADATA_TAG << " name=\"" << item.first << "\">"
<< xml_escape(item.second) << "</" << METADATA_TAG << ">\n";
if (item.first == BBL_APPLICATION_TAG) {
if (item.first == BBL_APPLICATION_TAG && !m_minimal_published) {
// The OrcaSlicer tag is only written for files that carry the Application
// tag, which a minimal published 3MF erases (see the map assignment above):
// the explicit !m_minimal_published guard keeps the tag-less guarantee from
// depending on that erase happening to run first.
stream << " <" << METADATA_TAG << " name=\"" << ORCASLICER_TAG << "\">"
<< xml_escape(SoftFever_VERSION) << "</" << METADATA_TAG << ">\n";
}
@@ -7648,6 +7677,19 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return true;
}
bool _BBS_3MF_Exporter::_add_cad_recipe_file_to_archive(mz_zip_archive& archive, Model& model)
{
if (model.cad_recipe.empty())
return true;
if (!mz_zip_writer_add_mem(&archive, ORCA_CAD_RECIPE_FILE.c_str(),
(const void*)model.cad_recipe.data(), model.cad_recipe.length(),
MZ_DEFAULT_COMPRESSION)) {
add_error("Unable to add CAD recipe file to archive");
return false;
}
return true;
}
bool _BBS_3MF_Exporter::_add_layer_config_ranges_file_to_archive(mz_zip_archive& archive, Model& model)
{
std::string out = "";
@@ -8834,7 +8876,7 @@ public:
auto model = object.get_model();
auto o = m_temp_model.add_object(object);
int backup_id = model->get_object_backup_id(object);
push_task({ AddObject, (size_t) backup_id, object.get_model()->get_backup_path(), o, 1 });
push_task({ AddObject, (size_t) backup_id, object.get_model()->get_backup_path(), o, { 1 } });
}
void remove_object_mesh(ModelObject& object) {
@@ -8844,7 +8886,7 @@ public:
void backup_soon() {
boost::lock_guard lock(m_mutex);
m_other_changes_backup = true;
m_tasks.push_back({ Backup, 0, std::string(), nullptr, ++m_task_seq });
m_tasks.push_back({ Backup, 0, std::string(), nullptr, { ++m_task_seq } });
m_cond.notify_all();
}
@@ -8862,7 +8904,7 @@ public:
m_ui_tasks.clear();
m_tasks.clear();
}
m_tasks.push_back({ RemoveBackup, model.id().id, model.get_backup_path(), nullptr, removeAll });
m_tasks.push_back({ RemoveBackup, model.id().id, model.get_backup_path(), nullptr, { removeAll } });
++m_task_seq;
if (model.is_need_backup()) {
m_other_changes = false;
@@ -8963,7 +9005,7 @@ private:
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " inital and interval = " << m_interval;
m_next_backup = boost::get_system_time() + boost::posix_time::seconds(m_interval);
boost::unique_lock lock(m_mutex);
m_thread = std::move(boost::thread(boost::ref(*this)));
m_thread = boost::thread(boost::ref(*this));
}
~_BBS_Backup_Manager() {
@@ -9077,7 +9119,7 @@ public:
else
m_cond.wait(lock);
if (m_interval > 0 && boost::get_system_time() > m_next_backup) {
m_tasks.push_back({ Backup, 0, std::string(), nullptr, ++m_task_seq });
m_tasks.push_back({ Backup, 0, std::string(), nullptr, { ++m_task_seq } });
m_next_backup += boost::posix_time::seconds(m_interval);
// Maybe wakeup from power sleep
if (m_next_backup < boost::get_system_time())
@@ -9173,6 +9215,126 @@ std::string bbs_3mf_get_thumbnail(const char *path)
return data;
}
namespace {
// Parses just the model-file <metadata> elements, mirroring the importer's
// _handle_start_metadata/_handle_end_metadata (attribute-order independent, entity-unescaped,
// whitespace tolerant). Stops the parser as soon as the published flag node is read so the
// geometry/resources that follow are skipped, which keeps the per-file cost small.
struct PublishedXmlProbe
{
XML_Parser parser{nullptr};
bool in_metadata{false};
bool found{false};
bool published{false};
std::string curr_name;
std::string curr_value;
static std::string attribute(const char** attrs, const char* key)
{
if (attrs == nullptr)
return std::string();
// expat hands the attrs as a NULL-terminated {name, value, ...} array.
for (unsigned int a = 0; attrs[a] != nullptr; a += 2)
if (::strcmp(attrs[a], key) == 0 && attrs[a + 1] != nullptr)
return attrs[a + 1];
return std::string();
}
static void XMLCALL start(void* user_data, const char* name, const char** attrs)
{
auto* self = static_cast<PublishedXmlProbe*>(user_data);
if (::strcmp(name, METADATA_TAG) == 0) {
self->in_metadata = true;
self->curr_name = attribute(attrs, NAME_ATTR);
self->curr_value.clear();
} else {
self->in_metadata = false;
}
}
static void XMLCALL characters(void* user_data, const XML_Char* s, int len)
{
auto* self = static_cast<PublishedXmlProbe*>(user_data);
if (self->in_metadata)
self->curr_value.append(s, len);
}
static void XMLCALL end(void* user_data, const char* name)
{
auto* self = static_cast<PublishedXmlProbe*>(user_data);
if (!self->in_metadata || ::strcmp(name, METADATA_TAG) != 0)
return;
self->in_metadata = false;
if (self->curr_name == ORCA_PUBLISHED_TAG) {
self->published = is_published_3mf_flag(xml_unescape(self->curr_value));
self->found = true;
if (self->parser != nullptr)
XML_StopParser(self->parser, false);
}
}
};
} // namespace
bool bbs_3mf_is_published(const std::string &path)
{
mz_zip_archive archive;
mz_zip_zero_struct(&archive);
struct close_lock
{
mz_zip_archive *archive;
void close()
{
if (archive) {
close_zip_reader(archive);
archive = nullptr;
}
}
~close_lock() { close(); }
} lock{&archive};
if (!open_zip_reader(&archive, path))
return false;
// Read just the model XML (the metadata node sits before the resources, so the probe below
// stops early) rather than by a raw substring match; no geometry parsing.
int index = mz_zip_reader_locate_file(&archive, MODEL_FILE.c_str(), nullptr, 0);
if (index < 0)
return false;
mz_zip_archive_file_stat stat;
if (!mz_zip_reader_file_stat(&archive, index, &stat))
return false;
std::string xml(stat.m_uncomp_size, '\0');
if (!mz_zip_reader_extract_to_mem(&archive, index, xml.data(), xml.size(), 0))
return false;
XML_Parser parser = XML_ParserCreate(nullptr);
if (parser == nullptr)
return false;
PublishedXmlProbe probe;
probe.parser = parser;
XML_SetUserData(parser, &probe);
XML_SetElementHandler(parser, PublishedXmlProbe::start, PublishedXmlProbe::end);
XML_SetCharacterDataHandler(parser, PublishedXmlProbe::characters);
// Never resolve external entities from a file we are only probing.
XML_SetExternalEntityRefHandler(parser, nullptr);
XML_SetEntityDeclHandler(parser, nullptr);
const XML_Status status = XML_Parse(parser, xml.data(), static_cast<int>(xml.size()), 1);
// XML_StopParser(parser, false) from the end handler makes XML_Parse return
// XML_STATUS_ERROR with XML_ERROR_ABORTED - treat that as success (we stopped on the flag).
const bool parse_ok = (status == XML_STATUS_OK) ||
(XML_GetErrorCode(parser) == XML_ERROR_ABORTED && probe.found);
XML_ParserFree(parser);
if (!parse_ok)
return false;
return probe.published;
}
bool load_gcode_3mf_from_stream(std::istream &data, DynamicPrintConfig *config, Model *model, PlateDataPtrs *plate_data_list, Semver *file_version)
{
CNumericLocalesSetter locales_setter;
+19
View File
@@ -159,12 +159,28 @@ enum class SaveStrategy
SkipAuxiliary = 1 << 9,
UseLoadedId = 1 << 10,
ShareMesh = 1 << 11,
// Keep this separate from SplitModel, which uses the 0x1000 bit as part of its
// production-extension value.
MinimalPublished = 1 << 13,
SplitModel = 0x1000 | ProductionExt,
Encrypted = SecureContentExt | SplitModel,
Backup = 0x10000 | WithGcode | Silence | SkipStatic | SplitModel,
};
// Model metadata keys of a "published" 3MF (see MinimalPublished): the flag marks a minimal,
// tag-less publish export, the others carry the author-selected settings payload. Namespaced
// with the "orca_published" prefix because metadata_items round-trips verbatim through other
// slicers, where a bare "published" key could collide.
inline constexpr const char *ORCA_PUBLISHED_TAG = "orca_published";
inline constexpr const char *ORCA_PUBLISHED_KEYS_TAG = "orca_published_keys";
inline constexpr const char *ORCA_PUBLISHED_MATERIAL_TAG = "orca_published_material_keys";
inline constexpr const char *ORCA_PUBLISHED_CONFIG_TAG = "orca_published_config";
// Published files are produced with "1". The importer and the GUI loader both gate on this
// exact value, so a "0"/"false"/unknown value is rejected consistently.
bool is_published_3mf_flag(const std::string &value);
inline SaveStrategy operator | (SaveStrategy lhs, SaveStrategy rhs)
{
using T = std::underlying_type_t <SaveStrategy>;
@@ -277,6 +293,9 @@ extern bool load_bbs_3mf(const char* path, DynamicPrintConfig* config, ConfigSub
extern std::string bbs_3mf_get_thumbnail(const char * path);
// Lightweight check: does this 3mf carry the "published" (orca_published == "1") marker? Only reads the 3D/3dmodel.model metadata node
extern bool bbs_3mf_is_published(const std::string &path);
extern bool load_gcode_3mf_from_stream(std::istream & data, DynamicPrintConfig* config, Model* model, PlateDataPtrs* plate_data_list,
Semver* file_version);
+1 -1
View File
@@ -352,7 +352,7 @@ bool load_svg(const char *path, Model *model, std::string &message)
for (Standard_Integer aNodeIter = 1; aNodeIter <= aTriangulation->NbNodes(); ++aNodeIter) {
gp_Pnt aPnt = aTriangulation->Node(aNodeIter);
aPnt.Transform(aTrsf);
points.emplace_back(std::move(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z())));
points.emplace_back(Vec3f(aPnt.X(), aPnt.Y(), aPnt.Z()));
}
// BBS: copy triangles
const TopAbs_Orientation anOrientation = anExpSF.Current().Orientation();
+199 -108
View File
@@ -1,5 +1,6 @@
#include "BoundingBox.hpp"
#include "Config.hpp"
#include "GCode/WipePathHelpers.hpp"
#include "GCodeWriter.hpp"
#include "Polygon.hpp"
#include "PrintConfig.hpp"
@@ -439,7 +440,6 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
auto& writer = gcodegen.writer();
auto& config = gcodegen.config();
auto extruder = writer.filament();
auto extruder_id = extruder->extruder_id();
auto last_pos = gcodegen.last_pos();
// Declare & initialize retraction lengths
@@ -476,13 +476,13 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
wipe_speed = std::max(wipe_speed, 10.0);
// Process wipe path & calculate wipe path length
double wipe_dist = scale_(config.wipe_distance.get_at(extruder_id));
double wipe_dist = scale_(config.wipe_distance.get_at(extruder->config_index()));
Polyline wipe_path = {last_pos};
wipe_path.append(this->path.points.begin() + 1, this->path.points.end());
double wipe_path_length = std::min(wipe_path.length(), wipe_dist);
// Calculate the maximum retraction amount during wipe
retraction_length_during_wipe = config.retraction_speed.get_at(extruder_id) *
retraction_length_during_wipe = config.retraction_speed.get_at(extruder->config_index()) *
unscale_(wipe_path_length) / wipe_speed;
// If the maximum retraction amount during wipe is too small,
@@ -565,6 +565,16 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
return default_value;
}
// Orca: rebuild the stored wipe path while preserving Polyline's boundary deduplication.
void Wipe::update_path(const ExtrusionPaths &paths, bool reverse)
{
reset_path();
for (const ExtrusionPath& extrusion_path : paths)
path.append(extrusion_path.polyline.to_polyline());
if (reverse)
path.reverse();
}
std::string Wipe::wipe(GCode& gcodegen,double length, bool toolchange, bool is_last)
{
std::string gcode;
@@ -617,14 +627,11 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
if (gcodegen.enable_cooling_markers() && !is_last)
cooling_mark = /*gcodegen.config().role_based_wipe_speed ? ";_EXTERNAL_PERIMETER" : */";_WIPE";
// Orca: set speed once because wipe_speed is constant for all segments.
gcode += gcodegen.writer().set_speed(_wipe_speed * 60, "", cooling_mark);
for (const Line& line : wipe_path.lines()) {
double segment_length = line.length();
double dE = length * (segment_length / wipe_dist);
//BBS: fix this FIXME
//FIXME one shall not generate the unnecessary G1 Fxxx commands, here wipe_speed is a constant inside this cycle.
// Is it here for the cooling markers? Or should it be outside of the cycle?
//gcode += gcodegen.writer().set_speed(wipe_speed * 60, "", gcodegen.enable_cooling_markers() ? ";_WIPE" : "");
gcode += gcodegen.writer().extrude_to_xy(
gcodegen.point_to_gcode(line.b),
-dE,
@@ -1022,11 +1029,21 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
double current_z = gcodegen.writer().get_position().z();
if (z == -1.) // in case no specific z was provided, print at current_z pos
z = current_z;
if (!is_approx(z, current_z)) {
// Orca: wipe_tower_no_sparse_layers crash guard. With sparse layers skipped the tower is
// compacted far below the object, so descending to it is only safe once the nozzle is parked
// over the tower - which is what the is_finish_first travel above does. Otherwise the nozzle
// is still over the model and this descent would drive it into the print, so defer it to the
// re-descents below, which run after the travel to the tower.
const bool defer_compacted_descend = m_sparse_layers_skipped
&& !tcr.priming && !tcr.is_finish_first && (current_z - z) > EPSILON;
if (!is_approx(z, current_z) && !defer_compacted_descend) {
gcode += gcodegen.writer().retract();
gcode += gcodegen.writer().travel_to_z(z, "Travel down to the last wipe tower layer.");
gcode += gcodegen.writer().unretract();
}
// Tower compacted below the object, so any extrusion emitted without an explicit z has to be
// pulled back down to it first.
const bool compacted_below_object = m_sparse_layers_skipped && z >= 0. && (tcr.print_z - z) > EPSILON;
// Process the end filament gcode.
bool add_change_filament_624 = false;
@@ -1079,11 +1096,23 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
std::string nozzle_change_gcode_trans;
if (is_nozzle_change) {
// move to start_pos before nozzle change
// Orca: travel_to() lifts to the object layer height to clear the print. That lift is
// needed when arriving from the model, but is a wasted full-height Z bounce when the
// nozzle already sits on the compacted tower, so travel at the compacted z instead.
const bool compact_intower_nc_travel = compacted_below_object
&& (tcr.print_z - gcodegen.writer().get_position().z()) > EPSILON;
std::string start_pos_str;
start_pos_str = gcodegen.travel_to(wipe_tower_point_to_object_point(gcodegen, transform_wt_pt(tcr.nozzle_change_result.start_pos) + plate_origin_2d), erMixed,
"Move to nozzle change start pos");
"Move to nozzle change start pos", compact_intower_nc_travel ? z : DBL_MAX);
check_add_eol(start_pos_str);
nozzle_change_gcode_trans += start_pos_str;
// The nozzle-change wipe below carries no explicit z, so it would extrude at the object
// layer height and float above the compacted tower. Descend unless the travel stayed down.
if (!compact_intower_nc_travel && compacted_below_object) {
std::string nc_z_descend = gcodegen.writer().travel_to_z(z, "Descend to compacted wipe tower z (no sparse layers)");
check_add_eol(nc_z_descend);
nozzle_change_gcode_trans += nc_z_descend;
}
nozzle_change_gcode_trans += gcodegen.unretract();
nozzle_change_gcode_trans += transform_gcode(tcr.nozzle_change_result.gcode, tcr.nozzle_change_result.start_pos, wipe_tower_offset, wipe_tower_rotation);
gcodegen.set_last_pos(wipe_tower_point_to_object_point(gcodegen, transform_wt_pt(tcr.nozzle_change_result.end_pos) + plate_origin_2d));
@@ -1422,6 +1451,15 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
start_filament_gcode_str = start_filament_gcode_str + wipe_next_start_point_str + toolchange_unretract_str;
// Orca: the custom change_filament_gcode lifts to the object layer height and the unretract
// de-hops back to it, so every tower extrusion emitted after it (purge moves, and the wall
// when it prints after the toolchange) would float above the compacted tower. Descend first.
if (compacted_below_object) {
std::string z_descend = gcodegen.writer().travel_to_z(z, "Descend to compacted wipe tower z (no sparse layers)");
check_add_eol(z_descend);
start_filament_gcode_str += z_descend;
}
// Insert the end filament, toolchange, and start filament gcode into the generated gcode.
DynamicConfig config;
config.set_key_value("filament_end_gcode", new ConfigOptionString(end_filament_gcode_str));
@@ -1909,11 +1947,9 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
// resulting in a wipe tower with sparse layers.
double wipe_tower_z = -1;
bool ignore_sparse = false;
if (gcodegen.config().wipe_tower_no_sparse_layers.value) {
if (m_sparse_layers_skipped) {
wipe_tower_z = m_last_wipe_tower_print_z;
ignore_sparse = (m_tool_changes[m_layer_idx].size() == 1 &&
m_tool_changes[m_layer_idx].front().initial_tool == m_tool_changes[m_layer_idx].front().new_tool &&
m_layer_idx != 0);
ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]) && m_layer_idx != 0;
if (m_tool_change_idx == 0 && !ignore_sparse)
wipe_tower_z = m_last_wipe_tower_print_z + m_tool_changes[m_layer_idx].front().layer_height;
}
@@ -1929,12 +1965,9 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
// resulting in a wipe tower with sparse layers.
double wipe_tower_z = -1;
bool ignore_sparse = false;
if (gcodegen.config().wipe_tower_no_sparse_layers.value) {
wipe_tower_z = m_last_wipe_tower_print_z;
ignore_sparse = (m_tool_changes[m_layer_idx].size() == 1 &&
m_tool_changes[m_layer_idx].front().initial_tool == m_tool_changes[m_layer_idx].front().new_tool);
if (m_tool_change_idx == 0 && !ignore_sparse)
wipe_tower_z = m_last_wipe_tower_print_z + m_tool_changes[m_layer_idx].front().layer_height;
if (m_sparse_layers_skipped) {
ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]);
wipe_tower_z = m_compacted_tower_z[m_layer_idx];
}
if ((m_enable_timelapse_print || m_enable_wrapping_detection) && m_is_first_print) {
@@ -1947,10 +1980,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
if (!(size_t(m_tool_change_idx) < m_tool_changes[m_layer_idx].size()))
throw Slic3r::RuntimeError("Wipe tower generation failed, possibly due to empty first layer.");
if (!ignore_sparse) {
if (!ignore_sparse)
gcode += append_tcr(gcodegen, m_tool_changes[m_layer_idx][m_tool_change_idx++], extruder_id, wipe_tower_z);
m_last_wipe_tower_print_z = wipe_tower_z;
}
}
}
@@ -1964,9 +1995,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
return true;
bool ignore_sparse = false;
if (gcodegen.config().wipe_tower_no_sparse_layers.value) {
ignore_sparse = (m_tool_changes[m_layer_idx].size() == 1 && m_tool_changes[m_layer_idx].front().initial_tool == m_tool_changes[m_layer_idx].front().new_tool);
}
if (m_sparse_layers_skipped)
ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]);
if ((m_enable_timelapse_print || m_enable_wrapping_detection) && m_is_first_print) {
return false;
@@ -2942,6 +2972,19 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
const bool skip_config_block = print.config().gcode_skip_config_block;
const WipeTowerType wipe_tower_type = print.wipe_tower_type();
m_calib_config.clear();
// Orca: Calibration overrides are reapplied after object/region settings in _extrude().
// Keep inward wiping from masking retraction and pressure advance artifacts.
switch (print.calib_mode()) {
case CalibMode::Calib_PA_Line:
case CalibMode::Calib_PA_Pattern:
case CalibMode::Calib_PA_Tower:
case CalibMode::Calib_Auto_PA_Line:
case CalibMode::Calib_Retraction_tower:
m_calib_config.set_key_value("wipe_inward", new ConfigOptionBool(false));
break;
default:
break;
}
// resets analyzer's tracking data
m_last_height = 0.f;
m_last_layer_z = 0.f;
@@ -3596,7 +3639,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
auto used_filaments = print.get_slice_used_filaments(false);
this->placeholder_parser().set("is_all_bbl_filament", std::all_of(used_filaments.begin(), used_filaments.end(), [&](auto idx) {
return m_config.filament_vendor.values[idx] == "Bambu Lab";
return m_config.filament_vendor.get_at(idx) == "Bambu Lab";
}));
//add during_print_exhaust_fan_speed
@@ -3613,7 +3656,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
this->placeholder_parser().set("outer_wall_volumetric_speed", new ConfigOptionFloat(outer_wall_volumetric_speed));
auto first_layer_filaments = print.get_slice_used_filaments(true);
bool has_tpu_in_first_layer = std::any_of(first_layer_filaments.begin(), first_layer_filaments.end(), [&](unsigned int idx) { return m_config.filament_type.values[idx] == "TPU"; });
bool has_tpu_in_first_layer = std::any_of(first_layer_filaments.begin(), first_layer_filaments.end(), [&](unsigned int idx) { return m_config.filament_type.get_at(idx) == "TPU"; });
this->placeholder_parser().set("has_tpu_in_first_layer", new ConfigOptionBool(has_tpu_in_first_layer));
if (print.calib_params().mode == CalibMode::Calib_PA_Line) {
@@ -6349,8 +6392,13 @@ LayerResult GCode::process_layer(
all_label_ids.insert(inst.label_object_id);
break;
}
std::vector<size_t> filament_instances_id(all_label_ids.begin(), all_label_ids.end());
m_filament_instances_code = _encode_label_ids_to_base64(filament_instances_id);
// Orca: A scheduled extruder may have no object instances on this layer.
// Clear any pending mask so it cannot be emitted for the wrong toolchange.
m_filament_instances_code.clear();
if (!all_label_ids.empty()) {
std::vector<size_t> filament_instances_id(all_label_ids.begin(), all_label_ids.end());
m_filament_instances_code = _encode_label_ids_to_base64(filament_instances_id);
}
}
// The inline _extrude hook may already have taken the snapshot mid-extrusion on a
@@ -6596,6 +6644,8 @@ LayerResult GCode::process_layer(
}
// Then print infill
gcode += this->extrude_infill(print, by_region_specific, false);
// Then the walls left hanging in mid air, now that the infill can anchor them
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, false, true);
// Then print perimeters of regions that has is_infill_first == true
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, true);
}
@@ -6891,6 +6941,7 @@ LayerResult GCode::process_layer(
has_insert_timelapse_gcode = true;
}
gcode += this->extrude_infill(print, by_region_specific, false);
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, false, true);
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, true);
// ironing
gcode += this->extrude_infill(print, by_region_specific, true);
@@ -7240,7 +7291,8 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref,
const std::string& description,
double speed,
const ExtrusionEntitiesPtr& region_perimeters,
const Point* start_point)
const Point* start_point,
const WipeInwardSupport* wipe_support)
{
// get a copy; don't modify the orientation of the original loop object otherwise
// next copies (if any) would not detect the correct orientation
@@ -7470,63 +7522,80 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref,
m_processor.result().print_statistics.total_seam_scarf_distance += static_cast<float>(seam_scarf_distance_mm);
}
// BBS
// Orca: share the post-extrusion nozzle position between wipe_inward and wipe_on_loops.
const bool is_ccw = loop.is_counter_clockwise();
std::optional<Point> wipe_on_loops_dest;
if (m_config.wipe_on_loops.value && paths.back().role() == erExternalPerimeter &&
m_layer != nullptr && m_config.wall_loops.value > 1 && paths.front().size() >= 2 &&
paths.back().polyline.points.size() >= 2)
wipe_on_loops_dest = wipe_on_loops_destination(paths, scale_(nozzle_diameter), is_ccw, is_hole);
bool wipe_inward_applied = false;
// Orca: store loop paths in print order because inward offsets use this orientation.
if (m_wipe.enable && FILAMENT_CONFIG(wipe)) {
m_wipe.path = Polyline();
for (ExtrusionPath &path : paths) {
//BBS: Don't need to save duplicated point into wipe path
if (!m_wipe.path.empty() && !path.empty() &&
m_wipe.path.last_point() == Point(path.first_point().x(), path.first_point().y())) {
// Convert Points3 to Points
for (auto it = path.polyline.points.begin() + 1; it != path.polyline.points.end(); ++it)
m_wipe.path.append(Point(it->x(), it->y()));
} else
m_wipe.path.append(path.polyline.to_polyline()); // TODO: don't limit wipe to last path
m_wipe.update_path(paths);
// Orca: loop wipe paths retain print direction. Their material side is
// therefore left for CCW contours and right for CW contours, with the
// result inverted for holes. Only external perimeters are eligible.
// Calibration overrides are applied during extrusion, after the region
// context was created. Check the effective setting again at execution.
if (m_config.wipe_inward && m_config.wipe_inward_distance.value > 0. &&
wipe_support != nullptr && !wipe_support->inner_lines.empty() &&
// A loop's role is its first path's role. An overhanging start must
// not hide ordinary external-wall segments elsewhere in the loop.
std::any_of(paths.begin(), paths.end(),
[](const ExtrusionPath &path) { return is_external_perimeter(path.role()); }) &&
m_wipe.path.points.size() >= 2) {
// Orca: use the actual extrusion width from the path, not the config
// value — outer_wall_line_width=0 (Auto) would make get_abs_value
// return 0 and silently disable the feature, and Arachne may produce
// a different width than the config default.
const double outer_wall_line_width = paths.front().width;
const double requested_offset = m_config.wipe_inward_distance.get_abs_value(outer_wall_line_width);
const double offset_dist = scale_(std::min(requested_offset, outer_wall_line_width));
if (offset_dist > SCALED_EPSILON) {
const Point seam_start = paths.front().first_point();
const Point seam_end = paths.back().last_point();
const Point wipe_start = wipe_on_loops_dest.value_or(seam_end);
const double max_wipe_length = scale_(FILAMENT_CONFIG(wipe_distance));
// Orca: Wipe::wipe() replaces points[0] with last_pos and executes
// from points[1]. The helper preserves that sentinel and atomically
// replaces the remaining points, or leaves the path untouched.
// Orca: a configured wall count does not guarantee that Arachne
// generated an adjacent wall for this particular loop. Only
// earlier entities are considered because later walls have
// not been printed yet (for example with Outer/Inner order).
// Inner walls determine the material side; every earlier wall
// remains available to validate the executable wipe path.
const double support_distance = scale_(std::max(nozzle_diameter, outer_wall_line_width));
Polyline inward_path = m_wipe.path;
if (offset_wipe_path_toward_support(
inward_path, seam_start, seam_end, wipe_start,
wipe_offset_direction(is_ccw, is_hole), offset_dist, max_wipe_length,
wipe_support->inner_lines, wipe_support->printed_lines,
m_wipe.path.lines(), support_distance)) {
m_wipe.path = std::move(inward_path);
wipe_inward_applied = true;
}
}
}
}
// make a little move inwards before leaving loop
if (m_config.wipe_on_loops.value && paths.back().role() == erExternalPerimeter && m_layer != NULL && m_config.wall_loops.value > 1 && paths.front().size() >= 2 && paths.back().polyline.points.size() >= 3) {
// detect angle between last and first segment
// the side depends on the original winding order of the polygon (inwards for contours, outwards for holes)
//FIXME improve the algorithm in case the loop is tiny.
//FIXME improve the algorithm in case the loop is split into segments with a low number of points (see the Point b query).
const Point3 &a3 = paths.front().polyline.points[1]; // second point
Point a = Point(a3.x(), a3.y());
const Point3 &b3 = *(paths.back().polyline.points.end()-3); // second to last point
Point b = Point(b3.x(), b3.y());
if (is_hole == loop.is_counter_clockwise()) {
// swap points
Point c = a; a = b; b = c;
}
double angle = paths.front().first_point().ccw_angle(a, b) / 3;
// turn inwards if contour, turn outwards if hole
if (is_hole == loop.is_counter_clockwise()) angle *= -1;
// create the destination point along the first segment and rotate it
// we make sure we don't exceed the segment length because we don't know
// the rotation of the second segment so we might cross the object boundary
Vec2d p1 = paths.front().polyline.points.front().cast<double>().head<2>();
Vec2d p2 = paths.front().polyline.points[1].cast<double>().head<2>();
Vec2d v = p2 - p1;
double nd = scale_(EXTRUDER_CONFIG(nozzle_diameter));
double l2 = v.squaredNorm();
// Shift by no more than a nozzle diameter.
//FIXME Hiding the seams will not work nicely for very densely discretized contours!
//BBS. shorten the travel distant before the wipe path
double threshold = 0.2;
Point pt = (p1 + v * threshold).cast<coord_t>();
if (nd * nd < l2)
pt = (p1 + threshold * v * (nd / sqrt(l2))).cast<coord_t>();
//Point pt = ((nd * nd >= l2) ? (p1+v*0.4): (p1 + 0.2 * v * (nd / sqrt(l2)))).cast<coord_t>();
const Point3 &center3 = paths.front().polyline.points.front();
pt.rotate(angle, Point(center3.x(), center3.y()));
// generate the travel move
gcode += m_writer.extrude_to_xy(this->point_to_gcode(pt), 0, "move inwards before travel", true);
// Orca: make the configured inward move before leaving the loop.
if (wipe_on_loops_dest) {
gcode += m_writer.extrude_to_xy(
this->point_to_gcode(*wipe_on_loops_dest), 0, "move inwards before travel", true);
this->set_last_pos(*wipe_on_loops_dest);
}
// Execute the accepted path before another extrusion replaces it. Wiping
// must not force retraction or Z-hop across a short travel to the next wall.
// Ordinary travel planning decides whether to retract from the new position.
if (wipe_inward_applied)
gcode += m_wipe.wipe(*this, 0.);
return gcode;
}
@@ -7560,21 +7629,9 @@ std::string GCode::extrude_multi_path(const ExtrusionMultiPath& multipath, const
m_multi_flow_segment_path_pa_set = true;
}
// BBS
if (m_wipe.enable && FILAMENT_CONFIG(wipe)) {
m_wipe.path = Polyline();
for (const ExtrusionPath &path : multipath.paths) {
//BBS: Don't need to save duplicated point into wipe path
if (!m_wipe.path.empty() && !path.empty() &&
m_wipe.path.last_point() == Point(path.first_point().x(), path.first_point().y())) {
// Convert Points3 to Points
for (auto it = path.polyline.points.begin() + 1; it != path.polyline.points.end(); ++it)
m_wipe.path.append(Point(it->x(), it->y()));
} else
m_wipe.path.append(path.polyline.to_polyline()); // TODO: don't limit wipe to last path
}
m_wipe.path.reverse();
}
// Orca: multipath wipes retrace the extrusion in reverse order.
if (m_wipe.enable && FILAMENT_CONFIG(wipe))
m_wipe.update_path(multipath.paths, true);
return gcode;
}
@@ -7582,14 +7639,15 @@ std::string GCode::extrude_multi_path(const ExtrusionMultiPath& multipath, const
std::string GCode::extrude_entity(const ExtrusionEntity& entity,
const std::string& description,
double speed,
const ExtrusionEntitiesPtr& region_perimeters)
const ExtrusionEntitiesPtr& region_perimeters,
const WipeInwardSupport* wipe_support)
{
if (const ExtrusionPath* path = dynamic_cast<const ExtrusionPath*>(&entity))
return this->extrude_path(*path, description, speed);
else if (const ExtrusionMultiPath* multipath = dynamic_cast<const ExtrusionMultiPath*>(&entity))
return this->extrude_multi_path(*multipath, description, speed);
else if (const ExtrusionLoop* loop = dynamic_cast<const ExtrusionLoop*>(&entity))
return this->extrude_loop(*loop, description, speed, region_perimeters);
return this->extrude_loop(*loop, description, speed, region_perimeters, nullptr, wipe_support);
else
throw Slic3r::InvalidArgument("Invalid argument supplied to extrude()");
return "";
@@ -7603,6 +7661,7 @@ std::string GCode::extrude_path(const ExtrusionPath& path, const std::string& de
// description += ExtrusionEntity::role_to_string(path.role());
std::string gcode = this->_extrude(path, description, speed);
if (m_wipe.enable && FILAMENT_CONFIG(wipe)) {
m_wipe.reset_path();
m_wipe.path = path.polyline.to_polyline();
if (is_tree(this->config().support_type) && is_support(path.role())) {
if ((m_wipe.path.first_point() - m_wipe.path.last_point()).cast<double>().norm() > scale_(0.2)) {
@@ -7623,7 +7682,7 @@ std::string GCode::extrude_path(const ExtrusionPath& path, const std::string& de
}
// Extrude perimeters: Decide where to put seams (hide or align seams).
std::string GCode::extrude_perimeters(const Print &print, const std::vector<ObjectByExtruder::Island::Region> &by_region, bool is_first_layer, bool is_infill_first)
std::string GCode::extrude_perimeters(const Print &print, const std::vector<ObjectByExtruder::Island::Region> &by_region, bool is_first_layer, bool is_infill_first, bool unsupported_loops_only)
{
std::string gcode;
for (const ObjectByExtruder::Island::Region &region : by_region)
@@ -7635,8 +7694,36 @@ std::string GCode::extrude_perimeters(const Print &print, const std::vector<Obje
: (m_config.is_infill_first == is_infill_first);
if (!should_print) continue;
for (const ExtrusionEntity* ee : region.perimeters)
gcode += this->extrude_entity(*ee, "perimeter", -1., region.perimeters);
// Build the printed prefix once in emission order, scoped to this
// region. Disabled or zero-length wipes need no support geometry.
std::optional<WipeInwardSupport> wipe_support;
if (m_wipe.enable && FILAMENT_CONFIG(wipe) && m_config.wipe_inward &&
m_config.wipe_inward_distance.value > 0. &&
scale_(FILAMENT_CONFIG(wipe_distance)) > SCALED_EPSILON)
wipe_support.emplace();
// ORCA: loops flagged as extruded in mid air, out of reach of the layer below, are held back
// for a second pass after the infill that anchors them. Infill already precedes infill first walls.
const bool defer_unsupported = !is_infill_first;
auto waits_for_infill = [](const ExtrusionEntity *ee) {
return ee->is_loop() && static_cast<const ExtrusionLoop *>(ee)->print_after_infill;
};
// The deferred pass runs after the infill, so the loops the first pass emitted are
// already down and belong in the prefix an inward wipe may land on.
if (wipe_support && defer_unsupported && unsupported_loops_only)
for (const ExtrusionEntity* ee : region.perimeters)
if (!waits_for_infill(ee))
wipe_support->append(*ee);
for (const ExtrusionEntity* ee : region.perimeters) {
if (defer_unsupported && waits_for_infill(ee) != unsupported_loops_only)
continue;
gcode += this->extrude_entity(*ee, "perimeter", -1., region.perimeters,
wipe_support ? &*wipe_support : nullptr);
if (wipe_support)
wipe_support->append(*ee);
}
}
return gcode;
}
@@ -7877,7 +7964,8 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
// path is 2D. But in slope lift case, lift z is done in travel_to function.
// Add m_need_change_layer_lift_z when change_layer in case of no lift if m_last_pos is equal to path.first_point() by chance
Point first_point = path.first_point();
if (!m_last_pos_defined || m_last_pos.to_point() != first_point || m_need_change_layer_lift_z || slope_need_z_travel) {
if (!m_last_pos_defined || m_last_pos.to_point() != first_point || m_need_change_layer_lift_z ||
slope_need_z_travel) {
const bool _last_pos_undefined = !m_last_pos_defined;
double z = DBL_MAX;
@@ -8238,9 +8326,10 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
}
variable_speed = std::any_of(new_points.begin(), new_points.end(),
[speed](const ProcessedPoint &p) { return fabs(double(p.speed) - speed) > 1; }); // Ignore small speed variations (under 1mm/sec)
if (!NOZZLE_CONFIG(enable_overhang_speed) && FILAMENT_CONFIG(enable_overhang_bridge_fan) && m_enable_cooling_markers) {
for (ProcessedPoint &point : new_points)
point.speed = speed;
if (FILAMENT_CONFIG(enable_overhang_bridge_fan) && m_enable_cooling_markers) {
if (!NOZZLE_CONFIG(enable_overhang_speed))
for (ProcessedPoint &point : new_points)
point.speed = speed;
variable_speed = new_points.size() > 1;
}
}
@@ -9120,7 +9209,7 @@ bool GCode::needs_retraction(const Polyline &travel, ExtrusionRole role, LiftTyp
continue;
Polygons temp;
temp.emplace_back(std::move(instance_bbox.polygon()));
temp.emplace_back(instance_bbox.polygon());
if (intersection_pl(travel, temp).empty())
continue;
@@ -9509,12 +9598,14 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
if (old_filament_id_in_new_extruder == -1)
wipe_volume = 0;
else {
wipe_volume = flush_matrix[old_filament_id_in_new_extruder * number_of_extruders + new_filament_id];
size_t flush_idx = size_t(old_filament_id_in_new_extruder) * number_of_extruders + new_filament_id;
wipe_volume = flush_idx < flush_matrix.size() ? flush_matrix[flush_idx] : 0.f;
wipe_volume *= m_config.flush_multiplier.get_at(new_extruder_id);
}
}
else {
wipe_volume = flush_matrix[old_filament_id * number_of_extruders + new_filament_id];
size_t flush_idx = size_t(old_filament_id) * number_of_extruders + new_filament_id;
wipe_volume = flush_idx < flush_matrix.size() ? flush_matrix[flush_idx] : 0.f;
wipe_volume *= m_config.flush_multiplier.get_at(new_extruder_id); // if is multi_extruder only use the fist extruder matrix
}
wipe_volume = std::max(0.f, wipe_volume-grab_purge_volume);
+21 -6
View File
@@ -39,6 +39,7 @@ namespace Slic3r {
// Forward declarations.
class GCode;
struct WipeInwardSupport;
namespace CustomGCode{ struct Item; }
struct PrintInstance;
@@ -61,7 +62,7 @@ public:
bool enable;
Polyline path;
// Orca:
// Orca: retraction portions emitted before, during, and after the wipe move.
struct RetractionValues{
double retraction_length_before_wipe = 0.;
double retraction_length_during_wipe = 0.;
@@ -73,8 +74,10 @@ public:
void reset_path() { this->path = Polyline(); }
std::string wipe(GCode &gcodegen, double length, bool toolchange = false, bool is_last = false);
// Orca:
// Orca: calculate the retraction portions that can be emitted at wipe speed.
RetractionValues calculateWipeRetractionLengths(GCode& gcodegen, bool toolchange);
// Orca: rebuild the stored path while deduplicating shared path boundaries.
void update_path(const ExtrusionPaths &paths, bool reverse = false);
};
class WipeTowerIntegration {
@@ -103,8 +106,13 @@ public:
m_enable_wrapping_detection(print_config.enable_wrapping_detection && (print_config.wrapping_exclude_area.values.size() > 2) && (slice_used_filaments.size() <= 1)),
m_is_first_print(true),
m_print_config(&print_config),
m_last_wipe_tower_print_z(print_config.z_offset.value)
m_last_wipe_tower_print_z(print_config.z_offset.value),
m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(print_config))
{
// Precomputed rather than accumulated while emitting, so that the clearance validator and
// the emitter cannot disagree about where the compacted tower sits on any given layer.
if (m_sparse_layers_skipped)
m_compacted_tower_z = compute_compacted_wipe_tower_z(tool_changes, float(print_config.z_offset.value));
// initialize with the extruder offset of master extruder id
m_extruder_offsets.resize(print_config.filament_map.size(), print_config.extruder_offset.get_at(print_config.master_extruder_id.value - 1));
const auto& filament_map = print_config.filament_map.values; // 1 based idx
@@ -164,6 +172,11 @@ private:
float m_wipe_tower_depth;
BoundingBoxf m_wipe_tower_bbx;
Vec2f m_rib_offset{Vec2f(0, 0)};
// wipe_tower_no_sparse_layers, as answered by the shared compaction rule rather than by the raw
// option: smooth timelapse and wrapping detection keep a tower on every layer regardless.
const bool m_sparse_layers_skipped;
// Print z of the compacted tower per planned layer. Empty when the tower is not compacted.
std::vector<float> m_compacted_tower_z;
};
class ColorPrintColors
@@ -430,14 +443,16 @@ private:
std::string extrude_entity(const ExtrusionEntity& entity,
const std::string& description = "",
double speed = -1.,
const ExtrusionEntitiesPtr& region_perimeters = ExtrusionEntitiesPtr());
const ExtrusionEntitiesPtr& region_perimeters = ExtrusionEntitiesPtr(),
const WipeInwardSupport* wipe_support = nullptr);
// Orca: pass the complete collection of region perimeters to the extrude loop to check whether the wipe before external loop
// should be executed
std::string extrude_loop(const ExtrusionLoop& loop,
const std::string& description,
double speed = -1.,
const ExtrusionEntitiesPtr& region_perimeters = ExtrusionEntitiesPtr(),
const Point* start_point = nullptr);
const Point* start_point = nullptr,
const WipeInwardSupport* wipe_support = nullptr);
std::string extrude_multi_path(const ExtrusionMultiPath& multipath, const std::string& description = "", double speed = -1.);
std::string extrude_path(const ExtrusionPath& path, const std::string& description = "", double speed = -1.);
@@ -519,7 +534,7 @@ private:
// For sequential print, the instance of the object to be printing has to be defined.
const size_t single_object_instance_idx);
std::string extrude_perimeters(const Print& print, const std::vector<ObjectByExtruder::Island::Region>& by_region, bool is_first_layer, bool is_infill_first);
std::string extrude_perimeters(const Print& print, const std::vector<ObjectByExtruder::Island::Region>& by_region, bool is_first_layer, bool is_infill_first, bool unsupported_loops_only = false);
std::string extrude_infill(const Print& print, const std::vector<ObjectByExtruder::Island::Region>& by_region, bool ironing);
std::string extrude_support(const ExtrusionEntityCollection& support_fills, const ExtrusionRole support_extrusion_role);
+11 -10
View File
@@ -1024,28 +1024,29 @@ std::string CoolingBuffer::apply_layer_cooldown(
}
if (need_set_fan) {
const auto set_fan = [&](int speed) {
if (m_current_fan_speed != speed) {
new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, speed, part_cooling_fan_min_pwm);
m_current_fan_speed = speed;
}
};
if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){
new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, overhang_fan_speed, part_cooling_fan_min_pwm);
m_current_fan_speed = overhang_fan_speed;
set_fan(overhang_fan_speed);
} else if (fan_speed_change_requests[CoolingLine::TYPE_INTERNAL_BRIDGE_FAN_START]){ // ORCA: Add support for separate internal bridge fan speed control
new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, internal_bridge_fan_speed, part_cooling_fan_min_pwm);
m_current_fan_speed = internal_bridge_fan_speed;
set_fan(internal_bridge_fan_speed);
}
else if (fan_speed_change_requests[CoolingLine::TYPE_SUPPORT_INTERFACE_FAN_START]){
new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, supp_interface_fan_speed, part_cooling_fan_min_pwm);
m_current_fan_speed = supp_interface_fan_speed;
set_fan(supp_interface_fan_speed);
}
else if (fan_speed_change_requests[CoolingLine::TYPE_IRONING_FAN_START]){
new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, ironing_fan_speed, part_cooling_fan_min_pwm);
m_current_fan_speed = ironing_fan_speed;
set_fan(ironing_fan_speed);
}
else if(fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] && m_current_fan_speed != -1){
new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_current_fan_speed, part_cooling_fan_min_pwm);
fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] = false;
}
else {
new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_fan_speed, part_cooling_fan_min_pwm);
m_current_fan_speed = m_fan_speed;
set_fan(m_fan_speed);
}
need_set_fan = false;
}
+5 -6
View File
@@ -170,14 +170,13 @@ void FanMover::_put_in_middle_G1(std::list<BufferData>::iterator item_to_split,
void FanMover::_print_in_middle_G1(BufferData& line_to_split, float nb_sec, const std::string &line_to_write) {
if (nb_sec < line_to_split.time * 0.1) {
// doesn't really need to be split, print it after
m_process_output += line_to_split.raw + "\n";
// Doesn't need to be split: the insertion point is at the start.
m_process_output += line_to_write + (line_to_write.back() == '\n'?"":"\n");
} else if (nb_sec > line_to_split.time * 0.9) {
// doesn't really need to be split, print it before
//will also print before if line_to_split.time == 0
m_process_output += line_to_write + (line_to_write.back() == '\n' ? "" : "\n");
m_process_output += line_to_split.raw + "\n";
} else if (nb_sec > line_to_split.time * 0.9) {
// Doesn't need to be split: the insertion point is at the end.
m_process_output += line_to_split.raw + "\n";
m_process_output += line_to_write + (line_to_write.back() == '\n' ? "" : "\n");
}else if(line_to_split.raw.size() > 2
&& line_to_split.raw[0] == 'G' && line_to_split.raw[1] == '1' && line_to_split.raw[2] == ' ') {
float percent = nb_sec / line_to_split.time;
+2 -1
View File
@@ -32,7 +32,8 @@ class FanMover
private:
const std::regex regex_fan_speed;
const float nb_seconds_delay;
const bool with_D_option;
// Set from fan_speedup_time at the call site, but nothing here reads it.
[[maybe_unused]] const bool with_D_option;
const bool relative_e;
const bool only_overhangs;
const float kickstart;
+15 -12
View File
@@ -532,7 +532,7 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
const float height = interpolate ? lerp(prev_move.height, curr_move.height, t) : curr_move.height;
// ORCA: Fix issue with flow rate changes being visualized incorrectly
const float mm3_per_mm = curr_move.mm3_per_mm;
const float fan_speed = interpolate ? lerp(prev_move.fan_speed, curr_move.fan_speed, t) : curr_move.fan_speed;
const float fan_speed = curr_move.fan_speed;
const float temperature = interpolate ? lerp(prev_move.temperature, curr_move.temperature, t) : curr_move.temperature;
actual_speed_moves.push_back({
block.move_id,
@@ -563,7 +563,7 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
const float height = interpolate ? lerp(prev_move.height, curr_move.height, t) : curr_move.height;
// ORCA: Fix issue with flow rate changes being visualized incorrectly
const float mm3_per_mm = curr_move.mm3_per_mm;
const float fan_speed = interpolate ? lerp(prev_move.fan_speed, curr_move.fan_speed, t) : curr_move.fan_speed;
const float fan_speed = curr_move.fan_speed;
const float temperature = interpolate ? lerp(prev_move.temperature, curr_move.temperature, t) : curr_move.temperature;
actual_speed_moves.push_back({
block.move_id,
@@ -1273,7 +1273,7 @@ void GCodeProcessor::run_post_process()
// add lines M73 to exported gcode
auto process_line_move = [
// Lambdas, mostly for string formatting, all with an empty capture block.
time_in_minutes, format_time_float, format_line_M73_main, format_line_M73_stop_int, format_line_M73_stop_float, time_in_last_minute,format_line_exhaust_fan_control,
time_in_minutes, format_time_float, format_line_M73_main, format_line_M73_stop_int, format_line_M73_stop_float, time_in_last_minute,
&self = std::as_const(m_time_processor),
// Caches, to be modified
&g1_times_cache_it, &last_exported_main, &last_exported_stop,
@@ -1468,9 +1468,11 @@ void GCodeProcessor::run_post_process()
// Append a per-filament usage block at a filament change.
auto handle_filament_change = [&](int filament_id, int cur_line_id, int nozzle_id) {
// skip filament changes emitted inside the machine start / end gcode
if (m_machine_start_gcode_end_line_id == (unsigned int) (-1) && (unsigned int) (cur_line_id) < m_machine_start_gcode_end_line_id ||
m_machine_end_gcode_start_line_id != (unsigned int) (-1) && (unsigned int) (cur_line_id) > m_machine_end_gcode_start_line_id)
// Skip filament changes emitted inside the machine start / end gcode. One forward pass assigns
// the tag ids and tests them in the same loop, so inside the start gcode the end tag is unseen
// and the id still holds the sentinel. That is why the first clause tests == and the second !=.
if ((m_machine_start_gcode_end_line_id == (unsigned int) (-1) && (unsigned int) (cur_line_id) < m_machine_start_gcode_end_line_id) ||
(m_machine_end_gcode_start_line_id != (unsigned int) (-1) && (unsigned int) (cur_line_id) > m_machine_end_gcode_start_line_id))
return;
if (!m_filament_blocks.empty())
m_filament_blocks.back().upper_gcode_id = cur_line_id;
@@ -2777,7 +2779,7 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int
std::map<int, std::map<int, GCodePosInfo>> gcode_path_pos; // object_id, filament_id, pos
for (const GCodeProcessorResult::MoveVertex &move : m_result.moves) {
// sometimes, the start line extrude was outside the edge of plate a little, this is allowed, so do not include into the gcode_path_pos
if (move.type == EMoveType::Extrude /* && move.extrusion_role != ExtrusionRole::erFlush || move.type == EMoveType::Travel*/)
if (move.type == EMoveType::Extrude /* && move.extrusion_role != ExtrusionRole::erFlush || move.type == EMoveType::Travel*/) {
if (move.extrusion_role == ExtrusionRole::erCustom) {
/*if (move.is_arc_move_with_interpolation_points()) {
for (int i = 0; i < move.interpolation_points.size(); i++) {
@@ -2799,6 +2801,7 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int
gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z = std::max(gcode_path_pos[move.object_label_id][int(move.extruder_id)].max_print_z,
move.print_z);
}
}
}
bool valid = true;
@@ -5304,7 +5307,7 @@ void GCodeProcessor::process_VG1(const GCodeReader::GCodeLine& line)
float filament_radius = 0.5f * filament_diameter;
float area_filament_cross_section = static_cast<float>(M_PI) * sqr(filament_radius);
auto absolute_position = [this, area_filament_cross_section](Axis axis, const GCodeReader::GCodeLine& lineG1) {
auto absolute_position = [this](Axis axis, const GCodeReader::GCodeLine& lineG1) {
bool is_relative = (m_global_positioning_type == EPositioningType::Relative);
if (axis == E)
is_relative |= (m_e_local_positioning_type == EPositioningType::Relative);
@@ -5755,7 +5758,7 @@ void GCodeProcessor::process_G2_G3(const GCodeReader::GCodeLine& line, bool cloc
if (travel_length < 0.001)
return;
auto adjust_target = [this, area_filament_cross_section](const AxisCoords& target, const AxisCoords& prev_position) {
auto adjust_target = [this](const AxisCoords& target, const AxisCoords& prev_position) {
AxisCoords ret = target;
if (m_global_positioning_type == EPositioningType::Relative) {
for (unsigned char a = X; a <= E; ++a) {
@@ -7006,7 +7009,7 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type,
get_acceleration(normal_mode));
const float junction_deviation = get_option_value(m_time_processor.machine_limits.machine_max_junction_deviation, normal_mode_id);
const bool use_jd_jerk = (m_flavor == gcfMarlinFirmware && junction_deviation > 0.0f);
const auto axis_jerk_for_preview = [this, normal_mode, use_jd_jerk, move_acceleration](Axis axis) {
const auto axis_jerk_for_preview = [this, use_jd_jerk, move_acceleration](Axis axis) {
return use_jd_jerk ? get_axis_max_jerk_with_jd(normal_mode, axis, move_acceleration) : get_axis_max_jerk(normal_mode, axis);
};
const float jerk_x = axis_jerk_for_preview(X);
@@ -7593,8 +7596,8 @@ void GCodeProcessor::update_slice_warnings()
if (used_filaments[idx] < m_result.required_nozzle_HRC.size())
filament_hrc = m_result.required_nozzle_HRC[used_filaments[idx]];
int filament_extruder_id = m_filament_maps[used_filaments[idx]];
int extruder_hrc = nozzle_hrc_lists[filament_extruder_id];
int filament_extruder_id = used_filaments[idx] < m_filament_maps.size() ? m_filament_maps[used_filaments[idx]] : -1;
int extruder_hrc = (filament_extruder_id >= 0 && (size_t) filament_extruder_id < nozzle_hrc_lists.size()) ? nozzle_hrc_lists[filament_extruder_id] : 0;
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": Check HRC: filament:%1%, hrc=%2%, extruder:%3%, hrc:%4%") % used_filaments[idx] % filament_hrc % filament_extruder_id % extruder_hrc;
+1 -1
View File
@@ -78,7 +78,7 @@ struct PlateBBoxData
int first_extruder = 0;
float nozzle_diameter = 0.4;
std::string bed_type;
float first_layer_time;
float first_layer_time = 0.0f;
// version 1: use view type ColorPrint (filament color)
// version 2: use view type FilamentId (filament id)
int version = 2;
+1 -1
View File
@@ -910,7 +910,7 @@ namespace Slic3r
unsigned int iterations = (1 << all_extruders.size());
unsigned int final_state = iterations - 1;
std::vector<std::vector<float>>cache(iterations, std::vector<float>(all_extruders.size(), 0x7fffffff));
std::vector<std::vector<float>>cache(iterations, std::vector<float>(all_extruders.size(), std::numeric_limits<float>::max()));
std::vector<std::vector<int>>prev(iterations, std::vector<int>(all_extruders.size(), -1));
cache[1][0] = 0.;
for (unsigned int state = 0; state < iterations; ++state) {
+76 -16
View File
@@ -10,6 +10,7 @@
#include "FilamentMixer.hpp"
#include "LocalesUtils.hpp"
#include "Utils.hpp"
#include "format.hpp"
#include "I18N.hpp"
#include <boost/log/trivial.hpp>
@@ -82,8 +83,9 @@ bool check_filament_printable_after_group(const std::vector<unsigned int> &used_
int printable_status = print_config->filament_printable.get_at(filament_id);
int extruder_idx = filament_maps[filament_id];
if (!(printable_status >> extruder_idx & 1)) {
std::string extruder_name = extruder_idx == 0 ? _L("left") : _L("right");
std::string error_msg = _L("Grouping error: ") + filament_type + _L(" can not be placed in the ") + extruder_name + _L(" nozzle");
std::string error_msg = extruder_idx == 0 ?
Slic3r::format(_L("Grouping error: %1% cannot be placed in the left nozzle"), filament_type) :
Slic3r::format(_L("Grouping error: %1% cannot be placed in the right nozzle"), filament_type);
throw Slic3r::RuntimeError(error_msg);
}
}
@@ -1488,10 +1490,10 @@ static FilamentGroupContext build_filament_group_context(
auto machine_filament_info = build_machine_filaments(print->get_extruder_filament_info(), extruder_ams_counts, ignore_ext_filament);
std::vector<std::string> filament_types = print_config.filament_type.values;
std::vector<std::string> filament_colours = print_config.filament_colour.values;
std::vector<unsigned char> filament_is_support = print_config.filament_is_support.values;
std::vector<std::string> filament_ids = print_config.filament_ids.values;
// The grouping code walks filament_ids and indexes filament_info by the same position.
std::vector<std::string> filament_ids = print_config.filament_ids.values;
if (filament_ids.size() > filament_nums)
filament_ids.resize(filament_nums);
FGMode fg_mode = mode == FilamentMapMode::fmmAutoForMatch ? FGMode::MatchMode : FGMode::FlushMode;
context.model_info.flush_matrix = std::move(nozzle_flush_mtx);
@@ -1500,11 +1502,14 @@ static FilamentGroupContext build_filament_group_context(
context.model_info.filament_ids = filament_ids;
context.model_info.unprintable_volumes = unprintable_volumes;
for (size_t idx = 0; idx < filament_types.size(); ++idx) {
// Consumers index filament_info by filament id, so it must span the filament count: a partial
// or legacy config can leave any of these arrays short, and get_at clamps.
context.model_info.filament_info.reserve(filament_nums);
for (size_t idx = 0; idx < filament_nums; ++idx) {
FilamentGroupUtils::FilamentInfo info;
info.color = filament_colours[idx];
info.type = filament_types[idx];
info.is_support = filament_is_support[idx];
info.color = print_config.filament_colour.get_at(idx);
info.type = print_config.filament_type.get_at(idx);
info.is_support = print_config.filament_is_support.get_at(idx);
context.model_info.filament_info.emplace_back(std::move(info));
}
@@ -2732,6 +2737,28 @@ void ToolOrdering::enforce_mixed_component_order()
}
}
// Declared in ToolOrdering.hpp (exposed for unit testing).
std::vector<unsigned int> parse_cyclic_order(const std::string& str, unsigned int number_of_extruders)
{
std::vector<unsigned int> order;
for (const std::string& token : split_string(str, ',')) {
try {
size_t pos = 0;
int filament = std::stoi(token, &pos); // stoi skips leading whitespace by itself
// stoi stops at the first non-digit, so "2x" would parse as 2. Require the whole token to be
// consumed (bar trailing whitespace) to drop it like any other garbage.
if (token.find_first_not_of(" \t\r\n", pos) != std::string::npos)
continue;
if (filament >= 1 && (unsigned int)filament <= number_of_extruders
&& std::find(order.begin(), order.end(), (unsigned int)(filament - 1)) == order.end())
order.emplace_back((unsigned int)(filament - 1));
} catch (const std::exception&) {
// Not a number, ignore it.
}
}
return order;
}
void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer)
{
const PrintConfig* print_config = m_print_config_ptr;
@@ -2829,11 +2856,41 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
const bool use_cyclic_ordering =
(print_config->toolchange_ordering == ToolChangeOrderingType::Cyclic);
// By default the first layer keeps its adhesion-optimized order (and any custom first layer
// sequence); the cyclic sequence is only forced onto it when the user opts in.
const bool cyclic_first_layer = use_cyclic_ordering && print_config->toolchange_cyclic_first_layer.value;
// Optional user defined cyclic sequence, given as 1-based filament numbers ("3,2,1,4"). Filaments
// missing from it keep their ascending order after the listed ones, so a partial or bogus entry
// still yields the default cyclic order.
const std::vector<unsigned int> cyclic_order =
use_cyclic_ordering ? parse_cyclic_order(print_config->toolchange_cyclic_order.value, number_of_extruders)
: std::vector<unsigned int>();
// Reorder a layer's filaments (0-based) for cyclic ordering: ascending by default, or following the
// user defined sequence when one was given. Filaments absent from the sequence keep ascending order
// after the listed ones.
auto apply_cyclic_order = [&cyclic_order](std::vector<unsigned int>& filaments) {
std::sort(filaments.begin(), filaments.end());
if (!cyclic_order.empty())
std::stable_sort(filaments.begin(), filaments.end(), [&cyclic_order](unsigned int lhs, unsigned int rhs) {
auto rank = [&cyclic_order](unsigned int filament) {
return size_t(std::find(cyclic_order.begin(), cyclic_order.end(), filament) - cyclic_order.begin());
};
return rank(lhs) < rank(rhs);
});
};
// other_layers_seq: the layer_idx and extruder_idx are base on 1
auto get_custom_seq = [&other_layers_seqs, &reorder_first_layer, &first_layer_filaments, &layer_filaments, use_cyclic_ordering](int layer_idx, std::vector<int>& out_seq) -> bool {
auto get_custom_seq = [&other_layers_seqs, &reorder_first_layer, &first_layer_filaments, &layer_filaments, use_cyclic_ordering, cyclic_first_layer, &apply_cyclic_order](int layer_idx, std::vector<int>& out_seq) -> bool {
if (!reorder_first_layer && layer_idx == 0) {
out_seq.resize(first_layer_filaments.size());
std::transform(first_layer_filaments.begin(), first_layer_filaments.end(), out_seq.begin(), [](auto item) {return item + 1; });
// The first layer tool order is already decided (adhesion-optimized, plus any custom first
// layer sequence). Only override it with the cyclic sequence when the user opted in.
std::vector<unsigned int> ordered = first_layer_filaments;
if (cyclic_first_layer)
apply_cyclic_order(ordered);
out_seq.resize(ordered.size());
std::transform(ordered.begin(), ordered.end(), out_seq.begin(), [](auto item) {return int(item) + 1; });
return true;
}
for (size_t idx = other_layers_seqs.size() - 1; idx != size_t(-1); --idx) {
@@ -2844,9 +2901,12 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
}
}
if (use_cyclic_ordering && layer_idx >= 0 && size_t(layer_idx) < layer_filaments.size()) {
// Skip the first layer here (layer_idx == 0 only reaches this point on the reorder_first_layer
// path) unless the user asked for cyclic order on it, so it keeps the default flush ordering.
if (use_cyclic_ordering && layer_idx >= 0 && (layer_idx != 0 || cyclic_first_layer)
&& size_t(layer_idx) < layer_filaments.size()) {
std::vector<unsigned int> ordered = layer_filaments[size_t(layer_idx)];
std::sort(ordered.begin(), ordered.end());
apply_cyclic_order(ordered);
out_seq.resize(ordered.size());
std::transform(ordered.begin(), ordered.end(), out_seq.begin(), [](auto item) { return int(item) + 1; });
return true;
@@ -3137,7 +3197,7 @@ void ToolOrdering::assign_custom_gcodes(const Print &print)
// Skip all custom G-codes above this layer and skip all extruder switches.
for (; custom_gcode_it != custom_gcode_per_print_z.gcodes.rend() && (
(print_z_above > lt.print_z && custom_gcode_it->print_z > 0.5 * (lt.print_z + print_z_above))
|| custom_gcode_it->type == CustomGCode::ToolChange); ++ custom_gcode_it);
|| custom_gcode_it->type == CustomGCode::ToolChange); ++ custom_gcode_it) {}
print_z_above = lt.print_z;
if (custom_gcode_it == custom_gcode_per_print_z.gcodes.rend())
// Custom G-codes were processed.
+5
View File
@@ -417,6 +417,11 @@ private:
int most_used_extruder;
};
// Parse the user defined cyclic toolchange sequence ("3,2 , 1 , 4") into 0-based filament indices.
// Out-of-range entries, duplicates and non-numeric tokens are dropped, so a partially valid string
// still orders the filaments it does name. Exposed for unit testing.
std::vector<unsigned int> parse_cyclic_order(const std::string& str, unsigned int number_of_extruders);
} // namespace SLic3r
#endif /* slic3r_ToolOrdering_hpp_ */
+920
View File
@@ -0,0 +1,920 @@
#include "WipePathHelpers.hpp"
#include "../AABBTreeLines.hpp"
#include <algorithm>
#include <cassert>
#include <cmath>
#include <limits>
#include <tuple>
namespace Slic3r {
void WipeInwardSupport::append(const ExtrusionEntity &entity)
{
const ExtrusionPaths *paths = nullptr;
if (const auto *loop = dynamic_cast<const ExtrusionLoop *>(&entity))
paths = &loop->paths;
else if (const auto *multipath = dynamic_cast<const ExtrusionMultiPath *>(&entity))
paths = &multipath->paths;
// A loop's role is its first path's role. An overhanging start must not
// hide the ordinary inner-wall segments elsewhere in the same loop.
const bool is_inner = paths ? std::any_of(paths->begin(), paths->end(),
[](const ExtrusionPath &path) { return is_internal_perimeter(path.role()); }) :
is_internal_perimeter(entity.role());
const Lines lines = entity.as_polyline().lines();
printed_lines.insert(printed_lines.end(), lines.begin(), lines.end());
if (is_inner)
inner_lines.insert(inner_lines.end(), lines.begin(), lines.end());
}
// Orca: miter limit ratio. Matches DefaultMiterLimit from ClipperUtils.hpp.
// When the miter join extends more than miter_limit * offset_dist from the
// original vertex, the miter is replaced by a bevel join.
static constexpr double miter_limit = 3.0;
// Orca: threshold for detecting near-reversal (backtracking spike).
// Normalized dot product below this means the segments point in nearly
// opposite directions (angle > ~172°). Offsetting such a path is unsafe.
static constexpr double reversal_dot_threshold = -0.99;
// Orca: candidates pointing more than 60 degrees away from the selected inner
// wall are too tangent to distinguish the material side reliably at a cusp.
static constexpr double min_support_alignment = 0.5;
// Keep a scaled-coordinate rounding floor while allowing the tolerance to
// follow the relevant offset or path length. Clearance allows a larger fraction.
static double wipe_tolerance(double distance, double relative_tolerance = 0.1)
{
return std::max(4. * SCALED_EPSILON, relative_tolerance * distance);
}
Point sample_path_at_distance(const ExtrusionPaths &paths, bool forward, double target)
{
assert(!paths.empty());
if (paths.empty())
return Point(0, 0);
double remaining = target;
Point result = forward ? paths.front().first_point() : paths.back().last_point();
for (int pi = forward ? 0 : (int)paths.size() - 1;
pi >= 0 && pi < (int)paths.size() && remaining > 0.;
pi += forward ? 1 : -1) {
const Points3 &pts = paths[pi].polyline.points;
for (int i = forward ? 0 : (int)pts.size() - 1;
remaining > 0. && (forward ? i + 1 < (int)pts.size() : i > 0);
i += forward ? 1 : -1) {
const int j = forward ? i + 1 : i - 1;
const Point cur(pts[i].x(), pts[i].y());
const Point next(pts[j].x(), pts[j].y());
const double segment_length = (next - cur).cast<double>().norm();
if (segment_length < SCALED_EPSILON)
continue;
if (remaining <= segment_length) {
const double ratio = remaining / segment_length;
return Point(coord_t(cur.x() + ratio * (next.x() - cur.x())),
coord_t(cur.y() + ratio * (next.y() - cur.y())));
}
remaining -= segment_length;
result = next;
}
}
return result;
}
// Orca: consecutive duplicates carry no path length and can be removed safely.
// A reversal, however, is real travelled distance: removing its vertex would
// replace a long backtracking wipe with a short, unrelated shortcut.
static bool prepare_source(Points &pts)
{
pts.erase(std::unique(pts.begin(), pts.end()), pts.end());
if (pts.size() < 2)
return false;
for (size_t i = 1; i + 1 < pts.size(); ++i) {
const Vec2d v_prev = (pts[i] - pts[i - 1]).cast<double>();
const Vec2d v_next = (pts[i + 1] - pts[i]).cast<double>();
const double dot = v_prev.dot(v_next) / (v_prev.norm() * v_next.norm());
if (dot < reversal_dot_threshold)
return false;
}
return true;
}
static bool build_offset_polyline(const Points &original, int dir, double offset_dist,
Points &result, size_t &first_join_index)
{
if (original.size() < 2)
return false;
// Orca: collapse all consecutive duplicates first, then reject any
// backtracking in the cleaned path instead of replacing travelled distance
// with a shortcut.
Points source = original;
if (! prepare_source(source))
return false;
const size_t n = source.size();
// Orca: compute the perpendicular offset for segment i->i+1 as an infinite Line.
auto offset_segment = [dir, offset_dist](const Point &a, const Point &b) -> Line {
Vec2d v = (b - a).cast<double>();
double len = v.norm();
Vec2d perp(0, 0);
if (len > SCALED_EPSILON)
perp = Vec2d(-v.y(), v.x()) * (dir * offset_dist / len);
return Line(Point(coord_t(a.x() + perp.x()), coord_t(a.y() + perp.y())),
Point(coord_t(b.x() + perp.x()), coord_t(b.y() + perp.y())));
};
result.clear();
result.reserve(n);
first_join_index = 0;
// Orca: the first point is perpendicular to the first segment.
Line l_prev = offset_segment(source[0], source[1]);
result.push_back(l_prev.a);
// Orca: use the analytic intersection of adjacent offset segments for a
// miter join. Intersecting the already rounded Line endpoints amplifies
// coordinate quantization when the source segments are nearly parallel.
for (size_t i = 1; i + 1 < n; ++i) {
Line l_next = offset_segment(source[i], source[i + 1]);
const Vec2d previous = (source[i] - source[i - 1]).cast<double>().normalized();
const Vec2d next = (source[i + 1] - source[i]).cast<double>().normalized();
const double denominator = 1. + previous.dot(next);
bool need_bevel = denominator <= EPSILON;
Point pt;
if (! need_bevel) {
const Vec2d previous_normal(-previous.y(), previous.x());
const Vec2d next_normal(-next.y(), next.x());
const Vec2d miter = (previous_normal + next_normal) * (dir * offset_dist / denominator);
if (miter.norm() > miter_limit * offset_dist) {
need_bevel = true;
} else {
pt = Point(coord_t(source[i].x() + miter.x()),
coord_t(source[i].y() + miter.y()));
}
}
if (need_bevel) {
result.push_back(l_prev.b);
if (l_next.a != result.back())
result.push_back(l_next.a);
} else {
result.push_back(pt);
}
if (i == 1)
first_join_index = result.size() - 1;
l_prev = l_next;
}
// Orca: the last point is perpendicular to the last segment.
result.push_back(l_prev.b);
return true;
}
int wipe_offset_direction(bool is_ccw, bool is_hole)
{
const int loop_inside = is_ccw ? +1 : -1;
return is_hole ? -loop_inside : loop_inside;
}
static bool starts_by_backtracking(const Polyline &path, Point actual_start)
{
if (path.points.size() < 3)
return false;
// Orca: points[0] is only a storage sentinel; use the nozzle position for
// the executable connector, particularly after a wipe_on_loops pre-move.
const Vec2d connector = (path.points[1] - actual_start).cast<double>();
const Vec2d outgoing = (path.points[2] - path.points[1]).cast<double>();
// An inward connector may be perpendicular to the outgoing offset edge.
// Rounded joins must not turn that right angle into a false backtrack.
return connector.dot(outgoing) < -4. * SCALED_EPSILON * outgoing.norm();
}
// Orca: sample the outgoing perimeter without copying or clipping its full loop.
static Point sample_polyline_at_distance(const Polyline &polyline, double target)
{
assert(! polyline.points.empty());
Point result = polyline.first_point();
for (size_t i = 1; i < polyline.points.size() && target > 0.; ++i) {
const Vec2d segment = (polyline.points[i] - result).cast<double>();
const double length = segment.norm();
if (length <= SCALED_EPSILON)
continue;
if (target <= length)
return (result.cast<double>() + segment * (target / length)).cast<coord_t>();
target -= length;
result = polyline.points[i];
}
return result;
}
// Orca: convert an executable path into Wipe::wipe()'s stored representation.
// The first point is a dummy replaced by the actual nozzle position, while the
// remaining points are clipped to the configured wipe distance.
static bool store_wipe_path(Polyline &destination, Point seam_start,
Polyline actual_path, double max_wipe_length)
{
if (actual_path.points.size() < 2 || max_wipe_length <= SCALED_EPSILON)
return false;
const double actual_length = actual_path.length();
if (actual_length <= SCALED_EPSILON)
return false;
if (actual_length - max_wipe_length > SCALED_EPSILON)
actual_path.clip_end(actual_length - max_wipe_length);
if (actual_path.points.size() < 2)
return false;
for (size_t i = 1; i < actual_path.points.size(); ++i)
if (actual_path.points[i - 1] == actual_path.points[i])
return false;
Polyline stored_path;
stored_path.points.reserve(actual_path.points.size());
stored_path.points.push_back(seam_start);
stored_path.points.insert(stored_path.points.end(), actual_path.points.begin() + 1, actual_path.points.end());
stored_path.reset_to_linear_move();
destination = std::move(stored_path);
return true;
}
bool offset_wipe_path(Polyline &polyline, Point seam_start, Point seam_end, Point wipe_start,
int dir, double offset_dist, double max_wipe_length)
{
assert(dir == +1 || dir == -1);
assert(offset_dist > 0);
if (polyline.points.empty() || polyline.first_point() != seam_start ||
max_wipe_length <= SCALED_EPSILON)
return false;
const Polyline original = polyline;
const double original_length = original.length();
if (original_length <= SCALED_EPSILON)
return false;
double source_length = std::min(original_length, max_wipe_length);
for (;;) {
Polyline source = original;
const double clip_distance = original_length - source_length;
if (clip_distance > SCALED_EPSILON)
source.clip_end(clip_distance);
Points wrapped_source;
wrapped_source.reserve(source.points.size() + 1);
if (seam_start == seam_end) {
// Orca: the stored loop is open at seam_start even when the seam gap is
// zero. Prepend the closing edge so build_offset_polyline() creates
// the proper join between that edge and the first outgoing edge,
// instead of leaving the first offset point on the closing wall.
size_t closing_index = original.points.size();
while (closing_index > 0 && original.points[closing_index - 1] == seam_start)
--closing_index;
if (closing_index == 0)
return false; // Orca: the entire path is a single point.
wrapped_source.push_back(original.points[closing_index - 1]);
} else {
// Orca: use the unextruded seam-gap edge to determine the incoming
// direction at the seam. Its offset is construction geometry only;
// wiping along it would create a Z-shaped detour before the outgoing
// perimeter offset.
wrapped_source.push_back(seam_end);
}
wrapped_source.insert(wrapped_source.end(), source.points.begin(), source.points.end());
Points offset_points;
size_t first_join_index = 0;
if (! build_offset_polyline(wrapped_source, dir, offset_dist, offset_points, first_join_index) ||
first_join_index == 0 || first_join_index >= offset_points.size())
return false;
// Orca: discard the offset of the prepended edge and, for a bevel, its
// incoming endpoint. The executable wipe starts at the seam join and
// then follows only the already printed outgoing perimeter.
offset_points.erase(offset_points.begin(), offset_points.begin() + first_join_index);
Polyline actual_path;
actual_path.points.reserve(offset_points.size() + 1);
actual_path.points.push_back(wipe_start);
actual_path.points.insert(actual_path.points.end(), offset_points.begin(), offset_points.end());
// A loop pre-move may advance past an otherwise valid offset join.
// Enter at the nozzle's projection instead of returning to the join.
// Do not repair a join that already backtracks across the seam gap;
// the caller must still validate wall crossings, material side and support.
if (seam_start != seam_end && wipe_start != seam_start && wipe_start != seam_end &&
starts_by_backtracking(actual_path, wipe_start) && ! starts_by_backtracking(actual_path, seam_end)) {
size_t entry = 1;
while (entry + 1 < actual_path.points.size()) {
const Vec2d edge = (actual_path.points[entry + 1] - actual_path.points[entry]).cast<double>();
const double projection = (wipe_start - actual_path.points[entry]).cast<double>().dot(edge);
if (projection <= 0.)
break;
if (projection < edge.squaredNorm()) {
actual_path.points[entry] = (actual_path.points[entry].cast<double>() +
edge * (projection / edge.squaredNorm())).cast<coord_t>();
break;
}
++entry;
}
actual_path.points.erase(actual_path.points.begin() + 1, actual_path.points.begin() + entry);
}
if (seam_start != seam_end && wipe_start == seam_end &&
starts_by_backtracking(actual_path, wipe_start)) {
// Orca: a wide seam gap or a sharp cusp may put the first miter
// behind its outgoing edge. Reject this offset candidate so the
// caller can try the opposite side or the translated fallback.
return false;
}
const double actual_length = actual_path.length();
const bool source_exhausted = original_length - source_length <= SCALED_EPSILON;
if (actual_length + SCALED_EPSILON < max_wipe_length && ! source_exhausted) {
// Orca: offset joins may shorten the path at every corner. Grow the
// source until the executable offset path, not a heuristic source
// margin, reaches the configured wipe distance.
const double deficit = max_wipe_length - actual_length;
const double next_length = std::min(original_length,
source_length + std::max(deficit, 2. * SCALED_EPSILON));
if (next_length - source_length <= SCALED_EPSILON)
return false;
source_length = next_length;
continue;
}
// Orca: unlike an extruded offset, a wipe may safely cross or retrace the
// just-printed perimeter. The caller validates the complete executable
// path against current and earlier printed perimeter geometry.
return store_wipe_path(polyline, seam_start, std::move(actual_path), max_wipe_length);
}
}
static bool translated_wipe_path(Polyline &polyline, Point seam_start, Point seam_end, Point wipe_start,
const Vec2d &translation, double max_wipe_length)
{
if (translation.norm() <= SCALED_EPSILON || max_wipe_length <= SCALED_EPSILON)
return false;
const Polyline original = polyline;
Polyline actual_path;
actual_path.points.reserve(original.points.size() + 2);
actual_path.points.push_back(wipe_start);
const auto append_translated = [&actual_path, &translation](const Point &point) {
const Point translated = (point.cast<double>() + translation).cast<coord_t>();
if (translated != actual_path.points.back())
actual_path.points.push_back(translated);
};
// Orca: translate the seam join directly. Translating seam_end and then
// following the unextruded gap back to seam_start makes the wipe double
// back whenever a gap ends near a sharp corner.
append_translated(seam_start);
for (const Point &point : original.points)
append_translated(point);
if (seam_start != seam_end && wipe_start == seam_end &&
starts_by_backtracking(actual_path, wipe_start)) {
// Orca: at a wide gap next to a cusp, the translated seam join may
// lie behind the outgoing edge. Prefer a shorter local inward move
// at the actual extrusion end over a longer lightning-shaped wipe.
actual_path.points.resize(1);
append_translated(seam_end);
}
return store_wipe_path(polyline, seam_start, std::move(actual_path), max_wipe_length);
}
// A segment whose endpoints lie within one line's distance capsule is fully
// supported, since that capsule is convex. Subdivide only when support changes
// between lines; fixed-distance sampling can miss an unsupported gap.
static bool segment_is_supported(Point start, Point end,
const AABBTreeLines::LinesDistancer<Line> &distancer,
double max_distance)
{
const Point midpoint = ((start.cast<double>() + end.cast<double>()) * 0.5).cast<coord_t>();
const auto [distance, line_index, nearest] = distancer.distance_from_lines_extra<false>(midpoint);
if (distance > max_distance)
return false;
const Line &line = distancer.get_line(line_index);
if (line.distance_to(start) <= max_distance && line.distance_to(end) <= max_distance)
return true;
if (distancer.distance_from_lines<false>(start) > max_distance ||
distancer.distance_from_lines<false>(end) > max_distance)
return false;
// Conservatively reject an unresolved transition at coordinate precision.
if ((end - start).cast<double>().norm() <= SCALED_EPSILON)
return false;
return segment_is_supported(start, midpoint, distancer, max_distance) &&
segment_is_supported(midpoint, end, distancer, max_distance);
}
std::optional<double> wipe_path_support_score(
const Polyline &polyline, Point wipe_start,
const AABBTreeLines::LinesDistancer<Line> &target_distancer,
const AABBTreeLines::LinesDistancer<Line> &all_support_distancer,
double max_distance)
{
if (polyline.points.size() < 2 || target_distancer.get_lines().empty() || max_distance <= 0)
return std::nullopt;
// Orca: require a local neighbour, not merely an earlier perimeter elsewhere in
// the region. At a convex corner, an inner wall's miter is farther from the
// external seam than its normal wall spacing, so allow the same bounded miter
// reach as the offset construction without accepting a remote island.
if (target_distancer.distance_from_lines<false>(wipe_start) >
miter_limit * max_distance + 4. * SCALED_EPSILON)
return std::nullopt;
Point previous = wipe_start;
for (size_t i = 1; i < polyline.points.size(); ++i) {
// Orca: a tightly curved inward path may cross back over the current wall.
// This is safe for a non-extruding wipe as long as the complete path
// remains over current or earlier printed perimeter geometry.
// Allow the same coordinate-rounding tolerance at every point, including
// the actual start substituted for the stored sentinel.
if (! segment_is_supported(previous, polyline.points[i], all_support_distancer,
max_distance + 4. * SCALED_EPSILON))
return std::nullopt;
previous = polyline.points[i];
}
// Orca: decide direction at the seam. Scoring the complete path may select
// the wrong initial side when two contours converge and the later prefix
// happens to run closer to unrelated support.
return target_distancer.distance_from_lines<false>(polyline.points[1]);
}
static bool initial_connector_is_clear(
const Polyline &polyline, Point wipe_start, Point seam_start,
AABBTreeLines::LinesDistancer<Line> &current_perimeter_distancer,
double contact_tolerance)
{
if (polyline.points.size() < 2 || polyline.points[1] == wipe_start)
return false;
// Orca: without a seam gap, the connector necessarily starts at the wall
// and a self-touching cusp may share that same endpoint on several edges.
if (seam_start == wipe_start)
return true;
const Line connector(wipe_start, polyline.points[1]);
const auto intersections = current_perimeter_distancer.intersections_with_line<false>(connector);
for (const auto &intersection : intersections) {
if ((intersection.first - wipe_start).cast<double>().norm() > contact_tolerance)
return false;
}
Point closest;
// Orca: integer offset joins may miss the exact seam-start coordinate by
// a few microns. Treat a close pass through that point as retracing the
// external wall, but keep the unavoidable contact at the actual start.
if (connector.distance_to_squared(seam_start, &closest) <= contact_tolerance * contact_tolerance &&
(closest - wipe_start).cast<double>().norm() > contact_tolerance)
return false;
return true;
}
static std::optional<Vec2d> support_offset_at_start(
const Polyline &source, Point local_origin, bool disambiguate_branch,
AABBTreeLines::LinesDistancer<Line> &support_distancer,
double max_support_distance)
{
if (source.points.size() < 2)
return std::nullopt;
// Orca: a nonzero gap may put the seam beside the wrong branch of a cusp.
// Sample farther along the path to identify its actual neighbouring wall.
const Point support_query = disambiguate_branch ?
sample_polyline_at_distance(source, 2. * max_support_distance) : source.first_point();
const auto nearest_result = support_distancer.distance_from_lines_extra<false>(support_query);
const Line &nearest_line = support_distancer.get_line(std::get<1>(nearest_result));
Vec2d sampled_offset = std::get<2>(nearest_result) - support_query.cast<double>();
if (disambiguate_branch) {
// Orca: an endpoint projection also contains distance along the support
// segment. Remove that tangent component before comparing wall sides.
const Vec2d support_edge = (nearest_line.b - nearest_line.a).cast<double>();
if (support_edge.norm() > SCALED_EPSILON) {
const Vec2d support_tangent = support_edge.normalized();
sampled_offset -= support_tangent * sampled_offset.dot(support_tangent);
}
}
if (sampled_offset.norm() <= SCALED_EPSILON)
return std::nullopt;
if (! disambiguate_branch)
return sampled_offset;
// Orca: find the local point on the same material-side branch. Using the
// sampled point itself would add the distance already travelled along the
// perimeter and turn a normal transition into a long diagonal move.
const Vec2d sampled_direction = sampled_offset.normalized();
Vec2d local_offset = sampled_offset;
double best_local_score = std::numeric_limits<double>::infinity();
for (size_t line_index : support_distancer.all_lines_in_radius(
local_origin, 2. * max_support_distance + 4. * SCALED_EPSILON)) {
Point local_support;
const Line &line = support_distancer.get_line(line_index);
const double distance_squared = line.distance_to_squared(local_origin, &local_support);
const Vec2d candidate_offset = local_support.cast<double>() - local_origin.cast<double>();
const double candidate_distance = std::sqrt(distance_squared);
if (candidate_distance <= SCALED_EPSILON)
continue;
const double alignment = candidate_offset.normalized().dot(sampled_direction);
if (alignment < min_support_alignment)
continue;
const double score = candidate_distance / alignment;
if (score < best_local_score) {
best_local_score = score;
local_offset = candidate_offset;
}
}
return local_offset;
}
static double executable_path_length(const Polyline &stored_path, Point wipe_start)
{
if (stored_path.points.size() < 2)
return 0.;
// Orca: points[0] is the storage sentinel, so measure the first segment
// from the actual nozzle position and the remaining stored segments normally.
double length = (stored_path.points[1] - wipe_start).cast<double>().norm();
for (size_t index = 2; index < stored_path.points.size(); ++index)
length += (stored_path.points[index] - stored_path.points[index - 1]).cast<double>().norm();
return length;
}
static Lines material_side_support_lines(const Polyline &path, Point seam, int preferred_dir,
const Lines &support_lines)
{
if (path.points.size() < 4 || path.first_point() != path.last_point())
return {};
// Orca: the bisector of the incoming and outgoing material-side normals is
// a local side test that remains valid for globally self-touching Arachne
// contours. Ignore repeated seam points when obtaining both tangents.
const auto outgoing_it = std::find_if(
path.points.begin() + 1, path.points.end(), [seam](const Point &point) { return point != seam; });
const auto incoming_it = std::find_if(
path.points.rbegin() + 1, path.points.rend(), [seam](const Point &point) { return point != seam; });
if (outgoing_it == path.points.end() || incoming_it == path.points.rend())
return {};
const Vec2d outgoing = (*outgoing_it - seam).cast<double>().normalized();
const Vec2d incoming = (seam - *incoming_it).cast<double>().normalized();
const Vec2d material_direction =
(Vec2d(-outgoing.y(), outgoing.x()) + Vec2d(-incoming.y(), incoming.x())) * preferred_dir;
if (material_direction.norm() <= EPSILON)
return {};
Lines result;
result.reserve(support_lines.size());
for (const Line &line : support_lines) {
Point closest;
line.distance_to_squared(seam, &closest);
if ((closest - seam).cast<double>().dot(material_direction) > SCALED_EPSILON)
result.push_back(line);
}
return result;
}
bool wipe_path_stays_on_material_side(
const Polyline &path, Point path_start, const Vec2d &support_direction,
const AABBTreeLines::LinesDistancer<Line> &target_perimeter_distancer,
const AABBTreeLines::LinesDistancer<Line> &current_perimeter_distancer,
double effective_offset, bool require_clearance)
{
if (path.points.size() < 2 || support_direction.norm() <= EPSILON ||
target_perimeter_distancer.get_lines().empty() || current_perimeter_distancer.get_lines().empty() ||
effective_offset <= SCALED_EPSILON)
return false;
const Vec2d initial_offset = (path.points[1] - path_start).cast<double>();
if (initial_offset.norm() <= SCALED_EPSILON ||
initial_offset.normalized().dot(support_direction.normalized()) < min_support_alignment)
return false;
// Orca: after the connector has left the extrusion endpoint, an inward
// offset must retain most of its requested clearance from the current
// external wall. Otherwise a tight turn may send an initially correct path
// back onto that wall, or make the opposite-side candidate look supported.
const double clearance_tolerance = wipe_tolerance(effective_offset, 0.25);
const double minimum_clearance = effective_offset - clearance_tolerance;
const Lines &lines = current_perimeter_distancer.get_lines();
const auto left_normal = [](const Line &line) -> Vec2d {
const Vec2d edge = (line.b - line.a).cast<double>();
if (edge.norm() <= SCALED_EPSILON)
return Vec2d::Zero();
return Vec2d(-edge.y(), edge.x()).normalized();
};
const auto on_material_side = [&](const Point &point, bool check_clearance) {
const auto [distance, line_index, nearest] =
current_perimeter_distancer.distance_from_lines_extra<false>(point);
if (line_index >= lines.size())
return false;
const Line &line = lines[line_index];
Vec2d normal = left_normal(line);
// At a shared vertex use both incident edges, so the result does not
// depend on which equally close edge the AABB query happens to return.
const Line &previous = lines[(line_index + lines.size() - 1) % lines.size()];
const Line &next = lines[(line_index + 1) % lines.size()];
if ((nearest - line.a.cast<double>()).norm() <= SCALED_EPSILON && previous.b == line.a)
normal += left_normal(previous);
if ((nearest - line.b.cast<double>()).norm() <= SCALED_EPSILON && next.a == line.b)
normal += left_normal(next);
if (normal.norm() <= EPSILON)
return false;
// An open or self-touching wall has no reliable polygon-wide sign.
// Orient its local normal toward the neighbouring printed inner wall,
// then test the candidate on that side at every sample.
normal.normalize();
const Point wall_point = nearest.cast<coord_t>();
const Vec2d support_point = std::get<2>(
target_perimeter_distancer.distance_from_lines_extra<false>(wall_point));
const double support_side = (support_point - nearest).dot(normal);
if (std::abs(support_side) <= 4. * SCALED_EPSILON)
return false;
const double side = (point.cast<double>() - nearest).dot(normal) * (support_side > 0. ? 1. : -1.);
return side >= -4. * SCALED_EPSILON &&
(! check_clearance || distance + 4. * SCALED_EPSILON >= minimum_clearance);
};
Point previous = path.points[1];
if (! on_material_side(previous, require_clearance))
return false;
for (size_t index = 2; index < path.points.size(); ++index) {
const Vec2d segment = (path.points[index] - previous).cast<double>();
const size_t samples = std::max<size_t>(1, size_t(std::ceil(segment.norm() / effective_offset)));
for (size_t sample = 1; sample <= samples; ++sample) {
const Point point = (previous.cast<double>() +
segment * (double(sample) / double(samples))).cast<coord_t>();
if (! on_material_side(point, require_clearance))
return false;
}
previous = path.points[index];
}
return true;
}
bool offset_wipe_path_toward_support(Polyline &polyline, Point seam_start, Point seam_end, Point wipe_start,
int preferred_dir, double offset_dist, double max_wipe_length,
const Lines &target_perimeter_lines, const Lines &printed_perimeter_lines,
const Lines &current_perimeter_lines,
double max_support_distance)
{
assert(preferred_dir == +1 || preferred_dir == -1);
if (polyline.points.size() < 2 || target_perimeter_lines.empty() || current_perimeter_lines.empty() ||
offset_dist <= SCALED_EPSILON ||
max_wipe_length <= SCALED_EPSILON || max_support_distance <= SCALED_EPSILON)
return false;
Lines material_support_lines;
const Lines *candidate_support_lines = &target_perimeter_lines;
if (seam_start == seam_end) {
// Orca: another contour may have a geometrically closer inner wall on
// this loop's air side. Restrict zero-gap support using the local seam
// normals before choosing the nearest wall.
material_support_lines = material_side_support_lines(
polyline, seam_start, preferred_dir, target_perimeter_lines);
if (material_support_lines.empty())
return false;
candidate_support_lines = &material_support_lines;
}
AABBTreeLines::LinesDistancer<Line> support_distancer(*candidate_support_lines);
const std::optional<Vec2d> support_offset = support_offset_at_start(
polyline, seam_end, seam_start != seam_end,
support_distancer, max_support_distance);
if (! support_offset)
return false;
const Vec2d toward_support = *support_offset;
const double local_support_distance = toward_support.norm();
const double effective_offset = std::min(offset_dist, local_support_distance);
if (effective_offset <= SCALED_EPSILON)
return false;
const Vec2d support_direction = toward_support / local_support_distance;
// Orca: every candidate is validated against the same generated geometry.
// Build these AABB trees once per loop instead of rebuilding them for each
// preferred, alternate, translated, direct, or reversed candidate.
Lines all_support_lines = printed_perimeter_lines;
all_support_lines.insert(all_support_lines.end(), current_perimeter_lines.begin(), current_perimeter_lines.end());
AABBTreeLines::LinesDistancer<Line> all_support_distancer(std::move(all_support_lines));
AABBTreeLines::LinesDistancer<Line> current_perimeter_distancer(current_perimeter_lines);
// Orca: allow only the contact needed to leave the extrusion endpoint. A
// connector that meets the current wall again is a seam-gap retrace, even
// if the rest of the non-extruding wipe remains over printed material.
const double contact_tolerance = wipe_tolerance(effective_offset);
struct Candidate {
Polyline path;
// Orca: support score chooses the material-side path; length is used
// only to replace a corner-truncated path with the reverse fallback.
double support_score;
double path_length;
};
// Direction and wall contact have different origins after a loop pre-move.
// Keep the construction's wall endpoint for intersection checks even when
// the candidate's direction must be checked from the current nozzle position.
const auto validate_candidate = [&](Polyline path, Point path_start, Point direction_start,
double path_contact_tolerance,
const Vec2d &candidate_support_direction,
double candidate_offset,
bool require_clearance = true) -> std::optional<Candidate> {
// Orca: backtracking indicates a wrong join only across a nonzero gap.
// A closed zero-gap offset may initially turn back at its miter while
// still remaining on the supported material side of the perimeter.
const bool backtracks_across_gap = seam_start != seam_end && starts_by_backtracking(path, wipe_start);
// At a clipped corner another branch of the current wall may be closer
// than the requested offset. Preserve the zero-gap clearance rule, but
// check direction and local material side independently for every gap.
const bool material_side = wipe_path_stays_on_material_side(
path, direction_start, candidate_support_direction,
support_distancer, current_perimeter_distancer, candidate_offset,
require_clearance && seam_start == seam_end);
const bool connector_clear = initial_connector_is_clear(
path, wipe_start, path_start, current_perimeter_distancer, path_contact_tolerance);
if (backtracks_across_gap || ! material_side || ! connector_clear)
return std::nullopt;
const std::optional<double> score = wipe_path_support_score(
path, wipe_start, support_distancer, all_support_distancer, max_support_distance);
if (! score)
return std::nullopt;
const double path_length = executable_path_length(path, wipe_start);
return Candidate{std::move(path), *score, path_length};
};
const auto offset_candidate = [&](int dir) -> std::optional<Candidate> {
Polyline path = polyline;
if (! offset_wipe_path(path, seam_start, seam_end, wipe_start, dir,
effective_offset, max_wipe_length))
return std::nullopt;
return validate_candidate(std::move(path), seam_start, seam_start,
contact_tolerance, support_direction, effective_offset);
};
std::optional<Candidate> preferred = offset_candidate(preferred_dir);
std::optional<Candidate> alternate = offset_candidate(-preferred_dir);
// Orca: forward and reverse fallbacks share the same clamping, translation,
// connector tolerance, and complete-path validation.
const auto translated_candidate = [&](Polyline source, Point source_start, Point source_end,
const Vec2d &candidate_support_offset) -> std::optional<Candidate> {
const double support_distance = candidate_support_offset.norm();
const double candidate_offset = std::min(offset_dist, support_distance);
if (candidate_offset <= SCALED_EPSILON)
return std::nullopt;
const Vec2d candidate_translation = candidate_support_offset * (candidate_offset / support_distance);
if (! translated_wipe_path(source, source_start, source_end, wipe_start,
candidate_translation, max_wipe_length))
return std::nullopt;
const double candidate_tolerance = wipe_tolerance(candidate_offset);
return validate_candidate(std::move(source), source_start, source_start, candidate_tolerance,
candidate_support_offset / support_distance, candidate_offset);
};
std::optional<Candidate> translated = translated_candidate(polyline, seam_start, seam_end, toward_support);
// Orca: if every full-length construction folds back onto the external
// wall, retain a short direct inward move instead of accepting an outward
// candidate or falling back to the standard wipe along the outer wall.
const auto direct_candidate = [&](Point origin, const Vec2d &candidate_support_offset) -> std::optional<Candidate> {
const double support_distance = candidate_support_offset.norm();
const double candidate_offset = std::min(offset_dist, support_distance);
if (candidate_offset <= SCALED_EPSILON)
return std::nullopt;
const Vec2d direction = candidate_support_offset / support_distance;
const Point destination = (origin.cast<double>() + direction * candidate_offset).cast<coord_t>();
if (destination == wipe_start)
return std::nullopt;
Polyline path;
if (! store_wipe_path(path, seam_start, Polyline{wipe_start, destination}, max_wipe_length))
return std::nullopt;
const double candidate_tolerance = wipe_tolerance(candidate_offset);
// Check the executed direction from the nozzle after any loop pre-move,
// but retain the wall origin for the connector's intersection checks.
return validate_candidate(std::move(path), origin, wipe_start,
candidate_tolerance, direction, candidate_offset, false);
};
std::optional<Candidate> direct = direct_candidate(seam_end, toward_support);
const double length_margin = wipe_tolerance(max_wipe_length);
std::optional<Candidate> reversed;
if (seam_start != seam_end && polyline.last_point() == seam_end) {
// Orca: when a large gap straddles a sharp corner, connecting the
// extrusion end to the forward offset may either reverse or leave only
// a short local move. The already printed incoming wall is equally safe:
// follow it backwards and determine its own material-side support.
Polyline reversed_source = polyline;
reversed_source.reverse();
const std::optional<Vec2d> reversed_support_offset = support_offset_at_start(
reversed_source, seam_end, true, support_distancer, max_support_distance);
if (reversed_support_offset) {
reversed = translated_candidate(reversed_source, seam_end, seam_end, *reversed_support_offset);
// A translated reverse path can backtrack or leave the material on
// a curved wall. Offset the incoming wall itself when translation
// cannot supply a complete wipe, retaining all candidate checks.
if (! reversed || reversed->path_length + length_margin < max_wipe_length) {
const double reverse_offset = std::min(offset_dist, reversed_support_offset->norm());
if (reverse_offset > SCALED_EPSILON &&
offset_wipe_path(reversed_source, seam_end, seam_start, wipe_start,
-preferred_dir, reverse_offset, max_wipe_length)) {
reversed_source.points.front() = seam_start;
auto candidate = validate_candidate(std::move(reversed_source), seam_end, seam_end,
wipe_tolerance(reverse_offset), reversed_support_offset->normalized(), reverse_offset);
if (candidate && (! reversed ||
(candidate->path_length > reversed->path_length + length_margin &&
candidate->support_score <= reversed->support_score + wipe_tolerance(reverse_offset))))
reversed = std::move(candidate);
}
}
}
}
// Orca: conventional offsets at a narrow cusp may form a bevel across the
// cusp. Candidates pointing away from the actual inner wall are rejected
// during validation; among the remaining paths, prefer the one whose first
// point is materially closer to that wall.
const double direction_change_margin = wipe_tolerance(effective_offset);
std::optional<Candidate> selected = std::move(preferred);
if (translated) {
if (! selected || translated->support_score + direction_change_margin < selected->support_score)
selected = std::move(translated);
}
if (! selected)
selected = std::move(direct);
// Prefer a direct inward move when the normal offset cannot be used.
// An alternate offset is eligible only after the same material-side checks.
if (! selected)
selected = std::move(alternate);
// Orca: prefer a complete reverse wipe over a forward fallback that had to
// stop at the corner. Equal-length paths keep the normal forward behavior.
if (reversed && (! selected ||
(reversed->path_length > selected->path_length + length_margin &&
reversed->support_score <= selected->support_score + direction_change_margin)))
selected = std::move(reversed);
if (! selected)
return false;
polyline = std::move(selected->path);
return true;
}
std::optional<Point> wipe_on_loops_destination(const ExtrusionPaths &paths, double nozzle_diam_scaled,
bool is_ccw, bool is_hole)
{
assert(!paths.empty());
assert(nozzle_diam_scaled > 0);
if (paths.empty() || nozzle_diam_scaled <= 0)
return std::nullopt;
// Orca: clamp sample distance to L/4 so forward/backward samples cannot meet.
double total_length = 0.;
for (const ExtrusionPath &path : paths)
total_length += path.length();
const double sample_distance = std::min(nozzle_diam_scaled, total_length * 0.25);
Point a = sample_path_at_distance(paths, true, sample_distance);
Point b = sample_path_at_distance(paths, false, sample_distance);
const Point seam_start = paths.front().first_point();
// Orca: skip the inward move for degenerate geometry.
if (a == b || a == seam_start || b == seam_start)
return std::nullopt;
const bool reverse_turn = is_hole == is_ccw;
if (reverse_turn)
std::swap(a, b);
double angle = seam_start.ccw_angle(a, b) / 3;
// Orca: reject degenerate angles near 0 or 2π.
static constexpr double angle_epsilon = 0.01;
if (angle < angle_epsilon || angle > 2 * PI / 3 - angle_epsilon)
return std::nullopt;
if (reverse_turn)
angle *= -1;
Point pt = sample_path_at_distance(paths, true, std::min(0.2 * nozzle_diam_scaled, sample_distance));
pt.rotate(angle, seam_start);
return pt;
}
} // namespace Slic3r
+96
View File
@@ -0,0 +1,96 @@
#pragma once
#include <optional>
#include "../ExtrusionEntity.hpp"
#include "../Polyline.hpp"
#include "../Line.hpp"
namespace Slic3r {
// Printed prefix of one region's perimeter sequence. Append each entity only
// after extrusion; later walls and other regions cannot support an inward wipe.
struct WipeInwardSupport {
Lines printed_lines;
Lines inner_lines;
void append(const ExtrusionEntity &entity);
};
namespace AABBTreeLines {
template <typename LineType> class LinesDistancer;
}
// Orca: sample a point at a given distance along ExtrusionPaths, walking
// across segment boundaries. forward=true walks from paths.front, false from
// paths.back. For tiny loops the walk stops early and returns the last
// reachable point. Returns the start point if target is zero.
// Precondition: paths must be non-empty.
Point sample_path_at_distance(const ExtrusionPaths &paths, bool forward, double target);
// Orca: return the side of the printed path on which the material lies.
// dir +1 is left and -1 is right, matching the offset-builder convention.
int wipe_offset_direction(bool is_ccw, bool is_hole);
// Orca: atomically offset a stored wipe path. The seam-gap or closing edge
// determines the join with the first outgoing perimeter edge, but its offset
// is not part of the executable wipe. Only the prefix needed by Wipe::wipe()
// is offset. Returns false and leaves polyline unchanged if that path cannot
// be constructed without degenerate segments. This only constructs a candidate;
// offset_wipe_path_toward_support() validates its support, material side and
// connector before accepting it. The first stored point
// remains a dummy preserving Wipe::wipe()'s convention of skipping points[0].
// Precondition: polyline starts at seam_start, dir is +1 or -1, and
// offset_dist > 0. A non-positive max_wipe_length returns false.
bool offset_wipe_path(Polyline &polyline, Point seam_start, Point seam_end, Point wipe_start,
int dir, double offset_dist, double max_wipe_length);
// Orca: score a candidate's first destination by distance to the target inner
// walls. Return nullopt if no target wall is near wipe_start or any executable
// segment lacks support. target_distancer contains eligible earlier walls;
// all_support_distancer includes the current wall and all earlier walls.
// The stored first point is a dummy: the first segment starts at wipe_start.
// This checks support only; material-side and connector checks belong to
// offset_wipe_path_toward_support(). Trees are reused across its candidates.
std::optional<double> wipe_path_support_score(
const Polyline &polyline, Point wipe_start,
const AABBTreeLines::LinesDistancer<Line> &target_distancer,
const AABBTreeLines::LinesDistancer<Line> &all_support_distancer,
double max_distance);
// Validate the initial inward direction and the local material side along the
// executable path, using the inner wall to orient the open current wall's
// normals. Clearance is optional for clipped corners and short direct fallbacks;
// the material-side check is mandatory. The straight connector is checked by
// its initial direction and separately by support and intersection validation.
// path_start is the construction origin; points[0] is only a storage sentinel.
bool wipe_path_stays_on_material_side(
const Polyline &path, Point path_start, const Vec2d &support_direction,
const AABBTreeLines::LinesDistancer<Line> &target_perimeter_distancer,
const AABBTreeLines::LinesDistancer<Line> &current_perimeter_distancer,
double effective_offset, bool require_clearance);
// Orca: identify the adjacent inner perimeter from the outgoing wall, excluding
// support on the air side of a closed zero-gap loop. Clamp the requested offset
// to the distance from the seam end to that support, then select the safest
// supported offset or translated path. If a wide seam gap at a corner truncates
// every forward candidate, the incoming printed wall may be followed backwards
// instead. All earlier printed perimeters still participate in the complete-path
// safety check. This handles converging, locally ambiguous, or self-touching
// contours whose global winding alone does not identify the material side.
// Returns false and leaves polyline unchanged when no candidate is supported.
// Precondition: preferred_dir is +1 or -1. Distances must be positive.
bool offset_wipe_path_toward_support(Polyline &polyline, Point seam_start, Point seam_end, Point wipe_start,
int preferred_dir, double offset_dist, double max_wipe_length,
const Lines &target_perimeter_lines, const Lines &printed_perimeter_lines,
const Lines &current_perimeter_lines,
double max_support_distance);
// Orca: compute the inward destination point for wipe_on_loops, or
// std::nullopt when the geometry is degenerate (tiny loop, coincident samples,
// angle near 0 or 2π). Returns the rotated destination or nullopt to skip the
// inward move entirely.
// Precondition: paths non-empty, nozzle_diam_scaled > 0.
std::optional<Point> wipe_on_loops_destination(const ExtrusionPaths &paths, double nozzle_diam_scaled,
bool is_ccw, bool is_hole);
} // namespace Slic3r
+127 -19
View File
@@ -25,6 +25,30 @@ static constexpr int arc_fit_size = 20;
enum class LimitFlow { None, LimitPrintFlow, LimitRammingFlow, LimitRammingFlowNC};//nc:nozzle change
static const std::map<float, float> nozzle_diameter_to_nozzle_change_width{{0.2f, 0.5f}, {0.4f, 1.0f}, {0.6f, 1.2f}, {0.8f, 1.4f}};
bool wipe_tower_sparse_layers_skipped(const PrintConfig &config)
{
return config.wipe_tower_no_sparse_layers.value && config.timelapse_type.value != TimelapseType::tlSmooth &&
! config.enable_wrapping_detection.value;
}
bool wipe_tower_layer_is_sparse(const std::vector<WipeTower::ToolChangeResult> &layer_tool_changes)
{
return layer_tool_changes.size() == 1 && layer_tool_changes.front().initial_tool == layer_tool_changes.front().new_tool;
}
std::vector<float> compute_compacted_wipe_tower_z(const std::vector<std::vector<WipeTower::ToolChangeResult>> &tool_changes,
float base_z)
{
std::vector<float> tower_z(tool_changes.size(), base_z);
float last = base_z;
for (size_t i = 0; i < tool_changes.size(); ++i) {
if (! tool_changes[i].empty() && ! wipe_tower_layer_is_sparse(tool_changes[i]))
last += tool_changes[i].front().layer_height;
tower_z[i] = last;
}
return tower_z;
}
inline float align_round(float value, float base)
{
return std::round(value / base) * base;
@@ -1349,7 +1373,7 @@ public:
// flavor it reaches understands, not the zero dwell the other flavors flush with.
buffer += "M400\n";
buffer += "M104";
if (target_extruder != -1)
if (target_extruder != -1 && target_extruder < int(m_physical_extruder_map.size()))
buffer += (" T" + std::to_string(m_physical_extruder_map[target_extruder]));
buffer += " S" + std::to_string(target_temp) + " N0"; // N0 means the gcode is generated by slicer
if (!comment.empty()) buffer += " ;" + comment;
@@ -1361,7 +1385,7 @@ public:
WipeTowerWriter &format_line_M109(int target_temp, int target_extruder, const std::string &comment = std::string())
{
std::string buffer = "M109";
if (target_extruder != -1)
if (target_extruder != -1 && target_extruder < int(m_physical_extruder_map.size()))
buffer += (" T" + std::to_string(m_physical_extruder_map[target_extruder]));
buffer += " S" + std::to_string(target_temp) + " N0"; // N0 means the gcode is generated by slicer
if (!comment.empty()) buffer += " ;" + comment;
@@ -1630,6 +1654,94 @@ float WipeTower::get_auto_brim_by_height(float max_height) {
return 8.f;
}
float WipeTower::estimate_brim_real_width(float brim_width, float nozzle_diameter, float first_layer_height, bool type2)
{
if (brim_width <= 0.f)
return brim_width;
const float spacing = nozzle_diameter * 1.25f - first_layer_height * float(1. - M_PI_4); // Width_To_Nozzle_Ratio
if (spacing <= EPSILON)
return brim_width;
const int loops_num = int((brim_width + spacing / 2.f) / spacing);
return loops_num * spacing + (type2 ? 0.f : spacing / 2.f);
}
float WipeTower::get_wrapping_detection_depth()
{
return float(wrapping_wipe_tower_depth);
}
float WipeTower::nozzle_change_perimeter_width(float nozzle_diameter)
{
auto it = nozzle_diameter_to_nozzle_change_width.find(nozzle_diameter);
return it != nozzle_diameter_to_nozzle_change_width.end() ? it->second : 2.f * nozzle_diameter * 1.25f;
}
float WipeTower::estimate_tower_blocks_depth(const std::vector<PurgeEstimate> &purges, float width, float layer_height, float nozzle_diameter, float extra_spacing)
{
if (purges.empty() || layer_height < EPSILON || nozzle_diameter < EPSILON)
return 0.f;
const float pw = nozzle_diameter * 1.25f; // Width_To_Nozzle_Ratio
const float ncpw = nozzle_change_perimeter_width(nozzle_diameter);
const float line_width = width - 2.f * pw;
if (line_width <= EPSILON)
return 0.f;
// Line cross-section as volume_to_length() sees it; the infill gap stretches the perimeter
// width by the configured ratio and nozzle-change lines keep their own width
// (calc_block_infill_gap).
auto line_area = [layer_height](float w) { return layer_height * (w - layer_height * float(1. - M_PI_4)); };
const float extra_width = (extra_spacing - 1.f) * pw;
const float gap = pw + extra_width;
const float nc_gap = ncpw + extra_width;
// A layer purges into at most (filaments - 1) targets, so a category holding every filament
// never sees its smallest purge (the layer's first filament) in its worst layer.
struct Block { float depth = 0.f; float min_purge = 0.f; size_t filaments = 0; };
std::map<int, Block> blocks;
for (const PurgeEstimate &purge : purges) {
Block &block = blocks[purge.category];
const float purge_depth = std::ceil(purge.prime_volume / line_area(pw) / line_width) * gap;
block.min_purge = block.filaments == 0 ? purge_depth : std::min(block.min_purge, purge_depth);
block.depth += purge_depth;
++block.filaments;
if (purge.filament_change_length > EPSILON) {
// The leaving filament is rammed over the nozzle-change flow, again in whole lines.
const float filament_area = float(M_PI) * purge.filament_diameter * purge.filament_diameter / 4.f;
const float nc_length = purge.filament_change_length * filament_area / line_area(ncpw);
block.depth += std::ceil(nc_length / (width - ncpw - pw)) * nc_gap;
}
}
float depth = pw; // plan_tower_new starts the first block one perimeter width in
for (const auto &[category, block] : blocks)
depth += block.filaments == purges.size() ? block.depth - block.min_purge : block.depth;
return depth;
}
float WipeTower::rib_footprint_side(float width, float depth, float rib_width, float extra_rib_length, float max_height)
{
if (width < EPSILON || depth < EPSILON)
return 0.f;
// Ribs run the diagonal; below the height-based minimum they are extended rather than the
// body, then by the extra length, never ending up shorter than the diagonal.
const float diagonal = std::sqrt(width * width + depth * depth);
float rib_length = diagonal;
if (depth + EPSILON < get_limit_depth_by_height(max_height))
rib_length = std::max(rib_length, get_limit_depth_by_height(max_height) * float(std::sqrt(2.)));
rib_length = std::max(diagonal, rib_length + extra_rib_length);
// Half the extension at each end of the diagonal plus half the rib width, projected onto the axes.
const float rib_w = std::min(rib_width, std::min(width, depth) / 2.f);
const float per_side = ((rib_length - diagonal) / 2.f + rib_w / 2.f) / float(std::sqrt(2.));
return std::max(width, depth) + 2.f * per_side;
}
float WipeTower::estimate_rib_tower_bbox_side(const std::vector<PurgeEstimate> &purges, float width, float layer_height, float nozzle_diameter, float extra_spacing, float rib_width, float extra_rib_length, float max_height)
{
if (purges.empty() || width < EPSILON || layer_height < EPSILON || nozzle_diameter < EPSILON)
return 0.f;
const float pw = nozzle_diameter * 1.25f; // Width_To_Nozzle_Ratio
const float square = align_ceil(std::sqrt(estimate_tower_blocks_depth(purges, width, layer_height, nozzle_diameter, extra_spacing) * width), pw);
const float depth = estimate_tower_blocks_depth(purges, square, layer_height, nozzle_diameter, extra_spacing);
return rib_footprint_side(square, depth, rib_width, extra_rib_length, max_height);
}
Vec2f WipeTower::move_box_inside_polygon(const BoundingBox &box, const Polygons &polygons, coord_t offset)
{
if (polygons.empty()) return Vec2f{0.f, 0.f};
@@ -1791,7 +1903,7 @@ WipeTower::WipeTower(const PrintConfig& config, int plate_idx, Vec3d plate_origi
m_z_pos(0.f),
//m_bridging(float(config.wipe_tower_bridging)),
m_bridging(10.f),
m_no_sparse_layers(config.wipe_tower_no_sparse_layers),
m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(config)),
m_gcode_flavor(config.gcode_flavor),
m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))),
m_current_tool(initial_tool),
@@ -2889,7 +3001,7 @@ WipeTower::ToolChangeResult WipeTower::finish_layer(bool extrude_perimeter, bool
// Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (! m_no_sparse_layers || toolchanges_on_layer)
if (! m_sparse_layers_skipped || toolchanges_on_layer)
if (m_current_tool < m_used_filament_length.size())
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
@@ -2933,7 +3045,7 @@ void WipeTower::plan_toolchange(float z_par, float layer_height_par, unsigned in
if (m_plan.empty() || m_plan.back().z + WT_EPSILON < z_par) // if we moved to a new layer, we'll add it to m_plan first
m_plan.push_back(WipeTowerInfo(z_par, layer_height_par));
if (m_first_layer_idx == size_t(-1) && (! m_no_sparse_layers || old_tool != new_tool))
if (m_first_layer_idx == size_t(-1) && (! m_sparse_layers_skipped || old_tool != new_tool))
m_first_layer_idx = m_plan.size() - 1;
if (old_tool == new_tool) // new layer without toolchanges - we are done
@@ -3221,7 +3333,7 @@ void WipeTower::get_wall_skip_points(const WipeTowerInfo &layer, int layer_id)
if (!cur_block_depth.count(m_filpar[new_filament].category)) cur_block_depth[m_filpar[new_filament].category] = block->start_depth;
process_depth = cur_block_depth[m_filpar[new_filament].category];
if (is_need_ramming(new_filament, old_filament, layer_id)) {
if (m_filament_categories[new_filament] == m_filament_categories[old_filament])
if (get_filament_category(new_filament) == get_filament_category(old_filament))
process_depth += nozzle_change_depth;
else {
if (!cur_block_depth.count(m_filpar[old_filament].category)) {
@@ -3786,7 +3898,7 @@ WipeTower::ToolChangeResult WipeTower::finish_layer_new(bool extrude_perimeter,
// Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (!m_no_sparse_layers || toolchanges_on_layer)
if (!m_sparse_layers_skipped || toolchanges_on_layer)
if (m_current_tool < m_used_filament_length.size())
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
@@ -3896,7 +4008,7 @@ WipeTower::ToolChangeResult WipeTower::finish_block(const WipeTowerBlock &block,
// Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (!m_no_sparse_layers || toolchanges_on_layer)
if (!m_sparse_layers_skipped || toolchanges_on_layer)
if (filament_id < m_used_filament_length.size())
m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length();
@@ -4013,7 +4125,7 @@ WipeTower::ToolChangeResult WipeTower::finish_block_solid(const WipeTowerBlock &
// Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (!m_no_sparse_layers || toolchanges_on_layer)
if (!m_sparse_layers_skipped || toolchanges_on_layer)
if (filament_id < m_used_filament_length.size())
m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length();
@@ -4083,7 +4195,7 @@ void WipeTower::toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinat
}
return time * 60.f;
};
auto estimate_wipe_time = [&estimate_time_kernel, & cleaning_box, &target_speed, &x_to_wipe, &xr, &xl, &dy, &WipeSpeedMap, &solid_tool_toolchange](int begin_line) -> float {
auto estimate_wipe_time = [&estimate_time_kernel, & cleaning_box, &x_to_wipe, &xr, &xl, &dy, &solid_tool_toolchange](int begin_line) -> float {
int n = std::ceil(x_to_wipe / (xr - xl));
if (solid_tool_toolchange) n = (cleaning_box.lu[1] - cleaning_box.ld[1]) / dy;
float total_time = estimate_time_kernel(n);
@@ -4695,7 +4807,7 @@ int WipeTower::get_wall_filament_for_all_layer()
int filament_id = -1;
int filament_count = 0;
for (auto iter = filament_counts.begin(); iter != filament_counts.end(); ++iter) {
if (m_filament_categories[iter->first] == selected_category && iter->second > filament_count) {
if (get_filament_category(iter->first) == selected_category && iter->second > filament_count) {
filament_id = iter->first;
filament_count = iter->second;
}
@@ -4883,12 +4995,8 @@ void WipeTower::generate_new(std::vector<std::vector<WipeTower::ToolChangeResult
}
}
if (!has_inserted) {
if (finish_block_tcr.gcode.empty())
finish_block_tcr = finish_block_tcr;
else
finish_layer_tcr = merge_tcr(finish_layer_tcr, finish_block_tcr);
}
if (!has_inserted && !finish_block_tcr.gcode.empty())
finish_layer_tcr = merge_tcr(finish_layer_tcr, finish_block_tcr);
}
}
// record the contact layers of different categories
@@ -5071,7 +5179,7 @@ WipeTower::ToolChangeResult WipeTower::only_generate_out_wall(bool is_new_mode)
// Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (!m_no_sparse_layers || toolchanges_on_layer)
if (!m_sparse_layers_skipped || toolchanges_on_layer)
if (m_current_tool < m_used_filament_length.size()) m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
return construct_tcr(writer, false, old_tool, true, false, 0.f, false);
@@ -5111,7 +5219,7 @@ Polygon WipeTower::generate_rib_polygon(const box_coordinates &wt_box)
Polygon WipeTower::generate_support_wall_new(WipeTowerWriter &writer, const box_coordinates &wt_box, double feedrate, bool first_layer,bool rib_wall, bool extrude_perimeter, bool skip_points)
{
auto get_closet_idx = [this, &writer](Polylines &pls) -> std::pair<int,int> {
auto get_closet_idx = [&writer](Polylines &pls) -> std::pair<int,int> {
Vec2f anchor{writer.x(), writer.y()};
int closestIndex = -1;
int closestPl = -1;
+46 -1
View File
@@ -42,9 +42,36 @@ public:
static const std::map<float, float> min_depth_per_height;
static float get_limit_depth_by_height(float max_height);
static float get_auto_brim_by_height(float max_height);
// Both generators lay the brim in whole loops one line spacing apart, so the printed width
// differs from the configured one. WipeTower reports it with half a spacing of line width
// added, WipeTower2 reports the loops alone; an estimate has to round like the generator
// whose G-code it stands in for.
static float estimate_brim_real_width(float brim_width, float nozzle_diameter, float first_layer_height, bool type2);
// Depth a Type1 tower reserves once nothing but wrapping detection asks for one.
static float get_wrapping_detection_depth();
// Line width of the nozzle-change purge lines at this nozzle diameter.
static float nozzle_change_perimeter_width(float nozzle_diameter);
static TriangleMesh its_make_rib_tower(float width, float depth, float height, float rib_length, float rib_width, bool fillet_wall);
static TriangleMesh its_make_rib_brim(const Polygon& brim, float layer_height);
static Polygon rib_section(float width, float depth, float rib_length, float rib_width, bool fillet_wall);
// One filament's share of a Type1 tower layer, as plan_tower_new() reserves it.
struct PurgeEstimate
{
float prime_volume = 0.f; // mm3 wiped after changing to this filament
int category = 0; // filament_adhesiveness_category; one purge block per category
float filament_change_length = 0.f; // mm of filament rammed when it leaves its nozzle; 0 when no nozzle change is planned
float filament_diameter = 1.75f;
};
// Depth of the Type1 purge stack at the given width (also the rectangle-wall depth): each
// purge is whole lines at the block infill gap, one block per adhesiveness category sized by
// its worst layer, stacked behind one perimeter width.
static float estimate_tower_blocks_depth(const std::vector<PurgeEstimate> &purges, float width, float layer_height, float nozzle_diameter, float extra_spacing);
// Side of the square bounding a rib-wall tower's first layer, brim excluded: the body plus the
// rib bulge, with the ribs extended to the height-based minimum as both generators do.
static float rib_footprint_side(float width, float depth, float rib_width, float extra_rib_length, float max_height);
// Type1 rib tower: plan_tower_new() squares the tower from the depth at the configured width,
// then re-plans the depth at the squared width.
static float estimate_rib_tower_bbox_side(const std::vector<PurgeEstimate> &purges, float width, float layer_height, float nozzle_diameter, float extra_spacing, float rib_width, float extra_rib_length, float max_height);
// Translation that brings a footprint inside the printable outline, padded by offset. The prime
// tower is validated against the real outline (see layered_print_cleareance_valid), so clamping
// against the bounding box alone would leave it off a delta or hexagonal bed. box and polygons
@@ -494,7 +521,7 @@ private:
//float m_parking_pos_retraction = 0.f;
//float m_extra_loading_move = 0.f;
float m_bridging = 0.f;
bool m_no_sparse_layers = false;
bool m_sparse_layers_skipped = false;
// BBS: remove useless config
//bool m_set_extruder_trimpot = false;
bool m_adhesion = true;
@@ -653,6 +680,24 @@ private:
};
// Compaction rule for wipe_tower_no_sparse_layers. Shared by the G-code emitter and by the
// clearance validator so that both agree on where the compacted tower actually sits; a drift
// between the two would either let a real nozzle collision through or reject a safe plate.
// Whether sparse layers are really skipped, i.e. whether the tower is compacted at all. Smooth
// timelapse and wrapping detection put a tower on every layer, so no layer is ever dropped and the
// tower keeps following the object even though the option is on. Tower planning, G-code emission and
// the clearance validator all ask this single question, so none of them can compact on its own.
bool wipe_tower_sparse_layers_skipped(const PrintConfig &config);
// A planned layer prints no tower at all when its only toolchange keeps the same filament.
bool wipe_tower_layer_is_sparse(const std::vector<WipeTower::ToolChangeResult> &layer_tool_changes);
// Print z the compacted tower reaches on every planned layer. Sparse layers carry over the
// previous value, so the tower falls one layer height behind the object for each of them. base_z is
// the z the tower starts from, which Orca offsets by z_offset.
std::vector<float> compute_compacted_wipe_tower_z(const std::vector<std::vector<WipeTower::ToolChangeResult>> &tool_changes,
float base_z = 0.f);
} // namespace Slic3r
+24 -7
View File
@@ -1032,7 +1032,7 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau
m_y_shift(0.f),
m_z_pos(0.f),
m_bridging(float(config.wipe_tower_bridging)),
m_no_sparse_layers(config.wipe_tower_no_sparse_layers),
m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(config)),
m_gcode_flavor(config.gcode_flavor),
m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))),
m_infill_speed(default_region_config.sparse_infill_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))),
@@ -1730,7 +1730,7 @@ void WipeTower2::toolchange_Change(
} else if (m_wall_type == (int)wtwCone) {
const double support_scale = get_wipe_tower_cone_base(m_wipe_tower_width, m_wipe_tower_height, m_wipe_tower_depth,
m_wipe_tower_cone_angle).second;
const double z = m_no_sparse_layers ? (m_current_height + m_layer_info->height) : m_layer_info->z;
const double z = m_sparse_layers_skipped ? (m_current_height + m_layer_info->height) : m_layer_info->z;
const double r = std::tan(Geometry::deg2rad(m_wipe_tower_cone_angle / 2.f)) * (m_wipe_tower_height - z);
const double w = m_layer_info->depth + m_perimeter_width;
if (r > 0.5 * w + 0.01) { // same guard as generate_support_cone_wall
@@ -1872,7 +1872,7 @@ void WipeTower2::toolchange_Wipe(
// All the calculations in all other places take the spacing into account for all the layers.
// If spare layers are excluded->if 1 or less toolchange has been done, it must be sill the first layer, too.So slow down.
const float target_speed = is_first_layer() || (m_num_tool_changes <= 1 && m_no_sparse_layers) ? m_first_layer_speed * 60.f : std::min(m_wipe_tower_max_purge_speed * 60.f, m_infill_speed * 60.f);
const float target_speed = is_first_layer() || (m_num_tool_changes <= 1 && m_sparse_layers_skipped) ? m_first_layer_speed * 60.f : std::min(m_wipe_tower_max_purge_speed * 60.f, m_infill_speed * 60.f);
float wipe_speed = 0.33f * target_speed;
// if there is less than 2.5*line_width to the edge, advance straightaway (there is likely a blob anyway)
@@ -1970,7 +1970,7 @@ WipeTower::ToolChangeResult WipeTower2::finish_layer()
// Slow down on the 1st layer.
// If spare layers are excluded -> if 1 or less toolchange has been done, it must be still the first layer, too. So slow down.
bool first_layer = is_first_layer() || (m_num_tool_changes <= 1 && m_no_sparse_layers);
bool first_layer = is_first_layer() || (m_num_tool_changes <= 1 && m_sparse_layers_skipped);
float feedrate = first_layer ? m_first_layer_speed * 60.f : std::min(m_wipe_tower_max_purge_speed * 60.f, m_infill_speed * 60.f);
if (m_enable_tower_interface_features && m_prev_layer_had_interface)
feedrate = std::min(feedrate, 20.f * 60.f);
@@ -2103,7 +2103,7 @@ WipeTower::ToolChangeResult WipeTower2::finish_layer()
// Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (! m_no_sparse_layers || toolchanges_on_layer || first_layer) {
if (! m_sparse_layers_skipped || toolchanges_on_layer || first_layer) {
if (m_current_tool < m_used_filament_length.size())
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
m_current_height += m_layer_info->height;
@@ -2129,6 +2129,23 @@ std::pair<double, double> WipeTower2::get_wipe_tower_cone_base(double width, dou
return std::make_pair(R, support_scale);
}
Polygon WipeTower2::cone_base_polygon(double width, double depth, double height, double angle_deg)
{
Polygon box({Point::new_scale(Vec2d(0., 0.)), Point::new_scale(Vec2d(width, 0.)),
Point::new_scale(Vec2d(width, depth)), Point::new_scale(Vec2d(0., depth))});
if (angle_deg <= EPSILON || height <= EPSILON || width <= EPSILON || depth <= EPSILON)
return box;
const auto [R, x_scale] = get_wipe_tower_cone_base(width, height, depth, angle_deg);
if (R <= EPSILON)
return box;
const Vec2d center(width / 2., depth / 2.);
Polygon ellipse;
for (double alpha = 0.; alpha < 2. * M_PI; alpha += M_PI / 20.)
ellipse.points.push_back(Point::new_scale(center + R * Vec2d(std::cos(alpha) / x_scale, std::sin(alpha))));
Polygons u = union_({box, ellipse});
return u.empty() ? box : u.front();
}
// Static method to extract wipe_volumes[from][to] from the configuration.
// Takes a ConfigBase so the GUI's wipe tower size estimate can pass the plate's
// DynamicPrintConfig directly instead of materializing a full PrintConfig per call.
@@ -2209,7 +2226,7 @@ void WipeTower2::plan_toolchange(float z_par, float layer_height_par, unsigned i
if (m_plan.empty() || m_plan.back().z + WT_EPSILON < z_par) // if we moved to a new layer, we'll add it to m_plan first
m_plan.push_back(WipeTowerInfo(z_par, layer_height_par));
if (m_first_layer_idx == size_t(-1) && (! m_no_sparse_layers || old_tool != new_tool || m_plan.size() == 1))
if (m_first_layer_idx == size_t(-1) && (! m_sparse_layers_skipped || old_tool != new_tool || m_plan.size() == 1))
m_first_layer_idx = m_plan.size() - 1;
if (old_tool == new_tool) // new layer without toolchanges - we are done
@@ -2635,7 +2652,7 @@ Polygon WipeTower2::generate_support_cone_wall(
const auto [R, support_scale] = get_wipe_tower_cone_base(m_wipe_tower_width, m_wipe_tower_height, m_wipe_tower_depth,
m_wipe_tower_cone_angle);
double z = m_no_sparse_layers ?
double z = m_sparse_layers_skipped ?
(m_current_height + m_layer_info->height) :
m_layer_info->z; // the former should actually work in both cases, but let's stay on the safe side (the 2.6.0 is close)
+5 -1
View File
@@ -27,6 +27,10 @@ public:
// in WipeTowerIntegration::append_tcr2 does not strip it.
static const std::string wait_for_temp_tag() { return ";_WAIT_FOR_TEMP_ON_WIPE_TOWER"; }
static std::pair<double, double> get_wipe_tower_cone_base(double width, double height, double depth, double angle_deg);
// First-layer outline of a cone-wall tower in tower-local (scaled) coordinates: body box
// unioned with the cone's base ellipse — the model first_layer_wipe_tower_corners uses,
// and generate_support_cone_wall stays within it. Brim not included.
static Polygon cone_base_polygon(double width, double depth, double height, double angle_deg);
static std::vector<std::vector<float>> extract_wipe_volumes(const ConfigBase& config);
// Estimated total flush volume of a SEMM print with the given number of filaments,
// used to reserve wipe tower space before the tower is generated.
@@ -263,7 +267,7 @@ private:
float m_parking_pos_retraction = 0.f;
float m_extra_loading_move = 0.f;
float m_bridging = 0.f;
bool m_no_sparse_layers = false;
bool m_sparse_layers_skipped = false;
bool m_set_extruder_trimpot = false;
bool m_adhesion = true;
GCodeFlavor m_gcode_flavor;
+202
View File
@@ -0,0 +1,202 @@
#include "WipeTowerEstimate.hpp"
#include "WipeTower.hpp"
#include "WipeTower2.hpp"
#include "../Config.hpp"
#include "../PrintConfig.hpp"
#include "../libslic3r.h"
#include <algorithm>
#include <cmath>
#include <set>
namespace Slic3r {
// Every caller today declares all these keys, but the signature accepts any ConfigBase: fall
// back to the key's declared default, never to a hand-copied constant.
static const ConfigOption *option_of(const ConfigBase &config, const char *key)
{
if (const ConfigOption *opt = config.option(key); opt != nullptr)
return opt;
if (const ConfigDef *def = config.def(); def != nullptr)
if (const ConfigOptionDef *opt_def = def->get(key); opt_def != nullptr)
return opt_def->default_value.get();
return nullptr;
}
WipeTowerType resolve_wipe_tower_type(const ConfigBase &config)
{
// printer_model is what the CLI keys its Bambu Lab detection on; the GUI's vendor flag
// agrees for every shipped profile.
if (const auto *model = dynamic_cast<const ConfigOptionString *>(config.option("printer_model"));
model != nullptr && model->value.compare(0, 9, "Bambu Lab") == 0)
return WipeTowerType::Type1;
// By value, not by concrete type: a static PrintConfig holds ConfigOptionEnum<T>, a
// DynamicConfig built from presets holds ConfigOptionEnumGeneric, and both answer getInt().
const ConfigOption *type = option_of(config, "wipe_tower_type");
return type != nullptr ? WipeTowerType(type->getInt()) : WipeTowerType::Type2;
}
Polygon estimate_wipe_tower_first_layer_outline(const ConfigBase &config, WipeTowerType tower_type, double width, double depth, double height)
{
// Type1 ignores the cone option. The wall type is read by value: a preset-shaped config
// holds it as ConfigOptionEnumGeneric, which a cast to ConfigOptionEnum<T> cannot see.
const ConfigOption *wall_type = option_of(config, "wipe_tower_wall_type");
const ConfigOption *cone_angle = option_of(config, "wipe_tower_cone_angle");
const bool cone = tower_type == WipeTowerType::Type2 && wall_type != nullptr &&
wall_type->getInt() == int(WipeTowerWallType::wtwCone) && cone_angle != nullptr;
return WipeTower2::cone_base_polygon(width, depth, height, cone ? cone_angle->getFloat() : 0.);
}
WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, WipeTowerType tower_type, const std::vector<unsigned int> &filament_ids, double layer_height, double max_object_height)
{
WipeTowerFootprint footprint;
footprint.height = max_object_height;
const size_t filaments_cnt = filament_ids.size();
if (filaments_cnt == 0 || layer_height < EPSILON)
return footprint;
auto opt_float = [&config](const char *key) {
const ConfigOption *opt = option_of(config, key);
return opt != nullptr ? opt->getFloat() : 0.;
};
auto opt_bool = [&config](const char *key) {
const ConfigOption *opt = option_of(config, key);
return opt != nullptr && opt->getBool();
};
auto opt_enum = [&config](const char *key, int fallback) {
const ConfigOption *opt = option_of(config, key);
return opt != nullptr ? opt->getInt() : fallback;
};
auto floats_of = [&config](const char *key) { return dynamic_cast<const ConfigOptionFloats *>(option_of(config, key)); };
auto max_of = [&floats_of](const char *key, double fallback) {
const auto *opt = floats_of(key);
return (opt != nullptr && !opt->values.empty()) ? *std::max_element(opt->values.begin(), opt->values.end()) : fallback;
};
auto float_at = [&floats_of](const char *key, unsigned int id, double fallback) {
const auto *opt = floats_of(key);
return (opt != nullptr && !opt->values.empty()) ? opt->get_at(id) : fallback;
};
auto int_at = [&config](const char *key, unsigned int id, int fallback) {
const auto *opt = dynamic_cast<const ConfigOptionInts *>(option_of(config, key));
return (opt != nullptr && !opt->values.empty()) ? opt->get_at(id) : fallback;
};
// Both planners size every layer, so the tower has to fit its thinnest one: the first layer
// when it is printed thinner than the rest.
const double first_layer_height = opt_float("initial_layer_print_height");
if (first_layer_height > EPSILON)
layer_height = std::min(layer_height, first_layer_height);
const bool type1 = tower_type == WipeTowerType::Type1;
const double width = opt_float("prime_tower_width");
const double prime_volume = opt_float("prime_volume");
// Type1 spaces its purge lines by prime_tower_infill_gap, Type2 by wipe_tower_extra_spacing.
// Type2's extra flow cancels out of the depth: the line length is divided by it and the row
// pitch multiplied by it (WipeTower2::get_wipe_depth).
const double extra_spacing = opt_float(type1 ? "prime_tower_infill_gap" : "wipe_tower_extra_spacing") / 100.;
const double rib_width = opt_float("wipe_tower_rib_width");
const double extra_rib_length = opt_float("wipe_tower_extra_rib_length");
const auto *nozzle_opt = floats_of("nozzle_diameter");
const double nozzle_diameter = (nozzle_opt != nullptr && !nozzle_opt->values.empty()) ? nozzle_opt->values.front() : 0.4;
const bool dual_nozzle = nozzle_opt != nullptr && nozzle_opt->values.size() == 2;
const bool rib_wall = opt_enum("wipe_tower_wall_type", int(WipeTowerWallType::wtwRectangle)) == int(WipeTowerWallType::wtwRib);
const bool smooth_timelapse = opt_enum("timelapse_type", int(TimelapseType::tlTraditional)) == int(TimelapseType::tlSmooth);
const bool wrapping = opt_bool("enable_wrapping_detection");
// Reasons a tower is printed with no tool change to purge for: the ones that stop
// normalize_fdm_2 clearing enable_prime_tower. Its mixed-filament case is not modelled.
const bool need_wipe_tower = smooth_timelapse || wrapping;
// A tower printed for one of the reasons above has no tool change to purge for; both
// planners give it the idle depth below and nothing more.
const size_t purge_count = filaments_cnt > 1 ? (dual_nozzle ? filaments_cnt : filaments_cnt - 1) : 0;
// Type2 purges one volume per tool change. Type1 plans per filament below; here the volume
// only decides whether a tower exists.
double volume = prime_volume * double(purge_count);
if (dual_nozzle) {
// Dual-nozzle printers also purge the filament change length on the tower.
const double length = max_of("filament_change_length", 0.);
const double diameter = max_of("filament_diameter", 1.75);
volume += length * PI * diameter * diameter / 4. * double(filaments_cnt / 2);
}
// Single-extruder multi-material purges the flush matrix instead of the prime volume.
const bool semm_flush = opt_bool("purge_in_prime_tower") && opt_bool("single_extruder_multi_material");
if (semm_flush)
volume = WipeTower2::estimate_semm_flush_volume(config, filaments_cnt);
// The Type1 planner wipes each filament's own prime volume after changing to it, in a block
// per adhesiveness category. On a two-nozzle printer the leaving filament is also rammed at
// every nozzle change; the tool order groups filaments by nozzle, so a layer crosses
// (nozzles used - 1) times, charged here to the longest ramming.
std::vector<WipeTower::PurgeEstimate> purges;
if (type1 && filaments_cnt > 1) {
const bool saving_mode = opt_enum("prime_volume_mode", int(PrimeVolumeMode::pvmDefault)) == int(PrimeVolumeMode::pvmSaving);
std::set<int> nozzles;
size_t longest_ramming = 0;
for (size_t i = 0; i < filaments_cnt; ++i) {
const unsigned int id = filament_ids[i];
WipeTower::PurgeEstimate purge;
purge.prime_volume = saving_mode ? 15.f : float(float_at("filament_prime_volume", id, prime_volume));
purge.category = int_at("filament_adhesiveness_category", id, 0);
purge.filament_diameter = float(float_at("filament_diameter", id, 1.75));
purges.push_back(purge);
if (dual_nozzle) {
nozzles.insert(int_at("filament_map", id, 1));
if (float_at("filament_change_length", id, 0.) > float_at("filament_change_length", filament_ids[longest_ramming], 0.))
longest_ramming = i;
}
}
if (nozzles.size() > 1)
purges[longest_ramming].filament_change_length = float(float_at("filament_change_length", filament_ids[longest_ramming], 0.) * double(nozzles.size() - 1));
}
// Both wall types decide this together: over-reserving only wastes bed area, but reporting
// no tower for one that is built collapses the validation hull to a point.
// A tool change is a reason on its own (see the base commit); Type1 already reserves
// per filament, Type2 has only the volume, which can resolve to zero.
const bool has_purge = type1 ? !purges.empty() : volume > EPSILON;
if (!has_purge && filaments_cnt < 2 && !need_wipe_tower)
return footprint;
const double min_depth = WipeTower::get_limit_depth_by_height(float(max_object_height));
const float perimeter_width = float(nozzle_diameter) * 1.25f; // Width_To_Nozzle_Ratio
// With nothing to purge, plan_tower_new sizes the tower for wrapping detection or the
// stability minimum; WipeTower2 only knows the latter.
const double idle_depth = (type1 && wrapping && !smooth_timelapse) ? WipeTower::get_wrapping_detection_depth() : min_depth;
if (rib_wall) {
// Both planners square the tower to the purge area and extend the ribs, not the body,
// below the stability minimum.
double side;
if (!purges.empty())
side = WipeTower::estimate_rib_tower_bbox_side(purges, float(width), float(layer_height), float(nozzle_diameter), float(extra_spacing), float(rib_width), float(extra_rib_length), float(max_object_height));
else {
const double square = has_purge ? std::sqrt(volume / layer_height * extra_spacing) : idle_depth;
side = WipeTower::rib_footprint_side(float(square), float(square), float(rib_width), float(extra_rib_length), float(max_object_height));
}
footprint.width = footprint.depth = side;
} else {
double depth;
if (type1) {
// plan_tower_new stretches a short purge stack to the stability minimum behind its
// leading perimeter width.
depth = purges.empty() ? idle_depth : std::max(min_depth + perimeter_width, double(WipeTower::estimate_tower_blocks_depth(purges, float(width), float(layer_height), float(nozzle_diameter), float(extra_spacing))));
} else {
depth = volume / (layer_height * width);
// The flush volumes already hold the spacing between wipes.
if (!semm_flush)
depth *= extra_spacing;
depth = std::max(min_depth, depth);
}
footprint.width = width;
footprint.depth = depth;
}
footprint.brim_width = opt_float("prime_tower_brim_width");
if (footprint.brim_width < 0)
footprint.brim_width = WipeTower::get_auto_brim_by_height(float(max_object_height));
footprint.brim_width = WipeTower::estimate_brim_real_width(float(footprint.brim_width), float(nozzle_diameter), float(first_layer_height > EPSILON ? first_layer_height : layer_height), !type1);
return footprint;
}
} // namespace Slic3r
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include <vector>
#include "../Polygon.hpp"
namespace Slic3r {
class ConfigBase;
enum class WipeTowerType;
// Pre-slice footprint of the wipe tower, shared by validation (Print), the GUI's placement
// clamp/preview/arrange and the CLI placement. The arithmetic is shared; the inputs below are
// not, so a change to how one caller derives them has to be mirrored in the others.
struct WipeTowerFootprint
{
double width = 0.; // effective width: equals depth for a rib wall, which squares the tower
double depth = 0.; // 0 when these inputs imply no tower
double height = 0.; // tallest object; drives the stability floor and the auto brim
double brim_width = 0.; // printed width: auto (-1) resolved by height, laid in whole loops
};
// Which planner builds the tower: Bambu Lab printers always get Type1, the rest follow
// wipe_tower_type. The rule Print::wipe_tower_type() and the CLI apply, read off the config so
// the GUI and CLI placement can resolve it without a Print.
WipeTowerType resolve_wipe_tower_type(const ConfigBase &config);
// First-layer outline of an estimated tower in tower-local scaled coordinates, brim excluded:
// the body box, or for a Type2 cone wall the box unioned with the cone's base. The preview,
// the placement margin and validation all take the outline from here so they cannot disagree
// about whether a cone exists.
Polygon estimate_wipe_tower_first_layer_outline(const ConfigBase &config, WipeTowerType tower_type, double width, double depth, double height);
// filament_ids: 0-based filaments purged on the plate. The config cannot see custom G-code tool
// changes, so ids derived from the model must include them
// (Print::extruders(true)) or a real tower is sized as if it were never built.
// layer_height: thinnest layer the objects are sliced at. The first layer is folded in here.
//
// A raft is deliberately not a reason: normalize_fdm_2 clears enable_prime_tower for a plate
// purging one filament unless smooth timelapse or wrapping detection is on.
WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config,
WipeTowerType tower_type,
const std::vector<unsigned int> &filament_ids,
double layer_height,
double max_object_height);
} // namespace Slic3r
-1
View File
@@ -165,7 +165,6 @@ public:
//BBS
unsigned int m_last_additional_fan_speed;
int m_last_bed_temperature;
bool m_last_bed_temperature_reached;
double m_lifted;
+1 -2
View File
@@ -57,7 +57,7 @@
#define HAS_INTRINSIC_128_TYPE
#endif
#if defined(_MSC_VER) && defined(_WIN64)
#if defined(_MSC_VER) && defined(_M_X64)
#include <intrin.h>
#pragma intrinsic(_mul128)
#endif
@@ -125,7 +125,6 @@ public:
/******************************************** Splitting the 128bit number into two 64bit words *********************************************/
Int128(int64_t lo = 0) : m_lo((uint64_t)lo), m_hi((lo < 0) ? -1 : 0) {}
Int128(const Int128 &val) : m_lo(val.m_lo), m_hi(val.m_hi) {}
Int128(const int64_t& hi, const uint64_t& lo) : m_lo(lo), m_hi(hi) {}
Int128& operator = (const int64_t &val)
+221
View File
@@ -0,0 +1,221 @@
#include "LayOnFace.hpp"
#include "Geometry.hpp"
#include "Geometry/ConvexHull.hpp"
#include "Model.hpp"
#include "TriangleMesh.hpp"
#include <algorithm>
#include <cmath>
#include <numeric>
namespace Slic3r {
std::vector<LayOnFacePlane> lay_on_face_planes(const ModelObject &object, const Transform3d &inst_matrix)
{
// An object can only rest on its convex hull, so candidate faces are taken from the hull of all model parts.
TriangleMesh ch;
for (const ModelVolume* vol : object.volumes) {
if (vol->type() != ModelVolumeType::MODEL_PART)
continue;
TriangleMesh vol_ch = vol->get_convex_hull();
vol_ch.transform(vol->get_matrix());
ch.merge(vol_ch);
}
ch = ch.convex_hull_3d();
std::vector<LayOnFacePlane> planes;
// Following constants are used for discarding too small polygons.
const float minimal_area = 5.f; // in square mm (world coordinates)
const float minimal_side = 1.f; // mm
const float minimal_angle = 1.f; // degree, initial value was 10, but cause bugs
// Now we'll go through all the facets and append Points of facets sharing the same normal.
// This part is still performed in mesh coordinate system.
const int num_of_facets = ch.facets_count();
const std::vector<Vec3f> face_normals = its_face_normals(ch.its);
const std::vector<Vec3i32> face_neighbors = its_face_neighbors(ch.its);
std::vector<int> facet_queue(num_of_facets, 0);
std::vector<bool> facet_visited(num_of_facets, false);
int facet_queue_cnt = 0;
const stl_normal* normal_ptr = nullptr;
int facet_idx = 0;
while (1) {
// Find next unvisited triangle:
for (; facet_idx < num_of_facets; ++ facet_idx)
if (!facet_visited[facet_idx]) {
facet_queue[facet_queue_cnt ++] = facet_idx;
facet_visited[facet_idx] = true;
normal_ptr = &face_normals[facet_idx];
planes.emplace_back();
break;
}
if (facet_idx == num_of_facets)
break; // Everything was visited already
while (facet_queue_cnt > 0) {
int facet_idx = facet_queue[-- facet_queue_cnt];
const stl_normal& this_normal = face_normals[facet_idx];
if (std::abs(this_normal(0) - (*normal_ptr)(0)) < 0.001 && std::abs(this_normal(1) - (*normal_ptr)(1)) < 0.001 && std::abs(this_normal(2) - (*normal_ptr)(2)) < 0.001) {
const Vec3i32 face = ch.its.indices[facet_idx];
for (int j=0; j<3; ++j)
planes.back().outline.emplace_back(ch.its.vertices[face[j]].cast<double>());
facet_visited[facet_idx] = true;
for (int j = 0; j < 3; ++ j)
if (int neighbor_idx = face_neighbors[facet_idx][j]; neighbor_idx >= 0 && ! facet_visited[neighbor_idx])
facet_queue[facet_queue_cnt ++] = neighbor_idx;
}
}
planes.back().normal = normal_ptr->cast<double>();
Pointf3s& verts = planes.back().outline;
// Now we'll transform all the points into world coordinates, so that the areas, angles and distances
// make real sense.
verts = transform(verts, inst_matrix);
// if this is a just a very small triangle, remove it to speed up further calculations (it would be rejected later anyway):
if (verts.size() == 3 &&
((verts[0] - verts[1]).norm() < minimal_side
|| (verts[0] - verts[2]).norm() < minimal_side
|| (verts[1] - verts[2]).norm() < minimal_side))
planes.pop_back();
}
// Let's prepare transformation of the normal vector from mesh to instance coordinates.
const Matrix3d normal_matrix = inst_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
// Now we'll go through all the polygons, transform the points into xy plane to process them:
for (unsigned int polygon_id=0; polygon_id < planes.size(); ++polygon_id) {
Pointf3s& polygon = planes[polygon_id].outline;
const Vec3d& normal = planes[polygon_id].normal;
// transform the normal according to the instance matrix:
const Vec3d normal_transformed = normal_matrix * normal;
// We are going to rotate about z and y to flatten the plane
Eigen::Quaterniond q;
Transform3d& m = planes[polygon_id].to_plane_frame;
m = Transform3d::Identity();
m.matrix().block(0, 0, 3, 3) = q.setFromTwoVectors(normal_transformed, Vec3d::UnitZ()).toRotationMatrix();
polygon = transform(polygon, m);
// Now to remove the inner points. We'll misuse Geometry::convex_hull for that, but since
// it works in fixed point representation, we will rescale the polygon to avoid overflows.
// And yes, it is a nasty thing to do. Whoever has time is free to refactor.
Vec3d bb_size = BoundingBoxf3(polygon).size();
float sf = std::min(1./bb_size(0), 1./bb_size(1));
Transform3d tr = Geometry::scale_transform({ sf, sf, 1.f });
polygon = transform(polygon, tr);
polygon = Slic3r::Geometry::convex_hull(polygon);
polygon = transform(polygon, tr.inverse());
// Calculate area of the polygons and discard ones that are too small
float& area = planes[polygon_id].area;
area = 0.f;
for (unsigned int i = 0; i < polygon.size(); i++) // Shoelace formula
area += polygon[i](0)*polygon[i + 1 < polygon.size() ? i + 1 : 0](1) - polygon[i + 1 < polygon.size() ? i + 1 : 0](0)*polygon[i](1);
area = 0.5f * std::abs(area);
bool discard = false;
if (area < minimal_area)
discard = true;
else {
// We also check the inner angles and discard polygons with angles smaller than the following threshold
const double angle_threshold = ::cos(minimal_angle * (double)PI / 180.0);
for (unsigned int i = 0; i < polygon.size(); ++i) {
const Vec3d& prec = polygon[(i == 0) ? polygon.size() - 1 : i - 1];
const Vec3d& curr = polygon[i];
const Vec3d& next = polygon[(i == polygon.size() - 1) ? 0 : i + 1];
if ((prec - curr).normalized().dot((next - curr).normalized()) > angle_threshold) {
discard = true;
break;
}
}
}
if (discard) {
planes[polygon_id--] = std::move(planes.back());
planes.pop_back();
continue;
}
const Vec3d centroid = std::accumulate(polygon.begin(), polygon.end(), Vec3d(0.0, 0.0, 0.0)) / double(polygon.size());
planes[polygon_id].center = inst_matrix.inverse() * (m.inverse() * centroid);
}
std::sort(planes.rbegin(), planes.rend(), [](const LayOnFacePlane& a, const LayOnFacePlane& b) { return a.area < b.area; });
return planes;
}
int find_largest_plane(const std::vector<LayOnFacePlane> &planes)
{
// The plane frame maps the instance normal to +Z, so the normal's z in instance coordinates is element (2, 2).
auto downward = [](const LayOnFacePlane &plane) { return -plane.to_plane_frame.linear()(2, 2); };
// Areas are floats from rounded geometry, so faces within 0.1% count as equal.
int best = -1;
for (size_t i = 0; i < planes.size() && planes[i].area >= planes.front().area * (1. - 1e-3); ++i)
if (best < 0 || downward(planes[i]) > downward(planes[best]))
best = int(i);
return best;
}
int find_plane_by_normal(const std::vector<LayOnFacePlane> &planes, const Vec3d &direction)
{
const Vec3d dir = direction.normalized();
int best = -1;
double best_dot = -2.;
for (size_t i = 0; i < planes.size(); ++i)
if (const double dot = planes[i].normal.dot(dir); dot > best_dot) {
best_dot = dot;
best = int(i);
}
return best;
}
int find_plane_at_point(const std::vector<LayOnFacePlane> &planes, const Transform3d &instance_matrix_no_offset,
const Vec3d &point, double tolerance)
{
const Vec3d instance_point = instance_matrix_no_offset * point;
for (size_t i = 0; i < planes.size(); ++i) {
const Pointf3s &outline = planes[i].outline;
if (outline.empty())
continue;
const Vec3d p = planes[i].to_plane_frame * instance_point;
// Facets with slightly different normals are merged into one face, so the outline is not exactly flat.
const double z = std::accumulate(outline.begin(), outline.end(), 0., [](double sum, const Vec3d &v) { return sum + v.z(); }) / double(outline.size());
if (std::abs(p.z() - z) > tolerance)
continue;
// The outline is convex: the point is inside when it is not on both sides of its edges.
bool left = false, right = false;
for (size_t j = 0; j < outline.size(); ++j) {
const Vec2d a = outline[j].head<2>();
const Vec2d edge = outline[(j + 1) % outline.size()].head<2>() - a;
const double len = edge.norm();
if (len < EPSILON)
continue;
const double side = cross2(edge, Vec2d(p.head<2>() - a)) / len;
left |= side > tolerance;
right |= side < -tolerance;
}
if (!(left && right))
return int(i);
}
return -1;
}
void lay_on_face(ModelObject &object, size_t instance_idx, const Vec3d &normal)
{
ModelInstance &instance = *object.instances[instance_idx];
const Geometry::Transformation &trafo = instance.get_transformation();
// Same rotation as Selection::flattening_rotate(): turn the transformed normal to point down.
const Vec3d tnormal = trafo.get_matrix().matrix().block(0, 0, 3, 3).inverse().transpose() * normal;
const Transform3d rotation = Transform3d(Eigen::Quaterniond().setFromTwoVectors(tnormal, -Vec3d::UnitZ()));
instance.set_transformation(Geometry::Transformation(trafo.get_offset_matrix() * rotation * trafo.get_matrix_no_offset()));
// Drop this instance only: ensure_on_bed() skips instances without auto_drop and measures the first instance.
object.translate_instance(instance_idx, -object.instance_bounding_box(instance_idx).min.z() * Vec3d::UnitZ());
}
} // namespace Slic3r
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include "Point.hpp"
#include <vector>
namespace Slic3r {
class ModelObject;
// A face of an object's convex hull that the object can rest on. These are the faces the
// "Lay on Face" gizmo offers and the ones the CLI --ground-* options choose from.
//
// Frames: "object" coordinates have the volume transformations applied but not the instance
// transformation. "Instance" coordinates additionally have the instance rotation, scale and
// mirror applied, but not its offset.
struct LayOnFacePlane
{
Vec3d normal; // outward unit normal, object coordinates
Vec3d center; // centroid of the outline, object coordinates; on the face's mean plane
float area; // mm², instance coordinates
Pointf3s outline; // convex outline in the plane frame, where the face is horizontal
Transform3d to_plane_frame; // rotation from instance coordinates to the plane frame
};
// Candidate faces of the object's model parts, largest first. The instance transformation
// (without offset) is applied before measuring, so faces too small to rest on are dropped
// by their printed size: under 5 mm², a side under 1 mm, or an inner angle under 1°.
std::vector<LayOnFacePlane> lay_on_face_planes(const ModelObject &object, const Transform3d &instance_matrix_no_offset);
// Index of the largest plane, or -1 if `planes` is empty. Of planes with the same area, such as
// the top and bottom of a box, the one already facing down the most wins, so flat parts stay put.
int find_largest_plane(const std::vector<LayOnFacePlane> &planes);
// Index of the plane whose normal is closest to `direction` (object coordinates),
// or -1 if `planes` is empty.
int find_plane_by_normal(const std::vector<LayOnFacePlane> &planes, const Vec3d &direction);
// Index of the plane whose face contains `point` (object coordinates) within `tolerance` mm, or -1
// if there is none. `instance_matrix_no_offset` is the one the planes were computed with.
int find_plane_at_point(const std::vector<LayOnFacePlane> &planes, const Transform3d &instance_matrix_no_offset,
const Vec3d &point, double tolerance);
// Rotates the instance so that `normal` (object coordinates) points down, the same rotation as
// the gizmo applies, then drops the instance so its lowest point is at z = 0.
void lay_on_face(ModelObject &object, size_t instance_idx, const Vec3d &normal);
} // namespace Slic3r
+17 -7
View File
@@ -153,6 +153,7 @@ bool Layer::is_perimeter_compatible(const Print& print, const PrintRegion& a, co
&& config.gap_infill_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id)) == other_config.gap_infill_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id))
&& config.filter_out_gap_fill.value == other_config.filter_out_gap_fill.value
&& config.detect_overhang_wall == other_config.detect_overhang_wall
&& config.unsupported_wall_last == other_config.unsupported_wall_last
&& config.overhang_reverse == other_config.overhang_reverse
&& config.overhang_reverse_threshold == other_config.overhang_reverse_threshold
&& config.wall_direction == other_config.wall_direction
@@ -187,6 +188,12 @@ void Layer::make_perimeters()
{
BOOST_LOG_TRIVIAL(trace) << "Generating perimeters for layer " << this->id();
const auto clear_generated_extrusions = [](LayerRegion *layer_region) {
layer_region->perimeters.clear();
layer_region->fills.clear();
layer_region->thin_fills.clear();
};
// keep track of regions whose perimeters we have already generated
std::vector<unsigned char> done(m_regions.size(), false);
@@ -217,13 +224,11 @@ void Layer::make_perimeters()
if (this_region.gradient_volume_id() != other_region.gradient_volume_id())
continue;
if (is_perimeter_compatible(*m_object->print(), this_region, other_region))
{
other_layerm->perimeters.clear();
other_layerm->fills.clear();
other_layerm->thin_fills.clear();
layerms.push_back(other_layerm);
done[it - m_regions.begin()] = true;
}
{
clear_generated_extrusions(other_layerm);
layerms.push_back(other_layerm);
done[it - m_regions.begin()] = true;
}
}
if (layerms.size() == 1) { // optimization
@@ -231,6 +236,10 @@ void Layer::make_perimeters()
(*layerm)->make_perimeters((*layerm)->slices, {*layerm}, &(*layerm)->fill_surfaces, &(*layerm)->fill_no_overlap_expolygons);
(*layerm)->fill_expolygons = to_expolygons((*layerm)->fill_surfaces.surfaces);
} else {
// Orca: Unlike the compatible regions above, the initiating region has not
// been cleared yet and may contain paths from a previous incompatible run.
clear_generated_extrusions(*layerm);
SurfaceCollection new_slices;
// Use the region with highest infill rate, as the make_perimeters() function below decides on the gap fill based on the infill existence.
LayerRegion *layerm_config = layerms.front();
@@ -419,6 +428,7 @@ coordf_t Layer::get_sparse_infill_max_void_area()
double spacing = flow.scaled_spacing() * (100 - density) / density;
switch (pattern) {
case ipConcentric:
case ipSpiralInset:
case ipRectilinear:
case ipLine:
case ipGyroid:
+6
View File
@@ -16,6 +16,7 @@ using LayerPtrs = std::vector<Layer*>;
class LayerRegion;
using LayerRegionPtrs = std::vector<LayerRegion*>;
class PrintRegion;
class PrintRegionConfig;
class PrintObject;
class Print;
@@ -200,6 +201,11 @@ public:
FillAdaptive::Octree *support_fill_octree,
FillLightning::Generator* lightning_generator) const;
void make_ironing();
// Returns the filament id (1-based) the region is ironed with, or -1 when the
// region is not ironed.
static int choose_ironing_extruder(const PrintRegionConfig &cfg,
bool spiral_mode,
bool is_topmost_layer);
void make_contour_z(const sla::IndexedMesh &mesh);
void export_region_slices_to_svg(const char *path) const;
+2 -2
View File
@@ -30,8 +30,8 @@ bool Line::intersection_infinite(const Line &other, Point* point) const
return false;
double t1 = cross2(v12, v2) / denom;
Vec2d result = (a1 + t1 * v1);
if (result.x() > std::numeric_limits<coord_t>::max() || result.x() < std::numeric_limits<coord_t>::lowest() ||
result.y() > std::numeric_limits<coord_t>::max() || result.y() < std::numeric_limits<coord_t>::lowest()) {
if (result.x() > double(std::numeric_limits<coord_t>::max()) || result.x() < double(std::numeric_limits<coord_t>::lowest()) ||
result.y() > double(std::numeric_limits<coord_t>::max()) || result.y() < double(std::numeric_limits<coord_t>::lowest())) {
// Intersection has at least one of the coordinates much bigger (or smaller) than coord_t maximum value (or minimum).
// So it can not be stored into the Point without integer overflows. That could mean that input lines are parallel or near parallel.
return false;
+2
View File
@@ -3,6 +3,8 @@
#ifdef _WIN32
#include <charconv>
#endif
#include <iomanip>
#include <sstream>
#include <stdexcept>
#include <fast_float/fast_float.h>
-9
View File
@@ -33,15 +33,6 @@ public:
SurfaceFeature(const Vec3d& pt)
: m_type{SurfaceFeatureType::Point}, m_pt1{pt} {}
SurfaceFeature(const SurfaceFeature& sf){
this->clone(sf);
volume = sf.volume;
plane_indices = sf.plane_indices;
world_tran = sf.world_tran;
world_plane_features = sf.world_plane_features;
origin_surface_feature = sf.origin_surface_feature;
}
void clone(const SurfaceFeature &sf)
{
m_type = sf.get_type();
+2 -2
View File
@@ -352,7 +352,7 @@ void segment(CGALMesh& src, std::vector<CGALMesh>& dst, double smoothing_alpha =
//}
//else
{
dst.emplace_back(std::move(CGALMesh(out)));
dst.emplace_back(CGALMesh(out));
}
}
//if (mesh_merged.is_empty() == false) {
@@ -371,7 +371,7 @@ std::vector<TriangleMesh> segment(const TriangleMesh& src, double smoothing_alph
std::vector<TriangleMesh> out_meshes;
for (auto& outf_cgal_mesh: out_cgal_meshes)
{
out_meshes.emplace_back(std::move(cgal_to_triangle_mesh(outf_cgal_mesh.m)));
out_meshes.emplace_back(cgal_to_triangle_mesh(outf_cgal_mesh.m));
}
return out_meshes;
+6 -1
View File
@@ -60,10 +60,15 @@ auto MinimumSpanningTree::prim(std::vector<Point> vertices) const -> AdjacencyGr
//This search is O(V) right now, which can be made down to O(log(V)). This reduces the overall time complexity from O(V*V) to O(V*log(E)).
//However that requires an implementation of a heap that supports the decreaseKey operation, which is not in the std library.
//TODO: Implement this?
// Break equal-distance ties on coordinates: the map is keyed by address, so its
// iteration order (and therefore the first minimum) would otherwise depend on where
// the vertices were allocated.
using MapValue = std::pair<const Point*, coordf_t>;
const auto closest = std::min_element(smallest_distance.begin(), smallest_distance.end(),
[](const MapValue& a, const MapValue& b) {
return a.second < b.second;
if (a.second != b.second)
return a.second < b.second;
return *a.first < *b.first;
});
//Add this point to the graph and remove it from the candidates.
+51 -2
View File
@@ -108,6 +108,8 @@ Model& Model::assign_copy(const Model &rhs)
this->md_value = rhs.md_value;
this->texture_mesh = rhs.texture_mesh;
this->cad_recipe = rhs.cad_recipe;
return *this;
}
@@ -152,6 +154,7 @@ Model& Model::assign_copy(Model &&rhs)
rhs.model_info.reset();
this->profile_info = rhs.profile_info;
rhs.profile_info.reset();
this->cad_recipe = std::move(rhs.cad_recipe);
return *this;
}
@@ -261,7 +264,7 @@ static void add_textured_mesh_to_model(Model& model, const TexturedMesh& tex_mes
its_remove_degenerate_faces(its);
its_compactify_vertices(its);
model.add_object(object_name.c_str(), input_file.c_str(), std::move(TriangleMesh(std::move(its))));
model.add_object(object_name.c_str(), input_file.c_str(), TriangleMesh(std::move(its)));
}
Model Model::read_from_file(const std::string& input_file,
@@ -877,7 +880,7 @@ void Model::convert_multipart_object(unsigned int max_extruders)
// Revert the centering operation.
trafo_volume.set_offset(trafo_volume.get_offset() - o->origin_translation);
int counter = 1;
auto copy_volume = [o, v, max_extruders, &counter, &extruder_counter](ModelVolume *new_v) {
auto copy_volume = [o, v, &counter](ModelVolume *new_v) {
assert(new_v != nullptr);
new_v->name = (counter > 1) ? o->name + "_" + std::to_string(counter++) : o->name;
//BBS: Use extruder priority: volumn > object > default
@@ -3598,6 +3601,15 @@ void FacetsAnnotation::shift_states_above(const ModelVolume &mv, EnforcerBlocker
this->set(selector);
}
void FacetsAnnotation::remap_states(const ModelVolume &mv, const EnforcerBlockerStateMap &state_map)
{
if (empty()) return;
TriangleSelector selector(mv.mesh());
selector.deserialize(m_data, false);
selector.remap_triangle_state(state_map);
this->set(selector);
}
void FacetsAnnotation::set_enforcer_block_type_limit(const ModelVolume &mv,
EnforcerBlockerType max_type,
EnforcerBlockerType to_delete_filament,
@@ -3862,6 +3874,43 @@ bool model_has_advanced_features(const Model &model)
return false;
}
void remap_model_filament_slots(Model &model, const std::map<int, int> &slot_relocations)
{
if (slot_relocations.empty())
return;
// Paint states and the object/volume "extruder" configs store one-based slot numbers
// (see Sidebar::on_action_add_filament's insertion remap for the same encoding).
std::map<int, int> one_based_slots;
for (const auto &[from, to] : slot_relocations)
one_based_slots.emplace(from + 1, to + 1);
EnforcerBlockerStateMap paint_state_map;
for (size_t state = 0; state < paint_state_map.size(); ++state)
paint_state_map[state] = EnforcerBlockerType(state);
for (const auto &[one_based_from, one_based_to] : one_based_slots) {
assert(one_based_from >= 0 && size_t(one_based_from) < paint_state_map.size());
assert(one_based_to > 0 && size_t(one_based_to) < paint_state_map.size());
paint_state_map[size_t(one_based_from)] = EnforcerBlockerType(one_based_to);
}
auto remap_extruder_config = [&one_based_slots](ModelConfig &config) -> bool {
const auto it = config.has("extruder") ? one_based_slots.find(config.extruder()) : one_based_slots.end();
if (it == one_based_slots.end())
return false;
config.set("extruder", it->second);
return true;
};
for (ModelObject *object : model.objects) {
remap_extruder_config(object->config);
for (ModelVolume *volume : object->volumes) {
remap_extruder_config(volume->config);
volume->mmu_segmentation_facets.remap_states(*volume, paint_state_map);
}
}
}
#ifndef NDEBUG
// Verify whether the IDs of Model / ModelObject / ModelVolume / ModelInstance / ModelMaterial are valid and unique.
void check_model_ids_validity(const Model &model)
+15
View File
@@ -745,6 +745,10 @@ public:
// Shift painted filament indices >= threshold by delta. Used when a physical filament is
// inserted ahead of existing slots (mixed-color slots are kept at the end of the list).
void shift_states_above(const ModelVolume &mv, EnforcerBlockerType threshold, int delta);
// Relabel painted filament indices according to state_map (old state value -> new state
// value; untouched states keep their identity). Used when published-3MF import relocates
// mixed-filament definitions onto new slot numbers.
void remap_states(const ModelVolume &mv, const EnforcerBlockerStateMap &state_map);
indexed_triangle_set get_facets_strict(const ModelVolume& mv, EnforcerBlockerType type) const;
bool has_facets(const ModelVolume& mv, EnforcerBlockerType type) const;
bool empty() const { return m_data.triangles_to_split.empty(); }
@@ -1565,6 +1569,10 @@ public:
std::vector<std::string> md_name;
std::vector<std::string> md_value;
// Opaque parametric CAD recipe (CadDocument::serialize_recipe()), round-tripped through
// the 3MF as Metadata/orca_cad.bin. Empty for non-CAD projects.
std::string cad_recipe;
void SetDesigner(std::string designer, std::string designer_user_id) {
if (design_info == nullptr) {
design_info = std::make_shared<ModelDesignInfo>();
@@ -1790,6 +1798,13 @@ bool model_has_multi_part_objects(const Model &model);
// If the model has advanced features, then it cannot be processed in simple mode.
bool model_has_advanced_features(const Model &model);
// Remap the model's filament-slot references after a published-3MF import relocated
// mixed-filament definitions onto new slot numbers: object/volume "extruder" configs and
// multi-material color-painting states (paint state stores the one-based slot number).
// slot_relocations maps the author's zero-based slot number to its final zero-based slot;
// entries are applied simultaneously (no chained lookups), untouched slots keep everything.
void remap_model_filament_slots(Model &model, const std::map<int, int> &slot_relocations);
#ifndef NDEBUG
// Verify whether the IDs of Model / ModelObject / ModelVolume / ModelInstance / ModelMaterial are valid and unique.
void check_model_ids_validity(const Model &model);
+1
View File
@@ -24,6 +24,7 @@ public:
explicit MultiPoint(const Points &_points) : points(_points) {}
MultiPoint& operator=(const MultiPoint &other) { points = other.points; return *this; }
MultiPoint& operator=(MultiPoint &&other) { points = std::move(other.points); return *this; }
virtual ~MultiPoint() = default;
void scale(double factor);
void scale(double factor_x, double factor_y);
void translate(double x, double y) { this->translate(Point(coord_t(x), coord_t(y))); }
+4
View File
@@ -170,6 +170,10 @@ public:
this->m_check_sum = rhs.check_sum();
this->m_connectors_cnt = rhs.connectors_cnt();
}
// A user-declared copy assignment or destructor deprecates the implicitly generated
// copy constructor, and this class has both, so declare it rather than rely on it.
CutObjectBase(const CutObjectBase &) = default;
CutObjectBase &operator=(const CutObjectBase &other)
{
this->copy(other);
-1
View File
@@ -39,7 +39,6 @@ namespace orientation {
float height_to_bottom_hull_ratio = 0; // affects stability, the lower the better
float unprintability = 0;
Eigen::VectorXf areas_cooling;
CostItems(CostItems const & other) = default;
CostItems() = default;
static std::string field_names() {
return " overhang, bottom, bothull, contour, A_laf, A_prj, unprintability";
+73 -2
View File
@@ -408,7 +408,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
ExtrusionRole role = is_external ? erExternalPerimeter : erPerimeter;
const bool is_contour = !extrusion->is_closed || pg_extrusion.is_contour;
apply_fuzzy_skin(extrusion, perimeter_generator, is_contour);
apply_fuzzy_skin(extrusion, perimeter_generator, is_contour, extrusion->is_closed);
ExtrusionPaths paths;
// detect overhanging/bridging perimeters
@@ -550,6 +550,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
if (!paths.empty()) {
if (extrusion->is_closed) {
ExtrusionLoop extrusion_loop(std::move(paths), pg_extrusion.is_contour ? elrDefault : elrHole);
extrusion_loop.inset_idx = extrusion->inset_idx;
if ((perimeter_generator.config->wall_direction == WallDirection::CounterClockwise) ==
(pg_extrusion.is_contour || pg_extrusions.size() == 2))
extrusion_loop.make_counter_clockwise();
@@ -1318,6 +1319,73 @@ static void reorient_perimeters(ExtrusionEntityCollection &entities, bool steep_
}
}
// A loop made of nothing but overhang paths lies entirely off the lower layer.
static bool is_unsupported_loop(const ExtrusionEntity *entity)
{
if (!entity->is_loop())
return false;
const ExtrusionPaths &paths = static_cast<const ExtrusionLoop *>(entity)->paths;
return !paths.empty() && std::all_of(paths.begin(), paths.end(),
[](const ExtrusionPath &path) { return path.role() == erOverhangPerimeter; });
}
// ORCA: A wall loop with nothing under it has nothing to lean on, so whatever the configured wall
// sequence it is extruded after the loops that anchor it, innermost first. A loop that runs alongside
// an anchored one belongs to the same wall stack and keeps its place ahead of the infill, which needs
// it as an anchor; one that touches nothing has only that infill to rest on, so it is flagged for the
// G-code writer to hold it back until the infill is down.
static void defer_unsupported_loops(const PerimeterGenerator &perimeter_generator, ExtrusionEntityCollection &entities)
{
if (!perimeter_generator.config->unsupported_wall_last)
return;
ExtrusionEntitiesPtr &src = entities.entities;
auto first_deferred = std::stable_partition(src.begin(), src.end(),
[](const ExtrusionEntity *entity) { return !is_unsupported_loop(entity); });
if (first_deferred == src.end())
return;
std::stable_sort(first_deferred, src.end(),
[](const ExtrusionEntity *lhs, const ExtrusionEntity *rhs) { return lhs->inset_idx > rhs->inset_idx; });
auto collect_lines = [](const ExtrusionEntity *entity, Lines &out) {
Polylines polylines;
entity->collect_polylines(polylines);
append(out, to_lines(polylines));
};
Lines anchored;
for (auto it = src.begin(); it != first_deferred; ++it)
collect_lines(*it, anchored);
std::vector<ExtrusionLoop *> unattached;
for (auto it = first_deferred; it != src.end(); ++it)
unattached.emplace_back(static_cast<ExtrusionLoop *>(*it));
// A loop leaning on a loop that is itself anchored is anchored as well, so spread outwards from
// the anchored loops until no unsupported loop is left touching what was reached.
const double touch_distance = 1.5 * std::max(perimeter_generator.ext_perimeter_flow.scaled_spacing(),
perimeter_generator.perimeter_flow.scaled_spacing());
while (!anchored.empty()) {
AABBTreeLines::LinesDistancer<Line> distancer{std::move(anchored)};
anchored.clear();
for (ExtrusionLoop *&loop : unattached) {
if (loop == nullptr)
continue;
const Points points = loop->as_polyline().points;
if (std::any_of(points.begin(), points.end(),
[&distancer, touch_distance](const Point &point) { return distancer.distance_from_lines<false>(point) < touch_distance; })) {
collect_lines(loop, anchored);
loop = nullptr;
}
}
}
for (ExtrusionLoop *loop : unattached)
if (loop != nullptr)
loop->print_after_infill = true;
}
void PerimeterGenerator::process_classic()
{
group_region_by_fuzzify(*this);
@@ -1804,6 +1872,8 @@ void PerimeterGenerator::process_classic()
}
}
defer_unsupported_loops(*this, entities);
// append perimeters for this slice as a collection
if (! entities.empty())
this->loops->append(entities);
@@ -2127,7 +2197,7 @@ void PerimeterGenerator::process_no_bridge(Surfaces& all_surfaces, coord_t perim
bridgeable_filtered = union_ex(offset_ex(remaining, perimeter_spacing), bridgeable_filtered);
bridgeable_filtered = offset_ex(bridgeable_filtered, -perimeter_spacing);
bridgeable_filtered = diff_ex(bridgeable_filtered, remaining, ApplySafetyOffset::Yes);
bridgeable_filtered = opening_ex(bridgeable_filtered, perimeter_spacing); // filter noise from the diff_ex
bridgeable_filtered = opening_ex(bridgeable_filtered, ext_perimeter_width / 2); // filter noise from the diff_ex
bridgeable_filtered = offset_ex(bridgeable_filtered, perimeter_spacing); // restore the size to the original bridgeable area
// Safety measure: Keep the bridge mask from intruding deeper into the
// supported anchor region than the explicit anchor overlap.
@@ -2742,6 +2812,7 @@ void PerimeterGenerator::process_arachne()
reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole,
this->config->overhang_reverse_internal_only);
}
defer_unsupported_loops(*this, extrusion_coll);
this->loops->append(extrusion_coll);
}
-2
View File
@@ -195,7 +195,6 @@ public:
Point(int64_t x, int32_t y) : Vec2crd(coord_t(x), coord_t(y)) {}
Point(int32_t x, int64_t y) : Vec2crd(coord_t(x), coord_t(y)) {}
Point(double x, double y) : Vec2crd(coord_t(std::round(x)), coord_t(std::round(y))) {}
Point(const Point &rhs) { *this = rhs; }
explicit Point(const Vec2d& rhs) : Vec2crd(coord_t(std::round(rhs.x())), coord_t(std::round(rhs.y()))) {}
// This constructor allows you to construct Point from Eigen expressions
// This constructor has to be implicit (non-explicit) to allow implicit conversion from Eigen expressions.
@@ -278,7 +277,6 @@ public:
Point3(int32_t x, int32_t y, int32_t z = 0) : Vec3crd(coord_t(x), coord_t(y), coord_t(z)) {}
Point3(int64_t x, int64_t y, int64_t z = 0) : Vec3crd(coord_t(x), coord_t(y), coord_t(z)) {}
Point3(double x, double y, double z = 0.0) : Vec3crd(coord_t(std::round(x)), coord_t(std::round(y)), coord_t(std::round(z))) {}
Point3(const Point3 &rhs) { *this = rhs; }
explicit Point3(const Vec2crd& vec2crd, coord_t z = 0) : Vec3crd(vec2crd.x(), vec2crd.y(), z) {}
explicit Point3(const Vec3crd &vec3crd) : Vec3crd(vec3crd) {}
// This constructor allows you to construct Point from Eigen expressions
+112 -18
View File
@@ -546,7 +546,7 @@ std::string generate_preset_setting_id(const std::string& vendor, const std::str
return "";
// Dedicated namespace for preset setting_ids, distinct from the cloud per-user
// namespace (OrcaCloudServiceAgent). Keep in sync with scripts/assign_vendor_setting_ids.py;
// namespace (OrcaCloudServiceAgent). Keep in sync with scripts/orca_profile_tool.py;
// never change this constant.
static const boost::uuids::uuid vendor_namespace =
boost::uuids::string_generator()("c1f4d9e2-7a3b-5c8d-9e0f-1a2b3c4d5e6f");
@@ -868,6 +868,20 @@ bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const Pre
return is_compatible_with_printer(preset, active_printer, &config);
}
// ORCA: see the header. The CLI resolves --load-settings into bare DynamicPrintConfigs and has no
// Preset objects to hand; without this it would have to reimplement the policy or build the shells
// at every call site.
bool is_compatible_with_printer(const DynamicPrintConfig &preset_config, Preset::Type preset_type,
const DynamicPrintConfig &printer_config, const std::string &printer_name)
{
Preset preset(preset_type, std::string("__compat_check"));
preset.config = preset_config;
Preset printer(Preset::TYPE_PRINTER, printer_name);
printer.config = printer_config;
return is_compatible_with_printer(PresetWithVendorProfile(preset, nullptr),
PresetWithVendorProfile(printer, nullptr));
}
void Preset::set_visible_from_appconfig(const AppConfig &app_config)
{
//BBS: add config related log
@@ -984,15 +998,19 @@ BedType Preset::get_default_bed_type(PresetBundle* preset_bundle)
if (config.has("default_bed_type") && !config.opt_string("default_bed_type").empty()) {
try {
std::string str_bed_type = config.opt_string("default_bed_type");
// Try parsing as integer first (legacy format)
BedType bed_type;
if (ConfigOptionEnum<BedType>::from_string(str_bed_type, bed_type) &&
bed_type > btDefault && bed_type < btCount) {
return bed_type;
}
// Try parsing as integer (legacy format)
int bed_type_value = atoi(str_bed_type.c_str());
if (bed_type_value > 0) {
if (bed_type_value > 0 && bed_type_value < BedType::btCount) {
return BedType(bed_type_value);
}
else {
BOOST_LOG_TRIVIAL(error) << "default_bed_type: invalid bed type: " << str_bed_type;
}
BOOST_LOG_TRIVIAL(error) << "default_bed_type: invalid bed type: " << str_bed_type;
return BedType::btPEI;
} catch(...) {
@@ -1041,6 +1059,7 @@ static std::vector<std::string> s_Preset_print_options{
"reduce_crossing_wall",
"detect_thin_wall",
"detect_overhang_wall",
"unsupported_wall_last",
"overhang_reverse",
"overhang_reverse_threshold",
"overhang_reverse_internal_only",
@@ -1265,6 +1284,8 @@ static std::vector<std::string> s_Preset_print_options{
"accel_to_decel_enable",
"accel_to_decel_factor",
"wipe_on_loops",
"wipe_inward",
"wipe_inward_distance",
"wipe_before_external_loop",
"bridge_density",
"internal_bridge_density",
@@ -1301,6 +1322,8 @@ static std::vector<std::string> s_Preset_print_options{
"wipe_tower_extra_flow",
"single_extruder_multi_material_priming",
"toolchange_ordering",
"toolchange_cyclic_order",
"toolchange_cyclic_first_layer",
"wipe_tower_rotation_angle",
"tree_support_branch_distance_organic",
"tree_support_branch_diameter_organic",
@@ -1426,7 +1449,7 @@ static std::vector<std::string> s_Preset_printer_options {
"gcode_skip_config_block", "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs",
"single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode",
"printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type",
"printable_height", "extruder_printable_height", "extruder_clearance_radius", "extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod",
"printable_height", "extruder_printable_height", "extruder_clearance_radius", "extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod", "extruder_clearance_dist_to_rod",
"nozzle_height", "master_extruder_id",
"default_print_profile", "inherits",
"silent_mode",
@@ -1650,7 +1673,7 @@ std::string PresetCollection::canonical_preset_name(const std::string &name, con
void PresetCollection::load_presets(
const std::string &dir_path, const std::string &subdir,
PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule substitution_rule,
std::function<void(Preset&)> preset_loaded_fn, const PresetOrigin &load_origin)
std::function<void(Preset&)> preset_loaded_fn, const PresetOrigin &load_origin, bool read_only)
{
// Don't use boost::filesystem::canonical() on Windows, it is broken in regard to reparse points,
// see https://github.com/prusa3d/PrusaSlicer/issues/732
@@ -1659,7 +1682,7 @@ void PresetCollection::load_presets(
// Load custom roots first
if (fs::exists(dir / "base")) {
load_presets(dir.string(), "base", substitutions, substitution_rule, nullptr, resolved_origin);
load_presets(dir.string(), "base", substitutions, substitution_rule, nullptr, resolved_origin, read_only);
}
//BBS: add config related logs
@@ -1667,7 +1690,8 @@ void PresetCollection::load_presets(
//BBS do not parse folder if not exists
m_dir_path = dir.string();
if (!fs::exists(dir)) {
fs::create_directory(dir);
if (!read_only)
fs::create_directory(dir);
return;
}
@@ -1717,10 +1741,10 @@ void PresetCollection::load_presets(
substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) });
if (!reason.empty()) {
fs::path file_path(preset.file);
if (fs::exists(file_path))
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
file_path.replace_extension(".info");
if (fs::exists(file_path))
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
BOOST_LOG_TRIVIAL(error) << boost::format("parse config %1% failed")%preset.file;
++m_errors;
@@ -1791,7 +1815,8 @@ void PresetCollection::load_presets(
size_t at_pos = name.find('@');
if (at_pos != std::string::npos && at_pos + 1 < name.length()) {
compatible_printers->values.push_back(name.substr(at_pos + 1));
preset.save(nullptr);
if (!read_only)
preset.save(nullptr);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << name;
}
}
@@ -1809,10 +1834,10 @@ void PresetCollection::load_presets(
++m_errors;
BOOST_LOG_TRIVIAL(error) << boost::format("The user-config cannot be loaded: %1%. Reason: %2%")%preset.file %err.what();
fs::path file_path(preset.file);
if (fs::exists(file_path))
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
file_path.replace_extension(".info");
if (fs::exists(file_path))
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
//throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + preset.file + "\n\tReason: " + err.what());
} catch (const std::runtime_error &err) {
@@ -1820,10 +1845,10 @@ void PresetCollection::load_presets(
BOOST_LOG_TRIVIAL(error) << boost::format("Failed loading the user-config file: %1%. Reason: %2%")%preset.file %err.what();
//throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + preset.file + "\n\tReason: " + err.what());
fs::path file_path(preset.file);
if (fs::exists(file_path))
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
file_path.replace_extension(".info");
if (fs::exists(file_path))
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
}
@@ -3066,6 +3091,75 @@ void PresetCollection::save_current_preset(const std::string &new_name, bool det
}
}
// A detached standalone preset for the Full Publish receiver: create a user preset holding
// the full resolved filament config (no inheritance, no vendor/alias links), parentless.
// Note: universal printer compatibility is not enforced here - callers apply
// make_publish_universal() to the config before handing it over when they need it.
// Mirrors save_current_preset(detach=true)'s creation branch but does not force-select or
// diff against a parent; the caller decides whether to select it.
// The published entry's filament_id is forwarded so user bases keep their stable
// material grouping (get_filament_presets() groups user bases by filament_id).
// The copy is a project-embedded preset: it lives inside the loaded project only
// (serialized into the saved .3mf, restored by load_project_embedded_presets) and
// never touches the user's library directory; Preset::save() early-returns for
// embedded presets, so persistence is skipped here too.
// Returns the final (uniquified) name; on collision "<base>" -> "<base> (Published)" ->
// "<base> (Published 2)" ...
std::string PresetCollection::add_detached_preset(const std::string &name_base, DynamicPrintConfig config,
const std::string &filament_id)
{
if (name_base.empty())
return std::string();
Preset stored(m_type, name_base);
stored.config = std::move(config);
stored.filament_id = filament_id;
// Uniquify verbatim; only on collision append " (Published)" then " (Published 2)".
const std::string base_name = name_base;
std::string final_name = base_name;
auto exists = [this](const std::string &candidate) -> bool {
const auto it = this->find_preset_internal(candidate);
return it != m_presets.end() && it->name == candidate;
};
if (exists(final_name)) {
final_name = base_name + " (Published)";
for (int i = 2; exists(final_name); ++i)
final_name = base_name + " (Published " + std::to_string(i) + ")";
}
// Creation branch of save_current_preset(detach=true), without its selection side
// effects or project-embedded path.
lock();
const auto it = this->find_preset_internal(final_name);
if (m_presets.begin() + m_idx_selected >= it)
++m_idx_selected;
Preset &preset = *m_presets.insert(it, stored);
preset.name = final_name;
preset.vendor = nullptr;
preset.alias.clear();
preset.renamed_from.clear();
preset.m_excluded_from.clear();
preset.setting_id.clear();
preset.inherits().clear();
preset.version = Semver::parse(SoftFever_VERSION).value_or(Semver());
preset.is_default = false;
preset.is_system = false;
preset.is_external = false;
preset.bundle_id.clear();
preset.file = this->path_for_preset(preset);
preset.is_visible = true;
preset.is_project_embedded = true;
if (m_type == Preset::TYPE_PRINT)
preset.config.option<ConfigOptionString>("print_settings_id", true)->value = final_name;
else if (m_type == Preset::TYPE_FILAMENT)
preset.config.option<ConfigOptionStrings>("filament_settings_id", true)->values[0] = final_name;
else if (m_type == Preset::TYPE_PRINTER)
preset.config.option<ConfigOptionString>("printer_settings_id", true)->value = final_name;
unlock();
return final_name;
}
bool PresetCollection::delete_current_preset()
{
Preset &selected = this->get_selected_preset();
+31 -10
View File
@@ -93,8 +93,8 @@ class PresetBundle;
// Deterministic preset setting_id: uuid5(vendor/type/name) -> 16 base62 chars.
// Pure function of a system preset's identity, so the value can be assigned by
// scripts/assign_vendor_setting_ids.py and recomputed here when a profile ships
// without it. MUST stay byte-identical to scripts/assign_vendor_setting_ids.py.
// scripts/orca_profile_tool.py and recomputed here when a profile ships without it.
// MUST stay byte-identical to scripts/orca_profile_tool.py.
// This is NOT the per-user cloud-sync setting_id
// (OrcaCloudServiceAgent::generate_uuid_for_setting_id) - do not conflate them.
std::string generate_preset_setting_id(const std::string& vendor,
@@ -459,6 +459,11 @@ protected:
bool is_compatible_with_print (const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_print, const PresetWithVendorProfile &active_printer);
bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_printer, const DynamicPrintConfig *extra_config);
bool is_compatible_with_printer(const PresetWithVendorProfile &preset, const PresetWithVendorProfile &active_printer);
// ORCA: same check for callers that hold raw configs rather than Presets (the CLI). Wraps them in
// throwaway Preset shells and delegates, so the compatibility policy -- including the fail-open on a
// malformed compatible_printers_condition -- lives in one place for the GUI and the CLI alike.
bool is_compatible_with_printer(const DynamicPrintConfig &preset_config, Preset::Type preset_type,
const DynamicPrintConfig &printer_config, const std::string &printer_name);
// Where a preset is being loaded from. `Auto` lets load_presets() infer from the directory path.
struct PresetOrigin {
@@ -558,7 +563,7 @@ public:
void add_default_preset(const std::vector<std::string> &keys, const Slic3r::StaticPrintConfig &defaults, const std::string &preset_name);
// Load ini files of the particular type from the provided directory path.
void load_presets(const std::string &dir_path, const std::string &subdir, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule, std::function<void(Preset&)> preset_loaded_fn = nullptr, const PresetOrigin &load_origin = PresetOrigin());
void load_presets(const std::string &dir_path, const std::string &subdir, PresetsConfigSubstitutions& substitutions, ForwardCompatibilitySubstitutionRule rule, std::function<void(Preset&)> preset_loaded_fn = nullptr, const PresetOrigin &load_origin = PresetOrigin(), bool read_only = false);
//BBS: update user presets directory
void update_user_presets_directory(const std::string& dir_path, const std::string& type);
@@ -631,6 +636,22 @@ public:
// All presets are marked as not modified and the new preset is activated.
//BBS: add project embedded preset logic
void save_current_preset(const std::string &new_name, bool detach = false, bool save_to_project = false, Preset* _curr_preset = nullptr);
// Insert a standalone user preset holding the full resolved config (no inheritance,
// no vendor links): the libslic3r equivalent of "Detach from parent". Takes a
// resolved config, clears parent/vendor/alias metadata, stamps filament_settings_id.
// Unlike save_current_preset it does not force-select or diff against a parent.
// Used by the published-3MF Full Publish path. The optional filament_id seeds the
// preset's stable material grouping (get_filament_presets groups user bases by
// filament_id); the published entry's filament_id is forwarded so the copy keeps
// the author's grouping.
// The copy is a project-embedded preset ("Preset Inside Project"): it lives inside
// the loaded project only, is serialized into the saved .3mf via
// get_current_project_embedded_presets(), and is never written to the user's
// library directory.
// Returns the final (uniquified) name; on collision the suffix rule is:
// "<base>" -> "<base> (Published)" -> "<base> (Published 2)" ...
std::string add_detached_preset(const std::string &name_base, DynamicPrintConfig config,
const std::string &filament_id = std::string());
// Delete the current preset, activate the first visible preset.
// returns true if the preset was deleted successfully.
@@ -840,13 +861,12 @@ public:
protected:
PresetCollection() = default;
// Copy constructor and copy operators are not to be used from outside PresetBundle,
// as the Profile::vendor points to an instance of VendorProfile stored at parent PresetBundle!
PresetCollection(const PresetCollection &other) = default;
//BBS: add operator= logic insteadof default
// Deleted by the std::recursive_mutex member. PresetBundle copies by assignment.
PresetCollection(const PresetCollection &other) = delete;
//BBS: hand-written because m_mutex cannot be copy-assigned.
PresetCollection& operator=(const PresetCollection &other);
// After copying a collection with the default operators above, call this function
// to adjust Profile::vendor pointers.
// Copying leaves every Preset::vendor pointing into the source bundle's vendor map.
// This re-points them at the matching entries in vendors.
void update_vendor_ptrs_after_copy(const VendorMap &vendors);
// Select a preset, if it exists. If it does not exist, select an invalid (-1) index.
@@ -984,7 +1004,8 @@ public:
bool only_default_printers() const;
private:
PrinterPresetCollection() = default;
PrinterPresetCollection(const PrinterPresetCollection &other) = default;
// Deleted along with the base copy constructor.
PrinterPresetCollection(const PrinterPresetCollection &other) = delete;
PrinterPresetCollection& operator=(const PrinterPresetCollection &other) = default;
friend class PresetBundle;
File diff suppressed because it is too large Load Diff
+83 -11
View File
@@ -4,11 +4,14 @@
#include "Preset.hpp"
#include "PresetCacheFormat.hpp"
#include "AppConfig.hpp"
#include "PublishSettings.hpp"
#include "enum_bitmask.hpp"
#include <memory>
#include <map>
#include <set>
#include <shared_mutex>
#include <tuple>
#include <unordered_map>
#include <optional>
#include <array>
@@ -168,6 +171,30 @@ struct PresetBundleMetadata
}
};
// A "published" 3MF project: keeps the user's currently-selected presets and overlays only the
// author-selected published keys onto the edited presets.
struct PublishedConfig
{
bool published = false;
std::vector<std::string> published_keys;
// Per-slot published material keys, applied positionally (author slot N -> receiver slot N).
// Partial entries are gated by the author's optional type requirement and written onto the
// slot's stored preset in place; full entries instead detach (see PublishedMaterialEntry in
// PublishSettings.hpp).
std::vector<PublishedMaterialEntry> material_keys;
// Keys that could not be applied (missing on the user's machine or vector size mismatch),
// filled in by load_config_file_config for notification purposes.
std::vector<std::string> skipped_keys;
// Human-readable notices of the slot material replacements performed while loading a
// published project, for the load notification.
std::vector<std::string> material_replacements;
// Mixed-filament entries that had to be moved off their authored slot on load (a real,
// physical filament occupied it): maps the author's zero-based slot number to its final
// zero-based slot. Consumers (e.g. model extruder/color-painting remapping) use this to
// keep geometry references pointing at the relocated definitions.
std::map<int, int> mixed_slot_relocations;
};
// Bundle of Print + Filament + Printer presets.
class PresetBundle
{
@@ -230,7 +257,22 @@ public:
// Load selections (current print, current filaments, current printer) from config.ini
// select preferred presets, if any exist
PresetsConfigSubstitutions load_presets(AppConfig &config, ForwardCompatibilitySubstitutionRule rule,
const PresetPreferences& preferred_selection = PresetPreferences());
const PresetPreferences& preferred_selection = PresetPreferences(),
std::string *errors = nullptr, bool read_only = false);
// Resolve an explicitly named source file through a canonical flattened
// preset. Exact loaded-file identity is preferred; otherwise a manifest-
// backed vendor tree is loaded from that source root without using caches.
bool resolve_preset_config(DynamicPrintConfig &config, Preset::Type type,
const std::string &source_file,
ForwardCompatibilitySubstitutionRule compatibility_rule,
std::string &error, bool allow_source_manifest = true);
// Resolve a source file whose JSON omits `type`. Succeeds only when exactly
// one FFF preset collection owns the file and returns that collection's type.
bool resolve_preset_config_type(DynamicPrintConfig &config, Preset::Type &type,
const std::string &source_file,
ForwardCompatibilitySubstitutionRule compatibility_rule,
std::string &error, bool allow_source_manifest = true);
// Load selections (current print, current filaments, current printer) from config.ini
// This is done just once on application start up.
@@ -238,7 +280,7 @@ public:
void load_selections(AppConfig &config, const PresetPreferences& preferred_selection = PresetPreferences());
// BBS Load user presets
PresetsConfigSubstitutions load_user_presets(std::string user, ForwardCompatibilitySubstitutionRule rule);
PresetsConfigSubstitutions load_user_presets(std::string user, ForwardCompatibilitySubstitutionRule rule, bool read_only = false);
PresetsConfigSubstitutions load_user_presets(AppConfig &config, std::map<std::string, std::map<std::string, std::string>>& my_presets, ForwardCompatibilitySubstitutionRule rule);
// Orca: Import subscribed bundle presets (load and save to disk in one operation), handles one bundle at a time
PresetsConfigSubstitutions update_subscribed_presets(AppConfig& config,
@@ -326,8 +368,9 @@ public:
// Export selections (current print, current filaments, current printer) into config.ini
void export_selections(AppConfig &config);
// BBS
void set_num_filaments(unsigned int n, std::vector<std::string> new_colors);
// n is the total slot count, and growth appends at the raw tail - which is where the mixed
// slots live. A caller adding physical filaments has to add num_mixed_filaments() on top and
// then move the new slots ahead of the mixed tail, as Sidebar::add_custom_filament does.
void set_num_filaments(unsigned int n, std::string new_col = "");
void update_num_filaments(unsigned int to_del_flament_id);
@@ -349,6 +392,13 @@ public:
std::vector<std::vector<DynamicPrintConfig>> get_extruder_filament_info() const;
std::set<std::string> get_printer_names_by_printer_type_and_nozzle(const std::string &printer_type, std::string nozzle_diameter_str, bool system_only = true);
// Orca: the root filament presets a connected machine can use, resolved with the rule the rest
// of the app applies (is_compatible_with_printer): an empty compatible_printers means every
// printer, minus the alias shadowing exclusions the Orca Filament Library records in
// Preset::m_excluded_from.
std::vector<Preset *> get_filament_presets_for_machine(const std::string &printer_type,
const std::string &nozzle_diameter_str,
bool include_user_presets);
bool check_filament_temp_equation_by_printer_type_and_nozzle_for_mas_tray(const std::string &printer_type,
std::string & nozzle_diameter_str,
std::string & setting_id,
@@ -441,8 +491,8 @@ public:
// Load configuration that comes from a model file containing configuration, such as 3MF et al.
// This method is called by the Plater.
void load_config_model(const std::string &name, DynamicPrintConfig config, Semver file_version = Semver())
{ this->load_config_file_config(name, true, std::move(config), file_version); }
void load_config_model(const std::string &name, DynamicPrintConfig config, Semver file_version = Semver(), PublishedConfig *published_config = nullptr)
{ this->load_config_file_config(name, true, std::move(config), file_version, false, published_config); }
// Load an external config file containing the print, filament and printer presets.
// Instead of a config file, a G-code may be loaded containing the full set of parameters.
@@ -473,10 +523,13 @@ public:
//Orca: load config bundle from json, pass the base bundle to support cross vendor inheritance
// Orca: `dir` is where the vendor is looked for — its own directory, whether or
// not the profile JSONs are still there. A whole-vendor load comes from the
// vendor's preset cache whenever one covers the profile on disk, and is parsed
// from the JSONs in `dir` only when none does. Nothing here reads resources.
// vendor's preset cache whenever one covers the profile on disk and allow_cache
// is true, and is parsed from the JSONs in `dir` otherwise. Nothing here reads
// resources implicitly.
std::pair<PresetsConfigSubstitutions, size_t> load_vendor_configs_from_json(
const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr);
const std::string &dir, const std::string &vendor_name, LoadConfigBundleAttributes flags,
ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle = nullptr,
bool allow_cache = true);
// Export a config bundle file containing all the presets and the names of the active presets.
//void export_configbundle(const std::string &path, bool export_system_settings = false, bool export_physical_printers = false);
@@ -503,6 +556,8 @@ public:
// How many slots are mixed. They sit at the tail of the filament list and have no nozzle of
// their own, so any resize driven by the printer's extruder count has to add this on top.
size_t num_mixed_filaments() const;
// How many slots hold a real filament, i.e. everything ahead of the mixed tail.
size_t num_physical_filaments() const;
void on_extruders_count_changed(int extruder_count);
@@ -562,6 +617,11 @@ public:
// compatible_prints references a deleted (unknown) or renamed (old) preset name.
bool check_preset_references() const;
// Validator-only: every system FFF printer variant needs a compatible system filament
// named in its model's default_materials, every name there and in the printer's
// default_filament_profile must resolve to a system filament.
bool check_printer_default_materials() const;
// Merge one vendor's presets with the other vendor's presets, report duplicates.
// Public so per-vendor-cache consumers (e.g. the setup wizard) can assemble a
// bundle out of several per-vendor caches loaded into separate PresetBundle instances.
@@ -596,6 +656,18 @@ private:
// Whether to (re)write a per-vendor cache after a JSON parse.
bool m_generate_vendor_caches { false };
bool m_preserve_vendor_source_paths { false };
// Vendor trees loaded by resolve_preset_config's manifest path, so every preset
// resolved through this bundle shares one load per source root and vendor. The
// filament library is one such tree, shared by every vendor under its root.
std::map<std::tuple<std::string, std::string, ForwardCompatibilitySubstitutionRule>, std::unique_ptr<PresetBundle>>
m_source_vendor_bundles;
const PresetBundle *load_source_vendor(const boost::filesystem::path &root_dir,
const std::string &vendor_id,
ForwardCompatibilitySubstitutionRule compatibility_rule,
std::string &error);
// Orca: validation only - flag any printer with two or more compatible
// filament presets sharing one filament_id (ambiguous AMS subtype match).
@@ -603,7 +675,7 @@ private:
//std::pair<PresetsConfigSubstitutions, std::string> load_system_presets(ForwardCompatibilitySubstitutionRule compatibility_rule);
//BBS: add json related logic
std::pair<PresetsConfigSubstitutions, std::string> load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule);
std::pair<PresetsConfigSubstitutions, std::string> load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache = true);
// Update the multicolor information for filaments.
void update_filament_multi_color();
// Update renamed_from and alias maps of system profiles.
@@ -617,7 +689,7 @@ private:
// Load print, filament & printer presets from a config. If it is an external config, then the name is extracted from the external path.
// and the external config is just referenced, not stored into user profile directory.
// If it is not an external config, then the config will be stored into the user profile directory.
void load_config_file_config(const std::string &name_or_path, bool is_external, DynamicPrintConfig &&config, Semver file_version = Semver(), bool selected = false);
void load_config_file_config(const std::string &name_or_path, bool is_external, DynamicPrintConfig &&config, Semver file_version = Semver(), bool selected = false, PublishedConfig *published_config = nullptr);
/*ConfigSubstitutions load_config_file_config_bundle(
const std::string &path, const boost::property_tree::ptree &tree, ForwardCompatibilitySubstitutionRule compatibility_rule);*/
+504 -102
View File
@@ -1,3 +1,8 @@
#ifdef _WIN32
// Keep this first. A header below reaches boost/regex, whose w32_regex_traits
// needs the Win32 types declared already.
#include <Windows.h>
#endif
#include "Config.hpp"
#include "Exception.hpp"
#include "Print.hpp"
@@ -16,6 +21,7 @@
#include "GCode.hpp"
#include "GCode/WipeTower.hpp"
#include "GCode/WipeTower2.hpp"
#include "GCode/WipeTowerEstimate.hpp"
#include "Utils.hpp"
#include "PrintConfig.hpp"
#include "MaterialType.hpp"
@@ -228,6 +234,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
"accel_to_decel_enable",
"accel_to_decel_factor",
"wipe_on_loops",
"wipe_inward",
"wipe_inward_distance",
"gcode_comments",
"gcode_label_objects",
"exclude_object",
@@ -353,6 +361,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|| opt_key == "other_layers_print_sequence"
|| opt_key == "other_layers_print_sequence_nums"
|| opt_key == "toolchange_ordering"
|| opt_key == "toolchange_cyclic_order"
|| opt_key == "toolchange_cyclic_first_layer"
|| opt_key == "extruder_ams_count"
|| opt_key == "extruder_nozzle_stats"
|| opt_key == "filament_map_mode"
@@ -957,6 +967,377 @@ StringObjectException Print::sequential_print_clearance_valid(const Print &print
return single_object_exception;
}
// ---------------------------------------------------------------------------------------------
// Clearance rule for a prime tower compacted by wipe_tower_no_sparse_layers.
// Ported from BambuStudio and adapted to Orca's printer config: Orca has no
// prime_tower_lift_height (z_hop alone bounds the spiral), spells the toolhead radius
// extruder_clearance_radius, and derives the spiral slope from the per-filament travel_slope instead
// of one global constant.
// ---------------------------------------------------------------------------------------------
double compacted_tower_footprint_padding(const PrintConfig &config, double brim_width)
{
// The brim is deposited material like any other and reaches past the wall on the first layer, so
// the sweeping rod has to clear it too.
//
// On top of it, two effects make a nominal outline fall short of the printed tower on its low
// corner even though it overshoots by millimetres on the high one: WipeTower re-centres the tower
// by rib_offset once its first-layer wall is known, and the precise check hulls extrusion centre
// lines, so the deposited material reaches half a line width further still. Allowing a line width
// per side covers both, which is what keeps an estimated footprint enclosing the real one and the
// pre-slice check stricter than the precise one.
return std::max(0., brim_width) + 2. * config.nozzle_diameter.get_at(0);
}
Polygons compacted_wipe_tower_rings(const CompactedTowerZone &zone, bool any_body_tier)
{
Polygons rings = zone.grown_nozzle;
if (any_body_tier)
append(rings, zone.grown_body);
return rings;
}
CompactedTowerZone compacted_wipe_tower_zone(const PrintConfig &config, const Polygon &tower_footprint)
{
CompactedTowerZone zone;
if (tower_footprint.points.empty())
return zone;
// Spiral Z-hop at wipe-tower entry (the G3 Z I J that GCodeWriter emits for a SpiralLift) starts on
// the tower outline at a low Z. The spiral centre sits one radius away from the start point, so the
// circle reaches 2 * radius beyond the outline. radius = lift / (2*pi*atan(travel_slope)) is the
// same formula GCodeWriter uses; both are per filament, so take the widest any filament can make.
double spiral_reach = 0.;
for (size_t i = 0; i < config.z_hop.size(); ++i) {
const double lift = std::min(double(config.z_hop.get_at(i)), 5.);
if (lift < EPSILON)
continue;
const double slope = i < config.travel_slope.size() ? double(config.travel_slope.get_at(i)) : 0.;
if (slope < EPSILON)
continue;
spiral_reach = std::max(spiral_reach, 2. * lift / (2. * PI * std::atan(slope)));
}
// Working footprint = outline grown by the spiral envelope. All later clearance tests use this, so
// a travel that leaves the deposited wall at low Z is still treated as part of the tower.
zone.hull = tower_footprint;
if (spiral_reach > EPSILON) {
const Polygons grown = offset(tower_footprint, float(scale_(spiral_reach)), jtRound, scale_(0.1));
if (! grown.empty())
zone.hull = Geometry::convex_hull(grown);
}
// The rod sweeps the whole X axis, so its keep-out band is the tower's Y span widened by half
// the nozzle-to-rod offset per side (the instance carries the other half). Orca's sequential
// check has no such margin, having had no option to read it from until now.
zone.bbox_rod = zone.hull.bounding_box();
zone.bbox_rod.offset(scale_(config.extruder_clearance_dist_to_rod.value * 0.5));
// Horizontal clearance, mirroring the sequential print check down to how the distance is split:
// there each of the two object hulls grows by half of extruder_clearance_radius, so the two
// outlines touch exactly when the objects are the full radius apart. Splitting it the same way
// here (half on the tower, half on the instance in compacted_wipe_tower_clearance) states the
// same criterion, and it is what lets the plater draw both outlines: they meet at the instant the
// check trips, instead of one of them being already buried inside the other. The smaller
// MAX_OUTER_NOZZLE_DIAMETER tier is the bare nozzle cone, the only part narrow enough to sit
// beside an object rising less than nozzle_height. The 0.2 mm shaved off is the same rounding
// slack the sequential check applies, 0.1 mm per side. Both rings are built here; which one a
// given object is measured against depends on its own height and is decided in
// compacted_wipe_tower_clearance().
zone.body_radius = config.extruder_clearance_radius.value;
zone.grown_body = offset(zone.hull, float(scale_(compacted_tower_half_clearance(zone.body_radius))), jtRound, scale_(0.1));
zone.grown_nozzle = offset(zone.hull, float(scale_(compacted_tower_half_clearance(MAX_OUTER_NOZZLE_DIAMETER))), jtRound, scale_(0.1));
return zone;
}
CompactedTowerClearance compacted_wipe_tower_clearance(const PrintConfig &config, const CompactedTowerZone &zone,
const Polygon &inst_hull, double object_rise)
{
BoundingBox inst_bbox = inst_hull.bounding_box();
inst_bbox.offset(scale_(config.extruder_clearance_dist_to_rod.value * 0.5));
// Only the Y span matters for the rod: it spans the whole X axis, so an object sharing the tower's
// Y band passes under it however far apart the two are in X.
const bool overlaps_in_y = std::min(inst_bbox.max.y(), zone.bbox_rod.max.y()) - std::max(inst_bbox.min.y(), zone.bbox_rod.min.y()) > 0;
CompactedTowerClearance result;
result.far_clearance = overlaps_in_y ? config.extruder_clearance_height_to_rod.value : config.extruder_clearance_height_to_lid.value;
// The rod and the lid are the only obstacles once the object stands far enough away. Closer than
// the toolhead radius it is the head body itself that hits the object, and it does so as soon as
// the object rises past the nozzle cone, which is far below the rod.
// The instance carries the other half of each clearance, the tower rings already hold the first
// half; see compacted_wipe_tower_zone(). Both halves are needed for the verdict to mean
// "a full radius apart", and drawing what is tested is what keeps the plater honest.
//
// Which tier applies is a property of this object alone: the head body sits above the nozzle cone,
// so it cannot reach an object that stays below nozzle_height however close it stands, and however
// tall the rest of the plate is.
const bool object_is_short = object_rise <= double(config.nozzle_height.value) + EPSILON;
result.body_clearance = object_is_short ? double(MAX_OUTER_NOZZLE_DIAMETER) : zone.body_radius;
const Polygons inst_near_nozzle = offset(inst_hull, float(scale_(compacted_tower_half_clearance(MAX_OUTER_NOZZLE_DIAMETER))), jtRound, scale_(0.1));
const bool near_nozzle = ! intersection(zone.grown_nozzle, inst_near_nozzle).empty();
result.near_body = false;
if (! object_is_short) {
const Polygons inst_near_body = offset(inst_hull, float(scale_(compacted_tower_half_clearance(zone.body_radius))), jtRound, scale_(0.1));
result.near_body = ! intersection(zone.grown_body, inst_near_body).empty();
}
result.allowed_rise = result.far_clearance;
if (near_nozzle)
result.allowed_rise = 0.;
else if (result.near_body)
result.allowed_rise = std::min(result.far_clearance, double(config.nozzle_height.value));
return result;
}
Polygon compacted_wipe_tower_offender_outline(const Polygon &inst_hull, double body_clearance)
{
// Exactly the half-clearance the check grew this instance by, so the halo drawn around an object is
// the very outline that was tested against the tower ring of the same tier. Passing the clearance
// the object was actually judged on keeps a short object from being drawn with the wide ring it is
// not subject to.
const Polygons grown = offset(inst_hull, float(scale_(compacted_tower_half_clearance(body_clearance))), jtRound, scale_(0.1));
return grown.empty() ? inst_hull : grown.front();
}
// Shared user-facing message for every compacted-tower clearance failure. Height-limit and too-close
// are the same class of layout violation under "No sparse layers", so they share one wording.
static std::string compacted_wipe_tower_clearance_error()
{
return L("The relative position of the model and the prime tower does not meet the requirements of the \"No sparse layers\" feature. Please adjust their relative positions, lower the model height, or turn off \"No sparse layers\".");
}
// Convex hull of one print instance in bed coordinates, the same outline both compacted tower checks
// compare against the tower.
static Polygon compacted_tower_print_instance_hull(const PrintObject &object, const PrintInstance &instance)
{
Points pts;
for (const ModelVolume *v : object.model_object()->volumes) {
if (! v->is_model_part())
continue;
Polygon hull = v->get_convex_hull_2d(Geometry::assemble_transform(Vec3d::Zero(), instance.model_instance->get_rotation(),
instance.model_instance->get_scaling_factor(), instance.model_instance->get_mirror()));
hull.translate(instance.shift - object.center_offset());
append(pts, hull.points);
}
return pts.empty() ? Polygon() : Geometry::convex_hull(pts);
}
// Footprint the compacted prime tower is expected to occupy on the plate, in bed coordinates.
// Before psWipeTower has run there is no tower geometry at all, so this falls back to the same
// estimate the plater builds its preview box from. Answering while the user is still arranging the
// plate is the whole point of the pre-slice check, and an estimate is all that can be had then.
static Polygon estimated_wipe_tower_footprint(const Print &print)
{
const PrintConfig &config = print.config();
const size_t filaments_cnt = print.extruders().size();
if (filaments_cnt == 0)
return Polygon();
const WipeTowerData &wtd = print.wipe_tower_data(filaments_cnt);
double width, depth, brim;
Vec2d local_min;
if (wtd.bbx.size().x() > EPSILON && wtd.bbx.size().y() > EPSILON) {
// The tower has already been generated once, so use its real box (brim included) instead of
// re-estimating. Same frame first_layer_wipe_tower_corners() works in.
width = wtd.bbx.size().x();
depth = wtd.bbx.size().y();
local_min = wtd.bbx.min + wtd.rib_offset.cast<double>();
brim = 0.;
} else {
depth = wtd.depth;
if (depth < EPSILON)
return Polygon();
// PartPlate::estimate_wipe_tower_size() squares the rib tower off and the preview box the user
// drags around is built from that, so match it here rather than keeping the nominal width.
width = config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib ? depth : double(config.prime_tower_width.value);
local_min = Vec2d::Zero();
brim = double(wtd.brim_width);
}
const double padding = compacted_tower_footprint_padding(config, brim);
local_min -= Vec2d(padding, padding);
width += 2. * padding;
depth += 2. * padding;
const Eigen::Rotation2Dd rot(Geometry::deg2rad(config.wipe_tower_rotation_angle.value));
const Vec2d translate(config.wipe_tower_x.get_at(print.get_plate_index()) + print.get_plate_origin()(0),
config.wipe_tower_y.get_at(print.get_plate_index()) + print.get_plate_origin()(1));
Polygon footprint;
for (const Vec2d &corner : { local_min,
Vec2d(local_min.x() + width, local_min.y()),
Vec2d(local_min.x() + width, local_min.y() + depth),
Vec2d(local_min.x(), local_min.y() + depth) }) {
const Vec2d p = rot * corner + translate;
footprint.points.emplace_back(scale_(p.x()), scale_(p.y()));
}
return footprint;
}
// Pre-slice counterpart of validate_compacted_wipe_tower_clearance(). It applies the very same
// clearance rule, but to an estimated tower footprint instead of the real tool-change extrusions,
// which is what lets it run from Print::validate() before anything has been sliced. Reporting through
// polygons / height_polygons rather than by throwing is what puts the collision area and the height
// limit plane on the plater, exactly the way sequential printing does it.
StringObjectException Print::compacted_wipe_tower_clearance_valid(const Print &print, Polygons *polygons, std::vector<std::pair<Polygon, float>> *height_polygons)
{
const PrintConfig &config = print.config();
if (! wipe_tower_sparse_layers_skipped(config) || config.print_sequence != PrintSequence::ByLayer || ! print.has_wipe_tower())
return {};
const CompactedTowerZone zone = compacted_wipe_tower_zone(config, estimated_wipe_tower_footprint(print));
if (zone.empty())
return {};
StringObjectException exception;
Polygons offenders;
bool body_tier_used = false;
for (const PrintObject *object : print.objects()) {
const double object_top = unscaled<double>(object->max_z());
for (const PrintInstance &instance : object->instances()) {
const Polygon inst_hull = compacted_tower_print_instance_hull(*object, instance);
if (inst_hull.points.empty())
continue;
const CompactedTowerClearance clearance = compacted_wipe_tower_clearance(config, zone, inst_hull, object_top);
body_tier_used = body_tier_used || compacted_tower_body_tier(clearance);
// Every tier the precise check applies is applied here too, otherwise an object standing
// within the toolhead radius would pass here and then be rejected mid-slice, which is the
// one outcome this check exists to prevent. The compacted tower base is unknown before
// slicing, so the rise is measured from the plate rather than from the tower top; that
// overstates it by the tower's own height and makes this check err strict, never lax.
if (object_top <= clearance.allowed_rise + EPSILON)
continue;
// Height-limit and too-close cases share one user-facing message: both mean the layout
// violates the "No sparse layers" clearance rule, and the remedies are the same.
const std::string msg = compacted_wipe_tower_clearance_error();
if (exception.string.empty()) {
exception.string = msg;
exception.object = instance.model_instance;
} else {
// Same wording for every offender; keep a single copy and drop the object pointer.
exception.object = nullptr;
}
const Polygon outline = compacted_wipe_tower_offender_outline(inst_hull, clearance.body_clearance);
offenders.emplace_back(outline);
if (height_polygons)
height_polygons->emplace_back(outline, float(clearance.allowed_rise));
}
}
// Draw the tower's keep-out ring alongside the offending objects, so the collision area reads as
// "this object reaches into the space the toolhead needs around the tower" rather than as a lone
// highlighted object. Emitted only on a real collision; the plater discards polygons otherwise.
// Only the rings some object on this plate is actually measured against are drawn, so that a ring
// and an object outline touching always means that object is over its limit.
if (polygons && ! offenders.empty()) {
append(*polygons, compacted_wipe_tower_rings(zone, body_tier_used));
append(*polygons, offenders);
}
return exception;
}
// With wipe_tower_no_sparse_layers the tower only grows on layers that carry a real toolchange,
// so it ends up far below the object and the nozzle has to descend to it. While the nozzle sits
// down on the compacted tower the rod is at tower_z + extruder_clearance_height_to_rod, and it
// sweeps the tower's Y band across the whole X axis. Anything already printed above that line and
// sharing the band gets hit. Nearer than the toolhead radius the head body hits the object well before
// the rod does, which is the horizontal half of the same problem. The spiral Z-hop that opens a wipe-
// tower travel also leaves the extrusion outline at a low Z, so the footprint used here is the
// deposited hull grown by the spiral circle's maximum reach. This mirrors both clearance checks of
// sequential printing, except that the tower is revisited over and over, so every object is compared
// against it.
void Print::validate_compacted_wipe_tower_clearance() const
{
// Nothing to check when the tower is not compacted: it then follows the object as usual and the
// regular by-layer clearance check already covers it. Asking wipe_tower_sparse_layers_skipped()
// rather than the raw option keeps this from rejecting plates whose tower is in fact full height.
if (! wipe_tower_sparse_layers_skipped(m_config) || m_config.print_sequence != PrintSequence::ByLayer)
return;
const std::vector<std::vector<WipeTower::ToolChangeResult>> &tool_changes = m_wipe_tower_data.tool_changes;
if (tool_changes.empty() || m_objects.empty())
return;
// Same accumulation the G-code emitter runs, so validation and output cannot disagree.
const std::vector<float> tower_z = compute_compacted_wipe_tower_z(tool_changes, float(m_config.z_offset.value));
// Wipe tower footprint: build it from the ACTUAL tool-change extrusions rather than the nominal
// width x depth rectangle returned by first_layer_wipe_tower_corners(). With a rib wall the printed
// wall bulges past the nominal box and the first-layer brim reaches even further; the nominal box
// (m_wipe_tower_data.bbx) undercounts that outermost extent by several millimetres, which is
// exactly the extent that decides how close the sweeping rod comes to a neighbouring object. The
// extrusion end-points are stored in the wipe-tower local frame, so we map them to the bed frame
// with the same transform the G-code emitter applies. The two emitters differ in where rib_offset
// enters: WipeTowerIntegration::append_tcr() (type 1) rotates the point and then adds the offset,
// append_tcr2() (type 2) adds it before rotating. On a rotated rib-wall tower the two land several
// millimetres apart, which is exactly the margin this check measures, so follow the emitter in use.
const Eigen::Rotation2Dd wt_rot(Geometry::deg2rad(m_config.wipe_tower_rotation_angle.value));
const Vec2d wt_translate(m_config.wipe_tower_x.get_at(m_plate_index) + m_origin(0),
m_config.wipe_tower_y.get_at(m_plate_index) + m_origin(1));
const Vec2d rib_off = m_wipe_tower_data.rib_offset.cast<double>();
const bool rib_off_rotates = this->wipe_tower_type() == WipeTowerType::Type2;
auto to_bed = [&wt_rot, &wt_translate, &rib_off, rib_off_rotates](const Vec2d &pt) {
return rib_off_rotates ? Vec2d(wt_rot * (pt + rib_off) + wt_translate) : Vec2d(wt_rot * pt + wt_translate + rib_off);
};
Points tower_pts;
for (const std::vector<WipeTower::ToolChangeResult> &layer : tool_changes) {
if (layer.empty() || wipe_tower_layer_is_sparse(layer))
continue;
for (const WipeTower::ToolChangeResult &tcr : layer)
for (size_t i = 0; i < tcr.extrusions.size(); ++i) {
// A zero width marks a travel end-point. Keep it only when it opens a real extrusion, so
// the hull covers the deposited material and nothing else; travels reach a bit further out
// than the walls do.
const WipeTower::Extrusion &e = tcr.extrusions[i];
if (e.width == 0.f && (i + 1 == tcr.extrusions.size() || tcr.extrusions[i + 1].width == 0.f))
continue;
const Vec2d p = to_bed(Vec2d(e.pos.x(), e.pos.y()));
tower_pts.emplace_back(scale_(p.x()), scale_(p.y()));
}
}
if (tower_pts.empty())
return;
const CompactedTowerZone zone = compacted_wipe_tower_zone(m_config, Geometry::convex_hull(tower_pts));
if (zone.empty())
return;
for (const PrintObject *object : m_objects) {
const double object_top = unscaled<double>(object->max_z());
for (const PrintInstance &instance : object->instances()) {
const Polygon inst_hull = compacted_tower_print_instance_hull(*object, instance);
if (inst_hull.points.empty())
continue;
// Report the worst layer rather than the first offending one, it is the one that explains the
// collision best. The rise has to be known before the clearance: it is what selects the
// horizontal tier, the nozzle cone being out of the head body's reach.
double max_rise = 0.;
for (size_t i = 0; i < tool_changes.size(); ++i) {
if (tool_changes[i].empty() || wipe_tower_layer_is_sparse(tool_changes[i]))
continue;
// Nothing above the current layer exists yet, so a tall object only counts up to it.
const double rise = std::min(object_top, double(tool_changes[i].front().print_z)) - tower_z[i];
if (rise > max_rise)
max_rise = rise;
}
const CompactedTowerClearance clearance = compacted_wipe_tower_clearance(m_config, zone, inst_hull, max_rise);
if (max_rise <= clearance.allowed_rise + EPSILON)
continue;
// Same wording as compacted_wipe_tower_clearance_valid(): height-limit and too-close
// share one message, since both are layout violations of "No sparse layers".
throw Slic3r::SlicingError(compacted_wipe_tower_clearance_error());
}
}
}
//BBS
static StringObjectException layered_print_cleareance_valid(const Print &print, StringObjectException *warning)
{
@@ -1027,20 +1408,21 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
//BBS: add the wipe tower check logic
const PrintConfig & config = print.config();
int filaments_count = print.extruders().size();
// Custom G-code tool changes (MultiAsSingle) build a real tower on a plate whose objects
// all use one filament, so they have to be counted or the hull below collapses to a point.
int filaments_count = print.extruders(true).size();
int plate_index = print.get_plate_index();
const Vec3d plate_origin = print.get_plate_origin();
float x = config.wipe_tower_x.get_at(plate_index) + plate_origin(0);
float y = config.wipe_tower_y.get_at(plate_index) + plate_origin(1);
float width = config.prime_tower_width.value;
float a = config.wipe_tower_rotation_angle.value;
//float v = config.wiping_volume.value;
float depth = print.wipe_tower_data(filaments_count).depth;
//float brim_width = print.wipe_tower_data(filaments_count).brim_width;
if (config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib)
width = depth;
// The estimate resolves the effective width (a rib wall squares the tower).
const WipeTowerData &wipe_tower_estimate = print.wipe_tower_data(filaments_count);
float width = wipe_tower_estimate.width;
float depth = wipe_tower_estimate.depth;
float brim_width = wipe_tower_estimate.brim_width;
Polygons convex_hulls_temp;
if (print.has_wipe_tower()) {
@@ -1062,36 +1444,54 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
convex_hulls_temp.push_back(wipe_tower_polygon);
}
}
// Post-generation the mesh bottom already carries the brim. Pre-generation the body grows
// by the brim only when its width is explicit; the auto brim and a Type2 cone base depend on
// the tower height, exact only once generated, so they only warn here - the exact footprint
// is re-checked in _make_wipe_tower.
const bool exact_footprint = print.is_step_done(psWipeTower);
Polygons tower_polys_checked = (!exact_footprint && config.prime_tower_brim_width.value >= 0) ?
offset(convex_hulls_temp, float(scale_(brim_width))) :
convex_hulls_temp;
Polygons tower_polys_estimated;
if (!exact_footprint && !convex_hulls_temp.empty()) {
double max_height = 0.;
for (const PrintObject *object : print.objects())
max_height = std::max(max_height, unscale_(object->size().z()));
Polygon base = estimate_wipe_tower_first_layer_outline(config, print.wipe_tower_type(), width, depth, max_height);
base.rotate(Geometry::deg2rad(a));
base.translate(Point(scale_(x), scale_(y)));
tower_polys_estimated = offset(base, float(scale_(brim_width)));
}
// Object proximity stays a body-only warning: brim near-misses would newly warn on
// many setups that print fine.
if (!intersection(convex_hulls_other, convex_hulls_temp).empty()) {
if (warning) {
warning->string += L("Prime Tower") + L(" is too close to others, and collisions may be caused.\n");
}
}
if (!intersection(exclude_polys, convex_hulls_temp).empty()) {
/*if (warning) {
warning->string += L("Prime Tower is too close to exclusion area, there may be collisions when printing.\n");
}*/
if (!intersection(exclude_polys, tower_polys_checked).empty()) {
return {L("Prime Tower") + L(" is too close to an exclusion area, and collisions will be caused.\n")};
}
if (print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, convex_hulls_temp).empty()) {
if (print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, tower_polys_checked).empty()) {
return {L("Prime Tower") + L(" is too close to clumping detection area, and collisions will be caused.\n")};
}
// Skip the containment check for towers that will never be printed (single-filament
// prints without smooth timelapse keep the config's tower position but emit nothing).
// Pre-generation only the body square is tested — the auto-brim estimate can overshoot
// the generated brim by several mm and must not hard-fail a print that physically fits.
// Post-generation the mesh bottom already includes the real brim, so the exact
// footprint is tested.
if (filaments_count > 1 || print.enable_timelapse_print()) {
// The shared printable polygon is plate-local, while the tower polygons above are
// already shifted by the plate origin.
Polygons printable_polys = print.get_extruder_shared_printable_polygon();
const Point plate_shift(scale_(plate_origin.x()), scale_(plate_origin.y()));
for (Polygon &p : printable_polys)
p.translate(plate_shift);
if (!diff(convex_hulls_temp, printable_polys).empty())
return {L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n")};
if (warning && !intersection(exclude_polys, tower_polys_estimated).empty()) {
warning->string += L("Prime Tower") + L(" is too close to exclusion area, there may be collisions when printing.") + "\n";
}
if (warning && print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, tower_polys_estimated).empty()) {
warning->string += L("Prime Tower") + L(" is too close to clumping detection area, there may be collisions when printing.") + "\n";
}
// No gate on "is there a tower": one that is not printed estimates to zero, so the hulls
// are degenerate and every check passes. Re-deriving it here missed the wrapping-detection
// tower on a single-filament plate.
Polygons printable_polys = print.get_extruder_shared_printable_polygon();
const Point plate_shift(scale_(plate_origin.x()), scale_(plate_origin.y()));
for (Polygon &p : printable_polys)
p.translate(plate_shift);
if (!diff(tower_polys_checked, printable_polys).empty())
return {L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n")};
if (warning && !diff(tower_polys_estimated, printable_polys).empty())
warning->string += L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n");
return {};
}
@@ -1382,6 +1782,16 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
}
if (!layer_warning.string.empty())
add_warning(layer_warning);
// Orca: a compacted prime tower drags the nozzle back down to the plate on every toolchange, so
// tall objects collide with it much like they do in sequential printing. Checking it here rather
// than only during slicing is what lets the plater show the collision area and the height limit
// while the plate is still being arranged.
ret = compacted_wipe_tower_clearance_valid(*this, collison_polygons, height_polygons);
if (!ret.string.empty()) {
ret.type = STRING_EXCEPT_OBJECT_COLLISION_IN_LAYER_PRINT;
return ret;
}
}
if (m_config.enable_prime_tower) {
@@ -2296,7 +2706,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
obj->clear_shared_object();
//add the print_object share check logic
auto is_print_object_the_same = [this](const PrintObject* object1, const PrintObject* object2) -> bool{
auto is_print_object_the_same = [](const PrintObject* object1, const PrintObject* object2) -> bool{
if (object1->trafo().matrix() != object2->trafo().matrix())
return false;
const ModelObject* model_obj1 = object1->model_object();
@@ -2601,6 +3011,12 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
if (this->has_wipe_tower()) {
m_fake_wipe_tower.set_pos({ m_config.wipe_tower_x.get_at(m_plate_index), m_config.wipe_tower_y.get_at(m_plate_index) });
// Validated on every process() run rather than only when the wipe tower step is (re)generated.
// Moving the tower changes only wipe_tower_x/y, which invalidates psSkirtBrim but not psWipeTower,
// so a validate call living inside _make_wipe_tower would be skipped and keep using the stale
// position, missing a fresh collision. The tower geometry (tool_changes) is stored in the local
// frame and is position independent, so re-checking here with the current position is correct.
this->validate_compacted_wipe_tower_clearance();
}
if (this->set_started(psSkirtBrim)) {
@@ -3805,7 +4221,7 @@ std::vector<Polygons> Print::get_extruder_printable_polygons() const
Polygons ploys = {Polygon::new_scale(e_printable_area)};
extruder_printable_polys.emplace_back(ploys);
}
return std::move(extruder_printable_polys);
return extruder_printable_polys;
}
std::vector<Polygons> Print::get_extruder_unprintable_polygons() const
@@ -3818,7 +4234,7 @@ std::vector<Polygons> Print::get_extruder_unprintable_polygons() const
Polygons ploys = diff(printable_poly, Polygon::new_scale(e_printable_area));
extruder_unprintable_polys.emplace_back(ploys);
}
return std::move(extruder_unprintable_polys);
return extruder_unprintable_polys;
}
size_t Print::get_extruder_id(unsigned int filament_id) const
@@ -4007,74 +4423,25 @@ bool Print::has_wipe_tower() const
const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const
{
// If the wipe tower wasn't created yet, make sure the depth and brim_width members are set to default.
double max_height = 0;
for (size_t obj_idx = 0; obj_idx < m_objects.size(); obj_idx++) {
double object_z = (double) m_objects[obj_idx]->size().z();
max_height = std::max(unscale_(object_z), max_height);
// Until the tower is generated, size it with the estimate the GUI/CLI placement uses, so
// validation cannot reject a position the clamp just accepted.
if (is_step_done(psWipeTower) || filaments_cnt == 0)
return m_wipe_tower_data;
double max_height = 0.;
double layer_height = std::numeric_limits<double>::max();
for (const PrintObject *object : m_objects) {
max_height = std::max(max_height, unscale_(double(object->size().z())));
layer_height = std::min(layer_height, object->config().layer_height.value);
}
if (max_height < EPSILON) return m_wipe_tower_data;
if (max_height < EPSILON)
return m_wipe_tower_data;
double layer_height = 0.08f; // hard code layer height
layer_height = m_objects.front()->config().layer_height.value;
auto timelapse_type = config().option<ConfigOptionEnum<TimelapseType>>("timelapse_type");
bool need_wipe_tower = (timelapse_type ? (timelapse_type->value == TimelapseType::tlSmooth) : false) | (m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib);
double extra_spacing = config().option("prime_tower_infill_gap")->getFloat() / 100.;
double rib_width = config().option("wipe_tower_rib_width")->getFloat();
double filament_change_volume = 0.;
{
std::vector<double> filament_change_lengths;
auto filament_change_lengths_opt = config().option<ConfigOptionFloats>("filament_change_length");
if (filament_change_lengths_opt) filament_change_lengths = filament_change_lengths_opt->values;
double length = filament_change_lengths.empty() ? 0 : *std::max_element(filament_change_lengths.begin(), filament_change_lengths.end());
double diameter = 1.75;
std::vector<double> diameters;
auto filament_diameter_opt = config().option<ConfigOptionFloats>("filament_diameter");
if (filament_diameter_opt) diameters = filament_diameter_opt->values;
diameter = diameters.empty() ? diameter : *std::max_element(diameters.begin(), diameters.end());
filament_change_volume = length * PI * diameter * diameter / 4.;
}
if (! is_step_done(psWipeTower) && filaments_cnt !=0) {
double wipe_volume = m_config.prime_volume;
int filament_depth_count = m_config.nozzle_diameter.values.size() == 2 ? filaments_cnt : filaments_cnt - 1;
if (filaments_cnt == 1 && enable_timelapse_print()) filament_depth_count = 1;
double volume = wipe_volume * filament_depth_count;
if (m_config.nozzle_diameter.values.size() == 2) volume += filament_change_volume * (int) (filaments_cnt / 2);
// Sizing should take into account currently set wiping volumes.
// For a long time, the initial preview would just use 900/width per toolchange (15mm on a 60mm wide tower)
// and it worked well enough. Let's try to do slightly better by accounting for the purging volumes.
const bool semm_flush = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material;
if (semm_flush) volume = WipeTower2::estimate_semm_flush_volume(m_config, filaments_cnt);
if (m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib) {
double depth = std::sqrt(volume / layer_height * extra_spacing);
if (need_wipe_tower || filaments_cnt > 1) {
float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height);
depth = std::max((double) min_wipe_tower_depth, depth);
depth += rib_width / std::sqrt(2) + config().wipe_tower_extra_rib_length.value;
const_cast<Print *>(this)->m_wipe_tower_data.depth = depth;
const_cast<Print *>(this)->m_wipe_tower_data.brim_width = m_config.prime_tower_brim_width;
}
}
else {
double width = m_config.prime_tower_width;
double depth = volume / (layer_height * width);
// The flush volumes already hold the spacing between wipes.
if (!semm_flush) depth *= extra_spacing;
if (need_wipe_tower || depth > EPSILON) {
float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height);
depth = std::max((double) min_wipe_tower_depth, depth);
}
const_cast<Print *>(this)->m_wipe_tower_data.depth = depth;
const_cast<Print *>(this)->m_wipe_tower_data.brim_width = m_config.prime_tower_brim_width;
}
if (m_config.prime_tower_brim_width < 0) const_cast<Print *>(this)->m_wipe_tower_data.brim_width = WipeTower::get_auto_brim_by_height(max_height);
}
const WipeTowerFootprint footprint = estimate_wipe_tower_footprint(m_config, this->wipe_tower_type(), this->extruders(true), layer_height, max_height);
WipeTowerData &data = const_cast<Print *>(this)->m_wipe_tower_data;
data.depth = float(footprint.depth);
data.width = float(footprint.width);
data.brim_width = float(footprint.brim_width);
return m_wipe_tower_data;
}
@@ -4300,6 +4667,7 @@ void Print::_make_wipe_tower()
m_wipe_tower_data.tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size());
wipe_tower.generate_new(m_wipe_tower_data.tool_changes);
m_wipe_tower_data.depth = wipe_tower.get_depth();
m_wipe_tower_data.width = wipe_tower.width();
m_wipe_tower_data.brim_width = wipe_tower.get_brim_width();
m_wipe_tower_data.bbx = wipe_tower.get_bbx();
m_wipe_tower_data.rib_offset = wipe_tower.get_rib_offset();
@@ -4413,6 +4781,7 @@ void Print::_make_wipe_tower()
m_wipe_tower_data.tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size());
wipe_tower.generate(m_wipe_tower_data.tool_changes);
m_wipe_tower_data.depth = wipe_tower.get_depth();
m_wipe_tower_data.width = wipe_tower.width();
m_wipe_tower_data.z_and_depth_pairs = wipe_tower.get_z_and_depth_pairs();
m_wipe_tower_data.brim_width = wipe_tower.get_brim_width();
m_wipe_tower_data.height = wipe_tower.get_wipe_tower_height();
@@ -4448,7 +4817,9 @@ void Print::_make_wipe_tower()
wipe_tower.get_wipe_tower_height(), wipe_tower.get_brim_width(),
config().wipe_tower_wall_type.value == WipeTowerWallType::wtwRib,
wipe_tower.get_rib_width(), wipe_tower.get_rib_length(),
config().wipe_tower_fillet_wall.value);
config().wipe_tower_fillet_wall.value,
config().wipe_tower_wall_type.value == WipeTowerWallType::wtwCone ?
(float) config().wipe_tower_cone_angle.value : 0.f);
const Vec3d origin = Vec3d::Zero();
// FakeWipeTower::pos is a bed-frame translation applied after rotation
// (getFakeExtrusionPathsFromWipeTower2 rotates about the local origin), so the
@@ -4461,6 +4832,28 @@ void Print::_make_wipe_tower()
config().wipe_tower_rotation_angle, config().wipe_tower_cone_angle,
{scale_(origin.x()), scale_(origin.y())});
}
// The clamps and checks above work from estimates; re-test the exact generated footprint
// so an off-plate tower fails with a clear error instead of exporting unprintable G-code
// (validate() only sees the mesh on its next run).
if (m_wipe_tower_data.wipe_tower_mesh_data) {
Polygon footprint = m_wipe_tower_data.wipe_tower_mesh_data->bottom; // includes brim and rib offset
footprint.rotate(Geometry::deg2rad(m_config.wipe_tower_rotation_angle.value));
footprint.translate(Point(scale_(m_config.wipe_tower_x.get_at(m_plate_index)),
scale_(m_config.wipe_tower_y.get_at(m_plate_index))));
const Polygons printable_polys = this->get_extruder_shared_printable_polygon();
if (!printable_polys.empty() && !diff(Polygons{footprint}, printable_polys).empty()) {
const BoundingBox fp = get_extents(footprint);
const BoundingBox pr = get_extents(printable_polys);
BOOST_LOG_TRIVIAL(error) << boost::format("wipe tower footprint [%1%,%2%]-[%3%,%4%] leaves printable [%5%,%6%]-[%7%,%8%]") %
unscaled(fp.min.x()) % unscaled(fp.min.y()) % unscaled(fp.max.x()) % unscaled(fp.max.y()) %
unscaled(pr.min.x()) % unscaled(pr.min.y()) % unscaled(pr.max.x()) % unscaled(pr.max.y());
throw Slic3r::SlicingError(L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n"));
}
// The cutter/purge corner is a physical obstacle — the brim must stay out like the body.
if (!intersection(get_bed_excluded_area(m_config), Polygons{footprint}).empty())
throw Slic3r::SlicingError(L("Prime Tower") + L(" is too close to an exclusion area, and collisions will be caused.\n"));
}
}
// Generate a recommended G-code output file name based on the format template, default extension, and template parameters
@@ -5737,7 +6130,7 @@ int Print::load_cached_data(const std::string& directory)
return CLI_IMPORT_CACHE_NOT_FOUND;
}
auto find_region = [this](PrintObject* object, size_t config_hash) -> const PrintRegion* {
auto find_region = [](PrintObject* object, size_t config_hash) -> const PrintRegion* {
int regions_count = object->num_printing_regions();
for (int index = 0; index < regions_count; index++ )
{
@@ -6032,17 +6425,26 @@ ExtrusionLayers FakeWipeTower::getTrueExtrusionLayersFromWipeTower() const
}
return wtels;
}
void WipeTowerData::construct_mesh(float width, float depth, float height, float brim_width, bool is_rib_wipe_tower, float rib_width, float rib_length,bool fillet_wall)
void WipeTowerData::construct_mesh(float width, float depth, float height, float brim_width, bool is_rib_wipe_tower, float rib_width, float rib_length,bool fillet_wall, float cone_angle)
{
wipe_tower_mesh_data = WipeTowerMeshData{};
float first_layer_height=0.08; //brim height
if (width < EPSILON || depth < EPSILON || height < EPSILON) return;
if (!is_rib_wipe_tower || rib_length < EPSILON) {
if (cone_angle > EPSILON && (!is_rib_wipe_tower || rib_length < EPSILON)) {
// Cone tower: the base bulges past the body box; this bottom polygon feeds the
// containment checks, so it must carry the bulge and the brim (cone not lofted).
wipe_tower_mesh_data->real_wipe_tower_mesh = make_cube(width, depth, height);
wipe_tower_mesh_data->bottom = WipeTower2::cone_base_polygon(width, depth, height, cone_angle);
auto brim_bottom = offset(wipe_tower_mesh_data->bottom, scaled(brim_width));
if (!brim_bottom.empty())
wipe_tower_mesh_data->bottom = brim_bottom.front();
wipe_tower_mesh_data->real_brim_mesh = WipeTower::its_make_rib_brim(wipe_tower_mesh_data->bottom, first_layer_height);
} else if (!is_rib_wipe_tower || rib_length < EPSILON) {
wipe_tower_mesh_data->real_wipe_tower_mesh = make_cube(width, depth, height);
wipe_tower_mesh_data->real_brim_mesh = make_cube(width + 2 * brim_width, depth + 2 * brim_width, first_layer_height);
wipe_tower_mesh_data->real_brim_mesh.translate({-brim_width, -brim_width, 0});
wipe_tower_mesh_data->bottom = {scaled(Vec2f{-brim_width, -brim_width}), scaled(Vec2f{width + brim_width, 0}), scaled(Vec2f{width + brim_width, depth + brim_width}),
scaled(Vec2f{0, depth})};
wipe_tower_mesh_data->bottom = {scaled(Vec2f{-brim_width, -brim_width}), scaled(Vec2f{width + brim_width, -brim_width}),
scaled(Vec2f{width + brim_width, depth + brim_width}), scaled(Vec2f{-brim_width, depth + brim_width})};
} else {
wipe_tower_mesh_data->real_wipe_tower_mesh = WipeTower::its_make_rib_tower(width, depth, height, rib_length, rib_width, fillet_wall);
wipe_tower_mesh_data->bottom = WipeTower::rib_section(width, depth, rib_length, rib_width, fillet_wall);
+92 -1
View File
@@ -782,6 +782,9 @@ struct WipeTowerData
// Depth of the wipe tower to pass to GLCanvas3D for exact bounding box:
float depth;
// Effective width (a rib wall squares the tower): the estimate until generation, then the
// generated width, so it never disagrees with depth.
float width;
std::vector<std::pair<float, float>> z_and_depth_pairs;
float brim_width;
float height;
@@ -795,12 +798,13 @@ struct WipeTowerData
used_filament.clear();
number_of_toolchanges = -1;
depth = 0.f;
width = 0.f;
brim_width = 0.f;
height = 0.f;
rib_offset = Vec2f::Zero();
wipe_tower_mesh_data = std::nullopt;
}
void construct_mesh(float width, float depth, float height, float brim_width, bool is_rib_wipe_tower, float rib_width, float rib_length, bool fillet_wall);
void construct_mesh(float width, float depth, float height, float brim_width, bool is_rib_wipe_tower, float rib_width, float rib_length, bool fillet_wall, float cone_angle = 0.f);
private:
// Only allow the WipeTowerData to be instantiated internally by Print,
@@ -1156,6 +1160,8 @@ public:
//BBS
static StringObjectException sequential_print_clearance_valid(const Print &print, Polygons *polygons = nullptr, std::vector<std::pair<Polygon, float>>* height_polygons = nullptr);
// Orca: pre-slice clearance check for a prime tower compacted by "No sparse layers".
static StringObjectException compacted_wipe_tower_clearance_valid(const Print &print, Polygons *polygons = nullptr, std::vector<std::pair<Polygon, float>>* height_polygons = nullptr);
ConflictResultOpt get_conflict_result() const { return m_conflict_result; }
// Return 4 wipe tower corners in the world coordinates (shifted and rotated), including the wipe tower brim.
@@ -1170,6 +1176,8 @@ public:
void set_calib_params(const Calib_Params& params);
const Calib_Params& calib_params() const { return m_calib_params; }
Vec2d translate_to_print_space(const Vec2d &point) const;
// Orca: precise counterpart of compacted_wipe_tower_clearance_valid(), run once the tower exists.
void validate_compacted_wipe_tower_clearance() const;
float get_wipe_tower_depth() const { return m_wipe_tower_data.depth; }
BoundingBoxf get_wipe_tower_bbx() const { return m_wipe_tower_data.bbx; }
Vec2f get_rib_offset() const { return m_wipe_tower_data.rib_offset; }
@@ -1390,6 +1398,89 @@ public:
};
// ---------------------------------------------------------------------------------------------
// Clearance rule for a prime tower compacted by wipe_tower_no_sparse_layers. Shared by the precise
// check that runs on the real extrusions, the pre-slice estimate that feeds the plater with collision
// polygons, and the plater's own live preview while the user drags the tower or an object around.
// Keeping the rule in one place is what stops those three from drifting apart and reporting different
// things for the same plate.
// ---------------------------------------------------------------------------------------------
// Half of a clearance distance, the share each of the two outlines carries. Sequential printing splits
// extruder_clearance_radius between the two object hulls this way; the tower checks split their
// clearances between the tower ring and the instance hull for the same reason, so that the two
// outlines the plater draws touch precisely when the check trips. The 0.2 mm comes off first: it is
// the rounding slack the sequential check applies, 0.1 mm per side.
inline double compacted_tower_half_clearance(double clearance) { return 0.5 * (clearance - 0.2); }
// Keep-out geometry a compacted tower projects onto the plate, derived from its bare footprint.
struct CompactedTowerZone
{
// Footprint the checks work on: the raw outline grown by the spiral Z-hop envelope.
Polygon hull;
// hull grown by half the toolhead radius; an object whose own half-grown hull reaches into it is
// hit by the head body. This is also the ring the plater draws.
Polygons grown_body;
// hull grown by half the bare nozzle cone radius, the innermost tier.
Polygons grown_nozzle;
// hull bounding box, the Y band the rod sweeps.
BoundingBox bbox_rod;
// Full body clearance, of which grown_body carries half. Which of the two tiers applies is decided
// per object rather than here; see compacted_wipe_tower_clearance().
double body_radius { 0. };
bool empty() const { return hull.points.empty(); }
};
// Per-side padding a bare wipe tower outline needs before the clearance checks may treat it as the
// tower's footprint. Callers whose outline already carries the first-layer brim pass zero for it.
// Shared by the pre-slice estimate and the plater's live preview: both start from an outline that
// falls short of the printed tower in the same two ways, and padding them by different amounts is
// exactly how the preview and the validation behind it would end up disagreeing.
double compacted_tower_footprint_padding(const PrintConfig &config, double brim_width);
// Grow a bare tower footprint (bed frame, scaled) into its keep-out zone.
CompactedTowerZone compacted_wipe_tower_zone(const PrintConfig &config, const Polygon &tower_footprint);
// How far an object may rise above the compacted tower base before the toolhead hits it.
struct CompactedTowerClearance
{
// Height the object may reach above the tower base. Zero means it may not rise at all.
double allowed_rise;
// Clearance that applies once the object stands clear of the toolhead in XY, i.e. rod or lid.
double far_clearance;
// The object sits within the toolhead radius, so the head body limits it rather than the rod.
bool near_body;
// Horizontal clearance this particular object has to keep from the tower: the full toolhead
// radius once it rises past the nozzle cone, the bare cone while it stays below. It is what the
// error message quotes and what the plater grows the object outline by.
double body_clearance;
};
// object_rise is the height above the tower base that the caller is going to compare against
// allowed_rise. It also selects the horizontal tier, so the two cannot disagree.
CompactedTowerClearance compacted_wipe_tower_clearance(const PrintConfig &config, const CompactedTowerZone &zone,
const Polygon &inst_hull, double object_rise);
// This object was judged on a tier reaching past the bare nozzle cone, so the wide ring is the one its
// outline has to be drawn against.
inline bool compacted_tower_body_tier(const CompactedTowerClearance &clearance)
{
return clearance.body_clearance > double(MAX_OUTER_NOZZLE_DIAMETER);
}
// Keep-out rings to draw around the tower. The nozzle one always applies; the wide body one is drawn
// only when some object on the plate is actually measured against it, otherwise it would show a
// keep-out zone no object can violate.
Polygons compacted_wipe_tower_rings(const CompactedTowerZone &zone, bool any_body_tier);
// Outline to hand the plater for an offending object: the instance hull grown by the same half
// clearance the check grew it by, which is CompactedTowerClearance::body_clearance for that object.
// Sequential printing reports its hulls the same way, and it doubles as the fix for the bare hull
// being unusable on screen, where drawn flat it hides under the object and drawn at the height limit
// it ends up buried inside the mesh.
Polygon compacted_wipe_tower_offender_outline(const Polygon &inst_hull, double body_clearance);
} /* slic3r_Print_hpp_ */
#endif
+5 -3
View File
@@ -560,9 +560,11 @@ static inline bool model_volume_solid_or_modifier(const ModelVolume &mv)
static inline Transform3f trafo_for_bbox(const Transform3d &object_trafo, const Transform3d &volume_trafo)
{
Transform3d m = object_trafo * volume_trafo;
m.translation().x() = 0.;
m.translation().y() = 0.;
// Orca: Keep the volume's local XY offset for multipart overlap checks, but remove the object's bed placement.
Transform3d object_trafo_local = object_trafo;
object_trafo_local.translation().x() = 0.;
object_trafo_local.translation().y() = 0.;
Transform3d m = object_trafo_local * volume_trafo;
return m.cast<float>();
}
+272 -218
View File
@@ -275,6 +275,7 @@ static t_config_enum_values s_keys_map_InfillPattern {
{ "tpmsfk", ipTpmsFK },
{ "gyroid", ipGyroid },
{ "concentric", ipConcentric },
{ "spiralinset", ipSpiralInset },
{ "hilbertcurve", ipHilbertCurve },
{ "archimedeanchords", ipArchimedeanChords },
{ "octagramspiral", ipOctagramSpiral }
@@ -371,6 +372,7 @@ static t_config_enum_values s_keys_map_SupportMaterialInterfacePattern {
{ "auto", smipAuto },
{ "rectilinear", smipRectilinear },
{ "concentric", smipConcentric },
{ "spiralinset", smipSpiralInset },
{ "rectilinear_interlaced", smipRectilinearInterlaced},
{ "grid", smipGrid }
};
@@ -1130,8 +1132,7 @@ void PrintConfigDef::init_fff_params()
// BBS
def = this->add("supertack_plate_temp", coInts);
def->label = L("Other layers");
def->tooltip = L("Bed temperature for layers except the initial one. "
"A value of 0 means the filament does not support printing on the Cool Plate SuperTack.");
def->tooltip = L("This is the bed temperature for layers except for the first one. A value of 0 means the filament does not support printing on the Cool Plate SuperTack.");
def->sidetext = L(u8"\u2103" /* °C */); // degrees Celsius, CIS languages need translation
def->full_label = L("Bed temperature");
def->min = 0;
@@ -2293,6 +2294,7 @@ void PrintConfigDef::init_fff_params()
def->enum_values.push_back("rectilinear");
def->enum_values.push_back("alignedrectilinear");
def->enum_values.push_back("concentric");
def->enum_values.push_back("spiralinset");
def->enum_values.push_back("hilbertcurve");
def->enum_values.push_back("archimedeanchords");
def->enum_values.push_back("octagramspiral");
@@ -2301,6 +2303,7 @@ void PrintConfigDef::init_fff_params()
def->enum_labels.push_back(L("Rectilinear"));
def->enum_labels.push_back(L("Aligned Rectilinear"));
def->enum_labels.push_back(L("Concentric"));
def->enum_labels.push_back(L("Spiral Inset"));
def->enum_labels.push_back(L("Hilbert Curve"));
def->enum_labels.push_back(L("Archimedean Chords"));
def->enum_labels.push_back(L("Octagram Spiral"));
@@ -2383,7 +2386,7 @@ void PrintConfigDef::init_fff_params()
def->label = L("Top surface fill order");
def->category = L("Strength");
def->tooltip = L("Direction in which top surfaces are filled when using a center-based pattern "
"(Concentric, Archimedean Chords, Octagram Spiral).\n"
"(Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n"
"Outward starts at the center of the surface, so any excess material is pushed "
"towards the edge where it is least visible. Inward starts at the edge and ends "
"with the tight curves at the center.\n"
@@ -2402,7 +2405,7 @@ void PrintConfigDef::init_fff_params()
def->label = L("Bottom surface fill order");
def->category = L("Strength");
def->tooltip = L("Direction in which bottom surfaces are filled when using a center-based pattern "
"(Concentric, Archimedean Chords, Octagram Spiral).\n"
"(Concentric, Spiral Inset, Archimedean Chords, Octagram Spiral).\n"
"Inward starts each surface with the wider outer curves, which improves first layer "
"adhesion on build plates where the tight curves at the center may not stick. "
"Outward starts at the center, pushing any excess material towards the edge.\n"
@@ -2546,6 +2549,16 @@ void PrintConfigDef::init_fff_params()
def->enum_labels.push_back("5");
def->mode = comAdvanced;
// Orca: already carried by the BBL/Qidi/Geeetech/Eryone machine profiles, which inherited it from
// the BambuStudio import; without a definition here it was parsed as an unknown key and dropped.
def = this->add("extruder_clearance_dist_to_rod", coFloat);
def->label = L("Distance to rod");
def->tooltip = L("Horizontal distance of the nozzle tip to the rod's farther edge. Used for collision avoidance in by-object printing.");
def->sidetext = L("mm"); // millimeters, CIS languages need translation
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(40));
def = this->add("extruder_clearance_height_to_rod", coFloat);
def->label = L("Height to rod");
def->tooltip = L("Distance from the nozzle tip to the lower rod. Used for collision avoidance in by-object printing.");
@@ -2770,7 +2783,7 @@ void PrintConfigDef::init_fff_params()
def = this->add("fan_cooling_layer_time", coFloats);
def->label = L("Layer time");
def->tooltip = L("The part cooling fan will be enabled for layers where the estimated time is shorter than this value. Fan speed is interpolated between the minimum and maximum fan speeds according to layer printing time.");
def->sidetext = L("s"); // seconds, CIS languages need translation
def->sidetext = L_CONTEXT("s", "second"); // seconds, CIS languages need translation
def->min = 0;
def->max = 1000;
def->mode = comSimple;
@@ -2923,7 +2936,7 @@ void PrintConfigDef::init_fff_params()
def->label = L("Filament load time");
def->tooltip = L("Time to load new filament when switch filament. It's usually applicable for single-extruder multi-material machines. "
"For tool changers or multi-tool machines, it's typically 0. For statistics only.");
def->sidetext = L("s"); // seconds, CIS languages need translation
def->sidetext = L_CONTEXT("s", "second"); // seconds, CIS languages need translation
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0.0));
@@ -2932,7 +2945,7 @@ void PrintConfigDef::init_fff_params()
def->label = L("Filament unload time");
def->tooltip = L("Time to unload old filament when switch filament. It's usually applicable for single-extruder multi-material machines. "
"For tool changers or multi-tool machines, it's typically 0. For statistics only.");
def->sidetext = L("s"); // seconds, CIS languages need translation
def->sidetext = L_CONTEXT("s", "second"); // seconds, CIS languages need translation
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0.0));
@@ -2941,7 +2954,7 @@ void PrintConfigDef::init_fff_params()
def->label = L("Tool change time");
def->tooltip = L("Time taken to switch tools. It's usually applicable for tool changers or multi-tool machines. "
"For single-extruder multi-material machines, it's typically 0. For statistics only.");
def->sidetext = L("s"); // seconds, CIS languages need translation
def->sidetext = L_CONTEXT("s", "second"); // seconds, CIS languages need translation
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat { 0. });
@@ -3087,7 +3100,7 @@ void PrintConfigDef::init_fff_params()
def->tooltip = L("Time to wait after the filament is unloaded. "
"May help to get reliable tool changes with flexible materials "
"that may need more time to shrink to original dimensions.");
def->sidetext = L("s"); // seconds, CIS languages need translation
def->sidetext = L_CONTEXT("s", "second"); // seconds, CIS languages need translation
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloats { 0. });
@@ -4206,7 +4219,7 @@ void PrintConfigDef::init_fff_params()
"\nIt won't move fan commands from custom G-code (they act as a sort of 'barrier')."
"\nIt won't move fan commands into the start G-code if the 'only custom start G-code' is activated."
"\nUse 0 to deactivate.");
def->sidetext = L("s"); // seconds, CIS languages need translation
def->sidetext = L_CONTEXT("s", "second"); // seconds, CIS languages need translation
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0));
@@ -4222,7 +4235,7 @@ void PrintConfigDef::init_fff_params()
"\nThis is useful for fans where a low PWM/power may be insufficient to get the fan started spinning from a stop, or to "
"get the fan up to speed faster."
"\nSet to 0 to deactivate.");
def->sidetext = L("s"); // seconds, CIS languages need translation
def->sidetext = L_CONTEXT("s", "second"); // seconds, CIS languages need translation
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0));
@@ -4866,7 +4879,7 @@ void PrintConfigDef::init_fff_params()
def->label = L("Ironing expansion");
def->category = L("Quality");
def->tooltip = L("Expand or contract the ironing area.");
def->sidetext = L("mm");
def->sidetext = L("mm"); // millimeters, CIS languages need translation
def->min = -100;
def->max = 100;
def->mode = comExpert;
@@ -4903,7 +4916,7 @@ void PrintConfigDef::init_fff_params()
def->category = L("Quality");
def->tooltip = L("Minimum Z-layer height.\n"
"Also controls the slicing plane.");
def->sidetext = L("mm");
def->sidetext = L("mm"); // millimeters, CIS languages need translation
def->min = 0;
def->max = 100;
def->mode = comExpert;
@@ -5102,7 +5115,7 @@ void PrintConfigDef::init_fff_params()
def->category = L("Machine limits");
def->readonly = false;
def->tooltip = L("The allowed maximum output force of Y axis");
def->sidetext = L("N");
def->sidetext = L_CONTEXT("N", "Newton"); // Newtons, CIS languages need translation
def->min = 0;
def->mode = comDevelop;
def->set_default_value(new ConfigOptionFloat(0));
@@ -5112,7 +5125,7 @@ void PrintConfigDef::init_fff_params()
def->category = L("Machine limits");
def->readonly = false;
def->tooltip = L("The machine bed mass load of Y axis");
def->sidetext = L("g");
def->sidetext = L_CONTEXT("g", "gram"); // grams, CIS languages need translation
def->min = 0;
def->mode = comDevelop;
def->set_default_value(new ConfigOptionFloat(0));
@@ -5122,7 +5135,7 @@ void PrintConfigDef::init_fff_params()
def->category = L("Machine limits");
def->readonly = false;
def->tooltip = L("The allowed max printed mass on a plate");
def->sidetext = L("g");
def->sidetext = L_CONTEXT("g", "gram"); // grams, CIS languages need translation
def->min = 0;
def->mode = comDevelop;
def->set_default_value(new ConfigOptionFloat(0));
@@ -5427,7 +5440,7 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->readonly = false;
def->nullable = true;
def->set_default_value(new ConfigOptionFloatsNullable { {0.0} });
def->set_default_value(new ConfigOptionFloatsNullable { 0.0 });
def = this->add("cooling_tube_retraction", coFloat);
def->label = L("Cooling tube position");
@@ -5481,7 +5494,7 @@ void PrintConfigDef::init_fff_params()
def = this->add("reduce_infill_retraction", coBool);
def->label = L("Reduce infill retraction");
def->tooltip = L("Don\'t retract when the travel is entirely within an infill area. That means the oozing can\'t been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that z-hop is also not performed in areas where retraction is skipped.");
def->tooltip = L("Don\'t retract when the travel is entirely within an infill area. That means the oozing can\'t been seen. This can reduce times of retraction for complex model and save printing time, but make slicing and G-code generating slower. Note that Z-hop is also not performed in areas where retraction is skipped.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
@@ -5534,6 +5547,16 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(true));
def = this->add("unsupported_wall_last", coBool);
def->label = L("Print unsupported walls last");
def->category = L("Quality");
def->tooltip = L("Wall loops that lie entirely in mid air are printed once something can hold them:\n"
"they are extruded after the other walls of their island, innermost first, whatever the wall order is.\n"
"A loop that only the bridges of this layer can anchor waits until those bridges are printed, while a loop running "
"alongside a supported wall keeps its place before the infill, which needs it as an anchor.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("outer_wall_filament_id", coInt);
def->gui_type = ConfigOptionDef::GUIType::i_enum_open;
def->label = L("Outer walls");
@@ -5736,7 +5759,7 @@ void PrintConfigDef::init_fff_params()
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"
def->tooltip = L("This is 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 = "%";
def->mode = comExpert;
@@ -5802,7 +5825,7 @@ void PrintConfigDef::init_fff_params()
def = this->add("z_hop", coFloats);
def->label = L("Z-hop height");
def->tooltip = L("Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift z can prevent stringing.");
def->tooltip = L("Whenever there is a retraction, the nozzle is lifted a little to create clearance between the nozzle and the print. This prevents the nozzle from hitting the print when traveling more. Using spiral lines to lift Z can prevent stringing.");
def->sidetext = L("mm"); // millimeters, CIS languages need translation
def->mode = comSimple;
def->min = 0;
@@ -6264,6 +6287,36 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("wipe_inward", coBool);
def->label = L("Wipe inward");
def->category = L("Quality");
def->tooltip = L("Applies only to external walls, including hole boundaries. Moves the hot nozzle toward printed "
"inner walls during wiping to reduce reheating of freshly printed plastic and seam marks.\n\n"
"Especially useful at layer heights below 0.1 mm, where wipe marks are more visible.\n\n"
"Uses the regular wipe if no adjacent inner wall is already printed (single-wall areas or "
"Outer/Inner wall order), or if no supported inward path can be found, for example at tight "
"corners or seam gaps.");
def->mode = comExpert;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("wipe_inward_distance", coFloatOrPercent);
def->label = L("Wipe inward distance");
def->category = L("Quality");
// xgettext:no-c-format, no-boost-format
def->tooltip = L("The distance the wipe path is shifted away from the external perimeter, specified in millimeters "
"or as a percentage of the actual outer-wall extrusion width.\n\n"
"For example, 50% shifts the path by half of the outer-wall width. The effective offset is limited "
"by both the actual outer-wall width and the available spacing to the adjacent wall, so values "
"above 100% or an equivalent absolute distance have no additional effect. "
"Set to 0 to disable the offset.");
def->sidetext = L("mm or %");
def->ratio_over = "outer_wall_line_width";
def->min = 0;
def->max = 100;
def->max_literal = 2; // Orca: G-code generation also clamps literal values to the actual outer-wall width.
def->mode = comExpert;
def->set_default_value(new ConfigOptionFloatOrPercent(50, true));
def = this->add("wipe_before_external_loop", coBool);
def->label = L("Wipe before external loop");
def->category = L("Quality");
@@ -6380,7 +6433,7 @@ void PrintConfigDef::init_fff_params()
def->label = L("Layer time");
def->tooltip = L("The printing speed in exported G-code will be slowed down when the estimated layer time is "
"shorter than this value in order to get better cooling for these layers.");
def->sidetext = L("s"); // seconds, CIS languages need translation
def->sidetext = L_CONTEXT("s", "second"); // seconds, CIS languages need translation
def->min = 0;
def->max = 1000;
def->mode = comSimple;
@@ -6533,7 +6586,7 @@ void PrintConfigDef::init_fff_params()
def->label = L("Preheat time");
def->tooltip = L("To reduce the waiting time after tool change, Orca can preheat the next tool while the current tool is still in use. "
"This setting specifies the time in seconds to preheat the next tool. Orca will insert a M104 command to preheat the tool in advance.");
def->sidetext = L("s"); // seconds, CIS languages need translation
def->sidetext = L_CONTEXT("s", "second"); // seconds, CIS languages need translation
def->min = 0;
def->max = 120;
def->mode = comAdvanced;
@@ -6641,8 +6694,10 @@ void PrintConfigDef::init_fff_params()
def = this->add("wipe_tower_no_sparse_layers", coBool);
def->label = L("No sparse layers (beta)");
def->tooltip = L("If enabled, the wipe tower will not be printed on layers with no tool changes. "
"On layers with a tool change, extruder will travel downward to print the wipe tower. "
"User is responsible for ensuring there is no collision with the print.");
"On layers with a tool change, extruder will travel downward to print the wipe tower, "
"so the tower ends up below the model and the toolhead has to reach down to it. "
"Layouts where that would collide with an already printed object are rejected. "
"Has no effect with smooth timelapse or clumping detection, which need a tower on every layer.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
@@ -6668,6 +6723,34 @@ void PrintConfigDef::init_fff_params()
def->enum_labels.emplace_back(L("Cyclic"));
def->set_default_value(new ConfigOptionEnum<ToolChangeOrderingType>(ToolChangeOrderingType::Default));
def = this->add("toolchange_cyclic_order", coString);
def->label = L("Cyclic order");
def->category = L("Advanced");
def->tooltip = L(
"Custom filament sequence used by the cyclic toolchange ordering, as filament numbers separated by commas (e.g. \"3,2,1,4\").\n"
"Each layer prints its filaments following this sequence; filaments not listed are printed last, in ascending order.\n"
"Leave empty to cycle through the filaments in ascending order."
);
def->mode = comExpert;
def->set_default_value(new ConfigOptionString(""));
def = this->add("toolchange_cyclic_first_layer", coBool);
def->label = L("Apply cyclic order to first layer");
def->category = L("Advanced");
def->tooltip = L(
"Applies the cyclic toolchange order to the first layer as well.\n"
"By default this is disabled, because the first layer is instead ordered for the best bed "
"adhesion: filaments that print small, fragile first-layer features are printed last, so the "
"following tool changes and travel moves are less likely to knock those weakly anchored parts "
"loose. This first-layer order also honors a custom first layer filament sequence when one is set. "
"The cyclic order's benefit (extra tool changes give each layer more time to cool) does not apply "
"to the first layer, which is printed slowly and hot for adhesion.\n"
"Enable this only if you need the exact same tool sequence on every layer, including the first, at "
"the cost of that adhesion optimization."
);
def->mode = comExpert;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("slice_closing_radius", coFloat);
def->label = L("Slice gap closing radius");
def->category = L("Quality");
@@ -6680,7 +6763,7 @@ void PrintConfigDef::init_fff_params()
def = this->add("slicing_mode", coEnum);
def->label = L("Slicing Mode");
def->category = L("Other");
def->category = L("Others");
def->tooltip = L("Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model.");
def->enum_keys_map = &ConfigOptionEnum<SlicingMode>::get_enum_values();
def->enum_values.push_back("regular");
@@ -6964,11 +7047,13 @@ void PrintConfigDef::init_fff_params()
def->enum_values.push_back("auto");
def->enum_values.push_back("rectilinear");
def->enum_values.push_back("concentric");
def->enum_values.push_back("spiralinset");
def->enum_values.push_back("rectilinear_interlaced");
def->enum_values.push_back("grid");
def->enum_labels.push_back(L("Default"));
def->enum_labels.push_back(L("Rectilinear"));
def->enum_labels.push_back(L("Concentric"));
def->enum_labels.push_back(L("Spiral Inset"));
def->enum_labels.push_back(L("Rectilinear Interlaced"));
def->enum_labels.push_back(L("Grid"));
def->mode = comAdvanced;
@@ -8025,7 +8110,7 @@ void PrintConfigDef::init_fff_params()
def = this->add("machine_hotend_change_time", coFloat);
def->label = L("Hotend change time");
def->tooltip = L("Time to change hotend.");
def->sidetext = L("s");
def->sidetext = L_CONTEXT("s", "second");
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0.0));
@@ -9866,7 +9951,15 @@ std::string DynamicPrintConfig::get_filament_type(std::string &displayed_filamen
auto* filament_type = dynamic_cast<const ConfigOptionStrings*>(this->option("filament_type"));
auto* filament_is_support = dynamic_cast<const ConfigOptionBools*>(this->option("filament_is_support"));
if (!filament_type)
// get_at() on an empty vector option is undefined behavior (.front() of an empty vector),
// and e.g. filament_id is never populated on a CLI from-scratch slice - treat an empty
// option the same as a missing one.
if (filament_id && filament_id->values.empty())
filament_id = nullptr;
if (filament_is_support && filament_is_support->values.empty())
filament_is_support = nullptr;
if (!filament_type || filament_type->values.empty())
return "";
if (!filament_is_support) {
@@ -10091,6 +10184,10 @@ int DynamicPrintConfig::update_values_from_single_to_multi(DynamicPrintConfig& m
for (int index = 0; index < variant_count; index++)
{
//variant_count is the variant column width, src_opt the value array;
//they disagree when the source was authored at a different width
if (index >= (int)src_opt->values.size())
break;
if (opt->values[index] > src_opt->values[index])
opt->values[index] = src_opt->values[index];
}
@@ -10108,6 +10205,8 @@ int DynamicPrintConfig::update_values_from_single_to_multi(DynamicPrintConfig& m
for (int index = 0; index < variant_count; index++)
{
if (index >= (int)src_opt->values.size())
break;
if (opt->values[index].value > src_opt->values[index].value)
opt->values[index] = src_opt->values[index];
}
@@ -10302,6 +10401,10 @@ int DynamicPrintConfig::update_values_from_multi_to_multi(DynamicPrintConfig& ne
for(auto idx : variant_indices){
assert(idx < old_count);
//the counts come from the variant columns, the arrays from the options;
//they disagree when a config was authored at a different variant width
if (idx >= old_count || new_variant_index >= (int)opt->values.size())
continue;
if (old_values[idx] < opt->values[new_variant_index])
opt->values[new_variant_index] = old_values[idx];
}
@@ -10332,6 +10435,10 @@ int DynamicPrintConfig::update_values_from_multi_to_multi(DynamicPrintConfig& ne
for(auto idx : variant_indices){
assert(idx < old_count);
//the counts come from the variant columns, the arrays from the options;
//they disagree when a config was authored at a different variant width
if (idx >= old_count || new_variant_index >= (int)opt->values.size())
continue;
if (old_values[idx] < opt->values[new_variant_index])
opt->values[new_variant_index] = old_values[idx];
}
@@ -10362,6 +10469,8 @@ int DynamicPrintConfig::update_values_from_multi_to_multi(DynamicPrintConfig& ne
for(auto idx : variant_indices){
assert(idx < old_count);
if (idx >= old_count || new_variant_index >= (int)opt->values.size())
continue;
if (old_values[idx]) //enabled
opt->values[new_variant_index] = old_values[idx];
}
@@ -10402,6 +10511,15 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector<st
same_variant_indices.emplace_back(indices);
}
//dst_values below is the destination PRINT preset's per-variant row, sized to its own
//print_extruder_variant; dst_extruder_variants is the PRINTER's list. They disagree until
//the print preset is re-selected, so size the row to the variant count before indexing it.
const size_t dst_variant_count = dst_extruder_variants.size();
if (dst_variant_count == 0) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(", Line %1%: empty destination variant list")%__LINE__;
return -1;
}
t_config_option_keys keys = this->keys();
for(auto& key : keys){
if(key_sets.find(key) == key_sets.end())
@@ -10417,7 +10535,13 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector<st
{
ConfigOptionFloatsNullable* opt = this->option<ConfigOptionFloatsNullable>(key);
auto src_values = opt->values;
auto dst_values = dst_config.option<ConfigOptionFloatsNullable>(key) ->values;
const auto* dst_opt = dst_config.option<ConfigOptionFloatsNullable>(key);
if(!dst_opt){
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: %2% missing from destination config")%__LINE__%key;
break;
}
auto dst_values = dst_opt->values;
dst_values.resize(dst_variant_count, ConfigOptionFloatsNullable::nil_value());
for(size_t dst_idx =0; dst_idx < same_variant_indices.size(); ++dst_idx){
auto& indices = same_variant_indices[dst_idx];
if(indices.empty())
@@ -10425,7 +10549,7 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector<st
bool has_value = false;
double target_value = std::numeric_limits<double>::max();
for(auto idx : indices){
if(opt && idx < opt->values.size() && !opt->is_nil(idx)){
if(idx < (int)opt->values.size() && !opt->is_nil(idx)){
has_value = true;
target_value = std::min(target_value, src_values[idx]);
}
@@ -10441,7 +10565,13 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector<st
{
ConfigOptionFloatsOrPercentsNullable* opt = this->option<ConfigOptionFloatsOrPercentsNullable>(key);
auto src_values = opt->values;
auto dst_values = dst_config.option<ConfigOptionFloatsOrPercentsNullable>(key) ->values;
const auto* dst_opt = dst_config.option<ConfigOptionFloatsOrPercentsNullable>(key);
if(!dst_opt){
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: %2% missing from destination config")%__LINE__%key;
break;
}
auto dst_values = dst_opt->values;
dst_values.resize(dst_variant_count, ConfigOptionFloatsOrPercentsNullable::nil_value());
for(size_t dst_idx =0; dst_idx < same_variant_indices.size(); ++dst_idx){
auto& indices = same_variant_indices[dst_idx];
if(indices.empty())
@@ -10449,7 +10579,7 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector<st
bool has_value = false;
FloatOrPercent target_value{9999.f, true};
for(auto idx : indices){
if(opt && !opt->is_nil(idx)){
if(idx < (int)opt->values.size() && !opt->is_nil(idx)){
has_value = true;
target_value = src_values[idx].value < target_value.value ? src_values[idx] : target_value;
}
@@ -10465,15 +10595,21 @@ int DynamicPrintConfig::update_values_from_multi_to_multi_2(const std::vector<st
{
ConfigOptionBoolsNullable* opt = this->option<ConfigOptionBoolsNullable>(key);
auto src_values = opt->values;
auto dst_values = dst_config.option<ConfigOptionBoolsNullable>(key) ->values;
const auto* dst_opt = dst_config.option<ConfigOptionBoolsNullable>(key);
if(!dst_opt){
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: %2% missing from destination config")%__LINE__%key;
break;
}
auto dst_values = dst_opt->values;
dst_values.resize(dst_variant_count, ConfigOptionBoolsNullable::nil_value());
for(size_t dst_idx =0; dst_idx < same_variant_indices.size(); ++dst_idx){
auto indices = same_variant_indices[dst_idx];
if(indices.empty())
continue;
bool has_value = false;
bool target_value;
bool target_value = false;
for(auto idx : indices){
if(opt && !opt->is_nil(idx)){
if(idx < (int)opt->values.size() && !opt->is_nil(idx)){
has_value = true;
target_value = src_values[idx];
break;
@@ -10880,6 +11016,28 @@ std::vector<int> DynamicPrintConfig::update_values_to_printer_extruders(DynamicP
return variant_index;
}
// Regathers a vector option's values through per-slot source indices (one input index per
// output slot). Out-of-range indices keep the first value, matching get_at's fallback.
template<typename OptType, typename ValueType>
static void gather_option_values(const char *caller, const std::string &key, OptType *opt, const std::vector<int> &slot_param_indices)
{
if (!opt || opt->values.empty()) {
BOOST_LOG_TRIVIAL(warning) << caller << boost::format(", Line %1%: option %2% not found or empty, skipping")%__LINE__%key;
return;
}
std::vector<ValueType> new_values;
new_values.reserve(slot_param_indices.size());
for (int idx : slot_param_indices) {
if (idx < 0 || static_cast<size_t>(idx) >= opt->values.size()) {
BOOST_LOG_TRIVIAL(warning) << caller << boost::format(", Line %1%: option %2% slot index %3% out of range, keeping first value")%__LINE__%key%idx;
new_values.emplace_back(opt->values.front());
}
else
new_values.emplace_back(opt->values[idx]);
}
opt->values = std::move(new_values);
}
void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filaments(DynamicPrintConfig& printer_config, int extruder_count, int extruder_nozzle_volume_count, std::set<std::string>& key_set, std::string id_name, std::string variant_name)
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", Line %1%: extruder_count %2%, extruder_nozzle_volume_count %3%")%__LINE__ %extruder_count %extruder_nozzle_volume_count;
@@ -10957,155 +11115,18 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: can not find opt define for %2%")%__LINE__%key;
continue;
}
// An empty option has no first value to fall back on; give it one registered default per filament.
if (auto *vec = dynamic_cast<ConfigOptionVectorBase*>(this->option(key)); vec && vec->empty() && optdef->default_value)
vec->resize(filament_count, optdef->default_value.get());
switch (optdef->type) {
case coStrings:
{
ConfigOptionStrings * opt = this->option<ConfigOptionStrings>(key);
if (!opt) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
break;
}
std::vector<std::string> new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
continue;
}
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
break;
}
case coInts:
{
ConfigOptionInts * opt = this->option<ConfigOptionInts>(key);
if (!opt) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
break;
}
std::vector<int> new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
continue;
}
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
break;
}
case coFloats:
{
ConfigOptionFloats * opt = this->option<ConfigOptionFloats>(key);
if (!opt) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
break;
}
std::vector<double> new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
continue;
}
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
break;
}
case coPercents:
{
ConfigOptionPercents * opt = this->option<ConfigOptionPercents>(key);
if (!opt) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
break;
}
std::vector<double> new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
continue;
}
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
break;
}
case coFloatsOrPercents:
{
ConfigOptionFloatsOrPercents * opt = this->option<ConfigOptionFloatsOrPercents>(key);
if (!opt) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
break;
}
std::vector<FloatOrPercent> new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
continue;
}
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
break;
}
case coBools:
{
ConfigOptionBools * opt = this->option<ConfigOptionBools>(key);
if (!opt) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
break;
}
std::vector<unsigned char> new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
continue;
}
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
break;
}
case coEnums:
{
ConfigOptionEnumsGeneric * opt = this->option<ConfigOptionEnumsGeneric>(key);
if (!opt) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found, skipping")%__LINE__%key;
break;
}
std::vector<int> new_values;
new_values.resize(filament_count);
for (int f_index = 0; f_index < filament_count; f_index++)
{
if (variant_index[f_index] < 0 || static_cast<size_t>(variant_index[f_index]) >= opt->size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% variant index %3% out of range, skipping")%__LINE__%key%variant_index[f_index];
continue;
}
new_values[f_index] = opt->get_at(variant_index[f_index]);
}
opt->values = new_values;
break;
}
case coStrings: gather_option_values<ConfigOptionStrings, std::string>(__FUNCTION__, key, this->option<ConfigOptionStrings>(key), variant_index); break;
case coInts: gather_option_values<ConfigOptionInts, int>(__FUNCTION__, key, this->option<ConfigOptionInts>(key), variant_index); break;
case coFloats: gather_option_values<ConfigOptionFloats, double>(__FUNCTION__, key, this->option<ConfigOptionFloats>(key), variant_index); break;
case coPercents: gather_option_values<ConfigOptionPercents, double>(__FUNCTION__, key, this->option<ConfigOptionPercents>(key), variant_index); break;
case coFloatsOrPercents: gather_option_values<ConfigOptionFloatsOrPercents, FloatOrPercent>(__FUNCTION__, key, this->option<ConfigOptionFloatsOrPercents>(key), variant_index); break;
case coBools: gather_option_values<ConfigOptionBools, unsigned char>(__FUNCTION__, key, this->option<ConfigOptionBools>(key), variant_index); break;
case coEnums: gather_option_values<ConfigOptionEnumsGeneric, int>(__FUNCTION__, key, this->option<ConfigOptionEnumsGeneric>(key), variant_index); break;
default:
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: unsupported option type for %2%")%__LINE__%key;
break;
@@ -11124,28 +11145,6 @@ void DynamicPrintConfig::update_values_to_printer_extruders_for_multiple_filamen
}
}
// Regathers a vector option's values through per-slot source indices (one input index per
// output slot). Out-of-range indices keep the first value, matching get_at's fallback.
template<typename OptType, typename ValueType>
static void gather_option_values(const std::string &key, OptType *opt, const std::vector<int> &slot_param_indices)
{
if (!opt || opt->values.empty()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% not found or empty, skipping")%__LINE__%key;
return;
}
std::vector<ValueType> new_values;
new_values.reserve(slot_param_indices.size());
for (int idx : slot_param_indices) {
if (idx < 0 || static_cast<size_t>(idx) >= opt->values.size()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: option %2% slot index %3% out of range, keeping first value")%__LINE__%key%idx;
new_values.emplace_back(opt->values.front());
}
else
new_values.emplace_back(opt->values[idx]);
}
opt->values = std::move(new_values);
}
void DynamicPrintConfig::update_filament_config_values_for_multiple_extruders(DynamicPrintConfig& printer_config,
const std::unordered_map<int, std::vector<FilamentVariantUse>>& filament_variant_uses,
int extruder_count, int extruder_nozzle_volume_count,
@@ -11240,13 +11239,13 @@ void DynamicPrintConfig::update_filament_config_values_for_multiple_extruders(Dy
continue;
}
switch (optdef->type) {
case coStrings: gather_option_values<ConfigOptionStrings, std::string>(key, this->option<ConfigOptionStrings>(key), slot_param_indices); break;
case coInts: gather_option_values<ConfigOptionInts, int>(key, this->option<ConfigOptionInts>(key), slot_param_indices); break;
case coFloats: gather_option_values<ConfigOptionFloats, double>(key, this->option<ConfigOptionFloats>(key), slot_param_indices); break;
case coPercents: gather_option_values<ConfigOptionPercents, double>(key, this->option<ConfigOptionPercents>(key), slot_param_indices); break;
case coFloatsOrPercents: gather_option_values<ConfigOptionFloatsOrPercents, FloatOrPercent>(key, this->option<ConfigOptionFloatsOrPercents>(key), slot_param_indices); break;
case coBools: gather_option_values<ConfigOptionBools, unsigned char>(key, this->option<ConfigOptionBools>(key), slot_param_indices); break;
case coEnums: gather_option_values<ConfigOptionEnumsGeneric, int>(key, this->option<ConfigOptionEnumsGeneric>(key), slot_param_indices); break;
case coStrings: gather_option_values<ConfigOptionStrings, std::string>(__FUNCTION__, key, this->option<ConfigOptionStrings>(key), slot_param_indices); break;
case coInts: gather_option_values<ConfigOptionInts, int>(__FUNCTION__, key, this->option<ConfigOptionInts>(key), slot_param_indices); break;
case coFloats: gather_option_values<ConfigOptionFloats, double>(__FUNCTION__, key, this->option<ConfigOptionFloats>(key), slot_param_indices); break;
case coPercents: gather_option_values<ConfigOptionPercents, double>(__FUNCTION__, key, this->option<ConfigOptionPercents>(key), slot_param_indices); break;
case coFloatsOrPercents: gather_option_values<ConfigOptionFloatsOrPercents, FloatOrPercent>(__FUNCTION__, key, this->option<ConfigOptionFloatsOrPercents>(key), slot_param_indices); break;
case coBools: gather_option_values<ConfigOptionBools, unsigned char>(__FUNCTION__, key, this->option<ConfigOptionBools>(key), slot_param_indices); break;
case coEnums: gather_option_values<ConfigOptionEnumsGeneric, int>(__FUNCTION__, key, this->option<ConfigOptionEnumsGeneric>(key), slot_param_indices); break;
default:
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", Line %1%: unsupported option type for %2%")%__LINE__%key;
break;
@@ -11948,13 +11947,11 @@ CLIActionsConfigDef::CLIActionsConfigDef()
def = this->add("load_defaultfila", coBool);
def->label = L("Load default filaments");
def->tooltip = L("Load first filament as default for those not loaded.");
def->cli_params = "option";
def->set_default_value(new ConfigOptionBool(false));
def = this->add("min_save", coBool);
def->label = L("Minimum save");
def->tooltip = L("Export 3MF with minimum size.");
def->cli_params = "option";
def->set_default_value(new ConfigOptionBool(false));
def = this->add("mtcpp", coInt);
@@ -11977,10 +11974,22 @@ CLIActionsConfigDef::CLIActionsConfigDef()
def->tooltip = L("Do not run any validity checks, such as G-code path conflicts check.");
def->set_default_value(new ConfigOptionBool(false));
// --strict turns the non-critical slicing warnings the CLI otherwise only logs into a
// failed run, and records strict_mode in result.json so consumers can tell the modes apart.
def = this->add("strict", coBool);
def->label = L("Strict mode");
def->tooltip = L("Exit non-zero when slicing raises a non-critical warning that is "
"otherwise only logged, such as a model that needs support while "
"support is disabled. Use this in CI or scripted pipelines that should "
"never ship a subtly broken slice. Each such warning is also listed "
"with a stable class in the `warnings` array of result.json, which is "
"written on Linux only. Cannot be combined with --no-check, which skips "
"the support check.");
def->set_default_value(new ConfigOptionBool(false));
def = this->add("normative_check", coBool);
def->label = L("Normative check");
def->tooltip = L("Check the normative items.");
def->cli_params = "option";
def->set_default_value(new ConfigOptionBool(true));
/*def = this->add("help_fff", coBool);
@@ -11998,9 +12007,29 @@ CLIActionsConfigDef::CLIActionsConfigDef()
def->tooltip = L("This outputs the model\u2019s information.");
def->set_default_value(new ConfigOptionBool(false));
def = this->add("inspect_mesh", coBool);
def->label = L("Inspect mesh (JSON to stdout)");
def->tooltip = L("Print a JSON summary of each loaded object to stdout, then exit: its bounding boxes and the "
"convex hull faces it can be laid on, with their normals, areas and centers. These are the faces "
"the --ground-* options choose from. Machine-readable alternative to --info.");
def->set_default_value(new ConfigOptionBool(false));
// --inspect-paint \u2014 dump the per-facet enforcer/blocker/extruder/fuzzy
// paint state stored on the loaded model (supports, seam, MMU color,
// fuzzy-skin) as JSON. Read-only; lets CI / scripted / AI tooling
// reason about existing paint on a .3mf without loading the GUI.
def = this->add("inspect_paint", coBool);
def->label = L("Inspect paint (JSON to stdout)");
def->tooltip = L("Print a structured JSON summary of every painted layer "
"(supports, seam, MMU color, fuzzy-skin) already stored on "
"the loaded model \u2014 per-state facet count, surface area, "
"and mesh-local bounding box \u2014 then exit. Machine-readable "
"alternative to opening the paint gizmos in the GUI.");
def->set_default_value(new ConfigOptionBool(false));
def = this->add("export_settings", coString);
def->label = L("Export Settings");
def->tooltip = L("This exports settings to a file.");
def->tooltip = L("This exports settings to a file. Use - to write them to stdout.");
def->cli_params = "settings.json";
def->set_default_value(new ConfigOptionString("output.json"));
@@ -12117,6 +12146,34 @@ CLITransformConfigDef::CLITransformConfigDef()
def->sidetext = u8"°"; // degrees, don't need translation
def->set_default_value(new ConfigOptionFloat(0));
// The --ground-* options choose from the faces the "Lay on Face" gizmo offers. Like the other
// transforms they run in command-line order, so they see the rotations given before them.
def = this->add("ground_largest_face", coBool);
def->label = L("Ground largest face");
def->tooltip = L("Lay each object on the largest face of its convex hull and drop it onto the bed. Of equally large "
"faces, the one already facing down is kept. Objects without a face large enough to rest on are left "
"as they are. Transforms run in command-line order, so rotations given before this option are respected. "
"--orient 1 runs after all transforms and replaces the orientation.");
def->set_default_value(new ConfigOptionBool(false));
def = this->add("ground_face_normal", coString);
def->label = L("Ground face by normal");
def->tooltip = L("Lay each object on the convex hull face whose outward normal is closest to the direction NX,NY,NZ "
"and drop it onto the bed. The direction is in object coordinates, which include the rotations given "
"before this option and match the plate axes unless the input file rotates the object. For example, "
"1,0,0 stands the object on its +X side. --orient 1 runs after all transforms and replaces the orientation.");
def->cli_params = "NX,NY,NZ";
def->set_default_value(new ConfigOptionString(""));
def = this->add("ground_face_point", coString);
def->label = L("Ground face at point");
def->tooltip = L("Lay each object on the convex hull face that contains the point X,Y,Z and drop it onto the bed. "
"The point is in object coordinates, which include the rotations given before this option; "
"--inspect-mesh reports face centers in them. Objects without such a face are left as they are, and "
"the run fails if no object has one. --orient 1 runs after all transforms and replaces the orientation.");
def->cli_params = "X,Y,Z";
def->set_default_value(new ConfigOptionString(""));
def = this->add("scale", coFloat);
def->label = L("Scale");
def->tooltip = L("Scale the model by a float factor.");
@@ -12247,7 +12304,7 @@ CLIMiscConfigDef::CLIMiscConfigDef()
def->cli_params = "level";
def->set_default_value(new ConfigOptionInt(1));
def = this->add("logfile", coInt);
def = this->add("logfile", coString);
def->label = L("Log file");
def->tooltip = L("Redirects debug logging to file.\n");
def->cli_params = "file";
@@ -12295,7 +12352,6 @@ CLIMiscConfigDef::CLIMiscConfigDef()
def = this->add("skip_modified_gcodes", coBool);
def->label = L("Skip modified G-code in 3MF");
def->tooltip = L("Skip the modified G-code in 3MF from printer or filament presets.");
def->cli_params = "option";
def->set_default_value(new ConfigOptionBool(false));
def = this->add("makerlab_name", coString);
@@ -12325,14 +12381,12 @@ CLIMiscConfigDef::CLIMiscConfigDef()
def = this->add("allow_newer_file", coBool);
def->label = L("Allow 3MF with newer version to be sliced");
def->tooltip = L("Allow 3MF with newer version to be sliced.");
def->cli_params = "option";
def->set_default_value(new ConfigOptionBool(false));
def = this->add("allow_mix_temp", coBool);
// internal use only, don't need translation
def->label = "Allow filaments with high/low temperature to be printed together";
def->tooltip = "Allow filaments with high/low temperature to be printed together.";
def->cli_params = "option";
def->set_default_value(new ConfigOptionBool(false));
}
+51 -43
View File
@@ -113,7 +113,7 @@ enum InfillPattern : int {
ipCubic, ipAdaptiveCubic, ipQuarterCubic, ipSupportCubic, ipLightning,
ipHoneycomb, ip3DHoneycomb, ipLateralHoneycomb, ipLateralLattice,
ipCrossHatch, ipTpmsD, ipTpmsFK, ipGyroid,
ipConcentric, ipHilbertCurve, ipArchimedeanChords, ipOctagramSpiral,
ipConcentric, ipSpiralInset, ipHilbertCurve, ipArchimedeanChords, ipOctagramSpiral,
ipSupportBase, ipConcentricInternal,
ipCount,
};
@@ -271,7 +271,7 @@ enum LongRectrationLevel
};
enum SupportMaterialInterfacePattern {
smipAuto, smipRectilinear, smipConcentric, smipRectilinearInterlaced, smipGrid
smipAuto, smipRectilinear, smipConcentric, smipSpiralInset, smipRectilinearInterlaced, smipGrid
};
// BBS
@@ -1011,41 +1011,46 @@ public: \
{ PrintConfigDef::handle_legacy(opt_key, value); }
#define PRINT_CONFIG_CLASS_ELEMENT_DEFINITION(r, data, elem) BOOST_PP_TUPLE_ELEM(0, elem) BOOST_PP_TUPLE_ELEM(1, elem);
#define PRINT_CONFIG_CLASS_ELEMENT_INITIALIZATION2(KEY) cache.opt_add(BOOST_PP_STRINGIZE(KEY), base_ptr, this->KEY);
#define PRINT_CONFIG_CLASS_ELEMENT_INITIALIZATION(r, data, elem) PRINT_CONFIG_CLASS_ELEMENT_INITIALIZATION2(BOOST_PP_TUPLE_ELEM(1, elem))
#define PRINT_CONFIG_CLASS_ELEMENT_HASH(r, data, elem) boost::hash_combine(seed, BOOST_PP_TUPLE_ELEM(1, elem).hash());
#define PRINT_CONFIG_CLASS_ELEMENT_EQUAL(r, data, elem) if (! (BOOST_PP_TUPLE_ELEM(1, elem) == rhs.BOOST_PP_TUPLE_ELEM(1, elem))) return false;
#define PRINT_CONFIG_CLASS_ELEMENT_LOWER(r, data, elem) \
if (BOOST_PP_TUPLE_ELEM(1, elem) < rhs.BOOST_PP_TUPLE_ELEM(1, elem)) return true; \
if (! (BOOST_PP_TUPLE_ELEM(1, elem) == rhs.BOOST_PP_TUPLE_ELEM(1, elem))) return false;
#define PRINT_CONFIG_CLASS_ELEMENT_VISIT(r, data, elem) if (! f(BOOST_PP_STRINGIZE(BOOST_PP_TUPLE_ELEM(1, elem)), this->BOOST_PP_TUPLE_ELEM(1, elem), rhs.BOOST_PP_TUPLE_ELEM(1, elem))) return;
// Each option list is expanded into the members and again into for_each_option_pair(), which calls
// f(key, this->option, rhs.option) in declaration order and stops when f returns false. hash(),
// operator==, operator< and initialize() iterate the options through that visitor.
#define PRINT_CONFIG_CLASS_COMMON_BODY(CLASS_NAME) \
size_t hash() const throw() \
{ \
size_t seed = 0; \
this->for_each_option_pair(*this, [&seed](const char*, const auto &a, const auto&) { boost::hash_combine(seed, a.hash()); return true; }); \
return seed; \
} \
bool operator==(const CLASS_NAME &rhs) const throw() \
{ \
bool eq = true; \
this->for_each_option_pair(rhs, [&eq](const char*, const auto &a, const auto &b) { eq = (a == b); return eq; }); \
return eq; \
} \
bool operator!=(const CLASS_NAME &rhs) const throw() { return ! (*this == rhs); } \
bool operator<(const CLASS_NAME &rhs) const throw() \
{ \
int c = 0; \
this->for_each_option_pair(rhs, [&c](const char*, const auto &a, const auto &b) { if (a < b) c = -1; else if (! (a == b)) c = 1; return c == 0; }); \
return c < 0; \
} \
protected: \
void initialize(StaticCacheBase &cache, const char *base_ptr) \
{ \
this->for_each_option_pair(*this, [&cache, base_ptr](const char *key, const auto &a, const auto&) { cache.opt_add(key, base_ptr, a); return true; }); \
}
#define PRINT_CONFIG_CLASS_DEFINE(CLASS_NAME, PARAMETER_DEFINITION_SEQ) \
class CLASS_NAME : public StaticPrintConfig { \
STATIC_PRINT_CONFIG_CACHE(CLASS_NAME) \
public: \
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_DEFINITION, _, PARAMETER_DEFINITION_SEQ) \
size_t hash() const throw() \
template<typename F> void for_each_option_pair(const CLASS_NAME &rhs, F &&f) const \
{ \
size_t seed = 0; \
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_HASH, _, PARAMETER_DEFINITION_SEQ) \
return seed; \
} \
bool operator==(const CLASS_NAME &rhs) const throw() \
{ \
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_EQUAL, _, PARAMETER_DEFINITION_SEQ) \
return true; \
} \
bool operator!=(const CLASS_NAME &rhs) const throw() { return ! (*this == rhs); } \
bool operator<(const CLASS_NAME &rhs) const throw() \
{ \
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_LOWER, _, PARAMETER_DEFINITION_SEQ) \
return false; \
} \
protected: \
void initialize(StaticCacheBase &cache, const char *base_ptr) \
{ \
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_INITIALIZATION, _, PARAMETER_DEFINITION_SEQ) \
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_VISIT, _, PARAMETER_DEFINITION_SEQ) \
} \
PRINT_CONFIG_CLASS_COMMON_BODY(CLASS_NAME) \
};
#define PRINT_CONFIG_CLASS_DERIVED_CLASS_LIST_ITEM(r, data, i, elem) BOOST_PP_COMMA_IF(i) public elem
@@ -1059,43 +1064,43 @@ protected: \
if (! (*static_cast<const elem*>(this) == static_cast<const elem&>(rhs))) return false;
// Generic version, with or without new parameters. Don't use this directly.
#define PRINT_CONFIG_CLASS_DERIVED_DEFINE1(CLASS_NAME, CLASSES_PARENTS_TUPLE, PARAMETER_DEFINITION, PARAMETER_REGISTRATION, PARAMETER_HASHES, PARAMETER_EQUALS) \
#define PRINT_CONFIG_CLASS_DERIVED_DEFINE1(CLASS_NAME, CLASSES_PARENTS_TUPLE, PARAMETER_DEFINITION, PARAMETER_VISIT) \
class CLASS_NAME : PRINT_CONFIG_CLASS_DERIVED_CLASS_LIST(CLASSES_PARENTS_TUPLE) { \
STATIC_PRINT_CONFIG_CACHE_DERIVED(CLASS_NAME) \
CLASS_NAME() : PRINT_CONFIG_CLASS_DERIVED_INITIALIZER(CLASSES_PARENTS_TUPLE, 0) { assert(s_cache_##CLASS_NAME.initialized()); *this = s_cache_##CLASS_NAME.defaults(); } \
public: \
PARAMETER_DEFINITION \
template<typename F> void for_each_option_pair(const CLASS_NAME &rhs, F &&f) const { PARAMETER_VISIT } \
size_t hash() const throw() \
{ \
size_t seed = 0; \
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_DERIVED_HASH, _, BOOST_PP_TUPLE_TO_SEQ(CLASSES_PARENTS_TUPLE)) \
PARAMETER_HASHES \
this->for_each_option_pair(*this, [&seed](const char*, const auto &a, const auto&) { boost::hash_combine(seed, a.hash()); return true; }); \
return seed; \
} \
bool operator==(const CLASS_NAME &rhs) const throw() \
{ \
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_DERIVED_EQUAL, _, BOOST_PP_TUPLE_TO_SEQ(CLASSES_PARENTS_TUPLE)) \
PARAMETER_EQUALS \
return true; \
bool eq = true; \
this->for_each_option_pair(rhs, [&eq](const char*, const auto &a, const auto &b) { eq = (a == b); return eq; }); \
return eq; \
} \
bool operator!=(const CLASS_NAME &rhs) const throw() { return ! (*this == rhs); } \
protected: \
CLASS_NAME(int) : PRINT_CONFIG_CLASS_DERIVED_INITIALIZER(CLASSES_PARENTS_TUPLE, 1) {} \
void initialize(StaticCacheBase &cache, const char* base_ptr) { \
PRINT_CONFIG_CLASS_DERIVED_INITCACHE(CLASSES_PARENTS_TUPLE) \
PARAMETER_REGISTRATION \
this->for_each_option_pair(*this, [&cache, base_ptr](const char *key, const auto &a, const auto&) { cache.opt_add(key, base_ptr, a); return true; }); \
} \
};
// Variant without adding new parameters.
#define PRINT_CONFIG_CLASS_DERIVED_DEFINE0(CLASS_NAME, CLASSES_PARENTS_TUPLE) \
PRINT_CONFIG_CLASS_DERIVED_DEFINE1(CLASS_NAME, CLASSES_PARENTS_TUPLE, BOOST_PP_EMPTY(), BOOST_PP_EMPTY(), BOOST_PP_EMPTY(), BOOST_PP_EMPTY())
PRINT_CONFIG_CLASS_DERIVED_DEFINE1(CLASS_NAME, CLASSES_PARENTS_TUPLE, BOOST_PP_EMPTY(), BOOST_PP_EMPTY())
// Variant with adding new parameters.
#define PRINT_CONFIG_CLASS_DERIVED_DEFINE(CLASS_NAME, CLASSES_PARENTS_TUPLE, PARAMETER_DEFINITION_SEQ) \
PRINT_CONFIG_CLASS_DERIVED_DEFINE1(CLASS_NAME, CLASSES_PARENTS_TUPLE, \
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_DEFINITION, _, PARAMETER_DEFINITION_SEQ), \
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_INITIALIZATION, _, PARAMETER_DEFINITION_SEQ), \
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_HASH, _, PARAMETER_DEFINITION_SEQ), \
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_EQUAL, _, PARAMETER_DEFINITION_SEQ))
BOOST_PP_SEQ_FOR_EACH(PRINT_CONFIG_CLASS_ELEMENT_VISIT, _, PARAMETER_DEFINITION_SEQ))
// This object is mapped to Perl as Slic3r::Config::PrintObject.
PRINT_CONFIG_CLASS_DEFINE(
@@ -1348,6 +1353,7 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionFloatsNullable, filament_ironing_speed))
// Detect bridging perimeters
((ConfigOptionBool, detect_overhang_wall))
((ConfigOptionBool, unsupported_wall_last))
((ConfigOptionInt, outer_wall_filament_id))
((ConfigOptionInt, inner_wall_filament_id))
((ConfigOptionFloatOrPercent, inner_wall_line_width))
@@ -1386,6 +1392,8 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionBool, role_based_wipe_speed))
((ConfigOptionFloatOrPercent, wipe_speed))
((ConfigOptionBool, wipe_on_loops))
((ConfigOptionBool, wipe_inward))
((ConfigOptionFloatOrPercent, wipe_inward_distance))
((ConfigOptionBool, wipe_before_external_loop))
((ConfigOptionEnum<WallInfillOrder>, wall_infill_order))
((ConfigOptionBool, precise_outer_wall))
@@ -1620,6 +1628,8 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionBool, manual_filament_change))
((ConfigOptionBool, single_extruder_multi_material_priming))
((ConfigOptionEnum<ToolChangeOrderingType>, toolchange_ordering))
((ConfigOptionString, toolchange_cyclic_order))
((ConfigOptionBool, toolchange_cyclic_first_layer))
((ConfigOptionBool, wipe_tower_no_sparse_layers))
((ConfigOptionString, change_filament_gcode))
((ConfigOptionString, change_extrusion_role_gcode))
@@ -1781,6 +1791,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionBools, slow_down_for_layer_cooling))
((ConfigOptionInts, close_fan_the_first_x_layers))
((ConfigOptionEnum<DraftShield>, draft_shield))
((ConfigOptionFloat, extruder_clearance_dist_to_rod))//BBS
((ConfigOptionFloat, extruder_clearance_height_to_rod))//BBs
((ConfigOptionFloat, extruder_clearance_height_to_lid))//BBS
((ConfigOptionFloat, extruder_clearance_radius))
@@ -2148,11 +2159,8 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE0(
#undef STATIC_PRINT_CONFIG_CACHE_BASE
#undef STATIC_PRINT_CONFIG_CACHE_DERIVED
#undef PRINT_CONFIG_CLASS_ELEMENT_DEFINITION
#undef PRINT_CONFIG_CLASS_ELEMENT_EQUAL
#undef PRINT_CONFIG_CLASS_ELEMENT_LOWER
#undef PRINT_CONFIG_CLASS_ELEMENT_HASH
#undef PRINT_CONFIG_CLASS_ELEMENT_INITIALIZATION
#undef PRINT_CONFIG_CLASS_ELEMENT_INITIALIZATION2
#undef PRINT_CONFIG_CLASS_ELEMENT_VISIT
#undef PRINT_CONFIG_CLASS_COMMON_BODY
#undef PRINT_CONFIG_CLASS_DEFINE
#undef PRINT_CONFIG_CLASS_DERIVED_CLASS_LIST
#undef PRINT_CONFIG_CLASS_DERIVED_CLASS_LIST_ITEM

Some files were not shown because too many files have changed in this diff Show More