Merge remote-tracking branch 'origin/main' into codex/region-measurement-persistence

# Conflicts:
#	src/app/doc_api.rs
This commit is contained in:
ramox81 committed 2026-09-11 15:10:37 +03:00
commit 17efc86cc1
149 files changed
+15933 -16047

No files matched your search

+2 -1
View File
@@ -317,6 +317,8 @@ impl OpenCADStudio {
let i = self.active_tab;
self.tabs[i].scene.clear();
self.tabs[i].scene.document = doc;
self.tabs[i].scene.load_sketch_constraints_from_document();
self.tabs[i].scene.load_named_parameters_from_document();
self.tabs[i].scene.material_base_dir = path_buf.parent().map(PathBuf::from);
crate::app::style_ops::ensure_standard_styles(
&mut self.tabs[i].scene.document,
@@ -1604,4 +1606,3 @@ mod tests {
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("MTP"));
}
}
+1201 -80
View File
File diff suppressed because it is too large. Load diff
+18 -34
View File
@@ -654,42 +654,13 @@ impl OpenCADStudio {
}
}
// NCOPY — copy the nested objects of the selected block reference(s)
// into model space, keeping the block (a non-destructive extraction;
// every nested object is copied — the block is not exploded).
"NCOPY" | "NCOPYALL" => {
use crate::modules::draw::modify::explode::explode_entity;
let inserts: Vec<acadrust::Handle> = self.tabs[i]
.scene
.selected_entities()
.iter()
.filter(|(_, e)| matches!(e, acadrust::EntityType::Insert(_)))
.map(|(h, _)| *h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if inserts.is_empty() {
self.command_line
.push_error(crate::t!("NCOPY: select a block reference first.").as_ref());
return Some(Task::none());
}
self.push_undo_snapshot(i, "NCOPY");
let mut n = 0usize;
for h in &inserts {
let nested = match self.tabs[i].scene.document.get_entity(*h).cloned() {
Some(ins) => explode_entity(&ins, &self.tabs[i].scene.document),
None => Vec::new(),
};
for e in nested {
self.tabs[i].scene.add_entity(e);
n += 1;
}
}
self.tabs[i].dirty = true;
self.command_line.push_output(crate::tf!(
"NCOPY: copied {n} nested object(s) into model space (blocks kept)."
).as_ref());
let command = crate::modules::draw::modify::ncopy::NcopyCommand::new(
&self.tabs[i].scene.document, self.ncopy_bind,
);
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
_ => return None,
}
Some(self.finish_dispatch(cmd))
@@ -706,6 +677,19 @@ mod tests {
app
}
#[test]
fn ncopy_starts_nested_object_selection() {
let mut app = fresh_app();
let _ = app.run_command_line("NCOPY");
assert_eq!(
app.tabs[app.active_tab]
.active_cmd
.as_ref()
.map(|active| active.name()),
Some("NCOPY")
);
}
/// Run one command line and return only the command-line text it appended.
fn run_capture(app: &mut OpenCADStudio, cmd: &str) -> String {
let start = app.command_line.history.len();
+257 -14
View File
@@ -596,9 +596,23 @@ impl OpenCADStudio {
"REVERSE" => {
use crate::modules::draw::modify::reverse::ReverseCommand;
let new_cmd = ReverseCommand::new();
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
if self.tabs[i].scene.selected.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand;
let selection = SelectObjectsCommand::new("REVERSE");
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()
.filter(|(handle, _)| !self.tabs[i].scene.is_layer_locked(*handle))
.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(),
)));
}
}
}
"MEASUREGEOM" => {
@@ -881,7 +895,7 @@ impl OpenCADStudio {
SplineCommand::control_vertices()
} else {
SplineCommand::new()
};
}.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));
}
@@ -963,6 +977,193 @@ impl OpenCADStudio {
}
}
// ── Persistent constraints ────────────────────────────────────
"HCONSTRAINT" | "VCONSTRAINT" | "FXCONSTRAINT" => {
let handles = self.tabs[i].scene.selected_handles_in_order();
if handles.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand;
let sel = SelectObjectsCommand::new(cmd);
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 entity, then run this constraint again.");
} else {
use crate::command::CmdResult;
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
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 {
kind,
refs: vec![SketchRef::whole(handles[0])],
driving_param: None,
label,
}));
}
}
"CCONSTRAINT" => {
use crate::modules::draw::constrain::CoincidentConstraintCommand;
let new_cmd = CoincidentConstraintCommand::new();
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
"EDCONSTRAINT" => {
use crate::modules::draw::constrain::EqualDistanceConstraintCommand;
let new_cmd = EqualDistanceConstraintCommand::new();
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
"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;
let sel = SelectObjectsCommand::new(cmd);
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 entities (first = reference, second = the one that moves), then run this constraint again.",
);
} else {
use crate::command::CmdResult;
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
let (kind, label) = match cmd {
"PCONSTRAINT" => (ConstraintKind::Parallel, "Parallel 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 {
kind,
refs: vec![SketchRef::whole(handles[0]), SketchRef::whole(handles[1])],
driving_param: None,
label,
}));
}
}
"NCONSTRAINT" => {
let handles = self.tabs[i].scene.selected_handles_in_order();
if handles.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand;
let sel = SelectObjectsCommand::new(cmd);
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.");
} else {
use crate::command::CmdResult;
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
return Some(self.apply_cmd_result(CmdResult::AddSketchConstraint {
kind: ConstraintKind::Concentric,
refs: vec![SketchRef::center(handles[0]), SketchRef::center(handles[1])],
driving_param: None,
label: "Concentric constraint",
}));
}
}
"SYCONSTRAINT" => {
let handles = self.tabs[i].scene.selected_handles_in_order();
if handles.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand;
let sel = SelectObjectsCommand::new(cmd);
self.command_line.push_info(&sel.prompt());
self.tabs[i].active_cmd = Some(Box::new(sel));
} else if handles.len() != 3 {
self.command_line.push_output(
"Select exactly three entities (two circles/arcs, then the mirror line), 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 {
kind: ConstraintKind::Symmetric,
refs: vec![SketchRef::center(handles[0]), SketchRef::center(handles[1]), SketchRef::whole(handles[2])],
driving_param: None,
label: "Symmetric constraint",
}));
}
}
"CPCONSTRAINT" | "MPCONSTRAINT" | "OCCONSTRAINT" => {
let handles = self.tabs[i].scene.selected_handles_in_order();
if handles.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand;
let sel = SelectObjectsCommand::new(cmd);
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 entity (the circle or line the point should attach to), then run this constraint again.",
);
} else {
use crate::modules::draw::constrain::PointOnEntityConstraintCommand;
use crate::scene::sketch_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"),
};
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));
}
}
"DCONSTRAINT" => {
let handles = self.tabs[i].scene.selected_handles_in_order();
if handles.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand;
let sel = SelectObjectsCommand::new(cmd);
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.");
} else {
use crate::modules::draw::constrain::DistanceConstraintCommand;
match DistanceConstraintCommand::new(&self.tabs[i].scene, handles[0]) {
Some(new_cmd) => {
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."),
}
}
}
"ACONSTRAINT" => {
let handles = self.tabs[i].scene.selected_handles_in_order();
if handles.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand;
let sel = SelectObjectsCommand::new(cmd);
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 lines (first = reference, second = the one that rotates), then run this constraint again.",
);
} else {
use crate::modules::draw::constrain::AngleConstraintCommand;
match AngleConstraintCommand::new(&self.tabs[i].scene, handles[0], handles[1]) {
Some(new_cmd) => {
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."),
}
}
}
// ── Model commands (3D primitives) ─────────────────────────────
"CYLINDER" => {
use crate::modules::model::cylinder_cmd::CylinderCommand;
@@ -1114,8 +1315,7 @@ impl OpenCADStudio {
return Some(self.fit_spline());
}
// REGION — convert selected closed boundaries (closed polylines /
// circles) into Region entities (one wire loop each).
// REGION — convert exact closed planar profiles into regions.
"REGION" | "REG" => {
if self.tabs[i].scene.selected_entities().is_empty() {
use crate::modules::draw::select::SelectObjectsCommand;
@@ -1129,12 +1329,7 @@ impl OpenCADStudio {
let mut regions = Vec::new();
let mut sources = Vec::new();
for (handle, e) in self.tabs[i].scene.selected_entities().iter() {
let supported = matches!(
e,
acadrust::EntityType::LwPolyline(pl)
if pl.is_closed && pl.vertices.len() >= 3
) || matches!(e, acadrust::EntityType::Circle(_));
if supported {
if !matches!(e, acadrust::EntityType::Region(_)) {
let Some((plane, loops, true)) =
crate::scene::model::presspull_model::profile_geometry(e)
else {
@@ -1154,9 +1349,55 @@ impl OpenCADStudio {
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()
.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 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.",
);
}
if regions.is_empty() {
self.command_line
.push_error(crate::t!("REGION: select closed polylines or circles.").as_ref());
if !open_plane_rejected {
self.command_line.push_error(
"REGION: select closed planar profiles or connected coplanar edges.",
);
}
} else {
self.push_undo_snapshot(i, "REGION");
let count = regions.len();
@@ -1171,6 +1412,8 @@ impl OpenCADStudio {
created.push(handle);
}
if self.delete_objects != 0 {
sources.sort_by_key(|handle| handle.value());
sources.dedup();
self.tabs[i].scene.erase_entities(&sources);
self.refresh_properties();
}
+16 -2
View File
@@ -218,7 +218,10 @@ impl OpenCADStudio {
self.apply_cmd_result(crate::command::CmdResult::JoinEntities(selected));
return Some(task);
}
let cmd = JoinCommand::new();
let mut cmd = JoinCommand::new();
if let Some(handle) = selected.first() {
if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle).cloned() { cmd = cmd.with_source(*handle, entity); }
}
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
@@ -294,6 +297,7 @@ impl OpenCADStudio {
header.surface_u_density,
header.surface_v_density,
)
.with_entities(self.tabs[i].scene.document.entities().cloned())
.with_preselection(&preselected);
self.command_line.push_info(&cmd_obj.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
@@ -329,7 +333,17 @@ impl OpenCADStudio {
"SPLINEDIT" => {
use crate::modules::draw::modify::splinedit::SplineditCommand;
let cmd_obj = SplineditCommand::new();
let mut cmd_obj = SplineditCommand::new().with_delete_source(self.delete_objects != 0);
let selected: Vec<_> = self.tabs[i].scene.selected.iter().copied().collect();
if let [handle] = selected.as_slice() {
if let Some(entity @ acadrust::EntityType::Spline(_)) =
self.tabs[i].scene.document.get_entity(*handle).cloned()
{
if self.reject_locked_edit(i, *handle) { return Some(Task::none()); }
cmd_obj.inject_picked_entity(entity);
cmd_obj.on_entity_pick(*handle, glam::DVec3::ZERO);
}
}
self.command_line.push_info(&cmd_obj.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
}
+5
View File
@@ -322,6 +322,7 @@ inventory::submit!(crate::command::CommandRegistration {
"CLEANSCREEN",
"CUI",
"DSETTINGS",
"PARAMETERS",
"GRID",
"ISODRAFT",
"ISOPLANE",
@@ -598,6 +599,10 @@ inventory::submit!(crate::command::CommandRegistration {
"DIMSTYLE",
"DONATE",
"DRAWORDER",
"DRAWORDER_FRONT",
"DRAWORDER_BACK",
"DRAWORDER_ABOVE",
"DRAWORDER_UNDER",
"DWGPROP",
"DWGPROPS",
"EATTEXT",
+200 -63
View File
@@ -645,98 +645,221 @@ impl OpenCADStudio {
// ── SETBYLAYER — clear color/linetype/lineweight overrides ────
// Resets the selected entities' direct property overrides back to
// ByLayer so they follow their layer again.
"SETBYLAYERMODE" => {
let command = crate::modules::draw::modify::setbylayer::ModeCommand;
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
value if value.starts_with("SETBYLAYERMODE ") => {
if let Ok(mode) = value.trim_start_matches("SETBYLAYERMODE ").trim().parse::<u8>() {
crate::modules::draw::modify::setbylayer::set_mode(mode);
} else { self.command_line.push_error("SETBYLAYERMODE requires an integer from 0 to 255."); }
}
"SETBYLAYER" => {
let handles: Vec<_> = self.tabs[i]
let command = crate::modules::draw::modify::setbylayer::SetByLayerCommand::new(
self.tabs[i].scene.selected.iter().copied().collect(),
);
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
cmd if cmd.starts_with("SETBYLAYER_APPLY ") => {
let flags: Vec<_> = cmd.split_whitespace().skip(1).collect();
if flags.len() != 2 || flags.iter().any(|flag| !matches!(*flag, "0" | "1")) {
return Some(Task::none());
}
let mask = crate::modules::draw::modify::setbylayer::mode();
let change_byblock = flags[0] == "1";
let include_blocks = flags[1] == "1";
let mut handles: Vec<_> = self.tabs[i]
.scene
.selected_entities()
.into_iter()
.map(|(h, _)| h)
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
.collect();
if include_blocks {
let mut visited: std::collections::HashSet<_> = handles.iter().copied().collect();
let mut index = 0;
while index < handles.len() {
let children = match self.tabs[i].scene.document.get_entity(handles[index]) {
Some(acadrust::EntityType::Insert(insert)) => self.tabs[i].scene.document
.block_records.get(&insert.block_name)
.map(|block| block.entity_handles.clone()).unwrap_or_default(),
_ => Vec::new(),
};
for child in children {
if visited.insert(child) && !self.tabs[i].scene.is_layer_locked(child) {
handles.push(child);
}
}
index += 1;
}
}
if handles.is_empty() {
self.command_line
.push_error(crate::t!("SETBYLAYER: select entities first.").as_ref());
} else {
let has_changes = handles.iter().any(|handle| {
self.tabs[i].scene.document.get_entity(*handle).is_some_and(|entity| {
let mut common = entity.common().clone();
crate::modules::draw::modify::setbylayer::apply_mask(
&mut common, mask, change_byblock,
)
})
});
if !has_changes {
self.command_line.push_output(
crate::t!("SETBYLAYER: no properties required changes.").as_ref(),
);
return Some(Task::none());
}
self.push_undo_snapshot(i, "SETBYLAYER");
let mut changed = 0usize;
let mut changed = Vec::new();
for handle in &handles {
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(*handle) {
let common = entity.common_mut();
common.color = acadrust::types::Color::ByLayer;
common.color_name = None;
common.color_book_handle = None;
common.linetype = "ByLayer".to_string();
common.line_weight = acadrust::types::LineWeight::ByLayer;
changed += 1;
if crate::modules::draw::modify::setbylayer::apply_mask(
entity.common_mut(), mask, change_byblock,
) {
changed.push(*handle);
}
}
}
self.tabs[i].dirty = true;
let changes: Vec<_> = handles
.into_iter()
let changes: Vec<_> = changed
.iter()
.copied()
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
.collect();
self.tabs[i].scene.bump_entities(&changes);
self.refresh_properties();
self.command_line.push_output(crate::tf!(
"SETBYLAYER: reset {changed} entity/entities to ByLayer."
"SETBYLAYER: reset {} entity/entities to ByLayer.",
changed.len()
).as_ref());
}
}
// ── OVERKILL — delete duplicate (identical) objects ──────────
// Removes objects that are identical in geometry AND properties to
// another object (compared with the handle ignored). Operates on
// the current selection, or the whole drawing when nothing is
// selected. Conservative: only exact duplicates are removed.
"OVERKILL" => {
use acadrust::Handle;
let selected: std::collections::HashSet<u64> = self.tabs[i]
.scene
.selected_entities()
.into_iter()
.map(|(h, _)| h.value())
.collect();
// Capture (handle, type-name, handle-normalized clone) for each
// candidate while the document is borrowed immutably.
let candidates: Vec<(Handle, String, acadrust::EntityType)> = self.tabs[i]
.scene
.document
.entities()
.filter(|e| {
(selected.is_empty() || selected.contains(&e.common().handle.value()))
&& !self.tabs[i].scene.is_layer_locked(e.common().handle)
})
.map(|e| {
let key = crate::entities::names::dxf_name(e).to_string();
let mut norm = e.clone();
norm.common_mut().handle = Handle::NULL;
(e.common().handle, key, norm)
})
.collect();
// Bucket by (type, layer) so only like objects are compared.
let mut kept: Vec<(String, acadrust::EntityType)> = Vec::new();
let mut dups: Vec<Handle> = Vec::new();
for (h, key, norm) in &candidates {
let bucket = format!("{key}\u{0}{}", norm.common().layer);
if kept.iter().any(|(b, e)| b == &bucket && e == norm) {
dups.push(*h);
} else {
kept.push((bucket, norm.clone()));
// OVERKILL gathers objects, exposes cleanup settings, then applies one undo group.
"OVERKILL" | "-OVERKILL" => {
let handles=self.tabs[i].scene.selected_entities().iter().map(|(h,_)|*h).collect();
let command=crate::modules::draw::modify::overkill::OverkillCommand::new(handles);
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd=Some(Box::new(command));
}
value if value.starts_with("OVERKILL_APPLY ") => {
use crate::modules::draw::modify::overkill::{normalized,optimize};
let values:Vec<_>=value.split_whitespace().skip(1).collect();
if values.len()!=6 { return Some(self.finish_dispatch(cmd)); }
let tolerance=values[0].parse::<f64>().unwrap_or(1e-6);
let ignore=values[1].parse::<u16>().unwrap_or(0);
let optimize_plines=values[2]=="1";
let overlap=values[3]=="1";
let end_to_end=values[4]=="1";
let preserve_associative=values[5]=="1";
let mut candidates:Vec<_>=self.tabs[i].scene.selected_entities().into_iter()
.filter(|(h,e)|!self.tabs[i].scene.is_layer_locked(*h)
&& (!preserve_associative||e.common().reactors.is_empty()))
.map(|(h,e)|(h,e.clone())).collect();
candidates.sort_by_key(|(h,_)|h.value());
let mut changed=std::collections::HashSet::new();
if optimize_plines { for (h,e) in &mut candidates {
let before=e.clone(); optimize(e,tolerance); if *e!=before {changed.insert(*h);}
} }
let mut removed=std::collections::HashSet::new();
// Iterate to a fixed point: a bridge can join two previously disjoint intervals.
loop {
let mut progress=false;
for a in 0..candidates.len() {
if removed.contains(&candidates[a].0) {continue;}
for b in a+1..candidates.len() {
if removed.contains(&candidates[b].0) {continue;}
let left=normalized(&candidates[a].1,ignore);
let right=normalized(&candidates[b].1,ignore);
if left==right {
removed.insert(candidates[b].0);progress=true;continue;
}
if overlap {
let contained = |circle: &acadrust::entities::Circle, arc: &acadrust::entities::Arc| {
circle.common == arc.common && circle.thickness == arc.thickness
&& cadkernel::space::arc_union::circle_contains_arc(
[circle.center.x,circle.center.y,circle.center.z],
[circle.normal.x,circle.normal.y,circle.normal.z],circle.radius,
cadkernel::space::arc_union::CircularArc { center:[arc.center.x,arc.center.y,arc.center.z],
normal:[arc.normal.x,arc.normal.y,arc.normal.z],radius:arc.radius,start:arc.start_angle,end:arc.end_angle })
};
match (&left,&right) {
(acadrust::EntityType::Circle(circle),acadrust::EntityType::Arc(arc)) if contained(circle,arc) => {
removed.insert(candidates[b].0);progress=true;continue;
}
(acadrust::EntityType::Arc(arc),acadrust::EntityType::Circle(circle)) if contained(circle,arc) => {
removed.insert(candidates[a].0);progress=true;break;
}
_ => {},
}
}
if let (acadrust::EntityType::Arc(l),acadrust::EntityType::Arc(r))=(&left,&right) {
use cadkernel::space::arc_union::{CircularArc,ArcUnionKind,circular_arc_union};
if l.common!=r.common || l.thickness!=r.thickness {continue;}
let arc=|v:&acadrust::entities::Arc|CircularArc{center:[v.center.x,v.center.y,v.center.z],normal:[v.normal.x,v.normal.y,v.normal.z],radius:v.radius,start:v.start_angle,end:v.end_angle};
let Some(union)=circular_arc_union(arc(l),arc(r),tolerance) else {continue;};
let allowed=match union.kind {ArcUnionKind::Duplicate=>true,ArcUnionKind::Overlap=>overlap,ArcUnionKind::EndToEnd=>end_to_end};
if !allowed {continue;}
if let acadrust::EntityType::Arc(source)=&candidates[a].1 {
let replacement=if union.full_circle {
let mut circle=acadrust::entities::Circle::new();
circle.common=source.common.clone();circle.center=source.center.clone();circle.normal=source.normal.clone();circle.radius=source.radius;circle.thickness=source.thickness;
acadrust::EntityType::Circle(circle)
} else {
let mut arc=source.clone();arc.start_angle=union.start;arc.end_angle=union.end;acadrust::EntityType::Arc(arc)
};
candidates[a].1=replacement;
}
changed.insert(candidates[a].0);removed.insert(candidates[b].0);progress=true;
continue;
}
let (acadrust::EntityType::Line(l),acadrust::EntityType::Line(r))=(&left,&right)
else {continue;};
if l.common!=r.common||l.thickness!=r.thickness||l.normal!=r.normal {continue;}
let point=|p:acadrust::types::Vector3|[p.x,p.y,p.z];
let Some(union)=cadkernel::space::line_union(
[point(l.start),point(l.end)],[point(r.start),point(r.end)],tolerance)
else {continue;};
let allowed=match union.kind {
cadkernel::space::LineUnionKind::Duplicate=>true,
cadkernel::space::LineUnionKind::Overlap=>overlap,
cadkernel::space::LineUnionKind::EndToEnd=>end_to_end,
};
if !allowed {continue;}
if let acadrust::EntityType::Line(line)=&mut candidates[a].1 {
line.start=acadrust::types::Vector3::new(union.start[0],union.start[1],union.start[2]);
line.end=acadrust::types::Vector3::new(union.end[0],union.end[1],union.end[2]);
}
changed.insert(candidates[a].0);removed.insert(candidates[b].0);progress=true;
}
}
if !progress {break;}
}
if dups.is_empty() {
self.command_line
.push_output(crate::t!("OVERKILL: no duplicate objects found.").as_ref());
if !changed.is_empty()||!removed.is_empty() {
self.push_undo_snapshot(i,"OVERKILL");
let updates:Vec<_>=candidates.into_iter().filter(|(h,_)|changed.contains(h)&&!removed.contains(h)).collect();
let updated_count = updates.len();
let mut changes = Vec::new();
for (handle, entity) in updates {
if let Some(target) = self.tabs[i].scene.document.get_entity_mut(handle) {
*target = entity;
changes.push((handle, crate::scene::ChangeKind::Modified));
}
}
self.tabs[i].scene.bump_entities(&changes);
let mut handles:Vec<_>=removed.iter().copied().collect();
handles.sort_by_key(|handle| handle.value());
self.tabs[i].scene.erase_entities(&handles);
self.tabs[i].dirty=true;self.refresh_properties();
self.command_line.push_output(&format!("OVERKILL: removed {} objects; updated {} objects.",removed.len(),updated_count));
} else {
let n = dups.len();
self.push_undo_snapshot(i, "OVERKILL");
self.tabs[i].scene.erase_entities(&dups);
self.tabs[i].dirty = true;
self.refresh_properties();
self.command_line
.push_output(crate::tf!("OVERKILL: deleted {n} duplicate object(s).").as_ref());
self.command_line.push_output("OVERKILL: no objects required changes.");
}
}
// ── PICKADD / PICKDRAG — selection UX (#226, app settings) ───
// Bare form reports; `<name> 0|1` sets and persists. Defaults keep
// today's behaviour (PICKADD 1, PICKDRAG 0).
@@ -2983,6 +3106,21 @@ mod tests {
app
}
#[test]
fn selection_commands_gather_instead_of_using_the_whole_drawing() {
for command in ["SETBYLAYER", "OVERKILL"] {
let mut app = fresh_app();
let _ = app.run_command_line(command);
assert!(
app.tabs[app.active_tab]
.active_cmd
.as_ref()
.is_some_and(|active| active.is_selection_gathering()),
"{command} should gather an explicit selection"
);
}
}
#[test]
fn test_cad_selection_and_model_space_sysvars() {
let mut app = fresh_app();
@@ -3195,4 +3333,3 @@ mod scale_validation_tests {
}
}
}
+335 -143
View File
@@ -1,5 +1,62 @@
use super::*;
// Sort tables belong to the block's extension dictionary. Reserve real handles
// and reconnect older unattached tables without losing their existing entries.
fn ensure_draw_order_table(
doc: &mut acadrust::CadDocument,
block: acadrust::Handle,
) -> acadrust::Handle {
use acadrust::objects::{Dictionary, ObjectType, SortEntitiesTable};
// Older commands inserted objects using the unreserved next-handle value.
// Repair the allocator floor before creating anything beside those objects.
if let Some(maximum) = doc.objects.keys().map(|handle| handle.value()).max() {
doc.header.handle_seed = doc.header.handle_seed.max(maximum.saturating_add(1));
}
let dictionary = doc.extension_dictionary_handle(block).filter(|handle| {
matches!(doc.objects.get(handle), Some(ObjectType::Dictionary(_)))
});
let named_table = dictionary.and_then(|handle| match doc.objects.get(&handle) {
Some(ObjectType::Dictionary(value)) => value.get(SortEntitiesTable::DICTIONARY_KEY),
_ => None,
}).filter(|handle| matches!(doc.objects.get(handle),
Some(ObjectType::SortEntitiesTable(table)) if table.block_owner_handle == block));
let existing = named_table.or_else(|| doc.objects.iter().find_map(|(handle, object)| {
match object {
ObjectType::SortEntitiesTable(table) if table.block_owner_handle == block => Some(*handle),
_ => None,
}
}));
let dictionary = dictionary.unwrap_or_else(|| {
let handle = doc.allocate_handle();
let mut value = Dictionary::new();
value.handle = handle;
value.owner = block;
value.hard_owner = true;
doc.objects.insert(handle, ObjectType::Dictionary(value));
handle
});
let handle = existing.unwrap_or_else(|| {
let handle = doc.allocate_handle();
let mut table = SortEntitiesTable::for_block(block);
table.handle = handle;
doc.objects.insert(handle, ObjectType::SortEntitiesTable(table));
handle
});
if let Some(ObjectType::SortEntitiesTable(table)) = doc.objects.get_mut(&handle) {
table.owner_handle = dictionary;
}
if let Some(ObjectType::Dictionary(value)) = doc.objects.get_mut(&dictionary) {
if let Some((_, entry)) = value.entries.iter_mut()
.find(|(name, _)| name.eq_ignore_ascii_case(SortEntitiesTable::DICTIONARY_KEY)) {
*entry = handle;
} else {
value.add_entry(SortEntitiesTable::DICTIONARY_KEY, handle);
}
}
handle
}
impl OpenCADStudio {
pub(crate) fn dispatch_view(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
match cmd {
@@ -253,6 +310,10 @@ impl OpenCADStudio {
return Some(Task::done(Message::OptionsOpen));
}
"PARAMETERS" => {
return Some(Task::done(Message::NamedParametersOpen));
}
// CUIEXPORT <path> — write the keyboard-shortcut customizations
// (the drawing-independent CUI data) to a plain "KEY COMMAND" file.
"CUIEXPORT" => {
@@ -817,17 +878,15 @@ impl OpenCADStudio {
// HATCHTOBACK ÔÇö move every hatch object in the active space to the back of the draw order.
"HATCHTOBACK" => {
use acadrust::objects::{ObjectType, SortEntitiesTable};
use acadrust::objects::ObjectType;
let block_handle = self.tabs[i].scene.current_layout_block_handle_pub();
let doc_ref = &self.tabs[i].scene.document;
// 1. Single scan over objects to find existing SortEntitiesTable handle & overrides.
let mut existing_table_handle = None;
// 1. Read existing SortEntitiesTable overrides.
let mut overrides: Option<rustc_hash::FxHashMap<u64, u64>> = None;
for (h, obj) in &doc_ref.objects {
for obj in doc_ref.objects.values() {
if let ObjectType::SortEntitiesTable(t) = obj {
if t.block_owner_handle == block_handle {
existing_table_handle = Some(*h);
if !t.is_empty() {
overrides = Some(
t.entries()
@@ -894,24 +953,9 @@ impl OpenCADStudio {
&locked_layers,
);
// 4. Ultra-fast targeted Delta Undo (snapshots ONLY the SortEntitiesTable, zero full-drawing clone).
let pending_delta = self.begin_undo(i, "DRAWORDER", hatches_to_move.len(), true);
// 5. Update or insert SortEntitiesTable directly.
let table_before = existing_table_handle
.and_then(|h| self.tabs[i].scene.document.objects.get(&h).cloned());
let th = existing_table_handle.unwrap_or_else(|| {
let nh = acadrust::Handle::new(self.tabs[i].scene.document.next_handle());
let mut table = SortEntitiesTable::for_block(block_handle);
table.handle = nh;
self.tabs[i]
.scene
.document
.objects
.insert(nh, ObjectType::SortEntitiesTable(table));
nh
});
self.tabs[i].scene.record_undo_object_before(th, table_before);
// Dictionary ownership and handle allocation are part of this undo step.
let pending_delta = self.begin_undo(i, "DRAWORDER", hatches_to_move.len(), false);
let th = ensure_draw_order_table(&mut self.tabs[i].scene.document, block_handle);
if let Some(ObjectType::SortEntitiesTable(table)) =
self.tabs[i].scene.document.objects.get_mut(&th)
@@ -941,6 +985,37 @@ impl OpenCADStudio {
return Some(Task::none());
}
"DRAWORDER_FRONT" | "DRAWORDER_BACK" | "DRAWORDER_ABOVE" | "DRAWORDER_UNDER" => {
let selected: Vec<acadrust::Handle> = self.tabs[i]
.scene
.selected_entities()
.iter()
.map(|(h, _)| *h)
.collect();
if selected.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand;
let selection = SelectObjectsCommand::plain("DRAWORDER", cmd);
self.command_line.push_info(&selection.prompt());
self.tabs[i].active_cmd = Some(Box::new(selection));
} else if matches!(cmd, "DRAWORDER_ABOVE" | "DRAWORDER_UNDER") {
let command = DrawOrderCommand::for_reference_pick(
selected,
cmd == "DRAWORDER_ABOVE",
);
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
} else {
let primitive = if cmd == "DRAWORDER_FRONT" {
"DRAWORDER FRONT"
} else {
"DRAWORDER BACK"
};
return Some(self.apply_cmd_result(crate::command::CmdResult::Relaunch(
primitive.to_string(),
selected,
)));
}
}
"DRAWORDER" => {
let selected: Vec<acadrust::Handle> = self.tabs[i]
.scene
@@ -953,7 +1028,7 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(c));
}
cmd if cmd.starts_with("DRAWORDER ") => {
use acadrust::objects::{ObjectType, SortEntitiesTable};
use acadrust::objects::ObjectType;
let parts: Vec<&str> = cmd.split_whitespace().collect();
let option = parts.get(1).unwrap_or(&"").to_uppercase();
let i = self.active_tab;
@@ -968,25 +1043,29 @@ impl OpenCADStudio {
self.command_line
.push_error(crate::t!("DRAWORDER: select entities first.").as_ref());
} else {
// Parse relative target handle for ABOVE/UNDER.
let relative_target: Option<(bool, acadrust::Handle)> = match option.as_str() {
"A" | "ABOVE" => {
let h_val = parts.get(2).and_then(|s| u64::from_str_radix(s, 16).ok());
h_val.map(|v| (true, acadrust::Handle::new(v)))
}
"U" | "UNDER" | "BELOW" => {
let h_val = parts.get(2).and_then(|s| u64::from_str_radix(s, 16).ok());
h_val.map(|v| (false, acadrust::Handle::new(v)))
}
let relative_above = match option.as_str() {
"A" | "ABOVE" => Some(true),
"U" | "UNDER" | "BELOW" => Some(false),
_ => None,
};
let references: Vec<_> = parts.iter().skip(2).filter_map(|text| {
u64::from_str_radix(text.trim_start_matches("0x").trim_start_matches("0X"), 16)
.ok().map(acadrust::Handle::new)
}).filter(|handle| !selected.contains(handle)).collect();
let relative_assignments = relative_above.and_then(|above| {
assign_relative_group_keys(
&self.tabs[i].scene.document,
self.tabs[i].scene.current_layout_block_handle_pub(),
&selected, &references, above,
)
});
let to_front_opt = match option.as_str() {
"F" | "FRONT" => Some(true),
"B" | "BACK" => Some(false),
_ => None,
};
if relative_target.is_some() || to_front_opt.is_some() {
if relative_assignments.is_some() || to_front_opt.is_some() {
self.push_undo_snapshot(i, "DRAWORDER");
let block_handle = self.tabs[i].scene.current_layout_block_handle_pub();
@@ -1063,63 +1142,14 @@ impl OpenCADStudio {
};
let doc = &mut self.tabs[i].scene.document;
let table_handle = doc.objects.iter().find_map(|(h, obj)| {
if let ObjectType::SortEntitiesTable(t) = obj {
if t.block_owner_handle == block_handle {
Some(*h)
} else {
None
}
} else {
None
}
});
let th = table_handle.unwrap_or_else(|| {
let nh = acadrust::Handle::new(doc.next_handle());
let mut table = SortEntitiesTable::for_block(block_handle);
table.handle = nh;
doc.objects.insert(nh, ObjectType::SortEntitiesTable(table));
nh
});
let th = ensure_draw_order_table(doc, block_handle);
if let Some(ObjectType::SortEntitiesTable(table)) = doc.objects.get_mut(&th)
{
if let Some((above, target)) = relative_target {
// move_above/move_below recompute target±1 per
// call, so looping them over N selected entities
// ties the whole group onto one key. Read the
// reference key once and hand out per-index
// keys instead, keeping the moved entities a
// distinct, selection-ordered block adjacent to
// the reference.
let target_sort = match table.get_sort_handle(target) {
Some(h) => h.value(),
None => {
// A reference object that was never
// reordered isn't in the table yet;
// seed it with its own handle as the
// implicit sort key.
table.add_entry(target, target);
table
.get_sort_handle(target)
.map_or(target.value(), |h| h.value())
}
};
for (k, h) in selected.iter().enumerate() {
let offset = 1 + k as u64;
let sort = if above {
target_sort.saturating_add(offset)
} else {
target_sort.saturating_sub(offset).max(1)
};
table.add_entry(*h, acadrust::Handle::new(sort));
if let Some(assignments) = &relative_assignments {
for (handle, sort) in assignments {
table.add_entry(*handle, acadrust::Handle::new(*sort));
}
let rel = if above { "above" } else { "below" };
self.command_line.push_info(crate::tf!(
"DRAWORDER: moved {} entities {} {:x}.",
selected.len(),
rel,
target.value()
).as_ref());
self.command_line.push_info(crate::t!("DRAWORDER: selection reordered relative to reference objects.").as_ref());
} else if let Some(to_front) = to_front_opt {
if to_front {
let (_, max_eff) = fb_baseline.unwrap_or((1, 0));
@@ -1250,11 +1280,12 @@ enum DrawOrderStep {
/// - `Front` / `F`: moves selection to front.
/// - `Back` / `B` / Enter: moves selection to back.
/// - `Above` / `A` / `Under` / `U`: advances to reference object pick.
/// 3. Reference object pick: user can click the reference entity in the viewport
/// or type its hex handle on the command line.
/// 3. Gather reference objects, excluding the moved selection; Enter applies.
/// Typed hexadecimal handles may also be accumulated before confirming.
pub(crate) struct DrawOrderCommand {
selected: Vec<acadrust::Handle>,
step: DrawOrderStep,
references: Vec<acadrust::Handle>,
}
impl DrawOrderCommand {
@@ -1264,13 +1295,14 @@ impl DrawOrderCommand {
} else {
DrawOrderStep::ChooseVerb
};
Self { selected, step }
Self { selected, step, references: Vec::new() }
}
pub(crate) fn for_reference_pick(selected: Vec<acadrust::Handle>, above: bool) -> Self {
Self {
selected,
step: DrawOrderStep::PickReference { above },
references: Vec::new(),
}
}
}
@@ -1289,10 +1321,10 @@ impl CadCommand for DrawOrderCommand {
crate::t!("DRAWORDER [Above / Under / Front / Back] <Back>:").into_owned()
}
DrawOrderStep::PickReference { above: true } => {
crate::t!("DRAWORDER Select reference object (move selection above):").into_owned()
format!("DRAWORDER Select reference objects to move above ({} selected, Enter when done):", self.references.len())
}
DrawOrderStep::PickReference { above: false } => {
crate::t!("DRAWORDER Select reference object (move selection under):").into_owned()
format!("DRAWORDER Select reference objects to move under ({} selected, Enter when done):", self.references.len())
}
}
}
@@ -1310,7 +1342,7 @@ impl CadCommand for DrawOrderCommand {
}
fn input_kind(&self) -> crate::command::InputKind {
if matches!(self.step, DrawOrderStep::SelectObjects) {
if matches!(self.step, DrawOrderStep::SelectObjects | DrawOrderStep::PickReference { .. }) {
crate::command::InputKind::Point
} else {
crate::command::InputKind::SingleToken
@@ -1318,11 +1350,15 @@ impl CadCommand for DrawOrderCommand {
}
fn is_selection_gathering(&self) -> bool {
matches!(self.step, DrawOrderStep::SelectObjects)
matches!(self.step, DrawOrderStep::SelectObjects | DrawOrderStep::PickReference { .. })
}
fn on_selection_complete(&mut self, handles: Vec<acadrust::Handle>) -> crate::command::CmdResult {
self.selected = handles;
if matches!(self.step, DrawOrderStep::PickReference { .. }) {
self.references = handles.into_iter().filter(|handle| !self.selected.contains(handle)).collect();
} else {
self.selected = handles;
}
crate::command::CmdResult::NeedPoint
}
@@ -1341,7 +1377,12 @@ impl CadCommand for DrawOrderCommand {
let handles = std::mem::take(&mut self.selected);
crate::command::CmdResult::Relaunch("DRAWORDER BACK".into(), handles)
}
DrawOrderStep::PickReference { .. } => crate::command::CmdResult::Cancel,
DrawOrderStep::PickReference { above } => {
if self.references.is_empty() { return crate::command::CmdResult::Cancel; }
let option = if above { "ABOVE" } else { "UNDER" };
let references = self.references.iter().map(|handle| format!("{:x}", handle.value())).collect::<Vec<_>>().join(" ");
crate::command::CmdResult::Relaunch(format!("DRAWORDER {option} {references}"), std::mem::take(&mut self.selected))
}
}
}
@@ -1374,47 +1415,63 @@ impl CadCommand for DrawOrderCommand {
_ => Some(crate::command::CmdResult::NeedPoint),
}
}
DrawOrderStep::PickReference { above } => {
let hex_str = t.trim_start_matches("0x").trim_start_matches("0X");
if let Ok(val) = u64::from_str_radix(hex_str, 16) {
let opt = if above { "A" } else { "U" };
let cmd = format!("DRAWORDER {} {:x}", opt, val);
let handles = std::mem::take(&mut self.selected);
Some(crate::command::CmdResult::Relaunch(cmd, handles))
} else {
Some(crate::command::CmdResult::NeedPoint)
DrawOrderStep::PickReference { .. } => {
for text in t.split_whitespace() {
let hex = text.trim_start_matches("0x").trim_start_matches("0X");
if let Ok(value) = u64::from_str_radix(hex, 16) {
let handle = acadrust::Handle::new(value);
if !handle.is_null() && !self.selected.contains(&handle) && !self.references.contains(&handle) {
self.references.push(handle);
}
}
}
Some(crate::command::CmdResult::NeedPoint)
}
}
}
fn needs_entity_pick(&self) -> bool {
matches!(self.step, DrawOrderStep::PickReference { .. })
}
fn on_entity_pick(
&mut self,
handle: acadrust::Handle,
_pt: glam::DVec3,
) -> crate::command::CmdResult {
if handle.is_null() {
return crate::command::CmdResult::NeedPoint;
}
if let DrawOrderStep::PickReference { above } = self.step {
let opt = if above { "A" } else { "U" };
let cmd = format!("DRAWORDER {} {:x}", opt, handle.value());
let handles = std::mem::take(&mut self.selected);
crate::command::CmdResult::Relaunch(cmd, handles)
} else {
crate::command::CmdResult::NeedPoint
fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: glam::DVec3) -> crate::command::CmdResult {
if matches!(self.step, DrawOrderStep::PickReference { .. }) && !handle.is_null()
&& !self.selected.contains(&handle) && !self.references.contains(&handle) {
self.references.push(handle);
}
crate::command::CmdResult::NeedPoint
}
fn on_point(&mut self, _pt: glam::DVec3) -> crate::command::CmdResult {
crate::command::CmdResult::NeedPoint
}
}
/// Insert the selected block next to the extremal reference in effective draw
/// order. Renumber the ordered siblings to avoid ties or saturated sort keys.
fn assign_relative_group_keys(
document: &acadrust::CadDocument,
block: acadrust::Handle,
selected: &[acadrust::Handle],
references: &[acadrust::Handle],
above: bool,
) -> Option<Vec<(acadrust::Handle, u64)>> {
use acadrust::objects::ObjectType;
let overrides: std::collections::HashMap<_, _> = document.objects.values().find_map(|object| {
if let ObjectType::SortEntitiesTable(table) = object {
(table.block_owner_handle == block).then(|| table.entries().map(|entry| (entry.entity_handle, entry.sort_handle.value())).collect())
} else { None }
}).unwrap_or_default();
let mut ordered: Vec<_> = document.entities().filter(|entity| {
let owner = entity.common().owner_handle;
owner == block || owner.is_null()
}).map(|entity| entity.common().handle).collect();
ordered.sort_by_key(|handle| (overrides.get(handle).copied().unwrap_or(handle.value()), handle.value()));
let selected_set: std::collections::HashSet<_> = selected.iter().copied().collect();
let reference_set: std::collections::HashSet<_> = references.iter().copied().collect();
let moved: Vec<_> = ordered.iter().copied().filter(|handle| selected_set.contains(handle)).collect();
if moved.is_empty() { return None; }
ordered.retain(|handle| !selected_set.contains(handle));
let indices: Vec<_> = ordered.iter().enumerate().filter_map(|(index, handle)| reference_set.contains(handle).then_some(index)).collect();
let insertion = if above { indices.into_iter().max()? + 1 } else { indices.into_iter().min()? };
ordered.splice(insertion..insertion, moved);
Some(ordered.into_iter().enumerate().map(|(index, handle)| (handle, index as u64 + 1)).collect())
}
/// Sort-key assignments sending `group` to the back of the active space.
///
/// Normal case: the floor is the lowest effective sort key among non-moved
@@ -2062,11 +2119,112 @@ mod tests {
assert!(hatch2_sort < ref_sort, "hatch 2 ({hatch2_sort}) must be under reference ({ref_sort})");
assert_ne!(hatch1_sort, hatch2_sort, "multi-select UNDER must not tie selected entities");
assert!(
hatch1_sort > hatch2_sort,
hatch1_sort < hatch2_sort,
"selection order must be preserved within the moved group ({hatch1_sort}, {hatch2_sort})"
);
}
#[test]
fn relative_group_uses_extremal_references_and_preserves_internal_order() {
let mut app = fresh_app();
let i = app.active_tab;
let moved_first = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
let reference_low = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
let middle = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
let reference_high = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
let moved_last = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
let block = app.tabs[i].scene.current_layout_block_handle_pub();
let selected = [moved_first, moved_last];
let references = [reference_low, reference_high];
let ordered = |above| {
let mut assignments = assign_relative_group_keys(
&app.tabs[i].scene.document,
block,
&selected,
&references,
above,
)
.expect("references belong to the active block");
assignments.sort_by_key(|(_, sort)| *sort);
assignments.into_iter().map(|(handle, _)| handle).collect::<Vec<_>>()
};
assert_eq!(
ordered(true),
vec![reference_low, middle, reference_high, moved_first, moved_last]
);
assert_eq!(
ordered(false),
vec![moved_first, moved_last, reference_low, middle, reference_high]
);
}
#[test]
fn relative_group_rejects_unusable_references_without_creating_a_table() {
let mut app = fresh_app();
let i = app.active_tab;
let moved = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
app.tabs[i].scene.replace_selection(std::iter::once(moved).collect());
let _ = app.run_command_line("DRAWORDER ABOVE deadbeef");
assert!(!app.tabs[i].scene.document.objects.values().any(|object| {
matches!(object, ObjectType::SortEntitiesTable(_))
}));
}
#[test]
fn draw_order_table_reattaches_orphan_and_advances_object_handles() {
use acadrust::objects::SortEntitiesTable;
let mut app = fresh_app();
let i = app.active_tab;
let block = app.tabs[i].scene.current_layout_block_handle_pub();
let entity = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
let orphan_handle = acadrust::Handle::new(0x10_0000);
let mut orphan = SortEntitiesTable::for_block(block);
orphan.handle = orphan_handle;
orphan.add_entry(entity, acadrust::Handle::new(7));
app.tabs[i]
.scene
.document
.objects
.insert(orphan_handle, ObjectType::SortEntitiesTable(orphan));
let resolved = ensure_draw_order_table(&mut app.tabs[i].scene.document, block);
assert_eq!(resolved, orphan_handle);
let dictionary_handle = app.tabs[i]
.scene
.document
.extension_dictionary_handle(block)
.expect("block extension dictionary");
let ObjectType::Dictionary(dictionary) = app.tabs[i]
.scene
.document
.objects
.get(&dictionary_handle)
.expect("dictionary object")
else {
panic!("extension object must be a dictionary");
};
assert_eq!(dictionary.get(SortEntitiesTable::DICTIONARY_KEY), Some(orphan_handle));
let ObjectType::SortEntitiesTable(table) = app.tabs[i]
.scene
.document
.objects
.get(&orphan_handle)
.expect("sort table")
else {
panic!("orphan must remain a sort table");
};
assert_eq!(table.owner_handle, dictionary_handle);
assert!(table.entries().any(|entry| {
entry.entity_handle == entity && entry.sort_handle.value() == 7
}));
assert!(app.tabs[i].scene.document.allocate_handle().value() > orphan_handle.value());
}
#[test]
fn draworder_front_moves_hatch_in_front_of_all() {
let mut app = fresh_app();
@@ -2175,18 +2333,18 @@ mod tests {
_ => panic!("Expected Relaunch for shortcut B"),
}
// 3. Shortcut 'A' -> Advances to PickReference (above)
// 3. Shortcut 'A' -> Advances to reference gathering (above)
let mut cmd = DrawOrderCommand::new(vec![h_hatch]);
assert!(!cmd.needs_entity_pick());
let res = cmd.on_text_input("A");
assert!(matches!(res, Some(crate::command::CmdResult::NeedPoint)));
assert!(cmd.needs_entity_pick(), "Needs entity pick after choosing 'A'");
assert!(cmd.is_selection_gathering());
// 4. Shortcut 'U' -> Advances to PickReference (under)
// 4. Shortcut 'U' -> Advances to reference gathering (under)
let mut cmd = DrawOrderCommand::new(vec![h_hatch]);
let res = cmd.on_text_input("U");
assert!(matches!(res, Some(crate::command::CmdResult::NeedPoint)));
assert!(cmd.needs_entity_pick(), "Needs entity pick after choosing 'U'");
assert!(cmd.is_selection_gathering());
}
#[test]
@@ -2198,14 +2356,15 @@ mod tests {
let h_ref = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
let h_hatch = app.tabs[i].scene.add_entity_clone(EntityType::Hatch(Default::default()));
// Above with viewport pick
// Above with a gathered viewport selection
let mut cmd = DrawOrderCommand::new(vec![h_hatch]);
let _ = cmd.on_text_input("A");
assert!(cmd.needs_entity_pick());
let pick_res = cmd.on_entity_pick(h_ref, glam::DVec3::ZERO);
assert!(cmd.is_selection_gathering());
let _ = cmd.on_selection_complete(vec![h_hatch, h_ref]);
let pick_res = cmd.on_enter();
match pick_res {
crate::command::CmdResult::Relaunch(relaunch_cmd, handles) => {
assert_eq!(relaunch_cmd, format!("DRAWORDER A {:x}", h_ref.value()));
assert_eq!(relaunch_cmd, format!("DRAWORDER ABOVE {:x}", h_ref.value()));
assert_eq!(handles, vec![h_hatch]);
app.tabs[i].scene.replace_selection(handles.into_iter().collect());
let _ = app.run_command_line(&relaunch_cmd);
@@ -2218,14 +2377,15 @@ mod tests {
let ref_sort = entries.get(&h_ref.value()).copied().unwrap_or(h_ref.value());
assert!(hatch_sort > ref_sort, "Hatch must be above reference after viewport pick");
// Under with viewport pick
// Under with a gathered viewport selection
let mut cmd = DrawOrderCommand::new(vec![h_hatch]);
let _ = cmd.on_text_input("U");
assert!(cmd.needs_entity_pick());
let pick_res = cmd.on_entity_pick(h_ref, glam::DVec3::ZERO);
assert!(cmd.is_selection_gathering());
let _ = cmd.on_selection_complete(vec![h_hatch, h_ref]);
let pick_res = cmd.on_enter();
match pick_res {
crate::command::CmdResult::Relaunch(relaunch_cmd, handles) => {
assert_eq!(relaunch_cmd, format!("DRAWORDER U {:x}", h_ref.value()));
assert_eq!(relaunch_cmd, format!("DRAWORDER UNDER {:x}", h_ref.value()));
assert_eq!(handles, vec![h_hatch]);
app.tabs[i].scene.replace_selection(handles.into_iter().collect());
let _ = app.run_command_line(&relaunch_cmd);
@@ -2252,9 +2412,10 @@ mod tests {
let _ = cmd.on_text_input("Above");
let hex_input = format!("0x{:x}", h_ref.value());
let typed_res = cmd.on_text_input(&hex_input);
match typed_res {
Some(crate::command::CmdResult::Relaunch(relaunch_cmd, handles)) => {
assert_eq!(relaunch_cmd, format!("DRAWORDER A {:x}", h_ref.value()));
assert!(matches!(typed_res, Some(crate::command::CmdResult::NeedPoint)));
match cmd.on_enter() {
crate::command::CmdResult::Relaunch(relaunch_cmd, handles) => {
assert_eq!(relaunch_cmd, format!("DRAWORDER ABOVE {:x}", h_ref.value()));
assert_eq!(handles, vec![h_hatch]);
app.tabs[i].scene.replace_selection(handles.into_iter().collect());
let _ = app.run_command_line(&relaunch_cmd);
@@ -2306,4 +2467,35 @@ mod tests {
let line_sort = entries.get(&h_line.value()).copied().unwrap_or(h_line.value());
assert!(hatch_sort > line_sort);
}
#[test]
fn draworder_mode_aliases_route_directly() {
let mut app = fresh_app();
let i = app.active_tab;
let line = app.tabs[i]
.scene
.add_entity_clone(EntityType::Line(Default::default()));
let hatch = app.tabs[i]
.scene
.add_entity_clone(EntityType::Hatch(Default::default()));
app.tabs[i]
.scene
.replace_selection(std::iter::once(hatch).collect());
let _ = app.run_command_line("DRAWORDER_FRONT");
let entries = effective_sort_map(&app);
assert!(
entries.get(&hatch.value()).copied().unwrap_or(hatch.value())
> entries.get(&line.value()).copied().unwrap_or(line.value())
);
app.tabs[i]
.scene
.replace_selection(std::iter::once(hatch).collect());
let _ = app.run_command_line("DRAWORDER_ABOVE");
assert!(app.tabs[i]
.active_cmd
.as_ref()
.is_some_and(|command| command.name() == "DRAWORDER" && command.is_selection_gathering()));
}
}
-4504
View File
File diff suppressed because it is too large. Load diff
+48 -1
View File
@@ -240,6 +240,17 @@ impl DocumentTab {
.and_then(|index| self.block_edits.get_mut(index))
}
/// The [`crate::scene::sketch_constraints::SketchScope`] 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 {
match self.active_block_edit_session() {
Some(session) => crate::scene::sketch_constraints::SketchScope::Block(session.br_handle),
None => crate::scene::sketch_constraints::SketchScope::ModelSpace,
}
}
/// The active WCS↔UCS converter for this tab — identity when no UCS is set.
/// Every consumer that needs UCS-relative coordinates goes through this.
pub(super) fn ucs_xform(&self) -> super::helpers::UcsXform {
@@ -644,7 +655,11 @@ impl DocumentTab {
/// full entity store.
#[derive(Clone)]
pub(super) enum HistorySnapshot {
Delta(DeltaSnapshot),
// Boxed: `DeltaSnapshot` grew past the other variants once
// `sketch_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>),
ObjectVisibility(ObjectVisibilitySnapshot),
}
@@ -677,6 +692,18 @@ impl HistorySnapshot {
.as_ref()
.map_or(0, |(before, after)| before.len().saturating_add(after.len())),
)
.saturating_add(
d.sketch_constraints
.iter()
.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.label.len()),
HistorySnapshot::ObjectVisibility(v) => v
.before
@@ -733,6 +760,17 @@ 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
/// entities (e.g. ERASE removing a constraint set's touched constraints)
/// — `sketch_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>,
/// Drawing-wide parameter table changed by this transaction.
pub(super) named_parameters: Option<(
crate::scene::named_parameters::ParameterTable,
crate::scene::named_parameters::ParameterTable,
)>,
pub(super) label: String,
}
@@ -793,6 +831,15 @@ pub(super) struct ObjectEntryDelta {
pub(super) after: Option<acadrust::objects::ObjectType>,
}
/// One sketch-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,
}
#[derive(Default)]
pub(super) struct HistoryState {
pub(super) undo_stack: Vec<HistorySnapshot>,
+53 -10
View File
@@ -1,7 +1,7 @@
use super::{
document::{
DeltaSnapshot, HistorySnapshot, ObjectEntryDelta, ObjectVisibilitySnapshot,
PendingHistorySnapshot, StructureSnapshot, TableEntryDelta,
PendingHistorySnapshot, SketchConstraintsEntryDelta, StructureSnapshot, TableEntryDelta,
},
OpenCADStudio,
};
@@ -225,9 +225,11 @@ impl OpenCADStudio {
dirty_after: true,
active_layer: None,
structure: (!objects.is_empty()).then_some(StructureSnapshot::Objects(objects)),
sketch_constraints: Vec::new(),
named_parameters: None,
label: label.into(),
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
}
pub(super) fn defer_live_entity_history_after(&mut self, i: usize, handle: Handle) {
@@ -334,9 +336,11 @@ impl OpenCADStudio {
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(),
named_parameters: None,
label: pending.label,
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
}
pub(super) fn finish_all_pending_history(&mut self) {
@@ -441,9 +445,11 @@ impl OpenCADStudio {
dirty_after: self.tabs[i].dirty,
active_layer: None,
structure: Some(StructureSnapshot::Layers(entries)),
sketch_constraints: Vec::new(),
named_parameters: None,
label: pending.label,
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
}
pub(super) fn begin_text_style_undo(
@@ -496,9 +502,11 @@ impl OpenCADStudio {
dirty_after: self.tabs[i].dirty,
active_layer: None,
structure: Some(StructureSnapshot::TextStyles(entries)),
sketch_constraints: Vec::new(),
named_parameters: None,
label: pending.label,
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
}
pub(super) fn begin_dim_style_undo(
@@ -551,9 +559,11 @@ impl OpenCADStudio {
dirty_after: self.tabs[i].dirty,
active_layer: None,
structure: Some(StructureSnapshot::DimStyles(entries)),
sketch_constraints: Vec::new(),
named_parameters: None,
label: pending.label,
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
}
fn group_object_state(&self, i: usize) -> FxHashMap<Handle, acadrust::objects::ObjectType> {
@@ -614,9 +624,11 @@ impl OpenCADStudio {
dirty_after: self.tabs[i].dirty,
active_layer: None,
structure: Some(StructureSnapshot::Objects(entries)),
sketch_constraints: Vec::new(),
named_parameters: None,
label: pending.label,
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
}
pub(super) fn commit_style_undo(
@@ -647,9 +659,11 @@ impl OpenCADStudio {
dim_names,
object_handles,
}),
sketch_constraints: Vec::new(),
named_parameters: None,
label: "STYLE".to_string(),
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
}
/// Dimension copies still need a full snapshot because they clone a fresh
@@ -727,7 +741,8 @@ impl OpenCADStudio {
pending.label
);
}
let (entity_before, object_before) = rec.into_recorded_images();
let (entity_before, object_before, sketch_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)| {
@@ -746,6 +761,25 @@ 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,
})
})
.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))
});
let selected_after = self.tabs[i].scene.selected.iter().copied().collect();
let dirty_after = self.tabs[i].dirty;
let mut structure = pending.structure_before.map(StructureSnapshot::Full);
@@ -781,9 +815,11 @@ impl OpenCADStudio {
dirty_after,
active_layer: None,
structure,
sketch_constraints,
named_parameters,
label: pending.label,
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
}
/// Apply one side of a delta entry in place: `undo` restores each entity's
@@ -909,6 +945,13 @@ 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 {
let value = if undo { &entry.before } else { &entry.after };
*scene.sketch_constraint_set_mut(entry.scope) = value.clone();
}
if let Some((before, after)) = &d.named_parameters {
scene.named_parameters = if undo { before } else { after }.clone();
}
let (sel, dirty) = if undo {
(&d.selected_before, d.dirty_before)
} else {
+72 -2
View File
@@ -9,8 +9,6 @@ pub(crate) mod config;
pub use automation::{export_headless, serve};
mod command_driver;
pub(crate) mod commands;
#[cfg(not(target_arch = "wasm32"))]
mod doc_api;
mod document;
pub(crate) mod expr_eval;
mod find_replace;
@@ -511,6 +509,7 @@ pub(super) struct OpenCADStudio {
/// Selected-object count past which grips stop being generated
/// (GRIPOBJLIMIT, 0..=32767; 0 = no limit).
grip_object_limit: i32,
ncopy_bind: bool,
/// Drawing viewport cursor style (CURSORTYPE).
cursor_type: settings::CursorType,
/// Explicit crosshair colour; `None` retains automatic contrast.
@@ -547,6 +546,14 @@ 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
/// glyph plus a driven value or named-parameter name. When false, every
/// pill shows only the glyph.
pub show_constraint_values: bool,
/// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command);
/// 0 disables autosave.
pub savetime_min: i32,
@@ -1040,6 +1047,15 @@ pub(super) struct OpenCADStudio {
/// edited. Seeded from `command_aliases` on open, committed back on close.
alias_editor_rows: Vec<(String, String)>,
// ── Named Parameters ──────────────────────────────────────────────────
/// Working buffer for the PARAMETERS modal. Unlike `alias_editor_rows`,
/// this isn't a copy of a separate app-level store — the real state
/// lives per-document at `Scene::named_parameters`; this buffer is
/// seeded from the active tab's table on open and only written back to
/// it on Apply (`apply_named_parameter_editor_rows`,
/// `src/app/named_parameters.rs`).
named_parameter_editor_rows: Vec<crate::ui::window::named_parameters::ParamEditorRow>,
// ── Layout Manager Panel ──────────────────────────────────────────────
layout_manager_selected: String,
layout_manager_rename_buf: String,
@@ -1717,6 +1733,7 @@ pub enum ModalKind {
AttributeEditor,
LayerDeleteWarning,
Aliases,
NamedParameters,
ScaleManager,
/// Add / remove the annotation scales a single selected object has a
/// per-object representation for.
@@ -2071,6 +2088,11 @@ 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),
/// Switch the interface language and redraw localized views.
LanguageChanged(crate::i18n::Language),
/// Drop every entity from the active drawing.
@@ -2494,6 +2516,10 @@ pub enum Message {
CloseLayoutList,
/// Cycle the coordinate readout mode ($COORDS): static → live → polar.
CycleCoordsMode,
/// Removes one flagged redundant or conflicting constraint from the
/// current sketch scope.
/// No-op if the scope currently has no flagged conflict.
ResolveOneSketchConflict,
/// Toggle the status-bar customization menu open/closed.
ToggleStatusBarMenu,
/// Close the status-bar customization menu.
@@ -2789,6 +2815,46 @@ pub enum Message {
AliasEditorRemove(usize),
/// Commit the edited rows to the alias table (Apply button); stays open.
AliasEditorApply,
// ── Named Parameters (PARAMETERS) ───────────────────────────────────
/// Open the named-parameter editor, seeding rows from the active tab's
/// `Scene::named_parameters`.
NamedParametersOpen,
/// Live edit of the name or formula in row `idx`.
NamedParametersInput {
idx: usize,
field: crate::ui::window::named_parameters::ParamField,
value: String,
},
/// Append a blank parameter row.
NamedParametersAdd,
/// Remove parameter row `idx`.
NamedParametersRemove(usize),
/// Commit the edited rows to `Scene::named_parameters` (Apply button)
/// and re-solve every constraint that reads a named parameter; stays
/// open.
NamedParametersApply,
// ── Parameters / Constraints sections embedded in Properties ──────────
/// Live text of one column of parameter row `index`, keyed by its
/// `ParameterTable::iter()` position — see `PropValue::ParamRow`.
PropParamInput {
index: usize,
field: crate::ui::window::named_parameters::ParamField,
value: String,
},
/// Commit row `index`'s buffered edit for `field` to `Scene::
/// named_parameters` (Enter / losing focus) and re-solve whatever it
/// drives.
PropParamCommit {
index: usize,
field: crate::ui::window::named_parameters::ParamField,
},
/// Remove parameter row `index` immediately.
PropParamDelete(usize),
/// Append a fresh, uniquely-named parameter to `Scene::named_parameters`.
PropParamAddNew,
/// A Constraints-section row was clicked: select every entity in the
/// list (replacing the current selection) in the viewport.
PropConstraintLinkClick(Vec<acadrust::Handle>),
// ── About window ────────────────────────────────────────────────────
AboutOpen,
/// Close whatever in-canvas modal dialog is open (Plan B).
@@ -3461,6 +3527,7 @@ impl OpenCADStudio {
double_click_block_refedit: false,
double_click_block_attedit: true,
grip_object_limit: settings::DEFAULT_GRIP_OBJECT_LIMIT,
ncopy_bind: false,
cursor_type: settings::CursorType::Crosshair,
crosshair_color: None,
crosshair_color_input: String::new(),
@@ -3478,6 +3545,8 @@ 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,
default_paper_bg_color: None,
@@ -3669,6 +3738,7 @@ impl OpenCADStudio {
// Command aliases (populated from ocad.pgp just after construction)
command_aliases: rustc_hash::FxHashMap::default(),
alias_editor_rows: Vec::new(),
named_parameter_editor_rows: Vec::new(),
// Layout Manager
layout_manager_selected: "Model".to_string(),
layer_state_selected: None,
-40
View File
@@ -16,8 +16,6 @@ pub(crate) struct HostSession<'a> {
app: &'a mut OpenCADStudio,
tab: usize,
doc_store: Option<DocumentSnapshotStore<DocumentViewData>>,
/// In-flight DocApi v2 undo delta (between `push_undo` and `finalize_op`).
doc_api_pending: Option<super::history::PendingDelta>,
}
impl<'a> HostSession<'a> {
@@ -26,7 +24,6 @@ impl<'a> HostSession<'a> {
app,
tab,
doc_store: None,
doc_api_pending: None,
}
}
@@ -245,39 +242,6 @@ impl<'a> HostSession<'a> {
self.app.push_undo_snapshot(self.tab, label);
}
// ── DocApi v2 accessors (used by `app::doc_api` backend) ───────────────
pub(crate) fn scene(&self) -> &crate::scene::Scene {
&self.app.tabs[self.tab].scene
}
pub(crate) fn scene_mut(&mut self) -> &mut crate::scene::Scene {
&mut self.app.tabs[self.tab].scene
}
/// DocApi `push_undo`: capture entity and document-structure changes for one op.
pub(crate) fn begin_doc_api_undo(&mut self, label: &str) {
self.doc_api_pending = self.app.begin_undo(self.tab, label, 1, false);
}
pub(crate) fn cancel_doc_api_undo(&mut self) {
if self.doc_api_pending.take().is_some() {
self.scene_mut().take_undo_recording();
}
}
/// DocApi `finalize_op`: close the delta + republish the document view.
/// The underlying entity op (add/update/remove/register_solid_model) already
/// bumped `geometry_epoch`, so we do NOT bump again here — exactly one undo
/// step is recorded, and the epoch has advanced for this op.
pub(crate) fn commit_doc_api_undo(&mut self) {
self.set_dirty();
if let Some(pending) = self.doc_api_pending.take() {
self.app.commit_undo_delta(self.tab, pending);
}
self.publish_document_view();
}
pub fn set_dirty(&mut self) {
self.app.tabs[self.tab].dirty = true;
}
@@ -396,10 +360,6 @@ impl HostApi for HostSession<'_> {
fn close_document_view_v4(&mut self, tab_id: u64) {
self.close_document_view_v4(tab_id)
}
#[cfg(not(target_arch = "wasm32"))]
fn doc_api_dispatch(&mut self, tab_id: u64, bytes: &[u8]) -> Result<Vec<u8>, String> {
super::doc_api::execute_doc_api(self, tab_id, bytes)
}
}
/// Bridges a plugin's [`InteractiveCommand`](ocs_plugin_api::host::InteractiveCommand)
+87
View File
@@ -406,6 +406,37 @@ impl OpenCADStudio {
read_only(t!("Visual Style").as_ref(), tab.visual_style.clone()),
],
},
// Document-wide named-parameter table embedded here
// instead of a separate modal — see `PropValue::
// ParamRow`'s doc comment. Belongs on the no-
// selection (drawing-level) page, not a per-entity
// one: a parameter isn't owned by any one entity.
PropSection {
title: t!("Parameters").into_owned(),
props: {
let mut props: Vec<Property> = scene
.named_parameters()
.iter()
.enumerate()
.map(|(index, p)| Property {
label: String::new(),
field: "named_parameter",
value: PropValue::ParamRow {
index,
name: p.name.clone(),
formula: p.source.clone(),
resolved: scene.named_parameters().resolve(&p.name).map_err(|e| e.to_string()),
},
})
.collect();
props.push(Property {
label: String::new(),
field: "named_parameter_add",
value: PropValue::ParamAddRow,
});
props
},
},
];
ui::PropertiesPanel {
title: t!("No selection").into_owned(),
@@ -2126,6 +2157,53 @@ impl OpenCADStudio {
if compact_solid {
retain_compact_solid_sections(&mut sections);
}
// Persistent sketch-constraint list for this entity —
// the Properties-panel analogue of the viewport's
// constraint glyph pills: one row per constraint
// touching `handle`, clickable to select every entity
// it relates (PropConstraintLinkClick). Omitted
// entirely when the entity has none — an unconstrained
// entity (the common case) shouldn't gain empty
// bookkeeping.
{
let scope = self.tabs[i].current_sketch_scope();
let scene = &self.tabs[i].scene;
if let Some(set) = scene.sketch_constraint_set(scope) {
let props: Vec<crate::scene::model::object::Property> = set
.constraints_touching(handle)
.map(|c| {
let conflicting = set.conflicts.iter().any(|(id, _)| *id == c.id);
let value_suffix = match &c.driving_param {
Some(crate::scene::named_parameters::DrivingValue::Literal(v)) => {
format!(" = {v:.2}")
}
Some(crate::scene::named_parameters::DrivingValue::Named(name)) => {
match scene.named_parameters().resolve(name) {
Ok(v) => format!(" = {name} ({v:.2})"),
Err(_) => format!(" = {name} (?)"),
}
}
None => String::new(),
};
let label = format!("{} {:?}{}", c.kind.glyph_symbol(), c.kind, value_suffix);
let mut handles: Vec<acadrust::Handle> = c.refs.iter().map(|r| r.entity).collect();
handles.sort_by_key(|h| h.value());
handles.dedup();
crate::scene::model::object::Property {
label,
field: "sketch_constraint",
value: crate::scene::model::object::PropValue::EntityLink { handles, conflicting },
}
})
.collect();
if !props.is_empty() {
sections.push(crate::scene::model::object::PropSection {
title: t!("Constraints").into_owned(),
props,
});
}
}
}
let title = match entity {
acadrust::EntityType::Insert(ins) => {
let is_xref = self.tabs[i]
@@ -3112,6 +3190,15 @@ fn make_sections_read_only(
}
PropValue::Stepper { display, .. } => display.clone(),
PropValue::AttrText { value, .. } => value.clone(),
// The Constraints/Parameters sections aren't per-entity editable
// fields to begin with (a constraint link is a navigation
// action, not a value; a parameter belongs to the whole
// document, not the locked entity) — locking the entity doesn't
// change either, so just carry through readable text instead of
// collapsing them to a value that was never editable anyway.
PropValue::EntityLink { handles, .. } => format!("{} entity link(s)", handles.len()),
PropValue::ParamRow { name, formula, .. } => format!("{name} = {formula}"),
PropValue::ParamAddRow => String::new(),
};
property.field = "locked_read_only";
property.value = PropValue::ReadOnly(text);
+19
View File
@@ -203,6 +203,8 @@ pub struct UserSettings {
/// GRIPOBJLIMIT: past this many selected objects, no grips are drawn at
/// all. 0 means no limit. The drawing header carries no slot for it.
pub grip_object_limit: i32,
/// Nested-copy symbol handling: false inserts, true binds.
pub ncopy_bind: bool,
/// Last Options page; unknown saved names fall back without rejecting the config.
#[serde(default, deserialize_with = "deserialize_options_tab")]
pub options_tab: crate::ui::window::options::OptionsTab,
@@ -276,6 +278,16 @@ pub struct UserSettings {
/// When true (default), the app (re)registers itself as a .dwg/.dxf/.bak
/// handler on every launch. Toggle with the FILEASSOC command.
pub file_assoc_enabled: bool,
/// When true, saving also writes sketch constraints as native drawing
/// objects alongside the application's own persistence record.
#[serde(default)]
pub write_dwg_native_constraints: bool,
/// When true (default), a sketch constraint's viewport pill shows its
/// glyph plus a driven value or named-parameter name. When false, every
/// pill shows just the bare glyph, so the value/name text doesn't cover
/// canvas detail on a dense sketch.
#[serde(default = "default_show_constraint_values")]
pub show_constraint_values: bool,
/// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command).
/// 0 disables autosave.
pub savetime_min: i32,
@@ -342,6 +354,10 @@ fn default_dimension_continue_mode() -> i16 {
1
}
fn default_show_constraint_values() -> bool {
true
}
fn deserialize_clipromptlines<'de, D>(de: D) -> Result<i32, D::Error>
where
D: serde::Deserializer<'de>,
@@ -372,6 +388,7 @@ impl Default for UserSettings {
double_click_block_refedit: false,
double_click_block_attedit: true,
grip_object_limit: DEFAULT_GRIP_OBJECT_LIMIT,
ncopy_bind: false,
cursor_type: CursorType::Crosshair,
crosshair_color: None,
lineweight_display_scale: 100,
@@ -395,6 +412,8 @@ 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(),
pick_add: true,
+3
View File
@@ -890,6 +890,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
}
pub(super) fn on_command_escape(&mut self) -> Task<Message> {
if self.ribbon.escape_extension() {
return Task::none();
}
// Esc drops an unconsumed one-shot snap override and closes
// its menu (#337). Falls through — Esc keeps its usual effect.
self.snap_override_popup = None;
+21
View File
@@ -404,6 +404,7 @@ impl OpenCADStudio {
double_click_block_refedit: self.double_click_block_refedit,
double_click_block_attedit: self.double_click_block_attedit,
grip_object_limit: self.grip_object_limit,
ncopy_bind: self.ncopy_bind,
cursor_type: self.cursor_type,
crosshair_color: self.crosshair_color,
lineweight_display_scale: self.lineweight_display_scale,
@@ -432,6 +433,8 @@ impl OpenCADStudio {
textfill: crate::scene::text::sdf_atlas::textfill(),
backup_on_save: self.backup_on_save,
file_assoc_enabled: self.file_assoc_enabled,
write_dwg_native_constraints: self.write_dwg_native_constraints,
show_constraint_values: self.show_constraint_values,
savetime_min: self.savetime_min,
default_save_format: self.default_save_format.clone(),
pick_add: self.pick_add,
@@ -467,6 +470,7 @@ impl OpenCADStudio {
self.double_click_block_refedit = s.double_click_block_refedit;
self.double_click_block_attedit = s.double_click_block_attedit;
self.grip_object_limit = s.grip_object_limit.clamp(0, 32767);
self.ncopy_bind = s.ncopy_bind;
self.cursor_type = s.cursor_type;
self.crosshair_color = s.crosshair_color;
self.crosshair_color_input = s
@@ -510,6 +514,8 @@ impl OpenCADStudio {
crate::scene::text::sdf_atlas::set_textfill(s.textfill);
self.backup_on_save = s.backup_on_save;
self.file_assoc_enabled = s.file_assoc_enabled;
self.write_dwg_native_constraints = s.write_dwg_native_constraints;
self.show_constraint_values = s.show_constraint_values;
self.savetime_min = s.savetime_min;
self.default_save_format =
crate::io::canonical_save_format(&s.default_save_format).to_string();
@@ -1438,6 +1444,11 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
self.tabs[i].scene.material_base_dir =
path.parent().map(std::path::Path::to_path_buf);
self.tabs[i].scene.document = doc;
// Load persisted constraints after installing the document.
self.tabs[i].scene.load_sketch_constraints_from_document();
// named_parameters_design.md stage 2: same load-time hook,
// for the document-wide parameter table.
self.tabs[i].scene.load_named_parameters_from_document();
self.tabs[i].active_layer = self.tabs[i]
.scene
.document
@@ -1701,6 +1712,13 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
self.tabs[i].scene.document.header.user_real1 =
self.tabs[i].scene.annotation_scale as f64;
self.sync_solid_models_for_save(i);
// Materialize constraints immediately before serialization.
self.tabs[i].scene.materialize_sketch_constraints_for_save();
// named_parameters_design.md stage 2: same save-time hook, for the
// document-wide parameter table.
self.tabs[i].scene.materialize_named_parameters_for_save();
// Keep the optional native constraint graph synchronized on save.
self.tabs[i].scene.materialize_dwg_native_constraints_for_save(self.write_dwg_native_constraints);
}
#[cfg(not(target_arch = "wasm32"))]
@@ -2651,6 +2669,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
sync_annotation_scale_header(&mut self.tabs[i].scene);
self.stamp_header_sysvars(i);
self.sync_solid_models_for_save(i);
self.tabs[i].scene.materialize_sketch_constraints_for_save();
self.tabs[i].scene.materialize_named_parameters_for_save();
self.tabs[i].scene.materialize_dwg_native_constraints_for_save(self.write_dwg_native_constraints);
let tab_id = self.tabs[i].id;
let bounds = crate::ui::wrap_bar::dropdown_bounds(
crate::app::view::VIEWPORT_CAPTURE_BOUNDS_ID,
+73
View File
@@ -3356,6 +3356,10 @@ impl OpenCADStudio {
}
Task::none()
}
Message::ResolveOneSketchConflict => {
self.resolve_one_sketch_conflict();
Task::none()
}
Message::TogglePolar => {
self.polar_mode ^= true;
if self.polar_mode {
@@ -5926,6 +5930,65 @@ impl OpenCADStudio {
Task::none()
}
// ── Named Parameters (PARAMETERS) ─────────────────────────────────
Message::NamedParametersOpen => {
let i = self.active_tab;
self.named_parameter_editor_rows = self.tabs[i]
.scene
.named_parameters()
.iter()
.map(|p| crate::ui::window::named_parameters::ParamEditorRow { name: p.name.clone(), formula: p.source.clone() })
.collect();
self.active_modal = Some(super::ModalKind::NamedParameters);
Task::none()
}
Message::NamedParametersInput { idx, field, value } => {
use crate::ui::window::named_parameters::ParamField;
if let Some(row) = self.named_parameter_editor_rows.get_mut(idx) {
match field {
ParamField::Name => row.name = value,
ParamField::Formula => row.formula = value,
}
}
Task::none()
}
Message::NamedParametersAdd => {
self.named_parameter_editor_rows.push(crate::ui::window::named_parameters::ParamEditorRow::default());
Task::none()
}
Message::NamedParametersRemove(idx) => {
if idx < self.named_parameter_editor_rows.len() {
self.named_parameter_editor_rows.remove(idx);
}
Task::none()
}
Message::NamedParametersApply => {
self.apply_named_parameter_editor_rows();
Task::none()
}
// ── Parameters / Constraints sections embedded in the
// Properties panel ──────────────────────────────────────────────
Message::PropParamInput { index, field, value } => {
self.tabs[self.active_tab]
.properties
.edit_buf
.insert(crate::ui::properties::FieldKey::Param(index, field), value);
Task::none()
}
Message::PropParamCommit { index, field } => self.on_prop_param_commit(index, field),
Message::PropParamDelete(index) => self.on_prop_param_delete(index),
Message::PropParamAddNew => self.on_prop_param_add_new(),
Message::PropConstraintLinkClick(handles) => {
let i = self.active_tab;
self.tabs[i].scene.deselect_all();
for h in handles {
self.tabs[i].scene.select_entity(h, false);
}
self.refresh_properties();
Task::none()
}
// ── Options / About windows ───────────────────────────────────
Message::OptionsOpen => {
self.active_modal = Some(super::ModalKind::Options);
@@ -6448,6 +6511,16 @@ impl OpenCADStudio {
}
Task::none()
}
Message::WriteDwgNativeConstraintsChanged(enabled) => {
self.write_dwg_native_constraints = enabled;
self.persist_settings_if_changed();
Task::none()
}
Message::ShowConstraintValuesChanged(enabled) => {
self.show_constraint_values = enabled;
self.persist_settings_if_changed();
Task::none()
}
Message::LanguageChanged(language) => {
if self.language == language {
return Task::none();
+26 -1
View File
@@ -1246,7 +1246,7 @@ impl OpenCADStudio {
};
let mut seen_handles = rustc_hash::FxHashSet::default();
let edited_handles: Vec<_> = grip
let mut edited_handles: Vec<_> = grip
.targets
.iter()
.map(|target| target.handle)
@@ -1706,6 +1706,29 @@ impl OpenCADStudio {
);
}
}
// Re-solve constrained neighbors on each drag frame and include
// their original state in the gesture's undo record.
let solved_by_constraints = self.tabs[i].scene.solve_sketch_constraints_preview(&edited_handles);
for (handle, _) in &solved_by_constraints {
let handle = *handle;
if !self.grip_preview_handles.contains(&handle) {
if let Some(original) = self.tabs[i].scene.document.get_entity(handle).cloned() {
self.grip_originals.push((handle, original));
}
self.grip_preview_handles.push(handle);
if !self.tabs[i].scene.meshes.contains_key(&handle) {
self.tabs[i].scene.preview_hidden.insert(handle);
}
}
if !edited_handles.contains(&handle) {
edited_handles.push(handle);
}
}
for (handle, new_entity) in solved_by_constraints {
if let Some(slot) = self.tabs[i].scene.document.get_entity_mut(handle) {
*slot = new_entity;
}
}
let mesh_changes: Vec<_> = edited_handles
.iter()
.copied()
@@ -3887,6 +3910,7 @@ impl OpenCADStudio {
} else if self.tabs[i].active_cmd.as_ref()
.is_some_and(|command| command.entity_pick_accepts_points())
{
self.refresh_command_point_pick_context(i);
self.tabs[i].active_cmd.as_mut().map(|command| command.on_point(pick_wcs))
} else if self.tabs[i]
.active_cmd
@@ -3963,6 +3987,7 @@ impl OpenCADStudio {
// (LINE tangent to two circles, which needs both). When
// it does, sync last_point to the command's resolved
// anchor since it replaced the picked coordinate.
self.refresh_command_point_pick_context(i);
let handled = self.tabs[i]
.active_cmd
.as_mut()
+47
View File
@@ -805,6 +805,50 @@ bg={bg_ms:.1}ms n={view_count}"
let point_cursor = tab.active_cmd.as_ref().is_some_and(|cmd| {
!cmd.needs_entity_pick() && !cmd.is_selection_gathering()
});
// Constraint glyphs are displayed in model space only.
let constraint_glyphs: Vec<(iced::Point, String, bool)> = if is_paper {
Vec::new()
} else {
let (vw, vh) = sel_ref.vp_size;
let scope = tab.current_sketch_scope();
match tab.scene.sketch_constraint_set(scope) {
Some(set) if !set.constraints.is_empty() => {
let edit_frame = tab.scene.viewport_edit_frame((vw, vh));
let bounds = match &edit_frame {
Some((_, full)) => *full,
None => tab.scene.active_model_tile_bounds(vw, vh),
};
let (view_rot, eye) = if let Some((cam, _)) = &edit_frame {
(cam.view_proj_rte(bounds), cam.eye())
} else {
let cam = tab.scene.camera.borrow();
(cam.view_proj_rte(bounds), cam.eye())
};
set.constraints
.iter()
.filter(|c| c.enabled)
.filter_map(|c| {
let anchor = crate::scene::sketch_constraints::glyph_anchor(&tab.scene.document, c)?;
let screen = crate::scene::pick::grip::project_rte(
glam::DVec3::new(anchor.x, anchor.y, anchor.z),
view_rot,
eye,
bounds,
)?;
let point = iced::Point::new(bounds.x + screen.x, bounds.y + screen.y);
let is_conflicting = set.conflicts.iter().any(|(id, _)| *id == c.id);
let label = if self.show_constraint_values {
crate::scene::sketch_constraints::glyph_label(c)
} else {
c.kind.glyph_symbol().to_string()
};
point.x.is_finite().then(|| (point, label, is_conflicting))
})
.collect()
}
_ => Vec::new(),
}
};
crate::ui::overlay::selection_overlay(
std::sync::Arc::clone(&tab.scene.selection),
snap_info,
@@ -847,6 +891,7 @@ bg={bg_ms:.1}ms n={view_count}"
grip_hot: self.model_space.grip_hot,
grip_hover: self.model_space.grip_hover,
},
constraint_glyphs,
)
};
@@ -2091,6 +2136,8 @@ bg={bg_ms:.1}ms n={view_count}"
self.selection_cycling,
&self.statusbar_config,
status_menu_data,
tab.scene.sketch_constraint_set(tab.current_sketch_scope()).and_then(|s| s.dof),
tab.scene.sketch_constraint_set(tab.current_sketch_scope()).map(|s| s.conflicts.len()).unwrap_or(0),
)
})
.width(Fill)
+18
View File
@@ -13,6 +13,7 @@ impl OpenCADStudio {
Some(K::About) => crate::tr!("modal", "about"),
Some(K::Shortcuts) => crate::tr!("modal", "keyboard-shortcuts"),
Some(K::Aliases) => crate::tr!("modal", "command-aliases"),
Some(K::NamedParameters) => crate::t!("Named Parameters").into_owned(),
Some(K::Options) => crate::tr!("action", "options"),
Some(K::FindReplace) => crate::tr!("modal", "find-replace"),
Some(K::PluginManager) => crate::tr!("modal", "plugin-manager"),
@@ -173,6 +174,21 @@ impl OpenCADStudio {
},
)
}
super::super::ModalKind::NamedParameters => {
let scene = &self.tabs[self.active_tab].scene;
sized_flow(
ex,
820,
520,
|flow| {
crate::ui::window::named_parameters::view_window(
&self.named_parameter_editor_rows,
scene,
flow,
)
},
)
}
super::super::ModalKind::Options => sized_flow(
ex,
880,
@@ -181,6 +197,8 @@ 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,
self.language,