fix(snap): correct active intersection snapping
This commit is contained in:
parent
6a39b82a68
commit
f57c258ada
3 changed files with 118 additions and 232 deletions
|
|
@ -339,10 +339,8 @@ pub(super) fn polar_constrain(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Polar constraint that only engages when the cursor is within `tol_px`
|
/// Return the polar-constrained point while its ray is engaged.
|
||||||
/// screen pixels of the nearest polar ray; otherwise the cursor is left free
|
pub(super) fn polar_constrain_if_near(
|
||||||
/// so POLAR behaves as if off when pointing away from every angle (issue #70).
|
|
||||||
pub(super) fn polar_constrain_near(
|
|
||||||
pt: glam::DVec3,
|
pt: glam::DVec3,
|
||||||
base: glam::DVec3,
|
base: glam::DVec3,
|
||||||
step_deg: f32,
|
step_deg: f32,
|
||||||
|
|
@ -351,7 +349,7 @@ pub(super) fn polar_constrain_near(
|
||||||
bounds: iced::Rectangle,
|
bounds: iced::Rectangle,
|
||||||
tol_px: f32,
|
tol_px: f32,
|
||||||
xf: &UcsXform,
|
xf: &UcsXform,
|
||||||
) -> glam::DVec3 {
|
) -> Option<glam::DVec3> {
|
||||||
let snapped = polar_constrain(pt, base, step_deg, xf);
|
let snapped = polar_constrain(pt, base, step_deg, xf);
|
||||||
let to_screen = |w: glam::DVec3| {
|
let to_screen = |w: glam::DVec3| {
|
||||||
let ndc = view_rot.project_point3((w - eye).as_vec3());
|
let ndc = view_rot.project_point3((w - eye).as_vec3());
|
||||||
|
|
@ -362,11 +360,22 @@ pub(super) fn polar_constrain_near(
|
||||||
};
|
};
|
||||||
let (cx, cy) = to_screen(pt);
|
let (cx, cy) = to_screen(pt);
|
||||||
let (sx, sy) = to_screen(snapped);
|
let (sx, sy) = to_screen(snapped);
|
||||||
if ((cx - sx).powi(2) + (cy - sy).powi(2)).sqrt() <= tol_px {
|
(((cx - sx).powi(2) + (cy - sy).powi(2)).sqrt() <= tol_px).then_some(snapped)
|
||||||
snapped
|
}
|
||||||
} else {
|
|
||||||
pt
|
/// Constrain near an engaged polar ray; otherwise keep the cursor free.
|
||||||
}
|
pub(super) fn polar_constrain_near(
|
||||||
|
pt: glam::DVec3,
|
||||||
|
base: glam::DVec3,
|
||||||
|
step_deg: f32,
|
||||||
|
view_rot: glam::Mat4,
|
||||||
|
eye: glam::DVec3,
|
||||||
|
bounds: iced::Rectangle,
|
||||||
|
tol_px: f32,
|
||||||
|
xf: &UcsXform,
|
||||||
|
) -> glam::DVec3 {
|
||||||
|
polar_constrain_if_near(pt, base, step_deg, view_rot, eye, bounds, tol_px, xf)
|
||||||
|
.unwrap_or(pt)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hard axis lock (#312): the locked ray's direction — the nearest polar
|
/// Hard axis lock (#312): the locked ray's direction — the nearest polar
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@
|
||||||
use super::util::*;
|
use super::util::*;
|
||||||
use super::{format_size, VIEWCUBE_HIT_SIZE};
|
use super::{format_size, VIEWCUBE_HIT_SIZE};
|
||||||
use crate::app::helpers::{
|
use crate::app::helpers::{
|
||||||
axis_lock_apply, axis_lock_capture, drafting_axes, drafting_constrain, parse_coord, polar_constrain_near,
|
axis_lock_apply, axis_lock_capture, drafting_axes, drafting_constrain, parse_coord,
|
||||||
ucs_rotate_vec, ucs_to_wcs, ucs_z_axis, CoordKind,
|
polar_constrain_if_near, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis,
|
||||||
|
CoordKind,
|
||||||
};
|
};
|
||||||
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
||||||
use crate::modules::ModuleEvent;
|
use crate::modules::ModuleEvent;
|
||||||
|
|
@ -117,6 +118,45 @@ fn over_ucs_icon(p: Point, h: &crate::ui::overlay::UcsIconHit) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
|
fn active_construction_ray(
|
||||||
|
&self,
|
||||||
|
tab: usize,
|
||||||
|
cursor: glam::DVec3,
|
||||||
|
base: glam::DVec3,
|
||||||
|
view_rot: glam::Mat4,
|
||||||
|
eye: glam::DVec3,
|
||||||
|
bounds: iced::Rectangle,
|
||||||
|
) -> Option<(glam::DVec3, glam::DVec3)> {
|
||||||
|
let ucs = self.tabs[tab].ucs_xform();
|
||||||
|
let mut target = if self.ortho_mode {
|
||||||
|
drafting_constrain(
|
||||||
|
cursor,
|
||||||
|
base,
|
||||||
|
&ucs,
|
||||||
|
self.isometric_drafting,
|
||||||
|
self.iso_plane,
|
||||||
|
self.snap_angle_deg,
|
||||||
|
)
|
||||||
|
} else if self.polar_mode {
|
||||||
|
polar_constrain_if_near(
|
||||||
|
cursor,
|
||||||
|
base,
|
||||||
|
self.polar_increment_deg,
|
||||||
|
view_rot,
|
||||||
|
eye,
|
||||||
|
bounds,
|
||||||
|
self.snapper.osnap_radius_px,
|
||||||
|
&ucs,
|
||||||
|
)?
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
if self.tabs[tab].active_ucs.is_none() {
|
||||||
|
target.z = base.z;
|
||||||
|
}
|
||||||
|
Some((base, target))
|
||||||
|
}
|
||||||
|
|
||||||
/// Write PDSIZE from the dialog buffer with the current relative/absolute
|
/// Write PDSIZE from the dialog buffer with the current relative/absolute
|
||||||
/// sign. A relative size is stored negative; absolute positive. Switching to
|
/// sign. A relative size is stored negative; absolute positive. Switching to
|
||||||
/// absolute with an empty/zero size seeds a positive value from the current
|
/// absolute with an empty/zero size seeds a positive value from the current
|
||||||
|
|
@ -1034,60 +1074,10 @@ impl OpenCADStudio {
|
||||||
// snapping must drop its foot from this point, including when a
|
// snapping must drop its foot from this point, including when a
|
||||||
// hot-grip set is moved by the same drag vector.
|
// 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());
|
||||||
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 (go, gr) = self.drafting_grid_basis(i);
|
||||||
|
|
||||||
let base = grip.origin_world;
|
let base = grip.origin_world;
|
||||||
let ucs_xf = self.tabs[i].ucs_xform();
|
let construction_ray =
|
||||||
|
self.active_construction_ray(i, raw, base, view_rot, eye, bounds);
|
||||||
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),
|
// `raw` is already model space (viewport camera or paper→model),
|
||||||
// and the wires are model space, so the snap result is model.
|
// and the wires are model space, so the snap result is model.
|
||||||
let snap_hit = self.snapper.snap(
|
let snap_hit = self.snapper.snap(
|
||||||
|
|
@ -1099,7 +1089,7 @@ impl OpenCADStudio {
|
||||||
bounds,
|
bounds,
|
||||||
go,
|
go,
|
||||||
gr,
|
gr,
|
||||||
construction_cursor,
|
construction_ray,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.tabs[i].snap_result = snap_hit;
|
self.tabs[i].snap_result = snap_hit;
|
||||||
|
|
@ -1521,60 +1511,18 @@ impl OpenCADStudio {
|
||||||
// the downcast point here.
|
// the downcast point here.
|
||||||
self.snapper.from_point = self.last_point.map(|p| p.as_vec3());
|
self.snapper.from_point = self.last_point.map(|p| p.as_vec3());
|
||||||
|
|
||||||
// Build the temporary construction direction before OSNAP.
|
let construction_ray = if is_window_corner {
|
||||||
// Intersection can then test the active rubber-band against
|
|
||||||
// existing geometry (#704).
|
|
||||||
let construction_cursor = if is_window_corner {
|
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
self.last_point.and_then(|base| {
|
self.last_point.and_then(|base| {
|
||||||
let ucs_xf = self.tabs[i].ucs_xform();
|
self.active_construction_ray(
|
||||||
|
i,
|
||||||
let mut pt = if self.ortho_mode {
|
snap_cursor,
|
||||||
Some(drafting_constrain(
|
base,
|
||||||
snap_cursor,
|
view_rot,
|
||||||
base,
|
eye,
|
||||||
&ucs_xf,
|
bounds,
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1587,7 +1535,7 @@ impl OpenCADStudio {
|
||||||
bounds,
|
bounds,
|
||||||
go,
|
go,
|
||||||
gr,
|
gr,
|
||||||
construction_cursor,
|
construction_ray,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -2906,57 +2854,18 @@ impl OpenCADStudio {
|
||||||
let (go, gr) = self.drafting_grid_basis(i);
|
let (go, gr) = self.drafting_grid_basis(i);
|
||||||
self.snapper.from_point = self.last_point.map(|p| p.as_vec3());
|
self.snapper.from_point = self.last_point.map(|p| p.as_vec3());
|
||||||
|
|
||||||
let construction_cursor = if is_window_corner {
|
let construction_ray = if is_window_corner {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
self.last_point.and_then(|base| {
|
self.last_point.and_then(|base| {
|
||||||
let ucs_xf = self.tabs[i].ucs_xform();
|
self.active_construction_ray(
|
||||||
|
i,
|
||||||
let mut pt = if self.ortho_mode {
|
|
||||||
Some(drafting_constrain(
|
|
||||||
snap_cursor,
|
snap_cursor,
|
||||||
base,
|
base,
|
||||||
&ucs_xf,
|
view_rot,
|
||||||
self.isometric_drafting,
|
eye,
|
||||||
self.iso_plane,
|
bounds,
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -2969,7 +2878,7 @@ impl OpenCADStudio {
|
||||||
bounds,
|
bounds,
|
||||||
go,
|
go,
|
||||||
gr,
|
gr,
|
||||||
construction_cursor,
|
construction_ray,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
// Snap runs in model space; the result is already model.
|
// Snap runs in model space; the result is already model.
|
||||||
|
|
|
||||||
104
src/snap.rs
104
src/snap.rs
|
|
@ -865,7 +865,7 @@ impl Snapper {
|
||||||
// rotated or isometric grid the user sees.
|
// rotated or isometric grid the user sees.
|
||||||
grid_origin: Vec3,
|
grid_origin: Vec3,
|
||||||
grid_axes: (Vec3, Vec3, Vec3),
|
grid_axes: (Vec3, Vec3, Vec3),
|
||||||
construction_cursor: Option<glam::DVec3>,
|
construction_ray: Option<(glam::DVec3, glam::DVec3)>,
|
||||||
) -> Option<SnapResult> {
|
) -> Option<SnapResult> {
|
||||||
// Object-snap selection is priority-then-distance, NOT nearest-wins.
|
// Object-snap selection is priority-then-distance, NOT nearest-wins.
|
||||||
// "Continuous" snaps (Nearest, Perpendicular, …) sit on the geometry
|
// "Continuous" snaps (Nearest, Perpendicular, …) sit on the geometry
|
||||||
|
|
@ -1217,42 +1217,45 @@ impl Snapper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Intersection with active construction segment (#704) ──────────
|
let mut try_ray_intersections = |origin: glam::DVec3, through: glam::DVec3| {
|
||||||
//
|
if (through - origin).length_squared() <= 1e-18 {
|
||||||
// The existing Intersection pass below only compares document
|
return;
|
||||||
// entities with each other. Also compare the rubber-band currently
|
}
|
||||||
// being drawn against existing geometry.
|
if let Some(segments) = &local_segments {
|
||||||
if self.is_on(SnapType::Intersection) {
|
for segment in segments {
|
||||||
if let (Some(from), Some(to)) = (self.from_point, construction_cursor) {
|
if let Some(point) =
|
||||||
let from = from.as_dvec3();
|
ray_segment_intersect_3d(origin, through, segment.a, segment.b)
|
||||||
if (to - from).length_squared() > 1e-18 {
|
{
|
||||||
// Active/deferred Intersection uses the actual nearby wires directly.
|
if (point - origin).length_squared() > 1e-18 {
|
||||||
// The interaction candidate set may contain wires while its optional
|
try_pt(point, SnapType::Intersection);
|
||||||
// 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);
|
} else {
|
||||||
let b1 = wp_f64(wire, index + 1);
|
for wire in wires.iter().filter(|wire| wire_in_range(wire)) {
|
||||||
|
for index in 0..wire.points.len().saturating_sub(1) {
|
||||||
if let Some(pt) = ray_segment_intersect_3d(from, to, b0, b1) {
|
if let Some(point) = ray_segment_intersect_3d(
|
||||||
if (pt - from).length_squared() > 1e-18 {
|
origin,
|
||||||
try_pt(pt, SnapType::Intersection);
|
through,
|
||||||
}
|
wp_f64(wire, index),
|
||||||
|
wp_f64(wire, index + 1),
|
||||||
|
) {
|
||||||
|
if (point - origin).length_squared() > 1e-18 {
|
||||||
|
try_pt(point, SnapType::Intersection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.is_on(SnapType::Intersection) {
|
||||||
|
if let Some((origin, through)) = construction_ray {
|
||||||
|
try_ray_intersections(origin, through);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// ── Intersection with active perpendicular OTRACK ray (#704) ──────
|
|
||||||
//
|
// Check engaged perpendicular tracking rays against nearby geometry.
|
||||||
// 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)
|
if self.is_on(SnapType::Intersection)
|
||||||
&& self.otrack_enabled
|
&& self.otrack_enabled
|
||||||
&& self.is_on(SnapType::Perpendicular)
|
&& self.is_on(SnapType::Perpendicular)
|
||||||
|
|
@ -1263,50 +1266,23 @@ impl Snapper {
|
||||||
};
|
};
|
||||||
|
|
||||||
for &dir in perp_dirs {
|
for &dir in perp_dirs {
|
||||||
// Project the cursor onto this perpendicular tracking line.
|
|
||||||
let rel = cursor_world - origin;
|
let rel = cursor_world - origin;
|
||||||
let t = rel.x * dir.x + rel.y * dir.y;
|
let t = rel.x * dir.x + rel.y * dir.y;
|
||||||
|
|
||||||
let aligned = glam::DVec3::new(
|
let aligned = glam::DVec3::new(
|
||||||
origin.x + dir.x * t,
|
origin.x + dir.x * t,
|
||||||
origin.y + dir.y * t,
|
origin.y + dir.y * t,
|
||||||
origin.z,
|
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);
|
let aligned_screen = world_to_screen(aligned, view_rot, eye, bounds);
|
||||||
if dist2(aligned_screen, cursor_screen) > radius2 {
|
if dist2(aligned_screen, cursor_screen) > radius2 {
|
||||||
continue;
|
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 dir_to_cursor = if t >= 0.0 { dir } else { -dir };
|
||||||
let through = origin + dir_to_cursor;
|
try_ray_intersections(origin, 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
drop(try_ray_intersections);
|
||||||
|
|
||||||
// ── Intersection — segment-segment intersections (pairwise, gated) ──
|
// ── Intersection — segment-segment intersections (pairwise, gated) ──
|
||||||
if self.is_on(SnapType::Intersection)
|
if self.is_on(SnapType::Intersection)
|
||||||
|
|
@ -2103,13 +2079,7 @@ fn perp_foot(query: glam::DVec3, p0: glam::DVec3, p1: glam::DVec3) -> Option<gla
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Intersection between a forward construction ray and an existing segment.
|
/// True 3D intersection of a forward ray and a finite 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(
|
fn ray_segment_intersect_3d(
|
||||||
ray_origin: glam::DVec3,
|
ray_origin: glam::DVec3,
|
||||||
ray_through: glam::DVec3,
|
ray_through: glam::DVec3,
|
||||||
|
|
@ -2132,8 +2102,6 @@ fn ray_segment_intersect_3d(
|
||||||
let t = (ex * d2y - ey * d2x) / cross;
|
let t = (ex * d2y - ey * d2x) / cross;
|
||||||
let s = (ex * d1y - ey * d1x) / 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 {
|
if t < 0.0 || s < 0.0 || s > 1.0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue