Merge branch 'fix/704-active-intersection-snap'
This commit is contained in:
commit
bd839b9cc0
2 changed files with 323 additions and 4 deletions
|
|
@ -1034,12 +1034,73 @@ impl OpenCADStudio {
|
|||
// snapping must drop its foot from this point, including when a
|
||||
// hot-grip set is moved by the same drag vector.
|
||||
self.snapper.from_point = Some(grip.origin_world.as_vec3());
|
||||
self.snapper.from_point = Some(grip.origin_world.as_vec3());
|
||||
|
||||
// Keep the updated drafting basis from main so grid/snap follows the
|
||||
// current drafting axes, including isometric drafting and SNAPANG.
|
||||
let (go, gr) = self.drafting_grid_basis(i);
|
||||
|
||||
let base = grip.origin_world;
|
||||
let ucs_xf = self.tabs[i].ucs_xform();
|
||||
|
||||
let mut construction_cursor = if self.ortho_mode {
|
||||
Some(drafting_constrain(
|
||||
raw,
|
||||
base,
|
||||
&ucs_xf,
|
||||
self.isometric_drafting,
|
||||
self.iso_plane,
|
||||
self.snap_angle_deg,
|
||||
))
|
||||
} else if self.polar_mode {
|
||||
let pt = polar_constrain_near(
|
||||
raw,
|
||||
base,
|
||||
self.polar_increment_deg,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
self.snapper.osnap_radius_px,
|
||||
&ucs_xf,
|
||||
);
|
||||
|
||||
// POLAR being enabled is not enough: the cursor must actually
|
||||
// be engaged on one of the polar tracking rays.
|
||||
let delta = pt - base;
|
||||
let step = (self.polar_increment_deg as f64).to_radians();
|
||||
let angle = delta.y.atan2(delta.x);
|
||||
|
||||
let polar_engaged = step > 1e-9
|
||||
&& (((angle / step).round() * step - angle).abs() < 1e-3)
|
||||
&& (delta.x * delta.x + delta.y * delta.y).sqrt() > 1e-9;
|
||||
|
||||
if polar_engaged {
|
||||
Some(pt)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(pt) = construction_cursor.as_mut() {
|
||||
if self.tabs[i].active_ucs.is_none() {
|
||||
pt.z = base.z;
|
||||
}
|
||||
}
|
||||
// `raw` is already model space (viewport camera or paper→model),
|
||||
// and the wires are model space, so the snap result is model.
|
||||
let snap_hit =
|
||||
self.snapper
|
||||
.snap(raw, p, &snap_candidates, view_rot, eye, bounds, go, gr);
|
||||
let snap_hit = self.snapper.snap(
|
||||
raw,
|
||||
p,
|
||||
&snap_candidates,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
go,
|
||||
gr,
|
||||
construction_cursor,
|
||||
);
|
||||
|
||||
self.tabs[i].snap_result = snap_hit;
|
||||
|
||||
|
|
@ -1458,6 +1519,64 @@ impl OpenCADStudio {
|
|||
// base only matters for typed-input precision, so hand it
|
||||
// the downcast point here.
|
||||
self.snapper.from_point = self.last_point.map(|p| p.as_vec3());
|
||||
|
||||
// Build the temporary construction direction before OSNAP.
|
||||
// Intersection can then test the active rubber-band against
|
||||
// existing geometry (#704).
|
||||
let construction_cursor = if is_window_corner {
|
||||
None
|
||||
} else {
|
||||
self.last_point.and_then(|base| {
|
||||
let ucs_xf = self.tabs[i].ucs_xform();
|
||||
|
||||
let mut pt = if self.ortho_mode {
|
||||
Some(drafting_constrain(
|
||||
snap_cursor,
|
||||
base,
|
||||
&ucs_xf,
|
||||
self.isometric_drafting,
|
||||
self.iso_plane,
|
||||
self.snap_angle_deg,
|
||||
))
|
||||
} else if self.polar_mode {
|
||||
let pt = polar_constrain_near(
|
||||
snap_cursor,
|
||||
base,
|
||||
self.polar_increment_deg,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
self.snapper.osnap_radius_px,
|
||||
&ucs_xf,
|
||||
);
|
||||
|
||||
// POLAR being enabled is not enough: the cursor must actually
|
||||
// be engaged on one of the polar tracking rays.
|
||||
let delta = pt - base;
|
||||
let step = (self.polar_increment_deg as f64).to_radians();
|
||||
let angle = delta.y.atan2(delta.x);
|
||||
|
||||
let polar_engaged = step > 1e-9
|
||||
&& (((angle / step).round() * step - angle).abs() < 1e-3)
|
||||
&& (delta.x * delta.x + delta.y * delta.y).sqrt() > 1e-9;
|
||||
|
||||
if polar_engaged {
|
||||
Some(pt)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}?;
|
||||
|
||||
if self.tabs[i].active_ucs.is_none() {
|
||||
pt.z = base.z;
|
||||
}
|
||||
|
||||
Some(pt)
|
||||
})
|
||||
};
|
||||
|
||||
self.snapper.snap(
|
||||
snap_cursor,
|
||||
p,
|
||||
|
|
@ -1467,6 +1586,7 @@ impl OpenCADStudio {
|
|||
bounds,
|
||||
go,
|
||||
gr,
|
||||
construction_cursor,
|
||||
)
|
||||
};
|
||||
|
||||
|
|
@ -2121,7 +2241,7 @@ impl OpenCADStudio {
|
|||
let (go, gr) = self.drafting_grid_basis(i);
|
||||
let snap_hit = self
|
||||
.snapper
|
||||
.snap(raw, p, &snap_candidates, view_rot, eye, bounds, go, gr);
|
||||
.snap(raw, p, &snap_candidates, view_rot, eye, bounds, go, gr, None);
|
||||
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() {
|
||||
|
|
@ -2783,6 +2903,61 @@ impl OpenCADStudio {
|
|||
} else {
|
||||
let (go, gr) = self.drafting_grid_basis(i);
|
||||
self.snapper.from_point = self.last_point.map(|p| p.as_vec3());
|
||||
|
||||
let construction_cursor = if is_window_corner {
|
||||
None
|
||||
} else {
|
||||
self.last_point.and_then(|base| {
|
||||
let ucs_xf = self.tabs[i].ucs_xform();
|
||||
|
||||
let mut pt = if self.ortho_mode {
|
||||
Some(drafting_constrain(
|
||||
snap_cursor,
|
||||
base,
|
||||
&ucs_xf,
|
||||
self.isometric_drafting,
|
||||
self.iso_plane,
|
||||
self.snap_angle_deg,
|
||||
))
|
||||
} else if self.polar_mode {
|
||||
let pt = polar_constrain_near(
|
||||
snap_cursor,
|
||||
base,
|
||||
self.polar_increment_deg,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
self.snapper.osnap_radius_px,
|
||||
&ucs_xf,
|
||||
);
|
||||
|
||||
// POLAR being enabled is not enough: the cursor must actually
|
||||
// be engaged on one of the polar tracking rays.
|
||||
let delta = pt - base;
|
||||
let step = (self.polar_increment_deg as f64).to_radians();
|
||||
let angle = delta.y.atan2(delta.x);
|
||||
|
||||
let polar_engaged = step > 1e-9
|
||||
&& (((angle / step).round() * step - angle).abs() < 1e-3)
|
||||
&& (delta.x * delta.x + delta.y * delta.y).sqrt() > 1e-9;
|
||||
|
||||
if polar_engaged {
|
||||
Some(pt)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}?;
|
||||
|
||||
if self.tabs[i].active_ucs.is_none() {
|
||||
pt.z = base.z;
|
||||
}
|
||||
|
||||
Some(pt)
|
||||
})
|
||||
};
|
||||
|
||||
self.snapper.snap(
|
||||
snap_cursor,
|
||||
p,
|
||||
|
|
@ -2792,6 +2967,7 @@ impl OpenCADStudio {
|
|||
bounds,
|
||||
go,
|
||||
gr,
|
||||
construction_cursor,
|
||||
)
|
||||
};
|
||||
// Snap runs in model space; the result is already model.
|
||||
|
|
|
|||
143
src/snap.rs
143
src/snap.rs
|
|
@ -831,6 +831,7 @@ impl Snapper {
|
|||
bounds,
|
||||
Vec3::ZERO,
|
||||
(Vec3::X, Vec3::Y, Vec3::Z),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -847,6 +848,7 @@ impl Snapper {
|
|||
// rotated or isometric grid the user sees.
|
||||
grid_origin: Vec3,
|
||||
grid_axes: (Vec3, Vec3, Vec3),
|
||||
construction_cursor: Option<glam::DVec3>,
|
||||
) -> Option<SnapResult> {
|
||||
// Object-snap selection is priority-then-distance, NOT nearest-wins.
|
||||
// "Continuous" snaps (Nearest, Perpendicular, …) sit on the geometry
|
||||
|
|
@ -1198,6 +1200,97 @@ impl Snapper {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Intersection with active construction segment (#704) ──────────
|
||||
//
|
||||
// The existing Intersection pass below only compares document
|
||||
// entities with each other. Also compare the rubber-band currently
|
||||
// being drawn against existing geometry.
|
||||
if self.is_on(SnapType::Intersection) {
|
||||
if let (Some(from), Some(to)) = (self.from_point, construction_cursor) {
|
||||
let from = from.as_dvec3();
|
||||
if (to - from).length_squared() > 1e-18 {
|
||||
// Active/deferred Intersection uses the actual nearby wires directly.
|
||||
// The interaction candidate set may contain wires while its optional
|
||||
// segment index is empty.
|
||||
for wire in wires.iter() {
|
||||
if !wire_in_range(wire) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for index in 0..wire.points.len().saturating_sub(1) {
|
||||
let b0 = wp_f64(wire, index);
|
||||
let b1 = wp_f64(wire, index + 1);
|
||||
|
||||
if let Some(pt) = ray_segment_intersect_3d(from, to, b0, b1) {
|
||||
if (pt - from).length_squared() > 1e-18 {
|
||||
try_pt(pt, SnapType::Intersection);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// ── Intersection with active perpendicular OTRACK ray (#704) ──────
|
||||
//
|
||||
// A perpendicular tracking ray is not part of the command's ordinary
|
||||
// ORTHO/POLAR construction cursor. When the cursor is actually tracking
|
||||
// one of those rays, allow Intersection to stop it on nearby geometry.
|
||||
if self.is_on(SnapType::Intersection)
|
||||
&& self.otrack_enabled
|
||||
&& self.is_on(SnapType::Perpendicular)
|
||||
{
|
||||
for (gi, &origin) in self.tracking_points.iter().enumerate() {
|
||||
let Some(perp_dirs) = self.tracking_perp_dirs.get(gi) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for &dir in perp_dirs {
|
||||
// Project the cursor onto this perpendicular tracking line.
|
||||
let rel = cursor_world - origin;
|
||||
let t = rel.x * dir.x + rel.y * dir.y;
|
||||
|
||||
let aligned = glam::DVec3::new(
|
||||
origin.x + dir.x * t,
|
||||
origin.y + dir.y * t,
|
||||
origin.z,
|
||||
);
|
||||
|
||||
// Only consider the ray when the cursor is genuinely tracking it.
|
||||
// This prevents Intersection from behaving like Nearest.
|
||||
let aligned_screen = world_to_screen(aligned, view_rot, eye, bounds);
|
||||
if dist2(aligned_screen, cursor_screen) > radius2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Perpendicular tracking is an infinite line, but once the cursor
|
||||
// chooses a side we only want intersections forward on that side.
|
||||
let dir_to_cursor = if t >= 0.0 { dir } else { -dir };
|
||||
let through = origin + dir_to_cursor;
|
||||
|
||||
for wire in wires.iter() {
|
||||
if !wire_in_range(wire) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for index in 0..wire.points.len().saturating_sub(1) {
|
||||
let b0 = wp_f64(wire, index);
|
||||
let b1 = wp_f64(wire, index + 1);
|
||||
|
||||
if let Some(pt) =
|
||||
ray_segment_intersect_3d(origin, through, b0, b1)
|
||||
{
|
||||
// Do not re-offer the tracking origin itself.
|
||||
if (pt - origin).length_squared() > 1e-18 {
|
||||
try_pt(pt, SnapType::Intersection);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Intersection — segment-segment intersections (pairwise, gated) ──
|
||||
if self.is_on(SnapType::Intersection)
|
||||
&& (local_segments.is_some() || allow_unindexed_pairwise)
|
||||
|
|
@ -1993,6 +2086,56 @@ fn perp_foot(query: glam::DVec3, p0: glam::DVec3, p1: glam::DVec3) -> Option<gla
|
|||
))
|
||||
}
|
||||
|
||||
/// Intersection between a forward construction ray and an existing segment.
|
||||
///
|
||||
/// The ray starts at `ray_origin` and points toward `ray_through`, but extends
|
||||
/// indefinitely forward. The existing geometry remains a finite segment.
|
||||
///
|
||||
/// Like `seg_intersect_3d`, this is a true 3D intersection: the XY projections
|
||||
/// must cross and both geometries must have the same Z at the crossing.
|
||||
fn ray_segment_intersect_3d(
|
||||
ray_origin: glam::DVec3,
|
||||
ray_through: glam::DVec3,
|
||||
b0: glam::DVec3,
|
||||
b1: glam::DVec3,
|
||||
) -> Option<glam::DVec3> {
|
||||
let d1x = ray_through.x - ray_origin.x;
|
||||
let d1y = ray_through.y - ray_origin.y;
|
||||
let d2x = b1.x - b0.x;
|
||||
let d2y = b1.y - b0.y;
|
||||
|
||||
let cross = d1x * d2y - d1y * d2x;
|
||||
if cross.abs() < 1e-9 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let ex = b0.x - ray_origin.x;
|
||||
let ey = b0.y - ray_origin.y;
|
||||
|
||||
let t = (ex * d2y - ey * d2x) / cross;
|
||||
let s = (ex * d1y - ey * d1x) / cross;
|
||||
|
||||
// `t >= 0`: only forward along the construction ray.
|
||||
// `s` must remain inside the existing finite segment.
|
||||
if t < 0.0 || s < 0.0 || s > 1.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let za = ray_origin.z + t * (ray_through.z - ray_origin.z);
|
||||
let zb = b0.z + s * (b1.z - b0.z);
|
||||
let tol = 1e-6_f64.max(1e-9 * za.abs().max(zb.abs()));
|
||||
|
||||
if (za - zb).abs() > tol {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(glam::DVec3::new(
|
||||
ray_origin.x + t * d1x,
|
||||
ray_origin.y + t * d1y,
|
||||
0.5 * (za + zb),
|
||||
))
|
||||
}
|
||||
|
||||
/// XY-plane segment-segment intersection. Returns `None` if parallel or outside.
|
||||
/// True 3D intersection of two segments: the point where their plan (XY)
|
||||
/// projections cross **and** both segments are at the same height there. Returns
|
||||
|
|
|
|||
Loading…
Reference in a new issue