From bec9e63679f800f6c5d47d780b86c35a11a0492b Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 15 Oct 2026 22:18:34 +0300 Subject: [PATCH] feat(dimension): the standard arrowheads in every arrowhead picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dimension style, multileader style and Properties pickers listed every block of the drawing as an arrowhead, and nothing else. They now list the 19 standard arrowheads (Closed filled, Open, Dot, Architectural tick, Oblique, Right angle, Datum triangle, …), followed by any user arrow block the dimension or style already uses. Picking one creates its block (`_Open`, `_ArchTick`, …) with the geometry the reference program writes, so other programs draw the same arrow; ODA reads the saved file cleanly. `_Small` now draws as the reference's "Dot small blank", a hollow circle half the arrow size, instead of a small filled triangle. Also drops a duplicated "DGN Import" catalog entry left by #1810/#1812. (#1820, #1821) Co-Authored-By: Claude Opus 5.5 --- src/app/command_driver/clipboard.rs | 2 +- src/app/command_driver/mod.rs | 4 +- src/app/mod.rs | 4 +- src/app/properties.rs | 24 +--- src/app/update/command.rs | 21 +-- src/app/update/style.rs | 20 ++- src/app/view/modal.rs | 56 +++----- src/entities/arrowheads.rs | 210 ++++++++++++++++++++++++++++ src/entities/dim_override.rs | 9 +- src/entities/dimension.rs | 30 ++-- src/entities/mod.rs | 1 + src/locale_catalog.rs | 1 - src/scene/convert/tessellate.rs | 9 +- src/ui/style/dimstyle.rs | 4 +- 14 files changed, 288 insertions(+), 107 deletions(-) create mode 100644 src/entities/arrowheads.rs diff --git a/src/app/command_driver/clipboard.rs b/src/app/command_driver/clipboard.rs index 6346cfb8c..e7280b189 100644 --- a/src/app/command_driver/clipboard.rs +++ b/src/app/command_driver/clipboard.rs @@ -707,7 +707,7 @@ fn add_entity_clone_doc( /// Document-level verbatim block-definition recreate: the `&mut CadDocument` /// half of `Scene::define_block_raw` (no-op when the block exists). The /// trailing `bump_geometry` stays caller-side. -fn define_block_raw_doc( +pub(crate) fn define_block_raw_doc( doc: &mut codec::CadDocument, name: &str, base_point: codec::types::Vector3, diff --git a/src/app/command_driver/mod.rs b/src/app/command_driver/mod.rs index c5be52a97..a920cbee3 100644 --- a/src/app/command_driver/mod.rs +++ b/src/app/command_driver/mod.rs @@ -95,8 +95,8 @@ mod utilities; mod viewport; pub(crate) use clipboard::{ - copy_to_clipboard_kernel, paste_entities_kernel, remap_ext_subtree_reference, remap_object, - PasteSpace, + copy_to_clipboard_kernel, define_block_raw_doc, paste_entities_kernel, + remap_ext_subtree_reference, remap_object, PasteSpace, }; impl OpenCADStudio { diff --git a/src/app/mod.rs b/src/app/mod.rs index 9852313a3..782bc15e0 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -20,7 +20,9 @@ mod block_test; #[cfg(test)] mod block_test_tests; mod command_driver; -pub(crate) use command_driver::{copy_to_clipboard_kernel, paste_entities_kernel, PasteSpace}; +pub(crate) use command_driver::{ + copy_to_clipboard_kernel, define_block_raw_doc, paste_entities_kernel, PasteSpace, +}; pub(crate) mod commands; pub(crate) mod dim_viewport; #[cfg(test)] diff --git a/src/app/properties.rs b/src/app/properties.rs index c1ad8d851..ea34c0a23 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -554,14 +554,8 @@ impl OpenCADStudio { // Arrowheads are blocks; the closed-filled default has no // block, so seed the list with it and add the arrowhead // blocks (leading underscore) present in the drawing. - let arrow_names: Vec = std::iter::once("Closed filled".to_string()) - .chain( - doc.block_records - .iter() - .filter(|b| b.name.starts_with('_')) - .map(|b| b.name.clone()), - ) - .collect(); + // Filled in below from the current arrowhead. (#1820) + let arrow_names: Vec; let block_names: Vec = doc .block_records .iter() @@ -590,15 +584,11 @@ impl OpenCADStudio { .map(|s| s.name.clone()) }) .unwrap_or_else(|| "Standard".to_string()); - let cur_arrow = ml - .arrowhead_handle - .and_then(|h| { - doc.block_records - .iter() - .find(|b| b.handle == h) - .map(|b| b.name.clone()) - }) - .unwrap_or_else(|| "Closed filled".to_string()); + let cur_arrow = crate::entities::arrowheads::label( + doc, + ml.arrowhead_handle.unwrap_or(codec::Handle::NULL), + ); + arrow_names = crate::entities::arrowheads::options([cur_arrow.as_str()]); let cur_ltype = ml .line_type_handle .and_then(|h| { diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 787916d40..2d2a8ca8e 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -3184,6 +3184,16 @@ impl OpenCADStudio { // handle on a failed lookup; the arrowhead/linetype rows // take the resolved value directly (None = the default // "Closed filled" / "ByBlock" option). + // A standard arrowhead's block is created on first use. + let picked_arrow = (field == "arrowhead_handle") + .then(|| { + crate::entities::arrowheads::resolve( + &mut self.tabs[i].scene.document, + &value, + ) + }) + .flatten() + .filter(|h| !h.is_null()); let doc = &self.tabs[i].scene.document; let resolved_mleader_style = (field == "mleader_style") .then(|| { @@ -3204,16 +3214,7 @@ impl OpenCADStudio { .iter() .find(|s| s.name == value) .map(|s| s.handle), - "arrowhead_handle" => { - if value == "Closed filled" { - None - } else { - doc.block_records - .iter() - .find(|b| b.name == value) - .map(|b| b.handle) - } - } + "arrowhead_handle" => picked_arrow, "line_type_handle" => { if value == "ByBlock" { None diff --git a/src/app/update/style.rs b/src/app/update/style.rs index b3daf77d8..2ff2d6b09 100644 --- a/src/app/update/style.rs +++ b/src/app/update/style.rs @@ -976,6 +976,17 @@ impl OpenCADStudio { value: String, ) -> Task { let i = self.active_tab; + if field == "arrowhead_handle" { + let handle = crate::entities::arrowheads::resolve( + &mut self.tabs[i].scene.document, + &value, + ) + .filter(|h| !h.is_null()); + if let Some(s) = self.mleaderstyle_mut(i) { + s.arrowhead_handle = handle; + } + return Task::none(); + } let doc = &self.tabs[i].scene.document; let handle: Option = if value == "None" || value == "ByBlock" || value == "Closed filled" { @@ -1255,7 +1266,7 @@ impl OpenCADStudio { field, "dimltex_handle" | "dimltex1_handle" | "dimltex2_handle" ); - let doc = &self.tabs[i].scene.document; + let doc = &mut self.tabs[i].scene.document; let handle = if value == "Default" || value == "ByBlock" { codec::types::Handle::NULL } else if is_lt { @@ -1265,11 +1276,8 @@ impl OpenCADStudio { .map(|lt| lt.handle) .unwrap_or(codec::types::Handle::NULL) } else { - doc.block_records - .iter() - .find(|b| b.name == value) - .map(|b| b.handle) - .unwrap_or(codec::types::Handle::NULL) + // A standard arrowhead's block is created on first use. + crate::entities::arrowheads::resolve(doc, &value).unwrap_or(codec::types::Handle::NULL) }; // Staged: persists on Apply. if let Some(ds) = self.tabs[i].scene.document.dim_styles.get_mut(&name) { diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 97a8447e8..6bb78ae38 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -1164,8 +1164,12 @@ impl OpenCADStudio { let doc = &tab.scene.document; let mut block_opts: Vec = vec!["None".to_string()]; block_opts.extend(doc.block_records.iter().map(|b| b.name.clone())); - let mut arrow_opts: Vec = vec!["Closed filled".to_string()]; - arrow_opts.extend(doc.block_records.iter().map(|b| b.name.clone())); + let arrow_opts = crate::entities::arrowheads::options( + selected_style + .and_then(|s| s.arrowhead_handle) + .map(|h| crate::entities::arrowheads::label(doc, h)) + .as_deref(), + ); let mut lt_opts: Vec = vec!["ByBlock".to_string()]; lt_opts.extend(doc.line_types.iter().map(|lt| lt.name.clone())); let mut textstyle_opts: Vec = vec!["None".to_string()]; @@ -1212,14 +1216,10 @@ impl OpenCADStudio { match selected_style { Some(s) => ( opt_lt(s.line_type_handle), - s.arrowhead_handle - .and_then(|handle| { - doc.block_records - .iter() - .find(|record| record.handle == handle) - .map(|record| record.name.clone()) - }) - .unwrap_or_else(|| "Closed filled".to_string()), + crate::entities::arrowheads::label( + doc, + s.arrowhead_handle.unwrap_or(codec::types::Handle::NULL), + ), opt_ts(s.text_style_handle), opt_block(s.block_content_handle), ), @@ -1417,20 +1417,17 @@ impl OpenCADStudio { let doc = &tab.scene.document; // Dropdown options (names must match the records exactly so the // selection can be resolved back to a handle on the update side). - let mut block_opts: Vec = vec!["Default".to_string()]; - block_opts.extend( - doc.block_records + // The standard arrowheads, then the user arrow blocks the + // selected style uses; not every block of the drawing. (#1820) + let block_opts = crate::entities::arrowheads::options( + doc.dim_styles + .get(&self.dimstyle_selected) .iter() - .filter(|b| { - !b.is_layout() - && !b.is_model_space() - && !b.is_paper_space() - && !b.flags.is_xref - && !b.flags.is_xref_overlay - && !b.flags.is_external - && !b.name.starts_with('*') - }) - .map(|b| b.name.clone()), + .flat_map(|d| [d.dimblk, d.dimblk1, d.dimblk2, d.dimldrblk]) + .map(|h| crate::entities::arrowheads::label(doc, h)) + .collect::>() + .iter() + .map(String::as_str), ); let mut lt_opts: Vec = vec!["ByBlock".to_string()]; lt_opts.extend(doc.line_types.iter().map(|lt| lt.name.clone())); @@ -1445,17 +1442,8 @@ impl OpenCADStudio { .get(&self.ds_dimtxsty) .map(|style| style.height) .filter(|height| *height > 0.0); - let blk_name = |h: codec::types::Handle| -> String { - if h.is_null() { - "Default".to_string() - } else { - doc.block_records - .iter() - .find(|b| b.handle == h) - .map(|b| b.name.clone()) - .unwrap_or_else(|| "Default".to_string()) - } - }; + let blk_name = + |h: codec::types::Handle| crate::entities::arrowheads::label(doc, h); let lt_name = |h: codec::types::Handle| -> String { if h.is_null() { "ByBlock".to_string() diff --git a/src/entities/arrowheads.rs b/src/entities/arrowheads.rs new file mode 100644 index 000000000..222a1bc3a --- /dev/null +++ b/src/entities/arrowheads.rs @@ -0,0 +1,210 @@ +//! The standard arrowheads offered by the dimension, multileader and +//! properties pickers, and the blocks that carry them in a drawing. +//! +//! An arrowhead is a block record: "Closed filled" is the null handle, every +//! other standard one is a block named like `_Open`. The renderer draws the +//! standard names itself; the block still needs its geometry so other +//! programs draw the same arrow. It is created the first time it is picked. + +use codec::entities::{Circle, Line, LwPolyline, LwVertex, Solid}; +use codec::types::{Color, LineWeight, Vector3}; +use codec::{CadDocument, EntityType, Handle}; + +/// Name shown, block name (empty: closed filled, the null handle). +pub(crate) const STANDARD: &[(&str, &str)] = &[ + ("Closed filled", ""), + ("Closed blank", "_ClosedBlank"), + ("Closed", "_Closed"), + ("Dot", "_Dot"), + ("Architectural tick", "_ArchTick"), + ("Oblique", "_Oblique"), + ("Open", "_Open"), + ("Origin indicator", "_Origin"), + ("Origin indicator 2", "_Origin2"), + ("Right angle", "_Open90"), + ("Open 30", "_Open30"), + ("Dot small", "_DotSmall"), + ("Dot blank", "_DotBlank"), + ("Dot small blank", "_Small"), + ("Box blank", "_BoxBlank"), + ("Box filled", "_BoxFilled"), + ("Datum triangle", "_DatumBlank"), + ("Datum triangle filled", "_DatumFilled"), + ("None", "_None"), +]; + +fn standard_by_block(block: &str) -> Option<&'static str> { + STANDARD + .iter() + .find(|(_, name)| name.eq_ignore_ascii_case(block)) + .map(|(label, _)| *label) +} + +fn standard_by_label(label: &str) -> Option<&'static str> { + STANDARD + .iter() + .find(|(shown, _)| *shown == label) + .map(|(_, name)| *name) +} + +/// The block name a picker value stands for: a standard name's block, or +/// the value itself (a user arrow block). +pub(crate) fn block_name(value: &str) -> &str { + standard_by_label(value).unwrap_or(value) +} + +/// What a picker shows for the arrowhead block `name`. +pub(crate) fn label_for_block(name: &str) -> String { + if name.is_empty() { + return STANDARD[0].0.to_string(); + } + standard_by_block(name).map(str::to_string).unwrap_or_else(|| name.to_string()) +} + +/// What a picker shows for the arrowhead `handle`. +pub(crate) fn label(doc: &CadDocument, handle: Handle) -> String { + if handle.is_null() { + return STANDARD[0].0.to_string(); + } + match doc.block_records.iter().find(|b| b.handle == handle) { + Some(record) => standard_by_block(&record.name) + .map(str::to_string) + .unwrap_or_else(|| record.name.clone()), + None => STANDARD[0].0.to_string(), + } +} + +/// The picker's choices: the standard arrowheads, then each user arrow +/// block in `current` (the values shown now), not every block of the +/// drawing. +pub(crate) fn options<'a>(current: impl IntoIterator) -> Vec { + let mut out: Vec = STANDARD.iter().map(|(label, _)| label.to_string()).collect(); + for value in current { + if !value.is_empty() && !out.iter().any(|known| known == value) { + out.push(value.to_string()); + } + } + out +} + +/// The handle a picker value selects, creating a standard arrowhead's block +/// when the drawing does not have it yet. `None` for an unknown name. +pub(crate) fn resolve(doc: &mut CadDocument, value: &str) -> Option { + let value = value.trim(); + if value.is_empty() || value == "Default" { + return Some(Handle::NULL); + } + let name = block_name(value); + if name.is_empty() { + return Some(Handle::NULL); + } + if let Some(record) = doc.block_records.get(name) { + return Some(record.handle); + } + let entities = standard_geometry(name)?; + crate::app::define_block_raw_doc( + doc, + name, + Vector3::ZERO, + &entities, + ); + doc.block_records.get(name).map(|record| record.handle) +} + +/// A standard arrowhead block's contents at unit size, tip at the origin +/// pointing along +X, as the reference program writes them. +fn standard_geometry(name: &str) -> Option> { + let line = |x1: f64, y1: f64, x2: f64, y2: f64| { + EntityType::Line(Line::from_points( + Vector3::new(x1, y1, 0.0), + Vector3::new(x2, y2, 0.0), + )) + }; + let circle = |r: f64| EntityType::Circle(Circle::from_center_radius(Vector3::ZERO, r)); + // A filled dot: a closed two-arc polyline of constant width. + let dot = |half: f64| { + let mut p = LwPolyline::new(); + for x in [-half, half] { + let mut v = LwVertex::from_coords(x, 0.0); + v.bulge = 1.0; + p.vertices.push(v); + } + p.is_closed = true; + p.constant_width = 0.5; + EntityType::LwPolyline(p) + }; + let solid = |c: [[f64; 2]; 4]| { + let v = |i: usize| Vector3::new(c[i][0], c[i][1], 0.0); + EntityType::Solid(Solid::new(v(0), v(1), v(2), v(3))) + }; + let (w, s) = (1.0 / 6.0, 0.267949); + let d = 0.57735; + let mut entities = match name { + "_ClosedBlank" => vec![ + line(-1.0, w, 0.0, 0.0), + line(0.0, 0.0, -1.0, -w), + line(-1.0, w, -1.0, -w), + ], + "_Closed" => vec![ + line(-1.0, w, 0.0, 0.0), + line(0.0, 0.0, -1.0, -w), + line(-1.0, w, -1.0, -w), + line(0.0, 0.0, -1.0, 0.0), + ], + "_Dot" => vec![dot(0.25), line(-0.5, 0.0, -1.0, 0.0)], + "_ArchTick" => { + let mut p = LwPolyline::new(); + p.vertices.push(LwVertex::from_coords(-0.5, -0.5)); + p.vertices.push(LwVertex::from_coords(0.5, 0.5)); + p.constant_width = 0.15; + vec![EntityType::LwPolyline(p)] + } + "_Oblique" => vec![line(-0.5, -0.5, 0.5, 0.5)], + "_Open" => vec![ + line(-1.0, w, 0.0, 0.0), + line(-1.0, -w, 0.0, 0.0), + line(0.0, 0.0, -1.0, 0.0), + ], + "_Origin" => vec![circle(0.5), line(0.0, 0.0, -1.0, 0.0)], + "_Origin2" => vec![circle(0.5), circle(0.25), line(-0.5, 0.0, -1.0, 0.0)], + "_Open90" => vec![ + line(-0.5, 0.5, 0.0, 0.0), + line(0.0, 0.0, -0.5, -0.5), + line(0.0, 0.0, -1.0, 0.0), + ], + "_Open30" => vec![ + line(-1.0, s, 0.0, 0.0), + line(0.0, 0.0, -1.0, -s), + line(0.0, 0.0, -1.0, 0.0), + ], + "_DotSmall" => vec![dot(0.0625)], + "_DotBlank" => vec![circle(0.5), line(-0.5, 0.0, -1.0, 0.0)], + "_Small" => vec![circle(0.25)], + "_BoxBlank" => vec![ + line(-0.5, -0.5, 0.5, -0.5), + line(0.5, -0.5, 0.5, 0.5), + line(0.5, 0.5, -0.5, 0.5), + line(-0.5, 0.5, -0.5, -0.5), + line(-0.5, 0.0, -1.0, 0.0), + ], + "_BoxFilled" => vec![ + solid([[-0.5, 0.5], [0.5, 0.5], [-0.5, -0.5], [0.5, -0.5]]), + line(-0.5, 0.0, -1.0, 0.0), + ], + "_DatumBlank" => vec![ + line(0.0, d, -1.0, 0.0), + line(-1.0, 0.0, 0.0, -d), + line(0.0, -d, 0.0, d), + ], + "_DatumFilled" => vec![solid([[0.0, d], [-1.0, 0.0], [0.0, -d], [0.0, -d]])], + "_None" => Vec::new(), + _ => return None, + }; + // Arrowheads take the dimension's colour and lineweight. + for entity in &mut entities { + let common = entity.common_mut(); + common.color = Color::ByBlock; + common.line_weight = LineWeight::ByBlock; + } + Some(entities) +} diff --git a/src/entities/dim_override.rs b/src/entities/dim_override.rs index 75a306900..cfbfe5775 100644 --- a/src/entities/dim_override.rs +++ b/src/entities/dim_override.rs @@ -507,14 +507,7 @@ fn apply_property(doc: &mut CadDocument, handle: Handle, field: &str, value: &st if let Some(code) = handle_field { let resolved = match field { "dim_radial_arrow" | "dim_arrowhead_1" | "dim_arrowhead_2" => { - if trimmed == "Closed filled" { - Some(Handle::NULL) - } else { - doc.block_records - .iter() - .find(|record| record.name == trimmed) - .map(|record| record.handle) - } + crate::entities::arrowheads::resolve(doc, trimmed) } "dim_text_style" => doc .text_styles diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index 736576627..9b3eb9129 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -1953,15 +1953,6 @@ pub fn style_sections( } }; - let mut arrow_options: Vec = std::iter::once("Closed filled".to_string()) - .chain( - document - .block_records - .iter() - .map(|record| record.name.clone()) - .filter(|name| !name.is_empty()), - ) - .collect(); let mut linetype_options: Vec = document .line_types .iter() @@ -2001,11 +1992,9 @@ pub fn style_sections( } else { arrow_name(ov::DIMBLK2, s.dimblk2, &s.dimblk2_name) }; - for current in [&arrow_1, &arrow_2] { - if !arrow_options.contains(current) { - arrow_options.push(current.clone()); - } - } + // The standard arrowheads, then any user arrow block in use. (#1820) + let arrow_options = + crate::entities::arrowheads::options([&arrow_1, &arrow_2].map(String::as_str)); for current in [ linetype(ov::DIMLTYPE, s.dimltex_handle), linetype(ov::DIMLTEX1, s.dimltex1_handle), @@ -3402,16 +3391,13 @@ fn choice_value(selected: &str, options: &[&str]) -> crate::scene::model::object } } +/// The arrowhead picker's name for `handle`; `fallback` names a block the +/// drawing does not hold. fn block_name(document: &CadDocument, handle: codec::Handle, fallback: &str) -> String { - if handle.is_null() { - return "Closed filled".to_string(); + if !handle.is_null() && !document.block_records.iter().any(|b| b.handle == handle) { + return crate::entities::arrowheads::label_for_block(fallback); } - document - .block_records - .iter() - .find(|record| record.handle == handle) - .map(|record| record.name.clone()) - .unwrap_or_else(|| fallback.to_string()) + crate::entities::arrowheads::label(document, handle) } fn linetype_name(document: &CadDocument, handle: codec::Handle) -> String { diff --git a/src/entities/mod.rs b/src/entities/mod.rs index d23a3d9d1..a0dcdbb7c 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -1,4 +1,5 @@ pub mod arc; +pub(crate) mod arrowheads; pub mod attribute; pub mod circle; pub mod common; diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index d3fdd0e14..0e55bfe4e 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -6143,7 +6143,6 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Remote image \"{url}\" could not be fetched: the server returned HTTP {status}." => Some(("common", "remote-image-url-could-not-be-fetched-the-server-returned-http-status")), "Remote image \"{url}\" was not loaded: it exceeds the 64 MB remote-image limit." => Some(("common", "remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit")), "Remote image \"{url}\" could not be fetched: the request timed out." => Some(("common", "remote-image-url-could-not-be-fetched-the-request-timed-out")), - "DGN Import" => Some(("import-panel", "dgn-import")), "Recognize SHX Text" => Some(("import-panel", "recognize-shx")), "Recognition Settings" => Some(("import-panel", "recognition-settings")), "Combine Text" => Some(("import-panel", "combine-text")), diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 56dbe4255..a82a2d940 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -2803,10 +2803,11 @@ fn builtin_arrow_from_block_name(name: &str, dimasz: f32) -> Option { filled: false, size_mul: 1.0, }), - "SMALL" => Some(ArrowKind::Triangle { - size: dimasz, - filled: true, - size_mul: 0.5, + // `_Small` is the reference's "Dot small blank": a circle half the + // arrow size across. + "SMALL" => Some(ArrowKind::Dot { + size: dimasz * 0.5, + filled: false, }), "OPEN" => Some(ArrowKind::Open { size: dimasz, diff --git a/src/ui/style/dimstyle.rs b/src/ui/style/dimstyle.rs index 9d9a9301a..8c362826a 100644 --- a/src/ui/style/dimstyle.rs +++ b/src/ui/style/dimstyle.rs @@ -188,7 +188,9 @@ fn preview_arrow_kinds( dimblk2: &str, dimasz: f32, ) -> (ArrowKind, ArrowKind) { - let kind = |name: &str| arrow_from_block_name(Some(name), dimasz); + let kind = |name: &str| { + arrow_from_block_name(Some(crate::entities::arrowheads::block_name(name)), dimasz) + }; if dimsah { (kind(dimblk1), kind(dimblk2)) } else {