Track mixed paper and viewport dimension references

This commit is contained in:
Christopher Overbeck committed 2026-09-15 16:00:11 -05:00
1 parent bd00d679a9
commit 5424a19482
28 files changed
+823 -846

No files matched your search

+425
View File
@@ -0,0 +1,425 @@
//! Pick dimension geometry through a layout viewport. Resolve block instances
//! in model space, then project the picked entity onto the paper sheet.
use super::*;
use crate::command::EntityTransform;
use crate::scene::viewport_ref::ViewportFrame;
use acadrust::types::{Matrix4, Transform};
#[cfg(test)]
use glam::DVec2;
use glam::DVec3;
/// A model entity resolved by clicking inside a paper-space content viewport.
#[derive(Clone, Debug)]
pub struct ViewportDimensionPick {
/// The viewport the click looked through.
pub frame: ViewportFrame,
/// Handle of the innermost model entity that owns the picked geometry.
pub entity_handle: Handle,
/// INSERT handles from the outermost instance down to the instance that
/// directly contains `entity_handle`; empty for top-level model geometry.
pub block_path: Vec<Handle>,
/// The picked geometry with the block-instance transform *and* the
/// viewport model→paper transform applied, so paper-space dimension
/// commands can consume it unchanged.
pub paper_entity: EntityType,
/// The click position on the sheet.
pub paper_point: DVec3,
/// The click position in model space.
pub model_point: DVec3,
}
impl Scene {
/// Resolve an explicit dimension object pick made on the sheet but landing
/// inside a content viewport, against the *model* geometry that viewport
/// displays.
///
/// `aperture_paper` is the pick aperture in paper units.
pub fn dimension_pick_through_viewport(
&self,
paper: DVec3,
aperture_paper: f64,
) -> Option<ViewportDimensionPick> {
for frame in self.viewport_frames_at_paper_point(paper) {
let model_point = frame.paper_to_model(paper);
let aperture_model =
(aperture_paper.max(0.0) * frame.paper_to_model_length_factor()).max(1e-9);
let Some(top) = self.nearest_model_wire_handle(&frame, model_point, aperture_model)
else {
continue;
};
let Some((entity, block_path)) =
self.resolve_measurable_entity(top, model_point, Some(frame.viewport))
else {
continue;
};
if !planar_pick_distance(&entity, model_point).is_some_and(|d| d <= aperture_model) {
continue;
}
let mut paper_entity = entity;
crate::scene::view::dispatch::apply_transform(
&mut paper_entity,
&EntityTransform::Affine(viewport_model_to_paper_transform(&frame)),
);
let entity_handle = paper_entity.as_entity().handle();
return Some(ViewportDimensionPick {
frame,
entity_handle: if entity_handle.is_valid() {
entity_handle
} else {
top
},
block_path,
paper_entity,
paper_point: paper,
model_point,
});
}
None
}
/// Nearest resident model wire of `viewport` to `model_point`, within
/// `aperture` model units. Wires are the same tessellation the viewport
/// draws, so this matches what the user sees.
fn nearest_model_wire_handle(
&self,
frame: &ViewportFrame,
model_point: DVec3,
aperture: f64,
) -> Option<Handle> {
let viewport = frame.viewport;
let camera = self.camera_for_viewport(viewport)?;
let bounds = iced::Rectangle {
x: 0.0,
y: 0.0,
width: 1000.0,
height: 1000.0,
};
let radius_px =
(aperture / (2.0 * camera.ortho_size() as f64) * bounds.height as f64) as f32;
let wires = self.interaction_pick_candidates_near(
self.model_wires_for_viewport_arc(viewport, 0.0),
model_point,
camera.view_proj_rte(bounds),
camera.eye(),
bounds,
radius_px.max(1.0),
);
let mut best: Option<(f64, Handle)> = None;
for wire in wires.iter() {
if !wire.display_visible {
continue;
}
let Some(handle) = Self::handle_from_wire_name(&wire.name) else {
continue;
};
let offset = wire
.render_instance
.as_ref()
.map(|inst| DVec3::from_array(inst.translation))
.unwrap_or(DVec3::ZERO);
let distance = wire_polyline_nearest(wire, offset, model_point);
if let Some((distance, nearest)) = distance {
if !self.viewport_displays_paper_point(
viewport,
frame.model_to_paper(nearest).truncate(),
) {
continue;
}
if distance <= aperture && best.as_ref().is_none_or(|(d, _)| distance < *d) {
best = Some((distance, handle));
}
}
}
best.map(|(_, handle)| handle)
}
/// Turn a picked handle into a measurable planar entity in model (WCS)
/// coordinates, descending through block instances and baking the
/// instance transform into the returned clone.
pub(crate) fn resolve_measurable_entity(
&self,
handle: Handle,
model_point: DVec3,
viewport: Option<Handle>,
) -> Option<(EntityType, Vec<Handle>)> {
self.resolve_measurable_feature(handle, model_point, viewport, None)
}
pub(crate) fn resolve_measurable_feature(
&self,
handle: Handle,
model_point: DVec3,
viewport: Option<Handle>,
kind: Option<crate::snap::SnapType>,
) -> Option<(EntityType, Vec<Handle>)> {
let entity = self.document.get_entity(handle)?;
match entity {
EntityType::Insert(insert) => self.descend_block_instance(
insert,
Transform::identity(),
model_point,
0,
viewport,
kind,
),
other => {
feature_pick_distance(other, model_point, kind)?;
Some((other.clone(), Vec::new()))
}
}
}
/// Depth-first search for the nearest measurable entity inside an INSERT.
/// Returns the entity already transformed into WCS plus the INSERT path.
fn descend_block_instance(
&self,
insert: &acadrust::entities::Insert,
outer: Transform,
model_point: DVec3,
depth: usize,
viewport: Option<Handle>,
kind: Option<crate::snap::SnapType>,
) -> Option<(EntityType, Vec<Handle>)> {
const MAX_DEPTH: usize = 8;
if depth >= MAX_DEPTH {
return None;
}
if insert.row_count > 1 || insert.column_count > 1 {
return None;
}
let insert_handle = insert.common.handle;
let local = crate::scene::render_graph::insert_transform(&self.document, insert);
let combined = local.then(&outer);
let block = self.document.block_records.get(&insert.block_name)?;
let mut best: Option<(f64, EntityType, Vec<Handle>)> = None;
for &child in &block.entity_handles {
let Some(child_entity) = self.document.get_entity(child) else {
continue;
};
let common = child_entity.common();
let layer = self.document.layers.get(&common.layer);
let frozen = viewport.and_then(|h| self.document.get_entity(h)).is_some_and(|entity| {
matches!(entity, EntityType::Viewport(vp) if layer.is_some_and(|l| vp.frozen_layers.contains(&l.handle)))
});
if common.invisible || layer.is_some_and(|l| l.flags.off || l.flags.frozen) || frozen {
continue;
}
if let EntityType::Insert(child_insert) = child_entity {
if let Some((entity, mut nested_path)) = self.descend_block_instance(
child_insert,
combined,
model_point,
depth + 1,
viewport,
kind,
) {
if let Some(distance) = feature_pick_distance(&entity, model_point, kind) {
if best.as_ref().is_none_or(|(d, _, _)| distance < *d) {
let mut full = vec![insert_handle];
full.append(&mut nested_path);
best = Some((distance, entity, full));
}
}
}
continue;
}
// A nonuniform block transform turns circular geometry into an
// ellipse. The entity transform API retains the circle type, so
// decline this object pick instead of manufacturing a radius.
let curved = matches!(child_entity, EntityType::Circle(_) | EntityType::Arc(_))
|| matches!(child_entity, EntityType::LwPolyline(p) if p.vertices.iter().any(|v| v.bulge != 0.0));
let m = &combined.matrix.m;
let x = DVec3::new(m[0][0], m[1][0], m[2][0]);
let y = DVec3::new(m[0][1], m[1][1], m[2][1]);
let magnitude = x.length_squared().max(y.length_squared());
if curved
&& kind != Some(crate::snap::SnapType::Center)
&& (magnitude < 1e-24
|| (x.length_squared() - y.length_squared()).abs() > magnitude * 1e-10
|| x.dot(y).abs() > magnitude * 1e-10)
{
continue;
}
let mut placed = child_entity.clone();
crate::scene::view::dispatch::apply_transform(
&mut placed,
&EntityTransform::Affine(combined),
);
let Some(distance) = feature_pick_distance(&placed, model_point, kind) else {
continue;
};
if best.as_ref().is_none_or(|(d, _, _)| distance < *d) {
best = Some((distance, placed, vec![insert_handle]));
}
}
let (_, entity, found_path) = best?;
Some((entity, found_path))
}
}
/// Model→paper for a viewport, as an affine entity transform. Z passes
/// through unchanged so planar geometry keeps its elevation.
pub fn viewport_model_to_paper_transform(frame: &ViewportFrame) -> Transform {
let (sin, cos) = frame.twist.sin_cos();
let s = frame.scale;
let a = cos * s;
let b = sin * s;
// t = paper_center - R*s*model_target
let tx = frame.paper_center.x - (a * frame.model_target.x - b * frame.model_target.y);
let ty = frame.paper_center.y - (b * frame.model_target.x + a * frame.model_target.y);
Transform::from_matrix(Matrix4 {
m: [
[a, -b, 0.0, tx],
[b, a, 0.0, ty],
[0.0, 0.0, 1.0, 0.0],
[0.0, 0.0, 0.0, 1.0],
],
})
}
/// Distance from `point` to a wire's polyline, in XY. `None` when the wire has
/// no usable segment.
fn wire_polyline_nearest(
wire: &crate::scene::WireModel,
offset: DVec3,
point: DVec3,
) -> Option<(f64, DVec3)> {
let count = wire.points.len();
if count == 0 {
return None;
}
let at = |i: usize| -> DVec3 {
let hi = wire.points[i];
let lo = wire.points_low.get(i).copied().unwrap_or([0.0; 3]);
DVec3::new(
hi[0] as f64 + lo[0] as f64,
hi[1] as f64 + lo[1] as f64,
hi[2] as f64 + lo[2] as f64,
) + offset
};
let mut best = f64::INFINITY;
let mut best_point = DVec3::ZERO;
let mut previous: Option<DVec3> = None;
for i in 0..count {
let current = at(i);
if !current.x.is_finite() || !current.y.is_finite() {
previous = None;
continue;
}
let nearest = previous.map_or(current, |start| {
let delta = current - start;
let len2 = delta.truncate().length_squared();
let t = if len2 > 1e-24 {
((point - start).truncate().dot(delta.truncate()) / len2).clamp(0.0, 1.0)
} else {
0.0
};
start + delta * t
});
let distance = (nearest - point).truncate().length();
if distance < best {
best = distance;
best_point = nearest;
}
previous = Some(current);
}
best.is_finite().then_some((best, best_point))
}
/// Distance from `point` to the planar geometry object-pick dimensioning
/// supports. `None` for entity types that cannot be dimensioned by picking.
pub fn planar_pick_distance(entity: &EntityType, point: DVec3) -> Option<f64> {
let planar = crate::entities::curve::entity_curve(entity)?;
let normal = DVec3::from_array(planar.plane.normal()?);
if normal.cross(DVec3::Z).length_squared() > 1e-12 {
return None;
}
let uv = planar.plane.project(point.to_array())?;
let nearest = cadkernel::geom2d::closest_point(&planar.curve, uv);
Some(
(DVec3::from_array(planar.plane.point_at(nearest.point)) - point)
.truncate()
.length(),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn frame() -> ViewportFrame {
ViewportFrame {
viewport: Handle::NULL,
paper_center: DVec2::new(100.0, 50.0),
model_target: DVec2::new(1000.0, 2000.0),
scale: 0.1,
twist: 0.4,
locked: false,
}
}
#[test]
fn affine_transform_matches_the_frame_mapping() {
let f = frame();
let t = viewport_model_to_paper_transform(&f);
for model in [
DVec3::new(1000.0, 2000.0, 0.0),
DVec3::new(1100.0, 2000.0, 0.0),
DVec3::new(940.5, 2113.25, 0.0),
] {
let expected = f.model_to_paper(model);
let actual = t.apply(acadrust::types::Vector3::new(model.x, model.y, model.z));
assert!(
(actual.x - expected.x).abs() < 1e-9 && (actual.y - expected.y).abs() < 1e-9,
"{actual:?} != {expected:?}"
);
}
}
}
/// Distance to the particular feature accepted by a snap, rather than to the
/// nearest curve. A circle's center is deliberately not on its circumference.
pub(crate) fn feature_pick_distance(
entity: &EntityType,
point: DVec3,
kind: Option<crate::snap::SnapType>,
) -> Option<f64> {
use crate::snap::SnapType as S;
let points: Vec<acadrust::types::Vector3> = match kind {
Some(S::Center) => match entity {
EntityType::Circle(c) => vec![c.center_wcs()],
EntityType::Arc(a) => vec![a.center_wcs()],
EntityType::Ellipse(e) => vec![e.center],
_ => return None,
},
Some(S::Endpoint) => crate::scene::dimension_assoc::source_points(entity),
Some(S::Node) => match entity {
EntityType::Point(p) => vec![p.location],
_ => return None,
},
Some(S::Midpoint | S::Quadrant) => {
let planar = crate::entities::curve::entity_curve(entity)?;
cadkernel::geom2d::snap::characteristic_points(&planar.curve)
.into_iter()
.filter(|p| match kind {
Some(S::Midpoint) => p.kind == cadkernel::geom2d::snap::SnapKind::Midpoint,
_ => p.kind == cadkernel::geom2d::snap::SnapKind::Quadrant,
})
.map(|p| {
let p = planar.plane.point_at(p.point);
acadrust::types::Vector3::new(p[0], p[1], p[2])
})
.collect()
}
_ => return planar_pick_distance(entity, point),
};
points
.into_iter()
.map(|p| (DVec3::new(p.x, p.y, p.z) - point).truncate().length())
.min_by(f64::total_cmp)
}