mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-25 11:26:46 -06:00
- A dimension snaps to what it draws: the objects of its block (read from a file), expanded like block references, or else the lines generated from it (Endpoint on their ends, Midpoint, Intersection, Nearest); Node on its definition points. - TEXT justified other than left-baseline (as read from DXF, group 11) snaps (Insertion) and grips at its alignment point; attributes too. - The snap grid starts at SNAPBASE and turns by SNAPANG (drawing variables, defaults 0,0 and 0 degrees). SETVAR takes `x,y` for point variables. - Hatches are left out of object snaps unless OSNAPHATCH is 1 (new session variable, default 0 as in AutoCAD). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
101 lines
4.8 KiB
Rust
101 lines
4.8 KiB
Rust
//! Snaps on dimensions, justified text and hatches; the snap grid's base and angle (#334).
|
|
|
|
use cadcraft_doc::{Common, EntityKind, HAlign, Space, Text, VAlign};
|
|
use cadcraft_engine::Session;
|
|
use cadcraft_engine::snap::{self, mode};
|
|
use cadcraft_geom::{Vec2, Vec3};
|
|
use serde_json::json;
|
|
|
|
/// The snap of `modes` near `p` (a hair off it, inside a 0.1 aperture).
|
|
fn snap_at(s: &Session, p: Vec2, modes: u32) -> Option<(Vec2, u32)> {
|
|
snap::osnap(s.doc().unwrap(), &Space::Model, p + Vec2::new(0.02, -0.01), 0.1, modes, None, false).map(|h| (h.point, h.mode))
|
|
}
|
|
|
|
fn handle(s: &mut Session, cmd: &str, p: serde_json::Value) -> String {
|
|
let r = s.execute(cmd, &p).unwrap();
|
|
r.get("handle").unwrap_or(&r["handles"][0]).as_str().unwrap().to_string()
|
|
}
|
|
|
|
#[test]
|
|
fn dimensions_snap_to_their_lines_and_definition_points() {
|
|
let mut s = Session::new();
|
|
s.cmdline("dimlinear 0,0 10,0 5,3").unwrap();
|
|
assert!(s.running.is_none());
|
|
// Endpoint: the dimension line's end; Node: the extension line origins.
|
|
assert_eq!(snap_at(&s, Vec2::new(10.0, 3.0), mode::END), Some((Vec2::new(10.0, 3.0), mode::END)));
|
|
assert_eq!(snap_at(&s, Vec2::new(0.0, 0.0), mode::NOD), Some((Vec2::new(0.0, 0.0), mode::NOD)));
|
|
assert_eq!(snap_at(&s, Vec2::new(10.0, 0.0), mode::NOD), Some((Vec2::new(10.0, 0.0), mode::NOD)));
|
|
|
|
// A dimension with a block (as read from a file) snaps to the block's objects.
|
|
let l = handle(&mut s, "line", json!({ "points": [[0, 7], [10, 7]] }));
|
|
s.execute("block", &json!({ "name": "DIMGEOM", "base": [0, 0], "handles": [l], "keep": "delete" })).unwrap();
|
|
let d = s.doc_mut().unwrap();
|
|
let dim = d.model.iter().find(|e| matches!(e.kind, EntityKind::Dimension(_))).unwrap().handle;
|
|
d.model.modify(dim, |e| {
|
|
if let EntityKind::Dimension(dm) = &mut e.kind {
|
|
dm.block = Some("DIMGEOM".into());
|
|
}
|
|
});
|
|
assert_eq!(snap_at(&s, Vec2::new(10.0, 7.0), mode::END), Some((Vec2::new(10.0, 7.0), mode::END)));
|
|
}
|
|
|
|
#[test]
|
|
fn justified_text_snaps_and_grips_at_its_alignment_point() {
|
|
let mut s = Session::new();
|
|
// As read from DXF: a right-justified text keeps group 10 (start) and group 11 (alignment).
|
|
let t = Text {
|
|
insert: Vec3::new(0.0, 0.0, 0.0),
|
|
align_pt: Some(Vec3::new(10.0, 0.0, 0.0)),
|
|
height: 1.0,
|
|
value: "ABCDEFGHIJ".into(),
|
|
rotation: 0.0,
|
|
width_factor: 1.0,
|
|
oblique: 0.0,
|
|
style: "Standard".into(),
|
|
halign: HAlign::Right,
|
|
valign: VAlign::Baseline,
|
|
};
|
|
s.doc_mut().unwrap().add(&Space::Model, Common::default(), EntityKind::Text(t.clone())).unwrap();
|
|
assert_eq!(snap_at(&s, Vec2::new(10.0, 0.0), mode::INS), Some((Vec2::new(10.0, 0.0), mode::INS)));
|
|
assert_eq!(snap_at(&s, Vec2::new(0.0, 0.0), mode::INS), None);
|
|
assert_eq!(EntityKind::Text(t.clone()).grips(), vec![Vec2::new(10.0, 0.0)]);
|
|
// Left-baseline and fit text: the start point.
|
|
for (h, v) in [(HAlign::Left, VAlign::Baseline), (HAlign::Fit, VAlign::Baseline)] {
|
|
assert_eq!(Text { halign: h, valign: v, ..t.clone() }.justify_point(), Vec2::new(0.0, 0.0));
|
|
}
|
|
assert_eq!(Text { halign: HAlign::Left, valign: VAlign::Top, ..t.clone() }.justify_point(), Vec2::new(10.0, 0.0));
|
|
}
|
|
|
|
#[test]
|
|
fn hatches_snap_only_with_osnaphatch() {
|
|
let mut s = Session::new();
|
|
let r = handle(&mut s, "rectang", json!({ "p1": [0, 0], "p2": [10, 5] }));
|
|
s.execute("hatch", &json!({ "handles": [r] })).unwrap();
|
|
s.execute("erase", &json!({ "handles": [r] })).unwrap();
|
|
assert!(matches!(s.doc().unwrap().model.iter().last().map(|e| &e.kind), Some(EntityKind::Hatch(_))));
|
|
assert_eq!(snap_at(&s, Vec2::new(10.0, 5.0), mode::END), None);
|
|
assert_eq!(s.execute("getvar", &json!({ "name": "OSNAPHATCH" })).unwrap()["value"], json!(0));
|
|
s.cmdline("setvar osnaphatch 1").unwrap();
|
|
assert!(s.settings.osnaphatch);
|
|
let modes = mode::END | mode::HATCH;
|
|
assert_eq!(snap_at(&s, Vec2::new(10.0, 5.0), modes).map(|h| h.0), Some(Vec2::new(10.0, 5.0)));
|
|
}
|
|
|
|
#[test]
|
|
fn grid_snap_follows_snapbase_and_snapang() {
|
|
let mut s = Session::new();
|
|
assert_eq!(snap::grid_frame(s.doc().unwrap()), (Vec2::ZERO, 0.0));
|
|
s.cmdline("setvar snapbase 0.5,0.25").unwrap();
|
|
s.cmdline("setvar snapang 30").unwrap();
|
|
let (origin, angle) = snap::grid_frame(s.doc().unwrap());
|
|
assert_eq!(origin, Vec2::new(0.5, 0.25));
|
|
assert!((angle - 30f64.to_radians()).abs() < 1e-12);
|
|
// Two units along the turned x axis and one along its y axis, nudged off the grid.
|
|
let (u, v) = (Vec2::from_angle(angle), Vec2::from_angle(angle).perp());
|
|
let on = origin + u * 2.0 + v * 1.0;
|
|
let p = snap::grid_snap(on + Vec2::new(0.1, -0.15), Vec2::new(1.0, 1.0), origin, angle);
|
|
assert!(p.near(on, 1e-12), "{p:?} vs {on:?}");
|
|
// Unturned, from SNAPBASE.
|
|
assert_eq!(snap::grid_snap(Vec2::new(1.4, 1.1), Vec2::new(1.0, 1.0), origin, 0.0), Vec2::new(1.5, 1.25));
|
|
}
|