diff --git a/Cargo.lock b/Cargo.lock index 6c0ac18c5..10090ba49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -896,7 +896,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=935d90b86a99377dc95cba92e99a9b109180e2f6#935d90b86a99377dc95cba92e99a9b109180e2f6" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=d3830bb5230bd80e9f8abaa78b9f85d15161536a#d3830bb5230bd80e9f8abaa78b9f85d15161536a" dependencies = [ "acadrust", "cavalier_contours", @@ -907,7 +907,7 @@ dependencies = [ [[package]] name = "cadkernel-constraints" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=935d90b86a99377dc95cba92e99a9b109180e2f6#935d90b86a99377dc95cba92e99a9b109180e2f6" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=d3830bb5230bd80e9f8abaa78b9f85d15161536a#d3830bb5230bd80e9f8abaa78b9f85d15161536a" dependencies = [ "nalgebra", ] diff --git a/Cargo.toml b/Cargo.toml index 6d559022b..ab49a55da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,8 +32,8 @@ rfd = "0.17" clap = { version = "4", features = ["derive", "string"] } env_logger = "0.11" acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "6f3711809e39e9ffdf16a63086079b6475d97175", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "935d90b86a99377dc95cba92e99a9b109180e2f6", features = ["acis", "offset"] } -cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "935d90b86a99377dc95cba92e99a9b109180e2f6" } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "d3830bb5230bd80e9f8abaa78b9f85d15161536a", features = ["acis", "offset"] } +cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "d3830bb5230bd80e9f8abaa78b9f85d15161536a" } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index 667990bd2..c2f704fa2 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -3768,9 +3768,9 @@ fn tessellate_dimension_inner( // DIMTMOVE=1 connects the dimension line to rendered text. if let Some(s) = style { if s.dimtmove == 1 { - if let Some((anchor, txt)) = dimtmove_leader_endpoints(dim, text_layout.position) { + if let Some((anchor, txt, under_text)) = dimtmove_leader_endpoints(dim, text_layout) { let gap = dim_txt as f32 * 0.5; - if (txt - anchor).length() > gap * 2.0 { + if under_text || (txt - anchor).length() > gap * 2.0 { add_segment_with_text_break( &mut geom.dim_lines, anchor, @@ -4297,28 +4297,51 @@ fn split_ext_lines(points: &[[f32; 3]]) -> (Vec<[f32; 3]>, Vec<[f32; 3]>) { } /// Endpoints for the DIMTMOVE=1 leader. -fn dimtmove_leader_endpoints(dim: &Dimension, txt: Vec3) -> Option<(Vec3, Vec3)> { +fn dimtmove_leader_endpoints( + dim: &Dimension, + text: DimensionTextLayout, +) -> Option<(Vec3, Vec3, bool)> { let lv = |v| vec3_local(v); - let anchor = match dim { + let txt = text.position; + let linear = |first: Vec3, second: Vec3, definition: Vec3, axis: [f64; 2]| { + let bounds = text.break_box?; + let xy = |point: Vec3| [point.x as f64, point.y as f64]; + let (leader, under_text) = cadkernel::geom2d::linear_dimension_leader( + cadkernel::geom2d::Line { start: xy(first), end: xy(second) }, + xy(definition), + axis, + xy(txt), + [ + (bounds.half_width + bounds.padding) as f64, + (bounds.half_height + bounds.padding) as f64, + ], + bounds.rotation, + )?; + let point = |p: [f64; 2]| Vec3::new(p[0] as f32, p[1] as f32, first.z); + Some((point(leader.start), point(leader.end), under_text)) + }; + match dim { Dimension::Linear(d) => { - let perp = Vec3::new(-(d.rotation.sin() as f32), d.rotation.cos() as f32, 0.0); - let first = lv(d.first_point); - let second = lv(d.second_point); - let def = lv(d.definition_point); - let off1 = def.dot(perp) - first.dot(perp); - let off2 = def.dot(perp) - second.dot(perp); - (first + perp * off1 + second + perp * off2) * 0.5 + return linear( + lv(d.first_point), + lv(d.second_point), + lv(d.definition_point), + [d.rotation.cos(), d.rotation.sin()], + ); } Dimension::Aligned(d) => { let first = lv(d.first_point); let second = lv(d.second_point); - let axis = normalized_or(second - first, Vec3::X); - let perp = Vec3::new(-axis.y, axis.x, 0.0); - let def = lv(d.definition_point); - let off1 = def.dot(perp) - first.dot(perp); - let off2 = def.dot(perp) - second.dot(perp); - (first + perp * off1 + second + perp * off2) * 0.5 + return linear( + first, + second, + lv(d.definition_point), + [d.second_point.x - d.first_point.x, d.second_point.y - d.first_point.y], + ); } + _ => {} + } + let anchor = match dim { Dimension::Radius(_) => return None, Dimension::Diameter(d) => { let chord = lv(d.angle_vertex); @@ -4353,7 +4376,7 @@ fn dimtmove_leader_endpoints(dim: &Dimension, txt: Vec3) -> Option<(Vec3, Vec3)> } _ => return None, }; - Some((anchor, txt)) + Some((anchor, txt, false)) } /// Build a rectangle of filled triangles sitting under the dim text, used @@ -7188,8 +7211,10 @@ pub(crate) fn baked_large_radial_geometry( } } if style.is_some_and(|style| style.dimtmove == 1) { - if let Some((anchor, endpoint)) = dimtmove_leader_endpoints(dimension, text.position) { - if anchor.distance(endpoint) > text_height as f32 { + if let Some((anchor, endpoint, under_text)) = + dimtmove_leader_endpoints(dimension, text) + { + if under_text || anchor.distance(endpoint) > text_height as f32 { add_segment_with_text_break( &mut geometry.dim_lines, anchor, @@ -7527,3 +7552,89 @@ mod arch_format_tests { assert_eq!(format_engineering(-0.001, 2), "0'-0.00\""); } } + +#[cfg(test)] +mod dimtmove_leader_tests { + use super::*; + use acadrust::entities::DimensionLinear; + use acadrust::types::Vector3; + + fn v(x: f64, y: f64) -> Vector3 { + Vector3::new(x, y, 0.0) + } + + fn layout(position: Vec3, half_width: f32) -> DimensionTextLayout { + DimensionTextLayout { + position, + width: half_width * 2.0, + break_box: Some(TextBreak { + center: position, + half_width, + half_height: 1.0, + padding: 0.0, + rotation: 0.0, + }), + horizontal: true, + } + } + + /// Text outside along the axis → the dim line extends underneath the text. + #[test] + fn text_outside_extends_dim_line_under_text() { + let mut d = DimensionLinear::horizontal(v(0.0, 0.0), v(20.0, 0.0)); + d.definition_point = v(0.0, -5.0); // dimension line at y=-5 + let dim = Dimension::Linear(d); + // text anchor outside on the right: centre x=30, half width 3 → far edge 33 + let txt = vec3_local(v(30.0, -3.0)); + let (anchor, end, under) = dimtmove_leader_endpoints(&dim, layout(txt, 3.0)).expect("leader"); + assert!(under, "outside text extends the dimension line"); + assert!((anchor.x - 20.0).abs() < 1e-4 && (anchor.y + 5.0).abs() < 1e-4, "near end should be the dim line's right end (20,-5), got {anchor:?}"); + assert!((end.x - 33.0).abs() < 1e-4 && (end.y + 5.0).abs() < 1e-4, "far end should be the text's far edge (33,-5), got {end:?}"); + } + + /// Text outside on the left → extends leftwards. + #[test] + fn text_outside_left_extends_leftwards() { + let mut d = DimensionLinear::horizontal(v(0.0, 0.0), v(20.0, 0.0)); + d.definition_point = v(0.0, -5.0); + let dim = Dimension::Linear(d); + let txt = vec3_local(v(-12.0, -3.0)); + let (anchor, end, under) = dimtmove_leader_endpoints(&dim, layout(txt, 2.0)).expect("leader"); + assert!(under); + assert!((anchor.x - 0.0).abs() < 1e-4, "near end should be the dim line's left end, got {anchor:?}"); + assert!((end.x + 14.0).abs() < 1e-4, "far end should be the text's far edge at -14, got {end:?}"); + } + + /// Text still inside (only moved up/down) → leader from the dim line to the text. + #[test] + fn text_inside_connects_to_text() { + let mut d = DimensionLinear::horizontal(v(0.0, 0.0), v(20.0, 0.0)); + d.definition_point = v(0.0, -5.0); + let dim = Dimension::Linear(d); + let txt = vec3_local(v(10.0, 2.0)); + let (anchor, end, under) = dimtmove_leader_endpoints(&dim, layout(txt, 3.0)).expect("leader"); + assert!(!under, "inside → keep the connect-to-text leader (text break allowed)"); + assert!((anchor.x - 10.0).abs() < 1e-4 && (anchor.y + 5.0).abs() < 1e-4, "anchor should be the dim line point facing the text, got {anchor:?}"); + assert!((end.y - 2.0).abs() < 1e-4, "end should be the text position, got {end:?}"); + } + + /// Radius dimensions draw no leader (unchanged). + #[test] + fn radius_has_no_leader() { + let r = acadrust::entities::DimensionRadius::new(v(0.0, 0.0), v(0.0, 5.0)); + assert!(dimtmove_leader_endpoints(&Dimension::Radius(r), layout(vec3_local(v(0.0, 8.0)), 2.0)).is_none()); + } + + #[test] + fn extension_does_not_draw_through_text() { + let mut d = DimensionLinear::horizontal(v(0.0, 0.0), v(20.0, 0.0)); + d.definition_point = v(0.0, -5.0); + let text = layout(Vec3::new(30.0, -5.0, 0.0), 3.0); + let (start, end, outside) = dimtmove_leader_endpoints(&Dimension::Linear(d), text).unwrap(); + assert!(outside); + let mut points = Vec::new(); + add_segment_with_text_break(&mut points, start, end, text.break_box); + let finite: Vec<_> = points.into_iter().filter(|point| point[0].is_finite()).collect(); + assert_eq!(finite, vec![[20.0, -5.0, 0.0], [27.0, -5.0, 0.0]]); + } +} diff --git a/tests/dim_leader_render_check.rs b/tests/dim_leader_render_check.rs new file mode 100644 index 000000000..8f0e9916c --- /dev/null +++ b/tests/dim_leader_render_check.rs @@ -0,0 +1,78 @@ +//! Check DIMTMOVE through the scene's actual dimension tessellation. + +use acadrust::entities::{Dimension, DimensionAligned, DimensionLinear}; +use acadrust::types::Vector3; +use acadrust::xdata::XDataValue; +use acadrust::EntityType; +use OpenCADStudio::entities::dim_override as ov; +use OpenCADStudio::scene::Scene; + +#[test] +fn dim_leader_extends_only_when_requested() { + for aligned in [false, true] { + for movement in [0, 1, 2] { + for (text_x, text_y) in [ + (-18.0, 89.0), + (98.0, 89.0), + (19.9, 89.0), + (80.1, 89.0), + (-18.0, 86.0), + (98.0, 86.0), + ] { + let mut scene = Scene::new(); + let first = Vector3::new(20.0, 100.0, 0.0); + let second = Vector3::new(80.0, 100.0, 0.0); + let definition = Vector3::new(20.0, 86.0, 0.0); + let mut dim = if aligned { + let mut d = DimensionAligned::new(first, second); + d.definition_point = definition; + Dimension::Aligned(d) + } else { + let mut d = DimensionLinear::horizontal(first, second); + d.definition_point = definition; + Dimension::Linear(d) + }; + let base = dim.base_mut(); + base.definition_point = definition; + base.text_middle_point = Vector3::new(text_x, text_y, 0.0); + base.insertion_point = base.text_middle_point; + base.text_user_positioned = true; + base.style_name = "Standard".into(); + let handle = scene.add_entity(EntityType::Dimension(dim)); + for (code, value) in [ + (ov::DIMTMOVE, XDataValue::Integer16(movement)), + (ov::DIMTXT, XDataValue::Real(3.5)), + (ov::DIMASZ, XDataValue::Real(3.5)), + (ov::DIMGAP, XDataValue::Real(1.0)), + ] { + ov::set(&mut scene.document, handle, code, Some(value)); + } + scene.bump_entities(&[(handle, OpenCADStudio::scene::ChangeKind::Modified)]); + let wires = scene.entity_wires(); + let extension = wires.iter().any(|wire| { + (0..wire.points.len().saturating_sub(1)).any(|index| { + let a = wire.point_world(index, 1.0); + let b = wire.point_world(index + 1, 1.0); + if !a.is_finite() || !b.is_finite() { + return false; + } + let near = if text_x > 80.0 { 80.0 } else { 20.0 }; + (a.x - near).abs() < 1e-4 + && (a.y - 86.0).abs() < 1e-4 + && (b.y - 86.0).abs() < 1e-4 + && if text_x > 80.0 { + b.x > text_x + } else { + b.x < text_x + } + }) + }); + assert_eq!( + extension, + movement == 1 && text_y == 89.0, + "aligned={aligned}, DIMTMOVE={movement}, text=({text_x},{text_y})" + ); + } + } + } +}