mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-24 02:46:51 -06:00
* Build: line tables only in dev, no debug info for dependencies * Tests: one integration-test binary per crate Each crate's tests/*.rs becomes a module of tests/it/main.rs. Files that install process-wide state (the file io hooks, user aliases, SAVETIME and SAVEFILEPATH, user linetype and pattern files, registered fonts) stay their own test binaries. * xtask ci: nextest, tests without examples, one wasm check call
297 lines
13 KiB
Rust
297 lines
13 KiB
Rust
//! 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());
|
|
}
|
|
}
|