2026-03-22 22:44:32 +03:00
|
|
|
|
use super::{H7CAD, Message};
|
|
|
|
|
|
use crate::command::CadCommand;
|
|
|
|
|
|
use crate::scene::Scene;
|
|
|
|
|
|
use iced::Task;
|
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
|
|
|
|
impl H7CAD {
|
|
|
|
|
|
pub(super) fn dispatch_command(&mut self, cmd: &str) -> Task<Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
// Cancel any running command before starting a new one.
|
|
|
|
|
|
if self.tabs[i].active_cmd.is_some() {
|
|
|
|
|
|
self.tabs[i].scene.clear_preview_wire();
|
|
|
|
|
|
self.tabs[i].active_cmd = None;
|
|
|
|
|
|
}
|
2026-03-31 00:31:28 +03:00
|
|
|
|
// Reset the last committed point so the first click of the new command
|
|
|
|
|
|
// is not constrained by ortho/polar relative to a previous command's endpoint.
|
|
|
|
|
|
self.last_point = None;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
|
|
|
|
|
if let Some(path_str) = cmd.strip_prefix("OPEN_RECENT:") {
|
|
|
|
|
|
let path = PathBuf::from(path_str);
|
|
|
|
|
|
return Task::perform(crate::io::open_path(path), Message::FileOpened);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
match cmd {
|
|
|
|
|
|
"NEW" => return Task::done(Message::ClearScene),
|
|
|
|
|
|
"OPEN" => return Task::done(Message::OpenFile),
|
|
|
|
|
|
"SAVE"|"QSAVE" => return Task::done(Message::SaveFile),
|
|
|
|
|
|
"SAVEAS" => return Task::done(Message::SaveAs),
|
|
|
|
|
|
"UNDO"|"U" => return Task::done(Message::Undo),
|
|
|
|
|
|
"REDO" => return Task::done(Message::Redo),
|
|
|
|
|
|
"CLEAR"|"CLR" => return Task::done(Message::ClearScene),
|
|
|
|
|
|
"WIREFRAME"|"VW" => return Task::done(Message::SetWireframe(true)),
|
|
|
|
|
|
"SOLID"|"VS" => return Task::done(Message::SetWireframe(false)),
|
|
|
|
|
|
"ORTHO" => return Task::done(Message::SetProjection(true)),
|
|
|
|
|
|
"PERSP" => return Task::done(Message::SetProjection(false)),
|
|
|
|
|
|
"LAYERS"|"LA" => return Task::done(Message::ToggleLayers),
|
|
|
|
|
|
|
|
|
|
|
|
// ── Layer object commands ──────────────────────────────────────
|
|
|
|
|
|
"LAYOFF" => {
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|
|
|
|
|
.into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
if handles.is_empty() {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::select::SelectObjectsCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = SelectObjectsCommand::new("LAYOFF");
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let layers: std::collections::HashSet<String> = self.tabs[i].scene
|
|
|
|
|
|
.selected_entities().into_iter()
|
|
|
|
|
|
.map(|(_, e)| e.common().layer.clone()).collect();
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYOFF");
|
|
|
|
|
|
for name in &layers {
|
|
|
|
|
|
if name == "0" { continue; }
|
|
|
|
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
|
|
|
|
|
dl.turn_off();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
self.command_line.push_info("Layer(s) turned off.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"LAYFRZ" => {
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|
|
|
|
|
.into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
if handles.is_empty() {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::select::SelectObjectsCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = SelectObjectsCommand::new("LAYFRZ");
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let layers: std::collections::HashSet<String> = self.tabs[i].scene
|
|
|
|
|
|
.selected_entities().into_iter()
|
|
|
|
|
|
.map(|(_, e)| e.common().layer.clone()).collect();
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYFRZ");
|
|
|
|
|
|
for name in &layers {
|
|
|
|
|
|
if name == "0" { continue; }
|
|
|
|
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
|
|
|
|
|
dl.freeze();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
self.command_line.push_info("Layer(s) frozen.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"LAYLCK" => {
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|
|
|
|
|
.into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
if handles.is_empty() {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::select::SelectObjectsCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = SelectObjectsCommand::new("LAYLCK");
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let layers: std::collections::HashSet<String> = self.tabs[i].scene
|
|
|
|
|
|
.selected_entities().into_iter()
|
|
|
|
|
|
.map(|(_, e)| e.common().layer.clone()).collect();
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYLCK");
|
|
|
|
|
|
for name in &layers {
|
|
|
|
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
|
|
|
|
|
dl.lock();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
self.command_line.push_info("Layer(s) locked.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"LAYMCUR" => {
|
|
|
|
|
|
let entities = self.tabs[i].scene.selected_entities();
|
|
|
|
|
|
if entities.is_empty() {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::select::SelectObjectsCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = SelectObjectsCommand::new("LAYMCUR");
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let layer = entities[0].1.common().layer.clone();
|
|
|
|
|
|
self.tabs[i].active_layer = layer.clone();
|
|
|
|
|
|
self.ribbon.active_layer = layer.clone();
|
|
|
|
|
|
self.tabs[i].layers.current_layer = layer.clone();
|
|
|
|
|
|
self.command_line.push_info(&format!("Current layer set to \"{layer}\"."));
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"LAYON" => {
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYON");
|
|
|
|
|
|
for name in self.tabs[i].scene.document.layers.iter()
|
|
|
|
|
|
.map(|l| l.name.clone()).collect::<Vec<_>>()
|
|
|
|
|
|
{
|
|
|
|
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
|
|
|
|
|
dl.turn_on();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
self.command_line.push_info("All layers turned on.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"LAYTHW" => {
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYTHW");
|
|
|
|
|
|
for name in self.tabs[i].scene.document.layers.iter()
|
|
|
|
|
|
.map(|l| l.name.clone()).collect::<Vec<_>>()
|
|
|
|
|
|
{
|
|
|
|
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
|
|
|
|
|
dl.thaw();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
self.command_line.push_info("All layers thawed.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"LAYULK" => {
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|
|
|
|
|
.into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
if handles.is_empty() {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::select::SelectObjectsCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = SelectObjectsCommand::new("LAYULK");
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let layers: std::collections::HashSet<String> = self.tabs[i].scene
|
|
|
|
|
|
.selected_entities().into_iter()
|
|
|
|
|
|
.map(|(_, e)| e.common().layer.clone()).collect();
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYULK");
|
|
|
|
|
|
for name in &layers {
|
|
|
|
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
|
|
|
|
|
dl.unlock();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
self.command_line.push_info("Layer(s) unlocked.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 09:25:39 +03:00
|
|
|
|
// LAYISO — turn off all layers except those used by selected entities
|
|
|
|
|
|
"LAYISO" => {
|
|
|
|
|
|
let sel_layers: std::collections::HashSet<String> = self.tabs[i].scene
|
|
|
|
|
|
.selected_entities().into_iter()
|
|
|
|
|
|
.map(|(_, e)| e.common().layer.clone()).collect();
|
|
|
|
|
|
if sel_layers.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("LAYISO: select entities on the layers to isolate first.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYISO");
|
|
|
|
|
|
let names: Vec<String> = self.tabs[i].scene.document.layers
|
|
|
|
|
|
.iter().map(|l| l.name.clone()).collect();
|
|
|
|
|
|
for name in names {
|
|
|
|
|
|
if !sel_layers.contains(&name) {
|
|
|
|
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
|
|
|
|
|
dl.turn_off();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
"LAYISO: isolated {} layer(s).", sel_layers.len()
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// LAYUNISO — restore all layers that were turned off by LAYISO (turn all on)
|
|
|
|
|
|
"LAYUNISO" => {
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYUNISO");
|
|
|
|
|
|
let names: Vec<String> = self.tabs[i].scene.document.layers
|
|
|
|
|
|
.iter().map(|l| l.name.clone()).collect();
|
|
|
|
|
|
for name in names {
|
|
|
|
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
|
|
|
|
|
dl.turn_on();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
self.command_line.push_info("LAYUNISO: all layers restored.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
|
"LAYMATCH"|"LAYMCH" => {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::layers::match_layer::LayMatchCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let dest: Vec<_> = self.tabs[i].scene.selected_entities()
|
|
|
|
|
|
.into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
let cmd = LayMatchCommand::new(dest);
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"MATCHPROP"|"MA" => {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::properties::match_prop::MatchPropCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
|
|
let cmd = MatchPropCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"GROUP"|"G" => {
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i]
|
|
|
|
|
|
.scene.selected_entities().into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
if handles.is_empty() {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::select::SelectObjectsCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = SelectObjectsCommand::new("GROUP");
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let auto_name = super::helpers::next_group_auto_name(&self.tabs[i].scene);
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::groups::group::GroupCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = GroupCommand::new(handles, auto_name);
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"UNGROUP"|"UG" => {
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i]
|
|
|
|
|
|
.scene.selected_entities().into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
if handles.is_empty() {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::groups::ungroup::UngroupCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = UngroupCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.push_undo_snapshot(i, "UNGROUP");
|
|
|
|
|
|
let count = self.tabs[i].scene.delete_groups_containing(&handles);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
if count > 0 {
|
|
|
|
|
|
self.command_line.push_info(&format!("{} group(s) dissolved.", count));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_info("No groups found for selected objects.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"COPYCLIP"|"CC" => {
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i]
|
|
|
|
|
|
.scene.selected_entities().into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
if handles.is_empty() {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::select::SelectObjectsCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = SelectObjectsCommand::new("COPYCLIP");
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let entities: Vec<_> = handles.iter()
|
|
|
|
|
|
.filter_map(|&h| self.tabs[i].scene.document.get_entity(h).cloned())
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
self.clipboard_centroid = super::helpers::entities_centroid(
|
|
|
|
|
|
&self.tabs[i].scene.wire_models_for(&handles),
|
|
|
|
|
|
);
|
|
|
|
|
|
self.clipboard = entities;
|
|
|
|
|
|
self.command_line.push_info(
|
|
|
|
|
|
&format!("{} object(s) copied to clipboard.", self.clipboard.len()),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"CUTCLIP"|"CX" => {
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i]
|
|
|
|
|
|
.scene.selected_entities().into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
if handles.is_empty() {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::select::SelectObjectsCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = SelectObjectsCommand::new("CUTCLIP");
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let entities: Vec<_> = handles.iter()
|
|
|
|
|
|
.filter_map(|&h| self.tabs[i].scene.document.get_entity(h).cloned())
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
self.clipboard_centroid = super::helpers::entities_centroid(
|
|
|
|
|
|
&self.tabs[i].scene.wire_models_for(&handles),
|
|
|
|
|
|
);
|
|
|
|
|
|
let count = entities.len();
|
|
|
|
|
|
self.clipboard = entities;
|
|
|
|
|
|
self.push_undo_snapshot(i, "CUTCLIP");
|
|
|
|
|
|
self.tabs[i].scene.erase_entities(&handles);
|
|
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
self.command_line.push_info(
|
|
|
|
|
|
&format!("{} object(s) cut to clipboard.", count),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"PASTECLIP"|"PC" => {
|
|
|
|
|
|
if self.clipboard.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("Clipboard is empty.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let wires = self.tabs[i].scene.wires_for_entities(&self.clipboard);
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::clipboard::paste::PasteCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = PasteCommand::new(wires, self.clipboard_centroid);
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 09:27:21 +03:00
|
|
|
|
// PASTEORIG — paste at original coordinates (no move to pick point)
|
|
|
|
|
|
"PASTEORIG" => {
|
|
|
|
|
|
if self.clipboard.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("PASTEORIG: clipboard is empty.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let count = self.clipboard.len();
|
|
|
|
|
|
self.push_undo_snapshot(i, "PASTEORIG");
|
|
|
|
|
|
for entity in &self.clipboard {
|
|
|
|
|
|
self.tabs[i].scene.add_entity(entity.clone());
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"PASTEORIG: {} object(s) pasted at original coordinates.", count
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
|
"BLOCK" => {
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i]
|
|
|
|
|
|
.scene.selected_entities().into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
if handles.is_empty() {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::select::SelectObjectsCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = SelectObjectsCommand::new("BLOCK");
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
use crate::modules::insert::create_block::CreateBlockCommand;
|
|
|
|
|
|
let cmd = CreateBlockCommand::new(handles);
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"INSERT" => {
|
|
|
|
|
|
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::insert_block::InsertBlockCommand;
|
|
|
|
|
|
let cmd = InsertBlockCommand::new(blocks);
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Draw commands ──────────────────────────────────────────────
|
|
|
|
|
|
"LINE"|"L" => {
|
|
|
|
|
|
use crate::modules::home::draw::line::LineCommand;
|
|
|
|
|
|
let new_cmd = LineCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 09:17:45 +03:00
|
|
|
|
"MLINE"|"ML" => {
|
|
|
|
|
|
use crate::modules::home::draw::mline::MlineCommand;
|
2026-04-06 14:54:05 +03:00
|
|
|
|
let style = self.tabs[i].scene.document.header.multiline_style.clone();
|
|
|
|
|
|
let cmd_obj = MlineCommand::with_style(style);
|
2026-04-03 09:17:45 +03:00
|
|
|
|
self.command_line.push_info(&cmd_obj.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 23:54:07 +03:00
|
|
|
|
cmd if cmd == "WIPEOUT" || cmd == "WO" || cmd.starts_with("WIPEOUT ") => {
|
|
|
|
|
|
use crate::modules::home::draw::wipeout::WipeoutCommand;
|
|
|
|
|
|
let args = cmd.split_once(' ').map(|(_, r)| r.trim().to_uppercase()).unwrap_or_default();
|
|
|
|
|
|
let wo_cmd = if args == "P" || args == "POLYGONAL" {
|
|
|
|
|
|
WipeoutCommand::new_polygonal()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
WipeoutCommand::new_rectangular()
|
|
|
|
|
|
};
|
|
|
|
|
|
self.command_line.push_info(&wo_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(wo_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 23:51:17 +03:00
|
|
|
|
"REVCLOUD" => {
|
|
|
|
|
|
use crate::modules::home::draw::revcloud::RevCloudCommand;
|
|
|
|
|
|
let cmd = RevCloudCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
"ATTDEF" => {
|
|
|
|
|
|
use crate::modules::home::draw::attdef::AttdefCommand;
|
|
|
|
|
|
let cmd = AttdefCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ATTEDIT — list or edit attribute values on selected Insert entities.
|
|
|
|
|
|
// Usage:
|
|
|
|
|
|
// ATTEDIT — list all attributes on selected Insert(s)
|
|
|
|
|
|
// ATTEDIT <tag> <v> — set the value of attribute <tag> to <v>
|
|
|
|
|
|
cmd if cmd == "ATTEDIT" || cmd.starts_with("ATTEDIT ") => {
|
|
|
|
|
|
let rest = cmd.trim_start_matches("ATTEDIT").trim();
|
|
|
|
|
|
let parts: Vec<&str> = rest.splitn(2, char::is_whitespace).collect();
|
|
|
|
|
|
let selected_handles: Vec<acadrust::Handle> = self.tabs[i].scene
|
|
|
|
|
|
.selected_entities()
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|(h, _)| *h)
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
if selected_handles.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("ATTEDIT: select an Insert entity first.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let mut found_any = false;
|
|
|
|
|
|
for sh in &selected_handles {
|
|
|
|
|
|
if let Some(acadrust::EntityType::Insert(ins)) =
|
|
|
|
|
|
self.tabs[i].scene.document.entities().find(|e| e.common().handle == *sh)
|
|
|
|
|
|
{
|
|
|
|
|
|
found_any = true;
|
|
|
|
|
|
if rest.is_empty() {
|
|
|
|
|
|
// List attributes.
|
|
|
|
|
|
if ins.attributes.is_empty() {
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
" Insert {:x}: no attributes.", sh.value()
|
|
|
|
|
|
));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for attr in &ins.attributes {
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
" [{tag}] = {val}",
|
|
|
|
|
|
tag = attr.tag,
|
|
|
|
|
|
val = attr.get_value()
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !found_any {
|
|
|
|
|
|
self.command_line.push_error("ATTEDIT: no Insert entities in selection.");
|
|
|
|
|
|
}
|
|
|
|
|
|
// If tag + value supplied, mutate attributes.
|
|
|
|
|
|
if parts.len() == 2 && !parts[0].is_empty() {
|
|
|
|
|
|
let tag_up = parts[0].to_uppercase();
|
|
|
|
|
|
let new_val = parts[1];
|
|
|
|
|
|
let mut changed = 0usize;
|
|
|
|
|
|
self.push_undo_snapshot(i, "ATTEDIT");
|
|
|
|
|
|
for sh in &selected_handles {
|
|
|
|
|
|
if let Some(acadrust::EntityType::Insert(ins)) =
|
|
|
|
|
|
self.tabs[i].scene.document.entities_mut().find(|e| e.common().handle == *sh)
|
|
|
|
|
|
{
|
|
|
|
|
|
for attr in &mut ins.attributes {
|
|
|
|
|
|
if attr.tag.to_uppercase() == tag_up {
|
|
|
|
|
|
attr.set_value(new_val);
|
|
|
|
|
|
changed += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if changed > 0 {
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"ATTEDIT: updated {changed} attribute(s) [{tag_up}] = {new_val}."
|
|
|
|
|
|
));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error(&format!(
|
|
|
|
|
|
"ATTEDIT: tag '{tag_up}' not found in selection."
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ATTDISP — control attribute display visibility.
|
|
|
|
|
|
// ATTDISP ON — make all AttributeDefinitions visible
|
|
|
|
|
|
// ATTDISP OFF — make all AttributeDefinitions invisible
|
|
|
|
|
|
// ATTDISP NORMAL — restore: show only those without the invisible flag
|
|
|
|
|
|
cmd if cmd == "ATTDISP" || cmd.starts_with("ATTDISP ") => {
|
|
|
|
|
|
let sub = cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase();
|
|
|
|
|
|
match sub.as_str() {
|
|
|
|
|
|
"ON" | "OFF" | "NORMAL" => {
|
|
|
|
|
|
self.push_undo_snapshot(i, "ATTDISP");
|
|
|
|
|
|
let mut count = 0usize;
|
|
|
|
|
|
for entity in self.tabs[i].scene.document.entities_mut() {
|
|
|
|
|
|
if let acadrust::EntityType::AttributeDefinition(ad) = entity {
|
|
|
|
|
|
match sub.as_str() {
|
|
|
|
|
|
"ON" => { ad.flags.invisible = false; count += 1; }
|
|
|
|
|
|
"OFF" => { ad.flags.invisible = true; count += 1; }
|
|
|
|
|
|
"NORMAL" => { /* leave existing flags — they are already the "normal" state */ }
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"ATTDISP {sub}: {count} attribute definition(s) updated."
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
self.command_line.push_info("Usage: ATTDISP ON | OFF | NORMAL");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 23:50:27 +03:00
|
|
|
|
"DONUT"|"DO" => {
|
|
|
|
|
|
use crate::modules::home::draw::donut::DonutCommand;
|
|
|
|
|
|
let cmd = DonutCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
|
"CIRCLE"|"C" => {
|
|
|
|
|
|
use crate::modules::home::draw::circle::CircleCommand;
|
|
|
|
|
|
let new_cmd = CircleCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
"CIRCLE_CD" => {
|
|
|
|
|
|
use crate::modules::home::draw::circle::CircleCDCommand;
|
|
|
|
|
|
let new_cmd = CircleCDCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
"CIRCLE_2P" => {
|
|
|
|
|
|
use crate::modules::home::draw::circle::Circle2PCommand;
|
|
|
|
|
|
let new_cmd = Circle2PCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
"CIRCLE_3P" => {
|
|
|
|
|
|
use crate::modules::home::draw::circle::Circle3PCommand;
|
|
|
|
|
|
let new_cmd = Circle3PCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
"CIRCLE_TTR" => {
|
|
|
|
|
|
use crate::modules::home::draw::circle::CircleTTRCommand;
|
|
|
|
|
|
let new_cmd = CircleTTRCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.pre_cmd_tangent = Some(self.snapper.is_on(crate::snap::SnapType::Tangent));
|
|
|
|
|
|
self.snapper.enabled.insert(crate::snap::SnapType::Tangent);
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
"CIRCLE_TTT" => {
|
|
|
|
|
|
use crate::modules::home::draw::circle::CircleTTTCommand;
|
|
|
|
|
|
let new_cmd = CircleTTTCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.pre_cmd_tangent = Some(self.snapper.is_on(crate::snap::SnapType::Tangent));
|
|
|
|
|
|
self.snapper.enabled.insert(crate::snap::SnapType::Tangent);
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"ARC"|"A" => {
|
|
|
|
|
|
use crate::modules::home::draw::arc::ArcCommand;
|
|
|
|
|
|
let new_cmd = ArcCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
"ARC_3P" => {
|
|
|
|
|
|
use crate::modules::home::draw::arc::Arc3PCommand;
|
|
|
|
|
|
let new_cmd = Arc3PCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
"ARC_SCE" => {
|
|
|
|
|
|
use crate::modules::home::draw::arc::ArcSCECommand;
|
|
|
|
|
|
let new_cmd = ArcSCECommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
"ARC_SCA" => {
|
|
|
|
|
|
use crate::modules::home::draw::arc::ArcSCACommand;
|
|
|
|
|
|
let new_cmd = ArcSCACommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
"ARC_SCL" => {
|
|
|
|
|
|
use crate::modules::home::draw::arc::ArcSCLCommand;
|
|
|
|
|
|
let new_cmd = ArcSCLCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
"ARC_SEA" => {
|
|
|
|
|
|
use crate::modules::home::draw::arc::ArcSEACommand;
|
|
|
|
|
|
let new_cmd = ArcSEACommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
"ARC_SER" => {
|
|
|
|
|
|
use crate::modules::home::draw::arc::ArcSERCommand;
|
|
|
|
|
|
let new_cmd = ArcSERCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
"ARC_SED" => {
|
|
|
|
|
|
use crate::modules::home::draw::arc::ArcSEDCommand;
|
|
|
|
|
|
let new_cmd = ArcSEDCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
"ARC_CSA" => {
|
|
|
|
|
|
use crate::modules::home::draw::arc::ArcCSACommand;
|
|
|
|
|
|
let new_cmd = ArcCSACommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
"ARC_CSL" => {
|
|
|
|
|
|
use crate::modules::home::draw::arc::ArcCSLCommand;
|
|
|
|
|
|
let new_cmd = ArcCSLCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"RECT" => {
|
|
|
|
|
|
use crate::modules::home::draw::shapes::RectCommand;
|
|
|
|
|
|
let new_cmd = RectCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
"RECT_ROT" => {
|
|
|
|
|
|
use crate::modules::home::draw::shapes::RectRotCommand;
|
|
|
|
|
|
let new_cmd = RectRotCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
"RECT_CEN" => {
|
|
|
|
|
|
use crate::modules::home::draw::shapes::RectCenCommand;
|
|
|
|
|
|
let new_cmd = RectCenCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
"POLY" => {
|
|
|
|
|
|
use crate::modules::home::draw::shapes::PolyCommand;
|
|
|
|
|
|
let new_cmd = PolyCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
"POLY_C" => {
|
|
|
|
|
|
use crate::modules::home::draw::shapes::PolyCCommand;
|
|
|
|
|
|
let new_cmd = PolyCCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
"POLY_E" => {
|
|
|
|
|
|
use crate::modules::home::draw::shapes::PolyECommand;
|
|
|
|
|
|
let new_cmd = PolyECommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"PLINE"|"PL" => {
|
|
|
|
|
|
use crate::modules::home::draw::polyline::PlineCommand;
|
|
|
|
|
|
let new_cmd = PlineCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Modify commands ────────────────────────────────────────────
|
|
|
|
|
|
"MOVE"|"M" => {
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|
|
|
|
|
.into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
if handles.is_empty() {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::select::SelectObjectsCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = SelectObjectsCommand::new("MOVE");
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
use crate::modules::home::modify::translate::MoveCommand;
|
|
|
|
|
|
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
|
|
|
|
|
let new_cmd = MoveCommand::new(handles, wires);
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"COPY"|"CO" => {
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|
|
|
|
|
.into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
if handles.is_empty() {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::select::SelectObjectsCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = SelectObjectsCommand::new("COPY");
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
use crate::modules::home::modify::copy::CopyCommand;
|
|
|
|
|
|
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
|
|
|
|
|
let new_cmd = CopyCommand::new(handles, wires);
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"ROTATE"|"RO" => {
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|
|
|
|
|
.into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
if handles.is_empty() {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::select::SelectObjectsCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = SelectObjectsCommand::new("ROTATE");
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
use crate::modules::home::modify::rotate::RotateCommand;
|
|
|
|
|
|
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
|
|
|
|
|
let new_cmd = RotateCommand::new(handles, wires);
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"POINT"|"PO" => {
|
|
|
|
|
|
use crate::modules::home::draw::point::PointCommand;
|
|
|
|
|
|
let new_cmd = PointCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
2026-04-02 23:05:57 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"RAY" => {
|
|
|
|
|
|
use crate::modules::home::draw::ray::RayCommand;
|
|
|
|
|
|
let new_cmd = RayCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"XLINE"|"XL" => {
|
|
|
|
|
|
use crate::modules::home::draw::ray::XLineCommand;
|
|
|
|
|
|
let new_cmd = XLineCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
2026-03-22 22:44:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"HATCH"|"H" => {
|
|
|
|
|
|
use crate::modules::home::draw::hatch::HatchCommand;
|
|
|
|
|
|
let outlines = self.tabs[i].scene.closed_outlines();
|
|
|
|
|
|
let new_cmd = HatchCommand::new(outlines);
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"GRADIENT" => {
|
|
|
|
|
|
use crate::modules::home::draw::hatch::GradientCommand;
|
|
|
|
|
|
let outlines = self.tabs[i].scene.closed_outlines();
|
|
|
|
|
|
let new_cmd = GradientCommand::new(outlines);
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"BOUNDARY" => {
|
|
|
|
|
|
use crate::modules::home::draw::hatch::BoundaryCommand;
|
|
|
|
|
|
let outlines = self.tabs[i].scene.closed_outlines();
|
|
|
|
|
|
let new_cmd = BoundaryCommand::new(outlines);
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"ELLIPSE"|"EL" => {
|
|
|
|
|
|
use crate::modules::home::draw::ellipse::EllipseCommand;
|
|
|
|
|
|
let new_cmd = EllipseCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"ELLIPSE_AXIS" => {
|
|
|
|
|
|
use crate::modules::home::draw::ellipse::EllipseAxisCommand;
|
|
|
|
|
|
let new_cmd = EllipseAxisCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"ELLIPSE_ARC" => {
|
|
|
|
|
|
use crate::modules::home::draw::ellipse::EllipseArcCommand;
|
|
|
|
|
|
let new_cmd = EllipseArcCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"SPLINE"|"SPL" => {
|
|
|
|
|
|
use crate::modules::home::draw::spline::SplineCommand;
|
|
|
|
|
|
let new_cmd = SplineCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"SCALE"|"SC" => {
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|
|
|
|
|
.into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
if handles.is_empty() {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::select::SelectObjectsCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = SelectObjectsCommand::new("SCALE");
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
use crate::modules::home::modify::scale::ScaleCommand;
|
|
|
|
|
|
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
|
|
|
|
|
let new_cmd = ScaleCommand::new(handles, wires);
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"MIRROR"|"MI" => {
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|
|
|
|
|
.into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
if handles.is_empty() {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::select::SelectObjectsCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = SelectObjectsCommand::new("MIRROR");
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
use crate::modules::home::modify::mirror::MirrorCommand;
|
|
|
|
|
|
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
|
|
|
|
|
let new_cmd = MirrorCommand::new(handles, wires);
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"ERASE"|"E" => {
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|
|
|
|
|
.into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
if handles.is_empty() {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::select::SelectObjectsCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = SelectObjectsCommand::new("ERASE");
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let n = handles.len();
|
|
|
|
|
|
self.push_undo_snapshot(i, "ERASE");
|
|
|
|
|
|
self.tabs[i].scene.erase_entities(&handles);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
self.command_line.push_output(&format!("{n} object(s) erased."));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Annotate commands ──────────────────────────────────────────
|
|
|
|
|
|
"TEXT"|"T"|"DT" => {
|
|
|
|
|
|
use crate::modules::annotate::text::TextCommand;
|
|
|
|
|
|
let new_cmd = TextCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"MTEXT"|"MT" => {
|
|
|
|
|
|
use crate::modules::annotate::mtext::MTextCommand;
|
|
|
|
|
|
let new_cmd = MTextCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 09:29:11 +03:00
|
|
|
|
"DIMALIGNED"|"DAL" => {
|
|
|
|
|
|
use crate::modules::annotate::aligned_dim::AlignedDimensionCommand;
|
|
|
|
|
|
let cmd = AlignedDimensionCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"DIMDIAMETER"|"DDI" => {
|
|
|
|
|
|
use crate::modules::annotate::diameter_dim::DiameterDimensionCommand;
|
|
|
|
|
|
let cmd = DiameterDimensionCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
|
"DIMLINEAR" => {
|
|
|
|
|
|
use crate::modules::annotate::linear_dim::LinearDimensionCommand;
|
|
|
|
|
|
let new_cmd = LinearDimensionCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"DIMRADIUS" => {
|
|
|
|
|
|
use crate::modules::annotate::radius_dim::RadiusDimensionCommand;
|
|
|
|
|
|
let new_cmd = RadiusDimensionCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"DIMANGULAR" => {
|
|
|
|
|
|
use crate::modules::annotate::angular_dim::AngularDimensionCommand;
|
|
|
|
|
|
let new_cmd = AngularDimensionCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"LEADER"|"LE" => {
|
|
|
|
|
|
use crate::modules::annotate::leader_cmd::LeaderCommand;
|
|
|
|
|
|
let new_cmd = LeaderCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"MLEADER"|"MLD" => {
|
|
|
|
|
|
use crate::modules::annotate::mleader_cmd::MLeaderCommand;
|
|
|
|
|
|
let new_cmd = MLeaderCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
"TOLERANCE"|"TOL" => {
|
|
|
|
|
|
use crate::modules::annotate::tolerance_cmd::ToleranceCommand;
|
|
|
|
|
|
let cmd = ToleranceCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"TABLE" => {
|
|
|
|
|
|
use crate::modules::annotate::table_cmd::TableCommand;
|
|
|
|
|
|
let cmd = TableCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"DIMCONTINUE"|"DCO" => {
|
|
|
|
|
|
use crate::modules::annotate::dim_continue::DimContinueCommand;
|
|
|
|
|
|
let cmd = if let Some((p1, p2, dp, rot)) = find_last_linear_dim(&self.tabs[i].scene) {
|
|
|
|
|
|
DimContinueCommand::from_base(p1, p2, dp, rot)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
DimContinueCommand::new()
|
|
|
|
|
|
};
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"DIMBASELINE"|"DBA" => {
|
|
|
|
|
|
use crate::modules::annotate::dim_baseline::DimBaselineCommand;
|
|
|
|
|
|
let cmd = if let Some((p1, p2, dp, rot)) = find_last_linear_dim(&self.tabs[i].scene) {
|
|
|
|
|
|
DimBaselineCommand::from_base(p1, p2, dp, rot)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
DimBaselineCommand::new()
|
|
|
|
|
|
};
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
|
"ZOOM EXTENTS"|"ZOOMEXTENTS"|"ZE" => {
|
|
|
|
|
|
self.tabs[i].scene.fit_all();
|
|
|
|
|
|
self.command_line.push_output("Zoom Extents");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 23:28:00 +03:00
|
|
|
|
"ZOOM IN"|"ZI" => {
|
|
|
|
|
|
self.tabs[i].scene.zoom_camera(1.0 / 1.5);
|
|
|
|
|
|
self.command_line.push_output("Zoom In");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"ZOOM OUT"|"ZO" => {
|
|
|
|
|
|
self.tabs[i].scene.zoom_camera(1.5);
|
|
|
|
|
|
self.command_line.push_output("Zoom Out");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 09:26:15 +03:00
|
|
|
|
// ZOOM ALL — fit all entities (same as EXTENTS for now)
|
|
|
|
|
|
"ZOOM ALL"|"ZOOM A"|"ZA" => {
|
|
|
|
|
|
self.tabs[i].scene.fit_all();
|
|
|
|
|
|
self.command_line.push_output("Zoom All");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ZOOM SCALE — set zoom factor (e.g. "ZOOM SCALE 2" or "ZS 0.5")
|
|
|
|
|
|
cmd if cmd.starts_with("ZOOM SCALE ") || cmd.starts_with("ZS ") => {
|
|
|
|
|
|
let rest = cmd.split_once(' ')
|
|
|
|
|
|
.and_then(|(_, r)| r.split_once(' ').map(|(_, v)| v).or(Some(r)))
|
|
|
|
|
|
.unwrap_or("1");
|
|
|
|
|
|
if let Ok(factor) = rest.trim().parse::<f32>() {
|
|
|
|
|
|
if factor > 0.0 {
|
|
|
|
|
|
self.tabs[i].scene.zoom_camera(1.0 / factor);
|
|
|
|
|
|
self.command_line.push_output(&format!("Zoom Scale ×{factor:.3}"));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
"PLOTWINDOW"|"PW" => {
|
|
|
|
|
|
use crate::modules::view::plot_window::PlotWindowCommand;
|
|
|
|
|
|
let cmd = PlotWindowCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 23:28:00 +03:00
|
|
|
|
"ZOOM WINDOW"|"ZOOM W"|"ZW" => {
|
|
|
|
|
|
use crate::modules::view::zoom_window::ZoomWindowCommand;
|
|
|
|
|
|
let new_cmd = ZoomWindowCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
|
"STRETCH"|"SS" => {
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|
|
|
|
|
.into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
if handles.is_empty() {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::select::SelectObjectsCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = SelectObjectsCommand::new("STRETCH");
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
use crate::modules::home::modify::stretch::StretchCommand;
|
|
|
|
|
|
let new_cmd = StretchCommand::new(handles);
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"FILLET"|"F" => {
|
|
|
|
|
|
use crate::modules::home::modify::fillet::FilletCommand;
|
|
|
|
|
|
let entities: Vec<_> = self.tabs[i].scene.entity_wires().iter()
|
|
|
|
|
|
.filter_map(|w| {
|
|
|
|
|
|
let h = Scene::handle_from_wire_name(&w.name)?;
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e))
|
|
|
|
|
|
}).collect();
|
|
|
|
|
|
let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect();
|
|
|
|
|
|
let new_cmd = FilletCommand::new(
|
|
|
|
|
|
crate::modules::home::defaults::get_fillet_radius(), all_entities);
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"ARRAY"|"AR"|"ARRAYRECT" => {
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|
|
|
|
|
.into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
if handles.is_empty() {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::select::SelectObjectsCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = SelectObjectsCommand::new("ARRAYRECT");
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
use crate::modules::home::modify::array::ArrayRectCommand;
|
|
|
|
|
|
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
|
|
|
|
|
let new_cmd = ArrayRectCommand::new(handles, wires);
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"ARRAYPOLAR" => {
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|
|
|
|
|
.into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
if handles.is_empty() {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::select::SelectObjectsCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = SelectObjectsCommand::new("ARRAYPOLAR");
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
use crate::modules::home::modify::array::ArrayPolarCommand;
|
|
|
|
|
|
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
|
|
|
|
|
let new_cmd = ArrayPolarCommand::new(handles, wires);
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"ARRAYPATH" => {
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|
|
|
|
|
.into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
if handles.is_empty() {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::select::SelectObjectsCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = SelectObjectsCommand::new("ARRAYPATH");
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
use crate::modules::home::modify::array::ArrayPathCommand;
|
|
|
|
|
|
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
|
|
|
|
|
let all_entities: Vec<_> = self.tabs[i].scene.entity_wires().iter()
|
|
|
|
|
|
.filter_map(|w| {
|
|
|
|
|
|
let h = Scene::handle_from_wire_name(&w.name)?;
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity(h).cloned()
|
|
|
|
|
|
}).collect();
|
|
|
|
|
|
let new_cmd = ArrayPathCommand::new(handles, wires, all_entities);
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"CHAMFER"|"CHA" => {
|
|
|
|
|
|
use crate::modules::home::modify::fillet::ChamferCommand;
|
|
|
|
|
|
let entities: Vec<_> = self.tabs[i].scene.entity_wires().iter()
|
|
|
|
|
|
.filter_map(|w| {
|
|
|
|
|
|
let h = Scene::handle_from_wire_name(&w.name)?;
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e))
|
|
|
|
|
|
}).collect();
|
|
|
|
|
|
let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect();
|
|
|
|
|
|
let new_cmd = ChamferCommand::new(
|
|
|
|
|
|
crate::modules::home::defaults::get_chamfer_dist1(), all_entities);
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"EXPLODE"|"X" => {
|
|
|
|
|
|
use crate::modules::home::modify::explode::explode_entity;
|
|
|
|
|
|
let entities: Vec<_> = self.tabs[i].scene.selected_entities()
|
|
|
|
|
|
.into_iter().collect();
|
|
|
|
|
|
if entities.is_empty() {
|
2026-03-22 23:21:29 +03:00
|
|
|
|
use crate::modules::home::select::SelectObjectsCommand;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let cmd = SelectObjectsCommand::new("EXPLODE");
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let replacements: Vec<(acadrust::Handle, Vec<acadrust::EntityType>)> = entities
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.filter_map(|(h, e)| {
|
|
|
|
|
|
let pieces = explode_entity(e, &self.tabs[i].scene.document);
|
|
|
|
|
|
if pieces.is_empty() { None } else { Some((*h, pieces)) }
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let exploded = replacements.len();
|
|
|
|
|
|
if exploded > 0 {
|
|
|
|
|
|
self.push_undo_snapshot(i, "EXPLODE");
|
|
|
|
|
|
}
|
|
|
|
|
|
for (handle, pieces) in replacements {
|
|
|
|
|
|
self.tabs[i].scene.erase_entities(&[handle]);
|
|
|
|
|
|
for piece in pieces {
|
|
|
|
|
|
self.tabs[i].scene.add_entity(piece);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if exploded > 0 {
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
self.command_line.push_output(&format!("{exploded} object(s) exploded."));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_info("EXPLODE: no explodable objects selected.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"OFFSET"|"O" => {
|
|
|
|
|
|
use crate::modules::home::modify::offset::OffsetCommand;
|
|
|
|
|
|
let all_entities: Vec<_> = self.tabs[i].scene.entity_wires().iter()
|
|
|
|
|
|
.filter_map(|w| {
|
|
|
|
|
|
let h = Scene::handle_from_wire_name(&w.name)?;
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity(h).cloned()
|
|
|
|
|
|
}).collect();
|
|
|
|
|
|
let new_cmd = OffsetCommand::new(
|
|
|
|
|
|
crate::modules::home::defaults::get_offset_dist(), all_entities);
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"TRIM"|"TR" => {
|
|
|
|
|
|
use crate::modules::home::modify::trim::TrimCommand;
|
|
|
|
|
|
let entities: Vec<_> = self.tabs[i].scene.entity_wires().iter()
|
|
|
|
|
|
.filter_map(|w| {
|
|
|
|
|
|
let h = Scene::handle_from_wire_name(&w.name)?;
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e))
|
|
|
|
|
|
}).collect();
|
|
|
|
|
|
let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect();
|
|
|
|
|
|
let new_cmd = TrimCommand::new(all_entities);
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"EXTEND"|"EX" => {
|
|
|
|
|
|
use crate::modules::home::modify::trim::ExtendCommand;
|
|
|
|
|
|
let entities: Vec<_> = self.tabs[i].scene.entity_wires().iter()
|
|
|
|
|
|
.filter_map(|w| {
|
|
|
|
|
|
let h = Scene::handle_from_wire_name(&w.name)?;
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e))
|
|
|
|
|
|
}).collect();
|
|
|
|
|
|
let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect();
|
|
|
|
|
|
let new_cmd = ExtendCommand::new(all_entities);
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"3DORBIT"|"3O" => {
|
|
|
|
|
|
self.command_line.push_info("3D Orbit: drag with right mouse button.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 23:30:36 +03:00
|
|
|
|
// ── Selection utilities ───────────────────────────────────────
|
|
|
|
|
|
"SELECTALL"|"SA" => {
|
|
|
|
|
|
use crate::scene::Scene;
|
|
|
|
|
|
let handles: Vec<acadrust::Handle> = self.tabs[i].scene.entity_wires()
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.filter_map(|w| Scene::handle_from_wire_name(&w.name))
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let count = handles.len();
|
|
|
|
|
|
for h in handles {
|
|
|
|
|
|
self.tabs[i].scene.select_entity(h, false);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.command_line.push_output(&format!("SELECTALL: {} object(s) selected.", count));
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"DESELECT"|"DE"|"DESELALL" => {
|
|
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
|
|
self.command_line.push_output("Deselected.");
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── LIST — entity info ────────────────────────────────────────
|
|
|
|
|
|
"LIST" | "LI" => {
|
|
|
|
|
|
let selected: Vec<_> = self.tabs[i].scene.selected_entities();
|
|
|
|
|
|
if selected.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("LIST: no entities selected. Select entities first.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for (handle, _) in &selected {
|
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) {
|
2026-04-03 09:31:03 +03:00
|
|
|
|
let type_name = entity_type_name(entity);
|
2026-04-02 23:30:36 +03:00
|
|
|
|
let common = entity.common();
|
2026-04-03 09:31:03 +03:00
|
|
|
|
let color_str = common.color.index()
|
|
|
|
|
|
.map(|c| c.to_string())
|
|
|
|
|
|
.unwrap_or_else(|| "ByLayer".to_string());
|
|
|
|
|
|
let linetype = if common.linetype.is_empty() || common.linetype == "ByLayer" {
|
|
|
|
|
|
"ByLayer".to_string()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
common.linetype.clone()
|
|
|
|
|
|
};
|
|
|
|
|
|
// Entity-specific details
|
|
|
|
|
|
let details = entity_list_details(entity);
|
2026-04-02 23:30:36 +03:00
|
|
|
|
self.command_line.push_output(&format!(
|
2026-04-03 09:31:03 +03:00
|
|
|
|
"{type_name} Handle:{:X} Layer:{} Color:{} LT:{}{}",
|
|
|
|
|
|
handle.value(), common.layer, color_str, linetype,
|
|
|
|
|
|
if details.is_empty() { String::new() } else { format!("\n {details}") }
|
2026-04-02 23:30:36 +03:00
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 23:41:53 +03:00
|
|
|
|
// ── Break / Join ─────────────────────────────────────────────────
|
|
|
|
|
|
"JOIN"|"J" => {
|
|
|
|
|
|
use crate::modules::home::modify::join::JoinCommand;
|
|
|
|
|
|
let cmd = JoinCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 23:39:51 +03:00
|
|
|
|
"BREAK"|"BR" => {
|
|
|
|
|
|
use crate::modules::home::modify::break_cmd::BreakInteractiveCommand;
|
|
|
|
|
|
let cmd = BreakInteractiveCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
"BREAKATPOINT"|"BAP" => {
|
|
|
|
|
|
use crate::modules::home::modify::break_cmd::BreakAtPointCommand;
|
|
|
|
|
|
let cmd = BreakAtPointCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 09:19:36 +03:00
|
|
|
|
"PEDIT"|"PE" => {
|
|
|
|
|
|
use crate::modules::home::modify::pedit::PeditCommand;
|
|
|
|
|
|
let cmd_obj = PeditCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&cmd_obj.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 23:46:48 +03:00
|
|
|
|
"ALIGN"|"AL" => {
|
|
|
|
|
|
use crate::modules::home::modify::align::AlignCommand;
|
|
|
|
|
|
let cmd = AlignCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 23:45:06 +03:00
|
|
|
|
"LENGTHEN"|"LEN" => {
|
|
|
|
|
|
use crate::modules::home::modify::lengthen::LengthenCommand;
|
|
|
|
|
|
let cmd = LengthenCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 23:43:33 +03:00
|
|
|
|
"DIVIDE"|"DIV" => {
|
|
|
|
|
|
use crate::modules::home::inquiry::divide::DivideCommand;
|
|
|
|
|
|
let cmd = DivideCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"MEASURE"|"ME" => {
|
|
|
|
|
|
use crate::modules::home::inquiry::divide::MeasureCommand;
|
|
|
|
|
|
let cmd = MeasureCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 23:36:53 +03:00
|
|
|
|
// ── Inquiry ──────────────────────────────────────────────────────
|
|
|
|
|
|
"DIST"|"DI" => {
|
|
|
|
|
|
use crate::modules::home::inquiry::dist::DistCommand;
|
|
|
|
|
|
let cmd = DistCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"ID" => {
|
|
|
|
|
|
use crate::modules::home::inquiry::id::IdCommand;
|
|
|
|
|
|
let cmd = IdCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"AREA" => {
|
|
|
|
|
|
use crate::modules::home::inquiry::area::AreaCommand;
|
|
|
|
|
|
let cmd = AreaCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 09:25:39 +03:00
|
|
|
|
// ── FLATTEN — move selected (or all) entities to Z=0 ─────────────
|
|
|
|
|
|
"FLATTEN" => {
|
|
|
|
|
|
let handles: Vec<acadrust::Handle> = {
|
|
|
|
|
|
let sel = self.tabs[i].scene.selected_entities();
|
|
|
|
|
|
if sel.is_empty() {
|
|
|
|
|
|
// Flatten all entities
|
|
|
|
|
|
self.tabs[i].scene.document.entities()
|
|
|
|
|
|
.map(|e| e.common().handle)
|
|
|
|
|
|
.collect()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
sel.into_iter().map(|(h, _)| h).collect()
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
if handles.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("FLATTEN: no entities.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.push_undo_snapshot(i, "FLATTEN");
|
|
|
|
|
|
for h in &handles {
|
|
|
|
|
|
if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) {
|
|
|
|
|
|
flatten_entity_z(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"FLATTEN: {} entity(ies) moved to Z=0.", handles.len()
|
|
|
|
|
|
));
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 09:23:57 +03:00
|
|
|
|
// ── QSELECT — quick-select entities by property ───────────────────
|
|
|
|
|
|
// QSELECT TYPE <type> — select all entities of given type
|
|
|
|
|
|
// QSELECT LAYER <name> — select all entities on layer
|
|
|
|
|
|
// QSELECT COLOR <n> — select all entities with color index n
|
|
|
|
|
|
// QSELECT LINETYPE <name> — select all entities with linetype
|
|
|
|
|
|
cmd if cmd == "QSELECT" || cmd.starts_with("QSELECT ") => {
|
|
|
|
|
|
let rest = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or("");
|
|
|
|
|
|
let parts: Vec<&str> = rest.splitn(2, ' ').collect();
|
|
|
|
|
|
let prop = parts.first().map(|s| s.to_uppercase()).unwrap_or_default();
|
|
|
|
|
|
let val = parts.get(1).map(|s| s.trim()).unwrap_or("").to_uppercase();
|
|
|
|
|
|
|
|
|
|
|
|
let matched: Vec<acadrust::Handle> = self.tabs[i].scene.document
|
|
|
|
|
|
.entities()
|
|
|
|
|
|
.filter(|e| {
|
|
|
|
|
|
let c = e.common();
|
|
|
|
|
|
match prop.as_str() {
|
|
|
|
|
|
"TYPE" => entity_type_name(e).to_uppercase() == val,
|
|
|
|
|
|
"LAYER" => c.layer.to_uppercase() == val,
|
|
|
|
|
|
"COLOR" => c.color.index()
|
|
|
|
|
|
.map(|n| n.to_string() == val)
|
|
|
|
|
|
.unwrap_or(val == "BYLAYER"),
|
|
|
|
|
|
"LINETYPE" => c.linetype.to_uppercase() == val,
|
|
|
|
|
|
_ => false,
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.map(|e| e.common().handle)
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
|
|
if prop.is_empty() {
|
|
|
|
|
|
self.command_line.push_info(
|
|
|
|
|
|
"Usage: QSELECT TYPE|LAYER|COLOR|LINETYPE <value>"
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if matched.is_empty() {
|
|
|
|
|
|
self.command_line.push_output("QSELECT: no matching entities.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
|
|
for h in &matched {
|
|
|
|
|
|
self.tabs[i].scene.select_entity(*h, false);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"QSELECT: {} entity(ies) selected.", matched.len()
|
|
|
|
|
|
));
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 09:23:17 +03:00
|
|
|
|
// ── COUNT — entity statistics ─────────────────────────────────────
|
|
|
|
|
|
cmd if cmd == "COUNT" || cmd.starts_with("COUNT ") => {
|
|
|
|
|
|
let filter = cmd.split_once(' ').map(|(_, r)| r.trim().to_uppercase());
|
|
|
|
|
|
let mut counts: std::collections::BTreeMap<String, usize> = Default::default();
|
|
|
|
|
|
for e in self.tabs[i].scene.document.entities() {
|
|
|
|
|
|
let layer = &e.common().layer;
|
|
|
|
|
|
let type_name = entity_type_name(e);
|
|
|
|
|
|
let key = match &filter {
|
|
|
|
|
|
Some(f) if f == "LAYER" => layer.clone(),
|
|
|
|
|
|
Some(f) if f == "TYPE" => type_name.to_string(),
|
|
|
|
|
|
Some(f) => {
|
|
|
|
|
|
// Filter by layer name
|
|
|
|
|
|
if layer.to_uppercase() != *f { continue; }
|
|
|
|
|
|
type_name.to_string()
|
|
|
|
|
|
}
|
|
|
|
|
|
None => type_name.to_string(),
|
|
|
|
|
|
};
|
|
|
|
|
|
*counts.entry(key).or_default() += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
let total: usize = counts.values().sum();
|
|
|
|
|
|
for (k, n) in &counts {
|
|
|
|
|
|
self.command_line.push_output(&format!(" {k}: {n}"));
|
|
|
|
|
|
}
|
|
|
|
|
|
self.command_line.push_output(&format!("COUNT: {total} entity(ies) total."));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 09:22:27 +03:00
|
|
|
|
// ── Find / Replace ────────────────────────────────────────────────
|
|
|
|
|
|
// FIND <search> — list all Text/MText/Dimension containing <search>
|
|
|
|
|
|
// FIND <search> REPLACE <rep> — replace first occurrence (case-insensitive)
|
|
|
|
|
|
// FINDALL <search> REPLACE <rep> — replace all occurrences
|
|
|
|
|
|
cmd if cmd == "FIND" || cmd.starts_with("FIND ") || cmd == "FINDALL" || cmd.starts_with("FINDALL ") => {
|
|
|
|
|
|
let all_mode = cmd.starts_with("FINDALL");
|
|
|
|
|
|
let rest = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or("");
|
|
|
|
|
|
|
|
|
|
|
|
// Split at " REPLACE " keyword (case-insensitive)
|
|
|
|
|
|
let (search, replacement) = if let Some(pos) = rest.to_uppercase().find(" REPLACE ") {
|
|
|
|
|
|
(&rest[..pos], Some(rest[pos + 9..].trim()))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
(rest, None)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if search.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("FIND: specify search text.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let search_lc = search.to_lowercase();
|
|
|
|
|
|
let mut count = 0usize;
|
|
|
|
|
|
let handles: Vec<acadrust::Handle> = self.tabs[i].scene.document
|
|
|
|
|
|
.entities()
|
|
|
|
|
|
.filter_map(|e| {
|
|
|
|
|
|
let txt = entity_text_content(e)?;
|
|
|
|
|
|
if txt.to_lowercase().contains(&search_lc) {
|
|
|
|
|
|
Some(e.common().handle)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(rep) = replacement {
|
|
|
|
|
|
// Replace mode
|
|
|
|
|
|
let targets: Vec<_> = if all_mode {
|
|
|
|
|
|
handles.clone()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
handles.iter().copied().take(1).collect()
|
|
|
|
|
|
};
|
|
|
|
|
|
if targets.is_empty() {
|
|
|
|
|
|
self.command_line.push_output(&format!("FIND: \"{}\" not found.", search));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.push_undo_snapshot(i, "FIND/REPLACE");
|
|
|
|
|
|
for h in &targets {
|
|
|
|
|
|
if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) {
|
|
|
|
|
|
replace_entity_text(e, search, rep);
|
|
|
|
|
|
count += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"FIND/REPLACE: replaced {} occurrence(s) of \"{}\" → \"{}\".",
|
|
|
|
|
|
count, search, rep
|
|
|
|
|
|
));
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// List mode
|
|
|
|
|
|
if handles.is_empty() {
|
|
|
|
|
|
self.command_line.push_output(&format!("FIND: \"{}\" not found.", search));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for h in &handles {
|
|
|
|
|
|
if let Some(e) = self.tabs[i].scene.document.get_entity(*h) {
|
|
|
|
|
|
let txt = entity_text_content(e).unwrap_or_default();
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
" Handle {:X}: \"{}\"", h.value(), txt
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"FIND: {} match(es) for \"{}\".", handles.len(), search
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
|
"HELP"|"?" => {
|
|
|
|
|
|
self.command_line.push_output(
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
"Draw: LINE CIRCLE ARC PLINE RECT POLY POINT ELLIPSE SPLINE RAY XLINE HATCH DONUT REVCLOUD WIPEOUT MLINE ATTDEF | \
|
|
|
|
|
|
Modify: MOVE COPY ROTATE SCALE MIRROR ERASE OFFSET EXTEND FILLET CHAMFER STRETCH EXPLODE TRIM BREAK JOIN LENGTHEN ALIGN PEDIT | \
|
2026-04-02 23:23:34 +03:00
|
|
|
|
Array: ARRAY ARRAYRECT ARRAYPOLAR ARRAYPATH | \
|
|
|
|
|
|
Text: TEXT MTEXT LEADER MLEADER | \
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
Dimension: DIMLINEAR DIMALIGNED DIMANGULAR DIMRADIUS DIMDIAMETER DIMCONTINUE DIMBASELINE | \
|
|
|
|
|
|
Annotation: TOLERANCE | \
|
2026-04-03 09:25:39 +03:00
|
|
|
|
Inquiry: DIST ID AREA LIST FIND FINDALL COUNT QSELECT | Draw on entity: DIVIDE MEASURE | \
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
Attributes: ATTEDIT ATTDISP | \
|
|
|
|
|
|
Utilities: FLATTEN LAYISO LAYUNISO | \
|
|
|
|
|
|
View: ZOOM EXTENTS ZOOM WINDOW VIEW LIST/SAVE/RESTORE/DELETE | \
|
2026-04-02 23:23:34 +03:00
|
|
|
|
Layer: LAYER LIST/NEW/ON/OFF/FREEZE/THAW/LOCK/UNLOCK/COLOR/SET | \
|
|
|
|
|
|
Viewport: MVIEW VPLAYER VPORTS MS PS DRAWORDER | \
|
2026-04-02 23:31:36 +03:00
|
|
|
|
Tables: STYLE DIMSTYLE LINETYPE UCS RENAME PURGE | \
|
2026-04-02 23:23:34 +03:00
|
|
|
|
File: NEW OPEN SAVE SAVEAS PRINT PURGE UNDO REDO"
|
2026-03-22 22:44:32 +03:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
"DONATE" => {
|
|
|
|
|
|
let _ = open::that("https://patreon.com/HakanSeven12");
|
|
|
|
|
|
self.command_line.push_info("Opening Patreon page...");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Layout / viewport ──────────────────────────────────────────
|
|
|
|
|
|
"MVIEW"|"MV" => {
|
|
|
|
|
|
if self.tabs[i].scene.current_layout == "Model" {
|
2026-03-31 08:05:17 +03:00
|
|
|
|
self.command_line.push_error("MVIEW: switch to a paper space layout first.");
|
2026-03-22 22:44:32 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
use crate::modules::layout::mview::MviewCommand;
|
|
|
|
|
|
let new_cmd = MviewCommand::new();
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 12:52:07 +03:00
|
|
|
|
// ── MSPACE / PSPACE ───────────────────────────────────────────
|
|
|
|
|
|
"MS"|"MSPACE" => {
|
|
|
|
|
|
return Task::done(Message::MspaceCommand);
|
|
|
|
|
|
}
|
|
|
|
|
|
"PSPACE" => {
|
|
|
|
|
|
return Task::done(Message::PspaceCommand);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Feat: viewport display lock, extended properties, VPLAYER and VPORTS commands
- Enforce status.locked in pan_active_viewport and zoom_active_viewport:
locked viewports now reject pan/zoom inputs entirely
- Add perspective, render_mode, shade_plot_mode, hide_plot, ucs_icon_visible
fields to the viewport property panel with appropriate pickers/toggles
- Add VPORTS command: lists all user viewports in the current layout
(id, size, position, scale, on/locked state) in the command line
- Add VPLAYER command: per-viewport layer freeze/thaw from the command line
(F <layer> to freeze, T <layer> to thaw, Enter to exit)
- Add CmdResult::VpLayerUpdate variant and its handler in apply_cmd_result
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 23:52:18 +03:00
|
|
|
|
// ── VPORTS — list viewports in current layout ─────────────────
|
|
|
|
|
|
"VPORTS" => {
|
|
|
|
|
|
let scene = &self.tabs[i].scene;
|
|
|
|
|
|
if scene.current_layout == "Model" {
|
|
|
|
|
|
self.command_line.push_error("VPORTS: switch to a paper space layout first.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let layout_block = scene.current_layout_block_handle_pub();
|
|
|
|
|
|
let viewports: Vec<_> = scene.document.entities()
|
|
|
|
|
|
.filter_map(|e| {
|
|
|
|
|
|
if let acadrust::EntityType::Viewport(vp) = e {
|
|
|
|
|
|
if vp.id > 1 && vp.common.owner_handle == layout_block {
|
|
|
|
|
|
Some((vp.id, vp.center.clone(), vp.width, vp.height, vp.custom_scale, vp.status.is_on, vp.status.locked))
|
|
|
|
|
|
} else { None }
|
|
|
|
|
|
} else { None }
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
if viewports.is_empty() {
|
|
|
|
|
|
self.command_line.push_info("No viewports in current layout. Use MVIEW to create one.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_output(&format!("{} viewport(s) in layout \"{}\":", viewports.len(), scene.current_layout));
|
|
|
|
|
|
for (id, center, w, h, scale, is_on, locked) in &viewports {
|
|
|
|
|
|
let state = match (is_on, locked) {
|
|
|
|
|
|
(true, true) => "On, Locked",
|
|
|
|
|
|
(true, false) => "On",
|
|
|
|
|
|
(false, _) => "Off",
|
|
|
|
|
|
};
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
" VP #{id}: {w:.1}×{h:.1} @ ({:.1},{:.1}) scale={scale:.4} [{state}]",
|
|
|
|
|
|
center.x, center.y
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── VPLAYER — per-viewport layer freeze/thaw ──────────────────
|
|
|
|
|
|
"VPLAYER" => {
|
|
|
|
|
|
let scene = &self.tabs[i].scene;
|
|
|
|
|
|
if scene.current_layout == "Model" {
|
|
|
|
|
|
self.command_line.push_error("VPLAYER: switch to a paper space layout first.");
|
|
|
|
|
|
} else if scene.active_viewport.is_none() {
|
|
|
|
|
|
self.command_line.push_error("VPLAYER: enter a viewport first (double-click or MS).");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
use crate::modules::layout::vplayer::VplayerCommand;
|
|
|
|
|
|
let vp_handle = scene.active_viewport.unwrap();
|
|
|
|
|
|
// Collect current frozen layer names for display.
|
|
|
|
|
|
let frozen_names: Vec<String> = {
|
|
|
|
|
|
if let Some(acadrust::EntityType::Viewport(vp)) =
|
|
|
|
|
|
scene.document.get_entity(vp_handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
vp.frozen_layers.iter().filter_map(|h| {
|
|
|
|
|
|
scene.document.layers.iter().find(|l| l.handle == *h).map(|l| l.name.clone())
|
|
|
|
|
|
}).collect()
|
|
|
|
|
|
} else { vec![] }
|
|
|
|
|
|
};
|
|
|
|
|
|
if frozen_names.is_empty() {
|
|
|
|
|
|
self.command_line.push_info("VPLAYER: no frozen layers in active viewport.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
"VPLAYER: frozen layers: {}",
|
|
|
|
|
|
frozen_names.join(", ")
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
let new_cmd = VplayerCommand::new(vp_handle);
|
|
|
|
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
|
|
|
|
}
|
2026-04-02 19:13:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Draw Order ────────────────────────────────────────────────
|
|
|
|
|
|
cmd if cmd.starts_with("DRAWORDER") => {
|
|
|
|
|
|
use acadrust::objects::{ObjectType, SortEntitiesTable};
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
let parts: Vec<&str> = cmd.split_whitespace().collect();
|
|
|
|
|
|
let option = parts.get(1).unwrap_or(&"").to_uppercase();
|
2026-04-02 19:13:37 +03:00
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let selected: Vec<acadrust::Handle> = self.tabs[i].scene
|
|
|
|
|
|
.selected_entities()
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|(h, _)| *h)
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
if selected.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("DRAWORDER: select entities first.");
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
// Parse relative target handle for ABOVE/UNDER.
|
|
|
|
|
|
let relative_target: Option<(bool, acadrust::Handle)> = match option.as_str() {
|
|
|
|
|
|
"A" | "ABOVE" => {
|
|
|
|
|
|
let h_val = parts.get(2).and_then(|s| u64::from_str_radix(s, 16).ok());
|
|
|
|
|
|
h_val.map(|v| (true, acadrust::Handle::new(v)))
|
|
|
|
|
|
}
|
|
|
|
|
|
"U" | "UNDER" | "BELOW" => {
|
|
|
|
|
|
let h_val = parts.get(2).and_then(|s| u64::from_str_radix(s, 16).ok());
|
|
|
|
|
|
h_val.map(|v| (false, acadrust::Handle::new(v)))
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
};
|
|
|
|
|
|
let to_front_opt = match option.as_str() {
|
|
|
|
|
|
"F" | "FRONT" => Some(true),
|
|
|
|
|
|
"B" | "BACK" => Some(false),
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if relative_target.is_some() || to_front_opt.is_some() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "DRAWORDER");
|
|
|
|
|
|
let block_handle = self.tabs[i].scene.current_layout_block_handle_pub();
|
|
|
|
|
|
let doc = &mut self.tabs[i].scene.document;
|
|
|
|
|
|
let table_handle = doc.objects.iter()
|
|
|
|
|
|
.find_map(|(h, obj)| {
|
|
|
|
|
|
if let ObjectType::SortEntitiesTable(t) = obj {
|
|
|
|
|
|
if t.block_owner_handle == block_handle { Some(*h) } else { None }
|
|
|
|
|
|
} else { None }
|
|
|
|
|
|
});
|
|
|
|
|
|
let get_or_create = |doc: &mut acadrust::CadDocument, block_handle| -> acadrust::Handle {
|
|
|
|
|
|
if let Some(th) = doc.objects.iter()
|
|
|
|
|
|
.find_map(|(h, obj)| {
|
|
|
|
|
|
if let ObjectType::SortEntitiesTable(t) = obj {
|
|
|
|
|
|
if t.block_owner_handle == block_handle { Some(*h) } else { None }
|
|
|
|
|
|
} else { None }
|
|
|
|
|
|
})
|
|
|
|
|
|
{
|
|
|
|
|
|
th
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let nh = acadrust::Handle::new(doc.next_handle());
|
|
|
|
|
|
let mut table = SortEntitiesTable::for_block(block_handle);
|
|
|
|
|
|
table.handle = nh;
|
|
|
|
|
|
doc.objects.insert(nh, ObjectType::SortEntitiesTable(table));
|
|
|
|
|
|
nh
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
let th = table_handle.unwrap_or_else(|| {
|
|
|
|
|
|
let nh = acadrust::Handle::new(doc.next_handle());
|
|
|
|
|
|
let mut table = SortEntitiesTable::for_block(block_handle);
|
|
|
|
|
|
table.handle = nh;
|
|
|
|
|
|
doc.objects.insert(nh, ObjectType::SortEntitiesTable(table));
|
|
|
|
|
|
nh
|
2026-04-02 19:13:37 +03:00
|
|
|
|
});
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
let _ = get_or_create; // suppress unused warning
|
|
|
|
|
|
if let Some(ObjectType::SortEntitiesTable(table)) = doc.objects.get_mut(&th) {
|
|
|
|
|
|
if let Some((above, target)) = relative_target {
|
|
|
|
|
|
for h in &selected {
|
|
|
|
|
|
if above { table.move_above(*h, target); }
|
|
|
|
|
|
else { table.move_below(*h, target); }
|
|
|
|
|
|
}
|
|
|
|
|
|
let rel = if above { "above" } else { "below" };
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
"DRAWORDER: moved {} entities {} {:x}.", selected.len(), rel, target.value()
|
|
|
|
|
|
));
|
|
|
|
|
|
} else if let Some(to_front) = to_front_opt {
|
|
|
|
|
|
for h in &selected {
|
|
|
|
|
|
if to_front { table.bring_to_front(*h); }
|
|
|
|
|
|
else { table.send_to_back(*h); }
|
|
|
|
|
|
}
|
|
|
|
|
|
let dir = if to_front { "front" } else { "back" };
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
"DRAWORDER: moved {} entities to {}.", selected.len(), dir
|
|
|
|
|
|
));
|
2026-04-02 19:13:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
self.tabs[i].dirty = true;
|
2026-04-02 19:13:37 +03:00
|
|
|
|
} else {
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
self.command_line.push_info(
|
|
|
|
|
|
"Usage: DRAWORDER F|FRONT | B|BACK | A|ABOVE <handle> | U|UNDER <handle>"
|
|
|
|
|
|
);
|
2026-04-02 19:13:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-02 19:19:12 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 23:18:41 +03:00
|
|
|
|
// ── LAYER management ─────────────────────────────────────────
|
2026-04-02 23:20:34 +03:00
|
|
|
|
cmd if cmd == "LAYER" || cmd.starts_with("LAYER ") || cmd.starts_with("LA ") => {
|
2026-04-02 23:18:41 +03:00
|
|
|
|
use acadrust::tables::Layer;
|
|
|
|
|
|
let raw_rest = if cmd.starts_with("LAYER ") {
|
|
|
|
|
|
cmd.trim_start_matches("LAYER ").trim()
|
|
|
|
|
|
} else if cmd.starts_with("LA ") {
|
|
|
|
|
|
cmd.trim_start_matches("LA ").trim()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
""
|
|
|
|
|
|
};
|
|
|
|
|
|
let parts: Vec<&str> = raw_rest.split_whitespace().collect();
|
|
|
|
|
|
let sub = parts.get(0).map(|s| s.to_uppercase()).unwrap_or_default();
|
|
|
|
|
|
match sub.as_str() {
|
|
|
|
|
|
"" | "LIST" | "?" => {
|
|
|
|
|
|
let info: Vec<String> = self.tabs[i].scene.document.layers.iter().map(|l| {
|
|
|
|
|
|
let state = if l.flags.frozen { "frozen" }
|
|
|
|
|
|
else if l.flags.off { "off" }
|
|
|
|
|
|
else if l.flags.locked { "locked" }
|
|
|
|
|
|
else { "on" };
|
|
|
|
|
|
format!("{}({})", l.name, state)
|
|
|
|
|
|
}).collect();
|
|
|
|
|
|
self.command_line.push_output(&format!("Layers: {}", info.join(", ")));
|
|
|
|
|
|
}
|
|
|
|
|
|
"NEW" | "N" => {
|
|
|
|
|
|
let name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string();
|
|
|
|
|
|
if name.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("Usage: LAYER NEW <name>");
|
|
|
|
|
|
} else if self.tabs[i].scene.document.layers.contains(&name) {
|
|
|
|
|
|
self.command_line.push_error(&format!("LAYER: '{}' already exists.", name));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let layer = Layer::new(&name);
|
|
|
|
|
|
let _ = self.tabs[i].scene.document.layers.add(layer);
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYER NEW");
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!("LAYER: '{}' created.", name));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"ON" => {
|
|
|
|
|
|
for name in &parts[1..] {
|
|
|
|
|
|
if let Some(l) = self.tabs[i].scene.document.layers.get_mut(name) {
|
|
|
|
|
|
l.flags.off = false; l.flags.frozen = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYER ON");
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output("LAYER: layers turned on.");
|
|
|
|
|
|
}
|
|
|
|
|
|
"OFF" => {
|
|
|
|
|
|
for name in &parts[1..] {
|
|
|
|
|
|
if let Some(l) = self.tabs[i].scene.document.layers.get_mut(name) {
|
|
|
|
|
|
l.flags.off = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYER OFF");
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output("LAYER: layers turned off.");
|
|
|
|
|
|
}
|
|
|
|
|
|
"FREEZE" | "FR" => {
|
|
|
|
|
|
for name in &parts[1..] {
|
|
|
|
|
|
if let Some(l) = self.tabs[i].scene.document.layers.get_mut(name) {
|
|
|
|
|
|
l.flags.frozen = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYER FREEZE");
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output("LAYER: layers frozen.");
|
|
|
|
|
|
}
|
|
|
|
|
|
"THAW" | "TH" => {
|
|
|
|
|
|
for name in &parts[1..] {
|
|
|
|
|
|
if let Some(l) = self.tabs[i].scene.document.layers.get_mut(name) {
|
|
|
|
|
|
l.flags.frozen = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYER THAW");
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output("LAYER: layers thawed.");
|
|
|
|
|
|
}
|
|
|
|
|
|
"LOCK" | "LO" => {
|
|
|
|
|
|
for name in &parts[1..] {
|
|
|
|
|
|
if let Some(l) = self.tabs[i].scene.document.layers.get_mut(name) {
|
|
|
|
|
|
l.flags.locked = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYER LOCK");
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output("LAYER: layers locked.");
|
|
|
|
|
|
}
|
|
|
|
|
|
"UNLOCK" | "UL" => {
|
|
|
|
|
|
for name in &parts[1..] {
|
|
|
|
|
|
if let Some(l) = self.tabs[i].scene.document.layers.get_mut(name) {
|
|
|
|
|
|
l.flags.locked = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYER UNLOCK");
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output("LAYER: layers unlocked.");
|
|
|
|
|
|
}
|
|
|
|
|
|
"COLOR" | "C" => {
|
|
|
|
|
|
// LAYER COLOR <name> <aci_index>
|
|
|
|
|
|
let layer_name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string();
|
|
|
|
|
|
let color_str = parts.get(2).map(|s| s.trim()).unwrap_or("");
|
|
|
|
|
|
if let Ok(idx) = color_str.parse::<i16>() {
|
|
|
|
|
|
if let Some(l) = self.tabs[i].scene.document.layers.get_mut(&layer_name) {
|
|
|
|
|
|
l.color = acadrust::types::Color::from_index(idx);
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYER COLOR");
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!("LAYER: '{}' color set to ACI {}.", layer_name, idx));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error(&format!("LAYER: '{}' not found.", layer_name));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error("Usage: LAYER COLOR <name> <aci_index>");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"SET" | "S" | "CURRENT" => {
|
|
|
|
|
|
let name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string();
|
|
|
|
|
|
if self.tabs[i].scene.document.layers.contains(&name) {
|
|
|
|
|
|
self.tabs[i].layers.current_layer = name.clone();
|
|
|
|
|
|
self.command_line.push_output(&format!("LAYER: current layer set to '{}'.", name));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error(&format!("LAYER: '{}' not found.", name));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
self.command_line.push_info(
|
|
|
|
|
|
"Usage: LAYER LIST | NEW <name> | ON/OFF/FREEZE/THAW/LOCK/UNLOCK <name> | COLOR <name> <aci> | SET <name>"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 23:16:37 +03:00
|
|
|
|
// ── UCS management ───────────────────────────────────────────
|
|
|
|
|
|
cmd if cmd == "UCS" || cmd.starts_with("UCS ") => {
|
|
|
|
|
|
use acadrust::tables::Ucs;
|
2026-04-06 14:16:06 +03:00
|
|
|
|
use acadrust::types::Vector3;
|
|
|
|
|
|
use super::helpers::{ucs_to_wcs, ucs_z_axis, ucs_rotated_z};
|
|
|
|
|
|
let parts: Vec<&str> = cmd.splitn(4, ' ').collect();
|
2026-04-02 23:16:37 +03:00
|
|
|
|
let sub = parts.get(1).map(|s| s.to_uppercase()).unwrap_or_default();
|
|
|
|
|
|
match sub.as_str() {
|
|
|
|
|
|
"" | "LIST" | "?" => {
|
2026-04-06 14:16:06 +03:00
|
|
|
|
let active_name = self.tabs[i].active_ucs.as_ref()
|
|
|
|
|
|
.map(|u| u.name.clone())
|
|
|
|
|
|
.unwrap_or_else(|| "WCS".into());
|
2026-04-02 23:16:37 +03:00
|
|
|
|
let names: Vec<String> = self.tabs[i].scene.document
|
|
|
|
|
|
.ucss.iter().map(|u| u.name.clone()).collect();
|
|
|
|
|
|
if names.is_empty() {
|
2026-04-06 14:16:06 +03:00
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"Active UCS: {} | No named UCSs defined.", active_name
|
|
|
|
|
|
));
|
2026-04-02 23:16:37 +03:00
|
|
|
|
} else {
|
2026-04-06 14:16:06 +03:00
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"Active UCS: {} | Named: {}", active_name, names.join(", ")
|
|
|
|
|
|
));
|
2026-04-02 23:16:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"SAVE" | "S" => {
|
|
|
|
|
|
let name = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string();
|
|
|
|
|
|
if name.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("Usage: UCS SAVE <name>");
|
|
|
|
|
|
} else {
|
2026-04-06 14:16:06 +03:00
|
|
|
|
// Save the current active UCS under this name.
|
|
|
|
|
|
let ucs = match &self.tabs[i].active_ucs {
|
|
|
|
|
|
Some(u) => {
|
|
|
|
|
|
let mut saved = u.clone();
|
|
|
|
|
|
saved.name = name.clone();
|
|
|
|
|
|
saved
|
|
|
|
|
|
}
|
|
|
|
|
|
None => Ucs::new(&name), // save WCS (identity)
|
|
|
|
|
|
};
|
2026-04-02 23:16:37 +03:00
|
|
|
|
self.tabs[i].scene.document.ucss.add_or_replace(ucs);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
2026-04-06 14:16:06 +03:00
|
|
|
|
self.command_line.push_output(&format!("UCS '{}' saved.", name));
|
2026-04-02 23:16:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"DELETE" | "DEL" | "D" => {
|
|
|
|
|
|
let name = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string();
|
|
|
|
|
|
if name.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("Usage: UCS DELETE <name>");
|
|
|
|
|
|
} else if self.tabs[i].scene.document.ucss.remove(&name).is_some() {
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!("UCS '{}' deleted.", name));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error(&format!("UCS '{}' not found.", name));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"W" | "WORLD" => {
|
2026-04-06 14:16:06 +03:00
|
|
|
|
self.tabs[i].active_ucs = None;
|
2026-04-02 23:16:37 +03:00
|
|
|
|
self.command_line.push_output("UCS reset to World Coordinate System.");
|
|
|
|
|
|
}
|
2026-04-06 14:16:06 +03:00
|
|
|
|
// UCS ORIGIN x,y,z — shift the active UCS origin, keep axes
|
|
|
|
|
|
"ORIGIN" | "O" => {
|
|
|
|
|
|
let coord_str = parts.get(2).copied().unwrap_or("");
|
|
|
|
|
|
if let Some(pt) = super::helpers::parse_coord(coord_str) {
|
|
|
|
|
|
// `pt` is in current UCS space; convert to WCS
|
|
|
|
|
|
let wcs_origin = if let Some(ref ucs) = self.tabs[i].active_ucs {
|
|
|
|
|
|
ucs_to_wcs(pt, ucs)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
pt
|
|
|
|
|
|
};
|
|
|
|
|
|
let ucs = self.tabs[i].active_ucs.get_or_insert_with(|| Ucs::new("*ACTIVE*"));
|
|
|
|
|
|
ucs.origin = Vector3::new(
|
|
|
|
|
|
wcs_origin.x as f64, wcs_origin.y as f64, wcs_origin.z as f64,
|
|
|
|
|
|
);
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"UCS origin set to ({:.4}, {:.4}, {:.4}).",
|
|
|
|
|
|
wcs_origin.x, wcs_origin.y, wcs_origin.z
|
|
|
|
|
|
));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error("Usage: UCS ORIGIN x,y,z");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// UCS Z angle — rotate active UCS around its Z axis by degrees
|
|
|
|
|
|
"Z" => {
|
|
|
|
|
|
let deg: Option<f32> = parts.get(2).and_then(|s| s.trim().parse().ok());
|
|
|
|
|
|
if let Some(angle_deg) = deg {
|
|
|
|
|
|
let rad = angle_deg.to_radians();
|
|
|
|
|
|
let current = self.tabs[i].active_ucs.as_ref();
|
|
|
|
|
|
let origin = current.map(|u| {
|
|
|
|
|
|
glam::Vec3::new(
|
|
|
|
|
|
u.origin.x as f32, u.origin.y as f32, u.origin.z as f32,
|
|
|
|
|
|
)
|
|
|
|
|
|
}).unwrap_or(glam::Vec3::ZERO);
|
|
|
|
|
|
let mut new_ucs = ucs_rotated_z(origin, rad);
|
|
|
|
|
|
// If already had axes, compose rotation on top
|
|
|
|
|
|
if let Some(ref ucs) = self.tabs[i].active_ucs {
|
|
|
|
|
|
let old_x = glam::Vec3::new(
|
|
|
|
|
|
ucs.x_axis.x as f32, ucs.x_axis.y as f32, ucs.x_axis.z as f32,
|
|
|
|
|
|
);
|
|
|
|
|
|
let old_y = glam::Vec3::new(
|
|
|
|
|
|
ucs.y_axis.x as f32, ucs.y_axis.y as f32, ucs.y_axis.z as f32,
|
|
|
|
|
|
);
|
|
|
|
|
|
let z_ax = ucs_z_axis(ucs);
|
|
|
|
|
|
let rot = glam::Quat::from_axis_angle(z_ax, rad);
|
|
|
|
|
|
let nx = rot * old_x;
|
|
|
|
|
|
let ny = rot * old_y;
|
|
|
|
|
|
new_ucs.x_axis = Vector3::new(
|
|
|
|
|
|
nx.x as f64, nx.y as f64, nx.z as f64,
|
|
|
|
|
|
);
|
|
|
|
|
|
new_ucs.y_axis = Vector3::new(
|
|
|
|
|
|
ny.x as f64, ny.y as f64, ny.z as f64,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].active_ucs = Some(new_ucs);
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"UCS rotated {:.2}° around Z.", angle_deg
|
|
|
|
|
|
));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error("Usage: UCS Z <angle_degrees>");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// UCS X angle — rotate around current UCS X axis
|
|
|
|
|
|
"X" => {
|
|
|
|
|
|
let deg: Option<f32> = parts.get(2).and_then(|s| s.trim().parse().ok());
|
|
|
|
|
|
if let Some(angle_deg) = deg {
|
|
|
|
|
|
let rad = angle_deg.to_radians();
|
|
|
|
|
|
let ucs = self.tabs[i].active_ucs.get_or_insert_with(|| Ucs::new("*ACTIVE*"));
|
|
|
|
|
|
let x_ax = glam::Vec3::new(
|
|
|
|
|
|
ucs.x_axis.x as f32, ucs.x_axis.y as f32, ucs.x_axis.z as f32,
|
|
|
|
|
|
);
|
|
|
|
|
|
let old_y = glam::Vec3::new(
|
|
|
|
|
|
ucs.y_axis.x as f32, ucs.y_axis.y as f32, ucs.y_axis.z as f32,
|
|
|
|
|
|
);
|
|
|
|
|
|
let rot = glam::Quat::from_axis_angle(x_ax, rad);
|
|
|
|
|
|
let ny = rot * old_y;
|
|
|
|
|
|
ucs.y_axis = Vector3::new(ny.x as f64, ny.y as f64, ny.z as f64);
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"UCS rotated {:.2}° around X.", angle_deg
|
|
|
|
|
|
));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error("Usage: UCS X <angle_degrees>");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// UCS Y angle — rotate around current UCS Y axis
|
|
|
|
|
|
"Y" => {
|
|
|
|
|
|
let deg: Option<f32> = parts.get(2).and_then(|s| s.trim().parse().ok());
|
|
|
|
|
|
if let Some(angle_deg) = deg {
|
|
|
|
|
|
let rad = angle_deg.to_radians();
|
|
|
|
|
|
let ucs = self.tabs[i].active_ucs.get_or_insert_with(|| Ucs::new("*ACTIVE*"));
|
|
|
|
|
|
let y_ax = glam::Vec3::new(
|
|
|
|
|
|
ucs.y_axis.x as f32, ucs.y_axis.y as f32, ucs.y_axis.z as f32,
|
|
|
|
|
|
);
|
|
|
|
|
|
let old_x = glam::Vec3::new(
|
|
|
|
|
|
ucs.x_axis.x as f32, ucs.x_axis.y as f32, ucs.x_axis.z as f32,
|
|
|
|
|
|
);
|
|
|
|
|
|
let rot = glam::Quat::from_axis_angle(y_ax, rad);
|
|
|
|
|
|
let nx = rot * old_x;
|
|
|
|
|
|
ucs.x_axis = Vector3::new(nx.x as f64, nx.y as f64, nx.z as f64);
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"UCS rotated {:.2}° around Y.", angle_deg
|
|
|
|
|
|
));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error("Usage: UCS Y <angle_degrees>");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-02 23:16:37 +03:00
|
|
|
|
_ => {
|
2026-04-06 14:16:06 +03:00
|
|
|
|
// UCS <name> — activate a named UCS
|
2026-04-02 23:16:37 +03:00
|
|
|
|
let name = sub.clone();
|
2026-04-06 14:16:06 +03:00
|
|
|
|
if let Some(named) = self.tabs[i].scene.document.ucss.get(&name).cloned() {
|
|
|
|
|
|
self.tabs[i].active_ucs = Some(named);
|
|
|
|
|
|
self.command_line.push_output(&format!("UCS '{}' activated.", name));
|
2026-04-02 23:16:37 +03:00
|
|
|
|
} else {
|
2026-04-06 14:16:06 +03:00
|
|
|
|
self.command_line.push_error(&format!(
|
|
|
|
|
|
"UCS '{}' not found. Usage: UCS LIST | SAVE <name> | DELETE <name> | W | ORIGIN x,y,z | X/Y/Z <angle>",
|
|
|
|
|
|
name
|
|
|
|
|
|
));
|
2026-04-02 23:16:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 19:19:12 +03:00
|
|
|
|
// ── Named Views (VIEW command) ────────────────────────────────
|
|
|
|
|
|
cmd if cmd == "VIEW" || cmd.starts_with("VIEW ") => {
|
|
|
|
|
|
let parts: Vec<&str> = cmd.splitn(3, ' ').collect();
|
|
|
|
|
|
let sub = parts.get(1).map(|s| s.to_uppercase()).unwrap_or_default();
|
|
|
|
|
|
match sub.as_str() {
|
|
|
|
|
|
"" | "LIST" | "?" => {
|
|
|
|
|
|
let views: Vec<String> = self.tabs[i].scene.document
|
|
|
|
|
|
.views.iter().map(|v| v.name.clone()).collect();
|
|
|
|
|
|
if views.is_empty() {
|
|
|
|
|
|
self.command_line.push_output("No named views saved.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"Named views: {}", views.join(", ")
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"SAVE" | "S" => {
|
|
|
|
|
|
let name = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string();
|
|
|
|
|
|
if name.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("Usage: VIEW SAVE <name>");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let new_view = self.tabs[i].scene.current_as_named_view(&name);
|
|
|
|
|
|
self.tabs[i].scene.document.views.add_or_replace(new_view);
|
|
|
|
|
|
self.command_line.push_output(&format!("View '{}' saved.", name));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"DELETE" | "DEL" | "D" => {
|
|
|
|
|
|
let name = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string();
|
|
|
|
|
|
if name.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("Usage: VIEW DELETE <name>");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if self.tabs[i].scene.document.views.remove(&name).is_some() {
|
|
|
|
|
|
self.command_line.push_output(&format!("View '{}' deleted.", name));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error(&format!("View '{}' not found.", name));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"RESTORE" | "R" => {
|
|
|
|
|
|
let name = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string();
|
|
|
|
|
|
if name.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("Usage: VIEW RESTORE <name>");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let found = self.tabs[i].scene.document.views.get(&name).cloned();
|
|
|
|
|
|
if let Some(v) = found {
|
|
|
|
|
|
self.tabs[i].scene.restore_named_view(&v);
|
|
|
|
|
|
self.command_line.push_output(&format!("View '{}' restored.", v.name));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error(&format!("View '{}' not found.", name));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// VIEW <name> shortcut for restore
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
let name = sub.clone();
|
|
|
|
|
|
let found = self.tabs[i].scene.document.views.get(&name).cloned();
|
|
|
|
|
|
if let Some(v) = found {
|
|
|
|
|
|
self.tabs[i].scene.restore_named_view(&v);
|
|
|
|
|
|
self.command_line.push_output(&format!("View '{}' restored.", v.name));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error(
|
|
|
|
|
|
"Usage: VIEW LIST | VIEW SAVE <name> | VIEW RESTORE <name> | VIEW DELETE <name>"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-02 23:14:51 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 23:19:57 +03:00
|
|
|
|
// ── DimStyle management ───────────────────────────────────────
|
2026-04-06 14:57:11 +03:00
|
|
|
|
// TABLESTYLE — Table Style Manager.
|
|
|
|
|
|
cmd if cmd == "TABLESTYLE" || cmd == "TS" || cmd.starts_with("TABLESTYLE ") => {
|
|
|
|
|
|
use acadrust::objects::{TableStyle, ObjectType};
|
|
|
|
|
|
let raw_rest = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or("");
|
|
|
|
|
|
let parts: Vec<&str> = raw_rest.split_whitespace().collect();
|
|
|
|
|
|
let sub = parts.first().map(|s| s.to_uppercase()).unwrap_or_default();
|
|
|
|
|
|
match sub.as_str() {
|
|
|
|
|
|
"" | "DIALOG" | "UI" => {
|
|
|
|
|
|
return Task::done(Message::TableStyleDialogOpen);
|
|
|
|
|
|
}
|
|
|
|
|
|
"LIST" | "?" => {
|
|
|
|
|
|
let doc = &self.tabs[i].scene.document;
|
|
|
|
|
|
let styles: Vec<String> = doc.objects.values()
|
|
|
|
|
|
.filter_map(|o| if let ObjectType::TableStyle(s) = o { Some(s) } else { None })
|
|
|
|
|
|
.map(|s| format!("{} (h_margin:{:.2} v_margin:{:.2})", s.name, s.horizontal_margin, s.vertical_margin))
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
if styles.is_empty() {
|
|
|
|
|
|
self.command_line.push_output("No table styles.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_output(&format!("TableStyles:\n {}", styles.join("\n ")));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"NEW" | "N" => {
|
|
|
|
|
|
let name = parts.get(1).copied().unwrap_or("").to_string();
|
|
|
|
|
|
if name.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("Usage: TABLESTYLE NEW <name>");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let doc = &self.tabs[i].scene.document;
|
|
|
|
|
|
let exists = doc.objects.values().any(|o| {
|
|
|
|
|
|
matches!(o, ObjectType::TableStyle(s) if s.name.eq_ignore_ascii_case(&name))
|
|
|
|
|
|
});
|
|
|
|
|
|
if exists {
|
|
|
|
|
|
self.command_line.push_error(&format!("TABLESTYLE: '{}' already exists.", name));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.push_undo_snapshot(i, "TABLESTYLE NEW");
|
|
|
|
|
|
let mut style = TableStyle::standard();
|
|
|
|
|
|
style.name = name.clone();
|
|
|
|
|
|
let nh = acadrust::Handle::new(self.tabs[i].scene.document.next_handle());
|
|
|
|
|
|
style.handle = nh;
|
|
|
|
|
|
self.tabs[i].scene.document.objects.insert(nh, ObjectType::TableStyle(style));
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!("TABLESTYLE: '{}' created.", name));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
self.command_line.push_error(
|
|
|
|
|
|
"Usage: TABLESTYLE [LIST|NEW <name>]"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-06 14:54:05 +03:00
|
|
|
|
// MLSTYLE — Multiline Style Manager.
|
|
|
|
|
|
// Usage:
|
|
|
|
|
|
// MLSTYLE — open dialog
|
|
|
|
|
|
// MLSTYLE LIST / ? — list all multiline styles
|
|
|
|
|
|
// MLSTYLE NEW <name> — create a new style
|
|
|
|
|
|
// MLSTYLE SET <name> — set current multiline style
|
|
|
|
|
|
// MLSTYLE DEL <name> — delete a style (not Standard)
|
|
|
|
|
|
cmd if cmd == "MLSTYLE" || cmd.starts_with("MLSTYLE ") => {
|
|
|
|
|
|
use acadrust::objects::{MLineStyle, ObjectType};
|
|
|
|
|
|
let raw_rest = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or("");
|
|
|
|
|
|
let parts: Vec<&str> = raw_rest.split_whitespace().collect();
|
|
|
|
|
|
let sub = parts.first().map(|s| s.to_uppercase()).unwrap_or_default();
|
|
|
|
|
|
match sub.as_str() {
|
|
|
|
|
|
"" | "DIALOG" | "UI" => {
|
|
|
|
|
|
return Task::done(Message::MlStyleDialogOpen);
|
|
|
|
|
|
}
|
|
|
|
|
|
"LIST" | "?" => {
|
|
|
|
|
|
let doc = &self.tabs[i].scene.document;
|
|
|
|
|
|
let current = &doc.header.multiline_style;
|
|
|
|
|
|
let styles: Vec<String> = doc.objects.values()
|
|
|
|
|
|
.filter_map(|o| if let ObjectType::MLineStyle(s) = o { Some(s) } else { None })
|
|
|
|
|
|
.map(|s| {
|
|
|
|
|
|
let cur = if &s.name == current { " (current)" } else { "" };
|
|
|
|
|
|
format!("{} [{}]{}",
|
|
|
|
|
|
s.name,
|
|
|
|
|
|
s.elements.len(),
|
|
|
|
|
|
cur)
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
if styles.is_empty() {
|
|
|
|
|
|
self.command_line.push_output("No multiline styles.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_output(&format!("MLineStyles:\n {}", styles.join("\n ")));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"NEW" | "N" => {
|
|
|
|
|
|
let name = parts.get(1).copied().unwrap_or("").to_string();
|
|
|
|
|
|
if name.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("Usage: MLSTYLE NEW <name>");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let doc = &self.tabs[i].scene.document;
|
|
|
|
|
|
let exists = doc.objects.values().any(|o| {
|
|
|
|
|
|
matches!(o, ObjectType::MLineStyle(s) if s.name.eq_ignore_ascii_case(&name))
|
|
|
|
|
|
});
|
|
|
|
|
|
if exists {
|
|
|
|
|
|
self.command_line.push_error(&format!("MLSTYLE: '{}' already exists.", name));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.push_undo_snapshot(i, "MLSTYLE NEW");
|
|
|
|
|
|
let mut style = MLineStyle::standard();
|
|
|
|
|
|
style.name = name.clone();
|
|
|
|
|
|
let nh = acadrust::Handle::new(self.tabs[i].scene.document.next_handle());
|
|
|
|
|
|
style.handle = nh;
|
|
|
|
|
|
self.tabs[i].scene.document.objects.insert(
|
|
|
|
|
|
nh, ObjectType::MLineStyle(style)
|
|
|
|
|
|
);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!("MLSTYLE: '{}' created.", name));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"SET" | "S" => {
|
|
|
|
|
|
let name = parts.get(1).copied().unwrap_or("").to_string();
|
|
|
|
|
|
if name.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("Usage: MLSTYLE SET <name>");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let doc = &self.tabs[i].scene.document;
|
|
|
|
|
|
let exists = doc.objects.values().any(|o| {
|
|
|
|
|
|
matches!(o, ObjectType::MLineStyle(s) if s.name.eq_ignore_ascii_case(&name))
|
|
|
|
|
|
});
|
|
|
|
|
|
if exists {
|
|
|
|
|
|
self.push_undo_snapshot(i, "MLSTYLE SET");
|
|
|
|
|
|
self.tabs[i].scene.document.header.multiline_style = name.clone();
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!("MLSTYLE: current style set to '{}'.", name));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error(&format!("MLSTYLE: '{}' not found.", name));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"DEL" | "DELETE" => {
|
|
|
|
|
|
let name = parts.get(1).copied().unwrap_or("").to_string();
|
|
|
|
|
|
if name.is_empty() || name.eq_ignore_ascii_case("Standard") {
|
|
|
|
|
|
self.command_line.push_error("Cannot delete the Standard style.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let doc = &self.tabs[i].scene.document;
|
|
|
|
|
|
let handle = doc.objects.iter()
|
|
|
|
|
|
.find_map(|(&h, o)| {
|
|
|
|
|
|
if let ObjectType::MLineStyle(s) = o {
|
|
|
|
|
|
if s.name.eq_ignore_ascii_case(&name) { Some(h) } else { None }
|
|
|
|
|
|
} else { None }
|
|
|
|
|
|
});
|
|
|
|
|
|
if let Some(h) = handle {
|
|
|
|
|
|
self.push_undo_snapshot(i, "MLSTYLE DEL");
|
|
|
|
|
|
self.tabs[i].scene.document.objects.remove(&h);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!("MLSTYLE: '{}' deleted.", name));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error(&format!("MLSTYLE: '{}' not found.", name));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
self.command_line.push_error(
|
|
|
|
|
|
"Usage: MLSTYLE [LIST|NEW <name>|SET <name>|DEL <name>]"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 23:19:57 +03:00
|
|
|
|
cmd if cmd == "DIMSTYLE" || cmd == "DDIM" || cmd.starts_with("DIMSTYLE ") || cmd.starts_with("DDIM ") => {
|
|
|
|
|
|
use acadrust::tables::DimStyle;
|
|
|
|
|
|
let raw_rest = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or("");
|
|
|
|
|
|
let parts: Vec<&str> = raw_rest.split_whitespace().collect();
|
|
|
|
|
|
let sub = parts.get(0).map(|s| s.to_uppercase()).unwrap_or_default();
|
|
|
|
|
|
match sub.as_str() {
|
2026-04-06 14:25:13 +03:00
|
|
|
|
// No sub-command or "DIALOG" → open the DimStyle Manager dialog
|
|
|
|
|
|
"" | "DIALOG" | "UI" => {
|
|
|
|
|
|
return Task::done(Message::DimStyleDialogOpen);
|
|
|
|
|
|
}
|
|
|
|
|
|
"LIST" | "?" => {
|
2026-04-02 23:19:57 +03:00
|
|
|
|
let styles: Vec<String> = self.tabs[i].scene.document
|
|
|
|
|
|
.dim_styles.iter()
|
|
|
|
|
|
.map(|s| format!("{}(txt:{:.2} asz:{:.2})", s.name, s.dimtxt, s.dimasz))
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
if styles.is_empty() {
|
|
|
|
|
|
self.command_line.push_output("No dim styles defined.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_output(&format!("DimStyles: {}", styles.join(", ")));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"NEW" | "N" => {
|
|
|
|
|
|
let name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string();
|
|
|
|
|
|
if name.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("Usage: DIMSTYLE NEW <name>");
|
|
|
|
|
|
} else if self.tabs[i].scene.document.dim_styles.contains(&name) {
|
|
|
|
|
|
self.command_line.push_error(&format!("DIMSTYLE: '{}' already exists.", name));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let style = DimStyle::new(&name);
|
|
|
|
|
|
let _ = self.tabs[i].scene.document.dim_styles.add(style);
|
|
|
|
|
|
self.push_undo_snapshot(i, "DIMSTYLE NEW");
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!("DIMSTYLE: '{}' created.", name));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"SET" | "S" => {
|
|
|
|
|
|
// DIMSTYLE SET <name> <property> <value>
|
|
|
|
|
|
// e.g. DIMSTYLE SET Standard dimtxt 2.5
|
|
|
|
|
|
let style_name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string();
|
|
|
|
|
|
let prop = parts.get(2).map(|s| s.to_lowercase()).unwrap_or_default();
|
|
|
|
|
|
let val_str = parts.get(3).map(|s| s.trim()).unwrap_or("");
|
|
|
|
|
|
if let Ok(val) = val_str.parse::<f64>() {
|
|
|
|
|
|
if let Some(ds) = self.tabs[i].scene.document.dim_styles.get_mut(&style_name) {
|
|
|
|
|
|
match prop.as_str() {
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
"dimtxt" => { ds.dimtxt = val; }
|
|
|
|
|
|
"dimasz" => { ds.dimasz = val; }
|
|
|
|
|
|
"dimdli" => { ds.dimdli = val; }
|
|
|
|
|
|
"dimexo" => { ds.dimexo = val; }
|
|
|
|
|
|
"dimexe" => { ds.dimexe = val; }
|
|
|
|
|
|
"dimgap" => { ds.dimgap = val; }
|
|
|
|
|
|
"dimscale" => { ds.dimscale = val; }
|
|
|
|
|
|
"dimlfac" => { ds.dimlfac = val; }
|
|
|
|
|
|
"dimdle" => { ds.dimdle = val; }
|
|
|
|
|
|
"dimtvp" => { ds.dimtvp = val; }
|
|
|
|
|
|
"dimcen" => { ds.dimcen = val; }
|
|
|
|
|
|
"dimtsz" => { ds.dimtsz = val; }
|
|
|
|
|
|
"dimfxl" => { ds.dimfxl = val; }
|
2026-04-02 23:19:57 +03:00
|
|
|
|
_ => {
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
self.command_line.push_error(&format!(
|
|
|
|
|
|
"DIMSTYLE: unknown property '{}'. Try: dimtxt dimasz dimdli dimexo dimexe dimgap dimscale dimlfac dimdle dimcen dimtsz", prop
|
|
|
|
|
|
));
|
2026-04-02 23:19:57 +03:00
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.push_undo_snapshot(i, "DIMSTYLE SET");
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!("DIMSTYLE: '{style_name}'.{prop} = {val:.3}"));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error(&format!("DIMSTYLE: '{}' not found.", style_name));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error("Usage: DIMSTYLE SET <name> <property> <value>");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
self.command_line.push_info(
|
|
|
|
|
|
"Usage: DIMSTYLE LIST | NEW <name> | SET <name> <prop> <val>"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 23:14:51 +03:00
|
|
|
|
// ── TextStyle / Style management ──────────────────────────────
|
|
|
|
|
|
cmd if cmd == "STYLE" || cmd == "TEXTSTYLE" || cmd.starts_with("STYLE ") || cmd.starts_with("TEXTSTYLE ") => {
|
|
|
|
|
|
let (prefix, rest) = if cmd.starts_with("TEXTSTYLE") {
|
|
|
|
|
|
("TEXTSTYLE", cmd.trim_start_matches("TEXTSTYLE").trim())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
("STYLE", cmd.trim_start_matches("STYLE").trim())
|
|
|
|
|
|
};
|
|
|
|
|
|
let parts: Vec<&str> = rest.splitn(3, ' ').collect();
|
|
|
|
|
|
let sub = parts.get(0).map(|s| s.to_uppercase()).unwrap_or_default();
|
|
|
|
|
|
match sub.as_str() {
|
|
|
|
|
|
"" | "LIST" | "?" => {
|
|
|
|
|
|
let styles: Vec<String> = self.tabs[i].scene.document
|
|
|
|
|
|
.text_styles.iter()
|
|
|
|
|
|
.map(|s| format!("{} (font: {}, w: {:.2}, oblique: {:.1}°)",
|
|
|
|
|
|
s.name, s.font_file, s.width_factor, s.oblique_angle.to_degrees()))
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
if styles.is_empty() {
|
|
|
|
|
|
self.command_line.push_output("No text styles defined.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_output(&format!("Text styles: {}", styles.join(" | ")));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"SET" | "S" => {
|
|
|
|
|
|
// STYLE SET <name> — set active text style (for future text commands)
|
|
|
|
|
|
let name = parts.get(1).map(|s| s.trim()).unwrap_or("");
|
|
|
|
|
|
if self.tabs[i].scene.document.text_styles.get(name).is_some() {
|
|
|
|
|
|
self.command_line.push_output(&format!("{prefix}: active style set to '{name}'."));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error(&format!("{prefix}: style '{name}' not found."));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"NEW" | "N" => {
|
|
|
|
|
|
let name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string();
|
|
|
|
|
|
if name.is_empty() {
|
|
|
|
|
|
self.command_line.push_error(&format!("Usage: {prefix} NEW <name>"));
|
|
|
|
|
|
} else if self.tabs[i].scene.document.text_styles.contains(&name) {
|
|
|
|
|
|
self.command_line.push_error(&format!("{prefix}: style '{name}' already exists."));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let style = acadrust::tables::TextStyle::new(&name);
|
|
|
|
|
|
let _ = self.tabs[i].scene.document.text_styles.add(style);
|
|
|
|
|
|
self.push_undo_snapshot(i, "STYLE NEW");
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!("{prefix}: style '{name}' created."));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"FONT" | "F" => {
|
|
|
|
|
|
// STYLE FONT <name> <font_file>
|
|
|
|
|
|
let style_name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string();
|
|
|
|
|
|
let font = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string();
|
|
|
|
|
|
if style_name.is_empty() || font.is_empty() {
|
|
|
|
|
|
self.command_line.push_error(&format!("Usage: {prefix} FONT <style> <font_file>"));
|
|
|
|
|
|
} else if let Some(s) = self.tabs[i].scene.document.text_styles.get_mut(&style_name) {
|
|
|
|
|
|
s.font_file = font.clone();
|
|
|
|
|
|
self.push_undo_snapshot(i, "STYLE FONT");
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!("{prefix}: '{style_name}' font set to '{font}'."));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error(&format!("{prefix}: style '{style_name}' not found."));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"WIDTH" | "W" => {
|
|
|
|
|
|
// STYLE WIDTH <name> <factor>
|
|
|
|
|
|
let style_name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string();
|
|
|
|
|
|
let factor_str = parts.get(2).map(|s| s.trim()).unwrap_or("");
|
|
|
|
|
|
if let Ok(factor) = factor_str.parse::<f64>() {
|
|
|
|
|
|
if let Some(s) = self.tabs[i].scene.document.text_styles.get_mut(&style_name) {
|
|
|
|
|
|
s.width_factor = factor;
|
|
|
|
|
|
self.push_undo_snapshot(i, "STYLE WIDTH");
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!("{prefix}: '{style_name}' width factor set to {factor:.3}."));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error(&format!("{prefix}: style '{style_name}' not found."));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error(&format!("Usage: {prefix} WIDTH <style> <factor>"));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
"OBLIQUE" => {
|
|
|
|
|
|
// STYLE OBLIQUE <name> <angle_degrees>
|
|
|
|
|
|
let style_name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string();
|
|
|
|
|
|
let angle_str = parts.get(2).map(|s| s.trim()).unwrap_or("");
|
|
|
|
|
|
if let Ok(deg) = angle_str.parse::<f64>() {
|
|
|
|
|
|
if let Some(s) = self.tabs[i].scene.document.text_styles.get_mut(&style_name) {
|
|
|
|
|
|
s.oblique_angle = deg.to_radians();
|
|
|
|
|
|
self.push_undo_snapshot(i, "STYLE OBLIQUE");
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!("{prefix}: '{style_name}' oblique angle set to {deg:.1}°."));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error(&format!("{prefix}: style '{style_name}' not found."));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error(&format!("Usage: {prefix} OBLIQUE <style> <angle_degrees>"));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-02 23:14:51 +03:00
|
|
|
|
_ => {
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
"Usage: {prefix} LIST | NEW <name> | FONT <style> <file> | WIDTH <style> <factor> | OBLIQUE <style> <angle>"
|
2026-04-02 23:14:51 +03:00
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-02 23:23:08 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── LINETYPE management ───────────────────────────────────────
|
|
|
|
|
|
cmd if cmd == "LINETYPE" || cmd == "LT" || cmd.starts_with("LINETYPE ") || cmd.starts_with("LT ") => {
|
|
|
|
|
|
let raw_rest = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or("");
|
|
|
|
|
|
let parts: Vec<&str> = raw_rest.split_whitespace().collect();
|
|
|
|
|
|
let sub = parts.get(0).map(|s| s.to_uppercase()).unwrap_or_default();
|
|
|
|
|
|
match sub.as_str() {
|
|
|
|
|
|
"" | "LIST" | "?" => {
|
|
|
|
|
|
let ltypes: Vec<String> = self.tabs[i].scene.document
|
|
|
|
|
|
.line_types.iter()
|
|
|
|
|
|
.map(|lt| format!("{} ({})", lt.name, lt.description))
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
if ltypes.is_empty() {
|
|
|
|
|
|
self.command_line.push_output("No linetypes defined.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_output(&format!("Linetypes: {}", ltypes.join(", ")));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
self.command_line.push_info("Usage: LINETYPE LIST");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── PURGE unused definitions ──────────────────────────────────
|
|
|
|
|
|
cmd if cmd == "PURGE" || cmd.starts_with("PURGE ") => {
|
|
|
|
|
|
let sub = cmd.split_whitespace().nth(1).unwrap_or("ALL").to_uppercase();
|
|
|
|
|
|
let all = sub == "ALL" || sub.is_empty();
|
|
|
|
|
|
|
|
|
|
|
|
// Collect names in use (immutable borrows — done in their own scope)
|
|
|
|
|
|
let used_layers: std::collections::HashSet<String> = self.tabs[i].scene.document.entities()
|
|
|
|
|
|
.filter_map(|e| {
|
|
|
|
|
|
let name = &e.common().layer;
|
|
|
|
|
|
if name.is_empty() { None } else { Some(name.clone()) }
|
|
|
|
|
|
}).collect();
|
|
|
|
|
|
let used_text_styles: std::collections::HashSet<String> = self.tabs[i].scene.document.entities()
|
|
|
|
|
|
.filter_map(|e| match e {
|
|
|
|
|
|
acadrust::EntityType::Text(t) => Some(t.style.clone()),
|
|
|
|
|
|
acadrust::EntityType::MText(t) => Some(t.style.clone()),
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter(|s| !s.is_empty())
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let used_linetypes: std::collections::HashSet<String> = self.tabs[i].scene.document.entities()
|
|
|
|
|
|
.filter_map(|e| {
|
|
|
|
|
|
let lt = &e.common().linetype;
|
|
|
|
|
|
if lt.is_empty() || lt == "ByLayer" || lt == "ByBlock" { None } else { Some(lt.clone()) }
|
|
|
|
|
|
}).collect();
|
|
|
|
|
|
|
|
|
|
|
|
// Build removal lists (still immutable)
|
|
|
|
|
|
let layer_remove: Vec<String> = if all || sub == "LAYERS" {
|
|
|
|
|
|
self.tabs[i].scene.document.layers.iter()
|
|
|
|
|
|
.filter(|l| l.name != "0" && !used_layers.contains(&l.name))
|
|
|
|
|
|
.map(|l| l.name.clone()).collect()
|
|
|
|
|
|
} else { vec![] };
|
|
|
|
|
|
let style_remove: Vec<String> = if all || sub == "TEXTSTYLES" || sub == "STYLES" {
|
|
|
|
|
|
self.tabs[i].scene.document.text_styles.iter()
|
|
|
|
|
|
.filter(|s| s.name != "Standard" && !used_text_styles.contains(&s.name))
|
|
|
|
|
|
.map(|s| s.name.clone()).collect()
|
|
|
|
|
|
} else { vec![] };
|
|
|
|
|
|
let lt_remove: Vec<String> = if all || sub == "LINETYPES" || sub == "LT" {
|
|
|
|
|
|
let standard = ["Continuous", "ByLayer", "ByBlock"];
|
|
|
|
|
|
self.tabs[i].scene.document.line_types.iter()
|
|
|
|
|
|
.filter(|lt| !standard.iter().any(|s| s.eq_ignore_ascii_case(<.name))
|
|
|
|
|
|
&& !used_linetypes.contains(<.name))
|
|
|
|
|
|
.map(|lt| lt.name.clone()).collect()
|
|
|
|
|
|
} else { vec![] };
|
|
|
|
|
|
|
|
|
|
|
|
// Apply removals (mutable)
|
|
|
|
|
|
let purged = layer_remove.len() + style_remove.len() + lt_remove.len();
|
|
|
|
|
|
for name in &layer_remove { self.tabs[i].scene.document.layers.remove(name); }
|
|
|
|
|
|
for name in &style_remove { self.tabs[i].scene.document.text_styles.remove(name); }
|
|
|
|
|
|
for name in <_remove { self.tabs[i].scene.document.line_types.remove(name); }
|
|
|
|
|
|
|
|
|
|
|
|
if purged > 0 {
|
|
|
|
|
|
self.push_undo_snapshot(i, "PURGE");
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!("PURGE: {} definition(s) removed.", purged));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_output("PURGE: nothing to purge.");
|
|
|
|
|
|
}
|
2026-04-02 23:24:13 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 23:29:24 +03:00
|
|
|
|
// ── CHPROP — change entity properties from command line ───────
|
|
|
|
|
|
cmd if cmd == "CHPROP" || cmd.starts_with("CHPROP ") => {
|
|
|
|
|
|
// Usage: CHPROP <property> <value>
|
|
|
|
|
|
// Applies to currently selected entities.
|
|
|
|
|
|
// Properties: LAYER, COLOR, LINETYPE, LTSCALE
|
|
|
|
|
|
let parts: Vec<&str> = cmd.split_whitespace().collect();
|
|
|
|
|
|
let prop = parts.get(1).map(|s| s.to_uppercase()).unwrap_or_default();
|
|
|
|
|
|
let value = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string();
|
|
|
|
|
|
|
|
|
|
|
|
if prop.is_empty() {
|
|
|
|
|
|
self.command_line.push_info(
|
|
|
|
|
|
"Usage: CHPROP <prop> <val> (props: LAYER COLOR LINETYPE LTSCALE)"
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i].scene.selected_entities()
|
|
|
|
|
|
.into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
if handles.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("CHPROP: no entities selected.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Validate value early to give clear errors
|
|
|
|
|
|
let color_val: Option<acadrust::types::Color> = if prop == "COLOR" {
|
|
|
|
|
|
value.parse::<i16>().ok().map(acadrust::types::Color::from_index)
|
|
|
|
|
|
} else { None };
|
|
|
|
|
|
let ltscale_val: Option<f64> = if prop == "LTSCALE" {
|
|
|
|
|
|
value.parse().ok()
|
|
|
|
|
|
} else { None };
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
let transparency_val: Option<acadrust::types::Transparency> = if prop == "TRANSPARENCY" {
|
|
|
|
|
|
value.parse::<f64>().ok().map(acadrust::types::Transparency::from_percent)
|
|
|
|
|
|
} else { None };
|
2026-04-02 23:29:24 +03:00
|
|
|
|
|
|
|
|
|
|
if (prop == "COLOR" && color_val.is_none())
|
|
|
|
|
|
|| (prop == "LTSCALE" && ltscale_val.is_none())
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
|| (prop == "TRANSPARENCY" && transparency_val.is_none())
|
2026-04-02 23:29:24 +03:00
|
|
|
|
{
|
|
|
|
|
|
self.command_line.push_error(&format!("CHPROP: invalid value '{}' for {}.", value, prop));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let mut changed = 0usize;
|
|
|
|
|
|
for handle in &handles {
|
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(*handle) {
|
|
|
|
|
|
let common = entity.common_mut();
|
|
|
|
|
|
match prop.as_str() {
|
|
|
|
|
|
"LAYER" => { common.layer = value.clone(); changed += 1; }
|
|
|
|
|
|
"LINETYPE" | "LT" => { common.linetype = value.clone(); changed += 1; }
|
|
|
|
|
|
"LTSCALE" => { common.linetype_scale = ltscale_val.unwrap(); changed += 1; }
|
|
|
|
|
|
"COLOR" => { common.color = color_val.unwrap(); changed += 1; }
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
"TRANSPARENCY" => { common.transparency = transparency_val.unwrap(); changed += 1; }
|
2026-04-02 23:29:24 +03:00
|
|
|
|
_ => {
|
|
|
|
|
|
self.command_line.push_error(&format!(
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
"CHPROP: unknown property '{}'. Use: LAYER COLOR LINETYPE LTSCALE TRANSPARENCY", prop
|
2026-04-02 23:29:24 +03:00
|
|
|
|
));
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if changed > 0 {
|
|
|
|
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!("CHPROP: {} entity/entities updated.", changed));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 23:24:13 +03:00
|
|
|
|
// ── RENAME table entries ──────────────────────────────────────
|
|
|
|
|
|
cmd if cmd == "RENAME" || cmd.starts_with("RENAME ") => {
|
|
|
|
|
|
// Usage: RENAME <type> <old_name> <new_name>
|
|
|
|
|
|
// Types: LAYER BLOCK STYLE DIMSTYLE LINETYPE UCS VIEW
|
|
|
|
|
|
let parts: Vec<&str> = cmd.split_whitespace().collect();
|
|
|
|
|
|
let type_str = parts.get(1).map(|s| s.to_uppercase()).unwrap_or_default();
|
|
|
|
|
|
let old_name = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string();
|
|
|
|
|
|
let new_name = parts.get(3).map(|s| s.trim()).unwrap_or("").to_string();
|
|
|
|
|
|
|
|
|
|
|
|
if type_str.is_empty() || old_name.is_empty() || new_name.is_empty() {
|
|
|
|
|
|
self.command_line.push_info(
|
|
|
|
|
|
"Usage: RENAME <type> <old> <new> (types: LAYER BLOCK STYLE DIMSTYLE LINETYPE UCS VIEW)"
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let doc = &mut self.tabs[i].scene.document;
|
|
|
|
|
|
let ok = match type_str.as_str() {
|
|
|
|
|
|
"LAYER" => {
|
|
|
|
|
|
if let Some(l) = doc.layers.get_mut(&old_name) {
|
|
|
|
|
|
l.name = new_name.clone();
|
|
|
|
|
|
// Update entity references
|
|
|
|
|
|
for e in doc.entities_mut() {
|
|
|
|
|
|
if e.common().layer == old_name {
|
|
|
|
|
|
e.common_mut().layer = new_name.clone();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
true
|
|
|
|
|
|
} else { false }
|
|
|
|
|
|
}
|
|
|
|
|
|
"STYLE" | "TEXTSTYLE" => {
|
|
|
|
|
|
if let Some(s) = doc.text_styles.get_mut(&old_name) {
|
|
|
|
|
|
s.name = new_name.clone(); true
|
|
|
|
|
|
} else { false }
|
|
|
|
|
|
}
|
|
|
|
|
|
"DIMSTYLE" => {
|
|
|
|
|
|
if let Some(s) = doc.dim_styles.get_mut(&old_name) {
|
|
|
|
|
|
s.name = new_name.clone(); true
|
|
|
|
|
|
} else { false }
|
|
|
|
|
|
}
|
|
|
|
|
|
"LINETYPE" | "LT" => {
|
|
|
|
|
|
if let Some(lt) = doc.line_types.get_mut(&old_name) {
|
|
|
|
|
|
lt.name = new_name.clone(); true
|
|
|
|
|
|
} else { false }
|
|
|
|
|
|
}
|
|
|
|
|
|
"UCS" => {
|
|
|
|
|
|
if let Some(u) = doc.ucss.get_mut(&old_name) {
|
|
|
|
|
|
u.name = new_name.clone(); true
|
|
|
|
|
|
} else { false }
|
|
|
|
|
|
}
|
|
|
|
|
|
"VIEW" => {
|
|
|
|
|
|
if let Some(v) = doc.views.get_mut(&old_name) {
|
|
|
|
|
|
v.name = new_name.clone(); true
|
|
|
|
|
|
} else { false }
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
self.command_line.push_error(&format!("RENAME: unknown type '{}'. Use LAYER BLOCK STYLE DIMSTYLE LINETYPE UCS VIEW", type_str));
|
|
|
|
|
|
false
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
if ok {
|
|
|
|
|
|
self.push_undo_snapshot(i, "RENAME");
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!("RENAME: '{}' → '{}'.", old_name, new_name));
|
|
|
|
|
|
} else if type_str != "BLOCK" {
|
|
|
|
|
|
self.command_line.push_error(&format!("RENAME: '{}' not found in {}.", old_name, type_str));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
Feat: viewport display lock, extended properties, VPLAYER and VPORTS commands
- Enforce status.locked in pan_active_viewport and zoom_active_viewport:
locked viewports now reject pan/zoom inputs entirely
- Add perspective, render_mode, shade_plot_mode, hide_plot, ucs_icon_visible
fields to the viewport property panel with appropriate pickers/toggles
- Add VPORTS command: lists all user viewports in the current layout
(id, size, position, scale, on/locked state) in the command line
- Add VPLAYER command: per-viewport layer freeze/thaw from the command line
(F <layer> to freeze, T <layer> to thaw, Enter to exit)
- Add CmdResult::VpLayerUpdate variant and its handler in apply_cmd_result
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 23:52:18 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
// ── System variable getters/setters ──────────────────────────────────
|
|
|
|
|
|
// CLAYER [name] — get or set current layer
|
|
|
|
|
|
// TEXTSTYLE [name] — already handled above under STYLE SET
|
|
|
|
|
|
// DIMSTYLE [name] — get or set active dim style
|
|
|
|
|
|
// LTSCALE [val] — global linetype scale
|
|
|
|
|
|
cmd if cmd == "CLAYER" || cmd.starts_with("CLAYER ") => {
|
|
|
|
|
|
let name_arg = cmd.trim_start_matches("CLAYER").trim();
|
|
|
|
|
|
if name_arg.is_empty() {
|
|
|
|
|
|
let cur = &self.tabs[i].scene.document.header.current_layer_name;
|
|
|
|
|
|
self.command_line.push_output(&format!("CLAYER = \"{cur}\""));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if self.tabs[i].scene.document.layers.contains(name_arg) {
|
|
|
|
|
|
self.tabs[i].scene.document.header.current_layer_name = name_arg.to_string();
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!("CLAYER set to \"{name_arg}\""));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error(&format!("CLAYER: layer '{}' not found.", name_arg));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
cmd if cmd == "CDIMSTY" || cmd == "DIMCURRENT" || cmd.starts_with("CDIMSTY ") || cmd.starts_with("DIMCURRENT ") => {
|
|
|
|
|
|
let name_arg = cmd.split_whitespace().skip(1).collect::<Vec<_>>().join(" ");
|
|
|
|
|
|
if name_arg.is_empty() {
|
|
|
|
|
|
let cur = &self.tabs[i].scene.document.header.current_dimstyle_name;
|
|
|
|
|
|
self.command_line.push_output(&format!("CDIMSTY = \"{cur}\""));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if self.tabs[i].scene.document.dim_styles.contains(&name_arg) {
|
|
|
|
|
|
self.tabs[i].scene.document.header.current_dimstyle_name = name_arg.clone();
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!("Active dim style set to \"{name_arg}\""));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error(&format!("CDIMSTY: dim style '{}' not found.", name_arg));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
cmd if cmd == "LTSCALE" || cmd.starts_with("LTSCALE ") => {
|
|
|
|
|
|
let val_str = cmd.trim_start_matches("LTSCALE").trim();
|
|
|
|
|
|
if val_str.is_empty() {
|
|
|
|
|
|
let v = self.tabs[i].scene.document.header.linetype_scale;
|
|
|
|
|
|
self.command_line.push_output(&format!("LTSCALE = {v:.4}"));
|
|
|
|
|
|
} else if let Ok(v) = val_str.parse::<f64>() {
|
|
|
|
|
|
if v > 0.0 {
|
|
|
|
|
|
self.push_undo_snapshot(i, "LTSCALE");
|
|
|
|
|
|
self.tabs[i].scene.document.header.linetype_scale = v;
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!("LTSCALE set to {v:.4}"));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error("LTSCALE: value must be positive.");
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error("Usage: LTSCALE [value]");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
cmd if cmd == "CELTSCALE" || cmd.starts_with("CELTSCALE ") => {
|
|
|
|
|
|
let val_str = cmd.trim_start_matches("CELTSCALE").trim();
|
|
|
|
|
|
if val_str.is_empty() {
|
|
|
|
|
|
let v = self.tabs[i].scene.document.header.current_entity_linetype_scale;
|
|
|
|
|
|
self.command_line.push_output(&format!("CELTSCALE = {v:.4}"));
|
|
|
|
|
|
} else if let Ok(v) = val_str.parse::<f64>() {
|
|
|
|
|
|
if v > 0.0 {
|
|
|
|
|
|
self.tabs[i].scene.document.header.current_entity_linetype_scale = v;
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!("CELTSCALE set to {v:.4}"));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error("CELTSCALE: value must be positive.");
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error("Usage: CELTSCALE [value]");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── SCALETEXT — rescale selected Text/MText entities ─────────────────
|
|
|
|
|
|
// Usage: SCALETEXT <factor> e.g. SCALETEXT 2
|
|
|
|
|
|
// SCALETEXT H <height> set absolute height
|
|
|
|
|
|
cmd if cmd == "SCALETEXT" || cmd.starts_with("SCALETEXT ") => {
|
|
|
|
|
|
let rest = cmd.trim_start_matches("SCALETEXT").trim();
|
|
|
|
|
|
let parts: Vec<&str> = rest.split_whitespace().collect();
|
|
|
|
|
|
let selected_handles: Vec<acadrust::Handle> = self.tabs[i].scene
|
|
|
|
|
|
.selected_entities()
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|(h, _)| *h)
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
if selected_handles.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("SCALETEXT: select Text/MText entities first.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let (use_absolute, value) = match (parts.first().map(|s| s.to_uppercase()).as_deref(), parts.get(1)) {
|
|
|
|
|
|
(Some("H"), Some(v)) => (true, v.parse::<f64>().ok()),
|
|
|
|
|
|
(Some(v), None) => (false, v.parse::<f64>().ok()),
|
|
|
|
|
|
_ => (false, None),
|
|
|
|
|
|
};
|
|
|
|
|
|
if let Some(val) = value {
|
|
|
|
|
|
if val <= 0.0 {
|
|
|
|
|
|
self.command_line.push_error("SCALETEXT: value must be positive.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.push_undo_snapshot(i, "SCALETEXT");
|
|
|
|
|
|
let mut count = 0usize;
|
|
|
|
|
|
for sh in &selected_handles {
|
|
|
|
|
|
for entity in self.tabs[i].scene.document.entities_mut() {
|
|
|
|
|
|
if entity.common().handle != *sh { continue; }
|
|
|
|
|
|
match entity {
|
|
|
|
|
|
acadrust::EntityType::Text(t) => {
|
|
|
|
|
|
t.height = if use_absolute { val } else { t.height * val };
|
|
|
|
|
|
count += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
acadrust::EntityType::MText(t) => {
|
|
|
|
|
|
t.height = if use_absolute { val } else { t.height * val };
|
|
|
|
|
|
count += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if count > 0 {
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"SCALETEXT: scaled {count} text entity(ies)."
|
|
|
|
|
|
));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error("SCALETEXT: no Text/MText in selection.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_info("Usage: SCALETEXT <factor> or SCALETEXT H <height>");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Display refresh (no-op in GPU raster pipeline) ────────────────
|
|
|
|
|
|
"REGEN"|"REGENALL"|"REDRAW"|"REDRWALL" => {
|
|
|
|
|
|
// Display is always up-to-date in the GPU raster pipeline.
|
|
|
|
|
|
self.command_line.push_output("Display regenerated.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── TABLE cell editing ─────────────────────────────────────────────
|
|
|
|
|
|
// TABLE CELL <row> <col> <text> — set text for a cell in the selected Table
|
|
|
|
|
|
cmd if cmd.starts_with("TABLE ") => {
|
|
|
|
|
|
let rest = cmd.trim_start_matches("TABLE").trim();
|
|
|
|
|
|
let sub_up = rest.split_whitespace().next().unwrap_or("").to_uppercase();
|
|
|
|
|
|
if sub_up == "CELL" {
|
|
|
|
|
|
let parts: Vec<&str> = rest.splitn(4, char::is_whitespace).collect();
|
|
|
|
|
|
// parts: ["CELL", "<row>", "<col>", "<text>"]
|
|
|
|
|
|
let row_res = parts.get(1).and_then(|s| s.parse::<usize>().ok());
|
|
|
|
|
|
let col_res = parts.get(2).and_then(|s| s.parse::<usize>().ok());
|
|
|
|
|
|
let text = parts.get(3).copied().unwrap_or("");
|
|
|
|
|
|
match (row_res, col_res) {
|
|
|
|
|
|
(Some(row), Some(col)) => {
|
|
|
|
|
|
let selected_handles: Vec<acadrust::Handle> = self.tabs[i].scene
|
|
|
|
|
|
.selected_entities()
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|(h, _)| *h)
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let mut found = false;
|
|
|
|
|
|
for sh in &selected_handles {
|
|
|
|
|
|
if let Some(acadrust::EntityType::Table(tbl)) =
|
|
|
|
|
|
self.tabs[i].scene.document.entities_mut().find(|e| e.common().handle == *sh)
|
|
|
|
|
|
{
|
|
|
|
|
|
if tbl.set_cell_text(row, col, text) {
|
|
|
|
|
|
found = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if found {
|
|
|
|
|
|
self.push_undo_snapshot(i, "TABLE CELL");
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"TABLE CELL: set [{row},{col}] = \"{text}\"."
|
|
|
|
|
|
));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_error(
|
|
|
|
|
|
"TABLE CELL: select a Table entity first, or row/col out of range."
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
self.command_line.push_info("Usage: TABLE CELL <row> <col> <text>");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_info("Usage: TABLE (creates new table) or TABLE CELL <row> <col> <text>");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── UCSICON — toggle UCS icon visibility on all viewports ────────────
|
|
|
|
|
|
// UCSICON ON — show UCS icon in all viewports
|
|
|
|
|
|
// UCSICON OFF — hide UCS icon in all viewports
|
|
|
|
|
|
// UCSICON NOORIGIN — show icon but not at origin (show at corner)
|
|
|
|
|
|
// UCSICON ORIGIN — show icon at UCS origin
|
|
|
|
|
|
cmd if cmd == "UCSICON" || cmd.starts_with("UCSICON ") => {
|
|
|
|
|
|
let sub = cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase();
|
|
|
|
|
|
match sub.as_str() {
|
|
|
|
|
|
"ON" | "OFF" | "NOORIGIN" | "ORIGIN" => {
|
|
|
|
|
|
self.push_undo_snapshot(i, "UCSICON");
|
|
|
|
|
|
let visible = sub != "OFF";
|
|
|
|
|
|
let at_origin = sub == "ORIGIN";
|
2026-04-03 19:25:55 +03:00
|
|
|
|
// Update model-space icon flag.
|
|
|
|
|
|
self.show_ucs_icon = visible;
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
let mut count = 0usize;
|
|
|
|
|
|
for entity in self.tabs[i].scene.document.entities_mut() {
|
|
|
|
|
|
if let acadrust::EntityType::Viewport(vp) = entity {
|
|
|
|
|
|
vp.status.ucs_icon_visible = visible;
|
|
|
|
|
|
if sub == "NOORIGIN" || sub == "ORIGIN" {
|
|
|
|
|
|
vp.status.ucs_icon_at_origin = at_origin;
|
|
|
|
|
|
}
|
|
|
|
|
|
count += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
2026-04-03 19:25:55 +03:00
|
|
|
|
"UCSICON {sub}: updated {count} viewport(s) + model space."
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
self.command_line.push_info("Usage: UCSICON ON | OFF | NOORIGIN | ORIGIN");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── XDATA — read/write extended entity data ──────────────────────────
|
|
|
|
|
|
// XDATA LIST — show all xdata records on selected entities
|
|
|
|
|
|
// XDATA SET <app> <str> — append a string xdata value for <app>
|
|
|
|
|
|
// XDATA CLEAR — remove all xdata from selected entities
|
|
|
|
|
|
// XDATA CLEAR <app> — remove xdata for a specific application
|
|
|
|
|
|
cmd if cmd == "XDATA" || cmd.starts_with("XDATA ") => {
|
|
|
|
|
|
use acadrust::xdata::{ExtendedDataRecord, XDataValue};
|
|
|
|
|
|
let rest = cmd.trim_start_matches("XDATA").trim();
|
|
|
|
|
|
let parts: Vec<&str> = rest.splitn(3, char::is_whitespace).collect();
|
|
|
|
|
|
let sub = parts.first().map(|s| s.to_uppercase()).unwrap_or_default();
|
|
|
|
|
|
let selected_handles: Vec<acadrust::Handle> = self.tabs[i].scene
|
|
|
|
|
|
.selected_entities()
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|(h, _)| *h)
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
if selected_handles.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("XDATA: select entities first.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
match sub.as_str() {
|
|
|
|
|
|
"LIST" | "" => {
|
|
|
|
|
|
for sh in &selected_handles {
|
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity(*sh) {
|
|
|
|
|
|
let xd = &entity.common().extended_data;
|
|
|
|
|
|
if xd.is_empty() {
|
|
|
|
|
|
self.command_line.push_output(&format!(" {:x}: no xdata.", sh.value()));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for rec in xd.records() {
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
" {:x} [{}]: {} value(s)", sh.value(), rec.application_name, rec.values.len()
|
|
|
|
|
|
));
|
|
|
|
|
|
for v in &rec.values {
|
|
|
|
|
|
self.command_line.push_output(&format!(" {:?}", v));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"SET" => {
|
|
|
|
|
|
let app = parts.get(1).copied().unwrap_or("H7CAD");
|
|
|
|
|
|
let val = parts.get(2).copied().unwrap_or("");
|
|
|
|
|
|
self.push_undo_snapshot(i, "XDATA SET");
|
|
|
|
|
|
for sh in &selected_handles {
|
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(*sh) {
|
|
|
|
|
|
let mut rec = ExtendedDataRecord::new(app);
|
|
|
|
|
|
rec.add_value(XDataValue::String(val.to_string()));
|
|
|
|
|
|
entity.common_mut().extended_data.add_record(rec);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"XDATA: set [{app}] = \"{val}\" on {} entity/entities.", selected_handles.len()
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
"CLEAR" => {
|
|
|
|
|
|
let app_filter = parts.get(1).copied();
|
|
|
|
|
|
self.push_undo_snapshot(i, "XDATA CLEAR");
|
|
|
|
|
|
for sh in &selected_handles {
|
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(*sh) {
|
|
|
|
|
|
let xd = &mut entity.common_mut().extended_data;
|
|
|
|
|
|
if let Some(app) = app_filter {
|
|
|
|
|
|
// Rebuild without the matching app.
|
|
|
|
|
|
let kept: Vec<_> = xd.records().iter()
|
|
|
|
|
|
.filter(|r| r.application_name != app)
|
|
|
|
|
|
.cloned()
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
xd.clear();
|
|
|
|
|
|
for r in kept { xd.add_record(r); }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
xd.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output("XDATA: cleared.");
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
self.command_line.push_info("Usage: XDATA LIST | SET <app> <value> | CLEAR [app]");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 08:05:17 +03:00
|
|
|
|
// ── Plot / Page Setup ──────────────────────────────────────────
|
|
|
|
|
|
"PRINT"|"PLOT"|"EXPORT" => {
|
|
|
|
|
|
return Task::done(Message::PlotExport);
|
|
|
|
|
|
}
|
2026-04-06 14:37:23 +03:00
|
|
|
|
// PLOTSTYLE — load or clear CTB/STB plot style table
|
|
|
|
|
|
cmd if cmd == "PLOTSTYLE" || cmd.starts_with("PLOTSTYLE ") => {
|
|
|
|
|
|
let sub = cmd.split_once(' ')
|
|
|
|
|
|
.map(|(_, r)| r.trim().to_uppercase())
|
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
match sub.as_str() {
|
|
|
|
|
|
"CLEAR" | "NONE" => {
|
|
|
|
|
|
return Task::done(Message::PlotStyleClear);
|
|
|
|
|
|
}
|
|
|
|
|
|
"" | "LOAD" => {
|
|
|
|
|
|
let active = self.active_plot_style.as_ref()
|
|
|
|
|
|
.map(|t| format!("Active: {}", t.name))
|
|
|
|
|
|
.unwrap_or_else(|| "No plot style loaded.".into());
|
|
|
|
|
|
self.command_line.push_info(&active);
|
|
|
|
|
|
return Task::done(Message::PlotStyleLoad);
|
|
|
|
|
|
}
|
|
|
|
|
|
"?" | "STATUS" => {
|
|
|
|
|
|
let msg = self.active_plot_style.as_ref()
|
|
|
|
|
|
.map(|t| format!(
|
|
|
|
|
|
"Plot style: {} ({} color overrides)",
|
|
|
|
|
|
t.name,
|
|
|
|
|
|
t.aci_entries.iter().filter(|e| e.color.is_some()).count()
|
|
|
|
|
|
))
|
|
|
|
|
|
.unwrap_or_else(|| "No plot style table loaded.".into());
|
|
|
|
|
|
self.command_line.push_output(&msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
self.command_line.push_error(
|
|
|
|
|
|
"Usage: PLOTSTYLE [LOAD | CLEAR | STATUS]"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
feat: add Underlay entity support (PDF/DWF/DGN)
- New src/entities/underlay.rs: TruckConvertible (clip boundary polygon
or cross marker), Grippable (insertion + boundary vertex grips),
PropertyEditable (position, scale, rotation, contrast, fade, flags),
Transformable (translate, mirror, scale, rotate)
- Wire EntityType::Underlay into all four match arms in traits.rs
- UNDERLAY command: FADE/CONTRAST/ON/OFF/CLIP ON|OFF/MONO ON|OFF
subcommands to edit selected underlay entities
- Entity type name map entry for UNDERLAY
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 14:43:44 +03:00
|
|
|
|
// UNDERLAY — edit properties of selected PDF/DWF/DGN underlay entities.
|
|
|
|
|
|
// Usage:
|
|
|
|
|
|
// UNDERLAY FADE <0-80>
|
|
|
|
|
|
// UNDERLAY CONTRAST <0-100>
|
|
|
|
|
|
// UNDERLAY ON | OFF
|
|
|
|
|
|
// UNDERLAY CLIP ON | OFF
|
|
|
|
|
|
// UNDERLAY MONO ON | OFF
|
|
|
|
|
|
cmd if cmd == "UNDERLAY" || cmd.starts_with("UNDERLAY ") => {
|
|
|
|
|
|
let sub = cmd.split_once(' ')
|
|
|
|
|
|
.map(|(_, r)| r.trim().to_uppercase())
|
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
let handles: Vec<acadrust::Handle> = self.tabs[i].scene
|
|
|
|
|
|
.selected_entities()
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|(h, _)| *h)
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
if handles.is_empty() {
|
|
|
|
|
|
self.command_line.push_error("UNDERLAY: select underlay entities first.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let parts: Vec<&str> = sub.splitn(2, char::is_whitespace).collect();
|
|
|
|
|
|
let action = parts.first().copied().unwrap_or("");
|
|
|
|
|
|
let arg = parts.get(1).copied().unwrap_or("").trim();
|
|
|
|
|
|
let mut changed = 0usize;
|
|
|
|
|
|
self.push_undo_snapshot(i, "UNDERLAY");
|
|
|
|
|
|
for h in &handles {
|
|
|
|
|
|
if let Some(acadrust::EntityType::Underlay(ul)) = self.tabs[i].scene
|
|
|
|
|
|
.document.entities_mut()
|
|
|
|
|
|
.find(|e| e.common().handle == *h)
|
|
|
|
|
|
{
|
|
|
|
|
|
match action {
|
|
|
|
|
|
"FADE" => {
|
|
|
|
|
|
if let Ok(v) = arg.parse::<u8>() {
|
|
|
|
|
|
ul.set_fade(v);
|
|
|
|
|
|
changed += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"CONTRAST" => {
|
|
|
|
|
|
if let Ok(v) = arg.parse::<u8>() {
|
|
|
|
|
|
ul.set_contrast(v);
|
|
|
|
|
|
changed += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"ON" => { ul.set_on(true); changed += 1; }
|
|
|
|
|
|
"OFF" => { ul.set_on(false); changed += 1; }
|
|
|
|
|
|
"CLIP" => {
|
|
|
|
|
|
match arg {
|
|
|
|
|
|
"ON" => {
|
|
|
|
|
|
ul.flags |= acadrust::entities::UnderlayDisplayFlags::CLIPPING;
|
|
|
|
|
|
changed += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
"OFF" => {
|
|
|
|
|
|
ul.clear_clip();
|
|
|
|
|
|
changed += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"MONO" => {
|
|
|
|
|
|
match arg {
|
|
|
|
|
|
"ON" => { ul.set_monochrome(true); changed += 1; }
|
|
|
|
|
|
"OFF" => { ul.set_monochrome(false); changed += 1; }
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
// No sub-command: print status.
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"Underlay {:x}: fade={}, contrast={}, on={}, clip={}, mono={}",
|
|
|
|
|
|
h.value(),
|
|
|
|
|
|
ul.fade,
|
|
|
|
|
|
ul.contrast,
|
|
|
|
|
|
ul.is_on(),
|
|
|
|
|
|
ul.is_clipping(),
|
|
|
|
|
|
ul.is_monochrome(),
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if changed > 0 {
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
"Updated {changed} underlay(s)."
|
|
|
|
|
|
));
|
|
|
|
|
|
} else if !action.is_empty() {
|
|
|
|
|
|
self.command_line.push_error(
|
|
|
|
|
|
"Usage: UNDERLAY [FADE <n>|CONTRAST <n>|ON|OFF|CLIP ON|OFF|MONO ON|OFF]"
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 12:52:07 +03:00
|
|
|
|
"PAGESETUP" => {
|
2026-03-31 08:05:17 +03:00
|
|
|
|
if self.tabs[i].scene.current_layout == "Model" {
|
|
|
|
|
|
self.command_line.push_error("PAGESETUP: switch to a paper space layout first.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return Task::done(Message::PageSetupOpen);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
|
_ => self.command_line.push_error(&format!("Unknown command: {cmd}")),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Focus the command line whenever a command just became active.
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if self.tabs[i].active_cmd.is_some() {
|
|
|
|
|
|
self.tabs[i].last_cmd = Some(cmd.to_string());
|
|
|
|
|
|
self.focus_cmd_input()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-03 09:22:27 +03:00
|
|
|
|
|
|
|
|
|
|
// ── FIND/REPLACE helpers ───────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-04-03 09:31:03 +03:00
|
|
|
|
fn entity_list_details(entity: &acadrust::EntityType) -> String {
|
|
|
|
|
|
use std::f64::consts::PI;
|
|
|
|
|
|
match entity {
|
|
|
|
|
|
acadrust::EntityType::Line(l) => format!(
|
|
|
|
|
|
"from ({:.4},{:.4},{:.4}) to ({:.4},{:.4},{:.4}) len={:.4}",
|
|
|
|
|
|
l.start.x, l.start.y, l.start.z,
|
|
|
|
|
|
l.end.x, l.end.y, l.end.z,
|
|
|
|
|
|
((l.end.x-l.start.x).powi(2)+(l.end.y-l.start.y).powi(2)+(l.end.z-l.start.z).powi(2)).sqrt()
|
|
|
|
|
|
),
|
|
|
|
|
|
acadrust::EntityType::Circle(c) => format!(
|
|
|
|
|
|
"center ({:.4},{:.4},{:.4}) r={:.4} area={:.4}",
|
|
|
|
|
|
c.center.x, c.center.y, c.center.z, c.radius,
|
|
|
|
|
|
PI * c.radius * c.radius
|
|
|
|
|
|
),
|
|
|
|
|
|
acadrust::EntityType::Arc(a) => format!(
|
|
|
|
|
|
"center ({:.4},{:.4},{:.4}) r={:.4} start={:.2}° end={:.2}°",
|
|
|
|
|
|
a.center.x, a.center.y, a.center.z, a.radius, a.start_angle, a.end_angle
|
|
|
|
|
|
),
|
|
|
|
|
|
acadrust::EntityType::LwPolyline(p) => format!(
|
|
|
|
|
|
"{} vertices closed={} elevation={:.4}",
|
|
|
|
|
|
p.vertices.len(), p.is_closed, p.elevation
|
|
|
|
|
|
),
|
|
|
|
|
|
acadrust::EntityType::Text(t) => format!(
|
|
|
|
|
|
"\"{}\" h={:.4} at ({:.4},{:.4})",
|
|
|
|
|
|
t.value, t.height, t.insertion_point.x, t.insertion_point.y
|
|
|
|
|
|
),
|
|
|
|
|
|
acadrust::EntityType::MText(t) => format!(
|
|
|
|
|
|
"\"{}\" h={:.4} at ({:.4},{:.4})",
|
|
|
|
|
|
t.value.chars().take(40).collect::<String>(),
|
|
|
|
|
|
t.height, t.insertion_point.x, t.insertion_point.y
|
|
|
|
|
|
),
|
|
|
|
|
|
acadrust::EntityType::Insert(ins) => format!(
|
|
|
|
|
|
"block=\"{}\" at ({:.4},{:.4},{:.4}) scale=({:.4},{:.4},{:.4}) rot={:.2}°",
|
|
|
|
|
|
ins.block_name, ins.insert_point.x, ins.insert_point.y, ins.insert_point.z,
|
|
|
|
|
|
ins.x_scale(), ins.y_scale(), ins.z_scale(),
|
|
|
|
|
|
ins.rotation.to_degrees()
|
|
|
|
|
|
),
|
|
|
|
|
|
acadrust::EntityType::Spline(s) => format!(
|
|
|
|
|
|
"{} ctrl pts degree={} closed={}",
|
|
|
|
|
|
s.control_points.len(), s.degree, s.flags.closed
|
|
|
|
|
|
),
|
|
|
|
|
|
acadrust::EntityType::Ellipse(e) => format!(
|
|
|
|
|
|
"center ({:.4},{:.4}) major_len={:.4} ratio={:.4}",
|
|
|
|
|
|
e.center.x, e.center.y, e.major_axis_length(), e.minor_axis_ratio
|
|
|
|
|
|
),
|
|
|
|
|
|
_ => String::new(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 09:25:39 +03:00
|
|
|
|
fn flatten_entity_z(entity: &mut acadrust::EntityType) {
|
|
|
|
|
|
match entity {
|
|
|
|
|
|
acadrust::EntityType::Line(l) => { l.start.z = 0.0; l.end.z = 0.0; }
|
|
|
|
|
|
acadrust::EntityType::Circle(c) => { c.center.z = 0.0; }
|
|
|
|
|
|
acadrust::EntityType::Arc(a) => { a.center.z = 0.0; }
|
|
|
|
|
|
acadrust::EntityType::LwPolyline(p) => { p.elevation = 0.0; }
|
|
|
|
|
|
acadrust::EntityType::Text(t) => { t.insertion_point.z = 0.0; }
|
|
|
|
|
|
acadrust::EntityType::MText(t) => { t.insertion_point.z = 0.0; }
|
|
|
|
|
|
acadrust::EntityType::Insert(ins) => { ins.insert_point.z = 0.0; }
|
|
|
|
|
|
acadrust::EntityType::Point(p) => { p.location.z = 0.0; }
|
|
|
|
|
|
acadrust::EntityType::Spline(s) => {
|
|
|
|
|
|
for cp in &mut s.control_points { cp.z = 0.0; }
|
|
|
|
|
|
for fp in &mut s.fit_points { fp.z = 0.0; }
|
|
|
|
|
|
}
|
|
|
|
|
|
acadrust::EntityType::Ellipse(e) => { e.center.z = 0.0; }
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Feat: ROADMAP Phase 1-5 batch — trim/extend, plot scale, viewport UX, mline caps, table alignment
- TRIM/EXTEND: Ray and XLine entities supported as cutting boundaries and trim targets;
trim results in Ray→Line/Ray, XLine→Ray/Ray or Line; hover preview added
- Plot scale override: Fit/1:1/1:2/1:5/1:10/1:20/1:50/1:100/2:1 buttons added to Page Setup
panel; PageSetupCommit applies scale to PlotSettings
- Viewport UX: locked viewport gets orange border; selected viewport gets white border
- Named view → viewport assignment: Choice dropdown in properties panel copies
view_target/direction/height to the viewport entity
- MLine caps: perpendicular start/end cap lines added for open (non-closed) MLines
- Wipeout polygon grips: boundary vertices become editable grips when clipping_enabled+Polygonal;
world→pixel back-projection on drag
- Table cell alignment: CellStyle.alignment (1-9) drives left/center/right + top/middle/bottom
text positioning; cxf::measure_text() helper added
- New commands/features: DIMCONTINUE (DCO), DIMBASELINE (DBA), TOLERANCE (TOL), TABLE,
ATTDEF, ATTEDIT, ATTDISP, BREAKATPOINT (BAP), PLOTWINDOW (PW), MLINE (ML);
DIMSTYLE/DIMCURRENT/CLAYER/LTSCALE/CELTSCALE/SCALETEXT/XDATA/UCSICON/REGEN/DRAWORDER extended
- PLINE arc mode: A/L/CLOSE text input, bulge computation, arc preview
- Warning cleanup: removed unused imports and unreachable patterns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 15:31:05 +03:00
|
|
|
|
/// Find the last placed linear or aligned dimension in the document.
|
|
|
|
|
|
/// Returns `(first_point, second_point, definition_point, rotation_rad)` in world-space.
|
|
|
|
|
|
fn find_last_linear_dim(scene: &crate::scene::Scene) -> Option<(glam::Vec3, glam::Vec3, glam::Vec3, f64)> {
|
|
|
|
|
|
use acadrust::entities::Dimension;
|
|
|
|
|
|
let mut best_handle: u64 = 0;
|
|
|
|
|
|
let mut result: Option<(glam::Vec3, glam::Vec3, glam::Vec3, f64)> = None;
|
|
|
|
|
|
|
|
|
|
|
|
for entity in scene.document.entities() {
|
|
|
|
|
|
if let acadrust::EntityType::Dimension(dim) = entity {
|
|
|
|
|
|
let h = entity.common().handle.value();
|
|
|
|
|
|
if h <= best_handle {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
let item = match dim {
|
|
|
|
|
|
Dimension::Linear(d) => {
|
|
|
|
|
|
let p1 = glam::Vec3::new(d.first_point.x as f32, d.first_point.y as f32, d.first_point.z as f32);
|
|
|
|
|
|
let p2 = glam::Vec3::new(d.second_point.x as f32, d.second_point.y as f32, d.second_point.z as f32);
|
|
|
|
|
|
let dp = glam::Vec3::new(d.base.definition_point.x as f32, d.base.definition_point.y as f32, d.base.definition_point.z as f32);
|
|
|
|
|
|
Some((p1, p2, dp, d.rotation))
|
|
|
|
|
|
}
|
|
|
|
|
|
Dimension::Aligned(d) => {
|
|
|
|
|
|
let p1 = glam::Vec3::new(d.first_point.x as f32, d.first_point.y as f32, d.first_point.z as f32);
|
|
|
|
|
|
let p2 = glam::Vec3::new(d.second_point.x as f32, d.second_point.y as f32, d.second_point.z as f32);
|
|
|
|
|
|
let dp = glam::Vec3::new(d.base.definition_point.x as f32, d.base.definition_point.y as f32, d.base.definition_point.z as f32);
|
|
|
|
|
|
let dx = (d.second_point.x - d.first_point.x) as f32;
|
|
|
|
|
|
let dy = (d.second_point.y - d.first_point.y) as f32;
|
|
|
|
|
|
let rot = dy.atan2(dx) as f64;
|
|
|
|
|
|
Some((p1, p2, dp, rot))
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
};
|
|
|
|
|
|
if let Some(data) = item {
|
|
|
|
|
|
best_handle = h;
|
|
|
|
|
|
result = Some(data);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 09:23:17 +03:00
|
|
|
|
fn entity_type_name(entity: &acadrust::EntityType) -> &'static str {
|
|
|
|
|
|
match entity {
|
|
|
|
|
|
acadrust::EntityType::Line(_) => "LINE",
|
|
|
|
|
|
acadrust::EntityType::Circle(_) => "CIRCLE",
|
|
|
|
|
|
acadrust::EntityType::Arc(_) => "ARC",
|
|
|
|
|
|
acadrust::EntityType::LwPolyline(_) => "LWPOLYLINE",
|
|
|
|
|
|
acadrust::EntityType::Polyline(_) => "POLYLINE",
|
|
|
|
|
|
acadrust::EntityType::Polyline2D(_) => "POLYLINE2D",
|
|
|
|
|
|
acadrust::EntityType::Polyline3D(_) => "POLYLINE3D",
|
|
|
|
|
|
acadrust::EntityType::Text(_) => "TEXT",
|
|
|
|
|
|
acadrust::EntityType::MText(_) => "MTEXT",
|
|
|
|
|
|
acadrust::EntityType::Insert(_) => "INSERT",
|
|
|
|
|
|
acadrust::EntityType::Hatch(_) => "HATCH",
|
|
|
|
|
|
acadrust::EntityType::Dimension(_) => "DIMENSION",
|
|
|
|
|
|
acadrust::EntityType::Viewport(_) => "VIEWPORT",
|
|
|
|
|
|
acadrust::EntityType::Spline(_) => "SPLINE",
|
|
|
|
|
|
acadrust::EntityType::Ellipse(_) => "ELLIPSE",
|
|
|
|
|
|
acadrust::EntityType::Point(_) => "POINT",
|
|
|
|
|
|
acadrust::EntityType::Ray(_) => "RAY",
|
|
|
|
|
|
acadrust::EntityType::XLine(_) => "XLINE",
|
|
|
|
|
|
acadrust::EntityType::Face3D(_) => "3DFACE",
|
|
|
|
|
|
acadrust::EntityType::Table(_) => "TABLE",
|
|
|
|
|
|
acadrust::EntityType::MLine(_) => "MLINE",
|
|
|
|
|
|
acadrust::EntityType::RasterImage(_) => "RASTERIMAGE",
|
|
|
|
|
|
acadrust::EntityType::Wipeout(_) => "WIPEOUT",
|
feat: add Underlay entity support (PDF/DWF/DGN)
- New src/entities/underlay.rs: TruckConvertible (clip boundary polygon
or cross marker), Grippable (insertion + boundary vertex grips),
PropertyEditable (position, scale, rotation, contrast, fade, flags),
Transformable (translate, mirror, scale, rotate)
- Wire EntityType::Underlay into all four match arms in traits.rs
- UNDERLAY command: FADE/CONTRAST/ON/OFF/CLIP ON|OFF/MONO ON|OFF
subcommands to edit selected underlay entities
- Entity type name map entry for UNDERLAY
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 14:43:44 +03:00
|
|
|
|
acadrust::EntityType::Underlay(_) => "UNDERLAY",
|
2026-04-03 09:23:17 +03:00
|
|
|
|
acadrust::EntityType::AttributeDefinition(_)=> "ATTDEF",
|
|
|
|
|
|
acadrust::EntityType::AttributeEntity(_) => "ATTRIB",
|
|
|
|
|
|
acadrust::EntityType::Leader(_) => "LEADER",
|
|
|
|
|
|
acadrust::EntityType::Tolerance(_) => "TOLERANCE",
|
|
|
|
|
|
_ => "ENTITY",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-03 09:22:27 +03:00
|
|
|
|
fn entity_text_content(entity: &acadrust::EntityType) -> Option<String> {
|
|
|
|
|
|
match entity {
|
|
|
|
|
|
acadrust::EntityType::Text(t) => Some(t.value.clone()),
|
|
|
|
|
|
acadrust::EntityType::MText(t) => Some(t.value.clone()),
|
|
|
|
|
|
acadrust::EntityType::AttributeDefinition(a) => Some(a.default_value.clone()),
|
|
|
|
|
|
acadrust::EntityType::AttributeEntity(a) => Some(a.get_value().to_string()),
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn replace_entity_text(entity: &mut acadrust::EntityType, search: &str, rep: &str) {
|
|
|
|
|
|
let search_lc = search.to_lowercase();
|
|
|
|
|
|
match entity {
|
|
|
|
|
|
acadrust::EntityType::Text(t) => {
|
|
|
|
|
|
if t.value.to_lowercase().contains(&search_lc) {
|
|
|
|
|
|
t.value = t.value.replace(search, rep);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
acadrust::EntityType::MText(t) => {
|
|
|
|
|
|
if t.value.to_lowercase().contains(&search_lc) {
|
|
|
|
|
|
t.value = t.value.replace(search, rep);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
acadrust::EntityType::AttributeDefinition(a) => {
|
|
|
|
|
|
if a.default_value.to_lowercase().contains(&search_lc) {
|
|
|
|
|
|
a.default_value = a.default_value.replace(search, rep);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
acadrust::EntityType::AttributeEntity(a) => {
|
|
|
|
|
|
let cur = a.get_value().to_string();
|
|
|
|
|
|
if cur.to_lowercase().contains(&search_lc) {
|
|
|
|
|
|
a.set_value(cur.replace(search, rep));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|