#include "slic3r/GUI/CAD/DesignCanvas.hpp" #include "slic3r/GUI/CAD/SketchInlineEditor.hpp" #include "slic3r/GUI/CAD/DesignInteraction.hpp" #include "slic3r/GUI/GLCanvas3D.hpp" #include "slic3r/GUI/OpenGLManager.hpp" #include "slic3r/GUI/3DBed.hpp" #include "slic3r/GUI/Camera.hpp" // N: look down the sketch plane normal #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/Plater.hpp" #include "libslic3r/Model.hpp" #include "libslic3r/TriangleMesh.hpp" #include "slic3r/GUI/3DScene.hpp" #include "slic3r/GUI/MeshUtils.hpp" // ClippingPlane (section view) #include "libslic3r/Config.hpp" #include #include #include #include #include // wxGetLocalTimeMillis: the right-click vs right-hold budget #include #include #include #include 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); // Nothing to slice here, but GLCanvas3D derefs the process unguarded every frame, so it // cannot be null. Borrow the plater's, as the editor canvases do. m_canvas->set_process(&wxGetApp().plater()->background_process()); m_canvas->set_type(GLCanvas3D::ECanvasType::CanvasView3D); // CAD navigation, this canvas only: left-drag sweeps a selection rubber band, so orbit // moves to middle-drag and pan to right-drag. Design is a different modality from // Prepare/Preview and every CAD the user already knows maps the mouse this way; the other // tabs are untouched. m_canvas->set_cad_navigation(true); 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_axes_at_bed_center(true); // triad at bed centre = modeling origin 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& ents, const std::vector& 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(); // The tool draws it: it owns the frame's ImGui pass and the render scale. Handing it a raw // pointer rather than the unique_ptr keeps the ownership where it was. m_sketch_tool.inline_editor = m_inline_editor.get(); m_inline_editor->request_frame = [this] { request_repaint(); }; m_sketch_tool.on_inline_edit = [this](wxPoint screen_px, double current, const std::string& title, std::function commit, std::function cancel) { if (!m_inline_editor) { if (cancel) cancel(); return; } // The tool hands us canvas device px and the field is now drawn IN the canvas, so this // is already the coordinate space it wants — no conversion to screen coordinates, and no // window to place there. // Freeze the sketch tool while the field is open so a stray click/move on the GL // canvas can't draw under the field; released on commit or cancel. m_sketch_tool.set_inline_busy(true); // AND PUT THE KEYBOARD ON THE CANVAS. The field is drawn by ImGui, and ImGui is fed from // GLCanvas3D's own key handler, so a key only reaches it if the canvas is the focused // widget. That is a focus move WITHIN one window — the toolkit's business, not the window // manager's, which is the whole point of not being a window any more — but it still has // to be asked for: after a toolbar click or a tree selection the focus is elsewhere in // the panel, and the field would sit there taking nothing. if (m_canvas_widget) m_canvas_widget->SetFocus(); m_inline_editor->open(screen_px, current, title, [this, commit](double v) { m_sketch_tool.set_inline_busy(false); if (commit) commit(v); request_repaint(); }, [this, cancel]() { m_sketch_tool.set_inline_busy(false); if (cancel) cancel(); request_repaint(); }); }; // 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(); }; m_sketch_tool.on_inline_commit = [this]() { if (m_inline_editor) m_inline_editor->commit(); }; // 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. // NON-FOCUSABLE IS THE LOAD-BEARING WORD, and a wxFrame is not: see the header. The chip // outlives the gesture that drew it, and while it held the X input focus every sketch // shortcut was swallowed until the user clicked the canvas. Same window class as the status // chip below for the same reason. Do not "simplify" it back to a wxFrame. { wxWindow* top = wxGetTopLevelParent(m_canvas_widget); m_hud = new wxPopupWindow(top, wxBORDER_NONE); 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); }; // Bottom-LEFT twin, carrying the status line. Top-level for the same reason as the readout // (a child widget is hidden by the GL surface) but a wxPopupWindow rather than a wxFrame, // because a popup cannot take keyboard focus. The readout gets away with a frame only // because it appears mid-gesture and the next input is the mouse; this one is up // permanently and is re-raised on every status change. As a frame it took the WM's focus // each time and the canvas stopped receiving keys at all — every sketch shortcut silently // dead, which reads as a broken tool. Do not "simplify" it back to a wxFrame. // Its colour is set per message — the panel decides whether a line is neutral or an error. { wxWindow* top = wxGetTopLevelParent(m_canvas_widget); m_status_hud = new wxPopupWindow(top, wxBORDER_NONE); m_status_hud->SetBackgroundColour(wxColour(28, 30, 34)); m_status_hud_label = new wxStaticText(m_status_hud, wxID_ANY, wxEmptyString); auto* ss = new wxBoxSizer(wxHORIZONTAL); // The line never wraps — there is a whole window's width down here — so the chip is // ONE LINE tall. Spacers rather than a wxALL border because the two axes want // different numbers: roomy at the sides so it reads as a label, and just enough top // and bottom to clear the descenders. Zero vertical clips the glyphs; 6 (what the // readout chip uses) makes it look like a two-line box. ss->AddSpacer(10); ss->Add(m_status_hud_label, 0, wxTOP | wxBOTTOM, 3); ss->AddSpacer(10); m_status_hud->SetSizerAndFit(ss); m_status_hud->Hide(); } // A floating frame does not follow its parent, so the anchor has to be recomputed whenever // the canvas changes size (that bind is below bind_event_handlers(), for the reason given // there). The readout HUD gets away without this because it is transient; the status line is // on screen almost permanently and would visibly detach. // ...and it does not follow the WINDOW either. A popup is override-redirect: the window // manager does not own it, so minimising the app leaves the chip sitting on the bare desktop // (seen on the rig: whole screen black, chip still there), and it stacks above other // applications rather than behind them. IsShownOnScreen does not catch this — an iconised // frame still counts as shown — so the frame has to say so itself. Deactivating the app is // the same case one step weaker: the chip belongs to a viewport the user is no longer // looking at. Showing it back is safe because a popup cannot take focus, so neither event // can be re-triggered by our own Show(). // Members rather than lambdas so unbind_canvas_event_handlers() can Unbind them: these sit on // a frame that OUTLIVES this canvas, and a lambda cannot be unbound. if (wxWindow* top = wxGetTopLevelParent(m_canvas_widget)) { top->Bind(wxEVT_ICONIZE, &DesignCanvas::on_frame_iconize, this); top->Bind(wxEVT_ACTIVATE, &DesignCanvas::on_frame_activate, this); // The anchor is an ABSOLUTE SCREEN position (ClientToScreen below), so moving the window // moves the canvas out from under a chip that stays where it was. Dragging the frame by // its title bar left the chip stranded mid-viewport until the next size, status or tab // change happened to re-place it. Nothing on the canvas fires for a move that does not // also resize, so it has to come from the frame. top->Bind(wxEVT_MOVE, &DesignCanvas::on_status_hud_reanchor, this); } refresh_bed(); // The view this canvas opens on. Built lazily, on the way into the Design tab, so this // is the view the user is looking at right now. m_parked_camera = wxGetApp().plater()->get_camera(); // Before any of this class's own Binds below: wx calls dynamically bound handlers in // reverse order of binding, and GLCanvas3D swallows several events without skipping them — // wxEVT_RIGHT_UP and wxEVT_ENTER_WINDOW in on_mouse, and wxEVT_SIZE in on_size, which is // just `m_dirty = true;`. For those, whatever is bound LAST is the only handler that runs. // The context menu, the focus-follows-mouse and the status-chip re-anchor all depend on // running first, which is only true while this call stays ahead of them. m_canvas->bind_event_handlers(); // The status-chip re-anchor promised above, bound AFTER the call so it runs first — ahead of // it the handler never ran at all, leaving a stale anchor and wrap width after any resize // that did not also move the frame or change the text. Its e.Skip() is load-bearing the // other way: it falls through to on_size, which is what still marks the canvas dirty. m_canvas_widget->Bind(wxEVT_SIZE, &DesignCanvas::on_status_hud_reanchor, this); // 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)); } // Both are idempotent, and neither destroys anything: the destructor still owns that. void DesignCanvas::unbind_canvas_event_handlers() { if (wxWindow* top = wxGetTopLevelParent(m_canvas_widget)) { top->Unbind(wxEVT_ICONIZE, &DesignCanvas::on_frame_iconize, this); top->Unbind(wxEVT_ACTIVATE, &DesignCanvas::on_frame_activate, this); top->Unbind(wxEVT_MOVE, &DesignCanvas::on_status_hud_reanchor, this); } // Before the popups go down, or a resize still in flight re-places and re-shows the chip. if (m_canvas_widget) m_canvas_widget->Unbind(wxEVT_SIZE, &DesignCanvas::on_status_hud_reanchor, this); // A popup is override-redirect: it does not go down with the frame, so one left showing sits // on the bare desktop for however long the teardown takes. show_status_hud(false); if (m_hud) m_hud->Hide(); if (m_canvas) m_canvas->unbind_event_handlers(); } void DesignCanvas::reset_canvas_volumes() { if (m_canvas) m_canvas->reset_volumes(); } 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::request_repaint() { if (!m_canvas) return; m_canvas->set_as_dirty(); // NOTHING may touch GL before the canvas has initialised it. The backend probe below calls // OpenGLManager::get_gl_info().get_renderer(), which runs GLInfo::detect() -> glGetString // with no context current and, before init_opengl(), no loaded function pointers — a // segfault at startup with no window and nothing in the log. Anything that asks for a // repaint while the panel is still being built lands here, so the guard belongs at the top // rather than around the render() call: the crash was in the PROBE, not in the paint. if (!m_canvas->is_initialized()) { if (m_canvas_widget) m_canvas_widget->Refresh(); // the first real paint draws the current state anyway return; } if (m_sw_gl < 0) { // Cache the backend once it's known; the renderer string is empty until // GL is initialised, so stay "unknown" and take the safe direct path till then. const std::string& r = OpenGLManager::get_gl_info().get_renderer(); if (!r.empty()) m_sw_gl = (boost::icontains(r, "llvmpipe") || boost::icontains(r, "softpipe") || boost::icontains(r, "swrast") || boost::icontains(r, "software")) ? 1 : 0; } if (m_sw_gl == 0) { if (m_canvas_widget) m_canvas_widget->Refresh(); // hardware GL: paint cycle drives render() } else { m_canvas->render(); // software GL or backend not yet known } } void DesignCanvas::enter_viewport() { if (!m_camera_swapped) swap_camera(); } void DesignCanvas::leave_viewport() { if (m_camera_swapped) swap_camera(); } void DesignCanvas::swap_camera() { Plater* plater = wxGetApp().plater(); if (plater == nullptr) return; std::swap(plater->get_camera(), m_parked_camera); m_camera_swapped = !m_camera_swapped; } void DesignCanvas::force_repaint() { if (m_canvas == nullptr || m_canvas_widget == nullptr) return; CallAfter([this]() { if (m_canvas == nullptr || m_canvas_widget == nullptr) return; m_canvas->set_as_dirty(); m_canvas_widget->Refresh(); m_canvas_widget->Update(); // synchronous: an expose may never come after a page show }); } void DesignCanvas::repaint_now() { if (m_canvas == nullptr || m_canvas_widget == nullptr) return; request_repaint(); // mark dirty + Refresh (hardware GL) or render (software GL) m_canvas_widget->Update(); // service the pending paint immediately (a modal popup owns the loop) } 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 = design_selection_color(); // same colour as every other selection 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) { // An EXPLICIT colour outranks the selection tint. m_body_selected is a // document-wide flag raised whenever a non-Sketch feature row is selected — // the normal resting state after any modelling operation — so painting every // body gold on it made the Color tool look broken: the override was written, // carried across recompute and read back correctly, and then overpainted here // every single frame. A body the user deliberately coloured keeps its colour; // the rest still tint, which is all the tint was ever for. const bool overridden = m_color_bodies != nullptr && b >= 0 && b < int(m_color_bodies->size()) && (*m_color_bodies)[b].has_color; ColorRGBA c = (m_body_selected && !overridden) ? sel_gold : body_color(b); if (b == m_hl_body_target) c = ColorRGBA(0.30f, 0.90f, 0.70f, 1.0f); // target = teal-green else if (b == m_hl_body_tool) c = ColorRGBA(1.00f, 0.55f, 0.15f, 1.0f); // tool = orange if (m_body_translucent) c.a(0.30f); else if (m_xray_focus >= 0 && b != m_xray_focus) c.a(0.25f); 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& body_meshes, const std::vector& 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::set_sketch_plane(const SketchPlane& plane) { m_sketch_tool.set_plane(plane); // keeps the 2D entities; only the carrier plane changes if (m_canvas) m_canvas->set_as_dirty(); if (m_canvas_widget) m_canvas_widget->Refresh(); } void DesignCanvas::edit_sketch(const std::vector& entities, const std::vector& 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); } bool DesignCanvas::edit_sketch_selection_value() { const bool ok = m_sketch_tool.open_selection_dimension_editor(); if (ok) request_repaint(); return ok; } int DesignCanvas::toggle_sketch_construction_selection() { const int n = m_sketch_tool.toggle_selection_construction(); if (n > 0) request_repaint(); return n; } bool DesignCanvas::add_sketch_regions( const std::vector>>& regions) { const bool ok = m_sketch_tool.add_imported_regions(regions); if (ok) request_repaint(); return ok; } 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("printable_area"); if (!bed_shape_opt) return; double printable_height = 100.0; const auto* ph_opt = config->opt("printable_height"); if (ph_opt) printable_height = ph_opt->value; m_bed.set_shape(bed_shape_opt->values, printable_height, {}, {}, "", false); // mainline added extruder_areas/heights params } void DesignCanvas::set_show_bed(bool b) { if (!m_canvas) return; if (m_canvas->get_show_bed() == b) return; // no repaint for a no-op toggle m_canvas->set_show_bed(b); request_repaint(); } 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 cb) { m_on_sketch_commit = std::move(cb); } void DesignCanvas::set_on_sketch_entities_commit( std::function&, const std::vector&, const SketchPlane&)> cb) { m_on_sketch_entities_commit = std::move(cb); } void DesignCanvas::set_on_segment_drawn(std::function cb) { m_sketch_tool.on_segment_drawn = std::move(cb); } void DesignCanvas::set_on_cursor_metrics(std::function cb) { m_sketch_tool.on_cursor_metrics = std::move(cb); } void DesignCanvas::set_on_solve_state(std::function cb) { m_sketch_tool.on_solve_state = std::move(cb); } void DesignCanvas::set_on_sketch_step(std::function cb) { m_sketch_tool.on_step_changed = std::move(cb); } void DesignCanvas::apply_segment_length(double len) { m_sketch_tool.apply_segment_length(len); request_repaint(); } void DesignCanvas::keep_segment_as_drawn() { m_sketch_tool.keep_segment_as_drawn(); request_repaint(); } void DesignCanvas::set_on_sketch_selection_changed(std::function cb) { m_sketch_tool.on_selection_changed = std::move(cb); } void DesignCanvas::set_on_sketch_face_selected(std::function cb) { m_sketch_tool.on_face_selected = std::move(cb); } void DesignCanvas::set_on_display_sketch_selected(std::function cb) { m_sketch_tool.on_display_sketch_selected = std::move(cb); } void DesignCanvas::set_on_display_sketch_activated(std::function cb) { m_sketch_tool.on_display_sketch_activated = std::move(cb); } std::vector DesignCanvas::selected_loop_entities() const { return m_sketch_tool.selected_loop_entities(); } std::vector> DesignCanvas::region_entity_indices(const std::vector& ents) const { return m_sketch_tool.region_entity_indices(ents); } std::vector> DesignCanvas::region_entity_indices_with_holes(const std::vector& ents) const { return m_sketch_tool.region_entity_indices_with_holes(ents); } void DesignCanvas::clear_loop_pick() { m_sketch_tool.clear_display_pick(); } void DesignCanvas::set_loop_pick(int feature, int region) { m_sketch_tool.set_display_pick(feature, region); request_repaint(); } void DesignCanvas::set_escalate_on_repick(bool on) { m_sketch_tool.set_escalate_on_repick(on); } void DesignCanvas::set_solid_pick(const std::vector* bodies, const TriangleMesh* mesh, const std::vector* tri_face, const std::vector* tri_body, const std::vector* visible, const std::vector* 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, double body_radius) { m_sketch_tool.set_move_gizmo(body, pivot, base_xform, body_radius); request_repaint(); } void DesignCanvas::clear_move_gizmo() { m_sketch_tool.clear_move_gizmo(); request_repaint(); } bool DesignCanvas::moving_body() const { return m_sketch_tool.moving_body(); } void DesignCanvas::set_on_body_move_changed(std::function 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); request_repaint(); return ok; } void DesignCanvas::clear_fillet_gizmo() { m_sketch_tool.clear_fillet_gizmo(); request_repaint(); } bool DesignCanvas::filleting() const { return m_sketch_tool.filleting(); } void DesignCanvas::set_on_fillet_radius_changed(std::function 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); request_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(); request_repaint(); } bool DesignCanvas::holing() const { return m_sketch_tool.holing(); } void DesignCanvas::set_on_hole_changed(std::function 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); request_repaint(); } void DesignCanvas::clear_thread_gizmo() { m_sketch_tool.clear_thread_gizmo(); request_repaint(); } bool DesignCanvas::threading() const { return m_sketch_tool.threading(); } void DesignCanvas::set_on_thread_changed(std::function 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); request_repaint(); } void DesignCanvas::clear_shell_gizmo() { m_sketch_tool.clear_shell_gizmo(); request_repaint(); } bool DesignCanvas::shelling() const { return m_sketch_tool.shelling(); } void DesignCanvas::set_on_shell_thickness_changed(std::function 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); request_repaint(); } void DesignCanvas::clear_revolve_gizmo() { m_sketch_tool.clear_revolve_gizmo(); request_repaint(); } bool DesignCanvas::revolving() const { return m_sketch_tool.revolving(); } void DesignCanvas::set_on_revolve_angle_changed(std::function cb) { m_sketch_tool.on_revolve_angle_changed = std::move(cb); } void DesignCanvas::set_draft_gizmo(const Vec3d& face_centroid, const Vec3d& face_normal, double angle) { m_sketch_tool.set_draft_gizmo(face_centroid, face_normal, angle); request_repaint(); } void DesignCanvas::clear_draft_gizmo() { m_sketch_tool.clear_draft_gizmo(); request_repaint(); } bool DesignCanvas::drafting() const { return m_sketch_tool.drafting(); } void DesignCanvas::set_on_draft_angle_changed(std::function cb) { m_sketch_tool.set_on_draft_angle_changed(std::move(cb)); } void DesignCanvas::set_cut_gizmo(const SketchPlane& plane, double offset, const Vec3d& body_center, double half_extent) { m_sketch_tool.set_cut_gizmo(plane, offset, body_center, half_extent); request_repaint(); } void DesignCanvas::clear_cut_gizmo() { m_sketch_tool.clear_cut_gizmo(); request_repaint(); } bool DesignCanvas::cutting() const { return m_sketch_tool.cutting(); } void DesignCanvas::set_on_cut_offset_changed(std::function cb) { m_sketch_tool.set_on_cut_offset_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); request_repaint(); } void DesignCanvas::clear_pattern_gizmo() { m_sketch_tool.clear_pattern_gizmo(); request_repaint(); } bool DesignCanvas::patterning() const { return m_sketch_tool.patterning(); } void DesignCanvas::set_on_pattern_changed(std::function cb) { m_sketch_tool.on_pattern_changed = std::move(cb); } void DesignCanvas::set_on_solid_selection_changed(std::function cb) { m_sketch_tool.on_solid_selection_changed = std::move(cb); } void DesignCanvas::set_on_place_on_face(std::function cb) { m_sketch_tool.on_place_on_face = std::move(cb); } void DesignCanvas::select_body(int body) { m_sketch_tool.select_body(body); request_repaint(); } 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); request_repaint(); } void DesignCanvas::clear_extrude_gizmo() { m_sketch_tool.clear_extrude_gizmo(); request_repaint(); } void DesignCanvas::set_on_extrude_depth_changed(std::function cb) { m_sketch_tool.on_extrude_depth_changed = std::move(cb); } void DesignCanvas::set_datum_gizmo(const SketchPlane& plane, double usize, double vsize, const Vec3d& base_origin, const Vec3d& base_normal, double offset, bool offset_on) { m_sketch_tool.set_datum_gizmo(plane, usize, vsize, base_origin, base_normal, offset, offset_on); request_repaint(); } void DesignCanvas::clear_datum_gizmo() { m_sketch_tool.clear_datum_gizmo(); request_repaint(); } void DesignCanvas::set_on_datum_size_changed(std::function cb) { m_sketch_tool.on_datum_size_changed = std::move(cb); } void DesignCanvas::set_on_datum_offset_changed(std::function cb) { m_sketch_tool.on_datum_offset_changed = std::move(cb); } void DesignCanvas::set_helix_gizmo(const SketchPlane& plane, double radius, double pitch, double height, double taper, bool left_handed) { m_sketch_tool.set_helix_gizmo(plane, radius, pitch, height, taper, left_handed); request_repaint(); } void DesignCanvas::clear_helix_gizmo() { m_sketch_tool.clear_helix_gizmo(); request_repaint(); } void DesignCanvas::set_on_helix_changed(std::function cb) { m_sketch_tool.on_helix_changed = std::move(cb); } void DesignCanvas::set_rib_gizmo(const SketchPlane& plane, const Vec2d& p0, const Vec2d& p1, double thickness) { m_sketch_tool.set_rib_gizmo(plane, p0, p1, thickness); request_repaint(); } void DesignCanvas::clear_rib_gizmo() { m_sketch_tool.clear_rib_gizmo(); request_repaint(); } void DesignCanvas::set_on_rib_thickness_changed(std::function cb) { m_sketch_tool.on_rib_thickness_changed = std::move(cb); } void DesignCanvas::set_base_pick(std::vector planes, std::vector bases, std::vector labels) { m_sketch_tool.set_base_pick(std::move(planes), std::move(bases), std::move(labels)); request_repaint(); } void DesignCanvas::clear_base_pick() { m_sketch_tool.clear_base_pick(); request_repaint(); } void DesignCanvas::set_on_datum_base_picked(std::function cb) { m_sketch_tool.on_datum_base_picked = std::move(cb); } void DesignCanvas::set_on_sketch_exit(std::function cb) { m_sketch_tool.on_exit = std::move(cb); } void DesignCanvas::set_on_sketch_exit_refused(std::function cb) { m_sketch_tool.on_exit_refused = std::move(cb); } void DesignCanvas::set_on_move_exit(std::function cb) { m_sketch_tool.on_move_exit = std::move(cb); } void DesignCanvas::set_on_context_menu(std::function cb) { m_on_context_menu = std::move(cb); if (!m_canvas_widget || m_ctx_bound) return; m_ctx_bound = true; // Bound AFTER GLCanvas3D's own handlers, so this runs first and can consume the event. // It only consumes when it actually opens the offer; every other right-click still falls // through to the polyline-chain end and the move gizmo, which were there first. // Right-drag pans. Without remembering where the press landed, every pan ended by popping // the offer over wherever the camera stopped — the menu appearing as the reward for moving // the view. The offer is the release of a STATIONARY right-click, at the same 8 px budget // the left-click pick uses. m_canvas_widget->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& e) { m_ctx_press = e.GetPosition(); m_ctx_press_ms = wxGetLocalTimeMillis().GetValue(); e.Skip(); // the canvas still needs the press to seed the orbit }); m_canvas_widget->Bind(wxEVT_RIGHT_UP, [this](wxMouseEvent& e) { const wxPoint d = e.GetPosition() - m_ctx_press; const long long dt = wxGetLocalTimeMillis().GetValue() - m_ctx_press_ms; // Always read-and-clear, even when another guard already rules the offer out, or a // terminator recorded under one condition would still be pending under the next. const bool terminated = m_sketch_tool.take_right_consumed(); // Click, or navigation? Both budgets must hold: a press that travelled orbited, and a // press that was HELD was aiming to orbit even if the hand never quite moved. Two // independent budgets because the two failure modes are independent — the drift one // alone still popped a menu at the end of a slow, careful orbit. const bool is_click = std::max(std::abs(d.x), std::abs(d.y)) <= kCadRightClickDriftPx && dt <= kCadRightClickMs; if (m_on_context_menu && !terminated && !inline_busy() && is_click) { // The menu belongs to what you POINTED AT — and pointing happened at the PRESS, not // at the release, so the raycast uses the press position. Within a 3 px budget the // two are the same pixel in practice; using the press is what makes that a // guarantee rather than a coincidence. Pick first, so a right-click on a line offers // that line's verbs instead of the empty-selection vocabulary. Selecting an entity // that is already selected is a no-op, so a multi-entity pick survives a // right-click on one of its members. if (m_canvas && m_sketch_tool.select_at_screen(*m_canvas, m_ctx_press.x, m_ctx_press.y)) request_repaint(); m_on_context_menu(m_canvas_widget->ClientToScreen(m_ctx_press)); return; // consumed } e.Skip(); }); } void DesignCanvas::set_on_undo_redo(std::function cb) { m_sketch_tool.on_undo_redo = std::move(cb); } void DesignCanvas::set_display_sketches(std::vector ds) { m_sketch_tool.set_display_sketches(std::move(ds)); // Overlay changed programmatically (no mouse event) — force a repaint. request_repaint(); } void DesignCanvas::set_datum_planes(std::vector planes, std::vector sizes) { m_sketch_tool.set_datum_planes(std::move(planes), std::move(sizes)); request_repaint(); } void DesignCanvas::set_mate_connectors(std::vector g) { m_sketch_tool.set_mate_connectors(std::move(g)); request_repaint(); } void DesignCanvas::set_mate_links(std::vector> l) { m_sketch_tool.set_mate_links(std::move(l)); request_repaint(); } bool DesignCanvas::toggle_planes() { const bool on = m_sketch_tool.toggle_show_planes(); request_repaint(); return on; } bool DesignCanvas::toggle_axes() { const bool on = m_sketch_tool.toggle_show_axes(); request_repaint(); return on; } void DesignCanvas::set_section_plane(bool on, double z, bool keep_upper) { m_section_on = on; // The kept half must read as a SOLID part, never a see-through ghost: make sure no leftover // preview translucency is applied while the section is on. Guarded — a no-op if already opaque. if (on) { set_body_translucent(false); set_body_hidden(false); } if (m_canvas) { if (on) { // GLCanvas3D turns the two clipping planes into a Z-RANGE: set_z_range(-p0.offset, // p1.offset). keep_upper=false keeps the LOWER half (z in [-1e5, z]); keep_upper=true // keeps the OPPOSITE, UPPER half (z in [z, +1e5]). Only HIDES geometry — no bodies. if (keep_upper) { m_canvas->set_clipping_plane(0, ClippingPlane(Vec3d(0.0, 0.0, 1.0), -z)); // min_z = z m_canvas->set_clipping_plane(1, ClippingPlane(Vec3d(0.0, 0.0, 1.0), 1.0e5)); // max_z = +1e5 } else { m_canvas->set_clipping_plane(0, ClippingPlane(Vec3d(0.0, 0.0, 1.0), 1.0e5)); // min_z = -1e5 m_canvas->set_clipping_plane(1, ClippingPlane(Vec3d(0.0, 0.0, 1.0), z)); // max_z = z } m_canvas->set_use_clipping_planes(true); } else { m_canvas->set_use_clipping_planes(false); } m_canvas->set_as_dirty(); } request_repaint(); } double DesignCanvas::model_mid_z() const { const BoundingBoxf3 bb = m_model.bounding_box_exact(); return bb.defined ? bb.center().z() : 0.0; } 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)); place_readout_hud(); } void DesignCanvas::place_readout_hud() { if (!m_hud || !m_hud_label || !m_canvas_widget) return; if (m_hud_last.empty() || !m_canvas_widget->IsShownOnScreen()) { m_hud->Hide(); return; } 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(); } // A popup is override-redirect: the window manager does not own it, so an iconised or // deactivated app would leave the chip sitting on the bare desktop. The status chip already // had to answer this; now that the readout is a popup too, it answers it the same way. void DesignCanvas::show_readout_hud(bool on) { if (!m_hud) return; if (on) place_readout_hud(); else m_hud->Hide(); } // Clear of the view cube and the two round view buttons, which own the bottom-left corner. // Shared by the placement and by the wrap width, which have to agree or the chip wraps to a // width it is then not given. static constexpr int kStatusHudLeftInsetDip = 190; void DesignCanvas::set_status_text(const wxString& text, const wxColour& colour) { if (!m_status_hud || !m_status_hud_label || !m_canvas_widget) return; if (text == m_status_hud_last && colour == m_status_hud_colour) return; m_status_hud_last = text; m_status_hud_colour = colour; if (text.IsEmpty()) { m_status_hud->Hide(); return; } m_status_hud_label->SetForegroundColour(colour); apply_status_label(); place_status_hud(); } // SetLabel + Wrap + Fit, in that order and always together. Moving the status out of the panel // removed the clipping of snaporca-8cc but not the underlying problem: the chip is a top-level // popup that Fit()s to its text, so a long sentence simply grew past the right edge of the canvas // and hung over the window. Wrapping to the room actually available is what makes the earlier // promise — "a sentence can be a sentence" — true at every window width, including the charter's // 1366 reach. Wrap() rewrites the label it is given, so it must follow a fresh SetLabel every // time; that is the whole reason this is one function instead of three call sites. void DesignCanvas::apply_status_label() { if (!m_status_hud || !m_status_hud_label || !m_canvas_widget) return; m_status_hud_label->SetLabel(m_status_hud_last); const int avail = m_canvas_widget->GetClientSize().GetWidth() - m_canvas_widget->FromDIP(kStatusHudLeftInsetDip) - m_canvas_widget->FromDIP(24); if (avail > m_canvas_widget->FromDIP(120)) // a uselessly narrow canvas: leave it unwrapped m_status_hud_label->Wrap(avail); m_status_hud->Fit(); } void DesignCanvas::place_status_hud() { if (!m_status_hud || !m_canvas_widget || m_status_hud_last.IsEmpty()) return; // The canvas has a client size even while its page is hidden, and it is not the size the // page will have when shown — anchoring against it put the chip up on the tab bar, where it // then stayed until the next status change moved it. Nothing to anchor to: stay down. if (!m_canvas_widget->IsShownOnScreen()) { m_status_hud->Hide(); return; } const wxSize cs = m_canvas_widget->GetClientSize(); // Re-wrap first: this also runs on resize, and a chip wrapped for the old width either // overhangs a narrowed canvas or wastes a widened one. apply_status_label(); const wxSize hs = m_status_hud->GetSize(); const int kLeftInset = m_canvas_widget->FromDIP(kStatusHudLeftInsetDip); const wxPoint bl = m_canvas_widget->ClientToScreen( wxPoint(kLeftInset, cs.GetHeight() - hs.GetHeight() - 12)); // No Raise() and no focus juggling: a popup neither takes focus nor falls behind. This was // caught with SNAPORCA_KEYTRACE — shift+S logged a line, the following R logged nothing, and // the only thing between them was the first status update showing this window. if (!m_status_hud->IsShown()) m_status_hud->Show(); // Show before Move (GTK ignores pre-map Move) m_status_hud->Move(bl); } void DesignCanvas::on_frame_iconize(wxIconizeEvent& e) { show_status_hud(!e.IsIconized()); show_readout_hud(!e.IsIconized()); e.Skip(); } void DesignCanvas::on_frame_activate(wxActivateEvent& e) { show_status_hud(e.GetActive()); show_readout_hud(e.GetActive()); e.Skip(); } // wxEvent& so one handler serves both events that invalidate the anchor: the frame moving out // from under the chip, and the canvas resizing under it. void DesignCanvas::on_status_hud_reanchor(wxEvent& e) { place_status_hud(); e.Skip(); } void DesignCanvas::show_status_hud(bool on) { if (!m_status_hud) return; if (on) place_status_hud(); // re-anchors first: the page may have been resized while away else m_status_hud->Hide(); } 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_operand_bodies(int target_body, int tool_body) { if (m_hl_body_target == target_body && m_hl_body_tool == tool_body) return; m_hl_body_target = target_body; m_hl_body_tool = tool_body; reload(true); // recolours the body volumes (same idiom set_body_highlight uses) } void DesignCanvas::set_highlight_sketches(std::vector> hl) { m_sketch_tool.set_highlight_sketches(std::move(hl)); request_repaint(); } 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_xray_focus(int body) { if (m_xray_focus == body) return; m_xray_focus = body; m_sketch_tool.set_pick_only_body(body); reload(true); // re-applies per-body alpha so the non-focused bodies fade } 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(); request_repaint(); } bool DesignCanvas::inline_busy() const { // Two sources, still: the TOOL's flag says a value is pending, the editor says a field is // drawn. They agree now that the field is not a window — the orphan state (logically closed, // still on screen, still eating keys) cannot be represented when there is nothing to leave // mapped — but the union costs nothing and is the honest question to ask. return m_sketch_tool.inline_busy() || (m_inline_editor && m_inline_editor->is_open()); } bool DesignCanvas::inline_has_focus() const { return m_inline_editor && m_inline_editor->has_focus(); } bool DesignCanvas::inline_type_char(int key) { return m_inline_editor && m_inline_editor->type_char(key); } void DesignCanvas::inline_commit() { if (m_inline_editor) m_inline_editor->commit(); } void DesignCanvas::inline_cancel() { if (m_inline_editor) m_inline_editor->cancel(); } void DesignCanvas::request_sketch_exit() { m_sketch_tool.request_exit(); request_repaint(); } bool DesignCanvas::live_sketch_has_work() const { return m_sketch_tool.live_sketch_has_work(); } bool DesignCanvas::undo_last_sketch_entity() { const bool did = m_sketch_tool.undo_last_entity(); if (did) request_repaint(); return did; } bool DesignCanvas::delete_selected_or_last_sketch_entity() { const bool did = m_sketch_tool.delete_selected_or_last(); if (did) request_repaint(); return did; } void DesignCanvas::clear_sketch_selection() { m_sketch_tool.clear_selection(); request_repaint(); } 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); request_repaint(); } void DesignCanvas::open_inline_value(double current, std::function commit, std::function 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 middle of the canvas, where the sketch is // in view. Everything here is canvas DEVICE px, the space the field is drawn in. const wxSize cs = m_canvas_widget->GetClientSize(); const double sf = m_canvas_widget->GetContentScaleFactor(); wxPoint anchor(int(cs.GetWidth() * sf) / 2, int(cs.GetHeight() * sf) / 2); m_sketch_tool.constrain_value_anchor(anchor); // device px in the canvas viewport m_sketch_tool.set_inline_busy(true); m_canvas_widget->SetFocus(); // same reason as the draw-then-edit path: ImGui reads the // canvas's key events, so the canvas must be the focused widget m_inline_editor->open(anchor, current, "", [this, commit](double v) { m_sketch_tool.set_inline_busy(false); if (commit) commit(v); request_repaint(); }, [this, cancel]() { m_sketch_tool.set_inline_busy(false); if (cancel) cancel(); request_repaint(); }); } void DesignCanvas::set_on_dimension_pick_complete(std::function 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); request_repaint(); } void DesignCanvas::cancel_sketch_dimension() { m_sketch_tool.cancel_dimension_value(); request_repaint(); } 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). request_repaint(); } void DesignCanvas::begin_imported_transform( int feat, const std::vector>>& 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); request_repaint(); } void DesignCanvas::set_on_imported_transform(std::function 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(); request_repaint(); } 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& pts) { m_sketch_tool.set_profile_points(pts); request_repaint(); } void DesignCanvas::begin_constrain_entities(const std::vector& ents, const SketchPlane& plane) { m_sketch_tool.begin_constrain_entities(ents, plane); request_repaint(); } bool DesignCanvas::is_constraining_entities() const { return m_sketch_tool.is_constraining_entities(); } int DesignCanvas::sketch_selection_count() const { return int(m_sketch_tool.selection().size()); } bool DesignCanvas::view_normal_to_sketch() { if (m_canvas == nullptr) return false; const SketchPlane& pl = m_sketch_tool.plane(); Camera& cam = wxGetApp().plater()->get_camera(); // Keep the distance: this is an orientation change, not a zoom. The plane's own y axis is // the up vector, so "up" on screen is up in sketch coordinates — which is what makes a // dimension typed after pressing N land where the eye expects it. const double dist = cam.get_distance(); cam.look_at(pl.origin + pl.normal * dist, pl.origin, pl.y_axis); request_repaint(); return true; } bool DesignCanvas::sketch_abort_gesture() { if (!m_sketch_tool.abort_gesture()) return false; request_repaint(); // the rubber band is gone; the canvas must stop drawing it return true; } bool DesignCanvas::sketch_disarm_tool() { if (!m_sketch_tool.disarm_tool()) return false; request_repaint(); return true; } bool DesignCanvas::drawing_in_progress() const { return m_sketch_tool.pending_points() > 0; } bool DesignCanvas::has_any_selection() const { return m_sketch_tool.has_solid_selection() || m_sketch_tool.sketch_has_selection(); } bool DesignCanvas::clear_any_selection() { if (!has_any_selection()) return false; // Both, unconditionally: which of the two is live depends on the mode, and Esc at idle means // "nothing is picked" in either of them. clear_selection() reports through the tool's own // on_selection_changed; the solid side has no such notification, so the panel refreshes what // depends on it (see DesignPanel::escape). m_sketch_tool.clear_selection(); m_sketch_tool.clear_solid_selection(); // clear_solid_selection() is silent by design (recomputes call it while ids are invalid), but // the panel mirrors the pick to aim Extrude and the dress-up tools. An Esc that cleared the // highlight without telling the panel would leave those aimed at a body nothing points to. if (m_sketch_tool.on_solid_selection_changed) m_sketch_tool.on_solid_selection_changed(0, -1, -1, -1); request_repaint(); return true; } bool DesignCanvas::sketch_first_selected_type(SketchEntity::Type& out) const { return m_sketch_tool.first_selected_type(out); } const std::vector& DesignCanvas::sketch_selection() const { return m_sketch_tool.selection(); } const std::vector& DesignCanvas::sketch_entities() const { return m_sketch_tool.entities(); } int DesignCanvas::sketch_constraint_count() const { return int(m_sketch_tool.constraints().size()); } const std::vector& DesignCanvas::sketch_constraints() const { return m_sketch_tool.constraints(); } bool DesignCanvas::remove_sketch_constraint(int idx) { const bool removed = m_sketch_tool.remove_constraint(idx); if (removed) request_repaint(); return removed; } void DesignCanvas::set_on_sketch_constraints_changed(std::function cb) { m_sketch_tool.on_constraints_changed = std::move(cb); } bool DesignCanvas::try_add_sketch_constraints(const std::vector& defs) { return m_sketch_tool.try_add_constraints(defs); } 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& ents) { m_sketch_tool.set_constrain_entities(ents); request_repaint(); } void DesignCanvas::set_constraint_highlight(std::vector entities) { m_sketch_tool.set_constraint_highlight(std::move(entities)); request_repaint(); } void DesignCanvas::set_constraint_glyphs(std::vector cons) { m_sketch_tool.set_constraint_glyphs(std::move(cons)); request_repaint(); } }} // namespace Slic3r::GUI