mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-25 11:26:46 -06:00
Plain TEXT and MTEXT are picked anywhere inside their box, but a dimension or multileader was only found near its lines: its hit geometry is built without text. Hovering or clicking the words of a dimension found nothing (on a survey plan, 2 of 34 letters of a dimension's text). A point inside the box around the words, turned with the text, now picks the object. A dimension with a block, as AutoCAD saves them, is drawn from that block, so its text box comes from the TEXT and MTEXT in the block, not from the text the dimension's own geometry would place (on the same plan that was a different place, eight times smaller). A multileader's text box comes from its MTEXT layout. Co-authored-by: Cody Vogt <18035288+codyvogt@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
126 lines
5.7 KiB
Rust
126 lines
5.7 KiB
Rust
//! Picking (and the hover highlight, which picks the same way) finds dimensions and multileaders
|
|
//! anywhere on their words, as plain text is found anywhere inside its box.
|
|
|
|
use cadcraft_engine::Session;
|
|
use cadcraft_engine::doc::{EntityKind, Handle, Space};
|
|
use cadcraft_engine::geom::{Bounds2, Vec2};
|
|
use cadcraft_engine::render;
|
|
use cadcraft_engine::select::pick;
|
|
use serde_json::json;
|
|
|
|
/// The last entity of `kind` and the box (centre, half size along the text, angle) of its words.
|
|
fn text_of(s: &Session, kind: &str) -> (Handle, Vec2, Vec2, f64) {
|
|
let d = s.doc().unwrap();
|
|
let e = d.model.iter().filter(|e| format!("{:?}", e.kind).starts_with(kind)).last().unwrap();
|
|
let (pts, angle): (Vec<Vec2>, f64) = match &e.kind {
|
|
EntityKind::Dimension(dm) => {
|
|
let g = render::dimension_in(d, dm);
|
|
(g.text.iter().flatten().chain(g.text_glyphs.iter().flatten().flatten()).copied().collect(), g.text_angle)
|
|
}
|
|
EntityKind::MLeader(m) => {
|
|
let t = m.text.as_ref().unwrap();
|
|
(render::layout_mtext_entity(d, t).strokes.into_iter().flatten().collect(), t.rotation)
|
|
}
|
|
_ => unreachable!(),
|
|
};
|
|
let b = Bounds2::from_points(pts.iter().map(|q| q.rotate(-angle)));
|
|
(e.handle, b.center().rotate(angle), Vec2::new(b.width() / 2.0, b.height() / 2.0), angle)
|
|
}
|
|
|
|
/// A point `(u, v)` half-sizes from the text box's centre, along and across the text.
|
|
fn at(c: Vec2, half: Vec2, angle: f64, u: f64, v: f64) -> Vec2 {
|
|
c + Vec2::new(half.x * u, half.y * v).rotate(angle)
|
|
}
|
|
|
|
#[test]
|
|
fn dimension_and_multileader_text_is_picked_anywhere_on_its_words() {
|
|
let mut s = Session::new();
|
|
s.execute("dimlinear", &json!({"p1": [0, 0], "p2": [100, 0], "at": [50, 30], "text": "TWO WORDS HERE"})).unwrap();
|
|
let (h, c, half, angle) = text_of(&s, "Dimension");
|
|
let tiny = half.y * 0.01;
|
|
let d = s.doc().unwrap();
|
|
// Between letters, at the ends and the corners of the words: all the dimension.
|
|
for (u, v) in [(0.0, 0.0), (0.33, 0.1), (-0.9, -0.8), (0.9, 0.8), (0.5, -0.5)] {
|
|
assert_eq!(pick(d, &Space::Model, at(c, half, angle, u, v), tiny), Some(h), "({u}, {v})");
|
|
}
|
|
assert_eq!(pick(d, &Space::Model, at(c, half, angle, 0.0, 6.0), tiny), None, "well above the words: nothing");
|
|
|
|
s.execute("mleader", &json!({"points": [[200, 0], [220, 20]], "text": "LEADER NOTE"})).unwrap();
|
|
let (h, c, half, angle) = text_of(&s, "MLeader");
|
|
let d = s.doc().unwrap();
|
|
for (u, v) in [(0.0, 0.0), (0.4, 0.2), (-0.8, -0.6)] {
|
|
assert_eq!(pick(d, &Space::Model, at(c, half, angle, u, v), half.y * 0.01), Some(h), "leader text ({u}, {v})");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn slanted_dimension_text_is_picked_inside_its_turned_box_only() {
|
|
let mut s = Session::new();
|
|
// Text along the dimension line (not kept horizontal), as in most surveying styles.
|
|
for name in ["DIMTIH", "DIMTOH"] {
|
|
s.execute("setvar", &json!({"name": name, "value": 0})).unwrap();
|
|
}
|
|
s.execute("dimaligned", &json!({"p1": [0, 0], "p2": [100, 100], "at": [40, 60], "text": "SLANTED LABEL"})).unwrap();
|
|
let (h, c, half, angle) = text_of(&s, "Dimension");
|
|
assert!((angle - std::f64::consts::FRAC_PI_4).abs() < 1e-6, "aligned with the 45° line: {angle}");
|
|
let d = s.doc().unwrap();
|
|
let tiny = half.y * 0.01;
|
|
assert_eq!(pick(d, &Space::Model, at(c, half, angle, 0.6, 0.0), tiny), Some(h));
|
|
// Inside the axis-aligned box around the turned words, but off them (and off the lines).
|
|
let off = at(c, half, angle, 0.3, 4.0);
|
|
let aabb = Bounds2::from_points([
|
|
at(c, half, angle, -1.0, -1.0),
|
|
at(c, half, angle, 1.0, -1.0),
|
|
at(c, half, angle, 1.0, 1.0),
|
|
at(c, half, angle, -1.0, 1.0),
|
|
]);
|
|
assert!(aabb.contains(off), "the probe sits in the loose box");
|
|
assert_ne!(pick(d, &Space::Model, off, tiny), Some(h), "but not on the words");
|
|
}
|
|
|
|
#[test]
|
|
fn a_dimension_drawn_from_its_block_is_picked_on_the_block_text() {
|
|
// As AutoCAD saves dimensions: the geometry in an anonymous block, here with the text moved
|
|
// well away from where the dimension's own geometry would set it.
|
|
let mut s = Session::new();
|
|
s.execute("dimlinear", &json!({"p1": [0, 0], "p2": [100, 0], "at": [50, 30]})).unwrap();
|
|
s.execute("mtext", &json!({"at": [400, 400], "text": "MOVED TEXT", "height": 5})).unwrap();
|
|
let d = s.doc_mut().unwrap();
|
|
let text = d.model.iter().find(|e| matches!(e.kind, EntityKind::MText(_))).unwrap().as_ref().clone();
|
|
let dim = d.model.iter().find(|e| matches!(e.kind, EntityKind::Dimension(_))).unwrap().handle;
|
|
d.model.remove(text.handle);
|
|
let mut entities = cadcraft_engine::doc::EntityStore::new();
|
|
entities.push(text);
|
|
let block = cadcraft_engine::doc::Block {
|
|
xref_overlay: false,
|
|
xref_status: Default::default(),
|
|
name: "*D1".into(),
|
|
annotative: false,
|
|
base: Default::default(),
|
|
entities,
|
|
description: String::new(),
|
|
anonymous: true,
|
|
xref_path: None,
|
|
explodable: true,
|
|
units: 0,
|
|
array: None,
|
|
};
|
|
d.blocks.insert("*D1".into(), std::sync::Arc::new(block));
|
|
d.model.modify(dim, |e| {
|
|
if let EntityKind::Dimension(dm) = &mut e.kind {
|
|
dm.block = Some("*D1".into());
|
|
}
|
|
});
|
|
let d = s.doc().unwrap();
|
|
let m = render::layout_mtext_entity(
|
|
d,
|
|
match &d.block("*D1").unwrap().entities.iter().next().unwrap().kind {
|
|
EntityKind::MText(t) => t,
|
|
_ => unreachable!(),
|
|
},
|
|
);
|
|
let mid = m.bounds.center();
|
|
assert_eq!(pick(d, &Space::Model, mid, 0.01), Some(dim), "on the block's text");
|
|
assert_eq!(pick(d, &Space::Model, Vec2::new(50.0, 33.0), 0.01), None, "not where the dimension would have set it");
|
|
}
|