feat: add missing Annotate tab commands and icons
New dimension commands: QDIM, DIMEDIT, DIMTEDIT, DIMBREAK, DIMSPACE, DIMJOGLINE with full command workflows and SVG icons. New multileader commands: MLEADERADD, MLEADERREMOVE, MLEADERALIGN, MLEADERCOLLECT with entity injection pattern for in-place editing. Updated annotate ribbon: all tools exposed across Text, Dimensions, Leaders, Tables, Markup, and Annotation Scaling groups. Added STYLE, FIND, DIMSTYLE shortcut buttons. Added `inject_picked_entity` to the CadCommand trait for commands that need a cloned copy of the picked entity to modify (DIMTEDIT, MLEADERADD, MLEADERREMOVE). Sentinel-based dispatch in cmd_result.rs handles DIMBREAK, DIMSPACE, DIMJOGLINE, MLEADERALIGN, and MLEADERCOLLECT. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
510bcf11e5
commit
08b9a21d31
25 changed files with 1408 additions and 1 deletions
|
|
@ -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: "<base_handle>,<h1>;<h2>;...;<hN>,<spacing>"
|
||||
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<u64> = parts[1].split(';')
|
||||
.filter_map(|s| s.parse::<u64>().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: "<h1>,<h2>,...;<fx>,<fz>;<tx>,<tz>"
|
||||
let parts: Vec<&str> = encoded.splitn(3, ';').collect();
|
||||
if parts.len() < 3 { return; }
|
||||
let handles: Vec<acadrust::Handle> = parts[0].split(',')
|
||||
.filter_map(|s| s.parse::<u64>().ok().map(acadrust::Handle::from))
|
||||
.collect();
|
||||
let from_parts: Vec<f64> = parts[1].split(',')
|
||||
.filter_map(|s| s.parse().ok())
|
||||
.collect();
|
||||
let to_parts: Vec<f64> = 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<acadrust::Handle> = parts[0].split(',')
|
||||
.filter_map(|s| s.parse::<u64>().ok().map(acadrust::Handle::from))
|
||||
.collect();
|
||||
let pos_parts: Vec<f64> = 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<acadrust::entities::LeaderRoot> = 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..]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
|
|
|||
Loading…
Reference in a new issue