Support tracking and direct distance input for grip editing

This commit is contained in:
gianlucafiore 2026-08-06 23:35:58 -03:00
commit 62457be90e
4 changed files with 231 additions and 11 deletions

View file

@ -281,6 +281,95 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
self.refresh_properties();
return Task::none();
}
// Direct-distance entry while a normal grip stretch is active.
//
// The live grip-drag path has already resolved OSNAP / OTRACK /
// Extension / Polar / Ortho into `last_cursor_world`. A typed scalar
// therefore means: move from the grip's original position by that
// distance along the currently indicated direction.
{
let i = self.active_tab;
if let Some(grip) = self.tabs[i].active_grip.clone() {
if grip.mode == GripEditMode::Stretch {
let text =
crate::app::expr_eval::eval_to_string(self.command_line.input.trim());
if let Some(dist) = crate::app::expr_eval::eval_number(text.trim()) {
let cursor = self.tabs[i].last_cursor_world;
// If the cursor is following OTRACK or Extension, use the actual
// reference ray that is currently driving the cursor. Otherwise
// fall back to the direction from the original grip position,
// which covers Polar / Ortho / free direct-distance entry.
let target = if let Some((base, dir)) = self.active_distance_ray(i) {
base + dir * dist
} else {
let direction = cursor - grip.origin_world;
let len = direction.length();
if len <= 1e-12 {
self.command_line.push_error(
"Move the cursor in a direction before entering a distance.",
);
return self.focus_cmd_input();
}
let dir = direction / len;
grip.origin_world + dir * dist
};
// The entity is already being edited live. Apply only the
// incremental movement from its current grip position to the
// exact typed-distance position.
let delta = target - grip.last_world;
let actions: Vec<_> = grip
.targets
.iter()
.map(|target_grip| {
let apply = if target_grip.is_translate {
GripApply::Translate(delta)
} else {
GripApply::Absolute(target_grip.last_world + delta)
};
(
target_grip.handle,
target_grip.grip_id,
apply,
)
})
.collect();
for (handle, grip_id, apply) in actions {
self.tabs[i]
.scene
.apply_grip(handle, grip_id, apply);
}
// Keep the live GripEdit state synchronized so the normal
// grip commit path sees the exact final position.
if let Some(active) = self.tabs[i].active_grip.as_mut() {
active.last_world = target;
for target_grip in &mut active.targets {
target_grip.last_world += delta;
}
}
self.tabs[i].last_cursor_world = target;
self.command_line.input.clear();
self.tabs[i].dirty = true;
// Reuse the existing click-move-click grip finalization:
// undo grouping, preview restoration, grip cleanup, etc.
return self.on_viewport_left_release();
}
}
}
}
// Interactive VPORTS: the entry after a bare `VPORTS` is the
// tiled configuration. Empty input defaults to SINGLE.
if self.awaiting_vports {
@ -386,10 +475,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
return self.feed_command(crate::command::StepInput::Enter);
}
// OTRACK: while aligned to a tracking ray, a bare distance
// places the point along the ray from the tracking point
// (issue #69).
if let Some((base, dir)) = self.otrack_active {
// OTRACK and Extension both allow a bare scalar to act as a
// distance measured along the active reference ray.
if let Some((base, dir)) = self.active_distance_ray(i) {
if let Some(dist) = crate::app::expr_eval::eval_number(text.trim()) {
let pt = base + dir * dist;
if !self.command_point_allowed(i, pt) {