forked from 1b-agent/opencadstudio-upstream
Moves the exhaustive CmdResult router and its 116 handlers out of a single file into command_driver/: thin router stays in mod.rs (no wildcard, compiler-enforced coverage); handlers grouped by domain (entity_commit, clipboard, property_match, modify, solid3d, transform, viewport, text_edit, constraint, dim_edit); shared helpers in utilities/parameters; tests in tests.rs. All moves verbatim 1:1; all entry-point signatures and visibilities unchanged, so every existing caller (ribbon, commands, headless, plugins) resolves untouched.
578 lines
25 KiB
Rust
578 lines
25 KiB
Rust
use super::*;
|
|
|
|
impl OpenCADStudio {
|
|
pub(super) fn handle_transform_selected(&mut self, mut handles: Vec<Handle>, transform: crate::command::EntityTransform) -> Option<Task<Message>> {
|
|
let i = self.active_tab;
|
|
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
|
|
if handles.is_empty() {
|
|
self.tabs[i].active_cmd = None;
|
|
return Some(Task::none());
|
|
}
|
|
if matches!(&transform, crate::command::EntityTransform::Translate(_)) {
|
|
let scope = self.tabs[i].current_parametric_scope();
|
|
handles = self.tabs[i]
|
|
.scene
|
|
.parametric_connected_handles(scope, &handles, false);
|
|
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
|
|
}
|
|
let label = self.history_label_from_active_cmd(i, "MOVE");
|
|
// A translation carries its complete connected constraint
|
|
// component so each member keeps its own dimensions. Other
|
|
// transforms retain their selected-object behavior.
|
|
let pending = self.begin_undo(i, label, handles.len(), true);
|
|
self.tabs[i].scene.transform_entities(&handles, &transform);
|
|
self.apply_inferred_constraints(i, &handles);
|
|
self.tabs[i].dirty = true;
|
|
self.tabs[i].scene.clear_preview_wire();
|
|
self.tabs[i].active_cmd = None;
|
|
self.tabs[i].snap_result = None;
|
|
self.restore_pre_cmd_tangent();
|
|
self.refresh_properties();
|
|
if let Some(p) = pending {
|
|
self.commit_undo_delta(i, p);
|
|
}
|
|
None
|
|
}
|
|
|
|
pub(super) fn handle_copy_selected(&mut self, mut handles: Vec<Handle>, transform: crate::command::EntityTransform) -> Option<Task<Message>> {
|
|
let i = self.active_tab;
|
|
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
|
|
if handles.is_empty() {
|
|
return Some(Task::none());
|
|
}
|
|
let label = self.history_label_from_active_cmd(i, "COPY");
|
|
// Copying a dimension clones a *D block record, so gate delta on
|
|
// the selection being dimension-free.
|
|
let delta_safe = self.delta_copy_safe(i, &handles);
|
|
let pending = self.begin_undo(i, label, handles.len(), delta_safe);
|
|
let new_handles = self.tabs[i].scene.copy_entities(&handles, &transform);
|
|
self.tabs[i].dirty = true;
|
|
self.tabs[i].scene.deselect_all();
|
|
for h in new_handles {
|
|
self.tabs[i].scene.select_entity(h, false);
|
|
}
|
|
self.tabs[i].scene.clear_preview_wire();
|
|
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
|
if let Some(p) = prompt {
|
|
self.command_line.push_info(&p);
|
|
}
|
|
self.refresh_properties();
|
|
if let Some(pd) = pending {
|
|
self.commit_undo_delta(i, pd);
|
|
}
|
|
None
|
|
}
|
|
|
|
pub(super) fn handle_align_selected(&mut self, result: CmdResult) {
|
|
let i = self.active_tab;
|
|
let CmdResult::AlignSelected {
|
|
mut handles,
|
|
src1,
|
|
dst1,
|
|
angle_rad,
|
|
scale,
|
|
} = result
|
|
else {
|
|
unreachable!("router only routes the matching variant");
|
|
};
|
|
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
|
|
if handles.is_empty() {
|
|
self.tabs[i].active_cmd = None;
|
|
self.tabs[i].snap_result = None;
|
|
self.tabs[i].scene.clear_preview_wire();
|
|
self.restore_pre_cmd_tangent();
|
|
} else {
|
|
let label = self.history_label_from_active_cmd(i, "ALIGN");
|
|
// A chain of transform_entities on the same handles — the
|
|
// recording keeps each entity's first (pre-align) image, so
|
|
// it's delta-safe like a single transform.
|
|
let pending = self.begin_undo(i, label, handles.len(), true);
|
|
// Step 1: translate so src1 is at origin
|
|
self.tabs[i].scene.transform_entities(
|
|
&handles,
|
|
&crate::command::EntityTransform::Translate(-src1),
|
|
);
|
|
// Step 2: uniform scale (only when != 1)
|
|
if (scale - 1.0).abs() > 1e-4 {
|
|
self.tabs[i].scene.transform_entities(
|
|
&handles,
|
|
&crate::command::EntityTransform::Scale {
|
|
center: glam::DVec3::ZERO,
|
|
factor: scale,
|
|
},
|
|
);
|
|
}
|
|
// Step 3: rotate in the XY plane by angle_rad
|
|
if angle_rad.abs() > 1e-4 {
|
|
self.tabs[i].scene.transform_entities(
|
|
&handles,
|
|
&crate::command::EntityTransform::Rotate {
|
|
center: glam::DVec3::ZERO,
|
|
axis: glam::DVec3::Z,
|
|
angle_rad,
|
|
},
|
|
);
|
|
}
|
|
// Step 4: translate to dst1
|
|
self.tabs[i].scene.transform_entities(
|
|
&handles,
|
|
&crate::command::EntityTransform::Translate(dst1),
|
|
);
|
|
self.tabs[i].dirty = true;
|
|
self.tabs[i].scene.deselect_all();
|
|
for h in &handles {
|
|
self.tabs[i].scene.select_entity(*h, false);
|
|
}
|
|
self.tabs[i].scene.clear_preview_wire();
|
|
self.tabs[i].active_cmd = None;
|
|
self.tabs[i].snap_result = None;
|
|
self.restore_pre_cmd_tangent();
|
|
self.command_line
|
|
.push_output(crate::t!("ALIGN: applied.").as_ref());
|
|
self.refresh_properties();
|
|
if let Some(pd) = pending {
|
|
self.commit_undo_delta(i, pd);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn handle_stretch_window(&mut self, mut handles: Vec<Handle>, windows: Vec<(glam::DVec3, glam::DVec3)>) {
|
|
let i = self.active_tab;
|
|
// Accumulate every entity touched by any crossing window. Keep STRETCH
|
|
// in its selection stage; Enter is what advances to the base point.
|
|
{
|
|
let scene = &self.tabs[i].scene;
|
|
|
|
for (win_min, win_max) in &windows {
|
|
handles.extend(
|
|
scene
|
|
.interaction_handles_in_world_aabb([
|
|
win_min.x, win_min.y, win_max.x, win_max.y,
|
|
])
|
|
.into_iter()
|
|
.filter(|&handle| !scene.is_layer_locked(handle)),
|
|
);
|
|
}
|
|
}
|
|
|
|
handles.sort_unstable_by_key(|handle| handle.value());
|
|
handles.dedup();
|
|
|
|
if handles.is_empty() {
|
|
self.command_line
|
|
.push_output(crate::t!("STRETCH: nothing crosses the window.").as_ref());
|
|
} else {
|
|
// Visual feedback must only highlight entities that actually have a
|
|
// stretchable point inside one of the crossing windows. `handles` can
|
|
// contain the whole candidate/preselection set, while `windows` decides
|
|
// which vertices will really move.
|
|
let point_inside = |point: glam::DVec3| {
|
|
const EPS: f64 = 1.0e-9;
|
|
|
|
windows.iter().any(|(win_min, win_max)| {
|
|
point.x >= win_min.x - EPS
|
|
&& point.x <= win_max.x + EPS
|
|
&& point.y >= win_min.y - EPS
|
|
&& point.y <= win_max.y + EPS
|
|
&& point.z >= win_min.z - EPS
|
|
&& point.z <= win_max.z + EPS
|
|
})
|
|
};
|
|
|
|
let mut visual_handles = Vec::new();
|
|
|
|
for &handle in &handles {
|
|
let wires = self.tabs[i].scene.wire_models_for(&[handle]);
|
|
|
|
let affected = wires.iter().any(|wire| {
|
|
// LINE / LWPOLYLINE / other vertex-based entities.
|
|
let vertex_hit = wire
|
|
.key_vertices
|
|
.iter()
|
|
.any(|point| point_inside(glam::DVec3::from_array(*point)));
|
|
|
|
if vertex_hit {
|
|
return true;
|
|
}
|
|
|
|
// Whole-object STRETCH cases such as circles/arcs/inserts/points:
|
|
// use only their meaningful anchor snap, not quadrants or
|
|
// tessellated display points.
|
|
wire.snap_pts.iter().any(|(point, hint)| {
|
|
matches!(
|
|
hint,
|
|
crate::scene::model::wire_model::SnapHint::Center
|
|
| crate::scene::model::wire_model::SnapHint::Insertion
|
|
| crate::scene::model::wire_model::SnapHint::Node
|
|
) && point_inside(*point)
|
|
})
|
|
});
|
|
|
|
if affected {
|
|
visual_handles.push(handle);
|
|
}
|
|
}
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
self.tabs[i].scene.select_entities(&visual_handles);
|
|
self.refresh_selected_grips();
|
|
// STRETCH temporary grips: keep only the actual vertices/endpoints
|
|
// that lie inside one of the accumulated crossing windows.
|
|
//
|
|
// Mid-segment grips are deliberately excluded: STRETCH moves vertices,
|
|
// not the midpoint affordances used by normal grip editing.
|
|
let mut stretch_grips = Vec::new();
|
|
let mut stretch_grip_handles = Vec::new();
|
|
|
|
for (handle, grip) in std::mem::take(&mut self.tabs[i].selected_grip_handles)
|
|
.into_iter()
|
|
.zip(std::mem::take(&mut self.tabs[i].selected_grips))
|
|
{
|
|
if !grip.is_midpoint && point_inside(grip.world) {
|
|
stretch_grip_handles.push(handle);
|
|
stretch_grips.push(grip);
|
|
}
|
|
}
|
|
|
|
self.tabs[i].selected_grip_handles = stretch_grip_handles;
|
|
self.tabs[i].selected_grips = stretch_grips;
|
|
}
|
|
|
|
use crate::command::CadCommand;
|
|
use crate::modules::draw::modify::stretch::StretchCommand;
|
|
|
|
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
|
|
|
let cmd = StretchCommand::with_windows(handles, wires, windows);
|
|
|
|
self.command_line.push_info(&CadCommand::prompt(&cmd));
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
}
|
|
|
|
pub(super) fn handle_stretch_entities(&mut self, mut handles: Vec<Handle>, windows: Vec<(glam::DVec3, glam::DVec3)>, delta: glam::DVec3) -> Option<Task<Message>> {
|
|
let i = self.active_tab;
|
|
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
|
|
if handles.is_empty() {
|
|
self.tabs[i].active_cmd = None;
|
|
return Some(Task::none());
|
|
}
|
|
let structural = handles.iter().any(|handle| {
|
|
matches!(
|
|
self.tabs[i].scene.document.get_entity(*handle),
|
|
Some(acadrust::EntityType::Dimension(_))
|
|
)
|
|
});
|
|
let pending = self.begin_undo(i, "STRETCH", handles.len(), !structural);
|
|
let mut count = 0usize;
|
|
let mut changed_handles = Vec::new();
|
|
let mut driven_refs = Vec::new();
|
|
|
|
// Helper: is DXF point (x, y) inside the world-space window?
|
|
// Drawing plane is world XY (= DXF XY).
|
|
let in_win = |x: f64, y: f64| -> bool {
|
|
windows.iter().any(|(win_min, win_max)| {
|
|
x >= win_min.x && x <= win_max.x && y >= win_min.y && y <= win_max.y
|
|
})
|
|
};
|
|
|
|
let dx = delta.x as f64;
|
|
let dy = delta.y as f64; // drawing plane is world XY
|
|
let dz = delta.z as f64;
|
|
|
|
// Dimensions whose points moved — their baked *D block is
|
|
// stale afterwards and must be dropped (see #398 / #372).
|
|
let mut stretched_dims: Vec<acadrust::Handle> = Vec::new();
|
|
for handle in &handles {
|
|
let before = self.tabs[i].scene.document.get_entity_arc(*handle);
|
|
if let Some(entity) = before.as_deref() {
|
|
use crate::scene::parametric_constraints::ParametricRef;
|
|
match entity {
|
|
acadrust::EntityType::Line(line) => {
|
|
if in_win(line.start.x, line.start.y) {
|
|
driven_refs.push(ParametricRef::point(*handle, 0));
|
|
}
|
|
if in_win(line.end.x, line.end.y) {
|
|
driven_refs.push(ParametricRef::point(*handle, 1));
|
|
}
|
|
}
|
|
acadrust::EntityType::LwPolyline(polyline) => {
|
|
if let Some(world) =
|
|
crate::entities::curve::lwpolyline_world_xy(polyline)
|
|
{
|
|
for (index, vertex) in world.vertices.iter().enumerate() {
|
|
if in_win(vertex.location.x, vertex.location.y) {
|
|
driven_refs.push(ParametricRef::point(
|
|
*handle,
|
|
index as i32,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
acadrust::EntityType::Polyline2D(polyline) => {
|
|
for (index, vertex) in polyline.vertices.iter().enumerate() {
|
|
if in_win(vertex.location.x, vertex.location.y) {
|
|
driven_refs.push(ParametricRef::point(
|
|
*handle,
|
|
index as i32,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
acadrust::EntityType::Circle(circle)
|
|
if in_win(circle.center.x, circle.center.y) =>
|
|
{
|
|
driven_refs.push(ParametricRef::center(*handle));
|
|
}
|
|
acadrust::EntityType::Arc(arc)
|
|
if in_win(arc.center.x, arc.center.y) =>
|
|
{
|
|
driven_refs.push(ParametricRef::center(*handle));
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
let Some(entity) = self.tabs[i].scene.document.get_entity_mut(*handle) else {
|
|
continue;
|
|
};
|
|
let mut stretched = false;
|
|
match entity {
|
|
acadrust::EntityType::Line(l) => {
|
|
let s_in = in_win(l.start.x, l.start.y);
|
|
let e_in = in_win(l.end.x, l.end.y);
|
|
if s_in {
|
|
l.start.x += dx;
|
|
l.start.y += dy;
|
|
l.start.z += dz;
|
|
stretched = true;
|
|
}
|
|
if e_in {
|
|
l.end.x += dx;
|
|
l.end.y += dy;
|
|
l.end.z += dz;
|
|
stretched = true;
|
|
}
|
|
}
|
|
acadrust::EntityType::LwPolyline(p) => {
|
|
let Some(mut world) = crate::entities::curve::lwpolyline_world_xy(p)
|
|
else {
|
|
continue;
|
|
};
|
|
for v in &mut world.vertices {
|
|
if in_win(v.location.x, v.location.y) {
|
|
v.location.x += dx;
|
|
v.location.y += dy;
|
|
stretched = true;
|
|
}
|
|
}
|
|
if stretched {
|
|
*p = world;
|
|
}
|
|
}
|
|
acadrust::EntityType::Polyline2D(p) => {
|
|
for v in &mut p.vertices {
|
|
if in_win(v.location.x, v.location.y) {
|
|
v.location.x += dx;
|
|
v.location.y += dy;
|
|
stretched = true;
|
|
}
|
|
}
|
|
}
|
|
acadrust::EntityType::Polyline(p) => {
|
|
for v in &mut p.vertices {
|
|
if in_win(v.location.x, v.location.z) {
|
|
v.location.x += dx;
|
|
v.location.z += dy;
|
|
stretched = true;
|
|
}
|
|
}
|
|
}
|
|
acadrust::EntityType::Arc(a) => {
|
|
if in_win(a.center.x, a.center.y) {
|
|
a.center.x += dx;
|
|
a.center.y += dy;
|
|
a.center.z += dz;
|
|
stretched = true;
|
|
}
|
|
}
|
|
acadrust::EntityType::Circle(c) => {
|
|
if in_win(c.center.x, c.center.y) {
|
|
c.center.x += dx;
|
|
c.center.y += dy;
|
|
c.center.z += dz;
|
|
stretched = true;
|
|
}
|
|
}
|
|
acadrust::EntityType::Ellipse(e) => {
|
|
if in_win(e.center.x, e.center.y) {
|
|
e.center.x += dx;
|
|
e.center.y += dy;
|
|
e.center.z += dz;
|
|
stretched = true;
|
|
}
|
|
}
|
|
acadrust::EntityType::Insert(ins) => {
|
|
if in_win(ins.insert_point.x, ins.insert_point.y) {
|
|
ins.insert_point.x += dx;
|
|
ins.insert_point.y += dy;
|
|
ins.insert_point.z += dz;
|
|
stretched = true;
|
|
}
|
|
}
|
|
acadrust::EntityType::Text(t) => {
|
|
if in_win(t.insertion_point.x, t.insertion_point.y) {
|
|
t.insertion_point.x += dx;
|
|
t.insertion_point.y += dy;
|
|
t.insertion_point.z += dz;
|
|
stretched = true;
|
|
}
|
|
}
|
|
acadrust::EntityType::MText(t) => {
|
|
if in_win(t.insertion_point.x, t.insertion_point.y) {
|
|
t.insertion_point.x += dx;
|
|
t.insertion_point.y += dy;
|
|
t.insertion_point.z += dz;
|
|
stretched = true;
|
|
}
|
|
}
|
|
acadrust::EntityType::Viewport(vp) => {
|
|
stretched = windows.iter().any(|(win_min, win_max)| {
|
|
crate::entities::viewport::stretch(vp, *win_min, *win_max, delta)
|
|
});
|
|
}
|
|
acadrust::EntityType::Dimension(dim) => {
|
|
use acadrust::entities::Dimension;
|
|
// Move every definition point that falls inside the
|
|
// window — the same points the grips expose — then
|
|
// refresh the stored measurement so the value tracks
|
|
// the stretched geometry.
|
|
let mv = |p: &mut acadrust::types::Vector3| {
|
|
if in_win(p.x, p.y) {
|
|
p.x += dx;
|
|
p.y += dy;
|
|
p.z += dz;
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
};
|
|
match dim {
|
|
Dimension::Linear(d) => {
|
|
stretched |= mv(&mut d.first_point);
|
|
stretched |= mv(&mut d.second_point);
|
|
stretched |= mv(&mut d.definition_point);
|
|
}
|
|
Dimension::Aligned(d) => {
|
|
stretched |= mv(&mut d.first_point);
|
|
stretched |= mv(&mut d.second_point);
|
|
stretched |= mv(&mut d.definition_point);
|
|
}
|
|
Dimension::Radius(d) => {
|
|
stretched |= mv(&mut d.angle_vertex);
|
|
stretched |= mv(&mut d.definition_point);
|
|
}
|
|
Dimension::Diameter(d) => {
|
|
stretched |= mv(&mut d.angle_vertex);
|
|
stretched |= mv(&mut d.definition_point);
|
|
}
|
|
Dimension::Angular2Ln(d) => {
|
|
stretched |= mv(&mut d.angle_vertex);
|
|
stretched |= mv(&mut d.first_point);
|
|
stretched |= mv(&mut d.second_point);
|
|
stretched |= mv(&mut d.definition_point);
|
|
}
|
|
Dimension::Angular3Pt(d) => {
|
|
stretched |= mv(&mut d.angle_vertex);
|
|
stretched |= mv(&mut d.first_point);
|
|
stretched |= mv(&mut d.second_point);
|
|
stretched |= mv(&mut d.definition_point);
|
|
}
|
|
Dimension::Ordinate(d) => {
|
|
stretched |= mv(&mut d.feature_location);
|
|
stretched |= mv(&mut d.leader_endpoint);
|
|
}
|
|
Dimension::Arc(d) => {
|
|
stretched |= mv(&mut d.definition_point);
|
|
stretched |= mv(&mut d.first_extension_point);
|
|
stretched |= mv(&mut d.second_extension_point);
|
|
stretched |= mv(&mut d.center_point);
|
|
if d.has_leader {
|
|
stretched |= mv(&mut d.first_leader_point);
|
|
stretched |= mv(&mut d.second_leader_point);
|
|
}
|
|
}
|
|
Dimension::LargeRadial(d) => {
|
|
stretched |= mv(&mut d.definition_point);
|
|
stretched |= mv(&mut d.chord_point);
|
|
stretched |= mv(&mut d.override_center);
|
|
stretched |= mv(&mut d.jog_point);
|
|
}
|
|
}
|
|
// Pinned text follows too; the zero sentinel means
|
|
// "auto placement" and must not be captured by a
|
|
// window that happens to cover the origin.
|
|
let t = dim.base().text_middle_point;
|
|
if t.x * t.x + t.y * t.y + t.z * t.z > 1e-16 {
|
|
let mut t = t;
|
|
if mv(&mut t) {
|
|
dim.base_mut().text_middle_point = t;
|
|
stretched = true;
|
|
}
|
|
}
|
|
if stretched {
|
|
dim.base_mut().actual_measurement = dim.measurement();
|
|
stretched_dims.push(*handle);
|
|
}
|
|
}
|
|
_ => {
|
|
// Generic: move entire entity (treat as block-level)
|
|
stretched = false; // skip generic types
|
|
}
|
|
}
|
|
if stretched {
|
|
if let Some(before) = before {
|
|
self.tabs[i].scene.record_undo_before(*handle, Some(before));
|
|
}
|
|
self.tabs[i].scene.mark_entity_dirty(*handle);
|
|
changed_handles.push(*handle);
|
|
count += 1;
|
|
}
|
|
}
|
|
// A stretched dimension renders through its baked *D block when
|
|
// one exists (file roundtrip) — drop it so tessellation falls
|
|
// back to the live points and the next save re-bakes. (#372)
|
|
for h in stretched_dims {
|
|
self.tabs[i].scene.invalidate_dim_block_recorded(h);
|
|
}
|
|
|
|
// Geometry was edited in place via get_entity_mut, which the
|
|
// scene's tessellation cache doesn't observe. Re-tessellate the
|
|
// moved entities so the viewport reflects the stretch right away
|
|
// instead of only on the next unrelated redraw. See #95.
|
|
if count > 0 {
|
|
let changes: Vec<_> = changed_handles
|
|
.iter()
|
|
.map(|&handle| (handle, crate::scene::ChangeKind::Modified))
|
|
.collect();
|
|
self.tabs[i].scene.bump_entities_with_parametric_policy(
|
|
&changes,
|
|
&driven_refs,
|
|
self.constraint_solve_mode && !driven_refs.is_empty(),
|
|
);
|
|
self.apply_inferred_constraints(i, &changed_handles);
|
|
}
|
|
self.tabs[i].dirty = true;
|
|
self.tabs[i].active_cmd = None;
|
|
self.tabs[i].snap_result = None;
|
|
self.tabs[i].scene.clear_preview_wire();
|
|
self.restore_pre_cmd_tangent();
|
|
self.command_line
|
|
.push_output(crate::tf!("STRETCH: {count} entity(ies) stretched.").as_ref());
|
|
self.refresh_properties();
|
|
if let Some(pending) = pending {
|
|
self.commit_undo_delta(i, pending);
|
|
}
|
|
None
|
|
}
|
|
}
|