mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-23 17:02:39 +00:00
Merge main
This commit is contained in:
@@ -73,6 +73,7 @@ using namespace nlohmann;
|
||||
#include "libslic3r/Thread.hpp"
|
||||
#include "libslic3r/BlacklistedLibraryCheck.hpp"
|
||||
#include "libslic3r/FlushVolCalc.hpp"
|
||||
#include "libslic3r/LayOnFace.hpp"
|
||||
|
||||
#include "libslic3r/Orient.hpp"
|
||||
#include "libslic3r/PNGReadWrite.hpp"
|
||||
@@ -85,6 +86,8 @@ using namespace nlohmann;
|
||||
#ifdef WIN32
|
||||
#include "dev-utils/BaseException.h"
|
||||
#endif
|
||||
#include "slic3r/Utils/MeshInspect.hpp"
|
||||
#include "slic3r/Utils/PaintCLI.hpp"
|
||||
#include "slic3r/GUI/PartPlate.hpp"
|
||||
#include "slic3r/GUI/BitmapCache.hpp"
|
||||
#include "slic3r/GUI/OpenGLManager.hpp"
|
||||
@@ -189,6 +192,9 @@ typedef struct _sliced_info {
|
||||
int wall_loops{0};
|
||||
std::vector<std::string> upward_machines;
|
||||
std::vector<std::string> downward_machines;
|
||||
// Structured slicing warnings for result.json, and whether --strict was on.
|
||||
nlohmann::json warnings = nlohmann::json::array();
|
||||
bool strict_mode {false};
|
||||
}sliced_info_t;
|
||||
std::vector<PrintBase::SlicingStatus> g_slicing_warnings;
|
||||
|
||||
@@ -424,6 +430,21 @@ static PrinterTechnology get_printer_technology(const DynamicConfig &config)
|
||||
return(ret);}
|
||||
#endif
|
||||
|
||||
// Records a structured slicing warning so a CI or scripted consumer can branch on
|
||||
// a stable `class` string instead of matching stderr. Warnings are kept on the
|
||||
// run's sliced_info and emitted as the top-level "warnings" array of result.json;
|
||||
// a non-empty array does not by itself mean the run failed. Under --strict a
|
||||
// NON_CRITICAL warning additionally ends the run non-zero.
|
||||
//
|
||||
// result.json is written on Linux only (see the guard in record_exit_reson), so
|
||||
// neither "warnings" nor "strict_mode" reaches Windows or macOS.
|
||||
static void cli_record_warning(sliced_info_t &sliced_info, const std::string &cls,
|
||||
nlohmann::json details = nlohmann::json::object())
|
||||
{
|
||||
details["class"] = cls;
|
||||
sliced_info.warnings.push_back(std::move(details));
|
||||
}
|
||||
|
||||
void record_exit_reson(std::string outputdir, int code, int plate_id, std::string error_message, sliced_info_t& sliced_info, std::map<std::string, std::string> key_values = std::map<std::string, std::string>())
|
||||
{
|
||||
#if defined(__linux__) || defined(__LINUX__)
|
||||
@@ -462,6 +483,9 @@ void record_exit_reson(std::string outputdir, int code, int plate_id, std::strin
|
||||
for (auto& iter: key_values)
|
||||
j[iter.first] = iter.second;
|
||||
|
||||
j["warnings"] = sliced_info.warnings;
|
||||
j["strict_mode"] = sliced_info.strict_mode;
|
||||
|
||||
boost::nowide::ofstream c;
|
||||
c.open(result_file, std::ios::out | std::ios::trunc);
|
||||
c << j.dump(1, '\t') << std::endl;
|
||||
@@ -1381,12 +1405,68 @@ int CLI::run(int argc, char **argv)
|
||||
bool need_skip = (skip_objects.size() > 0)?true:false;
|
||||
long long global_begin_time = 0, global_current_time;
|
||||
sliced_info_t sliced_info;
|
||||
// Read up front so result.json reports it for early failures too.
|
||||
sliced_info.strict_mode = m_config.opt_bool("strict");
|
||||
// --no-check skips the check behind the only NON_CRITICAL warning --strict acts on
|
||||
// (support needed but disabled), from the point it appears among the actions. The pair
|
||||
// would make --strict a no-op or depend on argument order, so refuse it.
|
||||
if (sliced_info.strict_mode && m_config.opt_bool("no_check")) {
|
||||
boost::nowide::cerr << "--strict cannot be combined with --no-check" << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
std::map<std::string, std::string> record_key_values;
|
||||
|
||||
ConfigOptionBool* downward_check_option = m_config.option<ConfigOptionBool>("downward_check");
|
||||
if (downward_check_option)
|
||||
downward_check = downward_check_option->value;
|
||||
|
||||
// --inspect-mesh prints its JSON and exits, so any action that does work of its
|
||||
// own (slicing, exporting) would be skipped without notice. Reject those up front;
|
||||
// only options that merely tune how the input is loaded may come along.
|
||||
if (std::find(m_actions.begin(), m_actions.end(), "inspect_mesh") != m_actions.end()) {
|
||||
static const std::set<std::string> inspect_compatible = { "inspect_mesh", "uptodate", "load_defaultfila", "min_save",
|
||||
"mtcpp", "mstpp", "no_check", "normative_check", "pipe" };
|
||||
for (const std::string &action : m_actions) {
|
||||
if (inspect_compatible.count(action) == 0) {
|
||||
std::string flag = action;
|
||||
std::replace(flag.begin(), flag.end(), '_', '-');
|
||||
boost::nowide::cerr << "--inspect-mesh cannot be combined with --" << flag << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
// Without input there is nothing to inspect; fail rather than print nothing and exit 0.
|
||||
if (m_input_files.empty() && m_config.opt_string("load_assemble_list").empty()) {
|
||||
boost::nowide::cerr << "--inspect-mesh needs an input file or --load-assemble-list" << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
|
||||
// --inspect-paint prints its JSON and exits, so any action that does work of its
|
||||
// own (slicing, exporting) would be skipped without notice. Reject those up front;
|
||||
// only options that merely tune how the input is loaded may come along.
|
||||
if (std::find(m_actions.begin(), m_actions.end(), "inspect_paint") != m_actions.end()) {
|
||||
static const std::set<std::string> inspect_compatible = { "inspect_paint", "uptodate", "load_defaultfila", "min_save",
|
||||
"mtcpp", "mstpp", "no_check", "normative_check", "pipe" };
|
||||
for (const std::string &action : m_actions) {
|
||||
if (inspect_compatible.count(action) == 0) {
|
||||
std::string flag = action;
|
||||
std::replace(flag.begin(), flag.end(), '_', '-');
|
||||
boost::nowide::cerr << "--inspect-paint cannot be combined with --" << flag << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
// Without input there is nothing to inspect; fail rather than print nothing and exit 0.
|
||||
if (m_input_files.empty() && m_config.opt_string("load_assemble_list").empty()) {
|
||||
boost::nowide::cerr << "--inspect-paint needs an input file or --load-assemble-list" << std::endl;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
|
||||
// --export-settings - writes its JSON to stdout, so reject every action or transform that may write there
|
||||
// too (--info, --help, --orient, slicing and exporting). The allowed ones do nothing when nothing is
|
||||
// sliced or exported.
|
||||
@@ -4810,6 +4890,64 @@ int CLI::run(int argc, char **argv)
|
||||
for (auto &o : model.objects)
|
||||
// this affects volumes:
|
||||
o->rotate(Geometry::deg2rad(m_config.opt_float(opt_key)), Y);
|
||||
} else if (opt_key == "ground_largest_face" || opt_key == "ground_face_normal" || opt_key == "ground_face_point") {
|
||||
// Each instance is laid on one of its lay-on-face planes, which are computed from the current part
|
||||
// transformations, so the rotations given before this option are respected. A direction or point is in
|
||||
// object coordinates, so it names the same face for every instance of an object.
|
||||
std::function<int(const std::vector<LayOnFacePlane>&, const Transform3d&)> pick;
|
||||
if (opt_key == "ground_largest_face") {
|
||||
if (m_config.opt_bool(opt_key))
|
||||
pick = [](const std::vector<LayOnFacePlane>& planes, const Transform3d&) { return find_largest_plane(planes); };
|
||||
} else {
|
||||
// Only options given on the command line reach this loop, so an empty value is malformed input too.
|
||||
const std::string& value = m_config.opt_string(opt_key);
|
||||
Vec3d v;
|
||||
int consumed = 0;
|
||||
if (sscanf(value.c_str(), "%lf,%lf,%lf%n", &v.x(), &v.y(), &v.z(), &consumed) != 3 || consumed != int(value.size()) ||
|
||||
!v.allFinite() || (opt_key == "ground_face_normal" && v.norm() < EPSILON)) {
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("Invalid params: %1% expects three comma-separated numbers, got \"%2%\"") % opt_key % value;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
if (opt_key == "ground_face_normal")
|
||||
pick = [v](const std::vector<LayOnFacePlane>& planes, const Transform3d&) { return find_plane_by_normal(planes, v); };
|
||||
else
|
||||
pick = [v](const std::vector<LayOnFacePlane>& planes, const Transform3d& inst_matrix) {
|
||||
return find_plane_at_point(planes, inst_matrix, v, 0.01);
|
||||
};
|
||||
}
|
||||
if (pick) {
|
||||
size_t laid = 0, missed = 0;
|
||||
for (auto& model : m_models) {
|
||||
model.add_default_instances();
|
||||
for (ModelObject* o : model.objects)
|
||||
for (size_t i = 0; i < o->instances.size(); ++i) {
|
||||
const Transform3d inst_matrix = o->instances[i]->get_matrix_no_offset();
|
||||
const std::vector<LayOnFacePlane> planes = lay_on_face_planes(*o, inst_matrix);
|
||||
if (planes.empty()) {
|
||||
// Small or smooth parts (e.g. a sphere) have no face to rest on; the gizmo offers none either.
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: object %2% has no face large enough to lay on, left as it is") % opt_key % o->name;
|
||||
continue;
|
||||
}
|
||||
const int idx = pick(planes, inst_matrix);
|
||||
if (idx < 0) {
|
||||
// Only a point can miss: with several objects it usually belongs to one of them.
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("%1%: no face of object %2% contains the point, left as it is") % opt_key % o->name;
|
||||
++missed;
|
||||
continue;
|
||||
}
|
||||
BOOST_LOG_TRIVIAL(info) << boost::format("%1%: object %2% instance %3% laid on the %4% mm2 face with normal %5%")
|
||||
% opt_key % o->name % i % planes[idx].area % planes[idx].normal.transpose();
|
||||
lay_on_face(*o, i, planes[idx].normal);
|
||||
++laid;
|
||||
}
|
||||
}
|
||||
if (laid == 0 && missed > 0) {
|
||||
BOOST_LOG_TRIVIAL(error) << boost::format("Invalid params: %1%: no face of any object contains the point") % opt_key;
|
||||
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||
flush_and_exit(CLI_INVALID_PARAMS);
|
||||
}
|
||||
}
|
||||
} else if (opt_key == "scale") {
|
||||
float ratio = m_config.opt_float(opt_key);
|
||||
if (ratio <= 0.f) {
|
||||
@@ -5969,6 +6107,53 @@ int CLI::run(int argc, char **argv)
|
||||
model.add_default_instances();
|
||||
model.print_info();
|
||||
}
|
||||
} else if (opt_key == "inspect_mesh") {
|
||||
// Machine-readable alternative to --info. Registered as an action so it satisfies the
|
||||
// "needs an action" check and bypasses the GUI fallback, then exits once the JSON is out.
|
||||
for (Model &model : m_models) {
|
||||
model.add_default_instances();
|
||||
Slic3r::MeshInspect::inspect_to_json(model, m_input_files, boost::nowide::cout);
|
||||
}
|
||||
boost::nowide::cout.flush();
|
||||
// Conflicting actions were rejected before loading. Finish like the end of run().
|
||||
// flush_and_exit() is not usable here: it prints "found error ..." to stdout,
|
||||
// which would corrupt the JSON.
|
||||
#if defined(__linux__) || defined(__LINUX__)
|
||||
if (g_cli_callback_mgr.is_started()) {
|
||||
PrintBase::SlicingStatus slicing_status{100, "All done, Success"};
|
||||
cli_status_callback(slicing_status);
|
||||
}
|
||||
g_cli_callback_mgr.stop();
|
||||
#endif
|
||||
for (Model &m : m_models)
|
||||
m.remove_backup_path_if_exist();
|
||||
record_exit_reson(outfile_dir, CLI_SUCCESS, plate_to_slice, cli_errors[CLI_SUCCESS], sliced_info);
|
||||
boost::nowide::cerr.flush();
|
||||
return CLI_SUCCESS;
|
||||
} else if (opt_key == "inspect_paint") {
|
||||
// --inspect-paint — read the per-facet enforcer/blocker/extruder/
|
||||
// fuzzy state from the loaded model and emit a JSON summary.
|
||||
// Machine-readable alternative to opening the paint gizmos.
|
||||
for (Model &model : m_models) {
|
||||
model.add_default_instances();
|
||||
Slic3r::PaintCLI::inspect_to_json(model, m_input_files, boost::nowide::cout);
|
||||
}
|
||||
boost::nowide::cout.flush();
|
||||
// The tooltip promises "then exit"; conflicting actions were rejected before
|
||||
// loading. Finish like the end of run(). flush_and_exit() is not usable here:
|
||||
// it prints "found error ..." to stdout, which would corrupt the JSON.
|
||||
#if defined(__linux__) || defined(__LINUX__)
|
||||
if (g_cli_callback_mgr.is_started()) {
|
||||
PrintBase::SlicingStatus slicing_status{100, "All done, Success"};
|
||||
cli_status_callback(slicing_status);
|
||||
}
|
||||
g_cli_callback_mgr.stop();
|
||||
#endif
|
||||
for (Model &m : m_models)
|
||||
m.remove_backup_path_if_exist();
|
||||
record_exit_reson(outfile_dir, CLI_SUCCESS, plate_to_slice, cli_errors[CLI_SUCCESS], sliced_info);
|
||||
boost::nowide::cerr.flush();
|
||||
return CLI_SUCCESS;
|
||||
} else if (opt_key == "uptodate") {
|
||||
//already processed before
|
||||
} else if (opt_key == "min_save") {
|
||||
@@ -6009,6 +6194,8 @@ int CLI::run(int argc, char **argv)
|
||||
export_3mf_file = m_config.opt_string(opt_key);
|
||||
}else if(opt_key=="no_check"){
|
||||
no_check = m_config.opt_bool(opt_key);
|
||||
}else if(opt_key=="strict"){
|
||||
//already read into sliced_info at the start of run()
|
||||
//} else if (opt_key == "export_gcode" || opt_key == "export_sla" || opt_key == "slice") {
|
||||
} else if (opt_key == "normative_check") {
|
||||
//already processed before
|
||||
@@ -6717,6 +6904,15 @@ int CLI::run(int argc, char **argv)
|
||||
|
||||
if (status.warning_level == PrintStateBase::WarningLevel::NON_CRITICAL) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "plate "<< index+1<< ": found NON_CRITICAL slicing warnings: "<<status.text <<std::endl;
|
||||
// Always record for AI/CI consumers; under --strict, elevate to a
|
||||
// non-zero exit so scripted pipelines don't ship a "warning OK" slice.
|
||||
cli_record_warning(sliced_info, "slicing_warning_non_critical",
|
||||
nlohmann::json{{"plate_id", index+1}, {"text", status.text}});
|
||||
if (sliced_info.strict_mode) {
|
||||
sliced_info.sliced_plates.push_back(sliced_plate_info);
|
||||
record_exit_reson(outfile_dir, CLI_SLICING_ERROR, index+1, cli_errors[CLI_SLICING_ERROR], sliced_info);
|
||||
flush_and_exit(CLI_SLICING_ERROR);
|
||||
}
|
||||
}
|
||||
else {
|
||||
BOOST_LOG_TRIVIAL(warning) << boost::format("plate %1%: found slicing warnings: %2%, no_check=%3%")%(index+1) %status.text %no_check;
|
||||
|
||||
@@ -315,6 +315,26 @@ void AppConfig::set_defaults()
|
||||
if (get("zoom_to_mouse").empty())
|
||||
set_bool("zoom_to_mouse", false);
|
||||
|
||||
#ifdef SLIC3R_CAD
|
||||
// Experimental parametric Design tab. Off by default: the tab is not created at all
|
||||
// until this is turned on, so nothing it builds reaches an unsuspecting user.
|
||||
if (get("enable_cad_feature").empty())
|
||||
set_bool("enable_cad_feature", false);
|
||||
|
||||
// Auto-weld sketch endpoints within kSketchJoinTol when building closed loops.
|
||||
// Default ON: it is what the ~90% case wants; OFF makes the kernel demand an exact
|
||||
// joint. The GUI pushes it into SketchEngine via set_sketch_auto_close().
|
||||
if (get("auto_close_sketch_loops").empty())
|
||||
set_bool("auto_close_sketch_loops", true);
|
||||
|
||||
// Design tab: draw a mate connector as a face rather than as the abstract disc + roll
|
||||
// quadrant. Defaults ON — face orientation is hardwired perception, so the roll and the
|
||||
// verse read without being learned, which no abstract glyph achieves. Turning it off
|
||||
// restores the conventional CAD representation for users who expect it (x0kd).
|
||||
if (get("design_connector_face_glyph").empty())
|
||||
set_bool("design_connector_face_glyph", true);
|
||||
#endif
|
||||
|
||||
//#ifdef SUPPORT_SHOW_HINTS
|
||||
if (get("show_hints").empty())
|
||||
set_bool("show_hints", false);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,776 @@
|
||||
#ifndef slic3r_CadDocument_hpp_
|
||||
#define slic3r_CadDocument_hpp_
|
||||
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/CAD/SketchEngine.hpp"
|
||||
#include "libslic3r/CAD/GeometryEngine.hpp" // FaceGroup
|
||||
#include "libslic3r/Color.hpp" // ColorRGBA (per-body display colour override)
|
||||
|
||||
#include <TopoDS_Shape.hxx>
|
||||
#include <TopoDS_Wire.hxx>
|
||||
#include <cereal/cereal.hpp>
|
||||
#include <cereal/types/vector.hpp>
|
||||
#include <cereal/types/string.hpp>
|
||||
#include <map>
|
||||
#include <cereal/types/map.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
enum class CadFeatureType { Sketch, Extrude, Fillet, Chamfer, Hole, Thread, Shell, Revolve, Sweep, Pattern, Plane, Loft, Draft, Import, Boolean, Cut, Mirror, Axis, CoordSys, Helix, Transform, Thicken, Project, DeleteFace, Rib, SurfaceExtrude, SurfaceRevolve, ThickenSurface, SurfaceOffset, SurfaceLoft, SurfaceFill, Mate };
|
||||
enum class SketchShape { Rectangle, Circle };
|
||||
enum class PlaneType { Offset, Angle, Midplane, Tangent, TwoEdges, Coincident };
|
||||
enum class AxisType { TwoPoints, FaceNormal, CylinderCenterline, PlaneIntersection, AlongEdge };
|
||||
enum class CoordSysType { PointWorld, FaceAndDirection };
|
||||
enum class BooleanMode { New, Add, Cut, Intersect };
|
||||
|
||||
enum class ExtrudeEnd { Blind, Symmetric, TwoSided, ThroughAll, UpToFace, UpToVertex };
|
||||
|
||||
// Serialize a TopoDS_Shape to/from a BRep string (declared before CadFeature so its
|
||||
// inline cereal save()/load() can resolve these non-dependent calls).
|
||||
std::string brep_to_string(const TopoDS_Shape& s);
|
||||
TopoDS_Shape brep_from_string(const std::string& d);
|
||||
|
||||
struct CadFeature {
|
||||
CadFeatureType type{CadFeatureType::Sketch};
|
||||
std::string name;
|
||||
bool enabled{true};
|
||||
|
||||
// Sketch params (centered on the plane origin)
|
||||
SketchShape shape{SketchShape::Rectangle};
|
||||
SketchPlane plane{SketchPlane::XY()};
|
||||
double width{20};
|
||||
double height{20};
|
||||
double radius{10};
|
||||
|
||||
// Real 2D sketch geometry (Onshape-style). When non-empty this takes
|
||||
// precedence over the shape/width/height/radius enum path in build_sketch_wire.
|
||||
SketchProfile profile;
|
||||
|
||||
// Onshape-style multi-entity sketch geometry. When non-empty this takes
|
||||
// precedence over both `profile` and the shape-enum path in build_sketch_wire.
|
||||
std::vector<SketchEntity> entities;
|
||||
|
||||
// 2D geometric constraints on `profile` (point indices). Solved in place.
|
||||
std::vector<SketchConstraintDef> constraints;
|
||||
|
||||
// Onshape-style constraints on `entities` (Fase 4.2). Solved in place against
|
||||
// entity endpoints. Used when `entities` is non-empty (the legacy `constraints`
|
||||
// vector applies only to the `profile` path).
|
||||
std::vector<SketchEntityConstraintDef> entity_constraints;
|
||||
|
||||
// Imported rigid 2D art (Text glyphs / SVG vector paths) as filled regions.
|
||||
// Each region: contour[0] = outer loop, contour[1..] = holes; points in
|
||||
// plane (u,v) millimetres. Rendered as a sketch overlay and extruded via a
|
||||
// faces-with-holes path (SketchEngine::make_extrude_regions) — deliberately
|
||||
// NOT solver entities, so imported art contributes zero DoF and never
|
||||
// pollutes the constraint solver / DoF readout. When non-empty it takes
|
||||
// precedence over the entities/profile/shape paths in the Extrude case.
|
||||
std::vector<std::vector<std::vector<Vec2d>>> imported_regions;
|
||||
|
||||
// Imported rigid 3D B-rep solid (STEP). When the feature type is Import this carries
|
||||
// the OCCT shape verbatim — it is adopted as a base body in route_feature (no parametric
|
||||
// recipe). Downstream face/edge features (fillet/chamfer/cut/shell/...) act on it like any
|
||||
// other body. TopoDS_Shape is a cheap handle, so copying it through recompute/checkpoint
|
||||
// snapshots is cheap. In-session only for now (no BRep serialization yet).
|
||||
TopoDS_Shape imported_solid;
|
||||
|
||||
// Non-destructive placement transform for imported_regions (Text/SVG),
|
||||
// applied at display + extrude time as
|
||||
// p -> (p.x*import_scale_x + import_offset.x, p.y*import_scale_y + import_offset.y).
|
||||
// Lets the art be moved / enlarged / stretched (independent X/Y) repeatedly
|
||||
// without re-vectorising. Identity = no change.
|
||||
Vec2d import_offset{0, 0};
|
||||
double import_scale_x{1.0};
|
||||
double import_scale_y{1.0};
|
||||
// Text/SVG dropped ONTO a solid face (centred on it): the extrude then defaults to an
|
||||
// inward Cut (engraving) targeting `import_face_body`. False = free art on a plane.
|
||||
bool import_on_face{false};
|
||||
int import_face_body{-1};
|
||||
|
||||
// Extrude params
|
||||
int sketch_ref{-1}; // index into features[] of the consumed sketch
|
||||
double distance{10};
|
||||
bool symmetric{false};
|
||||
BooleanMode mode{BooleanMode::New};
|
||||
ExtrudeEnd extrude_end{ExtrudeEnd::Blind};
|
||||
double distance2{0}; // second-side depth for TwoSided
|
||||
double taper_deg{0}; // draft angle (C4-part2)
|
||||
bool flip{false}; // reverse the extrude direction (negate plane normal)
|
||||
int up_to_face{-1}; // target solid-face id for UpToFace (C4-part2)
|
||||
int extrude_src_face{-1}; // global face id on the current body to extrude as a profile; -1 = use sketch wire
|
||||
Vec3d up_to_point{0,0,0}; // target for UpToVertex (C4-part2)
|
||||
// Multi-body target: which body (index into CadDocument::bodies) this feature acts on.
|
||||
// -1 = auto (last body). A New extrude appends a fresh body; Add/Cut/Intersect, dress-up,
|
||||
// hole and face-extrude(non-New) mutate bodies[target]; face-extrude reads its source
|
||||
// face from bodies[target] too. The source-face owner for face-extrude lives here.
|
||||
int target_body{-1};
|
||||
|
||||
// Dress-up params (Fillet/Chamfer) — applied to the current body in order
|
||||
double dressup_size{1.0}; // fillet radius or chamfer distance
|
||||
FaceGroup face_group{FaceGroup::All};
|
||||
int dressup_edge{-1}; // global edge id for edge-targeted fillet/chamfer; -1 = use face_group
|
||||
|
||||
// Hole params (positioned circular cut into the current body)
|
||||
double hole_diameter{5};
|
||||
double hole_depth{10};
|
||||
bool hole_through{true}; // true = symmetric through-cut, ignores hole_depth
|
||||
double hole_x{0}; // position on the plane (plane u/x axis)
|
||||
double hole_y{0}; // position on the plane (plane v/y axis)
|
||||
|
||||
// Hole standards library (extends the plain bore above).
|
||||
// hole_style: 0 = simple, 1 = counterbore, 2 = countersink.
|
||||
int hole_style{0};
|
||||
double hole_cbore_diameter{0}; // counterbore cylinder diameter (mm), style==1
|
||||
double hole_cbore_depth{0}; // counterbore depth from the entry face (mm), style==1
|
||||
double hole_csink_diameter{0}; // countersink major diameter at entry face (mm), style==2
|
||||
double hole_csink_angle{90}; // countersink included angle (degrees), style==2
|
||||
std::string hole_standard; // provenance only, e.g. "M6" / "1/4-20"; not used by geometry
|
||||
|
||||
// Thread params (helical thread about the plane normal at a positioned point)
|
||||
double thread_radius{5}; // nominal cylinder radius
|
||||
double thread_pitch{2}; // axial advance per turn
|
||||
double thread_height{10}; // total axial length
|
||||
double thread_depth{1}; // radial crest depth of the thread profile
|
||||
bool thread_internal{false}; // false = external threaded rod (New body);
|
||||
// true = tapped bore cut into the current body
|
||||
double thread_x{0}; // axis position on the plane (u/x axis)
|
||||
double thread_y{0}; // axis position on the plane (v/y axis)
|
||||
|
||||
// Shell params (hollow the current body to a wall thickness, removing one open face)
|
||||
double shell_thickness{2}; // wall thickness (inward offset)
|
||||
int shell_face{-1}; // global face id to remove (open the shell); -1 = none
|
||||
|
||||
// Draft params (taper a single solid face about a neutral plane = body bbox bottom, pull +Z)
|
||||
int draft_face{-1}; // global face id to draft; -1 = none
|
||||
double draft_angle{5}; // draft angle in degrees (signed: + leans the face inward)
|
||||
|
||||
// Revolve params (sweep a profile about an in-plane axis through the plane origin).
|
||||
// Reuses sketch_ref / entities (profile), flip (direction), mode (boolean) and
|
||||
// target_body. revolve_axis: 0 = plane X axis, 1 = plane Y axis.
|
||||
double revolve_angle{360}; // sweep angle in degrees (1..360)
|
||||
int revolve_axis{0}; // 0 = plane X, 1 = plane Y
|
||||
|
||||
// Sweep: profile carried by sketch_ref / entities (like Extrude); the spine is a
|
||||
// second Sketch referenced by sweep_path_ref (an open or closed wire). Reuses
|
||||
// mode (boolean) and target_body.
|
||||
int sweep_path_ref{-1}; // index into features[] of the path Sketch
|
||||
|
||||
// Loft: build a solid through 2+ closed profile Sketches (loft_profile_refs, in
|
||||
// order, each on its own plane). loft_ruled=false → smooth sections, true → ruled.
|
||||
// Reuses mode (boolean) and target_body.
|
||||
std::vector<int> loft_profile_refs; // ordered indices into features[] of profile Sketches
|
||||
bool loft_ruled{false};
|
||||
|
||||
// Pattern: replicate the target body, copies fused into it. pattern_circular=false
|
||||
// → linear (pattern_count instances spaced pattern_spacing along plane axis
|
||||
// pattern_dir: 0=X, 1=Y); true → circular (pattern_count instances over
|
||||
// pattern_angle° total about the plane normal through the plane origin, so a seed
|
||||
// offset from the origin orbits the axis). Reuses target_body + plane.
|
||||
bool pattern_circular{false};
|
||||
int pattern_count{3}; // total instances incl. the seed (>=1)
|
||||
double pattern_spacing{20}; // linear step (mm)
|
||||
int pattern_dir{0}; // linear direction: 0 = plane X, 1 = plane Y
|
||||
double pattern_angle{360}; // circular total angle (degrees)
|
||||
|
||||
// Pattern along a curve: when pattern_curve_sketch >= 0 this mode takes precedence over
|
||||
// linear/circular. Copies are placed at equal-parameter points along entity
|
||||
// pattern_curve_entity of sketch pattern_curve_sketch, translated by (P_i - P_0).
|
||||
int pattern_curve_sketch{-1}; // feature index of the Sketch holding the guide curve
|
||||
int pattern_curve_entity{-1}; // entity index of the guide curve within that sketch
|
||||
|
||||
// Parametric bindings: field-member-name -> expression string. On recompute() each entry
|
||||
// is evaluated against the document variables and written into the named numeric field
|
||||
// BEFORE geometry runs. Empty (the common case) means the feature uses its literal fields.
|
||||
std::map<std::string, std::string> expr;
|
||||
|
||||
// Datum/reference plane: a derived SketchPlane the document offers as a selectable
|
||||
// sketch plane (no solid). plane_base selects the reference (0=XY,1=XZ,2=YZ, or 3+N
|
||||
// = the Nth earlier datum plane); plane_offset shifts along the base normal;
|
||||
// plane_angle tilts plane_angle° about the base axis plane_axis (0=base X, 1=base Y).
|
||||
int plane_base{0};
|
||||
double plane_offset{20};
|
||||
double plane_angle_tilt{0}; // degrees (named *_tilt to avoid clash w/ revolve)
|
||||
int plane_axis{0}; // tilt axis: 0 = base X, 1 = base Y
|
||||
PlaneType plane_type{PlaneType::Offset};
|
||||
int plane_face_body{-1};
|
||||
int plane_face{-1};
|
||||
int plane_face2_body{-1};
|
||||
int plane_face2{-1};
|
||||
int plane_edge_body{-1};
|
||||
int plane_edge{-1};
|
||||
int plane_edge2_body{-1};
|
||||
int plane_edge2{-1};
|
||||
double plane_u_size{60};
|
||||
double plane_v_size{60};
|
||||
|
||||
// Boolean: combine two EXISTING bodies. `mode` reuses BooleanMode (Add = union,
|
||||
// Cut = subtract tool from target, Intersect = keep overlap; New unused). `target_body`
|
||||
// is the body that survives (result written back to it); `bool_tool_body` is the other
|
||||
// operand, consumed (erased) unless `bool_keep_tool`. `bool_tolerance` = OCCT fuzzy value
|
||||
// (0 = exact). Per-face merge: when both bool_target_face/bool_tool_face are set, the tool
|
||||
// is first snapped so those two faces are coincident (gap closed within bool_tolerance),
|
||||
// then the boolean welds them and coplanar faces are unified into one clean face.
|
||||
int bool_tool_body{-1};
|
||||
bool bool_keep_tool{false};
|
||||
double bool_tolerance{0.0};
|
||||
int bool_target_face{-1}; // global face id on the target body to mate (-1 = none)
|
||||
int bool_tool_face{-1}; // global face id on the tool body to mate (-1 = none)
|
||||
|
||||
// Cut: split one target body with a plane, keeping the upper half, lower half, or both.
|
||||
// Reuses `plane` for the cut plane and `target_body` for which body is cut.
|
||||
double cut_offset{0.0}; // offset along the cut-plane normal (mm)
|
||||
bool cut_flip{false}; // flip the normal => swaps which side is "upper"
|
||||
bool cut_keep_upper{true}; // keep the +normal half
|
||||
bool cut_keep_lower{false}; // keep the -normal half (both => split into two bodies)
|
||||
|
||||
// Mirror: reflect a body about a plane. Reuses `plane` (mirror plane, as Cut does),
|
||||
// `target_body` (body to mirror), and `mode` (New = separate mirrored copy,
|
||||
// Add = fuse the mirror back into the source). mirror_keep_original decides whether
|
||||
// the source body survives when mode is New.
|
||||
bool mirror_keep_original{true};
|
||||
|
||||
// Datum axis: reference line (no solid). Construction params stored; resolve_datum_axes()
|
||||
// computes the world-space origin + unit direction on demand.
|
||||
AxisType axis_type{AxisType::TwoPoints};
|
||||
Vec3d axis_p1{0, 0, 0};
|
||||
Vec3d axis_p2{0, 0, 10};
|
||||
int axis_body{-1};
|
||||
int axis_face{-1};
|
||||
int axis_edge{-1};
|
||||
int axis_plane_a{-1};
|
||||
int axis_plane_b{-1};
|
||||
|
||||
// Datum coordinate system (no solid). Stored as point + two orthonormal axes.
|
||||
CoordSysType coordsys_type{CoordSysType::PointWorld};
|
||||
Vec3d coordsys_point{0, 0, 0};
|
||||
int coordsys_body{-1};
|
||||
int coordsys_face{-1};
|
||||
int coordsys_edge{-1};
|
||||
Vec3d coordsys_x_hint{1, 0, 0};
|
||||
|
||||
// Fingerprint of the face this connector was bound to, for drift detection. -1 = not yet
|
||||
// recorded (an old recipe, or a connector that has never resolved).
|
||||
//
|
||||
// Surface TYPE and EDGE COUNT specifically, because they survive every legitimate edit:
|
||||
// Transform moves the body, Draft tilts the face, a dimension change resizes it, and none
|
||||
// of those change either value. Centroid, area and normal all fail that test — see the
|
||||
// issue. The cost is that a slide from one planar 4-edge face to another planar 4-edge face
|
||||
// is invisible; a detector that never cries wolf is worth more here than a total one.
|
||||
int coordsys_face_kind{-1}; // GeomAbs_SurfaceType as int
|
||||
int coordsys_face_edges{-1}; // number of edges bounding the face
|
||||
|
||||
// Helix curve params (consumed as a sweep path to build springs/coils/augers).
|
||||
// Axis = plane normal through plane origin. pitch = axial rise per full turn.
|
||||
// left_handed flips the winding direction. taper_deg != 0 gives a conical helix.
|
||||
double helix_radius{10};
|
||||
double helix_pitch{5};
|
||||
double helix_height{20};
|
||||
bool helix_left_handed{false};
|
||||
double helix_taper_deg{0};
|
||||
|
||||
// Transform feature: rigid move/rotate of an existing body. Rotation is applied
|
||||
// first (about xf_axis through xf_pivot), then the translation.
|
||||
Vec3d xf_translate{0, 0, 0};
|
||||
Vec3d xf_axis{0, 0, 1};
|
||||
Vec3d xf_pivot{0, 0, 0};
|
||||
double xf_angle_deg{0};
|
||||
bool xf_copy{false}; // true: keep the original, append the moved copy as a new body
|
||||
|
||||
// Thicken feature: offset one face of an existing body into a new thin solid body.
|
||||
// The face belongs to `target_body`; the offset runs along the face normal.
|
||||
int thicken_face{-1}; // global face id on the target body; -1 = invalid
|
||||
double thicken_thickness{2}; // wall thickness (always used as |value|)
|
||||
bool thicken_flip{false}; // true: offset against the face normal
|
||||
|
||||
// Cut-by-face: when cut_face >= 0, apply_cut derives the cut plane from this face
|
||||
// (via SketchPlane::from_face) instead of the base `plane`. cut_offset / cut_flip
|
||||
// still apply along the derived normal.
|
||||
int cut_face_body{-1}; // body owning the face; -1 = the target body
|
||||
int cut_face{-1}; // global face id to cut along; -1 = use `plane`
|
||||
|
||||
// Project feature: convert edges of an existing solid into sketch entities on `plane`.
|
||||
int project_source_body{-1}; // body owning the edges; -1 = last body
|
||||
std::vector<int> project_edges; // global edge ids to project; empty => use project_face
|
||||
int project_face{-1}; // if project_edges empty, project every edge of this face
|
||||
|
||||
// Direct edit: faces to remove (global face indices into target_body's shape),
|
||||
// healed via BRepAlgoAPI_Defeaturing.
|
||||
std::vector<int> delete_faces;
|
||||
|
||||
// Rib: a thin wall grown from an open sketch line, fused to the body.
|
||||
int rib_sketch_ref{-1}; // feature index of the Sketch holding the profile
|
||||
int rib_entity{-1}; // index of the open Line entity within that sketch
|
||||
double rib_thickness{2}; // wall thickness (mm), centred on the line
|
||||
double rib_depth{10}; // extrude distance along the sketch-plane normal (mm)
|
||||
|
||||
// --- Mate (assembly) ---
|
||||
// 0 Fastened — all 6 DOF fixed: B's frame is driven onto A's exactly.
|
||||
// 1 Planar — z axes aligned, normal distance set to mate_offset; in-plane position free.
|
||||
// 2 Revolute — axes collinear, position on the axis fixed; rotation about z free.
|
||||
// 3 Slider — orientation fully fixed, perpendicular position fixed; axial slide free.
|
||||
// 4 Cylindrical— axes collinear, perpendicular fixed; both spin and axial slide free.
|
||||
// A "free" DOF is preserved from the body's current placement, not zeroed.
|
||||
int mate_kind{0};
|
||||
int mate_cs_a{-1}; // feature index of the FIXED CoordSys (mate connector A)
|
||||
int mate_cs_b{-1}; // feature index of the CoordSys on the body that MOVES
|
||||
double mate_offset{0}; // translation along A's z, mm
|
||||
double mate_angle{0}; // rotation about A's z, degrees
|
||||
bool mate_flip{false}; // oppose the two z axes (face-to-face)
|
||||
|
||||
template<class Archive>
|
||||
void save(Archive& ar) const {
|
||||
std::string brep = (type == CadFeatureType::Import) ? brep_to_string(imported_solid) : std::string();
|
||||
ar(type, name, enabled, shape, plane, width, height, radius,
|
||||
profile, entities, constraints, entity_constraints, imported_regions,
|
||||
import_offset, import_scale_x, import_scale_y, import_on_face, import_face_body,
|
||||
sketch_ref, distance, symmetric, mode, extrude_end, distance2, taper_deg, flip,
|
||||
up_to_face, extrude_src_face, up_to_point, target_body,
|
||||
dressup_size, face_group, dressup_edge,
|
||||
hole_diameter, hole_depth, hole_through, hole_x, hole_y,
|
||||
thread_radius, thread_pitch, thread_height, thread_depth, thread_internal, thread_x, thread_y,
|
||||
shell_thickness, shell_face,
|
||||
draft_face, draft_angle,
|
||||
revolve_angle, revolve_axis,
|
||||
sweep_path_ref, loft_profile_refs, loft_ruled,
|
||||
pattern_circular, pattern_count, pattern_spacing, pattern_dir, pattern_angle,
|
||||
plane_base, plane_offset, plane_angle_tilt, plane_axis,
|
||||
bool_tool_body, bool_keep_tool, bool_tolerance, bool_target_face, bool_tool_face,
|
||||
cut_offset, cut_flip, cut_keep_upper, cut_keep_lower,
|
||||
brep,
|
||||
plane_type, plane_face_body, plane_face, plane_face2_body, plane_face2,
|
||||
plane_edge_body, plane_edge, plane_edge2_body, plane_edge2, plane_u_size, plane_v_size,
|
||||
mirror_keep_original,
|
||||
axis_type, axis_p1, axis_p2, axis_body, axis_face, axis_edge, axis_plane_a, axis_plane_b,
|
||||
coordsys_type, coordsys_point, coordsys_body, coordsys_face, coordsys_edge, coordsys_x_hint,
|
||||
helix_radius, helix_pitch, helix_height, helix_left_handed, helix_taper_deg,
|
||||
xf_translate, xf_axis, xf_pivot, xf_angle_deg, xf_copy,
|
||||
thicken_face, thicken_thickness, thicken_flip,
|
||||
cut_face_body, cut_face,
|
||||
project_source_body, project_edges, project_face,
|
||||
delete_faces,
|
||||
hole_style, hole_cbore_diameter, hole_cbore_depth,
|
||||
hole_csink_diameter, hole_csink_angle, hole_standard,
|
||||
rib_sketch_ref, rib_entity, rib_thickness, rib_depth,
|
||||
pattern_curve_sketch, pattern_curve_entity,
|
||||
expr,
|
||||
mate_kind, mate_cs_a, mate_cs_b, mate_offset, mate_angle, mate_flip,
|
||||
coordsys_face_kind, coordsys_face_edges);
|
||||
}
|
||||
template<class Archive>
|
||||
void load(Archive& ar) {
|
||||
std::string brep;
|
||||
ar(type, name, enabled, shape, plane, width, height, radius,
|
||||
profile, entities, constraints, entity_constraints, imported_regions,
|
||||
import_offset, import_scale_x, import_scale_y, import_on_face, import_face_body,
|
||||
sketch_ref, distance, symmetric, mode, extrude_end, distance2, taper_deg, flip,
|
||||
up_to_face, extrude_src_face, up_to_point, target_body,
|
||||
dressup_size, face_group, dressup_edge,
|
||||
hole_diameter, hole_depth, hole_through, hole_x, hole_y,
|
||||
thread_radius, thread_pitch, thread_height, thread_depth, thread_internal, thread_x, thread_y,
|
||||
shell_thickness, shell_face,
|
||||
draft_face, draft_angle,
|
||||
revolve_angle, revolve_axis,
|
||||
sweep_path_ref, loft_profile_refs, loft_ruled,
|
||||
pattern_circular, pattern_count, pattern_spacing, pattern_dir, pattern_angle,
|
||||
plane_base, plane_offset, plane_angle_tilt, plane_axis,
|
||||
bool_tool_body, bool_keep_tool, bool_tolerance, bool_target_face, bool_tool_face,
|
||||
cut_offset, cut_flip, cut_keep_upper, cut_keep_lower,
|
||||
brep,
|
||||
plane_type, plane_face_body, plane_face, plane_face2_body, plane_face2,
|
||||
plane_edge_body, plane_edge, plane_edge2_body, plane_edge2, plane_u_size, plane_v_size,
|
||||
mirror_keep_original,
|
||||
axis_type, axis_p1, axis_p2, axis_body, axis_face, axis_edge, axis_plane_a, axis_plane_b,
|
||||
coordsys_type, coordsys_point, coordsys_body, coordsys_face, coordsys_edge, coordsys_x_hint,
|
||||
helix_radius, helix_pitch, helix_height, helix_left_handed, helix_taper_deg,
|
||||
xf_translate, xf_axis, xf_pivot, xf_angle_deg, xf_copy,
|
||||
thicken_face, thicken_thickness, thicken_flip,
|
||||
cut_face_body, cut_face,
|
||||
project_source_body, project_edges, project_face,
|
||||
delete_faces,
|
||||
hole_style, hole_cbore_diameter, hole_cbore_depth,
|
||||
hole_csink_diameter, hole_csink_angle, hole_standard,
|
||||
rib_sketch_ref, rib_entity, rib_thickness, rib_depth,
|
||||
pattern_curve_sketch, pattern_curve_entity,
|
||||
expr,
|
||||
mate_kind, mate_cs_a, mate_cs_b, mate_offset, mate_angle, mate_flip,
|
||||
coordsys_face_kind, coordsys_face_edges);
|
||||
imported_solid = brep_from_string(brep);
|
||||
}
|
||||
};
|
||||
|
||||
// Serialize a TopoDS_Shape to/from a BRep string for cereal persistence.
|
||||
std::string brep_to_string(const TopoDS_Shape& s);
|
||||
TopoDS_Shape brep_from_string(const std::string& d);
|
||||
|
||||
// One independent solid in a multi-body document.
|
||||
struct CadBody {
|
||||
TopoDS_Shape shape;
|
||||
std::string name;
|
||||
// The name the USER gave this body. A body is NOT its first feature: an Extrude, a Cut and
|
||||
// a Fillet all land on the same body, so renaming `source_feature` renames one operation in
|
||||
// the history, not the object — which is exactly the bug this field exists to end. `name`
|
||||
// above is the DERIVED label (the maker's name, restamped every recompute) and stays that;
|
||||
// this one is set only by a rename, carried across recompute() by body index, and written
|
||||
// into the recipe so it survives save/load.
|
||||
bool has_user_name{false};
|
||||
std::string user_name;
|
||||
// Per-body display colour override (Color tool). When has_color is false the GUI
|
||||
// falls back to the auto body-index palette. Carried across recompute() by body index.
|
||||
bool has_color{false};
|
||||
ColorRGBA color;
|
||||
// Index into `features` of the feature that CREATED this body, or -1. A body is a
|
||||
// recomputed result, so without this there is no way back to its maker and "delete this
|
||||
// body" cannot be expressed at all — the GUI could only answer "select the FEATURE that
|
||||
// created this body". Stamped in one place, the recompute loop; see the note there for
|
||||
// why a single "still unset?" test is sufficient and stays correct for new feature types.
|
||||
int source_feature{-1};
|
||||
};
|
||||
|
||||
// OCCT-only feature tree backing the Design tab. No GUI dependencies (lives in libslic3r).
|
||||
class CadDocument {
|
||||
public:
|
||||
std::vector<CadFeature> features;
|
||||
// Named document variables: name -> expression. Evaluated topologically each recompute();
|
||||
// an expression may reference other variables. Feature `expr` bindings resolve against these.
|
||||
std::map<std::string, std::string> variables;
|
||||
// Multi-body result of the last replay. A "New" extrude appends a body; other ops
|
||||
// mutate a target body. Empty after a failed/empty recompute.
|
||||
std::vector<CadBody> bodies;
|
||||
TopoDS_Shape body; // compound of all bodies (1 body => that body) — display/compat
|
||||
TriangleMesh display_mesh; // tessellation of all bodies, concatenated (picking)
|
||||
std::vector<TriangleMesh> display_body_meshes; // one mesh per body, in `bodies` order (per-body color)
|
||||
std::vector<int> display_tri_face; // per-triangle face id WITHIN its source body
|
||||
std::vector<int> display_tri_body; // per-triangle source body index (into bodies)
|
||||
std::string error; // last recompute error ("" = ok)
|
||||
|
||||
// Mate diagnostics, refilled by every recompute(). Non-fatal by design: the
|
||||
// document still evaluates — this only names what the user should look at.
|
||||
// .first = index of the offending Mate feature, .second = human-readable reason.
|
||||
// NOT "over-constraint" — this kernel has no solver, so there is no DOF analysis
|
||||
// behind these; they are graph facts about which mate drives which body.
|
||||
std::vector<std::pair<int, std::string>> mate_conflicts;
|
||||
|
||||
// Modeling origin: the world point the default XY/XZ/YZ planes pass through. The GUI sets this
|
||||
// to the bed centre so sketches/datums land in the middle of the bed (not the bed corner =
|
||||
// world 0). Not serialized — the GUI re-applies it from the live bed on every tab show.
|
||||
Vec3d modeling_origin{Vec3d::Zero()};
|
||||
|
||||
// Tessellation quality, matched to Orca's OWN STEP importer (Format/STEP.hpp defaults:
|
||||
// linear 0.003, angular 0.5 rad) so a body modelled here reaches the screen at the same
|
||||
// density as the identical body imported through Prepare. It was 0.01 linear — 3.3x coarser
|
||||
// than anything else in the app, which is why curved faces read as faceted next to an
|
||||
// imported part. Angular already matched. Same BRepMesh_IncrementalMesh call, same GLVolume
|
||||
// path, same shaders: the renderer was never the difference, the mesh fed to it was.
|
||||
double linear_deflection{0.003};
|
||||
double angular_deflection{0.5};
|
||||
|
||||
int add_sketch(SketchShape shape, const SketchPlane& plane,
|
||||
double width, double height, double radius,
|
||||
const std::string& name);
|
||||
int add_sketch_profile(const SketchProfile& profile, const SketchPlane& plane,
|
||||
const std::string& name);
|
||||
// Onshape-style multi-entity sketch: stores the entity list verbatim. When
|
||||
// non-empty it takes precedence over profile/enum in build_sketch_wire.
|
||||
int add_sketch_entities(const std::vector<SketchEntity>& entities,
|
||||
const SketchPlane& plane, const std::string& name,
|
||||
const std::vector<SketchEntityConstraintDef>& constraints = {});
|
||||
// Project edges of source_body onto plane, producing a sketch feature whose
|
||||
// entities are (re)derived on every recompute.
|
||||
int add_project_edges(int source_body, const std::vector<int>& edge_ids, int face,
|
||||
const SketchPlane& plane, const std::string& name);
|
||||
// Onshape's "Use" / SolidWorks' "Convert Entities": project a body's edges onto the plane of
|
||||
// an EXISTING sketch feature and append them to that sketch as CONSTRUCTION entities, so new
|
||||
// geometry can be constrained to them. Returns the number of entities appended, or -1 if the
|
||||
// sketch or body reference is invalid. Unlike add_project_edges this creates no feature: the
|
||||
// references become part of the sketch that borrows them.
|
||||
int project_edges_into_sketch(int sketch_feature, int source_body,
|
||||
const std::vector<int>& edge_ids, int face);
|
||||
// Append a bridging BSpline entity connecting endpoint `end_a` of entity `ent_a` to
|
||||
// endpoint `end_b` of entity `ent_b`, both within sketch feature `sketch_ref`. Returns
|
||||
// the new entity's index within that sketch's entities vector. Non-parametric: computed
|
||||
// once from the current endpoints (does not auto-follow later solver moves).
|
||||
int add_bridge(int sketch_ref, int ent_a, int end_a, int ent_b, int end_b,
|
||||
const std::string& name);
|
||||
// Solve features[index]'s sketch constraints, writing solved coordinates back
|
||||
// into its profile.points. No-op (returns true) if the feature has no
|
||||
// constraints. Returns false if index is invalid / not a Sketch / solve fails.
|
||||
bool solve_sketch_feature(int index);
|
||||
int add_extrude(int sketch_ref, double distance, bool symmetric,
|
||||
BooleanMode mode, const std::string& name);
|
||||
// Extrude a single loop given directly as entities (sketch_ref = -1, plane carried).
|
||||
int add_extrude_entities(const std::vector<SketchEntity>& entities,
|
||||
const SketchPlane& plane, double distance, bool symmetric,
|
||||
BooleanMode mode, const std::string& name);
|
||||
// Extrude an existing solid FACE (global face id on the body) as the profile.
|
||||
int add_extrude_face(int src_face, double distance, bool symmetric,
|
||||
BooleanMode mode, const std::string& name);
|
||||
int add_fillet(double radius, FaceGroup faces, const std::string& name);
|
||||
int add_fillet(double radius, int edge_id, const std::string& name);
|
||||
int add_chamfer(double distance, FaceGroup faces, const std::string& name);
|
||||
int add_chamfer(double distance, int edge_id, const std::string& name);
|
||||
int add_hole(double diameter, double depth, bool through,
|
||||
double x, double y, const SketchPlane& plane,
|
||||
const std::string& name);
|
||||
int add_hole_styled(double diameter, double depth, bool through,
|
||||
double x, double y, const SketchPlane& plane, int style,
|
||||
double cbore_diameter, double cbore_depth,
|
||||
double csink_diameter, double csink_angle,
|
||||
const std::string& standard, const std::string& name);
|
||||
int add_hole_standard(const std::string& designation, int style, bool through,
|
||||
double depth, double x, double y,
|
||||
const SketchPlane& plane, const std::string& name);
|
||||
int add_thread(double radius, double pitch, double height, double depth,
|
||||
bool internal, double x, double y, const SketchPlane& plane,
|
||||
const std::string& name);
|
||||
int add_revolve(int sketch_ref, double angle, int axis, bool flip,
|
||||
BooleanMode mode, const std::string& name);
|
||||
// Self-contained revolve of a single loop given directly as entities (sketch_ref=-1).
|
||||
int add_revolve_entities(const std::vector<SketchEntity>& entities,
|
||||
const SketchPlane& plane, double angle, int axis, bool flip,
|
||||
BooleanMode mode, const std::string& name);
|
||||
// Sweep the profile Sketch (profile_sketch_ref) along the path Sketch (path_sketch_ref).
|
||||
int add_pattern(bool circular, int count, double spacing, int dir,
|
||||
double angle_deg, int target_body, const std::string& name);
|
||||
// Pattern `count` copies of `target` along entity `curve_entity` of sketch `curve_sketch`.
|
||||
int add_pattern_on_curve(int count, int curve_sketch, int curve_entity, int target,
|
||||
const std::string& name);
|
||||
int add_sweep(int profile_sketch_ref, int path_sketch_ref, BooleanMode mode,
|
||||
const std::string& name);
|
||||
// Loft through the ordered profile Sketches (each a closed wire on its own plane).
|
||||
int add_loft(const std::vector<int>& profile_refs, bool ruled, BooleanMode mode,
|
||||
const std::string& name);
|
||||
// Skin 2+ profile sketches open (no end caps) -> a sheet body.
|
||||
int add_surface_loft(const std::vector<int>& profile_refs, bool ruled, const std::string& name);
|
||||
// Fill sketch sketch_ref's closed boundary wire with a smooth face -> a one-face sheet body.
|
||||
int add_surface_fill(int sketch_ref, const std::string& name);
|
||||
int add_shell(double thickness, int face, int target_body, const std::string& name);
|
||||
// Grow a thin rib wall (thickness, depth) from the open Line entity `entity` inside sketch
|
||||
// feature `sketch_ref`, fused to `target_body`. Returns the new feature index.
|
||||
int add_rib(int sketch_ref, int entity, double thickness, double depth,
|
||||
int target_body, const std::string& name);
|
||||
int add_draft(double angle, int face, int target_body, const std::string& name);
|
||||
// Boolean between two existing bodies. op reuses BooleanMode (Add=union, Cut=subtract,
|
||||
// Intersect=common; New invalid). target survives, tool is consumed unless keep_tool.
|
||||
// tolerance = OCCT fuzzy value; target_face/tool_face (-1 = none) drive the per-face snap+merge.
|
||||
int add_boolean(BooleanMode op, int target_body, int tool_body, bool keep_tool,
|
||||
double tolerance, int target_face, int tool_face, const std::string& name);
|
||||
// Plane Cut (Onshape split-by-plane): trim target_body by the plane (origin offset along
|
||||
// its normal by `offset`, normal flipped iff `flip`). keep_upper/keep_lower select the
|
||||
// +normal / -normal half; both => the body is split into two coexisting bodies.
|
||||
int add_cut(const SketchPlane& plane, double offset, bool flip,
|
||||
bool keep_upper, bool keep_lower, int target_body, const std::string& name);
|
||||
// Split target_body along the plane of face `face` (owned by face_body, -1 = target).
|
||||
// keep_upper/keep_lower select which half survives; both => split into two bodies.
|
||||
int add_split_by_face(int target_body, int face_body, int face,
|
||||
bool keep_upper, bool keep_lower, const std::string& name);
|
||||
int add_mirror(const SketchPlane& plane, int target_body, BooleanMode mode,
|
||||
const std::string& name);
|
||||
// Rigid body transform: rotate `angle_deg` about `axis` through `pivot`, then translate.
|
||||
// copy=true keeps the source body and appends the transformed one as a new body.
|
||||
int add_transform(int target_body, const Vec3d& translate, const Vec3d& axis,
|
||||
const Vec3d& pivot, double angle_deg, bool copy, const std::string& name);
|
||||
// Offset face `face` of `target_body` by `thickness` along its normal, producing a new
|
||||
// thin solid appended as a new body. flip=true offsets against the normal.
|
||||
int add_thicken(int target_body, int face, double thickness, bool flip, const std::string& name);
|
||||
// Thicken an entire SHEET body's shell into a solid.
|
||||
int add_thicken_surface(int target_body, double thickness, bool flip, const std::string& name);
|
||||
// Offset a SHEET body's shell by a signed distance, producing another SHEET body.
|
||||
int add_surface_offset(int target_body, double offset, const std::string& name);
|
||||
int add_delete_face(int target_body, const std::vector<int>& faces,
|
||||
const std::string& name);
|
||||
int add_surface_extrude(int sketch_ref, double distance, const std::string& name);
|
||||
int add_surface_revolve(int sketch_ref, double angle_deg, int axis, const std::string& name);
|
||||
// Datum plane: derived from base (0=XY/1=XZ/2=YZ/3+N=Nth earlier datum), offset
|
||||
// along its normal, optional tilt about a base axis. Produces no solid.
|
||||
int add_plane(int base, double offset, double angle_tilt, int axis,
|
||||
const std::string& name);
|
||||
// Datum axis: construction method axis_type determines which ref fields are read.
|
||||
int add_axis(AxisType axis_type, const std::string& name);
|
||||
// Datum coordinate system.
|
||||
int add_coordsys(CoordSysType type, const Vec3d& point, const std::string& name);
|
||||
int add_mate(int kind, int cs_a, int cs_b, double offset, double angle_deg, bool flip,
|
||||
const std::string& name);
|
||||
|
||||
// Which mate types apply to a connector pair, as reported to the viewport palette.
|
||||
struct MateOption {
|
||||
int kind{0}; // 0..4, the five mate types in CadDocument.hpp:308-314
|
||||
bool viable{true};
|
||||
std::string reason; // empty when viable; why not, when not
|
||||
};
|
||||
// ALWAYS all five entries, ALWAYS in kind order. Never filtered: the caller dims what is
|
||||
// not viable rather than hiding it, so the list must be stable in length and order between
|
||||
// calls. Pure query over existing data — records nothing, mutates nothing.
|
||||
std::vector<MateOption> mate_options(int cs_a, int cs_b) const;
|
||||
|
||||
int add_helix(const SketchPlane& plane, double radius, double pitch, double height,
|
||||
bool left_handed, double taper_deg, const std::string& name);
|
||||
// Build the helix wire from a Helix feature's params (exposed for tests).
|
||||
TopoDS_Wire build_helix_wire(const CadFeature& f, std::string& err) const;
|
||||
// Every datum plane currently in the recipe, in feature order, as (name, plane).
|
||||
// Used by the GUI to populate plane pickers (after the 3 base planes).
|
||||
std::vector<std::pair<std::string, SketchPlane>> resolve_datum_planes() const;
|
||||
|
||||
// World-space sketch plane lying on a body's PLANAR face, so a face picked in the viewport can
|
||||
// be sketched on directly — no datum plane in between and nothing to choose from a list.
|
||||
// Returns false when the indices don't resolve or the face isn't planar (a cylinder or a fillet
|
||||
// has no single plane, and guessing one from a mid-parameter normal would silently sketch on a
|
||||
// tangent). Same derivation the Coincident datum method uses, shared so the two cannot drift.
|
||||
bool plane_of_face(int body_idx, int face_idx, SketchPlane& out) const;
|
||||
// Resolved datum axes in feature order. axis_err is non-empty if construction failed.
|
||||
struct DatumAxis { std::string name; Vec3d origin{0,0,0}; Vec3d direction{0,0,1};
|
||||
std::string error; };
|
||||
std::vector<DatumAxis> resolve_datum_axes() const;
|
||||
// Resolved datum coordinate systems. X/Y unit, orthonormal (Z = X.cross(Y)).
|
||||
struct DatumCoordSys { std::string name; Vec3d origin{0,0,0}; Vec3d x{1,0,0};
|
||||
Vec3d y{0,1,0}; std::string error; };
|
||||
std::vector<DatumCoordSys> resolve_datum_coordsys() const;
|
||||
void clear();
|
||||
bool recompute(); // replay features -> body + display_mesh; false on error
|
||||
|
||||
// CadRecipe serialization contract:
|
||||
// - v1 blobs are deliberately not loadable; there is no migration path by design
|
||||
// - append fields ONLY at the end of save/load, never reorder (golden fixture enforces this)
|
||||
// Bumped every time the bodies are rebuilt, i.e. every time the face and edge MAPS change.
|
||||
// Global face/edge ids are indices into TopExp::MapShapes and mean nothing across a rebuild,
|
||||
// so any caller holding an id from an earlier state is holding a wrong one. This is the
|
||||
// handle that lets it find out instead of silently addressing the wrong edge.
|
||||
//
|
||||
// Session-scoped and deliberately NOT serialized: an id is only meaningful within the run
|
||||
// that produced it, so persisting the counter would imply a promise across loads that the
|
||||
// ids themselves cannot keep.
|
||||
uint64_t topo_generation{1};
|
||||
|
||||
// v5: every feature is length-framed, so a reader can stop early on an older file and skip
|
||||
// the tail of a newer one. This is the LAST version that has to break anything — from here a
|
||||
// new field only needs appending to save/load, with no bump and no orphaned projects.
|
||||
// v6: no wire-format change — the bytes are v5's, and both are read by the same framed path.
|
||||
// The stamp advances only to put a project-container change on the record; the 3MF backends
|
||||
// own that story. v4 still opens through the pre-framing flat path.
|
||||
static constexpr uint32_t ORCA_CAD_RECIPE_VERSION = 6;
|
||||
std::string serialize_recipe() const;
|
||||
bool deserialize_recipe(const std::string& blob);
|
||||
|
||||
// Export every body to a STEP file as native B-rep (not mesh). body_xforms is the
|
||||
// per-body display transform (Move gizmo); when supplied the bodies are written at
|
||||
// those positions so the STEP matches what Commit ships. false + err on failure.
|
||||
bool export_step(const std::string& path,
|
||||
const std::vector<Transform3d>& body_xforms,
|
||||
std::string& err) const;
|
||||
|
||||
GeometryEngine::MassProps body_mass_properties(int body_index) const;
|
||||
|
||||
// One overlapping pair of solid bodies. Indices are into `bodies`, a_ < b_.
|
||||
struct Interference { int body_a{-1}; int body_b{-1}; double volume{0}; };
|
||||
// Every pair of solid bodies whose intersection encloses more than min_volume (mm^3).
|
||||
// Reports only — mutates nothing, so mates and placements are unaffected by calling it.
|
||||
// Sheet bodies are skipped: an intersection involving one encloses no volume.
|
||||
std::vector<Interference> check_interference(double min_volume = 1e-6) const;
|
||||
|
||||
// ponytail: derived from the OCCT shape type; no stored flag, bodies aren't serialized anyway.
|
||||
static bool is_sheet_shape(const TopoDS_Shape& s); // true if TopExp finds no TopAbs_SOLID
|
||||
|
||||
// Undo/redo of the feature recipe (Onshape-style Ctrl+Z). The caller marks a
|
||||
// user-action boundary by calling checkpoint() BEFORE the mutation(s) for that
|
||||
// action (add/delete/move/replace, or a direct features edit). undo()/redo() then
|
||||
// restore the snapshot and recompute(). Because everything else (bodies/meshes/
|
||||
// body) is derived by recompute(), snapshotting `features` alone is a complete,
|
||||
// exact history; one checkpoint == one Ctrl+Z step.
|
||||
void checkpoint(); // snapshot `features` for undo + invalidate redo
|
||||
// Drop the most recent checkpoint. For a mutation that took a checkpoint, then failed
|
||||
// and restored the pre-mutation state itself (the constraint paths reject an
|
||||
// over-constrained addition this way): the snapshot now describes a state identical to
|
||||
// the current one, and leaving it turns the next Ctrl+Z into a press that does nothing.
|
||||
void abandon_checkpoint();
|
||||
bool can_undo() const { return !m_undo.empty(); }
|
||||
bool can_redo() const { return !m_redo.empty(); }
|
||||
size_t undo_depth() const { return m_undo.size(); }
|
||||
size_t redo_depth() const { return m_redo.size(); }
|
||||
bool undo(); // restore the previous feature list + recompute(); false if no history
|
||||
bool redo(); // re-apply the most recently undone change; false if none
|
||||
|
||||
// Feature-tree editing (Onshape-style). All are transactional: they snapshot
|
||||
// features, mutate, recompute(), and roll back to the snapshot (re-recomputing)
|
||||
// if the result is invalid — so a failed edit never leaves a broken body.
|
||||
//
|
||||
// remove_feature: erase features[index]; deleting a Sketch cascades to the
|
||||
// Extrude(s) that consume it; surviving sketch_ref indices are remapped.
|
||||
// move_feature: shift features[index] by delta (-1 up / +1 down), clamped;
|
||||
// sketch_ref indices of the two swapped slots are remapped.
|
||||
// replace_feature: overwrite features[index] with `edited` (its name and, for
|
||||
// an Extrude, its sketch_ref are preserved from the original).
|
||||
bool remove_feature(int index);
|
||||
bool move_feature(int index, int delta);
|
||||
bool replace_feature(int index, const CadFeature& edited);
|
||||
// replace_sketch_extrude: a box is two linked features (Sketch + Extrude);
|
||||
// overwrite both slots from one `edited` candidate (sketch params ->
|
||||
// features[sketch_idx], extrude params -> features[extrude_idx]), keeping
|
||||
// each slot's name/type and the sketch_ref link. Transactional like above.
|
||||
bool replace_sketch_extrude(int sketch_idx, int extrude_idx, const CadFeature& edited);
|
||||
|
||||
// Apply ONE candidate feature on top of the current committed body and
|
||||
// tessellate the result into out_mesh, WITHOUT modifying features/body/
|
||||
// display_mesh. Returns false (with err set) if the candidate is invalid.
|
||||
// Used by the Design tab to show a translucent ghost before Confirm.
|
||||
bool preview(const CadFeature& candidate, TriangleMesh& out_mesh, std::string& err) const;
|
||||
// Same, but also returns the per-body meshes (in `bodies` order; the candidate may append
|
||||
// one), so the GUI can apply its display-only per-body Move transforms to the ghost and keep
|
||||
// it overlaid on the moved body instead of floating back at the untransformed origin.
|
||||
bool preview(const CadFeature& candidate, TriangleMesh& out_mesh,
|
||||
std::vector<TriangleMesh>& out_body_meshes, std::string& err) const;
|
||||
|
||||
private:
|
||||
TopoDS_Wire build_sketch_wire(const CadFeature& sketch, bool closed_only = false) const;
|
||||
// The planar region an Extrude sweeps: the sketch's outer loop with its inner loops as
|
||||
// holes. Falls back to a face over build_sketch_wire() for the legacy profile/shape paths,
|
||||
// which have no concept of a second loop.
|
||||
TopoDS_Face build_sketch_face(const CadFeature& sketch) const;
|
||||
// Apply a single feature to (result, have_body), throwing std::runtime_error on
|
||||
// failure. `context` is the body whose faces/edges the feature reads (face-extrude
|
||||
// source, up-to-face target, dress-up, hole) — it differs from `result` only when the
|
||||
// feature builds a NEW body from an existing one (face-extrude New). Shared by route.
|
||||
void apply_feature(TopoDS_Shape& result, bool& have_body,
|
||||
const TopoDS_Shape& context, const CadFeature& f) const;
|
||||
// Route one feature into the bodies list: resolve its target body, decide whether it
|
||||
// starts a new body (empty list, or an Extrude with mode New) vs mutates an existing
|
||||
// one, then apply_feature. Shared by recompute() (replay all) and preview() (candidate).
|
||||
void route_feature(std::vector<CadBody>& bodies, const CadFeature& f) const;
|
||||
// Boolean between two existing bodies: resolve target + tool, optionally snap the tool so
|
||||
// the picked faces mate, run the OCCT op (with fuzzy tolerance), write the result back to the
|
||||
// target and erase the consumed tool. Mutates the bodies vector directly (unlike apply_feature,
|
||||
// which works on a single result shape). Throws std::runtime_error on a failed op.
|
||||
void apply_boolean(std::vector<CadBody>& bodies, const CadFeature& f) const;
|
||||
void apply_cut(std::vector<CadBody>& bodies, const CadFeature& f) const;
|
||||
void apply_mirror(std::vector<CadBody>& bodies, const CadFeature& f) const;
|
||||
void apply_transform(std::vector<CadBody>& bodies, const CadFeature& f) const;
|
||||
void apply_thicken(std::vector<CadBody>& bodies, const CadFeature& f) const;
|
||||
void apply_thicken_surface(std::vector<CadBody>& bodies, const CadFeature& f) const;
|
||||
void apply_surface_offset(std::vector<CadBody>& bodies, const CadFeature& f) const;
|
||||
void apply_project(const std::vector<CadBody>& bodies, CadFeature& f) const;
|
||||
static DatumCoordSys datum_frame(const std::vector<CadBody>& bodies, const CadFeature& f);
|
||||
void apply_mate(std::vector<CadBody>& bodies, const CadFeature& f) const;
|
||||
void detect_mate_conflicts(); // refills mate_conflicts from the feature list alone
|
||||
|
||||
// Undo/redo stacks of recipe snapshots. checkpoint() pushes onto m_undo and clears
|
||||
// m_redo; undo()/redo() shuffle the current state between them. Capped so a long
|
||||
// session can't grow unbounded.
|
||||
//
|
||||
// The snapshot MUST carry `variables` as well as `features`: a caller that sets a bad
|
||||
// variable, sees recompute() fail and calls undo() to roll it back would otherwise be
|
||||
// left with the bad variable still in the document, so every later recompute fails —
|
||||
// the exact corruption the checkpoint/undo pattern exists to prevent. Not serialized,
|
||||
// so this changes no on-disk format.
|
||||
struct Snapshot {
|
||||
std::vector<CadFeature> features;
|
||||
std::map<std::string, std::string> variables;
|
||||
};
|
||||
std::vector<Snapshot> m_undo;
|
||||
std::vector<Snapshot> m_redo;
|
||||
static constexpr size_t k_undo_cap = 200;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_CadDocument_hpp_
|
||||
@@ -0,0 +1,711 @@
|
||||
#include "libslic3r/CAD/GeometryEngine.hpp"
|
||||
|
||||
#include <BRepMesh_IncrementalMesh.hxx>
|
||||
#include <BRep_Tool.hxx>
|
||||
#include <BRepAdaptor_Surface.hxx>
|
||||
#include <BRepLProp_SLProps.hxx>
|
||||
#include <gp_Cylinder.hxx>
|
||||
#include <BRepFilletAPI_MakeFillet.hxx>
|
||||
#include <BRepFilletAPI_MakeChamfer.hxx>
|
||||
#include <stdexcept>
|
||||
#include <TopExp_Explorer.hxx>
|
||||
#include <TopoDS.hxx>
|
||||
#include <TopoDS_Face.hxx>
|
||||
#include <TopoDS_Edge.hxx>
|
||||
#include <TopExp.hxx>
|
||||
#include <TopTools.hxx>
|
||||
#include <TopTools_IndexedMapOfShape.hxx>
|
||||
#include <Poly_Triangulation.hxx>
|
||||
#include <gp_Ax2.hxx>
|
||||
#include <gp_Dir.hxx>
|
||||
#include <gp_Pnt.hxx>
|
||||
#include <BRepGProp.hxx>
|
||||
#include <GProp_GProps.hxx>
|
||||
#include <GeomLProp_SLProps.hxx>
|
||||
#include <BRepAdaptor_Curve.hxx>
|
||||
#include <gp_Circ.hxx>
|
||||
#include <GCPnts_TangentialDeflection.hxx>
|
||||
#include <STEPControl_Reader.hxx>
|
||||
#include <IFSelect_ReturnStatus.hxx>
|
||||
#include <Standard_Failure.hxx>
|
||||
#include <BRepExtrema_DistShapeShape.hxx>
|
||||
#include <BRepBuilderAPI_MakeVertex.hxx>
|
||||
#include <BRepBuilderAPI_MakeEdge.hxx>
|
||||
#include <BRepBuilderAPI_MakeWire.hxx>
|
||||
#include <BRepBuilderAPI_MakeFace.hxx>
|
||||
#include <BRepBuilderAPI_MakeSolid.hxx>
|
||||
#include <BRep_Builder.hxx>
|
||||
#include <TopoDS_Shell.hxx>
|
||||
#include <TopoDS_Vertex.hxx>
|
||||
#include <ShapeUpgrade_UnifySameDomain.hxx>
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <cmath>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// ---- STEP import (B-rep, not mesh) ----
|
||||
std::vector<TopoDS_Shape> GeometryEngine::read_step_solids(const std::string& path, std::string& err)
|
||||
{
|
||||
err.clear();
|
||||
std::vector<TopoDS_Shape> out;
|
||||
try {
|
||||
STEPControl_Reader reader;
|
||||
if (reader.ReadFile(path.c_str()) != IFSelect_RetDone) {
|
||||
err = "cannot read STEP file";
|
||||
return out;
|
||||
}
|
||||
reader.TransferRoots();
|
||||
const TopoDS_Shape shape = reader.OneShape();
|
||||
if (shape.IsNull()) { err = "STEP file has no geometry"; return out; }
|
||||
// One body per top-level solid; fall back to the whole shape (shells/faces) if none.
|
||||
for (TopExp_Explorer ex(shape, TopAbs_SOLID); ex.More(); ex.Next())
|
||||
out.push_back(ex.Current());
|
||||
if (out.empty())
|
||||
out.push_back(shape);
|
||||
} catch (const Standard_Failure& e) {
|
||||
err = e.GetMessageString() ? e.GetMessageString() : "OCCT failed to read STEP";
|
||||
out.clear();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- Mesh -> B-rep (faceted, shared topology by construction) ----
|
||||
//
|
||||
// Port of mesh2step's brep_build.py. Two properties are load-bearing and easy to lose:
|
||||
//
|
||||
// 1. The edge cache is keyed on the UNORDERED vertex-index pair, and a triangle that walks
|
||||
// the edge backwards (i > j) gets edge.Reversed(). Consistently-wound meshes (STL/OBJ/3MF
|
||||
// all are) walk every shared edge in opposite directions from its two adjacent triangles,
|
||||
// so this reversal is exactly what leaves the faces coherently outward-oriented.
|
||||
// 2. Degeneracy is split in two, deliberately. A triangle is dropped as sub-resolution noise
|
||||
// only if its longest edge is below `tolerance` (an absolute floor), while sliver rejection
|
||||
// is scale-INDEPENDENT (area < 1e-9 * longest_edge^2). Folding the two together under one
|
||||
// `area < tolerance^2` test rejects legitimate thin CAD slivers whenever tolerance is coarse
|
||||
// relative to them, turning a watertight input into a falsely-open shell — a real regression
|
||||
// mesh2step hit on a 62k-triangle mechanical part.
|
||||
TopoDS_Shape GeometryEngine::mesh_to_brep(const indexed_triangle_set& its,
|
||||
double tolerance,
|
||||
double merge_angle_deg,
|
||||
MeshBrepStats& stats)
|
||||
{
|
||||
stats = MeshBrepStats{};
|
||||
stats.input_tris = int(its.indices.size());
|
||||
if (tolerance <= 0.0)
|
||||
throw std::runtime_error("mesh_to_brep: tolerance must be > 0");
|
||||
if (its.indices.empty())
|
||||
throw std::runtime_error("mesh_to_brep: mesh has no triangles");
|
||||
|
||||
// 1. Tolerance-quantized vertex dedup. A merged vertex keeps the exact coordinates of the
|
||||
// first input occurrence — vertices are grouped by a cell, never snapped onto its grid.
|
||||
std::map<std::array<long long, 3>, int> cell_to_new;
|
||||
std::vector<int> old_to_new(its.vertices.size(), -1);
|
||||
std::vector<Vec3d> verts;
|
||||
verts.reserve(its.vertices.size());
|
||||
for (size_t i = 0; i < its.vertices.size(); ++i) {
|
||||
const Vec3d p = its.vertices[i].cast<double>();
|
||||
const std::array<long long, 3> cell{ (long long) std::llround(p.x() / tolerance),
|
||||
(long long) std::llround(p.y() / tolerance),
|
||||
(long long) std::llround(p.z() / tolerance) };
|
||||
auto ins = cell_to_new.emplace(cell, int(verts.size()));
|
||||
if (ins.second)
|
||||
verts.push_back(p);
|
||||
old_to_new[i] = ins.first->second;
|
||||
}
|
||||
|
||||
// 2. Reject degenerate triangles (see the two-part rule in the comment above).
|
||||
std::vector<Vec3i32> tris;
|
||||
tris.reserve(its.indices.size());
|
||||
for (const Vec3i32& t : its.indices) {
|
||||
const int a = old_to_new[t(0)], b = old_to_new[t(1)], c = old_to_new[t(2)];
|
||||
if (a == b || b == c || a == c) { ++stats.degenerate_collapsed; continue; }
|
||||
const Vec3d& pa = verts[a]; const Vec3d& pb = verts[b]; const Vec3d& pc = verts[c];
|
||||
const double e0 = (pb - pa).norm(), e1 = (pc - pb).norm(), e2 = (pa - pc).norm();
|
||||
const double longest = std::max(e0, std::max(e1, e2));
|
||||
if (longest < tolerance) { ++stats.degenerate_collapsed; continue; }
|
||||
const double area = 0.5 * (pb - pa).cross(pc - pa).norm();
|
||||
if (area < 1e-9 * longest * longest) { ++stats.degenerate_sliver; continue; }
|
||||
tris.emplace_back(a, b, c);
|
||||
}
|
||||
stats.kept_tris = int(tris.size());
|
||||
if (tris.empty())
|
||||
throw std::runtime_error("mesh_to_brep: every triangle was rejected as degenerate "
|
||||
"(try a smaller tolerance)");
|
||||
|
||||
// 3. One face per triangle, sharing vertices and edges through the caches.
|
||||
std::vector<TopoDS_Vertex> vertex_cache(verts.size());
|
||||
std::vector<bool> vertex_made(verts.size(), false);
|
||||
auto get_vertex = [&](int i) -> const TopoDS_Vertex& {
|
||||
if (!vertex_made[i]) {
|
||||
const Vec3d& p = verts[i];
|
||||
vertex_cache[i] = BRepBuilderAPI_MakeVertex(gp_Pnt(p.x(), p.y(), p.z())).Vertex();
|
||||
vertex_made[i] = true;
|
||||
}
|
||||
return vertex_cache[i];
|
||||
};
|
||||
|
||||
std::map<std::pair<int, int>, TopoDS_Edge> edge_cache;
|
||||
std::map<std::pair<int, int>, int> edge_usage;
|
||||
auto get_edge = [&](int i, int j) -> TopoDS_Edge {
|
||||
const std::pair<int, int> key = (i < j) ? std::make_pair(i, j) : std::make_pair(j, i);
|
||||
++edge_usage[key];
|
||||
auto it = edge_cache.find(key);
|
||||
if (it == edge_cache.end())
|
||||
it = edge_cache.emplace(key,
|
||||
BRepBuilderAPI_MakeEdge(get_vertex(key.first), get_vertex(key.second)).Edge()).first;
|
||||
return (i > j) ? TopoDS::Edge(it->second.Reversed()) : it->second;
|
||||
};
|
||||
|
||||
BRep_Builder builder;
|
||||
TopoDS_Shell shell;
|
||||
builder.MakeShell(shell);
|
||||
|
||||
for (const Vec3i32& t : tris) {
|
||||
try {
|
||||
BRepBuilderAPI_MakeWire mk_wire(get_edge(t(0), t(1)), get_edge(t(1), t(2)), get_edge(t(2), t(0)));
|
||||
if (!mk_wire.IsDone()) { ++stats.faces_failed; continue; }
|
||||
BRepBuilderAPI_MakeFace mk_face(mk_wire.Wire());
|
||||
if (!mk_face.IsDone()) { ++stats.faces_failed; continue; }
|
||||
builder.Add(shell, mk_face.Face());
|
||||
++stats.faces_built;
|
||||
} catch (const Standard_Failure&) {
|
||||
++stats.faces_failed;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Watertightness falls straight out of the usage counts the cache already gathered.
|
||||
for (const auto& kv : edge_usage) {
|
||||
if (kv.second == 1) ++stats.boundary_edges;
|
||||
else if (kv.second >= 3) ++stats.nonmanifold_edges;
|
||||
}
|
||||
stats.unique_edges = int(edge_usage.size());
|
||||
stats.watertight = stats.boundary_edges == 0 && stats.nonmanifold_edges == 0 && stats.unique_edges > 0;
|
||||
|
||||
TopoDS_Shape shape = shell;
|
||||
if (stats.watertight && stats.faces_built > 0) {
|
||||
BRepBuilderAPI_MakeSolid mk_solid(shell);
|
||||
if (mk_solid.IsDone()) {
|
||||
TopoDS_Solid solid = mk_solid.Solid();
|
||||
GProp_GProps props;
|
||||
BRepGProp::VolumeProperties(solid, props);
|
||||
double vol = props.Mass();
|
||||
if (vol < 0.0) { // inward-wound input
|
||||
solid = TopoDS::Solid(solid.Reversed());
|
||||
vol = -vol;
|
||||
}
|
||||
if (vol > 0.0) {
|
||||
shape = solid;
|
||||
stats.is_solid = true;
|
||||
stats.volume = vol;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Optional coplanar merge. Faceted output is one planar face per triangle — exact, but
|
||||
// you cannot meaningfully fillet or extrude a face that IS a single triangle. Merging
|
||||
// coplanar neighbours is what turns the import into something the face/edge tools can
|
||||
// actually operate on (a 12-triangle cube collapses to its 6 real faces).
|
||||
if (merge_angle_deg > 0.0) {
|
||||
try {
|
||||
ShapeUpgrade_UnifySameDomain unifier(shape, true, true, true);
|
||||
unifier.SetAngularTolerance(merge_angle_deg * M_PI / 180.0);
|
||||
unifier.SetLinearTolerance(tolerance);
|
||||
unifier.Build();
|
||||
const TopoDS_Shape merged = unifier.Shape();
|
||||
if (!merged.IsNull())
|
||||
shape = merged;
|
||||
} catch (const Standard_Failure&) {
|
||||
// Merging is an optimisation, not a correctness step: keep the exact faceted shape.
|
||||
}
|
||||
}
|
||||
stats.faces_final = face_count(shape);
|
||||
return shape;
|
||||
}
|
||||
|
||||
// ---- Primitive creation ----
|
||||
|
||||
TopoDS_Solid GeometryEngine::make_primitive(const PrimitiveParams& params)
|
||||
{
|
||||
switch (params.type) {
|
||||
case PrimitiveType::Box:
|
||||
return BRepPrimAPI_MakeBox(gp_Pnt(-params.box_w/2, -params.box_d/2, 0),
|
||||
params.box_w, params.box_d, params.box_h).Solid();
|
||||
case PrimitiveType::Cylinder:
|
||||
return BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(0,0,0), gp_Dir(0,0,1)),
|
||||
params.cyl_radius, params.cyl_height).Solid();
|
||||
case PrimitiveType::Sphere:
|
||||
return BRepPrimAPI_MakeSphere(gp_Pnt(0,0,params.sph_radius), params.sph_radius).Solid();
|
||||
case PrimitiveType::Cone:
|
||||
return BRepPrimAPI_MakeCone(gp_Ax2(gp_Pnt(0,0,0), gp_Dir(0,0,1)),
|
||||
params.cone_r1, params.cone_r2, params.cone_height).Solid();
|
||||
case PrimitiveType::Torus:
|
||||
return BRepPrimAPI_MakeTorus(gp_Ax2(gp_Pnt(0,0,params.torus_r2), gp_Dir(0,0,1)),
|
||||
params.torus_r1, params.torus_r2).Solid();
|
||||
default:
|
||||
return BRepPrimAPI_MakeBox(gp_Pnt(-10,-10,0), 20,20,20).Solid();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Face classification ----
|
||||
|
||||
FaceGroup GeometryEngine::classify_face(const TopoDS_Face& face, const TopoDS_Shape& /*solid*/)
|
||||
{
|
||||
try {
|
||||
BRepAdaptor_Surface surf(face);
|
||||
if (surf.GetType() == GeomAbs_Plane) {
|
||||
// Sample normal at center UV
|
||||
double u = (surf.FirstUParameter() + surf.LastUParameter()) / 2.0;
|
||||
double v = (surf.FirstVParameter() + surf.LastVParameter()) / 2.0;
|
||||
gp_Pnt pt; gp_Vec du, dv;
|
||||
surf.D1(u, v, pt, du, dv);
|
||||
gp_Dir n = du.Crossed(dv);
|
||||
if (face.Orientation() == TopAbs_REVERSED) n.Reverse();
|
||||
|
||||
if (n.Z() > 0.7) return FaceGroup::Top;
|
||||
if (n.Z() < -0.7) return FaceGroup::Bottom;
|
||||
return FaceGroup::Lateral;
|
||||
}
|
||||
} catch (...) {}
|
||||
return FaceGroup::Lateral;
|
||||
}
|
||||
|
||||
// ---- Edge collection ----
|
||||
|
||||
std::vector<TopoDS_Edge> GeometryEngine::collect_edges(const TopoDS_Shape& solid, FaceGroup target)
|
||||
{
|
||||
std::vector<TopoDS_Edge> result;
|
||||
if (target == FaceGroup::All) {
|
||||
for (TopExp_Explorer exp(solid, TopAbs_EDGE); exp.More(); exp.Next())
|
||||
result.push_back(TopoDS::Edge(exp.Current()));
|
||||
return result;
|
||||
}
|
||||
|
||||
// Build edge-to-face map once
|
||||
TopTools_IndexedDataMapOfShapeListOfShape edgeFaceMap;
|
||||
TopExp::MapShapesAndAncestors(solid, TopAbs_EDGE, TopAbs_FACE, edgeFaceMap);
|
||||
|
||||
for (TopExp_Explorer edgeExp(solid, TopAbs_EDGE); edgeExp.More(); edgeExp.Next()) {
|
||||
const TopoDS_Edge& edge = TopoDS::Edge(edgeExp.Current());
|
||||
if (!edgeFaceMap.Contains(edge)) continue;
|
||||
const TopTools_ListOfShape& faces = edgeFaceMap.FindFromKey(edge);
|
||||
|
||||
bool include = false;
|
||||
for (auto it = faces.begin(); it != faces.end(); ++it) {
|
||||
FaceGroup fg = classify_face(TopoDS::Face(*it), solid);
|
||||
if (target == FaceGroup::Top && fg == FaceGroup::Top) { include = true; break; }
|
||||
if (target == FaceGroup::Bottom && fg == FaceGroup::Bottom) { include = true; break; }
|
||||
if (target == FaceGroup::Lateral && fg == FaceGroup::Lateral) { include = true; break; }
|
||||
}
|
||||
|
||||
if (!include && target == FaceGroup::Top) {
|
||||
for (auto it = faces.begin(); it != faces.end(); ++it) {
|
||||
if (classify_face(TopoDS::Face(*it), solid) == FaceGroup::Top) { include = true; break; }
|
||||
}
|
||||
}
|
||||
if (!include && target == FaceGroup::Bottom) {
|
||||
for (auto it = faces.begin(); it != faces.end(); ++it) {
|
||||
if (classify_face(TopoDS::Face(*it), solid) == FaceGroup::Bottom) { include = true; break; }
|
||||
}
|
||||
}
|
||||
if (target == FaceGroup::Lateral && !include) {
|
||||
int lateralCount = 0;
|
||||
for (auto it = faces.begin(); it != faces.end(); ++it) {
|
||||
if (classify_face(TopoDS::Face(*it), solid) == FaceGroup::Lateral) ++lateralCount;
|
||||
}
|
||||
if (lateralCount >= 2) include = true;
|
||||
}
|
||||
|
||||
if (include) result.push_back(edge);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ---- Fillet/Chamfer ----
|
||||
|
||||
TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radius, FaceGroup faces)
|
||||
{
|
||||
if (radius <= 0.001) return solid;
|
||||
|
||||
std::vector<TopoDS_Edge> edges = collect_edges(solid, faces);
|
||||
if (edges.empty()) return solid;
|
||||
|
||||
BRepFilletAPI_MakeFillet fillet(solid);
|
||||
for (const auto& edge : edges)
|
||||
fillet.Add(radius, edge);
|
||||
fillet.Build();
|
||||
|
||||
// A too-large radius (e.g. >= half the smallest spanned dimension) makes the
|
||||
// operation degenerate; OCCT leaves IsDone() false. Report it instead of
|
||||
// silently returning the unfilleted solid (which reads as a false success).
|
||||
if (!fillet.IsDone()) throw std::runtime_error("fillet radius too large for this geometry");
|
||||
return fillet.Shape();
|
||||
}
|
||||
|
||||
TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double distance, FaceGroup faces)
|
||||
{
|
||||
if (distance <= 0.001) return solid;
|
||||
|
||||
std::vector<TopoDS_Edge> edges = collect_edges(solid, faces);
|
||||
if (edges.empty()) return solid;
|
||||
|
||||
BRepFilletAPI_MakeChamfer chamfer(solid);
|
||||
for (const auto& edge : edges)
|
||||
chamfer.Add(distance, edge); // symmetric chamfer
|
||||
chamfer.Build();
|
||||
|
||||
if (!chamfer.IsDone()) throw std::runtime_error("chamfer distance too large for this geometry");
|
||||
return chamfer.Shape();
|
||||
}
|
||||
|
||||
TopoDS_Shape GeometryEngine::apply_fillet(const TopoDS_Shape& solid, double radius, int edge_id)
|
||||
{
|
||||
if (radius <= 0.001) return solid;
|
||||
|
||||
TopoDS_Edge edge = edge_by_index(solid, edge_id);
|
||||
if (edge.IsNull()) throw std::runtime_error("apply_fillet: invalid edge id");
|
||||
|
||||
BRepFilletAPI_MakeFillet mk(solid);
|
||||
mk.Add(radius, edge);
|
||||
mk.Build();
|
||||
|
||||
if (!mk.IsDone()) throw std::runtime_error("apply_fillet: OCCT fillet failed");
|
||||
return mk.Shape();
|
||||
}
|
||||
|
||||
TopoDS_Shape GeometryEngine::apply_chamfer(const TopoDS_Shape& solid, double distance, int edge_id)
|
||||
{
|
||||
if (distance <= 0.001) return solid;
|
||||
|
||||
TopoDS_Edge edge = edge_by_index(solid, edge_id);
|
||||
if (edge.IsNull()) throw std::runtime_error("apply_chamfer: invalid edge id");
|
||||
|
||||
BRepFilletAPI_MakeChamfer mk(solid);
|
||||
mk.Add(distance, edge);
|
||||
mk.Build();
|
||||
|
||||
if (!mk.IsDone()) throw std::runtime_error("apply_chamfer: OCCT chamfer failed");
|
||||
return mk.Shape();
|
||||
}
|
||||
|
||||
// ---- Tessellation ----
|
||||
|
||||
TriangleMesh GeometryEngine::tessellate(const TopoDS_Shape& shape,
|
||||
double linear_deflection,
|
||||
double angular_deflection)
|
||||
{
|
||||
BRepMesh_IncrementalMesh mesh(shape, linear_deflection, false, angular_deflection, true);
|
||||
|
||||
int nbNodes = 0, nbTri = 0;
|
||||
for (TopExp_Explorer exp(shape, TopAbs_FACE); exp.More(); exp.Next()) {
|
||||
TopLoc_Location loc;
|
||||
Handle(Poly_Triangulation) tri = BRep_Tool::Triangulation(TopoDS::Face(exp.Current()), loc);
|
||||
if (!tri.IsNull()) { nbNodes += tri->NbNodes(); nbTri += tri->NbTriangles(); }
|
||||
}
|
||||
if (nbTri == 0 || nbNodes == 0) return TriangleMesh{};
|
||||
|
||||
stl_file stl;
|
||||
stl.stats.type = inmemory;
|
||||
stl.stats.number_of_facets = (uint32_t)nbTri;
|
||||
stl.stats.original_num_facets = stl.stats.number_of_facets;
|
||||
stl_allocate(&stl);
|
||||
|
||||
std::vector<Vec3f> pts; pts.reserve(nbNodes);
|
||||
int ndOff = 0, trOff = 0;
|
||||
for (TopExp_Explorer exp(shape, TopAbs_FACE); exp.More(); exp.Next()) {
|
||||
const TopoDS_Shape& F = exp.Current();
|
||||
TopLoc_Location loc;
|
||||
Handle(Poly_Triangulation) tri = BRep_Tool::Triangulation(TopoDS::Face(F), loc);
|
||||
if (tri.IsNull()) continue;
|
||||
gp_Trsf T = loc.Transformation();
|
||||
for (int i = 1; i <= tri->NbNodes(); ++i) {
|
||||
gp_Pnt p = tri->Node(i); p.Transform(T);
|
||||
pts.emplace_back(Vec3f(p.X(), p.Y(), p.Z()));
|
||||
}
|
||||
auto orient = exp.Current().Orientation();
|
||||
int ids[3];
|
||||
for (int i = 1; i <= tri->NbTriangles(); ++i) {
|
||||
Poly_Triangle t = tri->Triangle(i); t.Get(ids[0], ids[1], ids[2]);
|
||||
if (orient == TopAbs_REVERSED) std::swap(ids[1], ids[2]);
|
||||
stl_facet f;
|
||||
f.vertex[0] = pts[ids[0]+ndOff-1].cast<float>();
|
||||
f.vertex[1] = pts[ids[1]+ndOff-1].cast<float>();
|
||||
f.vertex[2] = pts[ids[2]+ndOff-1].cast<float>();
|
||||
f.extra[0]=0; f.extra[1]=0;
|
||||
stl_normal n; stl_calculate_normal(n,&f); stl_normalize_vector(n);
|
||||
f.normal=n; stl.facet_start[trOff+i-1]=f;
|
||||
}
|
||||
ndOff += tri->NbNodes(); trOff += tri->NbTriangles();
|
||||
}
|
||||
TriangleMesh result; result.from_stl(stl); return result;
|
||||
}
|
||||
|
||||
GeometryEngine::Deviation
|
||||
GeometryEngine::surface_deviation(const TopoDS_Shape& candidate,
|
||||
const TopoDS_Shape& reference,
|
||||
double linear_deflection)
|
||||
{
|
||||
Deviation d;
|
||||
if (candidate.IsNull() || reference.IsNull()) return d;
|
||||
TriangleMesh mesh = tessellate(candidate, linear_deflection, 0.5);
|
||||
const auto& verts = mesh.its.vertices;
|
||||
if (verts.empty()) return d;
|
||||
double sum = 0.0, sumsq = 0.0;
|
||||
int n = 0;
|
||||
for (const auto& v : verts) {
|
||||
gp_Pnt p(v.x(), v.y(), v.z());
|
||||
BRepExtrema_DistShapeShape dss(BRepBuilderAPI_MakeVertex(p).Vertex(), reference);
|
||||
if (!dss.IsDone() || dss.NbSolution() < 1) continue;
|
||||
double dist = dss.Value();
|
||||
d.max_mm = std::max(d.max_mm, dist);
|
||||
sum += dist; sumsq += dist * dist; ++n;
|
||||
}
|
||||
d.sample_count = n;
|
||||
if (n > 0) { d.mean_mm = sum / n; d.rms_mm = std::sqrt(sumsq / n); }
|
||||
return d;
|
||||
}
|
||||
|
||||
GeometryEngine::MassProps GeometryEngine::mass_properties(const TopoDS_Shape& shape)
|
||||
{
|
||||
MassProps p;
|
||||
if (shape.IsNull()) return p;
|
||||
try {
|
||||
// A sheet body (open shell, no solid) encloses nothing, and BRepGProp::VolumeProperties
|
||||
// integrates the divergence theorem over whatever faces exist — on an open shell that is
|
||||
// not a volume at all. It came back as 96000 with an inertia diagonal of
|
||||
// [-4.2e7, -4.2e7, -6.9e7] for a 60x60x40 four-walled box: negative principal moments,
|
||||
// which no real body can have. The old code then hid the only obvious tell by taking
|
||||
// std::abs() of the mass. Report the honest answer instead — surface area is still
|
||||
// meaningful, so this is not a failure, just not a solid.
|
||||
p.is_solid = TopExp_Explorer(shape, TopAbs_SOLID).More();
|
||||
if (!p.is_solid) {
|
||||
GProp_GProps sonly;
|
||||
BRepGProp::SurfaceProperties(shape, sonly);
|
||||
p.surface_area = sonly.Mass();
|
||||
p.valid = true; // the area IS trustworthy; volume/inertia stay zero
|
||||
return p;
|
||||
}
|
||||
GProp_GProps vprops;
|
||||
BRepGProp::VolumeProperties(shape, vprops);
|
||||
double mass = vprops.Mass();
|
||||
if (std::abs(mass) < 1e-30) return p;
|
||||
p.volume = std::abs(mass);
|
||||
p.center_of_mass = Vec3d(vprops.CentreOfMass().X(), vprops.CentreOfMass().Y(), vprops.CentreOfMass().Z());
|
||||
gp_Mat mat = vprops.MatrixOfInertia();
|
||||
p.inertia = {{
|
||||
mat(1,1), mat(1,2), mat(1,3),
|
||||
mat(2,1), mat(2,2), mat(2,3),
|
||||
mat(3,1), mat(3,2), mat(3,3),
|
||||
}};
|
||||
GProp_GProps sprops;
|
||||
BRepGProp::SurfaceProperties(shape, sprops);
|
||||
p.surface_area = sprops.Mass();
|
||||
p.valid = true;
|
||||
} catch (const Standard_Failure&) {
|
||||
// leave valid = false
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
std::string GeometryEngine::primitive_name(PrimitiveType type)
|
||||
{
|
||||
switch (type) {
|
||||
case PrimitiveType::Box: return "Box";
|
||||
case PrimitiveType::Cylinder: return "Cylinder";
|
||||
case PrimitiveType::Sphere: return "Sphere";
|
||||
case PrimitiveType::Cone: return "Cone";
|
||||
case PrimitiveType::Torus: return "Torus";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Topology accessors ----
|
||||
|
||||
int GeometryEngine::face_count(const TopoDS_Shape& shape)
|
||||
{
|
||||
int n = 0;
|
||||
for (TopExp_Explorer e(shape, TopAbs_FACE); e.More(); e.Next())
|
||||
++n;
|
||||
return n;
|
||||
}
|
||||
|
||||
TopoDS_Face GeometryEngine::face_by_index(const TopoDS_Shape& shape, int index)
|
||||
{
|
||||
if (index < 0) return TopoDS_Face();
|
||||
int ordinal = 0;
|
||||
for (TopExp_Explorer e(shape, TopAbs_FACE); e.More(); e.Next()) {
|
||||
if (ordinal == index)
|
||||
return TopoDS::Face(e.Current());
|
||||
++ordinal;
|
||||
}
|
||||
return TopoDS_Face();
|
||||
}
|
||||
|
||||
std::vector<TopoDS_Face> GeometryEngine::faces_of(const TopoDS_Shape& shape)
|
||||
{
|
||||
std::vector<TopoDS_Face> out;
|
||||
for (TopExp_Explorer e(shape, TopAbs_FACE); e.More(); e.Next())
|
||||
out.push_back(TopoDS::Face(e.Current())); // same order as face_by_index
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<TopoDS_Edge> GeometryEngine::edges_of(const TopoDS_Shape& shape)
|
||||
{
|
||||
TopTools_IndexedMapOfShape map;
|
||||
TopExp::MapShapes(shape, TopAbs_EDGE, map); // same order as edge_by_index
|
||||
std::vector<TopoDS_Edge> out;
|
||||
out.reserve(map.Extent());
|
||||
for (int i = 1; i <= map.Extent(); ++i)
|
||||
out.push_back(TopoDS::Edge(map(i)));
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<TopoDS_Edge> GeometryEngine::edges_of_face(const TopoDS_Face& face)
|
||||
{
|
||||
std::vector<TopoDS_Edge> result;
|
||||
TopTools_IndexedMapOfShape map;
|
||||
TopExp::MapShapes(face, TopAbs_EDGE, map);
|
||||
for (int i = 1; i <= map.Extent(); ++i)
|
||||
result.push_back(TopoDS::Edge(map(i)));
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<Vec3d> GeometryEngine::sample_edge_world(const TopoDS_Edge& edge, double chord_tol)
|
||||
{
|
||||
if (BRep_Tool::Degenerated(edge))
|
||||
return {};
|
||||
|
||||
BRepAdaptor_Curve curve(edge);
|
||||
GCPnts_TangentialDeflection disc(curve, 0.1, chord_tol);
|
||||
|
||||
std::vector<Vec3d> pts;
|
||||
if (disc.NbPoints() >= 2) {
|
||||
for (int i = 1; i <= disc.NbPoints(); ++i) {
|
||||
gp_Pnt p = disc.Value(i);
|
||||
pts.emplace_back(p.X(), p.Y(), p.Z());
|
||||
}
|
||||
} else {
|
||||
gp_Pnt p0 = curve.Value(curve.FirstParameter());
|
||||
gp_Pnt p1 = curve.Value(curve.LastParameter());
|
||||
pts.emplace_back(p0.X(), p0.Y(), p0.Z());
|
||||
pts.emplace_back(p1.X(), p1.Y(), p1.Z());
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
Vec3d GeometryEngine::face_centroid_world(const TopoDS_Face& face)
|
||||
{
|
||||
GProp_GProps props;
|
||||
BRepGProp::SurfaceProperties(face, props);
|
||||
gp_Pnt c = props.CentreOfMass();
|
||||
return Vec3d(c.X(), c.Y(), c.Z());
|
||||
}
|
||||
|
||||
Vec3d GeometryEngine::face_normal_world(const TopoDS_Face& face)
|
||||
{
|
||||
BRepAdaptor_Surface surf(face);
|
||||
const double u = 0.5 * (surf.FirstUParameter() + surf.LastUParameter());
|
||||
const double v = 0.5 * (surf.FirstVParameter() + surf.LastVParameter());
|
||||
BRepLProp_SLProps props(surf, u, v, 1, 1e-6);
|
||||
gp_Dir n(0.0, 0.0, 1.0);
|
||||
if (props.IsNormalDefined()) n = props.Normal();
|
||||
if (face.Orientation() == TopAbs_REVERSED) n.Reverse(); // outward (account for face winding)
|
||||
return Vec3d(n.X(), n.Y(), n.Z());
|
||||
}
|
||||
|
||||
GeometryEngine::CylinderFace GeometryEngine::cylinder_of_face(const TopoDS_Face& face)
|
||||
{
|
||||
CylinderFace cf;
|
||||
if (face.IsNull()) return cf;
|
||||
BRepAdaptor_Surface surf(face);
|
||||
if (surf.GetType() != GeomAbs_Cylinder) return cf;
|
||||
|
||||
const gp_Cylinder cyl = surf.Cylinder();
|
||||
const gp_Ax1 ax = cyl.Axis();
|
||||
const Vec3d axis(ax.Direction().X(), ax.Direction().Y(), ax.Direction().Z());
|
||||
const Vec3d apt (ax.Location().X(), ax.Location().Y(), ax.Location().Z());
|
||||
cf.radius = cyl.Radius();
|
||||
|
||||
// Axial extent: V is the axial parameter on a cylinder; bound the face's two ends and
|
||||
// order them so `axis` points base -> top.
|
||||
const double umid = 0.5 * (surf.FirstUParameter() + surf.LastUParameter());
|
||||
const gp_Pnt e0 = surf.Value(umid, surf.FirstVParameter());
|
||||
const gp_Pnt e1 = surf.Value(umid, surf.LastVParameter());
|
||||
double t0 = (Vec3d(e0.X(), e0.Y(), e0.Z()) - apt).dot(axis);
|
||||
double t1 = (Vec3d(e1.X(), e1.Y(), e1.Z()) - apt).dot(axis);
|
||||
if (t1 < t0) std::swap(t0, t1);
|
||||
cf.base = apt + axis * t0;
|
||||
cf.axis = axis;
|
||||
cf.height = t1 - t0;
|
||||
|
||||
// Internal (bore) vs external: compare the face's outward normal at its centre to the
|
||||
// outward radial direction. A bore's normal points toward the axis (dot < 0).
|
||||
const gp_Pnt sp = surf.Value(umid, 0.5 * (surf.FirstVParameter() + surf.LastVParameter()));
|
||||
const Vec3d S(sp.X(), sp.Y(), sp.Z());
|
||||
const Vec3d axpt = cf.base + axis * (S - cf.base).dot(axis);
|
||||
const Vec3d radial = (S - axpt).normalized();
|
||||
cf.internal = face_normal_world(face).dot(radial) < 0.0;
|
||||
cf.ok = true;
|
||||
return cf;
|
||||
}
|
||||
|
||||
GeometryEngine::CylinderFace GeometryEngine::circle_of_edge(const TopoDS_Edge& edge)
|
||||
{
|
||||
CylinderFace cf;
|
||||
if (edge.IsNull()) return cf;
|
||||
BRepAdaptor_Curve curve(edge);
|
||||
if (curve.GetType() != GeomAbs_Circle) return cf;
|
||||
const gp_Circ c = curve.Circle();
|
||||
const gp_Ax1 ax = c.Axis();
|
||||
cf.base = Vec3d(c.Location().X(), c.Location().Y(), c.Location().Z());
|
||||
cf.axis = Vec3d(ax.Direction().X(), ax.Direction().Y(), ax.Direction().Z());
|
||||
cf.radius = c.Radius();
|
||||
cf.height = 0.0; // an edge carries no axial extent; the card keeps the current length
|
||||
cf.internal = false; // ambiguous from an edge alone — default external, user can toggle
|
||||
cf.ok = true;
|
||||
return cf;
|
||||
}
|
||||
|
||||
bool GeometryEngine::face_plane_bounds(const TopoDS_Face& face, const Vec3d& origin,
|
||||
const Vec3d& x_axis, const Vec3d& y_axis,
|
||||
double& umin, double& umax, double& vmin, double& vmax)
|
||||
{
|
||||
umin = vmin = 1e30; umax = vmax = -1e30;
|
||||
bool any = false;
|
||||
for (TopExp_Explorer ex(face, TopAbs_VERTEX); ex.More(); ex.Next()) {
|
||||
const gp_Pnt p = BRep_Tool::Pnt(TopoDS::Vertex(ex.Current()));
|
||||
const Vec3d P(p.X(), p.Y(), p.Z());
|
||||
const double u = (P - origin).dot(x_axis);
|
||||
const double v = (P - origin).dot(y_axis);
|
||||
umin = std::min(umin, u); umax = std::max(umax, u);
|
||||
vmin = std::min(vmin, v); vmax = std::max(vmax, v);
|
||||
any = true;
|
||||
}
|
||||
return any;
|
||||
}
|
||||
|
||||
int GeometryEngine::edge_count(const TopoDS_Shape& shape)
|
||||
{
|
||||
TopTools_IndexedMapOfShape map;
|
||||
TopExp::MapShapes(shape, TopAbs_EDGE, map);
|
||||
return map.Extent();
|
||||
}
|
||||
|
||||
TopoDS_Edge GeometryEngine::edge_by_index(const TopoDS_Shape& shape, int index)
|
||||
{
|
||||
TopTools_IndexedMapOfShape map;
|
||||
TopExp::MapShapes(shape, TopAbs_EDGE, map);
|
||||
if (index < 0 || index >= map.Extent())
|
||||
return TopoDS_Edge();
|
||||
return TopoDS::Edge(map(index + 1));
|
||||
}
|
||||
|
||||
int GeometryEngine::edge_index_of(const TopoDS_Shape& shape, const TopoDS_Edge& edge)
|
||||
{
|
||||
TopTools_IndexedMapOfShape map;
|
||||
TopExp::MapShapes(shape, TopAbs_EDGE, map);
|
||||
int idx = map.FindIndex(edge);
|
||||
return (idx > 0) ? (idx - 1) : -1;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,183 @@
|
||||
#ifndef slic3r_GeometryEngine_hpp_
|
||||
#define slic3r_GeometryEngine_hpp_
|
||||
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
|
||||
#include <BRepPrimAPI_MakeBox.hxx>
|
||||
#include <BRepPrimAPI_MakeCylinder.hxx>
|
||||
#include <BRepPrimAPI_MakeSphere.hxx>
|
||||
#include <BRepPrimAPI_MakeCone.hxx>
|
||||
#include <BRepPrimAPI_MakeTorus.hxx>
|
||||
#include <gp_Ax2.hxx>
|
||||
#include <TopoDS_Solid.hxx>
|
||||
#include <TopoDS_Face.hxx>
|
||||
#include <TopoDS_Edge.hxx>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
enum class PrimitiveType { Box, Cylinder, Sphere, Cone, Torus, COUNT };
|
||||
enum class DressUpType { Fillet, Chamfer };
|
||||
enum class FaceGroup { Top, Bottom, Lateral, All };
|
||||
|
||||
struct PrimitiveParams {
|
||||
PrimitiveType type{PrimitiveType::Box};
|
||||
double box_w{20}, box_h{20}, box_d{20};
|
||||
double cyl_radius{10}, cyl_height{20};
|
||||
double sph_radius{10};
|
||||
double cone_r1{10}, cone_r2{5}, cone_height{20};
|
||||
double torus_r1{10}, torus_r2{3};
|
||||
|
||||
// Dress-up
|
||||
bool dressup_enabled{false};
|
||||
DressUpType dressup_type{DressUpType::Fillet};
|
||||
FaceGroup dressup_faces{FaceGroup::All};
|
||||
double dressup_radius{1.0}; // fillet radius
|
||||
double dressup_chamfer_dist{1.0}; // chamfer distance (symmetric)
|
||||
|
||||
// Mesh quality
|
||||
double linear_deflection{0.01};
|
||||
double angular_deflection{0.5};
|
||||
|
||||
template<class Archive>
|
||||
void serialize(Archive& ar) {
|
||||
ar(type, box_w, box_h, box_d, cyl_radius, cyl_height, sph_radius,
|
||||
cone_r1, cone_r2, cone_height, torus_r1, torus_r2,
|
||||
dressup_enabled, dressup_type, dressup_faces, dressup_radius, dressup_chamfer_dist,
|
||||
linear_deflection, angular_deflection);
|
||||
}
|
||||
};
|
||||
|
||||
class GeometryEngine
|
||||
{
|
||||
public:
|
||||
static TopoDS_Solid make_primitive(const PrimitiveParams& params);
|
||||
|
||||
// Read a STEP file into its top-level solids (one TopoDS_Shape per solid; falls back to
|
||||
// the whole shape if it contains no closed solids). Reuses OCCT's STEPControl_Reader,
|
||||
// already linked via Format/STEP.cpp — no new dependency. err is set on failure (empty result).
|
||||
static std::vector<TopoDS_Shape> read_step_solids(const std::string& path, std::string& err);
|
||||
|
||||
// Triangle mesh -> B-rep solid. Native port of mesh2step
|
||||
// (github.com/tommasobbianchi/mesh2step): vertices and edges are SHARED across triangles
|
||||
// at construction time (vertex cache by deduped index, edge cache by unordered index pair),
|
||||
// so there is no BRepBuilderAPI_Sewing pass to reconstruct topology afterwards — which is
|
||||
// both faster and what makes watertightness fall out of the edge-usage counts for free.
|
||||
// Runs in-process on the OCCT kernel libslic3r already links: no STEP file is written or
|
||||
// re-read (a faceted STEP of a 62k-triangle mesh is ~149 MB and takes OCCT's reader >300 s
|
||||
// to parse back, so routing the Design tab through a file would hang the GUI).
|
||||
struct MeshBrepStats {
|
||||
int input_tris{0};
|
||||
int kept_tris{0};
|
||||
int degenerate_collapsed{0}; // <3 distinct vertices after tolerance quantization
|
||||
int degenerate_sliver{0}; // 3 distinct vertices but near-collinear
|
||||
int faces_built{0};
|
||||
int faces_failed{0};
|
||||
int unique_edges{0};
|
||||
int boundary_edges{0}; // used by exactly 1 triangle -> open shell
|
||||
int nonmanifold_edges{0}; // used by >=3 triangles
|
||||
bool watertight{false}; // every edge used exactly twice
|
||||
bool is_solid{false}; // watertight AND MakeSolid gave a positive volume
|
||||
double volume{0.0};
|
||||
int faces_final{0}; // after the optional coplanar merge
|
||||
};
|
||||
// tolerance: spatial quantization cell used ONLY for vertex dedup and as the
|
||||
// sub-resolution floor below which a triangle is noise. Never a sew tolerance.
|
||||
// merge_angle_deg > 0: run ShapeUpgrade_UnifySameDomain to merge coplanar neighbours into
|
||||
// single faces (a 12-triangle cube -> 6 pickable faces). This is what makes the imported
|
||||
// body editable with the face/edge tools; <= 0 keeps the exact one-face-per-triangle form.
|
||||
// Never wraps a non-watertight shell as a fake solid: an open mesh comes back as a shell,
|
||||
// with the reason (boundary / non-manifold edge counts) reported in stats.
|
||||
static TopoDS_Shape mesh_to_brep(const indexed_triangle_set& its,
|
||||
double tolerance,
|
||||
double merge_angle_deg,
|
||||
MeshBrepStats& stats);
|
||||
|
||||
struct MassProps {
|
||||
double volume{0.0};
|
||||
double surface_area{0.0};
|
||||
Vec3d center_of_mass{Vec3d::Zero()};
|
||||
std::array<double, 9> inertia{};
|
||||
bool valid{false};
|
||||
// False for a sheet body (an open shell with no solid). Volume and inertia are then
|
||||
// meaningless and are reported as zero; surface_area stays meaningful. See the .cpp.
|
||||
bool is_solid{false};
|
||||
};
|
||||
static MassProps mass_properties(const TopoDS_Shape& shape);
|
||||
|
||||
struct Deviation { double max_mm{0}; double mean_mm{0}; double rms_mm{0}; int sample_count{0}; };
|
||||
static Deviation surface_deviation(const TopoDS_Shape& candidate,
|
||||
const TopoDS_Shape& reference,
|
||||
double linear_deflection = 0.5);
|
||||
|
||||
static TopoDS_Shape apply_fillet(const TopoDS_Shape& solid, double radius,
|
||||
FaceGroup faces = FaceGroup::All);
|
||||
static TopoDS_Shape apply_fillet(const TopoDS_Shape& solid, double radius,
|
||||
int edge_id);
|
||||
static TopoDS_Shape apply_chamfer(const TopoDS_Shape& solid, double distance,
|
||||
FaceGroup faces = FaceGroup::All);
|
||||
static TopoDS_Shape apply_chamfer(const TopoDS_Shape& solid, double distance,
|
||||
int edge_id);
|
||||
|
||||
static TriangleMesh tessellate(const TopoDS_Shape& shape,
|
||||
double linear_deflection = 0.01,
|
||||
double angular_deflection = 0.5);
|
||||
static std::string primitive_name(PrimitiveType type);
|
||||
|
||||
// Topology accessors for in-viewport face/edge picking (Design tab). Face index is the
|
||||
// TopExp_Explorer(shape, TopAbs_FACE) ordinal — identical to SketchEngine::tessellate's
|
||||
// per-triangle face id, so a picked triangle's id maps back to a face here.
|
||||
static TopoDS_Face face_by_index(const TopoDS_Shape& shape, int index); // null if out of range
|
||||
static int face_count(const TopoDS_Shape& shape);
|
||||
// Bulk enumeration in the SAME order as face_by_index / edge_by_index, so ids are
|
||||
// interchangeable. Walking a body with the _by_index accessors is quadratic (each call
|
||||
// rescans the shape — edge_by_index even rebuilds the whole indexed map), which cost
|
||||
// ~15 s on a 4.7k-face imported solid; enumerate once instead.
|
||||
static std::vector<TopoDS_Face> faces_of(const TopoDS_Shape& shape);
|
||||
static std::vector<TopoDS_Edge> edges_of(const TopoDS_Shape& shape);
|
||||
static std::vector<TopoDS_Edge> edges_of_face(const TopoDS_Face& face);
|
||||
// Centre of mass (world) of a face — used to compute the extrude length for "up to face".
|
||||
static Vec3d face_centroid_world(const TopoDS_Face& face);
|
||||
// Outward unit normal of a face at its UV midpoint (orientation-aware) — for the shell gizmo.
|
||||
static Vec3d face_normal_world(const TopoDS_Face& face);
|
||||
// Sample an edge into a world-space polyline (>=2 pts) for pick-distance + highlight.
|
||||
static std::vector<Vec3d> sample_edge_world(const TopoDS_Edge& edge, double chord_tol = 0.05);
|
||||
// 0-based edge index into TopExp::MapShapes(shape, TopAbs_EDGE, map).
|
||||
static int edge_count(const TopoDS_Shape& shape);
|
||||
static TopoDS_Edge edge_by_index(const TopoDS_Shape& shape, int index);
|
||||
static int edge_index_of(const TopoDS_Shape& shape, const TopoDS_Edge& edge);
|
||||
|
||||
// Analysis of a cylindrical face for the Thread tool (a hole bore or a cylinder's lateral
|
||||
// surface): axis (base at the lower axial end + unit direction), radius, axial extent, and
|
||||
// whether it is a bore (face normal points toward the axis = internal thread). ok=false if
|
||||
// the face is not a cylinder.
|
||||
struct CylinderFace {
|
||||
bool ok{false};
|
||||
Vec3d base{0, 0, 0};
|
||||
Vec3d axis{0, 0, 1};
|
||||
double radius{0};
|
||||
double height{0};
|
||||
bool internal{false};
|
||||
};
|
||||
static CylinderFace cylinder_of_face(const TopoDS_Face& face);
|
||||
// Circular edge (a cylinder's perimeter): base = circle centre, axis = circle normal,
|
||||
// radius = circle radius, height = 0 (unknown from an edge), internal = false. ok=false if
|
||||
// the edge is not a circle. Lets the Thread tool be driven by a picked circular rim.
|
||||
static CylinderFace circle_of_edge(const TopoDS_Edge& edge);
|
||||
|
||||
// Plane-coordinate (u,v) bounding box of a face's vertices, measured from `origin` along
|
||||
// `x_axis`/`y_axis`. Lets the Hole tool dimension the hole from the face SIDES (umin/vmin =
|
||||
// two adjacent edges) instead of from the centre. Returns false if the face has no vertices.
|
||||
static bool face_plane_bounds(const TopoDS_Face& face, const Vec3d& origin,
|
||||
const Vec3d& x_axis, const Vec3d& y_axis,
|
||||
double& umin, double& umax, double& vmin, double& vmax);
|
||||
|
||||
private:
|
||||
static std::vector<TopoDS_Edge> collect_edges(const TopoDS_Shape& solid, FaceGroup faces);
|
||||
static FaceGroup classify_face(const TopoDS_Face& face, const TopoDS_Shape& solid);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_GeometryEngine_hpp_
|
||||
@@ -0,0 +1,307 @@
|
||||
#include "libslic3r/CAD/SketchConstraints.hpp"
|
||||
#include <Eigen/Dense>
|
||||
#include <cmath>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
int SketchConstraints::add_point(double x, double y)
|
||||
{
|
||||
m_vars.push_back(x);
|
||||
m_vars.push_back(y);
|
||||
return static_cast<int>(m_vars.size() / 2) - 1;
|
||||
}
|
||||
|
||||
void SketchConstraints::set_point(int id, double x, double y)
|
||||
{
|
||||
size_t idx = 2 * id;
|
||||
m_vars[idx] = x;
|
||||
m_vars[idx + 1] = y;
|
||||
}
|
||||
|
||||
Vec2d SketchConstraints::get_point(int id) const
|
||||
{
|
||||
size_t idx = 2 * id;
|
||||
return Vec2d(m_vars[idx], m_vars[idx + 1]);
|
||||
}
|
||||
|
||||
int SketchConstraints::point_count() const
|
||||
{
|
||||
return static_cast<int>(m_vars.size() / 2);
|
||||
}
|
||||
|
||||
void SketchConstraints::fix_point(int id)
|
||||
{
|
||||
size_t idx = 2 * id;
|
||||
Con c;
|
||||
c.type = FIX_POINT;
|
||||
c.a = id;
|
||||
c.b = c.c = c.d = 0;
|
||||
c.k0 = m_vars[idx];
|
||||
c.k1 = m_vars[idx + 1];
|
||||
m_cons.push_back(c);
|
||||
}
|
||||
|
||||
void SketchConstraints::coincident(int a, int b)
|
||||
{
|
||||
Con c;
|
||||
c.type = COINCIDENT;
|
||||
c.a = a; c.b = b; c.c = c.d = 0;
|
||||
c.k0 = c.k1 = 0;
|
||||
m_cons.push_back(c);
|
||||
}
|
||||
|
||||
void SketchConstraints::horizontal(int a, int b)
|
||||
{
|
||||
Con c;
|
||||
c.type = HORIZONTAL;
|
||||
c.a = a; c.b = b; c.c = c.d = 0;
|
||||
c.k0 = c.k1 = 0;
|
||||
m_cons.push_back(c);
|
||||
}
|
||||
|
||||
void SketchConstraints::vertical(int a, int b)
|
||||
{
|
||||
Con c;
|
||||
c.type = VERTICAL;
|
||||
c.a = a; c.b = b; c.c = c.d = 0;
|
||||
c.k0 = c.k1 = 0;
|
||||
m_cons.push_back(c);
|
||||
}
|
||||
|
||||
void SketchConstraints::distance(int a, int b, double d)
|
||||
{
|
||||
Con c;
|
||||
c.type = DISTANCE;
|
||||
c.a = a; c.b = b; c.c = c.d = 0;
|
||||
c.k0 = d; c.k1 = 0;
|
||||
m_cons.push_back(c);
|
||||
}
|
||||
|
||||
void SketchConstraints::lock_x(int id, double x)
|
||||
{
|
||||
Con c;
|
||||
c.type = LOCK_X;
|
||||
c.a = id;
|
||||
c.b = c.c = c.d = 0;
|
||||
c.k0 = x; c.k1 = 0;
|
||||
m_cons.push_back(c);
|
||||
}
|
||||
|
||||
void SketchConstraints::lock_y(int id, double y)
|
||||
{
|
||||
Con c;
|
||||
c.type = LOCK_Y;
|
||||
c.a = id;
|
||||
c.b = c.c = c.d = 0;
|
||||
c.k0 = y; c.k1 = 0;
|
||||
m_cons.push_back(c);
|
||||
}
|
||||
|
||||
void SketchConstraints::equal_length(int a, int b, int c, int d)
|
||||
{
|
||||
Con con;
|
||||
con.type = EQUAL_LENGTH;
|
||||
con.a = a; con.b = b; con.c = c; con.d = d;
|
||||
con.k0 = con.k1 = 0;
|
||||
m_cons.push_back(con);
|
||||
}
|
||||
|
||||
void SketchConstraints::parallel(int a, int b, int c, int d)
|
||||
{
|
||||
Con con;
|
||||
con.type = PARALLEL;
|
||||
con.a = a; con.b = b; con.c = c; con.d = d;
|
||||
con.k0 = con.k1 = 0;
|
||||
m_cons.push_back(con);
|
||||
}
|
||||
|
||||
void SketchConstraints::perpendicular(int a, int b, int c, int d)
|
||||
{
|
||||
Con con;
|
||||
con.type = PERPENDICULAR;
|
||||
con.a = a; con.b = b; con.c = c; con.d = d;
|
||||
con.k0 = con.k1 = 0;
|
||||
m_cons.push_back(con);
|
||||
}
|
||||
|
||||
void SketchConstraints::midpoint(int m, int a, int b)
|
||||
{
|
||||
Con con;
|
||||
con.type = MIDPOINT;
|
||||
con.a = m; con.b = a; con.c = b; con.d = -1;
|
||||
con.k0 = con.k1 = 0;
|
||||
m_cons.push_back(con);
|
||||
}
|
||||
|
||||
void SketchConstraints::symmetric(int a, int b, int c, int d)
|
||||
{
|
||||
Con con;
|
||||
con.type = SYMMETRIC;
|
||||
con.a = a; con.b = b; con.c = c; con.d = d;
|
||||
con.k0 = con.k1 = 0;
|
||||
m_cons.push_back(con);
|
||||
}
|
||||
|
||||
void SketchConstraints::angle(int a, int b, int c, int d, double radians)
|
||||
{
|
||||
Con con;
|
||||
con.type = ANGLE;
|
||||
con.a = a; con.b = b; con.c = c; con.d = d;
|
||||
con.k0 = radians; con.k1 = 0;
|
||||
m_cons.push_back(con);
|
||||
}
|
||||
|
||||
void SketchConstraints::point_line_distance(int p, int a, int b, double dist)
|
||||
{
|
||||
Con con;
|
||||
con.type = PT_LINE_DIST;
|
||||
con.a = p; con.b = a; con.c = b; con.d = -1;
|
||||
con.k0 = dist; con.k1 = 0;
|
||||
m_cons.push_back(con);
|
||||
}
|
||||
|
||||
Eigen::VectorXd SketchConstraints::residuals(const std::vector<double>& v) const
|
||||
{
|
||||
auto X = [&](int i) { return v[2 * i]; };
|
||||
auto Y = [&](int i) { return v[2 * i + 1]; };
|
||||
|
||||
std::vector<double> res;
|
||||
for (const auto& c : m_cons) {
|
||||
switch (c.type) {
|
||||
case FIX_POINT:
|
||||
res.push_back(X(c.a) - c.k0);
|
||||
res.push_back(Y(c.a) - c.k1);
|
||||
break;
|
||||
case COINCIDENT:
|
||||
res.push_back(X(c.a) - X(c.b));
|
||||
res.push_back(Y(c.a) - Y(c.b));
|
||||
break;
|
||||
case HORIZONTAL:
|
||||
res.push_back(Y(c.a) - Y(c.b));
|
||||
break;
|
||||
case VERTICAL:
|
||||
res.push_back(X(c.a) - X(c.b));
|
||||
break;
|
||||
case DISTANCE:
|
||||
res.push_back(std::hypot(X(c.a) - X(c.b), Y(c.a) - Y(c.b)) - c.k0);
|
||||
break;
|
||||
case LOCK_X:
|
||||
res.push_back(X(c.a) - c.k0);
|
||||
break;
|
||||
case LOCK_Y:
|
||||
res.push_back(Y(c.a) - c.k0);
|
||||
break;
|
||||
case EQUAL_LENGTH:
|
||||
res.push_back(std::hypot(X(c.a) - X(c.b), Y(c.a) - Y(c.b)) -
|
||||
std::hypot(X(c.c) - X(c.d), Y(c.c) - Y(c.d)));
|
||||
break;
|
||||
case PARALLEL:
|
||||
res.push_back((X(c.b) - X(c.a)) * (Y(c.d) - Y(c.c)) -
|
||||
(Y(c.b) - Y(c.a)) * (X(c.d) - X(c.c)));
|
||||
break;
|
||||
case PERPENDICULAR:
|
||||
res.push_back((X(c.b) - X(c.a)) * (X(c.d) - X(c.c)) +
|
||||
(Y(c.b) - Y(c.a)) * (Y(c.d) - Y(c.c)));
|
||||
break;
|
||||
case MIDPOINT:
|
||||
res.push_back(X(c.a) - 0.5 * (X(c.b) + X(c.c)));
|
||||
res.push_back(Y(c.a) - 0.5 * (Y(c.b) + Y(c.c)));
|
||||
break;
|
||||
case SYMMETRIC: {
|
||||
const double abx = X(c.b) - X(c.a), aby = Y(c.b) - Y(c.a);
|
||||
const double cdx = X(c.d) - X(c.c), cdy = Y(c.d) - Y(c.c);
|
||||
res.push_back(abx * cdx + aby * cdy);
|
||||
const double mx = 0.5 * (X(c.a) + X(c.b));
|
||||
const double my = 0.5 * (Y(c.a) + Y(c.b));
|
||||
res.push_back((mx - X(c.c)) * cdy - (my - Y(c.c)) * cdx);
|
||||
break;
|
||||
}
|
||||
case ANGLE: {
|
||||
const double ux = X(c.b) - X(c.a), uy = Y(c.b) - Y(c.a);
|
||||
const double wx = X(c.d) - X(c.c), wy = Y(c.d) - Y(c.c);
|
||||
const double cross = ux * wy - uy * wx;
|
||||
const double dot = ux * wx + uy * wy;
|
||||
res.push_back(std::atan2(cross, dot) - c.k0);
|
||||
break;
|
||||
}
|
||||
case PT_LINE_DIST: {
|
||||
const double bx = X(c.b), by = Y(c.b);
|
||||
const double cx = X(c.c), cy = Y(c.c);
|
||||
const double L = std::hypot(cx - bx, cy - by);
|
||||
const double num = (X(c.a) - bx) * (cy - by) - (Y(c.a) - by) * (cx - bx);
|
||||
res.push_back((L > 1e-12 ? std::abs(num) / L : 0.0) - c.k0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Eigen::VectorXd r(static_cast<Eigen::Index>(res.size()));
|
||||
for (size_t i = 0; i < res.size(); ++i)
|
||||
r(static_cast<Eigen::Index>(i)) = res[i];
|
||||
return r;
|
||||
}
|
||||
|
||||
Eigen::MatrixXd SketchConstraints::jacobian(const std::vector<double>& v) const
|
||||
{
|
||||
int m = static_cast<int>(residuals(v).size());
|
||||
int n = static_cast<int>(v.size());
|
||||
Eigen::MatrixXd J(m, n);
|
||||
const double eps = 1e-7;
|
||||
|
||||
std::vector<double> vp = v;
|
||||
std::vector<double> vm = v;
|
||||
|
||||
for (int j = 0; j < n; ++j) {
|
||||
vp[j] = v[j] + eps;
|
||||
vm[j] = v[j] - eps;
|
||||
Eigen::VectorXd rp = residuals(vp);
|
||||
Eigen::VectorXd rm = residuals(vm);
|
||||
vp[j] = v[j];
|
||||
vm[j] = v[j];
|
||||
J.col(j) = (rp - rm) / (2.0 * eps);
|
||||
}
|
||||
|
||||
return J;
|
||||
}
|
||||
|
||||
bool SketchConstraints::solve(int max_iter, double tol)
|
||||
{
|
||||
if (m_cons.empty()) return true;
|
||||
double lambda = 1e-3;
|
||||
Eigen::VectorXd r = residuals(m_vars);
|
||||
for (int it = 0; it < max_iter; ++it) {
|
||||
double rn = r.norm();
|
||||
if (rn < tol) return true;
|
||||
Eigen::MatrixXd J = jacobian(m_vars);
|
||||
Eigen::MatrixXd A = J.transpose() * J;
|
||||
Eigen::VectorXd g = J.transpose() * r;
|
||||
bool stepped = false;
|
||||
for (int t = 0; t < 12; ++t) {
|
||||
Eigen::MatrixXd Ad = A;
|
||||
for (int i = 0; i < Ad.rows(); ++i)
|
||||
Ad(i, i) += lambda * (1.0 + Ad(i, i));
|
||||
Eigen::VectorXd dx = Ad.ldlt().solve(-g);
|
||||
std::vector<double> cand = m_vars;
|
||||
for (size_t i = 0; i < cand.size(); ++i)
|
||||
cand[i] += dx[static_cast<Eigen::Index>(i)];
|
||||
Eigen::VectorXd rc = residuals(cand);
|
||||
if (rc.norm() < rn) {
|
||||
m_vars = cand;
|
||||
r = rc;
|
||||
lambda = std::max(lambda * 0.4, 1e-12);
|
||||
stepped = true;
|
||||
break;
|
||||
}
|
||||
lambda *= 3.0;
|
||||
}
|
||||
if (!stepped) break;
|
||||
}
|
||||
return r.norm() < tol * 100;
|
||||
}
|
||||
|
||||
double SketchConstraints::residual_norm() const
|
||||
{
|
||||
return residuals(m_vars).norm();
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,68 @@
|
||||
#ifndef slic3r_SketchConstraints_hpp_
|
||||
#define slic3r_SketchConstraints_hpp_
|
||||
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include <vector>
|
||||
#include <Eigen/Dense>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class SketchConstraints {
|
||||
public:
|
||||
int add_point(double x, double y);
|
||||
void set_point(int id, double x, double y);
|
||||
Vec2d get_point(int id) const;
|
||||
int point_count() const;
|
||||
|
||||
void fix_point(int id);
|
||||
void coincident(int a, int b);
|
||||
void horizontal(int a, int b);
|
||||
void vertical(int a, int b);
|
||||
void distance(int a, int b, double d);
|
||||
void lock_x(int id, double x);
|
||||
void lock_y(int id, double y);
|
||||
void equal_length(int a, int b, int c, int d);
|
||||
void parallel(int a, int b, int c, int d);
|
||||
void perpendicular(int a, int b, int c, int d);
|
||||
void midpoint(int m, int a, int b);
|
||||
void symmetric(int a, int b, int c, int d);
|
||||
void angle(int a, int b, int c, int d, double radians);
|
||||
void point_line_distance(int p, int a, int b, double dist);
|
||||
|
||||
bool solve(int max_iter = 200, double tol = 1e-10);
|
||||
double residual_norm() const;
|
||||
|
||||
private:
|
||||
std::vector<double> m_vars;
|
||||
|
||||
enum ConType : int {
|
||||
FIX_POINT = 0,
|
||||
COINCIDENT,
|
||||
HORIZONTAL,
|
||||
VERTICAL,
|
||||
DISTANCE,
|
||||
LOCK_X,
|
||||
LOCK_Y,
|
||||
EQUAL_LENGTH,
|
||||
PARALLEL,
|
||||
PERPENDICULAR,
|
||||
MIDPOINT,
|
||||
SYMMETRIC,
|
||||
ANGLE,
|
||||
PT_LINE_DIST
|
||||
};
|
||||
|
||||
struct Con {
|
||||
int type;
|
||||
int a, b, c, d;
|
||||
double k0, k1;
|
||||
};
|
||||
std::vector<Con> m_cons;
|
||||
|
||||
Eigen::VectorXd residuals(const std::vector<double>& v) const;
|
||||
Eigen::MatrixXd jacobian(const std::vector<double>& v) const;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_SketchConstraints_hpp_
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,373 @@
|
||||
#ifndef slic3r_SketchEngine_hpp_
|
||||
#define slic3r_SketchEngine_hpp_
|
||||
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/CAD/GeometryEngine.hpp"
|
||||
|
||||
#include <gp_Pln.hxx>
|
||||
#include <gp_Ax3.hxx>
|
||||
#include <TopoDS_Wire.hxx>
|
||||
#include <TopoDS_Shape.hxx>
|
||||
#include <TopoDS_Face.hxx>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
struct SketchSegment {
|
||||
enum Type { Line, Arc, Circle, Rectangle, Polygon };
|
||||
Type type{Line};
|
||||
Vec2d p0{0,0}, p1{0,0};
|
||||
Vec2d center{0,0};
|
||||
double radius{0}, start_angle{0}, end_angle{0};
|
||||
std::vector<Vec2d> points;
|
||||
template<class Archive>
|
||||
void serialize(Archive& ar) { ar(type, p0, p1, center, radius, start_angle, end_angle, points); }
|
||||
};
|
||||
|
||||
struct SketchEntity {
|
||||
enum class Type { Line, Arc, Circle, Point, Ellipse, EllipseArc, BSpline };
|
||||
Type type{Type::Line};
|
||||
Vec2d p0{0,0}; // Line: start; Arc/EllipseArc: start; Circle/Point/Ellipse: center; BSpline: first pole
|
||||
Vec2d p1{0,0}; // Line: end; Arc/EllipseArc: end; (unused for Circle/Point/Ellipse); BSpline: last pole
|
||||
Vec2d center{0,0}; // Arc/Circle/Ellipse(Arc) center
|
||||
double radius{0}; // Circle/Arc radius; Ellipse(Arc): semi-major axis (a)
|
||||
double start_angle{0}; // Arc sweep start; Ellipse(Arc): parametric start angle (radians)
|
||||
double end_angle{0}; // Arc sweep end; Ellipse(Arc): parametric end angle
|
||||
bool construction{false};
|
||||
double rminor{0}; // Ellipse(Arc): semi-minor axis (b)
|
||||
double rotation{0}; // Ellipse(Arc): major-axis angle phi (radians, about center)
|
||||
std::vector<Vec2d> ctrl; // BSpline: control points (poles); p0/p1 mirror first/last pole
|
||||
template<class Archive>
|
||||
void serialize(Archive& ar) {
|
||||
// Append-only: rminor/rotation added for Ellipse(Arc) (P2 Tier-B.1); ctrl for BSpline (B.2).
|
||||
ar(type, p0, p1, center, radius, start_angle, end_angle, construction, rminor, rotation, ctrl);
|
||||
}
|
||||
};
|
||||
|
||||
struct SketchPlane {
|
||||
Vec3d origin{0,0,0};
|
||||
Vec3d normal{0,0,1};
|
||||
Vec3d x_axis{1,0,0};
|
||||
Vec3d y_axis{0,1,0};
|
||||
|
||||
gp_Pln to_occt() const;
|
||||
static SketchPlane from_face(const TopoDS_Face& face);
|
||||
static SketchPlane XY() { return {}; }
|
||||
static SketchPlane XZ() { return {{0,0,0}, {0,1,0}, {1,0,0}, {0,0,1}}; }
|
||||
static SketchPlane YZ() { return {{0,0,0}, {1,0,0}, {0,1,0}, {0,0,1}}; }
|
||||
|
||||
Vec2d project(const Vec3d& ray_origin, const Vec3d& ray_dir) const;
|
||||
Vec3d to_world(const Vec2d& pt) const;
|
||||
|
||||
template<class Archive>
|
||||
void serialize(Archive& ar) { ar(origin, normal, x_axis, y_axis); }
|
||||
};
|
||||
|
||||
struct SketchProfile {
|
||||
std::vector<Vec2d> points;
|
||||
bool closed{false};
|
||||
|
||||
bool is_closed(double tolerance = 0.5) const;
|
||||
bool try_close(double tolerance = 0.5);
|
||||
void clear() { points.clear(); closed = false; }
|
||||
TopoDS_Wire to_occt_wire(const SketchPlane& plane) const;
|
||||
|
||||
template<class Archive>
|
||||
void serialize(Archive& ar) { ar(points, closed); }
|
||||
};
|
||||
|
||||
// Two sketch endpoints this close are ONE joint. Shared deliberately by the viewport
|
||||
// (region_loops / connected_loop / open-end detection) and by the kernel
|
||||
// (entities_to_wires): the viewport is what shades a region closed and offers it for
|
||||
// extrude, so the kernel MUST be able to build every loop the viewport shades. When
|
||||
// these two numbers disagreed the viewport promised a closed region at 1e-3 and the
|
||||
// kernel refused it at 1e-4, which extruded a solid the user never drew.
|
||||
// Nothing legitimate in a mm-scale sketch is 1 um apart.
|
||||
inline constexpr double kSketchJoinTol = 1e-3; // mm
|
||||
|
||||
// Effective sketch joint tolerance. ONE value for the viewport (region_loops /
|
||||
// loop_report / connected_loop) and the kernel (entities_to_wires): if these ever
|
||||
// disagree again, the viewport shades a region closed that the kernel refuses to
|
||||
// build, which is how a sketch got extruded into the wrong solid. The GUI pushes
|
||||
// the "auto_close_sketch_loops" preference in via set_sketch_auto_close(); the
|
||||
// kernel defaults to ON so headless/kernel-only callers keep welding.
|
||||
double sketch_join_tol();
|
||||
void set_sketch_auto_close(bool on);
|
||||
|
||||
enum class SketchConstraintType {
|
||||
Fix, Coincident, Horizontal, Vertical, Distance,
|
||||
LockX, LockY, EqualLength, Parallel, Perpendicular,
|
||||
Concentric,
|
||||
Tangent, Midpoint, Symmetric, Angle,
|
||||
Radius, Diameter,
|
||||
PointOnLine, // a point lies on a line (or at signed perpendicular distance `value`)
|
||||
PointOnObject, // a point lies on an entity edge (line -> PT_ON_LINE, circle -> PT_ON_CIRCLE)
|
||||
// Append-only: cereal serializes this enum positionally as its underlying int, so
|
||||
// inserting anywhere but the end reinterprets every constraint in every saved recipe.
|
||||
EqualRadius,
|
||||
Collinear,
|
||||
DistanceX, // |dx| between two points, projected onto the sketch X axis
|
||||
DistanceY, // |dy| between two points, projected onto the sketch Y axis
|
||||
SymmetricAboutY, // mirror across the sketch's vertical axis (x = 0); axis is implicit
|
||||
SymmetricAboutX // mirror across the sketch's horizontal axis (y = 0); axis is implicit
|
||||
};
|
||||
|
||||
// Constraint on a SketchProfile, referencing profile point indices (a,b,c,d).
|
||||
// `value` carries the target for Distance/LockX/LockY (ignored otherwise).
|
||||
struct SketchConstraintDef {
|
||||
SketchConstraintType type{SketchConstraintType::Coincident};
|
||||
int a{-1}, b{-1}, c{-1}, d{-1};
|
||||
double value{0.0};
|
||||
template<class Archive> void serialize(Archive& ar) { ar(type, a, b, c, d, value); }
|
||||
};
|
||||
|
||||
// Which point of an entity a constraint reference names.
|
||||
// P0 = SketchEntity::p0 (Line start / Point position)
|
||||
// P1 = SketchEntity::p1 (Line end)
|
||||
// Center = SketchEntity::center (Arc/Circle center)
|
||||
enum class SketchPointRole { P0, P1, Center };
|
||||
|
||||
// Constraint on coexisting SketchEntity objects (Fase 4.2). Each reference is an
|
||||
// (entity index, point role) pair. Point-form constraints
|
||||
// (Fix/Coincident/Horizontal/Vertical/Distance/LockX/LockY) use refs A and B as
|
||||
// individual points. Segment-form constraints (Parallel/Perpendicular/EqualLength)
|
||||
// use entity indices `ea`/`eb` as whole line segments (their P0->P1); roles are
|
||||
// ignored for those. `value` carries the target for Distance/LockX/LockY.
|
||||
struct SketchEntityConstraintDef {
|
||||
SketchConstraintType type{SketchConstraintType::Coincident};
|
||||
int ea{-1}, eb{-1}; // entity indices
|
||||
SketchPointRole ra{SketchPointRole::P0}; // role within ea
|
||||
SketchPointRole rb{SketchPointRole::P0}; // role within eb
|
||||
double value{0.0};
|
||||
int ec{-1}; // third entity ref (Symmetric axis)
|
||||
SketchPointRole rc{SketchPointRole::P0}; // role within ec
|
||||
template<class Archive> void serialize(Archive& ar) { ar(type, ea, eb, ra, rb, value, ec, rc); }
|
||||
};
|
||||
|
||||
// Implicit references every sketch has, addressable from a constraint's ea/eb/ec without
|
||||
// existing as SketchEntity objects. NEGATIVE so they cannot collide with an entity index;
|
||||
// -1 is already "unset" and stays that way. Values are serialized inside existing int
|
||||
// fields, so they are append-only in spirit: never renumber these.
|
||||
constexpr int kSketchRefOrigin = -2; // the sketch origin point (0,0)
|
||||
constexpr int kSketchRefAxisX = -3; // the sketch X axis, through the origin, +X
|
||||
constexpr int kSketchRefAxisY = -4; // the sketch Y axis, through the origin, +Y
|
||||
|
||||
inline bool is_sketch_ref(int ei) { return ei <= kSketchRefOrigin; }
|
||||
|
||||
// How many real endpoints a type exposes, and which roles they are. p1 is UNUSED for
|
||||
// Circle/Point/Ellipse (SketchEntity::p1 above) and reads (0,0) — walking {P0,P1} blindly
|
||||
// over those invents a phantom endpoint at the origin, which for a pair of Points always
|
||||
// wins a closest-pair search at distance 0 and binds a role the solver silently refuses.
|
||||
int sketch_entity_ends(const SketchEntity& e, std::pair<SketchPointRole, Vec2d> out[2]);
|
||||
bool sketch_closest_ends(const SketchEntity& A, const SketchEntity& B,
|
||||
SketchPointRole& ra, SketchPointRole& rb, Vec2d& pa, Vec2d& pb);
|
||||
|
||||
// Why an entity-constraint pick is refused. The caller maps a reason to a localized string;
|
||||
// the planner itself stays translation-free.
|
||||
enum class ConstraintReject {
|
||||
None, NeedOneEntity, NeedTwoEntities, NeedALine, NeedTwoLines,
|
||||
NeedTwoRounds, NeedTangentPair, NeedJoinablePoints, NeedMeasurablePoints,
|
||||
// The following are not in the GUI's current switch but are the faithful outcomes of
|
||||
// its remaining branches; they need a reason too or the caller cannot tell them apart.
|
||||
NeedPointAndLine, // Midpoint: one Point + one Line
|
||||
NeedTwoPointsOrLines, // Symmetric / SymmetricAboutX/Y: two Points or two Lines
|
||||
NeedAxisLine, // Symmetric: e2 must be a Line to act as the axis
|
||||
NeedRound, // Radius/Diameter: a Circle or Arc
|
||||
Unsupported // entity-constraint path has no binding for this type
|
||||
};
|
||||
|
||||
struct ConstraintPlan {
|
||||
enum class Kind { Reject, Apply, AskValue };
|
||||
Kind kind{Kind::Reject};
|
||||
ConstraintReject reason{ConstraintReject::None};
|
||||
// Apply/AskValue only: the defs to commit. One element for every ordinary type, TWO for
|
||||
// Symmetric/SymmetricAboutX/Y on two lines (P0/P0 and P1/P1), matching the GUI's builds.
|
||||
std::vector<SketchEntityConstraintDef> defs{};
|
||||
double prefill{0.0}; // AskValue only: the value to show pre-filled
|
||||
};
|
||||
|
||||
// Pure: no wx, no translation, no UI. The caller maps `reason` to a localized string.
|
||||
// e2 is the axis-line pick Symmetric needs (def.ec); every other type ignores it.
|
||||
ConstraintPlan plan_entity_constraint(const std::vector<SketchEntity>& ents,
|
||||
int e0, int e1, int e2, SketchConstraintType type);
|
||||
|
||||
// Solve a bare entity list in place against entity-form constraints. Shared by
|
||||
// CadDocument::solve_sketch_feature (committed features) and the in-session GUI
|
||||
// sketch tool (live solving as dimensions/constraints are added). Returns true on
|
||||
// convergence; an empty constraint list is a no-op that returns true.
|
||||
bool solve_sketch_entities(std::vector<SketchEntity>& entities,
|
||||
const std::vector<SketchEntityConstraintDef>& constraints);
|
||||
|
||||
struct SketchParams {
|
||||
// Extrude/Revolve
|
||||
double extrude_len{10}; bool extrude_sym{false}; double extrude_taper{0};
|
||||
double revolve_deg{360};
|
||||
bool is_pocket{false}; // cut into selected object instead of new
|
||||
|
||||
// Dress-up
|
||||
bool dressup_enabled{false};
|
||||
DressUpType dressup_type{DressUpType::Fillet};
|
||||
FaceGroup dressup_faces{FaceGroup::All};
|
||||
double dressup_radius{1.0};
|
||||
double dressup_chamfer_dist{1.0};
|
||||
|
||||
// Mesh
|
||||
double linear_deflection{0.01};
|
||||
|
||||
template<class Archive>
|
||||
void serialize(Archive& ar) {
|
||||
ar(extrude_len, extrude_sym, extrude_taper, revolve_deg, is_pocket,
|
||||
dressup_enabled, dressup_type, dressup_faces, dressup_radius, dressup_chamfer_dist,
|
||||
linear_deflection);
|
||||
}
|
||||
};
|
||||
|
||||
class SketchEngine
|
||||
{
|
||||
public:
|
||||
static TopoDS_Shape make_extrude(const TopoDS_Wire& wire, const SketchPlane& plane,
|
||||
double length, bool symmetric = false, double taper_deg = 0.0);
|
||||
static TopoDS_Shape make_extrude(const TopoDS_Face& face, const SketchPlane& plane,
|
||||
double length, bool symmetric = false, double taper_deg = 0.0);
|
||||
// Asymmetric two-sided prism: extrude the wire's face by `up` along +normal and `down`
|
||||
// along -normal, fused into one solid. up/down are non-negative magnitudes.
|
||||
// Tapered (draft) extrude of a planar wire: the top profile is the base wire offset in its
|
||||
// plane by length*tan(taper_deg), lofted from base to top. Falls back to a straight prism on
|
||||
// any failure (self-intersecting offset / loft error). taper_deg>0 widens the top.
|
||||
static TopoDS_Shape make_extrude_taper(const TopoDS_Wire& wire, const SketchPlane& plane,
|
||||
double length, double taper_deg);
|
||||
static TopoDS_Shape make_extrude_two_sided(const TopoDS_Wire& wire, const SketchPlane& plane,
|
||||
double up, double down);
|
||||
static TopoDS_Shape make_extrude_two_sided(const TopoDS_Face& face, const SketchPlane& plane,
|
||||
double up, double down);
|
||||
static TopoDS_Shape make_extrude_face(const TopoDS_Face& face, const SketchPlane& plane,
|
||||
double length, bool symmetric = false, double taper_deg = 0.0);
|
||||
|
||||
// Extrude a set of imported rigid regions (Text/SVG). Each region is
|
||||
// contour[0]=outer loop + contour[1..]=hole loops, in plane (u,v) mm. Builds
|
||||
// one planar face-with-holes per region, extrudes it, and fuses all region
|
||||
// solids into a single shape. Empty/degenerate contours are skipped.
|
||||
static TopoDS_Shape make_extrude_regions(
|
||||
const std::vector<std::vector<std::vector<Vec2d>>>& regions,
|
||||
const SketchPlane& plane, double length, bool symmetric = false);
|
||||
|
||||
// Revolve a planar profile wire about an axis lying in the sketch plane and
|
||||
// passing through the plane origin: axis_sel 0 = plane X axis, 1 = plane Y axis.
|
||||
// A negative angle_deg sweeps the opposite direction (Flip). The profile must
|
||||
// lie to one side of the axis (Onshape rule); a straddling profile self-intersects.
|
||||
static TopoDS_Shape make_revolve(const TopoDS_Wire& wire, const SketchPlane& plane,
|
||||
double angle_deg = 360.0, int axis_sel = 0);
|
||||
|
||||
// Sweep a planar profile wire along a path (spine) wire. The profile is turned
|
||||
// into a face and swept with BRepOffsetAPI_MakePipe, which keeps the profile
|
||||
// perpendicular to the spine along its length. The path may be open or closed;
|
||||
// for a clean solid the path's first point should sit on/near the profile plane.
|
||||
static TopoDS_Shape make_sweep(const TopoDS_Wire& profile, const TopoDS_Wire& path);
|
||||
|
||||
// Loft a solid through 2+ closed profile wires (each on its own plane), in the
|
||||
// given order. ruled=true => straight (ruled) sections; false => smooth (C2).
|
||||
static TopoDS_Shape make_loft(const std::vector<TopoDS_Wire>& profiles, bool ruled);
|
||||
|
||||
// Skin `profiles` WITHOUT end caps -> an open shell (sheet). Same as make_loft but the
|
||||
// ThruSections solid flag is false. // ponytail: a sibling instead of a bool param, so no
|
||||
// existing call site changes.
|
||||
static TopoDS_Shape make_loft_surface(const std::vector<TopoDS_Wire>& profiles, bool ruled);
|
||||
|
||||
static TopoDS_Shape make_pocket(const TopoDS_Wire& wire, const SketchPlane& plane,
|
||||
const TopoDS_Shape& target, double depth);
|
||||
|
||||
static TriangleMesh tessellate(const TopoDS_Shape& shape,
|
||||
double linear_deflection = 0.01,
|
||||
double angular_deflection = 0.5);
|
||||
|
||||
static TriangleMesh tessellate(const TopoDS_Shape& shape,
|
||||
std::vector<int>& tri_face,
|
||||
double linear_deflection = 0.01,
|
||||
double angular_deflection = 0.5);
|
||||
|
||||
static TopoDS_Wire entities_to_wire(const std::vector<SketchEntity>& entities,
|
||||
const SketchPlane& plane,
|
||||
bool closed_only = false);
|
||||
|
||||
// Every loop the sketch holds, in the order each loop's FIRST entity appears in
|
||||
// `entities`. A Circle or Ellipse is a loop on its own; Line/Arc/EllipseArc/BSpline
|
||||
// entities are grouped into loops by shared endpoints. An OPEN chain is returned too —
|
||||
// a sweep path is legitimately open, so open-ness is not an error here — unless
|
||||
// `closed_only` is true, in which case an open chain is DISCARDED (skipped, not an
|
||||
// error). Empty vector = nothing usable; the caller decides whether that is an error.
|
||||
static std::vector<TopoDS_Wire> entities_to_wires(const std::vector<SketchEntity>& entities,
|
||||
const SketchPlane& plane,
|
||||
bool closed_only = false);
|
||||
|
||||
// A planar face from a set of coplanar loops: the largest-area loop is the outer boundary
|
||||
// and every other loop is a hole in it. Throws std::runtime_error with a message naming the
|
||||
// problem when the loops do not describe one such region.
|
||||
static TopoDS_Face wires_to_face(const std::vector<TopoDS_Wire>& wires,
|
||||
const SketchPlane& plane);
|
||||
|
||||
static std::vector<SketchEntity> mirror_entities(
|
||||
const std::vector<SketchEntity>& src, const Vec2d& a, const Vec2d& b);
|
||||
|
||||
// Offset a sketch by `d`, PRESERVING CHAINS. Entities joined by shared endpoints are
|
||||
// offset together and their seams repaired (miter join), so a closed profile comes back
|
||||
// closed and can still be extruded; per-entity offsetting cannot do that. Sign convention:
|
||||
// +d moves each curve to the LEFT of its direction of travel, which for a CCW closed loop
|
||||
// is inward. Ellipses and splines are not offset (a parallel of either is not the same
|
||||
// kind of curve) and are dropped from the result.
|
||||
static std::vector<SketchEntity> offset_entities(
|
||||
const std::vector<SketchEntity>& src, double d);
|
||||
|
||||
// Rigid-transform array. Returns the (count-1) copies for instance i=1..count-1
|
||||
// (the originals in `src` are NOT included). Each copy i is `src` rigidly
|
||||
// transformed by: rotate by i*angle_step about `pivot`, then translate by i*step.
|
||||
// Rectangular/linear array: angle_step = 0, step = spacing*direction (pivot unused).
|
||||
// Polar array: step = (0,0), angle_step = sweep/count, pivot = centre.
|
||||
// Orientation-preserving, so arc/ellipse parametric angles shift by i*angle_step.
|
||||
static std::vector<SketchEntity> array_entities(
|
||||
const std::vector<SketchEntity>& src, int count,
|
||||
const Vec2d& step, double angle_step, const Vec2d& pivot);
|
||||
|
||||
// General affine transform (move / rotate / scale), applied IN PLACE: returns
|
||||
// the SAME entities (same count and order), each mapped by
|
||||
// p -> pivot + scale * R(angle) * (p - pivot) + move
|
||||
// (radii scale by |scale|; arc/ellipse parametric/rotation angles shift by
|
||||
// `angle`). Unlike array_entities this mutates the subjects rather than adding
|
||||
// copies. Move: angle=0, scale=1. Rotate-in-place: move=(0,0), scale=1,
|
||||
// pivot=centroid. Scale: angle=0.
|
||||
static std::vector<SketchEntity> transform_entities(
|
||||
const std::vector<SketchEntity>& src,
|
||||
const Vec2d& move, double angle, double scale, const Vec2d& pivot);
|
||||
|
||||
static bool fillet_lines(const SketchEntity& a, const SketchEntity& b, double r,
|
||||
SketchEntity& a_out, SketchEntity& b_out, SketchEntity& arc_out);
|
||||
|
||||
// Symmetric chamfer between two lines meeting at a corner: trims each line back
|
||||
// by setback distance `d` from the shared corner and returns the connecting
|
||||
// straight segment (seg_out) in place of the corner. a_out/b_out are the trimmed
|
||||
// lines; seg_out goes seg_out.p0 (on a) -> seg_out.p1 (on b). False if the lines
|
||||
// are parallel or `d` overruns either line.
|
||||
static bool chamfer_lines(const SketchEntity& a, const SketchEntity& b, double d,
|
||||
SketchEntity& a_out, SketchEntity& b_out, SketchEntity& seg_out);
|
||||
|
||||
static bool trim_entity(SketchEntity& e, const std::vector<SketchEntity>& others,
|
||||
const Vec2d& pick);
|
||||
|
||||
static bool extend_entity(SketchEntity& e, const std::vector<SketchEntity>& others,
|
||||
const Vec2d& pick);
|
||||
|
||||
// Build a cubic-Bezier G1 bridge (as a BSpline entity, 4 poles) connecting endpoint
|
||||
// `a_end` of `a` to endpoint `b_end` of `b` (0 = start/p0 side, 1 = end/p1 side).
|
||||
// Tangent-continuous with both entities where the endpoint tangent is defined.
|
||||
static SketchEntity make_bridge(const SketchEntity& a, int a_end,
|
||||
const SketchEntity& b, int b_end);
|
||||
};
|
||||
|
||||
// Free endpoints of a sketch: the sketch-space points where a chain fails to close.
|
||||
// Same weld tolerance as the wire build, so it can never contradict it.
|
||||
std::vector<Vec2d> sketch_open_ends(const std::vector<SketchEntity>&, const SketchPlane&);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_SketchEngine_hpp_
|
||||
@@ -0,0 +1,145 @@
|
||||
#include "libslic3r/CAD/SketchImport.hpp"
|
||||
|
||||
#include "libslic3r/Emboss.hpp"
|
||||
#include "libslic3r/NSVGUtils.hpp"
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "libslic3r/TextConfiguration.hpp" // FontProp
|
||||
#include "libslic3r/libslic3r.h" // SCALING_FACTOR
|
||||
#include "libslic3r/Utils.hpp" // resources_dir
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Convert one ExPolygon (outer contour + CW holes) into an ImportRegion,
|
||||
// mapping each integer Point to plane (u,v) mm via `to_mm`.
|
||||
template<class ToMm>
|
||||
static ImportRegion expoly_to_region(const ExPolygon& ex, ToMm to_mm)
|
||||
{
|
||||
auto contour_pts = [&](const Polygon& poly) {
|
||||
std::vector<Vec2d> c;
|
||||
c.reserve(poly.points.size());
|
||||
for (const Point& p : poly.points)
|
||||
c.push_back(to_mm(p));
|
||||
return c;
|
||||
};
|
||||
ImportRegion region;
|
||||
region.push_back(contour_pts(ex.contour));
|
||||
for (const Polygon& h : ex.holes)
|
||||
region.push_back(contour_pts(h));
|
||||
return region;
|
||||
}
|
||||
|
||||
// Shift all regions so their common bounding-box centre sits on the origin
|
||||
// (Onshape/typical CAD insert places imported art centred on the sketch).
|
||||
static void center_regions(ImportRegions& regs)
|
||||
{
|
||||
double lo_x = std::numeric_limits<double>::max();
|
||||
double lo_y = std::numeric_limits<double>::max();
|
||||
double hi_x = -std::numeric_limits<double>::max();
|
||||
double hi_y = -std::numeric_limits<double>::max();
|
||||
bool any = false;
|
||||
for (const auto& region : regs)
|
||||
for (const auto& contour : region)
|
||||
for (const Vec2d& p : contour) {
|
||||
lo_x = std::min(lo_x, p.x()); hi_x = std::max(hi_x, p.x());
|
||||
lo_y = std::min(lo_y, p.y()); hi_y = std::max(hi_y, p.y());
|
||||
any = true;
|
||||
}
|
||||
if (!any) return;
|
||||
const Vec2d c(0.5 * (lo_x + hi_x), 0.5 * (lo_y + hi_y));
|
||||
for (auto& region : regs)
|
||||
for (auto& contour : region)
|
||||
for (Vec2d& p : contour)
|
||||
p -= c;
|
||||
}
|
||||
|
||||
static std::string default_font_path()
|
||||
{
|
||||
return resources_dir() + "/fonts/HarmonyOS_Sans_SC_Regular.ttf";
|
||||
}
|
||||
|
||||
ImportRegions text_to_regions(const std::string& utf8, double size_mm,
|
||||
const std::string& font_path)
|
||||
{
|
||||
if (utf8.empty() || size_mm <= 0.0)
|
||||
return {};
|
||||
|
||||
const std::string path = font_path.empty() ? default_font_path() : font_path;
|
||||
std::unique_ptr<Emboss::FontFile> ff = Emboss::create_font_file(path.c_str());
|
||||
if (!ff)
|
||||
return {};
|
||||
Emboss::FontFileWithCache fwc(std::move(ff));
|
||||
if (!fwc.has_value())
|
||||
return {};
|
||||
|
||||
FontProp prop(static_cast<float>(size_mm)); // per_glyph=false
|
||||
HealedExPolygons healed = Emboss::text2shapes(fwc, utf8.c_str(), prop);
|
||||
if (healed.expolygons.empty())
|
||||
return {};
|
||||
|
||||
// Shape points are integers scaled by 1/SHAPE_SCALE in font units;
|
||||
// get_text_shape_scale collapses (size_in_mm / unit_per_em) * SHAPE_SCALE
|
||||
// into a single mm-per-shape-unit factor. FreeType y is up already.
|
||||
const double s = Emboss::get_text_shape_scale(prop, *fwc.font_file);
|
||||
auto to_mm = [s](const Point& p) { return Vec2d(p.x() * s, p.y() * s); };
|
||||
|
||||
ImportRegions regs;
|
||||
regs.reserve(healed.expolygons.size());
|
||||
for (const ExPolygon& ex : healed.expolygons)
|
||||
regs.push_back(expoly_to_region(ex, to_mm));
|
||||
|
||||
center_regions(regs);
|
||||
return regs;
|
||||
}
|
||||
|
||||
ImportRegions svg_to_regions(const std::string& svg_path, double scale)
|
||||
{
|
||||
if (svg_path.empty() || scale <= 0.0)
|
||||
return {};
|
||||
|
||||
NSVGimage_ptr image = nsvgParseFromFile(svg_path, "mm", 96.0f);
|
||||
if (!image)
|
||||
return {};
|
||||
|
||||
// A filled shape that also carries a stroke would import the stroke as a
|
||||
// thick outline band wrapped around the fill (the reported "too large line
|
||||
// width"). For CAD import the fill silhouette is what's wanted, so drop the
|
||||
// stroke on any shape that has a fill; stroke-only line art is kept.
|
||||
for (NSVGshape* s = image->shapes; s != nullptr; s = s->next)
|
||||
if (s->fill.type != NSVG_PAINT_NONE)
|
||||
s->stroke.type = NSVG_PAINT_NONE;
|
||||
|
||||
// tesselation tolerance is in image (mm) scale; 0.3 mm keeps curves smooth
|
||||
// without exploding the contour count. is_y_negative (default) flips SVG's
|
||||
// y-down to the sketch's y-up.
|
||||
NSVGLineParams param(0.3);
|
||||
ExPolygonsWithIds ids = create_shape_with_ids(*image, param);
|
||||
|
||||
// NSVG points are integers scaled by 1/SCALING_FACTOR (param.scale default):
|
||||
// mm = point * SCALING_FACTOR, then the user scale factor.
|
||||
const double s = SCALING_FACTOR * scale;
|
||||
auto to_mm = [s](const Point& p) { return Vec2d(p.x() * s, p.y() * s); };
|
||||
|
||||
ImportRegions regs;
|
||||
for (const ExPolygonsWithId& w : ids)
|
||||
for (const ExPolygon& ex : w.expoly)
|
||||
regs.push_back(expoly_to_region(ex, to_mm));
|
||||
|
||||
center_regions(regs);
|
||||
return regs;
|
||||
}
|
||||
|
||||
ImportRegions transform_regions(const ImportRegions& src, const Vec2d& offset,
|
||||
double scale_x, double scale_y)
|
||||
{
|
||||
ImportRegions out = src;
|
||||
for (auto& region : out)
|
||||
for (auto& contour : region)
|
||||
for (Vec2d& p : contour)
|
||||
p = Vec2d(p.x() * scale_x + offset.x(), p.y() * scale_y + offset.y());
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,37 @@
|
||||
#ifndef slic3r_SketchImport_hpp_
|
||||
#define slic3r_SketchImport_hpp_
|
||||
|
||||
#include "libslic3r/Point.hpp" // Vec2d
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// A rigid imported region: contour[0] = outer loop, contour[1..] = holes;
|
||||
// points in plane (u,v) millimetres. The nested vector type matches
|
||||
// CadFeature::imported_regions exactly, so results assign directly.
|
||||
using ImportRegion = std::vector<std::vector<Vec2d>>;
|
||||
using ImportRegions = std::vector<ImportRegion>;
|
||||
|
||||
// Vectorize UTF-8 text into filled regions (mm), centred on the origin.
|
||||
// `size_mm` is the cap/line height. `font_path` empty -> a bundled default
|
||||
// font (resources/fonts). Returns an empty vector on any failure.
|
||||
ImportRegions text_to_regions(const std::string& utf8, double size_mm,
|
||||
const std::string& font_path = std::string());
|
||||
|
||||
// Parse an SVG file's filled paths into regions (mm), centred on the origin.
|
||||
// `scale` multiplies the authored size (1.0 = as authored). Returns an empty
|
||||
// vector on any failure.
|
||||
ImportRegions svg_to_regions(const std::string& svg_path, double scale = 1.0);
|
||||
|
||||
// Apply an axis-aligned placement transform to regions:
|
||||
// p -> ( p.x * scale_x + offset.x, p.y * scale_y + offset.y )
|
||||
// Used to move / enlarge / stretch imported art non-destructively (the
|
||||
// feature keeps the centred source regions + this transform).
|
||||
ImportRegions transform_regions(const ImportRegions& src, const Vec2d& offset,
|
||||
double scale_x, double scale_y);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_SketchImport_hpp_
|
||||
@@ -0,0 +1,234 @@
|
||||
#include "libslic3r/CAD/SketchInference.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Candidate target collected during the scan; we keep the closest within each
|
||||
// priority tier and resolve ties by tier then distance.
|
||||
namespace {
|
||||
struct Cand {
|
||||
InferenceSnap::Kind kind{InferenceSnap::Kind::None};
|
||||
int entity{-1};
|
||||
SketchPointRole role{SketchPointRole::P0};
|
||||
Vec2d point{0, 0};
|
||||
double dist{0.0};
|
||||
};
|
||||
|
||||
// Lower number = higher priority.
|
||||
int tier(InferenceSnap::Kind k)
|
||||
{
|
||||
switch (k) {
|
||||
case InferenceSnap::Kind::Endpoint: return 0;
|
||||
case InferenceSnap::Kind::Center: return 1;
|
||||
case InferenceSnap::Kind::Origin: return 2;
|
||||
case InferenceSnap::Kind::Midpoint: return 3;
|
||||
case InferenceSnap::Kind::OnEdge: return 4;
|
||||
default: return 9;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
InferenceSnap infer_point_snap(const std::vector<SketchEntity>& entities,
|
||||
const Vec2d& query, double tol,
|
||||
bool include_origin)
|
||||
{
|
||||
Cand best;
|
||||
best.kind = InferenceSnap::Kind::None;
|
||||
best.point = query;
|
||||
|
||||
auto offer = [&](InferenceSnap::Kind k, int ent, SketchPointRole r, const Vec2d& q) {
|
||||
const double d = (q - query).norm();
|
||||
if (d > tol) return;
|
||||
const bool better = (best.kind == InferenceSnap::Kind::None) ||
|
||||
(tier(k) < tier(best.kind)) ||
|
||||
(tier(k) == tier(best.kind) && d < best.dist);
|
||||
if (better) { best.kind = k; best.entity = ent; best.role = r; best.point = q; best.dist = d; }
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < entities.size(); ++i) {
|
||||
const SketchEntity& e = entities[i];
|
||||
const int ei = int(i);
|
||||
switch (e.type) {
|
||||
case SketchEntity::Type::Line: {
|
||||
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P0, e.p0);
|
||||
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P1, e.p1);
|
||||
offer(InferenceSnap::Kind::Midpoint, ei, SketchPointRole::P0, 0.5 * (e.p0 + e.p1));
|
||||
// Projection onto the segment interior (PointOnObject candidate).
|
||||
const Vec2d d = e.p1 - e.p0;
|
||||
const double L2 = d.squaredNorm();
|
||||
if (L2 > 1e-12) {
|
||||
double t = (query - e.p0).dot(d) / L2;
|
||||
if (t > 0.02 && t < 0.98)
|
||||
offer(InferenceSnap::Kind::OnEdge, ei, SketchPointRole::P0, e.p0 + t * d);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SketchEntity::Type::Arc: {
|
||||
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P0, e.p0);
|
||||
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P1, e.p1);
|
||||
offer(InferenceSnap::Kind::Center, ei, SketchPointRole::Center, e.center);
|
||||
// Mid-arc point, so an arc is as snappable in its middle as a line is.
|
||||
const double am = 0.5 * (e.start_angle + e.end_angle);
|
||||
offer(InferenceSnap::Kind::Midpoint, ei, SketchPointRole::P0,
|
||||
Vec2d(e.center.x() + e.radius * std::cos(am),
|
||||
e.center.y() + e.radius * std::sin(am)));
|
||||
break;
|
||||
}
|
||||
case SketchEntity::Type::Circle: {
|
||||
offer(InferenceSnap::Kind::Center, ei, SketchPointRole::Center, e.center);
|
||||
// Nearest point on the circle rim (PointOnObject candidate).
|
||||
const Vec2d v = query - e.center;
|
||||
const double n = v.norm();
|
||||
if (n > 1e-9 && e.radius > 1e-9)
|
||||
offer(InferenceSnap::Kind::OnEdge, ei, SketchPointRole::Center,
|
||||
e.center + v * (e.radius / n));
|
||||
break;
|
||||
}
|
||||
case SketchEntity::Type::Point:
|
||||
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P0, e.p0);
|
||||
break;
|
||||
case SketchEntity::Type::EllipseArc:
|
||||
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P0, e.p0);
|
||||
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P1, e.p1);
|
||||
offer(InferenceSnap::Kind::Center, ei, SketchPointRole::Center, e.center);
|
||||
break;
|
||||
case SketchEntity::Type::Ellipse:
|
||||
offer(InferenceSnap::Kind::Center, ei, SketchPointRole::Center, e.center);
|
||||
break;
|
||||
case SketchEntity::Type::BSpline:
|
||||
// Endpoints (first/last pole) snap for loop closure.
|
||||
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P0, e.p0);
|
||||
offer(InferenceSnap::Kind::Endpoint, ei, SketchPointRole::P1, e.p1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (include_origin)
|
||||
offer(InferenceSnap::Kind::Origin, -1, SketchPointRole::P0, Vec2d(0, 0));
|
||||
|
||||
InferenceSnap r;
|
||||
r.kind = best.kind; r.entity = best.entity; r.role = best.role; r.point = best.point;
|
||||
return r;
|
||||
}
|
||||
|
||||
std::optional<SketchConstraintType>
|
||||
infer_axis_constraint(const Vec2d& anchor, const Vec2d& tip, double ang_tol_rad)
|
||||
{
|
||||
const Vec2d d = tip - anchor;
|
||||
if (d.squaredNorm() < 1e-12) return std::nullopt;
|
||||
const double ang = std::atan2(std::abs(d.y()), std::abs(d.x())); // 0=horizontal, pi/2=vertical
|
||||
if (ang <= ang_tol_rad) return SketchConstraintType::Horizontal;
|
||||
if (ang >= M_PI / 2.0 - ang_tol_rad) return SketchConstraintType::Vertical;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Unsigned angle between two (unnormalized) direction vectors, in [0, pi]. 0 = same
|
||||
// direction, pi = opposite, pi/2 = perpendicular. Inputs must be non-degenerate.
|
||||
// static: this is a file-local helper, not part of the module's interface -- at namespace
|
||||
// scope with external linkage it would be a link-time collision waiting to happen.
|
||||
static double unsigned_angle(const Vec2d& a, const Vec2d& b)
|
||||
{
|
||||
const double cross = a.x() * b.y() - a.y() * b.x();
|
||||
const double dot = a.x() * b.x() + a.y() * b.y();
|
||||
return std::atan2(std::abs(cross), dot);
|
||||
}
|
||||
|
||||
std::vector<SketchEntityConstraintDef>
|
||||
infer_relations(const std::vector<SketchEntity>& entities, int new_ei,
|
||||
double ang_tol_rad, double len_tol_frac)
|
||||
{
|
||||
std::vector<SketchEntityConstraintDef> out;
|
||||
if (new_ei <= 0 || new_ei >= int(entities.size())) return out;
|
||||
|
||||
// AT MOST ONE constraint per rule per new entity, not one per PAIR. Without this the
|
||||
// function is quadratic in the sketch: a drawing with 200 equal holes yields ~20000
|
||||
// EqualRadius candidates, the batch is rejected as over-constrained, and the caller's
|
||||
// one-at-a-time fallback then runs a solve per constraint. Measured 2026-08-31: that
|
||||
// pinned the app at 95% of a core with the MCP socket unresponsive -- the same failure
|
||||
// the axes batch above already carries a warning about. Keep the best candidate only.
|
||||
int best_ang_j = -1, best_rad_j = -1, best_tan_j = -1;
|
||||
double best_ang_err = 1e30, best_rad_err = 1e30, best_tan_err = 1e30;
|
||||
SketchConstraintType best_ang_type = SketchConstraintType::Parallel;
|
||||
|
||||
const SketchEntity& n = entities[new_ei];
|
||||
const bool n_line = n.type == SketchEntity::Type::Line;
|
||||
const bool n_curve = n.type == SketchEntity::Type::Arc || n.type == SketchEntity::Type::Circle;
|
||||
if (!n_line && !n_curve) return out; // not a Line / Arc / Circle
|
||||
if (n_line && (n.p1 - n.p0).squaredNorm() < 1e-18) return out; // degenerate
|
||||
if (n_curve && n.radius < 1e-9) return out;
|
||||
|
||||
for (int j = 0; j < new_ei; ++j) {
|
||||
const SketchEntity& o = entities[j];
|
||||
const bool o_line = o.type == SketchEntity::Type::Line;
|
||||
const bool o_curve = o.type == SketchEntity::Type::Arc || o.type == SketchEntity::Type::Circle;
|
||||
if (!o_line && !o_curve) continue;
|
||||
if (o_line && (o.p1 - o.p0).squaredNorm() < 1e-18) continue;
|
||||
if (o_curve && o.radius < 1e-9) continue;
|
||||
|
||||
if (n_line && o_line) {
|
||||
// R1 — parallel / perpendicular, restricted to CONNECTED lines. Connection is
|
||||
// what keeps this from firing on every distant line that is roughly parallel.
|
||||
const bool connected = (n.p0 - o.p0).squaredNorm() <= 1e-14 ||
|
||||
(n.p0 - o.p1).squaredNorm() <= 1e-14 ||
|
||||
(n.p1 - o.p0).squaredNorm() <= 1e-14 ||
|
||||
(n.p1 - o.p1).squaredNorm() <= 1e-14;
|
||||
if (!connected) continue;
|
||||
const double ang = unsigned_angle(n.p1 - n.p0, o.p1 - o.p0);
|
||||
const double par_err = std::min(ang, M_PI - ang);
|
||||
const double per_err = std::abs(ang - M_PI / 2.0);
|
||||
if (par_err <= ang_tol_rad && par_err < best_ang_err) {
|
||||
best_ang_err = par_err; best_ang_j = j;
|
||||
best_ang_type = SketchConstraintType::Parallel;
|
||||
} else if (per_err <= ang_tol_rad && per_err < best_ang_err) {
|
||||
best_ang_err = per_err; best_ang_j = j;
|
||||
best_ang_type = SketchConstraintType::Perpendicular;
|
||||
}
|
||||
} else if (n_curve && o_curve) {
|
||||
// R2 — equal radius between circles / arcs, relative to the larger.
|
||||
const double larger = n.radius > o.radius ? n.radius : o.radius;
|
||||
const double err = std::abs(n.radius - o.radius) / larger;
|
||||
if (err <= len_tol_frac && err < best_rad_err) { best_rad_err = err; best_rad_j = j; }
|
||||
} else {
|
||||
// R3 — tangent where a line meets a circle / arc at a shared endpoint, and only
|
||||
// when the line is ALREADY perpendicular to the radius at that point.
|
||||
const SketchEntity& ln = n_line ? n : o;
|
||||
const SketchEntity& cv = n_line ? o : n;
|
||||
const Vec2d ldir = ln.p1 - ln.p0;
|
||||
bool tangent = false;
|
||||
const Vec2d le[2] = { ln.p0, ln.p1 };
|
||||
for (int k = 0; k < 2 && !tangent; ++k) {
|
||||
if (cv.type == SketchEntity::Type::Arc) {
|
||||
const Vec2d ce[2] = { cv.p0, cv.p1 };
|
||||
for (int m = 0; m < 2; ++m) {
|
||||
if ((le[k] - ce[m]).squaredNorm() > 1e-14) continue;
|
||||
const Vec2d r = ce[m] - cv.center;
|
||||
if (r.squaredNorm() < 1e-18) continue;
|
||||
tangent = std::abs(unsigned_angle(ldir, r) - M_PI / 2.0) <= ang_tol_rad;
|
||||
if (tangent) break;
|
||||
}
|
||||
} else { // Circle: shared point is a line endpoint on the rim.
|
||||
const Vec2d r = le[k] - cv.center;
|
||||
if (std::abs(r.norm() - cv.radius) > 1e-7) continue;
|
||||
if (r.squaredNorm() < 1e-18) continue;
|
||||
tangent = std::abs(unsigned_angle(ldir, r) - M_PI / 2.0) <= ang_tol_rad;
|
||||
}
|
||||
}
|
||||
if (tangent && best_tan_err > 0.0) { best_tan_err = 0.0; best_tan_j = j; }
|
||||
}
|
||||
}
|
||||
|
||||
auto emit = [&](SketchConstraintType t, int j) {
|
||||
if (j < 0) return;
|
||||
SketchEntityConstraintDef c;
|
||||
c.type = t; c.ea = j; c.eb = new_ei;
|
||||
out.push_back(c);
|
||||
};
|
||||
emit(best_ang_type, best_ang_j); // R1
|
||||
emit(SketchConstraintType::EqualRadius, best_rad_j); // R2
|
||||
emit(SketchConstraintType::Tangent, best_tan_j); // R3
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,55 @@
|
||||
#ifndef slic3r_SketchInference_hpp_
|
||||
#define slic3r_SketchInference_hpp_
|
||||
|
||||
#include "libslic3r/CAD/SketchEngine.hpp"
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <cmath>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Result of snapping a free cursor point onto the most relevant inference target
|
||||
// among the committed sketch entities and the sketch origin. This is the backbone
|
||||
// that lets geometry self-constrain as it is drawn: the GUI records the returned
|
||||
// target at click time and, once the entity it belongs to exists, emits the
|
||||
// matching constraint (Coincident onto an endpoint/centre, Fix onto the origin,
|
||||
// PointOnObject onto an edge) so the relation survives a re-solve.
|
||||
struct InferenceSnap {
|
||||
enum class Kind { None, Endpoint, Center, Midpoint, OnEdge, Origin };
|
||||
Kind kind{Kind::None};
|
||||
int entity{-1}; // hit entity index (-1 = origin/none)
|
||||
SketchPointRole role{SketchPointRole::P0}; // which point of `entity` (Endpoint/Center)
|
||||
Vec2d point{0, 0}; // snapped coordinate (== query when None)
|
||||
|
||||
bool snapped() const { return kind != Kind::None; }
|
||||
};
|
||||
|
||||
// Snap `query` onto the best inference target within `tol` plane units. Priority,
|
||||
// highest first: Endpoint, Center, Origin, Midpoint, OnEdge. Construction entities
|
||||
// participate (you constrain to them too). Returns {None, query} when nothing is in
|
||||
// range. Pure — no GUI / GL dependencies, so it is unit-testable in libslic3r.
|
||||
InferenceSnap infer_point_snap(const std::vector<SketchEntity>& entities,
|
||||
const Vec2d& query, double tol,
|
||||
bool include_origin = true);
|
||||
|
||||
// Relational inference for an in-progress segment anchor->tip. If its direction is
|
||||
// within `ang_tol_rad` of an axis, returns Horizontal or Vertical (the constraint to
|
||||
// auto-emit on the committed segment); std::nullopt otherwise. Degenerate (near-zero
|
||||
// length) segments return nullopt.
|
||||
std::optional<SketchConstraintType>
|
||||
infer_axis_constraint(const Vec2d& anchor, const Vec2d& tip, double ang_tol_rad = 3.0 * M_PI / 180.0);
|
||||
|
||||
// Relational constraints to auto-emit for a newly drawn entity `new_ei` against the
|
||||
// entities already in the sketch. Pure, no GUI/GL dependencies, unit-testable.
|
||||
//
|
||||
// Deliberately conservative: every rule requires the relation to be ALREADY TRUE within
|
||||
// tolerance, so an inferred constraint never moves geometry the user drew — it only pins a
|
||||
// relation that is visibly there. Returns an empty vector when nothing qualifies.
|
||||
std::vector<SketchEntityConstraintDef>
|
||||
infer_relations(const std::vector<SketchEntity>& entities, int new_ei,
|
||||
double ang_tol_rad = 2.0 * M_PI / 180.0,
|
||||
double len_tol_frac = 0.01);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_SketchInference_hpp_
|
||||
@@ -0,0 +1,555 @@
|
||||
#include "libslic3r/CAD/SketchSolver.hpp"
|
||||
|
||||
#include <slvs.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
using CT = SketchConstraintType;
|
||||
using Role = SketchPointRole;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr Slvs_hGroup G_FIXED = 1; // workplane / reference: held constant
|
||||
constexpr Slvs_hGroup G_SK = 2; // sketch geometry: the group we solve
|
||||
|
||||
// Per-entity slvs handles. p0/p1/center are point2d entity handles; prim is the
|
||||
// line/arc/circle entity; rparam is the circle radius param.
|
||||
struct Slots {
|
||||
Slvs_hEntity prim{0}, p0{0}, p1{0}, center{0};
|
||||
Slvs_hParam rparam{0};
|
||||
std::vector<Slvs_hEntity> pts; // BSpline control points (point2d handles)
|
||||
};
|
||||
|
||||
struct Build {
|
||||
std::vector<Slvs_Param> params;
|
||||
std::vector<Slvs_Entity> ents;
|
||||
std::vector<Slvs_Constraint> cons;
|
||||
Slvs_hParam ph{0};
|
||||
Slvs_hEntity eh{0};
|
||||
Slvs_hConstraint ch{0};
|
||||
Slvs_hEntity wp{0}, normal{0};
|
||||
|
||||
Slvs_hParam P(Slvs_hGroup g, double v) { params.push_back(Slvs_MakeParam(++ph, g, v)); return ph; }
|
||||
Slvs_hEntity E(Slvs_Entity e) { ents.push_back(e); return e.h; }
|
||||
Slvs_hEntity pt2d(Slvs_hGroup g, double u, double v)
|
||||
{ return E(Slvs_MakePoint2d(++eh, g, wp, P(g, u), P(g, v))); }
|
||||
|
||||
// Generic constraint (entityC unused by Slvs_MakeConstraint — set it manually below).
|
||||
void C(int type, double val, Slvs_hEntity ptA, Slvs_hEntity ptB,
|
||||
Slvs_hEntity eA, Slvs_hEntity eB, Slvs_hEntity eC = 0, int other = 0)
|
||||
{
|
||||
Slvs_Constraint c = Slvs_MakeConstraint(++ch, G_SK, type, wp, val, ptA, ptB, eA, eB);
|
||||
c.entityC = eC;
|
||||
c.other = other;
|
||||
cons.push_back(c);
|
||||
}
|
||||
};
|
||||
|
||||
inline int role_idx(Role r) { return int(r); }
|
||||
|
||||
} // namespace
|
||||
|
||||
static SketchSolveResult solve_system(std::vector<SketchEntity>& entities,
|
||||
const std::vector<SketchEntityConstraintDef>& constraints,
|
||||
int dragged_ei, Role dragged_role)
|
||||
{
|
||||
SketchSolveResult out;
|
||||
if (constraints.empty()) { out.ok = true; out.dof = -1; return out; }
|
||||
|
||||
Build b;
|
||||
|
||||
// ---- Fixed 2D XY workplane (origin at 0,0,0; identity normal) -------------------
|
||||
Slvs_hEntity origin = b.E(Slvs_MakePoint3d(++b.eh, G_FIXED,
|
||||
b.P(G_FIXED, 0.0), b.P(G_FIXED, 0.0), b.P(G_FIXED, 0.0)));
|
||||
double qw, qx, qy, qz;
|
||||
Slvs_MakeQuaternion(1, 0, 0, 0, 1, 0, &qw, &qx, &qy, &qz);
|
||||
b.normal = b.E(Slvs_MakeNormal3d(++b.eh, G_FIXED,
|
||||
b.P(G_FIXED, qw), b.P(G_FIXED, qx), b.P(G_FIXED, qy), b.P(G_FIXED, qz)));
|
||||
b.wp = b.E(Slvs_MakeWorkplane(++b.eh, G_FIXED, origin, b.normal));
|
||||
|
||||
// Unit direction references for the axis-projected distance constraints. Both live in
|
||||
// G_FIXED, so they are held constant and add no DOF to the system.
|
||||
// libslvs defines a LINE_SEGMENT's direction as point[0] - point[1] (entity.cpp
|
||||
// VectorGetExprs), so the unit vector's head is listed first to yield +X / +Y.
|
||||
const Slvs_hEntity dir_x [[maybe_unused]] = b.E(Slvs_MakeLineSegment(++b.eh, G_FIXED, b.wp,
|
||||
b.pt2d(G_FIXED, 1.0, 0.0), b.pt2d(G_FIXED, 0.0, 0.0)));
|
||||
const Slvs_hEntity dir_y [[maybe_unused]] = b.E(Slvs_MakeLineSegment(++b.eh, G_FIXED, b.wp,
|
||||
b.pt2d(G_FIXED, 0.0, 1.0), b.pt2d(G_FIXED, 0.0, 0.0)));
|
||||
|
||||
// Implicit sketch references (origin, X axis, Y axis), addressable by the negative
|
||||
// sentinels in SketchEngine.hpp. G_FIXED: held constant, zero added DOF. The axis lines
|
||||
// are built head-first so their direction reads +X / +Y, matching dir_x / dir_y.
|
||||
const Slvs_hEntity ref_origin_pt = b.pt2d(G_FIXED, 0.0, 0.0);
|
||||
const Slvs_hEntity ref_axis_x = b.E(Slvs_MakeLineSegment(++b.eh, G_FIXED, b.wp,
|
||||
b.pt2d(G_FIXED, 1.0, 0.0), ref_origin_pt));
|
||||
const Slvs_hEntity ref_axis_y = b.E(Slvs_MakeLineSegment(++b.eh, G_FIXED, b.wp,
|
||||
b.pt2d(G_FIXED, 0.0, 1.0), ref_origin_pt));
|
||||
|
||||
// ---- Entities -------------------------------------------------------------------
|
||||
std::vector<Slots> slot(entities.size());
|
||||
for (size_t i = 0; i < entities.size(); ++i) {
|
||||
const SketchEntity& e = entities[i];
|
||||
Slots s;
|
||||
switch (e.type) {
|
||||
case SketchEntity::Type::Line:
|
||||
s.p0 = b.pt2d(G_SK, e.p0.x(), e.p0.y());
|
||||
s.p1 = b.pt2d(G_SK, e.p1.x(), e.p1.y());
|
||||
s.prim = b.E(Slvs_MakeLineSegment(++b.eh, G_SK, b.wp, s.p0, s.p1));
|
||||
break;
|
||||
case SketchEntity::Type::Point:
|
||||
s.p0 = b.pt2d(G_SK, e.p0.x(), e.p0.y());
|
||||
break;
|
||||
case SketchEntity::Type::Circle: {
|
||||
s.center = b.pt2d(G_SK, e.center.x(), e.center.y());
|
||||
s.p0 = s.center; // p0 mirrors centre for circles
|
||||
s.rparam = b.P(G_SK, e.radius > 1e-9 ? e.radius : 1.0);
|
||||
Slvs_hEntity dist = b.E(Slvs_MakeDistance(++b.eh, G_SK, b.wp, s.rparam));
|
||||
s.prim = b.E(Slvs_MakeCircle(++b.eh, G_SK, b.wp, s.center, b.normal, dist));
|
||||
break;
|
||||
}
|
||||
case SketchEntity::Type::Arc:
|
||||
s.center = b.pt2d(G_SK, e.center.x(), e.center.y());
|
||||
s.p0 = b.pt2d(G_SK, e.p0.x(), e.p0.y()); // start
|
||||
s.p1 = b.pt2d(G_SK, e.p1.x(), e.p1.y()); // end
|
||||
s.prim = b.E(Slvs_MakeArcOfCircle(++b.eh, G_SK, b.wp, b.normal, s.center, s.p0, s.p1));
|
||||
break;
|
||||
// libslvs has no conic entity (scope note): register the ellipse's defining
|
||||
// points only (center + arc endpoints) so center/endpoint constraints solve;
|
||||
// the a/b/phi shape params pass through unsolved.
|
||||
case SketchEntity::Type::Ellipse:
|
||||
s.center = b.pt2d(G_SK, e.center.x(), e.center.y());
|
||||
s.p0 = s.center; // p0 mirrors centre (circle convention)
|
||||
break;
|
||||
case SketchEntity::Type::EllipseArc:
|
||||
s.center = b.pt2d(G_SK, e.center.x(), e.center.y());
|
||||
s.p0 = b.pt2d(G_SK, e.p0.x(), e.p0.y()); // start
|
||||
s.p1 = b.pt2d(G_SK, e.p1.x(), e.p1.y()); // end
|
||||
break;
|
||||
// No native slvs curve for an arbitrary-degree spline: register the control
|
||||
// poles as point2d so endpoints (and any pole-targeted constraint) solve. The
|
||||
// OCCT curve is rebuilt from the solved poles. p0/p1 mirror first/last pole so
|
||||
// Coincident at the spline ends closes loops just like a Line.
|
||||
case SketchEntity::Type::BSpline:
|
||||
s.pts.reserve(e.ctrl.size());
|
||||
for (const Vec2d& cp : e.ctrl)
|
||||
s.pts.push_back(b.pt2d(G_SK, cp.x(), cp.y()));
|
||||
if (!s.pts.empty()) { s.p0 = s.pts.front(); s.p1 = s.pts.back(); }
|
||||
break;
|
||||
}
|
||||
slot[i] = s;
|
||||
}
|
||||
|
||||
auto valid = [&](int ei) { return ei >= 0 && ei < int(entities.size()); };
|
||||
auto ptOf = [&](int ei, Role r) -> Slvs_hEntity {
|
||||
if (ei == kSketchRefOrigin) return ref_origin_pt;
|
||||
if (ei == kSketchRefAxisX || ei == kSketchRefAxisY) return ref_origin_pt; // axes pass through it
|
||||
if (!valid(ei)) return 0;
|
||||
const Slots& s = slot[ei];
|
||||
switch (r) {
|
||||
case Role::P0: return s.p0;
|
||||
case Role::P1: return s.p1;
|
||||
case Role::Center: return s.center ? s.center : s.p0;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
auto primOf = [&](int ei) -> Slvs_hEntity {
|
||||
if (ei == kSketchRefAxisX) return ref_axis_x;
|
||||
if (ei == kSketchRefAxisY) return ref_axis_y;
|
||||
return valid(ei) ? slot[ei].prim : 0; // origin has no prim: it is a point
|
||||
};
|
||||
auto coordOf = [&](int ei, Role r) -> Vec2d {
|
||||
if (is_sketch_ref(ei)) return Vec2d(0, 0); // all three pass through the origin
|
||||
if (!valid(ei)) return Vec2d(0, 0);
|
||||
const SketchEntity& e = entities[ei];
|
||||
switch (r) { case Role::P0: return e.p0; case Role::P1: return e.p1; case Role::Center: return e.center; }
|
||||
return e.p0;
|
||||
};
|
||||
// A fixed reference point at (x,y) — used to pin coordinates (Fix / LockX / LockY).
|
||||
auto fixedRef = [&](double x, double y) -> Slvs_hEntity { return b.pt2d(G_FIXED, x, y); };
|
||||
|
||||
// ---- Constraints ----------------------------------------------------------------
|
||||
for (const auto& c : constraints) {
|
||||
// Robustness: never feed libslvs a null handle. A constraint that references an
|
||||
// entity which produced no solver primitive (Point/Ellipse/EllipseArc/BSpline get
|
||||
// no `prim`) or no point for the requested role would make Slvs FindById abort the
|
||||
// whole process. Skip such a constraint instead of crashing.
|
||||
bool ref_ok = true;
|
||||
switch (c.type) {
|
||||
case CT::Coincident: case CT::Horizontal: case CT::Vertical: case CT::Distance:
|
||||
ref_ok = ptOf(c.ea, c.ra) && ptOf(c.eb, c.rb); break;
|
||||
case CT::DistanceX:
|
||||
case CT::DistanceY:
|
||||
ref_ok = ptOf(c.ea, c.ra) && ptOf(c.eb, c.rb); break;
|
||||
case CT::Concentric:
|
||||
ref_ok = ptOf(c.ea, Role::Center) && ptOf(c.eb, Role::Center); break;
|
||||
case CT::Fix: case CT::LockX: case CT::LockY:
|
||||
ref_ok = ptOf(c.ea, c.ra) != 0; break;
|
||||
case CT::EqualLength: case CT::Parallel: case CT::Perpendicular:
|
||||
case CT::Angle: case CT::Tangent:
|
||||
ref_ok = primOf(c.ea) && primOf(c.eb); break;
|
||||
case CT::Radius: case CT::Diameter:
|
||||
ref_ok = primOf(c.ea) != 0; break;
|
||||
case CT::Midpoint:
|
||||
ref_ok = ptOf(c.ea, c.ra) && primOf(c.eb); break;
|
||||
case CT::Symmetric:
|
||||
ref_ok = ptOf(c.ea, c.ra) && ptOf(c.eb, c.rb) && primOf(c.ec); break;
|
||||
case CT::SymmetricAboutY: case CT::SymmetricAboutX:
|
||||
ref_ok = ptOf(c.ea, c.ra) && ptOf(c.eb, c.rb); break;
|
||||
case CT::PointOnLine: case CT::PointOnObject:
|
||||
ref_ok = ptOf(c.ea, c.ra) && primOf(c.eb); break;
|
||||
case CT::EqualRadius:
|
||||
case CT::Collinear:
|
||||
ref_ok = primOf(c.ea) && primOf(c.eb); break;
|
||||
}
|
||||
if (!ref_ok) continue;
|
||||
switch (c.type) {
|
||||
case CT::Coincident:
|
||||
b.C(SLVS_C_POINTS_COINCIDENT, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), 0, 0);
|
||||
break;
|
||||
case CT::Concentric:
|
||||
b.C(SLVS_C_POINTS_COINCIDENT, 0, ptOf(c.ea, Role::Center), ptOf(c.eb, Role::Center), 0, 0);
|
||||
break;
|
||||
case CT::Horizontal:
|
||||
b.C(SLVS_C_HORIZONTAL, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), 0, 0);
|
||||
break;
|
||||
case CT::Vertical:
|
||||
b.C(SLVS_C_VERTICAL, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), 0, 0);
|
||||
break;
|
||||
case CT::Distance:
|
||||
b.C(SLVS_C_PT_PT_DISTANCE, c.value, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), 0, 0);
|
||||
break;
|
||||
case CT::DistanceX:
|
||||
// Distance between the two points measured along X only: project the vector
|
||||
// between them onto the fixed unit X direction.
|
||||
b.C(SLVS_C_PROJ_PT_DISTANCE, c.value, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), dir_x, 0);
|
||||
break;
|
||||
case CT::DistanceY:
|
||||
b.C(SLVS_C_PROJ_PT_DISTANCE, c.value, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), dir_y, 0);
|
||||
break;
|
||||
case CT::Fix: {
|
||||
const Vec2d p = coordOf(c.ea, c.ra);
|
||||
b.C(SLVS_C_POINTS_COINCIDENT, 0, ptOf(c.ea, c.ra), fixedRef(p.x(), p.y()), 0, 0);
|
||||
break;
|
||||
}
|
||||
case CT::LockX: {
|
||||
const Vec2d p = coordOf(c.ea, c.ra);
|
||||
b.C(SLVS_C_VERTICAL, 0, ptOf(c.ea, c.ra), fixedRef(c.value, p.y()), 0, 0);
|
||||
break;
|
||||
}
|
||||
case CT::LockY: {
|
||||
const Vec2d p = coordOf(c.ea, c.ra);
|
||||
b.C(SLVS_C_HORIZONTAL, 0, ptOf(c.ea, c.ra), fixedRef(p.x(), c.value), 0, 0);
|
||||
break;
|
||||
}
|
||||
case CT::EqualLength:
|
||||
b.C(SLVS_C_EQUAL_LENGTH_LINES, 0, 0, 0, primOf(c.ea), primOf(c.eb));
|
||||
break;
|
||||
case CT::Parallel:
|
||||
b.C(SLVS_C_PARALLEL, 0, 0, 0, primOf(c.ea), primOf(c.eb));
|
||||
break;
|
||||
case CT::Perpendicular:
|
||||
b.C(SLVS_C_PERPENDICULAR, 0, 0, 0, primOf(c.ea), primOf(c.eb));
|
||||
break;
|
||||
case CT::Midpoint:
|
||||
b.C(SLVS_C_AT_MIDPOINT, 0, ptOf(c.ea, c.ra), 0, primOf(c.eb), 0);
|
||||
break;
|
||||
case CT::Symmetric:
|
||||
// ptA, ptB symmetric about the axis line (ec).
|
||||
b.C(SLVS_C_SYMMETRIC_LINE, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), primOf(c.ec), 0);
|
||||
break;
|
||||
case CT::SymmetricAboutY:
|
||||
b.C(SLVS_C_SYMMETRIC_LINE, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), primOf(kSketchRefAxisY), 0);
|
||||
break;
|
||||
case CT::SymmetricAboutX:
|
||||
b.C(SLVS_C_SYMMETRIC_LINE, 0, ptOf(c.ea, c.ra), ptOf(c.eb, c.rb), primOf(kSketchRefAxisX), 0);
|
||||
break;
|
||||
case CT::Angle:
|
||||
// model stores radians; slvs angle is in degrees.
|
||||
b.C(SLVS_C_ANGLE, c.value * 180.0 / M_PI, 0, 0, primOf(c.ea), primOf(c.eb));
|
||||
break;
|
||||
case CT::Radius:
|
||||
b.C(SLVS_C_DIAMETER, 2.0 * c.value, 0, 0, primOf(c.ea), 0);
|
||||
break;
|
||||
case CT::Diameter:
|
||||
b.C(SLVS_C_DIAMETER, c.value, 0, 0, primOf(c.ea), 0);
|
||||
break;
|
||||
case CT::Tangent: {
|
||||
const bool aCurve = valid(c.ea) && entities[c.ea].type != SketchEntity::Type::Line;
|
||||
const bool bCurve = valid(c.eb) && entities[c.eb].type != SketchEntity::Type::Line;
|
||||
if (aCurve && bCurve)
|
||||
b.C(SLVS_C_CURVE_CURVE_TANGENT, 0, 0, 0, primOf(c.ea), primOf(c.eb));
|
||||
else {
|
||||
const int ci = aCurve ? c.ea : c.eb; // the curve
|
||||
const int li = aCurve ? c.eb : c.ea; // the line
|
||||
if (valid(ci) && entities[ci].type == SketchEntity::Type::Circle) {
|
||||
// A FULL circle cannot use SLVS_C_ARC_LINE_TANGENT. That constraint reads
|
||||
// arc->point[1] / point[2] — the arc's endpoints (see constrainteq.cpp,
|
||||
// Type::ARC_LINE_TANGENT) — and a circle entity only has point[0], its
|
||||
// centre. The zero handles send FindById into "Cannot find handle", which
|
||||
// ABORTS the process rather than failing the solve, taking every later test
|
||||
// with it. It is also the wrong equation for a circle: it only makes the
|
||||
// line perpendicular to the radius AT AN ENDPOINT that does not exist.
|
||||
//
|
||||
// For a circle, tangency is exactly "the centre sits one radius away from
|
||||
// the line", which slvs expresses directly.
|
||||
//
|
||||
// ponytail: the radius is captured here rather than tied as a variable —
|
||||
// the C API takes a constant distance and offers no way to reference the
|
||||
// circle's radius parameter. Exact whenever the radius is fixed or simply
|
||||
// not being changed by another constraint in the same solve; if some other
|
||||
// constraint drives the radius, re-solving restores tangency. Tying them
|
||||
// would need an auxiliary point constrained onto both circle and line.
|
||||
b.C(SLVS_C_PT_LINE_DISTANCE, entities[ci].radius,
|
||||
ptOf(ci, Role::Center), 0, primOf(li), 0);
|
||||
} else {
|
||||
b.C(SLVS_C_ARC_LINE_TANGENT, 0, 0, 0, primOf(ci), primOf(li));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CT::PointOnLine:
|
||||
if (std::abs(c.value) < 1e-9)
|
||||
b.C(SLVS_C_PT_ON_LINE, 0, ptOf(c.ea, c.ra), 0, primOf(c.eb), 0);
|
||||
else
|
||||
b.C(SLVS_C_PT_LINE_DISTANCE, std::abs(c.value), ptOf(c.ea, c.ra), 0, primOf(c.eb), 0);
|
||||
break;
|
||||
case CT::PointOnObject:
|
||||
// Point (ea,ra) lies on entity edge eb: a circle rim -> PT_ON_CIRCLE,
|
||||
// otherwise the segment line -> PT_ON_LINE.
|
||||
if (valid(c.eb) && entities[c.eb].type == SketchEntity::Type::Circle)
|
||||
b.C(SLVS_C_PT_ON_CIRCLE, 0, ptOf(c.ea, c.ra), 0, primOf(c.eb), 0);
|
||||
else
|
||||
b.C(SLVS_C_PT_ON_LINE, 0, ptOf(c.ea, c.ra), 0, primOf(c.eb), 0);
|
||||
break;
|
||||
case CT::EqualRadius:
|
||||
b.C(SLVS_C_EQUAL_RADIUS, 0, 0, 0, primOf(c.ea), primOf(c.eb));
|
||||
break;
|
||||
case CT::Collinear:
|
||||
// libslvs has no collinear code. Two lines are collinear iff they are parallel
|
||||
// AND a point of one lies on the other's infinite line — emit both.
|
||||
b.C(SLVS_C_PARALLEL, 0, 0, 0, primOf(c.ea), primOf(c.eb));
|
||||
// Point-on-infinite-line via PT_LINE_DISTANCE=0 rather than PT_ON_LINE: the
|
||||
// latter creates an internal `valP` param that this port's Slvs_Solve leaves at
|
||||
// 0 in the working set (ModifyToSatisfy only updates SK.param), so an already
|
||||
// collinear pair drifts. PT_LINE_DISTANCE=0 is the same condition with no extra
|
||||
// parameter, so an already-satisfied solve is a clean no-op.
|
||||
b.C(SLVS_C_PT_LINE_DISTANCE, 0, ptOf(c.eb, Role::P0), 0, primOf(c.ea), 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Solve ----------------------------------------------------------------------
|
||||
Slvs_System sys;
|
||||
std::memset(&sys, 0, sizeof(sys));
|
||||
sys.param = b.params.data(); sys.params = int(b.params.size());
|
||||
sys.entity = b.ents.data(); sys.entities = int(b.ents.size());
|
||||
sys.constraint = b.cons.data(); sys.constraints = int(b.cons.size());
|
||||
std::vector<Slvs_hConstraint> failed(b.cons.size() + 1, 0);
|
||||
sys.failed = failed.data();
|
||||
sys.faileds = int(failed.size());
|
||||
sys.calculateFaileds = 1;
|
||||
|
||||
// Drag pin: feed the dragged point's two params into sys.dragged[] so the solver
|
||||
// favours keeping that point at the cursor and re-solves the rest around it.
|
||||
if (dragged_ei >= 0) {
|
||||
const Slvs_hEntity h = ptOf(dragged_ei, dragged_role);
|
||||
for (const Slvs_Entity& en : b.ents)
|
||||
if (en.h == h) { sys.dragged[0] = en.param[0]; sys.dragged[1] = en.param[1]; break; }
|
||||
}
|
||||
|
||||
Slvs_Solve(&sys, G_SK);
|
||||
|
||||
out.result = sys.result;
|
||||
out.dof = sys.dof;
|
||||
out.ok = (sys.result == SLVS_RESULT_OKAY);
|
||||
|
||||
// Map solved param handles -> values, then read points back.
|
||||
std::unordered_map<Slvs_hParam, double> pv;
|
||||
pv.reserve(sys.params * 2);
|
||||
for (int i = 0; i < sys.params; ++i) pv[sys.param[i].h] = sys.param[i].val;
|
||||
std::unordered_map<Slvs_hEntity, const Slvs_Entity*> byH;
|
||||
byH.reserve(sys.entities * 2);
|
||||
for (int i = 0; i < sys.entities; ++i) byH[sys.entity[i].h] = &sys.entity[i];
|
||||
auto coord = [&](Slvs_hEntity h) -> Vec2d {
|
||||
auto it = byH.find(h);
|
||||
if (it == byH.end()) return Vec2d(0, 0);
|
||||
return Vec2d(pv[it->second->param[0]], pv[it->second->param[1]]);
|
||||
};
|
||||
|
||||
// Map failed constraint handles back to indices into `constraints`.
|
||||
if (!out.ok && sys.faileds > 0) {
|
||||
std::unordered_map<Slvs_hConstraint, int> chToIdx;
|
||||
// constraint handles were assigned in order starting after the fixed group; the
|
||||
// i-th sketch constraint in b.cons has handle = its position. Rebuild by scanning.
|
||||
for (size_t k = 0; k < b.cons.size(); ++k) chToIdx[b.cons[k].h] = int(k);
|
||||
for (int i = 0; i < sys.faileds; ++i) {
|
||||
auto it = chToIdx.find(failed[i]);
|
||||
if (it != chToIdx.end() && it->second < int(constraints.size()))
|
||||
out.bad.push_back(it->second);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Read solved geometry back --------------------------------------------------
|
||||
// ONLY on success. A failed solve leaves libslvs' params holding its last Newton
|
||||
// iterate — geometry that satisfies nothing and is usually wildly deformed. Writing
|
||||
// that back made every rejected attempt destructive: the caller rolls the constraints
|
||||
// back, but the sketch it rolls back to is already wreckage, so the next attempt starts
|
||||
// from the corpse. The fillet degrade ladder hit this on every corner — rung 1 (a
|
||||
// tangent on each leg) is legitimately over-constrained against the legs' own H/V, and
|
||||
// its wreckage then failed rungs 2 and 3, which solve cleanly on their own. The arc
|
||||
// ended up with no constraints at all and the solver snapped the corner shut. pl5.
|
||||
if (!out.ok) return out;
|
||||
for (size_t i = 0; i < entities.size(); ++i) {
|
||||
SketchEntity& e = entities[i];
|
||||
const Slots& s = slot[i];
|
||||
if (s.p0) e.p0 = coord(s.p0);
|
||||
if (s.p1) e.p1 = coord(s.p1);
|
||||
if (s.center) e.center = coord(s.center);
|
||||
|
||||
if (e.type == SketchEntity::Type::BSpline) {
|
||||
for (size_t k = 0; k < s.pts.size() && k < e.ctrl.size(); ++k)
|
||||
e.ctrl[k] = coord(s.pts[k]);
|
||||
if (!e.ctrl.empty()) { e.p0 = e.ctrl.front(); e.p1 = e.ctrl.back(); }
|
||||
} else if (e.type == SketchEntity::Type::Circle) {
|
||||
if (s.rparam) { auto it = pv.find(s.rparam); if (it != pv.end()) e.radius = it->second; }
|
||||
e.p0 = e.center;
|
||||
} else if (e.type == SketchEntity::Type::Arc && s.center) {
|
||||
// Reflow arc angles from solved centre + endpoints, preserving sweep sign.
|
||||
const double old_sweep = e.end_angle - e.start_angle;
|
||||
const double ns = std::atan2(e.p0.y() - e.center.y(), e.p0.x() - e.center.x());
|
||||
const double ne = std::atan2(e.p1.y() - e.center.y(), e.p1.x() - e.center.x());
|
||||
double sweep = ne - ns;
|
||||
const double TWO_PI = 2.0 * M_PI;
|
||||
while (sweep <= -TWO_PI) sweep += TWO_PI;
|
||||
while (sweep >= TWO_PI) sweep -= TWO_PI;
|
||||
if (old_sweep >= 0.0 && sweep < 0.0) sweep += TWO_PI;
|
||||
if (old_sweep < 0.0 && sweep > 0.0) sweep -= TWO_PI;
|
||||
e.start_angle = ns;
|
||||
e.end_angle = ns + sweep;
|
||||
e.radius = 0.5 * ((e.p0 - e.center).norm() + (e.p1 - e.center).norm());
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// libslvs carries a COMPILE-TIME ceiling: solvespace.h declares `enum { MAX_UNKNOWNS = 1024 }`
|
||||
// and sizes the System's param and equation arrays with it. solve_system() hands the solver every
|
||||
// entity in the sketch, constrained or not, at 2 params per point — so a sketch of about 480 lines
|
||||
// is the last one that fits, and the very next one comes back TOO_MANY_UNKNOWNS.
|
||||
//
|
||||
// What that did, before this: DesignSketchTool::try_add_constraints rolls the whole batch back
|
||||
// when the solve fails, so the auto-constraint pass over a large sketch dropped EVERY constraint
|
||||
// it had just inferred. Measured on the rig — 480 lines: 960 constraints, dof 480. 520 lines:
|
||||
// 0 constraints, dof unknown. Nothing was said, and from there on no dimension and no constraint
|
||||
// could ever be applied to that sketch, because each attempt re-solved the same oversized system
|
||||
// and was rejected in turn. A typed length simply did nothing.
|
||||
//
|
||||
// Constraints only couple entities that SHARE a point, so a sketch is naturally a set of
|
||||
// independent systems — a plate with 300 cut-outs is 301 little problems, not one big one.
|
||||
// Solving them separately keeps every one of them far under the ceiling AND is faster, since the
|
||||
// solver's work is superlinear in system size.
|
||||
//
|
||||
// The whole system is still tried FIRST, and this runs only on TOO_MANY_UNKNOWNS, so every sketch
|
||||
// that fits today keeps its exact current behaviour, including its reported degrees of freedom.
|
||||
// A genuinely over-constrained sketch still fails: the conflict lives inside one component and
|
||||
// that component still rejects it.
|
||||
static SketchSolveResult solve_partitioned(std::vector<SketchEntity>& entities,
|
||||
const std::vector<SketchEntityConstraintDef>& constraints,
|
||||
int dragged_ei, Role dragged_role)
|
||||
{
|
||||
const int n = int(entities.size());
|
||||
std::vector<int> parent(n);
|
||||
for (int i = 0; i < n; ++i) parent[i] = i;
|
||||
std::function<int(int)> find = [&](int a) {
|
||||
while (parent[a] != a) { parent[a] = parent[parent[a]]; a = parent[a]; }
|
||||
return a;
|
||||
};
|
||||
auto unite = [&](int a, int b) {
|
||||
if (a < 0 || b < 0 || a >= n || b >= n) return;
|
||||
a = find(a); b = find(b);
|
||||
if (a != b) parent[a] = b;
|
||||
};
|
||||
for (const auto& c : constraints) { unite(c.ea, c.eb); unite(c.ea, c.ec); }
|
||||
|
||||
// Group the constraints by the component they belong to.
|
||||
std::map<int, std::vector<int>> groups;
|
||||
for (size_t i = 0; i < constraints.size(); ++i) {
|
||||
const int a = constraints[i].ea;
|
||||
if (a < 0 || a >= n) continue;
|
||||
groups[find(a)].push_back(int(i));
|
||||
}
|
||||
|
||||
SketchSolveResult out;
|
||||
out.ok = true;
|
||||
out.dof = 0;
|
||||
// Solve into COPIES and commit only if every component succeeded. The contract callers rely
|
||||
// on is all-or-nothing — try_add_constraints rolls the batch back and expects the geometry it
|
||||
// rolls back to be untouched — and partial writes would break it.
|
||||
std::vector<std::pair<std::vector<int>, std::vector<SketchEntity>>> solved;
|
||||
for (const auto& [root, cidx] : groups) {
|
||||
std::vector<int> ents; // global indices, in order
|
||||
std::map<int, int> local; // global -> local
|
||||
auto take = [&](int e) {
|
||||
if (e < 0 || e >= n || local.count(e)) return;
|
||||
local[e] = int(ents.size());
|
||||
ents.push_back(e);
|
||||
};
|
||||
for (int ci : cidx) { take(constraints[ci].ea); take(constraints[ci].eb); take(constraints[ci].ec); }
|
||||
std::vector<SketchEntity> sub;
|
||||
sub.reserve(ents.size());
|
||||
for (int e : ents) sub.push_back(entities[e]);
|
||||
std::vector<SketchEntityConstraintDef> subc;
|
||||
subc.reserve(cidx.size());
|
||||
for (int ci : cidx) {
|
||||
SketchEntityConstraintDef d = constraints[ci];
|
||||
auto map1 = [&](int& e) { e = (e >= 0 && local.count(e)) ? local[e] : -1; };
|
||||
map1(d.ea); map1(d.eb); map1(d.ec);
|
||||
subc.push_back(d);
|
||||
}
|
||||
const int sub_drag = (dragged_ei >= 0 && local.count(dragged_ei)) ? local[dragged_ei] : -1;
|
||||
SketchSolveResult r = solve_system(sub, subc, sub_drag, dragged_role);
|
||||
if (!r.ok) {
|
||||
out.ok = false;
|
||||
out.result = r.result;
|
||||
for (int bi : r.bad)
|
||||
if (bi >= 0 && bi < int(cidx.size())) out.bad.push_back(cidx[bi]);
|
||||
}
|
||||
if (r.dof > 0) out.dof += r.dof;
|
||||
solved.emplace_back(std::move(ents), std::move(sub));
|
||||
}
|
||||
if (!out.ok) return out;
|
||||
for (auto& [ents, sub] : solved)
|
||||
for (size_t k = 0; k < ents.size(); ++k) entities[ents[k]] = sub[k];
|
||||
return out;
|
||||
}
|
||||
|
||||
static SketchSolveResult solve_impl(std::vector<SketchEntity>& entities,
|
||||
const std::vector<SketchEntityConstraintDef>& constraints,
|
||||
int dragged_ei, Role dragged_role)
|
||||
{
|
||||
SketchSolveResult out = solve_system(entities, constraints, dragged_ei, dragged_role);
|
||||
if (out.ok || out.result != SLVS_RESULT_TOO_MANY_UNKNOWNS) return out;
|
||||
return solve_partitioned(entities, constraints, dragged_ei, dragged_role);
|
||||
}
|
||||
|
||||
SketchSolveResult sketch_solve(std::vector<SketchEntity>& entities,
|
||||
const std::vector<SketchEntityConstraintDef>& constraints)
|
||||
{
|
||||
return solve_impl(entities, constraints, -1, Role::P0);
|
||||
}
|
||||
|
||||
SketchSolveResult sketch_solve_drag(std::vector<SketchEntity>& entities,
|
||||
const std::vector<SketchEntityConstraintDef>& constraints,
|
||||
int dragged_ei, SketchPointRole dragged_role)
|
||||
{
|
||||
return solve_impl(entities, constraints, dragged_ei, dragged_role);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,37 @@
|
||||
#ifndef slic3r_SketchSolver_hpp_
|
||||
#define slic3r_SketchSolver_hpp_
|
||||
|
||||
// Bridge from the Design tab's SketchEntity / SketchEntityConstraintDef model onto the
|
||||
// vendored SolveSpace constraint solver (src/libslic3r/slvs, libslvs). Replaces the
|
||||
// hand-rolled SketchConstraints: full constraint set, real DoF counting, and
|
||||
// over-constrained (bad-constraint) detection. Solves on a fixed 2D XY workplane.
|
||||
|
||||
#include "libslic3r/CAD/SketchEngine.hpp"
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
struct SketchSolveResult {
|
||||
bool ok{false}; // solver converged & consistent
|
||||
int dof{-1}; // remaining degrees of freedom (>0 under-constrained)
|
||||
int result{0}; // raw SLVS_RESULT_* code
|
||||
std::vector<int> bad; // indices (into `constraints`) of conflicting constraints
|
||||
};
|
||||
|
||||
// Solve `constraints` over `entities` in place (writes solved coordinates back into the
|
||||
// entities; arc angles are reflowed preserving sweep direction). No-op success when
|
||||
// `constraints` is empty.
|
||||
SketchSolveResult sketch_solve(std::vector<SketchEntity>& entities,
|
||||
const std::vector<SketchEntityConstraintDef>& constraints);
|
||||
|
||||
// Drag-aware solve: pins the (dragged_ei, dragged_role) point's parameters via the
|
||||
// solver's `dragged[]` priority list so the solver keeps that point where the cursor
|
||||
// placed it (caller must have moved it first) and moves the OTHER free geometry to
|
||||
// re-satisfy the constraints. dragged_ei < 0 behaves identically to sketch_solve.
|
||||
SketchSolveResult sketch_solve_drag(std::vector<SketchEntity>& entities,
|
||||
const std::vector<SketchEntityConstraintDef>& constraints,
|
||||
int dragged_ei, SketchPointRole dragged_role);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,95 @@
|
||||
#include "libslic3r/CAD/ThreadStandards.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Imperial helpers: convert nominal inch diameter / threads-per-inch to mm.
|
||||
static constexpr double IN = 25.4;
|
||||
static inline double tpi_pitch(double tpi) { return IN / tpi; }
|
||||
|
||||
const std::vector<ThreadSpec>& thread_standards()
|
||||
{
|
||||
using S = ThreadSpec::Series;
|
||||
static const std::vector<ThreadSpec> table = {
|
||||
// --- ISO metric, coarse pitch (ISO 261 preferred series) ---
|
||||
{"M1", 1.0, 0.25, S::MetricCoarse},
|
||||
{"M1.2", 1.2, 0.25, S::MetricCoarse},
|
||||
{"M1.6", 1.6, 0.35, S::MetricCoarse},
|
||||
{"M2", 2.0, 0.40, S::MetricCoarse},
|
||||
{"M2.5", 2.5, 0.45, S::MetricCoarse},
|
||||
{"M3", 3.0, 0.50, S::MetricCoarse},
|
||||
{"M4", 4.0, 0.70, S::MetricCoarse},
|
||||
{"M5", 5.0, 0.80, S::MetricCoarse},
|
||||
{"M6", 6.0, 1.00, S::MetricCoarse},
|
||||
{"M8", 8.0, 1.25, S::MetricCoarse},
|
||||
{"M10", 10.0, 1.50, S::MetricCoarse},
|
||||
{"M12", 12.0, 1.75, S::MetricCoarse},
|
||||
{"M14", 14.0, 2.00, S::MetricCoarse},
|
||||
{"M16", 16.0, 2.00, S::MetricCoarse},
|
||||
{"M20", 20.0, 2.50, S::MetricCoarse},
|
||||
{"M24", 24.0, 3.00, S::MetricCoarse},
|
||||
{"M30", 30.0, 3.50, S::MetricCoarse},
|
||||
{"M36", 36.0, 4.00, S::MetricCoarse},
|
||||
{"M42", 42.0, 4.50, S::MetricCoarse},
|
||||
{"M48", 48.0, 5.00, S::MetricCoarse},
|
||||
{"M56", 56.0, 5.50, S::MetricCoarse},
|
||||
{"M64", 64.0, 6.00, S::MetricCoarse},
|
||||
|
||||
// --- ISO metric, common fine pitches (ISO 261 fine series) ---
|
||||
{"M8x1", 8.0, 1.00, S::MetricFine},
|
||||
{"M10x1.25", 10.0, 1.25, S::MetricFine},
|
||||
{"M10x1", 10.0, 1.00, S::MetricFine},
|
||||
{"M12x1.5", 12.0, 1.50, S::MetricFine},
|
||||
{"M12x1.25", 12.0, 1.25, S::MetricFine},
|
||||
{"M16x1.5", 16.0, 1.50, S::MetricFine},
|
||||
{"M20x1.5", 20.0, 1.50, S::MetricFine},
|
||||
{"M24x2", 24.0, 2.00, S::MetricFine},
|
||||
|
||||
// --- Unified National Coarse (UTS / ASME B1.1) ---
|
||||
{"#1-64 UNC", 0.073 * IN, tpi_pitch(64), S::UNC},
|
||||
{"#2-56 UNC", 0.086 * IN, tpi_pitch(56), S::UNC},
|
||||
{"#3-48 UNC", 0.099 * IN, tpi_pitch(48), S::UNC},
|
||||
{"#4-40 UNC", 0.112 * IN, tpi_pitch(40), S::UNC},
|
||||
{"#5-40 UNC", 0.125 * IN, tpi_pitch(40), S::UNC},
|
||||
{"#6-32 UNC", 0.138 * IN, tpi_pitch(32), S::UNC},
|
||||
{"#8-32 UNC", 0.164 * IN, tpi_pitch(32), S::UNC},
|
||||
{"#10-24 UNC", 0.190 * IN, tpi_pitch(24), S::UNC},
|
||||
{"#12-24 UNC", 0.216 * IN, tpi_pitch(24), S::UNC},
|
||||
{"1/4-20 UNC", 0.250 * IN, tpi_pitch(20), S::UNC},
|
||||
{"5/16-18 UNC", 0.3125 * IN, tpi_pitch(18), S::UNC},
|
||||
{"3/8-16 UNC", 0.375 * IN, tpi_pitch(16), S::UNC},
|
||||
{"7/16-14 UNC", 0.4375 * IN, tpi_pitch(14), S::UNC},
|
||||
{"1/2-13 UNC", 0.500 * IN, tpi_pitch(13), S::UNC},
|
||||
{"9/16-12 UNC", 0.5625 * IN, tpi_pitch(12), S::UNC},
|
||||
{"5/8-11 UNC", 0.625 * IN, tpi_pitch(11), S::UNC},
|
||||
{"3/4-10 UNC", 0.750 * IN, tpi_pitch(10), S::UNC},
|
||||
{"7/8-9 UNC", 0.875 * IN, tpi_pitch(9), S::UNC},
|
||||
{"1-8 UNC", 1.000 * IN, tpi_pitch(8), S::UNC},
|
||||
|
||||
// --- Unified National Fine (UTS / ASME B1.1) ---
|
||||
{"#2-64 UNF", 0.086 * IN, tpi_pitch(64), S::UNF},
|
||||
{"#4-48 UNF", 0.112 * IN, tpi_pitch(48), S::UNF},
|
||||
{"#6-40 UNF", 0.138 * IN, tpi_pitch(40), S::UNF},
|
||||
{"#8-36 UNF", 0.164 * IN, tpi_pitch(36), S::UNF},
|
||||
{"#10-32 UNF", 0.190 * IN, tpi_pitch(32), S::UNF},
|
||||
{"1/4-28 UNF", 0.250 * IN, tpi_pitch(28), S::UNF},
|
||||
{"5/16-24 UNF", 0.3125 * IN, tpi_pitch(24), S::UNF},
|
||||
{"3/8-24 UNF", 0.375 * IN, tpi_pitch(24), S::UNF},
|
||||
{"7/16-20 UNF", 0.4375 * IN, tpi_pitch(20), S::UNF},
|
||||
{"1/2-20 UNF", 0.500 * IN, tpi_pitch(20), S::UNF},
|
||||
{"9/16-18 UNF", 0.5625 * IN, tpi_pitch(18), S::UNF},
|
||||
{"5/8-18 UNF", 0.625 * IN, tpi_pitch(18), S::UNF},
|
||||
{"3/4-16 UNF", 0.750 * IN, tpi_pitch(16), S::UNF},
|
||||
{"1-12 UNF", 1.000 * IN, tpi_pitch(12), S::UNF},
|
||||
};
|
||||
return table;
|
||||
}
|
||||
|
||||
const ThreadSpec* find_thread_standard(const std::string& name)
|
||||
{
|
||||
for (const ThreadSpec& s : thread_standards())
|
||||
if (s.name == name)
|
||||
return &s;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,39 @@
|
||||
#ifndef slic3r_ThreadStandards_hpp_
|
||||
#define slic3r_ThreadStandards_hpp_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Canonical mechanical thread specifications (ISO metric + Unified imperial).
|
||||
// All dimensions are stored in millimetres so the CAD kernel can consume them
|
||||
// directly. The profile is the common 60deg V shared by ISO 261/965 and ASME
|
||||
// B1.1 (UTS), so the cut/ridge depth used by the Design-tab Thread tool is the
|
||||
// basic external thread height h = 0.6134 * pitch, and the internal (tapped)
|
||||
// minor diameter is D1 = D - 1.0825 * pitch (= D - 2*5H/8).
|
||||
struct ThreadSpec {
|
||||
enum class Series { MetricCoarse, MetricFine, UNC, UNF };
|
||||
|
||||
std::string name; // designation, e.g. "M6", "1/4-20 UNC"
|
||||
double major_diameter_mm; // nominal (crest) diameter
|
||||
double pitch_mm; // axial advance per turn
|
||||
Series series;
|
||||
|
||||
// 60deg basic external thread height (radial crest-to-root engagement).
|
||||
double thread_depth_mm() const { return 0.6134 * pitch_mm; }
|
||||
// Internal/tapped minor (tap-drill) diameter for the same nominal thread.
|
||||
double minor_diameter_mm() const { return major_diameter_mm - 1.0825 * pitch_mm; }
|
||||
|
||||
bool imperial() const { return series == Series::UNC || series == Series::UNF; }
|
||||
};
|
||||
|
||||
// Full ordered table (metric coarse, metric fine, UNC, UNF) for GUI listing.
|
||||
const std::vector<ThreadSpec>& thread_standards();
|
||||
|
||||
// Exact case-sensitive designation lookup; nullptr if not a known standard.
|
||||
const ThreadSpec* find_thread_standard(const std::string& name);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
@@ -21,6 +21,11 @@ endif()
|
||||
option(BUILD_SHARED_LIBS "Build shared libs" OFF)
|
||||
option(USE_SLIC3R_CONSOLE_LOG "Enable console logging in RelWithDebInfo builds" OFF)
|
||||
|
||||
# SolveSpace constraint solver (2D sketch solver backbone), built in deps/SLVS.
|
||||
if (SLIC3R_CAD)
|
||||
find_package(SLVS REQUIRED)
|
||||
endif ()
|
||||
|
||||
set(lisbslic3r_sources
|
||||
AABBMesh.cpp
|
||||
AABBMesh.hpp
|
||||
@@ -304,6 +309,8 @@ set(lisbslic3r_sources
|
||||
Layer.cpp
|
||||
Layer.hpp
|
||||
LayerRegion.cpp
|
||||
LayOnFace.cpp
|
||||
LayOnFace.hpp
|
||||
libslic3r.cpp
|
||||
libslic3r.h
|
||||
Line.cpp
|
||||
@@ -506,6 +513,29 @@ set(lisbslic3r_sources
|
||||
FlushVolPredictor.cpp
|
||||
)
|
||||
|
||||
# Parametric Design/CAD kernel. Needs OCCT's ModelingAlgorithms module and the
|
||||
# vendored SolveSpace solver; both are pulled in only when SLIC3R_CAD is ON.
|
||||
if (SLIC3R_CAD)
|
||||
list(APPEND lisbslic3r_sources
|
||||
CAD/GeometryEngine.cpp
|
||||
CAD/GeometryEngine.hpp
|
||||
CAD/SketchEngine.cpp
|
||||
CAD/SketchEngine.hpp
|
||||
CAD/SketchConstraints.cpp
|
||||
CAD/SketchConstraints.hpp
|
||||
CAD/SketchSolver.cpp
|
||||
CAD/SketchSolver.hpp
|
||||
CAD/SketchInference.cpp
|
||||
CAD/SketchInference.hpp
|
||||
CAD/SketchImport.cpp
|
||||
CAD/SketchImport.hpp
|
||||
CAD/CadDocument.cpp
|
||||
CAD/CadDocument.hpp
|
||||
CAD/ThreadStandards.cpp
|
||||
CAD/ThreadStandards.hpp
|
||||
)
|
||||
endif ()
|
||||
|
||||
if (APPLE)
|
||||
list(APPEND lisbslic3r_sources
|
||||
MacUtils.mm
|
||||
@@ -617,6 +647,30 @@ set(OCCT_LIBS
|
||||
TKMath
|
||||
TKernel
|
||||
)
|
||||
# The CAD kernel is the only consumer of OCCT's ModelingAlgorithms module: TKFillet
|
||||
# (BRepFilletAPI), TKOffset (BRepOffsetAPI) and TKBool, which the other two need.
|
||||
#
|
||||
# PREPEND, never append: this list is single-pass static link order, dependents before
|
||||
# dependencies — note TKernel, which everything needs, is deliberately last. TKOffset
|
||||
# references BRepAlgo_Loop, which TKBool defines, so TKOffset must come BEFORE TKBool.
|
||||
# Appending put it after, and a strictly single-pass linker (the Flatpak build) failed with
|
||||
# libTKOffset.a(BRepOffset_MakeLoops.cxx.o): undefined reference to
|
||||
# `BRepAlgo_Loop::BRepAlgo_Loop()'
|
||||
# while the ordinary Linux, macOS and Windows links resolved it anyway. Use set() rather
|
||||
# than list(PREPEND), which needs CMake 3.15 and this project supports 3.13.
|
||||
if (SLIC3R_CAD)
|
||||
set(OCCT_LIBS TKFillet TKOffset TKBool ${OCCT_LIBS})
|
||||
# deps is configured separately, so its SLIC3R_CAD can differ from ours. The module is
|
||||
# all-or-nothing, so one absent toolkit proves it; fail here rather than at link time.
|
||||
if (NOT TARGET TKFillet)
|
||||
message(FATAL_ERROR
|
||||
"SLIC3R_CAD is ON, but the OpenCASCADE in ${CMAKE_PREFIX_PATH} was built without "
|
||||
"BUILD_MODULE_ModelingAlgorithms. Rebuild the dependencies with -DSLIC3R_CAD=ON, "
|
||||
"or configure this project with -DSLIC3R_CAD=OFF.")
|
||||
endif ()
|
||||
endif ()
|
||||
# Published for the Windows packaging step in the top-level CMakeLists.txt.
|
||||
set(OCCT_LIBS "${OCCT_LIBS}" CACHE INTERNAL "OCCT toolkits linked by libslic3r")
|
||||
|
||||
target_link_libraries(libslic3r
|
||||
PUBLIC
|
||||
@@ -669,6 +723,10 @@ if (TARGET OpenVDB::openvdb)
|
||||
target_link_libraries(libslic3r PRIVATE OpenVDB::openvdb)
|
||||
endif()
|
||||
|
||||
if (SLIC3R_CAD)
|
||||
target_link_libraries(libslic3r PUBLIC SLVS::slvs)
|
||||
endif ()
|
||||
|
||||
if(WIN32)
|
||||
target_link_libraries(libslic3r PRIVATE Psapi.lib bcrypt.lib)
|
||||
endif()
|
||||
|
||||
@@ -454,6 +454,10 @@ class ExtrusionLoop : public ExtrusionEntity
|
||||
{
|
||||
public:
|
||||
ExtrusionPaths paths;
|
||||
// ORCA: Set on a loop extruded entirely in mid air and out of reach of the layer below: it has
|
||||
// nothing to lean on until this layer is bridged, so the G-code writer holds it back until the
|
||||
// infill is down. See defer_unsupported_loops() in PerimeterGenerator.cpp.
|
||||
bool print_after_infill = false;
|
||||
|
||||
ExtrusionLoop(ExtrusionLoopRole role = elrDefault) : m_loop_role(role) {}
|
||||
ExtrusionLoop(const ExtrusionPaths &paths, ExtrusionLoopRole role = elrDefault) : paths(paths), m_loop_role(role) {}
|
||||
|
||||
@@ -175,6 +175,10 @@ const std::string BBS_MODEL_CONFIG_RELS_FILE = "Metadata/_rels/model_settings.co
|
||||
const std::string SLICE_INFO_CONFIG_FILE = "Metadata/slice_info.config";
|
||||
const std::string FILAMENT_SEQUENCE_FILE = "Metadata/filament_sequence.json";
|
||||
const std::string BBS_LAYER_HEIGHTS_PROFILE_FILE = "Metadata/layer_heights_profile.txt";
|
||||
const std::string ORCA_CAD_RECIPE_FILE = "Metadata/orca_cad.bin";
|
||||
// Read-only: the recipe entry's pre-rename name. A reader that knows only the new one drops the
|
||||
// feature tree of every project written before the move, without a word. Never written.
|
||||
const std::string LEGACY_CAD_RECIPE_FILE = "Metadata/SnapOrca_cad.bin";
|
||||
const std::string LAYER_CONFIG_RANGES_FILE = "Metadata/layer_config_ranges.xml";
|
||||
const std::string BRIM_EAR_POINTS_FILE = "Metadata/brim_ear_points.txt";
|
||||
/*const std::string SLA_SUPPORT_POINTS_FILE = "Metadata/Slic3r_PE_sla_support_points.txt";
|
||||
@@ -1950,6 +1954,15 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
// extract slic3r print config file
|
||||
_extract_project_config_from_archive(archive, stat, config, config_substitutions, model);
|
||||
}
|
||||
else if (boost::algorithm::iequals(name, ORCA_CAD_RECIPE_FILE)
|
||||
|| boost::algorithm::iequals(name, LEGACY_CAD_RECIPE_FILE)) {
|
||||
// Restore the editable CAD recipe (optional; absent in non-CAD projects).
|
||||
if (stat.m_uncomp_size > 0) {
|
||||
std::string buf((size_t)stat.m_uncomp_size, '\0');
|
||||
if (mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, buf.data(), buf.size(), 0))
|
||||
model.cad_recipe = std::move(buf);
|
||||
}
|
||||
}
|
||||
else if (boost::algorithm::iequals(name, CUT_INFORMATION_FILE)) {
|
||||
// extract object cut info
|
||||
_extract_cut_information_from_archive(archive, stat, config_substitutions);
|
||||
@@ -6008,6 +6021,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
bool _add_mesh_to_object_stream(std::function<bool(std::string &, bool)> const &flush, ObjectData const &object_data) const;
|
||||
bool _add_build_to_model_stream(std::stringstream& stream, const BuildItemsList& build_items) const;
|
||||
bool _add_layer_height_profile_file_to_archive(mz_zip_archive& archive, Model& model);
|
||||
bool _add_cad_recipe_file_to_archive(mz_zip_archive& archive, Model& model);
|
||||
bool _add_layer_config_ranges_file_to_archive(mz_zip_archive& archive, Model& model);
|
||||
bool _add_brim_ear_points_file_to_archive(mz_zip_archive& archive, Model& model);
|
||||
bool _add_sla_support_points_file_to_archive(mz_zip_archive& archive, Model& model);
|
||||
@@ -6404,6 +6418,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_add_cad_recipe_file_to_archive(archive, model)) {
|
||||
close_zip_writer(&archive);
|
||||
return false;
|
||||
}
|
||||
|
||||
// BBS progress point
|
||||
/*BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format("export 3mf EXPORT_STAGE_ADD_LAYER_RANGE\n");
|
||||
if (proFn) {
|
||||
@@ -7658,6 +7677,19 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _BBS_3MF_Exporter::_add_cad_recipe_file_to_archive(mz_zip_archive& archive, Model& model)
|
||||
{
|
||||
if (model.cad_recipe.empty())
|
||||
return true;
|
||||
if (!mz_zip_writer_add_mem(&archive, ORCA_CAD_RECIPE_FILE.c_str(),
|
||||
(const void*)model.cad_recipe.data(), model.cad_recipe.length(),
|
||||
MZ_DEFAULT_COMPRESSION)) {
|
||||
add_error("Unable to add CAD recipe file to archive");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _BBS_3MF_Exporter::_add_layer_config_ranges_file_to_archive(mz_zip_archive& archive, Model& model)
|
||||
{
|
||||
std::string out = "";
|
||||
|
||||
+62
-19
@@ -1028,11 +1028,21 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
double current_z = gcodegen.writer().get_position().z();
|
||||
if (z == -1.) // in case no specific z was provided, print at current_z pos
|
||||
z = current_z;
|
||||
if (!is_approx(z, current_z)) {
|
||||
// Orca: wipe_tower_no_sparse_layers crash guard. With sparse layers skipped the tower is
|
||||
// compacted far below the object, so descending to it is only safe once the nozzle is parked
|
||||
// over the tower - which is what the is_finish_first travel above does. Otherwise the nozzle
|
||||
// is still over the model and this descent would drive it into the print, so defer it to the
|
||||
// re-descents below, which run after the travel to the tower.
|
||||
const bool defer_compacted_descend = m_sparse_layers_skipped
|
||||
&& !tcr.priming && !tcr.is_finish_first && (current_z - z) > EPSILON;
|
||||
if (!is_approx(z, current_z) && !defer_compacted_descend) {
|
||||
gcode += gcodegen.writer().retract();
|
||||
gcode += gcodegen.writer().travel_to_z(z, "Travel down to the last wipe tower layer.");
|
||||
gcode += gcodegen.writer().unretract();
|
||||
}
|
||||
// Tower compacted below the object, so any extrusion emitted without an explicit z has to be
|
||||
// pulled back down to it first.
|
||||
const bool compacted_below_object = m_sparse_layers_skipped && z >= 0. && (tcr.print_z - z) > EPSILON;
|
||||
|
||||
// Process the end filament gcode.
|
||||
bool add_change_filament_624 = false;
|
||||
@@ -1085,11 +1095,23 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
std::string nozzle_change_gcode_trans;
|
||||
if (is_nozzle_change) {
|
||||
// move to start_pos before nozzle change
|
||||
// Orca: travel_to() lifts to the object layer height to clear the print. That lift is
|
||||
// needed when arriving from the model, but is a wasted full-height Z bounce when the
|
||||
// nozzle already sits on the compacted tower, so travel at the compacted z instead.
|
||||
const bool compact_intower_nc_travel = compacted_below_object
|
||||
&& (tcr.print_z - gcodegen.writer().get_position().z()) > EPSILON;
|
||||
std::string start_pos_str;
|
||||
start_pos_str = gcodegen.travel_to(wipe_tower_point_to_object_point(gcodegen, transform_wt_pt(tcr.nozzle_change_result.start_pos) + plate_origin_2d), erMixed,
|
||||
"Move to nozzle change start pos");
|
||||
"Move to nozzle change start pos", compact_intower_nc_travel ? z : DBL_MAX);
|
||||
check_add_eol(start_pos_str);
|
||||
nozzle_change_gcode_trans += start_pos_str;
|
||||
// The nozzle-change wipe below carries no explicit z, so it would extrude at the object
|
||||
// layer height and float above the compacted tower. Descend unless the travel stayed down.
|
||||
if (!compact_intower_nc_travel && compacted_below_object) {
|
||||
std::string nc_z_descend = gcodegen.writer().travel_to_z(z, "Descend to compacted wipe tower z (no sparse layers)");
|
||||
check_add_eol(nc_z_descend);
|
||||
nozzle_change_gcode_trans += nc_z_descend;
|
||||
}
|
||||
nozzle_change_gcode_trans += gcodegen.unretract();
|
||||
nozzle_change_gcode_trans += transform_gcode(tcr.nozzle_change_result.gcode, tcr.nozzle_change_result.start_pos, wipe_tower_offset, wipe_tower_rotation);
|
||||
gcodegen.set_last_pos(wipe_tower_point_to_object_point(gcodegen, transform_wt_pt(tcr.nozzle_change_result.end_pos) + plate_origin_2d));
|
||||
@@ -1428,6 +1450,15 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
|
||||
start_filament_gcode_str = start_filament_gcode_str + wipe_next_start_point_str + toolchange_unretract_str;
|
||||
|
||||
// Orca: the custom change_filament_gcode lifts to the object layer height and the unretract
|
||||
// de-hops back to it, so every tower extrusion emitted after it (purge moves, and the wall
|
||||
// when it prints after the toolchange) would float above the compacted tower. Descend first.
|
||||
if (compacted_below_object) {
|
||||
std::string z_descend = gcodegen.writer().travel_to_z(z, "Descend to compacted wipe tower z (no sparse layers)");
|
||||
check_add_eol(z_descend);
|
||||
start_filament_gcode_str += z_descend;
|
||||
}
|
||||
|
||||
// Insert the end filament, toolchange, and start filament gcode into the generated gcode.
|
||||
DynamicConfig config;
|
||||
config.set_key_value("filament_end_gcode", new ConfigOptionString(end_filament_gcode_str));
|
||||
@@ -1915,11 +1946,9 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
// resulting in a wipe tower with sparse layers.
|
||||
double wipe_tower_z = -1;
|
||||
bool ignore_sparse = false;
|
||||
if (gcodegen.config().wipe_tower_no_sparse_layers.value) {
|
||||
if (m_sparse_layers_skipped) {
|
||||
wipe_tower_z = m_last_wipe_tower_print_z;
|
||||
ignore_sparse = (m_tool_changes[m_layer_idx].size() == 1 &&
|
||||
m_tool_changes[m_layer_idx].front().initial_tool == m_tool_changes[m_layer_idx].front().new_tool &&
|
||||
m_layer_idx != 0);
|
||||
ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]) && m_layer_idx != 0;
|
||||
if (m_tool_change_idx == 0 && !ignore_sparse)
|
||||
wipe_tower_z = m_last_wipe_tower_print_z + m_tool_changes[m_layer_idx].front().layer_height;
|
||||
}
|
||||
@@ -1935,12 +1964,9 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
// resulting in a wipe tower with sparse layers.
|
||||
double wipe_tower_z = -1;
|
||||
bool ignore_sparse = false;
|
||||
if (gcodegen.config().wipe_tower_no_sparse_layers.value) {
|
||||
wipe_tower_z = m_last_wipe_tower_print_z;
|
||||
ignore_sparse = (m_tool_changes[m_layer_idx].size() == 1 &&
|
||||
m_tool_changes[m_layer_idx].front().initial_tool == m_tool_changes[m_layer_idx].front().new_tool);
|
||||
if (m_tool_change_idx == 0 && !ignore_sparse)
|
||||
wipe_tower_z = m_last_wipe_tower_print_z + m_tool_changes[m_layer_idx].front().layer_height;
|
||||
if (m_sparse_layers_skipped) {
|
||||
ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]);
|
||||
wipe_tower_z = m_compacted_tower_z[m_layer_idx];
|
||||
}
|
||||
|
||||
if ((m_enable_timelapse_print || m_enable_wrapping_detection) && m_is_first_print) {
|
||||
@@ -1953,10 +1979,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
if (!(size_t(m_tool_change_idx) < m_tool_changes[m_layer_idx].size()))
|
||||
throw Slic3r::RuntimeError("Wipe tower generation failed, possibly due to empty first layer.");
|
||||
|
||||
if (!ignore_sparse) {
|
||||
if (!ignore_sparse)
|
||||
gcode += append_tcr(gcodegen, m_tool_changes[m_layer_idx][m_tool_change_idx++], extruder_id, wipe_tower_z);
|
||||
m_last_wipe_tower_print_z = wipe_tower_z;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1970,9 +1994,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
|
||||
return true;
|
||||
|
||||
bool ignore_sparse = false;
|
||||
if (gcodegen.config().wipe_tower_no_sparse_layers.value) {
|
||||
ignore_sparse = (m_tool_changes[m_layer_idx].size() == 1 && m_tool_changes[m_layer_idx].front().initial_tool == m_tool_changes[m_layer_idx].front().new_tool);
|
||||
}
|
||||
if (m_sparse_layers_skipped)
|
||||
ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]);
|
||||
|
||||
if ((m_enable_timelapse_print || m_enable_wrapping_detection) && m_is_first_print) {
|
||||
return false;
|
||||
@@ -6580,6 +6603,8 @@ LayerResult GCode::process_layer(
|
||||
}
|
||||
// Then print infill
|
||||
gcode += this->extrude_infill(print, by_region_specific, false);
|
||||
// Then the walls left hanging in mid air, now that the infill can anchor them
|
||||
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, false, true);
|
||||
// Then print perimeters of regions that has is_infill_first == true
|
||||
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, true);
|
||||
}
|
||||
@@ -6875,6 +6900,7 @@ LayerResult GCode::process_layer(
|
||||
has_insert_timelapse_gcode = true;
|
||||
}
|
||||
gcode += this->extrude_infill(print, by_region_specific, false);
|
||||
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, false, true);
|
||||
gcode += this->extrude_perimeters(print, by_region_specific, first_layer, true);
|
||||
// ironing
|
||||
gcode += this->extrude_infill(print, by_region_specific, true);
|
||||
@@ -7615,7 +7641,7 @@ std::string GCode::extrude_path(const ExtrusionPath& path, const std::string& de
|
||||
}
|
||||
|
||||
// Extrude perimeters: Decide where to put seams (hide or align seams).
|
||||
std::string GCode::extrude_perimeters(const Print &print, const std::vector<ObjectByExtruder::Island::Region> &by_region, bool is_first_layer, bool is_infill_first)
|
||||
std::string GCode::extrude_perimeters(const Print &print, const std::vector<ObjectByExtruder::Island::Region> &by_region, bool is_first_layer, bool is_infill_first, bool unsupported_loops_only)
|
||||
{
|
||||
std::string gcode;
|
||||
for (const ObjectByExtruder::Island::Region ®ion : by_region)
|
||||
@@ -7634,7 +7660,24 @@ std::string GCode::extrude_perimeters(const Print &print, const std::vector<Obje
|
||||
m_config.wipe_inward_distance.value > 0. &&
|
||||
scale_(FILAMENT_CONFIG(wipe_distance)) > SCALED_EPSILON)
|
||||
wipe_support.emplace();
|
||||
|
||||
// ORCA: loops flagged as extruded in mid air, out of reach of the layer below, are held back
|
||||
// for a second pass after the infill that anchors them. Infill already precedes infill first walls.
|
||||
const bool defer_unsupported = !is_infill_first;
|
||||
auto waits_for_infill = [](const ExtrusionEntity *ee) {
|
||||
return ee->is_loop() && static_cast<const ExtrusionLoop *>(ee)->print_after_infill;
|
||||
};
|
||||
|
||||
// The deferred pass runs after the infill, so the loops the first pass emitted are
|
||||
// already down and belong in the prefix an inward wipe may land on.
|
||||
if (wipe_support && defer_unsupported && unsupported_loops_only)
|
||||
for (const ExtrusionEntity* ee : region.perimeters)
|
||||
if (!waits_for_infill(ee))
|
||||
wipe_support->append(*ee);
|
||||
|
||||
for (const ExtrusionEntity* ee : region.perimeters) {
|
||||
if (defer_unsupported && waits_for_infill(ee) != unsupported_loops_only)
|
||||
continue;
|
||||
gcode += this->extrude_entity(*ee, "perimeter", -1., region.perimeters,
|
||||
wipe_support ? &*wipe_support : nullptr);
|
||||
if (wipe_support)
|
||||
|
||||
+12
-2
@@ -106,8 +106,13 @@ public:
|
||||
m_enable_wrapping_detection(print_config.enable_wrapping_detection && (print_config.wrapping_exclude_area.values.size() > 2) && (slice_used_filaments.size() <= 1)),
|
||||
m_is_first_print(true),
|
||||
m_print_config(&print_config),
|
||||
m_last_wipe_tower_print_z(print_config.z_offset.value)
|
||||
m_last_wipe_tower_print_z(print_config.z_offset.value),
|
||||
m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(print_config))
|
||||
{
|
||||
// Precomputed rather than accumulated while emitting, so that the clearance validator and
|
||||
// the emitter cannot disagree about where the compacted tower sits on any given layer.
|
||||
if (m_sparse_layers_skipped)
|
||||
m_compacted_tower_z = compute_compacted_wipe_tower_z(tool_changes, float(print_config.z_offset.value));
|
||||
// initialize with the extruder offset of master extruder id
|
||||
m_extruder_offsets.resize(print_config.filament_map.size(), print_config.extruder_offset.get_at(print_config.master_extruder_id.value - 1));
|
||||
const auto& filament_map = print_config.filament_map.values; // 1 based idx
|
||||
@@ -167,6 +172,11 @@ private:
|
||||
float m_wipe_tower_depth;
|
||||
BoundingBoxf m_wipe_tower_bbx;
|
||||
Vec2f m_rib_offset{Vec2f(0, 0)};
|
||||
// wipe_tower_no_sparse_layers, as answered by the shared compaction rule rather than by the raw
|
||||
// option: smooth timelapse and wrapping detection keep a tower on every layer regardless.
|
||||
const bool m_sparse_layers_skipped;
|
||||
// Print z of the compacted tower per planned layer. Empty when the tower is not compacted.
|
||||
std::vector<float> m_compacted_tower_z;
|
||||
};
|
||||
|
||||
class ColorPrintColors
|
||||
@@ -524,7 +534,7 @@ private:
|
||||
// For sequential print, the instance of the object to be printing has to be defined.
|
||||
const size_t single_object_instance_idx);
|
||||
|
||||
std::string extrude_perimeters(const Print& print, const std::vector<ObjectByExtruder::Island::Region>& by_region, bool is_first_layer, bool is_infill_first);
|
||||
std::string extrude_perimeters(const Print& print, const std::vector<ObjectByExtruder::Island::Region>& by_region, bool is_first_layer, bool is_infill_first, bool unsupported_loops_only = false);
|
||||
std::string extrude_infill(const Print& print, const std::vector<ObjectByExtruder::Island::Region>& by_region, bool ironing);
|
||||
std::string extrude_support(const ExtrusionEntityCollection& support_fills, const ExtrusionRole support_extrusion_role);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "FilamentMixer.hpp"
|
||||
#include "LocalesUtils.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "format.hpp"
|
||||
#include "I18N.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
@@ -82,8 +83,9 @@ bool check_filament_printable_after_group(const std::vector<unsigned int> &used_
|
||||
int printable_status = print_config->filament_printable.get_at(filament_id);
|
||||
int extruder_idx = filament_maps[filament_id];
|
||||
if (!(printable_status >> extruder_idx & 1)) {
|
||||
std::string extruder_name = extruder_idx == 0 ? _L("left") : _L("right");
|
||||
std::string error_msg = _L("Grouping error: ") + filament_type + _L(" can not be placed in the ") + extruder_name + _L(" nozzle");
|
||||
std::string error_msg = extruder_idx == 0 ?
|
||||
Slic3r::format(_L("Grouping error: %1% cannot be placed in the left nozzle"), filament_type) :
|
||||
Slic3r::format(_L("Grouping error: %1% cannot be placed in the right nozzle"), filament_type);
|
||||
throw Slic3r::RuntimeError(error_msg);
|
||||
}
|
||||
}
|
||||
@@ -2735,6 +2737,28 @@ void ToolOrdering::enforce_mixed_component_order()
|
||||
}
|
||||
}
|
||||
|
||||
// Declared in ToolOrdering.hpp (exposed for unit testing).
|
||||
std::vector<unsigned int> parse_cyclic_order(const std::string& str, unsigned int number_of_extruders)
|
||||
{
|
||||
std::vector<unsigned int> order;
|
||||
for (const std::string& token : split_string(str, ',')) {
|
||||
try {
|
||||
size_t pos = 0;
|
||||
int filament = std::stoi(token, &pos); // stoi skips leading whitespace by itself
|
||||
// stoi stops at the first non-digit, so "2x" would parse as 2. Require the whole token to be
|
||||
// consumed (bar trailing whitespace) to drop it like any other garbage.
|
||||
if (token.find_first_not_of(" \t\r\n", pos) != std::string::npos)
|
||||
continue;
|
||||
if (filament >= 1 && (unsigned int)filament <= number_of_extruders
|
||||
&& std::find(order.begin(), order.end(), (unsigned int)(filament - 1)) == order.end())
|
||||
order.emplace_back((unsigned int)(filament - 1));
|
||||
} catch (const std::exception&) {
|
||||
// Not a number, ignore it.
|
||||
}
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer)
|
||||
{
|
||||
const PrintConfig* print_config = m_print_config_ptr;
|
||||
@@ -2832,11 +2856,41 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
|
||||
const bool use_cyclic_ordering =
|
||||
(print_config->toolchange_ordering == ToolChangeOrderingType::Cyclic);
|
||||
|
||||
// By default the first layer keeps its adhesion-optimized order (and any custom first layer
|
||||
// sequence); the cyclic sequence is only forced onto it when the user opts in.
|
||||
const bool cyclic_first_layer = use_cyclic_ordering && print_config->toolchange_cyclic_first_layer.value;
|
||||
|
||||
// Optional user defined cyclic sequence, given as 1-based filament numbers ("3,2,1,4"). Filaments
|
||||
// missing from it keep their ascending order after the listed ones, so a partial or bogus entry
|
||||
// still yields the default cyclic order.
|
||||
const std::vector<unsigned int> cyclic_order =
|
||||
use_cyclic_ordering ? parse_cyclic_order(print_config->toolchange_cyclic_order.value, number_of_extruders)
|
||||
: std::vector<unsigned int>();
|
||||
|
||||
// Reorder a layer's filaments (0-based) for cyclic ordering: ascending by default, or following the
|
||||
// user defined sequence when one was given. Filaments absent from the sequence keep ascending order
|
||||
// after the listed ones.
|
||||
auto apply_cyclic_order = [&cyclic_order](std::vector<unsigned int>& filaments) {
|
||||
std::sort(filaments.begin(), filaments.end());
|
||||
if (!cyclic_order.empty())
|
||||
std::stable_sort(filaments.begin(), filaments.end(), [&cyclic_order](unsigned int lhs, unsigned int rhs) {
|
||||
auto rank = [&cyclic_order](unsigned int filament) {
|
||||
return size_t(std::find(cyclic_order.begin(), cyclic_order.end(), filament) - cyclic_order.begin());
|
||||
};
|
||||
return rank(lhs) < rank(rhs);
|
||||
});
|
||||
};
|
||||
|
||||
// other_layers_seq: the layer_idx and extruder_idx are base on 1
|
||||
auto get_custom_seq = [&other_layers_seqs, &reorder_first_layer, &first_layer_filaments, &layer_filaments, use_cyclic_ordering](int layer_idx, std::vector<int>& out_seq) -> bool {
|
||||
auto get_custom_seq = [&other_layers_seqs, &reorder_first_layer, &first_layer_filaments, &layer_filaments, use_cyclic_ordering, cyclic_first_layer, &apply_cyclic_order](int layer_idx, std::vector<int>& out_seq) -> bool {
|
||||
if (!reorder_first_layer && layer_idx == 0) {
|
||||
out_seq.resize(first_layer_filaments.size());
|
||||
std::transform(first_layer_filaments.begin(), first_layer_filaments.end(), out_seq.begin(), [](auto item) {return item + 1; });
|
||||
// The first layer tool order is already decided (adhesion-optimized, plus any custom first
|
||||
// layer sequence). Only override it with the cyclic sequence when the user opted in.
|
||||
std::vector<unsigned int> ordered = first_layer_filaments;
|
||||
if (cyclic_first_layer)
|
||||
apply_cyclic_order(ordered);
|
||||
out_seq.resize(ordered.size());
|
||||
std::transform(ordered.begin(), ordered.end(), out_seq.begin(), [](auto item) {return int(item) + 1; });
|
||||
return true;
|
||||
}
|
||||
for (size_t idx = other_layers_seqs.size() - 1; idx != size_t(-1); --idx) {
|
||||
@@ -2847,9 +2901,12 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume(bool reorder_first
|
||||
}
|
||||
}
|
||||
|
||||
if (use_cyclic_ordering && layer_idx >= 0 && size_t(layer_idx) < layer_filaments.size()) {
|
||||
// Skip the first layer here (layer_idx == 0 only reaches this point on the reorder_first_layer
|
||||
// path) unless the user asked for cyclic order on it, so it keeps the default flush ordering.
|
||||
if (use_cyclic_ordering && layer_idx >= 0 && (layer_idx != 0 || cyclic_first_layer)
|
||||
&& size_t(layer_idx) < layer_filaments.size()) {
|
||||
std::vector<unsigned int> ordered = layer_filaments[size_t(layer_idx)];
|
||||
std::sort(ordered.begin(), ordered.end());
|
||||
apply_cyclic_order(ordered);
|
||||
out_seq.resize(ordered.size());
|
||||
std::transform(ordered.begin(), ordered.end(), out_seq.begin(), [](auto item) { return int(item) + 1; });
|
||||
return true;
|
||||
|
||||
@@ -417,6 +417,11 @@ private:
|
||||
int most_used_extruder;
|
||||
};
|
||||
|
||||
// Parse the user defined cyclic toolchange sequence ("3,2 , 1 , 4") into 0-based filament indices.
|
||||
// Out-of-range entries, duplicates and non-numeric tokens are dropped, so a partially valid string
|
||||
// still orders the filaments it does name. Exposed for unit testing.
|
||||
std::vector<unsigned int> parse_cyclic_order(const std::string& str, unsigned int number_of_extruders);
|
||||
|
||||
} // namespace SLic3r
|
||||
|
||||
#endif /* slic3r_ToolOrdering_hpp_ */
|
||||
|
||||
@@ -25,6 +25,30 @@ static constexpr int arc_fit_size = 20;
|
||||
enum class LimitFlow { None, LimitPrintFlow, LimitRammingFlow, LimitRammingFlowNC};//nc:nozzle change
|
||||
static const std::map<float, float> nozzle_diameter_to_nozzle_change_width{{0.2f, 0.5f}, {0.4f, 1.0f}, {0.6f, 1.2f}, {0.8f, 1.4f}};
|
||||
|
||||
bool wipe_tower_sparse_layers_skipped(const PrintConfig &config)
|
||||
{
|
||||
return config.wipe_tower_no_sparse_layers.value && config.timelapse_type.value != TimelapseType::tlSmooth &&
|
||||
! config.enable_wrapping_detection.value;
|
||||
}
|
||||
|
||||
bool wipe_tower_layer_is_sparse(const std::vector<WipeTower::ToolChangeResult> &layer_tool_changes)
|
||||
{
|
||||
return layer_tool_changes.size() == 1 && layer_tool_changes.front().initial_tool == layer_tool_changes.front().new_tool;
|
||||
}
|
||||
|
||||
std::vector<float> compute_compacted_wipe_tower_z(const std::vector<std::vector<WipeTower::ToolChangeResult>> &tool_changes,
|
||||
float base_z)
|
||||
{
|
||||
std::vector<float> tower_z(tool_changes.size(), base_z);
|
||||
float last = base_z;
|
||||
for (size_t i = 0; i < tool_changes.size(); ++i) {
|
||||
if (! tool_changes[i].empty() && ! wipe_tower_layer_is_sparse(tool_changes[i]))
|
||||
last += tool_changes[i].front().layer_height;
|
||||
tower_z[i] = last;
|
||||
}
|
||||
return tower_z;
|
||||
}
|
||||
|
||||
inline float align_round(float value, float base)
|
||||
{
|
||||
return std::round(value / base) * base;
|
||||
@@ -1879,7 +1903,7 @@ WipeTower::WipeTower(const PrintConfig& config, int plate_idx, Vec3d plate_origi
|
||||
m_z_pos(0.f),
|
||||
//m_bridging(float(config.wipe_tower_bridging)),
|
||||
m_bridging(10.f),
|
||||
m_no_sparse_layers(config.wipe_tower_no_sparse_layers),
|
||||
m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(config)),
|
||||
m_gcode_flavor(config.gcode_flavor),
|
||||
m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))),
|
||||
m_current_tool(initial_tool),
|
||||
@@ -2977,7 +3001,7 @@ WipeTower::ToolChangeResult WipeTower::finish_layer(bool extrude_perimeter, bool
|
||||
|
||||
// Ask our writer about how much material was consumed.
|
||||
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
|
||||
if (! m_no_sparse_layers || toolchanges_on_layer)
|
||||
if (! m_sparse_layers_skipped || toolchanges_on_layer)
|
||||
if (m_current_tool < m_used_filament_length.size())
|
||||
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
|
||||
|
||||
@@ -3021,7 +3045,7 @@ void WipeTower::plan_toolchange(float z_par, float layer_height_par, unsigned in
|
||||
if (m_plan.empty() || m_plan.back().z + WT_EPSILON < z_par) // if we moved to a new layer, we'll add it to m_plan first
|
||||
m_plan.push_back(WipeTowerInfo(z_par, layer_height_par));
|
||||
|
||||
if (m_first_layer_idx == size_t(-1) && (! m_no_sparse_layers || old_tool != new_tool))
|
||||
if (m_first_layer_idx == size_t(-1) && (! m_sparse_layers_skipped || old_tool != new_tool))
|
||||
m_first_layer_idx = m_plan.size() - 1;
|
||||
|
||||
if (old_tool == new_tool) // new layer without toolchanges - we are done
|
||||
@@ -3874,7 +3898,7 @@ WipeTower::ToolChangeResult WipeTower::finish_layer_new(bool extrude_perimeter,
|
||||
|
||||
// Ask our writer about how much material was consumed.
|
||||
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
|
||||
if (!m_no_sparse_layers || toolchanges_on_layer)
|
||||
if (!m_sparse_layers_skipped || toolchanges_on_layer)
|
||||
if (m_current_tool < m_used_filament_length.size())
|
||||
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
|
||||
|
||||
@@ -3984,7 +4008,7 @@ WipeTower::ToolChangeResult WipeTower::finish_block(const WipeTowerBlock &block,
|
||||
|
||||
// Ask our writer about how much material was consumed.
|
||||
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
|
||||
if (!m_no_sparse_layers || toolchanges_on_layer)
|
||||
if (!m_sparse_layers_skipped || toolchanges_on_layer)
|
||||
if (filament_id < m_used_filament_length.size())
|
||||
m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length();
|
||||
|
||||
@@ -4101,7 +4125,7 @@ WipeTower::ToolChangeResult WipeTower::finish_block_solid(const WipeTowerBlock &
|
||||
|
||||
// Ask our writer about how much material was consumed.
|
||||
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
|
||||
if (!m_no_sparse_layers || toolchanges_on_layer)
|
||||
if (!m_sparse_layers_skipped || toolchanges_on_layer)
|
||||
if (filament_id < m_used_filament_length.size())
|
||||
m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length();
|
||||
|
||||
@@ -5155,7 +5179,7 @@ WipeTower::ToolChangeResult WipeTower::only_generate_out_wall(bool is_new_mode)
|
||||
|
||||
// Ask our writer about how much material was consumed.
|
||||
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
|
||||
if (!m_no_sparse_layers || toolchanges_on_layer)
|
||||
if (!m_sparse_layers_skipped || toolchanges_on_layer)
|
||||
if (m_current_tool < m_used_filament_length.size()) m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
|
||||
|
||||
return construct_tcr(writer, false, old_tool, true, false, 0.f, false);
|
||||
|
||||
@@ -521,7 +521,7 @@ private:
|
||||
//float m_parking_pos_retraction = 0.f;
|
||||
//float m_extra_loading_move = 0.f;
|
||||
float m_bridging = 0.f;
|
||||
bool m_no_sparse_layers = false;
|
||||
bool m_sparse_layers_skipped = false;
|
||||
// BBS: remove useless config
|
||||
//bool m_set_extruder_trimpot = false;
|
||||
bool m_adhesion = true;
|
||||
@@ -680,6 +680,24 @@ private:
|
||||
};
|
||||
|
||||
|
||||
// Compaction rule for wipe_tower_no_sparse_layers. Shared by the G-code emitter and by the
|
||||
// clearance validator so that both agree on where the compacted tower actually sits; a drift
|
||||
// between the two would either let a real nozzle collision through or reject a safe plate.
|
||||
|
||||
// Whether sparse layers are really skipped, i.e. whether the tower is compacted at all. Smooth
|
||||
// timelapse and wrapping detection put a tower on every layer, so no layer is ever dropped and the
|
||||
// tower keeps following the object even though the option is on. Tower planning, G-code emission and
|
||||
// the clearance validator all ask this single question, so none of them can compact on its own.
|
||||
bool wipe_tower_sparse_layers_skipped(const PrintConfig &config);
|
||||
|
||||
// A planned layer prints no tower at all when its only toolchange keeps the same filament.
|
||||
bool wipe_tower_layer_is_sparse(const std::vector<WipeTower::ToolChangeResult> &layer_tool_changes);
|
||||
|
||||
// Print z the compacted tower reaches on every planned layer. Sparse layers carry over the
|
||||
// previous value, so the tower falls one layer height behind the object for each of them. base_z is
|
||||
// the z the tower starts from, which Orca offsets by z_offset.
|
||||
std::vector<float> compute_compacted_wipe_tower_z(const std::vector<std::vector<WipeTower::ToolChangeResult>> &tool_changes,
|
||||
float base_z = 0.f);
|
||||
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -1032,7 +1032,7 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau
|
||||
m_y_shift(0.f),
|
||||
m_z_pos(0.f),
|
||||
m_bridging(float(config.wipe_tower_bridging)),
|
||||
m_no_sparse_layers(config.wipe_tower_no_sparse_layers),
|
||||
m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(config)),
|
||||
m_gcode_flavor(config.gcode_flavor),
|
||||
m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))),
|
||||
m_infill_speed(default_region_config.sparse_infill_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))),
|
||||
@@ -1730,7 +1730,7 @@ void WipeTower2::toolchange_Change(
|
||||
} else if (m_wall_type == (int)wtwCone) {
|
||||
const double support_scale = get_wipe_tower_cone_base(m_wipe_tower_width, m_wipe_tower_height, m_wipe_tower_depth,
|
||||
m_wipe_tower_cone_angle).second;
|
||||
const double z = m_no_sparse_layers ? (m_current_height + m_layer_info->height) : m_layer_info->z;
|
||||
const double z = m_sparse_layers_skipped ? (m_current_height + m_layer_info->height) : m_layer_info->z;
|
||||
const double r = std::tan(Geometry::deg2rad(m_wipe_tower_cone_angle / 2.f)) * (m_wipe_tower_height - z);
|
||||
const double w = m_layer_info->depth + m_perimeter_width;
|
||||
if (r > 0.5 * w + 0.01) { // same guard as generate_support_cone_wall
|
||||
@@ -1872,7 +1872,7 @@ void WipeTower2::toolchange_Wipe(
|
||||
// All the calculations in all other places take the spacing into account for all the layers.
|
||||
|
||||
// If spare layers are excluded->if 1 or less toolchange has been done, it must be sill the first layer, too.So slow down.
|
||||
const float target_speed = is_first_layer() || (m_num_tool_changes <= 1 && m_no_sparse_layers) ? m_first_layer_speed * 60.f : std::min(m_wipe_tower_max_purge_speed * 60.f, m_infill_speed * 60.f);
|
||||
const float target_speed = is_first_layer() || (m_num_tool_changes <= 1 && m_sparse_layers_skipped) ? m_first_layer_speed * 60.f : std::min(m_wipe_tower_max_purge_speed * 60.f, m_infill_speed * 60.f);
|
||||
float wipe_speed = 0.33f * target_speed;
|
||||
|
||||
// if there is less than 2.5*line_width to the edge, advance straightaway (there is likely a blob anyway)
|
||||
@@ -1970,7 +1970,7 @@ WipeTower::ToolChangeResult WipeTower2::finish_layer()
|
||||
|
||||
// Slow down on the 1st layer.
|
||||
// If spare layers are excluded -> if 1 or less toolchange has been done, it must be still the first layer, too. So slow down.
|
||||
bool first_layer = is_first_layer() || (m_num_tool_changes <= 1 && m_no_sparse_layers);
|
||||
bool first_layer = is_first_layer() || (m_num_tool_changes <= 1 && m_sparse_layers_skipped);
|
||||
float feedrate = first_layer ? m_first_layer_speed * 60.f : std::min(m_wipe_tower_max_purge_speed * 60.f, m_infill_speed * 60.f);
|
||||
if (m_enable_tower_interface_features && m_prev_layer_had_interface)
|
||||
feedrate = std::min(feedrate, 20.f * 60.f);
|
||||
@@ -2103,7 +2103,7 @@ WipeTower::ToolChangeResult WipeTower2::finish_layer()
|
||||
|
||||
// Ask our writer about how much material was consumed.
|
||||
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
|
||||
if (! m_no_sparse_layers || toolchanges_on_layer || first_layer) {
|
||||
if (! m_sparse_layers_skipped || toolchanges_on_layer || first_layer) {
|
||||
if (m_current_tool < m_used_filament_length.size())
|
||||
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
|
||||
m_current_height += m_layer_info->height;
|
||||
@@ -2226,7 +2226,7 @@ void WipeTower2::plan_toolchange(float z_par, float layer_height_par, unsigned i
|
||||
if (m_plan.empty() || m_plan.back().z + WT_EPSILON < z_par) // if we moved to a new layer, we'll add it to m_plan first
|
||||
m_plan.push_back(WipeTowerInfo(z_par, layer_height_par));
|
||||
|
||||
if (m_first_layer_idx == size_t(-1) && (! m_no_sparse_layers || old_tool != new_tool || m_plan.size() == 1))
|
||||
if (m_first_layer_idx == size_t(-1) && (! m_sparse_layers_skipped || old_tool != new_tool || m_plan.size() == 1))
|
||||
m_first_layer_idx = m_plan.size() - 1;
|
||||
|
||||
if (old_tool == new_tool) // new layer without toolchanges - we are done
|
||||
@@ -2652,7 +2652,7 @@ Polygon WipeTower2::generate_support_cone_wall(
|
||||
const auto [R, support_scale] = get_wipe_tower_cone_base(m_wipe_tower_width, m_wipe_tower_height, m_wipe_tower_depth,
|
||||
m_wipe_tower_cone_angle);
|
||||
|
||||
double z = m_no_sparse_layers ?
|
||||
double z = m_sparse_layers_skipped ?
|
||||
(m_current_height + m_layer_info->height) :
|
||||
m_layer_info->z; // the former should actually work in both cases, but let's stay on the safe side (the 2.6.0 is close)
|
||||
|
||||
|
||||
@@ -267,7 +267,7 @@ private:
|
||||
float m_parking_pos_retraction = 0.f;
|
||||
float m_extra_loading_move = 0.f;
|
||||
float m_bridging = 0.f;
|
||||
bool m_no_sparse_layers = false;
|
||||
bool m_sparse_layers_skipped = false;
|
||||
bool m_set_extruder_trimpot = false;
|
||||
bool m_adhesion = true;
|
||||
GCodeFlavor m_gcode_flavor;
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
#include "LayOnFace.hpp"
|
||||
|
||||
#include "Geometry.hpp"
|
||||
#include "Geometry/ConvexHull.hpp"
|
||||
#include "Model.hpp"
|
||||
#include "TriangleMesh.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <numeric>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
std::vector<LayOnFacePlane> lay_on_face_planes(const ModelObject &object, const Transform3d &inst_matrix)
|
||||
{
|
||||
// An object can only rest on its convex hull, so candidate faces are taken from the hull of all model parts.
|
||||
TriangleMesh ch;
|
||||
for (const ModelVolume* vol : object.volumes) {
|
||||
if (vol->type() != ModelVolumeType::MODEL_PART)
|
||||
continue;
|
||||
TriangleMesh vol_ch = vol->get_convex_hull();
|
||||
vol_ch.transform(vol->get_matrix());
|
||||
ch.merge(vol_ch);
|
||||
}
|
||||
ch = ch.convex_hull_3d();
|
||||
std::vector<LayOnFacePlane> planes;
|
||||
|
||||
// Following constants are used for discarding too small polygons.
|
||||
const float minimal_area = 5.f; // in square mm (world coordinates)
|
||||
const float minimal_side = 1.f; // mm
|
||||
const float minimal_angle = 1.f; // degree, initial value was 10, but cause bugs
|
||||
|
||||
// Now we'll go through all the facets and append Points of facets sharing the same normal.
|
||||
// This part is still performed in mesh coordinate system.
|
||||
const int num_of_facets = ch.facets_count();
|
||||
const std::vector<Vec3f> face_normals = its_face_normals(ch.its);
|
||||
const std::vector<Vec3i32> face_neighbors = its_face_neighbors(ch.its);
|
||||
std::vector<int> facet_queue(num_of_facets, 0);
|
||||
std::vector<bool> facet_visited(num_of_facets, false);
|
||||
int facet_queue_cnt = 0;
|
||||
const stl_normal* normal_ptr = nullptr;
|
||||
int facet_idx = 0;
|
||||
while (1) {
|
||||
// Find next unvisited triangle:
|
||||
for (; facet_idx < num_of_facets; ++ facet_idx)
|
||||
if (!facet_visited[facet_idx]) {
|
||||
facet_queue[facet_queue_cnt ++] = facet_idx;
|
||||
facet_visited[facet_idx] = true;
|
||||
normal_ptr = &face_normals[facet_idx];
|
||||
planes.emplace_back();
|
||||
break;
|
||||
}
|
||||
if (facet_idx == num_of_facets)
|
||||
break; // Everything was visited already
|
||||
|
||||
while (facet_queue_cnt > 0) {
|
||||
int facet_idx = facet_queue[-- facet_queue_cnt];
|
||||
const stl_normal& this_normal = face_normals[facet_idx];
|
||||
if (std::abs(this_normal(0) - (*normal_ptr)(0)) < 0.001 && std::abs(this_normal(1) - (*normal_ptr)(1)) < 0.001 && std::abs(this_normal(2) - (*normal_ptr)(2)) < 0.001) {
|
||||
const Vec3i32 face = ch.its.indices[facet_idx];
|
||||
for (int j=0; j<3; ++j)
|
||||
planes.back().outline.emplace_back(ch.its.vertices[face[j]].cast<double>());
|
||||
|
||||
facet_visited[facet_idx] = true;
|
||||
for (int j = 0; j < 3; ++ j)
|
||||
if (int neighbor_idx = face_neighbors[facet_idx][j]; neighbor_idx >= 0 && ! facet_visited[neighbor_idx])
|
||||
facet_queue[facet_queue_cnt ++] = neighbor_idx;
|
||||
}
|
||||
}
|
||||
planes.back().normal = normal_ptr->cast<double>();
|
||||
|
||||
Pointf3s& verts = planes.back().outline;
|
||||
// Now we'll transform all the points into world coordinates, so that the areas, angles and distances
|
||||
// make real sense.
|
||||
verts = transform(verts, inst_matrix);
|
||||
|
||||
// if this is a just a very small triangle, remove it to speed up further calculations (it would be rejected later anyway):
|
||||
if (verts.size() == 3 &&
|
||||
((verts[0] - verts[1]).norm() < minimal_side
|
||||
|| (verts[0] - verts[2]).norm() < minimal_side
|
||||
|| (verts[1] - verts[2]).norm() < minimal_side))
|
||||
planes.pop_back();
|
||||
}
|
||||
|
||||
// Let's prepare transformation of the normal vector from mesh to instance coordinates.
|
||||
const Matrix3d normal_matrix = inst_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
|
||||
|
||||
// Now we'll go through all the polygons, transform the points into xy plane to process them:
|
||||
for (unsigned int polygon_id=0; polygon_id < planes.size(); ++polygon_id) {
|
||||
Pointf3s& polygon = planes[polygon_id].outline;
|
||||
const Vec3d& normal = planes[polygon_id].normal;
|
||||
|
||||
// transform the normal according to the instance matrix:
|
||||
const Vec3d normal_transformed = normal_matrix * normal;
|
||||
|
||||
// We are going to rotate about z and y to flatten the plane
|
||||
Eigen::Quaterniond q;
|
||||
Transform3d& m = planes[polygon_id].to_plane_frame;
|
||||
m = Transform3d::Identity();
|
||||
m.matrix().block(0, 0, 3, 3) = q.setFromTwoVectors(normal_transformed, Vec3d::UnitZ()).toRotationMatrix();
|
||||
polygon = transform(polygon, m);
|
||||
|
||||
// Now to remove the inner points. We'll misuse Geometry::convex_hull for that, but since
|
||||
// it works in fixed point representation, we will rescale the polygon to avoid overflows.
|
||||
// And yes, it is a nasty thing to do. Whoever has time is free to refactor.
|
||||
Vec3d bb_size = BoundingBoxf3(polygon).size();
|
||||
float sf = std::min(1./bb_size(0), 1./bb_size(1));
|
||||
Transform3d tr = Geometry::scale_transform({ sf, sf, 1.f });
|
||||
polygon = transform(polygon, tr);
|
||||
polygon = Slic3r::Geometry::convex_hull(polygon);
|
||||
polygon = transform(polygon, tr.inverse());
|
||||
|
||||
// Calculate area of the polygons and discard ones that are too small
|
||||
float& area = planes[polygon_id].area;
|
||||
area = 0.f;
|
||||
for (unsigned int i = 0; i < polygon.size(); i++) // Shoelace formula
|
||||
area += polygon[i](0)*polygon[i + 1 < polygon.size() ? i + 1 : 0](1) - polygon[i + 1 < polygon.size() ? i + 1 : 0](0)*polygon[i](1);
|
||||
area = 0.5f * std::abs(area);
|
||||
|
||||
bool discard = false;
|
||||
if (area < minimal_area)
|
||||
discard = true;
|
||||
else {
|
||||
// We also check the inner angles and discard polygons with angles smaller than the following threshold
|
||||
const double angle_threshold = ::cos(minimal_angle * (double)PI / 180.0);
|
||||
|
||||
for (unsigned int i = 0; i < polygon.size(); ++i) {
|
||||
const Vec3d& prec = polygon[(i == 0) ? polygon.size() - 1 : i - 1];
|
||||
const Vec3d& curr = polygon[i];
|
||||
const Vec3d& next = polygon[(i == polygon.size() - 1) ? 0 : i + 1];
|
||||
|
||||
if ((prec - curr).normalized().dot((next - curr).normalized()) > angle_threshold) {
|
||||
discard = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (discard) {
|
||||
planes[polygon_id--] = std::move(planes.back());
|
||||
planes.pop_back();
|
||||
continue;
|
||||
}
|
||||
|
||||
const Vec3d centroid = std::accumulate(polygon.begin(), polygon.end(), Vec3d(0.0, 0.0, 0.0)) / double(polygon.size());
|
||||
planes[polygon_id].center = inst_matrix.inverse() * (m.inverse() * centroid);
|
||||
}
|
||||
|
||||
std::sort(planes.rbegin(), planes.rend(), [](const LayOnFacePlane& a, const LayOnFacePlane& b) { return a.area < b.area; });
|
||||
return planes;
|
||||
}
|
||||
|
||||
int find_largest_plane(const std::vector<LayOnFacePlane> &planes)
|
||||
{
|
||||
// The plane frame maps the instance normal to +Z, so the normal's z in instance coordinates is element (2, 2).
|
||||
auto downward = [](const LayOnFacePlane &plane) { return -plane.to_plane_frame.linear()(2, 2); };
|
||||
// Areas are floats from rounded geometry, so faces within 0.1% count as equal.
|
||||
int best = -1;
|
||||
for (size_t i = 0; i < planes.size() && planes[i].area >= planes.front().area * (1. - 1e-3); ++i)
|
||||
if (best < 0 || downward(planes[i]) > downward(planes[best]))
|
||||
best = int(i);
|
||||
return best;
|
||||
}
|
||||
|
||||
int find_plane_by_normal(const std::vector<LayOnFacePlane> &planes, const Vec3d &direction)
|
||||
{
|
||||
const Vec3d dir = direction.normalized();
|
||||
int best = -1;
|
||||
double best_dot = -2.;
|
||||
for (size_t i = 0; i < planes.size(); ++i)
|
||||
if (const double dot = planes[i].normal.dot(dir); dot > best_dot) {
|
||||
best_dot = dot;
|
||||
best = int(i);
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
int find_plane_at_point(const std::vector<LayOnFacePlane> &planes, const Transform3d &instance_matrix_no_offset,
|
||||
const Vec3d &point, double tolerance)
|
||||
{
|
||||
const Vec3d instance_point = instance_matrix_no_offset * point;
|
||||
for (size_t i = 0; i < planes.size(); ++i) {
|
||||
const Pointf3s &outline = planes[i].outline;
|
||||
if (outline.empty())
|
||||
continue;
|
||||
const Vec3d p = planes[i].to_plane_frame * instance_point;
|
||||
// Facets with slightly different normals are merged into one face, so the outline is not exactly flat.
|
||||
const double z = std::accumulate(outline.begin(), outline.end(), 0., [](double sum, const Vec3d &v) { return sum + v.z(); }) / double(outline.size());
|
||||
if (std::abs(p.z() - z) > tolerance)
|
||||
continue;
|
||||
// The outline is convex: the point is inside when it is not on both sides of its edges.
|
||||
bool left = false, right = false;
|
||||
for (size_t j = 0; j < outline.size(); ++j) {
|
||||
const Vec2d a = outline[j].head<2>();
|
||||
const Vec2d edge = outline[(j + 1) % outline.size()].head<2>() - a;
|
||||
const double len = edge.norm();
|
||||
if (len < EPSILON)
|
||||
continue;
|
||||
const double side = cross2(edge, Vec2d(p.head<2>() - a)) / len;
|
||||
left |= side > tolerance;
|
||||
right |= side < -tolerance;
|
||||
}
|
||||
if (!(left && right))
|
||||
return int(i);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void lay_on_face(ModelObject &object, size_t instance_idx, const Vec3d &normal)
|
||||
{
|
||||
ModelInstance &instance = *object.instances[instance_idx];
|
||||
const Geometry::Transformation &trafo = instance.get_transformation();
|
||||
// Same rotation as Selection::flattening_rotate(): turn the transformed normal to point down.
|
||||
const Vec3d tnormal = trafo.get_matrix().matrix().block(0, 0, 3, 3).inverse().transpose() * normal;
|
||||
const Transform3d rotation = Transform3d(Eigen::Quaterniond().setFromTwoVectors(tnormal, -Vec3d::UnitZ()));
|
||||
instance.set_transformation(Geometry::Transformation(trafo.get_offset_matrix() * rotation * trafo.get_matrix_no_offset()));
|
||||
// Drop this instance only: ensure_on_bed() skips instances without auto_drop and measures the first instance.
|
||||
object.translate_instance(instance_idx, -object.instance_bounding_box(instance_idx).min.z() * Vec3d::UnitZ());
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "Point.hpp"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class ModelObject;
|
||||
|
||||
// A face of an object's convex hull that the object can rest on. These are the faces the
|
||||
// "Lay on Face" gizmo offers and the ones the CLI --ground-* options choose from.
|
||||
//
|
||||
// Frames: "object" coordinates have the volume transformations applied but not the instance
|
||||
// transformation. "Instance" coordinates additionally have the instance rotation, scale and
|
||||
// mirror applied, but not its offset.
|
||||
struct LayOnFacePlane
|
||||
{
|
||||
Vec3d normal; // outward unit normal, object coordinates
|
||||
Vec3d center; // centroid of the outline, object coordinates; on the face's mean plane
|
||||
float area; // mm², instance coordinates
|
||||
Pointf3s outline; // convex outline in the plane frame, where the face is horizontal
|
||||
Transform3d to_plane_frame; // rotation from instance coordinates to the plane frame
|
||||
};
|
||||
|
||||
// Candidate faces of the object's model parts, largest first. The instance transformation
|
||||
// (without offset) is applied before measuring, so faces too small to rest on are dropped
|
||||
// by their printed size: under 5 mm², a side under 1 mm, or an inner angle under 1°.
|
||||
std::vector<LayOnFacePlane> lay_on_face_planes(const ModelObject &object, const Transform3d &instance_matrix_no_offset);
|
||||
|
||||
// Index of the largest plane, or -1 if `planes` is empty. Of planes with the same area, such as
|
||||
// the top and bottom of a box, the one already facing down the most wins, so flat parts stay put.
|
||||
int find_largest_plane(const std::vector<LayOnFacePlane> &planes);
|
||||
|
||||
// Index of the plane whose normal is closest to `direction` (object coordinates),
|
||||
// or -1 if `planes` is empty.
|
||||
int find_plane_by_normal(const std::vector<LayOnFacePlane> &planes, const Vec3d &direction);
|
||||
|
||||
// Index of the plane whose face contains `point` (object coordinates) within `tolerance` mm, or -1
|
||||
// if there is none. `instance_matrix_no_offset` is the one the planes were computed with.
|
||||
int find_plane_at_point(const std::vector<LayOnFacePlane> &planes, const Transform3d &instance_matrix_no_offset,
|
||||
const Vec3d &point, double tolerance);
|
||||
|
||||
// Rotates the instance so that `normal` (object coordinates) points down, the same rotation as
|
||||
// the gizmo applies, then drops the instance so its lowest point is at z = 0.
|
||||
void lay_on_face(ModelObject &object, size_t instance_idx, const Vec3d &normal);
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -153,6 +153,7 @@ bool Layer::is_perimeter_compatible(const Print& print, const PrintRegion& a, co
|
||||
&& config.gap_infill_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id)) == other_config.gap_infill_speed.get_at(print.get_extruder_id(config.outer_wall_filament_id))
|
||||
&& config.filter_out_gap_fill.value == other_config.filter_out_gap_fill.value
|
||||
&& config.detect_overhang_wall == other_config.detect_overhang_wall
|
||||
&& config.unsupported_wall_last == other_config.unsupported_wall_last
|
||||
&& config.overhang_reverse == other_config.overhang_reverse
|
||||
&& config.overhang_reverse_threshold == other_config.overhang_reverse_threshold
|
||||
&& config.wall_direction == other_config.wall_direction
|
||||
|
||||
@@ -108,6 +108,8 @@ Model& Model::assign_copy(const Model &rhs)
|
||||
this->md_value = rhs.md_value;
|
||||
this->texture_mesh = rhs.texture_mesh;
|
||||
|
||||
this->cad_recipe = rhs.cad_recipe;
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -152,6 +154,7 @@ Model& Model::assign_copy(Model &&rhs)
|
||||
rhs.model_info.reset();
|
||||
this->profile_info = rhs.profile_info;
|
||||
rhs.profile_info.reset();
|
||||
this->cad_recipe = std::move(rhs.cad_recipe);
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
@@ -1569,6 +1569,10 @@ public:
|
||||
std::vector<std::string> md_name;
|
||||
std::vector<std::string> md_value;
|
||||
|
||||
// Opaque parametric CAD recipe (CadDocument::serialize_recipe()), round-tripped through
|
||||
// the 3MF as Metadata/orca_cad.bin. Empty for non-CAD projects.
|
||||
std::string cad_recipe;
|
||||
|
||||
void SetDesigner(std::string designer, std::string designer_user_id) {
|
||||
if (design_info == nullptr) {
|
||||
design_info = std::make_shared<ModelDesignInfo>();
|
||||
|
||||
@@ -550,6 +550,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
|
||||
if (!paths.empty()) {
|
||||
if (extrusion->is_closed) {
|
||||
ExtrusionLoop extrusion_loop(std::move(paths), pg_extrusion.is_contour ? elrDefault : elrHole);
|
||||
extrusion_loop.inset_idx = extrusion->inset_idx;
|
||||
if ((perimeter_generator.config->wall_direction == WallDirection::CounterClockwise) ==
|
||||
(pg_extrusion.is_contour || pg_extrusions.size() == 2))
|
||||
extrusion_loop.make_counter_clockwise();
|
||||
@@ -1318,6 +1319,73 @@ static void reorient_perimeters(ExtrusionEntityCollection &entities, bool steep_
|
||||
}
|
||||
}
|
||||
|
||||
// A loop made of nothing but overhang paths lies entirely off the lower layer.
|
||||
static bool is_unsupported_loop(const ExtrusionEntity *entity)
|
||||
{
|
||||
if (!entity->is_loop())
|
||||
return false;
|
||||
const ExtrusionPaths &paths = static_cast<const ExtrusionLoop *>(entity)->paths;
|
||||
return !paths.empty() && std::all_of(paths.begin(), paths.end(),
|
||||
[](const ExtrusionPath &path) { return path.role() == erOverhangPerimeter; });
|
||||
}
|
||||
|
||||
// ORCA: A wall loop with nothing under it has nothing to lean on, so whatever the configured wall
|
||||
// sequence it is extruded after the loops that anchor it, innermost first. A loop that runs alongside
|
||||
// an anchored one belongs to the same wall stack and keeps its place ahead of the infill, which needs
|
||||
// it as an anchor; one that touches nothing has only that infill to rest on, so it is flagged for the
|
||||
// G-code writer to hold it back until the infill is down.
|
||||
static void defer_unsupported_loops(const PerimeterGenerator &perimeter_generator, ExtrusionEntityCollection &entities)
|
||||
{
|
||||
if (!perimeter_generator.config->unsupported_wall_last)
|
||||
return;
|
||||
|
||||
ExtrusionEntitiesPtr &src = entities.entities;
|
||||
auto first_deferred = std::stable_partition(src.begin(), src.end(),
|
||||
[](const ExtrusionEntity *entity) { return !is_unsupported_loop(entity); });
|
||||
if (first_deferred == src.end())
|
||||
return;
|
||||
|
||||
std::stable_sort(first_deferred, src.end(),
|
||||
[](const ExtrusionEntity *lhs, const ExtrusionEntity *rhs) { return lhs->inset_idx > rhs->inset_idx; });
|
||||
|
||||
auto collect_lines = [](const ExtrusionEntity *entity, Lines &out) {
|
||||
Polylines polylines;
|
||||
entity->collect_polylines(polylines);
|
||||
append(out, to_lines(polylines));
|
||||
};
|
||||
|
||||
Lines anchored;
|
||||
for (auto it = src.begin(); it != first_deferred; ++it)
|
||||
collect_lines(*it, anchored);
|
||||
|
||||
std::vector<ExtrusionLoop *> unattached;
|
||||
for (auto it = first_deferred; it != src.end(); ++it)
|
||||
unattached.emplace_back(static_cast<ExtrusionLoop *>(*it));
|
||||
|
||||
// A loop leaning on a loop that is itself anchored is anchored as well, so spread outwards from
|
||||
// the anchored loops until no unsupported loop is left touching what was reached.
|
||||
const double touch_distance = 1.5 * std::max(perimeter_generator.ext_perimeter_flow.scaled_spacing(),
|
||||
perimeter_generator.perimeter_flow.scaled_spacing());
|
||||
while (!anchored.empty()) {
|
||||
AABBTreeLines::LinesDistancer<Line> distancer{std::move(anchored)};
|
||||
anchored.clear();
|
||||
for (ExtrusionLoop *&loop : unattached) {
|
||||
if (loop == nullptr)
|
||||
continue;
|
||||
const Points points = loop->as_polyline().points;
|
||||
if (std::any_of(points.begin(), points.end(),
|
||||
[&distancer, touch_distance](const Point &point) { return distancer.distance_from_lines<false>(point) < touch_distance; })) {
|
||||
collect_lines(loop, anchored);
|
||||
loop = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (ExtrusionLoop *loop : unattached)
|
||||
if (loop != nullptr)
|
||||
loop->print_after_infill = true;
|
||||
}
|
||||
|
||||
void PerimeterGenerator::process_classic()
|
||||
{
|
||||
group_region_by_fuzzify(*this);
|
||||
@@ -1804,6 +1872,8 @@ void PerimeterGenerator::process_classic()
|
||||
}
|
||||
}
|
||||
|
||||
defer_unsupported_loops(*this, entities);
|
||||
|
||||
// append perimeters for this slice as a collection
|
||||
if (! entities.empty())
|
||||
this->loops->append(entities);
|
||||
@@ -2742,6 +2812,7 @@ void PerimeterGenerator::process_arachne()
|
||||
reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole,
|
||||
this->config->overhang_reverse_internal_only);
|
||||
}
|
||||
defer_unsupported_loops(*this, extrusion_coll);
|
||||
this->loops->append(extrusion_coll);
|
||||
}
|
||||
|
||||
|
||||
@@ -545,7 +545,7 @@ std::string generate_preset_setting_id(const std::string& vendor, const std::str
|
||||
return "";
|
||||
|
||||
// Dedicated namespace for preset setting_ids, distinct from the cloud per-user
|
||||
// namespace (OrcaCloudServiceAgent). Keep in sync with scripts/orca_id_tool.py;
|
||||
// namespace (OrcaCloudServiceAgent). Keep in sync with scripts/orca_profile_tool.py;
|
||||
// never change this constant.
|
||||
static const boost::uuids::uuid vendor_namespace =
|
||||
boost::uuids::string_generator()("c1f4d9e2-7a3b-5c8d-9e0f-1a2b3c4d5e6f");
|
||||
@@ -1058,6 +1058,7 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"reduce_crossing_wall",
|
||||
"detect_thin_wall",
|
||||
"detect_overhang_wall",
|
||||
"unsupported_wall_last",
|
||||
"overhang_reverse",
|
||||
"overhang_reverse_threshold",
|
||||
"overhang_reverse_internal_only",
|
||||
@@ -1320,6 +1321,8 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"wipe_tower_extra_flow",
|
||||
"single_extruder_multi_material_priming",
|
||||
"toolchange_ordering",
|
||||
"toolchange_cyclic_order",
|
||||
"toolchange_cyclic_first_layer",
|
||||
"wipe_tower_rotation_angle",
|
||||
"tree_support_branch_distance_organic",
|
||||
"tree_support_branch_diameter_organic",
|
||||
@@ -1445,7 +1448,7 @@ static std::vector<std::string> s_Preset_printer_options {
|
||||
"gcode_skip_config_block", "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs",
|
||||
"single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode",
|
||||
"printer_model", "printer_variant", "printer_extruder_id", "printer_extruder_variant", "extruder_variant_list", "default_nozzle_volume_type",
|
||||
"printable_height", "extruder_printable_height", "extruder_clearance_radius", "extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod",
|
||||
"printable_height", "extruder_printable_height", "extruder_clearance_radius", "extruder_clearance_height_to_lid", "extruder_clearance_height_to_rod", "extruder_clearance_dist_to_rod",
|
||||
"nozzle_height", "master_extruder_id",
|
||||
"default_print_profile", "inherits",
|
||||
"silent_mode",
|
||||
|
||||
@@ -93,8 +93,8 @@ class PresetBundle;
|
||||
|
||||
// Deterministic preset setting_id: uuid5(vendor/type/name) -> 16 base62 chars.
|
||||
// Pure function of a system preset's identity, so the value can be assigned by
|
||||
// scripts/orca_id_tool.py and recomputed here when a profile ships without it.
|
||||
// MUST stay byte-identical to scripts/orca_id_tool.py.
|
||||
// scripts/orca_profile_tool.py and recomputed here when a profile ships without it.
|
||||
// MUST stay byte-identical to scripts/orca_profile_tool.py.
|
||||
// This is NOT the per-user cloud-sync setting_id
|
||||
// (OrcaCloudServiceAgent::generate_uuid_for_setting_id) - do not conflate them.
|
||||
std::string generate_preset_setting_id(const std::string& vendor,
|
||||
|
||||
@@ -6783,7 +6783,7 @@ std::string PresetBundle::load_vendor_preset(
|
||||
loaded.description = entry.description;
|
||||
loaded.setting_id = entry.setting_id;
|
||||
// Derive the preset setting_id on the fly when a profile ships without one,
|
||||
// matching scripts/orca_id_tool.py. Only instantiated presets carry an id;
|
||||
// matching scripts/orca_profile_tool.py. Only instantiated presets carry an id;
|
||||
// non-instantiated base profiles return earlier above. This never
|
||||
// touches the per-user cloud-sync setting_id written into user .info files.
|
||||
if (loaded.setting_id.empty() && entry.instantiation == "true")
|
||||
@@ -7619,6 +7619,9 @@ bool PresetBundle::has_errors(bool check_duplicate_filament_subtypes) const
|
||||
if (this->check_preset_references())
|
||||
has_errors = true;
|
||||
|
||||
if (this->check_printer_default_materials())
|
||||
has_errors = true;
|
||||
|
||||
return has_errors;
|
||||
}
|
||||
|
||||
@@ -7711,6 +7714,70 @@ bool PresetBundle::check_preset_references() const
|
||||
return found;
|
||||
}
|
||||
|
||||
bool PresetBundle::check_printer_default_materials() const
|
||||
{
|
||||
bool found = false;
|
||||
// A model's default_materials list is shared by its variants, so report each unknown name once.
|
||||
std::set<const VendorProfile::PrinterModel *> checked_models;
|
||||
// default_filament_profile is inherited from shared base machine presets, so one bad name can
|
||||
// surface on many variants; report it once, at the first printer that names it.
|
||||
std::set<std::string> reported_unknown_profiles;
|
||||
for (const Preset &printer : printers) {
|
||||
if (!printer.is_system || printer.vendor == nullptr || printer.printer_technology() != ptFFF)
|
||||
continue;
|
||||
|
||||
const VendorProfile::PrinterModel *model = PresetUtils::system_printer_model(printer);
|
||||
const PresetWithVendorProfile active_printer = printers.get_preset_with_vendor_profile(printer);
|
||||
// Use the same name lookup as load_installed_filaments, not UI aliases or fuzzy matching.
|
||||
// A model's defaults can cover different nozzles, but at least one must cover this variant.
|
||||
const bool has_default = model != nullptr && std::any_of(model->default_materials.begin(), model->default_materials.end(),
|
||||
[&](const std::string &name) {
|
||||
const Preset *filament = filaments.find_preset(name, false);
|
||||
return filament != nullptr && filament->is_system &&
|
||||
is_compatible_with_printer(filaments.get_preset_with_vendor_profile(*filament), active_printer);
|
||||
});
|
||||
if (!has_default) {
|
||||
found = true;
|
||||
BOOST_LOG_TRIVIAL(error) << "Printer preset \"" << printer.name << "\" (vendor \"" << printer.vendor->name
|
||||
<< "\", model \"" << printer.config.opt_string("printer_model") << "\", variant \""
|
||||
<< printer.config.opt_string("printer_variant")
|
||||
<< "\") has no compatible system filament in its model's \"default_materials\". "
|
||||
"Add at least one full filament preset name compatible with this printer variant:\n"
|
||||
<< preset_file_uri(printer.file);
|
||||
}
|
||||
|
||||
if (model != nullptr && checked_models.insert(model).second) {
|
||||
for (const std::string &name : model->default_materials) {
|
||||
const Preset *filament = filaments.find_preset(name, false);
|
||||
if (filament == nullptr || !filament->is_system) {
|
||||
found = true;
|
||||
BOOST_LOG_TRIVIAL(error) << "Printer model \"" << model->name << "\" (vendor \"" << printer.vendor->name
|
||||
<< "\") names the unknown system filament \"" << name
|
||||
<< "\" in its \"default_materials\":\n" << preset_file_uri(printer.file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (printer.config.has("default_filament_profile")) {
|
||||
for (const std::string &name : printer.config.opt<ConfigOptionStrings>("default_filament_profile")->values) {
|
||||
// A ";"-separated list can leave an empty trailing segment; formatting noise, not a name.
|
||||
if (name.empty())
|
||||
continue;
|
||||
const Preset *filament = filaments.find_preset(name, false);
|
||||
if ((filament == nullptr || !filament->is_system) && reported_unknown_profiles.insert(name).second) {
|
||||
found = true;
|
||||
BOOST_LOG_TRIVIAL(error) << "Printer preset \"" << printer.name << "\" (vendor \"" << printer.vendor->name
|
||||
<< "\", model \"" << printer.config.opt_string("printer_model") << "\", variant \""
|
||||
<< printer.config.opt_string("printer_variant")
|
||||
<< "\") names the unknown system filament \"" << name
|
||||
<< "\" in its \"default_filament_profile\":\n" << preset_file_uri(printer.file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
// Orca: a filament is matched from the AMS by (filament_id + printer compatibility).
|
||||
// For any one printer, at most one instantiated filament preset with a given
|
||||
// filament_id may be compatible - otherwise the AMS match is ambiguous and the
|
||||
|
||||
@@ -617,6 +617,11 @@ public:
|
||||
// compatible_prints references a deleted (unknown) or renamed (old) preset name.
|
||||
bool check_preset_references() const;
|
||||
|
||||
// Validator-only: every system FFF printer variant needs a compatible system filament
|
||||
// named in its model's default_materials, every name there and in the printer's
|
||||
// default_filament_profile must resolve to a system filament.
|
||||
bool check_printer_default_materials() const;
|
||||
|
||||
// Merge one vendor's presets with the other vendor's presets, report duplicates.
|
||||
// Public so per-vendor-cache consumers (e.g. the setup wizard) can assemble a
|
||||
// bundle out of several per-vendor caches loaded into separate PresetBundle instances.
|
||||
|
||||
@@ -360,6 +360,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|
||||
|| opt_key == "other_layers_print_sequence"
|
||||
|| opt_key == "other_layers_print_sequence_nums"
|
||||
|| opt_key == "toolchange_ordering"
|
||||
|| opt_key == "toolchange_cyclic_order"
|
||||
|| opt_key == "toolchange_cyclic_first_layer"
|
||||
|| opt_key == "extruder_ams_count"
|
||||
|| opt_key == "extruder_nozzle_stats"
|
||||
|| opt_key == "filament_map_mode"
|
||||
@@ -964,6 +966,377 @@ StringObjectException Print::sequential_print_clearance_valid(const Print &print
|
||||
return single_object_exception;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Clearance rule for a prime tower compacted by wipe_tower_no_sparse_layers.
|
||||
// Ported from BambuStudio and adapted to Orca's printer config: Orca has no
|
||||
// prime_tower_lift_height (z_hop alone bounds the spiral), spells the toolhead radius
|
||||
// extruder_clearance_radius, and derives the spiral slope from the per-filament travel_slope instead
|
||||
// of one global constant.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
double compacted_tower_footprint_padding(const PrintConfig &config, double brim_width)
|
||||
{
|
||||
// The brim is deposited material like any other and reaches past the wall on the first layer, so
|
||||
// the sweeping rod has to clear it too.
|
||||
//
|
||||
// On top of it, two effects make a nominal outline fall short of the printed tower on its low
|
||||
// corner even though it overshoots by millimetres on the high one: WipeTower re-centres the tower
|
||||
// by rib_offset once its first-layer wall is known, and the precise check hulls extrusion centre
|
||||
// lines, so the deposited material reaches half a line width further still. Allowing a line width
|
||||
// per side covers both, which is what keeps an estimated footprint enclosing the real one and the
|
||||
// pre-slice check stricter than the precise one.
|
||||
return std::max(0., brim_width) + 2. * config.nozzle_diameter.get_at(0);
|
||||
}
|
||||
|
||||
Polygons compacted_wipe_tower_rings(const CompactedTowerZone &zone, bool any_body_tier)
|
||||
{
|
||||
Polygons rings = zone.grown_nozzle;
|
||||
if (any_body_tier)
|
||||
append(rings, zone.grown_body);
|
||||
return rings;
|
||||
}
|
||||
|
||||
CompactedTowerZone compacted_wipe_tower_zone(const PrintConfig &config, const Polygon &tower_footprint)
|
||||
{
|
||||
CompactedTowerZone zone;
|
||||
if (tower_footprint.points.empty())
|
||||
return zone;
|
||||
|
||||
// Spiral Z-hop at wipe-tower entry (the G3 Z I J that GCodeWriter emits for a SpiralLift) starts on
|
||||
// the tower outline at a low Z. The spiral centre sits one radius away from the start point, so the
|
||||
// circle reaches 2 * radius beyond the outline. radius = lift / (2*pi*atan(travel_slope)) is the
|
||||
// same formula GCodeWriter uses; both are per filament, so take the widest any filament can make.
|
||||
double spiral_reach = 0.;
|
||||
for (size_t i = 0; i < config.z_hop.size(); ++i) {
|
||||
const double lift = std::min(double(config.z_hop.get_at(i)), 5.);
|
||||
if (lift < EPSILON)
|
||||
continue;
|
||||
const double slope = i < config.travel_slope.size() ? double(config.travel_slope.get_at(i)) : 0.;
|
||||
if (slope < EPSILON)
|
||||
continue;
|
||||
spiral_reach = std::max(spiral_reach, 2. * lift / (2. * PI * std::atan(slope)));
|
||||
}
|
||||
|
||||
// Working footprint = outline grown by the spiral envelope. All later clearance tests use this, so
|
||||
// a travel that leaves the deposited wall at low Z is still treated as part of the tower.
|
||||
zone.hull = tower_footprint;
|
||||
if (spiral_reach > EPSILON) {
|
||||
const Polygons grown = offset(tower_footprint, float(scale_(spiral_reach)), jtRound, scale_(0.1));
|
||||
if (! grown.empty())
|
||||
zone.hull = Geometry::convex_hull(grown);
|
||||
}
|
||||
|
||||
// The rod sweeps the whole X axis, so its keep-out band is the tower's Y span widened by half
|
||||
// the nozzle-to-rod offset per side (the instance carries the other half). Orca's sequential
|
||||
// check has no such margin, having had no option to read it from until now.
|
||||
zone.bbox_rod = zone.hull.bounding_box();
|
||||
zone.bbox_rod.offset(scale_(config.extruder_clearance_dist_to_rod.value * 0.5));
|
||||
|
||||
// Horizontal clearance, mirroring the sequential print check down to how the distance is split:
|
||||
// there each of the two object hulls grows by half of extruder_clearance_radius, so the two
|
||||
// outlines touch exactly when the objects are the full radius apart. Splitting it the same way
|
||||
// here (half on the tower, half on the instance in compacted_wipe_tower_clearance) states the
|
||||
// same criterion, and it is what lets the plater draw both outlines: they meet at the instant the
|
||||
// check trips, instead of one of them being already buried inside the other. The smaller
|
||||
// MAX_OUTER_NOZZLE_DIAMETER tier is the bare nozzle cone, the only part narrow enough to sit
|
||||
// beside an object rising less than nozzle_height. The 0.2 mm shaved off is the same rounding
|
||||
// slack the sequential check applies, 0.1 mm per side. Both rings are built here; which one a
|
||||
// given object is measured against depends on its own height and is decided in
|
||||
// compacted_wipe_tower_clearance().
|
||||
zone.body_radius = config.extruder_clearance_radius.value;
|
||||
zone.grown_body = offset(zone.hull, float(scale_(compacted_tower_half_clearance(zone.body_radius))), jtRound, scale_(0.1));
|
||||
zone.grown_nozzle = offset(zone.hull, float(scale_(compacted_tower_half_clearance(MAX_OUTER_NOZZLE_DIAMETER))), jtRound, scale_(0.1));
|
||||
return zone;
|
||||
}
|
||||
|
||||
CompactedTowerClearance compacted_wipe_tower_clearance(const PrintConfig &config, const CompactedTowerZone &zone,
|
||||
const Polygon &inst_hull, double object_rise)
|
||||
{
|
||||
BoundingBox inst_bbox = inst_hull.bounding_box();
|
||||
inst_bbox.offset(scale_(config.extruder_clearance_dist_to_rod.value * 0.5));
|
||||
|
||||
// Only the Y span matters for the rod: it spans the whole X axis, so an object sharing the tower's
|
||||
// Y band passes under it however far apart the two are in X.
|
||||
const bool overlaps_in_y = std::min(inst_bbox.max.y(), zone.bbox_rod.max.y()) - std::max(inst_bbox.min.y(), zone.bbox_rod.min.y()) > 0;
|
||||
|
||||
CompactedTowerClearance result;
|
||||
result.far_clearance = overlaps_in_y ? config.extruder_clearance_height_to_rod.value : config.extruder_clearance_height_to_lid.value;
|
||||
|
||||
// The rod and the lid are the only obstacles once the object stands far enough away. Closer than
|
||||
// the toolhead radius it is the head body itself that hits the object, and it does so as soon as
|
||||
// the object rises past the nozzle cone, which is far below the rod.
|
||||
// The instance carries the other half of each clearance, the tower rings already hold the first
|
||||
// half; see compacted_wipe_tower_zone(). Both halves are needed for the verdict to mean
|
||||
// "a full radius apart", and drawing what is tested is what keeps the plater honest.
|
||||
//
|
||||
// Which tier applies is a property of this object alone: the head body sits above the nozzle cone,
|
||||
// so it cannot reach an object that stays below nozzle_height however close it stands, and however
|
||||
// tall the rest of the plate is.
|
||||
const bool object_is_short = object_rise <= double(config.nozzle_height.value) + EPSILON;
|
||||
result.body_clearance = object_is_short ? double(MAX_OUTER_NOZZLE_DIAMETER) : zone.body_radius;
|
||||
|
||||
const Polygons inst_near_nozzle = offset(inst_hull, float(scale_(compacted_tower_half_clearance(MAX_OUTER_NOZZLE_DIAMETER))), jtRound, scale_(0.1));
|
||||
const bool near_nozzle = ! intersection(zone.grown_nozzle, inst_near_nozzle).empty();
|
||||
result.near_body = false;
|
||||
if (! object_is_short) {
|
||||
const Polygons inst_near_body = offset(inst_hull, float(scale_(compacted_tower_half_clearance(zone.body_radius))), jtRound, scale_(0.1));
|
||||
result.near_body = ! intersection(zone.grown_body, inst_near_body).empty();
|
||||
}
|
||||
|
||||
result.allowed_rise = result.far_clearance;
|
||||
if (near_nozzle)
|
||||
result.allowed_rise = 0.;
|
||||
else if (result.near_body)
|
||||
result.allowed_rise = std::min(result.far_clearance, double(config.nozzle_height.value));
|
||||
return result;
|
||||
}
|
||||
|
||||
Polygon compacted_wipe_tower_offender_outline(const Polygon &inst_hull, double body_clearance)
|
||||
{
|
||||
// Exactly the half-clearance the check grew this instance by, so the halo drawn around an object is
|
||||
// the very outline that was tested against the tower ring of the same tier. Passing the clearance
|
||||
// the object was actually judged on keeps a short object from being drawn with the wide ring it is
|
||||
// not subject to.
|
||||
const Polygons grown = offset(inst_hull, float(scale_(compacted_tower_half_clearance(body_clearance))), jtRound, scale_(0.1));
|
||||
return grown.empty() ? inst_hull : grown.front();
|
||||
}
|
||||
|
||||
// Shared user-facing message for every compacted-tower clearance failure. Height-limit and too-close
|
||||
// are the same class of layout violation under "No sparse layers", so they share one wording.
|
||||
static std::string compacted_wipe_tower_clearance_error()
|
||||
{
|
||||
return L("The relative position of the model and the prime tower does not meet the requirements of the \"No sparse layers\" feature. Please adjust their relative positions, lower the model height, or turn off \"No sparse layers\".");
|
||||
}
|
||||
|
||||
// Convex hull of one print instance in bed coordinates, the same outline both compacted tower checks
|
||||
// compare against the tower.
|
||||
static Polygon compacted_tower_print_instance_hull(const PrintObject &object, const PrintInstance &instance)
|
||||
{
|
||||
Points pts;
|
||||
for (const ModelVolume *v : object.model_object()->volumes) {
|
||||
if (! v->is_model_part())
|
||||
continue;
|
||||
Polygon hull = v->get_convex_hull_2d(Geometry::assemble_transform(Vec3d::Zero(), instance.model_instance->get_rotation(),
|
||||
instance.model_instance->get_scaling_factor(), instance.model_instance->get_mirror()));
|
||||
hull.translate(instance.shift - object.center_offset());
|
||||
append(pts, hull.points);
|
||||
}
|
||||
return pts.empty() ? Polygon() : Geometry::convex_hull(pts);
|
||||
}
|
||||
|
||||
// Footprint the compacted prime tower is expected to occupy on the plate, in bed coordinates.
|
||||
// Before psWipeTower has run there is no tower geometry at all, so this falls back to the same
|
||||
// estimate the plater builds its preview box from. Answering while the user is still arranging the
|
||||
// plate is the whole point of the pre-slice check, and an estimate is all that can be had then.
|
||||
static Polygon estimated_wipe_tower_footprint(const Print &print)
|
||||
{
|
||||
const PrintConfig &config = print.config();
|
||||
const size_t filaments_cnt = print.extruders().size();
|
||||
if (filaments_cnt == 0)
|
||||
return Polygon();
|
||||
|
||||
const WipeTowerData &wtd = print.wipe_tower_data(filaments_cnt);
|
||||
|
||||
double width, depth, brim;
|
||||
Vec2d local_min;
|
||||
if (wtd.bbx.size().x() > EPSILON && wtd.bbx.size().y() > EPSILON) {
|
||||
// The tower has already been generated once, so use its real box (brim included) instead of
|
||||
// re-estimating. Same frame first_layer_wipe_tower_corners() works in.
|
||||
width = wtd.bbx.size().x();
|
||||
depth = wtd.bbx.size().y();
|
||||
local_min = wtd.bbx.min + wtd.rib_offset.cast<double>();
|
||||
brim = 0.;
|
||||
} else {
|
||||
depth = wtd.depth;
|
||||
if (depth < EPSILON)
|
||||
return Polygon();
|
||||
// PartPlate::estimate_wipe_tower_size() squares the rib tower off and the preview box the user
|
||||
// drags around is built from that, so match it here rather than keeping the nominal width.
|
||||
width = config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib ? depth : double(config.prime_tower_width.value);
|
||||
local_min = Vec2d::Zero();
|
||||
brim = double(wtd.brim_width);
|
||||
}
|
||||
|
||||
const double padding = compacted_tower_footprint_padding(config, brim);
|
||||
local_min -= Vec2d(padding, padding);
|
||||
width += 2. * padding;
|
||||
depth += 2. * padding;
|
||||
|
||||
const Eigen::Rotation2Dd rot(Geometry::deg2rad(config.wipe_tower_rotation_angle.value));
|
||||
const Vec2d translate(config.wipe_tower_x.get_at(print.get_plate_index()) + print.get_plate_origin()(0),
|
||||
config.wipe_tower_y.get_at(print.get_plate_index()) + print.get_plate_origin()(1));
|
||||
|
||||
Polygon footprint;
|
||||
for (const Vec2d &corner : { local_min,
|
||||
Vec2d(local_min.x() + width, local_min.y()),
|
||||
Vec2d(local_min.x() + width, local_min.y() + depth),
|
||||
Vec2d(local_min.x(), local_min.y() + depth) }) {
|
||||
const Vec2d p = rot * corner + translate;
|
||||
footprint.points.emplace_back(scale_(p.x()), scale_(p.y()));
|
||||
}
|
||||
return footprint;
|
||||
}
|
||||
|
||||
// Pre-slice counterpart of validate_compacted_wipe_tower_clearance(). It applies the very same
|
||||
// clearance rule, but to an estimated tower footprint instead of the real tool-change extrusions,
|
||||
// which is what lets it run from Print::validate() before anything has been sliced. Reporting through
|
||||
// polygons / height_polygons rather than by throwing is what puts the collision area and the height
|
||||
// limit plane on the plater, exactly the way sequential printing does it.
|
||||
StringObjectException Print::compacted_wipe_tower_clearance_valid(const Print &print, Polygons *polygons, std::vector<std::pair<Polygon, float>> *height_polygons)
|
||||
{
|
||||
const PrintConfig &config = print.config();
|
||||
if (! wipe_tower_sparse_layers_skipped(config) || config.print_sequence != PrintSequence::ByLayer || ! print.has_wipe_tower())
|
||||
return {};
|
||||
|
||||
const CompactedTowerZone zone = compacted_wipe_tower_zone(config, estimated_wipe_tower_footprint(print));
|
||||
if (zone.empty())
|
||||
return {};
|
||||
|
||||
StringObjectException exception;
|
||||
Polygons offenders;
|
||||
bool body_tier_used = false;
|
||||
for (const PrintObject *object : print.objects()) {
|
||||
const double object_top = unscaled<double>(object->max_z());
|
||||
for (const PrintInstance &instance : object->instances()) {
|
||||
const Polygon inst_hull = compacted_tower_print_instance_hull(*object, instance);
|
||||
if (inst_hull.points.empty())
|
||||
continue;
|
||||
const CompactedTowerClearance clearance = compacted_wipe_tower_clearance(config, zone, inst_hull, object_top);
|
||||
body_tier_used = body_tier_used || compacted_tower_body_tier(clearance);
|
||||
// Every tier the precise check applies is applied here too, otherwise an object standing
|
||||
// within the toolhead radius would pass here and then be rejected mid-slice, which is the
|
||||
// one outcome this check exists to prevent. The compacted tower base is unknown before
|
||||
// slicing, so the rise is measured from the plate rather than from the tower top; that
|
||||
// overstates it by the tower's own height and makes this check err strict, never lax.
|
||||
if (object_top <= clearance.allowed_rise + EPSILON)
|
||||
continue;
|
||||
|
||||
// Height-limit and too-close cases share one user-facing message: both mean the layout
|
||||
// violates the "No sparse layers" clearance rule, and the remedies are the same.
|
||||
const std::string msg = compacted_wipe_tower_clearance_error();
|
||||
if (exception.string.empty()) {
|
||||
exception.string = msg;
|
||||
exception.object = instance.model_instance;
|
||||
} else {
|
||||
// Same wording for every offender; keep a single copy and drop the object pointer.
|
||||
exception.object = nullptr;
|
||||
}
|
||||
const Polygon outline = compacted_wipe_tower_offender_outline(inst_hull, clearance.body_clearance);
|
||||
offenders.emplace_back(outline);
|
||||
if (height_polygons)
|
||||
height_polygons->emplace_back(outline, float(clearance.allowed_rise));
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the tower's keep-out ring alongside the offending objects, so the collision area reads as
|
||||
// "this object reaches into the space the toolhead needs around the tower" rather than as a lone
|
||||
// highlighted object. Emitted only on a real collision; the plater discards polygons otherwise.
|
||||
// Only the rings some object on this plate is actually measured against are drawn, so that a ring
|
||||
// and an object outline touching always means that object is over its limit.
|
||||
if (polygons && ! offenders.empty()) {
|
||||
append(*polygons, compacted_wipe_tower_rings(zone, body_tier_used));
|
||||
append(*polygons, offenders);
|
||||
}
|
||||
return exception;
|
||||
}
|
||||
|
||||
// With wipe_tower_no_sparse_layers the tower only grows on layers that carry a real toolchange,
|
||||
// so it ends up far below the object and the nozzle has to descend to it. While the nozzle sits
|
||||
// down on the compacted tower the rod is at tower_z + extruder_clearance_height_to_rod, and it
|
||||
// sweeps the tower's Y band across the whole X axis. Anything already printed above that line and
|
||||
// sharing the band gets hit. Nearer than the toolhead radius the head body hits the object well before
|
||||
// the rod does, which is the horizontal half of the same problem. The spiral Z-hop that opens a wipe-
|
||||
// tower travel also leaves the extrusion outline at a low Z, so the footprint used here is the
|
||||
// deposited hull grown by the spiral circle's maximum reach. This mirrors both clearance checks of
|
||||
// sequential printing, except that the tower is revisited over and over, so every object is compared
|
||||
// against it.
|
||||
void Print::validate_compacted_wipe_tower_clearance() const
|
||||
{
|
||||
// Nothing to check when the tower is not compacted: it then follows the object as usual and the
|
||||
// regular by-layer clearance check already covers it. Asking wipe_tower_sparse_layers_skipped()
|
||||
// rather than the raw option keeps this from rejecting plates whose tower is in fact full height.
|
||||
if (! wipe_tower_sparse_layers_skipped(m_config) || m_config.print_sequence != PrintSequence::ByLayer)
|
||||
return;
|
||||
|
||||
const std::vector<std::vector<WipeTower::ToolChangeResult>> &tool_changes = m_wipe_tower_data.tool_changes;
|
||||
if (tool_changes.empty() || m_objects.empty())
|
||||
return;
|
||||
|
||||
// Same accumulation the G-code emitter runs, so validation and output cannot disagree.
|
||||
const std::vector<float> tower_z = compute_compacted_wipe_tower_z(tool_changes, float(m_config.z_offset.value));
|
||||
|
||||
// Wipe tower footprint: build it from the ACTUAL tool-change extrusions rather than the nominal
|
||||
// width x depth rectangle returned by first_layer_wipe_tower_corners(). With a rib wall the printed
|
||||
// wall bulges past the nominal box and the first-layer brim reaches even further; the nominal box
|
||||
// (m_wipe_tower_data.bbx) undercounts that outermost extent by several millimetres, which is
|
||||
// exactly the extent that decides how close the sweeping rod comes to a neighbouring object. The
|
||||
// extrusion end-points are stored in the wipe-tower local frame, so we map them to the bed frame
|
||||
// with the same transform the G-code emitter applies. The two emitters differ in where rib_offset
|
||||
// enters: WipeTowerIntegration::append_tcr() (type 1) rotates the point and then adds the offset,
|
||||
// append_tcr2() (type 2) adds it before rotating. On a rotated rib-wall tower the two land several
|
||||
// millimetres apart, which is exactly the margin this check measures, so follow the emitter in use.
|
||||
const Eigen::Rotation2Dd wt_rot(Geometry::deg2rad(m_config.wipe_tower_rotation_angle.value));
|
||||
const Vec2d wt_translate(m_config.wipe_tower_x.get_at(m_plate_index) + m_origin(0),
|
||||
m_config.wipe_tower_y.get_at(m_plate_index) + m_origin(1));
|
||||
const Vec2d rib_off = m_wipe_tower_data.rib_offset.cast<double>();
|
||||
const bool rib_off_rotates = this->wipe_tower_type() == WipeTowerType::Type2;
|
||||
auto to_bed = [&wt_rot, &wt_translate, &rib_off, rib_off_rotates](const Vec2d &pt) {
|
||||
return rib_off_rotates ? Vec2d(wt_rot * (pt + rib_off) + wt_translate) : Vec2d(wt_rot * pt + wt_translate + rib_off);
|
||||
};
|
||||
|
||||
Points tower_pts;
|
||||
for (const std::vector<WipeTower::ToolChangeResult> &layer : tool_changes) {
|
||||
if (layer.empty() || wipe_tower_layer_is_sparse(layer))
|
||||
continue;
|
||||
for (const WipeTower::ToolChangeResult &tcr : layer)
|
||||
for (size_t i = 0; i < tcr.extrusions.size(); ++i) {
|
||||
// A zero width marks a travel end-point. Keep it only when it opens a real extrusion, so
|
||||
// the hull covers the deposited material and nothing else; travels reach a bit further out
|
||||
// than the walls do.
|
||||
const WipeTower::Extrusion &e = tcr.extrusions[i];
|
||||
if (e.width == 0.f && (i + 1 == tcr.extrusions.size() || tcr.extrusions[i + 1].width == 0.f))
|
||||
continue;
|
||||
const Vec2d p = to_bed(Vec2d(e.pos.x(), e.pos.y()));
|
||||
tower_pts.emplace_back(scale_(p.x()), scale_(p.y()));
|
||||
}
|
||||
}
|
||||
if (tower_pts.empty())
|
||||
return;
|
||||
|
||||
const CompactedTowerZone zone = compacted_wipe_tower_zone(m_config, Geometry::convex_hull(tower_pts));
|
||||
if (zone.empty())
|
||||
return;
|
||||
|
||||
for (const PrintObject *object : m_objects) {
|
||||
const double object_top = unscaled<double>(object->max_z());
|
||||
for (const PrintInstance &instance : object->instances()) {
|
||||
const Polygon inst_hull = compacted_tower_print_instance_hull(*object, instance);
|
||||
if (inst_hull.points.empty())
|
||||
continue;
|
||||
|
||||
// Report the worst layer rather than the first offending one, it is the one that explains the
|
||||
// collision best. The rise has to be known before the clearance: it is what selects the
|
||||
// horizontal tier, the nozzle cone being out of the head body's reach.
|
||||
double max_rise = 0.;
|
||||
for (size_t i = 0; i < tool_changes.size(); ++i) {
|
||||
if (tool_changes[i].empty() || wipe_tower_layer_is_sparse(tool_changes[i]))
|
||||
continue;
|
||||
// Nothing above the current layer exists yet, so a tall object only counts up to it.
|
||||
const double rise = std::min(object_top, double(tool_changes[i].front().print_z)) - tower_z[i];
|
||||
if (rise > max_rise)
|
||||
max_rise = rise;
|
||||
}
|
||||
|
||||
const CompactedTowerClearance clearance = compacted_wipe_tower_clearance(m_config, zone, inst_hull, max_rise);
|
||||
if (max_rise <= clearance.allowed_rise + EPSILON)
|
||||
continue;
|
||||
// Same wording as compacted_wipe_tower_clearance_valid(): height-limit and too-close
|
||||
// share one message, since both are layout violations of "No sparse layers".
|
||||
throw Slic3r::SlicingError(compacted_wipe_tower_clearance_error());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//BBS
|
||||
static StringObjectException layered_print_cleareance_valid(const Print &print, StringObjectException *warning)
|
||||
{
|
||||
@@ -1408,6 +1781,16 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
|
||||
}
|
||||
if (!layer_warning.string.empty())
|
||||
add_warning(layer_warning);
|
||||
|
||||
// Orca: a compacted prime tower drags the nozzle back down to the plate on every toolchange, so
|
||||
// tall objects collide with it much like they do in sequential printing. Checking it here rather
|
||||
// than only during slicing is what lets the plater show the collision area and the height limit
|
||||
// while the plate is still being arranged.
|
||||
ret = compacted_wipe_tower_clearance_valid(*this, collison_polygons, height_polygons);
|
||||
if (!ret.string.empty()) {
|
||||
ret.type = STRING_EXCEPT_OBJECT_COLLISION_IN_LAYER_PRINT;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_config.enable_prime_tower) {
|
||||
@@ -2620,6 +3003,12 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
||||
|
||||
if (this->has_wipe_tower()) {
|
||||
m_fake_wipe_tower.set_pos({ m_config.wipe_tower_x.get_at(m_plate_index), m_config.wipe_tower_y.get_at(m_plate_index) });
|
||||
// Validated on every process() run rather than only when the wipe tower step is (re)generated.
|
||||
// Moving the tower changes only wipe_tower_x/y, which invalidates psSkirtBrim but not psWipeTower,
|
||||
// so a validate call living inside _make_wipe_tower would be skipped and keep using the stale
|
||||
// position, missing a fresh collision. The tower geometry (tool_changes) is stored in the local
|
||||
// frame and is position independent, so re-checking here with the current position is correct.
|
||||
this->validate_compacted_wipe_tower_clearance();
|
||||
}
|
||||
|
||||
if (this->set_started(psSkirtBrim)) {
|
||||
|
||||
@@ -1160,6 +1160,8 @@ public:
|
||||
|
||||
//BBS
|
||||
static StringObjectException sequential_print_clearance_valid(const Print &print, Polygons *polygons = nullptr, std::vector<std::pair<Polygon, float>>* height_polygons = nullptr);
|
||||
// Orca: pre-slice clearance check for a prime tower compacted by "No sparse layers".
|
||||
static StringObjectException compacted_wipe_tower_clearance_valid(const Print &print, Polygons *polygons = nullptr, std::vector<std::pair<Polygon, float>>* height_polygons = nullptr);
|
||||
ConflictResultOpt get_conflict_result() const { return m_conflict_result; }
|
||||
|
||||
// Return 4 wipe tower corners in the world coordinates (shifted and rotated), including the wipe tower brim.
|
||||
@@ -1174,6 +1176,8 @@ public:
|
||||
void set_calib_params(const Calib_Params& params);
|
||||
const Calib_Params& calib_params() const { return m_calib_params; }
|
||||
Vec2d translate_to_print_space(const Vec2d &point) const;
|
||||
// Orca: precise counterpart of compacted_wipe_tower_clearance_valid(), run once the tower exists.
|
||||
void validate_compacted_wipe_tower_clearance() const;
|
||||
float get_wipe_tower_depth() const { return m_wipe_tower_data.depth; }
|
||||
BoundingBoxf get_wipe_tower_bbx() const { return m_wipe_tower_data.bbx; }
|
||||
Vec2f get_rib_offset() const { return m_wipe_tower_data.rib_offset; }
|
||||
@@ -1394,6 +1398,89 @@ public:
|
||||
};
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Clearance rule for a prime tower compacted by wipe_tower_no_sparse_layers. Shared by the precise
|
||||
// check that runs on the real extrusions, the pre-slice estimate that feeds the plater with collision
|
||||
// polygons, and the plater's own live preview while the user drags the tower or an object around.
|
||||
// Keeping the rule in one place is what stops those three from drifting apart and reporting different
|
||||
// things for the same plate.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
// Half of a clearance distance, the share each of the two outlines carries. Sequential printing splits
|
||||
// extruder_clearance_radius between the two object hulls this way; the tower checks split their
|
||||
// clearances between the tower ring and the instance hull for the same reason, so that the two
|
||||
// outlines the plater draws touch precisely when the check trips. The 0.2 mm comes off first: it is
|
||||
// the rounding slack the sequential check applies, 0.1 mm per side.
|
||||
inline double compacted_tower_half_clearance(double clearance) { return 0.5 * (clearance - 0.2); }
|
||||
|
||||
// Keep-out geometry a compacted tower projects onto the plate, derived from its bare footprint.
|
||||
struct CompactedTowerZone
|
||||
{
|
||||
// Footprint the checks work on: the raw outline grown by the spiral Z-hop envelope.
|
||||
Polygon hull;
|
||||
// hull grown by half the toolhead radius; an object whose own half-grown hull reaches into it is
|
||||
// hit by the head body. This is also the ring the plater draws.
|
||||
Polygons grown_body;
|
||||
// hull grown by half the bare nozzle cone radius, the innermost tier.
|
||||
Polygons grown_nozzle;
|
||||
// hull bounding box, the Y band the rod sweeps.
|
||||
BoundingBox bbox_rod;
|
||||
// Full body clearance, of which grown_body carries half. Which of the two tiers applies is decided
|
||||
// per object rather than here; see compacted_wipe_tower_clearance().
|
||||
double body_radius { 0. };
|
||||
|
||||
bool empty() const { return hull.points.empty(); }
|
||||
};
|
||||
|
||||
// Per-side padding a bare wipe tower outline needs before the clearance checks may treat it as the
|
||||
// tower's footprint. Callers whose outline already carries the first-layer brim pass zero for it.
|
||||
// Shared by the pre-slice estimate and the plater's live preview: both start from an outline that
|
||||
// falls short of the printed tower in the same two ways, and padding them by different amounts is
|
||||
// exactly how the preview and the validation behind it would end up disagreeing.
|
||||
double compacted_tower_footprint_padding(const PrintConfig &config, double brim_width);
|
||||
|
||||
// Grow a bare tower footprint (bed frame, scaled) into its keep-out zone.
|
||||
CompactedTowerZone compacted_wipe_tower_zone(const PrintConfig &config, const Polygon &tower_footprint);
|
||||
|
||||
// How far an object may rise above the compacted tower base before the toolhead hits it.
|
||||
struct CompactedTowerClearance
|
||||
{
|
||||
// Height the object may reach above the tower base. Zero means it may not rise at all.
|
||||
double allowed_rise;
|
||||
// Clearance that applies once the object stands clear of the toolhead in XY, i.e. rod or lid.
|
||||
double far_clearance;
|
||||
// The object sits within the toolhead radius, so the head body limits it rather than the rod.
|
||||
bool near_body;
|
||||
// Horizontal clearance this particular object has to keep from the tower: the full toolhead
|
||||
// radius once it rises past the nozzle cone, the bare cone while it stays below. It is what the
|
||||
// error message quotes and what the plater grows the object outline by.
|
||||
double body_clearance;
|
||||
};
|
||||
|
||||
// object_rise is the height above the tower base that the caller is going to compare against
|
||||
// allowed_rise. It also selects the horizontal tier, so the two cannot disagree.
|
||||
CompactedTowerClearance compacted_wipe_tower_clearance(const PrintConfig &config, const CompactedTowerZone &zone,
|
||||
const Polygon &inst_hull, double object_rise);
|
||||
|
||||
// This object was judged on a tier reaching past the bare nozzle cone, so the wide ring is the one its
|
||||
// outline has to be drawn against.
|
||||
inline bool compacted_tower_body_tier(const CompactedTowerClearance &clearance)
|
||||
{
|
||||
return clearance.body_clearance > double(MAX_OUTER_NOZZLE_DIAMETER);
|
||||
}
|
||||
|
||||
// Keep-out rings to draw around the tower. The nozzle one always applies; the wide body one is drawn
|
||||
// only when some object on the plate is actually measured against it, otherwise it would show a
|
||||
// keep-out zone no object can violate.
|
||||
Polygons compacted_wipe_tower_rings(const CompactedTowerZone &zone, bool any_body_tier);
|
||||
|
||||
// Outline to hand the plater for an offending object: the instance hull grown by the same half
|
||||
// clearance the check grew it by, which is CompactedTowerClearance::body_clearance for that object.
|
||||
// Sequential printing reports its hulls the same way, and it doubles as the fix for the bare hull
|
||||
// being unusable on screen, where drawn flat it hides under the object and drawn at the height limit
|
||||
// it ends up buried inside the mesh.
|
||||
Polygon compacted_wipe_tower_offender_outline(const Polygon &inst_hull, double body_clearance);
|
||||
|
||||
} /* slic3r_Print_hpp_ */
|
||||
|
||||
#endif
|
||||
|
||||
@@ -2549,6 +2549,16 @@ void PrintConfigDef::init_fff_params()
|
||||
def->enum_labels.push_back("5");
|
||||
def->mode = comAdvanced;
|
||||
|
||||
// Orca: already carried by the BBL/Qidi/Geeetech/Eryone machine profiles, which inherited it from
|
||||
// the BambuStudio import; without a definition here it was parsed as an unknown key and dropped.
|
||||
def = this->add("extruder_clearance_dist_to_rod", coFloat);
|
||||
def->label = L("Distance to rod");
|
||||
def->tooltip = L("Horizontal distance of the nozzle tip to the rod's farther edge. Used for collision avoidance in by-object printing.");
|
||||
def->sidetext = L("mm"); // millimeters, CIS languages need translation
|
||||
def->min = 0;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloat(40));
|
||||
|
||||
def = this->add("extruder_clearance_height_to_rod", coFloat);
|
||||
def->label = L("Height to rod");
|
||||
def->tooltip = L("Distance from the nozzle tip to the lower rod. Used for collision avoidance in by-object printing.");
|
||||
@@ -5537,6 +5547,16 @@ void PrintConfigDef::init_fff_params()
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(true));
|
||||
|
||||
def = this->add("unsupported_wall_last", coBool);
|
||||
def->label = L("Print unsupported walls last");
|
||||
def->category = L("Quality");
|
||||
def->tooltip = L("Wall loops that lie entirely in mid air are printed once something can hold them:\n"
|
||||
"they are extruded after the other walls of their island, innermost first, whatever the wall order is.\n"
|
||||
"A loop that only the bridges of this layer can anchor waits until those bridges are printed, while a loop running "
|
||||
"alongside a supported wall keeps its place before the infill, which needs it as an anchor.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("outer_wall_filament_id", coInt);
|
||||
def->gui_type = ConfigOptionDef::GUIType::i_enum_open;
|
||||
def->label = L("Outer walls");
|
||||
@@ -6282,6 +6302,7 @@ void PrintConfigDef::init_fff_params()
|
||||
def = this->add("wipe_inward_distance", coFloatOrPercent);
|
||||
def->label = L("Wipe inward distance");
|
||||
def->category = L("Quality");
|
||||
// xgettext:no-c-format, no-boost-format
|
||||
def->tooltip = L("The distance the wipe path is shifted away from the external perimeter, specified in millimeters "
|
||||
"or as a percentage of the actual outer-wall extrusion width.\n\n"
|
||||
"For example, 50% shifts the path by half of the outer-wall width. The effective offset is limited "
|
||||
@@ -6673,8 +6694,10 @@ void PrintConfigDef::init_fff_params()
|
||||
def = this->add("wipe_tower_no_sparse_layers", coBool);
|
||||
def->label = L("No sparse layers (beta)");
|
||||
def->tooltip = L("If enabled, the wipe tower will not be printed on layers with no tool changes. "
|
||||
"On layers with a tool change, extruder will travel downward to print the wipe tower. "
|
||||
"User is responsible for ensuring there is no collision with the print.");
|
||||
"On layers with a tool change, extruder will travel downward to print the wipe tower, "
|
||||
"so the tower ends up below the model and the toolhead has to reach down to it. "
|
||||
"Layouts where that would collide with an already printed object are rejected. "
|
||||
"Has no effect with smooth timelapse or clumping detection, which need a tower on every layer.");
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
@@ -6700,6 +6723,34 @@ void PrintConfigDef::init_fff_params()
|
||||
def->enum_labels.emplace_back(L("Cyclic"));
|
||||
def->set_default_value(new ConfigOptionEnum<ToolChangeOrderingType>(ToolChangeOrderingType::Default));
|
||||
|
||||
def = this->add("toolchange_cyclic_order", coString);
|
||||
def->label = L("Cyclic order");
|
||||
def->category = L("Advanced");
|
||||
def->tooltip = L(
|
||||
"Custom filament sequence used by the cyclic toolchange ordering, as filament numbers separated by commas (e.g. \"3,2,1,4\").\n"
|
||||
"Each layer prints its filaments following this sequence; filaments not listed are printed last, in ascending order.\n"
|
||||
"Leave empty to cycle through the filaments in ascending order."
|
||||
);
|
||||
def->mode = comExpert;
|
||||
def->set_default_value(new ConfigOptionString(""));
|
||||
|
||||
def = this->add("toolchange_cyclic_first_layer", coBool);
|
||||
def->label = L("Apply cyclic order to first layer");
|
||||
def->category = L("Advanced");
|
||||
def->tooltip = L(
|
||||
"Applies the cyclic toolchange order to the first layer as well.\n"
|
||||
"By default this is disabled, because the first layer is instead ordered for the best bed "
|
||||
"adhesion: filaments that print small, fragile first-layer features are printed last, so the "
|
||||
"following tool changes and travel moves are less likely to knock those weakly anchored parts "
|
||||
"loose. This first-layer order also honors a custom first layer filament sequence when one is set. "
|
||||
"The cyclic order's benefit (extra tool changes give each layer more time to cool) does not apply "
|
||||
"to the first layer, which is printed slowly and hot for adhesion.\n"
|
||||
"Enable this only if you need the exact same tool sequence on every layer, including the first, at "
|
||||
"the cost of that adhesion optimization."
|
||||
);
|
||||
def->mode = comExpert;
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("slice_closing_radius", coFloat);
|
||||
def->label = L("Slice gap closing radius");
|
||||
def->category = L("Quality");
|
||||
@@ -6712,7 +6763,7 @@ void PrintConfigDef::init_fff_params()
|
||||
|
||||
def = this->add("slicing_mode", coEnum);
|
||||
def->label = L("Slicing Mode");
|
||||
def->category = L("Other");
|
||||
def->category = L("Others");
|
||||
def->tooltip = L("Use \"Even-odd\" for 3DLabPrint airplane models. Use \"Close holes\" to close all holes in the model.");
|
||||
def->enum_keys_map = &ConfigOptionEnum<SlicingMode>::get_enum_values();
|
||||
def->enum_values.push_back("regular");
|
||||
@@ -11923,6 +11974,19 @@ CLIActionsConfigDef::CLIActionsConfigDef()
|
||||
def->tooltip = L("Do not run any validity checks, such as G-code path conflicts check.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
// --strict turns the non-critical slicing warnings the CLI otherwise only logs into a
|
||||
// failed run, and records strict_mode in result.json so consumers can tell the modes apart.
|
||||
def = this->add("strict", coBool);
|
||||
def->label = L("Strict mode");
|
||||
def->tooltip = L("Exit non-zero when slicing raises a non-critical warning that is "
|
||||
"otherwise only logged, such as a model that needs support while "
|
||||
"support is disabled. Use this in CI or scripted pipelines that should "
|
||||
"never ship a subtly broken slice. Each such warning is also listed "
|
||||
"with a stable class in the `warnings` array of result.json, which is "
|
||||
"written on Linux only. Cannot be combined with --no-check, which skips "
|
||||
"the support check.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("normative_check", coBool);
|
||||
def->label = L("Normative check");
|
||||
def->tooltip = L("Check the normative items.");
|
||||
@@ -11943,6 +12007,26 @@ CLIActionsConfigDef::CLIActionsConfigDef()
|
||||
def->tooltip = L("This outputs the model\u2019s information.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("inspect_mesh", coBool);
|
||||
def->label = L("Inspect mesh (JSON to stdout)");
|
||||
def->tooltip = L("Print a JSON summary of each loaded object to stdout, then exit: its bounding boxes and the "
|
||||
"convex hull faces it can be laid on, with their normals, areas and centers. These are the faces "
|
||||
"the --ground-* options choose from. Machine-readable alternative to --info.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
// --inspect-paint \u2014 dump the per-facet enforcer/blocker/extruder/fuzzy
|
||||
// paint state stored on the loaded model (supports, seam, MMU color,
|
||||
// fuzzy-skin) as JSON. Read-only; lets CI / scripted / AI tooling
|
||||
// reason about existing paint on a .3mf without loading the GUI.
|
||||
def = this->add("inspect_paint", coBool);
|
||||
def->label = L("Inspect paint (JSON to stdout)");
|
||||
def->tooltip = L("Print a structured JSON summary of every painted layer "
|
||||
"(supports, seam, MMU color, fuzzy-skin) already stored on "
|
||||
"the loaded model \u2014 per-state facet count, surface area, "
|
||||
"and mesh-local bounding box \u2014 then exit. Machine-readable "
|
||||
"alternative to opening the paint gizmos in the GUI.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("export_settings", coString);
|
||||
def->label = L("Export Settings");
|
||||
def->tooltip = L("This exports settings to a file. Use - to write them to stdout.");
|
||||
@@ -12062,6 +12146,34 @@ CLITransformConfigDef::CLITransformConfigDef()
|
||||
def->sidetext = u8"°"; // degrees, don't need translation
|
||||
def->set_default_value(new ConfigOptionFloat(0));
|
||||
|
||||
// The --ground-* options choose from the faces the "Lay on Face" gizmo offers. Like the other
|
||||
// transforms they run in command-line order, so they see the rotations given before them.
|
||||
def = this->add("ground_largest_face", coBool);
|
||||
def->label = L("Ground largest face");
|
||||
def->tooltip = L("Lay each object on the largest face of its convex hull and drop it onto the bed. Of equally large "
|
||||
"faces, the one already facing down is kept. Objects without a face large enough to rest on are left "
|
||||
"as they are. Transforms run in command-line order, so rotations given before this option are respected. "
|
||||
"--orient 1 runs after all transforms and replaces the orientation.");
|
||||
def->set_default_value(new ConfigOptionBool(false));
|
||||
|
||||
def = this->add("ground_face_normal", coString);
|
||||
def->label = L("Ground face by normal");
|
||||
def->tooltip = L("Lay each object on the convex hull face whose outward normal is closest to the direction NX,NY,NZ "
|
||||
"and drop it onto the bed. The direction is in object coordinates, which include the rotations given "
|
||||
"before this option and match the plate axes unless the input file rotates the object. For example, "
|
||||
"1,0,0 stands the object on its +X side. --orient 1 runs after all transforms and replaces the orientation.");
|
||||
def->cli_params = "NX,NY,NZ";
|
||||
def->set_default_value(new ConfigOptionString(""));
|
||||
|
||||
def = this->add("ground_face_point", coString);
|
||||
def->label = L("Ground face at point");
|
||||
def->tooltip = L("Lay each object on the convex hull face that contains the point X,Y,Z and drop it onto the bed. "
|
||||
"The point is in object coordinates, which include the rotations given before this option; "
|
||||
"--inspect-mesh reports face centers in them. Objects without such a face are left as they are, and "
|
||||
"the run fails if no object has one. --orient 1 runs after all transforms and replaces the orientation.");
|
||||
def->cli_params = "X,Y,Z";
|
||||
def->set_default_value(new ConfigOptionString(""));
|
||||
|
||||
def = this->add("scale", coFloat);
|
||||
def->label = L("Scale");
|
||||
def->tooltip = L("Scale the model by a float factor.");
|
||||
|
||||
@@ -1353,6 +1353,7 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
((ConfigOptionFloatsNullable, filament_ironing_speed))
|
||||
// Detect bridging perimeters
|
||||
((ConfigOptionBool, detect_overhang_wall))
|
||||
((ConfigOptionBool, unsupported_wall_last))
|
||||
((ConfigOptionInt, outer_wall_filament_id))
|
||||
((ConfigOptionInt, inner_wall_filament_id))
|
||||
((ConfigOptionFloatOrPercent, inner_wall_line_width))
|
||||
@@ -1627,6 +1628,8 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
((ConfigOptionBool, manual_filament_change))
|
||||
((ConfigOptionBool, single_extruder_multi_material_priming))
|
||||
((ConfigOptionEnum<ToolChangeOrderingType>, toolchange_ordering))
|
||||
((ConfigOptionString, toolchange_cyclic_order))
|
||||
((ConfigOptionBool, toolchange_cyclic_first_layer))
|
||||
((ConfigOptionBool, wipe_tower_no_sparse_layers))
|
||||
((ConfigOptionString, change_filament_gcode))
|
||||
((ConfigOptionString, change_extrusion_role_gcode))
|
||||
@@ -1788,6 +1791,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
|
||||
((ConfigOptionBools, slow_down_for_layer_cooling))
|
||||
((ConfigOptionInts, close_fan_the_first_x_layers))
|
||||
((ConfigOptionEnum<DraftShield>, draft_shield))
|
||||
((ConfigOptionFloat, extruder_clearance_dist_to_rod))//BBS
|
||||
((ConfigOptionFloat, extruder_clearance_height_to_rod))//BBs
|
||||
((ConfigOptionFloat, extruder_clearance_height_to_lid))//BBS
|
||||
((ConfigOptionFloat, extruder_clearance_radius))
|
||||
|
||||
@@ -1501,6 +1501,7 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
|| opt_key == "fuzzy_skin_octaves"
|
||||
|| opt_key == "fuzzy_skin_persistence"
|
||||
|| opt_key == "detect_overhang_wall"
|
||||
|| opt_key == "unsupported_wall_last"
|
||||
|| opt_key == "overhang_reverse"
|
||||
|| opt_key == "overhang_reverse_internal_only"
|
||||
|| opt_key == "overhang_reverse_threshold"
|
||||
|
||||
@@ -684,6 +684,10 @@ set(SLIC3R_GUI_SOURCES
|
||||
Utils/bambu_networking.hpp
|
||||
Utils/Bonjour.cpp
|
||||
Utils/Bonjour.hpp
|
||||
Utils/MeshInspect.cpp
|
||||
Utils/MeshInspect.hpp
|
||||
Utils/PaintCLI.cpp
|
||||
Utils/PaintCLI.hpp
|
||||
Utils/CalibUtils.cpp
|
||||
Utils/CalibUtils.hpp
|
||||
Utils/ColorSpaceConvert.cpp
|
||||
@@ -788,6 +792,30 @@ set(SLIC3R_GUI_SOURCES
|
||||
Utils/wxInspectorPlugins/Registration.hpp
|
||||
)
|
||||
|
||||
# Design/CAD tab: parametric sketch UI, its gizmos, and the MCP control socket.
|
||||
# All of it sits behind SLIC3R_CAD and links the CAD kernel in libslic3r.
|
||||
if (SLIC3R_CAD)
|
||||
list(APPEND SLIC3R_GUI_SOURCES
|
||||
GUI/CAD/DesignPanel.cpp
|
||||
GUI/CAD/DesignPanel.hpp
|
||||
GUI/CAD/DesignCanvas.cpp
|
||||
GUI/CAD/DesignCanvas.hpp
|
||||
GUI/CAD/DesignSketchTool.cpp
|
||||
GUI/CAD/DesignSketchTool.hpp
|
||||
GUI/CAD/DesignOffer.hpp
|
||||
GUI/CAD/DesignInteraction.hpp
|
||||
GUI/CAD/SketchInlineEditor.cpp
|
||||
GUI/CAD/SketchInlineEditor.hpp
|
||||
GUI/CAD/McpControl.cpp
|
||||
GUI/CAD/McpControl.hpp
|
||||
GUI/Gizmos/GLGizmoSketch.cpp
|
||||
GUI/Gizmos/GLGizmoSketch.hpp
|
||||
# Needs GeometryEngine (make_primitive / apply_fillet / tessellate).
|
||||
GUI/Gizmos/GLGizmoPrimitive.cpp
|
||||
GUI/Gizmos/GLGizmoPrimitive.hpp
|
||||
)
|
||||
endif ()
|
||||
|
||||
add_subdirectory(GUI/DeviceCore)
|
||||
add_subdirectory(GUI/DeviceTab)
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ public:
|
||||
|
||||
void set_position(Vec2d& position);
|
||||
void set_axes_mode(bool origin);
|
||||
void set_axes_origin(const Vec3d& origin) { m_axes.set_origin(origin); } // Design tab: triad at bed centre
|
||||
const Vec2d& get_position() const { return m_position; }
|
||||
|
||||
// Build volume geometry for various collision detection tasks.
|
||||
|
||||
@@ -1197,11 +1197,11 @@ void AMSDryCtrWin::update_normal_description(DevAms* dev_ams)
|
||||
for (const auto& lim : ams_limits) {
|
||||
if (dev_ams->GetAmsType() == lim.type) {
|
||||
if (temp_val > lim.max_temp) {
|
||||
wxString msg = wxString(lim.name) + _L(" maximum drying temperature is ") + wxString::Format(wxT("%d"), lim.max_temp) + wxString::FromUTF8("°C.");
|
||||
wxString msg = wxString::Format(_L("%s maximum drying temperature is %d°C."), wxString(lim.name), lim.max_temp);
|
||||
warning_text += msg + "\n";
|
||||
can_enable_button = false;
|
||||
} else if (temp_val < lim.min_temp) {
|
||||
wxString msg = wxString(lim.name) + _L(" minimum drying temperature is ") + wxString::Format(wxT("%d"), lim.min_temp) + wxString::FromUTF8("°C.");
|
||||
wxString msg = wxString::Format(_L("%s minimum drying temperature is %d°C."), wxString(lim.name), lim.min_temp);
|
||||
warning_text += msg + "\n";
|
||||
can_enable_button = false;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,436 @@
|
||||
#ifndef slic3r_DesignCanvas_hpp_
|
||||
#define slic3r_DesignCanvas_hpp_
|
||||
|
||||
#include <wx/panel.h>
|
||||
#include <wx/popupwin.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "slic3r/GUI/3DBed.hpp"
|
||||
#include "slic3r/GUI/Camera.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/CAD/SketchEngine.hpp"
|
||||
#include "slic3r/GUI/CAD/DesignSketchTool.hpp"
|
||||
|
||||
class wxGLCanvas;
|
||||
class wxFrame;
|
||||
class wxStaticText;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class TriangleMesh;
|
||||
|
||||
namespace GUI {
|
||||
|
||||
class GLCanvas3D;
|
||||
class SketchInlineEditor;
|
||||
|
||||
class DesignCanvas : public wxPanel
|
||||
{
|
||||
public:
|
||||
explicit DesignCanvas(wxWindow* parent);
|
||||
~DesignCanvas() override;
|
||||
|
||||
void set_mesh(const TriangleMesh& mesh);
|
||||
// Multi-body display: one GLVolume per body, each coloured distinctly (per-body colour).
|
||||
// `visible` (optional, indexed by body) hides bodies whose flag is false.
|
||||
void set_bodies(const std::vector<TriangleMesh>& body_meshes,
|
||||
const std::vector<bool>& visible = {});
|
||||
void clear_mesh();
|
||||
|
||||
void set_preview_mesh(const TriangleMesh& mesh);
|
||||
void clear_preview();
|
||||
|
||||
void fit_view();
|
||||
void set_view(const std::string& view_name);
|
||||
|
||||
void begin_sketch(const SketchPlane& plane, DesignSketchTool::Mode mode);
|
||||
// Re-open a committed entity sketch for full in-canvas editing (load geometry +
|
||||
// constraints, re-detect feature groups). Re-commits via finish_sketch().
|
||||
void edit_sketch(const std::vector<SketchEntity>& entities,
|
||||
const std::vector<SketchEntityConstraintDef>& constraints,
|
||||
const SketchPlane& plane);
|
||||
void set_sketch_tool(DesignSketchTool::Mode mode);
|
||||
void set_sketch_plane(const SketchPlane& plane); // re-plane the live sketch when a reference plane is clicked in 3D
|
||||
void set_sketch_construction(bool c);
|
||||
// Flip the sketch selection between construction and real geometry; returns the
|
||||
// number of entities changed (0 = nothing selected, caller falls back to the mode).
|
||||
// Open the in-canvas value field on the sketch selection's defining number.
|
||||
bool edit_sketch_selection_value();
|
||||
int toggle_sketch_construction_selection();
|
||||
// Is the sketch tool on Select (as opposed to a draw/edit tool being armed)? The
|
||||
// Construction box needs it to tell "convert what I picked" from "arm what I draw next".
|
||||
bool sketch_is_selecting() const { return m_sketch_tool.mode() == DesignSketchTool::Mode::Select; }
|
||||
// Text / SVG art into the LIVE sketch, as ordinary editable lines. False = no session.
|
||||
bool add_sketch_regions(const std::vector<std::vector<std::vector<Vec2d>>>& regions);
|
||||
void set_sketch_polygon_sides(int n);
|
||||
void set_sketch_polygon_circumscribed(bool c);
|
||||
void finish_sketch();
|
||||
bool is_sketching() const;
|
||||
void refresh_bed(); // re-sync the bed to the current printer (call on tab activation)
|
||||
// The Camera is Plater-owned and shared with Prepare/Preview/Assemble; GLCanvas3D has no
|
||||
// per-canvas camera, so every orbit here would otherwise overwrite what the editor tabs
|
||||
// show. Exactly one of the two views is live at a time, so entering and leaving are the
|
||||
// same operation: trade the live camera for the parked one. That also keeps this canvas's
|
||||
// own view across a tab switch.
|
||||
void enter_viewport();
|
||||
void leave_viewport();
|
||||
void unbind_canvas_event_handlers(); // app close / language switch, from the plater's teardown
|
||||
void reset_canvas_volumes();
|
||||
void set_show_bed(bool b); // view option: draw the printer bed + plate grid, or not
|
||||
// N: look straight down the sketch plane's normal, keeping the current zoom. A sketch drawn
|
||||
// at an angle is a sketch drawn wrong, and no amount of orbiting by hand lands exactly square.
|
||||
bool view_normal_to_sketch();
|
||||
void cancel_sketch();
|
||||
void set_on_sketch_commit(std::function<void(const SketchProfile&, const SketchPlane&)> cb);
|
||||
void set_on_sketch_entities_commit(
|
||||
std::function<void(const std::vector<SketchEntity>&,
|
||||
const std::vector<SketchEntityConstraintDef>&,
|
||||
const SketchPlane&)> cb);
|
||||
|
||||
// Line tool: pending-segment length entry + live readout (Phase 2).
|
||||
void set_on_segment_drawn(std::function<void(double, double)> cb);
|
||||
void set_on_cursor_metrics(std::function<void(double, double, bool)> cb);
|
||||
void set_on_solve_state(std::function<void(int, bool, bool)> cb); // dof, ok, has_constraints
|
||||
// Live per-step guidance from the armed sketch tool (mode, step, picks). 1c0c.
|
||||
void set_on_sketch_step(std::function<void(DesignSketchTool::Mode, int, int)> cb);
|
||||
void apply_segment_length(double len); // exact length, then commit & repaint
|
||||
void keep_segment_as_drawn(); // commit as-drawn & repaint
|
||||
|
||||
// Sketch selection (Mode::Select).
|
||||
void set_on_sketch_selection_changed(std::function<void(int)> cb);
|
||||
void set_on_sketch_face_selected(std::function<void(int)> cb); // closed loop clicked: region index passed
|
||||
void set_on_display_sketch_selected(std::function<void(int, int, int)> cb); // committed loop clicked: (feature, region, entity)
|
||||
void set_on_display_sketch_activated(std::function<void(int)> cb); // committed sketch DOUBLE-clicked: edit it
|
||||
std::vector<SketchEntity> selected_loop_entities() const; // entities of the click-selected loop
|
||||
std::vector<std::vector<int>> region_entity_indices(const std::vector<SketchEntity>& ents) const;
|
||||
// Like region_entity_indices, but each region's entry is its OWN entities followed by the
|
||||
// entities of each of its holes — the same order selected_loop_entities() hands the kernel.
|
||||
// A per-loop extrude of a region WITH holes stores exactly this, so this is the shape a
|
||||
// consumed loop must be compared against.
|
||||
std::vector<std::vector<int>> region_entity_indices_with_holes(const std::vector<SketchEntity>& ents) const;
|
||||
void clear_loop_pick(); // drop the click-selected loop highlight (e.g. after extrude)
|
||||
void set_loop_pick(int feature, int region); // adopt a loop pick made before the commit
|
||||
void set_escalate_on_repick(bool on); // off while a card has armed a face/edge pick
|
||||
// Solid whole/face/edge selection: point the tool at the bodies + concatenated
|
||||
// tessellation (with per-triangle face & body ids), and a callback fired on each
|
||||
// whole->face->edge cycle (level, body index, face id, edge id).
|
||||
void set_solid_pick(const std::vector<CadBody>* bodies, const TriangleMesh* mesh,
|
||||
const std::vector<int>* tri_face, const std::vector<int>* tri_body,
|
||||
const std::vector<bool>* visible = nullptr,
|
||||
const std::vector<Transform3d>* xform = nullptr);
|
||||
void set_on_solid_selection_changed(std::function<void(int, int, int, int)> cb);
|
||||
void set_on_place_on_face(std::function<bool()> cb); // F key: Place on Face
|
||||
void select_body(int body); // Parts-list -> highlight a whole body by index
|
||||
// Effective display colour of a body: the per-body override (Color tool) when set,
|
||||
// otherwise the auto body-index palette. Single source of truth shared with reload().
|
||||
ColorRGBA body_color(int body) const;
|
||||
// Move-body gizmo (M5): three world-axis drag arrows on a body; drag fires the move
|
||||
// callback with the body index + accumulated translation (display-only, host applies it).
|
||||
// body_radius = bounding-sphere radius of the body in world mm; the gizmo scales with it so
|
||||
// the rotation rings sit OUTSIDE the solid (Orca's Prepare gizmos do the same).
|
||||
void begin_move_body(int body, const Vec3d& pivot, const Transform3d& base_xform,
|
||||
double body_radius);
|
||||
void clear_move_gizmo();
|
||||
bool moving_body() const;
|
||||
void set_on_body_move_changed(std::function<void(int, const Transform3d&)> cb);
|
||||
// Visual Fillet/Chamfer radius gizmo: when a solid edge is picked, anchor a radius arrow on
|
||||
// it; drag/edit fire the radius callback. Returns false if no edge is currently picked.
|
||||
bool begin_fillet_gizmo(const Vec3d& body_centroid, double radius);
|
||||
void clear_fillet_gizmo();
|
||||
bool filleting() const;
|
||||
void set_on_fillet_radius_changed(std::function<void(double)> cb);
|
||||
// Visual Hole gizmo: the panel feeds the hole plane + position + diameter/depth/through while
|
||||
// its Hole card is open; drag/edit fire the hole callback (x, y, diameter, depth).
|
||||
void begin_hole_gizmo(const SketchPlane& plane, double x, double y,
|
||||
double diameter, double depth, bool through);
|
||||
void set_hole_face_bounds(bool has, double umin, double umax, double vmin, double vmax);
|
||||
void clear_hole_gizmo();
|
||||
bool holing() const;
|
||||
void set_on_hole_changed(std::function<void(double, double, double, double)> cb);
|
||||
// Visual Thread gizmo: footprint circle + radius/length arrows + draggable centre.
|
||||
void begin_thread_gizmo(const SketchPlane& plane, double x, double y,
|
||||
double radius, double height);
|
||||
void clear_thread_gizmo();
|
||||
bool threading() const;
|
||||
void set_on_thread_changed(std::function<void(double, double, double, double)> cb);
|
||||
// Visual Shell gizmo: inward thickness arrow at the picked open-face centroid.
|
||||
void begin_shell_gizmo(const Vec3d& face_centroid, const Vec3d& inward_dir, double thickness);
|
||||
void clear_shell_gizmo();
|
||||
bool shelling() const;
|
||||
void set_on_shell_thickness_changed(std::function<void(double)> cb);
|
||||
// Visual Revolve angle-arc gizmo: the panel feeds the sketch plane + profile centroid + axis
|
||||
// (0=plane X, 1=plane Y) + angle + flip while its Revolve card is open; drag/edit fire the
|
||||
// angle callback.
|
||||
void begin_revolve_gizmo(const SketchPlane& plane, const Vec2d& centroid,
|
||||
int axis_sel, double angle, bool flip);
|
||||
void clear_revolve_gizmo();
|
||||
bool revolving() const;
|
||||
void set_on_revolve_angle_changed(std::function<void(double)> cb);
|
||||
// Visual Draft angle-arc gizmo: the panel feeds the face centroid + face normal + angle while
|
||||
// its Draft card is open; drag/edit fire the angle callback.
|
||||
void set_draft_gizmo(const Vec3d& face_centroid, const Vec3d& face_normal, double angle);
|
||||
void clear_draft_gizmo();
|
||||
bool drafting() const;
|
||||
void set_on_draft_angle_changed(std::function<void(double)> cb);
|
||||
// Visual Cut gizmo: plane-rectangle preview + draggable normal offset arrow while
|
||||
// the Cut card is open; drag fires the offset callback.
|
||||
void set_cut_gizmo(const SketchPlane& plane, double offset, const Vec3d& body_center, double half_extent);
|
||||
void clear_cut_gizmo();
|
||||
bool cutting() const;
|
||||
void set_on_cut_offset_changed(std::function<void(double)> cb);
|
||||
// Visual Pattern gizmo: the panel feeds the (world XY) plane + target body centroid + mode +
|
||||
// count/dir/spacing/angle while its Pattern card is open; drag/edit fire the value callback.
|
||||
void begin_pattern_gizmo(const SketchPlane& plane, const Vec3d& body_centroid, bool circular,
|
||||
int count, int dir, double spacing, double angle);
|
||||
void clear_pattern_gizmo();
|
||||
bool patterning() const;
|
||||
void set_on_pattern_changed(std::function<void(double)> cb);
|
||||
// Visual Extrude depth-arrow gizmo (C5b): the panel feeds the profile plane + centroid +
|
||||
// live depths/flags while its Extrude card is open; drag/edit fire the depth callback.
|
||||
void set_extrude_gizmo(const SketchPlane& plane, const Vec2d& centroid,
|
||||
double depth, double depth2, bool two_sided, bool flip);
|
||||
void clear_extrude_gizmo();
|
||||
void set_on_extrude_depth_changed(std::function<void(double, bool)> cb);
|
||||
void set_datum_gizmo(const SketchPlane& plane, double usize, double vsize,
|
||||
const Vec3d& base_origin, const Vec3d& base_normal,
|
||||
double offset, bool offset_on); // C3 resize handles + offset arrow
|
||||
void clear_datum_gizmo();
|
||||
void set_on_datum_size_changed(std::function<void(double, double)> cb);
|
||||
void set_on_datum_offset_changed(std::function<void(double)> cb);
|
||||
void set_helix_gizmo(const SketchPlane& plane, double radius, double pitch, double height,
|
||||
double taper, bool left_handed); // helix curve + 3 drag handles
|
||||
void clear_helix_gizmo();
|
||||
void set_on_helix_changed(std::function<void(double, double, double)> cb);
|
||||
void set_rib_gizmo(const SketchPlane& plane, const Vec2d& p0, const Vec2d& p1, double thickness); // rib slab footprint + 2 thickness handles
|
||||
void clear_rib_gizmo();
|
||||
void set_on_rib_thickness_changed(std::function<void(double)> cb);
|
||||
void set_base_pick(std::vector<SketchPlane> planes, std::vector<int> bases,
|
||||
std::vector<std::string> labels = {}); // clickable labelled reference planes
|
||||
void clear_base_pick();
|
||||
void set_on_datum_base_picked(std::function<void(int)> cb);
|
||||
void set_on_sketch_exit(std::function<void()> cb); // Esc -> exit the tool
|
||||
void set_on_sketch_exit_refused(std::function<void()> cb); // Esc declined: sketch has work
|
||||
void set_on_undo_redo(std::function<void(bool /*redo*/)> cb); // Ctrl+Z / Ctrl+Shift+Z
|
||||
// Persistently draw committed sketches (un-consumed ones stay visible).
|
||||
void set_display_sketches(std::vector<DesignSketchTool::DisplaySketch> ds);
|
||||
void set_highlight_sketches(std::vector<std::pair<int, ColorRGBA>> hl);
|
||||
void set_datum_planes(std::vector<SketchPlane> planes,
|
||||
std::vector<Vec2d> sizes = {}); // draw datum/reference planes (u/v extents)
|
||||
// Mate connectors, drawn as frames so their verse and polarity are visible (wgsc).
|
||||
void set_mate_connectors(std::vector<DesignSketchTool::MateConnectorGlyph> g);
|
||||
void set_mate_links(std::vector<std::pair<Vec3d, Vec3d>> l);
|
||||
void set_body_highlight(bool on); // tint the solid when its feature is tree-selected
|
||||
// The status line, shown along the BASE OF THE VIEWPORT rather than in the side panel:
|
||||
// the panel clips it at ~73 characters with no warning (8cc), the viewport's
|
||||
// bottom margin has the whole window width to spare. Empty text hides it.
|
||||
void set_status_text(const wxString& text, const wxColour& colour);
|
||||
// Take the status line down / bring it back when the Design page leaves and re-enters view.
|
||||
// A popup is a TOP-LEVEL window: hiding the page it belongs to does not hide it. Keeps the
|
||||
// text, so coming back needs no re-selection.
|
||||
void show_status_hud(bool on);
|
||||
void set_operand_bodies(int target_body, int tool_body); // -1,-1 clears
|
||||
void set_body_translucent(bool on); // render the solid see-through (fillet/chamfer preview)
|
||||
void set_xray_focus(int body); // >=0: fade+lock out every other body (CoordSys picking)
|
||||
void set_body_hidden(bool on); // preview-only: hide base bodies, show only the result ghost
|
||||
void set_on_move_exit(std::function<void()> cb); // right-click finished the move-body gizmo
|
||||
// Right-click (or its platform equivalent) on the viewport with no tool running: open the
|
||||
// object-driven offer there. Fires with SCREEN coordinates. Deliberately NOT fired while a
|
||||
// tool is live — right-click already ends a polyline chain and finishes the move gizmo, and
|
||||
// taking those over would break two working interactions in order to add a third.
|
||||
void set_on_context_menu(std::function<void(const wxPoint&)> cb);
|
||||
void delete_selected_sketch_entities();
|
||||
bool inline_busy() const; // a sketch value field is open (guard keys)
|
||||
bool inline_has_focus() const; // the field itself holds keyboard focus
|
||||
void inline_commit(); // accept the typed value (Enter/Tab)
|
||||
void inline_cancel(); // discard the typed value (Esc)
|
||||
// The layered Esc: abandon the points of the gesture in progress, else drop the armed tool
|
||||
// back to Select, else leave the sketch. Same call GLCanvas3D::on_char makes, exposed so the
|
||||
// panel can do it when focus is not on the canvas.
|
||||
void request_sketch_exit();
|
||||
bool live_sketch_has_work() const; // the live sketch holds entities a cancel would destroy
|
||||
bool undo_last_sketch_entity(); // Ctrl+Z in a sketch: drop the last entity
|
||||
bool delete_selected_or_last_sketch_entity(); // Delete in a sketch: selected, else last
|
||||
void clear_sketch_selection();
|
||||
|
||||
// View toggles (keys P / A): origin planes, world axis triad. Each returns the new on/off
|
||||
// state so the caller can echo it in the status bar.
|
||||
bool toggle_planes();
|
||||
bool toggle_axes();
|
||||
|
||||
// Section views (non-destructive): the panel owns the named "Section View N" list; the canvas
|
||||
// just applies/clears one horizontal clip at a time. model_mid_z() is the default cut height.
|
||||
void set_section_plane(bool on, double z, bool keep_upper = false);
|
||||
double model_mid_z() const;
|
||||
|
||||
// Dimension tool: act on the current sketch selection.
|
||||
DesignSketchTool::DimType sketch_dimension_kind() const;
|
||||
double sketch_dimension_current() const;
|
||||
void apply_sketch_dimension(double v);
|
||||
|
||||
// Open the in-canvas value editor at the cursor for a host-driven value (the
|
||||
// committed-feature Constrain path uses this instead of a docked numeric card).
|
||||
void open_inline_value(double current, std::function<void(double)> commit,
|
||||
std::function<void()> cancel = {});
|
||||
|
||||
// Dimension tool (Mode::Dimension): click-to-place quotes. The pick-complete
|
||||
// callback lets the panel pop the value card; set/cancel apply or keep the value.
|
||||
void set_on_dimension_pick_complete(std::function<void(double)> cb);
|
||||
DesignSketchTool::DimType pending_dimension_type() const;
|
||||
void set_sketch_dimension_value(double v);
|
||||
void cancel_sketch_dimension();
|
||||
|
||||
// Constrain mode: load a committed profile for picking + constraint editing.
|
||||
void begin_constrain(const SketchProfile& prof, const SketchPlane& plane);
|
||||
// Leave constrain mode and clear any picked-entity highlight from the overlay.
|
||||
void end_constrain();
|
||||
bool is_constraining() const;
|
||||
bool selected_segment(int& a, int& b) const;
|
||||
void update_constrain_profile(const std::vector<Vec2d>& pts);
|
||||
|
||||
// Entity-aware Constrain (Fase 4.2): pick Line entities of a committed sketch.
|
||||
void begin_constrain_entities(const std::vector<SketchEntity>& ents, const SketchPlane& plane);
|
||||
bool is_constraining_entities() const;
|
||||
// Sketch selection, for the offer menu: how many entities are selected and what the first
|
||||
// one is. Returns 0 when nothing is selected.
|
||||
int sketch_selection_count() const;
|
||||
// Esc routing (DesignInteraction.hpp). The panel decides WHICH level one press belongs to;
|
||||
// these are the levels it can act on inside the canvas. Each returns whether it did anything,
|
||||
// so the panel can fall through to the next level without asking twice.
|
||||
bool sketch_abort_gesture(); // CadLevel::Gesture — drop the entity being drawn
|
||||
bool sketch_disarm_tool(); // CadLevel::Tool — armed sketch tool falls back to Select
|
||||
bool drawing_in_progress() const;// an entity has clicks down but is not committed
|
||||
bool has_any_selection() const; // model pick or sketch pick
|
||||
bool clear_any_selection(); // CadLevel::Idle — drop both; true if anything was dropped
|
||||
bool sketch_first_selected_type(SketchEntity::Type& out) const;
|
||||
// Live sketch session (Fase 4.2 live constraint path): the panel reads the in-session
|
||||
// selection and entities, and commits a planned constraint through the tool's
|
||||
// append->solve->keep-or-rollback, rather than reaching into mcp_sketch_tool().
|
||||
const std::vector<int>& sketch_selection() const;
|
||||
const std::vector<SketchEntity>& sketch_entities() const;
|
||||
// How many constraints the LIVE session holds. Only a count: the hint line needs to know
|
||||
// whether any badge is on screen to talk about, nothing more.
|
||||
int sketch_constraint_count() const;
|
||||
const std::vector<SketchEntityConstraintDef>& sketch_constraints() const;
|
||||
bool remove_sketch_constraint(int idx);
|
||||
void set_on_sketch_constraints_changed(std::function<void()> cb);
|
||||
bool try_add_sketch_constraints(const std::vector<SketchEntityConstraintDef>& defs);
|
||||
|
||||
// In-canvas bbox transform of imported Text/SVG art (replaces the Move/Scale dialog).
|
||||
void begin_imported_transform(int feat,
|
||||
const std::vector<std::vector<std::vector<Vec2d>>>& base_regions,
|
||||
const SketchPlane& plane, const Vec2d& offset,
|
||||
double scale_x, double scale_y);
|
||||
void set_on_imported_transform(std::function<void(int, Vec2d, double, double)> cb);
|
||||
bool selected_constrain_entities(int& e0, int& e1) const;
|
||||
int selected_constrain_axis() const; // third pick slot (Symmetric axis), -1 if unset
|
||||
bool pick0_point(Vec2d& out) const; // plane-coords of the slot-0 pick (trim/extend)
|
||||
void update_constrain_entities(const std::vector<SketchEntity>& ents);
|
||||
// Constraint manager (C3.4): highlight the entities referenced by a selected
|
||||
// constraint (yellow tint in Constrain mode); empty clears the highlight.
|
||||
void set_constraint_highlight(std::vector<int> entities);
|
||||
// Constraint glyph badges (C3.4b): the feature's constraints, drawn as iconic
|
||||
// marks near their entities in Constrain mode; empty clears them.
|
||||
void set_constraint_glyphs(std::vector<SketchEntityConstraintDef> cons);
|
||||
// Repaint the embedded canvas the right way for the active GL backend:
|
||||
// hardware GL gets a scheduled wxEVT_PAINT (render() runs inside the paint
|
||||
// cycle); software GL (llvmpipe etc.) gets a direct render() because a
|
||||
// scheduled Refresh() is frequently dropped there. Backend cached on first use.
|
||||
// Public: DesignPanel calls it after a tree edit to force a frame on software GL.
|
||||
// Scripted (MCP) access to the live sketch. One accessor rather than a passthrough per
|
||||
// verb: the MCP layer drives the SAME tool the mouse drives, which is the whole point of
|
||||
// having it — a socket that talked to a private copy would prove nothing about the app.
|
||||
DesignSketchTool& mcp_sketch_tool() { return m_sketch_tool; }
|
||||
const DesignSketchTool& mcp_sketch_tool() const { return m_sketch_tool; }
|
||||
|
||||
void request_repaint();
|
||||
// Repaint synchronously, once the pending show/resize has settled. Needed when the
|
||||
// notebook re-shows the Design page: an invalidation issued while the page is still
|
||||
// being shown is dropped on hardware GL and no wxEVT_PAINT ever follows, leaving the
|
||||
// canvas blank until another tab switch forces an expose.
|
||||
void force_repaint();
|
||||
// Repaint synchronously, for use while a modal popup (the offer menu) owns the event loop:
|
||||
// a queued Refresh() is not serviced until the popup closes, so a hover ghost drawn behind it
|
||||
// would never appear. Mirrors DesignPanel's m_status->Update() flush.
|
||||
void repaint_now();
|
||||
|
||||
private:
|
||||
void reload(bool keep_view);
|
||||
void swap_camera(); // enter_viewport / leave_viewport, in the one direction they share
|
||||
|
||||
wxGLCanvas* m_canvas_widget{nullptr};
|
||||
GLCanvas3D* m_canvas{nullptr};
|
||||
int m_sw_gl{-1}; // -1 unknown, 0 hardware GL, 1 software GL
|
||||
|
||||
std::function<void(const wxPoint&)> m_on_context_menu;
|
||||
bool m_ctx_bound{false}; // bind the RIGHT_UP handler once, however often the cb is set
|
||||
wxPoint m_ctx_press{0, 0}; // right-press origin: a right-DRAG orbits, it must not offer
|
||||
long long m_ctx_press_ms{0}; // and a right-HOLD is navigation too, however still it is held
|
||||
|
||||
Bed3D m_bed;
|
||||
// The half of the camera swap above that is NOT on screen: the editor tabs' view while
|
||||
// Design is up, this canvas's view while it is not. Seeded in the constructor so the first
|
||||
// entry inherits the view the user was already looking at.
|
||||
Camera m_parked_camera;
|
||||
bool m_camera_swapped{false}; // guards a leave without an enter, and the reverse
|
||||
Model m_model;
|
||||
bool m_first_frame{true};
|
||||
bool m_body_selected{false}; // tree selected a body feature → tint the solid
|
||||
int m_hl_body_target{-1};
|
||||
int m_hl_body_tool{-1};
|
||||
bool m_body_translucent{false};// fillet/chamfer preview → render the body see-through
|
||||
int m_xray_focus{-1}; // >=0: only this body is opaque+clickable (CoordSys picking)
|
||||
bool m_body_hidden{false}; // preview-only mode → hide base bodies, ghost = the result
|
||||
std::vector<bool> m_body_visible; // per-body visibility (empty => all visible)
|
||||
// Live pointer to the document's bodies (stable address: m_doc.bodies), stashed by
|
||||
// set_solid_pick so reload()/body_color() can read each body's colour override.
|
||||
const std::vector<CadBody>* m_color_bodies{nullptr};
|
||||
|
||||
DesignSketchTool m_sketch_tool;
|
||||
|
||||
// Section view: whether a horizontal clip is currently applied (guards Alt+Wheel). The cut
|
||||
// height and the named-view list live in DesignPanel; the canvas is a dumb applier.
|
||||
bool m_section_on{false};
|
||||
|
||||
std::unique_ptr<SketchInlineEditor> m_inline_editor; // floating in-canvas value editor
|
||||
// Bottom-right viewport HUD: a borderless float label over the GL canvas showing the
|
||||
// active tool's current values (fed by the tool's on_readout). Empty text hides it.
|
||||
// A wxPopupWindow for the SAME reason as the status chip below, and it was a wxFrame until
|
||||
// the reason was measured rather than assumed: "it appears mid-gesture and the next input is
|
||||
// the mouse" is false. The chip keeps the last value on screen AFTER the gesture ends, and a
|
||||
// frame holds the X input focus once it has it — so the next keystroke went to a 119x31
|
||||
// window that has no use for it. Measured on :10: focus on the chip, `r` produced no
|
||||
// CHAR_HOOK line at all; one bare canvas click moved focus back and the same key armed the
|
||||
// tool. That is every sketch shortcut dead after every dimensioned entity.
|
||||
wxPopupWindow* m_hud{nullptr};
|
||||
wxStaticText* m_hud_label{nullptr};
|
||||
std::string m_hud_last;
|
||||
void set_readout(const std::string& text);
|
||||
void place_readout_hud(); // anchor + show, using m_hud_last
|
||||
void show_readout_hud(bool on); // iconise/deactivate: a popup would float on the desktop
|
||||
|
||||
// Bottom-LEFT viewport HUD: the selection / tool status line, written by DesignPanel.
|
||||
// A wxPopupWindow, NOT the wxFrame the readout HUD uses: a frame accepts keyboard focus,
|
||||
// and this one is on screen permanently and re-raised on every status change, so it stole
|
||||
// the keyboard from the canvas and killed every sketch shortcut in the tab.
|
||||
wxPopupWindow* m_status_hud{nullptr};
|
||||
wxStaticText* m_status_hud_label{nullptr};
|
||||
wxString m_status_hud_last;
|
||||
wxColour m_status_hud_colour;
|
||||
void place_status_hud(); // re-anchors to the canvas corner (also on resize)
|
||||
void apply_status_label(); // SetLabel + Wrap to the canvas width + Fit, always together
|
||||
// On the top-level frame, which outlives this canvas — members so they can be unbound.
|
||||
void on_frame_iconize(wxIconizeEvent& e);
|
||||
void on_frame_activate(wxActivateEvent& e);
|
||||
void on_status_hud_reanchor(wxEvent& e); // frame wxEVT_MOVE and canvas wxEVT_SIZE
|
||||
std::function<void(const SketchProfile&, const SketchPlane&)> m_on_sketch_commit;
|
||||
std::function<void(const std::vector<SketchEntity>&,
|
||||
const std::vector<SketchEntityConstraintDef>&,
|
||||
const SketchPlane&)> m_on_sketch_entities_commit;
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_DesignCanvas_hpp_
|
||||
@@ -0,0 +1,59 @@
|
||||
#ifndef slic3r_GUI_DesignInteraction_hpp_
|
||||
#define slic3r_GUI_DesignInteraction_hpp_
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
// The Design tab's interaction stack, and the ONE rule Esc obeys.
|
||||
//
|
||||
// Esc unwinds exactly one level per press, deepest first, and never more. The enum value IS
|
||||
// the LIFO depth, so "which level does this press belong to" is a comparison, not a chain of
|
||||
// special cases scattered over three files — which is what it was, and why two presses in a
|
||||
// row could reach past a tool and destroy the sketch underneath it.
|
||||
//
|
||||
// STRICT INVARIANT (the bug this exists to make unrepresentable): no level of Esc deletes a
|
||||
// feature, discards a sketch that holds geometry, or rolls history back. Destroying work needs
|
||||
// a gesture that says so — Delete/Backspace on an explicit selection, the banner's Cancel, or
|
||||
// Ctrl+Z. An Esc that can destroy is an Esc nobody can press with confidence, and being the
|
||||
// safe key is the whole point of it.
|
||||
enum class CadLevel : int {
|
||||
Idle = 0, // nothing transient is up: Esc clears the selection
|
||||
Tool = 1, // a feature card / armed sketch tool / constrain session: Esc exits it
|
||||
Gesture = 2, // an uncommitted delta (entity being drawn, body being dragged): Esc reverts it
|
||||
Transient = 3, // a value field or a popup menu: Esc closes just that
|
||||
};
|
||||
|
||||
// What the tab is doing, reduced to the four bits the routing actually needs. Kept as a POD of
|
||||
// answers rather than a pointer to the panel so the rule below is decidable — and checkable —
|
||||
// without a window, a GL context or an event loop.
|
||||
struct CadInteractionState {
|
||||
bool value_field_open{false}; // in-canvas value field, or the panel's value card
|
||||
bool gesture_active{false}; // in-progress entity points, or a body being moved
|
||||
bool tool_armed{false}; // feature card open, sketch draw tool armed, constrain session
|
||||
bool has_selection{false}; // something is picked (model or sketch)
|
||||
};
|
||||
|
||||
// The whole routing rule. Deepest live level wins; Idle is the floor.
|
||||
constexpr CadLevel cad_escape_level(const CadInteractionState& s)
|
||||
{
|
||||
if (s.value_field_open) return CadLevel::Transient;
|
||||
if (s.gesture_active) return CadLevel::Gesture;
|
||||
if (s.tool_armed) return CadLevel::Tool;
|
||||
return CadLevel::Idle;
|
||||
}
|
||||
|
||||
// The ordering is the entire contract, so it is checked where it is defined, at compile time.
|
||||
static_assert(cad_escape_level({true, true, true, true}) == CadLevel::Transient, "value field is deepest");
|
||||
static_assert(cad_escape_level({false, true, true, true}) == CadLevel::Gesture, "gesture beats tool");
|
||||
static_assert(cad_escape_level({false, false, true, true}) == CadLevel::Tool, "tool beats idle");
|
||||
static_assert(cad_escape_level({false, false, false, true}) == CadLevel::Idle, "selection is idle-level");
|
||||
static_assert(cad_escape_level({false, false, false, false}) == CadLevel::Idle, "empty is idle");
|
||||
|
||||
// Right-click vs. right-hold-orbit. A press that stays put and is let go promptly is a click and
|
||||
// summons the offer; anything longer or further was navigation, and navigation must never be
|
||||
// rewarded with a menu over wherever the camera happened to stop.
|
||||
inline constexpr int kCadRightClickMs = 200; // press->release budget
|
||||
inline constexpr int kCadRightClickDriftPx = 3; // cursor drift budget, max(|dx|,|dy|)
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_GUI_DesignInteraction_hpp_
|
||||
@@ -0,0 +1,189 @@
|
||||
// GENERATED FILE — DO NOT EDIT.
|
||||
// Source: docs/ux/tool_atlas.json Generator: docs/ux/mockups/gen_offer_table.py
|
||||
//
|
||||
// The object-driven tool offer (charter 4.1): every verb has ONE row index, that index
|
||||
// is the same in every selection it appears in, and verbs that do not apply are shown
|
||||
// disabled in place with their reason rather than removed. Row order was ratified
|
||||
// 2026-07-31; changing an index is a breaking change to every user's muscle memory.
|
||||
#ifndef slic3r_GUI_DesignOffer_hpp_
|
||||
#define slic3r_GUI_DesignOffer_hpp_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
// What the viewport has selected. Ordered as in tool_atlas.json; the bitmask in
|
||||
// OfferVerb::accepts indexes these.
|
||||
enum class OfferSel : int {
|
||||
None = 0,
|
||||
FacePlanar = 1,
|
||||
FaceCyl = 2,
|
||||
FaceOther = 3,
|
||||
EdgeStr = 4,
|
||||
EdgeCirc = 5,
|
||||
Vertex = 6,
|
||||
BodySolid = 7,
|
||||
BodySheet = 8,
|
||||
Bodies2 = 9,
|
||||
DatumPlane = 10,
|
||||
DatumAxis = 11,
|
||||
CoordSys = 12,
|
||||
Art = 13,
|
||||
SkLoop = 14,
|
||||
SkNone = 15,
|
||||
SkLine = 16,
|
||||
SkArc = 17,
|
||||
SkPoint = 18,
|
||||
Sk2Ent = 19,
|
||||
Count = 20
|
||||
};
|
||||
|
||||
inline uint32_t offer_bit(OfferSel s) { return 1u << int(s); }
|
||||
|
||||
// One row of the offer. `action` routes to the code that already implements the verb:
|
||||
// "key:S+E" -> m_keys_feature[SHIFT('E')]
|
||||
// "key:L" -> m_keys_sketch['L']
|
||||
// "fly:material#4" -> row 4 of the "material" feature flyout
|
||||
// "btn:delete" -> a standalone toolbar button
|
||||
// nullptr -> kernel support exists, no GUI path yet (row shows disabled)
|
||||
struct OfferVerb {
|
||||
const char* id;
|
||||
const char* name; // drawing-office word (L10); translated at use with wxGetTranslation
|
||||
int row; // 0..7, the ratified index — NEVER reorder
|
||||
const char* key; // shortcut shown in the row, or nullptr
|
||||
const char* action;
|
||||
const char* refusal; // why this row is greyed, in the product's own words
|
||||
uint32_t accepts; // bitmask over OfferSel
|
||||
int need_bodies;
|
||||
int need_sketches;
|
||||
bool need_sheet;
|
||||
bool sketch_mode; // belongs to the sketch-mode vocabulary, not the model one
|
||||
// Second level INSIDE a row, for tools that come in variants: "Rectangle" holds corner,
|
||||
// centre, oblique and rounded. nullptr = sits directly in the row. Keeps the row's own
|
||||
// address fixed (L4.1) while the variants hang one level below it, mirroring the toolbar's
|
||||
// grouping instead of flattening 19 create tools into one wall.
|
||||
const char* family;
|
||||
const char* icon; // resources/images name, or nullptr — the offer draws it beside the row
|
||||
const char* hint; // what the verb does / what to click; shown on hover
|
||||
};
|
||||
|
||||
// Row labels, in ratified order.
|
||||
static const char* const kOfferRowNames[] = {
|
||||
"Create",
|
||||
"Add material",
|
||||
"Remove",
|
||||
"Fillet / chamfer / draft",
|
||||
"Repeat",
|
||||
"Transform",
|
||||
"Reference",
|
||||
"Modify",
|
||||
};
|
||||
static const int kOfferRowCount = 8;
|
||||
|
||||
static const OfferVerb kOfferVerbs[] = {
|
||||
{"sketch", "Sketch", 0, "Shift+S", "key:S+S", "Click a face or a reference plane in the viewport, then a sketch tool", 0x00000403u, 0, 0, false, false, nullptr, "design_sketch", "Click a face or a reference plane, then pick a drawing tool"},
|
||||
{"extrude", "Extrude", 1, "Shift+E", "key:S+E", "Create a sketch, or pick a solid face, first", 0x00004002u, 0, 0, false, false, nullptr, "design_extrude", "Extrude a sketch profile, or push/pull a picked face"},
|
||||
{"revolve", "Revolve", 1, "Shift+R", "key:S+R", "Create a sketch profile to revolve first", 0x00004000u, 0, 0, false, false, nullptr, "design_revolve", "Revolve a profile about an axis"},
|
||||
{"sweep", "Sweep", 1, "Shift+W", "key:S+W", "Create a profile sketch to sweep first", 0x00004000u, 0, 2, false, false, nullptr, "design_sweep", "Sweep a profile along a path"},
|
||||
{"loft", "Loft", 1, "Shift+L", "key:S+L", "Create at least two profile sketches to loft", 0x00004000u, 0, 2, false, false, nullptr, "design_loft", "Loft (skin) between two or more profiles"},
|
||||
{"thicken", "Thicken", 1, nullptr, "fly:material#4", "Thicken needs a solid body — add or import one first", 0x0000000au, 1, 0, false, false, nullptr, "design_thicken", "Offset a solid face into a thin plate (new body)"},
|
||||
{"rib", "Rib", 1, nullptr, "fly:material#5", "Rib needs a solid body — add or import one first", 0x00010000u, 1, 0, false, false, nullptr, "design_rib", "Grow a thin wall from an open sketch line, fused to a body"},
|
||||
{"boolean", "Union", 1, "Shift+B", "btn:bool#0", "Boolean needs two bodies — create or import a second solid", 0x00000200u, 2, 0, false, false, nullptr, "design_boolean", "Fuse the tool body into the target — one solid, no seam"},
|
||||
{"bool_subtract", "Subtract", 1, nullptr, "btn:bool#1", "Boolean needs two bodies — create or import a second solid", 0x00000200u, 2, 0, false, false, nullptr, "design_boolean", "Cut the tool body out of the target"},
|
||||
{"bool_intersect", "Intersect", 1, nullptr, "btn:bool#2", "Boolean needs two bodies — create or import a second solid", 0x00000200u, 2, 0, false, false, nullptr, "design_boolean", "Keep only where the two bodies overlap"},
|
||||
{"surf_extrude", "Surface Extrude", 1, "Shift+G", "key:S+G", "Create a sketch first", 0x00004000u, 0, 0, false, false, nullptr, "design_extrude", "Extrude a sketch into a sheet body (no end caps)"},
|
||||
{"surf_revolve", "Surface Revolve", 1, nullptr, "fly:surface#1", "Create a sketch profile to revolve first", 0x00004000u, 0, 0, false, false, nullptr, "design_revolve", "Revolve a sketch profile into a sheet body"},
|
||||
{"surf_loft", "Surface Loft", 1, nullptr, "fly:surface#2", "Create at least two profile sketches to loft", 0x00004000u, 0, 2, false, false, nullptr, "design_loft", "Loft (skin) between 2+ profiles, open (no end caps)"},
|
||||
{"surf_fill", "Surface Fill", 1, nullptr, "fly:surface#3", "Create a closed sketch first", 0x00004000u, 0, 0, false, false, nullptr, "design_surface", "Fill a sketch boundary with a smooth face"},
|
||||
{"thicken_surf", "Thicken Surface", 1, nullptr, "fly:surface#5", "target is not a sheet body", 0x00000100u, 0, 0, true, false, nullptr, "design_thicken", "Thicken a sheet body into a solid"},
|
||||
{"hole", "Hole", 2, "Shift+H", "key:S+H", "Pick a face or a plane to drill into", 0x00000402u, 1, 0, false, false, nullptr, "design_hole", "Drill a hole, centred on a picked face or placed on a plane"},
|
||||
{"thread", "Thread", 2, "Shift+T", "key:S+T", "Pick a cylindrical surface (bore / outer) or a circular edge for a thread", 0x00000024u, 1, 0, false, false, nullptr, "design_thread", "Thread a cylindrical surface (inner bore / outer) or a circular edge"},
|
||||
{"shell", "Shell", 2, "Shift+K", "key:S+K", "Shell needs a solid body", 0x00000082u, 1, 0, false, false, nullptr, "design_shell", "Hollow the body to a wall thickness, opening a picked face"},
|
||||
{"cut", "Cut", 2, "Shift+X", "key:S+X", "Create a solid body to cut first", 0x000004feu, 1, 0, false, false, nullptr, "design_cut", "Trim the body with a plane — drag the offset arrow; keep one half or both"},
|
||||
{"split", "Split", 2, nullptr, nullptr, "Split needs a solid body", 0x000000feu, 1, 0, false, false, nullptr, nullptr, "Split the body along a picked face into two solids"},
|
||||
{"fillet", "Fillet", 3, "Shift+F", "btn:dress#0", "Pick an edge to round", 0x000000b2u, 1, 0, false, false, nullptr, "design_filletedge", "Pick an edge, then drag the radius arrow or type it"},
|
||||
{"chamfer", "Chamfer", 3, nullptr, "btn:dress#1", "Pick an edge to bevel", 0x000000b2u, 1, 0, false, false, nullptr, "design_chamfer", "Pick an edge, then drag the distance arrow or type it"},
|
||||
{"draft", "Draft", 3, "Shift+D", "key:S+D", "Pick a face to taper", 0x0000000au, 1, 0, false, false, nullptr, "design_draft", "Tilt a picked face by a draft angle"},
|
||||
{"surf_offset", "Surface Offset", 3, nullptr, "fly:surface#4", "target is not a sheet body", 0x00000100u, 0, 0, true, false, nullptr, "design_offset", "Offset a sheet body's shell by a signed distance"},
|
||||
{"pattern", "Linear pattern", 4, "Shift+N", "btn:pat#0", "Create a solid body to pattern first", 0x00006082u, 1, 0, false, false, nullptr, "design_array", "Repeat the body along a direction — drag the spacing, set the count"},
|
||||
{"pattern_circular", "Circular pattern", 4, nullptr, "btn:pat#1", "Create a solid body to pattern first", 0x00006082u, 1, 0, false, false, nullptr, "design_polararray", "Repeat the body around an axis — set the count and sweep"},
|
||||
{"mirror", "Mirror", 4, "Shift+Z", "key:S+Z", "Mirror needs a body — add or import one first", 0x000004feu, 1, 0, false, false, nullptr, "design_mirror", "Reflect a body about a plane"},
|
||||
{"pat_curve", "Pattern on Curve", 4, nullptr, nullptr, "Pattern on curve needs a body and a curve", 0x00000090u, 1, 0, false, false, nullptr, nullptr, "Repeat the body along a picked curve"},
|
||||
{"transform", "Move", 5, "Shift+Y", "key:S+Y", "Transform needs a body — add or import one first", 0x000021feu, 1, 0, false, false, nullptr, "design_move", "Move and/or rotate an existing body"},
|
||||
{"mate", "Mate", 5, nullptr, "fly:placement#2", "A mate needs two coordinate systems", 0x00001202u, 2, 0, false, false, nullptr, "design_c_coincident", "Assembly: align two CoordSys features (fastened, planar, revolute, slider, cylindrical)"},
|
||||
{"align", "Align to", 5, nullptr, nullptr, "Align needs a body", 0x00000002u, 1, 0, false, false, nullptr, nullptr, "Align the body to a picked face or plane"},
|
||||
{"plane", "Plane", 6, "Shift+P", "key:S+P", nullptr, 0x00000453u, 0, 0, false, false, nullptr, "design_plane", "Reference plane (offset / tilt / midplane / tangent / two edges / coincident)"},
|
||||
{"axis", "Axis", 6, "Shift+A", "key:S+A", nullptr, 0x00000057u, 0, 0, false, false, nullptr, "design_line", "Datum axis (two points, face normal, cylinder centerline, two planes, along edge)"},
|
||||
{"coordsys_v", "Coord Sys", 6, "Shift+C", "key:S+C", nullptr, 0x00000043u, 0, 0, false, false, nullptr, "design_point", "Datum coordinate system (world point, or face + direction edge)"},
|
||||
{"helix", "Helix", 6, nullptr, "fly:plane#3", nullptr, 0x00000405u, 0, 0, false, false, nullptr, "design_thread", "Helical curve (spring path) — use as a sweep path for coils / springs / augers"},
|
||||
{"project", "Project", 6, nullptr, "fly:plane#4", "Project needs a body — add or import one first", 0x00000482u, 1, 0, false, false, nullptr, "design_sketch", "Project body edges onto a plane as sketch entities"},
|
||||
{"measure", "Measure", 6, nullptr, nullptr, nullptr, 0x000b03feu, 0, 0, false, false, nullptr, nullptr, "Measure between the picked points, edges or faces"},
|
||||
{"mass_props", "Mass", 6, nullptr, "btn:mass", nullptr, 0x000000feu, 1, 0, false, false, nullptr, "info", "Report the volume and surface area of the selected body"},
|
||||
{"interference", "Interference", 6, nullptr, nullptr, nullptr, 0x00000200u, 2, 0, false, false, nullptr, nullptr, "Check whether two bodies overlap — reports, changes nothing"},
|
||||
{"edit_feature", "Edit", 7, nullptr, "btn:edit", nullptr, 0x00007d8eu, 0, 0, false, false, nullptr, "design_edit", "Reopen the selected feature to change what it was made from"},
|
||||
{"rename", "Rename…", 7, "F2", "btn:rename", "Select a feature, or a body, to rename it", 0x00004080u, 0, 0, false, false, nullptr, nullptr, "Give this feature a name you will recognise in the tree (a body takes its name from the feature that makes it)"},
|
||||
{"delete_face", "Delete Face", 7, nullptr, "fly:dressup#3", "Delete Face needs a body — add or import one first", 0x0000000eu, 1, 0, false, false, nullptr, "design_delete", "Remove faces from a body and heal the solid"},
|
||||
{"colour", "Colour", 7, nullptr, "btn:colour", nullptr, 0x000001feu, 1, 0, false, false, nullptr, "color_palette", "Set the selected body's display colour"},
|
||||
{"delete", "Delete", 7, "Del", "btn:delete", nullptr, 0x000f7c00u, 0, 0, false, false, nullptr, "design_delete", "Delete what is selected"},
|
||||
{"delete_body", "Delete Body", 7, nullptr, "btn:delete_body", nullptr, 0x000001feu, 1, 0, false, false, nullptr, "design_delete", "Delete this whole body — removes the feature it was made from"},
|
||||
{"sk_line_t", "Line", 0, "L", "key:L", nullptr, 0x000f8000u, 0, 0, false, true, "Line", "design_line", "Line — click start, then end"},
|
||||
{"sk_polyline", "Polyline", 0, nullptr, "fly:design_line#1", nullptr, 0x000f8000u, 0, 0, false, true, "Line", "design_polyline", "Click points; click the first point to close the loop, right-click to end it open"},
|
||||
{"sk_rect", "Corner rectangle", 0, "R", "key:R", nullptr, 0x000f8000u, 0, 0, false, true, "Rectangle", "design_rect", "Rectangle — click two opposite corners"},
|
||||
{"sk_rect_center", "Centre rectangle", 0, nullptr, "fly:design_rect#1", nullptr, 0x000f8000u, 0, 0, false, true, "Rectangle", "design_crect", "Click center, then a corner"},
|
||||
{"sk_rect_oblique", "Oblique rectangle", 0, nullptr, "fly:design_rect#2", nullptr, 0x000f8000u, 0, 0, false, true, "Rectangle", "design_rect_oblique", "Click two corners of one edge, then a point for the width"},
|
||||
{"sk_rect_rounded", "Rounded rectangle", 0, nullptr, "fly:design_rect#3", nullptr, 0x000f8000u, 0, 0, false, true, "Rectangle", "design_rect_rounded", "Click two opposite corners, then a point for the corner radius"},
|
||||
{"sk_circle", "Centre circle", 0, "C", "key:C", nullptr, 0x000f8000u, 0, 0, false, true, "Circle", "design_circle", "Circle — click center, then radius"},
|
||||
{"sk_circle_2pt", "2-point circle", 0, nullptr, "fly:design_circle#1", nullptr, 0x000f8000u, 0, 0, false, true, "Circle", "design_circle2pt", "Click two ends of the diameter"},
|
||||
{"sk_circle_3pt", "3-point circle", 0, nullptr, "fly:design_circle#2", nullptr, 0x000f8000u, 0, 0, false, true, "Circle", "design_circle3pt", "Click three points on the circle"},
|
||||
{"sk_arc_t", "3-point arc", 0, "A", "key:A", nullptr, 0x000f8000u, 0, 0, false, true, "Arc", "design_arc3pt", "Arc — click start, end, then a point"},
|
||||
{"sk_arc_tangent", "Tangent arc", 0, nullptr, "fly:design_arc3pt#1", nullptr, 0x000f8000u, 0, 0, false, true, "Arc", "design_tangentarc", "Click start (on the last entity) then end"},
|
||||
{"sk_arc_center", "Centre-point arc", 0, nullptr, "fly:design_arc3pt#2", nullptr, 0x000f8000u, 0, 0, false, true, "Arc", "design_arc_center", "Click center, then start, then a point for the end angle"},
|
||||
{"sk_slot", "Slot", 0, "S", "key:S", nullptr, 0x000f8000u, 0, 0, false, true, "Slot", "design_slot", "Slot — two centerline ends, then end radius"},
|
||||
{"sk_slot_arc", "Arc slot", 0, nullptr, "fly:design_slot#1", nullptr, 0x000f8000u, 0, 0, false, true, "Slot", "design_slot_arc", "Click center, start, end, then a point for the width"},
|
||||
{"sk_ellipse", "Ellipse", 0, "E", "key:E", nullptr, 0x000f8000u, 0, 0, false, true, "Ellipse", "design_ellipse", "Ellipse — center, major end, minor point"},
|
||||
{"sk_ellipse_arc", "Elliptical arc", 0, nullptr, "fly:design_ellipse#1", nullptr, 0x000f8000u, 0, 0, false, true, "Ellipse", "design_ellipse_arc", "Click center, major-axis end, minor point, then arc start and end"},
|
||||
{"sk_spline", "Spline", 0, "B", "key:B", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_bspline", "Spline — click control points"},
|
||||
{"sk_poly_3", "Triangle", 0, nullptr, "btn:poly#3", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Triangle — click centre, then a vertex"},
|
||||
{"sk_poly_4", "Square", 0, nullptr, "btn:poly#4", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Square — click centre, then a vertex"},
|
||||
{"sk_poly_5", "Pentagon", 0, nullptr, "btn:poly#5", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Pentagon — click centre, then a vertex"},
|
||||
{"sk_polygon", "Hexagon", 0, "G", "btn:poly#6", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Hexagon — click centre, then a vertex"},
|
||||
{"sk_poly_8", "Octagon", 0, nullptr, "btn:poly#8", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Octagon — click centre, then a vertex"},
|
||||
{"sk_poly_12", "Dodecagon", 0, nullptr, "btn:poly#12", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Dodecagon — click centre, then a vertex"},
|
||||
{"sk_poly_inscribed", "Inscribed", 0, nullptr, "btn:polyfit#0", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Measure the polygon to its corners (inscribed)"},
|
||||
{"sk_poly_circumscribed", "Circumscribed", 0, nullptr, "btn:polyfit#1", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Measure the polygon to its flats (circumscribed)"},
|
||||
{"sk_point_t", "Point", 0, "P", "key:P", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_point", "Point — click to place"},
|
||||
{"sk_text", "Text", 0, nullptr, "btn:text", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_text", "Type text; its outline is added to this sketch as editable lines"},
|
||||
{"sk_svg", "SVG", 0, nullptr, "btn:svg", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_svg", "Import an SVG outline into this sketch as editable lines"},
|
||||
{"sk_offset", "Offset", 1, "O", "key:O", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_offset", "Offset — pick an entity, drag the distance"},
|
||||
{"sk_trim", "Trim", 2, "T", "key:T", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_trim", "Trim — click a segment to trim it"},
|
||||
{"sk_fillet", "Fillet", 3, "F", "key:F", nullptr, 0x00090000u, 0, 0, false, true, nullptr, "design_filletedge", "Fillet — pick two lines, set the radius"},
|
||||
{"sk_chamfer", "Chamfer", 3, "H", "key:H", nullptr, 0x00090000u, 0, 0, false, true, nullptr, "design_chamfer", "Chamfer — pick two lines, set the distance"},
|
||||
{"sk_array", "Linear array", 4, nullptr, "fly:design_array#0", nullptr, 0x000b0000u, 0, 0, false, true, "Array", "design_array", "Pick entities, drag the spacing handle, click the count; click empty to apply"},
|
||||
{"sk_array_polar", "Polar array", 4, nullptr, "fly:design_array#1", nullptr, 0x000b0000u, 0, 0, false, true, "Array", "design_polararray", "Pick entities, drag the sweep handle, click the count; click empty to apply"},
|
||||
{"sk_mirror", "Mirror", 4, "M", "key:M", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_mirror", "Mirror — pick axis, then entities"},
|
||||
{"sk_move", "Move", 5, nullptr, "fly:design_move#0", nullptr, 0x000f0000u, 0, 0, false, true, "Move", "design_move", "Pick entities, then drag the handle or click the distance; click empty to apply"},
|
||||
{"sk_rotate", "Rotate", 5, nullptr, "fly:design_move#1", nullptr, 0x000f0000u, 0, 0, false, true, "Move", "design_rotate", "Pick entities, then drag around the pivot or click the angle; click empty to apply"},
|
||||
{"sk_scale", "Scale", 5, nullptr, "fly:design_move#2", nullptr, 0x000f0000u, 0, 0, false, true, "Move", "design_scale", "Pick entities, then drag the handle or click the factor; click empty to apply"},
|
||||
{"sk_dimension", "Dimension", 6, "D", "key:D", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_dimension", "Dimension — click 2 points or an entity"},
|
||||
{"sk_constrain", "Constrain", 6, "K", "key:K", nullptr, 0x000f0000u, 0, 0, false, true, nullptr, "design_constrain", "Constrain the selected sketch entities to each other"},
|
||||
// Same verb, model-mode vocabulary: offered when a SKETCH is selected (bit 14, SkLoop), the
|
||||
// state a user is in right after finishing one. Without this row the only way in was the
|
||||
// toolbar icon, and constraints read as absent — see the Onshape-comparison report.
|
||||
{"constrain", "Constrain sketch", 7, nullptr, "btn:constrain", "Select a sketch to constrain it", 0x00004000u, 0, 1, false, false, nullptr, "design_constrain", "Add dimensions and relations (coincident, tangent, parallel...) to the selected sketch"},
|
||||
{"sk_construct", "Construction", 6, "Q", "key:Q", nullptr, 0x000b8000u, 0, 0, false, true, nullptr, nullptr, "Toggle construction: geometry that guides but is never built"},
|
||||
{"sk_extend", "Extend", 7, "X", "key:X", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_extend", "Extend — click a line/arc to extend it"},
|
||||
{"sk_delete", "Delete", 7, "Del", "btn:sk_delete", nullptr, 0x000f0000u, 0, 0, false, true, nullptr, "design_delete", "Delete the selected sketch entities"},
|
||||
// Typing the defining number of the element you pointed at. Three rows rather than one so
|
||||
// each names the quantity in the drawing-office word for THAT element; all three land on
|
||||
// the same handler, because dimension_kind() already resolves the quantity from the
|
||||
// selection. Without these, an element's own numbers were reachable only by arming the
|
||||
// Dimension tool and re-picking geometry that was already selected.
|
||||
{"sk_length", "Length…", 7, "V", "key:V", nullptr, 0x00010000u, 0, 0, false, true, nullptr, "design_dimension", "Type the length of this line"},
|
||||
{"sk_radius", "Radius / diameter…", 7, "V", "key:V", nullptr, 0x00020000u, 0, 0, false, true, nullptr, "design_dimension", "Type the radius of this arc, or the diameter of this circle"},
|
||||
{"sk_angdist", "Angle / distance…", 7, "V", "key:V", nullptr, 0x00080000u, 0, 0, false, true, nullptr, "design_dimension", "Type the angle between two lines, or the distance between the two picks"},
|
||||
};
|
||||
static const int kOfferVerbCount = 92;
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_GUI_DesignOffer_hpp_
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,919 @@
|
||||
#ifndef slic3r_DesignPanel_hpp_
|
||||
#define slic3r_DesignPanel_hpp_
|
||||
|
||||
#include <wx/panel.h>
|
||||
#include <wx/scrolwin.h>
|
||||
#include <wx/treebase.h> // wxTreeItemId
|
||||
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
|
||||
#include "libslic3r/CAD/CadDocument.hpp"
|
||||
#include "slic3r/GUI/CAD/DesignInteraction.hpp" // CadLevel: what one Esc press means
|
||||
|
||||
class ComboBox; // Orca dropdown (Widgets/ComboBox.hpp) — replaces wxChoice everywhere here
|
||||
class StaticBox; // Orca rounded card frame (Widgets/StaticBox.hpp)
|
||||
class wxCheckBox;
|
||||
class wxCheckListBox;
|
||||
class wxSpinCtrl;
|
||||
class wxSpinCtrlDouble;
|
||||
class wxTreeCtrl;
|
||||
class wxImageList;
|
||||
class wxStaticText;
|
||||
class wxStaticLine;
|
||||
class Button; // Orca-styled button (Widgets/Button.hpp)
|
||||
class CheckBox; // Orca teal checkbox (Widgets/CheckBox.hpp)
|
||||
class wxSizer;
|
||||
// wxBoxSizer, wxTextCtrl and wxListCtrl are used here as pointers only, so a forward
|
||||
// declaration is enough — but they must be declared. Every ordinary build happened to pull
|
||||
// them in transitively through the wx/panel.h + wx/scrolwin.h chain. The Snapmaker fork's
|
||||
// Flatpak build does not, and it failed to compile this header with "'wxTextCtrl' does not
|
||||
// name a type; did you mean 'wxTreeCtrl'?". Declaring them keeps the header self-contained
|
||||
// instead of relying on whatever a particular wx configuration happens to include.
|
||||
class wxBoxSizer;
|
||||
class wxTextCtrl;
|
||||
class wxListCtrl;
|
||||
class wxButton;
|
||||
class wxPanel;
|
||||
class ScalableButton;
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
class DesignCanvas;
|
||||
|
||||
// Design (CAD) tab: a sketch-first, Onshape-style form-driven CAD panel.
|
||||
// Sketch and Extrude are independent tools: the user creates a Sketch first,
|
||||
// then selects it and Extrudes to produce a solid.
|
||||
class DesignPanel : public wxPanel
|
||||
{
|
||||
public:
|
||||
explicit DesignPanel(wxWindow* parent);
|
||||
void on_tab_shown(); // re-sync bed to the active printer when the Design tab is activated
|
||||
void on_tab_hidden(); // another tab took over: take the viewport status line down with us
|
||||
void unbind_canvas_event_handlers(); // app close / language switch, from the plater's teardown
|
||||
void reset_canvas_volumes();
|
||||
void clear_document(); // New Project / Open Project: drop the document with the project
|
||||
// Rebuild off the UI thread (progress dialog only if it turns out to be slow), so a feature
|
||||
// op on a heavy imported solid does not freeze the window. Returns m_doc.recompute()'s result.
|
||||
// Push the document's recipe into the Model so ANY save path persists it (vjk5).
|
||||
void sync_recipe_to_model();
|
||||
bool recompute_guarded(const wxString& message);
|
||||
|
||||
// MCP control hooks: let the external control server (McpControl.cpp) drive and
|
||||
// perceive the SAME kernel the GUI uses. Called only on the wx main thread.
|
||||
CadDocument& mcp_doc() { return m_doc; } // live document (read + mutate)
|
||||
void mcp_after_change() { after_tree_edit(true); } // refresh tree + viewport + status
|
||||
DesignCanvas* mcp_viewport() { return m_viewport; } // live sketch + 3D view
|
||||
// Put the PANEL into (or out of) sketch mode, not just the canvas tool. Measured on the
|
||||
// rig: a sketch started straight through DesignCanvas::begin_sketch leaves m_ui_mode at
|
||||
// Feature, and the keyboard map is dispatched on `m_ui_mode == UiMode::Sketch` while the
|
||||
// offer menu is dispatched on the looser sketch_map_applies() — so the menu offered the
|
||||
// line's verbs while every sketch shortcut was dead (KEYTRACE: key=81 ui_mode=0
|
||||
// is_sketching=1). Half-entering a mode is worse than not entering it.
|
||||
void mcp_set_sketch_mode(bool on)
|
||||
{
|
||||
set_ui_mode(on ? UiMode::Sketch : UiMode::Feature);
|
||||
update_action_bar();
|
||||
}
|
||||
// The offer-table vocabulary without a right-click: the external controller asks which verbs
|
||||
// exist (and which apply to the current selection) and fires one by id, so a deck key names a
|
||||
// verb instead of spending a letter and every verb is reachable — including the rows with no
|
||||
// keyboard shortcut, which are otherwise invisible to anything that parses key tables.
|
||||
int mcp_offer_selection_kind() const { return offer_selection_kind(); } // OfferSel as int
|
||||
void mcp_run_action(const char* action) { run_offer_action(action); } // dispatch an action string
|
||||
// Defined out of line in DesignPanel.cpp: it needs kOfferVerbs, which this header deliberately
|
||||
// does not include (the table is generated and belongs to the offer-menu code).
|
||||
bool mcp_run_verb(const char* verb_id);
|
||||
|
||||
private:
|
||||
enum class Tool { None, Sketch, Extrude, Dressup, Hole, Thread, Shell, Revolve, Sweep, Pattern, Plane, Loft, Draft, Boolean, Cut, Insert, Axis, CoordSys, SurfaceExtrude, SurfaceRevolve, SurfaceLoft, SurfaceFill, SurfaceOffset, ThickenSurface, Transform, Mirror, Thicken, Rib, Project, DeleteFace, Helix, Mate };
|
||||
// Which numeric fields an expression can be bound to, per feature type. A member rather
|
||||
// than a file-static helper so Tool — 32 values of purely internal card state — does not
|
||||
// have to become part of this panel's public API just to be named in a signature.
|
||||
static std::vector<std::string> fields_for_tool(Tool t);
|
||||
// Plane tool: which datum reference the next solid pick fills (declared early so the
|
||||
// method decls + card lambdas below can name it).
|
||||
enum class PlanePick { None, FaceA, FaceB, EdgeA, EdgeB };
|
||||
enum class AxisPick { None, Face, Edge };
|
||||
enum class CoordSysPick { None, Face, Edge };
|
||||
|
||||
// Onshape-style contextual top toolbar: only the active mode's tool group is
|
||||
// shown (Feature = sketch/extrude/dress/hole/thread; Sketch = entity tools;
|
||||
// Constrain = constraints + edit ops). Replaces the old always-visible wall.
|
||||
enum class UiMode { Feature, Sketch, Constrain };
|
||||
void set_ui_mode(UiMode m);
|
||||
void apply_dof_status(int dof, bool ok, bool has_constraints);
|
||||
// Unified action-bar dispatch: one Confirm / one Cancel for every tool and mode.
|
||||
void tool_confirm(); // ✓ : commit the active feature / sketch / constrain session
|
||||
void tool_cancel(); // ✗ : cancel the active feature / discard / exit
|
||||
// Esc. ONE press unwinds ONE level of the interaction stack (DesignInteraction.hpp), and no
|
||||
// level of it destroys committed work. escape_level() answers which level the press belongs
|
||||
// to; escape() acts on exactly that one. Every Esc in the tab routes through here — the key
|
||||
// used to be handled in four places that could not see each other, and that is how two
|
||||
// presses in a row reached past a tool and discarded the sketch under it.
|
||||
CadLevel escape_level() const;
|
||||
void escape();
|
||||
void update_action_bar(); // show the ✓/✗ bar iff a tool or mode is active
|
||||
|
||||
void on_shape_changed();
|
||||
void on_add_sketch();
|
||||
void on_add_extrude();
|
||||
void on_add_dressup();
|
||||
void on_add_hole();
|
||||
void on_add_thread();
|
||||
void apply_thread_standard(); // fill pitch/depth/radius from m_thread_std selection
|
||||
void infer_thread_spec(double diameter); // nearest M-standard from a picked cylinder diameter
|
||||
void on_add_revolve();
|
||||
void on_add_sweep();
|
||||
void on_add_loft();
|
||||
void on_add_pattern();
|
||||
bool on_add_plane(); // false = refused, card stays open
|
||||
void arm_plane_pick(PlanePick target); // Plane tool: next solid pick fills this reference
|
||||
void apply_plane_refs(CadFeature& f) const; // copy type + face/edge refs + sizes from the card
|
||||
void refresh_plane_labels(); // update the 4 pick labels from the captured refs
|
||||
void reset_plane_refs(); // clear captured refs (fresh Plane add)
|
||||
void on_add_shell();
|
||||
void on_add_draft();
|
||||
void on_add_boolean();
|
||||
void on_add_cut(); // commit a plane Cut (split-by-plane)
|
||||
void on_add_axis();
|
||||
void arm_axis_pick(AxisPick target);
|
||||
void apply_axis_refs(CadFeature& f) const;
|
||||
void refresh_axis_labels();
|
||||
void reset_axis_refs();
|
||||
void on_add_coordsys();
|
||||
void arm_coordsys_pick(CoordSysPick target);
|
||||
void apply_coordsys_refs(CadFeature& f) const;
|
||||
void refresh_coordsys_labels();
|
||||
void refresh_cs_body_choice(); // fill the CoordSys body chooser from current document
|
||||
void reset_coordsys_refs();
|
||||
void on_add_surface_extrude();
|
||||
void on_add_surface_revolve();
|
||||
void on_add_surface_loft();
|
||||
void on_add_surface_fill();
|
||||
void on_add_surface_offset();
|
||||
void on_add_thicken_surface();
|
||||
void on_add_transform();
|
||||
void xf_live_preview(); // typed Transform fields -> body display transform (live)
|
||||
void xf_clear_preview(); // hand a previewed body back to its pre-card pose
|
||||
void on_add_mirror();
|
||||
void on_add_thicken();
|
||||
void on_add_rib();
|
||||
void on_add_project();
|
||||
void on_add_delete_face();
|
||||
void on_add_helix();
|
||||
void on_add_mate();
|
||||
void on_check_interference();
|
||||
void on_mass_properties(); // read-only report on the selected solid; edits nothing
|
||||
// Fill m_bool_target / m_bool_tool / m_cut_target. as_of_feature < 0 = current bodies (add);
|
||||
// >= 0 = the bodies as they existed just before that feature index (Boolean re-edit, so a
|
||||
// consumed tool body still appears and its saved selection round-trips).
|
||||
// Which body a tool should act on when it opens: the one picked in the VIEWPORT, else
|
||||
// the first. Selection comes first and the tool consumes it — every body combo used to
|
||||
// default to index 0, so picking body 3 and opening Mirror silently mirrored body 1.
|
||||
// Clamped to the list, so it is safe to hand straight to SetSelection. e1p.
|
||||
int selected_body_default() const;
|
||||
void populate_body_choices(int as_of_feature = -1);
|
||||
// Fill `c` with the bodies as they existed just before `as_of_feature` and select
|
||||
// `want`. Re-editing any feature that stores a body index needs this: the index was
|
||||
// recorded against the body list at that point in the timeline, not the final one.
|
||||
void fill_body_choice(ComboBox* c, int as_of_feature, int want);
|
||||
void populate_sheet_body_choices(ComboBox* c) const; // bodies where is_sheet_shape() is true
|
||||
// Rows of a sheet-filtered picker are not body indices; go through these two, never
|
||||
// GetSelection()/SetSelection() directly.
|
||||
static int sheet_choice_body(ComboBox* c); // real body index of the current row, or -1
|
||||
static void select_sheet_choice(ComboBox* c, int body);// select the row holding this body index
|
||||
// Import rigid 2D art (Text / SVG) as a new Sketch feature carrying
|
||||
// imported_regions (no solver entities). on_add_text/on_import_svg gather
|
||||
// input; add_imported_sketch builds the feature, refreshes tree + display.
|
||||
void on_add_text();
|
||||
void on_import_svg();
|
||||
void on_import_step(); // STEP -> editable B-rep body (keeps the OCCT solid, not a mesh)
|
||||
void on_import_mesh(); // STL/OBJ -> B-rep body via GeometryEngine::mesh_to_brep
|
||||
bool place_on_face(); // Prepare's Place on Face (F): lay the selected body face on the bed
|
||||
void add_imported_sketch(const std::vector<std::vector<std::vector<Vec2d>>>& regions,
|
||||
const wxString& base_name);
|
||||
// Imported Text/SVG art is placed/sized in-canvas then explicitly committed via a
|
||||
// small Confirm/Cancel card (Onshape Button->Dialog->Preview->Confirm). The feature
|
||||
// is added provisionally by add_imported_sketch; Confirm keeps it, Cancel undoes it.
|
||||
void open_insert_card(const wxString& base_name);
|
||||
void finalize_insert(); // Confirm: keep the placed art, leave the placement gizmo
|
||||
void cancel_insert(); // Cancel: undo the provisional insert
|
||||
// Move / enlarge / stretch (independent X/Y) an imported Text/SVG sketch:
|
||||
// a modal dialog editing the feature's placement transform in place.
|
||||
void on_transform_imported(int feat_idx);
|
||||
void on_commit();
|
||||
void on_export_step(); // write all bodies to a .step file (native B-rep)
|
||||
// Rehydrate the parametric model from a project's saved recipe (3MF
|
||||
// Metadata/orca_cad.bin): deserialize -> recompute -> refresh viewport + tree.
|
||||
void load_recipe(const std::string& blob);
|
||||
void refresh_tree();
|
||||
void set_status_ok();
|
||||
|
||||
// Feature-tree editing (Onshape-style): act on the selected tree row.
|
||||
void on_delete_feature();
|
||||
// "Delete Body" — the geometry-first counterpart, reached by pointing at a body or any of
|
||||
// its faces. Resolves the body to the feature that created it and removes THAT, because a
|
||||
// body is a recomputed result and has nothing else to delete.
|
||||
void on_delete_body();
|
||||
void on_new_design();
|
||||
void on_move_feature(int delta); // -1 = up, +1 = down
|
||||
void on_toggle_visibility(); // show/hide the selected feature (CadFeature::enabled)
|
||||
|
||||
// Constrain mode: enter on the tree-selected sketch, then apply a geometric
|
||||
// constraint to the in-canvas picked segment and re-solve in the kernel.
|
||||
void on_begin_constrain(int sel_override = -1);
|
||||
// Sketch-toolbar Constrain entry: commit the live sketch in place, then enter Constrain
|
||||
// mode on it (so the constraint palette + Trim/Extend are reachable without leaving the
|
||||
// sketch flow). Returns true if constrain mode was entered.
|
||||
bool enter_constrain_inline();
|
||||
void apply_constraint(SketchConstraintType type);
|
||||
void apply_entity_constraint(SketchConstraintType type); // Fase 4.2 entity path
|
||||
void apply_live_constraint(SketchConstraintType type); // Fase 4.2 live-sketch path (no commit needed)
|
||||
enum class EditOp { Mirror, Offset, Fillet, Trim, Extend, Array, Move, Chamfer, Rotate, Scale, PolarArray }; // Fase 4.4/4.5/4.6 sketch edit ops
|
||||
void apply_edit_op(EditOp op); // mutate selected sketch entities
|
||||
// Onshape-style docked value entry (replaces wxGetTextFromUser popups for
|
||||
// Angle/Radius/Diameter constraints + Offset/Fillet edit ops). request_value
|
||||
// shows the card and stows a continuation run by confirm_value().
|
||||
void request_value(const wxString& label, double def, double mn, double mx,
|
||||
std::function<void(double)> cont,
|
||||
std::function<void()> on_cancel = nullptr);
|
||||
void confirm_value();
|
||||
void cancel_value();
|
||||
void commit_entity_constraints(const std::vector<SketchEntityConstraintDef>& defs); // multi-def (Symmetric)
|
||||
|
||||
// Constraint manager (C3.4): a docked list of the constrained sketch's
|
||||
// entity-constraints with per-row select (highlight the referenced entities in
|
||||
// the viewport) and delete (drop the constraint + re-solve). Shown in Constrain
|
||||
// mode only; operates on m_doc.features[m_constrain_feat].entity_constraints.
|
||||
// True when the constraint UI must address the LIVE sketch session rather than a committed
|
||||
// feature. Same discriminator apply_constraint uses to choose apply_live_constraint: both
|
||||
// Constrain modes set m_active too, so is_sketching() alone would claim the live scope while
|
||||
// the committed manager is open.
|
||||
bool live_constraint_scope() const;
|
||||
void rebuild_constraint_list(); // refill m_constraint_rows
|
||||
void delete_constraint(int idx); // erase + re-solve + refresh
|
||||
void highlight_constraint_entities(int idx); // push referenced entities to viewport
|
||||
void refresh_constrain_dof(); // re-solve feature, mirror DoF readout
|
||||
wxString constraint_label(const SketchEntityConstraintDef& d) const; // human-readable row text
|
||||
void after_edit_op(); // shared edit-op refresh tail
|
||||
void on_edit_feature(); // reopen the selected feature's dialog populated
|
||||
void after_tree_edit(bool ok); // shared post-op refresh of tree/viewport/status
|
||||
void load_feature_into_dialog(const CadFeature& f);
|
||||
void reset_edit_state(); // back to add-mode (m_edit_index = -1)
|
||||
|
||||
// Onshape loop: Button -> open_tool (show dialog) -> refresh_preview (ghost) ->
|
||||
// confirm_tool (commit) / cancel_tool (abort).
|
||||
void open_tool(Tool t);
|
||||
void close_tool();
|
||||
void refresh_preview();
|
||||
void confirm_tool();
|
||||
void cancel_tool();
|
||||
// Ctrl+Z / Ctrl+Shift+Z (Ctrl+Y) from the viewport. With a tool/dialog open it
|
||||
// cancels that (Esc-like); otherwise it undoes/redoes the committed feature history.
|
||||
void do_undo_redo(bool redo);
|
||||
// The plane the Hole tool drills on: a picked face (inward, centred) or the dropdown.
|
||||
SketchPlane hole_plane() const;
|
||||
// The plane the Thread tool builds on: a picked cylindrical face (axis) or the dropdown.
|
||||
SketchPlane thread_plane() const;
|
||||
// Name the geometry the card has LATCHED, so it never has to be inferred from the viewport.
|
||||
// Pass -1 for "none, falling back to the plane dropdown". See 200.
|
||||
void set_hole_target_label(int face);
|
||||
void set_thread_target_label(int face, int edge);
|
||||
CadFeature build_candidate(Tool t) const;
|
||||
// Merge per-body ghost meshes with the per-body display transforms applied. The kernel builds
|
||||
// a ghost from the untransformed bodies, so without this it floats back at the origin once a
|
||||
// body has been moved.
|
||||
TriangleMesh ghost_from_bodies(const std::vector<TriangleMesh>& per_body) const;
|
||||
// A mate makes no new geometry but it MOVES a body, and the moved assembly is the ghost worth
|
||||
// showing. Used both by the Mate card and by hovering a row of the offer's mate palette.
|
||||
bool show_mate_ghost(int kind, int cs_a, int cs_b,
|
||||
double offset, double angle_deg, bool flip, std::string& err);
|
||||
int resolve_extrude_sketch() const;
|
||||
// Plane pickers: fill a choice with XY/XZ/YZ + the document's datum planes, and
|
||||
// map a choice row back to the actual SketchPlane (rows 0-2 base, 3+ datum).
|
||||
void populate_plane_choices(ComboBox* c) const;
|
||||
wxString ref_plane_name(int row) const; // "XY" / a datum's name, for the on-geometry hint
|
||||
SketchPlane plane_from_choice(int row) const;
|
||||
// Where a new sketch goes, resolved from what is SELECTED IN THE VIEWPORT rather than from a
|
||||
// list: a picked planar face wins, otherwise the reference plane last clicked in 3D. `what`
|
||||
// comes back as something to show the user, so the choice is visible without a combo.
|
||||
SketchPlane sketch_plane_from_selection(wxString& what) const;
|
||||
// Whether that resolution has anything the USER picked behind it, rather than the default
|
||||
// reference plane. Lets a caller say "sketching on XZ" only when it is actually true.
|
||||
bool sketch_plane_target(wxString& what) const;
|
||||
// True when Extrude should build only the click-selected loop (a region of the
|
||||
// resolved sketch is selected and it carries entities).
|
||||
bool extrude_uses_loop() const;
|
||||
void sync_sketch_display(); // push un-consumed committed sketches to the viewport
|
||||
// Feed the viewport's visual Extrude depth-arrow gizmo (C5b) with the current profile
|
||||
// plane + centroid + live depths while the Extrude card is open (self-gates on m_active).
|
||||
void update_extrude_gizmo();
|
||||
void update_fillet_gizmo(); // edge-anchored radius arrow (Dressup card)
|
||||
void sync_dressup_target(); // Dressup card: show picked edge vs group, gate the combo
|
||||
void update_hole_gizmo(); // footprint circle + diameter/depth arrows (Hole card)
|
||||
// A FEATURE button whose tool needs bodies it may not have yet. Greyed with an explanatory
|
||||
// tooltip below min_bodies, rather than accepting the click and refusing afterwards.
|
||||
struct BodyGate { wxWindow* btn{nullptr}; int min_bodies{1}; wxString tip_live, tip_gated; };
|
||||
std::vector<BodyGate> m_body_gates;
|
||||
void update_body_gates(); // re-evaluate them against the current body count
|
||||
void update_thread_gizmo(); // footprint circle + radius/length arrows (Thread card)
|
||||
void update_shell_gizmo(); // inward thickness arrow on the picked face (Shell card)
|
||||
void update_revolve_gizmo(); // angle-arc around the axis (Revolve card)
|
||||
void update_draft_gizmo(); // angle-arc around the face centroid (Draft card)
|
||||
void update_cut_gizmo(); // plane-rectangle + offset arrow (Cut card)
|
||||
void update_operand_highlight(); // Boolean/Sweep/Loft operand tinting on the canvas
|
||||
void update_pattern_gizmo(); // linear spacing arrow / circular angle-arc (Pattern card)
|
||||
void update_datum_gizmo(); // resize handles on the datum plane being created/edited (C3)
|
||||
void update_helix_gizmo(); // live helix curve + radius/height/pitch handles (Helix card)
|
||||
void update_rib_gizmo(); // in-plane slab footprint + thickness handles (Rib card)
|
||||
void refresh_datum_planes(); // push resolved datum frames + per-plane u/v extents to viewport
|
||||
void refresh_mate_connectors(); // push connector frames so verse + polarity are visible
|
||||
void update_reference_planes(); // persistent XY/XZ/YZ reference planes (fallback when no object)
|
||||
|
||||
CadDocument m_doc;
|
||||
|
||||
Tool m_active{Tool::None};
|
||||
|
||||
// Keyboard shortcuts (Onshape-style, three scoped layers). Keys are encoded as the
|
||||
// upper-cased letter, OR'd with 0x10000 when Shift is required. m_keys_sketch fires only
|
||||
// while a sketch is open (single letters = sketch tools); m_keys_feature fires only when
|
||||
// no sketch is open (Shift+letter = feature tools; single letters = view toggles/section).
|
||||
static constexpr int SC_SHIFT = 0x10000;
|
||||
// ...and with 0x20000 when Ctrl is required too. The Shift+letter space is full, so an
|
||||
// action that arrives late lives on Ctrl+Shift; plain Ctrl-combos are still passed
|
||||
// straight through, which is what leaves this layer free.
|
||||
static constexpr int SC_CTRL = 0x20000;
|
||||
std::map<int, std::function<void()>> m_keys_sketch;
|
||||
std::map<int, std::function<void()>> m_keys_feature;
|
||||
|
||||
StaticBox* m_tree_box{nullptr}; // framed feature-tree section
|
||||
StaticBox* m_parts_box{nullptr}; // framed bodies section (hidden while empty)
|
||||
StaticBox* m_cards{nullptr}; // one framed panel holding every tool dialog (one visible at a time)
|
||||
void update_cards_frame(); // show that frame iff some card inside it is visible
|
||||
void show_move_card(bool show);
|
||||
void apply_move_card(); // numeric move/rotate -> same xform the gizmo builds
|
||||
void push_polygon_params();
|
||||
wxSizer* m_tb_commit{nullptr}; // far-right Commit to Plate, beside Confirm/Cancel
|
||||
wxSizer* m_tb_doc{nullptr}; // toolbar document/view actions (new, commit, export, section, place)
|
||||
CheckBox* m_show_bed{nullptr}; // view option: draw the printer bed + plate grid, or not
|
||||
wxSizer* m_box_move{nullptr}; // Move/Rotate numeric options (distance, axis, angle)
|
||||
wxSizer* m_box_sketch{nullptr};
|
||||
wxSizer* m_box_extrude{nullptr};
|
||||
wxSizer* m_box_dressup{nullptr};
|
||||
wxSizer* m_box_hole{nullptr};
|
||||
wxSizer* m_box_thread{nullptr};
|
||||
wxSizer* m_box_shell{nullptr};
|
||||
wxSizer* m_box_revolve{nullptr};
|
||||
wxSizer* m_box_sweep{nullptr};
|
||||
wxSizer* m_box_pattern{nullptr};
|
||||
wxSizer* m_box_plane{nullptr};
|
||||
wxSizer* m_box_loft{nullptr};
|
||||
wxSizer* m_box_draft{nullptr};
|
||||
wxSizer* m_box_boolean{nullptr};
|
||||
wxSizer* m_box_cut{nullptr};
|
||||
wxSizer* m_box_axis{nullptr};
|
||||
wxSizer* m_box_coordsys{nullptr};
|
||||
wxSizer* m_box_surf_extrude{nullptr};
|
||||
wxSizer* m_box_surf_revolve{nullptr};
|
||||
wxSizer* m_box_surf_loft{nullptr};
|
||||
wxSizer* m_box_surf_fill{nullptr};
|
||||
wxSizer* m_box_surf_offset{nullptr};
|
||||
wxSizer* m_box_surf_thicken{nullptr};
|
||||
wxSizer* m_box_transform{nullptr};
|
||||
wxSizer* m_box_mirror{nullptr};
|
||||
wxSizer* m_box_thicken{nullptr};
|
||||
wxSizer* m_box_rib{nullptr};
|
||||
wxSizer* m_box_project{nullptr};
|
||||
wxSizer* m_box_delete_face{nullptr};
|
||||
wxSizer* m_box_helix{nullptr};
|
||||
wxSizer* m_box_mate{nullptr};
|
||||
wxSizer* m_box_insert{nullptr}; // Confirm/Cancel card for placing Text/SVG art
|
||||
wxSizer* m_box_expr{nullptr}; // expression binding card (visible during edit only)
|
||||
int m_insert_feat{-1}; // provisional imported-art feature awaiting Confirm
|
||||
// Move-body gizmo runs through the unified action bar too: Confirm keeps the placement,
|
||||
// Cancel reverts to the pose captured when the move started.
|
||||
int m_move_body{-1};
|
||||
Transform3d m_move_prev{Transform3d::Identity()};
|
||||
// Set while the move gizmo is serving the Transform CARD rather than the Move button.
|
||||
// Both use the same gizmo; only this says which card owns the numbers it reports.
|
||||
int m_xf_gizmo_body{-1};
|
||||
Transform3d m_xf_gizmo_base{Transform3d::Identity()}; // pose when Transform armed it
|
||||
// Which body the Transform card's typed fields are currently previewing on, and the pose to
|
||||
// hand it back to. Separate from the gizmo pair because the card can retarget its Body combo.
|
||||
int m_xf_prev_body{-1};
|
||||
Transform3d m_xf_prev_base{Transform3d::Identity()};
|
||||
|
||||
// Onshape-style dialog-card title rows (icon + bold feature name), retitled
|
||||
// per tool in open_tool() (edit-mode shows the feature's actual name).
|
||||
wxStaticText* m_hdr_move{nullptr};
|
||||
wxStaticText* m_hdr_sketch{nullptr};
|
||||
// Onshape sketch-entry card (plane/orientation) that opens on "New sketch" and
|
||||
// persists until Finish (Phase 3).
|
||||
wxSizer* m_box_sketch_session{nullptr};
|
||||
wxStaticText* m_hdr_sketch_session{nullptr};
|
||||
wxStaticText* m_sketch_hint{nullptr}; // "click a plane" / "drawing on X" — must match the status
|
||||
wxStaticText* m_hdr_extrude{nullptr};
|
||||
wxStaticText* m_hdr_dressup{nullptr};
|
||||
wxStaticText* m_hdr_hole{nullptr};
|
||||
wxStaticText* m_hdr_thread{nullptr};
|
||||
wxStaticText* m_hdr_shell{nullptr};
|
||||
wxStaticText* m_hdr_revolve{nullptr};
|
||||
wxStaticText* m_hdr_sweep{nullptr};
|
||||
wxStaticText* m_hdr_pattern{nullptr};
|
||||
wxStaticText* m_hdr_plane{nullptr};
|
||||
wxStaticText* m_hdr_loft{nullptr};
|
||||
wxStaticText* m_hdr_draft{nullptr};
|
||||
wxStaticText* m_hdr_boolean{nullptr};
|
||||
wxStaticText* m_hdr_cut{nullptr};
|
||||
wxStaticText* m_hdr_axis{nullptr};
|
||||
wxStaticText* m_hdr_coordsys{nullptr};
|
||||
wxStaticText* m_hdr_surf_extrude{nullptr};
|
||||
wxStaticText* m_hdr_surf_revolve{nullptr};
|
||||
wxStaticText* m_hdr_surf_loft{nullptr};
|
||||
wxStaticText* m_hdr_surf_fill{nullptr};
|
||||
wxStaticText* m_hdr_surf_offset{nullptr};
|
||||
wxStaticText* m_hdr_surf_thicken{nullptr};
|
||||
wxStaticText* m_hdr_transform{nullptr};
|
||||
wxStaticText* m_hdr_mirror{nullptr};
|
||||
wxStaticText* m_hdr_thicken{nullptr};
|
||||
wxStaticText* m_hdr_rib{nullptr};
|
||||
wxStaticText* m_hdr_project{nullptr};
|
||||
wxStaticText* m_hdr_delete_face{nullptr};
|
||||
wxStaticText* m_hdr_helix{nullptr};
|
||||
wxStaticText* m_hdr_mate{nullptr};
|
||||
wxStaticText* m_hdr_insert{nullptr};
|
||||
|
||||
wxScrolledWindow* m_form{nullptr};
|
||||
DesignCanvas* m_viewport{nullptr};
|
||||
|
||||
// Top contextual toolbar (parented to the panel, above the form/viewport row).
|
||||
UiMode m_ui_mode{UiMode::Feature};
|
||||
// Sketch environment banner: a strip across the top of the viewport saying, in words, that
|
||||
// this is a sketch and which one. The mode used to be legible only from the toolbar and the
|
||||
// left card — both of which look like the rest of the app — so a sketch session and plate
|
||||
// preparation were one glance apart. Indicator only: Finish/Cancel stay on the ONE ribbon
|
||||
// action bar (the Design UX contract), and the banner never grows a second pair.
|
||||
wxPanel* m_sketch_banner{nullptr};
|
||||
wxStaticText* m_sketch_banner_txt{nullptr};
|
||||
wxScrolledWindow* m_toolbar{nullptr}; // horizontally scrollable so the action bar stays reachable on narrow windows
|
||||
wxSizer* m_tb_feature{nullptr};
|
||||
wxSizer* m_tb_sketch{nullptr};
|
||||
// The 20 constraint icon buttons, shown during BOTH Sketch and Constrain (Fase 4.2 live
|
||||
// path: a constraint must be applicable while drawing, not only after committing).
|
||||
wxSizer* m_tb_relations{nullptr};
|
||||
// Unified Confirm/Cancel action bar (right end of the ribbon). Shown whenever any
|
||||
// tool or mode is active; the single confirm/cancel surface for the whole tab.
|
||||
wxSizer* m_tb_action{nullptr};
|
||||
// Persistent Undo/Redo group at the left of the ribbon — always visible, independent
|
||||
// of the mode-gated tool groups. The buttons are greyed per the document history and
|
||||
// the do_undo_redo gate (see update_undo_redo_buttons).
|
||||
wxSizer* m_tb_history{nullptr};
|
||||
ScalableButton* m_btn_undo{nullptr};
|
||||
ScalableButton* m_btn_redo{nullptr};
|
||||
void update_undo_redo_buttons(); // enable/disable Undo/Redo from can_undo/can_redo + gate
|
||||
// All tool buttons, for the active-tool teal highlight (Onshape-style).
|
||||
std::vector<ScalableButton*> m_tool_btns;
|
||||
ScalableButton* m_active_tool_btn{nullptr};
|
||||
void set_active_tool_btn(ScalableButton* b); // nullptr clears the highlight
|
||||
// Owns the themed DropDown flyouts (and the item vectors they hold by ref).
|
||||
std::vector<std::shared_ptr<void>> m_flyout_keepalive;
|
||||
wxCheckBox* m_construction{nullptr}; // sketch-mode construction toggle
|
||||
wxSpinCtrlDouble* m_move_dx{nullptr}; // Move/Rotate card: world translation
|
||||
wxSpinCtrlDouble* m_move_dy{nullptr};
|
||||
wxSpinCtrlDouble* m_move_dz{nullptr};
|
||||
ComboBox* m_move_axis{nullptr}; // rotation axis: X/Y/Z
|
||||
wxSpinCtrlDouble* m_move_angle{nullptr}; // rotation angle (deg)
|
||||
// Polygon's two parameters are chosen FROM THE TOOL, in the offer's Polygon submenu, not
|
||||
// from a card on the left: the side count cannot be edited after drawing (the inline editor
|
||||
// offers Side and Angle only), so it has to be settled at the moment the tool is armed —
|
||||
// which is exactly where the offer already is. e1p.
|
||||
int m_poly_sides{6}; // 3..64; the submenu names the common ones
|
||||
bool m_poly_circumscribed{false};
|
||||
|
||||
// Which reference plane a sketch falls back to when no face is picked: 0/1/2 = XY/XZ/YZ,
|
||||
// >=3 indexes resolve_datum_planes(). Set by CLICKING a ghost plane in the viewport — there is
|
||||
// deliberately no dropdown for it. e1p.
|
||||
int m_ref_plane{0};
|
||||
// m_ref_plane is always a VALID plane, so it cannot itself distinguish "the user chose XY"
|
||||
// from "nobody has chosen anything yet". This does.
|
||||
bool m_plane_picked{false};
|
||||
ComboBox* m_shape{nullptr};
|
||||
ComboBox* m_mode{nullptr};
|
||||
wxSpinCtrlDouble* m_width{nullptr};
|
||||
wxSpinCtrlDouble* m_height{nullptr};
|
||||
wxSpinCtrlDouble* m_radius{nullptr};
|
||||
wxSpinCtrlDouble* m_distance{nullptr};
|
||||
ComboBox* m_extrude_end{nullptr}; // Blind/Symmetric/TwoSided/ThroughAll/UpTo*
|
||||
wxSpinCtrlDouble* m_distance2{nullptr}; // second-side depth (Two-sided)
|
||||
wxSpinCtrlDouble* m_taper{nullptr}; // draft angle (deg)
|
||||
CheckBox* m_flip{nullptr}; // reverse extrude direction
|
||||
|
||||
wxStaticText* m_extrude_sketch_label{nullptr};
|
||||
int m_extrude_sketch_ref{-1};
|
||||
|
||||
// Revolve controls (sweep a sketch profile about an in-plane axis).
|
||||
wxStaticText* m_revolve_sketch_label{nullptr};
|
||||
wxSpinCtrlDouble* m_revolve_angle{nullptr};
|
||||
ComboBox* m_revolve_axis{nullptr}; // 0 = plane X, 1 = plane Y
|
||||
ComboBox* m_revolve_mode{nullptr}; // New/Add/Cut/Intersect
|
||||
CheckBox* m_revolve_flip{nullptr};
|
||||
int m_revolve_sketch_ref{-1};
|
||||
|
||||
// Sweep controls (sweep a profile sketch along a path sketch).
|
||||
wxStaticText* m_sweep_profile_label{nullptr};
|
||||
ComboBox* m_sweep_path{nullptr}; // path Sketch picker (feature index in client data)
|
||||
ComboBox* m_sweep_mode{nullptr}; // New/Add/Cut/Intersect
|
||||
int m_sweep_profile_ref{-1};
|
||||
int m_sweep_path_ref{-1}; // path Sketch feature index (for re-edit pre-select)
|
||||
|
||||
// Loft controls (skin a solid through 2+ ordered profile Sketches).
|
||||
wxCheckListBox* m_loft_list{nullptr}; // every Sketch; check 2+ in list order = profiles
|
||||
CheckBox* m_loft_ruled{nullptr}; // ruled (straight) vs smooth sections
|
||||
ComboBox* m_loft_mode{nullptr}; // New/Add/Cut/Intersect
|
||||
std::vector<int> m_loft_sketch_idx; // feature index for each row in m_loft_list
|
||||
std::vector<int> m_loft_refs; // chosen profile refs (for re-edit pre-check)
|
||||
|
||||
// Surface Extrude controls (sheet from sketch profile).
|
||||
wxStaticText* m_surf_extrude_sketch_label{nullptr};
|
||||
wxSpinCtrlDouble* m_surf_extrude_distance{nullptr};
|
||||
int m_surf_extrude_sketch_ref{-1};
|
||||
|
||||
// Surface Revolve controls (sheet from sketch about axis).
|
||||
wxStaticText* m_surf_revolve_sketch_label{nullptr};
|
||||
wxSpinCtrlDouble* m_surf_revolve_angle{nullptr};
|
||||
ComboBox* m_surf_revolve_axis{nullptr}; // 0 = plane X, 1 = plane Y
|
||||
CheckBox* m_surf_revolve_flip{nullptr};
|
||||
int m_surf_revolve_sketch_ref{-1};
|
||||
|
||||
// Surface Loft controls (skin a sheet through 2+ ordered profile sketches).
|
||||
wxCheckListBox* m_surf_loft_list{nullptr}; // every Sketch; check 2+ in list order = profiles
|
||||
CheckBox* m_surf_loft_ruled{nullptr}; // ruled (straight) vs smooth sections
|
||||
std::vector<int> m_surf_loft_sketch_idx; // feature index for each row
|
||||
std::vector<int> m_surf_loft_refs; // chosen profile refs (for re-edit pre-check)
|
||||
|
||||
// Surface Fill controls (one-face sheet from a sketch boundary).
|
||||
wxStaticText* m_surf_fill_sketch_label{nullptr};
|
||||
int m_surf_fill_sketch_ref{-1};
|
||||
|
||||
// Surface Offset controls (offset a SHEET body).
|
||||
ComboBox* m_surf_offset_body{nullptr}; // sheet-body picker
|
||||
wxSpinCtrlDouble* m_surf_offset_distance{nullptr};
|
||||
|
||||
// Thicken Surface controls (thicken a SHEET body into a solid).
|
||||
ComboBox* m_surf_thicken_body{nullptr}; // sheet-body picker
|
||||
wxSpinCtrlDouble* m_surf_thicken_thickness{nullptr};
|
||||
CheckBox* m_surf_thicken_flip{nullptr};
|
||||
|
||||
// Transform controls (rigid move/rotate of a body).
|
||||
ComboBox* m_xf_body{nullptr}; // body to transform
|
||||
wxSpinCtrlDouble* m_xf_dx{nullptr}; // translate X
|
||||
wxSpinCtrlDouble* m_xf_dy{nullptr}; // translate Y
|
||||
wxSpinCtrlDouble* m_xf_dz{nullptr}; // translate Z
|
||||
ComboBox* m_xf_axis{nullptr}; // rotation axis: X/Y/Z
|
||||
wxSpinCtrlDouble* m_xf_angle{nullptr}; // rotation angle (deg)
|
||||
wxSpinCtrlDouble* m_xf_pivot_x{nullptr}; // pivot X
|
||||
wxSpinCtrlDouble* m_xf_pivot_y{nullptr}; // pivot Y
|
||||
wxSpinCtrlDouble* m_xf_pivot_z{nullptr}; // pivot Z
|
||||
CheckBox* m_xf_copy{nullptr}; // keep original (make a copy)
|
||||
|
||||
// Mirror controls (reflect a body about a plane).
|
||||
ComboBox* m_mirror_body{nullptr}; // body to mirror
|
||||
ComboBox* m_mirror_plane{nullptr}; // mirror plane (XY/XZ/YZ + datums)
|
||||
CheckBox* m_mirror_keep{nullptr}; // keep original body
|
||||
|
||||
// Thicken controls (offset one solid face into a thin plate).
|
||||
ComboBox* m_thicken_body{nullptr}; // source body
|
||||
wxStaticText* m_thicken_face_label{nullptr}; // picked face
|
||||
wxSpinCtrlDouble* m_thicken_thickness{nullptr};
|
||||
CheckBox* m_thicken_flip{nullptr}; // flip direction
|
||||
|
||||
// Rib controls (thin wall from an open sketch line).
|
||||
ComboBox* m_rib_body{nullptr}; // target body
|
||||
ComboBox* m_rib_sketch{nullptr}; // sketch holding the open line (feature index in client data)
|
||||
wxSpinCtrl* m_rib_entity{nullptr}; // entity index within the sketch
|
||||
wxSpinCtrlDouble* m_rib_thickness{nullptr};
|
||||
wxSpinCtrlDouble* m_rib_depth{nullptr};
|
||||
|
||||
// Project controls (project body edges onto a plane as sketch entities).
|
||||
ComboBox* m_proj_source_body{nullptr}; // source body
|
||||
wxStaticText* m_proj_face_label{nullptr}; // picked face (or "all edges")
|
||||
ComboBox* m_proj_plane{nullptr}; // target plane
|
||||
|
||||
// Delete Face controls (remove faces, heal the solid).
|
||||
ComboBox* m_del_face_body{nullptr}; // target body
|
||||
wxButton* m_del_face_add_btn{nullptr}; // "Add picked face" button
|
||||
wxStaticText* m_del_face_list{nullptr}; // shows the accumulated face ids
|
||||
std::vector<int> m_del_faces; // accumulated face list
|
||||
|
||||
// Helix controls (helical curve).
|
||||
ComboBox* m_helix_plane{nullptr}; // axis plane (XY/XZ/YZ + datums)
|
||||
wxSpinCtrlDouble* m_helix_radius{nullptr};
|
||||
wxSpinCtrlDouble* m_helix_pitch{nullptr};
|
||||
wxSpinCtrlDouble* m_helix_height{nullptr};
|
||||
CheckBox* m_helix_left_handed{nullptr};
|
||||
wxSpinCtrlDouble* m_helix_taper{nullptr};
|
||||
|
||||
// Mate (assembly) controls
|
||||
ComboBox* m_mate_kind{nullptr};
|
||||
ComboBox* m_mate_cs_a{nullptr};
|
||||
ComboBox* m_mate_cs_b{nullptr};
|
||||
wxSpinCtrlDouble* m_mate_offset{nullptr};
|
||||
wxSpinCtrlDouble* m_mate_angle{nullptr};
|
||||
CheckBox* m_mate_flip{nullptr};
|
||||
wxStaticText* m_offset_label{nullptr};
|
||||
wxStaticText* m_angle_label{nullptr};
|
||||
|
||||
// Expression binding (per-feature, visible during edit only)
|
||||
ComboBox* m_expr_field{nullptr}; // field-name picker (editable)
|
||||
wxTextCtrl* m_expr_text{nullptr}; // expression string
|
||||
wxButton* m_expr_set_btn{nullptr}; // Apply / bind
|
||||
wxButton* m_expr_clear_btn{nullptr}; // Remove binding
|
||||
wxStaticText* m_expr_status{nullptr}; // shows current bindings for the edited feature
|
||||
void populate_expr_fields(Tool t); // fill m_expr_field from feature-type fields
|
||||
void on_set_expr(); // checkpoint + write -> recompute -> undo on fail
|
||||
void on_clear_expr(); // remove selected binding
|
||||
|
||||
// Document variables panel (below the feature tree / parts)
|
||||
StaticBox* m_var_box{nullptr};
|
||||
wxListCtrl* m_var_list{nullptr};
|
||||
wxButton* m_btn_add_var{nullptr};
|
||||
wxButton* m_btn_edit_var{nullptr};
|
||||
wxButton* m_btn_del_var{nullptr};
|
||||
void refresh_variables(); // rebuild m_var_list from m_doc.variables
|
||||
void on_add_variable();
|
||||
void on_edit_variable();
|
||||
void on_remove_variable();
|
||||
|
||||
// Feature-tree button
|
||||
ScalableButton* m_btn_interfere{nullptr};
|
||||
|
||||
// Pattern controls (replicate the target body: linear or circular).
|
||||
ComboBox* m_pattern_type{nullptr}; // 0 = Linear, 1 = Circular
|
||||
wxSpinCtrlDouble* m_pattern_count{nullptr}; // total instances incl. seed
|
||||
wxSpinCtrlDouble* m_pattern_spacing{nullptr}; // linear step (mm)
|
||||
ComboBox* m_pattern_dir{nullptr}; // linear direction: 0 = plane X, 1 = plane Y
|
||||
wxSpinCtrlDouble* m_pattern_angle{nullptr}; // circular total angle (deg)
|
||||
// Boolean controls (combine two existing bodies).
|
||||
ComboBox* m_bool_op{nullptr}; // 0 = Union, 1 = Subtract, 2 = Intersect
|
||||
ComboBox* m_bool_target{nullptr}; // body that survives (selection == body index)
|
||||
ComboBox* m_bool_tool{nullptr}; // body consumed (selection == body index)
|
||||
// Which operand the NEXT viewport body pick fills: 0 = target, 1 = tool. Reset when the
|
||||
// card opens, so the first two clicks in the viewport always mean "keep this, cut with
|
||||
// that" in that order. The combos remain the typed half and mirror whatever is picked.
|
||||
int m_bool_next_slot{0};
|
||||
CheckBox* m_bool_keep{nullptr}; // keep the tool body after the op
|
||||
wxSpinCtrlDouble* m_bool_tol{nullptr}; // OCCT fuzzy tolerance (mm); robust cut on near-coincident faces
|
||||
|
||||
// Plane Cut (split-by-plane): a reference plane + offset splits the target body into
|
||||
// two separate bodies (both pieces kept).
|
||||
ComboBox* m_cut_plane{nullptr}; // XY/XZ/YZ + datum planes (cut plane)
|
||||
ComboBox* m_cut_target{nullptr}; // body to cut (selection == body index)
|
||||
wxSpinCtrlDouble* m_cut_offset{nullptr}; // offset along the plane normal (mm)
|
||||
// Datum plane controls (derive a selectable sketch plane: offset + tilt from a base).
|
||||
ComboBox* m_plane_base{nullptr}; // 0=XY,1=XZ,2=YZ, 3+N = Nth datum plane
|
||||
wxSpinCtrlDouble* m_plane_offset{nullptr}; // offset along base normal (mm)
|
||||
wxSpinCtrlDouble* m_plane_tilt{nullptr}; // tilt about a base axis (deg) / Angle / Tangent angle
|
||||
ComboBox* m_plane_tilt_axis{nullptr}; // 0 = base X, 1 = base Y
|
||||
// Plane construction method + contextual face/edge reference picks (Onshape/Fusion parity).
|
||||
ComboBox* m_plane_type{nullptr}; // PlaneType: Offset/Angle/Midplane/Tangent/TwoEdges/Coincident
|
||||
wxButton* m_plane_pick_faceA{nullptr}; wxStaticText* m_plane_faceA_lbl{nullptr};
|
||||
wxButton* m_plane_pick_faceB{nullptr}; wxStaticText* m_plane_faceB_lbl{nullptr};
|
||||
wxButton* m_plane_pick_edgeA{nullptr}; wxStaticText* m_plane_edgeA_lbl{nullptr};
|
||||
wxButton* m_plane_pick_edgeB{nullptr}; wxStaticText* m_plane_edgeB_lbl{nullptr};
|
||||
wxSpinCtrlDouble* m_plane_usize{nullptr}; // datum rectangle extent u (mm) — also driven by drag handles
|
||||
wxSpinCtrlDouble* m_plane_vsize{nullptr}; // datum rectangle extent v (mm)
|
||||
// Captured references for the candidate datum (body index + face/edge index, -1 = none).
|
||||
int m_pl_faceA_body{-1}, m_pl_faceA{-1};
|
||||
int m_pl_faceB_body{-1}, m_pl_faceB{-1};
|
||||
int m_pl_edgeA_body{-1}, m_pl_edgeA{-1};
|
||||
int m_pl_edgeB_body{-1}, m_pl_edgeB{-1};
|
||||
PlanePick m_plane_pick{PlanePick::None}; // which ref the next solid pick fills
|
||||
// Plate loop selection (click a committed sketch loop): the Sketch feature + the
|
||||
// clicked closed-region index, so Extrude builds just that one loop. -1 = none.
|
||||
int m_sel_sketch_feat{-1};
|
||||
int m_sel_sketch_region{-1};
|
||||
// Click-selected solid topology (whole/face/edge cycle): face id for up-to-face / dress-up.
|
||||
int m_sel_solid_body{-1}; // which body the face/edge selection is on
|
||||
int m_sel_solid_face{-1};
|
||||
int m_sel_solid_edge{-1};
|
||||
bool m_sel_solid_vertex{false}; // a corner is picked (body+point, no face/edge)
|
||||
// The face actually under the last solid click, INDEPENDENT of the whole/face/edge cycle level.
|
||||
// The first click on a solid selects the WHOLE body, but the ray has already resolved which face
|
||||
// it hit and the callback passes it. "Sketch on the face I clicked" must not require discovering
|
||||
// that a second click refines the selection, so keep it instead of throwing it away. 3a2.
|
||||
int m_pick_face_body{-1};
|
||||
int m_pick_face{-1};
|
||||
// What the live sketch was actually opened on ("the picked face", "XY", a datum's name), so the
|
||||
// hint can say it. Resolved from the selection at begin_sketch, not read back from a combo.
|
||||
wxString m_sketch_on;
|
||||
// --- the object-driven offer (charter 4.1) ---------------------------------------------
|
||||
// Right-click the geometry -> a vertical list in ratified row order, verbs that do not
|
||||
// apply disabled IN PLACE with their reason. The rows come from the generated table in
|
||||
// DesignOffer.hpp; this map is how a row reaches the code that already implements it, for
|
||||
// the verbs that have no keyboard shortcut to route through.
|
||||
std::map<std::string, std::function<void()>> m_verb_actions;
|
||||
// Append an offer row with its toolbar glyph. The bitmap must be set BEFORE Append —
|
||||
// wxGTK builds the GtkMenuItem there and only makes an image item if one is present.
|
||||
// Every status write goes through here so long hints wrap instead of clipping.
|
||||
void set_status(const wxString& text);
|
||||
wxString idle_hint() const; // what to say when nothing is selected
|
||||
// Reason detect_mate_conflicts() recorded for a feature, or nullptr. Marks the tree row and
|
||||
// feeds the status line; a conflict is a diagnostic, not a document error.
|
||||
const std::string* mate_conflict_reason(int feature) const;
|
||||
|
||||
wxMenuItem* append_offer_item(wxMenu* menu, int id, const wxString& text,
|
||||
const struct OfferVerb& v);
|
||||
void show_offer_menu(const wxPoint& screen_pos);
|
||||
// Where the offer opens when no mouse press anchors it: the keyboard route, and the automatic
|
||||
// open on entering Sketch. The pointer if it is over the viewport, else the viewport's centre.
|
||||
// A raw wxGetMousePosition() can be sitting on the toolbar, on the card column or on another
|
||||
// monitor, and the menu would map there — detached from the geometry it is about.
|
||||
wxPoint offer_anchor() const;
|
||||
int offer_selection_kind() const; // an OfferSel, as int to keep the header light
|
||||
// Does the SKETCH half of the map apply? A mode question, not a session one: begin_sketch
|
||||
// does not run until the first tool is armed, so between "press Sketch" and "pick a tool"
|
||||
// is_sketching() is still false — precisely when the drawing tools must be on offer. The
|
||||
// is_sketching() arm covers re-opening a committed sketch, which enters the session first.
|
||||
bool sketch_map_applies() const;
|
||||
void run_offer_action(const char* action);
|
||||
// Face-as-profile extrude (Onshape): when Extrude is opened on a picked solid face with
|
||||
// no sketch source, this carries that global face id so the kernel extrudes the face.
|
||||
// -1 = ordinary sketch/loop extrude. Set when opening the Extrude card, consumed on add.
|
||||
int m_extrude_face_src{-1};
|
||||
|
||||
ComboBox* m_dressup_type{nullptr};
|
||||
ComboBox* m_face_group{nullptr};
|
||||
wxSpinCtrlDouble* m_dressup_size{nullptr};
|
||||
wxStaticText* m_dressup_edge_label{nullptr}; // shows the picked edge, or the group fallback
|
||||
|
||||
ComboBox* m_hole_plane{nullptr};
|
||||
wxSpinCtrlDouble* m_hole_diameter{nullptr};
|
||||
wxSpinCtrlDouble* m_hole_depth{nullptr};
|
||||
CheckBox* m_hole_through{nullptr};
|
||||
wxSpinCtrlDouble* m_hole_x{nullptr};
|
||||
wxSpinCtrlDouble* m_hole_y{nullptr};
|
||||
// #2: when the Hole tool is opened on a picked solid face, drill on that face centred
|
||||
// on it (origin = face centroid, normal = inward). m_hole_x/y then read as the offset
|
||||
// from the face centre. Falls back to the m_hole_plane dropdown when no face is picked.
|
||||
bool m_hole_on_face{false};
|
||||
SketchPlane m_hole_face_plane;
|
||||
int m_hole_face_body{-1};
|
||||
// #2 Part B: the picked face's (u,v) bounds in m_hole_face_plane, so the hole's construction
|
||||
// dims read as distance from the face sides (umin/vmin edges) rather than from the centre.
|
||||
bool m_hole_has_bounds{false};
|
||||
double m_hole_umin{0}, m_hole_umax{0}, m_hole_vmin{0}, m_hole_vmax{0};
|
||||
// Says which face the latch above is holding. Thicken/Shell/Draft show theirs because their
|
||||
// face IS the live selection; this one has to be shown precisely BECAUSE it is not, and the
|
||||
// status line goes on saying "Nothing selected" while the ghost keeps drilling. 200.
|
||||
wxStaticText* m_hole_target_label{nullptr};
|
||||
|
||||
ComboBox* m_thread_plane{nullptr};
|
||||
ComboBox* m_thread_std{nullptr}; // standard designation (M6, 1/4-20 UNC, ...)
|
||||
wxSpinCtrlDouble* m_thread_radius{nullptr};
|
||||
wxSpinCtrlDouble* m_thread_pitch{nullptr};
|
||||
wxSpinCtrlDouble* m_thread_height{nullptr};
|
||||
wxSpinCtrlDouble* m_thread_depth{nullptr};
|
||||
CheckBox* m_thread_internal{nullptr};
|
||||
wxSpinCtrlDouble* m_thread_x{nullptr};
|
||||
wxSpinCtrlDouble* m_thread_y{nullptr};
|
||||
// #3: when the Thread tool is opened on a picked cylindrical face (a hole bore or a
|
||||
// cylinder), thread that surface — plane on its axis, radius/internal derived from it.
|
||||
bool m_thread_on_face{false};
|
||||
SketchPlane m_thread_face_plane;
|
||||
int m_thread_face_body{-1};
|
||||
wxStaticText* m_thread_target_label{nullptr}; // the latched face/edge — see m_hole_target_label
|
||||
|
||||
wxSpinCtrlDouble* m_shell_thickness{nullptr};
|
||||
wxStaticText* m_shell_face_label{nullptr}; // shows the picked face to remove
|
||||
|
||||
// Draft controls (taper a single picked solid face about the body bottom).
|
||||
wxSpinCtrlDouble* m_draft_angle{nullptr};
|
||||
wxStaticText* m_draft_face_label{nullptr}; // shows the picked face to draft
|
||||
|
||||
// Axis controls (datum axis: line through two points or derived from geometry).
|
||||
ComboBox* m_axis_type{nullptr}; // AxisType: TwoPoints/FaceNormal/CylinderCenterline/PlaneIntersection/AlongEdge
|
||||
wxButton* m_axis_pick_face{nullptr}; wxStaticText* m_axis_face_lbl{nullptr};
|
||||
wxButton* m_axis_pick_edge{nullptr}; wxStaticText* m_axis_edge_lbl{nullptr};
|
||||
ComboBox* m_axis_plane_a{nullptr};
|
||||
ComboBox* m_axis_plane_b{nullptr};
|
||||
wxSpinCtrlDouble* m_axis_p1x{nullptr}; wxSpinCtrlDouble* m_axis_p1y{nullptr}; wxSpinCtrlDouble* m_axis_p1z{nullptr};
|
||||
wxSpinCtrlDouble* m_axis_p2x{nullptr}; wxSpinCtrlDouble* m_axis_p2y{nullptr}; wxSpinCtrlDouble* m_axis_p2z{nullptr};
|
||||
int m_ax_face_body{-1}, m_ax_face{-1};
|
||||
int m_ax_edge_body{-1}, m_ax_edge{-1};
|
||||
AxisPick m_axis_pick{AxisPick::None};
|
||||
|
||||
// CoordSys controls (datum coordinate system: point + orthonormal frame).
|
||||
ComboBox* m_coordsys_type{nullptr}; // CoordSysType: PointWorld/FaceAndDirection
|
||||
ComboBox* m_cs_body{nullptr}; // body-focus chooser: restrict picking to one body
|
||||
wxSpinCtrlDouble* m_cs_x{nullptr}; wxSpinCtrlDouble* m_cs_y{nullptr}; wxSpinCtrlDouble* m_cs_z{nullptr};
|
||||
wxButton* m_cs_pick_face{nullptr}; wxStaticText* m_cs_face_lbl{nullptr};
|
||||
wxButton* m_cs_pick_edge{nullptr}; wxStaticText* m_cs_edge_lbl{nullptr};
|
||||
wxSpinCtrlDouble* m_cs_hx{nullptr}; wxSpinCtrlDouble* m_cs_hy{nullptr}; wxSpinCtrlDouble* m_cs_hz{nullptr};
|
||||
int m_cs_face_body{-1}, m_cs_face{-1};
|
||||
int m_cs_edge_body{-1}, m_cs_edge{-1};
|
||||
CoordSysPick m_coordsys_pick{CoordSysPick::None};
|
||||
|
||||
// Onshape-style docked value-entry card (Angle/Radius/Diameter/Offset/Fillet).
|
||||
wxSizer* m_box_value{nullptr};
|
||||
wxStaticText* m_value_label{nullptr};
|
||||
wxTextCtrl* m_value_input{nullptr}; // plain text field: forces en ('.') decimals
|
||||
double m_value_min{0.0}; // range for confirm-time clamping
|
||||
double m_value_max{0.0};
|
||||
std::function<void(double)> m_value_cont; // deferred apply, run on Confirm
|
||||
std::function<void()> m_value_cancel; // optional action when the card is cancelled
|
||||
|
||||
// Feature tree: a wxTreeCtrl with per-feature-type icons. Callers keep using
|
||||
// integer row indices via tree_selection()/set_tree_selection(); m_tree_items
|
||||
// maps feature order -> tree node, rebuilt by refresh_tree().
|
||||
wxTreeCtrl* m_tree{nullptr};
|
||||
wxTreeCtrl* m_parts{nullptr}; // Bodies list under the feature tree
|
||||
wxStaticText* m_parts_label{nullptr}; // its "Bodies" caption (hidden when empty)
|
||||
wxBoxSizer* m_parts_hdr{nullptr}; // Bodies card header (icon + title)
|
||||
wxStaticLine* m_parts_rule{nullptr}; // rule under that header
|
||||
wxBoxSizer* m_hdr_tree_row{nullptr}; // Feature tree header: title + row actions
|
||||
wxStaticText* m_hdr_tree{nullptr}; // its title label
|
||||
wxImageList* m_tree_images{nullptr};
|
||||
std::vector<wxTreeItemId> m_tree_items;
|
||||
// Parts list: tree rows for each body (parallel to m_doc.bodies). Selecting one
|
||||
// highlights that body and makes it the target for the next op.
|
||||
std::vector<wxTreeItemId> m_tree_body_items;
|
||||
|
||||
// Section views (non-destructive): named "Section View N" entries listed in the tree, each a
|
||||
// horizontal clip height. View-only — NOT bodies/features, never serialized. Key X adds one;
|
||||
// clicking a row activates it (again = off); Delete removes it; Alt+Wheel moves the active one.
|
||||
// Section view (single, non-destructive): ONE horizontal clip that hides half the model to
|
||||
// inspect inside — solid, no ghost of the hidden half. Toggled on/off; Flip shows the other
|
||||
// half. Never a body, no tree entry.
|
||||
bool m_section_on{false};
|
||||
double m_section_cut_z{0.0};
|
||||
bool m_section_upper{false}; // false = keep lower half, true = upper
|
||||
ScalableButton* m_section_flip_btn{nullptr}; // toolbar action; enabled only while the section is on
|
||||
void toggle_section_view(); // Section View button / X: on <-> off
|
||||
void flip_section_view(); // Flip button / F: opposite half
|
||||
void update_section_flip_btn(); // enable the Flip button iff the section is on
|
||||
// Per-body visibility (parallel to m_doc.bodies; index stable across recompute since
|
||||
// bodies are appended in feature order). Empty/grown to all-visible by sync_body_visible().
|
||||
std::vector<bool> m_body_visible;
|
||||
void sync_body_visible(); // grow/shrink m_body_visible to bodies.size()
|
||||
// Per-body display translation (Move-body, M5). Parallel to m_doc.bodies; default
|
||||
// identity. Applied to the display/pick meshes only — the OCCT shape (and face/edge
|
||||
// global ids) is never touched, so dress-up targeting stays stable across a move.
|
||||
std::vector<Transform3d> m_body_xform;
|
||||
std::vector<TriangleMesh> m_disp_body_meshes; // display_body_meshes with m_body_xform applied
|
||||
TriangleMesh m_disp_pick_mesh; // combined pick mesh with m_body_xform applied
|
||||
void sync_body_xform(); // grow m_body_xform to bodies.size() (identity)
|
||||
void rebuild_disp_meshes(); // recompute m_disp_* from m_doc + m_body_xform
|
||||
void feed_bodies(); // push m_disp_* + visibility/xform to the viewport
|
||||
void on_move_body(); // start the move gizmo on the selected body
|
||||
void arm_transform_gizmo(); // arm the move gizmo on the Transform card's body (add mode only)
|
||||
void on_set_body_color(); // Color tool: pick a per-body display colour override
|
||||
void on_boolean_tool(); // Boolean (combine bodies): needs two solids, then opens the tool
|
||||
int tree_selection() const; // selected feature row, or wxNOT_FOUND
|
||||
int tree_body_selection() const; // selected Parts-list body index, or -1
|
||||
void refresh_parts(); // rebuild the Bodies list under the feature tree
|
||||
void sync_sidebar_width(); // keep the panel as wide as Prepare's sidebar
|
||||
void set_tree_selection(int row);
|
||||
static int tree_icon_for(CadFeatureType t);
|
||||
|
||||
wxStaticText* m_status{nullptr};
|
||||
// m_status's foreground as created, captured before any caller touches it. Callers signal
|
||||
// "no opinion" by setting wxNullColour, which restores exactly this — so it is the only
|
||||
// reliable way to tell a chosen colour (the error red) from the default. See set_status().
|
||||
wxColour m_status_default_fg;
|
||||
// The guidance sentence for the step the armed sketch tool is on, kept so a transient
|
||||
// readout (the live length/angle while a segment is being dragged) can be appended to it
|
||||
// instead of replacing it — the guidance used to vanish on the first mouse move after a
|
||||
// click, which is precisely when it is needed. 1c0c.
|
||||
wxString m_sketch_step;
|
||||
// mode is a DesignSketchTool::Mode; passed as an int because this header deliberately does
|
||||
// not include the tool's, and the .cpp (which does) casts it back.
|
||||
void on_sketch_step(int mode, int step, int picks);
|
||||
wxStaticText* m_dof_status{nullptr}; // DoF / constraint-state readout (P3)
|
||||
// Last live-solve result, so entering Constrain can restore the readout without a solve.
|
||||
int m_dof_last{-1};
|
||||
bool m_dof_last_ok{true};
|
||||
bool m_dof_last_has{false};
|
||||
int m_feature_counter{0};
|
||||
|
||||
std::vector<wxButton*> m_confirm_btns;
|
||||
|
||||
// Edit-in-place state: add-mode is m_edit_index == -1. Single-feature edit
|
||||
// (Sketch or Extrude independently) uses only m_edit_index as the row to replace.
|
||||
int m_edit_index{-1};
|
||||
|
||||
// Tree row of the sketch currently being constrained (-1 = not constraining).
|
||||
int m_constrain_feat{-1};
|
||||
|
||||
// Constraint-manager card (C3.4): header + a rebuildable list of constraint rows.
|
||||
wxSizer* m_box_constraints{nullptr};
|
||||
wxStaticText* m_hdr_constraints{nullptr};
|
||||
wxSizer* m_constraint_rows{nullptr};
|
||||
int m_constraint_sel{-1}; // highlighted constraint row, or -1
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_DesignPanel_hpp_
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
#ifndef slic3r_GUI_McpControl_hpp_
|
||||
#define slic3r_GUI_McpControl_hpp_
|
||||
|
||||
// MCP control surface (slice 1): a local JSON-RPC 2.0 server, line-delimited over a
|
||||
// Unix domain socket, that lets an external MCP bridge drive and perceive the Design
|
||||
// tab. Off unless the env var ORCA_CAD_MCP is set:
|
||||
// ORCA_CAD_MCP=1 -> socket at /tmp/orca-cad-mcp.sock
|
||||
// ORCA_CAD_MCP=/path/to.sock -> socket at that path
|
||||
// All CAD work is marshalled onto the wx main thread and runs through the SAME
|
||||
// CadDocument kernel the GUI uses (no parallel engine). Slice-1 methods:
|
||||
// describe_tools, describe_scene, extrude.
|
||||
//
|
||||
// ponytail: Unix-socket only (POSIX). Windows compiles this to a no-op; add a named
|
||||
// pipe transport when a Windows agent actually needs it.
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
// Start the server thread iff ORCA_CAD_MCP is set. Safe to call once after the
|
||||
// MainFrame + DesignPanel exist. No-op when the env var is unset or on Windows.
|
||||
void start_mcp_control_if_enabled();
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_GUI_McpControl_hpp_
|
||||
@@ -0,0 +1,203 @@
|
||||
#include "slic3r/GUI/CAD/SketchInlineEditor.hpp"
|
||||
|
||||
#include "slic3r/GUI/ImGuiWrapper.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
|
||||
#include <imgui/imgui.h>
|
||||
#include <imgui/imgui_internal.h> // BringWindowToDisplayFront / GetCurrentWindow
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
namespace {
|
||||
|
||||
// Numbers are typed and shown with a POINT, whatever the locale: this field feeds a CAD kernel,
|
||||
// and a decimal comma reaching it as a thousands separator is a silent order-of-magnitude error.
|
||||
// Parsing accepts either separator because a keyboard's numeric pad may only offer one.
|
||||
std::string fmt_value(double v, int digits = 2)
|
||||
{
|
||||
char fmt[16];
|
||||
std::snprintf(fmt, sizeof(fmt), "%%.%df", digits);
|
||||
char buf[64];
|
||||
std::snprintf(buf, sizeof(buf), fmt, v);
|
||||
for (char* c = buf; *c; ++c)
|
||||
if (*c == ',') *c = '.';
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
bool parse_value(const char* text, double& out)
|
||||
{
|
||||
if (text == nullptr) return false;
|
||||
std::string t(text);
|
||||
for (char& c : t)
|
||||
if (c == ',') c = '.';
|
||||
// strtod, not std::stod: no exceptions, and `end` tells us whether the WHOLE field was a
|
||||
// number. "12mm" must be refused, not silently read as 12.
|
||||
const char* b = t.c_str();
|
||||
char* end = nullptr;
|
||||
const double v = std::strtod(b, &end);
|
||||
if (end == b) return false;
|
||||
while (*end == ' ' || *end == '\t') ++end;
|
||||
if (*end != '\0') return false;
|
||||
out = v;
|
||||
return true;
|
||||
}
|
||||
|
||||
// One machine-readable line per event of the click-edit contract, for the UX check that runs
|
||||
// after every build (scripts/CAD/check-gui-click-edit.py). Deliberately NOT the same switch as
|
||||
// ORCA_CAD_KEYTRACE: that one is a debugging firehose, this one is an assertion surface and its
|
||||
// format is a contract the script parses.
|
||||
//
|
||||
// The pair that matters is `open` vs `commit`: the check always types a value DIFFERENT from the
|
||||
// prefill, so a field that is on screen but not editable commits its prefill and the two lines
|
||||
// disagree. A focus flag cannot show that — it read 0 even when typing worked — but the number
|
||||
// the user actually gets can.
|
||||
void ux_trace(const char* event, const std::string& title, const std::string& detail)
|
||||
{
|
||||
if (!std::getenv("ORCA_CAD_UXTRACE")) return;
|
||||
std::fprintf(stderr, "[UX] %s title=%s %s\n", event, title.c_str(), detail.c_str());
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SketchInlineEditor::open(const wxPoint& canvas_px, double value, const std::string& title,
|
||||
std::function<void(double)> on_commit,
|
||||
std::function<void()> on_cancel)
|
||||
{
|
||||
m_anchor = canvas_px;
|
||||
m_title = title;
|
||||
m_err.clear();
|
||||
m_commit = std::move(on_commit);
|
||||
m_cancel = std::move(on_cancel);
|
||||
const std::string v = fmt_value(value);
|
||||
std::snprintf(m_buf, sizeof(m_buf), "%s", v.c_str());
|
||||
m_open = true;
|
||||
// ImGui takes keyboard focus for one frame on request; asking on the frame the field first
|
||||
// appears is what makes typing land without a click. There is no window manager to consult.
|
||||
m_focus_pending = true;
|
||||
ux_trace("open", m_title, "prefill=" + v);
|
||||
}
|
||||
|
||||
void SketchInlineEditor::close()
|
||||
{
|
||||
m_open = false;
|
||||
m_focus_pending = false;
|
||||
m_commit = nullptr;
|
||||
m_cancel = nullptr;
|
||||
m_err.clear();
|
||||
}
|
||||
|
||||
void SketchInlineEditor::cancel()
|
||||
{
|
||||
if (m_open) do_cancel();
|
||||
}
|
||||
|
||||
void SketchInlineEditor::commit()
|
||||
{
|
||||
if (m_open) do_commit();
|
||||
}
|
||||
|
||||
void SketchInlineEditor::do_cancel()
|
||||
{
|
||||
ux_trace("cancel", m_title, "");
|
||||
auto cb = m_cancel;
|
||||
close();
|
||||
if (cb) cb();
|
||||
}
|
||||
|
||||
void SketchInlineEditor::do_commit()
|
||||
{
|
||||
double v = 0.0;
|
||||
if (!parse_value(m_buf, v)) {
|
||||
// Refusing input in silence is indistinguishable from the app having frozen: the field
|
||||
// just sits there and the user has no idea what it wants. Say so in the title line and
|
||||
// keep editing.
|
||||
ux_trace("refused", m_title, std::string("typed=") + m_buf);
|
||||
m_err = (m_buf[0] == '\0') ? into_u8(_L("Enter a number")) : into_u8(_L("Not a number"));
|
||||
m_focus_pending = true;
|
||||
return;
|
||||
}
|
||||
ux_trace("commit", m_title, std::string("typed=") + m_buf + " value=" + fmt_value(v, 4));
|
||||
auto cb = m_commit;
|
||||
close();
|
||||
// AFTER close(): the callback may open the next queued dimension (a rectangle queues Width
|
||||
// then Height), and doing that into a field that still believes it is open would drop the
|
||||
// second one's prefill on the floor.
|
||||
if (cb) cb(v);
|
||||
}
|
||||
|
||||
bool SketchInlineEditor::render(ImGuiWrapper& imgui, float scale)
|
||||
{
|
||||
if (!m_open) return false;
|
||||
|
||||
ImGuiWrapper::push_common_window_style(scale);
|
||||
imgui.set_next_window_pos((float) m_anchor.x, (float) m_anchor.y, ImGuiCond_Always, 0.5f, 0.5f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3.0f);
|
||||
// NoInputs is what every other sketch overlay sets and is exactly what this one must not:
|
||||
// it is the only overlay in the tab that the user types into.
|
||||
imgui.begin(std::string("##sketchvalue"),
|
||||
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration
|
||||
| ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings);
|
||||
ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow());
|
||||
|
||||
if (!m_title.empty() || !m_err.empty()) {
|
||||
if (m_err.empty()) {
|
||||
imgui.text(m_title);
|
||||
} else {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGuiWrapper::to_ImVec4(ColorRGBA(0.91f, 0.42f, 0.42f, 1.0f)));
|
||||
imgui.text(m_err);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
}
|
||||
|
||||
if (m_focus_pending) {
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
m_focus_pending = false;
|
||||
}
|
||||
ImGui::PushItemWidth(90.0f * scale);
|
||||
// EnterReturnsTrue so Enter commits from inside the widget; AutoSelectAll so the prefill is
|
||||
// replaced by the first digit typed, which is what "pre-selected" meant when this was a
|
||||
// wxTextCtrl and is what makes typing a value a single gesture.
|
||||
const bool entered = ImGui::InputText("##sketchvalue_in", m_buf, sizeof(m_buf),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue
|
||||
| ImGuiInputTextFlags_AutoSelectAll
|
||||
| ImGuiInputTextFlags_CharsDecimal);
|
||||
// MEASUREMENT, not a fix: one line per frame saying whether ImGui believes it owns the
|
||||
// keyboard and whether our widget is the active one. "Typing does not arrive" has two very
|
||||
// different causes — no FRAMES (this canvas repaints on demand only, so an idle canvas never
|
||||
// processes ImGui's queued characters) versus frames that run while the input is not active —
|
||||
// and they are indistinguishable from outside.
|
||||
if (std::getenv("ORCA_CAD_UXTRACE")) {
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
std::fprintf(stderr, "[UX] frame title=%s want_text=%d want_kb=%d active=%d buf=%s\n",
|
||||
m_title.c_str(), (int) io.WantTextInput, (int) io.WantCaptureKeyboard,
|
||||
(int) ImGui::IsItemActive(), m_buf);
|
||||
std::fflush(stderr);
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
imgui.end();
|
||||
ImGui::PopStyleVar();
|
||||
ImGuiWrapper::pop_common_window_style();
|
||||
|
||||
// Keep the frames coming while the field is up — see request_frame's note in the header.
|
||||
if (m_open && request_frame)
|
||||
request_frame();
|
||||
|
||||
// Act AFTER end(): do_commit can reopen the field for the next queued dimension, and that
|
||||
// must not happen inside this frame's window.
|
||||
if (entered)
|
||||
do_commit();
|
||||
else if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape)))
|
||||
do_cancel();
|
||||
return true;
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
@@ -0,0 +1,95 @@
|
||||
#ifndef slic3r_SketchInlineEditor_hpp_
|
||||
#define slic3r_SketchInlineEditor_hpp_
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include <wx/gdicmn.h>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
class ImGuiWrapper;
|
||||
|
||||
// Onshape-style in-canvas value editor.
|
||||
//
|
||||
// IT IS NOT A WINDOW. It used to be a borderless top-level wxFrame holding a wxTextCtrl, and
|
||||
// that is the whole history of this file: a separate top-level window can only receive typing
|
||||
// if the window manager grants it focus, and whether it does is not ours to decide. openbox
|
||||
// grants it; mutter's focus-stealing prevention refuses it, so on a GNOME desktop the field
|
||||
// appeared, showed its value selected, and silently ignored every keystroke — Enter then
|
||||
// committed the number it opened with. Seven workarounds were tried against that (a real X11
|
||||
// server timestamp for gtk_window_present, re-asserted SetFocus, dropping the _UTILITY hint,
|
||||
// keeping the frame mapped between two queued fields, forwarding keys from the panel's
|
||||
// CHAR_HOOK), one of them caused a macOS regression, and the test harness ended up clicking the
|
||||
// field before typing — which is the workaround a user cannot be asked to perform, and is
|
||||
// exactly the "label value not editable" report.
|
||||
//
|
||||
// So the field stops asking. It is now drawn INSIDE the GL canvas as an ImGui overlay, at the
|
||||
// same screen point as before, and its keys arrive through the canvas's own key events, which
|
||||
// GLCanvas3D already feeds to ImGui (see GLCanvas3D::on_key / on_char -> update_key_data). The
|
||||
// canvas is part of the main window and already has focus, so there is no second window, no
|
||||
// second focus, and no window manager in the path. The dimension labels next to it are already
|
||||
// ImGui overlays (DesignSketchTool::draw_dim_label), so this is the same vocabulary, not a new
|
||||
// one.
|
||||
//
|
||||
// Ownership: DesignCanvas owns it; DesignSketchTool::render() calls render() once per frame.
|
||||
class SketchInlineEditor
|
||||
{
|
||||
public:
|
||||
SketchInlineEditor() = default;
|
||||
|
||||
// Open the field anchored at `canvas_px` (canvas DEVICE pixels, the coordinate space the
|
||||
// sketch tool works in), pre-filled with `value` and pre-selected. on_commit(parsed) fires
|
||||
// on Enter with a valid number; on_cancel() on Esc.
|
||||
void open(const wxPoint& canvas_px, double value, const std::string& title,
|
||||
std::function<void(double)> on_commit,
|
||||
std::function<void()> on_cancel);
|
||||
void close(); // drop it with neither callback
|
||||
void cancel(); // if open, run the registered cancel (keep-as-drawn)
|
||||
void commit(); // if open, run the registered commit (accept the typed value)
|
||||
bool is_open() const { return m_open; }
|
||||
|
||||
// Draw it, and let ImGui do the editing. Called from DesignSketchTool::render() inside the
|
||||
// frame's ImGui pass; `scale` is the tool's m_render_scale. Returns true if it drew.
|
||||
bool render(ImGuiWrapper& imgui, float scale);
|
||||
|
||||
// Ask for another frame. THE FIELD DOES NOT WORK WITHOUT THIS, and the reason is a deadlock
|
||||
// that only a per-frame trace shows:
|
||||
//
|
||||
// [UX] frame want_text=0 want_kb=0 active=0 <- frame 1: the widget is not active yet
|
||||
// [UX] frame want_text=0 want_kb=0 active=1 <- frame 2: it is now
|
||||
// (nothing further) <- the canvas has nothing to redraw, so it stops
|
||||
//
|
||||
// This canvas repaints ON DEMAND. ImGui decides whether it wants the keyboard at the END of a
|
||||
// frame, from the active item, and GLCanvas3D::on_char only calls render() when
|
||||
// update_key_data() says ImGui wants it. No frames -> WantTextInput never turns on -> no
|
||||
// render on a keystroke -> still no frames. The characters sit in ImGui's queue and the field
|
||||
// looks exactly as deaf as the window it replaced. One repaint per frame while it is open
|
||||
// breaks the circle.
|
||||
std::function<void()> request_frame;
|
||||
|
||||
// Kept because callers ask them, but there is no longer any difference to report: with no
|
||||
// window there is no state where the field is on screen but logically closed, and no state
|
||||
// where it is open but somebody else holds the keyboard.
|
||||
bool is_mapped() const { return m_open; }
|
||||
bool has_focus() const { return m_open; }
|
||||
void dismiss() { close(); }
|
||||
|
||||
private:
|
||||
void do_commit();
|
||||
void do_cancel();
|
||||
|
||||
std::function<void(double)> m_commit;
|
||||
std::function<void()> m_cancel;
|
||||
bool m_open{false};
|
||||
bool m_focus_pending{false}; // one frame of SetKeyboardFocusHere after opening
|
||||
wxPoint m_anchor{0, 0}; // canvas device px
|
||||
std::string m_title;
|
||||
std::string m_err; // why the last value was refused, shown in the title line
|
||||
char m_buf[64]{}; // the edited text; ImGui::InputText writes into it
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_SketchInlineEditor_hpp_
|
||||
@@ -395,8 +395,9 @@ bool confirm_create_decompose_missing_components(wxWindow* parent, const std::ve
|
||||
missing_text += missing[i].display_name;
|
||||
}
|
||||
|
||||
wxString message = _L("The current filament list does not contain ") + missing_text +
|
||||
_L(". A project filament required by the mixed filament will be created automatically after decomposition.");
|
||||
wxString message = wxString::Format(_L("The current filament list does not contain %s. A project filament required by "
|
||||
"the mixed filament will be created automatically after decomposition."),
|
||||
missing_text);
|
||||
|
||||
MessageDialog dlg(parent, message, _L("Tip"), wxOK | wxCANCEL | wxICON_INFORMATION);
|
||||
dlg.show_dsa_button();
|
||||
|
||||
@@ -1041,10 +1041,12 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
||||
|
||||
for (auto el : {"wipe_tower_rotation_angle", "wipe_tower_cone_angle",
|
||||
"wipe_tower_extra_spacing", "wipe_tower_max_purge_speed",
|
||||
"wipe_tower_bridging", "wipe_tower_extra_flow",
|
||||
"wipe_tower_no_sparse_layers"})
|
||||
"wipe_tower_bridging", "wipe_tower_extra_flow"})
|
||||
toggle_line(el, have_prime_tower && supports_wipe_tower_2);
|
||||
|
||||
// Orca: both tower generators skip sparse layers, so this is not a wipe tower 2 exclusive.
|
||||
toggle_line("wipe_tower_no_sparse_layers", have_prime_tower);
|
||||
|
||||
WipeTowerWallType wipe_tower_wall_type = config->opt_enum<WipeTowerWallType>("wipe_tower_wall_type");
|
||||
bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower;
|
||||
toggle_line("wipe_tower_cone_angle", have_prime_tower && supports_wipe_tower_2 && wipe_tower_wall_type == WipeTowerWallType::wtwCone);
|
||||
@@ -1055,6 +1057,10 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
||||
|
||||
toggle_line("single_extruder_multi_material_priming", !bSEMM && have_prime_tower && supports_wipe_tower_2);
|
||||
|
||||
bool use_cyclic_ordering = config->opt_enum<ToolChangeOrderingType>("toolchange_ordering") == ToolChangeOrderingType::Cyclic;
|
||||
toggle_line("toolchange_cyclic_order", use_cyclic_ordering);
|
||||
toggle_line("toolchange_cyclic_first_layer", use_cyclic_ordering);
|
||||
|
||||
toggle_line("prime_volume",have_prime_tower && (!purge_in_primetower || !bSEMM));
|
||||
|
||||
for (auto el : {"flush_into_infill", "flush_into_support", "flush_into_objects"})
|
||||
@@ -1128,6 +1134,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
||||
bool has_detect_overhang_wall = config->opt_bool("detect_overhang_wall");
|
||||
bool has_overhang_reverse = config->opt_bool("overhang_reverse");
|
||||
bool allow_overhang_reverse = !has_spiral_vase;
|
||||
toggle_line("unsupported_wall_last", has_detect_overhang_wall);
|
||||
toggle_line("overhang_reverse", allow_overhang_reverse);
|
||||
toggle_line("overhang_reverse_internal_only", allow_overhang_reverse && has_overhang_reverse);
|
||||
bool has_overhang_reverse_internal_only = config->opt_bool("overhang_reverse_internal_only");
|
||||
|
||||
@@ -188,6 +188,17 @@ wxString get_string_value(const std::string& opt_key, const DynamicPrintConfig&
|
||||
out = double_to_string(opt->value) + (opt->percent ? "%" : "");
|
||||
return out;
|
||||
}
|
||||
case coFloatsOrPercents: {
|
||||
const auto* values = static_cast<const ConfigOptionVector<FloatOrPercent>*>(option);
|
||||
// Orca: Preset comparison may request the entire vector instead of an indexed entry.
|
||||
if (orig_opt_idx < 0)
|
||||
return from_u8(option->serialize());
|
||||
if (opt_idx < values->size()) {
|
||||
const FloatOrPercent& value = values->get_at(opt_idx);
|
||||
return double_to_string(value.value) + (value.percent ? "%" : "");
|
||||
}
|
||||
return _L("Undefined");
|
||||
}
|
||||
case coEnum: {
|
||||
return get_string_from_enum(pure_key, config,
|
||||
pure_key == "top_surface_pattern" ||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "Plater.hpp"
|
||||
#include "Camera.hpp"
|
||||
#include "I18N.hpp"
|
||||
#include "format.hpp"
|
||||
#include "GUI_Utils.hpp"
|
||||
#include "GUI.hpp"
|
||||
#include "GLCanvas3D.hpp"
|
||||
@@ -3436,16 +3437,18 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
|
||||
return ret;
|
||||
};
|
||||
|
||||
// Whole sentences: the bare "up to"/"above"/"from"/"to" these used to be glued from gave a
|
||||
// translator no context, and left the unit and the numbers stuck in English word order.
|
||||
auto upto_label = [](double z) {
|
||||
char buf[64];
|
||||
::sprintf(buf, "%.2f", z);
|
||||
return _u8L("up to") + " " + std::string(buf) + " " + _u8L("mm");
|
||||
return format(_u8L("up to %1% mm"), buf);
|
||||
};
|
||||
|
||||
auto above_label = [](double z) {
|
||||
char buf[64];
|
||||
::sprintf(buf, "%.2f", z);
|
||||
return _u8L("above") + " " + std::string(buf) + " " + _u8L("mm");
|
||||
return format(_u8L("above %1% mm"), buf);
|
||||
};
|
||||
|
||||
auto fromto_label = [](double z1, double z2) {
|
||||
@@ -3453,7 +3456,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
|
||||
::sprintf(buf1, "%.2f", z1);
|
||||
char buf2[64];
|
||||
::sprintf(buf2, "%.2f", z2);
|
||||
return _u8L("from") + " " + std::string(buf1) + " " + _u8L("to") + " " + std::string(buf2) + " " + _u8L("mm");
|
||||
return format(_u8L("from %1% to %2% mm"), buf1, buf2);
|
||||
};
|
||||
|
||||
auto role_time_and_percent = [this, total_estimated_time](libvgcode::EGCodeExtrusionRole role) {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "GLCanvas3D.hpp"
|
||||
#ifdef SLIC3R_CAD
|
||||
#include "slic3r/GUI/CAD/DesignSketchTool.hpp" // Design tab: interactive 2D sketch tool
|
||||
#endif
|
||||
|
||||
#include <igl/unproject.h>
|
||||
|
||||
@@ -1826,6 +1829,16 @@ void GLCanvas3D::enable_separator_toolbar(bool enable)
|
||||
m_separator_toolbar.set_enabled(enable);
|
||||
}
|
||||
|
||||
void GLCanvas3D::enable_collapse_toolbar(bool enable)
|
||||
{
|
||||
m_collapse_toolbar_enabled = enable;
|
||||
}
|
||||
|
||||
void GLCanvas3D::enable_plate_chrome(bool enable)
|
||||
{
|
||||
m_plate_chrome_enabled = enable;
|
||||
}
|
||||
|
||||
bool GLCanvas3D::has_mouse_capture() const {
|
||||
return m_canvas != nullptr && m_canvas->HasCapture();
|
||||
}
|
||||
@@ -2047,14 +2060,24 @@ void GLCanvas3D::render(bool only_init)
|
||||
no_partplate = true;
|
||||
else if (gizmo_type == GLGizmosManager::BrimEars && !camera.is_looking_downward())
|
||||
show_grid = false;
|
||||
if (m_axes_at_bed_center)
|
||||
// Design tab: the plate grid is generated from the plate's front-left corner, so it
|
||||
// floats mid-cell under the modeling-origin triad. Suppress it here; a CAD grid centred
|
||||
// on the origin is rendered in its place (see _render_cad_grid).
|
||||
show_grid = false;
|
||||
|
||||
/* view3D render*/
|
||||
int hover_id = (m_hover_plate_idxs.size() > 0)?m_hover_plate_idxs.front():-1;
|
||||
if (m_canvas_type == ECanvasType::CanvasView3D) {
|
||||
if (!no_partplate)
|
||||
// m_show_bed gates the plate list too: hiding the bed but leaving its grid and outline
|
||||
// floating would read as a rendering fault rather than a deliberate view option.
|
||||
if (!no_partplate && m_show_bed)
|
||||
_render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), m_show_world_axes);
|
||||
if (!no_partplate) //BBS: add outline logic
|
||||
if (!no_partplate && m_show_bed) //BBS: add outline logic
|
||||
_render_platelist(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), only_current, only_body, hover_id, true, show_grid);
|
||||
if (m_axes_at_bed_center && m_show_bed && !no_partplate)
|
||||
// Design tab: replace the plate's corner-origin grid with the origin-centred CAD grid.
|
||||
_render_cad_grid(camera.get_view_matrix(), camera.get_projection_matrix());
|
||||
|
||||
//BBS: add outline logic
|
||||
// Depth pass for object-on-object and self shadows; consumed by the gouraud shader below.
|
||||
@@ -2120,6 +2143,13 @@ void GLCanvas3D::render(bool only_init)
|
||||
if (_is_fxaa_enabled())
|
||||
_render_fxaa_pass(static_cast<unsigned int>(cnv_size.get_width()), static_cast<unsigned int>(cnv_size.get_height()));
|
||||
|
||||
// Design tab: interactive 2D sketch overlay, drawn over the scene but
|
||||
// beneath the UI overlays (toolbars, labels).
|
||||
#ifdef SLIC3R_CAD
|
||||
if (m_design_sketch_tool != nullptr && m_design_sketch_tool->has_display())
|
||||
m_design_sketch_tool->render(*this);
|
||||
#endif
|
||||
|
||||
// draw overlays
|
||||
_render_overlays();
|
||||
|
||||
@@ -3202,7 +3232,11 @@ void GLCanvas3D::on_idle(wxIdleEvent& evt)
|
||||
// BBS
|
||||
//m_dirty |= wxGetApp().plater()->get_view_toolbar().update_items_state();
|
||||
m_dirty |= wxGetApp().plater()->get_collapse_toolbar().update_items_state();
|
||||
bool mouse3d_controller_applied = wxGetApp().plater()->get_mouse3d_controller().apply(wxGetApp().plater()->get_camera());
|
||||
// apply() DRAINS the 3D-mouse queue, so only the canvas actually on screen may call it: a
|
||||
// hidden canvas renders nothing, so the motion it swallowed moves the shared camera without
|
||||
// ever being drawn and the next visible frame jumps several states at once.
|
||||
bool mouse3d_controller_applied = _is_shown_on_screen()
|
||||
&& wxGetApp().plater()->get_mouse3d_controller().apply(wxGetApp().plater()->get_camera());
|
||||
m_dirty |= mouse3d_controller_applied;
|
||||
m_dirty |= wxGetApp().plater()->get_notification_manager()->update_notifications(*this);
|
||||
auto gizmo = wxGetApp().plater()->get_view3D_canvas3D()->get_gizmos_manager().get_current();
|
||||
@@ -3270,6 +3304,64 @@ void GLCanvas3D::on_char(wxKeyEvent& evt)
|
||||
return;
|
||||
}
|
||||
|
||||
// Design tab: Delete/Backspace removes the selected sketch entities while a
|
||||
// sketch tool is active and the canvas has focus (dialog text fields are separate
|
||||
// wx controls, so this never eats their editing keys).
|
||||
#ifdef SLIC3R_CAD
|
||||
if (m_design_sketch_tool != nullptr && m_design_sketch_tool->is_active()
|
||||
&& (keyCode == WXK_DELETE || keyCode == WXK_BACK)
|
||||
&& !m_design_sketch_tool->selection().empty()) {
|
||||
m_design_sketch_tool->delete_selected();
|
||||
m_dirty = true;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Esc exits the active sketch tool (Onshape-like, layered: abort in-progress entity ->
|
||||
// drop to Select -> exit the session back to Feature mode).
|
||||
#ifdef SLIC3R_CAD
|
||||
if (m_design_sketch_tool != nullptr && m_design_sketch_tool->is_active()
|
||||
&& keyCode == WXK_ESCAPE) {
|
||||
m_design_sketch_tool->request_exit();
|
||||
m_dirty = true;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Design tab: Ctrl+Z / Ctrl+Shift+Z (and Ctrl+Y) undo/redo the Design feature
|
||||
// history. Scoped by m_design_sketch_tool — only the Design canvas owns one — so the
|
||||
// main 3D editor's undo/redo (the CanvasView3D-gated cases further below) is untouched.
|
||||
// Handled here, before the generic Ctrl block, so it takes precedence and early-returns.
|
||||
#ifdef SLIC3R_CAD
|
||||
if (m_design_sketch_tool != nullptr && (evt.GetModifiers() & ctrlMask) != 0) {
|
||||
const bool is_z = (keyCode == 'z' || keyCode == 'Z' || keyCode == WXK_CONTROL_Z);
|
||||
const bool is_y = (keyCode == 'y' || keyCode == 'Y' || keyCode == WXK_CONTROL_Y);
|
||||
if (is_z || is_y) {
|
||||
const bool redo = is_y || ((evt.GetModifiers() & shiftMask) != 0);
|
||||
m_design_sketch_tool->request_undo_redo(redo);
|
||||
m_dirty = true;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Design tab: F = Place on Face (Prepare's lay-flat), when the Design viewport is up
|
||||
// and a body face is selected. The tool forwards to DesignPanel::place_on_face; it returns
|
||||
// false (no face picked) so F falls through to the default handler below.
|
||||
#ifdef SLIC3R_CAD
|
||||
if (m_design_sketch_tool != nullptr && m_design_sketch_tool->has_display()
|
||||
&& (keyCode == 'f' || keyCode == 'F') && (evt.GetModifiers() & ctrlMask) == 0) {
|
||||
if (m_design_sketch_tool->request_place_on_face()) {
|
||||
m_dirty = true;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
bool is_in_painting_mode = false;
|
||||
GLGizmoPainterBase *current_gizmo_painter = dynamic_cast<GLGizmoPainterBase *>(get_gizmos_manager().get_current());
|
||||
if (current_gizmo_painter != nullptr) {
|
||||
@@ -3642,6 +3734,20 @@ public:
|
||||
|
||||
void GLCanvas3D::on_key(wxKeyEvent& evt)
|
||||
{
|
||||
// Design tab: Delete/Backspace removes selected sketch entities. GTK delivers
|
||||
// these as KEY_DOWN rather than CHAR, so handle it here too.
|
||||
#ifdef SLIC3R_CAD
|
||||
if (evt.GetEventType() == wxEVT_KEY_DOWN
|
||||
&& m_design_sketch_tool != nullptr && m_design_sketch_tool->is_active()
|
||||
&& (evt.GetKeyCode() == WXK_DELETE || evt.GetKeyCode() == WXK_BACK)
|
||||
&& !m_design_sketch_tool->selection().empty()) {
|
||||
m_design_sketch_tool->delete_selected();
|
||||
m_dirty = true;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
static GLCanvas3D const * thiz = nullptr;
|
||||
static TranslationProcessor translationProcessor(nullptr, nullptr);
|
||||
if (thiz != this) {
|
||||
@@ -4206,6 +4312,23 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
|
||||
return;
|
||||
}
|
||||
|
||||
// Design tab: the interactive sketch tool owns the mouse whenever it has
|
||||
// something on screen — an active session OR committed sketch overlays that the user
|
||||
// can click to select. It runs after ImGui (so dialogs still work) but before
|
||||
// camera/toolbar/gizmo handling; on_mouse returns false for events it doesn't consume
|
||||
// (drag/orbit/wheel) so the camera keeps working over the display-only plate.
|
||||
#ifdef SLIC3R_CAD
|
||||
if (m_design_sketch_tool != nullptr && m_design_sketch_tool->has_display()) {
|
||||
if (evt.LeftDown() && m_canvas != nullptr)
|
||||
m_canvas->SetFocus(); // grab keyboard focus so Delete/keys reach this canvas
|
||||
if (m_design_sketch_tool->on_mouse(evt, *this)) {
|
||||
m_dirty = true;
|
||||
render(); // force an immediate redraw so the sketch overlay updates live
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef __WXMSW__
|
||||
bool on_enter_workaround = false;
|
||||
if (! evt.Entering() && ! evt.Leaving() && m_mouse.position.x() == -1.0) {
|
||||
@@ -4310,6 +4433,9 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
|
||||
if (can_sequential_clearance_show_in_gizmo())
|
||||
update_sequential_clearance();
|
||||
} else {
|
||||
// Orca: by-layer counterpart, for a prime tower compacted by "No sparse layers".
|
||||
if (current_printer_technology() == ptFFF && can_sequential_clearance_show_in_gizmo())
|
||||
update_compacted_wipe_tower_clearance();
|
||||
if (c == GLGizmosManager::EType::Move ||
|
||||
c == GLGizmosManager::EType::Scale ||
|
||||
c == GLGizmosManager::EType::Rotate)
|
||||
@@ -4543,8 +4669,12 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
|
||||
TransformationType trafo_type;
|
||||
trafo_type.set_relative();
|
||||
m_selection.translate(cur_pos - m_mouse.drag.start_position_3D, trafo_type);
|
||||
if (current_printer_technology() == ptFFF && (fff_print()->config().print_sequence == PrintSequence::ByObject))
|
||||
update_sequential_clearance();
|
||||
if (current_printer_technology() == ptFFF) {
|
||||
if (fff_print()->config().print_sequence == PrintSequence::ByObject)
|
||||
update_sequential_clearance();
|
||||
else
|
||||
update_compacted_wipe_tower_clearance();
|
||||
}
|
||||
// BBS
|
||||
//wxGetApp().obj_manipul()->set_dirty();
|
||||
m_dirty = true;
|
||||
@@ -4910,6 +5040,8 @@ bool GLCanvas3D::is_camera_rotate(const wxMouseEvent& evt, const std::map<MouseB
|
||||
{
|
||||
if (m_is_touchpad_navigation) {
|
||||
return evt.Moving() && evt.AltDown() && !evt.ShiftDown();
|
||||
} else if (m_cad_navigation) {
|
||||
return evt.Dragging() && evt.MiddleIsDown(); // left-drag is the selection rubber band
|
||||
} else {
|
||||
return evt.Dragging() && clicked_button_matches_action(evt, MouseAction::Rotation, mappings);
|
||||
}
|
||||
@@ -4919,6 +5051,8 @@ bool GLCanvas3D::is_camera_pan(const wxMouseEvent& evt, const std::map<MouseButt
|
||||
{
|
||||
if (m_is_touchpad_navigation) {
|
||||
return evt.Moving() && evt.ShiftDown() && !evt.AltDown();
|
||||
} else if (m_cad_navigation) {
|
||||
return evt.Dragging() && evt.RightIsDown(); // middle now orbits, so pan is right only
|
||||
} else {
|
||||
return evt.Dragging() && clicked_button_matches_action(evt, MouseAction::Pan, mappings);
|
||||
;
|
||||
@@ -5608,6 +5742,101 @@ bool GLCanvas3D::can_sequential_clearance_show_in_gizmo() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Live preview of the compacted prime tower clearance, the by-layer counterpart of
|
||||
// update_sequential_clearance(). Called while the user drags a volume / gizmo; idle visibility
|
||||
// matches sequential print (hidden when valid, filled when Print::validate reports a collision).
|
||||
// Print::compacted_wipe_tower_clearance_valid() answers the same question authoritatively, but it
|
||||
// reads the tower position from the config, which only catches up once do_move() writes it back on
|
||||
// mouse release. Recomputing from the volumes here is what makes the keep-out zone follow the tower
|
||||
// while it is still under the cursor.
|
||||
void GLCanvas3D::update_compacted_wipe_tower_clearance()
|
||||
{
|
||||
if (current_printer_technology() != ptFFF)
|
||||
return;
|
||||
const Print *print = fff_print();
|
||||
if (print == nullptr)
|
||||
return;
|
||||
const PrintConfig &config = print->config();
|
||||
if (config.print_sequence != PrintSequence::ByLayer || ! wipe_tower_sparse_layers_skipped(config) || ! print->has_wipe_tower())
|
||||
return;
|
||||
|
||||
PartPlateList &plate_list = wxGetApp().plater()->get_partplate_list();
|
||||
PartPlate *plate = plate_list.get_curr_plate();
|
||||
if (plate == nullptr)
|
||||
return;
|
||||
const int plate_id = plate_list.get_curr_plate_index();
|
||||
|
||||
// Once the tower has been generated the scene shows its real mesh with the brim merged in,
|
||||
// otherwise it is a bare estimated cube with no brim at all. Only the latter needs the brim added
|
||||
// here, and the width comes from WipeTowerData, the same source the preview box is sized from, so
|
||||
// the zone cannot be padded against a brim the preview was not built with.
|
||||
const bool preview_carries_brim = print->is_step_done(psWipeTower) && print->wipe_tower_data().wipe_tower_mesh_data.has_value();
|
||||
const double brim = preview_carries_brim ? 0. : double(print->wipe_tower_data(print->extruders().size()).brim_width);
|
||||
const double padding = compacted_tower_footprint_padding(config, brim);
|
||||
|
||||
// Tower footprint straight from the volume the user sees, so that dragging either the tower or an
|
||||
// object updates the zone on the very next frame.
|
||||
Polygon tower_footprint;
|
||||
for (const GLVolume *v : m_volumes.volumes) {
|
||||
if (! v->is_wipe_tower || v->object_idx() - 1000 != plate_id)
|
||||
continue;
|
||||
const BoundingBoxf3 bbox = v->transformed_convex_hull_bounding_box();
|
||||
tower_footprint = Polygon({ Point(scale_(bbox.min.x() - padding), scale_(bbox.min.y() - padding)),
|
||||
Point(scale_(bbox.max.x() + padding), scale_(bbox.min.y() - padding)),
|
||||
Point(scale_(bbox.max.x() + padding), scale_(bbox.max.y() + padding)),
|
||||
Point(scale_(bbox.min.x() - padding), scale_(bbox.max.y() + padding)) });
|
||||
break;
|
||||
}
|
||||
|
||||
const CompactedTowerZone zone = compacted_wipe_tower_zone(config, tower_footprint);
|
||||
if (zone.empty()) {
|
||||
reset_sequential_print_clearance();
|
||||
return;
|
||||
}
|
||||
|
||||
// While dragging, outline every on-plate instance next to the tower ring, the way sequential print
|
||||
// outlines every object. Both carry half of the clearance, so the two outlines meeting is precisely
|
||||
// the moment that object goes over its limit - which is what makes the pair worth drawing at all.
|
||||
// The tier is per object, so a short object gets the narrow nozzle outline rather than the wide
|
||||
// body one it is not subject to; without that, a 3 mm object parked beside the tower would be drawn
|
||||
// deep inside the keep-out ring while passing the check. Only the instances that already exceed
|
||||
// allowed_rise also get a height limit plane.
|
||||
Polygons outlines;
|
||||
std::vector<std::pair<Polygon, float>> height_polygons;
|
||||
bool body_tier_used = false;
|
||||
const BoundingBox plate_bb = plate->get_bounding_box_crd();
|
||||
for (const ModelObject *model_object : m_model->objects) {
|
||||
for (size_t i = 0; i < model_object->instances.size(); ++i) {
|
||||
Geometry::Transformation trafo(model_object->instances[i]->get_transformation());
|
||||
const Vec3d offset = trafo.get_offset();
|
||||
trafo.set_offset(Vec3d(offset.x(), offset.y(), 0.0));
|
||||
const Polygon inst_hull = model_object->convex_hull_2d(trafo.get_matrix());
|
||||
if (inst_hull.points.empty() || ! plate_bb.overlap(inst_hull.bounding_box()))
|
||||
continue;
|
||||
|
||||
// Same tiers and the same rise measured from the plate as
|
||||
// Print::compacted_wipe_tower_clearance_valid(), so that the preview and the validation
|
||||
// that follows it never contradict each other.
|
||||
const double object_top = model_object->get_instance_max_z(i);
|
||||
const CompactedTowerClearance clearance = compacted_wipe_tower_clearance(config, zone, inst_hull, object_top);
|
||||
body_tier_used = body_tier_used || compacted_tower_body_tier(clearance);
|
||||
|
||||
const Polygon outline = compacted_wipe_tower_offender_outline(inst_hull, clearance.body_clearance);
|
||||
outlines.emplace_back(outline);
|
||||
if (object_top <= clearance.allowed_rise + EPSILON)
|
||||
continue;
|
||||
height_polygons.emplace_back(outline, float(clearance.allowed_rise));
|
||||
}
|
||||
}
|
||||
|
||||
Polygons polygons = compacted_wipe_tower_rings(zone, body_tier_used);
|
||||
append(polygons, outlines);
|
||||
|
||||
set_sequential_print_clearance_visible(true);
|
||||
set_sequential_print_clearance_render_fill(false);
|
||||
set_sequential_print_clearance_polygons(polygons, height_polygons);
|
||||
}
|
||||
|
||||
void GLCanvas3D::update_sequential_clearance()
|
||||
{
|
||||
if (current_printer_technology() != ptFFF || (fff_print()->config().print_sequence == PrintSequence::ByLayer))
|
||||
@@ -7917,13 +8146,113 @@ void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d&
|
||||
*/
|
||||
//bool show_texture = true;
|
||||
//BBS set axes mode
|
||||
m_bed.set_axes_mode(m_main_toolbar.is_enabled());
|
||||
if (m_axes_at_bed_center) {
|
||||
// Design tab: triad at the bed centre = modeling origin (set every frame because
|
||||
// set_shape/set_axes_mode otherwise reset it to the bed corner).
|
||||
const Vec2d bc = m_bed.build_volume().bed_center();
|
||||
m_bed.set_axes_origin(Vec3d(bc.x(), bc.y(), 0.0));
|
||||
} else {
|
||||
m_bed.set_axes_mode(m_main_toolbar.is_enabled());
|
||||
}
|
||||
m_bed.render(*this, view_matrix, projection_matrix, bottom, scale_factor, show_axes);
|
||||
}
|
||||
|
||||
void GLCanvas3D::_render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid)
|
||||
{
|
||||
wxGetApp().plater()->get_partplate_list().render(view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid);
|
||||
wxGetApp().plater()->get_partplate_list().render(view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid, !m_plate_chrome_enabled);
|
||||
}
|
||||
|
||||
// Design tab: CAD grid on the bed plane, drawn in place of the plate's corner-origin grid.
|
||||
// Generated from the bed centre (= modeling origin) so a grid line passes exactly through the
|
||||
// triad in both axes. Minor lines every 10 mm, major every 50 mm; the two GLModels are built
|
||||
// once and rebuilt only when the bed shape changes, not per frame.
|
||||
void GLCanvas3D::_render_cad_grid(const Transform3d& view_matrix, const Transform3d& projection_matrix)
|
||||
{
|
||||
const BuildVolume& build_volume = m_bed.build_volume();
|
||||
if (!build_volume.valid())
|
||||
return;
|
||||
|
||||
const Vec2d center = build_volume.bed_center();
|
||||
const BoundingBoxf bb = build_volume.bounding_volume2d();
|
||||
if (!m_cad_grid_valid || m_cad_grid_center != center || m_cad_grid_bb != bb) {
|
||||
m_cad_grid_center = center;
|
||||
m_cad_grid_bb = bb;
|
||||
m_cad_grid_valid = true;
|
||||
|
||||
// Same z as PartPlate::GROUND_Z_GRIDLINE (-0.26f): just below the bed fill (GROUND_Z =
|
||||
// -0.03f, which is drawn with the depth mask disabled) and above the physical bed model
|
||||
// (offset z = -0.41), so the grid never z-fights the bed quad. Chosen by construction,
|
||||
// not by magic number: it is the exact z the plate grid already uses on the shared bed.
|
||||
const float z = -0.26f;
|
||||
|
||||
auto build_grid = [&z, ¢er, &bb](double step, GLModel& model) {
|
||||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||||
// Constant-x (vertical on screen) lines, both directions from the centre so the
|
||||
// centre column itself is always present. Clipped to the bed bounding box so nothing
|
||||
// spills past the bed quad.
|
||||
for (double x = center.x(); x >= bb.min.x(); x -= step)
|
||||
segs.emplace_back(Vec2d(x, bb.min.y()), Vec2d(x, bb.max.y()));
|
||||
for (double x = center.x() + step; x <= bb.max.x(); x += step)
|
||||
segs.emplace_back(Vec2d(x, bb.min.y()), Vec2d(x, bb.max.y()));
|
||||
// Constant-y (horizontal on screen) lines, same centre-first convention.
|
||||
for (double y = center.y(); y >= bb.min.y(); y -= step)
|
||||
segs.emplace_back(Vec2d(bb.min.x(), y), Vec2d(bb.max.x(), y));
|
||||
for (double y = center.y() + step; y <= bb.max.y(); y += step)
|
||||
segs.emplace_back(Vec2d(bb.min.x(), y), Vec2d(bb.max.x(), y));
|
||||
|
||||
GLModel::Geometry data;
|
||||
data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 };
|
||||
data.reserve_vertices(2 * segs.size());
|
||||
data.reserve_indices(2 * segs.size());
|
||||
for (const auto& s : segs) {
|
||||
data.add_vertex(Vec3f(float(s.first.x()), float(s.first.y()), z));
|
||||
data.add_vertex(Vec3f(float(s.second.x()), float(s.second.y()), z));
|
||||
const unsigned int vc = static_cast<unsigned int>(data.vertices_count());
|
||||
data.add_line(vc - 2, vc - 1);
|
||||
}
|
||||
model.init_from(std::move(data));
|
||||
};
|
||||
|
||||
m_cad_grid_minor.reset();
|
||||
m_cad_grid_major.reset();
|
||||
build_grid(10.0, m_cad_grid_minor);
|
||||
build_grid(50.0, m_cad_grid_major);
|
||||
}
|
||||
|
||||
if (!m_cad_grid_minor.is_initialized() || !m_cad_grid_major.is_initialized())
|
||||
return;
|
||||
|
||||
GLShaderProgram* shader = wxGetApp().get_shader("flat");
|
||||
if (shader == nullptr)
|
||||
return;
|
||||
|
||||
shader->start_using();
|
||||
glsafe(::glEnable(GL_BLEND));
|
||||
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
|
||||
shader->set_uniform("view_model_matrix", view_matrix);
|
||||
shader->set_uniform("projection_matrix", projection_matrix);
|
||||
|
||||
// White every 5 cm, grey every 1 cm — the SAME in both themes, deliberately. There is no
|
||||
// "white bed" to vanish against: the plate is dark grey either way, DEFAULT_MODEL_COLOR
|
||||
// {0.326,0.337,0.337} on light and DEFAULT_MODEL_COLOR_DARK {0.255,0.255,0.283} on dark
|
||||
// (3DBed.cpp:185-186), a difference of 0.07. A per-theme palette here would be a branch
|
||||
// that buys nothing and one more thing to keep in step.
|
||||
//
|
||||
// For contrast with what this replaces: the plate's own grid uses LINE_TOP_DARK_COLOR, a
|
||||
// 0.43 grey, for BOTH its thin and its bold family — which is most of why the stock grid
|
||||
// reads as a flat mesh with no scale to it.
|
||||
const ColorRGBA minor_color(0.40f, 0.40f, 0.42f, 1.0f);
|
||||
const ColorRGBA major_color(0.90f, 0.90f, 0.90f, 1.0f);
|
||||
|
||||
glsafe(::glLineWidth(1.0f));
|
||||
m_cad_grid_minor.set_color(minor_color);
|
||||
m_cad_grid_minor.render();
|
||||
|
||||
glsafe(::glLineWidth(2.0f));
|
||||
m_cad_grid_major.set_color(major_color);
|
||||
m_cad_grid_major.render();
|
||||
|
||||
glsafe(::glDisable(GL_BLEND));
|
||||
}
|
||||
|
||||
void GLCanvas3D::_render_shadows(const Transform3d& view_matrix, const Transform3d& projection_matrix)
|
||||
@@ -9596,6 +9925,9 @@ void GLCanvas3D::_render_separator_toolbar_left() const
|
||||
|
||||
void GLCanvas3D::_render_collapse_toolbar() const
|
||||
{
|
||||
if (!m_collapse_toolbar_enabled)
|
||||
return;
|
||||
|
||||
auto& plater = *wxGetApp().plater();
|
||||
const auto sidebar_docking_dir = plater.get_sidebar_docking_state();
|
||||
if (sidebar_docking_dir == Sidebar::None) {
|
||||
|
||||
@@ -57,6 +57,9 @@ namespace GUI {
|
||||
|
||||
class Bed3D;
|
||||
class PartPlateList;
|
||||
#ifdef SLIC3R_CAD
|
||||
class DesignSketchTool; // Design tab: interactive 2D sketch tool
|
||||
#endif
|
||||
|
||||
#if ENABLE_RETINA_GL
|
||||
class RetinaHelper;
|
||||
@@ -542,6 +545,27 @@ private:
|
||||
mutable Vec2i32 m_canvas_toolbar_pos = {140, 5};
|
||||
mutable float m_sc{1};
|
||||
mutable float m_paint_toolbar_width;
|
||||
bool m_collapse_toolbar_enabled{true};
|
||||
bool m_plate_chrome_enabled{true};
|
||||
// Design tab: render the world-axis triad at the bed centre (= modeling origin) instead of
|
||||
// the bed corner. Default false preserves the main editor's corner triad.
|
||||
bool m_axes_at_bed_center{false};
|
||||
// Design tab: draw the printer bed and its plate grid at all. Default true, so the
|
||||
// main editor is untouched; the Design tab lets the user hide it to model without a bed.
|
||||
bool m_show_bed{true};
|
||||
// Design tab: CAD grid drawn on the bed plane in place of the plate's corner-origin grid.
|
||||
// Two GLModels (10 mm minor / 50 mm major) generated from the bed centre so a line passes
|
||||
// exactly through the modeling origin; built once and rebuilt only when the bed shape changes.
|
||||
GLModel m_cad_grid_minor;
|
||||
GLModel m_cad_grid_major;
|
||||
// Geometry the CAD grid models were last built from, so they are rebuilt on bed-shape change
|
||||
// rather than every frame.
|
||||
BoundingBoxf m_cad_grid_bb;
|
||||
Vec2d m_cad_grid_center;
|
||||
bool m_cad_grid_valid{false};
|
||||
#ifdef SLIC3R_CAD
|
||||
DesignSketchTool* m_design_sketch_tool{nullptr};
|
||||
#endif
|
||||
|
||||
//BBS: add canvas type for assemble view usage
|
||||
ECanvasType m_canvas_type;
|
||||
@@ -569,6 +593,10 @@ private:
|
||||
std::array<unsigned int, 2> m_old_size{ 0, 0 };
|
||||
|
||||
bool m_is_touchpad_navigation{ false };
|
||||
// CAD navigation (Design tab only): left-drag is a selection rubber band, so orbit moves
|
||||
// to middle-drag and pan to right-drag — the Onshape/SolidWorks mapping. Off everywhere
|
||||
// else, so Prepare/Preview keep the mouse the user already learned.
|
||||
bool m_cad_navigation{ false };
|
||||
|
||||
// Screen is only refreshed from the OnIdle handler if it is dirty.
|
||||
bool m_dirty;
|
||||
@@ -882,6 +910,15 @@ public:
|
||||
void enable_assemble_view_toolbar(bool enable);
|
||||
void enable_return_toolbar(bool enable);
|
||||
void enable_separator_toolbar(bool enable);
|
||||
void enable_collapse_toolbar(bool enable);
|
||||
void enable_plate_chrome(bool enable);
|
||||
void set_axes_at_bed_center(bool b) { m_axes_at_bed_center = b; }
|
||||
void set_show_bed(bool b) { m_show_bed = b; }
|
||||
bool get_show_bed() const { return m_show_bed; }
|
||||
#ifdef SLIC3R_CAD
|
||||
void set_design_sketch_tool(DesignSketchTool* tool) { m_design_sketch_tool = tool; }
|
||||
DesignSketchTool* get_design_sketch_tool() const { return m_design_sketch_tool; }
|
||||
#endif
|
||||
void enable_dynamic_background(bool enable) { m_dynamic_background_enabled = enable; }
|
||||
void enable_labels(bool enable) { m_labels.enable(enable); }
|
||||
void enable_slope(bool enable) { m_slope.enable(enable); }
|
||||
@@ -1051,6 +1088,7 @@ public:
|
||||
bool clicked_button_matches_action(const wxMouseEvent& evt, MouseAction action, const std::map<MouseButton, MouseAction>& mappings) const;
|
||||
bool is_camera_rotate(const wxMouseEvent& evt, const std::map<MouseButton, MouseAction>& mappings) const;
|
||||
bool is_camera_pan(const wxMouseEvent& evt, const std::map<MouseButton, MouseAction>& mappings) const;
|
||||
void set_cad_navigation(bool b) { m_cad_navigation = b; }
|
||||
|
||||
Size get_canvas_size() const;
|
||||
Vec2d get_local_mouse_position() const;
|
||||
@@ -1190,6 +1228,8 @@ public:
|
||||
|
||||
bool can_sequential_clearance_show_in_gizmo();
|
||||
void update_sequential_clearance();
|
||||
// Orca: by-layer counterpart, for a prime tower compacted by "No sparse layers".
|
||||
void update_compacted_wipe_tower_clearance();
|
||||
|
||||
const Print* fff_print() const;
|
||||
const SLAPrint* sla_print() const;
|
||||
@@ -1252,6 +1292,10 @@ private:
|
||||
void _render_shadows(const Transform3d& view_matrix, const Transform3d& projection_matrix);
|
||||
//BBS: add part plate related logic
|
||||
void _render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true);
|
||||
// Design tab: draw the CAD grid (minor 10 mm + major 50 mm) in place of the plate's
|
||||
// corner-origin grid when the axes sit at the bed centre (modeling origin). Rebuilds its
|
||||
// GLModels lazily, only when the bed shape changed.
|
||||
void _render_cad_grid(const Transform3d& view_matrix, const Transform3d& projection_matrix);
|
||||
//BBS: add outline drawing logic
|
||||
void _render_objects(GLVolumeCollection::ERenderType type, bool with_outline = true);
|
||||
void _render_wireframe_overlay();
|
||||
|
||||
@@ -350,6 +350,11 @@ public:
|
||||
int OnExit() override;
|
||||
bool initialized() const { return m_initialized; }
|
||||
inline bool is_enable_multi_machine() { return this->app_config&& this->app_config->get("enable_multi_machine") == "true"; }
|
||||
#ifdef SLIC3R_CAD
|
||||
inline bool is_enable_cad_feature() { return this->app_config && this->app_config->get_bool("enable_cad_feature"); }
|
||||
inline bool is_auto_close_sketch_loops() { return !this->app_config
|
||||
|| this->app_config->get_bool("auto_close_sketch_loops"); }
|
||||
#endif
|
||||
|
||||
std::map<std::string, bool> test_url_state;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp"
|
||||
|
||||
#include "libslic3r/Geometry/ConvexHull.hpp"
|
||||
#include "libslic3r/LayOnFace.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
|
||||
#include <numeric>
|
||||
@@ -45,10 +45,10 @@ void GLGizmoFlatten::data_changed(bool is_serializing)
|
||||
const ModelObject *model_object = nullptr;
|
||||
int instance_id = -1;
|
||||
if (selection.is_single_full_instance() ||
|
||||
selection.is_from_single_object() ) {
|
||||
selection.is_from_single_object() ) {
|
||||
model_object = selection.get_model()->objects[selection.get_object_idx()];
|
||||
instance_id = selection.get_instance_idx();
|
||||
}
|
||||
}
|
||||
set_flattening_data(model_object, instance_id);
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ void GLGizmoFlatten::on_render()
|
||||
GLShaderProgram* shader = wxGetApp().get_shader("flat");
|
||||
if (shader == nullptr)
|
||||
return;
|
||||
|
||||
|
||||
shader->start_using();
|
||||
glsafe(::glClear(GL_DEPTH_BUFFER_BIT));
|
||||
|
||||
@@ -152,134 +152,18 @@ void GLGizmoFlatten::set_flattening_data(const ModelObject* model_object, int in
|
||||
void GLGizmoFlatten::update_planes()
|
||||
{
|
||||
const ModelObject* mo = m_c->selection_info()->model_object();
|
||||
TriangleMesh ch;
|
||||
for (const ModelVolume* vol : mo->volumes) {
|
||||
if (vol->type() != ModelVolumeType::MODEL_PART)
|
||||
continue;
|
||||
TriangleMesh vol_ch = vol->get_convex_hull();
|
||||
vol_ch.transform(vol->get_matrix());
|
||||
ch.merge(vol_ch);
|
||||
}
|
||||
ch = ch.convex_hull_3d();
|
||||
const Transform3d &inst_matrix = mo->instances.front()->get_matrix_no_offset();
|
||||
// The candidate faces are shared with the CLI --ground-* options, the rest only prepares them for rendering.
|
||||
std::vector<LayOnFacePlane> planes = lay_on_face_planes(*mo, inst_matrix);
|
||||
m_planes.clear();
|
||||
on_unregister_raycasters_for_picking();
|
||||
const Transform3d &inst_matrix = mo->instances.front()->get_matrix_no_offset();
|
||||
|
||||
// Following constants are used for discarding too small polygons.
|
||||
const float minimal_area = 5.f; // in square mm (world coordinates)
|
||||
const float minimal_side = 1.f; // mm
|
||||
const float minimal_angle = 1.f; // degree, initial value was 10, but cause bugs
|
||||
// We only keep the 254 largest planes (because of the picking pass limitations):
|
||||
planes.resize(std::min((int)planes.size(), 254));
|
||||
|
||||
// Now we'll go through all the facets and append Points of facets sharing the same normal.
|
||||
// This part is still performed in mesh coordinate system.
|
||||
const int num_of_facets = ch.facets_count();
|
||||
const std::vector<Vec3f> face_normals = its_face_normals(ch.its);
|
||||
const std::vector<Vec3i32> face_neighbors = its_face_neighbors(ch.its);
|
||||
std::vector<int> facet_queue(num_of_facets, 0);
|
||||
std::vector<bool> facet_visited(num_of_facets, false);
|
||||
int facet_queue_cnt = 0;
|
||||
const stl_normal* normal_ptr = nullptr;
|
||||
int facet_idx = 0;
|
||||
while (1) {
|
||||
// Find next unvisited triangle:
|
||||
for (; facet_idx < num_of_facets; ++ facet_idx)
|
||||
if (!facet_visited[facet_idx]) {
|
||||
facet_queue[facet_queue_cnt ++] = facet_idx;
|
||||
facet_visited[facet_idx] = true;
|
||||
normal_ptr = &face_normals[facet_idx];
|
||||
m_planes.emplace_back();
|
||||
break;
|
||||
}
|
||||
if (facet_idx == num_of_facets)
|
||||
break; // Everything was visited already
|
||||
|
||||
while (facet_queue_cnt > 0) {
|
||||
int facet_idx = facet_queue[-- facet_queue_cnt];
|
||||
const stl_normal& this_normal = face_normals[facet_idx];
|
||||
if (std::abs(this_normal(0) - (*normal_ptr)(0)) < 0.001 && std::abs(this_normal(1) - (*normal_ptr)(1)) < 0.001 && std::abs(this_normal(2) - (*normal_ptr)(2)) < 0.001) {
|
||||
const Vec3i32 face = ch.its.indices[facet_idx];
|
||||
for (int j=0; j<3; ++j)
|
||||
m_planes.back().vertices.emplace_back(ch.its.vertices[face[j]].cast<double>());
|
||||
|
||||
facet_visited[facet_idx] = true;
|
||||
for (int j = 0; j < 3; ++ j)
|
||||
if (int neighbor_idx = face_neighbors[facet_idx][j]; neighbor_idx >= 0 && ! facet_visited[neighbor_idx])
|
||||
facet_queue[facet_queue_cnt ++] = neighbor_idx;
|
||||
}
|
||||
}
|
||||
m_planes.back().normal = normal_ptr->cast<double>();
|
||||
|
||||
Pointf3s& verts = m_planes.back().vertices;
|
||||
// Now we'll transform all the points into world coordinates, so that the areas, angles and distances
|
||||
// make real sense.
|
||||
verts = transform(verts, inst_matrix);
|
||||
|
||||
// if this is a just a very small triangle, remove it to speed up further calculations (it would be rejected later anyway):
|
||||
if (verts.size() == 3 &&
|
||||
((verts[0] - verts[1]).norm() < minimal_side
|
||||
|| (verts[0] - verts[2]).norm() < minimal_side
|
||||
|| (verts[1] - verts[2]).norm() < minimal_side))
|
||||
m_planes.pop_back();
|
||||
}
|
||||
|
||||
// Let's prepare transformation of the normal vector from mesh to instance coordinates.
|
||||
const Matrix3d normal_matrix = inst_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
|
||||
|
||||
// Now we'll go through all the polygons, transform the points into xy plane to process them:
|
||||
for (unsigned int polygon_id=0; polygon_id < m_planes.size(); ++polygon_id) {
|
||||
Pointf3s& polygon = m_planes[polygon_id].vertices;
|
||||
const Vec3d& normal = m_planes[polygon_id].normal;
|
||||
|
||||
// transform the normal according to the instance matrix:
|
||||
const Vec3d normal_transformed = normal_matrix * normal;
|
||||
|
||||
// We are going to rotate about z and y to flatten the plane
|
||||
Eigen::Quaterniond q;
|
||||
Transform3d m = Transform3d::Identity();
|
||||
m.matrix().block(0, 0, 3, 3) = q.setFromTwoVectors(normal_transformed, Vec3d::UnitZ()).toRotationMatrix();
|
||||
polygon = transform(polygon, m);
|
||||
|
||||
// Now to remove the inner points. We'll misuse Geometry::convex_hull for that, but since
|
||||
// it works in fixed point representation, we will rescale the polygon to avoid overflows.
|
||||
// And yes, it is a nasty thing to do. Whoever has time is free to refactor.
|
||||
Vec3d bb_size = BoundingBoxf3(polygon).size();
|
||||
float sf = std::min(1./bb_size(0), 1./bb_size(1));
|
||||
Transform3d tr = Geometry::scale_transform({ sf, sf, 1.f });
|
||||
polygon = transform(polygon, tr);
|
||||
polygon = Slic3r::Geometry::convex_hull(polygon);
|
||||
polygon = transform(polygon, tr.inverse());
|
||||
|
||||
// Calculate area of the polygons and discard ones that are too small
|
||||
float& area = m_planes[polygon_id].area;
|
||||
area = 0.f;
|
||||
for (unsigned int i = 0; i < polygon.size(); i++) // Shoelace formula
|
||||
area += polygon[i](0)*polygon[i + 1 < polygon.size() ? i + 1 : 0](1) - polygon[i + 1 < polygon.size() ? i + 1 : 0](0)*polygon[i](1);
|
||||
area = 0.5f * std::abs(area);
|
||||
|
||||
bool discard = false;
|
||||
if (area < minimal_area)
|
||||
discard = true;
|
||||
else {
|
||||
// We also check the inner angles and discard polygons with angles smaller than the following threshold
|
||||
const double angle_threshold = ::cos(minimal_angle * (double)PI / 180.0);
|
||||
|
||||
for (unsigned int i = 0; i < polygon.size(); ++i) {
|
||||
const Vec3d& prec = polygon[(i == 0) ? polygon.size() - 1 : i - 1];
|
||||
const Vec3d& curr = polygon[i];
|
||||
const Vec3d& next = polygon[(i == polygon.size() - 1) ? 0 : i + 1];
|
||||
|
||||
if ((prec - curr).normalized().dot((next - curr).normalized()) > angle_threshold) {
|
||||
discard = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (discard) {
|
||||
m_planes[polygon_id--] = std::move(m_planes.back());
|
||||
m_planes.pop_back();
|
||||
continue;
|
||||
}
|
||||
for (LayOnFacePlane& plane : planes) {
|
||||
// The outline is convex and lies in the plane frame, where the plane is horizontal.
|
||||
Pointf3s& polygon = plane.outline;
|
||||
|
||||
// We will shrink the polygon a little bit so it does not touch the object edges:
|
||||
Vec3d centroid = std::accumulate(polygon.begin(), polygon.end(), Vec3d(0.0, 0.0, 0.0));
|
||||
@@ -332,13 +216,12 @@ void GLGizmoFlatten::update_planes()
|
||||
b(2) += 0.1f;
|
||||
|
||||
// Transform back to 3D (and also back to mesh coordinates)
|
||||
polygon = transform(polygon, inst_matrix.inverse() * m.inverse());
|
||||
m_planes.emplace_back();
|
||||
m_planes.back().normal = plane.normal;
|
||||
m_planes.back().area = plane.area;
|
||||
m_planes.back().vertices = transform(polygon, inst_matrix.inverse() * plane.to_plane_frame.inverse());
|
||||
}
|
||||
|
||||
// We'll sort the planes by area and only keep the 254 largest ones (because of the picking pass limitations):
|
||||
std::sort(m_planes.rbegin(), m_planes.rend(), [](const PlaneData& a, const PlaneData& b) { return a.area < b.area; });
|
||||
m_planes.resize(std::min((int)m_planes.size(), 254));
|
||||
|
||||
// Planes are finished - let's save what we calculated it from:
|
||||
m_volumes_matrices.clear();
|
||||
m_volumes_types.clear();
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
#include "GLGizmoPrimitive.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/ImGuiWrapper.hpp"
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/GUI_ObjectList.hpp"
|
||||
#include "slic3r/GUI/NotificationManager.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
|
||||
#ifndef IMGUI_DEFINE_MATH_OPERATORS
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#endif
|
||||
#include <imgui/imgui_internal.h>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
GLGizmoPrimitive::GLGizmoPrimitive(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id)
|
||||
: GLGizmoBase(parent, icon_filename, sprite_id) {}
|
||||
|
||||
bool GLGizmoPrimitive::on_init() { return true; }
|
||||
std::string GLGizmoPrimitive::on_get_name() const { return _u8L("Primitive"); }
|
||||
bool GLGizmoPrimitive::on_is_activable() const { return true; }
|
||||
void GLGizmoPrimitive::on_render() {}
|
||||
void GLGizmoPrimitive::on_set_state()
|
||||
{ if (m_state == EState::On) { m_params = PrimitiveParams{}; m_preview_dirty = true; } }
|
||||
|
||||
bool GLGizmoPrimitive::on_mouse(const wxMouseEvent&) { return false; }
|
||||
|
||||
CommonGizmosDataID GLGizmoPrimitive::on_get_requirements() const
|
||||
{ return CommonGizmosDataID(int(CommonGizmosDataID::SelectionInfo) | int(CommonGizmosDataID::InstancesHider)); }
|
||||
|
||||
void GLGizmoPrimitive::on_load(cereal::BinaryInputArchive& ar)
|
||||
{ ar(m_params); m_preview_dirty = true; }
|
||||
void GLGizmoPrimitive::on_save(cereal::BinaryOutputArchive& ar) const
|
||||
{ ar(m_params); }
|
||||
|
||||
void GLGizmoPrimitive::apply_preset(const char*, double w, double h, double d)
|
||||
{
|
||||
m_params.type = PrimitiveType::Box;
|
||||
m_params.box_w = w; m_params.box_h = h; m_params.box_d = d;
|
||||
m_preview_dirty = true;
|
||||
}
|
||||
|
||||
static void gen_mesh_and_add(PrimitiveParams& p, const char* snap_name)
|
||||
{
|
||||
TopoDS_Solid solid = GeometryEngine::make_primitive(p);
|
||||
TopoDS_Shape shape = solid;
|
||||
if (p.dressup_enabled) {
|
||||
if (p.dressup_type == DressUpType::Fillet)
|
||||
shape = GeometryEngine::apply_fillet(shape, p.dressup_radius, p.dressup_faces);
|
||||
else
|
||||
shape = GeometryEngine::apply_chamfer(shape, p.dressup_chamfer_dist, p.dressup_faces);
|
||||
}
|
||||
TriangleMesh mesh = GeometryEngine::tessellate(shape, p.linear_deflection, p.angular_deflection);
|
||||
if (mesh.its.indices.empty()) {
|
||||
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::WarningNotificationLevel, _u8L("Empty mesh generated"));
|
||||
return;
|
||||
}
|
||||
wxGetApp().plater()->take_snapshot(snap_name);
|
||||
ModelObject* mo = wxGetApp().model().add_object();
|
||||
std::string name = GeometryEngine::primitive_name(p.type);
|
||||
if (p.dressup_enabled && p.dressup_type == DressUpType::Fillet) name += " (Fillet)";
|
||||
else if (p.dressup_enabled) name += " (Chamfer)";
|
||||
mo->name = name;
|
||||
mo->add_volume(std::move(mesh))->set_new_unique_id();
|
||||
mo->ensure_on_bed();
|
||||
wxGetApp().plater()->update();
|
||||
}
|
||||
|
||||
void GLGizmoPrimitive::apply_primitive() { gen_mesh_and_add(m_params, "Add Primitive"); }
|
||||
|
||||
void GLGizmoPrimitive::on_render_input_window(float x, float y, float bottom_limit)
|
||||
{
|
||||
y = std::min(y, bottom_limit - ImGui::GetWindowHeight());
|
||||
const float scale = m_parent.get_scale();
|
||||
ImGuiWrapper::push_toolbar_style(scale);
|
||||
GizmoImguiSetNextWIndowPos(x, y, ImGuiCond_Always, 0.0f, 0.0f);
|
||||
GizmoImguiBegin("Primitive", ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove
|
||||
| ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse
|
||||
| ImGuiWindowFlags_NoTitleBar);
|
||||
|
||||
if (ImGui::CollapsingHeader("Shape", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
static const char* names[] = {"Box", "Cylinder", "Sphere", "Cone", "Torus"};
|
||||
int cur = (int)m_params.type;
|
||||
if (ImGui::Combo("##type", &cur, names, (int)PrimitiveType::COUNT)) {
|
||||
m_params.type = (PrimitiveType)cur;
|
||||
m_preview_dirty = true;
|
||||
}
|
||||
ImGui::Text("Quick:");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("10mm")) apply_preset("10mm cube", 10, 10, 10);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("20mm")) apply_preset("20mm cube", 20, 20, 20);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("50mm")) apply_preset("50mm cube", 50, 50, 50);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::CollapsingHeader("Dimensions", ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
auto dim = [&](const char* label, double& val, double step=0.5, double fast=5.0) {
|
||||
ImGui::SetNextItemWidth(130);
|
||||
if (ImGui::InputDouble(label, &val, step, fast, "%.1f mm")) m_preview_dirty = true;
|
||||
if (val < 0.5) val = 0.5;
|
||||
};
|
||||
switch (m_params.type) {
|
||||
case PrimitiveType::Box:
|
||||
dim("Width (X)", m_params.box_w);
|
||||
dim("Depth (Y)", m_params.box_d);
|
||||
dim("Height (Z)", m_params.box_h);
|
||||
break;
|
||||
case PrimitiveType::Cylinder:
|
||||
dim("Radius", m_params.cyl_radius);
|
||||
dim("Height", m_params.cyl_height);
|
||||
break;
|
||||
case PrimitiveType::Sphere:
|
||||
dim("Radius", m_params.sph_radius);
|
||||
break;
|
||||
case PrimitiveType::Cone:
|
||||
dim("Bottom R", m_params.cone_r1);
|
||||
dim("Top R", m_params.cone_r2);
|
||||
dim("Height", m_params.cone_height);
|
||||
break;
|
||||
case PrimitiveType::Torus:
|
||||
dim("Major R", m_params.torus_r1);
|
||||
dim("Minor R", m_params.torus_r2, 0.1, 1.0);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::CollapsingHeader("Fillet / Chamfer")) {
|
||||
ImGui::Checkbox("Enable", &m_params.dressup_enabled);
|
||||
if (m_params.dressup_enabled) {
|
||||
static const char* dn[] = {"Fillet", "Chamfer"};
|
||||
int du = (int)m_params.dressup_type;
|
||||
ImGui::SetNextItemWidth(100);
|
||||
if (ImGui::Combo("##dtype", &du, dn, 2)) { m_params.dressup_type = (DressUpType)du; m_preview_dirty = true; }
|
||||
static const char* fn[] = {"All edges", "Top edges", "Bottom edges", "Lateral edges"};
|
||||
int fg = (int)m_params.dressup_faces;
|
||||
ImGui::SetNextItemWidth(140);
|
||||
if (ImGui::Combo("Edges", &fg, fn, 4)) { m_params.dressup_faces = (FaceGroup)fg; m_preview_dirty = true; }
|
||||
if (m_params.dressup_type == DressUpType::Fillet) {
|
||||
ImGui::SetNextItemWidth(100);
|
||||
if (ImGui::InputDouble("Radius", &m_params.dressup_radius, 0.1, 1.0, "%.1f mm")) {
|
||||
if (m_params.dressup_radius < 0.1) m_params.dressup_radius = 0.1;
|
||||
m_preview_dirty = true;
|
||||
}
|
||||
} else {
|
||||
ImGui::SetNextItemWidth(100);
|
||||
if (ImGui::InputDouble("Distance", &m_params.dressup_chamfer_dist, 0.1, 1.0, "%.1f mm")) {
|
||||
if (m_params.dressup_chamfer_dist < 0.1) m_params.dressup_chamfer_dist = 0.1;
|
||||
m_preview_dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::CollapsingHeader("Quality")) {
|
||||
ImGui::SetNextItemWidth(130);
|
||||
if (ImGui::InputDouble("Mesh resolution", &m_params.linear_deflection, 0.001, 0.1, "%.3f mm")) {
|
||||
if (m_params.linear_deflection < 0.001) m_params.linear_deflection = 0.001;
|
||||
if (m_params.linear_deflection > 1.0) m_params.linear_deflection = 1.0;
|
||||
m_preview_dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::Button("Add Shape", {-1, 28}))
|
||||
apply_primitive();
|
||||
|
||||
if (ImGui::Button("Close", {-1, 0}))
|
||||
m_parent.reset_all_gizmos();
|
||||
|
||||
GizmoImguiEnd();
|
||||
ImGuiWrapper::pop_toolbar_style();
|
||||
}
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,43 @@
|
||||
#ifndef slic3r_GLGizmoPrimitive_hpp_
|
||||
#define slic3r_GLGizmoPrimitive_hpp_
|
||||
|
||||
#include "GLGizmoBase.hpp"
|
||||
#include "GLGizmosCommon.hpp"
|
||||
#include "libslic3r/CAD/GeometryEngine.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
class GLGizmoPrimitive : public GLGizmoBase
|
||||
{
|
||||
public:
|
||||
GLGizmoPrimitive(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id);
|
||||
~GLGizmoPrimitive() = default;
|
||||
|
||||
bool on_mouse(const wxMouseEvent& mouse_event) override;
|
||||
|
||||
protected:
|
||||
bool on_init() override;
|
||||
std::string on_get_name() const override;
|
||||
bool on_is_activable() const override;
|
||||
void on_render() override;
|
||||
void on_set_state() override;
|
||||
CommonGizmosDataID on_get_requirements() const override;
|
||||
void on_render_input_window(float x, float y, float bottom_limit) override;
|
||||
|
||||
void on_load(cereal::BinaryInputArchive& ar) override;
|
||||
void on_save(cereal::BinaryOutputArchive& ar) const override;
|
||||
|
||||
private:
|
||||
void apply_primitive();
|
||||
void apply_preset(const char* name, double w, double h, double d);
|
||||
|
||||
PrimitiveParams m_params;
|
||||
TriangleMesh m_preview_mesh;
|
||||
bool m_preview_dirty{true};
|
||||
};
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_GLGizmoPrimitive_hpp_
|
||||
@@ -0,0 +1,459 @@
|
||||
#include "GLGizmoSketch.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/ImGuiWrapper.hpp"
|
||||
#include "slic3r/GUI/GUI.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/GUI_ObjectList.hpp"
|
||||
#include "slic3r/GUI/NotificationManager.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include <BRepBuilderAPI_MakeFace.hxx>
|
||||
#include <BRepPrimAPI_MakeRevol.hxx>
|
||||
#include <BRepAlgoAPI_Fuse.hxx>
|
||||
|
||||
#ifndef IMGUI_DEFINE_MATH_OPERATORS
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#endif
|
||||
#include <imgui/imgui_internal.h>
|
||||
|
||||
#define L(s) Slic3r::GUI::I18N::translate((s)).c_str()
|
||||
#define UL(s) Slic3r::GUI::I18N::translate_utf8((s)).c_str()
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
GLGizmoSketch::GLGizmoSketch(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id)
|
||||
: GLGizmoBase(parent, icon_filename, sprite_id) {}
|
||||
|
||||
bool GLGizmoSketch::on_init() { return true; }
|
||||
std::string GLGizmoSketch::on_get_name() const { return _u8L("Sketch"); }
|
||||
bool GLGizmoSketch::on_is_activable() const { return true; }
|
||||
void GLGizmoSketch::on_render() {}
|
||||
void GLGizmoSketch::on_set_state() { if (m_state == EState::On) clear_all(); }
|
||||
bool GLGizmoSketch::on_mouse(const wxMouseEvent&) { return false; }
|
||||
|
||||
CommonGizmosDataID GLGizmoSketch::on_get_requirements() const
|
||||
{ return CommonGizmosDataID(int(CommonGizmosDataID::SelectionInfo)); }
|
||||
|
||||
void GLGizmoSketch::on_load(cereal::BinaryInputArchive& ar)
|
||||
{
|
||||
ar(m_tool, m_profiles, m_plane, m_sp, m_rect_w, m_rect_h, m_circle_r, m_poly_sides, m_poly_r, m_snap_grid, m_grid_step);
|
||||
m_active_profile = -1;
|
||||
}
|
||||
|
||||
void GLGizmoSketch::on_save(cereal::BinaryOutputArchive& ar) const
|
||||
{
|
||||
ar(m_tool, m_profiles, m_plane, m_sp, m_rect_w, m_rect_h, m_circle_r, m_poly_sides, m_poly_r, m_snap_grid, m_grid_step);
|
||||
}
|
||||
|
||||
SketchProfile& GLGizmoSketch::active_profile()
|
||||
{
|
||||
if (m_active_profile < 0 || m_active_profile >= (int)m_profiles.size()) {
|
||||
m_profiles.emplace_back();
|
||||
m_active_profile = (int)m_profiles.size() - 1;
|
||||
}
|
||||
return m_profiles[m_active_profile];
|
||||
}
|
||||
|
||||
bool GLGizmoSketch::has_closed_profile() const
|
||||
{
|
||||
for (auto& p : m_profiles) if (p.closed && p.points.size() >= 3) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void GLGizmoSketch::clear_all()
|
||||
{
|
||||
m_profiles.clear();
|
||||
m_canvas_points.clear();
|
||||
m_active_profile = -1;
|
||||
}
|
||||
|
||||
void GLGizmoSketch::add_closed_profile()
|
||||
{
|
||||
auto& ap = active_profile();
|
||||
if (ap.points.size() >= 3) {
|
||||
ap.closed = true;
|
||||
m_active_profile = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void GLGizmoSketch::delete_profile(int idx)
|
||||
{
|
||||
if (idx >= 0 && idx < (int)m_profiles.size()) {
|
||||
m_profiles.erase(m_profiles.begin() + idx);
|
||||
if (m_active_profile >= (int)m_profiles.size()) m_active_profile = -1;
|
||||
}
|
||||
}
|
||||
|
||||
Vec2d GLGizmoSketch::snap(Vec2d pt) const
|
||||
{
|
||||
if (!m_snap_grid) return pt;
|
||||
double gs = m_grid_step;
|
||||
return {round(pt.x() / gs) * gs, round(pt.y() / gs) * gs};
|
||||
}
|
||||
|
||||
void GLGizmoSketch::build_preset_profile()
|
||||
{
|
||||
auto& ap = active_profile();
|
||||
ap.clear();
|
||||
auto add = [&](double x, double y) { ap.points.emplace_back(x, y); };
|
||||
switch (m_tool) {
|
||||
case SketchTool::Rectangle:
|
||||
add(-m_rect_w/2, -m_rect_h/2); add( m_rect_w/2, -m_rect_h/2);
|
||||
add( m_rect_w/2, m_rect_h/2); add(-m_rect_w/2, m_rect_h/2);
|
||||
ap.closed = true; m_active_profile = -1; break;
|
||||
case SketchTool::Circle:
|
||||
for (int i = 0; i <= m_circle_seg; ++i) {
|
||||
double a = 2.0*M_PI*i/m_circle_seg;
|
||||
add(cos(a)*m_circle_r, sin(a)*m_circle_r);
|
||||
}
|
||||
ap.closed = true; m_active_profile = -1; break;
|
||||
case SketchTool::Polygon:
|
||||
for (int i = 0; i < m_poly_sides; ++i) {
|
||||
double a = 2.0*M_PI*i/m_poly_sides - M_PI/2;
|
||||
add(cos(a)*m_poly_r, sin(a)*m_poly_r);
|
||||
}
|
||||
ap.closed = true; m_active_profile = -1; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
void GLGizmoSketch::handle_canvas_click(ImVec2 pos)
|
||||
{
|
||||
Vec2d pt = snap({pos.x / m_canvas_scale, -pos.y / m_canvas_scale});
|
||||
if (m_tool == SketchTool::Line) {
|
||||
auto& ap = active_profile();
|
||||
if (ap.points.size() >= 3 && (pt - ap.points.front()).norm() < m_grid_step) {
|
||||
ap.points.push_back(ap.points.front());
|
||||
ap.closed = true;
|
||||
m_active_profile = -1;
|
||||
return;
|
||||
}
|
||||
ap.points.push_back(pt);
|
||||
}
|
||||
}
|
||||
|
||||
void GLGizmoSketch::draw_canvas()
|
||||
{
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
float w = 280, h = 200;
|
||||
ImVec2 end(pos.x+w, pos.y+h);
|
||||
float cx = pos.x+w/2, cy = pos.y+h/2;
|
||||
auto tc = [&](const ImVec2& p) { return ImVec2(cx+p.x*m_canvas_scale, cy-p.y*m_canvas_scale); };
|
||||
|
||||
dl->AddRectFilled(pos, end, IM_COL32(28,28,36,255));
|
||||
dl->AddRect(pos, end, IM_COL32(55,55,68,255));
|
||||
|
||||
float gs = m_grid_step;
|
||||
for (float g = 0; g < w; g += gs * m_canvas_scale) {
|
||||
ImU32 gc = (int(g/(gs*m_canvas_scale)) % 5 == 0) ? IM_COL32(60,60,75,100) : IM_COL32(45,45,55,60);
|
||||
dl->AddLine({pos.x+g,pos.y}, {pos.x+g,end.y}, gc);
|
||||
}
|
||||
for (float g = 0; g < h; g += gs * m_canvas_scale) {
|
||||
ImU32 gc = (int(g/(gs*m_canvas_scale)) % 5 == 0) ? IM_COL32(60,60,75,100) : IM_COL32(45,45,55,60);
|
||||
dl->AddLine({pos.x,pos.y+g}, {end.x,pos.y+g}, gc);
|
||||
}
|
||||
|
||||
dl->AddLine({cx,pos.y},{cx,end.y}, IM_COL32(70,70,85,180), 1.5f);
|
||||
dl->AddLine({pos.x,cy},{end.x,cy}, IM_COL32(70,70,85,180), 1.5f);
|
||||
dl->AddText({end.x-12, cy+2}, IM_COL32(120,120,140,200), "X");
|
||||
dl->AddText({cx+4, pos.y+2}, IM_COL32(120,120,140,200), "Y");
|
||||
|
||||
for (size_t pi = 0; pi < m_profiles.size(); ++pi) {
|
||||
auto& prof = m_profiles[pi];
|
||||
if (prof.points.size() < 2) continue;
|
||||
std::vector<ImVec2> sp;
|
||||
for (auto& p : prof.points) sp.push_back(tc({(float)p.x(), (float)p.y()}));
|
||||
if (prof.closed && sp.size() >= 3) {
|
||||
bool is_outer = (pi == 0);
|
||||
ImU32 fill = is_outer ? IM_COL32(0,180,90,35) : IM_COL32(180,60,60,35);
|
||||
ImU32 line = is_outer ? IM_COL32(0,220,100,255) : IM_COL32(220,80,80,255);
|
||||
dl->AddConvexPolyFilled(sp.data(), (int)sp.size(), fill);
|
||||
for (size_t i=0; i<sp.size(); ++i)
|
||||
dl->AddLine(sp[i], sp[(i+1)%sp.size()], line, (pi==0)?2.5f:2.0f);
|
||||
for (size_t i=0; i<sp.size()-1; ++i)
|
||||
dl->AddCircleFilled(sp[i], 3.0f, IM_COL32(255,255,255,255));
|
||||
}
|
||||
}
|
||||
|
||||
auto& ap = active_profile();
|
||||
if (!ap.closed && ap.points.size() >= 1) {
|
||||
std::vector<ImVec2> sp;
|
||||
for (auto& p : ap.points) sp.push_back(tc({(float)p.x(), (float)p.y()}));
|
||||
for (size_t i=1; i<sp.size(); ++i)
|
||||
dl->AddLine(sp[i-1], sp[i], IM_COL32(0,200,255,200), 2.0f);
|
||||
for (auto& s : sp) dl->AddCircleFilled(s, 3.5f, IM_COL32(100,200,255,255));
|
||||
ImVec2 mouse = ImGui::GetMousePos();
|
||||
if (mouse.x > pos.x && mouse.x < end.x && mouse.y > pos.y && mouse.y < end.y)
|
||||
dl->AddLine(sp.back(), mouse, IM_COL32(100,160,220,120), 1.5f);
|
||||
}
|
||||
|
||||
ImGui::InvisibleButton("canvas", ImVec2(w,h));
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImVec2 m = ImGui::GetMousePos();
|
||||
Vec2d sk({(m.x-cx)/m_canvas_scale, -(m.y-cy)/m_canvas_scale});
|
||||
if (m_snap_grid) sk = snap(sk);
|
||||
auto txt = wxString::Format("X:%.1f Y:%.1f", sk.x(), sk.y()).ToStdString();
|
||||
dl->AddText({pos.x+4, end.y-16}, IM_COL32(160,160,180,200), txt.c_str());
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left))
|
||||
handle_canvas_click({(m.x-cx)/m_canvas_scale, -(m.y-cy)/m_canvas_scale});
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
|
||||
auto& ap2 = active_profile();
|
||||
if (ap2.points.size() >= 3) {
|
||||
ap2.points.push_back(ap2.points.front());
|
||||
ap2.closed = true;
|
||||
m_active_profile = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TopoDS_Shape GLGizmoSketch::build_combined_shape()
|
||||
{
|
||||
if (m_profiles.empty() || !m_profiles[0].closed)
|
||||
throw std::runtime_error("No outer profile");
|
||||
|
||||
TopoDS_Wire outer_wire = m_profiles[0].to_occt_wire(m_plane);
|
||||
BRepBuilderAPI_MakeFace face_maker(outer_wire);
|
||||
if (!face_maker.IsDone()) throw std::runtime_error("Failed to make outer face");
|
||||
|
||||
for (size_t i = 1; i < m_profiles.size(); ++i) {
|
||||
if (!m_profiles[i].closed) continue;
|
||||
TopoDS_Wire inner = m_profiles[i].to_occt_wire(m_plane);
|
||||
face_maker.Add(inner);
|
||||
}
|
||||
face_maker.Build();
|
||||
if (!face_maker.IsDone()) throw std::runtime_error("Failed to build face with holes");
|
||||
|
||||
TopoDS_Face face = face_maker.Face();
|
||||
|
||||
TopoDS_Shape shape;
|
||||
if (m_sp.revolve_deg < 360.0 && m_sp.revolve_deg > 0.0) {
|
||||
gp_Pnt o(m_plane.origin.x(), m_plane.origin.y(), m_plane.origin.z());
|
||||
gp_Dir xd(m_plane.x_axis.x(), m_plane.x_axis.y(), m_plane.x_axis.z());
|
||||
gp_Ax1 axis(o, xd);
|
||||
BRepPrimAPI_MakeRevol rev(face, axis, m_sp.revolve_deg * M_PI / 180.0);
|
||||
if (!rev.IsDone()) throw std::runtime_error("Revolve failed");
|
||||
shape = rev.Shape();
|
||||
} else {
|
||||
shape = SketchEngine::make_extrude_face(face, m_plane, m_sp.extrude_len, m_sp.extrude_sym);
|
||||
}
|
||||
|
||||
if (m_sp.dressup_enabled) {
|
||||
if (m_sp.dressup_type == DressUpType::Fillet)
|
||||
shape = GeometryEngine::apply_fillet(shape, m_sp.dressup_radius, m_sp.dressup_faces);
|
||||
else
|
||||
shape = GeometryEngine::apply_chamfer(shape, m_sp.dressup_chamfer_dist, m_sp.dressup_faces);
|
||||
}
|
||||
return shape;
|
||||
}
|
||||
|
||||
void GLGizmoSketch::on_render_input_window(float x, float y, float bottom_limit)
|
||||
{
|
||||
y = std::min(y, bottom_limit - ImGui::GetWindowHeight());
|
||||
const float scale = m_parent.get_scale();
|
||||
ImGuiWrapper::push_toolbar_style(scale);
|
||||
GizmoImguiSetNextWIndowPos(x, y, ImGuiCond_Always, 0.0f, 0.0f);
|
||||
GizmoImguiBegin("Sketch", ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove
|
||||
| ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse
|
||||
| ImGuiWindowFlags_NoTitleBar);
|
||||
|
||||
if (ImGui::CollapsingHeader(UL("Profile"), ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
static const char* names[] = {"Line", "Rectangle", "Circle", "Polygon"};
|
||||
int cur = (int)m_tool;
|
||||
if (ImGui::Combo("##shape", &cur, names, (int)SketchTool::COUNT)) {
|
||||
m_tool = (SketchTool)cur;
|
||||
if (m_tool != SketchTool::Line) build_preset_profile();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (m_imgui->button("+##newprofile")) m_active_profile = -1;
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", UL("Start new profile (for holes)"));
|
||||
|
||||
if (m_tool == SketchTool::Rectangle) {
|
||||
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble("W", &m_rect_w,1,10,"%.0f")) build_preset_profile();
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble("H", &m_rect_h,1,10,"%.0f")) build_preset_profile();
|
||||
} else if (m_tool == SketchTool::Circle) {
|
||||
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble("R", &m_circle_r,1,5,"%.0f")) build_preset_profile();
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(80); if (ImGui::SliderInt("Seg", &m_circle_seg,8,64)) build_preset_profile();
|
||||
} else if (m_tool == SketchTool::Polygon) {
|
||||
ImGui::SetNextItemWidth(80); if (ImGui::SliderInt("Sides", &m_poly_sides,3,12)) build_preset_profile();
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble("R", &m_poly_r,1,5,"%.0f")) build_preset_profile();
|
||||
} else {
|
||||
ImGui::Text("%s", UL("Click on canvas to draw"));
|
||||
}
|
||||
|
||||
ImGui::Checkbox(UL("Snap to grid"), &m_snap_grid);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(80); ImGui::InputFloat("Step", &m_grid_step, 1, 5, "%.0f mm");
|
||||
|
||||
draw_canvas();
|
||||
|
||||
if (!m_profiles.empty()) {
|
||||
ImGui::Text("%s: %zu", UL("Profiles"), m_profiles.size());
|
||||
for (int i = 0; i < (int)m_profiles.size(); ++i) {
|
||||
auto& p = m_profiles[i];
|
||||
ImGui::PushID(i);
|
||||
bool outer = (i == 0);
|
||||
ImVec4 col = outer ? ImVec4(0,1,0,1) : ImVec4(1,0.3f,0.3f,1);
|
||||
const char* label = outer ? "Outer" : "Hole";
|
||||
ImGui::TextColored(col, "%s %d: %zu pts %s", label, i+1, p.points.size(), p.closed ? "CLOSED" : "");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("X")) delete_profile(i);
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
bool is_revolve = false;
|
||||
bool has_sel = false;
|
||||
|
||||
if (ImGui::CollapsingHeader(UL("Operation"), ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
static int pi = 0;
|
||||
if (ImGui::Combo(UL("Plane"), &pi, "XY (Top)\0XZ (Front)\0YZ (Side)\0"))
|
||||
m_plane = (pi==0) ? SketchPlane::XY() : (pi==1) ? SketchPlane::XZ() : SketchPlane::YZ();
|
||||
|
||||
is_revolve = (m_sp.revolve_deg > 0 && m_sp.revolve_deg < 360);
|
||||
ImGui::SetNextItemWidth(100);
|
||||
if (ImGui::InputDouble(UL("Revolve deg"), &m_sp.revolve_deg, 15, 90, "%.0f")) {
|
||||
if (m_sp.revolve_deg > 360) m_sp.revolve_deg = 360;
|
||||
if (m_sp.revolve_deg < 0) m_sp.revolve_deg = 0;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", UL("Set to 0 for extrude, >0 for revolve"));
|
||||
|
||||
if (!is_revolve) {
|
||||
ImGui::SetNextItemWidth(100);
|
||||
ImGui::InputDouble(UL("Length"), &m_sp.extrude_len, 0.5, 5, "%.1f mm");
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox(UL("Symmetric"), &m_sp.extrude_sym);
|
||||
}
|
||||
|
||||
has_sel = !m_parent.get_selection().is_empty();
|
||||
if (has_sel) {
|
||||
if (ImGui::Checkbox(UL("Pocket (cut)"), &m_sp.is_pocket))
|
||||
if (m_sp.is_pocket) m_sp.dressup_enabled = false;
|
||||
} else m_sp.is_pocket = false;
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (!m_sp.is_pocket && ImGui::CollapsingHeader(UL("Fillet / Chamfer"))) {
|
||||
ImGui::Checkbox(UL("Enable"), &m_sp.dressup_enabled);
|
||||
if (m_sp.dressup_enabled) {
|
||||
static const char* dn[] = {"Fillet", "Chamfer"};
|
||||
int du = (int)m_sp.dressup_type;
|
||||
ImGui::SetNextItemWidth(100);
|
||||
if (ImGui::Combo("##dtype", &du, dn, 2)) m_sp.dressup_type = (DressUpType)du;
|
||||
static const char* fn[] = {"All edges", "Top edges", "Bottom edges", "Lateral edges"};
|
||||
int fg = (int)m_sp.dressup_faces;
|
||||
ImGui::SetNextItemWidth(140);
|
||||
ImGui::Combo(UL("Edges"), &fg, fn, 4); m_sp.dressup_faces = (FaceGroup)fg;
|
||||
ImGui::SetNextItemWidth(100);
|
||||
if (m_sp.dressup_type == DressUpType::Fillet)
|
||||
ImGui::InputDouble(UL("Radius"), &m_sp.dressup_radius, 0.1, 1, "%.1f mm");
|
||||
else
|
||||
ImGui::InputDouble(UL("Distance"), &m_sp.dressup_chamfer_dist, 0.1, 1, "%.1f mm");
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
bool ok = has_closed_profile();
|
||||
if (ok) ImGui::TextColored({0,1,0,1}, "%zu %s", m_profiles.size(), UL("closed profile(s)"));
|
||||
else ImGui::TextColored({0.6f,0.6f,0.6f,1}, "%s", UL("Draw a closed profile to enable"));
|
||||
|
||||
auto btn = [&](const char* label, bool enabled) {
|
||||
if (!enabled) { ImGui::PushItemFlag(ImGuiItemFlags_Disabled,true); ImGui::PushStyleColor(ImGuiCol_Button,{0.25f,0.25f,0.25f,1}); }
|
||||
bool clicked = ImGui::Button(label, {-1,0});
|
||||
if (!enabled) { ImGui::PopStyleColor(); ImGui::PopItemFlag(); }
|
||||
return clicked && enabled;
|
||||
};
|
||||
|
||||
if (m_sp.is_pocket && has_sel) {
|
||||
if (btn(L("Pocket (Cut)"), ok)) apply_pocket();
|
||||
} else if (is_revolve) {
|
||||
if (btn(L("Revolve"), ok)) apply_revolve();
|
||||
} else {
|
||||
if (btn(L("Extrude"), ok)) apply_extrude();
|
||||
}
|
||||
|
||||
if (ImGui::Button(L("Clear All"), {-1,0})) clear_all();
|
||||
if (ImGui::Button(L("Close"), {-1,0})) m_parent.reset_all_gizmos();
|
||||
|
||||
GizmoImguiEnd();
|
||||
ImGuiWrapper::pop_toolbar_style();
|
||||
}
|
||||
|
||||
void GLGizmoSketch::apply_extrude()
|
||||
{
|
||||
try {
|
||||
TopoDS_Shape shape = build_combined_shape();
|
||||
TriangleMesh mesh = SketchEngine::tessellate(shape, m_sp.linear_deflection);
|
||||
if (mesh.its.indices.empty()) throw std::runtime_error("Empty result");
|
||||
wxGetApp().plater()->take_snapshot("Sketch Extrude");
|
||||
ModelObject* mo = wxGetApp().model().add_object();
|
||||
mo->name = "Extrusion";
|
||||
mo->add_volume(std::move(mesh))->set_new_unique_id();
|
||||
mo->ensure_on_bed();
|
||||
wxGetApp().plater()->update();
|
||||
clear_all();
|
||||
} catch (const std::exception& e) {
|
||||
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel, std::string("Extrude: ")+e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void GLGizmoSketch::apply_revolve()
|
||||
{
|
||||
try {
|
||||
TopoDS_Shape shape = build_combined_shape();
|
||||
TriangleMesh mesh = SketchEngine::tessellate(shape, m_sp.linear_deflection);
|
||||
if (mesh.its.indices.empty()) throw std::runtime_error("Empty result");
|
||||
wxGetApp().plater()->take_snapshot("Sketch Revolve");
|
||||
ModelObject* mo = wxGetApp().model().add_object();
|
||||
mo->name = "Revolve";
|
||||
mo->add_volume(std::move(mesh))->set_new_unique_id();
|
||||
mo->ensure_on_bed();
|
||||
wxGetApp().plater()->update();
|
||||
clear_all();
|
||||
} catch (const std::exception& e) {
|
||||
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel, std::string("Revolve: ")+e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void GLGizmoSketch::apply_pocket()
|
||||
{
|
||||
try {
|
||||
Selection& sel = m_parent.get_selection();
|
||||
int obj_idx = sel.get_object_idx();
|
||||
if (obj_idx < 0) throw std::runtime_error("No object selected");
|
||||
ModelObject* mo = wxGetApp().model().objects[obj_idx];
|
||||
|
||||
TopoDS_Wire outer = m_profiles[0].to_occt_wire(m_plane);
|
||||
BRepBuilderAPI_MakeFace fm(outer);
|
||||
if (!fm.IsDone()) throw std::runtime_error("Face failed");
|
||||
for (size_t i = 1; i < m_profiles.size(); ++i)
|
||||
if (m_profiles[i].closed) fm.Add(m_profiles[i].to_occt_wire(m_plane));
|
||||
fm.Build();
|
||||
if (!fm.IsDone()) throw std::runtime_error("Face with holes failed");
|
||||
|
||||
TopoDS_Shape tool = SketchEngine::make_extrude_face(fm.Face(), m_plane, m_sp.extrude_len + 5.0, false);
|
||||
TriangleMesh tool_mesh = SketchEngine::tessellate(tool, m_sp.linear_deflection);
|
||||
if (tool_mesh.its.indices.empty()) throw std::runtime_error("Tool mesh empty");
|
||||
|
||||
wxGetApp().plater()->take_snapshot("Sketch Pocket");
|
||||
mo->add_volume(std::move(tool_mesh), ModelVolumeType::NEGATIVE_VOLUME)->set_new_unique_id();
|
||||
mo->ensure_on_bed();
|
||||
wxGetApp().plater()->update();
|
||||
clear_all();
|
||||
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::RegularNotificationLevel, UL("Pocket added (negative volume)"));
|
||||
} catch (const std::exception& e) {
|
||||
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel, std::string("Pocket: ")+e.what());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,74 @@
|
||||
#ifndef slic3r_GLGizmoSketch_hpp_
|
||||
#define slic3r_GLGizmoSketch_hpp_
|
||||
|
||||
#include "GLGizmoBase.hpp"
|
||||
#include "GLGizmosCommon.hpp"
|
||||
#include "libslic3r/CAD/SketchEngine.hpp"
|
||||
#include <imgui/imgui.h>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
enum class SketchTool { Line, Rectangle, Circle, Polygon, COUNT };
|
||||
|
||||
class GLGizmoSketch : public GLGizmoBase
|
||||
{
|
||||
public:
|
||||
GLGizmoSketch(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id);
|
||||
|
||||
bool on_mouse(const wxMouseEvent& mouse_event) override;
|
||||
|
||||
protected:
|
||||
bool on_init() override;
|
||||
std::string on_get_name() const override;
|
||||
bool on_is_activable() const override;
|
||||
void on_render() override;
|
||||
void on_set_state() override;
|
||||
CommonGizmosDataID on_get_requirements() const override;
|
||||
void on_render_input_window(float x, float y, float bottom_limit) override;
|
||||
|
||||
void on_load(cereal::BinaryInputArchive& ar) override;
|
||||
void on_save(cereal::BinaryOutputArchive& ar) const override;
|
||||
|
||||
private:
|
||||
SketchTool m_tool{SketchTool::Line};
|
||||
std::vector<SketchProfile> m_profiles; // multiple profiles (outer + holes)
|
||||
SketchPlane m_plane{SketchPlane::XY()};
|
||||
SketchParams m_sp;
|
||||
|
||||
// Shape presets
|
||||
double m_rect_w{20}, m_rect_h{15};
|
||||
double m_circle_r{10}; int m_circle_seg{32};
|
||||
int m_poly_sides{6}; double m_poly_r{10};
|
||||
|
||||
// Canvas
|
||||
std::vector<ImVec2> m_canvas_points;
|
||||
Vec2d m_canvas_center{0,0};
|
||||
float m_canvas_scale{5.0f};
|
||||
bool m_snap_grid{true};
|
||||
float m_grid_step{5.0f};
|
||||
|
||||
// Current profile being drawn
|
||||
int m_active_profile{-1};
|
||||
|
||||
SketchProfile& active_profile();
|
||||
bool has_closed_profile() const;
|
||||
|
||||
void build_preset_profile();
|
||||
void add_closed_profile();
|
||||
void delete_profile(int idx);
|
||||
void clear_all();
|
||||
|
||||
TopoDS_Shape build_combined_shape(); // all profiles as face with holes
|
||||
void apply_extrude();
|
||||
void apply_revolve();
|
||||
void apply_pocket();
|
||||
void draw_canvas();
|
||||
void handle_canvas_click(ImVec2 pos);
|
||||
Vec2d snap(Vec2d pt) const;
|
||||
};
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_GLGizmoSketch_hpp_
|
||||
@@ -27,6 +27,10 @@
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoSVG.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoAssembly.hpp"
|
||||
#ifdef SLIC3R_CAD
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoPrimitive.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoSketch.hpp"
|
||||
#endif
|
||||
|
||||
#include "libslic3r/format.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
@@ -176,6 +180,14 @@ void GLGizmosManager::switch_gizmos_icon_filename()
|
||||
case (EType::BrimEars):
|
||||
gizmo->set_icon_filename(m_is_dark ? "toolbar_brimears_dark.svg" : "toolbar_brimears.svg");
|
||||
break;
|
||||
#ifdef SLIC3R_CAD
|
||||
case (EType::Primitive):
|
||||
gizmo->set_icon_filename(m_is_dark ? "toolbar_modifier_cube_dark.svg" : "toolbar_modifier_cube.svg");
|
||||
break;
|
||||
case (EType::Sketch):
|
||||
gizmo->set_icon_filename(m_is_dark ? "toolbar_sketch_dark.svg" : "toolbar_sketch.svg");
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
@@ -219,6 +231,12 @@ bool GLGizmosManager::init()
|
||||
m_gizmos.emplace_back(new GLGizmoAssembly(m_parent, m_is_dark ? "toolbar_assembly_dark.svg" : "toolbar_assembly.svg", EType::Assembly));
|
||||
m_gizmos.emplace_back(new GLGizmoSimplify(m_parent, "reduce_triangles.svg", EType::Simplify));
|
||||
m_gizmos.emplace_back(new GLGizmoBrimEars(m_parent, m_is_dark ? "toolbar_brimears_dark.svg" : "toolbar_brimears.svg", EType::BrimEars));
|
||||
#ifdef SLIC3R_CAD
|
||||
// Registered last: Primitive and Sketch are the final entries before Undefined, so
|
||||
// omitting them leaves every preceding m_gizmos index (indexed by EType) untouched.
|
||||
m_gizmos.emplace_back(new GLGizmoPrimitive(m_parent, m_is_dark ? "toolbar_modifier_cube_dark.svg" : "toolbar_modifier_cube.svg", static_cast<unsigned int>(Primitive)));
|
||||
m_gizmos.emplace_back(new GLGizmoSketch(m_parent, m_is_dark ? "toolbar_sketch_dark.svg" : "toolbar_sketch.svg", static_cast<unsigned int>(Sketch)));
|
||||
#endif
|
||||
//m_gizmos.emplace_back(new GLGizmoSlaSupports(m_parent, "sla_supports.svg", sprite_id++));
|
||||
//m_gizmos.emplace_back(new GLGizmoFaceDetector(m_parent, "face recognition.svg", sprite_id++));
|
||||
//m_gizmos.emplace_back(new GLGizmoHollow(m_parent, "hollow.svg", sprite_id++));
|
||||
|
||||
@@ -90,6 +90,12 @@ public:
|
||||
Assembly,
|
||||
Simplify,
|
||||
BrimEars,
|
||||
#ifdef SLIC3R_CAD
|
||||
// Both need the CAD kernel (GeometryEngine); keep them last so that with
|
||||
// SLIC3R_CAD off the enum matches upstream's numbering exactly.
|
||||
Primitive,
|
||||
Sketch,
|
||||
#endif
|
||||
//SlaSupports,
|
||||
// BBS
|
||||
//FaceRecognition,
|
||||
|
||||
@@ -505,6 +505,23 @@ bool ImGuiWrapper::update_key_data(wxKeyEvent &evt)
|
||||
if (evt.GetEventType() == wxEVT_CHAR) {
|
||||
// Char event
|
||||
const auto key = evt.GetUnicodeKey();
|
||||
// THE MEASUREMENT THAT CANNOT LIE. This is the ONLY place in the application where ImGui
|
||||
// is ever handed a character, so an ImGui text field that stays empty while reporting
|
||||
// itself active has exactly two possible causes, and this line separates them: no output
|
||||
// at all means the wxEVT_CHAR never reached the GL canvas (a focus problem, upstream of
|
||||
// ImGui entirely), while output with unicode=0 means the character arrived empty and is
|
||||
// being dropped right here.
|
||||
//
|
||||
// It lives here rather than on the canvas because a probe bound on the canvas CANNOT
|
||||
// answer this: GLCanvas3D::on_char is bound later than any constructor-time probe, wx
|
||||
// runs handlers in reverse bind order, and on_char returns without Skip() whenever this
|
||||
// function returns true — so such a probe stays silent whether or not the key arrived.
|
||||
// A day was lost to reading that silence as evidence.
|
||||
if (std::getenv("ORCA_CAD_UXTRACE")) {
|
||||
fprintf(stderr, "[UX] imgui_char unicode=%d keycode=%d want_text=%d\n",
|
||||
(int) key, evt.GetKeyCode(), (int) io.WantTextInput);
|
||||
fflush(stderr);
|
||||
}
|
||||
if (key != 0) {
|
||||
io.AddInputCharacter(key);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
#include "I18N.hpp"
|
||||
#include "GLCanvas3D.hpp"
|
||||
#include "Plater.hpp"
|
||||
#ifdef SLIC3R_CAD
|
||||
#include "slic3r/GUI/CAD/DesignPanel.hpp"
|
||||
#include "slic3r/GUI/CAD/McpControl.hpp"
|
||||
#endif
|
||||
#include "WebViewDialog.hpp"
|
||||
#include "../Utils/Process.hpp"
|
||||
// BBS
|
||||
@@ -1063,7 +1067,15 @@ void MainFrame::update_layout()
|
||||
// Right after Home — or first, when there is no Home tab (PositionAfter() would
|
||||
// append instead, and by now the other built-in tabs are already in place).
|
||||
const int home_idx = m_tabpanel->FindPageByName(TAB_ID_HOME);
|
||||
const size_t prepare_pos = (home_idx == wxNOT_FOUND) ? 0 : static_cast<size_t>(home_idx) + 1;
|
||||
size_t prepare_pos = (home_idx == wxNOT_FOUND) ? 0 : static_cast<size_t>(home_idx) + 1;
|
||||
#ifdef SLIC3R_CAD
|
||||
// Design sits between Home and Prepare, so it goes in first and pushes Prepare along.
|
||||
// The page only exists when the experimental CAD feature is enabled.
|
||||
if (m_design_page != nullptr) {
|
||||
m_design_page->Reparent(m_tabpanel);
|
||||
m_tabpanel->InsertPage(prepare_pos++, TAB_ID_DESIGN, m_design_page, _L("Design"), "tab_design_active");
|
||||
}
|
||||
#endif
|
||||
m_tabpanel->InsertPage(prepare_pos, TAB_ID_PREPARE, m_plater, _L("Prepare"), "tab_3d_active");
|
||||
m_tabpanel->InsertPage(prepare_pos + 1, TAB_ID_PREVIEW, m_plater, _L("Preview"), "tab_preview_active");
|
||||
m_main_sizer->Add(m_tabpanel, 1, wxEXPAND | wxTOP, 0);
|
||||
@@ -1281,6 +1293,19 @@ void MainFrame::show_option(bool show)
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef SLIC3R_CAD
|
||||
DesignPanel* MainFrame::ensure_design_panel()
|
||||
{
|
||||
if (m_design_panel == nullptr && m_design_page != nullptr) {
|
||||
wxBusyCursor busy;
|
||||
m_design_panel = new DesignPanel(m_design_page);
|
||||
m_design_page->GetSizer()->Add(m_design_panel, 1, wxEXPAND);
|
||||
m_design_page->Layout();
|
||||
}
|
||||
return m_design_panel;
|
||||
}
|
||||
#endif
|
||||
|
||||
void MainFrame::init_tabpanel() {
|
||||
// wxNB_NOPAGETHEME: Disable Windows Vista theme for the Notebook background. The theme performance is terrible on
|
||||
// Windows 10 with multiple high resolution displays connected.
|
||||
@@ -1321,9 +1346,26 @@ void MainFrame::init_tabpanel() {
|
||||
}
|
||||
//else if (panel == m_param_panel)
|
||||
// m_param_panel->OnActivate();
|
||||
#ifdef SLIC3R_CAD
|
||||
else if (m_design_page != nullptr && panel == m_design_page) {
|
||||
// Built on first activation, never at startup: the panel creates several hundred
|
||||
// controls and its own GL canvas, which a user who does not open the tab should
|
||||
// not pay for.
|
||||
ensure_design_panel();
|
||||
// Re-sync the Design bed to the active printer: the panel is built before the
|
||||
// printer profile is fully applied, so its bed must refresh on activation or the
|
||||
// grid (true bed) spills past the stale default bed quad.
|
||||
m_design_panel->on_tab_shown();
|
||||
}
|
||||
#endif
|
||||
else if (panel == m_monitor) {
|
||||
//monitor
|
||||
}
|
||||
#ifdef SLIC3R_CAD
|
||||
// Any page that is not Design takes the Design status line down with it — see
|
||||
// DesignPanel::on_tab_hidden for why the popup does not follow the page on its own.
|
||||
if (m_design_panel != nullptr && panel != m_design_page) m_design_panel->on_tab_hidden();
|
||||
#endif
|
||||
#ifndef __APPLE__
|
||||
if (m_last_selected_tab == TAB_ID_PREPARE) {
|
||||
m_topbar->EnableUndoRedoItems();
|
||||
@@ -1354,6 +1396,20 @@ void MainFrame::init_tabpanel() {
|
||||
|
||||
wxGetApp().plater_ = m_plater;
|
||||
|
||||
#ifdef SLIC3R_CAD
|
||||
// Stand-in page for the Design tab. The real DesignPanel is built into it the first time
|
||||
// the tab is selected (see the page-changed handler above), so nothing it constructs sits
|
||||
// on the startup path. The experimental feature is off by default, and when it is off the
|
||||
// page is never created, so the tab does not appear at all (the preference takes effect on
|
||||
// the next start, like the other feature toggles).
|
||||
if (wxGetApp().is_enable_cad_feature()) {
|
||||
m_design_page = new wxPanel(this);
|
||||
m_design_page->SetSizer(new wxBoxSizer(wxVERTICAL));
|
||||
m_design_page->Hide();
|
||||
start_mcp_control_if_enabled(); // opens the MCP socket iff ORCA_CAD_MCP is set
|
||||
}
|
||||
#endif
|
||||
|
||||
create_preset_tabs();
|
||||
|
||||
//BBS add pages
|
||||
|
||||
@@ -40,6 +40,9 @@
|
||||
// Stable identifiers for MainFrame::m_tabpanel's built-in pages. These are
|
||||
// names rather than positional indices so optional pages cannot shift them.
|
||||
#define TAB_ID_HOME "home"
|
||||
#ifdef SLIC3R_CAD
|
||||
#define TAB_ID_DESIGN "design"
|
||||
#endif
|
||||
#define TAB_ID_PREPARE "prepare"
|
||||
#define TAB_ID_PREVIEW "preview"
|
||||
#define TAB_ID_MONITOR "monitor"
|
||||
@@ -65,6 +68,9 @@ namespace GUI
|
||||
class Tab;
|
||||
class PrintHostQueueDialog;
|
||||
class Plater;
|
||||
#ifdef SLIC3R_CAD
|
||||
class DesignPanel;
|
||||
#endif
|
||||
class MainFrame;
|
||||
class WebViewPanel;
|
||||
class ParamsDialog;
|
||||
@@ -403,6 +409,17 @@ public:
|
||||
BBLTopbar* m_topbar{ nullptr };
|
||||
PrintHostQueueDialog* printhost_queue_dlg() { return m_printhost_queue_dlg; }
|
||||
Plater* m_plater { nullptr };
|
||||
#ifdef SLIC3R_CAD
|
||||
// The tab page is the placeholder; m_design_panel stays null until the tab is first
|
||||
// selected, so everything the Design panel builds stays off the startup path.
|
||||
wxPanel* m_design_page { nullptr };
|
||||
DesignPanel* m_design_panel { nullptr };
|
||||
// Builds the Design panel if it does not exist yet and returns it (null only before the
|
||||
// placeholder page itself exists). Main thread only -- it creates wx controls. Both the
|
||||
// tab activation and the MCP socket go through this: the socket is driven headlessly,
|
||||
// with nobody to click the tab, and without this every verb would answer "not ready".
|
||||
DesignPanel* ensure_design_panel();
|
||||
#endif
|
||||
//BBS: GUI refactor
|
||||
MonitorPanel* m_monitor{ nullptr };
|
||||
|
||||
|
||||
@@ -1053,7 +1053,13 @@ void PartPlate::render_grid(bool bottom) {
|
||||
|
||||
void PartPlate::render_height_limit(PartPlate::HeightLimitMode mode)
|
||||
{
|
||||
if (m_print && m_print->config().print_sequence == PrintSequence::ByObject && mode != HEIGHT_LIMIT_NONE)
|
||||
// Orca: a prime tower compacted by "No sparse layers" drags the nozzle back down to the plate on
|
||||
// every toolchange, so the rod and the lid limit how tall a neighbouring object may be exactly as
|
||||
// they do in sequential printing. The reference lines are just as useful there.
|
||||
const bool relevant_for_print_mode = m_print && (m_print->config().print_sequence == PrintSequence::ByObject ||
|
||||
(m_print->config().print_sequence == PrintSequence::ByLayer &&
|
||||
wipe_tower_sparse_layers_skipped(m_print->config()) && m_print->has_wipe_tower()));
|
||||
if (relevant_for_print_mode && mode != HEIGHT_LIMIT_NONE)
|
||||
{
|
||||
// draw lower limit
|
||||
// ORCA: OpenGL Core Profile
|
||||
@@ -3501,7 +3507,7 @@ bool PartPlate::intersects(const BoundingBoxf3& bb) const
|
||||
return print_volume.intersects(bb);
|
||||
}
|
||||
|
||||
void PartPlate::render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_body, bool force_background_color, HeightLimitMode mode, int hover_id, bool render_cali, bool show_grid)
|
||||
void PartPlate::render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_body, bool force_background_color, HeightLimitMode mode, int hover_id, bool render_cali, bool show_grid, bool hide_chrome)
|
||||
{
|
||||
glsafe(::glEnable(GL_DEPTH_TEST));
|
||||
|
||||
@@ -3552,16 +3558,18 @@ void PartPlate::render(const Transform3d& view_matrix, const Transform3d& projec
|
||||
if (wxGetApp().show_plate_gridlines() && show_grid)
|
||||
render_grid(bottom);
|
||||
|
||||
if (!bottom && m_selected && !force_background_color) {
|
||||
if (!hide_chrome && !bottom && m_selected && !force_background_color) {
|
||||
if (m_partplate_list)
|
||||
render_logo(bottom, m_partplate_list->render_cali_logo && render_cali);
|
||||
else
|
||||
render_logo(bottom);
|
||||
}
|
||||
|
||||
render_icons(bottom, only_body, hover_id);
|
||||
if (!force_background_color) {
|
||||
render_only_numbers(bottom);
|
||||
if (!hide_chrome) {
|
||||
render_icons(bottom, only_body, hover_id);
|
||||
if (!force_background_color) {
|
||||
render_only_numbers(bottom);
|
||||
}
|
||||
}
|
||||
|
||||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||||
@@ -5956,7 +5964,7 @@ void PartPlateList::postprocess_arrange_polygon(arrangement::ArrangePolygon& arr
|
||||
|
||||
/*rendering related functions*/
|
||||
//render
|
||||
void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid)
|
||||
void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid, bool hide_chrome)
|
||||
{
|
||||
const std::lock_guard<std::mutex> local_lock(m_plates_mutex);
|
||||
std::vector<PartPlate*>::iterator it = m_plate_list.begin();
|
||||
@@ -5981,15 +5989,15 @@ void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& pr
|
||||
if (current_index == m_current_plate) {
|
||||
PartPlate::HeightLimitMode height_mode = (only_current)?PartPlate::HEIGHT_LIMIT_NONE:m_height_limit_mode;
|
||||
if (plate_hover_index == current_index)
|
||||
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, height_mode, plate_hover_action, render_cali, show_grid);
|
||||
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, height_mode, plate_hover_action, render_cali, show_grid, hide_chrome);
|
||||
else
|
||||
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, height_mode, -1, render_cali, show_grid);
|
||||
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, height_mode, -1, render_cali, show_grid, hide_chrome);
|
||||
}
|
||||
else {
|
||||
if (plate_hover_index == current_index)
|
||||
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, PartPlate::HEIGHT_LIMIT_NONE, plate_hover_action, render_cali, show_grid);
|
||||
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, PartPlate::HEIGHT_LIMIT_NONE, plate_hover_action, render_cali, show_grid, hide_chrome);
|
||||
else
|
||||
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, PartPlate::HEIGHT_LIMIT_NONE, -1, render_cali, show_grid);
|
||||
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, PartPlate::HEIGHT_LIMIT_NONE, -1, render_cali, show_grid, hide_chrome);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,7 +428,7 @@ public:
|
||||
bool contains(const BoundingBoxf3& bb) const;
|
||||
bool intersects(const BoundingBoxf3& bb) const;
|
||||
|
||||
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_body = false, bool force_background_color = false, HeightLimitMode mode = HEIGHT_LIMIT_NONE, int hover_id = -1, bool render_cali = false, bool show_grid = true);
|
||||
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_body = false, bool force_background_color = false, HeightLimitMode mode = HEIGHT_LIMIT_NONE, int hover_id = -1, bool render_cali = false, bool show_grid = true, bool hide_chrome = false);
|
||||
|
||||
void set_selected();
|
||||
void set_unselected();
|
||||
@@ -857,7 +857,7 @@ public:
|
||||
|
||||
/*rendering related functions*/
|
||||
void on_change_color_mode(bool is_dark) { m_is_dark = is_dark; }
|
||||
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current = false, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true);
|
||||
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current = false, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true, bool hide_chrome = false);
|
||||
void set_render_option(bool bedtype_texture, bool plate_settings);
|
||||
void set_render_cali(bool value = true) { render_cali_logo = value; }
|
||||
void register_raycasters_for_picking(GLCanvas3D& canvas)
|
||||
|
||||
@@ -91,6 +91,9 @@
|
||||
#include "wxExtensions.hpp"
|
||||
#include "../Utils/PrintHost.hpp"
|
||||
#include "MainFrame.hpp"
|
||||
#ifdef SLIC3R_CAD
|
||||
#include "slic3r/GUI/CAD/DesignPanel.hpp"
|
||||
#endif
|
||||
#include "format.hpp"
|
||||
#include "3DScene.hpp"
|
||||
#include "GLCanvas3D.hpp"
|
||||
@@ -8328,6 +8331,9 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
int answer_convert_from_meters = wxOK_DEFAULT;
|
||||
int answer_convert_from_imperial_units = wxOK_DEFAULT;
|
||||
int tolal_model_count = 0;
|
||||
// Whether one of the files being loaded here carried a CAD recipe. A statement about these
|
||||
// files, not about the plater — q->model() may still hold the previous project's recipe.
|
||||
bool loaded_cad_recipe = false;
|
||||
|
||||
int progress_percent = 0;
|
||||
int total_files = input_files.size();
|
||||
@@ -9453,6 +9459,16 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
auto loaded_idxs = load_model_objects(model.objects, is_project_file);
|
||||
obj_idxs.insert(obj_idxs.end(), loaded_idxs.begin(), loaded_idxs.end());
|
||||
|
||||
// load_model_objects only transfers ModelObjects; carry the Model-level CAD recipe
|
||||
// onto the plater model so the Design tab can rehydrate the editable feature tree on
|
||||
// reopen. Assigned unconditionally on the project-replacing path so that opening a
|
||||
// project without a recipe clears whatever the previous one left behind; importing a
|
||||
// plain model into the open project leaves the current recipe alone.
|
||||
if (is_project_file) {
|
||||
q->model().cad_recipe = model.cad_recipe;
|
||||
loaded_cad_recipe = !model.cad_recipe.empty();
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", finished load_model_objects");
|
||||
wxString msg = wxString::Format(_L("Loading file: %s"), from_path(real_filename));
|
||||
dlg_cont = dlg.Update(progress_percent, msg);
|
||||
@@ -9669,7 +9685,12 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
|
||||
// q->model().stl_design_country = "";
|
||||
//}
|
||||
|
||||
if (tolal_model_count <= 0 && !q->m_exported_file) {
|
||||
// A CAD project legitimately carries no mesh: the model lives in the feature tree until it is
|
||||
// committed to the plate. Warning "no geometry data" for one is false, and it is the LAST thing
|
||||
// a user sees after opening a design they spent an hour on — it reads as "your work is gone"
|
||||
// when the recipe has in fact just been loaded and the Design tab will rehydrate it. Count a
|
||||
// recipe that came from THESE files as geometry.
|
||||
if (tolal_model_count <= 0 && !loaded_cad_recipe && !q->m_exported_file) {
|
||||
dlg.Hide();
|
||||
if (!is_user_cancel) {
|
||||
MessageDialog msg(wxGetApp().mainframe, _L("The file does not contain any geometry data."), _L("Warning"), wxYES | wxICON_WARNING);
|
||||
@@ -10216,6 +10237,16 @@ void Plater::priv::reset(bool apply_presets_change)
|
||||
// Stop and reset the Print content.
|
||||
this->background_process.reset();
|
||||
model.clear_objects();
|
||||
// clear_objects() only drops the ModelObjects; the CAD recipe is Model-level state and would
|
||||
// otherwise be written into every project saved for the rest of the session.
|
||||
model.cad_recipe.clear();
|
||||
#ifdef SLIC3R_CAD
|
||||
// Same reason, one level up: the Design tab keeps the editable document, not the Model, so
|
||||
// clearing the recipe alone leaves the tab showing the previous project's feature tree —
|
||||
// and its next edit syncs that tree straight back into the new project.
|
||||
if (wxGetApp().mainframe != nullptr && wxGetApp().mainframe->m_design_panel != nullptr)
|
||||
wxGetApp().mainframe->m_design_panel->clear_document();
|
||||
#endif
|
||||
assemble_view->get_canvas3d()->reset_explosion_ratio();
|
||||
update();
|
||||
|
||||
@@ -13709,6 +13740,14 @@ void Plater::priv::unbind_canvas_event_handlers()
|
||||
|
||||
if (assemble_view != nullptr)
|
||||
assemble_view->get_canvas3d()->unbind_event_handlers();
|
||||
|
||||
#ifdef SLIC3R_CAD
|
||||
// The Design tab's viewport is a fourth GLCanvas3D on the same shared GL context, owned by
|
||||
// MainFrame rather than by us — same reach as reset() uses for clear_document(). Null until
|
||||
// the tab has been opened once, so most sessions skip it.
|
||||
if (wxGetApp().mainframe != nullptr && wxGetApp().mainframe->m_design_panel != nullptr)
|
||||
wxGetApp().mainframe->m_design_panel->unbind_canvas_event_handlers();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Plater::priv::reset_canvas_volumes()
|
||||
@@ -13718,6 +13757,11 @@ void Plater::priv::reset_canvas_volumes()
|
||||
|
||||
if (preview != nullptr)
|
||||
preview->get_canvas3d()->reset_volumes();
|
||||
|
||||
#ifdef SLIC3R_CAD
|
||||
if (wxGetApp().mainframe != nullptr && wxGetApp().mainframe->m_design_panel != nullptr)
|
||||
wxGetApp().mainframe->m_design_panel->reset_canvas_volumes();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Plater::priv::check_ams_status_impl(bool is_slice_all)
|
||||
@@ -15669,8 +15713,12 @@ bool Plater::up_to_date(bool saved, bool backup)
|
||||
Slic3r::clear_other_changes(backup);
|
||||
return p->up_to_date(saved, backup);
|
||||
}
|
||||
return p->model.objects.empty() || (p->up_to_date(saved, backup) &&
|
||||
!Slic3r::has_other_changes(backup));
|
||||
// A Design-tab project is object-less until it is committed to the plate, but its feature
|
||||
// tree is real work: treating it as an empty project skipped both the autosave and the
|
||||
// "unsaved changes" prompt, so quitting threw it away without asking. Non-CAD projects
|
||||
// never carry a recipe, so the empty-project shortcut is unchanged for them.
|
||||
return (p->model.objects.empty() && p->model.cad_recipe.empty()) ||
|
||||
(p->up_to_date(saved, backup) && !Slic3r::has_other_changes(backup));
|
||||
}
|
||||
|
||||
bool Plater::add_model(bool imperial_units, std::string fname)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "I18N.hpp"
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
#include "libslic3r/Format/DRC.hpp"
|
||||
#include "libslic3r/CAD/SketchEngine.hpp"
|
||||
#include <wx/language.h>
|
||||
#include "OG_CustomCtrl.hpp"
|
||||
#include "wx/graphics.h"
|
||||
@@ -367,7 +368,8 @@ wxBoxSizer *PreferencesDialog::create_item_language_combobox(wxString title, wxS
|
||||
wxLANGUAGE_PORTUGUESE_BRAZILIAN,
|
||||
wxLANGUAGE_LITHUANIAN,
|
||||
wxLANGUAGE_VIETNAMESE,
|
||||
wxLANGUAGE_THAI
|
||||
wxLANGUAGE_THAI,
|
||||
wxLANGUAGE_ROMANIAN
|
||||
};
|
||||
|
||||
auto translations = wxTranslations::Get()->GetAvailableTranslations(SLIC3R_APP_KEY);
|
||||
@@ -1739,6 +1741,21 @@ void PreferencesDialog::create_items()
|
||||
SPEED_DIAL_RECENT_COUNT_MAX);
|
||||
g_sizer->Add(item_speed_dial_recents);
|
||||
|
||||
#ifdef SLIC3R_CAD
|
||||
auto item_cad_feature = create_item_checkbox(_L("CAD feature (experimental)"),
|
||||
_L("With this option enabled, the Design tab is shown, where models can be built and edited "
|
||||
"parametrically. This feature is experimental and still under development."),
|
||||
"enable_cad_feature", _L("(Requires restart)"));
|
||||
g_sizer->Add(item_cad_feature);
|
||||
|
||||
auto item_auto_close_sketch_loops = create_item_checkbox(_L("Auto-close sketch loops"),
|
||||
_L("Treat sketch endpoints within 0.001 mm as one joint and weld the loop shut. "
|
||||
"Off: only exactly coincident endpoints join, so a loop with a tiny gap is "
|
||||
"shown as open instead of being closed for you."),
|
||||
"auto_close_sketch_loops");
|
||||
g_sizer->Add(item_auto_close_sketch_loops);
|
||||
#endif
|
||||
|
||||
#if 0
|
||||
g_sizer->Add(create_item_title(_L("Filament Grouping")), 1, wxEXPAND);
|
||||
//temporarily disable it
|
||||
@@ -1815,6 +1832,21 @@ void PreferencesDialog::create_items()
|
||||
auto reverse_mouse_zoom = create_item_checkbox(_L("Reverse mouse zoom"), _L("If enabled, reverses the direction of zoom with mouse wheel."), "reverse_mouse_wheel_zoom");
|
||||
g_sizer->Add(reverse_mouse_zoom);
|
||||
|
||||
#ifdef SLIC3R_CAD
|
||||
// Design-tab only, so it stays out of the way while the CAD feature is switched off.
|
||||
if (wxGetApp().is_enable_cad_feature()) {
|
||||
auto item_connector_face_glyph = create_item_checkbox(_L("Draw mate connectors as a face"),
|
||||
_L("In the Design tab, draw a mate connector as a small face instead of the conventional "
|
||||
"disc with a roll quadrant. A face's orientation is read without being learned. "
|
||||
"Turn this off for the conventional CAD representation."), "design_connector_face_glyph");
|
||||
g_sizer->Add(item_connector_face_glyph);
|
||||
}
|
||||
|
||||
// Push the weld preference into the kernel now so toggling it takes effect without
|
||||
// a restart (the sketch tool also re-pushes on activation, see DesignSketchTool::begin).
|
||||
Slic3r::set_sketch_auto_close(wxGetApp().is_auto_close_sketch_loops());
|
||||
#endif
|
||||
|
||||
std::vector<wxString> ButtonDragActions = {_L("None"), _L("Pan"), _L("Rotate")};
|
||||
auto item_left_mouse_drag = create_item_combobox(_L("Left Mouse Drag"), _L("Set the action that dragging the left mouse button should perform."), "left_mouse_drag_action", ButtonDragActions);
|
||||
g_sizer->Add(item_left_mouse_drag);
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <wx/dcmemory.h>
|
||||
#include <wx/dcgraph.h>
|
||||
#include <wx/image.h>
|
||||
#include <wx/wrapsizer.h>
|
||||
#include <set>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@@ -416,12 +417,54 @@ std::set<size_t> project_used_filament_slots(const PresetBundle& bundle, const D
|
||||
return used;
|
||||
}
|
||||
|
||||
// Lays a translated sentence out along `row`, replacing each "%1%"-style placeholder with the
|
||||
// matching window from `chips`. Keeping the sentence in one msgid lets a translation put the
|
||||
// placeholders wherever its own grammar needs them; spacing comes from the translation itself.
|
||||
void add_sentence_with_chips(wxWindow* parent, wxSizer* row, const wxString& sentence, const std::vector<wxWindow*>& chips)
|
||||
{
|
||||
std::vector<bool> placed(chips.size(), false);
|
||||
auto add_text = [&](wxString text) {
|
||||
text.Replace("%%", "%"); // the sentence is a format string
|
||||
if (text.IsEmpty())
|
||||
return;
|
||||
auto* label = new wxStaticText(parent, wxID_ANY, text);
|
||||
label->SetFont(Label::Body_12);
|
||||
label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
|
||||
row->Add(label, 0, wxALIGN_CENTER_VERTICAL);
|
||||
};
|
||||
auto add_chip = [&](size_t i) {
|
||||
if (i < chips.size() && chips[i] != nullptr && !placed[i]) {
|
||||
placed[i] = true;
|
||||
row->Add(chips[i], 0, wxALIGN_CENTER_VERTICAL);
|
||||
}
|
||||
};
|
||||
|
||||
size_t literal = 0, pos = 0;
|
||||
while ((pos = sentence.find('%', pos)) != wxString::npos) {
|
||||
size_t end = pos + 1;
|
||||
while (end < sentence.length() && sentence[end] >= '0' && sentence[end] <= '9')
|
||||
++end;
|
||||
if (end == pos + 1 || end >= sentence.length() || sentence[end] != '%') {
|
||||
++pos; // a bare '%'
|
||||
continue;
|
||||
}
|
||||
long index = 0;
|
||||
sentence.Mid(pos + 1, end - pos - 1).ToLong(&index);
|
||||
add_text(sentence.Mid(literal, pos - literal));
|
||||
add_chip(size_t(index - 1));
|
||||
literal = pos = end + 1;
|
||||
}
|
||||
add_text(sentence.Mid(literal));
|
||||
for (size_t i = 0; i < chips.size(); ++i) // whatever the translation left out
|
||||
add_chip(i);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Warning shown on OK when an enabled mixed-filament slot relies on a filament that would ship
|
||||
// without its material. One row per unmet dependency: the mixed slot's colour chip, the
|
||||
// component filament's colour chip, and the reason. "Cancel" is the safe choice and keeps the
|
||||
// dialog open; "Publish anyway" continues.
|
||||
// without its material. One row per unmet dependency, each a single translated sentence whose
|
||||
// two placeholders are the mixed slot's and the component filament's colour chips. "Cancel" is
|
||||
// the safe choice and keeps the dialog open; "Publish anyway" continues.
|
||||
class MixedFilamentWarningDialog : public MsgDialog
|
||||
{
|
||||
public:
|
||||
@@ -437,47 +480,37 @@ public:
|
||||
content->AddSpacer(FromDIP(10));
|
||||
|
||||
const int swatch = FromDIP(20);
|
||||
for (const MixedDependencyIssue& issue : issues) {
|
||||
auto* row = new wxBoxSizer(wxHORIZONTAL);
|
||||
|
||||
// The mixed slot as just its own chip (gradient-aware, numbered like its tab);
|
||||
// falls back to a plain label when the chip cannot be built.
|
||||
const wxString mix_label = wxString::Format(_L("Filament %d (mixed)"), int(issue.mixed_slot) + 1);
|
||||
const wxBitmap mix_bmp = mixed_filament_chip_bitmap(full, issue.mixed_slot, swatch);
|
||||
if (mix_bmp.IsOk()) {
|
||||
auto* bmp = new wxStaticBitmap(this, wxID_ANY, mix_bmp);
|
||||
bmp->SetToolTip(mix_label);
|
||||
row->Add(bmp, 0, wxALIGN_CENTER_VERTICAL);
|
||||
} else {
|
||||
auto* label = new wxStaticText(this, wxID_ANY, mix_label);
|
||||
label->SetFont(Label::Body_12);
|
||||
row->Add(label, 0, wxALIGN_CENTER_VERTICAL);
|
||||
// The slot's colour swatch, numbered like its tab, with the slot name on hover; falls
|
||||
// back to a label so the sentence always names both filaments.
|
||||
auto make_chip = [&](const wxBitmap& bmp, const wxString& name) -> wxWindow* {
|
||||
if (bmp.IsOk()) {
|
||||
auto* chip = new wxStaticBitmap(this, wxID_ANY, bmp);
|
||||
chip->SetToolTip(name);
|
||||
return chip;
|
||||
}
|
||||
auto* label = new wxStaticText(this, wxID_ANY, name);
|
||||
label->SetFont(Label::Body_12);
|
||||
return label;
|
||||
};
|
||||
|
||||
auto* needs = new wxStaticText(this, wxID_ANY, _L("needs"));
|
||||
needs->SetFont(Label::Body_12);
|
||||
needs->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
|
||||
row->Add(needs, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, FromDIP(6));
|
||||
|
||||
// The component filament's colour chip, numbered like the tab strips; the slot
|
||||
// name stays on hover to keep the row itself short.
|
||||
for (const MixedDependencyIssue& issue : issues) {
|
||||
std::string hex = filament_color_hex(full, issue.component_slot);
|
||||
if (hex.empty())
|
||||
hex = "#D9D9D9";
|
||||
if (wxBitmap* chip = get_extruder_color_icon(hex, std::to_string(issue.component_slot + 1), swatch, swatch)) {
|
||||
auto* comp_bmp = new wxStaticBitmap(this, wxID_ANY, *chip);
|
||||
comp_bmp->SetToolTip(wxString::Format(_L("Filament %d"), int(issue.component_slot) + 1));
|
||||
row->Add(comp_bmp, 0, wxALIGN_CENTER_VERTICAL);
|
||||
}
|
||||
const wxBitmap* comp_bmp = get_extruder_color_icon(hex, std::to_string(issue.component_slot + 1), swatch, swatch);
|
||||
|
||||
auto* reason = new wxStaticText(this, wxID_ANY,
|
||||
issue.reason == MixedDependencyIssue::Reason::Disabled ? _L("not enabled") :
|
||||
_L("material not published"));
|
||||
reason->SetFont(Label::Body_12);
|
||||
reason->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#989898")));
|
||||
row->Add(reason, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
|
||||
wxWindow* mix_chip = make_chip(mixed_filament_chip_bitmap(full, issue.mixed_slot, swatch),
|
||||
wxString::Format(_L("Filament %d (mixed)"), int(issue.mixed_slot) + 1));
|
||||
wxWindow* comp_chip = make_chip(comp_bmp != nullptr ? *comp_bmp : wxNullBitmap,
|
||||
wxString::Format(_L("Filament %d"), int(issue.component_slot) + 1));
|
||||
|
||||
content->Add(row, 0, wxLEFT, FromDIP(10));
|
||||
auto* row = new wxWrapSizer(wxHORIZONTAL);
|
||||
add_sentence_with_chips(this, row,
|
||||
issue.reason == MixedDependencyIssue::Reason::Disabled ?
|
||||
_L("%1% needs %2%, which is not enabled.") :
|
||||
_L("%1% needs %2%, whose material will not be published."),
|
||||
{mix_chip, comp_chip});
|
||||
content->Add(row, 0, wxEXPAND | wxLEFT, FromDIP(10));
|
||||
content->AddSpacer(FromDIP(6));
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Search {
|
||||
|
||||
static std::string get_key(const std::string &opt_key, Preset::Type type) { return std::to_string(int(type)) + ";" + opt_key; }
|
||||
|
||||
std::string Option::opt_key() const { return into_u8(key).substr(2); }
|
||||
std::string Option::opt_key() const { return key.size() < 2 ? std::string() : into_u8(key).substr(2); }
|
||||
|
||||
template<class T>
|
||||
// void change_opt_key(std::string& opt_key, DynamicPrintConfig* config)
|
||||
@@ -79,6 +79,7 @@ void SettingsIndex::append_options(DynamicPrintConfig *config, Preset::Type type
|
||||
case coFloats: change_opt_key<ConfigOptionFloats>(opt_key, config, cnt); break;
|
||||
case coStrings: change_opt_key<ConfigOptionStrings>(opt_key, config, cnt); break;
|
||||
case coPercents: change_opt_key<ConfigOptionPercents>(opt_key, config, cnt); break;
|
||||
case coFloatsOrPercents: change_opt_key<ConfigOptionVector<FloatOrPercent>>(opt_key, config, cnt); break;
|
||||
case coPoints: change_opt_key<ConfigOptionPoints>(opt_key, config, cnt); break;
|
||||
// BBS
|
||||
case coEnums: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break;
|
||||
@@ -155,29 +156,46 @@ bool SettingsIndex::apply(DynamicPrintConfig *config, Preset::Type type, ConfigO
|
||||
|
||||
const Option &SettingsIndex::get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const
|
||||
{
|
||||
auto not_found = [&variant_index]() -> const Option & {
|
||||
static const Option empty_option;
|
||||
variant_index = -2;
|
||||
return empty_option;
|
||||
};
|
||||
|
||||
variant_index = -1;
|
||||
std::string opt_key2 = opt_key;
|
||||
if (auto n = opt_key.find('#'); n != std::string::npos) {
|
||||
variant_index = std::atoi(opt_key.c_str() + n + 1);
|
||||
opt_key2 = opt_key.substr(0, n);
|
||||
}
|
||||
auto it = std::lower_bound(m_options.begin(), m_options.end(), Option({boost::nowide::widen(get_key(opt_key2, type))}));
|
||||
// BBS: return the 0th option when not found in searcher caused by mode difference
|
||||
// assert(it != options.end());
|
||||
if (it == m_options.end()) { variant_index = -2 ; return m_options[0]; }
|
||||
if (it->opt_key() == opt_key2) {
|
||||
const std::wstring key = boost::nowide::widen(get_key(opt_key2, type));
|
||||
auto it = std::lower_bound(m_options.begin(), m_options.end(), Option({key}));
|
||||
if (it == m_options.end()) return not_found();
|
||||
if (it->key == key) {
|
||||
variant_index = -1;
|
||||
} else {
|
||||
const std::string opt_key3 = opt_key2 + "#";
|
||||
it = std::lower_bound(it, m_options.end(), Option({boost::nowide::widen(get_key(opt_key3, type))}));
|
||||
if (it == m_options.end() || it->opt_key().compare(0, opt_key3.length(), opt_key3) != 0) {
|
||||
variant_index = -2; // Not found
|
||||
return m_options[0];
|
||||
const std::wstring prefix = key + L"#";
|
||||
it = std::lower_bound(it, m_options.end(), Option({prefix}));
|
||||
if (it == m_options.end() || it->key.compare(0, prefix.length(), prefix) != 0)
|
||||
return not_found();
|
||||
// Orca: Copy-parameters dialogs request the base key, without a vector index.
|
||||
if (variant_index < 0) return *it;
|
||||
|
||||
const bool has_mode = type == Preset::TYPE_PRINTER && printer_options_with_variant_2.count(opt_key2) > 0;
|
||||
const bool has_variant =
|
||||
(type == Preset::TYPE_PRINT && print_options_with_variant.count(opt_key2) > 0) ||
|
||||
(type == Preset::TYPE_FILAMENT && filament_options_with_variant.count(opt_key2) > 0) ||
|
||||
(type == Preset::TYPE_PRINTER && printer_options_with_variant_1.count(opt_key2) > 0) || has_mode;
|
||||
if (!has_variant || has_mode) {
|
||||
// Orca: Machine limits store (Normal, Silent) pairs per variant; the UI registers only #0/#1.
|
||||
const std::wstring indexed_key = has_mode ? prefix + std::to_wstring(variant_index % 2) :
|
||||
boost::nowide::widen(get_key(opt_key, type));
|
||||
it = std::lower_bound(it, m_options.end(), Option({indexed_key}));
|
||||
if (it == m_options.end() || it->key != indexed_key)
|
||||
return not_found();
|
||||
if (!has_variant)
|
||||
variant_index = -1;
|
||||
}
|
||||
auto it2 = it;
|
||||
++it2;
|
||||
if (it2 != m_options.end() && it2->opt_key().compare(0, opt_key3.length(), opt_key3) == 0
|
||||
&& printer_options_with_variant_1.find(opt_key2) == printer_options_with_variant_1.end())
|
||||
variant_index = -2;
|
||||
}
|
||||
|
||||
return m_options[it - m_options.begin()];
|
||||
|
||||
@@ -2022,6 +2022,20 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
|
||||
|
||||
// reload scene to update timelapse wipe tower
|
||||
if (opt_key == "timelapse_type") {
|
||||
// Smooth timelapse parks the nozzle on the prime tower every layer, so it needs a tower on
|
||||
// every layer. That is exactly what "No sparse layers" removes, and with both on the tower is
|
||||
// planned full height and then dropped on emission. Drop "No sparse layers" and tell the user.
|
||||
if (boost::any_cast<int>(value) == (int) TimelapseType::tlSmooth && m_config->opt_bool("wipe_tower_no_sparse_layers")) {
|
||||
MessageDialog dlg(wxGetApp().plater(),
|
||||
_L("Smooth timelapse needs a prime tower on every layer, which is not compatible with \"No sparse layers\". "
|
||||
"\"No sparse layers\" has been turned off."),
|
||||
_L("Warning"), wxICON_WARNING | wxOK);
|
||||
dlg.ShowModal();
|
||||
DynamicPrintConfig new_conf = *m_config;
|
||||
new_conf.set_key_value("wipe_tower_no_sparse_layers", new ConfigOptionBool(false));
|
||||
m_config_manipulation.apply(m_config, &new_conf);
|
||||
}
|
||||
|
||||
bool wipe_tower_enabled = m_config->option<ConfigOptionBool>("enable_prime_tower")->value;
|
||||
if (!wipe_tower_enabled && boost::any_cast<int>(value) == (int)TimelapseType::tlSmooth) {
|
||||
MessageDialog dlg(wxGetApp().plater(), _L("A prime tower is required for smooth timelapse mode. There may be flaws on the model without prime tower. Do you want to enable the prime tower\?"),
|
||||
@@ -2037,6 +2051,23 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
|
||||
}
|
||||
}
|
||||
|
||||
// Mirror of the timelapse_type branch above: enabling "No sparse layers" while smooth timelapse
|
||||
// is active would leave the tower on every layer anyway, so fall back to traditional timelapse.
|
||||
if (opt_key == "wipe_tower_no_sparse_layers" && boost::any_cast<bool>(value)) {
|
||||
auto timelapse_type = m_config->option<ConfigOptionEnum<TimelapseType>>("timelapse_type");
|
||||
if (timelapse_type && timelapse_type->value == TimelapseType::tlSmooth) {
|
||||
MessageDialog dlg(wxGetApp().plater(),
|
||||
_L("\"No sparse layers\" is not compatible with smooth timelapse, which needs a prime tower on every layer. "
|
||||
"Timelapse has been switched to traditional mode."),
|
||||
_L("Warning"), wxICON_WARNING | wxOK);
|
||||
dlg.ShowModal();
|
||||
DynamicPrintConfig new_conf = *m_config;
|
||||
new_conf.set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(TimelapseType::tlTraditional));
|
||||
m_config_manipulation.apply(m_config, &new_conf);
|
||||
wxGetApp().plater()->update();
|
||||
}
|
||||
}
|
||||
|
||||
if (opt_key == "print_sequence" && m_config->opt_enum<PrintSequence>("print_sequence") == PrintSequence::ByObject) {
|
||||
auto printer_structure_opt = m_preset_bundle->printers.get_edited_preset().config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure");
|
||||
if (printer_structure_opt && printer_structure_opt->value == PrinterStructure::psI3) {
|
||||
@@ -2793,6 +2824,7 @@ void TabPrint::build()
|
||||
|
||||
optgroup = page->new_optgroup(L("Overhangs"), L"param_overhang");
|
||||
optgroup->append_single_option_line("detect_overhang_wall", "quality_settings_overhangs#detect-overhang-wall");
|
||||
optgroup->append_single_option_line("unsupported_wall_last", "quality_settings_overhangs#unsupported-wall-last");
|
||||
optgroup->append_single_option_line("make_overhang_printable", "quality_settings_overhangs#make-overhang-printable");
|
||||
optgroup->append_single_option_line("make_overhang_printable_angle", "quality_settings_overhangs#maximum-angle");
|
||||
optgroup->append_single_option_line("make_overhang_printable_hole_size", "quality_settings_overhangs#hole-area");
|
||||
@@ -3056,6 +3088,8 @@ void TabPrint::build()
|
||||
optgroup = page->new_optgroup(L("Advanced"), L"advanced");
|
||||
optgroup->append_single_option_line("interlocking_beam", "multimaterial_settings_advanced#interlocking-beam");
|
||||
optgroup->append_single_option_line("toolchange_ordering", "multimaterial_settings_advanced#toolchange-ordering");
|
||||
optgroup->append_single_option_line("toolchange_cyclic_order", "multimaterial_settings_advanced#toolchange-order");
|
||||
optgroup->append_single_option_line("toolchange_cyclic_first_layer", "multimaterial_settings_advanced#toolchange-order");
|
||||
optgroup->append_single_option_line("interface_shells", "multimaterial_settings_advanced#interface-shells");
|
||||
optgroup->append_single_option_line("mmu_segmented_region_max_width", "multimaterial_settings_advanced#maximum-width-of-segmented-region");
|
||||
optgroup->append_single_option_line("mmu_segmented_region_interlocking_depth", "multimaterial_settings_advanced#interlocking-depth-of-segmented-region");
|
||||
@@ -5137,6 +5171,7 @@ void TabPrinter::build_fff()
|
||||
|
||||
optgroup = page->new_optgroup(L("Extruder Clearance"), "param_extruder_clearance");
|
||||
optgroup->append_single_option_line("extruder_clearance_radius", "printer_basic_information_extruder_clearance#radius");
|
||||
optgroup->append_single_option_line("extruder_clearance_dist_to_rod", "printer_basic_information_extruder_clearance#distance-to-rod");
|
||||
optgroup->append_single_option_line("extruder_clearance_height_to_rod", "printer_basic_information_extruder_clearance#height-to-rod");
|
||||
optgroup->append_single_option_line("extruder_clearance_height_to_lid", "printer_basic_information_extruder_clearance#height-to-lid");
|
||||
|
||||
|
||||
@@ -1490,7 +1490,15 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, DynamicConfig * config
|
||||
|
||||
for (const std::string &opt_key : config->keys()) {
|
||||
int variant_index = -2;
|
||||
const Search::Option &option = index.get_option(opt_key, type, variant_index);
|
||||
Search::Option option = index.get_option(opt_key, type, variant_index);
|
||||
if (variant_index == -2) {
|
||||
// Orca: Every transferred setting must remain visible even when it is absent from the search index.
|
||||
const ConfigOptionDef* def = print_config_def.get(opt_key);
|
||||
const std::string label = def ? (def->full_label.empty() ? def->label : def->full_label) : std::string();
|
||||
option.label_local = (label.empty() ? from_u8(opt_key) : _L(label)).ToStdWstring();
|
||||
option.category_local = (def && !def->category.empty() ?
|
||||
Tab::translate_category(from_u8(def->category), type) : _L("Others")).ToStdWstring();
|
||||
}
|
||||
auto category = option.category_local;
|
||||
auto opt = dynamic_cast<ConfigOptionVectorBase*>(config->option(opt_key));
|
||||
std::string value_from = opt->vserialize()[from];
|
||||
@@ -1518,6 +1526,8 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres
|
||||
else
|
||||
presets_list.emplace_back(presets_);
|
||||
|
||||
const bool multiple_extruders = wxGetApp().preset_bundle->get_printer_extruder_count() > 1;
|
||||
|
||||
// Display a dialog showing the dirty options in a human readable form.
|
||||
for (PresetCollection* presets : presets_list)
|
||||
{
|
||||
@@ -1553,29 +1563,41 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres
|
||||
|
||||
auto variant_key = Preset::get_iot_type_string(type) + "_extruder_variant";
|
||||
auto id_key = Preset::get_iot_type_string(type) + "_extruder_id";
|
||||
auto extruder_variant = dynamic_cast<ConfigOptionStrings const *>(old_config.option(variant_key));
|
||||
auto extruder_id = dynamic_cast<ConfigOptionInts const *>(old_config.option(id_key));
|
||||
// Orca: Dirty indices belong to the edited config, which may contain newly added variants.
|
||||
auto extruder_variant = dynamic_cast<ConfigOptionStrings const *>(new_config.option(variant_key));
|
||||
auto extruder_id = dynamic_cast<ConfigOptionInts const *>(new_config.option(id_key));
|
||||
|
||||
for (const std::string& opt_key : dirty_options) {
|
||||
int variant_index = -2;
|
||||
const Search::Option &option = index.get_option(opt_key, type, variant_index);
|
||||
if (option.opt_key() != opt_key && variant_index < -1) {
|
||||
if (variant_index == -2) {
|
||||
// When founded option isn't the correct one.
|
||||
// It can be for dirty_options: "default_print_profile", "printer_model", "printer_settings_id",
|
||||
// because of they don't exist in the index
|
||||
continue;
|
||||
}
|
||||
auto category = option.category_local;
|
||||
if (variant_index >= 0) {
|
||||
if (printer_options_with_variant_2.count(opt_key.substr(0, opt_key.find_last_of('#'))) > 0)
|
||||
variant_index /= 2;
|
||||
if (boost::nowide::narrow(category).find("Extruder ") == 0)
|
||||
category = category.substr(0, 8);
|
||||
if (extruder_id)
|
||||
category = category + (wxString(" {") + (extruder_id->values[variant_index] == 1 ? _L("Left: ") : _L("Right: "))
|
||||
+ L(extruder_variant->values[variant_index]) + "}");
|
||||
else
|
||||
category = category + (wxString(" {") + L(extruder_variant->values[variant_index]) + "}");
|
||||
wxString category = option.category_local;
|
||||
wxString label = option.label_local;
|
||||
if (type == Preset::TYPE_PRINTER && variant_index >= 0 &&
|
||||
printer_options_with_variant_2.count(get_pure_opt_key(opt_key)) > 0) {
|
||||
// Orca: silent_mode is obsolete on import, but its option and two-column UI still exist.
|
||||
// Keep mode labels for configs that explicitly enable it; omit them in the default single-mode UI.
|
||||
if (new_config.opt_bool("silent_mode"))
|
||||
label += " (" + (variant_index % 2 == 0 ? _L("Normal") : _L("Silent")) + ")";
|
||||
variant_index /= 2;
|
||||
}
|
||||
if (variant_index >= 0 && extruder_variant && variant_index < extruder_variant->size()) {
|
||||
// Orca: Match the untranslated category and use the same extruder names as the printer tabs.
|
||||
if (option.category.compare(0, 9, L"Extruder ") == 0)
|
||||
category = _L("Extruder");
|
||||
wxString variant_label = L(extruder_variant->values[variant_index]);
|
||||
// Orca: An extruder name only disambiguates variants on printers with multiple extruders.
|
||||
if (multiple_extruders && extruder_id && variant_index < extruder_id->size() && extruder_id->values[variant_index] > 0) {
|
||||
const wxString extruder_name = Tab::translate_category(
|
||||
wxString::Format("Extruder %d", extruder_id->values[variant_index]), Preset::TYPE_PRINTER);
|
||||
variant_label = extruder_name + " (" + variant_label + ")";
|
||||
}
|
||||
category = variant_label + ": " + category;
|
||||
}
|
||||
|
||||
/*m_tree->Append(opt_key, type, option.category_local, option.group_local, option.label_local,
|
||||
@@ -1584,7 +1606,7 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres
|
||||
|
||||
//PresetItem pi = {opt_key, type, 1983};
|
||||
//m_presetitems.push_back()
|
||||
PresetItem pi = {type, opt_key, category, option.group_local, option.label_local, get_string_value(opt_key, old_config), get_string_value(opt_key, new_config)};
|
||||
PresetItem pi = {type, opt_key, category, option.group_local, label, get_string_value(opt_key, old_config), get_string_value(opt_key, new_config)};
|
||||
m_presetitems.push_back(pi);
|
||||
|
||||
}
|
||||
|
||||
@@ -275,9 +275,9 @@ void TempInput::Warning(bool warn, WarningType type)
|
||||
|
||||
wxString warning_string;
|
||||
if (type == WarningType::WARNING_TOO_HIGH)
|
||||
warning_string = _L("The maximum temperature cannot exceed ") + wxString::Format("%d", max_temp);
|
||||
warning_string = wxString::Format(_L("The maximum temperature cannot exceed %d"), max_temp);
|
||||
else if (type == WarningType::WARNING_TOO_LOW)
|
||||
warning_string = _L("The minmum temperature should not be less than ") + wxString::Format("%d", min_temp);
|
||||
warning_string = wxString::Format(_L("The minimum temperature should not be less than %d"), min_temp);
|
||||
warning_text->SetLabel(warning_string);
|
||||
warning_text->Wrap(-1);
|
||||
warning_text->Fit();
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#include "MeshInspect.hpp"
|
||||
|
||||
#include "libslic3r/LayOnFace.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <ostream>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace MeshInspect {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
static json to_json(const Vec3d &v) { return json::array({ v.x(), v.y(), v.z() }); }
|
||||
|
||||
static json to_json(const BoundingBoxf3 &bb)
|
||||
{
|
||||
return { { "min", to_json(bb.min) }, { "max", to_json(bb.max) }, { "size", to_json(bb.size()) } };
|
||||
}
|
||||
|
||||
void inspect_to_json(const Model &model, const std::vector<std::string> &source_paths, std::ostream &out, size_t max_planes)
|
||||
{
|
||||
json objects = json::array();
|
||||
for (const ModelObject *mo : model.objects) {
|
||||
json obj = { { "name", mo->name },
|
||||
{ "triangle_count", mo->facets_count() },
|
||||
{ "instance_count", mo->instances.size() },
|
||||
{ "bbox_object", to_json(mo->raw_mesh_bounding_box()) } };
|
||||
if (!mo->instances.empty()) {
|
||||
const std::vector<LayOnFacePlane> planes = lay_on_face_planes(*mo, mo->instances.front()->get_matrix_no_offset());
|
||||
json planes_json = json::array();
|
||||
for (size_t i = 0; i < std::min(planes.size(), max_planes); ++i)
|
||||
planes_json.push_back({ { "normal", to_json(planes[i].normal) },
|
||||
{ "area_mm2", std::round(double(planes[i].area) * 1000.) / 1000. },
|
||||
{ "center", to_json(planes[i].center) } });
|
||||
obj["bbox_world"] = to_json(mo->instance_bounding_box(0));
|
||||
obj["instance_offset"] = to_json(mo->instances.front()->get_offset());
|
||||
obj["plane_count"] = planes.size();
|
||||
obj["planes"] = std::move(planes_json);
|
||||
}
|
||||
objects.push_back(std::move(obj));
|
||||
}
|
||||
|
||||
const json root = {
|
||||
{ "sources", source_paths },
|
||||
{ "note", "Lengths in mm. bbox_object and the plane normals and centers are in object coordinates: the parts as "
|
||||
"currently transformed, without the instance transformation. --ground-face-normal and "
|
||||
"--ground-face-point take values in these coordinates. area_mm2 uses instance 0's scale, "
|
||||
"bbox_world is instance 0 on the plate." },
|
||||
{ "objects", std::move(objects) },
|
||||
};
|
||||
// Object names and file paths are arbitrary bytes, and dump() throws on invalid UTF-8 by default.
|
||||
// Replace such sequences with U+FFFD so the output is always valid JSON.
|
||||
out << root.dump(2, ' ', false, json::error_handler_t::replace) << std::endl;
|
||||
}
|
||||
|
||||
} // namespace MeshInspect
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <iosfwd>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class Model;
|
||||
|
||||
namespace MeshInspect {
|
||||
|
||||
// Writes the --inspect-mesh JSON for `model` to `out`: per object its bounding boxes and the faces
|
||||
// it can be laid on, taken from lay_on_face_planes() so they are the faces the --ground-* options
|
||||
// choose from, in the frame those options take. At most `max_planes` faces are listed per object,
|
||||
// largest first. `source_paths` lists every input file; the CLI merges them into one model.
|
||||
void inspect_to_json(const Model &model, const std::vector<std::string> &source_paths, std::ostream &out, size_t max_planes = 8);
|
||||
|
||||
} // namespace MeshInspect
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,204 @@
|
||||
// PaintCLI.cpp — CLI paint-inspection primitives. See PaintCLI.hpp.
|
||||
#include "PaintCLI.hpp"
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/TriangleSelector.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace PaintCLI {
|
||||
|
||||
namespace {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
double its_surface_area(const indexed_triangle_set &its)
|
||||
{
|
||||
double total = 0.0;
|
||||
for (const stl_triangle_vertex_indices &t : its.indices) {
|
||||
const Vec3f &a = its.vertices[t(0)];
|
||||
const Vec3f &b = its.vertices[t(1)];
|
||||
const Vec3f &c = its.vertices[t(2)];
|
||||
total += 0.5 * (b - a).cross(c - a).norm();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// Bbox over triangle-referenced vertices only. get_facets_strict() returns
|
||||
// an itset with the full source vertex list — using bounding_box() on it
|
||||
// would report the whole mesh's bbox even when only a few facets are painted.
|
||||
BoundingBoxf3 its_referenced_bbox(const indexed_triangle_set &its)
|
||||
{
|
||||
BoundingBoxf3 bb;
|
||||
bool first = true;
|
||||
for (const stl_triangle_vertex_indices &t : its.indices) {
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const Vec3d v = its.vertices[t(k)].cast<double>();
|
||||
if (first) { bb.min = bb.max = v; first = false; }
|
||||
else bb.merge(v);
|
||||
}
|
||||
}
|
||||
return bb;
|
||||
}
|
||||
|
||||
json vec3_to_json(const Vec3d &v)
|
||||
{
|
||||
return json::array({ v.x(), v.y(), v.z() });
|
||||
}
|
||||
|
||||
json bbox_to_json(const BoundingBoxf3 &bb)
|
||||
{
|
||||
return {
|
||||
{ "min", vec3_to_json(bb.min) },
|
||||
{ "max", vec3_to_json(bb.max) },
|
||||
{ "size", vec3_to_json(Vec3d(bb.max - bb.min)) },
|
||||
};
|
||||
}
|
||||
|
||||
// One (layer, state) row — empty ones are omitted at the caller level.
|
||||
json state_entry(const std::string &label, const indexed_triangle_set &its)
|
||||
{
|
||||
return {
|
||||
{ "state", label },
|
||||
{ "facets", its.indices.size() },
|
||||
{ "area_mm2", its_surface_area(its) },
|
||||
{ "bbox", bbox_to_json(its_referenced_bbox(its)) },
|
||||
};
|
||||
}
|
||||
|
||||
// Iterate the states relevant to one FacetsAnnotation kind, collecting
|
||||
// non-empty entries. Empty layer → {"empty": true}. `n_facets_out` is the
|
||||
// running total of painted facets — bumped for the summary.
|
||||
json inspect_layer(const ModelVolume &mv, const FacetsAnnotation &fa,
|
||||
const std::vector<std::pair<EnforcerBlockerType, std::string>> &states,
|
||||
size_t &n_facets_out)
|
||||
{
|
||||
if (fa.empty())
|
||||
return { { "empty", true } };
|
||||
|
||||
json entries = json::array();
|
||||
for (const auto &st : states) {
|
||||
if (!fa.has_facets(mv, st.first))
|
||||
continue;
|
||||
indexed_triangle_set its = fa.get_facets_strict(mv, st.first);
|
||||
if (its.indices.empty())
|
||||
continue;
|
||||
n_facets_out += its.indices.size();
|
||||
entries.push_back(state_entry(st.second, its));
|
||||
}
|
||||
return {
|
||||
{ "empty", entries.empty() },
|
||||
{ "states", std::move(entries) },
|
||||
};
|
||||
}
|
||||
|
||||
const std::vector<std::pair<EnforcerBlockerType, std::string>> &supports_states()
|
||||
{
|
||||
static const std::vector<std::pair<EnforcerBlockerType, std::string>> s = {
|
||||
{ EnforcerBlockerType::ENFORCER, "ENFORCER" },
|
||||
{ EnforcerBlockerType::BLOCKER, "BLOCKER" },
|
||||
};
|
||||
return s;
|
||||
}
|
||||
|
||||
const std::vector<std::pair<EnforcerBlockerType, std::string>> &fuzzy_states()
|
||||
{
|
||||
// FUZZY_SKIN is an enum alias for ENFORCER; the layer is single-state.
|
||||
static const std::vector<std::pair<EnforcerBlockerType, std::string>> s = {
|
||||
{ EnforcerBlockerType::FUZZY_SKIN, "FUZZY_SKIN" },
|
||||
};
|
||||
return s;
|
||||
}
|
||||
|
||||
const std::vector<std::pair<EnforcerBlockerType, std::string>> &mmu_states()
|
||||
{
|
||||
static std::vector<std::pair<EnforcerBlockerType, std::string>> s = []{
|
||||
std::vector<std::pair<EnforcerBlockerType, std::string>> v;
|
||||
for (int i = 1; i <= int(EnforcerBlockerType::ExtruderMax); ++i)
|
||||
v.emplace_back(EnforcerBlockerType(i), "extruder_" + std::to_string(i));
|
||||
return v;
|
||||
}();
|
||||
return s;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void inspect_to_json(const Model &model, const std::vector<std::string> &source_paths,
|
||||
std::ostream &out)
|
||||
{
|
||||
json root;
|
||||
root["sources"] = source_paths;
|
||||
root["frame"] = "mesh_local";
|
||||
root["note"] = "Coordinates are mesh-local (each volume's own frame). "
|
||||
"Paint gizmos operate in this frame.";
|
||||
|
||||
json objects = json::array();
|
||||
size_t total_objects = 0, total_volumes = 0, total_painted = 0, total_facets = 0;
|
||||
|
||||
for (size_t oi = 0; oi < model.objects.size(); ++oi) {
|
||||
const ModelObject *mo = model.objects[oi];
|
||||
if (!mo) continue;
|
||||
++total_objects;
|
||||
|
||||
json obj;
|
||||
obj["index"] = oi;
|
||||
obj["name"] = mo->name;
|
||||
|
||||
json volumes = json::array();
|
||||
for (size_t vi = 0; vi < mo->volumes.size(); ++vi) {
|
||||
const ModelVolume *mv = mo->volumes[vi];
|
||||
if (!mv) continue;
|
||||
++total_volumes;
|
||||
|
||||
const indexed_triangle_set &its = mv->mesh().its;
|
||||
json vol;
|
||||
vol["index"] = vi;
|
||||
vol["name"] = mv->name;
|
||||
vol["n_facets"] = its.indices.size();
|
||||
vol["is_model_part"] = mv->is_model_part();
|
||||
vol["bbox_mesh_local"] = bbox_to_json(bounding_box(its));
|
||||
|
||||
size_t vol_painted = 0;
|
||||
json paints;
|
||||
paints["supports"] = inspect_layer(*mv, mv->supported_facets,
|
||||
supports_states(), vol_painted);
|
||||
paints["seam"] = inspect_layer(*mv, mv->seam_facets,
|
||||
supports_states(), vol_painted);
|
||||
paints["mmu_segmentation"] = inspect_layer(*mv, mv->mmu_segmentation_facets,
|
||||
mmu_states(), vol_painted);
|
||||
paints["fuzzy_skin"] = inspect_layer(*mv, mv->fuzzy_skin_facets,
|
||||
fuzzy_states(), vol_painted);
|
||||
vol["paints"] = std::move(paints);
|
||||
vol["painted_facets_total"] = vol_painted;
|
||||
|
||||
if (vol_painted > 0) ++total_painted;
|
||||
total_facets += vol_painted;
|
||||
|
||||
volumes.push_back(std::move(vol));
|
||||
}
|
||||
obj["volumes"] = std::move(volumes);
|
||||
objects.push_back(std::move(obj));
|
||||
}
|
||||
root["objects"] = std::move(objects);
|
||||
root["summary"] = {
|
||||
{ "objects", total_objects },
|
||||
{ "volumes", total_volumes },
|
||||
{ "volumes_with_paint", total_painted },
|
||||
{ "painted_facets_total", total_facets },
|
||||
};
|
||||
|
||||
// Object names and file paths are arbitrary bytes, and dump() throws on invalid
|
||||
// UTF-8 by default. Replace such sequences with U+FFFD so the output is always
|
||||
// valid JSON rather than an exception out of the CLI.
|
||||
out << root.dump(2, ' ', false, json::error_handler_t::replace) << std::endl;
|
||||
}
|
||||
|
||||
} // namespace PaintCLI
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,31 @@
|
||||
// PaintCLI.hpp — CLI paint-inspection primitives.
|
||||
//
|
||||
// Backs the --inspect-paint CLI action. Reads the per-facet enforcer /
|
||||
// blocker / extruder / fuzzy-skin state that OrcaSlicer stores on every
|
||||
// ModelVolume (supports, seam, MMU color, fuzzy-skin) and emits a
|
||||
// structured JSON summary — facet count, surface area, and mesh-local
|
||||
// bounding box per state — so CI / scripted / AI tooling can reason
|
||||
// about existing paint on a .3mf without opening the GUI.
|
||||
//
|
||||
// Coordinates are mesh-local (each volume's own frame), matching the
|
||||
// frame that the paint gizmos operate in.
|
||||
#ifndef slic3r_PaintCLI_hpp_
|
||||
#define slic3r_PaintCLI_hpp_
|
||||
|
||||
#include <iosfwd>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
class Model;
|
||||
|
||||
namespace PaintCLI {
|
||||
|
||||
// `source_paths` lists every input file; the CLI merges them into one Model.
|
||||
void inspect_to_json(const Model &model, const std::vector<std::string> &source_paths,
|
||||
std::ostream &out);
|
||||
|
||||
} // namespace PaintCLI
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
@@ -1711,7 +1711,7 @@ void PresetUpdater::priv::check_new_vendors(const std::set<std::string>& system_
|
||||
GUI::wxGetApp().plater()->get_notification_manager()->push_notification(
|
||||
GUI::NotificationType::PresetUpdateFinished,
|
||||
GUI::NotificationManager::NotificationLevel::ImportantNotificationLevel,
|
||||
_u8L("Configuration package: ") + vendor_id + _u8L(" updated to ") + cur_ver.to_string());
|
||||
Slic3r::format(_u8L("Configuration package: %1% updated to %2%"), vendor_id, cur_ver.to_string()));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1806,7 +1806,7 @@ PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3
|
||||
->get_notification_manager()
|
||||
->push_notification(GUI::NotificationType::PresetUpdateFinished,
|
||||
GUI::NotificationManager::NotificationLevel::ImportantNotificationLevel,
|
||||
_u8L("Configuration package: ") + b + _u8L(" updated to ") + cur_ver.to_string());
|
||||
Slic3r::format(_u8L("Configuration package: %1% updated to %2%"), b, cur_ver.to_string()));
|
||||
}
|
||||
return R_UPDATE_INSTALLED;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user