// MLEADERADD / MLEADERREMOVE / MLEADERALIGN / MLEADERCOLLECT commands. // // MLEADERADD: pick a multileader → pick new arrowhead point → adds a leader line // MLEADERREMOVE: pick a multileader → pick a leader line to remove // MLEADERALIGN: select multileaders → pick base alignment direction // MLEADERCOLLECT: select block-content multileaders → pick collection point use acadrust::entities::{LeaderLine, MultiLeader}; use acadrust::types::Vector3; use acadrust::{EntityType, Handle}; use glam::Vec3; use crate::command::{CadCommand, CmdResult}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::wire_model::WireModel; // ── MLEADERADD ──────────────────────────────────────────────────────────────── pub const ICON_ADD: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/mleader_add.svg")); pub fn tool_add() -> ToolDef { ToolDef { id: "MLEADERADD", label: "Add Leader", icon: ICON_ADD, event: ModuleEvent::Command("MLEADERADD".to_string()), } } enum AddStep { PickMLeader, PickArrowhead { handle: Handle, entity: Option }, CollectPoints { handle: Handle, entity: EntityType, pts: Vec }, } pub struct MLeaderAddCommand { step: AddStep, } impl MLeaderAddCommand { pub fn new() -> Self { Self { step: AddStep::PickMLeader } } } impl CadCommand for MLeaderAddCommand { fn name(&self) -> &'static str { "MLEADERADD" } fn prompt(&self) -> String { match &self.step { AddStep::PickMLeader => "MLEADERADD Select a multileader:".into(), AddStep::PickArrowhead { .. } => "MLEADERADD Specify arrowhead location:".into(), AddStep::CollectPoints { pts, .. } => format!("MLEADERADD Specify next leader point ({} pts, Enter to finish):", pts.len()), } } fn needs_entity_pick(&self) -> bool { matches!(self.step, AddStep::PickMLeader) } fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult { if handle.is_null() { return CmdResult::NeedPoint; } self.step = AddStep::PickArrowhead { handle, entity: None }; CmdResult::NeedPoint } fn on_point(&mut self, pt: Vec3) -> CmdResult { match &mut self.step { AddStep::PickArrowhead { handle, entity } => { if let Some(ent) = entity.take() { let h = *handle; self.step = AddStep::CollectPoints { handle: h, entity: ent, pts: vec![pt] }; return CmdResult::NeedPoint; } CmdResult::NeedPoint } AddStep::CollectPoints { pts, .. } => { pts.push(pt); CmdResult::NeedPoint } _ => CmdResult::NeedPoint, } } fn on_enter(&mut self) -> CmdResult { if let AddStep::CollectPoints { handle, entity, pts } = &mut self.step { if pts.len() < 1 { return CmdResult::Cancel; } let h = *handle; if let EntityType::MultiLeader(ref mut ml) = entity { // Add a new leader root with the collected points let points: Vec = pts.iter() .map(|p| Vector3::new(p.x as f64, p.y as f64, p.z as f64)) .collect(); let root = ml.context.add_leader_root(); root.create_line(points); } let updated = std::mem::replace(entity, EntityType::XLine(Default::default())); return CmdResult::ReplaceEntity(h, vec![updated]); } CmdResult::Cancel } fn on_mouse_move(&mut self, pt: Vec3) -> Option { let existing_pts = match &self.step { AddStep::CollectPoints { pts, .. } => pts.clone(), _ => return None, }; let mut all_pts = existing_pts; all_pts.push(pt); Some(preview_wire(&all_pts)) } fn inject_picked_entity(&mut self, entity: EntityType) { if let AddStep::PickArrowhead { entity: slot, .. } = &mut self.step { *slot = Some(entity); } } } // ── MLEADERREMOVE ───────────────────────────────────────────────────────────── pub const ICON_REMOVE: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/mleader_remove.svg")); pub fn tool_remove() -> ToolDef { ToolDef { id: "MLEADERREMOVE", label: "Remove Leader", icon: ICON_REMOVE, event: ModuleEvent::Command("MLEADERREMOVE".to_string()), } } enum RemoveStep { PickMLeader, PickLeaderToRemove { handle: Handle, entity: Option }, } pub struct MLeaderRemoveCommand { step: RemoveStep, } impl MLeaderRemoveCommand { pub fn new() -> Self { Self { step: RemoveStep::PickMLeader } } } impl CadCommand for MLeaderRemoveCommand { fn name(&self) -> &'static str { "MLEADERREMOVE" } fn prompt(&self) -> String { match &self.step { RemoveStep::PickMLeader => "MLEADERREMOVE Select a multileader:".into(), RemoveStep::PickLeaderToRemove { .. } => "MLEADERREMOVE Click near the leader line to remove:".into(), } } fn needs_entity_pick(&self) -> bool { matches!(self.step, RemoveStep::PickMLeader) } fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult { if handle.is_null() { return CmdResult::NeedPoint; } self.step = RemoveStep::PickLeaderToRemove { handle, entity: None }; CmdResult::NeedPoint } fn on_point(&mut self, pt: Vec3) -> CmdResult { if let RemoveStep::PickLeaderToRemove { handle, entity } = &mut self.step { if let Some(mut ent) = entity.take() { let h = *handle; if let EntityType::MultiLeader(ref mut ml) = ent { // Remove the leader root whose first point is closest to `pt` let pick = Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64); let best = ml.context.leader_roots.iter().enumerate() .filter_map(|(i, root)| { root.lines.first() .and_then(|l| l.points.first()) .map(|p| { let dx = p.x - pick.x; let dz = p.z - pick.z; (i, dx * dx + dz * dz) }) }) .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)); if let Some((idx, _)) = best { if ml.context.leader_roots.len() > 1 { ml.context.leader_roots.remove(idx); } } } return CmdResult::ReplaceEntity(h, vec![ent]); } } CmdResult::NeedPoint } fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } fn on_mouse_move(&mut self, _pt: Vec3) -> Option { None } fn inject_picked_entity(&mut self, entity: EntityType) { if let RemoveStep::PickLeaderToRemove { entity: slot, .. } = &mut self.step { *slot = Some(entity); } } } // ── MLEADERALIGN ───────────────────────────────────────────────────────────── pub const ICON_ALIGN: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/mleader_align.svg")); pub fn tool_align() -> ToolDef { ToolDef { id: "MLEADERALIGN", label: "Align Leaders", icon: ICON_ALIGN, event: ModuleEvent::Command("MLEADERALIGN".to_string()), } } enum AlignStep { Gathering, PickDirection { handles: Vec }, PickEndDir { handles: Vec, from: Vec3 }, } pub struct MLeaderAlignCommand { step: AlignStep, } impl MLeaderAlignCommand { pub fn new() -> Self { Self { step: AlignStep::Gathering } } } impl CadCommand for MLeaderAlignCommand { fn name(&self) -> &'static str { "MLEADERALIGN" } fn prompt(&self) -> String { match &self.step { AlignStep::Gathering => "MLEADERALIGN Select multileaders to align (Enter when done):".into(), AlignStep::PickDirection { .. } => "MLEADERALIGN Specify direction — pick start point:".into(), AlignStep::PickEndDir { .. } => "MLEADERALIGN Specify end point of alignment direction:".into(), } } fn is_selection_gathering(&self) -> bool { matches!(self.step, AlignStep::Gathering) } fn on_selection_complete(&mut self, handles: Vec) -> CmdResult { if handles.is_empty() { return CmdResult::Cancel; } self.step = AlignStep::PickDirection { handles }; CmdResult::NeedPoint } fn on_point(&mut self, pt: Vec3) -> CmdResult { match &mut self.step { AlignStep::PickDirection { handles } => { let h = handles.clone(); self.step = AlignStep::PickEndDir { handles: h, from: pt }; CmdResult::NeedPoint } AlignStep::PickEndDir { handles, from } => { let h = handles.clone(); let f = *from; // Emit sentinel for commands.rs use acadrust::entities::XLine; let mut xl = XLine::default(); let hstr: Vec = h.iter().map(|hh| hh.value().to_string()).collect(); xl.common.layer = format!("__MLEADERALIGN__{};{:.4},{:.4};{:.4},{:.4}", hstr.join(","), f.x, f.z, pt.x, pt.z); // Use first handle as the "replaced" entity (sentinel) if let Some(&first) = h.first() { return CmdResult::ReplaceEntity(first, vec![EntityType::XLine(xl)]); } CmdResult::Cancel } _ => CmdResult::NeedPoint, } } fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } fn on_mouse_move(&mut self, _pt: Vec3) -> Option { None } } // ── MLEADERCOLLECT ──────────────────────────────────────────────────────────── pub const ICON_COLLECT: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/mleader_collect.svg")); pub fn tool_collect() -> ToolDef { ToolDef { id: "MLEADERCOLLECT", label: "Collect Leaders", icon: ICON_COLLECT, event: ModuleEvent::Command("MLEADERCOLLECT".to_string()), } } enum CollectStep { Gathering, PickLocation { handles: Vec }, } pub struct MLeaderCollectCommand { step: CollectStep, } impl MLeaderCollectCommand { pub fn new() -> Self { Self { step: CollectStep::Gathering } } } impl CadCommand for MLeaderCollectCommand { fn name(&self) -> &'static str { "MLEADERCOLLECT" } fn prompt(&self) -> String { match &self.step { CollectStep::Gathering => "MLEADERCOLLECT Select multileaders to collect (Enter when done):".into(), CollectStep::PickLocation { .. } => "MLEADERCOLLECT Specify collected multileader location:".into(), } } fn is_selection_gathering(&self) -> bool { matches!(self.step, CollectStep::Gathering) } fn on_selection_complete(&mut self, handles: Vec) -> CmdResult { if handles.is_empty() { return CmdResult::Cancel; } self.step = CollectStep::PickLocation { handles }; CmdResult::NeedPoint } fn on_point(&mut self, pt: Vec3) -> CmdResult { if let CollectStep::PickLocation { handles } = &self.step { let h = handles.clone(); // Build a new combined multileader at the collection point, delete originals. // The first handle is the "base" — the rest are erased and their leaders merged. use acadrust::entities::XLine; let mut xl = XLine::default(); let hstr: Vec = h.iter().map(|hh| hh.value().to_string()).collect(); xl.common.layer = format!("__MLEADERCOLLECT__{};{:.4},{:.4}", hstr.join(","), pt.x, pt.z); if let Some(&first) = h.first() { return CmdResult::ReplaceEntity(first, vec![EntityType::XLine(xl)]); } } CmdResult::NeedPoint } fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } fn on_mouse_move(&mut self, _pt: Vec3) -> Option { None } } // ── Shared helpers ──────────────────────────────────────────────────────────── fn preview_wire(pts: &[Vec3]) -> WireModel { WireModel { name: "mleader_edit_preview".into(), points: pts.iter().map(|p| [p.x, p.y, p.z]).collect(), color: WireModel::CYAN, selected: false, pattern_length: 0.0, pattern: [0.0; 8], line_weight_px: 1.0, snap_pts: vec![], tangent_geoms: vec![], aci: 0, key_vertices: vec![], aabb: WireModel::UNBOUNDED_AABB, plinegen: true, vp_scissor: None, fill_tris: vec![], } } // Silence unused-import warning for MultiLeader and LeaderLine if not used in all paths fn _uses_ml_types(_ml: &MultiLeader, _ll: &LeaderLine) {}