Merge pull request #1235 with kernel-based dimension leaders

This commit is contained in:
Hakan Seven committed 2026-09-13 21:14:47 +03:00
commit 64e64c80a1
4 files changed
+213 -24

No files matched your search

Generated
+2 -2
View File
@@ -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",
]
+2 -2
View File
@@ -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"] }
+131 -20
View File
@@ -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]]);
}
}
+78
View File
@@ -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})"
);
}
}
}
}