refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
//! `dialog` arms and helpers, split out of the original `update.rs` (#mechanical decomposition).
|
|
|
|
|
|
|
|
|
|
#![allow(unused_imports)]
|
|
|
|
|
use super::util::*;
|
2026-08-05 21:37:56 +01:00
|
|
|
use crate::ui::window::block_palette::BlockPaletteMsg;
|
2026-06-27 11:50:11 +03:00
|
|
|
use super::{format_size, VIEWCUBE_HIT_SIZE};
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
use crate::app::helpers::{
|
2026-08-08 21:32:44 +03:00
|
|
|
parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis,
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
CoordKind,
|
|
|
|
|
};
|
|
|
|
|
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
|
|
|
|
use crate::modules::ModuleEvent;
|
|
|
|
|
use crate::scene::pick::grip::{find_hit_grip, find_hit_grip_paper, find_hit_grip_rte, GripEdit};
|
|
|
|
|
use crate::scene::model::object::GripApply;
|
|
|
|
|
use crate::scene::{
|
2026-06-27 11:50:11 +03:00
|
|
|
self, hover_id, CubeRegion, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX,
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
};
|
|
|
|
|
use crate::ui::PropertiesPanel;
|
|
|
|
|
use acadrust::types::Color as AcadColor;
|
|
|
|
|
use acadrust::{EntityType as AcadEntityType, Handle};
|
|
|
|
|
use iced::time::Instant;
|
|
|
|
|
use iced::{mouse, Point, Task};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
impl OpenCADStudio {
|
|
|
|
|
pub(in crate::app) fn open_save_dialog_window(&mut self, tab_idx: usize) -> Task<Message> {
|
2026-07-23 13:32:26 +03:00
|
|
|
// Default the format dropdown to the loaded file's own format — its
|
|
|
|
|
// DWG-vs-DXF kind (from the extension) and its version (from the parsed
|
|
|
|
|
// document) — so Save-As round-trips the format instead of silently
|
2026-07-27 22:37:47 +03:00
|
|
|
// re-targeting it. A new/unsaved drawing has no source format, so it
|
|
|
|
|
// uses the application-wide default chosen in Options (#529).
|
|
|
|
|
self.save_dialog_format = if let Some(path) = &self.tabs[tab_idx].current_path {
|
|
|
|
|
let document = &self.tabs[tab_idx].scene.document;
|
2026-08-03 12:47:48 +03:00
|
|
|
let is_dxf = crate::io::source_is_dxf(Some(path), document);
|
2026-07-27 22:37:47 +03:00
|
|
|
let version = if is_dxf {
|
|
|
|
|
document.version
|
|
|
|
|
} else {
|
|
|
|
|
document.dwg_source_version.unwrap_or(document.version)
|
|
|
|
|
};
|
|
|
|
|
crate::io::format_for_version(version, is_dxf)
|
|
|
|
|
} else {
|
|
|
|
|
self.default_save_format.clone()
|
|
|
|
|
};
|
2026-06-28 02:46:13 +03:00
|
|
|
|
2026-07-11 09:12:36 +03:00
|
|
|
// Pre-fill the default file name from the current path or the tab name;
|
|
|
|
|
// the destination folder comes from the native OS dialog that follows.
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
if let Some(p) = &self.tabs[tab_idx].current_path.clone() {
|
|
|
|
|
if let Some(name) = p.file_name() {
|
|
|
|
|
self.save_dialog_filename = name.to_string_lossy().into_owned();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
let (ext, _) = crate::io::parse_save_format(&self.save_dialog_format);
|
|
|
|
|
self.save_dialog_filename = format!("{}.{ext}", self.tabs[tab_idx].tab_display_name());
|
|
|
|
|
}
|
2026-08-03 12:47:48 +03:00
|
|
|
if self.tabs[tab_idx].recovery_save_as_required {
|
|
|
|
|
let (ext, _) = crate::io::parse_save_format(&self.save_dialog_format);
|
|
|
|
|
let stem = std::path::Path::new(&self.save_dialog_filename)
|
|
|
|
|
.file_stem()
|
|
|
|
|
.map(|value| value.to_string_lossy().into_owned())
|
|
|
|
|
.unwrap_or_else(|| self.tabs[tab_idx].tab_display_name());
|
|
|
|
|
self.save_dialog_filename = format!("{stem}_recovered.{ext}");
|
|
|
|
|
}
|
2026-06-29 11:47:37 +03:00
|
|
|
self.aec_drop_acknowledged = false;
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
self.active_modal = Some(crate::app::ModalKind::SaveDialog);
|
|
|
|
|
Task::none()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub(in crate::app) fn close_save_dialog_window(&mut self) -> Task<Message> {
|
2026-06-29 11:47:37 +03:00
|
|
|
self.aec_drop_acknowledged = false;
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
if self.active_modal == Some(crate::app::ModalKind::SaveDialog) {
|
|
|
|
|
self.active_modal = None;
|
2026-08-01 18:24:05 +03:00
|
|
|
self.reset_modal_geometry();
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
}
|
|
|
|
|
Task::none()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub(in crate::app) fn open_unsaved_dialog_window(&mut self) -> Task<Message> {
|
|
|
|
|
self.active_modal = Some(crate::app::ModalKind::Unsaved);
|
|
|
|
|
// The unsaved-changes prompt renders inside the main window, so bring
|
|
|
|
|
// that window to the foreground — a close signal can arrive while the
|
|
|
|
|
// app is backgrounded, leaving the prompt unseen behind other windows.
|
|
|
|
|
// `gain_focus` alone is ignored by most Linux WMs (focus-stealing
|
|
|
|
|
// prevention), so pair it with an urgency hint so the window is at
|
|
|
|
|
// least flagged for attention when the compositor blocks the raise.
|
|
|
|
|
match self.main_window {
|
|
|
|
|
Some(id) => Task::batch([
|
|
|
|
|
iced::window::gain_focus(id),
|
|
|
|
|
iced::window::request_user_attention(
|
|
|
|
|
id,
|
|
|
|
|
Some(iced::window::UserAttention::Critical),
|
|
|
|
|
),
|
|
|
|
|
]),
|
|
|
|
|
None => Task::none(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub(in crate::app) fn close_unsaved_dialog_window(&mut self) -> Task<Message> {
|
|
|
|
|
if self.active_modal == Some(crate::app::ModalKind::Unsaved) {
|
|
|
|
|
self.active_modal = None;
|
2026-08-01 18:24:05 +03:00
|
|
|
self.reset_modal_geometry();
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
}
|
|
|
|
|
Task::none()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEvent) -> Task<Message> {
|
2026-08-11 13:15:09 +03:00
|
|
|
// Commands use `start_allowed`; other events need a drawing
|
|
|
|
|
// and stay blocked on the Start page (#299, #388, #389).
|
fix(start): let the welcome page's own buttons work (#388, #389)
Donate, Send Feedback, OCS Web and Manage > About all answered "No
drawing open — use New or Open first." and did nothing.
Two doors guarded the welcome tab and the outer one shadowed the inner.
`dispatch_command` already knew which commands stand alone — About,
Donate, Report and the links were all on its list — but
`on_ribbon_tool_click` refused every ribbon event before dispatch was
ever reached.
The welcome page's own buttons emit RibbonToolClick, so they are only
ever clickable while `is_start` holds: the refusal covered exactly the
case they exist in, and they had never worked. New Drawing / Open File /
Plugins send their own messages, which is why the page still looked
alive.
So the ribbon door now defers commands to dispatch and keeps refusing
only what touches the scene. The policy stays in one place instead of
being half-remembered in two. A drawing tool is still turned away on the
welcome tab (#299) — the wording just comes from dispatch now.
The same gap was swallowing tools the issues never mentioned: CUI and
ALIASEDIT configure the application, not a drawing, and were missing
from the list, so fixing only the ribbon door would have moved their
refusal one door down rather than removing it. HELP and CUILOAD /
CUIIMPORT join them.
The new test drives the real message path and fails on the old code with
the reported wording. It reads the allowlist from source rather than
dispatching DONATE / REPORT / WEBVERSION, which shell out to a browser —
that also fails the day someone adds a welcome-page button and forgets
the list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:29:42 +03:00
|
|
|
if self.tabs[self.active_tab].is_start && !matches!(event, ModuleEvent::Command(_)) {
|
2026-07-11 10:11:28 +03:00
|
|
|
self.ribbon.close_dropdown();
|
|
|
|
|
self.command_line
|
2026-08-02 22:09:25 +03:00
|
|
|
.push_info(crate::t!("No drawing open — use New or Open first.").as_ref());
|
2026-07-11 10:11:28 +03:00
|
|
|
return Task::none();
|
|
|
|
|
}
|
2026-07-05 18:04:55 +03:00
|
|
|
// Dismiss any open dropdown / collapsed-panel flyout on tool use,
|
|
|
|
|
// and remember this tool as its panel's last-used one.
|
|
|
|
|
self.ribbon.close_dropdown();
|
|
|
|
|
self.ribbon.note_panel_tool(&tool_id);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
self.ribbon.activate_tool(&tool_id);
|
|
|
|
|
match event {
|
fix(ribbon): highlight follows each tool's mechanism; start-tab dimming (#355)
Tool highlights used to stick: every click set active_tool, but only
interactive commands and a hand-maintained per-modal list ever cleared
it, so one-shots (Cleanup/AUDIT, view actions) and unlisted dialogs
(Customization/ALIASEDIT, CUI, Plugin Manager…) stayed blue forever.
Now the rule is mechanical: state toggles light from state only (the
click highlight drops immediately), interactive commands stay lit
while running, dialog openers stay lit until the modal closes (generic
clear replaces the per-modal list), and one-shots never keep it — the
dispatch site turns the highlight off when nothing stayed running.
On the Start tab, tools whose command the start gate refuses now render
dimmed (grey icon + label) across all button styles, dropdowns and the
quick-access strip; start_allowed() is the single shared authority.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 00:13:48 +03:00
|
|
|
ModuleEvent::Command(cmd) => {
|
|
|
|
|
let task = self.dispatch_command(&cmd);
|
|
|
|
|
// One-shot tools (view changes, clipboard, toggles,
|
|
|
|
|
// audits…) leave nothing running: no interactive
|
|
|
|
|
// command and no dialog. Their highlight would stick
|
|
|
|
|
// forever — turn it off now. Interactive commands and
|
|
|
|
|
// dialog owners keep theirs; the command end / modal
|
|
|
|
|
// close clears those. (#355)
|
|
|
|
|
let i = self.active_tab;
|
2026-08-04 18:21:52 +03:00
|
|
|
if self.tabs[i].active_cmd.is_none()
|
|
|
|
|
&& self.active_modal.is_none()
|
|
|
|
|
&& !self.tabs[i].pan_mode
|
|
|
|
|
&& !self.tabs[i].orbit_mode
|
2026-08-08 20:37:53 +03:00
|
|
|
&& !self.tabs[i].zoom_dynamic_mode
|
2026-08-04 18:21:52 +03:00
|
|
|
{
|
fix(ribbon): highlight follows each tool's mechanism; start-tab dimming (#355)
Tool highlights used to stick: every click set active_tool, but only
interactive commands and a hand-maintained per-modal list ever cleared
it, so one-shots (Cleanup/AUDIT, view actions) and unlisted dialogs
(Customization/ALIASEDIT, CUI, Plugin Manager…) stayed blue forever.
Now the rule is mechanical: state toggles light from state only (the
click highlight drops immediately), interactive commands stay lit
while running, dialog openers stay lit until the modal closes (generic
clear replaces the per-modal list), and one-shots never keep it — the
dispatch site turns the highlight off when nothing stayed running.
On the Start tab, tools whose command the start gate refuses now render
dimmed (grey icon + label) across all button styles, dropdowns and the
quick-access strip; start_allowed() is the single shared authority.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 00:13:48 +03:00
|
|
|
self.ribbon.deactivate_tool();
|
|
|
|
|
}
|
|
|
|
|
return task;
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
ModuleEvent::OpenFileDialog => {
|
|
|
|
|
self.command_line
|
2026-08-02 22:09:25 +03:00
|
|
|
.push_info(crate::t!("Open DWG/DXF: not yet implemented.").as_ref());
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
}
|
|
|
|
|
ModuleEvent::ClearModels => {
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
self.tabs[i].scene.clear();
|
|
|
|
|
self.tabs[i].properties = PropertiesPanel::empty();
|
2026-08-02 22:09:25 +03:00
|
|
|
self.command_line.push_output(crate::t!("Scene cleared.").as_ref());
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
}
|
2026-08-07 14:29:01 +03:00
|
|
|
ModuleEvent::SetVisualStyle(name) => {
|
|
|
|
|
use crate::modules::view::visual_style;
|
|
|
|
|
match visual_style::mode_for_keyword(&name) {
|
|
|
|
|
Some(mode) => return Task::done(Message::SetRenderMode(mode)),
|
|
|
|
|
None => {
|
|
|
|
|
// Name the styles that do exist, from the same
|
|
|
|
|
// list every other caller reads.
|
|
|
|
|
self.command_line.push_error(
|
|
|
|
|
crate::tf!("Unknown visual style \"{name}\".").as_ref(),
|
|
|
|
|
);
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_info(visual_style::keyword_prompt());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
ModuleEvent::ToggleLayers => {
|
|
|
|
|
return Task::done(Message::ToggleLayers);
|
|
|
|
|
}
|
|
|
|
|
ModuleEvent::PluginFileDialog {
|
|
|
|
|
command,
|
|
|
|
|
title,
|
|
|
|
|
filter_name,
|
|
|
|
|
extensions,
|
|
|
|
|
} => {
|
|
|
|
|
return Task::perform(
|
|
|
|
|
async move {
|
|
|
|
|
let exts: Vec<&str> =
|
|
|
|
|
extensions.iter().map(|s| s.as_str()).collect();
|
2026-07-21 09:27:44 +03:00
|
|
|
let path = crate::sys::file_dialog()
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
.set_title(title)
|
|
|
|
|
.add_filter(filter_name, &exts)
|
|
|
|
|
.add_filter("All Files", &["*"])
|
|
|
|
|
.pick_file()
|
|
|
|
|
.await
|
|
|
|
|
.map(|h| crate::sys::handle_path(&h));
|
|
|
|
|
(command, path)
|
|
|
|
|
},
|
|
|
|
|
|(command, path)| Message::PluginFileDialogResult { command, path },
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
fix(ribbon): highlight follows each tool's mechanism; start-tab dimming (#355)
Tool highlights used to stick: every click set active_tool, but only
interactive commands and a hand-maintained per-modal list ever cleared
it, so one-shots (Cleanup/AUDIT, view actions) and unlisted dialogs
(Customization/ALIASEDIT, CUI, Plugin Manager…) stayed blue forever.
Now the rule is mechanical: state toggles light from state only (the
click highlight drops immediately), interactive commands stay lit
while running, dialog openers stay lit until the modal closes (generic
clear replaces the per-modal list), and one-shots never keep it — the
dispatch site turns the highlight off when nothing stayed running.
On the Start tab, tools whose command the start gate refuses now render
dimmed (grey icon + label) across all button styles, dropdowns and the
quick-access strip; start_allowed() is the single shared authority.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 00:13:48 +03:00
|
|
|
// Every non-Command event above is a one-shot (state toggle,
|
|
|
|
|
// clear, dialog spawn) — nothing stays running to clear the
|
|
|
|
|
// highlight later, so turn it off here. (#355)
|
|
|
|
|
self.ribbon.deactivate_tool();
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
Task::none()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_unsaved_dialog_discard(&mut self) -> Task<Message> {
|
|
|
|
|
match self.pending_close.take() {
|
|
|
|
|
Some(crate::app::PendingClose::Tab(idx)) => {
|
|
|
|
|
let close_win = self.close_unsaved_dialog_window();
|
2026-06-29 16:39:42 +03:00
|
|
|
// Discarded — drop this tab's autosave recovery copy.
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
let _ = std::fs::remove_file(self.autosave_target(idx));
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
if self.tabs.len() == 1 {
|
|
|
|
|
self.tab_counter += 1;
|
|
|
|
|
self.tabs[0] =
|
|
|
|
|
crate::app::document::DocumentTab::new_drawing(self.tab_counter);
|
|
|
|
|
self.active_tab = 0;
|
|
|
|
|
self.apply_bg_default(0);
|
|
|
|
|
} else {
|
|
|
|
|
self.tabs.remove(idx);
|
|
|
|
|
if self.active_tab >= self.tabs.len() {
|
|
|
|
|
self.active_tab = self.tabs.len() - 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// The active tab is now a fresh blank or a
|
|
|
|
|
// different existing tab; sync ribbon chips so
|
|
|
|
|
// they don't keep showing the discarded tab's
|
|
|
|
|
// last selection. #21.
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
self.sync_ribbon_from_selection();
|
2026-07-27 12:02:50 +03:00
|
|
|
return Task::batch([close_win, self.continue_tab_close_queue()]);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
}
|
|
|
|
|
Some(crate::app::PendingClose::Quit) => {
|
|
|
|
|
if let Some(idx) = self.tabs.iter().position(|t| t.dirty) {
|
|
|
|
|
self.tabs[idx].dirty = false;
|
|
|
|
|
}
|
|
|
|
|
if self.tabs.iter().any(|t| t.dirty) {
|
|
|
|
|
// More dirty tabs remain — keep window open.
|
|
|
|
|
self.pending_close = Some(crate::app::PendingClose::Quit);
|
|
|
|
|
} else {
|
|
|
|
|
let close_win = self.close_unsaved_dialog_window();
|
2026-06-29 13:34:16 +03:00
|
|
|
return Task::batch(vec![close_win, self.exit_app()]);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => {}
|
|
|
|
|
}
|
|
|
|
|
Task::none()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_unsaved_dialog_save(&mut self) -> Task<Message> {
|
2026-07-25 02:58:46 +03:00
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
{
|
|
|
|
|
let Some(pending) = self.pending_close.clone() else {
|
|
|
|
|
return Task::none();
|
|
|
|
|
};
|
|
|
|
|
let (idx, continuation) = match pending {
|
|
|
|
|
crate::app::PendingClose::Tab(idx) => {
|
|
|
|
|
(idx, crate::app::SaveContinuation::CloseTab)
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
}
|
2026-07-25 02:58:46 +03:00
|
|
|
crate::app::PendingClose::Quit => {
|
|
|
|
|
let Some(idx) = self.tabs.iter().position(|tab| tab.dirty) else {
|
|
|
|
|
self.pending_close = None;
|
|
|
|
|
return Task::batch([
|
|
|
|
|
self.close_unsaved_dialog_window(),
|
|
|
|
|
self.exit_app(),
|
|
|
|
|
]);
|
|
|
|
|
};
|
|
|
|
|
(idx, crate::app::SaveContinuation::Quit)
|
|
|
|
|
}
|
|
|
|
|
};
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
2026-07-25 02:58:46 +03:00
|
|
|
if self.active_save_jobs.contains_key(&self.tabs[idx].id) {
|
|
|
|
|
self.command_line
|
2026-08-02 22:09:25 +03:00
|
|
|
.push_info(crate::t!("Save already running for this drawing.").as_ref());
|
2026-07-25 02:58:46 +03:00
|
|
|
return Task::none();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 12:47:48 +03:00
|
|
|
if !self.tabs[idx].recovery_save_as_required {
|
|
|
|
|
if let Some(path) = self.tabs[idx].current_path.clone() {
|
|
|
|
|
let version = self.tabs[idx].scene.document.version;
|
|
|
|
|
self.prepare_native_save(idx);
|
|
|
|
|
let close = self.close_unsaved_dialog_window();
|
|
|
|
|
let save = self.queue_native_save(
|
|
|
|
|
idx,
|
|
|
|
|
path,
|
|
|
|
|
version,
|
|
|
|
|
crate::app::SavePurpose::Manual,
|
|
|
|
|
continuation,
|
|
|
|
|
false,
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
return Task::batch([close, save]);
|
|
|
|
|
}
|
2026-07-25 02:58:46 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.active_tab = idx;
|
|
|
|
|
self.save_dialog_for_unsaved = true;
|
|
|
|
|
let close = self.close_unsaved_dialog_window();
|
2026-07-27 22:37:47 +03:00
|
|
|
let save = self.save_with_default_format(idx);
|
2026-07-25 02:58:46 +03:00
|
|
|
return Task::batch([close, save]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
{
|
|
|
|
|
match self.pending_close.take() {
|
|
|
|
|
Some(crate::app::PendingClose::Tab(idx)) => {
|
|
|
|
|
self.pending_close = Some(crate::app::PendingClose::Tab(idx));
|
|
|
|
|
self.save_dialog_for_unsaved = true;
|
|
|
|
|
let close = self.close_unsaved_dialog_window();
|
2026-07-27 22:37:47 +03:00
|
|
|
let save = self.save_with_default_format(idx);
|
2026-07-25 02:58:46 +03:00
|
|
|
Task::batch([close, save])
|
|
|
|
|
}
|
|
|
|
|
Some(crate::app::PendingClose::Quit) => {
|
|
|
|
|
if let Some(idx) = self.tabs.iter().position(|tab| tab.dirty) {
|
|
|
|
|
self.active_tab = idx;
|
|
|
|
|
self.pending_close = Some(crate::app::PendingClose::Quit);
|
|
|
|
|
self.save_dialog_for_unsaved = true;
|
|
|
|
|
let close = self.close_unsaved_dialog_window();
|
2026-07-27 22:37:47 +03:00
|
|
|
let save = self.save_with_default_format(idx);
|
2026-07-25 02:58:46 +03:00
|
|
|
Task::batch([close, save])
|
|
|
|
|
} else {
|
|
|
|
|
Task::batch([
|
|
|
|
|
self.close_unsaved_dialog_window(),
|
|
|
|
|
self.exit_app(),
|
|
|
|
|
])
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-07-25 02:58:46 +03:00
|
|
|
None => Task::none(),
|
|
|
|
|
}
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
}
|
2026-07-25 02:58:46 +03:00
|
|
|
|
2026-08-05 21:37:56 +01:00
|
|
|
/// Build a unique block name from a file stem: the stem itself, then
|
|
|
|
|
/// "stem (2)", "stem (3)", … on collisions.
|
|
|
|
|
fn block_name_from_file(&self, stem: &str) -> String {
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
let base = stem.trim();
|
|
|
|
|
if base.is_empty() {
|
|
|
|
|
return self.unique_block_name("Block");
|
|
|
|
|
}
|
|
|
|
|
let doc = &self.tabs[i].scene.document;
|
|
|
|
|
if doc.block_records.get(base).is_none() {
|
|
|
|
|
return base.to_string();
|
|
|
|
|
}
|
|
|
|
|
let mut n = 2;
|
|
|
|
|
loop {
|
|
|
|
|
let name = format!("{base} ({n})");
|
|
|
|
|
if doc.block_records.get(&name).is_none() {
|
|
|
|
|
return name;
|
|
|
|
|
}
|
|
|
|
|
n += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Load an external DWG/DXF at `path` and define its model-space contents as
|
|
|
|
|
/// one new block in the active drawing. Returns the new block's name, or an
|
|
|
|
|
/// error message. Nested block definitions are imported first so nested
|
|
|
|
|
/// INSERTs render (AutoCAD's "inserting a drawing imports its block defs").
|
|
|
|
|
fn import_file_as_block(&mut self, path: std::path::PathBuf) -> Result<String, String> {
|
|
|
|
|
let doc = crate::io::load_file(&path).map_err(|e| e.to_string())?;
|
|
|
|
|
let stem = path
|
|
|
|
|
.file_stem()
|
|
|
|
|
.map(|s| s.to_string_lossy().into_owned())
|
|
|
|
|
.unwrap_or_else(|| "Block".to_string());
|
|
|
|
|
self.import_document_as_block(doc, stem)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Define one block in the active drawing from a loaded `CadDocument`'s
|
|
|
|
|
/// model-space entities (base = the file's model-space insertion base).
|
|
|
|
|
fn import_document_as_block(
|
|
|
|
|
&mut self,
|
|
|
|
|
doc: acadrust::CadDocument,
|
|
|
|
|
stem: String,
|
|
|
|
|
) -> Result<String, String> {
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
// Model-space block record handle (Layout object first, name fallback).
|
|
|
|
|
let model_br = doc
|
|
|
|
|
.objects
|
|
|
|
|
.values()
|
|
|
|
|
.find_map(|o| {
|
|
|
|
|
if let acadrust::objects::ObjectType::Layout(l) = o {
|
|
|
|
|
(l.name == "Model" && !l.block_record.is_null()).then_some(l.block_record)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.or_else(|| doc.block_records.get("*Model_Space").map(|br| br.handle))
|
|
|
|
|
.unwrap_or(acadrust::Handle::NULL);
|
|
|
|
|
let mut entities: Vec<acadrust::EntityType> = if model_br.is_null() {
|
|
|
|
|
Vec::new()
|
|
|
|
|
} else {
|
|
|
|
|
let br = doc.block_records.iter().find(|br| br.handle == model_br);
|
|
|
|
|
let handles = br.map(|b| b.entity_handles.clone()).unwrap_or_default();
|
|
|
|
|
if !handles.is_empty() {
|
|
|
|
|
// Authoritative ownership list (DWG and well-formed DXF).
|
|
|
|
|
handles
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|h| doc.get_entity(*h))
|
|
|
|
|
.filter(|e| {
|
|
|
|
|
!matches!(e, acadrust::EntityType::Block(_) | acadrust::EntityType::BlockEnd(_))
|
|
|
|
|
})
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect()
|
|
|
|
|
} else {
|
|
|
|
|
// Legacy DXF that omits 330 group codes: treat null-owner
|
|
|
|
|
// entities as model-space content (mirrors belongs_to_visible_block).
|
|
|
|
|
doc.entities()
|
|
|
|
|
.filter(|e| {
|
|
|
|
|
let o = e.common().owner_handle;
|
|
|
|
|
o == model_br || o.is_null()
|
|
|
|
|
})
|
|
|
|
|
.filter(|e| {
|
|
|
|
|
!matches!(e, acadrust::EntityType::Block(_) | acadrust::EntityType::BlockEnd(_))
|
|
|
|
|
})
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
if entities.is_empty() {
|
|
|
|
|
return Err("No model-space entities in that file.".to_string());
|
|
|
|
|
}
|
|
|
|
|
let base = glam::DVec3::new(
|
|
|
|
|
doc.header.model_space_insertion_base.x,
|
|
|
|
|
doc.header.model_space_insertion_base.y,
|
|
|
|
|
doc.header.model_space_insertion_base.z,
|
|
|
|
|
);
|
|
|
|
|
let name = self.block_name_from_file(&stem);
|
|
|
|
|
// Capture every table record needed by the top-level entities and their
|
|
|
|
|
// nested block definitions. Importing only the definitions leaves
|
|
|
|
|
// source-only layers, linetypes, and text/dimension styles dangling.
|
|
|
|
|
let deps = crate::app::ClipboardDeps::capture(&doc, &entities);
|
|
|
|
|
// Capture the imported file's nested block definitions once. When a
|
|
|
|
|
// name collides with a block already in the active drawing, we must
|
|
|
|
|
// *preserve both*: keep the destination's block and import the file's
|
|
|
|
|
// under a unique name, then re-point every INSERT at the renamed one —
|
|
|
|
|
// otherwise a nested reference silently resolves to the destination's
|
|
|
|
|
// unrelated definition. (#135-style collision, but for file imports.)
|
|
|
|
|
let mut defs = deps.blocks.clone();
|
|
|
|
|
let mut rename_map: std::collections::HashMap<String, String> =
|
|
|
|
|
std::collections::HashMap::new();
|
|
|
|
|
// Reserve every destination block name plus every imported dependency
|
|
|
|
|
// name, so importing a source file that itself holds "Door" and
|
|
|
|
|
// "Door (2)" cannot generate a colliding second "Door (2)".
|
|
|
|
|
let mut reserved: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
|
|
|
|
|
for existing in self.tabs[i].scene.document.block_records.names() {
|
|
|
|
|
reserved.insert(existing.to_string());
|
|
|
|
|
}
|
|
|
|
|
for def in &defs {
|
|
|
|
|
reserved.insert(def.name.clone());
|
|
|
|
|
}
|
|
|
|
|
for def in &defs {
|
|
|
|
|
// Keep the name only if it is truly unused in the active drawing.
|
|
|
|
|
let used_in_dest = self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.block_records
|
|
|
|
|
.get(&def.name)
|
|
|
|
|
.is_some();
|
|
|
|
|
let dest_name = if used_in_dest {
|
|
|
|
|
let mut n = 2;
|
|
|
|
|
loop {
|
|
|
|
|
let candidate = format!("{} ({})", def.name, n);
|
|
|
|
|
if reserved.insert(candidate.clone()) {
|
|
|
|
|
break candidate;
|
|
|
|
|
}
|
|
|
|
|
n += 1;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
reserved.insert(def.name.clone());
|
|
|
|
|
def.name.clone()
|
|
|
|
|
};
|
|
|
|
|
rename_map.insert(def.name.clone(), dest_name);
|
|
|
|
|
}
|
|
|
|
|
// Rewrite every INSERT in the model-space entities and in every captured
|
|
|
|
|
// definition, then rename the definitions and define them.
|
|
|
|
|
for entity in entities
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.chain(defs.iter_mut().flat_map(|def| def.entities.iter_mut()))
|
|
|
|
|
{
|
|
|
|
|
if let acadrust::EntityType::Insert(ins) = entity {
|
|
|
|
|
if let Some(new_name) = rename_map.get(&ins.block_name) {
|
|
|
|
|
ins.block_name = new_name.clone();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for def in &mut defs {
|
|
|
|
|
if let Some(new_name) = rename_map.get(&def.name) {
|
|
|
|
|
def.name = new_name.clone();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Every mutation below belongs to the one INSERT FILE undo step.
|
|
|
|
|
self.push_undo_snapshot(i, "INSERT FILE");
|
|
|
|
|
self.merge_dependencies(i, &deps);
|
|
|
|
|
for def in defs {
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.define_block_raw(&def.name, def.base_point, def.entities);
|
|
|
|
|
}
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.define_block_from_owned_entities(entities, &name, base)?;
|
|
|
|
|
self.tabs[i].scene.populate_meshes_from_document();
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
Ok(name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_block_palette(&mut self, m: crate::ui::window::block_palette::BlockPaletteMsg) -> iced::Task<Message> {
|
|
|
|
|
use crate::ui::window::block_palette::{BlockEntry, BlockPaletteMsg};
|
|
|
|
|
match m {
|
|
|
|
|
BlockPaletteMsg::Search(s) => {
|
|
|
|
|
self.block_palette.search = s;
|
|
|
|
|
iced::Task::none()
|
|
|
|
|
}
|
|
|
|
|
BlockPaletteMsg::CyclePreviewSize => {
|
|
|
|
|
self.block_palette.preview_size =
|
|
|
|
|
crate::ui::window::block_palette::cycle_preview_size(
|
|
|
|
|
self.block_palette.preview_size,
|
|
|
|
|
);
|
|
|
|
|
iced::Task::none()
|
|
|
|
|
}
|
|
|
|
|
BlockPaletteMsg::Refresh => {
|
|
|
|
|
self.refresh_block_palette();
|
|
|
|
|
iced::Task::none()
|
|
|
|
|
}
|
|
|
|
|
BlockPaletteMsg::ToggleBar => {
|
|
|
|
|
// Collapse bar (mirrors the Properties panel).
|
|
|
|
|
self.block_palette_expanded ^= true;
|
|
|
|
|
if self.block_palette_expanded {
|
|
|
|
|
self.refresh_block_palette();
|
|
|
|
|
}
|
|
|
|
|
iced::Task::none()
|
|
|
|
|
}
|
|
|
|
|
BlockPaletteMsg::Close => {
|
|
|
|
|
self.show_block_palette = false;
|
|
|
|
|
iced::Task::none()
|
|
|
|
|
}
|
|
|
|
|
BlockPaletteMsg::PickFile => iced::Task::perform(
|
|
|
|
|
async {
|
|
|
|
|
let handle = rfd::AsyncFileDialog::new()
|
|
|
|
|
.set_title("Select Drawing to Insert as Block")
|
|
|
|
|
.add_filter("DWG/DXF Files", &["dwg", "dxf", "DWG", "DXF"])
|
|
|
|
|
.pick_file()
|
|
|
|
|
.await;
|
|
|
|
|
match handle {
|
|
|
|
|
Some(h) => Ok(crate::sys::handle_path(&h)),
|
|
|
|
|
None => Err("Cancelled".to_string()),
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|r| Message::BlockPalette(BlockPaletteMsg::FilePicked(r)),
|
|
|
|
|
),
|
|
|
|
|
BlockPaletteMsg::FilePicked(Ok(path)) => {
|
|
|
|
|
match self.import_file_as_block(path) {
|
|
|
|
|
Ok(name) => {
|
|
|
|
|
self.command_line
|
|
|
|
|
.push_output(&format!("Inserting \"{name}\" from file."));
|
|
|
|
|
self.refresh_block_palette();
|
|
|
|
|
self.start_block_placement(&name);
|
|
|
|
|
}
|
|
|
|
|
Err(e) if e != "Cancelled" => {
|
|
|
|
|
self.command_line.push_error(&format!("INSERT FILE: {e}"));
|
|
|
|
|
}
|
|
|
|
|
Err(_) => {}
|
|
|
|
|
}
|
|
|
|
|
iced::Task::none()
|
|
|
|
|
}
|
|
|
|
|
BlockPaletteMsg::FilePicked(Err(e)) => {
|
|
|
|
|
if e != "Cancelled" {
|
|
|
|
|
self.command_line.push_error(&e);
|
|
|
|
|
}
|
|
|
|
|
iced::Task::none()
|
|
|
|
|
}
|
|
|
|
|
BlockPaletteMsg::Insert(name) => {
|
|
|
|
|
self.start_block_placement(&name);
|
|
|
|
|
iced::Task::none()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Start placing `name` through the INSERT command, skipping the name prompt.
|
|
|
|
|
fn start_block_placement(&mut self, name: &str) {
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
let wires = self
|
|
|
|
|
.block_palette
|
|
|
|
|
.blocks
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|b| b.name == name)
|
|
|
|
|
.map(|b| b.wires.clone())
|
|
|
|
|
.unwrap_or_else(|| self.tabs[i].scene.block_preview_wires(name));
|
|
|
|
|
use crate::modules::insert::insert_block::InsertBlockCommand;
|
|
|
|
|
let cmd = InsertBlockCommand::new_for_block(name.to_string(), wires, glam::Vec3::ZERO);
|
|
|
|
|
use crate::command::CadCommand;
|
|
|
|
|
self.command_line.push_info(&cmd.prompt());
|
|
|
|
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
|
|
|
|
self.block_palette.placing = Some(name.to_string());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Rebuild the panel's block list + cached wires from the active drawing.
|
|
|
|
|
pub(crate) fn refresh_block_palette(&mut self) {
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
let names = self.tabs[i].scene.custom_block_names();
|
|
|
|
|
self.block_palette.cached_names = names.clone();
|
|
|
|
|
self.block_palette.source_tab_id = Some(self.tabs[i].id);
|
|
|
|
|
self.block_palette.source_block_epoch = self.tabs[i].scene.block_epoch;
|
|
|
|
|
self.block_palette.blocks = names
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|name| {
|
|
|
|
|
let wires = self.tabs[i].scene.block_preview_wires(&name);
|
|
|
|
|
crate::ui::window::block_palette::BlockEntry { name, wires }
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Cheap per-update check: rebuild when the active drawing's definitions
|
|
|
|
|
/// changed, even when their names happen to stay the same.
|
|
|
|
|
pub(crate) fn refresh_block_palette_if_stale(&mut self) {
|
|
|
|
|
if !self.show_block_palette {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
// `block_epoch` advances for every block-definition change, so it
|
|
|
|
|
// avoids re-scanning every block name on unrelated application updates.
|
|
|
|
|
if self.block_palette.source_tab_id != Some(self.tabs[i].id)
|
|
|
|
|
|| self.block_palette.source_block_epoch != self.tabs[i].scene.block_epoch
|
|
|
|
|
{
|
|
|
|
|
self.refresh_block_palette();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
use crate::app::OpenCADStudio;
|
|
|
|
|
use acadrust::entities::Line;
|
|
|
|
|
use acadrust::types::Vector3;
|
|
|
|
|
use acadrust::EntityType;
|
|
|
|
|
|
|
|
|
|
fn fresh() -> OpenCADStudio {
|
|
|
|
|
let mut app = OpenCADStudio::new_for_test();
|
|
|
|
|
app.automation_op(r#"{"op":"new"}"#);
|
|
|
|
|
app
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A foreign document: the fresh scene's document plus one model-space LINE.
|
|
|
|
|
fn foreign_doc(app: &OpenCADStudio) -> acadrust::CadDocument {
|
|
|
|
|
let mut doc = app.tabs[app.active_tab].scene.document.clone();
|
|
|
|
|
let model_br = doc
|
|
|
|
|
.objects
|
|
|
|
|
.values()
|
|
|
|
|
.find_map(|o| {
|
|
|
|
|
if let acadrust::objects::ObjectType::Layout(l) = o {
|
|
|
|
|
(l.name == "Model" && !l.block_record.is_null()).then_some(l.block_record)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.or_else(|| doc.block_records.get("*Model_Space").map(|br| br.handle))
|
|
|
|
|
.expect("fresh document has a model-space block record");
|
|
|
|
|
let mut line = Line::new();
|
|
|
|
|
line.common.owner_handle = model_br;
|
|
|
|
|
line.start = Vector3::new(0.0, 0.0, 0.0);
|
|
|
|
|
line.end = Vector3::new(100.0, 50.0, 0.0);
|
|
|
|
|
doc.add_entity(EntityType::Line(line)).unwrap();
|
|
|
|
|
doc
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn import_document_as_block_defines_block() {
|
|
|
|
|
let mut app = fresh();
|
|
|
|
|
let doc = foreign_doc(&app);
|
|
|
|
|
let name = app.import_document_as_block(doc, "Fixture".to_string()).unwrap();
|
|
|
|
|
assert_eq!(name, "Fixture");
|
|
|
|
|
assert!(app.tabs[app.active_tab]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.block_records
|
|
|
|
|
.get("Fixture")
|
|
|
|
|
.is_some());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn import_document_as_block_merges_source_only_layer() {
|
|
|
|
|
use acadrust::tables::Layer;
|
|
|
|
|
|
|
|
|
|
let mut app = fresh();
|
|
|
|
|
let mut doc = foreign_doc(&app);
|
|
|
|
|
let mut layer = Layer::new("Imported Fixtures");
|
|
|
|
|
layer.handle = doc.allocate_handle();
|
|
|
|
|
doc.layers.add(layer).unwrap();
|
|
|
|
|
let model = doc
|
|
|
|
|
.objects
|
|
|
|
|
.values()
|
|
|
|
|
.find_map(|o| match o {
|
|
|
|
|
acadrust::objects::ObjectType::Layout(l) if l.name == "Model" => {
|
|
|
|
|
Some(l.block_record)
|
|
|
|
|
}
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
let line = doc
|
|
|
|
|
.entities_mut()
|
|
|
|
|
.find(|entity| entity.common().owner_handle == model)
|
|
|
|
|
.unwrap();
|
|
|
|
|
line.common_mut().layer = "Imported Fixtures".to_string();
|
|
|
|
|
|
|
|
|
|
app.import_document_as_block(doc, "Fixture".to_string())
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert!(app.tabs[app.active_tab]
|
|
|
|
|
.scene
|
|
|
|
|
.document
|
|
|
|
|
.layers
|
|
|
|
|
.contains("Imported Fixtures"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A foreign document whose model space INSERTs a `Fixture` block, whose
|
|
|
|
|
/// definition itself INSERTs a `Door` block — so the imported `Door` is a
|
|
|
|
|
/// *nested dependency*, not a top-level entity. The `Door` in this file is
|
|
|
|
|
/// unrelated to any `Door` in the destination drawing.
|
|
|
|
|
fn nested_foreign_doc(app: &OpenCADStudio) -> acadrust::CadDocument {
|
|
|
|
|
use acadrust::entities::Insert;
|
|
|
|
|
use acadrust::tables::BlockRecord;
|
|
|
|
|
use acadrust::Handle;
|
|
|
|
|
let mut doc = foreign_doc(app);
|
|
|
|
|
|
|
|
|
|
// "Door" block definition: one LINE of geometry.
|
|
|
|
|
let door_h = Handle::new(doc.next_handle());
|
|
|
|
|
let mut door_br = BlockRecord::new("Door");
|
|
|
|
|
door_br.handle = door_h;
|
|
|
|
|
doc.block_records.add(door_br).unwrap();
|
|
|
|
|
let mut door_line = Line::new();
|
|
|
|
|
door_line.start = Vector3::new(0.0, 0.0, 0.0);
|
|
|
|
|
door_line.end = Vector3::new(5.0, 0.0, 0.0);
|
|
|
|
|
door_line.common.owner_handle = door_h;
|
|
|
|
|
doc.add_entity(EntityType::Line(door_line)).unwrap();
|
|
|
|
|
|
|
|
|
|
// "Fixture" block definition: INSERTs the nested "Door".
|
|
|
|
|
let fixture_h = Handle::new(doc.next_handle());
|
|
|
|
|
let mut fixture_br = BlockRecord::new("Fixture");
|
|
|
|
|
fixture_br.handle = fixture_h;
|
|
|
|
|
doc.block_records.add(fixture_br).unwrap();
|
|
|
|
|
let mut nested = Insert::new("Door", Vector3::new(2.0, 0.0, 0.0));
|
|
|
|
|
nested.common.owner_handle = fixture_h;
|
|
|
|
|
doc.add_entity(EntityType::Insert(nested)).unwrap();
|
|
|
|
|
|
|
|
|
|
// Model space references the fixture so the import captures it.
|
|
|
|
|
let model_br = doc
|
|
|
|
|
.objects
|
|
|
|
|
.values()
|
|
|
|
|
.find_map(|o| {
|
|
|
|
|
if let acadrust::objects::ObjectType::Layout(l) = o {
|
|
|
|
|
(l.name == "Model" && !l.block_record.is_null()).then_some(l.block_record)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.or_else(|| doc.block_records.get("*Model_Space").map(|br| br.handle))
|
|
|
|
|
.expect("fresh document has a model-space block record");
|
|
|
|
|
let mut top = Insert::new("Fixture", Vector3::new(0.0, 0.0, 0.0));
|
|
|
|
|
top.common.owner_handle = model_br;
|
|
|
|
|
doc.add_entity(EntityType::Insert(top)).unwrap();
|
|
|
|
|
|
|
|
|
|
doc
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The nested-INSERT's block name inside a defined block record.
|
|
|
|
|
fn nested_insert_target(
|
|
|
|
|
doc: &acadrust::CadDocument,
|
|
|
|
|
block: &str,
|
|
|
|
|
) -> Option<String> {
|
|
|
|
|
let br = doc.block_records.get(block)?;
|
|
|
|
|
br.entity_handles.iter().find_map(|h| match doc.get_entity(*h)? {
|
|
|
|
|
EntityType::Insert(ins) => Some(ins.block_name.clone()),
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A foreign document whose model space INSERTs a `Fixture` block whose
|
|
|
|
|
/// definition INSERTs `Door (2)`, whose definition in turn INSERTs `Door`.
|
|
|
|
|
/// The file therefore carries *both* `Door` and `Door (2)` as nested deps.
|
|
|
|
|
fn doubly_nested_foreign_doc(app: &OpenCADStudio) -> acadrust::CadDocument {
|
|
|
|
|
use acadrust::entities::Insert;
|
|
|
|
|
use acadrust::tables::BlockRecord;
|
|
|
|
|
use acadrust::Handle;
|
|
|
|
|
let mut doc = foreign_doc(app);
|
|
|
|
|
|
|
|
|
|
let door_h = Handle::new(doc.next_handle());
|
|
|
|
|
let mut door_br = BlockRecord::new("Door");
|
|
|
|
|
door_br.handle = door_h;
|
|
|
|
|
doc.block_records.add(door_br).unwrap();
|
|
|
|
|
let mut door_line = Line::new();
|
|
|
|
|
door_line.start = Vector3::new(0.0, 0.0, 0.0);
|
|
|
|
|
door_line.end = Vector3::new(5.0, 0.0, 0.0);
|
|
|
|
|
door_line.common.owner_handle = door_h;
|
|
|
|
|
doc.add_entity(EntityType::Line(door_line)).unwrap();
|
|
|
|
|
|
|
|
|
|
let door2_h = Handle::new(doc.next_handle());
|
|
|
|
|
let mut door2_br = BlockRecord::new("Door (2)");
|
|
|
|
|
door2_br.handle = door2_h;
|
|
|
|
|
doc.block_records.add(door2_br).unwrap();
|
|
|
|
|
let mut mid = Insert::new("Door", Vector3::ZERO);
|
|
|
|
|
mid.common.owner_handle = door2_h;
|
|
|
|
|
doc.add_entity(EntityType::Insert(mid)).unwrap();
|
|
|
|
|
|
|
|
|
|
let fixture_h = Handle::new(doc.next_handle());
|
|
|
|
|
let mut fixture_br = BlockRecord::new("Fixture");
|
|
|
|
|
fixture_br.handle = fixture_h;
|
|
|
|
|
doc.block_records.add(fixture_br).unwrap();
|
|
|
|
|
let mut nested = Insert::new("Door (2)", Vector3::new(2.0, 0.0, 0.0));
|
|
|
|
|
nested.common.owner_handle = fixture_h;
|
|
|
|
|
doc.add_entity(EntityType::Insert(nested)).unwrap();
|
|
|
|
|
|
|
|
|
|
let model_br = doc
|
|
|
|
|
.objects
|
|
|
|
|
.values()
|
|
|
|
|
.find_map(|o| {
|
|
|
|
|
if let acadrust::objects::ObjectType::Layout(l) = o {
|
|
|
|
|
(l.name == "Model" && !l.block_record.is_null()).then_some(l.block_record)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.or_else(|| doc.block_records.get("*Model_Space").map(|br| br.handle))
|
|
|
|
|
.expect("fresh document has a model-space block record");
|
|
|
|
|
let mut top = Insert::new("Fixture", Vector3::new(0.0, 0.0, 0.0));
|
|
|
|
|
top.common.owner_handle = model_br;
|
|
|
|
|
doc.add_entity(EntityType::Insert(top)).unwrap();
|
|
|
|
|
|
|
|
|
|
doc
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn import_reserves_source_names_so_nested_deps_cannot_collide() {
|
|
|
|
|
let mut app = fresh();
|
|
|
|
|
let i = app.active_tab;
|
|
|
|
|
// Source file itself carries "Door" and "Door (2)"; destination already
|
|
|
|
|
// has "Door". The imported "Door" must land on "Door (3)" — NOT steal
|
|
|
|
|
// "Door (2)", which the source file reserves for its own definition.
|
|
|
|
|
let doc = doubly_nested_foreign_doc(&app);
|
|
|
|
|
let mut dest_door = Line::new();
|
|
|
|
|
dest_door.start = Vector3::new(0.0, 0.0, 0.0);
|
|
|
|
|
dest_door.end = Vector3::new(3.0, 3.0, 0.0);
|
|
|
|
|
app.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.define_block_from_owned_entities(
|
|
|
|
|
vec![EntityType::Line(dest_door)],
|
|
|
|
|
"Door",
|
|
|
|
|
glam::DVec3::ZERO,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let _ = app.import_document_as_block(doc, "Imported".to_string()).unwrap();
|
|
|
|
|
let doc = &app.tabs[i].scene.document;
|
|
|
|
|
// The file's own "Door (2)" is kept intact and targets the file's
|
|
|
|
|
// renamed "Door" (now "Door (3)" — "Door (2)" was taken by the source).
|
|
|
|
|
assert_eq!(
|
|
|
|
|
nested_insert_target(doc, "Door (2)"),
|
|
|
|
|
Some("Door (3)".to_string())
|
|
|
|
|
);
|
|
|
|
|
// The file's plain "Door" got bumped to "Door (3)" so both stay distinct.
|
|
|
|
|
assert!(
|
|
|
|
|
doc.block_records.get("Door").is_some(),
|
|
|
|
|
"destination Door preserved"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
doc.block_records.get("Door (2)").is_some(),
|
|
|
|
|
"source Door (2) preserved under its own name"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
doc.block_records.get("Door (3)").is_some(),
|
|
|
|
|
"source Door renamed to Door (3), not Door (2)"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
nested_insert_target(doc, "Fixture"),
|
|
|
|
|
Some("Door (2)".to_string())
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn import_nested_block_collision_preserves_both_definitions() {
|
|
|
|
|
let mut app = fresh();
|
|
|
|
|
let i = app.active_tab;
|
|
|
|
|
// Build the foreign document from the pristine scene first, so its
|
|
|
|
|
// nested "Door" is genuinely distinct from the destination's.
|
|
|
|
|
let doc = nested_foreign_doc(&app);
|
|
|
|
|
// Destination drawing already has its own, unrelated "Door" block.
|
|
|
|
|
let mut dest_door = Line::new();
|
|
|
|
|
dest_door.start = Vector3::new(0.0, 0.0, 0.0);
|
|
|
|
|
dest_door.end = Vector3::new(3.0, 3.0, 0.0);
|
|
|
|
|
app.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.define_block_from_owned_entities(
|
|
|
|
|
vec![EntityType::Line(dest_door)],
|
|
|
|
|
"Door",
|
|
|
|
|
glam::DVec3::ZERO,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let name = app.import_document_as_block(doc, "Imported".to_string()).unwrap();
|
|
|
|
|
assert_eq!(name, "Imported");
|
|
|
|
|
|
|
|
|
|
let doc = &app.tabs[i].scene.document;
|
|
|
|
|
// Both definitions survive: the destination's original and the imported one.
|
|
|
|
|
assert!(
|
|
|
|
|
doc.block_records.get("Door").is_some(),
|
|
|
|
|
"destination's own Door must be preserved"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
doc.block_records.get("Door (2)").is_some(),
|
|
|
|
|
"imported nested Door must be renamed to Door (2)"
|
|
|
|
|
);
|
|
|
|
|
// The imported Fixture's nested INSERT must point at the imported Door (2),
|
|
|
|
|
// not silently resolve to the destination's unrelated Door.
|
|
|
|
|
assert_eq!(
|
|
|
|
|
nested_insert_target(doc, "Fixture"),
|
|
|
|
|
Some("Door (2)".to_string()),
|
|
|
|
|
"Fixture must reference the renamed imported Door, not the destination's"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
nested_insert_target(doc, "Door (2)"),
|
|
|
|
|
None,
|
|
|
|
|
"imported Door (2) has no nested INSERTs"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn blockpalette_refresh_lists_and_places_block() {
|
|
|
|
|
let mut app = fresh();
|
|
|
|
|
let doc = foreign_doc(&app);
|
|
|
|
|
let name = app.import_document_as_block(doc, "Fixture".to_string()).unwrap();
|
|
|
|
|
app.refresh_block_palette();
|
|
|
|
|
assert!(app.block_palette.blocks.iter().any(|b| b.name == "Fixture"));
|
|
|
|
|
app.start_block_placement(&name);
|
|
|
|
|
let cmd = app.tabs[app.active_tab].active_cmd.as_ref().expect("INSERT running");
|
|
|
|
|
assert_eq!(cmd.name(), "INSERT");
|
|
|
|
|
assert_eq!(app.block_palette.placing.as_deref(), Some("Fixture"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn blockpalette_collapse_and_close_toggle_state() {
|
|
|
|
|
let mut app = fresh();
|
|
|
|
|
app.show_block_palette = true;
|
|
|
|
|
app.block_palette_expanded = true;
|
|
|
|
|
let _ = app.on_block_palette(BlockPaletteMsg::ToggleBar);
|
|
|
|
|
assert!(!app.block_palette_expanded, "collapse hides the panel body");
|
|
|
|
|
let _ = app.on_block_palette(BlockPaletteMsg::ToggleBar);
|
|
|
|
|
assert!(app.block_palette_expanded, "bar click re-expands the panel");
|
|
|
|
|
let _ = app.on_block_palette(BlockPaletteMsg::Close);
|
|
|
|
|
assert!(!app.show_block_palette, "close dismisses the sidebar");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn block_name_from_file_avoids_collisions() {
|
|
|
|
|
let mut app = fresh();
|
|
|
|
|
let i = app.active_tab;
|
|
|
|
|
let mut line = Line::new();
|
|
|
|
|
line.start = Vector3::ZERO;
|
|
|
|
|
line.end = Vector3::new(1.0, 0.0, 0.0);
|
|
|
|
|
app.tabs[i]
|
|
|
|
|
.scene
|
|
|
|
|
.define_block_from_owned_entities(vec![EntityType::Line(line)], "Chair", glam::DVec3::ZERO)
|
|
|
|
|
.unwrap();
|
|
|
|
|
assert_eq!(app.block_name_from_file("Chair"), "Chair (2)");
|
|
|
|
|
assert_eq!(app.block_name_from_file("Table"), "Table");
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
}
|