Fix viewport dimension acquisition and preserve measurement scaling

This commit is contained in:
Christopher Overbeck committed 2026-09-15 14:13:19 -05:00
1 parent 369e0c9fc5
commit 2ee3782850
18 files changed
+957 -577

No files matched your search

+15 -2
View File
@@ -388,7 +388,9 @@ impl OpenCADStudio {
// accepted-snap list must stay index-parallel with the points the
// command collects (PR1). Interactive picks record themselves in
// the click handler and never reach here.
self.record_accepted_snap(None, None, *point);
if !self.record_accepted_snap(i, None, None, *point) {
return Task::none();
}
}
if default_start {
let StepInput::Point(point) = &input else {
@@ -402,6 +404,7 @@ impl OpenCADStudio {
self.push_ucs_to_cmd(i);
}
if let StepInput::EntityPick(handle, point) = &input {
if !self.dimension_acquisition_allowed(i, None) { return Task::none(); }
let solid_pick = matches!(
self.tabs[i].scene.document.get_entity(*handle),
Some(
@@ -474,6 +477,7 @@ impl OpenCADStudio {
}
let ctrl = self.ctrl_down;
let shift = self.shift_down;
let picked_handle = if let StepInput::EntityPick(h, _) = &input { Some(*h) } else { None };
let result: Option<CmdResult> = {
let Some(cmd) = self.tabs[i].active_cmd.as_mut() else {
return Task::none();
@@ -492,6 +496,10 @@ impl OpenCADStudio {
StepInput::Escape => Some(cmd.on_escape()),
}
};
if let Some(handle) = picked_handle {
self.record_dimension_entity_points(i, None, handle, Vec::new());
}
self.sync_dimension_snaps(i);
match result {
Some(r) => self.apply_cmd_result(r),
None => Task::none(),
@@ -2357,7 +2365,12 @@ impl OpenCADStudio {
// A dimension placed on the sheet but measuring model geometry
// through a viewport carries the compensation as a negative
// DIMLFAC override.
self.apply_viewport_dimension_measurement(i, &mut entity);
if !preserve_base_style {
crate::scene::creation_style::apply_current_creation_styles(&self.tabs[i].scene.document, &mut entity);
}
if !self.apply_viewport_dimension_measurement(i, &mut entity) {
return Task::none();
}
// ...and is deliberately *not* associated in paper space: its
// definition points are projected paper coordinates, so a
// paper-space association would bind it to geometry that is
+138 -242
View File
@@ -1,262 +1,162 @@
//! Viewport-aware dimension measurement (PR2).
//!
//! A dimension created in a paper-space layout while measuring geometry seen
//! through a content viewport must *place* itself in paper coordinates but
//! *report* the model measurement. Because a viewport frame is a similarity
//! (uniform scale plus in-plane twist), the model measurement of any linear,
//! aligned or radial span is exactly the raw paper measurement times the
//! viewport compensation `1 / scale` -- projecting the model span onto the
//! rotated paper axis and compensating is the same number. So no geometry is
//! rewritten: the compensation is persisted once, as a negative DIMLFAC
//! override on the dimension (see
//! [`MeasurementScale::viewport_dimlfac_override`]), and applied once, by the
//! linear formatter.
//!
//! Angular dimensions are never compensated -- a similarity preserves angles.
//!
//! ## Where the snaps come from
//! The per-point [`AcceptedSnap`] list is PR1's
//! ([`OpenCADStudio::accepted_snaps`]): the click handler records the real
//! snap for every interactive pick, and `apply_step_input` records a free
//! snap for typed / dynamic-input / headless points. There is exactly one
//! source of truth. Two things are layered on top here:
//!
//! * the explicit object-pick path ([`Scene::dimension_pick_through_viewport`])
//! is not a point step at all, so it records its own snaps
//! ([`OpenCADStudio::record_dimension_viewport_pick`]);
//! * a *typed* coordinate carries no snap, so PR1 records it as a free point
//! with no viewport. [`OpenCADStudio::measure_snap`] re-attaches the
//! content viewport under such a point, which is what makes
//! `DIMLINEAR` + typed coordinates inside a 1:10 viewport measure the model.
//! It is deliberately limited to snaps that are free *and* source-less, so a
//! genuine paper-sheet snap that happens to sit over a viewport is never
//! reinterpreted.
use acadrust::entities::Dimension;
use acadrust::types::Handle;
use acadrust::xdata::XDataValue;
use acadrust::EntityType;
use glam::DVec3;
//! Paper-space dimension acquisition and measurement. Only acquired geometry
//! carries viewport compensation; free and typed sheet coordinates stay on the sheet.
use super::OpenCADStudio;
use crate::command::DimensionAssociationSource;
use crate::entities::dim_override;
use crate::scene::viewport_ref::{AcceptedSnap, MeasurementScale, SnapSourceRef, ViewportFrame};
use acadrust::entities::Dimension;
use acadrust::types::Handle;
use acadrust::EntityType;
use glam::DVec3;
use super::OpenCADStudio;
/// How a pending dimension should be measured.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum DimensionMeasureSpace {
/// Plain measurement in the space the definition points live in.
Direct,
/// Every measuring point came through one viewport; compensate by its
/// scale.
Viewport(ViewportFrame),
/// The points came from different viewports, or from a viewport and the
/// bare sheet. Falls back to `Direct` with a warning — never a silently
/// wrong number.
Mixed,
}
impl OpenCADStudio {
/// Record the accepted snaps produced by the explicit object-pick path (a
/// dimension "select object" pick resolved through a content viewport).
///
/// An object pick is not a point step, so PR1's click handler never sees
/// it. One pick supplies *both* extension origins, so it is recorded
/// twice: that keeps the list index-parallel with the definition points
/// the command derives from the entity, and lets the "every measuring
/// point came through one viewport" classification see a complete set.
pub(crate) fn record_dimension_viewport_pick(
pub(crate) fn finish_command_click(&mut self, i: usize) {
let mut selection = self.tabs[i].scene.selection.borrow_mut();
selection.left_down = false;
selection.left_press_pos = None;
selection.left_press_time = None;
selection.left_dragging = false;
}
#[allow(dead_code)] // Consumed by the dependent association change.
pub(crate) fn dimension_measuring_snaps(&self, _i: usize) -> Vec<AcceptedSnap> {
self.accepted_snaps().to_vec()
}
pub(crate) fn dimension_measure_space(&self, i: usize) -> DimensionMeasureSpace {
let scene = &self.tabs[i].scene;
if scene.current_layout == "Model" || scene.active_viewport.is_some() {
return DimensionMeasureSpace::Direct;
}
let mut frame: Option<ViewportFrame> = None;
let mut direct = false;
for snap in self.accepted_snaps() {
match snap.frame {
Some(f) => match frame {
None => frame = Some(f),
Some(previous) if previous.viewport == f.viewport => {}
Some(_) => return DimensionMeasureSpace::Mixed,
},
None => direct = true,
}
}
match (frame, direct) {
(None, _) => DimensionMeasureSpace::Direct,
(Some(f), false) => DimensionMeasureSpace::Viewport(f),
_ => DimensionMeasureSpace::Mixed,
}
}
/// Reject an incompatible measuring input before it advances the command.
pub(crate) fn dimension_acquisition_allowed(
&mut self,
frame: ViewportFrame,
paper_point: DVec3,
model_point: DVec3,
i: usize,
viewport: Option<Handle>,
) -> bool {
if !self.tabs[i]
.active_cmd
.as_ref()
.is_some_and(|c| c.measures_through_viewports())
{
return true;
}
if self.accepted_snaps().iter().any(|s| s.viewport != viewport) {
self.command_line.push_error(crate::t!("Pick geometry from the same viewport, or use paper-space points for the whole dimension.").as_ref());
return false;
}
true
}
/// A command can reject a degenerate point without advancing its step.
pub(crate) fn sync_dimension_snaps(&mut self, i: usize) {
if let Some(cmd) = self.tabs[i]
.active_cmd
.as_ref()
.filter(|c| c.measures_through_viewports())
{
self.accepted_snaps
.truncate(cmd.dimension_acquired_points().len());
}
}
/// Record each definition point actually acquired by an object pick.
/// Arcs may supply three angular slots, lines two, and radial picks one.
pub(crate) fn record_dimension_entity_points(
&mut self,
i: usize,
frame: Option<ViewportFrame>,
entity: Handle,
block_path: Vec<Handle>,
) {
for _ in 0..2 {
let Some(cmd) = self.tabs[i]
.active_cmd
.as_ref()
.filter(|c| c.measures_through_viewports())
else {
return;
};
let points = cmd.dimension_acquired_points();
for paper_point in points.into_iter().skip(self.accepted_snaps.len()) {
self.push_accepted_snap(AcceptedSnap {
paper_point,
model_point,
viewport: Some(frame.viewport),
frame: Some(frame),
model_point: frame.map_or(paper_point, |f| f.paper_to_model(paper_point)),
viewport: frame.map(|f| f.viewport),
frame,
source: Some(SnapSourceRef {
source: DimensionAssociationSource::inferred(entity),
block_path: block_path.clone(),
// An object pick acquires the whole curve, not one
// feature; the per-slot feature point is derived from the
// committed dimension's own definition points.
snap_type: crate::snap::SnapType::Nearest,
snap_type: crate::snap::SnapType::ObjectPick,
intersection: None,
}),
});
}
}
/// The accepted snap for the `idx`-th collected point of the active
/// command, with the typed-coordinate viewport fallback applied.
///
/// Reads PR1's list -- there is no second store.
#[allow(dead_code)]
pub(crate) fn accepted_snap_for_point(&self, i: usize, idx: usize) -> Option<AcceptedSnap> {
self.accepted_snaps().get(idx).map(|snap| self.measure_snap(i, snap))
}
/// One accepted snap as *measurement* sees it.
///
/// Identical to the recorded snap except that a point which carries no
/// snap at all (a typed coordinate, a dynamic-input point, a headless
/// script point) and lands inside a content viewport is re-attached to
/// that viewport, so typing `100,50` inside a 1:10 viewport measures the
/// model just like clicking there would. A snap that landed on real
/// paper-sheet geometry keeps `viewport: None` and is never reinterpreted.
pub(crate) fn measure_snap(&self, i: usize, snap: &AcceptedSnap) -> AcceptedSnap {
if snap.frame.is_some() || snap.source.is_some() {
return snap.clone();
}
let scene = &self.tabs[i].scene;
if scene.current_layout == "Model" || scene.active_viewport.is_some() {
return snap.clone();
}
match scene.viewport_frame_at_paper_point(snap.paper_point) {
Some(frame) => AcceptedSnap {
paper_point: snap.paper_point,
model_point: frame.paper_to_model(snap.paper_point),
viewport: Some(frame.viewport),
frame: Some(frame),
source: None,
},
None => snap.clone(),
}
}
/// The accepted snaps that say what the pending dimension *measures*, in
/// collection order.
///
/// The final collected point of every dimension command in scope is the
/// dimension-line / text placement click, which says nothing about what is
/// being measured -- it routinely lands on bare sheet even for a viewport
/// measurement, and inside a viewport rectangle even for a plain paper
/// one. It is dropped here, which is also why association inference is
/// guarded on this set rather than on every recorded snap.
pub(crate) fn dimension_measuring_snaps(&self, i: usize) -> Vec<AcceptedSnap> {
let all = self.accepted_snaps();
let measuring = match all.len() {
0 | 1 => all,
n => &all[..n - 1],
};
measuring.iter().map(|snap| self.measure_snap(i, snap)).collect()
}
/// Classify how the pending dimension should be measured from the snaps
/// recorded for it.
pub(crate) fn dimension_measure_space(&self, i: usize) -> DimensionMeasureSpace {
let scene = &self.tabs[i].scene;
// Only a dimension being created *on the sheet* can measure through a
// viewport. Inside MSPACE the command already works in model space.
if scene.current_layout == "Model" || scene.active_viewport.is_some() {
return DimensionMeasureSpace::Direct;
}
let measuring = self.dimension_measuring_snaps(i);
let measuring = measuring.as_slice();
if measuring.is_empty() {
return DimensionMeasureSpace::Direct;
}
let mut frame: Option<ViewportFrame> = None;
let mut any_free = false;
for snap in measuring {
match snap.frame {
Some(f) => match frame {
None => frame = Some(f),
Some(existing) if existing.viewport == f.viewport => {}
Some(_) => return DimensionMeasureSpace::Mixed,
},
None => any_free = true,
}
}
match (frame, any_free) {
(None, _) => DimensionMeasureSpace::Direct,
(Some(f), false) => DimensionMeasureSpace::Viewport(f),
// One origin through a viewport and the other on the sheet: the
// two points are not in a common measurable space.
(Some(_), true) => DimensionMeasureSpace::Mixed,
}
}
/// Apply viewport compensation to a dimension about to be committed.
///
/// Whether the dimension may also take a paper-space association is a
/// separate question with the same answer source:
/// [`OpenCADStudio::dimension_association_allowed`].
pub(crate) fn apply_viewport_dimension_measurement(
&mut self,
i: usize,
entity: &mut EntityType,
) {
) -> bool {
let EntityType::Dimension(dimension) = entity else {
return;
return true;
};
let is_angular = matches!(
let angular = matches!(
dimension,
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_)
);
let frame = match self.dimension_measure_space(i) {
DimensionMeasureSpace::Direct => return,
DimensionMeasureSpace::Direct => return true,
DimensionMeasureSpace::Mixed => {
self.command_line.push_warning(
crate::t!(
"Mixed-space measurement is not supported: the extension origins come from different viewports, or from a viewport and the sheet. Measuring in paper space instead (no viewport scale compensation)."
)
.as_ref(),
);
return;
self.command_line.push_error(crate::t!("Pick geometry from the same viewport, or use paper-space points for the whole dimension.").as_ref());
return false;
}
DimensionMeasureSpace::Viewport(frame) => frame,
DimensionMeasureSpace::Viewport(f) => f,
};
// A similarity preserves angles, so an angular dimension reads the
// model angle already and needs no override.
if is_angular {
return;
if angular {
return true;
}
let compensation = frame.paper_to_model_length_factor();
let style_dimlfac = self.pending_dimension_style_dimlfac(i, entity);
if let Some(value) =
MeasurementScale::viewport_dimlfac_override(style_dimlfac, compensation)
{
// The negative DIMLFAC convention is resolved from the dimension's
// owner block, and the commit path (or, with DIMASSOC = 0, the
// explode path) reads the entity before `add_entity_to_layout`
// assigns one. Stamping the layout block here — the same handle
// the add would set — keeps the entity self-describing, so the
// text is compensated whichever path consumes it.
let layout_block = self.tabs[i].scene.current_layout_block_handle_pub();
if layout_block.is_valid() {
entity.common_mut().owner_handle = layout_block;
}
dim_override::set_on_entity(
entity,
dim_override::DIMLFAC,
Some(XDataValue::Real(value)),
);
let dimlfac = self.pending_dimension_style_dimlfac(i, entity);
let scale = MeasurementScale {
user_lfac: MeasurementScale::user_lfac_for_space(dimlfac, true),
viewport_compensation: frame.paper_to_model_length_factor(),
};
if !scale.paper_factor().is_finite() || scale.paper_factor() <= 0.0 {
return false;
}
// Owner is needed even by DIMASSOC=0's explode path, before insertion.
entity.common_mut().owner_handle = self.tabs[i].scene.current_layout_block_handle_pub();
scale.write_to_entity(entity);
true
}
/// Explicit "select object" pick for a dimension command, resolved through
/// a paper-space content viewport.
///
/// Paper-space hit testing only sees sheet entities, so a click on model
/// geometry displayed inside a viewport finds nothing. This maps the
/// cursor into model space through the viewport frame, hit-tests the
/// viewport's resident model wires (descending into block instances and
/// baking the instance transform), then hands the command a copy of the
/// picked entity projected onto the sheet. The commands therefore keep
/// working entirely in paper coordinates, and the measurement is
/// compensated at commit time like any other viewport measurement.
///
/// Returns `None` when the click is not inside a content viewport or hits
/// nothing, so the caller falls back to the ordinary paper-space pick.
pub(crate) fn try_dimension_viewport_entity_pick(
&mut self,
i: usize,
@@ -266,42 +166,38 @@ impl OpenCADStudio {
let pick = self.tabs[i]
.scene
.dimension_pick_through_viewport(paper, aperture_paper)?;
self.record_dimension_viewport_pick(
pick.frame,
pick.paper_point,
pick.model_point,
pick.entity_handle,
pick.block_path.clone(),
);
if !self.dimension_acquisition_allowed(i, Some(pick.frame.viewport)) {
return Some(crate::command::CmdResult::NeedPoint);
}
let command = self.tabs[i].active_cmd.as_mut()?;
command.inject_picked_entity(pick.paper_entity);
Some(command.on_entity_pick(pick.entity_handle, pick.paper_point))
let result = command.on_entity_pick(pick.entity_handle, pick.paper_point);
self.record_dimension_entity_points(
i,
Some(pick.frame),
pick.entity_handle,
pick.block_path,
);
Some(result)
}
/// The DIMLFAC the pending dimension would inherit *without* a viewport
/// override: its own existing override if it has one, otherwise its named
/// dimension style's value.
fn pending_dimension_style_dimlfac(&self, i: usize, entity: &EntityType) -> f64 {
let EntityType::Dimension(dimension) = entity else {
return 1.0;
};
if let Some(value) = dim_override::real(
dim_override::real(
&dimension.base().common.extended_data,
dim_override::DIMLFAC,
) {
return value;
}
let name = dimension.base().style_name.as_str();
self.tabs[i]
.scene
.document
.dim_styles
.iter()
.find(|style| {
style.name.eq_ignore_ascii_case(name)
|| (name.trim().is_empty() && style.name.eq_ignore_ascii_case("Standard"))
})
.map(|style| style.dimlfac)
.unwrap_or(1.0)
)
.or_else(|| {
self.tabs[i]
.scene
.document
.dim_styles
.iter()
.find(|s| s.name.eq_ignore_ascii_case(&dimension.base().style_name))
.map(|s| s.dimlfac)
})
.unwrap_or(1.0)
}
}
+2
View File
@@ -11,6 +11,8 @@ mod annotation_data;
mod command_driver;
pub(crate) mod commands;
pub(crate) mod dim_viewport;
#[cfg(test)]
mod viewport_dimension_tests;
mod document;
mod drafting_settings;
pub(crate) mod expr_eval;
+23 -26
View File
@@ -2374,6 +2374,8 @@ impl OpenCADStudio {
extension_dir: None,
viewport: None,
source: None,
secondary_source: None,
model_point: None,
});
if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
cmd.set_acquisition_hint(Some(pick.label));
@@ -3853,29 +3855,7 @@ impl OpenCADStudio {
command.set_ctrl(self.ctrl_down);
command.set_shift(self.shift_down);
}
// PR2: a dimension "select object" pick that lands inside a paper
// content viewport is resolved against the model geometry that
// viewport displays. Paper hit testing only sees sheet entities,
// so this runs first and falls through when it finds nothing.
let dimension_viewport_pick = if self.tabs[i].active_cmd.as_ref().is_some_and(
|command| command.needs_entity_pick() && command.measures_through_viewports(),
) {
let aperture_paper = {
let camera = self.tabs[i].scene.camera.borrow();
let per_px = if bounds.height > 0.0 {
camera.ortho_size() as f64 * 2.0 / bounds.height as f64
} else {
0.0
};
crate::ui::overlay::pick_box_aperture_px(self.pick_box) as f64 * per_px
};
self.try_dimension_viewport_entity_pick(i, pick_wcs, aperture_paper)
} else {
None
};
let result = if let Some(result) = dimension_viewport_pick {
Some(result)
} else if self.tabs[i]
let result = if self.tabs[i]
.active_cmd
.as_ref()
.map(|c| c.needs_structure_point_pick())
@@ -4065,6 +4045,7 @@ impl OpenCADStudio {
}
}
if !self.dimension_acquisition_allowed(i, None) { self.finish_command_click(i); return Task::none(); }
let shift = self.shift_down;
let result = self.tabs[i].active_cmd.as_mut().map(|c| {
// Shift-swap state for TRIM/EXTEND (#336).
@@ -4072,6 +4053,7 @@ impl OpenCADStudio {
c.set_entity_pick_direction(entity_pick_direction);
c.on_entity_pick(handle, entity_pick_point)
});
self.record_dimension_entity_points(i, None, handle, Vec::new());
// HATCHEDIT: after pick, inject hatch model data into the command.
if self.tabs[i]
.active_cmd
@@ -4142,11 +4124,24 @@ impl OpenCADStudio {
}
}
result
} else if let Some(result) = {
let dimension = self.tabs[i].active_cmd.as_ref().is_some_and(|c| c.measures_through_viewports());
if dimension {
let per_px = self.tabs[i].scene.camera.borrow().ortho_size() as f64 * 2.0 / (bounds.height as f64).max(1.0);
self.try_dimension_viewport_entity_pick(i, pick_wcs, crate::ui::overlay::pick_box_aperture_px(self.pick_box) as f64 * per_px)
} else { None }
} {
Some(result)
} else if self.tabs[i]
.active_cmd
.as_ref()
.is_some_and(|command| command.entity_pick_accepts_points())
{
let pending = self.pending_click_snap.take();
if !self.record_accepted_snap(i, pending.map(|(hit, _)| hit), pending.and_then(|(_, frame)| frame), pick_wcs) {
self.finish_command_click(i);
return Task::none();
}
self.refresh_command_point_pick_context(i);
self.tabs[i]
.active_cmd
@@ -4215,17 +4210,18 @@ impl OpenCADStudio {
self.tabs[i].dyn_active = 0;
}
}
self.last_point = Some(world_pt);
// Record what this point step accepted, alongside the point
// itself (PR1): paper point, model point, viewport, frame and
// geometry identity. Commands that don't care keep consuming
// `world_pt` exactly as before.
let pending = self.pending_click_snap.take();
self.record_accepted_snap(
if !self.record_accepted_snap(
i,
pending.map(|(hit, _)| hit),
pending.and_then(|(_, frame)| frame),
world_pt,
);
) { self.finish_command_click(i); return Task::none(); }
self.last_point = Some(world_pt);
// The one-shot snap override is spent by this pick —
// restore the running osnap configuration (#337).
self.snapper.clear_override();
@@ -4260,6 +4256,7 @@ impl OpenCADStudio {
}
};
self.sync_dimension_snaps(i);
if let Some(r) = result {
let task = self.apply_cmd_result(r);
let mut sel = self.tabs[i].scene.selection.borrow_mut();
+34 -34
View File
@@ -102,10 +102,11 @@ impl OpenCADStudio {
/// after ortho / polar / axis-lock / dynamic-input resolution.
pub(crate) fn record_accepted_snap(
&mut self,
tab: usize,
snap: Option<SnapResult>,
frame: Option<ViewportFrame>,
committed: glam::DVec3,
) {
) -> bool {
let accepted = match snap {
// A viewport snap keeps its frame only when the hit really came
// through it, so a paper-sheet snap never claims a model point.
@@ -115,7 +116,24 @@ impl OpenCADStudio {
}
None => AcceptedSnap::free(committed),
};
if self.tabs[tab]
.active_cmd
.as_ref()
.is_some_and(|cmd| cmd.measures_through_viewports())
{
if self.tabs[tab]
.active_cmd
.as_ref()
.is_some_and(|cmd| cmd.dimension_placement_pending())
{
return true;
}
if !self.dimension_acquisition_allowed(tab, accepted.viewport) {
return false;
}
}
self.push_accepted_snap(accepted);
true
}
/// Append an already-built accepted snap, keeping the retention cap.
@@ -165,30 +183,12 @@ impl OpenCADStudio {
return None;
}
let handles = self.tabs[i].scene.layout_content_viewports();
// Draw order puts the last-drawn viewport on top; probe top-most first
// so overlapping viewports resolve the same way they render.
for &handle in handles.iter().rev() {
if !self.tabs[i].scene.viewport_displays_content(handle) {
continue;
}
let Some(frame) = self.tabs[i].scene.viewport_frame(handle) else {
continue; // oblique / perspective viewport: unsupported
};
// Clip first (rectangle + non-rectangular clip boundary), in paper
// coordinates, so a cursor over the cut-away part of a clipped
// viewport sees nothing.
if !self
.tabs[i]
.scene
.viewport_displays_paper_point(handle, cursor_paper.truncate())
{
continue;
}
let Some((cam, rect)) = self
.tabs[i]
.scene
.viewport_edit_frame_for(handle, canvas)
for frame in self.tabs[i]
.scene
.viewport_frames_at_paper_point(cursor_paper)
{
let handle = frame.viewport;
let Some((cam, rect)) = self.tabs[i].scene.viewport_edit_frame_for(handle, canvas)
else {
continue;
};
@@ -208,8 +208,7 @@ impl OpenCADStudio {
// the viewport camera projects back onto `local`.
let model_cursor = frame.paper_to_model(cursor_paper);
let wires = self
.tabs[i]
let wires = self.tabs[i]
.scene
.model_wires_for_viewport_arc(handle, bounds.height);
let candidates = self.tabs[i].scene.interaction_candidates_near(
@@ -221,14 +220,14 @@ impl OpenCADStudio {
self.snapper.osnap_radius_px,
);
// Object snap only. The grid belongs to the paper sheet, and the
// rubber-band origin / tracking points are paper-space points, so
// perpendicular and extension feet would be nonsense in model
// space. Save and restore, since `Snapper` is shared state.
// The grid belongs to the sheet. Translate the construction
// anchor for perpendicular/tangent snaps into this viewport's
// model coordinates, then restore the shared snapper state.
let saved_grid = self.snapper.grid_snap_on;
let saved_from = self.snapper.from_point;
self.snapper.grid_snap_on = false;
self.snapper.from_point = None;
self.snapper.from_point =
saved_from.map(|p| frame.paper_to_model(p.as_dvec3()).as_vec3());
let hit = self.snapper.snap(
model_cursor,
local,
@@ -251,8 +250,7 @@ impl OpenCADStudio {
// the aperture reaching past the clip. Reject those: a snap must be
// a real feature the viewport actually displays.
let paper = frame.model_to_paper(hit.world);
if !self
.tabs[i]
if !self.tabs[i]
.scene
.viewport_displays_paper_point(handle, paper.truncate())
{
@@ -291,5 +289,7 @@ fn project_hit_to_paper(
}),
viewport: Some(viewport),
source: hit.source,
secondary_source: hit.secondary_source,
model_point: Some(hit.world),
}
}
+379
View File
@@ -0,0 +1,379 @@
use super::OpenCADStudio;
use crate::command::{DimensionAssociationSource, StepInput};
use crate::entities::dim_override;
use crate::scene::viewport_ref::{AcceptedSnap, MeasurementScale, ViewportFrame};
use crate::scene::Scene;
use crate::snap::{SnapResult, SnapType};
use acadrust::entities::{Circle, Dimension, Line, Viewport};
use acadrust::types::{Handle, Vector3};
use acadrust::{CadDocument, EntityType};
use glam::DVec3;
fn fixture() -> (OpenCADStudio, Handle, ViewportFrame) {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
let mut scene = Scene::new();
let line = scene.add_entity(EntityType::Line(Line::from_points(
Vector3::ZERO,
Vector3::new(100.0, 0.0, 0.0),
)));
scene.document.add_layout("Dimensions").unwrap();
scene.set_current_layout("Dimensions".into());
let mut viewport = Viewport::new();
viewport.id = 2;
viewport.center = Vector3::new(50.0, 50.0, 0.0);
viewport.width = 100.0;
viewport.height = 100.0;
viewport.view_height = 1000.0;
viewport.custom_scale = 0.1;
viewport.view_direction = Vector3::UNIT_Z;
viewport.status.is_on = true;
let viewport = scene.add_entity(EntityType::Viewport(viewport));
let frame = scene.viewport_frame(viewport).unwrap();
scene.document.header.dimension_associativity = 2;
let i = app.active_tab;
app.tabs[i].scene = scene;
(app, line, frame)
}
fn hit(frame: ViewportFrame, source: Handle, model: DVec3) -> SnapResult {
SnapResult {
world: frame.model_to_paper(model),
model_point: Some(model),
screen: iced::Point::ORIGIN,
snap_type: SnapType::Endpoint,
tangent_obj: None,
extension_base: None,
extension_base2: None,
extension_origin: None,
extension_dir: None,
viewport: Some(frame.viewport),
source: Some(DimensionAssociationSource::inferred(source)),
secondary_source: None,
}
}
fn point(app: &mut OpenCADStudio, frame: ViewportFrame, source: Handle, model: DVec3) {
let i = app.active_tab;
let hit = hit(frame, source, model);
let paper = hit.world.with_z(0.0);
assert!(app.record_accepted_snap(i, Some(hit), Some(frame), paper));
let result = app.tabs[i].active_cmd.as_mut().unwrap().on_point(paper);
app.sync_dimension_snaps(i);
let _ = app.apply_cmd_result(result);
}
fn dimension(doc: &CadDocument) -> &Dimension {
doc.entities()
.find_map(|e| match e {
EntityType::Dimension(d) => Some(d),
_ => None,
})
.unwrap()
}
fn displayed(doc: &CadDocument) -> f64 {
let dim = dimension(doc);
if matches!(dim, Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_)) {
return dim.measurement();
}
let factor = dim_override::real(&dim.base().common.extended_data, dim_override::DIMLFAC)
.or_else(|| {
doc.dim_styles
.get(&dim.base().style_name)
.map(|s| s.dimlfac)
})
.unwrap_or(1.0);
dim.measurement() * MeasurementScale::user_lfac_for_space(factor, true)
}
fn finish_aligned(app: &mut OpenCADStudio, line: Handle, frame: ViewportFrame) {
let _ = app.dispatch_command("DIMALIGNED");
point(app, frame, line, DVec3::ZERO);
point(app, frame, line, DVec3::X * 100.0);
let _ = app.feed_command(StepInput::Point(DVec3::new(250.0, 200.0, 0.0)));
}
#[test]
fn viewport_dimension_projected_snap_keeps_identity_and_elevation() {
let (_, line, frame) = fixture();
let model = DVec3::new(100.0, 0.0, 35.0);
let hit = hit(frame, line, model);
let accepted =
AcceptedSnap::from_snap(&hit, Some(frame)).with_paper_point(hit.world.with_z(0.0));
assert_eq!(accepted.model_point, model);
assert_eq!(accepted.source.as_ref().unwrap().source.handle, line);
let moved = accepted.with_paper_point(hit.world + DVec3::X);
assert!(moved.source.is_none());
assert!((moved.model_point - frame.paper_to_model(moved.paper_point)).length() < 1e-9);
}
#[test]
fn viewport_dimension_creation_preserves_active_units_and_roundtrips() {
for factor in [25.4, -25.4] {
let (mut app, line, frame) = fixture();
let i = app.active_tab;
let mut style = acadrust::tables::DimStyle::new("Millimetres");
style.dimlfac = factor;
app.tabs[i].scene.document.dim_styles.add(style).unwrap();
app.tabs[i].scene.document.header.current_dimstyle_name = "Millimetres".into();
finish_aligned(&mut app, line, frame);
let doc = &app.tabs[i].scene.document;
assert_eq!(dimension(doc).base().style_name, "Millimetres");
assert!((displayed(doc) - 2540.0).abs() < 1e-5, "{}", displayed(doc));
let data = MeasurementScale::read(&dimension(doc).base().common.extended_data).unwrap();
assert_eq!(data.user_lfac, 25.4);
assert_eq!(doc.header.dimension_associativity, 2);
for ext in ["dxf", "dwg"] {
let bytes = crate::io::save_to_bytes(doc, ext, doc.version).unwrap();
let loaded = crate::io::load_bytes(&format!("viewport.{ext}"), bytes).unwrap();
assert!(
(displayed(&loaded) - 2540.0).abs() < 1e-4,
"{ext}: {}",
displayed(&loaded)
);
assert_eq!(
MeasurementScale::read(&dimension(&loaded).base().common.extended_data),
Some(data)
);
}
app.undo_steps(1);
assert!(!app.tabs[i]
.scene
.document
.entities()
.any(|e| matches!(e, EntityType::Dimension(_))));
app.redo_steps(1);
assert!((displayed(&app.tabs[i].scene.document) - 2540.0).abs() < 1e-5);
}
}
#[test]
fn viewport_dimension_mixed_and_degenerate_picks_allow_retry() {
let (mut app, line, frame) = fixture();
let i = app.active_tab;
let _ = app.dispatch_command("DIMALIGNED");
point(&mut app, frame, line, DVec3::ZERO);
point(&mut app, frame, line, DVec3::ZERO);
assert_eq!(app.accepted_snaps().len(), 1);
let _ = app.feed_command(StepInput::Point(DVec3::new(60.0, 50.0, 0.0)));
assert_eq!(app.accepted_snaps().len(), 1);
assert_eq!(
app.tabs[i]
.active_cmd
.as_ref()
.unwrap()
.dimension_acquired_points()
.len(),
1
);
let other = ViewportFrame {
viewport: Handle::new(0xFFFF),
..frame
};
assert!(!app.record_accepted_snap(
i,
Some(hit(other, line, DVec3::X * 100.0)),
Some(other),
DVec3::new(60.0, 50.0, 0.0)
));
point(&mut app, frame, line, DVec3::X * 100.0);
let _ = app.feed_command(StepInput::Point(DVec3::new(250.0, 200.0, 0.0)));
assert!((displayed(&app.tabs[i].scene.document) - 100.0).abs() < 1e-5);
}
#[test]
fn viewport_dimension_typed_sheet_points_keep_paper_units() {
let (mut app, _, _) = fixture();
let result = app.automation_op(r#"{"op":"run","cmd":"DIMALIGNED 50,50 60,50 60,55"}"#);
assert_eq!(result["ok"], true);
let doc = &app.tabs[app.active_tab].scene.document;
assert!((displayed(doc) - 10.0).abs() < 1e-9);
assert!(MeasurementScale::read(&dimension(doc).base().common.extended_data).is_none());
}
#[test]
fn viewport_dimension_linear_aligned_and_radial_object_picks() {
for (name, expected) in [
("DIMLINEAR", 100.0),
("DIMALIGNED", 100.0),
("DIMRADIUS", 100.0),
("DIMDIAMETER", 200.0),
] {
let (mut app, _, frame) = fixture();
let i = app.active_tab;
let radial = name == "DIMRADIUS" || name == "DIMDIAMETER";
let model = if radial {
let mut circle = Circle::new();
circle.center = Vector3::new(0.0, 200.0, 0.0);
circle.radius = 100.0;
app.tabs[i].scene.set_current_layout("Model".into());
app.tabs[i].scene.add_entity(EntityType::Circle(circle));
app.tabs[i].scene.set_current_layout("Dimensions".into());
DVec3::new(100.0, 200.0, 0.0)
} else {
DVec3::new(50.0, 0.0, 0.0)
};
let _ = app.dispatch_command(name);
if !radial {
let _ = app.feed_command(StepInput::Enter);
}
let result = app
.try_dimension_viewport_entity_pick(i, frame.model_to_paper(model), 0.5)
.unwrap();
let _ = app.apply_cmd_result(result);
assert_eq!(
app.accepted_snaps().len(),
if radial { 1 } else { 2 },
"{name}"
);
let _ = app.feed_command(StepInput::Point(DVec3::new(55.0, 80.0, 0.0)));
assert!(
(displayed(&app.tabs[i].scene.document) - expected).abs() < 1e-4,
"{name}: {}",
displayed(&app.tabs[i].scene.document)
);
}
}
#[test]
fn viewport_dimension_angular_preserves_angle_without_length_factor() {
let (mut app, line, frame) = fixture();
let _ = app.dispatch_command("DIMANGULAR");
let _ = app.feed_command(StepInput::Enter);
point(&mut app, frame, line, DVec3::ZERO);
point(&mut app, frame, line, DVec3::X * 100.0);
point(&mut app, frame, line, DVec3::Y * 100.0);
let _ = app.feed_command(StepInput::Point(DVec3::new(70.0, 70.0, 0.0)));
let doc = &app.tabs[app.active_tab].scene.document;
assert!((dimension(doc).measurement() - 90.0).abs() < 1e-5);
assert!(MeasurementScale::read(&dimension(doc).base().common.extended_data).is_none());
}
#[test]
fn viewport_dimension_snap_query_matches_pan_scale_and_twist() {
for (height, twist, target) in [
(1000.0, 0.0, Vector3::ZERO),
(500.0, 0.45, Vector3::new(80.0, 20.0, 0.0)),
] {
let (mut app, line, initial) = fixture();
let i = app.active_tab;
let Some(EntityType::Viewport(vp)) =
app.tabs[i].scene.document.get_entity_mut(initial.viewport)
else {
panic!()
};
vp.view_height = height;
vp.custom_scale = vp.height / height;
vp.twist_angle = twist;
vp.view_target = target;
let frame = app.tabs[i].scene.viewport_frame(initial.viewport).unwrap();
let model = DVec3::X * 100.0;
let paper = frame.model_to_paper(model);
let bounds = iced::Rectangle {
x: 0.0,
y: 0.0,
width: 1000.0,
height: 1000.0,
};
let screen = {
let cam = app.tabs[i].scene.camera.borrow();
let ndc = cam
.view_proj_rte(bounds)
.project_point3((paper - cam.eye()).as_vec3());
iced::Point::new((ndc.x + 1.0) * 500.0, (1.0 - ndc.y) * 500.0)
};
app.snapper.snap_enabled = true;
let (hit, acquired_frame) = app
.paper_viewport_snap(i, screen, (1000.0, 1000.0), paper)
.unwrap();
assert_eq!(hit.source.unwrap().handle, line);
assert!((hit.world - paper).length() < 1e-5, "{hit:?}");
let accepted = AcceptedSnap::from_snap(&hit, Some(acquired_frame));
assert!((accepted.model_point - model).length() < 1e-6);
}
}
#[test]
fn viewport_dimension_eligibility_is_shared_and_uses_clipping() {
use acadrust::entities::LwPolyline;
use acadrust::types::Vector2;
let (mut app, _, first) = fixture();
let scene = &mut app.tabs[app.active_tab].scene;
let mut overlay = scene.document.get_entity(first.viewport).unwrap().clone();
if let EntityType::Viewport(vp) = &mut overlay {
vp.id = 3;
vp.width = 200.0;
}
overlay.common_mut().handle = Handle::NULL;
let top = scene.add_entity(overlay);
let paper = DVec3::new(55.0, 50.0, 0.0);
assert_eq!(
scene.viewport_frame_at_paper_point(paper).unwrap().viewport,
top
);
assert_eq!(
scene
.dimension_pick_through_viewport(paper, 0.2)
.unwrap()
.frame
.viewport,
top
);
let mut clip = LwPolyline::from_points(vec![
Vector2::new(0.0, 0.0),
Vector2::new(100.0, 0.0),
Vector2::new(0.0, 100.0),
]);
clip.is_closed = true;
let clip = scene.add_entity(EntityType::LwPolyline(clip));
if let Some(EntityType::Viewport(vp)) = scene.document.get_entity_mut(top) {
vp.clip_boundary_handle = clip;
}
assert_eq!(
scene.viewport_frame_at_paper_point(paper).unwrap().viewport,
first.viewport
);
assert_eq!(
scene
.dimension_pick_through_viewport(paper, 0.2)
.unwrap()
.frame
.viewport,
first.viewport
);
if let Some(EntityType::Viewport(vp)) = scene.document.get_entity_mut(first.viewport) {
vp.status.is_on = false;
}
assert!(scene.viewport_frame_at_paper_point(paper).is_none());
assert!(scene.dimension_pick_through_viewport(paper, 0.2).is_none());
}
#[test]
fn viewport_dimension_exploded_commit_uses_compensated_text_and_undo() {
let (mut app, line, frame) = fixture();
let i = app.active_tab;
app.tabs[i].scene.document.header.dimension_associativity = 0;
finish_aligned(&mut app, line, frame);
let doc = &app.tabs[i].scene.document;
assert!(!doc
.entities()
.any(|e| matches!(e, EntityType::Dimension(_))));
let texts: Vec<_> = doc
.entities()
.filter_map(|e| match e {
EntityType::Text(t) => Some(t.value.clone()),
EntityType::MText(t) => Some(t.value.clone()),
_ => None,
})
.collect();
assert!(texts.iter().any(|t| t.contains("100")), "{texts:?}");
app.undo_steps(1);
assert_eq!(app.tabs[i].scene.document.header.dimension_associativity, 0);
app.redo_steps(1);
assert!(app.tabs[i]
.scene
.document
.entities()
.any(|e| matches!(e, EntityType::Text(_) | EntityType::MText(_))));
}
+8
View File
@@ -2254,6 +2254,14 @@ pub trait CadCommand: Send {
false
}
/// Definition points acquired so far, in the command's working space.
/// Used to retain references only for accepted measuring inputs.
fn dimension_acquired_points(&self) -> Vec<DVec3> { Vec::new() }
/// The next point places annotation rather than acquiring geometry.
fn dimension_placement_pending(&self) -> bool { false }
fn needs_entity_pick(&self) -> bool {
false
}
+5 -19
View File
@@ -1638,25 +1638,11 @@ pub(crate) fn dimension_in_paper_space(dimension: &Dimension, document: &CadDocu
if !owner.is_valid() {
return false;
}
let model_block = document
.block_records
.get("*Model_Space")
.map(|record| record.handle)
.filter(|handle| handle.is_valid())
.or_else(|| {
document.objects.values().find_map(|object| match object {
acadrust::objects::ObjectType::Layout(layout)
if layout.name == "Model" && !layout.block_record.is_null() =>
{
Some(layout.block_record)
}
_ => None,
})
});
match model_block {
Some(model) => owner != model,
None => false,
}
document.objects.values().any(|object| matches!(object,
acadrust::objects::ObjectType::Layout(layout)
if layout.name != "Model" && layout.block_record == owner
))
}
/// The value a dimension's text reports for its measurement: the raw
+11
View File
@@ -167,6 +167,17 @@ impl CadCommand for AlignedDimensionCommand {
true
}
fn dimension_acquired_points(&self) -> Vec<DVec3> {
match self.step {
Step::First => vec![], Step::Second(p) => vec![p],
Step::DimLine { p1, p2 } => vec![p1, p2],
}
}
fn dimension_placement_pending(&self) -> bool {
matches!(self.step, Step::DimLine { .. })
}
fn input_kind(&self) -> InputKind {
if self.awaiting_text {
InputKind::FreeText
+14
View File
@@ -488,6 +488,20 @@ impl CadCommand for AngularDimensionCommand {
true
}
fn dimension_acquired_points(&self) -> Vec<DVec3> {
match self.step {
Step::Vertex => vec![], Step::FirstRay(p) => vec![p],
Step::SecondRay { vertex, first } | Step::CircleSecondRay { vertex, first, .. } => vec![vertex, first],
Step::SecondLine { first_start, first_end, .. } => vec![first_start, first_end],
Step::ArcPoint3 { vertex, first, second } => vec![vertex, first, second],
Step::ArcPoint2 { first_start, first_end, second_start, second_end } => vec![first_start, first_end, second_start, second_end],
}
}
fn dimension_placement_pending(&self) -> bool {
matches!(self.step, Step::ArcPoint3 { .. } | Step::ArcPoint2 { .. })
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
+10
View File
@@ -138,6 +138,16 @@ impl CadCommand for DiameterDimensionCommand {
true
}
fn dimension_acquired_points(&self) -> Vec<DVec3> {
match self.step {
Step::SelectObject => vec![], Step::DimLine(source) => vec![dvec(source.point_at_angle(source.start_angle))],
}
}
fn dimension_placement_pending(&self) -> bool {
matches!(self.step, Step::DimLine(_))
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
+11
View File
@@ -223,6 +223,17 @@ impl CadCommand for LinearDimensionCommand {
true
}
fn dimension_acquired_points(&self) -> Vec<DVec3> {
match self.step {
Step::FirstPoint => vec![], Step::SecondPoint(p) => vec![p],
Step::DimensionLine { first, second } => vec![first, second],
}
}
fn dimension_placement_pending(&self) -> bool {
matches!(self.step, Step::DimensionLine { .. })
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
+10
View File
@@ -136,6 +136,16 @@ impl CadCommand for RadiusDimensionCommand {
true
}
fn dimension_acquired_points(&self) -> Vec<DVec3> {
match self.step {
Step::SelectObject => vec![], Step::DimLine(source) => vec![dvec(source.point_at_angle(source.start_angle))],
}
}
fn dimension_placement_pending(&self) -> bool {
matches!(self.step, Step::DimLine(_))
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
+14 -3
View File
@@ -160,6 +160,18 @@ impl Scene {
})
}
/// Visible, supported content viewports at a sheet point, topmost first.
/// Shared by snapping and explicit dimension object picking.
pub fn viewport_frames_at_paper_point(&self, paper: glam::DVec3) -> Vec<crate::scene::viewport_ref::ViewportFrame> {
if self.current_layout == "Model" || self.active_viewport.is_some() { return Vec::new(); }
self.layout_content_viewports().iter().rev().filter_map(|&handle| {
if !self.viewport_displays_content(handle) || !self.viewport_displays_paper_point(handle, paper.truncate()) {
return None;
}
self.viewport_frame(handle)
}).collect()
}
/// Does `paper` lie inside the viewport's displayed area?
///
/// Always bounded by the viewport's paper rectangle; a viewport with a
@@ -184,9 +196,8 @@ impl Scene {
}
let poly = self.clip_boundary_polygon(vp.clip_boundary_handle, vp.center.z as f32);
if poly.len() < 3 {
// A clip entity we cannot tessellate: fall back to the rectangle
// rather than refusing every snap in the viewport.
return true;
// An unsupported boundary cannot establish snap eligibility.
return false;
}
point_in_polygon_xy(paper, &poly)
}
+128 -114
View File
@@ -21,7 +21,9 @@ use super::*;
use crate::command::EntityTransform;
use crate::scene::viewport_ref::ViewportFrame;
use acadrust::types::{Matrix4, Transform};
use glam::{DVec2, DVec3};
#[cfg(test)]
use glam::DVec2;
use glam::DVec3;
/// A model entity resolved by clicking inside a paper-space content viewport.
#[derive(Clone, Debug)]
@@ -44,36 +46,16 @@ pub struct ViewportDimPick {
}
impl Scene {
/// The smallest *on* content viewport of the current layout whose paper
/// rectangle contains `paper`. `None` on the Model tab, while editing
/// inside a viewport (MSPACE), or when the point is on bare sheet.
pub fn content_viewport_at_paper_point(&self, paper: DVec3) -> Option<Handle> {
if self.current_layout == "Model" || self.active_viewport.is_some() {
return None;
}
let (_, _, handles) = self.paper_viewport_handles();
handles
.iter()
.filter_map(|handle| {
let Some(EntityType::Viewport(vp)) = self.document.get_entity(*handle) else {
return None;
};
if !vp.status.is_on || vp.width <= 0.0 || vp.height <= 0.0 {
return None;
}
let dx = (paper.x - vp.center.x).abs();
let dy = (paper.y - vp.center.y).abs();
(dx <= vp.width * 0.5 && dy <= vp.height * 0.5)
.then(|| (*handle, vp.width * vp.height))
})
.min_by(|a, b| a.1.total_cmp(&b.1))
.map(|(handle, _)| handle)
self.viewport_frames_at_paper_point(paper)
.first()
.map(|f| f.viewport)
}
/// [`Scene::content_viewport_at_paper_point`] resolved to a frame.
pub fn viewport_frame_at_paper_point(&self, paper: DVec3) -> Option<ViewportFrame> {
self.viewport_frame(self.content_viewport_at_paper_point(paper)?)
self.viewport_frames_at_paper_point(paper)
.into_iter()
.next()
}
/// Resolve an explicit dimension object pick made on the sheet but landing
@@ -86,28 +68,42 @@ impl Scene {
paper: DVec3,
aperture_paper: f64,
) -> Option<ViewportDimPick> {
let frame = self.viewport_frame_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);
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 top = self.nearest_model_wire_handle(frame.viewport, model_point, aperture_model)?;
let (entity, block_path) = self.resolve_measurable_entity(top, model_point)?;
let Some(top) =
self.nearest_model_wire_handle(frame.viewport, model_point, aperture_model)
else {
continue;
};
let Some((entity, block_path)) =
self.resolve_measurable_entity(top, model_point, Some(frame.viewport))
else {
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();
Some(ViewportDimPick {
frame,
entity_handle: if entity_handle.is_valid() { entity_handle } else { top },
block_path,
paper_entity,
paper_point: paper,
model_point,
})
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(ViewportDimPick {
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
@@ -119,7 +115,23 @@ impl Scene {
model_point: DVec3,
aperture: f64,
) -> Option<Handle> {
let wires = self.model_wires_for_viewport_arc(viewport, 0.0);
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 {
@@ -133,8 +145,17 @@ impl Scene {
.as_ref()
.map(|inst| DVec3::from_array(inst.translation))
.unwrap_or(DVec3::ZERO);
let distance = wire_polyline_distance(wire, offset, model_point);
if let Some(distance) = distance {
let distance = wire_polyline_nearest(wire, offset, model_point);
if let Some((distance, nearest)) = distance {
let Some(frame) = self.viewport_frame(viewport) else {
continue;
};
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));
}
@@ -146,10 +167,11 @@ impl Scene {
/// 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.
fn resolve_measurable_entity(
pub(crate) fn resolve_measurable_entity(
&self,
handle: Handle,
model_point: DVec3,
viewport: Option<Handle>,
) -> Option<(EntityType, Vec<Handle>)> {
let entity = self.document.get_entity(handle)?;
match entity {
@@ -161,6 +183,7 @@ impl Scene {
model_point,
&mut path,
0,
viewport,
)?;
Some((found.0, found.1))
}
@@ -180,6 +203,7 @@ impl Scene {
model_point: DVec3,
path: &mut Vec<Handle>,
depth: usize,
viewport: Option<Handle>,
) -> Option<(EntityType, Vec<Handle>)> {
const MAX_DEPTH: usize = 8;
if depth > MAX_DEPTH {
@@ -199,6 +223,14 @@ impl Scene {
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 matches!(child_entity, EntityType::Insert(_)) {
let mut nested = Vec::new();
if let Some((entity, mut nested_path)) = self.descend_block_instance(
@@ -207,6 +239,7 @@ impl Scene {
model_point,
&mut nested,
depth + 1,
viewport,
) {
if let Some(distance) = planar_pick_distance(&entity, model_point) {
if best.as_ref().is_none_or(|(d, _, _)| distance < *d) {
@@ -218,6 +251,22 @@ impl Scene {
}
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
&& (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,
@@ -259,11 +308,11 @@ pub fn viewport_model_to_paper_transform(frame: &ViewportFrame) -> Transform {
/// Distance from `point` to a wire's polyline, in XY. `None` when the wire has
/// no usable segment.
fn wire_polyline_distance(
fn wire_polyline_nearest(
wire: &crate::scene::WireModel,
offset: DVec3,
point: DVec3,
) -> Option<f64> {
) -> Option<(f64, DVec3)> {
let count = wire.points.len();
if count == 0 {
return None;
@@ -278,6 +327,7 @@ fn wire_polyline_distance(
) + 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);
@@ -285,77 +335,41 @@ fn wire_polyline_distance(
previous = None;
continue;
}
match previous {
Some(start) => best = best.min(segment_distance_xy(start, current, point)),
None => best = best.min((current.truncate() - point.truncate()).length()),
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)
}
fn segment_distance_xy(a: DVec3, b: DVec3, p: DVec3) -> f64 {
let ab = b.truncate() - a.truncate();
let ap = p.truncate() - a.truncate();
let len2 = ab.length_squared();
if len2 < 1e-24 {
return ap.length();
}
let t = (ap.dot(ab) / len2).clamp(0.0, 1.0);
(ap - ab * t).length()
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 p = point.truncate();
match entity {
EntityType::Line(line) => Some(segment_distance_xy(
DVec3::new(line.start.x, line.start.y, line.start.z),
DVec3::new(line.end.x, line.end.y, line.end.z),
point,
)),
EntityType::Circle(circle) => {
let c = DVec2::new(circle.center.x, circle.center.y);
Some(((p - c).length() - circle.radius).abs())
}
EntityType::Arc(arc) => {
let c = DVec2::new(arc.center.x, arc.center.y);
Some(((p - c).length() - arc.radius).abs())
}
EntityType::LwPolyline(polyline) => polyline_distance(
polyline
.vertices
.iter()
.map(|v| DVec3::new(v.location.x, v.location.y, polyline.elevation)),
polyline.is_closed,
point,
),
EntityType::Polyline2D(polyline) => polyline_distance(
polyline
.vertices
.iter()
.map(|v| DVec3::new(v.location.x, v.location.y, polyline.elevation)),
polyline.is_closed(),
point,
),
_ => None,
}
}
fn polyline_distance(
points: impl IntoIterator<Item = DVec3>,
closed: bool,
point: DVec3,
) -> Option<f64> {
let points: Vec<DVec3> = points.into_iter().collect();
if points.len() < 2 {
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 count = if closed { points.len() } else { points.len() - 1 };
(0..count)
.map(|i| segment_distance_xy(points[i], points[(i + 1) % points.len()], point))
.reduce(f64::min)
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)]
+69 -104
View File
@@ -1,20 +1,4 @@
//! Shared interfaces for paper-space snapping through layout viewports and
//! for viewport-aware dimension measurement.
//!
//! Three PRs build on this module concurrently:
//! * PR1 (viewport snap) constructs [`ViewportFrame`] from the live viewport
//! camera and fills [`SnapSourceRef`] / [`AcceptedSnap`] when a snap is
//! accepted in paper space.
//! * PR2 (dimension measurement) consumes [`AcceptedSnap`] and uses
//! [`MeasurementScale`] to split the user's DIMLFAC from viewport
//! compensation so the compensation is applied exactly once.
//! * PR3 (associations) turns [`SnapSourceRef`] into persistent
//! `AssocDimensionReference` chains (dimension -> viewport -> block path ->
//! entity -> feature) and resolves them back.
//!
//! Anything here is a contract between those branches. Extend by adding
//! fields/methods; do not rename or remove what exists without updating the
//! sibling branches.
//! Coordinates and feature identity shared by viewport acquisition and dimensions.
use acadrust::types::Handle;
use glam::{DMat3, DVec2, DVec3};
@@ -27,8 +11,7 @@ use crate::snap::SnapType;
/// scale (paper units per model unit) and in-plane rotation (`twist`,
/// radians, counter-clockwise, applied to model geometry before scaling).
///
/// PR1 owns construction (`Scene::viewport_frame`); everyone else only reads
/// it. All arithmetic is `f64`.
/// Constructed by `Scene::viewport_frame`. All arithmetic is `f64`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ViewportFrame {
/// Viewport entity handle.
@@ -105,6 +88,8 @@ pub struct SnapSourceRef {
pub source: DimensionAssociationSource,
pub block_path: Vec<Handle>,
pub snap_type: SnapType,
/// The other entity/instance path for a true intersection.
pub intersection: Option<Box<SnapSourceRef>>,
}
/// A snap accepted by a point-input command, with enough information for
@@ -149,31 +134,29 @@ impl AcceptedSnap {
self.viewport.is_some()
}
/// Build an accepted snap from a [`crate::snap::SnapResult`].
///
/// * `frame` `Some`: the snap ran against MODEL geometry through that
/// viewport, so `snap.world` is a model point and the paper point is its
/// projection onto the sheet.
/// * `frame` `None`: the snap ran in the current space directly, so paper
/// and model point coincide.
///
/// PR1 fills `source` from `SnapResult::source` (the owning entity handle
/// the snap engine attributed the feature to); `block_path` stays empty
/// until PR3 resolves block instance chains.
/// `snap.world` is always in the command's current space. A projected
/// viewport hit also retains its original model coordinate explicitly.
pub fn from_snap(snap: &crate::snap::SnapResult, frame: Option<ViewportFrame>) -> Self {
let (paper_point, model_point) = match &frame {
Some(frame) => (frame.model_to_paper(snap.world), snap.world),
None => (snap.world, snap.world),
let frame = frame.filter(|frame| snap.viewport == Some(frame.viewport));
let source_ref = |source| SnapSourceRef {
source,
block_path: Vec::new(),
snap_type: snap.snap_type,
intersection: None,
};
Self {
paper_point,
model_point,
paper_point: snap.world,
model_point: frame.map_or(snap.world, |frame| {
snap.model_point
.unwrap_or_else(|| frame.paper_to_model(snap.world))
}),
viewport: frame.map(|f| f.viewport),
frame,
source: snap.source.map(|source| SnapSourceRef {
source,
block_path: Vec::new(),
snap_type: snap.snap_type,
intersection: snap
.secondary_source
.map(|source| Box::new(source_ref(source))),
..source_ref(source)
}),
}
}
@@ -222,6 +205,53 @@ pub struct MeasurementScale {
}
impl MeasurementScale {
/// OCS bookkeeping keeps user scaling separate from the changing viewport
/// scale. Standard DIMLFAC still carries the full factor for other readers.
pub const APP_ID: &'static str = "OCS_VIEWPORT_MEASUREMENT";
pub fn read(data: &acadrust::xdata::ExtendedData) -> Option<Self> {
use acadrust::xdata::XDataValue;
let record = data.get_record(Self::APP_ID)?;
let [XDataValue::Integer16(1), XDataValue::Real(user), XDataValue::Real(compensation)] =
record.values.as_slice()
else {
return None;
};
if !user.is_finite() || *user <= 0.0 || !compensation.is_finite() || *compensation <= 0.0 {
return None;
}
Some(Self {
user_lfac: *user,
viewport_compensation: *compensation,
})
}
pub fn write_to_entity(self, entity: &mut acadrust::EntityType) {
use acadrust::xdata::{ExtendedData, ExtendedDataRecord, XDataValue};
crate::entities::dim_override::set_on_entity(
entity,
crate::entities::dim_override::DIMLFAC,
Some(XDataValue::Real(-self.paper_factor())),
);
let common = entity.common_mut();
let mut data = ExtendedData::new();
for record in common
.extended_data
.records()
.iter()
.filter(|r| r.application_name != Self::APP_ID)
{
data.add_record(record.clone());
}
let mut record = ExtendedDataRecord::new(Self::APP_ID);
record.add_value(XDataValue::Integer16(1));
record.add_value(XDataValue::Real(self.user_lfac));
record.add_value(XDataValue::Real(self.viewport_compensation));
data.add_record(record);
// Parsed records are authoritative after changing measurement data.
common.extended_data = data;
}
pub const IDENTITY: Self = Self {
user_lfac: 1.0,
viewport_compensation: 1.0,
@@ -272,8 +302,7 @@ impl MeasurementScale {
///
/// Rendering an *existing* dimension only knows whether the entity lives
/// in a paper-space layout; the viewport compensation is already baked
/// into the persisted magnitude (see
/// [`MeasurementScale::viewport_dimlfac_override`]). This is the same rule
/// into the persisted magnitude. This is the same rule
/// as [`MeasurementScale::from_dimlfac`] with `viewport.is_some()`
/// replaced by `paper_space`.
pub fn user_lfac_for_space(dimlfac: f64, paper_space: bool) -> f64 {
@@ -287,45 +316,6 @@ impl MeasurementScale {
1.0
}
}
/// The persisted representation of viewport compensation, PR2's single
/// source of truth.
///
/// A paper-space dimension that measures model geometry through a viewport
/// keeps its definition points in **paper** coordinates and carries a
/// **negative DIMLFAC override** whose magnitude is
/// `user_lfac * viewport_compensation`. Because a viewport frame is a
/// similarity (uniform scale + in-plane twist), the raw paper measurement
/// times that magnitude is exactly the model measurement times the user's
/// linear factor — so the compensation is applied exactly once, at
/// formatting time, and round-trips through DXF/DWG unchanged (AutoCAD
/// applies |DIMLFAC| to paper-space dimensions by the same rule).
///
/// Returns `None` when no override is needed:
/// * the compensation is 1:1, or
/// * the drawing/style DIMLFAC is already negative with exactly this
/// magnitude (the drawing-wide DIMLFAC is set for this purpose).
///
/// `style_dimlfac` is the effective DIMLFAC the dimension would inherit
/// *without* an override.
pub fn viewport_dimlfac_override(style_dimlfac: f64, compensation: f64) -> Option<f64> {
if !compensation.is_finite() || compensation.abs() < 1e-12 {
return None;
}
if (compensation - 1.0).abs() < 1e-12 {
return None;
}
if style_dimlfac < 0.0
&& ((-style_dimlfac) - compensation).abs() <= 1e-9 * compensation.abs().max(1.0)
{
return None;
}
// A positive style DIMLFAC is the user's own multiplier and must be
// preserved; a negative one of a *different* magnitude was meant for
// another viewport scale, so the user multiplier there is 1.0.
let user = if style_dimlfac > 0.0 { style_dimlfac } else { 1.0 };
Some(-(user * compensation))
}
}
#[cfg(test)]
@@ -392,29 +382,4 @@ mod tests {
);
}
}
#[test]
fn override_encodes_compensation_once() {
// 1:10 viewport, no user factor -> -10, and a 10-unit paper span then
// formats as 100.
let ovr = MeasurementScale::viewport_dimlfac_override(1.0, 10.0).unwrap();
assert!((ovr + 10.0).abs() < 1e-12);
let lfac = MeasurementScale::user_lfac_for_space(ovr, true);
assert!((10.0 * lfac - 100.0).abs() < 1e-9);
// Same dimension read back in model space must not scale.
assert_eq!(MeasurementScale::user_lfac_for_space(ovr, false), 1.0);
}
#[test]
fn override_preserves_user_multiplier_and_skips_redundant_cases() {
assert_eq!(MeasurementScale::viewport_dimlfac_override(1.0, 1.0), None);
// Drawing-wide negative DIMLFAC already carries this compensation.
assert_eq!(MeasurementScale::viewport_dimlfac_override(-10.0, 10.0), None);
// Positive user factor is folded in.
let ovr = MeasurementScale::viewport_dimlfac_override(25.4, 10.0).unwrap();
assert!((ovr + 254.0).abs() < 1e-9);
// Negative but for a different scale -> user multiplier is 1.0.
let ovr = MeasurementScale::viewport_dimlfac_override(-2.0, 10.0).unwrap();
assert!((ovr + 10.0).abs() < 1e-12);
}
}
+38 -33
View File
@@ -97,6 +97,10 @@ pub struct SnapResult {
/// INSERT handle; the block path is resolved later by
/// [`crate::scene::viewport_ref::SnapSourceRef::block_path`]. (PR1)
pub source: Option<DimensionAssociationSource>,
/// Second real object at an intersection, when both objects are known.
pub secondary_source: Option<DimensionAssociationSource>,
/// Original model point when `world` has been projected onto a sheet.
pub model_point: Option<glam::DVec3>,
}
impl SnapResult {
@@ -984,6 +988,8 @@ impl Snapper {
extension_dir: None,
viewport: None,
source: None,
secondary_source: None,
model_point: None,
})
}
@@ -1119,6 +1125,8 @@ impl Snapper {
extension_dir: None,
viewport: None,
source: None,
secondary_source: None,
model_point: None,
});
best_rank = snap_tier(SnapType::Grid);
best_sub = snap_priority(SnapType::Grid);
@@ -1216,7 +1224,7 @@ impl Snapper {
let mut try_pt = |world: glam::DVec3,
snap_type: SnapType,
src: Option<DimensionAssociationSource>| {
src: Option<DimensionAssociationSource>, secondary: Option<DimensionAssociationSource>| {
let screen = world_to_screen(world, view_rot, eye, bounds);
if !in_bounds(screen) {
return;
@@ -1245,6 +1253,8 @@ impl Snapper {
extension_dir: None,
viewport: None,
source: src,
secondary_source: secondary,
model_point: None,
});
}
};
@@ -1262,7 +1272,7 @@ impl Snapper {
SnapHint::Endpoint => SnapType::Endpoint,
};
if self.is_on(snap_type) {
try_pt(world, snap_type, src);
try_pt(world, snap_type, src, None);
}
};
if let Some(points) = wires.snap_points() {
@@ -1298,8 +1308,7 @@ impl Snapper {
try_pt(
DVec3::from_array(point),
SnapType::Endpoint,
wire_source(wire),
);
wire_source(wire), None);
}
// Tessellated open curves have no key-vertex set. Their only
// endpoints are first/last, so testing candidate wires remains
@@ -1313,14 +1322,13 @@ impl Snapper {
continue;
}
if !wire.points.is_empty() {
try_pt(wp_f64(wire, 0), SnapType::Endpoint, wire_source(wire));
try_pt(wp_f64(wire, 0), SnapType::Endpoint, wire_source(wire), None);
}
if wire.points.len() > 1 {
try_pt(
wp_f64(wire, wire.points.len() - 1),
SnapType::Endpoint,
wire_source(wire),
);
wire_source(wire), None);
}
}
} else {
@@ -1328,7 +1336,7 @@ impl Snapper {
let src = wire_source(wire);
if !wire.key_vertices.is_empty() {
for &point in &wire.key_vertices {
try_pt(DVec3::from_array(point), SnapType::Endpoint, src);
try_pt(DVec3::from_array(point), SnapType::Endpoint, src, None);
}
} else {
let closed = wire
@@ -1337,14 +1345,13 @@ impl Snapper {
.any(|(_, hint)| matches!(hint, SnapHint::Quadrant));
if !closed {
if !wire.points.is_empty() {
try_pt(wp_f64(wire, 0), SnapType::Endpoint, src);
try_pt(wp_f64(wire, 0), SnapType::Endpoint, src, None);
}
if wire.points.len() > 1 {
try_pt(
wp_f64(wire, wire.points.len() - 1),
SnapType::Endpoint,
src,
);
src, None);
}
}
}
@@ -1371,7 +1378,7 @@ impl Snapper {
let a = DVec3::from_array(a);
let b = DVec3::from_array(b);
if a.distance_squared(b) > 1e-12 {
try_pt((a + b) * 0.5, SnapType::Midpoint, wire_source(wire));
try_pt((a + b) * 0.5, SnapType::Midpoint, wire_source(wire), None);
}
}
} else {
@@ -1381,7 +1388,7 @@ impl Snapper {
let a = DVec3::from_array(segment[0]);
let b = DVec3::from_array(segment[1]);
if a.distance_squared(b) > 1e-12 {
try_pt((a + b) * 0.5, SnapType::Midpoint, src);
try_pt((a + b) * 0.5, SnapType::Midpoint, src, None);
}
}
}
@@ -1395,8 +1402,7 @@ impl Snapper {
try_pt(
nearest_on_segment(cursor_world, seg.a, seg.b),
SnapType::Nearest,
wires.source_wire(seg.wire).and_then(wire_source),
);
wires.source_wire(seg.wire).and_then(wire_source), None);
}
} else {
for wire in in_range_wires.iter() {
@@ -1404,7 +1410,7 @@ impl Snapper {
for i in 0..wire.points.len().saturating_sub(1) {
let p =
nearest_on_segment(cursor_world, wp_f64(wire, i), wp_f64(wire, i + 1));
try_pt(p, SnapType::Nearest, src);
try_pt(p, SnapType::Nearest, src, None);
}
}
}
@@ -1420,8 +1426,7 @@ impl Snapper {
try_pt(
foot,
SnapType::Perpendicular,
wires.source_wire(seg.wire).and_then(wire_source),
);
wires.source_wire(seg.wire).and_then(wire_source), None);
}
}
} else {
@@ -1429,7 +1434,7 @@ impl Snapper {
let src = wire_source(wire);
for i in 0..wire.points.len().saturating_sub(1) {
if let Some(foot) = perp_foot(q, wp_f64(wire, i), wp_f64(wire, i + 1)) {
try_pt(foot, SnapType::Perpendicular, src);
try_pt(foot, SnapType::Perpendicular, src, None);
}
}
}
@@ -1450,8 +1455,7 @@ impl Snapper {
try_pt(
point,
SnapType::Intersection,
wires.source_wire(segment.wire).and_then(wire_source),
);
wires.source_wire(segment.wire).and_then(wire_source), None);
}
}
}
@@ -1466,7 +1470,7 @@ impl Snapper {
wp_f64(wire, index + 1),
) {
if (point - origin).length_squared() > 1e-18 {
try_pt(point, SnapType::Intersection, src);
try_pt(point, SnapType::Intersection, src, None);
}
}
}
@@ -1552,7 +1556,7 @@ impl Snapper {
// first. PR3 resolves the true pair.
let src = wire_source(wire_i);
for point in points {
try_pt(point, SnapType::Intersection, src);
try_pt(point, SnapType::Intersection, src, wire_source(wire_j));
}
resolved_pairs.insert(pair_key(wire_i, wire_j));
}
@@ -1586,8 +1590,7 @@ impl Snapper {
try_pt(
pt,
SnapType::Intersection,
wires.source_wire(a.wire).and_then(wire_source),
);
wires.source_wire(a.wire).and_then(wire_source), wires.source_wire(b.wire).and_then(wire_source));
if exact_cursor {
break 'intersection_sweep;
}
@@ -1609,7 +1612,7 @@ impl Snapper {
if let Some(pts) = exact_curve_intersections(wire_i, wire_j) {
let src = wire_source(wire_i);
for pt in pts {
try_pt(pt, SnapType::Intersection, src);
try_pt(pt, SnapType::Intersection, src, wire_source(wire_j));
}
continue;
}
@@ -1633,7 +1636,7 @@ impl Snapper {
continue;
}
if let Some(pt) = seg_intersect_3d(a0, a1, b0, b1) {
try_pt(pt, SnapType::Intersection, wire_source(wire_i));
try_pt(pt, SnapType::Intersection, wire_source(wire_i), wire_source(wire_j));
}
}
}
@@ -1659,7 +1662,7 @@ impl Snapper {
bounds,
self.osnap_radius_px,
) {
try_pt(ext, SnapType::Extension, None);
try_pt(ext, SnapType::Extension, None, None);
}
}
}
@@ -1706,7 +1709,7 @@ impl Snapper {
// feet (which sit closer to the cursor on their own lines),
// or the cursor would snap to a line instead of the crossing.
let pt = glam::DVec3::new(a0.x + t1 * d1.x, a0.y + t1 * d1.y, a0.z);
try_pt(pt, SnapType::Intersection, None);
try_pt(pt, SnapType::Intersection, None, None);
}
}
}
@@ -1759,8 +1762,7 @@ impl Snapper {
try_pt(
segment_a.a + ta as f64 * (segment_a.b - segment_a.a),
SnapType::ApparentIntersection,
wires.source_wire(segment_a.wire).and_then(wire_source),
);
wires.source_wire(segment_a.wire).and_then(wire_source), wires.source_wire(segment_b.wire).and_then(wire_source));
if dist2(apparent_screen, cursor_screen) <= f32::EPSILON {
break 'apparent_sweep;
}
@@ -1800,8 +1802,7 @@ impl Snapper {
try_pt(
wa0 + ta as f64 * (wa1 - wa0),
SnapType::ApparentIntersection,
wire_source(wire_i),
);
wire_source(wire_i), wire_source(wire_j));
}
}
}
@@ -2024,6 +2025,8 @@ impl Snapper {
extension_dir: None,
viewport: None,
source: wire_source(wire),
secondary_source: None,
model_point: None,
});
}
}
@@ -2088,6 +2091,8 @@ impl Snapper {
extension_dir: None,
viewport: None,
source: wire_source(wire),
secondary_source: None,
model_point: None,
});
}
};