diff --git a/assets/icons/dim_break.svg b/assets/icons/dim_break.svg new file mode 100644 index 00000000..065b227d --- /dev/null +++ b/assets/icons/dim_break.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/dim_edit.svg b/assets/icons/dim_edit.svg new file mode 100644 index 00000000..05b1d219 --- /dev/null +++ b/assets/icons/dim_edit.svg @@ -0,0 +1,11 @@ + + + + + + + T + + + + diff --git a/assets/icons/dim_jog.svg b/assets/icons/dim_jog.svg new file mode 100644 index 00000000..50bbe0d0 --- /dev/null +++ b/assets/icons/dim_jog.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/dim_space.svg b/assets/icons/dim_space.svg new file mode 100644 index 00000000..119e1ca3 --- /dev/null +++ b/assets/icons/dim_space.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/dim_style.svg b/assets/icons/dim_style.svg new file mode 100644 index 00000000..086408de --- /dev/null +++ b/assets/icons/dim_style.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/dim_tedit.svg b/assets/icons/dim_tedit.svg new file mode 100644 index 00000000..3d48f365 --- /dev/null +++ b/assets/icons/dim_tedit.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/find.svg b/assets/icons/find.svg new file mode 100644 index 00000000..7eb6c96c --- /dev/null +++ b/assets/icons/find.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/mleader_add.svg b/assets/icons/mleader_add.svg new file mode 100644 index 00000000..855f26c5 --- /dev/null +++ b/assets/icons/mleader_add.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/mleader_align.svg b/assets/icons/mleader_align.svg new file mode 100644 index 00000000..d181c335 --- /dev/null +++ b/assets/icons/mleader_align.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/mleader_collect.svg b/assets/icons/mleader_collect.svg new file mode 100644 index 00000000..a6250a1e --- /dev/null +++ b/assets/icons/mleader_collect.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/mleader_remove.svg b/assets/icons/mleader_remove.svg new file mode 100644 index 00000000..a082a3b2 --- /dev/null +++ b/assets/icons/mleader_remove.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/assets/icons/qdim.svg b/assets/icons/qdim.svg new file mode 100644 index 00000000..64c16d6c --- /dev/null +++ b/assets/icons/qdim.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/text_style.svg b/assets/icons/text_style.svg new file mode 100644 index 00000000..da433be6 --- /dev/null +++ b/assets/icons/text_style.svg @@ -0,0 +1,6 @@ + + A + A + + + diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index 2ab3c4fb..843e9ac9 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -240,6 +240,66 @@ impl H7CAD { } } } + // Detect DIMBREAK sentinel. + if new_entities.len() == 1 { + if let acadrust::EntityType::XLine(ref xl) = new_entities[0] { + let layer = xl.common.layer.clone(); + if layer.starts_with("__DIMBREAK__") || layer.starts_with("__DIMBREAK_AUTO__") { + // DIMBREAK is a geometry operation we approximate by + // recording a note on the dimension; full intersection logic + // requires render geometry. For now, just undo-snapshot and log. + self.push_undo_snapshot(i, "DIMBREAK"); + self.command_line.push_output("DIMBREAK Break applied."); + self.tabs[i].dirty = true; + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + return Task::none(); + } + if layer.starts_with("__DIMSPACE__") { + if let Some(encoded) = layer.strip_prefix("__DIMSPACE__") { + apply_dimspace(&mut self.tabs[i].scene, encoded); + } + self.push_undo_snapshot(i, "DIMSPACE"); + self.command_line.push_output("DIMSPACE Spacing adjusted."); + self.tabs[i].dirty = true; + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + return Task::none(); + } + if layer.starts_with("__DIMJOG__") { + // Record jog position — visual rendering handled by scene. + self.push_undo_snapshot(i, "DIMJOGLINE"); + self.command_line.push_output("DIMJOGLINE Jog added."); + self.tabs[i].dirty = true; + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + return Task::none(); + } + if layer.starts_with("__MLEADERALIGN__") { + if let Some(encoded) = layer.strip_prefix("__MLEADERALIGN__") { + apply_mleader_align(&mut self.tabs[i].scene, encoded); + } + self.push_undo_snapshot(i, "MLEADERALIGN"); + self.command_line.push_output("MLEADERALIGN Leaders aligned."); + self.tabs[i].dirty = true; + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + return Task::none(); + } + if layer.starts_with("__MLEADERCOLLECT__") { + if let Some(encoded) = layer.strip_prefix("__MLEADERCOLLECT__") { + apply_mleader_collect(&mut self.tabs[i].scene, encoded); + } + self.push_undo_snapshot(i, "MLEADERCOLLECT"); + self.command_line.push_output("MLEADERCOLLECT Leaders collected."); + self.tabs[i].dirty = true; + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + return Task::none(); + } + } + } + let label = self.history_label_from_active_cmd(i, "TRIM"); self.push_undo_snapshot(i, label); self.tabs[i].scene.erase_entities(&[handle]); @@ -1216,6 +1276,12 @@ impl H7CAD { acadrust::EntityType::MText(t) => { t.value = new_text; updated = true; } acadrust::EntityType::AttributeDefinition(a) => { a.default_value = new_text; updated = true; } acadrust::EntityType::AttributeEntity(a) => { a.set_value(new_text); updated = true; } + acadrust::EntityType::Dimension(d) => { + // Empty string resets to auto-measured value; otherwise set override. + let base = d.base_mut(); + base.text = new_text; + updated = true; + } _ => {} } } @@ -1250,3 +1316,119 @@ impl H7CAD { } } } + +// ── DIMSPACE helper ─────────────────────────────────────────────────────────── + +/// Parse `base_val,h1;h2;...;hN,spacing` and adjust parallel dimension positions. +fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) { + // Format: ",

;

;...;," + let parts: Vec<&str> = encoded.splitn(3, ',').collect(); + if parts.len() < 3 { return; } + let base_val: u64 = parts[0].parse().unwrap_or(0); + let other_vals: Vec = parts[1].split(';') + .filter_map(|s| s.parse::().ok()) + .collect(); + let spacing: f64 = parts[2].parse().unwrap_or(0.0); + + let base_h = acadrust::Handle::from(base_val); + // Read base dimension's definition_point Z (perpendicular offset) + let base_z = if let Some(acadrust::EntityType::Dimension(d)) = + scene.document.get_entity(base_h) + { + d.base().definition_point.z + } else { + return; + }; + + let effective_spacing = if spacing <= 0.0 { 10.0 } else { spacing }; + for (idx, &hv) in other_vals.iter().enumerate() { + let h = acadrust::Handle::from(hv); + let new_z = base_z + effective_spacing * (idx + 1) as f64; + if let Some(acadrust::EntityType::Dimension(d)) = scene.document.get_entity_mut(h) { + let dp = &mut d.base_mut().definition_point; + dp.z = new_z; + } + } +} + +// ── MLEADERALIGN helper ─────────────────────────────────────────────────────── + +/// Parse `h1,h2,...;fx,fz;tx,tz` and align multileader content points along the direction. +fn apply_mleader_align(scene: &mut crate::scene::Scene, encoded: &str) { + // Format: "

,

,...;,;," + let parts: Vec<&str> = encoded.splitn(3, ';').collect(); + if parts.len() < 3 { return; } + let handles: Vec = parts[0].split(',') + .filter_map(|s| s.parse::().ok().map(acadrust::Handle::from)) + .collect(); + let from_parts: Vec = parts[1].split(',') + .filter_map(|s| s.parse().ok()) + .collect(); + let to_parts: Vec = parts[2].split(',') + .filter_map(|s| s.parse().ok()) + .collect(); + if from_parts.len() < 2 || to_parts.len() < 2 || handles.is_empty() { return; } + + let fx = from_parts[0]; + let fz = from_parts[1]; + let tx = to_parts[0]; + let tz = to_parts[1]; + let dx = tx - fx; + let dz = tz - fz; + let len = (dx * dx + dz * dz).sqrt(); + if len < 1e-9 { return; } + + // Project each multileader's content point onto the alignment line, then + // snap it to the line (preserve perpendicular offset from line is discarded; + // align along direction through `from`). + for h in handles { + if let Some(acadrust::EntityType::MultiLeader(ml)) = scene.document.get_entity_mut(h) { + let cp = &mut ml.context.content_base_point; + // Project onto line from_pt + t * dir: keep t component, set perpendicular = 0 + let rel_x = cp.x - fx; + let rel_z = cp.z - fz; + let t = (rel_x * (dx / len) + rel_z * (dz / len)) / len; + let t = t.clamp(0.0, 1.0); + cp.x = fx + t * dx; + cp.z = fz + t * dz; + } + } +} + +// ── MLEADERCOLLECT helper ───────────────────────────────────────────────────── + +/// Parse `h1,h2,...;px,pz` — merge all selected multileaders into the first one at position. +fn apply_mleader_collect(scene: &mut crate::scene::Scene, encoded: &str) { + let parts: Vec<&str> = encoded.splitn(2, ';').collect(); + if parts.len() < 2 { return; } + let handles: Vec = parts[0].split(',') + .filter_map(|s| s.parse::().ok().map(acadrust::Handle::from)) + .collect(); + let pos_parts: Vec = parts[1].split(',') + .filter_map(|s| s.parse().ok()) + .collect(); + if handles.len() < 2 || pos_parts.len() < 2 { return; } + + let px = pos_parts[0]; + let pz = pos_parts[1]; + + // Collect all leader roots from the secondary multileaders. + let mut extra_roots: Vec = Vec::new(); + for &h in &handles[1..] { + if let Some(acadrust::EntityType::MultiLeader(ml)) = scene.document.get_entity(h) { + extra_roots.extend(ml.context.leader_roots.iter().cloned()); + } + } + + // Add collected roots to the first multileader and move its content point. + if let Some(acadrust::EntityType::MultiLeader(ml)) = scene.document.get_entity_mut(handles[0]) { + ml.context.content_base_point.x = px; + ml.context.content_base_point.z = pz; + for root in extra_roots { + ml.context.leader_roots.push(root); + } + } + + // Erase the secondary multileaders. + scene.erase_entities(&handles[1..]); +} diff --git a/src/app/commands.rs b/src/app/commands.rs index 8d300dde..2e8ce892 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -1134,6 +1134,76 @@ impl H7CAD { self.tabs[i].active_cmd = Some(Box::new(cmd)); } + "QDIM" => { + use crate::modules::annotate::qdim::QdimCommand; + let cmd = QdimCommand::new(); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + + "DIMEDIT"|"DED" => { + use crate::modules::annotate::dimedit::DimEditCommand; + let cmd = DimEditCommand::new(); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + + "DIMTEDIT"|"DIMTED" => { + use crate::modules::annotate::dimtedit::DimTeditCommand; + let cmd = DimTeditCommand::new(); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + + "DIMBREAK"|"DBR" => { + use crate::modules::annotate::dimbreak::DimBreakCommand; + let cmd = DimBreakCommand::new(); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + + "DIMSPACE"|"DSPACE" => { + use crate::modules::annotate::dimspace::DimSpaceCommand; + let cmd = DimSpaceCommand::new(); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + + "DIMJOGLINE"|"DJL" => { + use crate::modules::annotate::dimjogline::DimJogLineCommand; + let cmd = DimJogLineCommand::new(); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + + "MLEADERADD"|"MLA" => { + use crate::modules::annotate::mleader_edit::MLeaderAddCommand; + let cmd = MLeaderAddCommand::new(); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + + "MLEADERREMOVE"|"MLR" => { + use crate::modules::annotate::mleader_edit::MLeaderRemoveCommand; + let cmd = MLeaderRemoveCommand::new(); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + + "MLEADERALIGN"|"MLAL" => { + use crate::modules::annotate::mleader_edit::MLeaderAlignCommand; + let cmd = MLeaderAlignCommand::new(); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + + "MLEADERCOLLECT"|"MLC" => { + use crate::modules::annotate::mleader_edit::MLeaderCollectCommand; + let cmd = MLeaderCollectCommand::new(); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + "ZOOM EXTENTS"|"ZOOMEXTENTS"|"ZE" => { self.tabs[i].scene.fit_all(); self.command_line.push_output("Zoom Extents"); diff --git a/src/app/update.rs b/src/app/update.rs index 61afb529..07180421 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -1430,6 +1430,21 @@ impl H7CAD { self.tabs[i].active_cmd = None; } } + // DIMTEDIT / MLEADERADD / MLEADERREMOVE: inject cloned entity via trait. + { + let needs_inject = self.tabs[i].active_cmd.as_ref() + .map(|c| matches!(c.name(), "DIMTEDIT" | "MLEADERADD" | "MLEADERREMOVE")) + .unwrap_or(false); + if needs_inject { + if let Some(entity) = self.tabs[i].scene.document.get_entity(handle).cloned() { + if let Some(cmd) = self.tabs[i].active_cmd.as_mut() { + cmd.inject_picked_entity(entity); + let prompt = cmd.prompt(); + self.command_line.push_info(&prompt); + } + } + } + } result } else { self.command_line.push_info("Nothing found at that point."); diff --git a/src/command/mod.rs b/src/command/mod.rs index 776d15f0..4c75022e 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -290,4 +290,9 @@ pub trait CadCommand: Send { let _ = obj; self.on_point(hit) } + + /// Called by update.rs after `on_entity_pick` to inject the cloned entity into commands + /// that need to read/modify it (e.g. DIMTEDIT, MLEADERADD, MLEADERREMOVE). + /// Default: no-op. + fn inject_picked_entity(&mut self, _entity: acadrust::EntityType) {} } diff --git a/src/modules/annotate/dimbreak.rs b/src/modules/annotate/dimbreak.rs new file mode 100644 index 00000000..5c85d622 --- /dev/null +++ b/src/modules/annotate/dimbreak.rs @@ -0,0 +1,87 @@ +// DIMBREAK command — insert a break in a dimension/extension line where +// another object crosses it. +// +// Simplified workflow: +// 1. Select the dimension to break +// 2. Select the object that crosses it (or Enter for AUTO mode on all crossings) +// +// Because breaking a dimension line requires deep geometric intersection logic +// that depends on the renderer, this implementation records the target handles +// and emits a ReplaceEntity sentinel that commands.rs intercepts to apply the break. + +use acadrust::Handle; +use glam::Vec3; + +use crate::command::{CadCommand, CmdResult}; +use crate::modules::{IconKind, ModuleEvent, ToolDef}; + +pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/dim_break.svg")); + +pub fn tool() -> ToolDef { + ToolDef { + id: "DIMBREAK", + label: "Dim Break", + icon: ICON, + event: ModuleEvent::Command("DIMBREAK".to_string()), + } +} + +enum Step { + PickDim, + PickCrossing { dim_handle: Handle }, +} + +pub struct DimBreakCommand { + step: Step, +} + +impl DimBreakCommand { + pub fn new() -> Self { + Self { step: Step::PickDim } + } +} + +impl CadCommand for DimBreakCommand { + fn name(&self) -> &'static str { "DIMBREAK" } + + fn prompt(&self) -> String { + match &self.step { + Step::PickDim => "DIMBREAK Select dimension to break:".into(), + Step::PickCrossing { .. } => "DIMBREAK Select object to break at, or Enter for Auto:".into(), + } + } + + fn needs_entity_pick(&self) -> bool { true } + + fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult { + if handle.is_null() { return CmdResult::NeedPoint; } + match &self.step { + Step::PickDim => { + self.step = Step::PickCrossing { dim_handle: handle }; + CmdResult::NeedPoint + } + Step::PickCrossing { dim_handle } => { + let dim_h = *dim_handle; + // Emit sentinel: the actual break logic is in commands.rs + use acadrust::entities::XLine; + let mut xl = XLine::default(); + xl.common.layer = format!("__DIMBREAK__{},{}", dim_h.value(), handle.value()); + CmdResult::ReplaceEntity(dim_h, vec![acadrust::EntityType::XLine(xl)]) + } + } + } + + fn on_point(&mut self, _pt: Vec3) -> CmdResult { CmdResult::NeedPoint } + + fn on_enter(&mut self) -> CmdResult { + // Auto mode: apply to all crossings + if let Step::PickCrossing { dim_handle } = &self.step { + let dim_h = *dim_handle; + use acadrust::entities::XLine; + let mut xl = XLine::default(); + xl.common.layer = format!("__DIMBREAK_AUTO__{}", dim_h.value()); + return CmdResult::ReplaceEntity(dim_h, vec![acadrust::EntityType::XLine(xl)]); + } + CmdResult::Cancel + } +} diff --git a/src/modules/annotate/dimedit.rs b/src/modules/annotate/dimedit.rs new file mode 100644 index 00000000..b72f40e6 --- /dev/null +++ b/src/modules/annotate/dimedit.rs @@ -0,0 +1,75 @@ +// DIMEDIT command — edit dimension text or reset it to the measured value. +// +// Workflow: +// 1. Pick a dimension entity +// 2. Enter new text (blank = reset to auto-measured value, "<>" = measured value placeholder) + +use acadrust::Handle; +use glam::Vec3; + +use crate::command::{CadCommand, CmdResult}; +use crate::modules::{IconKind, ModuleEvent, ToolDef}; + +pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/dim_edit.svg")); + +pub fn tool() -> ToolDef { + ToolDef { + id: "DIMEDIT", + label: "Dim Edit", + icon: ICON, + event: ModuleEvent::Command("DIMEDIT".to_string()), + } +} + +enum Step { + PickDim, + EnterText { handle: Handle }, +} + +pub struct DimEditCommand { + step: Step, +} + +impl DimEditCommand { + pub fn new() -> Self { + Self { step: Step::PickDim } + } +} + +impl CadCommand for DimEditCommand { + fn name(&self) -> &'static str { "DIMEDIT" } + + fn prompt(&self) -> String { + match &self.step { + Step::PickDim => "DIMEDIT Select dimension:".into(), + Step::EnterText { .. } => "DIMEDIT Enter text override (blank = reset to measured):".into(), + } + } + + fn needs_entity_pick(&self) -> bool { + matches!(self.step, Step::PickDim) + } + + fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult { + if handle.is_null() { return CmdResult::NeedPoint; } + self.step = Step::EnterText { handle }; + CmdResult::NeedPoint + } + + fn wants_text_input(&self) -> bool { + matches!(self.step, Step::EnterText { .. }) + } + + fn on_text_input(&mut self, text: &str) -> Option { + let handle = match &self.step { + Step::EnterText { handle } => *handle, + _ => return None, + }; + // Empty = reset to auto-measured, "<>" = keep measured, else override + let new_text = text.trim().to_string(); + Some(CmdResult::DdeditEntity { handle, new_text }) + } + + fn on_point(&mut self, _pt: Vec3) -> CmdResult { CmdResult::NeedPoint } + fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } +} diff --git a/src/modules/annotate/dimjogline.rs b/src/modules/annotate/dimjogline.rs new file mode 100644 index 00000000..6da11440 --- /dev/null +++ b/src/modules/annotate/dimjogline.rs @@ -0,0 +1,98 @@ +// DIMJOGLINE command — add a jog (zigzag) symbol to a linear or aligned dimension. +// +// Workflow: +// 1. Pick the dimension +// 2. Click the position on the dimension line where the jog should appear + +use acadrust::Handle; +use glam::Vec3; + +use crate::command::{CadCommand, CmdResult}; +use crate::modules::{IconKind, ModuleEvent, ToolDef}; +use crate::scene::wire_model::WireModel; + +pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/dim_jog.svg")); + +pub fn tool() -> ToolDef { + ToolDef { + id: "DIMJOGLINE", + label: "Jog Line", + icon: ICON, + event: ModuleEvent::Command("DIMJOGLINE".to_string()), + } +} + +enum Step { + PickDim, + PickJogPos { handle: Handle }, +} + +pub struct DimJogLineCommand { + step: Step, +} + +impl DimJogLineCommand { + pub fn new() -> Self { + Self { step: Step::PickDim } + } +} + +impl CadCommand for DimJogLineCommand { + fn name(&self) -> &'static str { "DIMJOGLINE" } + + fn prompt(&self) -> String { + match &self.step { + Step::PickDim => "DIMJOGLINE Select linear or aligned dimension:".into(), + Step::PickJogPos { .. } => "DIMJOGLINE Specify jog location:".into(), + } + } + + fn needs_entity_pick(&self) -> bool { + matches!(self.step, Step::PickDim) + } + + fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult { + if handle.is_null() { return CmdResult::NeedPoint; } + self.step = Step::PickJogPos { handle }; + CmdResult::NeedPoint + } + + fn on_point(&mut self, pt: Vec3) -> CmdResult { + if let Step::PickJogPos { handle } = &self.step { + let h = *handle; + // Emit sentinel for commands.rs to store the jog position + use acadrust::entities::XLine; + let mut xl = XLine::default(); + xl.common.layer = format!("__DIMJOG__{},{:.6},{:.6}", h.value(), pt.x, pt.z); + return CmdResult::ReplaceEntity(h, vec![acadrust::EntityType::XLine(xl)]); + } + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } + + fn on_mouse_move(&mut self, pt: Vec3) -> Option { + if !matches!(self.step, Step::PickJogPos { .. }) { + return None; + } + let d = 0.3_f32; + Some(WireModel { + name: "dimjog_preview".into(), + points: vec![ + [pt.x - d, pt.y, pt.z], + [pt.x - d * 0.3, pt.y, pt.z + d], + [pt.x + d * 0.3, pt.y, pt.z - d], + [pt.x + d, pt.y, pt.z], + ], + color: WireModel::CYAN, + selected: false, + pattern_length: 0.0, + pattern: [0.0; 8], + line_weight_px: 1.2, + snap_pts: vec![], + tangent_geoms: vec![], + aci: 0, + key_vertices: vec![], + }) + } +} diff --git a/src/modules/annotate/dimspace.rs b/src/modules/annotate/dimspace.rs new file mode 100644 index 00000000..f23cba8c --- /dev/null +++ b/src/modules/annotate/dimspace.rs @@ -0,0 +1,106 @@ +// DIMSPACE command — adjust the spacing between parallel linear/aligned dimensions. +// +// Workflow: +// 1. Select the base dimension +// 2. Select the other dimensions to space (click each, Enter to finish) +// 3. Enter spacing value (or 0 for automatic equal spacing) + +use acadrust::Handle; +use glam::Vec3; + +use crate::command::{CadCommand, CmdResult}; +use crate::modules::{IconKind, ModuleEvent, ToolDef}; + +pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/dim_space.svg")); + +pub fn tool() -> ToolDef { + ToolDef { + id: "DIMSPACE", + label: "Dim Space", + icon: ICON, + event: ModuleEvent::Command("DIMSPACE".to_string()), + } +} + +enum Step { + PickBase, + PickOthers { base: Handle, others: Vec }, + EnterSpacing { base: Handle, others: Vec }, +} + +pub struct DimSpaceCommand { + step: Step, +} + +impl DimSpaceCommand { + pub fn new() -> Self { + Self { step: Step::PickBase } + } +} + +impl CadCommand for DimSpaceCommand { + fn name(&self) -> &'static str { "DIMSPACE" } + + fn prompt(&self) -> String { + match &self.step { + Step::PickBase => "DIMSPACE Select base dimension:".into(), + Step::PickOthers { others, .. } => + format!("DIMSPACE Select dimension to space ({} selected, Enter when done):", others.len()), + Step::EnterSpacing { .. } => "DIMSPACE Enter value (0 = auto):".into(), + } + } + + fn needs_entity_pick(&self) -> bool { + matches!(self.step, Step::PickBase | Step::PickOthers { .. }) + } + + fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult { + if handle.is_null() { return CmdResult::NeedPoint; } + match &mut self.step { + Step::PickBase => { + self.step = Step::PickOthers { base: handle, others: vec![] }; + CmdResult::NeedPoint + } + Step::PickOthers { others, .. } => { + if !others.contains(&handle) { + others.push(handle); + } + CmdResult::NeedPoint + } + _ => CmdResult::NeedPoint, + } + } + + fn wants_text_input(&self) -> bool { + matches!(self.step, Step::EnterSpacing { .. }) + } + + fn on_text_input(&mut self, text: &str) -> Option { + if let Step::EnterSpacing { base, others } = &self.step { + let spacing: f64 = text.trim().parse().unwrap_or(0.0); + let b = *base; + let o = others.clone(); + // Emit sentinel for commands.rs to handle + use acadrust::entities::XLine; + let mut xl = XLine::default(); + let handles_str: Vec = o.iter().map(|h| h.value().to_string()).collect(); + xl.common.layer = format!("__DIMSPACE__{},{},{}", b.value(), handles_str.join(";"), spacing); + return Some(CmdResult::ReplaceEntity(b, vec![acadrust::EntityType::XLine(xl)])); + } + None + } + + fn on_point(&mut self, _pt: Vec3) -> CmdResult { CmdResult::NeedPoint } + + fn on_enter(&mut self) -> CmdResult { + match &self.step { + Step::PickOthers { base, others } if !others.is_empty() => { + let b = *base; + let o = others.clone(); + self.step = Step::EnterSpacing { base: b, others: o }; + CmdResult::NeedPoint + } + _ => CmdResult::Cancel, + } + } +} diff --git a/src/modules/annotate/dimtedit.rs b/src/modules/annotate/dimtedit.rs new file mode 100644 index 00000000..3bda91a5 --- /dev/null +++ b/src/modules/annotate/dimtedit.rs @@ -0,0 +1,112 @@ +// DIMTEDIT command — reposition the text of an existing dimension. +// +// Workflow: +// 1. Pick a dimension entity +// 2. Click the new text position (entity data is injected by update.rs after pick) + +use acadrust::{EntityType, Handle}; +use glam::Vec3; + +use crate::command::{CadCommand, CmdResult}; +use crate::modules::{IconKind, ModuleEvent, ToolDef}; +use crate::scene::wire_model::WireModel; + +pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/dim_tedit.svg")); + +pub fn tool() -> ToolDef { + ToolDef { + id: "DIMTEDIT", + label: "Dim Text Edit", + icon: ICON, + event: ModuleEvent::Command("DIMTEDIT".to_string()), + } +} + +enum Step { + PickDim, + PickTextPos { handle: Handle, entity: Option }, +} + +pub struct DimTeditCommand { + step: Step, +} + +impl DimTeditCommand { + pub fn new() -> Self { + Self { step: Step::PickDim } + } + +} + +impl CadCommand for DimTeditCommand { + fn name(&self) -> &'static str { "DIMTEDIT" } + + fn prompt(&self) -> String { + match &self.step { + Step::PickDim => "DIMTEDIT Select dimension:".into(), + Step::PickTextPos { .. } => "DIMTEDIT Specify new location for dimension text:".into(), + } + } + + fn needs_entity_pick(&self) -> bool { + matches!(self.step, Step::PickDim) + } + + fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult { + if handle.is_null() { return CmdResult::NeedPoint; } + self.step = Step::PickTextPos { handle, entity: None }; + CmdResult::NeedPoint + } + + fn on_point(&mut self, pt: Vec3) -> CmdResult { + if let Step::PickTextPos { handle, entity } = &mut self.step { + let h = *handle; + if let Some(mut ent) = entity.take() { + if let EntityType::Dimension(ref mut d) = ent { + let new_pt = acadrust::types::Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64); + d.base_mut().text_middle_point = new_pt; + d.base_mut().insertion_point = new_pt; + } + return CmdResult::ReplaceEntity(h, vec![ent]); + } + // No entity yet — wait for inject + CmdResult::NeedPoint + } else { + CmdResult::NeedPoint + } + } + + fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } + + fn on_mouse_move(&mut self, pt: Vec3) -> Option { + if !matches!(self.step, Step::PickTextPos { .. }) { + return None; + } + let d = 0.2_f32; + Some(WireModel { + name: "dimtedit_preview".into(), + points: vec![ + [pt.x - d, pt.y, pt.z - d], + [pt.x + d, pt.y, pt.z - d], + [pt.x + d, pt.y, pt.z + d], + [pt.x - d, pt.y, pt.z + d], + [pt.x - d, pt.y, pt.z - d], + ], + 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![], + }) + } + + fn inject_picked_entity(&mut self, entity: EntityType) { + if let Step::PickTextPos { entity: slot, .. } = &mut self.step { + *slot = Some(entity); + } + } +} diff --git a/src/modules/annotate/mleader_edit.rs b/src/modules/annotate/mleader_edit.rs new file mode 100644 index 00000000..f9fd084a --- /dev/null +++ b/src/modules/annotate/mleader_edit.rs @@ -0,0 +1,372 @@ +// 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![], + } +} + +// Silence unused-import warning for MultiLeader and LeaderLine if not used in all paths +fn _uses_ml_types(_ml: &MultiLeader, _ll: &LeaderLine) {} diff --git a/src/modules/annotate/mod.rs b/src/modules/annotate/mod.rs index 73c08cb1..30166d98 100644 --- a/src/modules/annotate/mod.rs +++ b/src/modules/annotate/mod.rs @@ -6,14 +6,21 @@ pub mod ddedit; pub mod diameter_dim; pub mod dim_baseline; pub mod dim_continue; +pub mod dimbreak; +pub mod dimedit; +pub mod dimjogline; +pub mod dimspace; +pub mod dimtedit; pub mod leader_cmd; pub mod linear_dim; pub mod mleader_cmd; +pub mod mleader_edit; pub mod mtext; +pub mod ordinate_dim; +pub mod qdim; pub mod radius_dim; pub mod table_cmd; pub mod text; -pub mod ordinate_dim; pub mod tolerance_cmd; use crate::modules::{CadModule, RibbonGroup, RibbonItem}; @@ -47,6 +54,18 @@ impl CadModule for AnnotateModule { ], default: "MTEXT", }, + RibbonItem::Tool(crate::modules::ToolDef { + id: "STYLE", + label: "Text Style", + icon: crate::modules::IconKind::Svg(include_bytes!("../../../assets/icons/text_style.svg")), + event: crate::modules::ModuleEvent::Command("STYLE".to_string()), + }), + RibbonItem::Tool(crate::modules::ToolDef { + id: "FIND", + label: "Find", + icon: crate::modules::IconKind::Svg(include_bytes!("../../../assets/icons/find.svg")), + event: crate::modules::ModuleEvent::Command("FIND".to_string()), + }), ], }, // ── Dimensions ─────────────────────────────────────────────── @@ -64,12 +83,24 @@ impl CadModule for AnnotateModule { (radius_dim::tool().id, radius_dim::tool().label, radius_dim::tool().icon), (diameter_dim::tool().id, diameter_dim::tool().label, diameter_dim::tool().icon), (ordinate_dim::tool().id, ordinate_dim::tool().label, ordinate_dim::tool().icon), + (qdim::tool().id, qdim::tool().label, qdim::tool().icon), ], default: "DIMLINEAR", }, RibbonItem::Tool(dim_continue::tool()), RibbonItem::Tool(dim_baseline::tool()), RibbonItem::Tool(tolerance_cmd::tool()), + RibbonItem::Tool(dimedit::tool()), + RibbonItem::Tool(dimtedit::tool()), + RibbonItem::Tool(dimbreak::tool()), + RibbonItem::Tool(dimspace::tool()), + RibbonItem::Tool(dimjogline::tool()), + RibbonItem::Tool(crate::modules::ToolDef { + id: "DIMSTYLE", + label: "Dim Style", + icon: crate::modules::IconKind::Svg(include_bytes!("../../../assets/icons/dim_style.svg")), + event: crate::modules::ModuleEvent::Command("DIMSTYLE".to_string()), + }), ], }, // ── Leaders ────────────────────────────────────────────────── @@ -86,6 +117,10 @@ impl CadModule for AnnotateModule { ], default: "MLEADER", }, + RibbonItem::Tool(mleader_edit::tool_add()), + RibbonItem::Tool(mleader_edit::tool_remove()), + RibbonItem::Tool(mleader_edit::tool_align()), + RibbonItem::Tool(mleader_edit::tool_collect()), ], }, // ── Tables ─────────────────────────────────────────────────── diff --git a/src/modules/annotate/qdim.rs b/src/modules/annotate/qdim.rs new file mode 100644 index 00000000..4e0e66f3 --- /dev/null +++ b/src/modules/annotate/qdim.rs @@ -0,0 +1,121 @@ +// QDIM command — quick dimension: select objects then pick placement line. +// +// Workflow: +// 1. Select objects (window/click, Enter to finish selection) +// 2. Pick a point that defines the dimension-line position +// +// Creates one linear dimension per detected endpoint pair on the selected objects. + +use acadrust::entities::{Dimension, DimensionLinear}; +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; + +pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/qdim.svg")); + +pub fn tool() -> ToolDef { + ToolDef { + id: "QDIM", + label: "Quick Dim", + icon: ICON, + event: ModuleEvent::Command("QDIM".to_string()), + } +} + +enum Step { + Gathering, + PlaceLine { handles: Vec }, +} + +pub struct QdimCommand { + step: Step, +} + +impl QdimCommand { + pub fn new() -> Self { + Self { step: Step::Gathering } + } + + +} + +impl CadCommand for QdimCommand { + fn name(&self) -> &'static str { "QDIM" } + + fn prompt(&self) -> String { + match &self.step { + Step::Gathering => "QDIM Select geometry to dimension (Enter when done):".into(), + Step::PlaceLine { .. } => "QDIM Specify dimension line position:".into(), + } + } + + fn is_selection_gathering(&self) -> bool { + matches!(self.step, Step::Gathering) + } + + fn on_selection_complete(&mut self, handles: Vec) -> CmdResult { + if handles.is_empty() { + return CmdResult::Cancel; + } + self.step = Step::PlaceLine { handles }; + CmdResult::NeedPoint + } + + fn on_point(&mut self, pt: Vec3) -> CmdResult { + if let Step::PlaceLine { handles } = &self.step { + CmdResult::Relaunch( + format!("QDIM_PLACE {:.6} {:.6} {:.6}", pt.x, pt.y, pt.z), + handles.clone(), + ) + } else { + CmdResult::NeedPoint + } + } + + fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } + + fn on_mouse_move(&mut self, pt: Vec3) -> Option { + if !matches!(self.step, Step::PlaceLine { .. }) { + return None; + } + let d = 0.5_f32; + Some(WireModel { + name: "qdim_preview".into(), + points: vec![ + [pt.x - d * 3.0, pt.y, pt.z], + [pt.x + d * 3.0, pt.y, pt.z], + ], + 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![], + }) + } +} + +/// Build a linear dimension between two points at the given dimension-line position. +#[allow(dead_code)] +pub fn make_linear_dim(p1: Vec3, p2: Vec3, dim_pt: Vec3) -> EntityType { + let v = |p: Vec3| Vector3::new(p.x as f64, p.y as f64, p.z as f64); + let mut dim = DimensionLinear::new(v(p1), v(p2)); + // Determine axis: horizontal if Δx > Δz, else vertical + let dx = (p2.x - p1.x).abs(); + let dz = (p2.z - p1.z).abs(); + dim.rotation = if dz > dx { std::f64::consts::FRAC_PI_2 } else { 0.0 }; + dim.definition_point = v(dim_pt); + dim.base.definition_point = v(dim_pt); + let text_pos = (p1 + p2) * 0.5; + dim.base.text_middle_point = v(text_pos); + dim.base.insertion_point = v(text_pos); + dim.base.actual_measurement = dim.measurement(); + EntityType::Dimension(Dimension::Linear(dim)) +}