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
@@ -0,0 +1,48 @@
|
|||||||
|
# Dimensions through paper-space viewports
|
||||||
|
|
||||||
|
Paper-space point commands can snap to displayed model geometry through a planar,
|
||||||
|
orthographic layout viewport. The acquired result retains the original model
|
||||||
|
coordinate, its paper coordinate, the viewport frame, and feature identity.
|
||||||
|
Ordinary selection continues to operate on the active drawing space.
|
||||||
|
|
||||||
|
`DIMLINEAR`, `DIMALIGNED`, `DIMANGULAR`, `DIMRADIUS`, and `DIMDIAMETER` use this
|
||||||
|
acquisition context. The supported object-pick modes also query viewport geometry.
|
||||||
|
Snapping and object picking share viewport draw order, visibility, and clipping
|
||||||
|
rules. Object picking uses the resident interaction index and applies block
|
||||||
|
instance transforms. Nonuniformly transformed circles and circular arcs are
|
||||||
|
declined as radial object picks because their displayed geometry is elliptical.
|
||||||
|
|
||||||
|
## Input and measurement rules
|
||||||
|
|
||||||
|
- Free and typed sheet points measure paper units, including points inside a
|
||||||
|
viewport rectangle.
|
||||||
|
- Measuring points must all belong to the same space and, when applicable, the
|
||||||
|
same viewport. An incompatible pick leaves the command at its current step.
|
||||||
|
- Dimension-line and text placement do not change the measurement's space.
|
||||||
|
- Paper geometry takes priority over viewport geometry at an object pick.
|
||||||
|
- The final dimension style is applied before computing measurement scaling.
|
||||||
|
Length dimensions keep paper-space definition points and a negative `DIMLFAC`
|
||||||
|
override containing the effective user factor multiplied by `1 / viewport scale`.
|
||||||
|
Angular measurements do not use this length factor.
|
||||||
|
- `DIMASSOC=0` uses the same compensated value before exploding the annotation.
|
||||||
|
|
||||||
|
## Persistent measurement data
|
||||||
|
|
||||||
|
`OCS_VIEWPORT_MEASUREMENT` XData stores three values: version `1` (integer16),
|
||||||
|
the positive user factor (real), and viewport compensation (real). This keeps the
|
||||||
|
user's unit conversion separate from viewport scale. The ordinary `DIMLFAC`
|
||||||
|
override remains sufficient for displaying the saved measurement in other CAD
|
||||||
|
readers. The data survives DXF/DWG saves and undo/redo.
|
||||||
|
|
||||||
|
The dependent association change uses this metadata when viewport scale changes.
|
||||||
|
This creation change alone creates nonassociative viewport dimensions and leaves
|
||||||
|
the drawing's `DIMASSOC` setting unchanged. Direct model/paper associations retain
|
||||||
|
their existing behavior.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
`src/app/viewport_dimension_tests.rs` exercises snap-engine acquisition, command
|
||||||
|
commits for all five commands, object picks, input retries, active positive and
|
||||||
|
negative style factors, exploded dimensions, undo/redo, DXF/DWG persistence,
|
||||||
|
clipping, overlapping viewports, pan, scale, and twist. These are headless tests;
|
||||||
|
they do not establish visual acceptance in the desktop UI.
|
||||||
@@ -388,7 +388,9 @@ impl OpenCADStudio {
|
|||||||
// accepted-snap list must stay index-parallel with the points the
|
// accepted-snap list must stay index-parallel with the points the
|
||||||
// command collects (PR1). Interactive picks record themselves in
|
// command collects (PR1). Interactive picks record themselves in
|
||||||
// the click handler and never reach here.
|
// 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 {
|
if default_start {
|
||||||
let StepInput::Point(point) = &input else {
|
let StepInput::Point(point) = &input else {
|
||||||
@@ -402,6 +404,7 @@ impl OpenCADStudio {
|
|||||||
self.push_ucs_to_cmd(i);
|
self.push_ucs_to_cmd(i);
|
||||||
}
|
}
|
||||||
if let StepInput::EntityPick(handle, point) = &input {
|
if let StepInput::EntityPick(handle, point) = &input {
|
||||||
|
if !self.dimension_acquisition_allowed(i, None) { return Task::none(); }
|
||||||
let solid_pick = matches!(
|
let solid_pick = matches!(
|
||||||
self.tabs[i].scene.document.get_entity(*handle),
|
self.tabs[i].scene.document.get_entity(*handle),
|
||||||
Some(
|
Some(
|
||||||
@@ -474,6 +477,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
let ctrl = self.ctrl_down;
|
let ctrl = self.ctrl_down;
|
||||||
let shift = self.shift_down;
|
let shift = self.shift_down;
|
||||||
|
let picked_handle = if let StepInput::EntityPick(h, _) = &input { Some(*h) } else { None };
|
||||||
let result: Option<CmdResult> = {
|
let result: Option<CmdResult> = {
|
||||||
let Some(cmd) = self.tabs[i].active_cmd.as_mut() else {
|
let Some(cmd) = self.tabs[i].active_cmd.as_mut() else {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
@@ -492,6 +496,10 @@ impl OpenCADStudio {
|
|||||||
StepInput::Escape => Some(cmd.on_escape()),
|
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 {
|
match result {
|
||||||
Some(r) => self.apply_cmd_result(r),
|
Some(r) => self.apply_cmd_result(r),
|
||||||
None => Task::none(),
|
None => Task::none(),
|
||||||
@@ -2357,7 +2365,12 @@ impl OpenCADStudio {
|
|||||||
// A dimension placed on the sheet but measuring model geometry
|
// A dimension placed on the sheet but measuring model geometry
|
||||||
// through a viewport carries the compensation as a negative
|
// through a viewport carries the compensation as a negative
|
||||||
// DIMLFAC override.
|
// 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
|
// ...and is deliberately *not* associated in paper space: its
|
||||||
// definition points are projected paper coordinates, so a
|
// definition points are projected paper coordinates, so a
|
||||||
// paper-space association would bind it to geometry that is
|
// paper-space association would bind it to geometry that is
|
||||||
|
|||||||
+138
-242
@@ -1,262 +1,162 @@
|
|||||||
//! Viewport-aware dimension measurement (PR2).
|
//! Paper-space dimension acquisition and measurement. Only acquired geometry
|
||||||
//!
|
//! carries viewport compensation; free and typed sheet coordinates stay on the sheet.
|
||||||
//! 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;
|
|
||||||
|
|
||||||
|
use super::OpenCADStudio;
|
||||||
use crate::command::DimensionAssociationSource;
|
use crate::command::DimensionAssociationSource;
|
||||||
use crate::entities::dim_override;
|
use crate::entities::dim_override;
|
||||||
use crate::scene::viewport_ref::{AcceptedSnap, MeasurementScale, SnapSourceRef, ViewportFrame};
|
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)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub(crate) enum DimensionMeasureSpace {
|
pub(crate) enum DimensionMeasureSpace {
|
||||||
/// Plain measurement in the space the definition points live in.
|
|
||||||
Direct,
|
Direct,
|
||||||
/// Every measuring point came through one viewport; compensate by its
|
|
||||||
/// scale.
|
|
||||||
Viewport(ViewportFrame),
|
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,
|
Mixed,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
/// Record the accepted snaps produced by the explicit object-pick path (a
|
pub(crate) fn finish_command_click(&mut self, i: usize) {
|
||||||
/// dimension "select object" pick resolved through a content viewport).
|
let mut selection = self.tabs[i].scene.selection.borrow_mut();
|
||||||
///
|
selection.left_down = false;
|
||||||
/// An object pick is not a point step, so PR1's click handler never sees
|
selection.left_press_pos = None;
|
||||||
/// it. One pick supplies *both* extension origins, so it is recorded
|
selection.left_press_time = None;
|
||||||
/// twice: that keeps the list index-parallel with the definition points
|
selection.left_dragging = false;
|
||||||
/// 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(
|
#[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,
|
&mut self,
|
||||||
frame: ViewportFrame,
|
i: usize,
|
||||||
paper_point: DVec3,
|
viewport: Option<Handle>,
|
||||||
model_point: DVec3,
|
) -> 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,
|
entity: Handle,
|
||||||
block_path: Vec<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 {
|
self.push_accepted_snap(AcceptedSnap {
|
||||||
paper_point,
|
paper_point,
|
||||||
model_point,
|
model_point: frame.map_or(paper_point, |f| f.paper_to_model(paper_point)),
|
||||||
viewport: Some(frame.viewport),
|
viewport: frame.map(|f| f.viewport),
|
||||||
frame: Some(frame),
|
frame,
|
||||||
source: Some(SnapSourceRef {
|
source: Some(SnapSourceRef {
|
||||||
source: DimensionAssociationSource::inferred(entity),
|
source: DimensionAssociationSource::inferred(entity),
|
||||||
block_path: block_path.clone(),
|
block_path: block_path.clone(),
|
||||||
// An object pick acquires the whole curve, not one
|
snap_type: crate::snap::SnapType::ObjectPick,
|
||||||
// feature; the per-slot feature point is derived from the
|
intersection: None,
|
||||||
// committed dimension's own definition points.
|
|
||||||
snap_type: crate::snap::SnapType::Nearest,
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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(
|
pub(crate) fn apply_viewport_dimension_measurement(
|
||||||
&mut self,
|
&mut self,
|
||||||
i: usize,
|
i: usize,
|
||||||
entity: &mut EntityType,
|
entity: &mut EntityType,
|
||||||
) {
|
) -> bool {
|
||||||
let EntityType::Dimension(dimension) = entity else {
|
let EntityType::Dimension(dimension) = entity else {
|
||||||
return;
|
return true;
|
||||||
};
|
};
|
||||||
let is_angular = matches!(
|
let angular = matches!(
|
||||||
dimension,
|
dimension,
|
||||||
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_)
|
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_)
|
||||||
);
|
);
|
||||||
let frame = match self.dimension_measure_space(i) {
|
let frame = match self.dimension_measure_space(i) {
|
||||||
DimensionMeasureSpace::Direct => return,
|
DimensionMeasureSpace::Direct => return true,
|
||||||
DimensionMeasureSpace::Mixed => {
|
DimensionMeasureSpace::Mixed => {
|
||||||
self.command_line.push_warning(
|
self.command_line.push_error(crate::t!("Pick geometry from the same viewport, or use paper-space points for the whole dimension.").as_ref());
|
||||||
crate::t!(
|
return false;
|
||||||
"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;
|
|
||||||
}
|
}
|
||||||
DimensionMeasureSpace::Viewport(frame) => frame,
|
DimensionMeasureSpace::Viewport(f) => f,
|
||||||
};
|
};
|
||||||
|
if angular {
|
||||||
// A similarity preserves angles, so an angular dimension reads the
|
return true;
|
||||||
// model angle already and needs no override.
|
|
||||||
if is_angular {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
let dimlfac = self.pending_dimension_style_dimlfac(i, entity);
|
||||||
let compensation = frame.paper_to_model_length_factor();
|
let scale = MeasurementScale {
|
||||||
let style_dimlfac = self.pending_dimension_style_dimlfac(i, entity);
|
user_lfac: MeasurementScale::user_lfac_for_space(dimlfac, true),
|
||||||
if let Some(value) =
|
viewport_compensation: frame.paper_to_model_length_factor(),
|
||||||
MeasurementScale::viewport_dimlfac_override(style_dimlfac, compensation)
|
};
|
||||||
{
|
if !scale.paper_factor().is_finite() || scale.paper_factor() <= 0.0 {
|
||||||
// The negative DIMLFAC convention is resolved from the dimension's
|
return false;
|
||||||
// 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)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
// 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(
|
pub(crate) fn try_dimension_viewport_entity_pick(
|
||||||
&mut self,
|
&mut self,
|
||||||
i: usize,
|
i: usize,
|
||||||
@@ -266,42 +166,38 @@ impl OpenCADStudio {
|
|||||||
let pick = self.tabs[i]
|
let pick = self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.dimension_pick_through_viewport(paper, aperture_paper)?;
|
.dimension_pick_through_viewport(paper, aperture_paper)?;
|
||||||
self.record_dimension_viewport_pick(
|
if !self.dimension_acquisition_allowed(i, Some(pick.frame.viewport)) {
|
||||||
pick.frame,
|
return Some(crate::command::CmdResult::NeedPoint);
|
||||||
pick.paper_point,
|
}
|
||||||
pick.model_point,
|
|
||||||
pick.entity_handle,
|
|
||||||
pick.block_path.clone(),
|
|
||||||
);
|
|
||||||
let command = self.tabs[i].active_cmd.as_mut()?;
|
let command = self.tabs[i].active_cmd.as_mut()?;
|
||||||
command.inject_picked_entity(pick.paper_entity);
|
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 {
|
fn pending_dimension_style_dimlfac(&self, i: usize, entity: &EntityType) -> f64 {
|
||||||
let EntityType::Dimension(dimension) = entity else {
|
let EntityType::Dimension(dimension) = entity else {
|
||||||
return 1.0;
|
return 1.0;
|
||||||
};
|
};
|
||||||
if let Some(value) = dim_override::real(
|
dim_override::real(
|
||||||
&dimension.base().common.extended_data,
|
&dimension.base().common.extended_data,
|
||||||
dim_override::DIMLFAC,
|
dim_override::DIMLFAC,
|
||||||
) {
|
)
|
||||||
return value;
|
.or_else(|| {
|
||||||
}
|
self.tabs[i]
|
||||||
let name = dimension.base().style_name.as_str();
|
.scene
|
||||||
self.tabs[i]
|
.document
|
||||||
.scene
|
.dim_styles
|
||||||
.document
|
.iter()
|
||||||
.dim_styles
|
.find(|s| s.name.eq_ignore_ascii_case(&dimension.base().style_name))
|
||||||
.iter()
|
.map(|s| s.dimlfac)
|
||||||
.find(|style| {
|
})
|
||||||
style.name.eq_ignore_ascii_case(name)
|
.unwrap_or(1.0)
|
||||||
|| (name.trim().is_empty() && style.name.eq_ignore_ascii_case("Standard"))
|
|
||||||
})
|
|
||||||
.map(|style| style.dimlfac)
|
|
||||||
.unwrap_or(1.0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,8 @@ mod annotation_data;
|
|||||||
mod command_driver;
|
mod command_driver;
|
||||||
pub(crate) mod commands;
|
pub(crate) mod commands;
|
||||||
pub(crate) mod dim_viewport;
|
pub(crate) mod dim_viewport;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod viewport_dimension_tests;
|
||||||
mod document;
|
mod document;
|
||||||
mod drafting_settings;
|
mod drafting_settings;
|
||||||
pub(crate) mod expr_eval;
|
pub(crate) mod expr_eval;
|
||||||
|
|||||||
+23
-26
@@ -2374,6 +2374,8 @@ impl OpenCADStudio {
|
|||||||
extension_dir: None,
|
extension_dir: None,
|
||||||
viewport: None,
|
viewport: None,
|
||||||
source: None,
|
source: None,
|
||||||
|
secondary_source: None,
|
||||||
|
model_point: None,
|
||||||
});
|
});
|
||||||
if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
|
if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
|
||||||
cmd.set_acquisition_hint(Some(pick.label));
|
cmd.set_acquisition_hint(Some(pick.label));
|
||||||
@@ -3853,29 +3855,7 @@ impl OpenCADStudio {
|
|||||||
command.set_ctrl(self.ctrl_down);
|
command.set_ctrl(self.ctrl_down);
|
||||||
command.set_shift(self.shift_down);
|
command.set_shift(self.shift_down);
|
||||||
}
|
}
|
||||||
// PR2: a dimension "select object" pick that lands inside a paper
|
let result = if self.tabs[i]
|
||||||
// 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]
|
|
||||||
.active_cmd
|
.active_cmd
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|c| c.needs_structure_point_pick())
|
.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 shift = self.shift_down;
|
||||||
let result = self.tabs[i].active_cmd.as_mut().map(|c| {
|
let result = self.tabs[i].active_cmd.as_mut().map(|c| {
|
||||||
// Shift-swap state for TRIM/EXTEND (#336).
|
// Shift-swap state for TRIM/EXTEND (#336).
|
||||||
@@ -4072,6 +4053,7 @@ impl OpenCADStudio {
|
|||||||
c.set_entity_pick_direction(entity_pick_direction);
|
c.set_entity_pick_direction(entity_pick_direction);
|
||||||
c.on_entity_pick(handle, entity_pick_point)
|
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.
|
// HATCHEDIT: after pick, inject hatch model data into the command.
|
||||||
if self.tabs[i]
|
if self.tabs[i]
|
||||||
.active_cmd
|
.active_cmd
|
||||||
@@ -4142,11 +4124,24 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
result
|
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]
|
} else if self.tabs[i]
|
||||||
.active_cmd
|
.active_cmd
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|command| command.entity_pick_accepts_points())
|
.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.refresh_command_point_pick_context(i);
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
.active_cmd
|
.active_cmd
|
||||||
@@ -4215,17 +4210,18 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].dyn_active = 0;
|
self.tabs[i].dyn_active = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.last_point = Some(world_pt);
|
|
||||||
// Record what this point step accepted, alongside the point
|
// Record what this point step accepted, alongside the point
|
||||||
// itself (PR1): paper point, model point, viewport, frame and
|
// itself (PR1): paper point, model point, viewport, frame and
|
||||||
// geometry identity. Commands that don't care keep consuming
|
// geometry identity. Commands that don't care keep consuming
|
||||||
// `world_pt` exactly as before.
|
// `world_pt` exactly as before.
|
||||||
let pending = self.pending_click_snap.take();
|
let pending = self.pending_click_snap.take();
|
||||||
self.record_accepted_snap(
|
if !self.record_accepted_snap(
|
||||||
|
i,
|
||||||
pending.map(|(hit, _)| hit),
|
pending.map(|(hit, _)| hit),
|
||||||
pending.and_then(|(_, frame)| frame),
|
pending.and_then(|(_, frame)| frame),
|
||||||
world_pt,
|
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 —
|
// The one-shot snap override is spent by this pick —
|
||||||
// restore the running osnap configuration (#337).
|
// restore the running osnap configuration (#337).
|
||||||
self.snapper.clear_override();
|
self.snapper.clear_override();
|
||||||
@@ -4260,6 +4256,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.sync_dimension_snaps(i);
|
||||||
if let Some(r) = result {
|
if let Some(r) = result {
|
||||||
let task = self.apply_cmd_result(r);
|
let task = self.apply_cmd_result(r);
|
||||||
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
||||||
|
|||||||
@@ -102,10 +102,11 @@ impl OpenCADStudio {
|
|||||||
/// after ortho / polar / axis-lock / dynamic-input resolution.
|
/// after ortho / polar / axis-lock / dynamic-input resolution.
|
||||||
pub(crate) fn record_accepted_snap(
|
pub(crate) fn record_accepted_snap(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
tab: usize,
|
||||||
snap: Option<SnapResult>,
|
snap: Option<SnapResult>,
|
||||||
frame: Option<ViewportFrame>,
|
frame: Option<ViewportFrame>,
|
||||||
committed: glam::DVec3,
|
committed: glam::DVec3,
|
||||||
) {
|
) -> bool {
|
||||||
let accepted = match snap {
|
let accepted = match snap {
|
||||||
// A viewport snap keeps its frame only when the hit really came
|
// A viewport snap keeps its frame only when the hit really came
|
||||||
// through it, so a paper-sheet snap never claims a model point.
|
// through it, so a paper-sheet snap never claims a model point.
|
||||||
@@ -115,7 +116,24 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
None => AcceptedSnap::free(committed),
|
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);
|
self.push_accepted_snap(accepted);
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Append an already-built accepted snap, keeping the retention cap.
|
/// Append an already-built accepted snap, keeping the retention cap.
|
||||||
@@ -165,30 +183,12 @@ impl OpenCADStudio {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let handles = self.tabs[i].scene.layout_content_viewports();
|
for frame in self.tabs[i]
|
||||||
// Draw order puts the last-drawn viewport on top; probe top-most first
|
.scene
|
||||||
// so overlapping viewports resolve the same way they render.
|
.viewport_frames_at_paper_point(cursor_paper)
|
||||||
for &handle in handles.iter().rev() {
|
{
|
||||||
if !self.tabs[i].scene.viewport_displays_content(handle) {
|
let handle = frame.viewport;
|
||||||
continue;
|
let Some((cam, rect)) = self.tabs[i].scene.viewport_edit_frame_for(handle, canvas)
|
||||||
}
|
|
||||||
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)
|
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -208,8 +208,7 @@ impl OpenCADStudio {
|
|||||||
// the viewport camera projects back onto `local`.
|
// the viewport camera projects back onto `local`.
|
||||||
let model_cursor = frame.paper_to_model(cursor_paper);
|
let model_cursor = frame.paper_to_model(cursor_paper);
|
||||||
|
|
||||||
let wires = self
|
let wires = self.tabs[i]
|
||||||
.tabs[i]
|
|
||||||
.scene
|
.scene
|
||||||
.model_wires_for_viewport_arc(handle, bounds.height);
|
.model_wires_for_viewport_arc(handle, bounds.height);
|
||||||
let candidates = self.tabs[i].scene.interaction_candidates_near(
|
let candidates = self.tabs[i].scene.interaction_candidates_near(
|
||||||
@@ -221,14 +220,14 @@ impl OpenCADStudio {
|
|||||||
self.snapper.osnap_radius_px,
|
self.snapper.osnap_radius_px,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Object snap only. The grid belongs to the paper sheet, and the
|
// The grid belongs to the sheet. Translate the construction
|
||||||
// rubber-band origin / tracking points are paper-space points, so
|
// anchor for perpendicular/tangent snaps into this viewport's
|
||||||
// perpendicular and extension feet would be nonsense in model
|
// model coordinates, then restore the shared snapper state.
|
||||||
// space. Save and restore, since `Snapper` is shared state.
|
|
||||||
let saved_grid = self.snapper.grid_snap_on;
|
let saved_grid = self.snapper.grid_snap_on;
|
||||||
let saved_from = self.snapper.from_point;
|
let saved_from = self.snapper.from_point;
|
||||||
self.snapper.grid_snap_on = false;
|
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(
|
let hit = self.snapper.snap(
|
||||||
model_cursor,
|
model_cursor,
|
||||||
local,
|
local,
|
||||||
@@ -251,8 +250,7 @@ impl OpenCADStudio {
|
|||||||
// the aperture reaching past the clip. Reject those: a snap must be
|
// the aperture reaching past the clip. Reject those: a snap must be
|
||||||
// a real feature the viewport actually displays.
|
// a real feature the viewport actually displays.
|
||||||
let paper = frame.model_to_paper(hit.world);
|
let paper = frame.model_to_paper(hit.world);
|
||||||
if !self
|
if !self.tabs[i]
|
||||||
.tabs[i]
|
|
||||||
.scene
|
.scene
|
||||||
.viewport_displays_paper_point(handle, paper.truncate())
|
.viewport_displays_paper_point(handle, paper.truncate())
|
||||||
{
|
{
|
||||||
@@ -291,5 +289,7 @@ fn project_hit_to_paper(
|
|||||||
}),
|
}),
|
||||||
viewport: Some(viewport),
|
viewport: Some(viewport),
|
||||||
source: hit.source,
|
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
|
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 {
|
fn needs_entity_pick(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1638,25 +1638,11 @@ pub(crate) fn dimension_in_paper_space(dimension: &Dimension, document: &CadDocu
|
|||||||
if !owner.is_valid() {
|
if !owner.is_valid() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let model_block = document
|
document.objects.values().any(|object| matches!(object,
|
||||||
.block_records
|
acadrust::objects::ObjectType::Layout(layout)
|
||||||
.get("*Model_Space")
|
if layout.name != "Model" && layout.block_record == owner
|
||||||
.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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The value a dimension's text reports for its measurement: the raw
|
/// The value a dimension's text reports for its measurement: the raw
|
||||||
|
|||||||
@@ -167,6 +167,17 @@ impl CadCommand for AlignedDimensionCommand {
|
|||||||
true
|
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 {
|
fn input_kind(&self) -> InputKind {
|
||||||
if self.awaiting_text {
|
if self.awaiting_text {
|
||||||
InputKind::FreeText
|
InputKind::FreeText
|
||||||
|
|||||||
@@ -488,6 +488,20 @@ impl CadCommand for AngularDimensionCommand {
|
|||||||
true
|
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 {
|
fn on_escape(&mut self) -> CmdResult {
|
||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,6 +138,16 @@ impl CadCommand for DiameterDimensionCommand {
|
|||||||
true
|
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 {
|
fn on_escape(&mut self) -> CmdResult {
|
||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -223,6 +223,17 @@ impl CadCommand for LinearDimensionCommand {
|
|||||||
true
|
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 {
|
fn on_escape(&mut self) -> CmdResult {
|
||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,16 @@ impl CadCommand for RadiusDimensionCommand {
|
|||||||
true
|
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 {
|
fn on_escape(&mut self) -> CmdResult {
|
||||||
CmdResult::Cancel
|
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?
|
/// Does `paper` lie inside the viewport's displayed area?
|
||||||
///
|
///
|
||||||
/// Always bounded by the viewport's paper rectangle; a viewport with a
|
/// 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);
|
let poly = self.clip_boundary_polygon(vp.clip_boundary_handle, vp.center.z as f32);
|
||||||
if poly.len() < 3 {
|
if poly.len() < 3 {
|
||||||
// A clip entity we cannot tessellate: fall back to the rectangle
|
// An unsupported boundary cannot establish snap eligibility.
|
||||||
// rather than refusing every snap in the viewport.
|
return false;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
point_in_polygon_xy(paper, &poly)
|
point_in_polygon_xy(paper, &poly)
|
||||||
}
|
}
|
||||||
|
|||||||
+128
-114
@@ -21,7 +21,9 @@ use super::*;
|
|||||||
use crate::command::EntityTransform;
|
use crate::command::EntityTransform;
|
||||||
use crate::scene::viewport_ref::ViewportFrame;
|
use crate::scene::viewport_ref::ViewportFrame;
|
||||||
use acadrust::types::{Matrix4, Transform};
|
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.
|
/// A model entity resolved by clicking inside a paper-space content viewport.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -44,36 +46,16 @@ pub struct ViewportDimPick {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Scene {
|
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> {
|
pub fn content_viewport_at_paper_point(&self, paper: DVec3) -> Option<Handle> {
|
||||||
if self.current_layout == "Model" || self.active_viewport.is_some() {
|
self.viewport_frames_at_paper_point(paper)
|
||||||
return None;
|
.first()
|
||||||
}
|
.map(|f| f.viewport)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`Scene::content_viewport_at_paper_point`] resolved to a frame.
|
|
||||||
pub fn viewport_frame_at_paper_point(&self, paper: DVec3) -> Option<ViewportFrame> {
|
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
|
/// Resolve an explicit dimension object pick made on the sheet but landing
|
||||||
@@ -86,28 +68,42 @@ impl Scene {
|
|||||||
paper: DVec3,
|
paper: DVec3,
|
||||||
aperture_paper: f64,
|
aperture_paper: f64,
|
||||||
) -> Option<ViewportDimPick> {
|
) -> Option<ViewportDimPick> {
|
||||||
let frame = self.viewport_frame_at_paper_point(paper)?;
|
for frame in self.viewport_frames_at_paper_point(paper) {
|
||||||
let model_point = frame.paper_to_model(paper);
|
let model_point = frame.paper_to_model(paper);
|
||||||
let aperture_model =
|
let aperture_model =
|
||||||
(aperture_paper.max(0.0) * frame.paper_to_model_length_factor()).max(1e-9);
|
(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 Some(top) =
|
||||||
let (entity, block_path) = self.resolve_measurable_entity(top, model_point)?;
|
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;
|
let mut paper_entity = entity;
|
||||||
crate::scene::view::dispatch::apply_transform(
|
crate::scene::view::dispatch::apply_transform(
|
||||||
&mut paper_entity,
|
&mut paper_entity,
|
||||||
&EntityTransform::Affine(viewport_model_to_paper_transform(&frame)),
|
&EntityTransform::Affine(viewport_model_to_paper_transform(&frame)),
|
||||||
);
|
);
|
||||||
let entity_handle = paper_entity.as_entity().handle();
|
let entity_handle = paper_entity.as_entity().handle();
|
||||||
Some(ViewportDimPick {
|
return Some(ViewportDimPick {
|
||||||
frame,
|
frame,
|
||||||
entity_handle: if entity_handle.is_valid() { entity_handle } else { top },
|
entity_handle: if entity_handle.is_valid() {
|
||||||
block_path,
|
entity_handle
|
||||||
paper_entity,
|
} else {
|
||||||
paper_point: paper,
|
top
|
||||||
model_point,
|
},
|
||||||
})
|
block_path,
|
||||||
|
paper_entity,
|
||||||
|
paper_point: paper,
|
||||||
|
model_point,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Nearest resident model wire of `viewport` to `model_point`, within
|
/// Nearest resident model wire of `viewport` to `model_point`, within
|
||||||
@@ -119,7 +115,23 @@ impl Scene {
|
|||||||
model_point: DVec3,
|
model_point: DVec3,
|
||||||
aperture: f64,
|
aperture: f64,
|
||||||
) -> Option<Handle> {
|
) -> 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;
|
let mut best: Option<(f64, Handle)> = None;
|
||||||
for wire in wires.iter() {
|
for wire in wires.iter() {
|
||||||
if !wire.display_visible {
|
if !wire.display_visible {
|
||||||
@@ -133,8 +145,17 @@ impl Scene {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|inst| DVec3::from_array(inst.translation))
|
.map(|inst| DVec3::from_array(inst.translation))
|
||||||
.unwrap_or(DVec3::ZERO);
|
.unwrap_or(DVec3::ZERO);
|
||||||
let distance = wire_polyline_distance(wire, offset, model_point);
|
let distance = wire_polyline_nearest(wire, offset, model_point);
|
||||||
if let Some(distance) = distance {
|
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) {
|
if distance <= aperture && best.as_ref().is_none_or(|(d, _)| distance < *d) {
|
||||||
best = Some((distance, handle));
|
best = Some((distance, handle));
|
||||||
}
|
}
|
||||||
@@ -146,10 +167,11 @@ impl Scene {
|
|||||||
/// Turn a picked handle into a measurable planar entity in model (WCS)
|
/// Turn a picked handle into a measurable planar entity in model (WCS)
|
||||||
/// coordinates, descending through block instances and baking the
|
/// coordinates, descending through block instances and baking the
|
||||||
/// instance transform into the returned clone.
|
/// instance transform into the returned clone.
|
||||||
fn resolve_measurable_entity(
|
pub(crate) fn resolve_measurable_entity(
|
||||||
&self,
|
&self,
|
||||||
handle: Handle,
|
handle: Handle,
|
||||||
model_point: DVec3,
|
model_point: DVec3,
|
||||||
|
viewport: Option<Handle>,
|
||||||
) -> Option<(EntityType, Vec<Handle>)> {
|
) -> Option<(EntityType, Vec<Handle>)> {
|
||||||
let entity = self.document.get_entity(handle)?;
|
let entity = self.document.get_entity(handle)?;
|
||||||
match entity {
|
match entity {
|
||||||
@@ -161,6 +183,7 @@ impl Scene {
|
|||||||
model_point,
|
model_point,
|
||||||
&mut path,
|
&mut path,
|
||||||
0,
|
0,
|
||||||
|
viewport,
|
||||||
)?;
|
)?;
|
||||||
Some((found.0, found.1))
|
Some((found.0, found.1))
|
||||||
}
|
}
|
||||||
@@ -180,6 +203,7 @@ impl Scene {
|
|||||||
model_point: DVec3,
|
model_point: DVec3,
|
||||||
path: &mut Vec<Handle>,
|
path: &mut Vec<Handle>,
|
||||||
depth: usize,
|
depth: usize,
|
||||||
|
viewport: Option<Handle>,
|
||||||
) -> Option<(EntityType, Vec<Handle>)> {
|
) -> Option<(EntityType, Vec<Handle>)> {
|
||||||
const MAX_DEPTH: usize = 8;
|
const MAX_DEPTH: usize = 8;
|
||||||
if depth > MAX_DEPTH {
|
if depth > MAX_DEPTH {
|
||||||
@@ -199,6 +223,14 @@ impl Scene {
|
|||||||
let Some(child_entity) = self.document.get_entity(child) else {
|
let Some(child_entity) = self.document.get_entity(child) else {
|
||||||
continue;
|
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(_)) {
|
if matches!(child_entity, EntityType::Insert(_)) {
|
||||||
let mut nested = Vec::new();
|
let mut nested = Vec::new();
|
||||||
if let Some((entity, mut nested_path)) = self.descend_block_instance(
|
if let Some((entity, mut nested_path)) = self.descend_block_instance(
|
||||||
@@ -207,6 +239,7 @@ impl Scene {
|
|||||||
model_point,
|
model_point,
|
||||||
&mut nested,
|
&mut nested,
|
||||||
depth + 1,
|
depth + 1,
|
||||||
|
viewport,
|
||||||
) {
|
) {
|
||||||
if let Some(distance) = planar_pick_distance(&entity, model_point) {
|
if let Some(distance) = planar_pick_distance(&entity, model_point) {
|
||||||
if best.as_ref().is_none_or(|(d, _, _)| distance < *d) {
|
if best.as_ref().is_none_or(|(d, _, _)| distance < *d) {
|
||||||
@@ -218,6 +251,22 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
continue;
|
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();
|
let mut placed = child_entity.clone();
|
||||||
crate::scene::view::dispatch::apply_transform(
|
crate::scene::view::dispatch::apply_transform(
|
||||||
&mut placed,
|
&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
|
/// Distance from `point` to a wire's polyline, in XY. `None` when the wire has
|
||||||
/// no usable segment.
|
/// no usable segment.
|
||||||
fn wire_polyline_distance(
|
fn wire_polyline_nearest(
|
||||||
wire: &crate::scene::WireModel,
|
wire: &crate::scene::WireModel,
|
||||||
offset: DVec3,
|
offset: DVec3,
|
||||||
point: DVec3,
|
point: DVec3,
|
||||||
) -> Option<f64> {
|
) -> Option<(f64, DVec3)> {
|
||||||
let count = wire.points.len();
|
let count = wire.points.len();
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return None;
|
return None;
|
||||||
@@ -278,6 +327,7 @@ fn wire_polyline_distance(
|
|||||||
) + offset
|
) + offset
|
||||||
};
|
};
|
||||||
let mut best = f64::INFINITY;
|
let mut best = f64::INFINITY;
|
||||||
|
let mut best_point = DVec3::ZERO;
|
||||||
let mut previous: Option<DVec3> = None;
|
let mut previous: Option<DVec3> = None;
|
||||||
for i in 0..count {
|
for i in 0..count {
|
||||||
let current = at(i);
|
let current = at(i);
|
||||||
@@ -285,77 +335,41 @@ fn wire_polyline_distance(
|
|||||||
previous = None;
|
previous = None;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match previous {
|
let nearest = previous.map_or(current, |start| {
|
||||||
Some(start) => best = best.min(segment_distance_xy(start, current, point)),
|
let delta = current - start;
|
||||||
None => best = best.min((current.truncate() - point.truncate()).length()),
|
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);
|
previous = Some(current);
|
||||||
}
|
}
|
||||||
best.is_finite().then_some(best)
|
best.is_finite().then_some((best, best_point))
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Distance from `point` to the planar geometry object-pick dimensioning
|
/// Distance from `point` to the planar geometry object-pick dimensioning
|
||||||
/// supports. `None` for entity types that cannot be dimensioned by picking.
|
/// supports. `None` for entity types that cannot be dimensioned by picking.
|
||||||
pub fn planar_pick_distance(entity: &EntityType, point: DVec3) -> Option<f64> {
|
pub fn planar_pick_distance(entity: &EntityType, point: DVec3) -> Option<f64> {
|
||||||
let p = point.truncate();
|
let planar = crate::entities::curve::entity_curve(entity)?;
|
||||||
match entity {
|
let normal = DVec3::from_array(planar.plane.normal()?);
|
||||||
EntityType::Line(line) => Some(segment_distance_xy(
|
if normal.cross(DVec3::Z).length_squared() > 1e-12 {
|
||||||
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 {
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let count = if closed { points.len() } else { points.len() - 1 };
|
let uv = planar.plane.project(point.to_array())?;
|
||||||
(0..count)
|
let nearest = cadkernel::geom2d::closest_point(&planar.curve, uv);
|
||||||
.map(|i| segment_distance_xy(points[i], points[(i + 1) % points.len()], point))
|
Some(
|
||||||
.reduce(f64::min)
|
(DVec3::from_array(planar.plane.point_at(nearest.point)) - point)
|
||||||
|
.truncate()
|
||||||
|
.length(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
+69
-104
@@ -1,20 +1,4 @@
|
|||||||
//! Shared interfaces for paper-space snapping through layout viewports and
|
//! Coordinates and feature identity shared by viewport acquisition and dimensions.
|
||||||
//! 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.
|
|
||||||
|
|
||||||
use acadrust::types::Handle;
|
use acadrust::types::Handle;
|
||||||
use glam::{DMat3, DVec2, DVec3};
|
use glam::{DMat3, DVec2, DVec3};
|
||||||
@@ -27,8 +11,7 @@ use crate::snap::SnapType;
|
|||||||
/// scale (paper units per model unit) and in-plane rotation (`twist`,
|
/// scale (paper units per model unit) and in-plane rotation (`twist`,
|
||||||
/// radians, counter-clockwise, applied to model geometry before scaling).
|
/// radians, counter-clockwise, applied to model geometry before scaling).
|
||||||
///
|
///
|
||||||
/// PR1 owns construction (`Scene::viewport_frame`); everyone else only reads
|
/// Constructed by `Scene::viewport_frame`. All arithmetic is `f64`.
|
||||||
/// it. All arithmetic is `f64`.
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct ViewportFrame {
|
pub struct ViewportFrame {
|
||||||
/// Viewport entity handle.
|
/// Viewport entity handle.
|
||||||
@@ -105,6 +88,8 @@ pub struct SnapSourceRef {
|
|||||||
pub source: DimensionAssociationSource,
|
pub source: DimensionAssociationSource,
|
||||||
pub block_path: Vec<Handle>,
|
pub block_path: Vec<Handle>,
|
||||||
pub snap_type: SnapType,
|
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
|
/// A snap accepted by a point-input command, with enough information for
|
||||||
@@ -149,31 +134,29 @@ impl AcceptedSnap {
|
|||||||
self.viewport.is_some()
|
self.viewport.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build an accepted snap from a [`crate::snap::SnapResult`].
|
/// `snap.world` is always in the command's current space. A projected
|
||||||
///
|
/// viewport hit also retains its original model coordinate explicitly.
|
||||||
/// * `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.
|
|
||||||
pub fn from_snap(snap: &crate::snap::SnapResult, frame: Option<ViewportFrame>) -> Self {
|
pub fn from_snap(snap: &crate::snap::SnapResult, frame: Option<ViewportFrame>) -> Self {
|
||||||
let (paper_point, model_point) = match &frame {
|
let frame = frame.filter(|frame| snap.viewport == Some(frame.viewport));
|
||||||
Some(frame) => (frame.model_to_paper(snap.world), snap.world),
|
let source_ref = |source| SnapSourceRef {
|
||||||
None => (snap.world, snap.world),
|
source,
|
||||||
|
block_path: Vec::new(),
|
||||||
|
snap_type: snap.snap_type,
|
||||||
|
intersection: None,
|
||||||
};
|
};
|
||||||
Self {
|
Self {
|
||||||
paper_point,
|
paper_point: snap.world,
|
||||||
model_point,
|
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),
|
viewport: frame.map(|f| f.viewport),
|
||||||
frame,
|
frame,
|
||||||
source: snap.source.map(|source| SnapSourceRef {
|
source: snap.source.map(|source| SnapSourceRef {
|
||||||
source,
|
intersection: snap
|
||||||
block_path: Vec::new(),
|
.secondary_source
|
||||||
snap_type: snap.snap_type,
|
.map(|source| Box::new(source_ref(source))),
|
||||||
|
..source_ref(source)
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,6 +205,53 @@ pub struct MeasurementScale {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
pub const IDENTITY: Self = Self {
|
||||||
user_lfac: 1.0,
|
user_lfac: 1.0,
|
||||||
viewport_compensation: 1.0,
|
viewport_compensation: 1.0,
|
||||||
@@ -272,8 +302,7 @@ impl MeasurementScale {
|
|||||||
///
|
///
|
||||||
/// Rendering an *existing* dimension only knows whether the entity lives
|
/// Rendering an *existing* dimension only knows whether the entity lives
|
||||||
/// in a paper-space layout; the viewport compensation is already baked
|
/// in a paper-space layout; the viewport compensation is already baked
|
||||||
/// into the persisted magnitude (see
|
/// into the persisted magnitude. This is the same rule
|
||||||
/// [`MeasurementScale::viewport_dimlfac_override`]). This is the same rule
|
|
||||||
/// as [`MeasurementScale::from_dimlfac`] with `viewport.is_some()`
|
/// as [`MeasurementScale::from_dimlfac`] with `viewport.is_some()`
|
||||||
/// replaced by `paper_space`.
|
/// replaced by `paper_space`.
|
||||||
pub fn user_lfac_for_space(dimlfac: f64, paper_space: bool) -> f64 {
|
pub fn user_lfac_for_space(dimlfac: f64, paper_space: bool) -> f64 {
|
||||||
@@ -287,45 +316,6 @@ impl MeasurementScale {
|
|||||||
1.0
|
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)]
|
#[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
|
/// INSERT handle; the block path is resolved later by
|
||||||
/// [`crate::scene::viewport_ref::SnapSourceRef::block_path`]. (PR1)
|
/// [`crate::scene::viewport_ref::SnapSourceRef::block_path`]. (PR1)
|
||||||
pub source: Option<DimensionAssociationSource>,
|
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 {
|
impl SnapResult {
|
||||||
@@ -984,6 +988,8 @@ impl Snapper {
|
|||||||
extension_dir: None,
|
extension_dir: None,
|
||||||
viewport: None,
|
viewport: None,
|
||||||
source: None,
|
source: None,
|
||||||
|
secondary_source: None,
|
||||||
|
model_point: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1119,6 +1125,8 @@ impl Snapper {
|
|||||||
extension_dir: None,
|
extension_dir: None,
|
||||||
viewport: None,
|
viewport: None,
|
||||||
source: None,
|
source: None,
|
||||||
|
secondary_source: None,
|
||||||
|
model_point: None,
|
||||||
});
|
});
|
||||||
best_rank = snap_tier(SnapType::Grid);
|
best_rank = snap_tier(SnapType::Grid);
|
||||||
best_sub = snap_priority(SnapType::Grid);
|
best_sub = snap_priority(SnapType::Grid);
|
||||||
@@ -1216,7 +1224,7 @@ impl Snapper {
|
|||||||
|
|
||||||
let mut try_pt = |world: glam::DVec3,
|
let mut try_pt = |world: glam::DVec3,
|
||||||
snap_type: SnapType,
|
snap_type: SnapType,
|
||||||
src: Option<DimensionAssociationSource>| {
|
src: Option<DimensionAssociationSource>, secondary: Option<DimensionAssociationSource>| {
|
||||||
let screen = world_to_screen(world, view_rot, eye, bounds);
|
let screen = world_to_screen(world, view_rot, eye, bounds);
|
||||||
if !in_bounds(screen) {
|
if !in_bounds(screen) {
|
||||||
return;
|
return;
|
||||||
@@ -1245,6 +1253,8 @@ impl Snapper {
|
|||||||
extension_dir: None,
|
extension_dir: None,
|
||||||
viewport: None,
|
viewport: None,
|
||||||
source: src,
|
source: src,
|
||||||
|
secondary_source: secondary,
|
||||||
|
model_point: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1262,7 +1272,7 @@ impl Snapper {
|
|||||||
SnapHint::Endpoint => SnapType::Endpoint,
|
SnapHint::Endpoint => SnapType::Endpoint,
|
||||||
};
|
};
|
||||||
if self.is_on(snap_type) {
|
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() {
|
if let Some(points) = wires.snap_points() {
|
||||||
@@ -1298,8 +1308,7 @@ impl Snapper {
|
|||||||
try_pt(
|
try_pt(
|
||||||
DVec3::from_array(point),
|
DVec3::from_array(point),
|
||||||
SnapType::Endpoint,
|
SnapType::Endpoint,
|
||||||
wire_source(wire),
|
wire_source(wire), None);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// Tessellated open curves have no key-vertex set. Their only
|
// Tessellated open curves have no key-vertex set. Their only
|
||||||
// endpoints are first/last, so testing candidate wires remains
|
// endpoints are first/last, so testing candidate wires remains
|
||||||
@@ -1313,14 +1322,13 @@ impl Snapper {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !wire.points.is_empty() {
|
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 {
|
if wire.points.len() > 1 {
|
||||||
try_pt(
|
try_pt(
|
||||||
wp_f64(wire, wire.points.len() - 1),
|
wp_f64(wire, wire.points.len() - 1),
|
||||||
SnapType::Endpoint,
|
SnapType::Endpoint,
|
||||||
wire_source(wire),
|
wire_source(wire), None);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1328,7 +1336,7 @@ impl Snapper {
|
|||||||
let src = wire_source(wire);
|
let src = wire_source(wire);
|
||||||
if !wire.key_vertices.is_empty() {
|
if !wire.key_vertices.is_empty() {
|
||||||
for &point in &wire.key_vertices {
|
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 {
|
} else {
|
||||||
let closed = wire
|
let closed = wire
|
||||||
@@ -1337,14 +1345,13 @@ impl Snapper {
|
|||||||
.any(|(_, hint)| matches!(hint, SnapHint::Quadrant));
|
.any(|(_, hint)| matches!(hint, SnapHint::Quadrant));
|
||||||
if !closed {
|
if !closed {
|
||||||
if !wire.points.is_empty() {
|
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 {
|
if wire.points.len() > 1 {
|
||||||
try_pt(
|
try_pt(
|
||||||
wp_f64(wire, wire.points.len() - 1),
|
wp_f64(wire, wire.points.len() - 1),
|
||||||
SnapType::Endpoint,
|
SnapType::Endpoint,
|
||||||
src,
|
src, None);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1371,7 +1378,7 @@ impl Snapper {
|
|||||||
let a = DVec3::from_array(a);
|
let a = DVec3::from_array(a);
|
||||||
let b = DVec3::from_array(b);
|
let b = DVec3::from_array(b);
|
||||||
if a.distance_squared(b) > 1e-12 {
|
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 {
|
} else {
|
||||||
@@ -1381,7 +1388,7 @@ impl Snapper {
|
|||||||
let a = DVec3::from_array(segment[0]);
|
let a = DVec3::from_array(segment[0]);
|
||||||
let b = DVec3::from_array(segment[1]);
|
let b = DVec3::from_array(segment[1]);
|
||||||
if a.distance_squared(b) > 1e-12 {
|
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(
|
try_pt(
|
||||||
nearest_on_segment(cursor_world, seg.a, seg.b),
|
nearest_on_segment(cursor_world, seg.a, seg.b),
|
||||||
SnapType::Nearest,
|
SnapType::Nearest,
|
||||||
wires.source_wire(seg.wire).and_then(wire_source),
|
wires.source_wire(seg.wire).and_then(wire_source), None);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for wire in in_range_wires.iter() {
|
for wire in in_range_wires.iter() {
|
||||||
@@ -1404,7 +1410,7 @@ impl Snapper {
|
|||||||
for i in 0..wire.points.len().saturating_sub(1) {
|
for i in 0..wire.points.len().saturating_sub(1) {
|
||||||
let p =
|
let p =
|
||||||
nearest_on_segment(cursor_world, wp_f64(wire, i), wp_f64(wire, i + 1));
|
nearest_on_segment(cursor_world, 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(
|
try_pt(
|
||||||
foot,
|
foot,
|
||||||
SnapType::Perpendicular,
|
SnapType::Perpendicular,
|
||||||
wires.source_wire(seg.wire).and_then(wire_source),
|
wires.source_wire(seg.wire).and_then(wire_source), None);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1429,7 +1434,7 @@ impl Snapper {
|
|||||||
let src = wire_source(wire);
|
let src = wire_source(wire);
|
||||||
for i in 0..wire.points.len().saturating_sub(1) {
|
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)) {
|
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(
|
try_pt(
|
||||||
point,
|
point,
|
||||||
SnapType::Intersection,
|
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),
|
wp_f64(wire, index + 1),
|
||||||
) {
|
) {
|
||||||
if (point - origin).length_squared() > 1e-18 {
|
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.
|
// first. PR3 resolves the true pair.
|
||||||
let src = wire_source(wire_i);
|
let src = wire_source(wire_i);
|
||||||
for point in points {
|
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));
|
resolved_pairs.insert(pair_key(wire_i, wire_j));
|
||||||
}
|
}
|
||||||
@@ -1586,8 +1590,7 @@ impl Snapper {
|
|||||||
try_pt(
|
try_pt(
|
||||||
pt,
|
pt,
|
||||||
SnapType::Intersection,
|
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 {
|
if exact_cursor {
|
||||||
break 'intersection_sweep;
|
break 'intersection_sweep;
|
||||||
}
|
}
|
||||||
@@ -1609,7 +1612,7 @@ impl Snapper {
|
|||||||
if let Some(pts) = exact_curve_intersections(wire_i, wire_j) {
|
if let Some(pts) = exact_curve_intersections(wire_i, wire_j) {
|
||||||
let src = wire_source(wire_i);
|
let src = wire_source(wire_i);
|
||||||
for pt in pts {
|
for pt in pts {
|
||||||
try_pt(pt, SnapType::Intersection, src);
|
try_pt(pt, SnapType::Intersection, src, wire_source(wire_j));
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1633,7 +1636,7 @@ impl Snapper {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(pt) = seg_intersect_3d(a0, a1, b0, b1) {
|
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,
|
bounds,
|
||||||
self.osnap_radius_px,
|
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),
|
// feet (which sit closer to the cursor on their own lines),
|
||||||
// or the cursor would snap to a line instead of the crossing.
|
// 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);
|
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(
|
try_pt(
|
||||||
segment_a.a + ta as f64 * (segment_a.b - segment_a.a),
|
segment_a.a + ta as f64 * (segment_a.b - segment_a.a),
|
||||||
SnapType::ApparentIntersection,
|
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 {
|
if dist2(apparent_screen, cursor_screen) <= f32::EPSILON {
|
||||||
break 'apparent_sweep;
|
break 'apparent_sweep;
|
||||||
}
|
}
|
||||||
@@ -1800,8 +1802,7 @@ impl Snapper {
|
|||||||
try_pt(
|
try_pt(
|
||||||
wa0 + ta as f64 * (wa1 - wa0),
|
wa0 + ta as f64 * (wa1 - wa0),
|
||||||
SnapType::ApparentIntersection,
|
SnapType::ApparentIntersection,
|
||||||
wire_source(wire_i),
|
wire_source(wire_i), wire_source(wire_j));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2024,6 +2025,8 @@ impl Snapper {
|
|||||||
extension_dir: None,
|
extension_dir: None,
|
||||||
viewport: None,
|
viewport: None,
|
||||||
source: wire_source(wire),
|
source: wire_source(wire),
|
||||||
|
secondary_source: None,
|
||||||
|
model_point: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2088,6 +2091,8 @@ impl Snapper {
|
|||||||
extension_dir: None,
|
extension_dir: None,
|
||||||
viewport: None,
|
viewport: None,
|
||||||
source: wire_source(wire),
|
source: wire_source(wire),
|
||||||
|
secondary_source: None,
|
||||||
|
model_point: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user