forked from 1b-agent/opencadstudio-upstream
2277 lines
107 KiB
Rust
2277 lines
107 KiB
Rust
use super::*;
|
|
|
|
impl OpenCADStudio {
|
|
pub(super) fn dispatch_draw(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
|
match cmd {
|
|
// ── Draw commands ──────────────────────────────────────────────
|
|
"LINE" => {
|
|
use crate::modules::draw::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));
|
|
}
|
|
|
|
"MLINE" => {
|
|
use crate::modules::draw::draw::mline::MlineCommand;
|
|
let header = &self.tabs[i].scene.document.header;
|
|
let style_name = header.multiline_style.clone();
|
|
let scale = header.multiline_scale;
|
|
let justification = header.multiline_justification;
|
|
let styles = self.tabs[i]
|
|
.scene
|
|
.document
|
|
.objects
|
|
.iter()
|
|
.filter_map(|(handle, object)| match object {
|
|
acadrust::objects::ObjectType::MLineStyle(style) => {
|
|
Some((*handle, style.clone()))
|
|
}
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
let cmd_obj = MlineCommand::with_styles(styles, style_name, scale, justification);
|
|
self.command_line.push_info(&cmd_obj.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
|
|
}
|
|
|
|
cmd if cmd == "WIPEOUT" || cmd == "WO" || cmd.starts_with("WIPEOUT ") => {
|
|
use crate::modules::draw::draw::wipeout::WipeoutCommand;
|
|
let args = cmd
|
|
.split_once(' ')
|
|
.map(|(_, r)| r.trim().to_uppercase())
|
|
.unwrap_or_default();
|
|
let wo_cmd = match args.as_str() {
|
|
"P" | "POLYLINE" => WipeoutCommand::new_polyline(),
|
|
"R" | "RECTANGULAR" => WipeoutCommand::new_rectangular(),
|
|
_ => WipeoutCommand::new_polygonal(
|
|
crate::modules::draw::draw::wipeout::wipeout_frame_mode(
|
|
&self.tabs[i].scene.document,
|
|
),
|
|
),
|
|
};
|
|
self.command_line.push_info(&wo_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(wo_cmd));
|
|
}
|
|
|
|
cmd if cmd == "IMAGE" || cmd == "IMAGEATTACH" || cmd == "IM" => {
|
|
return Some(Task::done(Message::ImagePick));
|
|
}
|
|
|
|
cmd if cmd == "IMAGEEMBED" => {
|
|
return Some(Task::done(Message::ImageEmbedPick));
|
|
}
|
|
|
|
"REVCLOUD" | "REVCLOUD_RECTANGULAR" | "REVCLOUD_POLYGONAL" | "REVCLOUD_FREEHAND" => {
|
|
use crate::modules::draw::draw::revcloud::RevCloudCommand;
|
|
let view_height = self.tabs[i].scene.camera.borrow().ortho_size() as f64 * 2.0;
|
|
let default_arc_length = (view_height * 0.0125).max(1.0e-6);
|
|
let sources = self.tabs[i]
|
|
.scene
|
|
.document
|
|
.entities()
|
|
.map(|entity| (entity.common().handle, entity.clone()))
|
|
.collect();
|
|
let mut command = RevCloudCommand::new(default_arc_length, sources);
|
|
if let Some(mode) = cmd.strip_prefix("REVCLOUD_") {
|
|
command.on_text_input(mode);
|
|
}
|
|
self.command_line.push_info(&command.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
|
}
|
|
|
|
"ATTDEF" => {
|
|
use crate::modules::draw::draw::attdef::AttdefCommand;
|
|
let defaults = crate::scene::creation_style::current_text_defaults(
|
|
&self.tabs[i].scene.document,
|
|
);
|
|
let cmd = AttdefCommand::with_text_defaults(
|
|
defaults.height,
|
|
defaults.style_name,
|
|
defaults.width_factor,
|
|
defaults.oblique_angle,
|
|
);
|
|
self.command_line.push_info(&cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
}
|
|
|
|
// Command-line attribute editing on selected Insert entities. Bare
|
|
// ATTEDIT and the ATE alias launch the interactive editor instead
|
|
// (see the ATTEDIT arm in the inquiry family); the dash form is the
|
|
// command-line entry point.
|
|
// Usage:
|
|
// -ATTEDIT — list all attributes on selected Insert(s)
|
|
// ATTEDIT <tag> <v> — quick-set attribute <tag> to <v>
|
|
cmd if cmd.starts_with("ATTEDIT ")
|
|
|| cmd == "-ATTEDIT"
|
|
|| cmd.starts_with("-ATTEDIT ") =>
|
|
{
|
|
let rest = cmd
|
|
.trim_start_matches("-ATTEDIT")
|
|
.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(crate::t!("ATTEDIT: select an Insert entity first.").as_ref());
|
|
} 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(
|
|
crate::tf!(" Insert {:x}: no attributes.", sh.value())
|
|
.as_ref(),
|
|
);
|
|
} else {
|
|
for attr in &ins.attributes {
|
|
self.command_line.push_output(
|
|
crate::tf!(
|
|
" [{tag}] = {val}",
|
|
tag = attr.tag,
|
|
val = attr.get_value()
|
|
)
|
|
.as_ref(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if !found_any {
|
|
self.command_line.push_error(
|
|
crate::t!("ATTEDIT: no Insert entities in selection.").as_ref(),
|
|
);
|
|
}
|
|
// 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 self.tabs[i].scene.is_layer_locked(*sh) {
|
|
continue;
|
|
}
|
|
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(
|
|
crate::tf!(
|
|
"ATTEDIT: updated {changed} attribute(s) [{tag_up}] = {new_val}."
|
|
)
|
|
.as_ref(),
|
|
);
|
|
} else {
|
|
self.command_line.push_error(
|
|
crate::tf!("ATTEDIT: tag '{tag_up}' not found in selection.")
|
|
.as_ref(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
"ATTDISP" => {
|
|
use crate::command::KeywordCommand;
|
|
let c = KeywordCommand::new(
|
|
"ATTDISP",
|
|
"ATTDISP attribute display [On / Off / Normal]:",
|
|
vec![
|
|
("On", "ON", None),
|
|
("Off", "OFF", None),
|
|
("Normal", "NORMAL", None),
|
|
],
|
|
);
|
|
self.command_line.push_info(&c.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
|
}
|
|
cmd if cmd.starts_with("ATTDISP ") => {
|
|
let sub = cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase();
|
|
match sub.as_str() {
|
|
"ON" | "OFF" | "NORMAL" => {
|
|
let handles: Vec<_> = self.tabs[i]
|
|
.scene
|
|
.document
|
|
.entities()
|
|
.filter_map(|entity| {
|
|
matches!(entity, acadrust::EntityType::AttributeDefinition(_))
|
|
.then_some(entity.common().handle)
|
|
})
|
|
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
|
|
.collect();
|
|
self.push_undo_snapshot(i, "ATTDISP");
|
|
let mut count = 0usize;
|
|
for handle in handles {
|
|
if let Some(acadrust::EntityType::AttributeDefinition(ad)) =
|
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
|
{
|
|
match sub.as_str() {
|
|
"ON" => {
|
|
ad.flags.invisible = false;
|
|
count += 1;
|
|
}
|
|
"OFF" => {
|
|
ad.flags.invisible = true;
|
|
count += 1;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
self.tabs[i].dirty = true;
|
|
self.command_line.push_output(
|
|
crate::tf!("ATTDISP {sub}: {count} attribute definition(s) updated.")
|
|
.as_ref(),
|
|
);
|
|
}
|
|
_ => {
|
|
self.command_line
|
|
.push_info(crate::t!("Usage: ATTDISP ON | OFF | NORMAL").as_ref());
|
|
}
|
|
}
|
|
}
|
|
|
|
"DONUT" => {
|
|
use crate::modules::draw::draw::donut::DonutCommand;
|
|
let cmd = DonutCommand::new();
|
|
self.command_line.push_info(&cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
}
|
|
|
|
"CIRCLE" => {
|
|
use crate::modules::draw::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::draw::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::draw::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::draw::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::draw::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::draw::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" => {
|
|
use crate::modules::draw::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_3P" => {
|
|
use crate::modules::draw::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_CSE" => {
|
|
use crate::modules::draw::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_SCE" => {
|
|
use crate::modules::draw::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::draw::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::draw::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::draw::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::draw::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::draw::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::draw::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::draw::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));
|
|
}
|
|
"ARC_CONT" => {
|
|
use crate::modules::draw::draw::arc::{continue_anchor, ArcContCommand};
|
|
// Prefer the anchor recorded when the last line/arc was drawn (it
|
|
// knows the true drawing-end tangent); otherwise fall back to the
|
|
// last line/arc found in the document (e.g. after a file load).
|
|
let seed = self.cont_anchor.or_else(|| {
|
|
self.tabs[i]
|
|
.scene
|
|
.document
|
|
.entities()
|
|
.filter_map(|e| continue_anchor(e, None))
|
|
.last()
|
|
});
|
|
match seed {
|
|
Some((s, tangent)) => {
|
|
let new_cmd = ArcContCommand::new(s, tangent);
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
None => {
|
|
self.command_line.push_info(
|
|
crate::t!("ARC Continue No previous line or arc to continue.")
|
|
.as_ref(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
"RECT" | "RECTANG" => {
|
|
use crate::modules::draw::draw::shapes::RectCommand;
|
|
let new_cmd = RectCommand::new();
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
if self.ortho_mode {
|
|
self.rect_suppressed_ortho = true;
|
|
self.ortho_mode = false;
|
|
}
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
"RECT_ROT" => {
|
|
use crate::modules::draw::draw::shapes::RectRotCommand;
|
|
let new_cmd = RectRotCommand::new();
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
if self.ortho_mode {
|
|
self.rect_suppressed_ortho = true;
|
|
self.ortho_mode = false;
|
|
}
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
"RECT_CEN" => {
|
|
use crate::modules::draw::draw::shapes::RectCenCommand;
|
|
let new_cmd = RectCenCommand::new();
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
if self.ortho_mode {
|
|
self.rect_suppressed_ortho = true;
|
|
self.ortho_mode = false;
|
|
}
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
"POLY" | "POLYGON" => {
|
|
use crate::modules::draw::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::draw::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::draw::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" => {
|
|
use crate::modules::draw::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));
|
|
}
|
|
|
|
"3DPOLY" => {
|
|
use crate::modules::draw::draw::poly3d::Poly3dCommand;
|
|
let new_cmd = Poly3dCommand::new();
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
"3DMESH" => {
|
|
use crate::modules::draw::draw::mesh3d::Mesh3dCommand;
|
|
let new_cmd = Mesh3dCommand::new();
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
"3DFACE" => {
|
|
use crate::modules::draw::draw::face3d::Face3dCommand;
|
|
let new_cmd = Face3dCommand::new();
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
"EDGE" => {
|
|
use crate::modules::draw::draw::face3d::FaceEdgeCommand;
|
|
let faces = self.tabs[i]
|
|
.scene
|
|
.document
|
|
.entities()
|
|
.filter_map(|entity| match entity {
|
|
acadrust::EntityType::Face3D(face) => {
|
|
Some((face.common.handle, face.clone()))
|
|
}
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
let new_cmd = FaceEdgeCommand::new(faces);
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
// 2D filled solid. Reached via SO / SOLID2D — the bare SOLID verb is
|
|
// currently the shaded-display toggle (token collision tracked).
|
|
"SOLID" | "SOLID2D" => {
|
|
use crate::modules::draw::draw::solid2d::Solid2dCommand;
|
|
let new_cmd = Solid2dCommand::new();
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
"HELIX" => {
|
|
use crate::modules::draw::draw::helix::HelixCommand;
|
|
let new_cmd = HelixCommand::new();
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
"TRACE" => {
|
|
use crate::modules::draw::draw::trace::TraceCommand;
|
|
let new_cmd = TraceCommand::new();
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
"CENTERLINE" => {
|
|
use crate::modules::draw::draw::centerline::CenterLineCommand;
|
|
let settings = self.tabs[i].scene.centerline_settings();
|
|
let new_cmd = CenterLineCommand::new(settings);
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
"CENTERRESET" => {
|
|
let handles = self.tabs[i].scene.selected_handles_in_order();
|
|
self.push_undo_snapshot(i, "CENTERRESET");
|
|
let count = self.tabs[i].scene.reset_centerlines(&handles)
|
|
+ self.tabs[i].scene.reset_center_marks(&handles);
|
|
if count > 0 {
|
|
self.tabs[i].dirty = true;
|
|
}
|
|
self.command_line.push_output(&crate::tf!(
|
|
"CENTERRESET: {count} center object(s) updated."
|
|
));
|
|
}
|
|
|
|
"CENTERREASSOCIATE" => {
|
|
let handles = self.tabs[i].scene.selected_handles_in_order();
|
|
let mark_targets: Vec<_> = handles
|
|
.iter()
|
|
.copied()
|
|
.filter(|handle| {
|
|
let Some(acadrust::EntityType::Line(line)) =
|
|
self.tabs[i].scene.document.get_entity(*handle)
|
|
else {
|
|
return false;
|
|
};
|
|
acadrust::entities::CenterMarkAssociation::read(&line.common.extended_data)
|
|
.is_some()
|
|
})
|
|
.collect();
|
|
if mark_targets.len() == 1 && handles.len() == 1 {
|
|
use crate::modules::draw::draw::dimcenter::CenterMarkReassociateCommand;
|
|
let new_cmd = CenterMarkReassociateCommand::new(mark_targets[0]);
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
return Some(self.finish_dispatch(cmd));
|
|
}
|
|
self.push_undo_snapshot(i, "CENTERREASSOCIATE");
|
|
let count = self.tabs[i]
|
|
.scene
|
|
.set_centerline_association(&handles, true)
|
|
+ self.tabs[i]
|
|
.scene
|
|
.set_center_mark_association(&handles, true);
|
|
if count > 0 {
|
|
self.tabs[i].dirty = true;
|
|
}
|
|
self.command_line.push_output(&crate::tf!(
|
|
"CENTERREASSOCIATE: {count} center object(s) associated."
|
|
));
|
|
}
|
|
|
|
"CENTERDISASSOCIATE" => {
|
|
let handles = self.tabs[i].scene.selected_handles_in_order();
|
|
self.push_undo_snapshot(i, "CENTERDISASSOCIATE");
|
|
let count = self.tabs[i]
|
|
.scene
|
|
.set_centerline_association(&handles, false)
|
|
+ self.tabs[i]
|
|
.scene
|
|
.set_center_mark_association(&handles, false);
|
|
if count > 0 {
|
|
self.tabs[i].dirty = true;
|
|
}
|
|
self.command_line.push_output(&crate::tf!(
|
|
"CENTERDISASSOCIATE: {count} center object(s) detached."
|
|
));
|
|
}
|
|
|
|
"DIMCENTER" => {
|
|
use crate::modules::draw::draw::dimcenter::DimCenterCommand;
|
|
let new_cmd = DimCenterCommand::new();
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
"CENTERMARK" => {
|
|
use crate::modules::draw::draw::dimcenter::CenterMarkCommand;
|
|
let settings = self.tabs[i].scene.centerline_settings();
|
|
let new_cmd = CenterMarkCommand::new(settings);
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
"SKETCH" => {
|
|
use crate::modules::draw::draw::sketch::SketchCommand;
|
|
let header = &self.tabs[i].scene.document.header;
|
|
let new_cmd = SketchCommand::new(
|
|
header.sketch_type,
|
|
header.sketch_increment,
|
|
header.sketch_tolerance,
|
|
);
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
"REVERSE" => {
|
|
use crate::modules::draw::modify::reverse::ReverseCommand;
|
|
if self.tabs[i].scene.selected.is_empty() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
let selection = SelectObjectsCommand::new("REVERSE");
|
|
self.command_line.push_info(&selection.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(selection));
|
|
} else {
|
|
let replacements = self.tabs[i]
|
|
.scene
|
|
.selected_entities()
|
|
.into_iter()
|
|
.filter(|(handle, _)| !self.tabs[i].scene.is_layer_locked(*handle))
|
|
.filter_map(|(handle, entity)| {
|
|
ReverseCommand::reversed(entity)
|
|
.map(|reversed| (handle, vec![reversed]))
|
|
})
|
|
.collect::<Vec<_>>();
|
|
if !replacements.is_empty() {
|
|
return Some(self.apply_cmd_result(
|
|
crate::command::CmdResult::ReplaceMany(replacements, Vec::new()),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
"MEASUREGEOM" => {
|
|
use crate::modules::draw::inquiry::measuregeom::MeasureGeomCommand;
|
|
let new_cmd = MeasureGeomCommand::new();
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
// ── Modify commands ────────────────────────────────────────────
|
|
// MOVE works from picked points, so it already relocates entities
|
|
// in 3D; 3DMOVE is the same operation.
|
|
"MOVE" | "3DMOVE" => {
|
|
let handles: Vec<_> = self.tabs[i]
|
|
.scene
|
|
.selected_entities()
|
|
.into_iter()
|
|
.map(|(h, _)| h)
|
|
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
|
|
.collect();
|
|
if handles.is_empty() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
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::draw::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" => {
|
|
let handles: Vec<_> = self.tabs[i]
|
|
.scene
|
|
.selected_entities()
|
|
.into_iter()
|
|
.map(|(h, _)| h)
|
|
.collect();
|
|
if handles.is_empty() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
let cmd = SelectObjectsCommand::new("COPY");
|
|
self.command_line.push_info(&cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
} else {
|
|
use crate::modules::draw::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" => {
|
|
let handles: Vec<_> = self.tabs[i]
|
|
.scene
|
|
.selected_entities()
|
|
.into_iter()
|
|
.map(|(h, _)| h)
|
|
.collect();
|
|
if handles.is_empty() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
let cmd = SelectObjectsCommand::new("ROTATE");
|
|
self.command_line.push_info(&cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
} else {
|
|
use crate::modules::draw::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));
|
|
}
|
|
}
|
|
|
|
"TORIENT" => {
|
|
let handles: Vec<_> = self.tabs[i]
|
|
.scene
|
|
.selected_entities()
|
|
.into_iter()
|
|
.map(|(h, _)| h)
|
|
.collect();
|
|
if handles.is_empty() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
let cmd = SelectObjectsCommand::new("TORIENT");
|
|
self.command_line.push_info(&cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
} else {
|
|
use crate::modules::draw::modify::torient::TorientCommand;
|
|
let entities: Vec<_> = handles
|
|
.iter()
|
|
.filter_map(|&h| {
|
|
self.tabs[i]
|
|
.scene
|
|
.document
|
|
.get_entity(h)
|
|
.cloned()
|
|
.map(|e| (h, e))
|
|
})
|
|
.collect();
|
|
let cam_rot = self.tabs[i].scene.camera.borrow().rotation;
|
|
let right = cam_rot * glam::Vec3::X;
|
|
let view_twist = right.y.atan2(right.x) as f64;
|
|
let new_cmd = TorientCommand::new(entities, view_twist);
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
}
|
|
|
|
"POINT" | "MULTIPOINT" => {
|
|
use crate::modules::draw::draw::point::PointCommand;
|
|
let new_cmd = if cmd == "MULTIPOINT" {
|
|
PointCommand::multiple()
|
|
} else {
|
|
PointCommand::new()
|
|
};
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
"RAY" => {
|
|
use crate::modules::draw::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" | "CONSTRUCTIONLINE" => {
|
|
use crate::modules::draw::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));
|
|
}
|
|
|
|
"HATCH" => {
|
|
use crate::modules::draw::draw::hatch::HatchCommand;
|
|
let working_plane = if self.tabs[i].editing_model_space() {
|
|
self.tabs[i].ucs_xform().working_plane()
|
|
} else {
|
|
crate::command::WorkingPlane::default()
|
|
};
|
|
let normal = working_plane.z.normalize_or(glam::DVec3::Z);
|
|
let elevation = working_plane.origin.dot(normal);
|
|
let storage = crate::entities::curve::ocs_plane(
|
|
acadrust::types::Vector3::new(normal.x, normal.y, normal.z),
|
|
elevation,
|
|
);
|
|
let plane = crate::command::WorkingPlane::new(
|
|
glam::DVec3::from_array(storage.origin),
|
|
glam::DVec3::from_array(storage.x_axis),
|
|
glam::DVec3::from_array(storage.y_axis),
|
|
);
|
|
let boundary_sources = self.tabs[i].scene.boundary_sources_on_plane(plane, 1.0e-6);
|
|
let outlines = crate::scene::boundary_faces(&boundary_sources, 1.0e-6);
|
|
let selected = self.tabs[i]
|
|
.scene
|
|
.selected_entities()
|
|
.into_iter()
|
|
.map(|(handle, _)| handle)
|
|
.collect::<Vec<_>>();
|
|
let inherited = selected.iter().find_map(|handle| {
|
|
let model = self.tabs[i].scene.hatches.get(handle)?.clone();
|
|
let common = self.tabs[i].scene.document.get_entity(*handle)?.common();
|
|
Some((model, common.color.clone(), common.transparency))
|
|
});
|
|
let new_cmd =
|
|
HatchCommand::new(outlines, boundary_sources, selected, inherited, plane)
|
|
.with_origin(self.tabs[i].scene.document.hatch_origin());
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
self.refresh_area_preview(i);
|
|
}
|
|
|
|
"HATCHEDIT" => {
|
|
use crate::modules::draw::draw::hatchedit::HatcheditCommand;
|
|
// If a single hatch is already selected, skip the pick step.
|
|
let sel = self.tabs[i].scene.selected_entities();
|
|
if sel.len() == 1 {
|
|
let (h, _) = sel[0];
|
|
if let Some(model) = self.tabs[i].scene.hatches.get(&h).cloned() {
|
|
let entity = self.tabs[i].scene.document.get_entity(h);
|
|
let annotative = entity.is_some_and(|entity| {
|
|
crate::scene::annotative::is_annotative(
|
|
&self.tabs[i].scene.document,
|
|
entity,
|
|
)
|
|
});
|
|
let (scale, angle) = match entity {
|
|
Some(acadrust::EntityType::Hatch(hatch)) => (
|
|
hatch.pattern_scale as f32,
|
|
hatch.pattern_angle.to_degrees() as f32,
|
|
),
|
|
_ => (model.scale, model.angle_offset.to_degrees()),
|
|
};
|
|
let cmd = HatcheditCommand::with_handle(
|
|
h,
|
|
model.name.clone(),
|
|
scale,
|
|
angle,
|
|
annotative,
|
|
)
|
|
.with_appearance(
|
|
entity,
|
|
self.tabs[i].scene.document.header.current_entity_color,
|
|
self.tabs[i].scene.document.current_entity_transparency(),
|
|
)
|
|
.with_origin(self.tabs[i].scene.document.hatch_origin());
|
|
self.command_line.push_info(&cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
} else {
|
|
self.command_line.push_error(
|
|
crate::t!("HATCHEDIT: selected entity is not a hatch.").as_ref(),
|
|
);
|
|
}
|
|
} else {
|
|
let cmd = HatcheditCommand::new()
|
|
.with_appearance(
|
|
None,
|
|
self.tabs[i].scene.document.header.current_entity_color,
|
|
self.tabs[i].scene.document.current_entity_transparency(),
|
|
)
|
|
.with_origin(self.tabs[i].scene.document.hatch_origin());
|
|
self.command_line.push_info(&cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
}
|
|
}
|
|
|
|
"GRADIENT" => {
|
|
use crate::modules::draw::draw::hatch::GradientCommand;
|
|
let boundary_sources = self.tabs[i]
|
|
.scene
|
|
.boundary_sources_on_plane(crate::command::WorkingPlane::default(), 1.0e-6);
|
|
let outlines = crate::scene::boundary_faces(&boundary_sources, 1.0e-6);
|
|
let new_cmd = GradientCommand::new(outlines, boundary_sources);
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
"BOUNDARY" => {
|
|
use crate::modules::draw::draw::hatch::BoundaryCommand;
|
|
let plane = if self.tabs[i].editing_model_space() {
|
|
self.tabs[i].ucs_xform().working_plane()
|
|
} else {
|
|
crate::command::WorkingPlane::default()
|
|
};
|
|
let sources = self.tabs[i].scene.boundary_sources_on_plane(plane, 1.0e-6);
|
|
let selected = self.tabs[i]
|
|
.scene
|
|
.selected_entities()
|
|
.iter()
|
|
.map(|(handle, _)| *handle)
|
|
.collect();
|
|
let new_cmd = BoundaryCommand::new(sources, selected, plane);
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
"ELLIPSE" => {
|
|
use crate::modules::draw::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::draw::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::draw::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" | "SPLINECV" => {
|
|
use crate::modules::draw::draw::spline::SplineCommand;
|
|
let new_cmd = if cmd == "SPLINECV" {
|
|
SplineCommand::control_vertices()
|
|
} else {
|
|
SplineCommand::new()
|
|
}
|
|
.with_document(&self.tabs[i].scene.document);
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
"SCALE" => {
|
|
let handles: Vec<_> = self.tabs[i]
|
|
.scene
|
|
.selected_entities()
|
|
.into_iter()
|
|
.map(|(h, _)| h)
|
|
.collect();
|
|
if handles.is_empty() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
let cmd = SelectObjectsCommand::new("SCALE");
|
|
self.command_line.push_info(&cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
} else {
|
|
use crate::modules::draw::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" => {
|
|
let handles: Vec<_> = self.tabs[i]
|
|
.scene
|
|
.selected_entities()
|
|
.into_iter()
|
|
.map(|(h, _)| h)
|
|
.collect();
|
|
if handles.is_empty() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
let cmd = SelectObjectsCommand::new("MIRROR");
|
|
self.command_line.push_info(&cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
} else {
|
|
use crate::modules::draw::modify::mirror::MirrorCommand;
|
|
let (wires, text_ghosts) = self.tabs[i].scene.mirror_preview_parts(&handles);
|
|
let mirror_text = self.tabs[i].scene.document.header.mirror_text;
|
|
let new_cmd = MirrorCommand::new(handles, wires, text_ghosts, mirror_text);
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
}
|
|
|
|
"ERASE" => {
|
|
let handles: Vec<_> = self.tabs[i]
|
|
.scene
|
|
.selected_entities()
|
|
.into_iter()
|
|
.map(|(h, _)| h)
|
|
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
|
|
.collect();
|
|
if handles.is_empty() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
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();
|
|
let delta_safe = self.delta_erase_safe(i, &handles);
|
|
let pending = self.begin_undo(i, "ERASE", handles.len(), delta_safe);
|
|
// Stash the erased entities so OOPS can restore them.
|
|
self.oops_cache = handles
|
|
.iter()
|
|
.filter_map(|h| self.tabs[i].scene.document.get_entity_arc(*h))
|
|
.collect();
|
|
self.tabs[i].scene.erase_entities(&handles);
|
|
self.tabs[i].dirty = true;
|
|
self.refresh_properties();
|
|
self.command_line
|
|
.push_output(crate::tf!("{n} object(s) erased.").as_ref());
|
|
if let Some(pd) = pending {
|
|
self.commit_undo_delta(i, pd);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Persistent constraints ────────────────────────────────────
|
|
"GCSHOW" | "GCHIDE" | "DCSHOW" | "DCHIDE" => {
|
|
let handles = self.tabs[i].scene.selected_handles_in_order();
|
|
if handles.is_empty() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
let sel = SelectObjectsCommand::new(cmd);
|
|
self.command_line.push_info(&sel.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(sel));
|
|
} else {
|
|
let scope = self.tabs[i].current_parametric_scope();
|
|
let dimensional = cmd.starts_with("DC");
|
|
let visible = !matches!(cmd, "GCHIDE" | "DCHIDE");
|
|
let count = self.tabs[i].scene.set_parametric_constraint_visibility(
|
|
scope,
|
|
Some(&handles),
|
|
dimensional,
|
|
visible,
|
|
);
|
|
self.command_line.push_output(
|
|
format!("{} constraint indicator(s) updated.", count).as_str(),
|
|
);
|
|
}
|
|
}
|
|
|
|
"GCRESET" => {
|
|
let scope = self.tabs[i].current_parametric_scope();
|
|
let count = self.tabs[i]
|
|
.scene
|
|
.set_parametric_constraint_visibility(scope, None, false, true);
|
|
self.command_line
|
|
.push_output(format!("{} constraint indicator(s) reset.", count).as_str());
|
|
}
|
|
|
|
"GCSHOWALL" | "GCHIDEALL" | "DCSHOWALL" | "DCHIDEALL" => {
|
|
let scope = self.tabs[i].current_parametric_scope();
|
|
let dimensional = cmd.starts_with("DC");
|
|
let visible = matches!(cmd, "GCSHOWALL" | "DCSHOWALL");
|
|
let count = self.tabs[i].scene.set_parametric_constraint_visibility(
|
|
scope,
|
|
None,
|
|
dimensional,
|
|
visible,
|
|
);
|
|
self.command_line
|
|
.push_output(format!("{} constraint indicator(s) updated.", count).as_str());
|
|
}
|
|
|
|
"DELCONSTRAINT" => {
|
|
let handles = self.tabs[i].scene.selected_handles_in_order();
|
|
if handles.is_empty() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
let sel = SelectObjectsCommand::new(cmd);
|
|
self.command_line.push_info(&sel.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(sel));
|
|
} else {
|
|
let scope = self.tabs[i].current_parametric_scope();
|
|
let Some(before) = self.tabs[i].scene.parametric_constraint_set(scope).cloned()
|
|
else {
|
|
self.command_line.push_output("No constraints found.");
|
|
return None;
|
|
};
|
|
let mut ids: Vec<_> = before
|
|
.constraints
|
|
.iter()
|
|
.filter(|constraint| {
|
|
constraint
|
|
.refs
|
|
.iter()
|
|
.any(|reference| handles.contains(&reference.entity))
|
|
})
|
|
.map(|constraint| constraint.id)
|
|
.collect();
|
|
ids.sort_unstable();
|
|
ids.dedup();
|
|
if ids.is_empty() {
|
|
self.command_line.push_output("No constraints found.");
|
|
return None;
|
|
}
|
|
let pending = self.begin_undo(i, "Delete constraints", handles.len(), true);
|
|
self.tabs[i]
|
|
.scene
|
|
.record_undo_parametric_constraints_before(scope, before);
|
|
let set = self.tabs[i].scene.parametric_constraint_set_mut(scope);
|
|
for id in &ids {
|
|
set.remove(*id);
|
|
}
|
|
let changes: Vec<_> = handles
|
|
.iter()
|
|
.copied()
|
|
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
|
.collect();
|
|
self.tabs[i].scene.bump_entities(&changes);
|
|
self.tabs[i].dirty = true;
|
|
self.refresh_properties();
|
|
self.command_line
|
|
.push_output(format!("{} constraint(s) deleted.", ids.len()).as_str());
|
|
if let Some(pd) = pending {
|
|
self.commit_undo_delta(i, pd);
|
|
}
|
|
}
|
|
}
|
|
|
|
"CONSTRAINTSETTINGS" => {
|
|
self.auto_constrain_saved = Some(self.auto_constrain_settings.clone());
|
|
self.auto_constrain_selected_row = 0;
|
|
self.auto_constrain_distance_input =
|
|
format!("{}", self.auto_constrain_settings.distance_tolerance);
|
|
self.auto_constrain_angle_input =
|
|
format!("{}", self.auto_constrain_settings.angle_tolerance_deg);
|
|
self.active_modal = Some(crate::app::ModalKind::AutoConstrainSettings);
|
|
}
|
|
|
|
"AUTOCONSTRAIN" => {
|
|
let handles = self.tabs[i].scene.selected_handles_in_order();
|
|
if handles.is_empty() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
let sel = SelectObjectsCommand::auto_constrain(cmd);
|
|
self.command_line.push_info(&sel.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(sel));
|
|
} else {
|
|
let scope = self.tabs[i].current_parametric_scope();
|
|
let inferred = self.tabs[i]
|
|
.scene
|
|
.inferred_parametric_constraints(
|
|
scope,
|
|
&handles,
|
|
&self.auto_constrain_settings,
|
|
);
|
|
if inferred.is_empty() {
|
|
self.command_line.push_output(
|
|
format!(
|
|
"0 constraint(s) applied to {} object(s).",
|
|
handles.len()
|
|
)
|
|
.as_str(),
|
|
);
|
|
self.tabs[i].scene.deselect_all();
|
|
self.refresh_selected_grips();
|
|
} else {
|
|
let before = self.tabs[i]
|
|
.scene
|
|
.parametric_constraint_set(scope)
|
|
.cloned()
|
|
.unwrap_or_else(|| {
|
|
crate::scene::parametric_constraints::ParametricConstraintSet::new(
|
|
scope,
|
|
)
|
|
});
|
|
let pending = self.begin_undo(i, "Auto constrain", handles.len(), true);
|
|
self.tabs[i]
|
|
.scene
|
|
.record_undo_parametric_constraints_before(scope, before);
|
|
let count = inferred.len();
|
|
for (kind, refs) in inferred {
|
|
let id = self.tabs[i]
|
|
.scene
|
|
.parametric_constraint_set_mut(scope)
|
|
.add(kind, refs, None);
|
|
self.tabs[i].scene.note_parametric_constraint_applied(
|
|
scope,
|
|
id,
|
|
self.constraint_bar_display,
|
|
);
|
|
}
|
|
let changes: Vec<_> = handles
|
|
.iter()
|
|
.copied()
|
|
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
|
.collect();
|
|
self.tabs[i].scene.bump_entities_with_parametric_policy(
|
|
&changes,
|
|
&[],
|
|
self.constraint_solve_mode,
|
|
);
|
|
self.tabs[i].dirty = true;
|
|
self.refresh_properties();
|
|
self.command_line.push_output(
|
|
format!(
|
|
"{} constraint(s) applied to {} object(s).",
|
|
count,
|
|
handles.len()
|
|
)
|
|
.as_str(),
|
|
);
|
|
if let Some(pd) = pending {
|
|
self.commit_undo_delta(i, pd);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
"SMOOTHCONSTRAINT" => {
|
|
let handles = self.tabs[i].scene.selected_handles_in_order();
|
|
if handles.is_empty() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
let sel = SelectObjectsCommand::new(cmd);
|
|
self.command_line.push_info(&sel.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(sel));
|
|
} else {
|
|
let Some(refs) = self.tabs[i].scene.smooth_constraint_refs(&handles) else {
|
|
self.command_line
|
|
.push_output("Select one open spline and one open target curve.");
|
|
return None;
|
|
};
|
|
use crate::command::CmdResult;
|
|
use crate::scene::parametric_constraints::ConstraintKind;
|
|
return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint {
|
|
kind: ConstraintKind::Smooth,
|
|
refs,
|
|
driving_param: None,
|
|
label: "Smooth constraint",
|
|
}));
|
|
}
|
|
}
|
|
|
|
"DCCONVERT" => {
|
|
let handles = self.tabs[i].scene.selected_handles_in_order();
|
|
if handles.is_empty() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
let sel = SelectObjectsCommand::new(cmd);
|
|
self.command_line.push_info(&sel.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(sel));
|
|
} else {
|
|
let scope = self.tabs[i].current_parametric_scope();
|
|
let conversions: Vec<_> = handles
|
|
.iter()
|
|
.copied()
|
|
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
|
|
.filter_map(|handle| {
|
|
let (kind, refs, value) =
|
|
crate::scene::dimension_assoc::constraint_from_associative_dimension(
|
|
&self.tabs[i].scene.document,
|
|
handle,
|
|
)?;
|
|
self.tabs[i]
|
|
.scene
|
|
.validate_parametric_constraint(kind, &refs, Some(&value))
|
|
.ok()?;
|
|
Some((handle, kind, refs, value))
|
|
})
|
|
.collect();
|
|
if conversions.is_empty() {
|
|
self.command_line
|
|
.push_output("No supported associative dimensions were selected.");
|
|
return None;
|
|
}
|
|
let before = self.tabs[i]
|
|
.scene
|
|
.parametric_constraint_set(scope)
|
|
.cloned()
|
|
.unwrap_or_else(|| {
|
|
crate::scene::parametric_constraints::ParametricConstraintSet::new(
|
|
scope,
|
|
)
|
|
});
|
|
let mut touched: Vec<_> = conversions
|
|
.iter()
|
|
.flat_map(|(_, _, refs, _)| refs.iter().map(|reference| reference.entity))
|
|
.collect();
|
|
touched.sort_unstable();
|
|
touched.dedup();
|
|
let dimensions: Vec<_> = conversions
|
|
.iter()
|
|
.map(|(handle, _, _, _)| *handle)
|
|
.collect();
|
|
let pending = self.begin_undo(
|
|
i,
|
|
"Convert dimensions to constraints",
|
|
touched.len() + dimensions.len(),
|
|
false,
|
|
);
|
|
self.tabs[i]
|
|
.scene
|
|
.record_undo_parametric_constraints_before(scope, before);
|
|
for (_, kind, refs, value) in conversions {
|
|
let id = self.tabs[i].scene.parametric_constraint_set_mut(scope).add(
|
|
kind,
|
|
refs,
|
|
Some(value),
|
|
);
|
|
self.tabs[i].scene.note_parametric_constraint_applied(
|
|
scope,
|
|
id,
|
|
self.constraint_bar_display,
|
|
);
|
|
}
|
|
self.tabs[i].scene.erase_entities(&dimensions);
|
|
let changes: Vec<_> = touched
|
|
.into_iter()
|
|
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
|
.collect();
|
|
self.tabs[i].scene.bump_entities(&changes);
|
|
self.tabs[i].dirty = true;
|
|
self.refresh_properties();
|
|
self.command_line.push_output(
|
|
format!("{} dimension(s) converted.", dimensions.len()).as_str(),
|
|
);
|
|
if let Some(pd) = pending {
|
|
self.commit_undo_delta(i, pd);
|
|
}
|
|
}
|
|
}
|
|
|
|
"HCONSTRAINT" | "VCONSTRAINT" | "FXCONSTRAINT" => {
|
|
let handles = self.tabs[i].scene.selected_handles_in_order();
|
|
if handles.is_empty() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
let sel = SelectObjectsCommand::new(cmd);
|
|
self.command_line.push_info(&sel.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(sel));
|
|
} else if handles.len() != 1 {
|
|
self.command_line
|
|
.push_output("Select exactly one entity, then run this constraint again.");
|
|
} else {
|
|
use crate::command::CmdResult;
|
|
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef};
|
|
let (kind, label) = match cmd {
|
|
"HCONSTRAINT" => (ConstraintKind::Horizontal, "Horizontal constraint"),
|
|
"VCONSTRAINT" => (ConstraintKind::Vertical, "Vertical constraint"),
|
|
_ => (ConstraintKind::Fixed, "Fixed constraint"),
|
|
};
|
|
return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint {
|
|
kind,
|
|
refs: vec![ParametricRef::whole(handles[0])],
|
|
driving_param: None,
|
|
label,
|
|
}));
|
|
}
|
|
}
|
|
|
|
"CCONSTRAINT" | "GCCOINCIDENT" => {
|
|
use crate::modules::parametric::CoincidentConstraintCommand;
|
|
let new_cmd = CoincidentConstraintCommand::new();
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
"GEOMCONSTRAINT" => {
|
|
use crate::modules::parametric::GeomConstraintCommand;
|
|
let new_cmd = GeomConstraintCommand::new();
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
"EDCONSTRAINT" => {
|
|
use crate::modules::parametric::EqualDistanceConstraintCommand;
|
|
let new_cmd = EqualDistanceConstraintCommand::new();
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
"PCONSTRAINT" | "QCONSTRAINT" | "ECONSTRAINT" | "TCONSTRAINT" | "LCONSTRAINT"
|
|
| "NRCONSTRAINT" => {
|
|
let handles = self.tabs[i].scene.selected_handles_in_order();
|
|
if handles.is_empty() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
let sel = SelectObjectsCommand::new(cmd);
|
|
self.command_line.push_info(&sel.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(sel));
|
|
} else if handles.len() != 2 {
|
|
self.command_line.push_output(
|
|
"Select exactly two entities (first = reference, second = the one that moves), then run this constraint again.",
|
|
);
|
|
} else {
|
|
use crate::command::CmdResult;
|
|
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef};
|
|
let (kind, label) = match cmd {
|
|
"PCONSTRAINT" => (ConstraintKind::Parallel, "Parallel constraint"),
|
|
"QCONSTRAINT" => {
|
|
(ConstraintKind::Perpendicular, "Perpendicular constraint")
|
|
}
|
|
"TCONSTRAINT" => (ConstraintKind::Tangent, "Tangent constraint"),
|
|
"LCONSTRAINT" => (ConstraintKind::Colinear, "Colinear constraint"),
|
|
"NRCONSTRAINT" => (ConstraintKind::Normal, "Normal constraint"),
|
|
_ => (ConstraintKind::Equal, "Equal constraint"),
|
|
};
|
|
return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint {
|
|
kind,
|
|
refs: vec![
|
|
ParametricRef::whole(handles[0]),
|
|
ParametricRef::whole(handles[1]),
|
|
],
|
|
driving_param: None,
|
|
label,
|
|
}));
|
|
}
|
|
}
|
|
|
|
"NCONSTRAINT" => {
|
|
let handles = self.tabs[i].scene.selected_handles_in_order();
|
|
if handles.is_empty() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
let sel = SelectObjectsCommand::new(cmd);
|
|
self.command_line.push_info(&sel.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(sel));
|
|
} else if handles.len() != 2 {
|
|
self.command_line.push_output(
|
|
"Select exactly two circles/arcs, then run this constraint again.",
|
|
);
|
|
} else {
|
|
use crate::command::CmdResult;
|
|
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef};
|
|
return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint {
|
|
kind: ConstraintKind::Concentric,
|
|
refs: vec![
|
|
ParametricRef::center(handles[0]),
|
|
ParametricRef::center(handles[1]),
|
|
],
|
|
driving_param: None,
|
|
label: "Concentric constraint",
|
|
}));
|
|
}
|
|
}
|
|
|
|
"SYCONSTRAINT" => {
|
|
let handles = self.tabs[i].scene.selected_handles_in_order();
|
|
if handles.is_empty() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
let sel = SelectObjectsCommand::new(cmd);
|
|
self.command_line.push_info(&sel.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(sel));
|
|
} else if handles.len() != 3 {
|
|
self.command_line.push_output(
|
|
"Select exactly three entities (two circles/arcs, then the mirror line), then run this constraint again.",
|
|
);
|
|
} else {
|
|
use crate::command::CmdResult;
|
|
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef};
|
|
return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint {
|
|
kind: ConstraintKind::Symmetric,
|
|
refs: vec![
|
|
ParametricRef::center(handles[0]),
|
|
ParametricRef::center(handles[1]),
|
|
ParametricRef::whole(handles[2]),
|
|
],
|
|
driving_param: None,
|
|
label: "Symmetric constraint",
|
|
}));
|
|
}
|
|
}
|
|
|
|
"CPCONSTRAINT" | "MPCONSTRAINT" | "OCCONSTRAINT" => {
|
|
let handles = self.tabs[i].scene.selected_handles_in_order();
|
|
if handles.is_empty() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
let sel = SelectObjectsCommand::new(cmd);
|
|
self.command_line.push_info(&sel.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(sel));
|
|
} else if handles.len() != 1 {
|
|
self.command_line.push_output(
|
|
"Select exactly one entity (the circle or line the point should attach to), then run this constraint again.",
|
|
);
|
|
} else {
|
|
use crate::modules::parametric::PointOnEntityConstraintCommand;
|
|
use crate::scene::parametric_constraints::ConstraintKind;
|
|
let (name, kind, label) = match cmd {
|
|
"CPCONSTRAINT" => (
|
|
"CPCONSTRAINT",
|
|
ConstraintKind::CenterPoint,
|
|
"Center point constraint",
|
|
),
|
|
"MPCONSTRAINT" => (
|
|
"MPCONSTRAINT",
|
|
ConstraintKind::Midpoint,
|
|
"Midpoint constraint",
|
|
),
|
|
_ => (
|
|
"OCCONSTRAINT",
|
|
ConstraintKind::PointOnCurve,
|
|
"Point on curve constraint",
|
|
),
|
|
};
|
|
let new_cmd =
|
|
PointOnEntityConstraintCommand::new(name, kind, handles[0], label);
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
}
|
|
|
|
"DCONSTRAINT" | "DCLINEAR" | "DCHORIZONTAL" | "DCVERTICAL" | "DCALIGNED"
|
|
| "DCRADIUS" | "DCDIAMETER" => {
|
|
let handles = self.tabs[i].scene.selected_handles_in_order();
|
|
if handles.is_empty() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
let sel = SelectObjectsCommand::new(cmd);
|
|
self.command_line.push_info(&sel.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(sel));
|
|
} else if handles.len() != 1 {
|
|
self.command_line.push_output(
|
|
"Select exactly one line or circle, then run this constraint again.",
|
|
);
|
|
} else {
|
|
use crate::modules::parametric::{DistanceConstraintCommand, DistanceMode};
|
|
let (command_name, mode) = match cmd {
|
|
"DCLINEAR" => ("DCLINEAR", DistanceMode::Linear),
|
|
"DCHORIZONTAL" => ("DCHORIZONTAL", DistanceMode::X),
|
|
"DCVERTICAL" => ("DCVERTICAL", DistanceMode::Y),
|
|
"DCALIGNED" => ("DCALIGNED", DistanceMode::Aligned),
|
|
"DCRADIUS" => ("DCRADIUS", DistanceMode::Radius),
|
|
"DCDIAMETER" => ("DCDIAMETER", DistanceMode::Diameter),
|
|
_ => ("DCONSTRAINT", DistanceMode::Auto),
|
|
};
|
|
match DistanceConstraintCommand::with_mode(
|
|
&self.tabs[i].scene,
|
|
handles[0],
|
|
command_name,
|
|
mode,
|
|
) {
|
|
Some(new_cmd) => {
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
None => self
|
|
.command_line
|
|
.push_output("Select a line or a circle for a distance constraint."),
|
|
}
|
|
}
|
|
}
|
|
|
|
"ACONSTRAINT" | "DCANGULAR" => {
|
|
let handles = self.tabs[i].scene.selected_handles_in_order();
|
|
if handles.is_empty() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
let sel = SelectObjectsCommand::new(cmd);
|
|
self.command_line.push_info(&sel.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(sel));
|
|
} else if handles.len() != 2 {
|
|
self.command_line.push_output(
|
|
"Select exactly two lines (first = reference, second = the one that rotates), then run this constraint again.",
|
|
);
|
|
} else {
|
|
use crate::modules::parametric::AngleConstraintCommand;
|
|
let command_name = if cmd == "DCANGULAR" {
|
|
"DCANGULAR"
|
|
} else {
|
|
"ACONSTRAINT"
|
|
};
|
|
match AngleConstraintCommand::with_name(
|
|
&self.tabs[i].scene,
|
|
handles[0],
|
|
handles[1],
|
|
command_name,
|
|
) {
|
|
Some(new_cmd) => {
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
None => self
|
|
.command_line
|
|
.push_output("Select two lines for an angle constraint."),
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Model commands (3D primitives) ─────────────────────────────
|
|
"CYLINDER" => {
|
|
use crate::modules::model::cylinder_cmd::CylinderCommand;
|
|
let new_cmd = CylinderCommand::new();
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
"BOX" | "WEDGE" | "CONE" | "SPHERE" | "PYRAMID" | "PYR" | "TORUS" => {
|
|
use crate::modules::model::primitive_cmd::PrimitiveCommand;
|
|
let new_cmd = PrimitiveCommand::new(cmd);
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
"SHELL" | "SOLIDEDIT" => {
|
|
use crate::modules::model::shell_cmd::ShellCommand;
|
|
let selected = self.tabs[i]
|
|
.scene
|
|
.selected_handles_in_order()
|
|
.into_iter()
|
|
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
|
|
.filter(|handle| {
|
|
matches!(
|
|
self.tabs[i].scene.document.get_entity(*handle),
|
|
Some(acadrust::EntityType::Solid3D(_))
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let target = (selected.len() == 1).then_some(selected[0]);
|
|
let new_cmd = if cmd == "SHELL" {
|
|
ShellCommand::direct(target)
|
|
} else {
|
|
ShellCommand::solid_edit(target)
|
|
};
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
// ── Solid booleans ─────────────────────────────────────────────
|
|
"UNION" => {
|
|
use crate::modules::model::boolean_cmd::BoolOp;
|
|
if self.union_ready() {
|
|
return Some(self.solid_boolean(BoolOp::Union));
|
|
}
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
let selection = SelectObjectsCommand::plain("UNION", "UNIONAPPLY");
|
|
self.command_line.push_info(&selection.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(selection));
|
|
}
|
|
|
|
"UNIONAPPLY" => {
|
|
use crate::modules::model::boolean_cmd::BoolOp;
|
|
return Some(self.solid_boolean(BoolOp::Union));
|
|
}
|
|
|
|
"INTERSECT" => {
|
|
use crate::modules::model::boolean_cmd::BoolOp;
|
|
if !self.intersect_ready() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
let selection = SelectObjectsCommand::new(cmd);
|
|
self.command_line.push_info(&selection.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(selection));
|
|
} else {
|
|
return Some(self.solid_boolean(BoolOp::Intersect));
|
|
}
|
|
}
|
|
|
|
"SUBTRACT" => {
|
|
use crate::modules::model::boolean_cmd::SubtractCommand;
|
|
let (bases, bases_have_mesh) = {
|
|
let scene = &self.tabs[i].scene;
|
|
let bases = scene
|
|
.selected_handles_in_order()
|
|
.into_iter()
|
|
.filter(|handle| !scene.is_layer_locked(*handle))
|
|
.filter(|handle| {
|
|
matches!(
|
|
scene.document.get_entity(*handle),
|
|
Some(
|
|
acadrust::EntityType::Solid3D(_)
|
|
| acadrust::EntityType::Region(_)
|
|
| acadrust::EntityType::Surface(_)
|
|
| acadrust::EntityType::Mesh(_)
|
|
| acadrust::EntityType::PolygonMesh(_)
|
|
| acadrust::EntityType::PolyfaceMesh(_)
|
|
)
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let bases_have_mesh = bases.iter().any(|handle| {
|
|
matches!(
|
|
scene.document.get_entity(*handle),
|
|
Some(
|
|
acadrust::EntityType::Mesh(_)
|
|
| acadrust::EntityType::PolygonMesh(_)
|
|
| acadrust::EntityType::PolyfaceMesh(_)
|
|
)
|
|
)
|
|
});
|
|
(bases, bases_have_mesh)
|
|
};
|
|
self.tabs[i].scene.deselect_all();
|
|
let subtract = SubtractCommand::new(bases, bases_have_mesh);
|
|
self.command_line.push_info(&subtract.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(subtract));
|
|
}
|
|
|
|
// INTERFERE — non-destructive intersect: solid from the overlap.
|
|
"INTERFERE" => {
|
|
return Some(self.solid_interfere());
|
|
}
|
|
|
|
// FLATSHOT — flatten the selected solid's edges to 2D lines at Z=0.
|
|
"FLATSHOT" => {
|
|
return Some(self.solid_flatshot());
|
|
}
|
|
|
|
// CONVTOSURFACE — convert the selected solid(s) to surface entities.
|
|
"CONVTOSURFACE" => {
|
|
return Some(self.solid_convtosurface());
|
|
}
|
|
|
|
"POLYSOLID" => {
|
|
use crate::modules::model::polysolid_cmd::PolysolidCommand;
|
|
let preselected = self.tabs[i]
|
|
.scene
|
|
.selected_entities()
|
|
.into_iter()
|
|
.find(|(_, entity)| {
|
|
matches!(
|
|
entity,
|
|
acadrust::EntityType::Line(_)
|
|
| acadrust::EntityType::Arc(_)
|
|
| acadrust::EntityType::Circle(_)
|
|
| acadrust::EntityType::Ellipse(_)
|
|
| acadrust::EntityType::LwPolyline(_)
|
|
| acadrust::EntityType::Spline(_)
|
|
)
|
|
})
|
|
.map(|(handle, entity)| (handle, entity.clone()));
|
|
let c = PolysolidCommand::new(preselected);
|
|
self.command_line.push_info(&c.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
|
}
|
|
|
|
// SPLINEFIT — fit a smooth spline through the selected polyline's points.
|
|
"SPLINEFIT" | "FITSPLINE" => {
|
|
return Some(self.fit_spline());
|
|
}
|
|
|
|
// REGION — convert exact closed planar profiles into regions.
|
|
"REGION" | "REG" => {
|
|
if self.tabs[i].scene.selected_entities().is_empty() {
|
|
use crate::modules::draw::select::SelectObjectsCommand;
|
|
let command = SelectObjectsCommand::new("REGION");
|
|
self.command_line.push_info(&command.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
|
return Some(iced::Task::none());
|
|
}
|
|
use acadrust::entities::Region;
|
|
use acadrust::types::Vector3;
|
|
let mut regions = Vec::new();
|
|
let mut sources = Vec::new();
|
|
for (handle, e) in self.tabs[i].scene.selected_entities().iter() {
|
|
if !matches!(e, acadrust::EntityType::Region(_)) {
|
|
let Some((plane, loops, true)) =
|
|
crate::scene::model::presspull_model::profile_geometry(e)
|
|
else {
|
|
continue;
|
|
};
|
|
let Some(body) = cadkernel::brep::planar_region(plane, &loops) else {
|
|
continue;
|
|
};
|
|
let mut region = Region::new();
|
|
region.point_of_reference =
|
|
Vector3::new(plane.origin[0], plane.origin[1], plane.origin[2]);
|
|
region.common.layer = self.tabs[i].active_layer.clone();
|
|
regions.push((region, body));
|
|
sources.push(*handle);
|
|
}
|
|
}
|
|
// Open inputs must share one geometric plane, independently of the UCS.
|
|
let open_curves: Vec<_> = self.tabs[i]
|
|
.scene
|
|
.selected_entities()
|
|
.iter()
|
|
.filter(|(handle, _)| !sources.contains(handle))
|
|
.filter_map(|(_, entity)| crate::entities::curve::entity_curve(entity))
|
|
.collect();
|
|
let open_plane = cadkernel::space::common_curve_plane(&open_curves, 1.0e-6);
|
|
let open_plane_rejected = !open_curves.is_empty() && open_plane.is_none();
|
|
if let Some(plane) = open_plane {
|
|
let working_plane = crate::command::WorkingPlane::new(
|
|
glam::DVec3::from_array(plane.origin),
|
|
glam::DVec3::from_array(plane.x_axis),
|
|
glam::DVec3::from_array(plane.y_axis),
|
|
);
|
|
let selected_handles: rustc_hash::FxHashSet<_> = self.tabs[i]
|
|
.scene
|
|
.selected_entities()
|
|
.iter()
|
|
.map(|(handle, _)| *handle)
|
|
.collect();
|
|
let mut boundary_sources = self.tabs[i]
|
|
.scene
|
|
.boundary_sources_on_plane(working_plane, 1.0e-6);
|
|
boundary_sources.retain(|handle, _| {
|
|
selected_handles.contains(handle) && !sources.contains(handle)
|
|
});
|
|
for ring in crate::scene::boundary_faces(&boundary_sources, 1.0e-6) {
|
|
let paths = crate::scene::exact_hatch_paths(
|
|
std::slice::from_ref(&ring),
|
|
&[true],
|
|
&boundary_sources,
|
|
1.0e-6,
|
|
);
|
|
let Some(path) = paths.first() else {
|
|
continue;
|
|
};
|
|
let Some(curves) = path
|
|
.edges
|
|
.iter()
|
|
.map(crate::entities::hatch::edge_curve)
|
|
.collect::<Option<Vec<_>>>()
|
|
else {
|
|
continue;
|
|
};
|
|
let Some(body) = cadkernel::brep::planar_region(plane, &[curves]) else {
|
|
continue;
|
|
};
|
|
let mut region = Region::new();
|
|
region.point_of_reference =
|
|
Vector3::new(plane.origin[0], plane.origin[1], plane.origin[2]);
|
|
region.common.layer = self.tabs[i].active_layer.clone();
|
|
regions.push((region, body));
|
|
sources.extend(crate::scene::ring_source_handles(&ring, &boundary_sources));
|
|
}
|
|
} else if open_plane_rejected {
|
|
self.command_line.push_error(
|
|
"REGION: open objects must form coplanar, noncollinear boundaries.",
|
|
);
|
|
}
|
|
if regions.is_empty() {
|
|
if !open_plane_rejected {
|
|
self.command_line.push_error(
|
|
"REGION: select closed planar profiles or connected coplanar edges.",
|
|
);
|
|
}
|
|
} else {
|
|
self.push_undo_snapshot(i, "REGION");
|
|
let count = regions.len();
|
|
let mut created = Vec::with_capacity(count);
|
|
for (region, body) in regions {
|
|
let handle = self.add_region_model(region, body);
|
|
if handle.is_null() {
|
|
self.tabs[i].scene.rollback_new_entities(&created);
|
|
self.discard_last_undo_entry(i);
|
|
return Some(iced::Task::none());
|
|
}
|
|
created.push(handle);
|
|
}
|
|
if self.delete_objects != 0 {
|
|
sources.sort_by_key(|handle| handle.value());
|
|
sources.dedup();
|
|
self.tabs[i].scene.erase_entities(&sources);
|
|
self.refresh_properties();
|
|
}
|
|
self.tabs[i].dirty = true;
|
|
self.command_line
|
|
.push_output(crate::tf!("REGION: created {count} region(s).").as_ref());
|
|
}
|
|
}
|
|
|
|
// PYRAMID <radius> <height> [sides] keeps the direct numeric form.
|
|
cmd if cmd.starts_with("PYRAMID ") || cmd.starts_with("PYR ") => {
|
|
let nums: Vec<f64> = cmd
|
|
.split_whitespace()
|
|
.skip(1)
|
|
.filter_map(|s| s.parse::<f64>().ok())
|
|
.collect();
|
|
if nums.len() >= 2 && nums[0] > 0.0 && nums[1] > 0.0 {
|
|
let sides = nums.get(2).map(|s| *s as usize).unwrap_or(4);
|
|
return Some(self.solid_pyramid(nums[0], nums[1], sides));
|
|
}
|
|
self.command_line.push_info(
|
|
crate::t!("Usage: PYRAMID <radius> <height> [sides] (default 4 sides)")
|
|
.as_ref(),
|
|
);
|
|
}
|
|
|
|
// SECTION [X|Y|Z] <value> — draw the cross-section outline of the solid.
|
|
"SECTION" => {
|
|
use crate::command::SelectThenKeywordCommand;
|
|
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
|
|
let c = SelectThenKeywordCommand::new(
|
|
"SECTION",
|
|
"SECTION cutting-plane axis [X / Y / Z]:",
|
|
vec![
|
|
("X", "X", Some("SECTION offset along X:")),
|
|
("Y", "Y", Some("SECTION offset along Y:")),
|
|
("Z", "Z", Some("SECTION offset along Z:")),
|
|
],
|
|
has_sel,
|
|
);
|
|
self.command_line.push_info(&c.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
|
}
|
|
cmd if cmd.starts_with("SECTION ") => {
|
|
let parts: Vec<String> = cmd
|
|
.split_whitespace()
|
|
.skip(1)
|
|
.map(|s| s.to_uppercase())
|
|
.collect();
|
|
let (axis, val_idx) = match parts.first().map(String::as_str) {
|
|
Some("X") => (0, 1),
|
|
Some("Y") => (1, 1),
|
|
Some("Z") => (2, 1),
|
|
_ => (2, 0),
|
|
};
|
|
match parts.get(val_idx).and_then(|s| s.parse::<f64>().ok()) {
|
|
Some(v) => return Some(self.solid_section(axis, v)),
|
|
None => self.command_line.push_info(
|
|
crate::t!(
|
|
"Usage: SECTION [X|Y|Z] <value> (cross-sections the selected solid)"
|
|
)
|
|
.as_ref(),
|
|
),
|
|
}
|
|
}
|
|
|
|
"SECTIONPLANE" => {
|
|
use crate::modules::model::sectionplane_cmd::SectionPlaneCommand;
|
|
let (bounds, next_name) = {
|
|
let scene = &mut self.tabs[i].scene;
|
|
let solid_handles = scene
|
|
.document
|
|
.entities()
|
|
.filter_map(|entity| {
|
|
matches!(
|
|
entity,
|
|
acadrust::EntityType::Solid3D(_)
|
|
| acadrust::EntityType::Surface(_)
|
|
| acadrust::EntityType::Region(_)
|
|
| acadrust::EntityType::Body(_)
|
|
)
|
|
.then_some(entity.common().handle)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
scene.restore_solid_models(&solid_handles);
|
|
let bounds = solid_handles
|
|
.iter()
|
|
.filter_map(|handle| scene.solid_models.get(handle))
|
|
.filter_map(crate::scene::model::solid_model::extent)
|
|
.fold(None::<(glam::DVec3, glam::DVec3)>, |bounds, (low, high)| {
|
|
let low = glam::DVec3::from_array(low);
|
|
let high = glam::DVec3::from_array(high);
|
|
Some(match bounds {
|
|
None => (low, high),
|
|
Some((min, max)) => (min.min(low), max.max(high)),
|
|
})
|
|
});
|
|
let next_name = scene
|
|
.document
|
|
.entities()
|
|
.filter_map(|entity| {
|
|
let acadrust::EntityType::Extended(extended) = entity else {
|
|
return None;
|
|
};
|
|
let acadrust::entities::ExtendedEntityData::SectionObject(data) =
|
|
&extended.data
|
|
else {
|
|
return None;
|
|
};
|
|
data.name
|
|
.strip_prefix("Section Plane (")
|
|
.and_then(|name| name.strip_suffix(')'))
|
|
.and_then(|name| name.parse::<usize>().ok())
|
|
})
|
|
.max()
|
|
.unwrap_or(0)
|
|
+ 1;
|
|
(bounds, next_name)
|
|
};
|
|
let command = SectionPlaneCommand::new(bounds, next_name);
|
|
self.command_line.push_info(&command.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
|
}
|
|
|
|
// 3DALIGN <18 numbers> — align the selected solid by 3 source→3 dest points.
|
|
cmd if cmd == "3DALIGN"
|
|
|| cmd == "ALIGN3D"
|
|
|| cmd.starts_with("3DALIGN ")
|
|
|| cmd.starts_with("ALIGN3D ") =>
|
|
{
|
|
let n: Vec<f64> = cmd
|
|
.split_whitespace()
|
|
.skip(1)
|
|
.filter_map(|s| s.parse::<f64>().ok())
|
|
.collect();
|
|
if n.len() >= 18 {
|
|
let src = [[n[0], n[1], n[2]], [n[3], n[4], n[5]], [n[6], n[7], n[8]]];
|
|
let dst = [
|
|
[n[9], n[10], n[11]],
|
|
[n[12], n[13], n[14]],
|
|
[n[15], n[16], n[17]],
|
|
];
|
|
return Some(self.solid_align3d(src, dst));
|
|
}
|
|
self.command_line.push_info(
|
|
crate::t!("Usage: 3DALIGN <sx1 sy1 sz1 … sx3 sy3 sz3 dx1 dy1 dz1 … dx3 dy3 dz3> (18 numbers: 3 source then 3 destination points)").as_ref(),
|
|
);
|
|
}
|
|
|
|
// 3DMIRROR [X|Y|Z] — add a mirror of the selected solid across a plane.
|
|
"3DMIRROR" | "MIRROR3D" => {
|
|
use crate::command::SelectThenKeywordCommand;
|
|
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
|
|
let c = SelectThenKeywordCommand::new(
|
|
"3DMIRROR",
|
|
"3DMIRROR mirror plane [X / Y / Z]:",
|
|
vec![("X", "X", None), ("Y", "Y", None), ("Z", "Z", None)],
|
|
has_sel,
|
|
);
|
|
self.command_line.push_info(&c.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
|
}
|
|
cmd if cmd.starts_with("3DMIRROR ") || cmd.starts_with("MIRROR3D ") => {
|
|
let parts: Vec<String> = cmd
|
|
.split_whitespace()
|
|
.skip(1)
|
|
.map(|s| s.to_uppercase())
|
|
.collect();
|
|
let axis = match parts.first().map(String::as_str) {
|
|
Some("X") => 0,
|
|
Some("Y") => 1,
|
|
Some("Z") => 2,
|
|
_ => {
|
|
self.command_line.push_info(
|
|
crate::t!("Usage: 3DMIRROR [X|Y|Z] (mirrors the selected solid across that plane)").as_ref(),
|
|
);
|
|
return None;
|
|
}
|
|
};
|
|
return Some(self.solid_mirror3d(axis));
|
|
}
|
|
|
|
// 3DROTATE [X|Y|Z] <angle> — rotate the selected solid about an axis.
|
|
"3DROTATE" | "ROTATE3D" => {
|
|
use crate::command::SelectThenKeywordCommand;
|
|
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
|
|
let c = SelectThenKeywordCommand::new(
|
|
"3DROTATE",
|
|
"3DROTATE rotation axis [X / Y / Z]:",
|
|
vec![
|
|
("X", "X", Some("3DROTATE angle in degrees:")),
|
|
("Y", "Y", Some("3DROTATE angle in degrees:")),
|
|
("Z", "Z", Some("3DROTATE angle in degrees:")),
|
|
],
|
|
has_sel,
|
|
);
|
|
self.command_line.push_info(&c.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
|
}
|
|
cmd if cmd.starts_with("3DROTATE ") || cmd.starts_with("ROTATE3D ") => {
|
|
let parts: Vec<String> = cmd
|
|
.split_whitespace()
|
|
.skip(1)
|
|
.map(|s| s.to_uppercase())
|
|
.collect();
|
|
let axis = match parts.first().map(String::as_str) {
|
|
Some("X") => 0,
|
|
Some("Y") => 1,
|
|
_ => 2,
|
|
};
|
|
let angle: Option<f64> = parts.iter().find_map(|s| s.parse::<f64>().ok());
|
|
match angle {
|
|
Some(a) => return Some(self.solid_rotate3d(axis, a)),
|
|
None => self.command_line.push_info(
|
|
crate::t!("Usage: 3DROTATE [X|Y|Z] <angle> (rotates the selected solid)")
|
|
.as_ref(),
|
|
),
|
|
}
|
|
}
|
|
|
|
"SLICE" | "SL" => {
|
|
use crate::modules::model::slice_cmd::SliceCommand;
|
|
let (targets, centre, radius, view_normal) = {
|
|
let scene = &mut self.tabs[i].scene;
|
|
let mut targets = scene
|
|
.selected_handles_in_order()
|
|
.into_iter()
|
|
.filter(|handle| !scene.is_layer_locked(*handle))
|
|
.filter(|handle| {
|
|
matches!(
|
|
scene.document.get_entity(*handle),
|
|
Some(
|
|
acadrust::EntityType::Solid3D(_)
|
|
| acadrust::EntityType::Surface(_)
|
|
)
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
scene.restore_solid_models(&targets);
|
|
targets.retain(|handle| scene.solid_models.contains_key(handle));
|
|
let bounds = targets
|
|
.iter()
|
|
.filter_map(|handle| {
|
|
crate::scene::model::solid_model::extent(&scene.solid_models[handle])
|
|
})
|
|
.fold(None::<([f64; 3], [f64; 3])>, |bounds, (low, high)| {
|
|
Some(match bounds {
|
|
None => (low, high),
|
|
Some((mut min, mut max)) => {
|
|
for axis in 0..3 {
|
|
min[axis] = min[axis].min(low[axis]);
|
|
max[axis] = max[axis].max(high[axis]);
|
|
}
|
|
(min, max)
|
|
}
|
|
})
|
|
});
|
|
let (centre, radius) =
|
|
bounds.map_or((glam::DVec3::ZERO, 10.0), |(min, max)| {
|
|
let min = glam::DVec3::from_array(min);
|
|
let max = glam::DVec3::from_array(max);
|
|
((min + max) * 0.5, (max - min).length().max(2.0) * 0.65)
|
|
});
|
|
(targets, centre, radius, scene.active_gaze_dir().as_dvec3())
|
|
};
|
|
let c = SliceCommand::new(targets, view_normal, centre, radius);
|
|
self.command_line.push_info(&c.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
|
}
|
|
cmd if cmd.starts_with("SLICE ") || cmd.starts_with("SL ") => {
|
|
let parts: Vec<String> = cmd
|
|
.split_whitespace()
|
|
.skip(1)
|
|
.map(|s| s.to_uppercase())
|
|
.collect();
|
|
let (axis, val_idx) = match parts.first().map(String::as_str) {
|
|
Some("X") => (0, 1),
|
|
Some("Y") => (1, 1),
|
|
Some("Z") => (2, 1),
|
|
_ => (2, 0), // default Z plane
|
|
};
|
|
let value: Option<f64> = parts.get(val_idx).and_then(|s| s.parse().ok());
|
|
let keep_low = !parts.iter().any(|s| s == "TOP");
|
|
match value {
|
|
Some(v) => {
|
|
let (origin, x, normal) = match axis {
|
|
0 => ([v, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0]),
|
|
1 => ([0.0, v, 0.0], [0.0, 0.0, 1.0], [0.0, 1.0, 0.0]),
|
|
_ => ([0.0, 0.0, v], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]),
|
|
};
|
|
let plane = cadkernel::space::Plane::orthonormal(origin, x, normal)
|
|
.expect("fixed world axes define a plane");
|
|
let side = glam::DVec3::from_array(origin)
|
|
+ glam::DVec3::from_array(normal) * if keep_low { -1.0 } else { 1.0 };
|
|
return Some(self.slice_selected(plane, Some(side)));
|
|
}
|
|
None => self.command_line.push_info(
|
|
crate::t!(
|
|
"Usage: SLICE [X|Y|Z] <value> [TOP|BOTTOM] (cuts the selected solid)"
|
|
)
|
|
.as_ref(),
|
|
),
|
|
}
|
|
}
|
|
|
|
// ── Annotate commands ──────────────────────────────────────────
|
|
"TEXT" => {
|
|
use crate::modules::annotate::text::TextCommand;
|
|
let (defaults, styles, annotation_multiplier) = {
|
|
let scene = &self.tabs[i].scene;
|
|
let annotation_multiplier = scene.creation_annotation_multiplier();
|
|
let defaults =
|
|
crate::scene::creation_style::current_text_defaults(&scene.document);
|
|
let styles = scene.document.text_styles.iter().cloned().collect();
|
|
(defaults, styles, annotation_multiplier)
|
|
};
|
|
let new_cmd = TextCommand::with_defaults(defaults, styles, annotation_multiplier);
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
"DDEDIT" => {
|
|
use crate::modules::annotate::ddedit::DdeditCommand;
|
|
// A single text entity already selected opens its in-place
|
|
// editor directly; otherwise prompt for a pick.
|
|
let sel = self.tabs[i].scene.selected_entities();
|
|
let editable = (sel.len() == 1).then(|| sel[0].0).filter(|h| {
|
|
self.tabs[i].scene.document.get_entity(*h).is_some_and(|e| {
|
|
super::super::text_inline::read_text_field(e).is_some()
|
|
|| matches!(e, acadrust::EntityType::Leader(_))
|
|
})
|
|
});
|
|
if let Some(h) = editable {
|
|
return Some(self.begin_text_edit(h));
|
|
}
|
|
if sel.len() == 1 {
|
|
self.command_line
|
|
.push_error(crate::t!("DDEDIT: selected entity is not text.").as_ref());
|
|
} else {
|
|
let cmd = DdeditCommand::new();
|
|
self.command_line.push_info(&cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
}
|
|
}
|
|
|
|
"MTEXT" => {
|
|
use crate::modules::annotate::mtext::MTextCommand;
|
|
let height = crate::scene::creation_style::current_text_defaults(
|
|
&self.tabs[i].scene.document,
|
|
)
|
|
.height;
|
|
let style = self.tabs[i]
|
|
.scene
|
|
.document
|
|
.header
|
|
.current_text_style_name
|
|
.clone();
|
|
let new_cmd = MTextCommand::with_defaults(height, style);
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
"TEXTEDIT" | "TEDIT" => {
|
|
use crate::modules::annotate::textedit::TexteditCommand;
|
|
let mode_str = if self.texteditmode {
|
|
"Single"
|
|
} else {
|
|
"Multiple"
|
|
};
|
|
self.command_line
|
|
.push_output(crate::tf!("Current settings: Edit mode = {}", mode_str).as_ref());
|
|
let new_cmd = TexteditCommand::new(self.texteditmode);
|
|
self.command_line.push_info(&new_cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
|
}
|
|
|
|
"TEXTEDITMODE" => {
|
|
use crate::modules::annotate::textedit::TexteditmodeCommand;
|
|
let cmd = TexteditmodeCommand::new(self.texteditmode);
|
|
self.command_line.push_info(&cmd.prompt());
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
}
|
|
|
|
_ => return None,
|
|
}
|
|
Some(self.finish_dispatch(cmd))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod region_tests {
|
|
use crate::app::OpenCADStudio;
|
|
|
|
#[test]
|
|
fn region_respects_delobj_and_preserves_unconverted_sources() {
|
|
for delete_sources in [false, true] {
|
|
let mut app = OpenCADStudio::new_for_test();
|
|
app.automation_op(r#"{"op":"new"}"#);
|
|
app.automation_op(r#"{"op":"run","cmd":"CIRCLE 5,5 3"}"#);
|
|
app.automation_op(r#"{"op":"run","cmd":"LINE 0,0 10,10"}"#);
|
|
let i = app.active_tab;
|
|
let sources: Vec<_> = app.tabs[i]
|
|
.scene
|
|
.document
|
|
.entities()
|
|
.map(|entity| {
|
|
(
|
|
entity.common().handle,
|
|
matches!(entity, acadrust::EntityType::Circle(_)),
|
|
)
|
|
})
|
|
.collect();
|
|
assert_eq!(sources.len(), 2);
|
|
let handles: Vec<_> = sources.iter().map(|(handle, _)| *handle).collect();
|
|
app.tabs[i].scene.select_entities(&handles);
|
|
app.delete_objects = i16::from(delete_sources);
|
|
|
|
let _ = app.dispatch_command("REGION");
|
|
|
|
for (handle, is_circle) in &sources {
|
|
assert_eq!(
|
|
app.tabs[i].scene.document.get_entity(*handle).is_some(),
|
|
!delete_sources || !is_circle,
|
|
);
|
|
}
|
|
let region_count = app.tabs[i]
|
|
.scene
|
|
.document
|
|
.entities()
|
|
.filter(|entity| matches!(entity, acadrust::EntityType::Region(_)))
|
|
.count();
|
|
assert_eq!(region_count, 1);
|
|
|
|
app.automation_op(r#"{"op":"undo"}"#);
|
|
for (handle, _) in &sources {
|
|
assert!(app.tabs[i].scene.document.get_entity(*handle).is_some());
|
|
}
|
|
assert_eq!(app.tabs[i].scene.document.entities().count(), 2);
|
|
}
|
|
}
|
|
}
|