mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 14:32:36 +00:00
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>
10003 lines
473 KiB
C++
10003 lines
473 KiB
C++
#include "DesignSketchTool.hpp"
|
||
#include "GLCanvas3D.hpp"
|
||
#include "GUI_App.hpp"
|
||
#include "ImGuiWrapper.hpp"
|
||
#include "Plater.hpp"
|
||
|
||
#include <imgui/imgui.h>
|
||
#include <imgui/imgui_internal.h>
|
||
#include "libslic3r/BuildVolume.hpp"
|
||
#include "Camera.hpp"
|
||
#include "3DScene.hpp"
|
||
#include "GLShader.hpp"
|
||
#include "libslic3r/GeometryEngine.hpp"
|
||
#include "libslic3r/TriangleMesh.hpp"
|
||
|
||
#include <GL/glew.h>
|
||
#include <wx/gdicmn.h>
|
||
#include <algorithm>
|
||
#include <climits>
|
||
#include <limits>
|
||
#include <array>
|
||
#include <cmath>
|
||
#include <cstdio>
|
||
#include <cstdarg>
|
||
#include <cstdlib>
|
||
|
||
namespace Slic3r {
|
||
namespace GUI {
|
||
|
||
// Positioning helpers (defined lower down, used by the dimension methods above them).
|
||
static bool entity_ref_point(const SketchEntity& e, Vec2d& out);
|
||
static void translate_entity(SketchEntity& e, const Vec2d& d);
|
||
// Pick-distance helpers (defined lower down; used earlier by the edit-op gizmo).
|
||
static double point_segment_dist(const Vec2d& p, const Vec2d& a, const Vec2d& b);
|
||
static double entity_pick_dist(const Vec2d& p, const SketchEntity& e);
|
||
static bool point_in_poly(const Vec2d& q, const std::vector<Vec2d>& poly);
|
||
static bool ray_triangle(const Vec3d& ro, const Vec3d& rd, const Vec3d& v0, const Vec3d& v1,
|
||
const Vec3d& v2, double& t);
|
||
static double ray_segment_dist3(const Vec3d& ro, const Vec3d& rd, const Vec3d& a, const Vec3d& b);
|
||
|
||
// Project a world-space point to canvas screen pixels (device px, GL viewport units;
|
||
// origin top-left after the GL y-flip). Mirrors GLCanvas3D's world->screen pattern:
|
||
// projection * view, perspective divide, NDC -> viewport. Returns (-1,-1) if behind.
|
||
// (Retained for auto-emitted dimensions in Phase B, which have no click to anchor to;
|
||
// needs the design canvas's own camera/viewport, not the plater's.)
|
||
[[maybe_unused]] static wxPoint world_to_screen_px(const Camera& cam, const Vec3d& world)
|
||
{
|
||
// NB: multiply the raw 4x4 matrices, NOT the Transform3d objects — the projection is not
|
||
// affine, so Transform*Transform mangles it (Eigen assumes affine) and yields garbage w.
|
||
const Eigen::Matrix4d m = cam.get_projection_matrix().matrix() * cam.get_view_matrix().matrix();
|
||
const Eigen::Vector4d clip = m * world.homogeneous();
|
||
if (std::abs(clip.w()) < 1e-9) return wxPoint(-1, -1);
|
||
const Vec3d ndc = clip.head<3>() / clip.w();
|
||
const std::array<int, 4>& vp = cam.get_viewport();
|
||
const double sx = vp[0] + (ndc.x() * 0.5 + 0.5) * vp[2];
|
||
const double sy = vp[1] + (1.0 - (ndc.y() * 0.5 + 0.5)) * vp[3]; // GL y-up -> wx y-down
|
||
return wxPoint(int(sx + 0.5), int(sy + 0.5));
|
||
}
|
||
|
||
void DesignSketchTool::begin(const SketchPlane& plane, Mode mode)
|
||
{
|
||
m_plane = plane;
|
||
m_mode = mode;
|
||
m_points.clear();
|
||
m_entities.clear();
|
||
m_construction = false;
|
||
m_has_cursor = false;
|
||
m_sel_a = m_sel_b = -1;
|
||
m_constrain_entities = false;
|
||
m_pick0 = m_pick1 = m_pick2 = -1;
|
||
m_constraint_hl.clear();
|
||
m_constrain_cons.clear();
|
||
m_awaiting_length = false;
|
||
m_autoedit_seen = 0; // baseline: no entities yet; first commit triggers edit
|
||
m_autoedit_pending = false;
|
||
m_selection.clear();
|
||
m_point_sel.clear();
|
||
m_constraints.clear();
|
||
m_dimensions.clear();
|
||
m_dim_has0 = false;
|
||
m_pending_dim = -1;
|
||
m_dof = -1; m_solve_ok = true; m_entity_conflict.clear();
|
||
m_features.clear();
|
||
m_open_feature = -1;
|
||
m_active = true;
|
||
}
|
||
|
||
// Re-open a committed entity sketch for editing: mirror begin() (fresh Select session),
|
||
// then load the geometry + driving constraints, re-detect feature groups, and live-solve
|
||
// so handles/quotes/regular-drag work exactly as in the original draw session.
|
||
void DesignSketchTool::begin_edit(const std::vector<SketchEntity>& entities,
|
||
const std::vector<SketchEntityConstraintDef>& constraints,
|
||
const SketchPlane& plane)
|
||
{
|
||
begin(plane, Mode::Select);
|
||
m_entities = entities;
|
||
m_constraints = constraints;
|
||
rebuild_features_from_entities();
|
||
resolve_live();
|
||
}
|
||
|
||
// Walk the entity list grouping each consecutive CLOSED chain (entities are stored in
|
||
// gesture order, so a polygon/rect/slot's members are contiguous and end-to-end linked)
|
||
// and classify it: L,A,L,A -> Slot; 4 arcs (2 concentric + 2 caps) -> ArcSlot;
|
||
// L,A×4 (4 equal-radius corner arcs) -> RoundedRect; 4 right-angled lines -> Rect;
|
||
// N equal-length lines -> Polygon. Single/irregular entities stay ungrouped (they fall
|
||
// back to per-entity quotes). This reconstructs m_features for a re-opened sketch.
|
||
void DesignSketchTool::rebuild_features_from_entities()
|
||
{
|
||
m_features.clear();
|
||
m_open_feature = -1;
|
||
const int n = int(m_entities.size());
|
||
using T = SketchEntity::Type;
|
||
auto start = [&](int i) { return m_entities[i].p0; };
|
||
auto end = [&](int i) { const SketchEntity& e = m_entities[i];
|
||
return (e.type == T::Line || e.type == T::Arc) ? e.p1 : e.p0; };
|
||
auto is_near = [](const Vec2d& a, const Vec2d& b) {
|
||
return (a - b).norm() <= 0.05 + 1e-3 * std::max(a.norm(), b.norm()); };
|
||
|
||
int i = 0;
|
||
while (i < n) {
|
||
int j = i; bool closed = false; // greedily extend a consecutive chain
|
||
while (j + 1 < n && is_near(end(j), start(j + 1))) {
|
||
++j;
|
||
if ((j - i) >= 2 && is_near(end(j), start(i))) { closed = true; break; }
|
||
}
|
||
const int cnt = j - i + 1;
|
||
if (!closed || cnt < 3) { ++i; continue; }
|
||
|
||
int arcs = 0; bool all_line = true;
|
||
for (int k = i; k <= j; ++k) {
|
||
if (m_entities[k].type == T::Arc) ++arcs;
|
||
if (m_entities[k].type != T::Line) all_line = false;
|
||
}
|
||
Vec2d c(0, 0); for (int k = i; k <= j; ++k) c += start(k); c /= double(cnt);
|
||
Feature f; f.begin = i; f.end = j + 1; f.c0 = c;
|
||
|
||
if (cnt == 4 && arcs == 2 &&
|
||
m_entities[i].type == T::Line && m_entities[i + 1].type == T::Arc &&
|
||
m_entities[i + 2].type == T::Line && m_entities[i + 3].type == T::Arc) {
|
||
f.kind = FeatureKind::Slot; // make_slot: top, cap@c1, bottom, cap@c0
|
||
f.c0 = m_entities[i + 3].center;
|
||
f.c1 = m_entities[i + 1].center;
|
||
f.param = m_entities[i + 1].radius;
|
||
m_features.push_back(f);
|
||
} else if (cnt == 4 && arcs == 4 &&
|
||
is_near(m_entities[i].center, m_entities[i + 2].center) &&
|
||
std::abs(m_entities[i + 1].radius - m_entities[i + 3].radius) <=
|
||
0.02 * std::max(m_entities[i + 1].radius, 1e-6) &&
|
||
m_entities[i].radius > m_entities[i + 2].radius) {
|
||
// make_arc_slot: [0]outer(Rc+w), [1]cap@E(w), [2]inner(Rc-w), [3]cap@S(w).
|
||
// c0=main centre, c1=centreline start (cap@S centre = Sc), param=half-width.
|
||
f.kind = FeatureKind::ArcSlot;
|
||
f.c0 = m_entities[i].center;
|
||
f.c1 = m_entities[i + 3].center;
|
||
f.param = m_entities[i + 3].radius;
|
||
m_features.push_back(f);
|
||
} else if (cnt == 8 && arcs == 4 &&
|
||
m_entities[i].type == T::Line && m_entities[i + 1].type == T::Arc &&
|
||
m_entities[i + 2].type == T::Line && m_entities[i + 3].type == T::Arc &&
|
||
m_entities[i + 4].type == T::Line && m_entities[i + 5].type == T::Arc &&
|
||
m_entities[i + 6].type == T::Line && m_entities[i + 7].type == T::Arc) {
|
||
// rounded_rect_entities: 4 corner arcs (equal radius r) at the inset corners.
|
||
// Recover the axis-aligned bounds from the arc centres ± r.
|
||
const double r = m_entities[i + 1].radius;
|
||
bool equal_r = true;
|
||
for (int k : {3, 5, 7})
|
||
if (std::abs(m_entities[i + k].radius - r) > 0.02 * std::max(r, 1e-6))
|
||
equal_r = false;
|
||
if (equal_r && r > 1e-6) {
|
||
double cxmin = 1e18, cxmax = -1e18, cymin = 1e18, cymax = -1e18;
|
||
for (int k : {1, 3, 5, 7}) {
|
||
const Vec2d& o = m_entities[i + k].center;
|
||
cxmin = std::min(cxmin, o.x()); cxmax = std::max(cxmax, o.x());
|
||
cymin = std::min(cymin, o.y()); cymax = std::max(cymax, o.y());
|
||
}
|
||
f.kind = FeatureKind::RoundedRect;
|
||
f.c0 = Vec2d(cxmin - r, cymin - r); // (xmin,ymin)
|
||
f.c1 = Vec2d(cxmax + r, cymax + r); // (xmax,ymax)
|
||
f.param = r;
|
||
m_features.push_back(f);
|
||
}
|
||
} else if (all_line) {
|
||
std::vector<double> sidelen(cnt);
|
||
for (int k = 0; k < cnt; ++k) sidelen[k] = (m_entities[i + k].p1 - m_entities[i + k].p0).norm();
|
||
const double lmin = *std::min_element(sidelen.begin(), sidelen.end());
|
||
const double lmax = *std::max_element(sidelen.begin(), sidelen.end());
|
||
const bool equal_sides = lmin > 1e-6 && (lmax - lmin) / lmax < 0.02;
|
||
bool all_right = true;
|
||
for (int k = 0; k < cnt && all_right; ++k) {
|
||
Vec2d u = m_entities[i + k].p1 - m_entities[i + k].p0;
|
||
Vec2d v = m_entities[i + (k + 1) % cnt].p1 - m_entities[i + (k + 1) % cnt].p0;
|
||
if (u.norm() < 1e-9 || v.norm() < 1e-9) { all_right = false; break; }
|
||
if (std::abs(u.normalized().dot(v.normalized())) > 0.06) all_right = false; // ~3.4°
|
||
}
|
||
if (cnt == 4 && all_right) {
|
||
f.kind = FeatureKind::CornerRect; f.c0 = start(i); f.c1 = start(i + 2);
|
||
m_features.push_back(f);
|
||
} else if (equal_sides) {
|
||
f.kind = FeatureKind::Polygon; f.c0 = c; f.c1 = start(i);
|
||
f.sides = cnt; f.param = (start(i) - c).norm();
|
||
m_features.push_back(f);
|
||
}
|
||
}
|
||
i = j + 1;
|
||
}
|
||
}
|
||
|
||
void DesignSketchTool::set_tool(Mode mode)
|
||
{
|
||
// A READY edit-op carries the user's typed or dragged value, so switching tools commits it
|
||
// rather than dropping it — the same rule Tab follows in the dimension editor. Discarding it
|
||
// here is most of why Fillet looked like it simply did not work: every documented route (type
|
||
// the radius, or drag the arrow) left the op ready-but-pending, and the next tool click threw
|
||
// the value away and reverted the corner to sharp, with nothing on screen saying so.
|
||
// This MUST run before m_mode is reassigned: op_ready() and confirm_op() both switch on
|
||
// m_mode, so after the assignment they would test the tool being switched TO. That read
|
||
// op_ready()==0 with a=0 b=3 val=28.205 sitting right there — picked, valued, and dropped.
|
||
if (op_ready()) confirm_op();
|
||
|
||
// An OPEN inline value field freezes the canvas (on_mouse_impl returns early while
|
||
// m_awaiting_length) and blocks every keyboard shortcut (in_text includes inline_busy()).
|
||
// Drawing a rectangle opens one automatically for its Width/Height, so after a rectangle the
|
||
// sketch was STUCK: pressing C did nothing because the key never reached this function, and
|
||
// clicking on the canvas did nothing because the canvas was frozen. Committing here accepts
|
||
// the typed value and closes the field, which is the same rule the ready-edit-op above
|
||
// follows — leaving a tool must not silently discard what the user entered.
|
||
if (on_inline_commit) on_inline_commit();
|
||
|
||
// Switch the active drawing tool without dropping accumulated entities.
|
||
m_mode = mode;
|
||
m_points.clear();
|
||
m_has_cursor = false;
|
||
m_awaiting_length = false;
|
||
m_autoedit_seen = int(m_entities.size()); // resync baseline so a switch never fires
|
||
m_autoedit_pending = false;
|
||
// A READY edit-op carries the user's typed or dragged value, so switching tools commits it
|
||
// rather than dropping it — the same rule Tab follows in the dimension editor. Discarding it
|
||
// here is most of why Fillet looked like it simply did not work: every documented route (type
|
||
// the radius, or drag the arrow) left the op ready-but-pending, and the next tool click threw
|
||
// the value away and reverted the corner to sharp, with nothing on screen saying so.
|
||
reset_op(); // drop any in-progress (not yet ready) edit-op gizmo
|
||
reset_tf(); // drop any in-progress transform gizmo
|
||
m_selection.clear();
|
||
if (on_selection_changed) on_selection_changed(0);
|
||
}
|
||
|
||
void DesignSketchTool::cancel()
|
||
{
|
||
close_session_chrome(); // same orphaned-field freeze as finish() — see snaporca-yce
|
||
m_active = false;
|
||
m_points.clear();
|
||
m_entities.clear();
|
||
m_construction = false;
|
||
m_has_cursor = false;
|
||
m_sel_a = m_sel_b = -1;
|
||
m_constrain_entities = false;
|
||
m_pick0 = m_pick1 = m_pick2 = -1;
|
||
m_constraint_hl.clear();
|
||
m_constrain_cons.clear();
|
||
m_awaiting_length = false;
|
||
m_selection.clear();
|
||
m_point_sel.clear();
|
||
m_constraints.clear();
|
||
m_dimensions.clear();
|
||
m_dim_has0 = false;
|
||
m_pending_dim = -1;
|
||
m_dof = -1; m_solve_ok = true; m_entity_conflict.clear();
|
||
m_features.clear();
|
||
m_open_feature = -1;
|
||
reset_op();
|
||
reset_xform();
|
||
reset_tf();
|
||
}
|
||
|
||
// Esc while active: layered exit (Onshape-like). Abort an in-progress entity first, then
|
||
// drop a draw tool back to Select; only an idle Select session exits to Feature mode.
|
||
void DesignSketchTool::request_exit()
|
||
{
|
||
if (!m_points.empty()) { m_points.clear(); m_has_cursor = false; return; }
|
||
// Drop any pending edit-op BEFORE the downgrade: set_tool commits a ready one, and Esc must
|
||
// cancel it, never apply it. Right-click already discards it through its own branch.
|
||
if (m_mode != Mode::Select) { reset_op(); set_tool(Mode::Select); return; }
|
||
if (on_exit) on_exit(); else cancel();
|
||
}
|
||
|
||
void DesignSketchTool::request_undo_redo(bool redo)
|
||
{
|
||
if (on_undo_redo) on_undo_redo(redo);
|
||
}
|
||
|
||
void DesignSketchTool::clear_selection()
|
||
{
|
||
if (m_selection.empty() && m_point_sel.empty()) return;
|
||
m_selection.clear();
|
||
m_point_sel.clear();
|
||
if (on_selection_changed) on_selection_changed(0);
|
||
}
|
||
|
||
void DesignSketchTool::delete_selected()
|
||
{
|
||
if (m_selection.empty()) return;
|
||
const int n = int(m_entities.size());
|
||
std::vector<bool> del(n, false);
|
||
for (int i : m_selection)
|
||
if (i >= 0 && i < n) del[i] = true;
|
||
// old index -> new index (or -1 if deleted), to fix up constraint references.
|
||
std::vector<int> remap(n, -1);
|
||
int next = 0;
|
||
for (int i = 0; i < n; ++i)
|
||
if (!del[i]) remap[i] = next++;
|
||
for (int i = n - 1; i >= 0; --i)
|
||
if (del[i]) m_entities.erase(m_entities.begin() + i);
|
||
// Drop constraints touching a deleted entity; remap the survivors.
|
||
std::vector<SketchEntityConstraintDef> kept;
|
||
auto live = [&](int e) { return e < 0 || (e < n && remap[e] >= 0); };
|
||
auto map = [&](int e) { return e < 0 ? -1 : remap[e]; };
|
||
for (SketchEntityConstraintDef c : m_constraints) {
|
||
if (!live(c.ea) || !live(c.eb) || !live(c.ec)) continue;
|
||
c.ea = map(c.ea); c.eb = map(c.eb); c.ec = map(c.ec);
|
||
kept.push_back(c);
|
||
}
|
||
m_constraints.swap(kept);
|
||
m_selection.clear();
|
||
m_point_sel.clear();
|
||
// v1: placed quotes reference entity indices that have shifted; drop them rather
|
||
// than risk a dangling reference (the driving constraints survive, reindexed).
|
||
m_dimensions.clear();
|
||
m_dim_has0 = false;
|
||
m_pending_dim = -1;
|
||
if (on_selection_changed) on_selection_changed(0);
|
||
}
|
||
|
||
bool DesignSketchTool::selection_valid() const
|
||
{
|
||
for (int i : m_selection)
|
||
if (i < 0 || i >= int(m_entities.size())) return false;
|
||
return true;
|
||
}
|
||
|
||
DesignSketchTool::DimType DesignSketchTool::dimension_kind() const
|
||
{
|
||
if (!selection_valid()) return DimType::None;
|
||
if (m_selection.size() == 1) {
|
||
switch (m_entities[m_selection[0]].type) {
|
||
case SketchEntity::Type::Line: return DimType::Length;
|
||
case SketchEntity::Type::Circle: return DimType::Diameter;
|
||
case SketchEntity::Type::Arc: return DimType::Radius;
|
||
default: return DimType::None;
|
||
}
|
||
}
|
||
if (m_selection.size() == 2) {
|
||
const SketchEntity& a = m_entities[m_selection[0]];
|
||
const SketchEntity& b = m_entities[m_selection[1]];
|
||
const bool aLine = (a.type == SketchEntity::Type::Line);
|
||
const bool bLine = (b.type == SketchEntity::Type::Line);
|
||
Vec2d tmp(0, 0);
|
||
if (aLine && bLine) return DimType::Angle;
|
||
// one line + one point-like (point / circle-centre / arc-centre)
|
||
if (aLine && entity_ref_point(b, tmp)) return DimType::DistanceToLine;
|
||
if (bLine && entity_ref_point(a, tmp)) return DimType::DistanceToLine;
|
||
// two point-likes -> centre/point distance (0 = coincident/concentric)
|
||
if (entity_ref_point(a, tmp) && entity_ref_point(b, tmp)) return DimType::Distance;
|
||
}
|
||
return DimType::None;
|
||
}
|
||
|
||
double DesignSketchTool::dimension_current() const
|
||
{
|
||
switch (dimension_kind()) {
|
||
case DimType::Length: { const auto& e = m_entities[m_selection[0]]; return (e.p1 - e.p0).norm(); }
|
||
case DimType::Diameter: return 2.0 * m_entities[m_selection[0]].radius;
|
||
case DimType::Radius: return m_entities[m_selection[0]].radius;
|
||
case DimType::Angle: {
|
||
const auto& a = m_entities[m_selection[0]];
|
||
const auto& b = m_entities[m_selection[1]];
|
||
const Vec2d da = a.p1 - a.p0, db = b.p1 - b.p0;
|
||
const double na = da.norm(), nb = db.norm();
|
||
if (na < 1e-9 || nb < 1e-9) return 0.0;
|
||
const double c = std::max(-1.0, std::min(1.0, da.dot(db) / (na * nb)));
|
||
return std::acos(c) * 180.0 / M_PI;
|
||
}
|
||
case DimType::Distance: {
|
||
Vec2d ra(0, 0), rb(0, 0);
|
||
entity_ref_point(m_entities[m_selection[0]], ra);
|
||
entity_ref_point(m_entities[m_selection[1]], rb);
|
||
return (rb - ra).norm();
|
||
}
|
||
case DimType::DistanceToLine: {
|
||
const SketchEntity& a = m_entities[m_selection[0]];
|
||
const SketchEntity& b = m_entities[m_selection[1]];
|
||
const bool aLine = (a.type == SketchEntity::Type::Line);
|
||
const SketchEntity& L = aLine ? a : b;
|
||
const SketchEntity& P = aLine ? b : a;
|
||
Vec2d rp(0, 0); entity_ref_point(P, rp);
|
||
Vec2d dir = L.p1 - L.p0;
|
||
const double n = dir.norm();
|
||
if (n < 1e-9) return 0.0;
|
||
const Vec2d nrm(-dir.y() / n, dir.x() / n); // unit normal to the line
|
||
return std::abs((rp - L.p0).dot(nrm));
|
||
}
|
||
default: return 0.0;
|
||
}
|
||
}
|
||
|
||
void DesignSketchTool::apply_angle_between(int ia, int ib, double deg)
|
||
{
|
||
SketchEntity& A = m_entities[ia];
|
||
SketchEntity& B = m_entities[ib];
|
||
const Vec2d aE[2] = { A.p0, A.p1 };
|
||
const Vec2d bE[2] = { B.p0, B.p1 };
|
||
int si = -1, sj = -1;
|
||
double best = 1e-6;
|
||
for (int i = 0; i < 2; ++i)
|
||
for (int j = 0; j < 2; ++j) {
|
||
const double d = (aE[i] - bE[j]).squaredNorm();
|
||
if (d < best) { best = d; si = i; sj = j; }
|
||
}
|
||
Vec2d pivot, refDir, bMoving;
|
||
bool moveP1;
|
||
if (si >= 0) { // shared vertex: pivot there, A's arm is the reference
|
||
pivot = aE[si];
|
||
refDir = aE[1 - si] - pivot;
|
||
moveP1 = (sj == 0); // move the B end that is NOT at the pivot
|
||
bMoving = moveP1 ? B.p1 : B.p0;
|
||
} else { // no shared vertex: pivot B.p0, A's direction is reference
|
||
pivot = B.p0;
|
||
refDir = A.p1 - A.p0;
|
||
moveP1 = true;
|
||
bMoving = B.p1;
|
||
}
|
||
double nr = refDir.norm();
|
||
if (nr < 1e-9) return;
|
||
refDir /= nr;
|
||
const Vec2d db = bMoving - pivot;
|
||
const double Lb = db.norm();
|
||
if (Lb < 1e-9) return;
|
||
const double cross = refDir.x() * db.y() - refDir.y() * db.x();
|
||
const double sign = (cross >= 0.0) ? 1.0 : -1.0; // keep B on its current side
|
||
const double rad = sign * deg * M_PI / 180.0;
|
||
const Vec2d ndir(refDir.x() * std::cos(rad) - refDir.y() * std::sin(rad),
|
||
refDir.x() * std::sin(rad) + refDir.y() * std::cos(rad));
|
||
const Vec2d nb = pivot + Lb * ndir;
|
||
if (moveP1) B.p1 = nb; else B.p0 = nb;
|
||
}
|
||
|
||
void DesignSketchTool::apply_dimension(double v)
|
||
{
|
||
switch (dimension_kind()) {
|
||
case DimType::Length: {
|
||
SketchEntity& e = m_entities[m_selection[0]];
|
||
const Vec2d d = e.p1 - e.p0;
|
||
const double r = d.norm();
|
||
if (r > 1e-9 && v > 1e-9) e.p1 = e.p0 + (v / r) * d;
|
||
break;
|
||
}
|
||
case DimType::Diameter: if (v > 1e-9) m_entities[m_selection[0]].radius = 0.5 * v; break;
|
||
case DimType::Radius: if (v > 1e-9) m_entities[m_selection[0]].radius = v; break;
|
||
case DimType::Angle: apply_angle_between(m_selection[0], m_selection[1], v); break;
|
||
case DimType::Distance: {
|
||
// Move the 2nd selection so its reference point sits at distance v from the
|
||
// 1st (v == 0 -> coincident / concentric). Translate the whole entity.
|
||
if (v < 0.0) break;
|
||
Vec2d ra(0, 0), rb(0, 0);
|
||
entity_ref_point(m_entities[m_selection[0]], ra);
|
||
SketchEntity& b = m_entities[m_selection[1]];
|
||
entity_ref_point(b, rb);
|
||
const Vec2d d = rb - ra;
|
||
const double r = d.norm();
|
||
Vec2d target = ra;
|
||
if (r > 1e-9) target = ra + (v / r) * d;
|
||
else target = ra + Vec2d(v, 0.0);
|
||
translate_entity(b, target - rb);
|
||
break;
|
||
}
|
||
case DimType::DistanceToLine: {
|
||
// Move the point-like selection perpendicular to the line so its reference
|
||
// point is at distance v (v == 0 -> on the line / on the axis).
|
||
if (v < 0.0) break;
|
||
const bool aLine = (m_entities[m_selection[0]].type == SketchEntity::Type::Line);
|
||
const SketchEntity& L = m_entities[m_selection[aLine ? 0 : 1]];
|
||
SketchEntity& P = m_entities[m_selection[aLine ? 1 : 0]];
|
||
Vec2d rp(0, 0); entity_ref_point(P, rp);
|
||
Vec2d dir = L.p1 - L.p0;
|
||
const double n = dir.norm();
|
||
if (n < 1e-9) break;
|
||
const Vec2d nrm(-dir.y() / n, dir.x() / n);
|
||
const double d0 = (rp - L.p0).dot(nrm); // current signed distance
|
||
const double sign = (d0 >= 0.0) ? 1.0 : -1.0; // keep the point on its side
|
||
translate_entity(P, (sign * v - d0) * nrm);
|
||
break;
|
||
}
|
||
default: break;
|
||
}
|
||
record_dimension_constraint(v); // store a driving constraint for this dimension
|
||
resolve_live(); // live-solve so the viewport shows the solved sketch
|
||
m_selection.clear();
|
||
if (on_selection_changed) on_selection_changed(0);
|
||
}
|
||
|
||
// Append the SketchEntityConstraintDef that makes the just-applied dimension a
|
||
// driving constraint (enforced by the kernel live and at commit). DistanceToLine
|
||
// records a PointOnLine constraint so "centre onto axis" persists through re-solve.
|
||
void DesignSketchTool::record_dimension_constraint(double v)
|
||
{
|
||
const DimType k = dimension_kind();
|
||
auto role = [&](int i) {
|
||
return (m_entities[i].type == SketchEntity::Type::Point) ? SketchPointRole::P0
|
||
: SketchPointRole::Center;
|
||
};
|
||
SketchEntityConstraintDef c;
|
||
switch (k) {
|
||
case DimType::Length:
|
||
c.type = SketchConstraintType::Distance;
|
||
c.ea = m_selection[0]; c.ra = SketchPointRole::P0;
|
||
c.eb = m_selection[0]; c.rb = SketchPointRole::P1;
|
||
c.value = v; m_constraints.push_back(c); break;
|
||
case DimType::Diameter:
|
||
c.type = SketchConstraintType::Diameter; c.ea = m_selection[0]; c.value = v;
|
||
m_constraints.push_back(c); break;
|
||
case DimType::Radius:
|
||
c.type = SketchConstraintType::Radius; c.ea = m_selection[0]; c.value = v;
|
||
m_constraints.push_back(c); break;
|
||
case DimType::Angle:
|
||
c.type = SketchConstraintType::Angle;
|
||
c.ea = m_selection[0]; c.eb = m_selection[1]; c.value = v;
|
||
m_constraints.push_back(c); break;
|
||
case DimType::Distance: {
|
||
const int ia = m_selection[0], ib = m_selection[1];
|
||
if (v < 1e-9) c.type = SketchConstraintType::Coincident;
|
||
else { c.type = SketchConstraintType::Distance; c.value = v; }
|
||
c.ea = ia; c.ra = role(ia); c.eb = ib; c.rb = role(ib);
|
||
m_constraints.push_back(c); break;
|
||
}
|
||
case DimType::DistanceToLine: {
|
||
// Point-on-line driving constraint: hold the point-like entity at unsigned
|
||
// perpendicular distance v from the line (v == 0 -> on the axis).
|
||
const bool aLine = (m_entities[m_selection[0]].type == SketchEntity::Type::Line);
|
||
const int ip = m_selection[aLine ? 1 : 0]; // point-like (Point/Circle/Arc)
|
||
const int il = m_selection[aLine ? 0 : 1]; // line
|
||
c.type = SketchConstraintType::PointOnLine;
|
||
c.ea = ip; c.ra = role(ip); c.eb = il; c.value = v;
|
||
m_constraints.push_back(c); break;
|
||
}
|
||
default: break; // None: no driving constraint recorded
|
||
}
|
||
}
|
||
|
||
// Onshape-style live solve: enforce all accumulated driving constraints on the
|
||
// in-session entities immediately, so the viewport reflects the solved sketch as
|
||
// each dimension/constraint is added (not only at commit). The pre-edit geometry
|
||
// is the solver's initial guess, keeping convergence local and side-preserving.
|
||
void DesignSketchTool::resolve_live()
|
||
{
|
||
resolve_live_drag(-1, SketchPointRole::P0);
|
||
}
|
||
|
||
void DesignSketchTool::resolve_live_drag(int dragged_ei, SketchPointRole dragged_role)
|
||
{
|
||
const bool has = !m_constraints.empty();
|
||
m_entity_conflict.assign(m_entities.size(), 0);
|
||
if (has) {
|
||
// Dragging a line endpoint must move ONLY that endpoint (changing the line's angle +
|
||
// length), anchoring the other end — Onshape behaviour. Without this, a constraint on
|
||
// the line (e.g. a length dimension or an inferred H/V) lets the solver relocate the
|
||
// un-dragged endpoint too, so the whole line appears to shift. Temporarily Fix the
|
||
// opposite endpoint for this drag-solve only (set_point already moved just the grabbed
|
||
// point, so the other end's current coord is its anchor).
|
||
std::vector<SketchEntityConstraintDef> cons = m_constraints;
|
||
const bool line_end = dragged_ei >= 0 && dragged_ei < int(m_entities.size()) &&
|
||
m_entities[dragged_ei].type == SketchEntity::Type::Line &&
|
||
(dragged_role == SketchPointRole::P0 || dragged_role == SketchPointRole::P1);
|
||
if (line_end) {
|
||
SketchEntityConstraintDef fix;
|
||
fix.type = SketchConstraintType::Fix;
|
||
fix.ea = dragged_ei;
|
||
fix.ra = (dragged_role == SketchPointRole::P0) ? SketchPointRole::P1
|
||
: SketchPointRole::P0;
|
||
cons.push_back(fix);
|
||
}
|
||
const SketchSolveResult r = (dragged_ei >= 0)
|
||
? sketch_solve_drag(m_entities, cons, dragged_ei, dragged_role)
|
||
: sketch_solve(m_entities, cons);
|
||
m_dof = r.dof;
|
||
m_solve_ok = r.ok;
|
||
// Flag every entity referenced by a conflicting constraint so render() can
|
||
// tint it red (Onshape/SolveSpace over-constrained feedback).
|
||
for (int bi : r.bad) {
|
||
if (bi < 0 || bi >= int(m_constraints.size())) continue;
|
||
const SketchEntityConstraintDef& c = m_constraints[bi];
|
||
for (int e : {c.ea, c.eb, c.ec})
|
||
if (e >= 0 && e < int(m_entity_conflict.size())) m_entity_conflict[e] = 1;
|
||
}
|
||
} else {
|
||
m_dof = -1; m_solve_ok = true;
|
||
}
|
||
if (on_solve_state) on_solve_state(m_dof, m_solve_ok, has);
|
||
}
|
||
|
||
// ---- Onshape-style visual editing: feature grouping + handles -----------------
|
||
|
||
// Index of the Feature whose entity span contains ei, or -1 (last match wins so a
|
||
// later, tighter gesture shadows an earlier one if they ever overlap).
|
||
int DesignSketchTool::feature_of(int ei) const
|
||
{
|
||
for (int i = int(m_features.size()) - 1; i >= 0; --i)
|
||
if (ei >= m_features[i].begin && ei < m_features[i].end) return i;
|
||
return -1;
|
||
}
|
||
|
||
// Open a Feature spanning the entities a single gesture is about to append. The
|
||
// [begin,end) range is closed in end_feature() once the gesture's entities are in.
|
||
void DesignSketchTool::begin_feature(FeatureKind kind)
|
||
{
|
||
Feature f;
|
||
f.kind = kind;
|
||
f.begin = int(m_entities.size());
|
||
f.end = f.begin;
|
||
m_features.push_back(f);
|
||
m_open_feature = int(m_features.size()) - 1;
|
||
}
|
||
|
||
// Close the open Feature: record its entity span end + the gesture's parametric
|
||
// anchors (centres / corners / half-width / sides) for later handle + dim rebuild.
|
||
void DesignSketchTool::end_feature(const Vec2d& c0, const Vec2d& c1, double param, int sides)
|
||
{
|
||
if (m_open_feature < 0 || m_open_feature >= int(m_features.size())) return;
|
||
Feature& f = m_features[m_open_feature];
|
||
f.end = int(m_entities.size());
|
||
f.c0 = c0;
|
||
f.c1 = c1;
|
||
f.param = param;
|
||
f.sides = sides;
|
||
// Drop a degenerate feature (gesture appended nothing).
|
||
if (f.end <= f.begin) m_features.pop_back();
|
||
m_open_feature = -1;
|
||
}
|
||
|
||
// Forward decls: these ellipse helpers are defined further down but used by the
|
||
// handle/drag code above their definition.
|
||
static Vec2d ellipse_point(const Vec2d& c, double a, double b, double phi, double t);
|
||
static double ellipse_param_of(const Vec2d& center, double a, double b, double phi, const Vec2d& q);
|
||
|
||
// Live handle set (A3: Line + Circle). Recomputed from solved geometry every frame,
|
||
// never persisted — so handles always track the current solve. Derived roles (here
|
||
// the circle RadiusHandle, which is NOT a serialized SketchPointRole) are what let a
|
||
// tool expose a parametric control its raw entity points don't carry. A4 routes a
|
||
// Select-mode drag through hit_test_handle + set_handle; later phases add the
|
||
// slot/rect/polygon/ellipse roles off the Feature groups.
|
||
std::vector<DesignSketchTool::Handle> DesignSketchTool::build_handles() const
|
||
{
|
||
std::vector<Handle> hs;
|
||
hs.reserve(m_entities.size() * 2);
|
||
for (size_t i = 0; i < m_entities.size(); ++i) {
|
||
const SketchEntity& e = m_entities[i];
|
||
switch (e.type) {
|
||
case SketchEntity::Type::Line: {
|
||
Handle a; a.role = HandleRole::P0; a.ei = int(i); a.pos = e.p0; hs.push_back(a);
|
||
Handle b; b.role = HandleRole::P1; b.ei = int(i); b.pos = e.p1; hs.push_back(b);
|
||
break;
|
||
}
|
||
case SketchEntity::Type::Circle: {
|
||
Handle c; c.role = HandleRole::Center; c.ei = int(i); c.pos = e.center; hs.push_back(c);
|
||
// RadiusHandle sits on the circle to the +x side of its centre; dragging it
|
||
// (A4) edits the radius. Pure geometry, no constraint of its own.
|
||
Handle r; r.role = HandleRole::RadiusHandle; r.ei = int(i);
|
||
r.pos = e.center + Vec2d(e.radius, 0.0); hs.push_back(r);
|
||
break;
|
||
}
|
||
case SketchEntity::Type::Ellipse:
|
||
case SketchEntity::Type::EllipseArc: {
|
||
// 3 grips: centre (translate), major-axis end (semi-major a + orientation phi),
|
||
// minor-axis end (semi-minor b). a=e.radius, b=e.rminor, phi=e.rotation.
|
||
// (EllipseArc also exposes its two endpoints via hit_test_point for sweep.)
|
||
const Vec2d um(std::cos(e.rotation), std::sin(e.rotation)); // major dir
|
||
const Vec2d un(-um.y(), um.x()); // minor dir
|
||
Handle c; c.role = HandleRole::Center; c.ei = int(i); c.pos = e.center; hs.push_back(c);
|
||
Handle ma; ma.role = HandleRole::MajorAxis; ma.ei = int(i);
|
||
ma.pos = e.center + um * e.radius; hs.push_back(ma);
|
||
Handle mi; mi.role = HandleRole::MinorAxis; mi.ei = int(i);
|
||
mi.pos = e.center + un * e.rminor; hs.push_back(mi);
|
||
break;
|
||
}
|
||
case SketchEntity::Type::BSpline: {
|
||
// One draggable grip per control pole; dragging a pole reshapes the curve.
|
||
for (size_t k = 0; k < e.ctrl.size(); ++k) {
|
||
Handle h; h.role = HandleRole::BSplineCtrl; h.ei = int(i);
|
||
h.ctrl_index = int(k); h.pos = e.ctrl[k]; hs.push_back(h);
|
||
}
|
||
break;
|
||
}
|
||
default: break; // Arc derived handles land in later chunks
|
||
}
|
||
}
|
||
return hs;
|
||
}
|
||
|
||
// Nearest handle to plane-point p within tol. Ties broken by smallest distance.
|
||
bool DesignSketchTool::hit_test_handle(const Vec2d& p, double tol, Handle& out) const
|
||
{
|
||
bool found = false;
|
||
double best = tol;
|
||
for (const Handle& h : build_handles()) {
|
||
const double d = (h.pos - p).norm();
|
||
if (d <= best) { best = d; out = h; out.hovered = true; found = true; }
|
||
}
|
||
return found;
|
||
}
|
||
|
||
// Recompute the hovered handle on a plain (no-button) move. Returns true ONLY when the
|
||
// hovered handle changes, so on_mouse forces a single repaint per transition rather than
|
||
// re-rendering on every motion event. A no-op (false) for non-Moving events.
|
||
bool DesignSketchTool::update_hover(GLCanvas3D& canvas, wxMouseEvent& evt)
|
||
{
|
||
if (!evt.Moving()) return false;
|
||
Vec2d p;
|
||
screen_to_plane(canvas, evt, p);
|
||
// Zoom-aware pick tolerance: project a point a few px away and measure in plane units.
|
||
const Linef3 r2 = canvas.mouse_ray(Point(evt.GetX() + 6, evt.GetY()));
|
||
const double tol = std::max(1e-3, (m_plane.project(r2.a, r2.vector()) - p).norm());
|
||
const bool had = m_has_hover_handle;
|
||
const Handle prev = m_hover_handle;
|
||
Handle h;
|
||
m_has_hover_handle = hit_test_handle(p, tol, h);
|
||
if (m_has_hover_handle) m_hover_handle = h;
|
||
return (had != m_has_hover_handle) ||
|
||
(m_has_hover_handle && (prev.ei != h.ei || prev.role != h.role));
|
||
}
|
||
|
||
// Apply a handle drag (A4). Point handles (P0/P1/Center) move the entity point and
|
||
// pin it in the drag-solve so constraints settle around the cursor. The derived
|
||
// RadiusHandle isn't a solver point — it edits the circle radius directly, then a
|
||
// full re-solve lets any driving Radius/Diameter constraint reassert (an unconstrained
|
||
// radius is a free DoF, so the solver keeps the new value). Slot/rect/polygon/ellipse
|
||
// roles land in later phases (they rebuild their Feature group).
|
||
void DesignSketchTool::set_handle(const Handle& h, const Vec2d& target)
|
||
{
|
||
if (h.ei < 0 || h.ei >= int(m_entities.size())) return;
|
||
SketchEntity& e = m_entities[h.ei];
|
||
switch (h.role) {
|
||
case HandleRole::P0:
|
||
set_point(h.ei, SketchPointRole::P0, target); resolve_live_drag(h.ei, SketchPointRole::P0); break;
|
||
case HandleRole::P1:
|
||
set_point(h.ei, SketchPointRole::P1, target); resolve_live_drag(h.ei, SketchPointRole::P1); break;
|
||
case HandleRole::Center:
|
||
set_point(h.ei, SketchPointRole::Center, target); resolve_live_drag(h.ei, SketchPointRole::Center); break;
|
||
case HandleRole::RadiusHandle: {
|
||
const double r = (target - e.center).norm();
|
||
if (r > 1e-6) e.radius = r;
|
||
resolve_live();
|
||
break;
|
||
}
|
||
case HandleRole::MajorAxis: {
|
||
// The major grip defines the major-axis vector: sets semi-major a + orientation phi.
|
||
const Vec2d d = target - e.center;
|
||
const double a = d.norm();
|
||
if (a > 1e-6) {
|
||
e.rotation = std::atan2(d.y(), d.x());
|
||
e.radius = std::max(a, e.rminor); // keep OCCT invariant a >= b
|
||
}
|
||
if (e.type == SketchEntity::Type::EllipseArc) { // endpoints ride the reshaped frame
|
||
e.p0 = ellipse_point(e.center, e.radius, e.rminor, e.rotation, e.start_angle);
|
||
e.p1 = ellipse_point(e.center, e.radius, e.rminor, e.rotation, e.end_angle);
|
||
}
|
||
resolve_live();
|
||
break;
|
||
}
|
||
case HandleRole::MinorAxis: {
|
||
// The minor grip sets semi-minor b = perpendicular distance to the major axis.
|
||
const Vec2d um(std::cos(e.rotation), std::sin(e.rotation));
|
||
const Vec2d un(-um.y(), um.x());
|
||
const double b = std::abs((target - e.center).dot(un));
|
||
if (b > 1e-6) e.rminor = std::min(b, e.radius);
|
||
if (e.type == SketchEntity::Type::EllipseArc) {
|
||
e.p0 = ellipse_point(e.center, e.radius, e.rminor, e.rotation, e.start_angle);
|
||
e.p1 = ellipse_point(e.center, e.radius, e.rminor, e.rotation, e.end_angle);
|
||
}
|
||
resolve_live();
|
||
break;
|
||
}
|
||
case HandleRole::BSplineCtrl: {
|
||
// Move one control pole; the end poles mirror p0/p1 (kept in sync for picking).
|
||
const int k = h.ctrl_index;
|
||
if (k >= 0 && k < int(e.ctrl.size())) {
|
||
e.ctrl[k] = target;
|
||
if (k == 0) e.p0 = target;
|
||
if (k == int(e.ctrl.size()) - 1) e.p1 = target;
|
||
}
|
||
resolve_live();
|
||
break;
|
||
}
|
||
default: break;
|
||
}
|
||
}
|
||
|
||
// ---- Dimension tool (Mode::Dimension): click-to-place driving quotes ----------
|
||
|
||
bool DesignSketchTool::point_at(int ei, SketchPointRole role, Vec2d& out) const
|
||
{
|
||
if (ei < 0 || ei >= int(m_entities.size())) return false;
|
||
const SketchEntity& e = m_entities[ei];
|
||
switch (role) {
|
||
case SketchPointRole::P0: out = e.p0; return true;
|
||
case SketchPointRole::P1: out = e.p1; return true;
|
||
case SketchPointRole::Center: out = e.center; return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Move an entity point to v. Lines/Points set the coordinate directly; a circle/
|
||
// arc centre translates the whole entity (arc endpoints ride along). Dragging an
|
||
// arc's endpoint is intentionally a no-op (it would redefine radius + angles).
|
||
void DesignSketchTool::set_point(int ei, SketchPointRole role, const Vec2d& v)
|
||
{
|
||
if (ei < 0 || ei >= int(m_entities.size())) return;
|
||
SketchEntity& e = m_entities[ei];
|
||
switch (e.type) {
|
||
case SketchEntity::Type::Line:
|
||
if (role == SketchPointRole::P0) e.p0 = v;
|
||
else if (role == SketchPointRole::P1) e.p1 = v;
|
||
break;
|
||
case SketchEntity::Type::Point:
|
||
e.p0 = v;
|
||
break;
|
||
case SketchEntity::Type::Circle:
|
||
if (role == SketchPointRole::Center) e.center = v;
|
||
break;
|
||
case SketchEntity::Type::Arc:
|
||
case SketchEntity::Type::EllipseArc:
|
||
if (role == SketchPointRole::Center) {
|
||
const Vec2d d = v - e.center; // rigid translate, keep radius/angles
|
||
e.center = v; e.p0 += d; e.p1 += d;
|
||
}
|
||
break;
|
||
case SketchEntity::Type::Ellipse:
|
||
if (role == SketchPointRole::Center) { e.center = v; e.p0 = v; }
|
||
break;
|
||
case SketchEntity::Type::BSpline:
|
||
// P0/P1 drag the end poles; Center rigidly translates the whole curve.
|
||
if (role == SketchPointRole::P0 && !e.ctrl.empty()) { e.ctrl.front() = v; e.p0 = v; }
|
||
else if (role == SketchPointRole::P1 && !e.ctrl.empty()) { e.ctrl.back() = v; e.p1 = v; }
|
||
else if (role == SketchPointRole::Center) {
|
||
const Vec2d d = v - (e.ctrl.empty() ? e.p0 : e.ctrl.front());
|
||
for (auto& cp : e.ctrl) cp += d;
|
||
e.p0 += d; e.p1 += d;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Nearest entity *point* (endpoint / centre) within tol, with its role.
|
||
bool DesignSketchTool::hit_test_point(const Vec2d& p, double tol, int& ei, SketchPointRole& role) const
|
||
{
|
||
double best = tol;
|
||
bool found = false;
|
||
auto consider = [&](int i, SketchPointRole r, const Vec2d& q) {
|
||
const double d = (q - p).norm();
|
||
if (d < best) { best = d; ei = i; role = r; found = true; }
|
||
};
|
||
for (size_t i = 0; i < m_entities.size(); ++i) {
|
||
const SketchEntity& e = m_entities[i];
|
||
switch (e.type) {
|
||
case SketchEntity::Type::Line:
|
||
consider(int(i), SketchPointRole::P0, e.p0);
|
||
consider(int(i), SketchPointRole::P1, e.p1);
|
||
break;
|
||
case SketchEntity::Type::Arc:
|
||
case SketchEntity::Type::EllipseArc:
|
||
consider(int(i), SketchPointRole::P0, e.p0);
|
||
consider(int(i), SketchPointRole::P1, e.p1);
|
||
consider(int(i), SketchPointRole::Center, e.center);
|
||
break;
|
||
case SketchEntity::Type::Circle:
|
||
case SketchEntity::Type::Ellipse:
|
||
consider(int(i), SketchPointRole::Center, e.center);
|
||
break;
|
||
case SketchEntity::Type::BSpline:
|
||
consider(int(i), SketchPointRole::P0, e.p0);
|
||
consider(int(i), SketchPointRole::P1, e.p1);
|
||
break;
|
||
case SketchEntity::Type::Point:
|
||
consider(int(i), SketchPointRole::P0, e.p0);
|
||
break;
|
||
}
|
||
}
|
||
return found;
|
||
}
|
||
|
||
double DesignSketchTool::measure_dim(const DimAnnot& a) const
|
||
{
|
||
Vec2d pa, pb;
|
||
switch (a.kind) {
|
||
case DimType::Length:
|
||
return (a.ea >= 0 && a.ea < int(m_entities.size()))
|
||
? (m_entities[a.ea].p1 - m_entities[a.ea].p0).norm() : 0.0;
|
||
case DimType::Diameter:
|
||
return (a.ea >= 0 && a.ea < int(m_entities.size())) ? 2.0 * m_entities[a.ea].radius : 0.0;
|
||
case DimType::Radius:
|
||
return (a.ea >= 0 && a.ea < int(m_entities.size())) ? m_entities[a.ea].radius : 0.0;
|
||
case DimType::Angle: {
|
||
// Single line: angle to the +X axis, normalised to [0,360). (Line-to-line angle
|
||
// dimensions use the legacy dialog path.)
|
||
if (a.ea < 0 || a.ea >= int(m_entities.size())) return 0.0;
|
||
const SketchEntity& e = m_entities[a.ea];
|
||
if (e.type != SketchEntity::Type::Line) return 0.0;
|
||
const Vec2d d = e.p1 - e.p0;
|
||
if (d.squaredNorm() < 1e-18) return 0.0;
|
||
double deg = std::atan2(d.y(), d.x()) * 180.0 / M_PI;
|
||
if (deg < 0.0) deg += 360.0;
|
||
return deg;
|
||
}
|
||
case DimType::Distance:
|
||
return (point_at(a.ea, a.ra, pa) && point_at(a.eb, a.rb, pb)) ? (pb - pa).norm() : 0.0;
|
||
case DimType::DistanceToLine: {
|
||
if (!point_at(a.ea, a.ra, pa) || a.eb < 0 || a.eb >= int(m_entities.size())) return 0.0;
|
||
const SketchEntity& L = m_entities[a.eb];
|
||
const Vec2d d = L.p1 - L.p0;
|
||
const double n = d.norm();
|
||
if (n < 1e-9) return 0.0;
|
||
const Vec2d nrm(-d.y() / n, d.x() / n);
|
||
return std::abs((pa - L.p0).dot(nrm));
|
||
}
|
||
default: return 0.0;
|
||
}
|
||
}
|
||
|
||
// One driving constraint per (kind, operands) — update it in place rather than appending a
|
||
// second one every time its value is edited.
|
||
//
|
||
// Re-typing a quote used to push a duplicate alongside the original: draw a line and accept its
|
||
// length, and you get Distance(P0,P1)=64.9; click the quote later and type 30, and you get a
|
||
// SECOND Distance on the same two points asking for 30. That is over-constrained by
|
||
// construction, so the solver reported "Conflicting constraints" and refused to move anything —
|
||
// the number on screen changed and the geometry did not, which reads as the edit being ignored.
|
||
// A line carrying no dimension yet was unaffected, which is what made it look intermittent.
|
||
//
|
||
// Operand order is ignored: a Distance from A to B is the same constraint as B to A, and so is
|
||
// an Angle. Returns the index, so callers can keep the annotation's `con` link pointing at the
|
||
// constraint that is actually live — which is what makes the next edit an update too.
|
||
int DesignSketchTool::upsert_constraint(const SketchEntityConstraintDef& c)
|
||
{
|
||
auto same_operands = [&](const SketchEntityConstraintDef& o) {
|
||
if (o.ea == c.ea && o.ra == c.ra && o.eb == c.eb && o.rb == c.rb) return true;
|
||
return o.ea == c.eb && o.ra == c.rb && o.eb == c.ea && o.rb == c.ra;
|
||
};
|
||
for (int i = 0; i < int(m_constraints.size()); ++i)
|
||
if (m_constraints[i].type == c.type && same_operands(m_constraints[i])) {
|
||
m_constraints[i] = c;
|
||
return i;
|
||
}
|
||
m_constraints.push_back(c);
|
||
return int(m_constraints.size()) - 1;
|
||
}
|
||
|
||
// The same rule for the visible annotation: one quote per (kind, operands), so repeated edits
|
||
// do not stack labels on top of each other reading different values.
|
||
int DesignSketchTool::upsert_dimension(const DimAnnot& a)
|
||
{
|
||
for (int i = 0; i < int(m_dimensions.size()); ++i) {
|
||
const DimAnnot& o = m_dimensions[i];
|
||
if (o.kind == a.kind && ((o.ea == a.ea && o.eb == a.eb) || (o.ea == a.eb && o.eb == a.ea))) {
|
||
const Vec2d keep = m_dimensions[i].label_pos; // don't teleport a placed label
|
||
m_dimensions[i] = a;
|
||
m_dimensions[i].label_pos = keep;
|
||
return i;
|
||
}
|
||
}
|
||
m_dimensions.push_back(a);
|
||
return int(m_dimensions.size()) - 1;
|
||
}
|
||
|
||
SketchEntityConstraintDef DesignSketchTool::constraint_for(const DimAnnot& a) const
|
||
{
|
||
SketchEntityConstraintDef c;
|
||
switch (a.kind) {
|
||
case DimType::Length:
|
||
c.type = SketchConstraintType::Distance;
|
||
c.ea = a.ea; c.ra = SketchPointRole::P0;
|
||
c.eb = a.ea; c.rb = SketchPointRole::P1; c.value = a.value;
|
||
break;
|
||
case DimType::Diameter: c.type = SketchConstraintType::Diameter; c.ea = a.ea; c.value = a.value; break;
|
||
case DimType::Radius: c.type = SketchConstraintType::Radius; c.ea = a.ea; c.value = a.value; break;
|
||
case DimType::Distance:
|
||
if (a.value < 1e-9) c.type = SketchConstraintType::Coincident;
|
||
else { c.type = SketchConstraintType::Distance; c.value = a.value; }
|
||
c.ea = a.ea; c.ra = a.ra; c.eb = a.eb; c.rb = a.rb;
|
||
break;
|
||
case DimType::DistanceToLine:
|
||
c.type = SketchConstraintType::PointOnLine;
|
||
c.ea = a.ea; c.ra = a.ra; c.eb = a.eb; c.value = a.value;
|
||
break;
|
||
default: break;
|
||
}
|
||
return c;
|
||
}
|
||
|
||
// Measure the just-picked dimension, append its driving constraint, live-solve, and
|
||
// fire the value-card callback so the user can override the value.
|
||
int DesignSketchTool::place_dimension(DimAnnot a)
|
||
{
|
||
a.value = measure_dim(a);
|
||
a.con = upsert_constraint(constraint_for(a));
|
||
const int di = upsert_dimension(a);
|
||
resolve_live();
|
||
open_value_editor(di);
|
||
return m_pending_dim;
|
||
}
|
||
|
||
// Nearest placed-dimension label within tol (uses the centre cached by render).
|
||
int DesignSketchTool::hit_test_dimension(const Vec2d& p, double tol) const
|
||
{
|
||
double best = tol;
|
||
int bi = -1;
|
||
for (size_t i = 0; i < m_dimensions.size(); ++i) {
|
||
const double d = (m_dimensions[i].label_pos - p).norm();
|
||
if (d < best) { best = d; bi = int(i); }
|
||
}
|
||
return bi;
|
||
}
|
||
|
||
// Reopen the value editor on an existing dimension (click/double-click its label).
|
||
void DesignSketchTool::edit_dimension(int di)
|
||
{
|
||
if (di < 0 || di >= int(m_dimensions.size())) return;
|
||
open_value_editor(di);
|
||
}
|
||
|
||
// Representative plane anchor for a dimension's value editor: the cached label centre
|
||
// once render has computed it, otherwise a geometric midpoint (length/distance) or the
|
||
// entity centre (radius/diameter).
|
||
Vec2d DesignSketchTool::dim_anchor(const DimAnnot& a) const
|
||
{
|
||
if (a.label_pos.squaredNorm() > 1e-12) return a.label_pos;
|
||
Vec2d pa, pb;
|
||
if ((a.kind == DimType::Radius || a.kind == DimType::Diameter) &&
|
||
point_at(a.ea, SketchPointRole::Center, pa))
|
||
return pa;
|
||
const bool ga = point_at(a.ea, a.ra, pa);
|
||
const bool gb = point_at(a.eb, a.rb, pb);
|
||
if (ga && gb) return 0.5 * (pa + pb);
|
||
if (ga) return pa;
|
||
if (gb) return pb;
|
||
return Vec2d(0, 0);
|
||
}
|
||
|
||
// Open the in-canvas value editor on dimension `di`. Projects the dimension's anchor
|
||
// to screen pixels and hands the host (DesignCanvas) a commit/cancel pair that drive
|
||
// the value through the existing set/cancel_dimension_value path. Falls back to the
|
||
// modal pick-complete callback when no inline-edit host is wired.
|
||
void DesignSketchTool::open_value_editor(int di)
|
||
{
|
||
if (di < 0 || di >= int(m_dimensions.size())) return;
|
||
m_pending_dim = di;
|
||
if (!on_inline_edit) {
|
||
if (on_dimension_pick_complete) on_dimension_pick_complete(m_dimensions[di].value);
|
||
return;
|
||
}
|
||
const DimAnnot& a = m_dimensions[di];
|
||
// Anchor the field OVER the dimension (project its label/anchor to the viewport), same as
|
||
// the draw-then-edit tools and Constrain mode; fall back to the click point if it projects
|
||
// off-screen.
|
||
wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const wxPoint lp = world_to_screen_px(cam, m_plane.to_world(dim_anchor(a)));
|
||
const std::array<int, 4>& vp = cam.get_viewport();
|
||
if (lp.x >= vp[0] && lp.y >= vp[1] && lp.x <= vp[0] + vp[2] && lp.y <= vp[1] + vp[3])
|
||
px = lp;
|
||
on_inline_edit(px, a.value, dimtype_title(a.kind),
|
||
[this](double v) { set_dimension_value(v); },
|
||
[this]() { cancel_dimension_value(); });
|
||
}
|
||
|
||
// In-canvas editor for a line's angle-to-horizontal. Unlike length/radius, a single
|
||
// line's angle has no libslvs constraint here (SLVS_C_ANGLE is line-to-line), so the
|
||
// commit rotates the segment GEOMETRICALLY about P0 to the typed degrees, then re-solves
|
||
// — the angle is a free DoF, so the solver keeps the new orientation (mirrors the radius
|
||
// handle). Length-constrained lines keep their length.
|
||
void DesignSketchTool::open_angle_editor(int ei)
|
||
{
|
||
if (ei < 0 || ei >= int(m_entities.size())) return;
|
||
if (m_entities[ei].type != SketchEntity::Type::Line) return;
|
||
if (!on_inline_edit) return;
|
||
DimAnnot a; a.kind = DimType::Angle; a.ea = ei;
|
||
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
||
on_inline_edit(px, measure_dim(a), "Angle",
|
||
[this, ei](double deg) { set_line_angle(ei, deg); },
|
||
[]() {});
|
||
}
|
||
|
||
void DesignSketchTool::set_line_angle(int ei, double deg)
|
||
{
|
||
if (ei < 0 || ei >= int(m_entities.size())) return;
|
||
SketchEntity& e = m_entities[ei];
|
||
if (e.type != SketchEntity::Type::Line) return;
|
||
const double L = (e.p1 - e.p0).norm();
|
||
if (L < 1e-9) return;
|
||
drop_orientation_constraints(ei, ei + 1); // a typed angle overrides an inferred H/V
|
||
const double r = deg * M_PI / 180.0;
|
||
e.p1 = e.p0 + Vec2d(std::cos(r), std::sin(r)) * L; // rotate about P0, keep length
|
||
resolve_live();
|
||
}
|
||
|
||
// Open the queued scalar quote at m_autoedit_dim_idx. Commit appends the driving dimension
|
||
// AND advances to the next queued quote (deferred via CallAfter so the single SketchInlineEditor
|
||
// fully unwinds its Enter handler before being reopened). Cancel (Esc) aborts the whole chain —
|
||
// the shape is kept as drawn. This is what lets a rectangle edit Width THEN Height, a slot its
|
||
// centre-distance THEN width, etc., instead of only the first dimension.
|
||
void DesignSketchTool::open_next_autoedit_dim()
|
||
{
|
||
if (!on_inline_edit || !m_active) { m_autoedit_dim_idx = -1; return; }
|
||
if (m_autoedit_dim_idx < 0 || m_autoedit_dim_idx >= int(m_autoedit_dims.size())) {
|
||
m_autoedit_dim_idx = -1;
|
||
return;
|
||
}
|
||
const AutoEditStep step = m_autoedit_dims[m_autoedit_dim_idx];
|
||
// Anchor the field OVER this dimension's label (project its plane-coords centre to the
|
||
// viewport), not at the last cursor spot — otherwise each field pops up in an unrelated
|
||
// screen position. Fall back to the cursor if the label projects off-screen.
|
||
// Anchor the field OVER this dimension's label (project its plane-coords centre to the
|
||
// viewport). A label can project OFF-screen (near-degenerate perspective divide when the
|
||
// sketch plane is viewed at a grazing angle), in which case we fall back to the cursor —
|
||
// but staggered by step index, so a shape's successive fields (rrect W/H/R) don't all
|
||
// stack on the exact same pixel and hide each other.
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const wxPoint lp = world_to_screen_px(cam, m_plane.to_world(step.label));
|
||
const std::array<int, 4>& vp = cam.get_viewport();
|
||
wxPoint px(m_last_mouse_x, m_last_mouse_y + m_autoedit_dim_idx * 34);
|
||
if (lp.x >= vp[0] && lp.y >= vp[1] && lp.x <= vp[0] + vp[2] && lp.y <= vp[1] + vp[3])
|
||
px = lp;
|
||
on_inline_edit(px, step.value, step.title,
|
||
[this, step](double v) { // commit: apply this dimension, then next
|
||
if (step.apply) step.apply(v);
|
||
++m_autoedit_dim_idx;
|
||
wxGetApp().CallAfter([this] { open_next_autoedit_dim(); });
|
||
},
|
||
[this]() { m_autoedit_dim_idx = -1; }); // cancel: keep as drawn, stop the chain
|
||
}
|
||
|
||
// Polyline draw-then-edit: after each click places a chain vertex, refine THAT segment's
|
||
// Length then Angle through the same AutoEditStep queue. The polyline batch-creates its
|
||
// entities only when the chain ends, so here we edit the pending m_points vertex directly
|
||
// (geometric): Length rescales it along the segment, Angle rotates it about the previous
|
||
// vertex. The next click continues from the adjusted vertex. Same field/anchor/focus path as
|
||
// every other tool — Enter advances Length->Angle, Esc keeps the segment as clicked.
|
||
void DesignSketchTool::arm_polyline_segment_edit()
|
||
{
|
||
const int k = int(m_points.size()) - 1; // index of the just-placed vertex
|
||
if (k < 1 || !on_inline_edit) return;
|
||
const Vec2d a = m_points[k - 1]; // segment anchor (previous vertex)
|
||
const Vec2d mid = 0.5 * (a + m_points[k]); // label anchor = segment midpoint
|
||
const Vec2d d = m_points[k] - a;
|
||
const double L = d.norm();
|
||
if (L < 1e-9) return;
|
||
double deg = std::atan2(d.y(), d.x()) * 180.0 / M_PI; if (deg < 0.0) deg += 360.0;
|
||
|
||
m_autoedit_dims.clear();
|
||
m_autoedit_dims.push_back({ mid, L, [this, k, a](double len) { // Length
|
||
if (k < int(m_points.size())) {
|
||
Vec2d dd = m_points[k] - a; const double n = dd.norm();
|
||
if (n > 1e-9 && len > 1e-9) m_points[k] = a + (len / n) * dd;
|
||
}
|
||
}, {}, "Length" });
|
||
m_autoedit_dims.push_back({ mid, deg, [this, k, a](double dg) { // Angle
|
||
if (k < int(m_points.size())) {
|
||
const double len = (m_points[k] - a).norm();
|
||
const double r = dg * M_PI / 180.0;
|
||
m_points[k] = a + len * Vec2d(std::cos(r), std::sin(r));
|
||
}
|
||
}, {}, "Angle" });
|
||
m_autoedit_dim_idx = 0;
|
||
wxGetApp().CallAfter([this] { open_next_autoedit_dim(); });
|
||
}
|
||
|
||
std::string DesignSketchTool::dimtype_title(DimType k) const {
|
||
switch (k) {
|
||
case DimType::Length: return "Length";
|
||
case DimType::Diameter: return "Diameter";
|
||
case DimType::Radius: return "Radius";
|
||
case DimType::Angle: return "Angle";
|
||
case DimType::Distance: return "Distance";
|
||
case DimType::DistanceToLine: return "Distance";
|
||
default: return "Value";
|
||
}
|
||
}
|
||
|
||
// Draw-then-edit dispatcher: mirror the Select-mode quote-click logic, but target the
|
||
// freshly-drawn selection's PRIMARY value and use the tentative (clean-cancel) path for
|
||
// scalar quotes. Runs after render_live_quotes, so the live-quote state is populated.
|
||
void DesignSketchTool::open_primary_autoedit()
|
||
{
|
||
if (!on_inline_edit || m_awaiting_length) return; // no host, or a field is already open
|
||
if (!m_active) return; // session ended before the deferred tick
|
||
|
||
// Build ONE ordered list of edit steps covering EVERY characteristic dimension of the
|
||
// freshly-drawn shape — scalar quotes (constraint-based) AND geometric editors — so every
|
||
// 2D tool behaves like the rectangle: a linear sequence of value fields, each over its own
|
||
// label, Enter advances to the next, Esc keeps the shape as drawn. (Line keeps its own
|
||
// dedicated length field; Polyline/BSpline/Point have no two-click dimension set.)
|
||
m_autoedit_dims.clear();
|
||
|
||
// (1) Scalar quotes: rect Width+Height, slot Distance+Radius, circle/arc Radius, line
|
||
// Length. The lone Angle quote (only a single Line emits one) becomes a GEOMETRIC
|
||
// orientation step (set_line_angle, like the polygon angle) — so the Line tool gets
|
||
// Length THEN Angle, same as the rectangle gets W then H.
|
||
for (const DimAnnot& q : m_live_quotes) {
|
||
if (q.kind == DimType::Angle) {
|
||
const int ei = q.ea;
|
||
m_autoedit_dims.push_back({ q.label_pos, measure_dim(q),
|
||
[this, ei](double v) { set_line_angle(ei, v); }, { ei }, "Angle" });
|
||
continue;
|
||
}
|
||
DimAnnot a = q;
|
||
a.value = measure_dim(a);
|
||
m_autoedit_dims.push_back({ a.label_pos, a.value,
|
||
[this, a](double v) mutable {
|
||
a.value = v;
|
||
a.con = upsert_constraint(constraint_for(a));
|
||
upsert_dimension(a);
|
||
resolve_live();
|
||
}, { a.ea, a.eb }, dimtype_title(a.kind) });
|
||
}
|
||
|
||
// (2) Geometric editors (mutate geometry directly, no constraint). Each reads the CURRENT
|
||
// feature/entity state inside apply(), so sequential edits compose correctly.
|
||
// Entities to highlight = the feature's whole [begin,end) span, so editing any of its
|
||
// characteristic dims lights up the shape it drives.
|
||
auto span = [this](int fi) {
|
||
std::vector<int> v;
|
||
if (fi >= 0 && fi < int(m_features.size()))
|
||
for (int k = m_features[fi].begin; k < m_features[fi].end; ++k) v.push_back(k);
|
||
return v;
|
||
};
|
||
if (m_live_poly_fi >= 0) {
|
||
const Feature& f = m_features[m_live_poly_fi];
|
||
const int fi = m_live_poly_fi;
|
||
if (f.begin >= 0 && f.begin < int(m_entities.size())) {
|
||
const double side = (m_entities[f.begin].p1 - m_entities[f.begin].p0).norm();
|
||
const Vec2d sp = m_entities[f.begin].p0 - f.c0;
|
||
double deg = std::atan2(sp.y(), sp.x()) * 180.0 / M_PI; if (deg < 0.0) deg += 360.0;
|
||
m_autoedit_dims.push_back({ m_live_poly_side_label, side, [this, fi](double v){ set_polygon_side(fi, v); }, span(fi), "Side" });
|
||
m_autoedit_dims.push_back({ m_live_poly_angle_label, deg, [this, fi](double v){ set_polygon_angle(fi, v); }, span(fi), "Angle" });
|
||
}
|
||
}
|
||
if (m_live_rrect_fi >= 0) {
|
||
const Feature& f = m_features[m_live_rrect_fi];
|
||
const int fi = m_live_rrect_fi;
|
||
const double w = std::abs(f.c1.x() - f.c0.x()), h = std::abs(f.c1.y() - f.c0.y()), r = f.param;
|
||
auto rr_w = [this, fi](double v){ const Feature& g = m_features[fi]; set_rounded_rect(fi, v, std::abs(g.c1.y()-g.c0.y()), g.param); };
|
||
auto rr_h = [this, fi](double v){ const Feature& g = m_features[fi]; set_rounded_rect(fi, std::abs(g.c1.x()-g.c0.x()), v, g.param); };
|
||
auto rr_r = [this, fi](double v){ const Feature& g = m_features[fi]; set_rounded_rect(fi, std::abs(g.c1.x()-g.c0.x()), std::abs(g.c1.y()-g.c0.y()), v); };
|
||
m_autoedit_dims.push_back({ m_live_rrect_w_label, w, rr_w, span(fi), "Width" });
|
||
m_autoedit_dims.push_back({ m_live_rrect_h_label, h, rr_h, span(fi), "Height" });
|
||
m_autoedit_dims.push_back({ m_live_rrect_r_label, r, rr_r, span(fi), "Radius" });
|
||
}
|
||
if (m_live_aslot_fi >= 0) {
|
||
const Feature& f = m_features[m_live_aslot_fi];
|
||
const int fi = m_live_aslot_fi;
|
||
const double Rc = (f.c1 - f.c0).norm(), fw = 2.0 * f.param;
|
||
m_autoedit_dims.push_back({ m_live_aslot_r_label, Rc, [this, fi](double v){ const Feature& g = m_features[fi]; set_arc_slot(fi, v, g.param); }, span(fi), "Radius" });
|
||
m_autoedit_dims.push_back({ m_live_aslot_w_label, fw, [this, fi](double v){ const Feature& g = m_features[fi]; set_arc_slot(fi, (g.c1-g.c0).norm(), std::max(1e-3, v*0.5)); }, span(fi), "Width" });
|
||
}
|
||
if (m_live_slot_fi >= 0) {
|
||
const Feature& f = m_features[m_live_slot_fi];
|
||
const int fi = m_live_slot_fi;
|
||
// Slot dims, in order: (1) inter-centre distance, (2) radius (= half-width), (3) angle.
|
||
const Vec2d d = f.c1 - f.c0;
|
||
const double Lc = d.norm();
|
||
double deg = std::atan2(d.y(), d.x()) * 180.0 / M_PI; if (deg < 0.0) deg += 360.0;
|
||
m_autoedit_dims.push_back({ m_live_slot_len_label, Lc, [this, fi](double v){ const Feature& g = m_features[fi]; set_slot(fi, v, g.param); }, span(fi), "Length" });
|
||
m_autoedit_dims.push_back({ m_live_slot_w_label, f.param, [this, fi](double v){ const Feature& g = m_features[fi]; set_slot(fi, (g.c1-g.c0).norm(), std::max(1e-3, v)); }, span(fi), "Radius" });
|
||
m_autoedit_dims.push_back({ m_live_slot_angle_label, deg, [this, fi](double v){ set_slot_angle(fi, v); }, span(fi), "Angle" });
|
||
}
|
||
if (m_live_arc_ei >= 0) { // arc Radius is already a scalar step above; add its sweep angle
|
||
const int ei = m_live_arc_ei;
|
||
const SketchEntity& e = m_entities[ei];
|
||
const double swdeg = std::abs(e.end_angle - e.start_angle) * 180.0 / M_PI;
|
||
m_autoedit_dims.push_back({ m_live_arc_angle_label, swdeg, [this, ei](double v){ set_arc_sweep(ei, v); }, { ei }, "Angle" });
|
||
}
|
||
if (m_live_ellipse_ei >= 0) {
|
||
const int ei = m_live_ellipse_ei;
|
||
const SketchEntity& e = m_entities[ei];
|
||
m_autoedit_dims.push_back({ m_live_ellipse_major_label, e.radius, [this, ei](double v){ set_ellipse_axis(ei, true, v); }, { ei }, "Major" });
|
||
m_autoedit_dims.push_back({ m_live_ellipse_minor_label, e.rminor, [this, ei](double v){ set_ellipse_axis(ei, false, v); }, { ei }, "Minor" });
|
||
if (e.type == SketchEntity::Type::EllipseArc) { // + included sweep
|
||
const double swdeg = std::abs(e.end_angle - e.start_angle) * 180.0 / M_PI;
|
||
m_autoedit_dims.push_back({ m_live_ellipsearc_sweep_label, swdeg,
|
||
[this, ei](double v){ set_ellipsearc_sweep(ei, v); }, { ei }, "Angle" });
|
||
}
|
||
}
|
||
if (m_live_obrect_fi >= 0) { // oblique rect: W,H already added as scalars; + orientation
|
||
const int fi = m_live_obrect_fi;
|
||
const Feature& f = m_features[fi];
|
||
const SketchEntity& e0 = m_entities[f.begin];
|
||
double adeg = std::atan2(e0.p1.y() - e0.p0.y(), e0.p1.x() - e0.p0.x()) * 180.0 / M_PI;
|
||
if (adeg < 0.0) adeg += 360.0;
|
||
m_autoedit_dims.push_back({ m_live_obrect_angle_label, adeg,
|
||
[this, fi](double v){ set_rect_angle(fi, v); }, span(fi), "Angle" });
|
||
}
|
||
|
||
if (!m_autoedit_dims.empty()) {
|
||
m_autoedit_dim_idx = 0;
|
||
open_next_autoedit_dim();
|
||
}
|
||
}
|
||
|
||
// A regular polygon is N raw lines with no centre entity, so (like the line angle) its
|
||
// side and orientation edits transform the whole loop GEOMETRICALLY about its centre.
|
||
void DesignSketchTool::open_polygon_side_editor(int fi)
|
||
{
|
||
if (fi < 0 || fi >= int(m_features.size()) || !on_inline_edit) return;
|
||
const Feature& f = m_features[fi];
|
||
if (f.begin < 0 || f.begin >= int(m_entities.size())) return;
|
||
const double side = (m_entities[f.begin].p1 - m_entities[f.begin].p0).norm();
|
||
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
||
on_inline_edit(px, side, "Side",
|
||
[this, fi](double v) { set_polygon_side(fi, v); },
|
||
[]() {});
|
||
}
|
||
|
||
void DesignSketchTool::open_polygon_angle_editor(int fi)
|
||
{
|
||
if (fi < 0 || fi >= int(m_features.size()) || !on_inline_edit) return;
|
||
const Feature& f = m_features[fi];
|
||
if (f.begin < 0 || f.begin >= int(m_entities.size())) return;
|
||
const Vec2d sp = m_entities[f.begin].p0 - f.c0; // centre -> vertex0 spoke
|
||
double deg = std::atan2(sp.y(), sp.x()) * 180.0 / M_PI;
|
||
if (deg < 0.0) deg += 360.0;
|
||
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
||
on_inline_edit(px, deg, "Angle",
|
||
[this, fi](double v) { set_polygon_angle(fi, v); },
|
||
[]() {});
|
||
}
|
||
|
||
// Scale the loop uniformly about its centre so an edge equals `side`. For a regular
|
||
// n-gon, circumradius R = side / (2 sin(pi/n)).
|
||
void DesignSketchTool::set_polygon_side(int fi, double side)
|
||
{
|
||
if (fi < 0 || fi >= int(m_features.size()) || side < 1e-6) return;
|
||
const int n = std::max(3, m_features[fi].sides);
|
||
const double R = side / (2.0 * std::sin(M_PI / double(n)));
|
||
set_polygon_radius(fi, R);
|
||
}
|
||
|
||
// Remove orientation constraints touching [begin,end). A pure rotation makes inferred
|
||
// per-edge Horizontal/Vertical (and Parallel/Perp/Angle/Lock) inconsistent, so leaving
|
||
// them in would make resolve_live collapse the shape to satisfy them.
|
||
void DesignSketchTool::drop_orientation_constraints(int begin, int end)
|
||
{
|
||
using CT = SketchConstraintType;
|
||
auto orient = [](CT t) {
|
||
return t == CT::Horizontal || t == CT::Vertical || t == CT::Parallel ||
|
||
t == CT::Perpendicular || t == CT::Angle || t == CT::LockX || t == CT::LockY;
|
||
};
|
||
auto in = [&](int e) { return e >= begin && e < end; };
|
||
std::vector<int> remap(m_constraints.size(), -1);
|
||
std::vector<SketchEntityConstraintDef> kept;
|
||
kept.reserve(m_constraints.size());
|
||
for (int i = 0; i < int(m_constraints.size()); ++i) {
|
||
const SketchEntityConstraintDef& c = m_constraints[i];
|
||
if (orient(c.type) && (in(c.ea) || in(c.eb))) continue; // drop
|
||
remap[i] = int(kept.size());
|
||
kept.push_back(c);
|
||
}
|
||
if (kept.size() == m_constraints.size()) return; // nothing dropped
|
||
m_constraints.swap(kept);
|
||
for (DimAnnot& a : m_dimensions) // fix cached con indices
|
||
if (a.con >= 0) a.con = (a.con < int(remap.size())) ? remap[a.con] : -1;
|
||
}
|
||
|
||
void DesignSketchTool::drop_constraints_referencing(int ei)
|
||
{
|
||
std::vector<int> remap(m_constraints.size(), -1);
|
||
std::vector<SketchEntityConstraintDef> kept;
|
||
kept.reserve(m_constraints.size());
|
||
for (int i = 0; i < int(m_constraints.size()); ++i) {
|
||
const SketchEntityConstraintDef& c = m_constraints[i];
|
||
if (c.ea == ei || c.eb == ei || c.ec == ei) continue; // drop refs to the cut entity
|
||
remap[i] = int(kept.size());
|
||
kept.push_back(c);
|
||
}
|
||
if (kept.size() == m_constraints.size()) return;
|
||
m_constraints.swap(kept);
|
||
for (DimAnnot& a : m_dimensions)
|
||
if (a.con >= 0) a.con = (a.con < int(remap.size())) ? remap[a.con] : -1;
|
||
}
|
||
|
||
// Onshape scissors on the live sketch: cut the picked entity at its nearest intersection.
|
||
// trim_entity/extend_entity mutate the subject in place (slide one endpoint) given the other
|
||
// entities + the pick point — no entity is added/removed, so indices stay stable.
|
||
bool DesignSketchTool::apply_live_trim(const Vec2d& p, double tol, bool extend)
|
||
{
|
||
double best = 1e30; int bi = -1;
|
||
for (size_t i = 0; i < m_entities.size(); ++i) {
|
||
const double d = entity_pick_dist(p, m_entities[i]);
|
||
if (d < best) { best = d; bi = int(i); }
|
||
}
|
||
if (bi < 0 || best > tol) return false;
|
||
using Ty = SketchEntity::Type;
|
||
const Ty st = m_entities[bi].type;
|
||
const bool subject_ok = extend ? (st == Ty::Line || st == Ty::Arc)
|
||
: (st == Ty::Line || st == Ty::Arc || st == Ty::Circle);
|
||
if (!subject_ok) return false;
|
||
std::vector<SketchEntity> others;
|
||
others.reserve(m_entities.size());
|
||
for (size_t i = 0; i < m_entities.size(); ++i)
|
||
if (int(i) != bi) others.push_back(m_entities[i]);
|
||
const bool ok = extend ? SketchEngine::extend_entity(m_entities[bi], others, p)
|
||
: SketchEngine::trim_entity(m_entities[bi], others, p);
|
||
if (!ok) return false;
|
||
drop_constraints_referencing(bi); // the slid endpoint invalidates this entity's constraints
|
||
return true;
|
||
}
|
||
|
||
// Hover preview for the Trim/Extend scissors: replay apply_live_trim's pick and the engine
|
||
// cut on a COPY of the subject, then diff the copy against the original to recover the exact
|
||
// sub-portion a click would remove (Trim) / add (Extend). Pure computation, mutates nothing.
|
||
bool DesignSketchTool::compute_trim_preview(const Vec2d& p, double tol, bool extend,
|
||
int& subject_ei, std::vector<Vec2d>& removed_poly) const
|
||
{
|
||
subject_ei = -1;
|
||
removed_poly.clear();
|
||
|
||
double best = 1e30; int bi = -1;
|
||
for (size_t i = 0; i < m_entities.size(); ++i) {
|
||
const double d = entity_pick_dist(p, m_entities[i]);
|
||
if (d < best) { best = d; bi = int(i); }
|
||
}
|
||
if (bi < 0 || best > tol) return false;
|
||
|
||
using Ty = SketchEntity::Type;
|
||
const Ty st = m_entities[bi].type;
|
||
const bool subject_ok = extend ? (st == Ty::Line || st == Ty::Arc)
|
||
: (st == Ty::Line || st == Ty::Arc || st == Ty::Circle);
|
||
if (!subject_ok) return false;
|
||
|
||
std::vector<SketchEntity> others;
|
||
others.reserve(m_entities.size());
|
||
for (size_t i = 0; i < m_entities.size(); ++i)
|
||
if (int(i) != bi) others.push_back(m_entities[i]);
|
||
|
||
const SketchEntity& orig = m_entities[bi];
|
||
SketchEntity trimmed = orig; // cut on the copy, never the live entity
|
||
const bool ok = extend ? SketchEngine::extend_entity(trimmed, others, p)
|
||
: SketchEngine::trim_entity(trimmed, others, p);
|
||
if (!ok) return false;
|
||
|
||
// The highlighted portion is where `trimmed` differs from `orig`: the dropped sub-segment
|
||
// (Trim) or the grown one (Extend). Rebuild it as a temp entity and sample its polyline.
|
||
const double EPS2 = 1e-14; // squared plane-unit endpoint tolerance
|
||
const double AEPS = 1e-7; // radian tolerance
|
||
bool closed = false;
|
||
|
||
if (orig.type == Ty::Line) {
|
||
SketchEntity seg = orig;
|
||
if ((trimmed.p0 - orig.p0).squaredNorm() > EPS2) {
|
||
seg.p0 = orig.p0; seg.p1 = trimmed.p0; // start endpoint moved
|
||
} else if ((trimmed.p1 - orig.p1).squaredNorm() > EPS2) {
|
||
seg.p0 = trimmed.p1; seg.p1 = orig.p1; // end endpoint moved
|
||
} else {
|
||
return false; // nothing changed
|
||
}
|
||
removed_poly = entity_polyline(seg, closed);
|
||
} else if (orig.type == Ty::Arc) {
|
||
SketchEntity arc = orig; // same centre/radius
|
||
if (std::abs(trimmed.start_angle - orig.start_angle) > AEPS) {
|
||
arc.start_angle = orig.start_angle; arc.end_angle = trimmed.start_angle;
|
||
} else if (std::abs(trimmed.end_angle - orig.end_angle) > AEPS) {
|
||
arc.start_angle = trimmed.end_angle; arc.end_angle = orig.end_angle;
|
||
} else {
|
||
return false;
|
||
}
|
||
removed_poly = entity_polyline(arc, closed);
|
||
} else if (orig.type == Ty::Circle) {
|
||
// Trim opens the Circle into the kept Arc [start,end]; the removed gap is its
|
||
// complement, swept from the kept arc's end round to its start.
|
||
if (trimmed.type != Ty::Arc) return false;
|
||
SketchEntity gap = orig;
|
||
gap.type = Ty::Arc;
|
||
gap.start_angle = trimmed.end_angle;
|
||
gap.end_angle = trimmed.start_angle + 2.0 * M_PI;
|
||
removed_poly = entity_polyline(gap, closed);
|
||
} else {
|
||
return false;
|
||
}
|
||
|
||
if (removed_poly.size() < 2) return false;
|
||
subject_ei = bi;
|
||
return true;
|
||
}
|
||
|
||
// Rotate the whole loop about its centre so the centre->vertex0 spoke points at `deg`
|
||
// (degrees from +X).
|
||
void DesignSketchTool::set_polygon_angle(int fi, double deg)
|
||
{
|
||
if (fi < 0 || fi >= int(m_features.size())) return;
|
||
Feature& f = m_features[fi];
|
||
if (f.begin < 0 || f.begin >= int(m_entities.size())) return;
|
||
const Vec2d c = f.c0;
|
||
const Vec2d sp = m_entities[f.begin].p0 - c; // centre -> vertex0
|
||
if (sp.squaredNorm() < 1e-12) return;
|
||
const double cur = std::atan2(sp.y(), sp.x());
|
||
const double da = deg * M_PI / 180.0 - cur;
|
||
drop_orientation_constraints(f.begin, f.end); // rotation invalidates edge H/V
|
||
const double ca = std::cos(da), sa = std::sin(da);
|
||
// -> Vec2d is REQUIRED: an auto return deduces an Eigen expression template that holds
|
||
// a reference to the destroyed `c + Vec2d(...)` temporary (dangling -> garbage coords).
|
||
auto rot = [&](const Vec2d& pt) -> Vec2d { const Vec2d r = pt - c;
|
||
return c + Vec2d(r.x() * ca - r.y() * sa, r.x() * sa + r.y() * ca); };
|
||
for (int i = f.begin; i < f.end && i < int(m_entities.size()); ++i) {
|
||
SketchEntity& e = m_entities[i];
|
||
if (e.type != SketchEntity::Type::Line) continue;
|
||
e.p0 = rot(e.p0); e.p1 = rot(e.p1);
|
||
}
|
||
// Do NOT re-solve: the rotated geometry is already a correct regular polygon, and the
|
||
// inferred loop (redundant Coincident, now with no H/V anchor) collapses to a point
|
||
// under libslvs. We only drop the stale edge H/V (above) so a later commit-solve stays
|
||
// sane; the display renders the mutated entities directly.
|
||
}
|
||
|
||
// Drag a polygon vertex keeping the loop regular: scale + rotate the whole polygon
|
||
// about its centroid so the grabbed vertex lands on `target`. This adjusts both the
|
||
// circumradius (|target-centroid|) and the orientation (its direction) at once.
|
||
void DesignSketchTool::drag_polygon_vertex(int fi, int ei, SketchPointRole role, const Vec2d& target)
|
||
{
|
||
if (fi < 0 || fi >= int(m_features.size())) return;
|
||
Feature& f = m_features[fi];
|
||
if (f.begin < 0 || f.end > int(m_entities.size())) return;
|
||
// Centroid of the loop = the regular polygon's centre (robust if it was moved).
|
||
Vec2d c(0, 0); int n = 0;
|
||
for (int i = f.begin; i < f.end; ++i)
|
||
if (m_entities[i].type == SketchEntity::Type::Line) { c += m_entities[i].p0; ++n; }
|
||
if (n == 0) return;
|
||
c /= double(n);
|
||
Vec2d vpos;
|
||
if (!point_at(ei, role, vpos)) return;
|
||
const Vec2d cur = vpos - c; // current grabbed-vertex spoke
|
||
const Vec2d tgt = target - c; // desired spoke
|
||
const double curR = cur.norm(), newR = tgt.norm();
|
||
if (curR < 1e-9 || newR < 1e-6) return;
|
||
const double s = newR / curR;
|
||
const double da = std::atan2(tgt.y(), tgt.x()) - std::atan2(cur.y(), cur.x());
|
||
const double ca = std::cos(da), sa = std::sin(da);
|
||
auto tf = [&](const Vec2d& p) -> Vec2d { const Vec2d r = (p - c) * s; // -> Vec2d: avoid
|
||
return c + Vec2d(r.x() * ca - r.y() * sa, r.x() * sa + r.y() * ca); }; // Eigen dangling
|
||
|
||
for (int i = f.begin; i < f.end; ++i) {
|
||
SketchEntity& e = m_entities[i];
|
||
if (e.type != SketchEntity::Type::Line) continue;
|
||
e.p0 = tf(e.p0); e.p1 = tf(e.p1);
|
||
}
|
||
f.c0 = c; f.param = newR;
|
||
drop_orientation_constraints(f.begin, f.end); // the drag rotates -> edge H/V invalid
|
||
// No re-solve (see set_polygon_angle): the transformed geometry is already a correct
|
||
// regular polygon; solving the anchorless redundant loop would collapse it.
|
||
}
|
||
|
||
void DesignSketchTool::set_polygon_radius(int fi, double R)
|
||
{
|
||
if (fi < 0 || fi >= int(m_features.size()) || R < 1e-6) return;
|
||
Feature& f = m_features[fi];
|
||
if (f.begin < 0 || f.begin >= int(m_entities.size())) return;
|
||
const Vec2d c = f.c0;
|
||
const double curR = (m_entities[f.begin].p0 - c).norm();
|
||
if (curR < 1e-9) return;
|
||
const double s = R / curR; // uniform scale about the centre
|
||
for (int i = f.begin; i < f.end && i < int(m_entities.size()); ++i) {
|
||
SketchEntity& e = m_entities[i];
|
||
if (e.type != SketchEntity::Type::Line) continue;
|
||
e.p0 = c + (e.p0 - c) * s;
|
||
e.p1 = c + (e.p1 - c) * s;
|
||
}
|
||
f.param = R;
|
||
// Geometric only (no solve): consistent with the rotation edits, and avoids collapsing
|
||
// the loop if its H/V anchors were already dropped by a prior rotation.
|
||
}
|
||
|
||
void DesignSketchTool::open_arc_angle_editor(int ei)
|
||
{
|
||
if (ei < 0 || ei >= int(m_entities.size()) || !on_inline_edit) return;
|
||
const SketchEntity& e = m_entities[ei];
|
||
if (e.type != SketchEntity::Type::Arc) return;
|
||
double swdeg = std::abs(e.end_angle - e.start_angle) * 180.0 / M_PI;
|
||
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
||
on_inline_edit(px, swdeg, "Angle",
|
||
[this, ei](double v) { set_arc_sweep(ei, v); },
|
||
[]() {});
|
||
}
|
||
|
||
// Set the arc's included (sweep) angle to `deg`, keeping the start point and radius fixed
|
||
// and rotating the end point about the centre. Geometric (SLVS angle is line-to-line), so
|
||
// no re-solve; the mutated entity renders directly. Direction (CCW/CW) of the original
|
||
// sweep is preserved.
|
||
void DesignSketchTool::set_arc_sweep(int ei, double deg)
|
||
{
|
||
if (ei < 0 || ei >= int(m_entities.size())) return;
|
||
SketchEntity& e = m_entities[ei];
|
||
if (e.type != SketchEntity::Type::Arc || e.radius < 1e-6) return;
|
||
double sweep = std::max(1e-3, std::min(deg, 359.999)) * M_PI / 180.0;
|
||
const double sign = (e.end_angle >= e.start_angle) ? 1.0 : -1.0;
|
||
e.end_angle = e.start_angle + sign * sweep;
|
||
e.p1 = e.center + e.radius * Vec2d(std::cos(e.end_angle), std::sin(e.end_angle));
|
||
resolve_live();
|
||
}
|
||
|
||
// Drag one of the arc's three handles. Roles are split so each grip changes ONE property
|
||
// (Onshape-like): Center -> translate; START point (P0) -> radius only; END point (P1) ->
|
||
// sweep angle only. Geometric (mutates the entity directly), then re-solve for any
|
||
// coincident constraints on the arc endpoints.
|
||
void DesignSketchTool::drag_arc_handle(int ei, SketchPointRole role, const Vec2d& target)
|
||
{
|
||
if (ei < 0 || ei >= int(m_entities.size())) return;
|
||
SketchEntity& e = m_entities[ei];
|
||
if (e.type != SketchEntity::Type::Arc) return;
|
||
if (role == SketchPointRole::Center) {
|
||
const Vec2d d = target - e.center; // rigid translate, keep R + angles
|
||
e.center = target; e.p0 += d; e.p1 += d;
|
||
} else if (role == SketchPointRole::P0) { // start = RADIUS handle (keep angles)
|
||
const double R = (target - e.center).norm();
|
||
if (R < 1e-6) return;
|
||
e.radius = R;
|
||
e.p0 = e.center + R * Vec2d(std::cos(e.start_angle), std::sin(e.start_angle));
|
||
e.p1 = e.center + R * Vec2d(std::cos(e.end_angle), std::sin(e.end_angle));
|
||
} else if (role == SketchPointRole::P1) { // end = ANGLE handle (keep radius)
|
||
const Vec2d d = target - e.center;
|
||
if (d.squaredNorm() < 1e-12) return;
|
||
// Keep the CCW sweep continuous (0,2pi) so the arc never flips to its complement.
|
||
double da = std::atan2(d.y(), d.x()) - e.start_angle;
|
||
while (da < 0.0) da += 2.0 * M_PI;
|
||
while (da >= 2.0 * M_PI) da -= 2.0 * M_PI;
|
||
e.end_angle = e.start_angle + da;
|
||
e.p1 = e.center + e.radius * Vec2d(std::cos(e.end_angle), std::sin(e.end_angle));
|
||
}
|
||
resolve_live();
|
||
}
|
||
|
||
// Drag an elliptical-arc grip. Center rigidly translates (endpoints + frame move with it);
|
||
// P0/P1 set the sweep start/end to the cursor's parametric angle on the ellipse, keeping
|
||
// the ellipse shape (a/b/phi). Geometric, then resolve_live().
|
||
void DesignSketchTool::drag_ellipsearc_handle(int ei, SketchPointRole role, const Vec2d& target)
|
||
{
|
||
if (ei < 0 || ei >= int(m_entities.size())) return;
|
||
SketchEntity& e = m_entities[ei];
|
||
if (e.type != SketchEntity::Type::EllipseArc) return;
|
||
if (role == SketchPointRole::Center) {
|
||
const Vec2d d = target - e.center;
|
||
e.center = target; e.p0 += d; e.p1 += d;
|
||
} else if (role == SketchPointRole::P0 || role == SketchPointRole::P1) {
|
||
const double t = ellipse_param_of(e.center, e.radius, e.rminor, e.rotation, target);
|
||
if (role == SketchPointRole::P0) {
|
||
e.start_angle = t;
|
||
e.p0 = ellipse_point(e.center, e.radius, e.rminor, e.rotation, t);
|
||
} else {
|
||
// Keep the CCW sweep (end strictly after start) so the arc never inverts.
|
||
double t1 = t; while (t1 <= e.start_angle) t1 += 2.0 * M_PI;
|
||
e.end_angle = t1;
|
||
e.p1 = ellipse_point(e.center, e.radius, e.rminor, e.rotation, t1);
|
||
}
|
||
}
|
||
resolve_live();
|
||
}
|
||
|
||
void DesignSketchTool::open_ellipse_axis_editor(int ei, bool major)
|
||
{
|
||
if (ei < 0 || ei >= int(m_entities.size()) || !on_inline_edit) return;
|
||
const SketchEntity& e = m_entities[ei];
|
||
if (e.type != SketchEntity::Type::Ellipse && e.type != SketchEntity::Type::EllipseArc) return;
|
||
const double v = major ? e.radius : e.rminor;
|
||
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
||
on_inline_edit(px, v, major ? "Major" : "Minor",
|
||
[this, ei, major](double nv) { set_ellipse_axis(ei, major, nv); },
|
||
[]() {});
|
||
}
|
||
|
||
// Set a semi-axis to `v`: major -> e.radius, minor -> e.rminor; keep OCCT a >= b.
|
||
void DesignSketchTool::set_ellipse_axis(int ei, bool major, double v)
|
||
{
|
||
if (ei < 0 || ei >= int(m_entities.size()) || v < 1e-6) return;
|
||
SketchEntity& e = m_entities[ei];
|
||
if (e.type != SketchEntity::Type::Ellipse && e.type != SketchEntity::Type::EllipseArc) return;
|
||
if (major) e.radius = std::max(v, e.rminor);
|
||
else e.rminor = std::min(v, e.radius);
|
||
if (e.type == SketchEntity::Type::EllipseArc) { // endpoints ride the reshaped frame
|
||
e.p0 = ellipse_point(e.center, e.radius, e.rminor, e.rotation, e.start_angle);
|
||
e.p1 = ellipse_point(e.center, e.radius, e.rminor, e.rotation, e.end_angle);
|
||
}
|
||
resolve_live();
|
||
}
|
||
|
||
// Set an elliptical arc's included (parametric) sweep, keeping the start fixed and moving the
|
||
// end. Geometric (mirrors set_arc_sweep), then resolve_live for any endpoint coincidences.
|
||
void DesignSketchTool::set_ellipsearc_sweep(int ei, double deg)
|
||
{
|
||
if (ei < 0 || ei >= int(m_entities.size())) return;
|
||
SketchEntity& e = m_entities[ei];
|
||
if (e.type != SketchEntity::Type::EllipseArc) return;
|
||
const double sweep = std::max(1e-3, std::min(deg, 359.999)) * M_PI / 180.0;
|
||
const double sign = (e.end_angle >= e.start_angle) ? 1.0 : -1.0;
|
||
e.end_angle = e.start_angle + sign * sweep;
|
||
e.p1 = ellipse_point(e.center, e.radius, e.rminor, e.rotation, e.end_angle);
|
||
resolve_live();
|
||
}
|
||
|
||
// Rotate an oblique rectangle to an absolute orientation (angle of edge0 to +X), pivoting on
|
||
// its anchor corner f.c0. Geometric, mirrors set_polygon_angle: drop the now-inconsistent edge
|
||
// H/V first, rotate every member point + the opposite corner, and DON'T re-solve (the rotated
|
||
// loop is already consistent; a length Distance the user may have set is rotation-invariant).
|
||
void DesignSketchTool::set_rect_angle(int fi, double deg)
|
||
{
|
||
if (fi < 0 || fi >= int(m_features.size())) return;
|
||
Feature& f = m_features[fi];
|
||
if (f.begin < 0 || f.end > int(m_entities.size()) || f.end <= f.begin) return;
|
||
const SketchEntity& e0 = m_entities[f.begin];
|
||
const Vec2d d0 = e0.p1 - e0.p0;
|
||
if (d0.squaredNorm() < 1e-12) return;
|
||
const double da = deg * M_PI / 180.0 - std::atan2(d0.y(), d0.x());
|
||
drop_orientation_constraints(f.begin, f.end);
|
||
const Vec2d pivot = f.c0;
|
||
const double ca = std::cos(da), sa = std::sin(da);
|
||
// -> Vec2d REQUIRED (see set_polygon_angle): an auto return deduces an Eigen expression
|
||
// template referencing the destroyed temporary -> dangling.
|
||
auto rot = [&](const Vec2d& pt) -> Vec2d { const Vec2d r = pt - pivot;
|
||
return pivot + Vec2d(r.x() * ca - r.y() * sa, r.x() * sa + r.y() * ca); };
|
||
for (int i = f.begin; i < f.end && i < int(m_entities.size()); ++i) {
|
||
SketchEntity& e = m_entities[i];
|
||
e.p0 = rot(e.p0); e.p1 = rot(e.p1);
|
||
}
|
||
f.c1 = rot(f.c1); // keep the opposite corner consistent for later W/H quotes
|
||
}
|
||
|
||
// Screen anchor for a Constrain-mode value field: over the picked geometry (its representative
|
||
// point — circle/arc centre, else segment midpoint; averaged when two entities are picked),
|
||
// projected to the viewport. Lets a dimensional constraint's field open ON the geometry like
|
||
// the draw-then-edit tools, instead of floating at viewport centre. False if no valid pick or
|
||
// it projects off-screen (caller falls back to centre).
|
||
bool DesignSketchTool::constrain_value_anchor(wxPoint& out) const
|
||
{
|
||
if (m_pick0 < 0 || m_pick0 >= int(m_entities.size())) return false;
|
||
auto rep = [](const SketchEntity& e) -> Vec2d {
|
||
using T = SketchEntity::Type;
|
||
if (e.type == T::Circle || e.type == T::Arc ||
|
||
e.type == T::Ellipse || e.type == T::EllipseArc) return e.center;
|
||
return 0.5 * (e.p0 + e.p1);
|
||
};
|
||
Vec2d p = rep(m_entities[m_pick0]);
|
||
if (m_pick1 >= 0 && m_pick1 < int(m_entities.size()))
|
||
p = 0.5 * (p + rep(m_entities[m_pick1]));
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const wxPoint sp = world_to_screen_px(cam, m_plane.to_world(p));
|
||
const std::array<int, 4>& vp = cam.get_viewport();
|
||
if (sp.x < vp[0] || sp.y < vp[1] || sp.x > vp[0] + vp[2] || sp.y > vp[1] + vp[3]) return false;
|
||
out = sp;
|
||
return true;
|
||
}
|
||
|
||
void DesignSketchTool::open_rounded_rect_editor(int fi, int which)
|
||
{
|
||
if (fi < 0 || fi >= int(m_features.size()) || !on_inline_edit) return;
|
||
const Feature& f = m_features[fi];
|
||
const double w = std::abs(f.c1.x() - f.c0.x());
|
||
const double h = std::abs(f.c1.y() - f.c0.y());
|
||
const double r = f.param;
|
||
const double v = (which == 0) ? w : (which == 1) ? h : r;
|
||
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
||
on_inline_edit(px, v, which == 0 ? "Width" : which == 1 ? "Height" : "Radius",
|
||
[this, fi, which](double nv) {
|
||
const Feature& g = m_features[fi];
|
||
double gw = std::abs(g.c1.x() - g.c0.x());
|
||
double gh = std::abs(g.c1.y() - g.c0.y());
|
||
double gr = g.param;
|
||
if (which == 0) gw = nv; else if (which == 1) gh = nv; else gr = nv;
|
||
set_rounded_rect(fi, gw, gh, gr);
|
||
},
|
||
[]() {});
|
||
}
|
||
|
||
// Rebuild the rounded-rect's 8 entities in place for a new width/height/fillet radius,
|
||
// keeping the min corner (c0) fixed. Geometric (entity order/count preserved so constraint
|
||
// refs stay valid); fillet clamped to (0, min(w,h)/2].
|
||
void DesignSketchTool::set_rounded_rect(int fi, double w, double h, double r)
|
||
{
|
||
if (fi < 0 || fi >= int(m_features.size())) return;
|
||
Feature& f = m_features[fi];
|
||
if (f.begin < 0 || f.end > int(m_entities.size()) || f.end <= f.begin) return;
|
||
w = std::max(w, 1e-3); h = std::max(h, 1e-3);
|
||
r = std::max(1e-3, std::min(r, std::min(w, h) * 0.5 - 1e-4));
|
||
const double xmin = std::min(f.c0.x(), f.c1.x()), ymin = std::min(f.c0.y(), f.c1.y());
|
||
const double xmax = xmin + w, ymax = ymin + h;
|
||
std::vector<SketchEntity> rebuilt = rounded_rect_entities(xmin, ymin, xmax, ymax, r);
|
||
if (int(rebuilt.size()) != f.end - f.begin) return; // count must match to keep con refs
|
||
for (int i = 0; i < int(rebuilt.size()); ++i) {
|
||
rebuilt[i].construction = m_entities[f.begin + i].construction; // preserve flag
|
||
m_entities[f.begin + i] = rebuilt[i];
|
||
}
|
||
f.c0 = Vec2d(xmin, ymin); f.c1 = Vec2d(xmax, ymax); f.param = r;
|
||
resolve_live();
|
||
}
|
||
|
||
void DesignSketchTool::open_arc_slot_editor(int fi, bool radius)
|
||
{
|
||
if (fi < 0 || fi >= int(m_features.size()) || !on_inline_edit) return;
|
||
const Feature& f = m_features[fi];
|
||
const double Rc = (f.c1 - f.c0).norm();
|
||
const double v = radius ? Rc : (2.0 * f.param); // width quote shows the FULL width
|
||
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
||
on_inline_edit(px, v, radius ? "Radius" : "Width",
|
||
[this, fi, radius](double nv) {
|
||
const Feature& g = m_features[fi];
|
||
const double gRc = (g.c1 - g.c0).norm();
|
||
if (radius) set_arc_slot(fi, nv, g.param);
|
||
else set_arc_slot(fi, gRc, std::max(1e-3, nv * 0.5)); // full width -> half
|
||
},
|
||
[]() {});
|
||
}
|
||
|
||
// Rebuild the arc-slot's 4 arcs in place for a new centreline radius / half-width. Centre
|
||
// + the two centreline directions are kept (the end direction is recovered from the cap@E
|
||
// arc centre). Geometric; entity count preserved so constraint refs stay valid.
|
||
void DesignSketchTool::set_arc_slot(int fi, double Rc, double w)
|
||
{
|
||
if (fi < 0 || fi >= int(m_features.size())) return;
|
||
Feature& f = m_features[fi];
|
||
if (f.begin < 0 || f.end > int(m_entities.size()) || f.end - f.begin != 4) return;
|
||
const Vec2d center = f.c0;
|
||
Vec2d dirS = f.c1 - center;
|
||
const Vec2d Ec = m_entities[f.begin + 1].center; // cap@E centre = centreline end
|
||
Vec2d dirE = Ec - center;
|
||
if (dirS.squaredNorm() < 1e-12 || dirE.squaredNorm() < 1e-12) return;
|
||
dirS.normalize(); dirE.normalize();
|
||
Rc = std::max(Rc, 2e-3);
|
||
w = std::max(1e-3, std::min(w, Rc - 1e-3)); // make_arc_slot needs w < Rc
|
||
std::vector<SketchEntity> rebuilt =
|
||
make_arc_slot(center, center + Rc * dirS, center + Rc * dirE, w);
|
||
if (int(rebuilt.size()) != 4) return;
|
||
for (int i = 0; i < 4; ++i) {
|
||
rebuilt[i].construction = m_entities[f.begin + i].construction;
|
||
m_entities[f.begin + i] = rebuilt[i];
|
||
}
|
||
f.c1 = center + Rc * dirS; f.param = w;
|
||
resolve_live();
|
||
}
|
||
|
||
// Open the inline editor for a straight slot's dimension: which 0 = inter-centre distance,
|
||
// 1 = radius (half-width), 2 = centreline angle. Drives set_slot / set_slot_angle geometrically.
|
||
void DesignSketchTool::open_slot_editor(int fi, int which)
|
||
{
|
||
if (fi < 0 || fi >= int(m_features.size()) || !on_inline_edit) return;
|
||
const Feature& f = m_features[fi];
|
||
const Vec2d d = f.c1 - f.c0;
|
||
double deg = std::atan2(d.y(), d.x()) * 180.0 / M_PI; if (deg < 0.0) deg += 360.0;
|
||
const double v = (which == 0) ? d.norm() : (which == 1) ? f.param : deg;
|
||
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
||
on_inline_edit(px, v, which == 0 ? "Length" : which == 1 ? "Radius" : "Angle",
|
||
[this, fi, which](double nv) {
|
||
const Feature& g = m_features[fi];
|
||
if (which == 0) set_slot(fi, nv, g.param);
|
||
else if (which == 1) set_slot(fi, (g.c1 - g.c0).norm(), std::max(1e-3, nv));
|
||
else set_slot_angle(fi, nv);
|
||
},
|
||
[]() {});
|
||
}
|
||
|
||
// Rebuild the straight slot's 4 entities in place for a new centreline length / half-width.
|
||
// Centre c0 and the centreline direction are kept; only c1 (length) or param (width) change.
|
||
// Geometric; entity count preserved so constraint refs stay valid.
|
||
void DesignSketchTool::set_slot(int fi, double length, double w)
|
||
{
|
||
if (fi < 0 || fi >= int(m_features.size())) return;
|
||
Feature& f = m_features[fi];
|
||
if (f.begin < 0 || f.end > int(m_entities.size()) || f.end - f.begin != 4) return;
|
||
Vec2d dir = f.c1 - f.c0;
|
||
if (dir.squaredNorm() < 1e-12) return;
|
||
dir.normalize();
|
||
length = std::max(length, 2e-3);
|
||
w = std::max(1e-3, w);
|
||
const Vec2d c0 = f.c0, c1 = f.c0 + length * dir;
|
||
std::vector<SketchEntity> rebuilt = make_slot(c0, c1, w);
|
||
if (int(rebuilt.size()) != 4) return;
|
||
for (int i = 0; i < 4; ++i) {
|
||
rebuilt[i].construction = m_entities[f.begin + i].construction;
|
||
m_entities[f.begin + i] = rebuilt[i];
|
||
}
|
||
f.c1 = c1; f.param = w;
|
||
resolve_live();
|
||
}
|
||
|
||
// Rotate a straight slot about c0 to a new centreline angle (degrees), keeping length + radius.
|
||
void DesignSketchTool::set_slot_angle(int fi, double deg)
|
||
{
|
||
if (fi < 0 || fi >= int(m_features.size())) return;
|
||
Feature& f = m_features[fi];
|
||
if (f.begin < 0 || f.end > int(m_entities.size()) || f.end - f.begin != 4) return;
|
||
const double L = (f.c1 - f.c0).norm();
|
||
if (L < 1e-9) return;
|
||
const double a = deg * M_PI / 180.0;
|
||
const Vec2d c1 = f.c0 + L * Vec2d(std::cos(a), std::sin(a));
|
||
std::vector<SketchEntity> rebuilt = make_slot(f.c0, c1, f.param);
|
||
if (int(rebuilt.size()) != 4) return;
|
||
for (int i = 0; i < 4; ++i) {
|
||
rebuilt[i].construction = m_entities[f.begin + i].construction;
|
||
m_entities[f.begin + i] = rebuilt[i];
|
||
}
|
||
f.c1 = c1;
|
||
resolve_live();
|
||
}
|
||
|
||
// Resize an axis-aligned rectangle by dragging a corner: the diagonally-opposite corner
|
||
// (captured at grab as m_drag_rect_anchor) stays fixed; the box becomes [anchor, cursor].
|
||
// Geometric rebuild in place (4 lines, same order) — edges stay axis-aligned so the
|
||
// inferred H/V + corner-coincident constraints remain satisfied (no re-solve needed).
|
||
void DesignSketchTool::drag_rect_corner(int fi, const Vec2d& cursor)
|
||
{
|
||
if (fi < 0 || fi >= int(m_features.size())) return;
|
||
Feature& f = m_features[fi];
|
||
if (f.end - f.begin != 4) return;
|
||
const Vec2d A = m_drag_rect_anchor, B = cursor;
|
||
if (std::abs(B.x() - A.x()) < 1e-4 || std::abs(B.y() - A.y()) < 1e-4) return; // degenerate
|
||
const Vec2d corners[4] = { A, Vec2d(B.x(), A.y()), B, Vec2d(A.x(), B.y()) };
|
||
for (int i = 0; i < 4; ++i) {
|
||
SketchEntity e; e.type = SketchEntity::Type::Line;
|
||
e.p0 = corners[i]; e.p1 = corners[(i + 1) % 4];
|
||
e.construction = m_entities[f.begin + i].construction;
|
||
m_entities[f.begin + i] = e;
|
||
}
|
||
f.c0 = A; f.c1 = B;
|
||
}
|
||
|
||
// Move one end of a slot by dragging its cap centre (which cap captured at grab); the other
|
||
// centre + half-width are kept. Rebuilds the 4-entity span via make_slot. Geometric.
|
||
void DesignSketchTool::drag_slot_handle(int fi, const Vec2d& cursor)
|
||
{
|
||
if (fi < 0 || fi >= int(m_features.size())) return;
|
||
Feature& f = m_features[fi];
|
||
if (f.end - f.begin != 4) return;
|
||
const Vec2d c0 = m_drag_slot_c1 ? f.c0 : cursor;
|
||
const Vec2d c1 = m_drag_slot_c1 ? cursor : f.c1;
|
||
std::vector<SketchEntity> rebuilt = make_slot(c0, c1, f.param);
|
||
if (int(rebuilt.size()) != 4) return;
|
||
for (int i = 0; i < 4; ++i) {
|
||
rebuilt[i].construction = m_entities[f.begin + i].construction;
|
||
m_entities[f.begin + i] = rebuilt[i];
|
||
}
|
||
f.c0 = c0; f.c1 = c1;
|
||
}
|
||
|
||
DesignSketchTool::DimType DesignSketchTool::pending_dimension_type() const
|
||
{
|
||
return (m_pending_dim >= 0 && m_pending_dim < int(m_dimensions.size()))
|
||
? m_dimensions[m_pending_dim].kind : DimType::None;
|
||
}
|
||
|
||
void DesignSketchTool::set_dimension_value(double v)
|
||
{
|
||
if (m_pending_dim < 0 || m_pending_dim >= int(m_dimensions.size())) return;
|
||
DimAnnot& a = m_dimensions[m_pending_dim];
|
||
a.value = v;
|
||
if (a.con >= 0 && a.con < int(m_constraints.size()))
|
||
m_constraints[a.con] = constraint_for(a);
|
||
resolve_live();
|
||
m_pending_dim = -1;
|
||
}
|
||
|
||
void DesignSketchTool::cancel_dimension_value()
|
||
{
|
||
m_pending_dim = -1; // keep the placed dimension at its measured value
|
||
}
|
||
|
||
std::string DesignSketchTool::dim_text(const DimAnnot& a) const
|
||
{
|
||
char buf[32];
|
||
const char* prefix = (a.kind == DimType::Diameter) ? "\xC3\x98" // 'Ø'
|
||
: (a.kind == DimType::Radius) ? "R" : "";
|
||
const char* suffix = (a.kind == DimType::Angle) ? "\xC2\xB0" : ""; // '°'
|
||
std::snprintf(buf, sizeof(buf), "%s%.1f%s", prefix, a.value, suffix);
|
||
// Force the international (en) decimal point: wx sets LC_NUMERIC to the user
|
||
// locale at startup, so snprintf("%.1f") can emit a comma. Normalise it.
|
||
for (char& ch : buf)
|
||
if (ch == ',') ch = '.';
|
||
std::string out(buf);
|
||
if (a.kind != DimType::Angle) {
|
||
const bool use_in = wxGetApp().app_config->get_bool("use_inches");
|
||
out += use_in ? " in" : " mm";
|
||
}
|
||
return out;
|
||
}
|
||
|
||
void DesignSketchTool::apply_segment_length(double len)
|
||
{
|
||
if (m_points.size() == 2 && len > 1e-9) {
|
||
const Vec2d d = m_points[1] - m_points[0];
|
||
const double r = d.norm();
|
||
if (r > 1e-9)
|
||
m_points[1] = m_points[0] + (len / r) * d;
|
||
}
|
||
keep_segment_as_drawn();
|
||
// Driving length constraint on the just-committed Line entity.
|
||
if (len > 1e-9 && !m_entities.empty()) {
|
||
const int i = int(m_entities.size()) - 1;
|
||
if (m_entities[i].type == SketchEntity::Type::Line) {
|
||
SketchEntityConstraintDef c;
|
||
c.type = SketchConstraintType::Distance;
|
||
c.ea = i; c.ra = SketchPointRole::P0;
|
||
c.eb = i; c.rb = SketchPointRole::P1;
|
||
c.value = len;
|
||
m_constraints.push_back(c);
|
||
}
|
||
}
|
||
resolve_live(); // live-solve the in-session sketch
|
||
}
|
||
|
||
void DesignSketchTool::keep_segment_as_drawn()
|
||
{
|
||
if (m_points.size() == 2) {
|
||
const int base = int(m_entities.size());
|
||
push_line(m_points[0], m_points[1]); // accrues into the session's entities
|
||
infer_auto_constraints(base); // auto Coincident at snapped ends + H/V
|
||
}
|
||
m_points.clear();
|
||
m_has_cursor = false;
|
||
m_awaiting_length = false;
|
||
}
|
||
|
||
void DesignSketchTool::finish()
|
||
{
|
||
// A value field still open at commit time outlives the session — the editor is a top-level
|
||
// frame — and inline_busy stays set, so every later click is swallowed at on_mouse_impl's
|
||
// first branch and the viewport reads as dead. Dismiss it first (keep-as-drawn, the same
|
||
// contract as the polyline terminators) and drop any queued field with it.
|
||
close_session_chrome();
|
||
if (op_ready()) confirm_op(); // apply a pending edit-op gizmo before committing
|
||
if (tf_ready()) confirm_transform(); // apply a pending transform gizmo before committing
|
||
auto cb = on_commit_entities;
|
||
std::vector<SketchEntity> ents = m_entities;
|
||
std::vector<SketchEntityConstraintDef> cons = m_constraints;
|
||
SketchPlane pl = m_plane;
|
||
m_active = false;
|
||
m_points.clear();
|
||
m_entities.clear();
|
||
m_constraints.clear();
|
||
m_dimensions.clear();
|
||
m_point_sel.clear();
|
||
m_dim_has0 = false;
|
||
m_pending_dim = -1;
|
||
m_has_cursor = false;
|
||
m_features.clear();
|
||
m_open_feature = -1;
|
||
if (cb)
|
||
cb(ents, cons, pl);
|
||
}
|
||
|
||
void DesignSketchTool::begin_constrain(const SketchProfile& prof, const SketchPlane& plane)
|
||
{
|
||
m_plane = plane;
|
||
m_mode = Mode::Constrain;
|
||
m_points = prof.points;
|
||
m_entities.clear();
|
||
m_has_cursor = false;
|
||
m_sel_a = m_sel_b = -1;
|
||
m_constrain_entities = false;
|
||
m_pick0 = m_pick1 = m_pick2 = -1;
|
||
m_active = true;
|
||
}
|
||
|
||
void DesignSketchTool::begin_constrain_entities(const std::vector<SketchEntity>& ents,
|
||
const SketchPlane& plane)
|
||
{
|
||
m_plane = plane;
|
||
m_mode = Mode::Constrain;
|
||
m_constrain_entities = true;
|
||
m_points.clear();
|
||
m_entities = ents;
|
||
m_has_cursor = false;
|
||
m_sel_a = m_sel_b = -1;
|
||
m_pick0 = m_pick1 = m_pick2 = -1;
|
||
m_active = true;
|
||
}
|
||
|
||
bool DesignSketchTool::selected_segment(int& a, int& b) const
|
||
{
|
||
if (m_sel_a < 0 || m_sel_b < 0)
|
||
return false;
|
||
a = m_sel_a;
|
||
b = m_sel_b;
|
||
return true;
|
||
}
|
||
|
||
bool DesignSketchTool::screen_to_plane(GLCanvas3D& canvas, const wxMouseEvent& evt, Vec2d& out) const
|
||
{
|
||
Point pos(evt.GetX(), evt.GetY());
|
||
Linef3 r = canvas.mouse_ray(pos);
|
||
out = m_plane.project(r.a, r.vector());
|
||
return true;
|
||
}
|
||
|
||
bool DesignSketchTool::near_first(const Vec2d& p) const
|
||
{
|
||
return m_points.size() >= 3 && (p - m_points[0]).squaredNorm() < 4.0;
|
||
}
|
||
|
||
Vec2d DesignSketchTool::snap_dir(const Vec2d& anchor, const Vec2d& raw, bool& locked) const
|
||
{
|
||
locked = false;
|
||
if (m_snap_off) return raw;
|
||
|
||
const Vec2d d = raw - anchor;
|
||
const double r = d.norm();
|
||
if (r < 1e-9) return raw;
|
||
|
||
const double tol_deg = 5.0; // inference half-window
|
||
double ang = std::atan2(d.y(), d.x()) * 180.0 / M_PI; // (-180,180]
|
||
if (ang < 0.0) ang += 360.0; // [0,360)
|
||
|
||
// Base angles within one quadrant, replicated every 90 deg up to 360.
|
||
static const double base[] = {0.0, 30.0, 45.0, 60.0};
|
||
double best_cand = ang, best_diff = 1e30;
|
||
for (int q = 0; q < 4; ++q) {
|
||
for (double b : base) {
|
||
const double cand = b + 90.0 * q;
|
||
double diff = std::abs(ang - cand);
|
||
if (diff > 180.0) diff = 360.0 - diff;
|
||
if (diff < best_diff) { best_diff = diff; best_cand = cand; }
|
||
}
|
||
}
|
||
if (best_diff > tol_deg) return raw;
|
||
|
||
locked = true;
|
||
const double rad = best_cand * M_PI / 180.0;
|
||
return anchor + r * Vec2d(std::cos(rad), std::sin(rad));
|
||
}
|
||
|
||
double DesignSketchTool::screen_tol(GLCanvas3D& canvas, const wxMouseEvent& evt,
|
||
const Vec2d& at, double px) const
|
||
{
|
||
// Project a point `px` screen pixels away and measure the gap in plane units.
|
||
const Linef3 r2 = canvas.mouse_ray(Point(evt.GetX() + int(px), evt.GetY()));
|
||
const Vec2d p2 = m_plane.project(r2.a, r2.vector());
|
||
return std::max(1e-3, (p2 - at).norm());
|
||
}
|
||
|
||
InferenceSnap DesignSketchTool::infer_at(GLCanvas3D& canvas, const wxMouseEvent& evt,
|
||
const Vec2d& raw) const
|
||
{
|
||
if (evt.ShiftDown()) { InferenceSnap s; s.point = raw; return s; } // Shift suppresses
|
||
const double tol = screen_tol(canvas, evt, raw);
|
||
return infer_point_snap(m_entities, raw, tol);
|
||
}
|
||
|
||
Vec2d DesignSketchTool::snap_vertex(GLCanvas3D& canvas, const wxMouseEvent& evt,
|
||
const Vec2d& raw, bool& snapped) const
|
||
{
|
||
const InferenceSnap s = infer_at(canvas, evt, raw);
|
||
// Cache the target so render() can draw a snap hint; only endpoint/centre/origin
|
||
// count as a "vertex" snap for the callers that gate angle inference on it.
|
||
const_cast<DesignSketchTool*>(this)->m_cursor_snap = s;
|
||
snapped = s.snapped(); // any hard snap moves the cursor + suppresses angle lock
|
||
return s.point;
|
||
}
|
||
|
||
bool DesignSketchTool::has_coincident(int ea, SketchPointRole ra, int eb, SketchPointRole rb) const
|
||
{
|
||
for (const auto& c : m_constraints) {
|
||
if (c.type != SketchConstraintType::Coincident) continue;
|
||
if ((c.ea == ea && c.ra == ra && c.eb == eb && c.rb == rb) ||
|
||
(c.ea == eb && c.ra == rb && c.eb == ea && c.rb == ra))
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool DesignSketchTool::try_add_constraints(const std::vector<SketchEntityConstraintDef>& cands)
|
||
{
|
||
if (cands.empty()) return true;
|
||
const size_t mark = m_constraints.size();
|
||
for (const auto& c : cands) m_constraints.push_back(c);
|
||
if (solve_sketch_entities(m_entities, m_constraints))
|
||
return true;
|
||
m_constraints.resize(mark); // roll back the conflicting batch
|
||
// No re-solve to "restore": a failed solve no longer touches the geometry
|
||
// (SketchSolver.cpp only writes back on success), so m_entities still holds the
|
||
// prior solved state exactly. snaporca-pl5.
|
||
return false;
|
||
}
|
||
|
||
void DesignSketchTool::infer_auto_constraints(int base)
|
||
{
|
||
const int n = int(m_entities.size());
|
||
if (base < 0 || base >= n) return;
|
||
|
||
// Endpoint roles an entity exposes for coincidence matching.
|
||
auto roles_of = [](const SketchEntity& e, SketchPointRole out[2]) -> int {
|
||
switch (e.type) {
|
||
case SketchEntity::Type::Line: out[0] = SketchPointRole::P0; out[1] = SketchPointRole::P1; return 2;
|
||
case SketchEntity::Type::Arc: out[0] = SketchPointRole::P0; out[1] = SketchPointRole::P1; return 2;
|
||
case SketchEntity::Type::BSpline:out[0] = SketchPointRole::P0; out[1] = SketchPointRole::P1; return 2;
|
||
case SketchEntity::Type::Point: out[0] = SketchPointRole::P0; return 1;
|
||
default: return 0; // circle: centre coincidence handled by Concentric, not here
|
||
}
|
||
};
|
||
|
||
// 1) Coincident between a new endpoint and any (co-located) endpoint of another
|
||
// entity. snap_vertex already drove the coordinates together; this records it
|
||
// so a re-solve keeps the loop closed.
|
||
std::vector<SketchEntityConstraintDef> coincs;
|
||
for (int i = base; i < n; ++i) {
|
||
SketchPointRole ir[2]; const int ni = roles_of(m_entities[i], ir);
|
||
for (int a = 0; a < ni; ++a) {
|
||
Vec2d pa; if (!point_at(i, ir[a], pa)) continue;
|
||
for (int j = 0; j < n; ++j) {
|
||
if (j == i) continue;
|
||
SketchPointRole jr[2]; const int nj = roles_of(m_entities[j], jr);
|
||
for (int b = 0; b < nj; ++b) {
|
||
if (j >= base && j < i) continue; // avoid duplicate (i,j)/(j,i)
|
||
Vec2d pb; if (!point_at(j, jr[b], pb)) continue;
|
||
if ((pa - pb).squaredNorm() > 1e-6) continue;
|
||
if (has_coincident(i, ir[a], j, jr[b])) continue;
|
||
SketchEntityConstraintDef c;
|
||
c.type = SketchConstraintType::Coincident;
|
||
c.ea = i; c.ra = ir[a]; c.eb = j; c.rb = jr[b];
|
||
coincs.push_back(c);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
try_add_constraints(coincs); // co-located points: consistent by construction
|
||
|
||
// 2) Horizontal / Vertical on axis-aligned new line segments (added one at a time
|
||
// so a single conflict never drops the others).
|
||
for (int i = base; i < n; ++i) {
|
||
if (m_entities[i].type != SketchEntity::Type::Line) continue;
|
||
auto ax = infer_axis_constraint(m_entities[i].p0, m_entities[i].p1);
|
||
if (!ax) continue;
|
||
SketchEntityConstraintDef c;
|
||
c.type = *ax;
|
||
c.ea = i; c.ra = SketchPointRole::P0;
|
||
c.eb = i; c.rb = SketchPointRole::P1;
|
||
try_add_constraints({ c });
|
||
}
|
||
|
||
resolve_live();
|
||
}
|
||
|
||
static std::vector<Vec2d> circle_polygon(const Vec2d& c, double r, int n = 48)
|
||
{
|
||
std::vector<Vec2d> v; v.reserve(n);
|
||
for (int i = 0; i < n; ++i) {
|
||
const double a = 2.0 * M_PI * double(i) / double(n);
|
||
v.push_back(Vec2d(c.x() + r * std::cos(a), c.y() + r * std::sin(a)));
|
||
}
|
||
return v;
|
||
}
|
||
|
||
// Point on an ellipse at parametric angle theta: center + R(phi)*(a cos t, b sin t).
|
||
static Vec2d ellipse_point(const Vec2d& c, double a, double b, double phi, double t)
|
||
{
|
||
const double cu = std::cos(phi), su = std::sin(phi);
|
||
const double x = a * std::cos(t), y = b * std::sin(t);
|
||
return Vec2d(c.x() + x * cu - y * su, c.y() + x * su + y * cu);
|
||
}
|
||
|
||
// Tessellate an ellipse arc parametric range [t0,t1] into a polyline.
|
||
static std::vector<Vec2d> ellipse_polyline(const Vec2d& c, double a, double b, double phi,
|
||
double t0, double t1, int n = 48)
|
||
{
|
||
std::vector<Vec2d> v; v.reserve(n + 1);
|
||
for (int i = 0; i <= n; ++i)
|
||
v.push_back(ellipse_point(c, a, b, phi, t0 + (t1 - t0) * double(i) / double(n)));
|
||
return v;
|
||
}
|
||
|
||
// Clamped uniform B-spline (degree min(3, n-1)) through control poles. The knot
|
||
// construction mirrors SketchEngine::entities_to_wire's OCCT Geom_BSplineCurve so
|
||
// the previewed/extruded curve match. de Boor evaluation.
|
||
static int bspline_degree(int n) { return n >= 4 ? 3 : (n >= 2 ? n - 1 : 0); }
|
||
|
||
static std::vector<double> bspline_knots(int n, int p)
|
||
{
|
||
std::vector<double> U; // full knot vector, length n+p+1
|
||
const int interior = n - p - 1;
|
||
for (int i = 0; i <= p; ++i) U.push_back(0.0);
|
||
for (int i = 1; i <= interior; ++i) U.push_back(double(i));
|
||
const double last = double(interior + 1);
|
||
for (int i = 0; i <= p; ++i) U.push_back(last);
|
||
return U;
|
||
}
|
||
|
||
static Vec2d bspline_eval(const std::vector<Vec2d>& P, const std::vector<double>& U, int p, double u)
|
||
{
|
||
const int n = int(P.size());
|
||
if (u <= U[p]) return P.front();
|
||
if (u >= U[n]) return P.back(); // U[n] == domain max (clamped)
|
||
int k = p;
|
||
while (k < n - 1 && U[k + 1] <= u) ++k; // span: U[k] <= u < U[k+1]
|
||
std::vector<Vec2d> d(p + 1);
|
||
for (int j = 0; j <= p; ++j) d[j] = P[j + k - p];
|
||
for (int r = 1; r <= p; ++r)
|
||
for (int j = p; j >= r; --j) {
|
||
const double denom = U[j + 1 + k - r] - U[j + k - p];
|
||
const double a = denom > 1e-12 ? (u - U[j + k - p]) / denom : 0.0;
|
||
d[j] = (1.0 - a) * d[j - 1] + a * d[j];
|
||
}
|
||
return d[p];
|
||
}
|
||
|
||
static std::vector<Vec2d> bspline_polyline(const std::vector<Vec2d>& ctrl, int samples = 0)
|
||
{
|
||
const int n = int(ctrl.size());
|
||
if (n < 2) return ctrl;
|
||
const int p = bspline_degree(n);
|
||
const std::vector<double> U = bspline_knots(n, p);
|
||
const double umax = double(n - p);
|
||
if (samples <= 0) samples = std::max(24, 14 * (n - 1));
|
||
std::vector<Vec2d> out; out.reserve(samples + 1);
|
||
for (int i = 0; i <= samples; ++i)
|
||
out.push_back(bspline_eval(ctrl, U, p, umax * double(i) / double(samples)));
|
||
return out;
|
||
}
|
||
|
||
// ---- entity builders --------------------------------------------------------
|
||
|
||
void DesignSketchTool::push_line(const Vec2d& a, const Vec2d& b)
|
||
{
|
||
if ((b - a).squaredNorm() < 1e-9)
|
||
return;
|
||
SketchEntity e;
|
||
e.type = SketchEntity::Type::Line;
|
||
e.p0 = a;
|
||
e.p1 = b;
|
||
e.construction = m_construction;
|
||
m_entities.push_back(e);
|
||
}
|
||
|
||
bool DesignSketchTool::add_imported_regions(
|
||
const std::vector<std::vector<std::vector<Vec2d>>>& regions)
|
||
{
|
||
if (!m_active) return false; // no session to draw into; caller makes a feature
|
||
const bool saved = m_construction;
|
||
m_construction = false; // art is real geometry, never construction lines
|
||
size_t before = m_entities.size();
|
||
for (const auto& region : regions)
|
||
for (const auto& loop : region)
|
||
push_closed_lines(loop); // every glyph contour, holes included
|
||
m_construction = saved;
|
||
if (m_entities.size() == before) return false; // nothing importable — say so, do not lie
|
||
// Art is not "just drawn", so it must NOT enter the draw-then-edit queue. Without this the
|
||
// glyph contours are treated as fresh entities and a Length field opens on the first of
|
||
// them — on a word, that is one value editor per segment, and an open field freezes the
|
||
// canvas (snaporca-yce). reset_autoedit() marks every entity as already seen.
|
||
reset_autoedit();
|
||
// The new lines carry no constraints, so the solver has nothing to move; resolve anyway so
|
||
// the degrees-of-freedom readout counts them instead of going stale.
|
||
resolve_live();
|
||
return true;
|
||
}
|
||
|
||
void DesignSketchTool::push_closed_lines(const std::vector<Vec2d>& corners)
|
||
{
|
||
const size_t n = corners.size();
|
||
if (n < 2) return;
|
||
for (size_t i = 0; i < n; ++i)
|
||
push_line(corners[i], corners[(i + 1) % n]);
|
||
}
|
||
|
||
void DesignSketchTool::push_open_chain(const std::vector<Vec2d>& pts)
|
||
{
|
||
for (size_t i = 0; i + 1 < pts.size(); ++i)
|
||
push_line(pts[i], pts[i + 1]);
|
||
}
|
||
|
||
void DesignSketchTool::push_circle(const Vec2d& center, double radius)
|
||
{
|
||
if (radius < 1e-3) return;
|
||
SketchEntity e;
|
||
e.type = SketchEntity::Type::Circle;
|
||
e.center = center;
|
||
e.p0 = center;
|
||
e.radius = radius;
|
||
e.construction = m_construction;
|
||
m_entities.push_back(e);
|
||
}
|
||
|
||
void DesignSketchTool::push_point(const Vec2d& p)
|
||
{
|
||
SketchEntity e;
|
||
e.type = SketchEntity::Type::Point;
|
||
e.p0 = p;
|
||
e.center = p;
|
||
e.construction = m_construction;
|
||
m_entities.push_back(e);
|
||
}
|
||
|
||
void DesignSketchTool::append_entities(const std::vector<SketchEntity>& ents)
|
||
{
|
||
for (const SketchEntity& e : ents)
|
||
m_entities.push_back(e);
|
||
}
|
||
|
||
static double wrap_2pi(double a)
|
||
{
|
||
while (a < 0.0) a += 2.0 * M_PI;
|
||
while (a >= 2.0 * M_PI) a -= 2.0 * M_PI;
|
||
return a;
|
||
}
|
||
|
||
// Circumcircle of 3 points. Returns false if (nearly) collinear.
|
||
static bool circumcircle(const Vec2d& a, const Vec2d& b, const Vec2d& c,
|
||
Vec2d& center, double& radius)
|
||
{
|
||
const double d = 2.0 * (a.x() * (b.y() - c.y()) +
|
||
b.x() * (c.y() - a.y()) +
|
||
c.x() * (a.y() - b.y()));
|
||
if (std::abs(d) < 1e-9)
|
||
return false;
|
||
const double a2 = a.squaredNorm(), b2 = b.squaredNorm(), c2 = c.squaredNorm();
|
||
const double ux = (a2 * (b.y() - c.y()) + b2 * (c.y() - a.y()) + c2 * (a.y() - b.y())) / d;
|
||
const double uy = (a2 * (c.x() - b.x()) + b2 * (a.x() - c.x()) + c2 * (b.x() - a.x())) / d;
|
||
center = Vec2d(ux, uy);
|
||
radius = (a - center).norm();
|
||
return true;
|
||
}
|
||
|
||
// Build an Arc entity that sweeps start -> end passing through `through`. The
|
||
// kernel reconstructs the mid from (start_angle+end_angle)/2, so the angle pair
|
||
// must bracket `through` on the correct side of the circle.
|
||
static SketchEntity make_arc_through(const Vec2d& center, double radius,
|
||
const Vec2d& start, const Vec2d& end,
|
||
const Vec2d& through, bool construction)
|
||
{
|
||
const double a_start = std::atan2(start.y() - center.y(), start.x() - center.x());
|
||
const double a_end = std::atan2(end.y() - center.y(), end.x() - center.x());
|
||
const double a_thru = std::atan2(through.y() - center.y(), through.x() - center.x());
|
||
const double de = wrap_2pi(a_end - a_start); // CCW sweep to end (0,2π)
|
||
const double d3 = wrap_2pi(a_thru - a_start); // CCW position of through
|
||
SketchEntity e;
|
||
e.type = SketchEntity::Type::Arc;
|
||
e.center = center;
|
||
e.radius = radius;
|
||
e.p0 = start;
|
||
e.p1 = end;
|
||
e.start_angle = a_start;
|
||
e.end_angle = (d3 <= de) ? (a_start + de) : (a_start + de - 2.0 * M_PI);
|
||
e.construction = construction;
|
||
return e;
|
||
}
|
||
|
||
std::vector<SketchEntity> DesignSketchTool::make_three_point_circle(const Vec2d& a, const Vec2d& b, const Vec2d& c) const
|
||
{
|
||
Vec2d center; double radius;
|
||
if (!circumcircle(a, b, c, center, radius))
|
||
return {};
|
||
SketchEntity e;
|
||
e.type = SketchEntity::Type::Circle;
|
||
e.center = center;
|
||
e.p0 = center;
|
||
e.radius = radius;
|
||
e.construction = m_construction;
|
||
return { e };
|
||
}
|
||
|
||
std::vector<SketchEntity> DesignSketchTool::make_three_point_arc(const Vec2d& start, const Vec2d& end, const Vec2d& on_arc) const
|
||
{
|
||
Vec2d center; double radius;
|
||
if (!circumcircle(start, end, on_arc, center, radius))
|
||
return {};
|
||
return { make_arc_through(center, radius, start, end, on_arc, m_construction) };
|
||
}
|
||
|
||
std::vector<SketchEntity> DesignSketchTool::make_tangent_arc(const Vec2d& start, const Vec2d& end) const
|
||
{
|
||
// Tangent direction at `start` = exit direction of the previous entity.
|
||
Vec2d t(1, 0);
|
||
bool have_t = false;
|
||
if (!m_entities.empty()) {
|
||
const SketchEntity& prev = m_entities.back();
|
||
if (prev.type == SketchEntity::Type::Line) {
|
||
t = prev.p1 - prev.p0; have_t = (t.squaredNorm() > 1e-12);
|
||
} else if (prev.type == SketchEntity::Type::Arc) {
|
||
// Tangent at the arc end p1 is perpendicular to its radius, in the
|
||
// sweep direction.
|
||
const Vec2d r = prev.p1 - prev.center;
|
||
const double sweep = prev.end_angle - prev.start_angle;
|
||
t = (sweep >= 0.0) ? Vec2d(-r.y(), r.x()) : Vec2d(r.y(), -r.x());
|
||
have_t = (r.squaredNorm() > 1e-12);
|
||
}
|
||
}
|
||
const Vec2d se = end - start;
|
||
if (!have_t || se.squaredNorm() < 1e-12) {
|
||
// No tangent reference or zero length: fall back to a straight line.
|
||
SketchEntity e; e.type = SketchEntity::Type::Line; e.p0 = start; e.p1 = end;
|
||
e.construction = m_construction;
|
||
return { e };
|
||
}
|
||
t.normalize();
|
||
const Vec2d n(-t.y(), t.x()); // unit normal to the tangent
|
||
const double denom = 2.0 * n.dot(se);
|
||
if (std::abs(denom) < 1e-9) { // end lies along the tangent: line
|
||
SketchEntity e; e.type = SketchEntity::Type::Line; e.p0 = start; e.p1 = end;
|
||
e.construction = m_construction;
|
||
return { e };
|
||
}
|
||
const double R = se.squaredNorm() / denom; // signed radius along n
|
||
const Vec2d center = start + n * R;
|
||
const double radius = std::abs(R);
|
||
// Mid of the tangent arc: project the chord midpoint outward onto the circle.
|
||
const Vec2d chord_mid = (start + end) * 0.5;
|
||
Vec2d to_mid = chord_mid - center;
|
||
if (to_mid.squaredNorm() < 1e-12) to_mid = n;
|
||
to_mid.normalize();
|
||
const Vec2d through = center + to_mid * radius;
|
||
return { make_arc_through(center, radius, start, end, through, m_construction) };
|
||
}
|
||
|
||
std::vector<SketchEntity> DesignSketchTool::make_center_arc(const Vec2d& center, const Vec2d& start, const Vec2d& end_dir) const
|
||
{
|
||
const double radius = (start - center).norm();
|
||
if (radius < 1e-9)
|
||
return {};
|
||
const double a_start = std::atan2(start.y() - center.y(), start.x() - center.x());
|
||
const double a_end = std::atan2(end_dir.y() - center.y(), end_dir.x() - center.x());
|
||
const double de = wrap_2pi(a_end - a_start); // CCW sweep start -> end (0,2π)
|
||
const Vec2d end = center + radius * Vec2d(std::cos(a_end), std::sin(a_end));
|
||
const double a_mid = a_start + de * 0.5; // bisector brackets the sweep
|
||
const Vec2d through = center + radius * Vec2d(std::cos(a_mid), std::sin(a_mid));
|
||
return { make_arc_through(center, radius, start, end, through, m_construction) };
|
||
}
|
||
|
||
std::vector<SketchEntity> DesignSketchTool::make_slot(const Vec2d& c0, const Vec2d& c1, double half_width) const
|
||
{
|
||
std::vector<SketchEntity> out;
|
||
Vec2d u = c1 - c0;
|
||
if (u.squaredNorm() < 1e-12 || half_width < 1e-6)
|
||
return out;
|
||
u.normalize();
|
||
const Vec2d nrm(-u.y(), u.x());
|
||
const double w = half_width;
|
||
// Names avoid termios macros (B0 is a baud-rate #define pulled in transitively).
|
||
const Vec2d top0 = c0 + nrm * w, top1 = c1 + nrm * w; // upper side (c0 -> c1)
|
||
const Vec2d bot1 = c1 - nrm * w, bot0 = c0 - nrm * w; // lower side (c1 -> c0)
|
||
|
||
auto line = [&](const Vec2d& p0, const Vec2d& p1) {
|
||
SketchEntity e; e.type = SketchEntity::Type::Line; e.p0 = p0; e.p1 = p1;
|
||
e.construction = m_construction; return e;
|
||
};
|
||
out.push_back(line(top0, top1)); // top
|
||
out.push_back(make_arc_through(c1, w, top1, bot1, c1 + u * w, m_construction)); // cap @c1 (+u)
|
||
out.push_back(line(bot1, bot0)); // bottom
|
||
out.push_back(make_arc_through(c0, w, bot0, top0, c0 - u * w, m_construction)); // cap @c0 (-u)
|
||
return out;
|
||
}
|
||
|
||
// Arc slot: a slot whose centerline is a circular arc (center, start, end_dir on
|
||
// the same radius). Bounded by an outer arc (Rc+w), an inner arc (Rc-w) and two
|
||
// semicircular end caps. CCW closed loop, mirroring make_slot's structure.
|
||
std::vector<SketchEntity> DesignSketchTool::make_arc_slot(const Vec2d& center, const Vec2d& start,
|
||
const Vec2d& end_dir, double half_width) const
|
||
{
|
||
std::vector<SketchEntity> out;
|
||
const double Rc = (start - center).norm();
|
||
const double w = half_width;
|
||
if (Rc < 1e-6 || w < 1e-6 || w >= Rc) return out;
|
||
const Vec2d dirS = (start - center) / Rc;
|
||
Vec2d de = end_dir - center;
|
||
if (de.squaredNorm() < 1e-12) return out;
|
||
const Vec2d dirE = de.normalized();
|
||
const double aS = std::atan2(dirS.y(), dirS.x());
|
||
const double aE = std::atan2(dirE.y(), dirE.x());
|
||
const double sweep = wrap_2pi(aE - aS); // CCW start -> end
|
||
const double aMid = aS + sweep * 0.5;
|
||
const Vec2d uMid(std::cos(aMid), std::sin(aMid));
|
||
const Vec2d Sc = center + Rc * dirS; // centerline start point (cap centre)
|
||
const Vec2d Ec = center + Rc * dirE; // centerline end point (cap centre)
|
||
const Vec2d S_out = center + (Rc + w) * dirS, S_in = center + (Rc - w) * dirS;
|
||
const Vec2d E_out = center + (Rc + w) * dirE, E_in = center + (Rc - w) * dirE;
|
||
const Vec2d tE(-dirE.y(), dirE.x()); // CCW travel-forward tangent at E
|
||
const Vec2d tS(-dirS.y(), dirS.x()); // CCW travel-forward tangent at S
|
||
out.push_back(make_arc_through(center, Rc + w, S_out, E_out, center + (Rc + w) * uMid, m_construction)); // outer
|
||
out.push_back(make_arc_through(Ec, w, E_out, E_in, Ec + w * tE, m_construction)); // cap @E (forward)
|
||
out.push_back(make_arc_through(center, Rc - w, E_in, S_in, center + (Rc - w) * uMid, m_construction)); // inner
|
||
out.push_back(make_arc_through(Sc, w, S_in, S_out, Sc - w * tS, m_construction)); // cap @S (backward)
|
||
return out;
|
||
}
|
||
|
||
// Rounded rectangle: axis-aligned box (a,b opposite corners) with filleted
|
||
// corners. radius_pt's distance to the nearest corner sets the fillet radius.
|
||
// 4 straight edges + 4 quarter arcs, CCW.
|
||
// Build the 8 entities (4 lines + 4 corner arcs, CCW) of an axis-aligned rounded box
|
||
// from explicit bounds + fillet radius. Shared by make_rounded_rect (gesture) and
|
||
// set_rounded_rect (label/handle edit) so the entity order/count is identical → a rebuild
|
||
// in place keeps constraint indices into the feature span valid.
|
||
std::vector<SketchEntity> DesignSketchTool::rounded_rect_entities(double xmin, double ymin,
|
||
double xmax, double ymax, double r) const
|
||
{
|
||
std::vector<SketchEntity> out;
|
||
auto line = [&](const Vec2d& p0, const Vec2d& p1) {
|
||
SketchEntity e; e.type = SketchEntity::Type::Line; e.p0 = p0; e.p1 = p1;
|
||
e.construction = m_construction; return e; };
|
||
auto corner = [&](const Vec2d& O, const Vec2d& sharp, const Vec2d& start, const Vec2d& end) {
|
||
const Vec2d thr = O + r * (sharp - O).normalized();
|
||
return make_arc_through(O, r, start, end, thr, m_construction); };
|
||
out.push_back(line({xmin + r, ymin}, {xmax - r, ymin})); // bottom
|
||
out.push_back(corner({xmax - r, ymin + r}, {xmax, ymin}, {xmax - r, ymin}, {xmax, ymin + r})); // BR
|
||
out.push_back(line({xmax, ymin + r}, {xmax, ymax - r})); // right
|
||
out.push_back(corner({xmax - r, ymax - r}, {xmax, ymax}, {xmax, ymax - r}, {xmax - r, ymax})); // TR
|
||
out.push_back(line({xmax - r, ymax}, {xmin + r, ymax})); // top
|
||
out.push_back(corner({xmin + r, ymax - r}, {xmin, ymax}, {xmin + r, ymax}, {xmin, ymax - r})); // TL
|
||
out.push_back(line({xmin, ymax - r}, {xmin, ymin + r})); // left
|
||
out.push_back(corner({xmin + r, ymin + r}, {xmin, ymin}, {xmin, ymin + r}, {xmin + r, ymin})); // BL
|
||
return out;
|
||
}
|
||
|
||
std::vector<SketchEntity> DesignSketchTool::make_rounded_rect(const Vec2d& a, const Vec2d& b, const Vec2d& radius_pt) const
|
||
{
|
||
std::vector<SketchEntity> out;
|
||
const double xmin = std::min(a.x(), b.x()), xmax = std::max(a.x(), b.x());
|
||
const double ymin = std::min(a.y(), b.y()), ymax = std::max(a.y(), b.y());
|
||
const double bw = xmax - xmin, bh = ymax - ymin;
|
||
if (bw < 1e-6 || bh < 1e-6) return out;
|
||
const Vec2d cs[4] = { {xmin,ymin}, {xmax,ymin}, {xmax,ymax}, {xmin,ymax} };
|
||
double r = 1e18;
|
||
for (const Vec2d& c : cs) r = std::min(r, (radius_pt - c).norm());
|
||
r = std::min(r, std::min(bw, bh) * 0.5);
|
||
if (r < 1e-6) { // degenerate -> plain rectangle
|
||
auto line = [&](const Vec2d& p0, const Vec2d& p1) {
|
||
SketchEntity e; e.type = SketchEntity::Type::Line; e.p0 = p0; e.p1 = p1;
|
||
e.construction = m_construction; return e; };
|
||
for (int i = 0; i < 4; ++i) out.push_back(line(cs[i], cs[(i + 1) % 4]));
|
||
return out;
|
||
}
|
||
return rounded_rect_entities(xmin, ymin, xmax, ymax, r);
|
||
}
|
||
|
||
std::vector<SketchEntity> DesignSketchTool::make_polygon(const Vec2d& center, const Vec2d& vertex, int sides) const
|
||
{
|
||
std::vector<SketchEntity> out;
|
||
if (sides < 3) sides = 3;
|
||
const Vec2d rv = vertex - center;
|
||
const double d = rv.norm();
|
||
if (d < 1e-6) return out;
|
||
// Inscribed: cursor is a vertex (circumradius = d). Circumscribed: cursor is an
|
||
// edge midpoint (apothem = d) → circumradius R = d / cos(pi/n), rotated by half
|
||
// a step so an edge midpoint points at the cursor.
|
||
double R = d, a0 = std::atan2(rv.y(), rv.x());
|
||
if (m_polygon_circumscribed) {
|
||
R = d / std::cos(M_PI / double(sides));
|
||
a0 = std::atan2(rv.y(), rv.x()) - M_PI / double(sides);
|
||
}
|
||
std::vector<Vec2d> verts; verts.reserve(sides);
|
||
for (int i = 0; i < sides; ++i) {
|
||
const double a = a0 + 2.0 * M_PI * double(i) / double(sides);
|
||
verts.push_back(Vec2d(center.x() + R * std::cos(a), center.y() + R * std::sin(a)));
|
||
}
|
||
for (int i = 0; i < sides; ++i) {
|
||
SketchEntity e; e.type = SketchEntity::Type::Line;
|
||
e.p0 = verts[i]; e.p1 = verts[(i + 1) % sides];
|
||
e.construction = m_construction;
|
||
out.push_back(e);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
// Derive (a, b, phi) of an ellipse from the 3 defining clicks. First axis click =
|
||
// major (a, phi); the minor point's perpendicular distance to the major axis = b,
|
||
// clamped to a so OCCT's a >= b holds.
|
||
static void ellipse_axes(const Vec2d& center, const Vec2d& major_end, const Vec2d& minor_pt,
|
||
double& a, double& b, double& phi)
|
||
{
|
||
const Vec2d maj = major_end - center;
|
||
a = std::max(maj.norm(), 1e-6);
|
||
phi = std::atan2(maj.y(), maj.x());
|
||
const Vec2d n(-std::sin(phi), std::cos(phi)); // minor-axis direction
|
||
b = std::min(std::abs((minor_pt - center).dot(n)), a);
|
||
}
|
||
|
||
// Parametric angle on an ellipse of the point nearest `q` (q projected onto the frame).
|
||
static double ellipse_param_of(const Vec2d& center, double a, double b, double phi, const Vec2d& q)
|
||
{
|
||
const Vec2d d = q - center;
|
||
const double cu = std::cos(phi), su = std::sin(phi);
|
||
const double u = d.x() * cu + d.y() * su; // along major
|
||
const double v = -d.x() * su + d.y() * cu; // along minor
|
||
return std::atan2(v / std::max(b, 1e-9), u / std::max(a, 1e-9));
|
||
}
|
||
|
||
std::vector<SketchEntity> DesignSketchTool::make_ellipse(const Vec2d& center, const Vec2d& major_end,
|
||
const Vec2d& minor_pt) const
|
||
{
|
||
double a, b, phi;
|
||
ellipse_axes(center, major_end, minor_pt, a, b, phi);
|
||
if (b < 1e-6) return {};
|
||
SketchEntity e;
|
||
e.type = SketchEntity::Type::Ellipse;
|
||
e.center = center; e.p0 = center;
|
||
e.radius = a; e.rminor = b; e.rotation = phi;
|
||
e.start_angle = 0.0; e.end_angle = 2.0 * M_PI;
|
||
e.construction = m_construction;
|
||
return { e };
|
||
}
|
||
|
||
std::vector<SketchEntity> DesignSketchTool::make_ellipse_arc(const Vec2d& center, const Vec2d& major_end,
|
||
const Vec2d& minor_pt, const Vec2d& start_pt,
|
||
const Vec2d& end_pt) const
|
||
{
|
||
double a, b, phi;
|
||
ellipse_axes(center, major_end, minor_pt, a, b, phi);
|
||
if (b < 1e-6) return {};
|
||
double t0 = ellipse_param_of(center, a, b, phi, start_pt);
|
||
double t1 = ellipse_param_of(center, a, b, phi, end_pt);
|
||
// CCW sweep from t0 to t1.
|
||
while (t1 <= t0) t1 += 2.0 * M_PI;
|
||
SketchEntity e;
|
||
e.type = SketchEntity::Type::EllipseArc;
|
||
e.center = center;
|
||
e.radius = a; e.rminor = b; e.rotation = phi;
|
||
e.start_angle = t0; e.end_angle = t1;
|
||
e.p0 = ellipse_point(center, a, b, phi, t0);
|
||
e.p1 = ellipse_point(center, a, b, phi, t1);
|
||
e.construction = m_construction;
|
||
return { e };
|
||
}
|
||
|
||
std::vector<SketchEntity> DesignSketchTool::make_bspline(const std::vector<Vec2d>& ctrl) const
|
||
{
|
||
if (ctrl.size() < 2) return {};
|
||
SketchEntity e;
|
||
e.type = SketchEntity::Type::BSpline;
|
||
e.ctrl = ctrl;
|
||
e.p0 = ctrl.front();
|
||
e.p1 = ctrl.back();
|
||
e.construction = m_construction;
|
||
return { e };
|
||
}
|
||
|
||
std::vector<Vec2d> DesignSketchTool::entity_polyline(const SketchEntity& e, bool& closed) const
|
||
{
|
||
closed = false;
|
||
switch (e.type) {
|
||
case SketchEntity::Type::Line:
|
||
return { e.p0, e.p1 };
|
||
case SketchEntity::Type::Circle:
|
||
closed = true;
|
||
return circle_polygon(e.center, e.radius);
|
||
case SketchEntity::Type::Arc: {
|
||
const int n = 24;
|
||
std::vector<Vec2d> pts; pts.reserve(n + 1);
|
||
for (int i = 0; i <= n; ++i) {
|
||
const double a = e.start_angle + (e.end_angle - e.start_angle) * double(i) / double(n);
|
||
pts.push_back(Vec2d(e.center.x() + e.radius * std::cos(a),
|
||
e.center.y() + e.radius * std::sin(a)));
|
||
}
|
||
return pts;
|
||
}
|
||
case SketchEntity::Type::Ellipse:
|
||
closed = true;
|
||
return ellipse_polyline(e.center, e.radius, e.rminor, e.rotation, 0.0, 2.0 * M_PI);
|
||
case SketchEntity::Type::EllipseArc:
|
||
return ellipse_polyline(e.center, e.radius, e.rminor, e.rotation, e.start_angle, e.end_angle);
|
||
case SketchEntity::Type::BSpline:
|
||
return bspline_polyline(e.ctrl);
|
||
case SketchEntity::Type::Point:
|
||
return { e.p0 };
|
||
}
|
||
return {};
|
||
}
|
||
|
||
std::vector<std::vector<Vec2d>> DesignSketchTool::closed_regions() const
|
||
{
|
||
return closed_regions(m_entities);
|
||
}
|
||
|
||
std::vector<std::vector<Vec2d>> DesignSketchTool::closed_regions(const std::vector<SketchEntity>& ents) const
|
||
{
|
||
std::vector<std::vector<Vec2d>> out;
|
||
for (RegionLoop& r : region_loops(ents)) out.push_back(std::move(r.poly));
|
||
return out;
|
||
}
|
||
|
||
std::vector<std::vector<int>>
|
||
DesignSketchTool::region_entity_indices(const std::vector<SketchEntity>& ents) const
|
||
{
|
||
std::vector<std::vector<int>> out;
|
||
for (RegionLoop& r : region_loops(ents)) out.push_back(std::move(r.ents));
|
||
return out;
|
||
}
|
||
|
||
std::vector<std::vector<int>>
|
||
DesignSketchTool::region_entity_indices_with_holes(const std::vector<SketchEntity>& ents) const
|
||
{
|
||
const std::vector<RegionLoop> loops = region_loops(ents);
|
||
std::vector<std::vector<int>> out;
|
||
out.reserve(loops.size());
|
||
for (const RegionLoop& r : loops) {
|
||
std::vector<int> ids = r.ents;
|
||
for (int h : r.holes)
|
||
if (h >= 0 && h < int(loops.size()))
|
||
ids.insert(ids.end(), loops[h].ents.begin(), loops[h].ents.end());
|
||
out.push_back(std::move(ids));
|
||
}
|
||
return out;
|
||
}
|
||
|
||
// Nearest stroke, and the enclosing region if the cursor is inside one, for ONE committed
|
||
// sketch. Factored out so the single-click and double-click paths cannot drift apart: they must
|
||
// agree about what is under the pointer or one of them will act on something else.
|
||
//
|
||
// region_loops exists to find EXTRUDABLE regions, and by design it discards open chains — its
|
||
// own walk comment says so. Using it as the pick index meant a committed sketch of open lines
|
||
// had no pickable geometry whatsoever: the strokes drew, and not one of them could be clicked,
|
||
// so there was no way to select it and therefore none to edit or delete it. Whether a stroke
|
||
// bounds a region has nothing to do with whether the user can point at it. Region membership
|
||
// decides what a hit REPORTS, not whether the hit can happen.
|
||
static void dp_pick_trace(const char* fmt, ...); // defined below; used by the diagnostics here
|
||
static bool dp_pick_trace_on(); // ditto — lets callers skip building a message
|
||
void DesignSketchTool::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
|
||
{
|
||
const std::vector<RegionLoop> loops = region_loops(d.entities);
|
||
// What did the sketch decompose into, and what is under the click? This is the trace that
|
||
// settled snaporca-txp8 — it prints the loop table with each loop's hole count, so
|
||
// "containment is wrong" and "the click landed elsewhere" stop being indistinguishable.
|
||
// Guarded rather than merely silent: hit_display_sketch runs on every pick, and the message
|
||
// costs a string build and a heap allocation per loop even when nothing consumes it.
|
||
if (dp_pick_trace_on()) {
|
||
std::string h;
|
||
for (size_t r = 0; r < loops.size(); ++r) {
|
||
h += " loop" + std::to_string(r) + "(ents=" + std::to_string(loops[r].ents.size())
|
||
+ ",poly=" + std::to_string(loops[r].poly.size())
|
||
+ ",holes=" + std::to_string(loops[r].holes.size()) + ")";
|
||
}
|
||
dp_pick_trace("sketch feat=%d entities=%zu loops=%zu:%s", d.feature, d.entities.size(),
|
||
loops.size(), h.c_str());
|
||
}
|
||
std::vector<int> ent_region(d.entities.size(), -1);
|
||
for (int r = 0; r < int(loops.size()); ++r)
|
||
for (int ei : loops[r].ents)
|
||
if (ei >= 0 && ei < int(ent_region.size())) ent_region[ei] = r;
|
||
|
||
for (int ei = 0; ei < int(d.entities.size()); ++ei) {
|
||
if (d.entities[ei].construction) continue; // as region_loops filters it
|
||
const double ed = entity_pick_dist(p, d.entities[ei]);
|
||
if (ed <= tol * 3.0 && ed < edge_d) {
|
||
edge_d = ed; edge_feat = d.feature; edge_reg = ent_region[ei]; edge_ent = ei;
|
||
}
|
||
}
|
||
// Pick the region the point is REALLY in: inside its boundary and not inside any of its
|
||
// holes. Clicking the middle of a plate-with-hole must select the plate; clicking inside
|
||
// the hole must select the disc, not the plate. Previously the first containing polygon
|
||
// won, so a click inside the circle selected the rectangle.
|
||
for (int r = 0; r < int(loops.size()); ++r) {
|
||
if (face_feat >= 0) break;
|
||
if (!point_in_poly(p, loops[r].poly)) continue;
|
||
bool in_hole = false;
|
||
for (int h : loops[r].holes)
|
||
if (h >= 0 && h < int(loops.size()) && point_in_poly(p, loops[h].poly)) { in_hole = true; break; }
|
||
if (!in_hole) { face_feat = d.feature; face_reg = r; }
|
||
}
|
||
// edge_ent is printed because it is now DELIVERED (snaporca-3648) — a tool can ask for the
|
||
// line you pointed at, not just its loop, and "which entity did that click resolve to" is
|
||
// otherwise unanswerable from outside.
|
||
dp_pick_trace("region hit -> feat=%d reg=%d (edge_feat=%d edge_reg=%d edge_ent=%d)",
|
||
face_feat, face_reg, edge_feat, edge_reg, edge_ent);
|
||
}
|
||
|
||
std::vector<SketchEntity> DesignSketchTool::selected_loop_entities() const
|
||
{
|
||
if (m_display_pick < 0 || m_display_pick_region < 0) return {};
|
||
for (const DisplaySketch& d : m_display_sketches) {
|
||
if (d.feature != m_display_pick) continue;
|
||
const std::vector<RegionLoop> loops = region_loops(d.entities);
|
||
if (m_display_pick_region >= int(loops.size())) return {};
|
||
// The region's OWN boundary plus every loop nested in it. Handing over only the outer
|
||
// loop is what made a rectangle-with-a-circle extrude to a plain box: the circle was
|
||
// never passed to the kernel, so build_sketch_face had one loop to work with and the
|
||
// multi-loop path never ran.
|
||
std::vector<SketchEntity> out;
|
||
auto take = [&](int region) {
|
||
if (region < 0 || region >= int(loops.size())) return;
|
||
for (int ei : loops[region].ents)
|
||
if (ei >= 0 && ei < int(d.entities.size())) out.push_back(d.entities[ei]);
|
||
};
|
||
take(m_display_pick_region);
|
||
for (int h : loops[m_display_pick_region].holes) take(h);
|
||
return out;
|
||
}
|
||
return {};
|
||
}
|
||
|
||
// ---- Solid topology selection (whole -> face -> edge cycle) ----
|
||
|
||
void DesignSketchTool::set_solid_pick(const std::vector<CadBody>* bodies, const TriangleMesh* mesh,
|
||
const std::vector<int>* tri_face, const std::vector<int>* tri_body,
|
||
const std::vector<bool>* visible,
|
||
const std::vector<Transform3d>* xform)
|
||
{
|
||
// Treat no bodies or an empty mesh as "no solid" so has_display()/picking stay off.
|
||
if (bodies == nullptr || bodies->empty() || mesh == nullptr || mesh->its.indices.empty()) {
|
||
m_solid_bodies = nullptr; m_solid_mesh = nullptr; m_solid_tri_face = nullptr; m_solid_tri_body = nullptr;
|
||
m_solid_visible = nullptr; m_solid_xform = nullptr;
|
||
} else {
|
||
m_solid_bodies = bodies; m_solid_mesh = mesh; m_solid_tri_face = tri_face; m_solid_tri_body = tri_body;
|
||
m_solid_visible = visible; m_solid_xform = xform;
|
||
}
|
||
clear_solid_selection();
|
||
}
|
||
|
||
// Map a point sampled from the (untransformed) OCCT body shape through the body's display
|
||
// transform, so edge picking/highlight track a moved body. The pick MESH is already
|
||
// transformed by the host; only OCCT-sampled edges need this.
|
||
Vec3d DesignSketchTool::body_xform_pt(int body, const Vec3d& p) const
|
||
{
|
||
if (m_solid_xform != nullptr && body >= 0 && body < int(m_solid_xform->size()))
|
||
return (*m_solid_xform)[body] * p;
|
||
return p;
|
||
}
|
||
|
||
// A body is pickable unless an explicit visibility vector marks it hidden.
|
||
bool DesignSketchTool::body_pickable(int b) const
|
||
{
|
||
if (b < 0) return false;
|
||
// Body-focus mode. The focus is an INDEX held by the panel across recomputes, so it can
|
||
// outlive the body it names — delete a body and the stored index may point past the end.
|
||
// A restriction to a body that no longer exists rejects EVERY body, which is a viewport
|
||
// that silently accepts no clicks at all: the worst possible failure for a picking mode,
|
||
// because nothing on screen says why. Out of range therefore means NO restriction — fail
|
||
// open, never dead.
|
||
const bool focus_live = m_pick_only_body >= 0 && m_solid_bodies != nullptr
|
||
&& m_pick_only_body < int(m_solid_bodies->size());
|
||
if (focus_live && b != m_pick_only_body) return false;
|
||
if (m_solid_visible == nullptr || b >= int(m_solid_visible->size())) return true;
|
||
return (*m_solid_visible)[b];
|
||
}
|
||
|
||
void DesignSketchTool::clear_solid_selection()
|
||
{
|
||
m_solid_sel = SolidSel::None;
|
||
m_sel_body = m_sel_face = m_sel_edge = -1;
|
||
m_sel_edge_pts.clear();
|
||
// The pre-highlight names a face/edge/vertex by index into a shape that a recompute has just
|
||
// rebuilt, so it expires with the selection it was a promise about. Left behind it would keep
|
||
// glowing on whatever now sits at those indices — a real entity, but not the one meant.
|
||
m_pre = SolidPick{};
|
||
}
|
||
|
||
void DesignSketchTool::select_body(int body)
|
||
{
|
||
// Hidden bodies aren't highlighted (the tint overlay would otherwise draw over a
|
||
// body whose GLVolume is off, leaving a ghost after a hide).
|
||
if (m_solid_bodies == nullptr || body < 0 || body >= int(m_solid_bodies->size())
|
||
|| !body_pickable(body)) {
|
||
clear_solid_selection();
|
||
return;
|
||
}
|
||
m_sel_body = body;
|
||
m_sel_face = m_sel_edge = -1;
|
||
m_sel_edge_pts.clear();
|
||
m_solid_sel = SolidSel::Whole; // render_solid_highlight tints just this body
|
||
}
|
||
|
||
// Pick tracing. Selection failures on a real desktop have repeatedly turned out to be an
|
||
// event that never arrived rather than a ray that missed, and the two look identical from
|
||
// the UI. Set SNAPORCA_PICK_TRACE=1 and the whole press->release->ray path narrates itself
|
||
// on stderr. Off by default: no cost, no noise.
|
||
static bool dp_pick_trace_on()
|
||
{
|
||
static const bool on = ::getenv("SNAPORCA_PICK_TRACE") != nullptr;
|
||
return on;
|
||
}
|
||
|
||
static void dp_pick_trace(const char* fmt, ...)
|
||
{
|
||
if (!dp_pick_trace_on()) return;
|
||
va_list ap; va_start(ap, fmt);
|
||
std::fputs("[pick] ", stderr);
|
||
std::vfprintf(stderr, fmt, ap);
|
||
std::fputc('\n', stderr);
|
||
va_end(ap);
|
||
std::fflush(stderr);
|
||
}
|
||
|
||
// Resolve a swept rubber band into a whole-body selection.
|
||
//
|
||
// Sample points are the display mesh's triangle vertices plus each triangle's centroid. That
|
||
// mesh is already in world coordinates — the very points the ray pick tests — so no per-body
|
||
// transform is needed here. A body counts as swept when any of its samples lands inside the
|
||
// rectangle (crossing semantics: touching selects, which is the forgiving reading of a sweep),
|
||
// and the body with the most samples inside wins because the selection callback downstream
|
||
// carries exactly one body.
|
||
//
|
||
// ponytail: crossing over a triangle sample set. A rectangle small enough to sit entirely
|
||
// inside one flat triangle selects nothing — drag a bigger one, or click. Real multi-body
|
||
// selection (and the homogeneous-set rule that goes with it) is snaporca-9xw.
|
||
void DesignSketchTool::pick_bodies_in_rectangle()
|
||
{
|
||
if (m_solid_mesh == nullptr || m_solid_tri_body == nullptr || m_solid_bodies == nullptr)
|
||
return;
|
||
const indexed_triangle_set& its = m_solid_mesh->its;
|
||
std::vector<Vec3d> pts;
|
||
std::vector<int> owner;
|
||
pts.reserve(its.indices.size() * 4);
|
||
owner.reserve(its.indices.size() * 4);
|
||
for (size_t i = 0; i < its.indices.size(); ++i) {
|
||
const int b = (i < m_solid_tri_body->size()) ? (*m_solid_tri_body)[i] : -1;
|
||
if (!body_pickable(b)) continue; // hidden bodies aren't swept either
|
||
const auto& idx = its.indices[i];
|
||
Vec3d c = Vec3d::Zero();
|
||
for (int k = 0; k < 3; ++k) {
|
||
const Vec3d p = its.vertices[idx(k)].cast<double>();
|
||
pts.push_back(p); owner.push_back(b); c += p;
|
||
}
|
||
pts.push_back(c / 3.0); owner.push_back(b);
|
||
}
|
||
|
||
std::vector<int> hits(m_solid_bodies->size(), 0);
|
||
if (!pts.empty())
|
||
for (unsigned int i : m_rubber.contains(pts))
|
||
if (i < owner.size() && owner[i] >= 0 && owner[i] < int(hits.size()))
|
||
++hits[owner[i]];
|
||
|
||
int best = -1, best_n = 0;
|
||
for (int b = 0; b < int(hits.size()); ++b)
|
||
if (hits[b] > best_n) { best_n = hits[b]; best = b; }
|
||
|
||
if (best < 0) clear_solid_selection(); // swept empty space -> drop the selection
|
||
else select_body(best);
|
||
dp_pick_trace("rubber band -> body=%d (%d samples)", best, best_n);
|
||
if (on_solid_selection_changed)
|
||
on_solid_selection_changed(int(m_solid_sel), m_sel_body, m_sel_face, m_sel_edge);
|
||
}
|
||
|
||
// Resolve what a pick at (mx,my) would take. CONST, and it writes only into `out`: the hover
|
||
// path calls this many times a second and must not disturb the committed selection by doing so.
|
||
bool DesignSketchTool::resolve_solid_pick(GLCanvas3D& canvas, int mx, int my, SolidPick& out) const
|
||
{
|
||
out = SolidPick{};
|
||
if (m_solid_bodies == nullptr || m_solid_mesh == nullptr) {
|
||
dp_pick_trace("no solid data (bodies=%p mesh=%p)",
|
||
(const void*) m_solid_bodies, (const void*) m_solid_mesh);
|
||
return false;
|
||
}
|
||
const Linef3 r = canvas.mouse_ray(Point(mx, my));
|
||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||
|
||
// 1) nearest solid face under the cursor (ray vs display-mesh triangles). Resolve WHICH
|
||
// body and which face-within-that-body via the per-triangle (tri_body, tri_face) tags.
|
||
const indexed_triangle_set& its = m_solid_mesh->its;
|
||
int best_face = -1, best_body = -1; double best_t = 1e30;
|
||
for (size_t i = 0; i < its.indices.size(); ++i) {
|
||
const auto& idx = its.indices[i];
|
||
const Vec3d v0 = its.vertices[idx(0)].cast<double>();
|
||
const Vec3d v1 = its.vertices[idx(1)].cast<double>();
|
||
const Vec3d v2 = its.vertices[idx(2)].cast<double>();
|
||
double t;
|
||
if (ray_triangle(ro, rd, v0, v1, v2, t) && t < best_t) {
|
||
const int cand_body = (m_solid_tri_body && i < m_solid_tri_body->size()) ? (*m_solid_tri_body)[i] : -1;
|
||
if (!body_pickable(cand_body)) continue; // hidden bodies don't catch clicks
|
||
best_t = t;
|
||
best_face = (m_solid_tri_face && i < m_solid_tri_face->size()) ? (*m_solid_tri_face)[i] : -1;
|
||
best_body = cand_body;
|
||
}
|
||
}
|
||
dp_pick_trace("ray tris=%zu -> body=%d face=%d t=%.3f", its.indices.size(),
|
||
best_body, best_face, best_t);
|
||
if (best_face < 0 || best_body < 0 || best_body >= int(m_solid_bodies->size()))
|
||
return false; // missed the solid
|
||
|
||
// ---- one click, one deterministic result ---------------------------------------------
|
||
// NO CYCLE. A click selects the SMALLEST thing under the cursor: the edge if the pointer is
|
||
// within tolerance of one, otherwise the face. The WHOLE body is taken by a left-drag
|
||
// rubber band (pick_bodies_in_rectangle) — a different gesture for a different scale.
|
||
//
|
||
// What this replaces: click 1 = whole body, click 2 = face, click 3 = nearest edge, click 4
|
||
// = back to whole. That made "click a face" a two-click gesture and "click an edge" a
|
||
// three-click one, neither discoverable — the L5 violation §10 of the charter already
|
||
// listed, and the real reason sketching on a face kept reading as broken however often the
|
||
// plane resolution was fixed. Selection is the foundation the tool offer stands on: the
|
||
// offer can only ever be as truthful as the selection beneath it.
|
||
//
|
||
// Tolerance is measured in SCREEN PIXELS. The old edge step compared a ray-to-segment
|
||
// distance in millimetres, so the same gesture meant different things at different zooms —
|
||
// the pointer is a screen object and its tolerance has to be one too.
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const wxPoint cursor(mx, my);
|
||
// Vertex beats edge beats face, and the vertex tolerance is the larger of the two: a corner
|
||
// sits ON its edges, so an equal radius would make vertices unreachable — every click near
|
||
// one would resolve to the edge it lies on.
|
||
const double kVertexTolPx = 11.0;
|
||
const double kEdgeTolPx = 8.0;
|
||
|
||
auto seg_px = [](const wxPoint& p, const wxPoint& a, const wxPoint& b) { // 2D point→segment, px
|
||
const double vx = b.x - a.x, vy = b.y - a.y;
|
||
const double wx = p.x - a.x, wy = p.y - a.y;
|
||
const double L2 = vx * vx + vy * vy;
|
||
double t = (L2 > 1e-12) ? (wx * vx + wy * vy) / L2 : 0.0;
|
||
t = std::max(0.0, std::min(1.0, t));
|
||
return std::hypot(wx - t * vx, wy - t * vy);
|
||
};
|
||
|
||
out.body = best_body;
|
||
out.face = best_face;
|
||
|
||
{
|
||
const TopoDS_Shape& bshape = (*m_solid_bodies)[out.body].shape;
|
||
const TopoDS_Face face = GeometryEngine::face_by_index(bshape, out.face);
|
||
double best_ed = 1e30; std::vector<Vec3d> ed_pts; TopoDS_Edge ed_edge; bool have_edge = false;
|
||
double best_vd = 1e30; Vec3d vtx = Vec3d::Zero(); bool have_vtx = false;
|
||
// Screen extent of the face, accumulated from the same edge samples the loop already
|
||
// takes. A FIXED edge tolerance makes a narrow face unreachable: a 3 mm-wide plate is
|
||
// barely wider on screen than the 8 px budget, so every point on it is "on an edge" and
|
||
// the face level can never be picked — which also blocks the face-based Coord Sys that a
|
||
// mate needs. The tolerances below shrink with the face so its middle stays its own.
|
||
int fx0 = INT_MAX, fy0 = INT_MAX, fx1 = INT_MIN, fy1 = INT_MIN;
|
||
if (!face.IsNull()) {
|
||
for (const TopoDS_Edge& e : GeometryEngine::edges_of_face(face)) {
|
||
std::vector<Vec3d> pts = GeometryEngine::sample_edge_world(e);
|
||
for (Vec3d& q : pts) q = body_xform_pt(out.body, q); // follow a moved body
|
||
if (pts.size() < 2) continue;
|
||
// The polyline ends ARE the edge's vertices; every corner of the face is the
|
||
// end of one of its edges, so this covers them without a separate topology walk.
|
||
for (const Vec3d& v : {pts.front(), pts.back()}) {
|
||
const wxPoint sp = world_to_screen_px(cam, v);
|
||
if (sp.x < 0) continue;
|
||
const double d = std::hypot(double(sp.x - cursor.x), double(sp.y - cursor.y));
|
||
if (d < best_vd) { best_vd = d; vtx = v; have_vtx = true; }
|
||
}
|
||
double d = 1e30;
|
||
for (size_t s = 1; s < pts.size(); ++s) {
|
||
const wxPoint a = world_to_screen_px(cam, pts[s - 1]);
|
||
const wxPoint b = world_to_screen_px(cam, pts[s]);
|
||
if (a.x < 0 || b.x < 0) continue; // behind the camera
|
||
fx0 = std::min({fx0, a.x, b.x}); fx1 = std::max({fx1, a.x, b.x});
|
||
fy0 = std::min({fy0, a.y, b.y}); fy1 = std::max({fy1, a.y, b.y});
|
||
d = std::min(d, seg_px(cursor, a, b));
|
||
}
|
||
if (d < best_ed) { best_ed = d; ed_pts = pts; ed_edge = e; have_edge = true; }
|
||
}
|
||
}
|
||
// A third of the face's SHORTER on-screen side, so the two tolerances can never meet in
|
||
// the middle. Only ever shrinks: a face big enough keeps the full budget.
|
||
double vtol = kVertexTolPx, etol = kEdgeTolPx;
|
||
if (fx1 > fx0 && fy1 > fy0) {
|
||
const double narrow = double(std::min(fx1 - fx0, fy1 - fy0)) / 3.0;
|
||
vtol = std::min(vtol, narrow);
|
||
etol = std::min(etol, narrow);
|
||
}
|
||
if (have_vtx && best_vd <= vtol) {
|
||
out.vertex_pt = vtx;
|
||
out.kind = SolidSel::Vertex;
|
||
} else if (have_edge && best_ed <= etol) {
|
||
// Promote the face-relative pick to a STABLE GLOBAL edge id so dress-up ops
|
||
// (fillet/chamfer) can target this exact edge across recomputes.
|
||
out.edge = GeometryEngine::edge_index_of(bshape, ed_edge);
|
||
out.edge_pts = std::move(ed_pts);
|
||
out.kind = SolidSel::Edge;
|
||
} else {
|
||
out.kind = SolidSel::Face;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// A click on the solid takes the smallest thing under the cursor (vertex/edge/face). Returns
|
||
// true if the click hit the solid (consumed); false otherwise so the caller can try
|
||
// committed-sketch loop picking. Whole bodies are taken by the rubber band, not by clicking.
|
||
bool DesignSketchTool::handle_solid_click(GLCanvas3D& canvas, const wxMouseEvent& evt)
|
||
{
|
||
SolidPick p;
|
||
if (!resolve_solid_pick(canvas, evt.GetX(), evt.GetY(), p))
|
||
return false; // missed the solid, or no solid data — same two exits as before
|
||
|
||
// What was selected BEFORE this pick — the escalation below is the only thing that reads
|
||
// it, and everything from here on overwrites it.
|
||
const SolidSel prev_kind = m_solid_sel;
|
||
const int prev_body = m_sel_body, prev_face = m_sel_face, prev_edge = m_sel_edge;
|
||
const Vec3d prev_vtx = m_sel_vertex_pt;
|
||
|
||
m_sel_body = p.body;
|
||
m_sel_face = p.face;
|
||
m_sel_edge = p.edge;
|
||
m_sel_edge_pts = std::move(p.edge_pts);
|
||
m_sel_vertex_pt = p.vertex_pt;
|
||
m_solid_sel = p.kind;
|
||
|
||
// CLICK AGAIN ON THE SAME THING -> THE WHOLE BODY (snaporca-gem). Pointing at a face and
|
||
// pointing at its body are different intents, and until now only the rubber band could
|
||
// express the second one — so the status line said "face 0 selected" while the user
|
||
// believed they had taken the body, and every body verb had to opt into the face kinds to
|
||
// stay reachable. One more click on the SAME sub-element escalates.
|
||
//
|
||
// This is not the pick cycle that was removed (bc2b741ce9). That one was silent and three
|
||
// deep, so no click had a predictable meaning. Here the escalation is announced by the
|
||
// status line BEFORE you make the click, and a further click just takes the face under the
|
||
// cursor again — the ordinary meaning of clicking a face, which needs no teaching.
|
||
//
|
||
// Double-click is safe: wx sends Down/Up/DClick/Up, and only the first Up carries a
|
||
// pending press, so a fast double-click zooms to fit and picks ONCE. Escalation needs two
|
||
// separate clicks, the same "click, pause, click" distinction a file manager uses.
|
||
//
|
||
// "The same thing" is compared AT THE LEVEL THAT WAS PICKED, and nothing else. Requiring
|
||
// every field to match looked stricter and was simply wrong: an edge pick leaves m_sel_face
|
||
// set to whichever face the ray happened to hit, and a shared edge is reached through a
|
||
// different face depending on which side of the body you are looking from. So picking an
|
||
// edge, orbiting, and clicking that same edge from the other side left m_sel_edge equal and
|
||
// m_sel_face different, and the escalation the status line had just promised did not happen.
|
||
// The edge id here is already the STABLE GLOBAL one (edge_index_of, a few lines up) — it
|
||
// identifies the edge on its own and does not need the face to disambiguate it.
|
||
const bool same_pick = m_solid_sel == prev_kind && m_sel_body == prev_body
|
||
&& (m_solid_sel == SolidSel::Vertex ? (m_sel_vertex_pt - prev_vtx).norm() < 1e-9
|
||
: m_solid_sel == SolidSel::Edge ? m_sel_edge == prev_edge
|
||
: m_solid_sel == SolidSel::Face ? m_sel_face == prev_face
|
||
: true);
|
||
if (same_pick && m_escalate_repick) {
|
||
select_body(m_sel_body); // clears face/edge/vertex, tints the whole body
|
||
dp_pick_trace("re-pick -> escalated to whole body %d", m_sel_body);
|
||
}
|
||
dp_pick_trace("pick -> sel=%d body=%d face=%d edge=%d",
|
||
int(m_solid_sel), m_sel_body, m_sel_face, m_sel_edge);
|
||
if (on_solid_selection_changed)
|
||
on_solid_selection_changed(int(m_solid_sel), m_sel_body, m_sel_face, m_sel_edge);
|
||
return true;
|
||
}
|
||
|
||
// Recompute what the pointer is over. Returns true only when the answer CHANGED — this runs on
|
||
// every motion event, and a repaint per event would cost far more than the pick itself.
|
||
bool DesignSketchTool::update_solid_hover(GLCanvas3D& canvas, const wxMouseEvent& evt)
|
||
{
|
||
SolidPick p;
|
||
if (!resolve_solid_pick(canvas, evt.GetX(), evt.GetY(), p))
|
||
p = SolidPick{}; // off the solid: no promise to make
|
||
// Compared AT THE LEVEL THAT WAS PICKED, for the same reason the click's escalation is
|
||
// (see same_pick above): an edge pick carries whichever face the ray happened to cross, and
|
||
// that face changes as the pointer slides along the edge without the answer changing at all.
|
||
const bool same = p.kind == m_pre.kind && p.body == m_pre.body
|
||
&& (p.kind == SolidSel::Vertex ? (p.vertex_pt - m_pre.vertex_pt).norm() < 1e-9
|
||
: p.kind == SolidSel::Edge ? p.edge == m_pre.edge
|
||
: p.kind == SolidSel::Face ? p.face == m_pre.face
|
||
: true);
|
||
if (same) return false;
|
||
m_pre = std::move(p);
|
||
return true;
|
||
}
|
||
|
||
// One highlight, drawn from explicit arguments rather than from the selection members, so the
|
||
// committed selection and the hover pre-highlight cannot drift apart in how they look. alpha_mul
|
||
// scales every layer at once: the pre-highlight is the same shape in the same place, quieter.
|
||
void DesignSketchTool::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)
|
||
{
|
||
using EPT = GLModel::Geometry::EPrimitiveType;
|
||
using EVL = GLModel::Geometry::EVertexLayout;
|
||
// Opaque: the edge ribbon and the vertex square render with GL_BLEND OFF, so an alpha below 1
|
||
// here would be silently ignored. Those two are quietened by a MUTED rgb from the caller
|
||
// instead; alpha_mul only reaches the face fill, which is the one layer that is blended.
|
||
const ColorRGBA cyan(rgb.r(), rgb.g(), rgb.b(), 1.0f);
|
||
|
||
// Whole tints the picked BODY (all its triangles, lighter alpha); Face tints just the
|
||
// picked face on that body. Both filter by `body` so other bodies stay untinted.
|
||
if ((kind == SolidSel::Face || kind == SolidSel::Whole)
|
||
&& m_solid_mesh != nullptr && m_solid_tri_body != nullptr && body >= 0) {
|
||
const bool face_only = (kind == SolidSel::Face);
|
||
const indexed_triangle_set& its = m_solid_mesh->its;
|
||
GLModel::Geometry g; g.format = { EPT::Triangles, EVL::P3 };
|
||
unsigned int base = 0;
|
||
for (size_t i = 0; i < its.indices.size(); ++i) {
|
||
if (i >= m_solid_tri_body->size() || (*m_solid_tri_body)[i] != body) continue;
|
||
if (face_only && (m_solid_tri_face == nullptr || i >= m_solid_tri_face->size()
|
||
|| (*m_solid_tri_face)[i] != face)) continue;
|
||
const auto& idx = its.indices[i];
|
||
for (int j = 0; j < 3; ++j) g.add_vertex(its.vertices[idx(j)]);
|
||
g.add_triangle(base, base + 1, base + 2); base += 3;
|
||
}
|
||
if (base > 0) {
|
||
glsafe(::glEnable(GL_DEPTH_TEST));
|
||
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
|
||
glsafe(::glPolygonOffset(-2.0f, -2.0f));
|
||
glsafe(::glEnable(GL_BLEND));
|
||
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
|
||
m_solid_face_model.reset();
|
||
m_solid_face_model.init_from(std::move(g));
|
||
m_solid_face_model.set_color(ColorRGBA(rgb.r(), rgb.g(), rgb.b(),
|
||
(face_only ? 0.40f : 0.22f) * alpha_mul));
|
||
m_solid_face_model.render();
|
||
glsafe(::glDisable(GL_BLEND));
|
||
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
}
|
||
} else if (kind == SolidSel::Edge && edge_pts.size() >= 2) {
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const Vec3d vd = cam.get_dir_forward();
|
||
const double hw = 2.0 / std::max(cam.get_zoom(), 1e-6); // ~2 px ribbon half-width
|
||
GLModel::Geometry g; g.format = { EPT::Triangles, EVL::P3 };
|
||
unsigned int base = 0;
|
||
for (size_t s = 1; s < edge_pts.size(); ++s) {
|
||
const Vec3d a = edge_pts[s - 1], b = edge_pts[s];
|
||
Vec3d dir = b - a; if (dir.norm() < 1e-9) continue; dir.normalize();
|
||
Vec3d off = dir.cross(vd);
|
||
if (off.norm() < 1e-9) off = dir.cross(cam.get_dir_up());
|
||
if (off.norm() < 1e-9) continue;
|
||
off.normalize(); off *= hw;
|
||
g.add_vertex((Vec3f)(a + off).cast<float>());
|
||
g.add_vertex((Vec3f)(b + off).cast<float>());
|
||
g.add_vertex((Vec3f)(b - off).cast<float>());
|
||
g.add_vertex((Vec3f)(a - off).cast<float>());
|
||
g.add_triangle(base, base + 1, base + 2);
|
||
g.add_triangle(base, base + 2, base + 3); base += 4;
|
||
}
|
||
if (base > 0) {
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
m_solid_edge_model.reset();
|
||
m_solid_edge_model.init_from(std::move(g));
|
||
m_solid_edge_model.set_color(cyan);
|
||
m_solid_edge_model.render();
|
||
}
|
||
} else if (kind == SolidSel::Vertex) {
|
||
// A camera-facing square at the picked corner, sized in screen terms (the same
|
||
// 1/zoom trick the edge ribbon uses) so it stays a constant dot at every zoom. A
|
||
// selection you cannot see is not a selection (L5), which is why vertex picking waited
|
||
// for this rather than shipping without a highlight.
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
Vec3d right = cam.get_dir_right(), up = cam.get_dir_up();
|
||
const double h = 4.5 / std::max(cam.get_zoom(), 1e-6); // ~4.5 px half-size
|
||
right *= h; up *= h;
|
||
const Vec3d c = vertex_pt;
|
||
GLModel::Geometry g; g.format = { EPT::Triangles, EVL::P3 };
|
||
g.add_vertex((Vec3f)(c - right - up).cast<float>());
|
||
g.add_vertex((Vec3f)(c + right - up).cast<float>());
|
||
g.add_vertex((Vec3f)(c + right + up).cast<float>());
|
||
g.add_vertex((Vec3f)(c - right + up).cast<float>());
|
||
g.add_triangle(0, 1, 2);
|
||
g.add_triangle(0, 2, 3);
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
m_solid_vertex_model.reset();
|
||
m_solid_vertex_model.init_from(std::move(g));
|
||
m_solid_vertex_model.set_color(cyan);
|
||
m_solid_vertex_model.render();
|
||
}
|
||
}
|
||
|
||
// Cyan overlay for the picked face / edge / vertex, plus the quieter pre-highlight of whatever
|
||
// the pointer is currently over. Whole-solid tint is the panel's job (set_body_highlight).
|
||
// Called from render() while no sketch session is active.
|
||
void DesignSketchTool::render_solid_highlight()
|
||
{
|
||
const ColorRGBA sel_cyan = design_selection_color();
|
||
|
||
// The pre-highlight goes FIRST so the committed selection paints over it where the two
|
||
// overlap — what you HAVE outranks what you would get. Suppressed entirely when they are the
|
||
// same thing: two coats of the same colour on the same face reads as a rendering fault, and
|
||
// a promise about a click that would change nothing is not worth making.
|
||
const bool pre_is_sel = m_pre.kind == m_solid_sel && m_pre.body == m_sel_body
|
||
&& (m_pre.kind == SolidSel::Vertex ? (m_pre.vertex_pt - m_sel_vertex_pt).norm() < 1e-9
|
||
: m_pre.kind == SolidSel::Edge ? m_pre.edge == m_sel_edge
|
||
: m_pre.kind == SolidSel::Face ? m_pre.face == m_sel_face
|
||
: true);
|
||
if (m_pre.kind != SolidSel::None && !pre_is_sel)
|
||
// Desaturated toward white rather than a second hue: a distinct colour would read as a
|
||
// distinct KIND of selection, when it is the same selection one moment earlier.
|
||
render_solid_sel(m_pre.kind, m_pre.body, m_pre.face, m_pre.edge_pts, m_pre.vertex_pt,
|
||
design_selection_color(), 0.45f); // hover = the same colour, quieter
|
||
|
||
render_solid_sel(m_solid_sel, m_sel_body, m_sel_face, m_sel_edge_pts, m_sel_vertex_pt,
|
||
sel_cyan, 1.0f);
|
||
}
|
||
|
||
// Datum/reference planes (Plane feature) have no solid; draw each as a translucent indigo
|
||
// rectangle + border so it is visible in the viewport (Onshape-style finite plane). World
|
||
// space, depth-test off so it reads over the bed; indigo to stay distinct from the cyan
|
||
// solid-selection tint, orange sketches and amber feature ghosts.
|
||
void DesignSketchTool::render_view_helpers()
|
||
{
|
||
if (!m_show_planes && !m_show_axes) return;
|
||
using EPT = GLModel::Geometry::EPrimitiveType;
|
||
using EVL = GLModel::Geometry::EVertexLayout;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const Vec3d vd = cam.get_dir_forward();
|
||
const double hw = 1.5 / std::max(cam.get_zoom(), 1e-6); // billboard ribbon half-width (px)
|
||
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
glsafe(::glEnable(GL_BLEND));
|
||
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
|
||
|
||
if (m_show_planes) {
|
||
const double H = 40.0; // half-extent (mm)
|
||
SketchPlane pl[3]; // XY / XZ / YZ through the world origin
|
||
pl[0].origin = Vec3d(0,0,0); pl[0].x_axis = Vec3d(1,0,0); pl[0].y_axis = Vec3d(0,1,0); pl[0].normal = Vec3d(0,0,1);
|
||
pl[1].origin = Vec3d(0,0,0); pl[1].x_axis = Vec3d(1,0,0); pl[1].y_axis = Vec3d(0,0,1); pl[1].normal = Vec3d(0,1,0);
|
||
pl[2].origin = Vec3d(0,0,0); pl[2].x_axis = Vec3d(0,1,0); pl[2].y_axis = Vec3d(0,0,1); pl[2].normal = Vec3d(1,0,0);
|
||
GLModel::Geometry fill; fill.format = { EPT::Triangles, EVL::P3 };
|
||
unsigned int fb = 0;
|
||
for (const SketchPlane& p : pl) {
|
||
const Vec3d c[4] = { p.to_world(Vec2d(-H,-H)), p.to_world(Vec2d(H,-H)),
|
||
p.to_world(Vec2d(H,H)), p.to_world(Vec2d(-H,H)) };
|
||
fill.add_vertex((Vec3f)c[0].cast<float>()); fill.add_vertex((Vec3f)c[1].cast<float>());
|
||
fill.add_vertex((Vec3f)c[2].cast<float>()); fill.add_triangle(fb, fb+1, fb+2); fb += 3;
|
||
fill.add_vertex((Vec3f)c[0].cast<float>()); fill.add_vertex((Vec3f)c[2].cast<float>());
|
||
fill.add_vertex((Vec3f)c[3].cast<float>()); fill.add_triangle(fb, fb+1, fb+2); fb += 3;
|
||
}
|
||
if (fb > 0) { GLModel fm; fm.init_from(std::move(fill));
|
||
fm.set_color(ColorRGBA(0.42f, 0.52f, 0.78f, 0.20f)); fm.render(); }
|
||
}
|
||
|
||
if (m_show_axes) {
|
||
const double L = 60.0; // axis length (mm)
|
||
struct Ax { Vec3d dir; ColorRGBA col; };
|
||
const Ax axes[3] = { { Vec3d(1,0,0), ColorRGBA(0.92f, 0.28f, 0.28f, 0.9f) },
|
||
{ Vec3d(0,1,0), ColorRGBA(0.30f, 0.80f, 0.34f, 0.9f) },
|
||
{ Vec3d(0,0,1), ColorRGBA(0.32f, 0.55f, 0.95f, 0.9f) } };
|
||
for (const Ax& ax : axes) {
|
||
// Lines don't rasterise under the reused software GL context, so each axis is a
|
||
// thin view-facing ribbon (two triangles), like the datum-plane border.
|
||
const Vec3d a = Vec3d(0,0,0), b = ax.dir * L;
|
||
Vec3d dir = (b - a).normalized();
|
||
Vec3d off = dir.cross(vd);
|
||
if (off.norm() < 1e-9) off = dir.cross(cam.get_dir_up());
|
||
if (off.norm() < 1e-9) continue;
|
||
off.normalize(); off *= hw * 1.5;
|
||
GLModel::Geometry g; g.format = { EPT::Triangles, EVL::P3 };
|
||
g.add_vertex((Vec3f)(a + off).cast<float>()); g.add_vertex((Vec3f)(b + off).cast<float>());
|
||
g.add_vertex((Vec3f)(b - off).cast<float>()); g.add_vertex((Vec3f)(a - off).cast<float>());
|
||
g.add_triangle(0, 1, 2); g.add_triangle(0, 2, 3);
|
||
GLModel m; m.init_from(std::move(g)); m.set_color(ax.col); m.render();
|
||
}
|
||
}
|
||
glsafe(::glDisable(GL_BLEND));
|
||
}
|
||
|
||
void DesignSketchTool::render_datum_planes()
|
||
{
|
||
if (m_datum_planes.empty()) return;
|
||
using EPT = GLModel::Geometry::EPrimitiveType;
|
||
using EVL = GLModel::Geometry::EVertexLayout;
|
||
const double H = 40.0; // default half-extent when no per-plane size is given (mm)
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const Vec3d vd = cam.get_dir_forward();
|
||
const double hw = 1.5 / std::max(cam.get_zoom(), 1e-6); // ~1.5 px border ribbon
|
||
|
||
GLModel::Geometry fill; fill.format = { EPT::Triangles, EVL::P3 };
|
||
GLModel::Geometry border; border.format = { EPT::Triangles, EVL::P3 };
|
||
unsigned int fb = 0, bb = 0;
|
||
for (size_t pi = 0; pi < m_datum_planes.size(); ++pi) {
|
||
const SketchPlane& p = m_datum_planes[pi];
|
||
// Per-plane u/v half-extent (the GUI Size U/V + drag handles drive these); fall
|
||
// back to the square default when no size was supplied.
|
||
const double hu = (pi < m_datum_sizes.size() && m_datum_sizes[pi].x() > 1e-6)
|
||
? m_datum_sizes[pi].x() * 0.5 : H;
|
||
const double hv = (pi < m_datum_sizes.size() && m_datum_sizes[pi].y() > 1e-6)
|
||
? m_datum_sizes[pi].y() * 0.5 : H;
|
||
const Vec3d c[4] = { p.to_world(Vec2d(-hu, -hv)), p.to_world(Vec2d(hu, -hv)),
|
||
p.to_world(Vec2d(hu, hv)), p.to_world(Vec2d(-hu, hv)) };
|
||
fill.add_vertex((Vec3f)c[0].cast<float>()); fill.add_vertex((Vec3f)c[1].cast<float>());
|
||
fill.add_vertex((Vec3f)c[2].cast<float>()); fill.add_triangle(fb, fb + 1, fb + 2); fb += 3;
|
||
fill.add_vertex((Vec3f)c[0].cast<float>()); fill.add_vertex((Vec3f)c[2].cast<float>());
|
||
fill.add_vertex((Vec3f)c[3].cast<float>()); fill.add_triangle(fb, fb + 1, fb + 2); fb += 3;
|
||
for (int s = 0; s < 4; ++s) {
|
||
const Vec3d a = c[s], b = c[(s + 1) & 3];
|
||
Vec3d dir = b - a; if (dir.norm() < 1e-9) continue; dir.normalize();
|
||
Vec3d off = dir.cross(vd);
|
||
if (off.norm() < 1e-9) off = dir.cross(cam.get_dir_up());
|
||
if (off.norm() < 1e-9) continue;
|
||
off.normalize(); off *= hw;
|
||
border.add_vertex((Vec3f)(a + off).cast<float>());
|
||
border.add_vertex((Vec3f)(b + off).cast<float>());
|
||
border.add_vertex((Vec3f)(b - off).cast<float>());
|
||
border.add_vertex((Vec3f)(a - off).cast<float>());
|
||
border.add_triangle(bb, bb + 1, bb + 2);
|
||
border.add_triangle(bb, bb + 2, bb + 3); bb += 4;
|
||
}
|
||
}
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
glsafe(::glEnable(GL_BLEND));
|
||
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
|
||
if (fb > 0) {
|
||
GLModel fm; fm.init_from(std::move(fill));
|
||
fm.set_color(ColorRGBA(0.62f, 0.52f, 0.95f, 0.10f));
|
||
fm.render();
|
||
}
|
||
if (bb > 0) {
|
||
GLModel bm; bm.init_from(std::move(border));
|
||
bm.set_color(ColorRGBA(0.70f, 0.60f, 1.0f, 0.85f));
|
||
bm.render();
|
||
}
|
||
glsafe(::glDisable(GL_BLEND));
|
||
}
|
||
|
||
// ---- Visual Extrude gizmo (C5b) -------------------------------------------------------
|
||
void DesignSketchTool::set_extrude_gizmo(const SketchPlane& plane, const Vec2d& centroid,
|
||
double depth, double depth2, bool two_sided, bool flip)
|
||
{
|
||
m_ex_active = true;
|
||
m_ex_plane = plane;
|
||
m_ex_centroid = centroid;
|
||
m_ex_depth = std::max(0.0, depth);
|
||
m_ex_depth2 = std::max(0.0, depth2);
|
||
m_ex_two_sided = two_sided;
|
||
m_ex_flip = flip;
|
||
}
|
||
|
||
void DesignSketchTool::clear_extrude_gizmo()
|
||
{
|
||
m_ex_active = false;
|
||
m_ex_drag = -1;
|
||
}
|
||
|
||
// Camera-billboarded depth arrow(s) along the profile normal, drawn in WORLD via a billboard
|
||
// SketchPlane at the centroid (draw_strokes/draw_text lift 2D coords through m_plane.to_world,
|
||
// so swapping m_plane to a screen-facing frame renders a flat, screen-aligned arrow + label).
|
||
// The mate-connector glyph. Three elements, each answering one question, and every dimension is in
|
||
// SCREEN PIXELS via upp (= 1/zoom) like every other gizmo here — a connector is a symbol, not a part,
|
||
// so it must not shrink with the model.
|
||
//
|
||
// disc the XY plane "I am a frame, and this is the plane I sit in"
|
||
// quadrant the +x/+y sector "this is where X is" -- the roll, otherwise invisible
|
||
// Z arrow +Z only, never -Z "this is the way I point" -- the VERSE
|
||
//
|
||
// POLARITY (which one is anchored, which one is about to move) is carried by the head: a filled
|
||
// cone travels, an open collar receives. No surveyed CAD system encodes this at all; both ends of
|
||
// their mates are drawn identically, which is why "which part moves?" is a standing complaint.
|
||
//
|
||
// SNAPORCA_GLYPH=A|B selects the treatment while this is being judged on the rig:
|
||
// A three short axis arms, no head differentiation (the Onshape baseline)
|
||
// B one-sided Z arrow, filled vs open head (the proposal) -- default
|
||
void DesignSketchTool::render_mate_connectors()
|
||
{
|
||
if (m_mate_connectors.empty()) return;
|
||
static const bool style_A = [] {
|
||
const char* s = ::getenv("SNAPORCA_GLYPH");
|
||
return s && (*s == 'A' || *s == 'a');
|
||
}();
|
||
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const Vec3d right = cam.get_dir_right().normalized();
|
||
const Vec3d up = cam.get_dir_up().normalized();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const double R = 22.0 * upp; // disc radius, ~22 px
|
||
const double lw = std::max(0.9 * upp, 1e-4);
|
||
|
||
const ColorRGBA gold (0.93f, 0.66f, 0.09f, 1.0f);
|
||
const ColorRGBA blue (0.18f, 0.44f, 0.93f, 1.0f);
|
||
const ColorRGBA grey (0.42f, 0.46f, 0.52f, 1.0f);
|
||
// F5: roll-undefined was a loud red — the strongest colour in the viewport spent on the LEAST
|
||
// important connector, which pulled the eye away from the mate being made. It is a "this one
|
||
// could not be derived" mark, not an error: muted amber says look-here without shouting.
|
||
const ColorRGBA warn (0.72f, 0.55f, 0.22f, 1.0f);
|
||
|
||
const SketchPlane saved = m_plane;
|
||
|
||
for (const MateConnectorGlyph& g : m_mate_connectors) {
|
||
const Vec3d X = g.x.normalized();
|
||
const Vec3d Y = g.y.normalized();
|
||
const Vec3d Z = X.cross(Y).normalized();
|
||
const ColorRGBA body = (g.role == 2) ? blue : grey;
|
||
|
||
// ---- disc + quadrant, drawn IN the connector's own plane. This is the part that must not
|
||
// be billboarded: a disc that always faces the camera cannot show the frame's orientation,
|
||
// which is the only reason it is a disc and not a dot.
|
||
// Lifted off the surface by a sub-pixel epsilon. A connector's disc is EXACTLY coplanar with
|
||
// the face it sits on, so depth-testing it z-fights: on the rig the disc came out as a broken
|
||
// dotted arc that flickered with the camera. Depth off floats it through solids, depth on and
|
||
// coplanar tears it — the lift is what buys both. Scaled by upp so it stays sub-pixel at any
|
||
// zoom instead of becoming a visible gap when you zoom in.
|
||
SketchPlane cp; cp.origin = g.origin + Z * (0.7 * upp);
|
||
cp.x_axis = X; cp.y_axis = Y; cp.normal = Z;
|
||
m_plane = cp;
|
||
{
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
const int N = 40;
|
||
for (int i = 0; i < N; ++i) {
|
||
const double a0 = (2.0 * M_PI * i) / N, a1 = (2.0 * M_PI * (i + 1)) / N;
|
||
segs.emplace_back(Vec2d(R * std::cos(a0), R * std::sin(a0)),
|
||
Vec2d(R * std::cos(a1), R * std::sin(a1)));
|
||
}
|
||
// Depth test ON for the disc, deliberately. With it off, a connector on a face pointing
|
||
// AWAY from the camera still drew its disc over the solid, so the part looked covered in
|
||
// frames that were really on its back — and a disc floating over an edge read as
|
||
// detached rather than planted. render_hole_gizmo learned the same thing for its cube.
|
||
glsafe(::glEnable(GL_DEPTH_TEST));
|
||
draw_strokes(m_mc_stroke_model, segs, lw, g.roll_undefined ? warn : body);
|
||
|
||
// The quadrant: hatched when the roll could not be derived (a full circular face or a
|
||
// seam offers no in-plane direction), so the fallback is a mark you cannot miss rather
|
||
// than a silent guess.
|
||
std::vector<std::pair<Vec2d, Vec2d>> q;
|
||
const double qr = R * 0.84;
|
||
const int QN = 10;
|
||
for (int i = 0; i < QN; ++i) {
|
||
const double a0 = (M_PI_2 * i) / QN, a1 = (M_PI_2 * (i + 1)) / QN;
|
||
q.emplace_back(Vec2d(qr * std::cos(a0), qr * std::sin(a0)),
|
||
Vec2d(qr * std::cos(a1), qr * std::sin(a1)));
|
||
}
|
||
q.emplace_back(Vec2d(0, 0), Vec2d(qr, 0));
|
||
q.emplace_back(Vec2d(0, 0), Vec2d(0, qr));
|
||
if (!g.roll_undefined) {
|
||
for (int i = 1; i < 5; ++i) { // fan lines read as "filled" at any angle
|
||
const double a = (M_PI_2 * i) / 5.0;
|
||
q.emplace_back(Vec2d(0, 0), Vec2d(qr * std::cos(a), qr * std::sin(a)));
|
||
}
|
||
// F4: the quadrant collapses to a blob at grazing angles — exactly when the roll
|
||
// is hardest to read. A radial tick along +X, extending PAST the disc rim, is what
|
||
// survives that: as the disc flattens to a line the sector loses all area, but a
|
||
// radial spoke keeps its length and its direction along the one axis that still
|
||
// projects.
|
||
//
|
||
// The alternative on the issue was billboarding the quadrant. Rejected, and not
|
||
// on taste: at true grazing the view direction lies IN the connector's plane, so
|
||
// every in-plane direction projects onto the same screen line and the roll is
|
||
// geometrically unrecoverable. A billboarded quadrant would not recover it — it
|
||
// would face the camera and read as a definite orientation that is not the frame's.
|
||
// Better to degrade to a direction you can still trust than to draw a confident
|
||
// lie. Judge the tick on the rig at a true grazing view before calling F4 closed.
|
||
q.emplace_back(Vec2d(R, 0), Vec2d(R * 1.28, 0));
|
||
}
|
||
draw_strokes(m_mc_stroke_model, q, lw, g.roll_undefined ? warn : gold);
|
||
}
|
||
|
||
// ---- the axes. Billboarded at the origin: a 3D direction is projected onto the screen
|
||
// frame, which is the only way an arrow keeps a readable head at any viewing angle.
|
||
SketchPlane bb; bb.origin = g.origin; bb.x_axis = right; bb.y_axis = up;
|
||
bb.normal = cam.get_dir_forward().normalized();
|
||
m_plane = bb;
|
||
|
||
auto arrow = [&](const Vec3d& dir, double len, const ColorRGBA& col, bool filled_head) {
|
||
const Vec3d tipw = g.origin + dir * len;
|
||
const Vec2d tip2((tipw - g.origin).dot(right), (tipw - g.origin).dot(up));
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
// Foreshortening: when the axis points at (or away from) the camera it projects to
|
||
// nothing and an arrow degenerates into a dot. Draw a ring instead — "pointing at you"
|
||
// — rather than silently vanishing, which is what a naive projection does.
|
||
if (tip2.norm() < R * 0.28) {
|
||
const int N = 24;
|
||
const double rr = R * 0.30;
|
||
for (int i = 0; i < N; ++i) {
|
||
const double a0 = (2.0 * M_PI * i) / N, a1 = (2.0 * M_PI * (i + 1)) / N;
|
||
segs.emplace_back(Vec2d(rr * std::cos(a0), rr * std::sin(a0)),
|
||
Vec2d(rr * std::cos(a1), rr * std::sin(a1)));
|
||
}
|
||
draw_strokes(m_mc_stroke_model, segs, lw, col);
|
||
return;
|
||
}
|
||
const Vec2d u = tip2.normalized();
|
||
const Vec2d n(-u.y(), u.x());
|
||
const double as = R * 0.42;
|
||
const Vec2d back = tip2 - u * as;
|
||
segs.emplace_back(Vec2d(0, 0), back);
|
||
segs.emplace_back(tip2, back + n * (as * 0.45));
|
||
segs.emplace_back(tip2, back - n * (as * 0.45));
|
||
if (filled_head) {
|
||
// A closed head reads solid; the open one is left as two barbs. At 20-odd pixels
|
||
// this is the difference that says "this body travels".
|
||
segs.emplace_back(back + n * (as * 0.45), back - n * (as * 0.45));
|
||
segs.emplace_back(back + n * (as * 0.22), tip2);
|
||
segs.emplace_back(back - n * (as * 0.22), tip2);
|
||
} else {
|
||
segs.emplace_back(back + n * (as * 0.45), back - n * (as * 0.45));
|
||
}
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
draw_strokes(m_mc_stroke_model, segs, lw, col);
|
||
};
|
||
|
||
if (style_A) {
|
||
arrow(X, R * 1.15, ColorRGBA(0.85f, 0.29f, 0.24f, 1.0f), true);
|
||
arrow(Y, R * 1.15, ColorRGBA(0.23f, 0.65f, 0.35f, 1.0f), true);
|
||
arrow(Z, R * 1.60, blue, true);
|
||
} else {
|
||
arrow(Z, R * 2.10, body, g.role == 2); // +Z only. Nothing is ever drawn on -Z.
|
||
}
|
||
}
|
||
m_plane = saved;
|
||
}
|
||
|
||
void DesignSketchTool::render_extrude_gizmo()
|
||
{
|
||
if (!m_ex_active) return;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const Vec3d right = cam.get_dir_right().normalized();
|
||
const Vec3d up = cam.get_dir_up().normalized();
|
||
const Vec3d fwd = cam.get_dir_forward().normalized();
|
||
const Vec3d base = m_ex_plane.to_world(m_ex_centroid);
|
||
const Vec3d ndir = (m_ex_flip ? -1.0 : 1.0) * m_ex_plane.normal.normalized();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const double th = std::max(15.0 * upp, 1e-4);
|
||
|
||
// Express everything in the billboard frame (origin=base) so it always faces the camera.
|
||
const SketchPlane saved = m_plane;
|
||
SketchPlane bb; bb.origin = base; bb.x_axis = right; bb.y_axis = up; bb.normal = fwd;
|
||
m_plane = bb;
|
||
const ColorRGBA arrowc(1.0f, 0.62f, 0.16f, 1.0f); // CAD amber
|
||
|
||
auto draw_arrow = [&](double depth, bool flip_side) {
|
||
if (depth <= 1e-6) return;
|
||
const Vec3d dirw = (flip_side ? -1.0 : 1.0) * ndir; // world arrow direction
|
||
const Vec3d tipw = base + dirw * depth;
|
||
const Vec2d tip2((tipw - base).dot(right), (tipw - base).dot(up));
|
||
if (tip2.norm() < 1e-6) return; // axis ~ parallel to view: no arrow
|
||
const Vec2d u = tip2.normalized();
|
||
const Vec2d nrm(-u.y(), u.x());
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
segs.emplace_back(Vec2d(0, 0), tip2);
|
||
const double as = std::max(tip2.norm() * 0.18, th * 0.9); // arrowhead size
|
||
const Vec2d back = tip2 - u * as;
|
||
segs.emplace_back(tip2, back + nrm * (as * 0.5));
|
||
segs.emplace_back(tip2, back - nrm * (as * 0.5));
|
||
draw_strokes(m_ex_arrow_model, segs, std::max(0.7 * upp, 1e-4), arrowc);
|
||
DimAnnot a; a.kind = DimType::Distance; a.value = depth;
|
||
draw_text(m_line_model, dim_text(a), tip2 + u * (th * 1.4), th, arrowc);
|
||
};
|
||
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
draw_arrow(m_ex_depth, false);
|
||
if (m_ex_two_sided) draw_arrow(m_ex_depth2, true);
|
||
m_plane = saved;
|
||
}
|
||
|
||
// Ray vs the arrow segment(s) in world space; `which` = 0 primary, 1 second side.
|
||
bool DesignSketchTool::hit_test_extrude_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int& which) const
|
||
{
|
||
if (!m_ex_active) return false;
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const double tol = 7.0 / std::max(cam.get_zoom(), 1e-6); // ~7 px in world units
|
||
const Vec3d ndir = (m_ex_flip ? -1.0 : 1.0) * m_ex_plane.normal.normalized();
|
||
const Vec3d base = m_ex_plane.to_world(m_ex_centroid);
|
||
double dA = 1e30, dB = 1e30;
|
||
if (m_ex_depth > 1e-6) dA = ray_segment_dist3(ro, rd, base, base + ndir * m_ex_depth);
|
||
if (m_ex_two_sided && m_ex_depth2 > 1e-6)
|
||
dB = ray_segment_dist3(ro, rd, base, base - ndir * m_ex_depth2);
|
||
if (dA <= tol && dA <= dB) { which = 0; return true; }
|
||
if (dB <= tol) { which = 1; return true; }
|
||
return false;
|
||
}
|
||
|
||
// Drag the arrow handle: closest point on the world arrow axis to the mouse ray (skew-line
|
||
// closest-point), projected onto the axis direction -> signed depth (clamped positive).
|
||
void DesignSketchTool::drag_extrude_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int which)
|
||
{
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||
const Vec3d nd = (m_ex_flip ? -1.0 : 1.0) * m_ex_plane.normal.normalized();
|
||
const Vec3d e = (which == 1 ? -1.0 : 1.0) * nd; // axis dir for this arrow
|
||
const Vec3d base = m_ex_plane.to_world(m_ex_centroid);
|
||
const Vec3d w0 = base - ro;
|
||
const double a = e.dot(e), b = e.dot(rd), c = rd.dot(rd), dd = e.dot(w0), ee = rd.dot(w0);
|
||
const double denom = a * c - b * b;
|
||
if (std::abs(denom) < 1e-7) return; // camera ∥ axis: leave depth as-is
|
||
const double depth = std::max(0.01, (b * ee - c * dd) / denom);
|
||
if (which == 1) m_ex_depth2 = depth; else m_ex_depth = depth;
|
||
if (on_extrude_depth_changed) on_extrude_depth_changed(depth, which == 1);
|
||
}
|
||
|
||
void DesignSketchTool::open_extrude_editor(int which)
|
||
{
|
||
if (!on_inline_edit) return;
|
||
const double cur = (which == 1) ? m_ex_depth2 : m_ex_depth;
|
||
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
||
on_inline_edit(px, cur, "",
|
||
[this, which](double v) {
|
||
const double d = std::max(0.01, v);
|
||
if (which == 1) m_ex_depth2 = d; else m_ex_depth = d;
|
||
if (on_extrude_depth_changed) on_extrude_depth_changed(d, which == 1);
|
||
},
|
||
[]() {});
|
||
}
|
||
|
||
// ---- Datum-plane resize gizmo (C3) ----------------------------------------------------
|
||
void DesignSketchTool::set_datum_gizmo(const SketchPlane& plane, double usize, double vsize,
|
||
const Vec3d& base_origin, const Vec3d& base_normal,
|
||
double offset, bool offset_on)
|
||
{
|
||
m_dz_active = true;
|
||
m_dz_plane = plane;
|
||
m_dz_usize = std::max(1.0, usize);
|
||
m_dz_vsize = std::max(1.0, vsize);
|
||
m_dz_anchor = base_origin;
|
||
m_dz_normal = base_normal.normalized();
|
||
m_dz_offset = offset;
|
||
m_dz_offset_on = offset_on;
|
||
}
|
||
|
||
void DesignSketchTool::clear_datum_gizmo()
|
||
{
|
||
m_dz_active = false;
|
||
m_dz_drag = -1;
|
||
}
|
||
|
||
// Draw the datum rectangle outline + 4 camera-billboarded edge-midpoint handles. Self-contained
|
||
// so it shows even for an uncommitted (not-yet-Confirmed) datum that render_datum_planes can't draw.
|
||
void DesignSketchTool::render_datum_gizmo()
|
||
{
|
||
if (!m_dz_active) return;
|
||
using EPT = GLModel::Geometry::EPrimitiveType;
|
||
using EVL = GLModel::Geometry::EVertexLayout;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const Vec3d right = cam.get_dir_right().normalized();
|
||
const Vec3d up = cam.get_dir_up().normalized();
|
||
const Vec3d vd = cam.get_dir_forward();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const double hs = 6.0 * upp; // handle half-size (~6 px)
|
||
const double hw = 1.5 * upp; // outline ribbon half-width
|
||
const double hu = m_dz_usize * 0.5, hv = m_dz_vsize * 0.5;
|
||
const SketchPlane& p = m_dz_plane;
|
||
|
||
// Rectangle outline (4 thin camera-facing ribbons).
|
||
const Vec3d c[4] = { p.to_world(Vec2d(-hu, -hv)), p.to_world(Vec2d(hu, -hv)),
|
||
p.to_world(Vec2d(hu, hv)), p.to_world(Vec2d(-hu, hv)) };
|
||
GLModel::Geometry border; border.format = { EPT::Triangles, EVL::P3 };
|
||
unsigned int bb = 0;
|
||
for (int s = 0; s < 4; ++s) {
|
||
const Vec3d a = c[s], b = c[(s + 1) & 3];
|
||
Vec3d dir = b - a; if (dir.norm() < 1e-9) continue; dir.normalize();
|
||
Vec3d off = dir.cross(vd);
|
||
if (off.norm() < 1e-9) off = dir.cross(up);
|
||
if (off.norm() < 1e-9) continue;
|
||
off.normalize(); off *= hw;
|
||
border.add_vertex((Vec3f)(a + off).cast<float>()); border.add_vertex((Vec3f)(b + off).cast<float>());
|
||
border.add_vertex((Vec3f)(b - off).cast<float>()); border.add_vertex((Vec3f)(a - off).cast<float>());
|
||
border.add_triangle(bb, bb + 1, bb + 2); border.add_triangle(bb, bb + 2, bb + 3); bb += 4;
|
||
}
|
||
|
||
// 4 edge-midpoint handle squares.
|
||
const Vec2d hpos[4] = { Vec2d(hu, 0), Vec2d(-hu, 0), Vec2d(0, hv), Vec2d(0, -hv) };
|
||
GLModel::Geometry handles; handles.format = { EPT::Triangles, EVL::P3 };
|
||
unsigned int hb = 0;
|
||
for (int i = 0; i < 4; ++i) {
|
||
const Vec3d ctr = p.to_world(hpos[i]);
|
||
const Vec3d q0 = ctr - right * hs - up * hs, q1 = ctr + right * hs - up * hs,
|
||
q2 = ctr + right * hs + up * hs, q3 = ctr - right * hs + up * hs;
|
||
handles.add_vertex((Vec3f)q0.cast<float>()); handles.add_vertex((Vec3f)q1.cast<float>());
|
||
handles.add_vertex((Vec3f)q2.cast<float>()); handles.add_vertex((Vec3f)q3.cast<float>());
|
||
handles.add_triangle(hb, hb + 1, hb + 2); handles.add_triangle(hb, hb + 2, hb + 3); hb += 4;
|
||
}
|
||
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
if (bb > 0) {
|
||
GLModel m; m.init_from(std::move(border));
|
||
m.set_color(ColorRGBA(1.0f, 0.62f, 0.16f, 0.9f)); // CAD amber
|
||
m.render();
|
||
}
|
||
if (hb > 0) {
|
||
GLModel m; m.init_from(std::move(handles));
|
||
m.set_color(ColorRGBA(1.0f, 0.72f, 0.28f, 1.0f));
|
||
m.render();
|
||
}
|
||
|
||
// Offset arrow: a camera-facing ribbon from the base origin along the base normal to the
|
||
// datum origin, with a grabbable square at the tip. Drag the tip to set the offset distance.
|
||
if (m_dz_offset_on) {
|
||
const Vec3d tip = m_dz_anchor + m_dz_normal * m_dz_offset;
|
||
Vec3d off = m_dz_normal.cross(vd);
|
||
if (off.norm() < 1e-9) off = m_dz_normal.cross(up);
|
||
GLModel::Geometry shaft; shaft.format = { EPT::Triangles, EVL::P3 };
|
||
if (off.norm() > 1e-9 && (tip - m_dz_anchor).norm() > 1e-9) {
|
||
off.normalize(); off *= hw;
|
||
shaft.add_vertex((Vec3f)(m_dz_anchor + off).cast<float>());
|
||
shaft.add_vertex((Vec3f)(tip + off).cast<float>());
|
||
shaft.add_vertex((Vec3f)(tip - off).cast<float>());
|
||
shaft.add_vertex((Vec3f)(m_dz_anchor - off).cast<float>());
|
||
shaft.add_triangle(0, 1, 2); shaft.add_triangle(0, 2, 3);
|
||
GLModel sm; sm.init_from(std::move(shaft));
|
||
sm.set_color(ColorRGBA(0.30f, 0.78f, 1.0f, 0.95f)); // cyan offset axis
|
||
sm.render();
|
||
}
|
||
GLModel::Geometry tipsq; tipsq.format = { EPT::Triangles, EVL::P3 };
|
||
const Vec3d t0 = tip - right * hs - up * hs, t1 = tip + right * hs - up * hs,
|
||
t2 = tip + right * hs + up * hs, t3 = tip - right * hs + up * hs;
|
||
tipsq.add_vertex((Vec3f)t0.cast<float>()); tipsq.add_vertex((Vec3f)t1.cast<float>());
|
||
tipsq.add_vertex((Vec3f)t2.cast<float>()); tipsq.add_vertex((Vec3f)t3.cast<float>());
|
||
tipsq.add_triangle(0, 1, 2); tipsq.add_triangle(0, 2, 3);
|
||
GLModel tm; tm.init_from(std::move(tipsq));
|
||
tm.set_color(ColorRGBA(0.45f, 0.86f, 1.0f, 1.0f));
|
||
tm.render();
|
||
}
|
||
}
|
||
|
||
bool DesignSketchTool::hit_test_datum_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int& which) const
|
||
{
|
||
if (!m_dz_active) return false;
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const double tol = 9.0 / std::max(cam.get_zoom(), 1e-6); // ~9 px in world units
|
||
const double hu = m_dz_usize * 0.5, hv = m_dz_vsize * 0.5;
|
||
const Vec2d hpos[4] = { Vec2d(hu, 0), Vec2d(-hu, 0), Vec2d(0, hv), Vec2d(0, -hv) };
|
||
double best = tol; which = -1;
|
||
for (int i = 0; i < 4; ++i) {
|
||
const Vec3d pt = m_dz_plane.to_world(hpos[i]);
|
||
const Vec3d w = pt - ro;
|
||
const double t = w.dot(rd) / std::max(rd.dot(rd), 1e-12);
|
||
const double d = (w - rd * t).norm();
|
||
if (d < best) { best = d; which = i; }
|
||
}
|
||
if (m_dz_offset_on) { // offset arrow tip = handle 4
|
||
const Vec3d pt = m_dz_anchor + m_dz_normal * m_dz_offset;
|
||
const Vec3d w = pt - ro;
|
||
const double t = w.dot(rd) / std::max(rd.dot(rd), 1e-12);
|
||
const double d = (w - rd * t).norm();
|
||
if (d < best) { best = d; which = 4; }
|
||
}
|
||
return which >= 0;
|
||
}
|
||
|
||
// Drag a handle: closest point of the mouse ray to the plane axis (u or v) through the origin,
|
||
// |param| -> new half-extent, doubled to the full size.
|
||
void DesignSketchTool::drag_datum_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int which)
|
||
{
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||
if (which == 4) { // offset arrow: drag along base normal
|
||
const Vec3d e = m_dz_normal; // signed offset, may go negative
|
||
const Vec3d w0 = m_dz_anchor - ro;
|
||
const double a = e.dot(e), b = e.dot(rd), c = rd.dot(rd), dd = e.dot(w0), ee = rd.dot(w0);
|
||
const double denom = a * c - b * b;
|
||
if (std::abs(denom) < 1e-7) return; // camera ∥ normal: leave offset as-is
|
||
m_dz_offset = (b * ee - c * dd) / denom; // signed distance along the normal
|
||
m_dz_plane.origin = m_dz_anchor + m_dz_normal * m_dz_offset; // rectangle follows live
|
||
if (on_datum_offset_changed) on_datum_offset_changed(m_dz_offset);
|
||
return;
|
||
}
|
||
const bool uaxis = (which == 0 || which == 1);
|
||
const Vec3d e = (uaxis ? m_dz_plane.x_axis : m_dz_plane.y_axis).normalized();
|
||
const Vec3d base = m_dz_plane.origin;
|
||
const Vec3d w0 = base - ro;
|
||
const double a = e.dot(e), b = e.dot(rd), c = rd.dot(rd), dd = e.dot(w0), ee = rd.dot(w0);
|
||
const double denom = a * c - b * b;
|
||
if (std::abs(denom) < 1e-7) return; // camera ∥ axis: leave size as-is
|
||
const double s = (b * ee - c * dd) / denom; // signed coord along e of closest pt
|
||
const double size = std::max(2.0, 2.0 * std::abs(s));
|
||
if (uaxis) m_dz_usize = size; else m_dz_vsize = size;
|
||
if (on_datum_size_changed) on_datum_size_changed(m_dz_usize, m_dz_vsize);
|
||
}
|
||
|
||
// ---- Helix gizmo (plane-anchored curve + 3 drag handles) --------------------------------
|
||
void DesignSketchTool::set_helix_gizmo(const SketchPlane& plane, double radius, double pitch,
|
||
double height, double taper, bool left_handed)
|
||
{
|
||
m_hx_active = true;
|
||
m_hx_plane = plane;
|
||
m_hx_radius = radius;
|
||
m_hx_pitch = pitch;
|
||
m_hx_height = height;
|
||
m_hx_taper = taper;
|
||
m_hx_left = left_handed;
|
||
}
|
||
|
||
void DesignSketchTool::clear_helix_gizmo()
|
||
{
|
||
m_hx_active = false;
|
||
m_hx_drag = -1;
|
||
}
|
||
|
||
// Curve point at parameter t (t in turns): angle 2*pi*t, negated when left-handed, rising one
|
||
// pitch per turn along the plane normal.
|
||
//
|
||
// Taper is an ANGLE IN DEGREES and must be read the way the kernel reads it. CadDocument's
|
||
// helix_spine() builds a Geom_ConicalSurface of half-angle taper and computes the top radius as
|
||
// R + H*tan(taper), so the radius grows with the HEIGHT RISEN, not as a fraction of R consumed
|
||
// over the turn count. Getting that wrong draws a preview that collapses to a point for any
|
||
// non-zero taper while the committed feature is fine — a preview that lies is worse than none.
|
||
Vec3d DesignSketchTool::helix_point(double t) const
|
||
{
|
||
const Vec3d O = m_hx_plane.origin;
|
||
const Vec3d n = m_hx_plane.normal.normalized();
|
||
const Vec3d u = m_hx_plane.x_axis.normalized();
|
||
const Vec3d v = m_hx_plane.y_axis.normalized();
|
||
const double a = (m_hx_left ? -1.0 : 1.0) * 2.0 * M_PI * t;
|
||
const double z = m_hx_pitch * t; // height risen at this parameter
|
||
double rt = m_hx_radius + z * std::tan(m_hx_taper * M_PI / 180.0);
|
||
// The kernel REFUSES a taper that drives the radius negative before the full height; the
|
||
// preview shows it collapsing instead, so the user can see which value did it.
|
||
if (rt < 0.0) rt = 0.0;
|
||
return O + u * (rt * std::cos(a)) + v * (rt * std::sin(a)) + n * z;
|
||
}
|
||
|
||
// Draw the live helix as a connected camera-facing ribbon, a dim axis line, and three square
|
||
// handles (radius on the base circle, height on the axis top, pitch at the end of the first turn).
|
||
void DesignSketchTool::render_helix_gizmo()
|
||
{
|
||
if (!m_hx_active) return;
|
||
using EPT = GLModel::Geometry::EPrimitiveType;
|
||
using EVL = GLModel::Geometry::EVertexLayout;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const Vec3d right = cam.get_dir_right().normalized();
|
||
const Vec3d up = cam.get_dir_up().normalized();
|
||
const Vec3d vd = cam.get_dir_forward();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const double hs = 6.0 * upp; // handle half-size (~6 px)
|
||
const double hw = 1.5 * upp; // ribbon half-width
|
||
const Vec3d O = m_hx_plane.origin;
|
||
const Vec3d n = m_hx_plane.normal.normalized();
|
||
const double turns = m_hx_pitch > 1e-9 ? m_hx_height / m_hx_pitch : 0.0;
|
||
|
||
// Curve ribbon: connected thin camera-facing quads over t in [0, turns].
|
||
const int segs = std::min(2048, std::max(48, int(turns * 32.0)));
|
||
GLModel::Geometry curve; curve.format = { EPT::Triangles, EVL::P3 };
|
||
unsigned int cb = 0;
|
||
auto add_seg = [&](const Vec3d& a, const Vec3d& b) {
|
||
Vec3d dir = b - a; if (dir.norm() < 1e-9) return; dir.normalize();
|
||
Vec3d off = dir.cross(vd);
|
||
if (off.norm() < 1e-9) off = dir.cross(up);
|
||
if (off.norm() < 1e-9) return;
|
||
off.normalize(); off *= hw;
|
||
curve.add_vertex((Vec3f)(a + off).cast<float>()); curve.add_vertex((Vec3f)(b + off).cast<float>());
|
||
curve.add_vertex((Vec3f)(b - off).cast<float>()); curve.add_vertex((Vec3f)(a - off).cast<float>());
|
||
curve.add_triangle(cb, cb + 1, cb + 2); curve.add_triangle(cb, cb + 2, cb + 3); cb += 4;
|
||
};
|
||
for (int i = 0; i < segs; ++i)
|
||
add_seg(helix_point(turns * i / segs), helix_point(turns * (i + 1) / segs));
|
||
|
||
// Axis: a dim line from the plane origin to the top of the helix.
|
||
const Vec3d top = O + n * m_hx_height;
|
||
GLModel::Geometry axis; axis.format = { EPT::Triangles, EVL::P3 };
|
||
{
|
||
Vec3d dir = top - O;
|
||
if (dir.norm() > 1e-9) {
|
||
dir.normalize();
|
||
Vec3d off = dir.cross(vd);
|
||
if (off.norm() < 1e-9) off = dir.cross(up);
|
||
if (off.norm() > 1e-9) {
|
||
off.normalize(); off *= hw * 0.6;
|
||
axis.add_vertex((Vec3f)(O + off).cast<float>()); axis.add_vertex((Vec3f)(top + off).cast<float>());
|
||
axis.add_vertex((Vec3f)(top - off).cast<float>()); axis.add_vertex((Vec3f)(O - off).cast<float>());
|
||
axis.add_triangle(0, 1, 2); axis.add_triangle(0, 2, 3);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Three handles: 0=radius (t=0), 1=height (axis top), 2=pitch (end of first turn, or the
|
||
// whole curve if shorter than one turn so the handle never floats off a missing curve).
|
||
const Vec3d hpts[3] = { helix_point(0.0), top,
|
||
helix_point(m_hx_height < m_hx_pitch ? turns : 1.0) };
|
||
const ColorRGBA hcol[3] = { ColorRGBA(1.0f, 0.72f, 0.28f, 1.0f), // radius — amber
|
||
ColorRGBA(0.45f, 0.86f, 1.0f, 1.0f), // height — cyan
|
||
ColorRGBA(0.30f, 0.80f, 0.34f, 1.0f) }; // pitch — green
|
||
const ColorRGBA hot(1.0f, 0.85f, 0.2f, 1.0f);
|
||
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
if (cb > 0) {
|
||
GLModel m; m.init_from(std::move(curve));
|
||
m.set_color(ColorRGBA(1.0f, 0.62f, 0.16f, 0.9f)); // CAD amber helix curve
|
||
m.render();
|
||
}
|
||
if (!axis.vertices.empty()) {
|
||
GLModel m; m.init_from(std::move(axis));
|
||
m.set_color(ColorRGBA(0.42f, 0.46f, 0.52f, 0.55f)); // dim grey axis
|
||
m.render();
|
||
}
|
||
for (int i = 0; i < 3; ++i) {
|
||
const Vec3d ctr = hpts[i];
|
||
const Vec3d q0 = ctr - right * hs - up * hs, q1 = ctr + right * hs - up * hs,
|
||
q2 = ctr + right * hs + up * hs, q3 = ctr - right * hs + up * hs;
|
||
GLModel::Geometry sq; sq.format = { EPT::Triangles, EVL::P3 };
|
||
sq.add_vertex((Vec3f)q0.cast<float>()); sq.add_vertex((Vec3f)q1.cast<float>());
|
||
sq.add_vertex((Vec3f)q2.cast<float>()); sq.add_vertex((Vec3f)q3.cast<float>());
|
||
sq.add_triangle(0, 1, 2); sq.add_triangle(0, 2, 3);
|
||
GLModel m; m.init_from(std::move(sq));
|
||
m.set_color(m_hx_drag == i ? hot : hcol[i]);
|
||
m.render();
|
||
}
|
||
}
|
||
|
||
bool DesignSketchTool::hit_test_helix_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int& which) const
|
||
{
|
||
if (!m_hx_active) return false;
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const double tol = 9.0 / std::max(cam.get_zoom(), 1e-6); // ~9 px in world units
|
||
const Vec3d O = m_hx_plane.origin;
|
||
const Vec3d n = m_hx_plane.normal.normalized();
|
||
const double turns = m_hx_pitch > 1e-9 ? m_hx_height / m_hx_pitch : 0.0;
|
||
const Vec3d hpts[3] = { helix_point(0.0), O + n * m_hx_height,
|
||
helix_point(m_hx_height < m_hx_pitch ? turns : 1.0) };
|
||
double best = tol; which = -1;
|
||
for (int i = 0; i < 3; ++i) {
|
||
const Vec3d w = hpts[i] - ro;
|
||
const double t = w.dot(rd) / std::max(rd.dot(rd), 1e-12);
|
||
const double d = (w - rd * t).norm();
|
||
if (d < best) { best = d; which = i; }
|
||
}
|
||
return which >= 0;
|
||
}
|
||
|
||
// Drag a handle: radius = cursor's in-plane distance from the origin, height/pitch = the signed
|
||
// distance of the cursor's closest axis point. All fire the full (radius, pitch, height) triple.
|
||
void DesignSketchTool::drag_helix_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int which)
|
||
{
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||
const SketchPlane& p = m_hx_plane;
|
||
const Vec3d O = p.origin, n = p.normal.normalized();
|
||
|
||
if (which == 0) { // radius: in-plane distance from O
|
||
const Vec2d lp = p.project(ro, rd);
|
||
m_hx_radius = std::max(0.01, lp.norm());
|
||
} else { // height/pitch: signed distance along n
|
||
const Vec3d e = n;
|
||
const Vec3d w0 = O - ro;
|
||
const double a = e.dot(e), b = e.dot(rd), c = rd.dot(rd), dd = e.dot(w0), ee = rd.dot(w0);
|
||
const double denom = a * c - b * b;
|
||
if (std::abs(denom) < 1e-7) return; // camera ∥ axis: leave value as-is
|
||
const double s = (b * ee - c * dd) / denom; // signed distance along the axis
|
||
if (which == 1) m_hx_height = std::max(0.0, s);
|
||
else m_hx_pitch = std::max(0.01, s); // zero/negative pitch divides by zero
|
||
}
|
||
if (on_helix_changed) on_helix_changed(m_hx_radius, m_hx_pitch, m_hx_height);
|
||
}
|
||
|
||
// ---- Rib thickness gizmo (in-plane slab footprint + 2 symmetric handles) -----------------
|
||
void DesignSketchTool::set_rib_gizmo(const SketchPlane& plane, const Vec2d& p0, const Vec2d& p1,
|
||
double thickness)
|
||
{
|
||
m_rb_active = true;
|
||
m_rb_plane = plane;
|
||
m_rb_p0 = p0;
|
||
m_rb_p1 = p1;
|
||
m_rb_thickness = thickness;
|
||
}
|
||
|
||
void DesignSketchTool::clear_rib_gizmo()
|
||
{
|
||
m_rb_active = false;
|
||
m_rb_drag = -1;
|
||
}
|
||
|
||
// Resolve the rib line's in-plane frame: unit direction d, perpendicular perp, midpoint, and
|
||
// half-thickness. A degenerate line (zero length) has no direction to grow a slab perpendicular
|
||
// to — return false so the caller draws nothing and never divides by zero.
|
||
static bool rib_frame(const Vec2d& p0, const Vec2d& p1, double thickness,
|
||
Vec2d& d, Vec2d& perp, Vec2d& mid, double& half)
|
||
{
|
||
const Vec2d seg = p1 - p0;
|
||
const double len = seg.norm();
|
||
if (len < 1e-9) return false;
|
||
d = seg / len;
|
||
perp = Vec2d(-d.y(), d.x());
|
||
mid = 0.5 * (p0 + p1);
|
||
half = thickness * 0.5;
|
||
return true;
|
||
}
|
||
|
||
// Draw the rib slab's actual footprint (the rectangle p0±perp·half, p1±perp·half) as a thin
|
||
// closed ribbon plus two square handles at mid ± perp·half. Both handles sit at half-thickness,
|
||
// so a drag on either expresses the full thickness symmetrically.
|
||
void DesignSketchTool::render_rib_gizmo()
|
||
{
|
||
if (!m_rb_active) return;
|
||
Vec2d d, perp, mid; double half;
|
||
if (!rib_frame(m_rb_p0, m_rb_p1, m_rb_thickness, d, perp, mid, half)) return;
|
||
using EPT = GLModel::Geometry::EPrimitiveType;
|
||
using EVL = GLModel::Geometry::EVertexLayout;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const Vec3d right = cam.get_dir_right().normalized();
|
||
const Vec3d up = cam.get_dir_up().normalized();
|
||
const Vec3d vd = cam.get_dir_forward();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const double hs = 6.0 * upp; // handle half-size (~6 px)
|
||
const double hw = 1.5 * upp; // ribbon half-width
|
||
|
||
// Slab footprint outline (4 thin camera-facing ribbons).
|
||
const Vec3d c[4] = { m_rb_plane.to_world(m_rb_p0 + perp * half),
|
||
m_rb_plane.to_world(m_rb_p1 + perp * half),
|
||
m_rb_plane.to_world(m_rb_p1 - perp * half),
|
||
m_rb_plane.to_world(m_rb_p0 - perp * half) };
|
||
GLModel::Geometry border; border.format = { EPT::Triangles, EVL::P3 };
|
||
unsigned int bb = 0;
|
||
for (int s = 0; s < 4; ++s) {
|
||
const Vec3d a = c[s], b = c[(s + 1) & 3];
|
||
Vec3d dir = b - a; if (dir.norm() < 1e-9) continue; dir.normalize();
|
||
Vec3d off = dir.cross(vd);
|
||
if (off.norm() < 1e-9) off = dir.cross(up);
|
||
if (off.norm() < 1e-9) continue;
|
||
off.normalize(); off *= hw;
|
||
border.add_vertex((Vec3f)(a + off).cast<float>()); border.add_vertex((Vec3f)(b + off).cast<float>());
|
||
border.add_vertex((Vec3f)(b - off).cast<float>()); border.add_vertex((Vec3f)(a - off).cast<float>());
|
||
border.add_triangle(bb, bb + 1, bb + 2); border.add_triangle(bb, bb + 2, bb + 3); bb += 4;
|
||
}
|
||
|
||
// Two handles: 0 = +perp side, 1 = -perp side (both at half-thickness).
|
||
const Vec3d hpts[2] = { m_rb_plane.to_world(mid + perp * half),
|
||
m_rb_plane.to_world(mid - perp * half) };
|
||
const ColorRGBA hot(1.0f, 0.85f, 0.2f, 1.0f);
|
||
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
if (bb > 0) {
|
||
GLModel m; m.init_from(std::move(border));
|
||
m.set_color(ColorRGBA(1.0f, 0.62f, 0.16f, 0.9f)); // CAD amber slab footprint
|
||
m.render();
|
||
}
|
||
for (int i = 0; i < 2; ++i) {
|
||
const Vec3d ctr = hpts[i];
|
||
const Vec3d q0 = ctr - right * hs - up * hs, q1 = ctr + right * hs - up * hs,
|
||
q2 = ctr + right * hs + up * hs, q3 = ctr - right * hs + up * hs;
|
||
GLModel::Geometry sq; sq.format = { EPT::Triangles, EVL::P3 };
|
||
sq.add_vertex((Vec3f)q0.cast<float>()); sq.add_vertex((Vec3f)q1.cast<float>());
|
||
sq.add_vertex((Vec3f)q2.cast<float>()); sq.add_vertex((Vec3f)q3.cast<float>());
|
||
sq.add_triangle(0, 1, 2); sq.add_triangle(0, 2, 3);
|
||
GLModel m; m.init_from(std::move(sq));
|
||
m.set_color(m_rb_drag == i ? hot : ColorRGBA(0.30f, 0.80f, 0.34f, 1.0f)); // green
|
||
m.render();
|
||
}
|
||
}
|
||
|
||
bool DesignSketchTool::hit_test_rib_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int& which) const
|
||
{
|
||
if (!m_rb_active) return false;
|
||
Vec2d d, perp, mid; double half;
|
||
if (!rib_frame(m_rb_p0, m_rb_p1, m_rb_thickness, d, perp, mid, half)) return false;
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const double tol = 9.0 / std::max(cam.get_zoom(), 1e-6); // ~9 px in world units
|
||
const Vec3d hpts[2] = { m_rb_plane.to_world(mid + perp * half),
|
||
m_rb_plane.to_world(mid - perp * half) };
|
||
double best = tol; which = -1;
|
||
for (int i = 0; i < 2; ++i) {
|
||
const Vec3d w = hpts[i] - ro;
|
||
const double t = w.dot(rd) / std::max(rd.dot(rd), 1e-12);
|
||
const double dd = (w - rd * t).norm();
|
||
if (dd < best) { best = dd; which = i; }
|
||
}
|
||
return which >= 0;
|
||
}
|
||
|
||
// Drag a handle: project the cursor ray onto the rib plane (the same call the helix radius drag
|
||
// uses), take the perpendicular distance from the rib LINE to that point, and set the thickness
|
||
// to twice that distance — the slab is centred on the line and the handle sits at half-thickness.
|
||
void DesignSketchTool::drag_rib_handle(GLCanvas3D& canvas, const wxMouseEvent& evt, int which)
|
||
{
|
||
(void)which; // both handles behave identically: a drag on either sets the full thickness
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||
Vec2d d, perp, mid; double half;
|
||
if (!rib_frame(m_rb_p0, m_rb_p1, m_rb_thickness, d, perp, mid, half)) return;
|
||
const Vec2d lp = m_rb_plane.project(ro, rd);
|
||
const Vec2d w = lp - m_rb_p0;
|
||
const double dist = std::abs(w.x() * d.y() - w.y() * d.x()); // |cross(w, d)| = perp distance
|
||
m_rb_thickness = std::max(0.01, 2.0 * dist); // ×2: centred slab, handle at half
|
||
if (on_rib_thickness_changed) on_rib_thickness_changed(m_rb_thickness);
|
||
}
|
||
|
||
// ---- Reference/base planes (Onshape-style default planes) -----------------------------
|
||
void DesignSketchTool::set_base_pick(std::vector<SketchPlane> planes, std::vector<int> bases,
|
||
std::vector<std::string> labels)
|
||
{
|
||
m_dbp_active = !planes.empty();
|
||
m_dbp_planes = std::move(planes);
|
||
m_dbp_base = std::move(bases);
|
||
m_dbp_labels = std::move(labels);
|
||
if (m_dbp_hover >= int(m_dbp_planes.size())) m_dbp_hover = -1;
|
||
}
|
||
|
||
void DesignSketchTool::clear_base_pick()
|
||
{
|
||
m_dbp_active = false;
|
||
m_dbp_planes.clear();
|
||
m_dbp_base.clear();
|
||
m_dbp_labels.clear();
|
||
m_dbp_hover = -1;
|
||
}
|
||
|
||
// Reference planes are larger than the bed (Onshape default-plane feel). Half-extent = 0.6 * the
|
||
// bed's larger side, so the square fully overhangs the print area. Falls back to 150mm if the bed
|
||
// isn't queryable yet.
|
||
double DesignSketchTool::dbp_half_extent() const
|
||
{
|
||
double half = 150.0;
|
||
if (auto* pl = wxGetApp().plater()) {
|
||
const BoundingBoxf bb = pl->build_volume().bounding_volume2d();
|
||
const double w = bb.max.x() - bb.min.x(), d = bb.max.y() - bb.min.y();
|
||
if (w > 1.0 && d > 1.0) half = 0.6 * std::max(w, d);
|
||
}
|
||
return half;
|
||
}
|
||
|
||
// Draw the reference planes as large translucent labelled squares; the hovered one brightens.
|
||
void DesignSketchTool::render_base_pick()
|
||
{
|
||
if (!m_dbp_active || m_dbp_planes.empty()) return;
|
||
using EPT = GLModel::Geometry::EPrimitiveType;
|
||
using EVL = GLModel::Geometry::EVertexLayout;
|
||
const double H = dbp_half_extent();
|
||
// Onshape-ish per-plane tints: XY blue, XZ green, YZ red (keyed by base index 0/1/2; datums grey).
|
||
auto tint = [](int base, bool hot) -> ColorRGBA {
|
||
float a = hot ? 0.10f : 0.047f; // base planes kept faint (reduced ~2/3 from 0.30/0.14)
|
||
if (base == 0) return ColorRGBA(0.30f, 0.55f, 0.95f, a);
|
||
if (base == 1) return ColorRGBA(0.35f, 0.80f, 0.45f, a);
|
||
if (base == 2) return ColorRGBA(0.92f, 0.42f, 0.42f, a);
|
||
return ColorRGBA(0.70f, 0.72f, 0.78f, a);
|
||
};
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
glsafe(::glDisable(GL_CULL_FACE));
|
||
glsafe(::glEnable(GL_BLEND)); // alpha is ignored without this
|
||
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
|
||
const SketchPlane saved_plane = m_plane;
|
||
for (size_t i = 0; i < m_dbp_planes.size(); ++i) {
|
||
const SketchPlane& p = m_dbp_planes[i];
|
||
const Vec3d q0 = p.to_world(Vec2d(-H, -H)), q1 = p.to_world(Vec2d(H, -H)),
|
||
q2 = p.to_world(Vec2d(H, H)), q3 = p.to_world(Vec2d(-H, H));
|
||
GLModel::Geometry quad; quad.format = { EPT::Triangles, EVL::P3 };
|
||
quad.add_vertex((Vec3f)q0.cast<float>()); quad.add_vertex((Vec3f)q1.cast<float>());
|
||
quad.add_vertex((Vec3f)q2.cast<float>()); quad.add_vertex((Vec3f)q3.cast<float>());
|
||
quad.add_triangle(0, 1, 2); quad.add_triangle(0, 2, 3);
|
||
GLModel m; m.init_from(std::move(quad));
|
||
const bool hot = (int(i) == m_dbp_hover);
|
||
const int base = (i < m_dbp_base.size()) ? m_dbp_base[i] : -1;
|
||
m.set_color(tint(base, hot));
|
||
m.render();
|
||
|
||
// Label near the top-left corner, drawn in the plane (draw_text lifts through m_plane).
|
||
if (i < m_dbp_labels.size() && !m_dbp_labels[i].empty()) {
|
||
m_plane = p;
|
||
const double th = H * 0.10;
|
||
const ColorRGBA lc = tint(base, true); ColorRGBA lcs(lc.r(), lc.g(), lc.b(), 1.0f);
|
||
draw_text(m_line_model, m_dbp_labels[i], Vec2d(-H + th * 2.0, H - th * 1.6), th, lcs);
|
||
}
|
||
}
|
||
m_plane = saved_plane; // draw_text renders each label immediately (draw_strokes self-renders)
|
||
glsafe(::glDisable(GL_BLEND));
|
||
}
|
||
|
||
// Ray-pick the reference planes: intersect the mouse ray with each plane, keep hits inside the
|
||
// square, return the index of the nearest by |t|. -1 on miss.
|
||
int DesignSketchTool::hit_test_base_pick(GLCanvas3D& canvas, const wxMouseEvent& evt) const
|
||
{
|
||
if (!m_dbp_active) return -1;
|
||
const double H = dbp_half_extent();
|
||
|
||
// THE LABEL WINS, and it has to. Each plane's name is a screen-space chip centred on its
|
||
// own in-plane anchor, and it is the one part of a base plane a user aims at deliberately —
|
||
// the quads are near-transparent and overlap everywhere. Ray-casting the quads alone made
|
||
// the labels pure decoration: on a fresh document at 1920x1060, clicking "XY" reported
|
||
// "XZ plane selected", because the XZ quad happens to sit in front at that pixel. Nothing
|
||
// about the click was ambiguous to the user; they clicked the word XY.
|
||
// Anchor and text height must track render_base_pick's, which is where they are drawn.
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const double th = H * 0.10;
|
||
const Vec2d anchor(-H + th * 2.0, H - th * 1.6);
|
||
int lbest = -1; double lbest_d = 1e30;
|
||
for (size_t i = 0; i < m_dbp_planes.size(); ++i) {
|
||
if (i >= m_dbp_labels.size() || m_dbp_labels[i].empty()) continue;
|
||
const wxPoint sp = world_to_screen_px(cam, m_dbp_planes[i].to_world(anchor));
|
||
if (sp.x < 0 && sp.y < 0) continue; // behind the camera
|
||
const double dx = std::abs(double(evt.GetX() - sp.x));
|
||
const double dy = std::abs(double(evt.GetY() - sp.y));
|
||
// Chip half-extents in px, scaled like the label itself. Generous rather than tight:
|
||
// missing the text and silently selecting a different plane is the failure being fixed.
|
||
const double hw = (9.0 + 5.0 * double(m_dbp_labels[i].size())) * double(m_render_scale);
|
||
const double hh = 11.0 * double(m_render_scale);
|
||
if (dx > hw || dy > hh) continue;
|
||
const double d = dx * dx + dy * dy; // nearest label if chips overlap
|
||
if (d < lbest_d) { lbest_d = d; lbest = int(i); }
|
||
}
|
||
if (lbest >= 0) return lbest;
|
||
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||
int best = -1; double best_t = 1e30;
|
||
for (size_t i = 0; i < m_dbp_planes.size(); ++i) {
|
||
const SketchPlane& p = m_dbp_planes[i];
|
||
const double dn = rd.dot(p.normal);
|
||
if (std::abs(dn) < 1e-9) continue; // ray parallel to plane
|
||
const double t = (p.origin - ro).dot(p.normal) / dn;
|
||
if (t < 0) continue; // behind the camera
|
||
const Vec3d hit = ro + rd * t;
|
||
const Vec3d d = hit - p.origin;
|
||
if (std::abs(d.dot(p.x_axis)) > H || std::abs(d.dot(p.y_axis)) > H) continue;
|
||
if (t < best_t) { best_t = t; best = int(i); }
|
||
}
|
||
return best;
|
||
}
|
||
|
||
// ---- Move-body gizmo (M5) -------------------------------------------------------------
|
||
void DesignSketchTool::set_move_gizmo(int body, const Vec3d& pivot, const Transform3d& base_xform,
|
||
double body_radius)
|
||
{
|
||
m_mv_active = true;
|
||
m_mv_body = body;
|
||
m_mv_base = pivot; // body's world centroid at Move-open = rotation pivot
|
||
m_mv_base_xform = base_xform; // pose the deltas compose onto
|
||
m_mv_offset = Vec3d::Zero();
|
||
m_mv_rot = Eigen::Matrix3d::Identity();
|
||
m_mv_drag = -1;
|
||
m_mv_radius = std::max(body_radius, 0.0);
|
||
}
|
||
|
||
// Gizmo arm length in world mm. Orca's Prepare gizmos size themselves from the selection's
|
||
// bounding sphere (GLGizmoRotate3D: m_radius = Offset + sphere radius) so the handles always sit
|
||
// clear of the object; a fixed screen-size arm instead collapsed into a tangle buried inside a
|
||
// large solid, which is why the rotation rings read as "missing". Same idea here, with a
|
||
// screen-space floor so the gizmo stays grabbable on a tiny body or when zoomed far out.
|
||
double DesignSketchTool::move_gizmo_arm(const Camera& cam) const
|
||
{
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const double screen_min = 70.0 * upp; // never smaller than the old fixed size
|
||
return std::max(screen_min, m_mv_radius * 1.25); // 25% clear of the body surface
|
||
}
|
||
|
||
void DesignSketchTool::clear_move_gizmo()
|
||
{
|
||
m_mv_active = false;
|
||
m_mv_drag = -1;
|
||
m_mv_body = -1;
|
||
m_mv_offset = Vec3d::Zero();
|
||
m_mv_rot = Eigen::Matrix3d::Identity();
|
||
}
|
||
|
||
// Final body transform = translate(delta) then rotate(delta, about pivot) on the open pose.
|
||
Transform3d DesignSketchTool::compose_move_xform() const
|
||
{
|
||
const Vec3d p = m_mv_base;
|
||
Transform3d R = Transform3d::Identity();
|
||
R.linear() = m_mv_rot;
|
||
const Transform3d rot_about_pivot =
|
||
Transform3d(Eigen::Translation3d(p)) * R * Transform3d(Eigen::Translation3d(-p));
|
||
return Transform3d(Eigen::Translation3d(m_mv_offset)) * rot_about_pivot * m_mv_base_xform;
|
||
}
|
||
|
||
// World axis `e` of ring `axis` plus an in-plane orthonormal basis (u,v).
|
||
void DesignSketchTool::ring_basis(int axis, Vec3d& e, Vec3d& u, Vec3d& v) const
|
||
{
|
||
switch (axis) {
|
||
case 0: e = Vec3d::UnitX(); u = Vec3d::UnitY(); v = Vec3d::UnitZ(); break;
|
||
case 1: e = Vec3d::UnitY(); u = Vec3d::UnitZ(); v = Vec3d::UnitX(); break;
|
||
default:e = Vec3d::UnitZ(); u = Vec3d::UnitX(); v = Vec3d::UnitY(); break;
|
||
}
|
||
}
|
||
|
||
// Three world-axis arrows (X red / Y green / Z blue) from the body centroid + current
|
||
// offset, billboarded into a screen-facing frame like the extrude depth arrow.
|
||
void DesignSketchTool::render_move_gizmo()
|
||
{
|
||
if (!m_mv_active) return;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const Vec3d right = cam.get_dir_right().normalized();
|
||
const Vec3d up = cam.get_dir_up().normalized();
|
||
const Vec3d fwd = cam.get_dir_forward().normalized();
|
||
const Vec3d anchor = m_mv_base + m_mv_offset;
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const double th = std::max(15.0 * upp, 1e-4);
|
||
const double L = move_gizmo_arm(cam); // scales with the body (see move_gizmo_arm)
|
||
|
||
const SketchPlane saved = m_plane;
|
||
SketchPlane bb; bb.origin = anchor; bb.x_axis = right; bb.y_axis = up; bb.normal = fwd;
|
||
m_plane = bb;
|
||
|
||
const Vec3d axes[3] = { Vec3d::UnitX(), Vec3d::UnitY(), Vec3d::UnitZ() };
|
||
const ColorRGBA cols[3] = { ColorRGBA(0.92f, 0.28f, 0.28f, 1.0f), // X red
|
||
ColorRGBA(0.30f, 0.80f, 0.34f, 1.0f), // Y green
|
||
ColorRGBA(0.32f, 0.55f, 0.95f, 1.0f) }; // Z blue
|
||
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
for (int a = 0; a < 3; ++a) {
|
||
const Vec3d tipw = anchor + axes[a] * L;
|
||
const Vec2d tip2((tipw - anchor).dot(right), (tipw - anchor).dot(up));
|
||
if (tip2.norm() < 1e-6) continue; // axis ~parallel to view: skip
|
||
const Vec2d u = tip2.normalized();
|
||
const Vec2d nrm(-u.y(), u.x());
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
segs.emplace_back(Vec2d(0, 0), tip2);
|
||
const double as = std::max(tip2.norm() * 0.20, th * 0.9);
|
||
const Vec2d back = tip2 - u * as;
|
||
segs.emplace_back(tip2, back + nrm * (as * 0.5));
|
||
segs.emplace_back(tip2, back - nrm * (as * 0.5));
|
||
draw_strokes(m_mv_arrow_model, segs, std::max(0.7 * upp, 1e-4), cols[a]);
|
||
const double off = m_mv_offset[a];
|
||
if (std::abs(off) > 1e-4) {
|
||
DimAnnot da; da.kind = DimType::Distance; da.value = std::abs(off);
|
||
draw_text(m_line_model, dim_text(da), tip2 + u * (th * 1.4), th, cols[a]);
|
||
}
|
||
}
|
||
m_plane = saved;
|
||
|
||
// Three world-axis rotation rings (X/Y/Z), each a circle in the plane perpendicular to
|
||
// its axis through the gizmo anchor — drag a ring to rotate the body about that axis.
|
||
const double R = 0.83 * move_gizmo_arm(cam); // rings just inside the arrow tips
|
||
for (int a = 0; a < 3; ++a) {
|
||
Vec3d e, u, v; ring_basis(a, e, u, v);
|
||
SketchPlane rp; rp.origin = anchor; rp.x_axis = u; rp.y_axis = v; rp.normal = e;
|
||
m_plane = rp;
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
const int N = 48;
|
||
Vec2d prev(R, 0.0);
|
||
for (int i = 1; i <= N; ++i) {
|
||
const double t = 2.0 * M_PI * double(i) / double(N);
|
||
const Vec2d cur(R * std::cos(t), R * std::sin(t));
|
||
segs.emplace_back(prev, cur); prev = cur;
|
||
}
|
||
draw_strokes(m_mv_arrow_model, segs, std::max(0.55 * upp, 1e-4), cols[a]);
|
||
}
|
||
m_plane = saved;
|
||
}
|
||
|
||
// Ray vs each world-axis arrow segment; nearest within ~7 px wins.
|
||
bool DesignSketchTool::hit_test_move_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int& axis) const
|
||
{
|
||
if (!m_mv_active) return false;
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const double L = move_gizmo_arm(cam); // must match render_move_gizmo
|
||
const Vec3d anchor = m_mv_base + m_mv_offset;
|
||
const Vec3d axes[3] = { Vec3d::UnitX(), Vec3d::UnitY(), Vec3d::UnitZ() };
|
||
int best = -1; double bestd = 7.0 * upp; // ~7 px tolerance
|
||
for (int a = 0; a < 3; ++a) {
|
||
const double d = ray_segment_dist3(ro, rd, anchor, anchor + axes[a] * L);
|
||
if (d <= bestd) { bestd = d; best = a; }
|
||
}
|
||
if (best < 0) return false;
|
||
axis = best; return true;
|
||
}
|
||
|
||
// Skew-line closest point of the mouse ray to the axis line through the ORIGINAL centroid
|
||
// -> signed offset along that axis (no clamp; a body can move either way).
|
||
void DesignSketchTool::drag_move_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis)
|
||
{
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||
const Vec3d axes[3] = { Vec3d::UnitX(), Vec3d::UnitY(), Vec3d::UnitZ() };
|
||
const Vec3d e = axes[axis];
|
||
const Vec3d w0 = m_mv_base - ro;
|
||
const double a = e.dot(e), b = e.dot(rd), c = rd.dot(rd), dd = e.dot(w0), ee = rd.dot(w0);
|
||
const double denom = a * c - b * b;
|
||
if (std::abs(denom) < 1e-7) return; // camera ∥ axis: leave offset as-is
|
||
m_mv_offset[axis] = (b * ee - c * dd) / denom;
|
||
if (on_body_move_changed) on_body_move_changed(m_mv_body, compose_move_xform());
|
||
}
|
||
|
||
void DesignSketchTool::open_move_editor(int axis)
|
||
{
|
||
if (!on_inline_edit) return;
|
||
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
||
on_inline_edit(px, m_mv_offset[axis], "",
|
||
[this, axis](double v) {
|
||
m_mv_offset[axis] = v;
|
||
if (on_body_move_changed) on_body_move_changed(m_mv_body, compose_move_xform());
|
||
},
|
||
[]() {});
|
||
}
|
||
|
||
// Ray vs each rotation ring (sampled polyline); nearest within ~7 px wins -> axis 0/1/2.
|
||
bool DesignSketchTool::hit_test_move_arc(GLCanvas3D& canvas, const wxMouseEvent& evt, int& axis) const
|
||
{
|
||
if (!m_mv_active) return false;
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const double R = 0.83 * move_gizmo_arm(cam); // rings just inside the arrow tips
|
||
const Vec3d anchor = m_mv_base + m_mv_offset;
|
||
int best = -1; double bestd = 7.0 * upp;
|
||
const int N = 48;
|
||
for (int a = 0; a < 3; ++a) {
|
||
Vec3d e, u, v; ring_basis(a, e, u, v);
|
||
Vec3d prev = anchor + R * u;
|
||
for (int i = 1; i <= N; ++i) {
|
||
const double t = 2.0 * M_PI * double(i) / double(N);
|
||
const Vec3d cur = anchor + R * (std::cos(t) * u + std::sin(t) * v);
|
||
const double d = ray_segment_dist3(ro, rd, prev, cur);
|
||
if (d <= bestd) { bestd = d; best = a; }
|
||
prev = cur;
|
||
}
|
||
}
|
||
if (best < 0) return false;
|
||
axis = best; return true;
|
||
}
|
||
|
||
// Intersect the mouse ray with ring `axis`'s plane through the anchor -> in-plane angle.
|
||
bool DesignSketchTool::arc_mouse_angle(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis, double& ang) const
|
||
{
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||
Vec3d e, u, v; ring_basis(axis, e, u, v);
|
||
const Vec3d p = m_mv_base + m_mv_offset;
|
||
const double denom = rd.dot(e);
|
||
if (std::abs(denom) < 1e-9) return false; // ray ∥ ring plane
|
||
const Vec3d hit = ro + ((p - ro).dot(e) / denom) * rd;
|
||
const Vec3d d = hit - p;
|
||
ang = std::atan2(d.dot(v), d.dot(u));
|
||
return true;
|
||
}
|
||
|
||
// Drag a ring -> rotate the body about that world axis by (mouse angle - grab angle).
|
||
void DesignSketchTool::drag_move_arc(GLCanvas3D& canvas, const wxMouseEvent& evt, int axis)
|
||
{
|
||
double ang;
|
||
if (!arc_mouse_angle(canvas, evt, axis, ang)) return;
|
||
Vec3d e, u, v; ring_basis(axis, e, u, v);
|
||
const double delta = ang - m_mv_arc_a0;
|
||
m_mv_rot = Eigen::AngleAxisd(delta, e).toRotationMatrix() * m_mv_rot_start;
|
||
if (on_body_move_changed) on_body_move_changed(m_mv_body, compose_move_xform());
|
||
}
|
||
|
||
// ---- Fillet/Chamfer radius gizmo ------------------------------------------------------
|
||
// Anchor a single radius arrow at the picked edge midpoint (m_sel_edge_pts is already in world
|
||
// space, body-transformed), perpendicular to the edge and pointing away from the body centroid —
|
||
// the natural outward direction a fillet/chamfer grows.
|
||
bool DesignSketchTool::set_fillet_gizmo(const Vec3d& body_centroid, double radius)
|
||
{
|
||
if (m_sel_edge_pts.size() < 2) { m_fl_active = false; return false; }
|
||
const size_t n = m_sel_edge_pts.size();
|
||
// True geometric midpoint along the edge: a straight edge often samples to just its two
|
||
// endpoints, so the middle INDEX would land on an end. Walk the polyline to its half-length.
|
||
double total = 0.0;
|
||
for (size_t i = 1; i < n; ++i) total += (m_sel_edge_pts[i] - m_sel_edge_pts[i - 1]).norm();
|
||
m_fl_anchor = m_sel_edge_pts[0];
|
||
Vec3d t = m_sel_edge_pts[n - 1] - m_sel_edge_pts[0];
|
||
const double half = 0.5 * total;
|
||
double acc = 0.0;
|
||
for (size_t i = 1; i < n; ++i) {
|
||
const Vec3d seg = m_sel_edge_pts[i] - m_sel_edge_pts[i - 1];
|
||
const double L = seg.norm();
|
||
if (acc + L >= half && L > 1e-12) {
|
||
m_fl_anchor = m_sel_edge_pts[i - 1] + seg * ((half - acc) / L);
|
||
t = seg;
|
||
break;
|
||
}
|
||
acc += L;
|
||
}
|
||
if (t.norm() < 1e-9) return false;
|
||
t.normalize();
|
||
Vec3d r = m_fl_anchor - body_centroid; // radial offset from the body centre
|
||
r -= r.dot(t) * t; // strip the along-edge component
|
||
if (r.norm() < 1e-6) { // edge passes through the centroid
|
||
r = t.cross(Vec3d::UnitZ());
|
||
if (r.norm() < 1e-6) r = t.cross(Vec3d::UnitX());
|
||
}
|
||
m_fl_dir = r.normalized();
|
||
m_fl_radius = std::max(0.01, radius);
|
||
if (!m_fl_active) m_fl_drag = false; // re-anchored every preview: preserve an in-progress drag
|
||
m_fl_active = true;
|
||
return true;
|
||
}
|
||
|
||
void DesignSketchTool::clear_fillet_gizmo()
|
||
{
|
||
m_fl_active = false;
|
||
m_fl_drag = false;
|
||
}
|
||
|
||
// Single billboarded radius arrow from the edge midpoint along m_fl_dir; length = radius (world),
|
||
// floored to a grabbable screen size. Label shows the true radius (R-prefixed).
|
||
void DesignSketchTool::render_fillet_gizmo()
|
||
{
|
||
if (!m_fl_active) return;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const Vec3d right = cam.get_dir_right().normalized();
|
||
const Vec3d up = cam.get_dir_up().normalized();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const double th = std::max(15.0 * upp, 1e-4);
|
||
const double L = std::max(m_fl_radius, 40.0 * upp); // WYSIWYG, floored to a comfortable handle
|
||
const Vec3d tipw = m_fl_anchor + m_fl_dir * L;
|
||
|
||
const SketchPlane saved = m_plane;
|
||
SketchPlane bb; bb.origin = m_fl_anchor; bb.x_axis = right; bb.y_axis = up; bb.normal = cam.get_dir_forward().normalized();
|
||
m_plane = bb;
|
||
const ColorRGBA arrowc(1.0f, 0.62f, 0.16f, 1.0f); // CAD amber
|
||
|
||
const Vec2d tip2((tipw - m_fl_anchor).dot(right), (tipw - m_fl_anchor).dot(up));
|
||
if (tip2.norm() > 1e-6) {
|
||
const Vec2d u = tip2.normalized();
|
||
const Vec2d nrm(-u.y(), u.x());
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
segs.emplace_back(Vec2d(0, 0), tip2);
|
||
const double as = std::max(tip2.norm() * 0.20, th * 0.9);
|
||
const Vec2d back = tip2 - u * as;
|
||
segs.emplace_back(tip2, back + nrm * (as * 0.5));
|
||
segs.emplace_back(tip2, back - nrm * (as * 0.5));
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
draw_strokes(m_fl_arrow_model, segs, std::max(0.7 * upp, 1e-4), arrowc);
|
||
DimAnnot da; da.kind = DimType::Radius; da.value = m_fl_radius;
|
||
draw_text(m_line_model, dim_text(da), tip2 + u * (th * 1.4), th, arrowc);
|
||
}
|
||
m_plane = saved;
|
||
}
|
||
|
||
// Ray vs the radius arrow segment; ~12 px tolerance over the (floored) handle length.
|
||
bool DesignSketchTool::hit_test_fillet_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt) const
|
||
{
|
||
if (!m_fl_active) return false;
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const double L = std::max(m_fl_radius, 40.0 * upp);
|
||
return ray_segment_dist3(ro, rd, m_fl_anchor, m_fl_anchor + m_fl_dir * L) <= 12.0 * upp;
|
||
}
|
||
|
||
// Skew-line closest point of the mouse ray to the radius axis -> signed distance along m_fl_dir
|
||
// from the anchor. NaN when the camera is ~parallel to the axis (no meaningful projection).
|
||
double DesignSketchTool::fillet_axis_proj(GLCanvas3D& canvas, const wxMouseEvent& evt) const
|
||
{
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||
const Vec3d e = m_fl_dir;
|
||
const Vec3d w0 = m_fl_anchor - ro;
|
||
const double a = e.dot(e), b = e.dot(rd), c = rd.dot(rd), dd = e.dot(w0), ee = rd.dot(w0);
|
||
const double denom = a * c - b * b;
|
||
if (std::abs(denom) < 1e-7) return std::nan("");
|
||
return (b * ee - c * dd) / denom;
|
||
}
|
||
|
||
// Record the grab reference so the drag is RELATIVE (grab anywhere on the handle without the
|
||
// radius snapping to the grab point — important since the handle is floored to a min size).
|
||
void DesignSketchTool::start_fillet_drag(GLCanvas3D& canvas, const wxMouseEvent& evt)
|
||
{
|
||
m_fl_drag = true;
|
||
m_fl_press_x = evt.GetX();
|
||
m_fl_press_y = evt.GetY();
|
||
m_fl_grab_radius = m_fl_radius;
|
||
const double p = fillet_axis_proj(canvas, evt);
|
||
m_fl_grab_proj = std::isnan(p) ? 0.0 : p;
|
||
}
|
||
|
||
void DesignSketchTool::drag_fillet_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt)
|
||
{
|
||
const double proj = fillet_axis_proj(canvas, evt);
|
||
if (std::isnan(proj)) return; // camera ∥ axis: leave radius as-is
|
||
m_fl_radius = std::max(0.01, m_fl_grab_radius + (proj - m_fl_grab_proj));
|
||
if (on_fillet_radius_changed) on_fillet_radius_changed(m_fl_radius);
|
||
}
|
||
|
||
void DesignSketchTool::open_fillet_editor()
|
||
{
|
||
if (!on_inline_edit) return;
|
||
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
||
on_inline_edit(px, m_fl_radius, "",
|
||
[this](double v) {
|
||
m_fl_radius = std::max(0.01, v);
|
||
if (on_fillet_radius_changed) on_fillet_radius_changed(m_fl_radius);
|
||
},
|
||
[]() {});
|
||
}
|
||
|
||
// ---- Hole gizmo -----------------------------------------------------------------------------
|
||
// Positioned circular cut: footprint circle on the plane + a radial diameter arrow (plane u-axis)
|
||
// + a normal-axis depth arrow (drawn only when !through, matching the kernel's blind cut) + a
|
||
// draggable centre marker. The panel re-pushes this every preview, so (like the fillet gizmo) we
|
||
// must NOT reset an in-progress drag when already active.
|
||
void DesignSketchTool::set_hole_gizmo(const SketchPlane& plane, double x, double y,
|
||
double diameter, double depth, bool through)
|
||
{
|
||
m_hl_plane = plane;
|
||
m_hl_x = x;
|
||
m_hl_y = y;
|
||
m_hl_diameter = std::max(0.01, diameter);
|
||
m_hl_depth = std::max(0.01, depth);
|
||
m_hl_through = through;
|
||
if (!m_hl_active) m_hl_drag = -1; // re-pushed every preview: preserve an in-progress drag
|
||
m_hl_active = true;
|
||
}
|
||
|
||
void DesignSketchTool::clear_hole_gizmo()
|
||
{
|
||
m_hl_active = false;
|
||
m_hl_drag = -1;
|
||
}
|
||
|
||
void DesignSketchTool::set_hole_face_bounds(bool has, double umin, double umax, double vmin, double vmax)
|
||
{
|
||
m_hl_has_bounds = has;
|
||
m_hl_umin = umin; m_hl_umax = umax; m_hl_vmin = vmin; m_hl_vmax = vmax;
|
||
}
|
||
|
||
void DesignSketchTool::render_hole_gizmo()
|
||
{
|
||
if (!m_hl_active) return;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const Vec3d right = cam.get_dir_right().normalized();
|
||
const Vec3d up = cam.get_dir_up().normalized();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const double th = std::max(15.0 * upp, 1e-4);
|
||
|
||
const Vec3d centre = m_hl_plane.to_world(Vec2d(m_hl_x, m_hl_y));
|
||
const Vec3d nrm = m_hl_plane.normal.normalized();
|
||
const Vec3d ddir = m_hl_plane.x_axis.normalized(); // diameter arrow runs along plane u
|
||
const double r = std::max(0.01, m_hl_diameter * 0.5);
|
||
|
||
const SketchPlane saved = m_plane;
|
||
|
||
// (1) Footprint circle drawn ON the plane (lifts plane u/v -> world through to_world).
|
||
{
|
||
m_plane = m_hl_plane;
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
const int N = 48;
|
||
for (int i = 0; i < N; ++i) {
|
||
const double a0 = (2.0 * M_PI * i) / N, a1 = (2.0 * M_PI * (i + 1)) / N;
|
||
segs.emplace_back(Vec2d(m_hl_x + r * std::cos(a0), m_hl_y + r * std::sin(a0)),
|
||
Vec2d(m_hl_x + r * std::cos(a1), m_hl_y + r * std::sin(a1)));
|
||
}
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
draw_strokes(m_hl_stroke_model, segs, std::max(0.6 * upp, 1e-4), ColorRGBA(1.0f, 0.62f, 0.16f, 1.0f));
|
||
m_plane = saved;
|
||
}
|
||
|
||
// (2) Billboarded handles (centre marker + diameter arrow + depth arrow), screen-facing frame
|
||
// at the centre so draw_strokes/draw_text read on top regardless of orientation.
|
||
SketchPlane bb; bb.origin = centre; bb.x_axis = right; bb.y_axis = up; bb.normal = cam.get_dir_forward().normalized();
|
||
m_plane = bb;
|
||
const ColorRGBA amber(1.0f, 0.62f, 0.16f, 1.0f);
|
||
const ColorRGBA blue (0.30f, 0.55f, 1.0f, 1.0f);
|
||
|
||
auto arrow_to = [&](const Vec3d& tipw, const ColorRGBA& col, const DimAnnot& da) {
|
||
const Vec2d tip2((tipw - centre).dot(right), (tipw - centre).dot(up));
|
||
if (tip2.norm() <= 1e-6) return;
|
||
const Vec2d u = tip2.normalized();
|
||
const Vec2d nrm2(-u.y(), u.x());
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
segs.emplace_back(Vec2d(0, 0), tip2);
|
||
const double as = std::max(tip2.norm() * 0.20, th * 0.9);
|
||
const Vec2d back = tip2 - u * as;
|
||
segs.emplace_back(tip2, back + nrm2 * (as * 0.5));
|
||
segs.emplace_back(tip2, back - nrm2 * (as * 0.5));
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
draw_strokes(m_hl_stroke_model, segs, std::max(0.7 * upp, 1e-4), col);
|
||
draw_text(m_line_model, dim_text(da), tip2 + u * (th * 1.4), th, col);
|
||
};
|
||
|
||
// Diameter arrow: WYSIWYG radius, floored to a grabbable handle; label shows Ø (full diameter).
|
||
{
|
||
const double L = std::max(r, 40.0 * upp);
|
||
DimAnnot da; da.kind = DimType::Diameter; da.value = m_hl_diameter;
|
||
arrow_to(centre + ddir * L, amber, da);
|
||
}
|
||
// Depth arrow along +normal (blind cut). Through cuts ignore depth, so skip the arrow then.
|
||
if (!m_hl_through) {
|
||
const double L = std::max(m_hl_depth, 40.0 * upp);
|
||
DimAnnot da; da.kind = DimType::Distance; da.value = m_hl_depth;
|
||
arrow_to(centre + nrm * L, blue, da);
|
||
}
|
||
|
||
m_plane = saved;
|
||
|
||
// Move handle: a small 3D CUBE at the hole centre (Orca text/SVG-on-face feel) — grab and drag
|
||
// it to slide the hole across the face. Built from the plane axes so it sits flat on the face.
|
||
{
|
||
using EPT = GLModel::Geometry::EPrimitiveType;
|
||
using EVL = GLModel::Geometry::EVertexLayout;
|
||
const double hs = 9.0 * upp; // cube half-size (~9 px); hit-test uses the same below
|
||
const Vec3d U = ddir * hs, V = m_hl_plane.y_axis.normalized() * hs, Nn = nrm * hs;
|
||
// Cube centred EXACTLY on the surface point (= the hole). Depth-test ON occludes the inner
|
||
// half inside the solid, so the visible half-cube reads as planted at the hole — no depth-off
|
||
// float that looked offset from the on-surface footprint. Hit-test targets the same `centre`.
|
||
Vec3d c8[8];
|
||
for (int i = 0; i < 8; ++i)
|
||
c8[i] = centre + ((i & 1) ? U : -U) + ((i & 2) ? V : -V) + ((i & 4) ? Nn : -Nn);
|
||
// 6 faces (CCW), each as 2 triangles, lightly shaded so the box reads as a cube.
|
||
const int faces[6][4] = { {0,1,3,2},{4,6,7,5},{0,4,5,1},{2,3,7,6},{0,2,6,4},{1,5,7,3} };
|
||
const float shade[6] = { 0.78f, 1.0f, 0.86f, 0.92f, 0.70f, 0.96f };
|
||
glsafe(::glEnable(GL_DEPTH_TEST));
|
||
glsafe(::glDisable(GL_CULL_FACE));
|
||
for (int f = 0; f < 6; ++f) {
|
||
GLModel::Geometry g; g.format = { EPT::Triangles, EVL::P3 };
|
||
for (int k = 0; k < 4; ++k) g.add_vertex((Vec3f)c8[faces[f][k]].cast<float>());
|
||
g.add_triangle(0, 1, 2); g.add_triangle(0, 2, 3);
|
||
GLModel m; m.init_from(std::move(g));
|
||
m.set_color(ColorRGBA(1.0f * shade[f], 0.62f * shade[f], 0.16f * shade[f], 1.0f));
|
||
m.render();
|
||
}
|
||
}
|
||
}
|
||
|
||
// Best-matching hole handle under the cursor: centre (0) / diameter (1) / depth (2), or -1.
|
||
// Tested by ray-to-segment distance in world; ~12 px tolerance. Centre wins at the shared base.
|
||
int DesignSketchTool::hit_test_hole_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const
|
||
{
|
||
if (!m_hl_active) return -1;
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
|
||
const Vec3d centre = m_hl_plane.to_world(Vec2d(m_hl_x, m_hl_y));
|
||
const Vec3d nrm = m_hl_plane.normal.normalized();
|
||
const Vec3d ddir = m_hl_plane.x_axis.normalized();
|
||
const double rad = std::max(m_hl_diameter * 0.5, 40.0 * upp);
|
||
const double depL = std::max(m_hl_depth, 40.0 * upp);
|
||
const double tol = 12.0 * upp;
|
||
|
||
// Centre wins at the shared base: the move cube straddles the centre, so a grab within the cube
|
||
// half-size is a reposition. The arrows are only grabbable along the shaft extending outward,
|
||
// which also keeps an edge-on arrow (e.g. depth in top view) from stealing the reposition grab.
|
||
if (ray_segment_dist3(ro, rd, centre, centre) <= 9.0 * upp) return 0; // cube half-size
|
||
|
||
int best = -1; double bestd = tol;
|
||
const double dD = ray_segment_dist3(ro, rd, centre, centre + ddir * rad);
|
||
if (dD < bestd) { bestd = dD; best = 1; }
|
||
if (!m_hl_through) {
|
||
const double dZ = ray_segment_dist3(ro, rd, centre, centre + nrm * depL);
|
||
if (dZ < bestd) { bestd = dZ; best = 2; }
|
||
}
|
||
return best;
|
||
}
|
||
|
||
// Skew-line closest point of the mouse ray to an axis (anchor + t*dir) -> signed distance along
|
||
// dir. NaN when the camera is ~parallel to the axis (no meaningful projection).
|
||
double DesignSketchTool::hole_axis_proj(GLCanvas3D& canvas, const wxMouseEvent& evt,
|
||
const Vec3d& anchor, const Vec3d& dir) const
|
||
{
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||
const Vec3d e = dir;
|
||
const Vec3d w0 = anchor - ro;
|
||
const double a = e.dot(e), b = e.dot(rd), c = rd.dot(rd), dd = e.dot(w0), ee = rd.dot(w0);
|
||
const double denom = a * c - b * b;
|
||
// Relative near-parallel guard: when the camera ray is ~along the axis (e.g. the depth axis in
|
||
// top view) denom collapses; a tiny absolute floor lets a huge, unstable projection through.
|
||
if (std::abs(denom) < 1e-4 * std::max(a * c, 1e-12)) return std::nan("");
|
||
return (b * ee - c * dd) / denom;
|
||
}
|
||
|
||
void DesignSketchTool::start_hole_drag(GLCanvas3D& canvas, const wxMouseEvent& evt, int which)
|
||
{
|
||
m_hl_drag = which;
|
||
m_hl_press_x = evt.GetX();
|
||
m_hl_press_y = evt.GetY();
|
||
const Vec3d centre = m_hl_plane.to_world(Vec2d(m_hl_x, m_hl_y));
|
||
if (which == 0) {
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
m_hl_grab_uv = m_hl_plane.project(r.a, r.b - r.a);
|
||
m_hl_grab_x = m_hl_x;
|
||
m_hl_grab_y = m_hl_y;
|
||
} else if (which == 1 || which == 2) {
|
||
const Vec3d dir = (which == 1) ? m_hl_plane.x_axis.normalized() : m_hl_plane.normal.normalized();
|
||
const double p = hole_axis_proj(canvas, evt, centre, dir);
|
||
m_hl_grab_proj = std::isnan(p) ? 0.0 : p;
|
||
m_hl_grab_val = (which == 1) ? m_hl_diameter * 0.5 : m_hl_depth;
|
||
}
|
||
// which == 3/4 (X/Y dim labels) are edit-only: a stationary click opens the inline editor.
|
||
}
|
||
|
||
void DesignSketchTool::drag_hole_handle(GLCanvas3D& canvas, const wxMouseEvent& evt)
|
||
{
|
||
if (m_hl_drag >= 3) return; // X/Y dim labels are click-to-edit, not drag
|
||
if (m_hl_drag == 0) { // reposition centre on the plane
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec2d uv = m_hl_plane.project(r.a, r.b - r.a);
|
||
m_hl_x = m_hl_grab_x + (uv.x() - m_hl_grab_uv.x());
|
||
m_hl_y = m_hl_grab_y + (uv.y() - m_hl_grab_uv.y());
|
||
} else { // diameter (1) or depth (2): relative axis drag
|
||
const Vec3d centre = m_hl_plane.to_world(Vec2d(m_hl_x, m_hl_y));
|
||
const Vec3d dir = (m_hl_drag == 1) ? m_hl_plane.x_axis.normalized() : m_hl_plane.normal.normalized();
|
||
const double proj = hole_axis_proj(canvas, evt, centre, dir);
|
||
if (std::isnan(proj)) return; // camera ∥ axis: leave value as-is
|
||
const double v = std::max(0.01, m_hl_grab_val + (proj - m_hl_grab_proj));
|
||
if (m_hl_drag == 1) m_hl_diameter = 2.0 * v;
|
||
else m_hl_depth = v;
|
||
}
|
||
if (on_hole_changed) on_hole_changed(m_hl_x, m_hl_y, m_hl_diameter, m_hl_depth);
|
||
}
|
||
|
||
void DesignSketchTool::open_hole_editor(int which)
|
||
{
|
||
if (!on_inline_edit) return;
|
||
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
||
if (which == 1) {
|
||
on_inline_edit(px, m_hl_diameter, "",
|
||
[this](double v) {
|
||
m_hl_diameter = std::max(0.01, v);
|
||
if (on_hole_changed) on_hole_changed(m_hl_x, m_hl_y, m_hl_diameter, m_hl_depth);
|
||
},
|
||
[]() {});
|
||
} else if (which == 2) {
|
||
on_inline_edit(px, m_hl_depth, "",
|
||
[this](double v) {
|
||
m_hl_depth = std::max(0.01, v);
|
||
if (on_hole_changed) on_hole_changed(m_hl_x, m_hl_y, m_hl_diameter, m_hl_depth);
|
||
},
|
||
[]() {});
|
||
} else if (which == 3) { // #2 Part B: edit the distance from the u-side
|
||
const double ru = m_hl_has_bounds ? m_hl_umin : 0.0;
|
||
on_inline_edit(px, m_hl_x - ru, "",
|
||
[this, ru](double v) {
|
||
m_hl_x = ru + v;
|
||
if (on_hole_changed) on_hole_changed(m_hl_x, m_hl_y, m_hl_diameter, m_hl_depth);
|
||
},
|
||
[]() {});
|
||
} else if (which == 4) { // edit the distance from the v-side
|
||
const double rv = m_hl_has_bounds ? m_hl_vmin : 0.0;
|
||
on_inline_edit(px, m_hl_y - rv, "",
|
||
[this, rv](double v) {
|
||
m_hl_y = rv + v;
|
||
if (on_hole_changed) on_hole_changed(m_hl_x, m_hl_y, m_hl_diameter, m_hl_depth);
|
||
},
|
||
[]() {});
|
||
}
|
||
// which == 0 (centre): no scalar to edit inline — it's a drag-only reposition handle.
|
||
}
|
||
|
||
// ---- Thread gizmo ---------------------------------------------------------------------------
|
||
// Mirrors the hole gizmo: footprint circle on the plane at the nominal radius + a radial radius
|
||
// arrow (R label) + a normal-axis length arrow (always shown) + a draggable centre marker.
|
||
// Reuses hole_axis_proj() for the relative axis drags.
|
||
void DesignSketchTool::set_thread_gizmo(const SketchPlane& plane, double x, double y,
|
||
double radius, double height)
|
||
{
|
||
m_th_plane = plane;
|
||
m_th_x = x;
|
||
m_th_y = y;
|
||
m_th_radius = std::max(0.01, radius);
|
||
m_th_height = std::max(0.01, height);
|
||
if (!m_th_active) m_th_drag = -1; // re-pushed every preview: preserve an in-progress drag
|
||
m_th_active = true;
|
||
}
|
||
|
||
void DesignSketchTool::clear_thread_gizmo()
|
||
{
|
||
m_th_active = false;
|
||
m_th_drag = -1;
|
||
}
|
||
|
||
void DesignSketchTool::render_thread_gizmo()
|
||
{
|
||
if (!m_th_active) return;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const Vec3d right = cam.get_dir_right().normalized();
|
||
const Vec3d up = cam.get_dir_up().normalized();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const double th = std::max(15.0 * upp, 1e-4);
|
||
|
||
const Vec3d centre = m_th_plane.to_world(Vec2d(m_th_x, m_th_y));
|
||
const Vec3d nrm = m_th_plane.normal.normalized();
|
||
const Vec3d ddir = m_th_plane.x_axis.normalized(); // radius arrow runs along plane u
|
||
const double r = std::max(0.01, m_th_radius);
|
||
|
||
const SketchPlane saved = m_plane;
|
||
|
||
// (1) Footprint circle on the plane at the nominal radius.
|
||
{
|
||
m_plane = m_th_plane;
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
const int N = 48;
|
||
for (int i = 0; i < N; ++i) {
|
||
const double a0 = (2.0 * M_PI * i) / N, a1 = (2.0 * M_PI * (i + 1)) / N;
|
||
segs.emplace_back(Vec2d(m_th_x + r * std::cos(a0), m_th_y + r * std::sin(a0)),
|
||
Vec2d(m_th_x + r * std::cos(a1), m_th_y + r * std::sin(a1)));
|
||
}
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
draw_strokes(m_th_stroke_model, segs, std::max(0.6 * upp, 1e-4), ColorRGBA(0.55f, 0.80f, 0.30f, 1.0f));
|
||
m_plane = saved;
|
||
}
|
||
|
||
// (2) Billboarded handles.
|
||
SketchPlane bb; bb.origin = centre; bb.x_axis = right; bb.y_axis = up; bb.normal = cam.get_dir_forward().normalized();
|
||
m_plane = bb;
|
||
const ColorRGBA green(0.55f, 0.80f, 0.30f, 1.0f);
|
||
const ColorRGBA blue (0.30f, 0.55f, 1.0f, 1.0f);
|
||
|
||
auto arrow_to = [&](const Vec3d& tipw, const ColorRGBA& col, const DimAnnot& da) {
|
||
const Vec2d tip2((tipw - centre).dot(right), (tipw - centre).dot(up));
|
||
if (tip2.norm() <= 1e-6) return;
|
||
const Vec2d u = tip2.normalized();
|
||
const Vec2d nrm2(-u.y(), u.x());
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
segs.emplace_back(Vec2d(0, 0), tip2);
|
||
const double as = std::max(tip2.norm() * 0.20, th * 0.9);
|
||
const Vec2d back = tip2 - u * as;
|
||
segs.emplace_back(tip2, back + nrm2 * (as * 0.5));
|
||
segs.emplace_back(tip2, back - nrm2 * (as * 0.5));
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
draw_strokes(m_th_stroke_model, segs, std::max(0.7 * upp, 1e-4), col);
|
||
draw_text(m_line_model, dim_text(da), tip2 + u * (th * 1.4), th, col);
|
||
};
|
||
|
||
{ // Radius arrow (R label), floored to a grabbable handle.
|
||
const double L = std::max(r, 40.0 * upp);
|
||
DimAnnot da; da.kind = DimType::Radius; da.value = m_th_radius;
|
||
arrow_to(centre + ddir * L, green, da);
|
||
}
|
||
{ // Length arrow along +normal.
|
||
const double L = std::max(m_th_height, 40.0 * upp);
|
||
DimAnnot da; da.kind = DimType::Distance; da.value = m_th_height;
|
||
arrow_to(centre + nrm * L, blue, da);
|
||
}
|
||
{ // Centre marker.
|
||
const double s = 7.0 * upp;
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
segs.emplace_back(Vec2d(-s, -s), Vec2d(s, -s));
|
||
segs.emplace_back(Vec2d( s, -s), Vec2d(s, s));
|
||
segs.emplace_back(Vec2d( s, s), Vec2d(-s, s));
|
||
segs.emplace_back(Vec2d(-s, s), Vec2d(-s, -s));
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
draw_strokes(m_th_stroke_model, segs, std::max(0.7 * upp, 1e-4), green);
|
||
}
|
||
|
||
m_plane = saved;
|
||
}
|
||
|
||
int DesignSketchTool::hit_test_thread_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const
|
||
{
|
||
if (!m_th_active) return -1;
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
|
||
const Vec3d centre = m_th_plane.to_world(Vec2d(m_th_x, m_th_y));
|
||
const Vec3d nrm = m_th_plane.normal.normalized();
|
||
const Vec3d ddir = m_th_plane.x_axis.normalized();
|
||
const double rad = std::max(m_th_radius, 40.0 * upp);
|
||
const double hL = std::max(m_th_height, 40.0 * upp);
|
||
const double tol = 12.0 * upp;
|
||
|
||
if (ray_segment_dist3(ro, rd, centre, centre) <= tol) return 0; // centre wins at the base
|
||
int best = -1; double bestd = tol;
|
||
const double dR = ray_segment_dist3(ro, rd, centre, centre + ddir * rad);
|
||
if (dR < bestd) { bestd = dR; best = 1; }
|
||
const double dH = ray_segment_dist3(ro, rd, centre, centre + nrm * hL);
|
||
if (dH < bestd) { bestd = dH; best = 2; }
|
||
return best;
|
||
}
|
||
|
||
void DesignSketchTool::start_thread_drag(GLCanvas3D& canvas, const wxMouseEvent& evt, int which)
|
||
{
|
||
m_th_drag = which;
|
||
m_th_press_x = evt.GetX();
|
||
m_th_press_y = evt.GetY();
|
||
const Vec3d centre = m_th_plane.to_world(Vec2d(m_th_x, m_th_y));
|
||
if (which == 0) {
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
m_th_grab_uv = m_th_plane.project(r.a, r.b - r.a);
|
||
m_th_grab_x = m_th_x;
|
||
m_th_grab_y = m_th_y;
|
||
} else {
|
||
const Vec3d dir = (which == 1) ? m_th_plane.x_axis.normalized() : m_th_plane.normal.normalized();
|
||
const double p = hole_axis_proj(canvas, evt, centre, dir);
|
||
m_th_grab_proj = std::isnan(p) ? 0.0 : p;
|
||
m_th_grab_val = (which == 1) ? m_th_radius : m_th_height;
|
||
}
|
||
}
|
||
|
||
void DesignSketchTool::drag_thread_handle(GLCanvas3D& canvas, const wxMouseEvent& evt)
|
||
{
|
||
if (m_th_drag == 0) {
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec2d uv = m_th_plane.project(r.a, r.b - r.a);
|
||
m_th_x = m_th_grab_x + (uv.x() - m_th_grab_uv.x());
|
||
m_th_y = m_th_grab_y + (uv.y() - m_th_grab_uv.y());
|
||
} else {
|
||
const Vec3d centre = m_th_plane.to_world(Vec2d(m_th_x, m_th_y));
|
||
const Vec3d dir = (m_th_drag == 1) ? m_th_plane.x_axis.normalized() : m_th_plane.normal.normalized();
|
||
const double proj = hole_axis_proj(canvas, evt, centre, dir);
|
||
if (std::isnan(proj)) return;
|
||
const double v = std::max(0.01, m_th_grab_val + (proj - m_th_grab_proj));
|
||
if (m_th_drag == 1) m_th_radius = v;
|
||
else m_th_height = v;
|
||
}
|
||
if (on_thread_changed) on_thread_changed(m_th_x, m_th_y, m_th_radius, m_th_height);
|
||
}
|
||
|
||
void DesignSketchTool::open_thread_editor(int which)
|
||
{
|
||
if (!on_inline_edit) return;
|
||
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
||
if (which == 1) {
|
||
on_inline_edit(px, m_th_radius, "",
|
||
[this](double v) {
|
||
m_th_radius = std::max(0.01, v);
|
||
if (on_thread_changed) on_thread_changed(m_th_x, m_th_y, m_th_radius, m_th_height);
|
||
},
|
||
[]() {});
|
||
} else if (which == 2) {
|
||
on_inline_edit(px, m_th_height, "",
|
||
[this](double v) {
|
||
m_th_height = std::max(0.01, v);
|
||
if (on_thread_changed) on_thread_changed(m_th_x, m_th_y, m_th_radius, m_th_height);
|
||
},
|
||
[]() {});
|
||
}
|
||
}
|
||
|
||
// ---- Shell gizmo ----------------------------------------------------------------------------
|
||
// A single inward thickness arrow at the picked open-face centroid (mirrors the fillet radius
|
||
// arrow). RELATIVE drag (like fillet), reusing hole_axis_proj for the projection.
|
||
void DesignSketchTool::set_shell_gizmo(const Vec3d& face_centroid, const Vec3d& inward_dir,
|
||
double thickness)
|
||
{
|
||
m_sh_anchor = face_centroid;
|
||
if (inward_dir.norm() > 1e-9) m_sh_dir = inward_dir.normalized();
|
||
m_sh_thickness = std::max(0.01, thickness);
|
||
if (!m_sh_active) m_sh_drag = false; // re-pushed every preview: preserve an in-progress drag
|
||
m_sh_active = true;
|
||
}
|
||
|
||
void DesignSketchTool::clear_shell_gizmo()
|
||
{
|
||
m_sh_active = false;
|
||
m_sh_drag = false;
|
||
}
|
||
|
||
void DesignSketchTool::render_shell_gizmo()
|
||
{
|
||
if (!m_sh_active) return;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const Vec3d right = cam.get_dir_right().normalized();
|
||
const Vec3d up = cam.get_dir_up().normalized();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const double th = std::max(15.0 * upp, 1e-4);
|
||
const double L = std::max(m_sh_thickness, 40.0 * upp); // WYSIWYG, floored to a handle
|
||
const Vec3d tipw = m_sh_anchor + m_sh_dir * L;
|
||
|
||
const SketchPlane saved = m_plane;
|
||
SketchPlane bb; bb.origin = m_sh_anchor; bb.x_axis = right; bb.y_axis = up; bb.normal = cam.get_dir_forward().normalized();
|
||
m_plane = bb;
|
||
const ColorRGBA teal(0.20f, 0.80f, 0.75f, 1.0f);
|
||
|
||
const Vec2d tip2((tipw - m_sh_anchor).dot(right), (tipw - m_sh_anchor).dot(up));
|
||
if (tip2.norm() > 1e-6) {
|
||
const Vec2d u = tip2.normalized();
|
||
const Vec2d nrm(-u.y(), u.x());
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
segs.emplace_back(Vec2d(0, 0), tip2);
|
||
const double as = std::max(tip2.norm() * 0.20, th * 0.9);
|
||
const Vec2d back = tip2 - u * as;
|
||
segs.emplace_back(tip2, back + nrm * (as * 0.5));
|
||
segs.emplace_back(tip2, back - nrm * (as * 0.5));
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
draw_strokes(m_sh_stroke_model, segs, std::max(0.7 * upp, 1e-4), teal);
|
||
DimAnnot da; da.kind = DimType::Distance; da.value = m_sh_thickness;
|
||
draw_text(m_line_model, dim_text(da), tip2 + u * (th * 1.4), th, teal);
|
||
}
|
||
m_plane = saved;
|
||
}
|
||
|
||
bool DesignSketchTool::hit_test_shell_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt) const
|
||
{
|
||
if (!m_sh_active) return false;
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const double L = std::max(m_sh_thickness, 40.0 * upp);
|
||
return ray_segment_dist3(ro, rd, m_sh_anchor, m_sh_anchor + m_sh_dir * L) <= 12.0 * upp;
|
||
}
|
||
|
||
void DesignSketchTool::start_shell_drag(GLCanvas3D& canvas, const wxMouseEvent& evt)
|
||
{
|
||
m_sh_drag = true;
|
||
m_sh_press_x = evt.GetX();
|
||
m_sh_press_y = evt.GetY();
|
||
m_sh_grab_val = m_sh_thickness;
|
||
const double p = hole_axis_proj(canvas, evt, m_sh_anchor, m_sh_dir);
|
||
m_sh_grab_proj = std::isnan(p) ? 0.0 : p;
|
||
}
|
||
|
||
void DesignSketchTool::drag_shell_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt)
|
||
{
|
||
const double proj = hole_axis_proj(canvas, evt, m_sh_anchor, m_sh_dir);
|
||
if (std::isnan(proj)) return;
|
||
m_sh_thickness = std::max(0.01, m_sh_grab_val + (proj - m_sh_grab_proj));
|
||
if (on_shell_thickness_changed) on_shell_thickness_changed(m_sh_thickness);
|
||
}
|
||
|
||
void DesignSketchTool::open_shell_editor()
|
||
{
|
||
if (!on_inline_edit) return;
|
||
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
||
on_inline_edit(px, m_sh_thickness, "",
|
||
[this](double v) {
|
||
m_sh_thickness = std::max(0.01, v);
|
||
if (on_shell_thickness_changed) on_shell_thickness_changed(m_sh_thickness);
|
||
},
|
||
[]() {});
|
||
}
|
||
|
||
// ---- Revolve angle-arc gizmo ----------------------------------------------------------------
|
||
// Arc in the revolve plane (perpendicular to the axis) at the profile's radius. The arc center is
|
||
// the projection of the profile centroid onto the axis, so the arc rides at the profile's height.
|
||
// The yaxis sense follows m_rv_flip, matching the kernel's negative-angle reversed sweep.
|
||
static Vec3d rv_yaxis(const Vec3d& axis, const Vec3d& ref, bool flip)
|
||
{
|
||
Vec3d y = axis.cross(ref);
|
||
if (y.norm() < 1e-9) return ref; // degenerate; never used (ref ⟂ axis by construction)
|
||
y.normalize();
|
||
return flip ? -y : y;
|
||
}
|
||
|
||
// Arc in the draft plane (perpendicular to the world +Z axis through the face centroid).
|
||
// The arc sweeps from angle 0 to m_dr_angle, representing the taper amount.
|
||
static Vec3d dr_yaxis(const Vec3d& axis, const Vec3d& ref)
|
||
{
|
||
Vec3d y = axis.cross(ref);
|
||
if (y.norm() < 1e-9) return ref;
|
||
y.normalize();
|
||
return y;
|
||
}
|
||
|
||
void DesignSketchTool::set_draft_gizmo(const Vec3d& face_centroid, const Vec3d& face_normal, double angle)
|
||
{
|
||
m_dr_center = face_centroid;
|
||
m_dr_angle = std::min(89.0, std::max(-89.0, angle));
|
||
m_dr_axis = Vec3d::UnitZ(); // pull direction is world +Z
|
||
Vec3d ref = face_normal - face_normal.dot(m_dr_axis) * m_dr_axis; // horizontal component
|
||
if (ref.norm() < 1e-6) ref = Vec3d::UnitX(); // fallback: face normal is parallel to Z
|
||
m_dr_ref = ref / ref.norm();
|
||
m_dr_radius = 12.0; // fixed world-size manipulator
|
||
if (!m_dr_active) m_dr_drag = false;
|
||
m_dr_active = true;
|
||
}
|
||
|
||
void DesignSketchTool::clear_draft_gizmo()
|
||
{
|
||
m_dr_active = false;
|
||
m_dr_drag = false;
|
||
}
|
||
|
||
void DesignSketchTool::render_draft_gizmo()
|
||
{
|
||
if (!m_dr_active) return;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const double th = std::max(15.0 * upp, 1e-4);
|
||
const Vec3d yax = dr_yaxis(m_dr_axis, m_dr_ref);
|
||
|
||
const SketchPlane saved = m_plane;
|
||
SketchPlane rp; rp.origin = m_dr_center; rp.x_axis = m_dr_ref; rp.y_axis = yax; rp.normal = m_dr_axis;
|
||
m_plane = rp;
|
||
const ColorRGBA arcc(0.15f, 0.92f, 1.0f, 1.0f);
|
||
const double r = m_dr_radius;
|
||
// Draft angle can be negative: sweep counter-clockwise (positive) or clockwise (negative)
|
||
const double a = m_dr_angle * M_PI / 180.0;
|
||
const int N = std::max(8, int(std::abs(a) / (M_PI / 32.0))); // ~ every 5.6°
|
||
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
Vec2d prev(r, 0.0);
|
||
for (int i = 1; i <= N; ++i) {
|
||
const double t = a * double(i) / double(N);
|
||
const Vec2d cur(r * std::cos(t), r * std::sin(t));
|
||
segs.emplace_back(prev, cur);
|
||
prev = cur;
|
||
}
|
||
const Vec2d tip(r * std::cos(a), r * std::sin(a));
|
||
segs.emplace_back(Vec2d(0, 0), Vec2d(r, 0)); // spoke at angle 0
|
||
segs.emplace_back(Vec2d(0, 0), tip); // spoke at the swept angle (the handle)
|
||
const Vec2d tang(-std::sin(a), std::cos(a));
|
||
const Vec2d radial = tip.normalized();
|
||
const double as = std::max(r * 0.14, th);
|
||
segs.emplace_back(tip, tip - tang * as - radial * (as * 0.5));
|
||
segs.emplace_back(tip, tip - tang * as + radial * (as * 0.5));
|
||
const double hs = std::max(th * 0.8, r * 0.05);
|
||
const Vec2d du = radial * hs, dv = Vec2d(-radial.y(), radial.x()) * hs;
|
||
segs.emplace_back(tip + du, tip + dv);
|
||
segs.emplace_back(tip + dv, tip - du);
|
||
segs.emplace_back(tip - du, tip - dv);
|
||
segs.emplace_back(tip - dv, tip + du);
|
||
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
draw_strokes(m_dr_stroke_model, segs, std::max(0.8 * upp, 1e-4), arcc);
|
||
DimAnnot da; da.kind = DimType::Angle; da.value = m_dr_angle;
|
||
draw_text(m_line_model, dim_text(da), tip * 1.14, th, arcc);
|
||
m_plane = saved;
|
||
}
|
||
|
||
bool DesignSketchTool::hit_test_draft_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const
|
||
{
|
||
if (!m_dr_active) return false;
|
||
const Linef3 ray = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = ray.a, rd = ray.b - ray.a;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const Vec3d yax = dr_yaxis(m_dr_axis, m_dr_ref);
|
||
const double a = m_dr_angle * M_PI / 180.0;
|
||
const double tol = 16.0 * upp;
|
||
auto ray_pt = [&](const Vec3d& p) {
|
||
const double t = (p - ro).dot(rd) / std::max(rd.dot(rd), 1e-12);
|
||
return (p - (ro + t * rd)).norm();
|
||
};
|
||
const Vec3d tip = m_dr_center + m_dr_radius * (std::cos(a) * m_dr_ref + std::sin(a) * yax);
|
||
const Vec3d ref = m_dr_center + m_dr_radius * m_dr_ref; // angle-0 spoke end
|
||
const int N = 48;
|
||
for (int i = 0; i <= N; ++i) {
|
||
const double f = double(i) / double(N);
|
||
const double th = a * f;
|
||
const Vec3d arc = m_dr_center + m_dr_radius * (std::cos(th) * m_dr_ref + std::sin(th) * yax);
|
||
if (ray_pt(arc) <= tol) return true;
|
||
if (ray_pt(m_dr_center + f * (tip - m_dr_center)) <= tol) return true;
|
||
if (ray_pt(m_dr_center + f * (ref - m_dr_center)) <= tol) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void DesignSketchTool::drag_draft_arc(GLCanvas3D& canvas, const wxMouseEvent& evt)
|
||
{
|
||
const Linef3 ray = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = ray.a, rd = ray.b - ray.a;
|
||
const double denom = rd.dot(m_dr_axis);
|
||
if (std::abs(denom) < 1e-9) return;
|
||
const double t = (m_dr_center - ro).dot(m_dr_axis) / denom;
|
||
const Vec3d p = ro + t * rd;
|
||
const Vec3d yax = dr_yaxis(m_dr_axis, m_dr_ref);
|
||
const double u = (p - m_dr_center).dot(m_dr_ref);
|
||
const double v = (p - m_dr_center).dot(yax);
|
||
double deg = std::atan2(v, u) * 180.0 / M_PI;
|
||
deg = std::min(89.0, std::max(-89.0, deg));
|
||
m_dr_angle = deg;
|
||
if (m_on_draft_angle_changed) m_on_draft_angle_changed(deg);
|
||
}
|
||
|
||
// ---- Cut gizmo (plane normal arrow + wire rectangle preview) --------------------------------
|
||
// Arrow: shaft + arrowhead billboarded along the cut-plane normal from the projected body
|
||
// centre, with a signed Distance label = offset. Drag is RELATIVE via hole_axis_proj.
|
||
// Rectangle: 4 segments in the cut plane at the current offset, sized to the target body.
|
||
|
||
void DesignSketchTool::set_cut_gizmo(const SketchPlane& plane, double offset, const Vec3d& body_center, double half_extent)
|
||
{
|
||
m_ct_n = plane.normal.normalized();
|
||
m_ct_u = plane.x_axis.normalized();
|
||
m_ct_v = plane.y_axis.normalized();
|
||
Vec3d rel = body_center - plane.origin;
|
||
m_ct_base = plane.origin + (rel - rel.dot(m_ct_n) * m_ct_n); // body center projected into the cut plane
|
||
m_ct_offset = offset;
|
||
m_ct_half = std::max(half_extent, 10.0);
|
||
if (!m_ct_active) m_ct_drag = false;
|
||
m_ct_active = true;
|
||
}
|
||
|
||
void DesignSketchTool::clear_cut_gizmo()
|
||
{
|
||
m_ct_active = false;
|
||
m_ct_drag = false;
|
||
}
|
||
|
||
void DesignSketchTool::render_cut_gizmo()
|
||
{
|
||
if (!m_ct_active) return;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const Vec3d right = cam.get_dir_right().normalized();
|
||
const Vec3d up = cam.get_dir_up().normalized();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const double th = std::max(15.0 * upp, 1e-4);
|
||
const ColorRGBA teal(0.20f, 0.80f, 0.75f, 1.0f);
|
||
|
||
// ---- Wire rectangle in the cut plane ----
|
||
{
|
||
const Vec3d cutpos = m_ct_base + m_ct_offset * m_ct_n;
|
||
const SketchPlane saved = m_plane;
|
||
SketchPlane rp; rp.origin = cutpos; rp.x_axis = m_ct_u; rp.y_axis = m_ct_v; rp.normal = m_ct_n;
|
||
m_plane = rp;
|
||
const double h = m_ct_half;
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
segs.emplace_back(Vec2d(-h, -h), Vec2d( h, -h));
|
||
segs.emplace_back(Vec2d( h, -h), Vec2d( h, h));
|
||
segs.emplace_back(Vec2d( h, h), Vec2d(-h, h));
|
||
segs.emplace_back(Vec2d(-h, h), Vec2d(-h, -h));
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
draw_strokes(m_ct_rect_model, segs, std::max(0.7 * upp, 1e-4), teal);
|
||
m_plane = saved;
|
||
}
|
||
|
||
// ---- Offset arrow (billboarded, clone of render_shell_gizmo) ----
|
||
{
|
||
const double L_sign = (std::abs(m_ct_offset) < 40.0 * upp)
|
||
? std::copysign(40.0 * upp, (m_ct_offset == 0.0 ? 1.0 : m_ct_offset))
|
||
: m_ct_offset;
|
||
const Vec3d tipw = m_ct_base + m_ct_n * L_sign;
|
||
|
||
const SketchPlane saved = m_plane;
|
||
SketchPlane bb; bb.origin = m_ct_base; bb.x_axis = right; bb.y_axis = up;
|
||
bb.normal = cam.get_dir_forward().normalized();
|
||
m_plane = bb;
|
||
|
||
const Vec2d tip2((tipw - m_ct_base).dot(right), (tipw - m_ct_base).dot(up));
|
||
if (tip2.norm() > 1e-6) {
|
||
const Vec2d u = tip2.normalized();
|
||
const Vec2d nrm(-u.y(), u.x());
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
segs.emplace_back(Vec2d(0, 0), tip2);
|
||
const double as = std::max(tip2.norm() * 0.20, th * 0.9);
|
||
const Vec2d back = tip2 - u * as;
|
||
segs.emplace_back(tip2, back + nrm * (as * 0.5));
|
||
segs.emplace_back(tip2, back - nrm * (as * 0.5));
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
draw_strokes(m_ct_stroke_model, segs, std::max(0.7 * upp, 1e-4), teal);
|
||
DimAnnot da; da.kind = DimType::Distance; da.value = m_ct_offset;
|
||
draw_text(m_line_model, dim_text(da), tip2 + u * (th * 1.4), th, teal);
|
||
}
|
||
m_plane = saved;
|
||
}
|
||
}
|
||
|
||
bool DesignSketchTool::hit_test_cut_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt) const
|
||
{
|
||
if (!m_ct_active) return false;
|
||
const Linef3 r = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = r.a, rd = r.b - r.a;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const double L_sign = (std::abs(m_ct_offset) < 40.0 * upp)
|
||
? std::copysign(40.0 * upp, (m_ct_offset == 0.0 ? 1.0 : m_ct_offset))
|
||
: m_ct_offset;
|
||
return ray_segment_dist3(ro, rd, m_ct_base, m_ct_base + m_ct_n * L_sign) <= 12.0 * upp;
|
||
}
|
||
|
||
void DesignSketchTool::start_cut_drag(GLCanvas3D& canvas, const wxMouseEvent& evt)
|
||
{
|
||
m_ct_drag = true;
|
||
m_ct_grab_val = m_ct_offset;
|
||
const double p = hole_axis_proj(canvas, evt, m_ct_base, m_ct_n);
|
||
m_ct_grab_proj = std::isnan(p) ? 0.0 : p;
|
||
}
|
||
|
||
void DesignSketchTool::drag_cut_arrow(GLCanvas3D& canvas, const wxMouseEvent& evt)
|
||
{
|
||
const double proj = hole_axis_proj(canvas, evt, m_ct_base, m_ct_n);
|
||
if (std::isnan(proj)) return;
|
||
m_ct_offset = m_ct_grab_val + (proj - m_ct_grab_proj);
|
||
if (m_on_cut_offset_changed) m_on_cut_offset_changed(m_ct_offset);
|
||
}
|
||
|
||
void DesignSketchTool::set_revolve_gizmo(const SketchPlane& plane, const Vec2d& centroid,
|
||
int axis_sel, double angle, bool flip)
|
||
{
|
||
const Vec3d ax = (axis_sel == 1 ? plane.y_axis : plane.x_axis).normalized();
|
||
const Vec3d cw = plane.to_world(centroid);
|
||
const double axial = (cw - plane.origin).dot(ax);
|
||
m_rv_center = plane.origin + axial * ax; // foot of the centroid on the axis line
|
||
Vec3d ref = cw - m_rv_center; // perpendicular to ax by construction
|
||
double r = ref.norm();
|
||
if (r < 1e-6) { ref = plane.normal.normalized(); r = std::max(plane.normal.norm(), 1.0); }
|
||
m_rv_axis = ax;
|
||
m_rv_ref = ref / ref.norm();
|
||
m_rv_radius = std::max(r, 1.0);
|
||
m_rv_angle = std::min(360.0, std::max(1.0, std::abs(angle)));
|
||
m_rv_flip = flip;
|
||
if (!m_rv_active) m_rv_drag = false; // re-pushed every preview: keep an in-progress drag
|
||
m_rv_active = true;
|
||
}
|
||
|
||
void DesignSketchTool::clear_revolve_gizmo()
|
||
{
|
||
m_rv_active = false;
|
||
m_rv_drag = false;
|
||
}
|
||
|
||
void DesignSketchTool::render_revolve_gizmo()
|
||
{
|
||
if (!m_rv_active) return;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const double th = std::max(15.0 * upp, 1e-4);
|
||
const Vec3d yax = rv_yaxis(m_rv_axis, m_rv_ref, m_rv_flip);
|
||
|
||
// The arc lives in the true revolve plane (NOT billboarded) so the sweep reads in 3D.
|
||
const SketchPlane saved = m_plane;
|
||
SketchPlane rp; rp.origin = m_rv_center; rp.x_axis = m_rv_ref; rp.y_axis = yax; rp.normal = m_rv_axis;
|
||
m_plane = rp;
|
||
// Vivid cyan: the revolve ghost is amber, so the arc manipulator must contrast with it
|
||
// (it overlaps the solid, unlike the extrude depth-arrow which points away into empty space).
|
||
const ColorRGBA arcc(0.15f, 0.92f, 1.0f, 1.0f);
|
||
const double r = m_rv_radius;
|
||
const double a = m_rv_angle * M_PI / 180.0;
|
||
const int N = std::max(8, int(a / (M_PI / 32.0))); // ~ every 5.6°
|
||
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
Vec2d prev(r, 0.0);
|
||
for (int i = 1; i <= N; ++i) {
|
||
const double t = a * double(i) / double(N);
|
||
const Vec2d cur(r * std::cos(t), r * std::sin(t));
|
||
segs.emplace_back(prev, cur);
|
||
prev = cur;
|
||
}
|
||
const Vec2d tip(r * std::cos(a), r * std::sin(a));
|
||
segs.emplace_back(Vec2d(0, 0), Vec2d(r, 0)); // spoke at angle 0
|
||
segs.emplace_back(Vec2d(0, 0), tip); // spoke at the swept angle (the handle)
|
||
// Arrowhead at the tip, pointing along the sweep tangent (-sin,cos) rotated by a.
|
||
const Vec2d tang(-std::sin(a), std::cos(a));
|
||
const Vec2d radial = tip.normalized();
|
||
const double as = std::max(r * 0.14, th);
|
||
segs.emplace_back(tip, tip - tang * as - radial * (as * 0.5));
|
||
segs.emplace_back(tip, tip - tang * as + radial * (as * 0.5));
|
||
// A diamond grab-handle at the tip so the draggable target is unmistakable.
|
||
const double hs = std::max(th * 0.8, r * 0.05);
|
||
const Vec2d du = radial * hs, dv = Vec2d(-radial.y(), radial.x()) * hs;
|
||
segs.emplace_back(tip + du, tip + dv);
|
||
segs.emplace_back(tip + dv, tip - du);
|
||
segs.emplace_back(tip - du, tip - dv);
|
||
segs.emplace_back(tip - dv, tip + du);
|
||
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
draw_strokes(m_rv_stroke_model, segs, std::max(0.8 * upp, 1e-4), arcc);
|
||
DimAnnot da; da.kind = DimType::Angle; da.value = m_rv_angle;
|
||
draw_text(m_line_model, dim_text(da), tip * 1.14, th, arcc);
|
||
m_plane = saved;
|
||
}
|
||
|
||
bool DesignSketchTool::hit_test_revolve_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const
|
||
{
|
||
if (!m_rv_active) return false;
|
||
const Linef3 ray = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = ray.a, rd = ray.b - ray.a;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const Vec3d yax = rv_yaxis(m_rv_axis, m_rv_ref, m_rv_flip);
|
||
const double a = m_rv_angle * M_PI / 180.0;
|
||
const double tol = 16.0 * upp;
|
||
// Grab anywhere on the whole gizmo (Onshape-style): the arc curve OR either radial spoke.
|
||
// Take the nearest ray-to-point distance over a dense sampling.
|
||
auto ray_pt = [&](const Vec3d& p) {
|
||
const double t = (p - ro).dot(rd) / std::max(rd.dot(rd), 1e-12);
|
||
return (p - (ro + t * rd)).norm();
|
||
};
|
||
const Vec3d tip = m_rv_center + m_rv_radius * (std::cos(a) * m_rv_ref + std::sin(a) * yax);
|
||
const Vec3d ref = m_rv_center + m_rv_radius * m_rv_ref; // angle-0 spoke end
|
||
const int N = 48;
|
||
for (int i = 0; i <= N; ++i) {
|
||
const double f = double(i) / double(N);
|
||
const double th = a * f;
|
||
const Vec3d arc = m_rv_center + m_rv_radius * (std::cos(th) * m_rv_ref + std::sin(th) * yax);
|
||
if (ray_pt(arc) <= tol) return true; // on the arc curve
|
||
if (ray_pt(m_rv_center + f * (tip - m_rv_center)) <= tol) return true; // on the swept spoke
|
||
if (ray_pt(m_rv_center + f * (ref - m_rv_center)) <= tol) return true; // on the ref spoke
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Intersect the mouse ray with the revolve plane, read its angle around the center -> sweep angle.
|
||
void DesignSketchTool::drag_revolve_arc(GLCanvas3D& canvas, const wxMouseEvent& evt)
|
||
{
|
||
const Linef3 ray = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = ray.a, rd = ray.b - ray.a;
|
||
const double denom = rd.dot(m_rv_axis);
|
||
if (std::abs(denom) < 1e-9) return; // ray ∥ revolve plane: leave angle as-is
|
||
const double t = (m_rv_center - ro).dot(m_rv_axis) / denom;
|
||
const Vec3d p = ro + t * rd;
|
||
const Vec3d yax = rv_yaxis(m_rv_axis, m_rv_ref, m_rv_flip);
|
||
const double u = (p - m_rv_center).dot(m_rv_ref);
|
||
const double v = (p - m_rv_center).dot(yax);
|
||
double deg = std::atan2(v, u) * 180.0 / M_PI;
|
||
if (deg < 0.0) deg += 360.0;
|
||
deg = std::min(360.0, std::max(1.0, deg));
|
||
m_rv_angle = deg;
|
||
if (on_revolve_angle_changed) on_revolve_angle_changed(deg);
|
||
}
|
||
|
||
void DesignSketchTool::open_revolve_editor()
|
||
{
|
||
if (!on_inline_edit) return;
|
||
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
||
on_inline_edit(px, m_rv_angle, "",
|
||
[this](double v) {
|
||
m_rv_angle = std::min(360.0, std::max(1.0, v));
|
||
if (on_revolve_angle_changed) on_revolve_angle_changed(m_rv_angle);
|
||
},
|
||
[]() {});
|
||
}
|
||
|
||
// ---- Pattern gizmo (linear spacing arrow | circular angle-arc) ------------------------------
|
||
// Linear: a 3D arrow along the world march axis with a tick at each copy; the diamond at the end
|
||
// drags the spacing. Circular: a Revolve-style arc about the plane normal through the plane origin.
|
||
void DesignSketchTool::set_pattern_gizmo(const SketchPlane& plane, const Vec3d& body_centroid,
|
||
bool circular, int count, int dir, double spacing, double angle)
|
||
{
|
||
m_pt_circular = circular;
|
||
m_pt_base = body_centroid;
|
||
m_pt_count = std::max(1, count);
|
||
m_pt_spacing = std::max(0.01, spacing);
|
||
m_pt_angle = std::min(360.0, std::max(1.0, angle));
|
||
m_pt_dirw = (dir == 1 ? plane.y_axis : plane.x_axis).normalized();
|
||
m_pt_origin = plane.origin;
|
||
m_pt_normal = plane.normal.normalized();
|
||
// Circular arc center = foot of the body centroid on the rotation axis; ref = perpendicular dir.
|
||
const double axial = (body_centroid - plane.origin).dot(m_pt_normal);
|
||
m_pt_ccenter = plane.origin + axial * m_pt_normal;
|
||
Vec3d ref = body_centroid - m_pt_ccenter;
|
||
double r = ref.norm();
|
||
if (r < 1e-6) { ref = m_pt_dirw; r = 1.0; } // body centred on the axis: nominal radius
|
||
m_pt_cref = ref / ref.norm();
|
||
m_pt_radius = std::max(r, 1.0);
|
||
if (!m_pt_active) m_pt_drag = false; // re-pushed every preview: keep an in-progress drag
|
||
m_pt_active = true;
|
||
}
|
||
|
||
void DesignSketchTool::clear_pattern_gizmo()
|
||
{
|
||
m_pt_active = false;
|
||
m_pt_drag = false;
|
||
}
|
||
|
||
// Linear arrow span = spacing*(count-1), at least one step so count=1 still shows a direction.
|
||
static double pt_linear_len(double spacing, int count)
|
||
{
|
||
return std::max(spacing * double(std::max(1, count) - 1), spacing);
|
||
}
|
||
|
||
void DesignSketchTool::render_pattern_gizmo()
|
||
{
|
||
if (!m_pt_active) return;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
const double th = std::max(15.0 * upp, 1e-4);
|
||
const ColorRGBA col(0.15f, 0.92f, 1.0f, 1.0f); // vivid cyan, matches the gizmo family
|
||
const SketchPlane saved = m_plane;
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
|
||
if (!m_pt_circular) {
|
||
// The arrow lives in the true world plane (NOT billboarded) so the march reads in 3D.
|
||
const Vec3d perp = m_pt_normal.cross(m_pt_dirw).normalized();
|
||
SketchPlane fp; fp.origin = m_pt_base; fp.x_axis = m_pt_dirw; fp.y_axis = perp; fp.normal = m_pt_normal;
|
||
m_plane = fp;
|
||
const int copies = std::max(1, m_pt_count);
|
||
const double L = pt_linear_len(m_pt_spacing, copies);
|
||
segs.emplace_back(Vec2d(0, 0), Vec2d(L, 0)); // shaft
|
||
const double as = std::max(L * 0.12, th); // arrowhead
|
||
segs.emplace_back(Vec2d(L, 0), Vec2d(L - as, as * 0.5));
|
||
segs.emplace_back(Vec2d(L, 0), Vec2d(L - as, -as * 0.5));
|
||
const double tk = std::max(th, L * 0.05); // copy tick half-height
|
||
for (int i = 0; i < copies; ++i) {
|
||
const double x = m_pt_spacing * i;
|
||
segs.emplace_back(Vec2d(x, -tk), Vec2d(x, tk));
|
||
}
|
||
const Vec2d E(L, 0); // diamond grab-handle
|
||
const double hs = std::max(th * 0.8, L * 0.04);
|
||
segs.emplace_back(E + Vec2d(hs, 0), E + Vec2d(0, hs));
|
||
segs.emplace_back(E + Vec2d(0, hs), E + Vec2d(-hs, 0));
|
||
segs.emplace_back(E + Vec2d(-hs, 0), E + Vec2d(0, -hs));
|
||
segs.emplace_back(E + Vec2d(0, -hs), E + Vec2d(hs, 0));
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
draw_strokes(m_pt_stroke_model, segs, std::max(0.8 * upp, 1e-4), col);
|
||
DimAnnot da; da.kind = DimType::Distance; da.value = m_pt_spacing;
|
||
draw_text(m_line_model, dim_text(da), Vec2d(m_pt_spacing * 0.5, tk * 1.7), th, col);
|
||
m_plane = saved;
|
||
return;
|
||
}
|
||
|
||
// ---- Circular: clone the Revolve arc about the plane normal through the plane origin ----
|
||
const Vec3d yax = m_pt_normal.cross(m_pt_cref).normalized();
|
||
SketchPlane rp; rp.origin = m_pt_ccenter; rp.x_axis = m_pt_cref; rp.y_axis = yax; rp.normal = m_pt_normal;
|
||
m_plane = rp;
|
||
const double r = m_pt_radius;
|
||
const double a = m_pt_angle * M_PI / 180.0;
|
||
const int N = std::max(8, int(a / (M_PI / 32.0)));
|
||
Vec2d prev(r, 0.0);
|
||
for (int i = 1; i <= N; ++i) {
|
||
const double t = a * double(i) / double(N);
|
||
const Vec2d cur(r * std::cos(t), r * std::sin(t));
|
||
segs.emplace_back(prev, cur);
|
||
prev = cur;
|
||
}
|
||
const Vec2d tip(r * std::cos(a), r * std::sin(a));
|
||
segs.emplace_back(Vec2d(0, 0), Vec2d(r, 0)); // angle-0 spoke
|
||
segs.emplace_back(Vec2d(0, 0), tip); // swept spoke (the handle)
|
||
const Vec2d tang(-std::sin(a), std::cos(a));
|
||
const Vec2d radial = tip.normalized();
|
||
const double as = std::max(r * 0.14, th);
|
||
segs.emplace_back(tip, tip - tang * as - radial * (as * 0.5));
|
||
segs.emplace_back(tip, tip - tang * as + radial * (as * 0.5));
|
||
const double hs = std::max(th * 0.8, r * 0.05);
|
||
const Vec2d du = radial * hs, dv = Vec2d(-radial.y(), radial.x()) * hs;
|
||
segs.emplace_back(tip + du, tip + dv);
|
||
segs.emplace_back(tip + dv, tip - du);
|
||
segs.emplace_back(tip - du, tip - dv);
|
||
segs.emplace_back(tip - dv, tip + du);
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
draw_strokes(m_pt_stroke_model, segs, std::max(0.8 * upp, 1e-4), col);
|
||
DimAnnot da; da.kind = DimType::Angle; da.value = m_pt_angle;
|
||
draw_text(m_line_model, dim_text(da), tip * 1.14, th, col);
|
||
m_plane = saved;
|
||
}
|
||
|
||
bool DesignSketchTool::hit_test_pattern_handle(GLCanvas3D& canvas, const wxMouseEvent& evt) const
|
||
{
|
||
if (!m_pt_active) return false;
|
||
const Linef3 ray = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = ray.a, rd = ray.b - ray.a;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const double upp = 1.0 / std::max(cam.get_zoom(), 1e-6);
|
||
auto ray_pt = [&](const Vec3d& p) {
|
||
const double t = (p - ro).dot(rd) / std::max(rd.dot(rd), 1e-12);
|
||
return (p - (ro + t * rd)).norm();
|
||
};
|
||
if (!m_pt_circular) {
|
||
const double tol = 8.0 * upp;
|
||
const double L = pt_linear_len(m_pt_spacing, m_pt_count);
|
||
return ray_segment_dist3(ro, rd, m_pt_base, m_pt_base + m_pt_dirw * L) <= tol;
|
||
}
|
||
const double tol = 16.0 * upp;
|
||
const Vec3d yax = m_pt_normal.cross(m_pt_cref).normalized();
|
||
const double a = m_pt_angle * M_PI / 180.0;
|
||
const Vec3d tip = m_pt_ccenter + m_pt_radius * (std::cos(a) * m_pt_cref + std::sin(a) * yax);
|
||
const Vec3d ref = m_pt_ccenter + m_pt_radius * m_pt_cref;
|
||
const int N = 48;
|
||
for (int i = 0; i <= N; ++i) {
|
||
const double f = double(i) / double(N);
|
||
const Vec3d arc = m_pt_ccenter + m_pt_radius * (std::cos(a * f) * m_pt_cref + std::sin(a * f) * yax);
|
||
if (ray_pt(arc) <= tol) return true;
|
||
if (ray_pt(m_pt_ccenter + f * (tip - m_pt_ccenter)) <= tol) return true;
|
||
if (ray_pt(m_pt_ccenter + f * (ref - m_pt_ccenter)) <= tol) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void DesignSketchTool::drag_pattern_handle(GLCanvas3D& canvas, const wxMouseEvent& evt)
|
||
{
|
||
const Linef3 ray = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Vec3d ro = ray.a, rd = ray.b - ray.a;
|
||
if (!m_pt_circular) {
|
||
// Skew-line closest point of the mouse ray to the march axis -> span -> spacing.
|
||
const Vec3d e = m_pt_dirw;
|
||
const Vec3d w0 = m_pt_base - ro;
|
||
const double a = e.dot(e), b = e.dot(rd), c = rd.dot(rd), dd = e.dot(w0), ee = rd.dot(w0);
|
||
const double denom = a * c - b * b;
|
||
if (std::abs(denom) < 1e-7) return; // camera ∥ axis: leave spacing as-is
|
||
const double span = std::max(0.01, (b * ee - c * dd) / denom);
|
||
const int div = std::max(1, m_pt_count - 1);
|
||
m_pt_spacing = std::max(0.1, span / double(div));
|
||
if (on_pattern_changed) on_pattern_changed(m_pt_spacing);
|
||
return;
|
||
}
|
||
const double denom = rd.dot(m_pt_normal);
|
||
if (std::abs(denom) < 1e-9) return; // ray ∥ rotation plane: leave angle as-is
|
||
const double t = (m_pt_ccenter - ro).dot(m_pt_normal) / denom;
|
||
const Vec3d p = ro + t * rd;
|
||
const Vec3d yax = m_pt_normal.cross(m_pt_cref).normalized();
|
||
double deg = std::atan2((p - m_pt_ccenter).dot(yax), (p - m_pt_ccenter).dot(m_pt_cref)) * 180.0 / M_PI;
|
||
if (deg < 0.0) deg += 360.0;
|
||
m_pt_angle = std::min(360.0, std::max(1.0, deg));
|
||
if (on_pattern_changed) on_pattern_changed(m_pt_angle);
|
||
}
|
||
|
||
void DesignSketchTool::open_pattern_editor()
|
||
{
|
||
if (!on_inline_edit) return;
|
||
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
||
const double cur = m_pt_circular ? m_pt_angle : m_pt_spacing;
|
||
on_inline_edit(px, cur, "",
|
||
[this](double v) {
|
||
if (m_pt_circular) m_pt_angle = std::min(360.0, std::max(1.0, v));
|
||
else m_pt_spacing = std::max(0.1, v);
|
||
if (on_pattern_changed) on_pattern_changed(m_pt_circular ? m_pt_angle : m_pt_spacing);
|
||
},
|
||
[]() {});
|
||
}
|
||
|
||
// Closed loops + the entity indices that form each one. A circle/ellipse is its own loop;
|
||
// line/arc chains are walked endpoint-to-endpoint. Entity membership lets a single loop be
|
||
// highlighted and extruded on its own.
|
||
std::vector<DesignSketchTool::RegionLoop>
|
||
DesignSketchTool::region_loops(const std::vector<SketchEntity>& ents) const
|
||
{
|
||
std::vector<RegionLoop> regions;
|
||
const double eps2 = 1e-3 * 1e-3;
|
||
auto is_near = [&](const Vec2d& a, const Vec2d& b) { return (a - b).squaredNorm() < eps2; };
|
||
|
||
// Circles are self-closed regions; lines/arcs are open segments to be chained. Each
|
||
// Seg remembers the entity index it came from.
|
||
struct Seg { std::vector<Vec2d> pts; int ent{-1}; bool used{false}; };
|
||
std::vector<Seg> segs;
|
||
for (int i = 0; i < int(ents.size()); ++i) {
|
||
const SketchEntity& e = ents[i];
|
||
if (e.construction) continue;
|
||
bool closed = false;
|
||
if (e.type == SketchEntity::Type::Circle || e.type == SketchEntity::Type::Ellipse) {
|
||
regions.push_back({ entity_polyline(e, closed), { i } });
|
||
} else if (e.type == SketchEntity::Type::Line || e.type == SketchEntity::Type::Arc
|
||
|| e.type == SketchEntity::Type::EllipseArc
|
||
|| e.type == SketchEntity::Type::BSpline) {
|
||
std::vector<Vec2d> p = entity_polyline(e, closed);
|
||
if (p.size() >= 2) segs.push_back({ std::move(p), i, false });
|
||
}
|
||
}
|
||
|
||
// Walk each unused segment endpoint-to-endpoint until the chain returns to its
|
||
// start (a closed loop) or stalls (an open chain, discarded).
|
||
for (size_t s = 0; s < segs.size(); ++s) {
|
||
if (segs[s].used) continue;
|
||
segs[s].used = true;
|
||
std::vector<Vec2d> loop = segs[s].pts;
|
||
std::vector<int> loop_ents = { segs[s].ent };
|
||
const Vec2d start = loop.front();
|
||
Vec2d cur = loop.back();
|
||
bool extended = true;
|
||
while (extended && !is_near(cur, start)) {
|
||
extended = false;
|
||
for (size_t t = 0; t < segs.size(); ++t) {
|
||
if (segs[t].used) continue;
|
||
const std::vector<Vec2d>& q = segs[t].pts;
|
||
if (is_near(q.front(), cur)) {
|
||
for (size_t k = 1; k < q.size(); ++k) loop.push_back(q[k]);
|
||
cur = q.back();
|
||
} else if (is_near(q.back(), cur)) {
|
||
for (int k = int(q.size()) - 2; k >= 0; --k) loop.push_back(q[k]);
|
||
cur = q.front();
|
||
} else {
|
||
continue;
|
||
}
|
||
segs[t].used = true;
|
||
loop_ents.push_back(segs[t].ent);
|
||
extended = true;
|
||
break;
|
||
}
|
||
}
|
||
if (is_near(cur, start) && loop.size() >= 4) {
|
||
loop.pop_back(); // drop the duplicate closing vertex
|
||
regions.push_back({ std::move(loop), std::move(loop_ents), {} });
|
||
}
|
||
}
|
||
|
||
// NESTING. A loop drawn inside another one is that one's HOLE. Without this a sketch is
|
||
// just N disjoint filled polygons, so "the plate with the hole" is not expressible and the
|
||
// multi-loop kernel path (snaporca-88v) is unreachable from the viewport — which is exactly
|
||
// what Tommaso hit: a rectangle with a circle inside extruded to a plain box, because only
|
||
// the rectangle loop could be picked and only its entities were passed on.
|
||
//
|
||
// Loops in a well-formed sketch do not cross, so testing ONE vertex decides containment.
|
||
// Each loop is assigned to the SMALLEST loop that contains it, which is what makes a hole
|
||
// belong to the region that actually bounds it rather than to every enclosing loop.
|
||
auto poly_area = [](const std::vector<Vec2d>& q) {
|
||
double a2 = 0.0;
|
||
for (size_t i = 0, j = q.size() - 1; i < q.size(); j = i++)
|
||
a2 += (q[j].x() + q[i].x()) * (q[j].y() - q[i].y());
|
||
return std::abs(a2) * 0.5;
|
||
};
|
||
auto point_in = [](const Vec2d& pt, const std::vector<Vec2d>& q) {
|
||
bool in = false;
|
||
for (size_t i = 0, j = q.size() - 1; i < q.size(); j = i++) {
|
||
const Vec2d& A = q[i]; const Vec2d& B = q[j];
|
||
if (((A.y() > pt.y()) != (B.y() > pt.y())) &&
|
||
(pt.x() < (B.x() - A.x()) * (pt.y() - A.y()) / (B.y() - A.y()) + A.x()))
|
||
in = !in;
|
||
}
|
||
return in;
|
||
};
|
||
for (size_t i = 0; i < regions.size(); ++i) {
|
||
if (regions[i].poly.empty()) continue;
|
||
int best = -1;
|
||
double best_area = 0.0;
|
||
for (size_t j = 0; j < regions.size(); ++j) {
|
||
if (i == j || regions[j].poly.size() < 3) continue;
|
||
if (!point_in(regions[i].poly.front(), regions[j].poly)) continue;
|
||
const double a2 = poly_area(regions[j].poly);
|
||
if (best < 0 || a2 < best_area) { best = int(j); best_area = a2; }
|
||
}
|
||
if (best >= 0) regions[best].holes.push_back(int(i));
|
||
}
|
||
return regions;
|
||
}
|
||
|
||
int DesignSketchTool::region_at(const Vec2d& p) const
|
||
{
|
||
// Walk region_loops (which already knows about holes) rather than the raw polygons: the
|
||
// returned index must stay meaningful after the sketch is committed, so it has to use the
|
||
// SAME numbering selected_loop_entities() and hit_display_sketch consume.
|
||
const std::vector<RegionLoop> regions = region_loops(m_entities);
|
||
auto inside = [](const Vec2d& q, const std::vector<Vec2d>& poly) {
|
||
bool in = false;
|
||
for (size_t i = 0, j = poly.size() - 1; i < poly.size(); j = i++) {
|
||
const Vec2d& a = poly[i];
|
||
const Vec2d& b = poly[j];
|
||
if (((a.y() > q.y()) != (b.y() > q.y())) &&
|
||
(q.x() < (b.x() - a.x()) * (q.y() - a.y()) / (b.y() - a.y()) + a.x()))
|
||
in = !in;
|
||
}
|
||
return in;
|
||
};
|
||
// Shoelace area (absolute): the SMALLEST loop containing p is the innermost one, which is
|
||
// the region that actually owns the point — a bore inside a plate resolves to the disc, and
|
||
// a click on the plate material resolves to the plate even though the disc is also inside it.
|
||
auto poly_area = [](const std::vector<Vec2d>& q) {
|
||
double a2 = 0.0;
|
||
for (size_t i = 0, j = q.size() - 1; i < q.size(); j = i++)
|
||
a2 += (q[j].x() + q[i].x()) * (q[j].y() - q[i].y());
|
||
return std::abs(a2) * 0.5;
|
||
};
|
||
int best = -1;
|
||
double best_area = 0.0;
|
||
for (size_t i = 0; i < regions.size(); ++i) {
|
||
if (regions[i].poly.size() < 3) continue;
|
||
if (!inside(p, regions[i].poly)) continue;
|
||
bool in_hole = false; // p inside one of this region's holes → that hole owns it, not us
|
||
for (int h : regions[i].holes) {
|
||
if (h < 0 || h >= int(regions.size()) || regions[h].poly.size() < 3) continue;
|
||
if (inside(p, regions[h].poly)) { in_hole = true; break; }
|
||
}
|
||
if (in_hole) continue;
|
||
const double a = poly_area(regions[i].poly);
|
||
if (best < 0 || a < best_area) { best = int(i); best_area = a; }
|
||
}
|
||
return best;
|
||
}
|
||
|
||
// ---- rendering --------------------------------------------------------------
|
||
|
||
void DesignSketchTool::draw_quad_strip(GLModel& model, const std::vector<Vec2d>& pts, bool closed, const ColorRGBA& color)
|
||
{
|
||
if (pts.size() < 2)
|
||
return;
|
||
|
||
const double hw = 0.6;
|
||
GLModel::Geometry g;
|
||
g.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
|
||
unsigned int base = 0;
|
||
|
||
const size_t segs = closed ? pts.size() : pts.size() - 1;
|
||
for (size_t i = 0; i < segs; ++i) {
|
||
const Vec2d a = pts[i];
|
||
const Vec2d b = pts[(i + 1) % pts.size()];
|
||
const Vec2d d = b - a;
|
||
const double len = d.norm();
|
||
if (len < 1e-6)
|
||
continue;
|
||
const Vec2d n(-d.y() / len, d.x() / len);
|
||
const Vec2d o = n * hw;
|
||
g.add_vertex((Vec3f)m_plane.to_world(a + o).cast<float>());
|
||
g.add_vertex((Vec3f)m_plane.to_world(b + o).cast<float>());
|
||
g.add_vertex((Vec3f)m_plane.to_world(b - o).cast<float>());
|
||
g.add_vertex((Vec3f)m_plane.to_world(a - o).cast<float>());
|
||
g.add_triangle(base, base + 1, base + 2);
|
||
g.add_triangle(base, base + 2, base + 3);
|
||
base += 4;
|
||
}
|
||
|
||
if (base > 0) {
|
||
model.reset();
|
||
model.init_from(std::move(g));
|
||
model.set_color(color);
|
||
model.render();
|
||
}
|
||
}
|
||
|
||
namespace {
|
||
double poly_signed_area(const std::vector<Vec2d>& p)
|
||
{
|
||
double a = 0.0;
|
||
for (size_t i = 0, n = p.size(); i < n; ++i) {
|
||
const Vec2d& u = p[i];
|
||
const Vec2d& v = p[(i + 1) % n];
|
||
a += u.x() * v.y() - v.x() * u.y();
|
||
}
|
||
return 0.5 * a;
|
||
}
|
||
bool pt_in_tri(const Vec2d& p, const Vec2d& a, const Vec2d& b, const Vec2d& c)
|
||
{
|
||
auto cross = [](const Vec2d& u, const Vec2d& v, const Vec2d& w) {
|
||
return (v.x() - u.x()) * (w.y() - u.y()) - (v.y() - u.y()) * (w.x() - u.x());
|
||
};
|
||
const double d1 = cross(a, b, p), d2 = cross(b, c, p), d3 = cross(c, a, p);
|
||
const bool neg = (d1 < 0) || (d2 < 0) || (d3 < 0);
|
||
const bool pos = (d1 > 0) || (d2 > 0) || (d3 > 0);
|
||
return !(neg && pos); // inside iff all cross products share a sign
|
||
}
|
||
// Ear-clipping triangulation of a simple polygon; returns index triples into `poly`.
|
||
std::vector<std::array<unsigned, 3>> ear_clip(const std::vector<Vec2d>& poly)
|
||
{
|
||
std::vector<std::array<unsigned, 3>> tris;
|
||
const size_t n = poly.size();
|
||
if (n < 3) return tris;
|
||
std::vector<unsigned> idx(n);
|
||
for (unsigned i = 0; i < n; ++i) idx[i] = i;
|
||
if (poly_signed_area(poly) < 0.0) std::reverse(idx.begin(), idx.end()); // work CCW
|
||
int guard = 0;
|
||
while (idx.size() > 3 && guard++ < int(10 * n)) {
|
||
bool clipped = false;
|
||
const int m = int(idx.size());
|
||
for (int i = 0; i < m; ++i) {
|
||
const unsigned i0 = idx[(i + m - 1) % m];
|
||
const unsigned i1 = idx[i];
|
||
const unsigned i2 = idx[(i + 1) % m];
|
||
const Vec2d& a = poly[i0]; const Vec2d& b = poly[i1]; const Vec2d& c = poly[i2];
|
||
const double cr = (b.x() - a.x()) * (c.y() - a.y()) - (b.y() - a.y()) * (c.x() - a.x());
|
||
if (cr <= 0.0) continue; // reflex vertex, not an ear tip
|
||
bool ear = true;
|
||
for (int j = 0; j < m; ++j) {
|
||
const unsigned ij = idx[j];
|
||
if (ij == i0 || ij == i1 || ij == i2) continue;
|
||
if (pt_in_tri(poly[ij], a, b, c)) { ear = false; break; }
|
||
}
|
||
if (!ear) continue;
|
||
tris.push_back({ i0, i1, i2 });
|
||
idx.erase(idx.begin() + i);
|
||
clipped = true;
|
||
break;
|
||
}
|
||
if (!clipped) break; // degenerate input: bail rather than spin
|
||
}
|
||
if (idx.size() == 3) tris.push_back({ idx[0], idx[1], idx[2] });
|
||
return tris;
|
||
}
|
||
} // namespace
|
||
|
||
// Fill a region that has holes with an EVEN-ODD SCANLINE fill: each horizontal band is scanned
|
||
// across every contour, the crossings sorted, and the spans between alternate crossings emitted
|
||
// as quads. A hole is just two more crossings, so any number of holes and any concavity fall out
|
||
// of the parity rule — no bridge and no triangulation to fail.
|
||
void DesignSketchTool::draw_fill_holed(GLModel& model, const std::vector<Vec2d>& outer,
|
||
const std::vector<std::vector<Vec2d>>& holes,
|
||
const ColorRGBA& color)
|
||
{
|
||
if (outer.size() < 3) return;
|
||
if (holes.empty()) { draw_fill(model, outer, color); return; }
|
||
|
||
// EVEN-ODD SCANLINE, not a triangulation. The previous version spliced each hole into the
|
||
// outer contour through a keyhole corridor and ear-clipped the result; the corridor did not
|
||
// collapse to zero width and was drawn as a visible triangle running from the bore to the
|
||
// nearest rectangle corner. Rather than tune a bridge that can always find a new shape to
|
||
// fail on, this fills the region the way a rasteriser would: for each horizontal band, cross
|
||
// EVERY contour, sort the crossings, and fill between alternate pairs. A hole is simply two
|
||
// more crossings, so any number of holes and any concavity fall out of the same rule and no
|
||
// corridor exists to leak.
|
||
std::vector<const std::vector<Vec2d>*> contours;
|
||
contours.push_back(&outer);
|
||
for (const auto& h : holes) if (h.size() >= 3) contours.push_back(&h);
|
||
|
||
double ymin = outer[0].y(), ymax = ymin, xmin = outer[0].x(), xmax = xmin;
|
||
for (const auto* c : contours)
|
||
for (const Vec2d& v : *c) {
|
||
ymin = std::min(ymin, v.y()); ymax = std::max(ymax, v.y());
|
||
xmin = std::min(xmin, v.x()); xmax = std::max(xmax, v.x());
|
||
}
|
||
if (ymax - ymin < 1e-9) return;
|
||
|
||
// Enough bands that the step is far below a pixel at any sane zoom, cheap enough to rebuild
|
||
// every frame: this is a translucent highlight, not geometry.
|
||
const int ROWS = 256;
|
||
const double dy = (ymax - ymin) / ROWS;
|
||
|
||
GLModel::Geometry g;
|
||
g.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
|
||
unsigned base = 0;
|
||
std::vector<double> xs;
|
||
for (int row = 0; row < ROWS; ++row) {
|
||
const double y0 = ymin + dy * row, y1 = y0 + dy, ym = 0.5 * (y0 + y1);
|
||
xs.clear();
|
||
for (const auto* c : contours) {
|
||
const std::vector<Vec2d>& q = *c;
|
||
for (size_t i = 0, j = q.size() - 1; i < q.size(); j = i++) {
|
||
const Vec2d& A = q[i]; const Vec2d& B = q[j];
|
||
if ((A.y() > ym) == (B.y() > ym)) continue; // edge does not cross this row
|
||
xs.push_back(A.x() + (ym - A.y()) * (B.x() - A.x()) / (B.y() - A.y()));
|
||
}
|
||
}
|
||
if (xs.size() < 2) continue;
|
||
std::sort(xs.begin(), xs.end());
|
||
for (size_t k = 0; k + 1 < xs.size(); k += 2) { // even-odd: fill alternate spans
|
||
const double xa = xs[k], xb = xs[k + 1];
|
||
if (xb - xa < 1e-9) continue;
|
||
g.add_vertex((Vec3f)m_plane.to_world(Vec2d(xa, y0)).cast<float>());
|
||
g.add_vertex((Vec3f)m_plane.to_world(Vec2d(xb, y0)).cast<float>());
|
||
g.add_vertex((Vec3f)m_plane.to_world(Vec2d(xb, y1)).cast<float>());
|
||
g.add_vertex((Vec3f)m_plane.to_world(Vec2d(xa, y1)).cast<float>());
|
||
g.add_triangle(base, base + 1, base + 2);
|
||
g.add_triangle(base, base + 2, base + 3);
|
||
base += 4;
|
||
}
|
||
}
|
||
if (base == 0) return;
|
||
model.reset();
|
||
model.init_from(std::move(g));
|
||
model.set_color(color);
|
||
model.render();
|
||
}
|
||
|
||
void DesignSketchTool::draw_fill(GLModel& model, const std::vector<Vec2d>& poly, const ColorRGBA& color)
|
||
{
|
||
if (poly.size() < 3) return;
|
||
const auto tris = ear_clip(poly);
|
||
if (tris.empty()) return;
|
||
GLModel::Geometry g;
|
||
g.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
|
||
for (const Vec2d& p : poly)
|
||
g.add_vertex((Vec3f)m_plane.to_world(p).cast<float>());
|
||
for (const auto& t : tris)
|
||
g.add_triangle(t[0], t[1], t[2]);
|
||
model.reset();
|
||
model.init_from(std::move(g));
|
||
model.set_color(color);
|
||
model.render();
|
||
}
|
||
|
||
void DesignSketchTool::draw_vertices(GLModel& model, const std::vector<Vec2d>& pts, const ColorRGBA& color,
|
||
double half_size)
|
||
{
|
||
if (pts.empty())
|
||
return;
|
||
|
||
const double hs = half_size;
|
||
GLModel::Geometry g;
|
||
g.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
|
||
unsigned int base = 0;
|
||
for (const Vec2d& p : pts) {
|
||
g.add_vertex((Vec3f)m_plane.to_world(p + Vec2d(-hs, -hs)).cast<float>());
|
||
g.add_vertex((Vec3f)m_plane.to_world(p + Vec2d( hs, -hs)).cast<float>());
|
||
g.add_vertex((Vec3f)m_plane.to_world(p + Vec2d( hs, hs)).cast<float>());
|
||
g.add_vertex((Vec3f)m_plane.to_world(p + Vec2d(-hs, hs)).cast<float>());
|
||
g.add_triangle(base, base + 1, base + 2);
|
||
g.add_triangle(base, base + 2, base + 3);
|
||
base += 4;
|
||
}
|
||
|
||
model.reset();
|
||
model.init_from(std::move(g));
|
||
model.set_color(color);
|
||
model.render();
|
||
}
|
||
|
||
// Independent thick-line segments batched into one immediate-mode draw (quote lines,
|
||
// extension lines, arrowheads, glyph strokes). Mirrors draw_quad_strip's lift-to-world.
|
||
void DesignSketchTool::draw_strokes(GLModel& model, const std::vector<std::pair<Vec2d, Vec2d>>& segs,
|
||
double hw, const ColorRGBA& color)
|
||
{
|
||
GLModel::Geometry g;
|
||
g.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
|
||
unsigned int base = 0;
|
||
for (const auto& s : segs) {
|
||
const Vec2d a = s.first, b = s.second;
|
||
const Vec2d d = b - a;
|
||
const double len = d.norm();
|
||
if (len < 1e-6) continue;
|
||
const Vec2d n(-d.y() / len, d.x() / len);
|
||
const Vec2d o = n * hw;
|
||
g.add_vertex((Vec3f)m_plane.to_world(a + o).cast<float>());
|
||
g.add_vertex((Vec3f)m_plane.to_world(b + o).cast<float>());
|
||
g.add_vertex((Vec3f)m_plane.to_world(b - o).cast<float>());
|
||
g.add_vertex((Vec3f)m_plane.to_world(a - o).cast<float>());
|
||
g.add_triangle(base, base + 1, base + 2);
|
||
g.add_triangle(base, base + 2, base + 3);
|
||
base += 4;
|
||
}
|
||
if (base > 0) {
|
||
model.reset();
|
||
model.init_from(std::move(g));
|
||
model.set_color(color);
|
||
model.render();
|
||
}
|
||
}
|
||
|
||
namespace {
|
||
// Smooth single-stroke (Hershey-style) vector font for dimension labels. Glyphs
|
||
// live in a 0..0.6 (x) by 0..1 (y) cell, baseline at y=0, cap height y=1; curved
|
||
// digits are sampled as short segments so they read as rounded shapes, not blocks.
|
||
// `advance` is the pen step after the glyph.
|
||
constexpr double kPi = 3.14159265358979323846;
|
||
inline double rad(double deg) { return deg * kPi / 180.0; }
|
||
|
||
// Connect a list of points as a polyline.
|
||
void poly(std::vector<std::pair<Vec2d, Vec2d>>& out, std::initializer_list<Vec2d> p)
|
||
{
|
||
auto it = p.begin();
|
||
if (it == p.end()) return;
|
||
Vec2d prev = *it++;
|
||
for (; it != p.end(); ++it) { out.emplace_back(prev, *it); prev = *it; }
|
||
}
|
||
// Sample an elliptical arc (centre cx,cy; radii rx,ry) from angle a0..a1.
|
||
void arc(std::vector<std::pair<Vec2d, Vec2d>>& out, double cx, double cy, double rx, double ry,
|
||
double a0, double a1, int n = 14)
|
||
{
|
||
Vec2d prev(cx + rx * std::cos(a0), cy + ry * std::sin(a0));
|
||
for (int i = 1; i <= n; ++i) {
|
||
const double t = a0 + (a1 - a0) * (double)i / n;
|
||
const Vec2d cur(cx + rx * std::cos(t), cy + ry * std::sin(t));
|
||
out.emplace_back(prev, cur);
|
||
prev = cur;
|
||
}
|
||
}
|
||
|
||
void glyph_strokes(char c, std::vector<std::pair<Vec2d, Vec2d>>& out, double& advance)
|
||
{
|
||
advance = 0.72;
|
||
switch (c) {
|
||
case '0':
|
||
arc(out, 0.30, 0.50, 0.25, 0.48, 0.0, 2.0 * kPi);
|
||
break;
|
||
case '1':
|
||
poly(out, {Vec2d(0.13, 0.76), Vec2d(0.33, 1.0), Vec2d(0.33, 0.0)});
|
||
poly(out, {Vec2d(0.13, 0.0), Vec2d(0.53, 0.0)});
|
||
advance = 0.52;
|
||
break;
|
||
case '2':
|
||
arc(out, 0.30, 0.72, 0.25, 0.25, rad(170), rad(-45));
|
||
poly(out, {Vec2d(0.477, 0.543), Vec2d(0.06, 0.0), Vec2d(0.56, 0.0)});
|
||
break;
|
||
case '3':
|
||
arc(out, 0.30, 0.74, 0.24, 0.24, rad(160), rad(-90));
|
||
arc(out, 0.30, 0.26, 0.26, 0.26, rad(90), rad(-160));
|
||
break;
|
||
case '4':
|
||
poly(out, {Vec2d(0.42, 1.0), Vec2d(0.04, 0.32), Vec2d(0.58, 0.32)});
|
||
poly(out, {Vec2d(0.42, 1.0), Vec2d(0.42, 0.0)});
|
||
break;
|
||
case '5':
|
||
poly(out, {Vec2d(0.54, 1.0), Vec2d(0.12, 1.0), Vec2d(0.11, 0.52)});
|
||
arc(out, 0.27, 0.30, 0.27, 0.27, rad(130), rad(-120));
|
||
break;
|
||
case '6':
|
||
arc(out, 0.30, 0.28, 0.26, 0.26, 0.0, 2.0 * kPi);
|
||
arc(out, 0.30, 0.55, 0.30, 0.45, rad(90), rad(190));
|
||
break;
|
||
case '7':
|
||
poly(out, {Vec2d(0.05, 1.0), Vec2d(0.57, 1.0), Vec2d(0.22, 0.0)});
|
||
break;
|
||
case '8':
|
||
arc(out, 0.30, 0.73, 0.22, 0.25, 0.0, 2.0 * kPi);
|
||
arc(out, 0.30, 0.26, 0.26, 0.26, 0.0, 2.0 * kPi);
|
||
break;
|
||
case '9':
|
||
arc(out, 0.30, 0.70, 0.26, 0.26, 0.0, 2.0 * kPi);
|
||
arc(out, 0.28, 0.55, 0.28, 0.55, rad(15), rad(-90));
|
||
break;
|
||
case '.':
|
||
case ',': // locale (LC_NUMERIC) may format the decimal separator as a comma
|
||
// small solid dot: crossed short strokes so the quads fill a visible disk
|
||
poly(out, {Vec2d(0.10, 0.08), Vec2d(0.24, 0.08)});
|
||
poly(out, {Vec2d(0.17, 0.02), Vec2d(0.17, 0.15)});
|
||
advance = 0.30;
|
||
break;
|
||
case '-':
|
||
poly(out, {Vec2d(0.10, 0.5), Vec2d(0.50, 0.5)});
|
||
advance = 0.62;
|
||
break;
|
||
case 'R':
|
||
poly(out, {Vec2d(0.08, 0.0), Vec2d(0.08, 1.0), Vec2d(0.38, 1.0)});
|
||
arc(out, 0.38, 0.75, 0.17, 0.25, rad(90), rad(-90));
|
||
poly(out, {Vec2d(0.38, 0.50), Vec2d(0.08, 0.50)});
|
||
poly(out, {Vec2d(0.30, 0.50), Vec2d(0.58, 0.0)});
|
||
advance = 0.80;
|
||
break;
|
||
case 'X':
|
||
poly(out, {Vec2d(0.06, 1.0), Vec2d(0.58, 0.0)});
|
||
poly(out, {Vec2d(0.58, 1.0), Vec2d(0.06, 0.0)});
|
||
advance = 0.72;
|
||
break;
|
||
case 'Y':
|
||
poly(out, {Vec2d(0.06, 1.0), Vec2d(0.32, 0.52)});
|
||
poly(out, {Vec2d(0.58, 1.0), Vec2d(0.32, 0.52)});
|
||
poly(out, {Vec2d(0.32, 0.52), Vec2d(0.32, 0.0)});
|
||
advance = 0.72;
|
||
break;
|
||
case 'Z':
|
||
poly(out, {Vec2d(0.06, 1.0), Vec2d(0.58, 1.0), Vec2d(0.06, 0.0), Vec2d(0.58, 0.0)});
|
||
advance = 0.72;
|
||
break;
|
||
case ' ':
|
||
advance = 0.5;
|
||
break;
|
||
default:
|
||
advance = 0.5;
|
||
break;
|
||
}
|
||
}
|
||
} // namespace
|
||
|
||
void DesignSketchTool::draw_dim_label(const std::string& txt, const Vec2d& plane_center)
|
||
{
|
||
if (txt.empty()) return;
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const wxPoint sp = world_to_screen_px(cam, m_plane.to_world(plane_center));
|
||
if (sp.x < 0 && sp.y < 0) return;
|
||
ImGuiWrapper* imgui = wxGetApp().imgui();
|
||
// Identical to the Prepare/Preview Measure gizmo label (GLGizmoMeasure::render_dimensioning):
|
||
// push_common_window_style sets the white text colour + font/scale (without it the text is
|
||
// invisible); BringWindowToDisplayFront keeps the per-frame label window on top.
|
||
ImGuiWrapper::push_common_window_style(m_render_scale);
|
||
imgui->set_next_window_pos((float)sp.x, (float)sp.y, ImGuiCond_Always, 0.5f, 0.5f);
|
||
imgui->set_next_window_bg_alpha(0.0f);
|
||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(1.0f, 1.0f));
|
||
const std::string win = "##sketchdim" + std::to_string(m_dim_label_seq++);
|
||
imgui->begin(win, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration
|
||
| ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoFocusOnAppearing
|
||
| ImGuiWindowFlags_NoNav);
|
||
ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow());
|
||
ImGui::AlignTextToFramePadding();
|
||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||
const ImVec2 pos = ImGui::GetCursorScreenPos();
|
||
const ImVec2 ts = ImGui::CalcTextSize(txt.c_str());
|
||
const ImGuiStyle& st = ImGui::GetStyle();
|
||
dl->AddRectFilled(ImVec2(pos.x - st.FramePadding.x, pos.y + st.FramePadding.y),
|
||
ImVec2(pos.x + ts.x + 2.0f * st.FramePadding.x,
|
||
pos.y + ts.y + 2.0f * st.FramePadding.y),
|
||
ImGuiWrapper::to_ImU32(ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f)));
|
||
ImGui::SetCursorScreenPos(ImVec2(pos.x + st.FramePadding.x, pos.y));
|
||
imgui->text(txt);
|
||
imgui->end();
|
||
ImGui::PopStyleVar(3);
|
||
ImGuiWrapper::pop_common_window_style();
|
||
}
|
||
|
||
void DesignSketchTool::draw_text(GLModel& /*model*/, const std::string& s, const Vec2d& center,
|
||
double /*height*/, const ColorRGBA& /*color*/)
|
||
{
|
||
// ponytail: all sketch labels now render as Measure-gizmo-style ImGui labels for visual
|
||
// parity with the Prepare/Preview tabs; the old vector-font path (glyph_strokes/draw_strokes
|
||
// for text) is retired. Leader lines/arrows still draw via draw_strokes at the call sites.
|
||
draw_dim_label(s, center);
|
||
}
|
||
|
||
// Draw every placed dimension: extension lines, the offset dimension line, arrowheads
|
||
// and the numeric label. Geometry is recomputed from the (solved) entities each frame
|
||
// so the quote tracks the sketch.
|
||
// Draw one dimension's quote (extension/dimension lines, arrowheads, numeric label).
|
||
// Geometry is recomputed from the (solved) entities so the quote tracks the sketch.
|
||
// Returns the label centre in out_label; false if the annot references missing/degenerate
|
||
// geometry. Shared by placed (render_dimensions) and live (render_live_quotes) quotes.
|
||
bool DesignSketchTool::draw_dim_quote(const DimAnnot& a, double th, const ColorRGBA& dimcol,
|
||
Vec2d& out_label)
|
||
{
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
if (a.kind == DimType::Length || a.kind == DimType::Distance) {
|
||
Vec2d pa, pb;
|
||
if (a.kind == DimType::Length) {
|
||
if (a.ea < 0 || a.ea >= int(m_entities.size())) return false;
|
||
pa = m_entities[a.ea].p0; pb = m_entities[a.ea].p1;
|
||
} else if (!point_at(a.ea, a.ra, pa) || !point_at(a.eb, a.rb, pb)) {
|
||
return false;
|
||
}
|
||
const Vec2d d = pb - pa;
|
||
const double L = d.norm();
|
||
if (L < 1e-6) return false;
|
||
const Vec2d u = d / L;
|
||
const Vec2d nrm(-u.y(), u.x());
|
||
const double side = (a.side != 0.0) ? a.side : 1.0;
|
||
const double off = side * std::max(L * 0.18, 8.0);
|
||
const Vec2d A2 = pa + nrm * off, B2 = pb + nrm * off; // offset clear of the sketch line
|
||
segs.emplace_back(A2, B2); // dimension line (not on the geometry)
|
||
const double as = std::max(L * 0.04, 2.0);
|
||
auto arrow = [&](const Vec2d& tip, const Vec2d& dir) {
|
||
const Vec2d back = tip + dir * as;
|
||
segs.emplace_back(tip, back + nrm * (as * 0.5));
|
||
segs.emplace_back(tip, back - nrm * (as * 0.5));
|
||
};
|
||
arrow(A2, u); arrow(B2, -u);
|
||
out_label = (A2 + B2) * 0.5 + nrm * (side * (th * 0.7 + 1.5));
|
||
} else if (a.kind == DimType::Diameter || a.kind == DimType::Radius) {
|
||
if (a.ea < 0 || a.ea >= int(m_entities.size())) return false;
|
||
const SketchEntity& e = m_entities[a.ea];
|
||
const Vec2d c = e.center;
|
||
const double r = e.radius;
|
||
if (r < 1e-6) return false;
|
||
const Vec2d u(1.0, 0.0);
|
||
const double as = std::max(r * 0.12, 2.0);
|
||
if (a.kind == DimType::Diameter) {
|
||
const Vec2d p1 = c - u * r, p2 = c + u * r;
|
||
segs.emplace_back(p1, p2);
|
||
segs.emplace_back(p1, p1 + u * as + Vec2d(0, 1) * (as * 0.5));
|
||
segs.emplace_back(p1, p1 + u * as - Vec2d(0, 1) * (as * 0.5));
|
||
segs.emplace_back(p2, p2 - u * as + Vec2d(0, 1) * (as * 0.5));
|
||
segs.emplace_back(p2, p2 - u * as - Vec2d(0, 1) * (as * 0.5));
|
||
out_label = c + Vec2d(0, 1) * (th * 0.8);
|
||
} else {
|
||
const Vec2d p2 = c + u * r;
|
||
segs.emplace_back(c, p2);
|
||
segs.emplace_back(p2, p2 - u * as + Vec2d(0, 1) * (as * 0.5));
|
||
segs.emplace_back(p2, p2 - u * as - Vec2d(0, 1) * (as * 0.5));
|
||
out_label = (c + p2) * 0.5 + Vec2d(0, 1) * (th * 0.8);
|
||
}
|
||
} else if (a.kind == DimType::DistanceToLine) {
|
||
Vec2d pa;
|
||
if (!point_at(a.ea, a.ra, pa) || a.eb < 0 || a.eb >= int(m_entities.size())) return false;
|
||
const SketchEntity& Ln = m_entities[a.eb];
|
||
const Vec2d ld = Ln.p1 - Ln.p0;
|
||
const double n = ld.norm();
|
||
if (n < 1e-9) return false;
|
||
const Vec2d u = ld / n;
|
||
const double t = (pa - Ln.p0).dot(u);
|
||
const Vec2d foot = Ln.p0 + u * t; // perpendicular foot on the line
|
||
segs.emplace_back(pa, foot);
|
||
out_label = (pa + foot) * 0.5 + u * (th * 0.7 + 1.5);
|
||
} else if (a.kind == DimType::Angle) {
|
||
if (a.ea < 0 || a.ea >= int(m_entities.size())) return false;
|
||
const SketchEntity& e = m_entities[a.ea];
|
||
if (e.type != SketchEntity::Type::Line) return false;
|
||
const Vec2d d = e.p1 - e.p0;
|
||
const double L = d.norm();
|
||
if (L < 1e-6) return false;
|
||
double ang = std::atan2(d.y(), d.x()); // signed, matches measure_dim sweep
|
||
const double rr = std::max(std::min(L * 0.35, 40.0), th * 1.6); // arc radius
|
||
segs.emplace_back(e.p0, e.p0 + Vec2d(rr * 1.15, 0.0)); // horizontal reference leg
|
||
const int N = 20; // arc 0 -> ang about p0
|
||
Vec2d prev = e.p0 + Vec2d(rr, 0.0);
|
||
for (int i = 1; i <= N; ++i) {
|
||
const double t = ang * double(i) / N;
|
||
const Vec2d cur = e.p0 + Vec2d(rr * std::cos(t), rr * std::sin(t));
|
||
segs.emplace_back(prev, cur); prev = cur;
|
||
}
|
||
const double mid = ang * 0.5;
|
||
out_label = e.p0 + Vec2d(std::cos(mid), std::sin(mid)) * (rr + th * 1.1);
|
||
} else {
|
||
return false;
|
||
}
|
||
draw_strokes(m_highlight_model, segs, 0.2, dimcol);
|
||
draw_text(m_line_model, dim_text(a), out_label, th, dimcol);
|
||
return true;
|
||
}
|
||
|
||
// Draw every placed (driving) dimension; cache each label centre for picking.
|
||
void DesignSketchTool::render_dimensions(double unit_per_px)
|
||
{
|
||
if (m_dimensions.empty()) return;
|
||
const ColorRGBA dimcol(0.85f, 0.85f, 0.85f, 1.0f); // neutral leader (Measure parity)
|
||
// Label text is a CONSTANT screen size (like real CAD), not scaled to geometry,
|
||
// so a long line doesn't get huge text. ~15 px tall in plane units at this zoom.
|
||
const double th = std::max(15.0 * unit_per_px, 1e-4);
|
||
for (size_t di = 0; di < m_dimensions.size(); ++di) {
|
||
Vec2d label;
|
||
if (draw_dim_quote(m_dimensions[di], th, dimcol, label))
|
||
m_dimensions[di].label_pos = label;
|
||
}
|
||
}
|
||
|
||
// Per-entity-type characteristic dimensions, drawn as live non-driving quotes for the
|
||
// entity being edited (point/handle drag, or a lone selection). This is the Onshape
|
||
// pattern that scales to every tool: each kind reports its defining dimension(s); each
|
||
// is clickable (m_live_quotes) to promote to a driving dim + open the inline editor.
|
||
// A dim already driven on the entity is skipped (render_dimensions draws that one).
|
||
void DesignSketchTool::render_live_quotes(double unit_per_px)
|
||
{
|
||
m_live_quotes.clear();
|
||
m_live_poly_fi = -1;
|
||
m_live_poly_side_label = m_live_poly_angle_label = Vec2d(1e18, 1e18);
|
||
m_live_arc_ei = -1;
|
||
m_live_arc_angle_label = Vec2d(1e18, 1e18);
|
||
m_live_ellipse_ei = -1;
|
||
m_live_ellipse_major_label = m_live_ellipse_minor_label = Vec2d(1e18, 1e18);
|
||
m_live_ellipsearc_sweep_label = Vec2d(1e18, 1e18);
|
||
m_live_obrect_fi = -1;
|
||
m_live_obrect_angle_label = Vec2d(1e18, 1e18);
|
||
m_live_rrect_fi = -1;
|
||
m_live_rrect_w_label = m_live_rrect_h_label = m_live_rrect_r_label = Vec2d(1e18, 1e18);
|
||
m_live_aslot_fi = -1;
|
||
m_live_aslot_r_label = m_live_aslot_w_label = Vec2d(1e18, 1e18);
|
||
m_live_slot_fi = -1;
|
||
m_live_slot_len_label = m_live_slot_w_label = m_live_slot_angle_label = Vec2d(1e18, 1e18);
|
||
// Edit-op tools (Fillet/Chamfer/Offset/Mirror) put their picks in m_selection for the
|
||
// highlight, but their own arrow/label gizmo is the value affordance — don't also draw
|
||
// the picked entity's characteristic quotes (Length/Angle/…) or the view gets cluttered.
|
||
if (is_edit_op_mode() || is_transform_mode()) return;
|
||
int ei = -1;
|
||
if (m_dragging_point && m_drag_ei >= 0) ei = m_drag_ei;
|
||
else if (m_dragging_handle) ei = m_drag_handle.ei;
|
||
else if (m_selection.size() == 1) ei = m_selection[0];
|
||
if (ei < 0 || ei >= int(m_entities.size())) return;
|
||
const SketchEntity& e = m_entities[ei];
|
||
|
||
std::vector<DimAnnot> protos;
|
||
auto add_len = [&](int line_ei, double side) {
|
||
if (line_ei < 0 || line_ei >= int(m_entities.size())) return;
|
||
if (m_entities[line_ei].type != SketchEntity::Type::Line) return;
|
||
DimAnnot a; a.kind = DimType::Length; a.ea = line_ei; a.side = side; protos.push_back(a);
|
||
};
|
||
|
||
// A grouped gesture (rect/slot/polygon decomposes into raw lines/arcs) exposes its
|
||
// DERIVED characteristic dims off the Feature span, regardless of which member edge
|
||
// was picked. Rect: Width = first edge length, Height = second edge length (the two
|
||
// axes of the 4-line loop pushed by push_closed_lines: edge0 horizontal, edge1
|
||
// vertical). Quotes offset to opposite sides so they don't overlap.
|
||
const int fi = feature_of(ei);
|
||
if (fi >= 0) {
|
||
const Feature& f = m_features[fi];
|
||
switch (f.kind) {
|
||
case FeatureKind::CornerRect:
|
||
case FeatureKind::CenterRect: {
|
||
add_len(f.begin + 0, 1.0); // Width
|
||
add_len(f.begin + 1, -1.0); // Height
|
||
// OBLIQUE rect (drawn off-axis, also a CornerRect feature): expose its orientation
|
||
// too. Gated to genuinely-tilted edges so an axis-aligned Corner/Center rect never
|
||
// gets an angle quote (and its verified W/H behaviour is untouched).
|
||
if (f.begin >= 0 && f.begin < int(m_entities.size())) {
|
||
const SketchEntity& e0 = m_entities[f.begin];
|
||
Vec2d d0 = e0.p1 - e0.p0;
|
||
if (d0.squaredNorm() > 1e-12) {
|
||
double deg = std::atan2(d0.y(), d0.x()) * 180.0 / M_PI;
|
||
const double off = std::fmod(std::fmod(deg, 90.0) + 90.0, 90.0); // dist to axis
|
||
if (off > 2.0 && off < 88.0) {
|
||
const ColorRGBA dc(0.30f, 0.88f, 0.66f, 1.0f);
|
||
const double th = std::max(15.0 * unit_per_px, 1e-4);
|
||
double adeg = deg; if (adeg < 0.0) adeg += 360.0;
|
||
const Vec2d mid = 0.5 * (e0.p0 + e0.p1);
|
||
Vec2d nrm(-d0.y(), d0.x()); if (nrm.squaredNorm() > 1e-12) nrm.normalize();
|
||
m_live_obrect_angle_label = mid + nrm * (th * 1.4);
|
||
DimAnnot at; at.kind = DimType::Angle; at.value = adeg;
|
||
draw_text(m_line_model, dim_text(at), m_live_obrect_angle_label, th, dc);
|
||
m_live_obrect_fi = fi;
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
case FeatureKind::Slot: {
|
||
// Straight slot edits GEOMETRICALLY (like the arc-slot / rounded-rect), NOT through
|
||
// the constraint-based scalar quotes. Its dims are the centreline LENGTH (distance
|
||
// between the two cap centres c0,c1) and the WIDTH (2*param). The old
|
||
// Distance-between-arc-centres + Radius quotes never registered as editable, so the
|
||
// labels did nothing on click (nde #6). Draw both labels clear of the fillable face
|
||
// and remember the feature so open_primary_autoedit / click-to-promote drive set_slot.
|
||
// Slot dims: (1) inter-centre distance, (2) radius (= half-width), (3) centreline angle.
|
||
const ColorRGBA dc(0.30f, 0.88f, 0.66f, 1.0f);
|
||
const double th = std::max(15.0 * unit_per_px, 1e-4);
|
||
Vec2d u = f.c1 - f.c0;
|
||
const double Lc = u.norm();
|
||
if (Lc > 1e-9) {
|
||
u /= Lc;
|
||
const Vec2d n(-u.y(), u.x());
|
||
const double w = f.param;
|
||
DimAnnot len; len.kind = DimType::Length; len.value = Lc;
|
||
m_live_slot_len_label = 0.5 * (f.c0 + f.c1) + n * (w + th * 2.0);
|
||
draw_text(m_line_model, dim_text(len), m_live_slot_len_label, th, dc);
|
||
DimAnnot rd; rd.kind = DimType::Radius; rd.value = w;
|
||
m_live_slot_w_label = f.c1 + u * (w + th * 2.0);
|
||
draw_text(m_line_model, dim_text(rd), m_live_slot_w_label, th, dc);
|
||
double deg = std::atan2(f.c1.y() - f.c0.y(), f.c1.x() - f.c0.x()) * 180.0 / M_PI;
|
||
if (deg < 0.0) deg += 360.0;
|
||
DimAnnot an; an.kind = DimType::Angle; an.value = deg;
|
||
m_live_slot_angle_label = f.c0 - u * (w + th * 2.0);
|
||
draw_text(m_line_model, dim_text(an), m_live_slot_angle_label, th, dc);
|
||
m_live_slot_fi = fi;
|
||
}
|
||
break;
|
||
}
|
||
case FeatureKind::Polygon: {
|
||
// A regular polygon is N raw lines (no centre entity). Its natural editable
|
||
// dims are the SIDE length and the ORIENTATION — NOT a circumradius (a polygon
|
||
// is not a circle). Both edit the whole loop geometrically: side scales it
|
||
// uniformly, angle rotates it. Drawn off edge0 with draw_dim_quote (Length +
|
||
// Angle); the side quote is offset OUTWARD so its label clears the face.
|
||
if (f.begin < int(m_entities.size()) &&
|
||
m_entities[f.begin].type == SketchEntity::Type::Line) {
|
||
const ColorRGBA dc(0.30f, 0.88f, 0.66f, 1.0f);
|
||
const double th = std::max(15.0 * unit_per_px, 1e-4);
|
||
const SketchEntity& e0 = m_entities[f.begin];
|
||
const Vec2d m0 = 0.5 * (e0.p0 + e0.p1);
|
||
Vec2d u0 = e0.p1 - e0.p0;
|
||
if (u0.squaredNorm() > 1e-12) u0.normalize();
|
||
const Vec2d n0(-u0.y(), u0.x());
|
||
const double outsign = ((m0 + n0) - f.c0).norm() >= (m0 - f.c0).norm() ? 1.0 : -1.0;
|
||
DimAnnot side; side.kind = DimType::Length; side.ea = f.begin; side.side = outsign;
|
||
side.value = measure_dim(side);
|
||
Vec2d slbl;
|
||
if (draw_dim_quote(side, th, dc, slbl)) m_live_poly_side_label = slbl;
|
||
|
||
// Orientation = the angle of the centre->vertex0 spoke from +X (the
|
||
// intuitive "which way does the polygon point"), NOT the edge direction.
|
||
// Drawn as a wedge OUTSIDE the polygon (radius just past the circumradius)
|
||
// so its arc/label clear the fillable face.
|
||
const Vec2d sp = m_entities[f.begin].p0 - f.c0; // centre -> vertex0
|
||
const double R = sp.norm();
|
||
if (R > 1e-6) {
|
||
double av = std::atan2(sp.y(), sp.x());
|
||
double avdeg = av * 180.0 / M_PI; if (avdeg < 0.0) avdeg += 360.0;
|
||
const double rr = R + th * 2.5; // wedge just outside the loop
|
||
std::vector<std::pair<Vec2d, Vec2d>> asegs;
|
||
asegs.emplace_back(f.c0, f.c0 + Vec2d(rr, 0.0)); // +X leg
|
||
asegs.emplace_back(f.c0, f.c0 + Vec2d(std::cos(av), std::sin(av)) * rr); // spoke leg
|
||
const int N = 20; Vec2d prev = f.c0 + Vec2d(rr, 0.0);
|
||
for (int i = 1; i <= N; ++i) {
|
||
const double t = av * double(i) / N;
|
||
const Vec2d cur = f.c0 + Vec2d(rr * std::cos(t), rr * std::sin(t));
|
||
asegs.emplace_back(prev, cur); prev = cur;
|
||
}
|
||
const double mid = av * 0.5;
|
||
const Vec2d albl = f.c0 + Vec2d(std::cos(mid), std::sin(mid)) * (rr + th * 1.2);
|
||
DimAnnot at; at.kind = DimType::Angle; at.value = avdeg; // "NN.N°"
|
||
draw_strokes(m_highlight_model, asegs, 0.6, dc);
|
||
draw_text(m_line_model, dim_text(at), albl, th, dc);
|
||
m_live_poly_angle_label = albl;
|
||
}
|
||
m_live_poly_fi = fi;
|
||
}
|
||
break;
|
||
}
|
||
case FeatureKind::RoundedRect: {
|
||
// Width + Height (box bounds) + fillet Radius, drawn as clickable quotes that
|
||
// rebuild the box geometrically (set_rounded_rect). c0=min corner, c1=max, param=r.
|
||
const ColorRGBA dc(0.30f, 0.88f, 0.66f, 1.0f);
|
||
const double th = std::max(15.0 * unit_per_px, 1e-4);
|
||
const double xmin = std::min(f.c0.x(), f.c1.x()), xmax = std::max(f.c0.x(), f.c1.x());
|
||
const double ymin = std::min(f.c0.y(), f.c1.y()), ymax = std::max(f.c0.y(), f.c1.y());
|
||
const double w = xmax - xmin, h = ymax - ymin, r = f.param;
|
||
const double off = th * 2.0;
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
// Width quote below the box.
|
||
const double yb = ymin - off;
|
||
segs.emplace_back(Vec2d(xmin, ymin), Vec2d(xmin, yb));
|
||
segs.emplace_back(Vec2d(xmax, ymin), Vec2d(xmax, yb));
|
||
segs.emplace_back(Vec2d(xmin, yb), Vec2d(xmax, yb));
|
||
// Height quote left of the box.
|
||
const double xl = xmin - off;
|
||
segs.emplace_back(Vec2d(xmin, ymin), Vec2d(xl, ymin));
|
||
segs.emplace_back(Vec2d(xmin, ymax), Vec2d(xl, ymax));
|
||
segs.emplace_back(Vec2d(xl, ymin), Vec2d(xl, ymax));
|
||
// Fillet-radius leader from the TR arc centre out to the corner.
|
||
const Vec2d rc(xmax - r, ymax - r);
|
||
segs.emplace_back(rc, rc + Vec2d(r, r).normalized() * r);
|
||
draw_strokes(m_highlight_model, segs, 0.6, dc);
|
||
DimAnnot wa; wa.kind = DimType::Length; wa.value = w;
|
||
DimAnnot ha; ha.kind = DimType::Length; ha.value = h;
|
||
DimAnnot ra; ra.kind = DimType::Radius; ra.value = r;
|
||
m_live_rrect_w_label = Vec2d((xmin + xmax) * 0.5, yb - th * 0.8);
|
||
m_live_rrect_h_label = Vec2d(xl - th * 0.8, (ymin + ymax) * 0.5);
|
||
m_live_rrect_r_label = rc + Vec2d(r, r).normalized() * (r + th * 1.2);
|
||
draw_text(m_line_model, dim_text(wa), m_live_rrect_w_label, th, dc);
|
||
draw_text(m_line_model, dim_text(ha), m_live_rrect_h_label, th, dc);
|
||
draw_text(m_line_model, dim_text(ra), m_live_rrect_r_label, th, dc);
|
||
m_live_rrect_fi = fi;
|
||
break;
|
||
}
|
||
case FeatureKind::ArcSlot: {
|
||
// Centreline Radius + slot Width quotes. centre=f.c0, centreline start=f.c1,
|
||
// half-width=f.param; end direction from the cap@E arc centre (begin+1).
|
||
if (f.begin + 1 < int(m_entities.size())) {
|
||
const ColorRGBA dc(0.30f, 0.88f, 0.66f, 1.0f);
|
||
const double th = std::max(15.0 * unit_per_px, 1e-4);
|
||
const Vec2d center = f.c0;
|
||
const double Rc = (f.c1 - center).norm();
|
||
const double w = f.param;
|
||
Vec2d dirS = (f.c1 - center);
|
||
Vec2d dirE = (m_entities[f.begin + 1].center - center);
|
||
if (dirS.squaredNorm() > 1e-12 && dirE.squaredNorm() > 1e-12 && Rc > 1e-6) {
|
||
dirS.normalize(); dirE.normalize();
|
||
const double aS = std::atan2(dirS.y(), dirS.x());
|
||
double sweep = std::atan2(dirE.y(), dirE.x()) - aS;
|
||
while (sweep < 0) sweep += 2.0 * M_PI;
|
||
const double aMid = aS + sweep * 0.5;
|
||
const Vec2d uMid(std::cos(aMid), std::sin(aMid));
|
||
// Centreline-radius leader: centre -> centreline midpoint.
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
segs.emplace_back(center, center + uMid * Rc);
|
||
// Width tick across the slot at the start cap (outer<->inner).
|
||
segs.emplace_back(center + dirS * (Rc + w), center + dirS * (Rc - w));
|
||
draw_strokes(m_highlight_model, segs, 0.6, dc);
|
||
DimAnnot ra; ra.kind = DimType::Radius; ra.value = Rc;
|
||
DimAnnot wa; wa.kind = DimType::Length; wa.value = 2.0 * w;
|
||
m_live_aslot_r_label = center + uMid * (Rc * 0.5) + Vec2d(0, th);
|
||
m_live_aslot_w_label = center + dirS * (Rc + w) + dirS * (th * 1.2);
|
||
draw_text(m_line_model, dim_text(ra), m_live_aslot_r_label, th, dc);
|
||
draw_text(m_line_model, dim_text(wa), m_live_aslot_w_label, th, dc);
|
||
m_live_aslot_fi = fi;
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
default: break; // other features: later chunks
|
||
}
|
||
}
|
||
|
||
if (protos.empty() && m_live_poly_fi < 0 && m_live_rrect_fi < 0 &&
|
||
m_live_aslot_fi < 0 && m_live_slot_fi < 0) { // ungrouped single entity
|
||
switch (e.type) {
|
||
case SketchEntity::Type::Line: {
|
||
DimAnnot len; len.kind = DimType::Length; len.ea = ei; len.side = 1.0; protos.push_back(len);
|
||
DimAnnot ang; ang.kind = DimType::Angle; ang.ea = ei; ang.eb = -1; protos.push_back(ang);
|
||
break; // segment length + angle-to-horizontal
|
||
}
|
||
case SketchEntity::Type::Circle: {
|
||
DimAnnot a; a.kind = DimType::Radius; a.ea = ei; protos.push_back(a); break;
|
||
}
|
||
case SketchEntity::Type::Arc: {
|
||
// Arc radius is its single defining dimension (sweep angles edit via the end
|
||
// handles). radius lives in the same .radius field measure_dim/constraint_for
|
||
// read, so the Radius promotion path is identical to Circle.
|
||
DimAnnot a; a.kind = DimType::Radius; a.ea = ei; protos.push_back(a); break;
|
||
}
|
||
default: break; // ellipse/bspline: later
|
||
}
|
||
}
|
||
|
||
// Arc sweep-angle wedge: drawn inline (like the polygon orientation) because it is a
|
||
// GEOMETRIC edit (SLVS angle constraints are line-to-line). A wedge spans the arc's
|
||
// start->end angles just OUTSIDE the radius; its label shows the included angle and is
|
||
// clickable to type a new sweep. The radius quote is still emitted via `protos`.
|
||
if (m_live_poly_fi < 0 && m_live_rrect_fi < 0 && m_live_aslot_fi < 0 && m_live_slot_fi < 0 &&
|
||
e.type == SketchEntity::Type::Arc && e.radius > 1e-6) {
|
||
const ColorRGBA dc(0.30f, 0.88f, 0.66f, 1.0f);
|
||
const double th = std::max(15.0 * unit_per_px, 1e-4);
|
||
const Vec2d c = e.center;
|
||
const double a0 = e.start_angle, a1 = e.end_angle;
|
||
const double sweep = a1 - a0; // signed (CCW>0); |sweep| shown
|
||
double swdeg = std::abs(sweep) * 180.0 / M_PI;
|
||
const double rr = e.radius + th * 2.5; // wedge just outside the arc
|
||
std::vector<std::pair<Vec2d, Vec2d>> asegs;
|
||
asegs.emplace_back(c, c + Vec2d(std::cos(a0), std::sin(a0)) * rr); // start leg
|
||
asegs.emplace_back(c, c + Vec2d(std::cos(a1), std::sin(a1)) * rr); // end leg
|
||
const int N = 24; Vec2d prev = c + Vec2d(std::cos(a0), std::sin(a0)) * rr;
|
||
for (int i = 1; i <= N; ++i) {
|
||
const double t = a0 + sweep * double(i) / N;
|
||
const Vec2d cur = c + Vec2d(rr * std::cos(t), rr * std::sin(t));
|
||
asegs.emplace_back(prev, cur); prev = cur;
|
||
}
|
||
const double mid = a0 + sweep * 0.5;
|
||
const Vec2d albl = c + Vec2d(std::cos(mid), std::sin(mid)) * (rr + th * 1.2);
|
||
DimAnnot at; at.kind = DimType::Angle; at.value = swdeg; // "NN.N°"
|
||
draw_strokes(m_highlight_model, asegs, 0.6, dc);
|
||
draw_text(m_line_model, dim_text(at), albl, th, dc);
|
||
m_live_arc_angle_label = albl;
|
||
m_live_arc_ei = ei;
|
||
}
|
||
|
||
// Ellipse: two clickable axis quotes — semi-major (a) along the major direction and
|
||
// semi-minor (b) along the minor. Both edit geometrically (a=e.radius, b=e.rminor);
|
||
// phi (orientation) is changed by dragging the major grip, not via a label.
|
||
if (m_live_poly_fi < 0 && m_live_rrect_fi < 0 &&
|
||
(e.type == SketchEntity::Type::Ellipse || e.type == SketchEntity::Type::EllipseArc) &&
|
||
e.radius > 1e-6 && e.rminor > 1e-6) {
|
||
const ColorRGBA dc(0.30f, 0.88f, 0.66f, 1.0f);
|
||
const double th = std::max(15.0 * unit_per_px, 1e-4);
|
||
const Vec2d c = e.center;
|
||
const Vec2d um(std::cos(e.rotation), std::sin(e.rotation)); // major dir
|
||
const Vec2d un(-um.y(), um.x()); // minor dir
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
const Vec2d majEnd = c + um * e.radius, minEnd = c + un * e.rminor;
|
||
segs.emplace_back(c, majEnd);
|
||
segs.emplace_back(c, minEnd);
|
||
draw_strokes(m_highlight_model, segs, 0.6, dc);
|
||
DimAnnot ma; ma.kind = DimType::Length; ma.value = e.radius; // plain "NN.N"
|
||
DimAnnot mi; mi.kind = DimType::Length; mi.value = e.rminor;
|
||
const Vec2d majLbl = c + um * (e.radius * 0.5) + un * (th * 1.0);
|
||
const Vec2d minLbl = c + un * (e.rminor * 0.5) + um * (th * 1.0);
|
||
draw_text(m_line_model, dim_text(ma), majLbl, th, dc);
|
||
draw_text(m_line_model, dim_text(mi), minLbl, th, dc);
|
||
m_live_ellipse_major_label = majLbl;
|
||
m_live_ellipse_minor_label = minLbl;
|
||
m_live_ellipse_ei = ei;
|
||
// Elliptical arc also has a SWEEP (included parametric angle), drawn outside the arc
|
||
// midpoint; the full ellipse skips this (closed).
|
||
if (e.type == SketchEntity::Type::EllipseArc) {
|
||
const double midp = 0.5 * (e.start_angle + e.end_angle);
|
||
const Vec2d mp = ellipse_point(c, e.radius, e.rminor, e.rotation, midp);
|
||
Vec2d outw = mp - c; if (outw.squaredNorm() > 1e-12) outw.normalize();
|
||
m_live_ellipsearc_sweep_label = mp + outw * (th * 1.5);
|
||
DimAnnot sw; sw.kind = DimType::Angle;
|
||
sw.value = std::abs(e.end_angle - e.start_angle) * 180.0 / M_PI;
|
||
draw_text(m_line_model, dim_text(sw), m_live_ellipsearc_sweep_label, th, dc);
|
||
}
|
||
}
|
||
|
||
if (protos.empty()) return;
|
||
|
||
const ColorRGBA dimcol(0.30f, 0.88f, 0.66f, 1.0f);
|
||
const double th = std::max(15.0 * unit_per_px, 1e-4);
|
||
for (DimAnnot a : protos) {
|
||
bool driven = false; // skip if already a driving dim of this kind
|
||
for (const DimAnnot& d : m_dimensions)
|
||
if (d.ea == a.ea && d.kind == a.kind) { driven = true; break; }
|
||
if (driven) continue;
|
||
a.value = measure_dim(a);
|
||
Vec2d label;
|
||
if (draw_dim_quote(a, th, dimcol, label)) {
|
||
a.label_pos = label;
|
||
m_live_quotes.push_back(a); // remember for click-to-promote
|
||
}
|
||
}
|
||
}
|
||
|
||
// Iconic constraint badges drawn near each constraint's primary entity (C3.4b).
|
||
// Each glyph is authored in a unit cell [-0.5,0.5]^2 then scaled to a constant
|
||
// on-screen size and translated to the anchor; badges on the same entity stack
|
||
// upward so multiple constraints stay legible.
|
||
void DesignSketchTool::build_constraint_glyphs(double unit_per_px,
|
||
std::vector<std::pair<Vec2d, Vec2d>>& out) const
|
||
{
|
||
if (m_constrain_cons.empty() || m_entities.empty()) return;
|
||
using T = SketchConstraintType;
|
||
const double s = std::max(11.0 * unit_per_px, 1e-4); // glyph cell size in plane units
|
||
const double step = s * 1.5; // vertical stacking step
|
||
|
||
// Representative anchor point on an entity (line midpoint, round-entity centre).
|
||
auto anchor_of = [&](int ei) -> Vec2d {
|
||
if (ei < 0 || ei >= int(m_entities.size())) return Vec2d(0, 0);
|
||
const SketchEntity& e = m_entities[ei];
|
||
switch (e.type) {
|
||
case SketchEntity::Type::Line: return 0.5 * (e.p0 + e.p1);
|
||
case SketchEntity::Type::Circle:
|
||
case SketchEntity::Type::Ellipse:
|
||
case SketchEntity::Type::Arc:
|
||
case SketchEntity::Type::EllipseArc: return e.center;
|
||
case SketchEntity::Type::BSpline: return 0.5 * (e.p0 + e.p1);
|
||
case SketchEntity::Type::Point: return e.p0;
|
||
}
|
||
return e.p0;
|
||
};
|
||
|
||
// Unit-cell stroke authoring helpers (cell centred on origin).
|
||
auto seg = [&](std::vector<std::pair<Vec2d, Vec2d>>& v, Vec2d a, Vec2d b) { v.emplace_back(a, b); };
|
||
auto circ = [&](std::vector<std::pair<Vec2d, Vec2d>>& v, Vec2d c, double r) {
|
||
const int n = 12; Vec2d prev(c.x() + r, c.y());
|
||
for (int i = 1; i <= n; ++i) {
|
||
const double t = 2.0 * 3.14159265358979 * i / n;
|
||
Vec2d cur(c.x() + r * std::cos(t), c.y() + r * std::sin(t));
|
||
v.emplace_back(prev, cur); prev = cur;
|
||
}
|
||
};
|
||
// Author one glyph type into a unit-cell stroke list.
|
||
auto unit_glyph = [&](T type, std::vector<std::pair<Vec2d, Vec2d>>& v) {
|
||
switch (type) {
|
||
case T::Horizontal: seg(v, {-0.5, 0}, {0.5, 0}); break;
|
||
case T::Vertical: seg(v, {0, -0.5}, {0, 0.5}); break;
|
||
case T::Parallel: seg(v, {-0.35, -0.5}, {0.0, 0.5}); seg(v, {0.05, -0.5}, {0.4, 0.5}); break;
|
||
case T::Perpendicular: seg(v, {-0.4, 0.5}, {-0.4, -0.4}); seg(v, {-0.4, -0.4}, {0.5, -0.4}); break;
|
||
case T::Coincident: circ(v, {0, 0}, 0.42); break;
|
||
case T::Concentric: circ(v, {0, 0}, 0.5); circ(v, {0, 0}, 0.24); break;
|
||
case T::EqualLength:seg(v, {-0.4, 0.16}, {0.4, 0.16}); seg(v, {-0.4, -0.16}, {0.4, -0.16}); break;
|
||
case T::Tangent: circ(v, {0, -0.1}, 0.35); seg(v, {-0.5, 0.42}, {0.5, 0.42}); break;
|
||
case T::Midpoint: seg(v, {-0.4, 0}, {0.4, 0}); seg(v, {0, -0.18}, {0, 0.18}); break;
|
||
case T::Symmetric: seg(v, {0, -0.5}, {0, 0.5});
|
||
seg(v, {-0.5, 0.4}, {-0.15, 0}); seg(v, {-0.5, -0.4}, {-0.15, 0});
|
||
seg(v, {0.5, 0.4}, {0.15, 0}); seg(v, {0.5, -0.4}, {0.15, 0}); break;
|
||
case T::Fix: seg(v, {-0.4, -0.4}, {0.4, -0.4}); seg(v, {0.4, -0.4}, {0.4, 0.4});
|
||
seg(v, {0.4, 0.4}, {-0.4, 0.4}); seg(v, {-0.4, 0.4}, {-0.4, -0.4}); break;
|
||
case T::Angle: seg(v, {-0.4, -0.4}, {0.4, -0.4}); seg(v, {-0.4, -0.4}, {0.3, 0.4}); break;
|
||
case T::Radius: circ(v, {0, 0}, 0.45); seg(v, {0, 0}, {0.45, 0}); break;
|
||
case T::Diameter: circ(v, {0, 0}, 0.45); seg(v, {-0.45, 0}, {0.45, 0}); break;
|
||
case T::PointOnLine:
|
||
case T::PointOnObject: seg(v, {-0.5, -0.3}, {0.5, -0.3}); circ(v, {0, 0.05}, 0.16); break;
|
||
case T::Distance:
|
||
case T::LockX:
|
||
case T::LockY: seg(v, {-0.4, 0}, {0.4, 0}); break; // generic tick
|
||
}
|
||
};
|
||
|
||
// Stack count per entity so successive badges step upward.
|
||
std::vector<int> stack(m_entities.size(), 0);
|
||
const Vec2d up(0.0, 1.0); // plane-space up; offset so badge sits off the geometry
|
||
for (const SketchEntityConstraintDef& d : m_constrain_cons) {
|
||
if (d.ea < 0 || d.ea >= int(m_entities.size())) continue;
|
||
const int k = stack[d.ea]++;
|
||
const Vec2d center = anchor_of(d.ea) + up * (step * (1.0 + k));
|
||
std::vector<std::pair<Vec2d, Vec2d>> cell;
|
||
unit_glyph(d.type, cell);
|
||
for (auto& sgp : cell)
|
||
out.emplace_back(center + sgp.first * s, center + sgp.second * s);
|
||
}
|
||
}
|
||
|
||
void DesignSketchTool::draw_entities_preview(const std::vector<SketchEntity>& ents, const ColorRGBA& color)
|
||
{
|
||
for (const SketchEntity& e : ents) {
|
||
if (e.type == SketchEntity::Type::Point) continue;
|
||
bool closed = false;
|
||
std::vector<Vec2d> poly = entity_polyline(e, closed);
|
||
draw_quad_strip(m_highlight_model, poly, closed, color);
|
||
}
|
||
}
|
||
|
||
// ---- In-canvas edit-op gizmo (Fillet/Chamfer/Offset/Mirror toolbar tools) ----------
|
||
// These tools replace the docked numeric card. They operate on the LIVE session's
|
||
// m_entities/m_constraints, so they work both while drawing and after begin_edit re-opens
|
||
// a committed sketch. The SketchEngine op (context-free) is reused verbatim; the
|
||
// constraint binding is ported from DesignPanel::apply_entity_constraint into m_constraints
|
||
// via try_add_constraints (append→solve→keep / rollback).
|
||
|
||
void DesignSketchTool::reset_op()
|
||
{
|
||
m_op_a = m_op_b = -1;
|
||
m_op_value = 0.0;
|
||
m_op_anchor = Vec2d(0, 0);
|
||
m_op_dir = Vec2d(0, 0);
|
||
m_op_label = Vec2d(1e18, 1e18);
|
||
m_op_ghost.clear();
|
||
m_op_dragging_arrow = false;
|
||
m_mirror_targets.clear();
|
||
}
|
||
|
||
// ---- Imported-art bounding-box transform gizmo (Mode::TransformArt) ----
|
||
|
||
void DesignSketchTool::reset_xform()
|
||
{
|
||
m_xform_base.clear();
|
||
m_xform_feat = -1;
|
||
m_xform_handle = -1;
|
||
m_xform_offset = Vec2d(0, 0);
|
||
m_xform_sx = m_xform_sy = 1.0;
|
||
m_xform_min = m_xform_max = Vec2d(0, 0);
|
||
}
|
||
|
||
void DesignSketchTool::begin_imported_transform(
|
||
int feat, const std::vector<std::vector<std::vector<Vec2d>>>& base_regions,
|
||
const SketchPlane& plane, const Vec2d& offset, double sx, double sy)
|
||
{
|
||
cancel(); // drop any prior session, clears state
|
||
m_plane = plane;
|
||
m_mode = Mode::TransformArt;
|
||
m_xform_base = base_regions;
|
||
m_xform_feat = feat;
|
||
m_xform_offset = offset;
|
||
m_xform_sx = (std::abs(sx) > 1e-6) ? sx : 1.0;
|
||
m_xform_sy = (std::abs(sy) > 1e-6) ? sy : 1.0;
|
||
m_xform_handle = -1;
|
||
Vec2d mn(1e30, 1e30), mx(-1e30, -1e30);
|
||
for (const auto& region : m_xform_base)
|
||
for (const auto& contour : region)
|
||
for (const Vec2d& p : contour) {
|
||
mn.x() = std::min(mn.x(), p.x()); mn.y() = std::min(mn.y(), p.y());
|
||
mx.x() = std::max(mx.x(), p.x()); mx.y() = std::max(mx.y(), p.y());
|
||
}
|
||
if (mx.x() < mn.x()) { mn = Vec2d(0, 0); mx = Vec2d(0, 0); }
|
||
m_xform_min = mn; m_xform_max = mx;
|
||
m_active = true;
|
||
m_has_cursor = false;
|
||
}
|
||
|
||
// 4 bbox corners in plane coords: 0=min/min, 1=max/min, 2=max/max, 3=min/max. The art
|
||
// transform is world = base*scale + offset (CadFeature import convention).
|
||
void DesignSketchTool::xform_world_corners(Vec2d out[4]) const
|
||
{
|
||
const double x0 = m_xform_min.x() * m_xform_sx + m_xform_offset.x();
|
||
const double x1 = m_xform_max.x() * m_xform_sx + m_xform_offset.x();
|
||
const double y0 = m_xform_min.y() * m_xform_sy + m_xform_offset.y();
|
||
const double y1 = m_xform_max.y() * m_xform_sy + m_xform_offset.y();
|
||
out[0] = Vec2d(x0, y0); out[1] = Vec2d(x1, y0);
|
||
out[2] = Vec2d(x1, y1); out[3] = Vec2d(x0, y1);
|
||
}
|
||
|
||
int DesignSketchTool::hit_test_xform_handle(const Vec2d& p, double tol) const
|
||
{
|
||
Vec2d c[4]; xform_world_corners(c);
|
||
int best = -1; double bd = tol;
|
||
for (int i = 0; i < 4; ++i) { const double d = (c[i] - p).norm(); if (d < bd) { bd = d; best = i; } }
|
||
if (best >= 0) return best;
|
||
if ((0.5 * (c[0] + c[2]) - p).norm() <= tol) return 4; // centre move-handle
|
||
return -1;
|
||
}
|
||
|
||
void DesignSketchTool::drag_xform_handle(const Vec2d& target)
|
||
{
|
||
if (m_xform_handle < 0) return;
|
||
if (m_xform_handle == 4) { // centre move: translate by cursor delta
|
||
m_xform_offset += (target - m_xform_anchor);
|
||
m_xform_anchor = target;
|
||
emit_xform();
|
||
return;
|
||
}
|
||
// Corner scale: hold the opposite corner (fixed world anchor O), send the grabbed
|
||
// corner to the cursor. base coords of grabbed (bg) and opposite (ba) corners.
|
||
auto base_corner = [&](int i) {
|
||
return Vec2d((i == 1 || i == 2) ? m_xform_max.x() : m_xform_min.x(),
|
||
(i == 2 || i == 3) ? m_xform_max.y() : m_xform_min.y());
|
||
};
|
||
const int h = m_xform_handle;
|
||
const Vec2d bg = base_corner(h);
|
||
const Vec2d ba = base_corner((h + 2) % 4);
|
||
const Vec2d O = m_xform_anchor;
|
||
const double dbx = bg.x() - ba.x(), dby = bg.y() - ba.y();
|
||
if (std::abs(dbx) > 1e-9) {
|
||
double nsx = (target.x() - O.x()) / dbx;
|
||
if (std::abs(nsx) < 1e-4) nsx = (nsx < 0 ? -1e-4 : 1e-4);
|
||
m_xform_sx = nsx;
|
||
m_xform_offset.x() = O.x() - ba.x() * nsx;
|
||
}
|
||
if (std::abs(dby) > 1e-9) {
|
||
double nsy = (target.y() - O.y()) / dby;
|
||
if (std::abs(nsy) < 1e-4) nsy = (nsy < 0 ? -1e-4 : 1e-4);
|
||
m_xform_sy = nsy;
|
||
m_xform_offset.y() = O.y() - ba.y() * nsy;
|
||
}
|
||
emit_xform();
|
||
}
|
||
|
||
void DesignSketchTool::emit_xform()
|
||
{
|
||
if (on_imported_transform)
|
||
on_imported_transform(m_xform_feat, m_xform_offset, m_xform_sx, m_xform_sy);
|
||
}
|
||
|
||
void DesignSketchTool::render_xform_gizmo()
|
||
{
|
||
if (m_mode != Mode::TransformArt) return;
|
||
Vec2d c[4]; xform_world_corners(c);
|
||
const ColorRGBA box(0.30f, 0.88f, 0.66f, 1.0f);
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
for (int i = 0; i < 4; ++i) segs.emplace_back(c[i], c[(i + 1) % 4]);
|
||
draw_strokes(m_highlight_model, segs, 0.6, box);
|
||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||
const double hs = 7.0 / std::max(cam.get_zoom(), 1e-6); // screen-constant half-size
|
||
const ColorRGBA hcol(0.30f, 0.88f, 0.66f, 1.0f);
|
||
const ColorRGBA hhot(1.0f, 0.85f, 0.2f, 1.0f);
|
||
auto square = [&](const Vec2d& q, const ColorRGBA& col) {
|
||
const std::vector<Vec2d> sq = { q + Vec2d(-hs, -hs), q + Vec2d(hs, -hs),
|
||
q + Vec2d(hs, hs), q + Vec2d(-hs, hs) };
|
||
draw_fill(m_fill_model, sq, col);
|
||
};
|
||
for (int i = 0; i < 4; ++i) square(c[i], m_xform_handle == i ? hhot : hcol);
|
||
square(0.5 * (c[0] + c[2]), m_xform_handle == 4 ? hhot : hcol);
|
||
}
|
||
|
||
bool DesignSketchTool::op_ready() const
|
||
{
|
||
switch (m_mode) {
|
||
case Mode::Fillet:
|
||
case Mode::Chamfer: return m_op_a >= 0 && m_op_b >= 0;
|
||
case Mode::Offset: return m_op_a >= 0;
|
||
case Mode::Mirror: return m_op_a >= 0 && !m_mirror_targets.empty();
|
||
default: return false;
|
||
}
|
||
}
|
||
|
||
// Corner vertex of two lines + the inward angle bisector (unit), pointing from the vertex
|
||
// into the fillet/chamfer interior. Mirrors SketchEngine::fillet_lines's geometry so the
|
||
// arrow tracks the op exactly.
|
||
bool DesignSketchTool::op_corner(int a, int b, Vec2d& C, Vec2d& bis, double& theta) const
|
||
{
|
||
if (a < 0 || b < 0 || a >= int(m_entities.size()) || b >= int(m_entities.size())) return false;
|
||
const SketchEntity& ea = m_entities[a];
|
||
const SketchEntity& eb = m_entities[b];
|
||
if (ea.type != SketchEntity::Type::Line || eb.type != SketchEntity::Type::Line) return false;
|
||
const Vec2d da = ea.p1 - ea.p0, db = eb.p1 - eb.p0;
|
||
const double denom = da.x() * db.y() - da.y() * db.x();
|
||
if (std::abs(denom) < 1e-12) return false; // parallel
|
||
const Vec2d diff = eb.p0 - ea.p0;
|
||
const double s = (diff.x() * db.y() - diff.y() * db.x()) / denom;
|
||
C = ea.p0 + s * da;
|
||
Vec2d ua = ((ea.p0 - C).norm() <= (ea.p1 - C).norm()) ? (ea.p1 - C) : (ea.p0 - C);
|
||
Vec2d ub = ((eb.p0 - C).norm() <= (eb.p1 - C).norm()) ? (eb.p1 - C) : (eb.p0 - C);
|
||
if (ua.norm() < 1e-12 || ub.norm() < 1e-12) return false;
|
||
ua.normalize(); ub.normalize();
|
||
theta = std::acos(std::max(-1.0, std::min(1.0, ua.dot(ub))));
|
||
bis = ua + ub;
|
||
if (bis.norm() < 1e-12) return false; // 180° corner
|
||
bis.normalize();
|
||
return true;
|
||
}
|
||
|
||
void DesignSketchTool::recompute_op_ghost()
|
||
{
|
||
m_op_ghost.clear();
|
||
if (m_mode == Mode::Fillet || m_mode == Mode::Chamfer) {
|
||
if (m_op_a < 0 || m_op_b < 0) return;
|
||
Vec2d C, bis; double theta;
|
||
if (op_corner(m_op_a, m_op_b, C, bis, theta)) { m_op_anchor = C; m_op_dir = bis; }
|
||
SketchEntity a_out, b_out, extra;
|
||
const bool ok = (m_mode == Mode::Fillet)
|
||
? SketchEngine::fillet_lines(m_entities[m_op_a], m_entities[m_op_b], m_op_value, a_out, b_out, extra)
|
||
: SketchEngine::chamfer_lines(m_entities[m_op_a], m_entities[m_op_b], m_op_value, a_out, b_out, extra);
|
||
if (ok) m_op_ghost = { a_out, b_out, extra };
|
||
} else if (m_mode == Mode::Offset) {
|
||
if (m_op_a < 0) return;
|
||
const SketchEntity& e = m_entities[m_op_a];
|
||
if (e.type == SketchEntity::Type::Line) {
|
||
m_op_anchor = 0.5 * (e.p0 + e.p1);
|
||
Vec2d u = e.p1 - e.p0; if (u.norm() > 1e-12) u.normalize();
|
||
m_op_dir = Vec2d(-u.y(), u.x()); // left normal = +distance side
|
||
} else if (e.type == SketchEntity::Type::Circle || e.type == SketchEntity::Type::Arc) {
|
||
m_op_anchor = e.center + Vec2d(e.radius, 0.0);
|
||
m_op_dir = Vec2d(1, 0);
|
||
}
|
||
m_op_ghost = SketchEngine::offset_entities({ e }, m_op_value);
|
||
} else if (m_mode == Mode::Mirror) {
|
||
if (m_op_a < 0 || m_mirror_targets.empty()) return;
|
||
const SketchEntity& axis = m_entities[m_op_a];
|
||
std::vector<SketchEntity> src;
|
||
for (int ti : m_mirror_targets)
|
||
if (ti >= 0 && ti < int(m_entities.size())) src.push_back(m_entities[ti]);
|
||
m_op_ghost = SketchEngine::mirror_entities(src, axis.p0, axis.p1);
|
||
}
|
||
}
|
||
|
||
// Route an entity pick to the active op; sets an initial value + ghost once enough
|
||
// entities are picked. Highlights the running picks via m_selection.
|
||
void DesignSketchTool::op_pick(int ei)
|
||
{
|
||
if (ei < 0 || ei >= int(m_entities.size())) return;
|
||
const SketchEntity::Type t = m_entities[ei].type;
|
||
switch (m_mode) {
|
||
case Mode::Fillet:
|
||
case Mode::Chamfer:
|
||
if (t != SketchEntity::Type::Line) return; // corner ops need two lines
|
||
if (m_op_a < 0) m_op_a = ei;
|
||
else if (ei != m_op_a) {
|
||
m_op_b = ei;
|
||
const double la = (m_entities[m_op_a].p1 - m_entities[m_op_a].p0).norm();
|
||
const double lb = (m_entities[m_op_b].p1 - m_entities[m_op_b].p0).norm();
|
||
m_op_value = std::max(0.001, 0.2 * std::min(la, lb)); // a sensible starting size
|
||
recompute_op_ghost();
|
||
}
|
||
break;
|
||
case Mode::Offset: {
|
||
m_op_a = ei;
|
||
const SketchEntity& e = m_entities[ei];
|
||
const double sz = (e.type == SketchEntity::Type::Line) ? (e.p1 - e.p0).norm()
|
||
: std::max(e.radius * 2.0, 1.0);
|
||
m_op_value = std::max(0.001, 0.1 * sz);
|
||
recompute_op_ghost();
|
||
break;
|
||
}
|
||
case Mode::Mirror:
|
||
if (m_op_a < 0) {
|
||
if (t != SketchEntity::Type::Line) return; // axis must be a line
|
||
m_op_a = ei;
|
||
} else if (ei != m_op_a) {
|
||
auto it = std::find(m_mirror_targets.begin(), m_mirror_targets.end(), ei);
|
||
if (it == m_mirror_targets.end()) m_mirror_targets.push_back(ei);
|
||
else m_mirror_targets.erase(it);
|
||
recompute_op_ghost();
|
||
}
|
||
break;
|
||
default: break;
|
||
}
|
||
// Mirror the picks into m_selection so the existing highlight shows them.
|
||
m_selection.clear();
|
||
if (m_op_a >= 0) m_selection.push_back(m_op_a);
|
||
if (m_op_b >= 0) m_selection.push_back(m_op_b);
|
||
for (int ti : m_mirror_targets) m_selection.push_back(ti);
|
||
if (on_selection_changed) on_selection_changed(int(m_selection.size()));
|
||
}
|
||
|
||
bool DesignSketchTool::hit_test_op_arrow(const Vec2d& p, double tol) const
|
||
{
|
||
if (!op_ready() || m_mode == Mode::Mirror) return false;
|
||
const Vec2d tip = m_op_anchor + m_op_dir * m_op_value;
|
||
return point_segment_dist(p, m_op_anchor, tip) <= tol * 1.5;
|
||
}
|
||
|
||
void DesignSketchTool::drag_op_arrow(const Vec2d& target)
|
||
{
|
||
const double v = (target - m_op_anchor).dot(m_op_dir); // project onto the arrow axis
|
||
if (m_mode == Mode::Offset) m_op_value = v; // signed: chooses the side
|
||
else m_op_value = std::max(0.001, v);// fillet/chamfer: positive
|
||
recompute_op_ghost();
|
||
}
|
||
|
||
void DesignSketchTool::open_op_editor()
|
||
{
|
||
if (!on_inline_edit || !op_ready() || m_mode == Mode::Mirror) return;
|
||
const double sign = (m_mode == Mode::Offset && m_op_value < 0) ? -1.0 : 1.0;
|
||
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
||
on_inline_edit(px, std::abs(m_op_value), "",
|
||
[this, sign](double v) {
|
||
m_op_value = (m_mode == Mode::Offset) ? sign * std::abs(v) : std::max(0.001, v);
|
||
// Entering a radius IS the commit. Leaving it as a preview meant the most obvious
|
||
// route of all — click the radius, type it, press Return — ended with the value set,
|
||
// the ghost drawn, and no geometry written; the only paths that ever applied it were
|
||
// finishing the whole sketch or clicking empty space, neither of which is signposted.
|
||
if (op_ready()) confirm_op();
|
||
else recompute_op_ghost();
|
||
},
|
||
[]() {});
|
||
}
|
||
|
||
void DesignSketchTool::render_op_gizmo(double unit_per_px)
|
||
{
|
||
m_op_label = Vec2d(1e18, 1e18);
|
||
if (!op_ready()) return;
|
||
const ColorRGBA ghostc(0.30f, 0.88f, 0.66f, 0.55f);
|
||
draw_entities_preview(m_op_ghost, ghostc);
|
||
if (m_mode == Mode::Mirror) return; // pick-only, no arrow/label
|
||
const ColorRGBA dc(0.30f, 0.88f, 0.66f, 1.0f);
|
||
const double th = std::max(15.0 * unit_per_px, 1e-4);
|
||
const Vec2d dir = (m_op_value >= 0 ? m_op_dir : -m_op_dir);
|
||
const Vec2d tip = m_op_anchor + m_op_dir * m_op_value; // signed length picks the side
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
segs.emplace_back(m_op_anchor, tip);
|
||
const double as = std::max(std::abs(m_op_value) * 0.18, th * 0.8); // arrowhead size
|
||
const Vec2d nrm(-dir.y(), dir.x());
|
||
const Vec2d back = tip - dir * as;
|
||
segs.emplace_back(tip, back + nrm * (as * 0.5));
|
||
segs.emplace_back(tip, back - nrm * (as * 0.5));
|
||
draw_strokes(m_highlight_model, segs, 0.6, dc);
|
||
DimAnnot a;
|
||
a.kind = (m_mode == Mode::Fillet) ? DimType::Radius : DimType::Distance;
|
||
a.value = std::abs(m_op_value);
|
||
m_op_label = tip + dir * (th * 1.2);
|
||
draw_text(m_line_model, dim_text(a), m_op_label, th, dc);
|
||
}
|
||
|
||
void DesignSketchTool::confirm_op()
|
||
{
|
||
if (!op_ready()) return;
|
||
using R = SketchPointRole;
|
||
using CT = SketchConstraintType;
|
||
|
||
if (m_mode == Mode::Fillet || m_mode == Mode::Chamfer) {
|
||
const bool fillet = (m_mode == Mode::Fillet);
|
||
SketchEntity a_out, b_out, extra;
|
||
const bool ok = fillet
|
||
? SketchEngine::fillet_lines(m_entities[m_op_a], m_entities[m_op_b], m_op_value, a_out, b_out, extra)
|
||
: SketchEngine::chamfer_lines(m_entities[m_op_a], m_entities[m_op_b], m_op_value, a_out, b_out, extra);
|
||
if (!ok) { reset_op(); return; }
|
||
const int a = m_op_a, b = m_op_b;
|
||
m_entities[a] = a_out; m_entities[b] = b_out;
|
||
const int xi = int(m_entities.size());
|
||
m_entities.push_back(extra); // fillet arc / chamfer segment
|
||
auto role_near = [](const SketchEntity& ln, const Vec2d& q) -> R {
|
||
return ((ln.p0 - q).squaredNorm() <= (ln.p1 - q).squaredNorm()) ? R::P0 : R::P1; };
|
||
const R ra = role_near(m_entities[a], extra.p0);
|
||
const R rb = role_near(m_entities[b], extra.p1);
|
||
// Drop the now-stale corner Coincident + each line's own length Distance (the op
|
||
// trimmed both legs back), then bind the new entity onto the trimmed endpoints.
|
||
auto refs = [](const SketchEntityConstraintDef& d, int e, R r) {
|
||
return (d.ea == e && d.ra == r) || (d.eb == e && d.rb == r); };
|
||
auto self_len = [](const SketchEntityConstraintDef& d, int e) {
|
||
return d.type == CT::Distance && d.ea == e && d.eb == e; };
|
||
auto& cs = m_constraints;
|
||
cs.erase(std::remove_if(cs.begin(), cs.end(), [&](const SketchEntityConstraintDef& d) {
|
||
return (d.type == CT::Coincident && refs(d, a, ra) && refs(d, b, rb))
|
||
|| self_len(d, a) || self_len(d, b);
|
||
}), cs.end());
|
||
auto coin = [&](R xr, int ln, R lr) {
|
||
SketchEntityConstraintDef d; d.type = CT::Coincident; d.ea = xi; d.ra = xr; d.eb = ln; d.rb = lr; return d; };
|
||
if (fillet) {
|
||
auto tang = [&](int ln) {
|
||
SketchEntityConstraintDef d; d.type = CT::Tangent; d.ea = xi; d.eb = ln; return d; };
|
||
const std::vector<std::vector<SketchEntityConstraintDef>> ladder = {
|
||
{ coin(R::P0, a, ra), coin(R::P1, b, rb), tang(a), tang(b) },
|
||
{ coin(R::P0, a, ra), coin(R::P1, b, rb), tang(a) },
|
||
{ coin(R::P0, a, ra), coin(R::P1, b, rb) },
|
||
};
|
||
for (const auto& set : ladder) if (try_add_constraints(set)) break;
|
||
} else {
|
||
try_add_constraints({ coin(R::P0, a, ra), coin(R::P1, b, rb) });
|
||
}
|
||
} else if (m_mode == Mode::Offset) {
|
||
const int a = m_op_a;
|
||
auto out = SketchEngine::offset_entities({ m_entities[a] }, m_op_value);
|
||
if (out.empty()) { reset_op(); return; }
|
||
const int ni = int(m_entities.size());
|
||
for (auto& o : out) m_entities.push_back(o);
|
||
const SketchEntity::Type st = m_entities[a].type;
|
||
SketchEntityConstraintDef d; d.ea = a; d.eb = ni;
|
||
bool emit = true;
|
||
if (st == SketchEntity::Type::Line) d.type = CT::Parallel;
|
||
else if (st == SketchEntity::Type::Arc || st == SketchEntity::Type::Circle) d.type = CT::Concentric;
|
||
else emit = false;
|
||
if (emit) try_add_constraints({ d });
|
||
} else if (m_mode == Mode::Mirror) {
|
||
const SketchEntity axis = m_entities[m_op_a]; // by value (m_entities grows below)
|
||
for (int ti : m_mirror_targets) {
|
||
if (ti < 0 || ti >= int(m_entities.size())) continue;
|
||
auto out = SketchEngine::mirror_entities({ m_entities[ti] }, axis.p0, axis.p1);
|
||
if (out.empty()) continue;
|
||
const int mi = int(m_entities.size());
|
||
for (auto& m : out) m_entities.push_back(m);
|
||
SketchEntityConstraintDef d; d.type = CT::Symmetric; d.ea = ti; d.eb = mi; d.ec = m_op_a;
|
||
const SketchEntity::Type st = m_entities[ti].type;
|
||
std::vector<SketchEntityConstraintDef> cand;
|
||
if (st == SketchEntity::Type::Line) {
|
||
d.ra = R::P0; d.rb = R::P0; cand.push_back(d);
|
||
d.ra = R::P1; d.rb = R::P1; cand.push_back(d);
|
||
} else if (st == SketchEntity::Type::Arc || st == SketchEntity::Type::Circle) {
|
||
d.ra = R::Center; d.rb = R::Center; cand.push_back(d);
|
||
} else if (st == SketchEntity::Type::Point) {
|
||
d.ra = R::P0; d.rb = R::P0; cand.push_back(d);
|
||
}
|
||
if (!cand.empty()) try_add_constraints(cand);
|
||
}
|
||
}
|
||
reset_op();
|
||
m_selection.clear();
|
||
resolve_live();
|
||
if (on_selection_changed) on_selection_changed(0);
|
||
}
|
||
|
||
// ---- In-canvas transform gizmo (Move/Rotate/Scale/Array/PolarArray) ----
|
||
// These replace the docked numeric cards: pick subject entities in-canvas, then a single
|
||
// draggable handle drives the continuous parameter (Move/Array offset, Rotate/Polar angle,
|
||
// Scale factor) and an editable value label sets it exactly; Array/PolarArray expose a
|
||
// second label for the copy count. A live translucent ghost previews the result. Confirm
|
||
// applies the geometry and emits the per-op constraint web (mutating ops drop the classes
|
||
// the map invalidates; additive ops bind each copy to its source) into m_constraints.
|
||
|
||
void DesignSketchTool::reset_tf()
|
||
{
|
||
m_tf_targets.clear();
|
||
m_tf_pivot = Vec2d(0, 0);
|
||
m_tf_delta = Vec2d(0, 0);
|
||
m_tf_angle = 0.0;
|
||
m_tf_scale = 1.0;
|
||
m_tf_count = 3;
|
||
m_tf_handle_r = 1.0;
|
||
m_tf_ghost.clear();
|
||
m_tf_handle = -1;
|
||
m_tf_dragging = false;
|
||
m_tf_label_a = Vec2d(1e18, 1e18);
|
||
m_tf_label_b = Vec2d(1e18, 1e18);
|
||
}
|
||
|
||
bool DesignSketchTool::tf_ready() const { return !m_tf_targets.empty(); }
|
||
|
||
// Centroid of the picked subject set (the rotate/scale/polar pivot, and the array origin),
|
||
// plus a reference radius (max distance from the pivot to any subject extremum) used to
|
||
// size the rotate/polar handle ring and the scale handle's unit position.
|
||
void DesignSketchTool::compute_tf_pivot()
|
||
{
|
||
using T = SketchEntity::Type;
|
||
auto cen = [](const SketchEntity& e) -> Vec2d {
|
||
switch (e.type) {
|
||
case T::Line: return 0.5 * (e.p0 + e.p1);
|
||
case T::Arc: case T::Circle: case T::Ellipse: case T::EllipseArc: return e.center;
|
||
case T::BSpline:
|
||
if (!e.ctrl.empty()) { Vec2d s(0, 0); for (const auto& p : e.ctrl) s += p; return s / double(e.ctrl.size()); }
|
||
return 0.5 * (e.p0 + e.p1);
|
||
default: return e.p0;
|
||
}
|
||
};
|
||
Vec2d c(0, 0); int n = 0;
|
||
for (int ti : m_tf_targets)
|
||
if (ti >= 0 && ti < int(m_entities.size())) { c += cen(m_entities[ti]); ++n; }
|
||
if (n == 0) { m_tf_pivot = Vec2d(0, 0); m_tf_handle_r = 1.0; return; }
|
||
m_tf_pivot = c / double(n);
|
||
double r = 0.0;
|
||
for (int ti : m_tf_targets) {
|
||
if (ti < 0 || ti >= int(m_entities.size())) continue;
|
||
const SketchEntity& e = m_entities[ti];
|
||
auto upd = [&](const Vec2d& p) { r = std::max(r, (p - m_tf_pivot).norm()); };
|
||
switch (e.type) {
|
||
case T::Line: upd(e.p0); upd(e.p1); break;
|
||
case T::Arc: case T::Circle: case T::Ellipse: case T::EllipseArc:
|
||
upd(e.center + Vec2d(e.radius, 0)); upd(e.center - Vec2d(e.radius, 0)); break;
|
||
case T::BSpline: for (const auto& p : e.ctrl) upd(p); break;
|
||
default: upd(e.p0); break;
|
||
}
|
||
}
|
||
m_tf_handle_r = std::max(r, 1.0);
|
||
}
|
||
|
||
void DesignSketchTool::tf_pick(int ei)
|
||
{
|
||
if (ei < 0 || ei >= int(m_entities.size())) return;
|
||
auto it = std::find(m_tf_targets.begin(), m_tf_targets.end(), ei);
|
||
if (it == m_tf_targets.end()) m_tf_targets.push_back(ei); // toggle-select like Mirror
|
||
else m_tf_targets.erase(it);
|
||
compute_tf_pivot();
|
||
// Seed sensible starting parameters (mirrors the retired card defaults so the ghost is
|
||
// immediately visible). Only seed while still at the neutral value, so re-picking more
|
||
// targets keeps a value the user already dialled in.
|
||
if (!m_tf_targets.empty()) {
|
||
const double step = std::max(m_tf_handle_r * 1.5, 1.0);
|
||
switch (m_mode) {
|
||
case Mode::Move:
|
||
if (m_tf_delta.norm() < 1e-9) m_tf_delta = Vec2d(step, 0.0);
|
||
break;
|
||
case Mode::Array:
|
||
if (m_tf_delta.norm() < 1e-9) {
|
||
Vec2d d(step, 0.0); // default: perpendicular to a single line, else +X
|
||
if (m_tf_targets.size() == 1) {
|
||
const SketchEntity& e = m_entities[m_tf_targets[0]];
|
||
if (e.type == SketchEntity::Type::Line) {
|
||
Vec2d t = e.p1 - e.p0;
|
||
if (t.norm() > 1e-9) { t.normalize(); d = Vec2d(-t.y(), t.x()) * step; }
|
||
}
|
||
}
|
||
m_tf_delta = d;
|
||
}
|
||
break;
|
||
case Mode::Rotate: if (std::abs(m_tf_angle) < 1e-9) m_tf_angle = M_PI / 4.0; break; // 45°
|
||
case Mode::PolarArray: if (std::abs(m_tf_angle) < 1e-9) m_tf_angle = 2.0 * M_PI; break; // 360°
|
||
case Mode::Scale: if (std::abs(m_tf_scale - 1.0) < 1e-9) m_tf_scale = 2.0; break;
|
||
default: break;
|
||
}
|
||
}
|
||
recompute_tf_ghost();
|
||
m_selection = m_tf_targets; // reuse the existing selection highlight
|
||
if (on_selection_changed) on_selection_changed(int(m_selection.size()));
|
||
}
|
||
|
||
void DesignSketchTool::recompute_tf_ghost()
|
||
{
|
||
m_tf_ghost.clear();
|
||
if (m_tf_targets.empty()) return;
|
||
std::vector<SketchEntity> src;
|
||
for (int ti : m_tf_targets)
|
||
if (ti >= 0 && ti < int(m_entities.size())) src.push_back(m_entities[ti]);
|
||
if (src.empty()) return;
|
||
const int count = std::max(2, m_tf_count);
|
||
switch (m_mode) {
|
||
case Mode::Move:
|
||
m_tf_ghost = SketchEngine::transform_entities(src, m_tf_delta, 0.0, 1.0, Vec2d(0, 0));
|
||
break;
|
||
case Mode::Rotate:
|
||
m_tf_ghost = SketchEngine::transform_entities(src, Vec2d(0, 0), m_tf_angle, 1.0, m_tf_pivot);
|
||
break;
|
||
case Mode::Scale:
|
||
m_tf_ghost = SketchEngine::transform_entities(src, Vec2d(0, 0), 0.0, m_tf_scale, m_tf_pivot);
|
||
break;
|
||
case Mode::Array:
|
||
m_tf_ghost = SketchEngine::array_entities(src, count, m_tf_delta, 0.0, m_tf_pivot);
|
||
break;
|
||
case Mode::PolarArray:
|
||
m_tf_ghost = SketchEngine::array_entities(src, count, Vec2d(0, 0), m_tf_angle / double(count), m_tf_pivot);
|
||
break;
|
||
default: break;
|
||
}
|
||
}
|
||
|
||
// World position of the single drag handle: at the translated/spacing tip for the linear
|
||
// ops, on the pivot-centred ring at the current angle for the rotational ops, and at the
|
||
// scaled unit position along +X for Scale.
|
||
Vec2d DesignSketchTool::tf_handle_pos() const
|
||
{
|
||
switch (m_mode) {
|
||
case Mode::Move:
|
||
case Mode::Array: return m_tf_pivot + m_tf_delta;
|
||
case Mode::Rotate:
|
||
case Mode::PolarArray: return m_tf_pivot + m_tf_handle_r * Vec2d(std::cos(m_tf_angle), std::sin(m_tf_angle));
|
||
case Mode::Scale: return m_tf_pivot + Vec2d(m_tf_scale * m_tf_handle_r, 0.0);
|
||
default: return m_tf_pivot;
|
||
}
|
||
}
|
||
|
||
bool DesignSketchTool::hit_test_tf_handle(const Vec2d& p, double tol) const
|
||
{
|
||
if (!tf_ready()) return false;
|
||
return (tf_handle_pos() - p).norm() <= tol * 2.5;
|
||
}
|
||
|
||
void DesignSketchTool::drag_tf_handle(const Vec2d& target)
|
||
{
|
||
switch (m_mode) {
|
||
case Mode::Move:
|
||
case Mode::Array:
|
||
m_tf_delta = target - m_tf_pivot;
|
||
break;
|
||
case Mode::Rotate:
|
||
case Mode::PolarArray: {
|
||
const Vec2d d = target - m_tf_pivot;
|
||
if (d.norm() > 1e-9) m_tf_angle = std::atan2(d.y(), d.x());
|
||
break;
|
||
}
|
||
case Mode::Scale: {
|
||
const double r = (target - m_tf_pivot).norm();
|
||
m_tf_scale = std::max(1e-3, r / std::max(m_tf_handle_r, 1e-9));
|
||
break;
|
||
}
|
||
default: break;
|
||
}
|
||
recompute_tf_ghost();
|
||
}
|
||
|
||
void DesignSketchTool::open_tf_editor_a()
|
||
{
|
||
if (!on_inline_edit || !tf_ready()) return;
|
||
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
||
double cur;
|
||
if (m_mode == Mode::Rotate || m_mode == Mode::PolarArray) cur = std::abs(m_tf_angle) * 180.0 / M_PI;
|
||
else if (m_mode == Mode::Scale) cur = m_tf_scale;
|
||
else cur = m_tf_delta.norm();
|
||
Vec2d dir = m_tf_delta; if (dir.norm() > 1e-9) dir.normalize(); else dir = Vec2d(1, 0);
|
||
const double sgn = (m_tf_angle < 0) ? -1.0 : 1.0;
|
||
on_inline_edit(px, cur, "",
|
||
[this, dir, sgn](double v) {
|
||
switch (m_mode) {
|
||
case Mode::Move: case Mode::Array: m_tf_delta = dir * v; break;
|
||
case Mode::Rotate: case Mode::PolarArray: m_tf_angle = sgn * std::abs(v) * M_PI / 180.0; break;
|
||
case Mode::Scale: m_tf_scale = std::max(1e-3, v); break;
|
||
default: break;
|
||
}
|
||
recompute_tf_ghost();
|
||
},
|
||
[]() {});
|
||
}
|
||
|
||
void DesignSketchTool::open_tf_editor_count()
|
||
{
|
||
if (!on_inline_edit || !tf_ready()) return;
|
||
if (m_mode != Mode::Array && m_mode != Mode::PolarArray) return;
|
||
const wxPoint px(m_last_mouse_x, m_last_mouse_y);
|
||
on_inline_edit(px, double(std::max(2, m_tf_count)), "",
|
||
[this](double v) { m_tf_count = std::max(2, int(v + 0.5)); recompute_tf_ghost(); },
|
||
[]() {});
|
||
}
|
||
|
||
void DesignSketchTool::render_tf_gizmo(double unit_per_px)
|
||
{
|
||
m_tf_label_a = Vec2d(1e18, 1e18);
|
||
m_tf_label_b = Vec2d(1e18, 1e18);
|
||
if (!tf_ready()) return;
|
||
const ColorRGBA ghostc(0.30f, 0.88f, 0.66f, 0.55f);
|
||
draw_entities_preview(m_tf_ghost, ghostc);
|
||
|
||
const ColorRGBA dc(0.30f, 0.88f, 0.66f, 1.0f);
|
||
const ColorRGBA hot(1.0f, 0.85f, 0.2f, 1.0f);
|
||
const double th = std::max(15.0 * unit_per_px, 1e-4);
|
||
const Vec2d handle = tf_handle_pos();
|
||
|
||
// spoke from the pivot to the handle (+ arrowhead for the linear ops).
|
||
std::vector<std::pair<Vec2d, Vec2d>> segs;
|
||
segs.emplace_back(m_tf_pivot, handle);
|
||
if (m_mode == Mode::Move || m_mode == Mode::Array || m_mode == Mode::Scale) {
|
||
Vec2d dir = handle - m_tf_pivot; const double L = dir.norm();
|
||
if (L > 1e-9) {
|
||
dir /= L;
|
||
const double as = std::max(L * 0.15, th * 0.8);
|
||
const Vec2d nrm(-dir.y(), dir.x());
|
||
const Vec2d back = handle - dir * as;
|
||
segs.emplace_back(handle, back + nrm * (as * 0.5));
|
||
segs.emplace_back(handle, back - nrm * (as * 0.5));
|
||
}
|
||
}
|
||
draw_strokes(m_highlight_model, segs, 0.6, dc);
|
||
|
||
const double hs = 6.0 * unit_per_px; // screen-constant handle marker
|
||
const std::vector<Vec2d> sq = { handle + Vec2d(-hs, -hs), handle + Vec2d(hs, -hs),
|
||
handle + Vec2d(hs, hs), handle + Vec2d(-hs, hs) };
|
||
draw_fill(m_fill_model, sq, m_tf_dragging ? hot : dc);
|
||
draw_vertices(m_vertex_model, { m_tf_pivot }, dc, std::max(3.0 * unit_per_px, 1e-4));
|
||
|
||
// primary parameter label at the handle.
|
||
std::string txt_a;
|
||
if (m_mode == Mode::Rotate || m_mode == Mode::PolarArray) {
|
||
DimAnnot a; a.kind = DimType::Angle; a.value = std::abs(m_tf_angle) * 180.0 / M_PI;
|
||
txt_a = dim_text(a);
|
||
} else if (m_mode == Mode::Scale) {
|
||
char b[24]; std::snprintf(b, sizeof(b), "x%.2f", m_tf_scale);
|
||
for (char& ch : b) if (ch == ',') ch = '.';
|
||
txt_a = b;
|
||
} else {
|
||
DimAnnot a; a.kind = DimType::Distance; a.value = m_tf_delta.norm();
|
||
txt_a = dim_text(a);
|
||
}
|
||
Vec2d outw = handle - m_tf_pivot; if (outw.norm() > 1e-9) outw.normalize(); else outw = Vec2d(1, 0);
|
||
m_tf_label_a = handle + outw * (th * 1.2);
|
||
draw_text(m_line_model, txt_a, m_tf_label_a, th, dc);
|
||
|
||
if (m_mode == Mode::Array || m_mode == Mode::PolarArray) {
|
||
char cb[16]; std::snprintf(cb, sizeof(cb), "x%d", std::max(2, m_tf_count));
|
||
m_tf_label_b = m_tf_pivot + Vec2d(th * 1.5, th * 1.5);
|
||
draw_text(m_line_model, cb, m_tf_label_b, th, dc);
|
||
}
|
||
}
|
||
|
||
void DesignSketchTool::confirm_transform()
|
||
{
|
||
if (!tf_ready()) { reset_tf(); return; }
|
||
using CT = SketchConstraintType;
|
||
const std::vector<int> targets = m_tf_targets; // snapshot by value (m_entities grows)
|
||
auto is_target = [&](int e) {
|
||
return e >= 0 && std::find(targets.begin(), targets.end(), e) != targets.end();
|
||
};
|
||
|
||
if (m_mode == Mode::Move || m_mode == Mode::Rotate || m_mode == Mode::Scale) {
|
||
// MUTATING: map every subject in place, then drop the constraint classes the map
|
||
// invalidates for any constraint touching a subject. Surviving classes are
|
||
// satisfied by construction; the re-solve folds in the new placement.
|
||
const Mode mode = m_mode;
|
||
for (int ti : targets) {
|
||
if (ti < 0 || ti >= int(m_entities.size())) continue;
|
||
std::vector<SketchEntity> out;
|
||
if (mode == Mode::Move)
|
||
out = SketchEngine::transform_entities({ m_entities[ti] }, m_tf_delta, 0.0, 1.0, Vec2d(0, 0));
|
||
else if (mode == Mode::Rotate)
|
||
out = SketchEngine::transform_entities({ m_entities[ti] }, Vec2d(0, 0), m_tf_angle, 1.0, m_tf_pivot);
|
||
else
|
||
out = SketchEngine::transform_entities({ m_entities[ti] }, Vec2d(0, 0), 0.0, m_tf_scale, m_tf_pivot);
|
||
if (!out.empty()) m_entities[ti] = out[0];
|
||
}
|
||
auto& cs = m_constraints;
|
||
cs.erase(std::remove_if(cs.begin(), cs.end(), [&](const SketchEntityConstraintDef& d) {
|
||
if (!(is_target(d.ea) || is_target(d.eb) || is_target(d.ec))) return false;
|
||
const bool self = (d.ea == d.eb); // self-length Distance survives translate/rotate
|
||
if (mode == Mode::Move) {
|
||
switch (d.type) {
|
||
case CT::Coincident: case CT::PointOnLine: case CT::PointOnObject:
|
||
case CT::Concentric: case CT::Symmetric: case CT::Midpoint:
|
||
case CT::Fix: case CT::LockX: case CT::LockY: return true;
|
||
case CT::Distance: return !self;
|
||
default: return false; // orientation/length preserved by translation
|
||
}
|
||
} else if (mode == Mode::Rotate) {
|
||
switch (d.type) {
|
||
case CT::EqualLength: case CT::Radius: case CT::Diameter: return false;
|
||
case CT::Distance: return !self;
|
||
default: return true; // orientation + position broken by rotation
|
||
}
|
||
} else { // Scale (uniform / conformal)
|
||
switch (d.type) {
|
||
case CT::Horizontal: case CT::Vertical: case CT::Parallel:
|
||
case CT::Perpendicular: case CT::Angle: return false;
|
||
default: return true; // size + position broken
|
||
}
|
||
}
|
||
}), cs.end());
|
||
} else if (m_mode == Mode::Array || m_mode == Mode::PolarArray) {
|
||
// ADDITIVE: append copies of each subject, then bind each copy to its source. Lines
|
||
// get Parallel+EqualLength (linear) or EqualLength only (polar — rotation breaks
|
||
// Parallel); arc/circle copies get a per-copy Radius (equal-radius under any rigid
|
||
// map) plus Concentric when the polar pivot is the source's own centre. Emit each
|
||
// web as a degrade ladder (try_add_constraints keeps the first set the solver
|
||
// accepts, else the geometry stays unconstrained).
|
||
const bool polar = (m_mode == Mode::PolarArray);
|
||
const int count = std::max(2, m_tf_count);
|
||
for (int ti : targets) {
|
||
if (ti < 0 || ti >= int(m_entities.size())) continue;
|
||
const SketchEntity src = m_entities[ti]; // by value (m_entities grows below)
|
||
std::vector<SketchEntity> copies = polar
|
||
? SketchEngine::array_entities({ src }, count, Vec2d(0, 0), m_tf_angle / double(count), m_tf_pivot)
|
||
: SketchEngine::array_entities({ src }, count, m_tf_delta, 0.0, m_tf_pivot);
|
||
if (copies.empty()) continue;
|
||
const int base = int(m_entities.size());
|
||
for (auto& c : copies) m_entities.push_back(c);
|
||
const int nc = int(copies.size());
|
||
const SketchEntity::Type st = src.type;
|
||
std::vector<std::vector<SketchEntityConstraintDef>> ladder;
|
||
if (st == SketchEntity::Type::Line) {
|
||
auto mk = [&](bool eq, bool par) {
|
||
std::vector<SketchEntityConstraintDef> w;
|
||
for (int k = 0; k < nc; ++k) {
|
||
const int ci = base + k;
|
||
if (par) { SketchEntityConstraintDef dp; dp.type = CT::Parallel; dp.ea = ti; dp.eb = ci; w.push_back(dp); }
|
||
if (eq) { SketchEntityConstraintDef de; de.type = CT::EqualLength; de.ea = ti; de.eb = ci; w.push_back(de); }
|
||
}
|
||
return w;
|
||
};
|
||
if (polar) ladder = { mk(true, false) };
|
||
else ladder = { mk(true, true), mk(false, true) };
|
||
} else if (st == SketchEntity::Type::Arc || st == SketchEntity::Type::Circle) {
|
||
const bool can_conc = polar && (m_tf_pivot - src.center).norm() < 1e-6;
|
||
auto mk = [&](bool conc) {
|
||
std::vector<SketchEntityConstraintDef> w;
|
||
for (int k = 0; k < nc; ++k) {
|
||
const int ci = base + k;
|
||
SketchEntityConstraintDef dr; dr.type = CT::Radius; dr.ea = ci; dr.value = src.radius; w.push_back(dr);
|
||
if (conc) {
|
||
SketchEntityConstraintDef dco; dco.type = CT::Concentric;
|
||
dco.ea = ti; dco.ra = SketchPointRole::Center; dco.eb = ci; dco.rb = SketchPointRole::Center;
|
||
w.push_back(dco);
|
||
}
|
||
}
|
||
return w;
|
||
};
|
||
ladder = can_conc ? std::vector<std::vector<SketchEntityConstraintDef>>{ mk(true), mk(false) }
|
||
: std::vector<std::vector<SketchEntityConstraintDef>>{ mk(false) };
|
||
}
|
||
for (auto& w : ladder) if (!w.empty() && try_add_constraints(w)) break;
|
||
}
|
||
}
|
||
reset_tf();
|
||
m_selection.clear();
|
||
resolve_live();
|
||
if (on_selection_changed) on_selection_changed(0);
|
||
}
|
||
|
||
const ColorRGBA* DesignSketchTool::sketch_hl_color(int feature) const
|
||
{
|
||
for (const auto& h : m_hl_sketches) if (h.first == feature) return &h.second;
|
||
return nullptr;
|
||
}
|
||
|
||
void DesignSketchTool::render(GLCanvas3D& canvas)
|
||
{
|
||
m_dim_label_seq = 0;
|
||
m_render_scale = canvas.get_scale();
|
||
(void)canvas;
|
||
if (!has_display()) {
|
||
if (on_readout) on_readout(std::string()); // nothing to show -> hide HUD
|
||
return;
|
||
}
|
||
render_view_helpers(); // origin planes / world axes — drawn whenever their toggle is on
|
||
m_rubber.render(canvas); // left-drag rubber band (no-op unless one is being swept). Drawn
|
||
// here, ahead of every early return below, so a band over an empty
|
||
// plate is still visible.
|
||
if (m_active && m_mode != Mode::Constrain && m_entities.empty() && m_points.empty()
|
||
&& m_display_sketches.empty()) {
|
||
if (on_readout) on_readout(std::string());
|
||
return;
|
||
}
|
||
|
||
|
||
// Draw-then-edit: a creation tool that just committed a new entity/feature (gesture now
|
||
// idle) gets its result auto-selected — so render_live_quotes below computes its quotes —
|
||
// and the primary value editor armed (opened after those quotes exist, see service block).
|
||
if (m_active && is_creation_autoedit_mode() && m_points.empty() &&
|
||
m_open_feature < 0 && !m_awaiting_length) {
|
||
const int n = int(m_entities.size());
|
||
if (m_autoedit_seen >= 0 && n > m_autoedit_seen && n > 0) {
|
||
m_selection.clear();
|
||
m_selection.push_back(n - 1); // feature_of(last) groups rect/slot/poly/ellipse
|
||
m_autoedit_pending = true;
|
||
}
|
||
m_autoedit_seen = n;
|
||
}
|
||
|
||
GLShaderProgram* shader = wxGetApp().get_shader("flat");
|
||
if (shader == nullptr)
|
||
return;
|
||
|
||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||
glsafe(::glDisable(GL_CULL_FACE));
|
||
shader->start_using();
|
||
const Camera& camera = wxGetApp().plater()->get_camera();
|
||
shader->set_uniform("view_model_matrix", camera.get_view_matrix());
|
||
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
|
||
|
||
// Persistent committed sketches (e.g. an un-consumed sketch left visible after its
|
||
// extrude is removed): faces translucent, outlines orange. Each uses its own plane.
|
||
if (!m_display_sketches.empty()) {
|
||
const SketchPlane saved_plane = m_plane;
|
||
// CYAN/BLUE MEANS SELECTED — nothing else may wear it. The unselected fill used to be
|
||
// (0.30,0.60,1.0), one shade off the selected (0.30,0.80,1.0), so an ordinary region
|
||
// read as picked and a picked one added nothing. The outlines already got this right:
|
||
// orange for a sketch, cyan for the selection. The fill now follows the same logic, so
|
||
// an unselected region is faint amber — the sketch's own colour — and every blue thing
|
||
// on screen is something you selected.
|
||
const ColorRGBA dface = design_idle_face_color(); // unselected: neutral grey, never the selection colour
|
||
const ColorRGBA sface = design_selection_color(0.34f); // selected region
|
||
const ColorRGBA dwire(1.0f, 0.55f, 0.1f, 1.0f); // normal orange outline
|
||
const ColorRGBA swire = design_selection_color(); // selected outline
|
||
glsafe(::glEnable(GL_BLEND));
|
||
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
|
||
for (const DisplaySketch& ds : m_display_sketches) {
|
||
m_plane = ds.plane;
|
||
const std::vector<RegionLoop> loops = region_loops(ds.entities);
|
||
for (int r = 0; r < int(loops.size()); ++r) {
|
||
const bool sel = (ds.feature == m_display_pick && r == m_display_pick_region);
|
||
const ColorRGBA* hlc = sketch_hl_color(ds.feature);
|
||
ColorRGBA fc = sel ? sface : dface;
|
||
if (hlc && !sel) { fc = *hlc; fc.a(0.20f); }
|
||
std::vector<std::vector<Vec2d>> hp;
|
||
for (int h : loops[r].holes)
|
||
if (h >= 0 && h < int(loops.size())) hp.push_back(loops[h].poly);
|
||
draw_fill_holed(m_fill_model, loops[r].poly, hp, fc);
|
||
}
|
||
}
|
||
glsafe(::glDisable(GL_BLEND));
|
||
for (const DisplaySketch& ds : m_display_sketches) {
|
||
m_plane = ds.plane;
|
||
// Entities forming the selected loop (highlighted cyan); the rest stay orange.
|
||
std::vector<char> sel_ent(ds.entities.size(), 0);
|
||
if (ds.feature == m_display_pick && m_display_pick_region >= 0) {
|
||
const std::vector<RegionLoop> loops = region_loops(ds.entities);
|
||
if (m_display_pick_region < int(loops.size())) {
|
||
// The holes light up with their region. The highlight has to show what will
|
||
// be EXTRUDED, and a hole is part of that — a plate whose bore stays orange
|
||
// while its outline turns cyan would say the circle is not coming along,
|
||
// which is precisely the thing that used to be true and is now not.
|
||
auto mark = [&](int region) {
|
||
if (region < 0 || region >= int(loops.size())) return;
|
||
for (int ei : loops[region].ents)
|
||
if (ei >= 0 && ei < int(sel_ent.size())) sel_ent[ei] = 1;
|
||
};
|
||
mark(m_display_pick_region);
|
||
for (int h : loops[m_display_pick_region].holes) mark(h);
|
||
}
|
||
}
|
||
for (int i = 0; i < int(ds.entities.size()); ++i) {
|
||
const SketchEntity& e = ds.entities[i];
|
||
if (e.type == SketchEntity::Type::Point) continue;
|
||
bool closed = false;
|
||
std::vector<Vec2d> poly = entity_polyline(e, closed);
|
||
const ColorRGBA* hlc = sketch_hl_color(ds.feature);
|
||
ColorRGBA wc = sel_ent[i] ? swire : (hlc ? *hlc : dwire);
|
||
draw_quad_strip(m_line_model, poly, closed, wc);
|
||
}
|
||
}
|
||
m_plane = saved_plane;
|
||
}
|
||
|
||
// Nothing else to draw when no live sketch session is active — except the solid
|
||
// face/edge highlight overlay (whole-solid tint is handled by set_body_highlight).
|
||
if (!m_active) {
|
||
render_datum_planes();
|
||
render_mate_connectors();
|
||
render_solid_highlight();
|
||
if (m_dbp_active) render_base_pick();
|
||
if (m_dz_active) render_datum_gizmo();
|
||
if (m_hx_active) render_helix_gizmo();
|
||
if (m_rb_active) render_rib_gizmo();
|
||
if (m_ex_active) render_extrude_gizmo();
|
||
if (m_mv_active) render_move_gizmo();
|
||
if (m_fl_active) render_fillet_gizmo();
|
||
if (m_hl_active) render_hole_gizmo();
|
||
if (m_th_active) render_thread_gizmo();
|
||
if (m_sh_active) render_shell_gizmo();
|
||
if (m_rv_active) render_revolve_gizmo();
|
||
if (m_dr_active) render_draft_gizmo();
|
||
if (m_ct_active) render_cut_gizmo();
|
||
if (m_pt_active) render_pattern_gizmo();
|
||
shader->stop_using();
|
||
glsafe(::glEnable(GL_CULL_FACE));
|
||
glsafe(::glEnable(GL_DEPTH_TEST));
|
||
return;
|
||
}
|
||
|
||
// Imported-art transform: only the bbox + handles over the (display-overlay) art.
|
||
if (m_mode == Mode::TransformArt) {
|
||
render_xform_gizmo();
|
||
shader->stop_using();
|
||
glsafe(::glEnable(GL_CULL_FACE));
|
||
glsafe(::glEnable(GL_DEPTH_TEST));
|
||
return;
|
||
}
|
||
|
||
const ColorRGBA orange(1.0f, 0.55f, 0.1f, 1.0f);
|
||
const ColorRGBA yellow(1.0f, 0.85f, 0.2f, 1.0f);
|
||
const ColorRGBA grey(0.55f, 0.55f, 0.60f, 1.0f);
|
||
|
||
if (m_mode == Mode::Constrain) {
|
||
const ColorRGBA cyan(0.30f, 0.80f, 1.0f, 1.0f);
|
||
const ColorRGBA red(1.0f, 0.25f, 0.25f, 1.0f);
|
||
if (m_constrain_entities) {
|
||
// Draw all entities cyan; picked Line entities highlighted red.
|
||
std::vector<Vec2d> markers;
|
||
for (size_t i = 0; i < m_entities.size(); ++i) {
|
||
const SketchEntity& e = m_entities[i];
|
||
const bool sel = (int(i) == m_pick0 || int(i) == m_pick1 || int(i) == m_pick2);
|
||
// Constraint-manager highlight: the entities a selected constraint
|
||
// references glow yellow (picked entities still win as red).
|
||
const bool hl = !sel && std::find(m_constraint_hl.begin(), m_constraint_hl.end(),
|
||
int(i)) != m_constraint_hl.end();
|
||
const ColorRGBA col = sel ? red : (hl ? yellow : cyan);
|
||
if (e.type == SketchEntity::Type::Point) { markers.push_back(e.p0); continue; }
|
||
bool closed = false;
|
||
std::vector<Vec2d> poly = entity_polyline(e, closed);
|
||
draw_quad_strip((sel || hl) ? m_highlight_model : m_line_model, poly, closed, col);
|
||
}
|
||
if (!markers.empty())
|
||
draw_vertices(m_vertex_model, markers, cyan);
|
||
// Constraint badges (C3.4b): iconic glyphs near each constraint's entity.
|
||
{
|
||
const double upp = 1.0 / std::max(camera.get_zoom(), 1e-6);
|
||
std::vector<std::pair<Vec2d, Vec2d>> glyphs;
|
||
build_constraint_glyphs(upp, glyphs);
|
||
if (!glyphs.empty()) {
|
||
const ColorRGBA badge(0.45f, 0.95f, 0.70f, 1.0f); // CAD teal-green
|
||
draw_strokes(m_fill_model, glyphs, std::max(0.9 * upp, 1e-4), badge);
|
||
}
|
||
}
|
||
shader->stop_using();
|
||
glsafe(::glEnable(GL_CULL_FACE));
|
||
glsafe(::glEnable(GL_DEPTH_TEST));
|
||
return;
|
||
}
|
||
draw_quad_strip(m_line_model, m_points, true, cyan);
|
||
draw_vertices(m_vertex_model, m_points, cyan);
|
||
if (m_sel_a >= 0 && m_sel_b >= 0 &&
|
||
m_sel_a < int(m_points.size()) && m_sel_b < int(m_points.size())) {
|
||
std::vector<Vec2d> seg = { m_points[m_sel_a], m_points[m_sel_b] };
|
||
draw_quad_strip(m_highlight_model, seg, false, red);
|
||
}
|
||
shader->stop_using();
|
||
glsafe(::glEnable(GL_CULL_FACE));
|
||
glsafe(::glEnable(GL_DEPTH_TEST));
|
||
return;
|
||
}
|
||
|
||
// Closed loops fill as translucent faces (the "closed loop = selectable face"
|
||
// affordance). Drawn first so the entity outlines paint over the fill.
|
||
{
|
||
// region_loops(), NOT closed_regions(): the latter returns raw polygons with no notion
|
||
// of nesting, so a circle drawn inside a rectangle was filled as its own solid disc on
|
||
// top of a solid rectangle. That is why a live sketch still showed a filled blue circle
|
||
// however often the COMMITTED renderer below was corrected — these are two separate
|
||
// renderers and only one of them had been taught about holes.
|
||
const std::vector<RegionLoop> loops = region_loops(m_entities);
|
||
if (!loops.empty()) {
|
||
glsafe(::glEnable(GL_BLEND));
|
||
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
|
||
// A bore is not a face. Skip loops that are somebody's hole, and cut those holes out
|
||
// of the region that owns them, so a plate with a hole LOOKS like one while drawing.
|
||
std::vector<char> is_hole(loops.size(), 0);
|
||
for (const RegionLoop& L : loops)
|
||
for (int h : L.holes)
|
||
if (h >= 0 && h < int(is_hole.size())) is_hole[h] = 1;
|
||
for (size_t r = 0; r < loops.size(); ++r) {
|
||
if (is_hole[r]) continue;
|
||
std::vector<std::vector<Vec2d>> hp;
|
||
for (int h : loops[r].holes)
|
||
if (h >= 0 && h < int(loops.size())) hp.push_back(loops[h].poly);
|
||
draw_fill_holed(m_fill_model, loops[r].poly, hp, design_idle_face_color());
|
||
}
|
||
glsafe(::glDisable(GL_BLEND));
|
||
}
|
||
}
|
||
|
||
// Committed entities of this session. DoF feedback (P3): a fully-constrained
|
||
// sketch (dof==0, consistent) paints green; entities touched by a conflicting
|
||
// constraint paint red; otherwise the under-constrained default (orange / grey
|
||
// construction). Selected entities always override to white.
|
||
const ColorRGBA white(1.0f, 1.0f, 1.0f, 1.0f);
|
||
const ColorRGBA green(0.30f, 0.85f, 0.42f, 1.0f);
|
||
const ColorRGBA conflict(1.0f, 0.22f, 0.22f, 1.0f);
|
||
const ColorRGBA editing(1.0f, 0.78f, 0.10f, 1.0f); // amber: entity whose dim is being typed
|
||
const bool fully = (m_dof == 0 && m_solve_ok);
|
||
// While an auto-edit value field is open, the active step names the entities its dimension
|
||
// drives — light them up so it's obvious WHICH feature the number (e.g. a circle's radius)
|
||
// changes.
|
||
const std::vector<int>* edit_hi =
|
||
(m_autoedit_dim_idx >= 0 && m_autoedit_dim_idx < int(m_autoedit_dims.size()))
|
||
? &m_autoedit_dims[m_autoedit_dim_idx].hi : nullptr;
|
||
std::vector<Vec2d> point_markers, sel_point_markers;
|
||
for (size_t i = 0; i < m_entities.size(); ++i) {
|
||
const SketchEntity& e = m_entities[i];
|
||
const bool selected =
|
||
std::find(m_selection.begin(), m_selection.end(), int(i)) != m_selection.end();
|
||
const bool editing_this =
|
||
edit_hi && std::find(edit_hi->begin(), edit_hi->end(), int(i)) != edit_hi->end();
|
||
const bool bad = i < m_entity_conflict.size() && m_entity_conflict[i];
|
||
ColorRGBA col;
|
||
if (editing_this) col = editing;
|
||
else if (selected) col = white;
|
||
else if (bad) col = conflict;
|
||
else if (e.construction) col = grey;
|
||
else col = fully ? green : orange;
|
||
if (e.type == SketchEntity::Type::Point) {
|
||
(selected ? sel_point_markers : point_markers).push_back(e.p0);
|
||
continue;
|
||
}
|
||
bool closed = false;
|
||
std::vector<Vec2d> poly = entity_polyline(e, closed);
|
||
draw_quad_strip((selected || editing_this) ? m_highlight_model : m_line_model, poly, closed, col);
|
||
}
|
||
if (!point_markers.empty())
|
||
draw_vertices(m_vertex_model, point_markers, yellow);
|
||
if (!sel_point_markers.empty())
|
||
draw_vertices(m_highlight_model, sel_point_markers, white);
|
||
|
||
// Endpoint / centre handles so individual points are visible and pickable in the
|
||
// Select and Dimension tools (a line = a segment + 2 points). Selected ones white.
|
||
m_show_handles = (m_mode == Mode::Select || m_mode == Mode::Dimension);
|
||
if (m_show_handles) {
|
||
std::vector<Vec2d> handles, sel_handles;
|
||
auto add_h = [&](int ei, SketchPointRole r, const Vec2d& q) {
|
||
const bool s = std::find(m_point_sel.begin(), m_point_sel.end(),
|
||
std::make_pair(ei, r)) != m_point_sel.end();
|
||
(s ? sel_handles : handles).push_back(q);
|
||
};
|
||
for (size_t i = 0; i < m_entities.size(); ++i) {
|
||
const SketchEntity& e = m_entities[i];
|
||
switch (e.type) {
|
||
case SketchEntity::Type::Line:
|
||
add_h(int(i), SketchPointRole::P0, e.p0);
|
||
add_h(int(i), SketchPointRole::P1, e.p1);
|
||
break;
|
||
case SketchEntity::Type::Arc:
|
||
case SketchEntity::Type::EllipseArc:
|
||
add_h(int(i), SketchPointRole::P0, e.p0);
|
||
add_h(int(i), SketchPointRole::P1, e.p1);
|
||
add_h(int(i), SketchPointRole::Center, e.center);
|
||
break;
|
||
case SketchEntity::Type::Circle:
|
||
case SketchEntity::Type::Ellipse:
|
||
add_h(int(i), SketchPointRole::Center, e.center);
|
||
break;
|
||
case SketchEntity::Type::BSpline:
|
||
add_h(int(i), SketchPointRole::P0, e.p0);
|
||
add_h(int(i), SketchPointRole::P1, e.p1);
|
||
break;
|
||
case SketchEntity::Type::Point:
|
||
break; // its own marker is drawn above
|
||
}
|
||
}
|
||
if (!handles.empty()) draw_vertices(m_vertex_model, handles, ColorRGBA(0.65f, 0.65f, 0.30f, 1.0f));
|
||
if (!sel_handles.empty()) draw_vertices(m_highlight_model, sel_handles, white);
|
||
|
||
// Derived feature handles (A3): the circle RadiusHandle is not a SketchPointRole,
|
||
// so the per-point pass above doesn't draw it. Render it (cyan) + the hovered
|
||
// handle (white, larger) at a screen-constant size so they stay grabbable at any
|
||
// zoom. A4 makes these draggable; later phases add slot/rect/polygon handles.
|
||
const double upp = 1.0 / std::max(camera.get_zoom(), 1e-6);
|
||
std::vector<Vec2d> radius_h;
|
||
for (const Handle& h : build_handles())
|
||
if (h.role == HandleRole::RadiusHandle || h.role == HandleRole::MajorAxis ||
|
||
h.role == HandleRole::MinorAxis || h.role == HandleRole::BSplineCtrl)
|
||
radius_h.push_back(h.pos);
|
||
if (!radius_h.empty())
|
||
draw_vertices(m_vertex_model, radius_h, ColorRGBA(0.30f, 0.75f, 0.95f, 1.0f),
|
||
std::max(4.0 * upp, 1e-4));
|
||
if (m_has_hover_handle)
|
||
draw_vertices(m_highlight_model, { m_hover_handle.pos }, white,
|
||
std::max(5.5 * upp, 1e-4));
|
||
}
|
||
|
||
// Placed dimension quotes (drawn in every mode so they persist while sketching).
|
||
// Pass plane-units-per-pixel so labels keep a constant on-screen size.
|
||
render_dimensions(1.0 / std::max(camera.get_zoom(), 1e-6));
|
||
render_live_quotes(1.0 / std::max(camera.get_zoom(), 1e-6));
|
||
// Draw-then-edit: the selection's live quotes now exist. Open the primary value editor on
|
||
// the next event-loop tick (NOT here inside the paint) so the floating field grabs focus
|
||
// cleanly — the same context the Line path opens from. m_live_quotes persists until the
|
||
// next render_live_quotes(), so the deferred open still sees this frame's values.
|
||
if (m_autoedit_pending) {
|
||
m_autoedit_pending = false;
|
||
wxGetApp().CallAfter([this] { open_primary_autoedit(); });
|
||
}
|
||
if (is_edit_op_mode())
|
||
render_op_gizmo(1.0 / std::max(camera.get_zoom(), 1e-6));
|
||
if (is_transform_mode())
|
||
render_tf_gizmo(1.0 / std::max(camera.get_zoom(), 1e-6));
|
||
|
||
// In-progress entity preview for the active tool.
|
||
const ColorRGBA preview = m_construction ? grey : orange;
|
||
switch (m_mode) {
|
||
|
||
case Mode::Select:
|
||
case Mode::Dimension:
|
||
break; // selection highlight / placed quotes are drawn above; no rubber-band
|
||
|
||
case Mode::Polyline: {
|
||
std::vector<Vec2d> pts = m_points;
|
||
if (m_has_cursor)
|
||
pts.push_back(m_cursor);
|
||
draw_quad_strip(m_highlight_model, pts, false, preview);
|
||
draw_vertices(m_vertex_model, m_points, yellow);
|
||
// Teal rubber-band = the new segment is locked to an inference angle.
|
||
if (m_cursor_locked && m_has_cursor && !m_points.empty()) {
|
||
const ColorRGBA lock(0.10f, 0.85f, 0.80f, 1.0f);
|
||
std::vector<Vec2d> seg = { m_points.back(), m_cursor };
|
||
draw_quad_strip(m_line_model, seg, false, lock);
|
||
}
|
||
break;
|
||
}
|
||
|
||
case Mode::Line: {
|
||
std::vector<Vec2d> pts = m_points;
|
||
if (m_has_cursor && m_points.size() == 1)
|
||
pts.push_back(m_cursor);
|
||
if (pts.size() >= 2) {
|
||
const ColorRGBA col = (m_cursor_locked && m_points.size() == 1)
|
||
? ColorRGBA(0.10f, 0.85f, 0.80f, 1.0f) : preview;
|
||
draw_quad_strip(m_highlight_model, pts, false, col);
|
||
}
|
||
draw_vertices(m_vertex_model, m_points, yellow);
|
||
break;
|
||
}
|
||
|
||
case Mode::CornerRect: {
|
||
if (m_points.size() == 1 && m_has_cursor) {
|
||
const Vec2d A = m_points[0];
|
||
const Vec2d B = m_cursor;
|
||
std::vector<Vec2d> corners = { A, Vec2d(B.x(), A.y()), B, Vec2d(A.x(), B.y()) };
|
||
draw_quad_strip(m_highlight_model, corners, true, preview);
|
||
}
|
||
break;
|
||
}
|
||
|
||
case Mode::CenterRect: {
|
||
if (m_points.size() == 1 && m_has_cursor) {
|
||
const Vec2d C = m_points[0];
|
||
const Vec2d P = m_cursor;
|
||
const double hx = std::abs(P.x() - C.x());
|
||
const double hy = std::abs(P.y() - C.y());
|
||
std::vector<Vec2d> corners = {
|
||
Vec2d(C.x() - hx, C.y() - hy), Vec2d(C.x() + hx, C.y() - hy),
|
||
Vec2d(C.x() + hx, C.y() + hy), Vec2d(C.x() - hx, C.y() + hy) };
|
||
draw_quad_strip(m_highlight_model, corners, true, preview);
|
||
draw_vertices(m_vertex_model, { C }, yellow);
|
||
}
|
||
break;
|
||
}
|
||
|
||
case Mode::ObliqueRect: {
|
||
draw_vertices(m_vertex_model, m_points, yellow);
|
||
if (m_points.size() == 2 && m_has_cursor) {
|
||
const Vec2d A = m_points[0], B = m_points[1];
|
||
Vec2d u = B - A;
|
||
if (u.squaredNorm() > 1e-12) {
|
||
u.normalize();
|
||
const Vec2d n(-u.y(), u.x());
|
||
const double w = n.dot(m_cursor - A);
|
||
draw_quad_strip(m_highlight_model, { A, B, B + n * w, A + n * w }, true, preview);
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
|
||
case Mode::RoundedRect: {
|
||
draw_vertices(m_vertex_model, m_points, yellow);
|
||
if (m_points.size() == 1 && m_has_cursor) {
|
||
const Vec2d A = m_points[0], B = m_cursor; // box not yet fixed: plain rect
|
||
draw_quad_strip(m_highlight_model, { A, Vec2d(B.x(), A.y()), B, Vec2d(A.x(), B.y()) }, true, preview);
|
||
} else if (m_points.size() == 2 && m_has_cursor) {
|
||
draw_entities_preview(make_rounded_rect(m_points[0], m_points[1], m_cursor), preview);
|
||
}
|
||
break;
|
||
}
|
||
|
||
case Mode::CenterCircle: {
|
||
if (m_points.size() == 1 && m_has_cursor) {
|
||
const Vec2d C = m_points[0];
|
||
const double r = (m_cursor - C).norm();
|
||
draw_quad_strip(m_highlight_model, circle_polygon(C, r), true, preview);
|
||
draw_vertices(m_vertex_model, { C }, yellow);
|
||
}
|
||
break;
|
||
}
|
||
|
||
case Mode::TwoPointCircle: {
|
||
draw_vertices(m_vertex_model, m_points, yellow);
|
||
if (m_points.size() == 1 && m_has_cursor) {
|
||
const Vec2d C = (m_points[0] + m_cursor) * 0.5;
|
||
const double r = (m_cursor - m_points[0]).norm() * 0.5;
|
||
draw_quad_strip(m_highlight_model, circle_polygon(C, r), true, preview);
|
||
}
|
||
break;
|
||
}
|
||
|
||
case Mode::ThreePointCircle: {
|
||
draw_vertices(m_vertex_model, m_points, yellow);
|
||
if (m_points.size() == 2 && m_has_cursor)
|
||
draw_entities_preview(make_three_point_circle(m_points[0], m_points[1], m_cursor), preview);
|
||
break;
|
||
}
|
||
|
||
case Mode::ThreePointArc: {
|
||
draw_vertices(m_vertex_model, m_points, yellow);
|
||
if (m_points.size() == 2 && m_has_cursor)
|
||
draw_entities_preview(make_three_point_arc(m_points[0], m_points[1], m_cursor), preview);
|
||
break;
|
||
}
|
||
|
||
case Mode::TangentArc: {
|
||
draw_vertices(m_vertex_model, m_points, yellow);
|
||
if (m_points.size() == 1 && m_has_cursor)
|
||
draw_entities_preview(make_tangent_arc(m_points[0], m_cursor), preview);
|
||
break;
|
||
}
|
||
|
||
case Mode::CenterArc: {
|
||
draw_vertices(m_vertex_model, m_points, yellow);
|
||
if (m_points.size() == 1 && m_has_cursor) {
|
||
// center placed: show the radius rubber-band as a faint guide circle
|
||
SketchEntity g; g.type = SketchEntity::Type::Circle; g.center = m_points[0];
|
||
g.p0 = m_points[0]; g.radius = (m_cursor - m_points[0]).norm(); g.construction = true;
|
||
draw_entities_preview({ g }, preview);
|
||
} else if (m_points.size() == 2 && m_has_cursor) {
|
||
draw_entities_preview(make_center_arc(m_points[0], m_points[1], m_cursor), preview);
|
||
}
|
||
break;
|
||
}
|
||
|
||
case Mode::Slot: {
|
||
draw_vertices(m_vertex_model, m_points, yellow);
|
||
if (m_points.size() == 1 && m_has_cursor) {
|
||
draw_quad_strip(m_highlight_model, { m_points[0], m_cursor }, false, grey);
|
||
} else if (m_points.size() == 2 && m_has_cursor) {
|
||
Vec2d u = m_points[1] - m_points[0];
|
||
if (u.squaredNorm() > 1e-12) {
|
||
u.normalize();
|
||
const Vec2d n(-u.y(), u.x());
|
||
const double w = std::abs(n.dot(m_cursor - m_points[0]));
|
||
draw_entities_preview(make_slot(m_points[0], m_points[1], w), preview);
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
|
||
case Mode::ArcSlot: {
|
||
draw_vertices(m_vertex_model, m_points, yellow);
|
||
if (m_points.size() == 1 && m_has_cursor) {
|
||
// center placed: faint guide circle for the centerline radius
|
||
SketchEntity g; g.type = SketchEntity::Type::Circle; g.center = m_points[0];
|
||
g.p0 = m_points[0]; g.radius = (m_cursor - m_points[0]).norm(); g.construction = true;
|
||
draw_entities_preview({ g }, preview);
|
||
} else if (m_points.size() == 2 && m_has_cursor) {
|
||
draw_entities_preview(make_center_arc(m_points[0], m_points[1], m_cursor), preview); // centerline arc
|
||
} else if (m_points.size() == 3 && m_has_cursor) {
|
||
const double Rc = (m_points[1] - m_points[0]).norm();
|
||
const double w = std::abs((m_cursor - m_points[0]).norm() - Rc);
|
||
draw_entities_preview(make_arc_slot(m_points[0], m_points[1], m_points[2], w), preview);
|
||
}
|
||
break;
|
||
}
|
||
|
||
case Mode::Polygon: {
|
||
draw_vertices(m_vertex_model, m_points, yellow);
|
||
if (m_points.size() == 1 && m_has_cursor)
|
||
draw_entities_preview(make_polygon(m_points[0], m_cursor, m_polygon_sides), preview);
|
||
break;
|
||
}
|
||
|
||
case Mode::Ellipse: {
|
||
draw_vertices(m_vertex_model, m_points, yellow);
|
||
if (m_points.size() == 1 && m_has_cursor) {
|
||
SketchEntity g; g.type = SketchEntity::Type::Line;
|
||
g.p0 = m_points[0]; g.p1 = m_cursor; g.construction = true; // major-axis rubber band
|
||
draw_entities_preview({ g }, preview);
|
||
} else if (m_points.size() == 2 && m_has_cursor) {
|
||
draw_entities_preview(make_ellipse(m_points[0], m_points[1], m_cursor), preview);
|
||
}
|
||
break;
|
||
}
|
||
|
||
case Mode::EllipseArc: {
|
||
draw_vertices(m_vertex_model, m_points, yellow);
|
||
if (m_points.size() == 1 && m_has_cursor) {
|
||
SketchEntity g; g.type = SketchEntity::Type::Line;
|
||
g.p0 = m_points[0]; g.p1 = m_cursor; g.construction = true;
|
||
draw_entities_preview({ g }, preview);
|
||
} else if (m_points.size() == 2 && m_has_cursor) {
|
||
draw_entities_preview(make_ellipse(m_points[0], m_points[1], m_cursor), preview);
|
||
} else if (m_points.size() == 3 && m_has_cursor) {
|
||
std::vector<SketchEntity> full = make_ellipse(m_points[0], m_points[1], m_points[2]);
|
||
for (auto& e : full) e.construction = true; // faint full ellipse
|
||
draw_entities_preview(full, preview);
|
||
} else if (m_points.size() == 4 && m_has_cursor) {
|
||
draw_entities_preview(make_ellipse_arc(m_points[0], m_points[1], m_points[2],
|
||
m_points[3], m_cursor), preview);
|
||
}
|
||
break;
|
||
}
|
||
|
||
case Mode::BSpline: {
|
||
std::vector<Vec2d> poles = m_points;
|
||
if (m_has_cursor) poles.push_back(m_cursor);
|
||
// Faint control polygon as a placement guide.
|
||
if (poles.size() >= 2) {
|
||
std::vector<SketchEntity> guide;
|
||
for (size_t i = 1; i < poles.size(); ++i) {
|
||
SketchEntity g; g.type = SketchEntity::Type::Line;
|
||
g.p0 = poles[i - 1]; g.p1 = poles[i]; g.construction = true;
|
||
guide.push_back(g);
|
||
}
|
||
draw_entities_preview(guide, grey);
|
||
}
|
||
// The spline curve itself.
|
||
if (poles.size() >= 2)
|
||
draw_quad_strip(m_highlight_model, bspline_polyline(poles), false, preview);
|
||
draw_vertices(m_vertex_model, m_points, yellow);
|
||
break;
|
||
}
|
||
|
||
case Mode::Trim:
|
||
case Mode::Extend: {
|
||
// Hover preview: paint the exact sub-portion a click would cut (Trim) / add (Extend)
|
||
// in red, recomputed from the cursor every frame (never persisted). The pick tolerance
|
||
// matches on_mouse: ~8 px projected to plane units, x3 (here via zoom -> units/px).
|
||
if (m_has_cursor) {
|
||
const double upp = 1.0 / std::max(camera.get_zoom(), 1e-6);
|
||
int subj = -1;
|
||
std::vector<Vec2d> removed;
|
||
const ColorRGBA cut(1.0f, 0.2f, 0.2f, 1.0f);
|
||
if (compute_trim_preview(m_cursor, 24.0 * upp, m_mode == Mode::Extend, subj, removed))
|
||
draw_quad_strip(m_highlight_model, removed, false, cut);
|
||
}
|
||
break;
|
||
}
|
||
|
||
case Mode::Point:
|
||
case Mode::Constrain:
|
||
break;
|
||
}
|
||
|
||
// Inference hint: highlight the snapped target under the cursor (C1.3). Colour
|
||
// encodes what the placed point will be Coincident/PointOnObject/Fixed onto.
|
||
if (m_has_cursor && m_mode != Mode::Constrain && m_cursor_snap.snapped()) {
|
||
ColorRGBA hint(1.0f, 0.55f, 0.1f, 1.0f); // endpoint/midpoint: orange
|
||
switch (m_cursor_snap.kind) {
|
||
case InferenceSnap::Kind::Center: hint = ColorRGBA(0.30f, 0.80f, 1.0f, 1.0f); break; // cyan
|
||
case InferenceSnap::Kind::Origin: hint = ColorRGBA(1.0f, 0.30f, 0.85f, 1.0f); break; // magenta
|
||
case InferenceSnap::Kind::OnEdge: hint = ColorRGBA(0.45f, 0.70f, 1.0f, 1.0f); break; // blue
|
||
default: break;
|
||
}
|
||
draw_vertices(m_highlight_model, { m_cursor_snap.point }, hint);
|
||
}
|
||
|
||
shader->stop_using();
|
||
glsafe(::glEnable(GL_CULL_FACE));
|
||
glsafe(::glEnable(GL_DEPTH_TEST));
|
||
|
||
if (on_readout) on_readout(build_readout()); // bottom-right viewport HUD
|
||
}
|
||
|
||
// Compact "current values" for the bottom-right HUD: the live segment being drawn (length
|
||
// + bearing) takes priority; otherwise the selected entity's characteristic quotes (the
|
||
// same values render_live_quotes just drew on the geometry).
|
||
std::string DesignSketchTool::build_readout() const
|
||
{
|
||
auto en = [](char* b) { for (char* c = b; *c; ++c) if (*c == ',') *c = '.'; };
|
||
if (m_active && (m_mode == Mode::Line || m_mode == Mode::Polyline) &&
|
||
!m_points.empty() && m_has_cursor) {
|
||
const Vec2d d = m_cursor - m_points.back();
|
||
double ang = std::atan2(d.y(), d.x()) * 180.0 / M_PI; if (ang < 0.0) ang += 360.0;
|
||
char b[80]; std::snprintf(b, sizeof(b), "L %.2f mm %.1f\xC2\xB0", d.norm(), ang);
|
||
en(b);
|
||
std::string out = b;
|
||
// Tell the user how to end a polyline chain — there's no other affordance for it.
|
||
if (m_mode == Mode::Polyline && m_points.size() >= 2)
|
||
out += " right-click or double-click to finish, click start to close";
|
||
return out;
|
||
}
|
||
if (!m_active) return std::string();
|
||
std::string out;
|
||
for (const DimAnnot& q : m_live_quotes) { // selection's Length/Radius/Width/… quotes
|
||
if (!out.empty()) out += " ";
|
||
out += dim_text(q);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
// Distance from point p to the segment [a,b] in plane (2D) coordinates.
|
||
static double point_segment_dist(const Vec2d& p, const Vec2d& a, const Vec2d& b)
|
||
{
|
||
const Vec2d ab = b - a;
|
||
const double len2 = ab.squaredNorm();
|
||
if (len2 < 1e-12)
|
||
return (p - a).norm();
|
||
double t = (p - a).dot(ab) / len2;
|
||
t = std::max(0.0, std::min(1.0, t));
|
||
return (p - (a + t * ab)).norm();
|
||
}
|
||
|
||
// Even-odd point-in-polygon test (plane coords), for picking a closed-loop interior.
|
||
static bool point_in_poly(const Vec2d& q, const std::vector<Vec2d>& poly)
|
||
{
|
||
if (poly.size() < 3) return false;
|
||
bool in = false;
|
||
for (size_t i = 0, j = poly.size() - 1; i < poly.size(); j = i++) {
|
||
const Vec2d& a = poly[i];
|
||
const Vec2d& b = poly[j];
|
||
if (((a.y() > q.y()) != (b.y() > q.y())) &&
|
||
(q.x() < (b.x() - a.x()) * (q.y() - a.y()) / (b.y() - a.y()) + a.x()))
|
||
in = !in;
|
||
}
|
||
return in;
|
||
}
|
||
|
||
// Möller–Trumbore ray/triangle intersection in 3D world space. ro=ray origin, rd=ray dir
|
||
// (not necessarily unit). Returns true + the ray parameter t (>0) of the hit.
|
||
static bool ray_triangle(const Vec3d& ro, const Vec3d& rd,
|
||
const Vec3d& v0, const Vec3d& v1, const Vec3d& v2, double& t)
|
||
{
|
||
const Vec3d e1 = v1 - v0, e2 = v2 - v0;
|
||
const Vec3d p = rd.cross(e2);
|
||
const double det = e1.dot(p);
|
||
if (std::abs(det) < 1e-12) return false; // parallel
|
||
const double inv = 1.0 / det;
|
||
const Vec3d s = ro - v0;
|
||
const double u = s.dot(p) * inv;
|
||
if (u < -1e-9 || u > 1.0 + 1e-9) return false;
|
||
const Vec3d q = s.cross(e1);
|
||
const double v = rd.dot(q) * inv;
|
||
if (v < -1e-9 || u + v > 1.0 + 1e-9) return false;
|
||
t = e2.dot(q) * inv;
|
||
return t > 1e-9;
|
||
}
|
||
|
||
// Shortest distance between an infinite ray (ro+rd) and a 3D segment [a,b].
|
||
static double ray_segment_dist3(const Vec3d& ro, const Vec3d& rd, const Vec3d& a, const Vec3d& b)
|
||
{
|
||
const Vec3d d1 = rd, d2 = b - a, r = ro - a;
|
||
const double A = d1.dot(d1), B = d1.dot(d2), C = d2.dot(d2), D = d1.dot(r), E = d2.dot(r);
|
||
const double denom = A * C - B * B;
|
||
double s = (std::abs(denom) > 1e-12) ? (A * E - B * D) / denom : 0.0; // param on segment
|
||
s = std::max(0.0, std::min(1.0, s));
|
||
double tt = (B * s - D) / std::max(A, 1e-12); // param on ray
|
||
tt = std::max(0.0, tt); // ray is forward-only
|
||
const Vec3d pr = ro + tt * d1, ps = a + s * d2;
|
||
return (pr - ps).norm();
|
||
}
|
||
|
||
// Screen-plane distance from p to a sketch entity, for click picking in Constrain
|
||
// mode. Circles/arcs measure distance to the ring; points to their position.
|
||
static double entity_pick_dist(const Vec2d& p, const SketchEntity& e)
|
||
{
|
||
switch (e.type) {
|
||
case SketchEntity::Type::Line: return point_segment_dist(p, e.p0, e.p1);
|
||
case SketchEntity::Type::Point: return (p - e.p0).norm();
|
||
case SketchEntity::Type::Circle:
|
||
case SketchEntity::Type::Arc: return std::abs((p - e.center).norm() - e.radius);
|
||
case SketchEntity::Type::Ellipse:
|
||
case SketchEntity::Type::EllipseArc: {
|
||
// Accurate edge pick: sample the true ellipse outline as a polyline and take the
|
||
// min segment distance. The crude mean-radius circle mis-picks eccentric ellipses
|
||
// (the outline at the major/minor extremes is far from that circle), which made the
|
||
// face-fill swallow edge clicks. Full ellipse sweeps 0..2pi; an arc its param range.
|
||
const double cu = std::cos(e.rotation), su = std::sin(e.rotation);
|
||
const bool full = (e.type == SketchEntity::Type::Ellipse);
|
||
const double t0 = full ? 0.0 : e.start_angle;
|
||
const double t1 = full ? 2.0 * M_PI : e.end_angle;
|
||
const int n = 48;
|
||
double best = 1e30; Vec2d prev;
|
||
for (int i = 0; i <= n; ++i) {
|
||
const double t = t0 + (t1 - t0) * double(i) / n;
|
||
const double lx = e.radius * std::cos(t), ly = e.rminor * std::sin(t); // local
|
||
const Vec2d q(e.center.x() + lx * cu - ly * su,
|
||
e.center.y() + lx * su + ly * cu); // world
|
||
if (i > 0) best = std::min(best, point_segment_dist(p, prev, q));
|
||
prev = q;
|
||
}
|
||
return best;
|
||
}
|
||
case SketchEntity::Type::BSpline: {
|
||
const std::vector<Vec2d> poly = bspline_polyline(e.ctrl);
|
||
double best = 1e30;
|
||
for (size_t i = 1; i < poly.size(); ++i)
|
||
best = std::min(best, point_segment_dist(p, poly[i - 1], poly[i]));
|
||
return best;
|
||
}
|
||
}
|
||
return 1e30;
|
||
}
|
||
|
||
// Adjacency endpoints for loop walking (Circles/Points have none -> stand-alone).
|
||
static void entity_endpoints(const SketchEntity& e, std::vector<Vec2d>& out)
|
||
{
|
||
out.clear();
|
||
if (e.type == SketchEntity::Type::Line) {
|
||
out.push_back(e.p0);
|
||
out.push_back(e.p1);
|
||
} else if (e.type == SketchEntity::Type::Arc) {
|
||
out.push_back(e.center + e.radius * Vec2d(std::cos(e.start_angle), std::sin(e.start_angle)));
|
||
out.push_back(e.center + e.radius * Vec2d(std::cos(e.end_angle), std::sin(e.end_angle)));
|
||
}
|
||
}
|
||
|
||
// Reference point for positioning ops: a Point's position, or a Circle/Arc centre.
|
||
// Lines have no single reference point (return false).
|
||
static bool entity_ref_point(const SketchEntity& e, Vec2d& out)
|
||
{
|
||
switch (e.type) {
|
||
case SketchEntity::Type::Point: out = e.p0; return true;
|
||
case SketchEntity::Type::Circle:
|
||
case SketchEntity::Type::Arc:
|
||
case SketchEntity::Type::Ellipse:
|
||
case SketchEntity::Type::EllipseArc: out = e.center; return true;
|
||
default: return false; // Line
|
||
}
|
||
}
|
||
|
||
// Rigidly translate a whole entity (keeps size/shape).
|
||
static void translate_entity(SketchEntity& e, const Vec2d& d)
|
||
{
|
||
e.p0 += d;
|
||
e.p1 += d;
|
||
e.center += d;
|
||
for (auto& cp : e.ctrl) cp += d; // BSpline poles
|
||
}
|
||
|
||
int DesignSketchTool::hit_test(const Vec2d& p, double tol) const
|
||
{
|
||
double best = tol;
|
||
int bi = -1;
|
||
for (size_t i = 0; i < m_entities.size(); ++i) {
|
||
const double d = entity_pick_dist(p, m_entities[i]);
|
||
if (d < best) { best = d; bi = int(i); }
|
||
}
|
||
return bi;
|
||
}
|
||
|
||
std::vector<int> DesignSketchTool::connected_loop(int seed) const
|
||
{
|
||
std::vector<int> out;
|
||
if (seed < 0 || seed >= int(m_entities.size())) return out;
|
||
const double eps2 = 1e-6;
|
||
std::vector<bool> vis(m_entities.size(), false);
|
||
std::vector<int> stack = { seed };
|
||
vis[seed] = true;
|
||
while (!stack.empty()) {
|
||
const int cur = stack.back();
|
||
stack.pop_back();
|
||
out.push_back(cur);
|
||
std::vector<Vec2d> ce;
|
||
entity_endpoints(m_entities[cur], ce);
|
||
if (ce.empty()) continue; // circle/point: not part of a chain
|
||
for (size_t j = 0; j < m_entities.size(); ++j) {
|
||
if (vis[j]) continue;
|
||
std::vector<Vec2d> je;
|
||
entity_endpoints(m_entities[j], je);
|
||
if (je.empty()) continue;
|
||
bool adj = false;
|
||
for (const Vec2d& a : ce)
|
||
for (const Vec2d& b : je)
|
||
if ((a - b).squaredNorm() < eps2) { adj = true; break; }
|
||
if (adj) { vis[j] = true; stack.push_back(int(j)); }
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
// Right-click has two jobs in a sketch, and they were resolved by giving one of them everything:
|
||
// the offer was excluded in sketch mode wholesale so a right-click could end a polyline chain,
|
||
// abandon an anchor or exit a tool. That made every sketch row in the atlas unreachable.
|
||
// The honest test is not "which mode are we in" but "did the tool actually USE this right-click",
|
||
// and only the tool knows. Wrapping on_mouse records that once, for every terminator, instead of
|
||
// threading a flag through the twenty-odd sites that consume a RightDown.
|
||
bool DesignSketchTool::on_mouse(wxMouseEvent& evt, GLCanvas3D& canvas)
|
||
{
|
||
const bool consumed = on_mouse_impl(evt, canvas);
|
||
if (evt.RightDown())
|
||
m_right_consumed = consumed;
|
||
return consumed;
|
||
}
|
||
|
||
bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas)
|
||
{
|
||
// Track the cursor in canvas client px so the in-canvas value editor can open right
|
||
// where the user clicked (Onshape places the field at the click, not via a camera
|
||
// projection — the design canvas's viewport isn't valid outside its own paint).
|
||
m_last_mouse_x = evt.GetX();
|
||
m_last_mouse_y = evt.GetY();
|
||
|
||
// Line draw-then-edit: while the length editor is open right after the second click,
|
||
// freeze the canvas so a stray move/click can't push a third point or rubber-band a
|
||
// segment under the floating field. The editor's Enter/Esc resolves it
|
||
// (apply_segment_length / keep_segment_as_drawn, the latter clears this flag).
|
||
if (m_awaiting_length) {
|
||
// Polyline terminators must work even with a per-segment field open: right-click or
|
||
// double-click accepts the current segment as drawn (close the field) and falls through
|
||
// so the Polyline handler ends the chain. Without this the freeze ate every terminator.
|
||
if (m_mode == Mode::Polyline && (evt.RightDown() || evt.LeftDClick()) && on_inline_dismiss)
|
||
on_inline_dismiss(); // -> set_inline_busy(false), m_awaiting_length=false
|
||
// The freeze exists so a stray click can't draw under the floating field — it was
|
||
// never meant to trap the camera. Let drags and the wheel through, so a field that
|
||
// opens somewhere unexpected can't leave the viewport unusable.
|
||
else if (evt.Dragging() || evt.GetWheelRotation() != 0)
|
||
return false;
|
||
else
|
||
return true;
|
||
}
|
||
|
||
// No live session, but committed sketches are shown as overlays on the plate: a left
|
||
// click on a loop (its edge OR its closed interior) selects that Sketch feature. This
|
||
// is the ONLY interaction in display-only mode; everything else (drag/move/wheel/right)
|
||
// falls through (return false) so the camera can still orbit the plate.
|
||
if (!m_active) {
|
||
// Double-click on a committed sketch stroke OPENS IT FOR EDITING; on empty space it
|
||
// still fits the view. A sketch line has to be editable from the line, not from a tree
|
||
// row — selecting it already lit the feature, but "now go and press Edit in the panel"
|
||
// is the side-panel dependency this tab exists to remove. Fit keeps the rest of the
|
||
// plate, so nothing is taken away.
|
||
if (evt.LeftDClick()) {
|
||
int f = -1, r = -1, e = -1, ff = -1, fr = -1; double dbest = 1e30;
|
||
const Linef3 dray = canvas.mouse_ray(Point(evt.GetX(), evt.GetY()));
|
||
const Linef3 dray8 = canvas.mouse_ray(Point(evt.GetX() + 8, evt.GetY()));
|
||
for (const DisplaySketch& d : m_display_sketches) {
|
||
const Vec2d dp = d.plane.project(dray.a, dray.vector());
|
||
const Vec2d dp8 = d.plane.project(dray8.a, dray8.vector());
|
||
hit_display_sketch(d, dp, std::max(1e-3, (dp8 - dp).norm()), f, r, e, dbest, ff, fr);
|
||
}
|
||
const int target = (f >= 0) ? f : ff;
|
||
if (target >= 0 && on_display_sketch_activated) {
|
||
dp_pick_trace("double-click -> edit sketch feature %d (entity %d)", target, e);
|
||
on_display_sketch_activated(target);
|
||
return true;
|
||
}
|
||
canvas.zoom_to_volumes();
|
||
return true;
|
||
}
|
||
// Visual Extrude gizmo (C5b): while the Extrude card is open the depth arrow is
|
||
// grabbable — drag changes the depth live; a click (no drag) on the arrow opens the
|
||
// inline depth editor. Intercept before the early no-LeftDown bailout so Dragging/
|
||
// LeftUp reach us; a LeftDown that misses the arrow falls through to solid/loop pick.
|
||
// Move-body gizmo (M5): three world-axis arrows on the selected body. Drag an arrow to
|
||
// translate live; a stationary click on it opens the inline offset editor; a right click
|
||
// exits move mode. A LeftDown that misses the arrows falls through to solid re-pick.
|
||
if (m_mv_active) {
|
||
if (m_mv_drag >= 0 && evt.Dragging() && evt.LeftIsDown()) {
|
||
if (m_mv_drag < 3) drag_move_arrow(canvas, evt, m_mv_drag);
|
||
else drag_move_arc(canvas, evt, m_mv_drag - 3);
|
||
return true;
|
||
}
|
||
if (evt.LeftUp() && m_mv_drag >= 0) {
|
||
const int d = m_mv_drag; m_mv_drag = -1;
|
||
const bool moved = std::abs(evt.GetX() - m_mv_press_x) +
|
||
std::abs(evt.GetY() - m_mv_press_y) > 3;
|
||
if (!moved && d < 3) open_move_editor(d); // stationary click on an arrow = edit offset
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) { clear_move_gizmo(); canvas.set_as_dirty(); if (on_move_exit) on_move_exit(); return true; }
|
||
if (evt.LeftDown()) {
|
||
int axis = -1;
|
||
if (hit_test_move_arrow(canvas, evt, axis)) { // translate arrows win over rings
|
||
m_mv_drag = axis; m_mv_press_x = evt.GetX(); m_mv_press_y = evt.GetY();
|
||
return true;
|
||
}
|
||
if (hit_test_move_arc(canvas, evt, axis)) {
|
||
m_mv_drag = 3 + axis; m_mv_press_x = evt.GetX(); m_mv_press_y = evt.GetY();
|
||
m_mv_rot_start = m_mv_rot;
|
||
double a0; if (arc_mouse_angle(canvas, evt, axis, a0)) m_mv_arc_a0 = a0;
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
// Datum-plane resize gizmo (C3): while the Plane card is open the 4 edge handles are
|
||
// grabbable — drag changes the u/v extent live. A LeftDown that misses falls through.
|
||
if (m_dz_active) {
|
||
if (m_dz_drag >= 0 && evt.Dragging() && evt.LeftIsDown()) {
|
||
drag_datum_handle(canvas, evt, m_dz_drag);
|
||
return true;
|
||
}
|
||
if (evt.LeftUp() && m_dz_drag >= 0) { m_dz_drag = -1; return true; }
|
||
if (evt.LeftDown()) {
|
||
int which = -1;
|
||
if (hit_test_datum_handle(canvas, evt, which)) {
|
||
m_dz_drag = which; m_dz_press_x = evt.GetX(); m_dz_press_y = evt.GetY();
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
// Helix gizmo: radius/height/pitch handles on the live curve, while the Helix card is open.
|
||
if (m_hx_active) {
|
||
if (m_hx_drag >= 0 && evt.Dragging() && evt.LeftIsDown()) {
|
||
drag_helix_handle(canvas, evt, m_hx_drag);
|
||
return true;
|
||
}
|
||
if (evt.LeftUp() && m_hx_drag >= 0) { m_hx_drag = -1; return true; }
|
||
if (evt.LeftDown()) {
|
||
int which = -1;
|
||
if (hit_test_helix_handle(canvas, evt, which)) {
|
||
m_hx_drag = which; m_hx_press_x = evt.GetX(); m_hx_press_y = evt.GetY();
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
// Rib thickness gizmo: two in-plane handles on the slab footprint, while the Rib card is open.
|
||
if (m_rb_active) {
|
||
if (m_rb_drag >= 0 && evt.Dragging() && evt.LeftIsDown()) {
|
||
drag_rib_handle(canvas, evt, m_rb_drag);
|
||
return true;
|
||
}
|
||
if (evt.LeftUp() && m_rb_drag >= 0) { m_rb_drag = -1; return true; }
|
||
if (evt.LeftDown()) {
|
||
int which = -1;
|
||
if (hit_test_rib_handle(canvas, evt, which)) {
|
||
m_rb_drag = which;
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
// Datum base picker: HOVER highlight only here. The CLICK is handled at the very end of the
|
||
// selection fall-through (below), so picking existing geometry (committed sketch loops,
|
||
// solid faces/edges) always wins over a base-plane click — the planes never block selection.
|
||
if (m_dbp_active && evt.Moving() && !evt.LeftIsDown()) {
|
||
const int h = hit_test_base_pick(canvas, evt);
|
||
if (h != m_dbp_hover) {
|
||
m_dbp_hover = h;
|
||
canvas.set_as_dirty();
|
||
if (h >= 0) return true; // caller render()s on true -> hover repaints on software GL
|
||
}
|
||
}
|
||
if (m_ex_active) {
|
||
if (m_ex_drag >= 0 && evt.Dragging() && evt.LeftIsDown()) {
|
||
drag_extrude_arrow(canvas, evt, m_ex_drag);
|
||
return true;
|
||
}
|
||
if (evt.LeftUp() && m_ex_drag >= 0) {
|
||
const int which = m_ex_drag; m_ex_drag = -1;
|
||
const bool moved = std::abs(evt.GetX() - m_ex_press_x) +
|
||
std::abs(evt.GetY() - m_ex_press_y) > 3;
|
||
if (!moved) open_extrude_editor(which); // treat a stationary click as edit
|
||
return true;
|
||
}
|
||
if (evt.LeftDown()) {
|
||
int which = -1;
|
||
if (hit_test_extrude_arrow(canvas, evt, which)) {
|
||
m_ex_drag = which; m_ex_press_x = evt.GetX(); m_ex_press_y = evt.GetY();
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
// Revolve angle-arc gizmo: drag the arc tip to sweep the angle; a stationary click on the
|
||
// handle opens the inline editor. A LeftDown that misses falls through to normal picking.
|
||
if (m_rv_active) {
|
||
if (m_rv_drag && evt.Dragging() && evt.LeftIsDown()) {
|
||
drag_revolve_arc(canvas, evt);
|
||
return true;
|
||
}
|
||
if (evt.LeftUp() && m_rv_drag) {
|
||
m_rv_drag = false;
|
||
const bool moved = std::abs(evt.GetX() - m_rv_press_x) +
|
||
std::abs(evt.GetY() - m_rv_press_y) > 3;
|
||
if (!moved) open_revolve_editor();
|
||
return true;
|
||
}
|
||
if (evt.LeftDown() && hit_test_revolve_handle(canvas, evt)) {
|
||
m_rv_drag = true; m_rv_press_x = evt.GetX(); m_rv_press_y = evt.GetY();
|
||
return true;
|
||
}
|
||
}
|
||
// Draft angle-arc gizmo: drag the arc tip to sweep the draft angle; a stationary click
|
||
// edits it. A LeftDown that misses falls through to normal picking.
|
||
if (m_dr_active) {
|
||
if (m_dr_drag && evt.Dragging() && evt.LeftIsDown()) {
|
||
drag_draft_arc(canvas, evt);
|
||
return true;
|
||
}
|
||
if (evt.LeftUp() && m_dr_drag) {
|
||
m_dr_drag = false;
|
||
return true;
|
||
}
|
||
if (evt.LeftDown() && hit_test_draft_handle(canvas, evt)) {
|
||
m_dr_drag = true; m_dr_press_x = evt.GetX(); m_dr_press_y = evt.GetY();
|
||
return true;
|
||
}
|
||
}
|
||
// Cut gizmo: drag the offset arrow along the cut-plane normal; no positive clamp (offset is signed).
|
||
if (m_ct_active) {
|
||
if (m_ct_drag && evt.Dragging() && evt.LeftIsDown()) {
|
||
drag_cut_arrow(canvas, evt);
|
||
return true;
|
||
}
|
||
if (evt.LeftUp() && m_ct_drag) {
|
||
m_ct_drag = false;
|
||
return true;
|
||
}
|
||
if (evt.LeftDown() && hit_test_cut_arrow(canvas, evt)) {
|
||
start_cut_drag(canvas, evt);
|
||
return true;
|
||
}
|
||
}
|
||
// Pattern gizmo: drag the diamond/arc handle to set the spacing (linear) or angle (circular);
|
||
// a stationary click opens the inline editor. A LeftDown that misses falls through to picking.
|
||
if (m_pt_active) {
|
||
if (m_pt_drag && evt.Dragging() && evt.LeftIsDown()) {
|
||
drag_pattern_handle(canvas, evt);
|
||
return true;
|
||
}
|
||
if (evt.LeftUp() && m_pt_drag) {
|
||
m_pt_drag = false;
|
||
const bool moved = std::abs(evt.GetX() - m_pt_press_x) +
|
||
std::abs(evt.GetY() - m_pt_press_y) > 3;
|
||
if (!moved) open_pattern_editor();
|
||
return true;
|
||
}
|
||
if (evt.LeftDown() && hit_test_pattern_handle(canvas, evt)) {
|
||
m_pt_drag = true; m_pt_press_x = evt.GetX(); m_pt_press_y = evt.GetY();
|
||
return true;
|
||
}
|
||
}
|
||
// Fillet/Chamfer radius gizmo: drag the edge-anchored arrow to set the radius live; a
|
||
// stationary click opens the inline editor. A LeftDown that misses falls through to pick.
|
||
if (m_fl_active) {
|
||
if (m_fl_drag && evt.Dragging() && evt.LeftIsDown()) {
|
||
drag_fillet_arrow(canvas, evt);
|
||
return true;
|
||
}
|
||
if (evt.LeftUp() && m_fl_drag) {
|
||
m_fl_drag = false;
|
||
const bool moved = std::abs(evt.GetX() - m_fl_press_x) +
|
||
std::abs(evt.GetY() - m_fl_press_y) > 3;
|
||
if (!moved) open_fillet_editor(); // stationary click = edit
|
||
return true;
|
||
}
|
||
if (evt.LeftDown() && hit_test_fillet_arrow(canvas, evt)) {
|
||
start_fillet_drag(canvas, evt);
|
||
return true;
|
||
}
|
||
}
|
||
// Hole gizmo: drag the centre to reposition / the diameter or depth arrow to resize; a
|
||
// stationary click on an arrow opens its inline editor. A LeftDown that misses any handle
|
||
// falls through to solid/loop picking.
|
||
if (m_hl_active) {
|
||
if (m_hl_drag >= 0 && evt.Dragging() && evt.LeftIsDown()) {
|
||
drag_hole_handle(canvas, evt);
|
||
return true;
|
||
}
|
||
if (evt.LeftUp() && m_hl_drag >= 0) {
|
||
const int which = m_hl_drag; m_hl_drag = -1;
|
||
const bool moved = std::abs(evt.GetX() - m_hl_press_x) +
|
||
std::abs(evt.GetY() - m_hl_press_y) > 3;
|
||
if (!moved) open_hole_editor(which); // stationary click = edit
|
||
return true;
|
||
}
|
||
if (evt.LeftDown()) {
|
||
const int which = hit_test_hole_handle(canvas, evt);
|
||
if (which >= 0) { start_hole_drag(canvas, evt, which); return true; }
|
||
}
|
||
}
|
||
// Thread gizmo: same interaction as the hole gizmo (centre / radius / length handles).
|
||
if (m_th_active) {
|
||
if (m_th_drag >= 0 && evt.Dragging() && evt.LeftIsDown()) {
|
||
drag_thread_handle(canvas, evt);
|
||
return true;
|
||
}
|
||
if (evt.LeftUp() && m_th_drag >= 0) {
|
||
const int which = m_th_drag; m_th_drag = -1;
|
||
const bool moved = std::abs(evt.GetX() - m_th_press_x) +
|
||
std::abs(evt.GetY() - m_th_press_y) > 3;
|
||
if (!moved) open_thread_editor(which);
|
||
return true;
|
||
}
|
||
if (evt.LeftDown()) {
|
||
const int which = hit_test_thread_handle(canvas, evt);
|
||
if (which >= 0) { start_thread_drag(canvas, evt, which); return true; }
|
||
}
|
||
}
|
||
// Shell gizmo: drag the inward thickness arrow; stationary click opens the inline editor.
|
||
if (m_sh_active) {
|
||
if (m_sh_drag && evt.Dragging() && evt.LeftIsDown()) {
|
||
drag_shell_arrow(canvas, evt);
|
||
return true;
|
||
}
|
||
if (evt.LeftUp() && m_sh_drag) {
|
||
m_sh_drag = false;
|
||
const bool moved = std::abs(evt.GetX() - m_sh_press_x) +
|
||
std::abs(evt.GetY() - m_sh_press_y) > 3;
|
||
if (!moved) open_shell_editor();
|
||
return true;
|
||
}
|
||
if (evt.LeftDown() && hit_test_shell_arrow(canvas, evt)) {
|
||
start_shell_drag(canvas, evt);
|
||
return true;
|
||
}
|
||
}
|
||
// Left-drag rubber band -> whole body. Past the click budget the press becomes a sweep:
|
||
// the rectangle is anchored at the ORIGINAL press point (not at the frame where the
|
||
// threshold was crossed, which would lose the first few pixels) and the events are
|
||
// consumed from here on. Left-drag no longer orbits in this canvas — DesignCanvas puts
|
||
// orbit on middle-drag and pan on right-drag, the CAD convention — so nothing downstream
|
||
// is being starved of a gesture it used to own.
|
||
// HOVER PRE-HIGHLIGHT (snaporca-9xw part 3): say what a click would take, before it is
|
||
// taken. Plain motion only — no button down, no band running — because during a drag the
|
||
// pointer is doing something else and a promise about clicking would be a lie. Returns
|
||
// false so the event still reaches the camera; this only asks for a repaint, it does not
|
||
// consume the gesture, which is the difference between a hint and a handler.
|
||
if (evt.Moving() && !evt.LeftIsDown() && !m_rubber.is_dragging()) {
|
||
if (update_solid_hover(canvas, evt)) canvas.set_as_dirty();
|
||
return false;
|
||
}
|
||
if (evt.Dragging() && evt.LeftIsDown() && m_pick_pending) {
|
||
if (!m_rubber.is_dragging()) {
|
||
if (std::max(std::abs(evt.GetX() - m_pick_press_x),
|
||
std::abs(evt.GetY() - m_pick_press_y)) <= 8)
|
||
return false; // still inside the click budget
|
||
m_rubber.start_dragging(Vec2d(m_pick_press_x, m_pick_press_y),
|
||
GLSelectionRectangle::Select);
|
||
}
|
||
m_rubber.dragging(Vec2d(evt.GetX(), evt.GetY()));
|
||
return true;
|
||
}
|
||
// Any event with the left button up ends a band — not just LeftUp. A release that lands
|
||
// outside the canvas never sends us one, and a band left running would then paint a
|
||
// rectangle that follows the cursor with no button held.
|
||
if (m_rubber.is_dragging() && !evt.LeftIsDown()) {
|
||
m_pick_pending = false;
|
||
if (evt.LeftUp()) pick_bodies_in_rectangle(); // a release elsewhere selects nothing
|
||
m_rubber.stop_dragging();
|
||
return evt.LeftUp();
|
||
}
|
||
// Click vs drag. Consuming the LeftDown here killed camera orbit/pan the moment a
|
||
// solid was on screen: Orca starts a rotate drag on the press, so swallowing it meant
|
||
// the canvas never began one. (A SpaceMouse kept working — it never goes through
|
||
// wxMouseEvent.) Remember the press, let it fall through so the canvas can orbit, and
|
||
// resolve the pick on release only if the pointer stayed put.
|
||
if (evt.LeftDown()) {
|
||
m_pick_press_x = evt.GetX();
|
||
m_pick_press_y = evt.GetY();
|
||
m_pick_pending = true;
|
||
dp_pick_trace("down x=%d y=%d", evt.GetX(), evt.GetY());
|
||
return false;
|
||
}
|
||
if (!(evt.LeftUp() && m_pick_pending)) {
|
||
if (evt.LeftUp()) dp_pick_trace("up with no pending press (press was eaten upstream)");
|
||
return false;
|
||
}
|
||
m_pick_pending = false;
|
||
dp_pick_trace("up x=%d y=%d drift=%d", evt.GetX(), evt.GetY(),
|
||
std::max(std::abs(evt.GetX() - m_pick_press_x),
|
||
std::abs(evt.GetY() - m_pick_press_y)));
|
||
// Threshold per axis, at GTK's own drag threshold. A hand-held mouse drifts several
|
||
// pixels during an ordinary click — a tight budget silently swallowed real clicks and
|
||
// looked exactly like "selection does not work". Synthetic clicks never drift, which
|
||
// is why the headless rig could not show this.
|
||
if (std::max(std::abs(evt.GetX() - m_pick_press_x),
|
||
std::abs(evt.GetY() - m_pick_press_y)) > 8) {
|
||
dp_pick_trace("rejected as drag");
|
||
return false; // it was a drag: the canvas already orbited, don't also select
|
||
}
|
||
// Committed-sketch loop pick is computed FIRST. A click that lands on a loop's
|
||
// STROKE (edge) selects that loop even when it lies on a solid face — so a sketch
|
||
// drawn ON a face can be selected and extruded/cut (Onshape engraving workflow).
|
||
// Open-face area (no loop stroke under the cursor) falls through to the solid
|
||
// whole/face/edge cycle; an interior hit with no solid behind it is the last resort.
|
||
Point pos(evt.GetX(), evt.GetY());
|
||
const Linef3 ray = canvas.mouse_ray(pos);
|
||
const Linef3 ray8 = canvas.mouse_ray(Point(evt.GetX() + 8, evt.GetY()));
|
||
int edge_feat = -1, edge_reg = -1, edge_ent = -1; double edge_d = 1e30; // nearest stroke
|
||
int face_feat = -1, face_reg = -1; // interior (fallback)
|
||
dp_pick_trace("display sketches available: %zu", m_display_sketches.size());
|
||
for (const DisplaySketch& d : m_display_sketches) {
|
||
const Vec2d p = d.plane.project(ray.a, ray.vector());
|
||
const Vec2d p8 = d.plane.project(ray8.a, ray8.vector());
|
||
const double tol = std::max(1e-3, (p8 - p).norm());
|
||
hit_display_sketch(d, p, tol, edge_feat, edge_reg, edge_ent, edge_d, face_feat, face_reg);
|
||
}
|
||
// A precise hit on a loop outline wins over the solid face beneath it.
|
||
if (edge_feat >= 0) {
|
||
m_display_pick = edge_feat; m_display_pick_region = edge_reg;
|
||
if (on_display_sketch_selected) on_display_sketch_selected(edge_feat, edge_reg, edge_ent);
|
||
return true;
|
||
}
|
||
// No loop stroke under the cursor: the solid is the foreground (whole/face/edge cycle).
|
||
if (handle_solid_click(canvas, evt)) return true;
|
||
// Interior of a committed loop with no solid behind it.
|
||
if (face_feat >= 0) {
|
||
m_display_pick = face_feat; m_display_pick_region = face_reg;
|
||
if (on_display_sketch_selected) on_display_sketch_selected(face_feat, face_reg, -1);
|
||
return true;
|
||
}
|
||
m_display_pick = -1; m_display_pick_region = -1; // clicked bare plate -> drop highlight
|
||
// ...and the SOLID selection goes with it (snaporca-od0). A click that hits nothing has to
|
||
// mean what a rubber band that sweeps nothing already means — pick_bodies_in_rectangle
|
||
// clears on an empty sweep, and the two gestures cannot disagree about the same outcome.
|
||
// Until now the face survived a click on bare plate, so "click away, then click the face
|
||
// again" arrived here as the SECOND click on the same face and escalated to the whole
|
||
// body, when the click away was the user letting go of it.
|
||
//
|
||
// The cost is real and is the intended trade: Thicken / Shell / Draft hold their input
|
||
// face in the panel's m_sel_solid_face, so a stray click on empty canvas with one of
|
||
// those cards open gives that face back. Their handlers already write the "(pick a solid
|
||
// face)" placeholder and rebuild the ghost at level 0, so the card SAYS it lost the pick
|
||
// rather than confirming against a face the viewport has stopped highlighting.
|
||
//
|
||
// Guarded on there being something to clear: this runs on every click that misses, and
|
||
// the callback re-renders the open card's preview.
|
||
if (m_solid_sel != SolidSel::None) {
|
||
clear_solid_selection();
|
||
dp_pick_trace("clicked empty space -> selection cleared");
|
||
if (on_solid_selection_changed)
|
||
on_solid_selection_changed(int(m_solid_sel), m_sel_body, m_sel_face, m_sel_edge);
|
||
}
|
||
// Last resort: a click that hit no geometry but landed on a reference/base plane picks it.
|
||
if (m_dbp_active) {
|
||
const int h = hit_test_base_pick(canvas, evt);
|
||
if (h >= 0 && h < int(m_dbp_base.size())) {
|
||
if (on_datum_base_picked) on_datum_base_picked(m_dbp_base[h]);
|
||
return true;
|
||
}
|
||
}
|
||
return false; // let the stock canvas orbit / deselect
|
||
}
|
||
|
||
// In-canvas edit-op tools (Fillet/Chamfer/Offset/Mirror): pick entities, then a
|
||
// draggable arrow + editable value label (Mirror: a two-phase pick) drives a live
|
||
// ghost. A click on empty space confirms; right-click/Esc cancels the gesture.
|
||
// Standalone Trim / Extend scissors: click a segment to cut it back to (Trim) or out to
|
||
// (Extend) its nearest intersection with the other live entities. One cut per click; the
|
||
// tool stays active for more cuts; right-click exits. Drag falls through so the camera can
|
||
// still orbit. Operates directly on the live sketch — no Constrain mode.
|
||
if (m_mode == Mode::Trim || m_mode == Mode::Extend) {
|
||
if (evt.Moving()) { screen_to_plane(canvas, evt, m_cursor); m_has_cursor = true; return true; }
|
||
if (evt.LeftDown()) {
|
||
Vec2d p; screen_to_plane(canvas, evt, p);
|
||
const Linef3 r2 = canvas.mouse_ray(Point(evt.GetX() + 8, evt.GetY()));
|
||
const double tol = std::max(1e-3, (m_plane.project(r2.a, r2.vector()) - p).norm());
|
||
if (apply_live_trim(p, tol * 3.0, m_mode == Mode::Extend)) {
|
||
resolve_live();
|
||
} else if (on_readout) {
|
||
// nde #15: don't fail silently. The pick found nothing to cut/extend — either
|
||
// the click missed every live segment, or the picked segment has no crossing /
|
||
// target among the OTHER live entities (committed sketches aren't trimmed).
|
||
on_readout(m_mode == Mode::Extend
|
||
? std::string("Extend: click a line/arc that can reach another live entity")
|
||
: std::string("Trim: click a segment where it crosses another live entity"));
|
||
}
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) { request_exit(); return true; }
|
||
return false; // let move/drag orbit the camera
|
||
}
|
||
|
||
if (is_edit_op_mode()) {
|
||
if (evt.Moving()) {
|
||
screen_to_plane(canvas, evt, m_cursor);
|
||
m_has_cursor = true;
|
||
return true;
|
||
}
|
||
if (m_op_dragging_arrow && evt.Dragging() && evt.LeftIsDown()) {
|
||
Vec2d p; screen_to_plane(canvas, evt, p);
|
||
drag_op_arrow(p);
|
||
return true;
|
||
}
|
||
if (evt.LeftUp()) {
|
||
if (m_op_dragging_arrow) { m_op_dragging_arrow = false; return true; }
|
||
return false;
|
||
}
|
||
if (evt.LeftDown()) {
|
||
Vec2d p; screen_to_plane(canvas, evt, p);
|
||
const Linef3 r2 = canvas.mouse_ray(Point(evt.GetX() + 8, evt.GetY()));
|
||
const double tol = std::max(1e-3, (m_plane.project(r2.a, r2.vector()) - p).norm());
|
||
// 1) live gizmo: click the value label to type, or grab the arrow to drag.
|
||
if (op_ready() && m_mode != Mode::Mirror) {
|
||
const Linef3 rl = canvas.mouse_ray(Point(evt.GetX() + 24, evt.GetY()));
|
||
const double ltol = std::max(tol, (m_plane.project(rl.a, rl.vector()) - p).norm());
|
||
if ((m_op_label - p).norm() <= ltol) { open_op_editor(); return true; }
|
||
if (hit_test_op_arrow(p, tol)) { m_op_dragging_arrow = true; return true; }
|
||
}
|
||
// 2) entity pick (close enough to an entity edge).
|
||
double best = 1e30; int bi = -1;
|
||
for (size_t i = 0; i < m_entities.size(); ++i) {
|
||
const double d = entity_pick_dist(p, m_entities[i]);
|
||
if (d < best) { best = d; bi = int(i); }
|
||
}
|
||
if (bi >= 0 && best <= tol * 3.0) { op_pick(bi); return true; }
|
||
// 3) empty click confirms a ready gesture.
|
||
if (op_ready()) confirm_op();
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) {
|
||
if (m_op_a >= 0 || !m_mirror_targets.empty()) {
|
||
reset_op();
|
||
m_selection.clear();
|
||
if (on_selection_changed) on_selection_changed(0);
|
||
} else {
|
||
request_exit();
|
||
}
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// In-canvas transform tools (Move/Rotate/Scale/Array/PolarArray): pick subject
|
||
// entities, then a single draggable handle + editable value label(s) drive a live
|
||
// ghost. A click on empty space confirms; right-click drops the gesture / exits.
|
||
if (is_transform_mode()) {
|
||
if (evt.Moving()) {
|
||
screen_to_plane(canvas, evt, m_cursor);
|
||
m_has_cursor = true;
|
||
return true;
|
||
}
|
||
if (m_tf_dragging && evt.Dragging() && evt.LeftIsDown()) {
|
||
Vec2d p; screen_to_plane(canvas, evt, p);
|
||
drag_tf_handle(p);
|
||
return true;
|
||
}
|
||
if (evt.LeftUp()) {
|
||
if (m_tf_dragging) { m_tf_dragging = false; return true; }
|
||
return false;
|
||
}
|
||
if (evt.LeftDown()) {
|
||
Vec2d p; screen_to_plane(canvas, evt, p);
|
||
const Linef3 r2 = canvas.mouse_ray(Point(evt.GetX() + 8, evt.GetY()));
|
||
const double tol = std::max(1e-3, (m_plane.project(r2.a, r2.vector()) - p).norm());
|
||
// 1) live gizmo: click a value label to type, or grab the handle to drag.
|
||
if (tf_ready()) {
|
||
const Linef3 rl = canvas.mouse_ray(Point(evt.GetX() + 24, evt.GetY()));
|
||
const double ltol = std::max(tol, (m_plane.project(rl.a, rl.vector()) - p).norm());
|
||
if ((m_tf_label_a - p).norm() <= ltol) { open_tf_editor_a(); return true; }
|
||
if ((m_tf_label_b - p).norm() <= ltol) { open_tf_editor_count(); return true; }
|
||
if (hit_test_tf_handle(p, tol)) { m_tf_dragging = true; return true; }
|
||
}
|
||
// 2) entity pick (close enough to an entity edge).
|
||
double best = 1e30; int bi = -1;
|
||
for (size_t i = 0; i < m_entities.size(); ++i) {
|
||
const double d = entity_pick_dist(p, m_entities[i]);
|
||
if (d < best) { best = d; bi = int(i); }
|
||
}
|
||
if (bi >= 0 && best <= tol * 3.0) { tf_pick(bi); return true; }
|
||
// 3) empty click confirms a ready gesture.
|
||
if (tf_ready()) confirm_transform();
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) {
|
||
if (!m_tf_targets.empty()) {
|
||
reset_tf();
|
||
m_selection.clear();
|
||
if (on_selection_changed) on_selection_changed(0);
|
||
} else {
|
||
request_exit();
|
||
}
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Imported-art bbox transform: drag a corner to scale, the centre to move. Right-click
|
||
// ends the session. Values stream live to the host via on_imported_transform.
|
||
if (m_mode == Mode::TransformArt) {
|
||
if (evt.Moving()) {
|
||
screen_to_plane(canvas, evt, m_cursor);
|
||
m_has_cursor = true;
|
||
return true;
|
||
}
|
||
if (evt.LeftDown()) {
|
||
Vec2d p; screen_to_plane(canvas, evt, p);
|
||
const Linef3 r2 = canvas.mouse_ray(Point(evt.GetX() + 10, evt.GetY()));
|
||
const double tol = std::max(1e-3, (m_plane.project(r2.a, r2.vector()) - p).norm());
|
||
const int h = hit_test_xform_handle(p, tol);
|
||
if (h >= 0) {
|
||
m_xform_handle = h;
|
||
if (h == 4) m_xform_anchor = p; // centre move: track the delta
|
||
else { Vec2d c[4]; xform_world_corners(c); m_xform_anchor = c[(h + 2) % 4]; }
|
||
}
|
||
return true; // swallow (no camera orbit while placing)
|
||
}
|
||
if (m_xform_handle >= 0 && evt.Dragging() && evt.LeftIsDown()) {
|
||
Vec2d p; screen_to_plane(canvas, evt, p);
|
||
drag_xform_handle(p);
|
||
return true;
|
||
}
|
||
if (evt.LeftUp()) { m_xform_handle = -1; return true; }
|
||
if (evt.RightDown()) { if (on_exit) on_exit(); else cancel(); return true; }
|
||
return false;
|
||
}
|
||
|
||
// Constrain mode: pick a segment on click; let move/drag fall through so the
|
||
// camera can still orbit while inspecting the sketch.
|
||
if (m_mode == Mode::Constrain) {
|
||
if (m_constrain_entities) {
|
||
// Pick any entity (line/circle/arc/point): rolling two-slot selection.
|
||
if (evt.LeftDown()) {
|
||
Vec2d p;
|
||
screen_to_plane(canvas, evt, p);
|
||
double best = 1e30;
|
||
int bi = -1;
|
||
for (size_t i = 0; i < m_entities.size(); ++i) {
|
||
const double d = entity_pick_dist(p, m_entities[i]);
|
||
if (d < best) { best = d; bi = int(i); }
|
||
}
|
||
if (bi >= 0) {
|
||
// Rolling three-slot selection: slots 0/1 feed all 2-entity
|
||
// constraints; slot 2 is the Symmetric axis (only filled once
|
||
// 0 and 1 are set). A click past slot 2 restarts the cycle.
|
||
if (m_pick0 < 0) { m_pick0 = bi; m_pick0_pt = p; }
|
||
else if (m_pick1 < 0 && bi != m_pick0) m_pick1 = bi;
|
||
else if (m_pick1 >= 0 && m_pick2 < 0 && bi != m_pick0 && bi != m_pick1) m_pick2 = bi;
|
||
else { m_pick0 = bi; m_pick1 = m_pick2 = -1; m_pick0_pt = p; }
|
||
}
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) { cancel(); return true; }
|
||
return false;
|
||
}
|
||
if (evt.LeftDown()) {
|
||
if (m_points.size() < 2)
|
||
return false;
|
||
Vec2d p;
|
||
screen_to_plane(canvas, evt, p);
|
||
const size_t n = m_points.size();
|
||
double best = 1e30;
|
||
int bi = -1;
|
||
for (size_t i = 0; i < n; ++i) {
|
||
const double d = point_segment_dist(p, m_points[i], m_points[(i + 1) % n]);
|
||
if (d < best) { best = d; bi = int(i); }
|
||
}
|
||
if (bi >= 0) {
|
||
m_sel_a = bi;
|
||
m_sel_b = int((bi + 1) % n);
|
||
}
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) {
|
||
cancel();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Selection mode: click to pick an entity, Shift/Ctrl to extend, double-click
|
||
// to grab the whole connected loop. Drag falls through so the camera can orbit.
|
||
if (m_mode == Mode::Select) {
|
||
if (update_hover(canvas, evt)) return true; // repaint when the hovered handle changes
|
||
const bool extend = evt.ShiftDown() || evt.ControlDown();
|
||
|
||
// Live point drag: once an endpoint/centre was grabbed on LeftDown, dragging
|
||
// moves it (re-solving constraints live) until the button is released. When
|
||
// nothing is grabbed, drag falls through so the camera can still orbit.
|
||
if (m_dragging_point && evt.Dragging() && evt.LeftIsDown()) {
|
||
Vec2d p;
|
||
screen_to_plane(canvas, evt, p);
|
||
if (m_drag_poly_fi >= 0)
|
||
drag_polygon_vertex(m_drag_poly_fi, m_drag_ei, m_drag_role, p); // keep regular
|
||
else if (m_drag_rect_fi >= 0)
|
||
drag_rect_corner(m_drag_rect_fi, p); // resize axis-aligned box
|
||
else if (m_drag_slot_fi >= 0)
|
||
drag_slot_handle(m_drag_slot_fi, p); // move a slot end
|
||
else if (m_drag_ei >= 0 && m_drag_ei < int(m_entities.size()) &&
|
||
m_entities[m_drag_ei].type == SketchEntity::Type::Arc)
|
||
drag_arc_handle(m_drag_ei, m_drag_role, p); // center/radius/angle grips
|
||
else if (m_drag_ei >= 0 && m_drag_ei < int(m_entities.size()) &&
|
||
m_entities[m_drag_ei].type == SketchEntity::Type::EllipseArc)
|
||
drag_ellipsearc_handle(m_drag_ei, m_drag_role, p); // center/sweep endpoints
|
||
else {
|
||
set_point(m_drag_ei, m_drag_role, p);
|
||
resolve_live_drag(m_drag_ei, m_drag_role);
|
||
}
|
||
return true;
|
||
}
|
||
// Derived-handle drag (A4): the circle RadiusHandle (and later slot/rect/etc.)
|
||
// isn't an entity point, so it rides set_handle, which applies the role-specific
|
||
// geometry edit + re-solve.
|
||
if (m_dragging_handle && evt.Dragging() && evt.LeftIsDown()) {
|
||
Vec2d p;
|
||
screen_to_plane(canvas, evt, p);
|
||
set_handle(m_drag_handle, p);
|
||
return true;
|
||
}
|
||
if (evt.LeftUp()) {
|
||
if (m_dragging_point) {
|
||
Vec2d p;
|
||
screen_to_plane(canvas, evt, p);
|
||
if (m_drag_poly_fi >= 0)
|
||
drag_polygon_vertex(m_drag_poly_fi, m_drag_ei, m_drag_role, p);
|
||
else if (m_drag_rect_fi >= 0)
|
||
drag_rect_corner(m_drag_rect_fi, p);
|
||
else if (m_drag_slot_fi >= 0)
|
||
drag_slot_handle(m_drag_slot_fi, p);
|
||
else if (m_drag_ei >= 0 && m_drag_ei < int(m_entities.size()) &&
|
||
m_entities[m_drag_ei].type == SketchEntity::Type::Arc)
|
||
drag_arc_handle(m_drag_ei, m_drag_role, p);
|
||
else if (m_drag_ei >= 0 && m_drag_ei < int(m_entities.size()) &&
|
||
m_entities[m_drag_ei].type == SketchEntity::Type::EllipseArc)
|
||
drag_ellipsearc_handle(m_drag_ei, m_drag_role, p);
|
||
else {
|
||
set_point(m_drag_ei, m_drag_role, p);
|
||
resolve_live_drag(m_drag_ei, m_drag_role);
|
||
}
|
||
m_dragging_point = false;
|
||
m_drag_ei = -1;
|
||
m_drag_poly_fi = -1;
|
||
m_drag_rect_fi = -1;
|
||
m_drag_slot_fi = -1;
|
||
return true;
|
||
}
|
||
if (m_dragging_handle) {
|
||
Vec2d p;
|
||
screen_to_plane(canvas, evt, p);
|
||
set_handle(m_drag_handle, p);
|
||
m_dragging_handle = false;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
if (evt.LeftDown() || evt.LeftDClick()) {
|
||
m_dragging_point = false; // a fresh press disarms any stale grab
|
||
m_dragging_handle = false;
|
||
m_drag_poly_fi = -1;
|
||
m_drag_rect_fi = -1;
|
||
m_drag_slot_fi = -1;
|
||
Vec2d p;
|
||
screen_to_plane(canvas, evt, p);
|
||
if (evt.LeftDClick()) { // double-click a quote label -> edit it
|
||
const Linef3 rd = canvas.mouse_ray(Point(evt.GetX() + 28, evt.GetY()));
|
||
const double dtol = std::max(2.0, (m_plane.project(rd.a, rd.vector()) - p).norm());
|
||
const int di = hit_test_dimension(p, dtol);
|
||
if (di >= 0) { edit_dimension(di); return true; }
|
||
}
|
||
// Zoom-aware tolerance: project a point 8 px away and measure in plane units.
|
||
const Linef3 r2 = canvas.mouse_ray(Point(evt.GetX() + 8, evt.GetY()));
|
||
const Vec2d p2 = m_plane.project(r2.a, r2.vector());
|
||
const double tol = std::max(1e-3, (p2 - p).norm());
|
||
|
||
// Single-click a dimension quote label -> edit its value in place. A placed
|
||
// driving quote reopens its editor; a live (non-driving) characteristic quote
|
||
// is first promoted to a driving dimension (place_dimension), then its editor
|
||
// opens — so typing a value sets the precise dimension. Generous label
|
||
// tolerance (~24 px) since text labels are wider than a point grip.
|
||
if (evt.LeftDown()) {
|
||
const Linef3 rdl = canvas.mouse_ray(Point(evt.GetX() + 24, evt.GetY()));
|
||
const double ltol = std::max(tol, (m_plane.project(rdl.a, rdl.vector()) - p).norm());
|
||
const int di = hit_test_dimension(p, ltol);
|
||
if (di >= 0) { open_value_editor(di); return true; }
|
||
for (const DimAnnot& q : m_live_quotes) {
|
||
if ((q.label_pos - p).norm() <= ltol) {
|
||
if (q.kind == DimType::Angle)
|
||
open_angle_editor(q.ea); // geometric rotate to a typed angle
|
||
else
|
||
place_dimension(q); // promote -> driving dim + open editor
|
||
return true;
|
||
}
|
||
}
|
||
if (m_live_poly_fi >= 0) {
|
||
if ((m_live_poly_side_label - p).norm() <= ltol) {
|
||
open_polygon_side_editor(m_live_poly_fi); // geometric uniform scale
|
||
return true;
|
||
}
|
||
if ((m_live_poly_angle_label - p).norm() <= ltol) {
|
||
open_polygon_angle_editor(m_live_poly_fi); // geometric rotate
|
||
return true;
|
||
}
|
||
}
|
||
if (m_live_arc_ei >= 0 && (m_live_arc_angle_label - p).norm() <= ltol) {
|
||
open_arc_angle_editor(m_live_arc_ei); // geometric sweep change
|
||
return true;
|
||
}
|
||
if (m_live_ellipse_ei >= 0) {
|
||
if ((m_live_ellipse_major_label - p).norm() <= ltol) {
|
||
open_ellipse_axis_editor(m_live_ellipse_ei, true); // semi-major
|
||
return true;
|
||
}
|
||
if ((m_live_ellipse_minor_label - p).norm() <= ltol) {
|
||
open_ellipse_axis_editor(m_live_ellipse_ei, false); // semi-minor
|
||
return true;
|
||
}
|
||
}
|
||
if (m_live_rrect_fi >= 0) {
|
||
if ((m_live_rrect_w_label - p).norm() <= ltol) { open_rounded_rect_editor(m_live_rrect_fi, 0); return true; }
|
||
if ((m_live_rrect_h_label - p).norm() <= ltol) { open_rounded_rect_editor(m_live_rrect_fi, 1); return true; }
|
||
if ((m_live_rrect_r_label - p).norm() <= ltol) { open_rounded_rect_editor(m_live_rrect_fi, 2); return true; }
|
||
}
|
||
if (m_live_aslot_fi >= 0) {
|
||
if ((m_live_aslot_r_label - p).norm() <= ltol) { open_arc_slot_editor(m_live_aslot_fi, true); return true; }
|
||
if ((m_live_aslot_w_label - p).norm() <= ltol) { open_arc_slot_editor(m_live_aslot_fi, false); return true; }
|
||
}
|
||
if (m_live_slot_fi >= 0) {
|
||
if ((m_live_slot_len_label - p).norm() <= ltol) { open_slot_editor(m_live_slot_fi, 0); return true; }
|
||
if ((m_live_slot_w_label - p).norm() <= ltol) { open_slot_editor(m_live_slot_fi, 1); return true; }
|
||
if ((m_live_slot_angle_label - p).norm() <= ltol) { open_slot_editor(m_live_slot_fi, 2); return true; }
|
||
}
|
||
}
|
||
|
||
// A derived handle (the circle RadiusHandle — not an entity point, so
|
||
// hit_test_point can't grab it) arms a handle drag that resizes on motion.
|
||
// Checked before the point/entity hit-tests so the radius grip wins near
|
||
// the circle edge.
|
||
if (evt.LeftDown()) {
|
||
Handle hh;
|
||
if (hit_test_handle(p, tol, hh) &&
|
||
(hh.role == HandleRole::RadiusHandle || hh.role == HandleRole::MajorAxis ||
|
||
hh.role == HandleRole::MinorAxis || hh.role == HandleRole::BSplineCtrl)) {
|
||
m_dragging_handle = true;
|
||
m_drag_handle = hh;
|
||
m_selection.clear();
|
||
m_point_sel.clear();
|
||
m_selection.push_back(hh.ei); // highlight the circle being resized
|
||
if (on_selection_changed)
|
||
on_selection_changed(int(m_selection.size()));
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// A nearby endpoint/centre selects that POINT (a line = a segment + 2
|
||
// points); a click on the bare segment selects the whole entity.
|
||
if (evt.LeftDown()) {
|
||
int pe; SketchPointRole pr;
|
||
if (hit_test_point(p, tol, pe, pr)) {
|
||
const auto key = std::make_pair(pe, pr);
|
||
auto it = std::find(m_point_sel.begin(), m_point_sel.end(), key);
|
||
if (extend) {
|
||
if (it == m_point_sel.end()) m_point_sel.push_back(key);
|
||
else m_point_sel.erase(it);
|
||
} else {
|
||
m_selection.clear();
|
||
m_point_sel.clear();
|
||
m_point_sel.push_back(key);
|
||
}
|
||
// Arm the drag so the grabbed point follows the cursor.
|
||
m_dragging_point = true;
|
||
m_drag_ei = pe;
|
||
m_drag_role = pr;
|
||
// If the grabbed point is a polygon vertex, the drag must keep the
|
||
// polygon REGULAR — it adjusts the circumradius + orientation (the
|
||
// vertex follows the cursor) instead of moving one point freely.
|
||
const int pf = feature_of(pe);
|
||
m_drag_poly_fi = (pf >= 0 && m_features[pf].kind == FeatureKind::Polygon) ? pf : -1;
|
||
m_drag_rect_fi = -1; m_drag_slot_fi = -1;
|
||
if (pf >= 0 && m_drag_poly_fi < 0) {
|
||
const Feature& ft = m_features[pf];
|
||
if (ft.kind == FeatureKind::CornerRect || ft.kind == FeatureKind::CenterRect) {
|
||
// Only axis-aligned boxes resize by corner (oblique rects fall
|
||
// back to free point move). Capture the fixed opposite corner.
|
||
const SketchEntity& e0 = m_entities[ft.begin];
|
||
const Vec2d d0 = e0.p1 - e0.p0;
|
||
const bool aa = std::abs(d0.x()) < 1e-6 || std::abs(d0.y()) < 1e-6;
|
||
Vec2d gp;
|
||
if (aa && point_at(pe, pr, gp)) {
|
||
Vec2d opp;
|
||
opp.x() = (std::abs(gp.x() - ft.c0.x()) < std::abs(gp.x() - ft.c1.x())) ? ft.c1.x() : ft.c0.x();
|
||
opp.y() = (std::abs(gp.y() - ft.c0.y()) < std::abs(gp.y() - ft.c1.y())) ? ft.c1.y() : ft.c0.y();
|
||
m_drag_rect_fi = pf; m_drag_rect_anchor = opp;
|
||
}
|
||
} else if (ft.kind == FeatureKind::Slot &&
|
||
pr == SketchPointRole::Center &&
|
||
(pe == ft.begin + 1 || pe == ft.begin + 3)) {
|
||
m_drag_slot_fi = pf; // cap@c1 = begin+1, cap@c0 = begin+3
|
||
m_drag_slot_c1 = (pe == ft.begin + 1);
|
||
}
|
||
}
|
||
if (on_selection_changed)
|
||
on_selection_changed(int(m_selection.size() + m_point_sel.size()));
|
||
return true;
|
||
}
|
||
}
|
||
|
||
const int hit = hit_test(p, tol);
|
||
|
||
if (evt.LeftDClick() && hit >= 0) {
|
||
if (!extend) m_selection.clear();
|
||
for (int idx : connected_loop(hit))
|
||
if (std::find(m_selection.begin(), m_selection.end(), idx) == m_selection.end())
|
||
m_selection.push_back(idx);
|
||
} else if (hit >= 0) {
|
||
auto it = std::find(m_selection.begin(), m_selection.end(), hit);
|
||
if (extend) {
|
||
if (it == m_selection.end()) m_selection.push_back(hit);
|
||
else m_selection.erase(it); // toggle off
|
||
} else {
|
||
m_selection.clear();
|
||
m_point_sel.clear();
|
||
m_selection.push_back(hit);
|
||
}
|
||
} else if (!extend) {
|
||
// Inside a closed loop (not on an edge/point) → select it as a face
|
||
// and hand off to the panel, which commits the sketch and extrudes.
|
||
if (evt.LeftDown() && on_face_selected) {
|
||
const int reg = region_at(p);
|
||
if (reg >= 0) {
|
||
m_selection.clear();
|
||
m_point_sel.clear();
|
||
on_face_selected(reg);
|
||
return true;
|
||
}
|
||
}
|
||
m_selection.clear(); // clicked empty space
|
||
m_point_sel.clear();
|
||
}
|
||
if (on_selection_changed)
|
||
on_selection_changed(int(m_selection.size() + m_point_sel.size()));
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) {
|
||
// Hand the click back (return false) so the offer opens: the m_right_consumed flag
|
||
// this return value feeds means "the tool USED this right-click", and a plain
|
||
// right-click in Select mode is not a gesture terminator.
|
||
//
|
||
// But do NOT drop the selection on the way out. The offer menu describes WHAT IS
|
||
// SELECTED, so clearing first guaranteed it could only ever describe nothing: select
|
||
// a line, right-click, and the sketch verbs — Trim, Extend, Fillet, Chamfer, Offset,
|
||
// Mirror, the arrays, Constrain — were all greyed, because by the time the menu was
|
||
// built the line was no longer selected. Reported from the machine as "selected a
|
||
// line, right-click exit from selection: only create and reference are usable".
|
||
// Deselecting still has a gesture: left-click on empty space, a few lines above.
|
||
return false;
|
||
}
|
||
return false; // let drag orbit the camera
|
||
}
|
||
|
||
// Dimension mode: click entities directly. 2 points -> Distance, a line -> Length,
|
||
// a circle -> Diameter, an arc -> Radius, a point then a line -> DistanceToLine.
|
||
// Each resolved pick places a driving quote and pops the value card.
|
||
if (m_mode == Mode::Dimension) {
|
||
if (update_hover(canvas, evt)) return true; // repaint when the hovered handle changes
|
||
if (evt.LeftDClick()) { // double-click a quote label -> edit it
|
||
Vec2d p;
|
||
screen_to_plane(canvas, evt, p);
|
||
const Linef3 r2 = canvas.mouse_ray(Point(evt.GetX() + 28, evt.GetY()));
|
||
const Vec2d p2 = m_plane.project(r2.a, r2.vector());
|
||
const double di_tol = std::max(2.0, (p2 - p).norm());
|
||
const int di = hit_test_dimension(p, di_tol);
|
||
if (di >= 0) edit_dimension(di);
|
||
m_dim_has0 = false;
|
||
return true;
|
||
}
|
||
if (evt.LeftDown()) {
|
||
Vec2d p;
|
||
screen_to_plane(canvas, evt, p);
|
||
const Linef3 r2 = canvas.mouse_ray(Point(evt.GetX() + 8, evt.GetY()));
|
||
const Vec2d p2 = m_plane.project(r2.a, r2.vector());
|
||
const double tol = std::max(1e-3, (p2 - p).norm());
|
||
int pe; SketchPointRole pr;
|
||
const bool got_pt = hit_test_point(p, tol, pe, pr);
|
||
const int he = hit_test(p, tol);
|
||
if (!m_dim_has0) {
|
||
if (got_pt) { // first point picked: await a second
|
||
m_dim_e0 = pe; m_dim_r0 = pr; m_dim_has0 = true;
|
||
} else if (he >= 0) { // whole-entity dimension
|
||
DimAnnot a; a.ea = he;
|
||
const SketchEntity::Type t = m_entities[he].type;
|
||
if (t == SketchEntity::Type::Line) { a.kind = DimType::Length; place_dimension(a); }
|
||
else if (t == SketchEntity::Type::Circle) { a.kind = DimType::Diameter; place_dimension(a); }
|
||
else if (t == SketchEntity::Type::Arc) { a.kind = DimType::Radius; place_dimension(a); }
|
||
}
|
||
} else {
|
||
if (got_pt && !(pe == m_dim_e0 && pr == m_dim_r0)) {
|
||
DimAnnot a; a.kind = DimType::Distance;
|
||
a.ea = m_dim_e0; a.ra = m_dim_r0; a.eb = pe; a.rb = pr;
|
||
place_dimension(a);
|
||
} else if (he >= 0 && m_entities[he].type == SketchEntity::Type::Line) {
|
||
DimAnnot a; a.kind = DimType::DistanceToLine;
|
||
a.ea = m_dim_e0; a.ra = m_dim_r0; a.eb = he;
|
||
place_dimension(a);
|
||
}
|
||
m_dim_has0 = false; // reset after the second pick
|
||
}
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) { m_dim_has0 = false; return true; }
|
||
return false; // let drag orbit the camera
|
||
}
|
||
|
||
if (evt.Moving()) {
|
||
m_snap_off = evt.ShiftDown();
|
||
screen_to_plane(canvas, evt, m_cursor);
|
||
m_has_cursor = true;
|
||
m_cursor_locked = false;
|
||
bool vsnap = false;
|
||
m_cursor = snap_vertex(canvas, evt, m_cursor, vsnap); // preview-snap to endpoints
|
||
const bool line_like = (m_mode == Mode::Polyline || m_mode == Mode::Line);
|
||
if (line_like && !m_points.empty() && !vsnap)
|
||
m_cursor = snap_dir(m_points.back(), m_cursor, m_cursor_locked);
|
||
if (on_cursor_metrics && line_like && !m_points.empty() && !m_awaiting_length) {
|
||
const Vec2d d = m_cursor - m_points.back();
|
||
on_cursor_metrics(d.norm(), std::atan2(d.y(), d.x()) * 180.0 / M_PI, m_cursor_locked);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
switch (m_mode) {
|
||
|
||
case Mode::Polyline: {
|
||
if (evt.LeftDown()) {
|
||
Vec2d p;
|
||
screen_to_plane(canvas, evt, p);
|
||
m_snap_off = evt.ShiftDown();
|
||
bool vsnap = false;
|
||
p = snap_vertex(canvas, evt, p, vsnap); // snap onto an existing endpoint
|
||
if (near_first(p)) { // closing the current chain back to its start
|
||
const int base = int(m_entities.size());
|
||
push_closed_lines(m_points); // close the loop
|
||
infer_auto_constraints(base); // loop self-closes via auto Coincident + H/V
|
||
m_points.clear();
|
||
return true;
|
||
}
|
||
if (!m_points.empty() && !vsnap) {
|
||
bool lk = false;
|
||
p = snap_dir(m_points.back(), p, lk); // lock new segment to inference angle
|
||
}
|
||
m_points.push_back(p);
|
||
arm_polyline_segment_edit(); // refine this segment's Length+Angle, then continue
|
||
return true;
|
||
}
|
||
if (evt.LeftDClick()) {
|
||
if (m_points.size() >= 2) {
|
||
const int base = int(m_entities.size());
|
||
push_open_chain(m_points); // end as an open chain
|
||
infer_auto_constraints(base);
|
||
m_points.clear();
|
||
}
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) {
|
||
const int base = int(m_entities.size());
|
||
if (m_points.size() >= 3)
|
||
push_closed_lines(m_points);
|
||
else if (m_points.size() == 2)
|
||
push_open_chain(m_points);
|
||
infer_auto_constraints(base);
|
||
m_points.clear();
|
||
return true;
|
||
}
|
||
break;
|
||
}
|
||
|
||
case Mode::Line: {
|
||
if (evt.LeftDown()) {
|
||
Vec2d p;
|
||
screen_to_plane(canvas, evt, p);
|
||
m_snap_off = evt.ShiftDown();
|
||
bool vsnap = false;
|
||
p = snap_vertex(canvas, evt, p, vsnap); // snap onto an existing endpoint
|
||
if (m_points.empty()) { // first click = anchor
|
||
m_points.push_back(p);
|
||
return true;
|
||
}
|
||
bool lk = false;
|
||
if (!vsnap) p = snap_dir(m_points.back(), p, lk); // vertex snap wins over angle
|
||
m_points.push_back(p); // second click completes the segment
|
||
// Draw-then-edit: just commit the segment. The generic detect/service path
|
||
// (is_creation_autoedit_mode now includes Line) auto-selects it and opens its
|
||
// Length THEN Angle fields in sequence, each over its label — same UX as every
|
||
// other 2D tool. Enter advances (Length drives a Distance constraint, Angle rotates
|
||
// about P0); Esc keeps it as drawn.
|
||
keep_segment_as_drawn();
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) { // abandon the in-progress anchor
|
||
m_points.clear();
|
||
m_has_cursor = false;
|
||
return true;
|
||
}
|
||
break;
|
||
}
|
||
|
||
case Mode::CornerRect: {
|
||
if (evt.LeftDown()) {
|
||
Vec2d p;
|
||
screen_to_plane(canvas, evt, p);
|
||
if (m_points.empty()) {
|
||
m_points.push_back(p);
|
||
} else {
|
||
const Vec2d A = m_points[0];
|
||
const Vec2d B = p;
|
||
const int base = int(m_entities.size());
|
||
begin_feature(FeatureKind::CornerRect);
|
||
push_closed_lines({ A, Vec2d(B.x(), A.y()), B, Vec2d(A.x(), B.y()) });
|
||
infer_auto_constraints(base); // corners Coincident + sides H/V
|
||
end_feature(A, B); // group: Width/Height live quotes
|
||
m_points.clear();
|
||
}
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) { m_points.clear(); return true; }
|
||
break;
|
||
}
|
||
|
||
case Mode::CenterRect: {
|
||
if (evt.LeftDown()) {
|
||
Vec2d p;
|
||
screen_to_plane(canvas, evt, p);
|
||
if (m_points.empty()) {
|
||
m_points.push_back(p);
|
||
} else {
|
||
const Vec2d C = m_points[0];
|
||
const double hx = std::abs(p.x() - C.x());
|
||
const double hy = std::abs(p.y() - C.y());
|
||
const int base = int(m_entities.size());
|
||
begin_feature(FeatureKind::CenterRect);
|
||
push_closed_lines({
|
||
Vec2d(C.x() - hx, C.y() - hy), Vec2d(C.x() + hx, C.y() - hy),
|
||
Vec2d(C.x() + hx, C.y() + hy), Vec2d(C.x() - hx, C.y() + hy) });
|
||
infer_auto_constraints(base);
|
||
end_feature(Vec2d(C.x() - hx, C.y() - hy), Vec2d(C.x() + hx, C.y() + hy));
|
||
m_points.clear();
|
||
}
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) { m_points.clear(); return true; }
|
||
break;
|
||
}
|
||
|
||
case Mode::ObliqueRect: {
|
||
if (evt.LeftDown()) {
|
||
Vec2d p; screen_to_plane(canvas, evt, p);
|
||
bool vsnap = false;
|
||
if (m_points.size() < 2) // corners snap; 3rd click is the width
|
||
p = snap_vertex(canvas, evt, p, vsnap);
|
||
m_points.push_back(p);
|
||
if (m_points.size() == 3) {
|
||
const Vec2d A = m_points[0], B = m_points[1];
|
||
Vec2d u = B - A;
|
||
if (u.squaredNorm() > 1e-12) {
|
||
u.normalize();
|
||
const Vec2d n(-u.y(), u.x());
|
||
const double w = n.dot(m_points[2] - A); // signed perpendicular width
|
||
const int base = int(m_entities.size());
|
||
begin_feature(FeatureKind::CornerRect);
|
||
push_closed_lines({ A, B, B + n * w, A + n * w });
|
||
infer_auto_constraints(base); // corners Coincident + the AB pair parallel
|
||
end_feature(A, B + n * w); // group: Width/Height live quotes
|
||
}
|
||
m_points.clear();
|
||
}
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) { m_points.clear(); return true; }
|
||
break;
|
||
}
|
||
|
||
case Mode::RoundedRect: {
|
||
if (evt.LeftDown()) {
|
||
Vec2d p; screen_to_plane(canvas, evt, p);
|
||
bool vsnap = false;
|
||
if (m_points.size() < 2) // the two corners snap; 3rd sets radius
|
||
p = snap_vertex(canvas, evt, p, vsnap);
|
||
m_points.push_back(p);
|
||
if (m_points.size() == 3) {
|
||
const int base = int(m_entities.size());
|
||
begin_feature(FeatureKind::RoundedRect);
|
||
append_entities(make_rounded_rect(m_points[0], m_points[1], m_points[2]));
|
||
infer_auto_constraints(base);
|
||
// Group with the actual clamped fillet radius so the W/H/R live quotes
|
||
// and rebuild edits can recover the box. (Skip grouping if degenerate.)
|
||
const Vec2d a = m_points[0], b = m_points[1];
|
||
const double xmin=std::min(a.x(),b.x()), xmax=std::max(a.x(),b.x());
|
||
const double ymin=std::min(a.y(),b.y()), ymax=std::max(a.y(),b.y());
|
||
const double bw=xmax-xmin, bh=ymax-ymin;
|
||
const Vec2d cs[4]={{xmin,ymin},{xmax,ymin},{xmax,ymax},{xmin,ymax}};
|
||
double r=1e18; for(const Vec2d&c:cs) r=std::min(r,(m_points[2]-c).norm());
|
||
r=std::min(r, std::min(bw,bh)*0.5);
|
||
end_feature(Vec2d(xmin,ymin), Vec2d(xmax,ymax), r);
|
||
m_points.clear();
|
||
}
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) { m_points.clear(); return true; }
|
||
break;
|
||
}
|
||
|
||
case Mode::CenterCircle: {
|
||
if (evt.LeftDown()) {
|
||
Vec2d p;
|
||
screen_to_plane(canvas, evt, p);
|
||
if (m_points.empty()) {
|
||
m_points.push_back(p);
|
||
} else {
|
||
const Vec2d C = m_points[0];
|
||
push_circle(C, (p - C).norm());
|
||
m_points.clear();
|
||
}
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) { m_points.clear(); return true; }
|
||
break;
|
||
}
|
||
|
||
case Mode::TwoPointCircle: {
|
||
if (evt.LeftDown()) {
|
||
Vec2d p; screen_to_plane(canvas, evt, p);
|
||
bool vsnap = false;
|
||
p = snap_vertex(canvas, evt, p, vsnap); // diameter ends snap onto geometry
|
||
m_points.push_back(p);
|
||
if (m_points.size() == 2) {
|
||
const Vec2d C = (m_points[0] + m_points[1]) * 0.5;
|
||
push_circle(C, (m_points[1] - m_points[0]).norm() * 0.5);
|
||
m_points.clear();
|
||
}
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) { m_points.clear(); return true; }
|
||
break;
|
||
}
|
||
|
||
case Mode::ThreePointCircle: {
|
||
if (evt.LeftDown()) {
|
||
Vec2d p; screen_to_plane(canvas, evt, p);
|
||
m_points.push_back(p);
|
||
if (m_points.size() == 3) {
|
||
append_entities(make_three_point_circle(m_points[0], m_points[1], m_points[2]));
|
||
m_points.clear();
|
||
}
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) { m_points.clear(); return true; }
|
||
break;
|
||
}
|
||
|
||
case Mode::ThreePointArc: {
|
||
if (evt.LeftDown()) {
|
||
Vec2d p; screen_to_plane(canvas, evt, p);
|
||
bool vsnap = false;
|
||
if (m_points.size() < 2) // snap the start/end onto endpoints
|
||
p = snap_vertex(canvas, evt, p, vsnap); // (the 3rd click is the through-point)
|
||
m_points.push_back(p);
|
||
if (m_points.size() == 3) {
|
||
// clicks: start, end, point-on-arc
|
||
const int base = int(m_entities.size());
|
||
append_entities(make_three_point_arc(m_points[0], m_points[1], m_points[2]));
|
||
infer_auto_constraints(base); // arc ends Coincident onto snapped vertices
|
||
m_points.clear();
|
||
}
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) { m_points.clear(); return true; }
|
||
break;
|
||
}
|
||
|
||
case Mode::TangentArc: {
|
||
if (evt.LeftDown()) {
|
||
Vec2d p; screen_to_plane(canvas, evt, p);
|
||
bool vsnap = false;
|
||
p = snap_vertex(canvas, evt, p, vsnap); // snap both ends onto endpoints
|
||
m_points.push_back(p);
|
||
if (m_points.size() == 2) {
|
||
const int base = int(m_entities.size());
|
||
append_entities(make_tangent_arc(m_points[0], m_points[1]));
|
||
infer_auto_constraints(base); // tangent-arc ends Coincident onto vertices
|
||
m_points.clear();
|
||
}
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) { m_points.clear(); return true; }
|
||
break;
|
||
}
|
||
|
||
case Mode::CenterArc: {
|
||
if (evt.LeftDown()) {
|
||
Vec2d p; screen_to_plane(canvas, evt, p);
|
||
bool vsnap = false;
|
||
// The start (2nd click) snaps onto endpoints; center & end-dir are free.
|
||
if (m_points.size() == 1)
|
||
p = snap_vertex(canvas, evt, p, vsnap);
|
||
m_points.push_back(p);
|
||
if (m_points.size() == 3) {
|
||
const int base = int(m_entities.size());
|
||
append_entities(make_center_arc(m_points[0], m_points[1], m_points[2]));
|
||
infer_auto_constraints(base); // arc start Coincident onto a snapped vertex
|
||
m_points.clear();
|
||
}
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) { m_points.clear(); return true; }
|
||
break;
|
||
}
|
||
|
||
case Mode::Slot: {
|
||
if (evt.LeftDown()) {
|
||
Vec2d p; screen_to_plane(canvas, evt, p);
|
||
if (m_points.size() < 2) {
|
||
m_points.push_back(p);
|
||
} else {
|
||
// third click sets the half-width (distance to the centerline)
|
||
Vec2d u = m_points[1] - m_points[0];
|
||
if (u.squaredNorm() > 1e-12) {
|
||
u.normalize();
|
||
const Vec2d n(-u.y(), u.x());
|
||
const double w = std::abs(n.dot(p - m_points[0]));
|
||
const int base = int(m_entities.size());
|
||
begin_feature(FeatureKind::Slot);
|
||
append_entities(make_slot(m_points[0], m_points[1], w));
|
||
infer_auto_constraints(base);
|
||
end_feature(m_points[0], m_points[1], w); // centres + half-width
|
||
}
|
||
m_points.clear();
|
||
}
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) { m_points.clear(); return true; }
|
||
break;
|
||
}
|
||
|
||
case Mode::ArcSlot: {
|
||
if (evt.LeftDown()) {
|
||
Vec2d p; screen_to_plane(canvas, evt, p);
|
||
bool vsnap = false;
|
||
if (m_points.size() == 1) // start snaps; center & end-dir are free
|
||
p = snap_vertex(canvas, evt, p, vsnap);
|
||
m_points.push_back(p);
|
||
if (m_points.size() == 4) {
|
||
// clicks: center, start, end-dir, width
|
||
const double Rc = (m_points[1] - m_points[0]).norm();
|
||
const double w = std::abs((m_points[3] - m_points[0]).norm() - Rc);
|
||
const int base = int(m_entities.size());
|
||
begin_feature(FeatureKind::ArcSlot);
|
||
append_entities(make_arc_slot(m_points[0], m_points[1], m_points[2], w));
|
||
infer_auto_constraints(base);
|
||
// c0 = centre, c1 = centreline start point; param = half-width. The end
|
||
// direction is recovered from the cap@E arc centre when rebuilding.
|
||
end_feature(m_points[0], m_points[1], w);
|
||
m_points.clear();
|
||
}
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) { m_points.clear(); return true; }
|
||
break;
|
||
}
|
||
|
||
case Mode::Polygon: {
|
||
if (evt.LeftDown()) {
|
||
Vec2d p; screen_to_plane(canvas, evt, p);
|
||
if (m_points.empty()) {
|
||
m_points.push_back(p);
|
||
} else {
|
||
const int base = int(m_entities.size());
|
||
begin_feature(FeatureKind::Polygon);
|
||
append_entities(make_polygon(m_points[0], p, m_polygon_sides));
|
||
infer_auto_constraints(base);
|
||
end_feature(m_points[0], p, (p - m_points[0]).norm(), m_polygon_sides);
|
||
m_points.clear();
|
||
}
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) { m_points.clear(); return true; }
|
||
break;
|
||
}
|
||
|
||
case Mode::Ellipse: {
|
||
if (evt.LeftDown()) {
|
||
Vec2d p; screen_to_plane(canvas, evt, p);
|
||
bool vsnap = false;
|
||
if (m_points.empty()) p = snap_vertex(canvas, evt, p, vsnap); // center can snap
|
||
m_points.push_back(p);
|
||
if (m_points.size() == 3) { // center, major-end, minor
|
||
const int base = int(m_entities.size());
|
||
append_entities(make_ellipse(m_points[0], m_points[1], m_points[2]));
|
||
infer_auto_constraints(base);
|
||
m_points.clear();
|
||
}
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) { m_points.clear(); return true; }
|
||
break;
|
||
}
|
||
|
||
case Mode::EllipseArc: {
|
||
if (evt.LeftDown()) {
|
||
Vec2d p; screen_to_plane(canvas, evt, p);
|
||
bool vsnap = false;
|
||
if (m_points.empty()) p = snap_vertex(canvas, evt, p, vsnap); // center can snap
|
||
m_points.push_back(p);
|
||
if (m_points.size() == 5) { // center, major, minor, start, end
|
||
const int base = int(m_entities.size());
|
||
append_entities(make_ellipse_arc(m_points[0], m_points[1], m_points[2],
|
||
m_points[3], m_points[4]));
|
||
infer_auto_constraints(base);
|
||
m_points.clear();
|
||
}
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) { m_points.clear(); return true; }
|
||
break;
|
||
}
|
||
|
||
case Mode::BSpline: {
|
||
// Variable-length: left-click adds a control pole (each can snap onto existing
|
||
// geometry); double-click or right-click finishes as an open spline.
|
||
if (evt.LeftDown()) {
|
||
Vec2d p; screen_to_plane(canvas, evt, p);
|
||
m_snap_off = evt.ShiftDown();
|
||
bool vsnap = false;
|
||
p = snap_vertex(canvas, evt, p, vsnap); // poles can land on endpoints
|
||
m_points.push_back(p);
|
||
return true;
|
||
}
|
||
if (evt.LeftDClick() || evt.RightDown()) {
|
||
if (m_points.size() >= 2) {
|
||
const int base = int(m_entities.size());
|
||
append_entities(make_bspline(m_points));
|
||
infer_auto_constraints(base); // end poles auto-Coincident -> loops close
|
||
}
|
||
m_points.clear();
|
||
return true;
|
||
}
|
||
break;
|
||
}
|
||
|
||
case Mode::Point: {
|
||
if (evt.LeftDown()) {
|
||
Vec2d p;
|
||
screen_to_plane(canvas, evt, p);
|
||
push_point(p);
|
||
return true;
|
||
}
|
||
if (evt.RightDown()) { return true; }
|
||
break;
|
||
}
|
||
|
||
case Mode::Constrain:
|
||
break;
|
||
}
|
||
|
||
if (evt.Dragging())
|
||
return false;
|
||
|
||
return false;
|
||
}
|
||
|
||
}} // namespace Slic3r::GUI
|