Files
OneCAD-Draft/src/entities/leader.rs
T
Hakan SevenandClaude Opus 4.8 110683f419 refactor(text): per-entity SDF text — all text kinds, selection/hover highlight
Rearchitect SDF text from a document-wide collector to per-entity
production during tessellation, so each entity owns its glyph quads and
composite/nested text is covered for free (no re-explosion, no per-frame
document walk).

- WireModel gains `text_verts`: glyph quads ride on the wire, so they are
  cached by the tess memo, cloned on hit, and transformed by the block
  expand loop exactly like `points`.
- Text arm (tessellate.rs) builds the quads when SDF is on and suppresses
  the strokes; render gathers text_verts from the viewport wire set
  (`gather_text_verts`, cached on wire content id) — the old collector is
  removed. Covers model, dimension, table, block-internal and paper-sheet
  text uniformly.
- MLEADER text (lays out via layout_mtext, not the Text arm) gets its own
  SDF branch driven by the per-run GlyphRun.
- Pick box comes from the rendered glyph quads (true text extent), not
  entity_aabb which mis-places the box for MTEXT; composite paths no longer
  clobber the empty text wire's tight AABB (dim pick box hugs the text).
- Selection / hover highlight via a text-highlight overlay (upload_text_
  highlight, keyed on selection_generation) mirroring the selected-wire
  xray overlay; base glyphs stay neutral so a deselect leaves no stale tint.
- Debug: env OCS_TEXT_BOX draws each run's glyph-bounds rectangle.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-07 17:01:36 +03:00

687 lines
23 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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};
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;
// ── 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] };
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 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;
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],
]);
}
TruckEntity {
object: TruckObject::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 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 bool_toggle(label: &str, field: &'static str, value: bool) -> Property {
Property {
label: label.into(),
field,
value: PropValue::BoolToggle { field, value },
}
}
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(),
},
}
}
fn path_type_str(pt: &LeaderPathType) -> &'static str {
match pt {
LeaderPathType::StraightLine => "Straight",
LeaderPathType::Spline => "Spline",
}
}
fn creation_type_str(ct: &LeaderCreationType) -> &'static str {
match ct {
LeaderCreationType::WithText => "With Text",
LeaderCreationType::WithTolerance => "With Tolerance",
LeaderCreationType::WithBlock => "With Block",
LeaderCreationType::NoAnnotation => "No Annotation",
}
}
fn color_str(c: &acadrust::types::Color) -> String {
match c.rgb() {
Some((r, g, b)) => format!("RGB({},{},{})", r, g, b),
None => format!("{:?}", c),
}
}
fn properties(leader: &Leader) -> Vec<PropSection> {
let n = leader.vertices.len();
let arrow_size = leader.text_height.max(1.0);
let misc = vec![
choice_prop(
"Type",
"path_type",
path_type_str(&leader.path_type),
&["Straight", "Spline"],
),
choice_prop(
"Annotation type",
"creation_type",
creation_type_str(&leader.creation_type),
&["With Text", "With Tolerance", "With Block", "No Annotation"],
),
Property {
label: "Dim style".into(),
field: "dimension_style",
value: PropValue::EditText(leader.dimension_style.clone()),
},
];
let lines_arrows = vec![
ro("Dim line color", "override_color", color_str(&leader.override_color)),
ro(
"Dim line lineweight",
"line_weight",
format!("{:?}", leader.common.line_weight),
),
ro("Dim line linetype", "linetype", leader.common.linetype.clone()),
bool_toggle("Arrowhead", "arrow_enabled", leader.arrow_enabled),
ro("Arrow size", "arrow_size", format!("{:.4}", arrow_size)),
];
let text = vec![
edit("Text height", "text_height", leader.text_height),
ro(
"Text offset",
"text_offset",
format!("{:.4}", leader.annotation_offset.length()),
),
ro("Text style", "text_style", String::new()),
ro("Text color", "text_color", color_str(&leader.override_color)),
ro("Text position vert", "text_pos_vert", String::new()),
];
let fit = vec![ro("Dim scale overall", "dim_scale_overall", String::new())];
let mut geometry = vec![ro("Vertex", "vertex_count", n.to_string())];
if let Some(a) = leader.arrow_point() {
geometry.push(edit("Vertex X", "vertex_x", a.x));
geometry.push(edit("Vertex Y", "vertex_y", a.y));
geometry.push(edit("Vertex Z", "vertex_z", a.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()));
}
vec![
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,
},
PropSection {
title: "Geometry".into(),
props: geometry,
},
]
}
fn apply_geom_prop(leader: &mut Leader, field: &str, value: &str) {
let f64 = |s: &str| -> Option<f64> { s.trim().parse().ok() };
match field {
"dimension_style" => leader.dimension_style = value.to_string(),
"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(0)) {
vert.x = v;
}
}
"vertex_y" => {
if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(0)) {
vert.y = v;
}
}
"vertex_z" => {
if let (Some(v), Some(vert)) = (f64(value), leader.vertices.get_mut(0)) {
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 TruckConvertible for Leader {
fn to_truck(&self, _document: &acadrust::CadDocument) -> Option<TruckEntity> {
if self.vertices.is_empty() {
return None;
}
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};
use crate::scene::model::wire_model::WireModel;
let color = if selected {
WireModel::SELECTED
} else {
entity_color
};
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"))
});
let dim_scale = style
.map(|s| {
if s.dimscale > 1e-6 {
s.dimscale
} else {
anno_scale as f64
}
})
.unwrap_or(anno_scale as f64);
let arrow_size = match style {
Some(s) => (s.dimasz * dim_scale) as f32,
None => (self.text_height as f32).max(1.0) * anno_scale,
};
let arrow = match style {
Some(s) => arrow_from_block(document, s.dimldrblk, 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(),
}
}
}