Files
OneCAD-Draft/src/app/command_driver/transform.rs
T
Karim Jerbi e4fa5232f6 refactor(command_driver): split 11.5k-line driver into domain modules
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.
2026-09-24 21:47:14 +01:00

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
}
}