Design: double-click a feature row to edit it, and preview Transform live

snaporca-x1k7 filed three Transform defects. Two were real and are fixed here;
the third does not reproduce and is withdrawn with its measurement.

(1) The feature tree had no ITEM_ACTIVATED binding at all, so double-clicking any
row only highlighted it. Double-click is the documented edit gesture elsewhere
(a committed sketch opens that way on the canvas), which made every feature look
dead until the user found the Edit button in the section header. Bound to
on_edit_feature(), so the gesture now works for every feature type, not just
Transform.

(3) The Transform card's typed fields called refresh_preview(), but preview_fields
returns {} for Transform and the solid-preview path has no ghost to build for a
feature that moves an existing body — so typing a distance changed nothing on
screen until Confirm. The fields now drive the same channel the gizmo drag already
uses: the body's display transform. In EDIT mode the committed transform is
already baked into the kernel geometry, so the preview undoes it first; without
that term, re-opening a committed Z=20 and typing 40 would show the body at 60.

(2) NOT REPRODUCED. Dragging a rotation ring does fill the field: a tangential
drag on the red ring gave Rotate axis = X, Angle = 27.44 deg, plus the translation
that rotating about the card's pivot implies (Y 17.60, Z -62.11). The original
reading came from a drag that never grabbed the 7 px ring; this run took its
candidate points from the rendered ring pixels themselves and 6 of 6 answered.

Rig-measured (docker snaporca-gui, Xvfb :10, llvmpipe), vertical screen shift of
the body by image correlation:
  commit Translate Z 0 -> 20        : +140 px
  double-click the Transform row     : +0 px, and the card re-opens showing 20.00
  step the re-opened card 20 -> 40   : +142 px  (not +280 -> the undo term is right)
  Cancel                             : +0 px vs the committed frame, residual 0.46
Add mode: stepping Z moves the body immediately (viewport diff bbox
200,143-1181,999); Cancel puts it back with only the status strip differing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tommaso Bianchi
2026-08-15 14:16:08 +02:00
co-authored by Claude Opus 5
parent f4a0bf8845
commit 8e5d0d195c
2 changed files with 76 additions and 0 deletions
+70
View File
@@ -2997,6 +2997,11 @@ DesignPanel::DesignPanel(wxWindow* parent)
}
});
// Double-click a row = Edit, the same gesture that re-opens a committed sketch on the canvas.
// Without it the row only highlights and the feature looks dead until the user finds the
// Edit button in the section header.
m_tree->Bind(wxEVT_TREE_ITEM_ACTIVATED, [this](wxTreeEvent&) { on_edit_feature(); });
// Feature-tree edit actions: act on the selected feature (delete / reorder). These sit in the
// card header (Prepare puts its section actions there too) rather than on a loose row below.
{
@@ -5078,8 +5083,68 @@ void DesignPanel::on_add_thicken_surface()
refresh_tree();
}
// Compose the same matrix apply_transform() builds in the kernel: rotate about the pivot first,
// then translate. Kept here so the preview and the committed feature can never disagree.
static Transform3d xf_compose(const Vec3d& t, const Vec3d& axis, const Vec3d& pivot, double deg)
{
Transform3d r = Transform3d::Identity();
if (std::abs(deg) > 1e-12 && axis.norm() > 1e-9)
r = Transform3d(Eigen::Translation3d(pivot))
* Transform3d(Eigen::AngleAxisd(deg * M_PI / 180.0, axis.normalized()))
* Transform3d(Eigen::Translation3d(-pivot));
return Transform3d(Eigen::Translation3d(t)) * r;
}
// Live preview for the Transform card. The gizmo already writes its drag into the body's display
// transform; the typed fields must use the SAME channel or the numbers and the geometry disagree
// until Confirm — which is what makes typing a distance look like it did nothing. In edit mode the
// committed transform is already baked into the kernel geometry, so undo it first: without that,
// typing 80 over a committed 34 would show the body at 114.
void DesignPanel::xf_live_preview()
{
if (m_active != Tool::Transform || m_xf_dx == nullptr) return;
if (m_xf_copy && m_xf_copy->GetValue()) return; // a copy leaves the original where it is
sync_body_xform();
const int sel = m_xf_body ? m_xf_body->GetSelection() : wxNOT_FOUND;
const int b = (sel != wxNOT_FOUND) ? sel : int(m_body_xform.size()) - 1;
if (b < 0 || b >= int(m_body_xform.size())) return;
if (m_xf_prev_body != b) {
xf_clear_preview(); // the card retargeted: hand the old body back first
m_xf_prev_body = b;
m_xf_prev_base = (b == m_xf_gizmo_body) ? m_xf_gizmo_base : m_body_xform[b];
}
const int ax = m_xf_axis->GetSelection();
const Vec3d axis = (ax == 0) ? Vec3d::UnitX() : (ax == 1) ? Vec3d::UnitY() : Vec3d::UnitZ();
const Vec3d pivot(m_xf_pivot_x->GetValue(), m_xf_pivot_y->GetValue(), m_xf_pivot_z->GetValue());
const Transform3d want = xf_compose(Vec3d(m_xf_dx->GetValue(), m_xf_dy->GetValue(),
m_xf_dz->GetValue()),
axis, pivot, m_xf_angle->GetValue());
Transform3d undo = Transform3d::Identity();
if (m_edit_index >= 0 && m_edit_index < int(m_doc.features.size())) {
const CadFeature& f = m_doc.features[m_edit_index];
if (f.type == CadFeatureType::Transform && !f.xf_copy)
undo = xf_compose(f.xf_translate, f.xf_axis, f.xf_pivot, f.xf_angle_deg).inverse();
}
m_body_xform[b] = want * undo * m_xf_prev_base;
feed_bodies();
}
// Hand a previewed body back to the pose it had before the card touched it. Every exit from the
// Transform card passes through here, so a preview can never survive into the committed document.
void DesignPanel::xf_clear_preview()
{
if (m_xf_prev_body < 0) return;
sync_body_xform();
if (m_xf_prev_body < int(m_body_xform.size()))
m_body_xform[m_xf_prev_body] = m_xf_prev_base;
m_xf_prev_body = -1;
feed_bodies();
}
void DesignPanel::on_add_transform()
{
xf_clear_preview(); // the feature performs this motion parametrically; the preview must go
// 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.
@@ -9848,6 +9913,10 @@ void DesignPanel::refresh_preview()
{
if (m_active == Tool::None) { m_viewport->clear_preview(); return; }
// Transform has no ghost: it moves an existing body rather than building a new one, so its
// preview IS the body, shown through the display transform the gizmo already drives.
if (m_active == Tool::Transform) { xf_live_preview(); return; }
// Features that produce NO solid: a sketch, the three datums, a helix curve, and Project
// (which emits sketch entities). They have no ghost to build, so they must not go through
// the solid-preview path below — it finds nothing and reports "invalid: preview produced
@@ -10344,6 +10413,7 @@ void DesignPanel::close_tool()
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.
xf_clear_preview();
if (m_xf_gizmo_body >= 0) {
sync_body_xform();
if (m_xf_gizmo_body < int(m_body_xform.size()))
+6
View File
@@ -124,6 +124,8 @@ private:
void on_add_surface_offset();
void on_add_thicken_surface();
void on_add_transform();
void xf_live_preview(); // typed Transform fields -> body display transform (live)
void xf_clear_preview(); // hand a previewed body back to its pre-card pose
void on_add_mirror();
void on_add_thicken();
void on_add_rib();
@@ -358,6 +360,10 @@ private:
// Both use the same gizmo; only this says which card owns the numbers it reports.
int m_xf_gizmo_body{-1};
Transform3d m_xf_gizmo_base{Transform3d::Identity()}; // pose when Transform armed it
// Which body the Transform card's typed fields are currently previewing on, and the pose to
// hand it back to. Separate from the gizmo pair because the card can retarget its Body combo.
int m_xf_prev_body{-1};
Transform3d m_xf_prev_base{Transform3d::Identity()};
// Onshape-style dialog-card title rows (icon + bold feature name), retitled
// per tool in open_tool() (edit-mode shows the feature's actual name).