2026-03-22 22:44:32 +03:00
|
|
|
|
use super::{H7CAD, Message, POLY_START_DELAY_MS};
|
|
|
|
|
|
use super::helpers::{parse_coord, angle_close, ortho_constrain, polar_constrain};
|
|
|
|
|
|
use crate::scene::{self, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX};
|
|
|
|
|
|
use crate::scene::grip::{find_hit_grip, GripEdit};
|
|
|
|
|
|
use crate::scene::object::GripApply;
|
|
|
|
|
|
use crate::modules::ModuleEvent;
|
|
|
|
|
|
use crate::ui::PropertiesPanel;
|
|
|
|
|
|
use acadrust::types::Color as AcadColor;
|
2026-03-31 07:24:16 +03:00
|
|
|
|
use acadrust::{EntityType as AcadEntityType, Handle};
|
2026-03-22 22:44:32 +03:00
|
|
|
|
use iced::time::Instant;
|
|
|
|
|
|
use iced::window;
|
|
|
|
|
|
use iced::{mouse, Task};
|
|
|
|
|
|
|
|
|
|
|
|
const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_DRAW_PX;
|
|
|
|
|
|
|
|
|
|
|
|
impl H7CAD {
|
|
|
|
|
|
pub fn update(&mut self, msg: Message) -> Task<Message> {
|
|
|
|
|
|
match msg {
|
|
|
|
|
|
Message::Tick(t) => {
|
|
|
|
|
|
self.tabs[self.active_tab].scene.update(t - self.start);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::OpenFile => Task::perform(crate::io::pick_and_open(), Message::FileOpened),
|
|
|
|
|
|
|
|
|
|
|
|
Message::FileOpened(Ok((name, path, doc))) => {
|
|
|
|
|
|
let entity_count = doc.entities().count();
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("Opened \"{name}\" — {entity_count} entities"));
|
|
|
|
|
|
self.app_menu.push_recent(path.clone());
|
|
|
|
|
|
|
|
|
|
|
|
let current_is_empty = {
|
|
|
|
|
|
let t = &self.tabs[self.active_tab];
|
|
|
|
|
|
t.current_path.is_none()
|
|
|
|
|
|
&& !t.dirty
|
|
|
|
|
|
&& self.tabs[self.active_tab].scene.document.entities().count() == 0
|
|
|
|
|
|
};
|
|
|
|
|
|
let i = if current_is_empty {
|
|
|
|
|
|
self.active_tab
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.tab_counter += 1;
|
|
|
|
|
|
let new_tab = super::document::DocumentTab::new_drawing(self.tab_counter);
|
|
|
|
|
|
self.tabs.push(new_tab);
|
|
|
|
|
|
let idx = self.tabs.len() - 1;
|
|
|
|
|
|
self.active_tab = idx;
|
|
|
|
|
|
idx
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
self.tabs[i].current_path = Some(path);
|
|
|
|
|
|
self.tabs[i].scene.document = doc;
|
|
|
|
|
|
self.tabs[i].scene.populate_hatches_from_document();
|
|
|
|
|
|
self.tabs[i].scene.selected = std::collections::HashSet::new();
|
|
|
|
|
|
self.tabs[i].scene.preview_wires = vec![];
|
|
|
|
|
|
self.tabs[i].scene.current_layout = "Model".to_string();
|
|
|
|
|
|
crate::linetypes::populate_document(&mut self.tabs[i].scene.document);
|
|
|
|
|
|
self.tabs[i].properties = PropertiesPanel::empty();
|
|
|
|
|
|
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
|
|
|
|
|
self.tabs[i].layers.sync_from_doc(&doc_layers);
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
self.tabs[i].scene.fit_all();
|
|
|
|
|
|
self.tabs[i].dirty = false;
|
|
|
|
|
|
self.tabs[i].history = super::document::HistoryState::default();
|
|
|
|
|
|
self.refresh_selected_grips();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::FileOpened(Err(e)) => {
|
|
|
|
|
|
if e != "Cancelled" {
|
|
|
|
|
|
self.command_line.push_error(&format!("Open failed: {e}"));
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::SaveFile => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if let Some(path) = &self.tabs[i].current_path {
|
|
|
|
|
|
let path = path.clone();
|
|
|
|
|
|
match crate::io::save(&self.tabs[i].scene.document, &path) {
|
|
|
|
|
|
Ok(()) => {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("Saved: {}", path.display()));
|
|
|
|
|
|
self.tabs[i].dirty = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return Task::perform(crate::io::pick_save_path(), Message::PickedSavePath);
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::SaveAs => Task::perform(crate::io::pick_save_path(), Message::PickedSavePath),
|
|
|
|
|
|
|
|
|
|
|
|
Message::PickedSavePath(Some(path)) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
match crate::io::save(&self.tabs[i].scene.document, &path) {
|
|
|
|
|
|
Ok(()) => {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("Saved: {}", path.display()));
|
|
|
|
|
|
self.tabs[i].current_path = Some(path);
|
|
|
|
|
|
self.tabs[i].dirty = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PickedSavePath(None) => Task::none(),
|
|
|
|
|
|
|
|
|
|
|
|
Message::ClearScene => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.push_undo_snapshot(i, "NEW");
|
|
|
|
|
|
self.tabs[i].scene.clear();
|
|
|
|
|
|
crate::linetypes::populate_document(&mut self.tabs[i].scene.document);
|
|
|
|
|
|
self.tabs[i].properties = PropertiesPanel::empty();
|
|
|
|
|
|
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
|
|
|
|
|
self.tabs[i].layers.sync_from_doc(&doc_layers);
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output("Scene cleared. Standard linetypes loaded.");
|
|
|
|
|
|
self.tabs[i].current_path = None;
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::SetWireframe(w) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].wireframe = w;
|
|
|
|
|
|
self.ribbon.set_wireframe(w);
|
|
|
|
|
|
self.tabs[i].visual_style = if w { "Wireframe".into() } else { "Shaded".into() };
|
|
|
|
|
|
self.command_line.push_output(if w { "Visual style: Wireframe" } else { "Visual style: Shaded" });
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::SetProjection(ortho) => {
|
|
|
|
|
|
use crate::scene::Projection;
|
|
|
|
|
|
let proj = if ortho { Projection::Orthographic } else { Projection::Perspective };
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].scene.camera.borrow_mut().projection = proj;
|
|
|
|
|
|
self.tabs[i].scene.camera_generation += 1;
|
|
|
|
|
|
self.ribbon.set_ortho(ortho);
|
|
|
|
|
|
self.command_line.push_output(if ortho { "Projection: Orthographic" } else { "Projection: Perspective" });
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::RibbonSelectTab(idx) => {
|
|
|
|
|
|
self.ribbon.select(idx);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::RibbonToolClick { tool_id, event } => {
|
|
|
|
|
|
self.ribbon.activate_tool(&tool_id);
|
|
|
|
|
|
match event {
|
|
|
|
|
|
ModuleEvent::Command(cmd) => return self.dispatch_command(&cmd),
|
|
|
|
|
|
ModuleEvent::OpenFileDialog => {
|
|
|
|
|
|
self.command_line.push_info("Open DWG/DXF: not yet implemented.");
|
|
|
|
|
|
}
|
|
|
|
|
|
ModuleEvent::ClearModels => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].scene.clear();
|
|
|
|
|
|
self.tabs[i].properties = PropertiesPanel::empty();
|
|
|
|
|
|
self.command_line.push_output("Scene cleared.");
|
|
|
|
|
|
}
|
|
|
|
|
|
ModuleEvent::SetWireframe(w) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].wireframe = w;
|
|
|
|
|
|
self.ribbon.set_wireframe(w);
|
|
|
|
|
|
self.tabs[i].visual_style = if w { "Wireframe".into() } else { "Shaded".into() };
|
|
|
|
|
|
self.command_line.push_output(if w { "Visual style: Wireframe" } else { "Visual style: Shaded" });
|
|
|
|
|
|
}
|
|
|
|
|
|
ModuleEvent::ToggleLayers => {
|
|
|
|
|
|
return Task::done(Message::ToggleLayers);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Application menu ──────────────────────────────────────────
|
|
|
|
|
|
Message::ToggleAppMenu => { self.app_menu.toggle(); Task::none() }
|
|
|
|
|
|
Message::CloseAppMenu => { self.app_menu.close(); Task::none() }
|
|
|
|
|
|
Message::CloseAppMenuAndRun(cmd) => {
|
|
|
|
|
|
self.app_menu.close();
|
|
|
|
|
|
self.dispatch_command(&cmd.clone())
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::AppMenuSearch(s) => { self.app_menu.search = s; Task::none() }
|
|
|
|
|
|
|
|
|
|
|
|
// ── Document tabs ─────────────────────────────────────────────
|
|
|
|
|
|
Message::TabNew => {
|
|
|
|
|
|
self.tab_counter += 1;
|
|
|
|
|
|
let new_tab = super::document::DocumentTab::new_drawing(self.tab_counter);
|
|
|
|
|
|
self.tabs.push(new_tab);
|
|
|
|
|
|
self.active_tab = self.tabs.len() - 1;
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::TabSwitch(idx) => {
|
|
|
|
|
|
if idx < self.tabs.len() {
|
|
|
|
|
|
self.active_tab = idx;
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::TabClose(idx) => {
|
|
|
|
|
|
if self.tabs.len() == 1 {
|
|
|
|
|
|
self.tab_counter += 1;
|
|
|
|
|
|
self.tabs[0] = super::document::DocumentTab::new_drawing(self.tab_counter);
|
|
|
|
|
|
self.active_tab = 0;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.tabs.remove(idx);
|
|
|
|
|
|
if self.active_tab >= self.tabs.len() {
|
|
|
|
|
|
self.active_tab = self.tabs.len() - 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::CommandInput(s) => { self.command_line.input = s; Task::none() }
|
|
|
|
|
|
|
|
|
|
|
|
Message::CommandSubmit => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if self.tabs[i].active_cmd.is_some() {
|
|
|
|
|
|
let text = self.command_line.input.trim().to_string();
|
|
|
|
|
|
self.command_line.input.clear();
|
|
|
|
|
|
|
|
|
|
|
|
if self.tabs[i]
|
|
|
|
|
|
.active_cmd
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(|c| c.wants_text_input())
|
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
|
{
|
|
|
|
|
|
if let Some(result) = self.tabs[i]
|
|
|
|
|
|
.active_cmd
|
|
|
|
|
|
.as_mut()
|
|
|
|
|
|
.and_then(|c| c.on_text_input(&text))
|
|
|
|
|
|
{
|
|
|
|
|
|
return self.apply_cmd_result(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
|
|
|
|
|
if let Some(p) = prompt {
|
|
|
|
|
|
self.command_line.push_info(&p);
|
|
|
|
|
|
}
|
|
|
|
|
|
let pt = self.tabs[i].last_cursor_world;
|
|
|
|
|
|
let previews = self.tabs[i]
|
|
|
|
|
|
.active_cmd
|
|
|
|
|
|
.as_mut()
|
|
|
|
|
|
.map(|c| c.on_preview_wires(pt))
|
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
self.tabs[i].scene.set_preview_wires(previews);
|
|
|
|
|
|
return self.focus_cmd_input();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if text.is_empty() {
|
|
|
|
|
|
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_enter());
|
|
|
|
|
|
if let Some(r) = result {
|
|
|
|
|
|
return self.apply_cmd_result(r);
|
|
|
|
|
|
}
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(pt) = parse_coord(&text) {
|
|
|
|
|
|
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt));
|
|
|
|
|
|
if let Some(r) = result {
|
|
|
|
|
|
return self.apply_cmd_result(r);
|
|
|
|
|
|
}
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(result) = self.tabs[i]
|
|
|
|
|
|
.active_cmd
|
|
|
|
|
|
.as_mut()
|
|
|
|
|
|
.and_then(|c| c.on_text_input(&text))
|
|
|
|
|
|
{
|
|
|
|
|
|
return self.apply_cmd_result(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.command_line.push_error(&format!(
|
|
|
|
|
|
"Expected coordinates (x,y) or a number, got: \"{text}\""
|
|
|
|
|
|
));
|
|
|
|
|
|
return self.focus_cmd_input();
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(cmd) = self.command_line.submit() {
|
|
|
|
|
|
return self.dispatch_command(&cmd);
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::CommandFinalize => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if self.tabs[i].active_cmd.is_some() {
|
|
|
|
|
|
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_enter());
|
|
|
|
|
|
if let Some(r) = result {
|
|
|
|
|
|
return self.apply_cmd_result(r);
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
} else if let Some(cmd) = self.tabs[i].last_cmd.clone() {
|
|
|
|
|
|
self.dispatch_command(&cmd)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::CommandEscape => {
|
2026-03-31 07:13:36 +03:00
|
|
|
|
// Cancel layout rename / context menu first, then fall through.
|
|
|
|
|
|
if self.layout_rename_state.take().is_some() || self.layout_context_menu.take().is_some() {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if self.tabs[i].active_cmd.is_some() {
|
|
|
|
|
|
let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_escape());
|
|
|
|
|
|
if let Some(r) = result {
|
|
|
|
|
|
return self.apply_cmd_result(r);
|
|
|
|
|
|
}
|
2026-03-31 12:52:07 +03:00
|
|
|
|
} else if self.tabs[i].scene.active_viewport.is_some() {
|
|
|
|
|
|
// ESC while in MSPACE → exit back to paper space.
|
|
|
|
|
|
return Task::done(Message::ExitViewport);
|
2026-03-22 22:44:32 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|
|
|
|
|
sel.box_anchor = None;
|
|
|
|
|
|
sel.box_current = None;
|
|
|
|
|
|
sel.box_crossing = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::Command(cmd) => self.dispatch_command(&cmd),
|
|
|
|
|
|
|
|
|
|
|
|
Message::ToggleLayers => {
|
|
|
|
|
|
if let Some(id) = self.layer_window.take() {
|
|
|
|
|
|
window::close(id)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
let (id, task) = window::open(window::Settings {
|
|
|
|
|
|
size: iced::Size::new(900.0, 360.0),
|
|
|
|
|
|
resizable: true,
|
|
|
|
|
|
..Default::default()
|
|
|
|
|
|
});
|
|
|
|
|
|
self.layer_window = Some(id);
|
|
|
|
|
|
task.map(|_| Message::Noop)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::OsWindowClosed(id) => {
|
|
|
|
|
|
if self.main_window == Some(id) {
|
|
|
|
|
|
return iced::exit();
|
|
|
|
|
|
}
|
|
|
|
|
|
if self.layer_window == Some(id) {
|
|
|
|
|
|
self.layer_window = None;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Layer panel messages ───────────────────────────────────────
|
|
|
|
|
|
Message::LayerToggleVisible(idx) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if idx < self.tabs[i].layers.layers.len() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYER OFF/ON");
|
|
|
|
|
|
let l = &mut self.tabs[i].layers.layers[idx];
|
|
|
|
|
|
l.visible = !l.visible;
|
|
|
|
|
|
let name = l.name.clone();
|
|
|
|
|
|
let on = l.visible;
|
|
|
|
|
|
self.tabs[i].scene.toggle_layer_visibility(&name);
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"Layer \"{}\" {}", name, if on { "on" } else { "off" }
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerToggleLock(idx) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if idx < self.tabs[i].layers.layers.len() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYER LOCK/UNLOCK");
|
|
|
|
|
|
let l = &mut self.tabs[i].layers.layers[idx];
|
|
|
|
|
|
l.locked = !l.locked;
|
|
|
|
|
|
let name = l.name.clone();
|
|
|
|
|
|
let locked = l.locked;
|
|
|
|
|
|
self.tabs[i].scene.toggle_layer_lock(&name);
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"Layer \"{}\" {}", name, if locked { "locked" } else { "unlocked" }
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerToggleFreeze(idx) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if idx < self.tabs[i].layers.layers.len() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYER FREEZE");
|
|
|
|
|
|
let l = &mut self.tabs[i].layers.layers[idx];
|
|
|
|
|
|
l.frozen = !l.frozen;
|
|
|
|
|
|
let name = l.name.clone();
|
|
|
|
|
|
let frozen = l.frozen;
|
|
|
|
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
|
|
|
|
|
if frozen { dl.freeze(); } else { dl.thaw(); }
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerNew => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let mut n = 1;
|
|
|
|
|
|
let new_name = loop {
|
|
|
|
|
|
let candidate = format!("Layer{}", n);
|
|
|
|
|
|
if !self.tabs[i].scene.document.layers.contains(&candidate) {
|
|
|
|
|
|
break candidate;
|
|
|
|
|
|
}
|
|
|
|
|
|
n += 1;
|
|
|
|
|
|
};
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYER NEW");
|
|
|
|
|
|
use acadrust::tables::layer::Layer as DocLayer;
|
|
|
|
|
|
let _ = self.tabs[i].scene.document.layers.add(DocLayer::new(&new_name));
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
|
|
|
|
|
self.tabs[i].layers.sync_from_doc(&doc_layers);
|
|
|
|
|
|
let new_idx = self.tabs[i].layers.layers.iter()
|
|
|
|
|
|
.position(|l| l.name == new_name);
|
|
|
|
|
|
if let Some(idx) = new_idx {
|
|
|
|
|
|
self.tabs[i].layers.selected = Some(idx);
|
|
|
|
|
|
self.tabs[i].layers.editing = Some(idx);
|
|
|
|
|
|
self.tabs[i].layers.edit_buf = new_name.clone();
|
|
|
|
|
|
}
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerDelete => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if let Some(idx) = self.tabs[i].layers.selected {
|
|
|
|
|
|
let name = self.tabs[i].layers.layers.get(idx)
|
|
|
|
|
|
.map(|l| l.name.clone())
|
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
if name == "0" { return Task::none(); }
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYER DELETE");
|
|
|
|
|
|
self.tabs[i].scene.document.layers.remove(&name);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
|
|
|
|
|
self.tabs[i].layers.sync_from_doc(&doc_layers);
|
|
|
|
|
|
self.tabs[i].layers.selected = None;
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerSetCurrent => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if let Some(idx) = self.tabs[i].layers.selected {
|
|
|
|
|
|
if let Some(layer) = self.tabs[i].layers.layers.get(idx) {
|
|
|
|
|
|
let name = layer.name.clone();
|
|
|
|
|
|
self.tabs[i].active_layer = name.clone();
|
|
|
|
|
|
self.tabs[i].layers.current_layer = name.clone();
|
|
|
|
|
|
self.ribbon.active_layer = name;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerSelect(idx) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if self.tabs[i].layers.editing.is_some() {
|
|
|
|
|
|
return Task::done(Message::LayerRenameCommit);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].layers.selected = Some(idx);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerRenameStart(idx) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].layers.selected = Some(idx);
|
|
|
|
|
|
if let Some(layer) = self.tabs[i].layers.layers.get(idx) {
|
|
|
|
|
|
self.tabs[i].layers.edit_buf = layer.name.clone();
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].layers.editing = Some(idx);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerRenameEdit(s) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].layers.edit_buf = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerRenameCommit => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let editing_idx = self.tabs[i].layers.editing.take();
|
|
|
|
|
|
if let Some(idx) = editing_idx {
|
|
|
|
|
|
let new_name = self.tabs[i].layers.edit_buf.trim().to_string();
|
|
|
|
|
|
let old_name = self.tabs[i].layers.layers.get(idx)
|
|
|
|
|
|
.map(|l| l.name.clone())
|
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
if !new_name.is_empty() && new_name != old_name
|
|
|
|
|
|
&& !self.tabs[i].scene.document.layers.contains(&new_name)
|
|
|
|
|
|
{
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYER RENAME");
|
|
|
|
|
|
if let Some(old_layer) = self.tabs[i].scene.document.layers.get(&old_name) {
|
|
|
|
|
|
use acadrust::tables::layer::Layer as DocLayer;
|
|
|
|
|
|
let mut nl = DocLayer::new(&new_name);
|
|
|
|
|
|
nl.color = old_layer.color.clone();
|
|
|
|
|
|
nl.flags = old_layer.flags.clone();
|
|
|
|
|
|
let _ = self.tabs[i].scene.document.layers.add(nl);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].scene.document.layers.remove(&old_name);
|
|
|
|
|
|
for e in self.tabs[i].scene.document.entities_mut() {
|
|
|
|
|
|
if e.as_entity().layer() == old_name {
|
|
|
|
|
|
e.as_entity_mut().set_layer(new_name.clone());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
|
|
|
|
|
self.tabs[i].layers.sync_from_doc(&doc_layers);
|
|
|
|
|
|
self.tabs[i].layers.edit_buf.clear();
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerColorPickerToggle(idx) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let panel = &mut self.tabs[i].layers;
|
|
|
|
|
|
if panel.color_picker_row == Some(idx) {
|
|
|
|
|
|
panel.color_picker_row = None;
|
|
|
|
|
|
panel.color_full_palette = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
panel.color_picker_row = Some(idx);
|
|
|
|
|
|
panel.color_full_palette = false;
|
|
|
|
|
|
panel.selected = Some(idx);
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerColorMorePalette => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].layers.color_full_palette = !self.tabs[i].layers.color_full_palette;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerColorSet(aci) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if let Some(idx) = self.tabs[i].layers.selected {
|
|
|
|
|
|
if let Some(layer) = self.tabs[i].layers.layers.get(idx) {
|
|
|
|
|
|
let name = layer.name.clone();
|
|
|
|
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
|
|
|
|
|
dl.color = AcadColor::Index(aci);
|
|
|
|
|
|
}
|
|
|
|
|
|
use crate::ui::layers::iced_color_from_acad;
|
|
|
|
|
|
let new_color = iced_color_from_acad(&AcadColor::Index(aci));
|
|
|
|
|
|
if let Some(pl) = self.tabs[i].layers.layers.get_mut(idx) {
|
|
|
|
|
|
pl.color = new_color;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].layers.color_picker_row = None;
|
|
|
|
|
|
self.tabs[i].layers.color_full_palette = false;
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerLinetypeSet(lt) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if let Some(idx) = self.tabs[i].layers.selected {
|
|
|
|
|
|
if let Some(layer) = self.tabs[i].layers.layers.get(idx) {
|
|
|
|
|
|
let name = layer.name.clone();
|
|
|
|
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
|
|
|
|
|
dl.line_type = lt.clone();
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(pl) = self.tabs[i].layers.layers.get_mut(idx) {
|
|
|
|
|
|
pl.linetype = lt;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerLineweightSet(lw) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if let Some(idx) = self.tabs[i].layers.selected {
|
|
|
|
|
|
if let Some(layer) = self.tabs[i].layers.layers.get(idx) {
|
|
|
|
|
|
let name = layer.name.clone();
|
|
|
|
|
|
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) {
|
|
|
|
|
|
dl.line_weight = lw;
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(pl) = self.tabs[i].layers.layers.get_mut(idx) {
|
|
|
|
|
|
pl.lineweight = lw;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayerTransparencyEdit(idx, s) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if let Some(pl) = self.tabs[i].layers.layers.get_mut(idx) {
|
|
|
|
|
|
if let Ok(v) = s.parse::<i32>() {
|
|
|
|
|
|
pl.transparency = v.clamp(0, 90);
|
|
|
|
|
|
} else if s.is_empty() {
|
|
|
|
|
|
pl.transparency = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Cursor / viewport messages ─────────────────────────────────
|
|
|
|
|
|
Message::CursorMoved(p) => {
|
|
|
|
|
|
let (vw, _vh) = self.tabs[self.active_tab].scene.selection.borrow().vp_size;
|
|
|
|
|
|
self.cursor_pos = iced::Point::new(
|
|
|
|
|
|
vw - VIEWCUBE_PAD - VIEWCUBE_HIT_SIZE + p.x,
|
|
|
|
|
|
VIEWCUBE_PAD + p.y,
|
|
|
|
|
|
);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewportMove(p) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|
|
|
|
|
sel.last_move_pos = Some(p);
|
|
|
|
|
|
|
|
|
|
|
|
if sel.left_down {
|
|
|
|
|
|
let press = sel.left_press_pos.unwrap_or(p);
|
|
|
|
|
|
let dx = p.x - press.x;
|
|
|
|
|
|
let dy = p.y - press.y;
|
|
|
|
|
|
let dist2 = dx * dx + dy * dy;
|
|
|
|
|
|
let elapsed_ms = sel
|
|
|
|
|
|
.left_press_time
|
|
|
|
|
|
.map(|t| Instant::now().duration_since(t).as_millis())
|
|
|
|
|
|
.unwrap_or(u128::MAX);
|
|
|
|
|
|
if !sel.left_dragging && elapsed_ms >= POLY_START_DELAY_MS && dist2 > 9.0 {
|
|
|
|
|
|
sel.left_dragging = true;
|
|
|
|
|
|
sel.poly_active = true;
|
|
|
|
|
|
sel.poly_crossing = p.x < press.x;
|
|
|
|
|
|
sel.poly_points.clear();
|
|
|
|
|
|
sel.poly_points.push(press);
|
|
|
|
|
|
sel.poly_points.push(p);
|
|
|
|
|
|
} else if sel.left_dragging && sel.poly_active {
|
|
|
|
|
|
if sel.poly_points.last().map_or(true, |lp| {
|
|
|
|
|
|
let ddx = p.x - lp.x;
|
|
|
|
|
|
let ddy = p.y - lp.y;
|
|
|
|
|
|
ddx * ddx + ddy * ddy > 16.0
|
|
|
|
|
|
}) {
|
|
|
|
|
|
sel.poly_points.push(p);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if sel.box_anchor.is_some() {
|
|
|
|
|
|
sel.box_current = Some(p);
|
|
|
|
|
|
if let Some(a) = sel.box_anchor {
|
|
|
|
|
|
sel.box_crossing = p.x < a.x;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if sel.right_down {
|
|
|
|
|
|
if let Some(press) = sel.right_press_pos {
|
|
|
|
|
|
let dx = p.x - press.x;
|
|
|
|
|
|
let dy = p.y - press.y;
|
|
|
|
|
|
if !sel.right_dragging && (dx * dx + dy * dy) > 9.0 {
|
|
|
|
|
|
sel.right_dragging = true;
|
|
|
|
|
|
sel.context_menu = None;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if sel.right_dragging {
|
|
|
|
|
|
if let Some(last) = sel.right_last_pos {
|
|
|
|
|
|
let (dx, dy) = (p.x - last.x, p.y - last.y);
|
|
|
|
|
|
self.tabs[i].scene.camera.borrow_mut().orbit(dx, dy);
|
|
|
|
|
|
}
|
|
|
|
|
|
sel.right_last_pos = Some(p);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 07:24:16 +03:00
|
|
|
|
let (mid_down, mid_last, vp_size) =
|
|
|
|
|
|
(sel.middle_down, sel.middle_last_pos, sel.vp_size);
|
|
|
|
|
|
if mid_down {
|
|
|
|
|
|
if let Some(last) = mid_last {
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let (dx, dy) = (p.x - last.x, p.y - last.y);
|
2026-03-31 07:24:16 +03:00
|
|
|
|
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vp_size.0, height: vp_size.1 };
|
|
|
|
|
|
// Drop `sel` before calling mutable scene methods.
|
|
|
|
|
|
drop(sel);
|
|
|
|
|
|
if self.tabs[i].scene.active_viewport.is_some() {
|
|
|
|
|
|
self.tabs[i].scene.pan_active_viewport(dx, dy, bounds);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.tabs[i].scene.camera.borrow_mut().pan(dx, dy);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p);
|
|
|
|
|
|
return Task::none();
|
2026-03-22 22:44:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
sel.middle_last_pos = Some(p);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let vp_size = sel.vp_size;
|
|
|
|
|
|
drop(sel);
|
|
|
|
|
|
|
|
|
|
|
|
// ── Grip drag ─────────────────────────────────────────────
|
|
|
|
|
|
if let Some(grip) = self.tabs[i].active_grip.clone() {
|
|
|
|
|
|
let (vw, vh) = vp_size;
|
|
|
|
|
|
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
|
|
|
|
|
let cam = self.tabs[i].scene.camera.borrow();
|
2026-03-31 12:52:07 +03:00
|
|
|
|
let raw_paper = cam.pick_on_target_plane(p, bounds);
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let vp_mat = cam.view_proj(bounds);
|
|
|
|
|
|
drop(cam);
|
2026-03-31 12:52:07 +03:00
|
|
|
|
let raw = self.tabs[i].scene.paper_to_model(raw_paper);
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
|
|
|
|
|
let edited_name = grip.handle.value().to_string();
|
2026-03-31 13:07:25 +03:00
|
|
|
|
let all_wires = self.tabs[i].scene.hit_test_wires();
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let snap_wires: Vec<_> = all_wires
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.filter(|w| w.name != edited_name)
|
|
|
|
|
|
.cloned()
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let snap_hit = self.snapper.snap(raw, p, &snap_wires, vp_mat, bounds);
|
|
|
|
|
|
let mut snapped = snap_hit.map(|s| s.world).unwrap_or(raw);
|
|
|
|
|
|
self.tabs[i].snap_result = snap_hit;
|
|
|
|
|
|
|
|
|
|
|
|
if snap_hit.is_none() {
|
|
|
|
|
|
let base = grip.origin_world;
|
|
|
|
|
|
if self.ortho_mode {
|
|
|
|
|
|
snapped = ortho_constrain(snapped, base);
|
|
|
|
|
|
} else if self.polar_mode {
|
|
|
|
|
|
snapped = polar_constrain(snapped, base, 45.0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let apply = if grip.is_translate {
|
|
|
|
|
|
GripApply::Translate(snapped - grip.last_world)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
GripApply::Absolute(snapped)
|
|
|
|
|
|
};
|
|
|
|
|
|
self.tabs[i].scene.apply_grip(grip.handle, grip.grip_id, apply);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.tabs[i].active_grip.as_mut().unwrap().last_world = snapped;
|
|
|
|
|
|
self.refresh_selected_grips();
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if self.tabs[i].active_cmd.is_some() {
|
|
|
|
|
|
let (vw, vh) = vp_size;
|
|
|
|
|
|
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
|
|
|
|
|
let cam = self.tabs[i].scene.camera.borrow();
|
2026-03-31 12:52:07 +03:00
|
|
|
|
let cursor_paper = cam.pick_on_target_plane(p, bounds);
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let view_proj = cam.view_proj(bounds);
|
|
|
|
|
|
drop(cam);
|
2026-03-31 12:52:07 +03:00
|
|
|
|
// In MSPACE, map paper-space cursor to model space so that
|
|
|
|
|
|
// command previews and snapping work in the correct coordinate space.
|
|
|
|
|
|
let cursor_world = self.tabs[i].scene.paper_to_model(cursor_paper);
|
2026-03-22 22:44:32 +03:00
|
|
|
|
|
2026-03-31 13:07:25 +03:00
|
|
|
|
let all_wires = self.tabs[i].scene.hit_test_wires();
|
2026-03-31 23:05:24 +03:00
|
|
|
|
let needs_entity = self.tabs[i]
|
|
|
|
|
|
.active_cmd.as_ref().map(|c| c.needs_entity_pick()).unwrap_or(false);
|
|
|
|
|
|
let is_gathering = self.tabs[i]
|
|
|
|
|
|
.active_cmd.as_ref().map(|c| c.is_selection_gathering()).unwrap_or(false);
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let needs_tan = self.tabs[i]
|
|
|
|
|
|
.active_cmd.as_ref().map(|c| c.needs_tangent_pick()).unwrap_or(false);
|
2026-03-31 23:05:24 +03:00
|
|
|
|
self.tabs[i].snap_result = if needs_entity || is_gathering {
|
|
|
|
|
|
None
|
|
|
|
|
|
} else if needs_tan {
|
2026-03-22 22:44:32 +03:00
|
|
|
|
self.snapper.snap_tangent_only(cursor_world, p, &all_wires, view_proj, bounds)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.snapper.snap(cursor_world, p, &all_wires, view_proj, bounds)
|
|
|
|
|
|
};
|
|
|
|
|
|
let effective = {
|
2026-03-31 13:07:25 +03:00
|
|
|
|
// snap.world is paper-space for viewport-projected wires; convert
|
|
|
|
|
|
// to model-space so previews use consistent coordinates.
|
|
|
|
|
|
let mut pt = self.tabs[i].snap_result
|
|
|
|
|
|
.map(|s| self.tabs[i].scene.paper_to_model(s.world))
|
|
|
|
|
|
.unwrap_or(cursor_world);
|
2026-03-22 22:44:32 +03:00
|
|
|
|
if self.tabs[i].active_cmd.is_some() { pt.z = 0.0; }
|
|
|
|
|
|
if let Some(base) = self.last_point {
|
|
|
|
|
|
if self.ortho_mode {
|
|
|
|
|
|
pt = ortho_constrain(pt, base);
|
|
|
|
|
|
} else if self.polar_mode {
|
|
|
|
|
|
pt = polar_constrain(pt, base, 45.0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
pt
|
|
|
|
|
|
};
|
|
|
|
|
|
self.tabs[i].last_cursor_world = effective;
|
|
|
|
|
|
|
|
|
|
|
|
let previews = if needs_entity {
|
|
|
|
|
|
let hover_handle =
|
|
|
|
|
|
scene::hit_test::click_hit(p, &all_wires, view_proj, bounds)
|
|
|
|
|
|
.and_then(|s| Scene::handle_from_wire_name(s))
|
|
|
|
|
|
.unwrap_or(acadrust::Handle::NULL);
|
|
|
|
|
|
self.tabs[i].active_cmd.as_mut()
|
|
|
|
|
|
.map(|c| c.on_hover_entity(hover_handle, effective))
|
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.tabs[i].active_cmd.as_mut()
|
|
|
|
|
|
.map(|c| c.on_preview_wires(effective))
|
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
|
};
|
|
|
|
|
|
self.tabs[i].scene.set_preview_wires(previews);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.tabs[i].snap_result = None;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewportExit => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|
|
|
|
|
sel.left_down = false;
|
|
|
|
|
|
sel.left_press_pos = None;
|
|
|
|
|
|
sel.left_press_time = None;
|
|
|
|
|
|
sel.left_dragging = false;
|
|
|
|
|
|
sel.right_down = false;
|
|
|
|
|
|
sel.right_press_pos = None;
|
|
|
|
|
|
sel.right_last_pos = None;
|
|
|
|
|
|
sel.right_dragging = false;
|
|
|
|
|
|
sel.middle_down = false;
|
|
|
|
|
|
sel.middle_last_pos = None;
|
|
|
|
|
|
sel.box_anchor = None;
|
|
|
|
|
|
sel.box_current = None;
|
|
|
|
|
|
sel.box_crossing = false;
|
|
|
|
|
|
sel.poly_active = false;
|
|
|
|
|
|
sel.poly_points.clear();
|
|
|
|
|
|
sel.poly_crossing = false;
|
|
|
|
|
|
sel.context_menu = None;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewportLeftPress => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let (p, vp_size) = {
|
|
|
|
|
|
let sel = self.tabs[i].scene.selection.borrow();
|
|
|
|
|
|
let p = match sel.last_move_pos {
|
|
|
|
|
|
Some(p) => p,
|
|
|
|
|
|
None => return Task::none(),
|
|
|
|
|
|
};
|
|
|
|
|
|
(p, sel.vp_size)
|
|
|
|
|
|
};
|
|
|
|
|
|
let (vw, vh) = vp_size;
|
|
|
|
|
|
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
|
|
|
|
|
|
|
|
|
|
|
if vw > 1.0 && vh > 1.0 {
|
|
|
|
|
|
let cam = self.tabs[i].scene.camera.borrow();
|
|
|
|
|
|
if scene::hit_test(p.x, p.y, vw, vh, cam.view_rotation_mat(), VIEWCUBE_PX).is_some() {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if self.tabs[i].active_cmd.is_none() && !self.tabs[i].selected_grips.is_empty() {
|
|
|
|
|
|
if let Some(handle) = self.tabs[i].selected_handle {
|
|
|
|
|
|
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
|
|
|
|
|
let grip_hit = find_hit_grip(p, &self.tabs[i].selected_grips, vp_mat, bounds);
|
|
|
|
|
|
if let Some((grip_id, is_translate, world)) = grip_hit {
|
|
|
|
|
|
self.tabs[i].active_grip = Some(GripEdit {
|
|
|
|
|
|
handle,
|
|
|
|
|
|
grip_id,
|
|
|
|
|
|
is_translate,
|
|
|
|
|
|
origin_world: world,
|
|
|
|
|
|
last_world: world,
|
|
|
|
|
|
});
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|
|
|
|
|
sel.context_menu = None;
|
|
|
|
|
|
sel.left_down = true;
|
|
|
|
|
|
sel.left_press_pos = Some(p);
|
|
|
|
|
|
sel.left_press_time = Some(Instant::now());
|
|
|
|
|
|
sel.left_dragging = false;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewportLeftRelease => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let (p, is_click, is_down) = {
|
|
|
|
|
|
let sel = self.tabs[i].scene.selection.borrow();
|
|
|
|
|
|
let p = match sel.last_move_pos {
|
|
|
|
|
|
Some(p) => p,
|
|
|
|
|
|
None => return Task::none(),
|
|
|
|
|
|
};
|
|
|
|
|
|
(p, !sel.left_dragging, sel.left_down)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if self.tabs[i].active_grip.is_some() {
|
|
|
|
|
|
self.tabs[i].active_grip = None;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let is_gathering = self.tabs[i]
|
|
|
|
|
|
.active_cmd.as_ref().map(|c| c.is_selection_gathering()).unwrap_or(false);
|
|
|
|
|
|
|
|
|
|
|
|
if is_down && is_click && self.tabs[i].active_cmd.is_some() && !is_gathering {
|
|
|
|
|
|
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
|
|
|
|
|
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
|
|
|
|
|
|
|
|
|
|
|
let snap_taken = self.tabs[i].snap_result.take();
|
|
|
|
|
|
let tangent_obj_at_click = snap_taken.and_then(|s| s.tangent_obj);
|
|
|
|
|
|
|
|
|
|
|
|
let world_pt = {
|
2026-03-31 07:24:16 +03:00
|
|
|
|
let raw_paper = self.tabs[i].scene.camera.borrow().pick_on_target_plane(p, bounds);
|
|
|
|
|
|
// Convert paper-space → model-space when inside a viewport.
|
|
|
|
|
|
let raw = self.tabs[i].scene.paper_to_model(raw_paper);
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
2026-03-31 13:07:25 +03:00
|
|
|
|
let all_wires = self.tabs[i].scene.hit_test_wires();
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let needs_tan = self.tabs[i].active_cmd.as_ref()
|
|
|
|
|
|
.map(|c| c.needs_tangent_pick()).unwrap_or(false);
|
2026-03-31 23:05:24 +03:00
|
|
|
|
let needs_entity_click = self.tabs[i].active_cmd.as_ref()
|
|
|
|
|
|
.map(|c| c.needs_entity_pick()).unwrap_or(false);
|
|
|
|
|
|
let snap_hit = if needs_entity_click {
|
|
|
|
|
|
None
|
|
|
|
|
|
} else if needs_tan {
|
2026-03-22 22:44:32 +03:00
|
|
|
|
self.snapper.snap_tangent_only(raw, p, &all_wires, vp_mat, bounds)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.snapper.snap(raw, p, &all_wires, vp_mat, bounds)
|
|
|
|
|
|
};
|
2026-03-31 13:07:25 +03:00
|
|
|
|
// snap.world is in paper-space (projected wire coords in MSPACE);
|
|
|
|
|
|
// convert to model-space so commands receive consistent coordinates.
|
|
|
|
|
|
let mut pt = snap_hit
|
|
|
|
|
|
.map(|s| self.tabs[i].scene.paper_to_model(s.world))
|
|
|
|
|
|
.unwrap_or(raw);
|
2026-03-22 22:44:32 +03:00
|
|
|
|
pt.z = 0.0;
|
|
|
|
|
|
if let Some(base) = self.last_point {
|
|
|
|
|
|
if self.ortho_mode {
|
|
|
|
|
|
pt = ortho_constrain(pt, base);
|
|
|
|
|
|
} else if self.polar_mode {
|
|
|
|
|
|
pt = polar_constrain(pt, base, 45.0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
pt
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let result = if self.tabs[i].active_cmd.as_ref()
|
|
|
|
|
|
.map(|c| c.needs_entity_pick()).unwrap_or(false)
|
|
|
|
|
|
{
|
|
|
|
|
|
let vp_mat2 = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
2026-03-31 13:07:25 +03:00
|
|
|
|
let all_wires2 = self.tabs[i].scene.hit_test_wires();
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let hit = scene::hit_test::click_hit(p, &all_wires2, vp_mat2, bounds)
|
|
|
|
|
|
.and_then(|s| Scene::handle_from_wire_name(s));
|
|
|
|
|
|
if let Some(handle) = hit {
|
|
|
|
|
|
self.tabs[i].active_cmd.as_mut().map(|c| c.on_entity_pick(handle, world_pt))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_info("Nothing found at that point.");
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if self.tabs[i].active_cmd.as_ref()
|
|
|
|
|
|
.map(|c| c.needs_tangent_pick()).unwrap_or(false)
|
|
|
|
|
|
{
|
|
|
|
|
|
if let Some(obj) = tangent_obj_at_click {
|
|
|
|
|
|
self.tabs[i].active_cmd.as_mut().map(|c| c.on_tangent_point(obj, world_pt))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.command_line.push_info("Select a tangent object.");
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.last_point = Some(world_pt);
|
|
|
|
|
|
self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(world_pt))
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(r) = result {
|
|
|
|
|
|
let task = self.apply_cmd_result(r);
|
|
|
|
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|
|
|
|
|
sel.left_down = false;
|
|
|
|
|
|
sel.left_press_pos = None;
|
|
|
|
|
|
sel.left_press_time = None;
|
|
|
|
|
|
sel.left_dragging = false;
|
|
|
|
|
|
return task;
|
|
|
|
|
|
}
|
|
|
|
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|
|
|
|
|
sel.left_down = false;
|
|
|
|
|
|
sel.left_press_pos = None;
|
|
|
|
|
|
sel.left_press_time = None;
|
|
|
|
|
|
sel.left_dragging = false;
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let (is_down2, is_dragging, box_anchor, box_crossing, vp_size, elapsed_ms) = {
|
|
|
|
|
|
let sel = self.tabs[i].scene.selection.borrow();
|
|
|
|
|
|
let elapsed = sel.left_press_time
|
|
|
|
|
|
.map(|t| Instant::now().duration_since(t).as_millis())
|
|
|
|
|
|
.unwrap_or(u128::MAX);
|
|
|
|
|
|
(sel.left_down, sel.left_dragging, sel.box_anchor, sel.box_crossing, sel.vp_size, elapsed)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let mut selection_just_completed = false;
|
|
|
|
|
|
|
|
|
|
|
|
if is_down2 {
|
|
|
|
|
|
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vp_size.0, height: vp_size.1 };
|
|
|
|
|
|
|
|
|
|
|
|
if is_dragging {
|
|
|
|
|
|
if elapsed_ms < POLY_START_DELAY_MS {
|
|
|
|
|
|
if let Some(a) = box_anchor {
|
|
|
|
|
|
let crossing = box_crossing;
|
2026-03-31 13:07:25 +03:00
|
|
|
|
let all_wires = self.tabs[i].scene.hit_test_wires();
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
|
|
|
|
|
let mut handles: Vec<Handle> = scene::hit_test::box_hit(
|
|
|
|
|
|
a, p, crossing, &all_wires, vp_mat, bounds,
|
|
|
|
|
|
).into_iter().filter_map(|s| Scene::handle_from_wire_name(s)).collect();
|
|
|
|
|
|
handles.extend(scene::hit_test::box_hit_hatch(
|
|
|
|
|
|
a, p, crossing, &self.tabs[i].scene.hatches, vp_mat, bounds,
|
|
|
|
|
|
));
|
|
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
|
|
for h in &handles { self.tabs[i].scene.select_entity(*h, false); }
|
|
|
|
|
|
self.tabs[i].scene.expand_selection_for_groups(&handles);
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
selection_just_completed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let (poly_pts, crossing) = {
|
|
|
|
|
|
let sel = self.tabs[i].scene.selection.borrow();
|
|
|
|
|
|
(sel.poly_points.clone(), sel.poly_crossing)
|
|
|
|
|
|
};
|
|
|
|
|
|
self.tabs[i].scene.selection.borrow_mut().poly_last_crossing = crossing;
|
2026-03-31 13:07:25 +03:00
|
|
|
|
let all_wires = self.tabs[i].scene.hit_test_wires();
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
|
|
|
|
|
let mut handles: Vec<Handle> = scene::hit_test::poly_hit(
|
|
|
|
|
|
&poly_pts, crossing, &all_wires, vp_mat, bounds,
|
|
|
|
|
|
).into_iter().filter_map(|s| Scene::handle_from_wire_name(s)).collect();
|
|
|
|
|
|
handles.extend(scene::hit_test::poly_hit_hatch(
|
|
|
|
|
|
&poly_pts, crossing, &self.tabs[i].scene.hatches, vp_mat, bounds,
|
|
|
|
|
|
));
|
|
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
|
|
for h in &handles { self.tabs[i].scene.select_entity(*h, false); }
|
|
|
|
|
|
self.tabs[i].scene.expand_selection_for_groups(&handles);
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
selection_just_completed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|
|
|
|
|
sel.poly_active = false;
|
|
|
|
|
|
sel.poly_points.clear();
|
|
|
|
|
|
sel.poly_crossing = false;
|
|
|
|
|
|
sel.box_anchor = None;
|
|
|
|
|
|
sel.box_current = None;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if box_anchor.is_none() {
|
2026-03-31 13:07:25 +03:00
|
|
|
|
let all_wires = self.tabs[i].scene.hit_test_wires();
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
|
|
|
|
|
let hit = scene::hit_test::click_hit(p, &all_wires, vp_mat, bounds)
|
|
|
|
|
|
.and_then(|s| Scene::handle_from_wire_name(s))
|
|
|
|
|
|
.or_else(|| scene::hit_test::click_hit_hatch(
|
|
|
|
|
|
p, &self.tabs[i].scene.hatches, vp_mat, bounds,
|
|
|
|
|
|
));
|
|
|
|
|
|
if let Some(handle) = hit {
|
|
|
|
|
|
self.tabs[i].scene.select_entity(handle, true);
|
|
|
|
|
|
self.tabs[i].scene.expand_selection_for_groups(&[handle]);
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
selection_just_completed = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|
|
|
|
|
sel.box_anchor = Some(p);
|
|
|
|
|
|
sel.box_current = Some(p);
|
|
|
|
|
|
sel.box_crossing = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let a = box_anchor.unwrap();
|
|
|
|
|
|
let crossing = box_crossing;
|
2026-03-31 13:07:25 +03:00
|
|
|
|
let all_wires = self.tabs[i].scene.hit_test_wires();
|
2026-03-22 22:44:32 +03:00
|
|
|
|
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
|
|
|
|
|
let mut handles: Vec<Handle> = scene::hit_test::box_hit(
|
|
|
|
|
|
a, p, crossing, &all_wires, vp_mat, bounds,
|
|
|
|
|
|
).into_iter().filter_map(|s| Scene::handle_from_wire_name(s)).collect();
|
|
|
|
|
|
handles.extend(scene::hit_test::box_hit_hatch(
|
|
|
|
|
|
a, p, crossing, &self.tabs[i].scene.hatches, vp_mat, bounds,
|
|
|
|
|
|
));
|
|
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
|
|
for h in &handles { self.tabs[i].scene.select_entity(*h, false); }
|
|
|
|
|
|
self.tabs[i].scene.expand_selection_for_groups(&handles);
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|
|
|
|
|
sel.box_last = Some((a, p));
|
|
|
|
|
|
sel.box_last_crossing = crossing;
|
|
|
|
|
|
sel.box_anchor = None;
|
|
|
|
|
|
sel.box_current = None;
|
|
|
|
|
|
sel.box_crossing = false;
|
|
|
|
|
|
selection_just_completed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|
|
|
|
|
sel.left_down = false;
|
|
|
|
|
|
sel.left_press_pos = None;
|
|
|
|
|
|
sel.left_press_time = None;
|
|
|
|
|
|
sel.left_dragging = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if is_gathering && selection_just_completed {
|
|
|
|
|
|
let handles: Vec<Handle> = self.tabs[i]
|
|
|
|
|
|
.scene.selected_entities().into_iter().map(|(h, _)| h).collect();
|
|
|
|
|
|
if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
|
|
|
|
|
|
let result = cmd.on_selection_complete(handles);
|
|
|
|
|
|
return self.apply_cmd_result(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 07:24:16 +03:00
|
|
|
|
// ── Double-click: enter/exit MSPACE ───────────────────────
|
|
|
|
|
|
// Only when no command is running, no drag, and we're in paper space.
|
|
|
|
|
|
if is_click
|
2026-03-31 12:52:07 +03:00
|
|
|
|
&& is_down // ensures there was a matching left-press
|
2026-03-31 07:24:16 +03:00
|
|
|
|
&& self.tabs[i].active_cmd.is_none()
|
|
|
|
|
|
&& self.tabs[i].scene.current_layout != "Model"
|
|
|
|
|
|
{
|
|
|
|
|
|
let now = Instant::now();
|
|
|
|
|
|
let is_double = self
|
|
|
|
|
|
.last_vp_click_time
|
|
|
|
|
|
.map(|t| {
|
|
|
|
|
|
let dt = now.duration_since(t).as_millis();
|
|
|
|
|
|
let last = self.last_vp_click_pos.unwrap_or(p);
|
|
|
|
|
|
let d = (p.x - last.x).hypot(p.y - last.y);
|
|
|
|
|
|
dt < 400 && d < 8.0
|
|
|
|
|
|
})
|
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
|
|
|
|
|
|
self.last_vp_click_time = Some(now);
|
|
|
|
|
|
self.last_vp_click_pos = Some(p);
|
|
|
|
|
|
|
|
|
|
|
|
if is_double {
|
|
|
|
|
|
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
|
|
|
|
|
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
|
|
|
|
|
|
2026-03-31 12:52:07 +03:00
|
|
|
|
// 1) Try direct wire hit — works when the border is clicked.
|
|
|
|
|
|
let hit_vp: Option<acadrust::Handle> = {
|
|
|
|
|
|
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
2026-03-31 13:07:25 +03:00
|
|
|
|
let all_wires = self.tabs[i].scene.hit_test_wires();
|
2026-03-31 12:52:07 +03:00
|
|
|
|
scene::hit_test::click_hit(p, &all_wires, vp_mat, bounds)
|
|
|
|
|
|
.and_then(|s| Scene::handle_from_wire_name(s))
|
|
|
|
|
|
.and_then(|h| {
|
|
|
|
|
|
if let Some(AcadEntityType::Viewport(vp)) =
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity(h)
|
|
|
|
|
|
{
|
|
|
|
|
|
if vp.id > 1 { Some(h) } else { None }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 2) Geometric fallback: check if the cursor is inside any
|
|
|
|
|
|
// viewport's bounding rectangle in paper space. This handles
|
|
|
|
|
|
// double-clicks on model-entity content wires or empty areas.
|
|
|
|
|
|
let hit_vp = hit_vp.or_else(|| {
|
|
|
|
|
|
let paper_pt = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.camera
|
|
|
|
|
|
.borrow()
|
|
|
|
|
|
.pick_on_target_plane(p, bounds);
|
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.viewport_at_paper_point(paper_pt.x, paper_pt.y)
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(handle) = hit_vp {
|
|
|
|
|
|
return Task::done(Message::EnterViewport(handle));
|
2026-03-31 07:24:16 +03:00
|
|
|
|
} else if self.tabs[i].scene.active_viewport.is_some() {
|
2026-03-31 12:52:07 +03:00
|
|
|
|
// Double-clicked outside all viewports while in MSPACE → exit.
|
2026-03-31 07:24:16 +03:00
|
|
|
|
return Task::done(Message::ExitViewport);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewportRightPress => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|
|
|
|
|
let Some(p) = sel.last_move_pos else { return Task::none(); };
|
|
|
|
|
|
sel.context_menu = None;
|
|
|
|
|
|
sel.right_down = true;
|
|
|
|
|
|
sel.right_press_pos = Some(p);
|
|
|
|
|
|
sel.right_last_pos = Some(p);
|
|
|
|
|
|
sel.right_dragging = false;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewportRightRelease => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|
|
|
|
|
let Some(_p) = sel.last_move_pos else { return Task::none(); };
|
|
|
|
|
|
if sel.right_down {
|
|
|
|
|
|
if !sel.right_dragging {
|
|
|
|
|
|
sel.context_menu = sel.last_move_pos;
|
|
|
|
|
|
}
|
|
|
|
|
|
sel.right_down = false;
|
|
|
|
|
|
sel.right_press_pos = None;
|
|
|
|
|
|
sel.right_last_pos = None;
|
|
|
|
|
|
sel.right_dragging = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewportMiddlePress => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let now = Instant::now();
|
|
|
|
|
|
let is_double = {
|
|
|
|
|
|
let sel = self.tabs[i].scene.selection.borrow();
|
|
|
|
|
|
sel.middle_last_press_time
|
|
|
|
|
|
.map(|t| now.duration_since(t).as_millis() < 300)
|
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
|
};
|
|
|
|
|
|
{
|
|
|
|
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|
|
|
|
|
let Some(p) = sel.last_move_pos else { return Task::none(); };
|
|
|
|
|
|
sel.middle_down = true;
|
|
|
|
|
|
sel.middle_last_pos = Some(p);
|
|
|
|
|
|
sel.middle_last_press_time = Some(now);
|
|
|
|
|
|
}
|
|
|
|
|
|
if is_double {
|
|
|
|
|
|
self.tabs[i].scene.fit_all();
|
|
|
|
|
|
self.command_line.push_output("Zoom Extents");
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewportMiddleRelease => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
|
|
|
|
|
sel.middle_down = false;
|
|
|
|
|
|
sel.middle_last_pos = None;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewportScroll(delta) => {
|
|
|
|
|
|
let s = match delta {
|
|
|
|
|
|
mouse::ScrollDelta::Lines { y, .. } => y,
|
|
|
|
|
|
mouse::ScrollDelta::Pixels { y, .. } => y * 0.01,
|
|
|
|
|
|
};
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let cursor = self.tabs[i].scene.selection.borrow().last_move_pos;
|
|
|
|
|
|
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
|
|
|
|
|
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
2026-03-31 07:24:16 +03:00
|
|
|
|
if self.tabs[i].scene.active_viewport.is_some() {
|
2026-03-31 12:52:07 +03:00
|
|
|
|
// In MSPACE: zoom the active viewport's model-space view,
|
|
|
|
|
|
// keeping the model point under the cursor stationary.
|
|
|
|
|
|
let cursor_paper = cursor.map(|cp| {
|
|
|
|
|
|
let pt = self.tabs[i].scene.camera.borrow()
|
|
|
|
|
|
.pick_on_target_plane(cp, bounds);
|
|
|
|
|
|
glam::Vec2::new(pt.x, pt.y)
|
|
|
|
|
|
});
|
|
|
|
|
|
self.tabs[i].scene.zoom_active_viewport(s, cursor_paper);
|
2026-03-22 22:44:32 +03:00
|
|
|
|
} else {
|
2026-03-31 07:24:16 +03:00
|
|
|
|
let mut cam = self.tabs[i].scene.camera.borrow_mut();
|
|
|
|
|
|
if let Some(cursor) = cursor {
|
|
|
|
|
|
cam.zoom_about_point(cursor, bounds, s);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
cam.zoom(s);
|
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewportClick => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let cam = self.tabs[i].scene.camera.borrow();
|
|
|
|
|
|
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
|
|
|
|
|
if let Some(region) = scene::hit_test(
|
|
|
|
|
|
self.cursor_pos.x, self.cursor_pos.y, vw, vh, cam.view_rotation_mat(), VIEWCUBE_PX,
|
|
|
|
|
|
) {
|
|
|
|
|
|
return Task::done(Message::ViewCubeSnap(region));
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::WindowResized(w, h) => {
|
|
|
|
|
|
self.vp_size = ((w - 440.0).max(200.0), h);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ViewCubeSnap(region) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let mut region = region;
|
|
|
|
|
|
{
|
|
|
|
|
|
let mut cam = self.tabs[i].scene.camera.borrow_mut();
|
|
|
|
|
|
let (target_yaw, target_pitch) = region.snap_angles();
|
|
|
|
|
|
if angle_close(cam.yaw, target_yaw, 0.01)
|
|
|
|
|
|
&& angle_close(cam.pitch, target_pitch, 0.01)
|
|
|
|
|
|
{
|
|
|
|
|
|
region = region.opposite();
|
|
|
|
|
|
}
|
|
|
|
|
|
let (yaw, pitch) = region.snap_angles();
|
|
|
|
|
|
cam.snap_to_angles(yaw, pitch);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].scene.camera_generation += 1;
|
|
|
|
|
|
self.command_line.push_output(&format!("View: {}", region.label()));
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Snap / mode toggles ───────────────────────────────────────
|
|
|
|
|
|
Message::ToggleSnapEnabled => { self.snapper.toggle_global(); Task::none() }
|
|
|
|
|
|
Message::ToggleGridSnap => { self.snapper.toggle(crate::snap::SnapType::Grid); Task::none() }
|
|
|
|
|
|
Message::ToggleGrid => { self.show_grid ^= true; Task::none() }
|
|
|
|
|
|
Message::ToggleOrtho => {
|
|
|
|
|
|
self.ortho_mode ^= true;
|
|
|
|
|
|
if self.ortho_mode { self.polar_mode = false; }
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::TogglePolar => {
|
|
|
|
|
|
self.polar_mode ^= true;
|
|
|
|
|
|
if self.polar_mode { self.ortho_mode = false; }
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::ToggleSnap(t) => { self.snapper.toggle(t); Task::none() }
|
|
|
|
|
|
Message::ToggleSnapPopup => { self.snap_popup_open ^= true; Task::none() }
|
|
|
|
|
|
Message::CloseSnapPopup => { self.snap_popup_open = false; Task::none() }
|
|
|
|
|
|
Message::SnapSelectAll => { self.snapper.enable_all(); Task::none() }
|
|
|
|
|
|
Message::SnapClearAll => { self.snapper.disable_all(); Task::none() }
|
|
|
|
|
|
|
|
|
|
|
|
// ── Ribbon dropdowns ──────────────────────────────────────────
|
|
|
|
|
|
Message::ToggleRibbonDropdown(id) => { self.ribbon.toggle_dropdown(&id); Task::none() }
|
|
|
|
|
|
Message::CloseRibbonDropdown => { self.ribbon.close_dropdown(); Task::none() }
|
|
|
|
|
|
Message::DropdownSelectItem { dropdown_id, cmd } => {
|
|
|
|
|
|
self.ribbon.select_dropdown_item(dropdown_id, cmd);
|
|
|
|
|
|
self.ribbon.activate_tool(cmd);
|
|
|
|
|
|
self.dispatch_command(cmd)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::DeleteSelected => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i].scene.selected.iter().cloned().collect();
|
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "ERASE");
|
|
|
|
|
|
self.tabs[i].scene.erase_entities(&handles);
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Properties panel messages ─────────────────────────────────
|
|
|
|
|
|
Message::PropSelectionGroupChanged(group) => {
|
|
|
|
|
|
self.tabs[self.active_tab].properties.selected_group = Some(group);
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::RibbonLayerChanged(layer) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].active_layer = layer.clone();
|
|
|
|
|
|
self.tabs[i].layers.current_layer = layer.clone();
|
|
|
|
|
|
self.ribbon.active_layer = layer;
|
|
|
|
|
|
self.ribbon.close_dropdown();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::RibbonColorChanged(color) => {
|
|
|
|
|
|
self.ribbon.active_color = color;
|
|
|
|
|
|
self.ribbon.prop_color_palette_open = false;
|
|
|
|
|
|
self.ribbon.close_dropdown();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::RibbonColorPaletteToggle => {
|
|
|
|
|
|
self.ribbon.prop_color_palette_open ^= true;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::RibbonLinetypeChanged(lt) => {
|
|
|
|
|
|
self.ribbon.active_linetype = lt;
|
|
|
|
|
|
self.ribbon.close_dropdown();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::RibbonLineweightChanged(lw) => {
|
|
|
|
|
|
self.ribbon.active_lineweight = lw;
|
|
|
|
|
|
self.ribbon.close_dropdown();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropLayerChanged(layer) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|
|
|
|
|
for handle in handles {
|
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
|
|
|
|
|
crate::scene::dispatch::apply_common_prop(entity, "layer", &layer);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropColorChanged(color) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|
|
|
|
|
for handle in handles {
|
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
|
|
|
|
|
crate::scene::dispatch::apply_color(entity, color);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].properties.color_picker_open = false;
|
|
|
|
|
|
self.tabs[i].properties.color_palette_open = false;
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropLwChanged(lw) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|
|
|
|
|
for handle in handles {
|
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
|
|
|
|
|
crate::scene::dispatch::apply_line_weight(entity, lw);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropLinetypeChanged(lt) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|
|
|
|
|
for handle in handles {
|
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
|
|
|
|
|
crate::scene::dispatch::apply_common_prop(entity, "linetype", <);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropHatchPatternChanged(name) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
|
use crate::scene::hatch_patterns;
|
|
|
|
|
|
if let Some(entry) = hatch_patterns::find(&name) {
|
|
|
|
|
|
self.push_undo_snapshot(i, "HATCHEDIT");
|
|
|
|
|
|
for handle in handles {
|
|
|
|
|
|
if let Some(acadrust::EntityType::Hatch(dxf)) =
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
dxf.pattern = hatch_patterns::build_dxf_pattern(entry);
|
|
|
|
|
|
dxf.is_solid = matches!(
|
|
|
|
|
|
entry.gpu,
|
|
|
|
|
|
crate::scene::hatch_model::HatchPattern::Solid
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(model) = self.tabs[i].scene.hatches.get_mut(&handle) {
|
|
|
|
|
|
model.pattern = entry.gpu.clone();
|
|
|
|
|
|
model.name = name.clone();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropBoolToggle(field) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|
|
|
|
|
for handle in handles {
|
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
|
|
|
|
|
match field {
|
|
|
|
|
|
"invisible" => crate::scene::dispatch::toggle_invisible(entity),
|
|
|
|
|
|
_ => crate::scene::dispatch::apply_geom_prop(entity, field, "toggle"),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropGeomChoiceChanged { field, value } => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
|
self.push_undo_snapshot(i, "CHPROP");
|
|
|
|
|
|
for handle in handles {
|
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
|
|
|
|
|
crate::scene::dispatch::apply_geom_prop(entity, field, &value);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropGeomInput { field, value } => {
|
|
|
|
|
|
self.tabs[self.active_tab].properties.edit_buf.insert(field.to_string(), value);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropGeomCommit(field) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let handles = self.property_target_handles(i);
|
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
|
if let Some(val) = self.tabs[i].properties.edit_buf.remove(field) {
|
|
|
|
|
|
self.push_undo_snapshot(i, "CHPROP");
|
2026-03-31 08:18:29 +03:00
|
|
|
|
if field == "frozen_layers" {
|
|
|
|
|
|
// Resolve layer names → handles, then apply to viewports.
|
|
|
|
|
|
let layer_handles: Vec<acadrust::Handle> = val
|
|
|
|
|
|
.split(',')
|
|
|
|
|
|
.map(|s| s.trim())
|
|
|
|
|
|
.filter(|s| !s.is_empty())
|
|
|
|
|
|
.filter_map(|name| {
|
|
|
|
|
|
self.tabs[i].scene.document.layers.iter()
|
|
|
|
|
|
.find(|l| l.name.eq_ignore_ascii_case(name))
|
|
|
|
|
|
.map(|l| l.handle)
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
for handle in handles {
|
|
|
|
|
|
if let Some(acadrust::EntityType::Viewport(vp)) =
|
|
|
|
|
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
|
|
|
|
|
{
|
|
|
|
|
|
vp.frozen_layers = layer_handles.clone();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for handle in handles {
|
|
|
|
|
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
|
|
|
|
|
match field {
|
|
|
|
|
|
"linetype_scale" | "transparency" => {
|
|
|
|
|
|
crate::scene::dispatch::apply_common_prop(entity, field, &val);
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
crate::scene::dispatch::apply_geom_prop(entity, field, &val);
|
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropColorPickerToggle => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.tabs[i].properties.color_picker_open = !self.tabs[i].properties.color_picker_open;
|
|
|
|
|
|
if self.tabs[i].properties.color_picker_open {
|
|
|
|
|
|
self.tabs[i].properties.color_palette_open = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PropColorPaletteToggle => {
|
|
|
|
|
|
self.tabs[self.active_tab].properties.color_palette_open =
|
|
|
|
|
|
!self.tabs[self.active_tab].properties.color_palette_open;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayoutSwitch(name) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let going_to_paper = name != "Model";
|
2026-03-31 07:24:16 +03:00
|
|
|
|
// Cancel any pending rename/context-menu and active viewport when switching.
|
2026-03-31 07:13:36 +03:00
|
|
|
|
self.layout_rename_state = None;
|
|
|
|
|
|
self.layout_context_menu = None;
|
2026-03-31 07:24:16 +03:00
|
|
|
|
self.tabs[i].scene.active_viewport = None;
|
2026-03-22 22:44:32 +03:00
|
|
|
|
self.tabs[i].scene.current_layout = name;
|
|
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
|
|
self.tabs[i].scene.fit_all();
|
|
|
|
|
|
if going_to_paper {
|
|
|
|
|
|
if let Some(idx) = self.ribbon.layout_module_index() {
|
|
|
|
|
|
self.ribbon.select(idx);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if self.ribbon.active_is_layout() {
|
|
|
|
|
|
self.ribbon.select(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayoutCreate => {
|
|
|
|
|
|
let i = self.active_tab;
|
2026-03-31 07:13:36 +03:00
|
|
|
|
// Find a unique name (e.g. Layout2, Layout3, ...).
|
|
|
|
|
|
let existing = self.tabs[i].scene.layout_names();
|
|
|
|
|
|
let mut idx = existing.len();
|
|
|
|
|
|
let new_name = loop {
|
|
|
|
|
|
let candidate = format!("Layout{}", idx);
|
|
|
|
|
|
if !existing.contains(&candidate) {
|
|
|
|
|
|
break candidate;
|
|
|
|
|
|
}
|
|
|
|
|
|
idx += 1;
|
|
|
|
|
|
};
|
2026-03-22 22:44:32 +03:00
|
|
|
|
self.push_undo_snapshot(i, "LAYOUT");
|
|
|
|
|
|
match self.tabs[i].scene.document.add_layout(&new_name) {
|
|
|
|
|
|
Ok(_) => {
|
|
|
|
|
|
self.tabs[i].scene.current_layout = new_name.clone();
|
|
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
|
|
self.tabs[i].scene.fit_all();
|
|
|
|
|
|
if let Some(idx) = self.ribbon.layout_module_index() {
|
|
|
|
|
|
self.ribbon.select(idx);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"Layout \"{new_name}\" oluşturuldu — MVIEW ile viewport ekleyin"
|
|
|
|
|
|
));
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(e) => self.command_line.push_error(&format!("Layout oluşturulamadı: {e}")),
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 07:13:36 +03:00
|
|
|
|
Message::LayoutDelete(name) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYOUT DEL");
|
|
|
|
|
|
if self.tabs[i].scene.delete_layout(&name) {
|
|
|
|
|
|
self.layout_context_menu = None;
|
|
|
|
|
|
self.layout_rename_state = None;
|
|
|
|
|
|
// If we fell back to Model space, update ribbon.
|
|
|
|
|
|
if self.tabs[i].scene.current_layout == "Model"
|
|
|
|
|
|
&& self.ribbon.active_is_layout()
|
|
|
|
|
|
{
|
|
|
|
|
|
self.ribbon.select(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.command_line.push_output(&format!("Layout \"{name}\" silindi"));
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayoutRenameStart(name) => {
|
|
|
|
|
|
if name != "Model" {
|
|
|
|
|
|
self.layout_rename_state = Some((name.clone(), name));
|
|
|
|
|
|
self.layout_context_menu = None;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayoutRenameEdit(val) => {
|
|
|
|
|
|
if let Some((orig, _)) = &self.layout_rename_state {
|
|
|
|
|
|
let orig = orig.clone();
|
|
|
|
|
|
self.layout_rename_state = Some((orig, val));
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayoutRenameCommit => {
|
|
|
|
|
|
if let Some((orig, new_name)) = self.layout_rename_state.take() {
|
|
|
|
|
|
let new_name = new_name.trim().to_string();
|
|
|
|
|
|
if !new_name.is_empty() && new_name != orig {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let exists = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.layout_names()
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.any(|n| *n == new_name);
|
|
|
|
|
|
if exists {
|
|
|
|
|
|
self.command_line.push_error(&format!(
|
|
|
|
|
|
"\"{}\" adı zaten kullanımda",
|
|
|
|
|
|
new_name
|
|
|
|
|
|
));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.push_undo_snapshot(i, "LAYOUT RENAME");
|
|
|
|
|
|
self.tabs[i].scene.rename_layout(&orig, &new_name);
|
|
|
|
|
|
if self.tabs[i].scene.current_layout == orig {
|
|
|
|
|
|
self.tabs[i].scene.current_layout = new_name.clone();
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("Layout \"{orig}\" → \"{new_name}\""));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayoutRenameCancel => {
|
|
|
|
|
|
self.layout_rename_state = None;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayoutContextMenu(name) => {
|
|
|
|
|
|
if name != "Model" {
|
|
|
|
|
|
self.layout_context_menu = Some(name);
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::LayoutContextMenuClose => {
|
|
|
|
|
|
self.layout_context_menu = None;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 07:24:16 +03:00
|
|
|
|
Message::EnterViewport(handle) => {
|
|
|
|
|
|
let i = self.active_tab;
|
2026-03-31 13:07:25 +03:00
|
|
|
|
// Clear paper-space selection before entering model space.
|
|
|
|
|
|
self.tabs[i].scene.deselect_all();
|
2026-03-31 07:24:16 +03:00
|
|
|
|
self.tabs[i].scene.active_viewport = Some(handle);
|
2026-03-31 13:07:25 +03:00
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
self.command_line.push_output("MSPACE");
|
2026-03-31 07:24:16 +03:00
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::ExitViewport => {
|
|
|
|
|
|
let i = self.active_tab;
|
2026-03-31 13:07:25 +03:00
|
|
|
|
// Clear model-space selection before returning to paper space.
|
|
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
|
|
self.refresh_properties();
|
2026-03-31 07:24:16 +03:00
|
|
|
|
self.tabs[i].scene.active_viewport = None;
|
|
|
|
|
|
self.command_line.push_output("PSPACE");
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 12:52:07 +03:00
|
|
|
|
Message::MspaceCommand => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if self.tabs[i].scene.current_layout == "Model" {
|
|
|
|
|
|
self.command_line.push_error("MS is only available in paper space layouts.");
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
if self.tabs[i].scene.active_viewport.is_some() {
|
|
|
|
|
|
// Already in MSPACE — nothing to do.
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
match self.tabs[i].scene.first_user_viewport() {
|
|
|
|
|
|
Some(handle) => Task::done(Message::EnterViewport(handle)),
|
|
|
|
|
|
None => {
|
|
|
|
|
|
self.command_line.push_error("No viewport found in this layout.");
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::PspaceCommand => {
|
|
|
|
|
|
Task::done(Message::ExitViewport)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 22:44:32 +03:00
|
|
|
|
Message::Undo => { self.undo_active_tab(); Task::none() }
|
|
|
|
|
|
Message::Redo => { self.redo_active_tab(); Task::none() }
|
|
|
|
|
|
|
|
|
|
|
|
Message::UndoMany(steps) => {
|
|
|
|
|
|
self.ribbon.close_dropdown();
|
|
|
|
|
|
self.undo_steps(steps);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::RedoMany(steps) => {
|
|
|
|
|
|
self.ribbon.close_dropdown();
|
|
|
|
|
|
self.redo_steps(steps);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Message::Noop => Task::none(),
|
2026-03-31 08:05:17 +03:00
|
|
|
|
|
|
|
|
|
|
// ── Page Setup ────────────────────────────────────────────────
|
|
|
|
|
|
Message::PageSetupOpen => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
// Populate edit buffers from current paper limits.
|
|
|
|
|
|
let (w, h) = if let Some(((_, _), (x1, y1))) = self.tabs[i].scene.paper_limits() {
|
|
|
|
|
|
(x1, y1)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
(297.0, 210.0) // A4 default
|
|
|
|
|
|
};
|
|
|
|
|
|
self.page_setup_w = format!("{w:.1}");
|
|
|
|
|
|
self.page_setup_h = format!("{h:.1}");
|
|
|
|
|
|
self.page_setup_open = true;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PageSetupClose => {
|
|
|
|
|
|
self.page_setup_open = false;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PageSetupWidthEdit(s) => {
|
|
|
|
|
|
self.page_setup_w = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PageSetupHeightEdit(s) => {
|
|
|
|
|
|
self.page_setup_h = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PageSetupCommit => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let layout_name = self.tabs[i].scene.current_layout.clone();
|
|
|
|
|
|
if layout_name != "Model" {
|
|
|
|
|
|
let w: f64 = self.page_setup_w.parse::<f64>().unwrap_or(297.0).max(1.0);
|
|
|
|
|
|
let h: f64 = self.page_setup_h.parse::<f64>().unwrap_or(210.0).max(1.0);
|
|
|
|
|
|
// Update the Layout object's limits.
|
|
|
|
|
|
for obj in self.tabs[i].scene.document.objects.values_mut() {
|
|
|
|
|
|
if let acadrust::objects::ObjectType::Layout(l) = obj {
|
|
|
|
|
|
if l.name == layout_name {
|
|
|
|
|
|
l.min_limits = (0.0, 0.0);
|
|
|
|
|
|
l.max_limits = (w, h);
|
|
|
|
|
|
l.min_extents = (0.0, 0.0, 0.0);
|
|
|
|
|
|
l.max_extents = (w, h, 0.0);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
"Page size set to {w:.1} × {h:.1} mm."
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
self.page_setup_open = false;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Plot / Export ─────────────────────────────────────────────
|
|
|
|
|
|
Message::PlotExport => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let stem = self.tabs[i]
|
|
|
|
|
|
.current_path
|
|
|
|
|
|
.as_deref()
|
|
|
|
|
|
.and_then(|p: &std::path::Path| p.file_stem())
|
|
|
|
|
|
.map(|s: &std::ffi::OsStr| s.to_string_lossy().into_owned())
|
|
|
|
|
|
.unwrap_or_else(|| "drawing".into());
|
|
|
|
|
|
Task::perform(
|
|
|
|
|
|
crate::io::pdf_export::pick_pdf_path_owned(stem),
|
|
|
|
|
|
Message::PlotExportPath,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
Message::PlotExportPath(None) => Task::none(),
|
|
|
|
|
|
Message::PlotExportPath(Some(path)) => {
|
|
|
|
|
|
let i = self.active_tab;
|
2026-03-31 13:07:25 +03:00
|
|
|
|
let wires = self.tabs[i].scene.entity_wires(); // full scene for PDF
|
2026-03-31 08:18:29 +03:00
|
|
|
|
let (paper_w, paper_h, offset_x, offset_y) =
|
|
|
|
|
|
if let Some(((x0, y0), (x1, y1))) = self.tabs[i].scene.paper_limits() {
|
|
|
|
|
|
(x1 - x0, y1 - y0, -x0, -y0)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Model space: fit wires to computed extents with 5 % margin.
|
|
|
|
|
|
let margin = 1.05_f64;
|
|
|
|
|
|
if let Some((mn, mx)) = self.tabs[i].scene.model_space_extents() {
|
|
|
|
|
|
let w = ((mx.x - mn.x) as f64 * margin).max(1.0);
|
|
|
|
|
|
let h = ((mx.y - mn.y) as f64 * margin).max(1.0);
|
|
|
|
|
|
let pad_x = (w - (mx.x - mn.x) as f64) * 0.5;
|
|
|
|
|
|
let pad_y = (h - (mx.y - mn.y) as f64) * 0.5;
|
|
|
|
|
|
(w, h, -(mn.x as f64) + pad_x, -(mn.y as f64) + pad_y)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
(297.0, 210.0, 0.0, 0.0)
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
match crate::io::pdf_export::export_pdf(&wires, paper_w, paper_h, offset_x as f32, offset_y as f32, &path) {
|
2026-03-31 08:05:17 +03:00
|
|
|
|
Ok(()) => self.command_line.push_info(&format!(
|
|
|
|
|
|
"Exported: {}",
|
|
|
|
|
|
path.file_name().unwrap_or_default().to_string_lossy()
|
|
|
|
|
|
)),
|
|
|
|
|
|
Err(e) => self.command_line.push_error(&format!("Export failed: {e}")),
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
2026-03-22 22:44:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|