Refactor src/main.rs into modular src/app/ structure
Split the 4500-line main.rs into focused submodules under src/app/: - mod.rs: H7CAD struct, Message enum, boot/new/run - document.rs: DocumentTab, HistorySnapshot, HistoryState - helpers.rs: pure helper functions - history.rs: undo/redo logic - properties.rs: properties panel logic - layers.rs: layer sync - commands.rs: dispatch_command - cmd_result.rs: apply_cmd_result - view.rs: view, subscription, tab bar - update.rs: update message handler Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
854f07517e
commit
ed9cbc2092
19 changed files with 4150 additions and 4519 deletions
305
src/app/cmd_result.rs
Normal file
305
src/app/cmd_result.rs
Normal file
|
|
@ -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<Message> {
|
||||
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<acadrust::Handle> = 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<Handle> = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
913
src/app/commands.rs
Normal file
913
src/app/commands.rs
Normal file
|
|
@ -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<Message> {
|
||||
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<String> = 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<String> = 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<String> = 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::<Vec<_>>()
|
||||
{
|
||||
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::<Vec<_>>()
|
||||
{
|
||||
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<String> = 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<acadrust::EntityType>)> = 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
83
src/app/document.rs
Normal file
83
src/app/document.rs
Normal file
|
|
@ -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<PathBuf>,
|
||||
pub(super) dirty: bool,
|
||||
pub(super) tab_title: String,
|
||||
pub(super) properties: PropertiesPanel,
|
||||
pub(super) layers: LayerPanel,
|
||||
pub(super) active_cmd: Option<Box<dyn CadCommand>>,
|
||||
pub(super) last_cmd: Option<String>,
|
||||
pub(super) snap_result: Option<SnapResult>,
|
||||
pub(super) active_grip: Option<GripEdit>,
|
||||
pub(super) selected_grips: Vec<GripDef>,
|
||||
pub(super) selected_handle: Option<Handle>,
|
||||
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<Handle>,
|
||||
pub(super) dirty: bool,
|
||||
pub(super) label: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct HistoryState {
|
||||
pub(super) undo_stack: Vec<HistorySnapshot>,
|
||||
pub(super) redo_stack: Vec<HistorySnapshot>,
|
||||
}
|
||||
205
src/app/helpers.rs
Normal file
205
src/app/helpers.rs
Normal file
|
|
@ -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<glam::Vec3> {
|
||||
let parts: Vec<f32> = 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<String> =
|
||||
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::<String>();
|
||||
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<u8> {
|
||||
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<u8>, 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
|
||||
}
|
||||
107
src/app/history.rs
Normal file
107
src/app/history.rs
Normal file
|
|
@ -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<String>) -> 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<String>) {
|
||||
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::<HashSet<_>>();
|
||||
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<String> {
|
||||
stack.iter().rev().map(|snapshot| snapshot.label.clone()).collect()
|
||||
}
|
||||
43
src/app/layers.rs
Normal file
43
src/app/layers.rs
Normal file
|
|
@ -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<crate::ui::ribbon::LayerInfo> = 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<String> = 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<ui::properties::LinetypeItem> = 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);
|
||||
}
|
||||
}
|
||||
284
src/app/mod.rs
Normal file
284
src/app/mod.rs
Normal file
|
|
@ -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<DocumentTab>,
|
||||
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<bool>,
|
||||
/// 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<glam::Vec3>,
|
||||
/// OS window Id for the floating Layer Properties Manager (None when closed).
|
||||
layer_window: Option<window::Id>,
|
||||
/// OS window Id of the primary application window.
|
||||
main_window: Option<window::Id>,
|
||||
/// In-memory clipboard: cloned entities waiting to be pasted.
|
||||
clipboard: Vec<acadrust::EntityType>,
|
||||
/// 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<PathBuf>),
|
||||
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<Message>) {
|
||||
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()
|
||||
}
|
||||
347
src/app/properties.rs
Normal file
347
src/app/properties.rs
Normal file
|
|
@ -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<String> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
.iter()
|
||||
.map(|l| l.name.clone())
|
||||
.collect();
|
||||
let linetype_items: Vec<ui::properties::LinetypeItem> = 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<String> = 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<Handle> {
|
||||
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<ui::properties::SelectionGroup> {
|
||||
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<String, Vec<Handle>> =
|
||||
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<crate::scene::object::PropSection> {
|
||||
if selected.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let mut all_sections: Vec<Vec<crate::scene::object::PropSection>> = 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<crate::scene::object::PropSection> {
|
||||
left.iter()
|
||||
.filter_map(|section| {
|
||||
let rhs = right.iter().find(|candidate| candidate.title == section.title)?;
|
||||
let props: Vec<crate::scene::object::Property> = 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(),
|
||||
}
|
||||
}
|
||||
1484
src/app/update.rs
Normal file
1484
src/app/update.rs
Normal file
File diff suppressed because it is too large
Load diff
369
src/app/view.rs
Normal file
369
src/app/view.rs
Normal file
|
|
@ -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<overlay::GripMarker> =
|
||||
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<Message> {
|
||||
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<Message> {
|
||||
iced::widget::operation::focus(iced::widget::Id::new(crate::ui::command_line::CMD_INPUT_ID))
|
||||
}
|
||||
|
||||
pub(super) fn blur_cmd_input(&self) -> Task<Message> {
|
||||
let op = iced::advanced::widget::operation::focusable::unfocus::<Message>();
|
||||
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()
|
||||
}
|
||||
4513
src/main.rs
4513
src/main.rs
File diff suppressed because it is too large
Load diff
|
|
@ -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 ─────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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*";
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ───────────────────────────────────────────────────────
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue