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 {