fix(point): preserve relative marker sizing

Keep rendering, picking, block transforms, and plotted output on the
same viewport-relative PDSIZE geometry without overloading wire width
fields.
This commit is contained in:
Hakan Seven committed 2026-08-22 22:22:01 +03:00
1 parent c5fe70627a
commit 7293bec1b9
69 files changed
+823 -221

No files matched your search

+176 -20
View File
@@ -208,6 +208,31 @@ pub fn click_hit<'a, W: WireSource + ?Sized>(
let mut best_dist = f32::MAX;
let mut best: Option<&str> = None;
for wire in wires.iter().filter(|wire| wire.point_marker.is_some()) {
let tolerance = pick_tolerance_px(wire, lw_display, base_radius_px);
let mut previous = None;
for (index, point) in wire.points.iter().enumerate() {
if !point[0].is_finite() {
previous = None;
continue;
}
let screen = world_to_screen(
wire_point_world(wire, index, view_rot, eye),
view_rot,
eye,
bounds,
);
if let Some(start) = previous {
let distance = dist_point_to_segment(cursor, start, screen);
if distance < tolerance && distance < best_dist {
best_dist = distance;
best = Some(wire.name.as_str());
}
}
previous = Some(screen);
}
}
// World z only shifts the *screen* x/y when the view is tilted (orbit /
// perspective). In the flat top-down ortho view — the case where hover lag
// on large drawings actually bites — a wire's screen position depends only
@@ -224,18 +249,21 @@ pub fn click_hit<'a, W: WireSource + ?Sized>(
let Some(wire) = wires.source_wire(segment.wire) else {
continue;
};
if wire.point_marker.is_some() {
continue;
}
let start = segment.start as usize;
if start + 1 >= wire.points.len() {
continue;
}
let p0 = world_to_screen(
wp64(wire.points[start], &wire.points_low, start),
wire_point_world(wire, start, view_rot, eye),
view_rot,
eye,
bounds,
);
let p1 = world_to_screen(
wp64(wire.points[start + 1], &wire.points_low, start + 1),
wire_point_world(wire, start + 1, view_rot, eye),
view_rot,
eye,
bounds,
@@ -249,23 +277,27 @@ pub fn click_hit<'a, W: WireSource + ?Sized>(
} else {
// Q: lazy projection — no Vec allocation per wire; NaN resets the segment chain.
for wire in wires.iter() {
if wire.point_marker.is_some() {
continue;
}
let tol = pick_tolerance_px(wire, lw_display, base_radius_px);
// Cheap AABB pre-reject (flat view only; never for the unbounded
// sentinel used by previews / greeked text).
if z_flat
&& wire.point_marker.is_none()
&& wire.aabb != WireModel::UNBOUNDED_AABB
&& aabb_rejects(wire.aabb, cursor, tol, view_rot, eye, bounds)
{
continue;
}
let mut prev: Option<Point> = None;
for (i, &[px, py, pz]) in wire.points.iter().enumerate() {
for (i, &[px, _, _]) in wire.points.iter().enumerate() {
if px.is_nan() {
prev = None;
continue;
}
let cur = world_to_screen(
wp64([px, py, pz], &wire.points_low, i),
wire_point_world(wire, i, view_rot, eye),
view_rot,
eye,
bounds,
@@ -450,24 +482,55 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
return Vec::new();
}
let mut hits: Vec<(f32, &str)> = Vec::new();
let mut marker_hits: HashMap<&str, f32> = HashMap::default();
for wire in wires.iter().filter(|wire| wire.point_marker.is_some()) {
let tolerance = pick_tolerance_px(wire, lw_display, base_radius_px);
let mut previous = None;
for (index, point) in wire.points.iter().enumerate() {
if !point[0].is_finite() {
previous = None;
continue;
}
let screen = world_to_screen(
wire_point_world(wire, index, view_rot, eye),
view_rot,
eye,
bounds,
);
if let Some(start) = previous {
let distance = dist_point_to_segment(cursor, start, screen);
if distance < tolerance {
marker_hits
.entry(wire.name.as_str())
.and_modify(|best| *best = best.min(distance))
.or_insert(distance);
}
}
previous = Some(screen);
}
}
hits.extend(marker_hits.into_iter().map(|(name, distance)| (distance, name)));
if let Some(segments) = wires.segments() {
let mut best_by_wire: HashMap<u32, f32> = HashMap::default();
for segment in segments {
let Some(wire) = wires.source_wire(segment.wire) else {
continue;
};
if wire.point_marker.is_some() {
continue;
}
let start = segment.start as usize;
if start + 1 >= wire.points.len() {
continue;
}
let p0 = world_to_screen(
wp64(wire.points[start], &wire.points_low, start),
wire_point_world(wire, start, view_rot, eye),
view_rot,
eye,
bounds,
);
let p1 = world_to_screen(
wp64(wire.points[start + 1], &wire.points_low, start + 1),
wire_point_world(wire, start + 1, view_rot, eye),
view_rot,
eye,
bounds,
@@ -487,17 +550,20 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
}));
} else {
for wire in wires.iter() {
if wire.point_marker.is_some() {
continue;
}
let tol = pick_tolerance_px(wire, lw_display, base_radius_px);
let mut prev: Option<Point> = None;
let mut best_for_wire = tol;
let mut hit = false;
for (i, &[px, py, pz]) in wire.points.iter().enumerate() {
for (i, &[px, _, _]) in wire.points.iter().enumerate() {
if px.is_nan() {
prev = None;
continue;
}
let cur = world_to_screen(
wp64([px, py, pz], &wire.points_low, i),
wire_point_world(wire, i, view_rot, eye),
view_rot,
eye,
bounds,
@@ -1104,13 +1170,13 @@ fn indexed_box_crossing_hits<'a, W: WireSource + ?Sized>(
continue;
}
let a = world_to_screen(
wp64(wire.points[start], &wire.points_low, start),
wire_point_world(wire, start, view_rot, eye),
view_rot,
eye,
bounds,
);
let b = world_to_screen(
wp64(wire.points[start + 1], &wire.points_low, start + 1),
wire_point_world(wire, start + 1, view_rot, eye),
view_rot,
eye,
bounds,
@@ -1119,6 +1185,14 @@ fn indexed_box_crossing_hits<'a, W: WireSource + ?Sized>(
out.push(wire.name.as_str());
}
}
for wire in wires.iter().filter(|wire| wire.point_marker.is_some()) {
if !seen.contains(wire.name.as_str())
&& marker_segment_hit(wire, view_rot, eye, bounds, |a, b| segment_hits(a, b))
&& seen.insert(wire.name.as_str())
{
out.push(wire.name.as_str());
}
}
for (triangles, pick_only) in [
(wires.fill_triangles().unwrap_or_default(), false),
(wires.pick_triangles().unwrap_or_default(), true),
@@ -1181,7 +1255,7 @@ fn indexed_box_crossing_hits<'a, W: WireSource + ?Sized>(
if wire.points.iter().enumerate().any(|(index, &point)| {
point[0].is_finite()
&& inside(world_to_screen(
wp64(point, &wire.points_low, index),
wire_point_world(wire, index, view_rot, eye),
view_rot,
eye,
bounds,
@@ -1231,13 +1305,13 @@ pub fn poly_fence_hit<'a, W: WireSource + ?Sized>(
continue;
}
let a = world_to_screen(
wp64(wire.points[start], &wire.points_low, start),
wire_point_world(wire, start, view_rot, eye),
view_rot,
eye,
bounds,
);
let b = world_to_screen(
wp64(wire.points[start + 1], &wire.points_low, start + 1),
wire_point_world(wire, start + 1, view_rot, eye),
view_rot,
eye,
bounds,
@@ -1246,6 +1320,14 @@ pub fn poly_fence_hit<'a, W: WireSource + ?Sized>(
out.push(wire.name.as_str());
}
}
for wire in wires.iter().filter(|wire| wire.point_marker.is_some()) {
if !seen.contains(wire.name.as_str())
&& marker_segment_hit(wire, view_rot, eye, bounds, |a, b| cuts(a, b))
&& seen.insert(wire.name.as_str())
{
out.push(wire.name.as_str());
}
}
return out;
}
for wire in wires.iter() {
@@ -1254,13 +1336,13 @@ pub fn poly_fence_hit<'a, W: WireSource + ?Sized>(
}
let hit = (0..wire.points.len() - 1).any(|k| {
let a = world_to_screen(
wp64(wire.points[k], &wire.points_low, k),
wire_point_world(wire, k, view_rot, eye),
view_rot,
eye,
bounds,
);
let b = world_to_screen(
wp64(wire.points[k + 1], &wire.points_low, k + 1),
wire_point_world(wire, k + 1, view_rot, eye),
view_rot,
eye,
bounds,
@@ -1292,13 +1374,13 @@ fn indexed_polygon_crossing_hits<'a, W: WireSource + ?Sized>(
continue;
}
let a = world_to_screen(
wp64(wire.points[start], &wire.points_low, start),
wire_point_world(wire, start, view_rot, eye),
view_rot,
eye,
bounds,
);
let b = world_to_screen(
wp64(wire.points[start + 1], &wire.points_low, start + 1),
wire_point_world(wire, start + 1, view_rot, eye),
view_rot,
eye,
bounds,
@@ -1311,6 +1393,18 @@ fn indexed_polygon_crossing_hits<'a, W: WireSource + ?Sized>(
out.push(wire.name.as_str());
}
}
for wire in wires.iter().filter(|wire| wire.point_marker.is_some()) {
if !seen.contains(wire.name.as_str())
&& marker_segment_hit(wire, view_rot, eye, bounds, |a, b| {
point_in_polygon(a, poly)
|| point_in_polygon(b, poly)
|| segment_crosses_polygon(a, b, poly)
})
&& seen.insert(wire.name.as_str())
{
out.push(wire.name.as_str());
}
}
for (triangles, pick_only) in [
(wires.fill_triangles().unwrap_or_default(), false),
(wires.pick_triangles().unwrap_or_default(), true),
@@ -1371,7 +1465,12 @@ fn indexed_polygon_crossing_hits<'a, W: WireSource + ?Sized>(
if wire.points.iter().enumerate().any(|(index, &point)| {
point[0].is_finite()
&& point_in_polygon(
world_to_screen(wp64(point, &wire.points_low, index), view_rot, eye, bounds),
world_to_screen(
wire_point_world(wire, index, view_rot, eye),
view_rot,
eye,
bounds,
),
poly,
)
}) && seen.insert(wire.name.as_str())
@@ -1473,7 +1572,12 @@ pub fn box_hit<'a, W: WireSource + ?Sized>(
prev = None;
continue;
}
let sp = world_to_screen(wp64([px, py, pz], low, i), view_rot, eye, bounds);
let world = if !wire.points.is_empty() {
wire_point_world(wire, i, view_rot, eye)
} else {
wp64([px, py, pz], low, i)
};
let sp = world_to_screen(world, view_rot, eye, bounds);
if crossing {
if inside(sp) {
hit = true;
@@ -1595,7 +1699,12 @@ pub fn poly_hit<'a, W: WireSource + ?Sized>(
prev = None;
continue;
}
let sp = world_to_screen(wp64([px, py, pz], low, i), view_rot, eye, bounds);
let world = if !wire.points.is_empty() {
wire_point_world(wire, i, view_rot, eye)
} else {
wp64([px, py, pz], low, i)
};
let sp = world_to_screen(world, view_rot, eye, bounds);
// Reject points the GPU scissored out of a floating viewport so
// the lasso can't reach clipped geometry. No-op in model space.
if sp.x < 0.0 || sp.x > bounds.width || sp.y < 0.0 || sp.y > bounds.height {
@@ -1694,6 +1803,53 @@ fn wp64(hi: [f32; 3], low: &[[f32; 3]], i: usize) -> glam::DVec3 {
)
}
fn wire_point_world(
wire: &WireModel,
index: usize,
view_rot: Mat4,
eye: glam::DVec3,
) -> glam::DVec3 {
let Some(marker) = wire.point_marker else {
return wire.point_world(index, 0.0);
};
let row_y = glam::DVec3::new(
view_rot.x_axis.y as f64,
view_rot.y_axis.y as f64,
view_rot.z_axis.y as f64,
);
let projection_scale = row_y.length().max(1.0e-12);
let relative = (marker.origin - eye).as_vec3().extend(1.0);
let clip_w = (view_rot * relative).w.abs().max(1.0e-6);
wire.point_world(index, 2.0 * clip_w as f64 / projection_scale)
}
fn marker_segment_hit(
wire: &WireModel,
view_rot: Mat4,
eye: glam::DVec3,
bounds: Rectangle,
mut hit: impl FnMut(Point, Point) -> bool,
) -> bool {
let mut previous = None;
for (index, point) in wire.points.iter().enumerate() {
if !point[0].is_finite() {
previous = None;
continue;
}
let screen = world_to_screen(
wire_point_world(wire, index, view_rot, eye),
view_rot,
eye,
bounds,
);
if previous.is_some_and(|start| hit(start, screen)) {
return true;
}
previous = Some(screen);
}
false
}
/// Even-odd ray-casting test: is `p` inside the polygon?
///
/// Handles multi-path boundaries: NaN points (used as path separators by
+90 -20
View File
@@ -557,7 +557,9 @@ pub struct InteractionIndex {
pick_triangles: SpatialSet<TriangleRef>,
glyphs: SpatialSet<GlyphRef>,
unbounded_wires: Vec<u32>,
screen_unbounded_wires: Vec<u32>,
max_line_half_width_px: f32,
max_marker_radius_fraction: f32,
}
pub struct InteractionHandleIndex {
@@ -574,7 +576,9 @@ struct WireIndexEntries {
pick_triangles: Vec<Entry3<TriangleRef>>,
glyphs: Vec<Entry3<GlyphRef>>,
unbounded: bool,
screen_unbounded: bool,
max_line_half_width_px: f32,
max_marker_radius_fraction: f32,
}
fn collect_wire_index_entries(wire_idx: u32, wire: &WireModel) -> WireIndexEntries {
@@ -591,28 +595,34 @@ fn collect_wire_index_entries(wire_idx: u32, wire: &WireModel) -> WireIndexEntri
pick_triangles: Vec::with_capacity(wire.pick_tris.len() / 3),
glyphs: Vec::with_capacity(wire.text_verts.len() / 6),
unbounded: false,
screen_unbounded: wire.point_marker.is_some(),
max_line_half_width_px: if wire.line_weight_px.is_finite() {
(wire.line_weight_px * 0.5).max(0.0)
} else {
0.0
},
max_marker_radius_fraction: wire.point_marker.map_or(0.0, |marker| {
marker.viewport_percent.max(0.0) * 0.01 * std::f32::consts::SQRT_2
}),
};
entries.unbounded = entries.wire.is_none();
for start in 0..wire.points.len().saturating_sub(1) {
let Some(aabb) = points_aabb3([
wire_point(wire, start),
wire_point(wire, start + 1),
]) else {
continue;
};
entries.segments.push(Entry3 {
aabb,
value: SegmentRef {
wire: wire_idx,
start: start as u32,
},
});
if wire.point_marker.is_none() {
for start in 0..wire.points.len().saturating_sub(1) {
let Some(aabb) = points_aabb3([
wire_point(wire, start),
wire_point(wire, start + 1),
]) else {
continue;
};
entries.segments.push(Entry3 {
aabb,
value: SegmentRef {
wire: wire_idx,
start: start as u32,
},
});
}
}
for (index, (point, _)) in wire.snap_pts.iter().enumerate() {
if point.is_finite() {
@@ -773,7 +783,9 @@ impl InteractionIndex {
let collect_started = iced::time::Instant::now();
let mut wire_entries = Vec::with_capacity(wires.len());
let mut unbounded_wires = Vec::new();
let mut screen_unbounded_wires = Vec::new();
let mut max_line_half_width_px = 0.0f32;
let mut max_marker_radius_fraction = 0.0f32;
#[cfg(not(target_arch = "wasm32"))]
let per_wire: Vec<WireIndexEntries> = {
@@ -804,12 +816,17 @@ impl InteractionIndex {
for (wire_idx, entries) in per_wire.into_iter().enumerate() {
max_line_half_width_px =
max_line_half_width_px.max(entries.max_line_half_width_px);
max_marker_radius_fraction =
max_marker_radius_fraction.max(entries.max_marker_radius_fraction);
if let Some(entry) = entries.wire {
wire_entries.push(entry);
}
if entries.unbounded {
unbounded_wires.push(wire_idx as u32);
}
if entries.screen_unbounded {
screen_unbounded_wires.push(wire_idx as u32);
}
segment_parts.push(entries.segments);
snap_point_parts.push(entries.snap_points);
key_vertex_parts.push(entries.key_vertices);
@@ -952,7 +969,9 @@ impl InteractionIndex {
pick_triangles,
glyphs,
unbounded_wires,
screen_unbounded_wires,
max_line_half_width_px,
max_marker_radius_fraction,
}
}
@@ -1002,12 +1021,30 @@ impl InteractionIndex {
}
}
pub fn pick_radius_px(&self, base_radius_px: f32) -> f32 {
base_radius_px.max(self.max_line_half_width_px)
pub fn pick_radius_px(
&self,
base_radius_px: f32,
viewport_height_px: f32,
include_line_weight: bool,
) -> f32 {
let radius = base_radius_px
.max(self.max_marker_radius_fraction * viewport_height_px.max(0.0));
if include_line_weight {
radius.max(self.max_line_half_width_px)
} else {
radius
}
}
fn queried_wire_keys(&self, mut indices: Vec<u32>) -> Vec<(u64, u32)> {
fn queried_wire_keys(
&self,
mut indices: Vec<u32>,
include_screen_unbounded: bool,
) -> Vec<(u64, u32)> {
indices.extend_from_slice(&self.unbounded_wires);
if include_screen_unbounded {
indices.extend_from_slice(&self.screen_unbounded_wires);
}
let mut keys: Vec<(u64, u32)> = indices
.into_iter()
.filter_map(|index| {
@@ -1029,7 +1066,7 @@ impl InteractionIndex {
}
pub fn query_wire_keys_xy(&self, aabb: [f64; 4]) -> Vec<(u64, u32)> {
self.queried_wire_keys(self.wires.query_xy(aabb))
self.queried_wire_keys(self.wires.query_xy(aabb), false)
}
pub fn query_wire_keys_screen(
@@ -1039,7 +1076,10 @@ impl InteractionIndex {
eye: DVec3,
bounds: Rectangle,
) -> Vec<(u64, u32)> {
self.queried_wire_keys(self.wires.query_screen(screen_rect, view_rot, eye, bounds))
self.queried_wire_keys(
self.wires.query_screen(screen_rect, view_rot, eye, bounds),
true,
)
}
fn remap_wire_index(
@@ -1064,8 +1104,12 @@ impl InteractionIndex {
&self,
mut indices: Vec<u32>,
slots: &rustc_hash::FxHashMap<(u64, u32), u32>,
include_screen_unbounded: bool,
) -> Vec<u32> {
indices.extend_from_slice(&self.unbounded_wires);
if include_screen_unbounded {
indices.extend_from_slice(&self.screen_unbounded_wires);
}
let mut local: Vec<u32> = indices
.into_iter()
.filter_map(|index| self.remap_wire_index(index, slots))
@@ -1100,7 +1144,11 @@ impl InteractionIndex {
) -> InteractionCandidates {
InteractionCandidates {
wires,
wire_indices: Some(self.remap_wire_indices(self.wires.query_xy(aabb), slots)),
wire_indices: Some(self.remap_wire_indices(
self.wires.query_xy(aabb),
slots,
false,
)),
segments: Some(self.remap_refs(
self.segments.query_xy(aabb),
slots,
@@ -1164,6 +1212,7 @@ impl InteractionIndex {
self.wires
.query_screen(screen_rect, view_rot, eye, bounds),
slots,
true,
)),
segments: Some(self.remap_refs(
self.segments
@@ -1251,6 +1300,7 @@ impl InteractionIndex {
) -> InteractionCandidates {
let mut wire_indices = self.wires.query_screen(screen_rect, view_rot, eye, bounds);
wire_indices.extend_from_slice(&self.unbounded_wires);
wire_indices.extend_from_slice(&self.screen_unbounded_wires);
wire_indices.sort_unstable();
wire_indices.dedup();
InteractionCandidates {
@@ -1601,6 +1651,26 @@ impl<'a> Iterator for WireIter<'a> {
}
fn finite_wire_aabb3(wire: &WireModel) -> Option<[f64; 6]> {
if let Some(marker) = wire.point_marker {
if !marker.origin.is_finite() {
return None;
}
let normal = marker.normal.try_normalize().unwrap_or(DVec3::Z);
let mut min_axial = 0.0f64;
let mut max_axial = 0.0f64;
for index in 0..wire.points.len() {
let point = DVec3::from_array(wire_point(wire, index));
if point.is_finite() {
let axial = (point - marker.origin).dot(normal);
min_axial = min_axial.min(axial);
max_axial = max_axial.max(axial);
}
}
return points_aabb3([
(marker.origin + normal * min_axial).to_array(),
(marker.origin + normal * max_axial).to_array(),
]);
}
let [min_x, min_y, max_x, max_y] = wire.aabb;
if !min_x.is_finite() || !min_y.is_finite() || !max_x.is_finite() || !max_y.is_finite() {
return None;
+20 -1
View File
@@ -14,6 +14,7 @@ use acadrust::entities::Insert;
use acadrust::objects::{ObjectType, SpatialFilter};
use acadrust::types::{Handle, Transform, Vector3};
use acadrust::CadDocument;
use cadkernel::geom2d::{contains, Curve, Line, Tolerance};
use crate::scene::model::wire_model::WireModel;
@@ -116,6 +117,12 @@ pub fn clip_wires(wires: &mut Vec<WireModel>, poly: &[[f64; 2]]) {
.iter()
.map(|&[x, y]| [(x - rx) as f32, (y - ry) as f32])
.collect();
let boundary: Vec<Curve> = poly
.iter()
.zip(poly.iter().cycle().skip(1))
.take(poly.len())
.map(|(&start, &end)| Curve::Line(Line { start, end }))
.collect();
// Reconstruct an absolute-f64 wire point from its high/low pair, NaN-safe.
let abs = |hi: [f32; 3], lo: [f32; 3]| -> [f64; 3] {
@@ -127,7 +134,18 @@ pub fn clip_wires(wires: &mut Vec<WireModel>, poly: &[[f64; 2]]) {
};
for w in wires.iter_mut() {
if !w.points.is_empty() {
let keep_marker = w.point_marker.map(|marker| {
contains(
&boundary,
[marker.origin.x, marker.origin.y],
Tolerance::default(),
)
});
if keep_marker == Some(false) {
w.points.clear();
w.points_low.clear();
w.point_marker = None;
} else if keep_marker.is_none() && !w.points.is_empty() {
// Absolute → local f32 (NaN separators preserved).
let local: Vec<[f32; 3]> = (0..w.points.len())
.map(|i| {
@@ -257,6 +275,7 @@ pub fn frame_wire(
lo.push([lx, ly, 0.0]);
}
WireModel {
point_marker: None,
taper_widths: Vec::new(),
world_width: 0.0,
depth_override: None,