mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-07 15:51:08 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e027f8b225 |
@@ -295,8 +295,6 @@ src/slic3r/GUI/CAD/DesignSketchTool.cpp
|
||||
src/slic3r/GUI/CAD/SketchInlineEditor.cpp
|
||||
src/slic3r/GUI/CAD/DesignOffer.hpp
|
||||
src/slic3r/GUI/CAD/DesignTextDialog.cpp
|
||||
src/slic3r/GUI/Gizmos/GLGizmoPrimitive.cpp
|
||||
src/slic3r/GUI/Gizmos/GLGizmoSketch.cpp
|
||||
src/slic3r/GUI/KeyChord.cpp
|
||||
src/slic3r/GUI/Shortcuts.cpp
|
||||
src/libslic3r/CAD/CadDocument.cpp
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<svg width="35" height="35" viewBox="0 0 35 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.0007 3.15607L5.26748 9.97446C5.0759 10.0857 4.8723 10.2988 4.71546 10.5713C4.55862 10.8438 4.47656 11.127 4.47656 11.349V24.4715C4.47656 24.6929 4.55879 24.9752 4.71606 25.2472C4.87328 25.5192 5.07737 25.7322 5.26956 25.8438L17.1143 32.6992C17.3061 32.8102 17.5904 32.8795 17.902 32.8782C18.2136 32.8768 18.4968 32.8049 18.6868 32.6927L30.2729 25.8523C30.4645 25.739 30.6677 25.524 30.8242 25.2501C30.9806 24.9761 31.0625 24.6919 31.0625 24.4693V11.2435C31.0625 11.0216 30.9805 10.7392 30.8238 10.4679C30.667 10.1966 30.4635 9.98491 30.2715 9.87455L18.582 3.15275C18.3912 3.04273 18.1054 2.97271 17.7915 2.97339C17.4777 2.97408 17.192 3.045 17.0007 3.15607ZM6.3957 11.3642C6.53284 11.1245 6.83831 11.0414 7.07799 11.1785L16.9733 16.8405C17.3553 17.0587 17.6738 17.4171 17.8953 17.7985C18.1169 18.1801 18.2703 18.6338 18.2717 19.0733L18.2893 30.4075C18.2897 30.6837 18.0662 30.9079 17.7901 30.9083C17.5139 30.9087 17.2897 30.6852 17.2893 30.4091L17.2717 19.0767C17.2708 18.8547 17.1879 18.5717 17.0305 18.3007C16.8729 18.0294 16.6691 17.8183 16.4776 17.709L6.58136 12.0465C6.34168 11.9093 6.25855 11.6039 6.3957 11.3642Z" fill="#4db6ac"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.0809 2.28613C18.7002 2.0665 18.2304 1.97243 17.7893 1.9734C17.3481 1.97436 16.8787 2.07053 16.4985 2.29133L4.76515 9.10978C4.38527 9.33044 4.06884 9.69007 3.84874 10.0725C3.6286 10.455 3.47656 10.9093 3.47656 11.349V24.4715C3.47656 24.9118 3.62953 25.3658 3.85033 25.7477C4.07118 26.1297 4.38804 26.4882 4.76735 26.7085L16.6135 33.5647C16.9954 33.7857 17.4651 33.8801 17.9064 33.8782C18.3475 33.8762 18.8161 33.7778 19.1954 33.5537L30.7814 26.7133C31.1591 26.4902 31.4737 26.1292 31.6925 25.746C31.9114 25.3627 32.0625 24.9084 32.0625 24.4693V11.2435C32.0625 10.8037 31.9104 10.3497 31.6896 9.96764C31.4689 9.58563 31.1515 9.22695 30.7698 9.00759L19.0809 2.28613ZM29.6625 11.8015C29.786 12.0485 29.6859 12.3489 29.4389 12.4723L20.3956 16.994C20.1486 17.1175 19.8483 17.0174 19.7248 16.7704C19.6013 16.5234 19.7014 16.2231 19.9484 16.0996L28.9917 11.5779C29.2387 11.4544 29.539 11.5545 29.6625 11.8015Z" fill="#bdbdbd"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -1,8 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
|
||||
<!-- Pencil body -->
|
||||
<path d="M28.5 6.5l5 5L14 31l-5.5 1.5L10 27 28.5 6.5z" style="fill:#009688"/>
|
||||
<!-- Pencil tip (dark) -->
|
||||
<path d="M8.5 28.5l1.5 1.5 4.5-1L28.5 15l-3.5-3.5L8.5 28z" style="fill:#2b3436"/>
|
||||
<!-- Pencil line -->
|
||||
<line x1="10" y1="26" x2="25" y2="11" stroke="#2b3436" stroke-width="2"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 406 B |
@@ -1,8 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
|
||||
<!-- Pencil body (dark mode colors) -->
|
||||
<path d="M28.5 6.5l5 5L14 31l-5.5 1.5L10 27 28.5 6.5z" style="fill:#4db6ac"/>
|
||||
<!-- Pencil tip -->
|
||||
<path d="M8.5 28.5l1.5 1.5 4.5-1L28.5 15l-3.5-3.5L8.5 28z" style="fill:#e0e0e0"/>
|
||||
<!-- Pencil line -->
|
||||
<line x1="10" y1="26" x2="25" y2="11" stroke="#e0e0e0" stroke-width="2"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 418 B |
@@ -62,6 +62,7 @@
|
||||
#include <TopoDS_Face.hxx>
|
||||
#include <TopAbs.hxx>
|
||||
#include <TopoDS_Shell.hxx>
|
||||
#include <TopoDS_Solid.hxx>
|
||||
#include <TopoDS_Compound.hxx> // multi-body: compound of bodies for display/compat
|
||||
#include <BRep_Builder.hxx>
|
||||
#include <TopAbs_Orientation.hxx> // outward-normal orientation for face-extrude
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
#include <NCollection_IndexedMap.hxx>
|
||||
#include <NCollection_List.hxx>
|
||||
#include <Poly_Triangulation.hxx>
|
||||
#include <gp_Ax2.hxx>
|
||||
#include <gp_Dir.hxx>
|
||||
#include <gp_Pnt.hxx>
|
||||
#include <BRepGProp.hxx>
|
||||
@@ -53,6 +52,7 @@
|
||||
#include <BRepBuilderAPI_MakeSolid.hxx>
|
||||
#include <BRep_Builder.hxx>
|
||||
#include <TopoDS_Shell.hxx>
|
||||
#include <TopoDS_Solid.hxx>
|
||||
#include <TopoDS_Vertex.hxx>
|
||||
#include <ShapeUpgrade_UnifySameDomain.hxx>
|
||||
#include <array>
|
||||
@@ -244,30 +244,6 @@ TopoDS_Shape GeometryEngine::mesh_to_brep(const indexed_triangle_set& its,
|
||||
return shape;
|
||||
}
|
||||
|
||||
// ---- Primitive creation ----
|
||||
|
||||
TopoDS_Solid GeometryEngine::make_primitive(const PrimitiveParams& params)
|
||||
{
|
||||
switch (params.type) {
|
||||
case PrimitiveType::Box:
|
||||
return BRepPrimAPI_MakeBox(gp_Pnt(-params.box_w/2, -params.box_d/2, 0),
|
||||
params.box_w, params.box_d, params.box_h).Solid();
|
||||
case PrimitiveType::Cylinder:
|
||||
return BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(0,0,0), gp_Dir(0,0,1)),
|
||||
params.cyl_radius, params.cyl_height).Solid();
|
||||
case PrimitiveType::Sphere:
|
||||
return BRepPrimAPI_MakeSphere(gp_Pnt(0,0,params.sph_radius), params.sph_radius).Solid();
|
||||
case PrimitiveType::Cone:
|
||||
return BRepPrimAPI_MakeCone(gp_Ax2(gp_Pnt(0,0,0), gp_Dir(0,0,1)),
|
||||
params.cone_r1, params.cone_r2, params.cone_height).Solid();
|
||||
case PrimitiveType::Torus:
|
||||
return BRepPrimAPI_MakeTorus(gp_Ax2(gp_Pnt(0,0,params.torus_r2), gp_Dir(0,0,1)),
|
||||
params.torus_r1, params.torus_r2).Solid();
|
||||
default:
|
||||
return BRepPrimAPI_MakeBox(gp_Pnt(-10,-10,0), 20,20,20).Solid();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Face classification ----
|
||||
|
||||
FaceGroup GeometryEngine::classify_face(const TopoDS_Face& face, const TopoDS_Shape& /*solid*/)
|
||||
@@ -545,19 +521,6 @@ GeometryEngine::MassProps GeometryEngine::mass_properties(const TopoDS_Shape& sh
|
||||
return p;
|
||||
}
|
||||
|
||||
std::string GeometryEngine::primitive_name(PrimitiveType type)
|
||||
{
|
||||
switch (type) {
|
||||
// TRN Default name of an object created from the box primitive shape.
|
||||
case PrimitiveType::Box: return _u8L("Box");
|
||||
case PrimitiveType::Cylinder: return _u8L("Cylinder");
|
||||
case PrimitiveType::Sphere: return _u8L("Sphere");
|
||||
case PrimitiveType::Cone: return _u8L("Cone");
|
||||
case PrimitiveType::Torus: return _u8L("Torus");
|
||||
default: return _u8L("Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Topology accessors ----
|
||||
|
||||
int GeometryEngine::face_count(const TopoDS_Shape& shape)
|
||||
|
||||
@@ -4,15 +4,8 @@
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
|
||||
#include <BRepPrimAPI_MakeBox.hxx>
|
||||
#include <BRepPrimAPI_MakeCylinder.hxx>
|
||||
#include <BRepPrimAPI_MakeSphere.hxx>
|
||||
#include <BRepPrimAPI_MakeCone.hxx>
|
||||
#include <BRepPrimAPI_MakeTorus.hxx>
|
||||
#include <TopoDS_Shape.hxx>
|
||||
#include <array>
|
||||
#include <gp_Ax2.hxx>
|
||||
#include <TopoDS_Solid.hxx>
|
||||
#include <TopoDS_Face.hxx>
|
||||
#include <TopoDS_Edge.hxx>
|
||||
#include <vector>
|
||||
@@ -20,44 +13,11 @@
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
enum class PrimitiveType { Box, Cylinder, Sphere, Cone, Torus, COUNT };
|
||||
enum class DressUpType { Fillet, Chamfer };
|
||||
enum class FaceGroup { Top, Bottom, Lateral, All };
|
||||
|
||||
struct PrimitiveParams {
|
||||
PrimitiveType type{PrimitiveType::Box};
|
||||
double box_w{20}, box_h{20}, box_d{20};
|
||||
double cyl_radius{10}, cyl_height{20};
|
||||
double sph_radius{10};
|
||||
double cone_r1{10}, cone_r2{5}, cone_height{20};
|
||||
double torus_r1{10}, torus_r2{3};
|
||||
|
||||
// Dress-up
|
||||
bool dressup_enabled{false};
|
||||
DressUpType dressup_type{DressUpType::Fillet};
|
||||
FaceGroup dressup_faces{FaceGroup::All};
|
||||
double dressup_radius{1.0}; // fillet radius
|
||||
double dressup_chamfer_dist{1.0}; // chamfer distance (symmetric)
|
||||
|
||||
// Mesh quality — the Design tab's own density (CadDocument::linear_deflection), so a
|
||||
// primitive and the same body modelled in the Design tab reach the screen alike.
|
||||
double linear_deflection{0.003};
|
||||
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).
|
||||
@@ -132,7 +92,6 @@ public:
|
||||
static TriangleMesh tessellate(const TopoDS_Shape& shape,
|
||||
double linear_deflection = 0.003,
|
||||
double angular_deflection = 0.5);
|
||||
static std::string primitive_name(PrimitiveType type);
|
||||
|
||||
// Topology accessors for in-viewport face/edge picking (Design tab). Face index is the
|
||||
// TopExp_Explorer(shape, TopAbs_FACE) ordinal — identical to SketchEngine::tessellate's
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/CAD/GeometryEngine.hpp"
|
||||
|
||||
#include <gp_Pln.hxx>
|
||||
#include <gp_Ax1.hxx>
|
||||
@@ -211,30 +210,6 @@ ConstraintPlan plan_entity_constraint(const std::vector<SketchEntity>& ents,
|
||||
bool solve_sketch_entities(std::vector<SketchEntity>& entities,
|
||||
const std::vector<SketchEntityConstraintDef>& constraints);
|
||||
|
||||
struct SketchParams {
|
||||
// Extrude/Revolve
|
||||
double extrude_len{10}; bool extrude_sym{false}; double extrude_taper{0};
|
||||
double revolve_deg{360};
|
||||
bool is_pocket{false}; // cut into selected object instead of new
|
||||
|
||||
// Dress-up
|
||||
bool dressup_enabled{false};
|
||||
DressUpType dressup_type{DressUpType::Fillet};
|
||||
FaceGroup dressup_faces{FaceGroup::All};
|
||||
double dressup_radius{1.0};
|
||||
double dressup_chamfer_dist{1.0};
|
||||
|
||||
// Mesh
|
||||
double linear_deflection{0.01};
|
||||
|
||||
template<class Archive>
|
||||
void serialize(Archive& ar) {
|
||||
ar(extrude_len, extrude_sym, extrude_taper, revolve_deg, is_pocket,
|
||||
dressup_enabled, dressup_type, dressup_faces, dressup_radius, dressup_chamfer_dist,
|
||||
linear_deflection);
|
||||
}
|
||||
};
|
||||
|
||||
class SketchEngine
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -849,11 +849,6 @@ if (SLIC3R_CAD)
|
||||
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 ()
|
||||
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
#include "GLGizmoPrimitive.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/ImGuiWrapper.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/NotificationManager.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include <string>
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoBase.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp"
|
||||
#include "libslic3r/CAD/GeometryEngine.hpp"
|
||||
#include <TopoDS_Solid.hxx>
|
||||
#include <TopoDS_Shape.hxx>
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include <utility>
|
||||
#include <algorithm>
|
||||
#include <imgui.h>
|
||||
|
||||
#ifndef IMGUI_DEFINE_MATH_OPERATORS
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#endif
|
||||
#include <imgui/imgui_internal.h>
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
|
||||
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"); }
|
||||
// Part of the experimental CAD feature: built into every CAD build, but offered in the Prepare
|
||||
// toolbar only when that feature is switched on in Preferences — switched off, Prepare must be
|
||||
// exactly what it is without it.
|
||||
bool GLGizmoPrimitive::on_is_activable() const { return wxGetApp().is_enable_cad_feature(); }
|
||||
bool GLGizmoPrimitive::on_is_selectable() const { return wxGetApp().is_enable_cad_feature(); }
|
||||
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(_u8L("Shape").c_str(), ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
const std::string names_s[] = {_u8L("Box"), _u8L("Cylinder"), _u8L("Sphere"), _u8L("Cone"), _u8L("Torus")};
|
||||
const char* names[] = {names_s[0].c_str(), names_s[1].c_str(), names_s[2].c_str(), names_s[3].c_str(), names_s[4].c_str()};
|
||||
int cur = (int)m_params.type;
|
||||
if (ImGui::Combo("##type", &cur, names, (int)PrimitiveType::COUNT)) {
|
||||
m_params.type = (PrimitiveType)cur;
|
||||
m_preview_dirty = true;
|
||||
}
|
||||
// TRN Primitive gizmo: label in front of the preset cube size buttons
|
||||
ImGui::TextUnformatted(_u8L("Quick:").c_str());
|
||||
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(_u8L("Dimensions").c_str(), 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(_u8L("Width (X)").c_str(), m_params.box_w);
|
||||
dim(_u8L("Depth (Y)").c_str(), m_params.box_d);
|
||||
dim(_u8L("Height (Z)").c_str(), m_params.box_h);
|
||||
break;
|
||||
case PrimitiveType::Cylinder:
|
||||
dim(_u8L("Radius").c_str(), m_params.cyl_radius);
|
||||
dim(_u8L("Height").c_str(), m_params.cyl_height);
|
||||
break;
|
||||
case PrimitiveType::Sphere:
|
||||
dim(_u8L("Radius").c_str(), m_params.sph_radius);
|
||||
break;
|
||||
case PrimitiveType::Cone:
|
||||
// TRN Primitive gizmo: bottom radius of a cone ("Top R" is the top radius)
|
||||
dim(_u8L("Bottom R").c_str(), m_params.cone_r1);
|
||||
dim(_u8L("Top R").c_str(), m_params.cone_r2);
|
||||
dim(_u8L("Height").c_str(), m_params.cone_height);
|
||||
break;
|
||||
case PrimitiveType::Torus:
|
||||
// TRN Primitive gizmo: major radius of a torus ("Minor R" is the tube radius)
|
||||
dim(_u8L("Major R").c_str(), m_params.torus_r1);
|
||||
dim(_u8L("Minor R").c_str(), m_params.torus_r2, 0.1, 1.0);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
if (ImGui::CollapsingHeader(_u8L("Fillet / Chamfer").c_str())) {
|
||||
ImGui::Checkbox(_u8L("Enable").c_str(), &m_params.dressup_enabled);
|
||||
if (m_params.dressup_enabled) {
|
||||
const std::string dn_s[] = {_u8L("Fillet"), _u8L("Chamfer")};
|
||||
const char* dn[] = {dn_s[0].c_str(), dn_s[1].c_str()};
|
||||
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; }
|
||||
const std::string fn_s[] = {_u8L("All edges"), _u8L("Top edges"), _u8L("Bottom edges"), _u8L("Lateral edges")};
|
||||
const char* fn[] = {fn_s[0].c_str(), fn_s[1].c_str(), fn_s[2].c_str(), fn_s[3].c_str()};
|
||||
int fg = (int)m_params.dressup_faces;
|
||||
ImGui::SetNextItemWidth(140);
|
||||
if (ImGui::Combo(_u8L("Edges").c_str(), &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(_u8L("Radius").c_str(), &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(_u8L("Distance").c_str(), &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(_u8L("Quality").c_str())) {
|
||||
ImGui::SetNextItemWidth(130);
|
||||
if (ImGui::InputDouble(_u8L("Mesh resolution").c_str(), &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(_u8L("Add Shape").c_str(), {-1, 28}))
|
||||
apply_primitive();
|
||||
|
||||
if (ImGui::Button(_u8L("Close").c_str(), {-1, 0}))
|
||||
m_parent.reset_all_gizmos();
|
||||
|
||||
GizmoImguiEnd();
|
||||
ImGuiWrapper::pop_toolbar_style();
|
||||
}
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
@@ -1,48 +0,0 @@
|
||||
#ifndef slic3r_GLGizmoPrimitive_hpp_
|
||||
#define slic3r_GLGizmoPrimitive_hpp_
|
||||
|
||||
#include "GLGizmoBase.hpp"
|
||||
#include "GLGizmosCommon.hpp"
|
||||
#include "libslic3r/CAD/GeometryEngine.hpp"
|
||||
#include <string>
|
||||
#include <wx/event.h>
|
||||
#include <cereal/archives/binary.hpp>
|
||||
#include "libslic3r/TriangleMesh.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;
|
||||
bool on_is_selectable() 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_
|
||||
@@ -1,488 +0,0 @@
|
||||
#include "GLGizmoSketch.hpp"
|
||||
#include "slic3r/GUI/GLCanvas3D.hpp"
|
||||
#include "slic3r/GUI/ImGuiWrapper.hpp"
|
||||
#include "slic3r/GUI/Plater.hpp"
|
||||
#include "slic3r/GUI/NotificationManager.hpp"
|
||||
#include "slic3r/GUI/format.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include <BRepBuilderAPI_MakeFace.hxx>
|
||||
#include <BRepPrimAPI_MakeRevol.hxx>
|
||||
#include <BRepAlgoAPI_Fuse.hxx>
|
||||
#include <string>
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoBase.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp"
|
||||
#include "libslic3r/CAD/SketchEngine.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include <cmath>
|
||||
#include <math.h>
|
||||
#include <imgui.h>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
#include <stdexcept>
|
||||
#include <gp_Pnt.hxx>
|
||||
#include <gp_Dir.hxx>
|
||||
#include "libslic3r/CAD/GeometryEngine.hpp"
|
||||
#include <algorithm>
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include <utility>
|
||||
#include <exception>
|
||||
|
||||
#ifndef IMGUI_DEFINE_MATH_OPERATORS
|
||||
#define IMGUI_DEFINE_MATH_OPERATORS
|
||||
#endif
|
||||
#include <imgui/imgui_internal.h>
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/Selection.hpp"
|
||||
|
||||
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"); }
|
||||
// Part of the experimental CAD feature: built into every CAD build, but offered in the Prepare
|
||||
// toolbar only when that feature is switched on in Preferences — switched off, Prepare must be
|
||||
// exactly what it is without it.
|
||||
bool GLGizmoSketch::on_is_activable() const { return wxGetApp().is_enable_cad_feature(); }
|
||||
bool GLGizmoSketch::on_is_selectable() const { return wxGetApp().is_enable_cad_feature(); }
|
||||
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(_u8L("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(_u8L("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(_u8L("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(_u8L("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(_u8L("Profile").c_str(), ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
const std::string names_s[] = {_u8L("Line"), _u8L("Rectangle"), _u8L("Circle"), _u8L("Polygon")};
|
||||
const char* names[] = {names_s[0].c_str(), names_s[1].c_str(), names_s[2].c_str(), names_s[3].c_str()};
|
||||
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", _u8L("Start new profile (for holes)").c_str());
|
||||
|
||||
if (m_tool == SketchTool::Rectangle) {
|
||||
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble(_u8L("Width").c_str(), &m_rect_w,1,10,"%.0f")) build_preset_profile();
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble(_u8L("Height").c_str(), &m_rect_h,1,10,"%.0f")) build_preset_profile();
|
||||
} else if (m_tool == SketchTool::Circle) {
|
||||
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble(_u8L("Radius").c_str(), &m_circle_r,1,5,"%.0f")) build_preset_profile();
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(80); if (ImGui::SliderInt(_u8L("Segments").c_str(), &m_circle_seg,8,64)) build_preset_profile();
|
||||
} else if (m_tool == SketchTool::Polygon) {
|
||||
ImGui::SetNextItemWidth(80); if (ImGui::SliderInt(_u8L("Sides").c_str(), &m_poly_sides,3,12)) build_preset_profile();
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble((_u8L("Radius") + "##poly").c_str(), &m_poly_r,1,5,"%.0f")) build_preset_profile();
|
||||
} else {
|
||||
ImGui::Text("%s", _u8L("Click on canvas to draw").c_str());
|
||||
}
|
||||
|
||||
ImGui::Checkbox(_u8L("Snap to grid").c_str(), &m_snap_grid);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(80); ImGui::InputFloat(_u8L("Grid step").c_str(), &m_grid_step, 1, 5, "%.0f mm");
|
||||
|
||||
draw_canvas();
|
||||
|
||||
if (!m_profiles.empty()) {
|
||||
ImGui::Text("%s: %zu", _u8L("Profiles").c_str(), 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 std::string label = outer ? _u8L("Outer") : _u8L("Hole");
|
||||
const std::string state = p.closed ? _u8L("closed") : std::string();
|
||||
// TRN Sketch gizmo profile list: "Outer" or "Hole", profile number, point count, "closed" or nothing
|
||||
ImGui::TextColored(col, "%s", GUI::format(_u8L("%1% %2%: %3% points %4%"), label, i + 1, p.points.size(), state).c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("X")) delete_profile(i);
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
bool is_revolve = false;
|
||||
bool has_sel = false;
|
||||
|
||||
if (ImGui::CollapsingHeader(_u8L("Operation").c_str(), ImGuiTreeNodeFlags_DefaultOpen)) {
|
||||
static int pi = 0;
|
||||
const std::string plane_names[] = {_u8L("XY (Top)"), _u8L("XZ (Front)"), _u8L("YZ (Side)")};
|
||||
const char* planes[] = {plane_names[0].c_str(), plane_names[1].c_str(), plane_names[2].c_str()};
|
||||
if (ImGui::Combo(_u8L("Plane").c_str(), &pi, planes, 3))
|
||||
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(_u8L("Revolve deg").c_str(), &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", _u8L("Set to 0 for extrude, >0 for revolve").c_str());
|
||||
|
||||
if (!is_revolve) {
|
||||
ImGui::SetNextItemWidth(100);
|
||||
ImGui::InputDouble(_u8L("Length").c_str(), &m_sp.extrude_len, 0.5, 5, "%.1f mm");
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox(_u8L("Symmetric").c_str(), &m_sp.extrude_sym);
|
||||
}
|
||||
|
||||
has_sel = !m_parent.get_selection().is_empty();
|
||||
if (has_sel) {
|
||||
if (ImGui::Checkbox(_u8L("Pocket (cut)").c_str(), &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(_u8L("Fillet / Chamfer").c_str())) {
|
||||
ImGui::Checkbox(_u8L("Enable").c_str(), &m_sp.dressup_enabled);
|
||||
if (m_sp.dressup_enabled) {
|
||||
const std::string dn_s[] = {_u8L("Fillet"), _u8L("Chamfer")};
|
||||
const char* dn[] = {dn_s[0].c_str(), dn_s[1].c_str()};
|
||||
int du = (int)m_sp.dressup_type;
|
||||
ImGui::SetNextItemWidth(100);
|
||||
if (ImGui::Combo("##dtype", &du, dn, 2)) m_sp.dressup_type = (DressUpType)du;
|
||||
const std::string fn_s[] = {_u8L("All edges"), _u8L("Top edges"), _u8L("Bottom edges"), _u8L("Lateral edges")};
|
||||
const char* fn[] = {fn_s[0].c_str(), fn_s[1].c_str(), fn_s[2].c_str(), fn_s[3].c_str()};
|
||||
int fg = (int)m_sp.dressup_faces;
|
||||
ImGui::SetNextItemWidth(140);
|
||||
ImGui::Combo(_u8L("Edges").c_str(), &fg, fn, 4); m_sp.dressup_faces = (FaceGroup)fg;
|
||||
ImGui::SetNextItemWidth(100);
|
||||
if (m_sp.dressup_type == DressUpType::Fillet)
|
||||
ImGui::InputDouble(_u8L("Radius").c_str(), &m_sp.dressup_radius, 0.1, 1, "%.1f mm");
|
||||
else
|
||||
ImGui::InputDouble(_u8L("Distance").c_str(), &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}, "%s", wxString::Format(_L_PLURAL("%zu closed profile", "%zu closed profiles",
|
||||
unsigned(m_profiles.size())), m_profiles.size()).ToUTF8().data());
|
||||
else ImGui::TextColored({0.6f,0.6f,0.6f,1}, "%s", _u8L("Draw a closed profile to enable").c_str());
|
||||
|
||||
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(_u8L("Pocket (cut)").c_str(), ok)) apply_pocket();
|
||||
} else if (is_revolve) {
|
||||
if (btn(_u8L("Revolve").c_str(), ok)) apply_revolve();
|
||||
} else {
|
||||
if (btn(_u8L_CONTEXT("Extrude", "Design").c_str(), ok)) apply_extrude();
|
||||
}
|
||||
|
||||
if (ImGui::Button(_u8L("Clear all").c_str(), {-1,0})) clear_all();
|
||||
if (ImGui::Button(_u8L("Close").c_str(), {-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(_u8L("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, GUI::format(_u8L("Extrude: %1%"), 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(_u8L("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, GUI::format(_u8L("Revolve: %1%"), 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(_u8L("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(_u8L("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(_u8L("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(_u8L("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, _u8L("Pocket added (negative volume)").c_str());
|
||||
} catch (const std::exception& e) {
|
||||
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel, GUI::format(_u8L("Pocket: %1%"), e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace GUI
|
||||
} // namespace Slic3r
|
||||
@@ -1,81 +0,0 @@
|
||||
#ifndef slic3r_GLGizmoSketch_hpp_
|
||||
#define slic3r_GLGizmoSketch_hpp_
|
||||
|
||||
#include "GLGizmoBase.hpp"
|
||||
#include "GLGizmosCommon.hpp"
|
||||
#include "libslic3r/CAD/SketchEngine.hpp"
|
||||
#include <cereal/archives/binary.hpp>
|
||||
#include <TopoDS_Shape.hxx>
|
||||
#include <imgui/imgui.h>
|
||||
#include <string>
|
||||
#include <wx/event.h>
|
||||
#include <vector>
|
||||
#include "libslic3r/Point.hpp"
|
||||
|
||||
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;
|
||||
bool on_is_selectable() 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_
|
||||
@@ -50,10 +50,6 @@
|
||||
#include <wx/event.h>
|
||||
#include <optional>
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
#ifdef SLIC3R_CAD
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoPrimitive.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoSketch.hpp"
|
||||
#endif
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
|
||||
@@ -217,14 +213,6 @@ 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -270,14 +258,6 @@ 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.
|
||||
if (wxGetApp().is_enable_cad_feature()) {
|
||||
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++));
|
||||
@@ -1375,8 +1355,7 @@ GLGizmoBase* GLGizmosManager::get_current() const
|
||||
|
||||
GLGizmoBase* GLGizmosManager::get_gizmo(GLGizmosManager::EType type) const
|
||||
{
|
||||
// m_gizmos ends before the enum does when the CAD gizmos are not registered.
|
||||
return type < m_gizmos.size() ? m_gizmos[type].get() : nullptr;
|
||||
return ((type == Undefined) || m_gizmos.empty()) ? nullptr : m_gizmos[type].get();
|
||||
}
|
||||
|
||||
GLGizmosManager::EType GLGizmosManager::get_gizmo_from_name(const std::string& gizmo_name) const
|
||||
|
||||
@@ -102,12 +102,6 @@ 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,
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
#include <wx/app.h>
|
||||
#include <wx/defs.h>
|
||||
#include <wx/thread.h>
|
||||
#include <wx/timer.h>
|
||||
#include <wx/toplevel.h>
|
||||
#include <wx/event.h>
|
||||
#include <wx/progdlg.h>
|
||||
@@ -416,44 +415,6 @@ struct UiDockPanelHandle
|
||||
int id{0};
|
||||
};
|
||||
|
||||
// Polls until the plater is shown on screen, then runs the callback with it (null when the app
|
||||
// is closing instead) and deletes itself. A plugin loaded at startup opens its panes from
|
||||
// on_load, before the main window exists, and a pane added before that window is laid out is
|
||||
// sized against the unsized frame and keeps that width (AuiMgr::track_docked_size).
|
||||
class PlaterShownWaiter : public wxTimer
|
||||
{
|
||||
public:
|
||||
explicit PlaterShownWaiter(std::function<void(GUI::Plater*)> fn) : m_fn(std::move(fn)) {}
|
||||
|
||||
// True once the callback ran.
|
||||
bool try_run()
|
||||
{
|
||||
const bool closing = GUI::wxGetApp().is_closing();
|
||||
GUI::Plater* plater = closing ? nullptr : GUI::wxGetApp().plater();
|
||||
if (!closing && (plater == nullptr || !plater->IsShownOnScreen()))
|
||||
return false;
|
||||
Stop();
|
||||
m_fn(plater);
|
||||
// Off the timer callback's stack: wxGTK's timeout callback still reads the timer after Notify().
|
||||
GUI::wxGetApp().CallAfter([this]() { delete this; });
|
||||
return true;
|
||||
}
|
||||
|
||||
void Notify() override { try_run(); }
|
||||
|
||||
private:
|
||||
std::function<void(GUI::Plater*)> m_fn;
|
||||
};
|
||||
|
||||
void run_when_plater_shown(std::function<void(GUI::Plater*)> fn)
|
||||
{
|
||||
GUI::wxGetApp().CallAfter([fn = std::move(fn)]() mutable {
|
||||
auto* waiter = new PlaterShownWaiter(std::move(fn));
|
||||
if (!waiter->try_run())
|
||||
waiter->Start(100);
|
||||
});
|
||||
}
|
||||
|
||||
py::object ui_create_dock_panel(const std::string& html, const std::string& title, int width, int height,
|
||||
py::object on_message, py::object on_close, const std::string& dock)
|
||||
{
|
||||
@@ -473,13 +434,14 @@ py::object ui_create_dock_panel(const std::string& html, const std::string& titl
|
||||
const int new_id = UiRegistry::instance().reserve_id();
|
||||
UiRegistry::instance().bind(new_id, nullptr, plugin_key);
|
||||
|
||||
run_when_plater_shown([new_id, plugin_key, html, title, dock, w, h,
|
||||
msg_adapter = std::move(msg_adapter),
|
||||
close_holder = std::move(close_holder)](GUI::Plater* plater) mutable {
|
||||
GUI::wxGetApp().CallAfter([new_id, plugin_key, html, title, dock, w, h,
|
||||
msg_adapter = std::move(msg_adapter),
|
||||
close_holder = std::move(close_holder)]() mutable {
|
||||
if (!UiRegistry::instance().is_open(new_id))
|
||||
return;
|
||||
|
||||
if (plater == nullptr) {
|
||||
GUI::Plater* plater = GUI::wxGetApp().plater();
|
||||
if (plater == nullptr || GUI::wxGetApp().is_closing()) {
|
||||
UiRegistry::instance().remove(new_id);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user