forked from 1b-agent/opencadstudio-upstream
Merge remote-tracking branch 'origin/main' into codex/region-measurement-persistence
# Conflicts: # src/app/doc_api.rs
This commit is contained in:
commit
17efc86cc1
149 files changed
+15933
-16047
No files matched your search
@@ -317,6 +317,8 @@ impl OpenCADStudio {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].scene.clear();
|
||||
self.tabs[i].scene.document = doc;
|
||||
self.tabs[i].scene.load_sketch_constraints_from_document();
|
||||
self.tabs[i].scene.load_named_parameters_from_document();
|
||||
self.tabs[i].scene.material_base_dir = path_buf.parent().map(PathBuf::from);
|
||||
crate::app::style_ops::ensure_standard_styles(
|
||||
&mut self.tabs[i].scene.document,
|
||||
@@ -1604,4 +1606,3 @@ mod tests {
|
||||
assert_eq!(app.tabs[0].active_cmd.as_ref().map(|c| c.name()), Some("MTP"));
|
||||
}
|
||||
}
|
||||
|
||||
+1201
-80
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();
|
||||
|
||||
+257
-14
@@ -596,9 +596,23 @@ impl OpenCADStudio {
|
||||
|
||||
"REVERSE" => {
|
||||
use crate::modules::draw::modify::reverse::ReverseCommand;
|
||||
let new_cmd = ReverseCommand::new();
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
if self.tabs[i].scene.selected.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
let selection = SelectObjectsCommand::new("REVERSE");
|
||||
self.command_line.push_info(&selection.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(selection));
|
||||
} else {
|
||||
let replacements = self.tabs[i].scene.selected_entities().into_iter()
|
||||
.filter(|(handle, _)| !self.tabs[i].scene.is_layer_locked(*handle))
|
||||
.filter_map(|(handle, entity)| ReverseCommand::reversed(entity)
|
||||
.map(|reversed| (handle, vec![reversed])))
|
||||
.collect::<Vec<_>>();
|
||||
if !replacements.is_empty() {
|
||||
return Some(self.apply_cmd_result(crate::command::CmdResult::ReplaceMany(
|
||||
replacements, Vec::new(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"MEASUREGEOM" => {
|
||||
@@ -881,7 +895,7 @@ impl OpenCADStudio {
|
||||
SplineCommand::control_vertices()
|
||||
} else {
|
||||
SplineCommand::new()
|
||||
};
|
||||
}.with_document(&self.tabs[i].scene.document);
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
@@ -963,6 +977,193 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Persistent constraints ────────────────────────────────────
|
||||
"HCONSTRAINT" | "VCONSTRAINT" | "FXCONSTRAINT" => {
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
if handles.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
let sel = SelectObjectsCommand::new(cmd);
|
||||
self.command_line.push_info(&sel.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(sel));
|
||||
} else if handles.len() != 1 {
|
||||
self.command_line
|
||||
.push_output("Select exactly one entity, then run this constraint again.");
|
||||
} else {
|
||||
use crate::command::CmdResult;
|
||||
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
|
||||
let (kind, label) = match cmd {
|
||||
"HCONSTRAINT" => (ConstraintKind::Horizontal, "Horizontal constraint"),
|
||||
"VCONSTRAINT" => (ConstraintKind::Vertical, "Vertical constraint"),
|
||||
_ => (ConstraintKind::Fixed, "Fixed constraint"),
|
||||
};
|
||||
return Some(self.apply_cmd_result(CmdResult::AddSketchConstraint {
|
||||
kind,
|
||||
refs: vec![SketchRef::whole(handles[0])],
|
||||
driving_param: None,
|
||||
label,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
"CCONSTRAINT" => {
|
||||
use crate::modules::draw::constrain::CoincidentConstraintCommand;
|
||||
let new_cmd = CoincidentConstraintCommand::new();
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
"EDCONSTRAINT" => {
|
||||
use crate::modules::draw::constrain::EqualDistanceConstraintCommand;
|
||||
let new_cmd = EqualDistanceConstraintCommand::new();
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
"PCONSTRAINT" | "QCONSTRAINT" | "ECONSTRAINT" | "TCONSTRAINT" | "LCONSTRAINT" | "NRCONSTRAINT" => {
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
if handles.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
let sel = SelectObjectsCommand::new(cmd);
|
||||
self.command_line.push_info(&sel.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(sel));
|
||||
} else if handles.len() != 2 {
|
||||
self.command_line.push_output(
|
||||
"Select exactly two entities (first = reference, second = the one that moves), then run this constraint again.",
|
||||
);
|
||||
} else {
|
||||
use crate::command::CmdResult;
|
||||
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
|
||||
let (kind, label) = match cmd {
|
||||
"PCONSTRAINT" => (ConstraintKind::Parallel, "Parallel constraint"),
|
||||
"QCONSTRAINT" => (ConstraintKind::Perpendicular, "Perpendicular constraint"),
|
||||
"TCONSTRAINT" => (ConstraintKind::Tangent, "Tangent constraint"),
|
||||
"LCONSTRAINT" => (ConstraintKind::Colinear, "Colinear constraint"),
|
||||
"NRCONSTRAINT" => (ConstraintKind::Normal, "Normal constraint"),
|
||||
_ => (ConstraintKind::Equal, "Equal constraint"),
|
||||
};
|
||||
return Some(self.apply_cmd_result(CmdResult::AddSketchConstraint {
|
||||
kind,
|
||||
refs: vec![SketchRef::whole(handles[0]), SketchRef::whole(handles[1])],
|
||||
driving_param: None,
|
||||
label,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
"NCONSTRAINT" => {
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
if handles.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
let sel = SelectObjectsCommand::new(cmd);
|
||||
self.command_line.push_info(&sel.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(sel));
|
||||
} else if handles.len() != 2 {
|
||||
self.command_line.push_output("Select exactly two circles/arcs, then run this constraint again.");
|
||||
} else {
|
||||
use crate::command::CmdResult;
|
||||
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
|
||||
return Some(self.apply_cmd_result(CmdResult::AddSketchConstraint {
|
||||
kind: ConstraintKind::Concentric,
|
||||
refs: vec![SketchRef::center(handles[0]), SketchRef::center(handles[1])],
|
||||
driving_param: None,
|
||||
label: "Concentric constraint",
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
"SYCONSTRAINT" => {
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
if handles.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
let sel = SelectObjectsCommand::new(cmd);
|
||||
self.command_line.push_info(&sel.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(sel));
|
||||
} else if handles.len() != 3 {
|
||||
self.command_line.push_output(
|
||||
"Select exactly three entities (two circles/arcs, then the mirror line), then run this constraint again.",
|
||||
);
|
||||
} else {
|
||||
use crate::command::CmdResult;
|
||||
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
|
||||
return Some(self.apply_cmd_result(CmdResult::AddSketchConstraint {
|
||||
kind: ConstraintKind::Symmetric,
|
||||
refs: vec![SketchRef::center(handles[0]), SketchRef::center(handles[1]), SketchRef::whole(handles[2])],
|
||||
driving_param: None,
|
||||
label: "Symmetric constraint",
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
"CPCONSTRAINT" | "MPCONSTRAINT" | "OCCONSTRAINT" => {
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
if handles.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
let sel = SelectObjectsCommand::new(cmd);
|
||||
self.command_line.push_info(&sel.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(sel));
|
||||
} else if handles.len() != 1 {
|
||||
self.command_line.push_output(
|
||||
"Select exactly one entity (the circle or line the point should attach to), then run this constraint again.",
|
||||
);
|
||||
} else {
|
||||
use crate::modules::draw::constrain::PointOnEntityConstraintCommand;
|
||||
use crate::scene::sketch_constraints::ConstraintKind;
|
||||
let (name, kind, label) = match cmd {
|
||||
"CPCONSTRAINT" => ("CPCONSTRAINT", ConstraintKind::CenterPoint, "Center point constraint"),
|
||||
"MPCONSTRAINT" => ("MPCONSTRAINT", ConstraintKind::Midpoint, "Midpoint constraint"),
|
||||
_ => ("OCCONSTRAINT", ConstraintKind::PointOnCurve, "Point on curve constraint"),
|
||||
};
|
||||
let new_cmd = PointOnEntityConstraintCommand::new(name, kind, handles[0], label);
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
}
|
||||
|
||||
"DCONSTRAINT" => {
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
if handles.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
let sel = SelectObjectsCommand::new(cmd);
|
||||
self.command_line.push_info(&sel.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(sel));
|
||||
} else if handles.len() != 1 {
|
||||
self.command_line
|
||||
.push_output("Select exactly one line or circle, then run this constraint again.");
|
||||
} else {
|
||||
use crate::modules::draw::constrain::DistanceConstraintCommand;
|
||||
match DistanceConstraintCommand::new(&self.tabs[i].scene, handles[0]) {
|
||||
Some(new_cmd) => {
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
None => self.command_line.push_output("Select a line or a circle for a distance constraint."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"ACONSTRAINT" => {
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
if handles.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
let sel = SelectObjectsCommand::new(cmd);
|
||||
self.command_line.push_info(&sel.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(sel));
|
||||
} else if handles.len() != 2 {
|
||||
self.command_line.push_output(
|
||||
"Select exactly two lines (first = reference, second = the one that rotates), then run this constraint again.",
|
||||
);
|
||||
} else {
|
||||
use crate::modules::draw::constrain::AngleConstraintCommand;
|
||||
match AngleConstraintCommand::new(&self.tabs[i].scene, handles[0], handles[1]) {
|
||||
Some(new_cmd) => {
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
None => self.command_line.push_output("Select two lines for an angle constraint."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Model commands (3D primitives) ─────────────────────────────
|
||||
"CYLINDER" => {
|
||||
use crate::modules::model::cylinder_cmd::CylinderCommand;
|
||||
@@ -1114,8 +1315,7 @@ impl OpenCADStudio {
|
||||
return Some(self.fit_spline());
|
||||
}
|
||||
|
||||
// REGION — convert selected closed boundaries (closed polylines /
|
||||
// circles) into Region entities (one wire loop each).
|
||||
// REGION — convert exact closed planar profiles into regions.
|
||||
"REGION" | "REG" => {
|
||||
if self.tabs[i].scene.selected_entities().is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
@@ -1129,12 +1329,7 @@ impl OpenCADStudio {
|
||||
let mut regions = Vec::new();
|
||||
let mut sources = Vec::new();
|
||||
for (handle, e) in self.tabs[i].scene.selected_entities().iter() {
|
||||
let supported = matches!(
|
||||
e,
|
||||
acadrust::EntityType::LwPolyline(pl)
|
||||
if pl.is_closed && pl.vertices.len() >= 3
|
||||
) || matches!(e, acadrust::EntityType::Circle(_));
|
||||
if supported {
|
||||
if !matches!(e, acadrust::EntityType::Region(_)) {
|
||||
let Some((plane, loops, true)) =
|
||||
crate::scene::model::presspull_model::profile_geometry(e)
|
||||
else {
|
||||
@@ -1154,9 +1349,55 @@ impl OpenCADStudio {
|
||||
sources.push(*handle);
|
||||
}
|
||||
}
|
||||
// Open inputs must share one geometric plane, independently of the UCS.
|
||||
let open_curves: Vec<_> = self.tabs[i].scene.selected_entities().iter()
|
||||
.filter(|(handle, _)| !sources.contains(handle))
|
||||
.filter_map(|(_, entity)| crate::entities::curve::entity_curve(entity))
|
||||
.collect();
|
||||
let open_plane = cadkernel::space::common_curve_plane(&open_curves, 1.0e-6);
|
||||
let open_plane_rejected = !open_curves.is_empty() && open_plane.is_none();
|
||||
if let Some(plane) = open_plane {
|
||||
let working_plane = crate::command::WorkingPlane::new(
|
||||
glam::DVec3::from_array(plane.origin),
|
||||
glam::DVec3::from_array(plane.x_axis),
|
||||
glam::DVec3::from_array(plane.y_axis),
|
||||
);
|
||||
let selected_handles: rustc_hash::FxHashSet<_> = self.tabs[i].scene
|
||||
.selected_entities().iter().map(|(handle, _)| *handle).collect();
|
||||
let mut boundary_sources = self.tabs[i].scene
|
||||
.boundary_sources_on_plane(working_plane, 1.0e-6);
|
||||
boundary_sources.retain(|handle, _| {
|
||||
selected_handles.contains(handle) && !sources.contains(handle)
|
||||
});
|
||||
for ring in crate::scene::boundary_faces(&boundary_sources, 1.0e-6) {
|
||||
let paths = crate::scene::exact_hatch_paths(
|
||||
std::slice::from_ref(&ring), &[true], &boundary_sources, 1.0e-6,
|
||||
);
|
||||
let Some(path) = paths.first() else { continue; };
|
||||
let Some(curves) = path.edges.iter()
|
||||
.map(crate::entities::hatch::edge_curve).collect::<Option<Vec<_>>>()
|
||||
else { continue; };
|
||||
let Some(body) = cadkernel::brep::planar_region(plane, &[curves])
|
||||
else { continue; };
|
||||
let mut region = Region::new();
|
||||
region.point_of_reference = Vector3::new(
|
||||
plane.origin[0], plane.origin[1], plane.origin[2],
|
||||
);
|
||||
region.common.layer = self.tabs[i].active_layer.clone();
|
||||
regions.push((region, body));
|
||||
sources.extend(crate::scene::ring_source_handles(&ring, &boundary_sources));
|
||||
}
|
||||
} else if open_plane_rejected {
|
||||
self.command_line.push_error(
|
||||
"REGION: open objects must form coplanar, noncollinear boundaries.",
|
||||
);
|
||||
}
|
||||
if regions.is_empty() {
|
||||
self.command_line
|
||||
.push_error(crate::t!("REGION: select closed polylines or circles.").as_ref());
|
||||
if !open_plane_rejected {
|
||||
self.command_line.push_error(
|
||||
"REGION: select closed planar profiles or connected coplanar edges.",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
self.push_undo_snapshot(i, "REGION");
|
||||
let count = regions.len();
|
||||
@@ -1171,6 +1412,8 @@ impl OpenCADStudio {
|
||||
created.push(handle);
|
||||
}
|
||||
if self.delete_objects != 0 {
|
||||
sources.sort_by_key(|handle| handle.value());
|
||||
sources.dedup();
|
||||
self.tabs[i].scene.erase_entities(&sources);
|
||||
self.refresh_properties();
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -322,6 +322,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"CLEANSCREEN",
|
||||
"CUI",
|
||||
"DSETTINGS",
|
||||
"PARAMETERS",
|
||||
"GRID",
|
||||
"ISODRAFT",
|
||||
"ISOPLANE",
|
||||
@@ -598,6 +599,10 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"DIMSTYLE",
|
||||
"DONATE",
|
||||
"DRAWORDER",
|
||||
"DRAWORDER_FRONT",
|
||||
"DRAWORDER_BACK",
|
||||
"DRAWORDER_ABOVE",
|
||||
"DRAWORDER_UNDER",
|
||||
"DWGPROP",
|
||||
"DWGPROPS",
|
||||
"EATTEXT",
|
||||
|
||||
+200
-63
@@ -645,98 +645,221 @@ impl OpenCADStudio {
|
||||
// ── SETBYLAYER — clear color/linetype/lineweight overrides ────
|
||||
// Resets the selected entities' direct property overrides back to
|
||||
// ByLayer so they follow their layer again.
|
||||
"SETBYLAYERMODE" => {
|
||||
let command = crate::modules::draw::modify::setbylayer::ModeCommand;
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
value if value.starts_with("SETBYLAYERMODE ") => {
|
||||
if let Ok(mode) = value.trim_start_matches("SETBYLAYERMODE ").trim().parse::<u8>() {
|
||||
crate::modules::draw::modify::setbylayer::set_mode(mode);
|
||||
} else { self.command_line.push_error("SETBYLAYERMODE requires an integer from 0 to 255."); }
|
||||
}
|
||||
"SETBYLAYER" => {
|
||||
let handles: Vec<_> = self.tabs[i]
|
||||
let command = crate::modules::draw::modify::setbylayer::SetByLayerCommand::new(
|
||||
self.tabs[i].scene.selected.iter().copied().collect(),
|
||||
);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
cmd if cmd.starts_with("SETBYLAYER_APPLY ") => {
|
||||
let flags: Vec<_> = cmd.split_whitespace().skip(1).collect();
|
||||
if flags.len() != 2 || flags.iter().any(|flag| !matches!(*flag, "0" | "1")) {
|
||||
return Some(Task::none());
|
||||
}
|
||||
let mask = crate::modules::draw::modify::setbylayer::mode();
|
||||
let change_byblock = flags[0] == "1";
|
||||
let include_blocks = flags[1] == "1";
|
||||
let mut handles: Vec<_> = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
.into_iter()
|
||||
.map(|(h, _)| h)
|
||||
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
|
||||
.collect();
|
||||
if include_blocks {
|
||||
let mut visited: std::collections::HashSet<_> = handles.iter().copied().collect();
|
||||
let mut index = 0;
|
||||
while index < handles.len() {
|
||||
let children = match self.tabs[i].scene.document.get_entity(handles[index]) {
|
||||
Some(acadrust::EntityType::Insert(insert)) => self.tabs[i].scene.document
|
||||
.block_records.get(&insert.block_name)
|
||||
.map(|block| block.entity_handles.clone()).unwrap_or_default(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
for child in children {
|
||||
if visited.insert(child) && !self.tabs[i].scene.is_layer_locked(child) {
|
||||
handles.push(child);
|
||||
}
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
if handles.is_empty() {
|
||||
self.command_line
|
||||
.push_error(crate::t!("SETBYLAYER: select entities first.").as_ref());
|
||||
} else {
|
||||
let has_changes = handles.iter().any(|handle| {
|
||||
self.tabs[i].scene.document.get_entity(*handle).is_some_and(|entity| {
|
||||
let mut common = entity.common().clone();
|
||||
crate::modules::draw::modify::setbylayer::apply_mask(
|
||||
&mut common, mask, change_byblock,
|
||||
)
|
||||
})
|
||||
});
|
||||
if !has_changes {
|
||||
self.command_line.push_output(
|
||||
crate::t!("SETBYLAYER: no properties required changes.").as_ref(),
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
self.push_undo_snapshot(i, "SETBYLAYER");
|
||||
let mut changed = 0usize;
|
||||
let mut changed = Vec::new();
|
||||
for handle in &handles {
|
||||
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(*handle) {
|
||||
let common = entity.common_mut();
|
||||
common.color = acadrust::types::Color::ByLayer;
|
||||
common.color_name = None;
|
||||
common.color_book_handle = None;
|
||||
common.linetype = "ByLayer".to_string();
|
||||
common.line_weight = acadrust::types::LineWeight::ByLayer;
|
||||
changed += 1;
|
||||
if crate::modules::draw::modify::setbylayer::apply_mask(
|
||||
entity.common_mut(), mask, change_byblock,
|
||||
) {
|
||||
changed.push(*handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
let changes: Vec<_> = handles
|
||||
.into_iter()
|
||||
let changes: Vec<_> = changed
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
self.refresh_properties();
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"SETBYLAYER: reset {changed} entity/entities to ByLayer."
|
||||
"SETBYLAYER: reset {} entity/entities to ByLayer.",
|
||||
changed.len()
|
||||
).as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
// ── OVERKILL — delete duplicate (identical) objects ──────────
|
||||
// Removes objects that are identical in geometry AND properties to
|
||||
// another object (compared with the handle ignored). Operates on
|
||||
// the current selection, or the whole drawing when nothing is
|
||||
// selected. Conservative: only exact duplicates are removed.
|
||||
"OVERKILL" => {
|
||||
use acadrust::Handle;
|
||||
let selected: std::collections::HashSet<u64> = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
.into_iter()
|
||||
.map(|(h, _)| h.value())
|
||||
.collect();
|
||||
// Capture (handle, type-name, handle-normalized clone) for each
|
||||
// candidate while the document is borrowed immutably.
|
||||
let candidates: Vec<(Handle, String, acadrust::EntityType)> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.filter(|e| {
|
||||
(selected.is_empty() || selected.contains(&e.common().handle.value()))
|
||||
&& !self.tabs[i].scene.is_layer_locked(e.common().handle)
|
||||
})
|
||||
.map(|e| {
|
||||
let key = crate::entities::names::dxf_name(e).to_string();
|
||||
let mut norm = e.clone();
|
||||
norm.common_mut().handle = Handle::NULL;
|
||||
(e.common().handle, key, norm)
|
||||
})
|
||||
.collect();
|
||||
// Bucket by (type, layer) so only like objects are compared.
|
||||
let mut kept: Vec<(String, acadrust::EntityType)> = Vec::new();
|
||||
let mut dups: Vec<Handle> = Vec::new();
|
||||
for (h, key, norm) in &candidates {
|
||||
let bucket = format!("{key}\u{0}{}", norm.common().layer);
|
||||
if kept.iter().any(|(b, e)| b == &bucket && e == norm) {
|
||||
dups.push(*h);
|
||||
} else {
|
||||
kept.push((bucket, norm.clone()));
|
||||
// OVERKILL gathers objects, exposes cleanup settings, then applies one undo group.
|
||||
"OVERKILL" | "-OVERKILL" => {
|
||||
let handles=self.tabs[i].scene.selected_entities().iter().map(|(h,_)|*h).collect();
|
||||
let command=crate::modules::draw::modify::overkill::OverkillCommand::new(handles);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd=Some(Box::new(command));
|
||||
}
|
||||
value if value.starts_with("OVERKILL_APPLY ") => {
|
||||
use crate::modules::draw::modify::overkill::{normalized,optimize};
|
||||
let values:Vec<_>=value.split_whitespace().skip(1).collect();
|
||||
if values.len()!=6 { return Some(self.finish_dispatch(cmd)); }
|
||||
let tolerance=values[0].parse::<f64>().unwrap_or(1e-6);
|
||||
let ignore=values[1].parse::<u16>().unwrap_or(0);
|
||||
let optimize_plines=values[2]=="1";
|
||||
let overlap=values[3]=="1";
|
||||
let end_to_end=values[4]=="1";
|
||||
let preserve_associative=values[5]=="1";
|
||||
let mut candidates:Vec<_>=self.tabs[i].scene.selected_entities().into_iter()
|
||||
.filter(|(h,e)|!self.tabs[i].scene.is_layer_locked(*h)
|
||||
&& (!preserve_associative||e.common().reactors.is_empty()))
|
||||
.map(|(h,e)|(h,e.clone())).collect();
|
||||
candidates.sort_by_key(|(h,_)|h.value());
|
||||
let mut changed=std::collections::HashSet::new();
|
||||
if optimize_plines { for (h,e) in &mut candidates {
|
||||
let before=e.clone(); optimize(e,tolerance); if *e!=before {changed.insert(*h);}
|
||||
} }
|
||||
let mut removed=std::collections::HashSet::new();
|
||||
// Iterate to a fixed point: a bridge can join two previously disjoint intervals.
|
||||
loop {
|
||||
let mut progress=false;
|
||||
for a in 0..candidates.len() {
|
||||
if removed.contains(&candidates[a].0) {continue;}
|
||||
for b in a+1..candidates.len() {
|
||||
if removed.contains(&candidates[b].0) {continue;}
|
||||
let left=normalized(&candidates[a].1,ignore);
|
||||
let right=normalized(&candidates[b].1,ignore);
|
||||
if left==right {
|
||||
removed.insert(candidates[b].0);progress=true;continue;
|
||||
}
|
||||
if overlap {
|
||||
let contained = |circle: &acadrust::entities::Circle, arc: &acadrust::entities::Arc| {
|
||||
circle.common == arc.common && circle.thickness == arc.thickness
|
||||
&& cadkernel::space::arc_union::circle_contains_arc(
|
||||
[circle.center.x,circle.center.y,circle.center.z],
|
||||
[circle.normal.x,circle.normal.y,circle.normal.z],circle.radius,
|
||||
cadkernel::space::arc_union::CircularArc { center:[arc.center.x,arc.center.y,arc.center.z],
|
||||
normal:[arc.normal.x,arc.normal.y,arc.normal.z],radius:arc.radius,start:arc.start_angle,end:arc.end_angle })
|
||||
};
|
||||
match (&left,&right) {
|
||||
(acadrust::EntityType::Circle(circle),acadrust::EntityType::Arc(arc)) if contained(circle,arc) => {
|
||||
removed.insert(candidates[b].0);progress=true;continue;
|
||||
}
|
||||
(acadrust::EntityType::Arc(arc),acadrust::EntityType::Circle(circle)) if contained(circle,arc) => {
|
||||
removed.insert(candidates[a].0);progress=true;break;
|
||||
}
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
if let (acadrust::EntityType::Arc(l),acadrust::EntityType::Arc(r))=(&left,&right) {
|
||||
use cadkernel::space::arc_union::{CircularArc,ArcUnionKind,circular_arc_union};
|
||||
if l.common!=r.common || l.thickness!=r.thickness {continue;}
|
||||
let arc=|v:&acadrust::entities::Arc|CircularArc{center:[v.center.x,v.center.y,v.center.z],normal:[v.normal.x,v.normal.y,v.normal.z],radius:v.radius,start:v.start_angle,end:v.end_angle};
|
||||
let Some(union)=circular_arc_union(arc(l),arc(r),tolerance) else {continue;};
|
||||
let allowed=match union.kind {ArcUnionKind::Duplicate=>true,ArcUnionKind::Overlap=>overlap,ArcUnionKind::EndToEnd=>end_to_end};
|
||||
if !allowed {continue;}
|
||||
if let acadrust::EntityType::Arc(source)=&candidates[a].1 {
|
||||
let replacement=if union.full_circle {
|
||||
let mut circle=acadrust::entities::Circle::new();
|
||||
circle.common=source.common.clone();circle.center=source.center.clone();circle.normal=source.normal.clone();circle.radius=source.radius;circle.thickness=source.thickness;
|
||||
acadrust::EntityType::Circle(circle)
|
||||
} else {
|
||||
let mut arc=source.clone();arc.start_angle=union.start;arc.end_angle=union.end;acadrust::EntityType::Arc(arc)
|
||||
};
|
||||
candidates[a].1=replacement;
|
||||
}
|
||||
changed.insert(candidates[a].0);removed.insert(candidates[b].0);progress=true;
|
||||
continue;
|
||||
}
|
||||
let (acadrust::EntityType::Line(l),acadrust::EntityType::Line(r))=(&left,&right)
|
||||
else {continue;};
|
||||
if l.common!=r.common||l.thickness!=r.thickness||l.normal!=r.normal {continue;}
|
||||
let point=|p:acadrust::types::Vector3|[p.x,p.y,p.z];
|
||||
let Some(union)=cadkernel::space::line_union(
|
||||
[point(l.start),point(l.end)],[point(r.start),point(r.end)],tolerance)
|
||||
else {continue;};
|
||||
let allowed=match union.kind {
|
||||
cadkernel::space::LineUnionKind::Duplicate=>true,
|
||||
cadkernel::space::LineUnionKind::Overlap=>overlap,
|
||||
cadkernel::space::LineUnionKind::EndToEnd=>end_to_end,
|
||||
};
|
||||
if !allowed {continue;}
|
||||
if let acadrust::EntityType::Line(line)=&mut candidates[a].1 {
|
||||
line.start=acadrust::types::Vector3::new(union.start[0],union.start[1],union.start[2]);
|
||||
line.end=acadrust::types::Vector3::new(union.end[0],union.end[1],union.end[2]);
|
||||
}
|
||||
changed.insert(candidates[a].0);removed.insert(candidates[b].0);progress=true;
|
||||
}
|
||||
}
|
||||
if !progress {break;}
|
||||
}
|
||||
if dups.is_empty() {
|
||||
self.command_line
|
||||
.push_output(crate::t!("OVERKILL: no duplicate objects found.").as_ref());
|
||||
if !changed.is_empty()||!removed.is_empty() {
|
||||
self.push_undo_snapshot(i,"OVERKILL");
|
||||
let updates:Vec<_>=candidates.into_iter().filter(|(h,_)|changed.contains(h)&&!removed.contains(h)).collect();
|
||||
let updated_count = updates.len();
|
||||
let mut changes = Vec::new();
|
||||
for (handle, entity) in updates {
|
||||
if let Some(target) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
||||
*target = entity;
|
||||
changes.push((handle, crate::scene::ChangeKind::Modified));
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
let mut handles:Vec<_>=removed.iter().copied().collect();
|
||||
handles.sort_by_key(|handle| handle.value());
|
||||
self.tabs[i].scene.erase_entities(&handles);
|
||||
self.tabs[i].dirty=true;self.refresh_properties();
|
||||
self.command_line.push_output(&format!("OVERKILL: removed {} objects; updated {} objects.",removed.len(),updated_count));
|
||||
} else {
|
||||
let n = dups.len();
|
||||
self.push_undo_snapshot(i, "OVERKILL");
|
||||
self.tabs[i].scene.erase_entities(&dups);
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
self.command_line
|
||||
.push_output(crate::tf!("OVERKILL: deleted {n} duplicate object(s).").as_ref());
|
||||
self.command_line.push_output("OVERKILL: no objects required changes.");
|
||||
}
|
||||
}
|
||||
|
||||
// ── PICKADD / PICKDRAG — selection UX (#226, app settings) ───
|
||||
// Bare form reports; `<name> 0|1` sets and persists. Defaults keep
|
||||
// today's behaviour (PICKADD 1, PICKDRAG 0).
|
||||
@@ -2983,6 +3106,21 @@ mod tests {
|
||||
app
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_commands_gather_instead_of_using_the_whole_drawing() {
|
||||
for command in ["SETBYLAYER", "OVERKILL"] {
|
||||
let mut app = fresh_app();
|
||||
let _ = app.run_command_line(command);
|
||||
assert!(
|
||||
app.tabs[app.active_tab]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.is_some_and(|active| active.is_selection_gathering()),
|
||||
"{command} should gather an explicit selection"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cad_selection_and_model_space_sysvars() {
|
||||
let mut app = fresh_app();
|
||||
@@ -3195,4 +3333,3 @@ mod scale_validation_tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+335
-143
@@ -1,5 +1,62 @@
|
||||
use super::*;
|
||||
|
||||
// Sort tables belong to the block's extension dictionary. Reserve real handles
|
||||
// and reconnect older unattached tables without losing their existing entries.
|
||||
fn ensure_draw_order_table(
|
||||
doc: &mut acadrust::CadDocument,
|
||||
block: acadrust::Handle,
|
||||
) -> acadrust::Handle {
|
||||
use acadrust::objects::{Dictionary, ObjectType, SortEntitiesTable};
|
||||
|
||||
// Older commands inserted objects using the unreserved next-handle value.
|
||||
// Repair the allocator floor before creating anything beside those objects.
|
||||
if let Some(maximum) = doc.objects.keys().map(|handle| handle.value()).max() {
|
||||
doc.header.handle_seed = doc.header.handle_seed.max(maximum.saturating_add(1));
|
||||
}
|
||||
let dictionary = doc.extension_dictionary_handle(block).filter(|handle| {
|
||||
matches!(doc.objects.get(handle), Some(ObjectType::Dictionary(_)))
|
||||
});
|
||||
let named_table = dictionary.and_then(|handle| match doc.objects.get(&handle) {
|
||||
Some(ObjectType::Dictionary(value)) => value.get(SortEntitiesTable::DICTIONARY_KEY),
|
||||
_ => None,
|
||||
}).filter(|handle| matches!(doc.objects.get(handle),
|
||||
Some(ObjectType::SortEntitiesTable(table)) if table.block_owner_handle == block));
|
||||
let existing = named_table.or_else(|| doc.objects.iter().find_map(|(handle, object)| {
|
||||
match object {
|
||||
ObjectType::SortEntitiesTable(table) if table.block_owner_handle == block => Some(*handle),
|
||||
_ => None,
|
||||
}
|
||||
}));
|
||||
let dictionary = dictionary.unwrap_or_else(|| {
|
||||
let handle = doc.allocate_handle();
|
||||
let mut value = Dictionary::new();
|
||||
value.handle = handle;
|
||||
value.owner = block;
|
||||
value.hard_owner = true;
|
||||
doc.objects.insert(handle, ObjectType::Dictionary(value));
|
||||
handle
|
||||
});
|
||||
let handle = existing.unwrap_or_else(|| {
|
||||
let handle = doc.allocate_handle();
|
||||
let mut table = SortEntitiesTable::for_block(block);
|
||||
table.handle = handle;
|
||||
doc.objects.insert(handle, ObjectType::SortEntitiesTable(table));
|
||||
handle
|
||||
});
|
||||
if let Some(ObjectType::SortEntitiesTable(table)) = doc.objects.get_mut(&handle) {
|
||||
table.owner_handle = dictionary;
|
||||
}
|
||||
if let Some(ObjectType::Dictionary(value)) = doc.objects.get_mut(&dictionary) {
|
||||
if let Some((_, entry)) = value.entries.iter_mut()
|
||||
.find(|(name, _)| name.eq_ignore_ascii_case(SortEntitiesTable::DICTIONARY_KEY)) {
|
||||
*entry = handle;
|
||||
} else {
|
||||
value.add_entry(SortEntitiesTable::DICTIONARY_KEY, handle);
|
||||
}
|
||||
}
|
||||
handle
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(crate) fn dispatch_view(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||
match cmd {
|
||||
@@ -253,6 +310,10 @@ impl OpenCADStudio {
|
||||
return Some(Task::done(Message::OptionsOpen));
|
||||
}
|
||||
|
||||
"PARAMETERS" => {
|
||||
return Some(Task::done(Message::NamedParametersOpen));
|
||||
}
|
||||
|
||||
// CUIEXPORT <path> — write the keyboard-shortcut customizations
|
||||
// (the drawing-independent CUI data) to a plain "KEY COMMAND" file.
|
||||
"CUIEXPORT" => {
|
||||
@@ -817,17 +878,15 @@ impl OpenCADStudio {
|
||||
|
||||
// HATCHTOBACK ÔÇö move every hatch object in the active space to the back of the draw order.
|
||||
"HATCHTOBACK" => {
|
||||
use acadrust::objects::{ObjectType, SortEntitiesTable};
|
||||
use acadrust::objects::ObjectType;
|
||||
let block_handle = self.tabs[i].scene.current_layout_block_handle_pub();
|
||||
let doc_ref = &self.tabs[i].scene.document;
|
||||
|
||||
// 1. Single scan over objects to find existing SortEntitiesTable handle & overrides.
|
||||
let mut existing_table_handle = None;
|
||||
// 1. Read existing SortEntitiesTable overrides.
|
||||
let mut overrides: Option<rustc_hash::FxHashMap<u64, u64>> = None;
|
||||
for (h, obj) in &doc_ref.objects {
|
||||
for obj in doc_ref.objects.values() {
|
||||
if let ObjectType::SortEntitiesTable(t) = obj {
|
||||
if t.block_owner_handle == block_handle {
|
||||
existing_table_handle = Some(*h);
|
||||
if !t.is_empty() {
|
||||
overrides = Some(
|
||||
t.entries()
|
||||
@@ -894,24 +953,9 @@ impl OpenCADStudio {
|
||||
&locked_layers,
|
||||
);
|
||||
|
||||
// 4. Ultra-fast targeted Delta Undo (snapshots ONLY the SortEntitiesTable, zero full-drawing clone).
|
||||
let pending_delta = self.begin_undo(i, "DRAWORDER", hatches_to_move.len(), true);
|
||||
|
||||
// 5. Update or insert SortEntitiesTable directly.
|
||||
let table_before = existing_table_handle
|
||||
.and_then(|h| self.tabs[i].scene.document.objects.get(&h).cloned());
|
||||
let th = existing_table_handle.unwrap_or_else(|| {
|
||||
let nh = acadrust::Handle::new(self.tabs[i].scene.document.next_handle());
|
||||
let mut table = SortEntitiesTable::for_block(block_handle);
|
||||
table.handle = nh;
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.insert(nh, ObjectType::SortEntitiesTable(table));
|
||||
nh
|
||||
});
|
||||
self.tabs[i].scene.record_undo_object_before(th, table_before);
|
||||
// Dictionary ownership and handle allocation are part of this undo step.
|
||||
let pending_delta = self.begin_undo(i, "DRAWORDER", hatches_to_move.len(), false);
|
||||
let th = ensure_draw_order_table(&mut self.tabs[i].scene.document, block_handle);
|
||||
|
||||
if let Some(ObjectType::SortEntitiesTable(table)) =
|
||||
self.tabs[i].scene.document.objects.get_mut(&th)
|
||||
@@ -941,6 +985,37 @@ impl OpenCADStudio {
|
||||
return Some(Task::none());
|
||||
}
|
||||
|
||||
"DRAWORDER_FRONT" | "DRAWORDER_BACK" | "DRAWORDER_ABOVE" | "DRAWORDER_UNDER" => {
|
||||
let selected: Vec<acadrust::Handle> = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
.iter()
|
||||
.map(|(h, _)| *h)
|
||||
.collect();
|
||||
if selected.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
let selection = SelectObjectsCommand::plain("DRAWORDER", cmd);
|
||||
self.command_line.push_info(&selection.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(selection));
|
||||
} else if matches!(cmd, "DRAWORDER_ABOVE" | "DRAWORDER_UNDER") {
|
||||
let command = DrawOrderCommand::for_reference_pick(
|
||||
selected,
|
||||
cmd == "DRAWORDER_ABOVE",
|
||||
);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
} else {
|
||||
let primitive = if cmd == "DRAWORDER_FRONT" {
|
||||
"DRAWORDER FRONT"
|
||||
} else {
|
||||
"DRAWORDER BACK"
|
||||
};
|
||||
return Some(self.apply_cmd_result(crate::command::CmdResult::Relaunch(
|
||||
primitive.to_string(),
|
||||
selected,
|
||||
)));
|
||||
}
|
||||
}
|
||||
"DRAWORDER" => {
|
||||
let selected: Vec<acadrust::Handle> = self.tabs[i]
|
||||
.scene
|
||||
@@ -953,7 +1028,7 @@ impl OpenCADStudio {
|
||||
self.tabs[i].active_cmd = Some(Box::new(c));
|
||||
}
|
||||
cmd if cmd.starts_with("DRAWORDER ") => {
|
||||
use acadrust::objects::{ObjectType, SortEntitiesTable};
|
||||
use acadrust::objects::ObjectType;
|
||||
let parts: Vec<&str> = cmd.split_whitespace().collect();
|
||||
let option = parts.get(1).unwrap_or(&"").to_uppercase();
|
||||
let i = self.active_tab;
|
||||
@@ -968,25 +1043,29 @@ impl OpenCADStudio {
|
||||
self.command_line
|
||||
.push_error(crate::t!("DRAWORDER: select entities first.").as_ref());
|
||||
} else {
|
||||
// Parse relative target handle for ABOVE/UNDER.
|
||||
let relative_target: Option<(bool, acadrust::Handle)> = match option.as_str() {
|
||||
"A" | "ABOVE" => {
|
||||
let h_val = parts.get(2).and_then(|s| u64::from_str_radix(s, 16).ok());
|
||||
h_val.map(|v| (true, acadrust::Handle::new(v)))
|
||||
}
|
||||
"U" | "UNDER" | "BELOW" => {
|
||||
let h_val = parts.get(2).and_then(|s| u64::from_str_radix(s, 16).ok());
|
||||
h_val.map(|v| (false, acadrust::Handle::new(v)))
|
||||
}
|
||||
let relative_above = match option.as_str() {
|
||||
"A" | "ABOVE" => Some(true),
|
||||
"U" | "UNDER" | "BELOW" => Some(false),
|
||||
_ => None,
|
||||
};
|
||||
let references: Vec<_> = parts.iter().skip(2).filter_map(|text| {
|
||||
u64::from_str_radix(text.trim_start_matches("0x").trim_start_matches("0X"), 16)
|
||||
.ok().map(acadrust::Handle::new)
|
||||
}).filter(|handle| !selected.contains(handle)).collect();
|
||||
let relative_assignments = relative_above.and_then(|above| {
|
||||
assign_relative_group_keys(
|
||||
&self.tabs[i].scene.document,
|
||||
self.tabs[i].scene.current_layout_block_handle_pub(),
|
||||
&selected, &references, above,
|
||||
)
|
||||
});
|
||||
let to_front_opt = match option.as_str() {
|
||||
"F" | "FRONT" => Some(true),
|
||||
"B" | "BACK" => Some(false),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if relative_target.is_some() || to_front_opt.is_some() {
|
||||
if relative_assignments.is_some() || to_front_opt.is_some() {
|
||||
self.push_undo_snapshot(i, "DRAWORDER");
|
||||
let block_handle = self.tabs[i].scene.current_layout_block_handle_pub();
|
||||
|
||||
@@ -1063,63 +1142,14 @@ impl OpenCADStudio {
|
||||
};
|
||||
|
||||
let doc = &mut self.tabs[i].scene.document;
|
||||
let table_handle = doc.objects.iter().find_map(|(h, obj)| {
|
||||
if let ObjectType::SortEntitiesTable(t) = obj {
|
||||
if t.block_owner_handle == block_handle {
|
||||
Some(*h)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let th = table_handle.unwrap_or_else(|| {
|
||||
let nh = acadrust::Handle::new(doc.next_handle());
|
||||
let mut table = SortEntitiesTable::for_block(block_handle);
|
||||
table.handle = nh;
|
||||
doc.objects.insert(nh, ObjectType::SortEntitiesTable(table));
|
||||
nh
|
||||
});
|
||||
let th = ensure_draw_order_table(doc, block_handle);
|
||||
if let Some(ObjectType::SortEntitiesTable(table)) = doc.objects.get_mut(&th)
|
||||
{
|
||||
if let Some((above, target)) = relative_target {
|
||||
// move_above/move_below recompute target±1 per
|
||||
// call, so looping them over N selected entities
|
||||
// ties the whole group onto one key. Read the
|
||||
// reference key once and hand out per-index
|
||||
// keys instead, keeping the moved entities a
|
||||
// distinct, selection-ordered block adjacent to
|
||||
// the reference.
|
||||
let target_sort = match table.get_sort_handle(target) {
|
||||
Some(h) => h.value(),
|
||||
None => {
|
||||
// A reference object that was never
|
||||
// reordered isn't in the table yet;
|
||||
// seed it with its own handle as the
|
||||
// implicit sort key.
|
||||
table.add_entry(target, target);
|
||||
table
|
||||
.get_sort_handle(target)
|
||||
.map_or(target.value(), |h| h.value())
|
||||
}
|
||||
};
|
||||
for (k, h) in selected.iter().enumerate() {
|
||||
let offset = 1 + k as u64;
|
||||
let sort = if above {
|
||||
target_sort.saturating_add(offset)
|
||||
} else {
|
||||
target_sort.saturating_sub(offset).max(1)
|
||||
};
|
||||
table.add_entry(*h, acadrust::Handle::new(sort));
|
||||
if let Some(assignments) = &relative_assignments {
|
||||
for (handle, sort) in assignments {
|
||||
table.add_entry(*handle, acadrust::Handle::new(*sort));
|
||||
}
|
||||
let rel = if above { "above" } else { "below" };
|
||||
self.command_line.push_info(crate::tf!(
|
||||
"DRAWORDER: moved {} entities {} {:x}.",
|
||||
selected.len(),
|
||||
rel,
|
||||
target.value()
|
||||
).as_ref());
|
||||
self.command_line.push_info(crate::t!("DRAWORDER: selection reordered relative to reference objects.").as_ref());
|
||||
} else if let Some(to_front) = to_front_opt {
|
||||
if to_front {
|
||||
let (_, max_eff) = fb_baseline.unwrap_or((1, 0));
|
||||
@@ -1250,11 +1280,12 @@ enum DrawOrderStep {
|
||||
/// - `Front` / `F`: moves selection to front.
|
||||
/// - `Back` / `B` / Enter: moves selection to back.
|
||||
/// - `Above` / `A` / `Under` / `U`: advances to reference object pick.
|
||||
/// 3. Reference object pick: user can click the reference entity in the viewport
|
||||
/// or type its hex handle on the command line.
|
||||
/// 3. Gather reference objects, excluding the moved selection; Enter applies.
|
||||
/// Typed hexadecimal handles may also be accumulated before confirming.
|
||||
pub(crate) struct DrawOrderCommand {
|
||||
selected: Vec<acadrust::Handle>,
|
||||
step: DrawOrderStep,
|
||||
references: Vec<acadrust::Handle>,
|
||||
}
|
||||
|
||||
impl DrawOrderCommand {
|
||||
@@ -1264,13 +1295,14 @@ impl DrawOrderCommand {
|
||||
} else {
|
||||
DrawOrderStep::ChooseVerb
|
||||
};
|
||||
Self { selected, step }
|
||||
Self { selected, step, references: Vec::new() }
|
||||
}
|
||||
|
||||
pub(crate) fn for_reference_pick(selected: Vec<acadrust::Handle>, above: bool) -> Self {
|
||||
Self {
|
||||
selected,
|
||||
step: DrawOrderStep::PickReference { above },
|
||||
references: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1289,10 +1321,10 @@ impl CadCommand for DrawOrderCommand {
|
||||
crate::t!("DRAWORDER [Above / Under / Front / Back] <Back>:").into_owned()
|
||||
}
|
||||
DrawOrderStep::PickReference { above: true } => {
|
||||
crate::t!("DRAWORDER Select reference object (move selection above):").into_owned()
|
||||
format!("DRAWORDER Select reference objects to move above ({} selected, Enter when done):", self.references.len())
|
||||
}
|
||||
DrawOrderStep::PickReference { above: false } => {
|
||||
crate::t!("DRAWORDER Select reference object (move selection under):").into_owned()
|
||||
format!("DRAWORDER Select reference objects to move under ({} selected, Enter when done):", self.references.len())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1310,7 +1342,7 @@ impl CadCommand for DrawOrderCommand {
|
||||
}
|
||||
|
||||
fn input_kind(&self) -> crate::command::InputKind {
|
||||
if matches!(self.step, DrawOrderStep::SelectObjects) {
|
||||
if matches!(self.step, DrawOrderStep::SelectObjects | DrawOrderStep::PickReference { .. }) {
|
||||
crate::command::InputKind::Point
|
||||
} else {
|
||||
crate::command::InputKind::SingleToken
|
||||
@@ -1318,11 +1350,15 @@ impl CadCommand for DrawOrderCommand {
|
||||
}
|
||||
|
||||
fn is_selection_gathering(&self) -> bool {
|
||||
matches!(self.step, DrawOrderStep::SelectObjects)
|
||||
matches!(self.step, DrawOrderStep::SelectObjects | DrawOrderStep::PickReference { .. })
|
||||
}
|
||||
|
||||
fn on_selection_complete(&mut self, handles: Vec<acadrust::Handle>) -> crate::command::CmdResult {
|
||||
self.selected = handles;
|
||||
if matches!(self.step, DrawOrderStep::PickReference { .. }) {
|
||||
self.references = handles.into_iter().filter(|handle| !self.selected.contains(handle)).collect();
|
||||
} else {
|
||||
self.selected = handles;
|
||||
}
|
||||
crate::command::CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
@@ -1341,7 +1377,12 @@ impl CadCommand for DrawOrderCommand {
|
||||
let handles = std::mem::take(&mut self.selected);
|
||||
crate::command::CmdResult::Relaunch("DRAWORDER BACK".into(), handles)
|
||||
}
|
||||
DrawOrderStep::PickReference { .. } => crate::command::CmdResult::Cancel,
|
||||
DrawOrderStep::PickReference { above } => {
|
||||
if self.references.is_empty() { return crate::command::CmdResult::Cancel; }
|
||||
let option = if above { "ABOVE" } else { "UNDER" };
|
||||
let references = self.references.iter().map(|handle| format!("{:x}", handle.value())).collect::<Vec<_>>().join(" ");
|
||||
crate::command::CmdResult::Relaunch(format!("DRAWORDER {option} {references}"), std::mem::take(&mut self.selected))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1374,47 +1415,63 @@ impl CadCommand for DrawOrderCommand {
|
||||
_ => Some(crate::command::CmdResult::NeedPoint),
|
||||
}
|
||||
}
|
||||
DrawOrderStep::PickReference { above } => {
|
||||
let hex_str = t.trim_start_matches("0x").trim_start_matches("0X");
|
||||
if let Ok(val) = u64::from_str_radix(hex_str, 16) {
|
||||
let opt = if above { "A" } else { "U" };
|
||||
let cmd = format!("DRAWORDER {} {:x}", opt, val);
|
||||
let handles = std::mem::take(&mut self.selected);
|
||||
Some(crate::command::CmdResult::Relaunch(cmd, handles))
|
||||
} else {
|
||||
Some(crate::command::CmdResult::NeedPoint)
|
||||
DrawOrderStep::PickReference { .. } => {
|
||||
for text in t.split_whitespace() {
|
||||
let hex = text.trim_start_matches("0x").trim_start_matches("0X");
|
||||
if let Ok(value) = u64::from_str_radix(hex, 16) {
|
||||
let handle = acadrust::Handle::new(value);
|
||||
if !handle.is_null() && !self.selected.contains(&handle) && !self.references.contains(&handle) {
|
||||
self.references.push(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(crate::command::CmdResult::NeedPoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
matches!(self.step, DrawOrderStep::PickReference { .. })
|
||||
}
|
||||
|
||||
fn on_entity_pick(
|
||||
&mut self,
|
||||
handle: acadrust::Handle,
|
||||
_pt: glam::DVec3,
|
||||
) -> crate::command::CmdResult {
|
||||
if handle.is_null() {
|
||||
return crate::command::CmdResult::NeedPoint;
|
||||
}
|
||||
if let DrawOrderStep::PickReference { above } = self.step {
|
||||
let opt = if above { "A" } else { "U" };
|
||||
let cmd = format!("DRAWORDER {} {:x}", opt, handle.value());
|
||||
let handles = std::mem::take(&mut self.selected);
|
||||
crate::command::CmdResult::Relaunch(cmd, handles)
|
||||
} else {
|
||||
crate::command::CmdResult::NeedPoint
|
||||
fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: glam::DVec3) -> crate::command::CmdResult {
|
||||
if matches!(self.step, DrawOrderStep::PickReference { .. }) && !handle.is_null()
|
||||
&& !self.selected.contains(&handle) && !self.references.contains(&handle) {
|
||||
self.references.push(handle);
|
||||
}
|
||||
crate::command::CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: glam::DVec3) -> crate::command::CmdResult {
|
||||
crate::command::CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert the selected block next to the extremal reference in effective draw
|
||||
/// order. Renumber the ordered siblings to avoid ties or saturated sort keys.
|
||||
fn assign_relative_group_keys(
|
||||
document: &acadrust::CadDocument,
|
||||
block: acadrust::Handle,
|
||||
selected: &[acadrust::Handle],
|
||||
references: &[acadrust::Handle],
|
||||
above: bool,
|
||||
) -> Option<Vec<(acadrust::Handle, u64)>> {
|
||||
use acadrust::objects::ObjectType;
|
||||
let overrides: std::collections::HashMap<_, _> = document.objects.values().find_map(|object| {
|
||||
if let ObjectType::SortEntitiesTable(table) = object {
|
||||
(table.block_owner_handle == block).then(|| table.entries().map(|entry| (entry.entity_handle, entry.sort_handle.value())).collect())
|
||||
} else { None }
|
||||
}).unwrap_or_default();
|
||||
let mut ordered: Vec<_> = document.entities().filter(|entity| {
|
||||
let owner = entity.common().owner_handle;
|
||||
owner == block || owner.is_null()
|
||||
}).map(|entity| entity.common().handle).collect();
|
||||
ordered.sort_by_key(|handle| (overrides.get(handle).copied().unwrap_or(handle.value()), handle.value()));
|
||||
let selected_set: std::collections::HashSet<_> = selected.iter().copied().collect();
|
||||
let reference_set: std::collections::HashSet<_> = references.iter().copied().collect();
|
||||
let moved: Vec<_> = ordered.iter().copied().filter(|handle| selected_set.contains(handle)).collect();
|
||||
if moved.is_empty() { return None; }
|
||||
ordered.retain(|handle| !selected_set.contains(handle));
|
||||
let indices: Vec<_> = ordered.iter().enumerate().filter_map(|(index, handle)| reference_set.contains(handle).then_some(index)).collect();
|
||||
let insertion = if above { indices.into_iter().max()? + 1 } else { indices.into_iter().min()? };
|
||||
ordered.splice(insertion..insertion, moved);
|
||||
Some(ordered.into_iter().enumerate().map(|(index, handle)| (handle, index as u64 + 1)).collect())
|
||||
}
|
||||
/// Sort-key assignments sending `group` to the back of the active space.
|
||||
///
|
||||
/// Normal case: the floor is the lowest effective sort key among non-moved
|
||||
@@ -2062,11 +2119,112 @@ mod tests {
|
||||
assert!(hatch2_sort < ref_sort, "hatch 2 ({hatch2_sort}) must be under reference ({ref_sort})");
|
||||
assert_ne!(hatch1_sort, hatch2_sort, "multi-select UNDER must not tie selected entities");
|
||||
assert!(
|
||||
hatch1_sort > hatch2_sort,
|
||||
hatch1_sort < hatch2_sort,
|
||||
"selection order must be preserved within the moved group ({hatch1_sort}, {hatch2_sort})"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_group_uses_extremal_references_and_preserves_internal_order() {
|
||||
let mut app = fresh_app();
|
||||
let i = app.active_tab;
|
||||
let moved_first = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||
let reference_low = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||
let middle = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||
let reference_high = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||
let moved_last = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||
let block = app.tabs[i].scene.current_layout_block_handle_pub();
|
||||
let selected = [moved_first, moved_last];
|
||||
let references = [reference_low, reference_high];
|
||||
|
||||
let ordered = |above| {
|
||||
let mut assignments = assign_relative_group_keys(
|
||||
&app.tabs[i].scene.document,
|
||||
block,
|
||||
&selected,
|
||||
&references,
|
||||
above,
|
||||
)
|
||||
.expect("references belong to the active block");
|
||||
assignments.sort_by_key(|(_, sort)| *sort);
|
||||
assignments.into_iter().map(|(handle, _)| handle).collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
ordered(true),
|
||||
vec![reference_low, middle, reference_high, moved_first, moved_last]
|
||||
);
|
||||
assert_eq!(
|
||||
ordered(false),
|
||||
vec![moved_first, moved_last, reference_low, middle, reference_high]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_group_rejects_unusable_references_without_creating_a_table() {
|
||||
let mut app = fresh_app();
|
||||
let i = app.active_tab;
|
||||
let moved = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||
app.tabs[i].scene.replace_selection(std::iter::once(moved).collect());
|
||||
|
||||
let _ = app.run_command_line("DRAWORDER ABOVE deadbeef");
|
||||
|
||||
assert!(!app.tabs[i].scene.document.objects.values().any(|object| {
|
||||
matches!(object, ObjectType::SortEntitiesTable(_))
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw_order_table_reattaches_orphan_and_advances_object_handles() {
|
||||
use acadrust::objects::SortEntitiesTable;
|
||||
|
||||
let mut app = fresh_app();
|
||||
let i = app.active_tab;
|
||||
let block = app.tabs[i].scene.current_layout_block_handle_pub();
|
||||
let entity = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||
let orphan_handle = acadrust::Handle::new(0x10_0000);
|
||||
let mut orphan = SortEntitiesTable::for_block(block);
|
||||
orphan.handle = orphan_handle;
|
||||
orphan.add_entry(entity, acadrust::Handle::new(7));
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.insert(orphan_handle, ObjectType::SortEntitiesTable(orphan));
|
||||
|
||||
let resolved = ensure_draw_order_table(&mut app.tabs[i].scene.document, block);
|
||||
assert_eq!(resolved, orphan_handle);
|
||||
let dictionary_handle = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.extension_dictionary_handle(block)
|
||||
.expect("block extension dictionary");
|
||||
let ObjectType::Dictionary(dictionary) = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.get(&dictionary_handle)
|
||||
.expect("dictionary object")
|
||||
else {
|
||||
panic!("extension object must be a dictionary");
|
||||
};
|
||||
assert_eq!(dictionary.get(SortEntitiesTable::DICTIONARY_KEY), Some(orphan_handle));
|
||||
let ObjectType::SortEntitiesTable(table) = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.get(&orphan_handle)
|
||||
.expect("sort table")
|
||||
else {
|
||||
panic!("orphan must remain a sort table");
|
||||
};
|
||||
assert_eq!(table.owner_handle, dictionary_handle);
|
||||
assert!(table.entries().any(|entry| {
|
||||
entry.entity_handle == entity && entry.sort_handle.value() == 7
|
||||
}));
|
||||
assert!(app.tabs[i].scene.document.allocate_handle().value() > orphan_handle.value());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draworder_front_moves_hatch_in_front_of_all() {
|
||||
let mut app = fresh_app();
|
||||
@@ -2175,18 +2333,18 @@ mod tests {
|
||||
_ => panic!("Expected Relaunch for shortcut B"),
|
||||
}
|
||||
|
||||
// 3. Shortcut 'A' -> Advances to PickReference (above)
|
||||
// 3. Shortcut 'A' -> Advances to reference gathering (above)
|
||||
let mut cmd = DrawOrderCommand::new(vec![h_hatch]);
|
||||
assert!(!cmd.needs_entity_pick());
|
||||
let res = cmd.on_text_input("A");
|
||||
assert!(matches!(res, Some(crate::command::CmdResult::NeedPoint)));
|
||||
assert!(cmd.needs_entity_pick(), "Needs entity pick after choosing 'A'");
|
||||
assert!(cmd.is_selection_gathering());
|
||||
|
||||
// 4. Shortcut 'U' -> Advances to PickReference (under)
|
||||
// 4. Shortcut 'U' -> Advances to reference gathering (under)
|
||||
let mut cmd = DrawOrderCommand::new(vec![h_hatch]);
|
||||
let res = cmd.on_text_input("U");
|
||||
assert!(matches!(res, Some(crate::command::CmdResult::NeedPoint)));
|
||||
assert!(cmd.needs_entity_pick(), "Needs entity pick after choosing 'U'");
|
||||
assert!(cmd.is_selection_gathering());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2198,14 +2356,15 @@ mod tests {
|
||||
let h_ref = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||
let h_hatch = app.tabs[i].scene.add_entity_clone(EntityType::Hatch(Default::default()));
|
||||
|
||||
// Above with viewport pick
|
||||
// Above with a gathered viewport selection
|
||||
let mut cmd = DrawOrderCommand::new(vec![h_hatch]);
|
||||
let _ = cmd.on_text_input("A");
|
||||
assert!(cmd.needs_entity_pick());
|
||||
let pick_res = cmd.on_entity_pick(h_ref, glam::DVec3::ZERO);
|
||||
assert!(cmd.is_selection_gathering());
|
||||
let _ = cmd.on_selection_complete(vec![h_hatch, h_ref]);
|
||||
let pick_res = cmd.on_enter();
|
||||
match pick_res {
|
||||
crate::command::CmdResult::Relaunch(relaunch_cmd, handles) => {
|
||||
assert_eq!(relaunch_cmd, format!("DRAWORDER A {:x}", h_ref.value()));
|
||||
assert_eq!(relaunch_cmd, format!("DRAWORDER ABOVE {:x}", h_ref.value()));
|
||||
assert_eq!(handles, vec![h_hatch]);
|
||||
app.tabs[i].scene.replace_selection(handles.into_iter().collect());
|
||||
let _ = app.run_command_line(&relaunch_cmd);
|
||||
@@ -2218,14 +2377,15 @@ mod tests {
|
||||
let ref_sort = entries.get(&h_ref.value()).copied().unwrap_or(h_ref.value());
|
||||
assert!(hatch_sort > ref_sort, "Hatch must be above reference after viewport pick");
|
||||
|
||||
// Under with viewport pick
|
||||
// Under with a gathered viewport selection
|
||||
let mut cmd = DrawOrderCommand::new(vec![h_hatch]);
|
||||
let _ = cmd.on_text_input("U");
|
||||
assert!(cmd.needs_entity_pick());
|
||||
let pick_res = cmd.on_entity_pick(h_ref, glam::DVec3::ZERO);
|
||||
assert!(cmd.is_selection_gathering());
|
||||
let _ = cmd.on_selection_complete(vec![h_hatch, h_ref]);
|
||||
let pick_res = cmd.on_enter();
|
||||
match pick_res {
|
||||
crate::command::CmdResult::Relaunch(relaunch_cmd, handles) => {
|
||||
assert_eq!(relaunch_cmd, format!("DRAWORDER U {:x}", h_ref.value()));
|
||||
assert_eq!(relaunch_cmd, format!("DRAWORDER UNDER {:x}", h_ref.value()));
|
||||
assert_eq!(handles, vec![h_hatch]);
|
||||
app.tabs[i].scene.replace_selection(handles.into_iter().collect());
|
||||
let _ = app.run_command_line(&relaunch_cmd);
|
||||
@@ -2252,9 +2412,10 @@ mod tests {
|
||||
let _ = cmd.on_text_input("Above");
|
||||
let hex_input = format!("0x{:x}", h_ref.value());
|
||||
let typed_res = cmd.on_text_input(&hex_input);
|
||||
match typed_res {
|
||||
Some(crate::command::CmdResult::Relaunch(relaunch_cmd, handles)) => {
|
||||
assert_eq!(relaunch_cmd, format!("DRAWORDER A {:x}", h_ref.value()));
|
||||
assert!(matches!(typed_res, Some(crate::command::CmdResult::NeedPoint)));
|
||||
match cmd.on_enter() {
|
||||
crate::command::CmdResult::Relaunch(relaunch_cmd, handles) => {
|
||||
assert_eq!(relaunch_cmd, format!("DRAWORDER ABOVE {:x}", h_ref.value()));
|
||||
assert_eq!(handles, vec![h_hatch]);
|
||||
app.tabs[i].scene.replace_selection(handles.into_iter().collect());
|
||||
let _ = app.run_command_line(&relaunch_cmd);
|
||||
@@ -2306,4 +2467,35 @@ mod tests {
|
||||
let line_sort = entries.get(&h_line.value()).copied().unwrap_or(h_line.value());
|
||||
assert!(hatch_sort > line_sort);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draworder_mode_aliases_route_directly() {
|
||||
let mut app = fresh_app();
|
||||
let i = app.active_tab;
|
||||
let line = app.tabs[i]
|
||||
.scene
|
||||
.add_entity_clone(EntityType::Line(Default::default()));
|
||||
let hatch = app.tabs[i]
|
||||
.scene
|
||||
.add_entity_clone(EntityType::Hatch(Default::default()));
|
||||
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.replace_selection(std::iter::once(hatch).collect());
|
||||
let _ = app.run_command_line("DRAWORDER_FRONT");
|
||||
let entries = effective_sort_map(&app);
|
||||
assert!(
|
||||
entries.get(&hatch.value()).copied().unwrap_or(hatch.value())
|
||||
> entries.get(&line.value()).copied().unwrap_or(line.value())
|
||||
);
|
||||
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.replace_selection(std::iter::once(hatch).collect());
|
||||
let _ = app.run_command_line("DRAWORDER_ABOVE");
|
||||
assert!(app.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.is_some_and(|command| command.name() == "DRAWORDER" && command.is_selection_gathering()));
|
||||
}
|
||||
}
|
||||
-4504
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 {
|
||||
|
||||
+72
-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;
|
||||
@@ -511,6 +509,7 @@ pub(super) struct OpenCADStudio {
|
||||
/// Selected-object count past which grips stop being generated
|
||||
/// (GRIPOBJLIMIT, 0..=32767; 0 = no limit).
|
||||
grip_object_limit: i32,
|
||||
ncopy_bind: bool,
|
||||
/// Drawing viewport cursor style (CURSORTYPE).
|
||||
cursor_type: settings::CursorType,
|
||||
/// Explicit crosshair colour; `None` retains automatic contrast.
|
||||
@@ -547,6 +546,14 @@ pub(super) struct OpenCADStudio {
|
||||
/// When true (default), the app registers itself as a .dwg/.dxf/.bak file
|
||||
/// handler on each launch. Toggle with the FILEASSOC command.
|
||||
pub file_assoc_enabled: bool,
|
||||
/// When true, saving creates native constraint objects alongside the
|
||||
/// application's own persistence record. Existing native objects remain
|
||||
/// synchronized regardless of this setting.
|
||||
pub write_dwg_native_constraints: bool,
|
||||
/// When true (default), a sketch constraint's viewport pill shows its
|
||||
/// glyph plus a driven value or named-parameter name. When false, every
|
||||
/// pill shows only the glyph.
|
||||
pub show_constraint_values: bool,
|
||||
/// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command);
|
||||
/// 0 disables autosave.
|
||||
pub savetime_min: i32,
|
||||
@@ -1040,6 +1047,15 @@ pub(super) struct OpenCADStudio {
|
||||
/// edited. Seeded from `command_aliases` on open, committed back on close.
|
||||
alias_editor_rows: Vec<(String, String)>,
|
||||
|
||||
// ── Named Parameters ──────────────────────────────────────────────────
|
||||
/// Working buffer for the PARAMETERS modal. Unlike `alias_editor_rows`,
|
||||
/// this isn't a copy of a separate app-level store — the real state
|
||||
/// lives per-document at `Scene::named_parameters`; this buffer is
|
||||
/// seeded from the active tab's table on open and only written back to
|
||||
/// it on Apply (`apply_named_parameter_editor_rows`,
|
||||
/// `src/app/named_parameters.rs`).
|
||||
named_parameter_editor_rows: Vec<crate::ui::window::named_parameters::ParamEditorRow>,
|
||||
|
||||
// ── Layout Manager Panel ──────────────────────────────────────────────
|
||||
layout_manager_selected: String,
|
||||
layout_manager_rename_buf: String,
|
||||
@@ -1717,6 +1733,7 @@ pub enum ModalKind {
|
||||
AttributeEditor,
|
||||
LayerDeleteWarning,
|
||||
Aliases,
|
||||
NamedParameters,
|
||||
ScaleManager,
|
||||
/// Add / remove the annotation scales a single selected object has a
|
||||
/// per-object representation for.
|
||||
@@ -2071,6 +2088,11 @@ pub enum Message {
|
||||
/// Register or unregister as the .dwg/.dxf handler, from Options. Same
|
||||
/// setting the FILEASSOC command carries.
|
||||
FileAssocChanged(bool),
|
||||
/// Toggle writing native constraint objects on save.
|
||||
WriteDwgNativeConstraintsChanged(bool),
|
||||
/// Toggle showing driven values/named-parameter names on constraint
|
||||
/// pills, from Options. See `show_constraint_values`'s doc comment.
|
||||
ShowConstraintValuesChanged(bool),
|
||||
/// Switch the interface language and redraw localized views.
|
||||
LanguageChanged(crate::i18n::Language),
|
||||
/// Drop every entity from the active drawing.
|
||||
@@ -2494,6 +2516,10 @@ pub enum Message {
|
||||
CloseLayoutList,
|
||||
/// Cycle the coordinate readout mode ($COORDS): static → live → polar.
|
||||
CycleCoordsMode,
|
||||
/// Removes one flagged redundant or conflicting constraint from the
|
||||
/// current sketch scope.
|
||||
/// No-op if the scope currently has no flagged conflict.
|
||||
ResolveOneSketchConflict,
|
||||
/// Toggle the status-bar customization menu open/closed.
|
||||
ToggleStatusBarMenu,
|
||||
/// Close the status-bar customization menu.
|
||||
@@ -2789,6 +2815,46 @@ pub enum Message {
|
||||
AliasEditorRemove(usize),
|
||||
/// Commit the edited rows to the alias table (Apply button); stays open.
|
||||
AliasEditorApply,
|
||||
// ── Named Parameters (PARAMETERS) ───────────────────────────────────
|
||||
/// Open the named-parameter editor, seeding rows from the active tab's
|
||||
/// `Scene::named_parameters`.
|
||||
NamedParametersOpen,
|
||||
/// Live edit of the name or formula in row `idx`.
|
||||
NamedParametersInput {
|
||||
idx: usize,
|
||||
field: crate::ui::window::named_parameters::ParamField,
|
||||
value: String,
|
||||
},
|
||||
/// Append a blank parameter row.
|
||||
NamedParametersAdd,
|
||||
/// Remove parameter row `idx`.
|
||||
NamedParametersRemove(usize),
|
||||
/// Commit the edited rows to `Scene::named_parameters` (Apply button)
|
||||
/// and re-solve every constraint that reads a named parameter; stays
|
||||
/// open.
|
||||
NamedParametersApply,
|
||||
// ── Parameters / Constraints sections embedded in Properties ──────────
|
||||
/// Live text of one column of parameter row `index`, keyed by its
|
||||
/// `ParameterTable::iter()` position — see `PropValue::ParamRow`.
|
||||
PropParamInput {
|
||||
index: usize,
|
||||
field: crate::ui::window::named_parameters::ParamField,
|
||||
value: String,
|
||||
},
|
||||
/// Commit row `index`'s buffered edit for `field` to `Scene::
|
||||
/// named_parameters` (Enter / losing focus) and re-solve whatever it
|
||||
/// drives.
|
||||
PropParamCommit {
|
||||
index: usize,
|
||||
field: crate::ui::window::named_parameters::ParamField,
|
||||
},
|
||||
/// Remove parameter row `index` immediately.
|
||||
PropParamDelete(usize),
|
||||
/// Append a fresh, uniquely-named parameter to `Scene::named_parameters`.
|
||||
PropParamAddNew,
|
||||
/// A Constraints-section row was clicked: select every entity in the
|
||||
/// list (replacing the current selection) in the viewport.
|
||||
PropConstraintLinkClick(Vec<acadrust::Handle>),
|
||||
// ── About window ────────────────────────────────────────────────────
|
||||
AboutOpen,
|
||||
/// Close whatever in-canvas modal dialog is open (Plan B).
|
||||
@@ -3461,6 +3527,7 @@ impl OpenCADStudio {
|
||||
double_click_block_refedit: false,
|
||||
double_click_block_attedit: true,
|
||||
grip_object_limit: settings::DEFAULT_GRIP_OBJECT_LIMIT,
|
||||
ncopy_bind: false,
|
||||
cursor_type: settings::CursorType::Crosshair,
|
||||
crosshair_color: None,
|
||||
crosshair_color_input: String::new(),
|
||||
@@ -3478,6 +3545,8 @@ impl OpenCADStudio {
|
||||
dimension_continue_mode: 1,
|
||||
backup_on_save: true,
|
||||
file_assoc_enabled: true,
|
||||
write_dwg_native_constraints: false,
|
||||
show_constraint_values: true,
|
||||
savetime_min: 10,
|
||||
default_bg_color: None,
|
||||
default_paper_bg_color: None,
|
||||
@@ -3669,6 +3738,7 @@ impl OpenCADStudio {
|
||||
// Command aliases (populated from ocad.pgp just after construction)
|
||||
command_aliases: rustc_hash::FxHashMap::default(),
|
||||
alias_editor_rows: Vec::new(),
|
||||
named_parameter_editor_rows: Vec::new(),
|
||||
// Layout Manager
|
||||
layout_manager_selected: "Model".to_string(),
|
||||
layer_state_selected: None,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
@@ -3887,6 +3910,7 @@ impl OpenCADStudio {
|
||||
} else if self.tabs[i].active_cmd.as_ref()
|
||||
.is_some_and(|command| command.entity_pick_accepts_points())
|
||||
{
|
||||
self.refresh_command_point_pick_context(i);
|
||||
self.tabs[i].active_cmd.as_mut().map(|command| command.on_point(pick_wcs))
|
||||
} else if self.tabs[i]
|
||||
.active_cmd
|
||||
@@ -3963,6 +3987,7 @@ impl OpenCADStudio {
|
||||
// (LINE tangent to two circles, which needs both). When
|
||||
// it does, sync last_point to the command's resolved
|
||||
// anchor since it replaced the picked coordinate.
|
||||
self.refresh_command_point_pick_context(i);
|
||||
let handled = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_mut()
|
||||
|
||||
@@ -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