diff --git a/src/app/commands.rs b/src/app/commands.rs index e0c7fc985..2c07512f9 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -2104,15 +2104,27 @@ impl OpenCADStudio { } }; - // Validate: non-uniform scale is not supported. + // Build the INSERT's full placement transform (OCS + rotation + + // scale, including non-uniform / mirrored) and its inverse, so + // edits round-trip back to block-local coordinates on SAVE. let sx = insert.x_scale(); let sy = insert.y_scale(); let sz = insert.z_scale(); - if (sx - sy).abs() > 1e-6 || (sx - sz).abs() > 1e-6 { - self.command_line - .push_error("REFEDIT: non-uniform scale inserts are not supported."); - return Task::none(); - } + let forward = insert.get_transform(); + let inverse = { + use acadrust::types::{Matrix3, Matrix4, Transform}; + let ocs_t = + Matrix4::from_matrix3(Matrix3::arbitrary_axis(insert.normal).transpose()); + let t_inv = Matrix4::translation( + -insert.insert_point.x, + -insert.insert_point.y, + -insert.insert_point.z, + ); + let r_inv = Matrix4::rotation_z(-insert.rotation); + let s_inv = Matrix4::scaling(1.0 / sx, 1.0 / sy, 1.0 / sz); + // inverse(OCS·T·R·S) = S⁻¹·R⁻¹·T⁻¹·OCSᵀ + Transform::from_matrix(s_inv * r_inv * t_inv * ocs_t) + }; // Find the block record. let br_handle = match self.tabs[i] @@ -2162,11 +2174,8 @@ impl OpenCADStudio { block_name: insert.block_name.clone(), br_handle, temp_handles: vec![], - insert_x: insert.insert_point.x, - insert_y: insert.insert_point.y, - insert_z: insert.insert_point.z, - rotation_deg: insert.rotation.to_degrees(), - scale: sx, + forward, + inverse, }; self.push_undo_snapshot(i, "REFEDIT"); diff --git a/src/app/update.rs b/src/app/update.rs index bfec28f9a..3af0d4d00 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -4023,8 +4023,12 @@ impl OpenCADStudio { }; let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); let all_wires = self.tabs[i].scene.hit_test_wires(); + // Resolve the double-clicked object — its wire, or (for a + // block/solid with no wire under the cursor) its shaded + // body, which maps to the parent INSERT. let hit = scene::pick::hit_test::click_hit(p, &all_wires[..], vp_mat, bounds) - .and_then(|s| Scene::handle_from_wire_name(s)); + .and_then(|s| Scene::handle_from_wire_name(s)) + .or_else(|| self.tabs[i].scene.solid_click_hit(p, vp_mat, bounds)); if let Some(handle) = hit { // Any text-bearing entity opens its in-place editor // (plain box or rich MText editor, per type). A @@ -4040,6 +4044,19 @@ impl OpenCADStudio { if is_editable_text { return self.begin_text_edit(handle); } + // Double-clicking a block reference enters in-place + // block edit (REFEDIT), so its geometry can be edited + // and the change reflects in every instance. (#136) + let is_insert = matches!( + self.tabs[i].scene.document.get_entity(handle), + Some(AcadEntityType::Insert(_)) + ); + if is_insert && self.tabs[i].refedit_session.is_none() { + return Task::done(Message::Command(format!( + "REFEDIT_BEGIN:{}", + handle.value() + ))); + } } } } diff --git a/src/modules/draw/modify/refedit.rs b/src/modules/draw/modify/refedit.rs index 3b3ec6dad..318b4dcea 100644 --- a/src/modules/draw/modify/refedit.rs +++ b/src/modules/draw/modify/refedit.rs @@ -29,14 +29,12 @@ pub struct RefEditSession { pub br_handle: Handle, /// Handles of the temporary model-space entities added for editing. pub temp_handles: Vec, - // ── INSERT transform (needed for inverse on SAVE) ────────────────── - pub insert_x: f64, - pub insert_y: f64, - pub insert_z: f64, - /// Rotation in degrees (stored as degrees in acadrust). - pub rotation_deg: f64, - /// Uniform scale factor (same for X/Y/Z after validation). - pub scale: f64, + // ── INSERT placement, as full affine transforms ─────────────────── + /// Block-local → world (the INSERT's `get_transform`). Handles OCS, + /// rotation and non-uniform / mirrored scale in one matrix. + pub forward: acadrust::types::Transform, + /// World → block-local, applied on SAVE to bring edits back. + pub inverse: acadrust::types::Transform, } // ── REFEDIT pick command ─────────────────────────────────────────────────── @@ -124,87 +122,16 @@ impl CadCommand for RefCloseCommand { // ── Geometry helpers ─────────────────────────────────────────────────────── /// Apply the INSERT's forward transform to a block-local entity so it -/// appears at its correct world-space position. -/// Order: scale → rotate (around origin) → translate. +/// appears at its correct world-space position. The full affine handles +/// OCS, rotation and non-uniform / mirrored scale at once. pub fn apply_insert_transform(entity: &mut EntityType, session: &RefEditSession) { - use crate::command::EntityTransform; - use crate::scene::view::dispatch; - - let origin = Vec3::ZERO; - - // 1. Uniform scale (if not 1.0) - if (session.scale - 1.0).abs() > 1e-10 { - dispatch::apply_transform( - entity, - &EntityTransform::Scale { - center: origin, - factor: session.scale as f32, - }, - ); - } - - // 2. Rotate around origin - if session.rotation_deg.abs() > 1e-10 { - dispatch::apply_transform( - entity, - &EntityTransform::Rotate { - center: origin, - angle_rad: session.rotation_deg.to_radians() as f32, - }, - ); - } - - // 3. Translate to insert position - dispatch::apply_transform( - entity, - &EntityTransform::Translate(Vec3::new( - session.insert_x as f32, - session.insert_y as f32, - session.insert_z as f32, - )), - ); + entity.as_entity_mut().apply_transform(&session.forward); } /// Apply the INSERT's inverse transform to a world-space entity to bring it /// back to block-local coordinates. -/// Order: un-translate → un-rotate → un-scale. pub fn apply_insert_inverse_transform(entity: &mut EntityType, session: &RefEditSession) { - use crate::command::EntityTransform; - use crate::scene::view::dispatch; - - let origin = Vec3::ZERO; - - // 1. Un-translate - dispatch::apply_transform( - entity, - &EntityTransform::Translate(Vec3::new( - -session.insert_x as f32, - -session.insert_y as f32, - -session.insert_z as f32, - )), - ); - - // 2. Un-rotate - if session.rotation_deg.abs() > 1e-10 { - dispatch::apply_transform( - entity, - &EntityTransform::Rotate { - center: origin, - angle_rad: -session.rotation_deg.to_radians() as f32, - }, - ); - } - - // 3. Un-scale - if (session.scale - 1.0).abs() > 1e-10 && session.scale.abs() > 1e-12 { - dispatch::apply_transform( - entity, - &EntityTransform::Scale { - center: origin, - factor: (1.0 / session.scale) as f32, - }, - ); - } + entity.as_entity_mut().apply_transform(&session.inverse); }