fix(dimensions): render custom arrow blocks

Refs #612
This commit is contained in:
Hakan Seven 2026-08-02 16:06:37 +03:00
commit 20c6d419e2
6 changed files with 391 additions and 99 deletions

View file

@ -611,7 +611,20 @@ impl OpenCADStudio {
// 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<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()];
lt_opts.extend(doc.line_types.iter().map(|lt| lt.name.clone()));
let blk_name = |h: acadrust::types::Handle| -> String {

View file

@ -1388,6 +1388,27 @@ fn apply_dimension_breaks(
*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 {
fn tessellate(
&self,
@ -1528,7 +1549,7 @@ fn tessellate_dimension_inner(
let t = ArrowKind::Tick {
size: (dimtsz_raw as f32).max(0.001),
};
(t, t)
(t.clone(), t)
} else if let Some(s) = style {
if dimsah {
(
@ -1537,7 +1558,7 @@ fn tessellate_dimension_inner(
)
} else {
let a = arrow_from_block(document, s.dimblk, dimasz);
(a, a)
(a.clone(), a)
}
} else {
let a = ArrowKind::Triangle {
@ -1545,7 +1566,7 @@ fn tessellate_dimension_inner(
filled: true,
size_mul: 1.0,
};
(a, a)
(a.clone(), a)
};
// Text box (local space) so the dim line can be broken where the text

View file

@ -360,13 +360,13 @@ fn dim_terminator(
s.common.handle = Handle::NULL;
out.push(EntityType::Solid(s));
};
match *arrow {
match arrow {
A::None => {}
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 (l, r) = (pt(size, hw), pt(size, -hw));
if filled {
if *filled {
tri(tip, l, r, &mut out);
} else {
out.push(dim_seg(tip, l, common));
@ -375,7 +375,7 @@ fn dim_terminator(
}
}
A::Tick { size } => {
let s = size as f64;
let s = *size as f64;
let (ox, oy) = (dx + px, dy + py);
let m = (ox * ox + oy * oy).sqrt().max(1e-9);
let (ox, oy) = (ox / m * s, oy / m * s);
@ -386,42 +386,72 @@ fn dim_terminator(
));
}
A::Open { size, half_angle } => {
let size = size as f64;
let hw = size * (half_angle as f64).tan();
let size = *size as f64;
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));
}
A::Dot { size, filled } => {
let r = size as f64 * 0.5;
terminator_circle(tip, r, filled, common, &mut out);
let r = *size as f64 * 0.5;
terminator_circle(tip, r, *filled, common, &mut out);
}
A::Origin { size } => {
terminator_circle(tip, size as f64 * 0.25, true, common, &mut out);
let half = size as f64 * 0.5;
terminator_circle(tip, *size as f64 * 0.25, true, common, &mut out);
let half = *size as f64 * 0.5;
out.push(dim_seg(pt(0.0, -half), pt(0.0, half), common));
}
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));
out.push(dim_seg(p1, p2, common));
out.push(dim_seg(p2, p3, common));
out.push(dim_seg(p3, p4, common));
out.push(dim_seg(p4, p1, common));
if filled {
if *filled {
tri(p1, p2, p3, &mut out);
tri(p1, p3, p4, &mut out);
}
}
A::Datum { size, filled } => {
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 half = *size as f64 * 0.5;
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(apex, bb, common));
out.push(dim_seg(bb, ba, common));
if filled {
if *filled {
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
}
@ -576,17 +606,17 @@ fn dim_metrics(dim: &Dimension, doc: &CadDocument) -> DimMetrics {
// else closed-filled.
let (arrow1, arrow2) = if dimtsz > 1e-9 {
let t = ArrowKind::Tick { size: dimtsz as f32 };
(t, t)
(t.clone(), t)
} else if let Some(s) = style {
if s.dimsah {
(arrow_from_block(doc, s.dimblk1, asz), arrow_from_block(doc, s.dimblk2, asz))
} else {
let a = arrow_from_block(doc, s.dimblk, asz);
(a, a)
(a.clone(), a)
}
} else {
let a = ArrowKind::Triangle { size: asz, filled: true, size_mul: 1.0 };
(a, a)
(a.clone(), a)
};
DimMetrics {
dimasz,

View file

@ -434,7 +434,10 @@ fn build_defn(
// drew nothing. Expand the `*D` block's entities here as block-local
// subs so they transform with the parent insert. (Empty block_name
// — 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
.block_records
.iter()

View file

@ -738,15 +738,17 @@ pub(crate) fn tessellate_entity(
// ── Dimension baked-block fast path ─────────────────────────────────────
//
// AutoCAD bakes each dimension's final geometry (extension lines, dim
// line, arrows, text MText) into a per-instance block — usually
// A saved dimension may carry final geometry (extension lines, dim line,
// arrows and text) in a per-instance block — usually
// `*D<n>`, but custom names like `DIMBLOCK###-4NP` also occur. When the
// block exists we render its contents through `tessellate_entity` so
// sub-Text/MText get the standard baseline/greek/full LOD ladder, and
// DIMTXT × DIMSCALE isn't re-applied on already-baked geometry.
if let EntityType::Dimension(dim) = e {
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
.block_records
.iter()
@ -773,8 +775,8 @@ pub(crate) fn tessellate_entity(
continue;
};
// A dimension's definition points are baked into the
// block as POINTs on the Defpoints layer. AutoCAD never
// draws them as PDMODE glyphs — they're grip markers, not
// block as POINTs on the Defpoints layer. They are grip
// markers rather than PDMODE display geometry, so
// geometry — so rendering them adds a stray tick at each
// measured point that makes the extension lines look like
// they run past the geometry. Skip them.

View file

@ -1503,7 +1503,7 @@ pub fn tessellate(
#[derive(Clone, Copy)]
#[derive(Clone)]
pub(crate) enum ArrowKind {
None,
Triangle { size: f32, filled: bool, size_mul: f32 },
@ -1513,6 +1513,11 @@ pub(crate) enum ArrowKind {
Origin { size: f32 },
Box_ { 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(
@ -1520,100 +1525,288 @@ pub(crate) fn arrow_from_block(
handle: acadrust::types::Handle,
dimasz: f32,
) -> ArrowKind {
let name = if handle.is_null() {
None
} else {
doc.block_records
.iter()
.find(|b| b.handle == handle)
.map(|b| b.name.as_str())
if handle.is_null() {
return arrow_from_block_name(None, dimasz);
}
let Some(record) = doc.block_records.iter().find(|b| b.handle == handle) else {
return arrow_from_block_name(None, dimasz);
};
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 {
// AutoCAD's standard arrow blocks are prefixed with "_" (e.g. "_OPEN").
// Strip the prefix, upper-case, and switch on canonical names. Unknown
// / 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 {
name.and_then(|name| builtin_arrow_from_block_name(name, dimasz))
.unwrap_or(ArrowKind::Triangle {
size: dimasz,
filled: true,
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,
filled: false,
size_mul: 1.0,
},
"SMALL" => ArrowKind::Triangle {
}),
"SMALL" => Some(ArrowKind::Triangle {
size: dimasz,
filled: true,
size_mul: 0.5,
},
"OPEN" => ArrowKind::Open {
}),
"OPEN" => Some(ArrowKind::Open {
size: dimasz,
half_angle: 9.5_f32.to_radians(),
},
"OPEN30" => ArrowKind::Open {
}),
"OPEN30" => Some(ArrowKind::Open {
size: dimasz,
half_angle: 15.0_f32.to_radians(),
},
"OPEN90" => ArrowKind::Open {
}),
"OPEN90" => Some(ArrowKind::Open {
size: dimasz,
half_angle: 45.0_f32.to_radians(),
},
"DOT" => ArrowKind::Dot {
}),
"DOT" => Some(ArrowKind::Dot {
size: dimasz,
filled: true,
},
"DOTSMALL" => ArrowKind::Dot {
}),
"DOTSMALL" => Some(ArrowKind::Dot {
size: dimasz * 0.5,
filled: true,
},
"DOTBLANK" => ArrowKind::Dot {
}),
"DOTBLANK" => Some(ArrowKind::Dot {
size: dimasz,
filled: false,
},
"DOTSMALLBLANK" => ArrowKind::Dot {
}),
"DOTSMALLBLANK" => Some(ArrowKind::Dot {
size: dimasz * 0.5,
filled: false,
},
}),
"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
// (total 2·size — its `size` is a half-length, matching DIMTSZ). As an
// arrowhead block, though, DIMASZ is the stroke's *full* length like
// every other arrowhead here, so pass half of it — otherwise the
// oblique tick renders twice the intended DIMASZ.
"OBLIQUE" | "ARCHTICK" => ArrowKind::Tick { size: dimasz * 0.5 },
"BOXFILLED" => ArrowKind::Box_ {
// (total 2·size — its `size` is a half-length, matching DIMTSZ). For
// a block-selected tick DIMASZ is the full stroke length, so halve it.
"OBLIQUE" | "ARCHTICK" => Some(ArrowKind::Tick { size: dimasz * 0.5 }),
"BOXFILLED" => Some(ArrowKind::Box_ {
size: dimasz,
filled: true,
},
"BOXBLANK" | "BOX" => ArrowKind::Box_ {
}),
"BOXBLANK" | "BOX" => Some(ArrowKind::Box_ {
size: dimasz,
filled: false,
},
"DATUMFILLED" | "DATUMTRIANGLEFILLED" => ArrowKind::Datum {
}),
"DATUMFILLED" | "DATUMTRIANGLEFILLED" => Some(ArrowKind::Datum {
size: dimasz,
filled: true,
},
"DATUMBLANK" | "DATUMTRIANGLE" => ArrowKind::Datum {
}),
"DATUMBLANK" | "DATUMTRIANGLE" => Some(ArrowKind::Datum {
size: dimasz,
filled: false,
},
"NONE" => ArrowKind::None,
// INTEGRAL and other complex glyphs aren't reproduced here; fall through.
_ => ArrowKind::Triangle {
}),
"NONE" => Some(ArrowKind::None),
_ => None,
}
}
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,
filled: true,
size_mul: 1.0,
},
lines,
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) {
let dir = normalized_or(dir, Vec3::X);
let perp = Vec3::new(-dir.y, dir.x, 0.0);
match *arrow {
match arrow {
ArrowKind::None => {}
ArrowKind::Triangle {
size,
filled,
size_mul,
} => {
let size = size * size_mul;
let size = *size * *size_mul;
let base = tip + dir * size;
// ~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 left = base + perp * half_w;
let right = base - perp * half_w;
add_segment(&mut g.dim_lines, tip, left);
add_segment(&mut g.dim_lines, left, right);
add_segment(&mut g.dim_lines, right, tip);
if filled {
if *filled {
push_tri(&mut g.arrow_fill, tip, left, right);
}
}
ArrowKind::Tick { size } => {
// 45° oblique tick crossing the dim line at the tip; `size` is
// the half-length (matches AutoCAD's DIMTSZ semantics).
let off = (dir + perp).normalize_or_zero() * size;
// the half-length used by DIMTSZ.
let off = (dir + perp).normalize_or_zero() * *size;
add_segment(&mut g.dim_lines, tip - off, tip + off);
}
ArrowKind::Open { size, half_angle } => {
let base = tip + dir * size;
let half_w = size * half_angle.tan();
let base = tip + dir * *size;
let half_w = *size * half_angle.tan();
let left = 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, right);
}
ArrowKind::Dot { size, filled } => {
let r = size * 0.5;
let r = *size * 0.5;
const N: usize = 16;
let mut ring: Vec<Vec3> = Vec::with_capacity(N + 1);
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));
}
add_polyline(&mut g.dim_lines, &ring);
if filled {
if *filled {
for i in 0..N {
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 } => {
// Small filled dot at the tip with a perpendicular tick crossing
// the dim line — matches "_ORIGIN" / "_ORIGIN2" blocks.
let r = size * 0.25;
let r = *size * 0.25;
const N: usize = 12;
let mut ring: Vec<Vec3> = Vec::with_capacity(N + 1);
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 {
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);
}
ArrowKind::Box_ { size, filled } => {
let half = size * 0.5;
let half = *size * 0.5;
let p1 = tip - dir * half - perp * half;
let p2 = 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, p3, p4);
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, p3, p4);
}
@ -1860,17 +2053,47 @@ pub(crate) fn append_arrow(g: &mut DimGeom, tip: Vec3, dir: Vec3, arrow: &ArrowK
ArrowKind::Datum { size, filled } => {
// Right-pointing triangle with the base perpendicular to the dim
// 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_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, apex, base_b);
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);
}
}
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]),
);
}
}
}
}