diff --git a/ROADMAP.md b/ROADMAP.md index 152061f6c..e3fd5747e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -129,7 +129,7 @@ Underlay (PDF/DWF/DGN) | HATCHEDIT | ✅ | — | | ATTEDIT | ✅ Interactive tag-by-tag value editing | — | | DDEDIT (çift tık metin) | ✅ | — | -| REFEDIT | ⬜ | Block in-place düzenleme | +| REFEDIT | ✅ | Block in-place düzenleme | | DIVIDE (DIV) | ✅ | — | | MEASURE (ME) | ✅ | — | @@ -225,7 +225,7 @@ Underlay (PDF/DWF/DGN) | 3D Sphere primitive | ✅ | | 3D Cylinder primitive | ✅ | | OBJ dosyası içe aktarma | ✅ | -| REFEDIT (block yerinde düzenleme) | ⬜ | +| REFEDIT (block yerinde düzenleme) | ✅ | | WBLOCK (bloğu dış dosyaya yaz) | ✅ | | Attributeli INSERT akışı (ATTREQ) | ✅ | diff --git a/src/app/commands.rs b/src/app/commands.rs index d1745c406..b8fa4d5a0 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -1472,6 +1472,223 @@ impl H7CAD { self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); } + // ── REFEDIT — in-place block editing ───────────────────────────── + "REFEDIT" => { + use crate::modules::home::modify::refedit::RefEditPickCommand; + // If a session is already active, tell the user. + if self.tabs[i].refedit_session.is_some() { + self.command_line.push_error( + "REFEDIT: a session is already active. Use REFCLOSE first." + ); + } else { + // Check if a single INSERT is already selected. + let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect(); + if selected.len() == 1 { + if let Some(acadrust::EntityType::Insert(_)) = selected.first().map(|(_, e)| e) { + let handle = selected[0].0; + // Skip pick phase — jump straight to begin. + let _ = self.dispatch_command(&format!("REFEDIT_BEGIN:{}", handle.value())); + return Task::none(); + } + } + let cmd_obj = RefEditPickCommand::new(); + self.command_line.push_info(&cmd_obj.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); + } + } + + cmd if cmd.starts_with("REFEDIT_BEGIN:") => { + use crate::modules::home::modify::refedit::{RefEditSession, apply_insert_transform}; + use acadrust::Handle; + + let handle_u64: u64 = cmd["REFEDIT_BEGIN:".len()..] + .parse().unwrap_or(0); + let insert_handle = Handle::new(handle_u64); + + // Get INSERT entity. + let insert = match self.tabs[i].scene.document.get_entity(insert_handle) { + Some(acadrust::EntityType::Insert(ins)) => ins.clone(), + _ => { + self.command_line.push_error("REFEDIT: selected object is not an INSERT."); + return Task::none(); + } + }; + + // Validate: non-uniform scale is not supported. + 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(); + } + + // Find the block record. + let br_handle = match self.tabs[i].scene.document.block_records.get(&insert.block_name) { + Some(br) => br.handle, + None => { + self.command_line.push_error(&format!( + "REFEDIT: block \"{}\" not found.", insert.block_name + )); + return Task::none(); + } + }; + + // Collect block-local entities (skip structural Block/BlockEnd/AttDef). + let block_entities: Vec<_> = { + let br = self.tabs[i].scene.document.block_records.get(&insert.block_name).unwrap(); + br.entity_handles + .iter() + .filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned()) + .filter(|e| !matches!(e, + acadrust::EntityType::Block(_) | + acadrust::EntityType::BlockEnd(_) | + acadrust::EntityType::AttributeDefinition(_) + )) + .collect() + }; + + if block_entities.is_empty() { + self.command_line.push_error("REFEDIT: block is empty."); + return Task::none(); + } + + let session = RefEditSession { + insert_handle, + 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, + }; + + self.push_undo_snapshot(i, "REFEDIT"); + self.tabs[i].refedit_session = Some(session.clone()); + + // Add block entities to model space with INSERT transform applied. + let mut temp_handles = Vec::new(); + for mut entity in block_entities { + apply_insert_transform(&mut entity, &session); + entity.common_mut().handle = acadrust::Handle::NULL; + entity.common_mut().owner_handle = acadrust::Handle::NULL; + let h = self.tabs[i].scene.add_entity(entity); + temp_handles.push(h); + } + self.tabs[i].refedit_session.as_mut().unwrap().temp_handles = temp_handles.clone(); + + // Select the temp entities so user can see what they're editing. + self.tabs[i].scene.deselect_all(); + for h in &temp_handles { + self.tabs[i].scene.select_entity(*h, false); + } + self.tabs[i].dirty = true; + + self.command_line.push_info(&format!( + "REFEDIT: Editing block \"{}\". Use REFCLOSE when done.", + insert.block_name + )); + use crate::modules::home::modify::refedit::RefCloseCommand; + let cmd_obj = RefCloseCommand::new(); + self.command_line.push_info(&cmd_obj.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); + } + + "REFCLOSE" => { + if self.tabs[i].refedit_session.is_some() { + use crate::modules::home::modify::refedit::RefCloseCommand; + let cmd_obj = RefCloseCommand::new(); + self.command_line.push_info(&cmd_obj.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); + } else { + self.command_line.push_error("REFCLOSE: no REFEDIT session active."); + } + } + + "REFCLOSE_SAVE" => { + use crate::modules::home::modify::refedit::apply_insert_inverse_transform; + use crate::modules::home::modify::explode::normalize_entity_for_block; + + let session = match self.tabs[i].refedit_session.take() { + Some(s) => s, + None => { + self.command_line.push_error("REFCLOSE: no REFEDIT session active."); + return Task::none(); + } + }; + + self.push_undo_snapshot(i, "REFCLOSE"); + + // Collect the edited temp entities. + let mut new_entities: Vec = session.temp_handles + .iter() + .filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned()) + .collect(); + + // Remove temp entities from model space. + self.tabs[i].scene.erase_entities(&session.temp_handles); + + // Apply inverse INSERT transform → block-local coordinates. + let new_entities: Vec<_> = new_entities + .into_iter() + .map(|mut entity| { + apply_insert_inverse_transform(&mut entity, &session); + let mut entity = normalize_entity_for_block(entity); + entity.common_mut().handle = acadrust::Handle::NULL; + entity.common_mut().owner_handle = session.br_handle; + entity + }) + .collect(); + + // Remove old block entities from the document. + let old_handles: Vec<_> = match self.tabs[i].scene.document + .block_records.get(&session.block_name) + { + Some(br) => br.entity_handles.clone(), + None => vec![], + }; + for h in &old_handles { + self.tabs[i].scene.document.remove_entity(*h); + } + // Flush the entity_handles list from the block record. + if let Some(br) = self.tabs[i].scene.document + .block_records.get_mut(&session.block_name) + { + br.entity_handles.clear(); + } + + // Add the new block entities. + for entity in new_entities { + let _ = self.tabs[i].scene.document.add_entity(entity); + } + + self.tabs[i].dirty = true; + self.command_line.push_output(&format!( + "REFCLOSE: Block \"{}\" saved. All references updated.", + session.block_name + )); + // Rebuild hatch/image/mesh caches since block content changed. + self.tabs[i].scene.rebuild_derived_caches(); + } + + "REFCLOSE_DISCARD" => { + let session = match self.tabs[i].refedit_session.take() { + Some(s) => s, + None => { + self.command_line.push_error("REFCLOSE: no REFEDIT session active."); + return Task::none(); + } + }; + // Remove temp entities without modifying the block. + self.tabs[i].scene.erase_entities(&session.temp_handles); + self.tabs[i].scene.deselect_all(); + self.command_line.push_output("REFCLOSE: Changes discarded."); + } + "ALIGN"|"AL" => { use crate::modules::home::modify::align::AlignCommand; let cmd = AlignCommand::new(); diff --git a/src/app/document.rs b/src/app/document.rs index 8f7a985da..78dd8886d 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -4,6 +4,7 @@ use crate::command::CadCommand; use crate::snap::SnapResult; use crate::scene::grip::GripEdit; use crate::scene::GripDef; +use crate::modules::home::modify::refedit::RefEditSession; use acadrust::{CadDocument, Handle}; use acadrust::tables::Ucs; use crate::linetypes; @@ -37,6 +38,8 @@ pub(super) struct DocumentTab { pub(super) bg_color: Option<[f32; 4]>, /// Custom paper-space background color. `None` = default off-white grey. pub(super) paper_bg_color: Option<[f32; 4]>, + /// Active REFEDIT session, if any. + pub(super) refedit_session: Option, } impl DocumentTab { @@ -65,6 +68,7 @@ impl DocumentTab { active_ucs: None, bg_color: None, paper_bg_color: None, + refedit_session: None, } } diff --git a/src/modules/home/modify/mod.rs b/src/modules/home/modify/mod.rs index fe0ef897e..9613a8135 100644 --- a/src/modules/home/modify/mod.rs +++ b/src/modules/home/modify/mod.rs @@ -18,4 +18,5 @@ pub mod stretch; pub mod translate; pub mod spline_ops; pub mod splinedit; +pub mod refedit; pub mod trim; diff --git a/src/modules/home/modify/refedit.rs b/src/modules/home/modify/refedit.rs new file mode 100644 index 000000000..c3c8e2187 --- /dev/null +++ b/src/modules/home/modify/refedit.rs @@ -0,0 +1,210 @@ +// REFEDIT — in-place block reference editing. +// +// Workflow: +// 1. REFEDIT: user picks an INSERT entity. +// 2. The block's entities are copied into model space with the INSERT +// transform applied (translate + rotate + uniform scale). +// 3. The user edits them with normal commands (MOVE, COPY, DELETE, …). +// 4. REFCLOSE SAVE: temp entities are inverse-transformed back into the +// block definition; all INSERT references auto-update. +// REFCLOSE DISCARD: temp entities are removed, block unchanged. +// +// Limitation: non-uniform scale inserts (x_scale ≠ y_scale) are rejected +// with an error message — full matrix inversion for those cases would +// require per-entity matrix transforms not yet in EntityTransform. + +use acadrust::{EntityType, Handle}; +use glam::Vec3; + +use crate::command::{CadCommand, CmdResult}; + +// ── Session state (held in DocumentTab) ─────────────────────────────────── + +/// Active REFEDIT session. Lives in `DocumentTab::refedit_session`. +#[derive(Debug, Clone)] +pub struct RefEditSession { + /// The INSERT entity being edited. + pub insert_handle: Handle, + /// Name of the block being edited. + pub block_name: String, + /// Handle of the block record (owns the block entities). + 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, +} + +// ── REFEDIT pick command ─────────────────────────────────────────────────── + +/// Step 1: wait for the user to pick a single INSERT entity. +pub struct RefEditPickCommand; + +impl RefEditPickCommand { + pub fn new() -> Self { + Self + } +} + +impl CadCommand for RefEditPickCommand { + fn name(&self) -> &'static str { + "REFEDIT" + } + fn prompt(&self) -> String { + "REFEDIT Select block reference to edit:".into() + } + + fn needs_entity_pick(&self) -> bool { + true + } + + fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> CmdResult { + if handle.is_null() { + return CmdResult::NeedPoint; + } + // Signal the host to enter the editing session for this handle. + // We reuse Relaunch("REFEDIT_BEGIN:") as a convention. + CmdResult::Relaunch( + format!("REFEDIT_BEGIN:{}", handle.value()), + vec![handle], + ) + } + + fn on_point(&mut self, _pt: Vec3) -> CmdResult { + CmdResult::NeedPoint + } + fn on_enter(&mut self) -> CmdResult { + CmdResult::Cancel + } +} + +// ── REFCLOSE command ─────────────────────────────────────────────────────── + +/// Step 4: prompt for SAVE or DISCARD. +pub struct RefCloseCommand; + +impl RefCloseCommand { + pub fn new() -> Self { + Self + } +} + +impl CadCommand for RefCloseCommand { + fn name(&self) -> &'static str { + "REFCLOSE" + } + fn prompt(&self) -> String { + "REFCLOSE [Save/Discard] :".into() + } + fn wants_text_input(&self) -> bool { + true + } + fn on_text_input(&mut self, text: &str) -> Option { + let t = text.trim().to_uppercase(); + let save = t.is_empty() || t == "S" || t == "SAVE"; + let discard = t == "D" || t == "DISCARD"; + if save { + Some(CmdResult::Relaunch("REFCLOSE_SAVE".into(), vec![])) + } else if discard { + Some(CmdResult::Relaunch("REFCLOSE_DISCARD".into(), vec![])) + } else { + Some(CmdResult::NeedPoint) // re-prompt + } + } + fn on_enter(&mut self) -> CmdResult { + // Default: SAVE + CmdResult::Relaunch("REFCLOSE_SAVE".into(), vec![]) + } + fn on_point(&mut self, _pt: Vec3) -> CmdResult { + CmdResult::NeedPoint + } +} + +// ── 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. +pub fn apply_insert_transform(entity: &mut EntityType, session: &RefEditSession) { + use crate::command::EntityTransform; + use crate::scene::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 +/// 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::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, + }, + ); + } +} diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 968e0bbac..ef1d4e844 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1600,6 +1600,14 @@ impl Scene { } } + /// Rebuild hatch / image / mesh caches after the document is modified + /// outside the normal `add_entity` path (e.g. REFCLOSE SAVE). + pub fn rebuild_derived_caches(&mut self) { + self.populate_hatches_from_document(); + self.populate_images_from_document(); + self.populate_meshes_from_document(); + } + /// Build a solid-fill HatchModel for a DXF Solid entity. /// DXF SOLID corners are in "Z-order": p0-p1 top, p2-p3 bottom. /// Visual quad is p0→p1→p3→p2 (closed).