diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs new file mode 100644 index 00000000..8de0638d --- /dev/null +++ b/src/app/cmd_result.rs @@ -0,0 +1,305 @@ +use super::{H7CAD, Message}; +use crate::command::CmdResult; +use acadrust::Handle; +use iced::Task; + +impl H7CAD { + pub(super) fn apply_cmd_result(&mut self, result: CmdResult) -> Task { + let i = self.active_tab; + match result { + CmdResult::NeedPoint => { + let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); + if let Some(p) = prompt { + self.command_line.push_info(&p); + } + } + CmdResult::Preview(wire) => { + self.tabs[i].scene.set_preview_wires(vec![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); + } + } + CmdResult::InterimWire(wire) => { + self.tabs[i].scene.set_interim_wire(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); + } + } + CmdResult::CommitEntity(entity) => { + let label = self.history_label_from_active_cmd(i, "ENTITY"); + self.push_undo_snapshot(i, label); + self.commit_entity(entity); + self.tabs[i].dirty = true; + let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); + if let Some(p) = prompt { + self.command_line.push_info(&p); + } + } + CmdResult::TransformSelected(handles, transform) => { + let label = self.history_label_from_active_cmd(i, "MOVE"); + self.push_undo_snapshot(i, label); + self.tabs[i].scene.transform_entities(&handles, &transform); + 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(); + } + CmdResult::CopySelected(handles, transform) => { + let label = self.history_label_from_active_cmd(i, "COPY"); + self.push_undo_snapshot(i, label); + 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(); + } + CmdResult::CommitAndExit(entity) => { + let label = self.history_label_from_active_cmd(i, "ENTITY"); + self.push_undo_snapshot(i, label); + self.commit_entity(entity); + 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(); + } + CmdResult::CreateBlock { handles, name, base } => { + self.push_undo_snapshot(i, "BLOCK"); + match self.tabs[i].scene.create_block_from_entities(&handles, &name, base) { + Ok(insert_handle) => { + self.tabs[i].dirty = true; + self.tabs[i].scene.deselect_all(); + if !insert_handle.is_null() { + self.tabs[i].scene.select_entity(insert_handle, false); + } + self.tabs[i].scene.clear_preview_wire(); + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.command_line + .push_output(&format!("Block \"{name}\" created.")); + self.refresh_properties(); + } + Err(err) => { + let _ = self.tabs[i].history.undo_stack.pop(); + self.command_line.push_error(&err); + let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); + if let Some(p) = prompt { + self.command_line.push_info(&p); + } + } + } + } + CmdResult::CommitHatch(hatch) => { + let label = self.history_label_from_active_cmd(i, "HATCH"); + self.push_undo_snapshot(i, label); + let new_handle = self.tabs[i].scene.add_hatch(hatch); + if !new_handle.is_null() { + self.tabs[i].scene.select_entity(new_handle, true); + } + 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(); + } + CmdResult::BatchCopy(handles, transforms) => { + let label = self.history_label_from_active_cmd(i, "ARRAY"); + self.push_undo_snapshot(i, label); + let count = transforms.len(); + for t in &transforms { + self.tabs[i].scene.copy_entities(&handles, t); + } + 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.command_line + .push_output(&format!("ARRAY: {count} copies created.")); + self.refresh_properties(); + } + CmdResult::ReplaceMany(replacements, additions) => { + let label = self.history_label_from_active_cmd(i, "FILLET"); + self.push_undo_snapshot(i, label); + for (handle, entities) in replacements { + self.tabs[i].scene.erase_entities(&[handle]); + for entity in entities { + self.tabs[i].scene.add_entity(entity); + } + } + for entity in additions { + self.tabs[i].scene.add_entity(entity); + } + 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.refresh_properties(); + } + CmdResult::ReplaceEntity(handle, new_entities) => { + let label = self.history_label_from_active_cmd(i, "TRIM"); + self.push_undo_snapshot(i, label); + self.tabs[i].scene.erase_entities(&[handle]); + let new_handles: Vec = new_entities + .into_iter() + .map(|e| self.tabs[i].scene.add_entity(e)) + .collect(); + if let Some(cmd) = &mut self.tabs[i].active_cmd { + cmd.on_entity_replaced(handle, &new_handles); + } + self.tabs[i].dirty = true; + let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); + if let Some(p) = prompt { + self.command_line.push_info(&p); + } + } + CmdResult::Cancel => { + 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_info("Command cancelled."); + } + CmdResult::Relaunch(cmd, handles) => { + self.tabs[i].scene.deselect_all(); + for h in &handles { + self.tabs[i].scene.select_entity(*h, false); + } + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + self.restore_pre_cmd_tangent(); + let _ = self.dispatch_command(&cmd); + } + CmdResult::MatchEntityLayer { dest, src } => { + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + let src_layer = self.tabs[i].scene.document + .get_entity(src) + .map(|e| e.common().layer.clone()); + if let Some(layer) = src_layer { + self.push_undo_snapshot(i, "LAYMATCH"); + for h in &dest { + if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) { + e.as_entity_mut().set_layer(layer.clone()); + } + } + self.tabs[i].dirty = true; + self.command_line.push_info(&format!("Layer matched to \"{layer}\".")); + self.sync_ribbon_layers(); + } else { + self.command_line.push_error("Source object not found."); + } + } + CmdResult::MatchProperties { dest, src } => { + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + + let props = self.tabs[i].scene.document.get_entity(src).map(|e| { + let c = e.common(); + (c.layer.clone(), c.color, c.linetype.clone(), c.linetype_scale, c.line_weight) + }); + + if let Some((layer, color, linetype, lt_scale, lw)) = props { + self.push_undo_snapshot(i, "MATCHPROP"); + for h in &dest { + if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) { + e.as_entity_mut().set_layer(layer.clone()); + crate::scene::dispatch::apply_color(e, color); + crate::scene::dispatch::apply_line_weight(e, lw); + e.common_mut().linetype = linetype.clone(); + e.common_mut().linetype_scale = lt_scale; + } + } + self.tabs[i].dirty = true; + self.refresh_properties(); + self.command_line.push_info( + &format!("Properties matched to {} object(s).", dest.len()) + ); + } else { + self.command_line.push_error("Source object not found."); + } + } + CmdResult::PasteClipboard { base_pt } => { + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + if self.clipboard.is_empty() { + self.command_line.push_error("Clipboard is empty."); + } else { + let delta = base_pt - self.clipboard_centroid; + let translate = crate::command::EntityTransform::Translate(delta); + self.push_undo_snapshot(i, "PASTECLIP"); + let count = self.clipboard.len(); + let new_handles: Vec = self.clipboard.clone() + .into_iter() + .map(|mut entity| { + crate::scene::dispatch::apply_transform(&mut entity, &translate); + entity.common_mut().handle = acadrust::Handle::NULL; + self.tabs[i].scene.add_entity(entity) + }) + .filter(|h| !h.is_null()) + .collect(); + self.tabs[i].scene.deselect_all(); + for h in new_handles { + self.tabs[i].scene.select_entity(h, false); + } + self.tabs[i].dirty = true; + self.refresh_properties(); + self.command_line.push_info(&format!("{count} object(s) pasted.")); + } + } + CmdResult::CreateGroup { handles, name } => { + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + self.push_undo_snapshot(i, "GROUP"); + self.tabs[i].scene.create_group(name.clone(), handles); + self.tabs[i].dirty = true; + self.command_line.push_info(&format!("Group \"{}\" created.", name)); + } + CmdResult::DeleteGroups { handles } => { + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].scene.clear_preview_wire(); + self.push_undo_snapshot(i, "UNGROUP"); + let count = self.tabs[i].scene.delete_groups_containing(&handles); + self.tabs[i].dirty = true; + if count > 0 { + self.command_line.push_info(&format!("{} group(s) dissolved.", count)); + } else { + self.command_line.push_info("No groups found for selected objects."); + } + } + } + // Focus the command-line input while a command is active; blur it when the command ends. + if self.tabs[i].active_cmd.is_some() { + self.focus_cmd_input() + } else { + self.blur_cmd_input() + } + } + + /// Restore the tangent snap state that was in effect before the command started. + fn restore_pre_cmd_tangent(&mut self) { + if let Some(was_on) = self.pre_cmd_tangent.take() { + if !was_on { + self.snapper.enabled.remove(&crate::snap::SnapType::Tangent); + } + } + } +} diff --git a/src/app/commands.rs b/src/app/commands.rs new file mode 100644 index 00000000..16e43971 --- /dev/null +++ b/src/app/commands.rs @@ -0,0 +1,913 @@ +use super::{H7CAD, Message}; +use crate::command::CadCommand; +use crate::scene::Scene; +use iced::Task; +use std::path::PathBuf; + +impl H7CAD { + pub(super) fn dispatch_command(&mut self, cmd: &str) -> Task { + let i = self.active_tab; + // Cancel any running command before starting a new one. + if self.tabs[i].active_cmd.is_some() { + self.tabs[i].scene.clear_preview_wire(); + self.tabs[i].active_cmd = None; + } + + if let Some(path_str) = cmd.strip_prefix("OPEN_RECENT:") { + let path = PathBuf::from(path_str); + return Task::perform(crate::io::open_path(path), Message::FileOpened); + } + + match cmd { + "NEW" => return Task::done(Message::ClearScene), + "OPEN" => return Task::done(Message::OpenFile), + "SAVE"|"QSAVE" => return Task::done(Message::SaveFile), + "SAVEAS" => return Task::done(Message::SaveAs), + "UNDO"|"U" => return Task::done(Message::Undo), + "REDO" => return Task::done(Message::Redo), + "CLEAR"|"CLR" => return Task::done(Message::ClearScene), + "WIREFRAME"|"VW" => return Task::done(Message::SetWireframe(true)), + "SOLID"|"VS" => return Task::done(Message::SetWireframe(false)), + "ORTHO" => return Task::done(Message::SetProjection(true)), + "PERSP" => return Task::done(Message::SetProjection(false)), + "LAYERS"|"LA" => return Task::done(Message::ToggleLayers), + + // ── Layer object commands ────────────────────────────────────── + "LAYOFF" => { + let handles: Vec<_> = self.tabs[i].scene.selected_entities() + .into_iter().map(|(h, _)| h).collect(); + if handles.is_empty() { + use crate::command::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("LAYOFF"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + let layers: std::collections::HashSet = self.tabs[i].scene + .selected_entities().into_iter() + .map(|(_, e)| e.common().layer.clone()).collect(); + self.push_undo_snapshot(i, "LAYOFF"); + for name in &layers { + if name == "0" { continue; } + if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) { + dl.turn_off(); + } + } + self.tabs[i].dirty = true; + self.sync_ribbon_layers(); + self.command_line.push_info("Layer(s) turned off."); + } + } + + "LAYFRZ" => { + let handles: Vec<_> = self.tabs[i].scene.selected_entities() + .into_iter().map(|(h, _)| h).collect(); + if handles.is_empty() { + use crate::command::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("LAYFRZ"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + let layers: std::collections::HashSet = self.tabs[i].scene + .selected_entities().into_iter() + .map(|(_, e)| e.common().layer.clone()).collect(); + self.push_undo_snapshot(i, "LAYFRZ"); + for name in &layers { + if name == "0" { continue; } + if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) { + dl.freeze(); + } + } + self.tabs[i].dirty = true; + self.sync_ribbon_layers(); + self.command_line.push_info("Layer(s) frozen."); + } + } + + "LAYLCK" => { + let handles: Vec<_> = self.tabs[i].scene.selected_entities() + .into_iter().map(|(h, _)| h).collect(); + if handles.is_empty() { + use crate::command::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("LAYLCK"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + let layers: std::collections::HashSet = self.tabs[i].scene + .selected_entities().into_iter() + .map(|(_, e)| e.common().layer.clone()).collect(); + self.push_undo_snapshot(i, "LAYLCK"); + for name in &layers { + if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) { + dl.lock(); + } + } + self.tabs[i].dirty = true; + self.sync_ribbon_layers(); + self.command_line.push_info("Layer(s) locked."); + } + } + + "LAYMCUR" => { + let entities = self.tabs[i].scene.selected_entities(); + if entities.is_empty() { + use crate::command::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("LAYMCUR"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + let layer = entities[0].1.common().layer.clone(); + self.tabs[i].active_layer = layer.clone(); + self.ribbon.active_layer = layer.clone(); + self.tabs[i].layers.current_layer = layer.clone(); + self.command_line.push_info(&format!("Current layer set to \"{layer}\".")); + self.sync_ribbon_layers(); + } + } + + "LAYON" => { + self.push_undo_snapshot(i, "LAYON"); + for name in self.tabs[i].scene.document.layers.iter() + .map(|l| l.name.clone()).collect::>() + { + if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { + dl.turn_on(); + } + } + self.tabs[i].dirty = true; + self.sync_ribbon_layers(); + self.command_line.push_info("All layers turned on."); + } + + "LAYTHW" => { + self.push_undo_snapshot(i, "LAYTHW"); + for name in self.tabs[i].scene.document.layers.iter() + .map(|l| l.name.clone()).collect::>() + { + if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { + dl.thaw(); + } + } + self.tabs[i].dirty = true; + self.sync_ribbon_layers(); + self.command_line.push_info("All layers thawed."); + } + + "LAYULK" => { + let handles: Vec<_> = self.tabs[i].scene.selected_entities() + .into_iter().map(|(h, _)| h).collect(); + if handles.is_empty() { + use crate::command::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("LAYULK"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + let layers: std::collections::HashSet = self.tabs[i].scene + .selected_entities().into_iter() + .map(|(_, e)| e.common().layer.clone()).collect(); + self.push_undo_snapshot(i, "LAYULK"); + for name in &layers { + if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) { + dl.unlock(); + } + } + self.tabs[i].dirty = true; + self.sync_ribbon_layers(); + self.command_line.push_info("Layer(s) unlocked."); + } + } + + "LAYMATCH"|"LAYMCH" => { + use crate::command::laymatch::LayMatchCommand; + let dest: Vec<_> = self.tabs[i].scene.selected_entities() + .into_iter().map(|(h, _)| h).collect(); + let cmd = LayMatchCommand::new(dest); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + + "MATCHPROP"|"MA" => { + use crate::command::matchprop::MatchPropCommand; + self.tabs[i].scene.deselect_all(); + let cmd = MatchPropCommand::new(); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + + "GROUP"|"G" => { + let handles: Vec<_> = self.tabs[i] + .scene.selected_entities().into_iter().map(|(h, _)| h).collect(); + if handles.is_empty() { + use crate::command::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("GROUP"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + let auto_name = super::helpers::next_group_auto_name(&self.tabs[i].scene); + use crate::command::group::GroupCommand; + let cmd = GroupCommand::new(handles, auto_name); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + } + + "UNGROUP"|"UG" => { + let handles: Vec<_> = self.tabs[i] + .scene.selected_entities().into_iter().map(|(h, _)| h).collect(); + if handles.is_empty() { + use crate::command::ungroup::UngroupCommand; + let cmd = UngroupCommand::new(); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + self.push_undo_snapshot(i, "UNGROUP"); + let count = self.tabs[i].scene.delete_groups_containing(&handles); + self.tabs[i].dirty = true; + if count > 0 { + self.command_line.push_info(&format!("{} group(s) dissolved.", count)); + } else { + self.command_line.push_info("No groups found for selected objects."); + } + } + } + + "COPYCLIP"|"CC" => { + let handles: Vec<_> = self.tabs[i] + .scene.selected_entities().into_iter().map(|(h, _)| h).collect(); + if handles.is_empty() { + use crate::command::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("COPYCLIP"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + let entities: Vec<_> = handles.iter() + .filter_map(|&h| self.tabs[i].scene.document.get_entity(h).cloned()) + .collect(); + self.clipboard_centroid = super::helpers::entities_centroid( + &self.tabs[i].scene.wire_models_for(&handles), + ); + self.clipboard = entities; + self.command_line.push_info( + &format!("{} object(s) copied to clipboard.", self.clipboard.len()), + ); + } + } + + "CUTCLIP"|"CX" => { + let handles: Vec<_> = self.tabs[i] + .scene.selected_entities().into_iter().map(|(h, _)| h).collect(); + if handles.is_empty() { + use crate::command::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("CUTCLIP"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + let entities: Vec<_> = handles.iter() + .filter_map(|&h| self.tabs[i].scene.document.get_entity(h).cloned()) + .collect(); + self.clipboard_centroid = super::helpers::entities_centroid( + &self.tabs[i].scene.wire_models_for(&handles), + ); + let count = entities.len(); + self.clipboard = entities; + self.push_undo_snapshot(i, "CUTCLIP"); + self.tabs[i].scene.erase_entities(&handles); + self.tabs[i].scene.deselect_all(); + self.tabs[i].dirty = true; + self.refresh_properties(); + self.command_line.push_info( + &format!("{} object(s) cut to clipboard.", count), + ); + } + } + + "PASTECLIP"|"PC" => { + if self.clipboard.is_empty() { + self.command_line.push_error("Clipboard is empty."); + } else { + let wires = self.tabs[i].scene.wires_for_entities(&self.clipboard); + use crate::command::paste::PasteCommand; + let cmd = PasteCommand::new(wires, self.clipboard_centroid); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + } + + "BLOCK" => { + let handles: Vec<_> = self.tabs[i] + .scene.selected_entities().into_iter().map(|(h, _)| h).collect(); + if handles.is_empty() { + use crate::command::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("BLOCK"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + use crate::modules::insert::create_block::CreateBlockCommand; + let cmd = CreateBlockCommand::new(handles); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + } + + "INSERT" => { + let blocks = self.tabs[i].scene.custom_block_names(); + if blocks.is_empty() { + self.command_line + .push_error("No user-defined blocks found in this drawing."); + } else { + use crate::modules::insert::insert_block::InsertBlockCommand; + let cmd = InsertBlockCommand::new(blocks); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } + } + + // ── Draw commands ────────────────────────────────────────────── + "LINE"|"L" => { + use crate::modules::home::draw::line::LineCommand; + let new_cmd = LineCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "CIRCLE"|"C" => { + use crate::modules::home::draw::circle::CircleCommand; + let new_cmd = CircleCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "CIRCLE_CD" => { + use crate::modules::home::draw::circle::CircleCDCommand; + let new_cmd = CircleCDCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "CIRCLE_2P" => { + use crate::modules::home::draw::circle::Circle2PCommand; + let new_cmd = Circle2PCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "CIRCLE_3P" => { + use crate::modules::home::draw::circle::Circle3PCommand; + let new_cmd = Circle3PCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "CIRCLE_TTR" => { + use crate::modules::home::draw::circle::CircleTTRCommand; + let new_cmd = CircleTTRCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.pre_cmd_tangent = Some(self.snapper.is_on(crate::snap::SnapType::Tangent)); + self.snapper.enabled.insert(crate::snap::SnapType::Tangent); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "CIRCLE_TTT" => { + use crate::modules::home::draw::circle::CircleTTTCommand; + let new_cmd = CircleTTTCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.pre_cmd_tangent = Some(self.snapper.is_on(crate::snap::SnapType::Tangent)); + self.snapper.enabled.insert(crate::snap::SnapType::Tangent); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "ARC"|"A" => { + use crate::modules::home::draw::arc::ArcCommand; + let new_cmd = ArcCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "ARC_3P" => { + use crate::modules::home::draw::arc::Arc3PCommand; + let new_cmd = Arc3PCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "ARC_SCE" => { + use crate::modules::home::draw::arc::ArcSCECommand; + let new_cmd = ArcSCECommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "ARC_SCA" => { + use crate::modules::home::draw::arc::ArcSCACommand; + let new_cmd = ArcSCACommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "ARC_SCL" => { + use crate::modules::home::draw::arc::ArcSCLCommand; + let new_cmd = ArcSCLCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "ARC_SEA" => { + use crate::modules::home::draw::arc::ArcSEACommand; + let new_cmd = ArcSEACommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "ARC_SER" => { + use crate::modules::home::draw::arc::ArcSERCommand; + let new_cmd = ArcSERCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "ARC_SED" => { + use crate::modules::home::draw::arc::ArcSEDCommand; + let new_cmd = ArcSEDCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "ARC_CSA" => { + use crate::modules::home::draw::arc::ArcCSACommand; + let new_cmd = ArcCSACommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "ARC_CSL" => { + use crate::modules::home::draw::arc::ArcCSLCommand; + let new_cmd = ArcCSLCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "RECT" => { + use crate::modules::home::draw::shapes::RectCommand; + let new_cmd = RectCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "RECT_ROT" => { + use crate::modules::home::draw::shapes::RectRotCommand; + let new_cmd = RectRotCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "RECT_CEN" => { + use crate::modules::home::draw::shapes::RectCenCommand; + let new_cmd = RectCenCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "POLY" => { + use crate::modules::home::draw::shapes::PolyCommand; + let new_cmd = PolyCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "POLY_C" => { + use crate::modules::home::draw::shapes::PolyCCommand; + let new_cmd = PolyCCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "POLY_E" => { + use crate::modules::home::draw::shapes::PolyECommand; + let new_cmd = PolyECommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "PLINE"|"PL" => { + use crate::modules::home::draw::polyline::PlineCommand; + let new_cmd = PlineCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + // ── Modify commands ──────────────────────────────────────────── + "MOVE"|"M" => { + let handles: Vec<_> = self.tabs[i].scene.selected_entities() + .into_iter().map(|(h, _)| h).collect(); + if handles.is_empty() { + use crate::command::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("MOVE"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + use crate::modules::home::modify::translate::MoveCommand; + let wires = self.tabs[i].scene.wire_models_for(&handles); + let new_cmd = MoveCommand::new(handles, wires); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + } + + "COPY"|"CO" => { + let handles: Vec<_> = self.tabs[i].scene.selected_entities() + .into_iter().map(|(h, _)| h).collect(); + if handles.is_empty() { + use crate::command::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("COPY"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + use crate::modules::home::modify::copy::CopyCommand; + let wires = self.tabs[i].scene.wire_models_for(&handles); + let new_cmd = CopyCommand::new(handles, wires); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + } + + "ROTATE"|"RO" => { + let handles: Vec<_> = self.tabs[i].scene.selected_entities() + .into_iter().map(|(h, _)| h).collect(); + if handles.is_empty() { + use crate::command::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("ROTATE"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + use crate::modules::home::modify::rotate::RotateCommand; + let wires = self.tabs[i].scene.wire_models_for(&handles); + let new_cmd = RotateCommand::new(handles, wires); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + } + + "POINT"|"PO" => { + use crate::modules::home::draw::point::PointCommand; + let new_cmd = PointCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "HATCH"|"H" => { + use crate::modules::home::draw::hatch::HatchCommand; + let outlines = self.tabs[i].scene.closed_outlines(); + let new_cmd = HatchCommand::new(outlines); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "GRADIENT" => { + use crate::modules::home::draw::hatch::GradientCommand; + let outlines = self.tabs[i].scene.closed_outlines(); + let new_cmd = GradientCommand::new(outlines); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "BOUNDARY" => { + use crate::modules::home::draw::hatch::BoundaryCommand; + let outlines = self.tabs[i].scene.closed_outlines(); + let new_cmd = BoundaryCommand::new(outlines); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "ELLIPSE"|"EL" => { + use crate::modules::home::draw::ellipse::EllipseCommand; + let new_cmd = EllipseCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "ELLIPSE_AXIS" => { + use crate::modules::home::draw::ellipse::EllipseAxisCommand; + let new_cmd = EllipseAxisCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "ELLIPSE_ARC" => { + use crate::modules::home::draw::ellipse::EllipseArcCommand; + let new_cmd = EllipseArcCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "SPLINE"|"SPL" => { + use crate::modules::home::draw::spline::SplineCommand; + let new_cmd = SplineCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "SCALE"|"SC" => { + let handles: Vec<_> = self.tabs[i].scene.selected_entities() + .into_iter().map(|(h, _)| h).collect(); + if handles.is_empty() { + use crate::command::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("SCALE"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + use crate::modules::home::modify::scale::ScaleCommand; + let wires = self.tabs[i].scene.wire_models_for(&handles); + let new_cmd = ScaleCommand::new(handles, wires); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + } + + "MIRROR"|"MI" => { + let handles: Vec<_> = self.tabs[i].scene.selected_entities() + .into_iter().map(|(h, _)| h).collect(); + if handles.is_empty() { + use crate::command::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("MIRROR"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + use crate::modules::home::modify::mirror::MirrorCommand; + let wires = self.tabs[i].scene.wire_models_for(&handles); + let new_cmd = MirrorCommand::new(handles, wires); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + } + + "ERASE"|"E" => { + let handles: Vec<_> = self.tabs[i].scene.selected_entities() + .into_iter().map(|(h, _)| h).collect(); + if handles.is_empty() { + use crate::command::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("ERASE"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + let n = handles.len(); + self.push_undo_snapshot(i, "ERASE"); + self.tabs[i].scene.erase_entities(&handles); + self.tabs[i].dirty = true; + self.refresh_properties(); + self.command_line.push_output(&format!("{n} object(s) erased.")); + } + } + + // ── Annotate commands ────────────────────────────────────────── + "TEXT"|"T"|"DT" => { + use crate::modules::annotate::text::TextCommand; + let new_cmd = TextCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "MTEXT"|"MT" => { + use crate::modules::annotate::mtext::MTextCommand; + let new_cmd = MTextCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "DIMLINEAR" => { + use crate::modules::annotate::linear_dim::LinearDimensionCommand; + let new_cmd = LinearDimensionCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "DIMRADIUS" => { + use crate::modules::annotate::radius_dim::RadiusDimensionCommand; + let new_cmd = RadiusDimensionCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "DIMANGULAR" => { + use crate::modules::annotate::angular_dim::AngularDimensionCommand; + let new_cmd = AngularDimensionCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "LEADER"|"LE" => { + use crate::modules::annotate::leader_cmd::LeaderCommand; + let new_cmd = LeaderCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "MLEADER"|"MLD" => { + use crate::modules::annotate::mleader_cmd::MLeaderCommand; + let new_cmd = MLeaderCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "ZOOM EXTENTS"|"ZOOMEXTENTS"|"ZE" => { + self.tabs[i].scene.fit_all(); + self.command_line.push_output("Zoom Extents"); + } + + "STRETCH"|"SS" => { + let handles: Vec<_> = self.tabs[i].scene.selected_entities() + .into_iter().map(|(h, _)| h).collect(); + if handles.is_empty() { + use crate::command::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("STRETCH"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + use crate::modules::home::modify::stretch::StretchCommand; + let new_cmd = StretchCommand::new(handles); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + } + + "FILLET"|"F" => { + use crate::modules::home::modify::fillet::FilletCommand; + let entities: Vec<_> = self.tabs[i].scene.entity_wires().iter() + .filter_map(|w| { + let h = Scene::handle_from_wire_name(&w.name)?; + self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e)) + }).collect(); + let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect(); + let new_cmd = FilletCommand::new( + crate::modules::home::defaults::get_fillet_radius(), all_entities); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "ARRAY"|"AR"|"ARRAYRECT" => { + let handles: Vec<_> = self.tabs[i].scene.selected_entities() + .into_iter().map(|(h, _)| h).collect(); + if handles.is_empty() { + use crate::command::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("ARRAYRECT"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + use crate::modules::home::modify::array::ArrayRectCommand; + let wires = self.tabs[i].scene.wire_models_for(&handles); + let new_cmd = ArrayRectCommand::new(handles, wires); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + } + + "ARRAYPOLAR" => { + let handles: Vec<_> = self.tabs[i].scene.selected_entities() + .into_iter().map(|(h, _)| h).collect(); + if handles.is_empty() { + use crate::command::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("ARRAYPOLAR"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + use crate::modules::home::modify::array::ArrayPolarCommand; + let wires = self.tabs[i].scene.wire_models_for(&handles); + let new_cmd = ArrayPolarCommand::new(handles, wires); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + } + + "ARRAYPATH" => { + let handles: Vec<_> = self.tabs[i].scene.selected_entities() + .into_iter().map(|(h, _)| h).collect(); + if handles.is_empty() { + use crate::command::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("ARRAYPATH"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + use crate::modules::home::modify::array::ArrayPathCommand; + let wires = self.tabs[i].scene.wire_models_for(&handles); + let all_entities: Vec<_> = self.tabs[i].scene.entity_wires().iter() + .filter_map(|w| { + let h = Scene::handle_from_wire_name(&w.name)?; + self.tabs[i].scene.document.get_entity(h).cloned() + }).collect(); + let new_cmd = ArrayPathCommand::new(handles, wires, all_entities); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + } + + "CHAMFER"|"CHA" => { + use crate::modules::home::modify::fillet::ChamferCommand; + let entities: Vec<_> = self.tabs[i].scene.entity_wires().iter() + .filter_map(|w| { + let h = Scene::handle_from_wire_name(&w.name)?; + self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e)) + }).collect(); + let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect(); + let new_cmd = ChamferCommand::new( + crate::modules::home::defaults::get_chamfer_dist1(), all_entities); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "EXPLODE"|"X" => { + use crate::modules::home::modify::explode::explode_entity; + let entities: Vec<_> = self.tabs[i].scene.selected_entities() + .into_iter().collect(); + if entities.is_empty() { + use crate::command::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("EXPLODE"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + let replacements: Vec<(acadrust::Handle, Vec)> = entities + .iter() + .filter_map(|(h, e)| { + let pieces = explode_entity(e, &self.tabs[i].scene.document); + if pieces.is_empty() { None } else { Some((*h, pieces)) } + }) + .collect(); + let exploded = replacements.len(); + if exploded > 0 { + self.push_undo_snapshot(i, "EXPLODE"); + } + for (handle, pieces) in replacements { + self.tabs[i].scene.erase_entities(&[handle]); + for piece in pieces { + self.tabs[i].scene.add_entity(piece); + } + } + if exploded > 0 { + self.tabs[i].dirty = true; + self.refresh_properties(); + self.command_line.push_output(&format!("{exploded} object(s) exploded.")); + } else { + self.command_line.push_info("EXPLODE: no explodable objects selected."); + } + } + } + + "OFFSET"|"O" => { + use crate::modules::home::modify::offset::OffsetCommand; + let all_entities: Vec<_> = self.tabs[i].scene.entity_wires().iter() + .filter_map(|w| { + let h = Scene::handle_from_wire_name(&w.name)?; + self.tabs[i].scene.document.get_entity(h).cloned() + }).collect(); + let new_cmd = OffsetCommand::new( + crate::modules::home::defaults::get_offset_dist(), all_entities); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "TRIM"|"TR" => { + use crate::modules::home::modify::trim::TrimCommand; + let entities: Vec<_> = self.tabs[i].scene.entity_wires().iter() + .filter_map(|w| { + let h = Scene::handle_from_wire_name(&w.name)?; + self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e)) + }).collect(); + let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect(); + let new_cmd = TrimCommand::new(all_entities); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "EXTEND"|"EX" => { + use crate::modules::home::modify::trim::ExtendCommand; + let entities: Vec<_> = self.tabs[i].scene.entity_wires().iter() + .filter_map(|w| { + let h = Scene::handle_from_wire_name(&w.name)?; + self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e)) + }).collect(); + let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect(); + let new_cmd = ExtendCommand::new(all_entities); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + + "3DORBIT"|"3O" => { + self.command_line.push_info("3D Orbit: drag with right mouse button."); + } + + "HELP"|"?" => { + self.command_line.push_output( + "Draw: LINE CIRCLE ARC PLINE POINT ELLIPSE SPLINE | \ + Modify: MOVE COPY ROTATE SCALE MIRROR ERASE | \ + Text: TEXT MTEXT | File: OPEN SAVE SAVEAS" + ); + } + + "DONATE" => { + let _ = open::that("https://patreon.com/HakanSeven12"); + self.command_line.push_info("Opening Patreon page..."); + } + + // ── Layout / viewport ────────────────────────────────────────── + "MVIEW"|"MV" => { + if self.tabs[i].scene.current_layout == "Model" { + self.command_line.push_error("MVIEW: önce bir paper space layout'una geçin."); + } else { + use crate::modules::layout::mview::MviewCommand; + let new_cmd = MviewCommand::new(); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + } + + _ => self.command_line.push_error(&format!("Unknown command: {cmd}")), + } + + // Focus the command line whenever a command just became active. + let i = self.active_tab; + if self.tabs[i].active_cmd.is_some() { + self.tabs[i].last_cmd = Some(cmd.to_string()); + self.focus_cmd_input() + } else { + Task::none() + } + } +} diff --git a/src/app/document.rs b/src/app/document.rs new file mode 100644 index 00000000..0d4bb93a --- /dev/null +++ b/src/app/document.rs @@ -0,0 +1,83 @@ +use crate::scene::Scene; +use crate::ui::{LayerPanel, PropertiesPanel}; +use crate::command::CadCommand; +use crate::snap::SnapResult; +use crate::scene::grip::GripEdit; +use crate::scene::GripDef; +use acadrust::{CadDocument, Handle}; +use crate::linetypes; +use std::path::PathBuf; + +// ── Per-document tab state ───────────────────────────────────────────────── + +pub(super) struct DocumentTab { + pub(super) scene: Scene, + pub(super) current_path: Option, + pub(super) dirty: bool, + pub(super) tab_title: String, + pub(super) properties: PropertiesPanel, + pub(super) layers: LayerPanel, + pub(super) active_cmd: Option>, + pub(super) last_cmd: Option, + pub(super) snap_result: Option, + pub(super) active_grip: Option, + pub(super) selected_grips: Vec, + pub(super) selected_handle: Option, + pub(super) wireframe: bool, + pub(super) visual_style: String, + pub(super) last_cursor_world: glam::Vec3, + pub(super) history: HistoryState, + pub(super) active_layer: String, +} + +impl DocumentTab { + pub(super) fn new_drawing(n: usize) -> Self { + let mut scene = Scene::new(); + linetypes::populate_document(&mut scene.document); + Self { + scene, + current_path: None, + dirty: false, + tab_title: format!("Drawing{}", n), + properties: PropertiesPanel::empty(), + layers: LayerPanel::default(), + active_cmd: None, + last_cmd: None, + snap_result: None, + active_grip: None, + selected_grips: vec![], + selected_handle: None, + wireframe: false, + visual_style: "Shaded".into(), + last_cursor_world: glam::Vec3::ZERO, + history: HistoryState::default(), + active_layer: "0".to_string(), + } + } + + pub(super) fn tab_display_name(&self) -> String { + match &self.current_path { + Some(p) => p + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(), + None => self.tab_title.clone(), + } + } +} + +#[derive(Clone)] +pub(super) struct HistorySnapshot { + pub(super) document: CadDocument, + pub(super) current_layout: String, + pub(super) selected: Vec, + pub(super) dirty: bool, + pub(super) label: String, +} + +#[derive(Default)] +pub(super) struct HistoryState { + pub(super) undo_stack: Vec, + pub(super) redo_stack: Vec, +} diff --git a/src/app/helpers.rs b/src/app/helpers.rs new file mode 100644 index 00000000..15a80ea6 --- /dev/null +++ b/src/app/helpers.rs @@ -0,0 +1,205 @@ +use crate::ui::overlay::GridPlane; +use crate::scene::WireModel; + +// ── Coordinate parsing ───────────────────────────────────────────────────── + +/// Parse a typed coordinate string into a world-space Vec3. +/// Accepts "x,y" → Vec3(x, y, 0) +/// "x,y,z" → Vec3(x, y, z) +/// Separators: comma or semicolon; decimal point or decimal comma. +pub(super) fn parse_coord(text: &str) -> Option { + let parts: Vec = text + .split(|c| c == ',' || c == ';') + .map(|s| s.trim().replace(',', ".")) + .filter_map(|s| s.parse().ok()) + .collect(); + match parts.as_slice() { + [x, y] => Some(glam::Vec3::new(*x, *y, 0.0)), + [x, y, z] => Some(glam::Vec3::new(*x, *y, *z)), + _ => None, + } +} + +pub(super) fn angle_close(a: f32, b: f32, tol: f32) -> bool { + let diff = (a - b).rem_euclid(std::f32::consts::TAU); + let diff = if diff > std::f32::consts::PI { + diff - std::f32::consts::TAU + } else { + diff + }; + diff.abs() < tol +} + +// ── Grid plane detection ─────────────────────────────────────────────────── + +/// Choose the grid plane whose normal is most aligned with the camera view direction. +pub(super) fn grid_plane_from_camera(pitch: f32, yaw: f32) -> GridPlane { + let fz = pitch.sin().abs(); + let fy = (pitch.cos() * yaw.cos()).abs(); + let fx = (pitch.cos() * yaw.sin()).abs(); + if fz >= fy && fz >= fx { + GridPlane::Xy + } else if fy >= fx { + GridPlane::Xz + } else { + GridPlane::Yz + } +} + +// ── Drawing constraint helpers ───────────────────────────────────────────── + +/// Constrain `pt` to the nearest 90° direction from `base` (XY plane, Z-up). +pub(super) fn ortho_constrain(pt: glam::Vec3, base: glam::Vec3) -> glam::Vec3 { + let dx = (pt.x - base.x).abs(); + let dy = (pt.y - base.y).abs(); + if dx >= dy { + glam::Vec3::new(pt.x, base.y, pt.z) + } else { + glam::Vec3::new(base.x, pt.y, pt.z) + } +} + +/// Constrain `pt` to the nearest polar angle multiple from `base` (XY plane, Z-up). +pub(super) fn polar_constrain(pt: glam::Vec3, base: glam::Vec3, step_deg: f32) -> glam::Vec3 { + let dx = pt.x - base.x; + let dy = pt.y - base.y; + let dist = (dx * dx + dy * dy).sqrt(); + if dist < 1e-6 { + return pt; + } + let step = step_deg.to_radians(); + let angle = dy.atan2(dx); + let snapped = (angle / step).round() * step; + glam::Vec3::new( + base.x + dist * snapped.cos(), + base.y + dist * snapped.sin(), + pt.z, + ) +} + +// ── Clipboard / selection helpers ────────────────────────────────────────── + +/// Compute the centroid of a set of wire models (average of all points). +pub(super) fn entities_centroid(wires: &[WireModel]) -> glam::Vec3 { + let mut sum = glam::Vec3::ZERO; + let mut count = 0usize; + for w in wires { + for p in &w.points { + sum += glam::Vec3::from(*p); + count += 1; + } + } + if count > 0 { sum / count as f32 } else { glam::Vec3::ZERO } +} + +/// Generate the next available auto group name ("*A1", "*A2", …). +pub(super) fn next_group_auto_name(scene: &crate::scene::Scene) -> String { + let existing: std::collections::HashSet = + scene.groups().map(|g| g.name.clone()).collect(); + for n in 1..=9999 { + let name = format!("*A{n}"); + if !existing.contains(&name) { + return name; + } + } + "*A".to_string() +} + +// ── Entity type labels ───────────────────────────────────────────────────── + +pub(super) fn entity_type_label(entity: &acadrust::EntityType) -> String { + use acadrust::EntityType::*; + match entity { + Line(_) => "Line", + Circle(_) => "Circle", + Arc(_) => "Arc", + Ellipse(_) => "Ellipse", + Spline(_) => "Spline", + LwPolyline(_) => "Polyline", + Text(_) => "Text", + MText(_) => "MText", + Dimension(_) => "Dimension", + Insert(_) => "Block Reference", + Point(_) => "Point", + Hatch(_) => "Hatch", + _ => "Entity", + } + .to_string() +} + +pub(super) fn entity_type_key(entity: &acadrust::EntityType) -> String { + match entity { + acadrust::EntityType::LwPolyline(_) => "pline", + acadrust::EntityType::Circle(_) => "circle", + acadrust::EntityType::Line(_) => "line", + acadrust::EntityType::Arc(_) => "arc", + acadrust::EntityType::Ellipse(_) => "ellipse", + acadrust::EntityType::Spline(_) => "spline", + acadrust::EntityType::Text(_) => "text", + acadrust::EntityType::MText(_) => "mtext", + acadrust::EntityType::Dimension(_) => "dimension", + acadrust::EntityType::Insert(_) => "insert", + acadrust::EntityType::Point(_) => "point", + acadrust::EntityType::Hatch(_) => "hatch", + _ => "entity", + } + .to_string() +} + +pub(super) fn title_case_word(value: &str) -> String { + let mut chars = value.chars(); + match chars.next() { + Some(first) => { + let mut out = first.to_uppercase().collect::(); + out.push_str(chars.as_str()); + out + } + None => String::new(), + } +} + +// ── Window icon ──────────────────────────────────────────────────────────── + +/// Builds a 32×32 RGBA icon: red background with H7 drawn in white pixels. +pub(super) fn build_window_icon() -> Vec { + const W: usize = 32; + const SZ: usize = W * W * 4; + + let bg = [176u8, 48, 32, 255]; + let fg = [255u8, 255, 255, 255]; + + let mut px = vec![0u8; SZ]; + for i in 0..W * W { + px[i * 4..i * 4 + 4].copy_from_slice(&bg); + } + + fn stroke(px: &mut Vec, ax: i32, ay: i32, bx: i32, by: i32, fg: [u8; 4]) { + let steps = ((bx - ax).abs().max((by - ay).abs()) * 3).max(1); + for s in 0..=steps { + let t = s as f32 / steps as f32; + let cx = ax as f32 + (bx - ax) as f32 * t; + let cy = ay as f32 + (by - ay) as f32 * t; + for dy in -1i32..=1 { + for dx in -1i32..=1 { + let ix = cx.round() as i32 + dx; + let iy = cy.round() as i32 + dy; + if ix >= 0 && ix < W as i32 && iy >= 0 && iy < W as i32 { + let idx = (iy as usize * W + ix as usize) * 4; + px[idx..idx + 4].copy_from_slice(&fg); + } + } + } + } + } + + // H + stroke(&mut px, 4, 5, 4, 26, fg); + stroke(&mut px, 13, 5, 13, 26, fg); + stroke(&mut px, 4, 15, 13, 15, fg); + // 7 + stroke(&mut px, 17, 5, 27, 5, fg); + stroke(&mut px, 27, 5, 20, 26, fg); + stroke(&mut px, 20, 16, 26, 16, fg); + + px +} diff --git a/src/app/history.rs b/src/app/history.rs new file mode 100644 index 00000000..75b05346 --- /dev/null +++ b/src/app/history.rs @@ -0,0 +1,107 @@ +use super::{H7CAD, document::HistorySnapshot}; +use std::collections::HashSet; + +impl H7CAD { + pub(super) fn history_label_from_active_cmd(&self, i: usize, fallback: &'static str) -> String { + self.tabs[i] + .active_cmd + .as_ref() + .map(|cmd| cmd.name().to_string()) + .unwrap_or_else(|| fallback.to_string()) + } + + pub(super) fn capture_history_snapshot(&self, i: usize, label: impl Into) -> HistorySnapshot { + HistorySnapshot { + document: self.tabs[i].scene.document.clone(), + current_layout: self.tabs[i].scene.current_layout.clone(), + selected: self.tabs[i].scene.selected.iter().copied().collect(), + dirty: self.tabs[i].dirty, + label: label.into(), + } + } + + pub(super) fn push_undo_snapshot(&mut self, i: usize, label: impl Into) { + let snapshot = self.capture_history_snapshot(i, label); + self.tabs[i].history.undo_stack.push(snapshot); + self.tabs[i].history.redo_stack.clear(); + } + + pub(super) fn restore_history_snapshot(&mut self, i: usize, snapshot: HistorySnapshot) { + self.tabs[i].scene.document = snapshot.document; + self.tabs[i].scene.current_layout = snapshot.current_layout; + self.tabs[i].scene.selected = snapshot + .selected + .into_iter() + .filter(|h| self.tabs[i].scene.document.get_entity(*h).is_some()) + .collect::>(); + self.tabs[i].scene.populate_hatches_from_document(); + self.tabs[i].scene.clear_preview_wire(); + self.tabs[i].scene.meshes.clear(); + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.tabs[i].active_grip = None; + self.tabs[i].dirty = snapshot.dirty; + let doc_layers = self.tabs[i].scene.document.layers.clone(); + self.tabs[i].layers.sync_from_doc(&doc_layers); + self.sync_ribbon_layers(); + self.refresh_properties(); + } + + pub(super) fn undo_active_tab(&mut self) { + self.undo_steps(1); + } + + pub(super) fn redo_active_tab(&mut self) { + self.redo_steps(1); + } + + pub(super) fn undo_steps(&mut self, steps: usize) { + let i = self.active_tab; + let available = self.tabs[i].history.undo_stack.len(); + let steps = steps.min(available); + if steps == 0 { + self.command_line.push_info("Nothing to undo."); + return; + } + + let mut last_label = String::new(); + for _ in 0..steps { + let Some(snapshot) = self.tabs[i].history.undo_stack.pop() else { + break; + }; + let label = snapshot.label.clone(); + let current = self.capture_history_snapshot(i, label.clone()); + self.tabs[i].history.redo_stack.push(current); + self.restore_history_snapshot(i, snapshot); + last_label = label; + } + self.command_line.push_output(&format!("Undo: {last_label}")); + } + + pub(super) fn redo_steps(&mut self, steps: usize) { + let i = self.active_tab; + let available = self.tabs[i].history.redo_stack.len(); + let steps = steps.min(available); + if steps == 0 { + self.command_line.push_info("Nothing to redo."); + return; + } + + let mut last_label = String::new(); + for _ in 0..steps { + let Some(snapshot) = self.tabs[i].history.redo_stack.pop() else { + break; + }; + let label = snapshot.label.clone(); + let current = self.capture_history_snapshot(i, label.clone()); + self.tabs[i].history.undo_stack.push(current); + self.restore_history_snapshot(i, snapshot); + last_label = label; + } + self.command_line.push_output(&format!("Redo: {last_label}")); + } +} + +pub(super) fn history_dropdown_labels(stack: &[HistorySnapshot]) -> Vec { + stack.iter().rev().map(|snapshot| snapshot.label.clone()).collect() +} diff --git a/src/app/layers.rs b/src/app/layers.rs new file mode 100644 index 00000000..f96d3a6e --- /dev/null +++ b/src/app/layers.rs @@ -0,0 +1,43 @@ +use super::H7CAD; +use crate::ui; + +impl H7CAD { + pub(super) fn sync_ribbon_layers(&mut self) { + let i = self.active_tab; + let active = self.tabs[i].active_layer.clone(); + let infos: Vec = self.tabs[i] + .layers + .layers + .iter() + .map(|l| crate::ui::ribbon::LayerInfo { + name: l.name.clone(), + color: l.color, + visible: l.visible, + frozen: l.frozen, + locked: l.locked, + }) + .collect(); + let names: Vec = infos.iter().map(|l| l.name.clone()).collect(); + let active = if names.contains(&active) { active } else { "0".to_string() }; + self.tabs[i].active_layer = active.clone(); + self.tabs[i].layers.current_layer = active.clone(); + self.ribbon.set_layers(infos, &active); + let lt_items: Vec = self.tabs[i] + .scene + .document + .line_types + .iter() + .map(|lt| { + let name = if lt.name.eq_ignore_ascii_case("bylayer") { + "ByLayer".to_string() + } else { + lt.name.clone() + }; + let art = crate::linetypes::extract_pattern(<.description); + ui::properties::LinetypeItem { name, art } + }) + .collect(); + self.tabs[i].layers.sync_linetypes(lt_items.clone()); + self.ribbon.set_available_linetypes(lt_items); + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 00000000..60976d83 --- /dev/null +++ b/src/app/mod.rs @@ -0,0 +1,284 @@ +mod document; +mod helpers; +mod history; +mod properties; +mod layers; +mod commands; +mod cmd_result; +mod view; +mod update; + +use document::DocumentTab; + +use acadrust::types::{Color as AcadColor, LineWeight}; +use acadrust::CadDocument; +use crate::modules::ModuleEvent; +use crate::scene::CubeRegion; +use crate::snap::Snapper; +use crate::ui::{AppMenu, CommandLine, Ribbon, StatusBar}; + +use iced::time::Instant; +use iced::window; +use iced::{mouse, Point, Task, Theme}; + +pub(super) const POLY_START_DELAY_MS: u128 = 150; +pub(super) const VARIES_LABEL: &str = "*VARIES*"; + +// ── Application state ────────────────────────────────────────────────────── + +pub(super) struct H7CAD { + start: Instant, + tabs: Vec, + active_tab: usize, + tab_counter: usize, + ribbon: Ribbon, + app_menu: AppMenu, + command_line: CommandLine, + status_bar: StatusBar, + cursor_pos: Point, + vp_size: (f32, f32), + snapper: Snapper, + snap_popup_open: bool, + /// Whether Tangent snap was enabled before a tangent-pick command started. + pre_cmd_tangent: Option, + /// Orthogonal drawing constraint (F8): constrains picks to 0°/90°/180°/270°. + ortho_mode: bool, + /// Polar tracking (F10): constrains picks to 45° angle increments. + polar_mode: bool, + /// Show grid lines in the viewport (F7). + show_grid: bool, + /// Last point committed by a drawing command — used as ortho/polar base. + last_point: Option, + /// OS window Id for the floating Layer Properties Manager (None when closed). + layer_window: Option, + /// OS window Id of the primary application window. + main_window: Option, + /// In-memory clipboard: cloned entities waiting to be pasted. + clipboard: Vec, + /// Centroid of the clipboard entities (XZ plane, Y-up). + clipboard_centroid: glam::Vec3, +} + +#[derive(Debug, Clone)] +pub enum Message { + Tick(Instant), + OpenFile, + FileOpened(Result<(String, PathBuf, CadDocument), String>), + SaveFile, + SaveAs, + PickedSavePath(Option), + ClearScene, + SetWireframe(bool), + /// Switch camera projection: true = Orthographic, false = Perspective. + SetProjection(bool), + /// Select a ribbon module tab by index. + RibbonSelectTab(usize), + /// A ribbon tool button was clicked — highlights the tool and dispatches its event. + RibbonToolClick { + tool_id: String, + event: ModuleEvent, + }, + // ── Application menu ────────────────────────────────────────────────── + ToggleAppMenu, + CloseAppMenu, + /// Close the menu and immediately dispatch a CAD command. + CloseAppMenuAndRun(String), + AppMenuSearch(String), + // ── Document tabs ────────────────────────────────────────────────────── + /// Create a new empty document tab. + TabNew, + /// Switch to the given tab index. + TabSwitch(usize), + /// Close the given tab index. + TabClose(usize), + // ───────────────────────────────────────────────────────────────────── + CommandInput(String), + CommandSubmit, + Command(String), + ToggleLayers, + LayerToggleVisible(usize), + LayerToggleLock(usize), + LayerToggleFreeze(usize), + LayerNew, + LayerDelete, + LayerSetCurrent, + LayerSelect(usize), + LayerRenameStart(usize), + LayerRenameEdit(String), + LayerColorPickerToggle(usize), + LayerColorMorePalette, + LayerColorSet(u8), + LayerLinetypeSet(String), + LayerLineweightSet(LineWeight), + LayerTransparencyEdit(usize, String), + LayerRenameCommit, + CursorMoved(Point), + ViewportClick, + ViewportMove(Point), + ViewportLeftPress, + ViewportLeftRelease, + ViewportRightPress, + ViewportRightRelease, + ViewportMiddlePress, + ViewportMiddleRelease, + ViewportScroll(mouse::ScrollDelta), + ViewportExit, + ViewCubeSnap(CubeRegion), + WindowResized(f32, f32), + /// Enter pressed globally — finalises the active command (no text-input involvement). + CommandFinalize, + /// Escape pressed globally — cancels the active command. + CommandEscape, + /// Toggle the global snap on/off (OSNAP button body click). + ToggleSnapEnabled, + /// Toggle grid-snap on/off — F9 / SNAP status-bar button. + ToggleGridSnap, + /// Toggle grid display in the viewport — F7 / GRID status-bar button. + ToggleGrid, + /// Toggle orthogonal drawing constraint — F8 / ORTHO status-bar button. + ToggleOrtho, + /// Toggle polar-angle constraint (45° increments) — F10 / POLAR status-bar button. + TogglePolar, + /// Toggle an individual snap mode (from popup row click). + ToggleSnap(crate::snap::SnapType), + /// Open / close the OSNAP popup (▾ arrow click). + ToggleSnapPopup, + /// Close the OSNAP popup (click-catcher outside the panel). + CloseSnapPopup, + /// Enable all snap modes. + SnapSelectAll, + /// Disable all snap modes. + SnapClearAll, + /// Toggle a ribbon dropdown open/closed. + ToggleRibbonDropdown(String), + /// Close any open ribbon dropdown (click-catcher outside the panel). + CloseRibbonDropdown, + /// User selected a specific item from a ribbon dropdown. + DropdownSelectItem { + dropdown_id: &'static str, + cmd: &'static str, + }, + /// Delete key — erase all currently selected entities. + DeleteSelected, + Undo, + Redo, + UndoMany(usize), + RedoMany(usize), + // ── Ribbon ──────────────────────────────────────────────────────────── + /// User selected a layer from the layer combobox in the ribbon. + RibbonLayerChanged(String), + /// User changed the active color in the Properties toolbar. + RibbonColorChanged(AcadColor), + /// Toggle the full ACI palette inside the ribbon color picker. + RibbonColorPaletteToggle, + /// User changed the active linetype in the Properties toolbar. + RibbonLinetypeChanged(String), + /// User changed the active lineweight in the Properties toolbar. + RibbonLineweightChanged(LineWeight), + + // ── Properties panel ────────────────────────────────────────────────── + /// User selected a layer from the layer pick_list in the Properties panel. + PropLayerChanged(String), + PropSelectionGroupChanged(crate::ui::properties::SelectionGroup), + /// User picked a color from the Properties color picker. + PropColorChanged(AcadColor), + /// User selected a lineweight from the Properties pick_list. + PropLwChanged(LineWeight), + /// User selected a linetype from the linetype pick_list. + PropLinetypeChanged(String), + /// User toggled a boolean property (e.g. Invisible). + PropBoolToggle(&'static str), + /// User selected a hatch pattern from the pattern pick_list in Properties. + PropHatchPatternChanged(String), + /// User selected a generic choice field in the Properties panel. + PropGeomChoiceChanged { + field: &'static str, + value: String, + }, + /// User is typing in an editable geometry field (live buffer update). + PropGeomInput { + field: &'static str, + value: String, + }, + /// User committed a geometry/common field edit (Enter pressed). + PropGeomCommit(&'static str), + /// Toggle the inline color picker dropdown open/closed. + PropColorPickerToggle, + /// Toggle the full ACI colour palette expansion. + PropColorPaletteToggle, + /// Switch to a named layout ("Model" or paper space layout name). + LayoutSwitch(String), + /// Create a new paper space layout. + LayoutCreate, + /// A window was closed by the OS (e.g. the user clicked the title-bar ✕). + OsWindowClosed(window::Id), + /// No-op — used as a fallback when a TabEvent has no host mapping. + Noop, +} + +impl H7CAD { + fn new() -> Self { + let first_tab = DocumentTab::new_drawing(1); + let mut app = Self { + start: Instant::now(), + tabs: vec![first_tab], + active_tab: 0, + tab_counter: 1, + ribbon: Ribbon::new(), + app_menu: AppMenu::new(), + command_line: CommandLine::new(), + status_bar: StatusBar::new(), + cursor_pos: Point::ORIGIN, + vp_size: (1280.0, 720.0), + snapper: Snapper::default(), + snap_popup_open: false, + pre_cmd_tangent: None, + ortho_mode: false, + polar_mode: false, + show_grid: false, + last_point: None, + layer_window: None, + main_window: None, + clipboard: Vec::new(), + clipboard_centroid: glam::Vec3::ZERO, + }; + app.sync_ribbon_layers(); + app + } + + /// Boot function for `iced::daemon`: returns initial state plus a task that + /// opens the primary application window. + fn boot() -> (Self, Task) { + use helpers::build_window_icon; + let state = Self::new(); + let (id, open_task) = window::open(window::Settings { + maximized: true, + icon: window::icon::from_rgba(build_window_icon(), 32, 32).ok(), + ..Default::default() + }); + let mut s = state; + s.main_window = Some(id); + let task = open_task.map(|_| Message::Noop); + (s, task) + } +} + +use std::path::PathBuf; + +pub fn run() -> iced::Result { + iced::daemon(H7CAD::boot, H7CAD::update, H7CAD::view) + .subscription(H7CAD::subscription) + .title(|state: &H7CAD, window_id: window::Id| { + if Some(window_id) == state.layer_window { + "Layer Properties Manager".to_string() + } else if let Some(tab) = state.tabs.get(state.active_tab) { + let dot = if tab.dirty { "● " } else { "" }; + let name = tab.tab_display_name(); + format!("{}H7CAD — {}", dot, name) + } else { + "H7CAD".to_string() + } + }) + .theme(Theme::Dark) + .run() +} diff --git a/src/app/properties.rs b/src/app/properties.rs new file mode 100644 index 00000000..5b8f3839 --- /dev/null +++ b/src/app/properties.rs @@ -0,0 +1,347 @@ +use super::{H7CAD, VARIES_LABEL}; +use super::helpers::{entity_type_label, entity_type_key, title_case_word}; +use crate::scene::dispatch; +use crate::ui; +use crate::linetypes; +use acadrust::{EntityType, Handle}; + +impl H7CAD { + /// Rebuild the PropertiesPanel from the current entity selection. + /// Preserves UI state (open pickers, edit buffer) across refreshes. + pub(super) fn refresh_properties(&mut self) { + let i = self.active_tab; + let color_picker_open = self.tabs[i].properties.color_picker_open; + let color_palette_open = self.tabs[i].properties.color_palette_open; + let edit_buf = std::mem::take(&mut self.tabs[i].properties.edit_buf); + let selected_group = self.tabs[i].properties.selected_group.clone(); + + let layer_names: Vec = self.tabs[i] + .scene + .document + .layers + .iter() + .map(|l| l.name.clone()) + .collect(); + let linetype_items: Vec = self.tabs[i] + .scene + .document + .line_types + .iter() + .map(|lt| { + let name = if lt.name.is_empty() { + "ByLayer".to_string() + } else { + lt.name.clone() + }; + let art = linetypes::extract_pattern(<.description); + ui::properties::LinetypeItem { name, art } + }) + .collect(); + let text_style_names: Vec = self.tabs[i] + .scene + .document + .text_styles + .iter() + .map(|style| style.name.trim().to_string()) + .filter(|name| !name.is_empty()) + .collect(); + + let new_panel = { + let selected = self.tabs[i].scene.selected_entities(); + let mut panel = match selected.len() { + 0 => ui::PropertiesPanel::empty(), + 1 => { + let (handle, entity) = selected[0]; + let group_names = self.tabs[i].scene.group_names_for_entity(handle); + let mut sections = + dispatch::properties_sectioned(handle, entity, &text_style_names); + if !group_names.is_empty() { + let label = group_names.join(", "); + if let Some(general) = sections.first_mut() { + general.props.push(crate::scene::object::Property { + label: "Group".to_string(), + field: "group", + value: crate::scene::object::PropValue::ReadOnly(label), + }); + } + } + let title = entity_type_label(entity); + ui::PropertiesPanel { + choice_combos: sections + .iter() + .flat_map(|section| section.props.iter()) + .filter_map(|prop| match &prop.value { + crate::scene::object::PropValue::Choice { options, .. } => Some(( + prop.field.to_string(), + iced::widget::combo_box::State::new(options.clone()), + )), + _ => None, + }) + .collect(), + sections, + title, + layer_combo: iced::widget::combo_box::State::new(layer_names.clone()), + linetype_combo: iced::widget::combo_box::State::new(linetype_items.clone()), + hatch_pattern_combo: iced::widget::combo_box::State::new( + crate::scene::hatch_patterns::names(), + ), + lineweight_combo: iced::widget::combo_box::State::new( + ui::properties::lw_options(), + ), + linetype_items, + ..Default::default() + } + } + _ => { + let groups = build_selection_groups(&selected); + let active_group = selected_group + .and_then(|group| groups.iter().find(|g| g.label == group.label).cloned()) + .or_else(|| groups.first().cloned()); + + let filtered: Vec<(Handle, &EntityType)> = active_group + .as_ref() + .map(|group| { + selected + .iter() + .filter(|(handle, _)| group.handles.contains(handle)) + .copied() + .collect() + }) + .unwrap_or_default(); + + let sections = aggregate_sections(&filtered, &text_style_names); + ui::PropertiesPanel { + choice_combos: sections + .iter() + .flat_map(|section| section.props.iter()) + .filter_map(|prop| match &prop.value { + crate::scene::object::PropValue::Choice { options, .. } => Some(( + prop.field.to_string(), + iced::widget::combo_box::State::new(options.clone()), + )), + _ => None, + }) + .collect(), + sections, + title: format!("{} objects selected", selected.len()), + selection_group_combo: iced::widget::combo_box::State::new(groups.clone()), + selection_groups: groups, + selected_group: active_group, + layer_combo: iced::widget::combo_box::State::new(layer_names.clone()), + linetype_combo: iced::widget::combo_box::State::new(linetype_items.clone()), + hatch_pattern_combo: iced::widget::combo_box::State::new( + crate::scene::hatch_patterns::names(), + ), + lineweight_combo: iced::widget::combo_box::State::new( + ui::properties::lw_options(), + ), + linetype_items, + ..Default::default() + } + } + }; + panel.color_picker_open = color_picker_open; + panel.color_palette_open = color_palette_open; + panel.edit_buf = edit_buf; + panel + }; + + self.tabs[i].properties = new_panel; + self.refresh_selected_grips(); + } + + /// Rebuild the cached selected_grips from the current entity selection. + pub(super) fn refresh_selected_grips(&mut self) { + let i = self.active_tab; + let (new_handle, new_grips) = { + let selected = self.tabs[i].scene.selected_entities(); + if selected.len() == 1 { + let (handle, entity) = selected[0]; + let grips = dispatch::grips(entity); + (Some(handle), grips) + } else { + (None, vec![]) + } + }; + self.tabs[i].selected_handle = new_handle; + self.tabs[i].selected_grips = new_grips; + } + + pub(super) fn property_target_handles(&self, i: usize) -> Vec { + let handles = self.tabs[i].properties.selected_handles(); + if !handles.is_empty() { + handles + } else { + self.tabs[i].selected_handle.into_iter().collect() + } + } + + /// Add an entity to the correct space (model or paper space layout). + pub(super) fn commit_entity(&mut self, mut entity: acadrust::EntityType) { + let i = self.active_tab; + let layer = &self.tabs[i].active_layer; + if layer != "0" || entity.as_entity().layer().is_empty() { + entity.as_entity_mut().set_layer(layer.clone()); + } + + crate::scene::dispatch::apply_color(&mut entity, self.ribbon.active_color); + crate::scene::dispatch::apply_common_prop( + &mut entity, + "linetype", + &self.ribbon.active_linetype.clone(), + ); + crate::scene::dispatch::apply_line_weight(&mut entity, self.ribbon.active_lineweight); + + if matches!(&entity, acadrust::EntityType::Viewport(_)) + && self.tabs[i].scene.current_layout != "Model" + { + let layout = self.tabs[i].scene.current_layout.clone(); + match self.tabs[i] + .scene + .document + .add_entity_to_layout(entity, &layout) + { + Ok(_) => {} + Err(e) => self + .command_line + .push_error(&format!("Viewport eklenemedi: {e}")), + } + } else { + self.tabs[i].scene.add_entity(entity); + } + } +} + +// ── Multi-selection property aggregation ─────────────────────────────────── + +pub(super) fn build_selection_groups( + selected: &[(Handle, &EntityType)], +) -> Vec { + let mut groups = vec![ui::properties::SelectionGroup { + label: format!("All({})", selected.len()), + handles: selected.iter().map(|(handle, _)| *handle).collect(), + }]; + + let mut by_type: std::collections::BTreeMap> = + std::collections::BTreeMap::new(); + for (handle, entity) in selected { + by_type + .entry(entity_type_key(entity)) + .or_default() + .push(*handle); + } + + for (kind, handles) in by_type { + groups.push(ui::properties::SelectionGroup { + label: format!("{}({})", title_case_word(&kind), handles.len()), + handles, + }); + } + + groups +} + +pub(super) fn aggregate_sections( + selected: &[(Handle, &EntityType)], + text_style_names: &[String], +) -> Vec { + if selected.is_empty() { + return vec![]; + } + + let mut all_sections: Vec> = selected + .iter() + .map(|(handle, entity)| dispatch::properties_sectioned(*handle, entity, text_style_names)) + .collect(); + + let mut result = all_sections.remove(0); + for sections in all_sections { + result = merge_sections(&result, §ions); + } + result +} + +fn merge_sections( + left: &[crate::scene::object::PropSection], + right: &[crate::scene::object::PropSection], +) -> Vec { + left.iter() + .filter_map(|section| { + let rhs = right.iter().find(|candidate| candidate.title == section.title)?; + let props: Vec = section + .props + .iter() + .filter_map(|prop| { + let other = + rhs.props.iter().find(|candidate| candidate.field == prop.field)?; + Some(crate::scene::object::Property { + label: prop.label.clone(), + field: prop.field, + value: merge_prop_value(&prop.value, &other.value), + }) + }) + .collect(); + if props.is_empty() { + None + } else { + Some(crate::scene::object::PropSection { + title: section.title.clone(), + props, + }) + } + }) + .collect() +} + +fn merge_prop_value( + left: &crate::scene::object::PropValue, + right: &crate::scene::object::PropValue, +) -> crate::scene::object::PropValue { + use crate::scene::object::PropValue; + + if left == right { + return left.clone(); + } + + match (left, right) { + (PropValue::LayerChoice(_), PropValue::LayerChoice(_)) => { + PropValue::LayerChoice(VARIES_LABEL.into()) + } + (PropValue::ColorChoice(_), PropValue::ColorChoice(_)) + | (PropValue::ColorVaries, _) + | (_, PropValue::ColorVaries) => PropValue::ColorVaries, + (PropValue::LwChoice(_), PropValue::LwChoice(_)) + | (PropValue::LwVaries, _) + | (_, PropValue::LwVaries) => PropValue::LwVaries, + (PropValue::LinetypeChoice(_), PropValue::LinetypeChoice(_)) => { + PropValue::LinetypeChoice(VARIES_LABEL.into()) + } + ( + PropValue::Choice { options, .. }, + PropValue::Choice { + options: other_options, + .. + }, + ) if options == other_options => PropValue::Choice { + selected: VARIES_LABEL.into(), + options: options.clone(), + }, + (PropValue::EditText(_), PropValue::EditText(_)) => { + PropValue::EditText(VARIES_LABEL.into()) + } + (PropValue::ReadOnly(_), PropValue::ReadOnly(_)) => { + PropValue::ReadOnly(VARIES_LABEL.into()) + } + (PropValue::HatchPatternChoice(_), PropValue::HatchPatternChoice(_)) => { + PropValue::HatchPatternChoice(VARIES_LABEL.into()) + } + ( + PropValue::BoolToggle { field, .. }, + PropValue::BoolToggle { + field: other_field, + .. + }, + ) if field == other_field => PropValue::ReadOnly(VARIES_LABEL.into()), + _ => left.clone(), + } +} diff --git a/src/app/update.rs b/src/app/update.rs new file mode 100644 index 00000000..57521cbe --- /dev/null +++ b/src/app/update.rs @@ -0,0 +1,1484 @@ +use super::{H7CAD, Message, POLY_START_DELAY_MS}; +use super::helpers::{parse_coord, angle_close, ortho_constrain, polar_constrain}; +use crate::scene::{self, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX}; +use crate::scene::grip::{find_hit_grip, GripEdit}; +use crate::scene::object::GripApply; +use crate::modules::ModuleEvent; +use crate::ui::PropertiesPanel; +use acadrust::types::Color as AcadColor; +use acadrust::Handle; +use iced::time::Instant; +use iced::window; +use iced::{mouse, Task}; + +const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_DRAW_PX; + +impl H7CAD { + pub fn update(&mut self, msg: Message) -> Task { + match msg { + Message::Tick(t) => { + self.tabs[self.active_tab].scene.update(t - self.start); + Task::none() + } + + Message::OpenFile => Task::perform(crate::io::pick_and_open(), Message::FileOpened), + + Message::FileOpened(Ok((name, path, doc))) => { + let entity_count = doc.entities().count(); + self.command_line + .push_output(&format!("Opened \"{name}\" — {entity_count} entities")); + self.app_menu.push_recent(path.clone()); + + let current_is_empty = { + let t = &self.tabs[self.active_tab]; + t.current_path.is_none() + && !t.dirty + && self.tabs[self.active_tab].scene.document.entities().count() == 0 + }; + let i = if current_is_empty { + self.active_tab + } else { + self.tab_counter += 1; + let new_tab = super::document::DocumentTab::new_drawing(self.tab_counter); + self.tabs.push(new_tab); + let idx = self.tabs.len() - 1; + self.active_tab = idx; + idx + }; + + self.tabs[i].current_path = Some(path); + self.tabs[i].scene.document = doc; + self.tabs[i].scene.populate_hatches_from_document(); + self.tabs[i].scene.selected = std::collections::HashSet::new(); + self.tabs[i].scene.preview_wires = vec![]; + self.tabs[i].scene.current_layout = "Model".to_string(); + crate::linetypes::populate_document(&mut self.tabs[i].scene.document); + self.tabs[i].properties = PropertiesPanel::empty(); + let doc_layers = self.tabs[i].scene.document.layers.clone(); + self.tabs[i].layers.sync_from_doc(&doc_layers); + self.sync_ribbon_layers(); + self.tabs[i].scene.fit_all(); + self.tabs[i].dirty = false; + self.tabs[i].history = super::document::HistoryState::default(); + self.refresh_selected_grips(); + Task::none() + } + + Message::FileOpened(Err(e)) => { + if e != "Cancelled" { + self.command_line.push_error(&format!("Open failed: {e}")); + } + Task::none() + } + + Message::SaveFile => { + let i = self.active_tab; + if let Some(path) = &self.tabs[i].current_path { + let path = path.clone(); + match crate::io::save(&self.tabs[i].scene.document, &path) { + Ok(()) => { + self.command_line + .push_output(&format!("Saved: {}", path.display())); + self.tabs[i].dirty = false; + } + Err(e) => self.command_line.push_error(&format!("Save failed: {e}")), + } + } else { + return Task::perform(crate::io::pick_save_path(), Message::PickedSavePath); + } + Task::none() + } + + Message::SaveAs => Task::perform(crate::io::pick_save_path(), Message::PickedSavePath), + + Message::PickedSavePath(Some(path)) => { + let i = self.active_tab; + match crate::io::save(&self.tabs[i].scene.document, &path) { + Ok(()) => { + self.command_line + .push_output(&format!("Saved: {}", path.display())); + self.tabs[i].current_path = Some(path); + self.tabs[i].dirty = false; + } + Err(e) => self.command_line.push_error(&format!("Save failed: {e}")), + } + Task::none() + } + + Message::PickedSavePath(None) => Task::none(), + + Message::ClearScene => { + let i = self.active_tab; + self.push_undo_snapshot(i, "NEW"); + self.tabs[i].scene.clear(); + crate::linetypes::populate_document(&mut self.tabs[i].scene.document); + self.tabs[i].properties = PropertiesPanel::empty(); + let doc_layers = self.tabs[i].scene.document.layers.clone(); + self.tabs[i].layers.sync_from_doc(&doc_layers); + self.command_line + .push_output("Scene cleared. Standard linetypes loaded."); + self.tabs[i].current_path = None; + self.tabs[i].dirty = true; + self.sync_ribbon_layers(); + Task::none() + } + + Message::SetWireframe(w) => { + let i = self.active_tab; + self.tabs[i].wireframe = w; + self.ribbon.set_wireframe(w); + self.tabs[i].visual_style = if w { "Wireframe".into() } else { "Shaded".into() }; + self.command_line.push_output(if w { "Visual style: Wireframe" } else { "Visual style: Shaded" }); + Task::none() + } + + Message::SetProjection(ortho) => { + use crate::scene::Projection; + let proj = if ortho { Projection::Orthographic } else { Projection::Perspective }; + let i = self.active_tab; + self.tabs[i].scene.camera.borrow_mut().projection = proj; + self.tabs[i].scene.camera_generation += 1; + self.ribbon.set_ortho(ortho); + self.command_line.push_output(if ortho { "Projection: Orthographic" } else { "Projection: Perspective" }); + Task::none() + } + + Message::RibbonSelectTab(idx) => { + self.ribbon.select(idx); + Task::none() + } + + Message::RibbonToolClick { tool_id, event } => { + self.ribbon.activate_tool(&tool_id); + match event { + ModuleEvent::Command(cmd) => return self.dispatch_command(&cmd), + ModuleEvent::OpenFileDialog => { + self.command_line.push_info("Open DWG/DXF: not yet implemented."); + } + ModuleEvent::ClearModels => { + let i = self.active_tab; + self.tabs[i].scene.clear(); + self.tabs[i].properties = PropertiesPanel::empty(); + self.command_line.push_output("Scene cleared."); + } + ModuleEvent::SetWireframe(w) => { + let i = self.active_tab; + self.tabs[i].wireframe = w; + self.ribbon.set_wireframe(w); + self.tabs[i].visual_style = if w { "Wireframe".into() } else { "Shaded".into() }; + self.command_line.push_output(if w { "Visual style: Wireframe" } else { "Visual style: Shaded" }); + } + ModuleEvent::ToggleLayers => { + return Task::done(Message::ToggleLayers); + } + } + Task::none() + } + + // ── Application menu ────────────────────────────────────────── + Message::ToggleAppMenu => { self.app_menu.toggle(); Task::none() } + Message::CloseAppMenu => { self.app_menu.close(); Task::none() } + Message::CloseAppMenuAndRun(cmd) => { + self.app_menu.close(); + self.dispatch_command(&cmd.clone()) + } + Message::AppMenuSearch(s) => { self.app_menu.search = s; Task::none() } + + // ── Document tabs ───────────────────────────────────────────── + Message::TabNew => { + self.tab_counter += 1; + let new_tab = super::document::DocumentTab::new_drawing(self.tab_counter); + self.tabs.push(new_tab); + self.active_tab = self.tabs.len() - 1; + self.sync_ribbon_layers(); + Task::none() + } + + Message::TabSwitch(idx) => { + if idx < self.tabs.len() { + self.active_tab = idx; + self.sync_ribbon_layers(); + } + Task::none() + } + + Message::TabClose(idx) => { + if self.tabs.len() == 1 { + self.tab_counter += 1; + self.tabs[0] = super::document::DocumentTab::new_drawing(self.tab_counter); + self.active_tab = 0; + } else { + self.tabs.remove(idx); + if self.active_tab >= self.tabs.len() { + self.active_tab = self.tabs.len() - 1; + } + } + Task::none() + } + + Message::CommandInput(s) => { self.command_line.input = s; Task::none() } + + Message::CommandSubmit => { + let i = self.active_tab; + if self.tabs[i].active_cmd.is_some() { + let text = self.command_line.input.trim().to_string(); + self.command_line.input.clear(); + + if self.tabs[i] + .active_cmd + .as_ref() + .map(|c| c.wants_text_input()) + .unwrap_or(false) + { + if let Some(result) = self.tabs[i] + .active_cmd + .as_mut() + .and_then(|c| c.on_text_input(&text)) + { + return self.apply_cmd_result(result); + } + let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); + if let Some(p) = prompt { + self.command_line.push_info(&p); + } + let pt = self.tabs[i].last_cursor_world; + let previews = self.tabs[i] + .active_cmd + .as_mut() + .map(|c| c.on_preview_wires(pt)) + .unwrap_or_default(); + self.tabs[i].scene.set_preview_wires(previews); + return self.focus_cmd_input(); + } + + if text.is_empty() { + let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_enter()); + if let Some(r) = result { + return self.apply_cmd_result(r); + } + return Task::none(); + } + + if let Some(pt) = parse_coord(&text) { + let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt)); + if let Some(r) = result { + return self.apply_cmd_result(r); + } + return Task::none(); + } + + if let Some(result) = self.tabs[i] + .active_cmd + .as_mut() + .and_then(|c| c.on_text_input(&text)) + { + return self.apply_cmd_result(result); + } + + self.command_line.push_error(&format!( + "Expected coordinates (x,y) or a number, got: \"{text}\"" + )); + return self.focus_cmd_input(); + } + if let Some(cmd) = self.command_line.submit() { + return self.dispatch_command(&cmd); + } + Task::none() + } + + Message::CommandFinalize => { + let i = self.active_tab; + if self.tabs[i].active_cmd.is_some() { + let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_enter()); + if let Some(r) = result { + return self.apply_cmd_result(r); + } + Task::none() + } else if let Some(cmd) = self.tabs[i].last_cmd.clone() { + self.dispatch_command(&cmd) + } else { + Task::none() + } + } + + Message::CommandEscape => { + let i = self.active_tab; + if self.tabs[i].active_cmd.is_some() { + let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_escape()); + if let Some(r) = result { + return self.apply_cmd_result(r); + } + } else { + self.tabs[i].scene.deselect_all(); + self.refresh_properties(); + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + sel.box_anchor = None; + sel.box_current = None; + sel.box_crossing = false; + } + Task::none() + } + + Message::Command(cmd) => self.dispatch_command(&cmd), + + Message::ToggleLayers => { + if let Some(id) = self.layer_window.take() { + window::close(id) + } else { + self.sync_ribbon_layers(); + let (id, task) = window::open(window::Settings { + size: iced::Size::new(900.0, 360.0), + resizable: true, + ..Default::default() + }); + self.layer_window = Some(id); + task.map(|_| Message::Noop) + } + } + + Message::OsWindowClosed(id) => { + if self.main_window == Some(id) { + return iced::exit(); + } + if self.layer_window == Some(id) { + self.layer_window = None; + } + Task::none() + } + + // ── Layer panel messages ─────────────────────────────────────── + Message::LayerToggleVisible(idx) => { + let i = self.active_tab; + if idx < self.tabs[i].layers.layers.len() { + self.push_undo_snapshot(i, "LAYER OFF/ON"); + let l = &mut self.tabs[i].layers.layers[idx]; + l.visible = !l.visible; + let name = l.name.clone(); + let on = l.visible; + self.tabs[i].scene.toggle_layer_visibility(&name); + self.command_line.push_output(&format!( + "Layer \"{}\" {}", name, if on { "on" } else { "off" } + )); + } + Task::none() + } + + Message::LayerToggleLock(idx) => { + let i = self.active_tab; + if idx < self.tabs[i].layers.layers.len() { + self.push_undo_snapshot(i, "LAYER LOCK/UNLOCK"); + let l = &mut self.tabs[i].layers.layers[idx]; + l.locked = !l.locked; + let name = l.name.clone(); + let locked = l.locked; + self.tabs[i].scene.toggle_layer_lock(&name); + self.command_line.push_output(&format!( + "Layer \"{}\" {}", name, if locked { "locked" } else { "unlocked" } + )); + } + Task::none() + } + + Message::LayerToggleFreeze(idx) => { + let i = self.active_tab; + if idx < self.tabs[i].layers.layers.len() { + self.push_undo_snapshot(i, "LAYER FREEZE"); + let l = &mut self.tabs[i].layers.layers[idx]; + l.frozen = !l.frozen; + let name = l.name.clone(); + let frozen = l.frozen; + if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { + if frozen { dl.freeze(); } else { dl.thaw(); } + } + self.tabs[i].dirty = true; + } + Task::none() + } + + Message::LayerNew => { + let i = self.active_tab; + let mut n = 1; + let new_name = loop { + let candidate = format!("Layer{}", n); + if !self.tabs[i].scene.document.layers.contains(&candidate) { + break candidate; + } + n += 1; + }; + self.push_undo_snapshot(i, "LAYER NEW"); + use acadrust::tables::layer::Layer as DocLayer; + let _ = self.tabs[i].scene.document.layers.add(DocLayer::new(&new_name)); + self.tabs[i].dirty = true; + let doc_layers = self.tabs[i].scene.document.layers.clone(); + self.tabs[i].layers.sync_from_doc(&doc_layers); + let new_idx = self.tabs[i].layers.layers.iter() + .position(|l| l.name == new_name); + if let Some(idx) = new_idx { + self.tabs[i].layers.selected = Some(idx); + self.tabs[i].layers.editing = Some(idx); + self.tabs[i].layers.edit_buf = new_name.clone(); + } + self.sync_ribbon_layers(); + Task::none() + } + + Message::LayerDelete => { + let i = self.active_tab; + if let Some(idx) = self.tabs[i].layers.selected { + let name = self.tabs[i].layers.layers.get(idx) + .map(|l| l.name.clone()) + .unwrap_or_default(); + if name == "0" { return Task::none(); } + self.push_undo_snapshot(i, "LAYER DELETE"); + self.tabs[i].scene.document.layers.remove(&name); + self.tabs[i].dirty = true; + let doc_layers = self.tabs[i].scene.document.layers.clone(); + self.tabs[i].layers.sync_from_doc(&doc_layers); + self.tabs[i].layers.selected = None; + self.sync_ribbon_layers(); + } + Task::none() + } + + Message::LayerSetCurrent => { + let i = self.active_tab; + if let Some(idx) = self.tabs[i].layers.selected { + if let Some(layer) = self.tabs[i].layers.layers.get(idx) { + let name = layer.name.clone(); + self.tabs[i].active_layer = name.clone(); + self.tabs[i].layers.current_layer = name.clone(); + self.ribbon.active_layer = name; + } + } + Task::none() + } + + Message::LayerSelect(idx) => { + let i = self.active_tab; + if self.tabs[i].layers.editing.is_some() { + return Task::done(Message::LayerRenameCommit); + } + self.tabs[i].layers.selected = Some(idx); + Task::none() + } + + Message::LayerRenameStart(idx) => { + let i = self.active_tab; + self.tabs[i].layers.selected = Some(idx); + if let Some(layer) = self.tabs[i].layers.layers.get(idx) { + self.tabs[i].layers.edit_buf = layer.name.clone(); + } + self.tabs[i].layers.editing = Some(idx); + Task::none() + } + + Message::LayerRenameEdit(s) => { + let i = self.active_tab; + self.tabs[i].layers.edit_buf = s; + Task::none() + } + + Message::LayerRenameCommit => { + let i = self.active_tab; + let editing_idx = self.tabs[i].layers.editing.take(); + if let Some(idx) = editing_idx { + let new_name = self.tabs[i].layers.edit_buf.trim().to_string(); + let old_name = self.tabs[i].layers.layers.get(idx) + .map(|l| l.name.clone()) + .unwrap_or_default(); + if !new_name.is_empty() && new_name != old_name + && !self.tabs[i].scene.document.layers.contains(&new_name) + { + self.push_undo_snapshot(i, "LAYER RENAME"); + if let Some(old_layer) = self.tabs[i].scene.document.layers.get(&old_name) { + use acadrust::tables::layer::Layer as DocLayer; + let mut nl = DocLayer::new(&new_name); + nl.color = old_layer.color.clone(); + nl.flags = old_layer.flags.clone(); + let _ = self.tabs[i].scene.document.layers.add(nl); + } + self.tabs[i].scene.document.layers.remove(&old_name); + for e in self.tabs[i].scene.document.entities_mut() { + if e.as_entity().layer() == old_name { + e.as_entity_mut().set_layer(new_name.clone()); + } + } + self.tabs[i].dirty = true; + } + let doc_layers = self.tabs[i].scene.document.layers.clone(); + self.tabs[i].layers.sync_from_doc(&doc_layers); + self.tabs[i].layers.edit_buf.clear(); + self.sync_ribbon_layers(); + } + Task::none() + } + + Message::LayerColorPickerToggle(idx) => { + let i = self.active_tab; + let panel = &mut self.tabs[i].layers; + if panel.color_picker_row == Some(idx) { + panel.color_picker_row = None; + panel.color_full_palette = false; + } else { + panel.color_picker_row = Some(idx); + panel.color_full_palette = false; + panel.selected = Some(idx); + } + Task::none() + } + + Message::LayerColorMorePalette => { + let i = self.active_tab; + self.tabs[i].layers.color_full_palette = !self.tabs[i].layers.color_full_palette; + Task::none() + } + + Message::LayerColorSet(aci) => { + let i = self.active_tab; + if let Some(idx) = self.tabs[i].layers.selected { + if let Some(layer) = self.tabs[i].layers.layers.get(idx) { + let name = layer.name.clone(); + if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { + dl.color = AcadColor::Index(aci); + } + use crate::ui::layers::iced_color_from_acad; + let new_color = iced_color_from_acad(&AcadColor::Index(aci)); + if let Some(pl) = self.tabs[i].layers.layers.get_mut(idx) { + pl.color = new_color; + } + self.tabs[i].dirty = true; + } + self.tabs[i].layers.color_picker_row = None; + self.tabs[i].layers.color_full_palette = false; + self.sync_ribbon_layers(); + } + Task::none() + } + + Message::LayerLinetypeSet(lt) => { + let i = self.active_tab; + if let Some(idx) = self.tabs[i].layers.selected { + if let Some(layer) = self.tabs[i].layers.layers.get(idx) { + let name = layer.name.clone(); + if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { + dl.line_type = lt.clone(); + } + if let Some(pl) = self.tabs[i].layers.layers.get_mut(idx) { + pl.linetype = lt; + } + self.tabs[i].dirty = true; + } + } + Task::none() + } + + Message::LayerLineweightSet(lw) => { + let i = self.active_tab; + if let Some(idx) = self.tabs[i].layers.selected { + if let Some(layer) = self.tabs[i].layers.layers.get(idx) { + let name = layer.name.clone(); + if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { + dl.line_weight = lw; + } + if let Some(pl) = self.tabs[i].layers.layers.get_mut(idx) { + pl.lineweight = lw; + } + self.tabs[i].dirty = true; + } + } + Task::none() + } + + Message::LayerTransparencyEdit(idx, s) => { + let i = self.active_tab; + if let Some(pl) = self.tabs[i].layers.layers.get_mut(idx) { + if let Ok(v) = s.parse::() { + pl.transparency = v.clamp(0, 90); + } else if s.is_empty() { + pl.transparency = 0; + } + } + Task::none() + } + + // ── Cursor / viewport messages ───────────────────────────────── + Message::CursorMoved(p) => { + let (vw, _vh) = self.tabs[self.active_tab].scene.selection.borrow().vp_size; + self.cursor_pos = iced::Point::new( + vw - VIEWCUBE_PAD - VIEWCUBE_HIT_SIZE + p.x, + VIEWCUBE_PAD + p.y, + ); + Task::none() + } + + Message::ViewportMove(p) => { + let i = self.active_tab; + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + sel.last_move_pos = Some(p); + + if sel.left_down { + let press = sel.left_press_pos.unwrap_or(p); + let dx = p.x - press.x; + let dy = p.y - press.y; + let dist2 = dx * dx + dy * dy; + let elapsed_ms = sel + .left_press_time + .map(|t| Instant::now().duration_since(t).as_millis()) + .unwrap_or(u128::MAX); + if !sel.left_dragging && elapsed_ms >= POLY_START_DELAY_MS && dist2 > 9.0 { + sel.left_dragging = true; + sel.poly_active = true; + sel.poly_crossing = p.x < press.x; + sel.poly_points.clear(); + sel.poly_points.push(press); + sel.poly_points.push(p); + } else if sel.left_dragging && sel.poly_active { + if sel.poly_points.last().map_or(true, |lp| { + let ddx = p.x - lp.x; + let ddy = p.y - lp.y; + ddx * ddx + ddy * ddy > 16.0 + }) { + sel.poly_points.push(p); + } + } + } else if sel.box_anchor.is_some() { + sel.box_current = Some(p); + if let Some(a) = sel.box_anchor { + sel.box_crossing = p.x < a.x; + } + } + + if sel.right_down { + if let Some(press) = sel.right_press_pos { + let dx = p.x - press.x; + let dy = p.y - press.y; + if !sel.right_dragging && (dx * dx + dy * dy) > 9.0 { + sel.right_dragging = true; + sel.context_menu = None; + } + } + if sel.right_dragging { + if let Some(last) = sel.right_last_pos { + let (dx, dy) = (p.x - last.x, p.y - last.y); + self.tabs[i].scene.camera.borrow_mut().orbit(dx, dy); + } + sel.right_last_pos = Some(p); + } + } + + if sel.middle_down { + if let Some(last) = sel.middle_last_pos { + let (dx, dy) = (p.x - last.x, p.y - last.y); + self.tabs[i].scene.camera.borrow_mut().pan(dx, dy); + } + sel.middle_last_pos = Some(p); + } + + let vp_size = sel.vp_size; + drop(sel); + + // ── Grip drag ───────────────────────────────────────────── + if let Some(grip) = self.tabs[i].active_grip.clone() { + let (vw, vh) = vp_size; + let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh }; + let cam = self.tabs[i].scene.camera.borrow(); + let raw = cam.pick_on_target_plane(p, bounds); + let vp_mat = cam.view_proj(bounds); + drop(cam); + + let edited_name = grip.handle.value().to_string(); + let all_wires = self.tabs[i].scene.entity_wires(); + let snap_wires: Vec<_> = all_wires + .iter() + .filter(|w| w.name != edited_name) + .cloned() + .collect(); + let snap_hit = self.snapper.snap(raw, p, &snap_wires, vp_mat, bounds); + let mut snapped = snap_hit.map(|s| s.world).unwrap_or(raw); + self.tabs[i].snap_result = snap_hit; + + if snap_hit.is_none() { + let base = grip.origin_world; + if self.ortho_mode { + snapped = ortho_constrain(snapped, base); + } else if self.polar_mode { + snapped = polar_constrain(snapped, base, 45.0); + } + } + + let apply = if grip.is_translate { + GripApply::Translate(snapped - grip.last_world) + } else { + GripApply::Absolute(snapped) + }; + self.tabs[i].scene.apply_grip(grip.handle, grip.grip_id, apply); + self.tabs[i].dirty = true; + self.tabs[i].active_grip.as_mut().unwrap().last_world = snapped; + self.refresh_selected_grips(); + self.refresh_properties(); + return Task::none(); + } + + if self.tabs[i].active_cmd.is_some() { + let (vw, vh) = vp_size; + let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh }; + let cam = self.tabs[i].scene.camera.borrow(); + let cursor_world = cam.pick_on_target_plane(p, bounds); + let view_proj = cam.view_proj(bounds); + drop(cam); + + let all_wires = self.tabs[i].scene.entity_wires(); + let needs_tan = self.tabs[i] + .active_cmd.as_ref().map(|c| c.needs_tangent_pick()).unwrap_or(false); + self.tabs[i].snap_result = if needs_tan { + self.snapper.snap_tangent_only(cursor_world, p, &all_wires, view_proj, bounds) + } else { + self.snapper.snap(cursor_world, p, &all_wires, view_proj, bounds) + }; + let effective = { + let mut pt = self.tabs[i].snap_result.map(|s| s.world).unwrap_or(cursor_world); + if self.tabs[i].active_cmd.is_some() { pt.z = 0.0; } + if let Some(base) = self.last_point { + if self.ortho_mode { + pt = ortho_constrain(pt, base); + } else if self.polar_mode { + pt = polar_constrain(pt, base, 45.0); + } + } + pt + }; + self.tabs[i].last_cursor_world = effective; + + let needs_entity = self.tabs[i] + .active_cmd.as_ref().map(|c| c.needs_entity_pick()).unwrap_or(false); + let previews = if needs_entity { + let hover_handle = + scene::hit_test::click_hit(p, &all_wires, view_proj, bounds) + .and_then(|s| Scene::handle_from_wire_name(s)) + .unwrap_or(acadrust::Handle::NULL); + self.tabs[i].active_cmd.as_mut() + .map(|c| c.on_hover_entity(hover_handle, effective)) + .unwrap_or_default() + } else { + self.tabs[i].active_cmd.as_mut() + .map(|c| c.on_preview_wires(effective)) + .unwrap_or_default() + }; + self.tabs[i].scene.set_preview_wires(previews); + } else { + self.tabs[i].snap_result = None; + } + + Task::none() + } + + Message::ViewportExit => { + let i = self.active_tab; + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + sel.left_down = false; + sel.left_press_pos = None; + sel.left_press_time = None; + sel.left_dragging = false; + sel.right_down = false; + sel.right_press_pos = None; + sel.right_last_pos = None; + sel.right_dragging = false; + sel.middle_down = false; + sel.middle_last_pos = None; + sel.box_anchor = None; + sel.box_current = None; + sel.box_crossing = false; + sel.poly_active = false; + sel.poly_points.clear(); + sel.poly_crossing = false; + sel.context_menu = None; + Task::none() + } + + Message::ViewportLeftPress => { + let i = self.active_tab; + let (p, vp_size) = { + let sel = self.tabs[i].scene.selection.borrow(); + let p = match sel.last_move_pos { + Some(p) => p, + None => return Task::none(), + }; + (p, sel.vp_size) + }; + let (vw, vh) = vp_size; + let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh }; + + if vw > 1.0 && vh > 1.0 { + let cam = self.tabs[i].scene.camera.borrow(); + if scene::hit_test(p.x, p.y, vw, vh, cam.view_rotation_mat(), VIEWCUBE_PX).is_some() { + return Task::none(); + } + } + + if self.tabs[i].active_cmd.is_none() && !self.tabs[i].selected_grips.is_empty() { + if let Some(handle) = self.tabs[i].selected_handle { + let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); + let grip_hit = find_hit_grip(p, &self.tabs[i].selected_grips, vp_mat, bounds); + if let Some((grip_id, is_translate, world)) = grip_hit { + self.tabs[i].active_grip = Some(GripEdit { + handle, + grip_id, + is_translate, + origin_world: world, + last_world: world, + }); + return Task::none(); + } + } + } + + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + sel.context_menu = None; + sel.left_down = true; + sel.left_press_pos = Some(p); + sel.left_press_time = Some(Instant::now()); + sel.left_dragging = false; + Task::none() + } + + Message::ViewportLeftRelease => { + let i = self.active_tab; + let (p, is_click, is_down) = { + let sel = self.tabs[i].scene.selection.borrow(); + let p = match sel.last_move_pos { + Some(p) => p, + None => return Task::none(), + }; + (p, !sel.left_dragging, sel.left_down) + }; + + if self.tabs[i].active_grip.is_some() { + self.tabs[i].active_grip = None; + self.refresh_properties(); + return Task::none(); + } + + let is_gathering = self.tabs[i] + .active_cmd.as_ref().map(|c| c.is_selection_gathering()).unwrap_or(false); + + if is_down && is_click && self.tabs[i].active_cmd.is_some() && !is_gathering { + let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size; + let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh }; + + let snap_taken = self.tabs[i].snap_result.take(); + let tangent_obj_at_click = snap_taken.and_then(|s| s.tangent_obj); + + let world_pt = { + let raw = self.tabs[i].scene.camera.borrow().pick_on_target_plane(p, bounds); + let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); + let all_wires = self.tabs[i].scene.entity_wires(); + let needs_tan = self.tabs[i].active_cmd.as_ref() + .map(|c| c.needs_tangent_pick()).unwrap_or(false); + let snap_hit = if needs_tan { + self.snapper.snap_tangent_only(raw, p, &all_wires, vp_mat, bounds) + } else { + self.snapper.snap(raw, p, &all_wires, vp_mat, bounds) + }; + let mut pt = snap_hit.map(|s| s.world).unwrap_or(raw); + pt.z = 0.0; + if let Some(base) = self.last_point { + if self.ortho_mode { + pt = ortho_constrain(pt, base); + } else if self.polar_mode { + pt = polar_constrain(pt, base, 45.0); + } + } + pt + }; + + let result = if self.tabs[i].active_cmd.as_ref() + .map(|c| c.needs_entity_pick()).unwrap_or(false) + { + let vp_mat2 = self.tabs[i].scene.camera.borrow().view_proj(bounds); + let all_wires2 = self.tabs[i].scene.entity_wires(); + let hit = scene::hit_test::click_hit(p, &all_wires2, vp_mat2, bounds) + .and_then(|s| Scene::handle_from_wire_name(s)); + if let Some(handle) = hit { + self.tabs[i].active_cmd.as_mut().map(|c| c.on_entity_pick(handle, world_pt)) + } else { + self.command_line.push_info("Nothing found at that point."); + None + } + } else if self.tabs[i].active_cmd.as_ref() + .map(|c| c.needs_tangent_pick()).unwrap_or(false) + { + if let Some(obj) = tangent_obj_at_click { + self.tabs[i].active_cmd.as_mut().map(|c| c.on_tangent_point(obj, world_pt)) + } else { + self.command_line.push_info("Select a tangent object."); + None + } + } else { + self.last_point = Some(world_pt); + self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(world_pt)) + }; + + if let Some(r) = result { + let task = self.apply_cmd_result(r); + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + sel.left_down = false; + sel.left_press_pos = None; + sel.left_press_time = None; + sel.left_dragging = false; + return task; + } + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + sel.left_down = false; + sel.left_press_pos = None; + sel.left_press_time = None; + sel.left_dragging = false; + return Task::none(); + } + + let (is_down2, is_dragging, box_anchor, box_crossing, vp_size, elapsed_ms) = { + let sel = self.tabs[i].scene.selection.borrow(); + let elapsed = sel.left_press_time + .map(|t| Instant::now().duration_since(t).as_millis()) + .unwrap_or(u128::MAX); + (sel.left_down, sel.left_dragging, sel.box_anchor, sel.box_crossing, sel.vp_size, elapsed) + }; + + let mut selection_just_completed = false; + + if is_down2 { + let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vp_size.0, height: vp_size.1 }; + + if is_dragging { + if elapsed_ms < POLY_START_DELAY_MS { + if let Some(a) = box_anchor { + let crossing = box_crossing; + let all_wires = self.tabs[i].scene.entity_wires(); + let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); + let mut handles: Vec = scene::hit_test::box_hit( + a, p, crossing, &all_wires, vp_mat, bounds, + ).into_iter().filter_map(|s| Scene::handle_from_wire_name(s)).collect(); + handles.extend(scene::hit_test::box_hit_hatch( + a, p, crossing, &self.tabs[i].scene.hatches, vp_mat, bounds, + )); + self.tabs[i].scene.deselect_all(); + for h in &handles { self.tabs[i].scene.select_entity(*h, false); } + self.tabs[i].scene.expand_selection_for_groups(&handles); + self.refresh_properties(); + selection_just_completed = true; + } + } else { + let (poly_pts, crossing) = { + let sel = self.tabs[i].scene.selection.borrow(); + (sel.poly_points.clone(), sel.poly_crossing) + }; + self.tabs[i].scene.selection.borrow_mut().poly_last_crossing = crossing; + let all_wires = self.tabs[i].scene.entity_wires(); + let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); + let mut handles: Vec = scene::hit_test::poly_hit( + &poly_pts, crossing, &all_wires, vp_mat, bounds, + ).into_iter().filter_map(|s| Scene::handle_from_wire_name(s)).collect(); + handles.extend(scene::hit_test::poly_hit_hatch( + &poly_pts, crossing, &self.tabs[i].scene.hatches, vp_mat, bounds, + )); + self.tabs[i].scene.deselect_all(); + for h in &handles { self.tabs[i].scene.select_entity(*h, false); } + self.tabs[i].scene.expand_selection_for_groups(&handles); + self.refresh_properties(); + selection_just_completed = true; + } + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + sel.poly_active = false; + sel.poly_points.clear(); + sel.poly_crossing = false; + sel.box_anchor = None; + sel.box_current = None; + } else { + if box_anchor.is_none() { + let all_wires = self.tabs[i].scene.entity_wires(); + let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); + let hit = scene::hit_test::click_hit(p, &all_wires, vp_mat, bounds) + .and_then(|s| Scene::handle_from_wire_name(s)) + .or_else(|| scene::hit_test::click_hit_hatch( + p, &self.tabs[i].scene.hatches, vp_mat, bounds, + )); + if let Some(handle) = hit { + self.tabs[i].scene.select_entity(handle, true); + self.tabs[i].scene.expand_selection_for_groups(&[handle]); + self.refresh_properties(); + selection_just_completed = true; + } else { + self.tabs[i].scene.deselect_all(); + self.refresh_properties(); + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + sel.box_anchor = Some(p); + sel.box_current = Some(p); + sel.box_crossing = false; + } + } else { + let a = box_anchor.unwrap(); + let crossing = box_crossing; + let all_wires = self.tabs[i].scene.entity_wires(); + let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); + let mut handles: Vec = scene::hit_test::box_hit( + a, p, crossing, &all_wires, vp_mat, bounds, + ).into_iter().filter_map(|s| Scene::handle_from_wire_name(s)).collect(); + handles.extend(scene::hit_test::box_hit_hatch( + a, p, crossing, &self.tabs[i].scene.hatches, vp_mat, bounds, + )); + self.tabs[i].scene.deselect_all(); + for h in &handles { self.tabs[i].scene.select_entity(*h, false); } + self.tabs[i].scene.expand_selection_for_groups(&handles); + self.refresh_properties(); + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + sel.box_last = Some((a, p)); + sel.box_last_crossing = crossing; + sel.box_anchor = None; + sel.box_current = None; + sel.box_crossing = false; + selection_just_completed = true; + } + } + + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + sel.left_down = false; + sel.left_press_pos = None; + sel.left_press_time = None; + sel.left_dragging = false; + } + + if is_gathering && selection_just_completed { + let handles: Vec = self.tabs[i] + .scene.selected_entities().into_iter().map(|(h, _)| h).collect(); + if let Some(cmd) = self.tabs[i].active_cmd.as_mut() { + let result = cmd.on_selection_complete(handles); + return self.apply_cmd_result(result); + } + } + + Task::none() + } + + Message::ViewportRightPress => { + let i = self.active_tab; + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + let Some(p) = sel.last_move_pos else { return Task::none(); }; + sel.context_menu = None; + sel.right_down = true; + sel.right_press_pos = Some(p); + sel.right_last_pos = Some(p); + sel.right_dragging = false; + Task::none() + } + + Message::ViewportRightRelease => { + let i = self.active_tab; + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + let Some(_p) = sel.last_move_pos else { return Task::none(); }; + if sel.right_down { + if !sel.right_dragging { + sel.context_menu = sel.last_move_pos; + } + sel.right_down = false; + sel.right_press_pos = None; + sel.right_last_pos = None; + sel.right_dragging = false; + } + Task::none() + } + + Message::ViewportMiddlePress => { + let i = self.active_tab; + let now = Instant::now(); + let is_double = { + let sel = self.tabs[i].scene.selection.borrow(); + sel.middle_last_press_time + .map(|t| now.duration_since(t).as_millis() < 300) + .unwrap_or(false) + }; + { + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + let Some(p) = sel.last_move_pos else { return Task::none(); }; + sel.middle_down = true; + sel.middle_last_pos = Some(p); + sel.middle_last_press_time = Some(now); + } + if is_double { + self.tabs[i].scene.fit_all(); + self.command_line.push_output("Zoom Extents"); + } + Task::none() + } + + Message::ViewportMiddleRelease => { + let i = self.active_tab; + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + sel.middle_down = false; + sel.middle_last_pos = None; + Task::none() + } + + Message::ViewportScroll(delta) => { + let s = match delta { + mouse::ScrollDelta::Lines { y, .. } => y, + mouse::ScrollDelta::Pixels { y, .. } => y * 0.01, + }; + let i = self.active_tab; + let cursor = self.tabs[i].scene.selection.borrow().last_move_pos; + let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size; + let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh }; + let mut cam = self.tabs[i].scene.camera.borrow_mut(); + if let Some(cursor) = cursor { + cam.zoom_about_point(cursor, bounds, s); + } else { + cam.zoom(s); + } + Task::none() + } + + Message::ViewportClick => { + let i = self.active_tab; + let cam = self.tabs[i].scene.camera.borrow(); + let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size; + if let Some(region) = scene::hit_test( + self.cursor_pos.x, self.cursor_pos.y, vw, vh, cam.view_rotation_mat(), VIEWCUBE_PX, + ) { + return Task::done(Message::ViewCubeSnap(region)); + } + Task::none() + } + + Message::WindowResized(w, h) => { + self.vp_size = ((w - 440.0).max(200.0), h); + Task::none() + } + + Message::ViewCubeSnap(region) => { + let i = self.active_tab; + let mut region = region; + { + let mut cam = self.tabs[i].scene.camera.borrow_mut(); + let (target_yaw, target_pitch) = region.snap_angles(); + if angle_close(cam.yaw, target_yaw, 0.01) + && angle_close(cam.pitch, target_pitch, 0.01) + { + region = region.opposite(); + } + let (yaw, pitch) = region.snap_angles(); + cam.snap_to_angles(yaw, pitch); + } + self.tabs[i].scene.camera_generation += 1; + self.command_line.push_output(&format!("View: {}", region.label())); + Task::none() + } + + // ── Snap / mode toggles ─────────────────────────────────────── + Message::ToggleSnapEnabled => { self.snapper.toggle_global(); Task::none() } + Message::ToggleGridSnap => { self.snapper.toggle(crate::snap::SnapType::Grid); Task::none() } + Message::ToggleGrid => { self.show_grid ^= true; Task::none() } + Message::ToggleOrtho => { + self.ortho_mode ^= true; + if self.ortho_mode { self.polar_mode = false; } + Task::none() + } + Message::TogglePolar => { + self.polar_mode ^= true; + if self.polar_mode { self.ortho_mode = false; } + Task::none() + } + Message::ToggleSnap(t) => { self.snapper.toggle(t); Task::none() } + Message::ToggleSnapPopup => { self.snap_popup_open ^= true; Task::none() } + Message::CloseSnapPopup => { self.snap_popup_open = false; Task::none() } + Message::SnapSelectAll => { self.snapper.enable_all(); Task::none() } + Message::SnapClearAll => { self.snapper.disable_all(); Task::none() } + + // ── Ribbon dropdowns ────────────────────────────────────────── + Message::ToggleRibbonDropdown(id) => { self.ribbon.toggle_dropdown(&id); Task::none() } + Message::CloseRibbonDropdown => { self.ribbon.close_dropdown(); Task::none() } + Message::DropdownSelectItem { dropdown_id, cmd } => { + self.ribbon.select_dropdown_item(dropdown_id, cmd); + self.ribbon.activate_tool(cmd); + self.dispatch_command(cmd) + } + + Message::DeleteSelected => { + let i = self.active_tab; + let handles: Vec<_> = self.tabs[i].scene.selected.iter().cloned().collect(); + if !handles.is_empty() { + self.push_undo_snapshot(i, "ERASE"); + self.tabs[i].scene.erase_entities(&handles); + self.tabs[i].dirty = true; + self.refresh_properties(); + } + Task::none() + } + + // ── Properties panel messages ───────────────────────────────── + Message::PropSelectionGroupChanged(group) => { + self.tabs[self.active_tab].properties.selected_group = Some(group); + self.refresh_properties(); + Task::none() + } + + Message::RibbonLayerChanged(layer) => { + let i = self.active_tab; + self.tabs[i].active_layer = layer.clone(); + self.tabs[i].layers.current_layer = layer.clone(); + self.ribbon.active_layer = layer; + self.ribbon.close_dropdown(); + Task::none() + } + + Message::RibbonColorChanged(color) => { + self.ribbon.active_color = color; + self.ribbon.prop_color_palette_open = false; + self.ribbon.close_dropdown(); + Task::none() + } + Message::RibbonColorPaletteToggle => { + self.ribbon.prop_color_palette_open ^= true; + Task::none() + } + Message::RibbonLinetypeChanged(lt) => { + self.ribbon.active_linetype = lt; + self.ribbon.close_dropdown(); + Task::none() + } + Message::RibbonLineweightChanged(lw) => { + self.ribbon.active_lineweight = lw; + self.ribbon.close_dropdown(); + Task::none() + } + + Message::PropLayerChanged(layer) => { + let i = self.active_tab; + let handles = self.property_target_handles(i); + if !handles.is_empty() { + self.push_undo_snapshot(i, "CHPROP"); + for handle in handles { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { + crate::scene::dispatch::apply_common_prop(entity, "layer", &layer); + } + } + self.tabs[i].dirty = true; + self.refresh_properties(); + } + Task::none() + } + + Message::PropColorChanged(color) => { + let i = self.active_tab; + let handles = self.property_target_handles(i); + if !handles.is_empty() { + self.push_undo_snapshot(i, "CHPROP"); + for handle in handles { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { + crate::scene::dispatch::apply_color(entity, color); + } + } + self.tabs[i].properties.color_picker_open = false; + self.tabs[i].properties.color_palette_open = false; + self.tabs[i].dirty = true; + self.refresh_properties(); + } + Task::none() + } + + Message::PropLwChanged(lw) => { + let i = self.active_tab; + let handles = self.property_target_handles(i); + if !handles.is_empty() { + self.push_undo_snapshot(i, "CHPROP"); + for handle in handles { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { + crate::scene::dispatch::apply_line_weight(entity, lw); + } + } + self.tabs[i].dirty = true; + self.refresh_properties(); + } + Task::none() + } + + Message::PropLinetypeChanged(lt) => { + let i = self.active_tab; + let handles = self.property_target_handles(i); + if !handles.is_empty() { + self.push_undo_snapshot(i, "CHPROP"); + for handle in handles { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { + crate::scene::dispatch::apply_common_prop(entity, "linetype", <); + } + } + self.tabs[i].dirty = true; + self.refresh_properties(); + } + Task::none() + } + + Message::PropHatchPatternChanged(name) => { + let i = self.active_tab; + let handles = self.property_target_handles(i); + if !handles.is_empty() { + use crate::scene::hatch_patterns; + if let Some(entry) = hatch_patterns::find(&name) { + self.push_undo_snapshot(i, "HATCHEDIT"); + for handle in handles { + if let Some(acadrust::EntityType::Hatch(dxf)) = + self.tabs[i].scene.document.get_entity_mut(handle) + { + dxf.pattern = hatch_patterns::build_dxf_pattern(entry); + dxf.is_solid = matches!( + entry.gpu, + crate::scene::hatch_model::HatchPattern::Solid + ); + } + if let Some(model) = self.tabs[i].scene.hatches.get_mut(&handle) { + model.pattern = entry.gpu.clone(); + model.name = name.clone(); + } + } + self.tabs[i].dirty = true; + self.refresh_properties(); + } + } + Task::none() + } + + Message::PropBoolToggle(field) => { + let i = self.active_tab; + let handles = self.property_target_handles(i); + if !handles.is_empty() { + self.push_undo_snapshot(i, "CHPROP"); + for handle in handles { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { + match field { + "invisible" => crate::scene::dispatch::toggle_invisible(entity), + _ => crate::scene::dispatch::apply_geom_prop(entity, field, "toggle"), + } + } + } + self.tabs[i].dirty = true; + self.refresh_properties(); + } + Task::none() + } + + Message::PropGeomChoiceChanged { field, value } => { + let i = self.active_tab; + let handles = self.property_target_handles(i); + if !handles.is_empty() { + self.push_undo_snapshot(i, "CHPROP"); + for handle in handles { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { + crate::scene::dispatch::apply_geom_prop(entity, field, &value); + } + } + self.tabs[i].dirty = true; + self.refresh_properties(); + } + Task::none() + } + + Message::PropGeomInput { field, value } => { + self.tabs[self.active_tab].properties.edit_buf.insert(field.to_string(), value); + Task::none() + } + + Message::PropGeomCommit(field) => { + let i = self.active_tab; + let handles = self.property_target_handles(i); + if !handles.is_empty() { + if let Some(val) = self.tabs[i].properties.edit_buf.remove(field) { + self.push_undo_snapshot(i, "CHPROP"); + for handle in handles { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { + match field { + "linetype_scale" | "transparency" => { + crate::scene::dispatch::apply_common_prop(entity, field, &val); + } + _ => { + crate::scene::dispatch::apply_geom_prop(entity, field, &val); + } + } + } + } + self.tabs[i].dirty = true; + self.refresh_properties(); + } + } + Task::none() + } + + Message::PropColorPickerToggle => { + let i = self.active_tab; + self.tabs[i].properties.color_picker_open = !self.tabs[i].properties.color_picker_open; + if self.tabs[i].properties.color_picker_open { + self.tabs[i].properties.color_palette_open = false; + } + Task::none() + } + + Message::PropColorPaletteToggle => { + self.tabs[self.active_tab].properties.color_palette_open = + !self.tabs[self.active_tab].properties.color_palette_open; + Task::none() + } + + Message::LayoutSwitch(name) => { + let i = self.active_tab; + let going_to_paper = name != "Model"; + self.tabs[i].scene.current_layout = name; + self.tabs[i].scene.deselect_all(); + self.tabs[i].scene.fit_all(); + if going_to_paper { + if let Some(idx) = self.ribbon.layout_module_index() { + self.ribbon.select(idx); + } + } else if self.ribbon.active_is_layout() { + self.ribbon.select(0); + } + Task::none() + } + + Message::LayoutCreate => { + let i = self.active_tab; + let count = self.tabs[i].scene.layout_names().len(); + let new_name = format!("Layout{}", count); + self.push_undo_snapshot(i, "LAYOUT"); + match self.tabs[i].scene.document.add_layout(&new_name) { + Ok(_) => { + self.tabs[i].scene.current_layout = new_name.clone(); + self.tabs[i].scene.deselect_all(); + self.tabs[i].scene.fit_all(); + if let Some(idx) = self.ribbon.layout_module_index() { + self.ribbon.select(idx); + } + self.command_line.push_output(&format!( + "Layout \"{new_name}\" oluşturuldu — MVIEW ile viewport ekleyin" + )); + self.tabs[i].dirty = true; + } + Err(e) => self.command_line.push_error(&format!("Layout oluşturulamadı: {e}")), + } + Task::none() + } + + Message::Undo => { self.undo_active_tab(); Task::none() } + Message::Redo => { self.redo_active_tab(); Task::none() } + + Message::UndoMany(steps) => { + self.ribbon.close_dropdown(); + self.undo_steps(steps); + Task::none() + } + + Message::RedoMany(steps) => { + self.ribbon.close_dropdown(); + self.redo_steps(steps); + Task::none() + } + + Message::Noop => Task::none(), + } + } +} diff --git a/src/app/view.rs b/src/app/view.rs new file mode 100644 index 00000000..00971d51 --- /dev/null +++ b/src/app/view.rs @@ -0,0 +1,369 @@ +use super::{H7CAD, Message}; +use super::document::DocumentTab; +use super::history::history_dropdown_labels; +use super::helpers::grid_plane_from_camera; +use crate::scene::{VIEWCUBE_DRAW_PX, VIEWCUBE_PAD}; +use crate::scene::grip::grips_to_screen; +use crate::ui::overlay; +use iced::widget::{button, column, container, mouse_area, row, shader, stack, text, Row}; +use iced::window; +use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme}; + +const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_DRAW_PX; + +impl H7CAD { + pub fn view(&self, window_id: window::Id) -> Element<'_, Message> { + // ── Layer Properties Manager window ─────────────────────────────── + if Some(window_id) == self.layer_window { + let tab = &self.tabs[self.active_tab]; + return tab.layers.view_window(); + } + + let i = self.active_tab; + let tab = &self.tabs[i]; + let is_paper = tab.scene.current_layout != "Model"; + let viewport_3d = shader(&tab.scene).width(Fill).height(Fill); + + let selection_overlay = { + let sel = tab.scene.selection.borrow().clone(); + let snap_info = tab.snap_result.map(|s| (s.screen, s.snap_type)); + + let grips: Vec = + if tab.active_cmd.is_none() && !tab.selected_grips.is_empty() { + let (vw, vh) = tab.scene.selection.borrow().vp_size; + let bounds = iced::Rectangle { + x: 0.0, y: 0.0, width: vw, height: vh, + }; + let vp_mat = tab.scene.camera.borrow().view_proj(bounds); + let sel_h = tab.selected_handle; + grips_to_screen(&tab.selected_grips, vp_mat, bounds) + .into_iter() + .filter(|(_, screen, _, _)| { + screen.x.is_finite() + && screen.y.is_finite() + && screen.x >= -bounds.width + && screen.x <= bounds.width * 2.0 + && screen.y >= -bounds.height + && screen.y <= bounds.height * 2.0 + }) + .map(|(grip_id, screen, _is_midpoint, shape)| { + let is_hot = tab + .active_grip + .as_ref() + .map_or(false, |g| Some(g.handle) == sel_h && g.grip_id == grip_id); + overlay::GripMarker { pos: screen, shape, is_hot } + }) + .collect() + } else { + vec![] + }; + + let grid = if self.show_grid { + let (vw, vh) = tab.scene.selection.borrow().vp_size; + let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh }; + let cam = tab.scene.camera.borrow(); + let plane = grid_plane_from_camera(cam.pitch, cam.yaw); + Some(overlay::GridParams { + view_proj: cam.view_proj(bounds), + bounds, + plane, + }) + } else { + None + }; + overlay::selection_overlay(sel, snap_info, grips, grid) + }; + + let nav = container(overlay::nav_toolbar()) + .align_right(Fill) + .align_top(Fill) + .padding(iced::Padding { top: 148.0, right: 8.0, bottom: 0.0, left: 0.0 }); + + let info = container(overlay::info_bar( + if is_paper { &tab.scene.current_layout } else { "Custom View" }, + &tab.visual_style, + )) + .padding([4, 6]); + + let viewport_mouse = mouse_area(container( + iced::widget::Space::new().width(Fill).height(Fill), + )) + .on_move(Message::ViewportMove) + .on_press(Message::ViewportLeftPress) + .on_release(Message::ViewportLeftRelease) + .on_right_press(Message::ViewportRightPress) + .on_right_release(Message::ViewportRightRelease) + .on_middle_press(Message::ViewportMiddlePress) + .on_middle_release(Message::ViewportMiddleRelease) + .on_scroll(Message::ViewportScroll) + .on_exit(Message::ViewportExit); + + let cube_click = mouse_area(container( + iced::widget::Space::new() + .width(iced::Length::Fixed(VIEWCUBE_HIT_SIZE)) + .height(iced::Length::Fixed(VIEWCUBE_HIT_SIZE)), + )) + .on_move(Message::CursorMoved) + .on_press(Message::ViewportClick); + + let cube_click = container(cube_click) + .align_right(Fill) + .align_top(Fill) + .padding(iced::Padding { top: VIEWCUBE_PAD, right: VIEWCUBE_PAD, bottom: 0.0, left: 0.0 }) + .width(Fill) + .height(Fill); + + let bg_color = if is_paper { + Color { r: 0.22, g: 0.24, b: 0.28, a: 1.0 } + } else { + Color { r: 0.11, g: 0.11, b: 0.11, a: 1.0 } + }; + + let viewport_stack = stack![ + container(viewport_3d) + .style(move |_: &Theme| container::Style { + background: Some(Background::Color(bg_color)), + ..Default::default() + }) + .width(Fill) + .height(Fill), + container(info).width(Fill).height(Fill), + selection_overlay, + viewport_mouse, + nav, + cube_click, + ] + .width(Fill) + .height(Fill); + + let center_stack = iced::widget::stack![ + row![tab.properties.view(), viewport_stack] + .width(Fill) + .height(Fill), + ] + .width(Fill) + .height(Fill); + + let tab_bar = doc_tab_bar(&self.tabs, self.active_tab); + + let main_ui = container( + column![ + self.ribbon.view( + is_paper, + self.tabs[self.active_tab].history.undo_stack.len(), + self.tabs[self.active_tab].history.redo_stack.len(), + ), + tab_bar, + center_stack, + self.command_line.view(), + self.status_bar.view( + &self.snapper, + self.snap_popup_open, + self.ortho_mode, + self.polar_mode, + self.show_grid, + tab.scene.layout_names(), + tab.scene.current_layout.clone() + ) + ] + .width(Fill) + .height(Fill), + ) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(Color { r: 0.11, g: 0.11, b: 0.11, a: 1.0 })), + ..Default::default() + }) + .width(Fill) + .height(Fill); + + let snap_layer: Element<'_, Message> = if self.snap_popup_open { + crate::ui::snap_popup::snap_popup_overlay(&self.snapper, 4.0) + } else { + iced::widget::Space::new().width(0).height(0).into() + }; + + let dropdown_layer: Element<'_, Message> = self + .ribbon + .dropdown_overlay( + &history_dropdown_labels(&self.tabs[self.active_tab].history.undo_stack), + &history_dropdown_labels(&self.tabs[self.active_tab].history.redo_stack), + ) + .unwrap_or_else(|| iced::widget::Space::new().width(0).height(0).into()); + + stack![main_ui, self.app_menu.view(), snap_layer, dropdown_layer].into() + } + + pub fn subscription(&self) -> Subscription { + use iced::event; + iced::Subscription::batch([ + window::frames().map(Message::Tick), + event::listen_with(|ev, status, win_id| { + use iced::event::Status; + match ev { + iced::Event::Window(window::Event::Closed) => { + Some(Message::OsWindowClosed(win_id)) + } + iced::Event::Window(window::Event::Resized(sz)) => { + Some(Message::WindowResized(sz.width as f32, sz.height as f32)) + } + iced::Event::Keyboard(keyboard::Event::KeyPressed { + key, modifiers, .. + }) => { + let ctrl = modifiers.control(); + let shift = modifiers.shift(); + match key { + keyboard::Key::Named(keyboard::key::Named::Enter) + | keyboard::Key::Named(keyboard::key::Named::Space) + if status == Status::Ignored => + { + Some(Message::CommandFinalize) + } + keyboard::Key::Named(keyboard::key::Named::Escape) => { + Some(Message::CommandEscape) + } + keyboard::Key::Named(keyboard::key::Named::Delete) + if status == Status::Ignored => + { + Some(Message::DeleteSelected) + } + keyboard::Key::Named(keyboard::key::Named::F3) => { + Some(Message::ToggleSnapEnabled) + } + keyboard::Key::Named(keyboard::key::Named::F7) => { + Some(Message::ToggleGrid) + } + keyboard::Key::Named(keyboard::key::Named::F8) => { + Some(Message::ToggleOrtho) + } + keyboard::Key::Named(keyboard::key::Named::F9) => { + Some(Message::ToggleGridSnap) + } + keyboard::Key::Named(keyboard::key::Named::F10) => { + Some(Message::TogglePolar) + } + keyboard::Key::Character(c) if ctrl => match c.as_str() { + "n" => Some(Message::ClearScene), + "o" => Some(Message::OpenFile), + "s" if !shift => Some(Message::SaveFile), + "s" if shift => Some(Message::SaveAs), + "z" if !shift => Some(Message::Undo), + "z" if shift => Some(Message::Redo), + "y" => Some(Message::Redo), + "c" => Some(Message::Command("COPYCLIP".to_string())), + "x" => Some(Message::Command("CUTCLIP".to_string())), + "v" => Some(Message::Command("PASTECLIP".to_string())), + _ => None, + }, + _ => None, + } + } + _ => None, + } + }), + ]) + } + + pub(super) fn focus_cmd_input(&self) -> Task { + iced::widget::operation::focus(iced::widget::Id::new(crate::ui::command_line::CMD_INPUT_ID)) + } + + pub(super) fn blur_cmd_input(&self) -> Task { + let op = iced::advanced::widget::operation::focusable::unfocus::(); + iced::advanced::widget::operate(op) + } +} + +// ── Document tab bar ─────────────────────────────────────────────────────── + +pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Element<'a, Message> { + const BAR_BG: Color = Color { r: 0.13, g: 0.13, b: 0.13, a: 1.0 }; + const TAB_ACTIVE: Color = Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 }; + const TAB_HOVER: Color = Color { r: 0.18, g: 0.18, b: 0.18, a: 1.0 }; + const TAB_INACTIVE: Color = Color { r: 0.13, g: 0.13, b: 0.13, a: 1.0 }; + const ACCENT: Color = Color { r: 0.20, g: 0.55, b: 0.90, a: 1.0 }; + const TEXT_ACTIVE: Color = Color::WHITE; + const TEXT_INACTIVE: Color = Color { r: 0.60, g: 0.60, b: 0.60, a: 1.0 }; + const CLOSE_HOVER: Color = Color { r: 0.70, g: 0.22, b: 0.22, a: 1.0 }; + const BORDER_COLOR: Color = Color { r: 0.25, g: 0.25, b: 0.25, a: 1.0 }; + + let mut bar = Row::new().spacing(0).align_y(iced::Center); + + for (idx, tab) in tabs.iter().enumerate() { + let is_active = idx == active_tab; + let name = tab.tab_display_name(); + let label = if tab.dirty { format!("● {}", name) } else { name }; + + let title_btn = button(text(label).size(12)) + .on_press(Message::TabSwitch(idx)) + .padding([5, 12]) + .style(move |_: &Theme, status| button::Style { + background: Some(Background::Color(match (is_active, status) { + (true, _) => TAB_ACTIVE, + (false, button::Status::Hovered) => TAB_HOVER, + _ => TAB_INACTIVE, + })), + text_color: if is_active { TEXT_ACTIVE } else { TEXT_INACTIVE }, + border: Border { + color: if is_active { ACCENT } else { Color::TRANSPARENT }, + width: if is_active { 1.0 } else { 0.0 }, + radius: 0.0.into(), + }, + shadow: iced::Shadow::default(), + snap: false, + }); + + let close_btn = button(text("×").size(11).color(Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 })) + .on_press(Message::TabClose(idx)) + .padding([3, 5]) + .style(move |_: &Theme, status| button::Style { + background: Some(Background::Color(match status { + button::Status::Hovered => CLOSE_HOVER, + _ => if is_active { TAB_ACTIVE } else { TAB_INACTIVE }, + })), + border: Border { radius: 3.0.into(), ..Default::default() }, + ..Default::default() + }); + + bar = bar.push( + container(row![title_btn, close_btn].spacing(0).align_y(iced::Center)) + .style(move |_: &Theme| container::Style { + border: Border { + color: if is_active { BORDER_COLOR } else { Color::TRANSPARENT }, + width: if is_active { 1.0 } else { 0.0 }, + radius: 0.0.into(), + }, + ..Default::default() + }), + ); + } + + let new_btn = button(text("+").size(14).color(Color { r: 0.65, g: 0.65, b: 0.65, a: 1.0 })) + .on_press(Message::TabNew) + .padding([4, 10]) + .style(|_: &Theme, status| button::Style { + background: Some(Background::Color(match status { + button::Status::Hovered => TAB_HOVER, + _ => Color::TRANSPARENT, + })), + border: Border { radius: 0.0.into(), ..Default::default() }, + ..Default::default() + }); + + bar = bar.push(new_btn); + bar = bar.push(iced::widget::Space::new().width(Fill)); + + container(bar) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(BAR_BG)), + border: Border { + color: BORDER_COLOR, + width: 1.0, + radius: 0.0.into(), + }, + ..Default::default() + }) + .height(30) + .width(Fill) + .padding([0, 2]) + .into() +} diff --git a/src/main.rs b/src/main.rs index c1d29774..e83c1a52 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod app; mod command; mod entities; mod io; @@ -8,4516 +9,6 @@ mod scene; mod snap; mod ui; -use acadrust::types::{Color as AcadColor, LineWeight}; -use acadrust::{CadDocument, EntityType, Handle}; -use command::{CadCommand, CmdResult}; -use modules::ModuleEvent; -use scene::dispatch; -use scene::grip::{find_hit_grip, grips_to_screen, GripEdit}; -use scene::object::GripApply; -use snap::{SnapResult, Snapper}; -use std::collections::HashSet; -use std::path::PathBuf; - -use scene::{CubeRegion, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX}; -use ui::{overlay, AppMenu, CommandLine, LayerPanel, PropertiesPanel, Ribbon, StatusBar}; - -use iced::time::Instant; -use iced::widget::{button, column, container, mouse_area, row, shader, stack, text, Row}; -use iced::window; -use iced::{mouse, Background, Border, Color, Element, Fill, Point, Subscription, Task, Theme}; - -const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_DRAW_PX; -const POLY_START_DELAY_MS: u128 = 150; -const VARIES_LABEL: &str = "*VARIES*"; - -/// Parse a typed coordinate string into a world-space Vec3. -/// Accepts "x,y" → Vec3(x, 0, y) (XZ drawing plane, Y-up) -/// "x,y,z"→ Vec3(x, y, z) (full 3D) -/// Separators: comma or semicolon; decimal point or decimal comma. -fn parse_coord(text: &str) -> Option { - // Split on comma or semicolon, ignore spaces. - let parts: Vec = text - .split(|c| c == ',' || c == ';') - .map(|s| s.trim().replace(',', ".")) - .filter_map(|s| s.parse().ok()) - .collect(); - match parts.as_slice() { - [x, y] => Some(glam::Vec3::new(*x, *y, 0.0)), - [x, y, z] => Some(glam::Vec3::new(*x, *y, *z)), - _ => None, - } -} - -fn angle_close(a: f32, b: f32, tol: f32) -> bool { - let diff = (a - b).rem_euclid(std::f32::consts::TAU); - let diff = if diff > std::f32::consts::PI { - diff - std::f32::consts::TAU - } else { - diff - }; - diff.abs() < tol -} - fn main() -> iced::Result { - iced::daemon(H7CAD::boot, H7CAD::update, H7CAD::view) - .subscription(H7CAD::subscription) - .title(|state: &H7CAD, window_id: window::Id| { - if Some(window_id) == state.layer_window { - "Layer Properties Manager".to_string() - } else if let Some(tab) = state.tabs.get(state.active_tab) { - let dot = if tab.dirty { "● " } else { "" }; - let name = tab.tab_display_name(); - format!("{}H7CAD — {}", dot, name) - } else { - "H7CAD".to_string() - } - }) - .theme(Theme::Dark) - .run() -} - -// ── Per-document tab state ───────────────────────────────────────────────── - -struct DocumentTab { - scene: Scene, - current_path: Option, - dirty: bool, - tab_title: String, - properties: PropertiesPanel, - layers: LayerPanel, - active_cmd: Option>, - last_cmd: Option, - snap_result: Option, - active_grip: Option, - selected_grips: Vec, - selected_handle: Option, - wireframe: bool, - visual_style: String, - last_cursor_world: glam::Vec3, - history: HistoryState, - active_layer: String, -} - -impl DocumentTab { - fn new_drawing(n: usize) -> Self { - let mut scene = Scene::new(); - linetypes::populate_document(&mut scene.document); - Self { - scene, - current_path: None, - dirty: false, - tab_title: format!("Drawing{}", n), - properties: PropertiesPanel::empty(), - layers: LayerPanel::default(), - active_cmd: None, - last_cmd: None, - snap_result: None, - active_grip: None, - selected_grips: vec![], - selected_handle: None, - wireframe: false, - visual_style: "Shaded".into(), - last_cursor_world: glam::Vec3::ZERO, - history: HistoryState::default(), - active_layer: "0".to_string(), - } - } - - fn tab_display_name(&self) -> String { - match &self.current_path { - Some(p) => p - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string(), - None => self.tab_title.clone(), - } - } -} - -#[derive(Clone)] -struct HistorySnapshot { - document: CadDocument, - current_layout: String, - selected: Vec, - dirty: bool, - label: String, -} - -#[derive(Default)] -struct HistoryState { - undo_stack: Vec, - redo_stack: Vec, -} - -// ── Application state ────────────────────────────────────────────────────── - -struct H7CAD { - start: Instant, - tabs: Vec, - active_tab: usize, - tab_counter: usize, - ribbon: Ribbon, - app_menu: AppMenu, - command_line: CommandLine, - status_bar: StatusBar, - cursor_pos: Point, - vp_size: (f32, f32), - snapper: Snapper, - snap_popup_open: bool, - /// Whether Tangent snap was enabled before a tangent-pick command started. - pre_cmd_tangent: Option, - /// Orthogonal drawing constraint (F8): constrains picks to 0°/90°/180°/270°. - ortho_mode: bool, - /// Polar tracking (F10): constrains picks to 45° angle increments. - polar_mode: bool, - /// Show grid lines in the viewport (F7). - show_grid: bool, - /// Last point committed by a drawing command — used as ortho/polar base. - last_point: Option, - /// OS window Id for the floating Layer Properties Manager (None when closed). - layer_window: Option, - /// OS window Id of the primary application window. - main_window: Option, - /// In-memory clipboard: cloned entities waiting to be pasted. - clipboard: Vec, - /// Centroid of the clipboard entities (XZ plane, Y-up). - clipboard_centroid: glam::Vec3, -} - -#[derive(Debug, Clone)] -pub enum Message { - Tick(Instant), - OpenFile, - FileOpened(Result<(String, PathBuf, CadDocument), String>), - SaveFile, - SaveAs, - PickedSavePath(Option), - ClearScene, - SetWireframe(bool), - /// Switch camera projection: true = Orthographic, false = Perspective. - SetProjection(bool), - /// Select a ribbon module tab by index. - RibbonSelectTab(usize), - /// A ribbon tool button was clicked — highlights the tool and dispatches its event. - RibbonToolClick { - tool_id: String, - event: ModuleEvent, - }, - // ── Application menu ────────────────────────────────────────────────── - ToggleAppMenu, - CloseAppMenu, - /// Close the menu and immediately dispatch a CAD command. - CloseAppMenuAndRun(String), - AppMenuSearch(String), - // ── Document tabs ────────────────────────────────────────────────────── - /// Create a new empty document tab. - TabNew, - /// Switch to the given tab index. - TabSwitch(usize), - /// Close the given tab index. - TabClose(usize), - // ───────────────────────────────────────────────────────────────────── - CommandInput(String), - CommandSubmit, - Command(String), - ToggleLayers, - /// Emitted when the main application window has been opened (daemon boot). - MainWindowOpened(window::Id), - /// Emitted when the layer window has been successfully opened. - LayerWindowOpened(window::Id), - LayerToggleVisible(usize), - LayerToggleLock(usize), - LayerToggleFreeze(usize), - LayerNew, - LayerDelete, - LayerSetCurrent, - LayerSelect(usize), - LayerRenameStart(usize), - LayerRenameEdit(String), - LayerColorPickerToggle(usize), - LayerColorMorePalette, - LayerColorSet(u8), - LayerLinetypeSet(String), - LayerLineweightSet(LineWeight), - LayerTransparencyEdit(usize, String), - LayerRenameCommit, - CursorMoved(Point), - ViewportClick, - ViewportMove(Point), - ViewportLeftPress, - ViewportLeftRelease, - ViewportRightPress, - ViewportRightRelease, - ViewportMiddlePress, - ViewportMiddleRelease, - ViewportScroll(mouse::ScrollDelta), - ViewportExit, - ViewCubeSnap(CubeRegion), - WindowResized(f32, f32), - /// Enter pressed globally — finalises the active command (no text-input involvement). - CommandFinalize, - /// Escape pressed globally — cancels the active command. - CommandEscape, - /// Toggle the global snap on/off (OSNAP button body click). - ToggleSnapEnabled, - /// Toggle grid-snap on/off — F9 / SNAP status-bar button. - ToggleGridSnap, - /// Toggle grid display in the viewport — F7 / GRID status-bar button. - ToggleGrid, - /// Toggle orthogonal drawing constraint — F8 / ORTHO status-bar button. - ToggleOrtho, - /// Toggle polar-angle constraint (45° increments) — F10 / POLAR status-bar button. - TogglePolar, - /// Toggle an individual snap mode (from popup row click). - ToggleSnap(snap::SnapType), - /// Open / close the OSNAP popup (▾ arrow click). - ToggleSnapPopup, - /// Close the OSNAP popup (click-catcher outside the panel). - CloseSnapPopup, - /// Enable all snap modes. - SnapSelectAll, - /// Disable all snap modes. - SnapClearAll, - /// Toggle a ribbon dropdown open/closed. - ToggleRibbonDropdown(String), - /// Close any open ribbon dropdown (click-catcher outside the panel). - CloseRibbonDropdown, - /// User selected a specific item from a ribbon dropdown. - DropdownSelectItem { - dropdown_id: &'static str, - cmd: &'static str, - }, - /// Delete key — erase all currently selected entities. - DeleteSelected, - Undo, - Redo, - UndoMany(usize), - RedoMany(usize), - // ── Ribbon ──────────────────────────────────────────────────────────── - /// User selected a layer from the layer combobox in the ribbon. - RibbonLayerChanged(String), - /// User changed the active color in the Properties toolbar. - RibbonColorChanged(AcadColor), - /// Toggle the full ACI palette inside the ribbon color picker. - RibbonColorPaletteToggle, - /// User changed the active linetype in the Properties toolbar. - RibbonLinetypeChanged(String), - /// User changed the active lineweight in the Properties toolbar. - RibbonLineweightChanged(LineWeight), - - // ── Properties panel ────────────────────────────────────────────────── - /// User selected a layer from the layer pick_list in the Properties panel. - PropLayerChanged(String), - PropSelectionGroupChanged(ui::properties::SelectionGroup), - /// User picked a color from the Properties color picker. - PropColorChanged(AcadColor), - /// User selected a lineweight from the Properties pick_list. - PropLwChanged(LineWeight), - /// User selected a linetype from the linetype pick_list. - PropLinetypeChanged(String), - /// User toggled a boolean property (e.g. Invisible). - PropBoolToggle(&'static str), - /// User selected a hatch pattern from the pattern pick_list in Properties. - PropHatchPatternChanged(String), - /// User selected a generic choice field in the Properties panel. - PropGeomChoiceChanged { - field: &'static str, - value: String, - }, - /// User is typing in an editable geometry field (live buffer update). - PropGeomInput { - field: &'static str, - value: String, - }, - /// User committed a geometry/common field edit (Enter pressed). - PropGeomCommit(&'static str), - /// Toggle the inline color picker dropdown open/closed. - PropColorPickerToggle, - /// Toggle the full ACI colour palette expansion. - PropColorPaletteToggle, - /// Switch to a named layout ("Model" or paper space layout name). - LayoutSwitch(String), - /// Create a new paper space layout. - LayoutCreate, - /// A window was closed by the OS (e.g. the user clicked the title-bar ✕). - OsWindowClosed(window::Id), - /// No-op — used as a fallback when a TabEvent has no host mapping. - Noop, -} - -impl H7CAD { - fn new() -> Self { - let first_tab = DocumentTab::new_drawing(1); - let mut app = Self { - start: Instant::now(), - tabs: vec![first_tab], - active_tab: 0, - tab_counter: 1, - ribbon: Ribbon::new(), - app_menu: AppMenu::new(), - command_line: CommandLine::new(), - status_bar: StatusBar::new(), - cursor_pos: Point::ORIGIN, - vp_size: (1280.0, 720.0), - snapper: Snapper::default(), - snap_popup_open: false, - pre_cmd_tangent: None, - ortho_mode: false, - polar_mode: false, - show_grid: false, - last_point: None, - layer_window: None, - main_window: None, - clipboard: Vec::new(), - clipboard_centroid: glam::Vec3::ZERO, - }; - app.sync_ribbon_layers(); - app - } - - /// Boot function for `iced::daemon`: returns initial state plus a task that - /// opens the primary application window. - fn boot() -> (Self, Task) { - let state = Self::new(); - let (id, open_task) = window::open(window::Settings { - maximized: true, - icon: window::icon::from_rgba(build_window_icon(), 32, 32).ok(), - ..Default::default() - }); - let mut s = state; - s.main_window = Some(id); - let task = open_task.map(|_| Message::Noop); - (s, task) - } - - fn update(&mut self, msg: Message) -> Task { - match msg { - Message::Tick(t) => { - self.tabs[self.active_tab].scene.update(t - self.start); - Task::none() - } - - Message::OpenFile => Task::perform(io::pick_and_open(), Message::FileOpened), - - Message::FileOpened(Ok((name, path, doc))) => { - let entity_count = doc.entities().count(); - self.command_line - .push_output(&format!("Opened \"{name}\" — {entity_count} entities")); - self.app_menu.push_recent(path.clone()); - - // Eğer aktif sekme boşsa (kaydedilmemiş, yeni çizim, entity yok) dosyayı oraya yükle. - // Aksi hâlde yeni bir sekme aç. - let current_is_empty = { - let t = &self.tabs[self.active_tab]; - t.current_path.is_none() - && !t.dirty - && self.tabs[self.active_tab].scene.document.entities().count() == 0 - }; - let i = if current_is_empty { - self.active_tab - } else { - self.tab_counter += 1; - let new_tab = DocumentTab::new_drawing(self.tab_counter); - self.tabs.push(new_tab); - let idx = self.tabs.len() - 1; - self.active_tab = idx; - idx - }; - - self.tabs[i].current_path = Some(path); - self.tabs[i].scene.document = doc; - self.tabs[i].scene.populate_hatches_from_document(); - self.tabs[i].scene.selected = std::collections::HashSet::new(); - self.tabs[i].scene.preview_wires = vec![]; - self.tabs[i].scene.current_layout = "Model".to_string(); - // Merge standard linetypes not already present in the file. - linetypes::populate_document(&mut self.tabs[i].scene.document); - self.tabs[i].properties = PropertiesPanel::empty(); - let doc_layers = self.tabs[i].scene.document.layers.clone(); - self.tabs[i].layers.sync_from_doc(&doc_layers); - self.sync_ribbon_layers(); - self.tabs[i].scene.fit_all(); - self.tabs[i].dirty = false; - self.tabs[i].history = HistoryState::default(); - self.refresh_selected_grips(); - Task::none() - } - - Message::FileOpened(Err(e)) => { - if e != "Cancelled" { - self.command_line.push_error(&format!("Open failed: {e}")); - } - Task::none() - } - - Message::SaveFile => { - let i = self.active_tab; - if let Some(path) = &self.tabs[i].current_path { - let path = path.clone(); - match io::save(&self.tabs[i].scene.document, &path) { - Ok(()) => { - self.command_line - .push_output(&format!("Saved: {}", path.display())); - self.tabs[i].dirty = false; - } - Err(e) => self.command_line.push_error(&format!("Save failed: {e}")), - } - } else { - return Task::perform(io::pick_save_path(), Message::PickedSavePath); - } - Task::none() - } - - Message::SaveAs => Task::perform(io::pick_save_path(), Message::PickedSavePath), - - Message::PickedSavePath(Some(path)) => { - let i = self.active_tab; - match io::save(&self.tabs[i].scene.document, &path) { - Ok(()) => { - self.command_line - .push_output(&format!("Saved: {}", path.display())); - self.tabs[i].current_path = Some(path); - self.tabs[i].dirty = false; - } - Err(e) => self.command_line.push_error(&format!("Save failed: {e}")), - } - Task::none() - } - - Message::PickedSavePath(None) => Task::none(), - - Message::ClearScene => { - let i = self.active_tab; - self.push_undo_snapshot(i, "NEW"); - self.tabs[i].scene.clear(); - linetypes::populate_document(&mut self.tabs[i].scene.document); - self.tabs[i].properties = PropertiesPanel::empty(); - let doc_layers = self.tabs[i].scene.document.layers.clone(); - self.tabs[i].layers.sync_from_doc(&doc_layers); - self.command_line - .push_output("Scene cleared. Standard linetypes loaded."); - self.tabs[i].current_path = None; - self.tabs[i].dirty = true; - self.sync_ribbon_layers(); - Task::none() - } - - Message::SetWireframe(w) => { - let i = self.active_tab; - self.tabs[i].wireframe = w; - self.ribbon.set_wireframe(w); - self.tabs[i].visual_style = if w { - "Wireframe".into() - } else { - "Shaded".into() - }; - self.command_line.push_output(if w { - "Visual style: Wireframe" - } else { - "Visual style: Shaded" - }); - Task::none() - } - - Message::SetProjection(ortho) => { - use crate::scene::Projection; - let proj = if ortho { - Projection::Orthographic - } else { - Projection::Perspective - }; - let i = self.active_tab; - self.tabs[i].scene.camera.borrow_mut().projection = proj; - self.tabs[i].scene.camera_generation += 1; - self.ribbon.set_ortho(ortho); - self.command_line.push_output(if ortho { - "Projection: Orthographic" - } else { - "Projection: Perspective" - }); - Task::none() - } - - Message::RibbonSelectTab(idx) => { - self.ribbon.select(idx); - Task::none() - } - - Message::RibbonToolClick { tool_id, event } => { - self.ribbon.activate_tool(&tool_id); - match event { - ModuleEvent::Command(cmd) => return self.dispatch_command(&cmd), - ModuleEvent::OpenFileDialog => { - self.command_line - .push_info("Open DWG/DXF: not yet implemented."); - } - ModuleEvent::ClearModels => { - let i = self.active_tab; - self.tabs[i].scene.clear(); - self.tabs[i].properties = PropertiesPanel::empty(); - self.command_line.push_output("Scene cleared."); - } - ModuleEvent::SetWireframe(w) => { - let i = self.active_tab; - self.tabs[i].wireframe = w; - self.ribbon.set_wireframe(w); - self.tabs[i].visual_style = if w { - "Wireframe".into() - } else { - "Shaded".into() - }; - self.command_line.push_output(if w { - "Visual style: Wireframe" - } else { - "Visual style: Shaded" - }); - } - ModuleEvent::ToggleLayers => { - return Task::done(Message::ToggleLayers); - } - } - Task::none() - } - - // ── Application menu ────────────────────────────────────────── - Message::ToggleAppMenu => { - self.app_menu.toggle(); - Task::none() - } - - Message::CloseAppMenu => { - self.app_menu.close(); - Task::none() - } - - Message::CloseAppMenuAndRun(cmd) => { - self.app_menu.close(); - self.dispatch_command(&cmd.clone()) - } - - Message::AppMenuSearch(s) => { - self.app_menu.search = s; - Task::none() - } - - // ── Document tabs ───────────────────────────────────────────── - Message::TabNew => { - self.tab_counter += 1; - let new_tab = DocumentTab::new_drawing(self.tab_counter); - self.tabs.push(new_tab); - self.active_tab = self.tabs.len() - 1; - self.sync_ribbon_layers(); - Task::none() - } - - Message::TabSwitch(idx) => { - if idx < self.tabs.len() { - self.active_tab = idx; - self.sync_ribbon_layers(); - } - Task::none() - } - - Message::TabClose(idx) => { - if self.tabs.len() == 1 { - // Only one tab — reset it to a fresh drawing instead of closing - self.tab_counter += 1; - self.tabs[0] = DocumentTab::new_drawing(self.tab_counter); - self.active_tab = 0; - } else { - self.tabs.remove(idx); - if self.active_tab >= self.tabs.len() { - self.active_tab = self.tabs.len() - 1; - } - } - Task::none() - } - // ───────────────────────────────────────────────────────────── - Message::CommandInput(s) => { - self.command_line.input = s; - Task::none() - } - Message::CommandSubmit => { - let i = self.active_tab; - if self.tabs[i].active_cmd.is_some() { - let text = self.command_line.input.trim().to_string(); - self.command_line.input.clear(); - - // 1. Command explicitly owns text input (e.g. TTR radius prompt). - if self.tabs[i] - .active_cmd - .as_ref() - .map(|c| c.wants_text_input()) - .unwrap_or(false) - { - if let Some(result) = self.tabs[i] - .active_cmd - .as_mut() - .and_then(|c| c.on_text_input(&text)) - { - return self.apply_cmd_result(result); - } - // Step accepted but no final result yet — show updated prompt and refresh preview. - let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); - if let Some(p) = prompt { - self.command_line.push_info(&p); - } - let pt = self.tabs[i].last_cursor_world; - let previews = self.tabs[i] - .active_cmd - .as_mut() - .map(|c| c.on_preview_wires(pt)) - .unwrap_or_default(); - self.tabs[i].scene.set_preview_wires(previews); - return self.focus_cmd_input(); - } - - // 2. Empty input → Enter (finish / cancel). - if text.is_empty() { - let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_enter()); - if let Some(r) = result { - return self.apply_cmd_result(r); - } - return Task::none(); - } - - // 3. Coordinate input "x,y" or "x,y,z" → on_point. - if let Some(pt) = parse_coord(&text) { - let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt)); - if let Some(r) = result { - return self.apply_cmd_result(r); - } - return Task::none(); - } - - // 4. Numeric input (e.g. radius, angle, scale factor) → on_text_input. - if let Some(result) = self.tabs[i] - .active_cmd - .as_mut() - .and_then(|c| c.on_text_input(&text)) - { - return self.apply_cmd_result(result); - } - - // 5. Nothing matched — show hint. - self.command_line.push_error(&format!( - "Expected coordinates (x,y) or a number, got: \"{text}\"" - )); - return self.focus_cmd_input(); - } - if let Some(cmd) = self.command_line.submit() { - return self.dispatch_command(&cmd); - } - Task::none() - } - - Message::CommandFinalize => { - let i = self.active_tab; - if self.tabs[i].active_cmd.is_some() { - let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_enter()); - if let Some(r) = result { - return self.apply_cmd_result(r); - } - Task::none() - } else if let Some(cmd) = self.tabs[i].last_cmd.clone() { - self.dispatch_command(&cmd) - } else { - Task::none() - } - } - - Message::CommandEscape => { - let i = self.active_tab; - if self.tabs[i].active_cmd.is_some() { - let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_escape()); - if let Some(r) = result { - return self.apply_cmd_result(r); - } - } else { - // No active command → Escape deselects everything. - self.tabs[i].scene.deselect_all(); - self.refresh_properties(); - // Also cancel any in-progress selection box. - let mut sel = self.tabs[i].scene.selection.borrow_mut(); - sel.box_anchor = None; - sel.box_current = None; - sel.box_crossing = false; - } - Task::none() - } - Message::Command(cmd) => self.dispatch_command(&cmd), - - Message::ToggleLayers => { - if let Some(id) = self.layer_window.take() { - // Window already open — close it. - window::close(id) - } else { - // Sync layer/linetype data before opening so the panel is up to date. - self.sync_ribbon_layers(); - // Open a new OS window for the Layer Properties Manager. - let (id, task) = window::open(window::Settings { - size: iced::Size::new(900.0, 360.0), - resizable: true, - ..Default::default() - }); - self.layer_window = Some(id); - task.map(|_| Message::Noop) - } - } - - Message::MainWindowOpened(id) => { - self.main_window = Some(id); - Task::none() - } - - Message::LayerWindowOpened(_id) => Task::none(), - - Message::OsWindowClosed(id) => { - if self.main_window == Some(id) { - // Main window closed — exit the process entirely. - return iced::exit(); - } - if self.layer_window == Some(id) { - self.layer_window = None; - } - Task::none() - } - - Message::LayerToggleVisible(idx) => { - let i = self.active_tab; - if idx < self.tabs[i].layers.layers.len() { - self.push_undo_snapshot(i, "LAYER OFF/ON"); - let l = &mut self.tabs[i].layers.layers[idx]; - l.visible = !l.visible; - let name = l.name.clone(); - let on = l.visible; - self.tabs[i].scene.toggle_layer_visibility(&name); - self.command_line.push_output(&format!( - "Layer \"{}\" {}", - name, - if on { "on" } else { "off" } - )); - } - Task::none() - } - - Message::LayerToggleLock(idx) => { - let i = self.active_tab; - if idx < self.tabs[i].layers.layers.len() { - self.push_undo_snapshot(i, "LAYER LOCK/UNLOCK"); - let l = &mut self.tabs[i].layers.layers[idx]; - l.locked = !l.locked; - let name = l.name.clone(); - let locked = l.locked; - self.tabs[i].scene.toggle_layer_lock(&name); - self.command_line.push_output(&format!( - "Layer \"{}\" {}", - name, - if locked { "locked" } else { "unlocked" } - )); - } - Task::none() - } - - Message::LayerToggleFreeze(idx) => { - let i = self.active_tab; - if idx < self.tabs[i].layers.layers.len() { - self.push_undo_snapshot(i, "LAYER FREEZE"); - let l = &mut self.tabs[i].layers.layers[idx]; - l.frozen = !l.frozen; - let name = l.name.clone(); - let frozen = l.frozen; - if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { - if frozen { dl.freeze(); } else { dl.thaw(); } - } - self.tabs[i].dirty = true; - } - Task::none() - } - - Message::LayerNew => { - let i = self.active_tab; - // Find unique name - let mut n = 1; - let new_name = loop { - let candidate = format!("Layer{}", n); - if !self.tabs[i].scene.document.layers.contains(&candidate) { - break candidate; - } - n += 1; - }; - self.push_undo_snapshot(i, "LAYER NEW"); - use acadrust::tables::layer::Layer as DocLayer; - let _ = self.tabs[i].scene.document.layers.add(DocLayer::new(&new_name)); - self.tabs[i].dirty = true; - let doc_layers = self.tabs[i].scene.document.layers.clone(); - self.tabs[i].layers.sync_from_doc(&doc_layers); - // Select and start editing the new layer - let new_idx = self.tabs[i].layers.layers.iter() - .position(|l| l.name == new_name); - if let Some(idx) = new_idx { - self.tabs[i].layers.selected = Some(idx); - self.tabs[i].layers.editing = Some(idx); - self.tabs[i].layers.edit_buf = new_name.clone(); - } - self.sync_ribbon_layers(); - Task::none() - } - - Message::LayerDelete => { - let i = self.active_tab; - if let Some(idx) = self.tabs[i].layers.selected { - let name = self.tabs[i].layers.layers.get(idx) - .map(|l| l.name.clone()) - .unwrap_or_default(); - if name == "0" { return Task::none(); } - self.push_undo_snapshot(i, "LAYER DELETE"); - self.tabs[i].scene.document.layers.remove(&name); - self.tabs[i].dirty = true; - let doc_layers = self.tabs[i].scene.document.layers.clone(); - self.tabs[i].layers.sync_from_doc(&doc_layers); - self.tabs[i].layers.selected = None; - self.sync_ribbon_layers(); - } - Task::none() - } - - Message::LayerSetCurrent => { - let i = self.active_tab; - if let Some(idx) = self.tabs[i].layers.selected { - if let Some(layer) = self.tabs[i].layers.layers.get(idx) { - let name = layer.name.clone(); - self.tabs[i].active_layer = name.clone(); - self.tabs[i].layers.current_layer = name.clone(); - self.ribbon.active_layer = name; - } - } - Task::none() - } - - Message::LayerSelect(idx) => { - let i = self.active_tab; - // Commit any pending rename first - if self.tabs[i].layers.editing.is_some() { - return Task::done(Message::LayerRenameCommit); - } - self.tabs[i].layers.selected = Some(idx); - Task::none() - } - - Message::LayerRenameStart(idx) => { - let i = self.active_tab; - self.tabs[i].layers.selected = Some(idx); - if let Some(layer) = self.tabs[i].layers.layers.get(idx) { - self.tabs[i].layers.edit_buf = layer.name.clone(); - } - self.tabs[i].layers.editing = Some(idx); - Task::none() - } - - Message::LayerRenameEdit(s) => { - let i = self.active_tab; - self.tabs[i].layers.edit_buf = s; - Task::none() - } - - Message::LayerRenameCommit => { - let i = self.active_tab; - let editing_idx = self.tabs[i].layers.editing.take(); - if let Some(idx) = editing_idx { - let new_name = self.tabs[i].layers.edit_buf.trim().to_string(); - let old_name = self.tabs[i].layers.layers.get(idx) - .map(|l| l.name.clone()) - .unwrap_or_default(); - if !new_name.is_empty() && new_name != old_name - && !self.tabs[i].scene.document.layers.contains(&new_name) - { - self.push_undo_snapshot(i, "LAYER RENAME"); - // acadrust Table doesn't have rename; add new + remove old - if let Some(old_layer) = self.tabs[i].scene.document.layers.get(&old_name) { - use acadrust::tables::layer::Layer as DocLayer; - let mut nl = DocLayer::new(&new_name); - nl.color = old_layer.color.clone(); - nl.flags = old_layer.flags.clone(); - let _ = self.tabs[i].scene.document.layers.add(nl); - } - self.tabs[i].scene.document.layers.remove(&old_name); - // Update entities on that layer - for e in self.tabs[i].scene.document.entities_mut() { - if e.as_entity().layer() == old_name { - e.as_entity_mut().set_layer(new_name.clone()); - } - } - self.tabs[i].dirty = true; - } - let doc_layers = self.tabs[i].scene.document.layers.clone(); - self.tabs[i].layers.sync_from_doc(&doc_layers); - self.tabs[i].layers.edit_buf.clear(); - self.sync_ribbon_layers(); - } - Task::none() - } - - Message::LayerColorPickerToggle(idx) => { - let i = self.active_tab; - let panel = &mut self.tabs[i].layers; - if panel.color_picker_row == Some(idx) { - panel.color_picker_row = None; - panel.color_full_palette = false; - } else { - panel.color_picker_row = Some(idx); - panel.color_full_palette = false; - panel.selected = Some(idx); - } - Task::none() - } - - Message::LayerColorMorePalette => { - let i = self.active_tab; - self.tabs[i].layers.color_full_palette = - !self.tabs[i].layers.color_full_palette; - Task::none() - } - - Message::LayerColorSet(aci) => { - let i = self.active_tab; - if let Some(idx) = self.tabs[i].layers.selected { - if let Some(layer) = self.tabs[i].layers.layers.get(idx) { - let name = layer.name.clone(); - if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { - dl.color = AcadColor::Index(aci); - } - use crate::ui::layers::iced_color_from_acad; - let new_color = iced_color_from_acad(&AcadColor::Index(aci)); - if let Some(pl) = self.tabs[i].layers.layers.get_mut(idx) { - pl.color = new_color; - } - self.tabs[i].dirty = true; - } - self.tabs[i].layers.color_picker_row = None; - self.tabs[i].layers.color_full_palette = false; - self.sync_ribbon_layers(); - } - Task::none() - } - - Message::LayerLinetypeSet(lt) => { - let i = self.active_tab; - if let Some(idx) = self.tabs[i].layers.selected { - if let Some(layer) = self.tabs[i].layers.layers.get(idx) { - let name = layer.name.clone(); - if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { - dl.line_type = lt.clone(); - } - if let Some(pl) = self.tabs[i].layers.layers.get_mut(idx) { - pl.linetype = lt; - } - self.tabs[i].dirty = true; - } - } - Task::none() - } - - Message::LayerLineweightSet(lw) => { - let i = self.active_tab; - if let Some(idx) = self.tabs[i].layers.selected { - if let Some(layer) = self.tabs[i].layers.layers.get(idx) { - let name = layer.name.clone(); - if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { - dl.line_weight = lw; - } - if let Some(pl) = self.tabs[i].layers.layers.get_mut(idx) { - pl.lineweight = lw; - } - self.tabs[i].dirty = true; - } - } - Task::none() - } - - Message::LayerTransparencyEdit(idx, s) => { - let i = self.active_tab; - if let Some(pl) = self.tabs[i].layers.layers.get_mut(idx) { - if let Ok(v) = s.parse::() { - pl.transparency = v.clamp(0, 90); - } else if s.is_empty() { - pl.transparency = 0; - } - } - Task::none() - } - - Message::CursorMoved(p) => { - let (vw, _vh) = self.tabs[self.active_tab].scene.selection.borrow().vp_size; - self.cursor_pos = Point::new( - vw - VIEWCUBE_PAD - VIEWCUBE_HIT_SIZE + p.x, - VIEWCUBE_PAD + p.y, - ); - Task::none() - } - - Message::ViewportMove(p) => { - let i = self.active_tab; - let mut sel = self.tabs[i].scene.selection.borrow_mut(); - sel.last_move_pos = Some(p); - - if sel.left_down { - let press = sel.left_press_pos.unwrap_or(p); - let dx = p.x - press.x; - let dy = p.y - press.y; - let dist2 = dx * dx + dy * dy; - let elapsed_ms = sel - .left_press_time - .map(|t| Instant::now().duration_since(t).as_millis()) - .unwrap_or(u128::MAX); - if !sel.left_dragging && elapsed_ms >= POLY_START_DELAY_MS && dist2 > 9.0 { - sel.left_dragging = true; - sel.poly_active = true; - sel.poly_crossing = p.x < press.x; - sel.poly_points.clear(); - sel.poly_points.push(press); - sel.poly_points.push(p); - } else if sel.left_dragging && sel.poly_active { - if sel.poly_points.last().map_or(true, |lp| { - let ddx = p.x - lp.x; - let ddy = p.y - lp.y; - ddx * ddx + ddy * ddy > 16.0 - }) { - sel.poly_points.push(p); - } - } - } else if sel.box_anchor.is_some() { - sel.box_current = Some(p); - if let Some(a) = sel.box_anchor { - sel.box_crossing = p.x < a.x; - } - } - - if sel.right_down { - if let Some(press) = sel.right_press_pos { - let dx = p.x - press.x; - let dy = p.y - press.y; - if !sel.right_dragging && (dx * dx + dy * dy) > 9.0 { - sel.right_dragging = true; - sel.context_menu = None; - } - } - if sel.right_dragging { - if let Some(last) = sel.right_last_pos { - let (dx, dy) = (p.x - last.x, p.y - last.y); - self.tabs[i].scene.camera.borrow_mut().orbit(dx, dy); - } - sel.right_last_pos = Some(p); - } - } - - if sel.middle_down { - if let Some(last) = sel.middle_last_pos { - let (dx, dy) = (p.x - last.x, p.y - last.y); - self.tabs[i].scene.camera.borrow_mut().pan(dx, dy); - } - sel.middle_last_pos = Some(p); - } - - let vp_size = sel.vp_size; - drop(sel); - - // ── Grip drag ───────────────────────────────────────────── - if let Some(grip) = self.tabs[i].active_grip.clone() { - let (vw, vh) = vp_size; - let bounds = iced::Rectangle { - x: 0.0, - y: 0.0, - width: vw, - height: vh, - }; - let cam = self.tabs[i].scene.camera.borrow(); - let raw = cam.pick_on_target_plane(p, bounds); - let vp_mat = cam.view_proj(bounds); - drop(cam); - - // Exclude the entity being dragged so its own segments - // don't interfere with snap (avoids Nearest/Endpoint sticking). - let edited_name = grip.handle.value().to_string(); - let all_wires = self.tabs[i].scene.entity_wires(); - let snap_wires: Vec<_> = all_wires - .iter() - .filter(|w| w.name != edited_name) - .cloned() - .collect(); - let snap_hit = self.snapper.snap(raw, p, &snap_wires, vp_mat, bounds); - let mut snapped = snap_hit.map(|s| s.world).unwrap_or(raw); - self.tabs[i].snap_result = snap_hit; - - // Apply ortho/polar constraints relative to the grip's origin position. - // Only when snap did not find a geometry hit (snap takes priority). - if snap_hit.is_none() { - let base = grip.origin_world; - if self.ortho_mode { - snapped = ortho_constrain(snapped, base); - } else if self.polar_mode { - snapped = polar_constrain(snapped, base, 45.0); - } - } - - let apply = if grip.is_translate { - GripApply::Translate(snapped - grip.last_world) - } else { - GripApply::Absolute(snapped) - }; - self.tabs[i] - .scene - .apply_grip(grip.handle, grip.grip_id, apply); - self.tabs[i].dirty = true; - self.tabs[i].active_grip.as_mut().unwrap().last_world = snapped; - self.refresh_selected_grips(); - self.refresh_properties(); - return Task::none(); - } - - if self.tabs[i].active_cmd.is_some() { - let (vw, vh) = vp_size; - let bounds = iced::Rectangle { - x: 0.0, - y: 0.0, - width: vw, - height: vh, - }; - let cam = self.tabs[i].scene.camera.borrow(); - let cursor_world = cam.pick_on_target_plane(p, bounds); - let view_proj = cam.view_proj(bounds); - drop(cam); - - let all_wires = self.tabs[i].scene.entity_wires(); - let needs_tan = self.tabs[i] - .active_cmd - .as_ref() - .map(|c| c.needs_tangent_pick()) - .unwrap_or(false); - self.tabs[i].snap_result = if needs_tan { - self.snapper.snap_tangent_only( - cursor_world, - p, - &all_wires, - view_proj, - bounds, - ) - } else { - self.snapper - .snap(cursor_world, p, &all_wires, view_proj, bounds) - }; - let effective = { - let mut pt = self.tabs[i] - .snap_result - .map(|s| s.world) - .unwrap_or(cursor_world); - if self.tabs[i].active_cmd.is_some() { - pt.z = 0.0; - } - if let Some(base) = self.last_point { - if self.ortho_mode { - pt = ortho_constrain(pt, base); - } else if self.polar_mode { - pt = polar_constrain(pt, base, 45.0); - } - } - pt - }; - self.tabs[i].last_cursor_world = effective; - - let needs_entity = self.tabs[i] - .active_cmd - .as_ref() - .map(|c| c.needs_entity_pick()) - .unwrap_or(false); - let previews = if needs_entity { - let hover_handle = - scene::hit_test::click_hit(p, &all_wires, view_proj, bounds) - .and_then(|s| Scene::handle_from_wire_name(s)) - .unwrap_or(acadrust::Handle::NULL); - self.tabs[i] - .active_cmd - .as_mut() - .map(|c| c.on_hover_entity(hover_handle, effective)) - .unwrap_or_default() - } else { - self.tabs[i] - .active_cmd - .as_mut() - .map(|c| c.on_preview_wires(effective)) - .unwrap_or_default() - }; - self.tabs[i].scene.set_preview_wires(previews); - } else { - self.tabs[i].snap_result = None; - } - - Task::none() - } - - Message::ViewportExit => { - let i = self.active_tab; - let mut sel = self.tabs[i].scene.selection.borrow_mut(); - sel.left_down = false; - sel.left_press_pos = None; - sel.left_press_time = None; - sel.left_dragging = false; - sel.right_down = false; - sel.right_press_pos = None; - sel.right_last_pos = None; - sel.right_dragging = false; - sel.middle_down = false; - sel.middle_last_pos = None; - sel.box_anchor = None; - sel.box_current = None; - sel.box_crossing = false; - sel.poly_active = false; - sel.poly_points.clear(); - sel.poly_crossing = false; - sel.context_menu = None; - Task::none() - } - - Message::ViewportLeftPress => { - let i = self.active_tab; - let (p, vp_size) = { - let sel = self.tabs[i].scene.selection.borrow(); - let p = match sel.last_move_pos { - Some(p) => p, - None => return Task::none(), - }; - (p, sel.vp_size) - }; - let (vw, vh) = vp_size; - let bounds = iced::Rectangle { - x: 0.0, - y: 0.0, - width: vw, - height: vh, - }; - - // Viewcube check. - if vw > 1.0 && vh > 1.0 { - let cam = self.tabs[i].scene.camera.borrow(); - if scene::hit_test(p.x, p.y, vw, vh, cam.view_rotation_mat(), VIEWCUBE_PX) - .is_some() - { - return Task::none(); - } - } - - // Grip hit-test: only when no command is active and one entity selected. - if self.tabs[i].active_cmd.is_none() && !self.tabs[i].selected_grips.is_empty() { - if let Some(handle) = self.tabs[i].selected_handle { - let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); - let grip_hit = - find_hit_grip(p, &self.tabs[i].selected_grips, vp_mat, bounds); - if let Some((grip_id, is_translate, world)) = grip_hit { - self.tabs[i].active_grip = Some(GripEdit { - handle, - grip_id, - is_translate, - origin_world: world, - last_world: world, - }); - return Task::none(); // swallow press — selection box must not start - } - } - } - - let mut sel = self.tabs[i].scene.selection.borrow_mut(); - sel.context_menu = None; - sel.left_down = true; - sel.left_press_pos = Some(p); - sel.left_press_time = Some(Instant::now()); - sel.left_dragging = false; - Task::none() - } - - Message::ViewportLeftRelease => { - let i = self.active_tab; - // Extract the state we need before any mutable borrows. - let (p, is_click, is_down) = { - let sel = self.tabs[i].scene.selection.borrow(); - let p = match sel.last_move_pos { - Some(p) => p, - None => return Task::none(), - }; - (p, !sel.left_dragging, sel.left_down) - }; - - // ── Grip drag end ───────────────────────────────────────── - if self.tabs[i].active_grip.is_some() { - self.tabs[i].active_grip = None; - self.refresh_properties(); - return Task::none(); - } - - // Whether the active command is in selection-gathering mode. - let is_gathering = self.tabs[i] - .active_cmd - .as_ref() - .map(|c| c.is_selection_gathering()) - .unwrap_or(false); - - // ── Active command: plain click = point pick ────────────── - // Skip this path when gathering — let clicks flow into the - // normal selection system below. - if is_down && is_click && self.tabs[i].active_cmd.is_some() && !is_gathering { - let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size; - let bounds = iced::Rectangle { - x: 0.0, - y: 0.0, - width: vw, - height: vh, - }; - - // Take snap result once; preserve tangent_obj for pick routing. - let snap_taken = self.tabs[i].snap_result.take(); - let tangent_obj_at_click = snap_taken.and_then(|s| s.tangent_obj); - - let world_pt = { - let raw = self.tabs[i] - .scene - .camera - .borrow() - .pick_on_target_plane(p, bounds); - let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); - let all_wires = self.tabs[i].scene.entity_wires(); - let needs_tan = self.tabs[i] - .active_cmd - .as_ref() - .map(|c| c.needs_tangent_pick()) - .unwrap_or(false); - let snap_hit = if needs_tan { - self.snapper - .snap_tangent_only(raw, p, &all_wires, vp_mat, bounds) - } else { - self.snapper.snap(raw, p, &all_wires, vp_mat, bounds) - }; - let mut pt = snap_hit.map(|s| s.world).unwrap_or(raw); - pt.z = 0.0; - if let Some(base) = self.last_point { - if self.ortho_mode { - pt = ortho_constrain(pt, base); - } else if self.polar_mode { - pt = polar_constrain(pt, base, 45.0); - } - } - pt - }; - - let result = if self.tabs[i] - .active_cmd - .as_ref() - .map(|c| c.needs_entity_pick()) - .unwrap_or(false) - { - let vp_mat2 = self.tabs[i].scene.camera.borrow().view_proj(bounds); - let all_wires2 = self.tabs[i].scene.entity_wires(); - let hit = scene::hit_test::click_hit(p, &all_wires2, vp_mat2, bounds) - .and_then(|s| Scene::handle_from_wire_name(s)); - if let Some(handle) = hit { - self.tabs[i] - .active_cmd - .as_mut() - .map(|c| c.on_entity_pick(handle, world_pt)) - } else { - self.command_line.push_info("Nothing found at that point."); - None - } - } else if self.tabs[i] - .active_cmd - .as_ref() - .map(|c| c.needs_tangent_pick()) - .unwrap_or(false) - { - let tangent_obj = tangent_obj_at_click; - if let Some(obj) = tangent_obj { - self.tabs[i] - .active_cmd - .as_mut() - .map(|c| c.on_tangent_point(obj, world_pt)) - } else { - self.command_line.push_info("Select a tangent object."); - None - } - } else { - self.last_point = Some(world_pt); - self.tabs[i] - .active_cmd - .as_mut() - .map(|c| c.on_point(world_pt)) - }; - if let Some(r) = result { - let task = self.apply_cmd_result(r); - let mut sel = self.tabs[i].scene.selection.borrow_mut(); - sel.left_down = false; - sel.left_press_pos = None; - sel.left_press_time = None; - sel.left_dragging = false; - return task; - } - let mut sel = self.tabs[i].scene.selection.borrow_mut(); - sel.left_down = false; - sel.left_press_pos = None; - sel.left_press_time = None; - sel.left_dragging = false; - return Task::none(); - } - - let (is_down2, is_dragging, box_anchor, box_crossing, vp_size, elapsed_ms) = { - let sel = self.tabs[i].scene.selection.borrow(); - let elapsed = sel - .left_press_time - .map(|t| Instant::now().duration_since(t).as_millis()) - .unwrap_or(u128::MAX); - ( - sel.left_down, - sel.left_dragging, - sel.box_anchor, - sel.box_crossing, - sel.vp_size, - elapsed, - ) - }; - - // True when a selection action fully completed this release event, - // meaning the gathering command should fire immediately. - let mut selection_just_completed = false; - - if is_down2 { - let bounds = iced::Rectangle { - x: 0.0, - y: 0.0, - width: vp_size.0, - height: vp_size.1, - }; - - if is_dragging { - // ── Drag ended ──────────────────────────────────── - if elapsed_ms < POLY_START_DELAY_MS { - if let Some(a) = box_anchor { - let crossing = box_crossing; - let all_wires = self.tabs[i].scene.entity_wires(); - let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); - let mut handles: Vec = scene::hit_test::box_hit( - a, p, crossing, &all_wires, vp_mat, bounds, - ) - .into_iter() - .filter_map(|s| Scene::handle_from_wire_name(s)) - .collect(); - handles.extend(scene::hit_test::box_hit_hatch( - a, - p, - crossing, - &self.tabs[i].scene.hatches, - vp_mat, - bounds, - )); - self.tabs[i].scene.deselect_all(); - for h in &handles { - self.tabs[i].scene.select_entity(*h, false); - } - self.tabs[i].scene.expand_selection_for_groups(&handles); - self.refresh_properties(); - selection_just_completed = true; - } - } else { - // Long drag → lasso polygon selection. - let (poly_pts, crossing) = { - let sel = self.tabs[i].scene.selection.borrow(); - (sel.poly_points.clone(), sel.poly_crossing) - }; - self.tabs[i].scene.selection.borrow_mut().poly_last_crossing = crossing; - - let all_wires = self.tabs[i].scene.entity_wires(); - let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); - let mut handles: Vec = scene::hit_test::poly_hit( - &poly_pts, crossing, &all_wires, vp_mat, bounds, - ) - .into_iter() - .filter_map(|s| Scene::handle_from_wire_name(s)) - .collect(); - handles.extend(scene::hit_test::poly_hit_hatch( - &poly_pts, - crossing, - &self.tabs[i].scene.hatches, - vp_mat, - bounds, - )); - self.tabs[i].scene.deselect_all(); - for h in &handles { - self.tabs[i].scene.select_entity(*h, false); - } - self.tabs[i].scene.expand_selection_for_groups(&handles); - self.refresh_properties(); - selection_just_completed = true; - } - let mut sel = self.tabs[i].scene.selection.borrow_mut(); - sel.poly_active = false; - sel.poly_points.clear(); - sel.poly_crossing = false; - sel.box_anchor = None; - sel.box_current = None; - } else { - // ── Non-drag click ──────────────────────────────── - if box_anchor.is_none() { - let all_wires = self.tabs[i].scene.entity_wires(); - let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); - let hit = scene::hit_test::click_hit(p, &all_wires, vp_mat, bounds) - .and_then(|s| Scene::handle_from_wire_name(s)) - .or_else(|| { - scene::hit_test::click_hit_hatch( - p, - &self.tabs[i].scene.hatches, - vp_mat, - bounds, - ) - }); - if let Some(handle) = hit { - self.tabs[i].scene.select_entity(handle, true); - self.tabs[i].scene.expand_selection_for_groups(&[handle]); - self.refresh_properties(); - // Single-click hit completes the gather; miss starts a box. - selection_just_completed = true; - } else { - self.tabs[i].scene.deselect_all(); - self.refresh_properties(); - let mut sel = self.tabs[i].scene.selection.borrow_mut(); - sel.box_anchor = Some(p); - sel.box_current = Some(p); - sel.box_crossing = false; - } - } else { - // Second click → complete the selection box. - let a = box_anchor.unwrap(); - let crossing = box_crossing; - let all_wires = self.tabs[i].scene.entity_wires(); - let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); - let mut handles: Vec = scene::hit_test::box_hit( - a, p, crossing, &all_wires, vp_mat, bounds, - ) - .into_iter() - .filter_map(|s| Scene::handle_from_wire_name(s)) - .collect(); - handles.extend(scene::hit_test::box_hit_hatch( - a, - p, - crossing, - &self.tabs[i].scene.hatches, - vp_mat, - bounds, - )); - self.tabs[i].scene.deselect_all(); - for h in &handles { - self.tabs[i].scene.select_entity(*h, false); - } - self.tabs[i].scene.expand_selection_for_groups(&handles); - self.refresh_properties(); - let mut sel = self.tabs[i].scene.selection.borrow_mut(); - sel.box_last = Some((a, p)); - sel.box_last_crossing = crossing; - sel.box_anchor = None; - sel.box_current = None; - sel.box_crossing = false; - selection_just_completed = true; - } - } - - let mut sel = self.tabs[i].scene.selection.borrow_mut(); - sel.left_down = false; - sel.left_press_pos = None; - sel.left_press_time = None; - sel.left_dragging = false; - } - - // ── Fire gathering command after selection completes ─────── - if is_gathering && selection_just_completed { - let handles: Vec = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if let Some(cmd) = self.tabs[i].active_cmd.as_mut() { - let result = cmd.on_selection_complete(handles); - return self.apply_cmd_result(result); - } - } - - Task::none() - } - - Message::ViewportRightPress => { - let i = self.active_tab; - let mut sel = self.tabs[i].scene.selection.borrow_mut(); - let Some(p) = sel.last_move_pos else { - return Task::none(); - }; - sel.context_menu = None; - sel.right_down = true; - sel.right_press_pos = Some(p); - sel.right_last_pos = Some(p); - sel.right_dragging = false; - Task::none() - } - - Message::ViewportRightRelease => { - let i = self.active_tab; - let mut sel = self.tabs[i].scene.selection.borrow_mut(); - let Some(_p) = sel.last_move_pos else { - return Task::none(); - }; - if sel.right_down { - if !sel.right_dragging { - sel.context_menu = sel.last_move_pos; - } - sel.right_down = false; - sel.right_press_pos = None; - sel.right_last_pos = None; - sel.right_dragging = false; - } - Task::none() - } - - Message::ViewportMiddlePress => { - let i = self.active_tab; - let now = iced::time::Instant::now(); - let is_double = { - let sel = self.tabs[i].scene.selection.borrow(); - sel.middle_last_press_time - .map(|t| now.duration_since(t).as_millis() < 300) - .unwrap_or(false) - }; - { - let mut sel = self.tabs[i].scene.selection.borrow_mut(); - let Some(p) = sel.last_move_pos else { - return Task::none(); - }; - sel.middle_down = true; - sel.middle_last_pos = Some(p); - sel.middle_last_press_time = Some(now); - } - if is_double { - self.tabs[i].scene.fit_all(); - self.command_line.push_output("Zoom Extents"); - } - Task::none() - } - - Message::ViewportMiddleRelease => { - let i = self.active_tab; - let mut sel = self.tabs[i].scene.selection.borrow_mut(); - sel.middle_down = false; - sel.middle_last_pos = None; - Task::none() - } - - Message::ViewportScroll(delta) => { - let s = match delta { - mouse::ScrollDelta::Lines { y, .. } => y, - mouse::ScrollDelta::Pixels { y, .. } => y * 0.01, - }; - let i = self.active_tab; - let cursor = self.tabs[i].scene.selection.borrow().last_move_pos; - let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size; - let bounds = iced::Rectangle { - x: 0.0, - y: 0.0, - width: vw, - height: vh, - }; - - let mut cam = self.tabs[i].scene.camera.borrow_mut(); - if let Some(cursor) = cursor { - cam.zoom_about_point(cursor, bounds, s); - } else { - cam.zoom(s); - } - Task::none() - } - - Message::ViewportClick => { - let i = self.active_tab; - let cam = self.tabs[i].scene.camera.borrow(); - let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size; - if let Some(region) = scene::hit_test( - self.cursor_pos.x, - self.cursor_pos.y, - vw, - vh, - cam.view_rotation_mat(), - VIEWCUBE_PX, - ) { - return Task::done(Message::ViewCubeSnap(region)); - } - Task::none() - } - - Message::WindowResized(w, h) => { - self.vp_size = ((w - 440.0).max(200.0), h); - Task::none() - } - - Message::ViewCubeSnap(region) => { - let i = self.active_tab; - let mut region = region; - { - let mut cam = self.tabs[i].scene.camera.borrow_mut(); - let (target_yaw, target_pitch) = region.snap_angles(); - if angle_close(cam.yaw, target_yaw, 0.01) - && angle_close(cam.pitch, target_pitch, 0.01) - { - region = region.opposite(); - } - let (yaw, pitch) = region.snap_angles(); - cam.snap_to_angles(yaw, pitch); - } - self.tabs[i].scene.camera_generation += 1; - self.command_line - .push_output(&format!("View: {}", region.label())); - Task::none() - } - - Message::ToggleSnapEnabled => { - self.snapper.toggle_global(); - Task::none() - } - Message::ToggleGridSnap => { - self.snapper.toggle(snap::SnapType::Grid); - Task::none() - } - Message::ToggleGrid => { - self.show_grid ^= true; - Task::none() - } - Message::ToggleOrtho => { - self.ortho_mode ^= true; - if self.ortho_mode { - self.polar_mode = false; - } - Task::none() - } - Message::TogglePolar => { - self.polar_mode ^= true; - if self.polar_mode { - self.ortho_mode = false; - } - Task::none() - } - Message::ToggleSnap(t) => { - self.snapper.toggle(t); - Task::none() - } - Message::ToggleSnapPopup => { - self.snap_popup_open ^= true; - Task::none() - } - Message::CloseSnapPopup => { - self.snap_popup_open = false; - Task::none() - } - Message::SnapSelectAll => { - self.snapper.enable_all(); - Task::none() - } - Message::SnapClearAll => { - self.snapper.disable_all(); - Task::none() - } - - Message::ToggleRibbonDropdown(id) => { - self.ribbon.toggle_dropdown(&id); - Task::none() - } - Message::CloseRibbonDropdown => { - self.ribbon.close_dropdown(); - Task::none() - } - Message::DropdownSelectItem { dropdown_id, cmd } => { - self.ribbon.select_dropdown_item(dropdown_id, cmd); - self.ribbon.activate_tool(cmd); - self.dispatch_command(cmd) - } - - Message::DeleteSelected => { - let i = self.active_tab; - let handles: Vec<_> = self.tabs[i].scene.selected.iter().cloned().collect(); - if !handles.is_empty() { - self.push_undo_snapshot(i, "ERASE"); - self.tabs[i].scene.erase_entities(&handles); - self.tabs[i].dirty = true; - self.refresh_properties(); - } - Task::none() - } - - // ── Properties panel messages ───────────────────────────────── - Message::PropSelectionGroupChanged(group) => { - self.tabs[self.active_tab].properties.selected_group = Some(group); - self.refresh_properties(); - Task::none() - } - - Message::RibbonLayerChanged(layer) => { - let i = self.active_tab; - self.tabs[i].active_layer = layer.clone(); - self.tabs[i].layers.current_layer = layer.clone(); - self.ribbon.active_layer = layer; - self.ribbon.close_dropdown(); - Task::none() - } - - Message::RibbonColorChanged(color) => { - self.ribbon.active_color = color; - self.ribbon.prop_color_palette_open = false; - self.ribbon.close_dropdown(); - Task::none() - } - Message::RibbonColorPaletteToggle => { - self.ribbon.prop_color_palette_open ^= true; - Task::none() - } - Message::RibbonLinetypeChanged(lt) => { - self.ribbon.active_linetype = lt; - self.ribbon.close_dropdown(); - Task::none() - } - Message::RibbonLineweightChanged(lw) => { - self.ribbon.active_lineweight = lw; - self.ribbon.close_dropdown(); - Task::none() - } - - Message::PropLayerChanged(layer) => { - let i = self.active_tab; - let handles = self.property_target_handles(i); - if !handles.is_empty() { - self.push_undo_snapshot(i, "CHPROP"); - for handle in handles { - if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { - dispatch::apply_common_prop(entity, "layer", &layer); - } - } - self.tabs[i].dirty = true; - self.refresh_properties(); - } - Task::none() - } - - Message::PropColorChanged(color) => { - let i = self.active_tab; - let handles = self.property_target_handles(i); - if !handles.is_empty() { - self.push_undo_snapshot(i, "CHPROP"); - for handle in handles { - if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { - dispatch::apply_color(entity, color); - } - } - self.tabs[i].properties.color_picker_open = false; - self.tabs[i].properties.color_palette_open = false; - self.tabs[i].dirty = true; - self.refresh_properties(); - } - Task::none() - } - - Message::PropLwChanged(lw) => { - let i = self.active_tab; - let handles = self.property_target_handles(i); - if !handles.is_empty() { - self.push_undo_snapshot(i, "CHPROP"); - for handle in handles { - if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { - dispatch::apply_line_weight(entity, lw); - } - } - self.tabs[i].dirty = true; - self.refresh_properties(); - } - Task::none() - } - - Message::PropLinetypeChanged(lt) => { - let i = self.active_tab; - let handles = self.property_target_handles(i); - if !handles.is_empty() { - self.push_undo_snapshot(i, "CHPROP"); - for handle in handles { - if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { - dispatch::apply_common_prop(entity, "linetype", <); - } - } - self.tabs[i].dirty = true; - self.refresh_properties(); - } - Task::none() - } - - Message::PropHatchPatternChanged(name) => { - let i = self.active_tab; - let handles = self.property_target_handles(i); - if !handles.is_empty() { - use crate::scene::hatch_patterns; - if let Some(entry) = hatch_patterns::find(&name) { - self.push_undo_snapshot(i, "HATCHEDIT"); - for handle in handles { - if let Some(acadrust::EntityType::Hatch(dxf)) = - self.tabs[i].scene.document.get_entity_mut(handle) - { - dxf.pattern = hatch_patterns::build_dxf_pattern(entry); - dxf.is_solid = matches!( - entry.gpu, - crate::scene::hatch_model::HatchPattern::Solid - ); - } - if let Some(model) = self.tabs[i].scene.hatches.get_mut(&handle) { - model.pattern = entry.gpu.clone(); - model.name = name.clone(); - } - } - self.tabs[i].dirty = true; - self.refresh_properties(); - } - } - Task::none() - } - - Message::PropBoolToggle(field) => { - let i = self.active_tab; - let handles = self.property_target_handles(i); - if !handles.is_empty() { - self.push_undo_snapshot(i, "CHPROP"); - for handle in handles { - if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { - match field { - "invisible" => dispatch::toggle_invisible(entity), - _ => dispatch::apply_geom_prop(entity, field, "toggle"), - } - } - } - self.tabs[i].dirty = true; - self.refresh_properties(); - } - Task::none() - } - - Message::PropGeomChoiceChanged { field, value } => { - let i = self.active_tab; - let handles = self.property_target_handles(i); - if !handles.is_empty() { - self.push_undo_snapshot(i, "CHPROP"); - for handle in handles { - if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { - dispatch::apply_geom_prop(entity, field, &value); - } - } - self.tabs[i].dirty = true; - self.refresh_properties(); - } - Task::none() - } - - Message::PropGeomInput { field, value } => { - self.tabs[self.active_tab] - .properties - .edit_buf - .insert(field.to_string(), value); - Task::none() - } - - Message::PropGeomCommit(field) => { - let i = self.active_tab; - let handles = self.property_target_handles(i); - if !handles.is_empty() { - if let Some(val) = self.tabs[i].properties.edit_buf.remove(field) { - self.push_undo_snapshot(i, "CHPROP"); - for handle in handles { - if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) - { - match field { - "linetype_scale" | "transparency" => { - dispatch::apply_common_prop(entity, field, &val); - } - _ => { - dispatch::apply_geom_prop(entity, field, &val); - } - } - } - } - self.tabs[i].dirty = true; - self.refresh_properties(); - } - } - Task::none() - } - - Message::PropColorPickerToggle => { - let i = self.active_tab; - self.tabs[i].properties.color_picker_open = - !self.tabs[i].properties.color_picker_open; - if self.tabs[i].properties.color_picker_open { - self.tabs[i].properties.color_palette_open = false; - } - Task::none() - } - - Message::PropColorPaletteToggle => { - self.tabs[self.active_tab].properties.color_palette_open = - !self.tabs[self.active_tab].properties.color_palette_open; - Task::none() - } - - Message::LayoutSwitch(name) => { - let i = self.active_tab; - let going_to_paper = name != "Model"; - self.tabs[i].scene.current_layout = name; - self.tabs[i].scene.deselect_all(); - self.tabs[i].scene.fit_all(); - // Switch to Layout tab when entering paper space; back to Home when leaving. - if going_to_paper { - if let Some(idx) = self.ribbon.layout_module_index() { - self.ribbon.select(idx); - } - } else { - if self.ribbon.active_is_layout() { - self.ribbon.select(0); - } - } - Task::none() - } - - Message::LayoutCreate => { - let i = self.active_tab; - let count = self.tabs[i].scene.layout_names().len(); // "Model" + existing paper - let new_name = format!("Layout{}", count); - self.push_undo_snapshot(i, "LAYOUT"); - match self.tabs[i].scene.document.add_layout(&new_name) { - Ok(_) => { - self.tabs[i].scene.current_layout = new_name.clone(); - self.tabs[i].scene.deselect_all(); - self.tabs[i].scene.fit_all(); - if let Some(idx) = self.ribbon.layout_module_index() { - self.ribbon.select(idx); - } - self.command_line.push_output(&format!( - "Layout \"{new_name}\" oluşturuldu — MVIEW ile viewport ekleyin" - )); - self.tabs[i].dirty = true; - } - Err(e) => self - .command_line - .push_error(&format!("Layout oluşturulamadı: {e}")), - } - Task::none() - } - - Message::Undo => { - self.undo_active_tab(); - Task::none() - } - - Message::Redo => { - self.redo_active_tab(); - Task::none() - } - - Message::UndoMany(steps) => { - self.ribbon.close_dropdown(); - self.undo_steps(steps); - Task::none() - } - - Message::RedoMany(steps) => { - self.ribbon.close_dropdown(); - self.redo_steps(steps); - Task::none() - } - - Message::Noop => Task::none(), - } - } - - fn dispatch_command(&mut self, cmd: &str) -> Task { - let i = self.active_tab; - // Cancel any running command before starting a new one. - if self.tabs[i].active_cmd.is_some() { - self.tabs[i].scene.clear_preview_wire(); - self.tabs[i].active_cmd = None; - } - - if let Some(path_str) = cmd.strip_prefix("OPEN_RECENT:") { - let path = PathBuf::from(path_str); - return Task::perform(io::open_path(path), Message::FileOpened); - } - - match cmd { - "NEW" => return Task::done(Message::ClearScene), - "OPEN" => return Task::done(Message::OpenFile), - "SAVE"|"QSAVE" => return Task::done(Message::SaveFile), - "SAVEAS" => return Task::done(Message::SaveAs), - "UNDO"|"U" => return Task::done(Message::Undo), - "REDO" => return Task::done(Message::Redo), - "CLEAR"|"CLR" => return Task::done(Message::ClearScene), - "WIREFRAME"|"VW" => return Task::done(Message::SetWireframe(true)), - "SOLID"|"VS" => return Task::done(Message::SetWireframe(false)), - "ORTHO" => return Task::done(Message::SetProjection(true)), - "PERSP" => return Task::done(Message::SetProjection(false)), - "LAYERS"|"LA" => return Task::done(Message::ToggleLayers), - - // ── Layer object commands ────────────────────────────────────── - // Commands that operate on the layer of selected (or picked) objects. - - "LAYOFF" => { - let handles: Vec<_> = self.tabs[i].scene.selected_entities() - .into_iter().map(|(h, _)| h).collect(); - if handles.is_empty() { - use crate::command::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("LAYOFF"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - let layers: std::collections::HashSet = self.tabs[i].scene - .selected_entities().into_iter() - .map(|(_, e)| e.common().layer.clone()).collect(); - self.push_undo_snapshot(i, "LAYOFF"); - for name in &layers { - if name == "0" { continue; } - if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) { - dl.turn_off(); - } - } - self.tabs[i].dirty = true; - self.sync_ribbon_layers(); - self.command_line.push_info("Layer(s) turned off."); - } - } - - "LAYFRZ" => { - let handles: Vec<_> = self.tabs[i].scene.selected_entities() - .into_iter().map(|(h, _)| h).collect(); - if handles.is_empty() { - use crate::command::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("LAYFRZ"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - let layers: std::collections::HashSet = self.tabs[i].scene - .selected_entities().into_iter() - .map(|(_, e)| e.common().layer.clone()).collect(); - self.push_undo_snapshot(i, "LAYFRZ"); - for name in &layers { - if name == "0" { continue; } - if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) { - dl.freeze(); - } - } - self.tabs[i].dirty = true; - self.sync_ribbon_layers(); - self.command_line.push_info("Layer(s) frozen."); - } - } - - "LAYLCK" => { - let handles: Vec<_> = self.tabs[i].scene.selected_entities() - .into_iter().map(|(h, _)| h).collect(); - if handles.is_empty() { - use crate::command::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("LAYLCK"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - let layers: std::collections::HashSet = self.tabs[i].scene - .selected_entities().into_iter() - .map(|(_, e)| e.common().layer.clone()).collect(); - self.push_undo_snapshot(i, "LAYLCK"); - for name in &layers { - if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) { - dl.lock(); - } - } - self.tabs[i].dirty = true; - self.sync_ribbon_layers(); - self.command_line.push_info("Layer(s) locked."); - } - } - - "LAYMCUR" => { - let entities = self.tabs[i].scene.selected_entities(); - if entities.is_empty() { - use crate::command::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("LAYMCUR"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - let layer = entities[0].1.common().layer.clone(); - self.tabs[i].active_layer = layer.clone(); - self.ribbon.active_layer = layer.clone(); - self.tabs[i].layers.current_layer = layer.clone(); - self.command_line.push_info(&format!("Current layer set to \"{layer}\".")); - self.sync_ribbon_layers(); - } - } - - "LAYON" => { - self.push_undo_snapshot(i, "LAYON"); - for name in self.tabs[i].scene.document.layers.iter() - .map(|l| l.name.clone()).collect::>() - { - if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { - dl.turn_on(); - } - } - self.tabs[i].dirty = true; - self.sync_ribbon_layers(); - self.command_line.push_info("All layers turned on."); - } - - "LAYTHW" => { - self.push_undo_snapshot(i, "LAYTHW"); - for name in self.tabs[i].scene.document.layers.iter() - .map(|l| l.name.clone()).collect::>() - { - if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { - dl.thaw(); - } - } - self.tabs[i].dirty = true; - self.sync_ribbon_layers(); - self.command_line.push_info("All layers thawed."); - } - - "LAYULK" => { - let handles: Vec<_> = self.tabs[i].scene.selected_entities() - .into_iter().map(|(h, _)| h).collect(); - if handles.is_empty() { - use crate::command::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("LAYULK"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - let layers: std::collections::HashSet = self.tabs[i].scene - .selected_entities().into_iter() - .map(|(_, e)| e.common().layer.clone()).collect(); - self.push_undo_snapshot(i, "LAYULK"); - for name in &layers { - if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) { - dl.unlock(); - } - } - self.tabs[i].dirty = true; - self.sync_ribbon_layers(); - self.command_line.push_info("Layer(s) unlocked."); - } - } - - "LAYMATCH"|"LAYMCH" => { - use crate::command::laymatch::LayMatchCommand; - let dest: Vec<_> = self.tabs[i].scene.selected_entities() - .into_iter().map(|(h, _)| h).collect(); - let cmd = LayMatchCommand::new(dest); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "MATCHPROP"|"MA" => { - use crate::command::matchprop::MatchPropCommand; - self.tabs[i].scene.deselect_all(); - let cmd = MatchPropCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - - "GROUP"|"G" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::command::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("GROUP"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - let auto_name = Self::next_group_auto_name(&self.tabs[i].scene); - use crate::command::group::GroupCommand; - let cmd = GroupCommand::new(handles, auto_name); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - } - - "UNGROUP"|"UG" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::command::ungroup::UngroupCommand; - let cmd = UngroupCommand::new(); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - self.push_undo_snapshot(i, "UNGROUP"); - let count = self.tabs[i].scene.delete_groups_containing(&handles); - self.tabs[i].dirty = true; - if count > 0 { - self.command_line.push_info(&format!("{} group(s) dissolved.", count)); - } else { - self.command_line.push_info("No groups found for selected objects."); - } - } - } - - "COPYCLIP"|"CC" => { - let handles: Vec<_> = self.tabs[i] - .scene.selected_entities().into_iter().map(|(h, _)| h).collect(); - if handles.is_empty() { - use crate::command::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("COPYCLIP"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - let entities: Vec<_> = handles.iter() - .filter_map(|&h| self.tabs[i].scene.document.get_entity(h).cloned()) - .collect(); - self.clipboard_centroid = Self::entities_centroid( - &self.tabs[i].scene.wire_models_for(&handles), - ); - self.clipboard = entities; - self.command_line.push_info( - &format!("{} object(s) copied to clipboard.", self.clipboard.len()), - ); - } - } - - "CUTCLIP"|"CX" => { - let handles: Vec<_> = self.tabs[i] - .scene.selected_entities().into_iter().map(|(h, _)| h).collect(); - if handles.is_empty() { - use crate::command::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("CUTCLIP"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - let entities: Vec<_> = handles.iter() - .filter_map(|&h| self.tabs[i].scene.document.get_entity(h).cloned()) - .collect(); - self.clipboard_centroid = Self::entities_centroid( - &self.tabs[i].scene.wire_models_for(&handles), - ); - let count = entities.len(); - self.clipboard = entities; - self.push_undo_snapshot(i, "CUTCLIP"); - self.tabs[i].scene.erase_entities(&handles); - self.tabs[i].scene.deselect_all(); - self.tabs[i].dirty = true; - self.refresh_properties(); - self.command_line.push_info( - &format!("{} object(s) cut to clipboard.", count), - ); - } - } - - "PASTECLIP"|"PC" => { - if self.clipboard.is_empty() { - self.command_line.push_error("Clipboard is empty."); - } else { - let wires = self.tabs[i].scene.wires_for_entities(&self.clipboard); - use crate::command::paste::PasteCommand; - let cmd = PasteCommand::new(wires, self.clipboard_centroid); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - } - - "BLOCK" => { - let handles: Vec<_> = self.tabs[i] - .scene - .selected_entities() - .into_iter() - .map(|(h, _)| h) - .collect(); - if handles.is_empty() { - use crate::command::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("BLOCK"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::insert::create_block::CreateBlockCommand; - let cmd = CreateBlockCommand::new(handles); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - } - "INSERT" => { - let blocks = self.tabs[i].scene.custom_block_names(); - if blocks.is_empty() { - self.command_line - .push_error("No user-defined blocks found in this drawing."); - } else { - use crate::modules::insert::insert_block::InsertBlockCommand; - let cmd = InsertBlockCommand::new(blocks); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } - } - - "LINE"|"L" => { - use crate::modules::home::draw::line::LineCommand; - let new_cmd = LineCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "CIRCLE"|"C" => { - use crate::modules::home::draw::circle::CircleCommand; - let new_cmd = CircleCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "CIRCLE_CD" => { - use crate::modules::home::draw::circle::CircleCDCommand; - let new_cmd = CircleCDCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "CIRCLE_2P" => { - use crate::modules::home::draw::circle::Circle2PCommand; - let new_cmd = Circle2PCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "CIRCLE_3P" => { - use crate::modules::home::draw::circle::Circle3PCommand; - let new_cmd = Circle3PCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "CIRCLE_TTR" => { - use crate::modules::home::draw::circle::CircleTTRCommand; - let new_cmd = CircleTTRCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.pre_cmd_tangent = Some(self.snapper.is_on(crate::snap::SnapType::Tangent)); - self.snapper.enabled.insert(crate::snap::SnapType::Tangent); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "CIRCLE_TTT" => { - use crate::modules::home::draw::circle::CircleTTTCommand; - let new_cmd = CircleTTTCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.pre_cmd_tangent = Some(self.snapper.is_on(crate::snap::SnapType::Tangent)); - self.snapper.enabled.insert(crate::snap::SnapType::Tangent); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "ARC"|"A" => { - use crate::modules::home::draw::arc::ArcCommand; - let new_cmd = ArcCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "ARC_3P" => { - use crate::modules::home::draw::arc::Arc3PCommand; - let new_cmd = Arc3PCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "ARC_SCE" => { - use crate::modules::home::draw::arc::ArcSCECommand; - let new_cmd = ArcSCECommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "ARC_SCA" => { - use crate::modules::home::draw::arc::ArcSCACommand; - let new_cmd = ArcSCACommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "ARC_SCL" => { - use crate::modules::home::draw::arc::ArcSCLCommand; - let new_cmd = ArcSCLCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "ARC_SEA" => { - use crate::modules::home::draw::arc::ArcSEACommand; - let new_cmd = ArcSEACommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "ARC_SER" => { - use crate::modules::home::draw::arc::ArcSERCommand; - let new_cmd = ArcSERCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "ARC_SED" => { - use crate::modules::home::draw::arc::ArcSEDCommand; - let new_cmd = ArcSEDCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "ARC_CSA" => { - use crate::modules::home::draw::arc::ArcCSACommand; - let new_cmd = ArcCSACommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "ARC_CSL" => { - use crate::modules::home::draw::arc::ArcCSLCommand; - let new_cmd = ArcCSLCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "RECT" => { - use crate::modules::home::draw::shapes::RectCommand; - let new_cmd = RectCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "RECT_ROT" => { - use crate::modules::home::draw::shapes::RectRotCommand; - let new_cmd = RectRotCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "RECT_CEN" => { - use crate::modules::home::draw::shapes::RectCenCommand; - let new_cmd = RectCenCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "POLY" => { - use crate::modules::home::draw::shapes::PolyCommand; - let new_cmd = PolyCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "POLY_C" => { - use crate::modules::home::draw::shapes::PolyCCommand; - let new_cmd = PolyCCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "POLY_E" => { - use crate::modules::home::draw::shapes::PolyECommand; - let new_cmd = PolyECommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "PLINE"|"PL" => { - use crate::modules::home::draw::polyline::PlineCommand; - let new_cmd = PlineCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "MOVE"|"M" => { - let handles: Vec<_> = self.tabs[i].scene.selected_entities() - .into_iter().map(|(h, _)| h).collect(); - if handles.is_empty() { - use crate::command::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("MOVE"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::home::modify::translate::MoveCommand; - let wires = self.tabs[i].scene.wire_models_for(&handles); - let new_cmd = MoveCommand::new(handles, wires); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - "COPY"|"CO" => { - let handles: Vec<_> = self.tabs[i].scene.selected_entities() - .into_iter().map(|(h, _)| h).collect(); - if handles.is_empty() { - use crate::command::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("COPY"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::home::modify::copy::CopyCommand; - let wires = self.tabs[i].scene.wire_models_for(&handles); - let new_cmd = CopyCommand::new(handles, wires); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - "ROTATE"|"RO" => { - let handles: Vec<_> = self.tabs[i].scene.selected_entities() - .into_iter().map(|(h, _)| h).collect(); - if handles.is_empty() { - use crate::command::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("ROTATE"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::home::modify::rotate::RotateCommand; - let wires = self.tabs[i].scene.wire_models_for(&handles); - let new_cmd = RotateCommand::new(handles, wires); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - "POINT"|"PO" => { - use crate::modules::home::draw::point::PointCommand; - let new_cmd = PointCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "HATCH"|"H" => { - use crate::modules::home::draw::hatch::HatchCommand; - let outlines = self.tabs[i].scene.closed_outlines(); - let new_cmd = HatchCommand::new(outlines); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "GRADIENT" => { - use crate::modules::home::draw::hatch::GradientCommand; - let outlines = self.tabs[i].scene.closed_outlines(); - let new_cmd = GradientCommand::new(outlines); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "BOUNDARY" => { - use crate::modules::home::draw::hatch::BoundaryCommand; - let outlines = self.tabs[i].scene.closed_outlines(); - let new_cmd = BoundaryCommand::new(outlines); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "ELLIPSE"|"EL" => { - use crate::modules::home::draw::ellipse::EllipseCommand; - let new_cmd = EllipseCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "ELLIPSE_AXIS" => { - use crate::modules::home::draw::ellipse::EllipseAxisCommand; - let new_cmd = EllipseAxisCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "ELLIPSE_ARC" => { - use crate::modules::home::draw::ellipse::EllipseArcCommand; - let new_cmd = EllipseArcCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "SPLINE"|"SPL" => { - use crate::modules::home::draw::spline::SplineCommand; - let new_cmd = SplineCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "SCALE"|"SC" => { - let handles: Vec<_> = self.tabs[i].scene.selected_entities() - .into_iter().map(|(h, _)| h).collect(); - if handles.is_empty() { - use crate::command::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("SCALE"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::home::modify::scale::ScaleCommand; - let wires = self.tabs[i].scene.wire_models_for(&handles); - let new_cmd = ScaleCommand::new(handles, wires); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - "MIRROR"|"MI" => { - let handles: Vec<_> = self.tabs[i].scene.selected_entities() - .into_iter().map(|(h, _)| h).collect(); - if handles.is_empty() { - use crate::command::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("MIRROR"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::home::modify::mirror::MirrorCommand; - let wires = self.tabs[i].scene.wire_models_for(&handles); - let new_cmd = MirrorCommand::new(handles, wires); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - "ERASE"|"E" => { - let handles: Vec<_> = self.tabs[i].scene.selected_entities() - .into_iter().map(|(h, _)| h).collect(); - if handles.is_empty() { - use crate::command::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("ERASE"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - let n = handles.len(); - self.push_undo_snapshot(i, "ERASE"); - self.tabs[i].scene.erase_entities(&handles); - self.tabs[i].dirty = true; - self.refresh_properties(); - self.command_line.push_output(&format!("{n} object(s) erased.")); - } - } - - "TEXT"|"T"|"DT" => { - use crate::modules::annotate::text::TextCommand; - let new_cmd = TextCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "MTEXT"|"MT" => { - use crate::modules::annotate::mtext::MTextCommand; - let new_cmd = MTextCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "DIMLINEAR" => { - use crate::modules::annotate::linear_dim::LinearDimensionCommand; - let new_cmd = LinearDimensionCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "DIMRADIUS" => { - use crate::modules::annotate::radius_dim::RadiusDimensionCommand; - let new_cmd = RadiusDimensionCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "DIMANGULAR" => { - use crate::modules::annotate::angular_dim::AngularDimensionCommand; - let new_cmd = AngularDimensionCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "LEADER"|"LE" => { - use crate::modules::annotate::leader_cmd::LeaderCommand; - let new_cmd = LeaderCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "MLEADER"|"MLD" => { - use crate::modules::annotate::mleader_cmd::MLeaderCommand; - let new_cmd = MLeaderCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "ZOOM EXTENTS"|"ZOOMEXTENTS"|"ZE" => { - self.tabs[i].scene.fit_all(); - self.command_line.push_output("Zoom Extents"); - } - "STRETCH"|"SS" => { - let handles: Vec<_> = self.tabs[i].scene.selected_entities() - .into_iter().map(|(h, _)| h).collect(); - if handles.is_empty() { - use crate::command::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("STRETCH"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::home::modify::stretch::StretchCommand; - let new_cmd = StretchCommand::new(handles); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - "FILLET"|"F" => { - use crate::modules::home::modify::fillet::FilletCommand; - let entities: Vec<_> = self.tabs[i].scene.entity_wires().iter() - .filter_map(|w| { - let h = Scene::handle_from_wire_name(&w.name)?; - self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e)) - }).collect(); - let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect(); - let new_cmd = FilletCommand::new( - crate::modules::home::defaults::get_fillet_radius(), all_entities); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - - "ARRAY"|"AR"|"ARRAYRECT" => { - let handles: Vec<_> = self.tabs[i].scene.selected_entities() - .into_iter().map(|(h, _)| h).collect(); - if handles.is_empty() { - use crate::command::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("ARRAYRECT"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::home::modify::array::ArrayRectCommand; - let wires = self.tabs[i].scene.wire_models_for(&handles); - let new_cmd = ArrayRectCommand::new(handles, wires); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - "ARRAYPOLAR" => { - let handles: Vec<_> = self.tabs[i].scene.selected_entities() - .into_iter().map(|(h, _)| h).collect(); - if handles.is_empty() { - use crate::command::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("ARRAYPOLAR"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::home::modify::array::ArrayPolarCommand; - let wires = self.tabs[i].scene.wire_models_for(&handles); - let new_cmd = ArrayPolarCommand::new(handles, wires); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - "ARRAYPATH" => { - let handles: Vec<_> = self.tabs[i].scene.selected_entities() - .into_iter().map(|(h, _)| h).collect(); - if handles.is_empty() { - use crate::command::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("ARRAYPATH"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - use crate::modules::home::modify::array::ArrayPathCommand; - let wires = self.tabs[i].scene.wire_models_for(&handles); - let all_entities: Vec<_> = self.tabs[i].scene.entity_wires().iter() - .filter_map(|w| { - let h = Scene::handle_from_wire_name(&w.name)?; - self.tabs[i].scene.document.get_entity(h).cloned() - }).collect(); - let new_cmd = ArrayPathCommand::new(handles, wires, all_entities); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - "CHAMFER"|"CHA" => { - use crate::modules::home::modify::fillet::ChamferCommand; - let entities: Vec<_> = self.tabs[i].scene.entity_wires().iter() - .filter_map(|w| { - let h = Scene::handle_from_wire_name(&w.name)?; - self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e)) - }).collect(); - let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect(); - let new_cmd = ChamferCommand::new( - crate::modules::home::defaults::get_chamfer_dist1(), all_entities); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "EXPLODE"|"X" => { - use crate::modules::home::modify::explode::explode_entity; - // Collect into owned vec to release borrow before mutating - let entities: Vec<_> = self.tabs[i].scene.selected_entities() - .into_iter().collect(); - if entities.is_empty() { - use crate::command::select::SelectObjectsCommand; - let cmd = SelectObjectsCommand::new("EXPLODE"); - self.command_line.push_info(&cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd)); - } else { - // Pre-compute all replacements before mutating - let replacements: Vec<(acadrust::Handle, Vec)> = entities - .iter() - .filter_map(|(h, e)| { - let pieces = explode_entity(e, &self.tabs[i].scene.document); - if pieces.is_empty() { None } else { Some((*h, pieces)) } - }) - .collect(); - let exploded = replacements.len(); - if exploded > 0 { - self.push_undo_snapshot(i, "EXPLODE"); - } - for (handle, pieces) in replacements { - self.tabs[i].scene.erase_entities(&[handle]); - for piece in pieces { - self.tabs[i].scene.add_entity(piece); - } - } - if exploded > 0 { - self.tabs[i].dirty = true; - self.refresh_properties(); - self.command_line.push_output(&format!("{exploded} object(s) exploded.")); - } else { - self.command_line.push_info("EXPLODE: no explodable objects selected."); - } - } - } - - "OFFSET"|"O" => { - use crate::modules::home::modify::offset::OffsetCommand; - let all_entities: Vec<_> = self.tabs[i].scene.entity_wires().iter() - .filter_map(|w| { - let h = Scene::handle_from_wire_name(&w.name)?; - self.tabs[i].scene.document.get_entity(h).cloned() - }).collect(); - let new_cmd = OffsetCommand::new( - crate::modules::home::defaults::get_offset_dist(), all_entities); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - - "TRIM"|"TR" => { - use crate::modules::home::modify::trim::TrimCommand; - let entities: Vec<_> = self.tabs[i].scene.entity_wires().iter() - .filter_map(|w| { - let h = Scene::handle_from_wire_name(&w.name)?; - self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e)) - }).collect(); - let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect(); - let new_cmd = TrimCommand::new(all_entities); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "EXTEND"|"EX" => { - use crate::modules::home::modify::trim::ExtendCommand; - let entities: Vec<_> = self.tabs[i].scene.entity_wires().iter() - .filter_map(|w| { - let h = Scene::handle_from_wire_name(&w.name)?; - self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e)) - }).collect(); - let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect(); - let new_cmd = ExtendCommand::new(all_entities); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - "3DORBIT"|"3O"=> self.command_line.push_info("3D Orbit: drag with right mouse button."), - "HELP"|"?" => self.command_line.push_output("Draw: LINE CIRCLE ARC PLINE POINT ELLIPSE SPLINE | Modify: MOVE COPY ROTATE SCALE MIRROR ERASE | Text: TEXT MTEXT | File: OPEN SAVE SAVEAS"), - - "DONATE" => { - let _ = open::that("https://patreon.com/HakanSeven12"); - self.command_line.push_info("Opening Patreon page..."); - } - - // ── Layout / viewport ───────────────────────────────────────── - "MVIEW"|"MV" => { - if self.tabs[i].scene.current_layout == "Model" { - self.command_line.push_error("MVIEW: önce bir paper space layout'una geçin."); - } else { - use crate::modules::layout::mview::MviewCommand; - let new_cmd = MviewCommand::new(); - self.command_line.push_info(&new_cmd.prompt()); - self.tabs[i].active_cmd = Some(Box::new(new_cmd)); - } - } - - _ => self.command_line.push_error(&format!("Unknown command: {cmd}")), - } - // Focus the command line whenever a command just became active. - let i = self.active_tab; - if self.tabs[i].active_cmd.is_some() { - self.tabs[i].last_cmd = Some(cmd.to_string()); - self.focus_cmd_input() - } else { - Task::none() - } - } - - /// Rebuild the PropertiesPanel from the current entity selection. - /// Preserves UI state (open pickers, edit buffer) across refreshes. - fn refresh_properties(&mut self) { - let i = self.active_tab; - // Preserve transient UI state across rebuilds. - let color_picker_open = self.tabs[i].properties.color_picker_open; - let color_palette_open = self.tabs[i].properties.color_palette_open; - let edit_buf = std::mem::take(&mut self.tabs[i].properties.edit_buf); - let selected_group = self.tabs[i].properties.selected_group.clone(); - - let layer_names: Vec = self.tabs[i] - .scene - .document - .layers - .iter() - .map(|l| l.name.clone()) - .collect(); - let linetype_items: Vec = self.tabs[i] - .scene - .document - .line_types - .iter() - .map(|lt| { - let name = if lt.name.is_empty() { - "ByLayer".to_string() - } else { - lt.name.clone() - }; - let art = linetypes::extract_pattern(<.description); - ui::properties::LinetypeItem { name, art } - }) - .collect(); - let text_style_names: Vec = self.tabs[i] - .scene - .document - .text_styles - .iter() - .map(|style| style.name.trim().to_string()) - .filter(|name| !name.is_empty()) - .collect(); - - let new_panel = { - let selected = self.tabs[i].scene.selected_entities(); - let mut panel = match selected.len() { - 0 => ui::PropertiesPanel::empty(), - 1 => { - let (handle, entity) = selected[0]; - let group_names = self.tabs[i].scene.group_names_for_entity(handle); - let mut sections = - dispatch::properties_sectioned(handle, entity, &text_style_names); - if !group_names.is_empty() { - let label = group_names.join(", "); - if let Some(general) = sections.first_mut() { - general.props.push(crate::scene::object::Property { - label: "Group".to_string(), - field: "group", - value: crate::scene::object::PropValue::ReadOnly(label), - }); - } - } - let title = entity_type_label(entity); - ui::PropertiesPanel { - choice_combos: sections - .iter() - .flat_map(|section| section.props.iter()) - .filter_map(|prop| match &prop.value { - crate::scene::object::PropValue::Choice { options, .. } => Some(( - prop.field.to_string(), - iced::widget::combo_box::State::new(options.clone()), - )), - _ => None, - }) - .collect(), - sections, - title, - layer_combo: iced::widget::combo_box::State::new(layer_names.clone()), - linetype_combo: iced::widget::combo_box::State::new(linetype_items.clone()), - hatch_pattern_combo: iced::widget::combo_box::State::new( - crate::scene::hatch_patterns::names(), - ), - lineweight_combo: iced::widget::combo_box::State::new( - ui::properties::lw_options(), - ), - linetype_items, - ..Default::default() - } - } - _ => { - let groups = build_selection_groups(&selected); - let active_group = selected_group - .and_then(|group| groups.iter().find(|g| g.label == group.label).cloned()) - .or_else(|| groups.first().cloned()); - - let filtered: Vec<(Handle, &EntityType)> = active_group - .as_ref() - .map(|group| { - selected - .iter() - .filter(|(handle, _)| group.handles.contains(handle)) - .copied() - .collect() - }) - .unwrap_or_default(); - - let sections = aggregate_sections(&filtered, &text_style_names); - ui::PropertiesPanel { - choice_combos: sections - .iter() - .flat_map(|section| section.props.iter()) - .filter_map(|prop| match &prop.value { - crate::scene::object::PropValue::Choice { options, .. } => Some(( - prop.field.to_string(), - iced::widget::combo_box::State::new(options.clone()), - )), - _ => None, - }) - .collect(), - sections, - title: format!("{} objects selected", selected.len()), - selection_group_combo: iced::widget::combo_box::State::new(groups.clone()), - selection_groups: groups, - selected_group: active_group, - layer_combo: iced::widget::combo_box::State::new(layer_names.clone()), - linetype_combo: iced::widget::combo_box::State::new(linetype_items.clone()), - hatch_pattern_combo: iced::widget::combo_box::State::new( - crate::scene::hatch_patterns::names(), - ), - lineweight_combo: iced::widget::combo_box::State::new( - ui::properties::lw_options(), - ), - linetype_items, - ..Default::default() - } - } - }; - // Restore UI state. - panel.color_picker_open = color_picker_open; - panel.color_palette_open = color_palette_open; - panel.edit_buf = edit_buf; - panel - // `selected` is dropped here, releasing the borrow on `self.tabs[i].scene` - }; - - self.tabs[i].properties = new_panel; - self.refresh_selected_grips(); - } - - /// Rebuild the cached selected_grips from the current entity selection. - fn refresh_selected_grips(&mut self) { - let i = self.active_tab; - let (new_handle, new_grips) = { - let selected = self.tabs[i].scene.selected_entities(); - if selected.len() == 1 { - let (handle, entity) = selected[0]; - let grips = dispatch::grips(entity); - (Some(handle), grips) - } else { - (None, vec![]) - } - // `selected` is dropped here - }; - self.tabs[i].selected_handle = new_handle; - self.tabs[i].selected_grips = new_grips; - } - - fn property_target_handles(&self, i: usize) -> Vec { - let handles = self.tabs[i].properties.selected_handles(); - if !handles.is_empty() { - handles - } else { - self.tabs[i].selected_handle.into_iter().collect() - } - } - - fn history_label_from_active_cmd(&self, i: usize, fallback: &'static str) -> String { - self.tabs[i] - .active_cmd - .as_ref() - .map(|cmd| cmd.name().to_string()) - .unwrap_or_else(|| fallback.to_string()) - } - - fn capture_history_snapshot(&self, i: usize, label: impl Into) -> HistorySnapshot { - HistorySnapshot { - document: self.tabs[i].scene.document.clone(), - current_layout: self.tabs[i].scene.current_layout.clone(), - selected: self.tabs[i].scene.selected.iter().copied().collect(), - dirty: self.tabs[i].dirty, - label: label.into(), - } - } - - fn sync_ribbon_layers(&mut self) { - let i = self.active_tab; - let active = self.tabs[i].active_layer.clone(); - // Build LayerInfo from the panel (already synced from doc). - let infos: Vec = self.tabs[i] - .layers - .layers - .iter() - .map(|l| crate::ui::ribbon::LayerInfo { - name: l.name.clone(), - color: l.color, - visible: l.visible, - frozen: l.frozen, - locked: l.locked, - }) - .collect(); - let names: Vec = infos.iter().map(|l| l.name.clone()).collect(); - // If the active layer no longer exists, fall back to "0". - let active = if names.contains(&active) { active } else { "0".to_string() }; - self.tabs[i].active_layer = active.clone(); - self.tabs[i].layers.current_layer = active.clone(); - self.ribbon.set_layers(infos, &active); - // Sync linetypes into the layer panel (using LinetypeItem with ASCII art) - let lt_items: Vec = self.tabs[i] - .scene - .document - .line_types - .iter() - .map(|lt| { - let name = if lt.name.eq_ignore_ascii_case("bylayer") { - "ByLayer".to_string() - } else { - lt.name.clone() - }; - let art = linetypes::extract_pattern(<.description); - ui::properties::LinetypeItem { name, art } - }) - .collect(); - self.tabs[i].layers.sync_linetypes(lt_items.clone()); - self.ribbon.set_available_linetypes(lt_items); - } - - fn push_undo_snapshot(&mut self, i: usize, label: impl Into) { - let snapshot = self.capture_history_snapshot(i, label); - self.tabs[i].history.undo_stack.push(snapshot); - self.tabs[i].history.redo_stack.clear(); - } - - fn restore_history_snapshot(&mut self, i: usize, snapshot: HistorySnapshot) { - self.tabs[i].scene.document = snapshot.document; - self.tabs[i].scene.current_layout = snapshot.current_layout; - self.tabs[i].scene.selected = snapshot - .selected - .into_iter() - .filter(|h| self.tabs[i].scene.document.get_entity(*h).is_some()) - .collect::>(); - self.tabs[i].scene.populate_hatches_from_document(); - self.tabs[i].scene.clear_preview_wire(); - self.tabs[i].scene.meshes.clear(); - self.tabs[i].active_cmd = None; - self.tabs[i].snap_result = None; - self.tabs[i].active_grip = None; - self.tabs[i].dirty = snapshot.dirty; - let doc_layers = self.tabs[i].scene.document.layers.clone(); - self.tabs[i].layers.sync_from_doc(&doc_layers); - self.sync_ribbon_layers(); - self.refresh_properties(); - } - - fn undo_active_tab(&mut self) { - self.undo_steps(1); - } - - fn redo_active_tab(&mut self) { - self.redo_steps(1); - } - - fn undo_steps(&mut self, steps: usize) { - let i = self.active_tab; - let available = self.tabs[i].history.undo_stack.len(); - let steps = steps.min(available); - if steps == 0 { - self.command_line.push_info("Nothing to undo."); - return; - } - - let mut last_label = String::new(); - for _ in 0..steps { - let Some(snapshot) = self.tabs[i].history.undo_stack.pop() else { - break; - }; - let label = snapshot.label.clone(); - let current = self.capture_history_snapshot(i, label.clone()); - self.tabs[i].history.redo_stack.push(current); - self.restore_history_snapshot(i, snapshot); - last_label = label; - } - self.command_line.push_output(&format!("Undo: {last_label}")); - } - - fn redo_steps(&mut self, steps: usize) { - let i = self.active_tab; - let available = self.tabs[i].history.redo_stack.len(); - let steps = steps.min(available); - if steps == 0 { - self.command_line.push_info("Nothing to redo."); - return; - } - - let mut last_label = String::new(); - for _ in 0..steps { - let Some(snapshot) = self.tabs[i].history.redo_stack.pop() else { - break; - }; - let label = snapshot.label.clone(); - let current = self.capture_history_snapshot(i, label.clone()); - self.tabs[i].history.undo_stack.push(current); - self.restore_history_snapshot(i, snapshot); - last_label = label; - } - self.command_line.push_output(&format!("Redo: {last_label}")); - } - - /// Add an entity to the correct space: - /// - Viewport entities while in paper space → add_entity_to_layout - /// - Everything else → model space (add_entity) - fn commit_entity(&mut self, mut entity: acadrust::EntityType) { - let i = self.active_tab; - let layer = &self.tabs[i].active_layer; - if layer != "0" || entity.as_entity().layer().is_empty() { - entity.as_entity_mut().set_layer(layer.clone()); - } - - // Apply active color/linetype/lineweight from Properties toolbar. - crate::scene::dispatch::apply_color(&mut entity, self.ribbon.active_color); - crate::scene::dispatch::apply_common_prop( - &mut entity, - "linetype", - &self.ribbon.active_linetype.clone(), - ); - crate::scene::dispatch::apply_line_weight(&mut entity, self.ribbon.active_lineweight); - - if matches!(&entity, acadrust::EntityType::Viewport(_)) - && self.tabs[i].scene.current_layout != "Model" - { - let layout = self.tabs[i].scene.current_layout.clone(); - match self.tabs[i] - .scene - .document - .add_entity_to_layout(entity, &layout) - { - Ok(_) => {} - Err(e) => self - .command_line - .push_error(&format!("Viewport eklenemedi: {e}")), - } - } else { - self.tabs[i].scene.add_entity(entity); - } - } - - fn apply_cmd_result(&mut self, result: CmdResult) -> Task { - let i = self.active_tab; - match result { - CmdResult::NeedPoint => { - let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); - if let Some(p) = prompt { - self.command_line.push_info(&p); - } - } - CmdResult::Preview(wire) => { - self.tabs[i].scene.set_preview_wires(vec![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); - } - } - CmdResult::InterimWire(wire) => { - self.tabs[i].scene.set_interim_wire(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); - } - } - CmdResult::CommitEntity(entity) => { - let label = self.history_label_from_active_cmd(i, "ENTITY"); - self.push_undo_snapshot(i, label); - self.commit_entity(entity); - self.tabs[i].dirty = true; - let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); - if let Some(p) = prompt { - self.command_line.push_info(&p); - } - } - CmdResult::TransformSelected(handles, transform) => { - let label = self.history_label_from_active_cmd(i, "MOVE"); - self.push_undo_snapshot(i, label); - self.tabs[i].scene.transform_entities(&handles, &transform); - self.tabs[i].dirty = true; - self.tabs[i].scene.clear_preview_wire(); - self.tabs[i].active_cmd = None; - self.tabs[i].snap_result = None; - if let Some(was_on) = self.pre_cmd_tangent.take() { - if !was_on { - self.snapper.enabled.remove(&crate::snap::SnapType::Tangent); - } - } - self.refresh_properties(); - } - CmdResult::CopySelected(handles, transform) => { - let label = self.history_label_from_active_cmd(i, "COPY"); - self.push_undo_snapshot(i, label); - 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(); - } - CmdResult::CommitAndExit(entity) => { - let label = self.history_label_from_active_cmd(i, "ENTITY"); - self.push_undo_snapshot(i, label); - self.commit_entity(entity); - self.tabs[i].dirty = true; - self.tabs[i].scene.clear_preview_wire(); - self.tabs[i].active_cmd = None; - self.tabs[i].snap_result = None; - if let Some(was_on) = self.pre_cmd_tangent.take() { - if !was_on { - self.snapper.enabled.remove(&crate::snap::SnapType::Tangent); - } - } - } - CmdResult::CreateBlock { - handles, - name, - base, - } => { - self.push_undo_snapshot(i, "BLOCK"); - match self.tabs[i].scene.create_block_from_entities(&handles, &name, base) { - Ok(insert_handle) => { - self.tabs[i].dirty = true; - self.tabs[i].scene.deselect_all(); - if !insert_handle.is_null() { - self.tabs[i].scene.select_entity(insert_handle, false); - } - self.tabs[i].scene.clear_preview_wire(); - self.tabs[i].active_cmd = None; - self.tabs[i].snap_result = None; - self.command_line - .push_output(&format!("Block \"{name}\" created.")); - self.refresh_properties(); - } - Err(err) => { - let _ = self.tabs[i].history.undo_stack.pop(); - self.command_line.push_error(&err); - let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); - if let Some(p) = prompt { - self.command_line.push_info(&p); - } - } - } - } - CmdResult::CommitHatch(hatch) => { - let label = self.history_label_from_active_cmd(i, "HATCH"); - self.push_undo_snapshot(i, label); - let new_handle = self.tabs[i].scene.add_hatch(hatch); - // Select the newly created hatch so Properties reflects it immediately. - if !new_handle.is_null() { - self.tabs[i].scene.select_entity(new_handle, true); - } - self.tabs[i].dirty = true; - self.tabs[i].scene.clear_preview_wire(); - self.tabs[i].active_cmd = None; - self.tabs[i].snap_result = None; - if let Some(was_on) = self.pre_cmd_tangent.take() { - if !was_on { - self.snapper.enabled.remove(&crate::snap::SnapType::Tangent); - } - } - self.refresh_properties(); - } - CmdResult::BatchCopy(handles, transforms) => { - let label = self.history_label_from_active_cmd(i, "ARRAY"); - self.push_undo_snapshot(i, label); - let count = transforms.len(); - for t in &transforms { - self.tabs[i].scene.copy_entities(&handles, t); - } - self.tabs[i].dirty = true; - self.tabs[i].scene.clear_preview_wire(); - self.tabs[i].active_cmd = None; - self.tabs[i].snap_result = None; - if let Some(was_on) = self.pre_cmd_tangent.take() { - if !was_on { - self.snapper.enabled.remove(&crate::snap::SnapType::Tangent); - } - } - self.command_line - .push_output(&format!("ARRAY: {count} copies created.")); - self.refresh_properties(); - } - CmdResult::ReplaceMany(replacements, additions) => { - let label = self.history_label_from_active_cmd(i, "FILLET"); - self.push_undo_snapshot(i, label); - for (handle, entities) in replacements { - self.tabs[i].scene.erase_entities(&[handle]); - for entity in entities { - self.tabs[i].scene.add_entity(entity); - } - } - for entity in additions { - self.tabs[i].scene.add_entity(entity); - } - 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.refresh_properties(); - } - CmdResult::ReplaceEntity(handle, new_entities) => { - let label = self.history_label_from_active_cmd(i, "TRIM"); - self.push_undo_snapshot(i, label); - self.tabs[i].scene.erase_entities(&[handle]); - let new_handles: Vec = new_entities - .into_iter() - .map(|e| self.tabs[i].scene.add_entity(e)) - .collect(); - if let Some(cmd) = &mut self.tabs[i].active_cmd { - cmd.on_entity_replaced(handle, &new_handles); - } - self.tabs[i].dirty = true; - let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); - if let Some(p) = prompt { - self.command_line.push_info(&p); - } - } - CmdResult::Cancel => { - self.tabs[i].scene.clear_preview_wire(); - self.tabs[i].active_cmd = None; - self.tabs[i].snap_result = None; - if let Some(was_on) = self.pre_cmd_tangent.take() { - if !was_on { - self.snapper.enabled.remove(&crate::snap::SnapType::Tangent); - } - } - self.command_line.push_info("Command cancelled."); - } - CmdResult::Relaunch(cmd, handles) => { - // Install the gathered selection then re-dispatch the original command. - self.tabs[i].scene.deselect_all(); - for h in &handles { - self.tabs[i].scene.select_entity(*h, false); - } - self.tabs[i].active_cmd = None; - self.tabs[i].snap_result = None; - self.tabs[i].scene.clear_preview_wire(); - if let Some(was_on) = self.pre_cmd_tangent.take() { - if !was_on { - self.snapper.enabled.remove(&crate::snap::SnapType::Tangent); - } - } - let _ = self.dispatch_command(&cmd); - } - CmdResult::MatchEntityLayer { dest, src } => { - self.tabs[i].active_cmd = None; - self.tabs[i].snap_result = None; - self.tabs[i].scene.clear_preview_wire(); - // Look up source entity's layer name. - let src_layer = self.tabs[i].scene.document - .get_entity(src) - .map(|e| e.common().layer.clone()); - if let Some(layer) = src_layer { - self.push_undo_snapshot(i, "LAYMATCH"); - for h in &dest { - if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) { - e.as_entity_mut().set_layer(layer.clone()); - } - } - self.tabs[i].dirty = true; - self.command_line.push_info(&format!("Layer matched to \"{layer}\".")); - self.sync_ribbon_layers(); - } else { - self.command_line.push_error("Source object not found."); - } - } - CmdResult::MatchProperties { dest, src } => { - self.tabs[i].active_cmd = None; - self.tabs[i].snap_result = None; - self.tabs[i].scene.clear_preview_wire(); - - // Read all visual properties from source entity. - let props = self.tabs[i].scene.document.get_entity(src).map(|e| { - let c = e.common(); - ( - c.layer.clone(), - c.color, - c.linetype.clone(), - c.linetype_scale, - c.line_weight, - ) - }); - - if let Some((layer, color, linetype, lt_scale, lw)) = props { - self.push_undo_snapshot(i, "MATCHPROP"); - for h in &dest { - if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) { - e.as_entity_mut().set_layer(layer.clone()); - crate::scene::dispatch::apply_color(e, color); - crate::scene::dispatch::apply_line_weight(e, lw); - e.common_mut().linetype = linetype.clone(); - e.common_mut().linetype_scale = lt_scale; - } - } - self.tabs[i].dirty = true; - self.refresh_properties(); - self.command_line.push_info( - &format!("Properties matched to {} object(s).", dest.len()) - ); - } else { - self.command_line.push_error("Source object not found."); - } - } - CmdResult::PasteClipboard { base_pt } => { - self.tabs[i].active_cmd = None; - self.tabs[i].snap_result = None; - self.tabs[i].scene.clear_preview_wire(); - if self.clipboard.is_empty() { - self.command_line.push_error("Clipboard is empty."); - } else { - let delta = base_pt - self.clipboard_centroid; - let translate = command::EntityTransform::Translate(delta); - self.push_undo_snapshot(i, "PASTECLIP"); - let count = self.clipboard.len(); - let new_handles: Vec = self.clipboard.clone() - .into_iter() - .map(|mut entity| { - crate::scene::dispatch::apply_transform(&mut entity, &translate); - entity.common_mut().handle = acadrust::Handle::NULL; - self.tabs[i].scene.add_entity(entity) - }) - .filter(|h| !h.is_null()) - .collect(); - self.tabs[i].scene.deselect_all(); - for h in new_handles { - self.tabs[i].scene.select_entity(h, false); - } - self.tabs[i].dirty = true; - self.refresh_properties(); - self.command_line.push_info(&format!("{count} object(s) pasted.")); - } - } - CmdResult::CreateGroup { handles, name } => { - self.tabs[i].active_cmd = None; - self.tabs[i].snap_result = None; - self.tabs[i].scene.clear_preview_wire(); - self.push_undo_snapshot(i, "GROUP"); - self.tabs[i].scene.create_group(name.clone(), handles); - self.tabs[i].dirty = true; - self.command_line.push_info(&format!("Group \"{}\" created.", name)); - } - CmdResult::DeleteGroups { handles } => { - self.tabs[i].active_cmd = None; - self.tabs[i].snap_result = None; - self.tabs[i].scene.clear_preview_wire(); - self.push_undo_snapshot(i, "UNGROUP"); - let count = self.tabs[i].scene.delete_groups_containing(&handles); - self.tabs[i].dirty = true; - if count > 0 { - self.command_line.push_info(&format!("{} group(s) dissolved.", count)); - } else { - self.command_line.push_info("No groups found for selected objects."); - } - } - } - // Focus the command-line input while a command is active; blur it when the command ends. - if self.tabs[i].active_cmd.is_some() { - self.focus_cmd_input() - } else { - self.blur_cmd_input() - } - } - - /// Compute the centroid of a set of wire models (average of all points). - fn entities_centroid(wires: &[scene::WireModel]) -> glam::Vec3 { - let mut sum = glam::Vec3::ZERO; - let mut count = 0usize; - for w in wires { - for p in &w.points { - sum += glam::Vec3::from(*p); - count += 1; - } - } - if count > 0 { sum / count as f32 } else { glam::Vec3::ZERO } - } - - /// Generate the next available auto group name ("*A1", "*A2", …). - fn next_group_auto_name(scene: &scene::Scene) -> String { - let existing: HashSet = scene.groups().map(|g| g.name.clone()).collect(); - for n in 1..=9999 { - let name = format!("*A{n}"); - if !existing.contains(&name) { - return name; - } - } - "*A".to_string() - } - - fn focus_cmd_input(&self) -> Task { - iced::widget::operation::focus(iced::widget::Id::new(ui::command_line::CMD_INPUT_ID)) - } - - fn blur_cmd_input(&self) -> Task { - let op = iced::advanced::widget::operation::focusable::unfocus::(); - iced::advanced::widget::operate(op) - } - - fn view(&self, window_id: window::Id) -> Element<'_, Message> { - // ── Layer Properties Manager window ─────────────────────────────── - if Some(window_id) == self.layer_window { - let tab = &self.tabs[self.active_tab]; - return tab.layers.view_window(); - } - - let i = self.active_tab; - let tab = &self.tabs[i]; - let is_paper = tab.scene.current_layout != "Model"; - let viewport_3d = shader(&tab.scene).width(Fill).height(Fill); - - let selection_overlay = { - let sel = tab.scene.selection.borrow().clone(); - let snap_info = tab.snap_result.map(|s| (s.screen, s.snap_type)); - - // Compute grip markers when a single entity is selected and no command active. - let grips: Vec = - if tab.active_cmd.is_none() && !tab.selected_grips.is_empty() { - let (vw, vh) = tab.scene.selection.borrow().vp_size; - let bounds = iced::Rectangle { - x: 0.0, - y: 0.0, - width: vw, - height: vh, - }; - let vp_mat = tab.scene.camera.borrow().view_proj(bounds); - let sel_h = tab.selected_handle; - grips_to_screen(&tab.selected_grips, vp_mat, bounds) - .into_iter() - .filter(|(_, screen, _, _)| { - screen.x.is_finite() - && screen.y.is_finite() - && screen.x >= -bounds.width - && screen.x <= bounds.width * 2.0 - && screen.y >= -bounds.height - && screen.y <= bounds.height * 2.0 - }) - .map(|(grip_id, screen, _is_midpoint, shape)| { - let is_hot = tab - .active_grip - .as_ref() - .map_or(false, |g| Some(g.handle) == sel_h && g.grip_id == grip_id); - ui::overlay::GripMarker { - pos: screen, - shape, - is_hot, - } - }) - .collect() - } else { - vec![] - }; - - let grid = if self.show_grid { - let (vw, vh) = tab.scene.selection.borrow().vp_size; - let bounds = iced::Rectangle { - x: 0.0, - y: 0.0, - width: vw, - height: vh, - }; - let cam = tab.scene.camera.borrow(); - let plane = grid_plane_from_camera(cam.pitch, cam.yaw); - Some(overlay::GridParams { - view_proj: cam.view_proj(bounds), - bounds, - plane, - }) - } else { - None - }; - overlay::selection_overlay(sel, snap_info, grips, grid) - }; - - let nav = container(overlay::nav_toolbar()) - .align_right(Fill) - .align_top(Fill) - .padding(iced::Padding { - top: 148.0, - right: 8.0, - bottom: 0.0, - left: 0.0, - }); - - let info = container(overlay::info_bar( - if is_paper { - &tab.scene.current_layout - } else { - "Custom View" - }, - &tab.visual_style, - )) - .padding([4, 6]); - - let viewport_mouse = mouse_area(container( - iced::widget::Space::new().width(Fill).height(Fill), - )) - .on_move(Message::ViewportMove) - .on_press(Message::ViewportLeftPress) - .on_release(Message::ViewportLeftRelease) - .on_right_press(Message::ViewportRightPress) - .on_right_release(Message::ViewportRightRelease) - .on_middle_press(Message::ViewportMiddlePress) - .on_middle_release(Message::ViewportMiddleRelease) - .on_scroll(Message::ViewportScroll) - .on_exit(Message::ViewportExit); - - let cube_click = mouse_area(container( - iced::widget::Space::new() - .width(iced::Length::Fixed(VIEWCUBE_HIT_SIZE)) - .height(iced::Length::Fixed(VIEWCUBE_HIT_SIZE)), - )) - .on_move(Message::CursorMoved) - .on_press(Message::ViewportClick); - - let cube_click = container(cube_click) - .align_right(Fill) - .align_top(Fill) - .padding(iced::Padding { - top: VIEWCUBE_PAD, - right: VIEWCUBE_PAD, - bottom: 0.0, - left: 0.0, - }) - .width(Fill) - .height(Fill); - - let bg_color = if is_paper { - Color { - r: 0.22, - g: 0.24, - b: 0.28, - a: 1.0, - } // slightly blue-grey for paper space - } else { - Color { - r: 0.11, - g: 0.11, - b: 0.11, - a: 1.0, - } // dark for model space - }; - let viewport_stack = stack![ - container(viewport_3d) - .style(move |_: &Theme| container::Style { - background: Some(Background::Color(bg_color)), - ..Default::default() - }) - .width(Fill) - .height(Fill), - container(info).width(Fill).height(Fill), - selection_overlay, - viewport_mouse, - nav, - cube_click, - ]; - - let viewport_stack = viewport_stack.width(Fill).height(Fill); - let center_stack = iced::widget::stack![ - row![tab.properties.view(), viewport_stack] - .width(Fill) - .height(Fill), - ] - .width(Fill) - .height(Fill); - let center = center_stack; - - // Document tab bar — ribbon ile center arasında tam genişlikte - let tab_bar = doc_tab_bar(&self.tabs, self.active_tab); - - let main_ui = container( - column![ - self.ribbon.view( - is_paper, - self.tabs[self.active_tab].history.undo_stack.len(), - self.tabs[self.active_tab].history.redo_stack.len(), - ), - tab_bar, - center, - self.command_line.view(), - self.status_bar.view( - &self.snapper, - self.snap_popup_open, - self.ortho_mode, - self.polar_mode, - self.show_grid, - tab.scene.layout_names(), - tab.scene.current_layout.clone() - ) - ] - .width(Fill) - .height(Fill), - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { - r: 0.11, - g: 0.11, - b: 0.11, - a: 1.0, - })), - ..Default::default() - }) - .width(Fill) - .height(Fill); - - let snap_layer: Element<'_, Message> = if self.snap_popup_open { - ui::snap_popup::snap_popup_overlay(&self.snapper, 4.0) - } else { - iced::widget::Space::new().width(0).height(0).into() - }; - - let dropdown_layer: Element<'_, Message> = self - .ribbon - .dropdown_overlay( - &history_dropdown_labels(&self.tabs[self.active_tab].history.undo_stack), - &history_dropdown_labels(&self.tabs[self.active_tab].history.redo_stack), - ) - .unwrap_or_else(|| iced::widget::Space::new().width(0).height(0).into()); - - stack![main_ui, self.app_menu.view(), snap_layer, dropdown_layer].into() - } - - fn subscription(&self) -> Subscription { - use iced::{event, keyboard}; - iced::Subscription::batch([ - window::frames().map(Message::Tick), - event::listen_with(|ev, status, win_id| { - use iced::event::Status; - match ev { - iced::Event::Window(window::Event::Closed) => { - Some(Message::OsWindowClosed(win_id)) - } - iced::Event::Window(window::Event::Resized(sz)) => { - Some(Message::WindowResized(sz.width as f32, sz.height as f32)) - } - iced::Event::Keyboard(keyboard::Event::KeyPressed { - key, modifiers, .. - }) => { - let ctrl = modifiers.control(); - let shift = modifiers.shift(); - match key { - keyboard::Key::Named(keyboard::key::Named::Enter) - | keyboard::Key::Named(keyboard::key::Named::Space) - if status == Status::Ignored => - { - Some(Message::CommandFinalize) - } - keyboard::Key::Named(keyboard::key::Named::Escape) => { - Some(Message::CommandEscape) - } - keyboard::Key::Named(keyboard::key::Named::Delete) - if status == Status::Ignored => - { - Some(Message::DeleteSelected) - } - keyboard::Key::Named(keyboard::key::Named::F3) => { - Some(Message::ToggleSnapEnabled) - } - keyboard::Key::Named(keyboard::key::Named::F7) => { - Some(Message::ToggleGrid) - } - keyboard::Key::Named(keyboard::key::Named::F8) => { - Some(Message::ToggleOrtho) - } - keyboard::Key::Named(keyboard::key::Named::F9) => { - Some(Message::ToggleGridSnap) - } - keyboard::Key::Named(keyboard::key::Named::F10) => { - Some(Message::TogglePolar) - } - // ── File shortcuts ──────────────────────────── - keyboard::Key::Character(c) if ctrl => match c.as_str() { - "n" => Some(Message::ClearScene), - "o" => Some(Message::OpenFile), - "s" if !shift => Some(Message::SaveFile), - "s" if shift => Some(Message::SaveAs), - "z" if !shift => Some(Message::Undo), - "z" if shift => Some(Message::Redo), - "y" => Some(Message::Redo), - "c" => Some(Message::Command("COPYCLIP".to_string())), - "x" => Some(Message::Command("CUTCLIP".to_string())), - "v" => Some(Message::Command("PASTECLIP".to_string())), - _ => None, - }, - _ => None, - } - } - _ => None, - } - }), - ]) - } -} - -impl Default for H7CAD { - fn default() -> Self { - Self::new() - } -} - -fn history_dropdown_labels(stack: &[HistorySnapshot]) -> Vec { - stack.iter().rev().map(|snapshot| snapshot.label.clone()).collect() -} - -// ── Grid plane detection ─────────────────────────────────────────────────── - -/// Choose the grid plane whose normal is most aligned with the camera view direction. -/// Switches at the 45° octant boundary — no hysteresis needed since the grid is -/// a visual aid and not selection-sensitive. -fn grid_plane_from_camera(pitch: f32, yaw: f32) -> ui::overlay::GridPlane { - use ui::overlay::GridPlane; - // Camera forward direction components (Z-up, see camera.rs): - // forward.x = -cos(pitch) * sin(yaw) - // forward.y = -cos(pitch) * cos(yaw) - // forward.z = -sin(pitch) - let fz = pitch.sin().abs(); // Z-axis alignment → XY plane - let fy = (pitch.cos() * yaw.cos()).abs(); // Y-axis alignment → XZ plane - let fx = (pitch.cos() * yaw.sin()).abs(); // X-axis alignment → YZ plane - if fz >= fy && fz >= fx { - GridPlane::Xy - } else if fy >= fx { - GridPlane::Xz - } else { - GridPlane::Yz - } -} - -// ── Drawing constraint helpers ───────────────────────────────────────────── - -/// Constrain `pt` to the nearest 90° direction from `base` (XY plane, Z-up). -fn ortho_constrain(pt: glam::Vec3, base: glam::Vec3) -> glam::Vec3 { - let dx = (pt.x - base.x).abs(); - let dy = (pt.y - base.y).abs(); - if dx >= dy { - glam::Vec3::new(pt.x, base.y, pt.z) // horizontal - } else { - glam::Vec3::new(base.x, pt.y, pt.z) // vertical - } -} - -/// Constrain `pt` to the nearest polar angle multiple from `base` (XY plane, Z-up). -fn polar_constrain(pt: glam::Vec3, base: glam::Vec3, step_deg: f32) -> glam::Vec3 { - let dx = pt.x - base.x; - let dy = pt.y - base.y; - let dist = (dx * dx + dy * dy).sqrt(); - if dist < 1e-6 { - return pt; - } - let step = step_deg.to_radians(); - let angle = dy.atan2(dx); - let snapped = (angle / step).round() * step; - glam::Vec3::new( - base.x + dist * snapped.cos(), - base.y + dist * snapped.sin(), - pt.z, - ) -} - -// ── Document tab bar ─────────────────────────────────────────────────────── - -fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Element<'a, Message> { - const BAR_BG: Color = Color { - r: 0.13, - g: 0.13, - b: 0.13, - a: 1.0, - }; - const TAB_ACTIVE: Color = Color { - r: 0.22, - g: 0.22, - b: 0.22, - a: 1.0, - }; - const TAB_HOVER: Color = Color { - r: 0.18, - g: 0.18, - b: 0.18, - a: 1.0, - }; - const TAB_INACTIVE: Color = Color { - r: 0.13, - g: 0.13, - b: 0.13, - a: 1.0, - }; - const ACCENT: Color = Color { - r: 0.20, - g: 0.55, - b: 0.90, - a: 1.0, - }; - const TEXT_ACTIVE: Color = Color::WHITE; - const TEXT_INACTIVE: Color = Color { - r: 0.60, - g: 0.60, - b: 0.60, - a: 1.0, - }; - const CLOSE_HOVER: Color = Color { - r: 0.70, - g: 0.22, - b: 0.22, - a: 1.0, - }; - const BORDER_COLOR: Color = Color { - r: 0.25, - g: 0.25, - b: 0.25, - a: 1.0, - }; - - let mut bar = Row::new().spacing(0).align_y(iced::Center); - - for (idx, tab) in tabs.iter().enumerate() { - let is_active = idx == active_tab; - let name = tab.tab_display_name(); - let label = if tab.dirty { - format!("● {}", name) - } else { - name - }; - - let title_btn = button(text(label).size(12)) - .on_press(Message::TabSwitch(idx)) - .padding([5, 12]) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(match (is_active, status) { - (true, _) => TAB_ACTIVE, - (false, button::Status::Hovered) => TAB_HOVER, - _ => TAB_INACTIVE, - })), - text_color: if is_active { - TEXT_ACTIVE - } else { - TEXT_INACTIVE - }, - border: Border { - color: if is_active { - ACCENT - } else { - Color::TRANSPARENT - }, - width: if is_active { 1.0 } else { 0.0 }, - radius: 0.0.into(), - }, - shadow: iced::Shadow::default(), - snap: false, - }); - - // Show close button only when there are multiple tabs, or always show it - let close_btn = button(text("×").size(11).color(Color { - r: 0.55, - g: 0.55, - b: 0.55, - a: 1.0, - })) - .on_press(Message::TabClose(idx)) - .padding([3, 5]) - .style(move |_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => CLOSE_HOVER, - _ => { - if is_active { - TAB_ACTIVE - } else { - TAB_INACTIVE - } - } - })), - border: Border { - radius: 3.0.into(), - ..Default::default() - }, - ..Default::default() - }); - - bar = bar.push( - container(row![title_btn, close_btn].spacing(0).align_y(iced::Center)).style( - move |_: &Theme| container::Style { - border: Border { - color: if is_active { - BORDER_COLOR - } else { - Color::TRANSPARENT - }, - width: if is_active { 1.0 } else { 0.0 }, - radius: 0.0.into(), - }, - ..Default::default() - }, - ), - ); - } - - // "+" new tab button - let new_btn = button(text("+").size(14).color(Color { - r: 0.65, - g: 0.65, - b: 0.65, - a: 1.0, - })) - .on_press(Message::TabNew) - .padding([4, 10]) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered => TAB_HOVER, - _ => Color::TRANSPARENT, - })), - border: Border { - radius: 0.0.into(), - ..Default::default() - }, - ..Default::default() - }); - - bar = bar.push(new_btn); - bar = bar.push(iced::widget::Space::new().width(Fill)); - - container(bar) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BAR_BG)), - border: Border { - color: BORDER_COLOR, - width: 1.0, - radius: 0.0.into(), - }, - ..Default::default() - }) - .height(30) - .width(Fill) - .padding([0, 2]) - .into() -} - -// ── Window icon ──────────────────────────────────────────────────────────── - -/// Builds a 32×32 RGBA icon: red background with H7 drawn in white pixels. -fn build_window_icon() -> Vec { - const W: usize = 32; - const SZ: usize = W * W * 4; - - let bg = [176u8, 48, 32, 255]; - let fg = [255u8, 255, 255, 255]; - - let mut px = vec![0u8; SZ]; - for i in 0..W * W { - px[i * 4..i * 4 + 4].copy_from_slice(&bg); - } - - // Draw a 3px-wide line from (ax,ay) to (bx,by) - fn stroke(px: &mut Vec, ax: i32, ay: i32, bx: i32, by: i32, fg: [u8; 4]) { - let steps = ((bx - ax).abs().max((by - ay).abs()) * 3).max(1); - for s in 0..=steps { - let t = s as f32 / steps as f32; - let cx = ax as f32 + (bx - ax) as f32 * t; - let cy = ay as f32 + (by - ay) as f32 * t; - for dy in -1i32..=1 { - for dx in -1i32..=1 { - let ix = cx.round() as i32 + dx; - let iy = cy.round() as i32 + dy; - if ix >= 0 && ix < W as i32 && iy >= 0 && iy < W as i32 { - let idx = (iy as usize * W + ix as usize) * 4; - px[idx..idx + 4].copy_from_slice(&fg); - } - } - } - } - } - - // H - stroke(&mut px, 4, 5, 4, 26, fg); - stroke(&mut px, 13, 5, 13, 26, fg); - stroke(&mut px, 4, 15, 13, 15, fg); - // 7 - stroke(&mut px, 17, 5, 27, 5, fg); - stroke(&mut px, 27, 5, 20, 26, fg); - stroke(&mut px, 20, 16, 26, 16, fg); - - px -} - -// ── Standalone helpers ──────────────────────────────────────────────────── - -fn entity_type_label(entity: &acadrust::EntityType) -> String { - use acadrust::EntityType::*; - match entity { - Line(_) => "Line", - Circle(_) => "Circle", - Arc(_) => "Arc", - Ellipse(_) => "Ellipse", - Spline(_) => "Spline", - LwPolyline(_) => "Polyline", - Text(_) => "Text", - MText(_) => "MText", - Dimension(_) => "Dimension", - Insert(_) => "Block Reference", - Point(_) => "Point", - Hatch(_) => "Hatch", - _ => "Entity", - } - .to_string() -} - -fn entity_type_key(entity: &acadrust::EntityType) -> String { - match entity { - acadrust::EntityType::LwPolyline(_) => "pline", - acadrust::EntityType::Circle(_) => "circle", - acadrust::EntityType::Line(_) => "line", - acadrust::EntityType::Arc(_) => "arc", - acadrust::EntityType::Ellipse(_) => "ellipse", - acadrust::EntityType::Spline(_) => "spline", - acadrust::EntityType::Text(_) => "text", - acadrust::EntityType::MText(_) => "mtext", - acadrust::EntityType::Dimension(_) => "dimension", - acadrust::EntityType::Insert(_) => "insert", - acadrust::EntityType::Point(_) => "point", - acadrust::EntityType::Hatch(_) => "hatch", - _ => "entity", - } - .to_string() -} - -fn build_selection_groups( - selected: &[(Handle, &EntityType)], -) -> Vec { - let mut groups = vec![ui::properties::SelectionGroup { - label: format!("All({})", selected.len()), - handles: selected.iter().map(|(handle, _)| *handle).collect(), - }]; - - let mut by_type: std::collections::BTreeMap> = std::collections::BTreeMap::new(); - for (handle, entity) in selected { - by_type.entry(entity_type_key(entity)).or_default().push(*handle); - } - - for (kind, handles) in by_type { - groups.push(ui::properties::SelectionGroup { - label: format!("{}({})", title_case_word(&kind), handles.len()), - handles, - }); - } - - groups -} - -fn title_case_word(value: &str) -> String { - let mut chars = value.chars(); - match chars.next() { - Some(first) => { - let mut out = first.to_uppercase().collect::(); - out.push_str(chars.as_str()); - out - } - None => String::new(), - } -} - -fn aggregate_sections( - selected: &[(Handle, &EntityType)], - text_style_names: &[String], -) -> Vec { - if selected.is_empty() { - return vec![]; - } - - let mut all_sections: Vec> = selected - .iter() - .map(|(handle, entity)| dispatch::properties_sectioned(*handle, entity, text_style_names)) - .collect(); - - let mut result = all_sections.remove(0); - for sections in all_sections { - result = merge_sections(&result, §ions); - } - result -} - -fn merge_sections( - left: &[crate::scene::object::PropSection], - right: &[crate::scene::object::PropSection], -) -> Vec { - left.iter() - .filter_map(|section| { - let rhs = right.iter().find(|candidate| candidate.title == section.title)?; - let props: Vec = section - .props - .iter() - .filter_map(|prop| { - let other = rhs.props.iter().find(|candidate| candidate.field == prop.field)?; - Some(crate::scene::object::Property { - label: prop.label.clone(), - field: prop.field, - value: merge_prop_value(&prop.value, &other.value), - }) - }) - .collect(); - if props.is_empty() { - None - } else { - Some(crate::scene::object::PropSection { - title: section.title.clone(), - props, - }) - } - }) - .collect() -} - -fn merge_prop_value( - left: &crate::scene::object::PropValue, - right: &crate::scene::object::PropValue, -) -> crate::scene::object::PropValue { - use crate::scene::object::PropValue; - - if left == right { - return left.clone(); - } - - match (left, right) { - (PropValue::LayerChoice(_), PropValue::LayerChoice(_)) => { - PropValue::LayerChoice(VARIES_LABEL.into()) - } - (PropValue::ColorChoice(_), PropValue::ColorChoice(_)) - | (PropValue::ColorVaries, _) - | (_, PropValue::ColorVaries) => PropValue::ColorVaries, - (PropValue::LwChoice(_), PropValue::LwChoice(_)) - | (PropValue::LwVaries, _) - | (_, PropValue::LwVaries) => PropValue::LwVaries, - (PropValue::LinetypeChoice(_), PropValue::LinetypeChoice(_)) => { - PropValue::LinetypeChoice(VARIES_LABEL.into()) - } - ( - PropValue::Choice { options, .. }, - PropValue::Choice { - options: other_options, - .. - }, - ) if options == other_options => PropValue::Choice { - selected: VARIES_LABEL.into(), - options: options.clone(), - }, - (PropValue::EditText(_), PropValue::EditText(_)) => { - PropValue::EditText(VARIES_LABEL.into()) - } - (PropValue::ReadOnly(_), PropValue::ReadOnly(_)) => { - PropValue::ReadOnly(VARIES_LABEL.into()) - } - (PropValue::HatchPatternChoice(_), PropValue::HatchPatternChoice(_)) => { - PropValue::HatchPatternChoice(VARIES_LABEL.into()) - } - ( - PropValue::BoolToggle { field, .. }, - PropValue::BoolToggle { - field: other_field, .. - }, - ) if field == other_field => PropValue::ReadOnly(VARIES_LABEL.into()), - _ => left.clone(), - } + app::run() } diff --git a/src/ui/app_menu.rs b/src/ui/app_menu.rs index 20e37fea..03a34910 100644 --- a/src/ui/app_menu.rs +++ b/src/ui/app_menu.rs @@ -13,7 +13,7 @@ use iced::widget::{button, column, container, row, scrollable, text, text_input} use iced::{Background, Border, Color, Element, Length, Theme}; use std::path::PathBuf; -use crate::Message; +use crate::app::Message; // ── Menu item definitions ───────────────────────────────────────────────── diff --git a/src/ui/command_line.rs b/src/ui/command_line.rs index 88715200..258ddfb2 100644 --- a/src/ui/command_line.rs +++ b/src/ui/command_line.rs @@ -1,6 +1,6 @@ //! H7CAD-style command line — bottom panel with input and history -use crate::Message; +use crate::app::Message; use iced::widget::{column, container, row, text, text_input}; use iced::{Background, Border, Color, Element, Length, Theme}; diff --git a/src/ui/layers.rs b/src/ui/layers.rs index 54dc8b42..4f4cea05 100644 --- a/src/ui/layers.rs +++ b/src/ui/layers.rs @@ -1,7 +1,7 @@ //! Layer Manager — floating window. use crate::ui::properties::{color_picker_dropdown, LinetypeItem, LwItem, lw_options}; -use crate::Message; +use crate::app::Message; use acadrust::tables::layer::Layer as DocLayer; use acadrust::tables::Table; use acadrust::types::aci_table::aci_to_rgb; diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 575a2379..7c98ec7c 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -9,7 +9,7 @@ use iced::{Background, Border, Color, Element, Length, Point, Size, Theme}; use crate::scene::object::GripShape; use crate::scene::SelectionState; -use crate::Message; +use crate::app::Message; /// Half-size of the crosshair center square in screen pixels (square = SQ*2 × SQ*2). pub const CROSSHAIR_SQ: f32 = 7.5; diff --git a/src/ui/properties.rs b/src/ui/properties.rs index 74b63596..5ec43452 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -22,7 +22,7 @@ const COMBO_PAD_V: f32 = (ROW_H - FONT_SZ * 1.3 - 2.0) / 2.0; // fills combo const SWATCH_SZ: f32 = ROW_H * 0.54; // ≈14 px color swatch use crate::scene::object::{PropSection, PropValue}; -use crate::Message; +use crate::app::Message; const VARIES_LABEL: &str = "*VARIES*"; diff --git a/src/ui/ribbon.rs b/src/ui/ribbon.rs index 6c8af77f..93132f0e 100644 --- a/src/ui/ribbon.rs +++ b/src/ui/ribbon.rs @@ -18,7 +18,7 @@ use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme}; use crate::modules::registry; use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonItem, ToolDef}; use crate::ui::properties::{LwItem, LinetypeItem, acad_color_display, color_picker_dropdown, lw_options}; -use crate::Message; +use crate::app::Message; // ── Layout constants ─────────────────────────────────────────────────────── // diff --git a/src/ui/snap_popup.rs b/src/ui/snap_popup.rs index 6217170e..b39c9c03 100644 --- a/src/ui/snap_popup.rs +++ b/src/ui/snap_popup.rs @@ -7,7 +7,7 @@ use iced::widget::{button, column, container, mouse_area, row, text}; use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme}; use crate::snap::{SnapType, Snapper, ALL_SNAP_MODES}; -use crate::Message; +use crate::app::Message; /// Returns a full-screen overlay element: /// - a transparent click-catcher that closes the popup on outside click diff --git a/src/ui/statusbar.rs b/src/ui/statusbar.rs index 2feab62c..d9f08470 100644 --- a/src/ui/statusbar.rs +++ b/src/ui/statusbar.rs @@ -5,7 +5,7 @@ use iced::widget::{button, container, row, text, tooltip, Row}; use iced::{Background, Border, Color, Element, Length, Theme}; use crate::snap::Snapper; -use crate::Message; +use crate::app::Message; #[derive(Clone, Default)] pub struct StatusBar {