fix(dimensions): render custom arrow blocks

Refs #612
This commit is contained in:
Hakan Seven committed 2026-08-02 16:06:37 +03:00
1 parent c3df935905
commit 20c6d419e2
6 files changed
+393 -101

No files matched your search

+14 -1
View File
@@ -611,7 +611,20 @@ impl OpenCADStudio {
// Dropdown options (names must match the records exactly so the // Dropdown options (names must match the records exactly so the
// selection can be resolved back to a handle on the update side). // selection can be resolved back to a handle on the update side).
let mut block_opts: Vec<String> = vec!["Default".to_string()]; let mut block_opts: Vec<String> = vec!["Default".to_string()];
block_opts.extend(doc.block_records.iter().map(|b| b.name.clone())); block_opts.extend(
doc.block_records
.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()),
);
let mut lt_opts: Vec<String> = vec!["ByBlock".to_string()]; let mut lt_opts: Vec<String> = vec!["ByBlock".to_string()];
lt_opts.extend(doc.line_types.iter().map(|lt| lt.name.clone())); lt_opts.extend(doc.line_types.iter().map(|lt| lt.name.clone()));
let blk_name = |h: acadrust::types::Handle| -> String { let blk_name = |h: acadrust::types::Handle| -> String {
+24 -3
View File
@@ -1388,6 +1388,27 @@ fn apply_dimension_breaks(
*lines = output; *lines = output;
} }
pub(crate) fn uses_custom_arrow_blocks(document: &CadDocument, dim: &Dimension) -> bool {
let style_name = &dim.base().style_name;
let Some(style) = document.dim_styles.iter().find(|style| {
style.name.eq_ignore_ascii_case(style_name)
|| (style_name.trim().is_empty() && style.name.eq_ignore_ascii_case("Standard"))
}) else {
return false;
};
if style.dimtsz > 1e-9 {
return false;
}
let is_custom = |handle| {
crate::scene::convert::tessellate::arrow_block_is_custom(document, handle)
};
if style.dimsah {
is_custom(style.dimblk1) || is_custom(style.dimblk2)
} else {
is_custom(style.dimblk)
}
}
pub trait DimensionTess { pub trait DimensionTess {
fn tessellate( fn tessellate(
&self, &self,
@@ -1528,7 +1549,7 @@ fn tessellate_dimension_inner(
let t = ArrowKind::Tick { let t = ArrowKind::Tick {
size: (dimtsz_raw as f32).max(0.001), size: (dimtsz_raw as f32).max(0.001),
}; };
(t, t) (t.clone(), t)
} else if let Some(s) = style { } else if let Some(s) = style {
if dimsah { if dimsah {
( (
@@ -1537,7 +1558,7 @@ fn tessellate_dimension_inner(
) )
} else { } else {
let a = arrow_from_block(document, s.dimblk, dimasz); let a = arrow_from_block(document, s.dimblk, dimasz);
(a, a) (a.clone(), a)
} }
} else { } else {
let a = ArrowKind::Triangle { let a = ArrowKind::Triangle {
@@ -1545,7 +1566,7 @@ fn tessellate_dimension_inner(
filled: true, filled: true,
size_mul: 1.0, size_mul: 1.0,
}; };
(a, a) (a.clone(), a)
}; };
// Text box (local space) so the dim line can be broken where the text // Text box (local space) so the dim line can be broken where the text
+48 -18
View File
@@ -360,13 +360,13 @@ fn dim_terminator(
s.common.handle = Handle::NULL; s.common.handle = Handle::NULL;
out.push(EntityType::Solid(s)); out.push(EntityType::Solid(s));
}; };
match *arrow { match arrow {
A::None => {} A::None => {}
A::Triangle { size, filled, size_mul } => { A::Triangle { size, filled, size_mul } => {
let size = (size * size_mul) as f64; let size = (*size * *size_mul) as f64;
let hw = size / 6.0; let hw = size / 6.0;
let (l, r) = (pt(size, hw), pt(size, -hw)); let (l, r) = (pt(size, hw), pt(size, -hw));
if filled { if *filled {
tri(tip, l, r, &mut out); tri(tip, l, r, &mut out);
} else { } else {
out.push(dim_seg(tip, l, common)); out.push(dim_seg(tip, l, common));
@@ -375,7 +375,7 @@ fn dim_terminator(
} }
} }
A::Tick { size } => { A::Tick { size } => {
let s = size as f64; let s = *size as f64;
let (ox, oy) = (dx + px, dy + py); let (ox, oy) = (dx + px, dy + py);
let m = (ox * ox + oy * oy).sqrt().max(1e-9); let m = (ox * ox + oy * oy).sqrt().max(1e-9);
let (ox, oy) = (ox / m * s, oy / m * s); let (ox, oy) = (ox / m * s, oy / m * s);
@@ -386,42 +386,72 @@ fn dim_terminator(
)); ));
} }
A::Open { size, half_angle } => { A::Open { size, half_angle } => {
let size = size as f64; let size = *size as f64;
let hw = size * (half_angle as f64).tan(); let hw = size * (*half_angle as f64).tan();
out.push(dim_seg(tip, pt(size, hw), common)); out.push(dim_seg(tip, pt(size, hw), common));
out.push(dim_seg(tip, pt(size, -hw), common)); out.push(dim_seg(tip, pt(size, -hw), common));
} }
A::Dot { size, filled } => { A::Dot { size, filled } => {
let r = size as f64 * 0.5; let r = *size as f64 * 0.5;
terminator_circle(tip, r, filled, common, &mut out); terminator_circle(tip, r, *filled, common, &mut out);
} }
A::Origin { size } => { A::Origin { size } => {
terminator_circle(tip, size as f64 * 0.25, true, common, &mut out); terminator_circle(tip, *size as f64 * 0.25, true, common, &mut out);
let half = size as f64 * 0.5; let half = *size as f64 * 0.5;
out.push(dim_seg(pt(0.0, -half), pt(0.0, half), common)); out.push(dim_seg(pt(0.0, -half), pt(0.0, half), common));
} }
A::Box_ { size, filled } => { A::Box_ { size, filled } => {
let h = size as f64 * 0.5; let h = *size as f64 * 0.5;
let (p1, p2, p3, p4) = (pt(-h, -h), pt(h, -h), pt(h, h), pt(-h, h)); let (p1, p2, p3, p4) = (pt(-h, -h), pt(h, -h), pt(h, h), pt(-h, h));
out.push(dim_seg(p1, p2, common)); out.push(dim_seg(p1, p2, common));
out.push(dim_seg(p2, p3, common)); out.push(dim_seg(p2, p3, common));
out.push(dim_seg(p3, p4, common)); out.push(dim_seg(p3, p4, common));
out.push(dim_seg(p4, p1, common)); out.push(dim_seg(p4, p1, common));
if filled { if *filled {
tri(p1, p2, p3, &mut out); tri(p1, p2, p3, &mut out);
tri(p1, p3, p4, &mut out); tri(p1, p3, p4, &mut out);
} }
} }
A::Datum { size, filled } => { A::Datum { size, filled } => {
let half = size as f64 * 0.5; let half = *size as f64 * 0.5;
let (ba, bb, apex) = (pt(0.0, half), pt(0.0, -half), pt(size as f64, 0.0)); let (ba, bb, apex) = (pt(0.0, half), pt(0.0, -half), pt(*size as f64, 0.0));
out.push(dim_seg(ba, apex, common)); out.push(dim_seg(ba, apex, common));
out.push(dim_seg(apex, bb, common)); out.push(dim_seg(apex, bb, common));
out.push(dim_seg(bb, ba, common)); out.push(dim_seg(bb, ba, common));
if filled { if *filled {
tri(ba, apex, bb, &mut out); tri(ba, apex, bb, &mut out);
} }
} }
A::Custom { size, lines, fill } => {
let custom_pt = |point: &[f32; 3]| {
let mut placed = pt(
-(point[0] as f64) * *size as f64,
-(point[1] as f64) * *size as f64,
);
placed.z += point[2] as f64 * *size as f64;
placed
};
let mut previous = None;
for point in lines {
if point[0].is_nan() {
previous = None;
continue;
}
let current = custom_pt(point);
if let Some(previous) = previous {
out.push(dim_seg(previous, current, common));
}
previous = Some(current);
}
for triangle in fill.chunks_exact(3) {
tri(
custom_pt(&triangle[0]),
custom_pt(&triangle[1]),
custom_pt(&triangle[2]),
&mut out,
);
}
}
} }
out out
} }
@@ -576,17 +606,17 @@ fn dim_metrics(dim: &Dimension, doc: &CadDocument) -> DimMetrics {
// else closed-filled. // else closed-filled.
let (arrow1, arrow2) = if dimtsz > 1e-9 { let (arrow1, arrow2) = if dimtsz > 1e-9 {
let t = ArrowKind::Tick { size: dimtsz as f32 }; let t = ArrowKind::Tick { size: dimtsz as f32 };
(t, t) (t.clone(), t)
} else if let Some(s) = style { } else if let Some(s) = style {
if s.dimsah { if s.dimsah {
(arrow_from_block(doc, s.dimblk1, asz), arrow_from_block(doc, s.dimblk2, asz)) (arrow_from_block(doc, s.dimblk1, asz), arrow_from_block(doc, s.dimblk2, asz))
} else { } else {
let a = arrow_from_block(doc, s.dimblk, asz); let a = arrow_from_block(doc, s.dimblk, asz);
(a, a) (a.clone(), a)
} }
} else { } else {
let a = ArrowKind::Triangle { size: asz, filled: true, size_mul: 1.0 }; let a = ArrowKind::Triangle { size: asz, filled: true, size_mul: 1.0 };
(a, a) (a.clone(), a)
}; };
DimMetrics { DimMetrics {
dimasz, dimasz,
+4 -1
View File
@@ -434,7 +434,10 @@ fn build_defn(
// drew nothing. Expand the `*D` block's entities here as block-local // drew nothing. Expand the `*D` block's entities here as block-local
// subs so they transform with the parent insert. (Empty block_name // subs so they transform with the parent insert. (Empty block_name
// — a non-baked dimension — falls through to the default arm.) // — a non-baked dimension — falls through to the default arm.)
EntityType::Dimension(dim) if !dim.base().block_name.trim().is_empty() => { EntityType::Dimension(dim)
if !dim.base().block_name.trim().is_empty()
&& !crate::entities::dimension::uses_custom_arrow_blocks(doc, dim) =>
{
let dblk = doc let dblk = doc
.block_records .block_records
.iter() .iter()
+7 -5
View File
@@ -738,15 +738,17 @@ pub(crate) fn tessellate_entity(
// ── Dimension baked-block fast path ───────────────────────────────────── // ── Dimension baked-block fast path ─────────────────────────────────────
// //
// AutoCAD bakes each dimension's final geometry (extension lines, dim // A saved dimension may carry final geometry (extension lines, dim line,
// line, arrows, text MText) into a per-instance block — usually // arrows and text) in a per-instance block — usually
// `*D<n>`, but custom names like `DIMBLOCK###-4NP` also occur. When the // `*D<n>`, but custom names like `DIMBLOCK###-4NP` also occur. When the
// block exists we render its contents through `tessellate_entity` so // block exists we render its contents through `tessellate_entity` so
// sub-Text/MText get the standard baseline/greek/full LOD ladder, and // sub-Text/MText get the standard baseline/greek/full LOD ladder, and
// DIMTXT × DIMSCALE isn't re-applied on already-baked geometry. // DIMTXT × DIMSCALE isn't re-applied on already-baked geometry.
if let EntityType::Dimension(dim) = e { if let EntityType::Dimension(dim) = e {
let block_name = &dim.base().block_name; let block_name = &dim.base().block_name;
if !block_name.trim().is_empty() { if !block_name.trim().is_empty()
&& !crate::entities::dimension::uses_custom_arrow_blocks(document, dim)
{
if let Some(br) = document if let Some(br) = document
.block_records .block_records
.iter() .iter()
@@ -773,8 +775,8 @@ pub(crate) fn tessellate_entity(
continue; continue;
}; };
// A dimension's definition points are baked into the // A dimension's definition points are baked into the
// block as POINTs on the Defpoints layer. AutoCAD never // block as POINTs on the Defpoints layer. They are grip
// draws them as PDMODE glyphs — they're grip markers, not // markers rather than PDMODE display geometry, so
// geometry — so rendering them adds a stray tick at each // geometry — so rendering them adds a stray tick at each
// measured point that makes the extension lines look like // measured point that makes the extension lines look like
// they run past the geometry. Skip them. // they run past the geometry. Skip them.
+296 -73
View File
@@ -1503,7 +1503,7 @@ pub fn tessellate(
#[derive(Clone, Copy)] #[derive(Clone)]
pub(crate) enum ArrowKind { pub(crate) enum ArrowKind {
None, None,
Triangle { size: f32, filled: bool, size_mul: f32 }, Triangle { size: f32, filled: bool, size_mul: f32 },
@@ -1513,6 +1513,11 @@ pub(crate) enum ArrowKind {
Origin { size: f32 }, Origin { size: f32 },
Box_ { size: f32, filled: bool }, Box_ { size: f32, filled: bool },
Datum { size: f32, filled: bool }, Datum { size: f32, filled: bool },
Custom {
size: f32,
lines: Vec<[f32; 3]>,
fill: Vec<[f32; 3]>,
},
} }
pub(crate) fn arrow_from_block( pub(crate) fn arrow_from_block(
@@ -1520,100 +1525,288 @@ pub(crate) fn arrow_from_block(
handle: acadrust::types::Handle, handle: acadrust::types::Handle,
dimasz: f32, dimasz: f32,
) -> ArrowKind { ) -> ArrowKind {
let name = if handle.is_null() { if handle.is_null() {
None return arrow_from_block_name(None, dimasz);
} else { }
doc.block_records let Some(record) = doc.block_records.iter().find(|b| b.handle == handle) else {
.iter() return arrow_from_block_name(None, dimasz);
.find(|b| b.handle == handle)
.map(|b| b.name.as_str())
}; };
arrow_from_block_name(name, dimasz) if let Some(arrow) = builtin_arrow_from_block_name(&record.name, dimasz) {
return arrow;
}
custom_arrow_from_block(doc, record, dimasz)
.unwrap_or_else(|| arrow_from_block_name(None, dimasz))
}
pub(crate) fn arrow_block_is_custom(
doc: &CadDocument,
handle: acadrust::types::Handle,
) -> bool {
if handle.is_null() {
return false;
}
doc.block_records
.iter()
.find(|record| record.handle == handle)
.is_some_and(|record| {
!record.entity_handles.is_empty()
&& !record.is_layout()
&& !record.is_model_space()
&& !record.is_paper_space()
&& !record.flags.is_xref
&& !record.flags.is_xref_overlay
&& !record.flags.is_external
&& builtin_arrow_from_block_name(&record.name, 1.0).is_none()
})
} }
fn arrow_from_block_name(name: Option<&str>, dimasz: f32) -> ArrowKind { fn arrow_from_block_name(name: Option<&str>, dimasz: f32) -> ArrowKind {
// AutoCAD's standard arrow blocks are prefixed with "_" (e.g. "_OPEN"). name.and_then(|name| builtin_arrow_from_block_name(name, dimasz))
// Strip the prefix, upper-case, and switch on canonical names. Unknown .unwrap_or(ArrowKind::Triangle {
// / missing names default to ClosedFilled.
let n = name
.map(|s| s.trim().trim_start_matches('_').to_ascii_uppercase())
.unwrap_or_default();
match n.as_str() {
"" | "CLOSEDFILLED" => ArrowKind::Triangle {
size: dimasz, size: dimasz,
filled: true, filled: true,
size_mul: 1.0, size_mul: 1.0,
}, })
"CLOSED" | "CLOSEDBLANK" => ArrowKind::Triangle { }
fn builtin_arrow_from_block_name(name: &str, dimasz: f32) -> Option<ArrowKind> {
// Built-in arrow block names may carry a leading underscore. Normalize it
// before matching the canonical names.
let n = name
.trim()
.trim_start_matches('_')
.to_ascii_uppercase();
match n.as_str() {
"" | "CLOSEDFILLED" => Some(ArrowKind::Triangle {
size: dimasz,
filled: true,
size_mul: 1.0,
}),
"CLOSED" | "CLOSEDBLANK" => Some(ArrowKind::Triangle {
size: dimasz, size: dimasz,
filled: false, filled: false,
size_mul: 1.0, size_mul: 1.0,
}, }),
"SMALL" => ArrowKind::Triangle { "SMALL" => Some(ArrowKind::Triangle {
size: dimasz, size: dimasz,
filled: true, filled: true,
size_mul: 0.5, size_mul: 0.5,
}, }),
"OPEN" => ArrowKind::Open { "OPEN" => Some(ArrowKind::Open {
size: dimasz, size: dimasz,
half_angle: 9.5_f32.to_radians(), half_angle: 9.5_f32.to_radians(),
}, }),
"OPEN30" => ArrowKind::Open { "OPEN30" => Some(ArrowKind::Open {
size: dimasz, size: dimasz,
half_angle: 15.0_f32.to_radians(), half_angle: 15.0_f32.to_radians(),
}, }),
"OPEN90" => ArrowKind::Open { "OPEN90" => Some(ArrowKind::Open {
size: dimasz, size: dimasz,
half_angle: 45.0_f32.to_radians(), half_angle: 45.0_f32.to_radians(),
}, }),
"DOT" => ArrowKind::Dot { "DOT" => Some(ArrowKind::Dot {
size: dimasz, size: dimasz,
filled: true, filled: true,
}, }),
"DOTSMALL" => ArrowKind::Dot { "DOTSMALL" => Some(ArrowKind::Dot {
size: dimasz * 0.5, size: dimasz * 0.5,
filled: true, filled: true,
}, }),
"DOTBLANK" => ArrowKind::Dot { "DOTBLANK" => Some(ArrowKind::Dot {
size: dimasz, size: dimasz,
filled: false, filled: false,
}, }),
"DOTSMALLBLANK" => ArrowKind::Dot { "DOTSMALLBLANK" => Some(ArrowKind::Dot {
size: dimasz * 0.5, size: dimasz * 0.5,
filled: false, filled: false,
}, }),
"ORIGIN" | "ORIGIN2" | "ORIGININDICATOR" | "ORIGININDICATOR2" => { "ORIGIN" | "ORIGIN2" | "ORIGININDICATOR" | "ORIGININDICATOR2" => {
ArrowKind::Origin { size: dimasz } Some(ArrowKind::Origin { size: dimasz })
} }
// `ArrowKind::Tick` draws the stroke `size` to either side of the tip // `ArrowKind::Tick` draws the stroke `size` to either side of the tip
// (total 2·size — its `size` is a half-length, matching DIMTSZ). As an // (total 2·size — its `size` is a half-length, matching DIMTSZ). For
// arrowhead block, though, DIMASZ is the stroke's *full* length like // a block-selected tick DIMASZ is the full stroke length, so halve it.
// every other arrowhead here, so pass half of it — otherwise the "OBLIQUE" | "ARCHTICK" => Some(ArrowKind::Tick { size: dimasz * 0.5 }),
// oblique tick renders twice the intended DIMASZ. "BOXFILLED" => Some(ArrowKind::Box_ {
"OBLIQUE" | "ARCHTICK" => ArrowKind::Tick { size: dimasz * 0.5 },
"BOXFILLED" => ArrowKind::Box_ {
size: dimasz, size: dimasz,
filled: true, filled: true,
}, }),
"BOXBLANK" | "BOX" => ArrowKind::Box_ { "BOXBLANK" | "BOX" => Some(ArrowKind::Box_ {
size: dimasz, size: dimasz,
filled: false, filled: false,
}, }),
"DATUMFILLED" | "DATUMTRIANGLEFILLED" => ArrowKind::Datum { "DATUMFILLED" | "DATUMTRIANGLEFILLED" => Some(ArrowKind::Datum {
size: dimasz, size: dimasz,
filled: true, filled: true,
}, }),
"DATUMBLANK" | "DATUMTRIANGLE" => ArrowKind::Datum { "DATUMBLANK" | "DATUMTRIANGLE" => Some(ArrowKind::Datum {
size: dimasz, size: dimasz,
filled: false, filled: false,
}, }),
"NONE" => ArrowKind::None, "NONE" => Some(ArrowKind::None),
// INTEGRAL and other complex glyphs aren't reproduced here; fall through. _ => None,
_ => ArrowKind::Triangle { }
}
fn custom_arrow_from_block(
doc: &CadDocument,
record: &acadrust::tables::BlockRecord,
dimasz: f32,
) -> Option<ArrowKind> {
if record.is_layout()
|| record.is_model_space()
|| record.is_paper_space()
|| record.flags.is_xref
|| record.flags.is_xref_overlay
|| record.flags.is_external
{
return None;
}
let base = block_base_point(doc, record);
let mut lines = Vec::new();
let mut fill = Vec::new();
let mut stack = vec![record.name.clone()];
for &entity_handle in &record.entity_handles {
let Some(entity) = doc.get_entity(entity_handle) else {
continue;
};
collect_custom_arrow_entity(doc, entity, base, &mut lines, &mut fill, &mut stack, 0);
}
if lines.is_empty() && fill.is_empty() {
None
} else {
Some(ArrowKind::Custom {
size: dimasz, size: dimasz,
filled: true, lines,
size_mul: 1.0, fill,
}, })
}
}
fn block_base_point(
doc: &CadDocument,
record: &acadrust::tables::BlockRecord,
) -> acadrust::types::Vector3 {
doc.get_entity(record.block_entity_handle)
.and_then(|entity| match entity {
EntityType::Block(block) => Some(block.base_point),
_ => None,
})
.unwrap_or(acadrust::types::Vector3::ZERO)
}
fn collect_custom_arrow_entity(
doc: &CadDocument,
entity: &EntityType,
root_base: acadrust::types::Vector3,
lines: &mut Vec<[f32; 3]>,
fill: &mut Vec<[f32; 3]>,
stack: &mut Vec<String>,
depth: usize,
) {
if depth >= 32 || entity.common().invisible {
return;
}
match entity {
EntityType::Block(_)
| EntityType::BlockEnd(_)
| EntityType::AttributeDefinition(_)
| EntityType::Dimension(_)
| EntityType::Leader(_)
| EntityType::MultiLeader(_) => return,
EntityType::Insert(insert) => {
if stack.iter().any(|name| name.eq_ignore_ascii_case(&insert.block_name)) {
return;
}
let Some(record) = doc.block_records.get(&insert.block_name) else {
return;
};
let nested_base = block_base_point(doc, record);
let transform = insert.get_transform();
let transformed_zero = transform.apply(acadrust::types::Vector3::ZERO);
let transformed_base = transform.apply(nested_base);
let correction = transformed_zero - transformed_base;
stack.push(insert.block_name.clone());
for mut child in insert.explode_from_document(doc) {
child.as_entity_mut().translate(correction);
collect_custom_arrow_entity(
doc,
&child,
root_base,
lines,
fill,
stack,
depth + 1,
);
}
stack.pop();
return;
}
_ => {}
}
let wires = tessellate(
doc,
entity.common().handle,
entity,
false,
[1.0; 4],
0.0,
[0.0; 8],
1.0,
1.0,
None,
None,
[0.0, 0.0, 0.0, 1.0],
true,
);
for wire in wires {
append_custom_wire_points(&wire.points, &wire.points_low, root_base, lines);
append_custom_fill_points(&wire.fill_tris, &wire.fill_tris_low, root_base, fill);
}
}
fn append_custom_wire_points(
points: &[[f32; 3]],
points_low: &[[f32; 3]],
base: acadrust::types::Vector3,
out: &mut Vec<[f32; 3]>,
) {
if points.is_empty() {
return;
}
if !out.is_empty() && !out.last().is_some_and(|p| p[0].is_nan()) {
out.push([f32::NAN; 3]);
}
for (index, point) in points.iter().enumerate() {
if point[0].is_nan() {
if !out.last().is_some_and(|p| p[0].is_nan()) {
out.push([f32::NAN; 3]);
}
continue;
}
let low = points_low.get(index).copied().unwrap_or([0.0; 3]);
out.push([
(point[0] as f64 + low[0] as f64 - base.x) as f32,
(point[1] as f64 + low[1] as f64 - base.y) as f32,
(point[2] as f64 + low[2] as f64 - base.z) as f32,
]);
}
}
fn append_custom_fill_points(
points: &[[f32; 3]],
points_low: &[[f32; 3]],
base: acadrust::types::Vector3,
out: &mut Vec<[f32; 3]>,
) {
for (index, point) in points.iter().enumerate() {
let low = points_low.get(index).copied().unwrap_or([0.0; 3]);
out.push([
(point[0] as f64 + low[0] as f64 - base.x) as f32,
(point[1] as f64 + low[1] as f64 - base.y) as f32,
(point[2] as f64 + low[2] as f64 - base.z) as f32,
]);
} }
} }
@@ -1775,43 +1968,43 @@ pub(crate) fn push_tri(out: &mut Vec<[f32; 3]>, a: Vec3, b: Vec3, c: Vec3) {
pub(crate) fn append_arrow(g: &mut DimGeom, tip: Vec3, dir: Vec3, arrow: &ArrowKind) { pub(crate) fn append_arrow(g: &mut DimGeom, tip: Vec3, dir: Vec3, arrow: &ArrowKind) {
let dir = normalized_or(dir, Vec3::X); let dir = normalized_or(dir, Vec3::X);
let perp = Vec3::new(-dir.y, dir.x, 0.0); let perp = Vec3::new(-dir.y, dir.x, 0.0);
match *arrow { match arrow {
ArrowKind::None => {} ArrowKind::None => {}
ArrowKind::Triangle { ArrowKind::Triangle {
size, size,
filled, filled,
size_mul, size_mul,
} => { } => {
let size = size * size_mul; let size = *size * *size_mul;
let base = tip + dir * size; let base = tip + dir * size;
// ~1:6 length:half-width ratio (≈9.5° half-angle) matches // ~1:6 length:half-width ratio (≈9.5° half-angle) matches
// AutoCAD's standard ClosedFilled block. // the standard closed-filled block.
let half_w = size / 6.0; let half_w = size / 6.0;
let left = base + perp * half_w; let left = base + perp * half_w;
let right = base - perp * half_w; let right = base - perp * half_w;
add_segment(&mut g.dim_lines, tip, left); add_segment(&mut g.dim_lines, tip, left);
add_segment(&mut g.dim_lines, left, right); add_segment(&mut g.dim_lines, left, right);
add_segment(&mut g.dim_lines, right, tip); add_segment(&mut g.dim_lines, right, tip);
if filled { if *filled {
push_tri(&mut g.arrow_fill, tip, left, right); push_tri(&mut g.arrow_fill, tip, left, right);
} }
} }
ArrowKind::Tick { size } => { ArrowKind::Tick { size } => {
// 45° oblique tick crossing the dim line at the tip; `size` is // 45° oblique tick crossing the dim line at the tip; `size` is
// the half-length (matches AutoCAD's DIMTSZ semantics). // the half-length used by DIMTSZ.
let off = (dir + perp).normalize_or_zero() * size; let off = (dir + perp).normalize_or_zero() * *size;
add_segment(&mut g.dim_lines, tip - off, tip + off); add_segment(&mut g.dim_lines, tip - off, tip + off);
} }
ArrowKind::Open { size, half_angle } => { ArrowKind::Open { size, half_angle } => {
let base = tip + dir * size; let base = tip + dir * *size;
let half_w = size * half_angle.tan(); let half_w = *size * half_angle.tan();
let left = base + perp * half_w; let left = base + perp * half_w;
let right = base - perp * half_w; let right = base - perp * half_w;
add_segment(&mut g.dim_lines, tip, left); add_segment(&mut g.dim_lines, tip, left);
add_segment(&mut g.dim_lines, tip, right); add_segment(&mut g.dim_lines, tip, right);
} }
ArrowKind::Dot { size, filled } => { ArrowKind::Dot { size, filled } => {
let r = size * 0.5; let r = *size * 0.5;
const N: usize = 16; const N: usize = 16;
let mut ring: Vec<Vec3> = Vec::with_capacity(N + 1); let mut ring: Vec<Vec3> = Vec::with_capacity(N + 1);
for i in 0..=N { for i in 0..=N {
@@ -1819,7 +2012,7 @@ pub(crate) fn append_arrow(g: &mut DimGeom, tip: Vec3, dir: Vec3, arrow: &ArrowK
ring.push(tip + Vec3::new(a.cos() * r, a.sin() * r, 0.0)); ring.push(tip + Vec3::new(a.cos() * r, a.sin() * r, 0.0));
} }
add_polyline(&mut g.dim_lines, &ring); add_polyline(&mut g.dim_lines, &ring);
if filled { if *filled {
for i in 0..N { for i in 0..N {
push_tri(&mut g.arrow_fill, tip, ring[i], ring[i + 1]); push_tri(&mut g.arrow_fill, tip, ring[i], ring[i + 1]);
} }
@@ -1828,7 +2021,7 @@ pub(crate) fn append_arrow(g: &mut DimGeom, tip: Vec3, dir: Vec3, arrow: &ArrowK
ArrowKind::Origin { size } => { ArrowKind::Origin { size } => {
// Small filled dot at the tip with a perpendicular tick crossing // Small filled dot at the tip with a perpendicular tick crossing
// the dim line — matches "_ORIGIN" / "_ORIGIN2" blocks. // the dim line — matches "_ORIGIN" / "_ORIGIN2" blocks.
let r = size * 0.25; let r = *size * 0.25;
const N: usize = 12; const N: usize = 12;
let mut ring: Vec<Vec3> = Vec::with_capacity(N + 1); let mut ring: Vec<Vec3> = Vec::with_capacity(N + 1);
for i in 0..=N { for i in 0..=N {
@@ -1839,11 +2032,11 @@ pub(crate) fn append_arrow(g: &mut DimGeom, tip: Vec3, dir: Vec3, arrow: &ArrowK
for i in 0..N { for i in 0..N {
push_tri(&mut g.arrow_fill, tip, ring[i], ring[i + 1]); push_tri(&mut g.arrow_fill, tip, ring[i], ring[i + 1]);
} }
let half = size * 0.5; let half = *size * 0.5;
add_segment(&mut g.dim_lines, tip - perp * half, tip + perp * half); add_segment(&mut g.dim_lines, tip - perp * half, tip + perp * half);
} }
ArrowKind::Box_ { size, filled } => { ArrowKind::Box_ { size, filled } => {
let half = size * 0.5; let half = *size * 0.5;
let p1 = tip - dir * half - perp * half; let p1 = tip - dir * half - perp * half;
let p2 = tip + dir * half - perp * half; let p2 = tip + dir * half - perp * half;
let p3 = tip + dir * half + perp * half; let p3 = tip + dir * half + perp * half;
@@ -1852,7 +2045,7 @@ pub(crate) fn append_arrow(g: &mut DimGeom, tip: Vec3, dir: Vec3, arrow: &ArrowK
add_segment(&mut g.dim_lines, p2, p3); add_segment(&mut g.dim_lines, p2, p3);
add_segment(&mut g.dim_lines, p3, p4); add_segment(&mut g.dim_lines, p3, p4);
add_segment(&mut g.dim_lines, p4, p1); add_segment(&mut g.dim_lines, p4, p1);
if filled { if *filled {
push_tri(&mut g.arrow_fill, p1, p2, p3); push_tri(&mut g.arrow_fill, p1, p2, p3);
push_tri(&mut g.arrow_fill, p1, p3, p4); push_tri(&mut g.arrow_fill, p1, p3, p4);
} }
@@ -1860,17 +2053,47 @@ pub(crate) fn append_arrow(g: &mut DimGeom, tip: Vec3, dir: Vec3, arrow: &ArrowK
ArrowKind::Datum { size, filled } => { ArrowKind::Datum { size, filled } => {
// Right-pointing triangle with the base perpendicular to the dim // Right-pointing triangle with the base perpendicular to the dim
// line at the tip and the apex along +dir. // line at the tip and the apex along +dir.
let half = size * 0.5; let half = *size * 0.5;
let base_a = tip + perp * half; let base_a = tip + perp * half;
let base_b = tip - perp * half; let base_b = tip - perp * half;
let apex = tip + dir * size; let apex = tip + dir * *size;
add_segment(&mut g.dim_lines, base_a, apex); add_segment(&mut g.dim_lines, base_a, apex);
add_segment(&mut g.dim_lines, apex, base_b); add_segment(&mut g.dim_lines, apex, base_b);
add_segment(&mut g.dim_lines, base_b, base_a); add_segment(&mut g.dim_lines, base_b, base_a);
if filled { if *filled {
push_tri(&mut g.arrow_fill, base_a, apex, base_b); push_tri(&mut g.arrow_fill, base_a, apex, base_b);
} }
} }
ArrowKind::Custom { size, lines, fill } => {
let transform = |point: &[f32; 3]| {
tip - dir * (point[0] * *size) - perp * (point[1] * *size)
+ Vec3::Z * (point[2] * *size)
};
if !lines.is_empty()
&& !g.dim_lines.is_empty()
&& !g.dim_lines.last().is_some_and(|point| point[0].is_nan())
{
g.dim_lines.push([f32::NAN; 3]);
}
for point in lines {
if point[0].is_nan() {
if !g.dim_lines.last().is_some_and(|point| point[0].is_nan()) {
g.dim_lines.push([f32::NAN; 3]);
}
continue;
}
let point = transform(point);
g.dim_lines.push([point.x, point.y, point.z]);
}
for triangle in fill.chunks_exact(3) {
push_tri(
&mut g.arrow_fill,
transform(&triangle[0]),
transform(&triangle[1]),
transform(&triangle[2]),
);
}
}
} }
} }