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,
+125 -55
View File
@@ -25,10 +25,15 @@ pub enum HatchEditOperation {
style: Option<acadrust::entities::HatchStyleType>,
annotative: Option<bool>,
},
RecreateBoundary { associate: bool, region: bool },
RecreateBoundary {
associate: bool,
region: bool,
},
BeginAssociate,
AssociatePaths(Vec<acadrust::entities::BoundaryPath>),
DrawOrderBoundary { above: bool },
DrawOrderBoundary {
above: bool,
},
Separate,
AddBoundaries(Vec<Handle>),
RemoveBoundaries(Vec<Handle>),
@@ -102,8 +107,7 @@ impl WorkingPlane {
pub fn angle(self, from: DVec3, to: DVec3) -> Option<f64> {
let direction = self.vector_to_local(to - from);
(direction.x.hypot(direction.y) > f64::EPSILON)
.then(|| direction.y.atan2(direction.x))
(direction.x.hypot(direction.y) > f64::EPSILON).then(|| direction.y.atan2(direction.x))
}
pub fn to_world_transform(self) -> acadrust::types::Transform {
@@ -493,16 +497,18 @@ impl CadCommand for UserRegCommand {
fn prompt(&self) -> String {
match self.slot {
None => crate::t!("%{name} which register? [1-5]:", name = self.name)
.into_owned(),
Some(n) => crate::t!("%{name}%{slot} new value:", name = self.name, slot = n)
.into_owned(),
None => crate::t!("%{name} which register? [1-5]:", name = self.name).into_owned(),
Some(n) => {
crate::t!("%{name}%{slot} new value:", name = self.name, slot = n).into_owned()
}
}
}
fn options(&self) -> Vec<CmdOption> {
match self.slot {
None => (1..=5).map(|n| CmdOption::new(&n.to_string(), &n.to_string())).collect(),
None => (1..=5)
.map(|n| CmdOption::new(&n.to_string(), &n.to_string()))
.collect(),
Some(_) => Vec::new(),
}
}
@@ -593,9 +599,10 @@ fn match_cmd_option<'a>(
return None;
}
// 1. Exact match on keyword or label (case-insensitive)
if let Some(opt) = options.iter().find(|(label, k, _)| {
k.eq_ignore_ascii_case(&up) || label.eq_ignore_ascii_case(t)
}) {
if let Some(opt) = options
.iter()
.find(|(label, k, _)| k.eq_ignore_ascii_case(&up) || label.eq_ignore_ascii_case(t))
{
return Some(opt);
}
// 2. Unambiguous prefix match on keyword or label (e.g. "A" -> "ABOVE", "L" -> "LEFT")
@@ -650,8 +657,7 @@ impl CadCommand for KeywordCommand {
// Consumed inputs that keep prompting return `Some(NeedPoint)` —
// `None` would hand the same text to the command a second time.
None => {
let Some((_, keyword, value_prompt)) = match_cmd_option(&self.options, t)
else {
let Some((_, keyword, value_prompt)) = match_cmd_option(&self.options, t) else {
// Unknown verb — keep prompting rather than dispatch garbage.
return Some(CmdResult::NeedPoint);
};
@@ -809,7 +815,7 @@ impl CadCommand for Mid2PointCommand {
WireModel::CYAN,
false,
));
// Midpoint marker (AutoCAD style triangle glyph)
// Midpoint marker (triangle glyph)
let mid = (first + pt) * 0.5;
let dist = (pt - first).length();
let s = (dist * 0.02).clamp(0.5, 10.0);
@@ -834,7 +840,6 @@ inventory::submit!(CommandRegistration {
names: &["MTP", "M2P"],
});
/// Generic interactive front-end for a keyword command that operates on the
/// current selection (CHPROP, ADJUST, XDATA, UNDERLAY, DRAWORDER…). If nothing
/// is selected when it starts it first gathers a selection (Enter confirms),
@@ -942,8 +947,7 @@ impl CadCommand for SelectThenKeywordCommand {
}
}
None => {
let Some((_, keyword, value_prompt)) = match_cmd_option(&self.options, t)
else {
let Some((_, keyword, value_prompt)) = match_cmd_option(&self.options, t) else {
// Unknown verb — consumed, keep prompting (`None` would
// feed the same text to the command a second time).
return Some(CmdResult::NeedPoint);
@@ -1405,7 +1409,10 @@ pub enum CmdResult {
/// Copy selected entities with a transform; command stays active for more copies.
CopySelected(Vec<Handle>, EntityTransform),
/// Store selected entities in the shared clipboard; command stays active.
CopyToClipboard { handles: Vec<Handle>, base: DVec3 },
CopyToClipboard {
handles: Vec<Handle>,
base: DVec3,
},
/// Commit a hatch fill (stored in Scene::hatches, not the DXF document).
CommitHatch(HatchModel),
/// Commit a hatch with the selected hatch's entity colour and transparency.
@@ -1430,22 +1437,25 @@ pub enum CmdResult {
/// Erase `handle` and replace with new entities; command stays active.
ReplaceEntity(Handle, Vec<EntityType>),
/// Update one entity in place, preserve its handle, and end the command.
UpdateEntityAndFinish { handle: Handle, entity: EntityType },
UpdateEntityAndFinish {
handle: Handle,
entity: EntityType,
},
/// Replace / delete multiple entities and add new ones; command ends.
/// Each pair: (handle_to_erase, replacement_entities) — empty vec = delete only.
ReplaceMany(Vec<(Handle, Vec<EntityType>)>, Vec<EntityType>),
/// Replace several entities as one undo step while keeping the command active.
ReplaceManyContinue(Vec<(Handle, Vec<EntityType>)>),
/// Add a persistent parametric constraint to the current scope's
/// `SketchConstraintSet` and trigger its
/// `ParametricConstraintSet` and trigger its
/// first solve; end the command. Unlike `ReplaceMany`/`CommitEntity`,
/// nothing here is geometry to add or replace directly — the host adds
/// the constraint record, then re-solves through `Scene::bump_entities`'s
/// existing chain (`refresh_sketch_constraints`) the same way any later
/// existing chain (`refresh_parametric_constraints`) the same way any later
/// edit to these entities will.
AddSketchConstraint {
kind: crate::scene::sketch_constraints::ConstraintKind,
refs: Vec<crate::scene::sketch_constraints::SketchRef>,
AddParametricConstraint {
kind: crate::scene::parametric_constraints::ConstraintKind,
refs: Vec<crate::scene::parametric_constraints::ParametricRef>,
/// The typed target for a dimensional kind (Distance/Angle/Radius) —
/// a literal number, or a named-parameter reference recognized by
/// `DistanceConstraintCommand`/`AngleConstraintCommand::
@@ -1468,7 +1478,7 @@ pub enum CmdResult {
/// (`crate::modules::parametric::point_on_entity`) between one
/// picked point and `target`, a whole entity selected before the tool
/// ran. Like `AddCoincidentConstraint`, the host resolves `point` via
/// `sketch_constraints::nearest_sketch_point` (a `CadCommand` has no
/// `parametric_constraints::nearest_parametric_point` (a `CadCommand` has no
/// document access) and, for `CenterPoint` specifically, addresses
/// `target` via its center marker rather than as a whole entity — see
/// `ConstraintKind::CenterPoint`'s own doc comment for why it's the
@@ -1477,14 +1487,14 @@ pub enum CmdResult {
AddPointOnEntityConstraint {
point: DVec3,
target: Handle,
kind: crate::scene::sketch_constraints::ConstraintKind,
kind: crate::scene::parametric_constraints::ConstraintKind,
label: &'static str,
},
/// Add a persistent `EqualDistance` constraint
/// (`crate::modules::parametric::equal_distance`): the distance
/// between `points[0]`/`points[1]` equals the distance between
/// `points[2]`/`points[3]`. The host resolves each point via
/// `sketch_constraints::nearest_sketch_point`, same reasoning as
/// `parametric_constraints::nearest_parametric_point`, same reasoning as
/// `AddCoincidentConstraint`.
AddEqualDistanceConstraint {
points: [DVec3; 4],
@@ -1516,7 +1526,10 @@ pub enum CmdResult {
/// it owns the document needed for the table-style lookup — honors
/// content locks, and launches the cell editor, re-prompting the
/// command when the pick misses a cell.
EditTableCell { handle: Handle, point: DVec3 },
EditTableCell {
handle: Handle,
point: DVec3,
},
/// Cancel because the active drawing space changed. Cleanup is identical
/// to `Cancel`, but the host reports the context change explicitly.
CancelForSpaceChange,
@@ -1539,13 +1552,24 @@ pub enum CmdResult {
/// `Relaunch` it does not touch the selection.
Dispatch(String),
/// Move `dest` entities to the layer of the `src` entity; end command.
MatchEntityLayer { dest: Vec<Handle>, src: Handle },
MatchEntityLayer {
dest: Vec<Handle>,
src: Handle,
},
/// Copy all visual properties (layer/color/linetype/lineweight) from `src` to `dest`; end command.
MatchProperties { dest: Vec<Handle>, src: Handle },
MatchProperties {
dest: Vec<Handle>,
src: Handle,
},
/// Create a named group from the given entity handles; end command.
CreateGroup { handles: Vec<Handle>, name: String },
CreateGroup {
handles: Vec<Handle>,
name: String,
},
/// Dissolve all groups that contain any of the given handles; end command.
DeleteGroups { handles: Vec<Handle> },
DeleteGroups {
handles: Vec<Handle>,
},
/// Freeze or thaw layers by name in the given viewport; command stays active.
VpLayerUpdate {
vp_handle: Handle,
@@ -1553,9 +1577,14 @@ pub enum CmdResult {
thaw: Vec<String>,
},
/// Paste clipboard entities translated so their centroid lands at `base_pt`; end command.
PasteClipboard { base_pt: DVec3 },
PasteClipboard {
base_pt: DVec3,
},
/// Zoom the model-space camera to fit the given corner points; end command.
ZoomToWindow { p1: DVec3, p2: DVec3 },
ZoomToWindow {
p1: DVec3,
p2: DVec3,
},
/// Print a measurement result to the command line and end the command.
Measurement(String),
/// Print a measurement result and keep the command active.
@@ -1567,20 +1596,36 @@ pub enum CmdResult {
/// Clear the current selection and keep the command active at its updated step.
DeselectAndContinue,
/// Break `handle` at points `p1` and `p2`; replace with computed fragments.
BreakEntity { handle: Handle, p1: DVec3, p2: DVec3 },
BreakEntity {
handle: Handle,
p1: DVec3,
p2: DVec3,
},
/// Attempt to join the given entities into fewer merged entities.
JoinEntities(Vec<Handle>),
/// Join candidates into an explicitly selected source.
JoinToSource { source: Handle, handles: Vec<Handle> },
JoinToSource {
source: Handle,
handles: Vec<Handle>,
},
/// Apply a polyline-edit operation to one entity; keep command active.
PeditOp {
handle: Handle,
op: crate::modules::draw::modify::pedit::PeditOp,
},
/// Place Point entities at N equal intervals along the entity.
DivideEntity { handle: Handle, n: usize, marker: Option<CurveMarker> },
DivideEntity {
handle: Handle,
n: usize,
marker: Option<CurveMarker>,
},
/// Place Point entities at `segment_length` intervals along the entity.
MeasureEntity { handle: Handle, segment_length: f64, pick_point: DVec3, marker: Option<CurveMarker> },
MeasureEntity {
handle: Handle,
segment_length: f64,
pick_point: DVec3,
marker: Option<CurveMarker>,
},
/// Extend/trim a Line or Arc by the given mode; end command.
LengthenEntity {
handle: Handle,
@@ -1596,7 +1641,10 @@ pub enum CmdResult {
scale: f64,
},
/// Set the plot window on the active layout's PlotSettings.
SetPlotWindow { p1: DVec3, p2: DVec3 },
SetPlotWindow {
p1: DVec3,
p2: DVec3,
},
/// Create a paper-space viewport. `preserve_view` keeps an explicitly
/// selected/defined view instead of applying the normal model-extents fit.
MviewCreate {
@@ -1623,7 +1671,10 @@ pub enum CmdResult {
/// Quick-print the bounding box of the given selected entities to a PDF.
QuickPrint(Vec<Handle>),
/// Replace the text content of a Text/MText entity in-place.
DdeditEntity { handle: Handle, new_text: String },
DdeditEntity {
handle: Handle,
new_text: String,
},
/// Apply one DIMEDIT operation to every selected dimension.
EditDimensions {
handles: Vec<Handle>,
@@ -1631,7 +1682,9 @@ pub enum CmdResult {
},
/// Open the in-place editor (plain box or rich MText editor, per type) for
/// a text-bearing entity picked by a command such as DDEDIT.
EditTextEntity { handle: Handle },
EditTextEntity {
handle: Handle,
},
/// Open the in-place MText editor (formatting toolbar + multi-line text
/// area with live viewport preview). `handle` is `Some` when editing an
/// existing MText, `None` when creating a new one at `pos`.
@@ -1784,7 +1837,9 @@ pub enum CmdResult {
/// INSERT landed on a block that has AttributeDefinitions.
/// The host should look up the attdefs for `block_name` from the document
/// and call `attreq_set_attdefs()` on the command, then loop on text input.
AttreqNeeded { block_name: String },
AttreqNeeded {
block_name: String,
},
/// Add a command-owned "live" entity to the document mid-command and hand
/// its assigned handle back to the active command via `set_live_handle()`.
/// One undo snapshot is pushed here, so the whole in-progress object reverts
@@ -1808,7 +1863,9 @@ pub enum CmdResult {
/// PLINE's Undo popping back below the two vertices an entity needs.
RemoveLiveEntity(Handle),
/// Suspends command execution, moves it to suspended_cmd, and opens the text editor for the given handle.
SuspendForTextEdit { handle: Handle },
SuspendForTextEdit {
handle: Handle,
},
/// Requests a standard document-level undo while keeping the command active.
UndoDocument,
/// Sets the TEXTEDITMODE system variable and ends the command.
@@ -2048,7 +2105,6 @@ pub struct CurveMarker {
pub plane: WorkingPlane,
}
/// Current screen projection and configured aperture for point-feature picking.
#[derive(Clone, Copy)]
pub struct PointPickContext {
@@ -2060,10 +2116,16 @@ pub struct PointPickContext {
pub trait CadCommand: Send {
/// Preserve source appearance for commands that extract existing entities.
fn preserve_commit_style(&self) -> bool { false }
fn nested_copy_bind_setting(&self) -> Option<bool> { None }
fn preserve_commit_style(&self) -> bool {
false
}
fn nested_copy_bind_setting(&self) -> Option<bool> {
None
}
/// Symbol localization is committed together with the extracted entities.
fn nested_copy_symbol_names(&self) -> Option<&acadrust::nested_copy::NestedCopySymbolNames> { None }
fn nested_copy_symbol_names(&self) -> Option<&acadrust::nested_copy::NestedCopySymbolNames> {
None
}
/// Keep the layer already carried by entities committed by this command
/// instead of replacing it with the current drawing layer.
fn preserve_commit_layer(&self) -> bool {
@@ -2131,7 +2193,9 @@ pub trait CadCommand: Send {
fn on_point(&mut self, pt: DVec3) -> CmdResult;
/// Opt in only while a point input selects an existing point feature.
fn wants_point_pick_context(&self) -> bool { false }
fn wants_point_pick_context(&self) -> bool {
false
}
/// Refreshed for each point input so zoom and viewport changes are reflected.
fn set_point_pick_context(&mut self, _context: Option<PointPickContext>) {}
@@ -2203,7 +2267,10 @@ pub trait CadCommand: Send {
}
fn on_deferred_entity_hover(
&mut self, _scene: &Scene, _handle: Option<Handle>, _point: DVec3,
&mut self,
_scene: &Scene,
_handle: Option<Handle>,
_point: DVec3,
) -> Vec<WireModel> {
Vec::new()
}
@@ -2417,7 +2484,9 @@ pub trait CadCommand: Send {
}
/// Exclude locked-layer entities from injected selection geometry.
fn selection_entities_exclude_locked(&self) -> bool { false }
fn selection_entities_exclude_locked(&self) -> bool {
false
}
fn inject_selection_entities(&mut self, _entities: Vec<SelectionEntity>) {}
@@ -2425,9 +2494,7 @@ pub trait CadCommand: Send {
None
}
fn hatch_preview_models(
&self,
) -> Option<Vec<crate::scene::model::hatch_model::HatchModel>> {
fn hatch_preview_models(&self) -> Option<Vec<crate::scene::model::hatch_model::HatchModel>> {
None
}
@@ -2632,7 +2699,10 @@ mod constraint_registry_tests {
"DCONSTRAINT",
"ACONSTRAINT",
] {
assert!(names.contains(&id), "{id} is missing from the command registry");
assert!(
names.contains(&id),
"{id} is missing from the command registry"
);
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@
//! comment for the same constraint on `DCONSTRAINT`/`ACONSTRAINT`), so this
//! command only accumulates the two raw points `on_point` receives and hands
//! them back as `CmdResult::AddCoincidentConstraint` — the host resolves
//! each point to a real `SketchRef` (`sketch_constraints::nearest_sketch_point`)
//! each point to a real `ParametricRef` (`parametric_constraints::nearest_parametric_point`)
//! and adds the constraint, the same split `ReassociateCenterMark` already
//! uses for a similar reason.
+2 -2
View File
@@ -7,7 +7,7 @@
//! endpoint) so it doesn't need interactive picking; the point side does
//! (which of a line's two endpoints, or some other addressable point), so
//! this command accumulates just that one point pick and hands it back for
//! the host to resolve via `nearest_sketch_point` — the same split
//! the host to resolve via `nearest_parametric_point` — the same split
//! `CoincidentConstraintCommand` uses and for the same reason (`CadCommand`
//! has no document access).
@@ -15,7 +15,7 @@ use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::sketch_constraints::ConstraintKind;
use crate::scene::parametric_constraints::ConstraintKind;
use acadrust::types::Handle;
pub mod center_point_tool {
+4 -10
View File
@@ -64,9 +64,7 @@ pub mod equal {
ToolDef {
id: "ECONSTRAINT",
label: "Equal",
icon: IconKind::Svg(include_bytes!(
"../../../assets/icons/constrain/equal.svg"
)),
icon: IconKind::Svg(include_bytes!("../../../assets/icons/constrain/equal.svg")),
event: ModuleEvent::Command("ECONSTRAINT".to_string()),
}
}
@@ -105,7 +103,7 @@ pub mod concentric {
/// "the line passes through the circle's center" (a circle's radius is
/// always normal to its own tangent), so it's built from the same
/// `PointOnLine` primitive `PointOnCurve` already uses for a point-on-line
/// case (`sketch_solve.rs`). Distinct from `Perpendicular`, which only
/// case (`parametric_solve.rs`). Distinct from `Perpendicular`, which only
/// covers line-to-line.
pub mod normal {
use super::*;
@@ -113,9 +111,7 @@ pub mod normal {
ToolDef {
id: "NRCONSTRAINT",
label: "Normal",
icon: IconKind::Svg(include_bytes!(
"../../../assets/icons/constrain/normal.svg"
)),
icon: IconKind::Svg(include_bytes!("../../../assets/icons/constrain/normal.svg")),
event: ModuleEvent::Command("NRCONSTRAINT".to_string()),
}
}
@@ -141,9 +137,7 @@ pub mod fixed {
ToolDef {
id: "FXCONSTRAINT",
label: "Fixed",
icon: IconKind::Svg(include_bytes!(
"../../../assets/icons/constrain/fixed.svg"
)),
icon: IconKind::Svg(include_bytes!("../../../assets/icons/constrain/fixed.svg")),
event: ModuleEvent::Command("FXCONSTRAINT".to_string()),
}
}
+116 -155
View File
@@ -4,8 +4,8 @@
//!
//! These commands don't solve anything themselves. A `CadCommand`'s `on_text_input` only
//! gets `&mut self` — no document access — so it can't add a
//! `SketchConstraint` to the scene directly; instead these commands hand the
//! typed value back as `CmdResult::AddSketchConstraint`, and the host (which
//! `ParametricConstraint` to the scene directly; instead these commands hand the
//! typed value back as `CmdResult::AddParametricConstraint`, and the host (which
//! does have `&mut Scene`) adds the record and solves it via the same
//! `Scene::bump_entities` path any later edit to these entities will use too.
//! `default_value` (what the prompt shows in `<...>`, and what a bare Enter
@@ -19,7 +19,7 @@ use glam::DVec3;
use crate::command::{CadCommand, CmdResult, InputKind};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::named_parameters::DrivingValue;
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef};
use crate::scene::Scene;
/// Recognizes a typed prompt token as either a numeric literal or a
@@ -28,7 +28,7 @@ use crate::scene::Scene;
/// ::new`, which already has `&Scene`) since `on_text_input` itself has no
/// document access — the same constraint `default_value` already works
/// around. Only *existence* is checked here; the actual value is resolved
/// fresh at solve time (`sketch_solve::build_constraint`), consistent with
/// fresh at solve time (`parametric_solve::build_constraint`), consistent with
/// this project's "no incremental/cached resolution" approach throughout.
fn parse_driving_value(text: &str, known_names: &[String]) -> Option<DrivingValue> {
let text = text.trim();
@@ -64,17 +64,70 @@ pub mod dimensional_tools {
use super::*;
fn command(id: &'static str, label: &'static str, icon: &'static [u8]) -> ToolDef {
ToolDef { id, label, icon: IconKind::Svg(icon), event: ModuleEvent::Command(id.to_string()) }
ToolDef {
id,
label,
icon: IconKind::Svg(icon),
event: ModuleEvent::Command(id.to_string()),
}
}
pub fn linear() -> ToolDef { command("DCLINEAR", "Linear", include_bytes!("../../../assets/icons/dim_linear.svg")) }
pub fn horizontal() -> ToolDef { command("DCHORIZONTAL", "Horizontal", include_bytes!("../../../assets/icons/constrain/distance_x.svg")) }
pub fn vertical() -> ToolDef { command("DCVERTICAL", "Vertical", include_bytes!("../../../assets/icons/constrain/distance_y.svg")) }
pub fn aligned() -> ToolDef { command("DCALIGNED", "Aligned", include_bytes!("../../../assets/icons/dim_aligned.svg")) }
pub fn angular() -> ToolDef { command("DCANGULAR", "Angular", include_bytes!("../../../assets/icons/dim_angular.svg")) }
pub fn radius() -> ToolDef { command("DCRADIUS", "Radius", include_bytes!("../../../assets/icons/dim_radius.svg")) }
pub fn diameter() -> ToolDef { command("DCDIAMETER", "Diameter", include_bytes!("../../../assets/icons/dim_diameter.svg")) }
pub fn convert() -> ToolDef { command("DCCONVERT", "Convert", include_bytes!("../../../assets/icons/constrain/convert.svg")) }
pub fn linear() -> ToolDef {
command(
"DCLINEAR",
"Linear",
include_bytes!("../../../assets/icons/dim_linear.svg"),
)
}
pub fn horizontal() -> ToolDef {
command(
"DCHORIZONTAL",
"Horizontal",
include_bytes!("../../../assets/icons/constrain/distance_x.svg"),
)
}
pub fn vertical() -> ToolDef {
command(
"DCVERTICAL",
"Vertical",
include_bytes!("../../../assets/icons/constrain/distance_y.svg"),
)
}
pub fn aligned() -> ToolDef {
command(
"DCALIGNED",
"Aligned",
include_bytes!("../../../assets/icons/dim_aligned.svg"),
)
}
pub fn angular() -> ToolDef {
command(
"DCANGULAR",
"Angular",
include_bytes!("../../../assets/icons/dim_angular.svg"),
)
}
pub fn radius() -> ToolDef {
command(
"DCRADIUS",
"Radius",
include_bytes!("../../../assets/icons/dim_radius.svg"),
)
}
pub fn diameter() -> ToolDef {
command(
"DCDIAMETER",
"Diameter",
include_bytes!("../../../assets/icons/dim_diameter.svg"),
)
}
pub fn convert() -> ToolDef {
command(
"DCCONVERT",
"Convert",
include_bytes!("../../../assets/icons/constrain/convert.svg"),
)
}
}
pub mod angle_tool {
@@ -83,9 +136,7 @@ pub mod angle_tool {
ToolDef {
id: "ACONSTRAINT",
label: "Angle",
icon: IconKind::Svg(include_bytes!(
"../../../assets/icons/constrain/angle.svg"
)),
icon: IconKind::Svg(include_bytes!("../../../assets/icons/constrain/angle.svg")),
event: ModuleEvent::Command("ACONSTRAINT".to_string()),
}
}
@@ -104,14 +155,10 @@ pub enum DistanceMode {
X,
Y,
Diameter,
/// Arc length — only offered when `is_arc` (not a plain Circle, which
/// has no "arc length" distinct from its circumference).
ArcLength,
}
/// Constrains a single line's length (or its X/Y-only component) between
/// its two endpoints, or a circle/arc's radius (or diameter, or — for an
/// arc specifically — its swept arc length), to a typed target value.
/// its two endpoints, or a circle/arc's radius or diameter, to a typed target value.
pub struct DistanceConstraintCommand {
command_name: &'static str,
handle: Handle,
@@ -119,16 +166,10 @@ pub struct DistanceConstraintCommand {
/// (Distance/DistanceX/DistanceY) — decided once at construction from
/// the entity's type.
is_circle: bool,
/// Whether `handle` is specifically an Arc (as opposed to a full
/// Circle) — gates the `[Arclength]` prompt option.
is_arc: bool,
mode: DistanceMode,
default_value: f64,
default_x: f64,
default_y: f64,
/// The entity's current arc length, for `ArcLength` mode's `<default>`
/// — meaningless (left at 0) unless `is_arc`.
default_arc_length: f64,
/// Snapshot of `Scene::named_parameters`' names at construction time —
/// see `parse_driving_value`'s doc comment for why a snapshot.
known_param_names: Vec<String>,
@@ -147,17 +188,16 @@ impl DistanceConstraintCommand {
requested_mode: DistanceMode,
) -> Option<Self> {
let entity = scene.document.get_entity(handle)?;
let (is_circle, is_arc, default_value, default_x, default_y, default_arc_length) =
match entity {
EntityType::Line(l) => {
let dx = l.end.x - l.start.x;
let dy = l.end.y - l.start.y;
(false, false, (dx * dx + dy * dy).sqrt(), dx, dy, 0.0)
}
EntityType::Circle(c) => (true, false, c.radius, 0.0, 0.0, 0.0),
EntityType::Arc(a) => (true, true, a.radius, 0.0, 0.0, a.arc_length()),
_ => return None,
};
let (is_circle, default_value, default_x, default_y) = match entity {
EntityType::Line(l) => {
let dx = l.end.x - l.start.x;
let dy = l.end.y - l.start.y;
(false, (dx * dx + dy * dy).sqrt(), dx, dy)
}
EntityType::Circle(c) => (true, c.radius, 0.0, 0.0),
EntityType::Arc(a) => (true, a.radius, 0.0, 0.0),
_ => return None,
};
let known_param_names = scene
.named_parameters()
.iter()
@@ -165,7 +205,11 @@ impl DistanceConstraintCommand {
.collect();
let mode = match requested_mode {
DistanceMode::Linear if !is_circle => {
if default_x.abs() >= default_y.abs() { DistanceMode::X } else { DistanceMode::Y }
if default_x.abs() >= default_y.abs() {
DistanceMode::X
} else {
DistanceMode::Y
}
}
DistanceMode::Aligned if !is_circle => DistanceMode::Aligned,
DistanceMode::Radius if is_circle => DistanceMode::Radius,
@@ -178,12 +222,10 @@ impl DistanceConstraintCommand {
command_name,
handle,
is_circle,
is_arc,
mode,
default_value,
default_x,
default_y,
default_arc_length,
known_param_names,
})
}
@@ -198,46 +240,41 @@ impl DistanceConstraintCommand {
let (kind, refs, label) = match (self.is_circle, self.mode) {
(true, DistanceMode::Diameter) => (
ConstraintKind::Diameter,
vec![SketchRef::whole(self.handle)],
vec![ParametricRef::whole(self.handle)],
"Diameter constraint",
),
(true, DistanceMode::ArcLength) if self.is_arc => (
ConstraintKind::ArcLength,
vec![SketchRef::whole(self.handle)],
"Arc length constraint",
),
(true, DistanceMode::Auto | DistanceMode::Radius) => (
ConstraintKind::Radius,
vec![SketchRef::whole(self.handle)],
vec![ParametricRef::whole(self.handle)],
"Radius constraint",
),
(false, DistanceMode::X) => (
ConstraintKind::DistanceX,
vec![
SketchRef::point(self.handle, 0),
SketchRef::point(self.handle, 1),
ParametricRef::point(self.handle, 0),
ParametricRef::point(self.handle, 1),
],
"DistanceX constraint",
),
(false, DistanceMode::Y) => (
ConstraintKind::DistanceY,
vec![
SketchRef::point(self.handle, 0),
SketchRef::point(self.handle, 1),
ParametricRef::point(self.handle, 0),
ParametricRef::point(self.handle, 1),
],
"DistanceY constraint",
),
(false, DistanceMode::Auto | DistanceMode::Aligned | DistanceMode::Linear) => (
ConstraintKind::Distance,
vec![
SketchRef::point(self.handle, 0),
SketchRef::point(self.handle, 1),
ParametricRef::point(self.handle, 0),
ParametricRef::point(self.handle, 1),
],
"Distance constraint",
),
_ => return None,
};
Some(CmdResult::AddSketchConstraint {
Some(CmdResult::AddParametricConstraint {
kind,
refs,
driving_param: Some(target),
@@ -257,16 +294,6 @@ impl CadCommand for DistanceConstraintCommand {
"Specify diameter <{:.4}> or [Radius]: ",
self.default_value * 2.0
),
(true, DistanceMode::ArcLength) if self.is_arc => {
format!(
"Specify arc length <{:.4}> or [Radius]: ",
self.default_arc_length
)
}
(true, DistanceMode::Auto | DistanceMode::Radius) if self.is_arc => format!(
"Specify distance <{:.4}> or [Diameter/Arclength]: ",
self.default_value
),
(true, DistanceMode::Auto | DistanceMode::Radius) => format!(
"Specify distance <{:.4}> or [Diameter]: ",
self.default_value
@@ -292,10 +319,12 @@ impl CadCommand for DistanceConstraintCommand {
fn on_enter(&mut self) -> CmdResult {
let default = match self.mode {
DistanceMode::Diameter => self.default_value * 2.0,
DistanceMode::ArcLength => self.default_arc_length,
DistanceMode::X => self.default_x,
DistanceMode::Y => self.default_y,
DistanceMode::Auto | DistanceMode::Aligned | DistanceMode::Linear | DistanceMode::Radius => self.default_value,
DistanceMode::Auto
| DistanceMode::Aligned
| DistanceMode::Linear
| DistanceMode::Radius => self.default_value,
};
self.build(DrivingValue::Literal(default))
.unwrap_or(CmdResult::Cancel)
@@ -314,10 +343,6 @@ impl CadCommand for DistanceConstraintCommand {
self.mode = DistanceMode::Diameter;
return Some(CmdResult::NeedPoint);
}
(true, "A" | "ARCLENGTH") if self.is_arc => {
self.mode = DistanceMode::ArcLength;
return Some(CmdResult::NeedPoint);
}
(true, "R" | "RADIUS") => {
self.mode = DistanceMode::Radius;
return Some(CmdResult::NeedPoint);
@@ -390,11 +415,11 @@ impl AngleConstraintCommand {
if matches!(&target, DrivingValue::Literal(value) if !value.is_finite()) {
return None;
}
Some(CmdResult::AddSketchConstraint {
Some(CmdResult::AddParametricConstraint {
kind: ConstraintKind::Angle,
refs: vec![
SketchRef::whole(self.fixed_handle),
SketchRef::whole(self.moving_handle),
ParametricRef::whole(self.fixed_handle),
ParametricRef::whole(self.moving_handle),
],
driving_param: Some(target),
label: "Angle constraint",
@@ -437,8 +462,15 @@ impl CadCommand for AngleConstraintCommand {
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration {
names: &[
"DCONSTRAINT", "ACONSTRAINT", "DCLINEAR", "DCHORIZONTAL", "DCVERTICAL",
"DCALIGNED", "DCANGULAR", "DCRADIUS", "DCDIAMETER",
"DCONSTRAINT",
"ACONSTRAINT",
"DCLINEAR",
"DCHORIZONTAL",
"DCVERTICAL",
"DCALIGNED",
"DCANGULAR",
"DCRADIUS",
"DCDIAMETER",
]
});
@@ -509,17 +541,6 @@ mod tests {
))
}
fn add_arc(scene: &mut Scene) -> Handle {
scene.add_entity(EntityType::Arc(
acadrust::entities::Arc::from_center_radius_angles(
acadrust::types::Vector3::new(0.0, 0.0, 0.0),
3.0,
0.0,
std::f64::consts::FRAC_PI_2,
),
))
}
#[test]
fn x_keyword_switches_mode_and_then_builds_a_distance_x_constraint() {
let mut scene = Scene::new();
@@ -537,7 +558,7 @@ mod tests {
);
match cmd.on_text_input("10") {
Some(CmdResult::AddSketchConstraint {
Some(CmdResult::AddParametricConstraint {
kind,
driving_param: Some(DrivingValue::Literal(v)),
..
@@ -545,7 +566,7 @@ mod tests {
assert_eq!(kind, ConstraintKind::DistanceX);
assert_eq!(v, 10.0);
}
_ => panic!("expected an AddSketchConstraint{{DistanceX}}, got a different result"),
_ => panic!("expected an AddParametricConstraint{{DistanceX}}, got a different result"),
}
}
@@ -558,7 +579,7 @@ mod tests {
assert!(matches!(cmd.on_text_input("X"), Some(CmdResult::NeedPoint)));
assert!(matches!(
cmd.on_text_input("-3"),
Some(CmdResult::AddSketchConstraint {
Some(CmdResult::AddParametricConstraint {
kind: ConstraintKind::DistanceX,
driving_param: Some(DrivingValue::Literal(-3.0)),
..
@@ -576,7 +597,7 @@ mod tests {
assert!(cmd.prompt().contains("<8.0000>"));
assert!(matches!(
cmd.on_enter(),
CmdResult::AddSketchConstraint {
CmdResult::AddParametricConstraint {
kind: ConstraintKind::DistanceY,
driving_param: Some(DrivingValue::Literal(8.0)),
..
@@ -609,7 +630,7 @@ mod tests {
);
match cmd.on_text_input("16") {
Some(CmdResult::AddSketchConstraint {
Some(CmdResult::AddParametricConstraint {
kind,
driving_param: Some(DrivingValue::Literal(v)),
..
@@ -617,7 +638,7 @@ mod tests {
assert_eq!(kind, ConstraintKind::Diameter);
assert_eq!(v, 16.0);
}
_ => panic!("expected an AddSketchConstraint{{Diameter}}"),
_ => panic!("expected an AddParametricConstraint{{Diameter}}"),
}
}
@@ -629,70 +650,10 @@ mod tests {
DistanceConstraintCommand::new(&scene, circle).expect("Circle supports DCONSTRAINT");
match cmd.on_text_input("5") {
Some(CmdResult::AddSketchConstraint { kind, .. }) => {
Some(CmdResult::AddParametricConstraint { kind, .. }) => {
assert_eq!(kind, ConstraintKind::Radius)
}
_ => panic!("expected an AddSketchConstraint{{Radius}}"),
_ => panic!("expected an AddParametricConstraint{{Radius}}"),
}
}
#[test]
fn a_keyword_switches_an_arcs_command_to_arc_length_mode() {
let mut scene = Scene::new();
let arc = add_arc(&mut scene);
let mut cmd =
DistanceConstraintCommand::new(&scene, arc).expect("Arc supports DCONSTRAINT");
assert!(matches!(cmd.on_text_input("A"), Some(CmdResult::NeedPoint)));
assert!(
cmd.prompt().contains("arc length"),
"prompt should reflect ArcLength mode: {}",
cmd.prompt()
);
match cmd.on_text_input("8") {
Some(CmdResult::AddSketchConstraint {
kind,
driving_param: Some(DrivingValue::Literal(v)),
..
}) => {
assert_eq!(kind, ConstraintKind::ArcLength);
assert_eq!(v, 8.0);
}
_ => panic!("expected an AddSketchConstraint{{ArcLength}}"),
}
}
#[test]
fn arc_length_keyword_is_not_offered_for_a_plain_circle() {
let mut scene = Scene::new();
let circle = add_circle(&mut scene);
let mut cmd =
DistanceConstraintCommand::new(&scene, circle).expect("Circle supports DCONSTRAINT");
assert!(
!cmd.prompt().contains("Arclength"),
"a plain circle has no arc length option: {}",
cmd.prompt()
);
// "A" isn't a recognized keyword here and doesn't parse as a
// number or a named parameter either, so it's rejected outright.
assert!(cmd.on_text_input("A").is_none());
}
#[test]
fn arc_length_default_is_the_arcs_current_swept_length() {
let mut scene = Scene::new();
let arc = add_arc(&mut scene); // radius 3, 90° sweep -> length = 3 * pi/2
let mut cmd =
DistanceConstraintCommand::new(&scene, arc).expect("Arc supports DCONSTRAINT");
cmd.on_text_input("A");
let expected = 3.0 * std::f64::consts::FRAC_PI_2;
assert!(
cmd.prompt().contains(&format!("{expected:.4}")),
"prompt should default to the arc's current length: {}",
cmd.prompt()
);
}
}
+129 -127
View File
@@ -1,13 +1,13 @@
use acadrust::entities::Dimension;
use acadrust::objects::{
AssocDimensionAssociation, AssocDimensionReference, AssociativeData,
AssociativeObject, ObjectType,
AssocDimensionAssociation, AssocDimensionReference, AssociativeData, AssociativeObject,
ObjectType,
};
use acadrust::types::{Handle, Vector3};
use acadrust::EntityType;
use cadkernel::geom2d::{
angle_within_arc, arc as tessellate_arc, arc_span, closest_point, Arc as KernelArc,
BulgeArc, Circle as KernelCircle, Curve as KernelCurve, DEFAULT_SEGMENTS_PER_RADIAN,
angle_within_arc, arc as tessellate_arc, arc_span, closest_point, Arc as KernelArc, BulgeArc,
Circle as KernelCircle, Curve as KernelCurve, DEFAULT_SEGMENTS_PER_RADIAN,
};
use cadkernel::space::Plane;
use std::f64::consts::TAU;
@@ -25,8 +25,7 @@ pub(crate) fn polyline_arc_point_marker(segment: i32) -> i32 {
}
fn polyline_arc_segment_from_point_marker(marker: i32) -> Option<i32> {
(marker <= POLYLINE_ARC_POINT_MARKER_BASE)
.then_some(POLYLINE_ARC_POINT_MARKER_BASE - marker)
(marker <= POLYLINE_ARC_POINT_MARKER_BASE).then_some(POLYLINE_ARC_POINT_MARKER_BASE - marker)
}
#[derive(Clone, Copy, Debug)]
@@ -158,11 +157,13 @@ pub(crate) fn radial_source_at(
entity: &EntityType,
point: Vector3,
) -> Option<RadialSourceGeometry> {
radial_candidates(entity).into_iter().min_by(|first, second| {
first
.distance_squared_to(dpoint(point))
.total_cmp(&second.distance_squared_to(dpoint(point)))
})
radial_candidates(entity)
.into_iter()
.min_by(|first, second| {
first
.distance_squared_to(dpoint(point))
.total_cmp(&second.distance_squared_to(dpoint(point)))
})
}
fn radial_source_for_marker(entity: &EntityType, marker: i32) -> Option<RadialSourceGeometry> {
@@ -177,14 +178,16 @@ fn radial_source_matching(
radius: f64,
chord: Vector3,
) -> Option<RadialSourceGeometry> {
radial_candidates(entity).into_iter().min_by(|first, second| {
let score = |candidate: &RadialSourceGeometry| {
point_distance_squared(candidate.center_world(), center)
+ (candidate.radius - radius).powi(2)
+ candidate.distance_squared_to(dpoint(chord)) * 1e-6
};
score(first).total_cmp(&score(second))
})
radial_candidates(entity)
.into_iter()
.min_by(|first, second| {
let score = |candidate: &RadialSourceGeometry| {
point_distance_squared(candidate.center_world(), center)
+ (candidate.radius - radius).powi(2)
+ candidate.distance_squared_to(dpoint(chord)) * 1e-6
};
score(first).total_cmp(&score(second))
})
}
fn point_distance_squared(first: Vector3, second: Vector3) -> f64 {
@@ -235,9 +238,10 @@ fn polyline_arc_center(entity: &EntityType, segment: usize) -> Option<Vector3> {
EntityType::LwPolyline(polyline) => {
let count = polyline.vertices.len();
let first = *polyline.vertices.get(segment)?;
let second = *polyline
.vertices
.get(next_segment_index(count, polyline.is_closed, segment)?)?;
let second =
*polyline
.vertices
.get(next_segment_index(count, polyline.is_closed, segment)?)?;
bulge_center_world(
[first.location.x, first.location.y],
[second.location.x, second.location.y],
@@ -249,9 +253,10 @@ fn polyline_arc_center(entity: &EntityType, segment: usize) -> Option<Vector3> {
EntityType::Polyline2D(polyline) => {
let count = polyline.vertices.len();
let first = polyline.vertices.get(segment)?;
let second = polyline
.vertices
.get(next_segment_index(count, polyline.is_closed(), segment)?)?;
let second =
polyline
.vertices
.get(next_segment_index(count, polyline.is_closed(), segment)?)?;
bulge_center_world(
[first.location.x, first.location.y],
[second.location.x, second.location.y],
@@ -267,20 +272,24 @@ fn polyline_arc_center(entity: &EntityType, segment: usize) -> Option<Vector3> {
/// Ordered, named points for an entity (line start/end, polyline vertices,
/// ...), indexed by the same non-negative GsMarker convention
/// `AssocDimensionReference::main_gs_marker` uses. Promoted to
/// `pub(crate)` so `sketch::constraint_set` can address constraint
/// `pub(crate)` so the parametric solver can address constraint
/// endpoints the same way associative dimensions already address theirs,
/// rather than inventing a second sub-element scheme.
pub(crate) fn source_points(entity: &EntityType) -> Vec<Vector3> {
match entity {
EntityType::Line(line) => vec![line.start, line.end],
EntityType::Ray(ray) => vec![ray.base_point],
EntityType::Arc(arc) => vec![arc.start_point_wcs(), arc.end_point_wcs()],
EntityType::Circle(_) => Vec::new(),
EntityType::Spline(spline) => crate::entities::spline::nurbs3(spline)
.map(|curve| {
let (start, end) = curve.domain();
[curve.point_at_knot(start), curve.point_at_knot(end)].into_iter()
.map(|point| Vector3::new(point[0], point[1], point[2])).collect()
}).unwrap_or_default(),
[curve.point_at_knot(start), curve.point_at_knot(end)]
.into_iter()
.map(|point| Vector3::new(point[0], point[1], point[2]))
.collect()
})
.unwrap_or_default(),
EntityType::LwPolyline(polyline) => polyline
.vertices
.iter()
@@ -345,8 +354,7 @@ fn source_marker(entity: &EntityType, point: Vector3) -> Option<i32> {
.into_iter()
.enumerate()
.min_by(|(_, first), (_, second)| {
point_distance_squared(*first, point)
.total_cmp(&point_distance_squared(*second, point))
point_distance_squared(*first, point).total_cmp(&point_distance_squared(*second, point))
})
.map(|(index, _)| index as i32)
}
@@ -360,9 +368,7 @@ fn resolve_reference(scene: &Scene, reference: &AssocDimensionReference) -> Opti
let angle = radial.start_angle + sweep * reference.osnap_distance.clamp(0.0, 1.0);
return Some(radial.point_at_angle(angle));
}
if let Some(segment) =
polyline_arc_segment_from_point_marker(reference.main_gs_marker)
{
if let Some(segment) = polyline_arc_segment_from_point_marker(reference.main_gs_marker) {
let radial = radial_source_for_marker(entity, segment)?;
let sweep = positive_sweep(radial.start_angle, radial.end_angle);
let angle = radial.start_angle + sweep * reference.osnap_distance.clamp(0.0, 1.0);
@@ -381,9 +387,7 @@ fn resolve_reference(scene: &Scene, reference: &AssocDimensionReference) -> Opti
}
if reference.main_gs_marker == -2 {
return match entity {
EntityType::Circle(circle) => {
Some(circle.point_at_angle_wcs(reference.osnap_distance))
}
EntityType::Circle(circle) => Some(circle.point_at_angle_wcs(reference.osnap_distance)),
EntityType::Arc(arc) => Some(arc.point_at_angle_wcs(reference.osnap_distance)),
_ => None,
};
@@ -399,14 +403,17 @@ fn resolve_reference(scene: &Scene, reference: &AssocDimensionReference) -> Opti
);
let curve = circle_curve(circle);
let stored_parameter = curve.parameter_at([stored.0, stored.1]) * TAU;
let parameter = if reference.osnap_distance.abs() > 1e-12
|| stored_parameter.abs() <= 1e-12
let parameter = if reference.osnap_distance.abs() > 1e-12 || stored_parameter.abs() <= 1e-12
{
reference.osnap_distance
} else {
stored_parameter
};
let parameter = if parameter.is_finite() { parameter } else { 0.0 };
let parameter = if parameter.is_finite() {
parameter
} else {
0.0
};
let point = curve.point_at(parameter / TAU);
return Some(ocs_point(
point[0],
@@ -494,27 +501,13 @@ fn signed_angle_delta(value: f64) -> f64 {
(value + std::f64::consts::PI).rem_euclid(TAU) - std::f64::consts::PI
}
fn angle_about_plane(
plane: Plane,
center: Vector3,
point: Vector3,
) -> f64 {
let delta = [
point.x - center.x,
point.y - center.y,
point.z - center.z,
];
let dot = |axis: [f64; 3]| {
delta[0] * axis[0] + delta[1] * axis[1] + delta[2] * axis[2]
};
fn angle_about_plane(plane: Plane, center: Vector3, point: Vector3) -> f64 {
let delta = [point.x - center.x, point.y - center.y, point.z - center.z];
let dot = |axis: [f64; 3]| delta[0] * axis[0] + delta[1] * axis[1] + delta[2] * axis[2];
dot(plane.y_axis).atan2(dot(plane.x_axis))
}
fn point_on_radial_circle(
radial: RadialSourceGeometry,
radius: f64,
angle: f64,
) -> Vector3 {
fn point_on_radial_circle(radial: RadialSourceGeometry, radius: f64, angle: f64) -> Vector3 {
vector3(radial.plane.point_at([
radial.center[0] + radius * angle.cos(),
radial.center[1] + radius * angle.sin(),
@@ -522,9 +515,7 @@ fn point_on_radial_circle(
}
fn plane_from_normal(origin: Vector3, normal: Vector3) -> Plane {
let (x_axis, y_axis) = crate::scene::view::transform::ocs_axes((
normal.x, normal.y, normal.z,
));
let (x_axis, y_axis) = crate::scene::view::transform::ocs_axes((normal.x, normal.y, normal.z));
Plane::from_axes(
[origin.x, origin.y, origin.z],
[x_axis.0, x_axis.1, x_axis.2],
@@ -574,35 +565,52 @@ pub(crate) fn constraint_from_associative_dimension(
document: &acadrust::CadDocument,
handle: Handle,
) -> Option<(
super::sketch_constraints::ConstraintKind,
Vec<super::sketch_constraints::SketchRef>,
super::parametric_constraints::ConstraintKind,
Vec<super::parametric_constraints::ParametricRef>,
super::named_parameters::DrivingValue,
)> {
use super::named_parameters::DrivingValue;
use super::sketch_constraints::{ConstraintKind, SketchRef};
use super::parametric_constraints::{ConstraintKind, ParametricRef};
let EntityType::Dimension(dimension) = document.get_entity(handle)? else { return None };
let EntityType::Dimension(dimension) = document.get_entity(handle)? else {
return None;
};
let association = document.objects.values().find_map(|object| {
let ObjectType::Associative(object) = object else { return None };
let AssociativeData::DimensionAssociation(association) = &object.data else { return None };
let ObjectType::Associative(object) = object else {
return None;
};
let AssociativeData::DimensionAssociation(association) = &object.data else {
return None;
};
(association.dimension == handle && association.associativity != 0).then_some(association)
})?;
let point = |index: usize| {
let reference = association.references.get(index)?.first()?;
Some(SketchRef::point(*reference.xrefs.first()?, reference.main_gs_marker))
Some(ParametricRef::point(
*reference.xrefs.first()?,
reference.main_gs_marker,
))
};
let whole = |index: usize| {
let reference = association.references.get(index)?.first()?;
Some(SketchRef::whole(*reference.xrefs.first()?))
Some(ParametricRef::whole(*reference.xrefs.first()?))
};
let measurement = DrivingValue::Literal(dimension.measurement());
match dimension {
Dimension::Aligned(_) => Some((ConstraintKind::Distance, vec![point(0)?, point(1)?], measurement)),
Dimension::Aligned(_) => Some((
ConstraintKind::Distance,
vec![point(0)?, point(1)?],
measurement,
)),
Dimension::Linear(linear) => {
let direction = [linear.rotation.cos().abs(), linear.rotation.sin().abs()];
let kind = if direction[1] <= 1e-9 { ConstraintKind::DistanceX }
else if direction[0] <= 1e-9 { ConstraintKind::DistanceY }
else { return None };
let kind = if direction[1] <= 1e-9 {
ConstraintKind::DistanceX
} else if direction[0] <= 1e-9 {
ConstraintKind::DistanceY
} else {
return None;
};
Some((kind, vec![point(0)?, point(1)?], measurement))
}
Dimension::Radius(_) | Dimension::LargeRadial(_) => {
@@ -610,16 +618,24 @@ pub(crate) fn constraint_from_associative_dimension(
}
Dimension::Diameter(_) => Some((ConstraintKind::Diameter, vec![whole(0)?], measurement)),
Dimension::Angular2Ln(_) => {
let mut sources: Vec<Handle> = association.references.iter().flatten()
.filter_map(|reference| reference.xrefs.first().copied()).collect();
let mut sources: Vec<Handle> = association
.references
.iter()
.flatten()
.filter_map(|reference| reference.xrefs.first().copied())
.collect();
sources.sort_unstable();
sources.dedup();
let [first, second] = sources.as_slice() else { return None };
Some((ConstraintKind::Angle,
vec![SketchRef::whole(*first), SketchRef::whole(*second)], measurement))
let [first, second] = sources.as_slice() else {
return None;
};
Some((
ConstraintKind::Angle,
vec![ParametricRef::whole(*first), ParametricRef::whole(*second)],
measurement,
))
}
Dimension::Arc(_) => Some((ConstraintKind::ArcLength, vec![whole(0)?], measurement)),
Dimension::Angular3Pt(_) | Dimension::Ordinate(_) => None,
Dimension::Arc(_) | Dimension::Angular3Pt(_) | Dimension::Ordinate(_) => None,
}
}
@@ -716,8 +732,7 @@ impl Scene {
dimension: Handle,
sources: Vec<Option<DimensionAssociationSource>>,
) {
let Some(EntityType::Dimension(dimension_entity)) =
self.document.get_entity(dimension)
let Some(EntityType::Dimension(dimension_entity)) = self.document.get_entity(dimension)
else {
return;
};
@@ -803,22 +818,20 @@ impl Scene {
return;
}
let reference = |source: Handle,
marker: i32,
parameter: f64,
osnap_type: u8,
point: Vector3| AssocDimensionReference {
class_name: "AcDbOsnapPointRef".to_string(),
osnap_type,
xrefs: vec![source],
main_subent_type: 1,
main_gs_marker: marker,
osnap_distance: parameter,
osnap_point: point,
..AssocDimensionReference::default()
};
let mut references: [Vec<AssocDimensionReference>; 4] =
std::array::from_fn(|_| Vec::new());
let reference =
|source: Handle, marker: i32, parameter: f64, osnap_type: u8, point: Vector3| {
AssocDimensionReference {
class_name: "AcDbOsnapPointRef".to_string(),
osnap_type,
xrefs: vec![source],
main_subent_type: 1,
main_gs_marker: marker,
osnap_distance: parameter,
osnap_point: point,
..AssocDimensionReference::default()
}
};
let mut references: [Vec<AssocDimensionReference>; 4] = std::array::from_fn(|_| Vec::new());
let mut associativity = 0;
for (index, resolved) in resolved.into_iter().enumerate() {
if index >= references.len() {
@@ -873,12 +886,8 @@ impl Scene {
}
}
pub(crate) fn infer_dimension_sources(
&self,
dimension: Handle,
) -> Vec<Option<Handle>> {
let Some(EntityType::Dimension(entity)) = self.document.get_entity(dimension)
else {
pub(crate) fn infer_dimension_sources(&self, dimension: Handle) -> Vec<Option<Handle>> {
let Some(EntityType::Dimension(entity)) = self.document.get_entity(dimension) else {
return Vec::new();
};
let radial_data = match entity {
@@ -906,18 +915,11 @@ impl Scene {
.entities()
.filter(|candidate| candidate.common().handle != dimension)
.filter_map(|candidate| {
let radial = radial_source_matching(
candidate,
center,
radius,
chord,
)?;
let radial = radial_source_matching(candidate, center, radius, chord)?;
let center_error = point_distance_squared(radial.center_world(), center);
let radius_error = (radial.radius - radius).powi(2);
(center_error + radius_error <= tolerance * tolerance).then_some((
center_error + radius_error,
candidate.common().handle,
))
(center_error + radius_error <= tolerance * tolerance)
.then_some((center_error + radius_error, candidate.common().handle))
})
.min_by(|first, second| first.0.total_cmp(&second.0))
.map(|(_, handle)| handle);
@@ -946,8 +948,7 @@ impl Scene {
&mut self,
changes: &[(Handle, ChangeKind)],
) -> Vec<(Handle, ChangeKind)> {
let changed: rustc_hash::FxHashSet<_> =
changes.iter().map(|(handle, _)| *handle).collect();
let changed: rustc_hash::FxHashSet<_> = changes.iter().map(|(handle, _)| *handle).collect();
if changed.is_empty() {
return Vec::new();
}
@@ -966,7 +967,12 @@ impl Scene {
.references
.iter()
.flatten()
.any(|reference| reference.xrefs.iter().any(|handle| changed.contains(handle)))
.any(|reference| {
reference
.xrefs
.iter()
.any(|handle| changed.contains(handle))
})
.then_some(association.clone())
})
.collect();
@@ -989,9 +995,7 @@ impl Scene {
let entity = self.document.get_entity(source)?;
let segment = match reference.main_gs_marker {
-3 => 0,
POLYLINE_ARC_CENTER_MARKER => {
reference.osnap_distance.round().max(0.0) as i32
}
POLYLINE_ARC_CENTER_MARKER => reference.osnap_distance.round().max(0.0) as i32,
_ => return None,
};
radial_source_for_marker(entity, segment)
@@ -1144,10 +1148,7 @@ impl Scene {
let old_dim_radius = old_center.distance(&old_definition);
let radial_offset = old_dim_radius - old_source_radius;
let old_mid = arc.arc_start_parameter
+ positive_sweep(
arc.arc_start_parameter,
arc.arc_end_parameter,
) * 0.5;
+ positive_sweep(arc.arc_start_parameter, arc.arc_end_parameter) * 0.5;
let old_definition_angle =
angle_about_plane(old_plane, old_center, old_definition);
let definition_angle_offset =
@@ -1208,8 +1209,7 @@ impl Scene {
arc.base.insertion_point = definition;
}
if arc.has_leader {
arc.first_leader_point =
point_on_radial_circle(radial, dim_radius, middle);
arc.first_leader_point = point_on_radial_circle(radial, dim_radius, middle);
arc.second_leader_point = second_leader;
}
arc.base.actual_measurement = arc.measurement();
@@ -1228,7 +1228,9 @@ impl Scene {
fn linear_measurement(linear: &acadrust::entities::DimensionLinear) -> f64 {
let plane = plane_from_normal(linear.first_point, linear.base.normal);
let first = plane.project(dpoint(linear.first_point)).unwrap_or([0.0; 2]);
let first = plane
.project(dpoint(linear.first_point))
.unwrap_or([0.0; 2]);
let second = plane.project(dpoint(linear.second_point)).unwrap_or(first);
let axis = [linear.rotation.cos(), linear.rotation.sin()];
((second[0] - first[0]) * axis[0] + (second[1] - first[1]) * axis[1]).abs()
File diff suppressed because it is too large. Load diff
+202 -227
View File
@@ -146,8 +146,7 @@ impl Scene {
fn prepare_section_objects(&mut self, entity: &mut EntityType) -> bool {
use acadrust::entities::ExtendedEntityData;
use acadrust::objects::{
ClassObject, ClassObjectData, SectionManager, SectionSettings,
SectionTypeSettings,
ClassObject, ClassObjectData, SectionManager, SectionSettings, SectionTypeSettings,
};
let EntityType::Extended(extended) = entity else {
@@ -169,16 +168,15 @@ impl Scene {
);
if !valid_settings {
let settings_handle = self.document.allocate_handle();
let mut settings = ClassObject::new(ClassObjectData::SectionSettings(
SectionSettings {
let mut settings =
ClassObject::new(ClassObjectData::SectionSettings(SectionSettings {
current_type: 4,
types: vec![SectionTypeSettings {
section_type: 4,
generation: 17,
..SectionTypeSettings::default()
}],
},
));
}));
settings.handle = settings_handle;
settings.owner = entity_handle;
self.document
@@ -223,9 +221,9 @@ impl Scene {
if let Some(ObjectType::Dictionary(dictionary)) =
self.document.objects.get_mut(&root)
{
dictionary.entries.retain(|(name, _)| {
!name.eq_ignore_ascii_case("ACAD_SECTION_MANAGER")
});
dictionary
.entries
.retain(|(name, _)| !name.eq_ignore_ascii_case("ACAD_SECTION_MANAGER"));
dictionary.add_entry("ACAD_SECTION_MANAGER", handle);
}
handle
@@ -256,10 +254,7 @@ impl Scene {
let mut needs_geometry_bump = false;
for entity in entities {
let affects_blocks = matches!(
&entity,
EntityType::Block(_) | EntityType::BlockEnd(_)
);
let affects_blocks = matches!(&entity, EntityType::Block(_) | EntityType::BlockEnd(_));
let handle = self.add_entity_internal(entity, false);
handles.push(handle);
if handle.is_null() {
@@ -285,10 +280,7 @@ impl Scene {
// Only block sentinels mutate a block definition and require rebuilding
// the block cache. A top-level INSERT merely references an existing
// definition, so adding it can patch just that new render handle.
let affects_blocks = matches!(
&entity,
EntityType::Block(_) | EntityType::BlockEnd(_)
);
let affects_blocks = matches!(&entity, EntityType::Block(_) | EntityType::BlockEnd(_));
// INSERT invalidates rendered block instances, but it does not mutate
// the referenced block definition. Only block sentinels require a
// structure image for undo; ordinary owner membership is intrinsic add
@@ -363,8 +355,9 @@ impl Scene {
// Delta-undo poison inputs (captured before the mutations below): an
// add that also creates a new layer, adds a block, or inserts an image
// definition mutates non-entity state a pure-entity delta can't undo.
let creates_layer =
self.is_recording_undo() && !layer.trim().is_empty() && !self.document.layers.contains(&layer);
let creates_layer = self.is_recording_undo()
&& !layer.trim().is_empty()
&& !self.document.layers.contains(&layer);
self.ensure_layer(&layer);
let app_ids: Vec<String> = entity
.common()
@@ -374,9 +367,9 @@ impl Scene {
.map(|record| record.application_name.clone())
.collect();
let creates_app_id = self.is_recording_undo()
&& app_ids.iter().any(|name| {
!name.trim().is_empty() && !self.document.app_ids.contains(name)
});
&& app_ids
.iter()
.any(|name| !name.trim().is_empty() && !self.document.app_ids.contains(name));
for name in &app_ids {
self.ensure_app_id(name);
}
@@ -412,14 +405,13 @@ impl Scene {
if let Some(mut model) = mesh_seed {
if let Some(entity) = self.document.get_entity(handle) {
let color = self.render_style(entity).0;
let material =
crate::scene::model::material_model::resolve_material_with_base(
&self.document,
entity,
color,
None,
self.material_base_dir.as_deref(),
);
let material = crate::scene::model::material_model::resolve_material_with_base(
&self.document,
entity,
color,
None,
self.material_base_dir.as_deref(),
);
material.apply_to_with_face_overrides(
&mut model,
&self.document,
@@ -539,13 +531,8 @@ impl Scene {
// Replacing (or becoming) a block sentinel forces a full block-cache
// rebuild. INSERT edits (including retargeting to another existing
// definition) only change that top-level render handle.
let affects_blocks = matches!(
existing,
EntityType::Block(_) | EntityType::BlockEnd(_)
) || matches!(
&entity,
EntityType::Block(_) | EntityType::BlockEnd(_)
);
let affects_blocks = matches!(existing, EntityType::Block(_) | EntityType::BlockEnd(_))
|| matches!(&entity, EntityType::Block(_) | EntityType::BlockEnd(_));
// A plugin edit may retarget the entity to a novel layer; register it
// so the edited entity keeps that layer on save instead of collapsing
@@ -627,14 +614,13 @@ impl Scene {
if let Some(mut model) = mesh_seed {
if let Some(entity) = self.document.get_entity(handle) {
let color = self.render_style(entity).0;
let material =
crate::scene::model::material_model::resolve_material_with_base(
&self.document,
entity,
color,
None,
self.material_base_dir.as_deref(),
);
let material = crate::scene::model::material_model::resolve_material_with_base(
&self.document,
entity,
color,
None,
self.material_base_dir.as_deref(),
);
material.apply_to_with_face_overrides(
&mut model,
&self.document,
@@ -770,15 +756,14 @@ impl Scene {
_ => None,
})
.collect();
let entries: Vec<(Handle, std::sync::Arc<EntityType>, [f32; 4], bool)> =
mesh_entities
.into_iter()
.map(|(handle, entity)| {
let color = self.render_style(entity.as_ref()).0;
let top_level = layout_blocks.contains(&entity.common().owner_handle);
(handle, entity, color, top_level)
})
.collect();
let entries: Vec<(Handle, std::sync::Arc<EntityType>, [f32; 4], bool)> = mesh_entities
.into_iter()
.map(|(handle, entity)| {
let color = self.render_style(entity.as_ref()).0;
let top_level = layout_blocks.contains(&entity.common().owner_handle);
(handle, entity, color, top_level)
})
.collect();
let facet_res = self.document.header.facet_resolution;
let chordal_deflection =
crate::entities::solid3d::display_deflection(&self.document.header, facet_res);
@@ -1119,16 +1104,13 @@ impl Scene {
self.belongs_to_visible_block(handle, c.owner_handle, target_block)
})
.flat_map(|(&handle, model)| {
let contextual = self
.document
.get_entity(handle)
.map(|entity| {
crate::scene::annotative::entity_for_annotation_context(
&self.document,
entity,
annotation_scale_handle,
)
});
let contextual = self.document.get_entity(handle).map(|entity| {
crate::scene::annotative::entity_for_annotation_context(
&self.document,
entity,
annotation_scale_handle,
)
});
let entity = contextual.as_deref();
let mut m = match entity {
Some(EntityType::Hatch(dxf))
@@ -1196,10 +1178,7 @@ impl Scene {
),
index,
),
other => (
crate::scene::convert::tess_util::aci_to_rgba(&other),
0,
),
other => (crate::scene::convert::tess_util::aci_to_rgba(&other), 0),
};
b.color = bg_color;
b.aci = bg_aci;
@@ -1218,9 +1197,7 @@ impl Scene {
m.scale = dxf.pattern_scale as f32;
}
model::hatch_model::HatchPattern::Gradient {
angle_deg,
shift,
..
angle_deg, shift, ..
} => {
*angle_deg = dxf.gradient_color.angle.to_degrees() as f32;
*shift = dxf.gradient_color.shift as f32;
@@ -1351,7 +1328,10 @@ impl Scene {
.iter()
.copied()
.filter(|&handle| {
matches!(self.document.get_entity(handle), Some(EntityType::Insert(_)))
matches!(
self.document.get_entity(handle),
Some(EntityType::Insert(_))
)
})
.collect();
if targets.is_empty() {
@@ -1421,8 +1401,7 @@ impl Scene {
return true;
}
if self.object_isolation.hides(common.handle)
|| (!include_preview_hidden
&& self.preview_hidden.contains(&common.handle))
|| (!include_preview_hidden && self.preview_hidden.contains(&common.handle))
{
return false;
}
@@ -1462,10 +1441,7 @@ impl Scene {
}
let style = context.style_for(&self.document, entity);
let preserve_white_mask = source_hatch.is_solid
&& matches!(
source_hatch.common.color,
acadrust::types::Color::Index(7)
);
&& matches!(source_hatch.common.color, acadrust::types::Color::Index(7));
let color = if preserve_white_mask {
style.0
} else {
@@ -1504,9 +1480,15 @@ impl Scene {
}
let matrix = &context.transform.matrix.m;
let linear = [
matrix[0][0].to_bits(), matrix[0][1].to_bits(), matrix[0][2].to_bits(),
matrix[1][0].to_bits(), matrix[1][1].to_bits(), matrix[1][2].to_bits(),
matrix[2][0].to_bits(), matrix[2][1].to_bits(), matrix[2][2].to_bits(),
matrix[0][0].to_bits(),
matrix[0][1].to_bits(),
matrix[0][2].to_bits(),
matrix[1][0].to_bits(),
matrix[1][1].to_bits(),
matrix[1][2].to_bits(),
matrix[2][0].to_bits(),
matrix[2][1].to_bits(),
matrix[2][2].to_bits(),
];
let key = (
source_hatch.common.handle.value(),
@@ -1525,12 +1507,10 @@ impl Scene {
let source_id = *hatch_sources
.entry(key)
.or_insert_with(crate::scene::model::instance_model::next_source_id);
model.render_instance = Some(
crate::scene::model::instance_model::RenderInstance {
source_id,
translation: [matrix[0][3], matrix[1][3], matrix[2][3]],
},
);
model.render_instance = Some(crate::scene::model::instance_model::RenderInstance {
source_id,
translation: [matrix[0][3], matrix[1][3], matrix[2][3]],
});
models.push(model);
},
);
@@ -1598,48 +1578,37 @@ impl Scene {
};
let mut wipeout = source.clone();
if context.is_instanced() {
wipeout.insertion_point =
context.transform.apply(source.insertion_point);
wipeout.u_vector =
context.transform.apply_rotation(source.u_vector);
wipeout.v_vector =
context.transform.apply_rotation(source.v_vector);
wipeout.insertion_point = context.transform.apply(source.insertion_point);
wipeout.u_vector = context.transform.apply_rotation(source.u_vector);
wipeout.v_vector = context.transform.apply_rotation(source.v_vector);
}
let Some(mut fill_plane) = Self::wipeout_fill_plane(&wipeout) else {
return;
};
let (world_origin, mut boundary) =
Self::wipeout_boundary_2d(&wipeout);
let (world_origin, mut boundary) = Self::wipeout_boundary_2d(&wipeout);
for clip in &context.clips {
let clip: Vec<[f32; 2]> = clip
.iter()
.map(|point| [point[0] as f32, point[1] as f32])
.collect();
boundary = pick::xclip::clip_hatch_boundary(
&boundary,
world_origin,
&clip,
);
boundary = pick::xclip::clip_hatch_boundary(&boundary, world_origin, &clip);
}
if boundary.len() < 3 {
return;
}
if !context.clips.is_empty() {
let Some(local) = Self::wipeout_boundary_at_xy(
fill_plane.0,
world_origin,
&boundary,
) else {
let Some(local) =
Self::wipeout_boundary_at_xy(fill_plane.0, world_origin, &boundary)
else {
return;
};
fill_plane.1 = local;
}
let selection_handle =
if tint_insert_selection && context.is_instanced() {
context.root_handle
} else {
source.common.handle
};
let selection_handle = if tint_insert_selection && context.is_instanced() {
context.root_handle
} else {
source.common.handle
};
let color = if self.selected.contains(&selection_handle) {
[0.15, 0.55, 1.00, 0.35]
} else {
@@ -1648,9 +1617,15 @@ impl Scene {
let render_instance = if context.is_instanced() {
let matrix = &context.transform.matrix.m;
let linear = [
matrix[0][0].to_bits(), matrix[0][1].to_bits(), matrix[0][2].to_bits(),
matrix[1][0].to_bits(), matrix[1][1].to_bits(), matrix[1][2].to_bits(),
matrix[2][0].to_bits(), matrix[2][1].to_bits(), matrix[2][2].to_bits(),
matrix[0][0].to_bits(),
matrix[0][1].to_bits(),
matrix[0][2].to_bits(),
matrix[1][0].to_bits(),
matrix[1][1].to_bits(),
matrix[1][2].to_bits(),
matrix[2][0].to_bits(),
matrix[2][1].to_bits(),
matrix[2][2].to_bits(),
];
let key = (
source.common.handle.value(),
@@ -1661,9 +1636,9 @@ impl Scene {
.collect::<Vec<_>>(),
color.map(f32::to_bits),
);
let source_id = *wipeout_sources.entry(key).or_insert_with(
crate::scene::model::instance_model::next_source_id,
);
let source_id = *wipeout_sources
.entry(key)
.or_insert_with(crate::scene::model::instance_model::next_source_id);
Some(crate::scene::model::instance_model::RenderInstance {
source_id,
translation: [matrix[0][3], matrix[1][3], matrix[2][3]],
@@ -1692,8 +1667,7 @@ impl Scene {
angle_offset: 0.0,
scale: 1.0,
world_origin,
draw_depth: context
.draw_depth(source.common.handle, depth_map.as_ref()),
draw_depth: context.draw_depth(source.common.handle, depth_map.as_ref()),
});
},
);
@@ -1708,7 +1682,11 @@ impl Scene {
) -> ([f64; 2], Vec<[f32; 2]>) {
let origin = [wo.insertion_point.x, wo.insertion_point.y];
let plane = cadkernel::space::Plane::from_axes(
[wo.insertion_point.x, wo.insertion_point.y, wo.insertion_point.z],
[
wo.insertion_point.x,
wo.insertion_point.y,
wo.insertion_point.z,
],
[wo.u_vector.x, wo.u_vector.y, wo.u_vector.z],
[wo.v_vector.x, wo.v_vector.y, wo.v_vector.z],
);
@@ -1744,12 +1722,7 @@ impl Scene {
let mut clip: Vec<[f64; 2]> = wo
.clip_boundary_vertices
.iter()
.map(|point| {
[
point.x + wo.size.x * 0.5,
wo.size.y * 0.5 - point.y,
]
})
.map(|point| [point.x + wo.size.x * 0.5, wo.size.y * 0.5 - point.y])
.collect();
if clip.last() != clip.first() {
clip.push(clip[0]);
@@ -1793,11 +1766,7 @@ impl Scene {
world_origin: [f64; 2],
boundary: &[[f32; 2]],
) -> Option<Vec<[f32; 2]>> {
let plane = cadkernel::space::Plane::from_axes(
plane.origin,
plane.x_axis,
plane.y_axis,
);
let plane = cadkernel::space::Plane::from_axes(plane.origin, plane.x_axis, plane.y_axis);
boundary
.iter()
.map(|point| {
@@ -1815,10 +1784,7 @@ impl Scene {
.collect()
}
pub(crate) fn hatch_model_from_dxf(
dxf: &DxfHatch,
color: [f32; 4],
) -> Option<HatchModel> {
pub(crate) fn hatch_model_from_dxf(dxf: &DxfHatch, color: [f32; 4]) -> Option<HatchModel> {
let normal = (dxf.normal.x, dxf.normal.y, dxf.normal.z);
// Build the boundary in f64 first so the precision-preserving
// origin computation below sees full WCS precision. We only cast
@@ -1849,9 +1815,7 @@ impl Scene {
let mut edge_polys: Vec<Vec<[f64; 2]>> = Vec::new();
for edge in &path.edges {
if let Some(curve) = crate::entities::hatch::edge_curve(edge) {
edge_polys.push(
curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE),
);
edge_polys.push(curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE));
}
}
let mut local_ring = chain_path_edges(edge_polys);
@@ -1902,11 +1866,7 @@ impl Scene {
local_boundary.push([f32::NAN, f32::NAN]);
}
boundary.extend(ring);
local_boundary.extend(
local_ring
.into_iter()
.map(|[x, y]| [x as f32, y as f32]),
);
local_boundary.extend(local_ring.into_iter().map(|[x, y]| [x as f32, y as f32]));
boundary_exterior.push(depth == 0);
boundary_sources.push(sources);
}
@@ -1941,18 +1901,19 @@ impl Scene {
// lines. The stored offset is the authoritative world-unit spacing, so
// the resulting families are already final: no pattern_scale / angle
// is re-applied (see `prebaked` below and the HatchModel fields).
let prebaked = !dxf.is_solid
&& !dxf.gradient_color.is_enabled()
&& !dxf.pattern.lines.is_empty();
let prebaked =
!dxf.is_solid && !dxf.gradient_color.is_enabled() && !dxf.pattern.lines.is_empty();
// The gradient's first stop is the fill's start colour (not the
// entity colour); capture it so the HatchModel draws stop-0 → stop-1.
let mut gradient_color1: Option<[f32; 4]> = None;
let mut pattern = if dxf.gradient_color.is_enabled() {
let stop = |i: usize| {
dxf.gradient_color.colors.get(i).and_then(|e| e.color.rgb()).map(
|(r, g, b)| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0],
)
dxf.gradient_color
.colors
.get(i)
.and_then(|e| e.color.rgb())
.map(|(r, g, b)| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0])
};
gradient_color1 = stop(0);
let color1 = gradient_color1.unwrap_or(color);
@@ -1975,7 +1936,11 @@ impl Scene {
model::hatch_model::HatchPattern::Solid
} else if prebaked {
model::hatch_model::HatchPattern::Pattern(
dxf.pattern.lines.iter().map(family_from_stored_line).collect(),
dxf.pattern
.lines
.iter()
.map(family_from_stored_line)
.collect(),
)
} else {
let pat_name = &dxf.pattern.name;
@@ -2038,10 +2003,18 @@ impl Scene {
let mut max_y = f64::NEG_INFINITY;
for &[x, y] in &boundary {
if x.is_finite() && y.is_finite() {
if x < min_x { min_x = x; }
if y < min_y { min_y = y; }
if x > max_x { max_x = x; }
if y > max_y { max_y = y; }
if x < min_x {
min_x = x;
}
if y < min_y {
min_y = y;
}
if x > max_x {
max_x = x;
}
if y > max_y {
max_y = y;
}
}
}
let world_origin = if min_x.is_finite() && min_y.is_finite() {
@@ -2129,8 +2102,16 @@ impl Scene {
color: gradient_color1.unwrap_or(color),
aci: 0,
line_weight_px: 1.0,
angle_offset: if prebaked { 0.0 } else { dxf.pattern_angle as f32 },
scale: if prebaked { 1.0 } else { dxf.pattern_scale as f32 },
angle_offset: if prebaked {
0.0
} else {
dxf.pattern_angle as f32
},
scale: if prebaked {
1.0
} else {
dxf.pattern_scale as f32
},
world_origin,
draw_depth: 0.0,
})
@@ -2168,16 +2149,13 @@ impl Scene {
/// at load, so a pattern-scale / background / boundary edit stays
/// invisible until the cached model is refreshed (#415).
pub fn refresh_fill_model(&mut self, handle: Handle) {
let contextual = self
.document
.get_entity(handle)
.map(|entity| {
crate::scene::annotative::entity_for_annotation_context(
&self.document,
entity,
self.displayed_annotation_scale_handle(),
)
});
let contextual = self.document.get_entity(handle).map(|entity| {
crate::scene::annotative::entity_for_annotation_context(
&self.document,
entity,
self.displayed_annotation_scale_handle(),
)
});
let new_model = match contextual.as_deref() {
Some(EntityType::Hatch(dxf)) => {
let color = convert::tess_util::aci_to_rgba(&dxf.common.color);
@@ -2213,7 +2191,7 @@ impl Scene {
e,
self.displayed_annotation_scale_handle(),
)
.into_owned(),
.into_owned(),
)),
EntityType::Solid(s) => Some((s.common.handle, e.clone())),
_ => None,
@@ -2315,7 +2293,8 @@ impl Scene {
// Incremental (post-xref) pass: leave already-tessellated
// host solids untouched, only build the newly merged ones.
if incremental
&& (self.meshes.contains_key(&handle) || self.block_meshes.contains_key(&handle))
&& (self.meshes.contains_key(&handle)
|| self.block_meshes.contains_key(&handle))
{
return None;
}
@@ -2363,7 +2342,11 @@ impl Scene {
&self.document,
&entity,
);
let mesh = if top_level { offset_mesh_lod_set(mesh) } else { mesh };
let mesh = if top_level {
offset_mesh_lod_set(mesh)
} else {
mesh
};
(handle, mesh, top_level)
})
})
@@ -2478,18 +2461,18 @@ impl Scene {
} else {
// Otherwise reconstruct every ring from render offsets.
let reconstructed_wcs: Vec<[f64; 2]> = if model.boundary_wcs.is_none() {
let [wx, wy] = model.world_origin;
model
.boundary
.iter()
.map(|&[x, y]| {
if x.is_finite() && y.is_finite() {
[x as f64 + wx, y as f64 + wy]
} else {
[f64::NAN, f64::NAN]
}
})
.collect()
let [wx, wy] = model.world_origin;
model
.boundary
.iter()
.map(|&[x, y]| {
if x.is_finite() && y.is_finite() {
[x as f64 + wx, y as f64 + wy]
} else {
[f64::NAN, f64::NAN]
}
})
.collect()
} else {
Vec::new()
};
@@ -2502,31 +2485,31 @@ impl Scene {
let mut first = true;
let mut ring_index = 0usize;
let mut push_ring = |r: &mut Vec<Vector2>, is_outer: bool, index: usize| {
if !r.is_empty() {
let edge = PolylineEdge::new(std::mem::take(r), true);
let handles: Vec<_> = model
.boundary_sources
.as_deref()
.and_then(|sources| sources.get(index))
.into_iter()
.flatten()
.copied()
.filter(|handle| handle.is_valid())
.collect();
let mut bits = 0;
if is_outer {
bits |= acadrust::entities::hatch::BoundaryPathFlags::OUTERMOST.bits();
bits |= acadrust::entities::hatch::BoundaryPathFlags::EXTERNAL.bits();
if !r.is_empty() {
let edge = PolylineEdge::new(std::mem::take(r), true);
let handles: Vec<_> = model
.boundary_sources
.as_deref()
.and_then(|sources| sources.get(index))
.into_iter()
.flatten()
.copied()
.filter(|handle| handle.is_valid())
.collect();
let mut bits = 0;
if is_outer {
bits |= acadrust::entities::hatch::BoundaryPathFlags::OUTERMOST.bits();
bits |= acadrust::entities::hatch::BoundaryPathFlags::EXTERNAL.bits();
}
let mut path = BoundaryPath::with_flags(
acadrust::entities::hatch::BoundaryPathFlags::from_bits(bits),
);
path.add_edge(BoundaryEdge::Polyline(edge));
for handle in handles {
path.add_boundary_handle(handle);
}
dxf.paths.push(path);
}
let mut path = BoundaryPath::with_flags(
acadrust::entities::hatch::BoundaryPathFlags::from_bits(bits),
);
path.add_edge(BoundaryEdge::Polyline(edge));
for handle in handles {
path.add_boundary_handle(handle);
}
dxf.paths.push(path);
}
};
for &[x, y] in wcs {
if x.is_finite() && y.is_finite() {
@@ -2580,28 +2563,20 @@ impl Scene {
let family_angle = (family.angle_deg as f64).to_radians();
let angle = family_angle + rotation;
let (family_sin, family_cos) = family_angle.sin_cos();
let local_offset_x = family.dx as f64 * family_cos
- family.dy as f64 * family_sin;
let local_offset_y = family.dx as f64 * family_sin
+ family.dy as f64 * family_cos;
let local_offset_x = family.dx as f64 * family_cos - family.dy as f64 * family_sin;
let local_offset_y = family.dx as f64 * family_sin + family.dy as f64 * family_cos;
let base_x = family.x0 as f64 * pattern_scale;
let base_y = family.y0 as f64 * pattern_scale;
pattern.lines.push(acadrust::entities::HatchPatternLine {
angle,
base_point: Vector2::new(
pattern_origin[0]
+ base_x * rotation_cos
- base_y * rotation_sin,
pattern_origin[1]
+ base_x * rotation_sin
+ base_y * rotation_cos,
pattern_origin[0] + base_x * rotation_cos - base_y * rotation_sin,
pattern_origin[1] + base_x * rotation_sin + base_y * rotation_cos,
),
offset: Vector2::new(
(local_offset_x * rotation_cos
- local_offset_y * rotation_sin)
(local_offset_x * rotation_cos - local_offset_y * rotation_sin)
* pattern_scale,
(local_offset_x * rotation_sin
+ local_offset_y * rotation_cos)
(local_offset_x * rotation_sin + local_offset_y * rotation_cos)
* pattern_scale,
),
dash_lengths: family
@@ -2642,13 +2617,13 @@ impl Scene {
dxf.gradient_color.is_single_color = false;
// Linear has no INV name in the standard set: persist an inverted
// linear by swapping the colour stops instead.
let (c0, c1) =
if *invert && matches!(kind, crate::scene::model::hatch_model::GradientKind::Linear)
{
(*color2, model.color)
} else {
(model.color, *color2)
};
let (c0, c1) = if *invert
&& matches!(kind, crate::scene::model::hatch_model::GradientKind::Linear)
{
(*color2, model.color)
} else {
(model.color, *color2)
};
dxf.gradient_color.colors = vec![
acadrust::entities::hatch::GradientColorEntry {
value: 0.0,
@@ -2703,7 +2678,7 @@ impl Scene {
// resetting them, a "new"/"open" (e.g. via automation, which reuses
// this same reset) would leak the previous document's constraints
// and named parameters into the fresh one.
self.sketch_constraints.clear();
self.parametric_constraints.clear();
self.named_parameters = crate::scene::named_parameters::ParameterTable::new();
self.bump_geometry();
}
+627 -621
View File
File diff suppressed because it is too large. Load diff
+11 -8
View File
@@ -24,10 +24,7 @@ pub enum PropValue {
/// Editable text plus a dropdown of existing options (block reference
/// Name row): picking an option re-points the reference, submitting a
/// new name renames the definition.
EditChoice {
value: String,
options: Vec<String>,
},
EditChoice { value: String, options: Vec<String> },
/// ACI/RGB/ByLayer/ByBlock color — rendered as a color picker.
ColorChoice(AcadColor),
/// Color-book color with its file-provided display name.
@@ -51,7 +48,10 @@ pub enum PropValue {
BoolToggle { field: &'static str, value: bool },
/// A 0-based index navigated with ◀ / ▶ buttons (e.g. a polyline's Current
/// Vertex). `display` is the label shown between the arrows (e.g. "2 / 7").
Stepper { field: &'static str, display: String },
Stepper {
field: &'static str,
display: String,
},
/// Hatch pattern name — rendered as a combo_box from the catalog.
HatchPatternChoice(String),
/// Block attribute value keyed by its (dynamic, runtime) tag — rendered as
@@ -59,11 +59,14 @@ pub enum PropValue {
/// tag carried here, not the row's `&'static str` field.
AttrText { tag: String, value: String },
/// A clickable link to one or more entities — the Constraints section's
/// row for one persistent sketch constraint. Clicking it selects every
/// row for one parametric constraint. Clicking it selects every
/// entity in `handles` in the viewport. `conflicting` mirrors
/// `SketchConstraintSet::conflicts`, tinting the row the same danger
/// `ParametricConstraintSet::conflicts`, tinting the row the same danger
/// color the constraint's glyph pill already uses.
EntityLink { handles: Vec<Handle>, conflicting: bool },
EntityLink {
handles: Vec<Handle>,
conflicting: bool,
},
/// One row of the document-wide named-parameter table (Parameters
/// section, shown when nothing is selected). `index` is the row's
/// position in `ParameterTable::iter()` order — stable across edits to
+65 -93
View File
@@ -9,9 +9,7 @@ struct TextOrient {
x_scale: f64,
}
fn inverse_affine(
transform: &acadrust::types::Transform,
) -> Option<acadrust::types::Transform> {
fn inverse_affine(transform: &acadrust::types::Transform) -> Option<acadrust::types::Transform> {
use acadrust::types::{Matrix3, Matrix4, Transform, Vector3};
let matrix = &transform.matrix.m;
let linear = Matrix3::from_rows(
@@ -248,9 +246,7 @@ impl Scene {
let image_bearing = self.document.get_entity(h).is_some_and(|entity| {
matches!(
entity,
EntityType::RasterImage(_)
| EntityType::Ole2Frame(_)
| EntityType::Underlay(_)
EntityType::RasterImage(_) | EntityType::Ole2Frame(_) | EntityType::Underlay(_)
)
});
if image_bearing {
@@ -301,10 +297,7 @@ impl Scene {
/// Entities in anonymous dimension blocks that visually belong to
/// dimensions inside `block_record`. They must follow a block-coordinate
/// reframe and be included in BEDIT's discard snapshot.
pub(crate) fn block_definition_dependent_handles(
&self,
block_record: Handle,
) -> Vec<Handle> {
pub(crate) fn block_definition_dependent_handles(&self, block_record: Handle) -> Vec<Handle> {
let Some(record) = self
.document
.block_records
@@ -367,17 +360,16 @@ impl Scene {
let owned: HashSet<Handle> = handles.iter().copied().collect();
let transform = EntityTransform::Affine(*local_from_old);
let matrix = &local_from_old.matrix.m;
let dependent_transform =
EntityTransform::Affine(acadrust::types::Transform::from_matrix(
acadrust::types::Matrix4 {
m: [
[matrix[0][0], matrix[0][1], matrix[0][2], 0.0],
[matrix[1][0], matrix[1][1], matrix[1][2], 0.0],
[matrix[2][0], matrix[2][1], matrix[2][2], 0.0],
[0.0, 0.0, 0.0, 1.0],
],
},
));
let dependent_transform = EntityTransform::Affine(acadrust::types::Transform::from_matrix(
acadrust::types::Matrix4 {
m: [
[matrix[0][0], matrix[0][1], matrix[0][2], 0.0],
[matrix[1][0], matrix[1][1], matrix[1][2], 0.0],
[matrix[2][0], matrix[2][1], matrix[2][2], 0.0],
[0.0, 0.0, 0.0, 1.0],
],
},
));
let mut changed = Vec::new();
for handle in handles {
@@ -596,10 +588,7 @@ impl Scene {
let entity = self.document.get_entity(h)?.clone();
let annotation_scales = if leader_pair_handles.contains(&h) {
crate::scene::annotative::annotation_scale_handles_for_entity(
&self.document,
h,
)
crate::scene::annotative::annotation_scale_handles_for_entity(&self.document, h)
} else {
Vec::new()
};
@@ -691,8 +680,8 @@ impl Scene {
if let Some(model) = new_model {
self.hatches.insert(h, model);
}
let rebuilt_history = self.copy_solid_history(src_handle, h)
&& self.transform_solid_history(h, t);
let rebuilt_history =
self.copy_solid_history(src_handle, h) && self.transform_solid_history(h, t);
if !rebuilt_history
&& self.document.get_entity(h).is_some_and(|entity| {
matches!(
@@ -717,8 +706,7 @@ impl Scene {
let leader_links: Vec<(Handle, Handle)> = handle_map
.iter()
.filter_map(|(&source_handle, &copied_handle)| {
let EntityType::Leader(source_leader) =
self.document.get_entity(source_handle)?
let EntityType::Leader(source_leader) = self.document.get_entity(source_handle)?
else {
return None;
};
@@ -733,9 +721,7 @@ impl Scene {
.collect();
for (leader_handle, annotation_handle) in leader_links {
if let Some(EntityType::Leader(leader)) =
self.document.get_entity_mut(leader_handle)
{
if let Some(EntityType::Leader(leader)) = self.document.get_entity_mut(leader_handle) {
leader.annotation_handle = annotation_handle;
}
@@ -753,7 +739,7 @@ impl Scene {
.collect();
self.bump_entities(&changes);
// Constraints wholly within the duplicated selection follow it.
self.duplicate_sketch_constraints_for(&handle_map);
self.duplicate_parametric_constraints_for(&handle_map);
self.refresh_meshes_for_handles(&refresh_solid_handles);
new_handles
}
@@ -840,11 +826,7 @@ impl Scene {
let Some(reference) = reference else {
return;
};
let point = acadrust::types::Vector3::new(
reference.x,
reference.y,
reference.z,
);
let point = acadrust::types::Vector3::new(reference.x, reference.y, reference.z);
match self.document.get_entity_mut(handle) {
Some(EntityType::Solid3D(entity)) => entity.point_of_reference = point,
Some(EntityType::Surface(entity)) => entity.point_of_reference = point,
@@ -928,7 +910,9 @@ impl Scene {
(EntityType::Surface(_), SolidHistoryOperation::Loft(value)) => {
let EntityType::Surface(surface) =
crate::scene::model::loft_command_model::surface_entity(value)
else { return None; };
else {
return None;
};
Some(Some(surface.surface_data))
}
(EntityType::Solid3D(_), _) => Some(None),
@@ -953,9 +937,11 @@ impl Scene {
let Some(display) = self.prepare_solid_model_display(handle, &body) else {
return false;
};
if matches!(&operation, acadrust::objects::SolidHistoryOperation::Loft(_)
| acadrust::objects::SolidHistoryOperation::Extrusion(_))
&& !display.0.complete
if matches!(
&operation,
acadrust::objects::SolidHistoryOperation::Loft(_)
| acadrust::objects::SolidHistoryOperation::Extrusion(_)
) && !display.0.complete
{
return false;
}
@@ -970,7 +956,8 @@ impl Scene {
match self.document.get_entity_mut(handle) {
Some(EntityType::Solid3D(entity)) => entity.set_sat_document(&document),
Some(EntityType::Surface(entity)) => {
entity.acis_data = acadrust::entities::AcisData::from_sat(&document.to_sat_string());
entity.acis_data =
acadrust::entities::AcisData::from_sat(&document.to_sat_string());
if let Some(data) = surface_data {
if matches!(&data, acadrust::entities::SurfaceData::Extruded { .. }) {
entity.kind = acadrust::entities::SurfaceKind::Extruded;
@@ -1001,16 +988,19 @@ impl Scene {
let Some(display) = self.prepare_solid_model_display(handle, &body) else {
return false;
};
if matches!(&operation, acadrust::objects::SolidHistoryOperation::Loft(_)
| acadrust::objects::SolidHistoryOperation::Extrusion(_))
&& !display.0.complete
if matches!(
&operation,
acadrust::objects::SolidHistoryOperation::Loft(_)
| acadrust::objects::SolidHistoryOperation::Extrusion(_)
) && !display.0.complete
{
return false;
}
match self.document.get_entity_mut(handle) {
Some(EntityType::Solid3D(entity)) => entity.set_sat_document(&document),
Some(EntityType::Surface(entity)) => {
entity.acis_data = acadrust::entities::AcisData::from_sat(&document.to_sat_string());
entity.acis_data =
acadrust::entities::AcisData::from_sat(&document.to_sat_string());
if let Some(data) = surface_data {
if matches!(&data, acadrust::entities::SurfaceData::Extruded { .. }) {
entity.kind = acadrust::entities::SurfaceKind::Extruded;
@@ -1062,24 +1052,22 @@ impl Scene {
true
}
fn transform_solid_history(
&mut self,
handle: Handle,
transform: &EntityTransform,
) -> bool {
fn transform_solid_history(&mut self, handle: Handle, transform: &EntityTransform) -> bool {
let Some(mut operation) = self.document.solid_history_operation(handle).cloned() else {
return false;
};
if !crate::scene::model::solid_history::transform_operation(
&mut operation,
transform,
) {
if !crate::scene::model::solid_history::transform_operation(&mut operation, transform) {
return false;
}
if let EntityTransform::Translate(delta) = transform {
let surface_data = match (&operation, self.document.get_entity(handle)) {
(acadrust::objects::SolidHistoryOperation::Extrusion(value), Some(EntityType::Surface(_))) => {
let Some(data) = crate::scene::model::solid_history::extrusion_surface_data(value) else {
(
acadrust::objects::SolidHistoryOperation::Extrusion(value),
Some(EntityType::Surface(_)),
) => {
let Some(data) =
crate::scene::model::solid_history::extrusion_surface_data(value)
else {
return false;
};
Some(data)
@@ -1104,12 +1092,7 @@ impl Scene {
self.rebuild_solid_history(handle, operation)
}
pub fn apply_solid_history_choice(
&mut self,
handle: Handle,
field: &str,
value: &str,
) -> bool {
pub fn apply_solid_history_choice(&mut self, handle: Handle, field: &str, value: &str) -> bool {
self.record_solid_history_before(handle);
let mut created = false;
if self.document.solid_history_graph(handle).is_none() {
@@ -1137,7 +1120,10 @@ impl Scene {
_ => return false,
};
if create {
if !matches!(self.document.get_entity(handle), Some(EntityType::Solid3D(_))) {
if !matches!(
self.document.get_entity(handle),
Some(EntityType::Solid3D(_))
) {
return false;
}
self.restore_solid_models(&[handle]);
@@ -1195,13 +1181,9 @@ impl Scene {
return false;
};
self.restore_solid_models(&[handle]);
let Some(definition) = self
.solid_models
.get(&handle)
.and_then(|body| {
crate::scene::model::solid_history::fillet_radius_grip(body, radius)
})
else {
let Some(definition) = self.solid_models.get(&handle).and_then(|body| {
crate::scene::model::solid_history::fillet_radius_grip(body, radius)
}) else {
return false;
};
let Some(axis) = definition.axis else {
@@ -1247,8 +1229,7 @@ impl Scene {
let acadrust::objects::SolidHistoryOperation::Chamfer(value) = &mut operation else {
return false;
};
let distance = if grip_id
== crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE1
let distance = if grip_id == crate::scene::model::solid_history::GRIP_CHAMFER_DISTANCE1
{
&mut value.base_distance
} else {
@@ -1260,11 +1241,8 @@ impl Scene {
}
return self.preview_solid_history(handle, operation);
}
if !crate::scene::model::solid_history::apply_primitive_grip(
&mut operation,
grip_id,
apply,
) {
if !crate::scene::model::solid_history::apply_primitive_grip(&mut operation, grip_id, apply)
{
return false;
}
self.preview_solid_history(handle, operation)
@@ -1276,12 +1254,10 @@ impl Scene {
field: &str,
value: &str,
) -> bool {
let Some(mut operation) =
crate::scene::model::solid_history::primitive_property_operation(
&self.document,
handle,
)
else {
let Some(mut operation) = crate::scene::model::solid_history::primitive_property_operation(
&self.document,
handle,
) else {
return false;
};
if !crate::scene::model::solid_history::apply_primitive_property(
@@ -1407,7 +1383,8 @@ impl Scene {
};
let n = leader.vertices.len();
if n < 3 || (grip_id != n - 1 && grip_id != n - 2) || leader.annotation_handle.is_null() {
if n < 3 || (grip_id != n - 1 && grip_id != n - 2) || leader.annotation_handle.is_null()
{
return None;
}
@@ -1451,10 +1428,7 @@ impl Scene {
if self.sync_displayed_annotation_context(annotation_handle) {
self.poison_undo_recording();
}
self.bump_entities(&[(
annotation_handle,
crate::scene::ChangeKind::Modified,
)]);
self.bump_entities(&[(annotation_handle, crate::scene::ChangeKind::Modified)]);
}
}
}
@@ -1510,9 +1484,7 @@ impl Scene {
let image_bearing = self.document.get_entity(handle).is_some_and(|entity| {
matches!(
entity,
EntityType::RasterImage(_)
| EntityType::Ole2Frame(_)
| EntityType::Underlay(_)
EntityType::RasterImage(_) | EntityType::Ole2Frame(_) | EntityType::Underlay(_)
)
});
+6 -21
View File
@@ -2,14 +2,13 @@
//! Formulas are parsed locally so dependency cycles can be rejected before evaluation.
use super::Scene;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
/// A parsed formula. Ordinary arithmetic plus [`Expr::Ref`] (a reference to
/// another parameter by name) and [`Expr::Call`] (a function call). Only the
/// small builtin set in `call_builtin` is accepted.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
Number(f64),
Ref(String),
@@ -57,7 +56,7 @@ impl Expr {
/// stages, the UI) rather than panicking — matching this codebase's
/// established pattern of isolating a bad derived value instead of letting
/// it take down an unrelated computation (e.g. dangling-constraint handling
/// in `sketch_solve.rs`).
/// in `parametric_solve.rs`).
#[derive(Debug, Clone, PartialEq)]
pub enum ParamError {
/// A formula's text doesn't parse. The `String` is a short, human-
@@ -131,7 +130,7 @@ pub fn is_valid_name(name: &str) -> bool {
}
/// A dimensional constraint's literal or named driving value.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq)]
pub enum DrivingValue {
Literal(f64),
Named(String),
@@ -458,7 +457,7 @@ pub fn parse(source: &str) -> Result<Expr, ParamError> {
/// One named parameter: a name plus its formula, both the original typed
/// text (`source`, for redisplay/re-editing) and the parsed [`Expr`]
/// (`expr`, for evaluation).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq)]
pub struct Parameter {
pub name: String,
pub source: String,
@@ -468,11 +467,11 @@ pub struct Parameter {
/// Every named parameter for one document.
///
/// `parameters` is intentionally not `pub` (unlike the sibling
/// `SketchConstraintSet::constraints`): this table's entire value is that
/// `ParametricConstraintSet::constraints`): this table's entire value is that
/// every stored formula is already known to be parse-valid and cycle-free,
/// an invariant only `set`/`remove` maintain. A direct external push could
/// silently violate it.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ParameterTable {
parameters: Vec<Parameter>,
}
@@ -932,20 +931,6 @@ mod tests {
assert_eq!(table.resolve("a"), Ok(2.0));
}
#[test]
fn table_round_trips_through_bincode() {
let mut table = ParameterTable::new();
table.set("hole_dia", "5").unwrap();
table.set("hole_spacing", "2 * hole_dia + 1.5").unwrap();
let bytes = bincode::serialize(&table).expect("serialize");
let restored: ParameterTable = bincode::deserialize(&bytes).expect("deserialize");
assert_eq!(restored.len(), 2);
assert_eq!(restored.resolve("hole_spacing"), Ok(11.5));
assert_eq!(restored.get("hole_dia").unwrap().source, "5");
}
#[test]
fn iter_preserves_insertion_order_across_redefinitions() {
let mut table = ParameterTable::new();
-210
View File
@@ -1,210 +0,0 @@
//! Persists the drawing-wide named-parameter table in a versioned XRecord.
use super::named_parameters::ParameterTable;
use super::Scene;
use bincode::Options;
const XRECORD_KEY: &str = "OCS_NAMED_PARAMETERS";
/// See `sketch_persist::MAX_CHUNK_BYTES`'s doc comment: the vendored DWG
/// writer truncates any single `XRecordValue::Chunk` entry to `u8::MAX`
/// bytes, so a table past a handful of parameters must be split across
/// multiple same-code `310` entries on write and concatenated back on read.
const MAX_CHUNK_BYTES: usize = u8::MAX as usize;
const MAX_RECORD_BYTES: usize = 8 * 1024 * 1024;
/// Prefixed onto the serialized blob so a future schema change can be
/// detected and gracefully skipped rather than silently misreading bytes —
/// same call-out as `sketch_persist::FORMAT_VERSION`, same caveat that the
/// migration path itself is still undesigned.
const FORMAT_VERSION: u8 = 1;
fn encode(table: &ParameterTable) -> Option<Vec<u8>> {
let mut bytes = vec![FORMAT_VERSION];
let body = bincode::serialize(table).ok()?;
if body.len() >= MAX_RECORD_BYTES {
return None;
}
bytes.extend(body);
Some(bytes)
}
fn decode(bytes: &[u8]) -> Option<ParameterTable> {
let (&version, body) = bytes.split_first()?;
if version != FORMAT_VERSION {
return None;
}
bincode::DefaultOptions::new()
.with_fixint_encoding()
.allow_trailing_bytes()
.with_limit(MAX_RECORD_BYTES as u64)
.deserialize(body)
.ok()
}
impl Scene {
/// Writes `named_parameters` into the document's `OCS_NAMED_PARAMETERS`
/// XRecord, ready for whatever save call happens next to serialize
/// `self.document` as-is. Called from the same save entry points as
/// `materialize_sketch_constraints_for_save`
/// (`OpenCADStudio::prepare_native_save`, the wasm save path).
///
/// An empty table is skipped rather than writing an empty XRecord — an
/// unconstrained-by-parameters drawing (the common case, at least until
/// this feature is used) should not gain persisted-but-empty
/// bookkeeping on every save.
pub(crate) fn materialize_named_parameters_for_save(&mut self) {
if self.named_parameters.is_empty() {
return;
}
let Some(bytes) = encode(&self.named_parameters) else {
return;
};
let owner = self.document.header.model_space_block_handle;
if owner.is_null() {
return;
}
self.document.ensure_xrecord(owner, XRECORD_KEY);
if let Some(record) = self.document.xrecord_mut(owner, XRECORD_KEY) {
// Overwrite, not append: a resave must replace the prior blob,
// not accumulate more Chunk entries every time.
record.entries.clear();
for chunk in bytes.chunks(MAX_CHUNK_BYTES) {
record.entries.push(acadrust::objects::XRecordEntry::new(
310,
acadrust::objects::XRecordValue::Chunk(chunk.to_vec()),
));
}
}
}
/// Populates `named_parameters` from the just-loaded `self.document`'s
/// `OCS_NAMED_PARAMETERS` XRecord, if any — called right after a
/// document open installs its `CadDocument` into this `Scene`, the same
/// call sites `load_sketch_constraints_from_document` already has
/// (`OpenCADStudio::on_file_opened`, the automation `"open"`/`"new"`
/// ops). Replaces whatever was already in `named_parameters` (a fresh
/// table for a real open; `"new"`'s blank document has nothing to find
/// anyway).
pub(crate) fn load_named_parameters_from_document(&mut self) {
self.named_parameters = ParameterTable::new();
let owner = self.document.header.model_space_block_handle;
if owner.is_null() {
return;
}
let Some(record) = self.document.xrecord(owner, XRECORD_KEY) else {
return;
};
// Concatenate every Chunk entry in order, not just the first — see
// `MAX_CHUNK_BYTES`'s doc comment.
let mut bytes = Vec::new();
for entry in &record.entries {
if let acadrust::objects::XRecordValue::Chunk(chunk) = &entry.value {
if bytes.len().saturating_add(chunk.len()) > MAX_RECORD_BYTES {
bytes.clear();
break;
}
bytes.extend_from_slice(chunk);
}
}
if bytes.is_empty() {
return;
}
if let Some(table) = decode(&bytes) {
self.named_parameters = table;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_parameter_table_survives_materialize_and_reload() {
let mut scene = Scene::new();
scene.named_parameters_mut().set("hole_dia", "5").unwrap();
scene
.named_parameters_mut()
.set("hole_spacing", "2 * hole_dia + 1.5")
.unwrap();
scene.materialize_named_parameters_for_save();
// Simulate the load-time side of a save/reload round trip: drop the
// in-memory table (a fresh open starts empty) and repopulate purely
// from what materialize just wrote into `document.objects`.
scene.named_parameters = ParameterTable::new();
scene.load_named_parameters_from_document();
assert_eq!(scene.named_parameters().len(), 2);
assert_eq!(scene.named_parameters().resolve("hole_spacing"), Ok(11.5));
assert_eq!(
scene.named_parameters().get("hole_dia").unwrap().source,
"5"
);
}
/// The above test only proves `materialize`/`load` agree with each
/// other on the same in-memory `CadDocument` — this drives a real
/// `save_to_bytes`/`load_bytes` round trip (the same primitives
/// `OpenCADStudio::on_file_opened`'s native save/open path uses),
/// through both supported formats, mirroring
/// `sketch_persist`'s own real-bytes tests.
fn full_bytes_roundtrip(ext: &str) -> ParameterTable {
let mut scene = Scene::new();
scene
.named_parameters_mut()
.set("plate_len", "42.5")
.unwrap();
scene
.named_parameters_mut()
.set("plate_width", "plate_len / 2")
.unwrap();
scene.materialize_named_parameters_for_save();
let bytes = crate::io::save_to_bytes(&scene.document, ext, scene.document.version)
.unwrap_or_else(|e| panic!("save to {ext} bytes: {e}"));
let mut reloaded_scene = Scene::new();
reloaded_scene.document =
crate::io::load_bytes(&format!("named_params_roundtrip.{ext}"), bytes)
.unwrap_or_else(|e| panic!("reload {ext} bytes: {e}"));
reloaded_scene.load_named_parameters_from_document();
reloaded_scene.named_parameters
}
#[test]
fn a_parameter_table_survives_a_real_dxf_save_and_load_round_trip() {
let restored = full_bytes_roundtrip("dxf");
assert_eq!(restored.len(), 2);
assert_eq!(restored.resolve("plate_width"), Ok(21.25));
}
#[test]
fn a_parameter_table_survives_a_real_dwg_save_and_load_round_trip() {
let restored = full_bytes_roundtrip("dwg");
assert_eq!(restored.len(), 2);
assert_eq!(restored.resolve("plate_width"), Ok(21.25));
}
#[test]
fn an_empty_table_is_not_materialized() {
let mut scene = Scene::new();
scene.materialize_named_parameters_for_save();
let owner = scene.document.header.model_space_block_handle;
assert!(
scene.document.xrecord(owner, XRECORD_KEY).is_none(),
"an empty table should not create an XRecord"
);
}
#[test]
fn decode_rejects_a_mismatched_format_version() {
let mut bytes = vec![FORMAT_VERSION.wrapping_add(1)];
bytes.extend(bincode::serialize(&ParameterTable::new()).unwrap());
assert!(
decode(&bytes).is_none(),
"a future/unknown format version must be rejected, not misread"
);
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
-393
View File
@@ -1,393 +0,0 @@
//! Persists each sketch constraint set in a versioned, chunked XRecord.
use super::sketch_constraints::SketchConstraintSet;
use super::Scene;
use acadrust::{CadDocument, Handle};
use bincode::Options;
const XRECORD_KEY: &str = "OCS_SKETCH_CONSTRAINTS";
/// The vendored DWG writer's `XRecordValue::Chunk` encoder packs a single
/// `u8` length prefix per entry and silently truncates anything longer
/// (`cadcodec`'s `io/dwg/dwg_stream_writers/object_writer/objects.rs`,
/// `encode_xrecord_entries`) — unlike the DXF writer, which has no such
/// limit. A constraint set past a handful of entries serializes past 255
/// bytes easily, so a single `XRecordEntry` silently loses the tail and
/// `bincode::deserialize` then fails on the truncated bytes, dropping the
/// *entire* scope's constraints on the next DWG load — reproduced and
/// root-caused live (not from a change log): a 5-entry set round-tripped
/// clean, a 6-entry set of the same shapes lost everything. Splitting the
/// blob across as many same-code `310` entries as needed (below) works
/// around it without touching the vendored crate — `CadDocument::xrecord`'s
/// `entries: Vec<XRecordEntry>` already supports repeating a code.
const MAX_CHUNK_BYTES: usize = u8::MAX as usize;
const MAX_RECORD_BYTES: usize = 8 * 1024 * 1024;
/// Removes a named XRecord from `owner`'s extension dictionary, if present.
/// The dictionary itself is left in place even if now empty — other code may
/// already reference it via `xdictionary_handle`, and an empty extension
/// dictionary is harmless bookkeeping, unlike a stale XRecord that would
/// resurrect deleted data on the next load.
fn remove_xrecord(document: &mut CadDocument, owner: Handle, key: &str) {
let Some(dictionary_handle) = document.extension_dictionary_handle(owner) else {
return;
};
let Some(acadrust::objects::ObjectType::Dictionary(dictionary)) =
document.objects.get_mut(&dictionary_handle)
else {
return;
};
let Some(index) = dictionary
.entries
.iter()
.position(|(name, _)| name.eq_ignore_ascii_case(key))
else {
return;
};
let (_, record_handle) = dictionary.entries.remove(index);
document.objects.remove(&record_handle);
}
/// Serialized schema version. Unknown versions are skipped without affecting
/// the rest of the drawing.
const FORMAT_VERSION: u8 = 2;
fn encode(set: &SketchConstraintSet) -> Option<Vec<u8>> {
let mut bytes = vec![FORMAT_VERSION];
let body = bincode::serialize(set).ok()?;
if body.len() >= MAX_RECORD_BYTES {
return None;
}
bytes.extend(body);
Some(bytes)
}
fn decode(bytes: &[u8]) -> Option<SketchConstraintSet> {
let (&version, body) = bytes.split_first()?;
if version != FORMAT_VERSION {
return None;
}
bincode::DefaultOptions::new()
.with_fixint_encoding()
.allow_trailing_bytes()
.with_limit(MAX_RECORD_BYTES as u64)
.deserialize(body)
.ok()
}
impl Scene {
/// Writes every current `sketch_constraints` entry into its scope's
/// owner-handle XRecord, ready for whatever save call happens next to
/// serialize `self.document` as-is. Called right before a native/web
/// save (`OpenCADStudio::prepare_native_save` and the wasm save path)
/// — see those call sites' doc comments for why "right before save"
/// rather than keeping the XRecord resident and live-updated (design
/// doc §5.2's "live" model, deferred: it would also let constraint-set
/// edits ride the ordinary object-delta undo path per §5.1(b), but this
/// lazy model was the doc's own recommended first step).
///
/// A scope with an empty constraint set writes no XRecord — an
/// unconstrained drawing (the common case) should not gain
/// persisted-but-empty bookkeeping on every save. If a *previous* save
/// already materialized a non-empty XRecord for that scope, the now-empty
/// set actively removes it instead: leaving the stale blob in place would
/// resurrect the deleted constraints the next time the document loads.
pub(crate) fn materialize_sketch_constraints_for_save(&mut self) {
for index in 0..self.sketch_constraints.len() {
let set = &self.sketch_constraints[index];
if set.constraints.is_empty() {
let owner = set.scope.owner_handle(&self.document);
if !owner.is_null() {
remove_xrecord(&mut self.document, owner, XRECORD_KEY);
}
continue;
}
let owner = set.scope.owner_handle(&self.document);
if owner.is_null() {
continue;
}
let Some(bytes) = encode(set) else { continue };
self.document.ensure_xrecord(owner, XRECORD_KEY);
if let Some(record) = self.document.xrecord_mut(owner, XRECORD_KEY) {
// Overwrite, not append: a resave must replace the prior
// blob, not accumulate more Chunk entries every time.
record.entries.clear();
for chunk in bytes.chunks(MAX_CHUNK_BYTES) {
record.entries.push(acadrust::objects::XRecordEntry::new(
310,
acadrust::objects::XRecordValue::Chunk(chunk.to_vec()),
));
}
}
}
}
/// Populates `sketch_constraints` from every `OCS_SKETCH_CONSTRAINTS`
/// XRecord found in the just-loaded `self.document` — called right
/// after a document open installs its `CadDocument` into this `Scene`
/// (`OpenCADStudio::on_file_opened` and the automation `"open"`/`"new"`
/// ops). Every `BlockRecord` (model space, paper space, and named
/// block definitions) is a candidate owner; most will have no such
/// XRecord and are skipped cheaply. Replaces whatever was already in
/// `sketch_constraints` (a fresh/empty `Vec` for a real open; `"new"`'s
/// blank document has nothing to find anyway).
pub(crate) fn load_sketch_constraints_from_document(&mut self) {
self.sketch_constraints.clear();
let owners: Vec<acadrust::Handle> = self
.document
.block_records
.iter()
.map(|record| record.handle)
.collect();
for owner in owners {
if let Some(record) = self.document.xrecord(owner, XRECORD_KEY) {
let mut bytes = Vec::new();
for entry in &record.entries {
if let acadrust::objects::XRecordValue::Chunk(chunk) = &entry.value {
if bytes.len().saturating_add(chunk.len()) > MAX_RECORD_BYTES {
bytes.clear();
break;
}
bytes.extend_from_slice(chunk);
}
}
if let Some(set) = decode(&bytes) {
self.sketch_constraints.push(set);
}
continue;
}
if let Some(set) = super::dwg_native_constraints::native_constraint_set(
&self.document,
owner,
&mut self.named_parameters,
) {
self.sketch_constraints.push(set);
}
}
}
}
#[cfg(test)]
mod tests {
use super::super::sketch_constraints::{ConstraintKind, SketchRef, SketchScope};
use super::*;
use acadrust::entities::EntityType;
use acadrust::types::Vector3;
#[test]
fn a_model_space_constraint_set_survives_materialize_and_reload() {
let mut scene = Scene::new();
let a = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(10.0, 0.0, 0.0),
)));
scene
.sketch_constraint_set_mut(SketchScope::ModelSpace)
.add(ConstraintKind::Horizontal, vec![SketchRef::whole(a)], None);
scene.materialize_sketch_constraints_for_save();
// Simulate the load-time side of a save/reload round trip: drop the
// in-memory sets (a fresh open starts with none) and repopulate
// purely from what materialize just wrote into `document.objects`.
scene.sketch_constraints.clear();
scene.load_sketch_constraints_from_document();
let set = scene
.sketch_constraint_set(SketchScope::ModelSpace)
.expect("constraint set should have been reloaded");
assert_eq!(set.constraints.len(), 1);
assert_eq!(set.constraints[0].kind, ConstraintKind::Horizontal);
assert_eq!(set.constraints[0].refs, vec![SketchRef::whole(a)]);
}
/// The above test only proves `materialize`/`load` agree with each
/// other on the same in-memory `CadDocument` — it never actually
/// serializes anything. This drives a real `save_to_bytes`/`load_bytes`
/// round trip (the same primitives `OpenCADStudio::on_file_opened`'s
/// native save/open path uses), through both supported formats, closing
/// the gap stage 2's spike proved for the raw XRecord mechanism up to
/// this stage's actual app-level wiring.
fn full_bytes_roundtrip(ext: &str) -> SketchConstraintSet {
let mut scene = Scene::new();
let a = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(10.0, 5.0, 0.0),
)));
let b = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
Vector3::new(10.0, 5.0, 0.0),
Vector3::new(20.0, 5.0, 0.0),
)));
scene
.sketch_constraint_set_mut(SketchScope::ModelSpace)
.add(
ConstraintKind::Coincident,
vec![SketchRef::point(a, 1), SketchRef::point(b, 0)],
None,
);
scene
.sketch_constraint_set_mut(SketchScope::ModelSpace)
.add(
ConstraintKind::Distance,
vec![SketchRef::whole(b)],
Some(crate::scene::named_parameters::DrivingValue::Literal(12.5)),
);
scene.materialize_sketch_constraints_for_save();
let bytes = crate::io::save_to_bytes(&scene.document, ext, scene.document.version)
.unwrap_or_else(|e| panic!("save to {ext} bytes: {e}"));
let mut reloaded_scene = Scene::new();
reloaded_scene.document = crate::io::load_bytes(&format!("stage4_roundtrip.{ext}"), bytes)
.unwrap_or_else(|e| panic!("reload {ext} bytes: {e}"));
reloaded_scene.load_sketch_constraints_from_document();
reloaded_scene
.sketch_constraint_set(SketchScope::ModelSpace)
.cloned()
.unwrap_or_else(|| {
panic!("no ModelSpace constraint set survived the {ext} save/load round trip")
})
}
#[test]
fn a_constraint_set_survives_a_real_dxf_save_and_load_round_trip() {
let restored = full_bytes_roundtrip("dxf");
assert_eq!(restored.constraints.len(), 2);
assert_eq!(restored.constraints[0].kind, ConstraintKind::Coincident);
assert_eq!(restored.constraints[1].kind, ConstraintKind::Distance);
assert_eq!(
restored.constraints[1].driving_param,
Some(crate::scene::named_parameters::DrivingValue::Literal(12.5))
);
}
#[test]
fn a_constraint_set_survives_a_real_dwg_save_and_load_round_trip() {
let restored = full_bytes_roundtrip("dwg");
assert_eq!(restored.constraints.len(), 2);
assert_eq!(restored.constraints[0].kind, ConstraintKind::Coincident);
assert_eq!(restored.constraints[1].kind, ConstraintKind::Distance);
assert_eq!(
restored.constraints[1].driving_param,
Some(crate::scene::named_parameters::DrivingValue::Literal(12.5))
);
}
#[test]
fn a_block_scoped_constraint_set_survives_materialize_and_reload() {
let mut scene = Scene::new();
let a = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
)));
// `create_block_from_entities` returns the placed INSERT's handle,
// not the BlockRecord's own handle — `SketchScope::Block` needs the
// latter (it's what `BlockEditSession::br_handle` and
// `SketchScope::owner_handle` both mean by "a block definition's
// handle"), so look it up from the table by name instead of using
// the return value directly.
let _insert_handle = scene
.create_block_from_entities(
&[a],
"OCS_STAGE4_TEST_BLOCK",
&acadrust::types::Transform::identity(),
&acadrust::types::Transform::identity(),
)
.expect("create a block definition to own the constraint set");
let br_handle = scene
.document
.block_records
.get("OCS_STAGE4_TEST_BLOCK")
.expect("block record should exist")
.handle;
scene
.sketch_constraint_set_mut(SketchScope::Block(br_handle))
.add(ConstraintKind::Vertical, vec![SketchRef::whole(a)], None);
scene.materialize_sketch_constraints_for_save();
scene.sketch_constraints.clear();
scene.load_sketch_constraints_from_document();
let set = scene
.sketch_constraint_set(SketchScope::Block(br_handle))
.expect("block-scoped constraint set should have been reloaded");
assert_eq!(set.constraints.len(), 1);
assert_eq!(set.constraints[0].kind, ConstraintKind::Vertical);
}
#[test]
fn an_empty_constraint_set_is_not_materialized() {
// A scope touched (e.g. `sketch_constraint_set_mut` called once,
// then every constraint removed) but left with zero constraints
// should not persist an empty XRecord.
let mut scene = Scene::new();
let _ = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
scene.materialize_sketch_constraints_for_save();
let owner = scene.document.header.model_space_block_handle;
assert!(
scene.document.xrecord(owner, XRECORD_KEY).is_none(),
"an empty constraint set should not create an XRecord"
);
}
#[test]
fn clearing_a_previously_materialized_set_removes_its_stale_xrecord() {
// Regression for the audit-flagged case `an_empty_constraint_set_is_not_materialized`
// doesn't cover: a scope that already had a *non-empty* XRecord from a
// prior save must have that XRecord actively removed once its
// in-memory set goes back to empty — otherwise the stale blob
// survives the save and resurrects the deleted constraints on the
// next load.
let mut scene = Scene::new();
let a = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(10.0, 0.0, 0.0),
)));
scene
.sketch_constraint_set_mut(SketchScope::ModelSpace)
.add(ConstraintKind::Horizontal, vec![SketchRef::whole(a)], None);
scene.materialize_sketch_constraints_for_save();
let owner = scene.document.header.model_space_block_handle;
assert!(
scene.document.xrecord(owner, XRECORD_KEY).is_some(),
"sanity: the first save should have materialized an XRecord"
);
// Clear every constraint (mirrors deleting the constrained entity, or
// removing the last constraint by hand) and save again.
scene
.sketch_constraint_set_mut(SketchScope::ModelSpace)
.constraints
.clear();
scene.materialize_sketch_constraints_for_save();
assert!(
scene.document.xrecord(owner, XRECORD_KEY).is_none(),
"the stale XRecord from the earlier non-empty save must be removed, not left behind"
);
// And the load path must agree: nothing resurrects on reload.
scene.sketch_constraints.clear();
scene.load_sketch_constraints_from_document();
assert!(
scene
.sketch_constraint_set(SketchScope::ModelSpace)
.is_none(),
"no constraints should come back after the set was cleared and re-saved"
);
}
#[test]
fn decode_rejects_a_mismatched_format_version() {
let mut bytes = vec![FORMAT_VERSION.wrapping_add(1)];
bytes.extend(
bincode::serialize(&SketchConstraintSet::new(SketchScope::ModelSpace)).unwrap(),
);
assert!(
decode(&bytes).is_none(),
"a future/unknown format version must be rejected, not misread"
);
}
}
+96 -95
View File
@@ -1,13 +1,11 @@
//! Bottom status bar — Model/Layout tabs + OSNAP toggle + status info
pub mod status_menu;
pub mod statusbar_config;
pub mod statusbar_menu;
pub mod status_menu;
use iced::widget::tooltip::Position as TipPos;
use iced::widget::{
button, column, container, mouse_area, row, text, text_input, tooltip,
};
use iced::widget::{button, column, container, mouse_area, row, text, text_input, tooltip};
use iced::{Background, Border, Color, Element, Length, Theme};
use iced_aw::ContextMenu;
use std::sync::Arc;
@@ -23,10 +21,10 @@ pub const LAYOUT_RENAME_INPUT_ID: &str = "layout_rename_input";
use crate::app::Message;
use crate::snap::Snapper;
use crate::ui::statusbar::statusbar_config::{StatusBarConfig, StatusPill};
use crate::ui::statusbar::status_menu::Entry as StatusMenuEntry;
use crate::ui::wrap_bar::WrapBar;
use crate::t;
use crate::ui::statusbar::status_menu::Entry as StatusMenuEntry;
use crate::ui::statusbar::statusbar_config::{StatusBarConfig, StatusPill};
use crate::ui::wrap_bar::WrapBar;
/// Height of one status-bar row. Matches the drawing-tab strip above it so the
/// three horizontal strips — tabs, status bar, command line — line up, and it
@@ -125,16 +123,16 @@ impl StatusBar {
// Which pills the user has chosen to show on the bar.
config: &'a StatusBarConfig,
menu_data: StatusMenuData<'a>,
// Remaining degrees of freedom for the current sketch scope.
// Remaining degrees of freedom for the current parametric scope.
// `None` when the scope
// has no `SketchConstraintSet` yet (nothing constrained), so the
// has no `ParametricConstraintSet` yet (nothing constrained), so the
// badge stays invisible until it's actually relevant.
sketch_dof: Option<usize>,
parametric_dof: Option<usize>,
// Number of redundant/conflicting constraints in the current scope.
// Zero hides
// the pill entirely, so an ordinarily/fully-constrained drawing sees
// no new clutter.
sketch_conflicts: usize,
parametric_conflicts: usize,
// What is drawing the scene. Only a degraded verdict shows anything.
gpu_status: &'a crate::scene::pipeline::GpuStatus,
) -> Element<'a, Message> {
@@ -157,9 +155,7 @@ impl StatusBar {
} else {
crate::ui::icons::themed_secondary(crate::ui::icons::MENU, 16.0)
};
let menu_button = button(menu_icon)
.style(button::subtle)
.padding([4, 8]);
let menu_button = button(menu_icon).style(button::subtle).padding([4, 8]);
let menu_btn = if is_start {
tip(
menu_button.into(),
@@ -270,9 +266,9 @@ impl StatusBar {
);
}
// Not a `StatusPill` (so not user-hideable yet): invisible until the
// current scope actually has a SketchConstraintSet, so a drawing
// current scope actually has a ParametricConstraintSet, so a drawing
// that never uses parametric constraints sees no new clutter.
if let Some(dof) = sketch_dof {
if let Some(dof) = parametric_dof {
let pill = if dof == 0 {
success_pill(crate::tf!("DOF: {dof}").into_owned())
} else {
@@ -284,21 +280,21 @@ impl StatusBar {
if dof == 0 {
t!("Fully constrained — no remaining degrees of freedom")
} else {
t!("Remaining degrees of freedom in the current sketch's persistent constraints")
t!("Remaining degrees of freedom in the current parametric constraint scope")
},
)
.into(),
);
}
// Each click removes one flagged constraint.
if sketch_conflicts > 0 {
if parametric_conflicts > 0 {
pills.push(
tip(
action_pill(
crate::tf!("⚠ {sketch_conflicts} conflicting").into_owned(),
Message::ResolveOneSketchConflict,
crate::tf!("⚠ {parametric_conflicts} conflicting").into_owned(),
Message::ResolveOneParametricConflict,
),
t!("One or more constraints in this sketch conflict or are redundant\nClick to remove one and re-solve"),
t!("One or more parametric constraints conflict or are redundant\nClick to remove one and re-solve"),
)
.into(),
);
@@ -315,7 +311,11 @@ impl StatusBar {
if vis(StatusPill::Lwt) {
pills.push(
tip(
toggle_pill(crate::ui::icons::ST_LWT, lineweight_display, Message::ToggleLineweightDisplay),
toggle_pill(
crate::ui::icons::ST_LWT,
lineweight_display,
Message::ToggleLineweightDisplay,
),
t!("Show Lineweight\nLWDISPLAY"),
)
.into(),
@@ -462,10 +462,7 @@ impl StatusBar {
t!("Isolate Objects\nClick for Isolate / Hide / End"),
tooltip_hidden,
),
crate::ui::popup::isolate_popup::menu_entries(
has_selection,
isolation_active,
),
crate::ui::popup::isolate_popup::menu_entries(has_selection, isolation_active),
160.0,
)
.into(),
@@ -474,7 +471,11 @@ impl StatusBar {
if vis(StatusPill::QuickProps) {
pills.push(
tip(
toggle_pill(crate::ui::icons::ST_QUICKPROPS, quick_properties, Message::ToggleQuickProperties),
toggle_pill(
crate::ui::icons::ST_QUICKPROPS,
quick_properties,
Message::ToggleQuickProperties,
),
t!("Quick Properties\nFloating panel on selection"),
)
.into(),
@@ -504,7 +505,11 @@ impl StatusBar {
if vis(StatusPill::SelCycle) {
pills.push(
tip(
toggle_pill(crate::ui::icons::ST_SELCYCLE, selection_cycling, Message::ToggleSelectionCycling),
toggle_pill(
crate::ui::icons::ST_SELCYCLE,
selection_cycling,
Message::ToggleSelectionCycling,
),
t!("Selection Cycling\nRepeat-click to step through overlapping objects"),
)
.into(),
@@ -522,7 +527,11 @@ impl StatusBar {
if vis(StatusPill::CleanScreen) {
pills.push(
tip(
toggle_pill(crate::ui::icons::ST_CLEANSCREEN, clean_screen, Message::ToggleCleanScreen),
toggle_pill(
crate::ui::icons::ST_CLEANSCREEN,
clean_screen,
Message::ToggleCleanScreen,
),
t!("Clean Screen\nHide ribbon and panels"),
)
.into(),
@@ -607,13 +616,13 @@ impl StatusBar {
.style(|theme: &Theme| {
let palette = theme.palette();
container::Style {
background: Some(Background::Color(palette.background.base.color)),
border: Border {
color: palette.background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
..Default::default()
background: Some(Background::Color(palette.background.base.color)),
border: Border {
color: palette.background.neutral.color,
width: 1.0,
radius: 0.0.into(),
},
..Default::default()
}
})
.width(Length::Fill)
@@ -634,7 +643,12 @@ impl StatusBar {
/// drawing set to architectural units read its coordinates in decimals, and one
/// asking for two places got four. `format_length` is the same helper the
/// properties panel formats through, so both now say a length the same way.
fn format_coords(cursor: glam::DVec3, last: Option<glam::DVec3>, mode: i16, picking: bool) -> String {
fn format_coords(
cursor: glam::DVec3,
last: Option<glam::DVec3>,
mode: i16,
picking: bool,
) -> String {
use crate::entities::common::format_length as len;
let abs = |p: glam::DVec3| format!("{}, {}, {}", len(p.x), len(p.y), len(p.z));
match mode {
@@ -666,11 +680,14 @@ fn format_coords(cursor: glam::DVec3, last: Option<glam::DVec3>, mode: i16, pick
// ── Customization handle ──────────────────────────────────────────────────
fn customize_btn() -> Element<'static, Message> {
button(crate::ui::icons::themed_secondary(crate::ui::icons::MENU, 16.0))
.on_press(Message::StatusMenuTooltipHidden(true))
.style(button::subtle)
.padding([4, 8])
.into()
button(crate::ui::icons::themed_secondary(
crate::ui::icons::MENU,
16.0,
))
.on_press(Message::StatusMenuTooltipHidden(true))
.style(button::subtle)
.padding([4, 8])
.into()
}
// ── Tooltip helper ────────────────────────────────────────────────────────
@@ -689,11 +706,7 @@ fn menu_tip<'a>(
label: std::borrow::Cow<'static, str>,
hidden: bool,
) -> Element<'a, Message> {
let content = if hidden {
content
} else {
tip(content, label)
};
let content = if hidden { content } else { tip(content, label) };
mouse_area(content)
.on_exit(Message::StatusMenuTooltipHidden(false))
@@ -763,21 +776,21 @@ fn split_pill<'a>(
.style(move |theme: &Theme| {
let palette = theme.palette();
container::Style {
background: Some(Background::Color(if active {
palette.primary.weak.color
} else {
palette.background.weakest.color
})),
border: Border {
color: if active {
palette.primary.base.color
background: Some(Background::Color(if active {
palette.primary.weak.color
} else {
palette.background.neutral.color
palette.background.weakest.color
})),
border: Border {
color: if active {
palette.primary.base.color
} else {
palette.background.neutral.color
},
width: 1.0,
radius: 2.0.into(),
},
width: 1.0,
radius: 2.0.into(),
},
..Default::default()
..Default::default()
}
})
.padding([4, 6])
@@ -880,8 +893,8 @@ fn osnap_btn<'a>(
};
let main = tip(
mouse_area(snap_icon)
.on_press(Message::ToggleSnapEnabled)
.into(),
.on_press(Message::ToggleSnapEnabled)
.into(),
t!("Object Snap: toggle on/off\nF3"),
);
@@ -926,14 +939,10 @@ fn layout_tab_context_menu(name: String) -> Element<'static, Message> {
.on_press(Message::LayoutDelete(name))
.interaction(iced::mouse::Interaction::Pointer);
container(
column![rename, delete]
.spacing(0)
.width(160),
)
.style(container::bordered_box)
.padding([4, 0])
.into()
container(column![rename, delete].spacing(0).width(160))
.style(container::bordered_box)
.padding([4, 0])
.into()
}
/// A layout tab button.
@@ -1020,12 +1029,7 @@ fn space_tab<'a>(
let report_key = format!("{report_key_prefix}:{label}");
let tab = mouse_area(display).on_press(switch_msg);
let tab: Element<'a, Message> = if has_context_menu {
crate::ui::wrap_bar::ReorderTab::layout(
label.clone(),
reorderable_layouts,
tab,
)
.into()
crate::ui::wrap_bar::ReorderTab::layout(label.clone(), reorderable_layouts, tab).into()
} else {
tab.into()
};
@@ -1035,11 +1039,7 @@ fn space_tab<'a>(
} else {
tab
};
crate::ui::wrap_bar::PosReport::owned(
report_key,
tab,
)
.into()
crate::ui::wrap_bar::PosReport::owned(report_key, tab).into()
}
}
@@ -1051,7 +1051,7 @@ fn space_tab<'a>(
fn space_mode_btn(current_layout: &str, in_mspace: bool) -> Element<'static, Message> {
let is_model_tab = current_layout == "Model";
// Labels and styling follow AutoCAD convention:
// Labels and styling follow the conventional drafting layout:
// PAPER = currently in paper-space editing
// MODEL = currently in model-space editing (either the Model tab or MSPACE)
let (label, active, on_press) = if is_model_tab {
@@ -1089,9 +1089,9 @@ fn space_mode_btn(current_layout: &str, in_mspace: bool) -> Element<'static, Mes
fn status_pill(label: impl Into<String>) -> Element<'static, Message> {
container(text(label.into()).size(12))
.style(container::bordered_box)
.padding([4, 8])
.into()
.style(container::bordered_box)
.padding([4, 8])
.into()
}
/// A success-colored status pill used when no degrees of freedom remain.
@@ -1123,7 +1123,11 @@ fn success_pill(label: impl Into<String>) -> Element<'static, Message> {
container::Style {
background: Some(Background::Color(palette.success.weak.color)),
text_color: Some(palette.success.weak.text),
border: Border { color: palette.success.base.color, width: 1.0, radius: 4.0.into() },
border: Border {
color: palette.success.base.color,
width: 1.0,
radius: 4.0.into(),
},
..Default::default()
}
})
@@ -1141,10 +1145,10 @@ fn popup_pill(label: impl Into<String>) -> Element<'static, Message> {
fn action_pill(label: impl Into<String>, msg: Message) -> Element<'static, Message> {
let label = label.into();
button(text(label).size(12))
.on_press(msg)
.style(button::subtle)
.padding([4, 7])
.into()
.on_press(msg)
.style(button::subtle)
.padding([4, 7])
.into()
}
// ── Scale display ─────────────────────────────────────────────────────────
@@ -1196,13 +1200,10 @@ fn active_scale_label(
.iter()
.find(|(_, anno_scale, vp_scale)| {
if is_model {
(annotation_scale - *anno_scale).abs()
< 0.001 * annotation_scale.max(0.001)
(annotation_scale - *anno_scale).abs() < 0.001 * annotation_scale.max(0.001)
} else {
viewport_scale
.map(|current| {
(current - *vp_scale).abs() < 0.001 * vp_scale.max(0.001)
})
.map(|current| (current - *vp_scale).abs() < 0.001 * vp_scale.max(0.001))
.unwrap_or(false)
}
})
+6 -4
View File
@@ -9,7 +9,7 @@
//! Unlike the alias editor, each row also shows a live-computed resolved
//! value or error (`preview`, below) — built fresh from the whole buffer on
//! every render, not cached: the same "cheap enough to recompute from
//! scratch" philosophy `ParameterTable`/`sketch_solve.rs` already use
//! scratch" philosophy `ParameterTable`/`parametric_solve.rs` already use
//! throughout this project, and it means the preview reflects circular/
//! undefined-reference problems across rows immediately, before Apply.
@@ -393,7 +393,9 @@ mod tests {
#[test]
fn usage_lines_names_the_constraint_kind_and_its_entities() {
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef, SketchScope};
use crate::scene::parametric_constraints::{
ConstraintKind, ParametricRef, ParametricScope,
};
let mut scene = Scene::new();
let line = scene.add_entity(acadrust::EntityType::Line(
acadrust::entities::Line::from_points(
@@ -402,10 +404,10 @@ mod tests {
),
));
scene
.sketch_constraint_set_mut(SketchScope::ModelSpace)
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
.add(
ConstraintKind::Distance,
vec![SketchRef::point(line, 0), SketchRef::point(line, 1)],
vec![ParametricRef::point(line, 0), ParametricRef::point(line, 1)],
Some(crate::scene::named_parameters::DrivingValue::Named(
"gap".to_string(),
)),
-17
View File
@@ -145,7 +145,6 @@ impl<T> fmt::Display for Labelled<T> {
pub fn view_window<'a>(
default_save_format: &'a str,
file_assoc_enabled: bool,
write_dwg_native_constraints: bool,
show_constraint_values: bool,
ui_theme: &'a UiThemeConfig,
theme_color_inputs: &'a [String; 6],
@@ -352,22 +351,6 @@ pub fn view_window<'a>(
.size(11)
.width(sizing.width),
Space::new().height(14),
row![
iced::widget::checkbox(write_dwg_native_constraints)
.on_toggle(Message::WriteDwgNativeConstraintsChanged)
.size(15),
text(crate::t!("Write native constraint objects on save")).size(12),
]
.spacing(8)
.align_y(iced::Center),
Space::new().height(6),
text(crate::t!(
"Saves sketch constraints as native drawing objects alongside this app's own format. \
Off by default because it adds file size. Existing native objects stay synchronized."
))
.size(11)
.width(sizing.width),
Space::new().height(14),
row![
iced::widget::checkbox(show_constraint_values)
.on_toggle(Message::ShowConstraintValuesChanged)