forked from 1b-agent/opencadstudio-upstream
Merge remote-tracking branch 'origin/main' into feat/viewport-dimensions
# Conflicts: # src/app/command_driver.rs # src/scene/mod.rs
This commit is contained in:
commit
369e0c9fc5
89 files changed
+27311
-11037
No files matched your search
+77
-19
@@ -18,7 +18,7 @@
|
||||
use std::io::{BufRead, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use super::OpenCADStudio;
|
||||
|
||||
@@ -317,8 +317,10 @@ impl OpenCADStudio {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].scene.clear();
|
||||
self.tabs[i].scene.document = doc;
|
||||
self.tabs[i].scene.load_sketch_constraints_from_document();
|
||||
self.tabs[i].scene.load_named_parameters_from_document();
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.load_parametric_constraints_from_document();
|
||||
self.tabs[i].scene.material_base_dir = path_buf.parent().map(PathBuf::from);
|
||||
crate::app::style_ops::ensure_standard_styles(
|
||||
&mut self.tabs[i].scene.document,
|
||||
@@ -1399,7 +1401,8 @@ mod tests {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let stale = acadrust::Handle::from(9999);
|
||||
app.tabs[app.active_tab].scene.solid_models.insert(
|
||||
stale, cadkernel::brep::make::cuboid([0.0; 3], [1.0; 3]).unwrap(),
|
||||
stale,
|
||||
cadkernel::brep::make::cuboid([0.0; 3], [1.0; 3]).unwrap(),
|
||||
);
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"ocs_automation_finalize_test_{}.dxf",
|
||||
@@ -1415,7 +1418,8 @@ mod tests {
|
||||
let mut corrupt = acadrust::entities::Circle::new();
|
||||
corrupt.center = acadrust::types::Vector3::new(1.0, 1.0, 0.0);
|
||||
corrupt.radius = 0.0; // io::is_entity_corrupt rejects a zero-radius circle
|
||||
doc.add_entity(acadrust::EntityType::Circle(corrupt)).unwrap();
|
||||
doc.add_entity(acadrust::EntityType::Circle(corrupt))
|
||||
.unwrap();
|
||||
let bytes = crate::io::save_to_bytes(&doc, "dxf", doc.version)
|
||||
.expect("save a document containing a corrupt entity");
|
||||
std::fs::write(&path, bytes).unwrap();
|
||||
@@ -1423,19 +1427,29 @@ mod tests {
|
||||
let p = path.to_string_lossy().replace('\\', "\\\\");
|
||||
let result = app.automation_op(&format!(r#"{{"op":"open","path":"{p}"}}"#));
|
||||
assert_eq!(result["ok"], true, "{}", result["error"]);
|
||||
assert_eq!(result["total"], 1, "the corrupt circle must not survive the open");
|
||||
assert_eq!(result["purged"], 1, "the purge count must be reported, matching the UI open path's diagnostics");
|
||||
assert_eq!(
|
||||
result["total"], 1,
|
||||
"the corrupt circle must not survive the open"
|
||||
);
|
||||
assert_eq!(
|
||||
result["purged"], 1,
|
||||
"the purge count must be reported, matching the UI open path's diagnostics"
|
||||
);
|
||||
|
||||
let i = app.active_tab;
|
||||
assert!(!app.tabs[i].scene.solid_models.contains_key(&stale));
|
||||
assert_eq!(app.tabs[i].scene.material_base_dir.as_deref(), path.parent());
|
||||
assert_eq!(
|
||||
app.tabs[i].scene.material_base_dir.as_deref(),
|
||||
path.parent()
|
||||
);
|
||||
assert!(
|
||||
app.tabs[i].scene.document.source_path.is_some(),
|
||||
"automation open must run the same finalization as a path-based open, which sets source_path (load_bytes alone never does)"
|
||||
);
|
||||
|
||||
app.tabs[i].scene.solid_models.insert(
|
||||
stale, cadkernel::brep::make::cuboid([0.0; 3], [1.0; 3]).unwrap(),
|
||||
stale,
|
||||
cadkernel::brep::make::cuboid([0.0; 3], [1.0; 3]).unwrap(),
|
||||
);
|
||||
assert_eq!(app.automation_op(r#"{"op":"new"}"#)["ok"], true);
|
||||
assert!(app.tabs[i].scene.solid_models.is_empty());
|
||||
@@ -1523,26 +1537,41 @@ mod tests {
|
||||
// Start LINE
|
||||
let _ = app.update(Message::CommandInput("LINE".to_string()));
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("LINE"));
|
||||
assert_eq!(
|
||||
app.tabs[0].active_cmd.as_ref().map(|c| c.name()),
|
||||
Some("LINE")
|
||||
);
|
||||
|
||||
// Type M2P
|
||||
let _ = app.update(Message::CommandInput("M2P".to_string()));
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("MTP"));
|
||||
assert_eq!(
|
||||
app.tabs[0].active_cmd.as_ref().map(|c| c.name()),
|
||||
Some("MTP")
|
||||
);
|
||||
assert!(app.tabs[0].suspended_cmd.is_some());
|
||||
assert_eq!(app.tabs[0].suspended_cmd.as_ref().map(|c| c.name()), Some("LINE"));
|
||||
assert_eq!(
|
||||
app.tabs[0].suspended_cmd.as_ref().map(|c| c.name()),
|
||||
Some("LINE")
|
||||
);
|
||||
|
||||
// Point 1: 0,0
|
||||
let _ = app.update(Message::CommandInput("0,0".to_string()));
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("MTP"));
|
||||
assert_eq!(
|
||||
app.tabs[0].active_cmd.as_ref().map(|c| c.name()),
|
||||
Some("MTP")
|
||||
);
|
||||
|
||||
// Point 2: 10,20
|
||||
let _ = app.update(Message::CommandInput("10,20".to_string()));
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
|
||||
// MTP should have finished and restored LINE, with midpoint (5, 10, 0)
|
||||
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("LINE"));
|
||||
assert_eq!(
|
||||
app.tabs[0].active_cmd.as_ref().map(|c| c.name()),
|
||||
Some("LINE")
|
||||
);
|
||||
assert!(app.tabs[0].suspended_cmd.is_none());
|
||||
assert_eq!(app.last_point, Some(glam::DVec3::new(5.0, 10.0, 0.0)));
|
||||
|
||||
@@ -1561,7 +1590,10 @@ mod tests {
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
let _ = app.update(Message::SnapOverrideMtp);
|
||||
|
||||
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("MTP"));
|
||||
assert_eq!(
|
||||
app.tabs[0].active_cmd.as_ref().map(|c| c.name()),
|
||||
Some("MTP")
|
||||
);
|
||||
assert_eq!(
|
||||
app.tabs[0].suspended_cmd.as_ref().map(|c| c.name()),
|
||||
Some("LINE")
|
||||
@@ -1577,17 +1609,26 @@ mod tests {
|
||||
// Start LINE
|
||||
let _ = app.update(Message::CommandInput("LINE".to_string()));
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("LINE"));
|
||||
assert_eq!(
|
||||
app.tabs[0].active_cmd.as_ref().map(|c| c.name()),
|
||||
Some("LINE")
|
||||
);
|
||||
|
||||
// Type MTP
|
||||
let _ = app.update(Message::CommandInput("MTP".to_string()));
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("MTP"));
|
||||
assert_eq!(
|
||||
app.tabs[0].active_cmd.as_ref().map(|c| c.name()),
|
||||
Some("MTP")
|
||||
);
|
||||
|
||||
// Escape during MTP
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
// Parent LINE must be restored, not cancelled!
|
||||
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("LINE"));
|
||||
assert_eq!(
|
||||
app.tabs[0].active_cmd.as_ref().map(|c| c.name()),
|
||||
Some("LINE")
|
||||
);
|
||||
assert!(app.tabs[0].suspended_cmd.is_none());
|
||||
|
||||
// Escape again cancels LINE
|
||||
@@ -1605,7 +1646,10 @@ mod tests {
|
||||
// Start LINE
|
||||
let _ = app.update(Message::CommandInput("LINE".to_string()));
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("LINE"));
|
||||
assert_eq!(
|
||||
app.tabs[0].active_cmd.as_ref().map(|c| c.name()),
|
||||
Some("LINE")
|
||||
);
|
||||
|
||||
// Simulate typing 'm', '2', 'p' character by character
|
||||
let _ = app.update(Message::CommandAppendChar("m".to_string()));
|
||||
@@ -1620,6 +1664,20 @@ mod tests {
|
||||
|
||||
// Submit triggers MTP
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("MTP"));
|
||||
assert_eq!(
|
||||
app.tabs[0].active_cmd.as_ref().map(|c| c.name()),
|
||||
Some("MTP")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_open_color_dropdown() {
|
||||
use crate::app::Message;
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
let wid = app.main_window.unwrap_or_else(iced::window::Id::unique);
|
||||
let _ = app.view(wid);
|
||||
let _ = app.update(Message::ToggleRibbonDropdown("PROP_COLOR".to_string()));
|
||||
let _ = app.view(wid);
|
||||
}
|
||||
}
|
||||
+1867
-786
File diff suppressed because it is too large.
Load diff
+358
-221
@@ -29,12 +29,7 @@ impl OpenCADStudio {
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let cmd_obj = MlineCommand::with_styles(
|
||||
styles,
|
||||
style_name,
|
||||
scale,
|
||||
justification,
|
||||
);
|
||||
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));
|
||||
}
|
||||
@@ -133,25 +128,29 @@ impl OpenCADStudio {
|
||||
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());
|
||||
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());
|
||||
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());
|
||||
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() {
|
||||
@@ -179,13 +178,17 @@ impl OpenCADStudio {
|
||||
}
|
||||
if changed > 0 {
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(crate::tf!(
|
||||
self.command_line.push_output(
|
||||
crate::tf!(
|
||||
"ATTEDIT: updated {changed} attribute(s) [{tag_up}] = {new_val}."
|
||||
).as_ref());
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
} else {
|
||||
self.command_line.push_error(crate::tf!(
|
||||
"ATTEDIT: tag '{tag_up}' not found in selection."
|
||||
).as_ref());
|
||||
self.command_line.push_error(
|
||||
crate::tf!("ATTEDIT: tag '{tag_up}' not found in selection.")
|
||||
.as_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,9 +246,10 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
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_output(
|
||||
crate::tf!("ATTDISP {sub}: {count} attribute definition(s) updated.")
|
||||
.as_ref(),
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
self.command_line
|
||||
@@ -388,8 +392,10 @@ impl OpenCADStudio {
|
||||
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());
|
||||
self.command_line.push_info(
|
||||
crate::t!("ARC Continue No previous line or arc to continue.")
|
||||
.as_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -528,16 +534,26 @@ impl OpenCADStudio {
|
||||
if count > 0 {
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
self.command_line
|
||||
.push_output(&crate::tf!("CENTERRESET: {count} center object(s) updated."));
|
||||
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();
|
||||
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]);
|
||||
@@ -546,25 +562,35 @@ impl OpenCADStudio {
|
||||
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);
|
||||
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."));
|
||||
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);
|
||||
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."));
|
||||
self.command_line.push_output(&crate::tf!(
|
||||
"CENTERDISASSOCIATE: {count} center object(s) detached."
|
||||
));
|
||||
}
|
||||
|
||||
"DIMCENTER" => {
|
||||
@@ -602,15 +628,20 @@ impl OpenCADStudio {
|
||||
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()
|
||||
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])))
|
||||
.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(),
|
||||
)));
|
||||
return Some(self.apply_cmd_result(
|
||||
crate::command::CmdResult::ReplaceMany(replacements, Vec::new()),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -766,9 +797,7 @@ impl OpenCADStudio {
|
||||
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 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
|
||||
@@ -776,20 +805,14 @@ impl OpenCADStudio {
|
||||
.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());
|
||||
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);
|
||||
@@ -804,11 +827,11 @@ impl OpenCADStudio {
|
||||
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,
|
||||
)
|
||||
});
|
||||
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,
|
||||
@@ -822,15 +845,28 @@ impl OpenCADStudio {
|
||||
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());
|
||||
)
|
||||
.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());
|
||||
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());
|
||||
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));
|
||||
}
|
||||
@@ -854,9 +890,7 @@ impl OpenCADStudio {
|
||||
} else {
|
||||
crate::command::WorkingPlane::default()
|
||||
};
|
||||
let sources = self.tabs[i]
|
||||
.scene
|
||||
.boundary_sources_on_plane(plane, 1.0e-6);
|
||||
let sources = self.tabs[i].scene.boundary_sources_on_plane(plane, 1.0e-6);
|
||||
let selected = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
@@ -895,7 +929,8 @@ impl OpenCADStudio {
|
||||
SplineCommand::control_vertices()
|
||||
} else {
|
||||
SplineCommand::new()
|
||||
}.with_document(&self.tabs[i].scene.document);
|
||||
}
|
||||
.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));
|
||||
}
|
||||
@@ -935,8 +970,7 @@ impl OpenCADStudio {
|
||||
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 (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());
|
||||
@@ -986,10 +1020,10 @@ impl OpenCADStudio {
|
||||
self.command_line.push_info(&sel.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(sel));
|
||||
} else {
|
||||
let scope = self.tabs[i].current_sketch_scope();
|
||||
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_sketch_constraint_visibility(
|
||||
let count = self.tabs[i].scene.set_parametric_constraint_visibility(
|
||||
scope,
|
||||
Some(&handles),
|
||||
dimensional,
|
||||
@@ -1002,19 +1036,19 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
"GCRESET" => {
|
||||
let scope = self.tabs[i].current_sketch_scope();
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
let count = self.tabs[i]
|
||||
.scene
|
||||
.set_sketch_constraint_visibility(scope, None, false, true);
|
||||
.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_sketch_scope();
|
||||
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_sketch_constraint_visibility(
|
||||
let count = self.tabs[i].scene.set_parametric_constraint_visibility(
|
||||
scope,
|
||||
None,
|
||||
dimensional,
|
||||
@@ -1032,11 +1066,8 @@ impl OpenCADStudio {
|
||||
self.command_line.push_info(&sel.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(sel));
|
||||
} else {
|
||||
let scope = self.tabs[i].current_sketch_scope();
|
||||
let Some(before) = self.tabs[i]
|
||||
.scene
|
||||
.sketch_constraint_set(scope)
|
||||
.cloned()
|
||||
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;
|
||||
@@ -1061,8 +1092,8 @@ impl OpenCADStudio {
|
||||
let pending = self.begin_undo(i, "Delete constraints", handles.len(), true);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_sketch_constraints_before(scope, before);
|
||||
let set = self.tabs[i].scene.sketch_constraint_set_mut(scope);
|
||||
.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);
|
||||
}
|
||||
@@ -1082,53 +1113,86 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
"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::new(cmd);
|
||||
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_sketch_scope();
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
let inferred = self.tabs[i]
|
||||
.scene
|
||||
.inferred_sketch_constraints(scope, &handles);
|
||||
.inferred_parametric_constraints(
|
||||
scope,
|
||||
&handles,
|
||||
&self.auto_constrain_settings,
|
||||
);
|
||||
if inferred.is_empty() {
|
||||
self.command_line
|
||||
.push_output("No supported geometric relations were found.");
|
||||
return None;
|
||||
}
|
||||
let before = self.tabs[i]
|
||||
.scene
|
||||
.sketch_constraint_set(scope)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
crate::scene::sketch_constraints::SketchConstraintSet::new(scope)
|
||||
});
|
||||
let pending = self.begin_undo(i, "Auto constrain", handles.len(), true);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_sketch_constraints_before(scope, before);
|
||||
let count = inferred.len();
|
||||
for (kind, refs) in inferred {
|
||||
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
|
||||
.sketch_constraint_set_mut(scope)
|
||||
.add(kind, refs, None);
|
||||
}
|
||||
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) applied.", count).as_str());
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
.record_undo_parametric_constraints_before(scope, before);
|
||||
let count = inferred.len();
|
||||
for (kind, refs) in inferred {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set_mut(scope)
|
||||
.add(kind, refs, None);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1142,14 +1206,13 @@ impl OpenCADStudio {
|
||||
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.",
|
||||
);
|
||||
self.command_line
|
||||
.push_output("Select one open spline and one open target curve.");
|
||||
return None;
|
||||
};
|
||||
use crate::command::CmdResult;
|
||||
use crate::scene::sketch_constraints::ConstraintKind;
|
||||
return Some(self.apply_cmd_result(CmdResult::AddSketchConstraint {
|
||||
use crate::scene::parametric_constraints::ConstraintKind;
|
||||
return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint {
|
||||
kind: ConstraintKind::Smooth,
|
||||
refs,
|
||||
driving_param: None,
|
||||
@@ -1166,7 +1229,7 @@ impl OpenCADStudio {
|
||||
self.command_line.push_info(&sel.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(sel));
|
||||
} else {
|
||||
let scope = self.tabs[i].current_sketch_scope();
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
let conversions: Vec<_> = handles
|
||||
.iter()
|
||||
.copied()
|
||||
@@ -1179,23 +1242,24 @@ impl OpenCADStudio {
|
||||
)?;
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.validate_sketch_constraint(kind, &refs, Some(&value))
|
||||
.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.",
|
||||
);
|
||||
self.command_line
|
||||
.push_output("No supported associative dimensions were selected.");
|
||||
return None;
|
||||
}
|
||||
let before = self.tabs[i]
|
||||
.scene
|
||||
.sketch_constraint_set(scope)
|
||||
.parametric_constraint_set(scope)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
crate::scene::sketch_constraints::SketchConstraintSet::new(scope)
|
||||
crate::scene::parametric_constraints::ParametricConstraintSet::new(
|
||||
scope,
|
||||
)
|
||||
});
|
||||
let mut touched: Vec<_> = conversions
|
||||
.iter()
|
||||
@@ -1215,12 +1279,13 @@ impl OpenCADStudio {
|
||||
);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_sketch_constraints_before(scope, before);
|
||||
.record_undo_parametric_constraints_before(scope, before);
|
||||
for (_, kind, refs, value) in conversions {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.sketch_constraint_set_mut(scope)
|
||||
.add(kind, refs, Some(value));
|
||||
self.tabs[i].scene.parametric_constraint_set_mut(scope).add(
|
||||
kind,
|
||||
refs,
|
||||
Some(value),
|
||||
);
|
||||
}
|
||||
self.tabs[i].scene.erase_entities(&dimensions);
|
||||
let changes: Vec<_> = touched
|
||||
@@ -1251,28 +1316,35 @@ impl OpenCADStudio {
|
||||
.push_output("Select exactly one entity, then run this constraint again.");
|
||||
} else {
|
||||
use crate::command::CmdResult;
|
||||
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
|
||||
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::AddSketchConstraint {
|
||||
return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint {
|
||||
kind,
|
||||
refs: vec![SketchRef::whole(handles[0])],
|
||||
refs: vec![ParametricRef::whole(handles[0])],
|
||||
driving_param: None,
|
||||
label,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
"CCONSTRAINT" => {
|
||||
"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();
|
||||
@@ -1280,7 +1352,8 @@ impl OpenCADStudio {
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
"PCONSTRAINT" | "QCONSTRAINT" | "ECONSTRAINT" | "TCONSTRAINT" | "LCONSTRAINT" | "NRCONSTRAINT" => {
|
||||
"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;
|
||||
@@ -1293,18 +1366,23 @@ impl OpenCADStudio {
|
||||
);
|
||||
} else {
|
||||
use crate::command::CmdResult;
|
||||
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
|
||||
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef};
|
||||
let (kind, label) = match cmd {
|
||||
"PCONSTRAINT" => (ConstraintKind::Parallel, "Parallel constraint"),
|
||||
"QCONSTRAINT" => (ConstraintKind::Perpendicular, "Perpendicular 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::AddSketchConstraint {
|
||||
return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint {
|
||||
kind,
|
||||
refs: vec![SketchRef::whole(handles[0]), SketchRef::whole(handles[1])],
|
||||
refs: vec![
|
||||
ParametricRef::whole(handles[0]),
|
||||
ParametricRef::whole(handles[1]),
|
||||
],
|
||||
driving_param: None,
|
||||
label,
|
||||
}));
|
||||
@@ -1319,13 +1397,18 @@ impl OpenCADStudio {
|
||||
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.");
|
||||
self.command_line.push_output(
|
||||
"Select exactly two circles/arcs, then run this constraint again.",
|
||||
);
|
||||
} else {
|
||||
use crate::command::CmdResult;
|
||||
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
|
||||
return Some(self.apply_cmd_result(CmdResult::AddSketchConstraint {
|
||||
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef};
|
||||
return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint {
|
||||
kind: ConstraintKind::Concentric,
|
||||
refs: vec![SketchRef::center(handles[0]), SketchRef::center(handles[1])],
|
||||
refs: vec![
|
||||
ParametricRef::center(handles[0]),
|
||||
ParametricRef::center(handles[1]),
|
||||
],
|
||||
driving_param: None,
|
||||
label: "Concentric constraint",
|
||||
}));
|
||||
@@ -1345,10 +1428,14 @@ impl OpenCADStudio {
|
||||
);
|
||||
} else {
|
||||
use crate::command::CmdResult;
|
||||
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
|
||||
return Some(self.apply_cmd_result(CmdResult::AddSketchConstraint {
|
||||
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef};
|
||||
return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint {
|
||||
kind: ConstraintKind::Symmetric,
|
||||
refs: vec![SketchRef::center(handles[0]), SketchRef::center(handles[1]), SketchRef::whole(handles[2])],
|
||||
refs: vec![
|
||||
ParametricRef::center(handles[0]),
|
||||
ParametricRef::center(handles[1]),
|
||||
ParametricRef::whole(handles[2]),
|
||||
],
|
||||
driving_param: None,
|
||||
label: "Symmetric constraint",
|
||||
}));
|
||||
@@ -1368,13 +1455,26 @@ impl OpenCADStudio {
|
||||
);
|
||||
} else {
|
||||
use crate::modules::parametric::PointOnEntityConstraintCommand;
|
||||
use crate::scene::sketch_constraints::ConstraintKind;
|
||||
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"),
|
||||
"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);
|
||||
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));
|
||||
}
|
||||
@@ -1389,8 +1489,9 @@ impl OpenCADStudio {
|
||||
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.");
|
||||
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 {
|
||||
@@ -1412,7 +1513,9 @@ impl OpenCADStudio {
|
||||
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."),
|
||||
None => self
|
||||
.command_line
|
||||
.push_output("Select a line or a circle for a distance constraint."),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1445,7 +1548,9 @@ impl OpenCADStudio {
|
||||
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."),
|
||||
None => self
|
||||
.command_line
|
||||
.push_output("Select two lines for an angle constraint."),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1457,8 +1562,7 @@ impl OpenCADStudio {
|
||||
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" => {
|
||||
"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());
|
||||
@@ -1625,54 +1729,69 @@ impl OpenCADStudio {
|
||||
continue;
|
||||
};
|
||||
let mut region = Region::new();
|
||||
region.point_of_reference = Vector3::new(
|
||||
plane.origin[0],
|
||||
plane.origin[1],
|
||||
plane.origin[2],
|
||||
);
|
||||
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()
|
||||
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 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 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));
|
||||
}
|
||||
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.",
|
||||
@@ -1720,8 +1839,10 @@ impl OpenCADStudio {
|
||||
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());
|
||||
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.
|
||||
@@ -1756,7 +1877,10 @@ impl OpenCADStudio {
|
||||
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(),
|
||||
crate::t!(
|
||||
"Usage: SECTION [X|Y|Z] <value> (cross-sections the selected solid)"
|
||||
)
|
||||
.as_ref(),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -1909,7 +2033,8 @@ impl OpenCADStudio {
|
||||
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(),
|
||||
crate::t!("Usage: 3DROTATE [X|Y|Z] <angle> (rotates the selected solid)")
|
||||
.as_ref(),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -1951,11 +2076,12 @@ impl OpenCADStudio {
|
||||
}
|
||||
})
|
||||
});
|
||||
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)
|
||||
});
|
||||
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);
|
||||
@@ -1990,7 +2116,10 @@ impl OpenCADStudio {
|
||||
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(),
|
||||
crate::t!(
|
||||
"Usage: SLICE [X|Y|Z] <value> [TOP|BOTTOM] (cuts the selected solid)"
|
||||
)
|
||||
.as_ref(),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -2006,8 +2135,7 @@ impl OpenCADStudio {
|
||||
let styles = scene.document.text_styles.iter().cloned().collect();
|
||||
(defaults, styles, annotation_multiplier)
|
||||
};
|
||||
let new_cmd =
|
||||
TextCommand::with_defaults(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));
|
||||
}
|
||||
@@ -2092,9 +2220,15 @@ mod region_tests {
|
||||
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()
|
||||
let sources: Vec<_> = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.map(|entity| {
|
||||
(entity.common().handle, matches!(entity, acadrust::EntityType::Circle(_)))
|
||||
(
|
||||
entity.common().handle,
|
||||
matches!(entity, acadrust::EntityType::Circle(_)),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(sources.len(), 2);
|
||||
@@ -2110,7 +2244,10 @@ mod region_tests {
|
||||
!delete_sources || !is_circle,
|
||||
);
|
||||
}
|
||||
let region_count = app.tabs[i].scene.document.entities()
|
||||
let region_count = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.filter(|entity| matches!(entity, acadrust::EntityType::Region(_)))
|
||||
.count();
|
||||
assert_eq!(region_count, 1);
|
||||
|
||||
@@ -1,6 +1,82 @@
|
||||
use super::*;
|
||||
|
||||
impl OpenCADStudio {
|
||||
/// Align every selected object or complete group by one edge or center.
|
||||
fn align_selected_bounds(&mut self, i: usize, command: &str) {
|
||||
use crate::command::EntityTransform;
|
||||
use cadkernel::space::BoundsAlignment;
|
||||
use glam::DVec3;
|
||||
|
||||
let (alignment, label) = match command {
|
||||
"ALIGNLEFT" => (BoundsAlignment::Left, "Align Left"),
|
||||
"ALIGNHCENTER" => (BoundsAlignment::HorizontalCenter, "Align Horizontal Centers"),
|
||||
"ALIGNRIGHT" => (BoundsAlignment::Right, "Align Right"),
|
||||
"ALIGNTOP" => (BoundsAlignment::Top, "Align Top"),
|
||||
"ALIGNVCENTER" => (BoundsAlignment::VerticalCenter, "Align Vertical Centers"),
|
||||
"ALIGNBOTTOM" => (BoundsAlignment::Bottom, "Align Bottom"),
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let handles: Vec<_> = self.tabs[i]
|
||||
.scene
|
||||
.selected_handles_in_order()
|
||||
.into_iter()
|
||||
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
|
||||
.collect();
|
||||
if handles.len() < 2 {
|
||||
self.command_line
|
||||
.push_info(crate::t!("Select objects").as_ref());
|
||||
return;
|
||||
}
|
||||
|
||||
let bounds: Vec<_> = self.tabs[i]
|
||||
.scene
|
||||
.selected_object_units(&handles)
|
||||
.into_iter()
|
||||
.filter_map(|unit| {
|
||||
let mut min_x = f64::INFINITY;
|
||||
let mut min_y = f64::INFINITY;
|
||||
let mut max_x = f64::NEG_INFINITY;
|
||||
let mut max_y = f64::NEG_INFINITY;
|
||||
for handle in &unit {
|
||||
let entity = self.tabs[i].scene.document.get_entity(*handle)?;
|
||||
let bb = entity.as_entity().bounding_box();
|
||||
min_x = min_x.min(bb.min.x);
|
||||
min_y = min_y.min(bb.min.y);
|
||||
max_x = max_x.max(bb.max.x);
|
||||
max_y = max_y.max(bb.max.y);
|
||||
}
|
||||
let bounds = [min_x, min_y, max_x, max_y];
|
||||
bounds
|
||||
.iter()
|
||||
.all(|value| value.is_finite())
|
||||
.then_some((unit, bounds))
|
||||
})
|
||||
.collect();
|
||||
if bounds.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let extents: Vec<_> = bounds.iter().map(|(_, bounds)| *bounds).collect();
|
||||
let offsets = cadkernel::space::align_aabbs_2d(&extents, alignment)
|
||||
.expect("validated at least two finite bounds");
|
||||
let pending = self.begin_undo(i, label, handles.len(), true);
|
||||
for ((unit, _), [dx, dy]) in bounds.iter().zip(offsets) {
|
||||
self.tabs[i].scene.transform_entities(
|
||||
unit,
|
||||
&EntityTransform::Translate(DVec3::new(dx, dy, 0.0)),
|
||||
);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
if let Some(pending) = pending {
|
||||
self.commit_undo_delta(i, pending);
|
||||
}
|
||||
self.command_line.push_output(
|
||||
crate::tf!("{command}: aligned {} object(s).", bounds.len()).as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn dispatch_inquiry(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||
match cmd {
|
||||
"3DORBIT" => {
|
||||
@@ -980,6 +1056,11 @@ impl OpenCADStudio {
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
|
||||
"ALIGNLEFT" | "ALIGNHCENTER" | "ALIGNRIGHT" | "ALIGNTOP" | "ALIGNVCENTER"
|
||||
| "ALIGNBOTTOM" => {
|
||||
self.align_selected_bounds(i, cmd);
|
||||
}
|
||||
|
||||
"LENGTHEN" => {
|
||||
use crate::modules::draw::modify::lengthen::LengthenCommand;
|
||||
let cmd = LengthenCommand::new();
|
||||
@@ -2211,3 +2292,97 @@ mod flatten_tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod align_selected_bounds_tests {
|
||||
use super::*;
|
||||
use acadrust::entities::{EntityType, Line};
|
||||
use acadrust::types::Vector3;
|
||||
|
||||
fn add_line(app: &mut OpenCADStudio, x1: f64, y1: f64, x2: f64, y2: f64) -> acadrust::Handle {
|
||||
app.tabs[app.active_tab]
|
||||
.scene
|
||||
.add_entity(EntityType::Line(Line::from_points(
|
||||
Vector3::new(x1, y1, 0.0),
|
||||
Vector3::new(x2, y2, 0.0),
|
||||
)))
|
||||
}
|
||||
|
||||
fn line_start_x(app: &OpenCADStudio, handle: acadrust::Handle) -> f64 {
|
||||
match app.tabs[app.active_tab].scene.document.get_entity(handle) {
|
||||
Some(EntityType::Line(l)) => l.start.x.min(l.end.x),
|
||||
other => panic!("expected a Line, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn line_max_y(app: &OpenCADStudio, handle: acadrust::Handle) -> f64 {
|
||||
match app.tabs[app.active_tab].scene.document.get_entity(handle) {
|
||||
Some(EntityType::Line(l)) => l.start.y.max(l.end.y),
|
||||
other => panic!("expected a Line, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// A single selected object has no "other object" to align to — refuse
|
||||
/// with an informational message rather than silently moving nothing
|
||||
/// (or panicking on the empty-bounds case).
|
||||
#[test]
|
||||
fn refuses_with_fewer_than_two_selected_objects() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let _ = app.automation_op(r#"{"op":"new"}"#);
|
||||
let a = add_line(&mut app, 0.0, 0.0, 2.0, 1.0);
|
||||
app.tabs[app.active_tab].scene.select_entities(&[a]);
|
||||
let i = app.active_tab;
|
||||
|
||||
app.dispatch_inquiry("ALIGNLEFT", i);
|
||||
assert_eq!(line_start_x(&app, a), 0.0, "the lone object must not move");
|
||||
}
|
||||
|
||||
/// ALIGNLEFT moves every selected object's left edge to the leftmost
|
||||
/// edge already present in the selection; ALIGNTOP does the same for
|
||||
/// the topmost edge. Both are undoable as one step.
|
||||
#[test]
|
||||
fn aligns_left_and_top_edges_and_is_undoable() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let _ = app.automation_op(r#"{"op":"new"}"#);
|
||||
let left = add_line(&mut app, 0.0, 0.0, 1.0, 1.0); // leftmost & topmost already
|
||||
let right = add_line(&mut app, 5.0, -3.0, 8.0, -2.0); // further right and lower
|
||||
app.tabs[app.active_tab].scene.select_entities(&[left, right]);
|
||||
let i = app.active_tab;
|
||||
|
||||
app.dispatch_inquiry("ALIGNLEFT", i);
|
||||
assert_eq!(line_start_x(&app, left), 0.0, "the already-leftmost object must not move");
|
||||
assert_eq!(line_start_x(&app, right), 0.0, "the other object's left edge should meet it");
|
||||
|
||||
app.dispatch_inquiry("ALIGNTOP", i);
|
||||
assert_eq!(line_max_y(&app, left), 1.0, "the already-topmost object must not move");
|
||||
assert_eq!(line_max_y(&app, right), 1.0, "the other object's top edge should meet it");
|
||||
|
||||
app.undo_steps(1); // undo ALIGNTOP
|
||||
assert_eq!(line_max_y(&app, right), -2.0, "undo should restore the pre-ALIGNTOP position");
|
||||
app.undo_steps(1); // undo ALIGNLEFT
|
||||
assert_eq!(line_start_x(&app, right), 5.0, "undo should restore the original geometry");
|
||||
}
|
||||
|
||||
/// A complete, fully-selected group (`Scene::selected_object_units`)
|
||||
/// moves as one rigid unit instead of each member line sliding to meet
|
||||
/// the *other* members of its own group.
|
||||
#[test]
|
||||
fn a_complete_group_aligns_as_one_rigid_unit() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let _ = app.automation_op(r#"{"op":"new"}"#);
|
||||
let a = add_line(&mut app, 5.0, 0.0, 6.0, 1.0);
|
||||
let b = add_line(&mut app, 7.0, 0.0, 8.0, 1.0);
|
||||
let other = add_line(&mut app, 0.0, 0.0, 1.0, 1.0);
|
||||
app.tabs[app.active_tab].scene.create_group("pair".to_string(), vec![a, b]);
|
||||
app.tabs[app.active_tab].scene.select_entities(&[a, b, other]);
|
||||
let i = app.active_tab;
|
||||
|
||||
app.dispatch_inquiry("ALIGNLEFT", i);
|
||||
// The group's own left edge (a's, at x=5) moves to meet `other`'s
|
||||
// left edge (x=0): a shifts by -5, and b — sharing the same
|
||||
// transform — must shift by exactly the same amount, not collapse
|
||||
// onto `a`.
|
||||
assert_eq!(line_start_x(&app, a), 0.0);
|
||||
assert_eq!(line_start_x(&app, b), 2.0, "b must keep its offset from a, not collapse onto it");
|
||||
}
|
||||
}
|
||||
@@ -1046,6 +1046,9 @@ impl OpenCADStudio {
|
||||
| "GRIPHOT"
|
||||
| "GRIPHOVER"
|
||||
| "GRIPOBJLIMIT"
|
||||
| "CONSTRAINTSOLVEMODE"
|
||||
| "CONSTRAINTINFER"
|
||||
| "CONSTRAINTBARDISPLAY"
|
||||
) =>
|
||||
{
|
||||
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
|
||||
@@ -1068,7 +1071,7 @@ impl OpenCADStudio {
|
||||
let value = it.next().map(|s| s.trim().to_string());
|
||||
if name.is_empty() || name == "?" {
|
||||
self.command_line.push_info(
|
||||
crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(),
|
||||
crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(),
|
||||
);
|
||||
} else {
|
||||
if name == "CETRANSPARENCY" {
|
||||
@@ -1262,6 +1265,50 @@ impl OpenCADStudio {
|
||||
}
|
||||
return Some(self.finish_dispatch(cmd));
|
||||
}
|
||||
if matches!(
|
||||
name.as_str(),
|
||||
"CONSTRAINTSOLVEMODE" | "CONSTRAINTINFER" | "CONSTRAINTBARDISPLAY"
|
||||
) {
|
||||
let current = match name.as_str() {
|
||||
"CONSTRAINTSOLVEMODE" => i16::from(self.constraint_solve_mode),
|
||||
"CONSTRAINTINFER" => i16::from(self.constraint_infer),
|
||||
"CONSTRAINTBARDISPLAY" => self.constraint_bar_display,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let maximum = if name == "CONSTRAINTBARDISPLAY" { 3 } else { 1 };
|
||||
if let Some(value) = &value {
|
||||
match value
|
||||
.parse::<i16>()
|
||||
.ok()
|
||||
.filter(|value| (0..=maximum).contains(value))
|
||||
{
|
||||
Some(mode) => {
|
||||
match name.as_str() {
|
||||
"CONSTRAINTSOLVEMODE" => {
|
||||
self.constraint_solve_mode = mode != 0
|
||||
}
|
||||
"CONSTRAINTINFER" => self.constraint_infer = mode != 0,
|
||||
"CONSTRAINTBARDISPLAY" => {
|
||||
self.constraint_bar_display = mode
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
self.persist_settings_if_changed();
|
||||
self.command_line
|
||||
.push_output(&crate::tf!("{name} = {mode}"));
|
||||
}
|
||||
None => self.command_line.push_error(&crate::tf!(
|
||||
"{name}: expected an integer from 0 to {maximum}."
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"Enter new value for {name} <{current}>:"
|
||||
).as_ref());
|
||||
self.pending_setvar = Some(name.clone());
|
||||
}
|
||||
return Some(self.finish_dispatch(cmd));
|
||||
}
|
||||
if matches!(name.as_str(), "DONUTID" | "DONUTOD") {
|
||||
let current = if name == "DONUTID" {
|
||||
crate::modules::draw::defaults::get_donut_inner_diameter()
|
||||
|
||||
@@ -52,6 +52,7 @@ fn listen(sender: mpsc::Sender<Envelope>) -> std::io::Result<()> {
|
||||
}
|
||||
let mut file = options.open(&path)?;
|
||||
write!(file, "{descriptor}")?;
|
||||
drop(file);
|
||||
let clients = Arc::new(AtomicUsize::new(0));
|
||||
for stream in listener.incoming().flatten() {
|
||||
if clients.fetch_add(1, Ordering::SeqCst) >= 8 {
|
||||
|
||||
+59
-65
@@ -6,11 +6,11 @@ use crate::scene::pick::grip::GripEdit;
|
||||
use crate::scene::GripDef;
|
||||
use crate::scene::{ObjectIsolationState, Scene};
|
||||
use crate::snap::SnapResult;
|
||||
use crate::t;
|
||||
use crate::ui::{LayerPanel, PropertiesPanel};
|
||||
use acadrust::tables::{normalize_name, Ucs};
|
||||
use acadrust::{CadDocument, EntityType, Handle};
|
||||
use iced;
|
||||
use crate::t;
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
@@ -240,14 +240,18 @@ impl DocumentTab {
|
||||
.and_then(|index| self.block_edits.get_mut(index))
|
||||
}
|
||||
|
||||
/// The [`crate::scene::sketch_constraints::SketchScope`] a new persistent
|
||||
/// The [`crate::scene::parametric_constraints::ParametricScope`] a new persistent
|
||||
/// constraint should attach to right now — whatever's actually being
|
||||
/// edited: the open block definition if a BEDIT session is active, model
|
||||
/// space otherwise.
|
||||
pub(super) fn current_sketch_scope(&self) -> crate::scene::sketch_constraints::SketchScope {
|
||||
pub(super) fn current_parametric_scope(
|
||||
&self,
|
||||
) -> crate::scene::parametric_constraints::ParametricScope {
|
||||
match self.active_block_edit_session() {
|
||||
Some(session) => crate::scene::sketch_constraints::SketchScope::Block(session.br_handle),
|
||||
None => crate::scene::sketch_constraints::SketchScope::ModelSpace,
|
||||
Some(session) => {
|
||||
crate::scene::parametric_constraints::ParametricScope::Block(session.br_handle)
|
||||
}
|
||||
None => crate::scene::parametric_constraints::ParametricScope::ModelSpace,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,8 +295,7 @@ impl DocumentTab {
|
||||
u.origin = h.model_space_ucs_origin;
|
||||
u.x_axis = h.model_space_ucs_x_axis;
|
||||
u.y_axis = h.model_space_ucs_y_axis;
|
||||
if h.model_space_ucs_name.is_empty()
|
||||
&& super::helpers::UcsXform::from_ucs(&u).is_identity()
|
||||
if h.model_space_ucs_name.is_empty() && super::helpers::UcsXform::from_ucs(&u).is_identity()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
@@ -475,31 +478,30 @@ impl DocumentTab {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let (origin, x_axis, y_axis, elevation, ortho, named, base) =
|
||||
match &self.active_ucs {
|
||||
Some(ucs) => (
|
||||
ucs.origin,
|
||||
ucs.x_axis,
|
||||
ucs.y_axis,
|
||||
ucs.elevation,
|
||||
ucs.ortho_type,
|
||||
if ucs.named_ucs_handle.is_valid() {
|
||||
ucs.named_ucs_handle
|
||||
} else {
|
||||
ucs.handle
|
||||
},
|
||||
ucs.base_ucs_handle,
|
||||
),
|
||||
None => (
|
||||
Vector3::ZERO,
|
||||
Vector3::UNIT_X,
|
||||
Vector3::UNIT_Y,
|
||||
0.0,
|
||||
0,
|
||||
Handle::NULL,
|
||||
Handle::NULL,
|
||||
),
|
||||
};
|
||||
let (origin, x_axis, y_axis, elevation, ortho, named, base) = match &self.active_ucs {
|
||||
Some(ucs) => (
|
||||
ucs.origin,
|
||||
ucs.x_axis,
|
||||
ucs.y_axis,
|
||||
ucs.elevation,
|
||||
ucs.ortho_type,
|
||||
if ucs.named_ucs_handle.is_valid() {
|
||||
ucs.named_ucs_handle
|
||||
} else {
|
||||
ucs.handle
|
||||
},
|
||||
ucs.base_ucs_handle,
|
||||
),
|
||||
None => (
|
||||
Vector3::ZERO,
|
||||
Vector3::UNIT_X,
|
||||
Vector3::UNIT_Y,
|
||||
0.0,
|
||||
0,
|
||||
Handle::NULL,
|
||||
Handle::NULL,
|
||||
),
|
||||
};
|
||||
for object in self.scene.document.objects.values_mut() {
|
||||
let acadrust::objects::ObjectType::Layout(layout) = object else {
|
||||
continue;
|
||||
@@ -656,7 +658,7 @@ impl DocumentTab {
|
||||
#[derive(Clone)]
|
||||
pub(super) enum HistorySnapshot {
|
||||
// Boxed: `DeltaSnapshot` grew past the other variants once
|
||||
// `sketch_constraints` (ERASE-undo fix) was added, and every undo/redo
|
||||
// `parametric_constraints` (ERASE-undo fix) was added, and every undo/redo
|
||||
// stack slot costs as much as this enum's largest variant regardless of
|
||||
// which one it actually holds.
|
||||
Delta(Box<DeltaSnapshot>),
|
||||
@@ -687,41 +689,33 @@ impl HistorySnapshot {
|
||||
)
|
||||
.saturating_add(d.selected_before.len().saturating_mul(16))
|
||||
.saturating_add(d.selected_after.len().saturating_mul(16))
|
||||
.saturating_add(d.active_layer.as_ref().map_or(0, |(before, after)| {
|
||||
before.len().saturating_add(after.len())
|
||||
}))
|
||||
.saturating_add(
|
||||
d.active_layer
|
||||
.as_ref()
|
||||
.map_or(0, |(before, after)| before.len().saturating_add(after.len())),
|
||||
)
|
||||
.saturating_add(
|
||||
d.sketch_constraints
|
||||
d.parametric_constraints
|
||||
.iter()
|
||||
.map(|entry| entry.before.constraints.len().saturating_add(entry.after.constraints.len()))
|
||||
.map(|entry| {
|
||||
entry
|
||||
.before
|
||||
.constraints
|
||||
.len()
|
||||
.saturating_add(entry.after.constraints.len())
|
||||
})
|
||||
.sum::<usize>()
|
||||
.saturating_mul(96),
|
||||
)
|
||||
.saturating_add(
|
||||
d.named_parameters
|
||||
.as_ref()
|
||||
.map_or(0, |(before, after)| before.len().saturating_add(after.len()).saturating_mul(128)),
|
||||
)
|
||||
.saturating_add(d.named_parameters.as_ref().map_or(0, |(before, after)| {
|
||||
before.len().saturating_add(after.len()).saturating_mul(128)
|
||||
}))
|
||||
.saturating_add(d.label.len()),
|
||||
HistorySnapshot::ObjectVisibility(v) => v
|
||||
.before
|
||||
.hidden
|
||||
.len()
|
||||
.saturating_add(
|
||||
v.before
|
||||
.keep
|
||||
.as_ref()
|
||||
.map_or(0, rustc_hash::FxHashSet::len),
|
||||
)
|
||||
.saturating_add(v.before.keep.as_ref().map_or(0, rustc_hash::FxHashSet::len))
|
||||
.saturating_add(v.after.hidden.len())
|
||||
.saturating_add(
|
||||
v.after
|
||||
.keep
|
||||
.as_ref()
|
||||
.map_or(0, rustc_hash::FxHashSet::len),
|
||||
)
|
||||
.saturating_add(v.after.keep.as_ref().map_or(0, rustc_hash::FxHashSet::len))
|
||||
.saturating_add(v.selected_before.len())
|
||||
.saturating_add(v.selected_after.len())
|
||||
.saturating_mul(16)
|
||||
@@ -760,12 +754,12 @@ pub(super) struct DeltaSnapshot {
|
||||
/// Opposite non-entity document state. `apply_delta_state` swaps this with
|
||||
/// the live structure, so the same allocation shuttles between undo/redo.
|
||||
pub(super) structure: Option<StructureSnapshot>,
|
||||
/// Sketch-constraint scopes this same command changed alongside its
|
||||
/// Parametric-constraint scopes this same command changed alongside its
|
||||
/// entities (e.g. ERASE removing a constraint set's touched constraints)
|
||||
/// — `sketch_constraints` lives on `Scene`, not in `document`/
|
||||
/// — `parametric_constraints` lives on `Scene`, not in `document`/
|
||||
/// `document.objects`, so it needs its own channel here rather than
|
||||
/// riding along with `entities`/`structure`. Almost always empty.
|
||||
pub(super) sketch_constraints: Vec<SketchConstraintsEntryDelta>,
|
||||
pub(super) parametric_constraints: Vec<ParametricConstraintsEntryDelta>,
|
||||
/// Drawing-wide parameter table changed by this transaction.
|
||||
pub(super) named_parameters: Option<(
|
||||
crate::scene::named_parameters::ParameterTable,
|
||||
@@ -831,13 +825,13 @@ pub(super) struct ObjectEntryDelta {
|
||||
pub(super) after: Option<acadrust::objects::ObjectType>,
|
||||
}
|
||||
|
||||
/// One sketch-constraint scope's before/after image within an entity delta.
|
||||
/// One parametric-constraint scope's before/after image within an entity delta.
|
||||
/// This keeps constraint cleanup atomic with edits such as entity erasure.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct SketchConstraintsEntryDelta {
|
||||
pub(super) scope: crate::scene::sketch_constraints::SketchScope,
|
||||
pub(super) before: crate::scene::sketch_constraints::SketchConstraintSet,
|
||||
pub(super) after: crate::scene::sketch_constraints::SketchConstraintSet,
|
||||
pub(super) struct ParametricConstraintsEntryDelta {
|
||||
pub(super) scope: crate::scene::parametric_constraints::ParametricScope,
|
||||
pub(super) before: crate::scene::parametric_constraints::ParametricConstraintSet,
|
||||
pub(super) after: crate::scene::parametric_constraints::ParametricConstraintSet,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
||||
+70
-59
@@ -1,7 +1,8 @@
|
||||
use super::{
|
||||
document::{
|
||||
DeltaSnapshot, HistorySnapshot, ObjectEntryDelta, ObjectVisibilitySnapshot,
|
||||
PendingHistorySnapshot, SketchConstraintsEntryDelta, StructureSnapshot, TableEntryDelta,
|
||||
ParametricConstraintsEntryDelta, PendingHistorySnapshot, StructureSnapshot,
|
||||
TableEntryDelta,
|
||||
},
|
||||
OpenCADStudio,
|
||||
};
|
||||
@@ -167,8 +168,7 @@ impl OpenCADStudio {
|
||||
) {
|
||||
self.finish_pending_history(i);
|
||||
let after = self.tabs[i].scene.object_isolation.clone();
|
||||
let selected_after: Vec<Handle> =
|
||||
self.tabs[i].scene.selected.iter().copied().collect();
|
||||
let selected_after: Vec<Handle> = self.tabs[i].scene.selected.iter().copied().collect();
|
||||
if before == after && selected_before == selected_after {
|
||||
return;
|
||||
}
|
||||
@@ -225,7 +225,7 @@ impl OpenCADStudio {
|
||||
dirty_after: true,
|
||||
active_layer: None,
|
||||
structure: (!objects.is_empty()).then_some(StructureSnapshot::Objects(objects)),
|
||||
sketch_constraints: Vec::new(),
|
||||
parametric_constraints: Vec::new(),
|
||||
named_parameters: None,
|
||||
label: label.into(),
|
||||
};
|
||||
@@ -233,9 +233,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
pub(super) fn defer_live_entity_history_after(&mut self, i: usize, handle: Handle) {
|
||||
let Some(HistorySnapshot::Delta(delta)) =
|
||||
self.tabs[i].history.undo_stack.last_mut()
|
||||
else {
|
||||
let Some(HistorySnapshot::Delta(delta)) = self.tabs[i].history.undo_stack.last_mut() else {
|
||||
return;
|
||||
};
|
||||
if let Some((_, _, after)) = delta
|
||||
@@ -254,9 +252,7 @@ impl OpenCADStudio {
|
||||
let selected_after = self.tabs[i].scene.selected.iter().copied().collect();
|
||||
let current_layout_after = self.tabs[i].scene.current_layout.clone();
|
||||
let dirty_after = self.tabs[i].dirty;
|
||||
let Some(HistorySnapshot::Delta(delta)) =
|
||||
self.tabs[i].history.undo_stack.last_mut()
|
||||
else {
|
||||
let Some(HistorySnapshot::Delta(delta)) = self.tabs[i].history.undo_stack.last_mut() else {
|
||||
return;
|
||||
};
|
||||
let Some((_, _, entry_after)) = delta
|
||||
@@ -335,8 +331,9 @@ impl OpenCADStudio {
|
||||
dirty_after,
|
||||
active_layer: (pending.active_layer != active_layer_after)
|
||||
.then_some((pending.active_layer, active_layer_after)),
|
||||
structure: structure_changed.then_some(StructureSnapshot::Full(pending.structure_before)),
|
||||
sketch_constraints: Vec::new(),
|
||||
structure: structure_changed
|
||||
.then_some(StructureSnapshot::Full(pending.structure_before)),
|
||||
parametric_constraints: Vec::new(),
|
||||
named_parameters: None,
|
||||
label: pending.label,
|
||||
};
|
||||
@@ -445,7 +442,7 @@ impl OpenCADStudio {
|
||||
dirty_after: self.tabs[i].dirty,
|
||||
active_layer: None,
|
||||
structure: Some(StructureSnapshot::Layers(entries)),
|
||||
sketch_constraints: Vec::new(),
|
||||
parametric_constraints: Vec::new(),
|
||||
named_parameters: None,
|
||||
label: pending.label,
|
||||
};
|
||||
@@ -502,7 +499,7 @@ impl OpenCADStudio {
|
||||
dirty_after: self.tabs[i].dirty,
|
||||
active_layer: None,
|
||||
structure: Some(StructureSnapshot::TextStyles(entries)),
|
||||
sketch_constraints: Vec::new(),
|
||||
parametric_constraints: Vec::new(),
|
||||
named_parameters: None,
|
||||
label: pending.label,
|
||||
};
|
||||
@@ -559,7 +556,7 @@ impl OpenCADStudio {
|
||||
dirty_after: self.tabs[i].dirty,
|
||||
active_layer: None,
|
||||
structure: Some(StructureSnapshot::DimStyles(entries)),
|
||||
sketch_constraints: Vec::new(),
|
||||
parametric_constraints: Vec::new(),
|
||||
named_parameters: None,
|
||||
label: pending.label,
|
||||
};
|
||||
@@ -624,7 +621,7 @@ impl OpenCADStudio {
|
||||
dirty_after: self.tabs[i].dirty,
|
||||
active_layer: None,
|
||||
structure: Some(StructureSnapshot::Objects(entries)),
|
||||
sketch_constraints: Vec::new(),
|
||||
parametric_constraints: Vec::new(),
|
||||
named_parameters: None,
|
||||
label: pending.label,
|
||||
};
|
||||
@@ -659,7 +656,7 @@ impl OpenCADStudio {
|
||||
dim_names,
|
||||
object_handles,
|
||||
}),
|
||||
sketch_constraints: Vec::new(),
|
||||
parametric_constraints: Vec::new(),
|
||||
named_parameters: None,
|
||||
label: "STYLE".to_string(),
|
||||
};
|
||||
@@ -741,15 +738,16 @@ impl OpenCADStudio {
|
||||
pending.label
|
||||
);
|
||||
}
|
||||
let (entity_before, object_before, sketch_constraints_before, named_parameters_before) =
|
||||
let (entity_before, object_before, parametric_constraints_before, named_parameters_before) =
|
||||
rec.into_recorded_images();
|
||||
let entities: Vec<(Handle, Option<Arc<EntityType>>, Option<Arc<EntityType>>)> = entity_before
|
||||
.into_iter()
|
||||
.map(|(h, before)| {
|
||||
let after = self.tabs[i].scene.document.get_entity_arc(h);
|
||||
(h, before, after)
|
||||
})
|
||||
.collect();
|
||||
let entities: Vec<(Handle, Option<Arc<EntityType>>, Option<Arc<EntityType>>)> =
|
||||
entity_before
|
||||
.into_iter()
|
||||
.map(|(h, before)| {
|
||||
let after = self.tabs[i].scene.document.get_entity_arc(h);
|
||||
(h, before, after)
|
||||
})
|
||||
.collect();
|
||||
let objects: Vec<ObjectEntryDelta> = object_before
|
||||
.into_iter()
|
||||
.filter_map(|(handle, before)| {
|
||||
@@ -761,21 +759,28 @@ impl OpenCADStudio {
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let sketch_constraints: Vec<SketchConstraintsEntryDelta> = sketch_constraints_before
|
||||
.into_iter()
|
||||
.filter_map(|(scope, before)| {
|
||||
let after = self.tabs[i]
|
||||
.scene
|
||||
.sketch_constraint_set(scope)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| crate::scene::sketch_constraints::SketchConstraintSet::new(scope));
|
||||
(before.constraints != after.constraints).then_some(SketchConstraintsEntryDelta {
|
||||
scope,
|
||||
before,
|
||||
after,
|
||||
let parametric_constraints: Vec<ParametricConstraintsEntryDelta> =
|
||||
parametric_constraints_before
|
||||
.into_iter()
|
||||
.filter_map(|(scope, before)| {
|
||||
let after = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(scope)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
crate::scene::parametric_constraints::ParametricConstraintSet::new(
|
||||
scope,
|
||||
)
|
||||
});
|
||||
(before.constraints != after.constraints).then_some(
|
||||
ParametricConstraintsEntryDelta {
|
||||
scope,
|
||||
before,
|
||||
after,
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
.collect();
|
||||
let named_parameters = named_parameters_before.and_then(|before| {
|
||||
let after = self.tabs[i].scene.named_parameters().clone();
|
||||
(before != after).then_some((before, after))
|
||||
@@ -815,7 +820,7 @@ impl OpenCADStudio {
|
||||
dirty_after,
|
||||
active_layer: None,
|
||||
structure,
|
||||
sketch_constraints,
|
||||
parametric_constraints,
|
||||
named_parameters,
|
||||
label: pending.label,
|
||||
};
|
||||
@@ -839,9 +844,10 @@ impl OpenCADStudio {
|
||||
if let Some(structure) = d.structure.as_mut() {
|
||||
match structure {
|
||||
StructureSnapshot::Full(stored) => {
|
||||
let inverse = self.tabs[i].scene.document.swap_structure(
|
||||
std::mem::replace(stored, acadrust::CadDocument::new()),
|
||||
);
|
||||
let inverse = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.swap_structure(std::mem::replace(stored, acadrust::CadDocument::new()));
|
||||
*stored = inverse;
|
||||
self.tabs[i].scene.invalidate_dependency_index();
|
||||
}
|
||||
@@ -945,9 +951,9 @@ impl OpenCADStudio {
|
||||
}
|
||||
let changes = self.tabs[i].scene.apply_entity_delta(&d.entities, undo);
|
||||
let scene = &mut self.tabs[i].scene;
|
||||
for entry in &d.sketch_constraints {
|
||||
for entry in &d.parametric_constraints {
|
||||
let value = if undo { &entry.before } else { &entry.after };
|
||||
*scene.sketch_constraint_set_mut(entry.scope) = value.clone();
|
||||
*scene.parametric_constraint_set_mut(entry.scope) = value.clone();
|
||||
}
|
||||
if let Some((before, after)) = &d.named_parameters {
|
||||
scene.named_parameters = if undo { before } else { after }.clone();
|
||||
@@ -1070,6 +1076,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
}
|
||||
scene.sync_native_parametric_graph();
|
||||
scene.clear_preview_wire();
|
||||
}
|
||||
// UCS state is cached separately from its persisted header/viewport
|
||||
@@ -1104,7 +1111,8 @@ impl OpenCADStudio {
|
||||
let available = self.tabs[i].history.undo_stack.len();
|
||||
let steps = steps.min(available);
|
||||
if steps == 0 {
|
||||
self.command_line.push_info(crate::t!("Nothing to undo.").as_ref());
|
||||
self.command_line
|
||||
.push_info(crate::t!("Nothing to undo.").as_ref());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1127,14 +1135,15 @@ impl OpenCADStudio {
|
||||
structure,
|
||||
StructureSnapshot::Full(_) | StructureSnapshot::Layers(_)
|
||||
)
|
||||
}) || d.active_layer.is_some() || d.entities.iter().any(|(_, before, after)| {
|
||||
before
|
||||
.as_deref()
|
||||
.is_some_and(|entity| matches!(entity, EntityType::Viewport(_)))
|
||||
|| after
|
||||
}) || d.active_layer.is_some()
|
||||
|| d.entities.iter().any(|(_, before, after)| {
|
||||
before
|
||||
.as_deref()
|
||||
.is_some_and(|entity| matches!(entity, EntityType::Viewport(_)))
|
||||
});
|
||||
|| after
|
||||
.as_deref()
|
||||
.is_some_and(|entity| matches!(entity, EntityType::Viewport(_)))
|
||||
});
|
||||
had_full |= d.structure.as_ref().is_some_and(StructureSnapshot::is_full);
|
||||
changes.extend(self.apply_delta_state(i, &mut d, true));
|
||||
self.tabs[i]
|
||||
@@ -1162,7 +1171,8 @@ impl OpenCADStudio {
|
||||
let available = self.tabs[i].history.redo_stack.len();
|
||||
let steps = steps.min(available);
|
||||
if steps == 0 {
|
||||
self.command_line.push_info(crate::t!("Nothing to redo.").as_ref());
|
||||
self.command_line
|
||||
.push_info(crate::t!("Nothing to redo.").as_ref());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1182,14 +1192,15 @@ impl OpenCADStudio {
|
||||
structure,
|
||||
StructureSnapshot::Full(_) | StructureSnapshot::Layers(_)
|
||||
)
|
||||
}) || d.active_layer.is_some() || d.entities.iter().any(|(_, before, after)| {
|
||||
before
|
||||
.as_deref()
|
||||
.is_some_and(|entity| matches!(entity, EntityType::Viewport(_)))
|
||||
|| after
|
||||
}) || d.active_layer.is_some()
|
||||
|| d.entities.iter().any(|(_, before, after)| {
|
||||
before
|
||||
.as_deref()
|
||||
.is_some_and(|entity| matches!(entity, EntityType::Viewport(_)))
|
||||
});
|
||||
|| after
|
||||
.as_deref()
|
||||
.is_some_and(|entity| matches!(entity, EntityType::Viewport(_)))
|
||||
});
|
||||
had_full |= d.structure.as_ref().is_some_and(StructureSnapshot::is_full);
|
||||
changes.extend(self.apply_delta_state(i, &mut d, false));
|
||||
self.tabs[i]
|
||||
|
||||
+39
-13
@@ -7,6 +7,7 @@ pub(crate) fn automation_action_names() -> &'static [&'static str] {
|
||||
pub(crate) mod config;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use automation::{export_headless, serve};
|
||||
mod annotation_data;
|
||||
mod command_driver;
|
||||
pub(crate) mod commands;
|
||||
pub(crate) mod dim_viewport;
|
||||
@@ -31,7 +32,6 @@ mod startup;
|
||||
mod style_ops;
|
||||
mod text_inline;
|
||||
mod tolerance_dialog;
|
||||
mod annotation_data;
|
||||
mod update;
|
||||
mod view;
|
||||
mod visibility;
|
||||
@@ -261,12 +261,12 @@ pub(crate) enum FindMatchKey {
|
||||
}
|
||||
use crate::snap::Snapper;
|
||||
use crate::ui::{CommandLine, Ribbon, StatusBar};
|
||||
use acadrust::CadDocument;
|
||||
use acadrust::types::{Color as AcadColor, LineWeight};
|
||||
use acadrust::CadDocument;
|
||||
|
||||
use iced::time::Instant;
|
||||
use iced::window;
|
||||
use iced::{Point, Task, Theme, mouse};
|
||||
use iced::{mouse, Point, Task, Theme};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(super) const POLY_START_DELAY_MS: u128 = 150;
|
||||
@@ -553,14 +553,18 @@ pub(super) struct OpenCADStudio {
|
||||
/// When true (default), the app registers itself as a .dwg/.dxf/.bak file
|
||||
/// handler on each launch. Toggle with the FILEASSOC command.
|
||||
pub file_assoc_enabled: bool,
|
||||
/// When true, saving creates native constraint objects alongside the
|
||||
/// application's own persistence record. Existing native objects remain
|
||||
/// synchronized regardless of this setting.
|
||||
pub write_dwg_native_constraints: bool,
|
||||
/// When true (default), a sketch constraint's viewport pill shows its
|
||||
/// When true (default), a parametric constraint's viewport pill shows its
|
||||
/// glyph plus a driven value or named-parameter name. When false, every
|
||||
/// pill shows only the glyph.
|
||||
pub show_constraint_values: bool,
|
||||
pub auto_constrain_settings: settings::AutoConstrainSettings,
|
||||
auto_constrain_saved: Option<settings::AutoConstrainSettings>,
|
||||
auto_constrain_selected_row: usize,
|
||||
auto_constrain_distance_input: String,
|
||||
auto_constrain_angle_input: String,
|
||||
pub constraint_solve_mode: bool,
|
||||
pub constraint_infer: bool,
|
||||
pub constraint_bar_display: i16,
|
||||
/// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command);
|
||||
/// 0 disables autosave.
|
||||
pub savetime_min: i32,
|
||||
@@ -1746,6 +1750,7 @@ pub enum ModalKind {
|
||||
DrawingUnits,
|
||||
GeometricTolerance,
|
||||
DraftingSettings,
|
||||
AutoConstrainSettings,
|
||||
LayerStateEditor,
|
||||
Plot,
|
||||
PrintAll,
|
||||
@@ -2138,8 +2143,6 @@ pub enum Message {
|
||||
/// Register or unregister as the .dwg/.dxf handler, from Options. Same
|
||||
/// setting the FILEASSOC command carries.
|
||||
FileAssocChanged(bool),
|
||||
/// Toggle writing native constraint objects on save.
|
||||
WriteDwgNativeConstraintsChanged(bool),
|
||||
/// Toggle showing driven values/named-parameter names on constraint
|
||||
/// pills, from Options. See `show_constraint_values`'s doc comment.
|
||||
ShowConstraintValuesChanged(bool),
|
||||
@@ -2567,9 +2570,9 @@ pub enum Message {
|
||||
/// Cycle the coordinate readout mode ($COORDS): static → live → polar.
|
||||
CycleCoordsMode,
|
||||
/// Removes one flagged redundant or conflicting constraint from the
|
||||
/// current sketch scope.
|
||||
/// current parametric scope.
|
||||
/// No-op if the scope currently has no flagged conflict.
|
||||
ResolveOneSketchConflict,
|
||||
ResolveOneParametricConflict,
|
||||
/// Toggle the status-bar customization menu open/closed.
|
||||
ToggleStatusBarMenu,
|
||||
/// Close the status-bar customization menu.
|
||||
@@ -2666,6 +2669,20 @@ pub enum Message {
|
||||
DraftingSettingsClose,
|
||||
DraftingSettingsCloseDiscard,
|
||||
DraftingSettingsCloseKeep,
|
||||
AutoConstrainSelectRow(usize),
|
||||
AutoConstrainToggleKind(settings::AutoConstraintKind),
|
||||
AutoConstrainMoveUp,
|
||||
AutoConstrainMoveDown,
|
||||
AutoConstrainSelectAll,
|
||||
AutoConstrainClearAll,
|
||||
AutoConstrainReset,
|
||||
AutoConstrainToggleTangentPoint,
|
||||
AutoConstrainTogglePerpendicularIntersection,
|
||||
AutoConstrainDistanceChanged(String),
|
||||
AutoConstrainAngleChanged(String),
|
||||
AutoConstrainApply,
|
||||
AutoConstrainOk,
|
||||
AutoConstrainCancel,
|
||||
/// Toggle a ribbon dropdown open/closed.
|
||||
ToggleRibbonDropdown(String),
|
||||
/// Toggle a collapsed ribbon panel's flyout open/closed (by panel title).
|
||||
@@ -2928,6 +2945,8 @@ pub enum Message {
|
||||
/// A Constraints-section row was clicked: select every entity in the
|
||||
/// list (replacing the current selection) in the viewport.
|
||||
PropConstraintLinkClick(Vec<acadrust::Handle>),
|
||||
/// Remove one parametric constraint selected from Properties or the viewport.
|
||||
PropConstraintDelete(crate::scene::parametric_constraints::ConstraintId),
|
||||
// ── About window ────────────────────────────────────────────────────
|
||||
AboutOpen,
|
||||
// ── Graphics warning ────────────────────────────────────────────────
|
||||
@@ -3656,8 +3675,15 @@ impl OpenCADStudio {
|
||||
dimension_continue_mode: 1,
|
||||
backup_on_save: true,
|
||||
file_assoc_enabled: true,
|
||||
write_dwg_native_constraints: false,
|
||||
show_constraint_values: true,
|
||||
auto_constrain_settings: settings::AutoConstrainSettings::default(),
|
||||
auto_constrain_saved: None,
|
||||
auto_constrain_selected_row: 0,
|
||||
auto_constrain_distance_input: "0.05".to_string(),
|
||||
auto_constrain_angle_input: "1".to_string(),
|
||||
constraint_solve_mode: true,
|
||||
constraint_infer: false,
|
||||
constraint_bar_display: 3,
|
||||
savetime_min: 10,
|
||||
default_bg_color: None,
|
||||
default_paper_bg_color: None,
|
||||
|
||||
+408
-339
File diff suppressed because it is too large.
Load diff
+127
-5
@@ -11,6 +11,108 @@
|
||||
use crate::snap::SnapType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum AutoConstraintKind {
|
||||
Coincident,
|
||||
Collinear,
|
||||
Parallel,
|
||||
Perpendicular,
|
||||
Tangent,
|
||||
Concentric,
|
||||
Horizontal,
|
||||
Vertical,
|
||||
Equal,
|
||||
}
|
||||
|
||||
impl AutoConstraintKind {
|
||||
pub const ALL: [Self; 9] = [
|
||||
Self::Coincident,
|
||||
Self::Collinear,
|
||||
Self::Parallel,
|
||||
Self::Perpendicular,
|
||||
Self::Tangent,
|
||||
Self::Concentric,
|
||||
Self::Horizontal,
|
||||
Self::Vertical,
|
||||
Self::Equal,
|
||||
];
|
||||
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Coincident => "Coincident",
|
||||
Self::Collinear => "Collinear",
|
||||
Self::Parallel => "Parallel",
|
||||
Self::Perpendicular => "Perpendicular",
|
||||
Self::Tangent => "Tangent",
|
||||
Self::Concentric => "Concentric",
|
||||
Self::Horizontal => "Horizontal",
|
||||
Self::Vertical => "Vertical",
|
||||
Self::Equal => "Equal",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct AutoConstrainSettings {
|
||||
pub priority: Vec<AutoConstraintKind>,
|
||||
pub enabled: Vec<AutoConstraintKind>,
|
||||
pub tangent_must_share_point: bool,
|
||||
pub perpendicular_must_intersect: bool,
|
||||
pub distance_tolerance: f64,
|
||||
pub angle_tolerance_deg: f64,
|
||||
}
|
||||
|
||||
impl Default for AutoConstrainSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
priority: AutoConstraintKind::ALL.to_vec(),
|
||||
// Keep Equal available without creating redundant relations
|
||||
// between equal-length segments by default.
|
||||
enabled: AutoConstraintKind::ALL
|
||||
.into_iter()
|
||||
.filter(|kind| *kind != AutoConstraintKind::Equal)
|
||||
.collect(),
|
||||
tangent_must_share_point: true,
|
||||
perpendicular_must_intersect: true,
|
||||
distance_tolerance: 0.05,
|
||||
angle_tolerance_deg: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AutoConstrainSettings {
|
||||
pub fn sanitize(&mut self) {
|
||||
let mut priority = Vec::with_capacity(AutoConstraintKind::ALL.len());
|
||||
for kind in self
|
||||
.priority
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(AutoConstraintKind::ALL)
|
||||
{
|
||||
if !priority.contains(&kind) {
|
||||
priority.push(kind);
|
||||
}
|
||||
}
|
||||
self.priority = priority;
|
||||
self.enabled
|
||||
.retain(|kind| AutoConstraintKind::ALL.contains(kind));
|
||||
self.enabled.sort_by_key(|kind| {
|
||||
self.priority
|
||||
.iter()
|
||||
.position(|candidate| candidate == kind)
|
||||
.unwrap_or(usize::MAX)
|
||||
});
|
||||
self.enabled.dedup();
|
||||
if !self.distance_tolerance.is_finite() || self.distance_tolerance < 0.0 {
|
||||
self.distance_tolerance = 0.05;
|
||||
}
|
||||
if !self.angle_tolerance_deg.is_finite() || self.angle_tolerance_deg < 0.0 {
|
||||
self.angle_tolerance_deg = 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cursor shown over the drawing viewport.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum CursorType {
|
||||
@@ -283,16 +385,25 @@ pub struct UserSettings {
|
||||
/// When true (default), the app (re)registers itself as a .dwg/.dxf/.bak
|
||||
/// handler on every launch. Toggle with the FILEASSOC command.
|
||||
pub file_assoc_enabled: bool,
|
||||
/// When true, saving also writes sketch constraints as native drawing
|
||||
/// objects alongside the application's own persistence record.
|
||||
#[serde(default)]
|
||||
pub write_dwg_native_constraints: bool,
|
||||
/// When true (default), a sketch constraint's viewport pill shows its
|
||||
/// glyph plus a driven value or named-parameter name. When false, every
|
||||
/// pill shows just the bare glyph, so the value/name text doesn't cover
|
||||
/// canvas detail on a dense sketch.
|
||||
#[serde(default = "default_show_constraint_values")]
|
||||
pub show_constraint_values: bool,
|
||||
/// Inference types, priority, intersection rules, and tolerances used by
|
||||
/// the Auto Constrain command.
|
||||
#[serde(default)]
|
||||
pub auto_constrain: AutoConstrainSettings,
|
||||
/// Keep existing geometry size while solving after a constraint edit.
|
||||
#[serde(default = "default_constraint_solve_mode")]
|
||||
pub constraint_solve_mode: bool,
|
||||
/// Apply eligible geometric constraints while creating geometry.
|
||||
#[serde(default)]
|
||||
pub constraint_infer: bool,
|
||||
/// Constraint bar display bit mask: 1 after applying, 2 on selection.
|
||||
#[serde(default = "default_constraint_bar_display")]
|
||||
pub constraint_bar_display: i16,
|
||||
/// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command).
|
||||
/// 0 disables autosave.
|
||||
pub savetime_min: i32,
|
||||
@@ -363,6 +474,14 @@ fn default_show_constraint_values() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_constraint_solve_mode() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_constraint_bar_display() -> i16 {
|
||||
3
|
||||
}
|
||||
|
||||
fn deserialize_clipromptlines<'de, D>(de: D) -> Result<i32, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
@@ -418,8 +537,11 @@ impl Default for UserSettings {
|
||||
textfill: true,
|
||||
backup_on_save: true,
|
||||
file_assoc_enabled: true,
|
||||
write_dwg_native_constraints: false,
|
||||
show_constraint_values: true,
|
||||
auto_constrain: AutoConstrainSettings::default(),
|
||||
constraint_solve_mode: true,
|
||||
constraint_infer: false,
|
||||
constraint_bar_display: 3,
|
||||
savetime_min: 10,
|
||||
default_save_format: crate::io::DEFAULT_SAVE_FORMAT.to_string(),
|
||||
pick_add: true,
|
||||
|
||||
@@ -1999,10 +1999,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
}
|
||||
|
||||
pub(super) fn on_ribbon_color_changed(&mut self, color: AcadColor) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
self.ribbon.prop_color_palette_open = false;
|
||||
self.ribbon.close_dropdown();
|
||||
let handles = self.property_target_handles(i);
|
||||
let i = self.active_tab;
|
||||
self.ribbon.prop_color_palette_open = false;
|
||||
self.ribbon.close_dropdown();
|
||||
self.note_recent_color(color);
|
||||
let handles = self.property_target_handles(i);
|
||||
if handles.is_empty() {
|
||||
if self.has_property_selection(i) {
|
||||
return Task::none();
|
||||
@@ -3247,6 +3248,29 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
self.tabs[i].properties.active_field = None;
|
||||
let handles = self.property_target_handles(i);
|
||||
if !handles.is_empty() {
|
||||
let driven_marker = match field {
|
||||
"start_x" | "start_y" | "start_z" => Some(0),
|
||||
"end_x" | "end_y" | "end_z" => Some(1),
|
||||
"center_x" | "center_y" | "center_z" => Some(-3),
|
||||
_ => None,
|
||||
};
|
||||
let retained_originals: Vec<_> = if self.constraint_solve_mode
|
||||
&& driven_marker.is_some()
|
||||
{
|
||||
handles
|
||||
.iter()
|
||||
.filter_map(|handle| {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(*handle)
|
||||
.cloned()
|
||||
.map(|entity| (*handle, entity))
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let evaluates_expression = self.tabs[i]
|
||||
.properties
|
||||
.sections
|
||||
@@ -3563,7 +3587,22 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.invalidate_property_targets(i, &handles);
|
||||
let driven_refs: Vec<_> = driven_marker
|
||||
.into_iter()
|
||||
.flat_map(|marker| {
|
||||
handles.iter().copied().map(move |handle| {
|
||||
crate::scene::parametric_constraints::ParametricRef::point(
|
||||
handle, marker,
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
self.invalidate_property_targets_with_originals(
|
||||
i,
|
||||
&handles,
|
||||
&driven_refs,
|
||||
&retained_originals,
|
||||
);
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
}
|
||||
|
||||
+1016
-1243
File diff suppressed because it is too large.
Load diff
+674
-502
File diff suppressed because it is too large.
Load diff
+657
-553
File diff suppressed because it is too large.
Load diff
+1156
-1264
File diff suppressed because it is too large.
Load diff
+17
-3
@@ -25,6 +25,7 @@ impl OpenCADStudio {
|
||||
Some(K::DrawingUnits) => crate::t!("Drawing Units").into_owned(),
|
||||
Some(K::GeometricTolerance) => crate::t!("Geometric Tolerance").into_owned(),
|
||||
Some(K::DraftingSettings) => crate::t!("Drafting Settings").into_owned(),
|
||||
Some(K::AutoConstrainSettings) => crate::t!("Constraint Settings").into_owned(),
|
||||
Some(K::LayerStateEditor) => crate::tr!("modal", "edit-layer-state"),
|
||||
Some(K::Plot) => crate::tr!("modal", "plot"),
|
||||
Some(K::PrintAll) => t!("Print All").into_owned(),
|
||||
@@ -201,7 +202,6 @@ impl OpenCADStudio {
|
||||
crate::ui::window::options::view_window(
|
||||
&self.default_save_format,
|
||||
self.file_assoc_enabled,
|
||||
self.write_dwg_native_constraints,
|
||||
self.show_constraint_values,
|
||||
&self.ui_theme,
|
||||
&self.theme_color_inputs,
|
||||
@@ -292,8 +292,8 @@ impl OpenCADStudio {
|
||||
let confirm = self.drafting_settings_close_confirm;
|
||||
sized_flow(
|
||||
ex,
|
||||
700,
|
||||
480,
|
||||
780,
|
||||
500,
|
||||
|flow| {
|
||||
if let Some(state) = state {
|
||||
crate::ui::window::drafting_settings::view_window(
|
||||
@@ -308,6 +308,20 @@ impl OpenCADStudio {
|
||||
},
|
||||
)
|
||||
}
|
||||
super::super::ModalKind::AutoConstrainSettings => sized_flow(
|
||||
ex,
|
||||
620,
|
||||
610,
|
||||
|flow| {
|
||||
crate::ui::window::auto_constrain_settings::view_window(
|
||||
&self.auto_constrain_settings,
|
||||
self.auto_constrain_selected_row,
|
||||
&self.auto_constrain_distance_input,
|
||||
&self.auto_constrain_angle_input,
|
||||
flow,
|
||||
)
|
||||
},
|
||||
),
|
||||
super::super::ModalKind::FindReplace => automatic_flow(ex, |flow| {
|
||||
crate::ui::window::find_replace::view_window(
|
||||
&self.find_replace.search,
|
||||
|
||||
@@ -1114,6 +1114,7 @@ pub(super) fn viewport_context_menu_overlay(
|
||||
bottom_inset: f32,
|
||||
has_cmd: bool,
|
||||
has_selection: bool,
|
||||
selected_constraint: Option<crate::scene::parametric_constraints::ConstraintId>,
|
||||
isolation_active: bool,
|
||||
last_cmds: Vec<String>,
|
||||
draworder_open: bool,
|
||||
@@ -1250,6 +1251,15 @@ pub(super) fn viewport_context_menu_overlay(
|
||||
t!("Invert Selection").into_owned(),
|
||||
Message::InvertSelection,
|
||||
));
|
||||
} else if let Some(id) = selected_constraint {
|
||||
// A selected constraint-glyph pill gets its own minimal menu —
|
||||
// none of the entity-selection actions above (Move/Copy/Isolate/
|
||||
// Select Similar) apply to it.
|
||||
items.push(item(
|
||||
t!("Delete").into_owned(),
|
||||
Message::PropConstraintDelete(id),
|
||||
));
|
||||
items.push(sep());
|
||||
}
|
||||
if isolation_active {
|
||||
items.push(item(
|
||||
|
||||
Reference in new issue
Block a user