forked from 1b-agent/opencadstudio-upstream
Fix viewport dimension acquisition and preserve measurement scaling
This commit is contained in:
1 parent
369e0c9fc5
commit
2ee3782850
18 files changed
+957
-577
No files matched your search
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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(_))));
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user