forked from 1b-agent/opencadstudio-upstream
Standardize parametric constraints and interoperability
This commit is contained in:
1 parent
753df67ee4
commit
d738ebfcc6
41 files changed
+10038
-7847
No files matched your search
+66
-19
@@ -18,7 +18,7 @@
|
||||
use std::io::{BufRead, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use super::OpenCADStudio;
|
||||
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
+290
-191
@@ -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
@@ -6,11 +6,11 @@ use crate::scene::pick::grip::GripEdit;
|
||||
use crate::scene::GripDef;
|
||||
use crate::scene::{ObjectIsolationState, Scene};
|
||||
use crate::snap::SnapResult;
|
||||
use crate::t;
|
||||
use crate::ui::{LayerPanel, PropertiesPanel};
|
||||
use acadrust::tables::{normalize_name, Ucs};
|
||||
use acadrust::{CadDocument, EntityType, Handle};
|
||||
use iced;
|
||||
use crate::t;
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
@@ -240,14 +240,18 @@ impl DocumentTab {
|
||||
.and_then(|index| self.block_edits.get_mut(index))
|
||||
}
|
||||
|
||||
/// The [`crate::scene::sketch_constraints::SketchScope`] a new persistent
|
||||
/// The [`crate::scene::parametric_constraints::ParametricScope`] a new persistent
|
||||
/// constraint should attach to right now — whatever's actually being
|
||||
/// edited: the open block definition if a BEDIT session is active, model
|
||||
/// space otherwise.
|
||||
pub(super) fn current_sketch_scope(&self) -> crate::scene::sketch_constraints::SketchScope {
|
||||
pub(super) fn current_parametric_scope(
|
||||
&self,
|
||||
) -> crate::scene::parametric_constraints::ParametricScope {
|
||||
match self.active_block_edit_session() {
|
||||
Some(session) => crate::scene::sketch_constraints::SketchScope::Block(session.br_handle),
|
||||
None => crate::scene::sketch_constraints::SketchScope::ModelSpace,
|
||||
Some(session) => {
|
||||
crate::scene::parametric_constraints::ParametricScope::Block(session.br_handle)
|
||||
}
|
||||
None => crate::scene::parametric_constraints::ParametricScope::ModelSpace,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,8 +295,7 @@ impl DocumentTab {
|
||||
u.origin = h.model_space_ucs_origin;
|
||||
u.x_axis = h.model_space_ucs_x_axis;
|
||||
u.y_axis = h.model_space_ucs_y_axis;
|
||||
if h.model_space_ucs_name.is_empty()
|
||||
&& super::helpers::UcsXform::from_ucs(&u).is_identity()
|
||||
if h.model_space_ucs_name.is_empty() && super::helpers::UcsXform::from_ucs(&u).is_identity()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
@@ -475,31 +478,30 @@ impl DocumentTab {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let (origin, x_axis, y_axis, elevation, ortho, named, base) =
|
||||
match &self.active_ucs {
|
||||
Some(ucs) => (
|
||||
ucs.origin,
|
||||
ucs.x_axis,
|
||||
ucs.y_axis,
|
||||
ucs.elevation,
|
||||
ucs.ortho_type,
|
||||
if ucs.named_ucs_handle.is_valid() {
|
||||
ucs.named_ucs_handle
|
||||
} else {
|
||||
ucs.handle
|
||||
},
|
||||
ucs.base_ucs_handle,
|
||||
),
|
||||
None => (
|
||||
Vector3::ZERO,
|
||||
Vector3::UNIT_X,
|
||||
Vector3::UNIT_Y,
|
||||
0.0,
|
||||
0,
|
||||
Handle::NULL,
|
||||
Handle::NULL,
|
||||
),
|
||||
};
|
||||
let (origin, x_axis, y_axis, elevation, ortho, named, base) = match &self.active_ucs {
|
||||
Some(ucs) => (
|
||||
ucs.origin,
|
||||
ucs.x_axis,
|
||||
ucs.y_axis,
|
||||
ucs.elevation,
|
||||
ucs.ortho_type,
|
||||
if ucs.named_ucs_handle.is_valid() {
|
||||
ucs.named_ucs_handle
|
||||
} else {
|
||||
ucs.handle
|
||||
},
|
||||
ucs.base_ucs_handle,
|
||||
),
|
||||
None => (
|
||||
Vector3::ZERO,
|
||||
Vector3::UNIT_X,
|
||||
Vector3::UNIT_Y,
|
||||
0.0,
|
||||
0,
|
||||
Handle::NULL,
|
||||
Handle::NULL,
|
||||
),
|
||||
};
|
||||
for object in self.scene.document.objects.values_mut() {
|
||||
let acadrust::objects::ObjectType::Layout(layout) = object else {
|
||||
continue;
|
||||
@@ -656,7 +658,7 @@ impl DocumentTab {
|
||||
#[derive(Clone)]
|
||||
pub(super) enum HistorySnapshot {
|
||||
// Boxed: `DeltaSnapshot` grew past the other variants once
|
||||
// `sketch_constraints` (ERASE-undo fix) was added, and every undo/redo
|
||||
// `parametric_constraints` (ERASE-undo fix) was added, and every undo/redo
|
||||
// stack slot costs as much as this enum's largest variant regardless of
|
||||
// which one it actually holds.
|
||||
Delta(Box<DeltaSnapshot>),
|
||||
@@ -687,41 +689,33 @@ impl HistorySnapshot {
|
||||
)
|
||||
.saturating_add(d.selected_before.len().saturating_mul(16))
|
||||
.saturating_add(d.selected_after.len().saturating_mul(16))
|
||||
.saturating_add(d.active_layer.as_ref().map_or(0, |(before, after)| {
|
||||
before.len().saturating_add(after.len())
|
||||
}))
|
||||
.saturating_add(
|
||||
d.active_layer
|
||||
.as_ref()
|
||||
.map_or(0, |(before, after)| before.len().saturating_add(after.len())),
|
||||
)
|
||||
.saturating_add(
|
||||
d.sketch_constraints
|
||||
d.parametric_constraints
|
||||
.iter()
|
||||
.map(|entry| entry.before.constraints.len().saturating_add(entry.after.constraints.len()))
|
||||
.map(|entry| {
|
||||
entry
|
||||
.before
|
||||
.constraints
|
||||
.len()
|
||||
.saturating_add(entry.after.constraints.len())
|
||||
})
|
||||
.sum::<usize>()
|
||||
.saturating_mul(96),
|
||||
)
|
||||
.saturating_add(
|
||||
d.named_parameters
|
||||
.as_ref()
|
||||
.map_or(0, |(before, after)| before.len().saturating_add(after.len()).saturating_mul(128)),
|
||||
)
|
||||
.saturating_add(d.named_parameters.as_ref().map_or(0, |(before, after)| {
|
||||
before.len().saturating_add(after.len()).saturating_mul(128)
|
||||
}))
|
||||
.saturating_add(d.label.len()),
|
||||
HistorySnapshot::ObjectVisibility(v) => v
|
||||
.before
|
||||
.hidden
|
||||
.len()
|
||||
.saturating_add(
|
||||
v.before
|
||||
.keep
|
||||
.as_ref()
|
||||
.map_or(0, rustc_hash::FxHashSet::len),
|
||||
)
|
||||
.saturating_add(v.before.keep.as_ref().map_or(0, rustc_hash::FxHashSet::len))
|
||||
.saturating_add(v.after.hidden.len())
|
||||
.saturating_add(
|
||||
v.after
|
||||
.keep
|
||||
.as_ref()
|
||||
.map_or(0, rustc_hash::FxHashSet::len),
|
||||
)
|
||||
.saturating_add(v.after.keep.as_ref().map_or(0, rustc_hash::FxHashSet::len))
|
||||
.saturating_add(v.selected_before.len())
|
||||
.saturating_add(v.selected_after.len())
|
||||
.saturating_mul(16)
|
||||
@@ -760,12 +754,12 @@ pub(super) struct DeltaSnapshot {
|
||||
/// Opposite non-entity document state. `apply_delta_state` swaps this with
|
||||
/// the live structure, so the same allocation shuttles between undo/redo.
|
||||
pub(super) structure: Option<StructureSnapshot>,
|
||||
/// Sketch-constraint scopes this same command changed alongside its
|
||||
/// Parametric-constraint scopes this same command changed alongside its
|
||||
/// entities (e.g. ERASE removing a constraint set's touched constraints)
|
||||
/// — `sketch_constraints` lives on `Scene`, not in `document`/
|
||||
/// — `parametric_constraints` lives on `Scene`, not in `document`/
|
||||
/// `document.objects`, so it needs its own channel here rather than
|
||||
/// riding along with `entities`/`structure`. Almost always empty.
|
||||
pub(super) sketch_constraints: Vec<SketchConstraintsEntryDelta>,
|
||||
pub(super) parametric_constraints: Vec<ParametricConstraintsEntryDelta>,
|
||||
/// Drawing-wide parameter table changed by this transaction.
|
||||
pub(super) named_parameters: Option<(
|
||||
crate::scene::named_parameters::ParameterTable,
|
||||
@@ -831,13 +825,13 @@ pub(super) struct ObjectEntryDelta {
|
||||
pub(super) after: Option<acadrust::objects::ObjectType>,
|
||||
}
|
||||
|
||||
/// One sketch-constraint scope's before/after image within an entity delta.
|
||||
/// One parametric-constraint scope's before/after image within an entity delta.
|
||||
/// This keeps constraint cleanup atomic with edits such as entity erasure.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct SketchConstraintsEntryDelta {
|
||||
pub(super) scope: crate::scene::sketch_constraints::SketchScope,
|
||||
pub(super) before: crate::scene::sketch_constraints::SketchConstraintSet,
|
||||
pub(super) after: crate::scene::sketch_constraints::SketchConstraintSet,
|
||||
pub(super) struct ParametricConstraintsEntryDelta {
|
||||
pub(super) scope: crate::scene::parametric_constraints::ParametricScope,
|
||||
pub(super) before: crate::scene::parametric_constraints::ParametricConstraintSet,
|
||||
pub(super) after: crate::scene::parametric_constraints::ParametricConstraintSet,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
||||
+70
-59
@@ -1,7 +1,8 @@
|
||||
use super::{
|
||||
document::{
|
||||
DeltaSnapshot, HistorySnapshot, ObjectEntryDelta, ObjectVisibilitySnapshot,
|
||||
PendingHistorySnapshot, SketchConstraintsEntryDelta, StructureSnapshot, TableEntryDelta,
|
||||
ParametricConstraintsEntryDelta, PendingHistorySnapshot, StructureSnapshot,
|
||||
TableEntryDelta,
|
||||
},
|
||||
OpenCADStudio,
|
||||
};
|
||||
@@ -167,8 +168,7 @@ impl OpenCADStudio {
|
||||
) {
|
||||
self.finish_pending_history(i);
|
||||
let after = self.tabs[i].scene.object_isolation.clone();
|
||||
let selected_after: Vec<Handle> =
|
||||
self.tabs[i].scene.selected.iter().copied().collect();
|
||||
let selected_after: Vec<Handle> = self.tabs[i].scene.selected.iter().copied().collect();
|
||||
if before == after && selected_before == selected_after {
|
||||
return;
|
||||
}
|
||||
@@ -225,7 +225,7 @@ impl OpenCADStudio {
|
||||
dirty_after: true,
|
||||
active_layer: None,
|
||||
structure: (!objects.is_empty()).then_some(StructureSnapshot::Objects(objects)),
|
||||
sketch_constraints: Vec::new(),
|
||||
parametric_constraints: Vec::new(),
|
||||
named_parameters: None,
|
||||
label: label.into(),
|
||||
};
|
||||
@@ -233,9 +233,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
pub(super) fn defer_live_entity_history_after(&mut self, i: usize, handle: Handle) {
|
||||
let Some(HistorySnapshot::Delta(delta)) =
|
||||
self.tabs[i].history.undo_stack.last_mut()
|
||||
else {
|
||||
let Some(HistorySnapshot::Delta(delta)) = self.tabs[i].history.undo_stack.last_mut() else {
|
||||
return;
|
||||
};
|
||||
if let Some((_, _, after)) = delta
|
||||
@@ -254,9 +252,7 @@ impl OpenCADStudio {
|
||||
let selected_after = self.tabs[i].scene.selected.iter().copied().collect();
|
||||
let current_layout_after = self.tabs[i].scene.current_layout.clone();
|
||||
let dirty_after = self.tabs[i].dirty;
|
||||
let Some(HistorySnapshot::Delta(delta)) =
|
||||
self.tabs[i].history.undo_stack.last_mut()
|
||||
else {
|
||||
let Some(HistorySnapshot::Delta(delta)) = self.tabs[i].history.undo_stack.last_mut() else {
|
||||
return;
|
||||
};
|
||||
let Some((_, _, entry_after)) = delta
|
||||
@@ -335,8 +331,9 @@ impl OpenCADStudio {
|
||||
dirty_after,
|
||||
active_layer: (pending.active_layer != active_layer_after)
|
||||
.then_some((pending.active_layer, active_layer_after)),
|
||||
structure: structure_changed.then_some(StructureSnapshot::Full(pending.structure_before)),
|
||||
sketch_constraints: Vec::new(),
|
||||
structure: structure_changed
|
||||
.then_some(StructureSnapshot::Full(pending.structure_before)),
|
||||
parametric_constraints: Vec::new(),
|
||||
named_parameters: None,
|
||||
label: pending.label,
|
||||
};
|
||||
@@ -445,7 +442,7 @@ impl OpenCADStudio {
|
||||
dirty_after: self.tabs[i].dirty,
|
||||
active_layer: None,
|
||||
structure: Some(StructureSnapshot::Layers(entries)),
|
||||
sketch_constraints: Vec::new(),
|
||||
parametric_constraints: Vec::new(),
|
||||
named_parameters: None,
|
||||
label: pending.label,
|
||||
};
|
||||
@@ -502,7 +499,7 @@ impl OpenCADStudio {
|
||||
dirty_after: self.tabs[i].dirty,
|
||||
active_layer: None,
|
||||
structure: Some(StructureSnapshot::TextStyles(entries)),
|
||||
sketch_constraints: Vec::new(),
|
||||
parametric_constraints: Vec::new(),
|
||||
named_parameters: None,
|
||||
label: pending.label,
|
||||
};
|
||||
@@ -559,7 +556,7 @@ impl OpenCADStudio {
|
||||
dirty_after: self.tabs[i].dirty,
|
||||
active_layer: None,
|
||||
structure: Some(StructureSnapshot::DimStyles(entries)),
|
||||
sketch_constraints: Vec::new(),
|
||||
parametric_constraints: Vec::new(),
|
||||
named_parameters: None,
|
||||
label: pending.label,
|
||||
};
|
||||
@@ -624,7 +621,7 @@ impl OpenCADStudio {
|
||||
dirty_after: self.tabs[i].dirty,
|
||||
active_layer: None,
|
||||
structure: Some(StructureSnapshot::Objects(entries)),
|
||||
sketch_constraints: Vec::new(),
|
||||
parametric_constraints: Vec::new(),
|
||||
named_parameters: None,
|
||||
label: pending.label,
|
||||
};
|
||||
@@ -659,7 +656,7 @@ impl OpenCADStudio {
|
||||
dim_names,
|
||||
object_handles,
|
||||
}),
|
||||
sketch_constraints: Vec::new(),
|
||||
parametric_constraints: Vec::new(),
|
||||
named_parameters: None,
|
||||
label: "STYLE".to_string(),
|
||||
};
|
||||
@@ -741,15 +738,16 @@ impl OpenCADStudio {
|
||||
pending.label
|
||||
);
|
||||
}
|
||||
let (entity_before, object_before, sketch_constraints_before, named_parameters_before) =
|
||||
let (entity_before, object_before, parametric_constraints_before, named_parameters_before) =
|
||||
rec.into_recorded_images();
|
||||
let entities: Vec<(Handle, Option<Arc<EntityType>>, Option<Arc<EntityType>>)> = entity_before
|
||||
.into_iter()
|
||||
.map(|(h, before)| {
|
||||
let after = self.tabs[i].scene.document.get_entity_arc(h);
|
||||
(h, before, after)
|
||||
})
|
||||
.collect();
|
||||
let entities: Vec<(Handle, Option<Arc<EntityType>>, Option<Arc<EntityType>>)> =
|
||||
entity_before
|
||||
.into_iter()
|
||||
.map(|(h, before)| {
|
||||
let after = self.tabs[i].scene.document.get_entity_arc(h);
|
||||
(h, before, after)
|
||||
})
|
||||
.collect();
|
||||
let objects: Vec<ObjectEntryDelta> = object_before
|
||||
.into_iter()
|
||||
.filter_map(|(handle, before)| {
|
||||
@@ -761,21 +759,28 @@ impl OpenCADStudio {
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let sketch_constraints: Vec<SketchConstraintsEntryDelta> = sketch_constraints_before
|
||||
.into_iter()
|
||||
.filter_map(|(scope, before)| {
|
||||
let after = self.tabs[i]
|
||||
.scene
|
||||
.sketch_constraint_set(scope)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| crate::scene::sketch_constraints::SketchConstraintSet::new(scope));
|
||||
(before.constraints != after.constraints).then_some(SketchConstraintsEntryDelta {
|
||||
scope,
|
||||
before,
|
||||
after,
|
||||
let parametric_constraints: Vec<ParametricConstraintsEntryDelta> =
|
||||
parametric_constraints_before
|
||||
.into_iter()
|
||||
.filter_map(|(scope, before)| {
|
||||
let after = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(scope)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
crate::scene::parametric_constraints::ParametricConstraintSet::new(
|
||||
scope,
|
||||
)
|
||||
});
|
||||
(before.constraints != after.constraints).then_some(
|
||||
ParametricConstraintsEntryDelta {
|
||||
scope,
|
||||
before,
|
||||
after,
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
.collect();
|
||||
let named_parameters = named_parameters_before.and_then(|before| {
|
||||
let after = self.tabs[i].scene.named_parameters().clone();
|
||||
(before != after).then_some((before, after))
|
||||
@@ -815,7 +820,7 @@ impl OpenCADStudio {
|
||||
dirty_after,
|
||||
active_layer: None,
|
||||
structure,
|
||||
sketch_constraints,
|
||||
parametric_constraints,
|
||||
named_parameters,
|
||||
label: pending.label,
|
||||
};
|
||||
@@ -839,9 +844,10 @@ impl OpenCADStudio {
|
||||
if let Some(structure) = d.structure.as_mut() {
|
||||
match structure {
|
||||
StructureSnapshot::Full(stored) => {
|
||||
let inverse = self.tabs[i].scene.document.swap_structure(
|
||||
std::mem::replace(stored, acadrust::CadDocument::new()),
|
||||
);
|
||||
let inverse = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.swap_structure(std::mem::replace(stored, acadrust::CadDocument::new()));
|
||||
*stored = inverse;
|
||||
self.tabs[i].scene.invalidate_dependency_index();
|
||||
}
|
||||
@@ -945,9 +951,9 @@ impl OpenCADStudio {
|
||||
}
|
||||
let changes = self.tabs[i].scene.apply_entity_delta(&d.entities, undo);
|
||||
let scene = &mut self.tabs[i].scene;
|
||||
for entry in &d.sketch_constraints {
|
||||
for entry in &d.parametric_constraints {
|
||||
let value = if undo { &entry.before } else { &entry.after };
|
||||
*scene.sketch_constraint_set_mut(entry.scope) = value.clone();
|
||||
*scene.parametric_constraint_set_mut(entry.scope) = value.clone();
|
||||
}
|
||||
if let Some((before, after)) = &d.named_parameters {
|
||||
scene.named_parameters = if undo { before } else { after }.clone();
|
||||
@@ -1070,6 +1076,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
}
|
||||
scene.sync_native_parametric_graph();
|
||||
scene.clear_preview_wire();
|
||||
}
|
||||
// UCS state is cached separately from its persisted header/viewport
|
||||
@@ -1104,7 +1111,8 @@ impl OpenCADStudio {
|
||||
let available = self.tabs[i].history.undo_stack.len();
|
||||
let steps = steps.min(available);
|
||||
if steps == 0 {
|
||||
self.command_line.push_info(crate::t!("Nothing to undo.").as_ref());
|
||||
self.command_line
|
||||
.push_info(crate::t!("Nothing to undo.").as_ref());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1127,14 +1135,15 @@ impl OpenCADStudio {
|
||||
structure,
|
||||
StructureSnapshot::Full(_) | StructureSnapshot::Layers(_)
|
||||
)
|
||||
}) || d.active_layer.is_some() || d.entities.iter().any(|(_, before, after)| {
|
||||
before
|
||||
.as_deref()
|
||||
.is_some_and(|entity| matches!(entity, EntityType::Viewport(_)))
|
||||
|| after
|
||||
}) || d.active_layer.is_some()
|
||||
|| d.entities.iter().any(|(_, before, after)| {
|
||||
before
|
||||
.as_deref()
|
||||
.is_some_and(|entity| matches!(entity, EntityType::Viewport(_)))
|
||||
});
|
||||
|| after
|
||||
.as_deref()
|
||||
.is_some_and(|entity| matches!(entity, EntityType::Viewport(_)))
|
||||
});
|
||||
had_full |= d.structure.as_ref().is_some_and(StructureSnapshot::is_full);
|
||||
changes.extend(self.apply_delta_state(i, &mut d, true));
|
||||
self.tabs[i]
|
||||
@@ -1162,7 +1171,8 @@ impl OpenCADStudio {
|
||||
let available = self.tabs[i].history.redo_stack.len();
|
||||
let steps = steps.min(available);
|
||||
if steps == 0 {
|
||||
self.command_line.push_info(crate::t!("Nothing to redo.").as_ref());
|
||||
self.command_line
|
||||
.push_info(crate::t!("Nothing to redo.").as_ref());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1182,14 +1192,15 @@ impl OpenCADStudio {
|
||||
structure,
|
||||
StructureSnapshot::Full(_) | StructureSnapshot::Layers(_)
|
||||
)
|
||||
}) || d.active_layer.is_some() || d.entities.iter().any(|(_, before, after)| {
|
||||
before
|
||||
.as_deref()
|
||||
.is_some_and(|entity| matches!(entity, EntityType::Viewport(_)))
|
||||
|| after
|
||||
}) || d.active_layer.is_some()
|
||||
|| d.entities.iter().any(|(_, before, after)| {
|
||||
before
|
||||
.as_deref()
|
||||
.is_some_and(|entity| matches!(entity, EntityType::Viewport(_)))
|
||||
});
|
||||
|| after
|
||||
.as_deref()
|
||||
.is_some_and(|entity| matches!(entity, EntityType::Viewport(_)))
|
||||
});
|
||||
had_full |= d.structure.as_ref().is_some_and(StructureSnapshot::is_full);
|
||||
changes.extend(self.apply_delta_state(i, &mut d, false));
|
||||
self.tabs[i]
|
||||
|
||||
+6
-13
@@ -7,6 +7,7 @@ pub(crate) fn automation_action_names() -> &'static [&'static str] {
|
||||
pub(crate) mod config;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use automation::{export_headless, serve};
|
||||
mod annotation_data;
|
||||
mod command_driver;
|
||||
pub(crate) mod commands;
|
||||
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
File diff suppressed because it is too large.
Load diff
@@ -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
File diff suppressed because it is too large.
Load diff
+535
-487
File diff suppressed because it is too large.
Load diff
+419
-411
File diff suppressed because it is too large.
Load diff
+1140
-1215
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
+1809
-533
File diff suppressed because it is too large.
Load diff
+202
-227
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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(_)
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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(),
|
||||
)),
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user