feat(commands): expand standard CAD command coverage

Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.

3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)

Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
  (XData markers honoured by the existing geometry-gen / tessellator)

Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
  system variables, and layer/style/view commands

POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-29 08:23:58 +03:00
1 parent 76ef544cda
commit c19c18c2f2
24 files changed
+4405 -216

No files matched your search

+290 -6
View File
@@ -25,12 +25,24 @@ impl OpenCADStudio {
let is_paper = self.tabs[i].scene.current_layout != "Model";
self.push_undo_snapshot(i, "BASE");
if is_paper {
self.tabs[i].scene.document.header.paper_space_insertion_base = pt;
self.tabs[i]
.scene
.document
.header
.paper_space_insertion_base = pt;
} else {
self.tabs[i].scene.document.header.model_space_insertion_base = pt;
self.tabs[i]
.scene
.document
.header
.model_space_insertion_base = pt;
}
self.tabs[i].dirty = true;
let space = if is_paper { "paper space" } else { "model space" };
let space = if is_paper {
"paper space"
} else {
"model space"
};
self.command_line.push_output(&format!(
"Base point ({}, {}, {}) set for {space}.",
nums[0], nums[1], z
@@ -73,6 +85,62 @@ impl OpenCADStudio {
}
}
// COPYBASE — copy the selection to the clipboard with a picked base
// point (vs COPYCLIP, which uses the selection centroid).
"COPYBASE" => {
let handles: Vec<_> = self.tabs[i]
.scene
.selected_entities()
.into_iter()
.map(|(h, _)| h)
.collect();
if handles.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand;
let cmd = SelectObjectsCommand::new("COPYBASE");
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
} else {
use crate::modules::draw::clipboard::copy_base::CopyBaseCommand;
let cmd = CopyBaseCommand::new();
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
}
// Internal: the base point picked by COPYBASE — perform the copy.
cmd if cmd.starts_with("COPYBASE_AT ") => {
let coords: Vec<f64> = cmd
.trim_start_matches("COPYBASE_AT")
.split_whitespace()
.filter_map(|s| s.parse::<f64>().ok())
.collect();
let handles: Vec<_> = self.tabs[i]
.scene
.selected_entities()
.into_iter()
.map(|(h, _)| h)
.collect();
if coords.len() == 3 && !handles.is_empty() {
let base = glam::DVec3::new(coords[0], coords[1], coords[2]);
let entities: Vec<_> = handles
.iter()
.filter_map(|&h| self.tabs[i].scene.document.get_entity(h).cloned())
.collect();
self.clipboard_centroid = base;
self.clipboard = entities;
self.clipboard_deps = super::super::ClipboardDeps::capture(
&self.tabs[i].scene.document,
&self.clipboard,
);
self.command_line.push_info(&format!(
"{} object(s) copied to clipboard (base {:.3},{:.3}).",
self.clipboard.len(),
base.x,
base.y
));
}
}
"CUTCLIP" | "CX" => {
let handles: Vec<_> = self.tabs[i]
.scene
@@ -169,8 +237,7 @@ impl OpenCADStudio {
e.common_mut().xdictionary_handle = Some(root);
}
}
match self
.tabs[i]
match self.tabs[i]
.scene
.define_block_from_owned_entities(entities, &name, base)
{
@@ -183,7 +250,8 @@ impl OpenCADStudio {
self.tabs[i].dirty = true;
let wires = self.tabs[i].scene.wires_for_entities(&self.clipboard);
use crate::modules::insert::insert_block::InsertBlockCommand;
let cmd = InsertBlockCommand::new_for_block(name, wires, base.as_vec3());
let cmd =
InsertBlockCommand::new_for_block(name, wires, base.as_vec3());
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
@@ -225,6 +293,187 @@ impl OpenCADStudio {
}
}
"MINSERT" => {
let blocks = self.tabs[i].scene.custom_block_names();
if blocks.is_empty() {
self.command_line
.push_error("No user-defined blocks found in this drawing.");
} else {
use crate::modules::insert::minsert::MinsertCommand;
let cmd = MinsertCommand::new(blocks);
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
}
// ATTSYNC <block> — reconcile every insert of <block> against the
// block's current attribute definitions: drop attributes whose tag
// no longer exists and add any newly-defined ones (keeping the values
// of attributes that remain).
cmd if cmd == "ATTSYNC" || cmd.starts_with("ATTSYNC ") => {
let arg = cmd.trim_start_matches("ATTSYNC").trim();
let blocks = self.tabs[i].scene.custom_block_names();
if arg.is_empty() {
self.command_line.push_info(&format!(
"Usage: ATTSYNC <block name>. Blocks: {}",
if blocks.is_empty() {
"(none)".to_string()
} else {
blocks.join(", ")
}
));
return Some(Task::none());
}
let Some(block) = blocks.iter().find(|b| b.eq_ignore_ascii_case(arg)).cloned()
else {
self.command_line
.push_error(&format!("ATTSYNC: no block named \"{arg}\"."));
return Some(Task::none());
};
// Gather the block's attribute definitions (tag, default value).
let doc = &self.tabs[i].scene.document;
let attdefs: Vec<(String, String)> = doc
.block_records
.get(&block)
.map(|br| {
br.entity_handles
.iter()
.filter_map(|h| doc.get_entity(*h))
.filter_map(|e| match e {
acadrust::EntityType::AttributeDefinition(a) => {
Some((a.tag.clone(), a.default_value.clone()))
}
_ => None,
})
.collect()
})
.unwrap_or_default();
self.push_undo_snapshot(i, "ATTSYNC");
let mut synced = 0usize;
for e in self.tabs[i].scene.document.entities_mut() {
if let acadrust::EntityType::Insert(ins) = e {
if ins.block_name.eq_ignore_ascii_case(&block) {
ins.attributes.retain(|a| {
attdefs.iter().any(|(t, _)| t.eq_ignore_ascii_case(&a.tag))
});
for (tag, default) in &attdefs {
if !ins
.attributes
.iter()
.any(|a| a.tag.eq_ignore_ascii_case(tag))
{
ins.attributes
.push(acadrust::entities::AttributeEntity::new(
tag.clone(),
default.clone(),
));
}
}
synced += 1;
}
}
}
self.tabs[i].scene.bump_geometry();
self.tabs[i].dirty = true;
self.command_line.push_output(&format!(
"ATTSYNC: synchronised {synced} insert(s) of \"{block}\" against {} attribute definition(s).",
attdefs.len()
));
}
// ADCENTER / CONTENTBROWSER — report the drawing's named content
// (blocks and layers) from the command line in place of the browser
// panel.
"ADCENTER" | "CONTENTBROWSER" | "ADC" => {
let blocks = self.tabs[i].scene.custom_block_names();
let layers: Vec<String> = self.tabs[i]
.scene
.document
.layers
.names()
.map(|s| s.to_string())
.collect();
self.command_line.push_output(&format!(
"Blocks ({}): {}",
blocks.len(),
if blocks.is_empty() {
"(none)".to_string()
} else {
blocks.join(", ")
}
));
self.command_line.push_output(&format!(
"Layers ({}): {}",
layers.len(),
layers.join(", ")
));
}
// BLOCKPALETTE — list the blocks available to insert.
"BLOCKPALETTE" | "BLOCKSPALETTE" => {
let blocks = self.tabs[i].scene.custom_block_names();
if blocks.is_empty() {
self.command_line.push_info(
"No user-defined blocks. Define one with BLOCK, then INSERT it.",
);
} else {
self.command_line.push_output(&format!(
"Blocks ({}) — insert with INSERT <name>: {}",
blocks.len(),
blocks.join(", ")
));
}
}
// ATTMAN — list the attribute definitions of each block (or one named
// block). The full graphical manager is not built; this reports the
// same information from the command line.
cmd if cmd == "ATTMAN" || cmd == "BATTMAN" || cmd.starts_with("ATTMAN ") => {
let arg = cmd.trim_start_matches("ATTMAN").trim();
let blocks = self.tabs[i].scene.custom_block_names();
let targets: Vec<String> = if arg.is_empty() {
blocks
} else {
blocks
.into_iter()
.filter(|b| b.eq_ignore_ascii_case(arg))
.collect()
};
if targets.is_empty() {
self.command_line
.push_info("ATTMAN: no matching block. Usage: ATTMAN [block name]");
} else {
let doc = &self.tabs[i].scene.document;
let mut lines: Vec<String> = Vec::new();
for block in &targets {
let attdefs: Vec<String> = doc
.block_records
.get(block)
.map(|br| {
br.entity_handles
.iter()
.filter_map(|h| doc.get_entity(*h))
.filter_map(|e| match e {
acadrust::EntityType::AttributeDefinition(a) => {
Some(format!("{}={}", a.tag, a.default_value))
}
_ => None,
})
.collect()
})
.unwrap_or_default();
lines.push(if attdefs.is_empty() {
format!(" {block}: no attributes.")
} else {
format!(" {block}: {}", attdefs.join(", "))
});
}
for l in lines {
self.command_line.push_output(&l);
}
}
}
"XATTACH" | "XA" => {
// Launch the file picker; XAttachPickResult will start the command.
return Some(Task::done(Message::XAttachPick));
@@ -316,6 +565,41 @@ impl OpenCADStudio {
}
}
// NCOPY — copy the nested objects of the selected block reference(s)
// into model space, keeping the block (a non-destructive extraction;
// every nested object is copied — the block is not exploded).
"NCOPY" | "NCOPYALL" => {
use crate::modules::draw::modify::explode::explode_entity;
let inserts: Vec<acadrust::Handle> = self.tabs[i]
.scene
.selected_entities()
.iter()
.filter(|(_, e)| matches!(e, acadrust::EntityType::Insert(_)))
.map(|(h, _)| *h)
.collect();
if inserts.is_empty() {
self.command_line
.push_error("NCOPY: select a block reference first.");
return Some(Task::none());
}
self.push_undo_snapshot(i, "NCOPY");
let mut n = 0usize;
for h in &inserts {
let nested = match self.tabs[i].scene.document.get_entity(*h).cloned() {
Some(ins) => explode_entity(&ins, &self.tabs[i].scene.document),
None => Vec::new(),
};
for e in nested {
self.tabs[i].scene.add_entity(e);
n += 1;
}
}
self.tabs[i].dirty = true;
self.command_line.push_output(&format!(
"NCOPY: copied {n} nested object(s) into model space (blocks kept)."
));
}
_ => return None,
}
Some(self.finish_dispatch(cmd))