forked from 1b-agent/opencadstudio-upstream
Merge current main with division marker support
This commit is contained in:
commit
f076db6d53
160 files changed
+16859
-16176
No files matched your search
@@ -323,6 +323,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,
|
||||
@@ -1610,4 +1612,3 @@ mod tests {
|
||||
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("MTP"));
|
||||
}
|
||||
}
|
||||
|
||||
+1341
-128
File diff suppressed because it is too large.
Load diff
+18
-34
@@ -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();
|
||||
|
||||
+260
-17
@@ -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" => {
|
||||
@@ -775,7 +789,7 @@ impl OpenCADStudio {
|
||||
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);
|
||||
@@ -808,7 +822,7 @@ 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());
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
} else {
|
||||
@@ -816,7 +830,7 @@ impl OpenCADStudio {
|
||||
.push_error(crate::t!("HATCHEDIT: selected entity is not a hatch.").as_ref());
|
||||
}
|
||||
} else {
|
||||
let cmd = HatcheditCommand::new();
|
||||
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));
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -1373,6 +1387,7 @@ impl OpenCADStudio {
|
||||
crate::entities::names::dxf_name(e).to_string(),
|
||||
c.layer.clone(),
|
||||
c.color,
|
||||
c.transparency,
|
||||
c.linetype.clone(),
|
||||
c.linetype_scale,
|
||||
c.line_weight,
|
||||
@@ -1384,7 +1399,7 @@ impl OpenCADStudio {
|
||||
},
|
||||
)
|
||||
});
|
||||
let Some((verb, kind, layer, color, linetype, lt_scale, lw, template_dimstyle)) = info
|
||||
let Some((verb, kind, layer, color, transparency, linetype, lt_scale, lw, template_dimstyle)) = info
|
||||
else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("ADDSELECTED: selected object not found.").as_ref());
|
||||
@@ -1408,6 +1423,7 @@ impl OpenCADStudio {
|
||||
layer_name: self.tabs[i].scene.document.header.current_layer_name.clone(),
|
||||
layer_handle: self.tabs[i].scene.document.header.current_layer_handle,
|
||||
color: self.tabs[i].scene.document.header.current_entity_color,
|
||||
transparency: self.tabs[i].scene.document.current_entity_transparency(),
|
||||
linetype_name: self.tabs[i].scene.document.header.current_linetype_name.clone(),
|
||||
linetype_handle: self.tabs[i].scene.document.header.current_linetype_handle,
|
||||
line_weight: self.tabs[i].scene.document.header.current_line_weight,
|
||||
@@ -1422,6 +1438,11 @@ impl OpenCADStudio {
|
||||
ribbon_lineweight: self.ribbon.active_lineweight,
|
||||
};
|
||||
self.add_selected_restore = Some(restore);
|
||||
if !self.tabs[i].scene.document.set_current_entity_transparency(transparency) {
|
||||
self.add_selected_restore = None;
|
||||
self.command_line.push_error("ADDSELECTED: template transparency cannot be adopted.");
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
// Adopt the template's general properties as the current defaults. The
|
||||
// entity-creation path stamps new objects from the tab's active layer
|
||||
@@ -1504,6 +1525,9 @@ impl OpenCADStudio {
|
||||
h.current_dimstyle_name = r.dimstyle_name;
|
||||
h.current_dimstyle_handle = r.dimstyle_handle;
|
||||
}
|
||||
if !self.tabs[i].scene.document.set_current_entity_transparency(r.transparency) {
|
||||
self.command_line.push_error("ADDSELECTED: current transparency could not be restored.");
|
||||
}
|
||||
self.tabs[i].active_layer = r.tab_active_layer;
|
||||
self.tabs[i].layers.current_layer = r.tab_layers_current;
|
||||
self.ribbon.active_layer = r.ribbon_layer;
|
||||
|
||||
@@ -322,6 +322,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"CLEANSCREEN",
|
||||
"CUI",
|
||||
"DSETTINGS",
|
||||
"PARAMETERS",
|
||||
"GRID",
|
||||
"ISODRAFT",
|
||||
"ISOPLANE",
|
||||
@@ -368,6 +369,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"COLOR",
|
||||
"COLOUR",
|
||||
"CECOLOR",
|
||||
"CETRANSPARENCY",
|
||||
"DDCOLOR",
|
||||
"BYLAYER",
|
||||
// Synchronise block attributes.
|
||||
@@ -598,6 +600,10 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"DIMSTYLE",
|
||||
"DONATE",
|
||||
"DRAWORDER",
|
||||
"DRAWORDER_FRONT",
|
||||
"DRAWORDER_BACK",
|
||||
"DRAWORDER_ABOVE",
|
||||
"DRAWORDER_UNDER",
|
||||
"DWGPROP",
|
||||
"DWGPROPS",
|
||||
"EATTEXT",
|
||||
|
||||
+230
-64
@@ -3,6 +3,10 @@ use super::*;
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn dispatch_styleprops(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||
match cmd {
|
||||
"CETRANSPARENCY" => return self.dispatch_styleprops("SETVAR CETRANSPARENCY", i),
|
||||
cmd if cmd.starts_with("CETRANSPARENCY ") => {
|
||||
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
|
||||
}
|
||||
"FRAMES0" => return self.dispatch_styleprops("SETVAR FRAME 0", i),
|
||||
"FRAMES1" => return self.dispatch_styleprops("SETVAR FRAME 1", i),
|
||||
"FRAMES2" => return self.dispatch_styleprops("SETVAR FRAME 2", i),
|
||||
@@ -645,98 +649,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).
|
||||
@@ -941,9 +1068,34 @@ impl OpenCADStudio {
|
||||
let value = it.next().map(|s| s.trim().to_string());
|
||||
if name.is_empty() || name == "?" {
|
||||
self.command_line.push_info(
|
||||
crate::t!("SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(),
|
||||
crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(),
|
||||
);
|
||||
} else {
|
||||
if name == "CETRANSPARENCY" {
|
||||
let current = self.tabs[i].scene.document.current_entity_transparency();
|
||||
if let Some(value) = &value {
|
||||
match crate::scene::creation_style::parse_current_transparency(value) {
|
||||
Some(transparency) => {
|
||||
if current != transparency {
|
||||
self.push_undo_snapshot(i, &name);
|
||||
if !self.tabs[i].scene.document.set_current_entity_transparency(transparency) {
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line.push_error("CETRANSPARENCY: drawing variable dictionary is invalid.");
|
||||
return Some(self.finish_dispatch(cmd));
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
}
|
||||
self.command_line.push_output(&format!("CETRANSPARENCY = {}", crate::scene::creation_style::current_transparency_label(transparency)));
|
||||
}
|
||||
None => self.command_line.push_error("CETRANSPARENCY: expected ByLayer (-1), ByBlock (-2), or an integer from 0 to 90."),
|
||||
}
|
||||
} else {
|
||||
self.command_line.push_output(&format!("Enter new value for CETRANSPARENCY <{}>:", crate::scene::creation_style::current_transparency_label(current)));
|
||||
self.pending_setvar = Some(name.clone());
|
||||
}
|
||||
return Some(self.finish_dispatch(cmd));
|
||||
}
|
||||
if matches!(name.as_str(), "SHOWHIST" | "SOLIDHIST") {
|
||||
let current = if name == "SHOWHIST" {
|
||||
self.tabs[i].scene.document.header.show_solid_history.clamp(0, 2)
|
||||
@@ -2983,6 +3135,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 +3362,3 @@ mod scale_validation_tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+335
-143
@@ -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()));
|
||||
}
|
||||
}
|
||||
-4501
File diff suppressed because it is too large.
Load diff
+48
-1
@@ -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
@@ -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 {
|
||||
|
||||
+73
-2
@@ -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;
|
||||
@@ -307,6 +305,7 @@ struct AddSelectedRestore {
|
||||
layer_name: String,
|
||||
layer_handle: acadrust::types::Handle,
|
||||
color: AcadColor,
|
||||
transparency: acadrust::types::Transparency,
|
||||
linetype_name: String,
|
||||
linetype_handle: acadrust::types::Handle,
|
||||
line_weight: i16,
|
||||
@@ -511,6 +510,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 +547,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 +1048,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 +1734,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 +2089,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 +2517,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 +2816,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 +3528,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 +3546,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 +3739,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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
+88
-1
@@ -306,7 +306,7 @@ impl OpenCADStudio {
|
||||
label: t!("Transparency").into_owned(),
|
||||
field: "transparency",
|
||||
value: PropValue::EditChoice {
|
||||
value: "ByLayer".to_string(),
|
||||
value: crate::scene::creation_style::current_transparency_label(self.tabs[i].scene.document.current_entity_transparency()),
|
||||
options: vec!["ByLayer".to_string(), "ByBlock".to_string()],
|
||||
},
|
||||
},
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
+27
-15
@@ -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;
|
||||
@@ -1997,9 +2000,6 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
if let Some(acadrust::EntityType::Hatch(dxf)) =
|
||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||
{
|
||||
let old_origin = dxf.pattern.lines.first().map(|line| {
|
||||
(line.base_point.x, line.base_point.y)
|
||||
});
|
||||
let mut pattern = hatch_patterns::build_dxf_pattern(entry);
|
||||
// Stored pattern lines are final world-space
|
||||
// geometry. Preserve the selected hatch's scale,
|
||||
@@ -2013,17 +2013,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
&mut pattern,
|
||||
dxf.pattern_angle,
|
||||
);
|
||||
if let (Some((old_x, old_y)), Some(new_origin)) =
|
||||
(old_origin, pattern.lines.first())
|
||||
{
|
||||
let dx = old_x - new_origin.base_point.x;
|
||||
let dy = old_y - new_origin.base_point.y;
|
||||
crate::entities::hatch::translate_pattern_geometry(
|
||||
&mut pattern,
|
||||
dx,
|
||||
dy,
|
||||
);
|
||||
}
|
||||
let origin = dxf.pattern_origin();
|
||||
crate::entities::hatch::translate_pattern_geometry(&mut pattern, origin.x, origin.y);
|
||||
dxf.pattern = pattern;
|
||||
dxf.is_solid = matches!(
|
||||
entry.gpu,
|
||||
@@ -2808,7 +2799,28 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
} else {
|
||||
match field {
|
||||
"transparency" => {
|
||||
self.tabs[i].dirty = true;
|
||||
let Some(transparency) = crate::scene::creation_style::parse_current_transparency(&value) else {
|
||||
self.command_line.push_error("Transparency: expected ByLayer, ByBlock, or an integer from 0 to 90.");
|
||||
self.refresh_properties();
|
||||
return Task::none();
|
||||
};
|
||||
if self.tabs[i].scene.document.current_entity_transparency()
|
||||
!= transparency
|
||||
{
|
||||
self.push_undo_snapshot(i, "CETRANSPARENCY");
|
||||
if self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.set_current_entity_transparency(transparency)
|
||||
{
|
||||
self.tabs[i].dirty = true;
|
||||
} else {
|
||||
self.discard_last_undo_entry(i);
|
||||
self.command_line.push_error(
|
||||
"CETRANSPARENCY: drawing variable dictionary is invalid.",
|
||||
);
|
||||
}
|
||||
}
|
||||
self.refresh_properties();
|
||||
}
|
||||
"material" => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
@@ -3847,13 +3870,22 @@ impl OpenCADStudio {
|
||||
};
|
||||
use crate::command::CadCommand;
|
||||
use crate::modules::draw::draw::hatchedit::HatcheditCommand;
|
||||
let cmd: Box<dyn CadCommand> = Box::new(HatcheditCommand::with_handle(
|
||||
handle,
|
||||
model.name.clone(),
|
||||
scale,
|
||||
angle,
|
||||
annotative,
|
||||
));
|
||||
let current_color =
|
||||
self.tabs[i].scene.document.header.current_entity_color;
|
||||
let current_transparency =
|
||||
self.tabs[i].scene.document.current_entity_transparency();
|
||||
let current_origin = self.tabs[i].scene.document.hatch_origin();
|
||||
let cmd: Box<dyn CadCommand> = Box::new(
|
||||
HatcheditCommand::with_handle(
|
||||
handle,
|
||||
model.name.clone(),
|
||||
scale,
|
||||
angle,
|
||||
annotative,
|
||||
)
|
||||
.with_appearance(entity, current_color, current_transparency)
|
||||
.with_origin(current_origin),
|
||||
);
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(cmd);
|
||||
} else {
|
||||
@@ -3887,6 +3919,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 +3996,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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user