fix(snap): f64-precise computed snaps + UTM-tolerant cull + f64 previews
Computed snaps (midpoint/nearest/perpendicular/intersection/extension/ apparent/grid) reconstructed the snap point from f32 `wire.points` (absolute high half), so at UTM-scale they landed ~0.5 m off the geometry while endpoint/center (sourced from f64 key_vertices/snap_pts) stayed correct. Now they rebuild each vertex from points + points_low (the double-single low residual) and run the geometry in f64: - snap() cursor + geometry helpers (nearest_on_segment, perp_foot, seg_intersect_xy, extension_snap) are DVec3; wp_f64() reconstructs each vertex; midpoint uses f64 key_vertices directly (was downcast to f32). - snap callers pass the precise DVec3 cursor. Snap capture at deep zoom: the wire-AABB cull compared a sub-millimetre snap radius against f32 bounds quantized by ~1 ulp (~0.7 m at 5.7e6), wrongly excluding wires under the cursor. Pad the cull by the bound's own f32 quantization so it never rejects an in-range wire. Rubber-band previews (line/circle/arc/ellipse/rect/polygon) built f32 absolute points with an empty points_low, so the wire pass (relative-to- eye) drew them ~0.5 m off the cursor at UTM. Add WireModel::solid_f64, which splits f64 points into the double-single high/low pair; previews now track the cursor exactly. Known: snapping at extreme zoom on large drawings spikes CPU (per-move snap cost) — deferred. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
4f7a98a2ad
commit
bca8958025
8 changed files with 166 additions and 158 deletions
|
|
@ -2648,7 +2648,7 @@ impl OpenCADStudio {
|
||||||
self.snapper.from_point = None;
|
self.snapper.from_point = None;
|
||||||
let (go, gr) = self.tabs[i].ucs_grid_basis();
|
let (go, gr) = self.tabs[i].ucs_grid_basis();
|
||||||
let snap_hit =
|
let snap_hit =
|
||||||
self.snapper.snap(raw.as_vec3(), p, &all_wires[..], view_rot, eye, bounds, go, gr);
|
self.snapper.snap(raw, p, &all_wires[..], view_rot, eye, bounds, go, gr);
|
||||||
let mut snapped = snap_hit.map(|s| s.world).unwrap_or(raw);
|
let mut snapped = snap_hit.map(|s| s.world).unwrap_or(raw);
|
||||||
self.tabs[i].snap_result = snap_hit;
|
self.tabs[i].snap_result = snap_hit;
|
||||||
if let Some(s) = self.tabs[i].snap_result.as_mut() {
|
if let Some(s) = self.tabs[i].snap_result.as_mut() {
|
||||||
|
|
@ -2827,7 +2827,7 @@ impl OpenCADStudio {
|
||||||
let (go, gr) = self.tabs[i].ucs_grid_basis();
|
let (go, gr) = self.tabs[i].ucs_grid_basis();
|
||||||
self.snapper.from_point = self.last_point;
|
self.snapper.from_point = self.last_point;
|
||||||
self.snapper
|
self.snapper
|
||||||
.snap(cursor_world.as_vec3(), p, &all_wires[..], view_rot, eye, bounds, go, gr)
|
.snap(cursor_world, p, &all_wires[..], view_rot, eye, bounds, go, gr)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Object Snap Tracking: update dwell, then align the cursor
|
// Object Snap Tracking: update dwell, then align the cursor
|
||||||
|
|
@ -3464,7 +3464,7 @@ impl OpenCADStudio {
|
||||||
} else {
|
} else {
|
||||||
let (go, gr) = self.tabs[i].ucs_grid_basis();
|
let (go, gr) = self.tabs[i].ucs_grid_basis();
|
||||||
self.snapper.from_point = self.last_point;
|
self.snapper.from_point = self.last_point;
|
||||||
self.snapper.snap(raw.as_vec3(), p, &all_wires[..], view_rot, eye, bounds, go, gr)
|
self.snapper.snap(raw, p, &all_wires[..], view_rot, eye, bounds, go, gr)
|
||||||
};
|
};
|
||||||
// snap.world is in paper-space (projected wire coords in MSPACE);
|
// snap.world is in paper-space (projected wire coords in MSPACE);
|
||||||
// convert to model-space so commands receive consistent coordinates.
|
// convert to model-space so commands receive consistent coordinates.
|
||||||
|
|
|
||||||
|
|
@ -79,17 +79,17 @@ fn arc_preview(center: DVec3, radius: f64, start_angle: f64, end_angle: f64) ->
|
||||||
}
|
}
|
||||||
let span = (ea - start_angle).min(TAU);
|
let span = (ea - start_angle).min(TAU);
|
||||||
let segs = ((span / TAU) * 64.0).ceil().max(4.0) as u32;
|
let segs = ((span / TAU) * 64.0).ceil().max(4.0) as u32;
|
||||||
let pts: Vec<[f32; 3]> = (0..=segs)
|
let pts: Vec<[f64; 3]> = (0..=segs)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
let a = start_angle + span * (i as f64 / segs as f64);
|
let a = start_angle + span * (i as f64 / segs as f64);
|
||||||
[
|
[
|
||||||
(center.x + radius * a.cos()) as f32,
|
center.x + radius * a.cos(),
|
||||||
(center.y + radius * a.sin()) as f32,
|
center.y + radius * a.sin(),
|
||||||
center.z as f32,
|
center.z,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
WireModel::solid("rubber_band".into(), pts, WireModel::CYAN, false)
|
WireModel::solid_f64("rubber_band".into(), pts, WireModel::CYAN, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_arc(center: DVec3, radius: f64, start_angle: f64, end_angle: f64) -> EntityType {
|
fn make_arc(center: DVec3, radius: f64, start_angle: f64, end_angle: f64) -> EntityType {
|
||||||
|
|
@ -116,12 +116,9 @@ fn rot_delta(center: DVec3, prev: DVec3, curr: DVec3) -> f64 {
|
||||||
const DIR_TOL: f64 = 0.1745; // ~10°
|
const DIR_TOL: f64 = 0.1745; // ~10°
|
||||||
|
|
||||||
fn line_wire(a: DVec3, b: DVec3) -> WireModel {
|
fn line_wire(a: DVec3, b: DVec3) -> WireModel {
|
||||||
WireModel::solid(
|
WireModel::solid_f64(
|
||||||
"rubber_band".into(),
|
"rubber_band".into(),
|
||||||
vec![
|
vec![[a.x, a.y, a.z], [b.x, b.y, b.z]],
|
||||||
[a.x as f32, a.y as f32, a.z as f32],
|
|
||||||
[b.x as f32, b.y as f32, b.z as f32],
|
|
||||||
],
|
|
||||||
WireModel::CYAN,
|
WireModel::CYAN,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
@ -386,13 +383,9 @@ impl CadCommand for Arc3PCommand {
|
||||||
};
|
};
|
||||||
Some(arc_preview(center, radius, sa, ea))
|
Some(arc_preview(center, radius, sa, ea))
|
||||||
} else {
|
} else {
|
||||||
Some(WireModel::solid(
|
Some(WireModel::solid_f64(
|
||||||
"rubber_band".into(),
|
"rubber_band".into(),
|
||||||
vec![
|
vec![[p1.x, p1.y, p1.z], [p2.x, p2.y, p2.z], [pt.x, pt.y, pt.z]],
|
||||||
[p1.x as f32, p1.y as f32, p1.z as f32],
|
|
||||||
[p2.x as f32, p2.y as f32, p2.z as f32],
|
|
||||||
[pt.x as f32, pt.y as f32, pt.z as f32],
|
|
||||||
],
|
|
||||||
WireModel::CYAN,
|
WireModel::CYAN,
|
||||||
false,
|
false,
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -60,20 +60,20 @@ pub const ICON: IconKind = ICON_CR;
|
||||||
|
|
||||||
fn circle_wire(center: DVec3, radius: f64) -> WireModel {
|
fn circle_wire(center: DVec3, radius: f64) -> WireModel {
|
||||||
let segs = 64u32;
|
let segs = 64u32;
|
||||||
let mut pts: Vec<[f32; 3]> = (0..=segs)
|
let mut pts: Vec<[f64; 3]> = (0..=segs)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
let a = (i as f64) * TAU / segs as f64;
|
let a = (i as f64) * TAU / segs as f64;
|
||||||
[
|
[
|
||||||
(center.x + radius * a.cos()) as f32,
|
center.x + radius * a.cos(),
|
||||||
(center.y + radius * a.sin()) as f32,
|
center.y + radius * a.sin(),
|
||||||
center.z as f32,
|
center.z,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
if let Some(first) = pts.first().cloned() {
|
if let Some(first) = pts.first().cloned() {
|
||||||
pts.push(first);
|
pts.push(first);
|
||||||
}
|
}
|
||||||
WireModel::solid("rubber_band".into(), pts, WireModel::CYAN, false)
|
WireModel::solid_f64("rubber_band".into(), pts, WireModel::CYAN, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_circle(center: DVec3, radius: f64) -> EntityType {
|
fn make_circle(center: DVec3, radius: f64) -> EntityType {
|
||||||
|
|
@ -404,13 +404,9 @@ impl CadCommand for Circle3PCommand {
|
||||||
if let Some((center, radius)) = circumcircle(a, b, pt) {
|
if let Some((center, radius)) = circumcircle(a, b, pt) {
|
||||||
Some(circle_wire(center, radius))
|
Some(circle_wire(center, radius))
|
||||||
} else {
|
} else {
|
||||||
Some(WireModel::solid(
|
Some(WireModel::solid_f64(
|
||||||
"rubber_band".into(),
|
"rubber_band".into(),
|
||||||
vec![
|
vec![[a.x, a.y, a.z], [b.x, b.y, b.z], [pt.x, pt.y, pt.z]],
|
||||||
[a.x as f32, a.y as f32, a.z as f32],
|
|
||||||
[b.x as f32, b.y as f32, b.z as f32],
|
|
||||||
[pt.x as f32, pt.y as f32, pt.z as f32],
|
|
||||||
],
|
|
||||||
WireModel::CYAN,
|
WireModel::CYAN,
|
||||||
false,
|
false,
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -66,14 +66,14 @@ fn ellipse_wire(
|
||||||
let segs = 64u32;
|
let segs = 64u32;
|
||||||
// Unwrap t_end so the arc goes counter-clockwise.
|
// Unwrap t_end so the arc goes counter-clockwise.
|
||||||
let t_e = if t_end <= t_start { t_end + TAU } else { t_end };
|
let t_e = if t_end <= t_start { t_end + TAU } else { t_end };
|
||||||
let pts: Vec<[f32; 3]> = (0..=segs)
|
let pts: Vec<[f64; 3]> = (0..=segs)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
let t = t_start + (t_e - t_start) * (i as f64 / segs as f64);
|
let t = t_start + (t_e - t_start) * (i as f64 / segs as f64);
|
||||||
let p = center + t.cos() * r_major * major_dir + t.sin() * r_major * ratio * v;
|
let p = center + t.cos() * r_major * major_dir + t.sin() * r_major * ratio * v;
|
||||||
[p.x as f32, p.y as f32, p.z as f32]
|
[p.x, p.y, p.z]
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
WireModel::solid("rubber_band".into(), pts, WireModel::CYAN, false)
|
WireModel::solid_f64("rubber_band".into(), pts, WireModel::CYAN, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a world point to the parametric angle on the ellipse.
|
/// Convert a world point to the parametric angle on the ellipse.
|
||||||
|
|
@ -610,28 +610,12 @@ fn angle_from_point(center: DVec3, major: DVec3, ratio: f64, pt: DVec3) -> f64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn line_wire(from: DVec3, to: DVec3) -> WireModel {
|
fn line_wire(from: DVec3, to: DVec3) -> WireModel {
|
||||||
WireModel {
|
WireModel::solid_f64(
|
||||||
name: "rubber_band".into(),
|
"rubber_band".into(),
|
||||||
points: vec![
|
vec![[from.x, from.y, from.z], [to.x, to.y, to.z]],
|
||||||
[from.x as f32, from.y as f32, from.z as f32],
|
WireModel::CYAN,
|
||||||
[to.x as f32, to.y as f32, to.z as f32],
|
false,
|
||||||
],
|
)
|
||||||
points_low: Vec::new(),
|
|
||||||
color: WireModel::CYAN,
|
|
||||||
selected: false,
|
|
||||||
pattern_length: 0.0,
|
|
||||||
pattern: [0.0; 8],
|
|
||||||
line_weight_px: 1.0,
|
|
||||||
snap_pts: vec![],
|
|
||||||
tangent_geoms: vec![],
|
|
||||||
aci: 0,
|
|
||||||
key_vertices: vec![],
|
|
||||||
aabb: WireModel::UNBOUNDED_AABB,
|
|
||||||
plinegen: true,
|
|
||||||
vp_scissor: None,
|
|
||||||
fill_tris: vec![],
|
|
||||||
fill_tris_low: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -75,28 +75,12 @@ impl CadCommand for LineCommand {
|
||||||
|
|
||||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||||
let last = self.last?;
|
let last = self.last?;
|
||||||
Some(WireModel {
|
Some(WireModel::solid_f64(
|
||||||
name: "rubber_band".to_string(),
|
"rubber_band".to_string(),
|
||||||
points: vec![
|
vec![[last.x, last.y, last.z], [pt.x, pt.y, pt.z]],
|
||||||
[last.x as f32, last.y as f32, last.z as f32],
|
WireModel::CYAN,
|
||||||
[pt.x as f32, pt.y as f32, pt.z as f32],
|
false,
|
||||||
],
|
))
|
||||||
points_low: Vec::new(),
|
|
||||||
color: WireModel::CYAN,
|
|
||||||
selected: false,
|
|
||||||
pattern_length: 0.0,
|
|
||||||
pattern: [0.0; 8],
|
|
||||||
line_weight_px: 1.0,
|
|
||||||
snap_pts: vec![],
|
|
||||||
tangent_geoms: vec![],
|
|
||||||
aci: 0,
|
|
||||||
key_vertices: vec![],
|
|
||||||
aabb: WireModel::UNBOUNDED_AABB,
|
|
||||||
plinegen: true,
|
|
||||||
vp_scissor: None,
|
|
||||||
fill_tris: vec![],
|
|
||||||
fill_tris_low: Vec::new(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,21 +94,18 @@ fn make_pline(xy_pairs: &[[f64; 2]]) -> EntityType {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wire_loop(pts: Vec<[f32; 3]>) -> WireModel {
|
fn wire_loop(pts: Vec<[f64; 3]>) -> WireModel {
|
||||||
let mut p = pts;
|
let mut p = pts;
|
||||||
if let Some(&first) = p.first() {
|
if let Some(&first) = p.first() {
|
||||||
p.push(first);
|
p.push(first);
|
||||||
}
|
}
|
||||||
WireModel::solid("rubber_band".into(), p, WireModel::CYAN, false)
|
WireModel::solid_f64("rubber_band".into(), p, WireModel::CYAN, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wire_seg(a: DVec3, b: DVec3) -> WireModel {
|
fn wire_seg(a: DVec3, b: DVec3) -> WireModel {
|
||||||
WireModel::solid(
|
WireModel::solid_f64(
|
||||||
"rubber_band".into(),
|
"rubber_band".into(),
|
||||||
vec![
|
vec![[a.x, a.y, a.z], [b.x, b.y, b.z]],
|
||||||
[a.x as f32, a.y as f32, a.z as f32],
|
|
||||||
[b.x as f32, b.y as f32, b.z as f32],
|
|
||||||
],
|
|
||||||
WireModel::CYAN,
|
WireModel::CYAN,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
@ -129,13 +126,13 @@ fn poly_verts_xy(center: DVec3, vertex_r: f64, sides: u32, start_angle: f64) ->
|
||||||
}
|
}
|
||||||
|
|
||||||
fn poly_wire(center: DVec3, vertex_r: f64, sides: u32, start_angle: f64) -> WireModel {
|
fn poly_wire(center: DVec3, vertex_r: f64, sides: u32, start_angle: f64) -> WireModel {
|
||||||
let pts: Vec<[f32; 3]> = (0..sides)
|
let pts: Vec<[f64; 3]> = (0..sides)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
let a = start_angle + (i as f64) * TAU / sides as f64;
|
let a = start_angle + (i as f64) * TAU / sides as f64;
|
||||||
[
|
[
|
||||||
(center.x + vertex_r * a.cos()) as f32,
|
center.x + vertex_r * a.cos(),
|
||||||
(center.y + vertex_r * a.sin()) as f32,
|
center.y + vertex_r * a.sin(),
|
||||||
center.z as f32,
|
center.z,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -200,10 +197,10 @@ impl CadCommand for RectCommand {
|
||||||
let a = self.a?;
|
let a = self.a?;
|
||||||
let c = ucs_box_corners(a, pt, self.ucs);
|
let c = ucs_box_corners(a, pt, self.ucs);
|
||||||
Some(wire_loop(vec![
|
Some(wire_loop(vec![
|
||||||
[c[0].x as f32, c[0].y as f32, c[0].z as f32],
|
[c[0].x, c[0].y, c[0].z],
|
||||||
[c[1].x as f32, c[1].y as f32, c[1].z as f32],
|
[c[1].x, c[1].y, c[1].z],
|
||||||
[c[2].x as f32, c[2].y as f32, c[2].z as f32],
|
[c[2].x, c[2].y, c[2].z],
|
||||||
[c[3].x as f32, c[3].y as f32, c[3].z as f32],
|
[c[3].x, c[3].y, c[3].z],
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
|
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
|
||||||
|
|
@ -300,10 +297,10 @@ impl CadCommand for RectRotCommand {
|
||||||
let c = b + perp * h;
|
let c = b + perp * h;
|
||||||
let d = a + perp * h;
|
let d = a + perp * h;
|
||||||
Some(wire_loop(vec![
|
Some(wire_loop(vec![
|
||||||
[a.x as f32, a.y as f32, a.z as f32],
|
[a.x, a.y, a.z],
|
||||||
[b.x as f32, b.y as f32, b.z as f32],
|
[b.x, b.y, b.z],
|
||||||
[c.x as f32, c.y as f32, c.z as f32],
|
[c.x, c.y, c.z],
|
||||||
[d.x as f32, d.y as f32, d.z as f32],
|
[d.x, d.y, d.z],
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
|
|
@ -390,10 +387,10 @@ impl CadCommand for RectCenCommand {
|
||||||
let c = self.center?;
|
let c = self.center?;
|
||||||
let q = ucs_box_around_center(c, pt, self.ucs);
|
let q = ucs_box_around_center(c, pt, self.ucs);
|
||||||
Some(wire_loop(vec![
|
Some(wire_loop(vec![
|
||||||
[q[0].x as f32, q[0].y as f32, q[0].z as f32],
|
[q[0].x, q[0].y, q[0].z],
|
||||||
[q[1].x as f32, q[1].y as f32, q[1].z as f32],
|
[q[1].x, q[1].y, q[1].z],
|
||||||
[q[2].x as f32, q[2].y as f32, q[2].z as f32],
|
[q[2].x, q[2].y, q[2].z],
|
||||||
[q[3].x as f32, q[3].y as f32, q[3].z as f32],
|
[q[3].x, q[3].y, q[3].z],
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
|
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,34 @@ impl WireModel {
|
||||||
f32::INFINITY,
|
f32::INFINITY,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Double-single split: `high + low ≈ v` to ~f64 precision in two f32s.
|
||||||
|
/// Matches the renderer's relative-to-eye reconstruction.
|
||||||
|
#[inline]
|
||||||
|
pub fn split_ds(v: f64) -> (f32, f32) {
|
||||||
|
let high = v as f32;
|
||||||
|
(high, (v - high as f64) as f32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a solid preview wire from f64 points, filling the double-single
|
||||||
|
/// `points_low` buffer so the line stays precise at UTM-scale coordinates.
|
||||||
|
/// Rubber-band previews built straight from f32 absolute points jitter
|
||||||
|
/// ~0.5 m at UTM because the wire pass is relative-to-eye and expects the
|
||||||
|
/// low residual; this keeps the preview glued to the cursor.
|
||||||
|
pub fn solid_f64(name: String, points: Vec<[f64; 3]>, color: [f32; 4], selected: bool) -> Self {
|
||||||
|
let mut hi = Vec::with_capacity(points.len());
|
||||||
|
let mut lo = Vec::with_capacity(points.len());
|
||||||
|
for [x, y, z] in points {
|
||||||
|
let (hx, lx) = Self::split_ds(x);
|
||||||
|
let (hy, ly) = Self::split_ds(y);
|
||||||
|
let (hz, lz) = Self::split_ds(z);
|
||||||
|
hi.push([hx, hy, hz]);
|
||||||
|
lo.push([lx, ly, lz]);
|
||||||
|
}
|
||||||
|
let mut w = Self::solid(name, hi, color, selected);
|
||||||
|
w.points_low = lo;
|
||||||
|
w
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a solid wire (no dash pattern, 1px weight).
|
/// Create a solid wire (no dash pattern, 1px weight).
|
||||||
pub fn solid(name: String, points: Vec<[f32; 3]>, color: [f32; 4], selected: bool) -> Self {
|
pub fn solid(name: String, points: Vec<[f32; 3]>, color: [f32; 4], selected: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
|
||||||
142
src/snap/mod.rs
142
src/snap/mod.rs
|
|
@ -433,7 +433,7 @@ impl Snapper {
|
||||||
};
|
};
|
||||||
// Tangent-only: Grid is disabled here, so the grid basis is irrelevant.
|
// Tangent-only: Grid is disabled here, so the grid basis is irrelevant.
|
||||||
tmp.snap(
|
tmp.snap(
|
||||||
cursor_world,
|
cursor_world.as_dvec3(),
|
||||||
cursor_screen,
|
cursor_screen,
|
||||||
wires,
|
wires,
|
||||||
view_rot,
|
view_rot,
|
||||||
|
|
@ -447,7 +447,7 @@ impl Snapper {
|
||||||
/// Find the best snap candidate near the cursor.
|
/// Find the best snap candidate near the cursor.
|
||||||
pub fn snap(
|
pub fn snap(
|
||||||
&self,
|
&self,
|
||||||
cursor_world: Vec3,
|
cursor_world: glam::DVec3,
|
||||||
cursor_screen: Point,
|
cursor_screen: Point,
|
||||||
wires: &[WireModel],
|
wires: &[WireModel],
|
||||||
view_rot: Mat4,
|
view_rot: Mat4,
|
||||||
|
|
@ -492,11 +492,23 @@ impl Snapper {
|
||||||
// UNBOUNDED_AABB (±infinity) passes through automatically without a
|
// UNBOUNDED_AABB (±infinity) passes through automatically without a
|
||||||
// special-case branch because the arithmetic is exact for infinities.
|
// special-case branch because the arithmetic is exact for infinities.
|
||||||
let wire_in_range = |wire: &WireModel| -> bool {
|
let wire_in_range = |wire: &WireModel| -> bool {
|
||||||
let r = world_snap_r;
|
// The AABB is stored in f32, so at UTM-scale coordinates each bound
|
||||||
cursor_world.x + r >= wire.aabb[0]
|
// is quantized by up to ~1 ulp (≈ coord × 2⁻²³ ≈ 0.7 m at 5.7e6).
|
||||||
&& cursor_world.x - r <= wire.aabb[2]
|
// When zoomed in hard the snap radius shrinks below that, so the
|
||||||
&& cursor_world.y + r >= wire.aabb[1]
|
// raw f32 bound can wrongly exclude a wire the cursor is on. Pad the
|
||||||
&& cursor_world.y - r <= wire.aabb[3]
|
// test by the bound's own quantization so the cull never rejects a
|
||||||
|
// genuinely in-range wire (it only ever over-includes, which the
|
||||||
|
// per-vertex screen test below then rejects precisely).
|
||||||
|
let mag = wire
|
||||||
|
.aabb
|
||||||
|
.iter()
|
||||||
|
.fold(0.0f32, |m, c| m.max(c.abs()));
|
||||||
|
let pad = (mag * f32::EPSILON * 2.0) as f64;
|
||||||
|
let r = world_snap_r as f64 + pad;
|
||||||
|
cursor_world.x + r >= wire.aabb[0] as f64
|
||||||
|
&& cursor_world.x - r <= wire.aabb[2] as f64
|
||||||
|
&& cursor_world.y + r >= wire.aabb[1] as f64
|
||||||
|
&& cursor_world.y - r <= wire.aabb[3] as f64
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut try_pt = |world: glam::DVec3, snap_type: SnapType| {
|
let mut try_pt = |world: glam::DVec3, snap_type: SnapType| {
|
||||||
|
|
@ -579,10 +591,10 @@ impl Snapper {
|
||||||
}
|
}
|
||||||
if !wire.key_vertices.is_empty() {
|
if !wire.key_vertices.is_empty() {
|
||||||
for seg in wire.key_vertices.windows(2) {
|
for seg in wire.key_vertices.windows(2) {
|
||||||
let a = Vec3::new(seg[0][0] as f32, seg[0][1] as f32, seg[0][2] as f32);
|
let a = glam::DVec3::new(seg[0][0], seg[0][1], seg[0][2]);
|
||||||
let b = Vec3::new(seg[1][0] as f32, seg[1][1] as f32, seg[1][2] as f32);
|
let b = glam::DVec3::new(seg[1][0], seg[1][1], seg[1][2]);
|
||||||
if a.distance_squared(b) > 1e-12 {
|
if a.distance_squared(b) > 1e-12 {
|
||||||
try_pt(((a + b) * 0.5).as_dvec3(), SnapType::Midpoint);
|
try_pt((a + b) * 0.5, SnapType::Midpoint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -595,10 +607,9 @@ impl Snapper {
|
||||||
if !wire_in_range(wire) {
|
if !wire_in_range(wire) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for seg in wire.points.windows(2) {
|
for i in 0..wire.points.len().saturating_sub(1) {
|
||||||
let p =
|
let p = nearest_on_segment(cursor_world, wp_f64(wire, i), wp_f64(wire, i + 1));
|
||||||
nearest_on_segment(cursor_world, Vec3::from(seg[0]), Vec3::from(seg[1]));
|
try_pt(p, SnapType::Nearest);
|
||||||
try_pt(p.as_dvec3(), SnapType::Nearest);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -611,14 +622,14 @@ impl Snapper {
|
||||||
// on its screen distance to the cursor like every other snap, so it
|
// on its screen distance to the cursor like every other snap, so it
|
||||||
// offers when the cursor is near the perpendicular foot. (#118)
|
// offers when the cursor is near the perpendicular foot. (#118)
|
||||||
if self.is_on(SnapType::Perpendicular) {
|
if self.is_on(SnapType::Perpendicular) {
|
||||||
let q = self.from_point.unwrap_or(cursor_world);
|
let q = self.from_point.map(|v| v.as_dvec3()).unwrap_or(cursor_world);
|
||||||
for wire in wires {
|
for wire in wires {
|
||||||
if !wire_in_range(wire) {
|
if !wire_in_range(wire) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for seg in wire.points.windows(2) {
|
for i in 0..wire.points.len().saturating_sub(1) {
|
||||||
if let Some(foot) = perp_foot(q, Vec3::from(seg[0]), Vec3::from(seg[1])) {
|
if let Some(foot) = perp_foot(q, wp_f64(wire, i), wp_f64(wire, i + 1)) {
|
||||||
try_pt(foot.as_dvec3(), SnapType::Perpendicular);
|
try_pt(foot, SnapType::Perpendicular);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -634,17 +645,17 @@ impl Snapper {
|
||||||
if !wire_in_range(&wires[j]) {
|
if !wire_in_range(&wires[j]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for seg_a in wires[i].points.windows(2) {
|
for ai in 0..wires[i].points.len().saturating_sub(1) {
|
||||||
// S: pre-convert outside inner loop
|
// S: pre-convert outside inner loop
|
||||||
let a0 = Vec3::from(seg_a[0]);
|
let a0 = wp_f64(&wires[i], ai);
|
||||||
let a1 = Vec3::from(seg_a[1]);
|
let a1 = wp_f64(&wires[i], ai + 1);
|
||||||
let a_min_x = a0.x.min(a1.x);
|
let a_min_x = a0.x.min(a1.x);
|
||||||
let a_max_x = a0.x.max(a1.x);
|
let a_max_x = a0.x.max(a1.x);
|
||||||
let a_min_y = a0.y.min(a1.y);
|
let a_min_y = a0.y.min(a1.y);
|
||||||
let a_max_y = a0.y.max(a1.y);
|
let a_max_y = a0.y.max(a1.y);
|
||||||
for seg_b in wires[j].points.windows(2) {
|
for bi in 0..wires[j].points.len().saturating_sub(1) {
|
||||||
let b0 = Vec3::from(seg_b[0]);
|
let b0 = wp_f64(&wires[j], bi);
|
||||||
let b1 = Vec3::from(seg_b[1]);
|
let b1 = wp_f64(&wires[j], bi + 1);
|
||||||
// O: tight per-segment AABB overlap cull
|
// O: tight per-segment AABB overlap cull
|
||||||
if a_max_x < b0.x.min(b1.x)
|
if a_max_x < b0.x.min(b1.x)
|
||||||
|| a_min_x > b0.x.max(b1.x)
|
|| a_min_x > b0.x.max(b1.x)
|
||||||
|
|
@ -654,7 +665,7 @@ impl Snapper {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(pt) = seg_intersect_xy(a0, a1, b0, b1) {
|
if let Some(pt) = seg_intersect_xy(a0, a1, b0, b1) {
|
||||||
try_pt(pt.as_dvec3(), SnapType::Intersection);
|
try_pt(pt, SnapType::Intersection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -671,8 +682,8 @@ impl Snapper {
|
||||||
}
|
}
|
||||||
// Extend beyond the first point.
|
// Extend beyond the first point.
|
||||||
{
|
{
|
||||||
let p0 = Vec3::from(wire.points[0]);
|
let p0 = wp_f64(wire, 0);
|
||||||
let p1 = Vec3::from(wire.points[1]);
|
let p1 = wp_f64(wire, 1);
|
||||||
if let Some(ext) = extension_snap(
|
if let Some(ext) = extension_snap(
|
||||||
cursor_world,
|
cursor_world,
|
||||||
p0,
|
p0,
|
||||||
|
|
@ -682,13 +693,13 @@ impl Snapper {
|
||||||
bounds,
|
bounds,
|
||||||
self.osnap_radius_px,
|
self.osnap_radius_px,
|
||||||
) {
|
) {
|
||||||
try_pt(ext.as_dvec3(), SnapType::Extension);
|
try_pt(ext, SnapType::Extension);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Extend beyond the last point.
|
// Extend beyond the last point.
|
||||||
{
|
{
|
||||||
let p_last = Vec3::from(wire.points[n - 1]);
|
let p_last = wp_f64(wire, n - 1);
|
||||||
let p_prev = Vec3::from(wire.points[n - 2]);
|
let p_prev = wp_f64(wire, n - 2);
|
||||||
if let Some(ext) = extension_snap(
|
if let Some(ext) = extension_snap(
|
||||||
cursor_world,
|
cursor_world,
|
||||||
p_last,
|
p_last,
|
||||||
|
|
@ -698,7 +709,7 @@ impl Snapper {
|
||||||
bounds,
|
bounds,
|
||||||
self.osnap_radius_px,
|
self.osnap_radius_px,
|
||||||
) {
|
) {
|
||||||
try_pt(ext.as_dvec3(), SnapType::Extension);
|
try_pt(ext, SnapType::Extension);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -714,9 +725,8 @@ impl Snapper {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(
|
Some(
|
||||||
w.points
|
(0..w.points.len())
|
||||||
.iter()
|
.map(|i| world_to_screen(wp_f64(w, i), view_rot, eye, bounds))
|
||||||
.map(|&p| world_to_screen(glam::DVec3::new(p[0] as f64, p[1] as f64, p[2] as f64), view_rot, eye, bounds))
|
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
@ -730,16 +740,16 @@ impl Snapper {
|
||||||
let Some(ref sj) = screen_pts[j] else {
|
let Some(ref sj) = screen_pts[j] else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
for (ai, seg_a) in wires[i].points.windows(2).enumerate() {
|
for ai in 0..wires[i].points.len().saturating_sub(1) {
|
||||||
let sa0 = si[ai];
|
let sa0 = si[ai];
|
||||||
let sa1 = si[ai + 1];
|
let sa1 = si[ai + 1];
|
||||||
for (bi, _) in wires[j].points.windows(2).enumerate() {
|
for bi in 0..wires[j].points.len().saturating_sub(1) {
|
||||||
let sb0 = sj[bi];
|
let sb0 = sj[bi];
|
||||||
let sb1 = sj[bi + 1];
|
let sb1 = sj[bi + 1];
|
||||||
if let Some((ta, _)) = seg_intersect_2d(sa0, sa1, sb0, sb1) {
|
if let Some((ta, _)) = seg_intersect_2d(sa0, sa1, sb0, sb1) {
|
||||||
let wa0 = Vec3::from(seg_a[0]);
|
let wa0 = wp_f64(&wires[i], ai);
|
||||||
let wa1 = Vec3::from(seg_a[1]);
|
let wa1 = wp_f64(&wires[i], ai + 1);
|
||||||
try_pt((wa0 + ta * (wa1 - wa0)).as_dvec3(), SnapType::ApparentIntersection);
|
try_pt(wa0 + ta as f64 * (wa1 - wa0), SnapType::ApparentIntersection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -749,16 +759,17 @@ impl Snapper {
|
||||||
|
|
||||||
// ── Grid ───────────────────────────────────────────────────────────
|
// ── Grid ───────────────────────────────────────────────────────────
|
||||||
if self.is_on(SnapType::Grid) {
|
if self.is_on(SnapType::Grid) {
|
||||||
let s = self.grid_spacing;
|
let s = self.grid_spacing as f64;
|
||||||
// Round in the UCS grid frame, then map back to world.
|
// Round in the UCS grid frame, then map back to world.
|
||||||
let ax = grid_rot.transform_vector3(Vec3::X);
|
let ax = grid_rot.transform_vector3(Vec3::X).as_dvec3();
|
||||||
let ay = grid_rot.transform_vector3(Vec3::Y);
|
let ay = grid_rot.transform_vector3(Vec3::Y).as_dvec3();
|
||||||
let az = grid_rot.transform_vector3(Vec3::Z);
|
let az = grid_rot.transform_vector3(Vec3::Z).as_dvec3();
|
||||||
let rel = cursor_world - grid_origin;
|
let origin = grid_origin.as_dvec3();
|
||||||
|
let rel = cursor_world - origin;
|
||||||
let ux = (rel.dot(ax) / s).round() * s;
|
let ux = (rel.dot(ax) / s).round() * s;
|
||||||
let uy = (rel.dot(ay) / s).round() * s;
|
let uy = (rel.dot(ay) / s).round() * s;
|
||||||
let uz = (rel.dot(az) / s).round() * s;
|
let uz = (rel.dot(az) / s).round() * s;
|
||||||
try_pt((grid_origin + ax * ux + ay * uy + az * uz).as_dvec3(), SnapType::Grid);
|
try_pt(origin + ax * ux + ay * uy + az * uz, SnapType::Grid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tangent ────────────────────────────────────────────────────────
|
// ── Tangent ────────────────────────────────────────────────────────
|
||||||
|
|
@ -861,8 +872,23 @@ fn snap_priority(t: SnapType) -> u8 {
|
||||||
|
|
||||||
// ── Geometric helpers ─────────────────────────────────────────────────────
|
// ── Geometric helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Reconstruct the absolute f64 position of wire vertex `i` from its
|
||||||
|
/// double-single high/low pair. At UTM-scale coordinates the `points` (high)
|
||||||
|
/// f32 alone is ~0.5 m off; adding the low residual restores f64 precision so
|
||||||
|
/// computed snaps (nearest/perp/intersection/extension) land on the geometry.
|
||||||
|
#[inline]
|
||||||
|
fn wp_f64(wire: &WireModel, i: usize) -> glam::DVec3 {
|
||||||
|
let h = wire.points[i];
|
||||||
|
let l = wire.points_low.get(i).copied().unwrap_or([0.0; 3]);
|
||||||
|
glam::DVec3::new(
|
||||||
|
h[0] as f64 + l[0] as f64,
|
||||||
|
h[1] as f64 + l[1] as f64,
|
||||||
|
h[2] as f64 + l[2] as f64,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Closest point on segment [p0, p1] to `query`.
|
/// Closest point on segment [p0, p1] to `query`.
|
||||||
fn nearest_on_segment(query: Vec3, p0: Vec3, p1: Vec3) -> Vec3 {
|
fn nearest_on_segment(query: glam::DVec3, p0: glam::DVec3, p1: glam::DVec3) -> glam::DVec3 {
|
||||||
let d = p1 - p0;
|
let d = p1 - p0;
|
||||||
let len2 = d.x * d.x + d.y * d.y;
|
let len2 = d.x * d.x + d.y * d.y;
|
||||||
if len2 < 1e-12 {
|
if len2 < 1e-12 {
|
||||||
|
|
@ -870,12 +896,12 @@ fn nearest_on_segment(query: Vec3, p0: Vec3, p1: Vec3) -> Vec3 {
|
||||||
}
|
}
|
||||||
let t = ((query.x - p0.x) * d.x + (query.y - p0.y) * d.y) / len2;
|
let t = ((query.x - p0.x) * d.x + (query.y - p0.y) * d.y) / len2;
|
||||||
let t = t.clamp(0.0, 1.0);
|
let t = t.clamp(0.0, 1.0);
|
||||||
Vec3::new(p0.x + t * d.x, p0.y + t * d.y, p0.z + t * d.z)
|
glam::DVec3::new(p0.x + t * d.x, p0.y + t * d.y, p0.z + t * d.z)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Foot of perpendicular from `query` to the line through [p0, p1] (XY plane, unclamped).
|
/// Foot of perpendicular from `query` to the line through [p0, p1] (XY plane, unclamped).
|
||||||
/// Returns `None` if the segment is degenerate.
|
/// Returns `None` if the segment is degenerate.
|
||||||
fn perp_foot(query: Vec3, p0: Vec3, p1: Vec3) -> Option<Vec3> {
|
fn perp_foot(query: glam::DVec3, p0: glam::DVec3, p1: glam::DVec3) -> Option<glam::DVec3> {
|
||||||
let d = p1 - p0;
|
let d = p1 - p0;
|
||||||
let len2 = d.x * d.x + d.y * d.y;
|
let len2 = d.x * d.x + d.y * d.y;
|
||||||
if len2 < 1e-12 {
|
if len2 < 1e-12 {
|
||||||
|
|
@ -886,11 +912,11 @@ fn perp_foot(query: Vec3, p0: Vec3, p1: Vec3) -> Option<Vec3> {
|
||||||
if t < -1.0 || t > 2.0 {
|
if t < -1.0 || t > 2.0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(Vec3::new(p0.x + t * d.x, p0.y + t * d.y, p0.z + t * d.z))
|
Some(glam::DVec3::new(p0.x + t * d.x, p0.y + t * d.y, p0.z + t * d.z))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// XY-plane segment-segment intersection. Returns `None` if parallel or outside.
|
/// XY-plane segment-segment intersection. Returns `None` if parallel or outside.
|
||||||
fn seg_intersect_xy(a0: Vec3, a1: Vec3, b0: Vec3, b1: Vec3) -> Option<Vec3> {
|
fn seg_intersect_xy(a0: glam::DVec3, a1: glam::DVec3, b0: glam::DVec3, b1: glam::DVec3) -> Option<glam::DVec3> {
|
||||||
let d1x = a1.x - a0.x;
|
let d1x = a1.x - a0.x;
|
||||||
let d1y = a1.y - a0.y;
|
let d1y = a1.y - a0.y;
|
||||||
let d2x = b1.x - b0.x;
|
let d2x = b1.x - b0.x;
|
||||||
|
|
@ -906,7 +932,7 @@ fn seg_intersect_xy(a0: Vec3, a1: Vec3, b0: Vec3, b1: Vec3) -> Option<Vec3> {
|
||||||
if t < 0.0 || t > 1.0 || s < 0.0 || s > 1.0 {
|
if t < 0.0 || t > 1.0 || s < 0.0 || s > 1.0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(Vec3::new(a0.x + t * d1x, a0.y + t * d1y, 0.0))
|
Some(glam::DVec3::new(a0.x + t * d1x, a0.y + t * d1y, 0.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Intersection of two infinite lines in the XY plane, each given by an origin
|
/// Intersection of two infinite lines in the XY plane, each given by an origin
|
||||||
|
|
@ -945,14 +971,14 @@ fn seg_intersect_2d(a0: Point, a1: Point, b0: Point, b1: Point) -> Option<(f32,
|
||||||
/// Snap to the extension of a ray beyond `origin` in `dir` direction.
|
/// Snap to the extension of a ray beyond `origin` in `dir` direction.
|
||||||
/// Returns `None` if the cursor is not near the extension line.
|
/// Returns `None` if the cursor is not near the extension line.
|
||||||
fn extension_snap(
|
fn extension_snap(
|
||||||
cursor_world: Vec3,
|
cursor_world: glam::DVec3,
|
||||||
origin: Vec3,
|
origin: glam::DVec3,
|
||||||
dir: Vec3,
|
dir: glam::DVec3,
|
||||||
view_rot: Mat4,
|
view_rot: Mat4,
|
||||||
eye: glam::DVec3,
|
eye: glam::DVec3,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
radius_px: f32,
|
radius_px: f32,
|
||||||
) -> Option<Vec3> {
|
) -> Option<glam::DVec3> {
|
||||||
let len2 = dir.x * dir.x + dir.y * dir.y;
|
let len2 = dir.x * dir.x + dir.y * dir.y;
|
||||||
if len2 < 1e-12 {
|
if len2 < 1e-12 {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -961,9 +987,9 @@ fn extension_snap(
|
||||||
if t < 0.05 {
|
if t < 0.05 {
|
||||||
return None;
|
return None;
|
||||||
} // only beyond the endpoint
|
} // only beyond the endpoint
|
||||||
let world_pt = Vec3::new(origin.x + t * dir.x, origin.y + t * dir.y, origin.z);
|
let world_pt = glam::DVec3::new(origin.x + t * dir.x, origin.y + t * dir.y, origin.z);
|
||||||
let screen_pt = world_to_screen(world_pt.as_dvec3(), view_rot, eye, bounds);
|
let screen_pt = world_to_screen(world_pt, view_rot, eye, bounds);
|
||||||
let cursor_screen = world_to_screen(cursor_world.as_dvec3(), view_rot, eye, bounds);
|
let cursor_screen = world_to_screen(cursor_world, view_rot, eye, bounds);
|
||||||
if dist2(screen_pt, cursor_screen) > radius_px * radius_px {
|
if dist2(screen_pt, cursor_screen) > radius_px * radius_px {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue