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
|
|
|
|
//! `file` arms and helpers, split out of the original `update.rs` (#mechanical decomposition).
|
|
|
|
|
|
|
|
|
|
|
|
#![allow(unused_imports)]
|
|
|
|
|
|
use super::util::*;
|
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::{
|
|
|
|
|
|
ortho_constrain, parse_coord, polar_constrain_near, ucs_rotate_vec, ucs_to_wcs, ucs_z_axis,
|
|
|
|
|
|
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 {
|
2026-07-03 15:43:37 +03:00
|
|
|
|
/// Before a save, give every cached truck solid that still has no ACIS
|
|
|
|
|
|
/// geometry (EXTRUDE/REVOLVE/SWEEP/LOFT/boolean results) an exact modeler
|
|
|
|
|
|
/// body derived from its truck B-rep, so the written DWG/DXF carries real
|
|
|
|
|
|
/// 3-D geometry other CAD apps can open instead of an empty data stream.
|
|
|
|
|
|
/// Curved solids that the exact planar path can't yet express are left
|
|
|
|
|
|
/// untouched (handled by the NURBS path).
|
|
|
|
|
|
#[cfg(feature = "solid3d")]
|
|
|
|
|
|
fn sync_truck_solids_to_acis(&mut self, i: usize) {
|
|
|
|
|
|
use acadrust::EntityType;
|
|
|
|
|
|
let scene = &mut self.tabs[i].scene;
|
|
|
|
|
|
let targets: Vec<acadrust::Handle> = scene
|
|
|
|
|
|
.solid_models
|
|
|
|
|
|
.keys()
|
|
|
|
|
|
.copied()
|
|
|
|
|
|
.filter(|h| {
|
|
|
|
|
|
matches!(
|
|
|
|
|
|
scene.document.get_entity(*h),
|
|
|
|
|
|
Some(EntityType::Solid3D(s)) if !s.acis_data.has_data()
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
for h in targets {
|
|
|
|
|
|
// Build the SAT while borrowing solid_models; the returned document
|
|
|
|
|
|
// is owned, so the borrow ends before we mutate the entity.
|
|
|
|
|
|
let sat = scene
|
|
|
|
|
|
.solid_models
|
|
|
|
|
|
.get(&h)
|
|
|
|
|
|
.and_then(crate::scene::convert::acis_export::planar_solid_to_sat);
|
|
|
|
|
|
if let Some(sat) = sat {
|
|
|
|
|
|
if let Some(EntityType::Solid3D(s)) = scene.document.get_entity_mut(h) {
|
|
|
|
|
|
s.set_sat_document(&sat);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(feature = "solid3d"))]
|
|
|
|
|
|
fn sync_truck_solids_to_acis(&mut self, _i: usize) {}
|
|
|
|
|
|
|
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
|
|
|
|
/// Snapshot the persisted UI preferences from live state.
|
|
|
|
|
|
pub(in crate::app) fn current_settings(&self) -> crate::app::settings::UserSettings {
|
|
|
|
|
|
crate::app::settings::UserSettings {
|
|
|
|
|
|
dyn_input: self.dyn_input,
|
|
|
|
|
|
ortho: self.ortho_mode,
|
|
|
|
|
|
polar: self.polar_mode,
|
|
|
|
|
|
polar_increment_deg: self.polar_increment_deg,
|
|
|
|
|
|
snap_enabled: self.snapper.snap_enabled,
|
|
|
|
|
|
otrack: self.snapper.otrack_enabled,
|
|
|
|
|
|
snap_modes: crate::app::settings::UserSettings::modes_from(self.snapper.enabled.iter()),
|
|
|
|
|
|
default_assoc_prompted: self.default_assoc_prompted,
|
|
|
|
|
|
disabled_plugins: {
|
|
|
|
|
|
let mut v: Vec<String> = self.disabled_plugins.iter().cloned().collect();
|
|
|
|
|
|
v.sort();
|
|
|
|
|
|
v
|
|
|
|
|
|
},
|
|
|
|
|
|
plugin_repos: self.plugin_repos.clone(),
|
|
|
|
|
|
texteditmode: self.texteditmode,
|
2026-06-29 12:49:02 +03:00
|
|
|
|
backup_on_save: self.backup_on_save,
|
2026-06-29 12:58:23 +03:00
|
|
|
|
file_assoc_enabled: self.file_assoc_enabled,
|
2026-06-29 13:17:40 +03:00
|
|
|
|
savetime_min: self.savetime_min,
|
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
|
|
|
|
bg_color: self.default_bg_color.map(f4_to_u3),
|
|
|
|
|
|
paper_bg_color: self.default_paper_bg_color.map(f4_to_u3),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Apply restored preferences to live state.
|
|
|
|
|
|
pub(in crate::app) fn apply_settings(&mut self, s: &crate::app::settings::UserSettings) {
|
|
|
|
|
|
self.dyn_input = s.dyn_input;
|
|
|
|
|
|
self.ortho_mode = s.ortho;
|
|
|
|
|
|
self.polar_mode = s.polar;
|
|
|
|
|
|
self.polar_increment_deg = s.polar_increment_deg;
|
|
|
|
|
|
self.snapper.snap_enabled = s.snap_enabled;
|
|
|
|
|
|
self.snapper.otrack_enabled = s.otrack;
|
|
|
|
|
|
self.snapper.enabled = s.snap_modes.iter().copied().collect();
|
|
|
|
|
|
self.default_assoc_prompted = s.default_assoc_prompted;
|
|
|
|
|
|
self.disabled_plugins = s.disabled_plugins.iter().cloned().collect();
|
|
|
|
|
|
self.plugin_repos = s.plugin_repos.clone();
|
|
|
|
|
|
self.texteditmode = s.texteditmode;
|
2026-06-29 12:49:02 +03:00
|
|
|
|
self.backup_on_save = s.backup_on_save;
|
2026-06-29 12:58:23 +03:00
|
|
|
|
self.file_assoc_enabled = s.file_assoc_enabled;
|
2026-06-29 13:17:40 +03:00
|
|
|
|
self.savetime_min = s.savetime_min;
|
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.default_bg_color = s.bg_color.map(u3_to_f4);
|
|
|
|
|
|
self.default_paper_bg_color = s.paper_bg_color.map(u3_to_f4);
|
|
|
|
|
|
// Push the restored background onto every drawing tab that exists now
|
|
|
|
|
|
// (the start tab and any initial drawing). Tabs created later pick it
|
|
|
|
|
|
// up via `apply_bg_default` at their construction site.
|
|
|
|
|
|
for idx in 0..self.tabs.len() {
|
|
|
|
|
|
self.apply_bg_default(idx);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.rebuild_ribbon_modules();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Apply the persisted default background(s) to tab `idx`. No-op for the
|
|
|
|
|
|
/// start tab or when no default is set. Refreshes the tab's cached wires
|
|
|
|
|
|
/// and meshes so background-adaptive colours pick up the change.
|
|
|
|
|
|
pub(in crate::app) fn apply_bg_default(&mut self, idx: usize) {
|
|
|
|
|
|
let bg = self.default_bg_color;
|
|
|
|
|
|
let paper_bg = self.default_paper_bg_color;
|
|
|
|
|
|
if bg.is_none() && paper_bg.is_none() {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
let tab = &mut self.tabs[idx];
|
|
|
|
|
|
if tab.is_start {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(c) = bg {
|
|
|
|
|
|
tab.bg_color = Some(c);
|
|
|
|
|
|
tab.scene.bg_color = c;
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(c) = paper_bg {
|
|
|
|
|
|
tab.paper_bg_color = Some(c);
|
|
|
|
|
|
tab.scene.paper_bg_color = c;
|
|
|
|
|
|
}
|
|
|
|
|
|
tab.scene.recolor_meshes();
|
|
|
|
|
|
tab.scene.bump_geometry();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Check if a suspended command exists on the active tab and resume it
|
|
|
|
|
|
/// with the outcome of the text editor.
|
|
|
|
|
|
pub(in crate::app) fn post_editor_closed(&mut self, committed: bool) -> Task<Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if let Some(mut cmd) = self.tabs[i].suspended_cmd.take() {
|
|
|
|
|
|
let res = cmd.on_editor_closed(committed);
|
|
|
|
|
|
self.tabs[i].active_cmd = Some(cmd);
|
|
|
|
|
|
self.apply_cmd_result(res)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Rebuild the ribbon's tab list from the registry, dropping the tabs of any
|
|
|
|
|
|
/// disabled plugins. Call after `disabled_plugins` changes.
|
|
|
|
|
|
pub(in crate::app) fn rebuild_ribbon_modules(&mut self) {
|
|
|
|
|
|
let modules =
|
|
|
|
|
|
crate::plugin::ribbon_modules_enabled(&self.disabled_plugins);
|
|
|
|
|
|
self.ribbon.set_modules(modules);
|
2026-07-04 23:01:53 +03:00
|
|
|
|
// Refresh the command-line autocomplete pool so a newly enabled plugin's
|
|
|
|
|
|
// commands become typeable (and a disabled one's drop out). This runs on
|
|
|
|
|
|
// startup load, settings reload, and every enable/disable toggle (#272).
|
|
|
|
|
|
self.command_line.dynamic_commands =
|
|
|
|
|
|
crate::plugin::plugin_command_names(&self.disabled_plugins);
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Snapshot of disabled plugin ids — lets the registry skip them while it
|
|
|
|
|
|
/// holds a `&mut` borrow of the app via `HostSession`.
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub(crate) fn disabled_plugin_ids(&self) -> rustc_hash::FxHashSet<String> {
|
|
|
|
|
|
self.disabled_plugins.clone()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Background task: fetch the curated plugin registry.
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub(in crate::app) fn fetch_registry_task(&self) -> Task<Message> {
|
|
|
|
|
|
Task::perform(
|
|
|
|
|
|
async { crate::plugin::marketplace::fetch_registry() },
|
|
|
|
|
|
Message::PluginRegistryFetched,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Background task: fetch `owner/repo`'s installable release tags.
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub(in crate::app) fn fetch_releases_task(&self, repo: String) -> Task<Message> {
|
|
|
|
|
|
let label = repo.clone();
|
|
|
|
|
|
Task::perform(
|
|
|
|
|
|
async move {
|
|
|
|
|
|
crate::plugin::marketplace::fetch_releases(&repo).map(|rs| {
|
|
|
|
|
|
rs.into_iter()
|
|
|
|
|
|
.filter(|r| r.installable())
|
|
|
|
|
|
.map(|r| r.tag)
|
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
move |res| Message::PluginReleasesFetched(label, res),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
pub(in crate::app) fn fetch_releases_task(&self, _repo: String) -> Task<Message> {
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Background task: download and install the `tag` release of `owner/repo`.
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub(in crate::app) fn install_task(&self, repo: String, tag: String) -> Task<Message> {
|
|
|
|
|
|
Task::perform(
|
|
|
|
|
|
async move {
|
|
|
|
|
|
let releases = crate::plugin::marketplace::fetch_releases(&repo)?;
|
|
|
|
|
|
let rel = releases
|
|
|
|
|
|
.into_iter()
|
|
|
|
|
|
.find(|r| r.tag == tag)
|
|
|
|
|
|
.ok_or_else(|| format!("release {tag} not found"))?;
|
|
|
|
|
|
crate::plugin::marketplace::install(&rel)
|
|
|
|
|
|
},
|
|
|
|
|
|
Message::PluginInstalled,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
pub(in crate::app) fn install_task(&self, _repo: String, _tag: String) -> Task<Message> {
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Write preferences to disk only when they differ from the last write,
|
|
|
|
|
|
/// so a toggle persists immediately without thrashing the file.
|
|
|
|
|
|
pub(in crate::app) fn persist_settings_if_changed(&mut self) {
|
|
|
|
|
|
let cur = self.current_settings();
|
|
|
|
|
|
if self.last_saved_settings.as_ref() != Some(&cur) {
|
|
|
|
|
|
cur.save();
|
|
|
|
|
|
self.last_saved_settings = Some(cur);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Record that the one-time default-association prompt has been answered and
|
|
|
|
|
|
/// flush it to disk, so the dialog never reappears on later launches.
|
|
|
|
|
|
pub(in crate::app) fn mark_assoc_prompted(&mut self) {
|
|
|
|
|
|
self.default_assoc_prompted = true;
|
|
|
|
|
|
self.persist_settings_if_changed();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
|
|
|
|
|
// Native: pick a path, then load on a worker thread. Web: the
|
|
|
|
|
|
// browser hands back bytes, so pick + parse in one step and feed
|
|
|
|
|
|
// the shared `FileOpened` handler directly.
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
{
|
|
|
|
|
|
Task::perform(crate::io::pick_open_path(), Message::OpenPathPicked)
|
|
|
|
|
|
}
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
{
|
|
|
|
|
|
// `FileOpened` only installs the result when an open is in
|
|
|
|
|
|
// progress, so mark one. The browser picker + parse happen
|
|
|
|
|
|
// inside `pick_and_load_web`; the real name is unknown until
|
|
|
|
|
|
// then, so show a generic label meanwhile.
|
|
|
|
|
|
self.opening = Some(crate::app::OpenProgress {
|
|
|
|
|
|
name: "Opening…".into(),
|
|
|
|
|
|
size_bytes: 0,
|
|
|
|
|
|
phase: std::sync::Arc::new(std::sync::atomic::AtomicU8::new(
|
|
|
|
|
|
crate::app::OPEN_PHASE_READING,
|
|
|
|
|
|
)),
|
|
|
|
|
|
started: Instant::now(),
|
|
|
|
|
|
});
|
|
|
|
|
|
Task::perform(crate::io::pick_and_load_web(), Message::FileOpened)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_file_opened(&mut self, name: String, path: std::path::PathBuf, doc: acadrust::CadDocument, caches: crate::scene::DerivedCaches) -> Task<Message> {
|
|
|
|
|
|
// If the user clicked Cancel while the parser was running, the
|
|
|
|
|
|
// overlay state was cleared and we silently drop the result.
|
|
|
|
|
|
if self.opening.is_none() {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
let open_started = self.opening.take().map(|p| p.started);
|
|
|
|
|
|
let timings = caches.timings;
|
|
|
|
|
|
let entity_count = doc.entities().count();
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("Opened \"{name}\" — {entity_count} entities"));
|
|
|
|
|
|
if caches.corrupt_dropped > 0 {
|
|
|
|
|
|
self.command_line.push_error(&format!(
|
|
|
|
|
|
"Warning: {} corrupt entities dropped (parser junk — bad normals / counts)",
|
|
|
|
|
|
caches.corrupt_dropped
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
feat(ui): replace app-menu logo with a quick-access file toolbar
Remove the OCS-logo button and the dropdown menu it opened; the top strip
now shows direct New / Open / Save / Save As / Print buttons ahead of the
undo/redo controls. Drop the HELP and OPTIONS commands (HELP dumped text to
the command line, OPTIONS aliased the drafting-settings popup).
The app-menu overlay is gone, so its rendering code, MENU_ITEMS, four dead
messages, and the LOGO_RED / GEAR / HELP assets are removed. The recents
list it held is the only survivor — moved into OpenCADStudio.recent_files
via a new app/recent.rs, backing the Start page's Recent Documents panel.
Quick-access icons are redrawn in a cleaner style (file-plus, folder-open,
device-floppy, file-export, printer) and tinted light so they read on the
dark strip; FOLDER / DOC keep their old glyphs for the file browser.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:51:49 +03:00
|
|
|
|
self.push_recent(path.clone());
|
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
|
|
|
|
|
|
|
|
|
|
let current_is_empty = {
|
|
|
|
|
|
let t = &self.tabs[self.active_tab];
|
|
|
|
|
|
!t.is_start
|
|
|
|
|
|
&& 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 = crate::app::document::DocumentTab::new_drawing(self.tab_counter);
|
|
|
|
|
|
self.tabs.push(new_tab);
|
|
|
|
|
|
let idx = self.tabs.len() - 1;
|
|
|
|
|
|
self.active_tab = idx;
|
|
|
|
|
|
self.apply_bg_default(idx);
|
|
|
|
|
|
idx
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
self.tabs[i].current_path = Some(path.clone());
|
|
|
|
|
|
self.tabs[i].scene.document = doc;
|
|
|
|
|
|
// Follow the file's saved current UCS from the moment it opens.
|
|
|
|
|
|
self.tabs[i].adopt_active_ucs_from_header();
|
|
|
|
|
|
// Route shared CJK ideographs to the language matching this
|
|
|
|
|
|
// drawing's code page (web per-language font split). Drop the
|
|
|
|
|
|
// glyph cache if it changed so Han re-resolves to the new
|
|
|
|
|
|
// language's font; geometry is (re)built below regardless. (#141)
|
|
|
|
|
|
if crate::scene::text::web_font::set_cjk_lang_from_codepage(
|
|
|
|
|
|
&self.tabs[i].scene.document.header.code_page,
|
|
|
|
|
|
) {
|
|
|
|
|
|
crate::scene::text::ttf_glyph::clear_fallback_cache();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Current model-space annotation scale comes from the drawing's
|
|
|
|
|
|
// CANNOSCALEVALUE (paper/drawing factor); the multiplier we use
|
|
|
|
|
|
// for text/dim sizing is its inverse (1:50 -> 0.02 -> 50.0).
|
|
|
|
|
|
let cannoscale_value = self.tabs[i].scene.document.header.annotation_scale_value;
|
|
|
|
|
|
self.tabs[i].scene.annotation_scale = if cannoscale_value > 1e-9 {
|
|
|
|
|
|
(1.0 / cannoscale_value) as f32
|
|
|
|
|
|
} else {
|
|
|
|
|
|
1.0
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Auto-resolve XREFs relative to the opened file's directory.
|
|
|
|
|
|
let mut xref_ms = 0u32;
|
2026-07-02 10:20:24 +03:00
|
|
|
|
let mut xref_merged = 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 let Some(base_dir) = path.parent() {
|
|
|
|
|
|
// xref content arrives un-purged: parser-garbage entities
|
|
|
|
|
|
// inside the referenced file can trigger infinite loops in
|
|
|
|
|
|
// tessellation. `resolve_xrefs` runs the corrupt-entity
|
|
|
|
|
|
// guard inline as it merges each xref, so no second
|
|
|
|
|
|
// full-document walk is needed here.
|
|
|
|
|
|
let t_xref = Instant::now();
|
|
|
|
|
|
let (xrefs, extra_dropped) =
|
|
|
|
|
|
crate::io::xref::resolve_xrefs(&mut self.tabs[i].scene.document, base_dir);
|
|
|
|
|
|
xref_ms = t_xref.elapsed().as_millis() as u32;
|
|
|
|
|
|
if extra_dropped > 0 {
|
|
|
|
|
|
self.command_line.push_error(&format!(
|
|
|
|
|
|
"Warning: {extra_dropped} corrupt xref entities dropped"
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
for info in &xrefs {
|
|
|
|
|
|
match info.status {
|
|
|
|
|
|
crate::io::xref::XrefStatus::Loaded => {
|
2026-07-02 10:20:24 +03:00
|
|
|
|
xref_merged = true;
|
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.command_line
|
|
|
|
|
|
.push_output(&format!("XREF Loaded \"{}\"", info.name));
|
|
|
|
|
|
}
|
|
|
|
|
|
crate::io::xref::XrefStatus::NotFound => {
|
|
|
|
|
|
self.command_line.push_error(&format!(
|
|
|
|
|
|
"XREF Not found: \"{}\" ({})",
|
|
|
|
|
|
info.name, info.path
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
crate::io::xref::XrefStatus::Unloaded => {
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
"XREF Unloaded (skipped): \"{}\"",
|
|
|
|
|
|
info.name
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Open-time breakdown so regressions are visible immediately.
|
|
|
|
|
|
// `total` is wall time from the Open click to here (post-xref,
|
|
|
|
|
|
// pre-first-frame); the phase figures are the background-thread
|
|
|
|
|
|
// parse/purge/cache spans plus the UI-thread xref resolve.
|
|
|
|
|
|
let total_ms = open_started
|
|
|
|
|
|
.map(|s| s.elapsed().as_millis() as u32)
|
|
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
" parse {}ms · purge {}ms · caches {}ms · xref {}ms · total {}ms",
|
|
|
|
|
|
timings.parse_ms, timings.purge_ms, timings.caches_ms, xref_ms, total_ms
|
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
|
|
// Caches were built on the background thread inside open_path().
|
|
|
|
|
|
self.tabs[i].scene.local_extent_max = caches.local_extent_max;
|
|
|
|
|
|
self.tabs[i].scene.local_center = caches.local_center;
|
|
|
|
|
|
self.tabs[i].scene.hatches = caches.hatches;
|
|
|
|
|
|
self.tabs[i].scene.images = caches.images;
|
|
|
|
|
|
self.tabs[i].scene.meshes = caches.meshes;
|
|
|
|
|
|
self.tabs[i].scene.block_meshes = caches.block_meshes;
|
|
|
|
|
|
// Invalidate the wire cache so the new document is tessellated.
|
|
|
|
|
|
self.tabs[i].scene.bump_geometry();
|
2026-07-02 10:20:24 +03:00
|
|
|
|
// XREFs are merged into the document AFTER the background worker
|
|
|
|
|
|
// built the mesh caches above, so those caches contain none of
|
|
|
|
|
|
// the xref'd geometry. The wire pass rebuilds from the document
|
|
|
|
|
|
// each frame (bump_geometry covers it), but 3D-solid meshes are
|
2026-07-05 00:38:38 +03:00
|
|
|
|
// only tessellated by populate — run the incremental variant so
|
|
|
|
|
|
// the already-cached host solids are kept and only the newly
|
|
|
|
|
|
// merged xref solids (walls, floors, roofs) are tessellated,
|
|
|
|
|
|
// avoiding a full re-tessellation of the whole drawing. (#203)
|
2026-07-02 10:20:24 +03:00
|
|
|
|
if xref_merged {
|
2026-07-05 00:38:38 +03:00
|
|
|
|
self.tabs[i].scene.populate_missing_meshes_from_document();
|
2026-07-02 10:20:24 +03:00
|
|
|
|
}
|
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.tabs[i].scene.selected = rustc_hash::FxHashSet::default();
|
|
|
|
|
|
self.tabs[i].scene.preview_wires = vec![];
|
|
|
|
|
|
self.tabs[i].scene.current_layout = "Model".to_string();
|
2026-06-26 23:06:40 +03:00
|
|
|
|
crate::io::linetypes::populate_document(&mut self.tabs[i].scene.document);
|
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.tabs[i].properties = PropertiesPanel::empty();
|
|
|
|
|
|
// Seed the current table / multileader style from the file's
|
|
|
|
|
|
// header so the ✓ marks the right one (text/dim/mline come from
|
|
|
|
|
|
// the document header directly). DXF provides these via
|
|
|
|
|
|
// $CTABLESTYLE / $CMLEADERSTYLE; DWG leaves them at "Standard".
|
|
|
|
|
|
self.ribbon.active_table_style = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.header
|
|
|
|
|
|
.current_table_style_name
|
|
|
|
|
|
.clone();
|
|
|
|
|
|
self.tabs[i].active_mleader_style = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.header
|
|
|
|
|
|
.current_mleader_style_name
|
|
|
|
|
|
.clone();
|
|
|
|
|
|
let doc_layers = self.tabs[i].scene.document.layers.clone();
|
|
|
|
|
|
let vp_info = self.tabs[i].scene.viewport_list();
|
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
|
.layers
|
|
|
|
|
|
.sync_with_viewports(&doc_layers, vp_info);
|
|
|
|
|
|
self.sync_ribbon_layers();
|
|
|
|
|
|
// Load the Annotate-ribbon style dropdowns (text / dimension /
|
|
|
|
|
|
// multileader / table) from the opened document instead of
|
|
|
|
|
|
// leaving them on the hard-coded "Standard" default.
|
|
|
|
|
|
self.sync_ribbon_styles();
|
|
|
|
|
|
// Reset the Home-ribbon Color / Linetype / Lineweight chips
|
|
|
|
|
|
// to the newly opened document's CECOLOR / CELTYPE / CELWEIGHT
|
|
|
|
|
|
// defaults (or to ByLayer when the file leaves them empty).
|
|
|
|
|
|
// Without this they stick to whatever the prior tab had
|
|
|
|
|
|
// selected — see #21.
|
|
|
|
|
|
self.sync_ribbon_from_selection();
|
|
|
|
|
|
self.tabs[i].scene.restore_saved_camera();
|
|
|
|
|
|
// Grid/snap are per-drawing view settings — adopt the opened
|
|
|
|
|
|
// file's active viewport state rather than a global preference.
|
|
|
|
|
|
self.adopt_view_display(i);
|
|
|
|
|
|
self.sync_render_mode_to_active_tile(i);
|
|
|
|
|
|
self.tabs[i].last_synced_camera_gen = self.tabs[i].scene.camera_generation;
|
|
|
|
|
|
self.tabs[i].dirty = false;
|
|
|
|
|
|
self.tabs[i].history = crate::app::document::HistoryState::default();
|
|
|
|
|
|
self.refresh_selected_grips();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_wblock_save_result_some(&mut self, block_name: String, path: std::path::PathBuf) -> Task<Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let result = if block_name == "*" {
|
|
|
|
|
|
let handles: Vec<_> = self.tabs[i].scene.selected.iter().copied().collect();
|
|
|
|
|
|
crate::modules::insert::wblock::extract_entities_to_doc(
|
|
|
|
|
|
&self.tabs[i].scene.document,
|
|
|
|
|
|
&handles,
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
crate::modules::insert::wblock::extract_block_to_doc(
|
|
|
|
|
|
&self.tabs[i].scene.document,
|
|
|
|
|
|
&block_name,
|
|
|
|
|
|
)
|
|
|
|
|
|
};
|
|
|
|
|
|
match result {
|
|
|
|
|
|
Ok(doc) => match crate::io::save(&doc, &path) {
|
|
|
|
|
|
Ok(()) => {
|
|
|
|
|
|
let fname = path
|
|
|
|
|
|
.file_name()
|
|
|
|
|
|
.map(|n| n.to_string_lossy().into_owned())
|
|
|
|
|
|
.unwrap_or_else(|| path.to_string_lossy().into_owned());
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"WBLOCK Saved \"{block_name}\" → \"{fname}\""
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(e) => self
|
|
|
|
|
|
.command_line
|
|
|
|
|
|
.push_error(&format!("WBLOCK save failed: {e}")),
|
|
|
|
|
|
},
|
|
|
|
|
|
Err(e) => self.command_line.push_error(&format!("WBLOCK: {e}")),
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_stl_export_path_some(&mut self, path: std::path::PathBuf) -> Task<Message> {
|
|
|
|
|
|
// Re-build STL bytes (we can't easily pass them through the message).
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
// STL gets the highest-resolution LOD (slot 0) so the
|
|
|
|
|
|
// exported geometry isn't downgraded by the view-dependent
|
|
|
|
|
|
// mesh LOD ladder used for rendering.
|
|
|
|
|
|
let meshes: Vec<crate::scene::model::mesh_model::MeshModel> = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.meshes
|
|
|
|
|
|
.values()
|
|
|
|
|
|
.filter_map(|s| s.lods.first().cloned())
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let mesh_refs: Vec<&crate::scene::model::mesh_model::MeshModel> = meshes.iter().collect();
|
|
|
|
|
|
match crate::io::stl::build_stl(&mesh_refs) {
|
|
|
|
|
|
Some(bytes) => match std::fs::write(&path, bytes) {
|
|
|
|
|
|
Ok(()) => self
|
|
|
|
|
|
.command_line
|
|
|
|
|
|
.push_output(&format!("STLOUT: exported to \"{}\"", path.display())),
|
|
|
|
|
|
Err(e) => self
|
|
|
|
|
|
.command_line
|
|
|
|
|
|
.push_error(&format!("STLOUT: write error: {e}")),
|
|
|
|
|
|
},
|
|
|
|
|
|
None => self
|
|
|
|
|
|
.command_line
|
|
|
|
|
|
.push_error("STLOUT: no mesh data to export."),
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_step_export_path_some(&mut self, path: std::path::PathBuf) -> Task<Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
// Export uses LOD 0 (full resolution); see StlExportPath above.
|
|
|
|
|
|
let meshes: Vec<crate::scene::model::mesh_model::MeshModel> = self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.meshes
|
|
|
|
|
|
.values()
|
|
|
|
|
|
.filter_map(|s| s.lods.first().cloned())
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let mesh_refs: Vec<&crate::scene::model::mesh_model::MeshModel> = meshes.iter().collect();
|
|
|
|
|
|
match crate::io::step::build_step(&mesh_refs) {
|
|
|
|
|
|
Some(text) => match std::fs::write(&path, text.as_bytes()) {
|
|
|
|
|
|
Ok(()) => self
|
|
|
|
|
|
.command_line
|
|
|
|
|
|
.push_output(&format!("STEPOUT: exported to \"{}\"", path.display())),
|
|
|
|
|
|
Err(e) => self
|
|
|
|
|
|
.command_line
|
|
|
|
|
|
.push_error(&format!("STEPOUT: write error: {e}")),
|
|
|
|
|
|
},
|
|
|
|
|
|
None => self
|
|
|
|
|
|
.command_line
|
|
|
|
|
|
.push_error("STEPOUT: no mesh data to export."),
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_obj_import_path_some(&mut self, path: std::path::PathBuf) -> Task<Message> {
|
|
|
|
|
|
let src = match std::fs::read_to_string(&path) {
|
|
|
|
|
|
Ok(s) => s,
|
|
|
|
|
|
Err(e) => {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error(&format!("IMPORTOBJ: read error: {e}"));
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
let color = [0.7f32, 0.7, 0.85, 1.0];
|
|
|
|
|
|
match crate::io::obj::parse_obj(&src, color) {
|
|
|
|
|
|
None => {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error("IMPORTOBJ: no usable geometry in file.");
|
|
|
|
|
|
}
|
|
|
|
|
|
Some(mut mesh) => {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let file_stem = path
|
|
|
|
|
|
.file_stem()
|
|
|
|
|
|
.map(|s| s.to_string_lossy().into_owned())
|
|
|
|
|
|
.unwrap_or_else(|| "obj_mesh".into());
|
|
|
|
|
|
mesh.name = file_stem.clone();
|
|
|
|
|
|
self.push_undo_snapshot(i, "IMPORTOBJ");
|
|
|
|
|
|
use crate::modules::insert::solid3d_cmds::empty_solid3d;
|
|
|
|
|
|
let entity = empty_solid3d();
|
|
|
|
|
|
let handle = self.tabs[i].scene.add_entity(entity);
|
|
|
|
|
|
if !handle.is_null() {
|
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.meshes
|
|
|
|
|
|
.insert(handle, crate::scene::MeshLodSet::from_single(mesh));
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
self.command_line.push_output(&format!(
|
|
|
|
|
|
"IMPORTOBJ: imported \"{}\" as mesh.",
|
|
|
|
|
|
file_stem
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_save_file(&mut self) -> Task<Message> {
|
|
|
|
|
|
if self.read_only {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error("Read-only session (--read-only): saving is disabled.");
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
// Stamp the live grid/snap toggles onto the VPort so the file
|
|
|
|
|
|
// reflects them even if they came from settings with no
|
|
|
|
|
|
// in-session toggle (#121).
|
|
|
|
|
|
self.sync_vport_display(i);
|
|
|
|
|
|
// Native: save straight to the known path. Web has no path
|
|
|
|
|
|
// (downloads instead), so always go through the Save dialog.
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
if let Some(path) = self.tabs[i].current_path.clone() {
|
|
|
|
|
|
self.tabs[i].scene.document.header.user_real1 =
|
|
|
|
|
|
self.tabs[i].scene.annotation_scale as f64;
|
2026-06-28 20:31:30 +03:00
|
|
|
|
// A direct Save preserves the document's current version.
|
2026-06-29 12:49:02 +03:00
|
|
|
|
if self.backup_on_save {
|
|
|
|
|
|
crate::io::write_backup(&path);
|
|
|
|
|
|
}
|
2026-07-03 15:43:37 +03:00
|
|
|
|
self.sync_truck_solids_to_acis(i);
|
2026-06-28 20:31:30 +03:00
|
|
|
|
match crate::io::save(&self.tabs[i].scene.document, &path) {
|
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
|
|
|
|
Ok(()) => {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("Saved: {}", path.display()));
|
|
|
|
|
|
self.tabs[i].dirty = false;
|
2026-06-29 13:34:16 +03:00
|
|
|
|
// A clean save supersedes any autosave recovery copy.
|
|
|
|
|
|
let _ = std::fs::remove_file(path.with_extension("sv$"));
|
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
|
|
|
|
}
|
|
|
|
|
|
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
|
|
|
|
|
|
}
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
self.save_dialog_for_unsaved = false;
|
|
|
|
|
|
self.open_save_dialog_window(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_save_dialog_confirm(&mut self) -> Task<Message> {
|
|
|
|
|
|
let (ext, version) = crate::io::parse_save_format(&self.save_dialog_format);
|
2026-06-29 11:47:37 +03:00
|
|
|
|
// Warn before a lossy Save-As that would drop unsupported
|
|
|
|
|
|
// (AEC / application) objects kept only as verbatim
|
|
|
|
|
|
// source-version bytes — let the user keep them by saving in the
|
|
|
|
|
|
// source version, or proceed and drop them.
|
|
|
|
|
|
if !self.aec_drop_acknowledged {
|
|
|
|
|
|
let is_dxf = ext.eq_ignore_ascii_case("dxf");
|
|
|
|
|
|
let n = crate::io::dropped_on_save_count(
|
|
|
|
|
|
&self.tabs[self.active_tab].scene.document,
|
|
|
|
|
|
version,
|
|
|
|
|
|
is_dxf,
|
|
|
|
|
|
);
|
|
|
|
|
|
if n > 0 {
|
|
|
|
|
|
self.aec_drop_count = n;
|
|
|
|
|
|
self.active_modal = Some(crate::app::ModalKind::AecDropWarning);
|
|
|
|
|
|
return 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
|
|
|
|
// The user need not type an extension: append the selected
|
|
|
|
|
|
// format's one when the entered name carries none.
|
|
|
|
|
|
let name = self.save_dialog_filename.trim();
|
|
|
|
|
|
let filename = if name.is_empty() {
|
|
|
|
|
|
format!("drawing.{ext}")
|
|
|
|
|
|
} else if std::path::Path::new(name).extension().is_none() {
|
|
|
|
|
|
format!("{name}.{ext}")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
name.to_string()
|
|
|
|
|
|
};
|
|
|
|
|
|
self.save_dialog_filename = filename.clone();
|
2026-06-29 11:47:37 +03:00
|
|
|
|
// Confirm before overwriting an existing file in the folder.
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
if !self.overwrite_acknowledged
|
|
|
|
|
|
&& self.save_dialog_folder.join(&filename).exists()
|
|
|
|
|
|
{
|
|
|
|
|
|
self.active_modal = Some(crate::app::ModalKind::OverwriteWarning);
|
|
|
|
|
|
return 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
|
|
|
|
let close = self.close_save_dialog_window();
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
sync_annotation_scale_header(&mut self.tabs[i].scene);
|
2026-07-03 15:43:37 +03:00
|
|
|
|
self.sync_truck_solids_to_acis(i);
|
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
|
|
|
|
|
|
|
|
|
|
// Native: write to the chosen path. Web: download the bytes
|
|
|
|
|
|
// under the chosen name (no filesystem).
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
{
|
|
|
|
|
|
let path = self.save_dialog_folder.join(&filename);
|
2026-06-29 12:49:02 +03:00
|
|
|
|
if self.backup_on_save {
|
|
|
|
|
|
crate::io::write_backup(&path);
|
|
|
|
|
|
}
|
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
|
|
|
|
match crate::io::save_as_version(&self.tabs[i].scene.document, &path, version) {
|
|
|
|
|
|
Ok(()) => {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("Saved: {}", path.display()));
|
2026-06-29 16:39:42 +03:00
|
|
|
|
// Drop any prior autosave copy — including the temp
|
|
|
|
|
|
// one used while the drawing was still unsaved —
|
|
|
|
|
|
// before the tab takes on its new path.
|
|
|
|
|
|
let _ = std::fs::remove_file(self.autosave_target(i));
|
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.tabs[i].current_path = Some(path.clone());
|
|
|
|
|
|
self.tabs[i].dirty = false;
|
2026-06-29 13:34:16 +03:00
|
|
|
|
let _ = std::fs::remove_file(path.with_extension("sv$"));
|
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.save_dialog_for_unsaved {
|
|
|
|
|
|
let next = self.update(Message::UnsavedPickedSavePath(Some(path)));
|
|
|
|
|
|
return Task::batch([close, next]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
|
|
|
|
|
|
}
|
|
|
|
|
|
close
|
|
|
|
|
|
}
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
{
|
|
|
|
|
|
match crate::io::save_to_bytes(&self.tabs[i].scene.document, ext, version) {
|
|
|
|
|
|
Ok(bytes) => {
|
|
|
|
|
|
crate::sys::download_bytes(&filename, &bytes);
|
|
|
|
|
|
self.tabs[i].dirty = false;
|
|
|
|
|
|
self.command_line.push_output(&format!("Saved: {filename}"));
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(e) => self.command_line.push_error(&format!("Save failed: {e}")),
|
|
|
|
|
|
}
|
|
|
|
|
|
// Continue a pending tab close.
|
|
|
|
|
|
if self.save_dialog_for_unsaved {
|
|
|
|
|
|
if let Some(crate::app::PendingClose::Tab(idx)) = self.pending_close.take() {
|
|
|
|
|
|
let cont = self.update(Message::TabClose(idx));
|
|
|
|
|
|
return Task::batch([close, cont]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
close
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 11:47:37 +03:00
|
|
|
|
/// AEC-drop warning → "Save anyway": accept the loss and proceed with the
|
|
|
|
|
|
/// format the user already chose.
|
|
|
|
|
|
pub(super) fn on_aec_drop_proceed(&mut self) -> Task<Message> {
|
|
|
|
|
|
self.aec_drop_acknowledged = true;
|
|
|
|
|
|
self.active_modal = Some(crate::app::ModalKind::SaveDialog);
|
|
|
|
|
|
self.on_save_dialog_confirm()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// AEC-drop warning → "Save in source version": switch the target to the
|
|
|
|
|
|
/// document's own DWG version (where the unsupported objects round-trip as
|
|
|
|
|
|
/// verbatim bytes), then save.
|
|
|
|
|
|
pub(super) fn on_aec_drop_same_version(&mut self) -> Task<Message> {
|
|
|
|
|
|
let src = self.tabs[self.active_tab].scene.document.version;
|
|
|
|
|
|
self.save_dialog_format = crate::io::format_for_version(src, false);
|
|
|
|
|
|
// Strip any extension (e.g. .dxf) so the confirm path appends .dwg.
|
|
|
|
|
|
let stem = std::path::Path::new(&self.save_dialog_filename)
|
|
|
|
|
|
.file_stem()
|
|
|
|
|
|
.map(|s| s.to_string_lossy().into_owned())
|
|
|
|
|
|
.unwrap_or_else(|| self.save_dialog_filename.clone());
|
|
|
|
|
|
self.save_dialog_filename = stem;
|
|
|
|
|
|
self.aec_drop_acknowledged = true;
|
|
|
|
|
|
self.active_modal = Some(crate::app::ModalKind::SaveDialog);
|
|
|
|
|
|
self.on_save_dialog_confirm()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Overwrite warning → "Replace": save over the existing file.
|
|
|
|
|
|
pub(super) fn on_overwrite_confirm(&mut self) -> Task<Message> {
|
|
|
|
|
|
self.overwrite_acknowledged = true;
|
|
|
|
|
|
self.active_modal = Some(crate::app::ModalKind::SaveDialog);
|
|
|
|
|
|
self.on_save_dialog_confirm()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 16:39:42 +03:00
|
|
|
|
/// Where the autosave recovery copy for tab `i` lives: beside a saved
|
|
|
|
|
|
/// drawing as `<file>.sv$`, or — for an unsaved drawing with no path yet —
|
|
|
|
|
|
/// under the system temp dir keyed by the tab's display name.
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub(in crate::app) fn autosave_target(&self, i: usize) -> std::path::PathBuf {
|
|
|
|
|
|
match &self.tabs[i].current_path {
|
|
|
|
|
|
Some(p) => p.with_extension("sv$"),
|
|
|
|
|
|
None => {
|
|
|
|
|
|
let safe: String = self.tabs[i]
|
|
|
|
|
|
.tab_display_name()
|
|
|
|
|
|
.chars()
|
|
|
|
|
|
.map(|c| if c.is_alphanumeric() { c } else { '_' })
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
std::env::temp_dir().join(format!("OpenCADStudio_{safe}.sv$"))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 13:17:40 +03:00
|
|
|
|
/// Periodic autosave (SAVETIME): write a `.sv$` recovery copy for every
|
2026-06-29 16:39:42 +03:00
|
|
|
|
/// dirty tab — beside the file if it's saved, else under the temp dir — at
|
|
|
|
|
|
/// the document's own DWG version. Best-effort and non-destructive: it never
|
|
|
|
|
|
/// touches the original file or the dirty flag.
|
2026-06-29 13:17:40 +03:00
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub(super) fn on_autosave(&mut self) -> Task<Message> {
|
|
|
|
|
|
let mut n = 0;
|
|
|
|
|
|
for i in 0..self.tabs.len() {
|
|
|
|
|
|
if !self.tabs[i].dirty {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
let version = self.tabs[i].scene.document.version;
|
|
|
|
|
|
if let Ok(bytes) =
|
|
|
|
|
|
crate::io::save_to_bytes(&self.tabs[i].scene.document, "dwg", version)
|
|
|
|
|
|
{
|
2026-06-29 16:39:42 +03:00
|
|
|
|
if std::fs::write(self.autosave_target(i), bytes).is_ok() {
|
2026-06-29 13:17:40 +03:00
|
|
|
|
n += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if n > 0 {
|
|
|
|
|
|
self.command_line
|
2026-06-29 16:39:42 +03:00
|
|
|
|
.push_output(&format!("Autosaved {n} drawing(s)"));
|
2026-06-29 13:17:40 +03:00
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
pub(super) fn on_autosave(&mut self) -> Task<Message> {
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 13:34:16 +03:00
|
|
|
|
/// Delete the `.sv$` autosave recovery files for all open drawings. They
|
|
|
|
|
|
/// exist only to survive a crash, so a clean save or exit removes them.
|
|
|
|
|
|
pub(in crate::app) fn cleanup_autosaves(&self) {
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
2026-06-29 16:39:42 +03:00
|
|
|
|
for i in 0..self.tabs.len() {
|
|
|
|
|
|
let _ = std::fs::remove_file(self.autosave_target(i));
|
2026-06-29 13:34:16 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Remove the autosave recovery files, then quit the application.
|
|
|
|
|
|
pub(in crate::app) fn exit_app(&self) -> Task<Message> {
|
|
|
|
|
|
self.cleanup_autosaves();
|
|
|
|
|
|
iced::exit()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(plot): unified plot/print dialog, retire Page Setup
Replace the old Page Setup panel with a single OCS-styled plot/print
dialog (dropdowns for printer, paper, orientation, rotation, plot area,
scale, quality, shaded plot; checkboxes for plot options). PLOT / PRINT
/ PAGESETUP all open it.
- Printer output via lp: -d printer, -n copies, -o ColorModel=Gray,
print-quality, Resolution; Windows printto verb; list_printers via
lpstat. Preview opens a temp PDF in the OS viewer (never a save
dialog). PDF export reuses the tested pipeline.
- Window pick returns to the new dialog (was the old Page Setup) and the
clipped window plot drives PDF / printer / preview.
- Remember the user's print preferences across sessions in
config/plot.txt (printer, copies, quality, output options, area,
scale); drawing-specific fields (offset/center/rotation) seed from the
layout. Default plot area Window, scale Fit.
- Delete src/ui/window/page_setup.rs and its ModalKind / Message /
buffer state; fold on_page_setup_commit into apply_plot_page_settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 10:27:54 +03:00
|
|
|
|
/// Write the given plot page settings into the active layout's Layout +
|
|
|
|
|
|
/// PlotSettings objects (paper size, plot area, offset, rotation, scale).
|
|
|
|
|
|
/// No-op on the Model tab (which has no paper layout). Marks the tab dirty
|
|
|
|
|
|
/// and re-tessellates the sheet. Called by the Plot dialog on commit.
|
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
|
|
pub(super) fn apply_plot_page_settings(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
w: f64,
|
|
|
|
|
|
h: f64,
|
|
|
|
|
|
extents: bool,
|
|
|
|
|
|
center: bool,
|
|
|
|
|
|
offset_x: f64,
|
|
|
|
|
|
offset_y: f64,
|
|
|
|
|
|
rotation: i16,
|
|
|
|
|
|
scale_str: &str,
|
|
|
|
|
|
) {
|
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
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let layout_name = self.tabs[i].scene.current_layout.clone();
|
|
|
|
|
|
if layout_name != "Model" {
|
feat(plot): unified plot/print dialog, retire Page Setup
Replace the old Page Setup panel with a single OCS-styled plot/print
dialog (dropdowns for printer, paper, orientation, rotation, plot area,
scale, quality, shaded plot; checkboxes for plot options). PLOT / PRINT
/ PAGESETUP all open it.
- Printer output via lp: -d printer, -n copies, -o ColorModel=Gray,
print-quality, Resolution; Windows printto verb; list_printers via
lpstat. Preview opens a temp PDF in the OS viewer (never a save
dialog). PDF export reuses the tested pipeline.
- Window pick returns to the new dialog (was the old Page Setup) and the
clipped window plot drives PDF / printer / preview.
- Remember the user's print preferences across sessions in
config/plot.txt (printer, copies, quality, output options, area,
scale); drawing-specific fields (offset/center/rotation) seed from the
layout. Default plot area Window, scale Fit.
- Delete src/ui/window/page_setup.rs and its ModalKind / Message /
buffer state; fold on_page_setup_commit into apply_plot_page_settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 10:27:54 +03:00
|
|
|
|
let w: f64 = w.max(1.0);
|
|
|
|
|
|
let h: f64 = h.max(1.0);
|
|
|
|
|
|
let plot_area = if extents { "Extents" } else { "Layout" };
|
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
|
|
|
|
|
|
|
|
|
|
// Update the Layout object's limits AND its embedded
|
|
|
|
|
|
// PlotSettings fields. `paper_limits()` (sheet rendering) and
|
|
|
|
|
|
// the DWG writer both read these from the Layout, so a page
|
|
|
|
|
|
// setup that only touched a side PlotSettings object would not
|
|
|
|
|
|
// reflect on screen or survive a save. The dialog's w/h are
|
|
|
|
|
|
// the final sheet dimensions, so store them verbatim with no
|
|
|
|
|
|
// further rotation swap (#156).
|
|
|
|
|
|
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);
|
|
|
|
|
|
l.paper_width = w;
|
|
|
|
|
|
l.paper_height = h;
|
|
|
|
|
|
l.plot_rotation = 0;
|
|
|
|
|
|
l.plot_paper_units = 1; // millimetres
|
|
|
|
|
|
l.plot_origin_x = offset_x;
|
|
|
|
|
|
l.plot_origin_y = offset_y;
|
|
|
|
|
|
// Custom dimensions no longer match a named size.
|
|
|
|
|
|
l.paper_size = String::new();
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Find or create the PlotSettings object for this layout.
|
|
|
|
|
|
use acadrust::objects::{
|
|
|
|
|
|
ObjectType, PlotPaperUnits, PlotRotation, PlotSettings, PlotType,
|
|
|
|
|
|
};
|
|
|
|
|
|
let plot_handle =
|
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.objects
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find_map(|(h, obj)| {
|
|
|
|
|
|
if let ObjectType::PlotSettings(ps) = obj {
|
|
|
|
|
|
if ps.page_name == layout_name {
|
|
|
|
|
|
Some(*h)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
let ps_entry = if let Some(h) = plot_handle {
|
|
|
|
|
|
self.tabs[i].scene.document.objects.get_mut(&h)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Create a new PlotSettings object and insert it.
|
|
|
|
|
|
let mut ps = PlotSettings::new(layout_name.clone());
|
|
|
|
|
|
ps.handle = self.tabs[i].scene.document.allocate_handle();
|
|
|
|
|
|
let h = ps.handle;
|
|
|
|
|
|
self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.document
|
|
|
|
|
|
.objects
|
|
|
|
|
|
.insert(h, ObjectType::PlotSettings(ps));
|
|
|
|
|
|
self.tabs[i].scene.document.objects.get_mut(&h)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(ObjectType::PlotSettings(ps)) = ps_entry {
|
|
|
|
|
|
ps.paper_width = w;
|
|
|
|
|
|
ps.paper_height = h;
|
|
|
|
|
|
ps.paper_units = PlotPaperUnits::Millimeters;
|
|
|
|
|
|
ps.plot_type = if plot_area == "Extents" {
|
|
|
|
|
|
PlotType::Extents
|
|
|
|
|
|
} else {
|
|
|
|
|
|
PlotType::Layout
|
|
|
|
|
|
};
|
|
|
|
|
|
ps.flags.plot_centered = center;
|
|
|
|
|
|
ps.origin_x = offset_x;
|
|
|
|
|
|
ps.origin_y = offset_y;
|
|
|
|
|
|
ps.rotation = match rotation {
|
|
|
|
|
|
90 => PlotRotation::Degrees90,
|
|
|
|
|
|
180 => PlotRotation::Degrees180,
|
|
|
|
|
|
270 => PlotRotation::Degrees270,
|
|
|
|
|
|
_ => PlotRotation::None,
|
|
|
|
|
|
};
|
|
|
|
|
|
// Apply plot scale.
|
|
|
|
|
|
use acadrust::objects::ScaledType;
|
feat(plot): unified plot/print dialog, retire Page Setup
Replace the old Page Setup panel with a single OCS-styled plot/print
dialog (dropdowns for printer, paper, orientation, rotation, plot area,
scale, quality, shaded plot; checkboxes for plot options). PLOT / PRINT
/ PAGESETUP all open it.
- Printer output via lp: -d printer, -n copies, -o ColorModel=Gray,
print-quality, Resolution; Windows printto verb; list_printers via
lpstat. Preview opens a temp PDF in the OS viewer (never a save
dialog). PDF export reuses the tested pipeline.
- Window pick returns to the new dialog (was the old Page Setup) and the
clipped window plot drives PDF / printer / preview.
- Remember the user's print preferences across sessions in
config/plot.txt (printer, copies, quality, output options, area,
scale); drawing-specific fields (offset/center/rotation) seed from the
layout. Default plot area Window, scale Fit.
- Delete src/ui/window/page_setup.rs and its ModalKind / Message /
buffer state; fold on_page_setup_commit into apply_plot_page_settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 10:27:54 +03:00
|
|
|
|
let (num, den) = parse_plot_scale(scale_str);
|
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 scale_str == "Fit" {
|
|
|
|
|
|
ps.set_scale_to_fit();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ps.scale_type = ScaledType::CustomScale;
|
|
|
|
|
|
ps.scale_numerator = num;
|
|
|
|
|
|
ps.scale_denominator = den;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.tabs[i].dirty = true;
|
|
|
|
|
|
// The paper sheet fill is cached; bump geometry so the new
|
|
|
|
|
|
// sheet size re-tessellates and shows immediately.
|
|
|
|
|
|
self.tabs[i].scene.bump_geometry();
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
"Page setup: {w:.1}×{h:.1} mm area={plot_area} \
|
|
|
|
|
|
center={center} rot={rotation}°"
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_plot_export_path_some(&mut self, path: std::path::PathBuf) -> Task<Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let scene = &self.tabs[i].scene;
|
|
|
|
|
|
let wires = scene.entity_wires();
|
|
|
|
|
|
let hatches = scene.paper_canvas_hatches();
|
|
|
|
|
|
let wipeouts = scene.paper_canvas_wipeouts();
|
|
|
|
|
|
|
|
|
|
|
|
// Read PlotSettings for current layout (if available).
|
|
|
|
|
|
use acadrust::objects::PlotType;
|
|
|
|
|
|
let ps_snap = scene.effective_plot_settings();
|
|
|
|
|
|
|
|
|
|
|
|
// Determine paper size and drawing offset.
|
|
|
|
|
|
let (paper_w, paper_h, mut draw_ox, mut draw_oy, rotation_deg) =
|
|
|
|
|
|
if let Some(((x0, y0), (x1, y1))) = scene.paper_limits() {
|
|
|
|
|
|
let (pw, ph) = (x1 - x0, y1 - y0);
|
|
|
|
|
|
|
|
|
|
|
|
// If PlotSettings says Extents, use model space extents instead.
|
|
|
|
|
|
let use_extents = ps_snap
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(|ps| matches!(ps.plot_type, PlotType::Extents))
|
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
|
|
|
|
|
|
let (ox, oy) = if use_extents {
|
|
|
|
|
|
if let Some((mn, _mx)) = scene.model_space_extents() {
|
|
|
|
|
|
(-mn.x as f64, -mn.y as f64)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
(-x0, -y0)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
(-x0, -y0)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let rot = ps_snap
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(|ps| ps.rotation.to_degrees() as i32)
|
|
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
|
|
|
|
|
|
|
(pw, ph, ox, oy, rot)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Model space: fit with 5% margin.
|
|
|
|
|
|
let margin = 1.05_f64;
|
|
|
|
|
|
if let Some((mn, mx)) = 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, 0)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
(297.0, 210.0, 0.0, 0.0, 0)
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Apply PlotSettings offset and centering.
|
|
|
|
|
|
if let Some(ref ps) = ps_snap {
|
|
|
|
|
|
if ps.flags.plot_centered {
|
|
|
|
|
|
// Centering: compute wire extents and re-centre.
|
|
|
|
|
|
let all_x: Vec<f32> = wires
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.flat_map(|w| w.points.iter().map(|p| p[0]))
|
|
|
|
|
|
.filter(|v| !v.is_nan())
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let all_y: Vec<f32> = wires
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.flat_map(|w| w.points.iter().map(|p| p[1]))
|
|
|
|
|
|
.filter(|v| !v.is_nan())
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
if let (Some(&min_x), Some(&max_x), Some(&min_y), Some(&max_y)) = (
|
|
|
|
|
|
all_x.iter().copied().reduce(f32::min).as_ref(),
|
|
|
|
|
|
all_x.iter().copied().reduce(f32::max).as_ref(),
|
|
|
|
|
|
all_y.iter().copied().reduce(f32::min).as_ref(),
|
|
|
|
|
|
all_y.iter().copied().reduce(f32::max).as_ref(),
|
|
|
|
|
|
) {
|
|
|
|
|
|
let cx = (min_x + max_x) as f64 / 2.0;
|
|
|
|
|
|
let cy = (min_y + max_y) as f64 / 2.0;
|
|
|
|
|
|
draw_ox += paper_w / 2.0 - cx;
|
|
|
|
|
|
draw_oy += paper_h / 2.0 - cy;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
draw_ox += ps.origin_x;
|
|
|
|
|
|
draw_oy += ps.origin_y;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// For rotation: swap paper dimensions and note angle for export.
|
|
|
|
|
|
let (eff_w, eff_h) = match rotation_deg {
|
|
|
|
|
|
90 | 270 => (paper_h, paper_w),
|
|
|
|
|
|
_ => (paper_w, paper_h),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
match crate::io::pdf_export::export_pdf(
|
|
|
|
|
|
&wires,
|
|
|
|
|
|
hatches.as_slice(),
|
|
|
|
|
|
wipeouts.as_slice(),
|
|
|
|
|
|
eff_w,
|
|
|
|
|
|
eff_h,
|
|
|
|
|
|
draw_ox as f32,
|
|
|
|
|
|
draw_oy as f32,
|
|
|
|
|
|
rotation_deg,
|
2026-07-05 23:12:52 +02:00
|
|
|
|
1.0,
|
|
|
|
|
|
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
|
|
|
|
&path,
|
|
|
|
|
|
self.active_plot_style.as_ref(),
|
|
|
|
|
|
) {
|
|
|
|
|
|
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-07-05 23:12:52 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Export the pending model-space plot window (set by PLOTWINDOW while on
|
|
|
|
|
|
/// the Model tab) to PDF, using the chosen paper size/orientation/scale.
|
|
|
|
|
|
pub(super) fn on_plot_window_export_path_some(&mut self, path: std::path::PathBuf) -> Task<Message> {
|
|
|
|
|
|
use crate::io::paper_sizes::{sheet_mm, window_to_sheet, PlotScale};
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if self.tabs[i].scene.current_layout != "Model" {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error("Plot window export is for model space.");
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
let Some((x0, y0, x1, y1)) = self.plot_window else {
|
|
|
|
|
|
self.command_line.push_error("No plot window. Pick one first.");
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
if (x1 - x0) < 1e-6 || (y1 - y0) < 1e-6 {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error("Plot window is empty. Pick a larger window.");
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
let (sheet_w, sheet_h) = sheet_mm(self.plot_format, self.plot_orientation);
|
|
|
|
|
|
let win_w = (x1 - x0).max(1e-9);
|
|
|
|
|
|
let win_h = (y1 - y0).max(1e-9);
|
feat(plot): unified plot/print dialog, retire Page Setup
Replace the old Page Setup panel with a single OCS-styled plot/print
dialog (dropdowns for printer, paper, orientation, rotation, plot area,
scale, quality, shaded plot; checkboxes for plot options). PLOT / PRINT
/ PAGESETUP all open it.
- Printer output via lp: -d printer, -n copies, -o ColorModel=Gray,
print-quality, Resolution; Windows printto verb; list_printers via
lpstat. Preview opens a temp PDF in the OS viewer (never a save
dialog). PDF export reuses the tested pipeline.
- Window pick returns to the new dialog (was the old Page Setup) and the
clipped window plot drives PDF / printer / preview.
- Remember the user's print preferences across sessions in
config/plot.txt (printer, copies, quality, output options, area,
scale); drawing-specific fields (offset/center/rotation) seed from the
layout. Default plot area Window, scale Fit.
- Delete src/ui/window/page_setup.rs and its ModalKind / Message /
buffer state; fold on_page_setup_commit into apply_plot_page_settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 10:27:54 +03:00
|
|
|
|
let scale_sel = if self.plot_scale.trim().eq_ignore_ascii_case("fit") {
|
2026-07-05 23:12:52 +02:00
|
|
|
|
PlotScale::Fit
|
|
|
|
|
|
} else {
|
feat(plot): unified plot/print dialog, retire Page Setup
Replace the old Page Setup panel with a single OCS-styled plot/print
dialog (dropdowns for printer, paper, orientation, rotation, plot area,
scale, quality, shaded plot; checkboxes for plot options). PLOT / PRINT
/ PAGESETUP all open it.
- Printer output via lp: -d printer, -n copies, -o ColorModel=Gray,
print-quality, Resolution; Windows printto verb; list_printers via
lpstat. Preview opens a temp PDF in the OS viewer (never a save
dialog). PDF export reuses the tested pipeline.
- Window pick returns to the new dialog (was the old Page Setup) and the
clipped window plot drives PDF / printer / preview.
- Remember the user's print preferences across sessions in
config/plot.txt (printer, copies, quality, output options, area,
scale); drawing-specific fields (offset/center/rotation) seed from the
layout. Default plot area Window, scale Fit.
- Delete src/ui/window/page_setup.rs and its ModalKind / Message /
buffer state; fold on_page_setup_commit into apply_plot_page_settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 10:27:54 +03:00
|
|
|
|
let (num, den) = parse_plot_scale(&self.plot_scale);
|
2026-07-05 23:12:52 +02:00
|
|
|
|
if num > 0.0 && den > 0.0 {
|
|
|
|
|
|
PlotScale::Ratio(num / den)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
PlotScale::Fit
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-07-06 01:22:10 +03:00
|
|
|
|
let (scale, ox, oy) = window_to_sheet((win_w, win_h), (sheet_w, sheet_h), scale_sel);
|
2026-07-05 23:12:52 +02:00
|
|
|
|
|
|
|
|
|
|
let scene = &self.tabs[i].scene;
|
|
|
|
|
|
// Cull to the window so a small-area plot doesn't write the whole
|
|
|
|
|
|
// drawing into the PDF; the clip below trims the partial crossers.
|
|
|
|
|
|
// aabb is world X/Y [minx, miny, maxx, maxy].
|
|
|
|
|
|
let (wx0, wy0, wx1, wy1) = (x0 as f32, y0 as f32, x1 as f32, y1 as f32);
|
|
|
|
|
|
let wires: Vec<_> = scene
|
|
|
|
|
|
.entity_wires()
|
|
|
|
|
|
.into_iter()
|
|
|
|
|
|
.filter(|w| {
|
|
|
|
|
|
w.aabb[0] <= wx1 && w.aabb[2] >= wx0 && w.aabb[1] <= wy1 && w.aabb[3] >= wy0
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let hatches = scene.paper_canvas_hatches();
|
|
|
|
|
|
let wipeouts = scene.paper_canvas_wipeouts();
|
|
|
|
|
|
|
|
|
|
|
|
// build_pdf maps a wire coordinate to sheet mm as (coord + offset) *
|
|
|
|
|
|
// scale (rotation_deg == 0 adds no further CTM translation).
|
|
|
|
|
|
// window_to_sheet's (ox, oy) is the sheet-mm target for the window's
|
|
|
|
|
|
// min corner, so: scale * (x0 + offset_x) = ox => offset_x = ox/scale - x0.
|
|
|
|
|
|
let offset_x = ((ox / scale) - x0) as f32;
|
|
|
|
|
|
let offset_y = ((oy / scale) - y0) as f32;
|
|
|
|
|
|
// Clip rect in pre-scale space (build_pdf's CTM applies `scale` to it,
|
|
|
|
|
|
// same as the wires) so the final sheet-mm rect lands at (ox, oy).
|
|
|
|
|
|
let clip = Some(((ox / scale) as f32, (oy / scale) as f32, win_w as f32, win_h as f32));
|
|
|
|
|
|
|
|
|
|
|
|
let res = crate::io::pdf_export::export_pdf(
|
|
|
|
|
|
&wires,
|
|
|
|
|
|
hatches.as_slice(),
|
|
|
|
|
|
wipeouts.as_slice(),
|
|
|
|
|
|
sheet_w,
|
|
|
|
|
|
sheet_h,
|
|
|
|
|
|
offset_x,
|
|
|
|
|
|
offset_y,
|
|
|
|
|
|
0,
|
|
|
|
|
|
scale as f32,
|
|
|
|
|
|
clip,
|
|
|
|
|
|
&path,
|
|
|
|
|
|
self.active_plot_style.as_ref(),
|
|
|
|
|
|
);
|
|
|
|
|
|
match res {
|
|
|
|
|
|
Ok(()) => {
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
"Plotted window to {}",
|
|
|
|
|
|
path.file_name().unwrap_or_default().to_string_lossy()
|
|
|
|
|
|
));
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(e) => self.command_line.push_error(&format!("Plot failed: {e}")),
|
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
feat(plot): unified plot/print dialog, retire Page Setup
Replace the old Page Setup panel with a single OCS-styled plot/print
dialog (dropdowns for printer, paper, orientation, rotation, plot area,
scale, quality, shaded plot; checkboxes for plot options). PLOT / PRINT
/ PAGESETUP all open it.
- Printer output via lp: -d printer, -n copies, -o ColorModel=Gray,
print-quality, Resolution; Windows printto verb; list_printers via
lpstat. Preview opens a temp PDF in the OS viewer (never a save
dialog). PDF export reuses the tested pipeline.
- Window pick returns to the new dialog (was the old Page Setup) and the
clipped window plot drives PDF / printer / preview.
- Remember the user's print preferences across sessions in
config/plot.txt (printer, copies, quality, output options, area,
scale); drawing-specific fields (offset/center/rotation) seed from the
layout. Default plot area Window, scale Fit.
- Delete src/ui/window/page_setup.rs and its ModalKind / Message /
buffer state; fold on_page_setup_commit into apply_plot_page_settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 10:27:54 +03:00
|
|
|
|
/// Build the render inputs and page geometry for a full-layout plot: wires
|
|
|
|
|
|
/// plus hatch / wipeout fills, the effective (rotation-swapped) sheet size,
|
|
|
|
|
|
/// the draw-origin offset, and the rotation. Shared by print + preview so
|
|
|
|
|
|
/// the Plot dialog reuses the same tested derivation.
|
|
|
|
|
|
#[allow(clippy::type_complexity)]
|
|
|
|
|
|
pub(super) fn layout_plot_params(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
) -> (
|
|
|
|
|
|
Vec<crate::scene::WireModel>,
|
|
|
|
|
|
Vec<crate::scene::model::hatch_model::HatchModel>,
|
|
|
|
|
|
Vec<crate::scene::model::hatch_model::HatchModel>,
|
|
|
|
|
|
f64,
|
|
|
|
|
|
f64,
|
|
|
|
|
|
f32,
|
|
|
|
|
|
f32,
|
|
|
|
|
|
i32,
|
|
|
|
|
|
) {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let scene = &self.tabs[i].scene;
|
|
|
|
|
|
let wires = scene.entity_wires();
|
|
|
|
|
|
let hatches: Vec<_> = scene.paper_canvas_hatches().as_ref().clone();
|
|
|
|
|
|
let wipeouts: Vec<_> = scene.paper_canvas_wipeouts().as_ref().clone();
|
|
|
|
|
|
use acadrust::objects::PlotType;
|
|
|
|
|
|
let ps_snap = scene.effective_plot_settings();
|
|
|
|
|
|
let (paper_w, paper_h, draw_ox, draw_oy, rotation_deg) =
|
|
|
|
|
|
if let Some(((x0, y0), (x1, y1))) = scene.paper_limits() {
|
|
|
|
|
|
let (pw, ph) = (x1 - x0, y1 - y0);
|
|
|
|
|
|
let use_extents = ps_snap
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(|ps| matches!(ps.plot_type, PlotType::Extents))
|
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
let (ox, oy) = if use_extents {
|
|
|
|
|
|
if let Some((mn, _mx)) = scene.model_space_extents() {
|
|
|
|
|
|
(-mn.x as f64, -mn.y as f64)
|
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
|
|
|
|
} else {
|
feat(plot): unified plot/print dialog, retire Page Setup
Replace the old Page Setup panel with a single OCS-styled plot/print
dialog (dropdowns for printer, paper, orientation, rotation, plot area,
scale, quality, shaded plot; checkboxes for plot options). PLOT / PRINT
/ PAGESETUP all open it.
- Printer output via lp: -d printer, -n copies, -o ColorModel=Gray,
print-quality, Resolution; Windows printto verb; list_printers via
lpstat. Preview opens a temp PDF in the OS viewer (never a save
dialog). PDF export reuses the tested pipeline.
- Window pick returns to the new dialog (was the old Page Setup) and the
clipped window plot drives PDF / printer / preview.
- Remember the user's print preferences across sessions in
config/plot.txt (printer, copies, quality, output options, area,
scale); drawing-specific fields (offset/center/rotation) seed from the
layout. Default plot area Window, scale Fit.
- Delete src/ui/window/page_setup.rs and its ModalKind / Message /
buffer state; fold on_page_setup_commit into apply_plot_page_settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 10:27:54 +03:00
|
|
|
|
(-x0, -y0)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
(-x0, -y0)
|
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
|
|
|
|
};
|
feat(plot): unified plot/print dialog, retire Page Setup
Replace the old Page Setup panel with a single OCS-styled plot/print
dialog (dropdowns for printer, paper, orientation, rotation, plot area,
scale, quality, shaded plot; checkboxes for plot options). PLOT / PRINT
/ PAGESETUP all open it.
- Printer output via lp: -d printer, -n copies, -o ColorModel=Gray,
print-quality, Resolution; Windows printto verb; list_printers via
lpstat. Preview opens a temp PDF in the OS viewer (never a save
dialog). PDF export reuses the tested pipeline.
- Window pick returns to the new dialog (was the old Page Setup) and the
clipped window plot drives PDF / printer / preview.
- Remember the user's print preferences across sessions in
config/plot.txt (printer, copies, quality, output options, area,
scale); drawing-specific fields (offset/center/rotation) seed from the
layout. Default plot area Window, scale Fit.
- Delete src/ui/window/page_setup.rs and its ModalKind / Message /
buffer state; fold on_page_setup_commit into apply_plot_page_settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 10:27:54 +03:00
|
|
|
|
let rot = ps_snap
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(|ps| ps.rotation.to_degrees() as i32)
|
|
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
|
(pw, ph, ox, oy, rot)
|
|
|
|
|
|
} else if let Some((mn, mx)) = scene.model_space_extents() {
|
|
|
|
|
|
let margin = 1.05_f64;
|
|
|
|
|
|
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, 0)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
(297.0, 210.0, 0.0, 0.0, 0)
|
|
|
|
|
|
};
|
|
|
|
|
|
let (eff_w, eff_h) = match rotation_deg {
|
|
|
|
|
|
90 | 270 => (paper_h, paper_w),
|
|
|
|
|
|
_ => (paper_w, paper_h),
|
|
|
|
|
|
};
|
|
|
|
|
|
(
|
|
|
|
|
|
wires,
|
|
|
|
|
|
hatches,
|
|
|
|
|
|
wipeouts,
|
|
|
|
|
|
eff_w,
|
|
|
|
|
|
eff_h,
|
|
|
|
|
|
draw_ox as f32,
|
|
|
|
|
|
draw_oy as f32,
|
|
|
|
|
|
rotation_deg,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_print_to_printer(&mut self) -> Task<Message> {
|
|
|
|
|
|
let (wires, hatches, wipeouts, eff_w, eff_h, ox, oy, rotation_deg) =
|
|
|
|
|
|
self.layout_plot_params();
|
|
|
|
|
|
let plot_style = self.active_plot_style.clone();
|
|
|
|
|
|
self.command_line.push_info("Sending to system printer…");
|
|
|
|
|
|
Task::perform(
|
|
|
|
|
|
async move {
|
|
|
|
|
|
crate::io::print_to_printer::print_wires(
|
|
|
|
|
|
wires, hatches, wipeouts, eff_w, eff_h, ox, oy, rotation_deg, plot_style,
|
|
|
|
|
|
)
|
|
|
|
|
|
.await
|
|
|
|
|
|
},
|
|
|
|
|
|
Message::PrintResult,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-09 12:57:35 +03:00
|
|
|
|
/// QUICKPRINT / QP — use the current selection's bounding box as the plot
|
|
|
|
|
|
/// window and export a PDF (drawing folder + name + timestamp) with the
|
|
|
|
|
|
/// active page setup, no dialog. Model space only. (#325)
|
|
|
|
|
|
pub(crate) fn on_quick_print_handles(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
handles: Vec<acadrust::Handle>,
|
|
|
|
|
|
) -> Task<Message> {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if self.tabs[i].scene.current_layout != "Model" {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error("Quick print works in model space.");
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
let set: std::collections::HashSet<acadrust::Handle> = handles.into_iter().collect();
|
|
|
|
|
|
// Union the AABBs of the picked entities' wires (world XY), matched by
|
|
|
|
|
|
// each wire's handle.
|
|
|
|
|
|
let (x0, y0, x1, y1, any) = {
|
|
|
|
|
|
let scene = &self.tabs[i].scene;
|
|
|
|
|
|
let mut x0 = f32::INFINITY;
|
|
|
|
|
|
let mut y0 = f32::INFINITY;
|
|
|
|
|
|
let mut x1 = f32::NEG_INFINITY;
|
|
|
|
|
|
let mut y1 = f32::NEG_INFINITY;
|
|
|
|
|
|
let mut any = false;
|
|
|
|
|
|
for w in scene.entity_wires() {
|
|
|
|
|
|
let picked = crate::scene::Scene::handle_from_wire_name(&w.name)
|
|
|
|
|
|
.is_some_and(|h| set.contains(&h));
|
|
|
|
|
|
if !picked {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
let [ax0, ay0, ax1, ay1] = w.aabb;
|
|
|
|
|
|
if ax0.is_finite() && ay0.is_finite() && ax1.is_finite() && ay1.is_finite() {
|
|
|
|
|
|
x0 = x0.min(ax0);
|
|
|
|
|
|
y0 = y0.min(ay0);
|
|
|
|
|
|
x1 = x1.max(ax1);
|
|
|
|
|
|
y1 = y1.max(ay1);
|
|
|
|
|
|
any = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
(x0, y0, x1, y1, any)
|
|
|
|
|
|
};
|
|
|
|
|
|
if !any {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error("Selection has no printable geometry.");
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
if !(x1 > x0 && y1 > y0) {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error("Selection has no printable area.");
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Small margin so the outermost strokes aren't clipped flush to the edge.
|
|
|
|
|
|
let mx = ((x1 - x0) * 0.02).max(0.0);
|
|
|
|
|
|
let my = ((y1 - y0) * 0.02).max(0.0);
|
|
|
|
|
|
self.plot_window = Some((
|
|
|
|
|
|
(x0 - mx) as f64,
|
|
|
|
|
|
(y0 - my) as f64,
|
|
|
|
|
|
(x1 + mx) as f64,
|
|
|
|
|
|
(y1 + my) as f64,
|
|
|
|
|
|
));
|
|
|
|
|
|
let path = self.quick_print_path();
|
|
|
|
|
|
self.on_plot_window_export_path_some(path)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Auto output path for quick print: the drawing's folder + name + a
|
|
|
|
|
|
/// timestamp, falling back to the temp dir / "drawing" when unsaved.
|
|
|
|
|
|
fn quick_print_path(&self) -> std::path::PathBuf {
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let cur = self.tabs[i].current_path.as_deref();
|
|
|
|
|
|
let dir = cur
|
|
|
|
|
|
.and_then(|p| p.parent())
|
|
|
|
|
|
.map(|d| d.to_path_buf())
|
|
|
|
|
|
.unwrap_or_else(std::env::temp_dir);
|
|
|
|
|
|
let stem = cur
|
|
|
|
|
|
.and_then(|p| p.file_stem())
|
|
|
|
|
|
.map(|s| s.to_string_lossy().into_owned())
|
|
|
|
|
|
.unwrap_or_else(|| "drawing".into());
|
|
|
|
|
|
let ts = std::time::SystemTime::now()
|
|
|
|
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
|
|
|
|
.map(|d| d.as_secs())
|
|
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
|
dir.join(format!("{stem}_{ts}.pdf"))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(plot): unified plot/print dialog, retire Page Setup
Replace the old Page Setup panel with a single OCS-styled plot/print
dialog (dropdowns for printer, paper, orientation, rotation, plot area,
scale, quality, shaded plot; checkboxes for plot options). PLOT / PRINT
/ PAGESETUP all open it.
- Printer output via lp: -d printer, -n copies, -o ColorModel=Gray,
print-quality, Resolution; Windows printto verb; list_printers via
lpstat. Preview opens a temp PDF in the OS viewer (never a save
dialog). PDF export reuses the tested pipeline.
- Window pick returns to the new dialog (was the old Page Setup) and the
clipped window plot drives PDF / printer / preview.
- Remember the user's print preferences across sessions in
config/plot.txt (printer, copies, quality, output options, area,
scale); drawing-specific fields (offset/center/rotation) seed from the
layout. Default plot area Window, scale Fit.
- Delete src/ui/window/page_setup.rs and its ModalKind / Message /
buffer state; fold on_page_setup_commit into apply_plot_page_settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 10:27:54 +03:00
|
|
|
|
/// Open the full Plot / Print dialog, seeding its state from the active
|
|
|
|
|
|
/// layout's plot settings and the printers found on the system.
|
|
|
|
|
|
pub(super) fn on_plot_dialog_open(&mut self) -> Task<Message> {
|
|
|
|
|
|
use crate::io::paper_sizes::Orientation;
|
|
|
|
|
|
// Keep the user's last print preferences (printer, copies, quality,
|
|
|
|
|
|
// output options — persisted in `self.plot_dialog`); only refresh the
|
|
|
|
|
|
// runtime printer list and reseed the drawing-specific fields from the
|
|
|
|
|
|
// active layout.
|
|
|
|
|
|
let d = &mut self.plot_dialog;
|
|
|
|
|
|
d.printers = crate::io::print_to_printer::list_printers();
|
|
|
|
|
|
d.paper = self.plot_format.label().to_string();
|
|
|
|
|
|
d.orientation = match self.plot_orientation {
|
|
|
|
|
|
Orientation::Portrait => "Portrait",
|
|
|
|
|
|
Orientation::Landscape => "Landscape",
|
|
|
|
|
|
}
|
|
|
|
|
|
.to_string();
|
|
|
|
|
|
d.style_name = self
|
|
|
|
|
|
.active_plot_style
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(|t| t.name.clone())
|
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
// `area` and `scale` are remembered user choices (default Window / Fit),
|
|
|
|
|
|
// not reseeded from the layout. Offset / center / rotation ARE layout
|
|
|
|
|
|
// properties, so reflect them.
|
|
|
|
|
|
if let Some(ps) = self.tabs[self.active_tab].scene.effective_plot_settings() {
|
|
|
|
|
|
let d = &mut self.plot_dialog;
|
|
|
|
|
|
d.center = ps.flags.plot_centered;
|
|
|
|
|
|
d.offset_x = format!("{:.2}", ps.origin_x);
|
|
|
|
|
|
d.offset_y = format!("{:.2}", ps.origin_y);
|
|
|
|
|
|
let deg = ps.rotation.to_degrees() as i32;
|
|
|
|
|
|
d.rotation = format!("{deg}°");
|
|
|
|
|
|
}
|
2026-07-09 12:25:46 +03:00
|
|
|
|
self.plot_dialog.name_input = None;
|
|
|
|
|
|
self.plot_dialog.name_rename = false;
|
|
|
|
|
|
// Refresh the list (<none> / <previous> / layouts / named setups) and
|
|
|
|
|
|
// snapshot the just-loaded settings as the `<previous>` restore point.
|
|
|
|
|
|
self.refresh_page_setups();
|
|
|
|
|
|
self.plot_prev = Some(self.plot_dialog.clone());
|
|
|
|
|
|
// Default selection: model space → <none>; a layout → its own setup.
|
|
|
|
|
|
let cur = self.tabs[self.active_tab].scene.current_layout.clone();
|
|
|
|
|
|
if cur == "Model" {
|
|
|
|
|
|
self.select_page_setup(crate::ui::window::plot::SETUP_NONE);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.select_page_setup(&format!("*{cur}*"));
|
|
|
|
|
|
}
|
feat(plot): unified plot/print dialog, retire Page Setup
Replace the old Page Setup panel with a single OCS-styled plot/print
dialog (dropdowns for printer, paper, orientation, rotation, plot area,
scale, quality, shaded plot; checkboxes for plot options). PLOT / PRINT
/ PAGESETUP all open it.
- Printer output via lp: -d printer, -n copies, -o ColorModel=Gray,
print-quality, Resolution; Windows printto verb; list_printers via
lpstat. Preview opens a temp PDF in the OS viewer (never a save
dialog). PDF export reuses the tested pipeline.
- Window pick returns to the new dialog (was the old Page Setup) and the
clipped window plot drives PDF / printer / preview.
- Remember the user's print preferences across sessions in
config/plot.txt (printer, copies, quality, output options, area,
scale); drawing-specific fields (offset/center/rotation) seed from the
layout. Default plot area Window, scale Fit.
- Delete src/ui/window/page_setup.rs and its ModalKind / Message /
buffer state; fold on_page_setup_commit into apply_plot_page_settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 10:27:54 +03:00
|
|
|
|
self.active_modal = Some(crate::app::ModalKind::Plot);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Handle one edit / action from the Plot dialog.
|
|
|
|
|
|
pub(super) fn on_plot_dlg(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
msg: crate::ui::window::plot::PlotDlgMsg,
|
|
|
|
|
|
) -> Task<Message> {
|
|
|
|
|
|
use crate::ui::window::plot::{PlotDlgMsg as M, PlotFlag, OUT_DEFAULT, OUT_PDF};
|
|
|
|
|
|
match msg {
|
|
|
|
|
|
M::Close => {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::Printer(s) => {
|
|
|
|
|
|
if s == OUT_PDF {
|
|
|
|
|
|
self.plot_dialog.to_file = true;
|
|
|
|
|
|
} else if s == OUT_DEFAULT {
|
|
|
|
|
|
self.plot_dialog.to_file = false;
|
|
|
|
|
|
self.plot_dialog.printer = None;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.plot_dialog.to_file = false;
|
|
|
|
|
|
self.plot_dialog.printer = Some(s);
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::Paper(s) => {
|
|
|
|
|
|
self.plot_dialog.paper = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::Orientation(s) => {
|
|
|
|
|
|
self.plot_dialog.orientation = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::Rotation(s) => {
|
|
|
|
|
|
self.plot_dialog.rotation = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::Area(s) => {
|
|
|
|
|
|
self.plot_dialog.area = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::Scale(s) => {
|
|
|
|
|
|
self.plot_dialog.scale = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::Quality(s) => {
|
|
|
|
|
|
self.plot_dialog.quality = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::Shade(s) => {
|
|
|
|
|
|
self.plot_dialog.shade = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::Copies(s) => {
|
|
|
|
|
|
self.plot_dialog.copies = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::OffsetX(s) => {
|
|
|
|
|
|
self.plot_dialog.offset_x = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::OffsetY(s) => {
|
|
|
|
|
|
self.plot_dialog.offset_y = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::Dpi(s) => {
|
|
|
|
|
|
self.plot_dialog.dpi = s;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::Flag(f) => {
|
|
|
|
|
|
let d = &mut self.plot_dialog;
|
|
|
|
|
|
match f {
|
|
|
|
|
|
PlotFlag::Center => d.center = !d.center,
|
|
|
|
|
|
PlotFlag::ScaleLw => d.scale_lw = !d.scale_lw,
|
|
|
|
|
|
PlotFlag::Mono => d.mono = !d.mono,
|
|
|
|
|
|
PlotFlag::Lineweights => d.lineweights = !d.lineweights,
|
|
|
|
|
|
PlotFlag::WithStyles => d.with_styles = !d.with_styles,
|
|
|
|
|
|
PlotFlag::Transparency => d.transparency = !d.transparency,
|
|
|
|
|
|
PlotFlag::PaperspaceLast => d.paperspace_last = !d.paperspace_last,
|
|
|
|
|
|
PlotFlag::HidePaperspace => d.hide_paperspace = !d.hide_paperspace,
|
|
|
|
|
|
PlotFlag::Stamp => d.stamp = !d.stamp,
|
|
|
|
|
|
PlotFlag::SaveLayout => d.save_layout = !d.save_layout,
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::LoadStyle => Task::done(Message::PlotStyleLoad),
|
|
|
|
|
|
M::ClearStyle => {
|
|
|
|
|
|
self.active_plot_style = None;
|
|
|
|
|
|
self.plot_dialog.style_name.clear();
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::PickWindow => {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
Task::done(Message::Command("PLOTWINDOW".into()))
|
|
|
|
|
|
}
|
2026-07-09 12:25:46 +03:00
|
|
|
|
M::SelectSetup(name) => {
|
|
|
|
|
|
self.select_page_setup(&name);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::SetCurrent => {
|
|
|
|
|
|
self.apply_dialog_to_layout();
|
|
|
|
|
|
self.command_line.push_info("Page setup applied to the layout.");
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::NewSetup => {
|
|
|
|
|
|
// Create a setup from the current editor values, then start an
|
|
|
|
|
|
// inline rename so the user can name it.
|
|
|
|
|
|
let name = self.next_page_setup_name("Setup");
|
|
|
|
|
|
let ps = self.dialog_to_plotsettings();
|
|
|
|
|
|
self.tabs[self.active_tab].scene.page_setup_save(&name, ps);
|
|
|
|
|
|
self.tabs[self.active_tab].dirty = true;
|
|
|
|
|
|
self.plot_dialog.selected_setup = name.clone();
|
|
|
|
|
|
self.refresh_page_setups();
|
|
|
|
|
|
self.plot_dialog.name_input = Some(name);
|
|
|
|
|
|
self.plot_dialog.name_rename = true;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::CopySetup => {
|
|
|
|
|
|
// Duplicate the selected entry — a layout OR a named setup —
|
|
|
|
|
|
// into a new standalone named page setup.
|
|
|
|
|
|
let sel = self.plot_dialog.selected_setup.clone();
|
|
|
|
|
|
let scene = &self.tabs[self.active_tab].scene;
|
|
|
|
|
|
let (src_ps, base) = if is_layout_entry(&sel) {
|
|
|
|
|
|
let ln = layout_entry_name(&sel);
|
|
|
|
|
|
(scene.plot_settings_for(ln), format!("{ln} copy"))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
(scene.page_setup_get(&sel), format!("{sel} copy"))
|
|
|
|
|
|
};
|
|
|
|
|
|
if let Some(ps) = src_ps {
|
|
|
|
|
|
let name = self.next_page_setup_name(&base);
|
|
|
|
|
|
self.tabs[self.active_tab].scene.page_setup_save(&name, ps);
|
|
|
|
|
|
self.tabs[self.active_tab].dirty = true;
|
|
|
|
|
|
self.plot_dialog.selected_setup = name.clone();
|
|
|
|
|
|
self.refresh_page_setups();
|
|
|
|
|
|
self.plot_dialog.name_input = Some(name);
|
|
|
|
|
|
self.plot_dialog.name_rename = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::RenameStart(name) => {
|
|
|
|
|
|
// Only standalone named setups can be renamed.
|
|
|
|
|
|
if is_layout_entry(&name) || is_special_entry(&name) {
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
self.plot_dialog.selected_setup = name.clone();
|
|
|
|
|
|
if let Some(ps) = self.tabs[self.active_tab].scene.page_setup_get(&name) {
|
|
|
|
|
|
self.load_plotsettings_into_dialog(&ps);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.plot_dialog.name_input = Some(name);
|
|
|
|
|
|
self.plot_dialog.name_rename = true;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::DeleteSetup => {
|
|
|
|
|
|
let sel = self.plot_dialog.selected_setup.clone();
|
|
|
|
|
|
if !sel.is_empty() && !is_layout_entry(&sel) {
|
|
|
|
|
|
self.tabs[self.active_tab].scene.page_setup_delete(&sel);
|
|
|
|
|
|
self.tabs[self.active_tab].dirty = true;
|
|
|
|
|
|
self.plot_dialog.selected_setup.clear();
|
|
|
|
|
|
self.refresh_page_setups();
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::NameInput(s) => {
|
|
|
|
|
|
self.plot_dialog.name_input = Some(s);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::NameCommit => {
|
|
|
|
|
|
if let Some(name) = self.plot_dialog.name_input.take() {
|
|
|
|
|
|
let name = name.trim().to_string();
|
|
|
|
|
|
if !name.is_empty() {
|
|
|
|
|
|
if self.plot_dialog.name_rename {
|
|
|
|
|
|
let old = self.plot_dialog.selected_setup.clone();
|
|
|
|
|
|
self.tabs[self.active_tab].scene.page_setup_rename(&old, &name);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let ps = self.dialog_to_plotsettings();
|
|
|
|
|
|
self.tabs[self.active_tab].scene.page_setup_save(&name, ps);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[self.active_tab].dirty = true;
|
|
|
|
|
|
self.plot_dialog.selected_setup = name;
|
|
|
|
|
|
self.refresh_page_setups();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.plot_dialog.name_rename = false;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
M::NameCancel => {
|
|
|
|
|
|
self.plot_dialog.name_input = None;
|
|
|
|
|
|
self.plot_dialog.name_rename = false;
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
feat(plot): unified plot/print dialog, retire Page Setup
Replace the old Page Setup panel with a single OCS-styled plot/print
dialog (dropdowns for printer, paper, orientation, rotation, plot area,
scale, quality, shaded plot; checkboxes for plot options). PLOT / PRINT
/ PAGESETUP all open it.
- Printer output via lp: -d printer, -n copies, -o ColorModel=Gray,
print-quality, Resolution; Windows printto verb; list_printers via
lpstat. Preview opens a temp PDF in the OS viewer (never a save
dialog). PDF export reuses the tested pipeline.
- Window pick returns to the new dialog (was the old Page Setup) and the
clipped window plot drives PDF / printer / preview.
- Remember the user's print preferences across sessions in
config/plot.txt (printer, copies, quality, output options, area,
scale); drawing-specific fields (offset/center/rotation) seed from the
layout. Default plot area Window, scale Fit.
- Delete src/ui/window/page_setup.rs and its ModalKind / Message /
buffer state; fold on_page_setup_commit into apply_plot_page_settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 10:27:54 +03:00
|
|
|
|
M::Preview => self.on_plot_dlg_commit(true),
|
|
|
|
|
|
M::Commit => self.on_plot_dlg_commit(false),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-09 12:25:46 +03:00
|
|
|
|
fn refresh_page_setups(&mut self) {
|
|
|
|
|
|
use crate::ui::window::plot::{SETUP_NONE, SETUP_PREV};
|
|
|
|
|
|
let scene = &self.tabs[self.active_tab].scene;
|
|
|
|
|
|
// <none> / <previous>, then layouts (`*name*`), then named setups.
|
|
|
|
|
|
let mut list = vec![SETUP_NONE.to_string(), SETUP_PREV.to_string()];
|
|
|
|
|
|
list.extend(scene.layout_names().into_iter().map(|n| format!("*{n}*")));
|
|
|
|
|
|
list.extend(scene.page_setup_names());
|
|
|
|
|
|
self.plot_dialog.page_setups = list;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Apply a page-setup list selection to the editor. Handles the pseudo
|
|
|
|
|
|
/// entries (`<none>` / `<previous>`), layout rows (`*name*`) and named
|
|
|
|
|
|
/// setups.
|
|
|
|
|
|
fn select_page_setup(&mut self, name: &str) {
|
|
|
|
|
|
use crate::ui::window::plot::{SETUP_NONE, SETUP_PREV};
|
|
|
|
|
|
self.plot_dialog.selected_setup = name.to_string();
|
|
|
|
|
|
if name == SETUP_NONE {
|
|
|
|
|
|
// No page setup: default geometry + PDF output.
|
|
|
|
|
|
let is_model = self.tabs[self.active_tab].scene.current_layout == "Model";
|
|
|
|
|
|
let d = &mut self.plot_dialog;
|
|
|
|
|
|
d.to_file = true;
|
|
|
|
|
|
d.paper = "A4".into();
|
|
|
|
|
|
d.orientation = "Landscape".into();
|
|
|
|
|
|
d.area = if is_model { "Window".into() } else { "Layout".into() };
|
|
|
|
|
|
d.center = true;
|
|
|
|
|
|
d.offset_x = "0.0".into();
|
|
|
|
|
|
d.offset_y = "0.0".into();
|
|
|
|
|
|
d.rotation = "0°".into();
|
|
|
|
|
|
d.scale = "Fit".into();
|
|
|
|
|
|
} else if name == SETUP_PREV {
|
|
|
|
|
|
if let Some(prev) = self.plot_prev.clone() {
|
|
|
|
|
|
self.plot_dialog.copy_settings_from(&prev);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if is_layout_entry(name) {
|
|
|
|
|
|
if let Some(ps) = self
|
|
|
|
|
|
.tabs[self.active_tab]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.plot_settings_for(layout_entry_name(name))
|
|
|
|
|
|
{
|
|
|
|
|
|
self.load_plotsettings_into_dialog(&ps);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if let Some(ps) = self.tabs[self.active_tab].scene.page_setup_get(name) {
|
|
|
|
|
|
self.load_plotsettings_into_dialog(&ps);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// A page-setup name based on `base` that isn't already taken (`base`,
|
|
|
|
|
|
/// `base 2`, `base 3`, …).
|
|
|
|
|
|
fn next_page_setup_name(&self, base: &str) -> String {
|
|
|
|
|
|
let existing = self.tabs[self.active_tab].scene.page_setup_names();
|
|
|
|
|
|
if !existing.iter().any(|n| n == base) {
|
|
|
|
|
|
return base.to_string();
|
|
|
|
|
|
}
|
|
|
|
|
|
(2..)
|
|
|
|
|
|
.map(|i| format!("{base} {i}"))
|
|
|
|
|
|
.find(|c| !existing.iter().any(|n| n == c))
|
|
|
|
|
|
.unwrap_or_else(|| base.to_string())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Build a `PlotSettings` from the current dialog fields (for saving a named
|
|
|
|
|
|
/// page setup).
|
|
|
|
|
|
fn dialog_to_plotsettings(&self) -> acadrust::objects::PlotSettings {
|
|
|
|
|
|
use acadrust::objects::{
|
|
|
|
|
|
PlotPaperUnits, PlotRotation, PlotSettings, PlotType, ScaledType,
|
|
|
|
|
|
};
|
|
|
|
|
|
use crate::io::paper_sizes::{sheet_mm, Orientation, PaperSize};
|
|
|
|
|
|
let d = &self.plot_dialog;
|
|
|
|
|
|
let paper = match d.paper.as_str() {
|
|
|
|
|
|
"A3" => PaperSize::A3,
|
|
|
|
|
|
"A2" => PaperSize::A2,
|
|
|
|
|
|
"A1" => PaperSize::A1,
|
|
|
|
|
|
"A0" => PaperSize::A0,
|
|
|
|
|
|
_ => PaperSize::A4,
|
|
|
|
|
|
};
|
|
|
|
|
|
let orient = if d.orientation == "Portrait" {
|
|
|
|
|
|
Orientation::Portrait
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Orientation::Landscape
|
|
|
|
|
|
};
|
|
|
|
|
|
let (w, h) = sheet_mm(paper, orient);
|
|
|
|
|
|
let mut ps = PlotSettings::new("");
|
|
|
|
|
|
ps.paper_width = w;
|
|
|
|
|
|
ps.paper_height = h;
|
|
|
|
|
|
ps.paper_units = PlotPaperUnits::Millimeters;
|
|
|
|
|
|
ps.plot_type = match d.area.as_str() {
|
|
|
|
|
|
"Window" => PlotType::Window,
|
|
|
|
|
|
"Layout" => PlotType::Layout,
|
|
|
|
|
|
_ => PlotType::Extents,
|
|
|
|
|
|
};
|
|
|
|
|
|
ps.flags.plot_centered = d.center;
|
|
|
|
|
|
ps.origin_x = d.offset_x.parse::<f64>().unwrap_or(0.0);
|
|
|
|
|
|
ps.origin_y = d.offset_y.parse::<f64>().unwrap_or(0.0);
|
|
|
|
|
|
let rot: i16 = d.rotation.trim_end_matches('°').parse().unwrap_or(0);
|
|
|
|
|
|
ps.rotation = match rot {
|
|
|
|
|
|
90 => PlotRotation::Degrees90,
|
|
|
|
|
|
180 => PlotRotation::Degrees180,
|
|
|
|
|
|
270 => PlotRotation::Degrees270,
|
|
|
|
|
|
_ => PlotRotation::None,
|
|
|
|
|
|
};
|
|
|
|
|
|
if d.scale == "Fit" {
|
|
|
|
|
|
ps.set_scale_to_fit();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let (num, den) = parse_plot_scale(&d.scale);
|
|
|
|
|
|
ps.scale_type = ScaledType::CustomScale;
|
|
|
|
|
|
ps.scale_numerator = num;
|
|
|
|
|
|
ps.scale_denominator = den;
|
|
|
|
|
|
}
|
|
|
|
|
|
ps.printer_name = d.printer.clone().unwrap_or_default();
|
|
|
|
|
|
ps.current_style_sheet = d.style_name.clone();
|
|
|
|
|
|
ps
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Load a `PlotSettings` into the dialog editor fields.
|
|
|
|
|
|
fn load_plotsettings_into_dialog(&mut self, ps: &acadrust::objects::PlotSettings) {
|
|
|
|
|
|
use acadrust::objects::PlotType;
|
|
|
|
|
|
let (paper, orient) = paper_label_from_dims(ps.paper_width, ps.paper_height);
|
|
|
|
|
|
let d = &mut self.plot_dialog;
|
|
|
|
|
|
d.paper = paper;
|
|
|
|
|
|
d.orientation = orient;
|
|
|
|
|
|
d.area = match ps.plot_type {
|
|
|
|
|
|
PlotType::Window => "Window",
|
|
|
|
|
|
PlotType::Layout => "Layout",
|
|
|
|
|
|
_ => "Extents",
|
|
|
|
|
|
}
|
|
|
|
|
|
.to_string();
|
|
|
|
|
|
d.center = ps.flags.plot_centered;
|
|
|
|
|
|
d.offset_x = format!("{:.2}", ps.origin_x);
|
|
|
|
|
|
d.offset_y = format!("{:.2}", ps.origin_y);
|
|
|
|
|
|
let deg = ps.rotation.to_degrees() as i32;
|
|
|
|
|
|
d.rotation = format!("{deg}°");
|
|
|
|
|
|
d.scale = if ps.is_scale_to_fit() {
|
|
|
|
|
|
"Fit".into()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let n = ps.scale_numerator;
|
|
|
|
|
|
let m = ps.scale_denominator;
|
|
|
|
|
|
if (n - 1.0).abs() < 1e-9 {
|
|
|
|
|
|
format!("1:{}", m as i64)
|
|
|
|
|
|
} else if (m - 1.0).abs() < 1e-9 {
|
|
|
|
|
|
format!("{}:1", n as i64)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
"Fit".into()
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Write the current dialog fields into the active layout's plot settings.
|
|
|
|
|
|
/// Shared by the "Set current" action and the print/export commit.
|
|
|
|
|
|
fn apply_dialog_to_layout(&mut self) {
|
feat(plot): unified plot/print dialog, retire Page Setup
Replace the old Page Setup panel with a single OCS-styled plot/print
dialog (dropdowns for printer, paper, orientation, rotation, plot area,
scale, quality, shaded plot; checkboxes for plot options). PLOT / PRINT
/ PAGESETUP all open it.
- Printer output via lp: -d printer, -n copies, -o ColorModel=Gray,
print-quality, Resolution; Windows printto verb; list_printers via
lpstat. Preview opens a temp PDF in the OS viewer (never a save
dialog). PDF export reuses the tested pipeline.
- Window pick returns to the new dialog (was the old Page Setup) and the
clipped window plot drives PDF / printer / preview.
- Remember the user's print preferences across sessions in
config/plot.txt (printer, copies, quality, output options, area,
scale); drawing-specific fields (offset/center/rotation) seed from the
layout. Default plot area Window, scale Fit.
- Delete src/ui/window/page_setup.rs and its ModalKind / Message /
buffer state; fold on_page_setup_commit into apply_plot_page_settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 10:27:54 +03:00
|
|
|
|
use crate::io::paper_sizes::{sheet_mm, Orientation, PaperSize};
|
|
|
|
|
|
let d = self.plot_dialog.clone();
|
|
|
|
|
|
let paper = match d.paper.as_str() {
|
|
|
|
|
|
"A3" => PaperSize::A3,
|
|
|
|
|
|
"A2" => PaperSize::A2,
|
|
|
|
|
|
"A1" => PaperSize::A1,
|
|
|
|
|
|
"A0" => PaperSize::A0,
|
|
|
|
|
|
_ => PaperSize::A4,
|
|
|
|
|
|
};
|
|
|
|
|
|
let orient = if d.orientation == "Portrait" {
|
|
|
|
|
|
Orientation::Portrait
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Orientation::Landscape
|
|
|
|
|
|
};
|
|
|
|
|
|
let (sheet_w, sheet_h) = sheet_mm(paper, orient);
|
|
|
|
|
|
self.plot_format = paper;
|
|
|
|
|
|
self.plot_orientation = orient;
|
|
|
|
|
|
self.plot_scale = d.scale.clone();
|
|
|
|
|
|
let rotation: i16 = d.rotation.trim_end_matches('°').parse().unwrap_or(0);
|
|
|
|
|
|
let off_x = d.offset_x.parse::<f64>().unwrap_or(0.0);
|
|
|
|
|
|
let off_y = d.offset_y.parse::<f64>().unwrap_or(0.0);
|
|
|
|
|
|
let extents = d.area != "Layout";
|
|
|
|
|
|
self.apply_plot_page_settings(
|
|
|
|
|
|
sheet_w, sheet_h, extents, d.center, off_x, off_y, rotation, &d.scale,
|
|
|
|
|
|
);
|
2026-07-09 12:25:46 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Apply the dialog's page settings to the layout, then either open a
|
|
|
|
|
|
/// preview PDF, export a PDF, or send the job to the chosen printer.
|
|
|
|
|
|
fn on_plot_dlg_commit(&mut self, preview: bool) -> Task<Message> {
|
|
|
|
|
|
// Remember the user's print preferences across sessions.
|
|
|
|
|
|
self.plot_dialog.save();
|
|
|
|
|
|
let d = self.plot_dialog.clone();
|
|
|
|
|
|
// Persist the dialog's page settings into the layout, then reuse the
|
|
|
|
|
|
// tested layout-plot derivation.
|
|
|
|
|
|
self.apply_dialog_to_layout();
|
feat(plot): unified plot/print dialog, retire Page Setup
Replace the old Page Setup panel with a single OCS-styled plot/print
dialog (dropdowns for printer, paper, orientation, rotation, plot area,
scale, quality, shaded plot; checkboxes for plot options). PLOT / PRINT
/ PAGESETUP all open it.
- Printer output via lp: -d printer, -n copies, -o ColorModel=Gray,
print-quality, Resolution; Windows printto verb; list_printers via
lpstat. Preview opens a temp PDF in the OS viewer (never a save
dialog). PDF export reuses the tested pipeline.
- Window pick returns to the new dialog (was the old Page Setup) and the
clipped window plot drives PDF / printer / preview.
- Remember the user's print preferences across sessions in
config/plot.txt (printer, copies, quality, output options, area,
scale); drawing-specific fields (offset/center/rotation) seed from the
layout. Default plot area Window, scale Fit.
- Delete src/ui/window/page_setup.rs and its ModalKind / Message /
buffer state; fold on_page_setup_commit into apply_plot_page_settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 10:27:54 +03:00
|
|
|
|
self.active_modal = None;
|
|
|
|
|
|
|
|
|
|
|
|
let plot_style = if d.with_styles {
|
|
|
|
|
|
self.active_plot_style.clone()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ── Window area: clipped model-space plot ────────────────────────────
|
|
|
|
|
|
if d.area == "Window" {
|
|
|
|
|
|
// Preview renders to a temp PDF and opens it — never a save dialog,
|
|
|
|
|
|
// whatever the output target is.
|
|
|
|
|
|
if preview {
|
|
|
|
|
|
let Some((w_wires, w_hatches, w_wipeouts, sw, sh, wox, woy, wscale, wclip)) =
|
|
|
|
|
|
self.window_plot_job()
|
|
|
|
|
|
else {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error("Pick a plot window first (model space).");
|
|
|
|
|
|
self.active_modal = Some(crate::app::ModalKind::Plot);
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
let tmp = std::env::temp_dir().join("open_cad_studio_preview.pdf");
|
|
|
|
|
|
let exp = crate::io::pdf_export::export_pdf(
|
|
|
|
|
|
&w_wires, &w_hatches, &w_wipeouts, sw, sh, wox, woy, 0, wscale, wclip, &tmp,
|
|
|
|
|
|
plot_style.as_ref(),
|
|
|
|
|
|
);
|
|
|
|
|
|
match exp.and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp)) {
|
|
|
|
|
|
Ok(()) => self.command_line.push_info("Opened plot preview."),
|
|
|
|
|
|
Err(e) => self.command_line.push_error(&format!("Preview failed: {e}")),
|
|
|
|
|
|
}
|
|
|
|
|
|
self.active_modal = Some(crate::app::ModalKind::Plot);
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
if d.to_file {
|
|
|
|
|
|
// Tested clipped export (opens a save dialog).
|
|
|
|
|
|
return Task::done(Message::PlotWindowExport);
|
|
|
|
|
|
}
|
|
|
|
|
|
let Some((w_wires, w_hatches, w_wipeouts, sw, sh, wox, woy, wscale, wclip)) =
|
|
|
|
|
|
self.window_plot_job()
|
|
|
|
|
|
else {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error("Pick a plot window first (model space).");
|
|
|
|
|
|
self.active_modal = Some(crate::app::ModalKind::Plot);
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
let tmp = std::env::temp_dir().join("open_cad_studio_print.pdf");
|
|
|
|
|
|
let exp = crate::io::pdf_export::export_pdf(
|
|
|
|
|
|
&w_wires, &w_hatches, &w_wipeouts, sw, sh, wox, woy, 0, wscale, wclip, &tmp,
|
|
|
|
|
|
plot_style.as_ref(),
|
|
|
|
|
|
);
|
|
|
|
|
|
let opts = self.plot_print_options(&d);
|
|
|
|
|
|
match exp.and_then(|_| crate::io::print_to_printer::print_existing_pdf(&tmp, &opts)) {
|
|
|
|
|
|
Ok(printer) => self
|
|
|
|
|
|
.command_line
|
|
|
|
|
|
.push_info(&format!("Sent to printer: {printer}")),
|
|
|
|
|
|
Err(e) => self.command_line.push_error(&format!("Print failed: {e}")),
|
|
|
|
|
|
}
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let (wires, hatches, wipeouts, eff_w, eff_h, ox, oy, rot) = self.layout_plot_params();
|
|
|
|
|
|
|
|
|
|
|
|
if preview {
|
|
|
|
|
|
let tmp = std::env::temp_dir().join("open_cad_studio_preview.pdf");
|
|
|
|
|
|
let res = crate::io::pdf_export::export_pdf(
|
|
|
|
|
|
&wires,
|
|
|
|
|
|
&hatches,
|
|
|
|
|
|
&wipeouts,
|
|
|
|
|
|
eff_w,
|
|
|
|
|
|
eff_h,
|
|
|
|
|
|
ox,
|
|
|
|
|
|
oy,
|
|
|
|
|
|
rot,
|
|
|
|
|
|
1.0,
|
|
|
|
|
|
None,
|
|
|
|
|
|
&tmp,
|
|
|
|
|
|
plot_style.as_ref(),
|
|
|
|
|
|
);
|
|
|
|
|
|
match res.and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp)) {
|
|
|
|
|
|
Ok(()) => self.command_line.push_info("Opened plot preview."),
|
|
|
|
|
|
Err(e) => self.command_line.push_error(&format!("Preview failed: {e}")),
|
|
|
|
|
|
}
|
|
|
|
|
|
// Preview leaves the dialog open for further tweaks.
|
|
|
|
|
|
self.active_modal = Some(crate::app::ModalKind::Plot);
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if d.to_file {
|
|
|
|
|
|
// Reuse the tested PDF export flow (it opens a save dialog).
|
|
|
|
|
|
return Task::done(Message::PlotExport);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let opts = self.plot_print_options(&d);
|
|
|
|
|
|
self.command_line.push_info("Sending to system printer…");
|
|
|
|
|
|
Task::perform(
|
|
|
|
|
|
async move {
|
|
|
|
|
|
crate::io::print_to_printer::print_wires_with(
|
|
|
|
|
|
wires, hatches, wipeouts, eff_w, eff_h, ox, oy, rot, plot_style, opts,
|
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
|
|
|
|
)
|
feat(plot): unified plot/print dialog, retire Page Setup
Replace the old Page Setup panel with a single OCS-styled plot/print
dialog (dropdowns for printer, paper, orientation, rotation, plot area,
scale, quality, shaded plot; checkboxes for plot options). PLOT / PRINT
/ PAGESETUP all open it.
- Printer output via lp: -d printer, -n copies, -o ColorModel=Gray,
print-quality, Resolution; Windows printto verb; list_printers via
lpstat. Preview opens a temp PDF in the OS viewer (never a save
dialog). PDF export reuses the tested pipeline.
- Window pick returns to the new dialog (was the old Page Setup) and the
clipped window plot drives PDF / printer / preview.
- Remember the user's print preferences across sessions in
config/plot.txt (printer, copies, quality, output options, area,
scale); drawing-specific fields (offset/center/rotation) seed from the
layout. Default plot area Window, scale Fit.
- Delete src/ui/window/page_setup.rs and its ModalKind / Message /
buffer state; fold on_page_setup_commit into apply_plot_page_settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 10:27:54 +03:00
|
|
|
|
.await
|
|
|
|
|
|
},
|
|
|
|
|
|
Message::PrintResult,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Build a [`PrintOptions`](crate::io::print_to_printer::PrintOptions) from
|
|
|
|
|
|
/// the dialog state.
|
|
|
|
|
|
fn plot_print_options(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
d: &crate::ui::window::plot::PlotDialogState,
|
|
|
|
|
|
) -> crate::io::print_to_printer::PrintOptions {
|
|
|
|
|
|
crate::io::print_to_printer::PrintOptions {
|
|
|
|
|
|
printer: d.printer.clone(),
|
|
|
|
|
|
copies: d.copies.trim().parse::<u32>().unwrap_or(1).max(1),
|
|
|
|
|
|
mono: d.mono,
|
|
|
|
|
|
quality: Some(d.quality.clone()),
|
|
|
|
|
|
dpi: d.dpi.trim().parse::<u32>().ok().filter(|v| *v > 0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Render inputs + page geometry for a clipped model-space window plot
|
|
|
|
|
|
/// (the pending `plot_window`, fitted onto the dialog's sheet). Returns
|
|
|
|
|
|
/// `None` when not in model space or no window has been picked. Mirrors
|
|
|
|
|
|
/// [`Self::on_plot_window_export_path_some`] but hands back the params so
|
|
|
|
|
|
/// the plot can go to a printer or preview, not only a save-dialog PDF.
|
|
|
|
|
|
#[allow(clippy::type_complexity)]
|
|
|
|
|
|
fn window_plot_job(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
) -> Option<(
|
|
|
|
|
|
Vec<crate::scene::WireModel>,
|
|
|
|
|
|
Vec<crate::scene::model::hatch_model::HatchModel>,
|
|
|
|
|
|
Vec<crate::scene::model::hatch_model::HatchModel>,
|
|
|
|
|
|
f64,
|
|
|
|
|
|
f64,
|
|
|
|
|
|
f32,
|
|
|
|
|
|
f32,
|
|
|
|
|
|
f32,
|
|
|
|
|
|
Option<(f32, f32, f32, f32)>,
|
|
|
|
|
|
)> {
|
|
|
|
|
|
use crate::io::paper_sizes::{sheet_mm, window_to_sheet, PlotScale};
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if self.tabs[i].scene.current_layout != "Model" {
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
|
|
|
|
|
let (x0, y0, x1, y1) = self.plot_window?;
|
|
|
|
|
|
if (x1 - x0) < 1e-6 || (y1 - y0) < 1e-6 {
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
|
|
|
|
|
let (sheet_w, sheet_h) = sheet_mm(self.plot_format, self.plot_orientation);
|
|
|
|
|
|
let win_w = (x1 - x0).max(1e-9);
|
|
|
|
|
|
let win_h = (y1 - y0).max(1e-9);
|
|
|
|
|
|
let scale_sel = if self.plot_scale.trim().eq_ignore_ascii_case("fit") {
|
|
|
|
|
|
PlotScale::Fit
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let (num, den) = parse_plot_scale(&self.plot_scale);
|
|
|
|
|
|
if num > 0.0 && den > 0.0 {
|
|
|
|
|
|
PlotScale::Ratio(num / den)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
PlotScale::Fit
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
let (scale, ox, oy) = window_to_sheet((win_w, win_h), (sheet_w, sheet_h), scale_sel);
|
|
|
|
|
|
let scene = &self.tabs[i].scene;
|
|
|
|
|
|
let (wx0, wy0, wx1, wy1) = (x0 as f32, y0 as f32, x1 as f32, y1 as f32);
|
|
|
|
|
|
let wires: Vec<_> = scene
|
|
|
|
|
|
.entity_wires()
|
|
|
|
|
|
.into_iter()
|
|
|
|
|
|
.filter(|w| w.aabb[0] <= wx1 && w.aabb[2] >= wx0 && w.aabb[1] <= wy1 && w.aabb[3] >= wy0)
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
let hatches: Vec<_> = scene.paper_canvas_hatches().as_ref().clone();
|
|
|
|
|
|
let wipeouts: Vec<_> = scene.paper_canvas_wipeouts().as_ref().clone();
|
|
|
|
|
|
let offset_x = ((ox / scale) - x0) as f32;
|
|
|
|
|
|
let offset_y = ((oy / scale) - y0) as f32;
|
|
|
|
|
|
let clip = Some((
|
|
|
|
|
|
(ox / scale) as f32,
|
|
|
|
|
|
(oy / scale) as f32,
|
|
|
|
|
|
win_w as f32,
|
|
|
|
|
|
win_h as f32,
|
|
|
|
|
|
));
|
|
|
|
|
|
Some((
|
|
|
|
|
|
wires, hatches, wipeouts, sheet_w, sheet_h, offset_x, offset_y, scale as f32, clip,
|
|
|
|
|
|
))
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_plot_style_panel_apply(&mut self) -> Task<Message> {
|
|
|
|
|
|
let aci = self.plotstyle_panel_aci as usize;
|
|
|
|
|
|
if let Some(table) = self.active_plot_style.as_mut() {
|
|
|
|
|
|
if let Some(entry) = table.aci_entries.get_mut(aci) {
|
|
|
|
|
|
// Parse color.
|
|
|
|
|
|
let color_str = self.ps_color_buf.trim();
|
|
|
|
|
|
if color_str.is_empty() {
|
|
|
|
|
|
entry.color = None;
|
|
|
|
|
|
} else if color_str.starts_with('#') && color_str.len() == 7 {
|
|
|
|
|
|
let r = u8::from_str_radix(&color_str[1..3], 16).unwrap_or(0);
|
|
|
|
|
|
let g = u8::from_str_radix(&color_str[3..5], 16).unwrap_or(0);
|
|
|
|
|
|
let b = u8::from_str_radix(&color_str[5..7], 16).unwrap_or(0);
|
|
|
|
|
|
entry.color = Some([r, g, b]);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Ok(lw) = self.ps_lineweight_buf.trim().parse::<u8>() {
|
|
|
|
|
|
entry.lineweight = lw;
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Ok(sc) = self.ps_screening_buf.trim().parse::<u8>() {
|
|
|
|
|
|
entry.screening = sc.min(100);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("Plot style ACI {aci} updated."));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// No table loaded: create an identity table and apply.
|
|
|
|
|
|
let mut table = crate::io::plot_style::PlotStyleTable::identity("Custom.ctb");
|
|
|
|
|
|
if let Some(entry) = table.aci_entries.get_mut(aci) {
|
|
|
|
|
|
let color_str = self.ps_color_buf.trim();
|
|
|
|
|
|
if color_str.starts_with('#') && color_str.len() == 7 {
|
|
|
|
|
|
let r = u8::from_str_radix(&color_str[1..3], 16).unwrap_or(0);
|
|
|
|
|
|
let g = u8::from_str_radix(&color_str[3..5], 16).unwrap_or(0);
|
|
|
|
|
|
let b = u8::from_str_radix(&color_str[5..7], 16).unwrap_or(0);
|
|
|
|
|
|
entry.color = Some([r, g, b]);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Ok(lw) = self.ps_lineweight_buf.trim().parse::<u8>() {
|
|
|
|
|
|
entry.lineweight = lw;
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Ok(sc) = self.ps_screening_buf.trim().parse::<u8>() {
|
|
|
|
|
|
entry.screening = sc.min(100);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.active_plot_style = Some(table);
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("Created new CTB table, ACI {aci} updated."));
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_plot_style_panel_save(&mut self) -> Task<Message> {
|
|
|
|
|
|
if self.active_plot_style.is_none() {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error("No plot style table loaded. Load or create one first.");
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
let default_name = self
|
|
|
|
|
|
.active_plot_style
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.map(|t| t.name.clone())
|
|
|
|
|
|
.unwrap_or("export.ctb".into());
|
|
|
|
|
|
Task::perform(
|
|
|
|
|
|
async move {
|
|
|
|
|
|
rfd::AsyncFileDialog::new()
|
|
|
|
|
|
.set_title("Save Plot Style Table")
|
|
|
|
|
|
.set_file_name(&default_name)
|
|
|
|
|
|
.add_filter("Plot Style Files", &["ctb", "stb", "CTB", "STB"])
|
|
|
|
|
|
.add_filter("All Files", &["*"])
|
|
|
|
|
|
.save_file()
|
|
|
|
|
|
.await
|
|
|
|
|
|
.map(|h| crate::sys::handle_path(&h))
|
|
|
|
|
|
},
|
|
|
|
|
|
Message::PlotStylePanelSavePath,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|