diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 5661579dd..249662785 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -2781,7 +2781,10 @@ pub(crate) fn arrow_from_block_with_deferred_hatch( .unwrap_or_else(|| arrow_from_block_name(None, dimasz)) } -fn arrow_from_block_name(name: Option<&str>, dimasz: f32) -> ArrowKind { +/// Canonical block-name → arrow-kind mapping shared by the renderer and the +/// dimstyle preview so both agree on what a named arrowhead looks like. +/// Unknown / empty names fall back to the standard filled triangle. +pub(crate) fn arrow_from_block_name(name: Option<&str>, dimasz: f32) -> ArrowKind { name.and_then(|name| builtin_arrow_from_block_name(name, dimasz)) .unwrap_or(ArrowKind::Triangle { size: dimasz, @@ -3453,6 +3456,36 @@ pub(crate) fn normalized_or(v: Vec3, fallback: Vec3) -> Vec3 { mod tests { use super::*; + #[test] + fn arrow_name_mapping_covers_preview_kinds() { + // Pins the shared name→kind contract the dimstyle preview relies on. + assert!(matches!( + arrow_from_block_name(Some("OPEN"), 1.0), + ArrowKind::Open { .. } + )); + assert!(matches!( + arrow_from_block_name(Some("DOT"), 1.0), + ArrowKind::Dot { filled: true, .. } + )); + assert!(matches!( + arrow_from_block_name(Some("ARCHTICK"), 1.0), + ArrowKind::Tick { .. } + )); + assert!(matches!( + arrow_from_block_name(Some("NONE"), 1.0), + ArrowKind::None + )); + // Unknown / empty names fall back to the standard filled triangle. + assert!(matches!( + arrow_from_block_name(Some("NOPE"), 1.0), + ArrowKind::Triangle { filled: true, .. } + )); + assert!(matches!( + arrow_from_block_name(None, 1.0), + ArrowKind::Triangle { filled: true, .. } + )); + } + #[test] fn test_split_mixed_polyline_straight_lines_have_taper_widths() { let tg_line = TangentGeom::Line { diff --git a/src/ui/style/dimstyle.rs b/src/ui/style/dimstyle.rs index 93aef476c..7bc285518 100644 --- a/src/ui/style/dimstyle.rs +++ b/src/ui/style/dimstyle.rs @@ -1,6 +1,7 @@ //! Dimension Style Manager window — fills the entire OS window. use crate::app::{ColorPickTarget, DsField, Message}; +use crate::scene::convert::tessellate::{arrow_from_block_name, ArrowKind}; use iced::widget::{ button, canvas, checkbox, column, container, row, scrollable, text, text_input, Space, }; @@ -171,11 +172,32 @@ struct DimensionPreview { dim2: bool, tick: bool, arrow_size: f32, + arrow1: ArrowKind, + arrow2: ArrowKind, text_above: bool, basic: bool, text: String, } +/// Resolve the staged arrowhead block names to per-end preview kinds: the +/// shared `dimblk` when Dimsah is off, the per-end blocks when on — the same +/// selection the renderer makes. `dimasz` is the preview arrow size so +/// size-scaled kinds (SMALL, DOTSMALL) keep their proportions. +fn preview_arrow_kinds( + dimsah: bool, + dimblk: &str, + dimblk1: &str, + dimblk2: &str, + dimasz: f32, +) -> (ArrowKind, ArrowKind) { + let kind = |name: &str| arrow_from_block_name(Some(name), dimasz); + if dimsah { + (kind(dimblk1), kind(dimblk2)) + } else { + (kind(dimblk), kind(dimblk)) + } +} + impl canvas::Program for DimensionPreview { type State = (); @@ -218,23 +240,107 @@ impl canvas::Program for DimensionPreview { } let size = self.arrow_size.clamp(5.0, 14.0); - for (tip_x, sign) in [(x1, 1.0_f32), (x2, -1.0_f32)] { + // Per-end arrowheads: each end draws its own resolved kind so split + // (Dimsah) configurations read correctly. A global DIMTSZ tick still + // overrides both ends, matching AutoCAD precedence. + let tick_path = |tip_x: f32, tick_size: f32| { + canvas::Path::line( + Point::new(tip_x - tick_size * 0.55, y + tick_size * 0.75), + Point::new(tip_x + tick_size * 0.55, y - tick_size * 0.75), + ) + }; + for (kind, tip_x, sign) in [(&self.arrow1, x1, 1.0_f32), (&self.arrow2, x2, -1.0_f32)] { if self.tick { frame.stroke( - &line( - Point::new(tip_x - size * 0.55, y + size * 0.75), - Point::new(tip_x + size * 0.55, y - size * 0.75), - ), + &tick_path(tip_x, size), canvas::Stroke::default().with_color(guide).with_width(1.6), ); - } else { - let arrow = canvas::Path::new(|path| { - path.move_to(Point::new(tip_x, y)); - path.line_to(Point::new(tip_x + sign * size, y - size * 0.42)); - path.line_to(Point::new(tip_x + sign * size, y + size * 0.42)); - path.close(); - }); - frame.fill(&arrow, guide); + continue; + } + match kind { + ArrowKind::None => {} + ArrowKind::Triangle { size, filled, size_mul } => { + let s = size * size_mul; + let arrow = canvas::Path::new(|path| { + path.move_to(Point::new(tip_x, y)); + path.line_to(Point::new(tip_x + sign * s, y - s * 0.42)); + path.line_to(Point::new(tip_x + sign * s, y + s * 0.42)); + path.close(); + }); + if *filled { + frame.fill(&arrow, guide); + } else { + frame.stroke(&arrow, canvas::Stroke::default().with_color(guide).with_width(1.4)); + } + } + ArrowKind::Open { size, half_angle } => { + let (sin, cos) = half_angle.sin_cos(); + for side in [-1.0_f32, 1.0_f32] { + frame.stroke( + &line( + Point::new(tip_x, y), + Point::new(tip_x + sign * size * cos, y + side * size * sin), + ), + canvas::Stroke::default().with_color(guide).with_width(1.4), + ); + } + } + ArrowKind::Dot { size, filled } => { + let dot = canvas::Path::circle(Point::new(tip_x + sign * size * 0.5, y), size * 0.5); + if *filled { + frame.fill(&dot, guide); + } else { + frame.stroke(&dot, canvas::Stroke::default().with_color(guide).with_width(1.4)); + } + } + ArrowKind::Tick { size } => { + frame.stroke( + &tick_path(tip_x, *size), + canvas::Stroke::default().with_color(guide).with_width(1.6), + ); + } + ArrowKind::Box_ { size, filled } => { + let half = size * 0.45; + let square = canvas::Path::rectangle( + Point::new(tip_x + sign * half - half, y - half), + iced::Size::new(half * 2.0, half * 2.0), + ); + if *filled { + frame.fill(&square, guide); + } else { + frame.stroke(&square, canvas::Stroke::default().with_color(guide).with_width(1.4)); + } + } + // Datum/Origin have dedicated CAD geometry; the preview keeps + // the schematic triangle/dot respectively. + ArrowKind::Datum { size, filled } => { + let arrow = canvas::Path::new(|path| { + path.move_to(Point::new(tip_x, y)); + path.line_to(Point::new(tip_x + sign * size, y - size * 0.42)); + path.line_to(Point::new(tip_x + sign * size, y + size * 0.42)); + path.close(); + }); + if *filled { + frame.fill(&arrow, guide); + } else { + frame.stroke(&arrow, canvas::Stroke::default().with_color(guide).with_width(1.4)); + } + } + ArrowKind::Origin { size } => { + let dot = canvas::Path::circle(Point::new(tip_x + sign * size * 0.5, y), size * 0.4); + frame.stroke(&dot, canvas::Stroke::default().with_color(guide).with_width(1.4)); + } + // Custom DWG block geometry cannot render in the schematic + // preview; fall back to the standard filled triangle. + ArrowKind::Custom { .. } => { + let arrow = canvas::Path::new(|path| { + path.move_to(Point::new(tip_x, y)); + path.line_to(Point::new(tip_x + sign * size, y - size * 0.42)); + path.line_to(Point::new(tip_x + sign * size, y + size * 0.42)); + path.close(); + }); + frame.fill(&arrow, guide); + } } } @@ -1127,13 +1233,23 @@ pub fn view_window<'a>( preview_text.push_str(&format!(" +{} −{}", vals.dimtp.trim(), vals.dimtm.trim())); } } + let arrow_size = vals.dimasz.trim().parse::().unwrap_or(1.0).abs() * 5.0 + 5.0; + let (arrow1, arrow2) = preview_arrow_kinds( + vals.dimsah, + &vals.dimblk_name, + &vals.dimblk1_name, + &vals.dimblk2_name, + arrow_size.clamp(5.0, 14.0), + ); let preview = canvas(DimensionPreview { ext1: !vals.dimse1, ext2: !vals.dimse2, dim1: !vals.dimsd1, dim2: !vals.dimsd2, tick: vals.dimtsz.trim().parse::().unwrap_or(0.0) > 0.0, - arrow_size: vals.dimasz.trim().parse::().unwrap_or(1.0).abs() * 5.0 + 5.0, + arrow_size, + arrow1, + arrow2, text_above: vals.dimtad.trim() != "0", basic: vals.dimgap.trim().starts_with('-'), text: preview_text, @@ -1231,3 +1347,25 @@ pub fn view_window<'a>( editor: right_panel.into(), }) } + +#[cfg(test)] +mod preview_arrow_tests { + // Preview must resolve each end independently: shared block when Dimsah + // is off, per-end blocks when on. + use super::preview_arrow_kinds; + use crate::scene::convert::tessellate::ArrowKind; + + #[test] + fn shared_block_feeds_both_ends_when_dimsah_off() { + let (first, second) = preview_arrow_kinds(false, "OPEN", "DOT", "NONE", 10.0); + assert!(matches!(first, ArrowKind::Open { .. })); + assert!(matches!(second, ArrowKind::Open { .. })); + } + + #[test] + fn per_end_blocks_feed_each_end_when_dimsah_on() { + let (first, second) = preview_arrow_kinds(true, "OPEN", "DOT", "NONE", 10.0); + assert!(matches!(first, ArrowKind::Dot { .. })); + assert!(matches!(second, ArrowKind::None)); + } +}