forked from 1b-agent/opencadstudio-upstream
Preserve frame geometry for selection while separating viewport and plot visibility across supported reference entities. Refs #642
887 lines
32 KiB
Rust
887 lines
32 KiB
Rust
use acadrust::entities::{HooklineDirection, Leader, LeaderCreationType, LeaderPathType};
|
||
use acadrust::Entity;
|
||
use glam::Vec3;
|
||
|
||
use crate::command::EntityTransform;
|
||
use crate::entities::common::{
|
||
center_grip, edit_prop as edit, ro_prop as ro, square_grip, stepper_prop as stepper,
|
||
};
|
||
use crate::entities::traits::RenderConvertible;
|
||
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
|
||
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||
use crate::scene::model::wire_model::TangentGeom;
|
||
use crate::t;
|
||
|
||
// ── RenderConvertible (used for snap/grip key-vertices) ─────────────────────
|
||
|
||
/// Whether a leader draws a horizontal hookline. DWG stores no "hookline
|
||
/// exists" flag (only the direction bit), so the stored flag alone would drop
|
||
/// every hookline on DWG-loaded leaders: a text-annotated leader gets a
|
||
/// horizontal hook whenever its final segment leaves the horizontal by more
|
||
/// than 15°.
|
||
fn draws_hookline(leader: &Leader) -> bool {
|
||
if leader.hookline_enabled {
|
||
return true;
|
||
}
|
||
let verts = &leader.vertices;
|
||
if leader.creation_type != LeaderCreationType::WithText
|
||
|| leader.annotation_handle.is_null()
|
||
|| verts.len() < 2
|
||
{
|
||
return false;
|
||
}
|
||
let last = &verts[verts.len() - 1];
|
||
let prev = &verts[verts.len() - 2];
|
||
let (dx, dy) = (last.x - prev.x, last.y - prev.y);
|
||
let len = (dx * dx + dy * dy).sqrt();
|
||
let h = leader.horizontal_direction;
|
||
let hl = (h.x * h.x + h.y * h.y).sqrt();
|
||
let cos = if len > 1e-9 && hl > 1e-9 {
|
||
((dx * h.x + dy * h.y) / (len * hl)).abs()
|
||
} else {
|
||
1.0
|
||
};
|
||
cos < (15.0f64).to_radians().cos()
|
||
}
|
||
|
||
fn to_render(leader: &Leader) -> RenderEntity {
|
||
let verts = &leader.vertices;
|
||
let nan = [f64::NAN; 3];
|
||
let p3 = |v: &acadrust::types::Vector3| -> [f64; 3] { [v.x, v.y, v.z] };
|
||
let p3f = |v: &acadrust::types::Vector3| -> [f32; 3] { [v.x as f32, v.y as f32, v.z as f32] };
|
||
|
||
let mut points: Vec<[f64; 3]> = Vec::new();
|
||
let mut tangents: Vec<TangentGeom> = Vec::new();
|
||
let mut key_verts: Vec<[f64; 3]> = Vec::new();
|
||
|
||
// Main leader path
|
||
for v in verts {
|
||
points.push(p3(v));
|
||
key_verts.push(p3(v));
|
||
}
|
||
for i in 0..verts.len().saturating_sub(1) {
|
||
// TangentGeom uses f32 (UI-only); cast at construction.
|
||
tangents.push(TangentGeom::Line {
|
||
p1: p3f(&verts[i]),
|
||
p2: p3f(&verts[i + 1]),
|
||
});
|
||
}
|
||
|
||
// Arrowhead at vertex[0]
|
||
if leader.arrow_enabled && verts.len() >= 2 {
|
||
let tip = &verts[0];
|
||
let next = &verts[1];
|
||
let dx = next.x - tip.x;
|
||
let dy = next.y - tip.y;
|
||
let len = (dx * dx + dy * dy).sqrt().max(1e-9);
|
||
let (dx, dy) = (dx / len, dy / len);
|
||
// Arrowhead sized to the text height, matching the MLEADER arrowhead.
|
||
let sz = (leader.text_height).max(1.0);
|
||
let a = std::f64::consts::PI / 6.0;
|
||
let (s, c) = a.sin_cos();
|
||
let tip_f = p3(tip);
|
||
points.push(nan);
|
||
points.push([
|
||
tip_f[0] + (dx * c - dy * s) * sz,
|
||
tip_f[1] + (dx * s + dy * c) * sz,
|
||
tip_f[2],
|
||
]);
|
||
points.push(tip_f);
|
||
points.push([
|
||
tip_f[0] + (dx * c + dy * s) * sz,
|
||
tip_f[1] + (-dx * s + dy * c) * sz,
|
||
tip_f[2],
|
||
]);
|
||
}
|
||
|
||
// Landing line at last vertex
|
||
if draws_hookline(leader) && verts.len() >= 2 {
|
||
let last = verts.last().unwrap();
|
||
let prev = &verts[verts.len() - 2];
|
||
// Landing runs along the leader's horizontal direction (UCS X for
|
||
// UCS-placed leaders, world X otherwise), on the side the leader
|
||
// approaches from.
|
||
let (hx, hy) = {
|
||
let h = leader.horizontal_direction;
|
||
let l = (h.x * h.x + h.y * h.y).sqrt();
|
||
if l > 1e-9 {
|
||
(h.x / l, h.y / l)
|
||
} else {
|
||
(1.0, 0.0)
|
||
}
|
||
};
|
||
let sign = if (last.x - prev.x) * hx + (last.y - prev.y) * hy >= 0.0 {
|
||
1.0_f64
|
||
} else {
|
||
-1.0_f64
|
||
};
|
||
let len = leader.text_height * 1.5;
|
||
let last_f = p3(last);
|
||
points.push(nan);
|
||
points.push(last_f);
|
||
points.push([
|
||
last_f[0] + sign * len * hx,
|
||
last_f[1] + sign * len * hy,
|
||
last_f[2],
|
||
]);
|
||
}
|
||
|
||
RenderEntity {
|
||
pick_tris: Vec::new(),
|
||
object: RenderObject::Lines(points),
|
||
snap_pts: vec![],
|
||
tangent_geoms: tangents,
|
||
key_vertices: key_verts,
|
||
fill_tris: vec![],
|
||
}
|
||
}
|
||
|
||
// ── Grips ──────────────────────────────────────────────────────────────────
|
||
|
||
fn grips(leader: &Leader) -> Vec<GripDef> {
|
||
let n = leader.vertices.len();
|
||
let mut grips: Vec<GripDef> = leader
|
||
.vertices
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(i, v)| square_grip(i, glam::DVec3::new(v.x, v.y, v.z)))
|
||
.collect();
|
||
|
||
if n >= 2 {
|
||
let sum = leader.vertices.iter().fold(glam::DVec3::ZERO, |acc, v| {
|
||
acc + glam::DVec3::new(v.x, v.y, v.z)
|
||
});
|
||
grips.push(center_grip(n, sum / n as f64));
|
||
}
|
||
|
||
grips
|
||
}
|
||
|
||
fn apply_grip(leader: &mut Leader, grip_id: usize, apply: GripApply) {
|
||
let n = leader.vertices.len();
|
||
|
||
if grip_id < n {
|
||
if n >= 3 && leader.creation_type == LeaderCreationType::WithText {
|
||
// Grip del codo: mueve el codo libremente, pero arrastra también
|
||
// el extremo del renglón manteniendo la distancia relativa.
|
||
if grip_id == n - 2 {
|
||
let old_elbow = leader.vertices[n - 2];
|
||
let old_end = leader.vertices[n - 1];
|
||
|
||
let delta = match apply {
|
||
GripApply::Absolute(p) => acadrust::types::Vector3::new(
|
||
p.x as f64 - old_elbow.x,
|
||
p.y as f64 - old_elbow.y,
|
||
p.z as f64 - old_elbow.z,
|
||
),
|
||
GripApply::Translate(d) => acadrust::types::Vector3::new(
|
||
d.x as f64,
|
||
d.y as f64,
|
||
d.z as f64,
|
||
),
|
||
};
|
||
|
||
leader.vertices[n - 2].x = old_elbow.x + delta.x;
|
||
leader.vertices[n - 2].y = old_elbow.y + delta.y;
|
||
leader.vertices[n - 2].z = old_elbow.z + delta.z;
|
||
|
||
leader.vertices[n - 1].x = old_end.x + delta.x;
|
||
leader.vertices[n - 1].y = old_end.y + delta.y;
|
||
leader.vertices[n - 1].z = old_end.z + delta.z;
|
||
|
||
return;
|
||
}
|
||
|
||
// Grip del extremo horizontal: sólo debe estirar en X;
|
||
// Y/Z quedan pegados al codo para que siga horizontal.
|
||
if grip_id == n - 1 {
|
||
let elbow = leader.vertices[n - 2];
|
||
|
||
match apply {
|
||
GripApply::Absolute(p) => {
|
||
leader.vertices[n - 1].x = p.x as f64;
|
||
}
|
||
GripApply::Translate(d) => {
|
||
leader.vertices[n - 1].x += d.x as f64;
|
||
}
|
||
}
|
||
|
||
leader.vertices[n - 1].y = elbow.y;
|
||
leader.vertices[n - 1].z = elbow.z;
|
||
return;
|
||
}
|
||
}
|
||
|
||
if let Some(v) = leader.vertices.get_mut(grip_id) {
|
||
match apply {
|
||
GripApply::Absolute(p) => {
|
||
v.x = p.x as f64;
|
||
v.y = p.y as f64;
|
||
v.z = p.z as f64;
|
||
}
|
||
GripApply::Translate(d) => {
|
||
v.x += d.x as f64;
|
||
v.y += d.y as f64;
|
||
v.z += d.z as f64;
|
||
}
|
||
}
|
||
}
|
||
} else if let GripApply::Translate(d) = apply {
|
||
leader.translate(acadrust::types::Vector3::new(
|
||
d.x as f64,
|
||
d.y as f64,
|
||
d.z as f64,
|
||
));
|
||
}
|
||
}
|
||
|
||
// ── Properties ─────────────────────────────────────────────────────────────
|
||
|
||
fn choice_prop(label: &str, field: &'static str, selected: &str, options: &[&str]) -> Property {
|
||
Property {
|
||
label: label.into(),
|
||
field,
|
||
value: PropValue::Choice {
|
||
selected: selected.to_string(),
|
||
options: options.iter().map(|s| s.to_string()).collect(),
|
||
},
|
||
}
|
||
}
|
||
|
||
/// Combined path/arrow "Type" value (path shape × arrowhead flag).
|
||
fn leader_type_str(path: &LeaderPathType, arrow: bool) -> &'static str {
|
||
match (path, arrow) {
|
||
(LeaderPathType::StraightLine, true) => "Line with arrow",
|
||
(LeaderPathType::StraightLine, false) => "Line without arrow",
|
||
(LeaderPathType::Spline, true) => "Spline with arrow",
|
||
(LeaderPathType::Spline, false) => "Spline without arrow",
|
||
}
|
||
}
|
||
|
||
fn properties(leader: &Leader) -> Vec<PropSection> {
|
||
let n = leader.vertices.len();
|
||
// The panel's Current Vertex focus, clamped to this leader's range.
|
||
let vi = if n == 0 {
|
||
0
|
||
} else {
|
||
crate::scene::view::dispatch::prop_current_vertex().min(n - 1)
|
||
};
|
||
let vertex_label = if n == 0 {
|
||
"—".to_string()
|
||
} else {
|
||
format!("{} / {}", vi + 1, n)
|
||
};
|
||
|
||
// Geometry sits right after General — a leader owns editable path vertices,
|
||
// navigated one at a time by the Current Vertex spinner.
|
||
let mut geometry = vec![stepper(t!("Current Vertex").as_ref(), "current_vertex", vertex_label)];
|
||
if let Some(v) = leader.vertices.get(vi) {
|
||
geometry.push(edit(t!("Vertex X").as_ref(), "vertex_x", v.x));
|
||
geometry.push(edit(t!("Vertex Y").as_ref(), "vertex_y", v.y));
|
||
geometry.push(edit(t!("Vertex Z").as_ref(), "vertex_z", v.z));
|
||
} else {
|
||
geometry.push(ro(t!("Vertex X").as_ref(), "vertex_x", String::new()));
|
||
geometry.push(ro(t!("Vertex Y").as_ref(), "vertex_y", String::new()));
|
||
geometry.push(ro(t!("Vertex Z").as_ref(), "vertex_z", String::new()));
|
||
}
|
||
|
||
// Misc: Dim style (upgraded to a dropdown by the panel builder), the
|
||
// combined path/arrow Type, and annotative state (from the dim style).
|
||
let misc = vec![
|
||
Property {
|
||
label: t!("Dim style").into_owned(),
|
||
field: "dimension_style",
|
||
value: PropValue::PlainText(leader.dimension_style.clone()),
|
||
},
|
||
choice_prop(
|
||
t!("Type").as_ref(),
|
||
"leader_type",
|
||
leader_type_str(&leader.path_type, leader.arrow_enabled),
|
||
&[
|
||
"Line with arrow",
|
||
"Line without arrow",
|
||
"Spline with arrow",
|
||
"Spline without arrow",
|
||
],
|
||
),
|
||
ro(t!("Annotative").as_ref(), "annotative", "No"),
|
||
];
|
||
|
||
// Lines & Arrows / Text / Fit are dimension-style-derived; the panel builder
|
||
// resolves leader.dimension_style and fills these values from the DimStyle.
|
||
let lines_arrows = vec![
|
||
ro(t!("Arrow").as_ref(), "arrow_block", "Closed filled"),
|
||
ro(t!("Arrow size").as_ref(), "arrow_size", String::new()),
|
||
ro(t!("Dim line lineweight").as_ref(), "dim_line_lw", "ByLayer"),
|
||
ro(t!("Dim line color").as_ref(), "dim_line_color", "ByLayer"),
|
||
];
|
||
let text = vec![
|
||
ro(t!("Text offset").as_ref(), "text_offset", String::new()),
|
||
ro(t!("Text pos vert").as_ref(), "text_pos_vert", String::new()),
|
||
];
|
||
let fit = vec![ro(t!("Dim scale overall").as_ref(), "dim_scale_overall", String::new())];
|
||
|
||
vec![
|
||
PropSection {
|
||
title: t!("Geometry").into_owned(),
|
||
props: geometry,
|
||
},
|
||
PropSection {
|
||
title: t!("Misc").into_owned(),
|
||
props: misc,
|
||
},
|
||
PropSection {
|
||
title: t!("Lines & Arrows").into_owned(),
|
||
props: lines_arrows,
|
||
},
|
||
PropSection {
|
||
title: t!("Text").into_owned(),
|
||
props: text,
|
||
},
|
||
PropSection {
|
||
title: t!("Fit").into_owned(),
|
||
props: fit,
|
||
},
|
||
]
|
||
}
|
||
|
||
fn apply_geom_prop(leader: &mut Leader, field: &str, value: &str) {
|
||
let f64 = |s: &str| -> Option<f64> { s.trim().parse().ok() };
|
||
// Vertex X/Y/Z edit whichever vertex the Current Vertex navigator focuses.
|
||
let vi = if leader.vertices.is_empty() {
|
||
0
|
||
} else {
|
||
crate::scene::view::dispatch::prop_current_vertex().min(leader.vertices.len() - 1)
|
||
};
|
||
|
||
match field {
|
||
"dimension_style" => leader.dimension_style = value.to_string(),
|
||
"leader_type" => {
|
||
let (p, a) = match value {
|
||
"Line without arrow" => (LeaderPathType::StraightLine, false),
|
||
"Spline with arrow" => (LeaderPathType::Spline, true),
|
||
"Spline without arrow" => (LeaderPathType::Spline, false),
|
||
_ => (LeaderPathType::StraightLine, true),
|
||
};
|
||
leader.path_type = p;
|
||
leader.arrow_enabled = a;
|
||
}
|
||
"path_type" => {
|
||
leader.path_type = match value {
|
||
"Spline" => LeaderPathType::Spline,
|
||
_ => LeaderPathType::StraightLine,
|
||
};
|
||
}
|
||
"creation_type" => {
|
||
leader.creation_type = match value {
|
||
"With Tolerance" => LeaderCreationType::WithTolerance,
|
||
"With Block" => LeaderCreationType::WithBlock,
|
||
"No Annotation" => LeaderCreationType::NoAnnotation,
|
||
_ => LeaderCreationType::WithText,
|
||
};
|
||
}
|
||
"arrow_enabled" => {
|
||
leader.arrow_enabled = if value == "toggle" {
|
||
!leader.arrow_enabled
|
||
} else {
|
||
value == "true"
|
||
}
|
||
}
|
||
"hookline_enabled" => {
|
||
leader.hookline_enabled = if value == "toggle" {
|
||
!leader.hookline_enabled
|
||
} else {
|
||
value == "true"
|
||
}
|
||
}
|
||
"hookline_direction" => {
|
||
leader.hookline_direction = match value {
|
||
"Same" => HooklineDirection::Same,
|
||
_ => HooklineDirection::Opposite,
|
||
};
|
||
}
|
||
"text_height" => {
|
||
if let Some(v) = f64(value) {
|
||
leader.text_height = v;
|
||
}
|
||
}
|
||
"text_width" => {
|
||
if let Some(v) = f64(value) {
|
||
leader.text_width = v;
|
||
}
|
||
}
|
||
"normal_x" => {
|
||
if let Some(v) = f64(value) {
|
||
leader.normal.x = v;
|
||
}
|
||
}
|
||
"normal_y" => {
|
||
if let Some(v) = f64(value) {
|
||
leader.normal.y = v;
|
||
}
|
||
}
|
||
"normal_z" => {
|
||
if let Some(v) = f64(value) {
|
||
leader.normal.z = v;
|
||
}
|
||
}
|
||
"h_dir_x" => {
|
||
if let Some(v) = f64(value) {
|
||
leader.horizontal_direction.x = v;
|
||
}
|
||
}
|
||
"h_dir_y" => {
|
||
if let Some(v) = f64(value) {
|
||
leader.horizontal_direction.y = v;
|
||
}
|
||
}
|
||
"h_dir_z" => {
|
||
if let Some(v) = f64(value) {
|
||
leader.horizontal_direction.z = v;
|
||
}
|
||
}
|
||
"block_offset_x" => {
|
||
if let Some(v) = f64(value) {
|
||
leader.block_offset.x = v;
|
||
}
|
||
}
|
||
"block_offset_y" => {
|
||
if let Some(v) = f64(value) {
|
||
leader.block_offset.y = v;
|
||
}
|
||
}
|
||
"block_offset_z" => {
|
||
if let Some(v) = f64(value) {
|
||
leader.block_offset.z = v;
|
||
}
|
||
}
|
||
"ann_offset_x" => {
|
||
if let Some(v) = f64(value) {
|
||
leader.annotation_offset.x = v;
|
||
}
|
||
}
|
||
"ann_offset_y" => {
|
||
if let Some(v) = f64(value) {
|
||
leader.annotation_offset.y = v;
|
||
}
|
||
}
|
||
"ann_offset_z" => {
|
||
if let Some(v) = f64(value) {
|
||
leader.annotation_offset.z = v;
|
||
}
|
||
}
|
||
"vertex_x" => {
|
||
if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(vi)) {
|
||
vert.x = v;
|
||
}
|
||
}
|
||
"vertex_y" => {
|
||
if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(vi)) {
|
||
vert.y = v;
|
||
}
|
||
}
|
||
"vertex_z" => {
|
||
if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(vi)) {
|
||
vert.z = v;
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
// ── Transform ──────────────────────────────────────────────────────────────
|
||
|
||
fn apply_transform(leader: &mut Leader, t: &EntityTransform) {
|
||
crate::scene::view::transform::apply_standard_entity_transform(leader, t, |entity, p1, p2| {
|
||
for v in &mut entity.vertices {
|
||
crate::scene::view::transform::reflect_xy_point(&mut v.x, &mut v.y, p1, p2);
|
||
}
|
||
crate::scene::view::transform::reflect_xy_point(
|
||
&mut entity.block_offset.x,
|
||
&mut entity.block_offset.y,
|
||
p1,
|
||
p2,
|
||
);
|
||
crate::scene::view::transform::reflect_xy_point(
|
||
&mut entity.annotation_offset.x,
|
||
&mut entity.annotation_offset.y,
|
||
p1,
|
||
p2,
|
||
);
|
||
});
|
||
}
|
||
|
||
// ── Trait impls ────────────────────────────────────────────────────────────
|
||
|
||
impl RenderConvertible for Leader {
|
||
fn to_render(&self, _document: &acadrust::CadDocument) -> Option<RenderEntity> {
|
||
if self.vertices.is_empty() {
|
||
return None;
|
||
}
|
||
Some(to_render(self))
|
||
}
|
||
}
|
||
|
||
impl crate::entities::traits::Grippable for Leader {
|
||
fn grips(&self) -> Vec<GripDef> {
|
||
grips(self)
|
||
}
|
||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||
apply_grip(self, grip_id, apply);
|
||
}
|
||
fn grip_menu(&self, grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
|
||
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
||
let n = self.vertices.len();
|
||
if grip_id == 0 {
|
||
// Arrow head — stretch only.
|
||
vec![GripMenuItem {
|
||
label: "Stretch",
|
||
action: GripMenuAction::Stretch,
|
||
}]
|
||
} else if grip_id < n {
|
||
vec![
|
||
GripMenuItem {
|
||
label: "Stretch",
|
||
action: GripMenuAction::Stretch,
|
||
},
|
||
GripMenuItem {
|
||
label: "Add Vertex",
|
||
action: GripMenuAction::AddVertex,
|
||
},
|
||
GripMenuItem {
|
||
label: "Remove Vertex",
|
||
action: GripMenuAction::RemoveVertex,
|
||
},
|
||
]
|
||
} else {
|
||
// Centroid grip — move whole leader.
|
||
vec![GripMenuItem {
|
||
label: "Stretch",
|
||
action: GripMenuAction::Stretch,
|
||
}]
|
||
}
|
||
}
|
||
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
|
||
use crate::scene::model::object::GripMenuAction as A;
|
||
let n = self.vertices.len();
|
||
match action {
|
||
A::AddVertex if grip_id < n => {
|
||
let i1 = (grip_id + 1).min(n - 1);
|
||
if i1 == grip_id {
|
||
return;
|
||
}
|
||
let v0 = &self.vertices[grip_id];
|
||
let v1 = &self.vertices[i1];
|
||
let mid = acadrust::types::Vector3::new(
|
||
(v0.x + v1.x) * 0.5,
|
||
(v0.y + v1.y) * 0.5,
|
||
(v0.z + v1.z) * 0.5,
|
||
);
|
||
self.vertices.insert(i1, mid);
|
||
}
|
||
A::RemoveVertex if grip_id < n && n > 2 => {
|
||
self.vertices.remove(grip_id);
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
|
||
impl crate::entities::traits::PropertyEditable for Leader {
|
||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||
properties(self)
|
||
}
|
||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||
apply_geom_prop(self, field, value);
|
||
}
|
||
}
|
||
|
||
impl crate::entities::traits::Transformable for Leader {
|
||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||
apply_transform(self, t);
|
||
}
|
||
}
|
||
|
||
/// Per-entity tessellation entry for `Leader`. Lives here so all leader
|
||
/// tess code stays alongside the entity definition. Cross-entity dim
|
||
/// machinery (arrow shapes, `DimGeom`) lives in `scene::convert::tessellate` and
|
||
/// is reused via the dim arrow emitter so the leader matches the active
|
||
/// DIMSTYLE.
|
||
pub trait LeaderTess {
|
||
fn tessellate(
|
||
&self,
|
||
document: &acadrust::CadDocument,
|
||
handle: acadrust::Handle,
|
||
selected: bool,
|
||
entity_color: [f32; 4],
|
||
line_weight_px: f32,
|
||
anno_scale: f32,
|
||
) -> crate::scene::model::wire_model::WireModel;
|
||
}
|
||
|
||
impl LeaderTess for Leader {
|
||
fn tessellate(
|
||
&self,
|
||
document: &acadrust::CadDocument,
|
||
handle: acadrust::Handle,
|
||
selected: bool,
|
||
entity_color: [f32; 4],
|
||
line_weight_px: f32,
|
||
anno_scale: f32,
|
||
) -> crate::scene::model::wire_model::WireModel {
|
||
use crate::scene::convert::tessellate::{append_arrow, arrow_from_block, ArrowKind, DimGeom};
|
||
use crate::entities::dim_override as dov;
|
||
use crate::scene::model::wire_model::WireModel;
|
||
let xd = &self.common.extended_data;
|
||
// Dim-line colour: a per-object ACAD_DSTYLE override (code 176, an ACI
|
||
// index) wins over the assigned dim style's DIMCLRD; ByLayer / ByBlock
|
||
// (0 / 256) and no setting fall through to the entity colour.
|
||
let color = if selected {
|
||
WireModel::SELECTED
|
||
} else {
|
||
let dim_clr = dov::int(xd, dov::DIMCLRD).or_else(|| {
|
||
document
|
||
.dim_styles
|
||
.iter()
|
||
.find(|s| {
|
||
s.name.eq_ignore_ascii_case(&self.dimension_style)
|
||
|| (self.dimension_style.trim().is_empty()
|
||
&& s.name.eq_ignore_ascii_case("Standard"))
|
||
})
|
||
.map(|s| s.dimclrd)
|
||
});
|
||
match dim_clr {
|
||
Some(idx) if idx != 0 && idx != 256 => crate::scene::convert::tess_util::aci_to_rgba(
|
||
&acadrust::types::Color::from_index(idx),
|
||
),
|
||
_ => entity_color,
|
||
}
|
||
};
|
||
// A concrete DIMLWD override sets the leader line's weight; ByLayer /
|
||
// ByBlock / Default and no override keep the resolved weight passed in.
|
||
let line_weight_px = match dov::int(xd, dov::DIMLWD) {
|
||
Some(lwd) if lwd >= 0 => crate::scene::view::render::lineweight_to_px(
|
||
&acadrust::types::LineWeight::from_value(lwd),
|
||
),
|
||
_ => line_weight_px,
|
||
};
|
||
let name = handle.value().to_string();
|
||
let p3 = |v: &acadrust::types::Vector3| -> [f32; 3] {
|
||
[(v.x) as f32, (v.y) as f32, (v.z) as f32]
|
||
};
|
||
|
||
let verts = &self.vertices;
|
||
|
||
if verts.len() < 2 {
|
||
return WireModel {
|
||
taper_widths: Vec::new(),
|
||
world_width: 0.0,
|
||
depth_override: None,
|
||
display_visible: true,
|
||
plot_visible: true,
|
||
fill_is_3d: false,
|
||
fill_is_2d_solid: false,
|
||
render_instance: None,
|
||
pick_tris: Vec::new(),
|
||
pick_tris_low: Vec::new(),
|
||
dash_from_start: false,
|
||
dash_align_end: None,
|
||
text_verts: Vec::new(),
|
||
name,
|
||
points: vec![],
|
||
points_low: Vec::new(),
|
||
color,
|
||
selected,
|
||
aci: 0,
|
||
pattern_length: 0.0,
|
||
pattern: [0.0; 8],
|
||
line_weight_px,
|
||
snap_pts: vec![],
|
||
tangent_geoms: vec![],
|
||
key_vertices: vec![],
|
||
aabb: WireModel::UNBOUNDED_AABB,
|
||
plinegen: true,
|
||
fill_tris: vec![],
|
||
fill_tris_low: Vec::new(),
|
||
};
|
||
}
|
||
|
||
// Build all geometry in absolute f64 and RTE-split at the end. At UTM
|
||
// coordinates a raw `v.x as f32` quantises to ~0.5 units, which flattens
|
||
// the oblique arrow tick into a horizontal line and jitters the path.
|
||
let origin = [verts[0].x, verts[0].y, verts[0].z];
|
||
let mut points: Vec<[f64; 3]> = verts.iter().map(|v| [v.x, v.y, v.z]).collect();
|
||
let mut tangents: Vec<TangentGeom> = Vec::new();
|
||
let key_vertices: Vec<[f64; 3]> = verts.iter().map(|v| [v.x, v.y, v.z]).collect();
|
||
let mut fill_tris: Vec<[f64; 3]> = Vec::new();
|
||
|
||
for i in 0..verts.len().saturating_sub(1) {
|
||
tangents.push(TangentGeom::Line {
|
||
p1: p3(&verts[i]),
|
||
p2: p3(&verts[i + 1]),
|
||
});
|
||
}
|
||
|
||
if self.arrow_enabled {
|
||
// Resolve the active dim style → DIMLDRBLK to pick the arrow shape.
|
||
// DIMASZ × DIMSCALE drives the size when available; otherwise fall
|
||
// back to the legacy text-height heuristic.
|
||
let style = document.dim_styles.iter().find(|s| {
|
||
s.name.eq_ignore_ascii_case(&self.dimension_style)
|
||
|| (self.dimension_style.trim().is_empty()
|
||
&& s.name.eq_ignore_ascii_case("Standard"))
|
||
});
|
||
// Each of DIMSCALE / DIMASZ / DIMLDRBLK prefers a per-object override
|
||
// over the style, so an edited leader arrow renders at its new size,
|
||
// scale and shape.
|
||
let dim_scale = dov::real(xd, dov::DIMSCALE)
|
||
.filter(|v| *v > 1e-6)
|
||
.or_else(|| style.map(|s| s.dimscale).filter(|v| *v > 1e-6))
|
||
.unwrap_or(anno_scale as f64);
|
||
let ovr_asz = dov::real(xd, dov::DIMASZ);
|
||
let arrow_size = match (ovr_asz, style) {
|
||
(Some(a), _) => (a * dim_scale) as f32,
|
||
(None, Some(s)) => (s.dimasz * dim_scale) as f32,
|
||
(None, None) => (self.text_height as f32).max(1.0) * anno_scale,
|
||
};
|
||
let arrow_blk = dov::handle(xd, dov::DIMLDRBLK).or_else(|| style.map(|s| s.dimldrblk));
|
||
let arrow = match arrow_blk {
|
||
Some(h) => arrow_from_block(document, h, arrow_size.max(0.001)),
|
||
None => ArrowKind::Triangle {
|
||
size: arrow_size.max(0.001),
|
||
filled: true,
|
||
size_mul: 1.0,
|
||
},
|
||
};
|
||
|
||
let tip = &verts[0];
|
||
let next = &verts[1];
|
||
let dx = (next.x - tip.x) as f32;
|
||
let dy = (next.y - tip.y) as f32;
|
||
let len = (dx * dx + dy * dy).sqrt().max(1e-9);
|
||
let dir = Vec3::new(dx / len, dy / len, 0.0);
|
||
// Omit the arrowhead when it is larger than the first leader segment
|
||
// it would sit on: an oversized arrow on a short pointer is
|
||
// suppressed, not drawn overshooting the segment. Without this an
|
||
// annotation-scaled arrow (DIMASZ sized for a bigger plot) paints a
|
||
// stray stroke longer than the leader itself.
|
||
if arrow_size <= len {
|
||
// Emit the arrow around a local origin (the tip) so
|
||
// `append_arrow`'s f32 math stays precise, then lift each point
|
||
// back to absolute f64. Reuse the dim arrow emitter so the
|
||
// leader shape matches the DIMSTYLE in use (Closed Filled by
|
||
// default, Dot, Tick, …).
|
||
let mut arrow_geom = DimGeom::new();
|
||
append_arrow(&mut arrow_geom, Vec3::ZERO, dir, &arrow);
|
||
let lift = |p: &[f32; 3]| -> [f64; 3] {
|
||
[
|
||
p[0] as f64 + origin[0],
|
||
p[1] as f64 + origin[1],
|
||
p[2] as f64 + origin[2],
|
||
]
|
||
};
|
||
if !arrow_geom.dim_lines.is_empty() {
|
||
points.push([f64::NAN; 3]);
|
||
points.extend(arrow_geom.dim_lines.iter().map(&lift));
|
||
}
|
||
fill_tris.extend(arrow_geom.arrow_fill.iter().map(&lift));
|
||
}
|
||
}
|
||
|
||
if draws_hookline(self) {
|
||
let last = verts.last().unwrap();
|
||
let prev = &verts[verts.len() - 2];
|
||
let sign = if (last.x - prev.x) >= 0.0 {
|
||
1.0_f64
|
||
} else {
|
||
-1.0_f64
|
||
};
|
||
let style = document.dim_styles.iter().find(|s| {
|
||
s.name.eq_ignore_ascii_case(&self.dimension_style)
|
||
|| (self.dimension_style.trim().is_empty()
|
||
&& s.name.eq_ignore_ascii_case("Standard"))
|
||
});
|
||
let dim_scale = dov::real(xd, dov::DIMSCALE)
|
||
.filter(|v| *v > 1e-6)
|
||
.or_else(|| style.map(|s| s.dimscale).filter(|v| *v > 1e-6))
|
||
.unwrap_or(anno_scale as f64);
|
||
// Hook length: DIMASZ, like the arrowhead, when a style resolves;
|
||
// the legacy text-height heuristic otherwise.
|
||
let mut land_len = match dov::real(xd, dov::DIMASZ)
|
||
.or_else(|| style.map(|s| s.dimasz))
|
||
{
|
||
Some(a) => a * dim_scale,
|
||
None => self.text_height * 1.5 * anno_scale as f64,
|
||
};
|
||
// With DIMTAD "text above", the annotation sits on top of the hook
|
||
// and the hook underlines it: extend to the far edge of the MTEXT's
|
||
// laid-out extents.
|
||
let dimtad = dov::int(xd, dov::DIMTAD)
|
||
.or_else(|| style.map(|s| s.dimtad))
|
||
.unwrap_or(0);
|
||
if dimtad != 0 {
|
||
let mt = match document.get_entity(self.annotation_handle) {
|
||
Some(acadrust::entities::EntityType::MText(mt)) => {
|
||
Some((mt.extents_width, mt.insertion_point.x))
|
||
}
|
||
_ => None,
|
||
};
|
||
// Underline width: the annotation MTEXT's laid-out extents when
|
||
// known, otherwise the leader's own stored annotation width
|
||
// (DXF code 41) — a DXF-loaded MTEXT often has no computed
|
||
// extents (extents_width == 0), which left the hook too short.
|
||
let width = mt
|
||
.map(|(w, _)| w)
|
||
.filter(|w| *w > 0.0)
|
||
.unwrap_or(self.text_width);
|
||
if width > 0.0 {
|
||
let ins_x = mt.map(|(_, x)| x).unwrap_or(last.x);
|
||
let to_far_edge = sign * (ins_x - last.x) + width;
|
||
land_len = land_len.max(to_far_edge);
|
||
}
|
||
}
|
||
points.push([f64::NAN; 3]);
|
||
points.push([last.x, last.y, last.z]);
|
||
points.push([last.x + sign * land_len, last.y, last.z]);
|
||
}
|
||
|
||
// Split absolute f64 into the render/eye double-single so the GPU keeps
|
||
// full precision at UTM coordinates (empty low bufs = absolute f32).
|
||
let (points, points_low) = crate::scene::convert::tessellate::points_to_ds(points);
|
||
let (fill_tris, fill_tris_low) =
|
||
crate::scene::convert::tessellate::points_to_ds(fill_tris);
|
||
|
||
WireModel {
|
||
taper_widths: Vec::new(),
|
||
world_width: 0.0,
|
||
depth_override: None,
|
||
display_visible: true,
|
||
plot_visible: true,
|
||
fill_is_3d: false,
|
||
fill_is_2d_solid: false,
|
||
render_instance: None,
|
||
pick_tris: Vec::new(),
|
||
pick_tris_low: Vec::new(),
|
||
dash_from_start: false,
|
||
dash_align_end: None,
|
||
text_verts: Vec::new(),
|
||
name,
|
||
points,
|
||
points_low,
|
||
color,
|
||
selected,
|
||
aci: 0,
|
||
pattern_length: 0.0,
|
||
pattern: [0.0; 8],
|
||
line_weight_px,
|
||
snap_pts: vec![],
|
||
tangent_geoms: tangents,
|
||
key_vertices,
|
||
aabb: WireModel::UNBOUNDED_AABB,
|
||
plinegen: true,
|
||
fill_tris,
|
||
fill_tris_low,
|
||
}
|
||
}
|
||
}
|