mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-14 14:26:45 -04:00
Fix Measure Distance snapping through layout viewports (#616)
Squash of PR #616 by @PedroGuille. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
ce1cc631d5
commit
a181bbd609
8 files changed
+672
-30
No files matched your search
@@ -347,8 +347,10 @@ mod count;
|
||||
#[derive(Default)]
|
||||
struct DistM {
|
||||
first: Option<Vec2>,
|
||||
first_source: Option<crate::snap::ViewportPoint>,
|
||||
/// Multiple points: the points so far, the first included.
|
||||
multi: Option<Vec<Vec2>>,
|
||||
sources: Vec<Option<crate::snap::ViewportPoint>>,
|
||||
/// Asking for the length of a segment (the Length option).
|
||||
length: bool,
|
||||
}
|
||||
@@ -390,6 +392,12 @@ fn length_input(pts: &[Vec2], i: Input) -> Result<Option<Vec2>> {
|
||||
Ok(extend_last(pts, len))
|
||||
}
|
||||
|
||||
/// Model units apply only when every point was snapped through one and the same viewport.
|
||||
fn model_points(sources: &[Option<crate::snap::ViewportPoint>]) -> Option<Vec<Vec2>> {
|
||||
let viewport = sources.first().copied().flatten()?.viewport;
|
||||
sources.iter().map(|s| s.filter(|s| s.viewport == viewport).map(|s| s.point)).collect()
|
||||
}
|
||||
|
||||
impl Interactive for DistM {
|
||||
fn name(&self) -> &'static str {
|
||||
"DIST"
|
||||
@@ -413,9 +421,20 @@ impl Interactive for DistM {
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
if let Some(pts) = &mut self.multi {
|
||||
if self.length {
|
||||
if let Some(p) = length_input(pts, i)? {
|
||||
pts.push(p);
|
||||
s.echo(format!("Distance = {}", fmt_d(s, path_len(pts))));
|
||||
let model = model_points(&self.sources);
|
||||
if let Some(p) = length_input(model.as_deref().unwrap_or(pts), i)? {
|
||||
let source = model.as_ref().and_then(|_| self.sources.last().copied().flatten());
|
||||
let paper = match (source, pts.last()) {
|
||||
(Some(source), Some(last)) => *last + (p - source.point) * source.scale,
|
||||
_ => p,
|
||||
};
|
||||
if !paper.is_finite() {
|
||||
return Err(bad("dist", "the next point is too large"));
|
||||
}
|
||||
pts.push(paper);
|
||||
self.sources.push(source.map(|source| crate::snap::ViewportPoint { point: p, ..source }));
|
||||
let model = model_points(&self.sources);
|
||||
s.echo(format!("Distance = {}", fmt_d(s, path_len(model.as_deref().unwrap_or(pts)))));
|
||||
}
|
||||
self.length = false;
|
||||
return Ok(Step::Continue);
|
||||
@@ -423,11 +442,14 @@ impl Interactive for DistM {
|
||||
match i {
|
||||
Input::Point(p) => {
|
||||
pts.push(p);
|
||||
s.echo(format!("Distance = {}", fmt_d(s, path_len(pts))));
|
||||
self.sources.push(s.picked_viewport);
|
||||
let model = model_points(&self.sources);
|
||||
s.echo(format!("Distance = {}", fmt_d(s, path_len(model.as_deref().unwrap_or(pts)))));
|
||||
}
|
||||
Input::Keyword(k) if k == "Undo" => {
|
||||
if pts.len() > 1 {
|
||||
pts.pop();
|
||||
self.sources.pop();
|
||||
}
|
||||
}
|
||||
Input::Keyword(k) if k == "Length" => self.length = true,
|
||||
@@ -441,9 +463,14 @@ impl Interactive for DistM {
|
||||
match (self.first, i) {
|
||||
(None, Input::Point(p)) => {
|
||||
self.first = Some(p);
|
||||
self.first_source = s.picked_viewport;
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
(Some(a), Input::Point(b)) => {
|
||||
let (a, b) = match (self.first_source, s.picked_viewport) {
|
||||
(Some(first), Some(second)) if first.viewport == second.viewport => (first.point, second.point),
|
||||
_ => (a, b),
|
||||
};
|
||||
let t = dist_text(s, a, b);
|
||||
for l in t.lines() {
|
||||
s.echo(l.to_string());
|
||||
@@ -452,6 +479,7 @@ impl Interactive for DistM {
|
||||
}
|
||||
(Some(a), Input::Keyword(k)) if k == "Multiple points" => {
|
||||
self.multi = Some(vec![a]);
|
||||
self.sources = vec![self.first_source];
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
(_, Input::Enter) => Ok(Step::Done),
|
||||
@@ -472,7 +500,8 @@ impl Interactive for DistM {
|
||||
|
||||
impl DistM {
|
||||
fn total(&self, s: &mut Session) -> Result<Step> {
|
||||
let d = self.multi.as_deref().map_or(0.0, path_len);
|
||||
let model = model_points(&self.sources);
|
||||
let d = model.as_deref().or(self.multi.as_deref()).map_or(0.0, path_len);
|
||||
s.echo(format!("Distance = {}", fmt_d(s, d)));
|
||||
Ok(Step::Done)
|
||||
}
|
||||
|
||||
@@ -556,6 +556,8 @@ pub struct Session {
|
||||
/// The deferred tangent/perpendicular snap under the cursor, if any (from the UI; lets the
|
||||
/// rubber band show the line it resolves to).
|
||||
pub cursor_deferred: Option<snap::Deferred>,
|
||||
/// Origin of the point currently being fed by `input_snapped`; never reused by typed points.
|
||||
picked_viewport: Option<snap::ViewportPoint>,
|
||||
/// The last drawing comparison (COMPARE), drawn over the drawing it was made for.
|
||||
pub compare: Option<cmd::file::compare::Comparison>,
|
||||
/// Object snap tracking: acquired points and the path under the cursor (from the UI's
|
||||
@@ -606,6 +608,7 @@ impl Session {
|
||||
point_mods: Vec::new(),
|
||||
cursor: Vec2::ZERO,
|
||||
cursor_deferred: None,
|
||||
picked_viewport: None,
|
||||
compare: None,
|
||||
tracking: snap::tracking::Tracker::default(),
|
||||
viewport_px: (1200.0, 800.0),
|
||||
@@ -841,6 +844,16 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// Feed an object-snap pick, retaining its viewport origin for measurements. Point modifiers
|
||||
/// that combine or constrain coordinates produce ordinary drawing-space points.
|
||||
pub fn input_snapped(&mut self, hit: snap::SnapHit) -> Result<()> {
|
||||
let source = hit.viewport.filter(|_| self.point_mods.iter().all(|f| matches!(f, pointmod::Frame::Snap { mode } if *mode != 0)));
|
||||
let previous = std::mem::replace(&mut self.picked_viewport, source);
|
||||
let result = self.input(hit.input());
|
||||
self.picked_viewport = previous;
|
||||
result
|
||||
}
|
||||
|
||||
/// Give an input to the running command. With no command running, `Enter` repeats the last
|
||||
/// command (as in AutoCAD) and points do nothing.
|
||||
pub fn input(&mut self, input: Input) -> Result<()> {
|
||||
|
||||
@@ -349,7 +349,7 @@ impl Session {
|
||||
let deferred = prompt.as_ref().is_some_and(|q| q.deferred);
|
||||
let hit = crate::snap::osnap(self.doc()?, &space, p, aperture, mode | hatch, base, deferred);
|
||||
match hit {
|
||||
Some(SnapHit { deferred: Some(d), .. }) if self.point_mods.is_empty() => return self.input(Input::Deferred(d)),
|
||||
Some(h) if self.point_mods.is_empty() => return self.input_snapped(h),
|
||||
Some(h) => p = h.point,
|
||||
None => {
|
||||
self.echo(format!("No {} found for specified point.", snap_name(mode)));
|
||||
|
||||
+97
-21
@@ -3,6 +3,9 @@
|
||||
mod curves;
|
||||
pub mod iso;
|
||||
pub mod tracking;
|
||||
mod viewport;
|
||||
|
||||
pub use viewport::ViewportPoint;
|
||||
|
||||
use cadcraft_doc::{Drawing, EntityKind, Insert, MAX_BLOCK_DEPTH, Prim, Space};
|
||||
use cadcraft_geom::{Bounds2, Circle, Ellipse, Mat3, PI, Segment, Vec2, intersect_ext};
|
||||
@@ -52,6 +55,9 @@ pub struct SnapHit {
|
||||
pub point: Vec2,
|
||||
pub mode: u32,
|
||||
pub name: &'static str,
|
||||
/// Model coordinates when this paper-space hit is inside a layout viewport.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub viewport: Option<ViewportPoint>,
|
||||
/// A deferred tangent/perpendicular: `point` is only provisional (the nearest point on the
|
||||
/// curve) until the other end of the line is known. See [`resolve_deferred`].
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -148,10 +154,59 @@ pub(crate) fn osnap_with(
|
||||
base: Option<Vec2>,
|
||||
deferred: bool,
|
||||
) -> Option<SnapHit> {
|
||||
if osmode == 0 || osmode & mode::OFF != 0 {
|
||||
if osmode == 0 || osmode & mode::OFF != 0 || !cursor.is_finite() || !(aperture.is_finite() && aperture > 0.0) {
|
||||
return None;
|
||||
}
|
||||
let (mut cands, near_prims) = gather(d, space, ix, cursor, aperture, osmode)?;
|
||||
let mut best = osnap_in(d, space, ix, cursor, aperture, osmode, base, deferred, &[], None);
|
||||
let views = viewport::near(d, space, cursor, aperture);
|
||||
if views.is_empty() {
|
||||
return best;
|
||||
}
|
||||
let model = Space::Model;
|
||||
let model_ix = crate::spatial::index(d, &model);
|
||||
for view in views {
|
||||
let model_cursor = view.to_model(cursor);
|
||||
let model_aperture = aperture / view.scale;
|
||||
let model_base = base.map(|p| view.to_model(p));
|
||||
if !model_cursor.is_finite() || !(model_aperture.is_finite() && model_aperture > 0.0) || model_base.is_some_and(|p| !p.is_finite()) {
|
||||
continue;
|
||||
}
|
||||
let hit = osnap_in(
|
||||
d,
|
||||
&model,
|
||||
&model_ix,
|
||||
model_cursor,
|
||||
model_aperture,
|
||||
osmode,
|
||||
model_base,
|
||||
deferred,
|
||||
&view.viewport.frozen_layers,
|
||||
Some(view.model_clip),
|
||||
)
|
||||
.and_then(|h| view.hit(h));
|
||||
if let Some(hit) = hit
|
||||
&& best.as_ref().is_none_or(|b| snap_score(hit.mode, hit.point, cursor, aperture) < snap_score(b.mode, b.point, cursor, aperture))
|
||||
{
|
||||
best = Some(hit);
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn osnap_in(
|
||||
d: &Drawing,
|
||||
space: &Space,
|
||||
ix: &Option<std::sync::Arc<crate::spatial::SpatialIndex>>,
|
||||
cursor: Vec2,
|
||||
aperture: f64,
|
||||
osmode: u32,
|
||||
base: Option<Vec2>,
|
||||
deferred: bool,
|
||||
frozen: &[String],
|
||||
clip: Option<Bounds2>,
|
||||
) -> Option<SnapHit> {
|
||||
let (mut cands, near_prims) = gather_in(d, space, ix, cursor, aperture, osmode, frozen)?;
|
||||
// Intersections between nearby primitives; Apparent Intersection also where their extensions
|
||||
// meet (lines extended, arcs as full circles: in 2D, the "apparent" crossing of objects
|
||||
// that don't reach each other).
|
||||
@@ -181,7 +236,7 @@ pub(crate) fn osnap_with(
|
||||
}
|
||||
}
|
||||
}
|
||||
finish(cands, &near_prims, cursor, aperture, osmode, base, deferred)
|
||||
finish(cands, &near_prims, cursor, aperture, osmode, base, deferred, clip)
|
||||
}
|
||||
|
||||
/// Whether a primitive has a meaningful extension (a line, an arc, a circle, an xline or ray).
|
||||
@@ -198,6 +253,18 @@ pub(crate) fn gather(
|
||||
cursor: Vec2,
|
||||
aperture: f64,
|
||||
osmode: u32,
|
||||
) -> Option<(Vec<(u32, Vec2)>, Vec<Prim>)> {
|
||||
gather_in(d, space, ix, cursor, aperture, osmode, &[])
|
||||
}
|
||||
|
||||
fn gather_in(
|
||||
d: &Drawing,
|
||||
space: &Space,
|
||||
ix: &Option<std::sync::Arc<crate::spatial::SpatialIndex>>,
|
||||
cursor: Vec2,
|
||||
aperture: f64,
|
||||
osmode: u32,
|
||||
frozen: &[String],
|
||||
) -> Option<(Vec<(u32, Vec2)>, Vec<Prim>)> {
|
||||
let probe = Bounds2::new(cursor, cursor).expand(aperture);
|
||||
let mut cands: Vec<(u32, Vec2)> = Vec::new();
|
||||
@@ -205,7 +272,7 @@ pub(crate) fn gather(
|
||||
let mut count = 0usize;
|
||||
let mut budget = MAX_BLOCK_LEAVES;
|
||||
for (e, known) in crate::select::candidates(d, space, ix, &probe.expand(aperture), true, false)? {
|
||||
if !d.is_visible(e) {
|
||||
if !d.is_visible(e) || frozen.iter().any(|l| l.eq_ignore_ascii_case(&e.common.layer)) {
|
||||
continue;
|
||||
}
|
||||
let inf = crate::select::is_infinite(e);
|
||||
@@ -238,16 +305,17 @@ pub(crate) fn gather(
|
||||
sink.object(&e.kind, None);
|
||||
// A block reference snaps to the objects inside it, placed by its transform.
|
||||
if let EntityKind::Insert(ins) = &e.kind {
|
||||
block_leaves(d, ins, &Mat3::IDENTITY, 0, &mut budget, &mut |k, m| sink.object(k, Some(m)));
|
||||
block_leaves(d, ins, &Mat3::IDENTITY, 0, &mut budget, frozen, &mut |k, m| sink.object(k, Some(m)));
|
||||
}
|
||||
if let EntityKind::Dimension(dm) = &e.kind {
|
||||
sink.dimension(d, dm, &mut budget);
|
||||
sink.dimension(d, dm, &mut budget, frozen);
|
||||
}
|
||||
}
|
||||
Some((cands, near_prims))
|
||||
}
|
||||
|
||||
/// Picks the best of the gathered candidates, adding the base-dependent and deferred snaps.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn finish(
|
||||
mut cands: Vec<(u32, Vec2)>,
|
||||
near_prims: &[Prim],
|
||||
@@ -256,6 +324,7 @@ fn finish(
|
||||
osmode: u32,
|
||||
base: Option<Vec2>,
|
||||
deferred: bool,
|
||||
clip: Option<Bounds2>,
|
||||
) -> Option<SnapHit> {
|
||||
// Perpendicular / tangent from the base point.
|
||||
if let Some(bp) = base {
|
||||
@@ -283,18 +352,19 @@ fn finish(
|
||||
}
|
||||
}
|
||||
}
|
||||
let best = cands.iter().filter(|(_, p)| p.dist(cursor) <= aperture).min_by(|a, b| {
|
||||
let visible = |p: Vec2| p.is_finite() && clip.is_none_or(|c| c.contains(p));
|
||||
let best = cands.iter().filter(|(_, p)| visible(*p) && p.dist(cursor) <= aperture).min_by(|a, b| {
|
||||
// Prefer more specific modes when nearly equidistant.
|
||||
let da = a.1.dist(cursor) - if a.0 == mode::INT || a.0 == mode::END { aperture * 0.15 } else { 0.0 };
|
||||
let db = b.1.dist(cursor) - if b.0 == mode::INT || b.0 == mode::END { aperture * 0.15 } else { 0.0 };
|
||||
let da = snap_score(a.0, a.1, cursor, aperture);
|
||||
let db = snap_score(b.0, b.1, cursor, aperture);
|
||||
da.total_cmp(&db)
|
||||
});
|
||||
if let Some((m, p)) = best {
|
||||
return Some(SnapHit { point: *p, mode: *m, name: name_of(*m), deferred: None });
|
||||
return Some(SnapHit { point: *p, mode: *m, name: name_of(*m), viewport: None, deferred: None });
|
||||
}
|
||||
if base.is_none()
|
||||
&& deferred
|
||||
&& let Some(h) = deferred_hit(near_prims, cursor, aperture, osmode)
|
||||
&& let Some(h) = deferred_hit(near_prims, cursor, aperture, osmode, clip)
|
||||
{
|
||||
return Some(h);
|
||||
}
|
||||
@@ -303,7 +373,7 @@ fn finish(
|
||||
let mut nb: Option<(f64, Vec2)> = None;
|
||||
let mut consider = |c: Vec2| {
|
||||
let dd = c.dist(cursor);
|
||||
if dd <= aperture && nb.is_none_or(|(bd, _)| dd < bd) {
|
||||
if visible(c) && dd <= aperture && nb.is_none_or(|(bd, _)| dd < bd) {
|
||||
nb = Some((dd, c));
|
||||
}
|
||||
};
|
||||
@@ -316,12 +386,16 @@ fn finish(
|
||||
}
|
||||
}
|
||||
if let Some((_, p)) = nb {
|
||||
return Some(SnapHit { point: p, mode: mode::NEA, name: "Nearest", deferred: None });
|
||||
return Some(SnapHit { point: p, mode: mode::NEA, name: "Nearest", viewport: None, deferred: None });
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn snap_score(mode: u32, point: Vec2, cursor: Vec2, aperture: f64) -> f64 {
|
||||
point.dist(cursor) - if mode == mode::INT || mode == mode::END { aperture * 0.15 } else { 0.0 }
|
||||
}
|
||||
|
||||
/// Upper bound on the block-reference objects one snap query looks at (hostile files).
|
||||
const MAX_BLOCK_LEAVES: usize = 20_000;
|
||||
|
||||
@@ -463,7 +537,7 @@ impl Sink<'_> {
|
||||
/// A dimension snaps to what it draws, as in AutoCAD (into its block): the objects of its
|
||||
/// block when the drawing has one (read from a file), else the lines generated from it
|
||||
/// (Endpoint on their ends); Node on its definition points.
|
||||
fn dimension(&mut self, d: &Drawing, dm: &cadcraft_doc::Dimension, budget: &mut usize) {
|
||||
fn dimension(&mut self, d: &Drawing, dm: &cadcraft_doc::Dimension, budget: &mut usize, frozen: &[String]) {
|
||||
match dm.block.as_ref().and_then(|n| d.block(n)) {
|
||||
Some(blk) => {
|
||||
// The block holds world geometry: a reference at its own base point places it as is.
|
||||
@@ -478,7 +552,7 @@ impl Sink<'_> {
|
||||
col_spacing: 0.0,
|
||||
row_spacing: 0.0,
|
||||
};
|
||||
block_leaves(d, &ins, &Mat3::IDENTITY, 0, budget, &mut |k, m| self.object(k, Some(m)));
|
||||
block_leaves(d, &ins, &Mat3::IDENTITY, 0, budget, frozen, &mut |k, m| self.object(k, Some(m)));
|
||||
}
|
||||
None => {
|
||||
let g = cadcraft_render::dimension_in(d, dm);
|
||||
@@ -523,7 +597,7 @@ fn dim_def_points(dm: &cadcraft_doc::Dimension) -> Vec<Vec2> {
|
||||
/// Calls `f` with each object drawn by a block reference and the transform placing it, nested
|
||||
/// references expanded (within [`MAX_BLOCK_DEPTH`] levels and `budget` objects); objects that
|
||||
/// aren't drawn (hidden, on a layer that is off or frozen, attribute definitions) are left out.
|
||||
fn block_leaves(d: &Drawing, ins: &Insert, m: &Mat3, depth: usize, budget: &mut usize, f: &mut dyn FnMut(&EntityKind, &Mat3)) {
|
||||
fn block_leaves(d: &Drawing, ins: &Insert, m: &Mat3, depth: usize, budget: &mut usize, frozen: &[String], f: &mut dyn FnMut(&EntityKind, &Mat3)) {
|
||||
if depth >= MAX_BLOCK_DEPTH {
|
||||
return;
|
||||
}
|
||||
@@ -547,12 +621,14 @@ fn block_leaves(d: &Drawing, ins: &Insert, m: &Mat3, depth: usize, budget: &mut
|
||||
}
|
||||
*budget -= 1;
|
||||
// Objects on layer 0 take the reference's layer, which is visible.
|
||||
if !be.common.visible || (be.common.layer != "0" && !d.is_visible(be)) {
|
||||
if !be.common.visible
|
||||
|| (be.common.layer != "0" && (!d.is_visible(be) || frozen.iter().any(|l| l.eq_ignore_ascii_case(&be.common.layer))))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
match &be.kind {
|
||||
EntityKind::AttDef(_) => {}
|
||||
EntityKind::Insert(inner) => block_leaves(d, inner, &mm, depth + 1, budget, f),
|
||||
EntityKind::Insert(inner) => block_leaves(d, inner, &mm, depth + 1, budget, frozen, f),
|
||||
k => f(k, &mm),
|
||||
}
|
||||
}
|
||||
@@ -636,7 +712,7 @@ fn xform_ellipse(e: &Ellipse, m: &Mat3) -> Ellipse {
|
||||
}
|
||||
|
||||
/// The nearest deferred tangent/perpendicular within `aperture` of `cursor`.
|
||||
fn deferred_hit(prims: &[Prim], cursor: Vec2, aperture: f64, osmode: u32) -> Option<SnapHit> {
|
||||
fn deferred_hit(prims: &[Prim], cursor: Vec2, aperture: f64, osmode: u32, clip: Option<Bounds2>) -> Option<SnapHit> {
|
||||
let mut best: Option<(f64, SnapHit)> = None;
|
||||
for p in prims {
|
||||
let curve = match p {
|
||||
@@ -661,8 +737,8 @@ fn deferred_hit(prims: &[Prim], cursor: Vec2, aperture: f64, osmode: u32) -> Opt
|
||||
_ => continue,
|
||||
};
|
||||
let dd = at.dist(cursor);
|
||||
if at.is_finite() && dd <= aperture && best.is_none_or(|(bd, _)| dd < bd) {
|
||||
best = Some((dd, SnapHit { point: at, mode: m, name, deferred: Some(Deferred { mode: m, curve, at }) }));
|
||||
if at.is_finite() && clip.is_none_or(|c| c.contains(at)) && dd <= aperture && best.is_none_or(|(bd, _)| dd < bd) {
|
||||
best = Some((dd, SnapHit { point: at, mode: m, name, viewport: None, deferred: Some(Deferred { mode: m, curve, at }) }));
|
||||
}
|
||||
}
|
||||
best.map(|(_, h)| h)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
//! Snap queries through the same rectangular layout viewports that the renderer displays.
|
||||
|
||||
use cadcraft_doc::{Drawing, EntityKind, Handle, Space, Viewport};
|
||||
use cadcraft_geom::{Bounds2, Vec2};
|
||||
use serde::Serialize;
|
||||
|
||||
use super::{Deferred, SnapCurve, SnapHit};
|
||||
|
||||
/// A model-space snap seen through a layout viewport. Commands still receive paper coordinates;
|
||||
/// DIST can use these model coordinates when both picks came through the same viewport.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
|
||||
pub struct ViewportPoint {
|
||||
pub viewport: Handle,
|
||||
pub point: Vec2,
|
||||
/// Paper units per model unit at the time of the pick.
|
||||
pub scale: f64,
|
||||
}
|
||||
|
||||
pub(super) struct Projection<'a> {
|
||||
pub handle: Handle,
|
||||
pub viewport: &'a Viewport,
|
||||
pub scale: f64,
|
||||
pub model_clip: Bounds2,
|
||||
paper_clip: Bounds2,
|
||||
}
|
||||
|
||||
impl<'a> Projection<'a> {
|
||||
fn new(handle: Handle, viewport: &'a Viewport) -> Option<Self> {
|
||||
let positive = |v: f64| v.is_finite() && v > 0.0;
|
||||
let center = viewport.center.xy();
|
||||
if viewport.id == 1
|
||||
|| !(center.is_finite() && viewport.view_center.is_finite())
|
||||
|| ![viewport.width, viewport.height, viewport.view_height].into_iter().all(positive)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let scale = viewport.height / viewport.view_height;
|
||||
if !positive(scale) || !positive(1.0 / scale) {
|
||||
return None;
|
||||
}
|
||||
let half = Vec2::new(viewport.width / 2.0, viewport.height / 2.0);
|
||||
let paper_clip = Bounds2::new(center - half, center + half);
|
||||
let model_clip = Bounds2::new(viewport.view_center - half / scale, viewport.view_center + half / scale);
|
||||
if ![paper_clip.min, paper_clip.max, model_clip.min, model_clip.max].into_iter().all(Vec2::is_finite) {
|
||||
return None;
|
||||
}
|
||||
Some(Self { handle, viewport, scale, model_clip, paper_clip })
|
||||
}
|
||||
|
||||
pub fn to_model(&self, point: Vec2) -> Vec2 {
|
||||
self.viewport.view_center + (point - self.viewport.center.xy()) / self.scale
|
||||
}
|
||||
|
||||
fn to_paper(&self, point: Vec2) -> Vec2 {
|
||||
self.viewport.center.xy() + (point - self.viewport.view_center) * self.scale
|
||||
}
|
||||
|
||||
pub fn hit(&self, mut hit: SnapHit) -> Option<SnapHit> {
|
||||
let model_point = hit.point;
|
||||
hit.point = self.to_paper(model_point);
|
||||
if !hit.point.is_finite() || !self.paper_clip.contains(hit.point) {
|
||||
return None;
|
||||
}
|
||||
hit.viewport = Some(ViewportPoint { viewport: self.handle, point: model_point, scale: self.scale });
|
||||
if let Some(d) = hit.deferred {
|
||||
let curve = match d.curve {
|
||||
SnapCurve::Circle { center, radius } => {
|
||||
let (center, radius) = (self.to_paper(center), radius * self.scale);
|
||||
if !(center.is_finite() && radius.is_finite() && radius > 0.0) {
|
||||
return None;
|
||||
}
|
||||
SnapCurve::Circle { center, radius }
|
||||
}
|
||||
SnapCurve::Line { a, b } => {
|
||||
let (a, b) = (self.to_paper(a), self.to_paper(b));
|
||||
if !(a.is_finite() && b.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
SnapCurve::Line { a, b }
|
||||
}
|
||||
};
|
||||
hit.deferred = Some(Deferred { mode: d.mode, curve, at: hit.point });
|
||||
}
|
||||
Some(hit)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bound layout work like the renderer. An off viewport layer hides its border, while a frozen
|
||||
/// viewport layer (or an invisible viewport entity) also hides its model contents.
|
||||
pub(super) fn near<'a>(d: &'a Drawing, space: &Space, cursor: Vec2, aperture: f64) -> Vec<Projection<'a>> {
|
||||
if !matches!(space, Space::Paper(_)) {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(store) = d.space(space) else { return Vec::new() };
|
||||
let probe = Bounds2::new(cursor, cursor).expand(aperture);
|
||||
store
|
||||
.iter()
|
||||
.filter_map(|e| match &e.kind {
|
||||
EntityKind::Viewport(vp) => Some((e, vp)),
|
||||
_ => None,
|
||||
})
|
||||
.take(256)
|
||||
.filter(|(e, _)| e.common.visible && !d.layer(&e.common.layer).is_some_and(|l| l.frozen))
|
||||
.filter_map(|(e, vp)| Projection::new(e.handle, vp))
|
||||
.filter(|p| p.paper_clip.intersects(&probe))
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
//! DIST snaps to the model geometry displayed in paper-space viewports (#516).
|
||||
|
||||
use cadcraft_engine::doc::{Block, Circle, Common, Drawing, Entity, EntityKind, Handle, Insert, Line, Space, Viewport};
|
||||
use cadcraft_engine::geom::{Vec2, Vec3};
|
||||
use cadcraft_engine::snap::{SnapCurve, SnapHit, mode, osnap};
|
||||
use cadcraft_engine::{Input, Session};
|
||||
use serde_json::json;
|
||||
|
||||
fn drawing() -> (Drawing, Space) {
|
||||
let mut d = Drawing::new_imperial();
|
||||
let paper = Space::Paper("Layout1".into());
|
||||
for (a, b) in [(Vec2::new(0.0, 0.0), Vec2::new(20.0, 0.0)), (Vec2::new(10.0, -10.0), Vec2::new(10.0, 10.0))] {
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Line(Line { a: a.to3(0.0), b: b.to3(0.0) })).unwrap();
|
||||
}
|
||||
d.add(
|
||||
&paper,
|
||||
Common::default(),
|
||||
EntityKind::Viewport(Viewport {
|
||||
center: Vec3::new(100.0, 50.0, 0.0),
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
view_center: Vec2::new(10.0, 0.0),
|
||||
view_height: 40.0,
|
||||
id: 2,
|
||||
locked: false,
|
||||
frozen_layers: Vec::new(),
|
||||
layer_colors: Vec::new(),
|
||||
clip: None,
|
||||
on: true,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
(d, paper)
|
||||
}
|
||||
|
||||
fn viewport(d: &Drawing, paper: &Space) -> Handle {
|
||||
d.space(paper).unwrap().iter().find(|e| matches!(e.kind, EntityKind::Viewport(_))).unwrap().handle
|
||||
}
|
||||
|
||||
fn edit_viewport(d: &mut Drawing, paper: &Space, edit: impl FnOnce(&mut Viewport)) {
|
||||
d.modify_entity(viewport(d, paper), |e| match &mut e.kind {
|
||||
EntityKind::Viewport(vp) => edit(vp),
|
||||
_ => panic!("viewport fixture"),
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn session(d: Drawing) -> Session {
|
||||
let mut s = Session::empty();
|
||||
s.open_drawing(d, "Drawing", None);
|
||||
s.execute("layout.set", &json!({"name":"Layout1"})).unwrap();
|
||||
s.execute("view.set", &json!({"center":[100,50],"height":30})).unwrap();
|
||||
s.settings.osmode = mode::END | mode::INT;
|
||||
s
|
||||
}
|
||||
|
||||
fn pick(s: &Session, at: Vec2, modes: u32) -> SnapHit {
|
||||
osnap(s.doc().unwrap(), &s.space(), at + Vec2::new(0.05, 0.04), 0.5, modes, None, false).expect("model snap through viewport")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paper_space_snaps_to_visible_model_endpoints_and_intersections() {
|
||||
let (d, paper) = drawing();
|
||||
for (cursor, at, modes, expected) in [
|
||||
(Vec2::new(95.1, 50.1), Vec2::new(95.0, 50.0), mode::END | mode::INT, mode::END),
|
||||
(Vec2::new(100.1, 50.1), Vec2::new(100.0, 50.0), mode::INT, mode::INT),
|
||||
] {
|
||||
let hit = osnap(&d, &paper, cursor, 0.5, modes, None, false).expect("visible model geometry must snap from PAPER");
|
||||
assert_eq!(hit.mode, expected);
|
||||
assert!(hit.point.near(at, 1e-9));
|
||||
let source = hit.viewport.unwrap();
|
||||
assert_eq!(source.scale, 0.5);
|
||||
assert!(source.point.near(Vec2::new((at.x - 100.0) * 2.0 + 10.0, (at.y - 50.0) * 2.0), 1e-9));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_space_running_snaps_and_disabled_snaps_keep_their_behavior() {
|
||||
let (d, paper) = drawing();
|
||||
let hit = osnap(&d, &Space::Model, Vec2::new(0.1, 0.1), 0.5, mode::END, None, false).unwrap();
|
||||
assert_eq!(hit.point, Vec2::ZERO);
|
||||
assert!(hit.viewport.is_none());
|
||||
for modes in [0, mode::END | mode::INT | mode::OFF] {
|
||||
assert!(osnap(&d, &paper, Vec2::new(95.1, 50.1), 0.5, modes, None, false).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distance_uses_model_units_for_two_snaps_in_the_same_viewport() {
|
||||
let (d, _) = drawing();
|
||||
let mut s = session(d);
|
||||
let before = s.state().unwrap().is_dirty();
|
||||
s.start("dist").unwrap();
|
||||
s.input_snapped(pick(&s, Vec2::new(95.0, 50.0), mode::END)).unwrap();
|
||||
assert_eq!(s.current_prompt().unwrap().base, Some(Vec2::new(95.0, 50.0)), "preview remains in paper coordinates");
|
||||
s.input_snapped(pick(&s, Vec2::new(100.0, 50.0), mode::INT)).unwrap();
|
||||
assert!(s.running.is_none());
|
||||
assert!(s.log.iter().any(|l| l.starts_with("Distance = 10.0000,")), "{:?}", s.log);
|
||||
assert!(s.log.iter().any(|l| l.starts_with("Delta X = 10.0000, Delta Y = 0.0000")));
|
||||
assert_eq!(s.state().unwrap().is_dirty(), before, "inquiry must not edit the drawing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_and_programmatic_points_are_measured_in_paper_units() {
|
||||
let (d, _) = drawing();
|
||||
let mut s = session(d);
|
||||
for snapped_first in [true, false] {
|
||||
s.start("dist").unwrap();
|
||||
if snapped_first {
|
||||
s.input_snapped(pick(&s, Vec2::new(95.0, 50.0), mode::END)).unwrap();
|
||||
s.input(Input::Point(Vec2::new(100.0, 50.0))).unwrap();
|
||||
} else {
|
||||
s.input(Input::Point(Vec2::new(95.0, 50.0))).unwrap();
|
||||
s.input_snapped(pick(&s, Vec2::new(100.0, 50.0), mode::INT)).unwrap();
|
||||
}
|
||||
assert!(s.log.iter().any(|l| l.starts_with("Distance = 5.0000,")), "{:?}", s.log);
|
||||
s.log.clear();
|
||||
}
|
||||
assert_eq!(s.execute("dist", &json!({"p1":[95,50],"p2":[100,50]})).unwrap()["distance"], json!(5.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn points_from_different_viewports_use_paper_units() {
|
||||
let (mut d, paper) = drawing();
|
||||
let mut vp = match &d.entity(viewport(&d, &paper)).unwrap().kind {
|
||||
EntityKind::Viewport(vp) => vp.clone(),
|
||||
_ => panic!("viewport fixture"),
|
||||
};
|
||||
vp.center.x = 120.0;
|
||||
vp.view_height = 20.0;
|
||||
vp.id = 3;
|
||||
d.add(&paper, Common::default(), EntityKind::Viewport(vp)).unwrap();
|
||||
let mut s = session(d);
|
||||
s.start("dist").unwrap();
|
||||
let first = pick(&s, Vec2::new(95.0, 50.0), mode::END);
|
||||
let second = pick(&s, Vec2::new(110.0, 50.0), mode::END);
|
||||
assert_ne!(first.viewport.unwrap().viewport, second.viewport.unwrap().viewport);
|
||||
assert_eq!(first.viewport.unwrap().point, second.viewport.unwrap().point);
|
||||
s.input_snapped(first).unwrap();
|
||||
s.input_snapped(second).unwrap();
|
||||
assert!(s.log.iter().any(|l| l.starts_with("Distance = 15.0000,")), "{:?}", s.log);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_points_length_and_undo_keep_the_measurement_units() {
|
||||
let (d, _) = drawing();
|
||||
let mut s = session(d);
|
||||
s.start("dist").unwrap();
|
||||
s.input_snapped(pick(&s, Vec2::new(95.0, 50.0), mode::END)).unwrap();
|
||||
s.input(Input::Keyword("Multiple points".into())).unwrap();
|
||||
s.input_snapped(pick(&s, Vec2::new(100.0, 50.0), mode::INT)).unwrap();
|
||||
s.input_snapped(pick(&s, Vec2::new(105.0, 50.0), mode::END)).unwrap();
|
||||
s.input(Input::Keyword("Length".into())).unwrap();
|
||||
s.input(Input::Text("5".into())).unwrap();
|
||||
assert_eq!(s.log.last().unwrap(), "Distance = 25.0000");
|
||||
assert_eq!(s.current_prompt().unwrap().base, Some(Vec2::new(107.5, 50.0)));
|
||||
s.input(Input::Keyword("Undo".into())).unwrap();
|
||||
s.input(Input::Point(Vec2::new(102.0, 52.0))).unwrap();
|
||||
s.input(Input::Keyword("Undo".into())).unwrap();
|
||||
s.input(Input::Enter).unwrap();
|
||||
assert_eq!(s.log.last().unwrap(), "Distance = 20.0000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_endpoint_and_intersection_overrides_work_from_the_command_line() {
|
||||
let (d, _) = drawing();
|
||||
let mut s = session(d);
|
||||
s.settings.osmode = mode::OFF;
|
||||
s.cmdline("dist").unwrap();
|
||||
s.cmdline("end").unwrap();
|
||||
s.cmdline("95.05,50.04").unwrap();
|
||||
s.cmdline("int").unwrap();
|
||||
s.cmdline("100.05,50.04").unwrap();
|
||||
assert!(s.running.is_none());
|
||||
assert!(s.log.iter().any(|l| l.starts_with("Distance = 10.0000,")), "{:?}", s.log);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipped_endpoints_do_not_hide_a_visible_candidate() {
|
||||
let (mut d, paper) = drawing();
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Line(Line { a: Vec3::new(30.2, 2.0, 0.0), b: Vec3::new(29.0, 2.0, 0.0) })).unwrap();
|
||||
let hit = osnap(&d, &paper, Vec2::new(110.11, 51.0), 0.8, mode::END, None, false).unwrap();
|
||||
assert_eq!(hit.point, Vec2::new(109.5, 51.0), "only the endpoint inside the viewport is visible");
|
||||
assert!(osnap(&d, &paper, Vec2::new(110.11, 51.0), 0.2, mode::END, None, false).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_frozen_layers_do_not_hide_paper_space_geometry() {
|
||||
let (mut d, paper) = drawing();
|
||||
edit_viewport(&mut d, &paper, |vp| vp.frozen_layers.push("0".into()));
|
||||
assert!(osnap(&d, &paper, Vec2::new(95.1, 50.1), 0.5, mode::END, None, false).is_none());
|
||||
d.add(&paper, Common::default(), EntityKind::Line(Line { a: Vec3::new(99.0, 51.0, 0.0), b: Vec3::new(101.0, 51.0, 0.0) })).unwrap();
|
||||
let hit = osnap(&d, &paper, Vec2::new(99.1, 51.1), 0.5, mode::END, None, false).unwrap();
|
||||
assert_eq!(hit.point, Vec2::new(99.0, 51.0));
|
||||
assert!(hit.viewport.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_frozen_layers_apply_inside_nested_block_references() {
|
||||
let (mut d, paper) = drawing();
|
||||
d.layers.push(cadcraft_engine::doc::Layer { name: "Detail".into(), ..Default::default() });
|
||||
let mut inner = Block::new("Inner");
|
||||
inner.entities.push(Entity {
|
||||
handle: d.new_handle(),
|
||||
common: Common::default(),
|
||||
kind: EntityKind::Line(Line { a: Vec3::new(0.0, 4.0, 0.0), b: Vec3::new(2.0, 4.0, 0.0) }),
|
||||
});
|
||||
d.blocks.insert(inner.name.clone(), std::sync::Arc::new(inner));
|
||||
let insert = |block: &str| {
|
||||
EntityKind::Insert(Insert {
|
||||
block: block.into(),
|
||||
insert: Vec3::ZERO,
|
||||
scale: Vec3::new(1.0, 1.0, 1.0),
|
||||
rotation: 0.0,
|
||||
attribs: Vec::new(),
|
||||
cols: 1,
|
||||
rows: 1,
|
||||
col_spacing: 0.0,
|
||||
row_spacing: 0.0,
|
||||
})
|
||||
};
|
||||
let mut outer = Block::new("Outer");
|
||||
outer.entities.push(Entity { handle: d.new_handle(), common: Common { layer: "Detail".into(), ..Default::default() }, kind: insert("Inner") });
|
||||
d.blocks.insert(outer.name.clone(), std::sync::Arc::new(outer));
|
||||
d.add(&Space::Model, Common::default(), insert("Outer")).unwrap();
|
||||
let cursor = Vec2::new(95.05, 52.04);
|
||||
assert_eq!(osnap(&d, &paper, cursor, 0.5, mode::END, None, false).unwrap().point, Vec2::new(95.0, 52.0));
|
||||
edit_viewport(&mut d, &paper, |vp| vp.frozen_layers.push("detail".into()));
|
||||
assert!(osnap(&d, &paper, cursor, 0.5, mode::END, None, false).is_none());
|
||||
assert_eq!(osnap(&d, &Space::Model, Vec2::new(0.1, 4.1), 0.5, mode::END, None, false).unwrap().point, Vec2::new(0.0, 4.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipped_deferred_curves_do_not_hide_a_visible_candidate() {
|
||||
let (mut d, paper) = drawing();
|
||||
// Hide the viewport border so its perpendicular snap does not win this query.
|
||||
d.layers.push(cadcraft_engine::doc::Layer { name: "Viewports".into(), on: false, ..Default::default() });
|
||||
d.modify_entity(viewport(&d, &paper), |e| e.common.layer = "Viewports".into()).unwrap();
|
||||
for x in [30.2, 29.0] {
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Line(Line { a: Vec3::new(x, 1.0, 0.0), b: Vec3::new(x, 3.0, 0.0) })).unwrap();
|
||||
}
|
||||
let hit = osnap(&d, &paper, Vec2::new(110.11, 51.0), 0.8, mode::PER, None, true).unwrap();
|
||||
assert_eq!(hit.point, Vec2::new(109.5, 51.0));
|
||||
assert!(hit.deferred.is_some());
|
||||
assert!(hit.viewport.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn off_viewport_borders_keep_contents_but_frozen_or_hidden_viewports_do_not() {
|
||||
let (mut d, paper) = drawing();
|
||||
d.layer_mut("0").unwrap().on = false;
|
||||
// Put model geometry on a different, visible layer.
|
||||
d.layers.push(cadcraft_engine::doc::Layer { name: "Geometry".into(), ..Default::default() });
|
||||
let handles: Vec<Handle> = d.model.iter().map(|e| e.handle).collect();
|
||||
for h in handles {
|
||||
d.modify_entity(h, |e| e.common.layer = "Geometry".into()).unwrap();
|
||||
}
|
||||
assert!(osnap(&d, &paper, Vec2::new(95.1, 50.1), 0.5, mode::END, None, false).is_some());
|
||||
d.layer_mut("0").unwrap().frozen = true;
|
||||
assert!(osnap(&d, &paper, Vec2::new(95.1, 50.1), 0.5, mode::END, None, false).is_none());
|
||||
d.layer_mut("0").unwrap().frozen = false;
|
||||
let h = d.space(&paper).unwrap().iter().find(|e| matches!(e.kind, EntityKind::Viewport(_))).unwrap().handle;
|
||||
d.modify_entity(h, |e| e.common.visible = false).unwrap();
|
||||
assert!(osnap(&d, &paper, Vec2::new(95.1, 50.1), 0.5, mode::END, None, false).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferred_curves_are_projected_into_paper_coordinates() {
|
||||
let (mut d, paper) = drawing();
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Circle(Circle { center: Vec3::new(10.0, 4.0, 0.0), radius: 2.0 })).unwrap();
|
||||
let hit = osnap(&d, &paper, Vec2::new(101.1, 52.0), 0.5, mode::TAN, None, true).unwrap();
|
||||
assert!(hit.point.near(Vec2::new(101.0, 52.0), 1e-9));
|
||||
match hit.deferred.unwrap().curve {
|
||||
SnapCurve::Circle { center, radius } => {
|
||||
assert_eq!(center, Vec2::new(100.0, 52.0));
|
||||
assert_eq!(radius, 1.0);
|
||||
}
|
||||
_ => panic!("circle tangent"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_and_paper_space_viewports_are_not_snap_sources() {
|
||||
for (width, height, view_height, id) in [
|
||||
(20.0, 20.0, 40.0, 1),
|
||||
(-20.0, 20.0, 40.0, 2),
|
||||
(20.0, 20.0, 0.0, 2),
|
||||
(f64::NAN, 20.0, 40.0, 2),
|
||||
(20.0, f64::INFINITY, 40.0, 2),
|
||||
(20.0, 1e308, 1e-308, 2),
|
||||
] {
|
||||
let (mut d, paper) = drawing();
|
||||
edit_viewport(&mut d, &paper, |vp| (vp.width, vp.height, vp.view_height, vp.id) = (width, height, view_height, id));
|
||||
assert!(osnap(&d, &paper, Vec2::new(95.1, 50.1), 0.5, mode::END, None, false).is_none());
|
||||
}
|
||||
}
|
||||
@@ -774,9 +774,12 @@ pub fn show(app: &mut CadApp, ui: &mut egui::Ui) {
|
||||
{
|
||||
app.cmd.buffer.clear();
|
||||
}
|
||||
// A deferred tangent/perpendicular goes to the command as such.
|
||||
let input = app.canvas.snap.filter(|h| h.deferred.is_some()).map_or(Input::Point(p), |h| h.input());
|
||||
if let Err(e) = app.session.input(input) {
|
||||
// Preserve a snap's viewport origin for DIST, and its deferred curve where needed.
|
||||
let result = match app.canvas.snap {
|
||||
Some(hit) => app.session.input_snapped(hit),
|
||||
None => app.session.input(Input::Point(p)),
|
||||
};
|
||||
if let Err(e) = result {
|
||||
app.session.echo(e.to_string());
|
||||
}
|
||||
} else if let Some(g) = grip_at(app, &xf, hover_pos) {
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
//! Measure Distance uses real pointer snaps in MODEL and PAPER, including viewport scale (#516).
|
||||
|
||||
use cadcraft_engine::Session;
|
||||
use cadcraft_engine::doc::{Common, Drawing, EntityKind, Line, Space, Viewport};
|
||||
use cadcraft_engine::geom::{Vec2, Vec3};
|
||||
use cadcraft_engine::snap::mode;
|
||||
use cadcraft_ui_egui::{CadApp, Services};
|
||||
use egui::{Event, Modifiers, PointerButton, Pos2, vec2};
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use serde_json::json;
|
||||
|
||||
fn harness(paper: bool, render: bool) -> Harness<'static, CadApp> {
|
||||
let mut d = Drawing::new_imperial();
|
||||
for (a, b) in [(Vec2::new(0.0, 0.0), Vec2::new(20.0, 0.0)), (Vec2::new(10.0, -10.0), Vec2::new(10.0, 10.0))] {
|
||||
d.add(&Space::Model, Common::default(), EntityKind::Line(Line { a: a.to3(0.0), b: b.to3(0.0) })).unwrap();
|
||||
}
|
||||
d.add(
|
||||
&Space::Paper("Layout1".into()),
|
||||
Common::default(),
|
||||
EntityKind::Viewport(Viewport {
|
||||
center: Vec3::new(5.0, 4.0, 0.0),
|
||||
width: 8.0,
|
||||
height: 6.0,
|
||||
view_center: Vec2::new(10.0, 0.0),
|
||||
view_height: 40.0,
|
||||
id: 2,
|
||||
locked: false,
|
||||
frozen_layers: Vec::new(),
|
||||
layer_colors: Vec::new(),
|
||||
clip: None,
|
||||
on: true,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
let mut session = Session::empty();
|
||||
session.open_drawing(d, "Measure snap regression", None);
|
||||
if paper {
|
||||
session.execute("layout.set", &json!({"name":"Layout1"})).unwrap();
|
||||
}
|
||||
session.execute("view.set", &if paper { json!({"center":[5,4],"height":10}) } else { json!({"center":[10,0],"height":30}) }).unwrap();
|
||||
session.settings.osmode = mode::END | mode::INT;
|
||||
session.settings.snapmode = true;
|
||||
session.settings.orthomode = true;
|
||||
let mut app = CadApp::new(session, Services::default());
|
||||
app.canvas.zoom_pending = false;
|
||||
let builder = Harness::builder().with_size(vec2(1600.0, 1000.0));
|
||||
let builder = if render { builder.wgpu() } else { builder };
|
||||
let mut h = builder.build_ui_state(
|
||||
|ui, app: &mut CadApp| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(5);
|
||||
h
|
||||
}
|
||||
|
||||
fn hover(h: &mut Harness<'_, CadApp>, point: Vec2) -> Pos2 {
|
||||
let pos = h.state().canvas.xf.unwrap().to_screen(point) + vec2(3.0, 2.0);
|
||||
h.event(Event::PointerMoved(pos));
|
||||
h.run_steps(3);
|
||||
pos
|
||||
}
|
||||
|
||||
fn click(h: &mut Harness<'_, CadApp>, pos: Pos2) {
|
||||
for pressed in [true, false] {
|
||||
h.event(Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
|
||||
h.run_steps(2);
|
||||
}
|
||||
}
|
||||
|
||||
fn measure(paper: bool) {
|
||||
let shots = std::env::var_os("CADCRAFT_MEASURE_SHOTS").map(std::path::PathBuf::from);
|
||||
let mut h = harness(paper, shots.is_some());
|
||||
let dirty_before = h.state().session.state().unwrap().is_dirty();
|
||||
h.get_by_role_and_label(egui::accesskit::Role::Button, "Measure Distance").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().session.running.as_ref().unwrap().id, "dist");
|
||||
let (endpoint, intersection) = if paper { (Vec2::new(3.5, 4.0), Vec2::new(5.0, 4.0)) } else { (Vec2::ZERO, Vec2::new(10.0, 0.0)) };
|
||||
let pos = hover(&mut h, endpoint);
|
||||
let hit = h.state().canvas.snap.expect("endpoint marker");
|
||||
assert_eq!(hit.mode, mode::END);
|
||||
assert!(hit.point.near(endpoint, 1e-6));
|
||||
assert_eq!(hit.viewport.is_some(), paper);
|
||||
if let Some(path) = &shots {
|
||||
std::fs::create_dir_all(path).unwrap();
|
||||
h.render().unwrap().save(path.join(if paper { "paper-endpoint.png" } else { "model-endpoint.png" })).unwrap();
|
||||
}
|
||||
click(&mut h, pos);
|
||||
assert!(h.state().session.current_prompt().unwrap().base.unwrap().near(endpoint, 1e-6));
|
||||
let pos = hover(&mut h, intersection);
|
||||
let hit = h.state().canvas.snap.expect("intersection marker");
|
||||
assert_eq!(hit.mode, mode::INT);
|
||||
assert!(hit.point.near(intersection, 1e-6));
|
||||
if let Some(path) = &shots {
|
||||
h.render().unwrap().save(path.join(if paper { "paper-intersection.png" } else { "model-intersection.png" })).unwrap();
|
||||
}
|
||||
click(&mut h, pos);
|
||||
let app = h.state();
|
||||
assert!(app.session.running.is_none());
|
||||
assert!(app.session.log.iter().any(|l| l.starts_with("Distance = 10.0000,")), "{:?}", app.session.log);
|
||||
assert_eq!(app.session.state().unwrap().is_dirty(), dirty_before);
|
||||
if let Some(path) = &shots {
|
||||
h.render().unwrap().save(path.join(if paper { "paper-distance.png" } else { "model-distance.png" })).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measure_distance_toolbar_snaps_to_model_geometry_through_paper_viewports() {
|
||||
measure(true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measure_distance_toolbar_keeps_model_space_snaps_working() {
|
||||
measure(false);
|
||||
}
|
||||
Reference in new issue
Block a user