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:
Tommaso Bianchi
2026-06-28 12:40:38 +02:00
co-authored by Claude Opus 4.8
parent 449a4cf9fc
commit 0f4060c0a9
146 changed files with 34845 additions and 17 deletions
+885
View File
@@ -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
+236
View File
@@ -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
+420
View File
@@ -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
+980
View File
@@ -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_
+102 -1
View File
@@ -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) {
+8
View File
@@ -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); }
+186
View File
@@ -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_
+459
View File
@@ -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
+74
View File
@@ -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_
+10
View File
@@ -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,
+13 -2
View File
@@ -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
+11 -8
View File
@@ -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 };
+8 -4
View File
@@ -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));
+7
View File
@@ -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)
+5
View File
@@ -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();
+5
View File
@@ -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);
+133
View File
@@ -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
+48
View File
@@ -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_