cad-editor/src/entities/leader.rs

714 lines
25 KiB
Rust
Raw Normal View History

2026-03-22 14:56:27 +03:00
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::TruckConvertible;
use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject};
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
use crate::scene::model::wire_model::TangentGeom;
2026-03-22 14:56:27 +03:00
// ── TruckConvertible (used for snap/grip key-vertices) ─────────────────────
fn to_truck(leader: &Leader) -> TruckEntity {
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] };
2026-03-22 14:56:27 +03:00
let mut points: Vec<[f64; 3]> = Vec::new();
2026-03-22 14:56:27 +03:00
let mut tangents: Vec<TangentGeom> = Vec::new();
let mut key_verts: Vec<[f64; 3]> = Vec::new();
2026-03-22 14:56:27 +03:00
// 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.
2026-05-12 10:36:15 +03:00
tangents.push(TangentGeom::Line {
p1: p3f(&verts[i]),
p2: p3f(&verts[i + 1]),
2026-05-12 10:36:15 +03:00
});
2026-03-22 14:56:27 +03:00
}
// 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;
2026-03-22 14:56:27 +03:00
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;
2026-03-22 14:56:27 +03:00
let (s, c) = a.sin_cos();
let tip_f = p3(tip);
points.push(nan);
2026-05-12 10:36:15 +03:00
points.push([
tip_f[0] + (dx * c - dy * s) * sz,
tip_f[1] + (dx * s + dy * c) * sz,
tip_f[2],
]);
2026-03-22 14:56:27 +03:00
points.push(tip_f);
2026-05-12 10:36:15 +03:00
points.push([
tip_f[0] + (dx * c + dy * s) * sz,
tip_f[1] + (-dx * s + dy * c) * sz,
tip_f[2],
]);
2026-03-22 14:56:27 +03:00
}
// Landing line at last vertex
if leader.hookline_enabled && 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;
2026-03-22 14:56:27 +03:00
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],
]);
2026-03-22 14:56:27 +03:00
}
TruckEntity {
object: TruckObject::Lines(points),
snap_pts: vec![],
tangent_geoms: tangents,
key_vertices: key_verts,
fill_tris: vec![],
2026-03-22 14:56:27 +03:00
}
}
// ── 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)))
2026-03-22 14:56:27 +03:00
.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)
2026-03-22 14:56:27 +03:00
});
grips.push(center_grip(n, sum / n as f64));
2026-03-22 14:56:27 +03:00
}
grips
}
fn apply_grip(leader: &mut Leader, grip_id: usize, apply: GripApply) {
let n = leader.vertices.len();
if grip_id < n {
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(
2026-05-12 10:36:15 +03:00
d.x as f64, d.y as f64, d.z as f64,
2026-03-22 14:56:27 +03:00
));
}
}
// ── 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> {
2026-03-22 14:56:27 +03:00
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("Current Vertex", "current_vertex", vertex_label)];
if let Some(v) = leader.vertices.get(vi) {
geometry.push(edit("Vertex X", "vertex_x", v.x));
geometry.push(edit("Vertex Y", "vertex_y", v.y));
geometry.push(edit("Vertex Z", "vertex_z", v.z));
} else {
geometry.push(ro("Vertex X", "vertex_x", String::new()));
geometry.push(ro("Vertex Y", "vertex_y", String::new()));
geometry.push(ro("Vertex Z", "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: "Dim style".into(),
field: "dimension_style",
value: PropValue::EditText(leader.dimension_style.clone()),
},
choice_prop(
"Type",
"leader_type",
leader_type_str(&leader.path_type, leader.arrow_enabled),
&[
"Line with arrow",
"Line without arrow",
"Spline with arrow",
"Spline without arrow",
],
),
ro("Annotative", "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("Arrow", "arrow_block", "Closed filled"),
ro("Arrow size", "arrow_size", String::new()),
ro("Dim line lineweight", "dim_line_lw", "ByLayer"),
ro("Dim line color", "dim_line_color", "ByLayer"),
2026-03-22 14:56:27 +03:00
];
let text = vec![
ro("Text offset", "text_offset", String::new()),
ro("Text pos vert", "text_pos_vert", String::new()),
];
let fit = vec![ro("Dim scale overall", "dim_scale_overall", String::new())];
vec![
PropSection {
title: "Geometry".into(),
props: geometry,
},
PropSection {
title: "Misc".into(),
props: misc,
},
PropSection {
title: "Lines & Arrows".into(),
props: lines_arrows,
},
PropSection {
title: "Text".into(),
props: text,
},
PropSection {
title: "Fit".into(),
props: fit,
},
]
2026-03-22 14:56:27 +03:00
}
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)
};
2026-03-22 14:56:27 +03:00
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;
}
2026-03-22 14:56:27 +03:00
"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,
};
}
2026-05-12 10:36:15 +03:00
"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"
}
}
2026-03-22 14:56:27 +03:00
"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)) {
2026-03-22 14:56:27 +03:00
vert.x = v;
}
}
"vertex_y" => {
if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(vi)) {
2026-03-22 14:56:27 +03:00
vert.y = v;
}
}
"vertex_z" => {
if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(vi)) {
2026-03-22 14:56:27 +03:00
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| {
2026-03-22 14:56:27 +03:00
for v in &mut entity.vertices {
crate::scene::view::transform::reflect_xy_point(&mut v.x, &mut v.y, p1, p2);
2026-03-22 14:56:27 +03:00
}
crate::scene::view::transform::reflect_xy_point(
2026-03-22 14:56:27 +03:00
&mut entity.block_offset.x,
&mut entity.block_offset.y,
p1,
p2,
);
crate::scene::view::transform::reflect_xy_point(
2026-03-22 14:56:27 +03:00
&mut entity.annotation_offset.x,
&mut entity.annotation_offset.y,
p1,
p2,
);
});
}
// ── Trait impls ────────────────────────────────────────────────────────────
impl TruckConvertible for Leader {
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
2026-05-12 10:36:15 +03:00
if self.vertices.is_empty() {
return None;
}
2026-03-22 14:56:27 +03:00
Some(to_truck(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};
feat(properties): editable per-object leader dimension overrides A legacy leader's Lines & Arrows / Text / Fit rows are now editable per-object overrides of its dimension style, stored in the standard ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow block, arrow size, dim-line lineweight, text offset, vertical text position and overall scale each prefer an override over the style; the renderer honours arrow size / block / overall scale and the dim-line lineweight, so an edited leader redraws at its new arrow and weight. XDATA edits go through a new dispatch::set_entity_xdata, which registers the application in the APPID table (the DWG writer drops records for an unregistered app) and drops that app's stale verbatim EED block (which otherwise wins over the structured record on a DWG save), so edits and clears round-trip. The hyperlink editor moves onto this path too. Dim-line colour stays read-only: it would live in the leader's override_color, which the file layer doesn't serialise, so making it editable would silently lose the pick on save. Picking "Closed filled" writes an explicit null-handle arrow override rather than clearing, so it sticks even when the style's arrow differs; an unparseable numeric entry is ignored instead of wiping the stored override. Note: the DXF entity writer only emits XDATA for hatches, so these overrides (and hyperlinks) currently persist on DWG save but not DXF — an acadrust-side gap to close separately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:28:42 +03:00
use crate::entities::dim_override as dov;
use crate::scene::model::wire_model::WireModel;
feat(properties): editable per-object leader dimension overrides A legacy leader's Lines & Arrows / Text / Fit rows are now editable per-object overrides of its dimension style, stored in the standard ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow block, arrow size, dim-line lineweight, text offset, vertical text position and overall scale each prefer an override over the style; the renderer honours arrow size / block / overall scale and the dim-line lineweight, so an edited leader redraws at its new arrow and weight. XDATA edits go through a new dispatch::set_entity_xdata, which registers the application in the APPID table (the DWG writer drops records for an unregistered app) and drops that app's stale verbatim EED block (which otherwise wins over the structured record on a DWG save), so edits and clears round-trip. The hyperlink editor moves onto this path too. Dim-line colour stays read-only: it would live in the leader's override_color, which the file layer doesn't serialise, so making it editable would silently lose the pick on save. Picking "Closed filled" writes an explicit null-handle arrow override rather than clearing, so it sticks even when the style's arrow differs; an unparseable numeric entry is ignored instead of wiping the stored override. Note: the DXF entity writer only emits XDATA for hatches, so these overrides (and hyperlinks) currently persist on DWG save but not DXF — an acadrust-side gap to close separately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:28:42 +03:00
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,
}
};
feat(properties): editable per-object leader dimension overrides A legacy leader's Lines & Arrows / Text / Fit rows are now editable per-object overrides of its dimension style, stored in the standard ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow block, arrow size, dim-line lineweight, text offset, vertical text position and overall scale each prefer an override over the style; the renderer honours arrow size / block / overall scale and the dim-line lineweight, so an edited leader redraws at its new arrow and weight. XDATA edits go through a new dispatch::set_entity_xdata, which registers the application in the APPID table (the DWG writer drops records for an unregistered app) and drops that app's stale verbatim EED block (which otherwise wins over the structured record on a DWG save), so edits and clears round-trip. The hyperlink editor moves onto this path too. Dim-line colour stays read-only: it would live in the leader's override_color, which the file layer doesn't serialise, so making it editable would silently lose the pick on save. Picking "Closed filled" writes an explicit null-handle arrow override rather than clearing, so it sticks even when the style's arrow differs; an unparseable numeric entry is ignored instead of wiping the stored override. Note: the DXF entity writer only emits XDATA for hatches, so these overrides (and hyperlinks) currently persist on DWG save but not DXF — an acadrust-side gap to close separately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:28:42 +03:00
// 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 nan = [f32::NAN; 3];
let verts = &self.vertices;
if verts.len() < 2 {
return WireModel {
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,
vp_scissor: None,
fill_tris: vec![],
fill_tris_low: Vec::new(),
};
}
let mut points: Vec<[f32; 3]> = verts.iter().map(|v| p3(v)).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<[f32; 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"))
});
feat(properties): editable per-object leader dimension overrides A legacy leader's Lines & Arrows / Text / Fit rows are now editable per-object overrides of its dimension style, stored in the standard ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow block, arrow size, dim-line lineweight, text offset, vertical text position and overall scale each prefer an override over the style; the renderer honours arrow size / block / overall scale and the dim-line lineweight, so an edited leader redraws at its new arrow and weight. XDATA edits go through a new dispatch::set_entity_xdata, which registers the application in the APPID table (the DWG writer drops records for an unregistered app) and drops that app's stale verbatim EED block (which otherwise wins over the structured record on a DWG save), so edits and clears round-trip. The hyperlink editor moves onto this path too. Dim-line colour stays read-only: it would live in the leader's override_color, which the file layer doesn't serialise, so making it editable would silently lose the pick on save. Picking "Closed filled" writes an explicit null-handle arrow override rather than clearing, so it sticks even when the style's arrow differs; an unparseable numeric entry is ignored instead of wiping the stored override. Note: the DXF entity writer only emits XDATA for hatches, so these overrides (and hyperlinks) currently persist on DWG save but not DXF — an acadrust-side gap to close separately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:28:42 +03:00
// 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);
feat(properties): editable per-object leader dimension overrides A legacy leader's Lines & Arrows / Text / Fit rows are now editable per-object overrides of its dimension style, stored in the standard ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow block, arrow size, dim-line lineweight, text offset, vertical text position and overall scale each prefer an override over the style; the renderer honours arrow size / block / overall scale and the dim-line lineweight, so an edited leader redraws at its new arrow and weight. XDATA edits go through a new dispatch::set_entity_xdata, which registers the application in the APPID table (the DWG writer drops records for an unregistered app) and drops that app's stale verbatim EED block (which otherwise wins over the structured record on a DWG save), so edits and clears round-trip. The hyperlink editor moves onto this path too. Dim-line colour stays read-only: it would live in the leader's override_color, which the file layer doesn't serialise, so making it editable would silently lose the pick on save. Picking "Closed filled" writes an explicit null-handle arrow override rather than clearing, so it sticks even when the style's arrow differs; an unparseable numeric entry is ignored instead of wiping the stored override. Note: the DXF entity writer only emits XDATA for hatches, so these overrides (and hyperlinks) currently persist on DWG save but not DXF — an acadrust-side gap to close separately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:28:42 +03:00
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,
};
feat(properties): editable per-object leader dimension overrides A legacy leader's Lines & Arrows / Text / Fit rows are now editable per-object overrides of its dimension style, stored in the standard ACAD_DSTYLE XDATA record (new src/entities/dim_override.rs codec). Arrow block, arrow size, dim-line lineweight, text offset, vertical text position and overall scale each prefer an override over the style; the renderer honours arrow size / block / overall scale and the dim-line lineweight, so an edited leader redraws at its new arrow and weight. XDATA edits go through a new dispatch::set_entity_xdata, which registers the application in the APPID table (the DWG writer drops records for an unregistered app) and drops that app's stale verbatim EED block (which otherwise wins over the structured record on a DWG save), so edits and clears round-trip. The hyperlink editor moves onto this path too. Dim-line colour stays read-only: it would live in the leader's override_color, which the file layer doesn't serialise, so making it editable would silently lose the pick on save. Picking "Closed filled" writes an explicit null-handle arrow override rather than clearing, so it sticks even when the style's arrow differs; an unparseable numeric entry is ignored instead of wiping the stored override. Note: the DXF entity writer only emits XDATA for hatches, so these overrides (and hyperlinks) currently persist on DWG save but not DXF — an acadrust-side gap to close separately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:28:42 +03:00
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);
let tip_f = p3(tip);
let tip_v = Vec3::new(tip_f[0], tip_f[1], tip_f[2]);
// Reuse the dim arrow emitter so the leader shape matches the
// DIMSTYLE in use (Closed Filled by default, Dot, Tick, …).
let mut arrow_pts: Vec<[f32; 3]> = Vec::new();
let mut arrow_geom = DimGeom::new();
append_arrow(&mut arrow_geom, tip_v, dir, &arrow);
if !arrow_geom.dim_lines.is_empty() {
arrow_pts.push(nan);
arrow_pts.extend(arrow_geom.dim_lines);
}
points.extend(arrow_pts);
fill_tris.extend(arrow_geom.arrow_fill);
}
if self.hookline_enabled {
let last = verts.last().unwrap();
let prev = &verts[verts.len() - 2];
let sign = if (last.x - prev.x) >= 0.0 {
1.0_f32
} else {
-1.0_f32
};
let land_len = self.text_height as f32 * 1.5 * anno_scale;
let last_f = p3(last);
points.push(nan);
points.push(last_f);
points.push([last_f[0] + sign * land_len, last_f[1], last_f[2]]);
}
WireModel {
text_verts: Vec::new(),
name,
points,
points_low: Vec::new(),
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,
vp_scissor: None,
fill_tris,
fill_tris_low: Vec::new(),
}
}
}