forked from 1b-agent/opencadstudio-upstream
Merge viewport creation fixes and current upstream into associations
This commit is contained in:
commit
7f018ab0f1
102 files changed
+28293
-11663
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);
|
||||
}
|
||||
}
|
||||
+1882
-788
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 {
|
||||
|
||||
+172
-294
@@ -1,209 +1,208 @@
|
||||
//! Viewport-aware dimension measurement (PR2).
|
||||
//!
|
||||
//! A dimension created in a paper-space layout while measuring geometry seen
|
||||
//! through a content viewport must *place* itself in paper coordinates but
|
||||
//! *report* the model measurement. Because a viewport frame is a similarity
|
||||
//! (uniform scale plus in-plane twist), the model measurement of any linear,
|
||||
//! aligned or radial span is exactly the raw paper measurement times the
|
||||
//! viewport compensation `1 / scale` -- projecting the model span onto the
|
||||
//! rotated paper axis and compensating is the same number. So no geometry is
|
||||
//! rewritten: the compensation is persisted once, as a negative DIMLFAC
|
||||
//! override on the dimension (see
|
||||
//! [`MeasurementScale::viewport_dimlfac_override`]), and applied once, by the
|
||||
//! linear formatter.
|
||||
//!
|
||||
//! Angular dimensions are never compensated -- a similarity preserves angles.
|
||||
//!
|
||||
//! ## Where the snaps come from
|
||||
//! The per-point [`AcceptedSnap`] list is PR1's
|
||||
//! ([`OpenCADStudio::accepted_snaps`]): the click handler records the real
|
||||
//! snap for every interactive pick, and `apply_step_input` records a free
|
||||
//! snap for typed / dynamic-input / headless points. There is exactly one
|
||||
//! source of truth. Two things are layered on top here:
|
||||
//!
|
||||
//! * the explicit object-pick path ([`Scene::dimension_pick_through_viewport`])
|
||||
//! is not a point step at all, so it records its own snaps
|
||||
//! ([`OpenCADStudio::record_dimension_viewport_pick`]);
|
||||
//! * a *typed* coordinate carries no snap, so PR1 records it as a free point
|
||||
//! with no viewport. [`OpenCADStudio::measure_snap`] re-attaches the
|
||||
//! content viewport under such a point, which is what makes
|
||||
//! `DIMLINEAR` + typed coordinates inside a 1:10 viewport measure the model.
|
||||
//! It is deliberately limited to snaps that are free *and* source-less, so a
|
||||
//! genuine paper-sheet snap that happens to sit over a viewport is never
|
||||
//! reinterpreted.
|
||||
|
||||
use acadrust::entities::Dimension;
|
||||
use acadrust::types::Handle;
|
||||
use acadrust::xdata::XDataValue;
|
||||
use acadrust::EntityType;
|
||||
use glam::DVec3;
|
||||
//! Paper-space dimension acquisition and measurement. Only acquired geometry
|
||||
//! carries viewport compensation; free and typed sheet coordinates stay on the sheet.
|
||||
|
||||
use super::OpenCADStudio;
|
||||
use crate::command::DimensionAssociationSource;
|
||||
use crate::entities::dim_override;
|
||||
use crate::scene::viewport_ref::{AcceptedSnap, MeasurementScale, SnapSourceRef, ViewportFrame};
|
||||
use acadrust::entities::Dimension;
|
||||
use acadrust::types::Handle;
|
||||
use acadrust::EntityType;
|
||||
use glam::DVec3;
|
||||
|
||||
use super::OpenCADStudio;
|
||||
|
||||
/// How a pending dimension should be measured.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(crate) enum DimensionMeasureSpace {
|
||||
/// Plain measurement in the space the definition points live in.
|
||||
Direct,
|
||||
/// Every measuring point came through one viewport; compensate by its
|
||||
/// scale.
|
||||
Viewport(ViewportFrame),
|
||||
/// The points came from different viewports, or from a viewport and the
|
||||
/// bare sheet. Falls back to `Direct` with a warning — never a silently
|
||||
/// wrong number.
|
||||
Mixed,
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
/// Record the accepted snaps produced by the explicit object-pick path (a
|
||||
/// dimension "select object" pick resolved through a content viewport).
|
||||
///
|
||||
/// An object pick is not a point step, so PR1's click handler never sees
|
||||
/// it. One pick supplies *both* extension origins, so it is recorded
|
||||
/// twice: that keeps the list index-parallel with the definition points
|
||||
/// the command derives from the entity, and lets the "every measuring
|
||||
/// point came through one viewport" classification see a complete set.
|
||||
pub(crate) fn record_dimension_viewport_pick(
|
||||
pub(crate) fn finish_command_click(&mut self, i: usize) {
|
||||
let mut selection = self.tabs[i].scene.selection.borrow_mut();
|
||||
selection.left_down = false;
|
||||
selection.left_press_pos = None;
|
||||
selection.left_press_time = None;
|
||||
selection.left_dragging = false;
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // Consumed by the dependent association change.
|
||||
pub(crate) fn dimension_measuring_snaps(&self, _i: usize) -> Vec<AcceptedSnap> {
|
||||
self.accepted_snaps().to_vec()
|
||||
}
|
||||
|
||||
pub(crate) fn dimension_measure_space(&self, i: usize) -> DimensionMeasureSpace {
|
||||
let scene = &self.tabs[i].scene;
|
||||
if scene.current_layout == "Model" || scene.active_viewport.is_some() {
|
||||
return DimensionMeasureSpace::Direct;
|
||||
}
|
||||
let mut frame: Option<ViewportFrame> = None;
|
||||
let mut direct = false;
|
||||
for snap in self.accepted_snaps() {
|
||||
match snap.frame {
|
||||
Some(f) => match frame {
|
||||
None => frame = Some(f),
|
||||
Some(previous) if previous.viewport == f.viewport => {}
|
||||
Some(_) => return DimensionMeasureSpace::Mixed,
|
||||
},
|
||||
None => direct = true,
|
||||
}
|
||||
}
|
||||
match (frame, direct) {
|
||||
(None, _) => DimensionMeasureSpace::Direct,
|
||||
(Some(f), false) => DimensionMeasureSpace::Viewport(f),
|
||||
_ => DimensionMeasureSpace::Mixed,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reject an incompatible measuring input before it advances the command.
|
||||
pub(crate) fn dimension_acquisition_allowed(
|
||||
&mut self,
|
||||
frame: ViewportFrame,
|
||||
paper_point: DVec3,
|
||||
model_point: DVec3,
|
||||
i: usize,
|
||||
viewport: Option<Handle>,
|
||||
) -> bool {
|
||||
if !self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.is_some_and(|c| c.measures_through_viewports())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if self.accepted_snaps().iter().any(|s| s.viewport != viewport) {
|
||||
self.command_line.push_error(crate::t!("Pick geometry from the same viewport, or use paper-space points for the whole dimension.").as_ref());
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// A command can reject a degenerate point without advancing its step.
|
||||
pub(crate) fn sync_dimension_snaps(&mut self, i: usize) {
|
||||
if let Some(cmd) = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.filter(|c| c.measures_through_viewports())
|
||||
{
|
||||
self.accepted_snaps
|
||||
.truncate(cmd.dimension_acquired_points().len());
|
||||
}
|
||||
}
|
||||
|
||||
/// Record each definition point actually acquired by an object pick.
|
||||
/// Arcs may supply three angular slots, lines two, and radial picks one.
|
||||
pub(crate) fn record_dimension_entity_points(
|
||||
&mut self,
|
||||
i: usize,
|
||||
frame: Option<ViewportFrame>,
|
||||
entity: Handle,
|
||||
block_path: Vec<Handle>,
|
||||
) {
|
||||
for _ in 0..2 {
|
||||
let Some(cmd) = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.filter(|c| c.measures_through_viewports())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let points = cmd.dimension_acquired_points();
|
||||
for paper_point in points.into_iter().skip(self.accepted_snaps.len()) {
|
||||
self.push_accepted_snap(AcceptedSnap {
|
||||
paper_point,
|
||||
model_point,
|
||||
viewport: Some(frame.viewport),
|
||||
frame: Some(frame),
|
||||
model_point: frame.map_or(paper_point, |f| f.paper_to_model(paper_point)),
|
||||
viewport: frame.map(|f| f.viewport),
|
||||
frame,
|
||||
source: Some(SnapSourceRef {
|
||||
source: DimensionAssociationSource::inferred(entity),
|
||||
block_path: block_path.clone(),
|
||||
// An object pick acquires the whole curve, not one
|
||||
// feature; the per-slot feature point is derived from the
|
||||
// committed dimension's own definition points.
|
||||
snap_type: crate::snap::SnapType::Nearest,
|
||||
snap_type: crate::snap::SnapType::ObjectPick,
|
||||
intersection: None,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// The accepted snap for the `idx`-th collected point of the active
|
||||
/// command, with the typed-coordinate viewport fallback applied.
|
||||
///
|
||||
/// Reads PR1's list -- there is no second store.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn accepted_snap_for_point(&self, i: usize, idx: usize) -> Option<AcceptedSnap> {
|
||||
self.accepted_snaps().get(idx).map(|snap| self.measure_snap(i, snap))
|
||||
}
|
||||
|
||||
/// One accepted snap as *measurement* sees it.
|
||||
///
|
||||
/// Identical to the recorded snap except that a point which carries no
|
||||
/// snap at all (a typed coordinate, a dynamic-input point, a headless
|
||||
/// script point) and lands inside a content viewport is re-attached to
|
||||
/// that viewport, so typing `100,50` inside a 1:10 viewport measures the
|
||||
/// model just like clicking there would. A snap that landed on real
|
||||
/// paper-sheet geometry keeps `viewport: None` and is never reinterpreted.
|
||||
pub(crate) fn measure_snap(&self, i: usize, snap: &AcceptedSnap) -> AcceptedSnap {
|
||||
if snap.frame.is_some() || snap.source.is_some() {
|
||||
return snap.clone();
|
||||
}
|
||||
let scene = &self.tabs[i].scene;
|
||||
if scene.current_layout == "Model" || scene.active_viewport.is_some() {
|
||||
return snap.clone();
|
||||
}
|
||||
match scene.viewport_frame_at_paper_point(snap.paper_point) {
|
||||
Some(frame) => AcceptedSnap {
|
||||
paper_point: snap.paper_point,
|
||||
model_point: frame.paper_to_model(snap.paper_point),
|
||||
viewport: Some(frame.viewport),
|
||||
frame: Some(frame),
|
||||
source: None,
|
||||
},
|
||||
None => snap.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The accepted snaps that say what the pending dimension *measures*, in
|
||||
/// collection order.
|
||||
///
|
||||
/// The final collected point of every dimension command in scope is the
|
||||
/// dimension-line / text placement click, which says nothing about what is
|
||||
/// being measured -- it routinely lands on bare sheet even for a viewport
|
||||
/// measurement, and inside a viewport rectangle even for a plain paper
|
||||
/// one. It is dropped here, which is also why association inference is
|
||||
/// guarded on this set rather than on every recorded snap.
|
||||
pub(crate) fn dimension_measuring_snaps(&self, i: usize) -> Vec<AcceptedSnap> {
|
||||
let all = self.accepted_snaps();
|
||||
let measuring = match all.len() {
|
||||
0 | 1 => all,
|
||||
n => &all[..n - 1],
|
||||
pub(crate) fn apply_viewport_dimension_measurement(
|
||||
&mut self,
|
||||
i: usize,
|
||||
entity: &mut EntityType,
|
||||
) -> bool {
|
||||
let EntityType::Dimension(dimension) = entity else {
|
||||
return true;
|
||||
};
|
||||
measuring.iter().map(|snap| self.measure_snap(i, snap)).collect()
|
||||
}
|
||||
|
||||
/// Classify how the pending dimension should be measured from the snaps
|
||||
/// recorded for it.
|
||||
pub(crate) fn dimension_measure_space(&self, i: usize) -> DimensionMeasureSpace {
|
||||
let scene = &self.tabs[i].scene;
|
||||
// Only a dimension being created *on the sheet* can measure through a
|
||||
// viewport. Inside MSPACE the command already works in model space.
|
||||
if scene.current_layout == "Model" || scene.active_viewport.is_some() {
|
||||
return DimensionMeasureSpace::Direct;
|
||||
}
|
||||
let measuring = self.dimension_measuring_snaps(i);
|
||||
let measuring = measuring.as_slice();
|
||||
if measuring.is_empty() {
|
||||
return DimensionMeasureSpace::Direct;
|
||||
}
|
||||
let mut frame: Option<ViewportFrame> = None;
|
||||
let mut any_free = false;
|
||||
for snap in measuring {
|
||||
match snap.frame {
|
||||
Some(f) => match frame {
|
||||
None => frame = Some(f),
|
||||
Some(existing) if existing.viewport == f.viewport => {}
|
||||
Some(_) => return DimensionMeasureSpace::Mixed,
|
||||
},
|
||||
None => any_free = true,
|
||||
let angular = matches!(
|
||||
dimension,
|
||||
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_)
|
||||
);
|
||||
let frame = match self.dimension_measure_space(i) {
|
||||
DimensionMeasureSpace::Direct => return true,
|
||||
DimensionMeasureSpace::Mixed => {
|
||||
self.command_line.push_error(crate::t!("Pick geometry from the same viewport, or use paper-space points for the whole dimension.").as_ref());
|
||||
return false;
|
||||
}
|
||||
DimensionMeasureSpace::Viewport(f) => f,
|
||||
};
|
||||
if angular {
|
||||
return true;
|
||||
}
|
||||
match (frame, any_free) {
|
||||
(None, _) => DimensionMeasureSpace::Direct,
|
||||
(Some(f), false) => DimensionMeasureSpace::Viewport(f),
|
||||
// One origin through a viewport and the other on the sheet: the
|
||||
// two points are not in a common measurable space.
|
||||
(Some(_), true) => DimensionMeasureSpace::Mixed,
|
||||
let dimlfac = self.pending_dimension_style_dimlfac(i, entity);
|
||||
let scale = MeasurementScale {
|
||||
user_lfac: MeasurementScale::user_lfac_for_space(dimlfac, true),
|
||||
viewport_compensation: frame.paper_to_model_length_factor(),
|
||||
};
|
||||
if !scale.paper_factor().is_finite() || scale.paper_factor() <= 0.0 {
|
||||
return false;
|
||||
}
|
||||
// Owner is needed even by DIMASSOC=0's explode path, before insertion.
|
||||
entity.common_mut().owner_handle = self.tabs[i].scene.current_layout_block_handle_pub();
|
||||
scale.write_to_entity(entity);
|
||||
true
|
||||
}
|
||||
|
||||
/// The slot array [`Scene::attach_viewport_dimension_association`] wants,
|
||||
/// built from the snaps PR1 recorded for the command that just committed
|
||||
/// `dimension`.
|
||||
///
|
||||
/// Slot `k` describes the feature at
|
||||
/// `Scene::dimension_association_slot_points()[k]`, so the measuring snaps
|
||||
/// are taken in collection order and truncated / padded to that length.
|
||||
///
|
||||
/// A snap whose paper point is not (still) the definition point it would
|
||||
/// fill has its `model_point` recomputed from that definition point
|
||||
/// through the frame. Two cases need it:
|
||||
///
|
||||
/// * the explicit object pick, which records one click twice — once per
|
||||
/// extension origin — so neither recorded point is the endpoint the
|
||||
/// command derived from the entity;
|
||||
/// * a point the command moved after the snap (a rotated DIMLINEAR
|
||||
/// projects its origins onto the measuring axis).
|
||||
///
|
||||
/// The source identity is kept in both cases: it is the entity that was
|
||||
/// acquired, and the feature marker is re-derived from the corrected
|
||||
/// point.
|
||||
pub(crate) fn try_dimension_viewport_entity_pick(
|
||||
&mut self,
|
||||
i: usize,
|
||||
paper: DVec3,
|
||||
aperture_paper: f64,
|
||||
) -> Option<crate::command::CmdResult> {
|
||||
let pick = self.tabs[i]
|
||||
.scene
|
||||
.dimension_pick_through_viewport(paper, aperture_paper)?;
|
||||
if !self.dimension_acquisition_allowed(i, Some(pick.frame.viewport)) {
|
||||
return Some(crate::command::CmdResult::NeedPoint);
|
||||
}
|
||||
let command = self.tabs[i].active_cmd.as_mut()?;
|
||||
command.inject_picked_entity(pick.paper_entity);
|
||||
let result = command.on_entity_pick(pick.entity_handle, pick.paper_point);
|
||||
self.record_dimension_entity_points(
|
||||
i,
|
||||
Some(pick.frame),
|
||||
pick.entity_handle,
|
||||
pick.block_path,
|
||||
);
|
||||
Some(result)
|
||||
}
|
||||
|
||||
fn pending_dimension_style_dimlfac(&self, i: usize, entity: &EntityType) -> f64 {
|
||||
let EntityType::Dimension(dimension) = entity else {
|
||||
return 1.0;
|
||||
};
|
||||
dim_override::real(
|
||||
&dimension.base().common.extended_data,
|
||||
dim_override::DIMLFAC,
|
||||
)
|
||||
.or_else(|| {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.dim_styles
|
||||
.iter()
|
||||
.find(|s| s.name.eq_ignore_ascii_case(&dimension.base().style_name))
|
||||
.map(|s| s.dimlfac)
|
||||
})
|
||||
.unwrap_or(1.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(crate) fn viewport_dimension_snaps(
|
||||
&self,
|
||||
i: usize,
|
||||
@@ -260,125 +259,4 @@ impl OpenCADStudio {
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
}
|
||||
|
||||
/// Apply viewport compensation to a dimension about to be committed.
|
||||
///
|
||||
/// Whether the dimension may also take a paper-space association is a
|
||||
/// separate question with the same answer source:
|
||||
/// [`OpenCADStudio::dimension_association_allowed`].
|
||||
pub(crate) fn apply_viewport_dimension_measurement(
|
||||
&mut self,
|
||||
i: usize,
|
||||
entity: &mut EntityType,
|
||||
) {
|
||||
let EntityType::Dimension(dimension) = entity else {
|
||||
return;
|
||||
};
|
||||
let is_angular = matches!(
|
||||
dimension,
|
||||
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_)
|
||||
);
|
||||
let frame = match self.dimension_measure_space(i) {
|
||||
DimensionMeasureSpace::Direct => return,
|
||||
DimensionMeasureSpace::Mixed => {
|
||||
self.command_line.push_warning(
|
||||
crate::t!(
|
||||
"Mixed-space measurement is not supported: the extension origins come from different viewports, or from a viewport and the sheet. Measuring in paper space instead (no viewport scale compensation)."
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
DimensionMeasureSpace::Viewport(frame) => frame,
|
||||
};
|
||||
|
||||
// A similarity preserves angles, so an angular dimension reads the
|
||||
// model angle already and needs no override.
|
||||
if is_angular {
|
||||
return;
|
||||
}
|
||||
|
||||
let compensation = frame.paper_to_model_length_factor();
|
||||
let style_dimlfac = self.pending_dimension_style_dimlfac(i, entity);
|
||||
if let Some(value) =
|
||||
MeasurementScale::viewport_dimlfac_override(style_dimlfac, compensation)
|
||||
{
|
||||
// The negative DIMLFAC convention is resolved from the dimension's
|
||||
// owner block, and the commit path (or, with DIMASSOC = 0, the
|
||||
// explode path) reads the entity before `add_entity_to_layout`
|
||||
// assigns one. Stamping the layout block here — the same handle
|
||||
// the add would set — keeps the entity self-describing, so the
|
||||
// text is compensated whichever path consumes it.
|
||||
let layout_block = self.tabs[i].scene.current_layout_block_handle_pub();
|
||||
if layout_block.is_valid() {
|
||||
entity.common_mut().owner_handle = layout_block;
|
||||
}
|
||||
dim_override::set_on_entity(
|
||||
entity,
|
||||
dim_override::DIMLFAC,
|
||||
Some(XDataValue::Real(value)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Explicit "select object" pick for a dimension command, resolved through
|
||||
/// a paper-space content viewport.
|
||||
///
|
||||
/// Paper-space hit testing only sees sheet entities, so a click on model
|
||||
/// geometry displayed inside a viewport finds nothing. This maps the
|
||||
/// cursor into model space through the viewport frame, hit-tests the
|
||||
/// viewport's resident model wires (descending into block instances and
|
||||
/// baking the instance transform), then hands the command a copy of the
|
||||
/// picked entity projected onto the sheet. The commands therefore keep
|
||||
/// working entirely in paper coordinates, and the measurement is
|
||||
/// compensated at commit time like any other viewport measurement.
|
||||
///
|
||||
/// Returns `None` when the click is not inside a content viewport or hits
|
||||
/// nothing, so the caller falls back to the ordinary paper-space pick.
|
||||
pub(crate) fn try_dimension_viewport_entity_pick(
|
||||
&mut self,
|
||||
i: usize,
|
||||
paper: DVec3,
|
||||
aperture_paper: f64,
|
||||
) -> Option<crate::command::CmdResult> {
|
||||
let pick = self.tabs[i]
|
||||
.scene
|
||||
.dimension_pick_through_viewport(paper, aperture_paper)?;
|
||||
self.record_dimension_viewport_pick(
|
||||
pick.frame,
|
||||
pick.paper_point,
|
||||
pick.model_point,
|
||||
pick.entity_handle,
|
||||
pick.block_path.clone(),
|
||||
);
|
||||
let command = self.tabs[i].active_cmd.as_mut()?;
|
||||
command.inject_picked_entity(pick.paper_entity);
|
||||
Some(command.on_entity_pick(pick.entity_handle, pick.paper_point))
|
||||
}
|
||||
|
||||
/// The DIMLFAC the pending dimension would inherit *without* a viewport
|
||||
/// override: its own existing override if it has one, otherwise its named
|
||||
/// dimension style's value.
|
||||
fn pending_dimension_style_dimlfac(&self, i: usize, entity: &EntityType) -> f64 {
|
||||
let EntityType::Dimension(dimension) = entity else {
|
||||
return 1.0;
|
||||
};
|
||||
if let Some(value) = dim_override::real(
|
||||
&dimension.base().common.extended_data,
|
||||
dim_override::DIMLFAC,
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
let name = dimension.base().style_name.as_str();
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.dim_styles
|
||||
.iter()
|
||||
.find(|style| {
|
||||
style.name.eq_ignore_ascii_case(name)
|
||||
|| (name.trim().is_empty() && style.name.eq_ignore_ascii_case("Standard"))
|
||||
})
|
||||
.map(|style| style.dimlfac)
|
||||
.unwrap_or(1.0)
|
||||
}
|
||||
}
|
||||
+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]
|
||||
|
||||
+41
-13
@@ -7,9 +7,12 @@ 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;
|
||||
#[cfg(test)]
|
||||
mod viewport_dimension_tests;
|
||||
mod document;
|
||||
mod drafting_settings;
|
||||
pub(crate) mod expr_eval;
|
||||
@@ -31,7 +34,6 @@ mod startup;
|
||||
mod style_ops;
|
||||
mod text_inline;
|
||||
mod tolerance_dialog;
|
||||
mod annotation_data;
|
||||
mod update;
|
||||
mod view;
|
||||
mod visibility;
|
||||
@@ -261,12 +263,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 +555,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 +1752,7 @@ pub enum ModalKind {
|
||||
DrawingUnits,
|
||||
GeometricTolerance,
|
||||
DraftingSettings,
|
||||
AutoConstrainSettings,
|
||||
LayerStateEditor,
|
||||
Plot,
|
||||
PrintAll,
|
||||
@@ -2138,8 +2145,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 +2572,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 +2671,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 +2947,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 +3677,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
+679
-579
File diff suppressed because it is too large.
Load diff
@@ -106,7 +106,7 @@ impl OpenCADStudio {
|
||||
snap: Option<SnapResult>,
|
||||
frame: Option<ViewportFrame>,
|
||||
committed: glam::DVec3,
|
||||
) {
|
||||
) -> bool {
|
||||
let mut accepted = match snap {
|
||||
// A viewport snap keeps its frame only when the hit really came
|
||||
// through it, so a paper-sheet snap never claims a model point.
|
||||
@@ -116,8 +116,25 @@ impl OpenCADStudio {
|
||||
}
|
||||
None => AcceptedSnap::free(committed),
|
||||
};
|
||||
if self.tabs[tab]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.is_some_and(|cmd| cmd.measures_through_viewports())
|
||||
{
|
||||
if self.tabs[tab]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.is_some_and(|cmd| cmd.dimension_placement_pending())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if !self.dimension_acquisition_allowed(tab, accepted.viewport) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
self.resolve_source_block_path(tab, &mut accepted);
|
||||
self.push_accepted_snap(accepted);
|
||||
true
|
||||
}
|
||||
|
||||
/// Replace a snap source that is a block INSTANCE with the entity inside
|
||||
@@ -199,30 +216,12 @@ impl OpenCADStudio {
|
||||
return None;
|
||||
}
|
||||
|
||||
let handles = self.tabs[i].scene.layout_content_viewports();
|
||||
// Draw order puts the last-drawn viewport on top; probe top-most first
|
||||
// so overlapping viewports resolve the same way they render.
|
||||
for &handle in handles.iter().rev() {
|
||||
if !self.tabs[i].scene.viewport_displays_content(handle) {
|
||||
continue;
|
||||
}
|
||||
let Some(frame) = self.tabs[i].scene.viewport_frame(handle) else {
|
||||
continue; // oblique / perspective viewport: unsupported
|
||||
};
|
||||
// Clip first (rectangle + non-rectangular clip boundary), in paper
|
||||
// coordinates, so a cursor over the cut-away part of a clipped
|
||||
// viewport sees nothing.
|
||||
if !self
|
||||
.tabs[i]
|
||||
.scene
|
||||
.viewport_displays_paper_point(handle, cursor_paper.truncate())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let Some((cam, rect)) = self
|
||||
.tabs[i]
|
||||
.scene
|
||||
.viewport_edit_frame_for(handle, canvas)
|
||||
for frame in self.tabs[i]
|
||||
.scene
|
||||
.viewport_frames_at_paper_point(cursor_paper)
|
||||
{
|
||||
let handle = frame.viewport;
|
||||
let Some((cam, rect)) = self.tabs[i].scene.viewport_edit_frame_for(handle, canvas)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
@@ -242,8 +241,7 @@ impl OpenCADStudio {
|
||||
// the viewport camera projects back onto `local`.
|
||||
let model_cursor = frame.paper_to_model(cursor_paper);
|
||||
|
||||
let wires = self
|
||||
.tabs[i]
|
||||
let wires = self.tabs[i]
|
||||
.scene
|
||||
.model_wires_for_viewport_arc(handle, bounds.height);
|
||||
let candidates = self.tabs[i].scene.interaction_candidates_near(
|
||||
@@ -255,14 +253,14 @@ impl OpenCADStudio {
|
||||
self.snapper.osnap_radius_px,
|
||||
);
|
||||
|
||||
// Object snap only. The grid belongs to the paper sheet, and the
|
||||
// rubber-band origin / tracking points are paper-space points, so
|
||||
// perpendicular and extension feet would be nonsense in model
|
||||
// space. Save and restore, since `Snapper` is shared state.
|
||||
// The grid belongs to the sheet. Translate the construction
|
||||
// anchor for perpendicular/tangent snaps into this viewport's
|
||||
// model coordinates, then restore the shared snapper state.
|
||||
let saved_grid = self.snapper.grid_snap_on;
|
||||
let saved_from = self.snapper.from_point;
|
||||
self.snapper.grid_snap_on = false;
|
||||
self.snapper.from_point = None;
|
||||
self.snapper.from_point =
|
||||
saved_from.map(|p| frame.paper_to_model(p.as_dvec3()).as_vec3());
|
||||
let hit = self.snapper.snap(
|
||||
model_cursor,
|
||||
local,
|
||||
@@ -285,8 +283,7 @@ impl OpenCADStudio {
|
||||
// the aperture reaching past the clip. Reject those: a snap must be
|
||||
// a real feature the viewport actually displays.
|
||||
let paper = frame.model_to_paper(hit.world);
|
||||
if !self
|
||||
.tabs[i]
|
||||
if !self.tabs[i]
|
||||
.scene
|
||||
.viewport_displays_paper_point(handle, paper.truncate())
|
||||
{
|
||||
@@ -325,5 +322,7 @@ fn project_hit_to_paper(
|
||||
}),
|
||||
viewport: Some(viewport),
|
||||
source: hit.source,
|
||||
secondary_source: hit.secondary_source,
|
||||
model_point: Some(hit.world),
|
||||
}
|
||||
}
|
||||
+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(
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
use super::OpenCADStudio;
|
||||
use crate::command::{DimensionAssociationSource, StepInput};
|
||||
use crate::entities::dim_override;
|
||||
use crate::scene::viewport_ref::{AcceptedSnap, MeasurementScale, ViewportFrame};
|
||||
use crate::scene::Scene;
|
||||
use crate::snap::{SnapResult, SnapType};
|
||||
use acadrust::entities::{Circle, Dimension, Line, Viewport};
|
||||
use acadrust::types::{Handle, Vector3};
|
||||
use acadrust::{CadDocument, EntityType};
|
||||
use glam::DVec3;
|
||||
|
||||
fn fixture() -> (OpenCADStudio, Handle, ViewportFrame) {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
let mut scene = Scene::new();
|
||||
let line = scene.add_entity(EntityType::Line(Line::from_points(
|
||||
Vector3::ZERO,
|
||||
Vector3::new(100.0, 0.0, 0.0),
|
||||
)));
|
||||
scene.document.add_layout("Dimensions").unwrap();
|
||||
scene.set_current_layout("Dimensions".into());
|
||||
let mut viewport = Viewport::new();
|
||||
viewport.id = 2;
|
||||
viewport.center = Vector3::new(50.0, 50.0, 0.0);
|
||||
viewport.width = 100.0;
|
||||
viewport.height = 100.0;
|
||||
viewport.view_height = 1000.0;
|
||||
viewport.custom_scale = 0.1;
|
||||
viewport.view_direction = Vector3::UNIT_Z;
|
||||
viewport.status.is_on = true;
|
||||
let viewport = scene.add_entity(EntityType::Viewport(viewport));
|
||||
let frame = scene.viewport_frame(viewport).unwrap();
|
||||
scene.document.header.dimension_associativity = 2;
|
||||
let i = app.active_tab;
|
||||
app.tabs[i].scene = scene;
|
||||
(app, line, frame)
|
||||
}
|
||||
|
||||
fn hit(frame: ViewportFrame, source: Handle, model: DVec3) -> SnapResult {
|
||||
SnapResult {
|
||||
world: frame.model_to_paper(model),
|
||||
model_point: Some(model),
|
||||
screen: iced::Point::ORIGIN,
|
||||
snap_type: SnapType::Endpoint,
|
||||
tangent_obj: None,
|
||||
extension_base: None,
|
||||
extension_base2: None,
|
||||
extension_origin: None,
|
||||
extension_dir: None,
|
||||
viewport: Some(frame.viewport),
|
||||
source: Some(DimensionAssociationSource::inferred(source)),
|
||||
secondary_source: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn point(app: &mut OpenCADStudio, frame: ViewportFrame, source: Handle, model: DVec3) {
|
||||
let i = app.active_tab;
|
||||
let hit = hit(frame, source, model);
|
||||
let paper = hit.world.with_z(0.0);
|
||||
assert!(app.record_accepted_snap(i, Some(hit), Some(frame), paper));
|
||||
let result = app.tabs[i].active_cmd.as_mut().unwrap().on_point(paper);
|
||||
app.sync_dimension_snaps(i);
|
||||
let _ = app.apply_cmd_result(result);
|
||||
}
|
||||
|
||||
fn dimension(doc: &CadDocument) -> &Dimension {
|
||||
doc.entities()
|
||||
.find_map(|e| match e {
|
||||
EntityType::Dimension(d) => Some(d),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn displayed(doc: &CadDocument) -> f64 {
|
||||
let dim = dimension(doc);
|
||||
if matches!(dim, Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_)) {
|
||||
return dim.measurement();
|
||||
}
|
||||
let factor = dim_override::real(&dim.base().common.extended_data, dim_override::DIMLFAC)
|
||||
.or_else(|| {
|
||||
doc.dim_styles
|
||||
.get(&dim.base().style_name)
|
||||
.map(|s| s.dimlfac)
|
||||
})
|
||||
.unwrap_or(1.0);
|
||||
dim.measurement() * MeasurementScale::user_lfac_for_space(factor, true)
|
||||
}
|
||||
|
||||
fn finish_aligned(app: &mut OpenCADStudio, line: Handle, frame: ViewportFrame) {
|
||||
let _ = app.dispatch_command("DIMALIGNED");
|
||||
point(app, frame, line, DVec3::ZERO);
|
||||
point(app, frame, line, DVec3::X * 100.0);
|
||||
let _ = app.feed_command(StepInput::Point(DVec3::new(250.0, 200.0, 0.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_dimension_projected_snap_keeps_identity_and_elevation() {
|
||||
let (_, line, frame) = fixture();
|
||||
let model = DVec3::new(100.0, 0.0, 35.0);
|
||||
let hit = hit(frame, line, model);
|
||||
let accepted =
|
||||
AcceptedSnap::from_snap(&hit, Some(frame)).with_paper_point(hit.world.with_z(0.0));
|
||||
assert_eq!(accepted.model_point, model);
|
||||
assert_eq!(accepted.source.as_ref().unwrap().source.handle, line);
|
||||
let moved = accepted.with_paper_point(hit.world + DVec3::X);
|
||||
assert!(moved.source.is_none());
|
||||
assert!((moved.model_point - frame.paper_to_model(moved.paper_point)).length() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_dimension_creation_preserves_active_units_and_roundtrips() {
|
||||
for factor in [25.4, -25.4] {
|
||||
let (mut app, line, frame) = fixture();
|
||||
let i = app.active_tab;
|
||||
let mut style = acadrust::tables::DimStyle::new("Millimetres");
|
||||
style.dimlfac = factor;
|
||||
app.tabs[i].scene.document.dim_styles.add(style).unwrap();
|
||||
app.tabs[i].scene.document.header.current_dimstyle_name = "Millimetres".into();
|
||||
finish_aligned(&mut app, line, frame);
|
||||
let doc = &app.tabs[i].scene.document;
|
||||
assert_eq!(dimension(doc).base().style_name, "Millimetres");
|
||||
assert!((displayed(doc) - 2540.0).abs() < 1e-5, "{}", displayed(doc));
|
||||
let data = MeasurementScale::read(&dimension(doc).base().common.extended_data).unwrap();
|
||||
assert_eq!(data.user_lfac, 25.4);
|
||||
assert_eq!(doc.header.dimension_associativity, 2);
|
||||
for ext in ["dxf", "dwg"] {
|
||||
let bytes = crate::io::save_to_bytes(doc, ext, doc.version).unwrap();
|
||||
let loaded = crate::io::load_bytes(&format!("viewport.{ext}"), bytes).unwrap();
|
||||
assert!(
|
||||
(displayed(&loaded) - 2540.0).abs() < 1e-4,
|
||||
"{ext}: {}",
|
||||
displayed(&loaded)
|
||||
);
|
||||
assert_eq!(
|
||||
MeasurementScale::read(&dimension(&loaded).base().common.extended_data),
|
||||
Some(data)
|
||||
);
|
||||
}
|
||||
app.undo_steps(1);
|
||||
assert!(!app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.any(|e| matches!(e, EntityType::Dimension(_))));
|
||||
app.redo_steps(1);
|
||||
assert!((displayed(&app.tabs[i].scene.document) - 2540.0).abs() < 1e-5);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_dimension_mixed_and_degenerate_picks_allow_retry() {
|
||||
let (mut app, line, frame) = fixture();
|
||||
let i = app.active_tab;
|
||||
let _ = app.dispatch_command("DIMALIGNED");
|
||||
point(&mut app, frame, line, DVec3::ZERO);
|
||||
point(&mut app, frame, line, DVec3::ZERO);
|
||||
assert_eq!(app.accepted_snaps().len(), 1);
|
||||
let _ = app.feed_command(StepInput::Point(DVec3::new(60.0, 50.0, 0.0)));
|
||||
assert_eq!(app.accepted_snaps().len(), 1);
|
||||
assert_eq!(
|
||||
app.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.dimension_acquired_points()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
let other = ViewportFrame {
|
||||
viewport: Handle::new(0xFFFF),
|
||||
..frame
|
||||
};
|
||||
assert!(!app.record_accepted_snap(
|
||||
i,
|
||||
Some(hit(other, line, DVec3::X * 100.0)),
|
||||
Some(other),
|
||||
DVec3::new(60.0, 50.0, 0.0)
|
||||
));
|
||||
point(&mut app, frame, line, DVec3::X * 100.0);
|
||||
let _ = app.feed_command(StepInput::Point(DVec3::new(250.0, 200.0, 0.0)));
|
||||
assert!((displayed(&app.tabs[i].scene.document) - 100.0).abs() < 1e-5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_dimension_typed_sheet_points_keep_paper_units() {
|
||||
let (mut app, _, _) = fixture();
|
||||
let result = app.automation_op(r#"{"op":"run","cmd":"DIMALIGNED 50,50 60,50 60,55"}"#);
|
||||
assert_eq!(result["ok"], true);
|
||||
let doc = &app.tabs[app.active_tab].scene.document;
|
||||
assert!((displayed(doc) - 10.0).abs() < 1e-9);
|
||||
assert!(MeasurementScale::read(&dimension(doc).base().common.extended_data).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_dimension_linear_aligned_and_radial_object_picks() {
|
||||
for (name, expected) in [
|
||||
("DIMLINEAR", 100.0),
|
||||
("DIMALIGNED", 100.0),
|
||||
("DIMRADIUS", 100.0),
|
||||
("DIMDIAMETER", 200.0),
|
||||
] {
|
||||
let (mut app, _, frame) = fixture();
|
||||
let i = app.active_tab;
|
||||
let radial = name == "DIMRADIUS" || name == "DIMDIAMETER";
|
||||
let model = if radial {
|
||||
let mut circle = Circle::new();
|
||||
circle.center = Vector3::new(0.0, 200.0, 0.0);
|
||||
circle.radius = 100.0;
|
||||
app.tabs[i].scene.set_current_layout("Model".into());
|
||||
app.tabs[i].scene.add_entity(EntityType::Circle(circle));
|
||||
app.tabs[i].scene.set_current_layout("Dimensions".into());
|
||||
DVec3::new(100.0, 200.0, 0.0)
|
||||
} else {
|
||||
DVec3::new(50.0, 0.0, 0.0)
|
||||
};
|
||||
let _ = app.dispatch_command(name);
|
||||
if !radial {
|
||||
let _ = app.feed_command(StepInput::Enter);
|
||||
}
|
||||
let result = app
|
||||
.try_dimension_viewport_entity_pick(i, frame.model_to_paper(model), 0.5)
|
||||
.unwrap();
|
||||
let _ = app.apply_cmd_result(result);
|
||||
assert_eq!(
|
||||
app.accepted_snaps().len(),
|
||||
if radial { 1 } else { 2 },
|
||||
"{name}"
|
||||
);
|
||||
let _ = app.feed_command(StepInput::Point(DVec3::new(55.0, 80.0, 0.0)));
|
||||
assert!(
|
||||
(displayed(&app.tabs[i].scene.document) - expected).abs() < 1e-4,
|
||||
"{name}: {}",
|
||||
displayed(&app.tabs[i].scene.document)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_dimension_angular_preserves_angle_without_length_factor() {
|
||||
let (mut app, line, frame) = fixture();
|
||||
let _ = app.dispatch_command("DIMANGULAR");
|
||||
let _ = app.feed_command(StepInput::Enter);
|
||||
point(&mut app, frame, line, DVec3::ZERO);
|
||||
point(&mut app, frame, line, DVec3::X * 100.0);
|
||||
point(&mut app, frame, line, DVec3::Y * 100.0);
|
||||
let _ = app.feed_command(StepInput::Point(DVec3::new(70.0, 70.0, 0.0)));
|
||||
let doc = &app.tabs[app.active_tab].scene.document;
|
||||
assert!((dimension(doc).measurement() - 90.0).abs() < 1e-5);
|
||||
assert!(MeasurementScale::read(&dimension(doc).base().common.extended_data).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_dimension_snap_query_matches_pan_scale_and_twist() {
|
||||
for (height, twist, target) in [
|
||||
(1000.0, 0.0, Vector3::ZERO),
|
||||
(500.0, 0.45, Vector3::new(80.0, 20.0, 0.0)),
|
||||
] {
|
||||
let (mut app, line, initial) = fixture();
|
||||
let i = app.active_tab;
|
||||
let Some(EntityType::Viewport(vp)) =
|
||||
app.tabs[i].scene.document.get_entity_mut(initial.viewport)
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
vp.view_height = height;
|
||||
vp.custom_scale = vp.height / height;
|
||||
vp.twist_angle = twist;
|
||||
vp.view_target = target;
|
||||
let frame = app.tabs[i].scene.viewport_frame(initial.viewport).unwrap();
|
||||
let model = DVec3::X * 100.0;
|
||||
let paper = frame.model_to_paper(model);
|
||||
let bounds = iced::Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: 1000.0,
|
||||
height: 1000.0,
|
||||
};
|
||||
let screen = {
|
||||
let cam = app.tabs[i].scene.camera.borrow();
|
||||
let ndc = cam
|
||||
.view_proj_rte(bounds)
|
||||
.project_point3((paper - cam.eye()).as_vec3());
|
||||
iced::Point::new((ndc.x + 1.0) * 500.0, (1.0 - ndc.y) * 500.0)
|
||||
};
|
||||
app.snapper.snap_enabled = true;
|
||||
let (hit, acquired_frame) = app
|
||||
.paper_viewport_snap(i, screen, (1000.0, 1000.0), paper)
|
||||
.unwrap();
|
||||
assert_eq!(hit.source.unwrap().handle, line);
|
||||
assert!((hit.world - paper).length() < 1e-5, "{hit:?}");
|
||||
let accepted = AcceptedSnap::from_snap(&hit, Some(acquired_frame));
|
||||
assert!((accepted.model_point - model).length() < 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_dimension_eligibility_is_shared_and_uses_clipping() {
|
||||
use acadrust::entities::LwPolyline;
|
||||
use acadrust::types::Vector2;
|
||||
let (mut app, _, first) = fixture();
|
||||
let scene = &mut app.tabs[app.active_tab].scene;
|
||||
let mut overlay = scene.document.get_entity(first.viewport).unwrap().clone();
|
||||
if let EntityType::Viewport(vp) = &mut overlay {
|
||||
vp.id = 3;
|
||||
vp.width = 200.0;
|
||||
}
|
||||
overlay.common_mut().handle = Handle::NULL;
|
||||
let top = scene.add_entity(overlay);
|
||||
let paper = DVec3::new(55.0, 50.0, 0.0);
|
||||
assert_eq!(
|
||||
scene.viewport_frame_at_paper_point(paper).unwrap().viewport,
|
||||
top
|
||||
);
|
||||
assert_eq!(
|
||||
scene
|
||||
.dimension_pick_through_viewport(paper, 0.2)
|
||||
.unwrap()
|
||||
.frame
|
||||
.viewport,
|
||||
top
|
||||
);
|
||||
let mut clip = LwPolyline::from_points(vec![
|
||||
Vector2::new(0.0, 0.0),
|
||||
Vector2::new(100.0, 0.0),
|
||||
Vector2::new(0.0, 100.0),
|
||||
]);
|
||||
clip.is_closed = true;
|
||||
let clip = scene.add_entity(EntityType::LwPolyline(clip));
|
||||
if let Some(EntityType::Viewport(vp)) = scene.document.get_entity_mut(top) {
|
||||
vp.clip_boundary_handle = clip;
|
||||
}
|
||||
assert_eq!(
|
||||
scene.viewport_frame_at_paper_point(paper).unwrap().viewport,
|
||||
first.viewport
|
||||
);
|
||||
assert_eq!(
|
||||
scene
|
||||
.dimension_pick_through_viewport(paper, 0.2)
|
||||
.unwrap()
|
||||
.frame
|
||||
.viewport,
|
||||
first.viewport
|
||||
);
|
||||
if let Some(EntityType::Viewport(vp)) = scene.document.get_entity_mut(first.viewport) {
|
||||
vp.status.is_on = false;
|
||||
}
|
||||
assert!(scene.viewport_frame_at_paper_point(paper).is_none());
|
||||
assert!(scene.dimension_pick_through_viewport(paper, 0.2).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn viewport_dimension_exploded_commit_uses_compensated_text_and_undo() {
|
||||
let (mut app, line, frame) = fixture();
|
||||
let i = app.active_tab;
|
||||
app.tabs[i].scene.document.header.dimension_associativity = 0;
|
||||
finish_aligned(&mut app, line, frame);
|
||||
let doc = &app.tabs[i].scene.document;
|
||||
assert!(!doc
|
||||
.entities()
|
||||
.any(|e| matches!(e, EntityType::Dimension(_))));
|
||||
let texts: Vec<_> = doc
|
||||
.entities()
|
||||
.filter_map(|e| match e {
|
||||
EntityType::Text(t) => Some(t.value.clone()),
|
||||
EntityType::MText(t) => Some(t.value.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert!(texts.iter().any(|t| t.contains("100")), "{texts:?}");
|
||||
app.undo_steps(1);
|
||||
assert_eq!(app.tabs[i].scene.document.header.dimension_associativity, 0);
|
||||
app.redo_steps(1);
|
||||
assert!(app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.any(|e| matches!(e, EntityType::Text(_) | EntityType::MText(_))));
|
||||
}
|
||||
Reference in new issue
Block a user