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
+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()));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user