use super::super::testutil::{by_id, laid, layout_err, text_at, texts};
use crate::ledger::consts::{DIM_OFFSET, DIM_PITCH};
use crate::resolve::{MarkerKind, NodeKind, ResolvedValue};
#[test]
fn a_chain_shares_one_row_and_the_next_dim_packs_outside() {
let l = laid(
"{ layout: drawing; scale: 2; density: 1 }\n|rect#plate| { width: 150; height: 40 }\n|hole#a| { width: 8; translate: -50 0 }\n|hole#b| { width: 8; translate: 10 0 }\nplate:left (-) a (-) b (-) plate:right { side: bottom }\nplate:left (-) plate:right { side: bottom }\n",
);
let (_, y25, _) = text_at(&l.nodes, "25");
let (_, y60, _) = text_at(&l.nodes, "60");
let (_, y65, _) = text_at(&l.nodes, "65");
let (_, y150, _) = text_at(&l.nodes, "150");
assert!(
(y25 - y60).abs() < 1e-6 && (y60 - y65).abs() < 1e-6,
"hops share the row: {y25} / {y60} / {y65}"
);
assert!(
(y150 - y60 - DIM_PITCH).abs() < 0.01,
"the 150 packs one pitch out: {y150} vs {y60}"
);
assert!((y60 - (41.0 + DIM_OFFSET - 7.5)).abs() < 0.6, "y60={y60}");
}
#[test]
fn iso_text_turns_with_a_vertical_dim() {
let l = laid(
"{ layout: drawing; density: 1 }\n|rect#a| { width: 60; height: 40 }\na:top (-) a:bottom { side: right }\n",
);
let (x, _, rot) = text_at(&l.nodes, "40");
assert_eq!(rot, -90.0, "reads from the right");
let a = by_id(&l.nodes, "a");
assert!(x > a.cx + 30.0, "stacked right of the geometry: x={x}");
}
#[test]
fn a_narrow_span_flips_arrows_out_but_keeps_a_fitting_value_inside() {
let l = laid(
"{ layout: drawing; density: 1 }\n|rect#a| { width: 20; height: 10 }\na:left (-) a:right { side: bottom }\n",
);
let (x, _, _) = text_at(&l.nodes, "20");
assert!(x.abs() < 1e-6, "value centred inside the span: x={x}");
}
#[test]
fn a_span_too_tight_for_its_value_slides_the_text_past() {
let l = laid(
"{ layout: drawing; density: 1 }\n|rect#a| { width: 10; height: 8 }\na:left (-) a:right { side: bottom }\n",
);
let (x, _, _) = text_at(&l.nodes, "10");
assert!(x > 5.0, "text outside the span: x={x}");
}
#[test]
fn corner_anchors_on_one_edge_pull_the_dim_there() {
let l = laid(
"{ layout: drawing; density: 1 }\n|rect#a| { width: 40; height: 20 }\n|rect#b| { width: 40; height: 20; translate: 70 0 }\na:top-left (-) b:top-right\n",
);
let (_, y, _) = text_at(&l.nodes, "110");
let a = by_id(&l.nodes, "a");
assert!(y < a.cy - 10.0, "pulled to the top: y={y}");
}
#[test]
fn a_two_ended_label_replaces_the_number() {
let l = laid(
"{ layout: drawing; density: 1 }\n|rect#a| { width: 40; height: 20 }\na:left (-) a:right \"180\"\n",
);
text_at(&l.nodes, "180");
assert!(
!texts(&l.nodes).iter().any(|(t, ..)| t == "40"),
"the measured 40 is replaced"
);
}
#[test]
fn measured_values_read_bare_pre_scale_numbers() {
let l = laid(
"{ layout: drawing; unit: mm }\n|rect#a| { width: 40; height: 20 }\n|hole#h| { width: 12 }\na:left (-) a:right { side: bottom }\nh (o)\n",
);
text_at(&l.nodes, "40");
text_at(&l.nodes, "⌀12");
}
#[test]
fn dim_errors_speak_spec() {
assert_eq!(
layout_err(
"{ layout: drawing; density: 1 }\n|rect#a| { width: 40; height: 20 }\n|rect#b| { width: 40; height: 20 }\na:left (-) b:top\n"
),
"'a:left (-) b:top' mixes axes — anchor one axis"
);
assert_eq!(
layout_err(
"{ layout: drawing; density: 1 }\n|rect#a| { width: 40; height: 20 }\na:left (-) a:right { side: left }\n"
),
"a horizontal dimension stacks on top or bottom"
);
assert_eq!(
layout_err(
"{ layout: drawing; density: 1 }\n|rect#a| { width: 40; height: 20 }\na:top (-) a:bottom { side: top }\n"
),
"a vertical dimension stacks on left or right"
);
assert_eq!(
layout_err(
"{ layout: drawing; density: 1 }\n|rect#a| { width: 40; height: 20 }\na:left (-) a:right { tol: \"x\" }\n"
),
"'tol' takes a number, '+upper -lower', or a fit ident"
);
}
#[test]
fn tol_composes_its_three_forms() {
let sym = laid(
"{ layout: drawing; density: 1 }\n|rect#a| { width: 40; height: 20 }\na:left (-) a:right { tol: 0.1 }\n",
);
text_at(&sym.nodes, "40±0.1");
let fit = laid(
"{ layout: drawing; density: 1 }\n|rect#a| { width: 40; height: 20 }\na:left (-) a:right { tol: H7 }\n",
);
text_at(&fit.nodes, "40 H7");
let dev = laid(
"{ layout: drawing; density: 1 }\n|rect#a| { width: 40; height: 20 }\na:left (-) a:right { tol: +0.2 -0.05 }\n",
);
let (_, yu, _) = text_at(&dev.nodes, "+0.2");
let (_, yl, _) = text_at(&dev.nodes, "-0.05");
let (_, ym, _) = text_at(&dev.nodes, "40");
assert!(
yu < ym && ym < yl,
"raised {yu} / value {ym} / lowered {yl}"
);
}
#[test]
fn a_named_arc_reads_its_radius() {
let l = laid(
"{ layout: drawing; scale: 2; density: 1 }\n|sketch#s| { draw: move(0, 0) right(30) fillet(3):r1 up(20) left(30) down(20) close() }\ns:r1 (o)\n",
);
text_at(&l.nodes, "R3");
}
#[test]
fn a_circle_segment_reads_its_diameter() {
let l = laid(
"{ layout: drawing; density: 1 }\n|sketch#s| { draw: move(0, 0) right(40) up(20) left(40) close() move(20, -10) circle(5):c }\ns:c (o)\n",
);
text_at(&l.nodes, "⌀10");
}
#[test]
fn a_bare_round_node_leaders_its_diameter_with_the_copy_count() {
let l = laid(
"{ layout: drawing; density: 1 }\n|rect#plate| { width: 120; height: 60 } [\n |hole#pin| { width: 10; translate: -35 0; pattern: grid(2, 1, 70, 0) }\n]\nplate.pin (o) \"H7\"\n",
);
text_at(&l.nodes, "2× ⌀10 H7");
}
#[test]
fn a_side_anchor_on_a_round_node_draws_the_diametral_line() {
let l = laid(
"{ layout: drawing; density: 1 }\n|rect#plate| { width: 80; height: 40 }\n|hole#eye| { width: 16 }\neye:top (o)\n",
);
let (_, y, rot) = text_at(&l.nodes, "⌀16");
assert_eq!(rot, -90.0, "turned with the line");
assert!(y < -8.0, "spills past the top rim: y={y}");
}
#[test]
fn a_side_anchor_on_any_node_spans_to_the_opposite_side() {
let l = laid(
"{ layout: drawing; density: 1 }\n|rect#bore| { width: 60; height: 16 }\nbore:top (o) { side: right }\n",
);
let (x, _, _) = text_at(&l.nodes, "⌀16");
assert!(x > 30.0, "stacked on the right: x={x}");
}
#[test]
fn a_revolved_name_spans_its_station_across_the_axis() {
let l = laid(
"{ layout: drawing; scale: 2; density: 1 }\n|sketch#bar| { draw: move(-150, 0) up(10) right(40):thread right(260) down(10); revolve: x-axis }\nbar:thread (o) { side: left; tol: h6 }\n",
);
text_at(&l.nodes, "⌀20 h6");
}
#[test]
fn a_station_diameter_requires_a_revolve() {
assert_eq!(
layout_err(
"{ layout: drawing; scale: 2; density: 1 }\n|sketch#bar| { draw: move(-150, 0) up(10) right(40):thread right(260) down(10); mirror: x-axis }\nbar:thread (o) { side: left }\n"
),
"a station '⌀' reads a revolved profile — 'revolve: x-axis'"
);
}
#[test]
fn a_bare_round_measure_needs_an_axis() {
assert_eq!(
layout_err(
"{ layout: drawing; density: 1 }\n|rect#block| { width: 40; height: 20 }\nblock (o)\n"
),
"'(o)' can't pick an axis on 'block' — anchor a side ('block:top (o)') or a segment"
);
}
#[test]
fn an_angle_reads_two_edges_and_rides_its_arc() {
let l = laid(
"{ layout: drawing; scale: 2; density: 1 }\n|sketch#g| { draw: move(-40, 30) right(80):base up(60) line(-80, 60):flank close() }\ng:flank (<) g:base\n",
);
text_at(&l.nodes, "36.87°");
}
#[test]
fn a_unary_angle_measures_the_included_taper() {
let l = laid(
"{ layout: drawing; density: 1 }\n|sketch#cone| { draw: move(0, 0) line(40, -10):taper; mirror: x-axis }\ncone:taper (<)\n",
);
text_at(&l.nodes, "28.07°");
}
#[test]
fn angle_errors_speak_spec() {
assert_eq!(
layout_err(
"{ layout: drawing; density: 1 }\n|oval#a| { width: 20; height: 20 }\n|oval#b| { width: 20; height: 20 }\na (<) b\n"
),
"an angle reads two edges — a named segment, a '|line|', or a side"
);
assert_eq!(
layout_err(
"{ layout: drawing; density: 1 }\n|sketch#s| { draw: move(0, 0) line(40, -10):taper up(10) close() }\ns:taper (<)\n"
),
"'(<)' on ':taper' needs 'mirror:' — no twin to measure against"
);
assert_eq!(
layout_err(
"{ layout: drawing; density: 1 }\n|rect#a| { width: 40; height: 20 }\n|rect#b| { width: 40; height: 20 }\na:top (<) b:bottom\n"
),
"the angle's edges are parallel — they never meet"
);
}
fn arrow_tip(nodes: &[crate::layout::PlacedNode]) -> (f64, f64) {
nodes
.iter()
.find(|n| n.type_chain.iter().any(|t| t == "marker-dim"))
.map(|n| {
crate::layout::primitives::attr_points(&n.attrs, "points", n.span)
.unwrap()
.unwrap()[0]
})
.expect("a slender arrowhead")
}
#[test]
fn a_leader_tip_ray_casts_onto_the_outline_with_a_landing_elbow() {
let l = laid(
"{ layout: drawing; density: 1 }\n|oval#disc| { width: 40; height: 40 }\ndisc:top-right <- \"THRU\"\n",
);
let line = l
.nodes
.iter()
.find(|n| n.type_chain.iter().any(|t| t == "dim-line"))
.expect("the leader line");
let pts = crate::layout::primitives::attr_points(&line.attrs, "points", line.span)
.unwrap()
.unwrap();
assert_eq!(pts.len(), 3, "tip, elbow, landing");
let tip = arrow_tip(&l.nodes);
assert!(
(tip.0.hypot(tip.1) - 20.0).abs() < 0.75,
"tip on the rim: {tip:?}"
);
assert!((pts[1].1 - pts[2].1).abs() < 1e-9, "horizontal landing");
let (tx, ty, _) = text_at(&l.nodes, "THRU");
assert!(tx > pts[2].0, "text past the landing");
assert!((ty - pts[2].1).abs() < 1e-6, "text rides the landing");
}
#[test]
fn a_word_leader_tips_the_rim_of_a_patterned_hole() {
let l = laid(
"{ layout: drawing; density: 1 }\n|rect#plate| { width: 120; height: 60 } [\n |hole#pin| { width: 10; translate: -35 0; pattern: grid(2, 1, 70, 0) }\n]\nplate.pin <- \"THRU\" { side: top }\n",
);
let tip = arrow_tip(&l.nodes);
let d = ((tip.0 - -35.0).powi(2) + tip.1.powi(2)).sqrt();
assert!((d - 5.0).abs() < 0.75, "tip on the seed's rim: {tip:?}");
}
#[test]
fn a_circle_diameter_runs_across_with_both_arrows() {
let l = laid(
"{ layout: drawing; density: 1 }\n|rect#plate| { width: 80; height: 40 }\n|hole#eye| { width: 12 }\neye (o)\n",
);
let arrows: Vec<_> = l
.nodes
.iter()
.filter(|n| n.type_chain.iter().any(|t| t == "marker-dim"))
.collect();
assert_eq!(arrows.len(), 2, "an arrowhead on each rim");
let line = l
.nodes
.iter()
.find(|n| n.type_chain.iter().any(|t| t == "dim-line"))
.expect("the ⌀ line");
let pts = crate::layout::primitives::attr_points(&line.attrs, "points", line.span)
.unwrap()
.unwrap();
let start_r = (pts[0].0.powi(2) + pts[0].1.powi(2)).sqrt();
assert!(
start_r > 6.0 && start_r < 21.0,
"the line overshoots the far rim: {pts:?}"
);
}
#[test]
fn side_steers_a_leader() {
let l = laid(
"{ layout: drawing; density: 1 }\n|oval#disc| { width: 40; height: 40 }\ndisc <- \"A\" { side: left }\n",
);
let (tx, _, _) = text_at(&l.nodes, "A");
assert!(tx < -20.0, "text left of the disc: {tx}");
}
#[test]
fn the_datum_triangle_seats_on_the_surface() {
let l = laid(
"{ layout: drawing; density: 1 }\n|rect#block| { width: 60; height: 30 }\nblock:bottom >- \"A\"\n",
);
let tri = l
.nodes
.iter()
.find(|n| n.type_chain.iter().any(|t| t == "marker-datum"))
.expect("the seated datum triangle");
let pts = crate::layout::primitives::attr_points(&tri.attrs, "points", tri.span)
.unwrap()
.unwrap();
assert!(
(pts[0].1 - 15.0).abs() < 1e-6 && (pts[1].1 - 15.0).abs() < 1e-6,
"base on the bottom face: {pts:?}"
);
assert!(pts[2].1 > 15.0, "apex out along the normal: {pts:?}");
let line = l
.nodes
.iter()
.find(|n| n.type_chain.iter().any(|t| t == "dim-line"))
.expect("the datum leader");
let lp = crate::layout::primitives::attr_points(&line.attrs, "points", line.span)
.unwrap()
.unwrap();
assert!(
(lp[0].0 - lp[1].0).abs() < 1e-6,
"straight off the surface: {lp:?}"
);
let l = laid(
"{ layout: drawing; density: 1 }\n|oval#pin| { width: 20; height: 20 }\npin >- \"B\"\n",
);
assert!(
l.nodes
.iter()
.any(|n| n.kind == NodeKind::Line && n.markers.start == MarkerKind::Datum),
"the fallback datum marker"
);
}
#[test]
fn a_two_ended_arrow_trims_at_the_rim_and_dots_within() {
let l = laid(
"{ layout: drawing; density: 1 }\n|rect#part| { width: 60; height: 30 }\n|balloon#b1| \"1\" { translate: 60 -40 }\nb1 -* part\n",
);
let line = l
.nodes
.iter()
.find(|n| n.kind == NodeKind::Line && n.markers.end == MarkerKind::Dot)
.expect("the wire");
let pts = crate::layout::primitives::attr_points(&line.attrs, "points", line.span)
.unwrap()
.unwrap();
let b1 = by_id(&l.nodes, "b1");
assert!(
(pts[0].0 - b1.cx).hypot(pts[0].1 - b1.cy) > 7.0,
"start off the balloon's centre: {:?}",
pts[0]
);
assert_eq!(pts[1], (0.0, 0.0), "the dot lands on the part's origin");
}
#[test]
fn a_leader_tip_lands_on_a_recessed_edge_not_the_box() {
let l = laid(
"{ layout: drawing; scale: 3; density: 1 }\n|sketch#body| { draw: move(-80, 0) up(21) right(38):thread right(32):land up(4) right(90) down(25); mirror: x-axis }\nbody:thread <- \"M42\" { side: top }\nbody:land >- \"A\"\n",
);
let arrow_tip = arrow_tip(&l.nodes);
assert!(
(arrow_tip.1 + 63.0).abs() < 1e-6,
"the arrow touches the drawn surface: {arrow_tip:?}"
);
let tri = l
.nodes
.iter()
.find(|n| n.type_chain.iter().any(|t| t == "marker-datum"))
.expect("the seated datum triangle");
let pts = crate::layout::primitives::attr_points(&tri.attrs, "points", tri.span)
.unwrap()
.unwrap();
assert!(
(pts[0].1 + 63.0).abs() < 1e-6 && (pts[1].1 + 63.0).abs() < 1e-6,
"the datum base sits on the drawn surface: {pts:?}"
);
}
#[test]
fn a_dim_row_clears_leader_texts() {
let l = laid(
"{ layout: drawing; density: 1 }\n|rect#bar| { width: 200; height: 30 }\nbar:top <- \"M42\"\nbar:left (-) bar:right { side: top }\n",
);
let (_, ty, _) = text_at(&l.nodes, "M42");
let (_, dy, _) = text_at(&l.nodes, "200");
assert!(
dy < ty - 8.0,
"the 200 climbs past the callout text: dim {dy} vs callout {ty}"
);
}
#[test]
fn dimension_anatomy_wears_its_classes() {
let l = laid(
"{ layout: drawing; density: 1 }\n|rect#a| { width: 60; height: 20 }\na:left (-) a:right { side: bottom }\n",
);
let with_chain = |name: &str| {
l.nodes
.iter()
.filter(|n| n.type_chain.iter().any(|t| t == name))
.collect::<Vec<_>>()
};
assert_eq!(with_chain("ext-line").len(), 2, "two extension springs");
assert_eq!(with_chain("dim-line").len(), 1, "the dim line");
assert_eq!(with_chain("marker-dim").len(), 2, "two arrowheads");
assert!(
matches!(
with_chain("ext-line")[0].attrs.get("stroke"),
Some(ResolvedValue::LiveVar { name, .. }) if name == "stroke-light"
),
"--stroke-light by default"
);
let red = laid(
"{ layout: drawing; density: 1 }\n|rect#a| { width: 60; height: 20 }\na:left (-) a:right { side: bottom; stroke: red }\n",
);
let ext = red
.nodes
.iter()
.find(|n| n.type_chain.iter().any(|t| t == "ext-line"))
.expect("extension line");
assert!(
matches!(ext.attrs.get("stroke"), Some(ResolvedValue::Ident(c)) if c == "red"),
"a recoloured statement recolours its extension lines too"
);
}
#[test]
fn drawing_links_thin_to_stroke_width_1() {
let width_of = |src: &str| {
let l = laid(src);
let dim_line = l
.nodes
.iter()
.find(|n| n.kind == NodeKind::Line)
.expect("a dim line");
match dim_line.attrs.get("stroke-width") {
Some(ResolvedValue::Number(w)) => *w,
other => panic!("stroke-width: {other:?}"),
}
};
assert_eq!(
width_of(
"{ layout: drawing; density: 1 }\n|rect#a| { width: 40; height: 20 }\na:left (-) a:right\n"
),
1.0,
"the drawing-scope link default"
);
let l = laid(
"|drawing#d| { scale: 0.25 } [\n |rect#part| { width: 40; height: 20 }\n |row#legend| { translate: 0 60 } [\n |box#a| \"a\"\n |box#b| \"b\"\n a -> b\n ]\n]\n",
);
let wire = l.links.first().expect("the routed flow link");
assert!(
wire.attrs.number("stroke-width").is_none_or(|w| w != 1.0),
"a nested flow's links keep the flow weight: {:?}",
wire.attrs.get("stroke-width")
);
assert_eq!(
width_of(
"{ layout: drawing; scale: 1;\n |-| { stroke-width: 2 }\n}\n|rect#a| { width: 40; height: 20 }\na:left (-) a:right\n"
),
2.0,
"a user '|-|' rule wins over the scope default"
);
}