Files
OrcaSlicer/src/slic3r/GUI/DesignSketchTool.hpp
T
Tommaso BianchiandClaude Opus 5 dcbda7d42c CAD: deliver the picked sketch ENTITY to the panel, and point Rib at it (snaporca-3648)
Ported from snaporca 94b6b564de. See that commit for what is and is not measured.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 23:25:21 +02:00

1343 lines
90 KiB
C++

#ifndef slic3r_DesignSketchTool_hpp_
#define slic3r_DesignSketchTool_hpp_
#include "libslic3r/Point.hpp"
#include "libslic3r/SketchEngine.hpp"
#include "libslic3r/CadDocument.hpp" // CadBody for per-body solid picking
#include "libslic3r/SketchInference.hpp"
#include "libslic3r/SketchSolver.hpp"
#include "GLModel.hpp"
#include "GLSelectionRectangle.hpp" // left-drag rubber band over the committed bodies
#include <functional>
#include <vector>
#include <string>
#include <utility>
class wxMouseEvent;
class wxPoint;
namespace Slic3r {
class TriangleMesh; // fwd (libslic3r) — solid-pick mesh, non-owning pointer
namespace GUI {
class GLCanvas3D;
class Camera; // fwd — move_gizmo_arm() sizes the gizmo from the current zoom
// Onshape-style sketch session. `begin` enters a session on a plane; the active
// drawing tool (Mode) can be switched mid-session via `set_tool` while entities
// accumulate. `finish` commits the whole entity list as one sketch feature;
// `cancel` aborts. Constrain is a separate legacy mode that operates on a
// committed profile's points (entity constraints land in a later chunk).
// ONE colour means SELECTED — a face, an edge, a vertex, a whole body, a 2D sketch region.
// Nothing else on screen may wear it. Before this there were four near-identical cyans plus a
// constant still named sel_gold that had long since become cyan, and the UNSELECTED region fill
// was blue (0.30,0.60,1.0) — one shade from the selected one — so an ordinary region read as
// picked. Selection is a state, not a decoration: it gets its own colour and keeps it.
inline ColorRGBA design_selection_color(float alpha = 1.0f)
{
return ColorRGBA(0.20f, 0.85f, 1.00f, alpha);
}
// Unselected geometry — 2D regions and faces — is neutral translucent grey, so the only
// coloured thing in the viewport is the thing you picked.
inline ColorRGBA design_idle_face_color()
{
return ColorRGBA(0.72f, 0.76f, 0.80f, 0.14f);
}
class DesignSketchTool {
public:
enum class Mode { Select, Dimension, Polyline, Line, CornerRect, CenterRect, ObliqueRect,
RoundedRect, CenterCircle, TwoPointCircle, Point,
ThreePointCircle, ThreePointArc, TangentArc, CenterArc, Slot, ArcSlot, Polygon,
Ellipse, EllipseArc, BSpline,
// In-canvas edit-op TOOLBAR tools (drag-arrow + label, no numeric card):
Fillet, Chamfer, Offset, Mirror,
// Standalone scissors: click a segment to trim/extend it (immediate, no card):
Trim, Extend,
// In-canvas transform TOOLBAR tools (pick targets + drag handle/label, no card):
Move, Rotate, Scale, Array, PolarArray,
// In-canvas bounding-box transform for imported Text/SVG art:
TransformArt,
Constrain };
bool is_edit_op_mode() const { return m_mode == Mode::Fillet || m_mode == Mode::Chamfer ||
m_mode == Mode::Offset || m_mode == Mode::Mirror; }
bool is_transform_mode() const { return m_mode == Mode::Move || m_mode == Mode::Rotate ||
m_mode == Mode::Scale || m_mode == Mode::Array ||
m_mode == Mode::PolarArray; }
// Creation tools that get draw-then-edit: on commit the new entity/feature is selected
// and its primary value editor opens. Line is handled inline (its own length field);
// Polyline/BSpline/Point have no single primary value, so they opt out.
bool is_creation_autoedit_mode() const {
switch (m_mode) {
case Mode::Line:
case Mode::CornerRect: case Mode::CenterRect: case Mode::ObliqueRect:
case Mode::RoundedRect: case Mode::CenterCircle: case Mode::TwoPointCircle:
case Mode::ThreePointCircle: case Mode::ThreePointArc: case Mode::TangentArc:
case Mode::CenterArc: case Mode::Slot: case Mode::ArcSlot: case Mode::Polygon:
case Mode::Ellipse: case Mode::EllipseArc:
return true;
default: return false;
}
}
// The host (DesignCanvas) flags the canvas frozen while an inline value editor is open,
// so a stray click/move can't draw under the floating field. Reuses m_awaiting_length
// (Line's existing freeze flag) as the single "inline editor open" gate.
void set_inline_busy(bool b) { m_awaiting_length = b; }
bool inline_busy() const { return m_awaiting_length; } // true while a value field is open
// Does the live session hold anything a cancel would throw away? Escape must not silently
// destroy drawn geometry; the panel asks this before treating Escape as "discard sketch".
bool live_sketch_has_work() const { return !m_entities.empty(); }
// Clicking the same sub-element again escalates to the whole body. That is right for free
// picking and WRONG while a card has armed a face/edge pick: the card says "click a FACE",
// the user clicks the face it is already showing, and the escalation turns it into a
// whole-body pick that the armed capture then rejects. The host turns this off for as long
// as a pick is armed.
void set_escalate_on_repick(bool on) { m_escalate_repick = on; }
bool constrain_value_anchor(wxPoint& out) const; // screen anchor over the picked constrain geometry
void begin(const SketchPlane& plane, Mode mode = Mode::Polyline);
// Re-open a committed entity sketch for full in-canvas editing: load its entities +
// driving constraints, re-detect the polygon/rect/slot grouping, and live-solve. The
// caller re-commits via finish() (the panel replaces the feature, see m_edit_index).
void begin_edit(const std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints,
const SketchPlane& plane);
// Drop rigid 2D art (Text / SVG outlines) INTO the live sketch as ordinary line entities,
// so it joins the sketch being drawn instead of committing a separate Sketch feature.
// `regions` are loops in PLANE coordinates; every closed loop becomes a closed polyline, so
// the result is editable, constrainable and extrudable like anything else drawn by hand —
// unlike imported_regions, which are rigid and carry no solver entities.
// Returns false when no session is live, so the caller can fall back to a new feature.
bool add_imported_regions(const std::vector<std::vector<std::vector<Vec2d>>>& regions);
void set_tool(Mode mode); // switch tool, keep accumulated entities
void set_plane(const SketchPlane& plane) { m_plane = plane; } // re-plane a live sketch (a reference plane was clicked mid-session); entities are 2D, re-lifted through the new plane
void set_construction(bool c) { m_construction = c; }
void set_polygon_sides(int n) { m_polygon_sides = (n < 3 ? 3 : n); }
void set_polygon_circumscribed(bool c) { m_polygon_circumscribed = c; }
void finish(); // emit accumulated entities, end session
void cancel();
bool is_active() const { return m_active; }
bool has_entities() const { return !m_entities.empty(); }
bool on_mouse(wxMouseEvent& evt, GLCanvas3D& canvas);
// True if the LAST right-press was consumed as a gesture terminator (end a polyline chain,
// abandon an anchor, exit a tool). Read-and-clear: the canvas asks on the matching release to
// decide whether that right-click was the user's, in which case it opens the offer.
bool take_right_consumed() { const bool b = m_right_consumed; m_right_consumed = false; return b; }
void render(GLCanvas3D& canvas);
// Persistent committed sketches to draw even when no session is active (e.g. an
// un-consumed sketch left visible after its extrude is removed). Each carries its
// own plane. render() draws these as translucent faces + outlines.
struct DisplaySketch { std::vector<SketchEntity> entities; SketchPlane plane; int feature{-1}; };
void set_display_sketches(std::vector<DisplaySketch> ds) { m_display_sketches = std::move(ds); }
void set_highlight_sketches(std::vector<std::pair<int, ColorRGBA>> hl) { m_hl_sketches = std::move(hl); }
// Solid pick is resolved on LeftUp (see on_mouse): consuming the press broke orbit/pan.
int m_pick_press_x = 0;
int m_pick_press_y = 0;
bool m_pick_pending = false;
bool has_display() const { return m_active || !m_display_sketches.empty()
|| (m_solid_bodies != nullptr && !m_solid_bodies->empty())
|| !m_datum_planes.empty()
|| m_show_planes || m_show_axes
|| m_ex_active || m_mv_active || m_fl_active
|| m_hl_active || m_th_active || m_sh_active
|| m_dr_active || m_ct_active || m_dz_active || m_dbp_active || m_hx_active || m_rb_active; }
// View helpers: the 3 world origin planes (XY/XZ/YZ) and the world axis triad, each
// shown/hidden by a toggle (keys P / A). Off by default so the idle scene stays clean.
void set_show_planes(bool s) { m_show_planes = s; }
void set_show_axes(bool s) { m_show_axes = s; }
bool toggle_show_planes() { m_show_planes = !m_show_planes; return m_show_planes; }
bool toggle_show_axes() { m_show_axes = !m_show_axes; return m_show_axes; }
// Solid topology selection on the committed bodies: clicking a solid cycles
// whole-solid -> face -> edge (Onshape-style) to target fillet/chamfer/extrude. With
// multiple bodies the pick resolves WHICH body was hit (per-triangle body id).
// Appended, never reordered: DesignPanel maps this to a level int (Whole=1, Face=2,
// Edge=3, Vertex=4) and the offer table keys off it.
enum class SolidSel { None, Whole, Face, Edge, Vertex };
// Point the tool at the current bodies + their concatenated tessellation (non-owning;
// pass nullptr to clear). Call after each recompute — selection resets (ids invalidate).
// tri_face = per-triangle face id within its body; tri_body = per-triangle body index.
void set_solid_pick(const std::vector<CadBody>* bodies, const TriangleMesh* mesh,
const std::vector<int>* tri_face, const std::vector<int>* tri_body,
const std::vector<bool>* visible = nullptr,
const std::vector<Transform3d>* xform = nullptr);
// Body-focus picking: >=0 restricts every pick to that one body, so a face behind
// another solid is reachable without hiding anything. -1 = no restriction.
// Survives set_solid_pick() — it is owned by the panel, not by the mesh feed.
void set_pick_only_body(int b) { m_pick_only_body = b; }
void clear_solid_selection();
// Select a whole body by index (from the Parts list) — Whole-level highlight, no face/edge.
// body < 0 or out of range clears the selection.
void select_body(int body);
// Move-body gizmo (M5): translate a whole body with three world-axis drag arrows
// (X red / Y green / Z blue) anchored at the body centroid. Display-only — the host
// keeps a per-body Transform3d and re-feeds the moved display/pick meshes; the OCCT
// shape (and thus face/edge global ids) is never touched. Drag fires on_body_move_changed
// live; a stationary click on an arrow opens the inline offset editor for that axis.
void set_move_gizmo(int body, const Vec3d& pivot, const Transform3d& base_xform,
double body_radius = 0.0);
void clear_move_gizmo();
bool moving_body() const { return m_mv_active; }
int move_body_index() const { return m_mv_body; }
// F key forwarded from the canvas (Prepare's Place on Face): returns true if it acted.
bool request_place_on_face() { return on_place_on_face ? on_place_on_face() : false; }
std::function<bool()> on_place_on_face;
std::function<void(int body, const Transform3d& xform)> on_body_move_changed;
// Fired on each cycle change: (level 0=None/1=Whole/2=Face/3=Edge, body index, face id, edge id).
std::function<void(int level, int body, int face, int edge)> on_solid_selection_changed;
// Click a committed sketch overlay (no live session) -> select that loop: the Sketch
// feature index + the clicked closed-region index within it (-1 = no specific loop).
// entity = the sketch entity index under the cursor when the click landed on a loop
// STROKE, else -1 for an interior/region hit. Carried because a tool can legitimately
// want the LINE you pointed at, not just the loop it belongs to (Rib, snaporca-3648).
std::function<void(int feature, int region, int entity)> on_display_sketch_selected;
// Double-click on a committed sketch stroke: open THAT feature for editing. Selecting a line
// and then hunting for an Edit button in a panel is the dependency this tab exists to remove.
std::function<void(int feature)> on_display_sketch_activated;
// Entities forming the currently click-selected loop (for a per-loop extrude); empty
// if no loop is selected.
std::vector<SketchEntity> selected_loop_entities() const;
// Per closed loop, the indices into `ents` that form it (for hiding already-extruded
// loops from the committed-sketch overlay).
std::vector<std::vector<int>> region_entity_indices(const std::vector<SketchEntity>& ents) const;
// Same, but each region's entry is its OWN entities followed by the entities of each of its
// holes, in that order — the exact list a per-loop extrude of a region WITH holes stores
// (see selected_loop_entities()). Needed to match a consumed loop against its source sketch.
std::vector<std::vector<int>> region_entity_indices_with_holes(const std::vector<SketchEntity>& ents) const;
void clear_display_pick() { m_display_pick = -1; m_display_pick_region = -1; }
// Adopt a loop pick the tool did not make itself. The live-sketch path resolves the region
// BEFORE the sketch is committed, so once finish_sketch() has turned it into a display
// sketch there is nothing left that would set this — and selected_loop_entities(), which is
// what Extrude consumes, reads exactly these two fields.
void set_display_pick(int feature, int region) { m_display_pick = feature; m_display_pick_region = region; }
// Visual Extrude gizmo (C5b). The Extrude tool is a DesignPanel docked card, so the
// sketch tool is NOT active during it; the panel feeds the profile plane + a 2D centroid
// (arrow anchor) + the live depths/flags, and the tool renders an in-canvas world-space
// depth arrow along plane.normal with a draggable handle + editable label. TwoSided draws
// a second arrow along -normal driven by depth2. Drag/edit fire on_extrude_depth_changed
// back to the panel, which writes the spin value + refreshes the ghost preview.
void set_extrude_gizmo(const SketchPlane& plane, const Vec2d& centroid,
double depth, double depth2, bool two_sided, bool flip);
void clear_extrude_gizmo();
// (new_depth, second_side): second_side=false drives the primary depth, true the 2nd side.
std::function<void(double depth, bool second)> on_extrude_depth_changed;
// Datum-plane resize gizmo (C3). The Plane tool is a DesignPanel docked card (sketch tool
// NOT active), so the panel resolves the candidate plane's frame + current u/v extent and
// feeds them here; the tool draws the rectangle outline + 4 edge-midpoint handles. Dragging a
// handle changes the u (left/right) or v (top/bottom) extent live and fires on_datum_size_changed
// back to the panel, which writes the Size spins + re-pushes the rendered datum.
void set_datum_gizmo(const SketchPlane& plane, double usize, double vsize,
const Vec3d& base_origin, const Vec3d& base_normal,
double offset, bool offset_on);
void clear_datum_gizmo();
std::function<void(double usize, double vsize)> on_datum_size_changed;
std::function<void(double offset)> on_datum_offset_changed;
// Visual Helix gizmo. The Helix tool is a DesignPanel docked card (sketch tool NOT active),
// so the panel resolves the axis plane and feeds the live parameters here; the tool draws
// the helix curve itself plus three handles — radius on the base circle, height at the top
// of the axis, pitch at the end of the first turn. Taper and handedness stay on the card:
// one is a shape modifier and the other is a flag, and L2 governs numbers you can point at.
void set_helix_gizmo(const SketchPlane& plane, double radius, double pitch, double height,
double taper, bool left_handed);
void clear_helix_gizmo();
std::function<void(double radius, double pitch, double height)> on_helix_changed;
// Visual Rib thickness gizmo. The rib is a thin slab grown either side of an open sketch
// line, so its thickness is an IN-PLANE offset perpendicular to that line — the depth arrow
// (which points along the plane normal) cannot express it. Two handles, one per side,
// dragged symmetrically: the slab is centred on the line, so a drag on either side sets the
// full thickness rather than one half.
void set_rib_gizmo(const SketchPlane& plane, const Vec2d& p0, const Vec2d& p1, double thickness);
void clear_rib_gizmo();
std::function<void(double thickness)> on_rib_thickness_changed;
// Graphical base/origin pick: while the Plane card is open, the candidate base planes
// (XY/XZ/YZ origin planes + existing datums) draw as translucent clickable ghosts. A click
// on one fires on_datum_base_picked(base) with that plane's base index (0/1/2 or 3+N).
void set_base_pick(std::vector<SketchPlane> planes, std::vector<int> bases,
std::vector<std::string> labels = {});
void clear_base_pick();
std::function<void(int base)> on_datum_base_picked;
// Visual Fillet/Chamfer gizmo. The Dressup tool is a DesignPanel docked card, so the sketch
// tool is NOT active during it; when a solid EDGE is picked the panel passes the body centroid
// + current radius and the tool anchors a world-space radius arrow at the picked edge midpoint
// (from m_sel_edge_pts), perpendicular to the edge, pointing outward (away from the centroid).
// Dragging the arrow changes the radius live; a stationary click opens the inline editor; both
// fire on_fillet_radius_changed back to the panel, which writes the spin + refreshes the ghost.
// Returns true if it could anchor (needs a picked edge with >=2 sample points).
bool set_fillet_gizmo(const Vec3d& body_centroid, double radius);
void clear_fillet_gizmo();
bool filleting() const { return m_fl_active; }
std::function<void(double radius)> on_fillet_radius_changed;
// Visual Hole gizmo. Like Dressup, the Hole tool is a DesignPanel docked card, so the sketch
// tool is NOT active during it; the panel passes the hole plane + position + diameter + depth +
// through flag, and the tool draws an on-plane footprint circle plus a radial diameter arrow,
// a normal-axis depth arrow (only when !through), and a draggable centre marker. Dragging the
// centre repositions (plane u/v), the diameter arrow resizes, the depth arrow deepens — all
// live; a stationary click on an arrow opens its inline editor. Every change fires
// on_hole_changed back to the panel, which writes the spins + refreshes the ghost.
void set_hole_gizmo(const SketchPlane& plane, double x, double y,
double diameter, double depth, bool through);
// Provide the face (u,v) bounds so the hole's construction dims read from the face sides.
void set_hole_face_bounds(bool has, double umin, double umax, double vmin, double vmax);
void clear_hole_gizmo();
bool holing() const { return m_hl_active; }
std::function<void(double x, double y, double diameter, double depth)> on_hole_changed;
// Visual Thread gizmo. Same docked-card story as Hole: the panel feeds the thread plane +
// axis position + nominal radius + length; the tool draws an on-plane footprint circle plus a
// radial radius arrow and a normal-axis length arrow (always shown — a thread has no "through")
// and a draggable centre. Pitch/depth/internal stay in the card. Drag is live; a stationary
// click on an arrow opens its inline editor; every change fires on_thread_changed.
void set_thread_gizmo(const SketchPlane& plane, double x, double y,
double radius, double height);
void clear_thread_gizmo();
bool threading() const { return m_th_active; }
std::function<void(double x, double y, double radius, double height)> on_thread_changed;
// Visual Shell gizmo. The panel passes the picked open-face centroid + an inward direction
// (-outward normal) + the current wall thickness; the tool anchors a single thickness arrow
// there (mirrors the fillet radius arrow). Dragging sets the thickness live; a stationary
// click opens the inline editor; both fire on_shell_thickness_changed.
void set_shell_gizmo(const Vec3d& face_centroid, const Vec3d& inward_dir, double thickness);
void clear_shell_gizmo();
bool shelling() const { return m_sh_active; }
std::function<void(double thickness)> on_shell_thickness_changed;
// Datum/reference planes (Plane feature) carry no solid; the panel feeds their resolved
// SketchPlanes so they render as translucent rectangles in feature mode (otherwise a
// Plane feature is invisible in the canvas).
void set_datum_planes(std::vector<SketchPlane> planes, std::vector<Vec2d> sizes = {}) {
m_datum_planes = std::move(planes); m_datum_sizes = std::move(sizes);
}
// Mate connectors. Until now a connector was visible only to a program — resolve_datum_coordsys
// had exactly one consumer, the MCP socket — so the frame a mate is built on could not be seen
// at all. The glyph has to answer two questions on sight (snaporca-wgsc): which way does Z point
// (the VERSE), and which of the pair is anchored versus driven (the POLARITY). Nothing in any
// surveyed CAD system encodes the second one.
struct MateConnectorGlyph {
Vec3d origin{0, 0, 0};
Vec3d x{1, 0, 0}; // roll reference; the filled quadrant spans x -> y
Vec3d y{0, 1, 0};
int role{0}; // 0 = neutral, 1 = fixed (receives), 2 = driven (moves)
bool roll_undefined{false};
};
void set_mate_connectors(std::vector<MateConnectorGlyph> g) { m_mate_connectors = std::move(g); }
void clear_mate_connectors() { m_mate_connectors.clear(); }
// Visual Revolve gizmo. The panel feeds the sketch plane + profile centroid + axis (0=plane X,
// 1=plane Y) + angle + flip while its Revolve card is open; an angle-arc is drawn in the
// revolve plane at the profile radius. Dragging the tip sweeps the angle, a stationary click
// edits it; both fire on_revolve_angle_changed.
void set_revolve_gizmo(const SketchPlane& plane, const Vec2d& centroid,
int axis_sel, double angle, bool flip);
void clear_revolve_gizmo();
bool revolving() const { return m_rv_active; }
std::function<void(double angle)> on_revolve_angle_changed;
// Visual Draft angle-arc gizmo (taper a picked face; axis = world +Z, arc in XY).
void set_draft_gizmo(const Vec3d& face_centroid, const Vec3d& face_normal, double angle);
void clear_draft_gizmo();
bool drafting() const { return m_dr_active; }
void set_on_draft_angle_changed(std::function<void(double)> cb) { m_on_draft_angle_changed = std::move(cb); }
// Visual Cut gizmo (plane normal arrow + plane rectangle preview).
void set_cut_gizmo(const SketchPlane& plane, double offset, const Vec3d& body_center, double half_extent);
void clear_cut_gizmo();
bool cutting() const { return m_ct_active; }
void set_on_cut_offset_changed(std::function<void(double)> cb) { m_on_cut_offset_changed = std::move(cb); }
// Visual Pattern gizmo. Linear: a 3D arrow along the world axis (plane X/Y per `dir`) of length
// spacing*(count-1) with a tick at each copy; dragging the end sets the spacing. Circular: a
// revolve-style angle-arc about the plane normal through the plane origin sweeping `angle`.
// Both fire on_pattern_changed (spacing for linear, angle for circular).
void set_pattern_gizmo(const SketchPlane& plane, const Vec3d& body_centroid, bool circular,
int count, int dir, double spacing, double angle);
void clear_pattern_gizmo();
bool patterning() const { return m_pt_active; }
std::function<void(double value)> on_pattern_changed;
// Constrain mode: load an already-committed profile for entity picking +
// constraint application (the geometry is solved in the kernel, not here).
void begin_constrain(const SketchProfile& prof, const SketchPlane& plane);
bool is_constraining() const { return m_active && m_mode == Mode::Constrain; }
// Replace the displayed profile (e.g. after the kernel re-solved it).
void set_profile_points(const std::vector<Vec2d>& pts) { m_points = pts; }
// The currently picked segment's endpoint indices into the profile.
bool selected_segment(int& a, int& b) const;
// Entity-aware Constrain (Fase 4.2): load a committed entity sketch and pick
// Line entities (constraints are solved against entity endpoints in the kernel).
void begin_constrain_entities(const std::vector<SketchEntity>& ents, const SketchPlane& plane);
bool is_constraining_entities() const { return m_active && m_mode == Mode::Constrain && m_constrain_entities; }
// In-canvas bounding-box transform of imported Text/SVG art (replaces the Move/Scale
// dialog). `base_regions` are the untransformed region contours; the gizmo shows the
// current bbox with 4 corner scale-handles + a centre move-handle. Dragging fires
// on_imported_transform live with the new offset/scale, which the host writes back to
// the feature. Exiting (Esc/right-click) ends the session.
void begin_imported_transform(int feat,
const std::vector<std::vector<std::vector<Vec2d>>>& base_regions,
const SketchPlane& plane, const Vec2d& offset,
double scale_x, double scale_y);
std::function<void(int feat, Vec2d offset, double scale_x, double scale_y)> on_imported_transform;
// Up to two picked line-entity indices; returns true if at least one is picked.
bool selected_constrain_entities(int& e0, int& e1) const { e0 = m_pick0; e1 = m_pick1; return m_pick0 >= 0; }
// Third pick slot (Symmetric axis): only filled after slots 0 and 1 are set.
int pick2() const { return m_pick2; }
// Plane-coords of the click that filled slot 0 (for pick-point edit ops: trim/extend).
bool pick0_point(Vec2d& out) const { out = m_pick0_pt; return m_pick0 >= 0; }
// Refresh the displayed entities after the kernel re-solved them.
void set_constrain_entities(const std::vector<SketchEntity>& ents) { m_entities = ents; }
// Constraint manager (C3.4): entity indices the panel asks to highlight (the
// entities a selected constraint references); rendered yellow in Constrain mode.
void set_constraint_highlight(std::vector<int> v) { m_constraint_hl = std::move(v); }
// The committed feature's constraints, supplied so Constrain-mode render can draw
// an iconic glyph badge per constraint near its primary entity (C3.4b).
void set_constraint_glyphs(std::vector<SketchEntityConstraintDef> v) { m_constrain_cons = std::move(v); }
// Line tool: after a single segment is placed, the panel pops a length dialog
// (length, angle_deg are the as-drawn values); it then resolves via
// apply_segment_length() (exact length) or keep_segment_as_drawn() (cancel).
std::function<void(double length, double angle_deg)> on_segment_drawn;
void apply_segment_length(double len); // rescale the pending segment, then commit it
void keep_segment_as_drawn(); // commit the pending segment unchanged
// Live readout while drawing a Line/Polyline segment (anchor->cursor metrics).
std::function<void(double length, double angle_deg, bool locked)> on_cursor_metrics;
// DoF feedback (P3): solver state after each live solve. dof>0 = under-constrained,
// dof==0 = fully constrained, ok==false = conflicting/inconsistent constraints.
// has_constraints is false while the sketch carries no driving constraints yet.
std::function<void(int dof, bool ok, bool has_constraints)> on_solve_state;
// Selection (Mode::Select): pick points/lines/arcs/circles of the in-session
// sketch; Shift/Ctrl extends, double-click grabs the whole connected loop.
const std::vector<int>& selection() const { return m_selection; }
// Type of the first selected entity. False when nothing is selected, so the offer menu can
// tell a line from an arc from a point and stop collapsing every sketch selection to "none".
bool first_selected_type(SketchEntity::Type& out) const {
if (m_selection.empty()) return false;
const int i = m_selection.front();
if (i < 0 || i >= int(m_entities.size())) return false;
out = m_entities[i].type;
return true;
}
void clear_selection();
void delete_selected(); // erase selected entities
// Abort any pending/queued draw-then-edit value-field sequence. Removing an entity that
// still has a deferred auto-edit would otherwise open a field on a now-deleted entity and
// freeze the flow (its live quote label also lingers). Mirrors set_tool's resync.
void reset_autoedit() {
m_awaiting_length = false;
m_autoedit_pending = false;
m_autoedit_dims.clear();
m_autoedit_dim_idx = -1;
m_autoedit_seen = int(m_entities.size());
m_live_quotes.clear(); // rebuilt from current geometry on the next render
}
// Take down the session's floating chrome: the open value field (dismiss = keep-as-drawn),
// the queue of fields behind it, and the corner readout. All three are top-level windows fed
// only while the tool is live, so nothing else would ever clear them — reset_autoedit() alone
// clears the flag and leaves the frame on screen. Called by finish()/cancel(); safe when
// nothing is open.
void close_session_chrome() {
if (on_inline_dismiss) on_inline_dismiss(); // no-op when no field is open
reset_autoedit();
if (on_readout) on_readout(std::string()); // the HUD is not redrawn once the tool stops
}
// Ctrl+Z while sketching: drop the last drawn entity (reuses delete_selected's remap).
bool undo_last_entity() {
if (!m_active || m_entities.empty()) return false;
m_selection.assign(1, int(m_entities.size()) - 1);
delete_selected();
reset_autoedit();
return true;
}
// Delete while sketching: the selected entities, or the last drawn one if none is selected.
bool delete_selected_or_last() {
if (!m_active) return false;
if (m_selection.empty()) {
if (m_entities.empty()) return false;
m_selection.assign(1, int(m_entities.size()) - 1);
}
delete_selected();
reset_autoedit();
return true;
}
std::function<void(int count)> on_selection_changed;
// Dimension tool: infer a driving dimension from the current selection and set
// it exactly. Sizing: 1 line=Length, 1 circle=Diameter, 1 arc=Radius,
// 2 lines=Angle. Positioning (a value of 0 makes them coincident):
// 2 point-likes (point/circle-centre/arc-centre)=Distance, moving the 2nd onto
// the 1st; a point-like + a line=DistanceToLine, moving the point-like's
// reference point onto/away-from the line (e.g. a circle centre onto an axis).
enum class DimType { None, Length, Diameter, Radius, Angle, Distance, DistanceToLine };
DimType dimension_kind() const; // what the selection supports (None if invalid)
double dimension_current() const; // current value, to pre-fill the dialog
void apply_dimension(double v); // set it exactly, then clear the selection
// Onshape-style Dimension tool (Mode::Dimension): with the tool active you click
// directly in the viewport — 2 points -> Distance, a line -> Length, a circle ->
// Diameter, an arc -> Radius, a point then a line -> DistanceToLine. A quote line
// with extension lines, arrowheads and a numeric label is PLACED in the sketch and
// drives the geometry (auto-offset; label editable). on_dimension_pick_complete
// fires when a pick resolves so the panel can pop the value card pre-filled.
std::function<void(double current)> on_dimension_pick_complete;
DimType pending_dimension_type() const; // type of the dim awaiting a value, or None
void set_dimension_value(double v); // apply the typed value to the placed dim
void cancel_dimension_value(); // keep the placed dim at its measured value
// Onshape-style in-canvas value editing: open a floating text editor at the given
// screen pixel, pre-filled with `current`; commit applies the value, cancel keeps
// it. The owner (DesignCanvas) hosts the wxTextCtrl over the GL canvas. This is the
// single numeric-entry path for all sketch dimensions (replaces the modal cards).
std::function<void(wxPoint screen_px, double current, const std::string& title,
std::function<void(double)> commit,
std::function<void()> cancel)> on_inline_edit;
// Force-close any open inline field (runs its cancel = keep-as-drawn). Used by the polyline
// terminators (right-click / double-click) to end the chain even mid per-segment edit.
std::function<void()> on_inline_dismiss;
// Accept and close an open inline value field. dismiss() CANCELS; this one keeps the value,
// which is what leaving a tool should do — see set_tool().
std::function<void()> on_inline_commit;
// Bottom-right viewport readout: emitted each frame with the active tool's current
// values (live segment length/angle while drawing a line, or the selected entity's
// characteristic dimensions). Empty string -> hide the HUD. The owner (DesignCanvas)
// shows it as a floating corner label over the GL canvas.
std::function<void(const std::string&)> on_readout;
// Driving dimension constraints accumulated during the session (the Dimension
// tool records a SketchEntityConstraintDef per applied dimension); committed
// alongside the entities on finish() so the kernel keeps enforcing them.
const std::vector<SketchEntityConstraintDef>& constraints() const { return m_constraints; }
// Emitted by finish() with the accumulated entities + driving constraints.
std::function<void(const std::vector<SketchEntity>&,
const std::vector<SketchEntityConstraintDef>&,
const SketchPlane&)> on_commit_entities;
// Legacy single-profile commit (kept for compatibility; unused by entity tools).
std::function<void(const SketchProfile&, const SketchPlane&)> on_commit;
// Emitted when a closed-loop face is clicked in Select mode (Onshape: a region
// becomes a selectable face → extrude). The panel commits the sketch + extrudes.
std::function<void(int)> on_face_selected; // region index into region_loops(m_entities)
// Esc pressed while the tool is active: exit/cancel the session (the panel restores
// Feature mode). Layered: an in-progress entity or a non-Select draw tool is dropped
// first; a second Esc exits the session.
std::function<void()> on_exit;
std::function<void()> on_move_exit; // right-click finished the move-body gizmo
void request_exit();
// Ctrl+Z / Ctrl+Shift+Z (Ctrl+Y) while the Design canvas is focused: undo/redo the
// committed feature history. The tool just forwards to the host, which owns the
// CadDocument (the tool has no document of its own). redo == true requests redo.
std::function<void(bool /*redo*/)> on_undo_redo;
void request_undo_redo(bool redo);
private:
bool screen_to_plane(GLCanvas3D& canvas, const wxMouseEvent& evt, Vec2d& out) const;
bool near_first(const Vec2d& p) const;
// Onshape-style angle inference: snap the direction anchor->raw to the nearest
// of {0,30,45,60,90} deg (replicated every 90 deg) when within tolerance, keeping
// the same length. Sets `locked` when a snap was applied. Suppressed by m_snap_off.
Vec2d snap_dir(const Vec2d& anchor, const Vec2d& raw, bool& locked) const;
// Snap a placed point onto the nearest existing entity endpoint within ~8 px so
// chains join across entities (a line + an arc can close into one loop). Shift
// disables it. `snapped` reports whether a vertex was hit.
Vec2d snap_vertex(GLCanvas3D& canvas, const wxMouseEvent& evt, const Vec2d& raw, bool& snapped) const;
// --- P1 inference / auto-constraint engine ---------------------------------
// Plane-units tolerance equivalent to ~`px` screen pixels at the cursor.
double screen_tol(GLCanvas3D& canvas, const wxMouseEvent& evt, const Vec2d& at, double px = 8.0) const;
// Run kernel inference at the cursor, cache the target for the hint renderer.
InferenceSnap infer_at(GLCanvas3D& canvas, const wxMouseEvent& evt, const Vec2d& raw) const;
// True if m_constraints already holds an equivalent Coincident between the two refs.
bool has_coincident(int ea, SketchPointRole ra, int eb, SketchPointRole rb) const;
// Append candidates, live-solve, and roll back the batch if it turns the system
// inconsistent. Returns true when the batch was kept.
bool try_add_constraints(const std::vector<SketchEntityConstraintDef>& cands);
// After entities [base, end) were committed, auto-emit the constraints that make
// the new geometry stick: Coincident between co-located endpoints (so loops close
// on their own) and Horizontal/Vertical on axis-aligned new segments.
void infer_auto_constraints(int base);
// Selection helpers (Mode::Select).
int hit_test(const Vec2d& p, double tol) const; // nearest entity within tol, or -1
std::vector<int> connected_loop(int seed) const; // entities joined by shared endpoints
void apply_angle_between(int ia, int ib, double deg); // rotate line B to set the A^B angle
bool selection_valid() const; // all selection indices in range
void record_dimension_constraint(double v); // append the driving def for the selection
void resolve_live(); // solve accumulated constraints on m_entities now
// Drag-aware re-solve: pins the dragged point at its current coord and lets the
// solver move the rest (Slvs dragged[]). Used live while a point grab is active.
void resolve_live_drag(int dragged_ei, SketchPointRole dragged_role);
// Placed dimension annotation. References entity points/entities (not cached
// coords) so the quote follows the geometry as the kernel solves it. `value`
// drives the constraint stored at index `con` in m_constraints.
struct DimAnnot {
DimType kind{DimType::None};
int ea{-1}; SketchPointRole ra{SketchPointRole::P0};
int eb{-1}; SketchPointRole rb{SketchPointRole::P0};
double value{0.0};
double side{1.0}; // perpendicular offset sign of the quote line
int con{-1}; // slot in m_constraints driving this dimension
Vec2d label_pos{0, 0}; // cached label centre (plane coords), for picking
};
// --- Onshape-style visual editing: handles + parametric feature grouping -----
// A draggable handle on a defining point of an entity (or a derived point of a
// feature group). GUI-only; recomputed from solved geometry every frame (never
// persisted), so handles always track the current solve. Derived roles (radius,
// slot width/centres, rect corners, polygon vertex, ellipse axes) let tools that
// decompose into raw Line/Arc entities still expose their parametric controls.
enum class HandleRole { P0, P1, Center, RadiusHandle,
SlotCenter0, SlotCenter1, SlotWidth,
RectCorner, PolygonVertex, MajorAxis, MinorAxis, BSplineCtrl };
struct Handle {
HandleRole role{HandleRole::P0};
int ei{-1}; // primary entity index
int group{-1}; // index into m_features, or -1 for a raw-entity handle
int ctrl_index{-1}; // BSplineCtrl pole index
Vec2d pos{0, 0}; // current plane coords (recomputed each frame)
bool hovered{false};
};
// A parametric grouping over a contiguous run of entities produced by one gesture.
// Slot/Rect/Polygon/etc. have no SketchEntity type of their own — they decompose
// into raw Line/Arc entities — so the Feature carries the gesture's anchors so
// derived handles + characteristic dimensions can be reconstructed.
enum class FeatureKind { Free, Line, Circle, Arc, CornerRect, CenterRect,
Slot, ArcSlot, Polygon, Ellipse, RoundedRect, BSpline };
struct Feature {
FeatureKind kind{FeatureKind::Free};
int begin{0}, end{0}; // [begin,end) into m_entities
Vec2d c0{0, 0}, c1{0, 0}; // slot centres / rect corners / ellipse centre+major
double param{0.0}; // slot half-width / polygon circumradius / fillet radius
int sides{0}; // polygon side count
};
// Build the live handle set for the current selection / just-drawn feature.
std::vector<Handle> build_handles() const;
// Nearest handle to plane-point p within tol; fills `out`. (Phase A: stub.)
bool hit_test_handle(const Vec2d& p, double tol, Handle& out) const;
// Move a handle to `target`, applying the role-specific geometry edit + re-solve.
void set_handle(const Handle& h, const Vec2d& target);
// On a no-button move, recompute the hovered handle; returns true iff it changed
// (so the caller forces exactly one repaint). No-op for non-Moving events.
bool update_hover(GLCanvas3D& canvas, wxMouseEvent& evt);
// Index of the Feature whose [begin,end) entity span contains ei, or -1.
int feature_of(int ei) const;
// Re-detect parametric Feature groups (polygon / rect / slot) from the raw entity
// list — used when a committed sketch is re-opened, where m_features is empty.
void rebuild_features_from_entities();
// Open/close a Feature record around the entities a single gesture appends.
void begin_feature(FeatureKind kind);
void end_feature(const Vec2d& c0 = Vec2d(0, 0), const Vec2d& c1 = Vec2d(0, 0),
double param = 0.0, int sides = 0);
bool point_at(int ei, SketchPointRole role, Vec2d& out) const; // current coords
void set_point(int ei, SketchPointRole role, const Vec2d& v); // move an entity point
bool hit_test_point(const Vec2d& p, double tol, int& ei, SketchPointRole& role) const;
int hit_test_dimension(const Vec2d& p, double tol) const; // nearest dim label
void edit_dimension(int di); // reopen value card for di
// Representative plane-coords anchor of a dimension (label centre if known, else a
// geometric midpoint/centre) — where the in-canvas value editor is positioned.
Vec2d dim_anchor(const DimAnnot& a) const;
// Open the in-canvas value editor on dimension `di` (falls back to the modal
// pick-complete callback when no inline-edit host is wired).
void open_value_editor(int di);
// In-canvas editor for a line's angle-to-horizontal; commit rotates the segment
// geometrically about P0 (no single-line angle constraint in libslvs).
void open_angle_editor(int ei);
void set_line_angle(int ei, double deg);
// Draw-then-edit (all creation tools): open the inline editor on the freshly-drawn
// selection's PRIMARY characteristic value. Called after render_live_quotes has computed
// the selection's quotes, so it dispatches on the same live-quote state a Select-mode
// click would use.
void open_primary_autoedit();
// Compact "current values" string for the bottom-right HUD (see on_readout).
std::string build_readout() const;
// Open a characteristic live quote as a TENTATIVE driving dimension: the constraint is
// appended only if the user commits a value (Enter); cancel (Esc) adds nothing — so
// drawing never silently over-constrains. (place_dimension is the eager Select-mode twin.)
void open_next_autoedit_dim(); // opens m_autoedit_dims[idx]; commit -> next, Esc -> stop
void arm_polyline_segment_edit();// per-segment Length+Angle edit of the pending chain vertex
// In-canvas editors for a regular polygon's side length and orientation. Both edit
// the whole loop GEOMETRICALLY (polygon has no centre entity): side scales it
// uniformly about its centre, angle rotates it. set_polygon_radius is the shared
// uniform-scale primitive (circumradius).
void open_polygon_side_editor(int fi);
void open_polygon_angle_editor(int fi);
void set_polygon_side(int fi, double side);
void set_polygon_angle(int fi, double deg);
void set_polygon_radius(int fi, double R);
// Arc sweep-angle quote: geometric edit (SLVS angle is line-to-line only). Keeps the
// arc start point + radius fixed and moves the end point to span `deg` degrees.
void open_arc_angle_editor(int ei);
void set_arc_sweep(int ei, double deg);
// Arc handle drag (3 grips): Center rigidly translates; the START point changes the
// radius (keeps both sweep angles); the END point changes the sweep angle (keeps the
// radius). Geometric — no solver (SLVS has no arc radius/angle handle concept here).
void drag_arc_handle(int ei, SketchPointRole role, const Vec2d& target);
// Ellipse axis labels (geometric edit of the semi-axes a/b; phi via the major grip).
void open_ellipse_axis_editor(int ei, bool major);
void set_ellipse_axis(int ei, bool major, double v);
void set_ellipsearc_sweep(int ei, double deg); // draw-then-edit: included sweep of an elliptical arc
void set_rect_angle(int fi, double deg); // draw-then-edit: orientation of an oblique rect
// EllipseArc endpoint drag: Center translates; P0/P1 move the sweep start/end to the
// parametric angle of the cursor on the ellipse frame (radius/shape preserved).
void drag_ellipsearc_handle(int ei, SketchPointRole role, const Vec2d& target);
// Drop orientation constraints (H/V/Parallel/Perp/Angle/LockX/LockY) on entities in
// [begin,end). A ROTATION makes inferred per-edge H/V inconsistent, so re-solving
// against them collapses the shape — drop them first (fixes up DimAnnot.con indices).
void drop_orientation_constraints(int begin, int end);
// Drop every live constraint that references entity `ei` (Trim/Extend slide an endpoint,
// invalidating its constraints) and fix the dimensions' cached constraint indices.
void drop_constraints_referencing(int ei);
// Standalone Trim/Extend scissors on the LIVE sketch: pick the entity nearest `p` (within
// `tol` plane units) and cut it back to / out to its nearest intersection with the others.
// Returns true if an entity was modified.
bool apply_live_trim(const Vec2d& p, double tol, bool extend);
// Pure-computation hover preview for Trim/Extend: mirror apply_live_trim's pick + the
// engine's cut on a COPY (mutating nothing) and return, via `removed_poly`, the polyline
// of the sub-portion a click would REMOVE (Trim) or ADD (Extend). `subject_ei` is the
// picked entity. Returns false if nothing is in range or nothing would change.
bool compute_trim_preview(const Vec2d& p, double tol, bool extend,
int& subject_ei, std::vector<Vec2d>& removed_poly) const;
// Drag a polygon vertex while keeping the loop REGULAR: scale + rotate the whole
// polygon about its centroid so the grabbed vertex follows `target` (adjusts
// circumradius + orientation together).
void drag_polygon_vertex(int fi, int ei, SketchPointRole role, const Vec2d& target);
double measure_dim(const DimAnnot& a) const; // value from geometry
std::string dimtype_title(DimType k) const;
SketchEntityConstraintDef constraint_for(const DimAnnot& a) const; // driving def
// One driving constraint (and one visible quote) per kind+operands: re-typing a value must
// UPDATE it, not append a rival asking for something else. Both return the index.
int upsert_constraint(const SketchEntityConstraintDef& c);
int upsert_dimension(const DimAnnot& a);
int place_dimension(DimAnnot a); // create+drive+notify
std::string dim_text(const DimAnnot& a) const; // rendered label string
void render_dimensions(double unit_per_px); // quote lines + labels
// Draw ONE dimension's quote (extension/dimension lines, arrowheads, label) and
// return its label centre in out_label; false if the annot can't be drawn. Shared
// by render_dimensions (placed driving quotes) and render_live_quotes (live ones).
bool draw_dim_quote(const DimAnnot& a, double th, const ColorRGBA& col, Vec2d& out_label);
// Live, non-driving characteristic quotes for the entity being edited (point/handle
// drag, or a lone selection): the tool's defining dimensions shown Onshape-style so
// editing shows live values; click one (m_live_quotes) to promote it to a driving
// dim. Self-gates; skips a dim already driven on that entity.
void render_live_quotes(double unit_per_px);
// Iconic constraint badges (C3.4b): for each m_constrain_cons entry, append a
// small screen-constant glyph (H, V, ∥, ⊥, =, ○, …) near its primary entity into
// `out`; glyphs touching the same entity stack so they don't overlap.
void build_constraint_glyphs(double unit_per_px, std::vector<std::pair<Vec2d, Vec2d>>& out) const;
void draw_strokes(GLModel& model, const std::vector<std::pair<Vec2d, Vec2d>>& segs,
double hw, const ColorRGBA& color);
void draw_text(GLModel& model, const std::string& s, const Vec2d& center,
double height, const ColorRGBA& color); // GL stroke font
void draw_dim_label(const std::string& txt, const Vec2d& plane_center);
// Entity builders: append to m_entities (honoring the construction flag).
void push_line(const Vec2d& a, const Vec2d& b);
void push_closed_lines(const std::vector<Vec2d>& corners);
void push_open_chain(const std::vector<Vec2d>& pts);
void push_circle(const Vec2d& center, double radius);
void push_point(const Vec2d& p);
// Multi-click tool builders: return the entities for a finished gesture so
// both on_mouse (append) and render (preview) share one geometry path.
std::vector<SketchEntity> make_three_point_circle(const Vec2d& a, const Vec2d& b, const Vec2d& c) const;
std::vector<SketchEntity> make_three_point_arc(const Vec2d& start, const Vec2d& end, const Vec2d& on_arc) const;
std::vector<SketchEntity> make_tangent_arc(const Vec2d& start, const Vec2d& end) const;
// Center-start-end arc: click center, then start (sets radius), then a third
// point whose direction from the center sets the CCW end angle.
std::vector<SketchEntity> make_center_arc(const Vec2d& center, const Vec2d& start, const Vec2d& end_dir) const;
std::vector<SketchEntity> make_slot(const Vec2d& c0, const Vec2d& c1, double half_width) const;
std::vector<SketchEntity> make_arc_slot(const Vec2d& center, const Vec2d& start,
const Vec2d& end_dir, double half_width) const;
std::vector<SketchEntity> make_rounded_rect(const Vec2d& a, const Vec2d& b, const Vec2d& radius_pt) const;
std::vector<SketchEntity> rounded_rect_entities(double xmin, double ymin,
double xmax, double ymax, double r) const;
// Rounded-rect grouped edit: W/H/fillet-R labels rebuild the 8-entity span in place.
void open_rounded_rect_editor(int fi, int which); // 0=Width 1=Height 2=fillet R
void set_rounded_rect(int fi, double w, double h, double r);
// Arc-slot grouped edit: centreline-radius + width labels rebuild the 4-arc span.
void open_arc_slot_editor(int fi, bool radius); // true=centreline R, false=width
void set_arc_slot(int fi, double Rc, double w);
// Straight-slot grouped edit: centreline-length + width labels rebuild the 4-entity span.
void open_slot_editor(int fi, int which); // 0=inter-centre distance, 1=radius, 2=angle
void set_slot(int fi, double length, double w);
void set_slot_angle(int fi, double deg); // rotate the centreline about c0, keep len+radius
// Grouped derived-handle drag: resize an axis-aligned rect by a corner (opposite corner
// fixed); move a slot end by its cap centre. Both rebuild the feature span geometrically.
void drag_rect_corner(int fi, const Vec2d& cursor);
void drag_slot_handle(int fi, const Vec2d& cursor);
std::vector<SketchEntity> make_polygon(const Vec2d& center, const Vec2d& vertex, int sides) const;
// Ellipse: click center, then major-axis endpoint (sets a + rotation phi),
// then a point whose perpendicular distance to the major axis sets b.
std::vector<SketchEntity> make_ellipse(const Vec2d& center, const Vec2d& major_end,
const Vec2d& minor_pt) const;
// Elliptical arc: same 3 axis clicks, then start and end points whose parametric
// angles on the ellipse bound the CCW sweep.
std::vector<SketchEntity> make_bspline(const std::vector<Vec2d>& ctrl) const;
std::vector<SketchEntity> make_ellipse_arc(const Vec2d& center, const Vec2d& major_end,
const Vec2d& minor_pt, const Vec2d& start_pt,
const Vec2d& end_pt) const;
void append_entities(const std::vector<SketchEntity>& ents);
void draw_entities_preview(const std::vector<SketchEntity>& ents, const ColorRGBA& color);
// --- In-canvas edit-op gizmo (Fillet/Chamfer/Offset/Mirror toolbar tools) --------
// These replace the docked numeric card: pick the entities in-canvas, then a draggable
// arrow with a value label is projected toward the corner/centre (Fillet/Chamfer/Offset),
// or a two-phase pick (axis line, then targets) drives a live mirrored ghost. The
// SketchEngine op is recomputed live so a translucent ghost previews the result; confirm
// applies the geometry and binds constraints into m_constraints (try_add_constraints).
bool op_corner(int a, int b, Vec2d& C, Vec2d& bis, double& theta) const; // line-line vertex + inward bisector
void op_pick(int ei); // route an entity pick to the active op
void recompute_op_ghost(); // rebuild m_op_ghost from m_op_value
void render_op_gizmo(double unit_per_px); // ghost + arrow + value label (caches m_op_label)
bool hit_test_op_arrow(const Vec2d& p, double tol) const;
void drag_op_arrow(const Vec2d& target); // project cursor onto m_op_dir -> value
void open_op_editor(); // inline-edit the value label
void confirm_op(); // apply + bind, then reset for the next gesture
void reset_op(); // clear gizmo state (keeps the tool active)
bool op_ready() const; // required entities picked -> arrow/ghost live
// Sample an entity into a 2D polyline for the overlay renderer.
std::vector<Vec2d> entity_polyline(const SketchEntity& e, bool& closed) const;
// Closed regions formed by the current (non-construction) entities: each a CCW-
// ordered boundary polygon on the plane. A circle is its own region; line/arc
// chains are walked endpoint-to-endpoint into loops. Used to fill faces.
std::vector<std::vector<Vec2d>> closed_regions() const;
std::vector<std::vector<Vec2d>> closed_regions(const std::vector<SketchEntity>& ents) const;
// Same loops, but each carries the indices of the entities that form it — so a single
// loop can be highlighted / extruded on its own (per-region selection on the plate).
// A selectable sketch region: its own boundary, plus the loops nested INSIDE it, which
// are its holes. Modelling holes is what makes "the plate with the hole in it" a thing the
// user can point at — without it a sketch is N disjoint filled polygons and the only
// selectable things are the rectangle alone or the circle alone (snaporca-txp8).
struct RegionLoop {
std::vector<Vec2d> poly;
std::vector<int> ents;
std::vector<int> holes; // indices into the same vector; one nesting level
};
std::vector<RegionLoop> region_loops(const std::vector<SketchEntity>& ents) const;
// Index of the closed region containing plane-point p (point-in-polygon), or -1.
int region_at(const Vec2d& p) const;
void draw_quad_strip(GLModel& model, const std::vector<Vec2d>& pts, bool closed, const ColorRGBA& color);
// half_size is the square marker half-extent in PLANE units. Callers pass a
// zoom-scaled value (k / zoom) for screen-constant handles; the default keeps
// legacy point markers exactly as before.
void draw_vertices(GLModel& model, const std::vector<Vec2d>& pts, const ColorRGBA& color,
double half_size = 1.3);
void draw_fill(GLModel& model, const std::vector<Vec2d>& poly, const ColorRGBA& color);
// Same, with the region's holes cut out, so a selected plate-with-a-hole is drawn as an
// ANNULUS instead of a filled rectangle painted straight across its own bore.
void draw_fill_holed(GLModel& model, const std::vector<Vec2d>& outer,
const std::vector<std::vector<Vec2d>>& holes, const ColorRGBA& color);
const ColorRGBA* sketch_hl_color(int feature) const;
bool m_active{false};
SketchPlane m_plane;
std::vector<Vec2d> m_points; // clicks of the in-progress entity / chain
std::vector<SketchEntity> m_entities; // committed entities of this session
bool m_construction{false};
int m_polygon_sides{6};
bool m_polygon_circumscribed{false};
Vec2d m_cursor{0,0};
bool m_has_cursor{false};
bool m_snap_off{false}; // Shift held -> suppress angle snapping
InferenceSnap m_cursor_snap; // last cursor inference target (for hint render)
bool m_cursor_locked{false}; // rubber-band segment is angle-locked
bool m_awaiting_length{false}; // inline value editor open -> freeze canvas
int m_autoedit_seen{-1}; // entity count baseline for draw-then-edit
bool m_autoedit_pending{false};// a new entity just committed -> open editor
// Draw-then-edit step queue: every characteristic dimension of the freshly-drawn shape
// (scalar quote OR geometric editor) becomes one step, opened in sequence over its label.
struct AutoEditStep {
Vec2d label; // anchor (plane coords) — field opens over this
double value; // initial value shown
std::function<void(double)> apply; // commit: set the dimension
std::vector<int> hi; // entities to highlight while THIS field is open
std::string title; // label shown above the value field
};
std::vector<AutoEditStep> m_autoedit_dims; // queued steps to edit in sequence
int m_autoedit_dim_idx{-1}; // index into m_autoedit_dims (-1 = idle)
std::vector<int> m_selection; // selected entity indices (Mode::Select)
std::vector<std::pair<int, SketchPointRole>> m_point_sel; // selected individual points
int m_last_mouse_x{0}; // last cursor pos (canvas client px), for
int m_last_mouse_y{0}; // anchoring the in-canvas value editor
bool m_dragging_point{false}; // a point grab is in progress (Mode::Select)
int m_drag_ei{-1}; // entity whose point is being dragged
int m_drag_poly_fi{-1}; // >=0 if the grabbed point is a polygon
// vertex: drag scales+rotates the loop
int m_drag_rect_fi{-1}; // >=0 if dragging an axis-aligned rect corner
Vec2d m_drag_rect_anchor{0,0}; // the fixed (opposite) corner
int m_drag_slot_fi{-1}; // >=0 if dragging a slot cap centre
bool m_drag_slot_c1{false}; // true=cap@c1, false=cap@c0
SketchPointRole m_drag_role{SketchPointRole::P0};
std::vector<SketchEntityConstraintDef> m_constraints; // driving dims, committed on finish
// Onshape-style visual editing state.
bool m_show_handles{false}; // draw + interact with handles
bool m_dragging_handle{false};// a handle grab is in progress
Handle m_drag_handle; // the handle being dragged
bool m_has_hover_handle{false};// cursor is near a handle (highlight it)
Handle m_hover_handle; // the hovered handle (recomputed on move)
std::vector<DimAnnot> m_live_quotes; // live non-driving characteristic quotes,
// clickable to promote to driving dims
Vec2d m_live_poly_side_label{0,0}; // polygon side-length quote label
Vec2d m_live_poly_angle_label{0,0}; // polygon orientation quote label
int m_live_poly_fi{-1}; // their Feature (geometric edits)
Vec2d m_live_arc_angle_label{0,0}; // arc sweep-angle quote label
int m_live_arc_ei{-1}; // the arc it belongs to (geometric edit)
Vec2d m_live_ellipse_major_label{0,0}; // ellipse semi-major quote label
Vec2d m_live_ellipse_minor_label{0,0}; // ellipse semi-minor quote label
Vec2d m_live_ellipsearc_sweep_label{0,0}; // elliptical-arc sweep quote label
int m_live_ellipse_ei{-1}; // the ellipse the labels belong to
Vec2d m_live_obrect_angle_label{0,0}; // oblique-rect orientation quote label
int m_live_obrect_fi{-1}; // an OBLIQUE rect Feature (angle editable)
Vec2d m_live_rrect_w_label{0,0}; // rounded-rect width quote label
Vec2d m_live_rrect_h_label{0,0}; // rounded-rect height quote label
Vec2d m_live_rrect_r_label{0,0}; // rounded-rect fillet-radius label
int m_live_rrect_fi{-1}; // the rounded-rect Feature (rebuild edits)
Vec2d m_live_aslot_r_label{0,0}; // arc-slot centreline-radius label
Vec2d m_live_aslot_w_label{0,0}; // arc-slot width label
int m_live_aslot_fi{-1}; // the arc-slot Feature (rebuild edits)
Vec2d m_live_slot_len_label{0,0}; // straight-slot inter-centre distance label
Vec2d m_live_slot_w_label{0,0}; // straight-slot radius (half-width) label
Vec2d m_live_slot_angle_label{0,0}; // straight-slot centreline angle label
int m_live_slot_fi{-1}; // the straight-slot Feature (rebuild edits)
std::vector<Feature> m_features; // parametric groups over m_entities
int m_open_feature{-1}; // index of the Feature being built, or -1
// In-canvas edit-op gizmo state (Fillet/Chamfer/Offset/Mirror). GUI-only, reset by
// set_tool/cancel. Fillet/Chamfer: m_op_a,m_op_b = the two lines; Offset: m_op_a = src;
// Mirror: m_op_a = axis line, m_mirror_targets = entities to mirror.
int m_op_a{-1};
int m_op_b{-1};
double m_op_value{0.0}; // radius / setback / signed offset distance
Vec2d m_op_anchor{0,0}; // arrow base (corner vertex / entity midpoint)
Vec2d m_op_dir{0,0}; // unit arrow direction (inward bisector / outward normal)
Vec2d m_op_label{1e18,1e18}; // cached arrow-label centre, for picking
std::vector<SketchEntity> m_op_ghost; // live result preview (recomputed on value change)
bool m_op_dragging_arrow{false}; // arrowhead drag in progress
std::vector<int> m_mirror_targets; // Mirror: entities to be mirrored (axis = m_op_a)
// In-canvas imported-art transform gizmo (Mode::TransformArt). GUI-only. The art's
// untransformed contours + its bbox in base coords; the live offset/scale; the grabbed
// handle (0..3 = corners, 4 = centre move, -1 = none) and the fixed world anchor (the
// opposite corner during a corner-scale drag).
std::vector<std::vector<std::vector<Vec2d>>> m_xform_base;
int m_xform_feat{-1};
Vec2d m_xform_min{0,0}, m_xform_max{0,0}; // bbox of m_xform_base (untransformed)
Vec2d m_xform_offset{0,0};
double m_xform_sx{1.0}, m_xform_sy{1.0};
int m_xform_handle{-1};
Vec2d m_xform_anchor{0,0};
void xform_world_corners(Vec2d out[4]) const; // 4 bbox corners in plane coords
int hit_test_xform_handle(const Vec2d& p, double tol) const;
void drag_xform_handle(const Vec2d& target);
void render_xform_gizmo();
void emit_xform();
void reset_xform();
// In-canvas transform gizmo state (Mode::Move/Rotate/Scale/Array/PolarArray). GUI-only,
// reset by set_tool/cancel. Pick one or more subject entities (m_tf_targets), then a
// single draggable handle drives the continuous parameter and a live translucent ghost
// previews the result; Array/PolarArray add a second editable label for the copy count.
// Mutating ops (Move/Rotate/Scale) drop the constraint classes the map invalidates;
// additive ops (Array/PolarArray) bind each copy to its source. See confirm_transform().
std::vector<int> m_tf_targets; // picked subject entity indices
Vec2d m_tf_pivot{0,0}; // rotate/scale/polar pivot = set centroid
Vec2d m_tf_delta{0,0}; // Move translation / Array per-step vector
double m_tf_angle{0.0}; // Rotate angle / PolarArray total sweep (rad)
double m_tf_scale{1.0}; // Scale factor
int m_tf_count{3}; // Array/PolarArray copy count (incl. original)
double m_tf_handle_r{1.0}; // ring/handle reference radius (set on pick)
std::vector<SketchEntity> m_tf_ghost; // live result preview
int m_tf_handle{-1}; // 0 = primary drag handle grabbed, -1 = none
bool m_tf_dragging{false};
Vec2d m_tf_label_a{1e18,1e18}; // primary-param label centre (picking)
Vec2d m_tf_label_b{1e18,1e18}; // count label centre (Array/PolarArray)
bool tf_ready() const; // >=1 target picked -> gizmo + ghost live
void tf_pick(int ei); // accumulate a subject, seed defaults once
void compute_tf_pivot(); // centroid + extent of the target set
void recompute_tf_ghost();
Vec2d tf_handle_pos() const; // world position of the drag handle
bool hit_test_tf_handle(const Vec2d& p, double tol) const;
void drag_tf_handle(const Vec2d& target);
void render_tf_gizmo(double unit_per_px);
void open_tf_editor_a(); // inline-edit the continuous parameter
void open_tf_editor_count(); // inline-edit the copy count
void confirm_transform(); // apply geometry + constraint web
void reset_tf();
// DoF feedback state, refreshed by resolve_live() from the libslvs solve result.
int m_dof{-1}; // remaining DoF; 0 = fully constrained, <0 = unknown
bool m_solve_ok{true}; // solver consistent (no conflicting constraints)
std::vector<char> m_entity_conflict; // per-entity flag: touched by a conflicting constraint
std::vector<DimAnnot> m_dimensions; // placed dimension quotes (Mode::Dimension)
int m_dim_e0{-1}; // first picked point's entity (Dimension)
SketchPointRole m_dim_r0{SketchPointRole::P0};
bool m_dim_has0{false}; // a first point is pending
int m_pending_dim{-1}; // dim awaiting a value-card entry
Mode m_mode{Mode::Polyline};
int m_sel_a{-1}; // picked segment endpoints (legacy Constrain mode)
int m_sel_b{-1};
bool m_constrain_entities{false}; // Constrain mode acts on entities
int m_pick0{-1}; // picked line-entity indices (entity Constrain)
int m_pick1{-1};
int m_pick2{-1}; // third slot (Symmetric axis)
Vec2d m_pick0_pt{0,0}; // plane-coords of the slot-0 pick (trim/extend)
std::vector<int> m_constraint_hl; // entities highlighted by the constraint manager
std::vector<SketchEntityConstraintDef> m_constrain_cons; // for glyph badges (C3.4b)
GLModel m_line_model;
GLModel m_vertex_model;
GLModel m_highlight_model;
int m_dim_label_seq{0};
float m_render_scale{1.0f}; // canvas scale for Measure-style dim labels
GLModel m_fill_model; // translucent face fill for closed regions
std::vector<DisplaySketch> m_display_sketches; // committed sketches drawn persistently
std::vector<std::pair<int, ColorRGBA>> m_hl_sketches; // feature index -> outline colour (Sweep/Loft operands)
int m_display_pick{-1}; // FEATURE index of the click-selected display sketch (-1 none)
// Solid (whole/face/edge) selection on the committed bodies. Pointers are non-owning,
// into CadDocument (bodies + display_mesh + per-triangle face/body ids), refreshed each
// recompute via set_solid_pick. m_sel_edge_pts caches the picked edge's world polyline.
const std::vector<CadBody>* m_solid_bodies{nullptr};
const TriangleMesh* m_solid_mesh{nullptr};
const std::vector<int>* m_solid_tri_face{nullptr};
const std::vector<int>* m_solid_tri_body{nullptr};
const std::vector<bool>* m_solid_visible{nullptr}; // per-body visibility; hidden bodies aren't pickable
int m_pick_only_body{-1}; // >=0: only this body catches clicks (body-focus x-ray for CoordSys picking)
const std::vector<Transform3d>* m_solid_xform{nullptr}; // per-body display transform (for edge sampling)
Vec3d body_xform_pt(int body, const Vec3d& p) const; // map an OCCT-shape point through the body xform
bool body_pickable(int b) const; // false when the body is explicitly hidden
SolidSel m_solid_sel{SolidSel::None};
int m_sel_body{-1}; // which body the face/edge selection is on
int m_sel_face{-1};
int m_sel_edge{-1};
std::vector<Vec3d> m_sel_edge_pts;
Vec3d m_sel_vertex_pt{Vec3d::Zero()}; // world point of a picked vertex
bool handle_solid_click(GLCanvas3D& canvas, const wxMouseEvent& evt); // pick + notify
// What a click at (mx,my) WOULD take, resolved without touching the selection. One
// implementation, two callers: the click, and the hover pre-highlight that promises what the
// click is about to do. Split so the promise cannot drift from the act.
struct SolidPick {
SolidSel kind{SolidSel::None};
int body{-1}, face{-1}, edge{-1};
std::vector<Vec3d> edge_pts;
Vec3d vertex_pt{Vec3d::Zero()};
};
bool resolve_solid_pick(GLCanvas3D& canvas, int mx, int my, SolidPick& out) const;
// HOVER PRE-HIGHLIGHT (snaporca-9xw part 3). Vertex-beats-edge-beats-face is a rule the user
// cannot see until after they commit to a click; showing the outcome under the pointer is
// what makes the precedence learnable at all, and is the charter's L5 (one click, one visible
// change) read honestly — the change has to be predictable before the click, not only after.
SolidPick m_pre; // what the pointer is currently over (kind None = nothing)
bool update_solid_hover(GLCanvas3D& canvas, const wxMouseEvent& evt); // true when it changed
// Left-drag rubber band: sweep a rectangle over the plate to take a whole body. Orbit
// moves to middle-drag in this canvas (DesignCanvas::set_cad_navigation) so the left
// button is free for it, which is the CAD convention (Onshape/SolidWorks).
GLSelectionRectangle m_rubber;
void pick_bodies_in_rectangle(); // resolve the swept rectangle -> whole-body selection
bool on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas); // the body; on_mouse wraps it
// Nearest stroke + enclosing region of ONE committed sketch. Shared by the click and
// double-click paths so they cannot disagree about what is under the pointer.
void hit_display_sketch(const DisplaySketch& d, const Vec2d& p, double tol,
int& edge_feat, int& edge_reg, int& edge_ent,
double& edge_d, int& face_feat, int& face_reg) const;
bool m_right_consumed{false}; // last RightDown was a gesture terminator, not a menu
bool m_escalate_repick{true}; // re-picking the same sub-element takes the whole body
void render_solid_highlight();
// The shared body of the above: one highlight from explicit arguments, so the committed
// selection and the hover pre-highlight cannot drift apart in how they look.
void render_solid_sel(SolidSel kind, int body, int face, const std::vector<Vec3d>& edge_pts,
const Vec3d& vertex_pt, const ColorRGBA& rgb, float alpha_mul);
void render_datum_planes(); // translucent rectangles for datum/reference planes
void render_view_helpers(); // world origin planes + axis triad (P / A toggles)
bool m_show_planes{false};
bool m_show_axes{false};
std::vector<SketchPlane> m_datum_planes;
std::vector<Vec2d> m_datum_sizes; // per-plane (u,v) full extent; empty -> default
void render_mate_connectors(); // disc + roll quadrant + one-sided Z arrow
std::vector<MateConnectorGlyph> m_mate_connectors;
GLModel m_mc_stroke_model;
GLModel m_solid_face_model;
GLModel m_solid_edge_model;
GLModel m_solid_vertex_model;
int m_display_pick_region{-1}; // selected closed-region index within that feature (-1 none)
// Visual Extrude gizmo state (C5b). GUI-only; fed by the panel each refresh_preview.
bool m_ex_active{false};
SketchPlane m_ex_plane; // profile plane (gives normal + to_world anchor)
Vec2d m_ex_centroid{0,0}; // arrow base in plane coords (profile centroid)
double m_ex_depth{0.0}; // primary depth (= m_distance)
double m_ex_depth2{0.0}; // second-side depth (TwoSided, = m_distance2)
bool m_ex_two_sided{false};
bool m_ex_flip{false};
int m_ex_drag{-1}; // 0 = primary arrow, 1 = second arrow, -1 = none
int m_ex_press_x{0}, m_ex_press_y{0}; // press px to tell click-to-edit from drag
void render_extrude_gizmo();
bool hit_test_extrude_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int& which) const;
void drag_extrude_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int which);
void open_extrude_editor(int which);
GLModel m_ex_arrow_model;
// Datum-plane resize gizmo state (C3). GUI-only; fed by the panel while the Plane card is open.
bool m_dz_active{false};
SketchPlane m_dz_plane; // resolved datum frame (origin + axes)
double m_dz_usize{60.0}; // current u extent (full width)
double m_dz_vsize{60.0}; // current v extent (full height)
int m_dz_drag{-1}; // 0=+u,1=-u,2=+v,3=-v handle, 4=offset tip, -1 none
int m_dz_press_x{0}, m_dz_press_y{0};
Vec3d m_dz_anchor{Vec3d::Zero()}; // base-plane origin (offset arrow tail)
Vec3d m_dz_normal{0.0, 0.0, 1.0}; // base normal (offset arrow direction)
double m_dz_offset{0.0}; // current signed offset along the base normal
bool m_dz_offset_on{false}; // draw/allow the offset arrow (Offset-from-base only)
void render_datum_gizmo();
bool hit_test_datum_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int& which) const;
void drag_datum_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int which);
// Helix gizmo state (plane-anchored curve + 3 drag handles). Fed by the panel while the
// Helix card is open (sketch tool NOT active); the tool draws the live helix plus a handle
// on each length parameter (radius/height/pitch). Taper and handedness stay on the card.
bool m_hx_active{false};
SketchPlane m_hx_plane; // axis = plane normal, base circle in the plane
double m_hx_radius{10.0};
double m_hx_pitch{2.0};
double m_hx_height{20.0};
double m_hx_taper{0.0}; // DEGREES (cone half-angle), as the kernel reads it
bool m_hx_left{false};
int m_hx_drag{-1}; // 0=radius, 1=height, 2=pitch, -1 none
int m_hx_press_x{0}, m_hx_press_y{0};
Vec3d helix_point(double t) const; // curve point at parameter t (shared render/hit/drag)
void render_helix_gizmo();
bool hit_test_helix_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int& which) const;
void drag_helix_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int which);
// Rib thickness gizmo state (plane-anchored slab footprint + 2 drag handles). Fed by the
// panel while the Rib card is open (sketch tool NOT active); the tool draws the rib's
// footprint outline and a handle on each side of the line at half-thickness. Dragging either
// handle sets the full thickness (the slab is centred on the line).
bool m_rb_active{false};
SketchPlane m_rb_plane;
Vec2d m_rb_p0{Vec2d::Zero()}; // rib line endpoints, in plane coords
Vec2d m_rb_p1{Vec2d::Zero()};
double m_rb_thickness{2.0};
int m_rb_drag{-1}; // 0 = +perp handle, 1 = -perp handle, -1 none
void render_rib_gizmo();
bool hit_test_rib_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int& which) const;
void drag_rib_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int which);
// Datum base picker (translucent clickable origin/datum planes)
bool m_dbp_active{false};
std::vector<SketchPlane> m_dbp_planes;
std::vector<int> m_dbp_base;
std::vector<std::string> m_dbp_labels;
int m_dbp_hover{-1};
double dbp_half_extent() const; // bed-derived: reference planes are larger than the bed
void render_base_pick();
int hit_test_base_pick(GLCanvas3D& canvas, const wxMouseEvent& evt) const;
// Move-body gizmo state: 3 world-axis translate arrows + 3 world-axis rotate rings.
// Delta model: offset/rot are deltas about a fixed pivot, composed onto m_mv_base_xform
// (the body's pose when Move opened) so rotation works even on an already-placed body.
bool m_mv_active{false};
int m_mv_body{-1};
Vec3d m_mv_base{Vec3d::Zero()}; // pivot = body's world centroid at Move-open
Vec3d m_mv_offset{Vec3d::Zero()}; // delta translation along world X/Y/Z
Transform3d m_mv_base_xform{Transform3d::Identity()}; // pose when Move opened
Eigen::Matrix3d m_mv_rot{Eigen::Matrix3d::Identity()}; // accumulated delta rotation (world, about pivot)
Eigen::Matrix3d m_mv_rot_start{Eigen::Matrix3d::Identity()}; // rot snapshot at arc-drag start
double m_mv_arc_a0{0.0}; // mouse angle on the ring at drag start
int m_mv_drag{-1}; // 0..2 = X/Y/Z arrow, 3..5 = X/Y/Z ring, -1 none
double m_mv_radius{0.0}; // body bounding-sphere radius (mm); 0 = unknown
int m_mv_press_x{0}, m_mv_press_y{0};
Transform3d compose_move_xform() const; // T(offset)*T(pivot)*rot*T(-pivot)*base_xform
void ring_basis(int axis, Vec3d& e, Vec3d& u, Vec3d& v) const; // world axis + in-plane basis
void render_move_gizmo();
// Gizmo arm length (world mm): scales with the body so the rings clear its surface.
double move_gizmo_arm(const Camera& cam) const;
bool hit_test_move_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int& axis) const;
bool hit_test_move_arc(GLCanvas3D& canvas, const wxMouseEvent& evt, int& axis) const;
void drag_move_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis);
void drag_move_arc(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis);
bool arc_mouse_angle(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis, double& ang) const;
void open_move_editor(int axis);
GLModel m_mv_arrow_model;
// Fillet/Chamfer radius gizmo state (single world-space arrow at the picked edge midpoint).
bool m_fl_active{false};
Vec3d m_fl_anchor{Vec3d::Zero()}; // edge midpoint (world, already body-transformed)
Vec3d m_fl_dir{Vec3d::UnitZ()}; // unit radius direction (perp to edge, outward)
double m_fl_radius{1.0}; // current radius (= dressup size)
bool m_fl_drag{false};
int m_fl_press_x{0}, m_fl_press_y{0};
double m_fl_grab_proj{0.0}; // axis projection at grab (relative drag reference)
double m_fl_grab_radius{1.0}; // radius at grab (relative drag reference)
void render_fillet_gizmo();
bool hit_test_fillet_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt) const;
double fillet_axis_proj(GLCanvas3D& canvas, const wxMouseEvent& evt) const; // NaN if camera∥axis
void start_fillet_drag(GLCanvas3D& canvas, const wxMouseEvent& evt);
void drag_fillet_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt);
void open_fillet_editor();
GLModel m_fl_arrow_model;
// Hole gizmo state. The hole is a positioned circular cut on m_hl_plane at (m_hl_x, m_hl_y);
// the footprint circle is drawn on the plane, the diameter arrow runs along the plane u-axis,
// the depth arrow along +normal (matching the kernel's make_extrude). Three draggable handles:
// 0 = centre (reposition in plane u/v), 1 = diameter, 2 = depth (only shown when !through).
bool m_hl_active{false};
SketchPlane m_hl_plane;
double m_hl_x{0.0}, m_hl_y{0.0}; // centre on the plane (u/v mm)
double m_hl_diameter{6.0};
double m_hl_depth{10.0};
bool m_hl_through{true};
// #2 Part B: face (u,v) bounds, so the construction dims read as distance from the face SIDES
// (umin/vmin = two adjacent edges) rather than from the centre. Off for a dropdown-plane hole.
bool m_hl_has_bounds{false};
double m_hl_umin{0}, m_hl_umax{0}, m_hl_vmin{0}, m_hl_vmax{0};
int m_hl_drag{-1}; // 0=centre, 1=diameter, 2=depth, 3=X-dim, 4=Y-dim, -1=none
int m_hl_press_x{0}, m_hl_press_y{0};
double m_hl_grab_proj{0.0}; // diameter/depth axis projection at grab (relative)
double m_hl_grab_val{0.0}; // radius (diameter drag) or depth at grab
Vec2d m_hl_grab_uv{0.0, 0.0}; // centre drag: plane-projected grab point
double m_hl_grab_x{0.0}, m_hl_grab_y{0.0}; // centre drag: x/y at grab
void render_hole_gizmo();
int hit_test_hole_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const; // 0/1/2/-1
double hole_axis_proj(GLCanvas3D& canvas, const wxMouseEvent& evt,
const Vec3d& anchor, const Vec3d& dir) const; // NaN if camera∥axis
void start_hole_drag(GLCanvas3D& canvas, const wxMouseEvent& evt, int which);
void drag_hole_handle(GLCanvas3D& canvas, const wxMouseEvent& evt);
void open_hole_editor(int which);
GLModel m_hl_stroke_model;
// Thread gizmo state (mirrors the hole gizmo; radius arrow uses an R label, length arrow is
// always shown). Handles: 0 = centre (thread_x/y), 1 = radius, 2 = length.
bool m_th_active{false};
SketchPlane m_th_plane;
double m_th_x{0.0}, m_th_y{0.0};
double m_th_radius{5.0};
double m_th_height{10.0};
int m_th_drag{-1}; // 0=centre, 1=radius, 2=length, -1=none
int m_th_press_x{0}, m_th_press_y{0};
double m_th_grab_proj{0.0};
double m_th_grab_val{0.0};
Vec2d m_th_grab_uv{0.0, 0.0};
double m_th_grab_x{0.0}, m_th_grab_y{0.0};
void render_thread_gizmo();
int hit_test_thread_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const; // 0/1/2/-1
void start_thread_drag(GLCanvas3D& canvas, const wxMouseEvent& evt, int which);
void drag_thread_handle(GLCanvas3D& canvas, const wxMouseEvent& evt);
void open_thread_editor(int which);
GLModel m_th_stroke_model;
// Shell gizmo state (single inward thickness arrow at the picked face centroid).
bool m_sh_active{false};
Vec3d m_sh_anchor{Vec3d::Zero()}; // picked face centroid (world)
Vec3d m_sh_dir{Vec3d::UnitZ()}; // inward unit direction (-outward normal)
double m_sh_thickness{2.0};
bool m_sh_drag{false};
int m_sh_press_x{0}, m_sh_press_y{0};
double m_sh_grab_proj{0.0};
double m_sh_grab_val{2.0};
void render_shell_gizmo();
bool hit_test_shell_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt) const;
void start_shell_drag(GLCanvas3D& canvas, const wxMouseEvent& evt);
void drag_shell_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt);
void open_shell_editor();
GLModel m_sh_stroke_model;
// Revolve gizmo state (arc center = projection of the profile centroid onto the axis).
bool m_rv_active{false};
Vec3d m_rv_center{Vec3d::Zero()}; // arc center on the axis (world)
Vec3d m_rv_axis{Vec3d::UnitX()}; // revolve axis unit dir (world)
Vec3d m_rv_ref{Vec3d::UnitY()}; // angle-0 reference dir (perp to axis, toward profile)
double m_rv_radius{10.0}; // arc radius = profile perpendicular distance (world)
double m_rv_angle{360.0}; // current sweep magnitude (deg, 1..360)
bool m_rv_flip{false}; // sweep sense (matches the kernel's negative-angle flip)
bool m_rv_drag{false};
int m_rv_press_x{0}, m_rv_press_y{0};
void render_revolve_gizmo();
bool hit_test_revolve_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const;
void drag_revolve_arc(GLCanvas3D& canvas, const wxMouseEvent& evt);
void open_revolve_editor();
GLModel m_rv_stroke_model;
// Draft gizmo state (arc center = picked face centroid; axis = world +Z, taper pull direction).
bool m_dr_active{false};
Vec3d m_dr_center{Vec3d::Zero()}; // arc center = face centroid (world)
Vec3d m_dr_axis{Vec3d::UnitZ()}; // draft axis = world +Z (pull direction)
Vec3d m_dr_ref{Vec3d::UnitX()}; // angle-0 reference dir (perp to axis)
double m_dr_radius{10.0}; // arc radius (world)
double m_dr_angle{5.0}; // current sweep magnitude (deg, [-89, 89])
bool m_dr_drag{false};
int m_dr_press_x{0}, m_dr_press_y{0};
void render_draft_gizmo();
bool hit_test_draft_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const;
void drag_draft_arc(GLCanvas3D& canvas, const wxMouseEvent& evt);
GLModel m_dr_stroke_model;
std::function<void(double)> m_on_draft_angle_changed;
// Cut gizmo state (plane normal arrow + wire rectangle at the current offset).
bool m_ct_active{false};
Vec3d m_ct_base{Vec3d::Zero()}; // body centre projected into the cut plane
Vec3d m_ct_n{Vec3d::UnitZ()}; // cut plane normal (unit)
Vec3d m_ct_u{Vec3d::UnitX()}; // cut plane U axis (unit)
Vec3d m_ct_v{Vec3d::UnitY()}; // cut plane V axis (unit)
double m_ct_offset{0.0};
double m_ct_half{10.0};
bool m_ct_drag{false};
double m_ct_grab_val{0.0};
double m_ct_grab_proj{0.0};
void render_cut_gizmo();
bool hit_test_cut_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt) const;
void start_cut_drag(GLCanvas3D& canvas, const wxMouseEvent& evt);
void drag_cut_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt);
GLModel m_ct_stroke_model;
GLModel m_ct_rect_model;
std::function<void(double)> m_on_cut_offset_changed;
// Pattern gizmo state. Linear arrow along m_pt_dirw from m_pt_base; circular arc like Revolve
// but axis = m_pt_normal through m_pt_origin (the world XY plane by default).
bool m_pt_active{false};
bool m_pt_circular{false};
Vec3d m_pt_base{Vec3d::Zero()}; // target body centroid (world): linear anchor / radius ref
Vec3d m_pt_dirw{Vec3d::UnitX()}; // linear march direction (world)
Vec3d m_pt_origin{Vec3d::Zero()}; // circular rotation axis origin (world)
Vec3d m_pt_normal{Vec3d::UnitZ()}; // circular rotation axis (world)
Vec3d m_pt_cref{Vec3d::UnitX()}; // circular angle-0 reference dir (perp to normal, toward body)
Vec3d m_pt_ccenter{Vec3d::Zero()}; // circular arc center (foot of body centroid on the axis)
double m_pt_radius{10.0}; // circular arc radius (world)
int m_pt_count{3};
double m_pt_spacing{20.0};
double m_pt_angle{360.0};
bool m_pt_drag{false};
int m_pt_press_x{0}, m_pt_press_y{0};
void render_pattern_gizmo();
bool hit_test_pattern_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const;
void drag_pattern_handle(GLCanvas3D& canvas, const wxMouseEvent& evt);
void open_pattern_editor();
GLModel m_pt_stroke_model;
};
}} // namespace Slic3r::GUI
#endif // slic3r_DesignSketchTool_hpp_