Merge main

This commit is contained in:
Lam Wei Lun
2026-09-18 15:59:00 +08:00
2713 changed files with 238542 additions and 115600 deletions
+1
View File
@@ -134,6 +134,7 @@ public:
void set_position(Vec2d& position);
void set_axes_mode(bool origin);
void set_axes_origin(const Vec3d& origin) { m_axes.set_origin(origin); } // Design tab: triad at bed centre
const Vec2d& get_position() const { return m_position; }
// Build volume geometry for various collision detection tasks.
+2 -2
View File
@@ -1197,11 +1197,11 @@ void AMSDryCtrWin::update_normal_description(DevAms* dev_ams)
for (const auto& lim : ams_limits) {
if (dev_ams->GetAmsType() == lim.type) {
if (temp_val > lim.max_temp) {
wxString msg = wxString(lim.name) + _L(" maximum drying temperature is ") + wxString::Format(wxT("%d"), lim.max_temp) + wxString::FromUTF8("°C.");
wxString msg = wxString::Format(_L("%s maximum drying temperature is %d°C."), wxString(lim.name), lim.max_temp);
warning_text += msg + "\n";
can_enable_button = false;
} else if (temp_val < lim.min_temp) {
wxString msg = wxString(lim.name) + _L(" minimum drying temperature is ") + wxString::Format(wxT("%d"), lim.min_temp) + wxString::FromUTF8("°C.");
wxString msg = wxString::Format(_L("%s minimum drying temperature is %d°C."), wxString(lim.name), lim.min_temp);
warning_text += msg + "\n";
can_enable_button = false;
}
File diff suppressed because it is too large Load Diff
+436
View File
@@ -0,0 +1,436 @@
#ifndef slic3r_DesignCanvas_hpp_
#define slic3r_DesignCanvas_hpp_
#include <wx/panel.h>
#include <wx/popupwin.h>
#include <functional>
#include <memory>
#include <string>
#include "slic3r/GUI/3DBed.hpp"
#include "slic3r/GUI/Camera.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/CAD/SketchEngine.hpp"
#include "slic3r/GUI/CAD/DesignSketchTool.hpp"
class wxGLCanvas;
class wxFrame;
class wxStaticText;
namespace Slic3r {
class TriangleMesh;
namespace GUI {
class GLCanvas3D;
class SketchInlineEditor;
class DesignCanvas : public wxPanel
{
public:
explicit DesignCanvas(wxWindow* parent);
~DesignCanvas() override;
void set_mesh(const TriangleMesh& mesh);
// Multi-body display: one GLVolume per body, each coloured distinctly (per-body colour).
// `visible` (optional, indexed by body) hides bodies whose flag is false.
void set_bodies(const std::vector<TriangleMesh>& body_meshes,
const std::vector<bool>& visible = {});
void clear_mesh();
void set_preview_mesh(const TriangleMesh& mesh);
void clear_preview();
void fit_view();
void set_view(const std::string& view_name);
void begin_sketch(const SketchPlane& plane, DesignSketchTool::Mode mode);
// Re-open a committed entity sketch for full in-canvas editing (load geometry +
// constraints, re-detect feature groups). Re-commits via finish_sketch().
void edit_sketch(const std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints,
const SketchPlane& plane);
void set_sketch_tool(DesignSketchTool::Mode mode);
void set_sketch_plane(const SketchPlane& plane); // re-plane the live sketch when a reference plane is clicked in 3D
void set_sketch_construction(bool c);
// Flip the sketch selection between construction and real geometry; returns the
// number of entities changed (0 = nothing selected, caller falls back to the mode).
// Open the in-canvas value field on the sketch selection's defining number.
bool edit_sketch_selection_value();
int toggle_sketch_construction_selection();
// Is the sketch tool on Select (as opposed to a draw/edit tool being armed)? The
// Construction box needs it to tell "convert what I picked" from "arm what I draw next".
bool sketch_is_selecting() const { return m_sketch_tool.mode() == DesignSketchTool::Mode::Select; }
// Text / SVG art into the LIVE sketch, as ordinary editable lines. False = no session.
bool add_sketch_regions(const std::vector<std::vector<std::vector<Vec2d>>>& regions);
void set_sketch_polygon_sides(int n);
void set_sketch_polygon_circumscribed(bool c);
void finish_sketch();
bool is_sketching() const;
void refresh_bed(); // re-sync the bed to the current printer (call on tab activation)
// The Camera is Plater-owned and shared with Prepare/Preview/Assemble; GLCanvas3D has no
// per-canvas camera, so every orbit here would otherwise overwrite what the editor tabs
// show. Exactly one of the two views is live at a time, so entering and leaving are the
// same operation: trade the live camera for the parked one. That also keeps this canvas's
// own view across a tab switch.
void enter_viewport();
void leave_viewport();
void unbind_canvas_event_handlers(); // app close / language switch, from the plater's teardown
void reset_canvas_volumes();
void set_show_bed(bool b); // view option: draw the printer bed + plate grid, or not
// N: look straight down the sketch plane's normal, keeping the current zoom. A sketch drawn
// at an angle is a sketch drawn wrong, and no amount of orbiting by hand lands exactly square.
bool view_normal_to_sketch();
void cancel_sketch();
void set_on_sketch_commit(std::function<void(const SketchProfile&, const SketchPlane&)> cb);
void set_on_sketch_entities_commit(
std::function<void(const std::vector<SketchEntity>&,
const std::vector<SketchEntityConstraintDef>&,
const SketchPlane&)> cb);
// Line tool: pending-segment length entry + live readout (Phase 2).
void set_on_segment_drawn(std::function<void(double, double)> cb);
void set_on_cursor_metrics(std::function<void(double, double, bool)> cb);
void set_on_solve_state(std::function<void(int, bool, bool)> cb); // dof, ok, has_constraints
// Live per-step guidance from the armed sketch tool (mode, step, picks). 1c0c.
void set_on_sketch_step(std::function<void(DesignSketchTool::Mode, int, int)> cb);
void apply_segment_length(double len); // exact length, then commit & repaint
void keep_segment_as_drawn(); // commit as-drawn & repaint
// Sketch selection (Mode::Select).
void set_on_sketch_selection_changed(std::function<void(int)> cb);
void set_on_sketch_face_selected(std::function<void(int)> cb); // closed loop clicked: region index passed
void set_on_display_sketch_selected(std::function<void(int, int, int)> cb); // committed loop clicked: (feature, region, entity)
void set_on_display_sketch_activated(std::function<void(int)> cb); // committed sketch DOUBLE-clicked: edit it
std::vector<SketchEntity> selected_loop_entities() const; // entities of the click-selected loop
std::vector<std::vector<int>> region_entity_indices(const std::vector<SketchEntity>& ents) const;
// Like region_entity_indices, but each region's entry is its OWN entities followed by the
// entities of each of its holes — the same order selected_loop_entities() hands the kernel.
// A per-loop extrude of a region WITH holes stores exactly this, so this is the shape a
// consumed loop must be compared against.
std::vector<std::vector<int>> region_entity_indices_with_holes(const std::vector<SketchEntity>& ents) const;
void clear_loop_pick(); // drop the click-selected loop highlight (e.g. after extrude)
void set_loop_pick(int feature, int region); // adopt a loop pick made before the commit
void set_escalate_on_repick(bool on); // off while a card has armed a face/edge pick
// Solid whole/face/edge selection: point the tool at the bodies + concatenated
// tessellation (with per-triangle face & body ids), and a callback fired on each
// whole->face->edge cycle (level, body index, face id, edge id).
void set_solid_pick(const std::vector<CadBody>* bodies, const TriangleMesh* mesh,
const std::vector<int>* tri_face, const std::vector<int>* tri_body,
const std::vector<bool>* visible = nullptr,
const std::vector<Transform3d>* xform = nullptr);
void set_on_solid_selection_changed(std::function<void(int, int, int, int)> cb);
void set_on_place_on_face(std::function<bool()> cb); // F key: Place on Face
void select_body(int body); // Parts-list -> highlight a whole body by index
// Effective display colour of a body: the per-body override (Color tool) when set,
// otherwise the auto body-index palette. Single source of truth shared with reload().
ColorRGBA body_color(int body) const;
// Move-body gizmo (M5): three world-axis drag arrows on a body; drag fires the move
// callback with the body index + accumulated translation (display-only, host applies it).
// body_radius = bounding-sphere radius of the body in world mm; the gizmo scales with it so
// the rotation rings sit OUTSIDE the solid (Orca's Prepare gizmos do the same).
void begin_move_body(int body, const Vec3d& pivot, const Transform3d& base_xform,
double body_radius);
void clear_move_gizmo();
bool moving_body() const;
void set_on_body_move_changed(std::function<void(int, const Transform3d&)> cb);
// Visual Fillet/Chamfer radius gizmo: when a solid edge is picked, anchor a radius arrow on
// it; drag/edit fire the radius callback. Returns false if no edge is currently picked.
bool begin_fillet_gizmo(const Vec3d& body_centroid, double radius);
void clear_fillet_gizmo();
bool filleting() const;
void set_on_fillet_radius_changed(std::function<void(double)> cb);
// Visual Hole gizmo: the panel feeds the hole plane + position + diameter/depth/through while
// its Hole card is open; drag/edit fire the hole callback (x, y, diameter, depth).
void begin_hole_gizmo(const SketchPlane& plane, double x, double y,
double diameter, double depth, bool through);
void set_hole_face_bounds(bool has, double umin, double umax, double vmin, double vmax);
void clear_hole_gizmo();
bool holing() const;
void set_on_hole_changed(std::function<void(double, double, double, double)> cb);
// Visual Thread gizmo: footprint circle + radius/length arrows + draggable centre.
void begin_thread_gizmo(const SketchPlane& plane, double x, double y,
double radius, double height);
void clear_thread_gizmo();
bool threading() const;
void set_on_thread_changed(std::function<void(double, double, double, double)> cb);
// Visual Shell gizmo: inward thickness arrow at the picked open-face centroid.
void begin_shell_gizmo(const Vec3d& face_centroid, const Vec3d& inward_dir, double thickness);
void clear_shell_gizmo();
bool shelling() const;
void set_on_shell_thickness_changed(std::function<void(double)> cb);
// Visual Revolve angle-arc gizmo: the panel feeds the sketch plane + profile centroid + axis
// (0=plane X, 1=plane Y) + angle + flip while its Revolve card is open; drag/edit fire the
// angle callback.
void begin_revolve_gizmo(const SketchPlane& plane, const Vec2d& centroid,
int axis_sel, double angle, bool flip);
void clear_revolve_gizmo();
bool revolving() const;
void set_on_revolve_angle_changed(std::function<void(double)> cb);
// Visual Draft angle-arc gizmo: the panel feeds the face centroid + face normal + angle while
// its Draft card is open; drag/edit fire the angle callback.
void set_draft_gizmo(const Vec3d& face_centroid, const Vec3d& face_normal, double angle);
void clear_draft_gizmo();
bool drafting() const;
void set_on_draft_angle_changed(std::function<void(double)> cb);
// Visual Cut gizmo: plane-rectangle preview + draggable normal offset arrow while
// the Cut card is open; drag fires the offset callback.
void set_cut_gizmo(const SketchPlane& plane, double offset, const Vec3d& body_center, double half_extent);
void clear_cut_gizmo();
bool cutting() const;
void set_on_cut_offset_changed(std::function<void(double)> cb);
// Visual Pattern gizmo: the panel feeds the (world XY) plane + target body centroid + mode +
// count/dir/spacing/angle while its Pattern card is open; drag/edit fire the value callback.
void begin_pattern_gizmo(const SketchPlane& plane, const Vec3d& body_centroid, bool circular,
int count, int dir, double spacing, double angle);
void clear_pattern_gizmo();
bool patterning() const;
void set_on_pattern_changed(std::function<void(double)> cb);
// Visual Extrude depth-arrow gizmo (C5b): the panel feeds the profile plane + centroid +
// live depths/flags while its Extrude card is open; drag/edit fire the depth callback.
void set_extrude_gizmo(const SketchPlane& plane, const Vec2d& centroid,
double depth, double depth2, bool two_sided, bool flip);
void clear_extrude_gizmo();
void set_on_extrude_depth_changed(std::function<void(double, bool)> cb);
void set_datum_gizmo(const SketchPlane& plane, double usize, double vsize,
const Vec3d& base_origin, const Vec3d& base_normal,
double offset, bool offset_on); // C3 resize handles + offset arrow
void clear_datum_gizmo();
void set_on_datum_size_changed(std::function<void(double, double)> cb);
void set_on_datum_offset_changed(std::function<void(double)> cb);
void set_helix_gizmo(const SketchPlane& plane, double radius, double pitch, double height,
double taper, bool left_handed); // helix curve + 3 drag handles
void clear_helix_gizmo();
void set_on_helix_changed(std::function<void(double, double, double)> cb);
void set_rib_gizmo(const SketchPlane& plane, const Vec2d& p0, const Vec2d& p1, double thickness); // rib slab footprint + 2 thickness handles
void clear_rib_gizmo();
void set_on_rib_thickness_changed(std::function<void(double)> cb);
void set_base_pick(std::vector<SketchPlane> planes, std::vector<int> bases,
std::vector<std::string> labels = {}); // clickable labelled reference planes
void clear_base_pick();
void set_on_datum_base_picked(std::function<void(int)> cb);
void set_on_sketch_exit(std::function<void()> cb); // Esc -> exit the tool
void set_on_sketch_exit_refused(std::function<void()> cb); // Esc declined: sketch has work
void set_on_undo_redo(std::function<void(bool /*redo*/)> cb); // Ctrl+Z / Ctrl+Shift+Z
// Persistently draw committed sketches (un-consumed ones stay visible).
void set_display_sketches(std::vector<DesignSketchTool::DisplaySketch> ds);
void set_highlight_sketches(std::vector<std::pair<int, ColorRGBA>> hl);
void set_datum_planes(std::vector<SketchPlane> planes,
std::vector<Vec2d> sizes = {}); // draw datum/reference planes (u/v extents)
// Mate connectors, drawn as frames so their verse and polarity are visible (wgsc).
void set_mate_connectors(std::vector<DesignSketchTool::MateConnectorGlyph> g);
void set_mate_links(std::vector<std::pair<Vec3d, Vec3d>> l);
void set_body_highlight(bool on); // tint the solid when its feature is tree-selected
// The status line, shown along the BASE OF THE VIEWPORT rather than in the side panel:
// the panel clips it at ~73 characters with no warning (8cc), the viewport's
// bottom margin has the whole window width to spare. Empty text hides it.
void set_status_text(const wxString& text, const wxColour& colour);
// Take the status line down / bring it back when the Design page leaves and re-enters view.
// A popup is a TOP-LEVEL window: hiding the page it belongs to does not hide it. Keeps the
// text, so coming back needs no re-selection.
void show_status_hud(bool on);
void set_operand_bodies(int target_body, int tool_body); // -1,-1 clears
void set_body_translucent(bool on); // render the solid see-through (fillet/chamfer preview)
void set_xray_focus(int body); // >=0: fade+lock out every other body (CoordSys picking)
void set_body_hidden(bool on); // preview-only: hide base bodies, show only the result ghost
void set_on_move_exit(std::function<void()> cb); // right-click finished the move-body gizmo
// Right-click (or its platform equivalent) on the viewport with no tool running: open the
// object-driven offer there. Fires with SCREEN coordinates. Deliberately NOT fired while a
// tool is live — right-click already ends a polyline chain and finishes the move gizmo, and
// taking those over would break two working interactions in order to add a third.
void set_on_context_menu(std::function<void(const wxPoint&)> cb);
void delete_selected_sketch_entities();
bool inline_busy() const; // a sketch value field is open (guard keys)
bool inline_has_focus() const; // the field itself holds keyboard focus
void inline_commit(); // accept the typed value (Enter/Tab)
void inline_cancel(); // discard the typed value (Esc)
// The layered Esc: abandon the points of the gesture in progress, else drop the armed tool
// back to Select, else leave the sketch. Same call GLCanvas3D::on_char makes, exposed so the
// panel can do it when focus is not on the canvas.
void request_sketch_exit();
bool live_sketch_has_work() const; // the live sketch holds entities a cancel would destroy
bool undo_last_sketch_entity(); // Ctrl+Z in a sketch: drop the last entity
bool delete_selected_or_last_sketch_entity(); // Delete in a sketch: selected, else last
void clear_sketch_selection();
// View toggles (keys P / A): origin planes, world axis triad. Each returns the new on/off
// state so the caller can echo it in the status bar.
bool toggle_planes();
bool toggle_axes();
// Section views (non-destructive): the panel owns the named "Section View N" list; the canvas
// just applies/clears one horizontal clip at a time. model_mid_z() is the default cut height.
void set_section_plane(bool on, double z, bool keep_upper = false);
double model_mid_z() const;
// Dimension tool: act on the current sketch selection.
DesignSketchTool::DimType sketch_dimension_kind() const;
double sketch_dimension_current() const;
void apply_sketch_dimension(double v);
// Open the in-canvas value editor at the cursor for a host-driven value (the
// committed-feature Constrain path uses this instead of a docked numeric card).
void open_inline_value(double current, std::function<void(double)> commit,
std::function<void()> cancel = {});
// Dimension tool (Mode::Dimension): click-to-place quotes. The pick-complete
// callback lets the panel pop the value card; set/cancel apply or keep the value.
void set_on_dimension_pick_complete(std::function<void(double)> cb);
DesignSketchTool::DimType pending_dimension_type() const;
void set_sketch_dimension_value(double v);
void cancel_sketch_dimension();
// Constrain mode: load a committed profile for picking + constraint editing.
void begin_constrain(const SketchProfile& prof, const SketchPlane& plane);
// Leave constrain mode and clear any picked-entity highlight from the overlay.
void end_constrain();
bool is_constraining() const;
bool selected_segment(int& a, int& b) const;
void update_constrain_profile(const std::vector<Vec2d>& pts);
// Entity-aware Constrain (Fase 4.2): pick Line entities of a committed sketch.
void begin_constrain_entities(const std::vector<SketchEntity>& ents, const SketchPlane& plane);
bool is_constraining_entities() const;
// Sketch selection, for the offer menu: how many entities are selected and what the first
// one is. Returns 0 when nothing is selected.
int sketch_selection_count() const;
// Esc routing (DesignInteraction.hpp). The panel decides WHICH level one press belongs to;
// these are the levels it can act on inside the canvas. Each returns whether it did anything,
// so the panel can fall through to the next level without asking twice.
bool sketch_abort_gesture(); // CadLevel::Gesture — drop the entity being drawn
bool sketch_disarm_tool(); // CadLevel::Tool — armed sketch tool falls back to Select
bool drawing_in_progress() const;// an entity has clicks down but is not committed
bool has_any_selection() const; // model pick or sketch pick
bool clear_any_selection(); // CadLevel::Idle — drop both; true if anything was dropped
bool sketch_first_selected_type(SketchEntity::Type& out) const;
// Live sketch session (Fase 4.2 live constraint path): the panel reads the in-session
// selection and entities, and commits a planned constraint through the tool's
// append->solve->keep-or-rollback, rather than reaching into mcp_sketch_tool().
const std::vector<int>& sketch_selection() const;
const std::vector<SketchEntity>& sketch_entities() const;
// How many constraints the LIVE session holds. Only a count: the hint line needs to know
// whether any badge is on screen to talk about, nothing more.
int sketch_constraint_count() const;
const std::vector<SketchEntityConstraintDef>& sketch_constraints() const;
bool remove_sketch_constraint(int idx);
void set_on_sketch_constraints_changed(std::function<void()> cb);
bool try_add_sketch_constraints(const std::vector<SketchEntityConstraintDef>& defs);
// In-canvas bbox transform of imported Text/SVG art (replaces the Move/Scale dialog).
void begin_imported_transform(int feat,
const std::vector<std::vector<std::vector<Vec2d>>>& base_regions,
const SketchPlane& plane, const Vec2d& offset,
double scale_x, double scale_y);
void set_on_imported_transform(std::function<void(int, Vec2d, double, double)> cb);
bool selected_constrain_entities(int& e0, int& e1) const;
int selected_constrain_axis() const; // third pick slot (Symmetric axis), -1 if unset
bool pick0_point(Vec2d& out) const; // plane-coords of the slot-0 pick (trim/extend)
void update_constrain_entities(const std::vector<SketchEntity>& ents);
// Constraint manager (C3.4): highlight the entities referenced by a selected
// constraint (yellow tint in Constrain mode); empty clears the highlight.
void set_constraint_highlight(std::vector<int> entities);
// Constraint glyph badges (C3.4b): the feature's constraints, drawn as iconic
// marks near their entities in Constrain mode; empty clears them.
void set_constraint_glyphs(std::vector<SketchEntityConstraintDef> cons);
// Repaint the embedded canvas the right way for the active GL backend:
// hardware GL gets a scheduled wxEVT_PAINT (render() runs inside the paint
// cycle); software GL (llvmpipe etc.) gets a direct render() because a
// scheduled Refresh() is frequently dropped there. Backend cached on first use.
// Public: DesignPanel calls it after a tree edit to force a frame on software GL.
// Scripted (MCP) access to the live sketch. One accessor rather than a passthrough per
// verb: the MCP layer drives the SAME tool the mouse drives, which is the whole point of
// having it — a socket that talked to a private copy would prove nothing about the app.
DesignSketchTool& mcp_sketch_tool() { return m_sketch_tool; }
const DesignSketchTool& mcp_sketch_tool() const { return m_sketch_tool; }
void request_repaint();
// Repaint synchronously, once the pending show/resize has settled. Needed when the
// notebook re-shows the Design page: an invalidation issued while the page is still
// being shown is dropped on hardware GL and no wxEVT_PAINT ever follows, leaving the
// canvas blank until another tab switch forces an expose.
void force_repaint();
// Repaint synchronously, for use while a modal popup (the offer menu) owns the event loop:
// a queued Refresh() is not serviced until the popup closes, so a hover ghost drawn behind it
// would never appear. Mirrors DesignPanel's m_status->Update() flush.
void repaint_now();
private:
void reload(bool keep_view);
void swap_camera(); // enter_viewport / leave_viewport, in the one direction they share
wxGLCanvas* m_canvas_widget{nullptr};
GLCanvas3D* m_canvas{nullptr};
int m_sw_gl{-1}; // -1 unknown, 0 hardware GL, 1 software GL
std::function<void(const wxPoint&)> m_on_context_menu;
bool m_ctx_bound{false}; // bind the RIGHT_UP handler once, however often the cb is set
wxPoint m_ctx_press{0, 0}; // right-press origin: a right-DRAG orbits, it must not offer
long long m_ctx_press_ms{0}; // and a right-HOLD is navigation too, however still it is held
Bed3D m_bed;
// The half of the camera swap above that is NOT on screen: the editor tabs' view while
// Design is up, this canvas's view while it is not. Seeded in the constructor so the first
// entry inherits the view the user was already looking at.
Camera m_parked_camera;
bool m_camera_swapped{false}; // guards a leave without an enter, and the reverse
Model m_model;
bool m_first_frame{true};
bool m_body_selected{false}; // tree selected a body feature → tint the solid
int m_hl_body_target{-1};
int m_hl_body_tool{-1};
bool m_body_translucent{false};// fillet/chamfer preview → render the body see-through
int m_xray_focus{-1}; // >=0: only this body is opaque+clickable (CoordSys picking)
bool m_body_hidden{false}; // preview-only mode → hide base bodies, ghost = the result
std::vector<bool> m_body_visible; // per-body visibility (empty => all visible)
// Live pointer to the document's bodies (stable address: m_doc.bodies), stashed by
// set_solid_pick so reload()/body_color() can read each body's colour override.
const std::vector<CadBody>* m_color_bodies{nullptr};
DesignSketchTool m_sketch_tool;
// Section view: whether a horizontal clip is currently applied (guards Alt+Wheel). The cut
// height and the named-view list live in DesignPanel; the canvas is a dumb applier.
bool m_section_on{false};
std::unique_ptr<SketchInlineEditor> m_inline_editor; // floating in-canvas value editor
// Bottom-right viewport HUD: a borderless float label over the GL canvas showing the
// active tool's current values (fed by the tool's on_readout). Empty text hides it.
// A wxPopupWindow for the SAME reason as the status chip below, and it was a wxFrame until
// the reason was measured rather than assumed: "it appears mid-gesture and the next input is
// the mouse" is false. The chip keeps the last value on screen AFTER the gesture ends, and a
// frame holds the X input focus once it has it — so the next keystroke went to a 119x31
// window that has no use for it. Measured on :10: focus on the chip, `r` produced no
// CHAR_HOOK line at all; one bare canvas click moved focus back and the same key armed the
// tool. That is every sketch shortcut dead after every dimensioned entity.
wxPopupWindow* m_hud{nullptr};
wxStaticText* m_hud_label{nullptr};
std::string m_hud_last;
void set_readout(const std::string& text);
void place_readout_hud(); // anchor + show, using m_hud_last
void show_readout_hud(bool on); // iconise/deactivate: a popup would float on the desktop
// Bottom-LEFT viewport HUD: the selection / tool status line, written by DesignPanel.
// A wxPopupWindow, NOT the wxFrame the readout HUD uses: a frame accepts keyboard focus,
// and this one is on screen permanently and re-raised on every status change, so it stole
// the keyboard from the canvas and killed every sketch shortcut in the tab.
wxPopupWindow* m_status_hud{nullptr};
wxStaticText* m_status_hud_label{nullptr};
wxString m_status_hud_last;
wxColour m_status_hud_colour;
void place_status_hud(); // re-anchors to the canvas corner (also on resize)
void apply_status_label(); // SetLabel + Wrap to the canvas width + Fit, always together
// On the top-level frame, which outlives this canvas — members so they can be unbound.
void on_frame_iconize(wxIconizeEvent& e);
void on_frame_activate(wxActivateEvent& e);
void on_status_hud_reanchor(wxEvent& e); // frame wxEVT_MOVE and canvas wxEVT_SIZE
std::function<void(const SketchProfile&, const SketchPlane&)> m_on_sketch_commit;
std::function<void(const std::vector<SketchEntity>&,
const std::vector<SketchEntityConstraintDef>&,
const SketchPlane&)> m_on_sketch_entities_commit;
};
}} // namespace Slic3r::GUI
#endif // slic3r_DesignCanvas_hpp_
+59
View File
@@ -0,0 +1,59 @@
#ifndef slic3r_GUI_DesignInteraction_hpp_
#define slic3r_GUI_DesignInteraction_hpp_
namespace Slic3r { namespace GUI {
// The Design tab's interaction stack, and the ONE rule Esc obeys.
//
// Esc unwinds exactly one level per press, deepest first, and never more. The enum value IS
// the LIFO depth, so "which level does this press belong to" is a comparison, not a chain of
// special cases scattered over three files — which is what it was, and why two presses in a
// row could reach past a tool and destroy the sketch underneath it.
//
// STRICT INVARIANT (the bug this exists to make unrepresentable): no level of Esc deletes a
// feature, discards a sketch that holds geometry, or rolls history back. Destroying work needs
// a gesture that says so — Delete/Backspace on an explicit selection, the banner's Cancel, or
// Ctrl+Z. An Esc that can destroy is an Esc nobody can press with confidence, and being the
// safe key is the whole point of it.
enum class CadLevel : int {
Idle = 0, // nothing transient is up: Esc clears the selection
Tool = 1, // a feature card / armed sketch tool / constrain session: Esc exits it
Gesture = 2, // an uncommitted delta (entity being drawn, body being dragged): Esc reverts it
Transient = 3, // a value field or a popup menu: Esc closes just that
};
// What the tab is doing, reduced to the four bits the routing actually needs. Kept as a POD of
// answers rather than a pointer to the panel so the rule below is decidable — and checkable —
// without a window, a GL context or an event loop.
struct CadInteractionState {
bool value_field_open{false}; // in-canvas value field, or the panel's value card
bool gesture_active{false}; // in-progress entity points, or a body being moved
bool tool_armed{false}; // feature card open, sketch draw tool armed, constrain session
bool has_selection{false}; // something is picked (model or sketch)
};
// The whole routing rule. Deepest live level wins; Idle is the floor.
constexpr CadLevel cad_escape_level(const CadInteractionState& s)
{
if (s.value_field_open) return CadLevel::Transient;
if (s.gesture_active) return CadLevel::Gesture;
if (s.tool_armed) return CadLevel::Tool;
return CadLevel::Idle;
}
// The ordering is the entire contract, so it is checked where it is defined, at compile time.
static_assert(cad_escape_level({true, true, true, true}) == CadLevel::Transient, "value field is deepest");
static_assert(cad_escape_level({false, true, true, true}) == CadLevel::Gesture, "gesture beats tool");
static_assert(cad_escape_level({false, false, true, true}) == CadLevel::Tool, "tool beats idle");
static_assert(cad_escape_level({false, false, false, true}) == CadLevel::Idle, "selection is idle-level");
static_assert(cad_escape_level({false, false, false, false}) == CadLevel::Idle, "empty is idle");
// Right-click vs. right-hold-orbit. A press that stays put and is let go promptly is a click and
// summons the offer; anything longer or further was navigation, and navigation must never be
// rewarded with a menu over wherever the camera happened to stop.
inline constexpr int kCadRightClickMs = 200; // press->release budget
inline constexpr int kCadRightClickDriftPx = 3; // cursor drift budget, max(|dx|,|dy|)
}} // namespace Slic3r::GUI
#endif // slic3r_GUI_DesignInteraction_hpp_
+189
View File
@@ -0,0 +1,189 @@
// GENERATED FILE — DO NOT EDIT.
// Source: docs/ux/tool_atlas.json Generator: docs/ux/mockups/gen_offer_table.py
//
// The object-driven tool offer (charter 4.1): every verb has ONE row index, that index
// is the same in every selection it appears in, and verbs that do not apply are shown
// disabled in place with their reason rather than removed. Row order was ratified
// 2026-07-31; changing an index is a breaking change to every user's muscle memory.
#ifndef slic3r_GUI_DesignOffer_hpp_
#define slic3r_GUI_DesignOffer_hpp_
#include <cstdint>
namespace Slic3r { namespace GUI {
// What the viewport has selected. Ordered as in tool_atlas.json; the bitmask in
// OfferVerb::accepts indexes these.
enum class OfferSel : int {
None = 0,
FacePlanar = 1,
FaceCyl = 2,
FaceOther = 3,
EdgeStr = 4,
EdgeCirc = 5,
Vertex = 6,
BodySolid = 7,
BodySheet = 8,
Bodies2 = 9,
DatumPlane = 10,
DatumAxis = 11,
CoordSys = 12,
Art = 13,
SkLoop = 14,
SkNone = 15,
SkLine = 16,
SkArc = 17,
SkPoint = 18,
Sk2Ent = 19,
Count = 20
};
inline uint32_t offer_bit(OfferSel s) { return 1u << int(s); }
// One row of the offer. `action` routes to the code that already implements the verb:
// "key:S+E" -> m_keys_feature[SHIFT('E')]
// "key:L" -> m_keys_sketch['L']
// "fly:material#4" -> row 4 of the "material" feature flyout
// "btn:delete" -> a standalone toolbar button
// nullptr -> kernel support exists, no GUI path yet (row shows disabled)
struct OfferVerb {
const char* id;
const char* name; // drawing-office word (L10); translated at use with wxGetTranslation
int row; // 0..7, the ratified index — NEVER reorder
const char* key; // shortcut shown in the row, or nullptr
const char* action;
const char* refusal; // why this row is greyed, in the product's own words
uint32_t accepts; // bitmask over OfferSel
int need_bodies;
int need_sketches;
bool need_sheet;
bool sketch_mode; // belongs to the sketch-mode vocabulary, not the model one
// Second level INSIDE a row, for tools that come in variants: "Rectangle" holds corner,
// centre, oblique and rounded. nullptr = sits directly in the row. Keeps the row's own
// address fixed (L4.1) while the variants hang one level below it, mirroring the toolbar's
// grouping instead of flattening 19 create tools into one wall.
const char* family;
const char* icon; // resources/images name, or nullptr — the offer draws it beside the row
const char* hint; // what the verb does / what to click; shown on hover
};
// Row labels, in ratified order.
static const char* const kOfferRowNames[] = {
"Create",
"Add material",
"Remove",
"Fillet / chamfer / draft",
"Repeat",
"Transform",
"Reference",
"Modify",
};
static const int kOfferRowCount = 8;
static const OfferVerb kOfferVerbs[] = {
{"sketch", "Sketch", 0, "Shift+S", "key:S+S", "Click a face or a reference plane in the viewport, then a sketch tool", 0x00000403u, 0, 0, false, false, nullptr, "design_sketch", "Click a face or a reference plane, then pick a drawing tool"},
{"extrude", "Extrude", 1, "Shift+E", "key:S+E", "Create a sketch, or pick a solid face, first", 0x00004002u, 0, 0, false, false, nullptr, "design_extrude", "Extrude a sketch profile, or push/pull a picked face"},
{"revolve", "Revolve", 1, "Shift+R", "key:S+R", "Create a sketch profile to revolve first", 0x00004000u, 0, 0, false, false, nullptr, "design_revolve", "Revolve a profile about an axis"},
{"sweep", "Sweep", 1, "Shift+W", "key:S+W", "Create a profile sketch to sweep first", 0x00004000u, 0, 2, false, false, nullptr, "design_sweep", "Sweep a profile along a path"},
{"loft", "Loft", 1, "Shift+L", "key:S+L", "Create at least two profile sketches to loft", 0x00004000u, 0, 2, false, false, nullptr, "design_loft", "Loft (skin) between two or more profiles"},
{"thicken", "Thicken", 1, nullptr, "fly:material#4", "Thicken needs a solid body — add or import one first", 0x0000000au, 1, 0, false, false, nullptr, "design_thicken", "Offset a solid face into a thin plate (new body)"},
{"rib", "Rib", 1, nullptr, "fly:material#5", "Rib needs a solid body — add or import one first", 0x00010000u, 1, 0, false, false, nullptr, "design_rib", "Grow a thin wall from an open sketch line, fused to a body"},
{"boolean", "Union", 1, "Shift+B", "btn:bool#0", "Boolean needs two bodies — create or import a second solid", 0x00000200u, 2, 0, false, false, nullptr, "design_boolean", "Fuse the tool body into the target — one solid, no seam"},
{"bool_subtract", "Subtract", 1, nullptr, "btn:bool#1", "Boolean needs two bodies — create or import a second solid", 0x00000200u, 2, 0, false, false, nullptr, "design_boolean", "Cut the tool body out of the target"},
{"bool_intersect", "Intersect", 1, nullptr, "btn:bool#2", "Boolean needs two bodies — create or import a second solid", 0x00000200u, 2, 0, false, false, nullptr, "design_boolean", "Keep only where the two bodies overlap"},
{"surf_extrude", "Surface Extrude", 1, "Shift+G", "key:S+G", "Create a sketch first", 0x00004000u, 0, 0, false, false, nullptr, "design_extrude", "Extrude a sketch into a sheet body (no end caps)"},
{"surf_revolve", "Surface Revolve", 1, nullptr, "fly:surface#1", "Create a sketch profile to revolve first", 0x00004000u, 0, 0, false, false, nullptr, "design_revolve", "Revolve a sketch profile into a sheet body"},
{"surf_loft", "Surface Loft", 1, nullptr, "fly:surface#2", "Create at least two profile sketches to loft", 0x00004000u, 0, 2, false, false, nullptr, "design_loft", "Loft (skin) between 2+ profiles, open (no end caps)"},
{"surf_fill", "Surface Fill", 1, nullptr, "fly:surface#3", "Create a closed sketch first", 0x00004000u, 0, 0, false, false, nullptr, "design_surface", "Fill a sketch boundary with a smooth face"},
{"thicken_surf", "Thicken Surface", 1, nullptr, "fly:surface#5", "target is not a sheet body", 0x00000100u, 0, 0, true, false, nullptr, "design_thicken", "Thicken a sheet body into a solid"},
{"hole", "Hole", 2, "Shift+H", "key:S+H", "Pick a face or a plane to drill into", 0x00000402u, 1, 0, false, false, nullptr, "design_hole", "Drill a hole, centred on a picked face or placed on a plane"},
{"thread", "Thread", 2, "Shift+T", "key:S+T", "Pick a cylindrical surface (bore / outer) or a circular edge for a thread", 0x00000024u, 1, 0, false, false, nullptr, "design_thread", "Thread a cylindrical surface (inner bore / outer) or a circular edge"},
{"shell", "Shell", 2, "Shift+K", "key:S+K", "Shell needs a solid body", 0x00000082u, 1, 0, false, false, nullptr, "design_shell", "Hollow the body to a wall thickness, opening a picked face"},
{"cut", "Cut", 2, "Shift+X", "key:S+X", "Create a solid body to cut first", 0x000004feu, 1, 0, false, false, nullptr, "design_cut", "Trim the body with a plane — drag the offset arrow; keep one half or both"},
{"split", "Split", 2, nullptr, nullptr, "Split needs a solid body", 0x000000feu, 1, 0, false, false, nullptr, nullptr, "Split the body along a picked face into two solids"},
{"fillet", "Fillet", 3, "Shift+F", "btn:dress#0", "Pick an edge to round", 0x000000b2u, 1, 0, false, false, nullptr, "design_filletedge", "Pick an edge, then drag the radius arrow or type it"},
{"chamfer", "Chamfer", 3, nullptr, "btn:dress#1", "Pick an edge to bevel", 0x000000b2u, 1, 0, false, false, nullptr, "design_chamfer", "Pick an edge, then drag the distance arrow or type it"},
{"draft", "Draft", 3, "Shift+D", "key:S+D", "Pick a face to taper", 0x0000000au, 1, 0, false, false, nullptr, "design_draft", "Tilt a picked face by a draft angle"},
{"surf_offset", "Surface Offset", 3, nullptr, "fly:surface#4", "target is not a sheet body", 0x00000100u, 0, 0, true, false, nullptr, "design_offset", "Offset a sheet body's shell by a signed distance"},
{"pattern", "Linear pattern", 4, "Shift+N", "btn:pat#0", "Create a solid body to pattern first", 0x00006082u, 1, 0, false, false, nullptr, "design_array", "Repeat the body along a direction — drag the spacing, set the count"},
{"pattern_circular", "Circular pattern", 4, nullptr, "btn:pat#1", "Create a solid body to pattern first", 0x00006082u, 1, 0, false, false, nullptr, "design_polararray", "Repeat the body around an axis — set the count and sweep"},
{"mirror", "Mirror", 4, "Shift+Z", "key:S+Z", "Mirror needs a body — add or import one first", 0x000004feu, 1, 0, false, false, nullptr, "design_mirror", "Reflect a body about a plane"},
{"pat_curve", "Pattern on Curve", 4, nullptr, nullptr, "Pattern on curve needs a body and a curve", 0x00000090u, 1, 0, false, false, nullptr, nullptr, "Repeat the body along a picked curve"},
{"transform", "Move", 5, "Shift+Y", "key:S+Y", "Transform needs a body — add or import one first", 0x000021feu, 1, 0, false, false, nullptr, "design_move", "Move and/or rotate an existing body"},
{"mate", "Mate", 5, nullptr, "fly:placement#2", "A mate needs two coordinate systems", 0x00001202u, 2, 0, false, false, nullptr, "design_c_coincident", "Assembly: align two CoordSys features (fastened, planar, revolute, slider, cylindrical)"},
{"align", "Align to", 5, nullptr, nullptr, "Align needs a body", 0x00000002u, 1, 0, false, false, nullptr, nullptr, "Align the body to a picked face or plane"},
{"plane", "Plane", 6, "Shift+P", "key:S+P", nullptr, 0x00000453u, 0, 0, false, false, nullptr, "design_plane", "Reference plane (offset / tilt / midplane / tangent / two edges / coincident)"},
{"axis", "Axis", 6, "Shift+A", "key:S+A", nullptr, 0x00000057u, 0, 0, false, false, nullptr, "design_line", "Datum axis (two points, face normal, cylinder centerline, two planes, along edge)"},
{"coordsys_v", "Coord Sys", 6, "Shift+C", "key:S+C", nullptr, 0x00000043u, 0, 0, false, false, nullptr, "design_point", "Datum coordinate system (world point, or face + direction edge)"},
{"helix", "Helix", 6, nullptr, "fly:plane#3", nullptr, 0x00000405u, 0, 0, false, false, nullptr, "design_thread", "Helical curve (spring path) — use as a sweep path for coils / springs / augers"},
{"project", "Project", 6, nullptr, "fly:plane#4", "Project needs a body — add or import one first", 0x00000482u, 1, 0, false, false, nullptr, "design_sketch", "Project body edges onto a plane as sketch entities"},
{"measure", "Measure", 6, nullptr, nullptr, nullptr, 0x000b03feu, 0, 0, false, false, nullptr, nullptr, "Measure between the picked points, edges or faces"},
{"mass_props", "Mass", 6, nullptr, "btn:mass", nullptr, 0x000000feu, 1, 0, false, false, nullptr, "info", "Report the volume and surface area of the selected body"},
{"interference", "Interference", 6, nullptr, nullptr, nullptr, 0x00000200u, 2, 0, false, false, nullptr, nullptr, "Check whether two bodies overlap — reports, changes nothing"},
{"edit_feature", "Edit", 7, nullptr, "btn:edit", nullptr, 0x00007d8eu, 0, 0, false, false, nullptr, "design_edit", "Reopen the selected feature to change what it was made from"},
{"rename", "Rename…", 7, "F2", "btn:rename", "Select a feature, or a body, to rename it", 0x00004080u, 0, 0, false, false, nullptr, nullptr, "Give this feature a name you will recognise in the tree (a body takes its name from the feature that makes it)"},
{"delete_face", "Delete Face", 7, nullptr, "fly:dressup#3", "Delete Face needs a body — add or import one first", 0x0000000eu, 1, 0, false, false, nullptr, "design_delete", "Remove faces from a body and heal the solid"},
{"colour", "Colour", 7, nullptr, "btn:colour", nullptr, 0x000001feu, 1, 0, false, false, nullptr, "color_palette", "Set the selected body's display colour"},
{"delete", "Delete", 7, "Del", "btn:delete", nullptr, 0x000f7c00u, 0, 0, false, false, nullptr, "design_delete", "Delete what is selected"},
{"delete_body", "Delete Body", 7, nullptr, "btn:delete_body", nullptr, 0x000001feu, 1, 0, false, false, nullptr, "design_delete", "Delete this whole body — removes the feature it was made from"},
{"sk_line_t", "Line", 0, "L", "key:L", nullptr, 0x000f8000u, 0, 0, false, true, "Line", "design_line", "Line — click start, then end"},
{"sk_polyline", "Polyline", 0, nullptr, "fly:design_line#1", nullptr, 0x000f8000u, 0, 0, false, true, "Line", "design_polyline", "Click points; click the first point to close the loop, right-click to end it open"},
{"sk_rect", "Corner rectangle", 0, "R", "key:R", nullptr, 0x000f8000u, 0, 0, false, true, "Rectangle", "design_rect", "Rectangle — click two opposite corners"},
{"sk_rect_center", "Centre rectangle", 0, nullptr, "fly:design_rect#1", nullptr, 0x000f8000u, 0, 0, false, true, "Rectangle", "design_crect", "Click center, then a corner"},
{"sk_rect_oblique", "Oblique rectangle", 0, nullptr, "fly:design_rect#2", nullptr, 0x000f8000u, 0, 0, false, true, "Rectangle", "design_rect_oblique", "Click two corners of one edge, then a point for the width"},
{"sk_rect_rounded", "Rounded rectangle", 0, nullptr, "fly:design_rect#3", nullptr, 0x000f8000u, 0, 0, false, true, "Rectangle", "design_rect_rounded", "Click two opposite corners, then a point for the corner radius"},
{"sk_circle", "Centre circle", 0, "C", "key:C", nullptr, 0x000f8000u, 0, 0, false, true, "Circle", "design_circle", "Circle — click center, then radius"},
{"sk_circle_2pt", "2-point circle", 0, nullptr, "fly:design_circle#1", nullptr, 0x000f8000u, 0, 0, false, true, "Circle", "design_circle2pt", "Click two ends of the diameter"},
{"sk_circle_3pt", "3-point circle", 0, nullptr, "fly:design_circle#2", nullptr, 0x000f8000u, 0, 0, false, true, "Circle", "design_circle3pt", "Click three points on the circle"},
{"sk_arc_t", "3-point arc", 0, "A", "key:A", nullptr, 0x000f8000u, 0, 0, false, true, "Arc", "design_arc3pt", "Arc — click start, end, then a point"},
{"sk_arc_tangent", "Tangent arc", 0, nullptr, "fly:design_arc3pt#1", nullptr, 0x000f8000u, 0, 0, false, true, "Arc", "design_tangentarc", "Click start (on the last entity) then end"},
{"sk_arc_center", "Centre-point arc", 0, nullptr, "fly:design_arc3pt#2", nullptr, 0x000f8000u, 0, 0, false, true, "Arc", "design_arc_center", "Click center, then start, then a point for the end angle"},
{"sk_slot", "Slot", 0, "S", "key:S", nullptr, 0x000f8000u, 0, 0, false, true, "Slot", "design_slot", "Slot — two centerline ends, then end radius"},
{"sk_slot_arc", "Arc slot", 0, nullptr, "fly:design_slot#1", nullptr, 0x000f8000u, 0, 0, false, true, "Slot", "design_slot_arc", "Click center, start, end, then a point for the width"},
{"sk_ellipse", "Ellipse", 0, "E", "key:E", nullptr, 0x000f8000u, 0, 0, false, true, "Ellipse", "design_ellipse", "Ellipse — center, major end, minor point"},
{"sk_ellipse_arc", "Elliptical arc", 0, nullptr, "fly:design_ellipse#1", nullptr, 0x000f8000u, 0, 0, false, true, "Ellipse", "design_ellipse_arc", "Click center, major-axis end, minor point, then arc start and end"},
{"sk_spline", "Spline", 0, "B", "key:B", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_bspline", "Spline — click control points"},
{"sk_poly_3", "Triangle", 0, nullptr, "btn:poly#3", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Triangle — click centre, then a vertex"},
{"sk_poly_4", "Square", 0, nullptr, "btn:poly#4", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Square — click centre, then a vertex"},
{"sk_poly_5", "Pentagon", 0, nullptr, "btn:poly#5", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Pentagon — click centre, then a vertex"},
{"sk_polygon", "Hexagon", 0, "G", "btn:poly#6", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Hexagon — click centre, then a vertex"},
{"sk_poly_8", "Octagon", 0, nullptr, "btn:poly#8", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Octagon — click centre, then a vertex"},
{"sk_poly_12", "Dodecagon", 0, nullptr, "btn:poly#12", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Dodecagon — click centre, then a vertex"},
{"sk_poly_inscribed", "Inscribed", 0, nullptr, "btn:polyfit#0", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Measure the polygon to its corners (inscribed)"},
{"sk_poly_circumscribed", "Circumscribed", 0, nullptr, "btn:polyfit#1", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Measure the polygon to its flats (circumscribed)"},
{"sk_point_t", "Point", 0, "P", "key:P", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_point", "Point — click to place"},
{"sk_text", "Text", 0, nullptr, "btn:text", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_text", "Type text; its outline is added to this sketch as editable lines"},
{"sk_svg", "SVG", 0, nullptr, "btn:svg", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_svg", "Import an SVG outline into this sketch as editable lines"},
{"sk_offset", "Offset", 1, "O", "key:O", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_offset", "Offset — pick an entity, drag the distance"},
{"sk_trim", "Trim", 2, "T", "key:T", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_trim", "Trim — click a segment to trim it"},
{"sk_fillet", "Fillet", 3, "F", "key:F", nullptr, 0x00090000u, 0, 0, false, true, nullptr, "design_filletedge", "Fillet — pick two lines, set the radius"},
{"sk_chamfer", "Chamfer", 3, "H", "key:H", nullptr, 0x00090000u, 0, 0, false, true, nullptr, "design_chamfer", "Chamfer — pick two lines, set the distance"},
{"sk_array", "Linear array", 4, nullptr, "fly:design_array#0", nullptr, 0x000b0000u, 0, 0, false, true, "Array", "design_array", "Pick entities, drag the spacing handle, click the count; click empty to apply"},
{"sk_array_polar", "Polar array", 4, nullptr, "fly:design_array#1", nullptr, 0x000b0000u, 0, 0, false, true, "Array", "design_polararray", "Pick entities, drag the sweep handle, click the count; click empty to apply"},
{"sk_mirror", "Mirror", 4, "M", "key:M", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_mirror", "Mirror — pick axis, then entities"},
{"sk_move", "Move", 5, nullptr, "fly:design_move#0", nullptr, 0x000f0000u, 0, 0, false, true, "Move", "design_move", "Pick entities, then drag the handle or click the distance; click empty to apply"},
{"sk_rotate", "Rotate", 5, nullptr, "fly:design_move#1", nullptr, 0x000f0000u, 0, 0, false, true, "Move", "design_rotate", "Pick entities, then drag around the pivot or click the angle; click empty to apply"},
{"sk_scale", "Scale", 5, nullptr, "fly:design_move#2", nullptr, 0x000f0000u, 0, 0, false, true, "Move", "design_scale", "Pick entities, then drag the handle or click the factor; click empty to apply"},
{"sk_dimension", "Dimension", 6, "D", "key:D", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_dimension", "Dimension — click 2 points or an entity"},
{"sk_constrain", "Constrain", 6, "K", "key:K", nullptr, 0x000f0000u, 0, 0, false, true, nullptr, "design_constrain", "Constrain the selected sketch entities to each other"},
// Same verb, model-mode vocabulary: offered when a SKETCH is selected (bit 14, SkLoop), the
// state a user is in right after finishing one. Without this row the only way in was the
// toolbar icon, and constraints read as absent — see the Onshape-comparison report.
{"constrain", "Constrain sketch", 7, nullptr, "btn:constrain", "Select a sketch to constrain it", 0x00004000u, 0, 1, false, false, nullptr, "design_constrain", "Add dimensions and relations (coincident, tangent, parallel...) to the selected sketch"},
{"sk_construct", "Construction", 6, "Q", "key:Q", nullptr, 0x000b8000u, 0, 0, false, true, nullptr, nullptr, "Toggle construction: geometry that guides but is never built"},
{"sk_extend", "Extend", 7, "X", "key:X", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_extend", "Extend — click a line/arc to extend it"},
{"sk_delete", "Delete", 7, "Del", "btn:sk_delete", nullptr, 0x000f0000u, 0, 0, false, true, nullptr, "design_delete", "Delete the selected sketch entities"},
// Typing the defining number of the element you pointed at. Three rows rather than one so
// each names the quantity in the drawing-office word for THAT element; all three land on
// the same handler, because dimension_kind() already resolves the quantity from the
// selection. Without these, an element's own numbers were reachable only by arming the
// Dimension tool and re-picking geometry that was already selected.
{"sk_length", "Length…", 7, "V", "key:V", nullptr, 0x00010000u, 0, 0, false, true, nullptr, "design_dimension", "Type the length of this line"},
{"sk_radius", "Radius / diameter…", 7, "V", "key:V", nullptr, 0x00020000u, 0, 0, false, true, nullptr, "design_dimension", "Type the radius of this arc, or the diameter of this circle"},
{"sk_angdist", "Angle / distance…", 7, "V", "key:V", nullptr, 0x00080000u, 0, 0, false, true, nullptr, "design_dimension", "Type the angle between two lines, or the distance between the two picks"},
};
static const int kOfferVerbCount = 92;
}} // namespace Slic3r::GUI
#endif // slic3r_GUI_DesignOffer_hpp_
File diff suppressed because it is too large Load Diff
+919
View File
@@ -0,0 +1,919 @@
#ifndef slic3r_DesignPanel_hpp_
#define slic3r_DesignPanel_hpp_
#include <wx/panel.h>
#include <wx/scrolwin.h>
#include <wx/treebase.h> // wxTreeItemId
#include <vector>
#include <memory>
#include <functional>
#include <map>
#include "libslic3r/CAD/CadDocument.hpp"
#include "slic3r/GUI/CAD/DesignInteraction.hpp" // CadLevel: what one Esc press means
class ComboBox; // Orca dropdown (Widgets/ComboBox.hpp) — replaces wxChoice everywhere here
class StaticBox; // Orca rounded card frame (Widgets/StaticBox.hpp)
class wxCheckBox;
class wxCheckListBox;
class wxSpinCtrl;
class wxSpinCtrlDouble;
class wxTreeCtrl;
class wxImageList;
class wxStaticText;
class wxStaticLine;
class Button; // Orca-styled button (Widgets/Button.hpp)
class CheckBox; // Orca teal checkbox (Widgets/CheckBox.hpp)
class wxSizer;
// wxBoxSizer, wxTextCtrl and wxListCtrl are used here as pointers only, so a forward
// declaration is enough — but they must be declared. Every ordinary build happened to pull
// them in transitively through the wx/panel.h + wx/scrolwin.h chain. The Snapmaker fork's
// Flatpak build does not, and it failed to compile this header with "'wxTextCtrl' does not
// name a type; did you mean 'wxTreeCtrl'?". Declaring them keeps the header self-contained
// instead of relying on whatever a particular wx configuration happens to include.
class wxBoxSizer;
class wxTextCtrl;
class wxListCtrl;
class wxButton;
class wxPanel;
class ScalableButton;
namespace Slic3r { namespace GUI {
class DesignCanvas;
// Design (CAD) tab: a sketch-first, Onshape-style form-driven CAD panel.
// Sketch and Extrude are independent tools: the user creates a Sketch first,
// then selects it and Extrudes to produce a solid.
class DesignPanel : public wxPanel
{
public:
explicit DesignPanel(wxWindow* parent);
void on_tab_shown(); // re-sync bed to the active printer when the Design tab is activated
void on_tab_hidden(); // another tab took over: take the viewport status line down with us
void unbind_canvas_event_handlers(); // app close / language switch, from the plater's teardown
void reset_canvas_volumes();
void clear_document(); // New Project / Open Project: drop the document with the project
// Rebuild off the UI thread (progress dialog only if it turns out to be slow), so a feature
// op on a heavy imported solid does not freeze the window. Returns m_doc.recompute()'s result.
// Push the document's recipe into the Model so ANY save path persists it (vjk5).
void sync_recipe_to_model();
bool recompute_guarded(const wxString& message);
// MCP control hooks: let the external control server (McpControl.cpp) drive and
// perceive the SAME kernel the GUI uses. Called only on the wx main thread.
CadDocument& mcp_doc() { return m_doc; } // live document (read + mutate)
void mcp_after_change() { after_tree_edit(true); } // refresh tree + viewport + status
DesignCanvas* mcp_viewport() { return m_viewport; } // live sketch + 3D view
// Put the PANEL into (or out of) sketch mode, not just the canvas tool. Measured on the
// rig: a sketch started straight through DesignCanvas::begin_sketch leaves m_ui_mode at
// Feature, and the keyboard map is dispatched on `m_ui_mode == UiMode::Sketch` while the
// offer menu is dispatched on the looser sketch_map_applies() — so the menu offered the
// line's verbs while every sketch shortcut was dead (KEYTRACE: key=81 ui_mode=0
// is_sketching=1). Half-entering a mode is worse than not entering it.
void mcp_set_sketch_mode(bool on)
{
set_ui_mode(on ? UiMode::Sketch : UiMode::Feature);
update_action_bar();
}
// The offer-table vocabulary without a right-click: the external controller asks which verbs
// exist (and which apply to the current selection) and fires one by id, so a deck key names a
// verb instead of spending a letter and every verb is reachable — including the rows with no
// keyboard shortcut, which are otherwise invisible to anything that parses key tables.
int mcp_offer_selection_kind() const { return offer_selection_kind(); } // OfferSel as int
void mcp_run_action(const char* action) { run_offer_action(action); } // dispatch an action string
// Defined out of line in DesignPanel.cpp: it needs kOfferVerbs, which this header deliberately
// does not include (the table is generated and belongs to the offer-menu code).
bool mcp_run_verb(const char* verb_id);
private:
enum class Tool { None, Sketch, Extrude, Dressup, Hole, Thread, Shell, Revolve, Sweep, Pattern, Plane, Loft, Draft, Boolean, Cut, Insert, Axis, CoordSys, SurfaceExtrude, SurfaceRevolve, SurfaceLoft, SurfaceFill, SurfaceOffset, ThickenSurface, Transform, Mirror, Thicken, Rib, Project, DeleteFace, Helix, Mate };
// Which numeric fields an expression can be bound to, per feature type. A member rather
// than a file-static helper so Tool — 32 values of purely internal card state — does not
// have to become part of this panel's public API just to be named in a signature.
static std::vector<std::string> fields_for_tool(Tool t);
// Plane tool: which datum reference the next solid pick fills (declared early so the
// method decls + card lambdas below can name it).
enum class PlanePick { None, FaceA, FaceB, EdgeA, EdgeB };
enum class AxisPick { None, Face, Edge };
enum class CoordSysPick { None, Face, Edge };
// Onshape-style contextual top toolbar: only the active mode's tool group is
// shown (Feature = sketch/extrude/dress/hole/thread; Sketch = entity tools;
// Constrain = constraints + edit ops). Replaces the old always-visible wall.
enum class UiMode { Feature, Sketch, Constrain };
void set_ui_mode(UiMode m);
void apply_dof_status(int dof, bool ok, bool has_constraints);
// Unified action-bar dispatch: one Confirm / one Cancel for every tool and mode.
void tool_confirm(); // ✓ : commit the active feature / sketch / constrain session
void tool_cancel(); // ✗ : cancel the active feature / discard / exit
// Esc. ONE press unwinds ONE level of the interaction stack (DesignInteraction.hpp), and no
// level of it destroys committed work. escape_level() answers which level the press belongs
// to; escape() acts on exactly that one. Every Esc in the tab routes through here — the key
// used to be handled in four places that could not see each other, and that is how two
// presses in a row reached past a tool and discarded the sketch under it.
CadLevel escape_level() const;
void escape();
void update_action_bar(); // show the ✓/✗ bar iff a tool or mode is active
void on_shape_changed();
void on_add_sketch();
void on_add_extrude();
void on_add_dressup();
void on_add_hole();
void on_add_thread();
void apply_thread_standard(); // fill pitch/depth/radius from m_thread_std selection
void infer_thread_spec(double diameter); // nearest M-standard from a picked cylinder diameter
void on_add_revolve();
void on_add_sweep();
void on_add_loft();
void on_add_pattern();
bool on_add_plane(); // false = refused, card stays open
void arm_plane_pick(PlanePick target); // Plane tool: next solid pick fills this reference
void apply_plane_refs(CadFeature& f) const; // copy type + face/edge refs + sizes from the card
void refresh_plane_labels(); // update the 4 pick labels from the captured refs
void reset_plane_refs(); // clear captured refs (fresh Plane add)
void on_add_shell();
void on_add_draft();
void on_add_boolean();
void on_add_cut(); // commit a plane Cut (split-by-plane)
void on_add_axis();
void arm_axis_pick(AxisPick target);
void apply_axis_refs(CadFeature& f) const;
void refresh_axis_labels();
void reset_axis_refs();
void on_add_coordsys();
void arm_coordsys_pick(CoordSysPick target);
void apply_coordsys_refs(CadFeature& f) const;
void refresh_coordsys_labels();
void refresh_cs_body_choice(); // fill the CoordSys body chooser from current document
void reset_coordsys_refs();
void on_add_surface_extrude();
void on_add_surface_revolve();
void on_add_surface_loft();
void on_add_surface_fill();
void on_add_surface_offset();
void on_add_thicken_surface();
void on_add_transform();
void xf_live_preview(); // typed Transform fields -> body display transform (live)
void xf_clear_preview(); // hand a previewed body back to its pre-card pose
void on_add_mirror();
void on_add_thicken();
void on_add_rib();
void on_add_project();
void on_add_delete_face();
void on_add_helix();
void on_add_mate();
void on_check_interference();
void on_mass_properties(); // read-only report on the selected solid; edits nothing
// Fill m_bool_target / m_bool_tool / m_cut_target. as_of_feature < 0 = current bodies (add);
// >= 0 = the bodies as they existed just before that feature index (Boolean re-edit, so a
// consumed tool body still appears and its saved selection round-trips).
// Which body a tool should act on when it opens: the one picked in the VIEWPORT, else
// the first. Selection comes first and the tool consumes it — every body combo used to
// default to index 0, so picking body 3 and opening Mirror silently mirrored body 1.
// Clamped to the list, so it is safe to hand straight to SetSelection. e1p.
int selected_body_default() const;
void populate_body_choices(int as_of_feature = -1);
// Fill `c` with the bodies as they existed just before `as_of_feature` and select
// `want`. Re-editing any feature that stores a body index needs this: the index was
// recorded against the body list at that point in the timeline, not the final one.
void fill_body_choice(ComboBox* c, int as_of_feature, int want);
void populate_sheet_body_choices(ComboBox* c) const; // bodies where is_sheet_shape() is true
// Rows of a sheet-filtered picker are not body indices; go through these two, never
// GetSelection()/SetSelection() directly.
static int sheet_choice_body(ComboBox* c); // real body index of the current row, or -1
static void select_sheet_choice(ComboBox* c, int body);// select the row holding this body index
// Import rigid 2D art (Text / SVG) as a new Sketch feature carrying
// imported_regions (no solver entities). on_add_text/on_import_svg gather
// input; add_imported_sketch builds the feature, refreshes tree + display.
void on_add_text();
void on_import_svg();
void on_import_step(); // STEP -> editable B-rep body (keeps the OCCT solid, not a mesh)
void on_import_mesh(); // STL/OBJ -> B-rep body via GeometryEngine::mesh_to_brep
bool place_on_face(); // Prepare's Place on Face (F): lay the selected body face on the bed
void add_imported_sketch(const std::vector<std::vector<std::vector<Vec2d>>>& regions,
const wxString& base_name);
// Imported Text/SVG art is placed/sized in-canvas then explicitly committed via a
// small Confirm/Cancel card (Onshape Button->Dialog->Preview->Confirm). The feature
// is added provisionally by add_imported_sketch; Confirm keeps it, Cancel undoes it.
void open_insert_card(const wxString& base_name);
void finalize_insert(); // Confirm: keep the placed art, leave the placement gizmo
void cancel_insert(); // Cancel: undo the provisional insert
// Move / enlarge / stretch (independent X/Y) an imported Text/SVG sketch:
// a modal dialog editing the feature's placement transform in place.
void on_transform_imported(int feat_idx);
void on_commit();
void on_export_step(); // write all bodies to a .step file (native B-rep)
// Rehydrate the parametric model from a project's saved recipe (3MF
// Metadata/orca_cad.bin): deserialize -> recompute -> refresh viewport + tree.
void load_recipe(const std::string& blob);
void refresh_tree();
void set_status_ok();
// Feature-tree editing (Onshape-style): act on the selected tree row.
void on_delete_feature();
// "Delete Body" — the geometry-first counterpart, reached by pointing at a body or any of
// its faces. Resolves the body to the feature that created it and removes THAT, because a
// body is a recomputed result and has nothing else to delete.
void on_delete_body();
void on_new_design();
void on_move_feature(int delta); // -1 = up, +1 = down
void on_toggle_visibility(); // show/hide the selected feature (CadFeature::enabled)
// Constrain mode: enter on the tree-selected sketch, then apply a geometric
// constraint to the in-canvas picked segment and re-solve in the kernel.
void on_begin_constrain(int sel_override = -1);
// Sketch-toolbar Constrain entry: commit the live sketch in place, then enter Constrain
// mode on it (so the constraint palette + Trim/Extend are reachable without leaving the
// sketch flow). Returns true if constrain mode was entered.
bool enter_constrain_inline();
void apply_constraint(SketchConstraintType type);
void apply_entity_constraint(SketchConstraintType type); // Fase 4.2 entity path
void apply_live_constraint(SketchConstraintType type); // Fase 4.2 live-sketch path (no commit needed)
enum class EditOp { Mirror, Offset, Fillet, Trim, Extend, Array, Move, Chamfer, Rotate, Scale, PolarArray }; // Fase 4.4/4.5/4.6 sketch edit ops
void apply_edit_op(EditOp op); // mutate selected sketch entities
// Onshape-style docked value entry (replaces wxGetTextFromUser popups for
// Angle/Radius/Diameter constraints + Offset/Fillet edit ops). request_value
// shows the card and stows a continuation run by confirm_value().
void request_value(const wxString& label, double def, double mn, double mx,
std::function<void(double)> cont,
std::function<void()> on_cancel = nullptr);
void confirm_value();
void cancel_value();
void commit_entity_constraints(const std::vector<SketchEntityConstraintDef>& defs); // multi-def (Symmetric)
// Constraint manager (C3.4): a docked list of the constrained sketch's
// entity-constraints with per-row select (highlight the referenced entities in
// the viewport) and delete (drop the constraint + re-solve). Shown in Constrain
// mode only; operates on m_doc.features[m_constrain_feat].entity_constraints.
// True when the constraint UI must address the LIVE sketch session rather than a committed
// feature. Same discriminator apply_constraint uses to choose apply_live_constraint: both
// Constrain modes set m_active too, so is_sketching() alone would claim the live scope while
// the committed manager is open.
bool live_constraint_scope() const;
void rebuild_constraint_list(); // refill m_constraint_rows
void delete_constraint(int idx); // erase + re-solve + refresh
void highlight_constraint_entities(int idx); // push referenced entities to viewport
void refresh_constrain_dof(); // re-solve feature, mirror DoF readout
wxString constraint_label(const SketchEntityConstraintDef& d) const; // human-readable row text
void after_edit_op(); // shared edit-op refresh tail
void on_edit_feature(); // reopen the selected feature's dialog populated
void after_tree_edit(bool ok); // shared post-op refresh of tree/viewport/status
void load_feature_into_dialog(const CadFeature& f);
void reset_edit_state(); // back to add-mode (m_edit_index = -1)
// Onshape loop: Button -> open_tool (show dialog) -> refresh_preview (ghost) ->
// confirm_tool (commit) / cancel_tool (abort).
void open_tool(Tool t);
void close_tool();
void refresh_preview();
void confirm_tool();
void cancel_tool();
// Ctrl+Z / Ctrl+Shift+Z (Ctrl+Y) from the viewport. With a tool/dialog open it
// cancels that (Esc-like); otherwise it undoes/redoes the committed feature history.
void do_undo_redo(bool redo);
// The plane the Hole tool drills on: a picked face (inward, centred) or the dropdown.
SketchPlane hole_plane() const;
// The plane the Thread tool builds on: a picked cylindrical face (axis) or the dropdown.
SketchPlane thread_plane() const;
// Name the geometry the card has LATCHED, so it never has to be inferred from the viewport.
// Pass -1 for "none, falling back to the plane dropdown". See 200.
void set_hole_target_label(int face);
void set_thread_target_label(int face, int edge);
CadFeature build_candidate(Tool t) const;
// Merge per-body ghost meshes with the per-body display transforms applied. The kernel builds
// a ghost from the untransformed bodies, so without this it floats back at the origin once a
// body has been moved.
TriangleMesh ghost_from_bodies(const std::vector<TriangleMesh>& per_body) const;
// A mate makes no new geometry but it MOVES a body, and the moved assembly is the ghost worth
// showing. Used both by the Mate card and by hovering a row of the offer's mate palette.
bool show_mate_ghost(int kind, int cs_a, int cs_b,
double offset, double angle_deg, bool flip, std::string& err);
int resolve_extrude_sketch() const;
// Plane pickers: fill a choice with XY/XZ/YZ + the document's datum planes, and
// map a choice row back to the actual SketchPlane (rows 0-2 base, 3+ datum).
void populate_plane_choices(ComboBox* c) const;
wxString ref_plane_name(int row) const; // "XY" / a datum's name, for the on-geometry hint
SketchPlane plane_from_choice(int row) const;
// Where a new sketch goes, resolved from what is SELECTED IN THE VIEWPORT rather than from a
// list: a picked planar face wins, otherwise the reference plane last clicked in 3D. `what`
// comes back as something to show the user, so the choice is visible without a combo.
SketchPlane sketch_plane_from_selection(wxString& what) const;
// Whether that resolution has anything the USER picked behind it, rather than the default
// reference plane. Lets a caller say "sketching on XZ" only when it is actually true.
bool sketch_plane_target(wxString& what) const;
// True when Extrude should build only the click-selected loop (a region of the
// resolved sketch is selected and it carries entities).
bool extrude_uses_loop() const;
void sync_sketch_display(); // push un-consumed committed sketches to the viewport
// Feed the viewport's visual Extrude depth-arrow gizmo (C5b) with the current profile
// plane + centroid + live depths while the Extrude card is open (self-gates on m_active).
void update_extrude_gizmo();
void update_fillet_gizmo(); // edge-anchored radius arrow (Dressup card)
void sync_dressup_target(); // Dressup card: show picked edge vs group, gate the combo
void update_hole_gizmo(); // footprint circle + diameter/depth arrows (Hole card)
// A FEATURE button whose tool needs bodies it may not have yet. Greyed with an explanatory
// tooltip below min_bodies, rather than accepting the click and refusing afterwards.
struct BodyGate { wxWindow* btn{nullptr}; int min_bodies{1}; wxString tip_live, tip_gated; };
std::vector<BodyGate> m_body_gates;
void update_body_gates(); // re-evaluate them against the current body count
void update_thread_gizmo(); // footprint circle + radius/length arrows (Thread card)
void update_shell_gizmo(); // inward thickness arrow on the picked face (Shell card)
void update_revolve_gizmo(); // angle-arc around the axis (Revolve card)
void update_draft_gizmo(); // angle-arc around the face centroid (Draft card)
void update_cut_gizmo(); // plane-rectangle + offset arrow (Cut card)
void update_operand_highlight(); // Boolean/Sweep/Loft operand tinting on the canvas
void update_pattern_gizmo(); // linear spacing arrow / circular angle-arc (Pattern card)
void update_datum_gizmo(); // resize handles on the datum plane being created/edited (C3)
void update_helix_gizmo(); // live helix curve + radius/height/pitch handles (Helix card)
void update_rib_gizmo(); // in-plane slab footprint + thickness handles (Rib card)
void refresh_datum_planes(); // push resolved datum frames + per-plane u/v extents to viewport
void refresh_mate_connectors(); // push connector frames so verse + polarity are visible
void update_reference_planes(); // persistent XY/XZ/YZ reference planes (fallback when no object)
CadDocument m_doc;
Tool m_active{Tool::None};
// Keyboard shortcuts (Onshape-style, three scoped layers). Keys are encoded as the
// upper-cased letter, OR'd with 0x10000 when Shift is required. m_keys_sketch fires only
// while a sketch is open (single letters = sketch tools); m_keys_feature fires only when
// no sketch is open (Shift+letter = feature tools; single letters = view toggles/section).
static constexpr int SC_SHIFT = 0x10000;
// ...and with 0x20000 when Ctrl is required too. The Shift+letter space is full, so an
// action that arrives late lives on Ctrl+Shift; plain Ctrl-combos are still passed
// straight through, which is what leaves this layer free.
static constexpr int SC_CTRL = 0x20000;
std::map<int, std::function<void()>> m_keys_sketch;
std::map<int, std::function<void()>> m_keys_feature;
StaticBox* m_tree_box{nullptr}; // framed feature-tree section
StaticBox* m_parts_box{nullptr}; // framed bodies section (hidden while empty)
StaticBox* m_cards{nullptr}; // one framed panel holding every tool dialog (one visible at a time)
void update_cards_frame(); // show that frame iff some card inside it is visible
void show_move_card(bool show);
void apply_move_card(); // numeric move/rotate -> same xform the gizmo builds
void push_polygon_params();
wxSizer* m_tb_commit{nullptr}; // far-right Commit to Plate, beside Confirm/Cancel
wxSizer* m_tb_doc{nullptr}; // toolbar document/view actions (new, commit, export, section, place)
CheckBox* m_show_bed{nullptr}; // view option: draw the printer bed + plate grid, or not
wxSizer* m_box_move{nullptr}; // Move/Rotate numeric options (distance, axis, angle)
wxSizer* m_box_sketch{nullptr};
wxSizer* m_box_extrude{nullptr};
wxSizer* m_box_dressup{nullptr};
wxSizer* m_box_hole{nullptr};
wxSizer* m_box_thread{nullptr};
wxSizer* m_box_shell{nullptr};
wxSizer* m_box_revolve{nullptr};
wxSizer* m_box_sweep{nullptr};
wxSizer* m_box_pattern{nullptr};
wxSizer* m_box_plane{nullptr};
wxSizer* m_box_loft{nullptr};
wxSizer* m_box_draft{nullptr};
wxSizer* m_box_boolean{nullptr};
wxSizer* m_box_cut{nullptr};
wxSizer* m_box_axis{nullptr};
wxSizer* m_box_coordsys{nullptr};
wxSizer* m_box_surf_extrude{nullptr};
wxSizer* m_box_surf_revolve{nullptr};
wxSizer* m_box_surf_loft{nullptr};
wxSizer* m_box_surf_fill{nullptr};
wxSizer* m_box_surf_offset{nullptr};
wxSizer* m_box_surf_thicken{nullptr};
wxSizer* m_box_transform{nullptr};
wxSizer* m_box_mirror{nullptr};
wxSizer* m_box_thicken{nullptr};
wxSizer* m_box_rib{nullptr};
wxSizer* m_box_project{nullptr};
wxSizer* m_box_delete_face{nullptr};
wxSizer* m_box_helix{nullptr};
wxSizer* m_box_mate{nullptr};
wxSizer* m_box_insert{nullptr}; // Confirm/Cancel card for placing Text/SVG art
wxSizer* m_box_expr{nullptr}; // expression binding card (visible during edit only)
int m_insert_feat{-1}; // provisional imported-art feature awaiting Confirm
// Move-body gizmo runs through the unified action bar too: Confirm keeps the placement,
// Cancel reverts to the pose captured when the move started.
int m_move_body{-1};
Transform3d m_move_prev{Transform3d::Identity()};
// Set while the move gizmo is serving the Transform CARD rather than the Move button.
// Both use the same gizmo; only this says which card owns the numbers it reports.
int m_xf_gizmo_body{-1};
Transform3d m_xf_gizmo_base{Transform3d::Identity()}; // pose when Transform armed it
// Which body the Transform card's typed fields are currently previewing on, and the pose to
// hand it back to. Separate from the gizmo pair because the card can retarget its Body combo.
int m_xf_prev_body{-1};
Transform3d m_xf_prev_base{Transform3d::Identity()};
// Onshape-style dialog-card title rows (icon + bold feature name), retitled
// per tool in open_tool() (edit-mode shows the feature's actual name).
wxStaticText* m_hdr_move{nullptr};
wxStaticText* m_hdr_sketch{nullptr};
// Onshape sketch-entry card (plane/orientation) that opens on "New sketch" and
// persists until Finish (Phase 3).
wxSizer* m_box_sketch_session{nullptr};
wxStaticText* m_hdr_sketch_session{nullptr};
wxStaticText* m_sketch_hint{nullptr}; // "click a plane" / "drawing on X" — must match the status
wxStaticText* m_hdr_extrude{nullptr};
wxStaticText* m_hdr_dressup{nullptr};
wxStaticText* m_hdr_hole{nullptr};
wxStaticText* m_hdr_thread{nullptr};
wxStaticText* m_hdr_shell{nullptr};
wxStaticText* m_hdr_revolve{nullptr};
wxStaticText* m_hdr_sweep{nullptr};
wxStaticText* m_hdr_pattern{nullptr};
wxStaticText* m_hdr_plane{nullptr};
wxStaticText* m_hdr_loft{nullptr};
wxStaticText* m_hdr_draft{nullptr};
wxStaticText* m_hdr_boolean{nullptr};
wxStaticText* m_hdr_cut{nullptr};
wxStaticText* m_hdr_axis{nullptr};
wxStaticText* m_hdr_coordsys{nullptr};
wxStaticText* m_hdr_surf_extrude{nullptr};
wxStaticText* m_hdr_surf_revolve{nullptr};
wxStaticText* m_hdr_surf_loft{nullptr};
wxStaticText* m_hdr_surf_fill{nullptr};
wxStaticText* m_hdr_surf_offset{nullptr};
wxStaticText* m_hdr_surf_thicken{nullptr};
wxStaticText* m_hdr_transform{nullptr};
wxStaticText* m_hdr_mirror{nullptr};
wxStaticText* m_hdr_thicken{nullptr};
wxStaticText* m_hdr_rib{nullptr};
wxStaticText* m_hdr_project{nullptr};
wxStaticText* m_hdr_delete_face{nullptr};
wxStaticText* m_hdr_helix{nullptr};
wxStaticText* m_hdr_mate{nullptr};
wxStaticText* m_hdr_insert{nullptr};
wxScrolledWindow* m_form{nullptr};
DesignCanvas* m_viewport{nullptr};
// Top contextual toolbar (parented to the panel, above the form/viewport row).
UiMode m_ui_mode{UiMode::Feature};
// Sketch environment banner: a strip across the top of the viewport saying, in words, that
// this is a sketch and which one. The mode used to be legible only from the toolbar and the
// left card — both of which look like the rest of the app — so a sketch session and plate
// preparation were one glance apart. Indicator only: Finish/Cancel stay on the ONE ribbon
// action bar (the Design UX contract), and the banner never grows a second pair.
wxPanel* m_sketch_banner{nullptr};
wxStaticText* m_sketch_banner_txt{nullptr};
wxScrolledWindow* m_toolbar{nullptr}; // horizontally scrollable so the action bar stays reachable on narrow windows
wxSizer* m_tb_feature{nullptr};
wxSizer* m_tb_sketch{nullptr};
// The 20 constraint icon buttons, shown during BOTH Sketch and Constrain (Fase 4.2 live
// path: a constraint must be applicable while drawing, not only after committing).
wxSizer* m_tb_relations{nullptr};
// Unified Confirm/Cancel action bar (right end of the ribbon). Shown whenever any
// tool or mode is active; the single confirm/cancel surface for the whole tab.
wxSizer* m_tb_action{nullptr};
// Persistent Undo/Redo group at the left of the ribbon — always visible, independent
// of the mode-gated tool groups. The buttons are greyed per the document history and
// the do_undo_redo gate (see update_undo_redo_buttons).
wxSizer* m_tb_history{nullptr};
ScalableButton* m_btn_undo{nullptr};
ScalableButton* m_btn_redo{nullptr};
void update_undo_redo_buttons(); // enable/disable Undo/Redo from can_undo/can_redo + gate
// All tool buttons, for the active-tool teal highlight (Onshape-style).
std::vector<ScalableButton*> m_tool_btns;
ScalableButton* m_active_tool_btn{nullptr};
void set_active_tool_btn(ScalableButton* b); // nullptr clears the highlight
// Owns the themed DropDown flyouts (and the item vectors they hold by ref).
std::vector<std::shared_ptr<void>> m_flyout_keepalive;
wxCheckBox* m_construction{nullptr}; // sketch-mode construction toggle
wxSpinCtrlDouble* m_move_dx{nullptr}; // Move/Rotate card: world translation
wxSpinCtrlDouble* m_move_dy{nullptr};
wxSpinCtrlDouble* m_move_dz{nullptr};
ComboBox* m_move_axis{nullptr}; // rotation axis: X/Y/Z
wxSpinCtrlDouble* m_move_angle{nullptr}; // rotation angle (deg)
// Polygon's two parameters are chosen FROM THE TOOL, in the offer's Polygon submenu, not
// from a card on the left: the side count cannot be edited after drawing (the inline editor
// offers Side and Angle only), so it has to be settled at the moment the tool is armed —
// which is exactly where the offer already is. e1p.
int m_poly_sides{6}; // 3..64; the submenu names the common ones
bool m_poly_circumscribed{false};
// Which reference plane a sketch falls back to when no face is picked: 0/1/2 = XY/XZ/YZ,
// >=3 indexes resolve_datum_planes(). Set by CLICKING a ghost plane in the viewport — there is
// deliberately no dropdown for it. e1p.
int m_ref_plane{0};
// m_ref_plane is always a VALID plane, so it cannot itself distinguish "the user chose XY"
// from "nobody has chosen anything yet". This does.
bool m_plane_picked{false};
ComboBox* m_shape{nullptr};
ComboBox* m_mode{nullptr};
wxSpinCtrlDouble* m_width{nullptr};
wxSpinCtrlDouble* m_height{nullptr};
wxSpinCtrlDouble* m_radius{nullptr};
wxSpinCtrlDouble* m_distance{nullptr};
ComboBox* m_extrude_end{nullptr}; // Blind/Symmetric/TwoSided/ThroughAll/UpTo*
wxSpinCtrlDouble* m_distance2{nullptr}; // second-side depth (Two-sided)
wxSpinCtrlDouble* m_taper{nullptr}; // draft angle (deg)
CheckBox* m_flip{nullptr}; // reverse extrude direction
wxStaticText* m_extrude_sketch_label{nullptr};
int m_extrude_sketch_ref{-1};
// Revolve controls (sweep a sketch profile about an in-plane axis).
wxStaticText* m_revolve_sketch_label{nullptr};
wxSpinCtrlDouble* m_revolve_angle{nullptr};
ComboBox* m_revolve_axis{nullptr}; // 0 = plane X, 1 = plane Y
ComboBox* m_revolve_mode{nullptr}; // New/Add/Cut/Intersect
CheckBox* m_revolve_flip{nullptr};
int m_revolve_sketch_ref{-1};
// Sweep controls (sweep a profile sketch along a path sketch).
wxStaticText* m_sweep_profile_label{nullptr};
ComboBox* m_sweep_path{nullptr}; // path Sketch picker (feature index in client data)
ComboBox* m_sweep_mode{nullptr}; // New/Add/Cut/Intersect
int m_sweep_profile_ref{-1};
int m_sweep_path_ref{-1}; // path Sketch feature index (for re-edit pre-select)
// Loft controls (skin a solid through 2+ ordered profile Sketches).
wxCheckListBox* m_loft_list{nullptr}; // every Sketch; check 2+ in list order = profiles
CheckBox* m_loft_ruled{nullptr}; // ruled (straight) vs smooth sections
ComboBox* m_loft_mode{nullptr}; // New/Add/Cut/Intersect
std::vector<int> m_loft_sketch_idx; // feature index for each row in m_loft_list
std::vector<int> m_loft_refs; // chosen profile refs (for re-edit pre-check)
// Surface Extrude controls (sheet from sketch profile).
wxStaticText* m_surf_extrude_sketch_label{nullptr};
wxSpinCtrlDouble* m_surf_extrude_distance{nullptr};
int m_surf_extrude_sketch_ref{-1};
// Surface Revolve controls (sheet from sketch about axis).
wxStaticText* m_surf_revolve_sketch_label{nullptr};
wxSpinCtrlDouble* m_surf_revolve_angle{nullptr};
ComboBox* m_surf_revolve_axis{nullptr}; // 0 = plane X, 1 = plane Y
CheckBox* m_surf_revolve_flip{nullptr};
int m_surf_revolve_sketch_ref{-1};
// Surface Loft controls (skin a sheet through 2+ ordered profile sketches).
wxCheckListBox* m_surf_loft_list{nullptr}; // every Sketch; check 2+ in list order = profiles
CheckBox* m_surf_loft_ruled{nullptr}; // ruled (straight) vs smooth sections
std::vector<int> m_surf_loft_sketch_idx; // feature index for each row
std::vector<int> m_surf_loft_refs; // chosen profile refs (for re-edit pre-check)
// Surface Fill controls (one-face sheet from a sketch boundary).
wxStaticText* m_surf_fill_sketch_label{nullptr};
int m_surf_fill_sketch_ref{-1};
// Surface Offset controls (offset a SHEET body).
ComboBox* m_surf_offset_body{nullptr}; // sheet-body picker
wxSpinCtrlDouble* m_surf_offset_distance{nullptr};
// Thicken Surface controls (thicken a SHEET body into a solid).
ComboBox* m_surf_thicken_body{nullptr}; // sheet-body picker
wxSpinCtrlDouble* m_surf_thicken_thickness{nullptr};
CheckBox* m_surf_thicken_flip{nullptr};
// Transform controls (rigid move/rotate of a body).
ComboBox* m_xf_body{nullptr}; // body to transform
wxSpinCtrlDouble* m_xf_dx{nullptr}; // translate X
wxSpinCtrlDouble* m_xf_dy{nullptr}; // translate Y
wxSpinCtrlDouble* m_xf_dz{nullptr}; // translate Z
ComboBox* m_xf_axis{nullptr}; // rotation axis: X/Y/Z
wxSpinCtrlDouble* m_xf_angle{nullptr}; // rotation angle (deg)
wxSpinCtrlDouble* m_xf_pivot_x{nullptr}; // pivot X
wxSpinCtrlDouble* m_xf_pivot_y{nullptr}; // pivot Y
wxSpinCtrlDouble* m_xf_pivot_z{nullptr}; // pivot Z
CheckBox* m_xf_copy{nullptr}; // keep original (make a copy)
// Mirror controls (reflect a body about a plane).
ComboBox* m_mirror_body{nullptr}; // body to mirror
ComboBox* m_mirror_plane{nullptr}; // mirror plane (XY/XZ/YZ + datums)
CheckBox* m_mirror_keep{nullptr}; // keep original body
// Thicken controls (offset one solid face into a thin plate).
ComboBox* m_thicken_body{nullptr}; // source body
wxStaticText* m_thicken_face_label{nullptr}; // picked face
wxSpinCtrlDouble* m_thicken_thickness{nullptr};
CheckBox* m_thicken_flip{nullptr}; // flip direction
// Rib controls (thin wall from an open sketch line).
ComboBox* m_rib_body{nullptr}; // target body
ComboBox* m_rib_sketch{nullptr}; // sketch holding the open line (feature index in client data)
wxSpinCtrl* m_rib_entity{nullptr}; // entity index within the sketch
wxSpinCtrlDouble* m_rib_thickness{nullptr};
wxSpinCtrlDouble* m_rib_depth{nullptr};
// Project controls (project body edges onto a plane as sketch entities).
ComboBox* m_proj_source_body{nullptr}; // source body
wxStaticText* m_proj_face_label{nullptr}; // picked face (or "all edges")
ComboBox* m_proj_plane{nullptr}; // target plane
// Delete Face controls (remove faces, heal the solid).
ComboBox* m_del_face_body{nullptr}; // target body
wxButton* m_del_face_add_btn{nullptr}; // "Add picked face" button
wxStaticText* m_del_face_list{nullptr}; // shows the accumulated face ids
std::vector<int> m_del_faces; // accumulated face list
// Helix controls (helical curve).
ComboBox* m_helix_plane{nullptr}; // axis plane (XY/XZ/YZ + datums)
wxSpinCtrlDouble* m_helix_radius{nullptr};
wxSpinCtrlDouble* m_helix_pitch{nullptr};
wxSpinCtrlDouble* m_helix_height{nullptr};
CheckBox* m_helix_left_handed{nullptr};
wxSpinCtrlDouble* m_helix_taper{nullptr};
// Mate (assembly) controls
ComboBox* m_mate_kind{nullptr};
ComboBox* m_mate_cs_a{nullptr};
ComboBox* m_mate_cs_b{nullptr};
wxSpinCtrlDouble* m_mate_offset{nullptr};
wxSpinCtrlDouble* m_mate_angle{nullptr};
CheckBox* m_mate_flip{nullptr};
wxStaticText* m_offset_label{nullptr};
wxStaticText* m_angle_label{nullptr};
// Expression binding (per-feature, visible during edit only)
ComboBox* m_expr_field{nullptr}; // field-name picker (editable)
wxTextCtrl* m_expr_text{nullptr}; // expression string
wxButton* m_expr_set_btn{nullptr}; // Apply / bind
wxButton* m_expr_clear_btn{nullptr}; // Remove binding
wxStaticText* m_expr_status{nullptr}; // shows current bindings for the edited feature
void populate_expr_fields(Tool t); // fill m_expr_field from feature-type fields
void on_set_expr(); // checkpoint + write -> recompute -> undo on fail
void on_clear_expr(); // remove selected binding
// Document variables panel (below the feature tree / parts)
StaticBox* m_var_box{nullptr};
wxListCtrl* m_var_list{nullptr};
wxButton* m_btn_add_var{nullptr};
wxButton* m_btn_edit_var{nullptr};
wxButton* m_btn_del_var{nullptr};
void refresh_variables(); // rebuild m_var_list from m_doc.variables
void on_add_variable();
void on_edit_variable();
void on_remove_variable();
// Feature-tree button
ScalableButton* m_btn_interfere{nullptr};
// Pattern controls (replicate the target body: linear or circular).
ComboBox* m_pattern_type{nullptr}; // 0 = Linear, 1 = Circular
wxSpinCtrlDouble* m_pattern_count{nullptr}; // total instances incl. seed
wxSpinCtrlDouble* m_pattern_spacing{nullptr}; // linear step (mm)
ComboBox* m_pattern_dir{nullptr}; // linear direction: 0 = plane X, 1 = plane Y
wxSpinCtrlDouble* m_pattern_angle{nullptr}; // circular total angle (deg)
// Boolean controls (combine two existing bodies).
ComboBox* m_bool_op{nullptr}; // 0 = Union, 1 = Subtract, 2 = Intersect
ComboBox* m_bool_target{nullptr}; // body that survives (selection == body index)
ComboBox* m_bool_tool{nullptr}; // body consumed (selection == body index)
// Which operand the NEXT viewport body pick fills: 0 = target, 1 = tool. Reset when the
// card opens, so the first two clicks in the viewport always mean "keep this, cut with
// that" in that order. The combos remain the typed half and mirror whatever is picked.
int m_bool_next_slot{0};
CheckBox* m_bool_keep{nullptr}; // keep the tool body after the op
wxSpinCtrlDouble* m_bool_tol{nullptr}; // OCCT fuzzy tolerance (mm); robust cut on near-coincident faces
// Plane Cut (split-by-plane): a reference plane + offset splits the target body into
// two separate bodies (both pieces kept).
ComboBox* m_cut_plane{nullptr}; // XY/XZ/YZ + datum planes (cut plane)
ComboBox* m_cut_target{nullptr}; // body to cut (selection == body index)
wxSpinCtrlDouble* m_cut_offset{nullptr}; // offset along the plane normal (mm)
// Datum plane controls (derive a selectable sketch plane: offset + tilt from a base).
ComboBox* m_plane_base{nullptr}; // 0=XY,1=XZ,2=YZ, 3+N = Nth datum plane
wxSpinCtrlDouble* m_plane_offset{nullptr}; // offset along base normal (mm)
wxSpinCtrlDouble* m_plane_tilt{nullptr}; // tilt about a base axis (deg) / Angle / Tangent angle
ComboBox* m_plane_tilt_axis{nullptr}; // 0 = base X, 1 = base Y
// Plane construction method + contextual face/edge reference picks (Onshape/Fusion parity).
ComboBox* m_plane_type{nullptr}; // PlaneType: Offset/Angle/Midplane/Tangent/TwoEdges/Coincident
wxButton* m_plane_pick_faceA{nullptr}; wxStaticText* m_plane_faceA_lbl{nullptr};
wxButton* m_plane_pick_faceB{nullptr}; wxStaticText* m_plane_faceB_lbl{nullptr};
wxButton* m_plane_pick_edgeA{nullptr}; wxStaticText* m_plane_edgeA_lbl{nullptr};
wxButton* m_plane_pick_edgeB{nullptr}; wxStaticText* m_plane_edgeB_lbl{nullptr};
wxSpinCtrlDouble* m_plane_usize{nullptr}; // datum rectangle extent u (mm) — also driven by drag handles
wxSpinCtrlDouble* m_plane_vsize{nullptr}; // datum rectangle extent v (mm)
// Captured references for the candidate datum (body index + face/edge index, -1 = none).
int m_pl_faceA_body{-1}, m_pl_faceA{-1};
int m_pl_faceB_body{-1}, m_pl_faceB{-1};
int m_pl_edgeA_body{-1}, m_pl_edgeA{-1};
int m_pl_edgeB_body{-1}, m_pl_edgeB{-1};
PlanePick m_plane_pick{PlanePick::None}; // which ref the next solid pick fills
// Plate loop selection (click a committed sketch loop): the Sketch feature + the
// clicked closed-region index, so Extrude builds just that one loop. -1 = none.
int m_sel_sketch_feat{-1};
int m_sel_sketch_region{-1};
// Click-selected solid topology (whole/face/edge cycle): face id for up-to-face / dress-up.
int m_sel_solid_body{-1}; // which body the face/edge selection is on
int m_sel_solid_face{-1};
int m_sel_solid_edge{-1};
bool m_sel_solid_vertex{false}; // a corner is picked (body+point, no face/edge)
// The face actually under the last solid click, INDEPENDENT of the whole/face/edge cycle level.
// The first click on a solid selects the WHOLE body, but the ray has already resolved which face
// it hit and the callback passes it. "Sketch on the face I clicked" must not require discovering
// that a second click refines the selection, so keep it instead of throwing it away. 3a2.
int m_pick_face_body{-1};
int m_pick_face{-1};
// What the live sketch was actually opened on ("the picked face", "XY", a datum's name), so the
// hint can say it. Resolved from the selection at begin_sketch, not read back from a combo.
wxString m_sketch_on;
// --- the object-driven offer (charter 4.1) ---------------------------------------------
// Right-click the geometry -> a vertical list in ratified row order, verbs that do not
// apply disabled IN PLACE with their reason. The rows come from the generated table in
// DesignOffer.hpp; this map is how a row reaches the code that already implements it, for
// the verbs that have no keyboard shortcut to route through.
std::map<std::string, std::function<void()>> m_verb_actions;
// Append an offer row with its toolbar glyph. The bitmap must be set BEFORE Append —
// wxGTK builds the GtkMenuItem there and only makes an image item if one is present.
// Every status write goes through here so long hints wrap instead of clipping.
void set_status(const wxString& text);
wxString idle_hint() const; // what to say when nothing is selected
// Reason detect_mate_conflicts() recorded for a feature, or nullptr. Marks the tree row and
// feeds the status line; a conflict is a diagnostic, not a document error.
const std::string* mate_conflict_reason(int feature) const;
wxMenuItem* append_offer_item(wxMenu* menu, int id, const wxString& text,
const struct OfferVerb& v);
void show_offer_menu(const wxPoint& screen_pos);
// Where the offer opens when no mouse press anchors it: the keyboard route, and the automatic
// open on entering Sketch. The pointer if it is over the viewport, else the viewport's centre.
// A raw wxGetMousePosition() can be sitting on the toolbar, on the card column or on another
// monitor, and the menu would map there — detached from the geometry it is about.
wxPoint offer_anchor() const;
int offer_selection_kind() const; // an OfferSel, as int to keep the header light
// Does the SKETCH half of the map apply? A mode question, not a session one: begin_sketch
// does not run until the first tool is armed, so between "press Sketch" and "pick a tool"
// is_sketching() is still false — precisely when the drawing tools must be on offer. The
// is_sketching() arm covers re-opening a committed sketch, which enters the session first.
bool sketch_map_applies() const;
void run_offer_action(const char* action);
// Face-as-profile extrude (Onshape): when Extrude is opened on a picked solid face with
// no sketch source, this carries that global face id so the kernel extrudes the face.
// -1 = ordinary sketch/loop extrude. Set when opening the Extrude card, consumed on add.
int m_extrude_face_src{-1};
ComboBox* m_dressup_type{nullptr};
ComboBox* m_face_group{nullptr};
wxSpinCtrlDouble* m_dressup_size{nullptr};
wxStaticText* m_dressup_edge_label{nullptr}; // shows the picked edge, or the group fallback
ComboBox* m_hole_plane{nullptr};
wxSpinCtrlDouble* m_hole_diameter{nullptr};
wxSpinCtrlDouble* m_hole_depth{nullptr};
CheckBox* m_hole_through{nullptr};
wxSpinCtrlDouble* m_hole_x{nullptr};
wxSpinCtrlDouble* m_hole_y{nullptr};
// #2: when the Hole tool is opened on a picked solid face, drill on that face centred
// on it (origin = face centroid, normal = inward). m_hole_x/y then read as the offset
// from the face centre. Falls back to the m_hole_plane dropdown when no face is picked.
bool m_hole_on_face{false};
SketchPlane m_hole_face_plane;
int m_hole_face_body{-1};
// #2 Part B: the picked face's (u,v) bounds in m_hole_face_plane, so the hole's construction
// dims read as distance from the face sides (umin/vmin edges) rather than from the centre.
bool m_hole_has_bounds{false};
double m_hole_umin{0}, m_hole_umax{0}, m_hole_vmin{0}, m_hole_vmax{0};
// Says which face the latch above is holding. Thicken/Shell/Draft show theirs because their
// face IS the live selection; this one has to be shown precisely BECAUSE it is not, and the
// status line goes on saying "Nothing selected" while the ghost keeps drilling. 200.
wxStaticText* m_hole_target_label{nullptr};
ComboBox* m_thread_plane{nullptr};
ComboBox* m_thread_std{nullptr}; // standard designation (M6, 1/4-20 UNC, ...)
wxSpinCtrlDouble* m_thread_radius{nullptr};
wxSpinCtrlDouble* m_thread_pitch{nullptr};
wxSpinCtrlDouble* m_thread_height{nullptr};
wxSpinCtrlDouble* m_thread_depth{nullptr};
CheckBox* m_thread_internal{nullptr};
wxSpinCtrlDouble* m_thread_x{nullptr};
wxSpinCtrlDouble* m_thread_y{nullptr};
// #3: when the Thread tool is opened on a picked cylindrical face (a hole bore or a
// cylinder), thread that surface — plane on its axis, radius/internal derived from it.
bool m_thread_on_face{false};
SketchPlane m_thread_face_plane;
int m_thread_face_body{-1};
wxStaticText* m_thread_target_label{nullptr}; // the latched face/edge — see m_hole_target_label
wxSpinCtrlDouble* m_shell_thickness{nullptr};
wxStaticText* m_shell_face_label{nullptr}; // shows the picked face to remove
// Draft controls (taper a single picked solid face about the body bottom).
wxSpinCtrlDouble* m_draft_angle{nullptr};
wxStaticText* m_draft_face_label{nullptr}; // shows the picked face to draft
// Axis controls (datum axis: line through two points or derived from geometry).
ComboBox* m_axis_type{nullptr}; // AxisType: TwoPoints/FaceNormal/CylinderCenterline/PlaneIntersection/AlongEdge
wxButton* m_axis_pick_face{nullptr}; wxStaticText* m_axis_face_lbl{nullptr};
wxButton* m_axis_pick_edge{nullptr}; wxStaticText* m_axis_edge_lbl{nullptr};
ComboBox* m_axis_plane_a{nullptr};
ComboBox* m_axis_plane_b{nullptr};
wxSpinCtrlDouble* m_axis_p1x{nullptr}; wxSpinCtrlDouble* m_axis_p1y{nullptr}; wxSpinCtrlDouble* m_axis_p1z{nullptr};
wxSpinCtrlDouble* m_axis_p2x{nullptr}; wxSpinCtrlDouble* m_axis_p2y{nullptr}; wxSpinCtrlDouble* m_axis_p2z{nullptr};
int m_ax_face_body{-1}, m_ax_face{-1};
int m_ax_edge_body{-1}, m_ax_edge{-1};
AxisPick m_axis_pick{AxisPick::None};
// CoordSys controls (datum coordinate system: point + orthonormal frame).
ComboBox* m_coordsys_type{nullptr}; // CoordSysType: PointWorld/FaceAndDirection
ComboBox* m_cs_body{nullptr}; // body-focus chooser: restrict picking to one body
wxSpinCtrlDouble* m_cs_x{nullptr}; wxSpinCtrlDouble* m_cs_y{nullptr}; wxSpinCtrlDouble* m_cs_z{nullptr};
wxButton* m_cs_pick_face{nullptr}; wxStaticText* m_cs_face_lbl{nullptr};
wxButton* m_cs_pick_edge{nullptr}; wxStaticText* m_cs_edge_lbl{nullptr};
wxSpinCtrlDouble* m_cs_hx{nullptr}; wxSpinCtrlDouble* m_cs_hy{nullptr}; wxSpinCtrlDouble* m_cs_hz{nullptr};
int m_cs_face_body{-1}, m_cs_face{-1};
int m_cs_edge_body{-1}, m_cs_edge{-1};
CoordSysPick m_coordsys_pick{CoordSysPick::None};
// Onshape-style docked value-entry card (Angle/Radius/Diameter/Offset/Fillet).
wxSizer* m_box_value{nullptr};
wxStaticText* m_value_label{nullptr};
wxTextCtrl* m_value_input{nullptr}; // plain text field: forces en ('.') decimals
double m_value_min{0.0}; // range for confirm-time clamping
double m_value_max{0.0};
std::function<void(double)> m_value_cont; // deferred apply, run on Confirm
std::function<void()> m_value_cancel; // optional action when the card is cancelled
// Feature tree: a wxTreeCtrl with per-feature-type icons. Callers keep using
// integer row indices via tree_selection()/set_tree_selection(); m_tree_items
// maps feature order -> tree node, rebuilt by refresh_tree().
wxTreeCtrl* m_tree{nullptr};
wxTreeCtrl* m_parts{nullptr}; // Bodies list under the feature tree
wxStaticText* m_parts_label{nullptr}; // its "Bodies" caption (hidden when empty)
wxBoxSizer* m_parts_hdr{nullptr}; // Bodies card header (icon + title)
wxStaticLine* m_parts_rule{nullptr}; // rule under that header
wxBoxSizer* m_hdr_tree_row{nullptr}; // Feature tree header: title + row actions
wxStaticText* m_hdr_tree{nullptr}; // its title label
wxImageList* m_tree_images{nullptr};
std::vector<wxTreeItemId> m_tree_items;
// Parts list: tree rows for each body (parallel to m_doc.bodies). Selecting one
// highlights that body and makes it the target for the next op.
std::vector<wxTreeItemId> m_tree_body_items;
// Section views (non-destructive): named "Section View N" entries listed in the tree, each a
// horizontal clip height. View-only — NOT bodies/features, never serialized. Key X adds one;
// clicking a row activates it (again = off); Delete removes it; Alt+Wheel moves the active one.
// Section view (single, non-destructive): ONE horizontal clip that hides half the model to
// inspect inside — solid, no ghost of the hidden half. Toggled on/off; Flip shows the other
// half. Never a body, no tree entry.
bool m_section_on{false};
double m_section_cut_z{0.0};
bool m_section_upper{false}; // false = keep lower half, true = upper
ScalableButton* m_section_flip_btn{nullptr}; // toolbar action; enabled only while the section is on
void toggle_section_view(); // Section View button / X: on <-> off
void flip_section_view(); // Flip button / F: opposite half
void update_section_flip_btn(); // enable the Flip button iff the section is on
// Per-body visibility (parallel to m_doc.bodies; index stable across recompute since
// bodies are appended in feature order). Empty/grown to all-visible by sync_body_visible().
std::vector<bool> m_body_visible;
void sync_body_visible(); // grow/shrink m_body_visible to bodies.size()
// Per-body display translation (Move-body, M5). Parallel to m_doc.bodies; default
// identity. Applied to the display/pick meshes only — the OCCT shape (and face/edge
// global ids) is never touched, so dress-up targeting stays stable across a move.
std::vector<Transform3d> m_body_xform;
std::vector<TriangleMesh> m_disp_body_meshes; // display_body_meshes with m_body_xform applied
TriangleMesh m_disp_pick_mesh; // combined pick mesh with m_body_xform applied
void sync_body_xform(); // grow m_body_xform to bodies.size() (identity)
void rebuild_disp_meshes(); // recompute m_disp_* from m_doc + m_body_xform
void feed_bodies(); // push m_disp_* + visibility/xform to the viewport
void on_move_body(); // start the move gizmo on the selected body
void arm_transform_gizmo(); // arm the move gizmo on the Transform card's body (add mode only)
void on_set_body_color(); // Color tool: pick a per-body display colour override
void on_boolean_tool(); // Boolean (combine bodies): needs two solids, then opens the tool
int tree_selection() const; // selected feature row, or wxNOT_FOUND
int tree_body_selection() const; // selected Parts-list body index, or -1
void refresh_parts(); // rebuild the Bodies list under the feature tree
void sync_sidebar_width(); // keep the panel as wide as Prepare's sidebar
void set_tree_selection(int row);
static int tree_icon_for(CadFeatureType t);
wxStaticText* m_status{nullptr};
// m_status's foreground as created, captured before any caller touches it. Callers signal
// "no opinion" by setting wxNullColour, which restores exactly this — so it is the only
// reliable way to tell a chosen colour (the error red) from the default. See set_status().
wxColour m_status_default_fg;
// The guidance sentence for the step the armed sketch tool is on, kept so a transient
// readout (the live length/angle while a segment is being dragged) can be appended to it
// instead of replacing it — the guidance used to vanish on the first mouse move after a
// click, which is precisely when it is needed. 1c0c.
wxString m_sketch_step;
// mode is a DesignSketchTool::Mode; passed as an int because this header deliberately does
// not include the tool's, and the .cpp (which does) casts it back.
void on_sketch_step(int mode, int step, int picks);
wxStaticText* m_dof_status{nullptr}; // DoF / constraint-state readout (P3)
// Last live-solve result, so entering Constrain can restore the readout without a solve.
int m_dof_last{-1};
bool m_dof_last_ok{true};
bool m_dof_last_has{false};
int m_feature_counter{0};
std::vector<wxButton*> m_confirm_btns;
// Edit-in-place state: add-mode is m_edit_index == -1. Single-feature edit
// (Sketch or Extrude independently) uses only m_edit_index as the row to replace.
int m_edit_index{-1};
// Tree row of the sketch currently being constrained (-1 = not constraining).
int m_constrain_feat{-1};
// Constraint-manager card (C3.4): header + a rebuildable list of constraint rows.
wxSizer* m_box_constraints{nullptr};
wxStaticText* m_hdr_constraints{nullptr};
wxSizer* m_constraint_rows{nullptr};
int m_constraint_sel{-1}; // highlighted constraint row, or -1
};
}} // namespace Slic3r::GUI
#endif // slic3r_DesignPanel_hpp_
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
#ifndef slic3r_GUI_McpControl_hpp_
#define slic3r_GUI_McpControl_hpp_
// MCP control surface (slice 1): a local JSON-RPC 2.0 server, line-delimited over a
// Unix domain socket, that lets an external MCP bridge drive and perceive the Design
// tab. Off unless the env var ORCA_CAD_MCP is set:
// ORCA_CAD_MCP=1 -> socket at /tmp/orca-cad-mcp.sock
// ORCA_CAD_MCP=/path/to.sock -> socket at that path
// All CAD work is marshalled onto the wx main thread and runs through the SAME
// CadDocument kernel the GUI uses (no parallel engine). Slice-1 methods:
// describe_tools, describe_scene, extrude.
//
// ponytail: Unix-socket only (POSIX). Windows compiles this to a no-op; add a named
// pipe transport when a Windows agent actually needs it.
namespace Slic3r { namespace GUI {
// Start the server thread iff ORCA_CAD_MCP is set. Safe to call once after the
// MainFrame + DesignPanel exist. No-op when the env var is unset or on Windows.
void start_mcp_control_if_enabled();
}} // namespace Slic3r::GUI
#endif // slic3r_GUI_McpControl_hpp_
+203
View File
@@ -0,0 +1,203 @@
#include "slic3r/GUI/CAD/SketchInlineEditor.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "libslic3r/Color.hpp"
#include <imgui/imgui.h>
#include <imgui/imgui_internal.h> // BringWindowToDisplayFront / GetCurrentWindow
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
namespace Slic3r {
namespace GUI {
namespace {
// Numbers are typed and shown with a POINT, whatever the locale: this field feeds a CAD kernel,
// and a decimal comma reaching it as a thousands separator is a silent order-of-magnitude error.
// Parsing accepts either separator because a keyboard's numeric pad may only offer one.
std::string fmt_value(double v, int digits = 2)
{
char fmt[16];
std::snprintf(fmt, sizeof(fmt), "%%.%df", digits);
char buf[64];
std::snprintf(buf, sizeof(buf), fmt, v);
for (char* c = buf; *c; ++c)
if (*c == ',') *c = '.';
return std::string(buf);
}
bool parse_value(const char* text, double& out)
{
if (text == nullptr) return false;
std::string t(text);
for (char& c : t)
if (c == ',') c = '.';
// strtod, not std::stod: no exceptions, and `end` tells us whether the WHOLE field was a
// number. "12mm" must be refused, not silently read as 12.
const char* b = t.c_str();
char* end = nullptr;
const double v = std::strtod(b, &end);
if (end == b) return false;
while (*end == ' ' || *end == '\t') ++end;
if (*end != '\0') return false;
out = v;
return true;
}
// One machine-readable line per event of the click-edit contract, for the UX check that runs
// after every build (scripts/CAD/check-gui-click-edit.py). Deliberately NOT the same switch as
// ORCA_CAD_KEYTRACE: that one is a debugging firehose, this one is an assertion surface and its
// format is a contract the script parses.
//
// The pair that matters is `open` vs `commit`: the check always types a value DIFFERENT from the
// prefill, so a field that is on screen but not editable commits its prefill and the two lines
// disagree. A focus flag cannot show that — it read 0 even when typing worked — but the number
// the user actually gets can.
void ux_trace(const char* event, const std::string& title, const std::string& detail)
{
if (!std::getenv("ORCA_CAD_UXTRACE")) return;
std::fprintf(stderr, "[UX] %s title=%s %s\n", event, title.c_str(), detail.c_str());
std::fflush(stderr);
}
} // namespace
void SketchInlineEditor::open(const wxPoint& canvas_px, double value, const std::string& title,
std::function<void(double)> on_commit,
std::function<void()> on_cancel)
{
m_anchor = canvas_px;
m_title = title;
m_err.clear();
m_commit = std::move(on_commit);
m_cancel = std::move(on_cancel);
const std::string v = fmt_value(value);
std::snprintf(m_buf, sizeof(m_buf), "%s", v.c_str());
m_open = true;
// ImGui takes keyboard focus for one frame on request; asking on the frame the field first
// appears is what makes typing land without a click. There is no window manager to consult.
m_focus_pending = true;
ux_trace("open", m_title, "prefill=" + v);
}
void SketchInlineEditor::close()
{
m_open = false;
m_focus_pending = false;
m_commit = nullptr;
m_cancel = nullptr;
m_err.clear();
}
void SketchInlineEditor::cancel()
{
if (m_open) do_cancel();
}
void SketchInlineEditor::commit()
{
if (m_open) do_commit();
}
void SketchInlineEditor::do_cancel()
{
ux_trace("cancel", m_title, "");
auto cb = m_cancel;
close();
if (cb) cb();
}
void SketchInlineEditor::do_commit()
{
double v = 0.0;
if (!parse_value(m_buf, v)) {
// Refusing input in silence is indistinguishable from the app having frozen: the field
// just sits there and the user has no idea what it wants. Say so in the title line and
// keep editing.
ux_trace("refused", m_title, std::string("typed=") + m_buf);
m_err = (m_buf[0] == '\0') ? into_u8(_L("Enter a number")) : into_u8(_L("Not a number"));
m_focus_pending = true;
return;
}
ux_trace("commit", m_title, std::string("typed=") + m_buf + " value=" + fmt_value(v, 4));
auto cb = m_commit;
close();
// AFTER close(): the callback may open the next queued dimension (a rectangle queues Width
// then Height), and doing that into a field that still believes it is open would drop the
// second one's prefill on the floor.
if (cb) cb(v);
}
bool SketchInlineEditor::render(ImGuiWrapper& imgui, float scale)
{
if (!m_open) return false;
ImGuiWrapper::push_common_window_style(scale);
imgui.set_next_window_pos((float) m_anchor.x, (float) m_anchor.y, ImGuiCond_Always, 0.5f, 0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3.0f);
// NoInputs is what every other sketch overlay sets and is exactly what this one must not:
// it is the only overlay in the tab that the user types into.
imgui.begin(std::string("##sketchvalue"),
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration
| ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings);
ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow());
if (!m_title.empty() || !m_err.empty()) {
if (m_err.empty()) {
imgui.text(m_title);
} else {
ImGui::PushStyleColor(ImGuiCol_Text, ImGuiWrapper::to_ImVec4(ColorRGBA(0.91f, 0.42f, 0.42f, 1.0f)));
imgui.text(m_err);
ImGui::PopStyleColor();
}
}
if (m_focus_pending) {
ImGui::SetKeyboardFocusHere();
m_focus_pending = false;
}
ImGui::PushItemWidth(90.0f * scale);
// EnterReturnsTrue so Enter commits from inside the widget; AutoSelectAll so the prefill is
// replaced by the first digit typed, which is what "pre-selected" meant when this was a
// wxTextCtrl and is what makes typing a value a single gesture.
const bool entered = ImGui::InputText("##sketchvalue_in", m_buf, sizeof(m_buf),
ImGuiInputTextFlags_EnterReturnsTrue
| ImGuiInputTextFlags_AutoSelectAll
| ImGuiInputTextFlags_CharsDecimal);
// MEASUREMENT, not a fix: one line per frame saying whether ImGui believes it owns the
// keyboard and whether our widget is the active one. "Typing does not arrive" has two very
// different causes — no FRAMES (this canvas repaints on demand only, so an idle canvas never
// processes ImGui's queued characters) versus frames that run while the input is not active —
// and they are indistinguishable from outside.
if (std::getenv("ORCA_CAD_UXTRACE")) {
const ImGuiIO& io = ImGui::GetIO();
std::fprintf(stderr, "[UX] frame title=%s want_text=%d want_kb=%d active=%d buf=%s\n",
m_title.c_str(), (int) io.WantTextInput, (int) io.WantCaptureKeyboard,
(int) ImGui::IsItemActive(), m_buf);
std::fflush(stderr);
}
ImGui::PopItemWidth();
imgui.end();
ImGui::PopStyleVar();
ImGuiWrapper::pop_common_window_style();
// Keep the frames coming while the field is up — see request_frame's note in the header.
if (m_open && request_frame)
request_frame();
// Act AFTER end(): do_commit can reopen the field for the next queued dimension, and that
// must not happen inside this frame's window.
if (entered)
do_commit();
else if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape)))
do_cancel();
return true;
}
}} // namespace Slic3r::GUI
+95
View File
@@ -0,0 +1,95 @@
#ifndef slic3r_SketchInlineEditor_hpp_
#define slic3r_SketchInlineEditor_hpp_
#include <functional>
#include <string>
#include <wx/gdicmn.h>
namespace Slic3r {
namespace GUI {
class ImGuiWrapper;
// Onshape-style in-canvas value editor.
//
// IT IS NOT A WINDOW. It used to be a borderless top-level wxFrame holding a wxTextCtrl, and
// that is the whole history of this file: a separate top-level window can only receive typing
// if the window manager grants it focus, and whether it does is not ours to decide. openbox
// grants it; mutter's focus-stealing prevention refuses it, so on a GNOME desktop the field
// appeared, showed its value selected, and silently ignored every keystroke — Enter then
// committed the number it opened with. Seven workarounds were tried against that (a real X11
// server timestamp for gtk_window_present, re-asserted SetFocus, dropping the _UTILITY hint,
// keeping the frame mapped between two queued fields, forwarding keys from the panel's
// CHAR_HOOK), one of them caused a macOS regression, and the test harness ended up clicking the
// field before typing — which is the workaround a user cannot be asked to perform, and is
// exactly the "label value not editable" report.
//
// So the field stops asking. It is now drawn INSIDE the GL canvas as an ImGui overlay, at the
// same screen point as before, and its keys arrive through the canvas's own key events, which
// GLCanvas3D already feeds to ImGui (see GLCanvas3D::on_key / on_char -> update_key_data). The
// canvas is part of the main window and already has focus, so there is no second window, no
// second focus, and no window manager in the path. The dimension labels next to it are already
// ImGui overlays (DesignSketchTool::draw_dim_label), so this is the same vocabulary, not a new
// one.
//
// Ownership: DesignCanvas owns it; DesignSketchTool::render() calls render() once per frame.
class SketchInlineEditor
{
public:
SketchInlineEditor() = default;
// Open the field anchored at `canvas_px` (canvas DEVICE pixels, the coordinate space the
// sketch tool works in), pre-filled with `value` and pre-selected. on_commit(parsed) fires
// on Enter with a valid number; on_cancel() on Esc.
void open(const wxPoint& canvas_px, double value, const std::string& title,
std::function<void(double)> on_commit,
std::function<void()> on_cancel);
void close(); // drop it with neither callback
void cancel(); // if open, run the registered cancel (keep-as-drawn)
void commit(); // if open, run the registered commit (accept the typed value)
bool is_open() const { return m_open; }
// Draw it, and let ImGui do the editing. Called from DesignSketchTool::render() inside the
// frame's ImGui pass; `scale` is the tool's m_render_scale. Returns true if it drew.
bool render(ImGuiWrapper& imgui, float scale);
// Ask for another frame. THE FIELD DOES NOT WORK WITHOUT THIS, and the reason is a deadlock
// that only a per-frame trace shows:
//
// [UX] frame want_text=0 want_kb=0 active=0 <- frame 1: the widget is not active yet
// [UX] frame want_text=0 want_kb=0 active=1 <- frame 2: it is now
// (nothing further) <- the canvas has nothing to redraw, so it stops
//
// This canvas repaints ON DEMAND. ImGui decides whether it wants the keyboard at the END of a
// frame, from the active item, and GLCanvas3D::on_char only calls render() when
// update_key_data() says ImGui wants it. No frames -> WantTextInput never turns on -> no
// render on a keystroke -> still no frames. The characters sit in ImGui's queue and the field
// looks exactly as deaf as the window it replaced. One repaint per frame while it is open
// breaks the circle.
std::function<void()> request_frame;
// Kept because callers ask them, but there is no longer any difference to report: with no
// window there is no state where the field is on screen but logically closed, and no state
// where it is open but somebody else holds the keyboard.
bool is_mapped() const { return m_open; }
bool has_focus() const { return m_open; }
void dismiss() { close(); }
private:
void do_commit();
void do_cancel();
std::function<void(double)> m_commit;
std::function<void()> m_cancel;
bool m_open{false};
bool m_focus_pending{false}; // one frame of SetKeyboardFocusHere after opening
wxPoint m_anchor{0, 0}; // canvas device px
std::string m_title;
std::string m_err; // why the last value was refused, shown in the title line
char m_buf[64]{}; // the edited text; ImGui::InputText writes into it
};
}} // namespace Slic3r::GUI
#endif // slic3r_SketchInlineEditor_hpp_
+3 -2
View File
@@ -395,8 +395,9 @@ bool confirm_create_decompose_missing_components(wxWindow* parent, const std::ve
missing_text += missing[i].display_name;
}
wxString message = _L("The current filament list does not contain ") + missing_text +
_L(". A project filament required by the mixed filament will be created automatically after decomposition.");
wxString message = wxString::Format(_L("The current filament list does not contain %s. A project filament required by "
"the mixed filament will be created automatically after decomposition."),
missing_text);
MessageDialog dlg(parent, message, _L("Tip"), wxOK | wxCANCEL | wxICON_INFORMATION);
dlg.show_dsa_button();
+9 -2
View File
@@ -1041,10 +1041,12 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
for (auto el : {"wipe_tower_rotation_angle", "wipe_tower_cone_angle",
"wipe_tower_extra_spacing", "wipe_tower_max_purge_speed",
"wipe_tower_bridging", "wipe_tower_extra_flow",
"wipe_tower_no_sparse_layers"})
"wipe_tower_bridging", "wipe_tower_extra_flow"})
toggle_line(el, have_prime_tower && supports_wipe_tower_2);
// Orca: both tower generators skip sparse layers, so this is not a wipe tower 2 exclusive.
toggle_line("wipe_tower_no_sparse_layers", have_prime_tower);
WipeTowerWallType wipe_tower_wall_type = config->opt_enum<WipeTowerWallType>("wipe_tower_wall_type");
bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower;
toggle_line("wipe_tower_cone_angle", have_prime_tower && supports_wipe_tower_2 && wipe_tower_wall_type == WipeTowerWallType::wtwCone);
@@ -1055,6 +1057,10 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
toggle_line("single_extruder_multi_material_priming", !bSEMM && have_prime_tower && supports_wipe_tower_2);
bool use_cyclic_ordering = config->opt_enum<ToolChangeOrderingType>("toolchange_ordering") == ToolChangeOrderingType::Cyclic;
toggle_line("toolchange_cyclic_order", use_cyclic_ordering);
toggle_line("toolchange_cyclic_first_layer", use_cyclic_ordering);
toggle_line("prime_volume",have_prime_tower && (!purge_in_primetower || !bSEMM));
for (auto el : {"flush_into_infill", "flush_into_support", "flush_into_objects"})
@@ -1128,6 +1134,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
bool has_detect_overhang_wall = config->opt_bool("detect_overhang_wall");
bool has_overhang_reverse = config->opt_bool("overhang_reverse");
bool allow_overhang_reverse = !has_spiral_vase;
toggle_line("unsupported_wall_last", has_detect_overhang_wall);
toggle_line("overhang_reverse", allow_overhang_reverse);
toggle_line("overhang_reverse_internal_only", allow_overhang_reverse && has_overhang_reverse);
bool has_overhang_reverse_internal_only = config->opt_bool("overhang_reverse_internal_only");
+11
View File
@@ -188,6 +188,17 @@ wxString get_string_value(const std::string& opt_key, const DynamicPrintConfig&
out = double_to_string(opt->value) + (opt->percent ? "%" : "");
return out;
}
case coFloatsOrPercents: {
const auto* values = static_cast<const ConfigOptionVector<FloatOrPercent>*>(option);
// Orca: Preset comparison may request the entire vector instead of an indexed entry.
if (orig_opt_idx < 0)
return from_u8(option->serialize());
if (opt_idx < values->size()) {
const FloatOrPercent& value = values->get_at(opt_idx);
return double_to_string(value.value) + (value.percent ? "%" : "");
}
return _L("Undefined");
}
case coEnum: {
return get_string_from_enum(pure_key, config,
pure_key == "top_surface_pattern" ||
+6 -3
View File
@@ -17,6 +17,7 @@
#include "Plater.hpp"
#include "Camera.hpp"
#include "I18N.hpp"
#include "format.hpp"
#include "GUI_Utils.hpp"
#include "GUI.hpp"
#include "GLCanvas3D.hpp"
@@ -3436,16 +3437,18 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
return ret;
};
// Whole sentences: the bare "up to"/"above"/"from"/"to" these used to be glued from gave a
// translator no context, and left the unit and the numbers stuck in English word order.
auto upto_label = [](double z) {
char buf[64];
::sprintf(buf, "%.2f", z);
return _u8L("up to") + " " + std::string(buf) + " " + _u8L("mm");
return format(_u8L("up to %1% mm"), buf);
};
auto above_label = [](double z) {
char buf[64];
::sprintf(buf, "%.2f", z);
return _u8L("above") + " " + std::string(buf) + " " + _u8L("mm");
return format(_u8L("above %1% mm"), buf);
};
auto fromto_label = [](double z1, double z2) {
@@ -3453,7 +3456,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
::sprintf(buf1, "%.2f", z1);
char buf2[64];
::sprintf(buf2, "%.2f", z2);
return _u8L("from") + " " + std::string(buf1) + " " + _u8L("to") + " " + std::string(buf2) + " " + _u8L("mm");
return format(_u8L("from %1% to %2% mm"), buf1, buf2);
};
auto role_time_and_percent = [this, total_estimated_time](libvgcode::EGCodeExtrusionRole role) {
+339 -7
View File
@@ -1,5 +1,8 @@
#include "libslic3r/libslic3r.h"
#include "GLCanvas3D.hpp"
#ifdef SLIC3R_CAD
#include "slic3r/GUI/CAD/DesignSketchTool.hpp" // Design tab: interactive 2D sketch tool
#endif
#include <igl/unproject.h>
@@ -1826,6 +1829,16 @@ void GLCanvas3D::enable_separator_toolbar(bool enable)
m_separator_toolbar.set_enabled(enable);
}
void GLCanvas3D::enable_collapse_toolbar(bool enable)
{
m_collapse_toolbar_enabled = enable;
}
void GLCanvas3D::enable_plate_chrome(bool enable)
{
m_plate_chrome_enabled = enable;
}
bool GLCanvas3D::has_mouse_capture() const {
return m_canvas != nullptr && m_canvas->HasCapture();
}
@@ -2047,14 +2060,24 @@ void GLCanvas3D::render(bool only_init)
no_partplate = true;
else if (gizmo_type == GLGizmosManager::BrimEars && !camera.is_looking_downward())
show_grid = false;
if (m_axes_at_bed_center)
// Design tab: the plate grid is generated from the plate's front-left corner, so it
// floats mid-cell under the modeling-origin triad. Suppress it here; a CAD grid centred
// on the origin is rendered in its place (see _render_cad_grid).
show_grid = false;
/* view3D render*/
int hover_id = (m_hover_plate_idxs.size() > 0)?m_hover_plate_idxs.front():-1;
if (m_canvas_type == ECanvasType::CanvasView3D) {
if (!no_partplate)
// m_show_bed gates the plate list too: hiding the bed but leaving its grid and outline
// floating would read as a rendering fault rather than a deliberate view option.
if (!no_partplate && m_show_bed)
_render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), m_show_world_axes);
if (!no_partplate) //BBS: add outline logic
if (!no_partplate && m_show_bed) //BBS: add outline logic
_render_platelist(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), only_current, only_body, hover_id, true, show_grid);
if (m_axes_at_bed_center && m_show_bed && !no_partplate)
// Design tab: replace the plate's corner-origin grid with the origin-centred CAD grid.
_render_cad_grid(camera.get_view_matrix(), camera.get_projection_matrix());
//BBS: add outline logic
// Depth pass for object-on-object and self shadows; consumed by the gouraud shader below.
@@ -2120,6 +2143,13 @@ void GLCanvas3D::render(bool only_init)
if (_is_fxaa_enabled())
_render_fxaa_pass(static_cast<unsigned int>(cnv_size.get_width()), static_cast<unsigned int>(cnv_size.get_height()));
// Design tab: interactive 2D sketch overlay, drawn over the scene but
// beneath the UI overlays (toolbars, labels).
#ifdef SLIC3R_CAD
if (m_design_sketch_tool != nullptr && m_design_sketch_tool->has_display())
m_design_sketch_tool->render(*this);
#endif
// draw overlays
_render_overlays();
@@ -3202,7 +3232,11 @@ void GLCanvas3D::on_idle(wxIdleEvent& evt)
// BBS
//m_dirty |= wxGetApp().plater()->get_view_toolbar().update_items_state();
m_dirty |= wxGetApp().plater()->get_collapse_toolbar().update_items_state();
bool mouse3d_controller_applied = wxGetApp().plater()->get_mouse3d_controller().apply(wxGetApp().plater()->get_camera());
// apply() DRAINS the 3D-mouse queue, so only the canvas actually on screen may call it: a
// hidden canvas renders nothing, so the motion it swallowed moves the shared camera without
// ever being drawn and the next visible frame jumps several states at once.
bool mouse3d_controller_applied = _is_shown_on_screen()
&& wxGetApp().plater()->get_mouse3d_controller().apply(wxGetApp().plater()->get_camera());
m_dirty |= mouse3d_controller_applied;
m_dirty |= wxGetApp().plater()->get_notification_manager()->update_notifications(*this);
auto gizmo = wxGetApp().plater()->get_view3D_canvas3D()->get_gizmos_manager().get_current();
@@ -3270,6 +3304,64 @@ void GLCanvas3D::on_char(wxKeyEvent& evt)
return;
}
// Design tab: Delete/Backspace removes the selected sketch entities while a
// sketch tool is active and the canvas has focus (dialog text fields are separate
// wx controls, so this never eats their editing keys).
#ifdef SLIC3R_CAD
if (m_design_sketch_tool != nullptr && m_design_sketch_tool->is_active()
&& (keyCode == WXK_DELETE || keyCode == WXK_BACK)
&& !m_design_sketch_tool->selection().empty()) {
m_design_sketch_tool->delete_selected();
m_dirty = true;
render();
return;
}
#endif
// Esc exits the active sketch tool (Onshape-like, layered: abort in-progress entity ->
// drop to Select -> exit the session back to Feature mode).
#ifdef SLIC3R_CAD
if (m_design_sketch_tool != nullptr && m_design_sketch_tool->is_active()
&& keyCode == WXK_ESCAPE) {
m_design_sketch_tool->request_exit();
m_dirty = true;
render();
return;
}
#endif
// Design tab: Ctrl+Z / Ctrl+Shift+Z (and Ctrl+Y) undo/redo the Design feature
// history. Scoped by m_design_sketch_tool — only the Design canvas owns one — so the
// main 3D editor's undo/redo (the CanvasView3D-gated cases further below) is untouched.
// Handled here, before the generic Ctrl block, so it takes precedence and early-returns.
#ifdef SLIC3R_CAD
if (m_design_sketch_tool != nullptr && (evt.GetModifiers() & ctrlMask) != 0) {
const bool is_z = (keyCode == 'z' || keyCode == 'Z' || keyCode == WXK_CONTROL_Z);
const bool is_y = (keyCode == 'y' || keyCode == 'Y' || keyCode == WXK_CONTROL_Y);
if (is_z || is_y) {
const bool redo = is_y || ((evt.GetModifiers() & shiftMask) != 0);
m_design_sketch_tool->request_undo_redo(redo);
m_dirty = true;
render();
return;
}
}
#endif
// Design tab: F = Place on Face (Prepare's lay-flat), when the Design viewport is up
// and a body face is selected. The tool forwards to DesignPanel::place_on_face; it returns
// false (no face picked) so F falls through to the default handler below.
#ifdef SLIC3R_CAD
if (m_design_sketch_tool != nullptr && m_design_sketch_tool->has_display()
&& (keyCode == 'f' || keyCode == 'F') && (evt.GetModifiers() & ctrlMask) == 0) {
if (m_design_sketch_tool->request_place_on_face()) {
m_dirty = true;
render();
return;
}
}
#endif
bool is_in_painting_mode = false;
GLGizmoPainterBase *current_gizmo_painter = dynamic_cast<GLGizmoPainterBase *>(get_gizmos_manager().get_current());
if (current_gizmo_painter != nullptr) {
@@ -3642,6 +3734,20 @@ public:
void GLCanvas3D::on_key(wxKeyEvent& evt)
{
// Design tab: Delete/Backspace removes selected sketch entities. GTK delivers
// these as KEY_DOWN rather than CHAR, so handle it here too.
#ifdef SLIC3R_CAD
if (evt.GetEventType() == wxEVT_KEY_DOWN
&& m_design_sketch_tool != nullptr && m_design_sketch_tool->is_active()
&& (evt.GetKeyCode() == WXK_DELETE || evt.GetKeyCode() == WXK_BACK)
&& !m_design_sketch_tool->selection().empty()) {
m_design_sketch_tool->delete_selected();
m_dirty = true;
render();
return;
}
#endif
static GLCanvas3D const * thiz = nullptr;
static TranslationProcessor translationProcessor(nullptr, nullptr);
if (thiz != this) {
@@ -4206,6 +4312,23 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
return;
}
// Design tab: the interactive sketch tool owns the mouse whenever it has
// something on screen — an active session OR committed sketch overlays that the user
// can click to select. It runs after ImGui (so dialogs still work) but before
// camera/toolbar/gizmo handling; on_mouse returns false for events it doesn't consume
// (drag/orbit/wheel) so the camera keeps working over the display-only plate.
#ifdef SLIC3R_CAD
if (m_design_sketch_tool != nullptr && m_design_sketch_tool->has_display()) {
if (evt.LeftDown() && m_canvas != nullptr)
m_canvas->SetFocus(); // grab keyboard focus so Delete/keys reach this canvas
if (m_design_sketch_tool->on_mouse(evt, *this)) {
m_dirty = true;
render(); // force an immediate redraw so the sketch overlay updates live
return;
}
}
#endif
#ifdef __WXMSW__
bool on_enter_workaround = false;
if (! evt.Entering() && ! evt.Leaving() && m_mouse.position.x() == -1.0) {
@@ -4310,6 +4433,9 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
if (can_sequential_clearance_show_in_gizmo())
update_sequential_clearance();
} else {
// Orca: by-layer counterpart, for a prime tower compacted by "No sparse layers".
if (current_printer_technology() == ptFFF && can_sequential_clearance_show_in_gizmo())
update_compacted_wipe_tower_clearance();
if (c == GLGizmosManager::EType::Move ||
c == GLGizmosManager::EType::Scale ||
c == GLGizmosManager::EType::Rotate)
@@ -4543,8 +4669,12 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
TransformationType trafo_type;
trafo_type.set_relative();
m_selection.translate(cur_pos - m_mouse.drag.start_position_3D, trafo_type);
if (current_printer_technology() == ptFFF && (fff_print()->config().print_sequence == PrintSequence::ByObject))
update_sequential_clearance();
if (current_printer_technology() == ptFFF) {
if (fff_print()->config().print_sequence == PrintSequence::ByObject)
update_sequential_clearance();
else
update_compacted_wipe_tower_clearance();
}
// BBS
//wxGetApp().obj_manipul()->set_dirty();
m_dirty = true;
@@ -4910,6 +5040,8 @@ bool GLCanvas3D::is_camera_rotate(const wxMouseEvent& evt, const std::map<MouseB
{
if (m_is_touchpad_navigation) {
return evt.Moving() && evt.AltDown() && !evt.ShiftDown();
} else if (m_cad_navigation) {
return evt.Dragging() && evt.MiddleIsDown(); // left-drag is the selection rubber band
} else {
return evt.Dragging() && clicked_button_matches_action(evt, MouseAction::Rotation, mappings);
}
@@ -4919,6 +5051,8 @@ bool GLCanvas3D::is_camera_pan(const wxMouseEvent& evt, const std::map<MouseButt
{
if (m_is_touchpad_navigation) {
return evt.Moving() && evt.ShiftDown() && !evt.AltDown();
} else if (m_cad_navigation) {
return evt.Dragging() && evt.RightIsDown(); // middle now orbits, so pan is right only
} else {
return evt.Dragging() && clicked_button_matches_action(evt, MouseAction::Pan, mappings);
;
@@ -5608,6 +5742,101 @@ bool GLCanvas3D::can_sequential_clearance_show_in_gizmo() {
return false;
}
// Live preview of the compacted prime tower clearance, the by-layer counterpart of
// update_sequential_clearance(). Called while the user drags a volume / gizmo; idle visibility
// matches sequential print (hidden when valid, filled when Print::validate reports a collision).
// Print::compacted_wipe_tower_clearance_valid() answers the same question authoritatively, but it
// reads the tower position from the config, which only catches up once do_move() writes it back on
// mouse release. Recomputing from the volumes here is what makes the keep-out zone follow the tower
// while it is still under the cursor.
void GLCanvas3D::update_compacted_wipe_tower_clearance()
{
if (current_printer_technology() != ptFFF)
return;
const Print *print = fff_print();
if (print == nullptr)
return;
const PrintConfig &config = print->config();
if (config.print_sequence != PrintSequence::ByLayer || ! wipe_tower_sparse_layers_skipped(config) || ! print->has_wipe_tower())
return;
PartPlateList &plate_list = wxGetApp().plater()->get_partplate_list();
PartPlate *plate = plate_list.get_curr_plate();
if (plate == nullptr)
return;
const int plate_id = plate_list.get_curr_plate_index();
// Once the tower has been generated the scene shows its real mesh with the brim merged in,
// otherwise it is a bare estimated cube with no brim at all. Only the latter needs the brim added
// here, and the width comes from WipeTowerData, the same source the preview box is sized from, so
// the zone cannot be padded against a brim the preview was not built with.
const bool preview_carries_brim = print->is_step_done(psWipeTower) && print->wipe_tower_data().wipe_tower_mesh_data.has_value();
const double brim = preview_carries_brim ? 0. : double(print->wipe_tower_data(print->extruders().size()).brim_width);
const double padding = compacted_tower_footprint_padding(config, brim);
// Tower footprint straight from the volume the user sees, so that dragging either the tower or an
// object updates the zone on the very next frame.
Polygon tower_footprint;
for (const GLVolume *v : m_volumes.volumes) {
if (! v->is_wipe_tower || v->object_idx() - 1000 != plate_id)
continue;
const BoundingBoxf3 bbox = v->transformed_convex_hull_bounding_box();
tower_footprint = Polygon({ Point(scale_(bbox.min.x() - padding), scale_(bbox.min.y() - padding)),
Point(scale_(bbox.max.x() + padding), scale_(bbox.min.y() - padding)),
Point(scale_(bbox.max.x() + padding), scale_(bbox.max.y() + padding)),
Point(scale_(bbox.min.x() - padding), scale_(bbox.max.y() + padding)) });
break;
}
const CompactedTowerZone zone = compacted_wipe_tower_zone(config, tower_footprint);
if (zone.empty()) {
reset_sequential_print_clearance();
return;
}
// While dragging, outline every on-plate instance next to the tower ring, the way sequential print
// outlines every object. Both carry half of the clearance, so the two outlines meeting is precisely
// the moment that object goes over its limit - which is what makes the pair worth drawing at all.
// The tier is per object, so a short object gets the narrow nozzle outline rather than the wide
// body one it is not subject to; without that, a 3 mm object parked beside the tower would be drawn
// deep inside the keep-out ring while passing the check. Only the instances that already exceed
// allowed_rise also get a height limit plane.
Polygons outlines;
std::vector<std::pair<Polygon, float>> height_polygons;
bool body_tier_used = false;
const BoundingBox plate_bb = plate->get_bounding_box_crd();
for (const ModelObject *model_object : m_model->objects) {
for (size_t i = 0; i < model_object->instances.size(); ++i) {
Geometry::Transformation trafo(model_object->instances[i]->get_transformation());
const Vec3d offset = trafo.get_offset();
trafo.set_offset(Vec3d(offset.x(), offset.y(), 0.0));
const Polygon inst_hull = model_object->convex_hull_2d(trafo.get_matrix());
if (inst_hull.points.empty() || ! plate_bb.overlap(inst_hull.bounding_box()))
continue;
// Same tiers and the same rise measured from the plate as
// Print::compacted_wipe_tower_clearance_valid(), so that the preview and the validation
// that follows it never contradict each other.
const double object_top = model_object->get_instance_max_z(i);
const CompactedTowerClearance clearance = compacted_wipe_tower_clearance(config, zone, inst_hull, object_top);
body_tier_used = body_tier_used || compacted_tower_body_tier(clearance);
const Polygon outline = compacted_wipe_tower_offender_outline(inst_hull, clearance.body_clearance);
outlines.emplace_back(outline);
if (object_top <= clearance.allowed_rise + EPSILON)
continue;
height_polygons.emplace_back(outline, float(clearance.allowed_rise));
}
}
Polygons polygons = compacted_wipe_tower_rings(zone, body_tier_used);
append(polygons, outlines);
set_sequential_print_clearance_visible(true);
set_sequential_print_clearance_render_fill(false);
set_sequential_print_clearance_polygons(polygons, height_polygons);
}
void GLCanvas3D::update_sequential_clearance()
{
if (current_printer_technology() != ptFFF || (fff_print()->config().print_sequence == PrintSequence::ByLayer))
@@ -7917,13 +8146,113 @@ void GLCanvas3D::_render_bed(const Transform3d& view_matrix, const Transform3d&
*/
//bool show_texture = true;
//BBS set axes mode
m_bed.set_axes_mode(m_main_toolbar.is_enabled());
if (m_axes_at_bed_center) {
// Design tab: triad at the bed centre = modeling origin (set every frame because
// set_shape/set_axes_mode otherwise reset it to the bed corner).
const Vec2d bc = m_bed.build_volume().bed_center();
m_bed.set_axes_origin(Vec3d(bc.x(), bc.y(), 0.0));
} else {
m_bed.set_axes_mode(m_main_toolbar.is_enabled());
}
m_bed.render(*this, view_matrix, projection_matrix, bottom, scale_factor, show_axes);
}
void GLCanvas3D::_render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid)
{
wxGetApp().plater()->get_partplate_list().render(view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid);
wxGetApp().plater()->get_partplate_list().render(view_matrix, projection_matrix, bottom, only_current, only_body, hover_id, render_cali, show_grid, !m_plate_chrome_enabled);
}
// Design tab: CAD grid on the bed plane, drawn in place of the plate's corner-origin grid.
// Generated from the bed centre (= modeling origin) so a grid line passes exactly through the
// triad in both axes. Minor lines every 10 mm, major every 50 mm; the two GLModels are built
// once and rebuilt only when the bed shape changes, not per frame.
void GLCanvas3D::_render_cad_grid(const Transform3d& view_matrix, const Transform3d& projection_matrix)
{
const BuildVolume& build_volume = m_bed.build_volume();
if (!build_volume.valid())
return;
const Vec2d center = build_volume.bed_center();
const BoundingBoxf bb = build_volume.bounding_volume2d();
if (!m_cad_grid_valid || m_cad_grid_center != center || m_cad_grid_bb != bb) {
m_cad_grid_center = center;
m_cad_grid_bb = bb;
m_cad_grid_valid = true;
// Same z as PartPlate::GROUND_Z_GRIDLINE (-0.26f): just below the bed fill (GROUND_Z =
// -0.03f, which is drawn with the depth mask disabled) and above the physical bed model
// (offset z = -0.41), so the grid never z-fights the bed quad. Chosen by construction,
// not by magic number: it is the exact z the plate grid already uses on the shared bed.
const float z = -0.26f;
auto build_grid = [&z, &center, &bb](double step, GLModel& model) {
std::vector<std::pair<Vec2d, Vec2d>> segs;
// Constant-x (vertical on screen) lines, both directions from the centre so the
// centre column itself is always present. Clipped to the bed bounding box so nothing
// spills past the bed quad.
for (double x = center.x(); x >= bb.min.x(); x -= step)
segs.emplace_back(Vec2d(x, bb.min.y()), Vec2d(x, bb.max.y()));
for (double x = center.x() + step; x <= bb.max.x(); x += step)
segs.emplace_back(Vec2d(x, bb.min.y()), Vec2d(x, bb.max.y()));
// Constant-y (horizontal on screen) lines, same centre-first convention.
for (double y = center.y(); y >= bb.min.y(); y -= step)
segs.emplace_back(Vec2d(bb.min.x(), y), Vec2d(bb.max.x(), y));
for (double y = center.y() + step; y <= bb.max.y(); y += step)
segs.emplace_back(Vec2d(bb.min.x(), y), Vec2d(bb.max.x(), y));
GLModel::Geometry data;
data.format = { GLModel::Geometry::EPrimitiveType::Lines, GLModel::Geometry::EVertexLayout::P3 };
data.reserve_vertices(2 * segs.size());
data.reserve_indices(2 * segs.size());
for (const auto& s : segs) {
data.add_vertex(Vec3f(float(s.first.x()), float(s.first.y()), z));
data.add_vertex(Vec3f(float(s.second.x()), float(s.second.y()), z));
const unsigned int vc = static_cast<unsigned int>(data.vertices_count());
data.add_line(vc - 2, vc - 1);
}
model.init_from(std::move(data));
};
m_cad_grid_minor.reset();
m_cad_grid_major.reset();
build_grid(10.0, m_cad_grid_minor);
build_grid(50.0, m_cad_grid_major);
}
if (!m_cad_grid_minor.is_initialized() || !m_cad_grid_major.is_initialized())
return;
GLShaderProgram* shader = wxGetApp().get_shader("flat");
if (shader == nullptr)
return;
shader->start_using();
glsafe(::glEnable(GL_BLEND));
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
shader->set_uniform("view_model_matrix", view_matrix);
shader->set_uniform("projection_matrix", projection_matrix);
// White every 5 cm, grey every 1 cm — the SAME in both themes, deliberately. There is no
// "white bed" to vanish against: the plate is dark grey either way, DEFAULT_MODEL_COLOR
// {0.326,0.337,0.337} on light and DEFAULT_MODEL_COLOR_DARK {0.255,0.255,0.283} on dark
// (3DBed.cpp:185-186), a difference of 0.07. A per-theme palette here would be a branch
// that buys nothing and one more thing to keep in step.
//
// For contrast with what this replaces: the plate's own grid uses LINE_TOP_DARK_COLOR, a
// 0.43 grey, for BOTH its thin and its bold family — which is most of why the stock grid
// reads as a flat mesh with no scale to it.
const ColorRGBA minor_color(0.40f, 0.40f, 0.42f, 1.0f);
const ColorRGBA major_color(0.90f, 0.90f, 0.90f, 1.0f);
glsafe(::glLineWidth(1.0f));
m_cad_grid_minor.set_color(minor_color);
m_cad_grid_minor.render();
glsafe(::glLineWidth(2.0f));
m_cad_grid_major.set_color(major_color);
m_cad_grid_major.render();
glsafe(::glDisable(GL_BLEND));
}
void GLCanvas3D::_render_shadows(const Transform3d& view_matrix, const Transform3d& projection_matrix)
@@ -9596,6 +9925,9 @@ void GLCanvas3D::_render_separator_toolbar_left() const
void GLCanvas3D::_render_collapse_toolbar() const
{
if (!m_collapse_toolbar_enabled)
return;
auto& plater = *wxGetApp().plater();
const auto sidebar_docking_dir = plater.get_sidebar_docking_state();
if (sidebar_docking_dir == Sidebar::None) {
+44
View File
@@ -57,6 +57,9 @@ namespace GUI {
class Bed3D;
class PartPlateList;
#ifdef SLIC3R_CAD
class DesignSketchTool; // Design tab: interactive 2D sketch tool
#endif
#if ENABLE_RETINA_GL
class RetinaHelper;
@@ -542,6 +545,27 @@ private:
mutable Vec2i32 m_canvas_toolbar_pos = {140, 5};
mutable float m_sc{1};
mutable float m_paint_toolbar_width;
bool m_collapse_toolbar_enabled{true};
bool m_plate_chrome_enabled{true};
// Design tab: render the world-axis triad at the bed centre (= modeling origin) instead of
// the bed corner. Default false preserves the main editor's corner triad.
bool m_axes_at_bed_center{false};
// Design tab: draw the printer bed and its plate grid at all. Default true, so the
// main editor is untouched; the Design tab lets the user hide it to model without a bed.
bool m_show_bed{true};
// Design tab: CAD grid drawn on the bed plane in place of the plate's corner-origin grid.
// Two GLModels (10 mm minor / 50 mm major) generated from the bed centre so a line passes
// exactly through the modeling origin; built once and rebuilt only when the bed shape changes.
GLModel m_cad_grid_minor;
GLModel m_cad_grid_major;
// Geometry the CAD grid models were last built from, so they are rebuilt on bed-shape change
// rather than every frame.
BoundingBoxf m_cad_grid_bb;
Vec2d m_cad_grid_center;
bool m_cad_grid_valid{false};
#ifdef SLIC3R_CAD
DesignSketchTool* m_design_sketch_tool{nullptr};
#endif
//BBS: add canvas type for assemble view usage
ECanvasType m_canvas_type;
@@ -569,6 +593,10 @@ private:
std::array<unsigned int, 2> m_old_size{ 0, 0 };
bool m_is_touchpad_navigation{ false };
// CAD navigation (Design tab only): left-drag is a selection rubber band, so orbit moves
// to middle-drag and pan to right-drag — the Onshape/SolidWorks mapping. Off everywhere
// else, so Prepare/Preview keep the mouse the user already learned.
bool m_cad_navigation{ false };
// Screen is only refreshed from the OnIdle handler if it is dirty.
bool m_dirty;
@@ -882,6 +910,15 @@ public:
void enable_assemble_view_toolbar(bool enable);
void enable_return_toolbar(bool enable);
void enable_separator_toolbar(bool enable);
void enable_collapse_toolbar(bool enable);
void enable_plate_chrome(bool enable);
void set_axes_at_bed_center(bool b) { m_axes_at_bed_center = b; }
void set_show_bed(bool b) { m_show_bed = b; }
bool get_show_bed() const { return m_show_bed; }
#ifdef SLIC3R_CAD
void set_design_sketch_tool(DesignSketchTool* tool) { m_design_sketch_tool = tool; }
DesignSketchTool* get_design_sketch_tool() const { return m_design_sketch_tool; }
#endif
void enable_dynamic_background(bool enable) { m_dynamic_background_enabled = enable; }
void enable_labels(bool enable) { m_labels.enable(enable); }
void enable_slope(bool enable) { m_slope.enable(enable); }
@@ -1051,6 +1088,7 @@ public:
bool clicked_button_matches_action(const wxMouseEvent& evt, MouseAction action, const std::map<MouseButton, MouseAction>& mappings) const;
bool is_camera_rotate(const wxMouseEvent& evt, const std::map<MouseButton, MouseAction>& mappings) const;
bool is_camera_pan(const wxMouseEvent& evt, const std::map<MouseButton, MouseAction>& mappings) const;
void set_cad_navigation(bool b) { m_cad_navigation = b; }
Size get_canvas_size() const;
Vec2d get_local_mouse_position() const;
@@ -1190,6 +1228,8 @@ public:
bool can_sequential_clearance_show_in_gizmo();
void update_sequential_clearance();
// Orca: by-layer counterpart, for a prime tower compacted by "No sparse layers".
void update_compacted_wipe_tower_clearance();
const Print* fff_print() const;
const SLAPrint* sla_print() const;
@@ -1252,6 +1292,10 @@ private:
void _render_shadows(const Transform3d& view_matrix, const Transform3d& projection_matrix);
//BBS: add part plate related logic
void _render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true);
// Design tab: draw the CAD grid (minor 10 mm + major 50 mm) in place of the plate's
// corner-origin grid when the axes sit at the bed centre (modeling origin). Rebuilds its
// GLModels lazily, only when the bed shape changed.
void _render_cad_grid(const Transform3d& view_matrix, const Transform3d& projection_matrix);
//BBS: add outline drawing logic
void _render_objects(GLVolumeCollection::ERenderType type, bool with_outline = true);
void _render_wireframe_overlay();
+5
View File
@@ -350,6 +350,11 @@ public:
int OnExit() override;
bool initialized() const { return m_initialized; }
inline bool is_enable_multi_machine() { return this->app_config&& this->app_config->get("enable_multi_machine") == "true"; }
#ifdef SLIC3R_CAD
inline bool is_enable_cad_feature() { return this->app_config && this->app_config->get_bool("enable_cad_feature"); }
inline bool is_auto_close_sketch_loops() { return !this->app_config
|| this->app_config->get_bool("auto_close_sketch_loops"); }
#endif
std::map<std::string, bool> test_url_state;
+16 -133
View File
@@ -4,7 +4,7 @@
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Gizmos/GLGizmosCommon.hpp"
#include "libslic3r/Geometry/ConvexHull.hpp"
#include "libslic3r/LayOnFace.hpp"
#include "libslic3r/Model.hpp"
#include <numeric>
@@ -45,10 +45,10 @@ void GLGizmoFlatten::data_changed(bool is_serializing)
const ModelObject *model_object = nullptr;
int instance_id = -1;
if (selection.is_single_full_instance() ||
selection.is_from_single_object() ) {
selection.is_from_single_object() ) {
model_object = selection.get_model()->objects[selection.get_object_idx()];
instance_id = selection.get_instance_idx();
}
}
set_flattening_data(model_object, instance_id);
}
@@ -86,7 +86,7 @@ void GLGizmoFlatten::on_render()
GLShaderProgram* shader = wxGetApp().get_shader("flat");
if (shader == nullptr)
return;
shader->start_using();
glsafe(::glClear(GL_DEPTH_BUFFER_BIT));
@@ -152,134 +152,18 @@ void GLGizmoFlatten::set_flattening_data(const ModelObject* model_object, int in
void GLGizmoFlatten::update_planes()
{
const ModelObject* mo = m_c->selection_info()->model_object();
TriangleMesh ch;
for (const ModelVolume* vol : mo->volumes) {
if (vol->type() != ModelVolumeType::MODEL_PART)
continue;
TriangleMesh vol_ch = vol->get_convex_hull();
vol_ch.transform(vol->get_matrix());
ch.merge(vol_ch);
}
ch = ch.convex_hull_3d();
const Transform3d &inst_matrix = mo->instances.front()->get_matrix_no_offset();
// The candidate faces are shared with the CLI --ground-* options, the rest only prepares them for rendering.
std::vector<LayOnFacePlane> planes = lay_on_face_planes(*mo, inst_matrix);
m_planes.clear();
on_unregister_raycasters_for_picking();
const Transform3d &inst_matrix = mo->instances.front()->get_matrix_no_offset();
// Following constants are used for discarding too small polygons.
const float minimal_area = 5.f; // in square mm (world coordinates)
const float minimal_side = 1.f; // mm
const float minimal_angle = 1.f; // degree, initial value was 10, but cause bugs
// We only keep the 254 largest planes (because of the picking pass limitations):
planes.resize(std::min((int)planes.size(), 254));
// Now we'll go through all the facets and append Points of facets sharing the same normal.
// This part is still performed in mesh coordinate system.
const int num_of_facets = ch.facets_count();
const std::vector<Vec3f> face_normals = its_face_normals(ch.its);
const std::vector<Vec3i32> face_neighbors = its_face_neighbors(ch.its);
std::vector<int> facet_queue(num_of_facets, 0);
std::vector<bool> facet_visited(num_of_facets, false);
int facet_queue_cnt = 0;
const stl_normal* normal_ptr = nullptr;
int facet_idx = 0;
while (1) {
// Find next unvisited triangle:
for (; facet_idx < num_of_facets; ++ facet_idx)
if (!facet_visited[facet_idx]) {
facet_queue[facet_queue_cnt ++] = facet_idx;
facet_visited[facet_idx] = true;
normal_ptr = &face_normals[facet_idx];
m_planes.emplace_back();
break;
}
if (facet_idx == num_of_facets)
break; // Everything was visited already
while (facet_queue_cnt > 0) {
int facet_idx = facet_queue[-- facet_queue_cnt];
const stl_normal& this_normal = face_normals[facet_idx];
if (std::abs(this_normal(0) - (*normal_ptr)(0)) < 0.001 && std::abs(this_normal(1) - (*normal_ptr)(1)) < 0.001 && std::abs(this_normal(2) - (*normal_ptr)(2)) < 0.001) {
const Vec3i32 face = ch.its.indices[facet_idx];
for (int j=0; j<3; ++j)
m_planes.back().vertices.emplace_back(ch.its.vertices[face[j]].cast<double>());
facet_visited[facet_idx] = true;
for (int j = 0; j < 3; ++ j)
if (int neighbor_idx = face_neighbors[facet_idx][j]; neighbor_idx >= 0 && ! facet_visited[neighbor_idx])
facet_queue[facet_queue_cnt ++] = neighbor_idx;
}
}
m_planes.back().normal = normal_ptr->cast<double>();
Pointf3s& verts = m_planes.back().vertices;
// Now we'll transform all the points into world coordinates, so that the areas, angles and distances
// make real sense.
verts = transform(verts, inst_matrix);
// if this is a just a very small triangle, remove it to speed up further calculations (it would be rejected later anyway):
if (verts.size() == 3 &&
((verts[0] - verts[1]).norm() < minimal_side
|| (verts[0] - verts[2]).norm() < minimal_side
|| (verts[1] - verts[2]).norm() < minimal_side))
m_planes.pop_back();
}
// Let's prepare transformation of the normal vector from mesh to instance coordinates.
const Matrix3d normal_matrix = inst_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
// Now we'll go through all the polygons, transform the points into xy plane to process them:
for (unsigned int polygon_id=0; polygon_id < m_planes.size(); ++polygon_id) {
Pointf3s& polygon = m_planes[polygon_id].vertices;
const Vec3d& normal = m_planes[polygon_id].normal;
// transform the normal according to the instance matrix:
const Vec3d normal_transformed = normal_matrix * normal;
// We are going to rotate about z and y to flatten the plane
Eigen::Quaterniond q;
Transform3d m = Transform3d::Identity();
m.matrix().block(0, 0, 3, 3) = q.setFromTwoVectors(normal_transformed, Vec3d::UnitZ()).toRotationMatrix();
polygon = transform(polygon, m);
// Now to remove the inner points. We'll misuse Geometry::convex_hull for that, but since
// it works in fixed point representation, we will rescale the polygon to avoid overflows.
// And yes, it is a nasty thing to do. Whoever has time is free to refactor.
Vec3d bb_size = BoundingBoxf3(polygon).size();
float sf = std::min(1./bb_size(0), 1./bb_size(1));
Transform3d tr = Geometry::scale_transform({ sf, sf, 1.f });
polygon = transform(polygon, tr);
polygon = Slic3r::Geometry::convex_hull(polygon);
polygon = transform(polygon, tr.inverse());
// Calculate area of the polygons and discard ones that are too small
float& area = m_planes[polygon_id].area;
area = 0.f;
for (unsigned int i = 0; i < polygon.size(); i++) // Shoelace formula
area += polygon[i](0)*polygon[i + 1 < polygon.size() ? i + 1 : 0](1) - polygon[i + 1 < polygon.size() ? i + 1 : 0](0)*polygon[i](1);
area = 0.5f * std::abs(area);
bool discard = false;
if (area < minimal_area)
discard = true;
else {
// We also check the inner angles and discard polygons with angles smaller than the following threshold
const double angle_threshold = ::cos(minimal_angle * (double)PI / 180.0);
for (unsigned int i = 0; i < polygon.size(); ++i) {
const Vec3d& prec = polygon[(i == 0) ? polygon.size() - 1 : i - 1];
const Vec3d& curr = polygon[i];
const Vec3d& next = polygon[(i == polygon.size() - 1) ? 0 : i + 1];
if ((prec - curr).normalized().dot((next - curr).normalized()) > angle_threshold) {
discard = true;
break;
}
}
}
if (discard) {
m_planes[polygon_id--] = std::move(m_planes.back());
m_planes.pop_back();
continue;
}
for (LayOnFacePlane& plane : planes) {
// The outline is convex and lies in the plane frame, where the plane is horizontal.
Pointf3s& polygon = plane.outline;
// We will shrink the polygon a little bit so it does not touch the object edges:
Vec3d centroid = std::accumulate(polygon.begin(), polygon.end(), Vec3d(0.0, 0.0, 0.0));
@@ -332,13 +216,12 @@ void GLGizmoFlatten::update_planes()
b(2) += 0.1f;
// Transform back to 3D (and also back to mesh coordinates)
polygon = transform(polygon, inst_matrix.inverse() * m.inverse());
m_planes.emplace_back();
m_planes.back().normal = plane.normal;
m_planes.back().area = plane.area;
m_planes.back().vertices = transform(polygon, inst_matrix.inverse() * plane.to_plane_frame.inverse());
}
// We'll sort the planes by area and only keep the 254 largest ones (because of the picking pass limitations):
std::sort(m_planes.rbegin(), m_planes.rend(), [](const PlaneData& a, const PlaneData& b) { return a.area < b.area; });
m_planes.resize(std::min((int)m_planes.size(), 254));
// Planes are finished - let's save what we calculated it from:
m_volumes_matrices.clear();
m_volumes_types.clear();
+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/CAD/GeometryEngine.hpp"
namespace Slic3r {
namespace GUI {
class GLGizmoPrimitive : public GLGizmoBase
{
public:
GLGizmoPrimitive(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id);
~GLGizmoPrimitive() = default;
bool on_mouse(const wxMouseEvent& mouse_event) override;
protected:
bool on_init() override;
std::string on_get_name() const override;
bool on_is_activable() const override;
void on_render() override;
void on_set_state() override;
CommonGizmosDataID on_get_requirements() const override;
void on_render_input_window(float x, float y, float bottom_limit) override;
void on_load(cereal::BinaryInputArchive& ar) override;
void on_save(cereal::BinaryOutputArchive& ar) const override;
private:
void apply_primitive();
void apply_preset(const char* name, double w, double h, double d);
PrimitiveParams m_params;
TriangleMesh m_preview_mesh;
bool m_preview_dirty{true};
};
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_GLGizmoPrimitive_hpp_
+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/CAD/SketchEngine.hpp"
#include <imgui/imgui.h>
namespace Slic3r {
namespace GUI {
enum class SketchTool { Line, Rectangle, Circle, Polygon, COUNT };
class GLGizmoSketch : public GLGizmoBase
{
public:
GLGizmoSketch(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id);
bool on_mouse(const wxMouseEvent& mouse_event) override;
protected:
bool on_init() override;
std::string on_get_name() const override;
bool on_is_activable() const override;
void on_render() override;
void on_set_state() override;
CommonGizmosDataID on_get_requirements() const override;
void on_render_input_window(float x, float y, float bottom_limit) override;
void on_load(cereal::BinaryInputArchive& ar) override;
void on_save(cereal::BinaryOutputArchive& ar) const override;
private:
SketchTool m_tool{SketchTool::Line};
std::vector<SketchProfile> m_profiles; // multiple profiles (outer + holes)
SketchPlane m_plane{SketchPlane::XY()};
SketchParams m_sp;
// Shape presets
double m_rect_w{20}, m_rect_h{15};
double m_circle_r{10}; int m_circle_seg{32};
int m_poly_sides{6}; double m_poly_r{10};
// Canvas
std::vector<ImVec2> m_canvas_points;
Vec2d m_canvas_center{0,0};
float m_canvas_scale{5.0f};
bool m_snap_grid{true};
float m_grid_step{5.0f};
// Current profile being drawn
int m_active_profile{-1};
SketchProfile& active_profile();
bool has_closed_profile() const;
void build_preset_profile();
void add_closed_profile();
void delete_profile(int idx);
void clear_all();
TopoDS_Shape build_combined_shape(); // all profiles as face with holes
void apply_extrude();
void apply_revolve();
void apply_pocket();
void draw_canvas();
void handle_canvas_click(ImVec2 pos);
Vec2d snap(Vec2d pt) const;
};
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_GLGizmoSketch_hpp_
+18
View File
@@ -27,6 +27,10 @@
#include "slic3r/GUI/Gizmos/GLGizmoSVG.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoAssembly.hpp"
#ifdef SLIC3R_CAD
#include "slic3r/GUI/Gizmos/GLGizmoPrimitive.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoSketch.hpp"
#endif
#include "libslic3r/format.hpp"
#include "libslic3r/Model.hpp"
@@ -176,6 +180,14 @@ void GLGizmosManager::switch_gizmos_icon_filename()
case (EType::BrimEars):
gizmo->set_icon_filename(m_is_dark ? "toolbar_brimears_dark.svg" : "toolbar_brimears.svg");
break;
#ifdef SLIC3R_CAD
case (EType::Primitive):
gizmo->set_icon_filename(m_is_dark ? "toolbar_modifier_cube_dark.svg" : "toolbar_modifier_cube.svg");
break;
case (EType::Sketch):
gizmo->set_icon_filename(m_is_dark ? "toolbar_sketch_dark.svg" : "toolbar_sketch.svg");
break;
#endif
}
}
@@ -219,6 +231,12 @@ bool GLGizmosManager::init()
m_gizmos.emplace_back(new GLGizmoAssembly(m_parent, m_is_dark ? "toolbar_assembly_dark.svg" : "toolbar_assembly.svg", EType::Assembly));
m_gizmos.emplace_back(new GLGizmoSimplify(m_parent, "reduce_triangles.svg", EType::Simplify));
m_gizmos.emplace_back(new GLGizmoBrimEars(m_parent, m_is_dark ? "toolbar_brimears_dark.svg" : "toolbar_brimears.svg", EType::BrimEars));
#ifdef SLIC3R_CAD
// Registered last: Primitive and Sketch are the final entries before Undefined, so
// omitting them leaves every preceding m_gizmos index (indexed by EType) untouched.
m_gizmos.emplace_back(new GLGizmoPrimitive(m_parent, m_is_dark ? "toolbar_modifier_cube_dark.svg" : "toolbar_modifier_cube.svg", static_cast<unsigned int>(Primitive)));
m_gizmos.emplace_back(new GLGizmoSketch(m_parent, m_is_dark ? "toolbar_sketch_dark.svg" : "toolbar_sketch.svg", static_cast<unsigned int>(Sketch)));
#endif
//m_gizmos.emplace_back(new GLGizmoSlaSupports(m_parent, "sla_supports.svg", sprite_id++));
//m_gizmos.emplace_back(new GLGizmoFaceDetector(m_parent, "face recognition.svg", sprite_id++));
//m_gizmos.emplace_back(new GLGizmoHollow(m_parent, "hollow.svg", sprite_id++));
@@ -90,6 +90,12 @@ public:
Assembly,
Simplify,
BrimEars,
#ifdef SLIC3R_CAD
// Both need the CAD kernel (GeometryEngine); keep them last so that with
// SLIC3R_CAD off the enum matches upstream's numbering exactly.
Primitive,
Sketch,
#endif
//SlaSupports,
// BBS
//FaceRecognition,
+17
View File
@@ -505,6 +505,23 @@ bool ImGuiWrapper::update_key_data(wxKeyEvent &evt)
if (evt.GetEventType() == wxEVT_CHAR) {
// Char event
const auto key = evt.GetUnicodeKey();
// THE MEASUREMENT THAT CANNOT LIE. This is the ONLY place in the application where ImGui
// is ever handed a character, so an ImGui text field that stays empty while reporting
// itself active has exactly two possible causes, and this line separates them: no output
// at all means the wxEVT_CHAR never reached the GL canvas (a focus problem, upstream of
// ImGui entirely), while output with unicode=0 means the character arrived empty and is
// being dropped right here.
//
// It lives here rather than on the canvas because a probe bound on the canvas CANNOT
// answer this: GLCanvas3D::on_char is bound later than any constructor-time probe, wx
// runs handlers in reverse bind order, and on_char returns without Skip() whenever this
// function returns true — so such a probe stays silent whether or not the key arrived.
// A day was lost to reading that silence as evidence.
if (std::getenv("ORCA_CAD_UXTRACE")) {
fprintf(stderr, "[UX] imgui_char unicode=%d keycode=%d want_text=%d\n",
(int) key, evt.GetKeyCode(), (int) io.WantTextInput);
fflush(stderr);
}
if (key != 0) {
io.AddInputCharacter(key);
}
+57 -1
View File
@@ -38,6 +38,10 @@
#include "I18N.hpp"
#include "GLCanvas3D.hpp"
#include "Plater.hpp"
#ifdef SLIC3R_CAD
#include "slic3r/GUI/CAD/DesignPanel.hpp"
#include "slic3r/GUI/CAD/McpControl.hpp"
#endif
#include "WebViewDialog.hpp"
#include "../Utils/Process.hpp"
// BBS
@@ -1063,7 +1067,15 @@ void MainFrame::update_layout()
// Right after Home — or first, when there is no Home tab (PositionAfter() would
// append instead, and by now the other built-in tabs are already in place).
const int home_idx = m_tabpanel->FindPageByName(TAB_ID_HOME);
const size_t prepare_pos = (home_idx == wxNOT_FOUND) ? 0 : static_cast<size_t>(home_idx) + 1;
size_t prepare_pos = (home_idx == wxNOT_FOUND) ? 0 : static_cast<size_t>(home_idx) + 1;
#ifdef SLIC3R_CAD
// Design sits between Home and Prepare, so it goes in first and pushes Prepare along.
// The page only exists when the experimental CAD feature is enabled.
if (m_design_page != nullptr) {
m_design_page->Reparent(m_tabpanel);
m_tabpanel->InsertPage(prepare_pos++, TAB_ID_DESIGN, m_design_page, _L("Design"), "tab_design_active");
}
#endif
m_tabpanel->InsertPage(prepare_pos, TAB_ID_PREPARE, m_plater, _L("Prepare"), "tab_3d_active");
m_tabpanel->InsertPage(prepare_pos + 1, TAB_ID_PREVIEW, m_plater, _L("Preview"), "tab_preview_active");
m_main_sizer->Add(m_tabpanel, 1, wxEXPAND | wxTOP, 0);
@@ -1281,6 +1293,19 @@ void MainFrame::show_option(bool show)
}
}
#ifdef SLIC3R_CAD
DesignPanel* MainFrame::ensure_design_panel()
{
if (m_design_panel == nullptr && m_design_page != nullptr) {
wxBusyCursor busy;
m_design_panel = new DesignPanel(m_design_page);
m_design_page->GetSizer()->Add(m_design_panel, 1, wxEXPAND);
m_design_page->Layout();
}
return m_design_panel;
}
#endif
void MainFrame::init_tabpanel() {
// wxNB_NOPAGETHEME: Disable Windows Vista theme for the Notebook background. The theme performance is terrible on
// Windows 10 with multiple high resolution displays connected.
@@ -1321,9 +1346,26 @@ void MainFrame::init_tabpanel() {
}
//else if (panel == m_param_panel)
// m_param_panel->OnActivate();
#ifdef SLIC3R_CAD
else if (m_design_page != nullptr && panel == m_design_page) {
// Built on first activation, never at startup: the panel creates several hundred
// controls and its own GL canvas, which a user who does not open the tab should
// not pay for.
ensure_design_panel();
// Re-sync the Design bed to the active printer: the panel is built before the
// printer profile is fully applied, so its bed must refresh on activation or the
// grid (true bed) spills past the stale default bed quad.
m_design_panel->on_tab_shown();
}
#endif
else if (panel == m_monitor) {
//monitor
}
#ifdef SLIC3R_CAD
// Any page that is not Design takes the Design status line down with it — see
// DesignPanel::on_tab_hidden for why the popup does not follow the page on its own.
if (m_design_panel != nullptr && panel != m_design_page) m_design_panel->on_tab_hidden();
#endif
#ifndef __APPLE__
if (m_last_selected_tab == TAB_ID_PREPARE) {
m_topbar->EnableUndoRedoItems();
@@ -1354,6 +1396,20 @@ void MainFrame::init_tabpanel() {
wxGetApp().plater_ = m_plater;
#ifdef SLIC3R_CAD
// Stand-in page for the Design tab. The real DesignPanel is built into it the first time
// the tab is selected (see the page-changed handler above), so nothing it constructs sits
// on the startup path. The experimental feature is off by default, and when it is off the
// page is never created, so the tab does not appear at all (the preference takes effect on
// the next start, like the other feature toggles).
if (wxGetApp().is_enable_cad_feature()) {
m_design_page = new wxPanel(this);
m_design_page->SetSizer(new wxBoxSizer(wxVERTICAL));
m_design_page->Hide();
start_mcp_control_if_enabled(); // opens the MCP socket iff ORCA_CAD_MCP is set
}
#endif
create_preset_tabs();
//BBS add pages
+17
View File
@@ -40,6 +40,9 @@
// Stable identifiers for MainFrame::m_tabpanel's built-in pages. These are
// names rather than positional indices so optional pages cannot shift them.
#define TAB_ID_HOME "home"
#ifdef SLIC3R_CAD
#define TAB_ID_DESIGN "design"
#endif
#define TAB_ID_PREPARE "prepare"
#define TAB_ID_PREVIEW "preview"
#define TAB_ID_MONITOR "monitor"
@@ -65,6 +68,9 @@ namespace GUI
class Tab;
class PrintHostQueueDialog;
class Plater;
#ifdef SLIC3R_CAD
class DesignPanel;
#endif
class MainFrame;
class WebViewPanel;
class ParamsDialog;
@@ -403,6 +409,17 @@ public:
BBLTopbar* m_topbar{ nullptr };
PrintHostQueueDialog* printhost_queue_dlg() { return m_printhost_queue_dlg; }
Plater* m_plater { nullptr };
#ifdef SLIC3R_CAD
// The tab page is the placeholder; m_design_panel stays null until the tab is first
// selected, so everything the Design panel builds stays off the startup path.
wxPanel* m_design_page { nullptr };
DesignPanel* m_design_panel { nullptr };
// Builds the Design panel if it does not exist yet and returns it (null only before the
// placeholder page itself exists). Main thread only -- it creates wx controls. Both the
// tab activation and the MCP socket go through this: the socket is driven headlessly,
// with nobody to click the tab, and without this every verb would answer "not ready".
DesignPanel* ensure_design_panel();
#endif
//BBS: GUI refactor
MonitorPanel* m_monitor{ nullptr };
+19 -11
View File
@@ -1053,7 +1053,13 @@ void PartPlate::render_grid(bool bottom) {
void PartPlate::render_height_limit(PartPlate::HeightLimitMode mode)
{
if (m_print && m_print->config().print_sequence == PrintSequence::ByObject && mode != HEIGHT_LIMIT_NONE)
// Orca: a prime tower compacted by "No sparse layers" drags the nozzle back down to the plate on
// every toolchange, so the rod and the lid limit how tall a neighbouring object may be exactly as
// they do in sequential printing. The reference lines are just as useful there.
const bool relevant_for_print_mode = m_print && (m_print->config().print_sequence == PrintSequence::ByObject ||
(m_print->config().print_sequence == PrintSequence::ByLayer &&
wipe_tower_sparse_layers_skipped(m_print->config()) && m_print->has_wipe_tower()));
if (relevant_for_print_mode && mode != HEIGHT_LIMIT_NONE)
{
// draw lower limit
// ORCA: OpenGL Core Profile
@@ -3501,7 +3507,7 @@ bool PartPlate::intersects(const BoundingBoxf3& bb) const
return print_volume.intersects(bb);
}
void PartPlate::render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_body, bool force_background_color, HeightLimitMode mode, int hover_id, bool render_cali, bool show_grid)
void PartPlate::render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_body, bool force_background_color, HeightLimitMode mode, int hover_id, bool render_cali, bool show_grid, bool hide_chrome)
{
glsafe(::glEnable(GL_DEPTH_TEST));
@@ -3552,16 +3558,18 @@ void PartPlate::render(const Transform3d& view_matrix, const Transform3d& projec
if (wxGetApp().show_plate_gridlines() && show_grid)
render_grid(bottom);
if (!bottom && m_selected && !force_background_color) {
if (!hide_chrome && !bottom && m_selected && !force_background_color) {
if (m_partplate_list)
render_logo(bottom, m_partplate_list->render_cali_logo && render_cali);
else
render_logo(bottom);
}
render_icons(bottom, only_body, hover_id);
if (!force_background_color) {
render_only_numbers(bottom);
if (!hide_chrome) {
render_icons(bottom, only_body, hover_id);
if (!force_background_color) {
render_only_numbers(bottom);
}
}
glsafe(::glDisable(GL_DEPTH_TEST));
@@ -5956,7 +5964,7 @@ void PartPlateList::postprocess_arrange_polygon(arrangement::ArrangePolygon& arr
/*rendering related functions*/
//render
void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid)
void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid, bool hide_chrome)
{
const std::lock_guard<std::mutex> local_lock(m_plates_mutex);
std::vector<PartPlate*>::iterator it = m_plate_list.begin();
@@ -5981,15 +5989,15 @@ void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& pr
if (current_index == m_current_plate) {
PartPlate::HeightLimitMode height_mode = (only_current)?PartPlate::HEIGHT_LIMIT_NONE:m_height_limit_mode;
if (plate_hover_index == current_index)
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, height_mode, plate_hover_action, render_cali, show_grid);
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, height_mode, plate_hover_action, render_cali, show_grid, hide_chrome);
else
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, height_mode, -1, render_cali, show_grid);
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, height_mode, -1, render_cali, show_grid, hide_chrome);
}
else {
if (plate_hover_index == current_index)
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, PartPlate::HEIGHT_LIMIT_NONE, plate_hover_action, render_cali, show_grid);
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, PartPlate::HEIGHT_LIMIT_NONE, plate_hover_action, render_cali, show_grid, hide_chrome);
else
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, PartPlate::HEIGHT_LIMIT_NONE, -1, render_cali, show_grid);
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, PartPlate::HEIGHT_LIMIT_NONE, -1, render_cali, show_grid, hide_chrome);
}
}
}
+2 -2
View File
@@ -428,7 +428,7 @@ public:
bool contains(const BoundingBoxf3& bb) const;
bool intersects(const BoundingBoxf3& bb) const;
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_body = false, bool force_background_color = false, HeightLimitMode mode = HEIGHT_LIMIT_NONE, int hover_id = -1, bool render_cali = false, bool show_grid = true);
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_body = false, bool force_background_color = false, HeightLimitMode mode = HEIGHT_LIMIT_NONE, int hover_id = -1, bool render_cali = false, bool show_grid = true, bool hide_chrome = false);
void set_selected();
void set_unselected();
@@ -857,7 +857,7 @@ public:
/*rendering related functions*/
void on_change_color_mode(bool is_dark) { m_is_dark = is_dark; }
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current = false, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true);
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current = false, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true, bool hide_chrome = false);
void set_render_option(bool bedtype_texture, bool plate_settings);
void set_render_cali(bool value = true) { render_cali_logo = value; }
void register_raycasters_for_picking(GLCanvas3D& canvas)
+51 -3
View File
@@ -91,6 +91,9 @@
#include "wxExtensions.hpp"
#include "../Utils/PrintHost.hpp"
#include "MainFrame.hpp"
#ifdef SLIC3R_CAD
#include "slic3r/GUI/CAD/DesignPanel.hpp"
#endif
#include "format.hpp"
#include "3DScene.hpp"
#include "GLCanvas3D.hpp"
@@ -8328,6 +8331,9 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
int answer_convert_from_meters = wxOK_DEFAULT;
int answer_convert_from_imperial_units = wxOK_DEFAULT;
int tolal_model_count = 0;
// Whether one of the files being loaded here carried a CAD recipe. A statement about these
// files, not about the plater — q->model() may still hold the previous project's recipe.
bool loaded_cad_recipe = false;
int progress_percent = 0;
int total_files = input_files.size();
@@ -9453,6 +9459,16 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
auto loaded_idxs = load_model_objects(model.objects, is_project_file);
obj_idxs.insert(obj_idxs.end(), loaded_idxs.begin(), loaded_idxs.end());
// load_model_objects only transfers ModelObjects; carry the Model-level CAD recipe
// onto the plater model so the Design tab can rehydrate the editable feature tree on
// reopen. Assigned unconditionally on the project-replacing path so that opening a
// project without a recipe clears whatever the previous one left behind; importing a
// plain model into the open project leaves the current recipe alone.
if (is_project_file) {
q->model().cad_recipe = model.cad_recipe;
loaded_cad_recipe = !model.cad_recipe.empty();
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", finished load_model_objects");
wxString msg = wxString::Format(_L("Loading file: %s"), from_path(real_filename));
dlg_cont = dlg.Update(progress_percent, msg);
@@ -9669,7 +9685,12 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
// q->model().stl_design_country = "";
//}
if (tolal_model_count <= 0 && !q->m_exported_file) {
// A CAD project legitimately carries no mesh: the model lives in the feature tree until it is
// committed to the plate. Warning "no geometry data" for one is false, and it is the LAST thing
// a user sees after opening a design they spent an hour on — it reads as "your work is gone"
// when the recipe has in fact just been loaded and the Design tab will rehydrate it. Count a
// recipe that came from THESE files as geometry.
if (tolal_model_count <= 0 && !loaded_cad_recipe && !q->m_exported_file) {
dlg.Hide();
if (!is_user_cancel) {
MessageDialog msg(wxGetApp().mainframe, _L("The file does not contain any geometry data."), _L("Warning"), wxYES | wxICON_WARNING);
@@ -10216,6 +10237,16 @@ void Plater::priv::reset(bool apply_presets_change)
// Stop and reset the Print content.
this->background_process.reset();
model.clear_objects();
// clear_objects() only drops the ModelObjects; the CAD recipe is Model-level state and would
// otherwise be written into every project saved for the rest of the session.
model.cad_recipe.clear();
#ifdef SLIC3R_CAD
// Same reason, one level up: the Design tab keeps the editable document, not the Model, so
// clearing the recipe alone leaves the tab showing the previous project's feature tree —
// and its next edit syncs that tree straight back into the new project.
if (wxGetApp().mainframe != nullptr && wxGetApp().mainframe->m_design_panel != nullptr)
wxGetApp().mainframe->m_design_panel->clear_document();
#endif
assemble_view->get_canvas3d()->reset_explosion_ratio();
update();
@@ -13709,6 +13740,14 @@ void Plater::priv::unbind_canvas_event_handlers()
if (assemble_view != nullptr)
assemble_view->get_canvas3d()->unbind_event_handlers();
#ifdef SLIC3R_CAD
// The Design tab's viewport is a fourth GLCanvas3D on the same shared GL context, owned by
// MainFrame rather than by us — same reach as reset() uses for clear_document(). Null until
// the tab has been opened once, so most sessions skip it.
if (wxGetApp().mainframe != nullptr && wxGetApp().mainframe->m_design_panel != nullptr)
wxGetApp().mainframe->m_design_panel->unbind_canvas_event_handlers();
#endif
}
void Plater::priv::reset_canvas_volumes()
@@ -13718,6 +13757,11 @@ void Plater::priv::reset_canvas_volumes()
if (preview != nullptr)
preview->get_canvas3d()->reset_volumes();
#ifdef SLIC3R_CAD
if (wxGetApp().mainframe != nullptr && wxGetApp().mainframe->m_design_panel != nullptr)
wxGetApp().mainframe->m_design_panel->reset_canvas_volumes();
#endif
}
bool Plater::priv::check_ams_status_impl(bool is_slice_all)
@@ -15669,8 +15713,12 @@ bool Plater::up_to_date(bool saved, bool backup)
Slic3r::clear_other_changes(backup);
return p->up_to_date(saved, backup);
}
return p->model.objects.empty() || (p->up_to_date(saved, backup) &&
!Slic3r::has_other_changes(backup));
// A Design-tab project is object-less until it is committed to the plate, but its feature
// tree is real work: treating it as an empty project skipped both the autosave and the
// "unsaved changes" prompt, so quitting threw it away without asking. Non-CAD projects
// never carry a recipe, so the empty-project shortcut is unchanged for them.
return (p->model.objects.empty() && p->model.cad_recipe.empty()) ||
(p->up_to_date(saved, backup) && !Slic3r::has_other_changes(backup));
}
bool Plater::add_model(bool imperial_units, std::string fname)
+33 -1
View File
@@ -9,6 +9,7 @@
#include "I18N.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Format/DRC.hpp"
#include "libslic3r/CAD/SketchEngine.hpp"
#include <wx/language.h>
#include "OG_CustomCtrl.hpp"
#include "wx/graphics.h"
@@ -367,7 +368,8 @@ wxBoxSizer *PreferencesDialog::create_item_language_combobox(wxString title, wxS
wxLANGUAGE_PORTUGUESE_BRAZILIAN,
wxLANGUAGE_LITHUANIAN,
wxLANGUAGE_VIETNAMESE,
wxLANGUAGE_THAI
wxLANGUAGE_THAI,
wxLANGUAGE_ROMANIAN
};
auto translations = wxTranslations::Get()->GetAvailableTranslations(SLIC3R_APP_KEY);
@@ -1739,6 +1741,21 @@ void PreferencesDialog::create_items()
SPEED_DIAL_RECENT_COUNT_MAX);
g_sizer->Add(item_speed_dial_recents);
#ifdef SLIC3R_CAD
auto item_cad_feature = create_item_checkbox(_L("CAD feature (experimental)"),
_L("With this option enabled, the Design tab is shown, where models can be built and edited "
"parametrically. This feature is experimental and still under development."),
"enable_cad_feature", _L("(Requires restart)"));
g_sizer->Add(item_cad_feature);
auto item_auto_close_sketch_loops = create_item_checkbox(_L("Auto-close sketch loops"),
_L("Treat sketch endpoints within 0.001 mm as one joint and weld the loop shut. "
"Off: only exactly coincident endpoints join, so a loop with a tiny gap is "
"shown as open instead of being closed for you."),
"auto_close_sketch_loops");
g_sizer->Add(item_auto_close_sketch_loops);
#endif
#if 0
g_sizer->Add(create_item_title(_L("Filament Grouping")), 1, wxEXPAND);
//temporarily disable it
@@ -1815,6 +1832,21 @@ void PreferencesDialog::create_items()
auto reverse_mouse_zoom = create_item_checkbox(_L("Reverse mouse zoom"), _L("If enabled, reverses the direction of zoom with mouse wheel."), "reverse_mouse_wheel_zoom");
g_sizer->Add(reverse_mouse_zoom);
#ifdef SLIC3R_CAD
// Design-tab only, so it stays out of the way while the CAD feature is switched off.
if (wxGetApp().is_enable_cad_feature()) {
auto item_connector_face_glyph = create_item_checkbox(_L("Draw mate connectors as a face"),
_L("In the Design tab, draw a mate connector as a small face instead of the conventional "
"disc with a roll quadrant. A face's orientation is read without being learned. "
"Turn this off for the conventional CAD representation."), "design_connector_face_glyph");
g_sizer->Add(item_connector_face_glyph);
}
// Push the weld preference into the kernel now so toggling it takes effect without
// a restart (the sketch tool also re-pushes on activation, see DesignSketchTool::begin).
Slic3r::set_sketch_auto_close(wxGetApp().is_auto_close_sketch_loops());
#endif
std::vector<wxString> ButtonDragActions = {_L("None"), _L("Pan"), _L("Rotate")};
auto item_left_mouse_drag = create_item_combobox(_L("Left Mouse Drag"), _L("Set the action that dragging the left mouse button should perform."), "left_mouse_drag_action", ButtonDragActions);
g_sizer->Add(item_left_mouse_drag);
+70 -37
View File
@@ -26,6 +26,7 @@
#include <wx/dcmemory.h>
#include <wx/dcgraph.h>
#include <wx/image.h>
#include <wx/wrapsizer.h>
#include <set>
#include <algorithm>
#include <cmath>
@@ -416,12 +417,54 @@ std::set<size_t> project_used_filament_slots(const PresetBundle& bundle, const D
return used;
}
// Lays a translated sentence out along `row`, replacing each "%1%"-style placeholder with the
// matching window from `chips`. Keeping the sentence in one msgid lets a translation put the
// placeholders wherever its own grammar needs them; spacing comes from the translation itself.
void add_sentence_with_chips(wxWindow* parent, wxSizer* row, const wxString& sentence, const std::vector<wxWindow*>& chips)
{
std::vector<bool> placed(chips.size(), false);
auto add_text = [&](wxString text) {
text.Replace("%%", "%"); // the sentence is a format string
if (text.IsEmpty())
return;
auto* label = new wxStaticText(parent, wxID_ANY, text);
label->SetFont(Label::Body_12);
label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
row->Add(label, 0, wxALIGN_CENTER_VERTICAL);
};
auto add_chip = [&](size_t i) {
if (i < chips.size() && chips[i] != nullptr && !placed[i]) {
placed[i] = true;
row->Add(chips[i], 0, wxALIGN_CENTER_VERTICAL);
}
};
size_t literal = 0, pos = 0;
while ((pos = sentence.find('%', pos)) != wxString::npos) {
size_t end = pos + 1;
while (end < sentence.length() && sentence[end] >= '0' && sentence[end] <= '9')
++end;
if (end == pos + 1 || end >= sentence.length() || sentence[end] != '%') {
++pos; // a bare '%'
continue;
}
long index = 0;
sentence.Mid(pos + 1, end - pos - 1).ToLong(&index);
add_text(sentence.Mid(literal, pos - literal));
add_chip(size_t(index - 1));
literal = pos = end + 1;
}
add_text(sentence.Mid(literal));
for (size_t i = 0; i < chips.size(); ++i) // whatever the translation left out
add_chip(i);
}
} // namespace
// Warning shown on OK when an enabled mixed-filament slot relies on a filament that would ship
// without its material. One row per unmet dependency: the mixed slot's colour chip, the
// component filament's colour chip, and the reason. "Cancel" is the safe choice and keeps the
// dialog open; "Publish anyway" continues.
// without its material. One row per unmet dependency, each a single translated sentence whose
// two placeholders are the mixed slot's and the component filament's colour chips. "Cancel" is
// the safe choice and keeps the dialog open; "Publish anyway" continues.
class MixedFilamentWarningDialog : public MsgDialog
{
public:
@@ -437,47 +480,37 @@ public:
content->AddSpacer(FromDIP(10));
const int swatch = FromDIP(20);
for (const MixedDependencyIssue& issue : issues) {
auto* row = new wxBoxSizer(wxHORIZONTAL);
// The mixed slot as just its own chip (gradient-aware, numbered like its tab);
// falls back to a plain label when the chip cannot be built.
const wxString mix_label = wxString::Format(_L("Filament %d (mixed)"), int(issue.mixed_slot) + 1);
const wxBitmap mix_bmp = mixed_filament_chip_bitmap(full, issue.mixed_slot, swatch);
if (mix_bmp.IsOk()) {
auto* bmp = new wxStaticBitmap(this, wxID_ANY, mix_bmp);
bmp->SetToolTip(mix_label);
row->Add(bmp, 0, wxALIGN_CENTER_VERTICAL);
} else {
auto* label = new wxStaticText(this, wxID_ANY, mix_label);
label->SetFont(Label::Body_12);
row->Add(label, 0, wxALIGN_CENTER_VERTICAL);
// The slot's colour swatch, numbered like its tab, with the slot name on hover; falls
// back to a label so the sentence always names both filaments.
auto make_chip = [&](const wxBitmap& bmp, const wxString& name) -> wxWindow* {
if (bmp.IsOk()) {
auto* chip = new wxStaticBitmap(this, wxID_ANY, bmp);
chip->SetToolTip(name);
return chip;
}
auto* label = new wxStaticText(this, wxID_ANY, name);
label->SetFont(Label::Body_12);
return label;
};
auto* needs = new wxStaticText(this, wxID_ANY, _L("needs"));
needs->SetFont(Label::Body_12);
needs->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
row->Add(needs, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, FromDIP(6));
// The component filament's colour chip, numbered like the tab strips; the slot
// name stays on hover to keep the row itself short.
for (const MixedDependencyIssue& issue : issues) {
std::string hex = filament_color_hex(full, issue.component_slot);
if (hex.empty())
hex = "#D9D9D9";
if (wxBitmap* chip = get_extruder_color_icon(hex, std::to_string(issue.component_slot + 1), swatch, swatch)) {
auto* comp_bmp = new wxStaticBitmap(this, wxID_ANY, *chip);
comp_bmp->SetToolTip(wxString::Format(_L("Filament %d"), int(issue.component_slot) + 1));
row->Add(comp_bmp, 0, wxALIGN_CENTER_VERTICAL);
}
const wxBitmap* comp_bmp = get_extruder_color_icon(hex, std::to_string(issue.component_slot + 1), swatch, swatch);
auto* reason = new wxStaticText(this, wxID_ANY,
issue.reason == MixedDependencyIssue::Reason::Disabled ? _L("not enabled") :
_L("material not published"));
reason->SetFont(Label::Body_12);
reason->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#989898")));
row->Add(reason, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
wxWindow* mix_chip = make_chip(mixed_filament_chip_bitmap(full, issue.mixed_slot, swatch),
wxString::Format(_L("Filament %d (mixed)"), int(issue.mixed_slot) + 1));
wxWindow* comp_chip = make_chip(comp_bmp != nullptr ? *comp_bmp : wxNullBitmap,
wxString::Format(_L("Filament %d"), int(issue.component_slot) + 1));
content->Add(row, 0, wxLEFT, FromDIP(10));
auto* row = new wxWrapSizer(wxHORIZONTAL);
add_sentence_with_chips(this, row,
issue.reason == MixedDependencyIssue::Reason::Disabled ?
_L("%1% needs %2%, which is not enabled.") :
_L("%1% needs %2%, whose material will not be published."),
{mix_chip, comp_chip});
content->Add(row, 0, wxEXPAND | wxLEFT, FromDIP(10));
content->AddSpacer(FromDIP(6));
}
+34 -16
View File
@@ -22,7 +22,7 @@ namespace Search {
static std::string get_key(const std::string &opt_key, Preset::Type type) { return std::to_string(int(type)) + ";" + opt_key; }
std::string Option::opt_key() const { return into_u8(key).substr(2); }
std::string Option::opt_key() const { return key.size() < 2 ? std::string() : into_u8(key).substr(2); }
template<class T>
// void change_opt_key(std::string& opt_key, DynamicPrintConfig* config)
@@ -79,6 +79,7 @@ void SettingsIndex::append_options(DynamicPrintConfig *config, Preset::Type type
case coFloats: change_opt_key<ConfigOptionFloats>(opt_key, config, cnt); break;
case coStrings: change_opt_key<ConfigOptionStrings>(opt_key, config, cnt); break;
case coPercents: change_opt_key<ConfigOptionPercents>(opt_key, config, cnt); break;
case coFloatsOrPercents: change_opt_key<ConfigOptionVector<FloatOrPercent>>(opt_key, config, cnt); break;
case coPoints: change_opt_key<ConfigOptionPoints>(opt_key, config, cnt); break;
// BBS
case coEnums: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break;
@@ -155,29 +156,46 @@ bool SettingsIndex::apply(DynamicPrintConfig *config, Preset::Type type, ConfigO
const Option &SettingsIndex::get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const
{
auto not_found = [&variant_index]() -> const Option & {
static const Option empty_option;
variant_index = -2;
return empty_option;
};
variant_index = -1;
std::string opt_key2 = opt_key;
if (auto n = opt_key.find('#'); n != std::string::npos) {
variant_index = std::atoi(opt_key.c_str() + n + 1);
opt_key2 = opt_key.substr(0, n);
}
auto it = std::lower_bound(m_options.begin(), m_options.end(), Option({boost::nowide::widen(get_key(opt_key2, type))}));
// BBS: return the 0th option when not found in searcher caused by mode difference
// assert(it != options.end());
if (it == m_options.end()) { variant_index = -2 ; return m_options[0]; }
if (it->opt_key() == opt_key2) {
const std::wstring key = boost::nowide::widen(get_key(opt_key2, type));
auto it = std::lower_bound(m_options.begin(), m_options.end(), Option({key}));
if (it == m_options.end()) return not_found();
if (it->key == key) {
variant_index = -1;
} else {
const std::string opt_key3 = opt_key2 + "#";
it = std::lower_bound(it, m_options.end(), Option({boost::nowide::widen(get_key(opt_key3, type))}));
if (it == m_options.end() || it->opt_key().compare(0, opt_key3.length(), opt_key3) != 0) {
variant_index = -2; // Not found
return m_options[0];
const std::wstring prefix = key + L"#";
it = std::lower_bound(it, m_options.end(), Option({prefix}));
if (it == m_options.end() || it->key.compare(0, prefix.length(), prefix) != 0)
return not_found();
// Orca: Copy-parameters dialogs request the base key, without a vector index.
if (variant_index < 0) return *it;
const bool has_mode = type == Preset::TYPE_PRINTER && printer_options_with_variant_2.count(opt_key2) > 0;
const bool has_variant =
(type == Preset::TYPE_PRINT && print_options_with_variant.count(opt_key2) > 0) ||
(type == Preset::TYPE_FILAMENT && filament_options_with_variant.count(opt_key2) > 0) ||
(type == Preset::TYPE_PRINTER && printer_options_with_variant_1.count(opt_key2) > 0) || has_mode;
if (!has_variant || has_mode) {
// Orca: Machine limits store (Normal, Silent) pairs per variant; the UI registers only #0/#1.
const std::wstring indexed_key = has_mode ? prefix + std::to_wstring(variant_index % 2) :
boost::nowide::widen(get_key(opt_key, type));
it = std::lower_bound(it, m_options.end(), Option({indexed_key}));
if (it == m_options.end() || it->key != indexed_key)
return not_found();
if (!has_variant)
variant_index = -1;
}
auto it2 = it;
++it2;
if (it2 != m_options.end() && it2->opt_key().compare(0, opt_key3.length(), opt_key3) == 0
&& printer_options_with_variant_1.find(opt_key2) == printer_options_with_variant_1.end())
variant_index = -2;
}
return m_options[it - m_options.begin()];
+35
View File
@@ -2022,6 +2022,20 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
// reload scene to update timelapse wipe tower
if (opt_key == "timelapse_type") {
// Smooth timelapse parks the nozzle on the prime tower every layer, so it needs a tower on
// every layer. That is exactly what "No sparse layers" removes, and with both on the tower is
// planned full height and then dropped on emission. Drop "No sparse layers" and tell the user.
if (boost::any_cast<int>(value) == (int) TimelapseType::tlSmooth && m_config->opt_bool("wipe_tower_no_sparse_layers")) {
MessageDialog dlg(wxGetApp().plater(),
_L("Smooth timelapse needs a prime tower on every layer, which is not compatible with \"No sparse layers\". "
"\"No sparse layers\" has been turned off."),
_L("Warning"), wxICON_WARNING | wxOK);
dlg.ShowModal();
DynamicPrintConfig new_conf = *m_config;
new_conf.set_key_value("wipe_tower_no_sparse_layers", new ConfigOptionBool(false));
m_config_manipulation.apply(m_config, &new_conf);
}
bool wipe_tower_enabled = m_config->option<ConfigOptionBool>("enable_prime_tower")->value;
if (!wipe_tower_enabled && boost::any_cast<int>(value) == (int)TimelapseType::tlSmooth) {
MessageDialog dlg(wxGetApp().plater(), _L("A prime tower is required for smooth timelapse mode. There may be flaws on the model without prime tower. Do you want to enable the prime tower\?"),
@@ -2037,6 +2051,23 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
}
}
// Mirror of the timelapse_type branch above: enabling "No sparse layers" while smooth timelapse
// is active would leave the tower on every layer anyway, so fall back to traditional timelapse.
if (opt_key == "wipe_tower_no_sparse_layers" && boost::any_cast<bool>(value)) {
auto timelapse_type = m_config->option<ConfigOptionEnum<TimelapseType>>("timelapse_type");
if (timelapse_type && timelapse_type->value == TimelapseType::tlSmooth) {
MessageDialog dlg(wxGetApp().plater(),
_L("\"No sparse layers\" is not compatible with smooth timelapse, which needs a prime tower on every layer. "
"Timelapse has been switched to traditional mode."),
_L("Warning"), wxICON_WARNING | wxOK);
dlg.ShowModal();
DynamicPrintConfig new_conf = *m_config;
new_conf.set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(TimelapseType::tlTraditional));
m_config_manipulation.apply(m_config, &new_conf);
wxGetApp().plater()->update();
}
}
if (opt_key == "print_sequence" && m_config->opt_enum<PrintSequence>("print_sequence") == PrintSequence::ByObject) {
auto printer_structure_opt = m_preset_bundle->printers.get_edited_preset().config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure");
if (printer_structure_opt && printer_structure_opt->value == PrinterStructure::psI3) {
@@ -2793,6 +2824,7 @@ void TabPrint::build()
optgroup = page->new_optgroup(L("Overhangs"), L"param_overhang");
optgroup->append_single_option_line("detect_overhang_wall", "quality_settings_overhangs#detect-overhang-wall");
optgroup->append_single_option_line("unsupported_wall_last", "quality_settings_overhangs#unsupported-wall-last");
optgroup->append_single_option_line("make_overhang_printable", "quality_settings_overhangs#make-overhang-printable");
optgroup->append_single_option_line("make_overhang_printable_angle", "quality_settings_overhangs#maximum-angle");
optgroup->append_single_option_line("make_overhang_printable_hole_size", "quality_settings_overhangs#hole-area");
@@ -3056,6 +3088,8 @@ void TabPrint::build()
optgroup = page->new_optgroup(L("Advanced"), L"advanced");
optgroup->append_single_option_line("interlocking_beam", "multimaterial_settings_advanced#interlocking-beam");
optgroup->append_single_option_line("toolchange_ordering", "multimaterial_settings_advanced#toolchange-ordering");
optgroup->append_single_option_line("toolchange_cyclic_order", "multimaterial_settings_advanced#toolchange-order");
optgroup->append_single_option_line("toolchange_cyclic_first_layer", "multimaterial_settings_advanced#toolchange-order");
optgroup->append_single_option_line("interface_shells", "multimaterial_settings_advanced#interface-shells");
optgroup->append_single_option_line("mmu_segmented_region_max_width", "multimaterial_settings_advanced#maximum-width-of-segmented-region");
optgroup->append_single_option_line("mmu_segmented_region_interlocking_depth", "multimaterial_settings_advanced#interlocking-depth-of-segmented-region");
@@ -5137,6 +5171,7 @@ void TabPrinter::build_fff()
optgroup = page->new_optgroup(L("Extruder Clearance"), "param_extruder_clearance");
optgroup->append_single_option_line("extruder_clearance_radius", "printer_basic_information_extruder_clearance#radius");
optgroup->append_single_option_line("extruder_clearance_dist_to_rod", "printer_basic_information_extruder_clearance#distance-to-rod");
optgroup->append_single_option_line("extruder_clearance_height_to_rod", "printer_basic_information_extruder_clearance#height-to-rod");
optgroup->append_single_option_line("extruder_clearance_height_to_lid", "printer_basic_information_extruder_clearance#height-to-lid");
+38 -16
View File
@@ -1490,7 +1490,15 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, DynamicConfig * config
for (const std::string &opt_key : config->keys()) {
int variant_index = -2;
const Search::Option &option = index.get_option(opt_key, type, variant_index);
Search::Option option = index.get_option(opt_key, type, variant_index);
if (variant_index == -2) {
// Orca: Every transferred setting must remain visible even when it is absent from the search index.
const ConfigOptionDef* def = print_config_def.get(opt_key);
const std::string label = def ? (def->full_label.empty() ? def->label : def->full_label) : std::string();
option.label_local = (label.empty() ? from_u8(opt_key) : _L(label)).ToStdWstring();
option.category_local = (def && !def->category.empty() ?
Tab::translate_category(from_u8(def->category), type) : _L("Others")).ToStdWstring();
}
auto category = option.category_local;
auto opt = dynamic_cast<ConfigOptionVectorBase*>(config->option(opt_key));
std::string value_from = opt->vserialize()[from];
@@ -1518,6 +1526,8 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres
else
presets_list.emplace_back(presets_);
const bool multiple_extruders = wxGetApp().preset_bundle->get_printer_extruder_count() > 1;
// Display a dialog showing the dirty options in a human readable form.
for (PresetCollection* presets : presets_list)
{
@@ -1553,29 +1563,41 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres
auto variant_key = Preset::get_iot_type_string(type) + "_extruder_variant";
auto id_key = Preset::get_iot_type_string(type) + "_extruder_id";
auto extruder_variant = dynamic_cast<ConfigOptionStrings const *>(old_config.option(variant_key));
auto extruder_id = dynamic_cast<ConfigOptionInts const *>(old_config.option(id_key));
// Orca: Dirty indices belong to the edited config, which may contain newly added variants.
auto extruder_variant = dynamic_cast<ConfigOptionStrings const *>(new_config.option(variant_key));
auto extruder_id = dynamic_cast<ConfigOptionInts const *>(new_config.option(id_key));
for (const std::string& opt_key : dirty_options) {
int variant_index = -2;
const Search::Option &option = index.get_option(opt_key, type, variant_index);
if (option.opt_key() != opt_key && variant_index < -1) {
if (variant_index == -2) {
// When founded option isn't the correct one.
// It can be for dirty_options: "default_print_profile", "printer_model", "printer_settings_id",
// because of they don't exist in the index
continue;
}
auto category = option.category_local;
if (variant_index >= 0) {
if (printer_options_with_variant_2.count(opt_key.substr(0, opt_key.find_last_of('#'))) > 0)
variant_index /= 2;
if (boost::nowide::narrow(category).find("Extruder ") == 0)
category = category.substr(0, 8);
if (extruder_id)
category = category + (wxString(" {") + (extruder_id->values[variant_index] == 1 ? _L("Left: ") : _L("Right: "))
+ L(extruder_variant->values[variant_index]) + "}");
else
category = category + (wxString(" {") + L(extruder_variant->values[variant_index]) + "}");
wxString category = option.category_local;
wxString label = option.label_local;
if (type == Preset::TYPE_PRINTER && variant_index >= 0 &&
printer_options_with_variant_2.count(get_pure_opt_key(opt_key)) > 0) {
// Orca: silent_mode is obsolete on import, but its option and two-column UI still exist.
// Keep mode labels for configs that explicitly enable it; omit them in the default single-mode UI.
if (new_config.opt_bool("silent_mode"))
label += " (" + (variant_index % 2 == 0 ? _L("Normal") : _L("Silent")) + ")";
variant_index /= 2;
}
if (variant_index >= 0 && extruder_variant && variant_index < extruder_variant->size()) {
// Orca: Match the untranslated category and use the same extruder names as the printer tabs.
if (option.category.compare(0, 9, L"Extruder ") == 0)
category = _L("Extruder");
wxString variant_label = L(extruder_variant->values[variant_index]);
// Orca: An extruder name only disambiguates variants on printers with multiple extruders.
if (multiple_extruders && extruder_id && variant_index < extruder_id->size() && extruder_id->values[variant_index] > 0) {
const wxString extruder_name = Tab::translate_category(
wxString::Format("Extruder %d", extruder_id->values[variant_index]), Preset::TYPE_PRINTER);
variant_label = extruder_name + " (" + variant_label + ")";
}
category = variant_label + ": " + category;
}
/*m_tree->Append(opt_key, type, option.category_local, option.group_local, option.label_local,
@@ -1584,7 +1606,7 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres
//PresetItem pi = {opt_key, type, 1983};
//m_presetitems.push_back()
PresetItem pi = {type, opt_key, category, option.group_local, option.label_local, get_string_value(opt_key, old_config), get_string_value(opt_key, new_config)};
PresetItem pi = {type, opt_key, category, option.group_local, label, get_string_value(opt_key, old_config), get_string_value(opt_key, new_config)};
m_presetitems.push_back(pi);
}
+2 -2
View File
@@ -275,9 +275,9 @@ void TempInput::Warning(bool warn, WarningType type)
wxString warning_string;
if (type == WarningType::WARNING_TOO_HIGH)
warning_string = _L("The maximum temperature cannot exceed ") + wxString::Format("%d", max_temp);
warning_string = wxString::Format(_L("The maximum temperature cannot exceed %d"), max_temp);
else if (type == WarningType::WARNING_TOO_LOW)
warning_string = _L("The minmum temperature should not be less than ") + wxString::Format("%d", min_temp);
warning_string = wxString::Format(_L("The minimum temperature should not be less than %d"), min_temp);
warning_text->SetLabel(warning_string);
warning_text->Wrap(-1);
warning_text->Fit();