mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-13 02:41:16 +00:00
Merge branch 'main' into perf/slicing-core-memory
This commit is contained in:
+7
-2
@@ -91,6 +91,12 @@ if (SLIC3R_GUI)
|
||||
# list(REMOVE_ITEM wxWidgets_LIBRARIES oleacc)
|
||||
|
||||
find_package(wxInspector REQUIRED)
|
||||
# wxInspector 1.0.0 installs its headers but accidentally declares the
|
||||
# INSTALL_INTERFACE include directory PRIVATE, so its imported target does
|
||||
# not expose them to consumers. Restore the package prefix include path until
|
||||
# the upstream export is fixed.
|
||||
get_filename_component(WXINSPECTOR_PREFIX "${wxInspector_DIR}/../../.." ABSOLUTE)
|
||||
target_include_directories(wxInspector::wxInspector INTERFACE "${WXINSPECTOR_PREFIX}/include")
|
||||
|
||||
# wxInspector's exported interface names the release wxWidgets import
|
||||
# libraries, which a Debug build cannot link. wx is linked above instead.
|
||||
@@ -183,10 +189,9 @@ else ()
|
||||
target_link_libraries(OrcaSlicer ${CMAKE_DL_LIBS} -lstdc++ Threads::Threads pangoft2-1.0)
|
||||
endif ()
|
||||
|
||||
# Add the Slic3r GUI library, libcurl, OpenGL and GLU libraries.
|
||||
if (SLIC3R_GUI)
|
||||
# target_link_libraries(OrcaSlicer ws2_32 uxtheme setupapi libslic3r_gui ${wxWidgets_LIBRARIES})
|
||||
target_link_libraries(OrcaSlicer libslic3r_gui)
|
||||
target_link_libraries(OrcaSlicer libslic3r_gui wxInspector::wxInspector)
|
||||
if (MSVC)
|
||||
# Generate debug symbols even in release mode.
|
||||
target_link_options(OrcaSlicer PUBLIC "$<$<CONFIG:RELEASE>:/DEBUG>")
|
||||
|
||||
+190
-190
@@ -20,6 +20,49 @@
|
||||
#endif /* SLIC3R_GUI */
|
||||
#endif /* WIN32 */
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include "libslic3r/PrintBase.hpp"
|
||||
#include <boost/date_time/posix_time/posix_time_duration.hpp>
|
||||
#include <cerrno>
|
||||
#include <utility>
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
#include <boost/filesystem/fstream.hpp>
|
||||
#include "libslic3r/LocalesUtils.hpp"
|
||||
#include <fstream>
|
||||
#include <exception>
|
||||
#include <set>
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "libslic3r/TriangleSelector.hpp"
|
||||
#include <algorithm>
|
||||
#include "libslic3r/Format/bbs_3mf.hpp"
|
||||
#include "libslic3r/Format/AssembleList.hpp"
|
||||
#include <cstdlib>
|
||||
#include <stdlib.h>
|
||||
#include <stdexcept>
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r_version.h"
|
||||
#include "libslic3r/Semver.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/CustomGCode.hpp"
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <iterator>
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/GCode/WipeTowerEstimate.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoBase.hpp"
|
||||
#include <functional>
|
||||
#include "libslic3r/Arrange.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/GCode/ThumbnailData.hpp"
|
||||
#include "slic3r/GUI/3DScene.hpp"
|
||||
#include "slic3r/GUI/GLShader.hpp"
|
||||
#include <cassert>
|
||||
#include <boost/filesystem/exception.hpp>
|
||||
#include <signal.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
@@ -34,12 +77,10 @@
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <boost/thread.hpp>
|
||||
//add json logic
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
using namespace nlohmann;
|
||||
#endif
|
||||
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/nowide/args.hpp>
|
||||
@@ -97,6 +138,23 @@ using namespace nlohmann;
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/GuiColor.hpp"
|
||||
#include <GLFW/glfw3.h>
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
#include "libslic3r/BuildVolume.hpp"
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "libslic3r/Exception.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/GCode/ToolOrdering.hpp"
|
||||
#include "libslic3r/ObjectID.hpp"
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/ProjectTask.hpp"
|
||||
#include "slic3r/GUI/GUI_ObjectList.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/Jobs/SendJob.hpp"
|
||||
#include <boost/nowide/convert.hpp>
|
||||
#include <stdio.h>
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
using json = nlohmann::json;
|
||||
|
||||
#ifdef __WXGTK__
|
||||
#if __has_include(<X11/Xlib.h>)
|
||||
@@ -642,162 +700,6 @@ static void load_default_gcodes_to_config(DynamicPrintConfig& config, Preset::Ty
|
||||
}
|
||||
}
|
||||
|
||||
static int load_assemble_plate_list(std::string config_file, std::vector<assemble_plate_info_t> &assemble_plate_info_list)
|
||||
{
|
||||
int ret = 0;
|
||||
boost::filesystem::path directory_path(config_file);
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("%1% enter, file %2%")%__FUNCTION__ % config_file;
|
||||
if (!fs::exists(directory_path)) {
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("directory %1% not exist.")%config_file;
|
||||
return CLI_FILE_NOTFOUND;
|
||||
}
|
||||
|
||||
try {
|
||||
json root_json;
|
||||
boost::nowide::ifstream ifs(config_file);
|
||||
ifs >> root_json;
|
||||
ifs.close();
|
||||
|
||||
int plate_count = root_json[JSON_ASSEMPLE_PLATES].size();
|
||||
if ((plate_count <= 0) || (plate_count > MAX_PLATE_COUNT)) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< boost::format(": invalid plate count %1%")%plate_count;
|
||||
return CLI_CONFIG_FILE_ERROR;
|
||||
}
|
||||
assemble_plate_info_list.resize(plate_count);
|
||||
|
||||
for (int plate_index = 0; plate_index < plate_count; plate_index++)
|
||||
{
|
||||
assemble_plate_info_t &assemble_plate = assemble_plate_info_list[plate_index];
|
||||
const json& plate_json = root_json[JSON_ASSEMPLE_PLATES][plate_index];
|
||||
assemble_plate.plate_name = plate_json[JSON_ASSEMPLE_PLATE_NAME];
|
||||
assemble_plate.need_arrange = plate_json[JSON_ASSEMPLE_PLATE_NEED_ARRANGE];
|
||||
|
||||
if (plate_json.contains(JSON_ASSEMPLE_PLATE_PARAMS)) {
|
||||
assemble_plate.plate_params = plate_json[JSON_ASSEMPLE_PLATE_PARAMS].get<std::map<std::string, std::string>>();
|
||||
BOOST_LOG_TRIVIAL(debug) << boost::format("Plate %1%, has %2% plate params") % (plate_index + 1) % assemble_plate.plate_params.size();
|
||||
}
|
||||
|
||||
int object_count = plate_json[JSON_ASSEMPLE_OBJECTS].size();
|
||||
if (object_count <= 0) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< boost::format(": invalid object count %1% in plate %2%")%object_count %(plate_index+1);
|
||||
return CLI_CONFIG_FILE_ERROR;
|
||||
}
|
||||
|
||||
assemble_plate.assemble_obj_list.resize(object_count);
|
||||
for (int object_index = 0; object_index < object_count; object_index++)
|
||||
{
|
||||
assemble_object_info_t& assemble_object = assemble_plate.assemble_obj_list[object_index];
|
||||
const json& object_json = plate_json[JSON_ASSEMPLE_OBJECTS][object_index];
|
||||
|
||||
assemble_object.path = object_json[JSON_ASSEMPLE_OBJECT_PATH];
|
||||
assemble_object.count = object_json[JSON_ASSEMPLE_OBJECT_COUNT];
|
||||
|
||||
if (assemble_object.count <= 0) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": invalid object clone count %1% in plate %2% Object %3%") % assemble_object.count % (plate_index + 1) % assemble_object.path;
|
||||
return CLI_CONFIG_FILE_ERROR;
|
||||
}
|
||||
|
||||
assemble_object.filaments = object_json.at(JSON_ASSEMPLE_OBJECT_FILAMENTS).get<std::vector<int>>();
|
||||
if ((assemble_object.filaments.size() > 0) && (assemble_object.filaments.size() != assemble_object.count) && (assemble_object.filaments.size() != 1))
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s filaments count %2% not equal to clone count %3%, also not equal to 1") % assemble_object.path % assemble_object.filaments.size() % assemble_object.count;
|
||||
return CLI_CONFIG_FILE_ERROR;
|
||||
}
|
||||
|
||||
if (object_json.contains(JSON_ASSEMPLE_OBJECT_ASSEMBLE_INDEX)) {
|
||||
assemble_object.assemble_index = object_json[JSON_ASSEMPLE_OBJECT_ASSEMBLE_INDEX].get<std::vector<int>>();
|
||||
if ((assemble_object.assemble_index.size() > 0) && (assemble_object.assemble_index.size() != assemble_object.count) && (assemble_object.assemble_index.size() != 1))
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s assemble_index count %2% not equal to clone count %3%, also not equal to 1") % assemble_object.path % assemble_object.assemble_index.size() % assemble_object.count;
|
||||
return CLI_CONFIG_FILE_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
if (object_json.contains(JSON_ASSEMPLE_OBJECT_POS_X)) {
|
||||
assemble_object.pos_x = object_json[JSON_ASSEMPLE_OBJECT_POS_X].get<std::vector<float>>();
|
||||
if ((assemble_object.pos_x.size() > 0) && (assemble_object.pos_x.size() != assemble_object.count) && (assemble_object.pos_x.size() != 1))
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s pos_x count %2% not equal to clone count %3%, also not equal to 1") % assemble_object.path % assemble_object.pos_x.size() % assemble_object.count;
|
||||
return CLI_CONFIG_FILE_ERROR;
|
||||
}
|
||||
}
|
||||
if (object_json.contains(JSON_ASSEMPLE_OBJECT_POS_Y)) {
|
||||
assemble_object.pos_y = object_json[JSON_ASSEMPLE_OBJECT_POS_Y].get<std::vector<float>>();
|
||||
if ((assemble_object.pos_y.size() > 0) && (assemble_object.pos_y.size() != assemble_object.count) && (assemble_object.pos_y.size() != 1))
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s pos_y count %2% not equal to clone count %3%, also not equal to 1") % assemble_object.path % assemble_object.pos_y.size() % assemble_object.count;
|
||||
return CLI_CONFIG_FILE_ERROR;
|
||||
}
|
||||
}
|
||||
if (object_json.contains(JSON_ASSEMPLE_OBJECT_POS_Z)) {
|
||||
assemble_object.pos_z = object_json[JSON_ASSEMPLE_OBJECT_POS_Z].get<std::vector<float>>();
|
||||
if ((assemble_object.pos_z.size() > 0) && (assemble_object.pos_z.size() != assemble_object.count) && (assemble_object.pos_z.size() != 1))
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": object %1%'s pos_z count %2% not equal to clone count %3%, also not equal to 1") % assemble_object.path % assemble_object.pos_z.size() % assemble_object.count;
|
||||
return CLI_CONFIG_FILE_ERROR;
|
||||
}
|
||||
}
|
||||
if (object_json.contains(JSON_ASSEMPLE_OBJECT_PRINT_PARAMS)) {
|
||||
assemble_object.print_params = object_json[JSON_ASSEMPLE_OBJECT_PRINT_PARAMS].get<std::map<std::string, std::string>>();
|
||||
BOOST_LOG_TRIVIAL(debug) << boost::format("Plate %1%, object %2% has %3% print params") % (plate_index + 1) %assemble_object.path % assemble_object.print_params.size();
|
||||
}
|
||||
if (object_json.contains(JSON_ASSEMPLE_OBJECT_HEIGHT_RANGES)) {
|
||||
json height_range_json = object_json[JSON_ASSEMPLE_OBJECT_HEIGHT_RANGES];
|
||||
int range_count = height_range_json.size();
|
||||
|
||||
BOOST_LOG_TRIVIAL(debug) << boost::format("Plate %1%, object %2% has %3% height ranges") % (plate_index + 1) %assemble_object.path % range_count;
|
||||
|
||||
assemble_object.height_ranges.resize(range_count);
|
||||
for (int range_index = 0; range_index < range_count; range_index++)
|
||||
{
|
||||
height_range_info_t& height_range = assemble_object.height_ranges[range_index];
|
||||
height_range.min_z = height_range_json[range_index][JSON_ASSEMPLE_OBJECT_MIN_Z];
|
||||
height_range.max_z = height_range_json[range_index][JSON_ASSEMPLE_OBJECT_MAX_Z];
|
||||
height_range.range_params = height_range_json[range_index][JSON_ASSEMPLE_OBJECT_RANGE_PARAMS].get<std::map<std::string, std::string>>();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (plate_json.contains(JSON_ASSEMPLE_ASSEMBLE_PARAMS)) {
|
||||
json assemble_params_json = plate_json[JSON_ASSEMPLE_ASSEMBLE_PARAMS];
|
||||
int assemble_count = assemble_params_json.size();
|
||||
for (int i = 0; i < assemble_count; i++)
|
||||
{
|
||||
assembled_param_info_t assembled_param;
|
||||
int assemble_index = assemble_params_json[i][JSON_ASSEMPLE_OBJECT_ASSEMBLE_INDEX];
|
||||
if (assemble_params_json[i].contains(JSON_ASSEMPLE_OBJECT_PRINT_PARAMS)) {
|
||||
assembled_param.print_params = assemble_params_json[i][JSON_ASSEMPLE_OBJECT_PRINT_PARAMS].get<std::map<std::string, std::string>>();
|
||||
BOOST_LOG_TRIVIAL(debug) << boost::format("Plate %1%, assemble object %2% has %3% print params") % (plate_index + 1) %i % assembled_param.print_params.size();
|
||||
}
|
||||
if (assemble_params_json[i].contains(JSON_ASSEMPLE_OBJECT_HEIGHT_RANGES)) {
|
||||
json height_range_json = assemble_params_json[i][JSON_ASSEMPLE_OBJECT_HEIGHT_RANGES];
|
||||
int range_count = height_range_json.size();
|
||||
|
||||
BOOST_LOG_TRIVIAL(debug) << boost::format("Plate %1%, assemble object %2% has %3% height ranges") % (plate_index + 1) %i % range_count;
|
||||
|
||||
assembled_param.height_ranges.resize(range_count);
|
||||
for (int range_index = 0; range_index < range_count; range_index++)
|
||||
{
|
||||
height_range_info_t& height_range = assembled_param.height_ranges[range_index];
|
||||
height_range.min_z = height_range_json[range_index][JSON_ASSEMPLE_OBJECT_MIN_Z];
|
||||
height_range.max_z = height_range_json[range_index][JSON_ASSEMPLE_OBJECT_MAX_Z];
|
||||
height_range.range_params = height_range_json[range_index][JSON_ASSEMPLE_OBJECT_RANGE_PARAMS].get<std::map<std::string, std::string>>();
|
||||
}
|
||||
}
|
||||
assemble_plate.assembled_param_list.emplace(assemble_index, std::move(assembled_param));
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(debug) << boost::format("Plate %1%, has %2% plate params") % (plate_index + 1) % assemble_plate.plate_params.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(std::exception &err) {
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__<< ": parse file "<<config_file<<" got a generic exception, reason = " << err.what();
|
||||
ret = CLI_CONFIG_FILE_ERROR;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void merge_or_add_object(assemble_plate_info_t& assemble_plate_info, Model &model, int assemble_index, std::map<int, ModelObject*> &merged_objects, ModelObject *ori_object)
|
||||
{
|
||||
if (assemble_index > 0) {
|
||||
@@ -1863,6 +1765,14 @@ int CLI::run(int argc, char **argv)
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("current_printer_name %1%, current_process_name %2%")%current_printer_name %current_process_name;
|
||||
ConfigOptionStrings* option_strings = config.option<ConfigOptionStrings>("inherits_group");
|
||||
// One entry for the process, one per filament and one for the printer. A group of another
|
||||
// length still has the process first and the printer last; one too short for that is ignored.
|
||||
if (option_strings && option_strings->values.size() != current_filaments_name.size() + 2) {
|
||||
boost::nowide::cerr << "Warning: inherits_group has " << option_strings->values.size() << " entries, expected "
|
||||
<< current_filaments_name.size() + 2 << " for " << current_filaments_name.size() << " filaments" << std::endl;
|
||||
if (option_strings->values.size() < 2)
|
||||
option_strings = nullptr;
|
||||
}
|
||||
if (option_strings) {
|
||||
current_inherits_group = option_strings->values;
|
||||
size_t size = current_inherits_group.size();
|
||||
@@ -1884,14 +1794,11 @@ int CLI::run(int argc, char **argv)
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("inherits of process valid, current_process_system_name is %1%") %current_process_system_name;
|
||||
}
|
||||
|
||||
current_filaments_system_name.resize(size - 2);
|
||||
for (int index = 1; index < (size - 1); index++) {
|
||||
if (current_inherits_group[index].empty()) {
|
||||
current_filaments_system_name[index-1] = current_filaments_name[index-1];
|
||||
}
|
||||
else {
|
||||
// A filament without an entry of its own counts as a system preset.
|
||||
current_filaments_system_name = current_filaments_name;
|
||||
for (size_t index = 1; index < size - 1 && index <= current_filaments_name.size(); index++) {
|
||||
if (!current_inherits_group[index].empty())
|
||||
current_filaments_system_name[index-1] = current_inherits_group[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -1913,6 +1820,8 @@ int CLI::run(int argc, char **argv)
|
||||
old_printable_width = static_cast<int>(old_printable_bbox.size().x());
|
||||
old_printable_depth = static_cast<int>(old_printable_bbox.size().y());
|
||||
}
|
||||
// A 3mf can carry an empty project_settings.config - the models in
|
||||
// resources/handy_models do - and opt_float() dereferences without checking.
|
||||
if (config.option<ConfigOptionFloat>("printable_height"))
|
||||
old_printable_height = (int)(config.opt_float("printable_height"));
|
||||
|
||||
@@ -2024,7 +1933,12 @@ int CLI::run(int argc, char **argv)
|
||||
//parse the json and assemble object here
|
||||
Model model;
|
||||
|
||||
int ret = load_assemble_plate_list(load_assemble_list, assemble_plate_info_list);
|
||||
AssembleListResult list_result = load_assemble_plate_list(load_assemble_list, assemble_plate_info_list, MAX_PLATE_COUNT);
|
||||
int ret = CLI_SUCCESS;
|
||||
if (list_result == AssembleListResult::FileNotFound)
|
||||
ret = CLI_FILE_NOTFOUND;
|
||||
else if (list_result == AssembleListResult::ConfigError)
|
||||
ret = CLI_CONFIG_FILE_ERROR;
|
||||
if (ret) {
|
||||
record_exit_reson(outfile_dir, ret, 0, cli_errors[ret], sliced_info);
|
||||
flush_and_exit(ret);
|
||||
@@ -2593,7 +2507,8 @@ int CLI::run(int argc, char **argv)
|
||||
orig_printable_width = static_cast<int>(orig_printable_bbox.size().x());
|
||||
orig_printable_depth = static_cast<int>(orig_printable_bbox.size().y());
|
||||
}
|
||||
orig_printable_height = (int)(config.opt_float("printable_height"));
|
||||
if (config.option<ConfigOptionFloat>("printable_height"))
|
||||
orig_printable_height = (int)(config.opt_float("printable_height"));
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__<< boost::format(":%1%, check printable size: old_printable_width=%2%, orig_printable_width=%3%, old_printable_depth=%4%, orig_printable_depth=%5%, old_printable_height=%6%, orig_printable_height=%7%")
|
||||
%__LINE__ %old_printable_width %orig_printable_width %old_printable_depth %orig_printable_depth %old_printable_height %orig_printable_height;
|
||||
if ((orig_printable_width > 0) && (orig_printable_depth > 0) && (orig_printable_height > 0))
|
||||
@@ -2780,6 +2695,37 @@ int CLI::run(int argc, char **argv)
|
||||
fetch_compatible_values = true;
|
||||
}
|
||||
|
||||
// Refresh every project filament no loaded filament replaces from its current system preset, as the GUI
|
||||
// does when it loads the project; the filament merge below keeps the keys the project lists as changed.
|
||||
// Entries stay in slot order, which the merge's variant bookkeeping relies on.
|
||||
std::vector<bool> load_filaments_refresh(load_filaments_config.size(), false);
|
||||
if (is_bbl_3mf && new_printer_name.empty()) {
|
||||
const ConfigOptionStrings *project_filament_ids = m_print_config.option<ConfigOptionStrings>("filament_ids");
|
||||
for (size_t index = 0; index < current_filaments_system_name.size(); index++) {
|
||||
const int slot = static_cast<int>(index) + 1;
|
||||
if (std::find(load_filaments_index.begin(), load_filaments_index.end(), slot) != load_filaments_index.end())
|
||||
continue;
|
||||
std::string system_name = current_filaments_system_name[index];
|
||||
if (system_name.empty())
|
||||
continue;
|
||||
PresetBundle::convert_filament_preset_name(current_printer_name, system_name);
|
||||
DynamicPrintConfig config;
|
||||
std::string error;
|
||||
if (!ensure_system_preset_resolver().resolve_system_preset(config, Preset::TYPE_FILAMENT, system_name, config_substitution_rule, error)) {
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("CLI: system filament preset '%1%' not resolved (%2%); filament %3% keeps its values") % system_name % error % slot;
|
||||
continue;
|
||||
}
|
||||
const size_t at = std::upper_bound(load_filaments_index.begin(), load_filaments_index.end(), slot) - load_filaments_index.begin();
|
||||
load_filaments_id.insert(load_filaments_id.begin() + at,
|
||||
project_filament_ids != nullptr && index < project_filament_ids->size() ? project_filament_ids->values[index] : std::string());
|
||||
load_filaments_name.insert(load_filaments_name.begin() + at, system_name);
|
||||
load_filaments_config.insert(load_filaments_config.begin() + at, std::move(config));
|
||||
load_filaments_index.insert(load_filaments_index.begin() + at, slot);
|
||||
load_filaments_inherit.insert(load_filaments_inherit.begin() + at, system_name);
|
||||
load_filaments_refresh.insert(load_filaments_refresh.begin() + at, true);
|
||||
}
|
||||
}
|
||||
|
||||
//fetch upward_compatible_machine
|
||||
if (fetch_upward_values) {
|
||||
if (!current_printer_system_name.empty()) {
|
||||
@@ -3470,7 +3416,7 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
|
||||
//set the filament settings into print config
|
||||
if ((load_filament_count > 0) || (up_config_to_date))
|
||||
if ((load_filament_count > 0) || (up_config_to_date) || !load_filaments_config.empty())
|
||||
{
|
||||
//std::vector<int> filament_variant_count(filament_count, 1);
|
||||
std::vector<int> old_start_indice(filament_count, 0);
|
||||
@@ -3489,9 +3435,14 @@ int CLI::run(int argc, char **argv)
|
||||
max_self_index = std::max(max_self_index, v);
|
||||
min_self_index = std::min(min_self_index, v);
|
||||
}
|
||||
if (max_self_index > filament_count || min_self_index < 1) {
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("filament_self_index range [%1%, %2%] is invalid for filament_count %3%, regenerating")
|
||||
% min_self_index % max_self_index % filament_count;
|
||||
// And a project saved with FEWER filaments than are now loaded (a
|
||||
// one-filament project sliced with two --load-filaments) leaves the tables half filled:
|
||||
// the variant matching below then reads past filament_extruder_variant and
|
||||
// set_with_restore_2 throws an uncaught size error. Regenerate in that case too.
|
||||
if (max_self_index > filament_count || min_self_index < 1 || max_self_index < filament_count
|
||||
|| (int) filament_self_index_opt->values.size() < filament_count) {
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("filament_self_index range [%1%, %2%] (size %4%) is invalid for filament_count %3%, regenerating")
|
||||
% min_self_index % max_self_index % filament_count % filament_self_index_opt->values.size();
|
||||
need_regenerate_self_index = true;
|
||||
}
|
||||
}
|
||||
@@ -3529,6 +3480,8 @@ int CLI::run(int argc, char **argv)
|
||||
for (int index = 0; index < load_filaments_config.size(); index++) {
|
||||
DynamicPrintConfig& config = load_filaments_config[index];
|
||||
int filament_index = load_filaments_index[index];
|
||||
// A filament given with --load-filaments replaces the slot; a refreshed one keeps the project's changed keys.
|
||||
const bool loaded = load_filament_count > 0 && !load_filaments_refresh[index];
|
||||
std::vector<std::string> different_keys;
|
||||
|
||||
//ORCA: diff before load_default_gcodes_to_config, the way the process and machine
|
||||
@@ -3538,12 +3491,12 @@ int CLI::run(int argc, char **argv)
|
||||
// compared" to "compared as empty against the parent" and land in the column
|
||||
// as an override the user never made.
|
||||
std::string filament_different_settings;
|
||||
if (load_filament_count > 0)
|
||||
if (loaded)
|
||||
filament_different_settings = cli_different_settings(config, load_filaments_inherit[index], Preset::TYPE_FILAMENT);
|
||||
|
||||
load_default_gcodes_to_config(config, Preset::TYPE_FILAMENT);
|
||||
|
||||
if (load_filament_count > 0) {
|
||||
if (loaded) {
|
||||
ConfigOptionStrings *opt_filament_settings = static_cast<ConfigOptionStrings *> (m_print_config.option("filament_settings_id", true));
|
||||
std::string& filament_name = load_filaments_name[index];
|
||||
ConfigOptionString* filament_name_setting = new ConfigOptionString(filament_name);
|
||||
@@ -3577,9 +3530,13 @@ int CLI::run(int argc, char **argv)
|
||||
ConfigOptionStrings *curr_variant_opt = m_print_config.option<ConfigOptionStrings>("filament_extruder_variant");
|
||||
if (!curr_variant_opt) {
|
||||
curr_variant_opt = m_print_config.option<ConfigOptionStrings>("filament_extruder_variant", true);
|
||||
std::vector<string>& filament_variants = curr_variant_opt->values;
|
||||
std::vector<std::string>& filament_variants = curr_variant_opt->values;
|
||||
filament_variants.resize(filament_count, get_extruder_variant_string(etDirectDrive, nvtStandard));
|
||||
}
|
||||
// See the filament_self_index note above: one variant per filament for
|
||||
// the filaments the project did not know about.
|
||||
if ((int) curr_variant_opt->values.size() < filament_count)
|
||||
curr_variant_opt->values.resize(filament_count, get_extruder_variant_string(etDirectDrive, nvtStandard));
|
||||
const ConfigOptionStrings *new_variant_opt = dynamic_cast<const ConfigOptionStrings*>(config.option("filament_extruder_variant", true));
|
||||
|
||||
std::vector<int> new_variant_indice;
|
||||
@@ -3588,7 +3545,7 @@ int CLI::run(int argc, char **argv)
|
||||
|
||||
for (int i = 0; i < new_variant_count; i++)
|
||||
{
|
||||
for (int j = old_start_indice[filament_index - 1]; j < old_start_indice[filament_index - 1] + old_variant_count; j++)
|
||||
for (int j = old_start_indice[filament_index - 1]; j < old_start_indice[filament_index - 1] + old_variant_count && j < (int) curr_variant_opt->values.size(); j++)
|
||||
{
|
||||
if (curr_variant_opt->values[j] == new_variant_opt->values[i]) {
|
||||
new_variant_indice[i] = j;
|
||||
@@ -3615,7 +3572,7 @@ int CLI::run(int argc, char **argv)
|
||||
flush_and_exit(CLI_CONFIG_FILE_ERROR);
|
||||
}
|
||||
|
||||
if ((load_filament_count == 0) && !different_keys_set.empty())
|
||||
if (!loaded && !different_keys_set.empty())
|
||||
{
|
||||
std::set<std::string>::iterator iter = different_keys_set.find(opt_key);
|
||||
if ( iter != different_keys_set.end()) {
|
||||
@@ -3640,7 +3597,18 @@ int CLI::run(int argc, char **argv)
|
||||
ConfigOptionVectorBase* opt_vec_dst = static_cast<ConfigOptionVectorBase*>(opt);
|
||||
const ConfigOptionVectorBase* opt_vec_src = static_cast<const ConfigOptionVectorBase*>(source_opt);
|
||||
//set with index
|
||||
opt_vec_dst->set_with_restore_2(opt_vec_src, new_variant_indice, old_start_indice[filament_index - 1], old_variant_count);
|
||||
try {
|
||||
// A project with fewer filaments than are loaded: grow the
|
||||
// destination to the filament's slot first (set_with_restore_2 only restores).
|
||||
if (opt_vec_src->size() > 0 && opt_vec_dst->size() < size_t(old_start_indice[filament_index - 1] + old_variant_count))
|
||||
opt_vec_dst->resize(size_t(old_start_indice[filament_index - 1] + old_variant_count), opt_vec_src);
|
||||
opt_vec_dst->set_with_restore_2(opt_vec_src, new_variant_indice, old_start_indice[filament_index - 1], old_variant_count);
|
||||
} catch (const std::exception &ex) { // Was an uncaught abort
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("filament %1%: option %2% could not be applied: %3%") % filament_index % opt_key % ex.what();
|
||||
boost::nowide::cerr << "filament " << filament_index << ": option " << opt_key << " could not be applied: " << ex.what() << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_CONFIG_FILE_ERROR, 0, cli_errors[CLI_CONFIG_FILE_ERROR], sliced_info);
|
||||
flush_and_exit(CLI_CONFIG_FILE_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
@@ -3673,6 +3641,9 @@ int CLI::run(int argc, char **argv)
|
||||
{
|
||||
if (opt_key == "compatible_prints" || opt_key == "compatible_printers" || opt_key == "model_id" || opt_key == "dev_model_name" || opt_key == "filament_settings_id")
|
||||
continue;
|
||||
// rebuilt from every filament after this loop
|
||||
if (filament_dev_options.find(opt_key) != filament_dev_options.end())
|
||||
continue;
|
||||
ConfigOption *opt = m_print_config.option(opt_key, true);
|
||||
if (opt == nullptr) {
|
||||
// opt_key does not exist in this ConfigBase and it cannot be created, because it is not defined by this->def().
|
||||
@@ -3686,7 +3657,16 @@ int CLI::run(int argc, char **argv)
|
||||
if (filament_options_with_variant.find(opt_key) != filament_options_with_variant.end()) {
|
||||
std::vector<int> temp_variant_indice;
|
||||
temp_variant_indice.resize(new_variant_count, -1);
|
||||
opt_vec_dst->set_with_restore_2(opt_vec_src, temp_variant_indice, old_start_indice[filament_index - 1], old_variant_count, true);
|
||||
try {
|
||||
if (opt_vec_src->size() > 0 && opt_vec_dst->size() < size_t(old_start_indice[filament_index - 1] + old_variant_count)) // See above
|
||||
opt_vec_dst->resize(size_t(old_start_indice[filament_index - 1] + old_variant_count), opt_vec_src);
|
||||
opt_vec_dst->set_with_restore_2(opt_vec_src, temp_variant_indice, old_start_indice[filament_index - 1], old_variant_count, true);
|
||||
} catch (const std::exception &ex) { // Was an uncaught abort
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("filament %1%: option %2% could not be applied: %3%") % filament_index % opt_key % ex.what();
|
||||
boost::nowide::cerr << "filament " << filament_index << ": option " << opt_key << " could not be applied: " << ex.what() << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_CONFIG_FILE_ERROR, 0, cli_errors[CLI_CONFIG_FILE_ERROR], sliced_info);
|
||||
flush_and_exit(CLI_CONFIG_FILE_ERROR);
|
||||
}
|
||||
|
||||
if (opt_key == "filament_extruder_variant")
|
||||
new_variant_counts[filament_index - 1] = opt_vec_src->size();
|
||||
@@ -3736,6 +3716,14 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
}
|
||||
|
||||
// The stored values cannot be told apart per filament, so they are kept as they are unless every slot has a config.
|
||||
std::vector<const DynamicPrintConfig *> filament_configs(filament_count, nullptr);
|
||||
for (size_t index = 0; index < load_filaments_config.size(); index++)
|
||||
if (load_filaments_index[index] >= 1 && load_filaments_index[index] <= filament_count)
|
||||
filament_configs[load_filaments_index[index] - 1] = &load_filaments_config[index];
|
||||
if (std::find(filament_configs.begin(), filament_configs.end(), nullptr) == filament_configs.end())
|
||||
set_filament_dev_options(m_print_config, filament_configs);
|
||||
|
||||
if (m_print_config.option<ConfigOptionStrings>("filament_extruder_variant")) {
|
||||
std::vector<int>& filament_self_indice = m_print_config.option<ConfigOptionInts>("filament_self_index", true)->values;
|
||||
int index_size = m_print_config.option<ConfigOptionStrings>("filament_extruder_variant")->size();
|
||||
@@ -4138,6 +4126,8 @@ int CLI::run(int argc, char **argv)
|
||||
flush_and_exit(CLI_MIXED_FILAMENT_INVALID);
|
||||
}
|
||||
}
|
||||
if (filament_count > 0)
|
||||
resize_mixed_filament_metadata(m_print_config, size_t(filament_count), size_t(filament_count));
|
||||
|
||||
m_print_config.option<ConfigOptionEnum<PrinterTechnology>>("printer_technology", true)->value = printer_technology;
|
||||
|
||||
@@ -4192,6 +4182,10 @@ int CLI::run(int argc, char **argv)
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("%1%, set disable_wipe_tower_after_mapping back to false due to wrapping detect")%__LINE__;
|
||||
}
|
||||
|
||||
// Belt printers never get the classic wipe tower (see Print::has_wipe_tower()), so reserve no space for it.
|
||||
const ConfigOptionBool* belt_printer_opt = m_print_config.option<ConfigOptionBool>("belt_printer");
|
||||
const bool is_belt_printer = belt_printer_opt && belt_printer_opt->value;
|
||||
|
||||
auto timelapse_type_opt = m_print_config.option("timelapse_type");
|
||||
bool is_smooth_timelapse = false;
|
||||
if (enable_timelapse && timelapse_type_opt && (timelapse_type_opt->getInt() == TimelapseType::tlSmooth))
|
||||
@@ -4429,11 +4423,11 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
};
|
||||
|
||||
auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) {
|
||||
auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse, is_belt_printer](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) {
|
||||
plate_obj_size_info.obj_bbox= plate->get_objects_bounding_box();
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("plate %1%, object bbox: min {%2%, %3%, %4%} - max {%5%, %6%, %7%}")
|
||||
%(plate_index+1) %plate_obj_size_info.obj_bbox.min.x() % plate_obj_size_info.obj_bbox.min.y() % plate_obj_size_info.obj_bbox.min.z() %plate_obj_size_info.obj_bbox.max.x() % plate_obj_size_info.obj_bbox.max.y() % plate_obj_size_info.obj_bbox.max.z();
|
||||
if (!print_config.has("wipe_tower_x")) {
|
||||
if (is_belt_printer || !print_config.has("wipe_tower_x")) {
|
||||
plate_obj_size_info.has_wipe_tower = false;
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("can not found wipe_tower_x in config, set to no wipe tower");
|
||||
return;
|
||||
@@ -4659,7 +4653,8 @@ int CLI::run(int argc, char **argv)
|
||||
BoundingBoxf temp_printable_bbox(temp_printable_area);
|
||||
printer_plate.printable_width = static_cast<int>(temp_printable_bbox.size().x());
|
||||
printer_plate.printable_depth = static_cast<int>(temp_printable_bbox.size().y());
|
||||
printer_plate.printable_height = (int)(config.opt_float("printable_height"));
|
||||
if (config.option<ConfigOptionFloat>("printable_height"))
|
||||
printer_plate.printable_height = (int)(config.opt_float("printable_height"));
|
||||
}
|
||||
if (temp_exclude_area.size() >= 4) {
|
||||
printer_plate.exclude_width = (int)(temp_exclude_area[2].x() - temp_exclude_area[0].x());
|
||||
@@ -4805,6 +4800,11 @@ int CLI::run(int argc, char **argv)
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
if (m_models.empty()) {
|
||||
boost::nowide::cerr << "Invalid params: --assemble needs at least one input model." << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
Model m;
|
||||
ModelObject* new_object = m.add_object();
|
||||
new_object->name = _u8L("Assembly");
|
||||
@@ -5298,7 +5298,7 @@ int CLI::run(int argc, char **argv)
|
||||
}
|
||||
}
|
||||
|
||||
if ((!arrange_cfg.is_seq_print && (assemble_plate.filaments_count > 1))||(enable_wrapping_detect && !current_wrapping_exclude_area.empty()))
|
||||
if (!is_belt_printer && ((!arrange_cfg.is_seq_print && (assemble_plate.filaments_count > 1)) || (enable_wrapping_detect && !current_wrapping_exclude_area.empty())))
|
||||
{
|
||||
//prepare the wipe tower
|
||||
int plate_count = partplate_list.get_plate_count();
|
||||
@@ -5450,7 +5450,7 @@ int CLI::run(int argc, char **argv)
|
||||
bool is_seq_print = false;
|
||||
get_print_sequence(cur_plate, m_print_config, is_seq_print);
|
||||
|
||||
if (!is_seq_print && (assemble_plate.filaments_count > 1) && !has_wipe_tower_position)
|
||||
if (!is_belt_printer && !is_seq_print && (assemble_plate.filaments_count > 1) && !has_wipe_tower_position)
|
||||
{
|
||||
//prepare the wipe tower
|
||||
auto printer_structure_opt = m_print_config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure");
|
||||
@@ -5618,7 +5618,7 @@ int CLI::run(int argc, char **argv)
|
||||
};
|
||||
const int max_filament_count = plate_count > 0 ? *std::max_element(plate_filament_counts.begin(), plate_filament_counts.end()) : 0;
|
||||
|
||||
if (plate_needs_wipe_tower(max_filament_count))
|
||||
if (!is_belt_printer && plate_needs_wipe_tower(max_filament_count))
|
||||
{
|
||||
//prepare the wipe tower
|
||||
auto printer_structure_opt = m_print_config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure");
|
||||
@@ -5725,7 +5725,7 @@ int CLI::run(int argc, char **argv)
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": found single object mode");
|
||||
}
|
||||
|
||||
if (m_print_config.has("wipe_tower_x") && (is_smooth_timelapse || !arrange_cfg.is_seq_print || (selected.size() <= 1))) {
|
||||
if (!is_belt_printer && m_print_config.has("wipe_tower_x") && (is_smooth_timelapse || !arrange_cfg.is_seq_print || (selected.size() <= 1))) {
|
||||
float x;
|
||||
float y;
|
||||
if (duplicate_count > 0) {
|
||||
@@ -5745,7 +5745,7 @@ int CLI::run(int argc, char **argv)
|
||||
float w = dynamic_cast<const ConfigOptionFloat *>(m_print_config.option("prime_tower_width"))->value;
|
||||
float a = dynamic_cast<const ConfigOptionFloat *>(m_print_config.option("wipe_tower_rotation_angle"))->value;
|
||||
float v = dynamic_cast<const ConfigOptionFloat *>(m_print_config.option("prime_volume"))->value;
|
||||
unsigned int filaments_cnt = plate_data_src[plate_to_slice-1]->slice_filaments_info.size();
|
||||
unsigned int filaments_cnt = (plate_data_src.size() >= static_cast<size_t>(plate_to_slice)) ? plate_data_src[plate_to_slice-1]->slice_filaments_info.size() : 0;
|
||||
if ((filaments_cnt == 0) || need_skip)
|
||||
{
|
||||
// slice filaments info invalid
|
||||
@@ -6350,7 +6350,7 @@ int CLI::run(int argc, char **argv)
|
||||
// The stored (or default) tower position may not fit the tower these plates
|
||||
// need, and no CLI placement site runs on a plain slice - mirror the GUI's
|
||||
// reload clamp and fit every plate's tower into the printable area first.
|
||||
if (m_print_config.option<ConfigOptionBool>("enable_prime_tower", true)->value) {
|
||||
if (!is_belt_printer && m_print_config.option<ConfigOptionBool>("enable_prime_tower", true)->value) {
|
||||
for (int index = 0; index < partplate_list.get_plate_count(); index++) {
|
||||
if ((plate_to_slice != 0) && (plate_to_slice != (index + 1)))
|
||||
continue;
|
||||
@@ -6602,7 +6602,7 @@ int CLI::run(int argc, char **argv)
|
||||
std::vector<int> result_filaments;
|
||||
//result_filaments.reserve(conflict_filaments.size());
|
||||
std::set_intersection(conflict_filament_vector.begin(), conflict_filament_vector.end(), unprintable_filament_vec[index].begin(),
|
||||
unprintable_filament_vec[index].end(), insert_iterator<vector<int>>(result_filaments, result_filaments.begin()));
|
||||
unprintable_filament_vec[index].end(), std::insert_iterator<std::vector<int>>(result_filaments, result_filaments.begin()));
|
||||
conflict_filament_vector = result_filaments;
|
||||
}
|
||||
}
|
||||
|
||||
+11
-58
@@ -1,10 +1,21 @@
|
||||
#ifndef SLIC3R_HPP
|
||||
#define SLIC3R_HPP
|
||||
|
||||
#include <map>
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/Format/bbs_3mf.hpp"
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/Format/AssembleList.hpp"
|
||||
|
||||
namespace Slic3r { class Preset; }
|
||||
namespace Slic3r { struct PlateBBoxData; }
|
||||
namespace Slic3r { struct ThumbnailData; }
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -19,64 +30,6 @@ namespace IO {
|
||||
};
|
||||
}
|
||||
|
||||
#define JSON_ASSEMPLE_PLATES "plates"
|
||||
#define JSON_ASSEMPLE_PLATE_PARAMS "plate_params"
|
||||
#define JSON_ASSEMPLE_PLATE_NAME "plate_name"
|
||||
#define JSON_ASSEMPLE_PLATE_NEED_ARRANGE "need_arrange"
|
||||
#define JSON_ASSEMPLE_OBJECTS "objects"
|
||||
#define JSON_ASSEMPLE_OBJECT_PATH "path"
|
||||
#define JSON_ASSEMPLE_OBJECT_COUNT "count"
|
||||
#define JSON_ASSEMPLE_OBJECT_FILAMENTS "filaments"
|
||||
#define JSON_ASSEMPLE_OBJECT_POS_X "pos_x"
|
||||
#define JSON_ASSEMPLE_OBJECT_POS_Y "pos_y"
|
||||
#define JSON_ASSEMPLE_OBJECT_POS_Z "pos_z"
|
||||
#define JSON_ASSEMPLE_OBJECT_ASSEMBLE_INDEX "assemble_index"
|
||||
#define JSON_ASSEMPLE_OBJECT_PRINT_PARAMS "print_params"
|
||||
#define JSON_ASSEMPLE_ASSEMBLE_PARAMS "assembled_params"
|
||||
|
||||
|
||||
#define JSON_ASSEMPLE_OBJECT_MIN_Z "min_z"
|
||||
#define JSON_ASSEMPLE_OBJECT_MAX_Z "max_z"
|
||||
#define JSON_ASSEMPLE_OBJECT_HEIGHT_RANGES "height_ranges"
|
||||
#define JSON_ASSEMPLE_OBJECT_RANGE_PARAMS "range_params"
|
||||
|
||||
typedef struct _height_range_info {
|
||||
float min_z;
|
||||
float max_z;
|
||||
|
||||
std::map<std::string, std::string> range_params;
|
||||
}height_range_info_t;
|
||||
|
||||
typedef struct _assembled_param_info {
|
||||
std::map<std::string, std::string> print_params;
|
||||
std::vector<height_range_info_t> height_ranges;
|
||||
}assembled_param_info_t;
|
||||
|
||||
typedef struct _assemble_object_info {
|
||||
std::string path;
|
||||
int count;
|
||||
|
||||
std::vector<int> filaments;
|
||||
std::vector<int> assemble_index;
|
||||
std::vector<float> pos_x;
|
||||
std::vector<float> pos_y;
|
||||
std::vector<float> pos_z;
|
||||
std::map<std::string, std::string> print_params;
|
||||
std::vector<height_range_info_t> height_ranges;
|
||||
}assemble_object_info_t;
|
||||
|
||||
typedef struct _assemble_plate_info {
|
||||
std::string plate_name;
|
||||
bool need_arrange {false};
|
||||
int filaments_count {0};
|
||||
|
||||
std::map<std::string, std::string> plate_params;
|
||||
std::vector<assemble_object_info_t> assemble_obj_list;
|
||||
std::vector<ModelObject *> loaded_obj_list;
|
||||
std::map<int, assembled_param_info_t> assembled_param_list;
|
||||
}assemble_plate_info_t;
|
||||
|
||||
|
||||
typedef struct _printer_plate_info {
|
||||
std::string printer_name;
|
||||
int printable_width{0};
|
||||
|
||||
@@ -35,6 +35,7 @@ extern "C"
|
||||
#include <boost/algorithm/string/classification.hpp>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <boost/algorithm/string/constants.hpp>
|
||||
|
||||
#ifdef SLIC3R_GUI
|
||||
class OpenGLVersionCheck
|
||||
|
||||
@@ -11,6 +11,14 @@
|
||||
|
||||
#include "git_commit_hash.h"
|
||||
#include "libslic3r_version.h"
|
||||
#include "StackWalker.h"
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include <cstdarg>
|
||||
#include <cstddef>
|
||||
#include <ctime>
|
||||
#include <excpt.h>
|
||||
|
||||
static std::string g_log_folder;
|
||||
static std::atomic<int> g_crash_log_count = 0;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include "StackWalker.h"
|
||||
#include <eh.h>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
class CBaseException : public CStackWalker
|
||||
{
|
||||
|
||||
@@ -28,6 +28,12 @@ if (ORCA_TOOLS)
|
||||
target_link_libraries(generate_system_cache libslic3r boost_headeronly)
|
||||
target_compile_definitions(generate_system_cache PRIVATE ${_DEV_DEFS})
|
||||
|
||||
# texture_unwrap_dump: reports the LSCM unwrap of a saved project's texture displacement layers,
|
||||
# chart by chart, so a defect can be reproduced from the project file instead of from a screenshot.
|
||||
add_executable(texture_unwrap_dump texture_unwrap_dump.cpp)
|
||||
target_link_libraries(texture_unwrap_dump libslic3r boost_headeronly nanosvg)
|
||||
target_compile_definitions(texture_unwrap_dump PRIVATE ${_DEV_DEFS})
|
||||
|
||||
# profile_include_dump: prints what included templates contribute to a vendor's presets,
|
||||
# to diff against the same tool built in BambuStudio. Built only on request.
|
||||
add_executable(profile_include_dump EXCLUDE_FROM_ALL profile_include_dump.cpp)
|
||||
|
||||
@@ -8,6 +8,28 @@
|
||||
#define NANOSVGRAST_IMPLEMENTATION
|
||||
#include "nanosvg/nanosvgrast.h"
|
||||
|
||||
#include <vector>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <map>
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include <utility>
|
||||
#include "libslic3r/CustomGCode.hpp"
|
||||
#include <iterator>
|
||||
#include <boost/smart_ptr/make_shared_object.hpp>
|
||||
#include <boost/smart_ptr/shared_ptr.hpp>
|
||||
#include <boost/log/core/record_view.hpp>
|
||||
#include <boost/log/expressions/message.hpp>
|
||||
#include <cstddef>
|
||||
#include <exception>
|
||||
#include "libslic3r/PlaceholderParser.hpp"
|
||||
#include <boost/program_options/options_description.hpp>
|
||||
#include <boost/program_options/value_semantic.hpp>
|
||||
#include <boost/program_options/variables_map.hpp>
|
||||
#include <boost/program_options/errors.hpp>
|
||||
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/GCode.hpp"
|
||||
#include "libslic3r/GCode/WipeTower.hpp"
|
||||
@@ -35,6 +57,10 @@
|
||||
#include <iostream>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
#include "libslic3r/Format/STEP.hpp"
|
||||
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
using namespace Slic3r;
|
||||
namespace po = boost::program_options;
|
||||
@@ -176,6 +202,7 @@ Vec2d place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d ¢er)
|
||||
std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl, bool by_object)
|
||||
{
|
||||
const Vec2d center = printable_area_center(cfg);
|
||||
const bool belt = cfg.opt_bool("belt_printer");
|
||||
std::vector<Vec2d> cube_mins;
|
||||
if (by_object) {
|
||||
// By-object printing fires the hook only without a wipe tower, and rules out clumping detection and
|
||||
@@ -186,6 +213,12 @@ std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl,
|
||||
cfg.set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
|
||||
cfg.set_key_value("skirt_loops", new ConfigOptionInt(0));
|
||||
cube_mins = {center + Vec2d(-20., -5.), center + Vec2d(10., -5.)};
|
||||
} else if (belt) {
|
||||
// A belt object's slicing Z starts at the belt below its leading end, well below its first
|
||||
// printed layer, so a height range in slicing Z does not map onto the part. Two cubes one
|
||||
// behind the other along the belt, the second on filament 2, give the one filament change
|
||||
// instead (the purge prism is an object the GUI adds, so there is no tower to place).
|
||||
cube_mins = {center - Vec2d(5., 15.), center + Vec2d(-5., 5.)};
|
||||
} else {
|
||||
// Clumping detection changes the tower footprint, so turn it on before placing the tower.
|
||||
if (!cfg.opt_string("wrapping_detection_gcode").empty())
|
||||
@@ -203,14 +236,20 @@ std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl,
|
||||
obj->name = "cube"; // populates [input_filename_base] the way a loaded model does
|
||||
obj->add_volume(m);
|
||||
obj->add_instance();
|
||||
// Filament 2 is used only above z=4, so the upper layers carry a single filament change.
|
||||
DynamicPrintConfig range_config;
|
||||
range_config.set_key_value("extruder", new ConfigOptionInt(2));
|
||||
// Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm
|
||||
// printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that
|
||||
// trips Flow::with_spacing.
|
||||
range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height")));
|
||||
obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config));
|
||||
if (belt && !by_object) {
|
||||
// The second cube along the belt is on filament 2 (see cube_mins above).
|
||||
if (&cube_min == &cube_mins.back())
|
||||
obj->config.set_key_value("extruder", new ConfigOptionInt(2));
|
||||
} else {
|
||||
// Filament 2 is used only above z=4, so the upper layers carry a single filament change.
|
||||
DynamicPrintConfig range_config;
|
||||
range_config.set_key_value("extruder", new ConfigOptionInt(2));
|
||||
// Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm
|
||||
// printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that
|
||||
// trips Flow::with_spacing.
|
||||
range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height")));
|
||||
obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config));
|
||||
}
|
||||
obj->ensure_on_bed();
|
||||
print.auto_assign_extruders(obj);
|
||||
}
|
||||
@@ -495,11 +534,16 @@ int slice_all_printers(const std::string &vendor, const std::string &outdir)
|
||||
const std::string filament_name = bundle.filaments.get_selected_preset_name();
|
||||
const std::string what = "Printer \"" + printer + "\"";
|
||||
const std::string file_base = sanitize_filename(vendor_name) + "__" + sanitize_filename(printer);
|
||||
// A belt printer has no wipe tower (it purges into a prism object on the belt), so its
|
||||
// filament change is the plain tool change set_extruder() emits rather than the tower's block.
|
||||
const bool belt = bundle.printers.get_selected_preset().config.opt_bool("belt_printer");
|
||||
const std::string marker = belt ? "\nT1\n" : "CP TOOLCHANGE START";
|
||||
if (const std::string out = slice_selection(bundle, what, false, outdir, file_base); out.empty())
|
||||
++failures;
|
||||
else if (out.find("CP TOOLCHANGE START") == std::string::npos) {
|
||||
// The filament change never rode the tower, so change_filament_gcode was not exercised.
|
||||
BOOST_LOG_TRIVIAL(error) << what << " sliced but the filament change never fired (no CP TOOLCHANGE START)";
|
||||
else if (out.find(marker) == std::string::npos) {
|
||||
// The filament change never fired, so change_filament_gcode was not exercised.
|
||||
BOOST_LOG_TRIVIAL(error) << what << " sliced but the filament change never fired (no "
|
||||
<< (belt ? "T1" : "CP TOOLCHANGE START") << ")";
|
||||
++failures;
|
||||
}
|
||||
cover(bundle.prints.get_selected_preset());
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
#include <strsafe.h>
|
||||
//#include <atlconv.h>
|
||||
#include <dbghelp.h>
|
||||
#include <cstdarg>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#pragma comment(lib, "version.lib")
|
||||
#pragma comment( lib, "dbghelp.lib" )
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <Windows.h>
|
||||
#include <tchar.h>
|
||||
#include <vector>
|
||||
#include <cstddef>
|
||||
|
||||
namespace textconv_helper
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include <iosfwd>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
|
||||
@@ -4,9 +4,19 @@
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
#include <boost/filesystem/directory.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/program_options.hpp>
|
||||
#include <boost/program_options/options_description.hpp>
|
||||
#include <boost/program_options/value_semantic.hpp>
|
||||
#include <boost/program_options/variables_map.hpp>
|
||||
#include <boost/program_options/errors.hpp>
|
||||
#include <exception>
|
||||
#include <cstddef>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include "libslic3r/Config.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
@@ -5,5 +5,10 @@
|
||||
<!-- for dynamic loading of libraries without signature validation. Used for 3dconnection drivers.-->
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<!-- The Bambu network plug-in's code protector rewrites one of its own signed code pages
|
||||
after loading. Under the hardened runtime macOS kills the process when that page is
|
||||
paged back in; this lets it run, as Bambu Studio's signature does. -->
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -118,9 +118,13 @@ bundle_dependency_closure() {
|
||||
# (scripts/check_appimage_libs.sh).
|
||||
local -a search_dirs=("$dst_dir")
|
||||
|
||||
while [ ${#queue[@]} -gt 0 ]; do
|
||||
target="${queue[0]}"
|
||||
queue=("${queue[@]:1}")
|
||||
# Walked with a read index rather than by reslicing the array: "${queue[@]:1}" rebuilds the whole
|
||||
# queue on every pop, so draining it costs O(n^2). At the ~5k entries this bundle reaches that was
|
||||
# ~22 s of the image build spent copying an array around.
|
||||
local head=0
|
||||
while [ "$head" -lt ${#queue[@]} ]; do
|
||||
target="${queue[$head]}"
|
||||
head=$((head + 1))
|
||||
|
||||
if [ ! -e "$target" ] || ! appimage_is_elf_file "$target"; then
|
||||
continue
|
||||
@@ -299,10 +303,11 @@ else
|
||||
fi
|
||||
|
||||
has_host_runtime_library() {
|
||||
local lib_name path
|
||||
local lib_name="\$1"
|
||||
local path
|
||||
|
||||
if command -v ldconfig >/dev/null 2>&1; then
|
||||
if ldconfig -p 2>/dev/null | grep -Fq " \$lib_name"; then
|
||||
if ldconfig -p 2>/dev/null | grep -Fq "\$lib_name ("; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
@@ -339,7 +344,7 @@ TARGET_BIN="\$1"
|
||||
|
||||
if target_missing_runtime_library "\$TARGET_BIN" "libOpenGL.so.0" || ! has_host_runtime_library "libOpenGL.so.0"; then
|
||||
echo "Error: missing host OpenGL runtime library libOpenGL.so.0." >&2
|
||||
echo "On Ubuntu/Pop!_OS/Debian, install: libopengl0 and libglu1-mesa" >&2
|
||||
echo "On Ubuntu/Pop!_OS/Debian, install: libopengl0" >&2
|
||||
echo "On Arch/CachyOS, install: libglvnd" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -11,15 +11,22 @@
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
namespace fs = boost::filesystem;
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
// Diagnostic for the LSCM unwrap of a texture displacement layer.
|
||||
//
|
||||
// It exists because the defect it hunts only shows up on a real painted patch: the paint mask is built
|
||||
// by TriangleSelector splitting base triangles, so the patch topology cannot be written down by hand,
|
||||
// and reasoning about it from a screenshot of the 3D view had already produced three wrong diagnoses.
|
||||
// This loads a saved project, rebuilds exactly the patch the bake would act on, runs the same unwrap,
|
||||
// and reports what came out - per chart, so a bad one can be pointed at rather than guessed at.
|
||||
//
|
||||
// texture_unwrap_dump <project.3mf>
|
||||
|
||||
// nanosvg is header-only and libslic3r's 3mf import references it without carrying the implementation,
|
||||
// so every executable that links libslic3r has to supply it. Must precede any include that pulls the
|
||||
// header in, or its include guard suppresses the implementation. Same pattern as the other dev tools.
|
||||
#define NANOSVG_IMPLEMENTATION
|
||||
#include "nanosvg/nanosvg.h"
|
||||
#define NANOSVGRAST_IMPLEMENTATION
|
||||
#include "nanosvg/nanosvgrast.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "libslic3r/Format/bbs_3mf.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
namespace {
|
||||
|
||||
uint64_t edge_key(int a, int b)
|
||||
{
|
||||
if (a > b)
|
||||
std::swap(a, b);
|
||||
return (uint64_t(uint32_t(a)) << 32) | uint32_t(b);
|
||||
}
|
||||
|
||||
// Boundary loops and the Euler characteristic of a face set, which together say whether a chart is the
|
||||
// topological disk LSCM needs (one loop, V - E + F == 1).
|
||||
void chart_topology(const indexed_triangle_set &mesh, const std::vector<int> &faces, int &loops, int &euler)
|
||||
{
|
||||
std::unordered_map<uint64_t, int> edge_use;
|
||||
std::unordered_map<int, int> local;
|
||||
for (const int f : faces) {
|
||||
const stl_triangle_vertex_indices &t = mesh.indices[size_t(f)];
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
++edge_use[edge_key(t[i], t[(i + 1) % 3])];
|
||||
local.emplace(t[i], int(local.size()));
|
||||
}
|
||||
}
|
||||
euler = int(local.size()) - int(edge_use.size()) + int(faces.size());
|
||||
|
||||
std::unordered_map<int, int> parent;
|
||||
const std::function<int(int)> find = [&](int x) {
|
||||
while (parent[x] != x)
|
||||
x = parent[x] = parent[parent[x]];
|
||||
return x;
|
||||
};
|
||||
for (const auto &[key, uses] : edge_use)
|
||||
if (uses == 1)
|
||||
for (const int v : { int(key >> 32), int(uint32_t(key)) })
|
||||
parent.emplace(v, v);
|
||||
for (const auto &[key, uses] : edge_use)
|
||||
if (uses == 1) {
|
||||
const int a = find(int(key >> 32)), b = find(int(uint32_t(key)));
|
||||
if (a != b)
|
||||
parent[b] = a;
|
||||
}
|
||||
std::unordered_map<int, int> roots;
|
||||
for (const auto &[v, p] : parent)
|
||||
roots[find(v)] = 1;
|
||||
loops = int(roots.size());
|
||||
}
|
||||
|
||||
float signed_area_2d(const Vec2f &a, const Vec2f &b, const Vec2f &c)
|
||||
{
|
||||
return 0.5f * ((b.x() - a.x()) * (c.y() - a.y()) - (c.x() - a.x()) * (b.y() - a.y()));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc < 2) {
|
||||
std::printf("usage: texture_unwrap_dump <project.3mf>\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
Model model;
|
||||
DynamicPrintConfig config;
|
||||
ConfigSubstitutionContext ctx(ForwardCompatibilitySubstitutionRule::Enable);
|
||||
PlateDataPtrs plate_data;
|
||||
std::vector<Preset *> project_presets;
|
||||
bool is_bbl_3mf = false, is_orca_3mf = false;
|
||||
Semver file_version;
|
||||
// The importer writes a backup copy under the data dir and silently loses objects without one.
|
||||
const boost::filesystem::path tmp = boost::filesystem::temp_directory_path() / "texture_unwrap_dump";
|
||||
boost::filesystem::create_directories(tmp);
|
||||
set_data_dir(tmp.string());
|
||||
|
||||
// LoadModel so the meshes come through; AddDefaultInstances because an object with no instance is
|
||||
// dropped by the plate mapping, which is what "skip this object" in the log means.
|
||||
if (!load_bbs_3mf(argv[1], &config, &ctx, &model, &plate_data, &project_presets, &is_bbl_3mf, &is_orca_3mf,
|
||||
&file_version, nullptr,
|
||||
LoadStrategy::LoadModel | LoadStrategy::LoadConfig | LoadStrategy::AddDefaultInstances |
|
||||
LoadStrategy::Silence)) {
|
||||
std::printf("failed to load %s\n", argv[1]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::printf("loaded: %zu object(s)\n", model.objects.size());
|
||||
|
||||
for (const ModelObject *object : model.objects)
|
||||
for (const ModelVolume *volume : object->volumes) {
|
||||
if (volume->texture_displacement_layers.empty()) {
|
||||
std::printf("volume \"%s\": no texture displacement layers; paint masks per slot:",
|
||||
volume->name.c_str());
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
std::printf(" %zu", volume->texture_displacement_facet(i).get_data().triangles_to_split.size());
|
||||
std::printf("\n");
|
||||
continue;
|
||||
}
|
||||
std::printf("volume \"%s\": %zu base triangles, %zu layer(s)\n", volume->name.c_str(),
|
||||
volume->mesh().its.indices.size(), volume->texture_displacement_layers.size());
|
||||
|
||||
for (const TextureDisplacementLayer &layer : volume->texture_displacement_layers) {
|
||||
std::printf("\n layer %d \"%s\" mapping=%d seam_angle=%.1f connect=%d islands_stored=%zu\n",
|
||||
layer.slot, layer.name.c_str(), int(layer.projection_method),
|
||||
layer.lscm_seam_angle_deg, int(layer.auto_connect_islands), layer.islands.size());
|
||||
if (layer.projection_method != TextureProjectionMethod::LSCM)
|
||||
continue;
|
||||
|
||||
const indexed_triangle_set patch =
|
||||
extract_painted_patch(volume->mesh().its, volume->texture_displacement_facet(layer.slot).get_data());
|
||||
std::printf(" patch: %zu vertices, %zu triangles\n", patch.vertices.size(), patch.indices.size());
|
||||
if (patch.indices.empty())
|
||||
continue;
|
||||
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
const PatchUnwrap unwrap = compute_patch_unwrap(patch, layer.lscm_seam_angle_deg, 0.f,
|
||||
layer.lscm_seam_edges);
|
||||
const auto t1 = std::chrono::steady_clock::now();
|
||||
std::printf(" TIMING compute_patch_unwrap: %.0f ms\n",
|
||||
std::chrono::duration<double, std::milli>(t1 - t0).count());
|
||||
std::printf(" unwrap: %d charts, %zu unwrapped triangles\n", unwrap.chart_count,
|
||||
unwrap.indices.size());
|
||||
|
||||
// Group the patch's faces by chart so each can be examined on its own.
|
||||
std::vector<std::vector<int>> chart_faces(size_t(std::max(unwrap.chart_count, 0)));
|
||||
for (size_t i = 0; i < unwrap.indices.size(); ++i) {
|
||||
const int chart = unwrap.vertex_chart[size_t(unwrap.indices[i][0])];
|
||||
if (chart >= 0 && size_t(chart) < chart_faces.size())
|
||||
chart_faces[size_t(chart)].push_back(unwrap.source_face[i]);
|
||||
}
|
||||
|
||||
int bad_charts = 0;
|
||||
for (size_t c = 0; c < chart_faces.size(); ++c) {
|
||||
int loops = 0, euler = 0;
|
||||
chart_topology(patch, chart_faces[c], loops, euler);
|
||||
|
||||
// Flipped triangles: the unwrap folded over itself, which is what a planar fallback
|
||||
// does to a chart that is not flat. Measured on the unwrap's own triangles.
|
||||
int pos = 0, neg = 0;
|
||||
for (size_t i = 0; i < unwrap.indices.size(); ++i) {
|
||||
const stl_triangle_vertex_indices &t = unwrap.indices[i];
|
||||
if (unwrap.vertex_chart[size_t(t[0])] != int(c))
|
||||
continue;
|
||||
const float a = signed_area_2d(unwrap.uvs[size_t(t[0])], unwrap.uvs[size_t(t[1])],
|
||||
unwrap.uvs[size_t(t[2])]);
|
||||
if (a > 0.f) ++pos; else if (a < 0.f) ++neg;
|
||||
}
|
||||
const int flipped = std::min(pos, neg);
|
||||
const bool disk = loops == 1 && euler == 1;
|
||||
if (!disk || flipped > 0) {
|
||||
++bad_charts;
|
||||
std::printf(" chart %2zu: %4zu faces loops=%d euler=%d%s flipped=%d/%d%s\n", c,
|
||||
chart_faces[c].size(), loops, euler, disk ? "" : " NOT A DISK", flipped,
|
||||
pos + neg, flipped ? " FOLDED" : "");
|
||||
}
|
||||
}
|
||||
std::printf(" charts with a defect: %d / %d\n", bad_charts, unwrap.chart_count);
|
||||
|
||||
// What the eye actually sees. Every patch edge shared by two charts should carry the same
|
||||
// UV on both sides once the islands are laid out as a connected net; where it does not,
|
||||
// the texture jumps across that seam. Measured through compute_lscm_uvs(), i.e. the exact
|
||||
// coordinates the bake and the checker overlay sample.
|
||||
{
|
||||
const auto n0 = std::chrono::steady_clock::now();
|
||||
const std::vector<TextureIsland> net = compute_connected_net(unwrap);
|
||||
const auto n1 = std::chrono::steady_clock::now();
|
||||
std::printf(" TIMING compute_connected_net: %.0f ms (%zu islands)\n",
|
||||
std::chrono::duration<double, std::milli>(n1 - n0).count(), net.size());
|
||||
}
|
||||
const auto t2 = std::chrono::steady_clock::now();
|
||||
const std::vector<Vec2f> uv = compute_lscm_uvs(patch, layer);
|
||||
const auto t3 = std::chrono::steady_clock::now();
|
||||
std::printf(" TIMING compute_lscm_uvs: %.0f ms (called on every preview, overlay and bake)\n",
|
||||
std::chrono::duration<double, std::milli>(t3 - t2).count());
|
||||
if (uv.size() != patch.vertices.size()) {
|
||||
std::printf(" compute_lscm_uvs returned %zu uvs for %zu vertices\n", uv.size(),
|
||||
patch.vertices.size());
|
||||
continue;
|
||||
}
|
||||
// Per-corner UVs carry each chart's own placement, so an edge shared by two charts shows
|
||||
// the jump directly: the same mesh vertex lands at two different UVs. That is exactly what
|
||||
// the eye reads as the texture breaking.
|
||||
const auto t4 = std::chrono::steady_clock::now();
|
||||
const std::vector<Vec2f> corner = compute_lscm_corner_uvs(patch, layer);
|
||||
const auto t5 = std::chrono::steady_clock::now();
|
||||
std::printf(" TIMING compute_lscm_corner_uvs: %.0f ms\n",
|
||||
std::chrono::duration<double, std::milli>(t5 - t4).count());
|
||||
// Keyed by edge, holding the UV each incident face gives to the edge's *lower-numbered*
|
||||
// endpoint. Comparing that same vertex on both sides is the point: indexing by corner
|
||||
// position instead compares opposite ends of the edge, because the two faces wind it in
|
||||
// opposite directions.
|
||||
std::unordered_map<uint64_t, std::vector<Vec2f>> edge_seen;
|
||||
if (corner.size() == patch.indices.size() * 3)
|
||||
for (size_t f = 0; f < patch.indices.size(); ++f) {
|
||||
const stl_triangle_vertex_indices &t = patch.indices[f];
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const int a = t[k], b = t[(k + 1) % 3];
|
||||
const int probe = std::min(a, b);
|
||||
const int local = (a == probe) ? k : (k + 1) % 3;
|
||||
edge_seen[edge_key(a, b)].push_back(corner[f * 3 + size_t(local)]);
|
||||
}
|
||||
}
|
||||
// Which chart each patch face belongs to, so a broken edge can be attributed to a pair.
|
||||
std::vector<int> chart_of_face(patch.indices.size(), -1);
|
||||
for (size_t i = 0; i < unwrap.indices.size(); ++i)
|
||||
chart_of_face[size_t(unwrap.source_face[i])] = unwrap.vertex_chart[size_t(unwrap.indices[i][0])];
|
||||
|
||||
std::unordered_map<uint64_t, std::vector<int>> edge_faces;
|
||||
for (size_t f = 0; f < patch.indices.size(); ++f) {
|
||||
const stl_triangle_vertex_indices &t = patch.indices[f];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
edge_faces[edge_key(t[k], t[(k + 1) % 3])].push_back(int(f));
|
||||
}
|
||||
|
||||
int adjacent = 0, broken = 0, broken_same_chart = 0;
|
||||
float worst = 0.f;
|
||||
std::map<std::pair<int, int>, std::pair<int, float>> by_pair;
|
||||
for (const auto &[key, seen] : edge_seen) {
|
||||
if (seen.size() != 2)
|
||||
continue;
|
||||
++adjacent;
|
||||
const float d = (seen[0] - seen[1]).norm();
|
||||
if (d <= 1e-4f)
|
||||
continue;
|
||||
++broken;
|
||||
worst = std::max(worst, d);
|
||||
const auto &faces_here = edge_faces[key];
|
||||
int c1 = -1, c2 = -1;
|
||||
if (faces_here.size() == 2) {
|
||||
c1 = chart_of_face[size_t(faces_here[0])];
|
||||
c2 = chart_of_face[size_t(faces_here[1])];
|
||||
}
|
||||
if (c1 == c2)
|
||||
++broken_same_chart;
|
||||
auto &slot = by_pair[{ std::min(c1, c2), std::max(c1, c2) }];
|
||||
++slot.first;
|
||||
slot.second = std::max(slot.second, d);
|
||||
}
|
||||
std::printf(" broken edges inside a single chart: %d\n", broken_same_chart);
|
||||
std::printf(" broken by chart pair:");
|
||||
for (const auto &[pk, v] : by_pair)
|
||||
std::printf(" (%d,%d)x%d/%.1f", pk.first, pk.second, v.first, v.second);
|
||||
std::printf("\n");
|
||||
// Total length of the seams left broken, in mm: how much visibly torn edge the layout has,
|
||||
// which is what the eye adds up. A count alone hides whether the breaks are hairlines or
|
||||
// whole sides of an island.
|
||||
float seam_mm = 0.f;
|
||||
for (const auto &[key, seen] : edge_seen) {
|
||||
if (seen.size() != 2 || (seen[0] - seen[1]).norm() <= 1e-4f)
|
||||
continue;
|
||||
seam_mm += (patch.vertices[size_t(key >> 32)] - patch.vertices[size_t(uint32_t(key))]).norm();
|
||||
}
|
||||
std::printf(" interior edges: %d, discontinuous: %d, total torn seam: %.2f mm (worst jump %.3f)\n",
|
||||
adjacent, broken, seam_mm, worst);
|
||||
std::printf(" stored islands %zu vs charts %d -> %s\n", layer.islands.size(),
|
||||
unwrap.chart_count,
|
||||
layer.islands.size() == size_t(unwrap.chart_count) ? "stored placements used"
|
||||
: "net rebuilt");
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1,10 +1,19 @@
|
||||
#include "AABBMesh.hpp"
|
||||
#include "Point.hpp"
|
||||
#include <Eigen/Core>
|
||||
#include <Execution/ExecutionTBB.hpp>
|
||||
|
||||
#include <igl/Hit.h>
|
||||
#include <cstddef>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <libslic3r/AABBTreeIndirect.hpp>
|
||||
#include <libslic3r/TriangleMesh.hpp>
|
||||
|
||||
#include <limits>
|
||||
#include <numeric>
|
||||
#include <vector>
|
||||
|
||||
#ifdef SLIC3R_HOLE_RAYCASTER
|
||||
#include <libslic3r/SLA/Hollowing.hpp>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#ifndef PRUSASLICER_AABBMESH_H
|
||||
#define PRUSASLICER_AABBMESH_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <cmath>
|
||||
#include <cassert>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
|
||||
@@ -6,15 +6,20 @@
|
||||
#ifndef slic3r_AABBTreeIndirect_hpp_
|
||||
#define slic3r_AABBTreeIndirect_hpp_
|
||||
|
||||
#include <Eigen/Core>
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cassert>
|
||||
#include <limits>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <Eigen/Geometry>
|
||||
|
||||
#include "BoundingBox.hpp"
|
||||
#include "Utils.hpp" // for next_highest_power_of_2()
|
||||
#include "libslic3r.h"
|
||||
|
||||
// Definition of the ray intersection hit structure.
|
||||
#include <igl/Hit.h>
|
||||
|
||||
@@ -6,9 +6,15 @@
|
||||
#include "libslic3r.h"
|
||||
#include "libslic3r/AABBTreeIndirect.hpp"
|
||||
#include "libslic3r/Line.hpp"
|
||||
#include <Eigen/Core>
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <tuple>
|
||||
#include <limits>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -352,7 +358,7 @@ namespace AABBTreeLines {
|
||||
return dist;
|
||||
}
|
||||
|
||||
std::vector<size_t> all_lines_in_radius(const Vec<LineType::Dim, Scalar>& point, Floating radius)
|
||||
std::vector<size_t> all_lines_in_radius(const Vec<LineType::Dim, Scalar>& point, Floating radius) const
|
||||
{
|
||||
return AABBTreeLines::all_lines_in_radius(this->lines, this->tree, point.template cast<Floating>(), radius * radius);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
#ifndef ASTAR_HPP
|
||||
#define ASTAR_HPP
|
||||
|
||||
#include <cassert>
|
||||
#include <cmath> // std::isinf() is here
|
||||
#include <type_traits>
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "libslic3r/MutablePriorityQueue.hpp"
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
#include "LineSplit.hpp"
|
||||
|
||||
#include "AABBTreeLines.hpp"
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/ClipperZUtils.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Line.hpp"
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "SVG.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include <limits>
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
|
||||
//#define DEBUG_SPLIT_LINE
|
||||
|
||||
@@ -14,7 +26,7 @@ static std::atomic<std::uint32_t> g_dbg_id = 0;
|
||||
#endif
|
||||
|
||||
// Z for points from clip polygon
|
||||
static constexpr auto CLIP_IDX = std::numeric_limits<ClipperLib_Z::cInt>::max();
|
||||
static constexpr auto CLIP_IDX = std::numeric_limits<coord_t>::max();
|
||||
|
||||
static void cb_split_line(const ClipperZUtils::ZPoint& e1bot,
|
||||
const ClipperZUtils::ZPoint& e1top,
|
||||
@@ -91,14 +103,7 @@ SplittedLine do_split_line(const ClipperZUtils::ZPath& path, const ExPolygons& c
|
||||
clip_path.emplace_back(ClipperZUtils::to_zpath<false>(hole.points, CLIP_IDX));
|
||||
}
|
||||
|
||||
ClipperLib_Z::Clipper zclipper;
|
||||
zclipper.PreserveCollinear(true);
|
||||
zclipper.ZFillFunction(cb_split_line);
|
||||
zclipper.AddPaths(clip_path, ClipperLib_Z::ptClip, true);
|
||||
zclipper.AddPath(path, ClipperLib_Z::ptSubject, false);
|
||||
ClipperLib_Z::PolyTree polytree;
|
||||
zclipper.Execute(ClipperLib_Z::ctIntersection, polytree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero);
|
||||
ClipperLib_Z::PolyTreeToPaths(std::move(polytree), intersections);
|
||||
intersections = ClipperZUtils::clip_zpaths(ctIntersection, ClipperZUtils::ZPaths{ path }, true, clip_path, cb_split_line, true);
|
||||
}
|
||||
if (intersections.empty()) {
|
||||
return {};
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
#define SRC_LIBSLIC3R_ALGORITHM_LINE_SPLIT_HPP_
|
||||
|
||||
#include "ClipperZUtils.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
namespace Algorithm {
|
||||
@@ -50,7 +56,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));
|
||||
ClipperLib_Z::cInt z = 0;
|
||||
coord_t z = 0;
|
||||
for (const auto& point : path) {
|
||||
p.emplace_back(point.x(), point.y(), z);
|
||||
z++;
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
#include "RegionExpansion.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Polyline.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cassert>
|
||||
#include <iterator>
|
||||
#include <cstdint>
|
||||
#include <libslic3r/AABBTreeIndirect.hpp>
|
||||
#include <libslic3r/ClipperZUtils.hpp>
|
||||
#include <libslic3r/ClipperUtils.hpp>
|
||||
#include <libslic3r/Utils.hpp>
|
||||
|
||||
#include <math.h>
|
||||
#include <numeric>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
namespace Algorithm {
|
||||
|
||||
// Calculating radius discretization according to ClipperLib offsetter code, see void ClipperOffset::DoOffset(double delta)
|
||||
// Calculating radius discretization according to the Clipper offsetter code, see ClipperOffset::DoGroupOffset()
|
||||
inline double clipper_round_offset_error(double offset, double arc_tolerance)
|
||||
{
|
||||
static constexpr const double def_arc_tolerance = 0.25;
|
||||
@@ -61,7 +76,6 @@ RegionExpansionParameters RegionExpansionParameters::build(
|
||||
|
||||
// Accuracy of the offsetter for wave propagation.
|
||||
out.arc_tolerance = scaled<double>(0.1);
|
||||
out.shortest_edge_length = out.initial_step * ClipperOffsetShortestEdgeFactor;
|
||||
|
||||
// Maximum inflation of seed contours over the boundary. Used to trim boundary to speed up
|
||||
// clipping during wave propagation. Needs to be in sync with the offsetter accuracy.
|
||||
@@ -75,25 +89,20 @@ RegionExpansionParameters RegionExpansionParameters::build(
|
||||
|
||||
// similar to expolygons_to_zpaths(), but each contour is expanded before converted to zpath.
|
||||
// The expanded contours are then opened (the first point is repeated at the end).
|
||||
static ClipperLib_Z::Paths expolygons_to_zpaths_expanded_opened(
|
||||
static ClipperZUtils::ZPaths expolygons_to_zpaths_expanded_opened(
|
||||
const ExPolygons &src, const float expansion, coord_t &base_idx)
|
||||
{
|
||||
ClipperLib_Z::Paths out;
|
||||
ClipperZUtils::ZPaths out;
|
||||
out.reserve(2 * std::accumulate(src.begin(), src.end(), size_t(0),
|
||||
[](const size_t acc, const ExPolygon &expoly) { return acc + expoly.num_contours(); }));
|
||||
ClipperLib::ClipperOffset offsetter;
|
||||
offsetter.ShortestEdgeLength = expansion * ClipperOffsetShortestEdgeFactor;
|
||||
ClipperLib::Paths expansion_cache;
|
||||
for (const ExPolygon &expoly : src) {
|
||||
for (size_t icontour = 0; icontour < expoly.num_contours(); ++ icontour) {
|
||||
// Execute reorients the contours so that the outer most contour has a positive area. Thus the output
|
||||
// contours will be CCW oriented even though the input paths are CW oriented.
|
||||
// Offset is applied after contour reorientation, thus the signum of the offset value is reversed.
|
||||
offsetter.Clear();
|
||||
offsetter.AddPath(expoly.contour_or_hole(icontour).points, ClipperLib::jtSquare, ClipperLib::etClosedPolygon);
|
||||
expansion_cache.clear();
|
||||
offsetter.Execute(expansion_cache, icontour == 0 ? expansion : -expansion);
|
||||
append(out, ClipperZUtils::to_zpaths<true>(expansion_cache, base_idx));
|
||||
// Orient CCW, then grow the contour and shrink the holes. The output contours are CCW.
|
||||
Polygon contour = expoly.contour_or_hole(icontour);
|
||||
if (! contour.is_counter_clockwise())
|
||||
contour.reverse();
|
||||
for (const Polygon &expanded : offset(contour, icontour == 0 ? expansion : -expansion, jtSquare))
|
||||
out.emplace_back(ClipperZUtils::to_zpath<true>(expanded.points, base_idx));
|
||||
}
|
||||
++ base_idx;
|
||||
}
|
||||
@@ -104,21 +113,21 @@ static ClipperLib_Z::Paths expolygons_to_zpaths_expanded_opened(
|
||||
// Thus some pieces of the clipped polygons may now become split at the ends of the source polygons.
|
||||
// Those ends are sorted lexicographically in "splits".
|
||||
// Reconnect those split pieces.
|
||||
static inline void merge_splits(ClipperLib_Z::Paths &paths, std::vector<std::pair<ClipperLib_Z::IntPoint, int>> &splits)
|
||||
static inline void merge_splits(ClipperZUtils::ZPaths &paths, std::vector<std::pair<ClipperZUtils::ZPoint, int>> &splits)
|
||||
{
|
||||
for (auto it_path = paths.begin(); it_path != paths.end(); ) {
|
||||
ClipperLib_Z::Path &path = *it_path;
|
||||
ClipperZUtils::ZPath &path = *it_path;
|
||||
assert(path.size() >= 2);
|
||||
bool merged = false;
|
||||
if (path.size() >= 2) {
|
||||
const ClipperLib_Z::IntPoint &front = path.front();
|
||||
const ClipperLib_Z::IntPoint &back = path.back();
|
||||
const ClipperZUtils::ZPoint &front = path.front();
|
||||
const ClipperZUtils::ZPoint &back = path.back();
|
||||
// The path before clipping was supposed to cross the clipping boundary or be fully out of it.
|
||||
// Thus the clipped contour is supposed to become open, with one exception: The anchor expands into a closed hole.
|
||||
if (front.x() != back.x() || front.y() != back.y()) {
|
||||
// Look up the ends in "splits", possibly join the contours.
|
||||
// "splits" maps into the other piece connected to the same end point.
|
||||
auto find_end = [&splits](const ClipperLib_Z::IntPoint &pt) -> std::pair<ClipperLib_Z::IntPoint, int>* {
|
||||
auto find_end = [&splits](const ClipperZUtils::ZPoint &pt) -> std::pair<ClipperZUtils::ZPoint, int>* {
|
||||
auto it = std::lower_bound(splits.begin(), splits.end(), pt,
|
||||
[](const auto &l, const auto &r){ return ClipperZUtils::zpoint_lower(l.first, r); });
|
||||
return it != splits.end() && it->first == pt ? &(*it) : nullptr;
|
||||
@@ -136,7 +145,7 @@ static inline void merge_splits(ClipperLib_Z::Paths &paths, std::vector<std::pai
|
||||
end->second = int(it_path - paths.begin());
|
||||
} else {
|
||||
// Open end was found and matched with end->second
|
||||
ClipperLib_Z::Path &other_path = paths[end->second];
|
||||
ClipperZUtils::ZPath &other_path = paths[end->second];
|
||||
polylines_merge(other_path, other_path.front() == end->first, std::move(path), end_front);
|
||||
if (std::next(it_path) == paths.end()) {
|
||||
paths.pop_back();
|
||||
@@ -212,36 +221,29 @@ std::vector<WaveSeed> wave_seeds(
|
||||
using Intersection = ClipperZUtils::ClipperZIntersectionVisitor::Intersection;
|
||||
using Intersections = ClipperZUtils::ClipperZIntersectionVisitor::Intersections;
|
||||
|
||||
ClipperLib_Z::Paths segments;
|
||||
Intersections intersections;
|
||||
ClipperZUtils::ZPaths segments;
|
||||
Intersections intersections;
|
||||
|
||||
coord_t idx_boundary_begin = 1;
|
||||
coord_t idx_boundary_end = idx_boundary_begin;
|
||||
coord_t idx_src_end;
|
||||
coord_t idx_boundary_begin = 1;
|
||||
coord_t idx_boundary_end = idx_boundary_begin;
|
||||
coord_t idx_src_end;
|
||||
|
||||
{
|
||||
ClipperLib_Z::Clipper zclipper;
|
||||
ClipperZUtils::ClipperZIntersectionVisitor visitor(intersections);
|
||||
zclipper.ZFillFunction(visitor.clipper_callback());
|
||||
// as closed contours
|
||||
zclipper.AddPaths(ClipperZUtils::expolygons_to_zpaths(boundary, idx_boundary_end), ClipperLib_Z::ptClip, true);
|
||||
ClipperZUtils::ZPaths zboundary = ClipperZUtils::expolygons_to_zpaths(boundary, idx_boundary_end);
|
||||
// as open contours
|
||||
std::vector<std::pair<ClipperLib_Z::IntPoint, int>> zsrc_splits;
|
||||
{
|
||||
idx_src_end = idx_boundary_end;
|
||||
ClipperLib_Z::Paths zsrc = expolygons_to_zpaths_expanded_opened(src, tiny_expansion, idx_src_end);
|
||||
zclipper.AddPaths(zsrc, ClipperLib_Z::ptSubject, false);
|
||||
zsrc_splits.reserve(zsrc.size());
|
||||
for (const ClipperLib_Z::Path &path : zsrc) {
|
||||
assert(path.size() >= 2);
|
||||
assert(path.front() == path.back());
|
||||
zsrc_splits.emplace_back(path.front(), -1);
|
||||
}
|
||||
std::sort(zsrc_splits.begin(), zsrc_splits.end(), [](const auto &l, const auto &r){ return ClipperZUtils::zpoint_lower(l.first, r.first); });
|
||||
std::vector<std::pair<ClipperZUtils::ZPoint, int>> zsrc_splits;
|
||||
idx_src_end = idx_boundary_end;
|
||||
ClipperZUtils::ZPaths zsrc = expolygons_to_zpaths_expanded_opened(src, tiny_expansion, idx_src_end);
|
||||
zsrc_splits.reserve(zsrc.size());
|
||||
for (const ClipperZUtils::ZPath &path : zsrc) {
|
||||
assert(path.size() >= 2);
|
||||
assert(path.front() == path.back());
|
||||
zsrc_splits.emplace_back(path.front(), -1);
|
||||
}
|
||||
ClipperLib_Z::PolyTree polytree;
|
||||
zclipper.Execute(ClipperLib_Z::ctIntersection, polytree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero);
|
||||
ClipperLib_Z::PolyTreeToPaths(std::move(polytree), segments);
|
||||
std::sort(zsrc_splits.begin(), zsrc_splits.end(), [](const auto &l, const auto &r){ return ClipperZUtils::zpoint_lower(l.first, r.first); });
|
||||
segments = ClipperZUtils::clip_zpaths(ctIntersection, zsrc, true, zboundary, visitor.clipper_callback());
|
||||
merge_splits(segments, zsrc_splits);
|
||||
}
|
||||
|
||||
@@ -256,10 +258,10 @@ std::vector<WaveSeed> wave_seeds(
|
||||
WaveSeeds out;
|
||||
out.reserve(segments.size());
|
||||
int iseed = 0;
|
||||
for (const ClipperLib_Z::Path &path : segments) {
|
||||
for (const ClipperZUtils::ZPath &path : segments) {
|
||||
assert(path.size() >= 2);
|
||||
ClipperLib_Z::IntPoint front = path.front();
|
||||
ClipperLib_Z::IntPoint back = path.back();
|
||||
ClipperZUtils::ZPoint front = path.front();
|
||||
ClipperZUtils::ZPoint back = path.back();
|
||||
// Both ends of a seed segment are supposed to be inside a single boundary expolygon.
|
||||
// Thus as long as the seed contour is not closed, it should be open at a boundary point.
|
||||
assert((front == back && front.z() >= idx_boundary_end && front.z() < idx_src_end) ||
|
||||
@@ -280,7 +282,7 @@ std::vector<WaveSeed> wave_seeds(
|
||||
// boundary ID is not directly available).
|
||||
coord_t src_z = -1, boundary_z = -1;
|
||||
// Scan all path points for the information we need.
|
||||
for (const ClipperLib_Z::IntPoint &point : path) {
|
||||
for (const ClipperZUtils::ZPoint &point : path) {
|
||||
if (point.z() >= idx_boundary_end && point.z() < idx_src_end && src_z < 0)
|
||||
src_z = point.z();
|
||||
else if (point.z() >= idx_boundary_begin && point.z() < idx_boundary_end && boundary_z < 0)
|
||||
@@ -311,7 +313,7 @@ std::vector<WaveSeed> wave_seeds(
|
||||
// See https://github.com/prusa3d/PrusaSlicer/issues/12469.
|
||||
// Segement is open, yet its first point seems to be part of boundary polygon.
|
||||
// Take the first point with src polygon index.
|
||||
for (const ClipperLib_Z::IntPoint &point : path) {
|
||||
for (const ClipperZUtils::ZPoint &point : path) {
|
||||
if (point.z() >= idx_boundary_end) {
|
||||
front = point;
|
||||
back = point;
|
||||
@@ -360,17 +362,13 @@ std::vector<WaveSeed> wave_seeds(
|
||||
return out;
|
||||
}
|
||||
|
||||
static ClipperLib::Paths wavefront_initial(ClipperLib::ClipperOffset &co, const ClipperLib::Paths &polylines, float offset)
|
||||
static Polygons wavefront_initial(const VecOfPoints &polylines, float offset, double arc_tolerance)
|
||||
{
|
||||
ClipperLib::Paths out;
|
||||
Polygons out;
|
||||
out.reserve(polylines.size());
|
||||
ClipperLib::Paths out_this;
|
||||
for (const ClipperLib::Path &path : polylines) {
|
||||
for (const Points &path : polylines) {
|
||||
assert(path.size() >= 2);
|
||||
co.Clear();
|
||||
co.AddPath(path, jtRound, path.front() == path.back() ? ClipperLib::etClosedLine : ClipperLib::etOpenRound);
|
||||
co.Execute(out_this, offset);
|
||||
append(out, std::move(out_this));
|
||||
append(out, Slic3r::offset(Polyline(path), offset, jtRound, arc_tolerance, path.front() == path.back() ? etClosedLine : etOpenRound));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -378,43 +376,24 @@ static ClipperLib::Paths wavefront_initial(ClipperLib::ClipperOffset &co, const
|
||||
// Input polygons may consist of multiple expolygons, even nested expolygons.
|
||||
// After inflation some polygons may thus overlap, however the overlap is being resolved during the successive
|
||||
// clipping operation, thus it is not being done here.
|
||||
static ClipperLib::Paths wavefront_step(ClipperLib::ClipperOffset &co, const ClipperLib::Paths &polygons, float offset)
|
||||
static Polygons wavefront_step(const Polygons &polygons, float offset, double arc_tolerance)
|
||||
{
|
||||
ClipperLib::Paths out;
|
||||
Polygons out;
|
||||
out.reserve(polygons.size());
|
||||
ClipperLib::Paths out_this;
|
||||
for (const ClipperLib::Path &polygon : polygons) {
|
||||
co.Clear();
|
||||
// Execute reorients the contours so that the outer most contour has a positive area. Thus the output
|
||||
// contours will be CCW oriented even though the input paths are CW oriented.
|
||||
// Offset is applied after contour reorientation, thus the signum of the offset value is reversed.
|
||||
co.AddPath(polygon, jtRound, ClipperLib::etClosedPolygon);
|
||||
bool ccw = ClipperLib::Orientation(polygon);
|
||||
co.Execute(out_this, ccw ? offset : - offset);
|
||||
if (! ccw) {
|
||||
// Reverse the resulting contours.
|
||||
for (ClipperLib::Path &path : out_this)
|
||||
std::reverse(path.begin(), path.end());
|
||||
}
|
||||
append(out, std::move(out_this));
|
||||
}
|
||||
for (const Polygon &polygon : polygons)
|
||||
// CCW contours grow, CW holes shrink.
|
||||
append(out, Slic3r::offset(polygon, offset, jtRound, arc_tolerance));
|
||||
return out;
|
||||
}
|
||||
|
||||
static ClipperLib::Paths wavefront_clip(const ClipperLib::Paths &wavefront, const Polygons &clipping)
|
||||
static Polygons wavefront_clip(const Polygons &wavefront, const Polygons &clipping)
|
||||
{
|
||||
ClipperLib::Clipper clipper;
|
||||
clipper.AddPaths(wavefront, ClipperLib::ptSubject, true);
|
||||
clipper.AddPaths(ClipperUtils::PolygonsProvider(clipping), ClipperLib::ptClip, true);
|
||||
ClipperLib::Paths out;
|
||||
clipper.Execute(ClipperLib::ctIntersection, out, ClipperLib::pftPositive, ClipperLib::pftPositive);
|
||||
return out;
|
||||
return intersection(wavefront, clipping, pftPositive);
|
||||
}
|
||||
|
||||
static Polygons propagate_wave_from_boundary(
|
||||
ClipperLib::ClipperOffset &co,
|
||||
// Seed of the wave: Open polylines very close to the boundary.
|
||||
const ClipperLib::Paths &seed,
|
||||
const VecOfPoints &seed,
|
||||
// Boundary inside which the waveform will propagate.
|
||||
const ExPolygon &boundary,
|
||||
// How much to inflate the seed lines to produce the first wave area.
|
||||
@@ -425,25 +404,24 @@ static Polygons propagate_wave_from_boundary(
|
||||
const size_t num_other_steps,
|
||||
// Maximum inflation of seed contours over the boundary. Used to trim boundary to speed up
|
||||
// clipping during wave propagation.
|
||||
const float max_inflation)
|
||||
const float max_inflation,
|
||||
// Accuracy of the round offsets.
|
||||
const double arc_tolerance)
|
||||
{
|
||||
assert(! seed.empty() && seed.front().size() >= 2);
|
||||
Polygons clipping = ClipperUtils::clip_clipper_polygons_with_subject_bbox(boundary, get_extents<true>(seed).inflated(max_inflation));
|
||||
ClipperLib::Paths polygons = wavefront_clip(wavefront_initial(co, seed, initial_step), clipping);
|
||||
Polygons polygons = wavefront_clip(wavefront_initial(seed, initial_step, arc_tolerance), clipping);
|
||||
// Now offset the remaining
|
||||
for (size_t ioffset = 0; ioffset < num_other_steps; ++ ioffset)
|
||||
polygons = wavefront_clip(wavefront_step(co, polygons, other_step), clipping);
|
||||
return to_polygons(polygons);
|
||||
polygons = wavefront_clip(wavefront_step(polygons, other_step, arc_tolerance), clipping);
|
||||
return polygons;
|
||||
}
|
||||
|
||||
// Resulting regions are sorted by boundary id and source id.
|
||||
std::vector<RegionExpansion> propagate_waves(const WaveSeeds &seeds, const ExPolygons &boundary, const RegionExpansionParameters ¶ms)
|
||||
{
|
||||
std::vector<RegionExpansion> out;
|
||||
ClipperLib::Paths paths;
|
||||
ClipperLib::ClipperOffset co;
|
||||
co.ArcTolerance = params.arc_tolerance;
|
||||
co.ShortestEdgeLength = params.shortest_edge_length;
|
||||
VecOfPoints paths;
|
||||
for (auto it_seed = seeds.begin(); it_seed != seeds.end();) {
|
||||
auto it = it_seed;
|
||||
paths.clear();
|
||||
@@ -452,7 +430,7 @@ std::vector<RegionExpansion> propagate_waves(const WaveSeeds &seeds, const ExPol
|
||||
// Propagate the wavefront while clipping it with the trimmed boundary.
|
||||
// Collect the expanded polygons, merge them with the source polygons.
|
||||
RegionExpansion re;
|
||||
for (Polygon &polygon : propagate_wave_from_boundary(co, paths, boundary[it_seed->boundary], params.initial_step, params.other_step, params.num_other_steps, params.max_inflation))
|
||||
for (Polygon &polygon : propagate_wave_from_boundary(paths, boundary[it_seed->boundary], params.initial_step, params.other_step, params.num_other_steps, params.max_inflation, params.arc_tolerance))
|
||||
out.push_back({ std::move(polygon), it_seed->src, it_seed->boundary });
|
||||
it_seed = it;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#ifndef SRC_LIBSLIC3R_ALGORITHM_REGION_EXPANSION_HPP_
|
||||
#define SRC_LIBSLIC3R_ALGORITHM_REGION_EXPANSION_HPP_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <libslic3r/Point.hpp>
|
||||
#include <libslic3r/Polygon.hpp>
|
||||
#include <libslic3r/ExPolygon.hpp>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace Algorithm {
|
||||
@@ -25,7 +27,6 @@ struct RegionExpansionParameters
|
||||
|
||||
// Accuracy of the offsetter for wave propagation.
|
||||
double arc_tolerance;
|
||||
double shortest_edge_length;
|
||||
|
||||
static RegionExpansionParameters build(
|
||||
// Scaled expansion value
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef ANYPTR_HPP
|
||||
#define ANYPTR_HPP
|
||||
|
||||
#include <boost/variant/variant.hpp>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <boost/variant.hpp>
|
||||
|
||||
+78
-68
@@ -1,3 +1,8 @@
|
||||
#include "Technologies.hpp"
|
||||
#include "Config.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "calib.hpp"
|
||||
#include "Semver.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/Format/DRC.hpp"
|
||||
@@ -5,12 +10,22 @@
|
||||
//BBS
|
||||
#include "Preset.hpp"
|
||||
#include "Exception.hpp"
|
||||
#include "InstanceLock.hpp"
|
||||
#include "LocalesUtils.hpp"
|
||||
#include "Thread.hpp"
|
||||
#include "format.hpp"
|
||||
#include "libslic3r_version.h"
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <exception>
|
||||
#include <boost/none.hpp>
|
||||
#include <cstddef>
|
||||
#include <system_error>
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <stdexcept>
|
||||
@@ -27,6 +42,9 @@
|
||||
#include <boost/uuid/uuid.hpp>
|
||||
#include <boost/uuid/uuid_generators.hpp>
|
||||
#include <boost/uuid/uuid_io.hpp>
|
||||
#include <cassert>
|
||||
#include <iterator>
|
||||
#include <string_view>
|
||||
|
||||
#ifdef WIN32
|
||||
//FIXME replace the two following includes with <boost/md5.hpp> after it becomes mainstream.
|
||||
@@ -303,6 +321,9 @@ void AppConfig::set_defaults()
|
||||
if (get(SETTING_OPENGL_SHOW_FPS_OVERLAY).empty())
|
||||
set_bool(SETTING_OPENGL_SHOW_FPS_OVERLAY, false);
|
||||
|
||||
if (get(SETTING_OPENGL_SHOW_RENDER_TIMINGS).empty())
|
||||
set_bool(SETTING_OPENGL_SHOW_RENDER_TIMINGS, false);
|
||||
|
||||
if (get(SETTING_OPENGL_REALISTIC_MODE).empty())
|
||||
set_bool(SETTING_OPENGL_REALISTIC_MODE, false);
|
||||
|
||||
@@ -315,8 +336,9 @@ void AppConfig::set_defaults()
|
||||
if (get(SETTING_OPENGL_SHADING_MODEL).empty())
|
||||
set(SETTING_OPENGL_SHADING_MODEL, "gouraud");
|
||||
|
||||
if (get(SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS).empty())
|
||||
set_bool(SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS, false);
|
||||
// Replaces the on/off setting, whose shadows turned with the camera.
|
||||
if (get(SETTING_OPENGL_REALISTIC_SHADOWS).empty())
|
||||
set(SETTING_OPENGL_REALISTIC_SHADOWS, get_bool(SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS) ? "orbit" : "off");
|
||||
|
||||
if (get(SETTING_OPENGL_PHONG_SMOOTH_NORMALS).empty())
|
||||
set_bool(SETTING_OPENGL_PHONG_SMOOTH_NORMALS, false);
|
||||
@@ -348,6 +370,10 @@ void AppConfig::set_defaults()
|
||||
// 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);
|
||||
|
||||
// Design tab: draw the printer bed and its plate grid (the Feature tree's Bed row).
|
||||
if (get("design_show_bed").empty())
|
||||
set_bool("design_show_bed", true);
|
||||
#endif
|
||||
|
||||
//#ifdef SUPPORT_SHOW_HINTS
|
||||
@@ -385,6 +411,9 @@ void AppConfig::set_defaults()
|
||||
if (get("show_overhang").empty())
|
||||
set_bool("show_overhang", false);
|
||||
|
||||
if (get("show_center_of_mass").empty())
|
||||
set_bool("show_center_of_mass", false);
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
//#ifdef SUPPORT_3D_CONNEXION
|
||||
@@ -743,10 +772,13 @@ static bool verify_config_file_checksum(boost::nowide::ifstream &ifs)
|
||||
|
||||
|
||||
#ifdef USE_JSON_CONFIG
|
||||
std::string AppConfig::load()
|
||||
std::string AppConfig::load(bool read_only)
|
||||
{
|
||||
json j;
|
||||
|
||||
// Keep another instance from replacing or restoring the file mid-read.
|
||||
InstanceLock instance_lock(read_only ? std::string() : lock_path());
|
||||
|
||||
// 1) Read the complete config file into a boost::property_tree.
|
||||
namespace pt = boost::property_tree;
|
||||
pt::ptree tree;
|
||||
@@ -996,7 +1028,6 @@ void AppConfig::save()
|
||||
// The config is first written to a file with a PID suffix and then moved
|
||||
// to avoid race conditions with multiple instances of Slic3r
|
||||
const auto path = config_path();
|
||||
std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
|
||||
|
||||
json j;
|
||||
|
||||
@@ -1126,43 +1157,18 @@ void AppConfig::save()
|
||||
|
||||
j["local_machines"][local_machine.first] = m_json;
|
||||
}
|
||||
boost::nowide::ofstream c;
|
||||
c.open(path_pid, std::ios::out | std::ios::trunc);
|
||||
c << j.dump(1, '\t') << std::endl;
|
||||
|
||||
#ifdef WIN32
|
||||
// WIN32 specific: The final "rename_file()" call is not safe in case of an application crash, there is no atomic "rename file" API
|
||||
// provided by Windows (sic!). Therefore we save a MD5 checksum to be able to verify file corruption. In addition,
|
||||
// we save the config file into a backup first before moving it to the final destination.
|
||||
c << appconfig_md5_hash_line(j.dump(1, '\t'));
|
||||
#endif
|
||||
|
||||
c.close();
|
||||
if (c.fail()) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to write new configuration to " << path_pid << "; aborting attempt to overwrite original configuration";
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef WIN32
|
||||
// Make a backup of the configuration file before copying it to the final destination.
|
||||
std::string error_message;
|
||||
std::string backup_path = (boost::format("%1%.bak") % path).str();
|
||||
// Copy configuration file with PID suffix into the configuration file with "bak" suffix.
|
||||
if (copy_file(path_pid, backup_path, error_message, false) != SUCCESS)
|
||||
BOOST_LOG_TRIVIAL(error) << "Copying from " << path_pid << " to " << backup_path << " failed. Failed to create a backup configuration.";
|
||||
#endif
|
||||
|
||||
// Rename the config atomically.
|
||||
// On Windows, the rename is likely NOT atomic, thus it may fail if PrusaSlicer crashes on another thread in the meanwhile.
|
||||
// To cope with that, we already made a backup of the config on Windows.
|
||||
rename_file(path_pid, path);
|
||||
m_dirty = false;
|
||||
const std::string config_str = j.dump(1, '\t');
|
||||
if (write_config_file(path, config_str + "\n", config_str))
|
||||
m_dirty = false;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
std::string AppConfig::load()
|
||||
std::string AppConfig::load(bool read_only)
|
||||
{
|
||||
// Keep another instance from replacing or restoring the file mid-read.
|
||||
InstanceLock instance_lock(read_only ? std::string() : lock_path());
|
||||
|
||||
// 1) Read the complete config file into a boost::property_tree.
|
||||
namespace pt = boost::property_tree;
|
||||
pt::ptree tree;
|
||||
@@ -1300,7 +1306,6 @@ void AppConfig::save()
|
||||
// The config is first written to a file with a PID suffix and then moved
|
||||
// to avoid race conditions with multiple instances of Slic3r
|
||||
const auto path = config_path();
|
||||
std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
|
||||
|
||||
std::stringstream config_ss;
|
||||
if (m_mode == EAppMode::Editor)
|
||||
@@ -1336,39 +1341,39 @@ void AppConfig::save()
|
||||
// One empty line before the MD5 sum.
|
||||
config_ss << std::endl;
|
||||
|
||||
std::string config_str = config_ss.str();
|
||||
boost::nowide::ofstream c;
|
||||
c.open(path_pid, std::ios::out | std::ios::trunc);
|
||||
c << config_str;
|
||||
#ifdef WIN32
|
||||
// WIN32 specific: The final "rename_file()" call is not safe in case of an application crash, there is no atomic "rename file" API
|
||||
// provided by Windows (sic!). Therefore we save a MD5 checksum to be able to verify file corruption. In addition,
|
||||
// we save the config file into a backup first before moving it to the final destination.
|
||||
c << appconfig_md5_hash_line(config_str);
|
||||
#endif
|
||||
c.close();
|
||||
if (c.fail()) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to write new configuration to " << path_pid << "; aborting attempt to overwrite original configuration";
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef WIN32
|
||||
// Make a backup of the configuration file before copying it to the final destination.
|
||||
std::string error_message;
|
||||
std::string backup_path = (boost::format("%1%.bak") % path).str();
|
||||
// Copy configuration file with PID suffix into the configuration file with "bak" suffix.
|
||||
if (copy_file(path_pid, backup_path, error_message, false) != SUCCESS)
|
||||
BOOST_LOG_TRIVIAL(error) << "Copying from " << path_pid << " to " << backup_path << " failed. Failed to create a backup configuration.";
|
||||
#endif
|
||||
|
||||
// Rename the config atomically.
|
||||
// On Windows, the rename is likely NOT atomic, thus it may fail if PrusaSlicer crashes on another thread in the meanwhile.
|
||||
// To cope with that, we already made a backup of the config on Windows.
|
||||
rename_file(path_pid, path);
|
||||
m_dirty = false;
|
||||
const std::string config_str = config_ss.str();
|
||||
if (write_config_file(path, config_str, config_str))
|
||||
m_dirty = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool AppConfig::write_config_file(const std::string &path, std::string body, const std::string &checksum_source)
|
||||
{
|
||||
// Everything before this is assembly; only the writes need the other instances kept out.
|
||||
InstanceLock instance_lock(lock_path());
|
||||
#ifdef WIN32
|
||||
// WIN32 specific: the final replace is not safe in case of an application crash, there is no atomic "rename file" API
|
||||
// provided by Windows (sic!). Therefore we save a MD5 checksum to be able to verify file corruption. In addition,
|
||||
// we save the config file into a backup first before moving it to the final destination.
|
||||
body += appconfig_md5_hash_line(checksum_source);
|
||||
#endif
|
||||
// Not flushed to the device: the idle handler saves on the GUI thread after
|
||||
// any change, and the rename already gives a complete old or new file.
|
||||
if (const std::error_code ec = write_file_atomically(path, body)) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to write the configuration " << path << ": " << ec.message() << "; trying again in 10 s";
|
||||
m_retry_save_at = std::chrono::steady_clock::now() + std::chrono::seconds(10);
|
||||
return false;
|
||||
}
|
||||
m_retry_save_at = {};
|
||||
#ifdef WIN32
|
||||
// Written after the config, so the backup never holds a state that was not confirmed written.
|
||||
const std::string backup_path = (boost::format("%1%.bak") % path).str();
|
||||
if (const std::error_code ec = write_file_atomically(backup_path, body))
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to write the backup configuration " << backup_path << ": " << ec.message();
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AppConfig::get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const
|
||||
{
|
||||
const auto it_v = m_vendors.find(vendor);
|
||||
@@ -1857,6 +1862,11 @@ void AppConfig::reset_selections()
|
||||
}
|
||||
}
|
||||
|
||||
std::string AppConfig::lock_path()
|
||||
{
|
||||
return Slic3r::data_dir().empty() ? std::string() : config_path() + ".lock";
|
||||
}
|
||||
|
||||
std::string AppConfig::config_path()
|
||||
{
|
||||
#ifdef USE_JSON_CONFIG
|
||||
@@ -1893,7 +1903,7 @@ bool AppConfig::exists()
|
||||
|
||||
std::string AppConfig::load_if_exists()
|
||||
{
|
||||
return boost::filesystem::exists(loading_path()) ? load() : std::string();
|
||||
return boost::filesystem::exists(loading_path()) ? load(/*read_only=*/true) : std::string();
|
||||
}
|
||||
|
||||
}; // namespace Slic3r
|
||||
|
||||
@@ -2,17 +2,19 @@
|
||||
#define slic3r_AppConfig_hpp_
|
||||
|
||||
#include <set>
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include "LocalesUtils.hpp"
|
||||
#include "nlohmann/json.hpp"
|
||||
#include <boost/algorithm/string/trim_all.hpp>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/Semver.hpp"
|
||||
#include "calib.hpp"
|
||||
|
||||
using namespace nlohmann;
|
||||
|
||||
#define ENV_DEV_HOST "0"
|
||||
#define ENV_QAT_HOST "1"
|
||||
#define ENV_PRE_HOST "2"
|
||||
@@ -36,10 +38,13 @@ using namespace nlohmann;
|
||||
#define SETTING_OPENGL_SCENE_CACHE "opengl_scene_cache"
|
||||
#define SETTING_OPENGL_SKIP_IDENTICAL_FRAMES "opengl_skip_identical_frames"
|
||||
#define SETTING_OPENGL_SHOW_FPS_OVERLAY "opengl_show_fps_overlay"
|
||||
#define SETTING_OPENGL_SHOW_RENDER_TIMINGS "opengl_show_render_timings"
|
||||
#define SETTING_OPENGL_REALISTIC_MODE "opengl_realistic_mode"
|
||||
#define SETTING_OPENGL_REALISTIC_PHONG "opengl_realistic_phong"
|
||||
#define SETTING_OPENGL_SHADING_MODEL "opengl_shading_model"
|
||||
#define SETTING_OPENGL_PHONG_BASIC_PLATE_SHADOWS "opengl_phong_basic_plate_shadows"
|
||||
// off, static (light fixed in the world) or orbit (light turning with the camera)
|
||||
#define SETTING_OPENGL_REALISTIC_SHADOWS "opengl_realistic_shadows"
|
||||
#define SETTING_OPENGL_PHONG_SSAO "opengl_phong_ssao"
|
||||
#define SETTING_OPENGL_PHONG_SMOOTH_NORMALS "opengl_phong_smooth_normals"
|
||||
#define SETTING_OPENGL_REALISTIC_PREVIEW "opengl_realistic_preview"
|
||||
@@ -122,14 +127,18 @@ public:
|
||||
|
||||
// Load the slic3r.ini from a user profile directory (or a datadir, if configured).
|
||||
// Return an error string, or an empty string on success.
|
||||
std::string load();
|
||||
std::string load(bool read_only = false);
|
||||
// Treat a missing config as default state; otherwise load it normally.
|
||||
// The CLI's load: it never saves, so it takes no lock and creates no lock file.
|
||||
std::string load_if_exists();
|
||||
// Store the slic3r.ini into a user profile directory (or a datadir, if configured).
|
||||
void save();
|
||||
|
||||
// Does this config need to be saved?
|
||||
bool dirty() const { return m_dirty; }
|
||||
// False for ten seconds after a failed write, so the idle handler does not
|
||||
// repeat a hopeless attempt on every event; an explicit save() always tries.
|
||||
bool save_due() const { return std::chrono::steady_clock::now() >= m_retry_save_at; }
|
||||
|
||||
|
||||
void set_dirty() { m_dirty = true; }
|
||||
@@ -152,7 +161,7 @@ public:
|
||||
std::string get(const std::string &key) const
|
||||
{ std::string value; this->get("app", key, value); return value; }
|
||||
bool get_bool(const std::string §ion, const std::string &key) const
|
||||
{ return this->get(section, key) == "true" || this->get(key) == "1"; }
|
||||
{ const std::string value = this->get(section, key); return value == "true" || value == "1"; }
|
||||
bool get_bool(const std::string &key) const
|
||||
{ return this->get_bool("app", key); }
|
||||
void set(const std::string §ion, const std::string &key, const std::string &value)
|
||||
@@ -189,6 +198,9 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void set(const std::string& section, const std::string& key, const char* value)
|
||||
{ this->set(section, key, std::string(value)); }
|
||||
|
||||
void set(const std::string& section, const std::string &key, bool value)
|
||||
{
|
||||
if (value){
|
||||
@@ -339,6 +351,8 @@ public:
|
||||
|
||||
// Get the default config path from Slic3r::data_dir().
|
||||
std::string config_path();
|
||||
// Lock file guarding config_path() against other running instances; empty without a data dir.
|
||||
std::string lock_path();
|
||||
|
||||
// Returns true if the user's data directory comes from before Slic3r 1.40.0 (no updating)
|
||||
bool legacy_datadir() const { return m_legacy_datadir; }
|
||||
@@ -449,8 +463,16 @@ private:
|
||||
|
||||
// Preset for each machine
|
||||
MachineSettingMap m_printer_settings;
|
||||
// Writes the assembled config text, and on Windows its checksum and a backup copy; false when the
|
||||
// config itself could not be written, in which case the caller stays dirty and retries. `checksum_source`
|
||||
// is the text load() will verify, which for the JSON config ends before the trailing newline.
|
||||
bool write_config_file(const std::string &path, std::string body, const std::string &checksum_source);
|
||||
|
||||
// Has any value been modified since the config.ini has been last saved or loaded?
|
||||
bool m_dirty;
|
||||
// After a failed write, save_due() is false for the next ten seconds, so the
|
||||
// idle handler does not repeat a hopeless write on every event.
|
||||
std::chrono::steady_clock::time_point m_retry_save_at{};
|
||||
// Original version found in the ini file before it was overwritten
|
||||
Semver m_orig_version;
|
||||
// Whether the existing version is before system profiles & configuration updating
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#include "BeadingStrategy.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "DistributedBeadingStrategy.hpp"
|
||||
#include "RedistributeBeadingStrategy.hpp"
|
||||
#include "OuterWallInsetBeadingStrategy.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp"
|
||||
|
||||
namespace Slic3r::Arachne {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <cassert>
|
||||
|
||||
#include "DistributedBeadingStrategy.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <cstddef>
|
||||
|
||||
#include "LimitedBeadingStrategy.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp"
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <numeric>
|
||||
#include <utility>
|
||||
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
#include "WideningBeadingStrategy.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
|
||||
@@ -10,16 +10,26 @@
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <math.h>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <cassert>
|
||||
#include <cstdlib>
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp"
|
||||
#include "libslic3r/Geometry/VoronoiUtils.hpp"
|
||||
#include "ankerl/unordered_dense.h"
|
||||
#include "libslic3r/Arachne/SkeletalTrapezoidationEdge.hpp"
|
||||
#include "libslic3r/Arachne/SkeletalTrapezoidationJoint.hpp"
|
||||
#include "libslic3r/Arachne/utils/ExtrusionJunction.hpp"
|
||||
#include "libslic3r/Arachne/utils/ExtrusionLine.hpp"
|
||||
#include "libslic3r/Arachne/SkeletalTrapezoidationGraph.hpp"
|
||||
#include "libslic3r/Arachne/utils/PolygonsSegmentIndex.hpp"
|
||||
|
||||
#ifndef NDEBUG
|
||||
#include "libslic3r/EdgeGrid.hpp"
|
||||
@@ -456,8 +466,6 @@ void SkeletalTrapezoidation::constructFromPolygons(const Polygons& polys)
|
||||
edge.from->incident_edge = &edge;
|
||||
}
|
||||
|
||||
using NodeSet = SkeletalTrapezoidation::NodeSet;
|
||||
|
||||
void SkeletalTrapezoidation::separatePointyQuadEndNodes()
|
||||
{
|
||||
NodeSet visited_nodes;
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <list>
|
||||
#include <vector>
|
||||
|
||||
#include "utils/HalfEdgeGraph.hpp"
|
||||
#include "utils/PolygonsSegmentIndex.hpp"
|
||||
#include "utils/ExtrusionJunction.hpp"
|
||||
#include "utils/ExtrusionLine.hpp"
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
#ifndef SKELETAL_TRAPEZOIDATION_EDGE_H
|
||||
#define SKELETAL_TRAPEZOIDATION_EDGE_H
|
||||
|
||||
#include <cassert>
|
||||
#include <memory> // smart pointers
|
||||
#include <list>
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "utils/ExtrusionJunction.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
|
||||
@@ -6,11 +6,17 @@
|
||||
#include <ankerl/unordered_dense.h>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <cassert>
|
||||
#include <cinttypes>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
#include "../Line.hpp"
|
||||
#include "libslic3r/Arachne/utils/HalfEdge.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Arachne/utils/HalfEdgeNode.hpp"
|
||||
#include "libslic3r/Arachne/SkeletalTrapezoidationEdge.hpp"
|
||||
#include "libslic3r/Arachne/SkeletalTrapezoidationJoint.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <memory> // smart pointers
|
||||
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
|
||||
@@ -2,12 +2,31 @@
|
||||
// CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#include <algorithm> //For std::partition_copy and std::min_element.
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <math.h>
|
||||
#include <limits>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "WallToolPaths.hpp"
|
||||
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/Arachne/utils/ExtrusionLine.hpp"
|
||||
#include "libslic3r/Line.hpp"
|
||||
#include "libslic3r/Arachne/utils/PolygonsPointIndex.hpp"
|
||||
#include "libslic3r/Flow.hpp"
|
||||
#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.hpp"
|
||||
#include "libslic3r/Arachne/utils/ExtrusionJunction.hpp"
|
||||
#include "libslic3r/Arachne/utils/SparsePointGrid.hpp"
|
||||
#include "libslic3r/Arachne/utils/SquareGrid.hpp"
|
||||
#include "SkeletalTrapezoidation.hpp"
|
||||
#include "../ClipperUtils.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "utils/linearAlg2D.hpp"
|
||||
#include "EdgeGrid.hpp"
|
||||
#include "utils/SparseLineGrid.hpp"
|
||||
@@ -17,6 +36,8 @@
|
||||
#include "Utils.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
//#define ARACHNE_STITCH_PATCH_DEBUG
|
||||
|
||||
@@ -263,10 +284,8 @@ std::unique_ptr<LocToLineGrid> cre
|
||||
*/
|
||||
void fixSelfIntersections(const coord_t epsilon, Polygons &thiss)
|
||||
{
|
||||
if (epsilon < 1) {
|
||||
ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(thiss), ClipperLib::pftEvenOdd);
|
||||
if (epsilon < 1)
|
||||
return;
|
||||
}
|
||||
|
||||
const int64_t half_epsilon = (epsilon + 1) / 2;
|
||||
|
||||
@@ -304,8 +323,6 @@ void fixSelfIntersections(const coord_t epsilon, Polygons &thiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(thiss), ClipperLib::pftEvenOdd);
|
||||
}
|
||||
|
||||
/*!
|
||||
@@ -352,18 +369,11 @@ void removeDegenerateVerts(Polygons &thiss)
|
||||
|
||||
void removeSmallAreas(Polygons &thiss, const double min_area_size, const bool remove_holes)
|
||||
{
|
||||
auto to_path = [](const Polygon &poly) -> ClipperLib::Path {
|
||||
ClipperLib::Path out;
|
||||
for (const Point &pt : poly.points)
|
||||
out.emplace_back(ClipperLib::cInt(pt.x()), ClipperLib::cInt(pt.y()));
|
||||
return out;
|
||||
};
|
||||
|
||||
auto new_end = thiss.end();
|
||||
if (remove_holes) {
|
||||
for (auto it = thiss.begin(); it < new_end;) {
|
||||
// All polygons smaller than target are removed by replacing them with a polygon from the back of the vector.
|
||||
if (fabs(ClipperLib::Area(to_path(*it))) < min_area_size) {
|
||||
if (fabs(it->area()) < min_area_size) {
|
||||
--new_end;
|
||||
*it = std::move(*new_end);
|
||||
continue; // Don't increment the iterator such that the polygon just swapped in is checked next.
|
||||
@@ -374,7 +384,7 @@ void removeSmallAreas(Polygons &thiss, const double min_area_size, const bool re
|
||||
// For each polygon, computes the signed area, move small outlines at the end of the vector and keep pointer on small holes
|
||||
Polygons small_holes;
|
||||
for (auto it = thiss.begin(); it < new_end;) {
|
||||
if (double area = ClipperLib::Area(to_path(*it)); fabs(area) < min_area_size) {
|
||||
if (double area = it->area(); fabs(area) < min_area_size) {
|
||||
if (area >= 0) {
|
||||
--new_end;
|
||||
if (it < new_end) {
|
||||
@@ -773,7 +783,7 @@ void WallToolPaths::separateOutInnerContour()
|
||||
//To get a correct shape, we need to make the outside contour positive and any holes inside negative.
|
||||
//This can be done by applying the even-odd rule to the shape. This rule is not sensitive to the winding order of the polygon.
|
||||
//The even-odd rule would be incorrect if the polygon self-intersects, but that should never be generated by the skeletal trapezoidation.
|
||||
inner_contour = union_(inner_contour, ClipperLib::PolyFillType::pftEvenOdd);
|
||||
inner_contour = union_(inner_contour, pftEvenOdd);
|
||||
}
|
||||
|
||||
const Polygons& WallToolPaths::getInnerContour()
|
||||
|
||||
@@ -4,10 +4,15 @@
|
||||
#ifndef CURAENGINE_WALLTOOLPATHS_H
|
||||
#define CURAENGINE_WALLTOOLPATHS_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <boost/container_hash/hash.hpp>
|
||||
#include <memory>
|
||||
#include <ankerl/unordered_dense.h>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
|
||||
#include "BeadingStrategy/BeadingStrategyFactory.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "utils/ExtrusionLine.hpp"
|
||||
#include "../Polygon.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
#define UTILS_EXTRUSION_JUNCTION_H
|
||||
|
||||
#include "../../Point.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <vector>
|
||||
|
||||
#include "ExtrusionLine.hpp"
|
||||
#include "../../VariableWidth.hpp"
|
||||
#include "libslic3r/ClipperZUtils.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Arachne/utils/ExtrusionJunction.hpp"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/ExtrusionEntity.hpp"
|
||||
#include "libslic3r/Line.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
@@ -287,9 +292,9 @@ double ExtrusionLine::area() const
|
||||
} // namespace Slic3r::Arachne
|
||||
|
||||
namespace Slic3r {
|
||||
void extrusion_paths_append(ExtrusionPaths &dst, const ClipperLib_Z::Paths &extrusion_paths, const ExtrusionRole role, const Flow &flow)
|
||||
void extrusion_paths_append(ExtrusionPaths &dst, const ClipperZUtils::ZPaths &extrusion_paths, const ExtrusionRole role, const Flow &flow)
|
||||
{
|
||||
for (const ClipperLib_Z::Path &extrusion_path : extrusion_paths) {
|
||||
for (const ClipperZUtils::ZPath &extrusion_path : extrusion_paths) {
|
||||
ThickPolyline thick_polyline = Arachne::to_thick_polyline(extrusion_path);
|
||||
Slic3r::append(dst, thick_polyline_to_multi_path(thick_polyline, role, flow, scaled<float>(0.05), float(SCALED_EPSILON)).paths);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#ifndef UTILS_EXTRUSION_LINE_H
|
||||
#define UTILS_EXTRUSION_LINE_H
|
||||
|
||||
#include <clipper/clipper_z.hpp>
|
||||
#include "../../ClipperZUtils.hpp"
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
@@ -19,9 +19,8 @@
|
||||
#include "ExtrusionJunction.hpp"
|
||||
#include "../../Polyline.hpp"
|
||||
#include "../../Polygon.hpp"
|
||||
#include "../../BoundingBox.hpp"
|
||||
#include "../../ExtrusionEntity.hpp"
|
||||
#include "../../Flow.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Point.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -294,7 +293,7 @@ using VariableWidthLines = std::vector<ExtrusionLine>; //<! The ExtrusionLines g
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void extrusion_paths_append(ExtrusionPaths &dst, const ClipperLib_Z::Paths &extrusion_paths, const ExtrusionRole role, const Flow &flow);
|
||||
void extrusion_paths_append(ExtrusionPaths &dst, const ClipperZUtils::ZPaths &extrusion_paths, const ExtrusionRole role, const Flow &flow);
|
||||
void extrusion_paths_append(ExtrusionPaths &dst, const Arachne::ExtrusionLine &extrusion, const ExtrusionRole role, const Flow &flow);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
#ifndef UTILS_POLYGONS_POINT_INDEX_H
|
||||
#define UTILS_POLYGONS_POINT_INDEX_H
|
||||
|
||||
#include <utility>
|
||||
#include <functional>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
#include "../../Point.hpp"
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "PolygonsPointIndex.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "PolylineStitcher.hpp"
|
||||
|
||||
#include "ExtrusionLine.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Arachne/utils/PolygonsPointIndex.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
#include "SparsePointGrid.hpp"
|
||||
#include "PolygonsPointIndex.hpp"
|
||||
#include "../../Polygon.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
|
||||
@@ -6,11 +6,14 @@
|
||||
#define UTILS_SPARSE_GRID_H
|
||||
|
||||
#include <cassert>
|
||||
#include <unordered_map>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
|
||||
#include "../../Point.hpp"
|
||||
#include "SquareGrid.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
namespace Slic3r::Arachne {
|
||||
|
||||
|
||||
@@ -6,10 +6,15 @@
|
||||
#define UTILS_SPARSE_LINE_GRID_H
|
||||
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <utility>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "SparseGrid.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
namespace Slic3r::Arachne {
|
||||
|
||||
|
||||
@@ -6,9 +6,13 @@
|
||||
#define UTILS_SPARSE_POINT_GRID_H
|
||||
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "SparseGrid.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
namespace Slic3r::Arachne {
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
#include "SquareGrid.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
#include <functional>
|
||||
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Point.hpp"
|
||||
|
||||
using namespace Slic3r::Arachne;
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
#define UTILS_LINEAR_ALG_2D_H
|
||||
|
||||
#include "../../Point.hpp"
|
||||
#include <cstdint>
|
||||
#include <math.h>
|
||||
#include <cmath>
|
||||
|
||||
namespace Slic3r::Arachne::LinearAlg2D
|
||||
{
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
#include "ArcFitter.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Circle.hpp"
|
||||
#include "MultiPoint.hpp"
|
||||
#include "Polyline.hpp"
|
||||
#include "libslic3r.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <cstddef>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
#define slic3r_ArcFitter_hpp_
|
||||
|
||||
#include "Circle.hpp"
|
||||
#include <cstddef>
|
||||
#include "Point.hpp"
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
|
||||
+119
-18
@@ -1,18 +1,41 @@
|
||||
#include "Arrange.hpp"
|
||||
#include "ExPolygon.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Print.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "libslic3r.h"
|
||||
|
||||
#include <Eigen/Core>
|
||||
#include <boost/geometry/index/parameters.hpp>
|
||||
#include <functional>
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <boost/geometry/algorithms/convert.hpp>
|
||||
#include <array>
|
||||
#include <boost/geometry/index/predicates.hpp>
|
||||
#include <iterator>
|
||||
#include <exception>
|
||||
#include <libnest2d/backends/libslic3r/geometries.hpp>
|
||||
#include "libnest2d/common.hpp"
|
||||
#include "libnest2d/geometry_traits_nfp.hpp"
|
||||
#include "libnest2d/nester.hpp"
|
||||
#include "libnest2d/geometry_traits.hpp"
|
||||
#include <libnest2d/optimizers/nlopt/subplex.hpp>
|
||||
#include <libnest2d/placers/nfpplacer.hpp>
|
||||
#include <libnest2d/selections/firstfit.hpp>
|
||||
#include <libnest2d/utils/rotcalipers.hpp>
|
||||
|
||||
#include <numeric>
|
||||
#include <ClipperUtils.hpp>
|
||||
|
||||
#include <boost/geometry/index/rtree.hpp>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <tuple>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
#if defined(_MSC_VER) && defined(__clang__)
|
||||
#define BOOST_NO_CXX17_HDR_STRING_VIEW
|
||||
@@ -21,6 +44,7 @@
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <boost/multiprecision/integer.hpp>
|
||||
#include <boost/rational.hpp>
|
||||
#include "MultiMaterialSegmentation.hpp"
|
||||
|
||||
namespace libnest2d {
|
||||
#if !defined(_MSC_VER) && defined(__SIZEOF_INT128__) && !defined(__APPLE__)
|
||||
@@ -55,7 +79,7 @@ namespace Slic3r {
|
||||
|
||||
template<class Tout = double, class = FloatingOnly<Tout>, int...EigenArgs>
|
||||
inline constexpr Eigen::Matrix<Tout, 2, EigenArgs...> unscaled(
|
||||
const Slic3r::ClipperLib::IntPoint &v) noexcept
|
||||
const Slic3r::Point &v) noexcept
|
||||
{
|
||||
return Eigen::Matrix<Tout, 2, EigenArgs...>{unscaled<Tout>(v.x()),
|
||||
unscaled<Tout>(v.y())};
|
||||
@@ -69,7 +93,6 @@ using namespace libnest2d;
|
||||
using Item = _Item<ExPolygon>;
|
||||
using Box = _Box<Point>;
|
||||
using Circle = _Circle<Point>;
|
||||
using Segment = _Segment<Point>;
|
||||
using MultiPolygon = ExPolygons;
|
||||
|
||||
// Summon the spatial indexing facilities from boost
|
||||
@@ -276,7 +299,15 @@ Points get_shrink_bedpts(const DynamicPrintConfig* print_cfg, const ArrangeParam
|
||||
template<class PConf>
|
||||
void fill_config(PConf& pcfg, const ArrangeParams ¶ms) {
|
||||
|
||||
if (params.is_seq_print) {
|
||||
if (params.is_belt) {
|
||||
// Pack from the end of the belt that prints first, and keep the pile on the
|
||||
// bed when it is larger than the room around that end.
|
||||
pcfg.starting_point = !params.belt_reversed ? PConf::Alignment::BOTTOM_LEFT :
|
||||
params.belt_axis == 1 ? PConf::Alignment::TOP_LEFT :
|
||||
PConf::Alignment::BOTTOM_RIGHT;
|
||||
pcfg.clamp_to_bin = true;
|
||||
}
|
||||
else if (params.is_seq_print) {
|
||||
// Start placing the items from the center of the print bed
|
||||
pcfg.starting_point = PConf::Alignment::BOTTOM_LEFT;
|
||||
}
|
||||
@@ -405,7 +436,7 @@ protected:
|
||||
// 2) X distance of item corner to bed corner (low weight)
|
||||
// 3) item row occupancy (useful when rotation is enabled)
|
||||
// 4)需要允许往屏蔽区域的左边或下边去一点,不然很多物体可能认为摆不进去,实际上我们最后是可以做平移的
|
||||
double dist_for_BOTTOM_LEFT(Box ibb, const ClipperLib::IntPoint& origin_pack)
|
||||
double dist_for_BOTTOM_LEFT(Box ibb, const Slic3r::Point& origin_pack)
|
||||
{
|
||||
double dist_corner_y = ibb.minCorner().y() - origin_pack.y();
|
||||
double dist_corner_x = ibb.minCorner().x() - origin_pack.x();
|
||||
@@ -421,7 +452,51 @@ protected:
|
||||
return bindist;
|
||||
}
|
||||
|
||||
double dist_to_bin(const Box& ibb, const ClipperLib::IntPoint& origin_pack, typename Packer::PlacementConfig::Alignment starting_point_alignment)
|
||||
// Belt printers pack from the end of the belt that prints first, and a corner
|
||||
// packer's checks (pile inside the bin, pack origin) apply to them as well.
|
||||
bool corner_packing() const { return params.is_belt || m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT; }
|
||||
|
||||
static double at(const Box::PointType &pt, int i) { return double(i == 0 ? getX(pt) : getY(pt)); }
|
||||
|
||||
// Position along the belt in print order: increasing from the end that prints first.
|
||||
double belt_pos(const Box::PointType &pt) const { return params.belt_reversed ? -at(pt, params.belt_axis) : at(pt, params.belt_axis); }
|
||||
double belt_start(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.maxCorner() : bb.minCorner()); }
|
||||
double belt_end(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.minCorner() : bb.maxCorner()); }
|
||||
|
||||
// The corner of the bin the belt pile grows from.
|
||||
Box::PointType belt_origin() const
|
||||
{
|
||||
const Box bb = sl::boundingBox(m_bin);
|
||||
auto o = bb.minCorner();
|
||||
if (params.belt_reversed) {
|
||||
if (params.belt_axis == 0) setX(o, getX(bb.maxCorner()));
|
||||
else setY(o, getY(bb.maxCorner()));
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
// An item's far edge in print order is what it costs (so a row fills across the
|
||||
// belt before the pile advances), with a slight pull toward the near lateral
|
||||
// edge and the same penalty as the bottom-left heuristic for sitting outside
|
||||
// the corner.
|
||||
double dist_along_belt(const Box &ibb)
|
||||
{
|
||||
const Box bin = sl::boundingBox(m_bin);
|
||||
const int l = 1 - params.belt_axis;
|
||||
const double lat = at(ibb.minCorner(), l) - at(bin.minCorner(), l);
|
||||
double d = belt_end(ibb) - belt_start(bin);
|
||||
d += lat < 0 ? 10 * -lat : 0.1 * lat;
|
||||
if (double behind = belt_start(ibb) - belt_start(bin); behind < 0)
|
||||
d += 10 * -behind;
|
||||
return norm(d);
|
||||
}
|
||||
|
||||
double corner_bindist(const Box &ibb, const Slic3r::Point &origin_pack)
|
||||
{
|
||||
return params.is_belt ? dist_along_belt(ibb) : dist_for_BOTTOM_LEFT(ibb, origin_pack);
|
||||
}
|
||||
|
||||
double dist_to_bin(const Box& ibb, const Slic3r::Point& origin_pack, typename Packer::PlacementConfig::Alignment starting_point_alignment)
|
||||
{
|
||||
double bindist = 0;
|
||||
if (starting_point_alignment == PConfig::Alignment::BOTTOM_LEFT)
|
||||
@@ -439,7 +514,7 @@ protected:
|
||||
// as it possibly can be but at the same time, it has to provide
|
||||
// reasonable results.
|
||||
std::tuple<double /*score*/, Box /*farthest point from bin center*/>
|
||||
objfunc(const Item &item, const ClipperLib::IntPoint &origin_pack)
|
||||
objfunc(const Item &item, const Slic3r::Point &origin_pack)
|
||||
{
|
||||
const double bin_area = m_bin_area;
|
||||
const SpatIndex& spatindex = m_rtree;
|
||||
@@ -510,8 +585,8 @@ protected:
|
||||
|
||||
// The smalles distance from the arranged pile center:
|
||||
double dist = norm(*(std::min_element(dists.begin(), dists.end())));
|
||||
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) {
|
||||
double bindist = dist_for_BOTTOM_LEFT(ibb, origin_pack);
|
||||
if (corner_packing()) {
|
||||
double bindist = corner_bindist(ibb, origin_pack);
|
||||
score = 0.2 * dist + 0.8 * bindist;
|
||||
}
|
||||
else {
|
||||
@@ -568,8 +643,8 @@ protected:
|
||||
break;
|
||||
}
|
||||
case LAST_BIG_ITEM: {
|
||||
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) {
|
||||
score = dist_for_BOTTOM_LEFT(ibb, origin_pack);
|
||||
if (corner_packing()) {
|
||||
score = corner_bindist(ibb, origin_pack);
|
||||
}
|
||||
else {
|
||||
if (m_pilebb.defined)
|
||||
@@ -584,8 +659,8 @@ protected:
|
||||
// already processed bigger items.
|
||||
// No need to play around with the anchor points, the center will be
|
||||
// just fine for small items
|
||||
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT)
|
||||
score = dist_for_BOTTOM_LEFT(ibb, origin_pack);
|
||||
if (corner_packing())
|
||||
score = corner_bindist(ibb, origin_pack);
|
||||
else {
|
||||
// Align mainly around existing items
|
||||
score = 0.8 * norm(pl::distance(ibb.center(), bigbb.center()))+ 0.2*norm(pl::distance(ibb.center(), origin_pack));
|
||||
@@ -686,6 +761,28 @@ protected:
|
||||
score += 1 * (new_extruder_cnt-last_extruder_cnt);
|
||||
}
|
||||
|
||||
// On a belt the parts print in belt order, so every colour change between
|
||||
// parts is a filament change. Items arrive sorted by extruder and the pile
|
||||
// grows from the leading end; keep each colour's run contiguous by charging
|
||||
// an item for every packed item of another colour it does not fully follow,
|
||||
// counting the tilted layers that reach belt_tilt_slope * height past that
|
||||
// item's far edge.
|
||||
if (params.is_belt && !params.is_seq_print) {
|
||||
const std::set<int> item_colours(item.extrude_ids.begin(), item.extrude_ids.end());
|
||||
const double item_start = belt_start(ibb);
|
||||
for (Item &p : m_items) {
|
||||
if (p.is_virt_object)
|
||||
continue;
|
||||
const std::set<int> p_colours(p.extrude_ids.begin(), p.extrude_ids.end());
|
||||
const bool same_colour = std::includes(item_colours.begin(), item_colours.end(), p_colours.begin(), p_colours.end())
|
||||
|| std::includes(p_colours.begin(), p_colours.end(), item_colours.begin(), item_colours.end());
|
||||
if (same_colour)
|
||||
continue;
|
||||
if (item_start < belt_end(p.boundingBox()) + scaled(p.height * params.belt_tilt_slope))
|
||||
score += 10.;
|
||||
}
|
||||
}
|
||||
|
||||
return std::make_tuple(score, fullbb);
|
||||
}
|
||||
|
||||
@@ -762,7 +859,8 @@ public:
|
||||
|
||||
auto binbb = sl::boundingBox(m_bin);
|
||||
|
||||
auto starting_point = cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center();
|
||||
auto starting_point = this->params.is_belt ? belt_origin() :
|
||||
cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center();
|
||||
// if we have wipe tower, items should be arranged around wipe tower
|
||||
for (Item itm : items) {
|
||||
if (itm.is_wipe_tower) {
|
||||
@@ -913,7 +1011,7 @@ std::function<double(const Item &, const ItemGroup&)> AutoArranger<ExPolygon>::g
|
||||
auto mp = m_merged_pile;
|
||||
mp.emplace_back(itm.transformedShape());
|
||||
auto chull = sl::convexHull(mp);
|
||||
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT)
|
||||
if (corner_packing())
|
||||
{
|
||||
if (!sl::isInside(chull, m_bin))
|
||||
score += LARGE_COST_TO_REJECT;
|
||||
@@ -1014,7 +1112,12 @@ void _arrange(
|
||||
inline Box to_nestbin(const BoundingBox &bb) { return Box{{bb.min(X), bb.min(Y)}, {bb.max(X), bb.max(Y)}};}
|
||||
inline Circle to_nestbin(const CircleBed &c) { return Circle({c.center()(0), c.center()(1)}, c.radius()); }
|
||||
inline ExPolygon to_nestbin(const Polygon &p) { return ExPolygon{p}; }
|
||||
inline Box to_nestbin(const InfiniteBed &bed) { return Box::infinite({bed.center.x(), bed.center.y()}); }
|
||||
// libnest2d's infinite box reaches the int64 limit, where Clipper2's double math is no longer exact.
|
||||
inline Box to_nestbin(const InfiniteBed &bed)
|
||||
{
|
||||
const coord_t r = coord_t(1) << 50;
|
||||
return Box{{bed.center.x() - r, bed.center.y() - r}, {bed.center.x() + r, bed.center.y() + r}};
|
||||
}
|
||||
|
||||
inline coord_t width(const BoundingBox& box) { return box.max.x() - box.min.x(); }
|
||||
inline coord_t height(const BoundingBox& box) { return box.max.y() - box.min.y(); }
|
||||
@@ -1122,8 +1225,6 @@ void arrange(ArrangePolygons & arrangables,
|
||||
const BedT & bed,
|
||||
const ArrangeParams & params)
|
||||
{
|
||||
namespace clppr = Slic3r::ClipperLib;
|
||||
|
||||
std::vector<Item> items, fixeditems;
|
||||
items.reserve(arrangables.size());
|
||||
|
||||
|
||||
@@ -2,8 +2,17 @@
|
||||
#define ARRANGE_HPP
|
||||
|
||||
#include "ExPolygon.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "Print.hpp"
|
||||
#include <cmath>
|
||||
#include "libslic3r.h"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <ostream>
|
||||
|
||||
#define BED_SHRINK_SEQ_PRINT 5
|
||||
|
||||
@@ -137,6 +146,13 @@ struct ArrangeParams {
|
||||
float nozzle_height = 0;
|
||||
float printable_height = 256.0;
|
||||
Vec2d align_center{ 0.5,0.5 };
|
||||
// Belt printer: items print in the order they lie along the belt axis, from
|
||||
// its low end unless belt_reversed, and a part's top prints
|
||||
// belt_tilt_slope * height further along it than its base.
|
||||
bool is_belt = false;
|
||||
int belt_axis = 1; // 0 = X, 1 = Y
|
||||
bool belt_reversed = false;
|
||||
float belt_tilt_slope = 1.f; // cot(belt tilt angle), 0 when the belt is not tilted
|
||||
|
||||
ArrangePolygons excluded_regions; // regions cant't be used
|
||||
ArrangePolygons nonprefered_regions; // regions can be used but not prefered
|
||||
|
||||
@@ -0,0 +1,562 @@
|
||||
#include <limits>
|
||||
#include "BeltBrim.hpp"
|
||||
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "Flow.hpp"
|
||||
#include "Layer.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "Print.hpp"
|
||||
#include "ShortestPath.hpp"
|
||||
#include "Support/BeltFloorContext.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "ExPolygon.hpp"
|
||||
#include "ExtrusionEntity.hpp"
|
||||
#include "ExtrusionEntityCollection.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Polyline.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "libslic3r.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// ---------------------------------------------------------------- scaling
|
||||
|
||||
static inline Point scale_u_point(const Point &p, int from_axis, double factor)
|
||||
{
|
||||
// llround, not a cast: casting truncates toward zero, so a round trip would
|
||||
// walk every vertex toward the origin by up to one unit per pass.
|
||||
return from_axis == 0 ?
|
||||
Point(coord_t(std::llround(double(p.x()) * factor)), p.y()) :
|
||||
Point(p.x(), coord_t(std::llround(double(p.y()) * factor)));
|
||||
}
|
||||
|
||||
static inline void scale_u_polygon(Polygon &poly, int from_axis, double factor)
|
||||
{
|
||||
for (Point &p : poly.points)
|
||||
p = scale_u_point(p, from_axis, factor);
|
||||
}
|
||||
|
||||
ExPolygons belt_scale_u(const ExPolygons &src, const BeltBrimFrame &frame, double factor)
|
||||
{
|
||||
ExPolygons out = src;
|
||||
for (ExPolygon &ex : out) {
|
||||
scale_u_polygon(ex.contour, frame.from_axis, factor);
|
||||
for (Polygon &hole : ex.holes)
|
||||
scale_u_polygon(hole, frame.from_axis, factor);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Polylines belt_scale_u(const Polylines &src, const BeltBrimFrame &frame, double factor)
|
||||
{
|
||||
Polylines out = src;
|
||||
for (Polyline &pl : out)
|
||||
for (Point &p : pl.points)
|
||||
p = scale_u_point(p, frame.from_axis, factor);
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- sweep
|
||||
|
||||
ExPolygons sweep_ex(const ExPolygons &src, const Point &t)
|
||||
{
|
||||
if (src.empty())
|
||||
return {};
|
||||
if (t == Point(0, 0))
|
||||
return src;
|
||||
|
||||
// One parallelogram per boundary edge. Together with P and P + t these
|
||||
// cover the Minkowski sum exactly: for any q = p + s*t with p in P and
|
||||
// s in [0, 1], let s* be the smallest lambda >= 0 with q - lambda*t in P.
|
||||
// Either s* == 0 (so q is in P) or q - s* * t lies on some boundary edge e,
|
||||
// putting q in that edge's parallelogram. Hole edges must be included, or
|
||||
// holes narrower than t along t would wrongly survive the sweep.
|
||||
Polygons quads;
|
||||
for (const ExPolygon &ex : src)
|
||||
for (size_t c = 0; c < ex.num_contours(); ++ c)
|
||||
for (const Line &e : ex.contour_or_hole(c).lines()) {
|
||||
if (e.a == e.b)
|
||||
continue;
|
||||
Polygon q;
|
||||
q.points = { e.a, e.b, e.b + t, e.a + t };
|
||||
// The non-zero fill rule counts a clockwise ring as -1, which
|
||||
// would punch a hole instead of adding material. Edges parallel
|
||||
// to t give a zero-area quad; Clipper discards those harmlessly.
|
||||
if (q.is_clockwise())
|
||||
q.reverse();
|
||||
quads.emplace_back(std::move(q));
|
||||
}
|
||||
|
||||
ExPolygons shifted = src;
|
||||
for (ExPolygon &ex : shifted)
|
||||
ex.translate(t);
|
||||
|
||||
// union_ex(ExPolygons, Polygons) uses pftNonZero, which is the fill rule the
|
||||
// argument above relies on.
|
||||
return union_ex(union_ex(src, shifted), quads);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- brim region
|
||||
|
||||
ExPolygons belt_brim_region(const ExPolygons &footprint_flat,
|
||||
bool has_outer,
|
||||
bool has_inner,
|
||||
coord_t brim_width,
|
||||
coord_t object_gap,
|
||||
coord_t leading,
|
||||
coord_t lateral,
|
||||
const BeltBrimFrame &frame)
|
||||
{
|
||||
if (footprint_flat.empty() || (! has_outer && ! has_inner))
|
||||
return {};
|
||||
|
||||
ExPolygons out;
|
||||
|
||||
if (has_outer) {
|
||||
// Offset the outer ring from the contours only, so a hole cannot punch
|
||||
// through it. Same reasoning as the plate brim in Brim.cpp.
|
||||
Polygons contours;
|
||||
contours.reserve(footprint_flat.size());
|
||||
for (const ExPolygon &ex : footprint_flat)
|
||||
contours.emplace_back(ex.contour);
|
||||
|
||||
// Inner and outer boundary offset from the same polygon, to avoid
|
||||
// round-off mismatch between them.
|
||||
ExPolygons inner = offset_ex(contours, float(object_gap), jtRound, SCALED_RESOLUTION);
|
||||
|
||||
// Close the interior before offsetting outwards. A belt contact patch is often a
|
||||
// narrow, broken-up strip, and the offset rings of two islands less than
|
||||
// 2 x brim_width apart merge and fill the space between them - space that lies
|
||||
// UNDER the part, which is not what "outer brim" means. Closing also swallows
|
||||
// holes in the patch for the same reason. Concavity-filling only, so an apron or
|
||||
// any other outward protrusion is untouched.
|
||||
ExPolygons envelope = brim_width > 0 ? closing_ex(inner, float(brim_width)) : inner;
|
||||
|
||||
ExPolygons base = envelope;
|
||||
if (leading > 0) {
|
||||
// Sweep downhill from the gapped keep-out, so the apron is contiguous with
|
||||
// the ring instead of starting inside the gap.
|
||||
const Point t = frame.from_axis == 0 ?
|
||||
Point(frame.downhill_sign() * leading, 0) :
|
||||
Point(0, frame.downhill_sign() * leading);
|
||||
base = union_ex(base, sweep_ex(envelope, t));
|
||||
}
|
||||
if (lateral > 0) {
|
||||
// Across the belt, both ways. Swept from `base` so the apron is widened
|
||||
// too, and in the flattened frame the cross-belt axis is unscaled, so this
|
||||
// distance is already a true on-belt distance.
|
||||
const Point t = frame.from_axis == 0 ? Point(0, lateral) : Point(lateral, 0);
|
||||
ExPolygons widened = union_ex(sweep_ex(base, t), sweep_ex(base, Point(-t.x(), -t.y())));
|
||||
base = union_ex(base, to_polygons(widened));
|
||||
}
|
||||
ExPolygons outer = offset_ex(base, float(brim_width), jtRound, SCALED_RESOLUTION);
|
||||
expolygons_append(out, diff_ex(outer, envelope));
|
||||
}
|
||||
|
||||
if (has_inner) {
|
||||
// Holes reversed so a negative offset grows inward, mirroring Brim.cpp.
|
||||
// No apron here: an apron growing into a hole interior is never useful.
|
||||
Polygons holes;
|
||||
for (const ExPolygon &ex : footprint_flat)
|
||||
polygons_append(holes, ex.holes);
|
||||
polygons_reverse(holes);
|
||||
if (! holes.empty()) {
|
||||
ExPolygons hole_inner = offset_ex(holes, - float(brim_width + object_gap));
|
||||
ExPolygons hole_outer = offset_ex(holes, - float(object_gap));
|
||||
expolygons_append(out, intersection_ex(diff_ex(hole_outer, hole_inner), holes));
|
||||
}
|
||||
}
|
||||
|
||||
return union_ex(out);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- line lattice
|
||||
|
||||
std::vector<coord_t> belt_brim_line_positions(coord_t u_lo,
|
||||
coord_t u_hi,
|
||||
coord_t pitch_u,
|
||||
coord_t u_anchor)
|
||||
{
|
||||
std::vector<coord_t> out;
|
||||
if (pitch_u <= 0 || u_hi <= u_lo)
|
||||
return out;
|
||||
|
||||
// Walk the lattice from just below u_lo. Integer arithmetic throughout, so
|
||||
// the half-open interval needs no epsilon: a point landing exactly on u_hi
|
||||
// belongs to the next band.
|
||||
int64_t k = int64_t(std::floor(double(u_lo - u_anchor) / double(pitch_u))) - 1;
|
||||
while (u_anchor + coord_t(k) * pitch_u < u_lo)
|
||||
++ k;
|
||||
for (;; ++ k) {
|
||||
const coord_t u = u_anchor + coord_t(k) * pitch_u;
|
||||
if (u >= u_hi)
|
||||
break;
|
||||
out.emplace_back(u);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- pipeline
|
||||
|
||||
// A band of the belt surface as an explicit box, clamped to `bounds` along the
|
||||
// shear axis. Deliberately not BeltFloorContext::surface_polygon(): those
|
||||
// half-planes span +-1000 mm, which is wasteful to clip against and dangerous to
|
||||
// feed through the flattening scale.
|
||||
static Polygon band_box(const BoundingBox &bounds, int from_axis, coordf_t u_lo, coordf_t u_hi)
|
||||
{
|
||||
coord_t lo = scale_(u_lo);
|
||||
coord_t hi = scale_(u_hi);
|
||||
const coord_t bmin = from_axis == 0 ? bounds.min.x() : bounds.min.y();
|
||||
const coord_t bmax = from_axis == 0 ? bounds.max.x() : bounds.max.y();
|
||||
lo = std::max(lo, bmin);
|
||||
hi = std::min(hi, bmax);
|
||||
Polygon poly;
|
||||
if (hi <= lo)
|
||||
return poly;
|
||||
if (from_axis == 0)
|
||||
poly.points = { Point(lo, bounds.min.y()), Point(hi, bounds.min.y()),
|
||||
Point(hi, bounds.max.y()), Point(lo, bounds.max.y()) };
|
||||
else
|
||||
poly.points = { Point(bounds.min.x(), lo), Point(bounds.max.x(), lo),
|
||||
Point(bounds.max.x(), hi), Point(bounds.min.x(), hi) };
|
||||
return poly;
|
||||
}
|
||||
|
||||
ExPolygons belt_brim_clip_leading_edge(const ExPolygons ®ion, const BeltBrimFrame &frame, coordf_t u_cut)
|
||||
{
|
||||
if (region.empty())
|
||||
return region;
|
||||
BoundingBox keep_bb = get_extents(region);
|
||||
keep_bb.offset(scale_(1.));
|
||||
const bool low_side = frame.downhill_sign() < 0; // downhill is -u
|
||||
const Polygon keep = band_box(keep_bb, frame.from_axis,
|
||||
low_side ? unscale<double>(frame.from_axis == 0 ? keep_bb.min.x() : keep_bb.min.y()) : u_cut,
|
||||
low_side ? u_cut : unscale<double>(frame.from_axis == 0 ? keep_bb.max.x() : keep_bb.max.y()));
|
||||
return keep.empty() ? ExPolygons{} : intersection_ex(region, Polygons{ keep });
|
||||
}
|
||||
|
||||
// Everything the per-band line generator needs, gathered once per object.
|
||||
struct BeltBrimContext
|
||||
{
|
||||
BeltFloorContext ctx;
|
||||
BeltBrimFrame frame;
|
||||
ExPolygons region; // brim region, object-local slicing XY
|
||||
BoundingBox region_bbox;
|
||||
Flow brim_flow;
|
||||
coord_t pitch_u = 0;
|
||||
coord_t u_anchor = 0;
|
||||
double in_plane_pitch = 0.; // mm
|
||||
};
|
||||
|
||||
// Emit the cross-belt brim lines that belong to the band [print_z - height, print_z].
|
||||
static void belt_brim_band_paths(const BeltBrimContext &bc,
|
||||
coordf_t print_z,
|
||||
coordf_t height,
|
||||
const Polygons &obstacles,
|
||||
ExtrusionEntityCollection &out,
|
||||
ExPolygons &areas_out)
|
||||
{
|
||||
coordf_t u_lo = bc.ctx.cutoff_u(print_z - height);
|
||||
coordf_t u_hi = bc.ctx.cutoff_u(print_z);
|
||||
if (u_lo > u_hi)
|
||||
std::swap(u_lo, u_hi);
|
||||
|
||||
// How wide this band is measured ON the belt, versus one nominal bead.
|
||||
const double band_in_plane = (u_hi - u_lo) * bc.frame.u_stretch();
|
||||
|
||||
// Fraction of the layer height at which a line sits above the belt. Toward the
|
||||
// downhill edge, so the sheet is reasonably thick while the nozzle stays clear of
|
||||
// the belt itself.
|
||||
static constexpr double BAND_CLEARANCE_FRACTION = 0.75;
|
||||
|
||||
std::vector<coord_t> us;
|
||||
double uniform_clearance = 0.; // 0 => derive per line from its own position
|
||||
double line_pitch = bc.in_plane_pitch;
|
||||
// One line also serves a band up to half a bead wider than the nominal pitch (a 0.3 mm
|
||||
// first layer at 45 degrees): its flow is matched to the band, so the bead is that much
|
||||
// wider. Two lattice lines in such a band would land almost on top of each other.
|
||||
if (band_in_plane <= 1.5 * bc.in_plane_pitch + EPSILON) {
|
||||
// Steep belt, which is the normal case: the band is narrower than one bead, so
|
||||
// exactly one line fits. Place it at a FIXED fraction of the band rather than
|
||||
// on a nominal-spacing lattice. On a lattice each line lands at an arbitrary
|
||||
// point in its band, the clearance sweeps [0, height] from band to band, and the
|
||||
// bead width therefore varies by 2x - visible as ragged, uneven brim lines.
|
||||
// Anchoring to the band makes the clearance identical everywhere, so every bead
|
||||
// is the same width.
|
||||
//
|
||||
// The spacing is then whatever the bands give (height / sin(tilt) on the belt)
|
||||
// rather than the nominal bead spacing, so the flow below is matched to THAT
|
||||
// pitch. Matched flow at the real pitch is what keeps the sheet uniform and
|
||||
// gap-free; using nominal flow at band spacing would over-feed it.
|
||||
us.push_back(scale_(bc.ctx.cutoff_u(print_z - BAND_CLEARANCE_FRACTION * height)));
|
||||
uniform_clearance = BAND_CLEARANCE_FRACTION * height;
|
||||
line_pitch = band_in_plane;
|
||||
} else {
|
||||
// Shallow belt: the band is wider than a bead, so it takes several lines and they
|
||||
// have to sit on the nominal lattice. Their clearances then differ, and so do
|
||||
// their widths - unavoidable here, but shallow belts are the rare case.
|
||||
us = belt_brim_line_positions(scale_(u_lo), scale_(u_hi), bc.pitch_u, bc.u_anchor);
|
||||
}
|
||||
if (us.empty())
|
||||
return;
|
||||
|
||||
const Polygons region_polys = to_polygons(bc.region);
|
||||
|
||||
// One lattice line at a time: the clearance - and therefore the extrusion
|
||||
// volume - is a property of the line's u, so the pieces of different lines
|
||||
// must not be pooled before the flow is resolved.
|
||||
// Overshoot the region so the clip, not the line's ends, decides the extent.
|
||||
const coord_t margin = coord_t(SCALED_EPSILON) + 1;
|
||||
coord_t u_prev = std::numeric_limits<coord_t>::min();
|
||||
for (coord_t u : us) {
|
||||
// Nozzle-to-belt clearance for this line. Constant along the line, because the
|
||||
// belt height depends only on the shear-axis coordinate. Band-anchored lines
|
||||
// share one clearance by construction; lattice lines (shallow belts, or a first
|
||||
// layer thick enough that the band is wider than a bead) each get their own.
|
||||
//
|
||||
// A lattice line can fall where the belt is only a hair below the band's print_z.
|
||||
// The bead there would be laid scraping the belt while its flow is sized for a
|
||||
// taller cell, so it is moved uphill to the same fraction of the band the
|
||||
// single-line case uses. (The clearance is along slice Z; the real gap under the
|
||||
// nozzle is clearance x cos(tilt), 0.53 h at 45 degrees for the 0.75 fraction.)
|
||||
double clearance = uniform_clearance;
|
||||
if (clearance <= 0.) {
|
||||
const Point probe = bc.frame.from_axis == 0 ? Point(u, 0) : Point(0, u);
|
||||
clearance = print_z - bc.ctx.floor_print_z(probe);
|
||||
if (clearance < BAND_CLEARANCE_FRACTION * height) {
|
||||
clearance = BAND_CLEARANCE_FRACTION * height;
|
||||
u = scale_(bc.ctx.cutoff_u(print_z - clearance));
|
||||
}
|
||||
clearance = std::min(clearance, height);
|
||||
}
|
||||
// A line moved uphill can land on, or almost on, its neighbour; two beads closer
|
||||
// than half a pitch would be laid into the same cell.
|
||||
if (u_prev != std::numeric_limits<coord_t>::min() && std::abs(u - u_prev) < bc.pitch_u / 2)
|
||||
continue;
|
||||
u_prev = u;
|
||||
|
||||
Polyline line;
|
||||
if (bc.frame.from_axis == 0)
|
||||
line.points = { Point(u, coord_t(bc.region_bbox.min.y() - margin)),
|
||||
Point(u, coord_t(bc.region_bbox.max.y() + margin)) };
|
||||
else
|
||||
line.points = { Point(coord_t(bc.region_bbox.min.x() - margin), u),
|
||||
Point(coord_t(bc.region_bbox.max.x() + margin), u) };
|
||||
|
||||
Polylines pieces = intersection_pl(Polylines{ line }, region_polys);
|
||||
if (! obstacles.empty())
|
||||
pieces = diff_pl(pieces, obstacles);
|
||||
if (pieces.empty())
|
||||
continue;
|
||||
|
||||
// with_cross_section, not with_height: it reaches the prescribed volume while
|
||||
// KEEPING the extrusion spacing, so the bead is sized to fill exactly one
|
||||
// pitch x clearance cell of the sheet.
|
||||
const Flow f = bc.brim_flow.with_cross_section(float(line_pitch * clearance));
|
||||
|
||||
// Footprint of these beads, for the first-layer convex hull and bbox.
|
||||
for (const Polygon &p : offset(pieces, 0.5f * float(f.scaled_width())))
|
||||
areas_out.emplace_back(ExPolygon(p));
|
||||
|
||||
extrusion_entities_append_paths(out.entities, chain_polylines(std::move(pieces)),
|
||||
erBrim, f.mm3_per_mm(), f.width(), float(clearance));
|
||||
}
|
||||
}
|
||||
|
||||
// Union of everything extruded at `print_z` that the brim must keep clear of, expressed
|
||||
// in `self`'s local slicing frame. Includes `self` itself: its slice at this Z can
|
||||
// overhang outside the belt footprint and land in the brim ring, which the flattened
|
||||
// brim_object_gap - a belt-plane separation - does not cover.
|
||||
//
|
||||
// SEQUENCING: this reads every object's layers and this object's own support layers.
|
||||
// Another object's support step shifts that object's layer Z into the object frame for
|
||||
// the duration of the run (PrintObject::_generate_support_material()), so the brims must
|
||||
// not overlap with the parallel support step: Print::process() generates them one object
|
||||
// after the other once that step is over (PrintObject::generate_belt_brim()), and an
|
||||
// object that arrives on or leaves the plate invalidates the other brim owners' support
|
||||
// step (PrintApply.cpp) so their brims are clipped against what is there now. Only this
|
||||
// object's supports are dodged; another object's support at the same Z is not.
|
||||
// `region_bbox` bounds the brim; anything outside it cannot clip a brim line, so whole
|
||||
// objects are skipped without materialising their polygons. On a typical plate the
|
||||
// objects do not overlap and every foreign object drops out here, which matters because
|
||||
// this runs once per band - hundreds of times per object.
|
||||
static Polygons belt_brim_obstacles(const Print &print, const PrintObject &self,
|
||||
const BoundingBox ®ion_bbox, coordf_t print_z, coordf_t tol)
|
||||
{
|
||||
const Point shift_self = self.instances().empty() ? Point(0, 0)
|
||||
: self.instances().front().shift_without_plate_offset();
|
||||
Polygons out;
|
||||
for (const PrintObject *o : print.objects()) {
|
||||
const bool is_self = (o == &self);
|
||||
for (const PrintInstance &inst : o->instances()) {
|
||||
const Point delta = inst.shift_without_plate_offset() - shift_self;
|
||||
if (const Layer *l = o->get_layer_at_printz(print_z, tol)) {
|
||||
BoundingBox lb = get_extents(l->lslices);
|
||||
lb.translate(delta.x(), delta.y());
|
||||
if (lb.overlap(region_bbox)) {
|
||||
Polygons ps = to_polygons(l->lslices);
|
||||
for (Polygon &p : ps)
|
||||
p.translate(delta);
|
||||
polygons_append(out, std::move(ps));
|
||||
}
|
||||
}
|
||||
if (! is_self)
|
||||
continue;
|
||||
if (const SupportLayer *sl = o->get_support_layer_at_printz(print_z, tol)) {
|
||||
Polygons ps = sl->support_fills.polygons_covered_by_spacing();
|
||||
for (Polygon &p : ps)
|
||||
p.translate(delta);
|
||||
polygons_append(out, std::move(ps));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (out.size() < 2)
|
||||
return out; // union_() of 0 or 1 polygons is pure overhead
|
||||
return union_(out);
|
||||
}
|
||||
|
||||
void make_belt_brim(PrintObject &object)
|
||||
{
|
||||
object.clear_belt_brim();
|
||||
if (! object.has_belt_brim())
|
||||
return;
|
||||
|
||||
const Print &print = *object.print();
|
||||
BeltBrimContext bc;
|
||||
if (! bc.ctx.init(object.slicing_parameters(), print.config()))
|
||||
return;
|
||||
bc.frame = BeltBrimFrame{ bc.ctx.shear_factor(), bc.ctx.from_axis() };
|
||||
|
||||
const size_t nlayers = object.layers().size();
|
||||
if (nlayers == 0)
|
||||
return;
|
||||
|
||||
// 1. Belt footprint: the union of each layer's slice clipped to that layer's
|
||||
// own contact band. This is the object's bottom face, which on a belt is
|
||||
// spread over every layer instead of sitting in layer 0.
|
||||
ExPolygons footprint_acc;
|
||||
// The first layer that touches the belt: where the leading-edge brim is cut.
|
||||
const Layer *first_contact = nullptr;
|
||||
for (size_t i = 0; i < nlayers; ++ i) {
|
||||
const Layer &layer = *object.layers()[i];
|
||||
if (layer.lslices.empty())
|
||||
continue;
|
||||
// print_z - height, not the previous layer's print_z: variable layer
|
||||
// heights make the latter wrong.
|
||||
coordf_t u_lo = bc.ctx.cutoff_u(layer.print_z - layer.height);
|
||||
coordf_t u_hi = bc.ctx.cutoff_u(layer.print_z);
|
||||
if (u_lo > u_hi)
|
||||
std::swap(u_lo, u_hi);
|
||||
BoundingBox bb = get_extents(layer.lslices);
|
||||
bb.offset(scale_(1.));
|
||||
const Polygon band = band_box(bb, bc.frame.from_axis, u_lo, u_hi);
|
||||
if (band.empty())
|
||||
continue;
|
||||
ExPolygons contact = intersection_ex(layer.lslices, Polygons{ band });
|
||||
if (contact.empty())
|
||||
continue;
|
||||
if (first_contact == nullptr)
|
||||
first_contact = &layer;
|
||||
expolygons_append(footprint_acc, std::move(contact));
|
||||
}
|
||||
const ExPolygons footprint = union_ex(footprint_acc);
|
||||
if (footprint.empty())
|
||||
return;
|
||||
|
||||
// 2. Brim region, offset in the flattened (true on-belt) metric.
|
||||
const PrintObjectConfig &cfg = object.config();
|
||||
bc.brim_flow = print.brim_flow();
|
||||
const double flow_w = bc.brim_flow.scaled_spacing() * SCALING_FACTOR;
|
||||
// Quantize to an even number of lines, as the plate brim does.
|
||||
const coord_t width = scale_(std::floor(cfg.brim_width.value / flow_w / 2) * flow_w * 2);
|
||||
const coord_t leading = scale_(cfg.leading_brim_length.value);
|
||||
const coord_t lateral = scale_(cfg.extra_brim_width.value);
|
||||
const coord_t gap = scale_(cfg.brim_object_gap.value);
|
||||
|
||||
// Belt printers collapse Auto / Mouse ear / Painted to outer-only: the auto width
|
||||
// heuristic and flat ear discs have no meaning on a tilted plane. Leading-edge-only
|
||||
// is an outer brim too; it is narrowed down to the first contact below.
|
||||
const BrimType bt = cfg.brim_type.value;
|
||||
const bool has_outer = bt == btOuterOnly || bt == btOuterAndInner
|
||||
|| bt == btAutoBrim || bt == btEar || bt == btPainted
|
||||
|| bt == btLeadingEdgeOnly;
|
||||
const bool has_inner = bt == btInnerOnly || bt == btOuterAndInner;
|
||||
|
||||
bc.region = belt_unflatten(
|
||||
belt_brim_region(belt_flatten(footprint, bc.frame), has_outer, has_inner,
|
||||
width, gap, leading, lateral, bc.frame),
|
||||
bc.frame);
|
||||
|
||||
if (bt == btLeadingEdgeOnly && first_contact != nullptr && ! bc.region.empty())
|
||||
// The cut is the uphill edge of the first contact's band: everything past it
|
||||
// belongs to later contacts. The first contact is the first layer that touches
|
||||
// the belt (step 1), neither layers().front(), an empty lead-in layer, nor the
|
||||
// first layer with geometry, which is an overhang's tip when the part overhangs
|
||||
// its leading end: both lie ahead of the part.
|
||||
bc.region = belt_brim_clip_leading_edge(bc.region, bc.frame, bc.ctx.cutoff_u(first_contact->print_z));
|
||||
|
||||
if (bc.region.empty())
|
||||
return;
|
||||
bc.region_bbox = get_extents(bc.region);
|
||||
|
||||
// 3. Line lattice. Fixed pitch in the flattened metric, anchored at the
|
||||
// footprint's leading-most edge so lines stay collinear across
|
||||
// disconnected islands and across the apron prologue.
|
||||
bc.pitch_u = std::max<coord_t>(1, coord_t(bc.brim_flow.scaled_spacing() * bc.frame.cos_tilt()));
|
||||
bc.in_plane_pitch = unscale<double>(bc.pitch_u) * bc.frame.u_stretch();
|
||||
{
|
||||
const BoundingBox fbb = get_extents(footprint);
|
||||
const bool low_side = bc.frame.shear > 0.;
|
||||
bc.u_anchor = bc.frame.from_axis == 0 ? (low_side ? fbb.min.x() : fbb.max.x())
|
||||
: (low_side ? fbb.min.y() : fbb.max.y());
|
||||
}
|
||||
|
||||
// 4. Bands coincident with an object layer.
|
||||
std::vector<ExtrusionEntityCollection> by_layer(nlayers);
|
||||
std::vector<ExPolygons> areas_by_layer(nlayers);
|
||||
for (size_t i = 0; i < nlayers; ++ i) {
|
||||
const Layer &layer = *object.layers()[i];
|
||||
const Polygons obstacles = belt_brim_obstacles(print, object, bc.region_bbox, layer.print_z, 0.5 * layer.height);
|
||||
belt_brim_band_paths(bc, layer.print_z, layer.height, obstacles, by_layer[i], areas_by_layer[i]);
|
||||
}
|
||||
|
||||
// 5. Apron prologue: the part of the region downhill of the object's first
|
||||
// layer, which has no object layer to ride on.
|
||||
std::vector<BeltBrimBand> prologue;
|
||||
{
|
||||
const Layer &first = *object.layers().front();
|
||||
const coordf_t h = first.height;
|
||||
const bool low_side = bc.frame.shear > 0.;
|
||||
const coord_t u_lead_s = bc.frame.from_axis == 0
|
||||
? (low_side ? bc.region_bbox.min.x() : bc.region_bbox.max.x())
|
||||
: (low_side ? bc.region_bbox.min.y() : bc.region_bbox.max.y());
|
||||
const coordf_t u_lead = unscale<double>(u_lead_s);
|
||||
// print_z at which the belt surface crosses the region's leading edge.
|
||||
const coordf_t z_lead = bc.ctx.shear_factor() * u_lead
|
||||
+ bc.ctx.floor_offset() + bc.ctx.z_shift();
|
||||
if (h > EPSILON)
|
||||
for (coordf_t z = first.print_z - h; z > z_lead - h; z -= h) {
|
||||
const Polygons obstacles = belt_brim_obstacles(print, object, bc.region_bbox, z, 0.5 * h);
|
||||
BeltBrimBand band;
|
||||
band.print_z = z;
|
||||
band.height = h;
|
||||
belt_brim_band_paths(bc, z, h, obstacles, band.fills, band.areas);
|
||||
if (! band.fills.empty())
|
||||
prologue.emplace_back(std::move(band));
|
||||
}
|
||||
// Lowest Z first, so collect_layers_to_print sees them in print order.
|
||||
std::reverse(prologue.begin(), prologue.end());
|
||||
}
|
||||
|
||||
object.set_belt_brim(std::move(by_layer), std::move(areas_by_layer), std::move(prologue));
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,178 @@
|
||||
#ifndef slic3r_BeltBrim_hpp_
|
||||
#define slic3r_BeltBrim_hpp_
|
||||
|
||||
#include "ExPolygon.hpp"
|
||||
#include "ExtrusionEntityCollection.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Polyline.hpp"
|
||||
#include "libslic3r.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
// Belt-printer brim geometry.
|
||||
//
|
||||
// A belt printer slices in a ROTATED frame, so the belt surface is not the
|
||||
// Z=0 bed plane but a tilted plane in slicing space:
|
||||
//
|
||||
// z_slicing(u) = shear * u + floor_offset + z_shift, u = X or Y
|
||||
//
|
||||
// where `shear == tan(tilt)` (SlicingParameters::belt_floor_shear_factor) and
|
||||
// the axis is selected by SlicingParameters::belt_floor_from_axis. See
|
||||
// Support/BeltFloorContext.hpp for the canonical accessors.
|
||||
//
|
||||
// Consequences that drive everything in this file:
|
||||
//
|
||||
// * A horizontal slicing layer touches the belt only along a narrow strip at
|
||||
// its leading edge, `layer_height / shear` wide (~0.2 mm at 45 degrees).
|
||||
// The object's belt footprint - its bottom face - is therefore spread over
|
||||
// every layer, not contained in layer 0.
|
||||
// * Distances measured in slicing XY are NOT on-belt distances: moving `du`
|
||||
// along the shear axis travels `du / cos(tilt)` across the belt. So brim
|
||||
// offsets have to be taken in a "flattened" space where the shear axis is
|
||||
// stretched by `1 / cos(tilt)`, then mapped back.
|
||||
// * Brim ahead of the part (downhill) lies at slicing Z BELOW the object's
|
||||
// first layer, because the object's layer 0 is precisely its leading
|
||||
// contact with the belt.
|
||||
//
|
||||
// Everything here is pure geometry on ExPolygons/Polylines so it can be unit
|
||||
// tested without a Print. Keep user-visible strings out of this file: it is
|
||||
// not listed in localization/i18n/list.txt.
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Tilt window within which the BELT plane, not the bed plane, is the adhesion
|
||||
// surface. Below ~1 degree a belt is a flat bed as far as adhesion goes, and the
|
||||
// contact band would be layer_height/sin(tilt) - tens of millimetres - so the
|
||||
// ordinary plate brim is both correct and cheaper. Above ~85 degrees the whole
|
||||
// brim compresses into a sliver and is not worth generating.
|
||||
inline constexpr double BELT_BRIM_MIN_TILT_DEG = 1.;
|
||||
inline constexpr double BELT_BRIM_MAX_TILT_DEG = 85.;
|
||||
|
||||
// Description of the tilted belt plane, reduced to what the brim geometry needs.
|
||||
struct BeltBrimFrame
|
||||
{
|
||||
// tan(tilt). Sign selects which way is downhill.
|
||||
double shear = 0.;
|
||||
// 0 = X, 1 = Y. Matches BeltFloorContext::from_axis().
|
||||
int from_axis = 1;
|
||||
|
||||
// 1 / cos(tilt). Stretch factor that turns a projected distance along
|
||||
// `from_axis` into the true distance travelled across the belt.
|
||||
double u_stretch() const { return std::sqrt(1. + shear * shear); }
|
||||
// cos(tilt). The inverse mapping.
|
||||
double cos_tilt() const { return 1. / this->u_stretch(); }
|
||||
// Downhill is where the belt surface is lower, i.e. printed earlier, i.e.
|
||||
// the leading edge of the part. For shear > 0 that is -u.
|
||||
int downhill_sign() const { return shear > 0. ? -1 : +1; }
|
||||
};
|
||||
|
||||
// Scale only the `from_axis` component by `factor`, rounding to nearest.
|
||||
//
|
||||
// Deliberately not MultiPoint::scale(fx, fy) / ExPolygon::scale(fx, fy): those
|
||||
// truncate toward zero, which is asymmetric about the origin and loses up to a
|
||||
// full coordinate unit per vertex on every round trip.
|
||||
ExPolygons belt_scale_u(const ExPolygons &src, const BeltBrimFrame &frame, double factor);
|
||||
Polylines belt_scale_u(const Polylines &src, const BeltBrimFrame &frame, double factor);
|
||||
|
||||
// Into / out of the space where Euclidean offsets equal true on-belt distances.
|
||||
inline ExPolygons belt_flatten(const ExPolygons &src, const BeltBrimFrame &frame)
|
||||
{ return belt_scale_u(src, frame, frame.u_stretch()); }
|
||||
inline ExPolygons belt_unflatten(const ExPolygons &src, const BeltBrimFrame &frame)
|
||||
{ return belt_scale_u(src, frame, frame.cos_tilt()); }
|
||||
|
||||
// Minkowski sum of `src` with the segment [0, t]: the region swept by sliding
|
||||
// `src` along t. Used to grow the brim downhill for "extra brim width".
|
||||
//
|
||||
// Implemented as union_(P, P + t, {parallelogram per boundary edge}) over ALL
|
||||
// contours including holes, with every parallelogram forced counter-clockwise
|
||||
// so the non-zero fill rule closes holes narrower than t along the sweep
|
||||
// direction. A hole survives exactly when it is wider than |t| measured along
|
||||
// t - not when it is wider in its narrowest Euclidean direction.
|
||||
ExPolygons sweep_ex(const ExPolygons &src, const Point &t);
|
||||
|
||||
// "Leading edge only": keep the part of a brim region (unflattened, slicing XY)
|
||||
// at or downhill of the object's first contact with the belt, so the part is
|
||||
// supported as it lands and nothing is printed alongside it afterwards. `u_cut`
|
||||
// is the uphill edge of the first layer's contact band along `frame.from_axis`,
|
||||
// in mm (BeltFloorContext::cutoff_u of the first layer); downhill is the side
|
||||
// `frame.downhill_sign()` points to.
|
||||
ExPolygons belt_brim_clip_leading_edge(const ExPolygons ®ion, const BeltBrimFrame &frame, coordf_t u_cut);
|
||||
|
||||
// Brim region for one already-flattened belt footprint. All lengths are scaled
|
||||
// and measured in the flattened (true on-belt) metric.
|
||||
//
|
||||
// `has_outer` / `has_inner` are the resolved BrimType: belt printers collapse
|
||||
// Auto / Mouse ear / Painted to outer-only, so the caller does that mapping and
|
||||
// this function never needs PrintConfig.
|
||||
//
|
||||
// Two directional extras are applied to the footprint before the outer offset, so
|
||||
// each one buys reach in one direction only:
|
||||
//
|
||||
// `leading` (leading_brim_length) sweeps the footprint DOWNHILL along the belt,
|
||||
// so every leading-facing edge gains an apron ahead of it.
|
||||
// `lateral` (extra_brim_width) sweeps it BOTH WAYS across the belt, widening
|
||||
// the brim sideways without pushing it further ahead or behind.
|
||||
//
|
||||
// Neither is applied to the inner (hole) ring.
|
||||
ExPolygons belt_brim_region(const ExPolygons &footprint_flat,
|
||||
bool has_outer,
|
||||
bool has_inner,
|
||||
coord_t brim_width,
|
||||
coord_t object_gap,
|
||||
coord_t leading,
|
||||
coord_t lateral,
|
||||
const BeltBrimFrame &frame);
|
||||
|
||||
// Brim line positions for one layer band.
|
||||
//
|
||||
// Lines sit on a fixed lattice `u_anchor + k * pitch_u` so the on-belt spacing
|
||||
// between neighbouring brim lines is constant regardless of how the lattice
|
||||
// falls across layer bands. Snapping to band centres instead would quantise
|
||||
// the spacing to whole bands and under-deposit by ~35% at 45 degrees.
|
||||
//
|
||||
// The band is half-open, [u_lo, u_hi), so every lattice point belongs to
|
||||
// exactly one band: none duplicated at a boundary, none dropped. A band
|
||||
// narrower than the pitch simply yields nothing; a band much wider (shallow
|
||||
// tilt) yields several lines.
|
||||
std::vector<coord_t> belt_brim_line_positions(coord_t u_lo,
|
||||
coord_t u_hi,
|
||||
coord_t pitch_u,
|
||||
coord_t u_anchor);
|
||||
|
||||
// ---------------------------------------------------------------- pipeline
|
||||
|
||||
// One brim-only layer printed BEFORE the object's first layer, carrying the
|
||||
// apron that has to be stuck to the belt ahead of the part.
|
||||
//
|
||||
// Deliberately not a Layer subclass. A synthetic Layer would inherit id()
|
||||
// semantics that leak into initial-layer temperature selection, the spiral vase
|
||||
// probe, gradual interpolation, avoid-crossing-perimeters and cooling, all of
|
||||
// which key off Layer::id() == 0 or off a layer's regions. A plain record
|
||||
// carries only what the emitter needs.
|
||||
//
|
||||
// `height` is the LAYER height, used for the Z move and ordering metadata only.
|
||||
// Each extrusion path inside `fills` carries its own height, equal to that
|
||||
// line's nozzle-to-belt clearance, which varies across the band.
|
||||
struct BeltBrimBand
|
||||
{
|
||||
coordf_t print_z = 0.;
|
||||
coordf_t height = 0.;
|
||||
// erBrim paths in the object's local slicing frame, untranslated.
|
||||
ExtrusionEntityCollection fills;
|
||||
// Footprint of those paths, for the first-layer convex hull / bbox.
|
||||
ExPolygons areas;
|
||||
};
|
||||
|
||||
class PrintObject;
|
||||
|
||||
// Generate the belt brim for one object: fills its per-object-layer bands and
|
||||
// its apron prologue. No-op unless PrintObject::has_belt_brim().
|
||||
//
|
||||
// Runs inside posSupportMaterial rather than the brim step, because the prologue
|
||||
// print_z values must exist before ToolOrdering is built at psWipeTower.
|
||||
void make_belt_brim(PrintObject &object);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_BeltBrim_hpp_
|
||||
@@ -0,0 +1,50 @@
|
||||
#include "BeltGCode.hpp"
|
||||
#include "GCodeWriter.hpp"
|
||||
#include "GCode/BeltKinematics.hpp"
|
||||
#include "BeltTransform.hpp"
|
||||
#include "Print.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "libslic3r.h"
|
||||
#include <cstdlib>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void BeltGCode::init_belt_writer(Print &print)
|
||||
{
|
||||
// Axis remap and build volume max are set by base GCode after init_belt_writer
|
||||
// returns; set_kinematics() replays them, so install order does not matter.
|
||||
install_belt_kinematics(m_writer, print.config());
|
||||
m_writer.set_force_normal_lift(true);
|
||||
}
|
||||
|
||||
void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print)
|
||||
{
|
||||
const auto &full_cfg = print.full_print_config();
|
||||
// Slicing rotation: the belt tilt (axis + angle) and the single source of truth
|
||||
// for the physical tilt the G-code viewer uses to enable belt view.
|
||||
file.write_format("; belt_slice_rotation = %s\n", full_cfg.opt_serialize("belt_slice_rotation").c_str());
|
||||
file.write_format("; belt_slice_rotation_angle = %.1f\n", print.config().belt_slice_rotation_angle.value);
|
||||
// Machine-frame transform: shear (cot) + scale (1/|sin|) derived from the belt
|
||||
// tilt angle (or belt_frame_tilt_angle when decoupled).
|
||||
file.write_format("; belt_frame_tilt_decouple = %d\n", print.config().belt_frame_tilt_decouple.value ? 1 : 0);
|
||||
file.write_format("; belt_frame_tilt_angle = %.1f\n", print.config().belt_frame_tilt_angle.value);
|
||||
}
|
||||
|
||||
void BeltGCode::on_set_origin(const PrintObject * /*obj*/, const Point & /*inst_shift*/)
|
||||
{
|
||||
// Matches the per-instance Z-offset added in PrintObjectSlice.cpp: transform
|
||||
// the origin through the belt pipeline so that back_transform(T * origin) =
|
||||
// origin (correct machine position). The back_transform applied during
|
||||
// G-code emission is the inverse of the forward transform.
|
||||
|
||||
// Adjust origin: transform through belt forward pipeline so that
|
||||
// the back-transform correctly recovers model-space positions.
|
||||
Transform3d T = BeltTransformPipeline::build_forward_transform(m_config);
|
||||
Vec2d cur_origin = this->origin();
|
||||
Vec3d origin3d(cur_origin.x(), cur_origin.y(), 0.);
|
||||
Vec3d adjusted = T.linear() * origin3d;
|
||||
this->set_origin(Vec2d(adjusted.x(), adjusted.y()));
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "GCode.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Print.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Belt-printer-specific GCode export.
|
||||
//
|
||||
// Inherits from GCode and overrides virtual hooks to:
|
||||
// - Install a BeltKinematics on the GCodeWriter
|
||||
// - Write belt configuration to the G-code header
|
||||
// - Adjust the origin for global pre-slice transforms when switching instances
|
||||
// (Arc fitting is disabled for belt printers by BeltKinematics::supports_arc_moves(),
|
||||
// which the base GCode::should_disable_arc_fitting() consults -- no override needed.)
|
||||
class BeltGCode : public GCode
|
||||
{
|
||||
protected:
|
||||
void init_belt_writer(Print &print) override;
|
||||
void write_belt_header(GCodeOutputStream &file, const Print &print) override;
|
||||
void on_set_origin(const PrintObject *obj, const Point &inst_shift) override;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,433 @@
|
||||
// ORCA-Belt: backend of the belt purge tower (the belt replacement for the
|
||||
// classic wipe/prime tower).
|
||||
//
|
||||
// Kept in its own translation unit so the belt-purge logic stays out of the way
|
||||
// of unrelated upstream changes to Print.cpp / PrintObjectSlice.cpp and carries
|
||||
// no regression risk for normal printers: none of these methods do anything
|
||||
// unless the print is a belt printer with the belt purge tower enabled.
|
||||
//
|
||||
// Print::has_belt_purge_tower() - is the belt purge tower active?
|
||||
// Print::_align_belt_purge_layers() - snap the prism's layer grid onto the
|
||||
// printed objects' grid
|
||||
// Print::_plan_belt_purge() - route filament-change purging into the
|
||||
// prism (flush-into-objects), no wipe tower
|
||||
// PrintObject::belt_shift_layer_grid() - shift a sliced layer grid
|
||||
// PrintObject::belt_truncate_layers_above() - cancel the prism past the last swap
|
||||
//
|
||||
// (Declarations live in Print.hpp alongside the rest of the Print interface.)
|
||||
|
||||
#include "Print.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "Exception.hpp"
|
||||
#include "GCode/ToolOrdering.hpp"
|
||||
#include "Layer.hpp"
|
||||
#include "ExtrusionEntity.hpp"
|
||||
#include "ExtrusionEntityCollection.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "format.hpp"
|
||||
#include "LocalesUtils.hpp"
|
||||
#include "libslic3r.h"
|
||||
#include "BeltBrim.hpp"
|
||||
#include "PrintBase.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Belt purge prism: purging after filament changes is routed into a sliced
|
||||
// prism object via the flush-into-objects machinery instead of a wipe tower.
|
||||
bool Print::has_belt_purge_tower() const
|
||||
{
|
||||
// Its own purge-tower "type", gated by the belt-only enable_belt_purge_tower
|
||||
// option (not the classic enable_prime_tower).
|
||||
if (!(m_config.belt_printer.value
|
||||
&& m_config.enable_belt_purge_tower.value
|
||||
&& !m_config.spiral_mode.value
|
||||
&& m_config.filament_diameter.values.size() > 1))
|
||||
return false;
|
||||
|
||||
return std::any_of(m_objects.begin(), m_objects.end(), [](const PrintObject *object) {
|
||||
return object->config().belt_purge_tower_object.value;
|
||||
});
|
||||
}
|
||||
|
||||
// Belt mode: align ALL objects on the plate (the printed objects AND the purge
|
||||
// prism) onto one common layer grid, so the prism can absorb every toolchange.
|
||||
//
|
||||
// After belt slicing each object's layer print_z carries a per-object global z
|
||||
// offset (mesh-vertex-scan belt_z_shift + instance-Y-dependent terms), so
|
||||
// objects at different belt-Y positions get layer grids with DIFFERENT residues
|
||||
// (mod layer height). Purge marking looks absorbers up with
|
||||
// get_layer_at_printz(lt.print_z, EPSILON), so a toolchange on object B only
|
||||
// absorbs into the prism if the prism has a layer at B's print_z. Snapping only
|
||||
// the prism to one object therefore worked for a single (assembled) multi-color
|
||||
// object but failed with multiple separate objects — the prism could follow only
|
||||
// one grid, and toolchanges on the others went unabsorbed ("multiple layer
|
||||
// grids" warning).
|
||||
//
|
||||
// Fix: pick one reference grid (the tallest object) and shift every object onto
|
||||
// it. Each shift is at most half a layer height — a sub-100µm move along the
|
||||
// belt, the very same mechanism the per-object global_z_offset already uses, and
|
||||
// it keeps each object internally consistent (belt_shift_layer_grid moves the
|
||||
// object's layers, its support layers, and its belt floor together). Equal layer
|
||||
// height across objects is enforced by Print::validate(), so once residues match
|
||||
// every object steps on the same lattice {ref_offset + k*h} and every toolchange
|
||||
// layer coincides with a prism layer.
|
||||
void Print::_align_belt_purge_layers()
|
||||
{
|
||||
PrintObject *prism = nullptr;
|
||||
for (PrintObject *po : m_objects)
|
||||
if (po->config().belt_purge_tower_object.value && !po->layers().empty()) {
|
||||
prism = po;
|
||||
break;
|
||||
}
|
||||
if (prism == nullptr || prism->layers().empty())
|
||||
return;
|
||||
|
||||
const double h = prism->config().layer_height.value;
|
||||
if (h <= EPSILON)
|
||||
return;
|
||||
|
||||
// Grid residue of an object's layer grid: identical for all of an object's
|
||||
// layers above the first since they step by h.
|
||||
auto grid_offset = [h](const PrintObject *po) -> double {
|
||||
if (po->layers().empty())
|
||||
return 0.;
|
||||
const double z = po->layers().front()->print_z;
|
||||
return z - std::floor(z / h) * h; // in [0, h)
|
||||
};
|
||||
|
||||
// Reference grid: the tallest non-prism object (proxy for the object with
|
||||
// the most toolchange layers — minimizes how far the rest must move).
|
||||
const PrintObject *ref = nullptr;
|
||||
double ref_top = -std::numeric_limits<double>::max();
|
||||
for (const PrintObject *po : m_objects) {
|
||||
if (po->config().belt_purge_tower_object.value || po->layers().empty())
|
||||
continue;
|
||||
const double top = po->layers().back()->print_z;
|
||||
if (top > ref_top) {
|
||||
ref_top = top;
|
||||
ref = po;
|
||||
}
|
||||
}
|
||||
if (ref == nullptr)
|
||||
return;
|
||||
|
||||
const double ref_offset = grid_offset(ref);
|
||||
|
||||
// Snap every object (printed objects AND the prism) onto the reference grid.
|
||||
for (PrintObject *po : m_objects) {
|
||||
if (po->layers().empty())
|
||||
continue;
|
||||
double delta = ref_offset - grid_offset(po);
|
||||
if (delta > 0.5 * h)
|
||||
delta -= h;
|
||||
else if (delta <= -0.5 * h)
|
||||
delta += h;
|
||||
po->belt_shift_layer_grid(delta); // no-op for the reference object (delta ~ 0)
|
||||
}
|
||||
}
|
||||
|
||||
// Belt mode replacement for _make_wipe_tower(): plan filament-change purging
|
||||
// into the belt purge prism (and any other flush_into_* object) using the
|
||||
// flush-into-objects machinery, without generating classic wipe tower G-code.
|
||||
// The toolchange itself is emitted by GCode::set_extruder() via the
|
||||
// change_filament_gcode macro; the overrides marked here make the new
|
||||
// filament's first extrusions land in the purge prism.
|
||||
void Print::_plan_belt_purge()
|
||||
{
|
||||
m_wipe_tower_data.clear();
|
||||
|
||||
// psWipeTower may be invalidated without posSlice (for example after a
|
||||
// filament-map or tool-ordering change). Restore a prism shortened by the
|
||||
// previous plan so a newly higher toolchange can use its original layers.
|
||||
for (PrintObject *po : m_objects)
|
||||
if (po->config().belt_purge_tower_object.value)
|
||||
po->belt_undo_purge_plan();
|
||||
|
||||
// Must run before ToolOrdering is built: LayerTools merge per-object layer
|
||||
// print_z values, and the prism only absorbs purge where its (snapped)
|
||||
// layers coincide with the toolchange layers.
|
||||
this->_align_belt_purge_layers();
|
||||
|
||||
const unsigned int number_of_extruders = (unsigned int) m_config.filament_colour.values.size();
|
||||
|
||||
// No initial priming extrusions: there is no tower to prime on.
|
||||
m_wipe_tower_data.tool_ordering = ToolOrdering(*this, (unsigned int) -1, false);
|
||||
m_wipe_tower_data.tool_ordering.sort_and_build_data(*this, (unsigned int) -1, false);
|
||||
|
||||
if (m_wipe_tower_data.tool_ordering.empty() || m_wipe_tower_data.tool_ordering.last_extruder() == unsigned(-1))
|
||||
throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings.");
|
||||
|
||||
// Is there any filament change at all? Not ToolOrdering::has_wipe_tower(): that reads the
|
||||
// FIRST layer's flag, and on a belt the first layer may be a brim apron band, which carries
|
||||
// neither object nor support and so never gets the flag even when the print changes filament.
|
||||
{
|
||||
bool any_change = false;
|
||||
unsigned int cur = m_wipe_tower_data.tool_ordering.first_extruder();
|
||||
for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools())
|
||||
for (const unsigned int e : lt.extruders)
|
||||
if (e != cur) { any_change = true; cur = e; }
|
||||
if (! any_change)
|
||||
return;
|
||||
}
|
||||
|
||||
this->throw_if_canceled();
|
||||
|
||||
// Flush volumes per filament pair, mirroring the generic wipe tower path:
|
||||
// full flush matrix for single extruder multi material with purging enabled,
|
||||
// plain prime volume otherwise.
|
||||
std::vector<float> flush_matrix(cast<float>(
|
||||
get_flush_volumes_matrix(m_config.flush_volumes_matrix.values, 0, m_config.nozzle_diameter.values.size())));
|
||||
std::vector<std::vector<float>> wipe_volumes;
|
||||
for (unsigned int i = 0; i < number_of_extruders; ++i)
|
||||
wipe_volumes.push_back(std::vector<float>(flush_matrix.begin() + i * number_of_extruders,
|
||||
flush_matrix.begin() + (i + 1) * number_of_extruders));
|
||||
const bool use_flush_matrix = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material;
|
||||
const float flush_multiplier = (float) m_config.flush_multiplier.get_at(0);
|
||||
|
||||
// Cancel the purge prism early: pre-scan the tool ordering for the highest
|
||||
// print_z that actually has a toolchange, then drop the prism's layers above
|
||||
// it so the tower stops at the last color swap (saves filament/time). This
|
||||
// MUST happen before the marking loop below: ensure_perimeters_infills_order
|
||||
// force-overrides the prism's extrusions on every layer (it is a dedicated
|
||||
// flush object), so truncating afterwards would leave dangling overrides
|
||||
// pointing into deleted layers.
|
||||
{
|
||||
// The tool ordering covers the WHOLE print, and the prism is a printed
|
||||
// object in it. Left unbounded, the scan below sees the prism's own
|
||||
// toolchanges on layers above every model object -- the prism runs past
|
||||
// them by design (ramp/height compensation at the tilted ends) -- so
|
||||
// last_tc_z lands at the prism's own top and the truncation cancels
|
||||
// nothing. The tower ends up justifying its own existence.
|
||||
//
|
||||
// Nothing above the tallest printed object can require a color change,
|
||||
// so bound the scan there. On MCTEST5 that is 197 toolchanges spanning
|
||||
// z=154.00..193.20 with the tallest object topping out at 153.80, i.e.
|
||||
// 39.4 mm of tower that no swap ever needed.
|
||||
// Support layers count too: on a belt they can extend above the object's
|
||||
// own top, and a toolchange there is a real one.
|
||||
double obj_top_z = -1.;
|
||||
for (const PrintObject *po : m_objects) {
|
||||
if (po->config().belt_purge_tower_object.value)
|
||||
continue;
|
||||
if (!po->layers().empty())
|
||||
obj_top_z = std::max(obj_top_z, po->layers().back()->print_z);
|
||||
if (!po->support_layers().empty())
|
||||
obj_top_z = std::max(obj_top_z, po->support_layers().back()->print_z);
|
||||
}
|
||||
|
||||
double last_tc_z = -1.;
|
||||
unsigned int cur_ext = m_wipe_tower_data.tool_ordering.first_extruder();
|
||||
for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools()) {
|
||||
// layer_tools() is ordered by print_z ascending.
|
||||
if (obj_top_z >= 0. && lt.print_z > obj_top_z + EPSILON)
|
||||
break;
|
||||
for (const unsigned int e : lt.extruders)
|
||||
if (e != cur_ext) { last_tc_z = lt.print_z; cur_ext = e; }
|
||||
}
|
||||
// Deliberately NOT cancelling the prism outright when no object toolchange
|
||||
// exists: belt_truncate_layers_above(0.) empties m_layers, and an object
|
||||
// with zero layers is not something the rest of the pipeline expects. The
|
||||
// GUI already declines to create a prism unless more than one filament is
|
||||
// in use, so this case is a stale prism, not a hot path -- leave it whole
|
||||
// rather than risk a zero-layer object.
|
||||
if (last_tc_z >= 0.)
|
||||
for (PrintObject *po : m_objects)
|
||||
if (po->config().belt_purge_tower_object.value && !po->layers().empty()) {
|
||||
po->belt_truncate_layers_above(last_tc_z);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// The prism only absorbs purge at toolchange layers whose print_z coincides
|
||||
// with one of its own layers.
|
||||
PrintObject *prism_po = nullptr;
|
||||
for (PrintObject *po : m_objects)
|
||||
if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { prism_po = po; break; }
|
||||
|
||||
float total_leftover = 0.f;
|
||||
float worst_layer_leftover = 0.f;
|
||||
double worst_layer_z = 0.;
|
||||
|
||||
unsigned int current_extruder_id = m_wipe_tower_data.tool_ordering.first_extruder();
|
||||
for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) {
|
||||
float layer_leftover = 0.f;
|
||||
for (const unsigned int extruder_id : layer_tools.extruders) {
|
||||
if (extruder_id == current_extruder_id)
|
||||
continue;
|
||||
float volume_to_wipe = use_flush_matrix ?
|
||||
wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier :
|
||||
(float) m_config.prime_volume;
|
||||
float leftover = layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_extruder_id, extruder_id,
|
||||
volume_to_wipe);
|
||||
layer_leftover += leftover;
|
||||
current_extruder_id = extruder_id;
|
||||
}
|
||||
|
||||
// Plastic saving: drop the prism's fills that no toolchange on this layer
|
||||
// claimed. At this point the prism's OVERRIDDEN fills are exactly the
|
||||
// purge; the rest would print as solid infill in the prism's own filament
|
||||
// for nothing -- which is the whole prism on a layer with no toolchange
|
||||
// (141 of 692 layers on MCTEST5 before the truncation fix). Perimeters are
|
||||
// left alone so the bar keeps a continuous wall along the belt.
|
||||
//
|
||||
// Non-destructive: the entities are stashed with their positions and put
|
||||
// back by belt_restore_dropped_fills() at the top of the next plan. An
|
||||
// earlier version deleted them outright, which broke replanning when a
|
||||
// later tool ordering needed what this one had not claimed -- that is why
|
||||
// it was removed rather than kept.
|
||||
if (prism_po != nullptr) {
|
||||
const auto &we = layer_tools.wiping_extrusions();
|
||||
prism_po->belt_drop_unclaimed_fills(
|
||||
prism_po->get_layer_at_printz(layer_tools.print_z, EPSILON),
|
||||
[&we, prism_po](const ExtrusionEntity *e) { return we.is_entity_overridden(e, prism_po, 0); });
|
||||
}
|
||||
|
||||
layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this);
|
||||
if (layer_leftover > 0.f) {
|
||||
total_leftover += layer_leftover;
|
||||
if (layer_leftover > worst_layer_leftover) {
|
||||
worst_layer_leftover = layer_leftover;
|
||||
worst_layer_z = layer_tools.print_z;
|
||||
}
|
||||
}
|
||||
this->throw_if_canceled();
|
||||
}
|
||||
|
||||
if (total_leftover > 1.f) {
|
||||
this->active_step_add_warning(
|
||||
PrintStateBase::WarningLevel::CRITICAL,
|
||||
Slic3r::format(_u8L("The belt purge tower cannot absorb the full purge volume: %1% mm³ in total could not "
|
||||
"be purged (worst layer: %2% mm³ at height %3%). The print may show color bleeding. "
|
||||
"Increase the belt purge tower width, or reduce flushing volumes."),
|
||||
int(std::ceil(total_leftover)), int(std::ceil(worst_layer_leftover)),
|
||||
Slic3r::float_to_string_decimal_point(worst_layer_z, 2)));
|
||||
}
|
||||
}
|
||||
|
||||
// Belt mode: shift the sliced layer grid by delta. Mirrors the global_z_offset
|
||||
// application in slice() — layer print_z and belt_floor_z_shift move together
|
||||
// so belt floor clipping stays consistent with the shifted grid. Used by
|
||||
// Print::_align_belt_purge_layers() to snap the purge prism onto the printed
|
||||
// objects' layer grid; |delta| <= half a layer height, i.e. a sub-layer shift
|
||||
// of the prism along the belt.
|
||||
void PrintObject::belt_shift_layer_grid(double delta)
|
||||
{
|
||||
if (std::abs(delta) < EPSILON)
|
||||
return;
|
||||
for (Layer *layer : m_layers)
|
||||
layer->print_z += delta;
|
||||
for (SupportLayer *layer : m_support_layers)
|
||||
layer->print_z += delta;
|
||||
// The brim's apron bands below the first layer carry their own print_z.
|
||||
for (BeltBrimBand &band : m_belt_brim_prologue)
|
||||
band.print_z += delta;
|
||||
m_slicing_params.belt_floor_z_shift += delta;
|
||||
// The grid stays shifted across a support-only or brim-only change (posSlice does
|
||||
// not rerun), so everything slice() derived from it has to follow: the cached floor
|
||||
// that update_slicing_parameters() restores, and the global offset the organic
|
||||
// support layers and the adaptive infill octree are placed with. Left alone, the
|
||||
// next alignment finds a delta of 0 and the floor and the supports sit up to half
|
||||
// a layer off the grid, unlike a fresh slice.
|
||||
if (m_belt_floor_z_shift_cache_valid)
|
||||
m_belt_floor_z_shift_cached += delta;
|
||||
m_belt_global_z_offset += delta;
|
||||
}
|
||||
|
||||
// Belt mode: drop layers strictly above z (used to cancel the purge prism early
|
||||
// once there are no more toolchanges above z, so the tower stops at the last
|
||||
// color swap instead of wasting filament up the rest of the belt). Each layer's
|
||||
// cross-section is already sliced, so removing upper layers does not affect the
|
||||
// last toolchange's coverage. Deletes the Layer objects and clears the new top
|
||||
// layer's upper-layer link. Returns the number of layers removed.
|
||||
size_t PrintObject::belt_truncate_layers_above(coordf_t z)
|
||||
{
|
||||
// A repeated plan always starts from the restored full layer set.
|
||||
assert(m_belt_truncated_layers.empty());
|
||||
size_t keep = m_layers.size();
|
||||
while (keep > 0 && m_layers[keep - 1]->print_z > z + EPSILON)
|
||||
--keep;
|
||||
if (keep >= m_layers.size())
|
||||
return 0;
|
||||
const size_t removed = m_layers.size() - keep;
|
||||
m_belt_truncated_layers.assign(m_layers.begin() + keep, m_layers.end());
|
||||
m_layers.resize(keep);
|
||||
if (!m_layers.empty())
|
||||
m_layers.back()->upper_layer = nullptr;
|
||||
return removed;
|
||||
}
|
||||
|
||||
// Plastic saving on the purge prism: keep only the fills a toolchange claimed.
|
||||
//
|
||||
// Called per layer from _plan_belt_purge(), after the real-purge marking and
|
||||
// BEFORE ensure_perimeters_infills_order() -- that pass force-overrides every
|
||||
// remaining fill on the prism (it is a dedicated flush object), so afterwards
|
||||
// everything looks claimed and nothing could be distinguished.
|
||||
size_t PrintObject::belt_drop_unclaimed_fills(Layer *layer, const std::function<bool(const ExtrusionEntity*)> &claimed)
|
||||
{
|
||||
if (layer == nullptr)
|
||||
return 0;
|
||||
size_t dropped = 0;
|
||||
for (size_t ri = 0; ri < layer->regions().size(); ++ri) {
|
||||
LayerRegion *lr = layer->get_region(ri);
|
||||
auto &ents = lr->fills.entities;
|
||||
ExtrusionEntitiesPtr keep;
|
||||
keep.reserve(ents.size());
|
||||
for (size_t i = 0; i < ents.size(); ++i) {
|
||||
if (claimed(ents[i])) {
|
||||
keep.emplace_back(ents[i]);
|
||||
} else {
|
||||
// Stash with its original index so the restore is exact.
|
||||
m_belt_dropped_fills.push_back(BeltDroppedFill{ layer, ri, i, ents[i] });
|
||||
++dropped;
|
||||
}
|
||||
}
|
||||
ents = std::move(keep);
|
||||
}
|
||||
return dropped;
|
||||
}
|
||||
|
||||
void PrintObject::belt_restore_dropped_fills()
|
||||
{
|
||||
if (m_belt_dropped_fills.empty())
|
||||
return;
|
||||
// Ascending index per (layer, region): inserting in that order lands every
|
||||
// entity back at its original position, because each insertion shifts only
|
||||
// the entries after it, which are themselves still to be inserted.
|
||||
std::stable_sort(m_belt_dropped_fills.begin(), m_belt_dropped_fills.end(),
|
||||
[](const BeltDroppedFill &a, const BeltDroppedFill &b) {
|
||||
if (a.layer != b.layer) return a.layer < b.layer;
|
||||
if (a.region_idx != b.region_idx) return a.region_idx < b.region_idx;
|
||||
return a.index < b.index;
|
||||
});
|
||||
for (const BeltDroppedFill &d : m_belt_dropped_fills) {
|
||||
auto &ents = d.layer->get_region(d.region_idx)->fills.entities;
|
||||
ents.insert(ents.begin() + std::min(d.index, ents.size()), d.entity);
|
||||
}
|
||||
m_belt_dropped_fills.clear();
|
||||
}
|
||||
|
||||
void PrintObject::belt_restore_truncated_layers()
|
||||
{
|
||||
if (m_belt_truncated_layers.empty())
|
||||
return;
|
||||
|
||||
m_layers.insert(m_layers.end(), m_belt_truncated_layers.begin(), m_belt_truncated_layers.end());
|
||||
m_belt_truncated_layers.clear();
|
||||
for (size_t i = 0; i < m_layers.size(); ++i) {
|
||||
m_layers[i]->lower_layer = i == 0 ? nullptr : m_layers[i - 1];
|
||||
m_layers[i]->upper_layer = i + 1 < m_layers.size() ? m_layers[i + 1] : nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,82 @@
|
||||
#include "BeltSliceStrategy.hpp"
|
||||
#include "Model.hpp"
|
||||
#include "BeltTransform.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
|
||||
#include <limits>
|
||||
#include <algorithm>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo,
|
||||
const PrintConfig &config,
|
||||
const ModelVolumePtrs &model_volumes,
|
||||
double *out_belt_min_z)
|
||||
{
|
||||
// 1. Belt rotation — the sole mesh-side belt transform (matching
|
||||
// BeltTransformPipeline::build_forward_transform). Only active in
|
||||
// belt-printer mode.
|
||||
bool has_rotation = false;
|
||||
if (config.belt_printer.value) {
|
||||
const Matrix3d rot = BeltTransformPipeline::build_rotation_matrix(config, &has_rotation);
|
||||
if (has_rotation) {
|
||||
Transform3d belt_xform = Transform3d::Identity();
|
||||
belt_xform.linear() = rot;
|
||||
trafo = belt_xform * trafo;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_rotation)
|
||||
return;
|
||||
|
||||
// 2. Z-shift — detect if the mesh clips below the build plate after the
|
||||
// transforms and lift it. Each mesh vertex must be brought into object space
|
||||
// via mv->get_matrix() before applying the full trafo (which is in object
|
||||
// space). Missing this on assemblies (where per-volume get_matrix() positions
|
||||
// each volume within the object) would compute min_z against mesh-local vertex
|
||||
// coordinates rather than object-space coordinates, so volumes translated along
|
||||
// the slicer's Z axis would be silently excluded from the bound check.
|
||||
|
||||
//
|
||||
// The lift is measured to the lowest point of the SUPPORT region, not of the
|
||||
// mesh: the belt floor (z = shear * u in this rotated frame, u the from-axis
|
||||
// coordinate) runs below every vertex, and under the leading end of an
|
||||
// overhang it lies below the lowest vertex by up to the overhang's length
|
||||
// times the shear. Supports have to reach that floor, and every support
|
||||
// generator works in layers at z >= 0, so z = 0 has to be the lowest floor
|
||||
// point under the footprint. The layers between it and the first vertex
|
||||
// come out empty, which belt slicing already tolerates (the bottom corner
|
||||
// of a tilted part is a point). Vertices on the belt have z == floor, so
|
||||
// for a part resting on the belt this is simply the floor at its leading
|
||||
// extreme, less the frame margin (see BeltTransformPipeline::frame_margin).
|
||||
BeltTransformPipeline::BeltFloorParams floor;
|
||||
const bool has_floor = BeltTransformPipeline::floor_shear(config, floor);
|
||||
double min_z = std::numeric_limits<double>::max();
|
||||
for (const ModelVolume *mv : model_volumes) {
|
||||
if (!mv->is_model_part()) continue;
|
||||
Transform3d vol_trafo = trafo * mv->get_matrix();
|
||||
const auto &its = mv->mesh().its;
|
||||
for (const stl_vertex &v : its.vertices) {
|
||||
Vec3d vm = v.cast<double>();
|
||||
Vec3d pt = vol_trafo * vm;
|
||||
min_z = std::min(min_z, pt.z());
|
||||
if (has_floor)
|
||||
min_z = std::min(min_z, floor.shear_factor * (floor.from_axis == 0 ? pt.x() : pt.y()));
|
||||
}
|
||||
}
|
||||
if (has_floor && min_z != std::numeric_limits<double>::max())
|
||||
min_z -= BeltTransformPipeline::frame_margin(floor);
|
||||
const double z_shift_val = (min_z < 0. && min_z != std::numeric_limits<double>::max()) ? -min_z : 0.;
|
||||
if (z_shift_val > 0.) {
|
||||
Transform3d z_shift = Transform3d::Identity();
|
||||
z_shift.matrix()(2, 3) = z_shift_val;
|
||||
trafo = z_shift * trafo;
|
||||
}
|
||||
// out_belt_min_z is only meaningful in belt mode.
|
||||
if (out_belt_min_z && config.belt_printer.value) {
|
||||
*out_belt_min_z = (min_z != std::numeric_limits<double>::max()) ? min_z : 0.;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "libslic3r.h"
|
||||
#include "Point.hpp"
|
||||
#include "BeltTransform.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "Model.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Belt printer pre-slice transform strategy.
|
||||
//
|
||||
// Composes, in order, the mesh transforms applied before slicing on a belt printer:
|
||||
// 1. Belt rotation (the sole mesh-side belt transform; shear & scale are a
|
||||
// g-code-side stage, see MachineFrameTransform)
|
||||
// 2. Per-object Z-shift that lifts the mesh so its slicing frame starts at the
|
||||
// belt below its footprint
|
||||
//
|
||||
// Isolates this belt-specific logic from the generic slicing pipeline in
|
||||
// PrintObjectSlice.cpp.
|
||||
class BeltSliceStrategy
|
||||
{
|
||||
public:
|
||||
// Apply the belt rotation + Z-shift to `trafo` in place. No-op when no belt
|
||||
// rotation is configured.
|
||||
//
|
||||
// out_belt_min_z (if non-null) receives the minimum mesh Z after the transforms.
|
||||
static void apply_preslice_transforms(Transform3d &trafo,
|
||||
const PrintConfig &config,
|
||||
const ModelVolumePtrs &model_volumes,
|
||||
double *out_belt_min_z = nullptr);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,153 @@
|
||||
#include "BeltTransform.hpp"
|
||||
#include "Model.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "Config.hpp"
|
||||
#include "Geometry.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "libslic3r.h"
|
||||
|
||||
#include <limits>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// ---- Matrix builders ------------------------------------------------------
|
||||
|
||||
Matrix3d BeltTransformPipeline::build_rotation_matrix(const PrintConfig &config, bool *has_rot_out)
|
||||
{
|
||||
BeltRotationAxis axis = config.belt_slice_rotation.value;
|
||||
double angle_deg = config.belt_slice_rotation_angle.value;
|
||||
bool active = axis != BeltRotationAxis::None && std::abs(angle_deg) > EPSILON;
|
||||
if (has_rot_out) *has_rot_out = active;
|
||||
if (!active)
|
||||
return Matrix3d::Identity();
|
||||
double angle_rad = Geometry::deg2rad(angle_deg);
|
||||
Vec3d unit_axis;
|
||||
switch (axis) {
|
||||
case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break;
|
||||
case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break;
|
||||
case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break;
|
||||
default: return Matrix3d::Identity();
|
||||
}
|
||||
return Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix();
|
||||
}
|
||||
|
||||
Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &config)
|
||||
{
|
||||
// Mesh-side belt transform: the rotation. (Shear & scale are a g-code-side
|
||||
// stage, not part of the mesh transform.)
|
||||
Transform3d combined = Transform3d::Identity();
|
||||
combined.linear() = build_rotation_matrix(config);
|
||||
return combined;
|
||||
}
|
||||
|
||||
// ---- Belt floor parameters ------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
// Belt floor in the rotated slicer frame: the image of z_machine = 0 under R.
|
||||
// R(+α, X): point (·, y, 0) → (·, cos α · y, sin α · y) ⇒ z = tan(α) · y_s
|
||||
// R(+α, Y): point (x, ·, 0) → (cos α · x, ·, -sin α · x) ⇒ z = -tan(α) · x_s
|
||||
// R(+α, Z): point (·, ·, 0) → (·, ·, 0); no tilt → no floor
|
||||
void belt_floor_shear(BeltRotationAxis rot_axis, double angle_rad, BeltTransformPipeline::BeltFloorParams &out)
|
||||
{
|
||||
double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad);
|
||||
switch (rot_axis) {
|
||||
case BeltRotationAxis::X:
|
||||
out.shear_factor = (std::abs(cos_a) > EPSILON) ? sin_a / cos_a : 0.;
|
||||
out.from_axis = 1; // Y
|
||||
break;
|
||||
case BeltRotationAxis::Y:
|
||||
out.shear_factor = (std::abs(cos_a) > EPSILON) ? -sin_a / cos_a : 0.;
|
||||
out.from_axis = 0; // X
|
||||
break;
|
||||
case BeltRotationAxis::Z:
|
||||
default:
|
||||
out.shear_factor = 0.0;
|
||||
out.from_axis = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Z of the belt floor directly under a point of the rotated (unshifted) frame.
|
||||
inline double belt_floor_z(const BeltTransformPipeline::BeltFloorParams &fp, const Vec3d &pt)
|
||||
{
|
||||
return fp.shear_factor * (fp.from_axis == 0 ? pt.x() : pt.y());
|
||||
}
|
||||
|
||||
|
||||
BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl(
|
||||
const PrintConfig &config, const BoundingBoxf3 &bb, double original_height)
|
||||
{
|
||||
BeltTransformPipeline::BeltHeightResult result;
|
||||
result.object_height = original_height;
|
||||
|
||||
// The mesh rotation (the sole mesh-side belt transform).
|
||||
const BeltRotationAxis rot_axis = config.belt_slice_rotation.value;
|
||||
const double rot_angle = config.belt_slice_rotation_angle.value;
|
||||
|
||||
bool has_rotation = rot_axis != BeltRotationAxis::None && std::abs(rot_angle) > EPSILON;
|
||||
if (!has_rotation)
|
||||
return result;
|
||||
|
||||
// Rotation path: sweep the 8 bbox corners through R to get the rotated height,
|
||||
// then derive the belt floor (the image of machine-Z = 0 under R).
|
||||
double angle_rad = Geometry::deg2rad(rot_angle);
|
||||
Vec3d unit_axis;
|
||||
switch (rot_axis) {
|
||||
case BeltRotationAxis::X: unit_axis = Vec3d::UnitX(); break;
|
||||
case BeltRotationAxis::Y: unit_axis = Vec3d::UnitY(); break;
|
||||
case BeltRotationAxis::Z: unit_axis = Vec3d::UnitZ(); break;
|
||||
default: unit_axis = Vec3d::UnitX(); break;
|
||||
}
|
||||
Matrix3d R = Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix();
|
||||
belt_floor_shear(rot_axis, angle_rad, result.floor_params);
|
||||
// The slicing frame starts at the lowest point of the support region: the
|
||||
// lowest belt-floor point under the footprint, not the lowest vertex. The
|
||||
// belt under the leading end of an overhang lies below every vertex of the
|
||||
// part, and supports have to be able to reach it (see
|
||||
// BeltSliceStrategy::apply_preslice_transforms for the exact vertex-scan
|
||||
// counterpart of this bbox estimate).
|
||||
double min_rz = std::numeric_limits<double>::max();
|
||||
double max_rz = std::numeric_limits<double>::lowest();
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
Vec3d c((i & 1) ? bb.max.x() : bb.min.x(),
|
||||
(i & 2) ? bb.max.y() : bb.min.y(),
|
||||
(i & 4) ? bb.max.z() : bb.min.z());
|
||||
Vec3d rc = R * c;
|
||||
double z = rc.z();
|
||||
min_rz = std::min(min_rz, z);
|
||||
max_rz = std::max(max_rz, z);
|
||||
min_rz = std::min(min_rz, belt_floor_z(result.floor_params, rc));
|
||||
}
|
||||
min_rz -= BeltTransformPipeline::frame_margin(result.floor_params);
|
||||
result.object_height = max_rz - min_rz;
|
||||
|
||||
result.floor_params.z_shift = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor(
|
||||
const PrintConfig &config, const BoundingBoxf3 &bbox, double original_height)
|
||||
{
|
||||
return compute_belt_height_and_floor_impl(config, bbox, original_height);
|
||||
}
|
||||
|
||||
bool BeltTransformPipeline::floor_shear(const PrintConfig &config, BeltFloorParams &out)
|
||||
{
|
||||
out = BeltFloorParams{};
|
||||
const BeltRotationAxis rot_axis = config.belt_slice_rotation.value;
|
||||
const double rot_angle = config.belt_slice_rotation_angle.value;
|
||||
if (rot_axis == BeltRotationAxis::None || std::abs(rot_angle) <= EPSILON)
|
||||
return false;
|
||||
belt_floor_shear(rot_axis, Geometry::deg2rad(rot_angle), out);
|
||||
return std::abs(out.shear_factor) > EPSILON;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,132 @@
|
||||
#pragma once
|
||||
|
||||
#include "libslic3r.h"
|
||||
#include "Point.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "Geometry.hpp"
|
||||
#include "Config.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class ModelObject;
|
||||
|
||||
// Shared belt-printer transform math.
|
||||
//
|
||||
// The pre-slice pipeline applied in PrintObjectSlice.cpp is:
|
||||
// trafo_out = z_shift * rotation * trafo_in
|
||||
//
|
||||
// Rotation is the sole mesh-side belt transform; shear & scale are applied
|
||||
// to the g-code instead (see MachineFrameTransform). This class provides the
|
||||
// building blocks so every call site uses the same implementation. z_shift is
|
||||
// object-dependent (computed from mesh vertex bounds) and is NOT included in
|
||||
// build_forward_transform(). The machine-frame shear/scale is derived directly
|
||||
// from the tilt angle in MachineFrameTransform and no longer lives here.
|
||||
//
|
||||
// Design note: this mesh-rotation approach replaced an earlier pre-shear
|
||||
// method (now removed). While that initial pre-shear method was instrumental
|
||||
// in getting belt printer slicing off the ground in the first place, its place is
|
||||
// in the past. A big thank you goes to the Unlayered3D team, who recommended
|
||||
// switching to a pre-slice rotation stage instead. Doing so keeps the slicing
|
||||
// operation isometric — no distortion of the sliced geometry — while the
|
||||
// non-orthogonal machine-axis compensation is confined to a g-code-side shear/scale
|
||||
// derived from the same tilt angle.
|
||||
//
|
||||
// This fixed a number of issues, including several issues noticed by hotcubcar
|
||||
// regarding adaptive infills not working, gyroid becoming anisotropic, and more
|
||||
// that were all mostly resolved as a result of the switch.
|
||||
//
|
||||
// This also means that the pre-slice rotation transform methodology can be used
|
||||
// more cleanly on non-belt printers.
|
||||
// - HarrierPigeon (Joseph Robertson)
|
||||
|
||||
class BeltTransformPipeline
|
||||
{
|
||||
public:
|
||||
// ---- Identity checks --------------------------------------------------
|
||||
|
||||
// Whether the G-code axis remap applies at all. The remap fields are only
|
||||
// offered in the belt printer group, so a value left in a profile must not
|
||||
// change a non-belt print: with belt mode off every belt-only key is a no-op.
|
||||
// This is the one place to widen if a non-belt use ever needs them.
|
||||
static bool axis_remap_enabled(const PrintConfig &config) { return config.belt_printer.value; }
|
||||
|
||||
static bool has_rotation(const PrintConfig &config)
|
||||
{
|
||||
return config.belt_slice_rotation.value != BeltRotationAxis::None &&
|
||||
std::abs(config.belt_slice_rotation_angle.value) > EPSILON;
|
||||
}
|
||||
|
||||
// Physical belt tilt derived from the slicing rotation — the single source of
|
||||
// truth for bed rendering, support gravity tilt and the bed-exclusion
|
||||
// projection. Returns the tilt magnitude in degrees split onto the X and Y
|
||||
// build-plate tilt axes according to the rotation axis:
|
||||
// rotation about X → tilt_x = angle (gantry tilts in the YZ plane)
|
||||
// rotation about Y → tilt_y = angle (gantry tilts in the XZ plane)
|
||||
// rotation about Z / None → no tilt (in-plane spin doesn't tilt the belt)
|
||||
// The magnitude uses abs(angle) so a negative rotation still reports a positive
|
||||
// physical tilt.
|
||||
struct PhysicalTilt { double tilt_x_deg = 0.; double tilt_y_deg = 0.; };
|
||||
|
||||
static PhysicalTilt physical_tilt(BeltRotationAxis axis, double angle_deg)
|
||||
{
|
||||
PhysicalTilt t;
|
||||
double mag = std::abs(angle_deg);
|
||||
switch (axis) {
|
||||
case BeltRotationAxis::X: t.tilt_x_deg = mag; break;
|
||||
case BeltRotationAxis::Y: t.tilt_y_deg = mag; break;
|
||||
default: break; // Z / None: no physical tilt
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
// ---- Matrix builders --------------------------------------------------
|
||||
|
||||
// Build the 3x3 rotation matrix from belt_slice_rotation* config.
|
||||
// Returns Identity if rotation axis is None or angle is ~0.
|
||||
// Also sets has_rot_out if non-null.
|
||||
static Matrix3d build_rotation_matrix(const PrintConfig &config, bool *has_rot_out = nullptr);
|
||||
|
||||
// Forward transform (the rotation) — the mesh-side belt transform that
|
||||
// BeltSliceStrategy applies and BeltBackTransform inverts.
|
||||
// Does NOT include the per-object Z-shift.
|
||||
static Transform3d build_forward_transform(const PrintConfig &config);
|
||||
|
||||
// ---- Belt floor parameters --------------------------------------------
|
||||
|
||||
struct BeltFloorParams {
|
||||
double shear_factor = 0.0;
|
||||
int from_axis = 1;
|
||||
double z_shift = 0.0;
|
||||
};
|
||||
|
||||
// Shear factor and from-axis of the belt floor in the rotated slicer frame
|
||||
// (z_floor = shear_factor * u, u = the from-axis coordinate), for the
|
||||
// rotation the config selects. z_shift is left at 0. Returns false (and
|
||||
// zero shear) when the config has no tilt.
|
||||
static bool floor_shear(const PrintConfig &config, BeltFloorParams &out);
|
||||
|
||||
// How far below the lowest belt-floor point under the footprint the slicing
|
||||
// frame starts, in slicing Z. A support column meeting the belt is wider at
|
||||
// its base than at its tip, so under a leading overhang the base reaches ahead
|
||||
// of the part along the belt, and the layers that trim it to the belt plane
|
||||
// lie below that lowest point: 10 mm along the belt.
|
||||
static double frame_margin(const BeltFloorParams &fp) { return 10. * std::abs(fp.shear_factor); }
|
||||
|
||||
// Result of computing belt height + floor params.
|
||||
struct BeltHeightResult {
|
||||
double object_height; // Effective object height after shear/scale
|
||||
BeltFloorParams floor_params;
|
||||
};
|
||||
|
||||
// Compute effective object height and belt floor parameters from config
|
||||
// and the object's bounding box. original_height is the input height
|
||||
// (bb.size().z() or model_object.max_z()).
|
||||
static BeltHeightResult compute_belt_height_and_floor(
|
||||
const PrintConfig &config, const BoundingBoxf3 &bbox,
|
||||
double original_height);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -3,6 +3,9 @@
|
||||
#include <cstdio>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/nowide/convert.hpp>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifdef WIN32
|
||||
#include <psapi.h>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "BoundingBox.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "libslic3r.h"
|
||||
#include <Eigen/Core>
|
||||
#include <algorithm>
|
||||
#include <assert.h>
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
#include "Exception.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <ostream>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
#include "BridgeDetector.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "ExPolygon.hpp"
|
||||
#include "Geometry.hpp"
|
||||
#include "libslic3r.h"
|
||||
#include "Polygon.hpp"
|
||||
#include "Line.hpp"
|
||||
#include "Polyline.hpp"
|
||||
#include "Point.hpp"
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <cstddef>
|
||||
#include <cmath>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
|
||||
@@ -9,7 +9,14 @@
|
||||
#include "PrincipalComponents2D.hpp"
|
||||
#include "libslic3r.h"
|
||||
#include "ExPolygon.hpp"
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <tuple>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
|
||||
+38
-1
@@ -1,18 +1,42 @@
|
||||
#include "BoundingBox.hpp"
|
||||
#include "BrimEarsPoint.hpp"
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "EdgeGrid.hpp"
|
||||
#include "ExPolygon.hpp"
|
||||
#include "Flow.hpp"
|
||||
#include "Geometry.hpp"
|
||||
#include "ExtrusionEntity.hpp"
|
||||
#include "Layer.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "ObjectID.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "Line.hpp"
|
||||
#include "MultiPoint.hpp"
|
||||
#include "Polyline.hpp"
|
||||
#include "Print.hpp"
|
||||
#include "PrintBase.hpp"
|
||||
#include "ShortestPath.hpp"
|
||||
#include "libslic3r.h"
|
||||
#include "PrintConfig.hpp"
|
||||
#include "MaterialType.hpp"
|
||||
#include "Model.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <tbb/parallel_for.h>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include "Config.hpp"
|
||||
#include "ExtrusionEntityCollection.hpp"
|
||||
#include "MultiMaterialSegmentation.hpp"
|
||||
#include "SurfaceCollection.hpp"
|
||||
#include "TriangleMesh.hpp"
|
||||
|
||||
#ifndef NDEBUG
|
||||
// #define BRIM_DEBUG_TO_SVG
|
||||
@@ -450,7 +474,9 @@ static ExPolygons outer_inner_brim_area(const Print& print,
|
||||
const bool use_brim_ears = object->config().brim_type == btPainted;
|
||||
const bool use_inner_brim_ears = (use_auto_brim_ears || use_brim_ears) && !object->config().brim_ears_outer_only.value;
|
||||
const bool has_inner_brim = brim_type == btInnerOnly || brim_type == btOuterAndInner || use_inner_brim_ears;
|
||||
const bool has_outer_brim = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btAutoBrim || use_auto_brim_ears || use_brim_ears;
|
||||
// btLeadingEdgeOnly is a belt-printer mode; on a flat bed there is no leading
|
||||
// edge, so it degrades to an ordinary outer brim rather than silently to none.
|
||||
const bool has_outer_brim = brim_type == btOuterOnly || brim_type == btOuterAndInner || brim_type == btAutoBrim || brim_type == btLeadingEdgeOnly || use_auto_brim_ears || use_brim_ears;
|
||||
coord_t ear_detection_length = scale_(object->config().brim_ears_detection_length.value);
|
||||
coordf_t brim_ears_max_angle = object->config().brim_ears_max_angle.value;
|
||||
//ORCA: Select brim base slices from EFC-compensated outline when enabled.
|
||||
@@ -865,6 +891,17 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_
|
||||
std::vector<unsigned int>& printExtruders,
|
||||
std::map<ObjectInstanceID, ExPolygons>* objectBrimAreasByInstanceOut)
|
||||
{
|
||||
// Belt printers never use the flat plate brim.
|
||||
//
|
||||
// With a tilted belt the brim has to be laid onto the belt plane over many layers,
|
||||
// which BeltBrim.cpp does during posSupportMaterial. With an untilted belt this
|
||||
// could in principle fall through and produce an ordinary brim, but it would never
|
||||
// reach the G-code: the plate brim is emitted out of skirt_brim_groups(), which
|
||||
// _make_skirt() builds, and that returns early for every belt printer. Running the
|
||||
// generator anyway would just burn time on geometry nobody prints.
|
||||
if (print.config().belt_printer.value)
|
||||
return;
|
||||
|
||||
std::map<ObjectInstanceID, ExPolygons> brimAreaMap;
|
||||
Flow flow = print.brim_flow();
|
||||
ExPolygons islands_area_ex = outer_inner_brim_area(print,
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
#include "ExPolygon.hpp"
|
||||
#include "ObjectID.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "Polygon.hpp"
|
||||
|
||||
#include<map>
|
||||
#include <utility>
|
||||
#include<vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#ifndef BRIMEARSPOINT_HPP
|
||||
#define BRIMEARSPOINT_HPP
|
||||
|
||||
#include <cstdlib>
|
||||
#include "libslic3r.h"
|
||||
#include <libslic3r/Point.hpp>
|
||||
#include <vector>
|
||||
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
#include "BuildVolume.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "ClipperUtils.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "Geometry/Circle.hpp"
|
||||
#include "ExtrusionEntity.hpp"
|
||||
#include "TriangleMesh.hpp"
|
||||
#include "Geometry/ConvexHull.hpp"
|
||||
#include "GCode/GCodeProcessor.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "libslic3r.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <vector>
|
||||
#include <cassert>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
#include <cstddef>
|
||||
#include <string_view>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
|
||||
@@ -5,9 +5,13 @@
|
||||
#include "Geometry/Circle.hpp"
|
||||
#include "Polygon.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "libslic3r.h"
|
||||
#include <admesh/stl.h>
|
||||
|
||||
#include <array>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -80,7 +84,7 @@ public:
|
||||
indexed_triangle_set bounding_mesh(bool scale=true) const;
|
||||
|
||||
// Center of the print bed, unscaled.
|
||||
Vec2d bed_center() const { return to_2d(m_bboxf.center()); }
|
||||
Vec2d bed_center() const { return get_extents(m_bed_shape).center(); }
|
||||
// Convex hull of polygon(), scaled.
|
||||
const Polygon& convex_hull() const { return m_convex_hull; }
|
||||
// Smallest enclosing circle of polygon(), scaled.
|
||||
|
||||
+1049
-413
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,20 @@
|
||||
#ifndef slic3r_CadDocument_hpp_
|
||||
#define slic3r_CadDocument_hpp_
|
||||
|
||||
#include "libslic3r/Point.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_Face.hxx>
|
||||
#include <TopoDS_Shape.hxx>
|
||||
#include <TopoDS_Wire.hxx>
|
||||
#include <cereal/cereal.hpp>
|
||||
#include <cereal/types/vector.hpp>
|
||||
#include <cereal/types/string.hpp>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <map>
|
||||
#include <cereal/types/map.hpp>
|
||||
#include <string>
|
||||
@@ -74,7 +78,7 @@ struct CadFeature {
|
||||
// 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).
|
||||
// snapshots is cheap. Saved with the recipe as a BRep string (see save()/load()).
|
||||
TopoDS_Shape imported_solid;
|
||||
|
||||
// Non-destructive placement transform for imported_regions (Text/SVG),
|
||||
@@ -112,6 +116,25 @@ struct CadFeature {
|
||||
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
|
||||
// Several picked edges dressed in ONE operation, all resolved against the same body, so
|
||||
// the ids cannot drift the way they do across a chain of single-edge features. When set,
|
||||
// dressup_edge holds the first of them: a build that predates the list still dresses that
|
||||
// one edge instead of falling back to the whole face group.
|
||||
std::vector<int> dressup_edges;
|
||||
// The edges this dress-up targets: the list, else the single edge, else none (face group).
|
||||
// Text feature: a Sketch whose imported_regions were vectorised from this string in this font
|
||||
// (a WxFontUtils descriptor, bold/italic included) at this cap height in mm. The regions are
|
||||
// what gets built — they are saved too, so the project opens on a machine without the font —
|
||||
// and these three are what an edit reopens the Text dialog with. Empty = not a text feature.
|
||||
std::string text_string;
|
||||
std::string text_font;
|
||||
double text_height{0.0};
|
||||
bool is_text() const { return !text_string.empty(); }
|
||||
std::vector<int> dressup_edge_ids() const {
|
||||
if (!dressup_edges.empty()) return dressup_edges;
|
||||
if (dressup_edge >= 0) return { dressup_edge };
|
||||
return {};
|
||||
}
|
||||
|
||||
// Hole params (positioned circular cut into the current body)
|
||||
double hole_diameter{5};
|
||||
@@ -130,12 +153,21 @@ struct CadFeature {
|
||||
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_radius{5}; // MAJOR (nominal) radius when thread_major_nominal,
|
||||
// else the legacy reference radius (see below)
|
||||
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_depth{1}; // radial depth of the thread profile
|
||||
bool thread_internal{false}; // false = external: the thread goes on the target body
|
||||
// (or on a standalone rod when there is none);
|
||||
// true = tapped bore cut into the target body
|
||||
// How thread_radius is read. true (every thread made since): it is the nominal MAJOR
|
||||
// radius, for both kinds — an internal thread bores to R - depth and grooves out to R, an
|
||||
// external one is a rod of radius R with its groove cut in to R - depth, so an M6 is 6 mm
|
||||
// across its crests either way. false (older recipes, kept bit-for-bit): the ridge was
|
||||
// added OUTSIDE R, so an internal thread given the minor diameter came out undersized and
|
||||
// an external one given the major diameter came out oversized.
|
||||
bool thread_major_nominal{false};
|
||||
double thread_x{0}; // axis position on the plane (u/x axis)
|
||||
double thread_y{0}; // axis position on the plane (v/y axis)
|
||||
|
||||
@@ -152,6 +184,9 @@ struct CadFeature {
|
||||
// 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
|
||||
// A Line of the profile sketch to revolve about instead (index into its entities: a
|
||||
// centerline, usually construction, or an edge of the profile itself); -1 = revolve_axis.
|
||||
int revolve_axis_entity{-1};
|
||||
|
||||
// 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
|
||||
@@ -174,6 +209,12 @@ struct CadFeature {
|
||||
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)
|
||||
// How a circular pattern spreads its copies over pattern_angle. true (every pattern made
|
||||
// since): a full turn is split into `count` equal steps, anything less is spanned end to end,
|
||||
// first copy at 0 and last at pattern_angle — the way a pattern along a curve spans its
|
||||
// curve. false (older recipes, kept as they were built): always angle / count, so 90° with 3
|
||||
// copies stopped at 60°.
|
||||
bool pattern_inclusive{false};
|
||||
|
||||
// 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
|
||||
@@ -186,6 +227,16 @@ struct CadFeature {
|
||||
// BEFORE geometry runs. Empty (the common case) means the feature uses its literal fields.
|
||||
std::map<std::string, std::string> expr;
|
||||
|
||||
// Transient, never serialized. A body is referred to above by its INDEX, and an index is
|
||||
// only meaningful against the history it was taken in: delete, reorder or disable a
|
||||
// feature that makes a body and every later index points at a different body. So recompute
|
||||
// records, for each body field, the identity of the body it resolved to — the feature that
|
||||
// made it, and which of that feature's bodies it is — and when the history changes the
|
||||
// fields are re-pointed from those identities (see CadDocument::recompute).
|
||||
struct BodyId { int src{-1}; int ord{0}; };
|
||||
std::vector<BodyId> body_ref_ids;
|
||||
bool body_refs_pending{false};
|
||||
|
||||
// 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;
|
||||
@@ -357,10 +408,62 @@ struct CadFeature {
|
||||
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);
|
||||
coordsys_face_kind, coordsys_face_edges,
|
||||
thread_major_nominal, pattern_inclusive,
|
||||
dressup_edges,
|
||||
text_string, text_font, text_height,
|
||||
revolve_axis_entity);
|
||||
}
|
||||
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,
|
||||
thread_major_nominal, pattern_inclusive,
|
||||
dressup_edges,
|
||||
text_string, text_font, text_height,
|
||||
revolve_axis_entity);
|
||||
imported_solid = brep_from_string(brep);
|
||||
}
|
||||
// The pre-framing (v4) layout, FROZEN. A v4 recipe is one flat stream with no per-feature
|
||||
// length, so it can only be read with exactly the field list it was written with; reading it
|
||||
// through load() above breaks the moment a field is appended there (every field added since
|
||||
// v5 is appended ONLY above, never here).
|
||||
template<class Archive>
|
||||
void load_flat_v4(Archive& ar) {
|
||||
std::string brep;
|
||||
ar(type, name, enabled, shape, plane, width, height, radius,
|
||||
profile, entities, constraints, entity_constraints, imported_regions,
|
||||
@@ -399,6 +502,7 @@ struct CadFeature {
|
||||
coordsys_face_kind, coordsys_face_edges);
|
||||
imported_solid = brep_from_string(brep);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Serialize a TopoDS_Shape to/from a BRep string for cereal persistence.
|
||||
@@ -436,6 +540,13 @@ public:
|
||||
// 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;
|
||||
// Can a feature expression drive this numeric field? The single allow-list the evaluator
|
||||
// uses, so a caller can refuse a name before it breaks the next recompute.
|
||||
static bool is_bindable_field(const std::string& field);
|
||||
// Does a feature of this type leave a body behind? Sketches, datums, the helix curve and
|
||||
// Project (which emits sketch entities) do not. The one answer recompute(), the rollback
|
||||
// rule and the GUI's preview all use.
|
||||
static bool produces_body(CadFeatureType t);
|
||||
// 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;
|
||||
@@ -455,8 +566,16 @@ public:
|
||||
|
||||
// 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.
|
||||
// world 0) — for a NEW document. It is saved with the recipe: sketches bake it into their
|
||||
// planes while datum planes add it when they are resolved, so a project reopened on another
|
||||
// printer must keep the origin it was made with or its datums move and its sketches do not.
|
||||
Vec3d modeling_origin{Vec3d::Zero()};
|
||||
bool origin_from_recipe{false}; // modeling_origin came from the loaded project
|
||||
// Weld sketch endpoints within kSketchJoinTol (the "Auto-close sketch loops" preference, taken
|
||||
// when the document is started). A property of the DOCUMENT, saved with it: as a machine-wide
|
||||
// preference it made one project rebuild into a closed solid on one computer and an open
|
||||
// loop on another. recompute() pushes it into the kernel.
|
||||
bool auto_close_loops{true};
|
||||
|
||||
// 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
|
||||
@@ -509,8 +628,10 @@ public:
|
||||
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_fillet(double radius, const std::vector<int>& edge_ids, 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_chamfer(double distance, const std::vector<int>& edge_ids, const std::string& name);
|
||||
int add_hole(double diameter, double depth, bool through,
|
||||
double x, double y, const SketchPlane& plane,
|
||||
const std::string& name);
|
||||
@@ -703,6 +824,9 @@ public:
|
||||
// an Extrude, its sketch_ref are preserved from the original).
|
||||
bool remove_feature(int index);
|
||||
bool move_feature(int index, int delta);
|
||||
// Show or hide a feature. Transactional like the others, and it keeps body and datum-plane
|
||||
// references pointing at the same objects, which a bare `enabled` flip does not.
|
||||
bool set_feature_enabled(int index, bool enabled);
|
||||
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 ->
|
||||
@@ -714,6 +838,9 @@ public:
|
||||
// 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.
|
||||
// The solid body the closed profile of sketch `sketch_ref` lies on or touches, or -1. Drives
|
||||
// the Extrude default: a profile drawn on a body joins it, one in free space is a new body.
|
||||
int body_touching_sketch(int sketch_ref) const;
|
||||
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
|
||||
@@ -721,6 +848,16 @@ public:
|
||||
bool preview(const CadFeature& candidate, TriangleMesh& out_mesh,
|
||||
std::vector<TriangleMesh>& out_body_meshes, std::string& err) const;
|
||||
|
||||
// The faces of the current bodies that features[index] made, as (body, face id) pairs: what
|
||||
// the Design tab highlights when that feature is selected. A face counts when it lies on the
|
||||
// model's boundary, facing the same way, right after the feature and not right before it, so
|
||||
// a face a later feature trimmed still belongs to the one that made it. Positions are
|
||||
// compared, no history is kept: a face a later feature rebuilt in place stays the earlier
|
||||
// feature's. A feature with no face left of its own (a Boolean union, or one whose faces a
|
||||
// later feature removed) answers with every face of each body it changed. Empty for a feature
|
||||
// that leaves no body, a hidden one, or a history that no longer rebuilds up to it.
|
||||
std::vector<std::pair<int, int>> faces_made_by(int index) 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
|
||||
@@ -737,6 +874,13 @@ private:
|
||||
// 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;
|
||||
// The parametric pass every replay starts with: evaluate the variables and write each
|
||||
// feature's expression bindings into its numeric fields.
|
||||
void bind_expressions();
|
||||
// One step of the replay: route features[fi] into `built` and stamp the bodies it created
|
||||
// with fi. A hidden feature, a sketch, a helix or a datum leaves `built` alone. Throws on
|
||||
// failure.
|
||||
void replay_feature(size_t fi, std::vector<CadBody>& built);
|
||||
// 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,
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
#include "libslic3r/CAD/GeometryEngine.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/I18N.hpp"
|
||||
|
||||
#include <BRepMesh_IncrementalMesh.hxx>
|
||||
#include <BRep_Tool.hxx>
|
||||
#include <BRepAdaptor_Surface.hxx>
|
||||
#include <BRepLProp_SLProps.hxx>
|
||||
#include <TopAbs_ShapeEnum.hxx>
|
||||
#include <cstddef>
|
||||
#include <algorithm>
|
||||
#include <GeomAbs_SurfaceType.hxx>
|
||||
#include <TopAbs_Orientation.hxx>
|
||||
#include <Standard_Handle.hxx>
|
||||
#include <cstdint>
|
||||
#include <Poly_Triangle.hxx>
|
||||
#include <GeomAbs_CurveType.hxx>
|
||||
#include <gp_Cylinder.hxx>
|
||||
#include <BRepFilletAPI_MakeFillet.hxx>
|
||||
#include <BRepFilletAPI_MakeChamfer.hxx>
|
||||
#include <math.h>
|
||||
#include <gp_Mat.hxx>
|
||||
#include <stdexcept>
|
||||
#include <TopExp_Explorer.hxx>
|
||||
#include <TopoDS.hxx>
|
||||
@@ -14,11 +28,11 @@
|
||||
#include <TopoDS_Edge.hxx>
|
||||
#include <TopExp.hxx>
|
||||
#include <TopTools.hxx>
|
||||
#include <TopTools_IndexedMapOfShape.hxx>
|
||||
#include <TopTools_ListOfShape.hxx>
|
||||
#include <TopTools_IndexedDataMapOfShapeListOfShape.hxx>
|
||||
#include <TopTools_ShapeMapHasher.hxx>
|
||||
#include <NCollection_IndexedDataMap.hxx>
|
||||
#include <NCollection_IndexedMap.hxx>
|
||||
#include <NCollection_List.hxx>
|
||||
#include <Poly_Triangulation.hxx>
|
||||
#include <gp_Ax2.hxx>
|
||||
#include <gp_Dir.hxx>
|
||||
#include <gp_Pnt.hxx>
|
||||
#include <BRepGProp.hxx>
|
||||
@@ -38,14 +52,20 @@
|
||||
#include <BRepBuilderAPI_MakeSolid.hxx>
|
||||
#include <BRep_Builder.hxx>
|
||||
#include <TopoDS_Shell.hxx>
|
||||
#include <TopoDS_Solid.hxx>
|
||||
#include <TopoDS_Vertex.hxx>
|
||||
#include <ShapeUpgrade_UnifySameDomain.hxx>
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
using ShapeIndexMap = NCollection_IndexedMap<TopoDS_Shape, TopTools_ShapeMapHasher>;
|
||||
|
||||
// ---- STEP import (B-rep, not mesh) ----
|
||||
std::vector<TopoDS_Shape> GeometryEngine::read_step_solids(const std::string& path, std::string& err)
|
||||
{
|
||||
@@ -54,19 +74,19 @@ std::vector<TopoDS_Shape> GeometryEngine::read_step_solids(const std::string& pa
|
||||
try {
|
||||
STEPControl_Reader reader;
|
||||
if (reader.ReadFile(path.c_str()) != IFSelect_RetDone) {
|
||||
err = "cannot read STEP file";
|
||||
err = _u8L("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; }
|
||||
if (shape.IsNull()) { err = _u8L("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";
|
||||
err = *e.what() ? e.what() : _u8L("OCCT failed to read STEP");
|
||||
out.clear();
|
||||
}
|
||||
return out;
|
||||
@@ -96,7 +116,7 @@ TopoDS_Shape GeometryEngine::mesh_to_brep(const indexed_triangle_set& its,
|
||||
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");
|
||||
throw std::runtime_error(_u8L("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.
|
||||
@@ -131,8 +151,8 @@ TopoDS_Shape GeometryEngine::mesh_to_brep(const indexed_triangle_set& its,
|
||||
}
|
||||
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)");
|
||||
throw std::runtime_error(_u8L("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());
|
||||
@@ -224,30 +244,6 @@ TopoDS_Shape GeometryEngine::mesh_to_brep(const indexed_triangle_set& its,
|
||||
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*/)
|
||||
@@ -275,21 +271,21 @@ FaceGroup GeometryEngine::classify_face(const TopoDS_Face& face, const TopoDS_Sh
|
||||
|
||||
std::vector<TopoDS_Edge> GeometryEngine::collect_edges(const TopoDS_Shape& solid, FaceGroup target)
|
||||
{
|
||||
// Each edge ONCE. An explorer visits a shared edge from both of its faces, so walking one
|
||||
// handed every edge to the fillet twice; the de-duplicated map is also what edge_by_index
|
||||
// numbers edges by.
|
||||
std::vector<TopoDS_Edge> result;
|
||||
NCollection_IndexedDataMap<TopoDS_Shape, NCollection_List<TopoDS_Shape>, TopTools_ShapeMapHasher> edgeFaceMap;
|
||||
TopExp::MapShapesAndAncestors(solid, TopAbs_EDGE, TopAbs_FACE, edgeFaceMap);
|
||||
if (target == FaceGroup::All) {
|
||||
for (TopExp_Explorer exp(solid, TopAbs_EDGE); exp.More(); exp.Next())
|
||||
result.push_back(TopoDS::Edge(exp.Current()));
|
||||
for (int i = 1; i <= edgeFaceMap.Extent(); ++i)
|
||||
result.push_back(TopoDS::Edge(edgeFaceMap.FindKey(i)));
|
||||
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);
|
||||
for (int ei = 1; ei <= edgeFaceMap.Extent(); ++ei) {
|
||||
const TopoDS_Edge& edge = TopoDS::Edge(edgeFaceMap.FindKey(ei));
|
||||
const NCollection_List<TopoDS_Shape>& faces = edgeFaceMap.FindFromIndex(ei);
|
||||
|
||||
bool include = false;
|
||||
for (auto it = faces.begin(); it != faces.end(); ++it) {
|
||||
@@ -325,12 +321,14 @@ std::vector<TopoDS_Edge> GeometryEngine::collect_edges(const TopoDS_Shape& solid
|
||||
|
||||
// ---- Fillet/Chamfer ----
|
||||
|
||||
// All four dress-up entry points fail the same way — with a reason — instead of two of them
|
||||
// handing the solid back unchanged, which recompute then reported as a success.
|
||||
TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radius, FaceGroup faces)
|
||||
{
|
||||
if (radius <= 0.001) return solid;
|
||||
if (radius <= 0.001) throw std::runtime_error("the fillet radius must be greater than 0");
|
||||
|
||||
std::vector<TopoDS_Edge> edges = collect_edges(solid, faces);
|
||||
if (edges.empty()) return solid;
|
||||
if (edges.empty()) throw std::runtime_error("no edges to fillet in that group");
|
||||
|
||||
BRepFilletAPI_MakeFillet fillet(solid);
|
||||
for (const auto& edge : edges)
|
||||
@@ -340,53 +338,67 @@ TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radi
|
||||
// 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");
|
||||
if (!fillet.IsDone()) throw std::runtime_error(_u8L("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;
|
||||
if (distance <= 0.001) throw std::runtime_error("the chamfer distance must be greater than 0");
|
||||
|
||||
std::vector<TopoDS_Edge> edges = collect_edges(solid, faces);
|
||||
if (edges.empty()) return solid;
|
||||
if (edges.empty()) throw std::runtime_error("no edges to chamfer in that group");
|
||||
|
||||
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");
|
||||
if (!chamfer.IsDone()) throw std::runtime_error(_u8L("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();
|
||||
return apply_fillet(solid, radius, std::vector<int>{ edge_id });
|
||||
}
|
||||
|
||||
TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double distance, int edge_id)
|
||||
{
|
||||
if (distance <= 0.001) return solid;
|
||||
return apply_chamfer(solid, distance, std::vector<int>{ edge_id });
|
||||
}
|
||||
|
||||
TopoDS_Edge edge = edge_by_index(solid, edge_id);
|
||||
if (edge.IsNull()) throw std::runtime_error("apply_chamfer: invalid edge id");
|
||||
TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radius, const std::vector<int>& edge_ids)
|
||||
{
|
||||
if (radius <= 0.001) throw std::runtime_error("the fillet radius must be greater than 0");
|
||||
if (edge_ids.empty()) throw std::runtime_error("apply_fillet: no edge picked");
|
||||
|
||||
BRepFilletAPI_MakeChamfer mk(solid);
|
||||
mk.Add(distance, edge);
|
||||
BRepFilletAPI_MakeFillet mk(solid);
|
||||
for (int id : edge_ids) {
|
||||
TopoDS_Edge edge = edge_by_index(solid, id);
|
||||
if (edge.IsNull()) throw std::runtime_error("apply_fillet: invalid edge id");
|
||||
mk.Add(radius, edge);
|
||||
}
|
||||
mk.Build();
|
||||
|
||||
if (!mk.IsDone()) throw std::runtime_error("apply_chamfer: OCCT chamfer failed");
|
||||
if (!mk.IsDone()) throw std::runtime_error(_u8L("apply_fillet: OCCT fillet failed"));
|
||||
return mk.Shape();
|
||||
}
|
||||
|
||||
TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double distance, const std::vector<int>& edge_ids)
|
||||
{
|
||||
if (distance <= 0.001) throw std::runtime_error("the chamfer distance must be greater than 0");
|
||||
if (edge_ids.empty()) throw std::runtime_error("apply_chamfer: no edge picked");
|
||||
|
||||
BRepFilletAPI_MakeChamfer mk(solid);
|
||||
for (int id : edge_ids) {
|
||||
TopoDS_Edge edge = edge_by_index(solid, id);
|
||||
if (edge.IsNull()) throw std::runtime_error("apply_chamfer: invalid edge id");
|
||||
mk.Add(distance, edge);
|
||||
}
|
||||
mk.Build();
|
||||
|
||||
if (!mk.IsDone()) throw std::runtime_error(_u8L("apply_chamfer: OCCT chamfer failed"));
|
||||
return mk.Shape();
|
||||
}
|
||||
|
||||
@@ -509,18 +521,6 @@ GeometryEngine::MassProps GeometryEngine::mass_properties(const TopoDS_Shape& sh
|
||||
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)
|
||||
@@ -553,7 +553,7 @@ std::vector<TopoDS_Face> GeometryEngine::faces_of(const TopoDS_Shape& shape)
|
||||
|
||||
std::vector<TopoDS_Edge> GeometryEngine::edges_of(const TopoDS_Shape& shape)
|
||||
{
|
||||
TopTools_IndexedMapOfShape map;
|
||||
ShapeIndexMap map;
|
||||
TopExp::MapShapes(shape, TopAbs_EDGE, map); // same order as edge_by_index
|
||||
std::vector<TopoDS_Edge> out;
|
||||
out.reserve(map.Extent());
|
||||
@@ -565,7 +565,7 @@ std::vector<TopoDS_Edge> GeometryEngine::edges_of(const TopoDS_Shape& shape)
|
||||
std::vector<TopoDS_Edge> GeometryEngine::edges_of_face(const TopoDS_Face& face)
|
||||
{
|
||||
std::vector<TopoDS_Edge> result;
|
||||
TopTools_IndexedMapOfShape map;
|
||||
ShapeIndexMap map;
|
||||
TopExp::MapShapes(face, TopAbs_EDGE, map);
|
||||
for (int i = 1; i <= map.Extent(); ++i)
|
||||
result.push_back(TopoDS::Edge(map(i)));
|
||||
@@ -595,6 +595,27 @@ std::vector<Vec3d> GeometryEngine::sample_edge_world(const TopoDS_Edge& edge, do
|
||||
return pts;
|
||||
}
|
||||
|
||||
std::vector<std::vector<Vec3d>> GeometryEngine::display_edges(const TopoDS_Shape& shape, double chord_tol)
|
||||
{
|
||||
NCollection_IndexedDataMap<TopoDS_Shape, NCollection_List<TopoDS_Shape>, TopTools_ShapeMapHasher> faces_of_edge;
|
||||
TopExp::MapShapesAndAncestors(shape, TopAbs_EDGE, TopAbs_FACE, faces_of_edge);
|
||||
std::vector<std::vector<Vec3d>> out;
|
||||
for (int i = 1; i <= faces_of_edge.Extent(); ++i) {
|
||||
const TopoDS_Edge& edge = TopoDS::Edge(faces_of_edge.FindKey(i));
|
||||
if (BRep_Tool::Degenerated(edge))
|
||||
continue;
|
||||
bool seam = false;
|
||||
for (NCollection_List<TopoDS_Shape>::Iterator it(faces_of_edge.FindFromIndex(i)); it.More() && !seam; it.Next())
|
||||
seam = BRep_Tool::IsClosed(edge, TopoDS::Face(it.Value()));
|
||||
if (seam)
|
||||
continue;
|
||||
std::vector<Vec3d> pts = sample_edge_world(edge, chord_tol);
|
||||
if (pts.size() >= 2)
|
||||
out.push_back(std::move(pts));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Vec3d GeometryEngine::face_centroid_world(const TopoDS_Face& face)
|
||||
{
|
||||
GProp_GProps props;
|
||||
@@ -688,14 +709,14 @@ bool GeometryEngine::face_plane_bounds(const TopoDS_Face& face, const Vec3d& ori
|
||||
|
||||
int GeometryEngine::edge_count(const TopoDS_Shape& shape)
|
||||
{
|
||||
TopTools_IndexedMapOfShape map;
|
||||
ShapeIndexMap 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;
|
||||
ShapeIndexMap map;
|
||||
TopExp::MapShapes(shape, TopAbs_EDGE, map);
|
||||
if (index < 0 || index >= map.Extent())
|
||||
return TopoDS_Edge();
|
||||
@@ -704,7 +725,7 @@ TopoDS_Edge GeometryEngine::edge_by_index(const TopoDS_Shape& shape, int index)
|
||||
|
||||
int GeometryEngine::edge_index_of(const TopoDS_Shape& shape, const TopoDS_Edge& edge)
|
||||
{
|
||||
TopTools_IndexedMapOfShape map;
|
||||
ShapeIndexMap map;
|
||||
TopExp::MapShapes(shape, TopAbs_EDGE, map);
|
||||
int idx = map.FindIndex(edge);
|
||||
return (idx > 0) ? (idx - 1) : -1;
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
#ifndef slic3r_GeometryEngine_hpp_
|
||||
#define slic3r_GeometryEngine_hpp_
|
||||
|
||||
#include "libslic3r/Point.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_Shape.hxx>
|
||||
#include <array>
|
||||
#include <TopoDS_Face.hxx>
|
||||
#include <TopoDS_Edge.hxx>
|
||||
#include <vector>
|
||||
@@ -17,43 +13,11 @@
|
||||
|
||||
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).
|
||||
@@ -119,11 +83,15 @@ public:
|
||||
FaceGroup faces = FaceGroup::All);
|
||||
static TopoDS_Shape apply_chamfer(const TopoDS_Shape& solid, double distance,
|
||||
int edge_id);
|
||||
// Several edges in one operation, all ids resolved against `solid`.
|
||||
static TopoDS_Shape apply_fillet(const TopoDS_Shape& solid, double radius,
|
||||
const std::vector<int>& edge_ids);
|
||||
static TopoDS_Shape apply_chamfer(const TopoDS_Shape& solid, double distance,
|
||||
const std::vector<int>& edge_ids);
|
||||
|
||||
static TriangleMesh tessellate(const TopoDS_Shape& shape,
|
||||
double linear_deflection = 0.01,
|
||||
double linear_deflection = 0.003,
|
||||
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
|
||||
@@ -143,6 +111,10 @@ public:
|
||||
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);
|
||||
// The edges a viewer draws over a body, each as a polyline: every edge of the shape once,
|
||||
// without degenerate edges (a cone apex) and without the seam of a closed surface (the line
|
||||
// down a cylinder's side), which is where OCCT closes the parameter space, not a real edge.
|
||||
static std::vector<std::vector<Vec3d>> display_edges(const TopoDS_Shape& shape, double chord_tol);
|
||||
// 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);
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
#include "libslic3r/CAD/SketchConstraints.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include <Eigen/Core>
|
||||
#include <Eigen/Dense>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define slic3r_SketchConstraints_hpp_
|
||||
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include <Eigen/Core>
|
||||
#include <vector>
|
||||
#include <Eigen/Dense>
|
||||
|
||||
|
||||
+330
-114
@@ -1,13 +1,27 @@
|
||||
#include "libslic3r/CAD/SketchEngine.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/I18N.hpp"
|
||||
|
||||
#include <Standard_Handle.hxx>
|
||||
#include <GeomAbs_SurfaceType.hxx>
|
||||
#include <GeomAbs_JoinType.hxx>
|
||||
#include <TopAbs_ShapeEnum.hxx>
|
||||
#include <Standard_TypeDef.hxx>
|
||||
#include <TopAbs_Orientation.hxx>
|
||||
#include <Poly_Triangle.hxx>
|
||||
#include <TopAbs_State.hxx>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
|
||||
#include <BRepBuilderAPI_MakeWire.hxx>
|
||||
#include <BRepBuilderAPI_MakeVertex.hxx>
|
||||
#include <BRepBuilderAPI_MakeEdge.hxx>
|
||||
#include <BRepBuilderAPI_MakeFace.hxx>
|
||||
#include <BRepCheck_Analyzer.hxx>
|
||||
#include <BRepAdaptor_Curve.hxx>
|
||||
#include <BRepGProp.hxx>
|
||||
#include <GProp_GProps.hxx>
|
||||
#include <BRepClass_FaceClassifier.hxx>
|
||||
@@ -16,9 +30,7 @@
|
||||
#include <GC_MakeArcOfEllipse.hxx>
|
||||
#include <Geom_TrimmedCurve.hxx>
|
||||
#include <Geom_BSplineCurve.hxx>
|
||||
#include <TColgp_Array1OfPnt.hxx>
|
||||
#include <TColStd_Array1OfReal.hxx>
|
||||
#include <TColStd_Array1OfInteger.hxx>
|
||||
#include <NCollection_Array1.hxx>
|
||||
#include <gp_Circ.hxx>
|
||||
#include <gp_Elips.hxx>
|
||||
#include <gp_Ax2.hxx>
|
||||
@@ -47,29 +59,29 @@
|
||||
#include <TopoDS_Wire.hxx>
|
||||
#include <GeomAPI_IntCS.hxx>
|
||||
#include <map>
|
||||
#include <atomic>
|
||||
#include <math.h>
|
||||
#include <tuple>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
|
||||
class Geom_TrimmedCurve;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Single source of truth for the weld tolerance the viewport and the kernel share.
|
||||
// Defaults ON so headless/kernel-only callers keep welding; the GUI pushes the
|
||||
// "auto_close_sketch_loops" preference in via set_sketch_auto_close().
|
||||
static bool s_auto_close = true;
|
||||
// Single source of truth for the weld tolerance the viewport and the kernel share. Defaults ON
|
||||
// so headless/kernel-only callers keep welding. It is the DOCUMENT's setting
|
||||
// (CadDocument::auto_close_loops, saved with the recipe), pushed in by CadDocument::recompute:
|
||||
// the same project must rebuild into the same solid on every machine. Atomic because the GUI
|
||||
// rebuilds on a worker thread while the viewport reads it.
|
||||
static std::atomic<bool> s_auto_close{true};
|
||||
|
||||
double sketch_join_tol() { return s_auto_close ? kSketchJoinTol : 0.0; }
|
||||
void set_sketch_auto_close(bool on) { s_auto_close = on; }
|
||||
double sketch_join_tol() { return s_auto_close.load() ? kSketchJoinTol : 0.0; }
|
||||
void set_sketch_auto_close(bool on) { s_auto_close.store(on); }
|
||||
|
||||
// ---- SketchPlane ----
|
||||
|
||||
gp_Pln SketchPlane::to_occt() const
|
||||
{
|
||||
gp_Pnt o(origin.x(), origin.y(), origin.z());
|
||||
gp_Dir n(normal.x(), normal.y(), normal.z());
|
||||
gp_Dir x(x_axis.x(), x_axis.y(), x_axis.z());
|
||||
return gp_Pln(gp_Ax3(o, n, x));
|
||||
}
|
||||
|
||||
SketchPlane SketchPlane::from_face(const TopoDS_Face& face)
|
||||
{
|
||||
SketchPlane sp;
|
||||
@@ -124,30 +136,10 @@ Vec3d SketchPlane::to_world(const Vec2d& pt) const
|
||||
|
||||
// ---- SketchProfile ----
|
||||
|
||||
bool SketchProfile::is_closed(double tolerance) const
|
||||
{
|
||||
if (points.size() < 3) return false;
|
||||
return (points.front() - points.back()).norm() < tolerance;
|
||||
}
|
||||
|
||||
bool SketchProfile::try_close(double tolerance)
|
||||
{
|
||||
if (is_closed(tolerance)) {
|
||||
closed = true;
|
||||
return true;
|
||||
}
|
||||
if (points.size() < 2) return false;
|
||||
if ((points.front() - points.back()).norm() < tolerance) {
|
||||
closed = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
TopoDS_Wire SketchProfile::to_occt_wire(const SketchPlane& plane) const
|
||||
{
|
||||
if (points.size() < 2)
|
||||
throw std::runtime_error("Profile has fewer than 2 points");
|
||||
throw std::runtime_error(_u8L("Profile has fewer than 2 points"));
|
||||
|
||||
BRepBuilderAPI_MakeWire builder;
|
||||
for (size_t i = 0; i < points.size(); ++i) {
|
||||
@@ -159,34 +151,36 @@ TopoDS_Wire SketchProfile::to_occt_wire(const SketchPlane& plane) const
|
||||
}
|
||||
builder.Build();
|
||||
if (!builder.IsDone())
|
||||
throw std::runtime_error("Failed to build wire from profile");
|
||||
throw std::runtime_error(_u8L("Failed to build wire from profile"));
|
||||
return builder.Wire();
|
||||
}
|
||||
|
||||
// ---- SketchEngine ----
|
||||
|
||||
TopoDS_Shape SketchEngine::make_prism(const TopoDS_Shape& base, const gp_Vec& vec)
|
||||
{
|
||||
if (vec.Magnitude() < 1e-9) throw std::runtime_error(_u8L("extrude depth is zero"));
|
||||
BRepPrimAPI_MakePrism prism(base, vec);
|
||||
if (!prism.IsDone()) throw std::runtime_error(_u8L("extrude failed"));
|
||||
return prism.Shape();
|
||||
}
|
||||
|
||||
static TopoDS_Shape extrude_face_internal(const TopoDS_Face& face, const gp_Dir& dir, double length, bool symmetric)
|
||||
{
|
||||
gp_Vec vec = gp_Vec(dir) * length;
|
||||
if (symmetric) {
|
||||
gp_Vec halfVec = gp_Vec(dir) * (length / 2.0);
|
||||
BRepPrimAPI_MakePrism pos(face, halfVec);
|
||||
BRepPrimAPI_MakePrism neg(face, -halfVec);
|
||||
if (!pos.IsDone() || !neg.IsDone()) throw std::runtime_error("Symmetric extrude failed");
|
||||
BRepAlgoAPI_Fuse fuse(pos.Shape(), neg.Shape());
|
||||
if (!fuse.IsDone()) throw std::runtime_error("Fuse failed");
|
||||
BRepAlgoAPI_Fuse fuse(SketchEngine::make_prism(face, halfVec), SketchEngine::make_prism(face, -halfVec));
|
||||
if (!fuse.IsDone()) throw std::runtime_error(_u8L("Fuse failed"));
|
||||
return fuse.Shape();
|
||||
}
|
||||
BRepPrimAPI_MakePrism prism(face, vec);
|
||||
if (!prism.IsDone()) throw std::runtime_error("Extrude failed");
|
||||
return prism.Shape();
|
||||
return SketchEngine::make_prism(face, gp_Vec(dir) * length);
|
||||
}
|
||||
|
||||
TopoDS_Shape SketchEngine::make_extrude(const TopoDS_Wire& wire, const SketchPlane& plane,
|
||||
double length, bool symmetric, double taper_deg)
|
||||
{
|
||||
BRepBuilderAPI_MakeFace fm(wire);
|
||||
if (!fm.IsDone()) throw std::runtime_error("Failed to make face from wire");
|
||||
if (!fm.IsDone()) throw std::runtime_error(_u8L("Failed to make face from wire"));
|
||||
return make_extrude(fm.Face(), plane, length, symmetric, taper_deg);
|
||||
}
|
||||
|
||||
@@ -201,7 +195,7 @@ TopoDS_Shape SketchEngine::make_extrude_two_sided(const TopoDS_Wire& wire, const
|
||||
double up, double down)
|
||||
{
|
||||
BRepBuilderAPI_MakeFace fm(wire);
|
||||
if (!fm.IsDone()) throw std::runtime_error("Failed to make face from wire");
|
||||
if (!fm.IsDone()) throw std::runtime_error(_u8L("Failed to make face from wire"));
|
||||
return make_extrude_two_sided(fm.Face(), plane, up, down);
|
||||
}
|
||||
|
||||
@@ -210,13 +204,10 @@ TopoDS_Shape SketchEngine::make_extrude_two_sided(const TopoDS_Face& face, const
|
||||
{
|
||||
gp_Dir dir(plane.normal.x(), plane.normal.y(), plane.normal.z());
|
||||
const double u = std::abs(up), d = std::abs(down);
|
||||
if (u < 1e-9 && d < 1e-9) return TopoDS_Shape();
|
||||
if (d < 1e-9) { BRepPrimAPI_MakePrism p(face, gp_Vec(dir) * u); return p.Shape(); }
|
||||
if (u < 1e-9) { BRepPrimAPI_MakePrism p(face, gp_Vec(dir) * -d); return p.Shape(); }
|
||||
BRepPrimAPI_MakePrism pos(face, gp_Vec(dir) * u);
|
||||
BRepPrimAPI_MakePrism neg(face, gp_Vec(dir) * -d);
|
||||
BRepAlgoAPI_Fuse fuse(pos.Shape(), neg.Shape());
|
||||
if (!fuse.IsDone()) throw std::runtime_error("two-sided extrude fuse failed");
|
||||
if (d < 1e-9) return make_prism(face, gp_Vec(dir) * u);
|
||||
if (u < 1e-9) return make_prism(face, gp_Vec(dir) * -d);
|
||||
BRepAlgoAPI_Fuse fuse(make_prism(face, gp_Vec(dir) * u), make_prism(face, gp_Vec(dir) * -d));
|
||||
if (!fuse.IsDone()) throw std::runtime_error(_u8L("two-sided extrude fuse failed"));
|
||||
return fuse.Shape();
|
||||
}
|
||||
|
||||
@@ -226,8 +217,8 @@ TopoDS_Shape SketchEngine::make_extrude_taper(const TopoDS_Wire& wire, const Ske
|
||||
gp_Dir dir(plane.normal.x(), plane.normal.y(), plane.normal.z());
|
||||
auto straight = [&]() -> TopoDS_Shape {
|
||||
BRepBuilderAPI_MakeFace fm(wire);
|
||||
BRepPrimAPI_MakePrism prism(fm.Face(), gp_Vec(dir) * length);
|
||||
return prism.Shape();
|
||||
if (!fm.IsDone()) throw std::runtime_error(_u8L("Failed to make face from wire"));
|
||||
return make_prism(fm.Face(), gp_Vec(dir) * length);
|
||||
};
|
||||
if (std::abs(taper_deg) >= 89.0 || std::abs(length) < 1e-9) return straight();
|
||||
const double off = length * std::tan(taper_deg * M_PI / 180.0);
|
||||
@@ -244,10 +235,10 @@ TopoDS_Shape SketchEngine::make_extrude_taper(const TopoDS_Wire& wire, const Ske
|
||||
if (topFlat.IsNull()) return straight();
|
||||
// 2) lift it along the normal by `length`
|
||||
gp_Trsf tr; tr.SetTranslation(gp_Vec(dir) * length);
|
||||
BRepBuilderAPI_Transform xf(topFlat, tr, Standard_True);
|
||||
BRepBuilderAPI_Transform xf(topFlat, tr, true);
|
||||
TopoDS_Wire topWire = TopoDS::Wire(xf.Shape());
|
||||
// 3) loft base -> top into a solid
|
||||
BRepOffsetAPI_ThruSections loft(Standard_True /*solid*/, Standard_False /*ruled*/);
|
||||
BRepOffsetAPI_ThruSections loft(true /*solid*/, false /*ruled*/);
|
||||
loft.AddWire(wire);
|
||||
loft.AddWire(topWire);
|
||||
loft.Build();
|
||||
@@ -271,12 +262,17 @@ TopoDS_Shape SketchEngine::make_extrude_regions(
|
||||
const std::vector<std::vector<std::vector<Vec2d>>>& regions,
|
||||
const SketchPlane& plane, double length, bool symmetric)
|
||||
{
|
||||
// The loop below skips regions that fail, so a zero depth is rejected before it.
|
||||
if (std::abs(length) < 1e-9) throw std::runtime_error(_u8L("extrude depth is zero"));
|
||||
|
||||
// Drop consecutive coincident points and the closing duplicate. FreeType /
|
||||
// SVG flattening routinely emits repeated points which would build a
|
||||
// degenerate OCCT edge and make the wire builder throw — sanitising keeps a
|
||||
// single bad glyph from killing the whole extrude.
|
||||
auto clean = [](const std::vector<Vec2d>& pts) {
|
||||
const double eps2 = 1e-12; // ~1e-6 mm
|
||||
// The sketch's own joint tolerance: points closer than this are one point everywhere
|
||||
// else in the sketcher, so they must not become a sub-micron edge here either.
|
||||
const double eps2 = kSketchJoinTol * kSketchJoinTol;
|
||||
std::vector<Vec2d> out;
|
||||
out.reserve(pts.size());
|
||||
for (const Vec2d& p : pts)
|
||||
@@ -304,6 +300,10 @@ TopoDS_Shape SketchEngine::make_extrude_regions(
|
||||
};
|
||||
|
||||
gp_Dir dir(plane.normal.x(), plane.normal.y(), plane.normal.z());
|
||||
// Faces are built ON the sketch plane, as wires_to_face does: a surface inferred from the
|
||||
// outer wire need not share the sketch normal, and then the extrude direction and the hole
|
||||
// classification disagree with it.
|
||||
const gp_Pln pln(gp_Pnt(plane.origin.x(), plane.origin.y(), plane.origin.z()), dir);
|
||||
|
||||
// Accumulate each region's solid into a compound rather than boolean-fusing:
|
||||
// glyphs are independent profiles, so a compound avoids every boolean-failure
|
||||
@@ -324,7 +324,10 @@ TopoDS_Shape SketchEngine::make_extrude_regions(
|
||||
// classify outer vs holes by area/containment and set correct wire
|
||||
// orientations. This is winding-independent, so holed glyphs extrude
|
||||
// with a solid body and empty counters regardless of source winding.
|
||||
BRepBuilderAPI_MakeFace fm(outer);
|
||||
// Probe on the inferred surface first: naming a plane makes MakeFace accept a wire
|
||||
// that bounds nothing, and this check is what skips such a contour.
|
||||
if (!BRepBuilderAPI_MakeFace(outer).IsDone()) continue;
|
||||
BRepBuilderAPI_MakeFace fm(pln, outer);
|
||||
if (!fm.IsDone()) continue;
|
||||
for (size_t h = 1; h < region.size(); ++h) {
|
||||
TopoDS_Wire hole = contour_wire(region[h]);
|
||||
@@ -346,31 +349,48 @@ TopoDS_Shape SketchEngine::make_extrude_regions(
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0) throw std::runtime_error("imported regions produced no extrudable geometry");
|
||||
if (count == 0) throw std::runtime_error(_u8L("imported regions produced no extrudable geometry"));
|
||||
return count == 1 ? last : TopoDS_Shape(comp); // avoid a compound-of-one
|
||||
}
|
||||
|
||||
TopoDS_Shape SketchEngine::make_revolve(const TopoDS_Wire& wire, const SketchPlane& plane,
|
||||
double angle_deg, int axis_sel)
|
||||
TopoDS_Shape SketchEngine::make_revolve(const TopoDS_Wire& wire, const gp_Ax1& axis_in, double angle_deg)
|
||||
{
|
||||
BRepBuilderAPI_MakeFace faceMaker(wire);
|
||||
if (!faceMaker.IsDone())
|
||||
throw std::runtime_error("Failed to make face from wire");
|
||||
throw std::runtime_error(_u8L("Failed to make face from wire"));
|
||||
TopoDS_Face face = faceMaker.Face();
|
||||
|
||||
// Revolution axis lies in the sketch plane through its origin: X (0) or Y (1).
|
||||
const Vec3d& adir = (axis_sel == 1) ? plane.y_axis : plane.x_axis;
|
||||
gp_Pnt o(plane.origin.x(), plane.origin.y(), plane.origin.z());
|
||||
gp_Dir xd(adir.x(), adir.y(), adir.z());
|
||||
gp_Ax1 axis(o, xd);
|
||||
|
||||
// A profile on both sides of the axis sweeps through itself; MakeRevol then fails with no
|
||||
// reason, or builds an invalid solid. Sample every edge and name the cause instead.
|
||||
{
|
||||
const gp_Pnt o = axis_in.Location();
|
||||
const gp_Dir d = axis_in.Direction();
|
||||
bool pos = false, neg = false;
|
||||
gp_Vec side_ref;
|
||||
for (TopExp_Explorer ex(wire, TopAbs_EDGE); ex.More(); ex.Next()) {
|
||||
BRepAdaptor_Curve c(TopoDS::Edge(ex.Current()));
|
||||
for (int i = 0; i <= 16; ++i) {
|
||||
const gp_Pnt p = c.Value(c.FirstParameter() + (c.LastParameter() - c.FirstParameter()) * i / 16.0);
|
||||
const gp_Vec off = gp_Vec(o, p) - gp_Vec(d) * gp_Vec(o, p).Dot(gp_Vec(d)); // from the axis
|
||||
if (off.Magnitude() < 1e-6)
|
||||
continue;
|
||||
if (side_ref.Magnitude() == 0.0) { side_ref = off; pos = true; continue; }
|
||||
(off.Dot(side_ref) > 0.0 ? pos : neg) = true;
|
||||
}
|
||||
}
|
||||
if (pos && neg)
|
||||
throw std::runtime_error("the profile crosses the revolve axis — it must lie on one side of it");
|
||||
}
|
||||
gp_Ax1 axis = axis_in;
|
||||
double angle_rad = angle_deg * M_PI / 180.0;
|
||||
// A negative angle is expressed as a positive sweep about the reversed axis,
|
||||
// since BRepPrimAPI_MakeRevol expects an angle in (0, 2*pi].
|
||||
if (angle_rad < 0) { axis.Reverse(); angle_rad = -angle_rad; }
|
||||
BRepPrimAPI_MakeRevol rev(face, axis, angle_rad);
|
||||
if (!rev.IsDone())
|
||||
throw std::runtime_error("Failed to revolve");
|
||||
throw std::runtime_error(_u8L("Failed to revolve"));
|
||||
if (!BRepCheck_Analyzer(rev.Shape()).IsValid())
|
||||
throw std::runtime_error("the profile crosses the revolve axis — it must lie on one side of it");
|
||||
return rev.Shape();
|
||||
}
|
||||
|
||||
@@ -378,30 +398,29 @@ TopoDS_Shape SketchEngine::make_sweep(const TopoDS_Wire& profile, const TopoDS_W
|
||||
{
|
||||
BRepBuilderAPI_MakeFace faceMaker(profile);
|
||||
if (!faceMaker.IsDone())
|
||||
throw std::runtime_error("Failed to make face from sweep profile");
|
||||
throw std::runtime_error(_u8L("Failed to make face from sweep profile"));
|
||||
TopoDS_Face face = faceMaker.Face();
|
||||
|
||||
BRepOffsetAPI_MakePipe pipe(path, face);
|
||||
pipe.Build();
|
||||
if (!pipe.IsDone())
|
||||
throw std::runtime_error("Failed to sweep profile along path");
|
||||
throw std::runtime_error(_u8L("Failed to sweep profile along path"));
|
||||
return pipe.Shape();
|
||||
}
|
||||
|
||||
TopoDS_Shape SketchEngine::make_loft(const std::vector<TopoDS_Wire>& profiles, bool ruled)
|
||||
{
|
||||
if (profiles.size() < 2)
|
||||
throw std::runtime_error("loft needs at least 2 profiles");
|
||||
BRepOffsetAPI_ThruSections loft(Standard_True /*solid*/,
|
||||
ruled ? Standard_True : Standard_False);
|
||||
throw std::runtime_error(_u8L("loft needs at least 2 profiles"));
|
||||
BRepOffsetAPI_ThruSections loft(true /*solid*/, ruled);
|
||||
for (const TopoDS_Wire& w : profiles) {
|
||||
if (w.IsNull()) throw std::runtime_error("loft: null profile wire");
|
||||
loft.AddWire(w);
|
||||
}
|
||||
loft.Build();
|
||||
if (!loft.IsDone()) throw std::runtime_error("loft failed");
|
||||
if (!loft.IsDone()) throw std::runtime_error(_u8L("loft failed"));
|
||||
TopoDS_Shape s = loft.Shape();
|
||||
if (s.IsNull()) throw std::runtime_error("loft produced no solid");
|
||||
if (s.IsNull()) throw std::runtime_error(_u8L("loft produced no solid"));
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -409,17 +428,16 @@ TopoDS_Shape SketchEngine::make_loft(const std::vector<TopoDS_Wire>& profiles, b
|
||||
TopoDS_Shape SketchEngine::make_loft_surface(const std::vector<TopoDS_Wire>& profiles, bool ruled)
|
||||
{
|
||||
if (profiles.size() < 2)
|
||||
throw std::runtime_error("loft needs at least 2 profiles");
|
||||
BRepOffsetAPI_ThruSections loft(Standard_False /*shell, no end caps*/,
|
||||
ruled ? Standard_True : Standard_False);
|
||||
throw std::runtime_error(_u8L("loft needs at least 2 profiles"));
|
||||
BRepOffsetAPI_ThruSections loft(false /*shell, no end caps*/, ruled);
|
||||
for (const TopoDS_Wire& w : profiles) {
|
||||
if (w.IsNull()) throw std::runtime_error("loft: null profile wire");
|
||||
loft.AddWire(w);
|
||||
}
|
||||
loft.Build();
|
||||
if (!loft.IsDone()) throw std::runtime_error("loft failed");
|
||||
if (!loft.IsDone()) throw std::runtime_error(_u8L("loft failed"));
|
||||
TopoDS_Shape s = loft.Shape();
|
||||
if (s.IsNull()) throw std::runtime_error("loft produced no shape");
|
||||
if (s.IsNull()) throw std::runtime_error(_u8L("loft produced no shape"));
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -427,11 +445,11 @@ TopoDS_Shape SketchEngine::make_pocket(const TopoDS_Wire& wire, const SketchPlan
|
||||
const TopoDS_Shape& target, double depth)
|
||||
{
|
||||
BRepBuilderAPI_MakeFace fm(wire);
|
||||
if (!fm.IsDone()) throw std::runtime_error("Pocket face failed");
|
||||
if (!fm.IsDone()) throw std::runtime_error(_u8L("Pocket face failed"));
|
||||
TopoDS_Shape tool = extrude_face_internal(fm.Face(),
|
||||
gp_Dir(plane.normal.x(), plane.normal.y(), plane.normal.z()), depth + 1.0, false);
|
||||
BRepAlgoAPI_Cut cut(target, tool);
|
||||
if (!cut.IsDone()) throw std::runtime_error("Pocket cut failed");
|
||||
if (!cut.IsDone()) throw std::runtime_error(_u8L("Pocket cut failed"));
|
||||
return cut.Shape();
|
||||
}
|
||||
|
||||
@@ -567,7 +585,12 @@ std::vector<TopoDS_Wire> SketchEngine::entities_to_wires(const std::vector<Sketc
|
||||
auto make_elips = [&](const SketchEntity& c) -> gp_Elips {
|
||||
Vec3d c3 = plane.to_world(c.center);
|
||||
gp_Pnt center(c3.x(), c3.y(), c3.z());
|
||||
gp_Dir n(plane.normal.x(), plane.normal.y(), plane.normal.z());
|
||||
// The frame's normal is x_axis x y_axis, not plane.normal: OCCT takes the ellipse's Y
|
||||
// as N x X, and the parametric angles were measured in the sketch's own (x, y). The two
|
||||
// agree on XY and YZ; the XZ base plane stores normal = +Y while x x y = -Y, so there
|
||||
// every elliptical arc came out mirrored against what the sketch showed.
|
||||
const Vec3d nz = plane.x_axis.cross(plane.y_axis);
|
||||
gp_Dir n(nz.x(), nz.y(), nz.z());
|
||||
Vec2d maj2(std::cos(c.rotation), std::sin(c.rotation));
|
||||
Vec3d x3 = plane.to_world(c.center + maj2) - c3;
|
||||
gp_Dir xdir(x3.x(), x3.y(), x3.z());
|
||||
@@ -583,15 +606,15 @@ std::vector<TopoDS_Wire> SketchEngine::entities_to_wires(const std::vector<Sketc
|
||||
const int n = int(c.ctrl.size());
|
||||
const int p = n >= 4 ? 3 : (n >= 2 ? n - 1 : 0);
|
||||
if (p < 1) return Handle(Geom_BSplineCurve)();
|
||||
TColgp_Array1OfPnt poles(1, n);
|
||||
NCollection_Array1<gp_Pnt> poles(1, n);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
Vec3d w = plane.to_world(c.ctrl[i]);
|
||||
poles.SetValue(i + 1, gp_Pnt(w.x(), w.y(), w.z()));
|
||||
}
|
||||
const int interior = n - p - 1; // count of single interior knots
|
||||
const int nknots = interior + 2;
|
||||
TColStd_Array1OfReal knots(1, nknots);
|
||||
TColStd_Array1OfInteger mults(1, nknots);
|
||||
NCollection_Array1<double> knots(1, nknots);
|
||||
NCollection_Array1<int> mults(1, nknots);
|
||||
knots.SetValue(1, 0.0); mults.SetValue(1, p + 1);
|
||||
for (int i = 1; i <= interior; ++i) { knots.SetValue(i + 1, double(i)); mults.SetValue(i + 1, 1); }
|
||||
knots.SetValue(nknots, double(interior + 1)); mults.SetValue(nknots, p + 1);
|
||||
@@ -691,6 +714,9 @@ std::vector<TopoDS_Wire> SketchEngine::entities_to_wires(const std::vector<Sketc
|
||||
if (c.radius <= 1e-9 || c.rminor <= 1e-9) return {};
|
||||
e = BRepBuilderAPI_MakeEdge(make_elips(c)).Edge();
|
||||
} else {
|
||||
// Same guard as the ellipse above: a zero radius would reach OCCT and throw from
|
||||
// .Edge() instead of reporting a sketch that cannot be built.
|
||||
if (c.radius <= 1e-9) return {};
|
||||
Vec3d c3 = plane.to_world(c.center);
|
||||
gp_Pnt center(c3.x(), c3.y(), c3.z());
|
||||
gp_Dir n(plane.normal.x(), plane.normal.y(), plane.normal.z());
|
||||
@@ -794,7 +820,7 @@ std::vector<TopoDS_Wire> SketchEngine::entities_to_wires(const std::vector<Sketc
|
||||
wm.Add(BRepBuilderAPI_MakeEdge(va, vb).Edge());
|
||||
} else if (e->type == SketchEntity::Type::EllipseArc) {
|
||||
if (e->radius <= 1e-9 || e->rminor <= 1e-9) return {};
|
||||
GC_MakeArcOfEllipse arc_maker(make_elips(*e), e->start_angle, e->end_angle, Standard_True);
|
||||
GC_MakeArcOfEllipse arc_maker(make_elips(*e), e->start_angle, e->end_angle, true);
|
||||
if (!arc_maker.IsDone()) return {};
|
||||
wm.Add(BRepBuilderAPI_MakeEdge(arc_maker.Value(), va, vb).Edge());
|
||||
} else if (e->type == SketchEntity::Type::Arc) {
|
||||
@@ -889,10 +915,22 @@ std::vector<Vec2d> sketch_open_ends(const std::vector<SketchEntity>& entities,
|
||||
return out;
|
||||
}
|
||||
|
||||
// A closed wire can still fail to bound a region: a loop that crosses itself, or one that
|
||||
// doubles back along itself (a cusp, e.g. an arc leaving a line tangent to it but in the
|
||||
// opposite direction). MakeFace reports success on both and the prism built from the face is
|
||||
// an invalid solid with no caps. Refuse it here, with the reason, instead of shipping that.
|
||||
static TopoDS_Face checked_profile_face(const TopoDS_Face& f)
|
||||
{
|
||||
BRepCheck_Analyzer an(f);
|
||||
if (!an.IsValid())
|
||||
throw std::runtime_error("the profile crosses or folds back on itself, so it does not bound one region");
|
||||
return f;
|
||||
}
|
||||
|
||||
TopoDS_Face SketchEngine::wires_to_face(const std::vector<TopoDS_Wire>& wires,
|
||||
const SketchPlane& plane)
|
||||
{
|
||||
if (wires.empty()) throw std::runtime_error("sketch has no closed loop");
|
||||
if (wires.empty()) throw std::runtime_error(_u8L("sketch has no closed loop"));
|
||||
|
||||
// The ASSEMBLED face below is built on the SKETCH's own plane rather than on a surface OCCT
|
||||
// infers from the outer wire. The inferred plane has no reason to share the sketch's normal,
|
||||
@@ -907,8 +945,8 @@ TopoDS_Face SketchEngine::wires_to_face(const std::vector<TopoDS_Wire>& wires,
|
||||
|
||||
if (wires.size() == 1) {
|
||||
BRepBuilderAPI_MakeFace fm(wires[0]);
|
||||
if (!fm.IsDone()) throw std::runtime_error("sketch loop does not bound a face");
|
||||
return fm.Face();
|
||||
if (!fm.IsDone()) throw std::runtime_error(_u8L("sketch loop does not bound a face"));
|
||||
return checked_profile_face(fm.Face());
|
||||
}
|
||||
|
||||
// Two or more loops: build a face per wire and let the largest area be the outer
|
||||
@@ -919,7 +957,7 @@ TopoDS_Face SketchEngine::wires_to_face(const std::vector<TopoDS_Wire>& wires,
|
||||
areas.reserve(wires.size());
|
||||
for (const TopoDS_Wire& w : wires) {
|
||||
BRepBuilderAPI_MakeFace fm(w);
|
||||
if (!fm.IsDone()) throw std::runtime_error("sketch loop does not bound a face");
|
||||
if (!fm.IsDone()) throw std::runtime_error(_u8L("sketch loop does not bound a face"));
|
||||
faces.push_back(fm.Face());
|
||||
GProp_GProps props;
|
||||
BRepGProp::SurfaceProperties(faces.back(), props);
|
||||
@@ -945,10 +983,10 @@ TopoDS_Face SketchEngine::wires_to_face(const std::vector<TopoDS_Wire>& wires,
|
||||
got = true;
|
||||
break;
|
||||
}
|
||||
if (!got) throw std::runtime_error("sketch loop does not bound a face");
|
||||
if (!got) throw std::runtime_error(_u8L("sketch loop does not bound a face"));
|
||||
BRepClass_FaceClassifier fc(faces[outer], p, 1e-7);
|
||||
if (fc.State() != TopAbs_IN)
|
||||
throw std::runtime_error("sketch has two disjoint regions; put each in its own sketch");
|
||||
throw std::runtime_error(_u8L("sketch has two disjoint regions; put each in its own sketch"));
|
||||
// Add the hole loop AS-IS and let ShapeFix_Face sort the orientations out below.
|
||||
// Reversing it here only works when the sketch happened to wind both loops the same
|
||||
// way: a circle drawn clockwise inside a counter-clockwise rectangle comes out matching
|
||||
@@ -958,13 +996,13 @@ TopoDS_Face SketchEngine::wires_to_face(const std::vector<TopoDS_Wire>& wires,
|
||||
// 147520 — a body larger than its own bounding box, which is the signature of it.
|
||||
fm.Add(wires[i]);
|
||||
}
|
||||
if (!fm.IsDone()) throw std::runtime_error("sketch loop does not bound a face");
|
||||
if (!fm.IsDone()) throw std::runtime_error(_u8L("sketch loop does not bound a face"));
|
||||
// Winding-independent classification of outer vs holes — the same idiom make_extrude_regions
|
||||
// already uses for imported glyphs, which is why holed TEXT extruded correctly all along
|
||||
// while a holed SKETCH did not.
|
||||
ShapeFix_Face sff(fm.Face());
|
||||
sff.FixOrientation();
|
||||
return sff.Face();
|
||||
return checked_profile_face(sff.Face());
|
||||
}
|
||||
|
||||
std::vector<SketchEntity> SketchEngine::mirror_entities(
|
||||
@@ -1036,8 +1074,11 @@ std::vector<SketchEntity> SketchEngine::mirror_entities(
|
||||
} else {
|
||||
m.p0 = reflect(e.p0);
|
||||
m.p1 = reflect(e.p1);
|
||||
// Reflection reverses orientation: recompute parametric angles in
|
||||
// the reflected frame, original end -> new start (CCW sense kept).
|
||||
// Reflection reverses orientation. Like the Arc branch above, this pass keeps
|
||||
// each angle with ITS point (start with p0) and lets the sweep run clockwise;
|
||||
// the reversal pass below then swaps points and angles together, giving a CCW
|
||||
// arc whose start is p0. Pairing them crosswise here, as this used to, was
|
||||
// undone by that same swap and produced the complementary arc.
|
||||
auto param = [&](const Vec2d& P) {
|
||||
const Vec2d d = P - m.center;
|
||||
const double cu = std::cos(m.rotation), su = std::sin(m.rotation);
|
||||
@@ -1045,8 +1086,9 @@ std::vector<SketchEntity> SketchEngine::mirror_entities(
|
||||
const double v = -d.x() * su + d.y() * cu;
|
||||
return std::atan2(v / std::max(e.rminor, 1e-9), u / std::max(e.radius, 1e-9));
|
||||
};
|
||||
m.start_angle = param(m.p1);
|
||||
m.end_angle = param(m.p0);
|
||||
m.start_angle = param(m.p0);
|
||||
m.end_angle = param(m.p1);
|
||||
while (m.end_angle >= m.start_angle) m.end_angle -= 2.0 * M_PI;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1106,9 +1148,14 @@ std::vector<SketchEntity> SketchEngine::mirror_entities(
|
||||
// get their last-to-first seam repaired too, which is what makes the result closed again.
|
||||
namespace {
|
||||
|
||||
constexpr double kOffJoinEps = 1e-6;
|
||||
|
||||
bool off_same(const Vec2d& a, const Vec2d& b) { return (a - b).squaredNorm() < kOffJoinEps * kOffJoinEps; }
|
||||
// Two ends are the same point when the WIRE BUILDER would weld them: a loop the viewport and
|
||||
// the kernel treat as closed must offset as one closed chain, not as separate open pieces with
|
||||
// unrepaired seams. The floor keeps exact coincidence meaningful with auto-close switched off.
|
||||
bool off_same(const Vec2d& a, const Vec2d& b)
|
||||
{
|
||||
const double tol = std::max(sketch_join_tol(), 1e-6);
|
||||
return (a - b).squaredNorm() <= tol * tol;
|
||||
}
|
||||
|
||||
// Does this entity type take part in chaining (i.e. does it have two ends)?
|
||||
bool off_is_open_curve(const SketchEntity& e)
|
||||
@@ -1214,7 +1261,10 @@ bool off_one(const SketchEntity& e, double d, SketchEntity& out)
|
||||
return true;
|
||||
}
|
||||
case SketchEntity::Type::Circle: {
|
||||
const double r = e.radius + d;
|
||||
// A circle runs CCW (it is what a 360° CCW arc chain closes into), so the rule below
|
||||
// applies to it too: +d is the left side, the inside, and the radius SHRINKS. It used to
|
||||
// grow, so a full circle and the same outline drawn as arcs offset opposite ways.
|
||||
const double r = e.radius - d;
|
||||
if (r <= 1e-9) return false;
|
||||
out = e; out.radius = r; out.p0 = out.center;
|
||||
return true;
|
||||
@@ -1453,6 +1503,9 @@ std::vector<SketchEntity> SketchEngine::transform_entities(
|
||||
out.reserve(src.size());
|
||||
const double ca = std::cos(angle), sa = std::sin(angle);
|
||||
const double rs = std::abs(scale); // radii are unsigned magnitudes
|
||||
// A negative scale is |scale| plus a half turn about the pivot: points get that from xf()
|
||||
// below, and angle-valued fields (arc ends, ellipse axis) need the same extra pi.
|
||||
const double turn = angle + (scale < 0.0 ? M_PI : 0.0);
|
||||
// Affine map: translate pivot to origin, scale, rotate, then translate by `move`.
|
||||
auto xf = [&](const Vec2d& p) -> Vec2d {
|
||||
const Vec2d d = scale * (p - pivot);
|
||||
@@ -1475,8 +1528,8 @@ std::vector<SketchEntity> SketchEngine::transform_entities(
|
||||
case SketchEntity::Type::Arc: {
|
||||
m.center = xf(e.center);
|
||||
m.radius = e.radius * rs;
|
||||
m.start_angle = e.start_angle + angle;
|
||||
m.end_angle = e.end_angle + angle; // rigid sweep, shifted by rotation
|
||||
m.start_angle = e.start_angle + turn;
|
||||
m.end_angle = e.end_angle + turn; // rigid sweep, shifted by rotation
|
||||
m.p0 = m.center + m.radius * Vec2d(std::cos(m.start_angle), std::sin(m.start_angle));
|
||||
m.p1 = m.center + m.radius * Vec2d(std::cos(m.end_angle), std::sin(m.end_angle));
|
||||
break;
|
||||
@@ -1486,7 +1539,7 @@ std::vector<SketchEntity> SketchEngine::transform_entities(
|
||||
m.center = xf(e.center);
|
||||
m.radius = e.radius * rs;
|
||||
m.rminor = e.rminor * rs;
|
||||
m.rotation = e.rotation + angle; // major axis rotates with the body
|
||||
m.rotation = e.rotation + turn; // major axis rotates with the body
|
||||
if (e.type == SketchEntity::Type::Ellipse) {
|
||||
m.p0 = m.center;
|
||||
} else {
|
||||
@@ -1833,6 +1886,15 @@ static double wrap_2pi(double x)
|
||||
return x;
|
||||
}
|
||||
|
||||
// An arc's endpoints are stored twice: as angles and as p0/p1. Everything downstream reads
|
||||
// p0/p1 (the wire builder welds on them, the solver seeds from them, snapping uses them), so
|
||||
// any edit of the angles must write them back or the arc's ends silently stay where they were.
|
||||
static void sync_arc_ends(SketchEntity& e)
|
||||
{
|
||||
e.p0 = e.center + e.radius * Vec2d(std::cos(e.start_angle), std::sin(e.start_angle));
|
||||
e.p1 = e.center + e.radius * Vec2d(std::cos(e.end_angle), std::sin(e.end_angle));
|
||||
}
|
||||
|
||||
bool SketchEngine::trim_entity(SketchEntity& e, const std::vector<SketchEntity>& others,
|
||||
const Vec2d& pick)
|
||||
{
|
||||
@@ -1867,6 +1929,7 @@ bool SketchEngine::trim_entity(SketchEntity& e, const std::vector<SketchEntity>&
|
||||
if (uc == -std::numeric_limits<double>::max()) return false;
|
||||
e.end_angle = e.start_angle + uc * sweep; // drop (uc, 1]
|
||||
}
|
||||
sync_arc_ends(e);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1898,6 +1961,7 @@ bool SketchEngine::trim_entity(SketchEntity& e, const std::vector<SketchEntity>&
|
||||
e.type = SketchEntity::Type::Arc;
|
||||
e.start_angle = hi;
|
||||
e.end_angle = lo + 2.0 * M_PI;
|
||||
sync_arc_ends(e); // p0 was the circle's centre (circle convention); now the arc's start
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1970,6 +2034,7 @@ bool SketchEngine::extend_entity(SketchEntity& e, const std::vector<SketchEntity
|
||||
if (best == std::numeric_limits<double>::max()) return false;
|
||||
if (extend_end) e.end_angle += sgn * best;
|
||||
else e.start_angle -= sgn * best;
|
||||
sync_arc_ends(e);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2010,7 +2075,9 @@ bool SketchEngine::extend_entity(SketchEntity& e, const std::vector<SketchEntity
|
||||
}
|
||||
|
||||
// Bridge: cubic Bézier with G1 continuity at both ends.
|
||||
// Poles = {Pa, Pa + Ta*d/3, Pb - Tb*d/3, Pb}, where d = |Pb - Pa|.
|
||||
// Poles = {Pa, Pa + Ta*d/3, Pb + Tb*d/3, Pb}, where d = |Pb - Pa| and Ta/Tb are the OUTWARD
|
||||
// tangents at the two ends (the direction you would leave each entity in). The curve leaves
|
||||
// Pa along Ta and arrives at Pb along -Tb, i.e. continuing INTO b.
|
||||
SketchEntity SketchEngine::make_bridge(const SketchEntity& a, int a_end,
|
||||
const SketchEntity& b, int b_end)
|
||||
{
|
||||
@@ -2056,7 +2123,9 @@ SketchEntity SketchEngine::make_bridge(const SketchEntity& a, int a_end,
|
||||
SketchEntity e;
|
||||
e.type = SketchEntity::Type::BSpline;
|
||||
e.construction = false;
|
||||
e.ctrl = { Pa, Pa + Ta * k, Pb - Tb * k, Pb };
|
||||
// Pb + Tb*k, not minus: with Tb outward, minus put the last inner pole on b's side, so the
|
||||
// curve overshot Pb (and against a line arrived with a cusp instead of continuing into it).
|
||||
e.ctrl = { Pa, Pa + Ta * k, Pb + Tb * k, Pb };
|
||||
e.p0 = e.ctrl.front();
|
||||
e.p1 = e.ctrl.back();
|
||||
return e;
|
||||
@@ -2084,6 +2153,153 @@ int sketch_entity_ends(const SketchEntity& e, std::pair<SketchPointRole, Vec2d>
|
||||
return 0;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// An arc's signed angular offset of `ang` from its start, within its sweep: true when the angle
|
||||
// lies on the arc (with `tol` radians of slack at either end).
|
||||
bool angle_on_arc(const SketchEntity& e, double ang, double tol)
|
||||
{
|
||||
const double TWO_PI = 2.0 * M_PI;
|
||||
const double sweep = e.end_angle - e.start_angle;
|
||||
double d = (sweep >= 0.0) ? ang - e.start_angle : e.start_angle - ang;
|
||||
d = std::fmod(d, TWO_PI);
|
||||
if (d < 0.0) d += TWO_PI;
|
||||
if (d > TWO_PI - tol) d -= TWO_PI; // just before the start counts as the start
|
||||
return d >= -tol && d <= std::abs(sweep) + tol;
|
||||
}
|
||||
|
||||
// Every point where two lines/arcs meet (tangent contact included). Exact, no sampling.
|
||||
void entity_intersections(const SketchEntity& A, const SketchEntity& B, std::vector<Vec2d>& out)
|
||||
{
|
||||
using T = SketchEntity::Type;
|
||||
const double eps = 1e-9;
|
||||
auto on_seg = [](const SketchEntity& L, const Vec2d& p) {
|
||||
const Vec2d d = L.p1 - L.p0;
|
||||
const double l2 = d.squaredNorm();
|
||||
if (l2 < 1e-24) return false;
|
||||
const double t = (p - L.p0).dot(d) / l2;
|
||||
return t >= -1e-9 && t <= 1.0 + 1e-9;
|
||||
};
|
||||
if (A.type == T::Line && B.type == T::Line) {
|
||||
const Vec2d r = A.p1 - A.p0, q = B.p1 - B.p0, w = B.p0 - A.p0;
|
||||
const double den = r.x() * q.y() - r.y() * q.x();
|
||||
if (std::abs(den) < eps * r.norm() * q.norm()) {
|
||||
// Parallel. Collinear overlap is a crossing wherever it is: report the overlap's
|
||||
// nearest end, which the caller then tests against the shared joints.
|
||||
if (std::abs(r.x() * w.y() - r.y() * w.x()) > 1e-9 * std::max(1.0, r.norm())) return;
|
||||
for (const Vec2d& p : { B.p0, B.p1 }) if (on_seg(A, p)) out.push_back(p);
|
||||
for (const Vec2d& p : { A.p0, A.p1 }) if (on_seg(B, p)) out.push_back(p);
|
||||
return;
|
||||
}
|
||||
const double t = (w.x() * q.y() - w.y() * q.x()) / den;
|
||||
const double u = (w.x() * r.y() - w.y() * r.x()) / den;
|
||||
if (t >= -1e-9 && t <= 1.0 + 1e-9 && u >= -1e-9 && u <= 1.0 + 1e-9) out.push_back(A.p0 + t * r);
|
||||
return;
|
||||
}
|
||||
if (A.type == T::Arc && B.type == T::Line) { entity_intersections(B, A, out); return; }
|
||||
if (A.type == T::Line && B.type == T::Arc) {
|
||||
const Vec2d d = A.p1 - A.p0, f = A.p0 - B.center;
|
||||
const double a = d.squaredNorm(), b = 2.0 * f.dot(d), c = f.squaredNorm() - B.radius * B.radius;
|
||||
if (a < 1e-24) return;
|
||||
double disc = b * b - 4.0 * a * c;
|
||||
if (disc < -1e-9 * a * B.radius * B.radius) return;
|
||||
disc = std::sqrt(std::max(0.0, disc));
|
||||
for (double t : { (-b - disc) / (2.0 * a), (-b + disc) / (2.0 * a) }) {
|
||||
if (t < -1e-9 || t > 1.0 + 1e-9) continue;
|
||||
const Vec2d p = A.p0 + t * d;
|
||||
if (angle_on_arc(B, std::atan2(p.y() - B.center.y(), p.x() - B.center.x()), 1e-9))
|
||||
out.push_back(p);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (A.type == T::Arc && B.type == T::Arc) {
|
||||
const Vec2d dc = B.center - A.center;
|
||||
const double dd = dc.norm();
|
||||
if (dd < 1e-12) {
|
||||
// Concentric: they overlap only on the same circle, and then everywhere they share.
|
||||
if (std::abs(A.radius - B.radius) > 1e-9) return;
|
||||
for (const Vec2d& p : { B.p0, B.p1 })
|
||||
if (angle_on_arc(A, std::atan2(p.y() - A.center.y(), p.x() - A.center.x()), 1e-9)) out.push_back(p);
|
||||
for (const Vec2d& p : { A.p0, A.p1 })
|
||||
if (angle_on_arc(B, std::atan2(p.y() - B.center.y(), p.x() - B.center.x()), 1e-9)) out.push_back(p);
|
||||
return;
|
||||
}
|
||||
if (dd > A.radius + B.radius + 1e-9 || dd < std::abs(A.radius - B.radius) - 1e-9) return;
|
||||
const double x = (dd * dd + A.radius * A.radius - B.radius * B.radius) / (2.0 * dd);
|
||||
const double h = std::sqrt(std::max(0.0, A.radius * A.radius - x * x));
|
||||
const Vec2d u = dc / dd, m = A.center + x * u, n(-u.y(), u.x());
|
||||
for (const Vec2d& p : { Vec2d(m + h * n), Vec2d(m - h * n) }) {
|
||||
if (angle_on_arc(A, std::atan2(p.y() - A.center.y(), p.x() - A.center.x()), 1e-9) &&
|
||||
angle_on_arc(B, std::atan2(p.y() - B.center.y(), p.x() - B.center.x()), 1e-9))
|
||||
out.push_back(p);
|
||||
if (h < 1e-12) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool sketch_loop_defect(const std::vector<SketchEntity>& ents, const std::vector<int>& order, Vec2d& at)
|
||||
{
|
||||
using T = SketchEntity::Type;
|
||||
const size_t n = order.size();
|
||||
if (n < 2) return false;
|
||||
for (int ei : order)
|
||||
if (ei < 0 || ei >= int(ents.size()) || (ents[ei].type != T::Line && ents[ei].type != T::Arc))
|
||||
return false;
|
||||
const double tol = std::max(1e-6, sketch_join_tol());
|
||||
|
||||
// Traversal direction of each entity: the end it shares with the NEXT one is where it
|
||||
// finishes. start[k]/end[k] are the traversal's ends.
|
||||
std::vector<Vec2d> start(n), end(n);
|
||||
std::vector<bool> rev(n, false);
|
||||
for (size_t k = 0; k < n; ++k) {
|
||||
const SketchEntity& e = ents[order[k]];
|
||||
const SketchEntity& nx = ents[order[(k + 1) % n]];
|
||||
const double to_next_p1 = std::min((e.p1 - nx.p0).norm(), (e.p1 - nx.p1).norm());
|
||||
const double to_next_p0 = std::min((e.p0 - nx.p0).norm(), (e.p0 - nx.p1).norm());
|
||||
rev[k] = to_next_p0 < to_next_p1;
|
||||
start[k] = rev[k] ? e.p1 : e.p0;
|
||||
end[k] = rev[k] ? e.p0 : e.p1;
|
||||
}
|
||||
auto tangent = [&](size_t k, bool at_end) {
|
||||
const SketchEntity& e = ents[order[k]];
|
||||
Vec2d t;
|
||||
if (e.type == T::Line) {
|
||||
t = e.p1 - e.p0;
|
||||
} else {
|
||||
const double a = at_end ? (rev[k] ? e.start_angle : e.end_angle)
|
||||
: (rev[k] ? e.end_angle : e.start_angle);
|
||||
const double s = (e.end_angle >= e.start_angle) ? 1.0 : -1.0;
|
||||
t = s * Vec2d(-std::sin(a), std::cos(a));
|
||||
}
|
||||
if (rev[k]) t = -t;
|
||||
const double l = t.norm();
|
||||
return l > 1e-15 ? Vec2d(t / l) : Vec2d(0, 0);
|
||||
};
|
||||
|
||||
// Cusps: the curve arriving at a joint and the one leaving it point opposite ways.
|
||||
for (size_t k = 0; k < n; ++k) {
|
||||
const size_t j = (k + 1) % n;
|
||||
if (tangent(k, true).dot(tangent(j, false)) < -0.9999) { at = end[k]; return true; }
|
||||
}
|
||||
|
||||
// Crossings: any contact between two entities away from the joints they share.
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
for (size_t j = i + 1; j < n; ++j) {
|
||||
std::vector<Vec2d> hits;
|
||||
entity_intersections(ents[order[i]], ents[order[j]], hits);
|
||||
const bool next = (j == i + 1), prev = (i == 0 && j == n - 1);
|
||||
for (const Vec2d& p : hits) {
|
||||
bool joint = false;
|
||||
if (next && (p - end[i]).norm() < tol) joint = true;
|
||||
if (prev && (p - start[i]).norm() < tol) joint = true;
|
||||
if (!joint) { at = p; return true; }
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool sketch_closest_ends(const SketchEntity& A, const SketchEntity& B,
|
||||
SketchPointRole& ra, SketchPointRole& rb, Vec2d& pa, Vec2d& pb)
|
||||
{
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/CAD/GeometryEngine.hpp"
|
||||
|
||||
#include <gp_Pln.hxx>
|
||||
#include <gp_Ax1.hxx>
|
||||
#include <gp_Ax3.hxx>
|
||||
#include <gp_Vec.hxx>
|
||||
#include <TopoDS_Wire.hxx>
|
||||
#include <TopoDS_Shape.hxx>
|
||||
#include <TopoDS_Face.hxx>
|
||||
@@ -52,9 +53,11 @@ struct SketchPlane {
|
||||
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 {}; }
|
||||
// NOTE: XZ's stored normal (+Y) is the OPPOSITE of x_axis x y_axis (-Y). Kept as it is —
|
||||
// extrude directions and saved recipes depend on it — so anything that needs the frame's
|
||||
// own handedness takes x_axis.cross(y_axis) instead of `normal` (see make_elips).
|
||||
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}}; }
|
||||
|
||||
@@ -69,8 +72,6 @@ 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;
|
||||
|
||||
@@ -90,9 +91,9 @@ 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.
|
||||
// build, which is how a sketch got extruded into the wrong solid. The document's
|
||||
// own setting (CadDocument::auto_close_loops) is pushed 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);
|
||||
|
||||
@@ -108,8 +109,8 @@ enum class SketchConstraintType {
|
||||
// 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
|
||||
DistanceX, // signed dx between two points (eb - ea), projected onto the sketch X axis
|
||||
DistanceY, // signed dy between two points (eb - ea), 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
|
||||
};
|
||||
@@ -164,6 +165,15 @@ int sketch_entity_ends(const SketchEntity& e, std::pair<SketchPointRole, Vec2d>
|
||||
bool sketch_closest_ends(const SketchEntity& A, const SketchEntity& B,
|
||||
SketchPointRole& ra, SketchPointRole& rb, Vec2d& pa, Vec2d& pb);
|
||||
|
||||
// Where a CLOSED loop fails to bound one region, although every joint meets: two of its
|
||||
// entities touch somewhere other than a joint they share (the loop crosses itself), or a joint
|
||||
// where the curve turns straight back along itself (a cusp: an arc leaving a line tangent to it
|
||||
// but heading the other way). Either makes a face OCCT accepts and then builds an invalid solid
|
||||
// from. `order` lists the loop's entity indices in traversal order, as the chainer found them.
|
||||
// Lines and arcs are judged exactly; a loop holding any other kind is not judged (false).
|
||||
// On true, `at` is the offending point in sketch coordinates.
|
||||
bool sketch_loop_defect(const std::vector<SketchEntity>& ents, const std::vector<int>& order, Vec2d& at);
|
||||
|
||||
// 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 {
|
||||
@@ -200,33 +210,11 @@ ConstraintPlan plan_entity_constraint(const std::vector<SketchEntity>& ents,
|
||||
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:
|
||||
// Throws on a zero vector, for which OCCT returns a degenerate solid.
|
||||
static TopoDS_Shape make_prism(const TopoDS_Shape& base, const gp_Vec& vec);
|
||||
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,
|
||||
@@ -253,12 +241,10 @@ public:
|
||||
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);
|
||||
// Revolve the closed profile wire about `axis` (world) by angle_deg; a negative angle sweeps
|
||||
// the other way (Flip). The profile must lie to one side of the axis (Onshape rule): one that
|
||||
// straddles it sweeps through itself, and that is refused rather than returned broken.
|
||||
static TopoDS_Shape make_revolve(const TopoDS_Wire& wire, const gp_Ax1& axis, double angle_deg = 360.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
|
||||
@@ -314,7 +300,7 @@ public:
|
||||
// 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
|
||||
// is inward; a full circle counts as CCW, so +d shrinks it. 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);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "libslic3r/CAD/SketchImport.hpp"
|
||||
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/EmbossShape.hpp"
|
||||
#include "libslic3r/Emboss.hpp"
|
||||
#include "libslic3r/NSVGUtils.hpp"
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
@@ -9,6 +12,11 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <nanosvg/nanosvg.h>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -67,12 +75,18 @@ ImportRegions text_to_regions(const std::string& utf8, double size_mm,
|
||||
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 text_to_regions(utf8, size_mm,
|
||||
std::shared_ptr<const Emboss::FontFile>(Emboss::create_font_file(path.c_str())));
|
||||
}
|
||||
|
||||
ImportRegions text_to_regions(const std::string& utf8, double size_mm,
|
||||
const std::shared_ptr<const Emboss::FontFile>& ff)
|
||||
{
|
||||
if (utf8.empty() || size_mm <= 0.0 || !ff)
|
||||
return {};
|
||||
Emboss::FontFileWithCache fwc;
|
||||
fwc.font_file = ff;
|
||||
fwc.cache = std::make_shared<Emboss::Glyphs>();
|
||||
|
||||
FontProp prop(static_cast<float>(size_mm)); // per_glyph=false
|
||||
HealedExPolygons healed = Emboss::text2shapes(fwc, utf8.c_str(), prop);
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
|
||||
#include "libslic3r/Point.hpp" // Vec2d
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
namespace Emboss { struct FontFile; }
|
||||
|
||||
// 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.
|
||||
@@ -19,6 +22,11 @@ using ImportRegions = std::vector<ImportRegion>;
|
||||
// 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());
|
||||
// Same, from an already loaded font — the GUI resolves a system font (face, bold, italic)
|
||||
// to one and keeps it while the user types. Returns an empty vector when the font is null or
|
||||
// yields no shape.
|
||||
ImportRegions text_to_regions(const std::string& utf8, double size_mm,
|
||||
const std::shared_ptr<const Emboss::FontFile>& font);
|
||||
|
||||
// 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
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
#include "libslic3r/CAD/SketchInference.hpp"
|
||||
#include "libslic3r/CAD/SketchEngine.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <cstddef>
|
||||
#include <optional>
|
||||
#include <math.h>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -74,6 +80,18 @@ InferenceSnap infer_point_snap(const std::vector<SketchEntity>& entities,
|
||||
offer(InferenceSnap::Kind::Midpoint, ei, SketchPointRole::P0,
|
||||
Vec2d(e.center.x() + e.radius * std::cos(am),
|
||||
e.center.y() + e.radius * std::sin(am)));
|
||||
// Nearest point on the arc itself (PointOnObject candidate), as for a line or a
|
||||
// circle — the solver now takes a point on an arc's rim.
|
||||
const Vec2d v = query - e.center;
|
||||
const double n = v.norm();
|
||||
const double sweep = e.end_angle - e.start_angle;
|
||||
if (n > 1e-9 && e.radius > 1e-9 && std::abs(sweep) > 1e-9) {
|
||||
double u = (std::atan2(v.y(), v.x()) - e.start_angle) / sweep;
|
||||
while (u < 0.0) u += 2.0 * M_PI / std::abs(sweep);
|
||||
if (u > 0.02 && u < 0.98)
|
||||
offer(InferenceSnap::Kind::OnEdge, ei, SketchPointRole::Center,
|
||||
e.center + v * (e.radius / n));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SketchEntity::Type::Circle: {
|
||||
@@ -141,6 +159,10 @@ infer_relations(const std::vector<SketchEntity>& entities, int new_ei,
|
||||
{
|
||||
std::vector<SketchEntityConstraintDef> out;
|
||||
if (new_ei <= 0 || new_ei >= int(entities.size())) return out;
|
||||
// Ends count as meeting at the same tolerance the wire builder welds them at; floor keeps
|
||||
// exact coincidence meaningful when auto-close is switched off.
|
||||
const double weld = std::max(sketch_join_tol(), 1e-7);
|
||||
auto joined = [weld](const Vec2d& a, const Vec2d& b) { return (a - b).squaredNorm() <= weld * weld; };
|
||||
|
||||
// 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
|
||||
@@ -170,10 +192,9 @@ infer_relations(const std::vector<SketchEntity>& entities, int new_ei,
|
||||
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;
|
||||
// "Connected" is the wire builder's weld, so what the viewport shows joined is.
|
||||
const bool connected = joined(n.p0, o.p0) || joined(n.p0, o.p1) ||
|
||||
joined(n.p1, o.p0) || joined(n.p1, o.p1);
|
||||
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);
|
||||
@@ -202,7 +223,7 @@ infer_relations(const std::vector<SketchEntity>& entities, int new_ei,
|
||||
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;
|
||||
if (!joined(le[k], ce[m])) 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;
|
||||
@@ -210,7 +231,7 @@ infer_relations(const std::vector<SketchEntity>& entities, int new_ei,
|
||||
}
|
||||
} 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 (std::abs(r.norm() - cv.radius) > weld) continue;
|
||||
if (r.squaredNorm() < 1e-18) continue;
|
||||
tangent = std::abs(unsigned_angle(ldir, r) - M_PI / 2.0) <= ang_tol_rad;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#ifndef slic3r_SketchInference_hpp_
|
||||
#define slic3r_SketchInference_hpp_
|
||||
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/CAD/SketchEngine.hpp"
|
||||
#include <math.h>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <cmath>
|
||||
@@ -36,8 +38,12 @@ InferenceSnap infer_point_snap(const std::vector<SketchEntity>& entities,
|
||||
// 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.
|
||||
// One angular tolerance for every "is this relation already true?" inference, so a pair of
|
||||
// lines that reads as horizontal to one rule cannot read as not-quite-parallel to the other.
|
||||
inline constexpr double kSketchInferAngleTol = 3.0 * M_PI / 180.0;
|
||||
|
||||
std::optional<SketchConstraintType>
|
||||
infer_axis_constraint(const Vec2d& anchor, const Vec2d& tip, double ang_tol_rad = 3.0 * M_PI / 180.0);
|
||||
infer_axis_constraint(const Vec2d& anchor, const Vec2d& tip, double ang_tol_rad = kSketchInferAngleTol);
|
||||
|
||||
// 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.
|
||||
@@ -47,7 +53,7 @@ infer_axis_constraint(const Vec2d& anchor, const Vec2d& tip, double ang_tol_rad
|
||||
// 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 ang_tol_rad = kSketchInferAngleTol,
|
||||
double len_tol_frac = 0.01);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
#include "libslic3r/CAD/SketchSolver.hpp"
|
||||
#include "libslic3r/CAD/SketchEngine.hpp"
|
||||
|
||||
#include <math.h>
|
||||
#include <slvs.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
#include "libslic3r/Point.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -41,12 +47,15 @@ struct Build {
|
||||
{ return E(Slvs_MakePoint2d(++eh, g, wp, P(g, u), P(g, v))); }
|
||||
|
||||
// Generic constraint (entityC unused by Slvs_MakeConstraint — set it manually below).
|
||||
// `other` / `other2` pick the END point (point[2]) of entity A / B instead of its start
|
||||
// (point[1]) for the constraints that read an arc's endpoint (the tangencies).
|
||||
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_hEntity eA, Slvs_hEntity eB, Slvs_hEntity eC = 0, int other = 0, int other2 = 0)
|
||||
{
|
||||
Slvs_Constraint c = Slvs_MakeConstraint(++ch, G_SK, type, wp, val, ptA, ptB, eA, eB);
|
||||
c.entityC = eC;
|
||||
c.other = other;
|
||||
c.other2 = other2;
|
||||
cons.push_back(c);
|
||||
}
|
||||
};
|
||||
@@ -108,9 +117,15 @@ static SketchSolveResult solve_system(std::vector<SketchEntity>& entities,
|
||||
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));
|
||||
// A zero-radius circle is degenerate everywhere else (the wire builder, offset and
|
||||
// inference all reject it). It used to be seeded with radius 1 here and written
|
||||
// back, so any unrelated solve silently turned it into a 1 mm circle. It gets no
|
||||
// primitive: its centre still solves, anything needing the rim is skipped.
|
||||
if (e.radius > 1e-9) {
|
||||
s.rparam = b.P(G_SK, e.radius);
|
||||
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:
|
||||
@@ -170,6 +185,20 @@ static SketchSolveResult solve_system(std::vector<SketchEntity>& entities,
|
||||
switch (r) { case Role::P0: return e.p0; case Role::P1: return e.p1; case Role::Center: return e.center; }
|
||||
return e.p0;
|
||||
};
|
||||
// Which side of line `li` the point (ei, r) is on, in slvs' sign convention for the
|
||||
// in-workplane PT_LINE_DISTANCE: with d = point[0] - point[1] and a = point[0], the signed
|
||||
// distance has the sign of d x (p - a) (pinned by the tests "a point below the line stays
|
||||
// below it" and "tangent line to circle, line below it"). +1 when on the line.
|
||||
auto side_of = [&](int ei, Role r, int li) -> double {
|
||||
Vec2d a, bb;
|
||||
if (li == kSketchRefAxisX) { a = Vec2d(1, 0); bb = Vec2d(0, 0); }
|
||||
else if (li == kSketchRefAxisY) { a = Vec2d(0, 1); bb = Vec2d(0, 0); }
|
||||
else if (valid(li)) { a = entities[li].p0; bb = entities[li].p1; }
|
||||
else return 1.0;
|
||||
const Vec2d d = a - bb, ap = coordOf(ei, r) - a;
|
||||
const double cr = d.x() * ap.y() - d.y() * ap.x();
|
||||
return cr < 0.0 ? -1.0 : 1.0;
|
||||
};
|
||||
// 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); };
|
||||
|
||||
@@ -207,7 +236,7 @@ static SketchSolveResult solve_system(std::vector<SketchEntity>& entities,
|
||||
case CT::Collinear:
|
||||
ref_ok = primOf(c.ea) && primOf(c.eb); break;
|
||||
}
|
||||
if (!ref_ok) continue;
|
||||
if (!ref_ok) { out.skipped.push_back(int(&c - constraints.data())); 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);
|
||||
@@ -282,9 +311,36 @@ static SketchSolveResult solve_system(std::vector<SketchEntity>& entities,
|
||||
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 bool aCircle = aCurve && entities[c.ea].type == SketchEntity::Type::Circle;
|
||||
const bool bCircle = bCurve && entities[c.eb].type == SketchEntity::Type::Circle;
|
||||
if (aCurve && bCurve && (aCircle || bCircle)) {
|
||||
// CURVE_CURVE_TANGENT reads each curve's ENDPOINT, which a full circle does not
|
||||
// have: slvs asserts and takes the process down (the same trap the circle-line
|
||||
// case below documents). Two round curves are tangent exactly when their
|
||||
// centres are r1 + r2 apart (outside each other) or |r1 - r2| apart (one inside
|
||||
// the other); keep whichever the sketch is closer to now. Radii are captured as
|
||||
// constants, with the same caveat as the circle-line case.
|
||||
const SketchEntity& A = entities[c.ea];
|
||||
const SketchEntity& B = entities[c.eb];
|
||||
const double d = (A.center - B.center).norm();
|
||||
const double ext = A.radius + B.radius;
|
||||
const double in = std::abs(A.radius - B.radius);
|
||||
b.C(SLVS_C_PT_PT_DISTANCE, std::abs(d - ext) <= std::abs(d - in) ? ext : in,
|
||||
ptOf(c.ea, Role::Center), ptOf(c.eb, Role::Center), 0, 0);
|
||||
} else if (aCurve && bCurve) {
|
||||
// Tangent where the two arcs MEET: pick, for each arc, the endpoint closest to
|
||||
// the other arc's nearest endpoint. Always binding the start point (the old
|
||||
// behaviour) made a tangency at an arc's end act on its start instead.
|
||||
const SketchEntity& A = entities[c.ea];
|
||||
const SketchEntity& B = entities[c.eb];
|
||||
int oa = 0, ob = 0; double best = 1e300;
|
||||
for (int i = 0; i < 2; ++i)
|
||||
for (int j = 0; j < 2; ++j) {
|
||||
const double dd = ((i ? A.p1 : A.p0) - (j ? B.p1 : B.p0)).squaredNorm();
|
||||
if (dd < best) { best = dd; oa = i; ob = j; }
|
||||
}
|
||||
b.C(SLVS_C_CURVE_CURVE_TANGENT, 0, 0, 0, primOf(c.ea), primOf(c.eb), 0, oa, ob);
|
||||
} 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) {
|
||||
@@ -305,24 +361,47 @@ static SketchSolveResult solve_system(std::vector<SketchEntity>& entities,
|
||||
// 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,
|
||||
b.C(SLVS_C_PT_LINE_DISTANCE, side_of(ci, Role::Center, li) * 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));
|
||||
// The arc endpoint that touches the line is the one the tangency is at. The
|
||||
// constraint used to bind the START point always, so a fillet — its start on
|
||||
// one leg, its end on the other — forced both legs parallel.
|
||||
int other = 0;
|
||||
if (valid(ci) && valid(li)) {
|
||||
const SketchEntity& A = entities[ci];
|
||||
const SketchEntity& L = entities[li];
|
||||
auto seg_dist = [&](const Vec2d& p) {
|
||||
const Vec2d d = L.p1 - L.p0;
|
||||
const double t = d.squaredNorm() > 1e-18
|
||||
? std::clamp((p - L.p0).dot(d) / d.squaredNorm(), 0.0, 1.0) : 0.0;
|
||||
return (L.p0 + t * d - p).norm();
|
||||
};
|
||||
other = seg_dist(A.p1) < seg_dist(A.p0) ? 1 : 0;
|
||||
}
|
||||
b.C(SLVS_C_ARC_LINE_TANGENT, 0, 0, 0, primOf(ci), primOf(li), 0, other);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CT::PointOnLine:
|
||||
// slvs' in-workplane point-line distance is SIGNED. A negative stored value is an
|
||||
// explicit side; a positive one (what the UI stores) keeps the side the point is on
|
||||
// now. Passing |value| forced every point to the positive side, flipping any that
|
||||
// sat on the other one across the line.
|
||||
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);
|
||||
b.C(SLVS_C_PT_LINE_DISTANCE,
|
||||
c.value < 0.0 ? c.value : side_of(c.ea, c.ra, c.eb) * 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)
|
||||
// Point (ea,ra) lies on entity edge eb: a circle or arc rim -> PT_ON_CIRCLE (slvs
|
||||
// takes both), otherwise the segment line -> PT_ON_LINE. An arc used to fall to
|
||||
// PT_ON_LINE, which is not an equation about an arc at all.
|
||||
if (valid(c.eb) && (entities[c.eb].type == SketchEntity::Type::Circle ||
|
||||
entities[c.eb].type == SketchEntity::Type::Arc))
|
||||
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);
|
||||
@@ -433,6 +512,29 @@ static SketchSolveResult solve_system(std::vector<SketchEntity>& entities,
|
||||
e.start_angle = ns;
|
||||
e.end_angle = ns + sweep;
|
||||
e.radius = 0.5 * ((e.p0 - e.center).norm() + (e.p1 - e.center).norm());
|
||||
} else if (e.type == SketchEntity::Type::EllipseArc && s.center && e.radius > 1e-9 && e.rminor > 1e-9) {
|
||||
// The solver moves the centre and the two ends as free points (it has no conic), so
|
||||
// after a solve they need not agree with the stored angles, and the wire builder then
|
||||
// finds the arc's ends away from its vertices and drops the whole sketch. Re-derive
|
||||
// the parametric angles from the solved ends and put the ends back ON the ellipse.
|
||||
const double cr = std::cos(e.rotation), sr = std::sin(e.rotation);
|
||||
auto param = [&](const Vec2d& p) {
|
||||
const Vec2d d = p - e.center;
|
||||
const double x = d.x() * cr + d.y() * sr, y = -d.x() * sr + d.y() * cr;
|
||||
return std::atan2(y / e.rminor, x / e.radius);
|
||||
};
|
||||
auto at = [&](double t) {
|
||||
const double x = e.radius * std::cos(t), y = e.rminor * std::sin(t);
|
||||
return Vec2d(e.center.x() + x * cr - y * sr, e.center.y() + x * sr + y * cr);
|
||||
};
|
||||
const double old_sweep = e.end_angle - e.start_angle;
|
||||
double t0 = param(e.p0), t1 = param(e.p1);
|
||||
if (old_sweep >= 0.0) { while (t1 <= t0) t1 += 2.0 * M_PI; }
|
||||
else { while (t1 >= t0) t1 -= 2.0 * M_PI; }
|
||||
e.start_angle = t0;
|
||||
e.end_angle = t1;
|
||||
e.p0 = at(t0);
|
||||
e.p1 = at(t1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,6 +562,21 @@ static SketchSolveResult solve_system(std::vector<SketchEntity>& entities,
|
||||
// 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.
|
||||
// Degrees of freedom an entity has on its own, as the whole-system solve counts them.
|
||||
static int natural_dof(const SketchEntity& e)
|
||||
{
|
||||
switch (e.type) {
|
||||
case SketchEntity::Type::Line: return 4;
|
||||
case SketchEntity::Type::Point: return 2;
|
||||
case SketchEntity::Type::Circle: return e.radius > 1e-9 ? 3 : 2;
|
||||
case SketchEntity::Type::Arc: return 5; // centre + two ends, ends equidistant
|
||||
case SketchEntity::Type::Ellipse: return 2; // only the centre is a solver point
|
||||
case SketchEntity::Type::EllipseArc: return 6; // centre + two ends
|
||||
case SketchEntity::Type::BSpline: return 2 * int(e.ctrl.size());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static SketchSolveResult solve_partitioned(std::vector<SketchEntity>& entities,
|
||||
const std::vector<SketchEntityConstraintDef>& constraints,
|
||||
int dragged_ei, Role dragged_role)
|
||||
@@ -478,12 +595,18 @@ static SketchSolveResult solve_partitioned(std::vector<SketchEntity>& entities,
|
||||
};
|
||||
for (const auto& c : constraints) { unite(c.ea, c.eb); unite(c.ea, c.ec); }
|
||||
|
||||
// Group the constraints by the component they belong to.
|
||||
// Group the constraints by the component they belong to: that of the first ENTITY they
|
||||
// reference. ea can be a sketch reference (origin / axis, negative) while eb is the entity,
|
||||
// and such a constraint was dropped outright.
|
||||
std::map<int, std::vector<int>> groups;
|
||||
std::vector<bool> constrained(n, false);
|
||||
for (size_t i = 0; i < constraints.size(); ++i) {
|
||||
const int a = constraints[i].ea;
|
||||
if (a < 0 || a >= n) continue;
|
||||
const SketchEntityConstraintDef& c = constraints[i];
|
||||
const int a = (c.ea >= 0 && c.ea < n) ? c.ea : (c.eb >= 0 && c.eb < n) ? c.eb
|
||||
: (c.ec >= 0 && c.ec < n) ? c.ec : -1;
|
||||
if (a < 0) continue;
|
||||
groups[find(a)].push_back(int(i));
|
||||
for (int e : { c.ea, c.eb, c.ec }) if (e >= 0 && e < n) constrained[e] = true;
|
||||
}
|
||||
|
||||
SketchSolveResult out;
|
||||
@@ -509,7 +632,8 @@ static SketchSolveResult solve_partitioned(std::vector<SketchEntity>& entities,
|
||||
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; };
|
||||
// Sketch references (origin, axes: negative sentinels) are global and pass through.
|
||||
auto map1 = [&](int& e) { if (e >= 0) e = local.count(e) ? local[e] : -1; };
|
||||
map1(d.ea); map1(d.eb); map1(d.ec);
|
||||
subc.push_back(d);
|
||||
}
|
||||
@@ -522,8 +646,14 @@ static SketchSolveResult solve_partitioned(std::vector<SketchEntity>& entities,
|
||||
if (bi >= 0 && bi < int(cidx.size())) out.bad.push_back(cidx[bi]);
|
||||
}
|
||||
if (r.dof > 0) out.dof += r.dof;
|
||||
for (int si : r.skipped)
|
||||
if (si >= 0 && si < int(cidx.size())) out.skipped.push_back(cidx[si]);
|
||||
solved.emplace_back(std::move(ents), std::move(sub));
|
||||
}
|
||||
// Entities no constraint touches are in no group but still have their freedoms; the
|
||||
// whole-system solve counts them, so this path must too or the two report different dof.
|
||||
for (int i = 0; i < n; ++i)
|
||||
if (!constrained[i]) out.dof += natural_dof(entities[i]);
|
||||
if (!out.ok) return out;
|
||||
for (auto& [ents, sub] : solved)
|
||||
for (size_t k = 0; k < ents.size(); ++k) entities[ents[k]] = sub[k];
|
||||
|
||||
@@ -6,9 +6,12 @@
|
||||
// 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 { enum class SketchPointRole; }
|
||||
namespace Slic3r { struct SketchEntity; }
|
||||
namespace Slic3r { struct SketchEntityConstraintDef; }
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
struct SketchSolveResult {
|
||||
@@ -16,6 +19,10 @@ struct SketchSolveResult {
|
||||
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
|
||||
// Indices of constraints that were NOT applied because an entity they reference has no
|
||||
// solver representation for the role they need (an ellipse rim, a spline curve, a
|
||||
// zero-radius circle...). A solve can be ok with some skipped; callers should say so.
|
||||
std::vector<int> skipped;
|
||||
};
|
||||
|
||||
// Solve `constraints` over `entities` in place (writes solved coordinates back into the
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "libslic3r/CAD/ThreadStandards.hpp"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ struct ThreadSpec {
|
||||
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; }
|
||||
// Radial depth of the internal (tapped) thread, minor to major: (D - D1) / 2.
|
||||
double internal_depth_mm() const { return 0.5 * (major_diameter_mm - minor_diameter_mm()); }
|
||||
|
||||
bool imperial() const { return series == Series::UNC || series == Series::UNF; }
|
||||
};
|
||||
|
||||
@@ -85,6 +85,15 @@ set(lisbslic3r_sources
|
||||
BoundingBox.hpp
|
||||
BridgeDetector.cpp
|
||||
BridgeDetector.hpp
|
||||
BeltBrim.cpp
|
||||
BeltBrim.hpp
|
||||
BeltGCode.cpp
|
||||
BeltGCode.hpp
|
||||
BeltPurge.cpp
|
||||
BeltSliceStrategy.cpp
|
||||
BeltSliceStrategy.hpp
|
||||
BeltTransform.cpp
|
||||
BeltTransform.hpp
|
||||
Brim.cpp
|
||||
BrimEarsPoint.hpp
|
||||
Brim.hpp
|
||||
@@ -94,18 +103,17 @@ set(lisbslic3r_sources
|
||||
calib.hpp
|
||||
Circle.cpp
|
||||
Circle.hpp
|
||||
clipper.cpp
|
||||
clipper.hpp
|
||||
ClipperUtils.cpp
|
||||
ClipperUtils.hpp
|
||||
Clipper2Utils.cpp
|
||||
Clipper2Utils.hpp
|
||||
ClipperZUtils.cpp
|
||||
ClipperZUtils.hpp
|
||||
Color.cpp
|
||||
Color.hpp
|
||||
CommonDefs.hpp
|
||||
Config.cpp
|
||||
Config.hpp
|
||||
ConnectedBodies.cpp
|
||||
ConnectedBodies.hpp
|
||||
ContourZ.cpp
|
||||
CustomGCode.cpp
|
||||
CustomGCode.hpp
|
||||
@@ -174,6 +182,8 @@ set(lisbslic3r_sources
|
||||
Fill/FillPlanePath.hpp
|
||||
Fill/FillRectilinear.cpp
|
||||
Fill/FillRectilinear.hpp
|
||||
Fill/FillTpmsAdaptive.cpp
|
||||
Fill/FillTpmsAdaptive.hpp
|
||||
Fill/FillTpmsD.cpp
|
||||
Fill/FillTpmsD.hpp
|
||||
Fill/FillTpmsFK.cpp
|
||||
@@ -205,6 +215,8 @@ set(lisbslic3r_sources
|
||||
Format/3mf.hpp
|
||||
Format/AMF.cpp
|
||||
Format/AMF.hpp
|
||||
Format/AssembleList.cpp
|
||||
Format/AssembleList.hpp
|
||||
Format/DRC.cpp
|
||||
Format/DRC.hpp
|
||||
Format/bbs_3mf.cpp
|
||||
@@ -233,6 +245,14 @@ set(lisbslic3r_sources
|
||||
GCode/AdaptivePAProcessor.hpp
|
||||
GCode/AvoidCrossingPerimeters.cpp
|
||||
GCode/AvoidCrossingPerimeters.hpp
|
||||
GCode/BeltBackTransform.cpp
|
||||
GCode/BeltBackTransform.hpp
|
||||
GCode/MachineFrameTransform.cpp
|
||||
GCode/MachineFrameTransform.hpp
|
||||
GCode/BeltKinematics.cpp
|
||||
GCode/BeltKinematics.hpp
|
||||
GCode/MachineKinematics.cpp
|
||||
GCode/MachineKinematics.hpp
|
||||
GCode/ConflictChecker.cpp
|
||||
GCode/ConflictChecker.hpp
|
||||
GCode/CoolingBuffer.cpp
|
||||
@@ -261,8 +281,7 @@ set(lisbslic3r_sources
|
||||
GCode/SeamPlacer.hpp
|
||||
GCode/PreciseSeam.cpp
|
||||
GCode/PreciseSeam.hpp
|
||||
#GCodeSender.cpp
|
||||
#GCodeSender.hpp
|
||||
GCode/PreciseSeamInternal.hpp
|
||||
GCode/SmallAreaInfillFlowCompensator.cpp
|
||||
GCode/SmallAreaInfillFlowCompensator.hpp
|
||||
GCode/SpiralVase.cpp
|
||||
@@ -306,6 +325,14 @@ set(lisbslic3r_sources
|
||||
Geometry/VoronoiUtils.cpp
|
||||
Geometry/VoronoiUtils.hpp
|
||||
Geometry/VoronoiVisualUtils.hpp
|
||||
IMEXArrange.cpp
|
||||
IMEXArrange.hpp
|
||||
IMEXHelpers.cpp
|
||||
IMEXHelpers.hpp
|
||||
IMEXZones.cpp
|
||||
IMEXZones.hpp
|
||||
InstanceLock.cpp
|
||||
InstanceLock.hpp
|
||||
Int128.hpp
|
||||
KDTreeIndirect.hpp
|
||||
Layer.cpp
|
||||
@@ -378,6 +405,7 @@ set(lisbslic3r_sources
|
||||
Polyline.hpp
|
||||
PresetBundle.cpp
|
||||
PresetBundle.hpp
|
||||
PrinterBedAssets.hpp
|
||||
PresetCacheFormat.cpp
|
||||
PresetCacheFormat.hpp
|
||||
Preset.cpp
|
||||
@@ -445,7 +473,6 @@ set(lisbslic3r_sources
|
||||
SLA/SupportTreeBuildsteps.hpp
|
||||
SLA/SupportTree.cpp
|
||||
SLA/SupportTree.hpp
|
||||
#SLA/SupportTreeIGL.cpp
|
||||
SLA/SupportTreeMesher.cpp
|
||||
SLA/SupportTreeMesher.hpp
|
||||
SlicesToTriangleMesh.cpp
|
||||
@@ -454,6 +481,8 @@ set(lisbslic3r_sources
|
||||
SlicingAdaptive.hpp
|
||||
Slicing.cpp
|
||||
Slicing.hpp
|
||||
Support/BeltFloorContext.cpp
|
||||
Support/BeltFloorContext.hpp
|
||||
Support/SupportCommon.cpp
|
||||
Support/SupportCommon.hpp
|
||||
Support/SupportLayer.hpp
|
||||
@@ -583,10 +612,10 @@ set(CGAL_DO_NOT_WARN_ABOUT_CMAKE_BUILD_TYPE ON CACHE BOOL "" FORCE)
|
||||
|
||||
cmake_policy(PUSH)
|
||||
cmake_policy(SET CMP0011 NEW)
|
||||
# CGAL's config resets policies (cmake_minimum_required ...3.23), so a plain SET
|
||||
# CGAL's config resets policies (cmake_minimum_required ...3.31), so a plain SET
|
||||
# can't reach it; the default opts its Boost lookup into BoostConfig (CMP0167).
|
||||
set(CMAKE_POLICY_DEFAULT_CMP0167 NEW)
|
||||
find_package(CGAL REQUIRED)
|
||||
find_package(CGAL 6.2 REQUIRED)
|
||||
find_package(OpenCV REQUIRED core)
|
||||
find_package(assimp REQUIRED)
|
||||
unset(CMAKE_POLICY_DEFAULT_CMP0167)
|
||||
@@ -647,11 +676,8 @@ find_package(JPEG REQUIRED)
|
||||
find_package(Draco REQUIRED)
|
||||
|
||||
set(OCCT_LIBS
|
||||
TKXDESTEP
|
||||
TKSTEP
|
||||
TKSTEP209
|
||||
TKSTEPAttr
|
||||
TKSTEPBase
|
||||
TKDESTEP
|
||||
TKDE
|
||||
TKXCAF
|
||||
TKXSBase
|
||||
TKVCAF
|
||||
@@ -698,6 +724,22 @@ if (SLIC3R_CAD)
|
||||
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")
|
||||
if (WIN32)
|
||||
# OCCT installs RelWithDebInfo and Debug builds to bin/occti and bin/occtd, so each build
|
||||
# type's DLL directory is read from the imported target, falling back to the first installed
|
||||
# build, as CMake does when linking.
|
||||
get_target_property(_occt_configs TKernel IMPORTED_CONFIGURATIONS)
|
||||
list(GET _occt_configs 0 _occt_fallback)
|
||||
foreach (_build_config IN ITEMS DEBUG RELEASE RELWITHDEBINFO MINSIZEREL)
|
||||
set(_occt_config ${_occt_fallback})
|
||||
if (_build_config IN_LIST _occt_configs)
|
||||
set(_occt_config ${_build_config})
|
||||
endif ()
|
||||
get_target_property(_occt_dll TKernel IMPORTED_LOCATION_${_occt_config})
|
||||
get_filename_component(_occt_bin "${_occt_dll}" DIRECTORY)
|
||||
set(OCCT_BIN_DIR_${_build_config} "${_occt_bin}" CACHE INTERNAL "OCCT DLL directory for ${_build_config} builds")
|
||||
endforeach ()
|
||||
endif ()
|
||||
|
||||
target_link_libraries(libslic3r
|
||||
PUBLIC
|
||||
@@ -714,7 +756,6 @@ target_link_libraries(libslic3r
|
||||
${OCCT_LIBS}
|
||||
boost_libs
|
||||
cereal::cereal
|
||||
clipper
|
||||
Clipper2
|
||||
draco::draco
|
||||
glu-libtess
|
||||
@@ -733,6 +774,12 @@ target_link_libraries(libslic3r
|
||||
)
|
||||
|
||||
if(NOT WIN32)
|
||||
# OCCT's static targets name FreeType as a bare "freetype", which the macOS linker cannot
|
||||
# find; this target maps that name to FREETYPE_LIBRARIES.
|
||||
if (NOT TARGET freetype)
|
||||
add_library(freetype INTERFACE IMPORTED GLOBAL)
|
||||
target_link_libraries(freetype INTERFACE ${FREETYPE_LIBRARIES})
|
||||
endif ()
|
||||
# Link freetype for OCCT dependency (CAD operations need font rendering)
|
||||
target_link_libraries(libslic3r PRIVATE ${FREETYPE_LIBRARIES})
|
||||
if (NOT APPLE)
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
#define CSGMESH_HPP
|
||||
|
||||
#include <libslic3r/AnyPtr.hpp>
|
||||
#include <libslic3r/Point.hpp>
|
||||
#include <libslic3r/TriangleMesh.hpp>
|
||||
#include <admesh/stl.h>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r { namespace csg {
|
||||
|
||||
// A CSGPartT should be an object that can provide at least a mesh + trafo and an
|
||||
// associated csg operation. A collection of CSGPartT objects can then
|
||||
// be interpreted as one model and used in various contexts. It can be assembled
|
||||
// with CGAL or OpenVDB, rendered with OpenCSG or provided to a ray-tracer to
|
||||
// with CGAL or OpenVDB or provided to a ray-tracer to
|
||||
// deal with various parts of it according to the supported CSG types...
|
||||
//
|
||||
// A few simple templated interface functions are provided here and a default
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
#ifndef CSGMESHCOPY_HPP
|
||||
#define CSGMESHCOPY_HPP
|
||||
|
||||
#include "libslic3r/AnyPtr.hpp"
|
||||
#include "CSGMesh.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include <utility>
|
||||
#include <cstddef>
|
||||
|
||||
namespace Slic3r { namespace csg {
|
||||
|
||||
|
||||
@@ -3,9 +3,13 @@
|
||||
|
||||
#include "CSGMesh.hpp"
|
||||
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/SLA/Hollowing.hpp"
|
||||
#include "libslic3r/MeshSplitImpl.hpp"
|
||||
#include <utility>
|
||||
#include <memory>
|
||||
|
||||
namespace Slic3r { namespace csg {
|
||||
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
#ifndef PERFORMCSGMESHBOOLEANS_HPP
|
||||
#define PERFORMCSGMESHBOOLEANS_HPP
|
||||
|
||||
#include <cstddef>
|
||||
#include <stack>
|
||||
#include <utility>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include "CSGMesh.hpp"
|
||||
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Execution/ExecutionTBB.hpp"
|
||||
//#include "libslic3r/Execution/ExecutionSeq.hpp"
|
||||
#include "libslic3r/MeshBoolean.hpp"
|
||||
|
||||
@@ -3,8 +3,16 @@
|
||||
|
||||
#include "CSGMesh.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <cassert>
|
||||
#include <stack>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Execution/Execution.hpp"
|
||||
#include "libslic3r/TriangleMeshSlicer.hpp"
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/Execution/ExecutionTBB.hpp"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "CSGMesh.hpp"
|
||||
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
|
||||
namespace Slic3r { namespace csg {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user