mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-25 19:36:49 -06:00
Squash of PR #514 by @vangel76. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
283 lines
12 KiB
Rust
283 lines
12 KiB
Rust
//! Dimension style variables beyond the basics: fit (DIMATFIT, DIMTIX, DIMSOXD, DIMTOFL),
|
||
//! suppressed dimension line halves (DIMSD1/2), fixed-length extension lines (DIMFXL/DIMFXLON),
|
||
//! DIMTVP, DIMTMOVE, DIMTOLJ, DIMAZIN, DIMARCSYM, DIMLDRBLK and the dimension and extension
|
||
//! line linetypes and lineweights. They are SETVAR overrides, DIMOVERRIDE values and style fields.
|
||
|
||
use cadcraft_engine::Session;
|
||
use cadcraft_engine::doc::{DimStyle, Dimension, EntityKind, Linetype};
|
||
use cadcraft_engine::geom::Vec2;
|
||
use cadcraft_engine::render::{DimGeometry, LineRole};
|
||
use serde_json::{Value, json};
|
||
|
||
fn set(s: &mut Session, name: &str, value: Value) {
|
||
s.execute("setvar", &json!({ "name": name, "value": value })).unwrap();
|
||
}
|
||
|
||
fn last_dim(s: &Session) -> (Dimension, DimGeometry) {
|
||
let d = s.doc().unwrap();
|
||
let dm = d
|
||
.model
|
||
.iter()
|
||
.filter_map(|e| match &e.kind {
|
||
EntityKind::Dimension(dm) => Some(dm.clone()),
|
||
_ => None,
|
||
})
|
||
.last()
|
||
.unwrap();
|
||
let g = cadcraft_engine::render::dimension_in(d, &dm);
|
||
(dm, g)
|
||
}
|
||
|
||
fn dim_lines(g: &DimGeometry) -> Vec<Vec<Vec2>> {
|
||
g.lines_of(LineRole::Dim).cloned().collect()
|
||
}
|
||
|
||
/// Whether a dimension line on y = `y` covers `x`.
|
||
fn covers(g: &DimGeometry, y: f64, x: f64) -> bool {
|
||
dim_lines(g)
|
||
.iter()
|
||
.any(|l| l.windows(2).any(|w| (w[0].y - y).abs() < 1e-9 && (w[1].y - y).abs() < 1e-9 && w[0].x.min(w[1].x) < x && x < w[0].x.max(w[1].x)))
|
||
}
|
||
|
||
fn linear(s: &mut Session, x2: f64) {
|
||
s.execute("dimlinear", &json!({ "p1": [0, 0], "p2": [x2, 0], "at": [x2 / 2.0, 3] })).unwrap();
|
||
}
|
||
|
||
#[test]
|
||
fn new_variables_are_style_overrides_and_read_back() {
|
||
let mut s = Session::new();
|
||
for (name, v) in [
|
||
("DIMTOFL", json!(1)),
|
||
("DIMATFIT", json!(1)),
|
||
("DIMTIX", json!(1)),
|
||
("DIMSOXD", json!(1)),
|
||
("DIMTMOVE", json!(2)),
|
||
("DIMTVP", json!(1.5)),
|
||
("DIMTOLJ", json!(0)),
|
||
("DIMAZIN", json!(2)),
|
||
("DIMARCSYM", json!(1)),
|
||
("DIMLWD", json!(50)),
|
||
("DIMLWE", json!("ByLayer")),
|
||
("DIMFXLON", json!(1)),
|
||
("DIMFXL", json!(0.5)),
|
||
("DIMJOGANG", json!(60)),
|
||
("DIMSD1", json!(1)),
|
||
] {
|
||
set(&mut s, name, v);
|
||
}
|
||
let o = s.doc().unwrap().dim_overrides();
|
||
assert_eq!(o.get("forceLineInside"), Some(&json!(true)));
|
||
assert_eq!(o.get("fit"), Some(&json!(1)));
|
||
assert_eq!(o.get("textMove"), Some(&json!(2)));
|
||
assert_eq!(o.get("tolJust"), Some(&json!(0)));
|
||
assert_eq!(o.get("dimLineweight"), Some(&json!(50)));
|
||
assert_eq!(o.get("extLineweight"), Some(&json!(-1)));
|
||
assert_eq!(o.get("jogAngle"), Some(&json!(60.0)));
|
||
assert_eq!(o.get("suppressDim1"), Some(&json!(true)));
|
||
// A new dimension takes the overrides.
|
||
linear(&mut s, 10.0);
|
||
let (dm, _) = last_dim(&s);
|
||
let st = DimStyle::default().with_overrides(&dm.overrides);
|
||
assert!(st.force_line_inside && st.text_inside && st.suppress_outside_arrows && st.fixed_ext && st.suppress_dim1);
|
||
assert_eq!((st.fit, st.text_move, st.arc_symbol, st.angular_zero_suppression), (1, 2, 1, 2));
|
||
// Out-of-range values are refused and leave the style alone.
|
||
let mut st = DimStyle::default();
|
||
let bad = st.apply_fields(&serde_json::Map::from_iter([
|
||
("DIMATFIT".to_string(), json!(7)),
|
||
("DIMTMOVE".to_string(), json!(3)),
|
||
("DIMLWD".to_string(), json!(500)),
|
||
("DIMJOGANG".to_string(), json!(120)),
|
||
("DIMFXL".to_string(), json!(-1)),
|
||
]));
|
||
assert_eq!(bad.len(), 5, "{bad:?}");
|
||
assert_eq!(st, DimStyle::default());
|
||
// DIMOVERRIDE takes them per dimension.
|
||
let h = s.doc().unwrap().model.last().unwrap().handle.hex();
|
||
s.execute("dimoverride", &json!({ "handles": [h], "DIMTOFL": 0, "DIMLTYPE": "DASHED" })).unwrap();
|
||
let (dm, _) = last_dim(&s);
|
||
let st = DimStyle::default().with_overrides(&dm.overrides);
|
||
assert!(!st.force_line_inside && st.text_inside);
|
||
assert_eq!(st.dim_linetype, "DASHED");
|
||
}
|
||
|
||
#[test]
|
||
fn fit_moves_text_or_arrows_outside() {
|
||
// 0.5 between the extension lines: the text "0.5000" doesn't fit, the arrows (2 × 0.18) do.
|
||
let mut s = Session::new();
|
||
linear(&mut s, 0.5);
|
||
let (_, g) = last_dim(&s);
|
||
assert!(g.text_pos.x > 0.5, "best fit: text outside, {:?}", g.text_pos);
|
||
assert!(covers(&g, 3.0, 0.25), "arrows inside: the line runs between them");
|
||
// DIMATFIT 0: both outside; no line between the extension lines without DIMTOFL.
|
||
set(&mut s, "DIMATFIT", json!(0));
|
||
linear(&mut s, 0.5);
|
||
let (_, g) = last_dim(&s);
|
||
assert!(g.text_pos.x > 0.5);
|
||
assert!(!covers(&g, 3.0, 0.25));
|
||
assert!(covers(&g, 3.0, -0.2) && covers(&g, 3.0, 0.7), "stubs outside both extension lines");
|
||
// DIMTOFL draws it anyway.
|
||
set(&mut s, "DIMTOFL", json!(1));
|
||
linear(&mut s, 0.5);
|
||
let (_, g) = last_dim(&s);
|
||
assert!(covers(&g, 3.0, 0.25));
|
||
// DIMTIX keeps the text between the extension lines; DIMSOXD drops the outside arrows.
|
||
set(&mut s, "DIMTOFL", json!(0));
|
||
set(&mut s, "DIMTIX", json!(1));
|
||
linear(&mut s, 0.5);
|
||
let (_, g) = last_dim(&s);
|
||
assert!((g.text_pos.x - 0.25).abs() < 1e-9, "{:?}", g.text_pos);
|
||
assert_eq!(g.fills.len(), 2, "arrows outside");
|
||
set(&mut s, "DIMSOXD", json!(1));
|
||
linear(&mut s, 0.5);
|
||
let (_, g) = last_dim(&s);
|
||
assert!(g.fills.is_empty(), "outside arrows suppressed");
|
||
// DIMATFIT 1 (arrows first): text that fits alone stays inside, the arrows go out.
|
||
let mut s = Session::new();
|
||
set(&mut s, "DIMATFIT", json!(1));
|
||
set(&mut s, "DIMDEC", json!(0));
|
||
linear(&mut s, 0.5);
|
||
let (_, g) = last_dim(&s);
|
||
assert!((g.text_pos.x - 0.25).abs() < 1e-9, "{:?}", g.text_pos);
|
||
assert!(g.fills.iter().flatten().any(|p| p.x < 0.0) && g.fills.iter().flatten().any(|p| p.x > 0.5));
|
||
}
|
||
|
||
#[test]
|
||
fn suppressed_halves_fixed_extension_lines_and_vertical_position() {
|
||
let mut s = Session::new();
|
||
set(&mut s, "DIMTAD", json!(1));
|
||
set(&mut s, "DIMSD1", json!(1));
|
||
linear(&mut s, 10.0);
|
||
let (_, g) = last_dim(&s);
|
||
assert!(!covers(&g, 3.0, 2.0) && covers(&g, 3.0, 8.0));
|
||
assert_eq!(g.fills.len(), 1, "only the second arrowhead");
|
||
// Fixed-length extension lines: 0.5 from the dimension line toward the origin.
|
||
let mut s = Session::new();
|
||
set(&mut s, "DIMFXLON", json!(1));
|
||
set(&mut s, "DIMFXL", json!(0.5));
|
||
linear(&mut s, 10.0);
|
||
let (_, g) = last_dim(&s);
|
||
for l in g.lines_of(LineRole::Ext) {
|
||
let lo = l.iter().map(|p| p.y).fold(f64::INFINITY, f64::min);
|
||
assert!((lo - 2.5).abs() < 1e-9, "{l:?}");
|
||
}
|
||
// DIMTVP lifts centred text by text heights.
|
||
let mut s = Session::new();
|
||
set(&mut s, "DIMTVP", json!(1.0));
|
||
linear(&mut s, 10.0);
|
||
let (_, g) = last_dim(&s);
|
||
assert!((g.text_pos.y - (3.0 + 0.18)).abs() < 1e-9, "{:?}", g.text_pos);
|
||
assert!(covers(&g, 3.0, 5.0), "text off the line: the line isn't broken");
|
||
}
|
||
|
||
#[test]
|
||
fn text_move_settings() {
|
||
// DIMTMOVE 1: a leader joins text moved off the line; the line stays put.
|
||
let mut s = Session::new();
|
||
set(&mut s, "DIMTMOVE", json!(1));
|
||
linear(&mut s, 10.0);
|
||
let h = s.doc().unwrap().model.last().unwrap().handle.hex();
|
||
s.execute("dimtedit", &json!({ "handles": [h], "at": [5, 8] })).unwrap();
|
||
let (dm, g) = last_dim(&s);
|
||
assert!((dm.defpt.y - 3.0).abs() < 1e-9);
|
||
assert!(dim_lines(&g).iter().any(|l| l.first().is_some_and(|p| p.dist(Vec2::new(5.0, 3.0)) < 1e-9) && l.last().is_some_and(|p| p.y > 7.0)));
|
||
// DIMTMOVE 0: the line follows (DIMTAD 1 keeps it below the text).
|
||
let mut s = Session::new();
|
||
set(&mut s, "DIMTAD", json!(1));
|
||
linear(&mut s, 10.0);
|
||
let h = s.doc().unwrap().model.last().unwrap().handle.hex();
|
||
s.execute("dimtedit", &json!({ "handles": [h], "at": [5, 8] })).unwrap();
|
||
let (dm, _) = last_dim(&s);
|
||
assert!((dm.defpt.y - (8.0 - 0.09 - 0.09)).abs() < 1e-9, "{:?}", dm.defpt);
|
||
// Moving the text grip does the same.
|
||
let grips = s.doc().unwrap().model.last().unwrap().kind.grips();
|
||
let i = grips.iter().position(|p| p.dist(dm.text_mid.xy()) < 1e-9).unwrap();
|
||
s.execute("grip.stretch", &json!({ "handle": h, "index": i, "to": [5, 12] })).unwrap();
|
||
let (dm, _) = last_dim(&s);
|
||
assert!((dm.defpt.y - (12.0 - 0.18)).abs() < 1e-9, "{:?}", dm.defpt);
|
||
}
|
||
|
||
#[test]
|
||
fn tolerance_justification_angular_zeros_and_arc_symbol() {
|
||
let mut s = Session::new();
|
||
set(&mut s, "DIMTOL", json!(1));
|
||
set(&mut s, "DIMTP", json!(0.1));
|
||
set(&mut s, "DIMTM", json!(0.2));
|
||
linear(&mut s, 10.0);
|
||
let (_, middle) = last_dim(&s);
|
||
assert!(!middle.mtext.contains("\\A"));
|
||
set(&mut s, "DIMTOLJ", json!(0));
|
||
linear(&mut s, 10.0);
|
||
let (_, bottom) = last_dim(&s);
|
||
assert!(bottom.mtext.contains("\\A0;"), "{}", bottom.mtext);
|
||
// Bottom alignment: the lower tolerance sits on the baseline, higher than when centred.
|
||
let low = |g: &DimGeometry| g.text.iter().flatten().map(|p| p.y).fold(f64::INFINITY, f64::min);
|
||
assert!(low(&bottom) > low(&middle) + 1e-6);
|
||
|
||
// DIMAZIN 2 drops trailing zeros of angles.
|
||
let mut s = Session::new();
|
||
set(&mut s, "DIMADEC", json!(2));
|
||
s.execute("dimangular", &json!({ "vertex": [0, 0], "p1": [10, 0], "p2": [0, 10], "at": [3, 3] })).unwrap();
|
||
assert_eq!(last_dim(&s).1.value, "90.00°");
|
||
set(&mut s, "DIMAZIN", json!(2));
|
||
s.execute("dimangular", &json!({ "vertex": [0, 0], "p1": [10, 0], "p2": [0, 10], "at": [3, 3] })).unwrap();
|
||
assert_eq!(last_dim(&s).1.value, "90°");
|
||
|
||
// DIMARCSYM: before the text, over it, or none.
|
||
let mut s = Session::new();
|
||
s.execute("arc", &json!({ "center": [0, 0], "radius": 5, "start": 0, "end": 90 })).unwrap();
|
||
let a = s.doc().unwrap().model.last().unwrap().handle.hex();
|
||
s.execute("dimarc", &json!({ "handle": a, "at": [5, 5] })).unwrap();
|
||
let (_, g0) = last_dim(&s);
|
||
assert!(g0.value.starts_with('⌒'), "{}", g0.value);
|
||
set(&mut s, "DIMARCSYM", json!(2));
|
||
s.execute("dimarc", &json!({ "handle": a, "at": [5, 5] })).unwrap();
|
||
let (_, g2) = last_dim(&s);
|
||
assert!(!g2.value.contains('⌒'));
|
||
set(&mut s, "DIMARCSYM", json!(1));
|
||
s.execute("dimarc", &json!({ "handle": a, "at": [5, 5] })).unwrap();
|
||
let (_, g1) = last_dim(&s);
|
||
assert!(!g1.value.contains('⌒'));
|
||
assert_eq!(dim_lines(&g1).len(), dim_lines(&g2).len() + 1, "the symbol drawn over the text");
|
||
}
|
||
|
||
#[test]
|
||
fn linetypes_and_lineweights_of_dimension_and_extension_lines() {
|
||
let mut s = Session::new();
|
||
s.doc_mut().unwrap().linetypes.push(Linetype::simple("DASHED", "", &[0.5, -0.25]));
|
||
linear(&mut s, 10.0);
|
||
let opts = cadcraft_engine::render::Options { lineweights: true, ..Default::default() };
|
||
let count = |s: &Session| cadcraft_engine::render::build(s.doc().unwrap(), &cadcraft_engine::doc::Space::Model, &opts).prims.len();
|
||
let plain = count(&s);
|
||
let h = s.doc().unwrap().model.last().unwrap().handle.hex();
|
||
s.execute("dimoverride", &json!({ "handles": [h], "DIMLTEX2": "DASHED" })).unwrap();
|
||
let dashed_ext2 = count(&s);
|
||
assert!(dashed_ext2 > plain, "extension line 2 dashed: {plain} → {dashed_ext2}");
|
||
s.execute("dimoverride", &json!({ "handles": [h], "DIMLTYPE": "DASHED" })).unwrap();
|
||
assert!(count(&s) > dashed_ext2 + 4, "dimension line dashed too");
|
||
// Lineweights: 0.50 mm dimension lines, 0.13 mm extension lines.
|
||
s.execute("dimoverride", &json!({ "handles": [h], "clear": true })).unwrap();
|
||
s.execute("dimoverride", &json!({ "handles": [h], "DIMLWD": 50, "DIMLWE": 13 })).unwrap();
|
||
let dl = cadcraft_engine::render::build(s.doc().unwrap(), &cadcraft_engine::doc::Space::Model, &opts);
|
||
let lws: Vec<f32> = dl.prims.iter().filter(|p| p.kind == cadcraft_engine::render::Kind::Polyline).map(|p| p.lw).collect();
|
||
assert!(lws.contains(&0.5) && lws.contains(&0.13), "{lws:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn leaders_use_the_leader_arrow() {
|
||
let mut s = Session::new();
|
||
for st in &mut s.doc_mut().unwrap().dim_styles {
|
||
st.arrow_block = "_DOT".into();
|
||
}
|
||
s.execute("leader", &json!({ "points": [[0, 0], [5, 5]], "text": "A" })).unwrap();
|
||
let tris = |s: &Session| {
|
||
let dl = cadcraft_engine::render::build(s.doc().unwrap(), &cadcraft_engine::doc::Space::Model, &Default::default());
|
||
dl.prims.iter().filter(|p| p.kind == cadcraft_engine::render::Kind::Tris).map(|p| p.len).sum::<u32>()
|
||
};
|
||
// DIMLDRBLK "" is a closed filled arrow (one triangle) whatever DIMBLK is.
|
||
assert_eq!(tris(&s), 3);
|
||
for st in &mut s.doc_mut().unwrap().dim_styles {
|
||
st.leader_arrow_block = "_DOT".into();
|
||
}
|
||
assert_eq!(tris(&s), 72, "a dot: 24 triangles");
|
||
}
|