Standardize parametric constraints and interoperability

This commit is contained in:
Hakan Seven committed 2026-09-15 03:19:52 +03:00
1 parent 753df67ee4
commit d738ebfcc6
41 files changed
+10038 -7847

No files matched your search

+66 -19
View File
@@ -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;
@@ -318,7 +318,9 @@ impl OpenCADStudio {
self.tabs[i].scene.clear();
self.tabs[i].scene.document = doc;
self.tabs[i].scene.load_named_parameters_from_document();
self.tabs[i].scene.load_sketch_constraints_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,9 @@ 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")
);
}
}
+1365 -765
View File
File diff suppressed because it is too large. Load diff
+290 -191
View File
@@ -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);
}
@@ -1090,10 +1121,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 inferred = self.tabs[i]
.scene
.inferred_sketch_constraints(scope, &handles);
.inferred_parametric_constraints(scope, &handles);
if inferred.is_empty() {
self.command_line
.push_output("No supported geometric relations were found.");
@@ -1101,20 +1132,22 @@ impl OpenCADStudio {
}
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 pending = self.begin_undo(i, "Auto constrain", handles.len(), true);
self.tabs[i]
.scene
.record_undo_sketch_constraints_before(scope, before);
.record_undo_parametric_constraints_before(scope, before);
let count = inferred.len();
for (kind, refs) in inferred {
self.tabs[i]
.scene
.sketch_constraint_set_mut(scope)
.parametric_constraint_set_mut(scope)
.add(kind, refs, None);
}
let changes: Vec<_> = handles
@@ -1142,14 +1175,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 +1198,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 +1211,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 +1248,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,15 +1285,15 @@ 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,
}));
@@ -1280,7 +1314,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 +1328,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 +1359,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 +1390,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 +1417,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 +1451,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 +1475,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 +1510,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 +1524,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 +1691,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 +1801,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 +1839,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 +1995,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 +2038,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 +2078,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 +2097,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 +2182,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 +2206,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);
+59 -65
View File
@@ -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
View File
@@ -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]
+6 -13
View File
@@ -7,6 +7,7 @@ pub(crate) fn automation_action_names() -> &'static [&'static str] {
pub(crate) mod config;
#[cfg(not(target_arch = "wasm32"))]
pub use automation::{export_headless, serve};
mod annotation_data;
mod command_driver;
pub(crate) mod commands;
mod document;
@@ -30,7 +31,6 @@ mod startup;
mod style_ops;
mod text_inline;
mod tolerance_dialog;
mod annotation_data;
mod update;
mod view;
mod visibility;
@@ -260,12 +260,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;
@@ -552,11 +552,7 @@ 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,
@@ -2118,8 +2114,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),
@@ -2547,9 +2541,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.
@@ -3636,7 +3630,6 @@ impl OpenCADStudio {
dimension_continue_mode: 1,
backup_on_save: true,
file_assoc_enabled: true,
write_dwg_native_constraints: false,
show_constraint_values: true,
savetime_min: 10,
default_bg_color: None,
+363 -304
View File
File diff suppressed because it is too large. Load diff
-5
View File
@@ -283,10 +283,6 @@ 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
@@ -418,7 +414,6 @@ impl Default for UserSettings {
textfill: true,
backup_on_save: true,
file_assoc_enabled: true,
write_dwg_native_constraints: false,
show_constraint_values: true,
savetime_min: 10,
default_save_format: crate::io::DEFAULT_SAVE_FORMAT.to_string(),
+996 -1015
View File
File diff suppressed because it is too large. Load diff
+535 -487
View File
File diff suppressed because it is too large. Load diff
+419 -411
View File
File diff suppressed because it is too large. Load diff
+1140 -1215
View File
File diff suppressed because it is too large. Load diff
-1
View File
@@ -201,7 +201,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,