feat(dimension): the standard arrowheads in every arrowhead picker

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 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-15 22:18:35 +03:00
1 parent 4143a01ff4
commit bec9e63679
14 files changed
+288 -107

No files matched your search

+210
View File
@@ -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<Item = &'a str>) -> Vec<String> {
let mut out: Vec<String> = 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<Handle> {
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<Vec<EntityType>> {
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)
}
+1 -8
View File
@@ -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
+8 -22
View File
@@ -1953,15 +1953,6 @@ pub fn style_sections(
}
};
let mut arrow_options: Vec<String> = 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<String> = 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 {
+1
View File
@@ -1,4 +1,5 @@
pub mod arc;
pub(crate) mod arrowheads;
pub mod attribute;
pub mod circle;
pub mod common;