Transform: drag the body, the numbers follow

The Placement > Transform verb opened a card of spin controls — dx/dy/dz, an
axis combo, an angle — with nothing on the geometry. The 3-axis drag gizmo the
charter asks for already existed and was fully implemented (arrows, rotation
rings, click-to-type per axis), reachable only from a small icon button in the
tree card header. The prominent verb opened the form; the geometry-first
control was hidden behind an icon. That was backwards.

Transform now arms that same gizmo on the target body. The card stays as L2's
typed half: the drag writes dx/dy/dz, the axis and the angle, and the pivot is
seeded from the body's centroid so the parametric feature reproduces exactly
what was dragged.

Decomposition is exact for the interaction that matters — the gizmo's rings are
per-world-axis, so a ring drag is an axial rotation. A pose composed from two
rings is not axial and the card can only name one axis, so it reports the
dominant one rather than refusing to answer.

Three things this had to get right:

- The gizmo bakes its drag into the display transform so the body follows the
  cursor, and the feature performs the same motion parametrically. Committing
  without reverting first would move the body twice.
- tool_confirm() and tool_cancel() both tested moving_body() BEFORE the active
  tool, so with the gizmo armed Confirm would have dropped the gizmo and never
  created the feature. Both are now guarded on Tool::None.
- close_tool() is the single revert point. Esc, Cancel and switching tools all
  pass through it, so a Transform that was never committed cannot leave the body
  displaced.

Edit mode is untouched: re-seeding the gizmo from a stored feature is a separate
problem, so editing an existing Transform still gets the card alone.

Reviewed and compiled (libslic3r_gui, RC=0); not exercised. snaporca-qtf4.
This commit is contained in:
Tommaso Bianchi
2026-08-12 20:04:45 +02:00
parent 3f62d4d58d
commit b7397b48bd
2 changed files with 91 additions and 2 deletions
+86 -2
View File
@@ -3568,6 +3568,26 @@ DesignPanel::DesignPanel(wxWindow* parent)
if (body < 0 || body >= int(m_body_xform.size())) return;
m_body_xform[body] = xform; // full move+rotate transform, baked into the mesh at Commit
feed_bodies(); // rebuilds the transformed meshes in place + refreshes display + pick
// When the move gizmo is serving the Transform card, mirror the drag into the card's
// numeric fields (the card is the typed half, the gizmo the geometry-first half).
if (body == m_xf_gizmo_body && m_active == Tool::Transform) {
const Transform3d d = xform * m_xf_gizmo_base.inverse();
const Vec3d t = d.translation();
if (m_xf_dx) m_xf_dx->SetValue(t.x());
if (m_xf_dy) m_xf_dy->SetValue(t.y());
if (m_xf_dz) m_xf_dz->SetValue(t.z());
const Eigen::AngleAxisd aa(Eigen::Quaterniond(d.linear()).normalized());
// The gizmo's rings are per-world-axis, so a ring drag yields an exactly axial
// rotation and this is lossless for the normal interaction. A composed multi-ring
// pose is not axial; the card can only express one axis, so report the dominant one
// rather than refusing to answer.
int ax = 0; double best = std::abs(aa.axis().x());
if (std::abs(aa.axis().y()) > best) { ax = 1; best = std::abs(aa.axis().y()); }
if (std::abs(aa.axis().z()) > best) { ax = 2; best = std::abs(aa.axis().z()); }
const double sign = (aa.axis()[ax] < 0.0) ? -1.0 : 1.0;
if (m_xf_axis) m_xf_axis->SetSelection(ax);
if (m_xf_angle) m_xf_angle->SetValue(sign * aa.angle() * 180.0 / M_PI);
}
const int nb = int(m_doc.bodies.size());
const wxString tag = (nb > 1) ? wxString::Format(_L("Body %d "), body + 1) : wxString();
const Vec3d t = xform.translation();
@@ -4796,6 +4816,20 @@ void DesignPanel::on_add_thicken_surface()
void DesignPanel::on_add_transform()
{
// The gizmo baked its drag into the display transform so the body followed the cursor.
// The feature about to be created performs that same motion parametrically, so hand the
// body back to its pre-drag pose first or it moves twice.
if (m_xf_gizmo_body >= 0) {
sync_body_xform();
if (m_xf_gizmo_body < int(m_body_xform.size()))
m_body_xform[m_xf_gizmo_body] = m_xf_gizmo_base;
if (m_viewport) m_viewport->clear_move_gizmo();
feed_bodies(); // set_status_ok() re-feeds on success, but the recompute-ERROR path
// below does not — without this the body would keep showing the
// dragged pose after a Transform that failed to build.
m_xf_gizmo_body = -1;
m_move_body = -1;
}
if (m_doc.bodies.empty()) {
set_status(_L("Transform needs a body — add or import one first"));
return;
@@ -6079,6 +6113,40 @@ void DesignPanel::on_move_body()
m_status->Refresh();
}
// Transform card (add mode): arm the same move gizmo on the card's body so the geometry-first
// control is what the user drags, while the card's numeric fields mirror the result. The card
// stays visible as the typed half — the gizmo owns the drag, the fields own the numbers.
void DesignPanel::arm_transform_gizmo()
{
int b = tree_body_selection();
if (b < 0) b = m_sel_solid_body;
if (b < 0 && m_xf_body) b = m_xf_body->GetSelection();
if (b < 0 || b >= int(m_doc.display_body_meshes.size())) return; // card alone still works
if (m_xf_body && b < int(m_xf_body->GetCount())) m_xf_body->SetSelection(b); // feature must be created against the gizmo's body
sync_body_xform();
const Transform3d base = m_body_xform[b];
const BoundingBoxf3 bb = m_doc.display_body_meshes[b].bounding_box();
const Vec3d pivot = base * bb.center();
const double radius = (base.linear() * (bb.size() * 0.5)).norm();
if (m_viewport) m_viewport->begin_move_body(b, pivot, base, radius);
m_xf_gizmo_body = b;
m_xf_gizmo_base = base;
m_move_body = b; // the existing revert paths key off these two
m_move_prev = base;
// Write the pivot into the card so the parametric feature reproduces what was dragged;
// dx/dy/dz and angle start at zero (the gizmo reports deltas from this pose).
if (m_xf_pivot_x) m_xf_pivot_x->SetValue(pivot.x());
if (m_xf_pivot_y) m_xf_pivot_y->SetValue(pivot.y());
if (m_xf_pivot_z) m_xf_pivot_z->SetValue(pivot.z());
if (m_xf_dx) m_xf_dx->SetValue(0.0);
if (m_xf_dy) m_xf_dy->SetValue(0.0);
if (m_xf_dz) m_xf_dz->SetValue(0.0);
if (m_xf_angle) m_xf_angle->SetValue(0.0);
m_status->SetForegroundColour(wxNullColour);
set_status(_L("Drag the arrows to move, the rings to rotate — the numbers follow"));
m_status->Refresh();
}
// Color tool: open a colour picker on the selected body and store a per-body display-colour
// override on its CadBody. The override is carried across recompute() by body index and is
// read back by DesignCanvas::body_color()/reload(), so the body keeps its colour through edits.
@@ -9465,12 +9533,28 @@ void DesignPanel::open_tool(Tool t)
m_form->FitInside();
update_action_bar(); // a tool is now active -> show the unified ✓/✗
refresh_preview();
// Add-mode Transform arms the move gizmo on the card's body so the prominent verb is the
// geometry-first control; the card mirrors the drag. Edit mode keeps today's card-only path.
if (t == Tool::Transform && m_edit_index < 0)
arm_transform_gizmo();
}
void DesignPanel::close_tool()
{
m_active = Tool::None;
set_active_tool_btn(nullptr); // clear the active-tool teal highlight
// Single revert point for the Transform gizmo: Esc, switching tools, and Cancel all pass
// through here, so the body is never left displaced by a Transform that wasn't committed.
if (m_xf_gizmo_body >= 0) {
sync_body_xform();
if (m_xf_gizmo_body < int(m_body_xform.size()))
m_body_xform[m_xf_gizmo_body] = m_xf_gizmo_base;
if (m_viewport) m_viewport->clear_move_gizmo();
feed_bodies();
m_xf_gizmo_body = -1;
m_move_body = -1;
}
if (m_viewport) { m_viewport->set_body_translucent(false); m_viewport->set_body_hidden(false); m_viewport->set_xray_focus(-1); } // restore the opaque solid
wxSizer* s = m_cards->GetSizer();
s->Show(m_box_sketch, false, true);
@@ -9594,7 +9678,7 @@ void DesignPanel::cancel_tool()
void DesignPanel::tool_confirm()
{
if (m_value_cont) { confirm_value(); return; } // value card owns ribbon ✓ while a value is pending
if (m_viewport && m_viewport->moving_body()) { // keep the placement, drop the gizmo
if (m_active == Tool::None && m_viewport && m_viewport->moving_body()) { // keep the placement, drop the gizmo
m_viewport->clear_move_gizmo();
m_move_body = -1;
show_move_card(false);
@@ -9625,7 +9709,7 @@ void DesignPanel::tool_confirm()
void DesignPanel::tool_cancel()
{
if (m_value_cont) { cancel_value(); return; } // value card owns ribbon ✗ while a value is pending
if (m_viewport && m_viewport->moving_body()) { // revert to the pose at move-start
if (m_active == Tool::None && m_viewport && m_viewport->moving_body()) { // revert to the pose at move-start
sync_body_xform();
if (m_move_body >= 0 && m_move_body < int(m_body_xform.size()))
m_body_xform[m_move_body] = m_move_prev;
+5
View File
@@ -336,6 +336,10 @@ private:
// Cancel reverts to the pose captured when the move started.
int m_move_body{-1};
Transform3d m_move_prev{Transform3d::Identity()};
// Set while the move gizmo is serving the Transform CARD rather than the Move button.
// Both use the same gizmo; only this says which card owns the numbers it reports.
int m_xf_gizmo_body{-1};
Transform3d m_xf_gizmo_base{Transform3d::Identity()}; // pose when Transform armed it
// Onshape-style dialog-card title rows (icon + bold feature name), retitled
// per tool in open_tool() (edit-mode shows the feature's actual name).
@@ -783,6 +787,7 @@ private:
void rebuild_disp_meshes(); // recompute m_disp_* from m_doc + m_body_xform
void feed_bodies(); // push m_disp_* + visibility/xform to the viewport
void on_move_body(); // start the move gizmo on the selected body
void arm_transform_gizmo(); // arm the move gizmo on the Transform card's body (add mode only)
void on_set_body_color(); // Color tool: pick a per-body display colour override
int tree_selection() const; // selected feature row, or wxNOT_FOUND
int tree_body_selection() const; // selected Parts-list body index, or -1