mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-03 08:52:05 -06:00
Fix Measure Distance snapping through layout viewports (#616)
Squash of PR #616 by @PedroGuille. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ce1cc631d5
commit
a181bbd609
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user