mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 19:01:02 +00:00
Orca-Cad: port SnapOrca Design (parametric CAD tab) onto mainline OrcaSlicer
Grafts the sketch-first CAD environment from snaporca-cad onto the mainline OrcaSlicer/OrcaSlicer base (vs snaporca's Snapmaker/OrcaSlicer base): - 133 new files: CadDocument/SketchEngine/GeometryEngine/SketchConstraints/ SketchSolver/SketchInference/ThreadStandards + vendored libslvs solver; DesignPanel/DesignCanvas/DesignSketchTool/SketchInlineEditor GUI; GLGizmo Primitive/Sketch; 75 design icons; Catch2 tests. - Integration hooks ported to mainline's diverged versions: Design tab in MainFrame, embedded design viewport + sketch overlay + per-canvas chrome suppression in GLCanvas3D/PartPlate, gizmo registration, Plater accessors, CMake wiring (libslvs subdir, CAD sources, OCCT ModelingAlgorithms=ON). Structural integration complete; build verification pending. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
449a4cf9fc
commit
0f4060c0a9
@@ -0,0 +1,885 @@
|
||||
#include "DesignCanvas.hpp"
|
||||
|
||||
#include "SketchInlineEditor.hpp"
|
||||
#include "GLCanvas3D.hpp"
|
||||
#include "OpenGLManager.hpp"
|
||||
#include "3DBed.hpp"
|
||||
#include "GUI_App.hpp"
|
||||
#include "Plater.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "3DScene.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
|
||||
#include <wx/glcanvas.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/frame.h>
|
||||
#include <wx/stattext.h>
|
||||
#include <wx/toplevel.h>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
DesignCanvas::DesignCanvas(wxWindow* parent)
|
||||
: wxPanel()
|
||||
{
|
||||
if (!Create(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0))
|
||||
return;
|
||||
|
||||
m_canvas_widget = OpenGLManager::create_wxglcanvas(*this);
|
||||
if (m_canvas_widget == nullptr)
|
||||
return;
|
||||
|
||||
m_canvas = new GLCanvas3D(m_canvas_widget, m_bed);
|
||||
m_canvas->set_context(wxGetApp().init_glcontext(*m_canvas_widget));
|
||||
m_canvas->allow_multisample(OpenGLManager::can_multisample());
|
||||
m_canvas->set_config(wxGetApp().plater()->config());
|
||||
m_canvas->set_model(&m_model);
|
||||
// Reuse the editor's shared slicing process: GLCanvas3D::render() (via
|
||||
// _max_bounding_box) dereferences the process when canvas type == View3D.
|
||||
// Passing nullptr segfaults; this mirrors View3D/Preview/AssembleView.
|
||||
m_canvas->set_process(wxGetApp().plater()->get_background_process());
|
||||
m_canvas->set_type(GLCanvas3D::ECanvasType::CanvasView3D);
|
||||
|
||||
m_canvas->enable_picking(false); // viewport face/edge picking is custom (TODO)
|
||||
m_canvas->enable_moving(false);
|
||||
m_canvas->enable_gizmos(false);
|
||||
m_canvas->enable_selection(false); // stock volume selection unused; solid highlight is tree-driven
|
||||
m_canvas->enable_main_toolbar(false);
|
||||
m_canvas->enable_select_plate_toolbar(false);
|
||||
m_canvas->enable_assemble_view_toolbar(false);
|
||||
m_canvas->enable_separator_toolbar(false);
|
||||
m_canvas->enable_collapse_toolbar(false);
|
||||
m_canvas->enable_plate_chrome(false);
|
||||
m_canvas->enable_labels(false);
|
||||
|
||||
m_canvas->set_design_sketch_tool(&m_sketch_tool);
|
||||
m_sketch_tool.on_commit = [this](const SketchProfile& prof, const SketchPlane& pl) {
|
||||
if (m_on_sketch_commit) m_on_sketch_commit(prof, pl);
|
||||
if (m_canvas) m_canvas->set_as_dirty();
|
||||
if (m_canvas_widget) m_canvas_widget->Refresh();
|
||||
};
|
||||
m_sketch_tool.on_commit_entities = [this](const std::vector<SketchEntity>& ents,
|
||||
const std::vector<SketchEntityConstraintDef>& cons,
|
||||
const SketchPlane& pl) {
|
||||
if (m_on_sketch_entities_commit) m_on_sketch_entities_commit(ents, cons, pl);
|
||||
if (m_canvas) m_canvas->set_as_dirty();
|
||||
if (m_canvas_widget) m_canvas_widget->Refresh();
|
||||
};
|
||||
|
||||
// Onshape-style in-canvas value editor, floating over the GL canvas. The tool hands
|
||||
// us a screen pixel (device px) + a commit/cancel pair; we convert to logical client
|
||||
// px and wrap the callbacks so each one re-solves and re-renders the viewport.
|
||||
m_inline_editor = std::make_unique<SketchInlineEditor>(m_canvas_widget);
|
||||
m_sketch_tool.on_inline_edit = [this](wxPoint screen_px, double current,
|
||||
std::function<void(double)> commit,
|
||||
std::function<void()> cancel) {
|
||||
if (!m_inline_editor) { if (cancel) cancel(); return; }
|
||||
// The tool hands us canvas device px; convert to logical client px, then to
|
||||
// absolute screen coords for the floating editor frame.
|
||||
const double s = m_canvas_widget ? m_canvas_widget->GetContentScaleFactor() : 1.0;
|
||||
const wxPoint client_pt(int(screen_px.x / s), int(screen_px.y / s));
|
||||
const wxPoint scr = m_canvas_widget ? m_canvas_widget->ClientToScreen(client_pt) : client_pt;
|
||||
// Freeze the sketch tool while the field is open so a stray click/move on the GL
|
||||
// canvas can't draw under the floating editor; released on commit or cancel.
|
||||
m_sketch_tool.set_inline_busy(true);
|
||||
m_inline_editor->open(scr, current,
|
||||
[this, commit](double v) {
|
||||
m_sketch_tool.set_inline_busy(false);
|
||||
if (commit) commit(v);
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
},
|
||||
[this, cancel]() {
|
||||
m_sketch_tool.set_inline_busy(false);
|
||||
if (cancel) cancel();
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
});
|
||||
};
|
||||
// Let the tool force-close the field (keep-as-drawn) — polyline right-click/double-click
|
||||
// ends the chain even while a per-segment value field is open.
|
||||
m_sketch_tool.on_inline_dismiss = [this]() {
|
||||
if (m_inline_editor) m_inline_editor->cancel();
|
||||
};
|
||||
|
||||
// Bottom-right viewport HUD: a borderless, non-focusable float label showing the active
|
||||
// tool's current values. Top-level (a child widget is hidden by the GL surface, same as
|
||||
// the inline editor). Fed every frame by the tool's on_readout; empty text hides it.
|
||||
{
|
||||
wxWindow* top = wxGetTopLevelParent(m_canvas_widget);
|
||||
m_hud = new wxFrame(top, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize,
|
||||
wxFRAME_NO_TASKBAR | wxBORDER_NONE | wxFRAME_FLOAT_ON_PARENT |
|
||||
wxSTAY_ON_TOP | wxTRANSPARENT_WINDOW);
|
||||
m_hud->SetBackgroundColour(wxColour(28, 30, 34));
|
||||
m_hud_label = new wxStaticText(m_hud, wxID_ANY, wxEmptyString);
|
||||
m_hud_label->SetForegroundColour(wxColour(0x46, 0xE0, 0xC8)); // teal, reads on dark bed
|
||||
wxFont f = m_hud_label->GetFont(); f.MakeBold(); m_hud_label->SetFont(f);
|
||||
auto* hs = new wxBoxSizer(wxHORIZONTAL);
|
||||
hs->Add(m_hud_label, 0, wxALL, 6);
|
||||
m_hud->SetSizerAndFit(hs);
|
||||
m_hud->Hide();
|
||||
}
|
||||
m_sketch_tool.on_readout = [this](const std::string& s) { set_readout(s); };
|
||||
|
||||
refresh_bed();
|
||||
|
||||
m_canvas->bind_event_handlers();
|
||||
|
||||
// The Design GL canvas only receives key events (Esc to exit/enter Select, Ctrl+Z undo)
|
||||
// while it holds keyboard focus. Clicking a side-panel button steals focus, after which
|
||||
// Esc/Ctrl+Z silently do nothing until the viewport is clicked again. Restore focus
|
||||
// whenever the pointer enters the viewport (focus-follows-mouse, standard CAD behaviour).
|
||||
m_canvas_widget->Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& e) {
|
||||
// …but NOT while an inline value field is open: the field floats over the canvas, so
|
||||
// the smallest pointer jiggle re-enters the viewport and would yank focus off the
|
||||
// field (the "no cursor focus on the number, click to focus" bug).
|
||||
if (m_canvas_widget && !m_sketch_tool.inline_busy()) m_canvas_widget->SetFocus();
|
||||
e.Skip();
|
||||
});
|
||||
|
||||
auto* sizer = new wxBoxSizer(wxVERTICAL);
|
||||
sizer->Add(m_canvas_widget, 1, wxEXPAND);
|
||||
SetSizer(sizer);
|
||||
SetMinSize(wxSize(300, 300));
|
||||
}
|
||||
|
||||
DesignCanvas::~DesignCanvas()
|
||||
{
|
||||
if (m_hud) m_hud->Destroy();
|
||||
delete m_canvas;
|
||||
delete m_canvas_widget;
|
||||
}
|
||||
|
||||
// Distinct per-body colours (Onshape-style). Body 0 keeps the familiar gold; the rest
|
||||
// cycle through a small saturated palette so coexisting solids read as separate parts.
|
||||
static ColorRGBA body_palette(int body_idx)
|
||||
{
|
||||
static const ColorRGBA kPalette[] = {
|
||||
ColorRGBA(0.86f, 0.66f, 0.20f, 1.0f), // gold
|
||||
ColorRGBA(0.30f, 0.62f, 0.90f, 1.0f), // blue
|
||||
ColorRGBA(0.45f, 0.78f, 0.42f, 1.0f), // green
|
||||
ColorRGBA(0.86f, 0.45f, 0.40f, 1.0f), // coral
|
||||
ColorRGBA(0.70f, 0.52f, 0.86f, 1.0f), // violet
|
||||
ColorRGBA(0.90f, 0.70f, 0.35f, 1.0f), // amber
|
||||
};
|
||||
const int n = int(sizeof(kPalette) / sizeof(kPalette[0]));
|
||||
return kPalette[((body_idx % n) + n) % n];
|
||||
}
|
||||
|
||||
void DesignCanvas::reload(bool keep_view)
|
||||
{
|
||||
m_canvas->reset_volumes();
|
||||
|
||||
for (int i = 0; i < (int)m_model.objects.size(); ++i)
|
||||
m_canvas->load_object(m_model, i);
|
||||
|
||||
const ColorRGBA sel_gold(0.40f, 0.82f, 1.0f, 1.0f); // cyan tint = solid selected
|
||||
const ColorRGBA ghost(0.26f, 0.66f, 1.0f, 0.45f);
|
||||
|
||||
const auto& volumes = m_canvas->get_volumes().volumes;
|
||||
for (auto* v : volumes) {
|
||||
int obj_idx = v->object_idx();
|
||||
if (obj_idx == 0) {
|
||||
// Object 0 holds one volume per body — colour each by its body index so
|
||||
// multiple coexisting solids are visually distinct (Onshape per-part colour).
|
||||
const int b = v->volume_idx();
|
||||
bool hidden = (b >= 0 && b < int(m_body_visible.size())) && !m_body_visible[b];
|
||||
// Preview-only mode (fillet/chamfer/draft, once a valid target is picked): hide
|
||||
// every base body so only the result ghost is on screen until Confirm.
|
||||
if (m_body_hidden) hidden = true;
|
||||
v->is_active = !hidden; // per-body visibility toggle
|
||||
if (!hidden) {
|
||||
// Selection tint wins; otherwise the per-body override (Color tool) or the
|
||||
// auto palette via body_color().
|
||||
ColorRGBA c = m_body_selected ? sel_gold : body_color(b);
|
||||
if (m_body_translucent) c.a(0.30f);
|
||||
v->set_color(c);
|
||||
}
|
||||
} else if (obj_idx == 1) {
|
||||
// The ghost is normally a faint blue overlay on the visible body. In preview-only
|
||||
// mode it IS the result (base bodies hidden), so render it opaque so it reads as a
|
||||
// finished solid rather than a see-through hint.
|
||||
v->set_color(m_body_hidden ? ColorRGBA(0.40f, 0.82f, 1.0f, 1.0f) : ghost);
|
||||
}
|
||||
}
|
||||
|
||||
if (!keep_view) {
|
||||
if (m_first_frame && !m_model.objects.empty()) {
|
||||
m_canvas->select_view("iso");
|
||||
m_canvas->zoom_to_volumes();
|
||||
m_first_frame = false;
|
||||
}
|
||||
}
|
||||
|
||||
m_canvas->set_as_dirty();
|
||||
if (m_canvas_widget)
|
||||
m_canvas_widget->Refresh();
|
||||
}
|
||||
|
||||
void DesignCanvas::set_mesh(const TriangleMesh& mesh)
|
||||
{
|
||||
if (m_model.objects.empty()) {
|
||||
auto* obj = m_model.add_object();
|
||||
obj->add_volume(mesh);
|
||||
obj->add_instance();
|
||||
} else {
|
||||
ModelObject* obj = m_model.objects.front();
|
||||
obj->clear_volumes();
|
||||
obj->add_volume(mesh);
|
||||
if (obj->instances.empty())
|
||||
obj->add_instance();
|
||||
}
|
||||
|
||||
reload(!m_first_frame);
|
||||
}
|
||||
|
||||
void DesignCanvas::set_bodies(const std::vector<TriangleMesh>& body_meshes,
|
||||
const std::vector<bool>& visible)
|
||||
{
|
||||
// Object 0 carries one GLVolume per body so reload() can colour each distinctly.
|
||||
// Falls back to a single-volume object when there's only one body (identical look
|
||||
// to the old set_mesh path). Picking still uses the combined mesh via set_solid_pick.
|
||||
m_body_visible = visible; // empty => all visible; reload() reads this per volume
|
||||
if (body_meshes.empty()) { clear_mesh(); return; }
|
||||
|
||||
ModelObject* obj = m_model.objects.empty() ? m_model.add_object()
|
||||
: m_model.objects.front();
|
||||
obj->clear_volumes();
|
||||
for (const TriangleMesh& m : body_meshes)
|
||||
obj->add_volume(m);
|
||||
if (obj->instances.empty())
|
||||
obj->add_instance();
|
||||
|
||||
reload(!m_first_frame);
|
||||
}
|
||||
|
||||
void DesignCanvas::clear_mesh()
|
||||
{
|
||||
if (!m_model.objects.empty()) {
|
||||
m_model.delete_object((size_t)0);
|
||||
reload(true);
|
||||
}
|
||||
}
|
||||
|
||||
void DesignCanvas::set_preview_mesh(const TriangleMesh& mesh)
|
||||
{
|
||||
// Remove existing ghost (object 1) if present
|
||||
if (m_model.objects.size() > 1)
|
||||
m_model.delete_object((size_t)1);
|
||||
|
||||
auto* obj = m_model.add_object();
|
||||
obj->add_volume(mesh);
|
||||
obj->add_instance();
|
||||
|
||||
reload(true);
|
||||
}
|
||||
|
||||
void DesignCanvas::clear_preview()
|
||||
{
|
||||
if (m_model.objects.size() > 1) {
|
||||
m_model.delete_object((size_t)1);
|
||||
reload(true);
|
||||
}
|
||||
}
|
||||
|
||||
void DesignCanvas::fit_view()
|
||||
{
|
||||
if (m_canvas && !m_model.objects.empty()) {
|
||||
m_canvas->zoom_to_volumes();
|
||||
m_canvas->set_as_dirty();
|
||||
if (m_canvas_widget)
|
||||
m_canvas_widget->Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void DesignCanvas::set_view(const std::string& view_name)
|
||||
{
|
||||
if (m_canvas) {
|
||||
m_canvas->select_view(view_name);
|
||||
m_canvas->zoom_to_volumes();
|
||||
m_canvas->set_as_dirty();
|
||||
if (m_canvas_widget)
|
||||
m_canvas_widget->Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void DesignCanvas::begin_sketch(const SketchPlane& plane, DesignSketchTool::Mode mode)
|
||||
{
|
||||
m_sketch_tool.begin(plane, mode);
|
||||
if (m_canvas) m_canvas->set_as_dirty();
|
||||
if (m_canvas_widget) m_canvas_widget->Refresh();
|
||||
}
|
||||
|
||||
void DesignCanvas::edit_sketch(const std::vector<SketchEntity>& entities,
|
||||
const std::vector<SketchEntityConstraintDef>& constraints,
|
||||
const SketchPlane& plane)
|
||||
{
|
||||
m_sketch_tool.begin_edit(entities, constraints, plane);
|
||||
if (m_canvas) m_canvas->set_as_dirty();
|
||||
if (m_canvas_widget) m_canvas_widget->Refresh();
|
||||
}
|
||||
|
||||
void DesignCanvas::set_sketch_tool(DesignSketchTool::Mode mode)
|
||||
{
|
||||
m_sketch_tool.set_tool(mode);
|
||||
if (m_canvas) m_canvas->set_as_dirty();
|
||||
if (m_canvas_widget) m_canvas_widget->Refresh();
|
||||
}
|
||||
|
||||
void DesignCanvas::set_sketch_construction(bool c)
|
||||
{
|
||||
m_sketch_tool.set_construction(c);
|
||||
}
|
||||
|
||||
void DesignCanvas::set_sketch_polygon_sides(int n)
|
||||
{
|
||||
m_sketch_tool.set_polygon_sides(n);
|
||||
}
|
||||
|
||||
void DesignCanvas::set_sketch_polygon_circumscribed(bool c)
|
||||
{
|
||||
m_sketch_tool.set_polygon_circumscribed(c);
|
||||
}
|
||||
|
||||
void DesignCanvas::finish_sketch()
|
||||
{
|
||||
m_sketch_tool.finish();
|
||||
if (m_canvas) m_canvas->set_as_dirty();
|
||||
if (m_canvas_widget) m_canvas_widget->Refresh();
|
||||
}
|
||||
|
||||
// Sync the Design bed to the CURRENT printer bed. Done on every tab activation, not just at
|
||||
// construction: the panel is built early (before the active printer profile is fully applied),
|
||||
// so a one-shot read picked up the 200x200 default while the real bed (e.g. 270x270) only
|
||||
// loaded later — leaving the PartPlate grid spilling past the smaller bed quad.
|
||||
void DesignCanvas::refresh_bed()
|
||||
{
|
||||
const DynamicPrintConfig* config = wxGetApp().plater()->config();
|
||||
if (!config) return;
|
||||
const auto* bed_shape_opt = config->opt<ConfigOptionPoints>("printable_area");
|
||||
if (!bed_shape_opt) return;
|
||||
double printable_height = 100.0;
|
||||
const auto* ph_opt = config->opt<ConfigOptionFloat>("printable_height");
|
||||
if (ph_opt) printable_height = ph_opt->value;
|
||||
m_bed.set_shape(bed_shape_opt->values, printable_height, "", false);
|
||||
}
|
||||
|
||||
bool DesignCanvas::is_sketching() const { return m_sketch_tool.is_active(); }
|
||||
|
||||
void DesignCanvas::cancel_sketch()
|
||||
{
|
||||
m_sketch_tool.cancel();
|
||||
if (m_canvas) m_canvas->set_as_dirty();
|
||||
if (m_canvas_widget) m_canvas_widget->Refresh();
|
||||
}
|
||||
|
||||
void DesignCanvas::set_on_sketch_commit(std::function<void(const SketchProfile&, const SketchPlane&)> cb)
|
||||
{
|
||||
m_on_sketch_commit = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::set_on_sketch_entities_commit(
|
||||
std::function<void(const std::vector<SketchEntity>&,
|
||||
const std::vector<SketchEntityConstraintDef>&,
|
||||
const SketchPlane&)> cb)
|
||||
{
|
||||
m_on_sketch_entities_commit = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::set_on_segment_drawn(std::function<void(double, double)> cb)
|
||||
{
|
||||
m_sketch_tool.on_segment_drawn = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::set_on_cursor_metrics(std::function<void(double, double, bool)> cb)
|
||||
{
|
||||
m_sketch_tool.on_cursor_metrics = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::set_on_solve_state(std::function<void(int, bool, bool)> cb)
|
||||
{
|
||||
m_sketch_tool.on_solve_state = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::apply_segment_length(double len)
|
||||
{
|
||||
m_sketch_tool.apply_segment_length(len);
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
void DesignCanvas::keep_segment_as_drawn()
|
||||
{
|
||||
m_sketch_tool.keep_segment_as_drawn();
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
void DesignCanvas::set_on_sketch_selection_changed(std::function<void(int)> cb)
|
||||
{
|
||||
m_sketch_tool.on_selection_changed = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::set_on_sketch_face_selected(std::function<void()> cb)
|
||||
{
|
||||
m_sketch_tool.on_face_selected = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::set_on_display_sketch_selected(std::function<void(int, int)> cb)
|
||||
{
|
||||
m_sketch_tool.on_display_sketch_selected = std::move(cb);
|
||||
}
|
||||
|
||||
std::vector<SketchEntity> DesignCanvas::selected_loop_entities() const
|
||||
{
|
||||
return m_sketch_tool.selected_loop_entities();
|
||||
}
|
||||
|
||||
std::vector<std::vector<int>> DesignCanvas::region_entity_indices(const std::vector<SketchEntity>& ents) const
|
||||
{
|
||||
return m_sketch_tool.region_entity_indices(ents);
|
||||
}
|
||||
|
||||
void DesignCanvas::clear_loop_pick()
|
||||
{
|
||||
m_sketch_tool.clear_display_pick();
|
||||
}
|
||||
|
||||
void DesignCanvas::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,
|
||||
const std::vector<Transform3d>* xform)
|
||||
{
|
||||
m_color_bodies = bodies; // stable address (m_doc.bodies); reload() reads colour overrides
|
||||
m_sketch_tool.set_solid_pick(bodies, mesh, tri_face, tri_body, visible, xform);
|
||||
}
|
||||
|
||||
// Effective display colour for a body: per-body override (Color tool) when set, else the
|
||||
// auto body-index palette. body_palette() is the file-static helper defined above reload().
|
||||
ColorRGBA DesignCanvas::body_color(int body) const
|
||||
{
|
||||
if (m_color_bodies != nullptr && body >= 0 && body < int(m_color_bodies->size())
|
||||
&& (*m_color_bodies)[body].has_color)
|
||||
return (*m_color_bodies)[body].color;
|
||||
return body_palette(body);
|
||||
}
|
||||
|
||||
void DesignCanvas::begin_move_body(int body, const Vec3d& pivot, const Transform3d& base_xform)
|
||||
{
|
||||
m_sketch_tool.set_move_gizmo(body, pivot, base_xform);
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } // llvmpipe: force repaint
|
||||
}
|
||||
|
||||
void DesignCanvas::clear_move_gizmo()
|
||||
{
|
||||
m_sketch_tool.clear_move_gizmo();
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
bool DesignCanvas::moving_body() const { return m_sketch_tool.moving_body(); }
|
||||
|
||||
void DesignCanvas::set_on_body_move_changed(std::function<void(int, const Transform3d&)> cb)
|
||||
{
|
||||
m_sketch_tool.on_body_move_changed = std::move(cb);
|
||||
}
|
||||
|
||||
bool DesignCanvas::begin_fillet_gizmo(const Vec3d& body_centroid, double radius)
|
||||
{
|
||||
const bool ok = m_sketch_tool.set_fillet_gizmo(body_centroid, radius);
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } // llvmpipe: force repaint
|
||||
return ok;
|
||||
}
|
||||
|
||||
void DesignCanvas::clear_fillet_gizmo()
|
||||
{
|
||||
m_sketch_tool.clear_fillet_gizmo();
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
bool DesignCanvas::filleting() const { return m_sketch_tool.filleting(); }
|
||||
|
||||
void DesignCanvas::set_on_fillet_radius_changed(std::function<void(double)> cb)
|
||||
{
|
||||
m_sketch_tool.on_fillet_radius_changed = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::begin_hole_gizmo(const SketchPlane& plane, double x, double y,
|
||||
double diameter, double depth, bool through)
|
||||
{
|
||||
m_sketch_tool.set_hole_gizmo(plane, x, y, diameter, depth, through);
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } // llvmpipe: force repaint
|
||||
}
|
||||
|
||||
void DesignCanvas::set_hole_face_bounds(bool has, double umin, double umax, double vmin, double vmax)
|
||||
{
|
||||
m_sketch_tool.set_hole_face_bounds(has, umin, umax, vmin, vmax);
|
||||
}
|
||||
|
||||
void DesignCanvas::clear_hole_gizmo()
|
||||
{
|
||||
m_sketch_tool.clear_hole_gizmo();
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
bool DesignCanvas::holing() const { return m_sketch_tool.holing(); }
|
||||
|
||||
void DesignCanvas::set_on_hole_changed(std::function<void(double, double, double, double)> cb)
|
||||
{
|
||||
m_sketch_tool.on_hole_changed = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::begin_thread_gizmo(const SketchPlane& plane, double x, double y,
|
||||
double radius, double height)
|
||||
{
|
||||
m_sketch_tool.set_thread_gizmo(plane, x, y, radius, height);
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } // llvmpipe: force repaint
|
||||
}
|
||||
|
||||
void DesignCanvas::clear_thread_gizmo()
|
||||
{
|
||||
m_sketch_tool.clear_thread_gizmo();
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
bool DesignCanvas::threading() const { return m_sketch_tool.threading(); }
|
||||
|
||||
void DesignCanvas::set_on_thread_changed(std::function<void(double, double, double, double)> cb)
|
||||
{
|
||||
m_sketch_tool.on_thread_changed = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::begin_shell_gizmo(const Vec3d& face_centroid, const Vec3d& inward_dir,
|
||||
double thickness)
|
||||
{
|
||||
m_sketch_tool.set_shell_gizmo(face_centroid, inward_dir, thickness);
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } // llvmpipe: force repaint
|
||||
}
|
||||
|
||||
void DesignCanvas::clear_shell_gizmo()
|
||||
{
|
||||
m_sketch_tool.clear_shell_gizmo();
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
bool DesignCanvas::shelling() const { return m_sketch_tool.shelling(); }
|
||||
|
||||
void DesignCanvas::set_on_shell_thickness_changed(std::function<void(double)> cb)
|
||||
{
|
||||
m_sketch_tool.on_shell_thickness_changed = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::begin_revolve_gizmo(const SketchPlane& plane, const Vec2d& centroid,
|
||||
int axis_sel, double angle, bool flip)
|
||||
{
|
||||
m_sketch_tool.set_revolve_gizmo(plane, centroid, axis_sel, angle, flip);
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } // llvmpipe: force repaint
|
||||
}
|
||||
|
||||
void DesignCanvas::clear_revolve_gizmo()
|
||||
{
|
||||
m_sketch_tool.clear_revolve_gizmo();
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
bool DesignCanvas::revolving() const { return m_sketch_tool.revolving(); }
|
||||
|
||||
void DesignCanvas::set_on_revolve_angle_changed(std::function<void(double)> cb)
|
||||
{
|
||||
m_sketch_tool.on_revolve_angle_changed = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::begin_pattern_gizmo(const SketchPlane& plane, const Vec3d& body_centroid,
|
||||
bool circular, int count, int dir, double spacing, double angle)
|
||||
{
|
||||
m_sketch_tool.set_pattern_gizmo(plane, body_centroid, circular, count, dir, spacing, angle);
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } // llvmpipe: force repaint
|
||||
}
|
||||
|
||||
void DesignCanvas::clear_pattern_gizmo()
|
||||
{
|
||||
m_sketch_tool.clear_pattern_gizmo();
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
bool DesignCanvas::patterning() const { return m_sketch_tool.patterning(); }
|
||||
|
||||
void DesignCanvas::set_on_pattern_changed(std::function<void(double)> cb)
|
||||
{
|
||||
m_sketch_tool.on_pattern_changed = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::set_on_solid_selection_changed(std::function<void(int, int, int, int)> cb)
|
||||
{
|
||||
m_sketch_tool.on_solid_selection_changed = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::set_on_place_on_face(std::function<bool()> cb)
|
||||
{
|
||||
m_sketch_tool.on_place_on_face = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::select_body(int body)
|
||||
{
|
||||
m_sketch_tool.select_body(body);
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } // redraw the overlay (llvmpipe)
|
||||
}
|
||||
|
||||
void DesignCanvas::set_extrude_gizmo(const SketchPlane& plane, const Vec2d& centroid,
|
||||
double depth, double depth2, bool two_sided, bool flip)
|
||||
{
|
||||
m_sketch_tool.set_extrude_gizmo(plane, centroid, depth, depth2, two_sided, flip);
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } // llvmpipe: force repaint
|
||||
}
|
||||
|
||||
void DesignCanvas::clear_extrude_gizmo()
|
||||
{
|
||||
m_sketch_tool.clear_extrude_gizmo();
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
void DesignCanvas::set_on_extrude_depth_changed(std::function<void(double, bool)> cb)
|
||||
{
|
||||
m_sketch_tool.on_extrude_depth_changed = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::set_on_sketch_exit(std::function<void()> cb)
|
||||
{
|
||||
m_sketch_tool.on_exit = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::set_on_move_exit(std::function<void()> cb)
|
||||
{
|
||||
m_sketch_tool.on_move_exit = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::set_on_undo_redo(std::function<void(bool)> cb)
|
||||
{
|
||||
m_sketch_tool.on_undo_redo = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::set_display_sketches(std::vector<DesignSketchTool::DisplaySketch> ds)
|
||||
{
|
||||
m_sketch_tool.set_display_sketches(std::move(ds));
|
||||
// Direct render: under llvmpipe a scheduled Refresh() often doesn't repaint
|
||||
// unless some other event (e.g. a modal close) forces it, so programmatic
|
||||
// overlay changes (hide/show, re-solve) could leave a stale overlay.
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
void DesignCanvas::set_datum_planes(std::vector<SketchPlane> planes)
|
||||
{
|
||||
m_sketch_tool.set_datum_planes(std::move(planes));
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); } // llvmpipe: force repaint
|
||||
}
|
||||
|
||||
void DesignCanvas::set_readout(const std::string& text)
|
||||
{
|
||||
if (!m_hud || !m_hud_label || !m_canvas_widget) return;
|
||||
if (text == m_hud_last) return; // only touch the WM on a real change
|
||||
m_hud_last = text;
|
||||
if (text.empty()) { m_hud->Hide(); return; }
|
||||
m_hud_label->SetLabel(wxString::FromUTF8(text));
|
||||
m_hud->Fit();
|
||||
// Anchor to the canvas's bottom-right corner with a small margin (screen coords).
|
||||
const wxSize cs = m_canvas_widget->GetClientSize();
|
||||
const wxSize hs = m_hud->GetSize();
|
||||
const wxPoint br = m_canvas_widget->ClientToScreen(
|
||||
wxPoint(cs.GetWidth() - hs.GetWidth() - 12, cs.GetHeight() - hs.GetHeight() - 12));
|
||||
if (!m_hud->IsShown()) m_hud->Show(); // Show before Move (GTK ignores pre-map Move)
|
||||
m_hud->Move(br);
|
||||
m_hud->Raise();
|
||||
}
|
||||
|
||||
void DesignCanvas::set_body_highlight(bool on)
|
||||
{
|
||||
if (m_body_selected == on) return;
|
||||
m_body_selected = on;
|
||||
reload(true); // recolours the body volume (selected = cyan tint)
|
||||
}
|
||||
|
||||
void DesignCanvas::set_body_translucent(bool on)
|
||||
{
|
||||
if (m_body_translucent == on) return;
|
||||
m_body_translucent = on;
|
||||
reload(true); // re-applies object-0 alpha so the solid fades for the fillet preview
|
||||
}
|
||||
|
||||
void DesignCanvas::set_body_hidden(bool on)
|
||||
{
|
||||
if (m_body_hidden == on) return;
|
||||
m_body_hidden = on;
|
||||
reload(true); // hides/show base bodies + flips the ghost opaque/faint for preview-only mode
|
||||
}
|
||||
|
||||
void DesignCanvas::delete_selected_sketch_entities()
|
||||
{
|
||||
m_sketch_tool.delete_selected();
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
void DesignCanvas::clear_sketch_selection()
|
||||
{
|
||||
m_sketch_tool.clear_selection();
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
DesignSketchTool::DimType DesignCanvas::sketch_dimension_kind() const
|
||||
{
|
||||
return m_sketch_tool.dimension_kind();
|
||||
}
|
||||
|
||||
double DesignCanvas::sketch_dimension_current() const
|
||||
{
|
||||
return m_sketch_tool.dimension_current();
|
||||
}
|
||||
|
||||
void DesignCanvas::apply_sketch_dimension(double v)
|
||||
{
|
||||
m_sketch_tool.apply_dimension(v);
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
void DesignCanvas::open_inline_value(double current, std::function<void(double)> commit,
|
||||
std::function<void()> cancel)
|
||||
{
|
||||
if (!m_inline_editor || !m_canvas_widget) { if (cancel) cancel(); return; }
|
||||
// Host-driven value entry (committed-feature Constrain path): the trigger is a toolbar
|
||||
// button. Anchor the field OVER the picked geometry (same as the draw-then-edit tools) when
|
||||
// the tool can project it; else fall back to the viewport centre, where the sketch is in
|
||||
// view. GetScreenRect collapses GetClientSize()+ClientToScreen() into one call; if the GL
|
||||
// canvas reports degenerate geometry (transiently, right after a re-layout), fall back to the
|
||||
// always-realised top-level window so the editor never lands in the top-left corner.
|
||||
wxRect r = m_canvas_widget->GetScreenRect();
|
||||
if (r.GetWidth() <= 1 || r.GetHeight() <= 1) {
|
||||
if (wxWindow* top = wxGetTopLevelParent(m_canvas_widget))
|
||||
r = top->GetScreenRect();
|
||||
}
|
||||
wxPoint scr(r.GetLeft() + r.GetWidth() / 2, r.GetTop() + r.GetHeight() / 2);
|
||||
wxPoint anchor;
|
||||
if (m_sketch_tool.constrain_value_anchor(anchor)) { // device px in the canvas viewport
|
||||
const double s = m_canvas_widget->GetContentScaleFactor();
|
||||
scr = m_canvas_widget->ClientToScreen(wxPoint(int(anchor.x / s), int(anchor.y / s)));
|
||||
}
|
||||
// Freeze the canvas so focus-follows-mouse can't steal keyboard focus off the field — the
|
||||
// same fix the draw-then-edit path uses (cursor focus stays on the field, no pre-click).
|
||||
m_sketch_tool.set_inline_busy(true);
|
||||
m_inline_editor->open(scr, current,
|
||||
[this, commit](double v) {
|
||||
m_sketch_tool.set_inline_busy(false);
|
||||
if (commit) commit(v);
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
},
|
||||
[this, cancel]() {
|
||||
m_sketch_tool.set_inline_busy(false);
|
||||
if (cancel) cancel();
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
});
|
||||
}
|
||||
|
||||
void DesignCanvas::set_on_dimension_pick_complete(std::function<void(double)> cb)
|
||||
{
|
||||
m_sketch_tool.on_dimension_pick_complete = std::move(cb);
|
||||
}
|
||||
|
||||
DesignSketchTool::DimType DesignCanvas::pending_dimension_type() const
|
||||
{
|
||||
return m_sketch_tool.pending_dimension_type();
|
||||
}
|
||||
|
||||
void DesignCanvas::set_sketch_dimension_value(double v)
|
||||
{
|
||||
m_sketch_tool.set_dimension_value(v);
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
void DesignCanvas::cancel_sketch_dimension()
|
||||
{
|
||||
m_sketch_tool.cancel_dimension_value();
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
void DesignCanvas::begin_constrain(const SketchProfile& prof, const SketchPlane& plane)
|
||||
{
|
||||
m_sketch_tool.begin_constrain(prof, plane);
|
||||
// The overlay must appear immediately (no mouse move to trigger a repaint);
|
||||
// a direct render() is the proven path under llvmpipe.
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
void DesignCanvas::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)
|
||||
{
|
||||
m_sketch_tool.begin_imported_transform(feat, base_regions, plane, offset, scale_x, scale_y);
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
void DesignCanvas::set_on_imported_transform(std::function<void(int, Vec2d, double, double)> cb)
|
||||
{
|
||||
m_sketch_tool.on_imported_transform = std::move(cb);
|
||||
}
|
||||
|
||||
void DesignCanvas::end_constrain()
|
||||
{
|
||||
// cancel() clears m_active + the picked-segment/entity indices, so the
|
||||
// constrain overlay (highlighted picks) disappears on the next render.
|
||||
m_sketch_tool.cancel();
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
bool DesignCanvas::is_constraining() const { return m_sketch_tool.is_constraining(); }
|
||||
|
||||
bool DesignCanvas::selected_segment(int& a, int& b) const
|
||||
{
|
||||
return m_sketch_tool.selected_segment(a, b);
|
||||
}
|
||||
|
||||
void DesignCanvas::update_constrain_profile(const std::vector<Vec2d>& pts)
|
||||
{
|
||||
m_sketch_tool.set_profile_points(pts);
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
void DesignCanvas::begin_constrain_entities(const std::vector<SketchEntity>& ents,
|
||||
const SketchPlane& plane)
|
||||
{
|
||||
m_sketch_tool.begin_constrain_entities(ents, plane);
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
bool DesignCanvas::is_constraining_entities() const
|
||||
{
|
||||
return m_sketch_tool.is_constraining_entities();
|
||||
}
|
||||
|
||||
bool DesignCanvas::selected_constrain_entities(int& e0, int& e1) const
|
||||
{
|
||||
return m_sketch_tool.selected_constrain_entities(e0, e1);
|
||||
}
|
||||
|
||||
int DesignCanvas::selected_constrain_axis() const
|
||||
{
|
||||
return m_sketch_tool.pick2();
|
||||
}
|
||||
|
||||
bool DesignCanvas::pick0_point(Vec2d& out) const
|
||||
{
|
||||
return m_sketch_tool.pick0_point(out);
|
||||
}
|
||||
|
||||
void DesignCanvas::update_constrain_entities(const std::vector<SketchEntity>& ents)
|
||||
{
|
||||
m_sketch_tool.set_constrain_entities(ents);
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
void DesignCanvas::set_constraint_highlight(std::vector<int> entities)
|
||||
{
|
||||
m_sketch_tool.set_constraint_highlight(std::move(entities));
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
void DesignCanvas::set_constraint_glyphs(std::vector<SketchEntityConstraintDef> cons)
|
||||
{
|
||||
m_sketch_tool.set_constraint_glyphs(std::move(cons));
|
||||
if (m_canvas) { m_canvas->set_as_dirty(); m_canvas->render(); }
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
@@ -0,0 +1,236 @@
|
||||
#ifndef slic3r_DesignCanvas_hpp_
|
||||
#define slic3r_DesignCanvas_hpp_
|
||||
|
||||
#include <wx/panel.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "3DBed.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/SketchEngine.hpp"
|
||||
#include "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_construction(bool c);
|
||||
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)
|
||||
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
|
||||
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()> cb); // closed loop clicked
|
||||
void set_on_display_sketch_selected(std::function<void(int, int)> cb); // committed loop clicked: (feature, region)
|
||||
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;
|
||||
void clear_loop_pick(); // drop the click-selected loop highlight (e.g. after extrude)
|
||||
// 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).
|
||||
void begin_move_body(int body, const Vec3d& pivot, const Transform3d& base_xform);
|
||||
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 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_on_sketch_exit(std::function<void()> cb); // Esc -> exit the tool
|
||||
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_datum_planes(std::vector<SketchPlane> planes); // draw datum/reference planes
|
||||
void set_body_highlight(bool on); // tint the solid when its feature is tree-selected
|
||||
void set_body_translucent(bool on); // render the solid see-through (fillet/chamfer preview)
|
||||
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
|
||||
void delete_selected_sketch_entities();
|
||||
void clear_sketch_selection();
|
||||
|
||||
// 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;
|
||||
|
||||
// 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);
|
||||
|
||||
private:
|
||||
void reload(bool keep_view);
|
||||
|
||||
wxGLCanvas* m_canvas_widget{nullptr};
|
||||
GLCanvas3D* m_canvas{nullptr};
|
||||
Bed3D m_bed;
|
||||
Model m_model;
|
||||
bool m_first_frame{true};
|
||||
bool m_body_selected{false}; // tree selected a body feature → tint the solid
|
||||
bool m_body_translucent{false};// fillet/chamfer preview → render the body see-through
|
||||
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;
|
||||
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.
|
||||
wxFrame* m_hud{nullptr};
|
||||
wxStaticText* m_hud_label{nullptr};
|
||||
std::string m_hud_last;
|
||||
void set_readout(const std::string& text);
|
||||
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_
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,420 @@
|
||||
#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 "libslic3r/CadDocument.hpp"
|
||||
|
||||
class wxChoice;
|
||||
class wxCheckBox;
|
||||
class wxCheckListBox;
|
||||
class wxSpinCtrl;
|
||||
class wxSpinCtrlDouble;
|
||||
class wxTreeCtrl;
|
||||
class wxImageList;
|
||||
class wxStaticText;
|
||||
class wxSizer;
|
||||
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
|
||||
|
||||
private:
|
||||
enum class Tool { None, Sketch, Extrude, Dressup, Hole, Thread, Shell, Revolve, Sweep, Pattern, Plane, Loft, Draft, Boolean, Cut, Insert };
|
||||
|
||||
// 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);
|
||||
// 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(); // ✗ / Esc : cancel the active feature / discard / exit
|
||||
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 on_add_revolve();
|
||||
void on_add_sweep();
|
||||
void on_add_loft();
|
||||
void on_add_pattern();
|
||||
void on_add_plane();
|
||||
void on_add_shell();
|
||||
void on_add_draft();
|
||||
void on_add_boolean();
|
||||
void on_add_cut(); // commit a plane Cut (split-by-plane)
|
||||
void populate_body_choices(); // fill m_bool_target / m_bool_tool / m_cut_target from m_doc.bodies
|
||||
// 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)
|
||||
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 refresh_tree();
|
||||
void set_status_ok();
|
||||
|
||||
// Feature-tree editing (Onshape-style): act on the selected tree row.
|
||||
void on_delete_feature();
|
||||
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
|
||||
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_constraint(const SketchEntityConstraintDef& def); // shared solve/refresh tail
|
||||
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.
|
||||
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;
|
||||
CadFeature build_candidate(Tool t) const;
|
||||
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(wxChoice* c) const;
|
||||
SketchPlane plane_from_choice(int row) 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 update_hole_gizmo(); // footprint circle + diameter/depth arrows (Hole card)
|
||||
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_pattern_gizmo(); // linear spacing arrow / circular angle-arc (Pattern card)
|
||||
|
||||
CadDocument m_doc;
|
||||
|
||||
Tool m_active{Tool::None};
|
||||
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_insert{nullptr}; // Confirm/Cancel card for placing Text/SVG art
|
||||
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()};
|
||||
|
||||
// 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_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_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_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};
|
||||
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};
|
||||
wxSizer* m_tb_constrain{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
|
||||
wxSpinCtrl* m_sides{nullptr}; // polygon sides
|
||||
wxCheckBox* m_poly_circ{nullptr}; // polygon circumscribed toggle
|
||||
|
||||
wxChoice* m_draw_plane{nullptr};
|
||||
wxChoice* m_shape{nullptr};
|
||||
wxChoice* m_plane{nullptr};
|
||||
wxChoice* m_mode{nullptr};
|
||||
wxSpinCtrlDouble* m_width{nullptr};
|
||||
wxSpinCtrlDouble* m_height{nullptr};
|
||||
wxSpinCtrlDouble* m_radius{nullptr};
|
||||
wxSpinCtrlDouble* m_distance{nullptr};
|
||||
wxChoice* 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)
|
||||
wxCheckBox* 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};
|
||||
wxChoice* m_revolve_axis{nullptr}; // 0 = plane X, 1 = plane Y
|
||||
wxChoice* m_revolve_mode{nullptr}; // New/Add/Cut/Intersect
|
||||
wxCheckBox* 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};
|
||||
wxChoice* m_sweep_path{nullptr}; // path Sketch picker (feature index in client data)
|
||||
wxChoice* 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
|
||||
wxCheckBox* m_loft_ruled{nullptr}; // ruled (straight) vs smooth sections
|
||||
wxChoice* 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)
|
||||
|
||||
// Pattern controls (replicate the target body: linear or circular).
|
||||
wxChoice* 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)
|
||||
wxChoice* 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).
|
||||
wxChoice* m_bool_op{nullptr}; // 0 = Union, 1 = Subtract, 2 = Intersect
|
||||
wxChoice* m_bool_target{nullptr}; // body that survives (selection == body index)
|
||||
wxChoice* m_bool_tool{nullptr}; // body consumed (selection == body index)
|
||||
wxCheckBox* 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).
|
||||
wxChoice* m_cut_plane{nullptr}; // XY/XZ/YZ + datum planes (cut plane)
|
||||
wxChoice* 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).
|
||||
wxChoice* 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)
|
||||
wxChoice* m_plane_tilt_axis{nullptr}; // 0 = base X, 1 = base Y
|
||||
// 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};
|
||||
// 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};
|
||||
|
||||
wxChoice* m_dressup_type{nullptr};
|
||||
wxChoice* m_face_group{nullptr};
|
||||
wxSpinCtrlDouble* m_dressup_size{nullptr};
|
||||
|
||||
wxChoice* m_hole_plane{nullptr};
|
||||
wxSpinCtrlDouble* m_hole_diameter{nullptr};
|
||||
wxSpinCtrlDouble* m_hole_depth{nullptr};
|
||||
wxCheckBox* 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};
|
||||
|
||||
wxChoice* m_thread_plane{nullptr};
|
||||
wxChoice* 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};
|
||||
wxCheckBox* 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};
|
||||
|
||||
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
|
||||
|
||||
// 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};
|
||||
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;
|
||||
// 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 on_set_body_color(); // Color tool: pick a per-body display colour override
|
||||
int tree_selection() const; // selected feature row, or wxNOT_FOUND
|
||||
int tree_body_selection() const; // selected Parts-list body index, or -1
|
||||
void set_tree_selection(int row);
|
||||
static int tree_icon_for(CadFeatureType t);
|
||||
|
||||
wxStaticText* m_status{nullptr};
|
||||
wxStaticText* m_dof_status{nullptr}; // DoF / constraint-state readout (P3)
|
||||
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
@@ -0,0 +1,980 @@
|
||||
#ifndef slic3r_DesignSketchTool_hpp_
|
||||
#define slic3r_DesignSketchTool_hpp_
|
||||
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/SketchEngine.hpp"
|
||||
#include "libslic3r/CadDocument.hpp" // CadBody for per-body solid picking
|
||||
#include "libslic3r/SketchInference.hpp"
|
||||
#include "libslic3r/SketchSolver.hpp"
|
||||
#include "GLModel.hpp"
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
class wxMouseEvent;
|
||||
class wxPoint;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class TriangleMesh; // fwd (libslic3r) — solid-pick mesh, non-owning pointer
|
||||
|
||||
namespace GUI {
|
||||
|
||||
class GLCanvas3D;
|
||||
|
||||
// Onshape-style sketch session. `begin` enters a session on a plane; the active
|
||||
// drawing tool (Mode) can be switched mid-session via `set_tool` while entities
|
||||
// accumulate. `finish` commits the whole entity list as one sketch feature;
|
||||
// `cancel` aborts. Constrain is a separate legacy mode that operates on a
|
||||
// committed profile's points (entity constraints land in a later chunk).
|
||||
class DesignSketchTool {
|
||||
public:
|
||||
enum class Mode { Select, Dimension, Polyline, Line, CornerRect, CenterRect, ObliqueRect,
|
||||
RoundedRect, CenterCircle, TwoPointCircle, Point,
|
||||
ThreePointCircle, ThreePointArc, TangentArc, CenterArc, Slot, ArcSlot, Polygon,
|
||||
Ellipse, EllipseArc, BSpline,
|
||||
// In-canvas edit-op TOOLBAR tools (drag-arrow + label, no numeric card):
|
||||
Fillet, Chamfer, Offset, Mirror,
|
||||
// Standalone scissors: click a segment to trim/extend it (immediate, no card):
|
||||
Trim, Extend,
|
||||
// In-canvas transform TOOLBAR tools (pick targets + drag handle/label, no card):
|
||||
Move, Rotate, Scale, Array, PolarArray,
|
||||
// In-canvas bounding-box transform for imported Text/SVG art:
|
||||
TransformArt,
|
||||
Constrain };
|
||||
bool is_edit_op_mode() const { return m_mode == Mode::Fillet || m_mode == Mode::Chamfer ||
|
||||
m_mode == Mode::Offset || m_mode == Mode::Mirror; }
|
||||
bool is_transform_mode() const { return m_mode == Mode::Move || m_mode == Mode::Rotate ||
|
||||
m_mode == Mode::Scale || m_mode == Mode::Array ||
|
||||
m_mode == Mode::PolarArray; }
|
||||
// Creation tools that get draw-then-edit: on commit the new entity/feature is selected
|
||||
// and its primary value editor opens. Line is handled inline (its own length field);
|
||||
// Polyline/BSpline/Point have no single primary value, so they opt out.
|
||||
bool is_creation_autoedit_mode() const {
|
||||
switch (m_mode) {
|
||||
case Mode::Line:
|
||||
case Mode::CornerRect: case Mode::CenterRect: case Mode::ObliqueRect:
|
||||
case Mode::RoundedRect: case Mode::CenterCircle: case Mode::TwoPointCircle:
|
||||
case Mode::ThreePointCircle: case Mode::ThreePointArc: case Mode::TangentArc:
|
||||
case Mode::CenterArc: case Mode::Slot: case Mode::ArcSlot: case Mode::Polygon:
|
||||
case Mode::Ellipse: case Mode::EllipseArc:
|
||||
return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
// The host (DesignCanvas) flags the canvas frozen while an inline value editor is open,
|
||||
// so a stray click/move can't draw under the floating field. Reuses m_awaiting_length
|
||||
// (Line's existing freeze flag) as the single "inline editor open" gate.
|
||||
void set_inline_busy(bool b) { m_awaiting_length = b; }
|
||||
bool inline_busy() const { return m_awaiting_length; } // true while a value field is open
|
||||
bool constrain_value_anchor(wxPoint& out) const; // screen anchor over the picked constrain geometry
|
||||
|
||||
void begin(const SketchPlane& plane, Mode mode = Mode::Polyline);
|
||||
// Re-open a committed entity sketch for full in-canvas editing: load its entities +
|
||||
// driving constraints, re-detect the polygon/rect/slot grouping, and live-solve. The
|
||||
// caller re-commits via finish() (the panel replaces the feature, see m_edit_index).
|
||||
void begin_edit(const std::vector<SketchEntity>& entities,
|
||||
const std::vector<SketchEntityConstraintDef>& constraints,
|
||||
const SketchPlane& plane);
|
||||
void set_tool(Mode mode); // switch tool, keep accumulated entities
|
||||
void set_construction(bool c) { m_construction = c; }
|
||||
void set_polygon_sides(int n) { m_polygon_sides = (n < 3 ? 3 : n); }
|
||||
void set_polygon_circumscribed(bool c) { m_polygon_circumscribed = c; }
|
||||
void finish(); // emit accumulated entities, end session
|
||||
void cancel();
|
||||
bool is_active() const { return m_active; }
|
||||
bool has_entities() const { return !m_entities.empty(); }
|
||||
bool on_mouse(wxMouseEvent& evt, GLCanvas3D& canvas);
|
||||
void render(GLCanvas3D& canvas);
|
||||
|
||||
// Persistent committed sketches to draw even when no session is active (e.g. an
|
||||
// un-consumed sketch left visible after its extrude is removed). Each carries its
|
||||
// own plane. render() draws these as translucent faces + outlines.
|
||||
struct DisplaySketch { std::vector<SketchEntity> entities; SketchPlane plane; int feature{-1}; };
|
||||
void set_display_sketches(std::vector<DisplaySketch> ds) { m_display_sketches = std::move(ds); }
|
||||
bool has_display() const { return m_active || !m_display_sketches.empty()
|
||||
|| (m_solid_bodies != nullptr && !m_solid_bodies->empty())
|
||||
|| !m_datum_planes.empty()
|
||||
|| m_ex_active || m_mv_active || m_fl_active
|
||||
|| m_hl_active || m_th_active || m_sh_active; }
|
||||
|
||||
// Solid topology selection on the committed bodies: clicking a solid cycles
|
||||
// whole-solid -> face -> edge (Onshape-style) to target fillet/chamfer/extrude. With
|
||||
// multiple bodies the pick resolves WHICH body was hit (per-triangle body id).
|
||||
enum class SolidSel { None, Whole, Face, Edge };
|
||||
// Point the tool at the current bodies + their concatenated tessellation (non-owning;
|
||||
// pass nullptr to clear). Call after each recompute — selection resets (ids invalidate).
|
||||
// tri_face = per-triangle face id within its body; tri_body = per-triangle body index.
|
||||
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 clear_solid_selection();
|
||||
// Select a whole body by index (from the Parts list) — Whole-level highlight, no face/edge.
|
||||
// body < 0 or out of range clears the selection.
|
||||
void select_body(int body);
|
||||
|
||||
// Move-body gizmo (M5): translate a whole body with three world-axis drag arrows
|
||||
// (X red / Y green / Z blue) anchored at the body centroid. Display-only — the host
|
||||
// keeps a per-body Transform3d and re-feeds the moved display/pick meshes; the OCCT
|
||||
// shape (and thus face/edge global ids) is never touched. Drag fires on_body_move_changed
|
||||
// live; a stationary click on an arrow opens the inline offset editor for that axis.
|
||||
void set_move_gizmo(int body, const Vec3d& pivot, const Transform3d& base_xform);
|
||||
void clear_move_gizmo();
|
||||
bool moving_body() const { return m_mv_active; }
|
||||
int move_body_index() const { return m_mv_body; }
|
||||
// F key forwarded from the canvas (Prepare's Place on Face): returns true if it acted.
|
||||
bool request_place_on_face() { return on_place_on_face ? on_place_on_face() : false; }
|
||||
std::function<bool()> on_place_on_face;
|
||||
std::function<void(int body, const Transform3d& xform)> on_body_move_changed;
|
||||
// Fired on each cycle change: (level 0=None/1=Whole/2=Face/3=Edge, body index, face id, edge id).
|
||||
std::function<void(int level, int body, int face, int edge)> on_solid_selection_changed;
|
||||
// Click a committed sketch overlay (no live session) -> select that loop: the Sketch
|
||||
// feature index + the clicked closed-region index within it (-1 = no specific loop).
|
||||
std::function<void(int feature, int region)> on_display_sketch_selected;
|
||||
// Entities forming the currently click-selected loop (for a per-loop extrude); empty
|
||||
// if no loop is selected.
|
||||
std::vector<SketchEntity> selected_loop_entities() const;
|
||||
// Per closed loop, the indices into `ents` that form it (for hiding already-extruded
|
||||
// loops from the committed-sketch overlay).
|
||||
std::vector<std::vector<int>> region_entity_indices(const std::vector<SketchEntity>& ents) const;
|
||||
void clear_display_pick() { m_display_pick = -1; m_display_pick_region = -1; }
|
||||
|
||||
// Visual Extrude gizmo (C5b). The Extrude tool is a DesignPanel docked card, so the
|
||||
// sketch tool is NOT active during it; the panel feeds the profile plane + a 2D centroid
|
||||
// (arrow anchor) + the live depths/flags, and the tool renders an in-canvas world-space
|
||||
// depth arrow along plane.normal with a draggable handle + editable label. TwoSided draws
|
||||
// a second arrow along -normal driven by depth2. Drag/edit fire on_extrude_depth_changed
|
||||
// back to the panel, which writes the spin value + refreshes the ghost preview.
|
||||
void set_extrude_gizmo(const SketchPlane& plane, const Vec2d& centroid,
|
||||
double depth, double depth2, bool two_sided, bool flip);
|
||||
void clear_extrude_gizmo();
|
||||
// (new_depth, second_side): second_side=false drives the primary depth, true the 2nd side.
|
||||
std::function<void(double depth, bool second)> on_extrude_depth_changed;
|
||||
|
||||
// Visual Fillet/Chamfer gizmo. The Dressup tool is a DesignPanel docked card, so the sketch
|
||||
// tool is NOT active during it; when a solid EDGE is picked the panel passes the body centroid
|
||||
// + current radius and the tool anchors a world-space radius arrow at the picked edge midpoint
|
||||
// (from m_sel_edge_pts), perpendicular to the edge, pointing outward (away from the centroid).
|
||||
// Dragging the arrow changes the radius live; a stationary click opens the inline editor; both
|
||||
// fire on_fillet_radius_changed back to the panel, which writes the spin + refreshes the ghost.
|
||||
// Returns true if it could anchor (needs a picked edge with >=2 sample points).
|
||||
bool set_fillet_gizmo(const Vec3d& body_centroid, double radius);
|
||||
void clear_fillet_gizmo();
|
||||
bool filleting() const { return m_fl_active; }
|
||||
std::function<void(double radius)> on_fillet_radius_changed;
|
||||
|
||||
// Visual Hole gizmo. Like Dressup, the Hole tool is a DesignPanel docked card, so the sketch
|
||||
// tool is NOT active during it; the panel passes the hole plane + position + diameter + depth +
|
||||
// through flag, and the tool draws an on-plane footprint circle plus a radial diameter arrow,
|
||||
// a normal-axis depth arrow (only when !through), and a draggable centre marker. Dragging the
|
||||
// centre repositions (plane u/v), the diameter arrow resizes, the depth arrow deepens — all
|
||||
// live; a stationary click on an arrow opens its inline editor. Every change fires
|
||||
// on_hole_changed back to the panel, which writes the spins + refreshes the ghost.
|
||||
void set_hole_gizmo(const SketchPlane& plane, double x, double y,
|
||||
double diameter, double depth, bool through);
|
||||
// Provide the face (u,v) bounds so the hole's construction dims read from the face sides.
|
||||
void set_hole_face_bounds(bool has, double umin, double umax, double vmin, double vmax);
|
||||
void clear_hole_gizmo();
|
||||
bool holing() const { return m_hl_active; }
|
||||
std::function<void(double x, double y, double diameter, double depth)> on_hole_changed;
|
||||
|
||||
// Visual Thread gizmo. Same docked-card story as Hole: the panel feeds the thread plane +
|
||||
// axis position + nominal radius + length; the tool draws an on-plane footprint circle plus a
|
||||
// radial radius arrow and a normal-axis length arrow (always shown — a thread has no "through")
|
||||
// and a draggable centre. Pitch/depth/internal stay in the card. Drag is live; a stationary
|
||||
// click on an arrow opens its inline editor; every change fires on_thread_changed.
|
||||
void set_thread_gizmo(const SketchPlane& plane, double x, double y,
|
||||
double radius, double height);
|
||||
void clear_thread_gizmo();
|
||||
bool threading() const { return m_th_active; }
|
||||
std::function<void(double x, double y, double radius, double height)> on_thread_changed;
|
||||
|
||||
// Visual Shell gizmo. The panel passes the picked open-face centroid + an inward direction
|
||||
// (-outward normal) + the current wall thickness; the tool anchors a single thickness arrow
|
||||
// there (mirrors the fillet radius arrow). Dragging sets the thickness live; a stationary
|
||||
// click opens the inline editor; both fire on_shell_thickness_changed.
|
||||
void set_shell_gizmo(const Vec3d& face_centroid, const Vec3d& inward_dir, double thickness);
|
||||
void clear_shell_gizmo();
|
||||
bool shelling() const { return m_sh_active; }
|
||||
std::function<void(double thickness)> on_shell_thickness_changed;
|
||||
|
||||
// Datum/reference planes (Plane feature) carry no solid; the panel feeds their resolved
|
||||
// SketchPlanes so they render as translucent rectangles in feature mode (otherwise a
|
||||
// Plane feature is invisible in the canvas).
|
||||
void set_datum_planes(std::vector<SketchPlane> planes) { m_datum_planes = std::move(planes); }
|
||||
|
||||
// Visual Revolve gizmo. The panel feeds the sketch plane + profile centroid + axis (0=plane X,
|
||||
// 1=plane Y) + angle + flip while its Revolve card is open; an angle-arc is drawn in the
|
||||
// revolve plane at the profile radius. Dragging the tip sweeps the angle, a stationary click
|
||||
// edits it; both fire on_revolve_angle_changed.
|
||||
void set_revolve_gizmo(const SketchPlane& plane, const Vec2d& centroid,
|
||||
int axis_sel, double angle, bool flip);
|
||||
void clear_revolve_gizmo();
|
||||
bool revolving() const { return m_rv_active; }
|
||||
std::function<void(double angle)> on_revolve_angle_changed;
|
||||
|
||||
// Visual Pattern gizmo. Linear: a 3D arrow along the world axis (plane X/Y per `dir`) of length
|
||||
// spacing*(count-1) with a tick at each copy; dragging the end sets the spacing. Circular: a
|
||||
// revolve-style angle-arc about the plane normal through the plane origin sweeping `angle`.
|
||||
// Both fire on_pattern_changed (spacing for linear, angle for circular).
|
||||
void set_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 { return m_pt_active; }
|
||||
std::function<void(double value)> on_pattern_changed;
|
||||
|
||||
// Constrain mode: load an already-committed profile for entity picking +
|
||||
// constraint application (the geometry is solved in the kernel, not here).
|
||||
void begin_constrain(const SketchProfile& prof, const SketchPlane& plane);
|
||||
bool is_constraining() const { return m_active && m_mode == Mode::Constrain; }
|
||||
// Replace the displayed profile (e.g. after the kernel re-solved it).
|
||||
void set_profile_points(const std::vector<Vec2d>& pts) { m_points = pts; }
|
||||
// The currently picked segment's endpoint indices into the profile.
|
||||
bool selected_segment(int& a, int& b) const;
|
||||
|
||||
// Entity-aware Constrain (Fase 4.2): load a committed entity sketch and pick
|
||||
// Line entities (constraints are solved against entity endpoints in the kernel).
|
||||
void begin_constrain_entities(const std::vector<SketchEntity>& ents, const SketchPlane& plane);
|
||||
bool is_constraining_entities() const { return m_active && m_mode == Mode::Constrain && m_constrain_entities; }
|
||||
|
||||
// In-canvas bounding-box transform of imported Text/SVG art (replaces the Move/Scale
|
||||
// dialog). `base_regions` are the untransformed region contours; the gizmo shows the
|
||||
// current bbox with 4 corner scale-handles + a centre move-handle. Dragging fires
|
||||
// on_imported_transform live with the new offset/scale, which the host writes back to
|
||||
// the feature. Exiting (Esc/right-click) ends the session.
|
||||
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);
|
||||
std::function<void(int feat, Vec2d offset, double scale_x, double scale_y)> on_imported_transform;
|
||||
// Up to two picked line-entity indices; returns true if at least one is picked.
|
||||
bool selected_constrain_entities(int& e0, int& e1) const { e0 = m_pick0; e1 = m_pick1; return m_pick0 >= 0; }
|
||||
// Third pick slot (Symmetric axis): only filled after slots 0 and 1 are set.
|
||||
int pick2() const { return m_pick2; }
|
||||
// Plane-coords of the click that filled slot 0 (for pick-point edit ops: trim/extend).
|
||||
bool pick0_point(Vec2d& out) const { out = m_pick0_pt; return m_pick0 >= 0; }
|
||||
// Refresh the displayed entities after the kernel re-solved them.
|
||||
void set_constrain_entities(const std::vector<SketchEntity>& ents) { m_entities = ents; }
|
||||
// Constraint manager (C3.4): entity indices the panel asks to highlight (the
|
||||
// entities a selected constraint references); rendered yellow in Constrain mode.
|
||||
void set_constraint_highlight(std::vector<int> v) { m_constraint_hl = std::move(v); }
|
||||
// The committed feature's constraints, supplied so Constrain-mode render can draw
|
||||
// an iconic glyph badge per constraint near its primary entity (C3.4b).
|
||||
void set_constraint_glyphs(std::vector<SketchEntityConstraintDef> v) { m_constrain_cons = std::move(v); }
|
||||
|
||||
// Line tool: after a single segment is placed, the panel pops a length dialog
|
||||
// (length, angle_deg are the as-drawn values); it then resolves via
|
||||
// apply_segment_length() (exact length) or keep_segment_as_drawn() (cancel).
|
||||
std::function<void(double length, double angle_deg)> on_segment_drawn;
|
||||
void apply_segment_length(double len); // rescale the pending segment, then commit it
|
||||
void keep_segment_as_drawn(); // commit the pending segment unchanged
|
||||
|
||||
// Live readout while drawing a Line/Polyline segment (anchor->cursor metrics).
|
||||
std::function<void(double length, double angle_deg, bool locked)> on_cursor_metrics;
|
||||
|
||||
// DoF feedback (P3): solver state after each live solve. dof>0 = under-constrained,
|
||||
// dof==0 = fully constrained, ok==false = conflicting/inconsistent constraints.
|
||||
// has_constraints is false while the sketch carries no driving constraints yet.
|
||||
std::function<void(int dof, bool ok, bool has_constraints)> on_solve_state;
|
||||
|
||||
// Selection (Mode::Select): pick points/lines/arcs/circles of the in-session
|
||||
// sketch; Shift/Ctrl extends, double-click grabs the whole connected loop.
|
||||
const std::vector<int>& selection() const { return m_selection; }
|
||||
void clear_selection();
|
||||
void delete_selected(); // erase selected entities
|
||||
std::function<void(int count)> on_selection_changed;
|
||||
|
||||
// Dimension tool: infer a driving dimension from the current selection and set
|
||||
// it exactly. Sizing: 1 line=Length, 1 circle=Diameter, 1 arc=Radius,
|
||||
// 2 lines=Angle. Positioning (a value of 0 makes them coincident):
|
||||
// 2 point-likes (point/circle-centre/arc-centre)=Distance, moving the 2nd onto
|
||||
// the 1st; a point-like + a line=DistanceToLine, moving the point-like's
|
||||
// reference point onto/away-from the line (e.g. a circle centre onto an axis).
|
||||
enum class DimType { None, Length, Diameter, Radius, Angle, Distance, DistanceToLine };
|
||||
DimType dimension_kind() const; // what the selection supports (None if invalid)
|
||||
double dimension_current() const; // current value, to pre-fill the dialog
|
||||
void apply_dimension(double v); // set it exactly, then clear the selection
|
||||
|
||||
// Onshape-style Dimension tool (Mode::Dimension): with the tool active you click
|
||||
// directly in the viewport — 2 points -> Distance, a line -> Length, a circle ->
|
||||
// Diameter, an arc -> Radius, a point then a line -> DistanceToLine. A quote line
|
||||
// with extension lines, arrowheads and a numeric label is PLACED in the sketch and
|
||||
// drives the geometry (auto-offset; label editable). on_dimension_pick_complete
|
||||
// fires when a pick resolves so the panel can pop the value card pre-filled.
|
||||
std::function<void(double current)> on_dimension_pick_complete;
|
||||
DimType pending_dimension_type() const; // type of the dim awaiting a value, or None
|
||||
void set_dimension_value(double v); // apply the typed value to the placed dim
|
||||
void cancel_dimension_value(); // keep the placed dim at its measured value
|
||||
|
||||
// Onshape-style in-canvas value editing: open a floating text editor at the given
|
||||
// screen pixel, pre-filled with `current`; commit applies the value, cancel keeps
|
||||
// it. The owner (DesignCanvas) hosts the wxTextCtrl over the GL canvas. This is the
|
||||
// single numeric-entry path for all sketch dimensions (replaces the modal cards).
|
||||
std::function<void(wxPoint screen_px, double current,
|
||||
std::function<void(double)> commit,
|
||||
std::function<void()> cancel)> on_inline_edit;
|
||||
// Force-close any open inline field (runs its cancel = keep-as-drawn). Used by the polyline
|
||||
// terminators (right-click / double-click) to end the chain even mid per-segment edit.
|
||||
std::function<void()> on_inline_dismiss;
|
||||
|
||||
// Bottom-right viewport readout: emitted each frame with the active tool's current
|
||||
// values (live segment length/angle while drawing a line, or the selected entity's
|
||||
// characteristic dimensions). Empty string -> hide the HUD. The owner (DesignCanvas)
|
||||
// shows it as a floating corner label over the GL canvas.
|
||||
std::function<void(const std::string&)> on_readout;
|
||||
|
||||
// Driving dimension constraints accumulated during the session (the Dimension
|
||||
// tool records a SketchEntityConstraintDef per applied dimension); committed
|
||||
// alongside the entities on finish() so the kernel keeps enforcing them.
|
||||
const std::vector<SketchEntityConstraintDef>& constraints() const { return m_constraints; }
|
||||
|
||||
// Emitted by finish() with the accumulated entities + driving constraints.
|
||||
std::function<void(const std::vector<SketchEntity>&,
|
||||
const std::vector<SketchEntityConstraintDef>&,
|
||||
const SketchPlane&)> on_commit_entities;
|
||||
// Legacy single-profile commit (kept for compatibility; unused by entity tools).
|
||||
std::function<void(const SketchProfile&, const SketchPlane&)> on_commit;
|
||||
// Emitted when a closed-loop face is clicked in Select mode (Onshape: a region
|
||||
// becomes a selectable face → extrude). The panel commits the sketch + extrudes.
|
||||
std::function<void()> on_face_selected;
|
||||
// Esc pressed while the tool is active: exit/cancel the session (the panel restores
|
||||
// Feature mode). Layered: an in-progress entity or a non-Select draw tool is dropped
|
||||
// first; a second Esc exits the session.
|
||||
std::function<void()> on_exit;
|
||||
std::function<void()> on_move_exit; // right-click finished the move-body gizmo
|
||||
void request_exit();
|
||||
// Ctrl+Z / Ctrl+Shift+Z (Ctrl+Y) while the Design canvas is focused: undo/redo the
|
||||
// committed feature history. The tool just forwards to the host, which owns the
|
||||
// CadDocument (the tool has no document of its own). redo == true requests redo.
|
||||
std::function<void(bool /*redo*/)> on_undo_redo;
|
||||
void request_undo_redo(bool redo);
|
||||
|
||||
private:
|
||||
bool screen_to_plane(GLCanvas3D& canvas, const wxMouseEvent& evt, Vec2d& out) const;
|
||||
bool near_first(const Vec2d& p) const;
|
||||
|
||||
// Onshape-style angle inference: snap the direction anchor->raw to the nearest
|
||||
// of {0,30,45,60,90} deg (replicated every 90 deg) when within tolerance, keeping
|
||||
// the same length. Sets `locked` when a snap was applied. Suppressed by m_snap_off.
|
||||
Vec2d snap_dir(const Vec2d& anchor, const Vec2d& raw, bool& locked) const;
|
||||
// Snap a placed point onto the nearest existing entity endpoint within ~8 px so
|
||||
// chains join across entities (a line + an arc can close into one loop). Shift
|
||||
// disables it. `snapped` reports whether a vertex was hit.
|
||||
Vec2d snap_vertex(GLCanvas3D& canvas, const wxMouseEvent& evt, const Vec2d& raw, bool& snapped) const;
|
||||
|
||||
// --- P1 inference / auto-constraint engine ---------------------------------
|
||||
// Plane-units tolerance equivalent to ~`px` screen pixels at the cursor.
|
||||
double screen_tol(GLCanvas3D& canvas, const wxMouseEvent& evt, const Vec2d& at, double px = 8.0) const;
|
||||
// Run kernel inference at the cursor, cache the target for the hint renderer.
|
||||
InferenceSnap infer_at(GLCanvas3D& canvas, const wxMouseEvent& evt, const Vec2d& raw) const;
|
||||
// True if m_constraints already holds an equivalent Coincident between the two refs.
|
||||
bool has_coincident(int ea, SketchPointRole ra, int eb, SketchPointRole rb) const;
|
||||
// Append candidates, live-solve, and roll back the batch if it turns the system
|
||||
// inconsistent. Returns true when the batch was kept.
|
||||
bool try_add_constraints(const std::vector<SketchEntityConstraintDef>& cands);
|
||||
// After entities [base, end) were committed, auto-emit the constraints that make
|
||||
// the new geometry stick: Coincident between co-located endpoints (so loops close
|
||||
// on their own) and Horizontal/Vertical on axis-aligned new segments.
|
||||
void infer_auto_constraints(int base);
|
||||
|
||||
// Selection helpers (Mode::Select).
|
||||
int hit_test(const Vec2d& p, double tol) const; // nearest entity within tol, or -1
|
||||
std::vector<int> connected_loop(int seed) const; // entities joined by shared endpoints
|
||||
void apply_angle_between(int ia, int ib, double deg); // rotate line B to set the A^B angle
|
||||
bool selection_valid() const; // all selection indices in range
|
||||
void record_dimension_constraint(double v); // append the driving def for the selection
|
||||
void resolve_live(); // solve accumulated constraints on m_entities now
|
||||
// Drag-aware re-solve: pins the dragged point at its current coord and lets the
|
||||
// solver move the rest (Slvs dragged[]). Used live while a point grab is active.
|
||||
void resolve_live_drag(int dragged_ei, SketchPointRole dragged_role);
|
||||
|
||||
// Placed dimension annotation. References entity points/entities (not cached
|
||||
// coords) so the quote follows the geometry as the kernel solves it. `value`
|
||||
// drives the constraint stored at index `con` in m_constraints.
|
||||
struct DimAnnot {
|
||||
DimType kind{DimType::None};
|
||||
int ea{-1}; SketchPointRole ra{SketchPointRole::P0};
|
||||
int eb{-1}; SketchPointRole rb{SketchPointRole::P0};
|
||||
double value{0.0};
|
||||
double side{1.0}; // perpendicular offset sign of the quote line
|
||||
int con{-1}; // slot in m_constraints driving this dimension
|
||||
Vec2d label_pos{0, 0}; // cached label centre (plane coords), for picking
|
||||
};
|
||||
|
||||
// --- Onshape-style visual editing: handles + parametric feature grouping -----
|
||||
// A draggable handle on a defining point of an entity (or a derived point of a
|
||||
// feature group). GUI-only; recomputed from solved geometry every frame (never
|
||||
// persisted), so handles always track the current solve. Derived roles (radius,
|
||||
// slot width/centres, rect corners, polygon vertex, ellipse axes) let tools that
|
||||
// decompose into raw Line/Arc entities still expose their parametric controls.
|
||||
enum class HandleRole { P0, P1, Center, RadiusHandle,
|
||||
SlotCenter0, SlotCenter1, SlotWidth,
|
||||
RectCorner, PolygonVertex, MajorAxis, MinorAxis, BSplineCtrl };
|
||||
struct Handle {
|
||||
HandleRole role{HandleRole::P0};
|
||||
int ei{-1}; // primary entity index
|
||||
int group{-1}; // index into m_features, or -1 for a raw-entity handle
|
||||
int ctrl_index{-1}; // BSplineCtrl pole index
|
||||
Vec2d pos{0, 0}; // current plane coords (recomputed each frame)
|
||||
bool hovered{false};
|
||||
};
|
||||
// A parametric grouping over a contiguous run of entities produced by one gesture.
|
||||
// Slot/Rect/Polygon/etc. have no SketchEntity type of their own — they decompose
|
||||
// into raw Line/Arc entities — so the Feature carries the gesture's anchors so
|
||||
// derived handles + characteristic dimensions can be reconstructed.
|
||||
enum class FeatureKind { Free, Line, Circle, Arc, CornerRect, CenterRect,
|
||||
Slot, ArcSlot, Polygon, Ellipse, RoundedRect, BSpline };
|
||||
struct Feature {
|
||||
FeatureKind kind{FeatureKind::Free};
|
||||
int begin{0}, end{0}; // [begin,end) into m_entities
|
||||
Vec2d c0{0, 0}, c1{0, 0}; // slot centres / rect corners / ellipse centre+major
|
||||
double param{0.0}; // slot half-width / polygon circumradius / fillet radius
|
||||
int sides{0}; // polygon side count
|
||||
};
|
||||
// Build the live handle set for the current selection / just-drawn feature.
|
||||
std::vector<Handle> build_handles() const;
|
||||
// Nearest handle to plane-point p within tol; fills `out`. (Phase A: stub.)
|
||||
bool hit_test_handle(const Vec2d& p, double tol, Handle& out) const;
|
||||
// Move a handle to `target`, applying the role-specific geometry edit + re-solve.
|
||||
void set_handle(const Handle& h, const Vec2d& target);
|
||||
// On a no-button move, recompute the hovered handle; returns true iff it changed
|
||||
// (so the caller forces exactly one repaint). No-op for non-Moving events.
|
||||
bool update_hover(GLCanvas3D& canvas, wxMouseEvent& evt);
|
||||
// Index of the Feature whose [begin,end) entity span contains ei, or -1.
|
||||
int feature_of(int ei) const;
|
||||
// Re-detect parametric Feature groups (polygon / rect / slot) from the raw entity
|
||||
// list — used when a committed sketch is re-opened, where m_features is empty.
|
||||
void rebuild_features_from_entities();
|
||||
// Open/close a Feature record around the entities a single gesture appends.
|
||||
void begin_feature(FeatureKind kind);
|
||||
void end_feature(const Vec2d& c0 = Vec2d(0, 0), const Vec2d& c1 = Vec2d(0, 0),
|
||||
double param = 0.0, int sides = 0);
|
||||
|
||||
bool point_at(int ei, SketchPointRole role, Vec2d& out) const; // current coords
|
||||
void set_point(int ei, SketchPointRole role, const Vec2d& v); // move an entity point
|
||||
bool hit_test_point(const Vec2d& p, double tol, int& ei, SketchPointRole& role) const;
|
||||
int hit_test_dimension(const Vec2d& p, double tol) const; // nearest dim label
|
||||
void edit_dimension(int di); // reopen value card for di
|
||||
// Representative plane-coords anchor of a dimension (label centre if known, else a
|
||||
// geometric midpoint/centre) — where the in-canvas value editor is positioned.
|
||||
Vec2d dim_anchor(const DimAnnot& a) const;
|
||||
// Open the in-canvas value editor on dimension `di` (falls back to the modal
|
||||
// pick-complete callback when no inline-edit host is wired).
|
||||
void open_value_editor(int di);
|
||||
// In-canvas editor for a line's angle-to-horizontal; commit rotates the segment
|
||||
// geometrically about P0 (no single-line angle constraint in libslvs).
|
||||
void open_angle_editor(int ei);
|
||||
void set_line_angle(int ei, double deg);
|
||||
// Draw-then-edit (all creation tools): open the inline editor on the freshly-drawn
|
||||
// selection's PRIMARY characteristic value. Called after render_live_quotes has computed
|
||||
// the selection's quotes, so it dispatches on the same live-quote state a Select-mode
|
||||
// click would use.
|
||||
void open_primary_autoedit();
|
||||
// Compact "current values" string for the bottom-right HUD (see on_readout).
|
||||
std::string build_readout() const;
|
||||
// Open a characteristic live quote as a TENTATIVE driving dimension: the constraint is
|
||||
// appended only if the user commits a value (Enter); cancel (Esc) adds nothing — so
|
||||
// drawing never silently over-constrains. (place_dimension is the eager Select-mode twin.)
|
||||
void open_next_autoedit_dim(); // opens m_autoedit_dims[idx]; commit -> next, Esc -> stop
|
||||
void arm_polyline_segment_edit();// per-segment Length+Angle edit of the pending chain vertex
|
||||
// In-canvas editors for a regular polygon's side length and orientation. Both edit
|
||||
// the whole loop GEOMETRICALLY (polygon has no centre entity): side scales it
|
||||
// uniformly about its centre, angle rotates it. set_polygon_radius is the shared
|
||||
// uniform-scale primitive (circumradius).
|
||||
void open_polygon_side_editor(int fi);
|
||||
void open_polygon_angle_editor(int fi);
|
||||
void set_polygon_side(int fi, double side);
|
||||
void set_polygon_angle(int fi, double deg);
|
||||
void set_polygon_radius(int fi, double R);
|
||||
// Arc sweep-angle quote: geometric edit (SLVS angle is line-to-line only). Keeps the
|
||||
// arc start point + radius fixed and moves the end point to span `deg` degrees.
|
||||
void open_arc_angle_editor(int ei);
|
||||
void set_arc_sweep(int ei, double deg);
|
||||
// Arc handle drag (3 grips): Center rigidly translates; the START point changes the
|
||||
// radius (keeps both sweep angles); the END point changes the sweep angle (keeps the
|
||||
// radius). Geometric — no solver (SLVS has no arc radius/angle handle concept here).
|
||||
void drag_arc_handle(int ei, SketchPointRole role, const Vec2d& target);
|
||||
// Ellipse axis labels (geometric edit of the semi-axes a/b; phi via the major grip).
|
||||
void open_ellipse_axis_editor(int ei, bool major);
|
||||
void set_ellipse_axis(int ei, bool major, double v);
|
||||
void set_ellipsearc_sweep(int ei, double deg); // draw-then-edit: included sweep of an elliptical arc
|
||||
void set_rect_angle(int fi, double deg); // draw-then-edit: orientation of an oblique rect
|
||||
// EllipseArc endpoint drag: Center translates; P0/P1 move the sweep start/end to the
|
||||
// parametric angle of the cursor on the ellipse frame (radius/shape preserved).
|
||||
void drag_ellipsearc_handle(int ei, SketchPointRole role, const Vec2d& target);
|
||||
// Drop orientation constraints (H/V/Parallel/Perp/Angle/LockX/LockY) on entities in
|
||||
// [begin,end). A ROTATION makes inferred per-edge H/V inconsistent, so re-solving
|
||||
// against them collapses the shape — drop them first (fixes up DimAnnot.con indices).
|
||||
void drop_orientation_constraints(int begin, int end);
|
||||
// Drop every live constraint that references entity `ei` (Trim/Extend slide an endpoint,
|
||||
// invalidating its constraints) and fix the dimensions' cached constraint indices.
|
||||
void drop_constraints_referencing(int ei);
|
||||
// Standalone Trim/Extend scissors on the LIVE sketch: pick the entity nearest `p` (within
|
||||
// `tol` plane units) and cut it back to / out to its nearest intersection with the others.
|
||||
// Returns true if an entity was modified.
|
||||
bool apply_live_trim(const Vec2d& p, double tol, bool extend);
|
||||
// Pure-computation hover preview for Trim/Extend: mirror apply_live_trim's pick + the
|
||||
// engine's cut on a COPY (mutating nothing) and return, via `removed_poly`, the polyline
|
||||
// of the sub-portion a click would REMOVE (Trim) or ADD (Extend). `subject_ei` is the
|
||||
// picked entity. Returns false if nothing is in range or nothing would change.
|
||||
bool compute_trim_preview(const Vec2d& p, double tol, bool extend,
|
||||
int& subject_ei, std::vector<Vec2d>& removed_poly) const;
|
||||
// Drag a polygon vertex while keeping the loop REGULAR: scale + rotate the whole
|
||||
// polygon about its centroid so the grabbed vertex follows `target` (adjusts
|
||||
// circumradius + orientation together).
|
||||
void drag_polygon_vertex(int fi, int ei, SketchPointRole role, const Vec2d& target);
|
||||
double measure_dim(const DimAnnot& a) const; // value from geometry
|
||||
SketchEntityConstraintDef constraint_for(const DimAnnot& a) const; // driving def
|
||||
int place_dimension(DimAnnot a); // create+drive+notify
|
||||
std::string dim_text(const DimAnnot& a) const; // rendered label string
|
||||
void render_dimensions(double unit_per_px); // quote lines + labels
|
||||
// Draw ONE dimension's quote (extension/dimension lines, arrowheads, label) and
|
||||
// return its label centre in out_label; false if the annot can't be drawn. Shared
|
||||
// by render_dimensions (placed driving quotes) and render_live_quotes (live ones).
|
||||
bool draw_dim_quote(const DimAnnot& a, double th, const ColorRGBA& col, Vec2d& out_label);
|
||||
// Live, non-driving characteristic quotes for the entity being edited (point/handle
|
||||
// drag, or a lone selection): the tool's defining dimensions shown Onshape-style so
|
||||
// editing shows live values; click one (m_live_quotes) to promote it to a driving
|
||||
// dim. Self-gates; skips a dim already driven on that entity.
|
||||
void render_live_quotes(double unit_per_px);
|
||||
// Iconic constraint badges (C3.4b): for each m_constrain_cons entry, append a
|
||||
// small screen-constant glyph (H, V, ∥, ⊥, =, ○, …) near its primary entity into
|
||||
// `out`; glyphs touching the same entity stack so they don't overlap.
|
||||
void build_constraint_glyphs(double unit_per_px, std::vector<std::pair<Vec2d, Vec2d>>& out) const;
|
||||
void draw_strokes(GLModel& model, const std::vector<std::pair<Vec2d, Vec2d>>& segs,
|
||||
double hw, const ColorRGBA& color);
|
||||
void draw_text(GLModel& model, const std::string& s, const Vec2d& center,
|
||||
double height, const ColorRGBA& color); // GL stroke font
|
||||
|
||||
// Entity builders: append to m_entities (honoring the construction flag).
|
||||
void push_line(const Vec2d& a, const Vec2d& b);
|
||||
void push_closed_lines(const std::vector<Vec2d>& corners);
|
||||
void push_open_chain(const std::vector<Vec2d>& pts);
|
||||
void push_circle(const Vec2d& center, double radius);
|
||||
void push_point(const Vec2d& p);
|
||||
|
||||
// Multi-click tool builders: return the entities for a finished gesture so
|
||||
// both on_mouse (append) and render (preview) share one geometry path.
|
||||
std::vector<SketchEntity> make_three_point_circle(const Vec2d& a, const Vec2d& b, const Vec2d& c) const;
|
||||
std::vector<SketchEntity> make_three_point_arc(const Vec2d& start, const Vec2d& end, const Vec2d& on_arc) const;
|
||||
std::vector<SketchEntity> make_tangent_arc(const Vec2d& start, const Vec2d& end) const;
|
||||
// Center-start-end arc: click center, then start (sets radius), then a third
|
||||
// point whose direction from the center sets the CCW end angle.
|
||||
std::vector<SketchEntity> make_center_arc(const Vec2d& center, const Vec2d& start, const Vec2d& end_dir) const;
|
||||
std::vector<SketchEntity> make_slot(const Vec2d& c0, const Vec2d& c1, double half_width) const;
|
||||
std::vector<SketchEntity> make_arc_slot(const Vec2d& center, const Vec2d& start,
|
||||
const Vec2d& end_dir, double half_width) const;
|
||||
std::vector<SketchEntity> make_rounded_rect(const Vec2d& a, const Vec2d& b, const Vec2d& radius_pt) const;
|
||||
std::vector<SketchEntity> rounded_rect_entities(double xmin, double ymin,
|
||||
double xmax, double ymax, double r) const;
|
||||
// Rounded-rect grouped edit: W/H/fillet-R labels rebuild the 8-entity span in place.
|
||||
void open_rounded_rect_editor(int fi, int which); // 0=Width 1=Height 2=fillet R
|
||||
void set_rounded_rect(int fi, double w, double h, double r);
|
||||
// Arc-slot grouped edit: centreline-radius + width labels rebuild the 4-arc span.
|
||||
void open_arc_slot_editor(int fi, bool radius); // true=centreline R, false=width
|
||||
void set_arc_slot(int fi, double Rc, double w);
|
||||
// Grouped derived-handle drag: resize an axis-aligned rect by a corner (opposite corner
|
||||
// fixed); move a slot end by its cap centre. Both rebuild the feature span geometrically.
|
||||
void drag_rect_corner(int fi, const Vec2d& cursor);
|
||||
void drag_slot_handle(int fi, const Vec2d& cursor);
|
||||
std::vector<SketchEntity> make_polygon(const Vec2d& center, const Vec2d& vertex, int sides) const;
|
||||
// Ellipse: click center, then major-axis endpoint (sets a + rotation phi),
|
||||
// then a point whose perpendicular distance to the major axis sets b.
|
||||
std::vector<SketchEntity> make_ellipse(const Vec2d& center, const Vec2d& major_end,
|
||||
const Vec2d& minor_pt) const;
|
||||
// Elliptical arc: same 3 axis clicks, then start and end points whose parametric
|
||||
// angles on the ellipse bound the CCW sweep.
|
||||
std::vector<SketchEntity> make_bspline(const std::vector<Vec2d>& ctrl) const;
|
||||
std::vector<SketchEntity> make_ellipse_arc(const Vec2d& center, const Vec2d& major_end,
|
||||
const Vec2d& minor_pt, const Vec2d& start_pt,
|
||||
const Vec2d& end_pt) const;
|
||||
void append_entities(const std::vector<SketchEntity>& ents);
|
||||
void draw_entities_preview(const std::vector<SketchEntity>& ents, const ColorRGBA& color);
|
||||
|
||||
// --- In-canvas edit-op gizmo (Fillet/Chamfer/Offset/Mirror toolbar tools) --------
|
||||
// These replace the docked numeric card: pick the entities in-canvas, then a draggable
|
||||
// arrow with a value label is projected toward the corner/centre (Fillet/Chamfer/Offset),
|
||||
// or a two-phase pick (axis line, then targets) drives a live mirrored ghost. The
|
||||
// SketchEngine op is recomputed live so a translucent ghost previews the result; confirm
|
||||
// applies the geometry and binds constraints into m_constraints (try_add_constraints).
|
||||
bool op_corner(int a, int b, Vec2d& C, Vec2d& bis, double& theta) const; // line-line vertex + inward bisector
|
||||
void op_pick(int ei); // route an entity pick to the active op
|
||||
void recompute_op_ghost(); // rebuild m_op_ghost from m_op_value
|
||||
void render_op_gizmo(double unit_per_px); // ghost + arrow + value label (caches m_op_label)
|
||||
bool hit_test_op_arrow(const Vec2d& p, double tol) const;
|
||||
void drag_op_arrow(const Vec2d& target); // project cursor onto m_op_dir -> value
|
||||
void open_op_editor(); // inline-edit the value label
|
||||
void confirm_op(); // apply + bind, then reset for the next gesture
|
||||
void reset_op(); // clear gizmo state (keeps the tool active)
|
||||
bool op_ready() const; // required entities picked -> arrow/ghost live
|
||||
|
||||
// Sample an entity into a 2D polyline for the overlay renderer.
|
||||
std::vector<Vec2d> entity_polyline(const SketchEntity& e, bool& closed) const;
|
||||
|
||||
// Closed regions formed by the current (non-construction) entities: each a CCW-
|
||||
// ordered boundary polygon on the plane. A circle is its own region; line/arc
|
||||
// chains are walked endpoint-to-endpoint into loops. Used to fill faces.
|
||||
std::vector<std::vector<Vec2d>> closed_regions() const;
|
||||
std::vector<std::vector<Vec2d>> closed_regions(const std::vector<SketchEntity>& ents) const;
|
||||
// Same loops, but each carries the indices of the entities that form it — so a single
|
||||
// loop can be highlighted / extruded on its own (per-region selection on the plate).
|
||||
struct RegionLoop { std::vector<Vec2d> poly; std::vector<int> ents; };
|
||||
std::vector<RegionLoop> region_loops(const std::vector<SketchEntity>& ents) const;
|
||||
// Index of the closed region containing plane-point p (point-in-polygon), or -1.
|
||||
int region_at(const Vec2d& p) const;
|
||||
|
||||
void draw_quad_strip(GLModel& model, const std::vector<Vec2d>& pts, bool closed, const ColorRGBA& color);
|
||||
// half_size is the square marker half-extent in PLANE units. Callers pass a
|
||||
// zoom-scaled value (k / zoom) for screen-constant handles; the default keeps
|
||||
// legacy point markers exactly as before.
|
||||
void draw_vertices(GLModel& model, const std::vector<Vec2d>& pts, const ColorRGBA& color,
|
||||
double half_size = 1.3);
|
||||
void draw_fill(GLModel& model, const std::vector<Vec2d>& poly, const ColorRGBA& color);
|
||||
|
||||
bool m_active{false};
|
||||
SketchPlane m_plane;
|
||||
std::vector<Vec2d> m_points; // clicks of the in-progress entity / chain
|
||||
std::vector<SketchEntity> m_entities; // committed entities of this session
|
||||
bool m_construction{false};
|
||||
int m_polygon_sides{6};
|
||||
bool m_polygon_circumscribed{false};
|
||||
Vec2d m_cursor{0,0};
|
||||
bool m_has_cursor{false};
|
||||
bool m_snap_off{false}; // Shift held -> suppress angle snapping
|
||||
InferenceSnap m_cursor_snap; // last cursor inference target (for hint render)
|
||||
bool m_cursor_locked{false}; // rubber-band segment is angle-locked
|
||||
bool m_awaiting_length{false}; // inline value editor open -> freeze canvas
|
||||
int m_autoedit_seen{-1}; // entity count baseline for draw-then-edit
|
||||
bool m_autoedit_pending{false};// a new entity just committed -> open editor
|
||||
// Draw-then-edit step queue: every characteristic dimension of the freshly-drawn shape
|
||||
// (scalar quote OR geometric editor) becomes one step, opened in sequence over its label.
|
||||
struct AutoEditStep {
|
||||
Vec2d label; // anchor (plane coords) — field opens over this
|
||||
double value; // initial value shown
|
||||
std::function<void(double)> apply; // commit: set the dimension
|
||||
std::vector<int> hi; // entities to highlight while THIS field is open
|
||||
};
|
||||
std::vector<AutoEditStep> m_autoedit_dims; // queued steps to edit in sequence
|
||||
int m_autoedit_dim_idx{-1}; // index into m_autoedit_dims (-1 = idle)
|
||||
std::vector<int> m_selection; // selected entity indices (Mode::Select)
|
||||
std::vector<std::pair<int, SketchPointRole>> m_point_sel; // selected individual points
|
||||
int m_last_mouse_x{0}; // last cursor pos (canvas client px), for
|
||||
int m_last_mouse_y{0}; // anchoring the in-canvas value editor
|
||||
bool m_dragging_point{false}; // a point grab is in progress (Mode::Select)
|
||||
int m_drag_ei{-1}; // entity whose point is being dragged
|
||||
int m_drag_poly_fi{-1}; // >=0 if the grabbed point is a polygon
|
||||
// vertex: drag scales+rotates the loop
|
||||
int m_drag_rect_fi{-1}; // >=0 if dragging an axis-aligned rect corner
|
||||
Vec2d m_drag_rect_anchor{0,0}; // the fixed (opposite) corner
|
||||
int m_drag_slot_fi{-1}; // >=0 if dragging a slot cap centre
|
||||
bool m_drag_slot_c1{false}; // true=cap@c1, false=cap@c0
|
||||
SketchPointRole m_drag_role{SketchPointRole::P0};
|
||||
std::vector<SketchEntityConstraintDef> m_constraints; // driving dims, committed on finish
|
||||
|
||||
// Onshape-style visual editing state.
|
||||
bool m_show_handles{false}; // draw + interact with handles
|
||||
bool m_dragging_handle{false};// a handle grab is in progress
|
||||
Handle m_drag_handle; // the handle being dragged
|
||||
bool m_has_hover_handle{false};// cursor is near a handle (highlight it)
|
||||
Handle m_hover_handle; // the hovered handle (recomputed on move)
|
||||
std::vector<DimAnnot> m_live_quotes; // live non-driving characteristic quotes,
|
||||
// clickable to promote to driving dims
|
||||
Vec2d m_live_poly_side_label{0,0}; // polygon side-length quote label
|
||||
Vec2d m_live_poly_angle_label{0,0}; // polygon orientation quote label
|
||||
int m_live_poly_fi{-1}; // their Feature (geometric edits)
|
||||
Vec2d m_live_arc_angle_label{0,0}; // arc sweep-angle quote label
|
||||
int m_live_arc_ei{-1}; // the arc it belongs to (geometric edit)
|
||||
Vec2d m_live_ellipse_major_label{0,0}; // ellipse semi-major quote label
|
||||
Vec2d m_live_ellipse_minor_label{0,0}; // ellipse semi-minor quote label
|
||||
Vec2d m_live_ellipsearc_sweep_label{0,0}; // elliptical-arc sweep quote label
|
||||
int m_live_ellipse_ei{-1}; // the ellipse the labels belong to
|
||||
Vec2d m_live_obrect_angle_label{0,0}; // oblique-rect orientation quote label
|
||||
int m_live_obrect_fi{-1}; // an OBLIQUE rect Feature (angle editable)
|
||||
Vec2d m_live_rrect_w_label{0,0}; // rounded-rect width quote label
|
||||
Vec2d m_live_rrect_h_label{0,0}; // rounded-rect height quote label
|
||||
Vec2d m_live_rrect_r_label{0,0}; // rounded-rect fillet-radius label
|
||||
int m_live_rrect_fi{-1}; // the rounded-rect Feature (rebuild edits)
|
||||
Vec2d m_live_aslot_r_label{0,0}; // arc-slot centreline-radius label
|
||||
Vec2d m_live_aslot_w_label{0,0}; // arc-slot width label
|
||||
int m_live_aslot_fi{-1}; // the arc-slot Feature (rebuild edits)
|
||||
std::vector<Feature> m_features; // parametric groups over m_entities
|
||||
int m_open_feature{-1}; // index of the Feature being built, or -1
|
||||
|
||||
// In-canvas edit-op gizmo state (Fillet/Chamfer/Offset/Mirror). GUI-only, reset by
|
||||
// set_tool/cancel. Fillet/Chamfer: m_op_a,m_op_b = the two lines; Offset: m_op_a = src;
|
||||
// Mirror: m_op_a = axis line, m_mirror_targets = entities to mirror.
|
||||
int m_op_a{-1};
|
||||
int m_op_b{-1};
|
||||
double m_op_value{0.0}; // radius / setback / signed offset distance
|
||||
Vec2d m_op_anchor{0,0}; // arrow base (corner vertex / entity midpoint)
|
||||
Vec2d m_op_dir{0,0}; // unit arrow direction (inward bisector / outward normal)
|
||||
Vec2d m_op_label{1e18,1e18}; // cached arrow-label centre, for picking
|
||||
std::vector<SketchEntity> m_op_ghost; // live result preview (recomputed on value change)
|
||||
bool m_op_dragging_arrow{false}; // arrowhead drag in progress
|
||||
std::vector<int> m_mirror_targets; // Mirror: entities to be mirrored (axis = m_op_a)
|
||||
|
||||
// In-canvas imported-art transform gizmo (Mode::TransformArt). GUI-only. The art's
|
||||
// untransformed contours + its bbox in base coords; the live offset/scale; the grabbed
|
||||
// handle (0..3 = corners, 4 = centre move, -1 = none) and the fixed world anchor (the
|
||||
// opposite corner during a corner-scale drag).
|
||||
std::vector<std::vector<std::vector<Vec2d>>> m_xform_base;
|
||||
int m_xform_feat{-1};
|
||||
Vec2d m_xform_min{0,0}, m_xform_max{0,0}; // bbox of m_xform_base (untransformed)
|
||||
Vec2d m_xform_offset{0,0};
|
||||
double m_xform_sx{1.0}, m_xform_sy{1.0};
|
||||
int m_xform_handle{-1};
|
||||
Vec2d m_xform_anchor{0,0};
|
||||
void xform_world_corners(Vec2d out[4]) const; // 4 bbox corners in plane coords
|
||||
int hit_test_xform_handle(const Vec2d& p, double tol) const;
|
||||
void drag_xform_handle(const Vec2d& target);
|
||||
void render_xform_gizmo();
|
||||
void emit_xform();
|
||||
void reset_xform();
|
||||
|
||||
// In-canvas transform gizmo state (Mode::Move/Rotate/Scale/Array/PolarArray). GUI-only,
|
||||
// reset by set_tool/cancel. Pick one or more subject entities (m_tf_targets), then a
|
||||
// single draggable handle drives the continuous parameter and a live translucent ghost
|
||||
// previews the result; Array/PolarArray add a second editable label for the copy count.
|
||||
// Mutating ops (Move/Rotate/Scale) drop the constraint classes the map invalidates;
|
||||
// additive ops (Array/PolarArray) bind each copy to its source. See confirm_transform().
|
||||
std::vector<int> m_tf_targets; // picked subject entity indices
|
||||
Vec2d m_tf_pivot{0,0}; // rotate/scale/polar pivot = set centroid
|
||||
Vec2d m_tf_delta{0,0}; // Move translation / Array per-step vector
|
||||
double m_tf_angle{0.0}; // Rotate angle / PolarArray total sweep (rad)
|
||||
double m_tf_scale{1.0}; // Scale factor
|
||||
int m_tf_count{3}; // Array/PolarArray copy count (incl. original)
|
||||
double m_tf_handle_r{1.0}; // ring/handle reference radius (set on pick)
|
||||
std::vector<SketchEntity> m_tf_ghost; // live result preview
|
||||
int m_tf_handle{-1}; // 0 = primary drag handle grabbed, -1 = none
|
||||
bool m_tf_dragging{false};
|
||||
Vec2d m_tf_label_a{1e18,1e18}; // primary-param label centre (picking)
|
||||
Vec2d m_tf_label_b{1e18,1e18}; // count label centre (Array/PolarArray)
|
||||
bool tf_ready() const; // >=1 target picked -> gizmo + ghost live
|
||||
void tf_pick(int ei); // accumulate a subject, seed defaults once
|
||||
void compute_tf_pivot(); // centroid + extent of the target set
|
||||
void recompute_tf_ghost();
|
||||
Vec2d tf_handle_pos() const; // world position of the drag handle
|
||||
bool hit_test_tf_handle(const Vec2d& p, double tol) const;
|
||||
void drag_tf_handle(const Vec2d& target);
|
||||
void render_tf_gizmo(double unit_per_px);
|
||||
void open_tf_editor_a(); // inline-edit the continuous parameter
|
||||
void open_tf_editor_count(); // inline-edit the copy count
|
||||
void confirm_transform(); // apply geometry + constraint web
|
||||
void reset_tf();
|
||||
|
||||
// DoF feedback state, refreshed by resolve_live() from the libslvs solve result.
|
||||
int m_dof{-1}; // remaining DoF; 0 = fully constrained, <0 = unknown
|
||||
bool m_solve_ok{true}; // solver consistent (no conflicting constraints)
|
||||
std::vector<char> m_entity_conflict; // per-entity flag: touched by a conflicting constraint
|
||||
std::vector<DimAnnot> m_dimensions; // placed dimension quotes (Mode::Dimension)
|
||||
int m_dim_e0{-1}; // first picked point's entity (Dimension)
|
||||
SketchPointRole m_dim_r0{SketchPointRole::P0};
|
||||
bool m_dim_has0{false}; // a first point is pending
|
||||
int m_pending_dim{-1}; // dim awaiting a value-card entry
|
||||
Mode m_mode{Mode::Polyline};
|
||||
int m_sel_a{-1}; // picked segment endpoints (legacy Constrain mode)
|
||||
int m_sel_b{-1};
|
||||
bool m_constrain_entities{false}; // Constrain mode acts on entities
|
||||
int m_pick0{-1}; // picked line-entity indices (entity Constrain)
|
||||
int m_pick1{-1};
|
||||
int m_pick2{-1}; // third slot (Symmetric axis)
|
||||
Vec2d m_pick0_pt{0,0}; // plane-coords of the slot-0 pick (trim/extend)
|
||||
std::vector<int> m_constraint_hl; // entities highlighted by the constraint manager
|
||||
std::vector<SketchEntityConstraintDef> m_constrain_cons; // for glyph badges (C3.4b)
|
||||
GLModel m_line_model;
|
||||
GLModel m_vertex_model;
|
||||
GLModel m_highlight_model;
|
||||
GLModel m_fill_model; // translucent face fill for closed regions
|
||||
std::vector<DisplaySketch> m_display_sketches; // committed sketches drawn persistently
|
||||
int m_display_pick{-1}; // FEATURE index of the click-selected display sketch (-1 none)
|
||||
|
||||
// Solid (whole/face/edge) selection on the committed bodies. Pointers are non-owning,
|
||||
// into CadDocument (bodies + display_mesh + per-triangle face/body ids), refreshed each
|
||||
// recompute via set_solid_pick. m_sel_edge_pts caches the picked edge's world polyline.
|
||||
const std::vector<CadBody>* m_solid_bodies{nullptr};
|
||||
const TriangleMesh* m_solid_mesh{nullptr};
|
||||
const std::vector<int>* m_solid_tri_face{nullptr};
|
||||
const std::vector<int>* m_solid_tri_body{nullptr};
|
||||
const std::vector<bool>* m_solid_visible{nullptr}; // per-body visibility; hidden bodies aren't pickable
|
||||
const std::vector<Transform3d>* m_solid_xform{nullptr}; // per-body display transform (for edge sampling)
|
||||
Vec3d body_xform_pt(int body, const Vec3d& p) const; // map an OCCT-shape point through the body xform
|
||||
bool body_pickable(int b) const; // false when the body is explicitly hidden
|
||||
SolidSel m_solid_sel{SolidSel::None};
|
||||
int m_sel_body{-1}; // which body the face/edge selection is on
|
||||
int m_sel_face{-1};
|
||||
int m_sel_edge{-1};
|
||||
std::vector<Vec3d> m_sel_edge_pts;
|
||||
bool handle_solid_click(GLCanvas3D& canvas, const wxMouseEvent& evt); // cycle + notify
|
||||
void render_solid_highlight();
|
||||
void render_datum_planes(); // translucent rectangles for datum/reference planes
|
||||
std::vector<SketchPlane> m_datum_planes;
|
||||
GLModel m_solid_face_model;
|
||||
GLModel m_solid_edge_model;
|
||||
int m_display_pick_region{-1}; // selected closed-region index within that feature (-1 none)
|
||||
|
||||
// Visual Extrude gizmo state (C5b). GUI-only; fed by the panel each refresh_preview.
|
||||
bool m_ex_active{false};
|
||||
SketchPlane m_ex_plane; // profile plane (gives normal + to_world anchor)
|
||||
Vec2d m_ex_centroid{0,0}; // arrow base in plane coords (profile centroid)
|
||||
double m_ex_depth{0.0}; // primary depth (= m_distance)
|
||||
double m_ex_depth2{0.0}; // second-side depth (TwoSided, = m_distance2)
|
||||
bool m_ex_two_sided{false};
|
||||
bool m_ex_flip{false};
|
||||
int m_ex_drag{-1}; // 0 = primary arrow, 1 = second arrow, -1 = none
|
||||
int m_ex_press_x{0}, m_ex_press_y{0}; // press px to tell click-to-edit from drag
|
||||
void render_extrude_gizmo();
|
||||
bool hit_test_extrude_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int& which) const;
|
||||
void drag_extrude_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int which);
|
||||
void open_extrude_editor(int which);
|
||||
GLModel m_ex_arrow_model;
|
||||
|
||||
// Move-body gizmo state: 3 world-axis translate arrows + 3 world-axis rotate rings.
|
||||
// Delta model: offset/rot are deltas about a fixed pivot, composed onto m_mv_base_xform
|
||||
// (the body's pose when Move opened) so rotation works even on an already-placed body.
|
||||
bool m_mv_active{false};
|
||||
int m_mv_body{-1};
|
||||
Vec3d m_mv_base{Vec3d::Zero()}; // pivot = body's world centroid at Move-open
|
||||
Vec3d m_mv_offset{Vec3d::Zero()}; // delta translation along world X/Y/Z
|
||||
Transform3d m_mv_base_xform{Transform3d::Identity()}; // pose when Move opened
|
||||
Eigen::Matrix3d m_mv_rot{Eigen::Matrix3d::Identity()}; // accumulated delta rotation (world, about pivot)
|
||||
Eigen::Matrix3d m_mv_rot_start{Eigen::Matrix3d::Identity()}; // rot snapshot at arc-drag start
|
||||
double m_mv_arc_a0{0.0}; // mouse angle on the ring at drag start
|
||||
int m_mv_drag{-1}; // 0..2 = X/Y/Z arrow, 3..5 = X/Y/Z ring, -1 none
|
||||
int m_mv_press_x{0}, m_mv_press_y{0};
|
||||
Transform3d compose_move_xform() const; // T(offset)*T(pivot)*rot*T(-pivot)*base_xform
|
||||
void ring_basis(int axis, Vec3d& e, Vec3d& u, Vec3d& v) const; // world axis + in-plane basis
|
||||
void render_move_gizmo();
|
||||
bool hit_test_move_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int& axis) const;
|
||||
bool hit_test_move_arc(GLCanvas3D& canvas, const wxMouseEvent& evt, int& axis) const;
|
||||
void drag_move_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis);
|
||||
void drag_move_arc(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis);
|
||||
bool arc_mouse_angle(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis, double& ang) const;
|
||||
void open_move_editor(int axis);
|
||||
GLModel m_mv_arrow_model;
|
||||
|
||||
// Fillet/Chamfer radius gizmo state (single world-space arrow at the picked edge midpoint).
|
||||
bool m_fl_active{false};
|
||||
Vec3d m_fl_anchor{Vec3d::Zero()}; // edge midpoint (world, already body-transformed)
|
||||
Vec3d m_fl_dir{Vec3d::UnitZ()}; // unit radius direction (perp to edge, outward)
|
||||
double m_fl_radius{1.0}; // current radius (= dressup size)
|
||||
bool m_fl_drag{false};
|
||||
int m_fl_press_x{0}, m_fl_press_y{0};
|
||||
double m_fl_grab_proj{0.0}; // axis projection at grab (relative drag reference)
|
||||
double m_fl_grab_radius{1.0}; // radius at grab (relative drag reference)
|
||||
void render_fillet_gizmo();
|
||||
bool hit_test_fillet_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt) const;
|
||||
double fillet_axis_proj(GLCanvas3D& canvas, const wxMouseEvent& evt) const; // NaN if camera∥axis
|
||||
void start_fillet_drag(GLCanvas3D& canvas, const wxMouseEvent& evt);
|
||||
void drag_fillet_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt);
|
||||
void open_fillet_editor();
|
||||
GLModel m_fl_arrow_model;
|
||||
|
||||
// Hole gizmo state. The hole is a positioned circular cut on m_hl_plane at (m_hl_x, m_hl_y);
|
||||
// the footprint circle is drawn on the plane, the diameter arrow runs along the plane u-axis,
|
||||
// the depth arrow along +normal (matching the kernel's make_extrude). Three draggable handles:
|
||||
// 0 = centre (reposition in plane u/v), 1 = diameter, 2 = depth (only shown when !through).
|
||||
bool m_hl_active{false};
|
||||
SketchPlane m_hl_plane;
|
||||
double m_hl_x{0.0}, m_hl_y{0.0}; // centre on the plane (u/v mm)
|
||||
double m_hl_diameter{6.0};
|
||||
double m_hl_depth{10.0};
|
||||
bool m_hl_through{true};
|
||||
// #2 Part B: face (u,v) bounds, so the construction dims read as distance from the face SIDES
|
||||
// (umin/vmin = two adjacent edges) rather than from the centre. Off for a dropdown-plane hole.
|
||||
bool m_hl_has_bounds{false};
|
||||
double m_hl_umin{0}, m_hl_umax{0}, m_hl_vmin{0}, m_hl_vmax{0};
|
||||
int m_hl_drag{-1}; // 0=centre, 1=diameter, 2=depth, 3=X-dim, 4=Y-dim, -1=none
|
||||
int m_hl_press_x{0}, m_hl_press_y{0};
|
||||
double m_hl_grab_proj{0.0}; // diameter/depth axis projection at grab (relative)
|
||||
double m_hl_grab_val{0.0}; // radius (diameter drag) or depth at grab
|
||||
Vec2d m_hl_grab_uv{0.0, 0.0}; // centre drag: plane-projected grab point
|
||||
double m_hl_grab_x{0.0}, m_hl_grab_y{0.0}; // centre drag: x/y at grab
|
||||
void render_hole_gizmo();
|
||||
int hit_test_hole_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const; // 0/1/2/-1
|
||||
double hole_axis_proj(GLCanvas3D& canvas, const wxMouseEvent& evt,
|
||||
const Vec3d& anchor, const Vec3d& dir) const; // NaN if camera∥axis
|
||||
void start_hole_drag(GLCanvas3D& canvas, const wxMouseEvent& evt, int which);
|
||||
void drag_hole_handle(GLCanvas3D& canvas, const wxMouseEvent& evt);
|
||||
void open_hole_editor(int which);
|
||||
GLModel m_hl_stroke_model;
|
||||
|
||||
// Thread gizmo state (mirrors the hole gizmo; radius arrow uses an R label, length arrow is
|
||||
// always shown). Handles: 0 = centre (thread_x/y), 1 = radius, 2 = length.
|
||||
bool m_th_active{false};
|
||||
SketchPlane m_th_plane;
|
||||
double m_th_x{0.0}, m_th_y{0.0};
|
||||
double m_th_radius{5.0};
|
||||
double m_th_height{10.0};
|
||||
int m_th_drag{-1}; // 0=centre, 1=radius, 2=length, -1=none
|
||||
int m_th_press_x{0}, m_th_press_y{0};
|
||||
double m_th_grab_proj{0.0};
|
||||
double m_th_grab_val{0.0};
|
||||
Vec2d m_th_grab_uv{0.0, 0.0};
|
||||
double m_th_grab_x{0.0}, m_th_grab_y{0.0};
|
||||
void render_thread_gizmo();
|
||||
int hit_test_thread_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const; // 0/1/2/-1
|
||||
void start_thread_drag(GLCanvas3D& canvas, const wxMouseEvent& evt, int which);
|
||||
void drag_thread_handle(GLCanvas3D& canvas, const wxMouseEvent& evt);
|
||||
void open_thread_editor(int which);
|
||||
GLModel m_th_stroke_model;
|
||||
|
||||
// Shell gizmo state (single inward thickness arrow at the picked face centroid).
|
||||
bool m_sh_active{false};
|
||||
Vec3d m_sh_anchor{Vec3d::Zero()}; // picked face centroid (world)
|
||||
Vec3d m_sh_dir{Vec3d::UnitZ()}; // inward unit direction (-outward normal)
|
||||
double m_sh_thickness{2.0};
|
||||
bool m_sh_drag{false};
|
||||
int m_sh_press_x{0}, m_sh_press_y{0};
|
||||
double m_sh_grab_proj{0.0};
|
||||
double m_sh_grab_val{2.0};
|
||||
void render_shell_gizmo();
|
||||
bool hit_test_shell_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt) const;
|
||||
void start_shell_drag(GLCanvas3D& canvas, const wxMouseEvent& evt);
|
||||
void drag_shell_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt);
|
||||
void open_shell_editor();
|
||||
GLModel m_sh_stroke_model;
|
||||
|
||||
// Revolve gizmo state (arc center = projection of the profile centroid onto the axis).
|
||||
bool m_rv_active{false};
|
||||
Vec3d m_rv_center{Vec3d::Zero()}; // arc center on the axis (world)
|
||||
Vec3d m_rv_axis{Vec3d::UnitX()}; // revolve axis unit dir (world)
|
||||
Vec3d m_rv_ref{Vec3d::UnitY()}; // angle-0 reference dir (perp to axis, toward profile)
|
||||
double m_rv_radius{10.0}; // arc radius = profile perpendicular distance (world)
|
||||
double m_rv_angle{360.0}; // current sweep magnitude (deg, 1..360)
|
||||
bool m_rv_flip{false}; // sweep sense (matches the kernel's negative-angle flip)
|
||||
bool m_rv_drag{false};
|
||||
int m_rv_press_x{0}, m_rv_press_y{0};
|
||||
void render_revolve_gizmo();
|
||||
bool hit_test_revolve_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const;
|
||||
void drag_revolve_arc(GLCanvas3D& canvas, const wxMouseEvent& evt);
|
||||
void open_revolve_editor();
|
||||
GLModel m_rv_stroke_model;
|
||||
|
||||
// Pattern gizmo state. Linear arrow along m_pt_dirw from m_pt_base; circular arc like Revolve
|
||||
// but axis = m_pt_normal through m_pt_origin (the world XY plane by default).
|
||||
bool m_pt_active{false};
|
||||
bool m_pt_circular{false};
|
||||
Vec3d m_pt_base{Vec3d::Zero()}; // target body centroid (world): linear anchor / radius ref
|
||||
Vec3d m_pt_dirw{Vec3d::UnitX()}; // linear march direction (world)
|
||||
Vec3d m_pt_origin{Vec3d::Zero()}; // circular rotation axis origin (world)
|
||||
Vec3d m_pt_normal{Vec3d::UnitZ()}; // circular rotation axis (world)
|
||||
Vec3d m_pt_cref{Vec3d::UnitX()}; // circular angle-0 reference dir (perp to normal, toward body)
|
||||
Vec3d m_pt_ccenter{Vec3d::Zero()}; // circular arc center (foot of body centroid on the axis)
|
||||
double m_pt_radius{10.0}; // circular arc radius (world)
|
||||
int m_pt_count{3};
|
||||
double m_pt_spacing{20.0};
|
||||
double m_pt_angle{360.0};
|
||||
bool m_pt_drag{false};
|
||||
int m_pt_press_x{0}, m_pt_press_y{0};
|
||||
void render_pattern_gizmo();
|
||||
bool hit_test_pattern_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const;
|
||||
void drag_pattern_handle(GLCanvas3D& canvas, const wxMouseEvent& evt);
|
||||
void open_pattern_editor();
|
||||
GLModel m_pt_stroke_model;
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_DesignSketchTool_hpp_
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "GLCanvas3D.hpp"
|
||||
#include "DesignSketchTool.hpp" // SnapOrca Design: interactive 2D sketch tool
|
||||
|
||||
#include <igl/unproject.h>
|
||||
|
||||
@@ -1833,6 +1834,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;
|
||||
}
|
||||
|
||||
void GLCanvas3D::zoom_to_bed()
|
||||
{
|
||||
BoundingBoxf3 box = m_bed.build_volume().bounding_volume();
|
||||
@@ -2123,6 +2134,11 @@ 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()));
|
||||
|
||||
// SnapOrca Design: interactive 2D sketch overlay, drawn over the scene but
|
||||
// beneath the UI overlays (toolbars, labels).
|
||||
if (m_design_sketch_tool != nullptr && m_design_sketch_tool->has_display())
|
||||
m_design_sketch_tool->render(*this);
|
||||
|
||||
// draw overlays
|
||||
_render_overlays();
|
||||
|
||||
@@ -3278,6 +3294,56 @@ void GLCanvas3D::on_char(wxKeyEvent& evt)
|
||||
return;
|
||||
}
|
||||
|
||||
// SnapOrca Design: 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).
|
||||
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;
|
||||
}
|
||||
|
||||
// Esc exits the active sketch tool (Onshape-like, layered: abort in-progress entity ->
|
||||
// drop to Select -> exit the session back to Feature mode).
|
||||
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;
|
||||
}
|
||||
|
||||
// SnapOrca Design: 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.
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// SnapOrca Design: 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.
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_in_painting_mode = false;
|
||||
GLGizmoPainterBase *current_gizmo_painter = dynamic_cast<GLGizmoPainterBase *>(get_gizmos_manager().get_current());
|
||||
if (current_gizmo_painter != nullptr) {
|
||||
@@ -3650,6 +3716,18 @@ public:
|
||||
|
||||
void GLCanvas3D::on_key(wxKeyEvent& evt)
|
||||
{
|
||||
// SnapOrca Design: Delete/Backspace removes selected sketch entities. GTK delivers
|
||||
// these as KEY_DOWN rather than CHAR, so handle it here too.
|
||||
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;
|
||||
}
|
||||
|
||||
static GLCanvas3D const * thiz = nullptr;
|
||||
static TranslationProcessor translationProcessor(nullptr, nullptr);
|
||||
if (thiz != this) {
|
||||
@@ -4180,6 +4258,21 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
|
||||
return;
|
||||
}
|
||||
|
||||
// SnapOrca Design: 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.
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef __WXMSW__
|
||||
bool on_enter_workaround = false;
|
||||
if (! evt.Entering() && ! evt.Leaving() && m_mouse.position.x() == -1.0) {
|
||||
@@ -7853,7 +7946,12 @@ void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d&
|
||||
|
||||
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);
|
||||
// SnapOrca Design: transiently suppress plate chrome for opted-out canvases.
|
||||
auto& plate_list = wxGetApp().plater()->get_partplate_list();
|
||||
const bool prev_hide_chrome = plate_list.get_hide_chrome();
|
||||
plate_list.set_hide_chrome(!m_plate_chrome_enabled);
|
||||
plate_list.render(view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid);
|
||||
plate_list.set_hide_chrome(prev_hide_chrome);
|
||||
}
|
||||
|
||||
void GLCanvas3D::_render_cast_shadows_on_plate(const Transform3d& view_matrix, const Transform3d& projection_matrix)
|
||||
@@ -9359,6 +9457,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,7 @@ namespace GUI {
|
||||
|
||||
class Bed3D;
|
||||
class PartPlateList;
|
||||
class DesignSketchTool; // SnapOrca Design: interactive 2D sketch tool
|
||||
|
||||
#if ENABLE_RETINA_GL
|
||||
class RetinaHelper;
|
||||
@@ -542,6 +543,9 @@ 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};
|
||||
DesignSketchTool* m_design_sketch_tool{nullptr};
|
||||
|
||||
//BBS: add canvas type for assemble view usage
|
||||
ECanvasType m_canvas_type;
|
||||
@@ -879,6 +883,10 @@ 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_design_sketch_tool(DesignSketchTool* tool) { m_design_sketch_tool = tool; }
|
||||
DesignSketchTool* get_design_sketch_tool() const { return m_design_sketch_tool; }
|
||||
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); }
|
||||
|
||||
@@ -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/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/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,8 @@
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoSVG.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoAssembly.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoPrimitive.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoSketch.hpp"
|
||||
|
||||
#include "libslic3r/format.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
@@ -176,6 +178,12 @@ void GLGizmosManager::switch_gizmos_icon_filename()
|
||||
case (EType::BrimEars):
|
||||
gizmo->set_icon_filename(m_is_dark ? "toolbar_brimears_dark.svg" : "toolbar_brimears.svg");
|
||||
break;
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -219,6 +227,8 @@ 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));
|
||||
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)));
|
||||
//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,8 @@ public:
|
||||
Assembly,
|
||||
Simplify,
|
||||
BrimEars,
|
||||
Primitive,
|
||||
Sketch,
|
||||
//SlaSupports,
|
||||
// BBS
|
||||
//FaceRecognition,
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "I18N.hpp"
|
||||
#include "GLCanvas3D.hpp"
|
||||
#include "Plater.hpp"
|
||||
#include "DesignPanel.hpp"
|
||||
#include "WebViewDialog.hpp"
|
||||
#include "../Utils/Process.hpp"
|
||||
#include "format.hpp"
|
||||
@@ -1014,6 +1015,8 @@ void MainFrame::update_layout()
|
||||
{
|
||||
case ESettingsLayout::Old:
|
||||
{
|
||||
m_design_panel->Reparent(m_tabpanel);
|
||||
m_tabpanel->InsertPage(tpDesign, m_design_panel, _L("Design"), std::string("tab_design_active"), std::string("tab_design_active"), false);
|
||||
m_plater->Reparent(m_tabpanel);
|
||||
m_tabpanel->InsertPage(tp3DEditor, m_plater, _L("Prepare"), std::string("tab_3d_active"), std::string("tab_3d_active"), false);
|
||||
m_tabpanel->InsertPage(tpPreview, m_plater, _L("Preview"), std::string("tab_preview_active"), std::string("tab_preview_active"), false);
|
||||
@@ -1270,6 +1273,12 @@ void MainFrame::init_tabpanel() {
|
||||
}
|
||||
//else if (panel == m_param_panel)
|
||||
// m_param_panel->OnActivate();
|
||||
else if (panel == m_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();
|
||||
}
|
||||
else if (panel == m_monitor) {
|
||||
//monitor
|
||||
}
|
||||
@@ -1316,11 +1325,13 @@ void MainFrame::init_tabpanel() {
|
||||
}
|
||||
|
||||
m_plater = new Plater(this, this);
|
||||
// Register the plater with the app BEFORE constructing DesignPanel: its
|
||||
// DesignCanvas reads wxGetApp().plater()->config() at construction time.
|
||||
wxGetApp().plater_ = m_plater;
|
||||
m_design_panel = new DesignPanel(this);
|
||||
m_plater->SetBackgroundColour(*wxWHITE);
|
||||
m_plater->Hide();
|
||||
|
||||
wxGetApp().plater_ = m_plater;
|
||||
|
||||
create_preset_tabs();
|
||||
|
||||
//BBS add pages
|
||||
|
||||
@@ -50,6 +50,7 @@ namespace GUI
|
||||
class Tab;
|
||||
class PrintHostQueueDialog;
|
||||
class Plater;
|
||||
class DesignPanel;
|
||||
class MainFrame;
|
||||
class ParamsDialog;
|
||||
#ifdef __WXGTK__
|
||||
@@ -218,14 +219,15 @@ public:
|
||||
enum TabPosition
|
||||
{
|
||||
tpHome = 0,
|
||||
tp3DEditor = 1,
|
||||
tpPreview = 2,
|
||||
tpMonitor = 3,
|
||||
tpMultiDevice = 4,
|
||||
tpProject = 5,
|
||||
tpCalibration = 6,
|
||||
tpAuxiliary = 7,
|
||||
toDebugTool = 8,
|
||||
tpDesign = 1,
|
||||
tp3DEditor = 2,
|
||||
tpPreview = 3,
|
||||
tpMonitor = 4,
|
||||
tpMultiDevice = 5,
|
||||
tpProject = 6,
|
||||
tpCalibration = 7,
|
||||
tpAuxiliary = 8,
|
||||
toDebugTool = 9,
|
||||
};
|
||||
|
||||
//BBS: add slice&&print status update logic
|
||||
@@ -375,6 +377,7 @@ public:
|
||||
BBLTopbar* m_topbar{ nullptr };
|
||||
PrintHostQueueDialog* printhost_queue_dlg() { return m_printhost_queue_dlg; }
|
||||
Plater* m_plater { nullptr };
|
||||
DesignPanel* m_design_panel { nullptr };
|
||||
//BBS: GUI refactor
|
||||
MonitorPanel* m_monitor{ nullptr };
|
||||
|
||||
|
||||
@@ -3384,16 +3384,20 @@ 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) {
|
||||
const bool hide_chrome = m_partplate_list && m_partplate_list->get_hide_chrome();
|
||||
|
||||
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));
|
||||
|
||||
@@ -614,6 +614,11 @@ class PartPlateList : public ObjectBase
|
||||
bool render_bedtype_logo = true;
|
||||
bool render_plate_settings = true;
|
||||
bool render_cali_logo = true;
|
||||
// SnapOrca Design: when true, PartPlate::render skips all overlay chrome
|
||||
// (corner icons, logo watermark, plate numbers) but keeps the bed grid.
|
||||
// Toggled transiently per-frame by GLCanvas3D::_render_platelist for the
|
||||
// DesignCanvas; stays false for the main editor.
|
||||
bool m_hide_chrome = false;
|
||||
|
||||
bool m_is_dark = false;
|
||||
|
||||
@@ -838,6 +843,8 @@ public:
|
||||
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 set_render_option(bool bedtype_texture, bool plate_settings);
|
||||
void set_render_cali(bool value = true) { render_cali_logo = value; }
|
||||
void set_hide_chrome(bool value) { m_hide_chrome = value; }
|
||||
bool get_hide_chrome() const { return m_hide_chrome; }
|
||||
void register_raycasters_for_picking(GLCanvas3D& canvas)
|
||||
{
|
||||
for (auto plate : m_plate_list)
|
||||
|
||||
@@ -17477,6 +17477,11 @@ PartPlateList& Plater::get_partplate_list()
|
||||
return p->partplate_list;
|
||||
}
|
||||
|
||||
BackgroundSlicingProcess* Plater::get_background_process()
|
||||
{
|
||||
return &p->background_process;
|
||||
}
|
||||
|
||||
void Plater::apply_background_progress()
|
||||
{
|
||||
PartPlate* part_plate = p->partplate_list.get_curr_plate();
|
||||
|
||||
@@ -46,6 +46,7 @@ class Model;
|
||||
class ModelObject;
|
||||
class ModelInstance;
|
||||
class Print;
|
||||
class BackgroundSlicingProcess;
|
||||
class SLAPrint;
|
||||
//BBS: add partplatelist and SlicingStatusEvent
|
||||
class PartPlateList;
|
||||
@@ -706,6 +707,10 @@ public:
|
||||
|
||||
//BBS: partplate list related functions
|
||||
PartPlateList& get_partplate_list();
|
||||
// Shared background slicing process (same instance View3D/Preview/AssembleView
|
||||
// use). Exposed so the Design tab's native GLCanvas3D can be wired exactly like
|
||||
// the editor canvases (GLCanvas3D::render() dereferences the process).
|
||||
BackgroundSlicingProcess* get_background_process();
|
||||
void validate_current_plate(bool& model_fits, bool& validate_error);
|
||||
//BBS: select the plate by index
|
||||
int select_plate(int plate_index, bool need_slice = false);
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
#include "SketchInlineEditor.hpp"
|
||||
|
||||
#include <wx/frame.h>
|
||||
#include <wx/textctrl.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/window.h>
|
||||
#include <wx/toplevel.h>
|
||||
#include <wx/gdicmn.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
namespace {
|
||||
// Locale-safe value <-> text (wx sets LC_NUMERIC to the user locale, so snprintf may
|
||||
// emit a comma; parsing accepts either separator). Mirrors DesignPanel's en_*.
|
||||
wxString en_format(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 wxString::FromUTF8(buf);
|
||||
}
|
||||
bool en_parse(const wxString& text, double& out)
|
||||
{
|
||||
wxString t(text);
|
||||
t.Replace(wxT(","), wxT("."));
|
||||
return t.ToCDouble(&out);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SketchInlineEditor::SketchInlineEditor(wxWindow* parent_canvas)
|
||||
{
|
||||
wxWindow* top = parent_canvas ? wxGetTopLevelParent(parent_canvas) : nullptr;
|
||||
// Borderless floating frame: a top-level window so the WM composites it above the
|
||||
// GL canvas (a child widget would be hidden by the GL surface). Floats on its
|
||||
// parent and stays on top so it tracks the main window.
|
||||
// NB: no wxFRAME_FLOAT_ON_PARENT — that maps to a GTK _UTILITY_ window-type hint, which
|
||||
// many WMs (incl. the xrdp/x11vnc session on :10) refuse to give keyboard focus, so the
|
||||
// field opened un-focusable and needed a click before typing. Plain stay-on-top frame is
|
||||
// WM-focusable; we present + SetFocus it explicitly in open().
|
||||
m_frame = new wxFrame(top, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize,
|
||||
wxFRAME_NO_TASKBAR | wxBORDER_NONE | wxSTAY_ON_TOP);
|
||||
m_ctrl = new wxTextCtrl(m_frame, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(82, -1),
|
||||
wxTE_PROCESS_ENTER | wxTE_RIGHT | wxBORDER_SIMPLE);
|
||||
auto* sizer = new wxBoxSizer(wxVERTICAL);
|
||||
sizer->Add(m_ctrl, 1, wxEXPAND);
|
||||
m_frame->SetSizerAndFit(sizer);
|
||||
m_frame->Hide();
|
||||
|
||||
m_ctrl->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { do_commit(); });
|
||||
m_ctrl->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& e) {
|
||||
if (e.GetKeyCode() == WXK_ESCAPE) do_cancel();
|
||||
else e.Skip();
|
||||
});
|
||||
}
|
||||
|
||||
void SketchInlineEditor::open(const wxPoint& screen_px, double value,
|
||||
std::function<void(double)> on_commit,
|
||||
std::function<void()> on_cancel)
|
||||
{
|
||||
if (m_frame == nullptr || m_ctrl == nullptr) { if (on_cancel) on_cancel(); return; }
|
||||
if (m_open) close();
|
||||
m_commit = std::move(on_commit);
|
||||
m_cancel = std::move(on_cancel);
|
||||
m_ctrl->ChangeValue(en_format(value));
|
||||
m_frame->Fit();
|
||||
const wxSize sz = m_frame->GetSize();
|
||||
wxPoint pos(screen_px.x - sz.GetWidth() / 2, screen_px.y - sz.GetHeight() / 2);
|
||||
// Keep the frame fully on-screen: an anchor that maps off the display makes GTK drop
|
||||
// the window at a default corner (top-left) instead of the requested point.
|
||||
const wxRect area = wxGetClientDisplayRect();
|
||||
pos.x = std::max(area.GetLeft(), std::min(pos.x, area.GetRight() - sz.GetWidth()));
|
||||
pos.y = std::max(area.GetTop(), std::min(pos.y, area.GetBottom() - sz.GetHeight()));
|
||||
// Show() BEFORE Move(): GTK ignores a Move() issued before the window is mapped (the
|
||||
// WM places it at its default, i.e. the top-left corner). Move after Show sticks.
|
||||
m_frame->Show();
|
||||
m_frame->Move(pos);
|
||||
m_frame->Raise(); // gtk_window_present -> activate the top-level so SetFocus routes
|
||||
m_frame->SetFocus();
|
||||
m_ctrl->SetFocus();
|
||||
m_ctrl->SelectAll();
|
||||
m_open = true;
|
||||
// Re-assert on the next tick too: the GL canvas can reclaim focus while it finishes
|
||||
// handling the click/render that opened us, so a single immediate SetFocus may be stolen.
|
||||
m_ctrl->CallAfter([this] {
|
||||
if (m_open && m_ctrl) { m_frame->Raise(); m_ctrl->SetFocus(); m_ctrl->SelectAll(); }
|
||||
});
|
||||
}
|
||||
|
||||
void SketchInlineEditor::do_commit()
|
||||
{
|
||||
if (!m_open || m_ctrl == nullptr) return;
|
||||
double v = 0.0;
|
||||
if (!en_parse(m_ctrl->GetValue(), v)) { // invalid: keep editing
|
||||
m_ctrl->SetFocus();
|
||||
m_ctrl->SelectAll();
|
||||
return;
|
||||
}
|
||||
auto cb = m_commit; // copy-then-close: the callback re-enters (re-solve + render)
|
||||
close();
|
||||
if (cb) cb(v);
|
||||
}
|
||||
|
||||
void SketchInlineEditor::cancel()
|
||||
{
|
||||
if (m_open) do_cancel();
|
||||
}
|
||||
|
||||
void SketchInlineEditor::do_cancel()
|
||||
{
|
||||
if (!m_open) return;
|
||||
auto cb = m_cancel;
|
||||
close();
|
||||
if (cb) cb();
|
||||
}
|
||||
|
||||
void SketchInlineEditor::close()
|
||||
{
|
||||
if (m_frame == nullptr || !m_open) return;
|
||||
m_closing = true;
|
||||
m_open = false;
|
||||
m_frame->Hide();
|
||||
m_commit = nullptr;
|
||||
m_cancel = nullptr;
|
||||
m_closing = false;
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
@@ -0,0 +1,48 @@
|
||||
#ifndef slic3r_SketchInlineEditor_hpp_
|
||||
#define slic3r_SketchInlineEditor_hpp_
|
||||
|
||||
#include <functional>
|
||||
|
||||
class wxWindow;
|
||||
class wxFrame;
|
||||
class wxTextCtrl;
|
||||
class wxPoint;
|
||||
|
||||
namespace Slic3r {
|
||||
namespace GUI {
|
||||
|
||||
// Onshape-style in-canvas value editor: a small borderless floating frame holding a
|
||||
// wxTextCtrl, shown at screen coordinates over the GL canvas. A top-level frame is
|
||||
// used (not a child widget) because a native child cannot be composited over the
|
||||
// double-buffered wxGLCanvas under GTK3/llvmpipe — it stays invisible. Enter (or blur)
|
||||
// commits the parsed number, Esc cancels. This is the single numeric-entry path for
|
||||
// sketch dimensions, replacing the docked/modal value cards.
|
||||
class SketchInlineEditor
|
||||
{
|
||||
public:
|
||||
explicit SketchInlineEditor(wxWindow* parent_canvas);
|
||||
|
||||
// Show the editor centred on `screen_px` (absolute screen coords), pre-filled with
|
||||
// `value`. on_commit(parsed) fires on Enter with a valid number; on_cancel() on Esc.
|
||||
void open(const wxPoint& screen_px, double value,
|
||||
std::function<void(double)> on_commit,
|
||||
std::function<void()> on_cancel);
|
||||
void close();
|
||||
void cancel(); // if open, run the registered cancel (keep-as-drawn)
|
||||
bool is_open() const { return m_open; }
|
||||
|
||||
private:
|
||||
void do_commit();
|
||||
void do_cancel();
|
||||
|
||||
wxFrame* m_frame{nullptr};
|
||||
wxTextCtrl* m_ctrl{nullptr};
|
||||
std::function<void(double)> m_commit;
|
||||
std::function<void()> m_cancel;
|
||||
bool m_open{false};
|
||||
bool m_closing{false};
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_SketchInlineEditor_hpp_
|
||||
Reference in New Issue
Block a user