forked from 1b-agent/opencadstudio-upstream
Merge pull request #691 from gianlucafiore/fix/grip-direct-distance
Support tracking and direct distance input for grip editing
This commit is contained in:
commit
687bc2079d
4 files changed
+231
-11
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -22,6 +22,25 @@ use iced::{mouse, Point, Task};
|
||||
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(in crate::app) fn active_distance_ray(
|
||||
&self,
|
||||
i: usize,
|
||||
) -> Option<(glam::DVec3, glam::DVec3)> {
|
||||
// OTRACK already exposes its acquired world-space ray.
|
||||
if let Some((base, dir)) = self.otrack_active {
|
||||
return Some((base, dir));
|
||||
}
|
||||
|
||||
// Extension stores the snapped point. Recover the acquired endpoint
|
||||
// and outward segment direction that produced that snap.
|
||||
let snap = self.tabs[i].snap_result?;
|
||||
|
||||
if snap.snap_type == crate::snap::SnapType::Extension {
|
||||
return self.snapper.extension_input_ray(snap.world);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
/// Rebuild the active tab's dynamic-input field set to match what the
|
||||
/// command is currently asking for. Called on cursor move and after
|
||||
/// command-state changes. The field set only changes shape when the
|
||||
@@ -79,11 +98,11 @@ impl OpenCADStudio {
|
||||
let has_base = self.last_point.is_some();
|
||||
// While aligned to an OTRACK ray, the point step reads a single
|
||||
// distance along the ray (issue #69) — show one Distance box.
|
||||
let otrack_dist = self.otrack_active.is_some()
|
||||
let guided_dist = self.active_distance_ray(i).is_some()
|
||||
&& !wants_text
|
||||
&& matches!(field, crate::command::DynField::Point);
|
||||
let default: Vec<DynComponent> = match field {
|
||||
_ if otrack_dist => vec![DynComponent::Distance],
|
||||
_ if guided_dist => vec![DynComponent::Distance],
|
||||
crate::command::DynField::Distance => vec![DynComponent::Distance],
|
||||
crate::command::DynField::Angle => vec![DynComponent::Angle],
|
||||
crate::command::DynField::Scalar => vec![DynComponent::Scalar],
|
||||
@@ -528,9 +547,9 @@ impl OpenCADStudio {
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// OTRACK: while aligned to a tracking ray, a typed value is a distance
|
||||
// along the ray from the tracking point (issue #69).
|
||||
if let Some((base, dir)) = self.otrack_active {
|
||||
// OTRACK and Extension both allow a typed scalar to act as a
|
||||
// distance measured along the active reference ray.
|
||||
if let Some((base, dir)) = self.active_distance_ray(i) {
|
||||
let wants_text = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
|
||||
@@ -981,8 +981,55 @@ impl OpenCADStudio {
|
||||
let snap_hit =
|
||||
self.snapper
|
||||
.snap(raw, p, &snap_candidates, view_rot, eye, bounds, go, gr);
|
||||
let mut snapped = snap_hit.map(|s| s.world).unwrap_or(raw);
|
||||
|
||||
self.tabs[i].snap_result = snap_hit;
|
||||
|
||||
// Grip edits use the same tracking-point acquisition and OTRACK
|
||||
// resolution as normal interactive point commands.
|
||||
let otrack_hit = {
|
||||
let snap_world = snap_hit.map(|s| s.world);
|
||||
|
||||
self.snapper.update_otrack_dwell(
|
||||
snap_world,
|
||||
&snap_candidates,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
Instant::now(),
|
||||
);
|
||||
|
||||
if snap_hit.is_none() {
|
||||
let polar_step = if self.polar_mode {
|
||||
Some(self.polar_increment_deg)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (_, ucs_x, ucs_y, _) = self.tabs[i].ucs_xform().axes();
|
||||
|
||||
self.snapper.otrack_snap(
|
||||
raw,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
polar_step,
|
||||
Some(grip.origin_world),
|
||||
self.ortho_mode,
|
||||
ucs_x,
|
||||
ucs_y,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
self.otrack_active = otrack_hit.map(|hit| (hit.base, hit.dir));
|
||||
|
||||
let mut snapped = if let Some(hit) = otrack_hit {
|
||||
hit.aligned
|
||||
} else {
|
||||
snap_hit.map(|s| s.world).unwrap_or(raw)
|
||||
};
|
||||
if let Some(s) = self.tabs[i].snap_result.as_mut() {
|
||||
s.screen.x += tile_b.x;
|
||||
s.screen.y += tile_b.y;
|
||||
@@ -1007,7 +1054,9 @@ impl OpenCADStudio {
|
||||
}
|
||||
if let Some(dir) = self.axis_lock_dir {
|
||||
snapped = axis_lock_apply(snapped, grip.origin_world, dir);
|
||||
} else if !snap_hit.is_some_and(|s| s.snap_type != crate::snap::SnapType::Grid) {
|
||||
} else if otrack_hit.is_none()
|
||||
&& !snap_hit.is_some_and(|s| s.snap_type != crate::snap::SnapType::Grid)
|
||||
{
|
||||
let base = grip.origin_world;
|
||||
let ucs_xf = self.tabs[i].ucs_xform();
|
||||
if self.ortho_mode {
|
||||
@@ -1027,6 +1076,11 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
let snap_ms = snap_started.elapsed().as_secs_f64() * 1000.0;
|
||||
// The overlay builds the active tracking guide from `otrack_active.base`
|
||||
// to `last_cursor_world`. Keep it synchronized with the actual point used
|
||||
// by the grip, otherwise the guide and the edited geometry diverge.
|
||||
self.tabs[i].last_cursor_world = snapped;
|
||||
self.tabs[i].last_cursor_screen = p_full;
|
||||
let apply_started = Instant::now();
|
||||
let delta = snapped - grip.last_world;
|
||||
let lengthen = grip.mode == GripEditMode::Lengthen;
|
||||
@@ -1988,6 +2042,20 @@ impl OpenCADStudio {
|
||||
.snap(raw, p, &snap_candidates, view_rot, eye, bounds, go, gr);
|
||||
let world = snap_hit.map(|s| s.world).unwrap_or(raw);
|
||||
self.tabs[i].snap_result = snap_hit;
|
||||
if let Some(s) = self.tabs[i].snap_result.as_mut() {
|
||||
s.screen.x += tile_b.x;
|
||||
s.screen.y += tile_b.y;
|
||||
|
||||
if let Some(base) = s.extension_base.as_mut() {
|
||||
base.x += tile_b.x;
|
||||
base.y += tile_b.y;
|
||||
}
|
||||
|
||||
if let Some(base) = s.extension_base2.as_mut() {
|
||||
base.x += tile_b.x;
|
||||
base.y += tile_b.y;
|
||||
}
|
||||
}
|
||||
if let Some(s) = self.tabs[i].snap_result.as_mut() {
|
||||
// Snap marker is pane-local; lift it to absolute canvas px.
|
||||
s.screen.x += tile_b.x;
|
||||
|
||||
+45
@@ -1620,6 +1620,51 @@ impl Snapper {
|
||||
|
||||
best
|
||||
}
|
||||
pub fn extension_input_ray(
|
||||
&self,
|
||||
snapped: glam::DVec3,
|
||||
) -> Option<(glam::DVec3, glam::DVec3)> {
|
||||
let mut best: Option<(f64, glam::DVec3, glam::DVec3)> = None;
|
||||
|
||||
for (&origin, dirs) in self.tracking_points.iter().zip(&self.tracking_dirs) {
|
||||
for &dir in dirs {
|
||||
let len2 = dir.x * dir.x + dir.y * dir.y;
|
||||
if len2 < 1e-12 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dx = snapped.x - origin.x;
|
||||
let dy = snapped.y - origin.y;
|
||||
|
||||
let t = (dx * dir.x + dy * dir.y) / len2;
|
||||
|
||||
// Extension only exists beyond the acquired endpoint.
|
||||
if t < 0.05 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let projected = glam::DVec3::new(
|
||||
origin.x + dir.x * t,
|
||||
origin.y + dir.y * t,
|
||||
origin.z,
|
||||
);
|
||||
|
||||
let ex = snapped.x - projected.x;
|
||||
let ey = snapped.y - projected.y;
|
||||
let err2 = ex * ex + ey * ey;
|
||||
|
||||
let len = len2.sqrt();
|
||||
let unit_dir =
|
||||
glam::DVec3::new(dir.x / len, dir.y / len, 0.0);
|
||||
|
||||
if best.as_ref().map_or(true, |(best_err, _, _)| err2 < *best_err) {
|
||||
best = Some((err2, origin, unit_dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
best.map(|(_, origin, dir)| (origin, dir))
|
||||
}
|
||||
}
|
||||
|
||||
// ── Object-snap priority ───────────────────────────────────────────────────
|
||||
|
||||
Reference in new issue
Block a user