forked from 1b-agent/opencadstudio-upstream
Track mixed paper and viewport dimension references
This commit is contained in:
1 parent
bd00d679a9
commit
5424a19482
28 files changed
+823
-846
No files matched your search
+21
-27
@@ -98,7 +98,7 @@ impl OpenCADStudio {
|
||||
self.tabs[i].snap_result = None;
|
||||
self.last_point = None;
|
||||
// Points collected in the space being left are meaningless in the new
|
||||
// one (PR1).
|
||||
// one.
|
||||
self.clear_accepted_snaps();
|
||||
self.pending_click_snap = None;
|
||||
self.snapper.from_point = None;
|
||||
@@ -386,7 +386,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
// Typed / dynamic-input / headless points carry no snap, but the
|
||||
// accepted-snap list must stay index-parallel with the points the
|
||||
// command collects (PR1). Interactive picks record themselves in
|
||||
// command collects. Interactive picks record themselves in
|
||||
// the click handler and never reach here.
|
||||
if !self.record_accepted_snap(i, None, None, *point) {
|
||||
return Task::none();
|
||||
@@ -404,7 +404,9 @@ impl OpenCADStudio {
|
||||
self.push_ucs_to_cmd(i);
|
||||
}
|
||||
if let StepInput::EntityPick(handle, point) = &input {
|
||||
if !self.dimension_acquisition_allowed(i, None) { return Task::none(); }
|
||||
if !self.dimension_acquisition_allowed(i, None) {
|
||||
return Task::none();
|
||||
}
|
||||
let solid_pick = matches!(
|
||||
self.tabs[i].scene.document.get_entity(*handle),
|
||||
Some(
|
||||
@@ -477,7 +479,11 @@ impl OpenCADStudio {
|
||||
}
|
||||
let ctrl = self.ctrl_down;
|
||||
let shift = self.shift_down;
|
||||
let picked_handle = if let StepInput::EntityPick(h, _) = &input { Some(*h) } else { None };
|
||||
let picked_handle = if let StepInput::EntityPick(h, _) = &input {
|
||||
Some(*h)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let result: Option<CmdResult> = {
|
||||
let Some(cmd) = self.tabs[i].active_cmd.as_mut() else {
|
||||
return Task::none();
|
||||
@@ -2366,17 +2372,15 @@ impl OpenCADStudio {
|
||||
// through a viewport carries the compensation as a negative
|
||||
// DIMLFAC override.
|
||||
if !preserve_base_style {
|
||||
crate::scene::creation_style::apply_current_creation_styles(&self.tabs[i].scene.document, &mut entity);
|
||||
crate::scene::creation_style::apply_current_creation_styles(
|
||||
&self.tabs[i].scene.document,
|
||||
&mut entity,
|
||||
);
|
||||
}
|
||||
if !self.apply_viewport_dimension_measurement(i, &mut entity) {
|
||||
return Task::none();
|
||||
}
|
||||
// ...and is deliberately *not* associated in paper space: its
|
||||
// definition points are projected paper coordinates, so a
|
||||
// paper-space association would bind it to geometry that is
|
||||
// not there. PR3 records the real dimension -> viewport ->
|
||||
// entity chain instead. One gate, shared with
|
||||
// `infer_dimension_sources_guarded`.
|
||||
// Projected points cannot use direct paper-space source inference.
|
||||
let association_allowed = self.dimension_association_allowed(i);
|
||||
let inherited_dimension = if preserve_base_style {
|
||||
match &entity {
|
||||
@@ -2438,20 +2442,15 @@ impl OpenCADStudio {
|
||||
} else {
|
||||
let delta_safe = self.delta_add_safe(i, &entity);
|
||||
let pending = self.begin_undo(i, label, 1, delta_safe);
|
||||
if let Some(handle) = self.commit_entity_handle_with_dimension_policy(
|
||||
entity,
|
||||
preserve_base_style,
|
||||
) {
|
||||
if let Some(handle) =
|
||||
self.commit_entity_handle_with_dimension_policy(entity, preserve_base_style)
|
||||
{
|
||||
if association_mode == 2 && association_allowed {
|
||||
let mut changes = vec![
|
||||
(handle, crate::scene::ChangeKind::Modified),
|
||||
];
|
||||
let mut changes = vec![(handle, crate::scene::ChangeKind::Modified)];
|
||||
match association {
|
||||
crate::command::DimensionAssociationInput::Infer(source) => {
|
||||
let sources: Vec<_> = source.map_or_else(
|
||||
|| {
|
||||
self.infer_dimension_sources_guarded(i, handle)
|
||||
},
|
||||
|| self.infer_dimension_sources_guarded(i, handle),
|
||||
|source| {
|
||||
if single_source_dimension {
|
||||
vec![Some(source)]
|
||||
@@ -2481,12 +2480,7 @@ impl OpenCADStudio {
|
||||
changes.dedup_by_key(|(handle, _)| handle.value());
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
} else if association_mode == 2 {
|
||||
// Measured through a layout viewport: a paper-space
|
||||
// association would bind projected coordinates to
|
||||
// geometry that is not on the sheet. Record the real
|
||||
// chain instead — dimension -> viewport -> block
|
||||
// path -> entity -> feature — from the AcceptedSnaps
|
||||
// PR1 captured, in definition-point slot order.
|
||||
// Preserve the acquired viewport and source paths.
|
||||
self.attach_viewport_dimension_association(i, handle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ impl OpenCADStudio {
|
||||
// is not constrained by ortho/polar relative to a previous command's endpoint.
|
||||
self.last_point = None;
|
||||
// A new command collects its own points, so the previous command's
|
||||
// accepted snaps must not leak into it (PR1).
|
||||
// accepted snaps must not leak into it.
|
||||
self.clear_accepted_snaps();
|
||||
// Starting a command restarts the right-click cycle, so its first
|
||||
// right-click acts as Enter rather than opening the context menu.
|
||||
|
||||
+26
-29
@@ -1,5 +1,5 @@
|
||||
//! Paper-space dimension acquisition and measurement. Only acquired geometry
|
||||
//! carries viewport compensation; free and typed sheet coordinates stay on the sheet.
|
||||
//! Paper-space dimension acquisition and measurement. Acquiring model geometry
|
||||
//! supplies a viewport scale; sheet-only dimensions retain paper units.
|
||||
|
||||
use super::OpenCADStudio;
|
||||
use crate::command::DimensionAssociationSource;
|
||||
@@ -14,7 +14,7 @@ use glam::DVec3;
|
||||
pub(crate) enum DimensionMeasureSpace {
|
||||
Direct,
|
||||
Viewport(ViewportFrame),
|
||||
Mixed,
|
||||
ConflictingViewports,
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
@@ -26,33 +26,23 @@ impl OpenCADStudio {
|
||||
selection.left_dragging = false;
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // Consumed by the dependent association change.
|
||||
pub(crate) fn dimension_measuring_snaps(&self, _i: usize) -> Vec<AcceptedSnap> {
|
||||
self.accepted_snaps().to_vec()
|
||||
}
|
||||
|
||||
pub(crate) fn dimension_measure_space(&self, i: usize) -> DimensionMeasureSpace {
|
||||
let scene = &self.tabs[i].scene;
|
||||
if scene.current_layout == "Model" || scene.active_viewport.is_some() {
|
||||
return DimensionMeasureSpace::Direct;
|
||||
}
|
||||
let mut frame: Option<ViewportFrame> = None;
|
||||
let mut direct = false;
|
||||
for snap in self.accepted_snaps() {
|
||||
match snap.frame {
|
||||
Some(f) => match frame {
|
||||
None => frame = Some(f),
|
||||
Some(previous) if previous.viewport == f.viewport => {}
|
||||
Some(_) => return DimensionMeasureSpace::Mixed,
|
||||
},
|
||||
None => direct = true,
|
||||
for f in self.accepted_snaps().iter().filter_map(|snap| snap.frame) {
|
||||
match frame {
|
||||
None => frame = Some(f),
|
||||
Some(previous) if previous.viewport == f.viewport => {}
|
||||
Some(_) => return DimensionMeasureSpace::ConflictingViewports,
|
||||
}
|
||||
}
|
||||
match (frame, direct) {
|
||||
(None, _) => DimensionMeasureSpace::Direct,
|
||||
(Some(f), false) => DimensionMeasureSpace::Viewport(f),
|
||||
_ => DimensionMeasureSpace::Mixed,
|
||||
}
|
||||
frame.map_or(
|
||||
DimensionMeasureSpace::Direct,
|
||||
DimensionMeasureSpace::Viewport,
|
||||
)
|
||||
}
|
||||
|
||||
/// Reject an incompatible measuring input before it advances the command.
|
||||
@@ -68,8 +58,15 @@ impl OpenCADStudio {
|
||||
{
|
||||
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());
|
||||
// Sheet geometry and free sheet points may accompany one viewport.
|
||||
// A second viewport would make the measurement scale ambiguous.
|
||||
if viewport.is_some_and(|viewport| {
|
||||
self.accepted_snaps()
|
||||
.iter()
|
||||
.filter_map(|s| s.viewport)
|
||||
.any(|previous| previous != viewport)
|
||||
}) {
|
||||
self.command_line.push_error(crate::t!("Model geometry must come from one viewport. Paper-space points can be included.").as_ref());
|
||||
return false;
|
||||
}
|
||||
true
|
||||
@@ -134,8 +131,8 @@ impl OpenCADStudio {
|
||||
);
|
||||
let frame = match self.dimension_measure_space(i) {
|
||||
DimensionMeasureSpace::Direct => return true,
|
||||
DimensionMeasureSpace::Mixed => {
|
||||
self.command_line.push_error(crate::t!("Pick geometry from the same viewport, or use paper-space points for the whole dimension.").as_ref());
|
||||
DimensionMeasureSpace::ConflictingViewports => {
|
||||
self.command_line.push_error(crate::t!("Model geometry must come from one viewport. Paper-space points can be included.").as_ref());
|
||||
return false;
|
||||
}
|
||||
DimensionMeasureSpace::Viewport(f) => f,
|
||||
@@ -214,13 +211,13 @@ impl OpenCADStudio {
|
||||
if slots.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let measuring = self.dimension_measuring_snaps(i);
|
||||
let measuring = self.accepted_snaps();
|
||||
slots
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, slot)| {
|
||||
let snap = measuring.get(index)?.clone();
|
||||
let frame = snap.frame?;
|
||||
let frame = snap.frame;
|
||||
let slot = DVec3::new(slot.x, slot.y, slot.z);
|
||||
if snap
|
||||
.paper_point
|
||||
@@ -232,7 +229,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
Some(AcceptedSnap {
|
||||
paper_point: slot,
|
||||
model_point: frame.paper_to_model(slot),
|
||||
model_point: frame.map_or(slot, |frame| frame.paper_to_model(slot)),
|
||||
..snap
|
||||
})
|
||||
})
|
||||
|
||||
+4
-13
@@ -717,21 +717,12 @@ pub(super) struct OpenCADStudio {
|
||||
show_layout_tabs: bool,
|
||||
/// Last point committed by a drawing command — used as ortho/polar base.
|
||||
last_point: Option<glam::DVec3>,
|
||||
/// Viewport frame the live snap marker is currently looking through (PR1).
|
||||
/// Set by the cursor-move handler when the displayed snap came from a
|
||||
/// layout viewport, cleared otherwise. Display/bookkeeping only — the point
|
||||
/// itself is already projected onto the sheet.
|
||||
/// Viewport used by the current snap; the displayed point is in paper space.
|
||||
pub(crate) vp_snap_frame: Option<crate::scene::viewport_ref::ViewportFrame>,
|
||||
/// Snaps accepted by the active command's point steps, parallel to the
|
||||
/// points it collected (PR1). A command that does not care keeps using the
|
||||
/// plain paper point; dimension commands (PR2) read the model point,
|
||||
/// viewport and frame from here, and PR3 reads `source`. Cleared whenever a
|
||||
/// command starts or ends.
|
||||
/// Acquired coordinates and source identities in command-step order.
|
||||
/// Cleared when the command starts or ends.
|
||||
accepted_snaps: Vec<crate::scene::viewport_ref::AcceptedSnap>,
|
||||
/// Snap resolved by the click currently being handled, with the viewport
|
||||
/// frame it came through (PR1). Set inside the click handler's point
|
||||
/// resolution so the point-accept path further down can record the
|
||||
/// [`crate::scene::viewport_ref::AcceptedSnap`] without recomputing.
|
||||
/// Click result retained until the command accepts its point.
|
||||
pending_click_snap: Option<(
|
||||
crate::snap::SnapResult,
|
||||
Option<crate::scene::viewport_ref::ViewportFrame>,
|
||||
|
||||
+16
-5
@@ -1277,13 +1277,22 @@ impl OpenCADStudio {
|
||||
&self.tabs[i].scene.document,
|
||||
d.base().common.handle,
|
||||
);
|
||||
let statuses = self.tabs[i].scene.dimension_association_status(d.base().common.handle);
|
||||
let slots = self.tabs[i].scene.dimension_association_slot_points(d.base().common.handle).len();
|
||||
let statuses = self.tabs[i]
|
||||
.scene
|
||||
.dimension_association_status(d.base().common.handle);
|
||||
let slots = self.tabs[i]
|
||||
.scene
|
||||
.dimension_association_slot_points(d.base().common.handle)
|
||||
.len();
|
||||
let status = if statuses.is_empty() {
|
||||
t!("Nonassociative")
|
||||
} else if statuses.iter().any(|(_, status)| matches!(status, crate::scene::ReferenceStatus::Broken(_))) {
|
||||
} else if statuses.iter().any(|(_, status)| {
|
||||
matches!(status, crate::scene::ReferenceStatus::Broken(_))
|
||||
}) {
|
||||
t!("Broken reference")
|
||||
} else if statuses.iter().any(|(_, status)| matches!(status, crate::scene::ReferenceStatus::Unresolved)) {
|
||||
} else if statuses.iter().any(|(_, status)| {
|
||||
matches!(status, crate::scene::ReferenceStatus::Unresolved)
|
||||
}) {
|
||||
t!("Unresolved reference")
|
||||
} else if statuses.len() < slots {
|
||||
t!("Partially associated")
|
||||
@@ -1293,7 +1302,9 @@ impl OpenCADStudio {
|
||||
general.props.push(crate::scene::model::object::Property {
|
||||
label: t!("Association status").into_owned(),
|
||||
field: "association_status",
|
||||
value: crate::scene::model::object::PropValue::ReadOnly(status.into_owned()),
|
||||
value: crate::scene::model::object::PropValue::ReadOnly(
|
||||
status.into_owned(),
|
||||
),
|
||||
});
|
||||
general.props.push(crate::scene::model::object::Property {
|
||||
label: t!("Associative").into_owned(),
|
||||
|
||||
+38
-18
@@ -2059,24 +2059,24 @@ impl OpenCADStudio {
|
||||
)
|
||||
};
|
||||
|
||||
// Paper-space snapping THROUGH layout viewports (PR1). `edit_cam`
|
||||
// Paper-space snapping through layout viewports. `edit_cam`
|
||||
// is None here, so `cursor_world` is the paper point and the sheet
|
||||
// fills the canvas (pane-local == canvas pixels). The viewport hit
|
||||
// arrives already projected onto the sheet, so everything
|
||||
// downstream keeps working in paper coordinates — LINE still draws
|
||||
// at the projected paper point.
|
||||
self.vp_snap_frame = None;
|
||||
if edit_cam.is_none() && !needs_entity && !is_gathering && !needs_structure
|
||||
if edit_cam.is_none()
|
||||
&& !needs_entity
|
||||
&& !is_gathering
|
||||
&& !needs_structure
|
||||
&& !needs_tan
|
||||
{
|
||||
if let Some((vp_hit, frame)) =
|
||||
self.paper_viewport_snap(i, p_full, vp_size, cursor_world)
|
||||
{
|
||||
let merged = crate::snap::merge_snap(
|
||||
self.tabs[i].snap_result,
|
||||
Some(vp_hit),
|
||||
p_full,
|
||||
);
|
||||
let merged =
|
||||
crate::snap::merge_snap(self.tabs[i].snap_result, Some(vp_hit), p_full);
|
||||
if merged.is_some_and(|hit| hit.viewport.is_some()) {
|
||||
self.vp_snap_frame = Some(frame);
|
||||
}
|
||||
@@ -3701,7 +3701,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Paper-space snapping through layout viewports (PR1). Mirrors
|
||||
// Paper-space snapping through layout viewports. Mirrors
|
||||
// the cursor-move pass: the click recomputes snapping from
|
||||
// scratch, so the viewport query has to run here too. The hit
|
||||
// comes back already projected onto the sheet.
|
||||
@@ -3710,8 +3710,7 @@ impl OpenCADStudio {
|
||||
if let Some((vp_hit, frame)) =
|
||||
self.paper_viewport_snap(i, p_full, (vw, vh), raw)
|
||||
{
|
||||
let merged =
|
||||
crate::snap::merge_snap(snap_hit, Some(vp_hit), p_full);
|
||||
let merged = crate::snap::merge_snap(snap_hit, Some(vp_hit), p_full);
|
||||
if merged.is_some_and(|hit| hit.viewport.is_some()) {
|
||||
click_frame = Some(frame);
|
||||
}
|
||||
@@ -4046,7 +4045,10 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
if !self.dimension_acquisition_allowed(i, None) { self.finish_command_click(i); return Task::none(); }
|
||||
if !self.dimension_acquisition_allowed(i, None) {
|
||||
self.finish_command_click(i);
|
||||
return Task::none();
|
||||
}
|
||||
let shift = self.shift_down;
|
||||
let result = self.tabs[i].active_cmd.as_mut().map(|c| {
|
||||
// Shift-swap state for TRIM/EXTEND (#336).
|
||||
@@ -4126,11 +4128,21 @@ impl OpenCADStudio {
|
||||
}
|
||||
result
|
||||
} else if let Some(result) = {
|
||||
let dimension = self.tabs[i].active_cmd.as_ref().is_some_and(|c| c.measures_through_viewports());
|
||||
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 }
|
||||
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]
|
||||
@@ -4139,7 +4151,12 @@ impl OpenCADStudio {
|
||||
.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) {
|
||||
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();
|
||||
}
|
||||
@@ -4212,7 +4229,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
// Record what this point step accepted, alongside the point
|
||||
// itself (PR1): paper point, model point, viewport, frame and
|
||||
// itself: paper point, model point, viewport, frame and
|
||||
// geometry identity. Commands that don't care keep consuming
|
||||
// `world_pt` exactly as before.
|
||||
let pending = self.pending_click_snap.take();
|
||||
@@ -4221,7 +4238,10 @@ impl OpenCADStudio {
|
||||
pending.map(|(hit, _)| hit),
|
||||
pending.and_then(|(_, frame)| frame),
|
||||
world_pt,
|
||||
) { self.finish_command_click(i); return Task::none(); }
|
||||
) {
|
||||
self.finish_command_click(i);
|
||||
return Task::none();
|
||||
}
|
||||
self.last_point = Some(world_pt);
|
||||
// The one-shot snap override is spent by this pick —
|
||||
// restore the running osnap configuration (#337).
|
||||
|
||||
@@ -1,24 +1,7 @@
|
||||
//! Paper-space snapping THROUGH layout viewports (PR1).
|
||||
//!
|
||||
//! In a paper layout with no active viewport, `Scene::hit_test_wires` returns
|
||||
//! only the paper sheet's own entities — viewport contents are deliberately not
|
||||
//! interactive, and that must stay true for *selection*. Snapping, however,
|
||||
//! should see the model geometry a viewport displays, so a paper-space LINE /
|
||||
//! DIMENSION can pick a real model endpoint and land on the paper pixel where
|
||||
//! it is drawn.
|
||||
//!
|
||||
//! This module adds that as a SEPARATE query. It never changes
|
||||
//! `hit_test_wires`. For each displayed content viewport under the cursor
|
||||
//! (top-most first) it runs the EXISTING snap engine against that viewport's
|
||||
//! resident model wires, using the viewport's own camera and screen rectangle
|
||||
//! (`Scene::viewport_edit_frame_for` — the same adapter MSPACE editing uses),
|
||||
//! then maps the accepted model point back onto the sheet with
|
||||
//! [`ViewportFrame::model_to_paper`].
|
||||
//!
|
||||
//! Performance: no projected geometry is rebuilt per pointer event. The wire
|
||||
//! set comes from `model_wires_for_viewport_arc`, which is the renderer's own
|
||||
//! resident, camera-independent `Arc`, and the cursor-local broad phase reuses
|
||||
//! the shared interaction index built over it.
|
||||
//! Snap to displayed model geometry while a paper-space command is active.
|
||||
//! Viewport queries use the renderer's resident wires and interaction index,
|
||||
//! then project the accepted model point onto the sheet. Ordinary selection
|
||||
//! continues to use paper-space hit testing.
|
||||
|
||||
use iced::Point;
|
||||
|
||||
@@ -34,37 +17,14 @@ const MAX_ACCEPTED_SNAPS: usize = 64;
|
||||
|
||||
impl OpenCADStudio {
|
||||
/// Snaps accepted by the active command's point steps, oldest first, in
|
||||
/// step order. Consumed by the dimension commands in PR2/PR3.
|
||||
#[allow(dead_code)]
|
||||
/// step order.
|
||||
pub(crate) fn accepted_snaps(&self) -> &[AcceptedSnap] {
|
||||
&self.accepted_snaps
|
||||
}
|
||||
|
||||
/// The most recently accepted snap, if any.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn last_accepted_snap(&self) -> Option<&AcceptedSnap> {
|
||||
self.accepted_snaps.last()
|
||||
}
|
||||
|
||||
/// `Scene::infer_dimension_sources`, suppressed when the dimension being
|
||||
/// created measures through a layout viewport.
|
||||
///
|
||||
/// Inference works by looking for geometry near the dimension's own
|
||||
/// definition points, in whatever space it is handed. A paper-space
|
||||
/// dimension measured through a viewport has PAPER definition points that
|
||||
/// happen to sit over projected model geometry, so unguarded inference
|
||||
/// would either associate it with unrelated paper-sheet geometry at those
|
||||
/// coordinates, or match model geometry using paper coordinates. Neither
|
||||
/// is correct, and a wrong association is worse than none. Returning an
|
||||
/// empty source list makes `attach_dimension_association` a no-op.
|
||||
///
|
||||
/// The classification is
|
||||
/// [`OpenCADStudio::dimension_measure_space`] — the *same* one that
|
||||
/// decides whether to compensate the measurement, so the two can never
|
||||
/// disagree. In particular it ignores the trailing dimension-line
|
||||
/// placement click, which routinely lands inside a viewport rectangle for
|
||||
/// an ordinary paper-space dimension and must not suppress its
|
||||
/// association.
|
||||
/// Infer direct sources only when the measurement uses the active space.
|
||||
/// Projected model points must retain their acquired source identities;
|
||||
/// proximity on the sheet could bind them to unrelated paper geometry.
|
||||
pub(crate) fn infer_dimension_sources_guarded(
|
||||
&self,
|
||||
tab: usize,
|
||||
@@ -76,14 +36,8 @@ impl OpenCADStudio {
|
||||
self.tabs[tab].scene.infer_dimension_sources(dimension)
|
||||
}
|
||||
|
||||
/// The single gate on creating a paper-space association for the
|
||||
/// dimension the active command is committing.
|
||||
///
|
||||
/// `false` exactly when the dimension measures through (or partly through)
|
||||
/// a layout viewport, whatever supplied its sources — inference,
|
||||
/// an explicit object pick, or an explicit source list. Everything in the
|
||||
/// commit path that used to decide this for itself now asks here, so the
|
||||
/// measurement rule and the association rule cannot drift apart.
|
||||
/// Whether the dimension can use ordinary active-space associations.
|
||||
/// Placement clicks are excluded by the measurement classification.
|
||||
pub(crate) fn dimension_association_allowed(&self, tab: usize) -> bool {
|
||||
matches!(
|
||||
self.dimension_measure_space(tab),
|
||||
@@ -177,7 +131,7 @@ impl OpenCADStudio {
|
||||
) else {
|
||||
return false;
|
||||
};
|
||||
let distance = crate::scene::viewport_dim_seam::feature_pick_distance(
|
||||
let distance = crate::scene::viewport_dimension_pick::feature_pick_distance(
|
||||
&entity,
|
||||
accepted.model_point,
|
||||
Some(source.snap_type),
|
||||
@@ -207,33 +161,15 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
/// Append an already-built accepted snap, keeping the retention cap.
|
||||
///
|
||||
/// Used by paths that are not point steps (the dimension object pick
|
||||
/// resolved through a viewport) and therefore never reach the click
|
||||
/// handler.
|
||||
/// Retain a bounded history for point and object picks.
|
||||
pub(crate) fn push_accepted_snap(&mut self, accepted: AcceptedSnap) {
|
||||
if self.accepted_snaps.len() >= MAX_ACCEPTED_SNAPS {
|
||||
self.accepted_snaps.remove(0);
|
||||
}
|
||||
self.accepted_snaps.push(accepted);
|
||||
}
|
||||
/// Snap the paper-space cursor to model geometry seen through a layout
|
||||
/// viewport.
|
||||
///
|
||||
/// * `i` — tab index.
|
||||
/// * `cursor_canvas` — cursor in canvas pixels.
|
||||
/// * `canvas` — canvas size in pixels.
|
||||
/// * `cursor_paper` — the paper-space point under the cursor.
|
||||
///
|
||||
/// Returns the hit expressed in PAPER coordinates (`world` projected onto
|
||||
/// the sheet, `screen` in canvas pixels) together with the frame it came
|
||||
/// through, so the caller can merge it with the ordinary paper-sheet snap
|
||||
/// and later recover the model point.
|
||||
///
|
||||
/// `None` in the Model layout, while a viewport is active (MSPACE already
|
||||
/// snaps in model space), when snapping is off, or when the cursor is not
|
||||
/// over a displayed viewport.
|
||||
/// Query visible viewport geometry with a canvas-pixel cursor and sheet point.
|
||||
/// Returns paper coordinates and canvas pixels, plus the source viewport frame.
|
||||
pub(in crate::app) fn paper_viewport_snap(
|
||||
&mut self,
|
||||
i: usize,
|
||||
@@ -316,10 +252,7 @@ impl OpenCADStudio {
|
||||
let Some(hit) = hit.filter(|h| h.snap_type != SnapType::Grid) else {
|
||||
continue;
|
||||
};
|
||||
// The wire set is the whole model, not a clipped copy, so the only
|
||||
// way a feature outside the visible part could be offered is via
|
||||
// the aperture reaching past the clip. Reject those: a snap must be
|
||||
// a real feature the viewport actually displays.
|
||||
// The snap aperture can reach outside the visible clip boundary.
|
||||
let paper = frame.model_to_paper(hit.world);
|
||||
if !self.tabs[i]
|
||||
.scene
|
||||
|
||||
@@ -149,24 +149,13 @@ fn viewport_dimension_creation_preserves_active_units_and_roundtrips() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_dimension_mixed_and_degenerate_picks_allow_retry() {
|
||||
fn viewport_dimension_conflicting_viewports_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
|
||||
@@ -318,7 +307,12 @@ fn viewport_dimension_eligibility_is_shared_and_uses_clipping() {
|
||||
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,
|
||||
scene
|
||||
.viewport_frames_at_paper_point(paper)
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.viewport,
|
||||
top
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -340,7 +334,12 @@ fn viewport_dimension_eligibility_is_shared_and_uses_clipping() {
|
||||
vp.clip_boundary_handle = clip;
|
||||
}
|
||||
assert_eq!(
|
||||
scene.viewport_frame_at_paper_point(paper).unwrap().viewport,
|
||||
scene
|
||||
.viewport_frames_at_paper_point(paper)
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.viewport,
|
||||
first.viewport
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -354,7 +353,7 @@ fn viewport_dimension_eligibility_is_shared_and_uses_clipping() {
|
||||
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.viewport_frames_at_paper_point(paper).is_empty());
|
||||
assert!(scene.dimension_pick_through_viewport(paper, 0.2).is_none());
|
||||
}
|
||||
|
||||
@@ -623,8 +622,8 @@ fn viewport_dimension_spline_tangency_is_parallel_and_validated() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "Set OPENCAD_VIEWPORT_REGRESSION_DXF to the original local regression drawing"]
|
||||
fn viewport_dimension_original_drawing_keeps_unchanged_measurements() {
|
||||
#[ignore = "Set OPENCAD_VIEWPORT_REGRESSION_DXF to an external regression fixture"]
|
||||
fn viewport_dimension_external_fixture_keeps_unchanged_measurements() {
|
||||
use crate::scene::ChangeKind;
|
||||
let path = std::env::var("OPENCAD_VIEWPORT_REGRESSION_DXF").expect("regression drawing path");
|
||||
let doc = acadrust::DxfReader::from_file(std::path::Path::new(&path))
|
||||
@@ -820,7 +819,9 @@ fn viewport_dimension_nested_block_path_survives_edit_and_copy() {
|
||||
let root = scene.add_entity(EntityType::Insert(root));
|
||||
scene.set_current_layout("Dimensions".into());
|
||||
let transform =
|
||||
crate::scene::dimension_assoc_chain::block_transform(&scene.document, &[root, nested]);
|
||||
crate::scene::dimension_assoc_chain::walk_chain(&scene.document, &[root, nested, leaf])
|
||||
.unwrap()
|
||||
.transform;
|
||||
let model = |x| {
|
||||
let v = transform.apply(Vector3::new(x, 0.0, 0.0));
|
||||
DVec3::new(v.x, v.y, v.z)
|
||||
@@ -1095,3 +1096,249 @@ fn viewport_dimension_quadrant_keeps_its_feature_after_translation() {
|
||||
.bump_entities(&[(source, ChangeKind::Modified)]);
|
||||
assert!((displayed(&app.tabs[i].scene.document) - 400.0).abs() < 1e-5);
|
||||
}
|
||||
|
||||
fn start_centerline_dimension(
|
||||
command: &str,
|
||||
paper_first: bool,
|
||||
paper_kind: SnapType,
|
||||
factor: f64,
|
||||
) -> (OpenCADStudio, Handle, Handle, ViewportFrame) {
|
||||
let (mut app, model, frame) = fixture();
|
||||
let i = app.active_tab;
|
||||
let paper = app.tabs[i]
|
||||
.scene
|
||||
.add_entity(EntityType::Line(Line::from_points(
|
||||
Vector3::new(55.0, 40.0, 0.0),
|
||||
Vector3::new(55.0, 60.0, 0.0),
|
||||
)));
|
||||
let mut style = acadrust::tables::DimStyle::new("CenterlineUnits");
|
||||
style.dimlfac = factor;
|
||||
app.tabs[i].scene.document.dim_styles.add(style).unwrap();
|
||||
app.tabs[i].scene.document.header.current_dimstyle_name = "CenterlineUnits".into();
|
||||
let _ = app.dispatch_command(command);
|
||||
let mut paper_hit = hit(frame, paper, DVec3::new(55.0, 50.0, 0.0));
|
||||
paper_hit.world = DVec3::new(55.0, 50.0, 0.0);
|
||||
paper_hit.model_point = None;
|
||||
paper_hit.viewport = None;
|
||||
paper_hit.snap_type = paper_kind;
|
||||
let mut picks = [
|
||||
(paper_hit, None),
|
||||
(hit(frame, model, DVec3::X * 100.0), Some(frame)),
|
||||
];
|
||||
if !paper_first {
|
||||
picks.reverse();
|
||||
}
|
||||
for (snap, context) in picks {
|
||||
assert!(app.record_accepted_snap(i, Some(snap), context, snap.world));
|
||||
let result = app.tabs[i]
|
||||
.active_cmd
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.on_point(snap.world);
|
||||
app.sync_dimension_snaps(i);
|
||||
let _ = app.apply_cmd_result(result);
|
||||
}
|
||||
(app, model, paper, frame)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_dimension_paper_centerline_uses_viewport_units_in_either_order() {
|
||||
for command in ["DIMLINEAR", "DIMALIGNED"] {
|
||||
for paper_first in [true, false] {
|
||||
let (mut app, _, paper, _) =
|
||||
start_centerline_dimension(command, paper_first, SnapType::Midpoint, 25.4);
|
||||
let paper_snap = app
|
||||
.accepted_snaps()
|
||||
.iter()
|
||||
.find(|s| s.viewport.is_none())
|
||||
.unwrap();
|
||||
assert_eq!(paper_snap.source.as_ref().unwrap().source.handle, paper);
|
||||
assert_eq!(paper_snap.paper_point, paper_snap.model_point);
|
||||
let _ = app.feed_command(StepInput::Point(DVec3::new(60.0, 80.0, 0.0)));
|
||||
let doc = &app.tabs[app.active_tab].scene.document;
|
||||
assert!(
|
||||
(displayed(doc) - 1270.0).abs() < 1e-3,
|
||||
"{command}: {}",
|
||||
displayed(doc)
|
||||
);
|
||||
app.undo_steps(1);
|
||||
app.redo_steps(1);
|
||||
assert!((displayed(&app.tabs[app.active_tab].scene.document) - 1270.0).abs() < 1e-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_dimension_paper_centerline_tracks_each_owning_space() {
|
||||
use crate::scene::{ChangeKind, ReferenceStatus};
|
||||
for (paper_first, kind) in [(true, SnapType::Midpoint), (false, SnapType::Perpendicular)] {
|
||||
let (mut app, model, paper, frame) =
|
||||
start_centerline_dimension("DIMALIGNED", paper_first, kind, 1.0);
|
||||
let i = app.active_tab;
|
||||
let _ = app.feed_command(StepInput::Point(DVec3::new(60.0, 80.0, 0.0)));
|
||||
let dim = dimension(&app.tabs[i].scene.document).base().common.handle;
|
||||
assert!((displayed(&app.tabs[i].scene.document) - 50.0).abs() < 1e-4);
|
||||
let assoc = app.tabs[i].scene.dimension_association(dim).unwrap();
|
||||
assert!(assoc.trans_space);
|
||||
assert!(assoc
|
||||
.references
|
||||
.iter()
|
||||
.flatten()
|
||||
.any(|r| r.xrefs == vec![paper]));
|
||||
assert!(assoc
|
||||
.references
|
||||
.iter()
|
||||
.flatten()
|
||||
.any(|r| r.xrefs == vec![frame.viewport, model]));
|
||||
assert!(app.tabs[i]
|
||||
.scene
|
||||
.dimension_association_status(dim)
|
||||
.iter()
|
||||
.all(|(_, s)| *s == ReferenceStatus::Resolved));
|
||||
let pending = app.begin_undo(i, "Move paper centerline", 1, true).unwrap();
|
||||
let before = app.tabs[i].scene.document.get_entity_arc(paper);
|
||||
app.tabs[i].scene.record_undo_before(paper, before);
|
||||
let EntityType::Line(line) = app.tabs[i].scene.document.get_entity_mut(paper).unwrap()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
line.start.x = 56.0;
|
||||
line.end.x = 56.0;
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.bump_entities(&[(paper, ChangeKind::Modified)]);
|
||||
app.commit_undo_delta(i, pending);
|
||||
assert!((displayed(&app.tabs[i].scene.document) - 40.0).abs() < 1e-4);
|
||||
app.undo_steps(1);
|
||||
assert!((displayed(&app.tabs[i].scene.document) - 50.0).abs() < 1e-4);
|
||||
app.redo_steps(1);
|
||||
let EntityType::Viewport(vp) = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity_mut(frame.viewport)
|
||||
.unwrap()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
vp.view_height = 500.0;
|
||||
vp.custom_scale = 0.2;
|
||||
app.tabs[i].scene.notify_viewport_changed(frame.viewport);
|
||||
// Model endpoint now appears at paper x=70; paper centerline stays x=56.
|
||||
assert!((displayed(&app.tabs[i].scene.document) - 70.0).abs() < 1e-4);
|
||||
let EntityType::Viewport(vp) = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity_mut(frame.viewport)
|
||||
.unwrap()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
vp.view_target.x = 20.0;
|
||||
app.tabs[i].scene.notify_viewport_changed(frame.viewport);
|
||||
assert!((displayed(&app.tabs[i].scene.document) - 50.0).abs() < 1e-4);
|
||||
let EntityType::Line(line) = app.tabs[i].scene.document.get_entity_mut(model).unwrap()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
line.end.x = 160.0;
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.bump_entities(&[(model, ChangeKind::Modified)]);
|
||||
assert!((displayed(&app.tabs[i].scene.document) - 110.0).abs() < 1e-4);
|
||||
for ext in ["dxf", "dwg"] {
|
||||
let doc = &app.tabs[i].scene.document;
|
||||
let bytes = crate::io::save_to_bytes(doc, ext, doc.version).unwrap();
|
||||
let mut scene = Scene::new();
|
||||
scene.document = crate::io::load_bytes(&format!("centerline.{ext}"), bytes).unwrap();
|
||||
scene.notify_viewport_changed(frame.viewport);
|
||||
assert!((displayed(&scene.document) - 110.0).abs() < 1e-4, "{ext}");
|
||||
assert!(scene
|
||||
.dimension_association_status(dim)
|
||||
.iter()
|
||||
.all(|(_, s)| *s == ReferenceStatus::Resolved));
|
||||
}
|
||||
let before = app.tabs[i].scene.document.get_entity(dim).unwrap().clone();
|
||||
let model_owner = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(model)
|
||||
.unwrap()
|
||||
.common()
|
||||
.owner_handle;
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity_mut(paper)
|
||||
.unwrap()
|
||||
.common_mut()
|
||||
.owner_handle = model_owner;
|
||||
app.tabs[i].scene.notify_viewport_changed(frame.viewport);
|
||||
assert_eq!(app.tabs[i].scene.document.get_entity(dim).unwrap(), &before);
|
||||
assert!(app.tabs[i]
|
||||
.scene
|
||||
.dimension_association_status(dim)
|
||||
.iter()
|
||||
.any(|(_, s)| *s == ReferenceStatus::Unresolved));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_dimension_invalid_paths_leave_no_partial_association() {
|
||||
use crate::scene::viewport_ref::SnapSourceRef;
|
||||
use acadrust::entities::{DimensionAligned, Insert};
|
||||
use acadrust::tables::BlockRecord;
|
||||
|
||||
let (mut app, model, frame) = fixture();
|
||||
let scene = &mut app.tabs[app.active_tab].scene;
|
||||
let mut block = BlockRecord::new("Collapsed");
|
||||
block.handle = scene.document.allocate_handle();
|
||||
let owner = block.handle;
|
||||
scene.document.block_records.add(block).unwrap();
|
||||
let mut leaf = Line::from_points(Vector3::ZERO, Vector3::UNIT_X);
|
||||
leaf.common.owner_handle = owner;
|
||||
let leaf = scene.document.add_entity(EntityType::Line(leaf)).unwrap();
|
||||
scene.set_current_layout("Model".into());
|
||||
let mut insert = Insert::new("Collapsed", Vector3::ZERO);
|
||||
insert.set_x_scale(0.0);
|
||||
insert.set_y_scale(0.0);
|
||||
let root = scene.add_entity(EntityType::Insert(insert));
|
||||
scene.set_current_layout("Dimensions".into());
|
||||
let paper = scene.add_entity(EntityType::Line(Line::from_points(
|
||||
Vector3::new(55.0, 50.0, 0.0),
|
||||
Vector3::new(55.0, 60.0, 0.0),
|
||||
)));
|
||||
for (source, path) in [(model, vec![Handle::new(u64::MAX)]), (leaf, vec![root])] {
|
||||
let dim = scene.add_entity(EntityType::Dimension(Dimension::Aligned(
|
||||
DimensionAligned::new(Vector3::new(50.0, 50.0, 0.0), Vector3::new(55.0, 50.0, 0.0)),
|
||||
)));
|
||||
let invalid = AcceptedSnap {
|
||||
paper_point: DVec3::new(50.0, 50.0, 0.0),
|
||||
model_point: DVec3::ZERO,
|
||||
viewport: Some(frame.viewport),
|
||||
frame: Some(frame),
|
||||
source: Some(SnapSourceRef {
|
||||
source: DimensionAssociationSource::inferred(source),
|
||||
block_path: path,
|
||||
snap_type: SnapType::Endpoint,
|
||||
intersection: None,
|
||||
}),
|
||||
};
|
||||
let mut valid = AcceptedSnap::free(DVec3::new(55.0, 50.0, 0.0));
|
||||
valid.source = Some(SnapSourceRef {
|
||||
source: DimensionAssociationSource::inferred(paper),
|
||||
block_path: Vec::new(),
|
||||
snap_type: SnapType::Endpoint,
|
||||
intersection: None,
|
||||
});
|
||||
scene.attach_viewport_dimension_association(dim, &[Some(invalid), Some(valid)]);
|
||||
let association = scene.dimension_association(dim).unwrap();
|
||||
assert!(!association.trans_space);
|
||||
assert!(association.references[0].is_empty());
|
||||
assert_eq!(association.references[1][0].xrefs, vec![paper]);
|
||||
assert_eq!(scene.dimension_association_sources(dim), vec![paper]);
|
||||
assert_eq!(
|
||||
scene.dimension_association_status(dim),
|
||||
vec![(1, crate::scene::ReferenceStatus::Resolved)]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user