feat(block): double-click to edit a block in place (#136)
Double-clicking a block reference (its wire or its solid body) now enters in-place block edit (REFEDIT), so its geometry can be modified and the change reflects in every instance on REFCLOSE. Also drop REFEDIT's "non-uniform scale not supported" restriction: the session now carries the INSERT's full forward/inverse affine (OCS, rotation and non-uniform / mirrored scale) and applies it via the entity transform, instead of a uniform scale + rotate + translate sequence. Mirrored blocks (e.g. x_scale = -1) are editable now. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
65b8e85751
commit
9f447308a6
3 changed files with 48 additions and 95 deletions
|
|
@ -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 sx = insert.x_scale();
|
||||||
let sy = insert.y_scale();
|
let sy = insert.y_scale();
|
||||||
let sz = insert.z_scale();
|
let sz = insert.z_scale();
|
||||||
if (sx - sy).abs() > 1e-6 || (sx - sz).abs() > 1e-6 {
|
let forward = insert.get_transform();
|
||||||
self.command_line
|
let inverse = {
|
||||||
.push_error("REFEDIT: non-uniform scale inserts are not supported.");
|
use acadrust::types::{Matrix3, Matrix4, Transform};
|
||||||
return Task::none();
|
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.
|
// Find the block record.
|
||||||
let br_handle = match self.tabs[i]
|
let br_handle = match self.tabs[i]
|
||||||
|
|
@ -2162,11 +2174,8 @@ impl OpenCADStudio {
|
||||||
block_name: insert.block_name.clone(),
|
block_name: insert.block_name.clone(),
|
||||||
br_handle,
|
br_handle,
|
||||||
temp_handles: vec![],
|
temp_handles: vec![],
|
||||||
insert_x: insert.insert_point.x,
|
forward,
|
||||||
insert_y: insert.insert_point.y,
|
inverse,
|
||||||
insert_z: insert.insert_point.z,
|
|
||||||
rotation_deg: insert.rotation.to_degrees(),
|
|
||||||
scale: sx,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.push_undo_snapshot(i, "REFEDIT");
|
self.push_undo_snapshot(i, "REFEDIT");
|
||||||
|
|
|
||||||
|
|
@ -4023,8 +4023,12 @@ impl OpenCADStudio {
|
||||||
};
|
};
|
||||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
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)
|
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 {
|
if let Some(handle) = hit {
|
||||||
// Any text-bearing entity opens its in-place editor
|
// Any text-bearing entity opens its in-place editor
|
||||||
// (plain box or rich MText editor, per type). A
|
// (plain box or rich MText editor, per type). A
|
||||||
|
|
@ -4040,6 +4044,19 @@ impl OpenCADStudio {
|
||||||
if is_editable_text {
|
if is_editable_text {
|
||||||
return self.begin_text_edit(handle);
|
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()
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,14 +29,12 @@ pub struct RefEditSession {
|
||||||
pub br_handle: Handle,
|
pub br_handle: Handle,
|
||||||
/// Handles of the temporary model-space entities added for editing.
|
/// Handles of the temporary model-space entities added for editing.
|
||||||
pub temp_handles: Vec<Handle>,
|
pub temp_handles: Vec<Handle>,
|
||||||
// ── INSERT transform (needed for inverse on SAVE) ──────────────────
|
// ── INSERT placement, as full affine transforms ───────────────────
|
||||||
pub insert_x: f64,
|
/// Block-local → world (the INSERT's `get_transform`). Handles OCS,
|
||||||
pub insert_y: f64,
|
/// rotation and non-uniform / mirrored scale in one matrix.
|
||||||
pub insert_z: f64,
|
pub forward: acadrust::types::Transform,
|
||||||
/// Rotation in degrees (stored as degrees in acadrust).
|
/// World → block-local, applied on SAVE to bring edits back.
|
||||||
pub rotation_deg: f64,
|
pub inverse: acadrust::types::Transform,
|
||||||
/// Uniform scale factor (same for X/Y/Z after validation).
|
|
||||||
pub scale: f64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── REFEDIT pick command ───────────────────────────────────────────────────
|
// ── REFEDIT pick command ───────────────────────────────────────────────────
|
||||||
|
|
@ -124,87 +122,16 @@ impl CadCommand for RefCloseCommand {
|
||||||
// ── Geometry helpers ───────────────────────────────────────────────────────
|
// ── Geometry helpers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Apply the INSERT's forward transform to a block-local entity so it
|
/// Apply the INSERT's forward transform to a block-local entity so it
|
||||||
/// appears at its correct world-space position.
|
/// appears at its correct world-space position. The full affine handles
|
||||||
/// Order: scale → rotate (around origin) → translate.
|
/// OCS, rotation and non-uniform / mirrored scale at once.
|
||||||
pub fn apply_insert_transform(entity: &mut EntityType, session: &RefEditSession) {
|
pub fn apply_insert_transform(entity: &mut EntityType, session: &RefEditSession) {
|
||||||
use crate::command::EntityTransform;
|
entity.as_entity_mut().apply_transform(&session.forward);
|
||||||
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,
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply the INSERT's inverse transform to a world-space entity to bring it
|
/// Apply the INSERT's inverse transform to a world-space entity to bring it
|
||||||
/// back to block-local coordinates.
|
/// back to block-local coordinates.
|
||||||
/// Order: un-translate → un-rotate → un-scale.
|
|
||||||
pub fn apply_insert_inverse_transform(entity: &mut EntityType, session: &RefEditSession) {
|
pub fn apply_insert_inverse_transform(entity: &mut EntityType, session: &RefEditSession) {
|
||||||
use crate::command::EntityTransform;
|
entity.as_entity_mut().apply_transform(&session.inverse);
|
||||||
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,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue