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};
|
|
|
|
|
|
|
2026-07-25 18:49:18 +03:00
|
|
|
|
pub(super) fn background_task<T, F, M>(work: F, map: M) -> Task<Message>
|
|
|
|
|
|
where
|
|
|
|
|
|
T: Send + 'static,
|
|
|
|
|
|
F: FnOnce() -> T + Send + 'static,
|
|
|
|
|
|
M: FnOnce(T) -> Message + Send + 'static,
|
|
|
|
|
|
{
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
{
|
|
|
|
|
|
let (tx, rx) = iced::futures::channel::oneshot::channel();
|
|
|
|
|
|
std::thread::spawn(move || {
|
|
|
|
|
|
let _ = tx.send(work());
|
|
|
|
|
|
});
|
|
|
|
|
|
Task::perform(
|
|
|
|
|
|
async move { rx.await.expect("background export worker dropped") },
|
|
|
|
|
|
map,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
{
|
|
|
|
|
|
Task::perform(async move { work() }, map)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
|
2026-07-27 13:13:58 +03:00
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
fn native_paths_match(left: &std::path::Path, right: &std::path::Path) -> bool {
|
|
|
|
|
|
match (
|
|
|
|
|
|
std::fs::canonicalize(left),
|
|
|
|
|
|
std::fs::canonicalize(right),
|
|
|
|
|
|
) {
|
|
|
|
|
|
(Ok(left), Ok(right)) => left == right,
|
|
|
|
|
|
_ => left == right,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-31 11:16:14 +03:00
|
|
|
|
type LayoutPlotParams = (
|
|
|
|
|
|
std::sync::Arc<Vec<crate::scene::WireModel>>,
|
|
|
|
|
|
Vec<crate::scene::model::hatch_model::HatchModel>,
|
|
|
|
|
|
Vec<crate::scene::model::hatch_model::HatchModel>,
|
|
|
|
|
|
f64,
|
|
|
|
|
|
f64,
|
|
|
|
|
|
f64,
|
|
|
|
|
|
f64,
|
|
|
|
|
|
i32,
|
|
|
|
|
|
f32,
|
|
|
|
|
|
Option<(f32, f32, f32, f32)>,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
type ClippedPlotParams = (
|
|
|
|
|
|
Vec<crate::scene::WireModel>,
|
|
|
|
|
|
Vec<crate::scene::model::hatch_model::HatchModel>,
|
|
|
|
|
|
Vec<crate::scene::model::hatch_model::HatchModel>,
|
|
|
|
|
|
f64,
|
|
|
|
|
|
f64,
|
|
|
|
|
|
f64,
|
|
|
|
|
|
f64,
|
|
|
|
|
|
i32,
|
|
|
|
|
|
f32,
|
|
|
|
|
|
Option<(f32, f32, f32, f32)>,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
fn plot_content_extents(
|
|
|
|
|
|
wires: &[crate::scene::WireModel],
|
|
|
|
|
|
hatches: &[crate::scene::model::hatch_model::HatchModel],
|
|
|
|
|
|
wipeouts: &[crate::scene::model::hatch_model::HatchModel],
|
|
|
|
|
|
) -> Option<(f64, f64, f64, f64)> {
|
|
|
|
|
|
let mut bounds = (
|
|
|
|
|
|
f64::INFINITY,
|
|
|
|
|
|
f64::INFINITY,
|
|
|
|
|
|
f64::NEG_INFINITY,
|
|
|
|
|
|
f64::NEG_INFINITY,
|
|
|
|
|
|
);
|
|
|
|
|
|
let mut include = |x: f64, y: f64| {
|
|
|
|
|
|
if x.is_finite() && y.is_finite() {
|
|
|
|
|
|
bounds.0 = bounds.0.min(x);
|
|
|
|
|
|
bounds.1 = bounds.1.min(y);
|
|
|
|
|
|
bounds.2 = bounds.2.max(x);
|
|
|
|
|
|
bounds.3 = bounds.3.max(y);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
for wire in wires {
|
|
|
|
|
|
let [x0, y0, x1, y1] = wire.aabb;
|
|
|
|
|
|
include(x0 as f64, y0 as f64);
|
|
|
|
|
|
include(x1 as f64, y1 as f64);
|
|
|
|
|
|
}
|
|
|
|
|
|
for hatch in wipeouts.iter().chain(hatches.iter()) {
|
|
|
|
|
|
for &[x, y] in hatch.boundary.iter() {
|
|
|
|
|
|
include(hatch.world_origin[0] + x as f64, hatch.world_origin[1] + y as f64);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
(bounds.0.is_finite()
|
|
|
|
|
|
&& bounds.1.is_finite()
|
|
|
|
|
|
&& bounds.2.is_finite()
|
|
|
|
|
|
&& bounds.3.is_finite()
|
|
|
|
|
|
&& bounds.2 > bounds.0
|
|
|
|
|
|
&& bounds.3 > bounds.1)
|
|
|
|
|
|
.then_some(bounds)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn plot_scene_content(
|
|
|
|
|
|
scene: &crate::scene::Scene,
|
|
|
|
|
|
) -> (
|
|
|
|
|
|
std::sync::Arc<Vec<crate::scene::WireModel>>,
|
|
|
|
|
|
Vec<crate::scene::model::hatch_model::HatchModel>,
|
|
|
|
|
|
Vec<crate::scene::model::hatch_model::HatchModel>,
|
|
|
|
|
|
) {
|
|
|
|
|
|
let mut wires = scene.entity_wires().as_ref().clone();
|
|
|
|
|
|
let mut hatches = scene.paper_canvas_hatches().as_ref().clone();
|
|
|
|
|
|
let mut wipeouts = scene.paper_canvas_wipeouts().as_ref().clone();
|
|
|
|
|
|
if scene.current_layout != "Model" {
|
|
|
|
|
|
let (pattern_wires, viewport_hatches, viewport_wipeouts) =
|
|
|
|
|
|
scene.viewport_plot_fills();
|
|
|
|
|
|
wires.extend(pattern_wires);
|
|
|
|
|
|
hatches.extend(viewport_hatches);
|
|
|
|
|
|
wipeouts.extend(viewport_wipeouts);
|
|
|
|
|
|
}
|
|
|
|
|
|
(std::sync::Arc::new(wires), hatches, wipeouts)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
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,
|
|
|
|
|
|
polar: self.polar_mode,
|
|
|
|
|
|
polar_increment_deg: self.polar_increment_deg,
|
|
|
|
|
|
otrack: self.snapper.otrack_enabled,
|
|
|
|
|
|
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(),
|
2026-07-21 19:43:11 +03:00
|
|
|
|
literal_spaces: self.command_line.literal_spaces,
|
2026-07-21 22:00:55 +03:00
|
|
|
|
osmode: crate::app::settings::osmode_from_snaps(
|
|
|
|
|
|
self.snapper.enabled.iter(),
|
|
|
|
|
|
self.snapper.snap_enabled,
|
|
|
|
|
|
),
|
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
|
|
|
|
texteditmode: self.texteditmode,
|
2026-07-09 16:41:53 +03:00
|
|
|
|
textfill: crate::scene::text::sdf_atlas::textfill(),
|
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,
|
2026-07-27 22:37:47 +03:00
|
|
|
|
default_save_format: self.default_save_format.clone(),
|
2026-07-22 21:35:37 +03:00
|
|
|
|
pick_add: self.pick_add,
|
|
|
|
|
|
pick_drag_rect: self.pick_drag_rect,
|
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.polar_mode = s.polar;
|
|
|
|
|
|
self.polar_increment_deg = s.polar_increment_deg;
|
refactor(settings): drop Ortho/OSNAP from settings.txt, now per-drawing only
Ortho and the running object-snap set are stored in the drawing header
($ORTHOMODE / $OSMODE) since they were bound to the file, so persisting them
app-globally in settings.txt too was redundant. Remove the ortho, osnap and
snap_modes keys (and the now-dead modes_from / snap_id / snap_from_id helpers)
from UserSettings, and stop reading/writing them in apply_settings /
current_settings.
No boot behavior change: Snapper::new already defaults to the same running-snap
set (Endpoint, Midpoint, Center, Node, Quadrant, Intersection, Nearest) with
the master off, and Ortho defaults to false — exactly what settings.txt used to
seed. Old settings.txt files keep working; the stale keys are simply ignored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:05:12 +03:00
|
|
|
|
// Ortho + running OSNAP are per-drawing (adopted from the header on
|
|
|
|
|
|
// open / tab switch), not app-global, so they are not applied here.
|
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.snapper.otrack_enabled = s.otrack;
|
|
|
|
|
|
self.default_assoc_prompted = s.default_assoc_prompted;
|
|
|
|
|
|
self.disabled_plugins = s.disabled_plugins.iter().cloned().collect();
|
|
|
|
|
|
self.plugin_repos = s.plugin_repos.clone();
|
2026-07-21 19:43:11 +03:00
|
|
|
|
self.command_line.literal_spaces = s.literal_spaces;
|
2026-07-21 22:00:55 +03:00
|
|
|
|
let (modes, snap_enabled) = crate::app::settings::snaps_from_osmode(s.osmode);
|
|
|
|
|
|
self.snapper.enabled = modes.into_iter().collect();
|
|
|
|
|
|
self.snapper.snap_enabled = snap_enabled;
|
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.texteditmode = s.texteditmode;
|
2026-07-09 16:41:53 +03:00
|
|
|
|
crate::scene::text::sdf_atlas::set_textfill(s.textfill);
|
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;
|
2026-07-27 22:37:47 +03:00
|
|
|
|
self.default_save_format =
|
|
|
|
|
|
crate::io::canonical_save_format(&s.default_save_format).to_string();
|
2026-07-22 21:35:37 +03:00
|
|
|
|
self.pick_add = s.pick_add;
|
|
|
|
|
|
self.pick_drag_rect = s.pick_drag_rect;
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(sysvars): persist Ortho, running OSNAP and Isolate in the drawing
Ortho and the running object-snap set were app-global (settings.txt only) and
Isolate/Hide was a session-only view state, so none survived save/reopen and
none were per-drawing. Bind all three to the file like AutoCAD:
- Ortho -> header.ortho_mode ($ORTHOMODE); running OSNAP <-> header
.object_snap_mode ($OSMODE bitmask) via a new SnapType<->bit map in settings
(osmode_from_snaps / snaps_from_osmode, round-trip tested). The header is the
per-drawing store, live state mirrors the active drawing: adopt header->live
on open and tab-switch, stamp live->header on save and when leaving a tab, a
fresh drawing inherits the current values, and SETVAR ORTHOMODE/OSMODE now
also update the live state. settings.txt stays as the boot/new-drawing
default; the per-file header overrides it on open.
- Isolate/Hide now set each entity's persisted invisible flag (DXF code 60,
which acadrust round-trips in DXF and DWG) in addition to the session set;
End Isolation clears it. The set is reseeded from invisible entities on load
and after undo/redo, the commands are undoable and mark the tab dirty. The
renderer already skipped invisible entities, so no render change was needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:45:08 +03:00
|
|
|
|
/// Adopt the per-drawing sysvars stored in tab `i`'s document header —
|
|
|
|
|
|
/// Ortho (`$ORTHOMODE`) and the running object-snap set (`$OSMODE`) — into
|
|
|
|
|
|
/// the live app state, so each drawing keeps its own. Called when a drawing
|
|
|
|
|
|
/// becomes active (open, tab switch). The app-settings values seed the boot
|
|
|
|
|
|
/// default; the per-file header overrides it here.
|
|
|
|
|
|
pub(in crate::app) fn adopt_header_sysvars(&mut self, i: usize) {
|
|
|
|
|
|
if self.tabs[i].is_start {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
let (ortho, osmode) = {
|
|
|
|
|
|
let h = &self.tabs[i].scene.document.header;
|
|
|
|
|
|
(h.ortho_mode, h.object_snap_mode)
|
|
|
|
|
|
};
|
|
|
|
|
|
self.ortho_mode = ortho;
|
|
|
|
|
|
// Ortho and Polar are mutually exclusive (ToggleOrtho clears Polar).
|
|
|
|
|
|
if ortho {
|
|
|
|
|
|
self.polar_mode = false;
|
|
|
|
|
|
}
|
2026-07-21 22:00:55 +03:00
|
|
|
|
// OSMODE has no file slot in modern DWG (R2000+ moved it to the
|
|
|
|
|
|
// registry), so a header value of 0 just means "absent" — keep the
|
|
|
|
|
|
// user's app-level set instead of wiping it. Only a legacy R13/R14 or
|
|
|
|
|
|
// DXF file that really carries a nonzero mask overrides.
|
|
|
|
|
|
if osmode != 0 {
|
|
|
|
|
|
let (modes, snap_enabled) = crate::app::settings::snaps_from_osmode(osmode);
|
|
|
|
|
|
self.snapper.enabled = modes.into_iter().collect();
|
|
|
|
|
|
self.snapper.snap_enabled = snap_enabled;
|
|
|
|
|
|
}
|
feat(sysvars): persist Ortho, running OSNAP and Isolate in the drawing
Ortho and the running object-snap set were app-global (settings.txt only) and
Isolate/Hide was a session-only view state, so none survived save/reopen and
none were per-drawing. Bind all three to the file like AutoCAD:
- Ortho -> header.ortho_mode ($ORTHOMODE); running OSNAP <-> header
.object_snap_mode ($OSMODE bitmask) via a new SnapType<->bit map in settings
(osmode_from_snaps / snaps_from_osmode, round-trip tested). The header is the
per-drawing store, live state mirrors the active drawing: adopt header->live
on open and tab-switch, stamp live->header on save and when leaving a tab, a
fresh drawing inherits the current values, and SETVAR ORTHOMODE/OSMODE now
also update the live state. settings.txt stays as the boot/new-drawing
default; the per-file header overrides it on open.
- Isolate/Hide now set each entity's persisted invisible flag (DXF code 60,
which acadrust round-trips in DXF and DWG) in addition to the session set;
End Isolation clears it. The set is reseeded from invisible entities on load
and after undo/redo, the commands are undoable and mark the tab dirty. The
renderer already skipped invisible entities, so no render change was needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:45:08 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Stamp the live per-drawing sysvars (Ortho, running OSNAP) onto tab `i`'s
|
|
|
|
|
|
/// document header just before it is written to disk (or before switching
|
|
|
|
|
|
/// away from it), so they persist with the drawing.
|
|
|
|
|
|
pub(in crate::app) fn stamp_header_sysvars(&mut self, i: usize) {
|
|
|
|
|
|
if self.tabs[i].is_start {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
let osmode = crate::app::settings::osmode_from_snaps(
|
|
|
|
|
|
self.snapper.enabled.iter(),
|
|
|
|
|
|
self.snapper.snap_enabled,
|
|
|
|
|
|
);
|
|
|
|
|
|
let h = &mut self.tabs[i].scene.document.header;
|
|
|
|
|
|
h.ortho_mode = self.ortho_mode;
|
|
|
|
|
|
h.object_snap_mode = osmode;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
/// 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,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 16:34:07 +03:00
|
|
|
|
/// Background task: fetch `owner/repo`'s installable releases and their
|
|
|
|
|
|
/// manifest API versions.
|
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
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub(in crate::app) fn fetch_releases_task(&self, repo: String) -> Task<Message> {
|
|
|
|
|
|
let label = repo.clone();
|
|
|
|
|
|
Task::perform(
|
2026-07-30 16:34:07 +03:00
|
|
|
|
async move { crate::plugin::marketplace::fetch_release_info(&repo) },
|
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
|
|
|
|
move |res| Message::PluginReleasesFetched(label, res),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
2026-07-29 16:17:17 +03:00
|
|
|
|
pub(in crate::app) fn fetch_releases_task(&self, repo: String) -> Task<Message> {
|
|
|
|
|
|
Task::done(Message::PluginReleasesFetched(
|
|
|
|
|
|
repo,
|
|
|
|
|
|
Err("External plugins are available in the desktop app.".to_string()),
|
|
|
|
|
|
))
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-29 14:27:32 +03:00
|
|
|
|
/// Background task: fetch a repository README from its default branch.
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub(in crate::app) fn fetch_plugin_readme_task(&self, repo: String) -> Task<Message> {
|
|
|
|
|
|
let label = repo.clone();
|
|
|
|
|
|
Task::perform(
|
|
|
|
|
|
async move { crate::plugin::marketplace::fetch_readme(&repo) },
|
|
|
|
|
|
move |result| Message::PluginReadmeFetched(label, result),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
2026-07-29 16:17:17 +03:00
|
|
|
|
pub(in crate::app) fn fetch_plugin_readme_task(&self, repo: String) -> Task<Message> {
|
|
|
|
|
|
Task::done(Message::PluginReadmeFetched(
|
|
|
|
|
|
repo,
|
|
|
|
|
|
Err("Plugin details are available in the desktop app.".to_string()),
|
|
|
|
|
|
))
|
2026-07-29 14:27:32 +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
|
|
|
|
/// 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"))?;
|
2026-07-29 14:27:32 +03:00
|
|
|
|
crate::plugin::marketplace::install(&rel, &repo)
|
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
|
|
|
|
},
|
|
|
|
|
|
Message::PluginInstalled,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
pub(in crate::app) fn install_task(&self, _repo: String, _tag: String) -> Task<Message> {
|
2026-07-29 16:17:17 +03:00
|
|
|
|
Task::done(Message::PluginInstalled(Err(
|
|
|
|
|
|
"External plugins are available in the desktop app.".to_string(),
|
|
|
|
|
|
)))
|
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
|
|
|
|
}
|
|
|
|
|
|
|
refactor(config): consolidate settings into one grouped settings.json
App preferences were spread across six flat plain-text files (settings.txt,
recent.txt, recent_limit.txt, statusbar.txt, ribbon.txt, plot.txt), each with a
bespoke line parser and no grouping. Replace them with a single grouped JSON,
serialized via serde (enabling the derive feature; serde_json was already a
dependency).
- New app/config.rs: AppConfig { settings, recent, statusbar, ribbon, plot } →
<config>/settings.json (pretty JSON), load()/save().
- Make UserSettings, StatusBarConfig/StatusPill, CollapseMode and
PlotDialogState serde-derived; #[serde(skip)] marks the plot dialog's runtime
fields so only print preferences persist. Their bespoke .txt load/save is
removed.
- App gains current_config/apply_config/save_config: one AppConfig::load at
startup distributes into live state; every settings-change site
(persist_settings_if_changed, recent add/remove/limit, status-bar pill
toggle, ribbon density, plot commit) routes through save_config, which diffs
against the last write to avoid thrashing.
- Clean start: the old flat files are ignored (no migration). ocad.pgp stays a
separate hand-editable file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:41:34 +03:00
|
|
|
|
/// Gather the full persisted config (all sections) from live app state.
|
|
|
|
|
|
pub(in crate::app) fn current_config(&self) -> crate::app::config::AppConfig {
|
|
|
|
|
|
crate::app::config::AppConfig {
|
|
|
|
|
|
settings: self.current_settings(),
|
2026-07-28 09:33:12 +03:00
|
|
|
|
theme: self.ui_theme.clone(),
|
refactor(config): consolidate settings into one grouped settings.json
App preferences were spread across six flat plain-text files (settings.txt,
recent.txt, recent_limit.txt, statusbar.txt, ribbon.txt, plot.txt), each with a
bespoke line parser and no grouping. Replace them with a single grouped JSON,
serialized via serde (enabling the derive feature; serde_json was already a
dependency).
- New app/config.rs: AppConfig { settings, recent, statusbar, ribbon, plot } →
<config>/settings.json (pretty JSON), load()/save().
- Make UserSettings, StatusBarConfig/StatusPill, CollapseMode and
PlotDialogState serde-derived; #[serde(skip)] marks the plot dialog's runtime
fields so only print preferences persist. Their bespoke .txt load/save is
removed.
- App gains current_config/apply_config/save_config: one AppConfig::load at
startup distributes into live state; every settings-change site
(persist_settings_if_changed, recent add/remove/limit, status-bar pill
toggle, ribbon density, plot commit) routes through save_config, which diffs
against the last write to avoid thrashing.
- Clean start: the old flat files are ignored (no migration). ocad.pgp stays a
separate hand-editable file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:41:34 +03:00
|
|
|
|
recent: crate::app::config::RecentConfig {
|
|
|
|
|
|
files: self
|
|
|
|
|
|
.recent_files
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|p| p.to_string_lossy().into_owned())
|
|
|
|
|
|
.collect(),
|
|
|
|
|
|
limit: self.recent_limit,
|
|
|
|
|
|
},
|
2026-07-24 17:44:07 +03:00
|
|
|
|
start: crate::app::config::StartConfig {
|
|
|
|
|
|
section: self.start_section,
|
|
|
|
|
|
},
|
refactor(config): consolidate settings into one grouped settings.json
App preferences were spread across six flat plain-text files (settings.txt,
recent.txt, recent_limit.txt, statusbar.txt, ribbon.txt, plot.txt), each with a
bespoke line parser and no grouping. Replace them with a single grouped JSON,
serialized via serde (enabling the derive feature; serde_json was already a
dependency).
- New app/config.rs: AppConfig { settings, recent, statusbar, ribbon, plot } →
<config>/settings.json (pretty JSON), load()/save().
- Make UserSettings, StatusBarConfig/StatusPill, CollapseMode and
PlotDialogState serde-derived; #[serde(skip)] marks the plot dialog's runtime
fields so only print preferences persist. Their bespoke .txt load/save is
removed.
- App gains current_config/apply_config/save_config: one AppConfig::load at
startup distributes into live state; every settings-change site
(persist_settings_if_changed, recent add/remove/limit, status-bar pill
toggle, ribbon density, plot commit) routes through save_config, which diffs
against the last write to avoid thrashing.
- Clean start: the old flat files are ignored (no migration). ocad.pgp stays a
separate hand-editable file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:41:34 +03:00
|
|
|
|
statusbar: self.statusbar_config.clone(),
|
|
|
|
|
|
ribbon: crate::app::config::RibbonConfig {
|
|
|
|
|
|
collapse: self.ribbon.collapse_mode(),
|
|
|
|
|
|
},
|
|
|
|
|
|
plot: self.plot_dialog.clone(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Distribute a loaded config into live app state (called once at startup).
|
|
|
|
|
|
pub(in crate::app) fn apply_config(&mut self, cfg: crate::app::config::AppConfig) {
|
|
|
|
|
|
self.apply_settings(&cfg.settings);
|
2026-07-28 09:33:12 +03:00
|
|
|
|
self.ui_theme = cfg.theme.clone();
|
|
|
|
|
|
self.active_theme = self.ui_theme.to_iced();
|
|
|
|
|
|
self.theme_color_inputs = self.ui_theme.palette.hex_values();
|
refactor(config): consolidate settings into one grouped settings.json
App preferences were spread across six flat plain-text files (settings.txt,
recent.txt, recent_limit.txt, statusbar.txt, ribbon.txt, plot.txt), each with a
bespoke line parser and no grouping. Replace them with a single grouped JSON,
serialized via serde (enabling the derive feature; serde_json was already a
dependency).
- New app/config.rs: AppConfig { settings, recent, statusbar, ribbon, plot } →
<config>/settings.json (pretty JSON), load()/save().
- Make UserSettings, StatusBarConfig/StatusPill, CollapseMode and
PlotDialogState serde-derived; #[serde(skip)] marks the plot dialog's runtime
fields so only print preferences persist. Their bespoke .txt load/save is
removed.
- App gains current_config/apply_config/save_config: one AppConfig::load at
startup distributes into live state; every settings-change site
(persist_settings_if_changed, recent add/remove/limit, status-bar pill
toggle, ribbon density, plot commit) routes through save_config, which diffs
against the last write to avoid thrashing.
- Clean start: the old flat files are ignored (no migration). ocad.pgp stays a
separate hand-editable file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:41:34 +03:00
|
|
|
|
self.recent_files = cfg
|
|
|
|
|
|
.recent
|
|
|
|
|
|
.files
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(std::path::PathBuf::from)
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
self.recent_limit = cfg
|
|
|
|
|
|
.recent
|
|
|
|
|
|
.limit
|
|
|
|
|
|
.clamp(crate::app::recent::RECENT_MIN, crate::app::recent::RECENT_MAX);
|
|
|
|
|
|
self.recent_files.truncate(self.recent_limit);
|
|
|
|
|
|
self.recent_limit_input = self.recent_limit.to_string();
|
2026-07-21 10:06:55 +03:00
|
|
|
|
// Thumbnails are decoded by a background task queued at boot
|
|
|
|
|
|
// (`refresh_recent_thumbs`) — never here on the boot path.
|
2026-07-24 17:44:07 +03:00
|
|
|
|
self.start_section = cfg.start.section;
|
refactor(config): consolidate settings into one grouped settings.json
App preferences were spread across six flat plain-text files (settings.txt,
recent.txt, recent_limit.txt, statusbar.txt, ribbon.txt, plot.txt), each with a
bespoke line parser and no grouping. Replace them with a single grouped JSON,
serialized via serde (enabling the derive feature; serde_json was already a
dependency).
- New app/config.rs: AppConfig { settings, recent, statusbar, ribbon, plot } →
<config>/settings.json (pretty JSON), load()/save().
- Make UserSettings, StatusBarConfig/StatusPill, CollapseMode and
PlotDialogState serde-derived; #[serde(skip)] marks the plot dialog's runtime
fields so only print preferences persist. Their bespoke .txt load/save is
removed.
- App gains current_config/apply_config/save_config: one AppConfig::load at
startup distributes into live state; every settings-change site
(persist_settings_if_changed, recent add/remove/limit, status-bar pill
toggle, ribbon density, plot commit) routes through save_config, which diffs
against the last write to avoid thrashing.
- Clean start: the old flat files are ignored (no migration). ocad.pgp stays a
separate hand-editable file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:41:34 +03:00
|
|
|
|
self.statusbar_config = cfg.statusbar;
|
|
|
|
|
|
self.ribbon.set_collapse_mode(cfg.ribbon.collapse);
|
|
|
|
|
|
self.plot_dialog = cfg.plot;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-29 00:41:51 +03:00
|
|
|
|
/// Write the config only when it changed since the last write, so a toggle
|
|
|
|
|
|
/// persists immediately without thrashing native or browser storage.
|
refactor(config): consolidate settings into one grouped settings.json
App preferences were spread across six flat plain-text files (settings.txt,
recent.txt, recent_limit.txt, statusbar.txt, ribbon.txt, plot.txt), each with a
bespoke line parser and no grouping. Replace them with a single grouped JSON,
serialized via serde (enabling the derive feature; serde_json was already a
dependency).
- New app/config.rs: AppConfig { settings, recent, statusbar, ribbon, plot } →
<config>/settings.json (pretty JSON), load()/save().
- Make UserSettings, StatusBarConfig/StatusPill, CollapseMode and
PlotDialogState serde-derived; #[serde(skip)] marks the plot dialog's runtime
fields so only print preferences persist. Their bespoke .txt load/save is
removed.
- App gains current_config/apply_config/save_config: one AppConfig::load at
startup distributes into live state; every settings-change site
(persist_settings_if_changed, recent add/remove/limit, status-bar pill
toggle, ribbon density, plot commit) routes through save_config, which diffs
against the last write to avoid thrashing.
- Clean start: the old flat files are ignored (no migration). ocad.pgp stays a
separate hand-editable file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:41:34 +03:00
|
|
|
|
pub(in crate::app) fn save_config(&mut self) {
|
|
|
|
|
|
let cur = self.current_config();
|
|
|
|
|
|
if self.last_saved_config.as_ref() != Some(&cur) {
|
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
|
|
|
|
cur.save();
|
refactor(config): consolidate settings into one grouped settings.json
App preferences were spread across six flat plain-text files (settings.txt,
recent.txt, recent_limit.txt, statusbar.txt, ribbon.txt, plot.txt), each with a
bespoke line parser and no grouping. Replace them with a single grouped JSON,
serialized via serde (enabling the derive feature; serde_json was already a
dependency).
- New app/config.rs: AppConfig { settings, recent, statusbar, ribbon, plot } →
<config>/settings.json (pretty JSON), load()/save().
- Make UserSettings, StatusBarConfig/StatusPill, CollapseMode and
PlotDialogState serde-derived; #[serde(skip)] marks the plot dialog's runtime
fields so only print preferences persist. Their bespoke .txt load/save is
removed.
- App gains current_config/apply_config/save_config: one AppConfig::load at
startup distributes into live state; every settings-change site
(persist_settings_if_changed, recent add/remove/limit, status-bar pill
toggle, ribbon density, plot commit) routes through save_config, which diffs
against the last write to avoid thrashing.
- Clean start: the old flat files are ignored (no migration). ocad.pgp stays a
separate hand-editable file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:41:34 +03:00
|
|
|
|
self.last_saved_config = Some(cur);
|
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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor(config): consolidate settings into one grouped settings.json
App preferences were spread across six flat plain-text files (settings.txt,
recent.txt, recent_limit.txt, statusbar.txt, ribbon.txt, plot.txt), each with a
bespoke line parser and no grouping. Replace them with a single grouped JSON,
serialized via serde (enabling the derive feature; serde_json was already a
dependency).
- New app/config.rs: AppConfig { settings, recent, statusbar, ribbon, plot } →
<config>/settings.json (pretty JSON), load()/save().
- Make UserSettings, StatusBarConfig/StatusPill, CollapseMode and
PlotDialogState serde-derived; #[serde(skip)] marks the plot dialog's runtime
fields so only print preferences persist. Their bespoke .txt load/save is
removed.
- App gains current_config/apply_config/save_config: one AppConfig::load at
startup distributes into live state; every settings-change site
(persist_settings_if_changed, recent add/remove/limit, status-bar pill
toggle, ribbon density, plot commit) routes through save_config, which diffs
against the last write to avoid thrashing.
- Clean start: the old flat files are ignored (no migration). ocad.pgp stays a
separate hand-editable file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:41:34 +03:00
|
|
|
|
/// Back-compat name for the many "a preference changed, persist it" sites.
|
|
|
|
|
|
pub(in crate::app) fn persist_settings_if_changed(&mut self) {
|
|
|
|
|
|
self.save_config();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
/// 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.
|
2026-07-25 18:49:18 +03:00
|
|
|
|
let state = std::sync::Arc::new(crate::io::OpenProgressState::new(
|
|
|
|
|
|
crate::app::OPEN_PHASE_READING,
|
|
|
|
|
|
));
|
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.opening = Some(crate::app::OpenProgress {
|
|
|
|
|
|
name: "Opening…".into(),
|
|
|
|
|
|
size_bytes: 0,
|
2026-07-27 13:13:58 +03:00
|
|
|
|
state: state.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
|
|
|
|
started: Instant::now(),
|
|
|
|
|
|
});
|
2026-07-25 18:49:18 +03:00
|
|
|
|
Task::perform(crate::io::pick_and_load_web(state), Message::FileOpened)
|
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(open): open drawings in the running editor
Double-clicking a drawing started a whole second copy of the app. Now it
lands as a tab in the editor already open, and that window comes forward.
The election is the bind itself: every GUI launch tries a deterministic
per-user loopback port, and the OS grants it to exactly one process and
releases it on any death, SIGKILL included. Nothing to reap, no PID to
probe, no stale lock after a crash. The winner serves the port from an
iced subscription; the loser checks it is talking to a matching editor,
hands the paths over and exits. Every surprise — no answer, a stranger on
the port, a timeout — boots a normal window, i.e. the old behaviour: the
feature can degrade, but it cannot lose the file.
The gate in main is positional, after every headless mode has returned.
The plugin runner is this same binary re-spawning itself, so a flag list
would rot the first time a mode is added; a position cannot.
Handing a drawing over that is already open switches to its tab instead
of loading a second copy, comparing resolved paths so a symlink or a
`..` hop is still recognised as the same file.
`opening` is a single slot that a second open would overwrite, and
on_file_opened drops any result arriving once it is clear — so drawings
handed over at once would silently vanish. They queue in pending_opens,
drained from every path that clears the slot, error and cancel included.
Exec becomes %F so one launch takes the whole selection; the CLI takes a
list and boot fans it out through the same door. --new-instance forces a
separate process; --read-only / --script / --new always get their own.
Not reused: the automation server. It dispatches arbitrary commands and
writes arbitrary paths, and its open replaces the active tab's document
in place — publishing that on a port every local process can reach would
be unforced privilege escalation. Only its JSON-lines framing is shared.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:18:16 +03:00
|
|
|
|
/// Index of a tab already showing `path`, or `None`.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Compares resolved paths, so the same drawing reached through a symlink,
|
|
|
|
|
|
/// a `..` segment or a different relative spelling is recognised as the one
|
|
|
|
|
|
/// already open rather than loaded a second time. A path that cannot be
|
|
|
|
|
|
/// resolved (deleted since) matches nothing and falls through to the normal
|
|
|
|
|
|
/// open, which reports the miss.
|
|
|
|
|
|
pub(in crate::app) fn tab_showing(&self, path: &std::path::Path) -> Option<usize> {
|
2026-07-29 00:41:51 +03:00
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
{
|
|
|
|
|
|
return self
|
|
|
|
|
|
.tabs
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.position(|tab| tab.current_path.as_deref() == Some(path));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
{
|
|
|
|
|
|
let want = std::fs::canonicalize(path).ok()?;
|
|
|
|
|
|
self.tabs.iter().position(|t| {
|
|
|
|
|
|
t.current_path
|
|
|
|
|
|
.as_deref()
|
|
|
|
|
|
.and_then(|p| std::fs::canonicalize(p).ok())
|
|
|
|
|
|
.is_some_and(|p| p == want)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
feat(open): open drawings in the running editor
Double-clicking a drawing started a whole second copy of the app. Now it
lands as a tab in the editor already open, and that window comes forward.
The election is the bind itself: every GUI launch tries a deterministic
per-user loopback port, and the OS grants it to exactly one process and
releases it on any death, SIGKILL included. Nothing to reap, no PID to
probe, no stale lock after a crash. The winner serves the port from an
iced subscription; the loser checks it is talking to a matching editor,
hands the paths over and exits. Every surprise — no answer, a stranger on
the port, a timeout — boots a normal window, i.e. the old behaviour: the
feature can degrade, but it cannot lose the file.
The gate in main is positional, after every headless mode has returned.
The plugin runner is this same binary re-spawning itself, so a flag list
would rot the first time a mode is added; a position cannot.
Handing a drawing over that is already open switches to its tab instead
of loading a second copy, comparing resolved paths so a symlink or a
`..` hop is still recognised as the same file.
`opening` is a single slot that a second open would overwrite, and
on_file_opened drops any result arriving once it is clear — so drawings
handed over at once would silently vanish. They queue in pending_opens,
drained from every path that clears the slot, error and cancel included.
Exec becomes %F so one launch takes the whole selection; the CLI takes a
list and boot fans it out through the same door. --new-instance forces a
separate process; --read-only / --script / --new always get their own.
Not reused: the automation server. It dispatches arbitrary commands and
writes arbitrary paths, and its open replaces the active tab's document
in place — publishing that on a port every local process can reach would
be unforced privilege escalation. Only its JSON-lines framing is shared.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:18:16 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Start the next drawing a second launch handed us, if any.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Must be called from EVERY path that clears `opening` — completion, error
|
|
|
|
|
|
/// and cancel alike. Draining only the success path would strand the queue
|
|
|
|
|
|
/// forever the first time a file fails to parse.
|
|
|
|
|
|
pub(in crate::app) fn drain_pending_open(&mut self) -> Task<Message> {
|
|
|
|
|
|
match self.pending_opens.pop_front() {
|
|
|
|
|
|
Some(p) => Task::done(Message::OpenExternal(p)),
|
|
|
|
|
|
None => Task::none(),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-27 13:13:58 +03:00
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
fn install_native_edit_guard(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
i: usize,
|
|
|
|
|
|
path: &std::path::Path,
|
|
|
|
|
|
loaded_fingerprint: Option<crate::io::edit_lock::FileFingerprint>,
|
|
|
|
|
|
) {
|
|
|
|
|
|
let current_fingerprint =
|
|
|
|
|
|
crate::io::edit_lock::FileFingerprint::capture(path).ok();
|
|
|
|
|
|
if loaded_fingerprint
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.zip(current_fingerprint.as_ref())
|
|
|
|
|
|
.is_some_and(|(loaded, current)| loaded != current)
|
|
|
|
|
|
{
|
|
|
|
|
|
self.command_line.push_error_once(
|
|
|
|
|
|
"Drawing changed on disk while it was opening; Save will require conflict resolution.",
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].disk_fingerprint =
|
|
|
|
|
|
loaded_fingerprint.or(current_fingerprint);
|
|
|
|
|
|
match crate::io::edit_lock::EditLease::acquire(path) {
|
|
|
|
|
|
Ok(lease) => {
|
|
|
|
|
|
if let Some(warning) = lease.platform_warning() {
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
"Edit lease active; {warning}. External-change checks remain active."
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].edit_lease = Some(lease);
|
|
|
|
|
|
self.tabs[i].edit_lock_conflict = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(crate::io::edit_lock::EditLeaseError::Locked(error)) => {
|
|
|
|
|
|
self.tabs[i].edit_lease = None;
|
|
|
|
|
|
self.tabs[i].edit_lock_conflict = true;
|
|
|
|
|
|
self.command_line.push_error_once(&format!(
|
|
|
|
|
|
"Opened read-only against other editors: {error}"
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(crate::io::edit_lock::EditLeaseError::Unavailable(error)) => {
|
|
|
|
|
|
self.tabs[i].edit_lease = None;
|
|
|
|
|
|
self.tabs[i].edit_lock_conflict = false;
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
"{error}. External-change checks remain active."
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub(in crate::app) fn refresh_native_edit_guard_after_save(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
i: usize,
|
|
|
|
|
|
path: &std::path::Path,
|
|
|
|
|
|
path_changed: bool,
|
|
|
|
|
|
destination_lease: Option<crate::io::edit_lock::EditLease>,
|
|
|
|
|
|
) {
|
|
|
|
|
|
if path_changed {
|
|
|
|
|
|
self.tabs[i].edit_lease = None;
|
|
|
|
|
|
self.tabs[i].edit_lease = destination_lease;
|
|
|
|
|
|
self.tabs[i].edit_lock_conflict = false;
|
|
|
|
|
|
if self.tabs[i].edit_lease.is_none() {
|
|
|
|
|
|
self.install_native_edit_guard(i, path, None);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let refresh = self.tabs[i].edit_lease.as_mut().map(|lease| {
|
|
|
|
|
|
lease
|
|
|
|
|
|
.refresh_drawing_lock(path)
|
|
|
|
|
|
.map(|_| lease.platform_warning().map(str::to_owned))
|
|
|
|
|
|
});
|
|
|
|
|
|
match refresh {
|
|
|
|
|
|
Some(Ok(warning)) => {
|
|
|
|
|
|
self.tabs[i].edit_lock_conflict = false;
|
|
|
|
|
|
if let Some(warning) = warning {
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
"{warning}. External-change checks remain active."
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Some(Err(crate::io::edit_lock::EditLeaseError::Locked(error))) => {
|
|
|
|
|
|
self.tabs[i].edit_lock_conflict = true;
|
|
|
|
|
|
self.command_line.push_error_once(&format!(
|
|
|
|
|
|
"Saved, but the refreshed drawing is locked by another editor: {error}"
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
Some(Err(crate::io::edit_lock::EditLeaseError::Unavailable(error))) => {
|
|
|
|
|
|
self.tabs[i].edit_lock_conflict = false;
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
"{error}. External-change checks remain active."
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
None => self.install_native_edit_guard(i, path, None),
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].disk_fingerprint =
|
|
|
|
|
|
crate::io::edit_lock::FileFingerprint::capture(path).ok();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub(in crate::app) fn save_tab_synchronously_protected(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
i: usize,
|
|
|
|
|
|
path: std::path::PathBuf,
|
|
|
|
|
|
set_current_path: bool,
|
|
|
|
|
|
) -> Result<(), crate::io::SaveFailure> {
|
|
|
|
|
|
let path_changed = self.tabs[i]
|
|
|
|
|
|
.current_path
|
|
|
|
|
|
.as_deref()
|
|
|
|
|
|
.is_none_or(|current| !native_paths_match(current, &path));
|
|
|
|
|
|
if !path_changed && self.tabs[i].edit_lock_conflict {
|
|
|
|
|
|
return Err(crate::io::SaveFailure::file_in_use(
|
|
|
|
|
|
"drawing edit lock is held by another editor",
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let destination_lease = if path_changed {
|
|
|
|
|
|
match crate::io::edit_lock::EditLease::acquire(&path) {
|
|
|
|
|
|
Ok(lease) => Some(lease),
|
|
|
|
|
|
Err(crate::io::edit_lock::EditLeaseError::Locked(error)) => {
|
|
|
|
|
|
return Err(crate::io::SaveFailure::file_in_use(error));
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(crate::io::edit_lock::EditLeaseError::Unavailable(error)) => {
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
"{error}. External-change checks remain active."
|
|
|
|
|
|
));
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let expected_fingerprint = if path_changed {
|
|
|
|
|
|
match crate::io::edit_lock::FileFingerprint::capture(&path) {
|
|
|
|
|
|
Ok(fingerprint) => Some(fingerprint),
|
|
|
|
|
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
|
|
|
|
|
|
Err(error) => {
|
|
|
|
|
|
return Err(crate::io::SaveFailure::other(format!(
|
|
|
|
|
|
"could not verify {} before saving: {error}",
|
|
|
|
|
|
path.display()
|
|
|
|
|
|
)));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.tabs[i].disk_fingerprint.clone().or_else(|| {
|
|
|
|
|
|
crate::io::edit_lock::FileFingerprint::capture(&path).ok()
|
|
|
|
|
|
})
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
self.prepare_native_save(i);
|
|
|
|
|
|
let version = self.tabs[i].scene.document.version;
|
|
|
|
|
|
let snapshot = self.tabs[i].scene.document.clone();
|
|
|
|
|
|
crate::io::save_owned_as_version_atomic(
|
|
|
|
|
|
snapshot,
|
|
|
|
|
|
&path,
|
|
|
|
|
|
version,
|
|
|
|
|
|
self.backup_on_save,
|
|
|
|
|
|
expected_fingerprint,
|
|
|
|
|
|
)?;
|
|
|
|
|
|
|
|
|
|
|
|
if set_current_path {
|
|
|
|
|
|
self.tabs[i].current_path = Some(path.clone());
|
|
|
|
|
|
}
|
|
|
|
|
|
self.refresh_native_edit_guard_after_save(
|
|
|
|
|
|
i,
|
|
|
|
|
|
&path,
|
|
|
|
|
|
path_changed,
|
|
|
|
|
|
destination_lease,
|
|
|
|
|
|
);
|
|
|
|
|
|
self.tabs[i].dirty = false;
|
|
|
|
|
|
let _ = std::fs::remove_file(path.with_extension("sv$"));
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
fn retry_native_edit_guard(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
i: usize,
|
|
|
|
|
|
path: &std::path::Path,
|
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
|
if let Some(lease) = self.tabs[i].edit_lease.as_mut() {
|
|
|
|
|
|
match lease.refresh_drawing_lock(path) {
|
|
|
|
|
|
Ok(()) => {
|
|
|
|
|
|
let warning = lease.platform_warning().map(str::to_owned);
|
|
|
|
|
|
self.tabs[i].edit_lock_conflict = false;
|
|
|
|
|
|
if let Some(warning) = warning {
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
"{warning}. External-change checks remain active."
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
return Ok(());
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(crate::io::edit_lock::EditLeaseError::Locked(error)) => {
|
|
|
|
|
|
return Err(error);
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(crate::io::edit_lock::EditLeaseError::Unavailable(error)) => {
|
|
|
|
|
|
self.tabs[i].edit_lock_conflict = false;
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
"{error}. External-change checks remain active."
|
|
|
|
|
|
));
|
|
|
|
|
|
return Ok(());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
match crate::io::edit_lock::EditLease::acquire(path) {
|
|
|
|
|
|
Ok(lease) => {
|
|
|
|
|
|
let warning = lease.platform_warning().map(str::to_owned);
|
|
|
|
|
|
self.tabs[i].edit_lease = Some(lease);
|
|
|
|
|
|
self.tabs[i].edit_lock_conflict = false;
|
|
|
|
|
|
if let Some(warning) = warning {
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
"{warning}. External-change checks remain active."
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(crate::io::edit_lock::EditLeaseError::Locked(error)) => Err(error),
|
|
|
|
|
|
Err(crate::io::edit_lock::EditLeaseError::Unavailable(error)) => {
|
|
|
|
|
|
self.tabs[i].edit_lock_conflict = false;
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
"{error}. External-change checks remain active."
|
|
|
|
|
|
));
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 18:49:18 +03:00
|
|
|
|
pub(super) fn on_file_opened(&mut self, name: String, path: std::path::PathBuf, doc: acadrust::CadDocument,
|
|
|
|
|
|
mut caches: crate::scene::DerivedCaches,
|
|
|
|
|
|
) -> Task<Message> {
|
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 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();
|
|
|
|
|
|
}
|
2026-07-25 18:49:18 +03:00
|
|
|
|
let open_started = self.opening.as_ref().map(|p| p.started);
|
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 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
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
2026-07-25 18:49:18 +03:00
|
|
|
|
if caches.xref_dropped > 0 {
|
|
|
|
|
|
self.command_line.push_error(&format!(
|
|
|
|
|
|
"Warning: {} corrupt xref entities dropped",
|
|
|
|
|
|
caches.xref_dropped
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
for info in &caches.xrefs {
|
|
|
|
|
|
match info.status {
|
|
|
|
|
|
crate::io::xref::XrefStatus::Loaded => {
|
|
|
|
|
|
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));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-21 10:06:55 +03:00
|
|
|
|
let thumbs_task = 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
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-27 13:13:58 +03:00
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
let opened_fingerprint = self
|
|
|
|
|
|
.opening
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.and_then(|opening| opening.fingerprint.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
|
|
|
|
self.tabs[i].current_path = Some(path.clone());
|
2026-07-27 13:13:58 +03:00
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
self.install_native_edit_guard(i, &path, opened_fingerprint);
|
2026-07-26 22:30:14 +03:00
|
|
|
|
self.tabs[i].scene.material_base_dir =
|
|
|
|
|
|
path.parent().map(std::path::Path::to_path_buf);
|
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.document = doc;
|
2026-07-21 23:00:36 +03:00
|
|
|
|
// A file saved without the built-in Standard styles (foreign
|
|
|
|
|
|
// or damaged) gets them re-seeded so nothing dangles (#366).
|
|
|
|
|
|
crate::app::style_ops::ensure_standard_styles(
|
|
|
|
|
|
&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
|
|
|
|
// Follow the file's saved current UCS from the moment it opens.
|
|
|
|
|
|
self.tabs[i].adopt_active_ucs_from_header();
|
feat(sysvars): persist Ortho, running OSNAP and Isolate in the drawing
Ortho and the running object-snap set were app-global (settings.txt only) and
Isolate/Hide was a session-only view state, so none survived save/reopen and
none were per-drawing. Bind all three to the file like AutoCAD:
- Ortho -> header.ortho_mode ($ORTHOMODE); running OSNAP <-> header
.object_snap_mode ($OSMODE bitmask) via a new SnapType<->bit map in settings
(osmode_from_snaps / snaps_from_osmode, round-trip tested). The header is the
per-drawing store, live state mirrors the active drawing: adopt header->live
on open and tab-switch, stamp live->header on save and when leaving a tab, a
fresh drawing inherits the current values, and SETVAR ORTHOMODE/OSMODE now
also update the live state. settings.txt stays as the boot/new-drawing
default; the per-file header overrides it on open.
- Isolate/Hide now set each entity's persisted invisible flag (DXF code 60,
which acadrust round-trips in DXF and DWG) in addition to the session set;
End Isolation clears it. The set is reseeded from invisible entities on load
and after undo/redo, the commands are undoable and mark the tab dirty. The
renderer already skipped invisible entities, so no render change was needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 15:45:08 +03:00
|
|
|
|
// Adopt the drawing's own Ortho ($ORTHOMODE) and running OSNAP
|
|
|
|
|
|
// ($OSMODE) so each file keeps its state, like AutoCAD.
|
|
|
|
|
|
self.adopt_header_sysvars(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
|
|
|
|
// 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
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 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",
|
2026-07-25 18:49:18 +03:00
|
|
|
|
timings.parse_ms, timings.purge_ms, timings.caches_ms, timings.xref_ms, total_ms
|
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
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
|
|
// 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;
|
2026-07-26 23:38:38 +03:00
|
|
|
|
self.tabs[i].scene.object_data_cache = caches.object_data;
|
2026-07-25 18:49:18 +03:00
|
|
|
|
let prepared_geometry = caches.prepared_geometry.take();
|
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
|
|
|
|
// Invalidate the wire cache so the new document is tessellated.
|
|
|
|
|
|
self.tabs[i].scene.bump_geometry();
|
2026-07-25 18:49:18 +03:00
|
|
|
|
if let Some(prepared) = prepared_geometry {
|
|
|
|
|
|
self.tabs[i].scene.install_prepared_open_geometry(prepared);
|
2026-07-02 10:20:24 +03:00
|
|
|
|
}
|
2026-07-28 17:35:21 +03:00
|
|
|
|
self.tabs[i]
|
|
|
|
|
|
.scene
|
|
|
|
|
|
.replace_selection(rustc_hash::FxHashSet::default());
|
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.preview_wires = vec![];
|
2026-07-12 01:01:40 +03:00
|
|
|
|
// Reopen in whichever space the file was saved in — the CTAB
|
|
|
|
|
|
// tab name when recorded, else the $TILEMODE model/paper flag —
|
|
|
|
|
|
// instead of always landing in Model.
|
|
|
|
|
|
{
|
|
|
|
|
|
let names = self.tabs[i].scene.layout_names();
|
|
|
|
|
|
let saved = crate::io::saved_active_layout(&self.tabs[i].scene.document)
|
|
|
|
|
|
.and_then(|n| {
|
|
|
|
|
|
names.iter().find(|x| x.eq_ignore_ascii_case(&n)).cloned()
|
|
|
|
|
|
});
|
|
|
|
|
|
self.tabs[i].scene.current_layout = match saved {
|
|
|
|
|
|
Some(n) => n,
|
|
|
|
|
|
None if self.tabs[i].scene.document.header.show_model_space => {
|
|
|
|
|
|
"Model".to_string()
|
|
|
|
|
|
}
|
|
|
|
|
|
// Paper was active but no CTAB — fall back to the first
|
|
|
|
|
|
// paper layout (names[0] is always "Model").
|
|
|
|
|
|
None => names
|
|
|
|
|
|
.into_iter()
|
|
|
|
|
|
.nth(1)
|
|
|
|
|
|
.unwrap_or_else(|| "Model".to_string()),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-07-27 23:36:02 +03:00
|
|
|
|
// Object isolation is session-only. A newly opened drawing must
|
|
|
|
|
|
// not inherit the previous tab's filter, and persisted entity
|
|
|
|
|
|
// visibility remains independent (not an isolation session).
|
|
|
|
|
|
self.tabs[i].scene.reset_transient_visibility();
|
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();
|
2026-07-27 17:37:17 +03:00
|
|
|
|
self.refresh_properties();
|
2026-07-25 10:09:23 +03:00
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
let interaction_task = {
|
|
|
|
|
|
let wires = self.tabs[i].scene.hit_test_wires();
|
|
|
|
|
|
let screen_height = self.tabs[i].scene.selection.borrow().vp_size.1;
|
|
|
|
|
|
self.prepare_interaction_index_task(i, wires, screen_height)
|
|
|
|
|
|
.unwrap_or_else(Task::none)
|
|
|
|
|
|
};
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
let interaction_task = Task::none();
|
2026-07-25 18:49:18 +03:00
|
|
|
|
if let Some(opening) = &self.opening {
|
|
|
|
|
|
opening
|
|
|
|
|
|
.state
|
|
|
|
|
|
.set(crate::app::OPEN_PHASE_FINALIZING, 10000, 1, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
self.opening.take();
|
2026-07-25 10:09:23 +03:00
|
|
|
|
let pending_open_task = self.drain_pending_open();
|
|
|
|
|
|
Task::batch([thumbs_task, pending_open_task, interaction_task])
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 18:49:18 +03:00
|
|
|
|
pub(super) fn on_wblock_save_result_some(&mut self, block_name: String, path: std::path::PathBuf,
|
|
|
|
|
|
) -> Task<Message> {
|
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;
|
2026-07-25 18:49:18 +03:00
|
|
|
|
let document = self.tabs[i].scene.document.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 handles: Vec<_> = self.tabs[i].scene.selected.iter().copied().collect();
|
2026-07-25 18:49:18 +03:00
|
|
|
|
let worker_name = block_name.clone();
|
|
|
|
|
|
let worker_path = path.clone();
|
|
|
|
|
|
background_task(
|
|
|
|
|
|
move || {
|
|
|
|
|
|
let document = if worker_name == "*" {
|
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
|
|
|
|
crate::modules::insert::wblock::extract_entities_to_doc(
|
2026-07-25 18:49:18 +03:00
|
|
|
|
&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
|
|
|
|
&handles,
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
crate::modules::insert::wblock::extract_block_to_doc(
|
2026-07-25 18:49:18 +03:00
|
|
|
|
&document,
|
|
|
|
|
|
&worker_name)
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
2026-07-25 18:49:18 +03:00
|
|
|
|
.map_err(|e| e.to_string())?; crate::io::save(&document, &worker_path).map_err(|e| e.to_string())
|
|
|
|
|
|
},
|
|
|
|
|
|
move |result| Message::WblockWriteFinished(block_name, path, result),)
|
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_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();
|
2026-07-25 18:49:18 +03:00
|
|
|
|
let worker_path = path.clone();
|
|
|
|
|
|
background_task(
|
|
|
|
|
|
move || {
|
|
|
|
|
|
let mesh_refs: Vec<_> = meshes.iter().collect();
|
|
|
|
|
|
let bytes = crate::io::stl::build_stl(&mesh_refs)
|
|
|
|
|
|
.ok_or_else(|| "no mesh data to export".to_string())?;
|
|
|
|
|
|
std::fs::write(&worker_path, bytes).map_err(|e| e.to_string())
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
},
|
2026-07-25 18:49:18 +03:00
|
|
|
|
move |result| Message::StlExportFinished(path, result),)
|
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_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();
|
2026-07-25 18:49:18 +03:00
|
|
|
|
let worker_path = path.clone();
|
|
|
|
|
|
background_task(
|
|
|
|
|
|
move || {
|
|
|
|
|
|
let mesh_refs: Vec<_> = meshes.iter().collect();
|
|
|
|
|
|
let text = crate::io::step::build_step(&mesh_refs)
|
|
|
|
|
|
.ok_or_else(|| "no mesh data to export".to_string())?;
|
|
|
|
|
|
std::fs::write(&worker_path, text.as_bytes()).map_err(|e| e.to_string())
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
},
|
2026-07-25 18:49:18 +03:00
|
|
|
|
move |result| Message::StepExportFinished(path, result),)
|
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_obj_import_path_some(&mut self, path: std::path::PathBuf) -> Task<Message> {
|
2026-07-25 18:49:18 +03:00
|
|
|
|
let tab_id = self.tabs[self.active_tab].id;
|
|
|
|
|
|
let worker_path = path.clone();
|
|
|
|
|
|
background_task(
|
|
|
|
|
|
move || {
|
|
|
|
|
|
let src = std::fs::read_to_string(&worker_path).map_err(|e| e.to_string())?; crate::io::obj::parse_obj(&src, [0.7, 0.7, 0.85, 1.0])
|
|
|
|
|
|
.ok_or_else(|| "no usable geometry in file".to_string())
|
|
|
|
|
|
},
|
|
|
|
|
|
move |result| Message::ObjImportFinished(tab_id, path, result),)
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 02:58:46 +03:00
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub(in crate::app) fn prepare_native_save(&mut self, i: usize) {
|
|
|
|
|
|
self.sync_vport_display(i);
|
|
|
|
|
|
sync_annotation_scale_header(&mut self.tabs[i].scene);
|
|
|
|
|
|
self.stamp_header_sysvars(i);
|
|
|
|
|
|
self.tabs[i].scene.document.header.user_real1 =
|
|
|
|
|
|
self.tabs[i].scene.annotation_scale as f64;
|
|
|
|
|
|
self.sync_truck_solids_to_acis(i);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 18:49:18 +03:00
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
fn stamp_thumbnail(&mut self, i: usize, version: acadrust::DxfVersion) {
|
|
|
|
|
|
let scene = &self.tabs[i].scene;
|
|
|
|
|
|
let preview = crate::io::thumbnail::from_snapshot(
|
|
|
|
|
|
&scene.entity_wires(),
|
|
|
|
|
|
&scene.camera.borrow(),
|
|
|
|
|
|
scene.bg_color,
|
|
|
|
|
|
version >= acadrust::DxfVersion::AC1027,
|
|
|
|
|
|
self.vp_size,
|
|
|
|
|
|
);
|
|
|
|
|
|
self.tabs[i].scene.document.preview = preview;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-25 02:58:46 +03:00
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub(in crate::app) fn queue_native_save(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
i: usize,
|
|
|
|
|
|
path: std::path::PathBuf,
|
|
|
|
|
|
version: acadrust::DxfVersion,
|
|
|
|
|
|
purpose: crate::app::SavePurpose,
|
|
|
|
|
|
continuation: crate::app::SaveContinuation,
|
|
|
|
|
|
set_current_path: bool,
|
2026-07-27 13:13:58 +03:00
|
|
|
|
check_external_change: bool,
|
2026-07-25 02:58:46 +03:00
|
|
|
|
) -> Task<Message> {
|
|
|
|
|
|
let tab_id = self.tabs[i].id;
|
|
|
|
|
|
if self.active_save_jobs.contains_key(&tab_id) {
|
|
|
|
|
|
if purpose != crate::app::SavePurpose::Autosave {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_info("Save already running for this drawing.");
|
|
|
|
|
|
}
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
2026-07-27 13:13:58 +03:00
|
|
|
|
if purpose != crate::app::SavePurpose::Autosave
|
|
|
|
|
|
&& !set_current_path
|
|
|
|
|
|
&& self.tabs[i].edit_lock_conflict
|
|
|
|
|
|
{
|
|
|
|
|
|
let error = "Drawing edit lock is held by another editor.".to_string();
|
|
|
|
|
|
self.command_line.push_error_once(&format!(
|
|
|
|
|
|
"Unable to save \"{}\": {error}",
|
|
|
|
|
|
path.file_name()
|
|
|
|
|
|
.map(|name| name.to_string_lossy().into_owned())
|
|
|
|
|
|
.unwrap_or_else(|| path.display().to_string())
|
|
|
|
|
|
));
|
|
|
|
|
|
self.pending_save_failure = Some(crate::app::PendingSaveFailure {
|
|
|
|
|
|
tab_id,
|
|
|
|
|
|
path,
|
|
|
|
|
|
version,
|
|
|
|
|
|
purpose,
|
|
|
|
|
|
continuation,
|
|
|
|
|
|
set_current_path,
|
|
|
|
|
|
error,
|
|
|
|
|
|
});
|
|
|
|
|
|
self.restore_failed_save_continuation(continuation, i);
|
|
|
|
|
|
self.active_modal = Some(crate::app::ModalKind::FileInUse);
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let destination_is_current = self.tabs[i]
|
|
|
|
|
|
.current_path
|
|
|
|
|
|
.as_deref()
|
|
|
|
|
|
.is_some_and(|current| native_paths_match(current, &path));
|
|
|
|
|
|
if set_current_path && !destination_is_current {
|
|
|
|
|
|
match crate::io::edit_lock::EditLease::acquire(&path) {
|
|
|
|
|
|
Ok(lease) => {
|
|
|
|
|
|
self.pending_save_leases.insert(tab_id, lease);
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(crate::io::edit_lock::EditLeaseError::Locked(error)) => {
|
|
|
|
|
|
self.command_line.push_error_once(&format!(
|
|
|
|
|
|
"Unable to save \"{}\": {error}",
|
|
|
|
|
|
path.file_name()
|
|
|
|
|
|
.map(|name| name.to_string_lossy().into_owned())
|
|
|
|
|
|
.unwrap_or_else(|| path.display().to_string())
|
|
|
|
|
|
));
|
|
|
|
|
|
self.pending_save_failure = Some(crate::app::PendingSaveFailure {
|
|
|
|
|
|
tab_id,
|
|
|
|
|
|
path,
|
|
|
|
|
|
version,
|
|
|
|
|
|
purpose,
|
|
|
|
|
|
continuation,
|
|
|
|
|
|
set_current_path,
|
|
|
|
|
|
error,
|
|
|
|
|
|
});
|
|
|
|
|
|
self.restore_failed_save_continuation(continuation, i);
|
|
|
|
|
|
self.active_modal = Some(crate::app::ModalKind::FileInUse);
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
Err(crate::io::edit_lock::EditLeaseError::Unavailable(error)) => {
|
|
|
|
|
|
self.command_line.push_info(&format!(
|
|
|
|
|
|
"{error}. External-change checks remain active."
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-25 02:58:46 +03:00
|
|
|
|
|
|
|
|
|
|
let epoch = self.tabs[i].scene.geometry_epoch;
|
|
|
|
|
|
let revision = self.tabs[i].edit_revision;
|
|
|
|
|
|
let camera_generation = self.tabs[i].scene.camera_generation;
|
2026-07-25 09:05:52 +03:00
|
|
|
|
let thumbnail_key = (purpose != crate::app::SavePurpose::Autosave).then(|| {
|
|
|
|
|
|
let scene = &self.tabs[i].scene;
|
|
|
|
|
|
crate::app::ThumbnailCacheKey {
|
|
|
|
|
|
epoch,
|
|
|
|
|
|
camera_generation,
|
|
|
|
|
|
bg_color: scene.bg_color.map(f32::to_bits),
|
|
|
|
|
|
png: version >= acadrust::DxfVersion::AC1027,
|
|
|
|
|
|
viewport: [self.vp_size.0.to_bits(), self.vp_size.1.to_bits()],
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
let thumbnail = if thumbnail_key == self.tabs[i].thumbnail_cache_key {
|
2026-07-25 02:58:46 +03:00
|
|
|
|
None
|
|
|
|
|
|
} else {
|
2026-07-25 09:05:52 +03:00
|
|
|
|
thumbnail_key.map(|key| {
|
|
|
|
|
|
let scene = &self.tabs[i].scene;
|
|
|
|
|
|
(
|
|
|
|
|
|
scene.entity_wires(),
|
|
|
|
|
|
scene.camera.borrow().clone(),
|
|
|
|
|
|
scene.bg_color,
|
|
|
|
|
|
key.png,
|
|
|
|
|
|
self.vp_size,
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
2026-07-25 02:58:46 +03:00
|
|
|
|
};
|
2026-07-25 21:34:29 +03:00
|
|
|
|
let clone_started = iced::time::Instant::now();
|
2026-07-25 02:58:46 +03:00
|
|
|
|
let mut snapshot = self.tabs[i].scene.document.clone();
|
|
|
|
|
|
let clone_ms = clone_started.elapsed().as_secs_f64() * 1000.0;
|
2026-07-25 13:34:48 +03:00
|
|
|
|
if crate::perf::enabled() {
|
|
|
|
|
|
crate::perf_record!(
|
2026-07-25 02:58:46 +03:00
|
|
|
|
"[perf] save-snapshot {:.1}ms entities={} objects={} purpose={purpose:?}",
|
|
|
|
|
|
clone_ms,
|
|
|
|
|
|
snapshot.entities().count(),
|
|
|
|
|
|
snapshot.objects.len(),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.save_job_serial = self.save_job_serial.wrapping_add(1);
|
|
|
|
|
|
let job_id = self.save_job_serial;
|
|
|
|
|
|
self.active_save_jobs.insert(tab_id, job_id);
|
|
|
|
|
|
let previous_autosave = set_current_path.then(|| self.autosave_target(i));
|
|
|
|
|
|
let backup = purpose != crate::app::SavePurpose::Autosave && self.backup_on_save;
|
2026-07-27 13:13:58 +03:00
|
|
|
|
let expected_fingerprint =
|
|
|
|
|
|
if check_external_change && purpose != crate::app::SavePurpose::Autosave {
|
|
|
|
|
|
if set_current_path {
|
|
|
|
|
|
crate::io::edit_lock::FileFingerprint::capture(&path).ok()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.tabs[i].disk_fingerprint.clone()
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
2026-07-25 02:58:46 +03:00
|
|
|
|
let worker_path = path.clone();
|
|
|
|
|
|
|
|
|
|
|
|
Task::perform(
|
|
|
|
|
|
async move {
|
2026-07-25 09:05:52 +03:00
|
|
|
|
let (result, refreshed_preview) = std::thread::spawn(move || {
|
|
|
|
|
|
let mut refreshed_preview = None;
|
2026-07-25 02:58:46 +03:00
|
|
|
|
if let Some((wires, camera, bg_color, png, viewport)) = thumbnail {
|
2026-07-25 21:34:29 +03:00
|
|
|
|
let started = iced::time::Instant::now();
|
2026-07-25 02:58:46 +03:00
|
|
|
|
snapshot.preview = crate::io::thumbnail::from_snapshot(
|
|
|
|
|
|
&wires,
|
|
|
|
|
|
&camera,
|
|
|
|
|
|
bg_color,
|
|
|
|
|
|
png,
|
|
|
|
|
|
viewport,
|
|
|
|
|
|
);
|
2026-07-25 13:34:48 +03:00
|
|
|
|
if crate::perf::enabled() {
|
|
|
|
|
|
crate::perf_record!(
|
2026-07-25 02:58:46 +03:00
|
|
|
|
"[perf] save-thumbnail {:.1}ms wires={}",
|
|
|
|
|
|
started.elapsed().as_secs_f64() * 1000.0,
|
|
|
|
|
|
wires.len(),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-07-25 09:05:52 +03:00
|
|
|
|
refreshed_preview = Some(snapshot.preview.clone());
|
2026-07-25 02:58:46 +03:00
|
|
|
|
}
|
2026-07-25 09:05:52 +03:00
|
|
|
|
let result = crate::io::save_owned_as_version_atomic(
|
2026-07-25 02:58:46 +03:00
|
|
|
|
snapshot,
|
|
|
|
|
|
&worker_path,
|
|
|
|
|
|
version,
|
|
|
|
|
|
backup,
|
2026-07-27 13:13:58 +03:00
|
|
|
|
expected_fingerprint,
|
2026-07-25 09:05:52 +03:00
|
|
|
|
);
|
|
|
|
|
|
(result, refreshed_preview)
|
2026-07-25 02:58:46 +03:00
|
|
|
|
})
|
|
|
|
|
|
.join()
|
2026-07-27 13:13:58 +03:00
|
|
|
|
.unwrap_or_else(|_| {
|
|
|
|
|
|
(
|
|
|
|
|
|
Err(crate::io::SaveFailure::other("save worker panicked")),
|
|
|
|
|
|
None,
|
|
|
|
|
|
)
|
|
|
|
|
|
});
|
2026-07-25 02:58:46 +03:00
|
|
|
|
crate::app::SaveOutcome {
|
|
|
|
|
|
job_id,
|
|
|
|
|
|
tab_id,
|
|
|
|
|
|
epoch,
|
|
|
|
|
|
revision,
|
|
|
|
|
|
camera_generation,
|
|
|
|
|
|
path,
|
2026-07-27 13:13:58 +03:00
|
|
|
|
version,
|
2026-07-25 02:58:46 +03:00
|
|
|
|
previous_autosave,
|
|
|
|
|
|
set_current_path,
|
|
|
|
|
|
purpose,
|
|
|
|
|
|
continuation,
|
2026-07-25 09:05:52 +03:00
|
|
|
|
thumbnail_key,
|
|
|
|
|
|
refreshed_preview,
|
2026-07-25 02:58:46 +03:00
|
|
|
|
result,
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
Message::SaveFinished,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub(super) fn on_save_finished(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
outcome: crate::app::SaveOutcome,
|
|
|
|
|
|
) -> Task<Message> {
|
|
|
|
|
|
let latest = self.active_save_jobs.get(&outcome.tab_id).copied()
|
|
|
|
|
|
== Some(outcome.job_id);
|
|
|
|
|
|
if latest {
|
|
|
|
|
|
self.active_save_jobs.remove(&outcome.tab_id);
|
|
|
|
|
|
}
|
2026-07-27 13:13:58 +03:00
|
|
|
|
let destination_lease = self.pending_save_leases.remove(&outcome.tab_id);
|
2026-07-25 02:58:46 +03:00
|
|
|
|
|
|
|
|
|
|
let Some(i) = self.tabs.iter().position(|tab| tab.id == outcome.tab_id) else {
|
|
|
|
|
|
if outcome.purpose == crate::app::SavePurpose::Autosave {
|
|
|
|
|
|
let _ = std::fs::remove_file(&outcome.path);
|
|
|
|
|
|
}
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
if !latest {
|
|
|
|
|
|
if outcome.purpose == crate::app::SavePurpose::Autosave {
|
|
|
|
|
|
let _ = std::fs::remove_file(&outcome.path);
|
|
|
|
|
|
}
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-27 13:13:58 +03:00
|
|
|
|
if let Err(error) = &outcome.result {
|
|
|
|
|
|
if error.externally_modified
|
|
|
|
|
|
&& outcome.purpose != crate::app::SavePurpose::Autosave
|
|
|
|
|
|
{
|
|
|
|
|
|
let file_name = outcome
|
|
|
|
|
|
.path
|
|
|
|
|
|
.file_name()
|
|
|
|
|
|
.map(|name| name.to_string_lossy().into_owned())
|
|
|
|
|
|
.unwrap_or_else(|| outcome.path.display().to_string());
|
|
|
|
|
|
self.command_line.push_error_once(&format!(
|
|
|
|
|
|
"Save stopped: \"{file_name}\" changed outside Open CAD Studio."
|
|
|
|
|
|
));
|
|
|
|
|
|
self.pending_external_change = Some(crate::app::PendingExternalChange {
|
|
|
|
|
|
tab_id: outcome.tab_id,
|
|
|
|
|
|
path: outcome.path.clone(),
|
|
|
|
|
|
version: outcome.version,
|
|
|
|
|
|
purpose: outcome.purpose,
|
|
|
|
|
|
continuation: outcome.continuation,
|
|
|
|
|
|
set_current_path: outcome.set_current_path,
|
|
|
|
|
|
});
|
|
|
|
|
|
self.restore_failed_save_continuation(outcome.continuation, i);
|
|
|
|
|
|
self.active_modal = Some(crate::app::ModalKind::ExternalChange);
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
if error.file_in_use && outcome.purpose != crate::app::SavePurpose::Autosave {
|
|
|
|
|
|
let file_name = outcome
|
|
|
|
|
|
.path
|
|
|
|
|
|
.file_name()
|
|
|
|
|
|
.map(|name| name.to_string_lossy().into_owned())
|
|
|
|
|
|
.unwrap_or_else(|| outcome.path.display().to_string());
|
|
|
|
|
|
self.command_line.push_error_once(&format!(
|
|
|
|
|
|
"Unable to save \"{file_name}\": file is in use by another application."
|
|
|
|
|
|
));
|
|
|
|
|
|
self.pending_save_failure = Some(crate::app::PendingSaveFailure {
|
|
|
|
|
|
tab_id: outcome.tab_id,
|
|
|
|
|
|
path: outcome.path.clone(),
|
|
|
|
|
|
version: outcome.version,
|
|
|
|
|
|
purpose: outcome.purpose,
|
|
|
|
|
|
continuation: outcome.continuation,
|
|
|
|
|
|
set_current_path: outcome.set_current_path,
|
|
|
|
|
|
error: error.to_string(),
|
|
|
|
|
|
});
|
|
|
|
|
|
match outcome.continuation {
|
|
|
|
|
|
crate::app::SaveContinuation::CloseTab => {
|
|
|
|
|
|
self.pending_close = Some(crate::app::PendingClose::Tab(i));
|
|
|
|
|
|
}
|
|
|
|
|
|
crate::app::SaveContinuation::Quit => {
|
|
|
|
|
|
self.pending_close = Some(crate::app::PendingClose::Quit);
|
|
|
|
|
|
}
|
|
|
|
|
|
crate::app::SaveContinuation::None => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.active_modal = Some(crate::app::ModalKind::FileInUse);
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
2026-07-25 02:58:46 +03:00
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error(&format!("Save failed: {error}"));
|
|
|
|
|
|
return match outcome.continuation {
|
|
|
|
|
|
crate::app::SaveContinuation::CloseTab => {
|
|
|
|
|
|
self.pending_close = Some(crate::app::PendingClose::Tab(i));
|
|
|
|
|
|
self.open_unsaved_dialog_window()
|
|
|
|
|
|
}
|
|
|
|
|
|
crate::app::SaveContinuation::Quit => {
|
|
|
|
|
|
self.pending_close = Some(crate::app::PendingClose::Quit);
|
|
|
|
|
|
self.open_unsaved_dialog_window()
|
|
|
|
|
|
}
|
|
|
|
|
|
crate::app::SaveContinuation::None => Task::none(),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let snapshot_is_current = self.tabs[i].scene.geometry_epoch == outcome.epoch
|
|
|
|
|
|
&& self.tabs[i].edit_revision == outcome.revision
|
|
|
|
|
|
&& self.tabs[i].scene.camera_generation == outcome.camera_generation;
|
2026-07-25 09:05:52 +03:00
|
|
|
|
if snapshot_is_current && outcome.purpose != crate::app::SavePurpose::Autosave {
|
|
|
|
|
|
if let Some(preview) = outcome.refreshed_preview {
|
|
|
|
|
|
self.tabs[i].scene.document.preview = preview;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.tabs[i].thumbnail_cache_key = outcome.thumbnail_key;
|
|
|
|
|
|
}
|
2026-07-25 02:58:46 +03:00
|
|
|
|
let mut tasks = Vec::new();
|
|
|
|
|
|
match outcome.purpose {
|
|
|
|
|
|
crate::app::SavePurpose::Autosave => {
|
|
|
|
|
|
self.command_line.push_output("Autosaved 1 drawing");
|
|
|
|
|
|
}
|
|
|
|
|
|
crate::app::SavePurpose::Manual | crate::app::SavePurpose::SaveAs => {
|
2026-07-27 13:13:58 +03:00
|
|
|
|
let path_changed = outcome.set_current_path
|
|
|
|
|
|
&& self.tabs[i]
|
|
|
|
|
|
.current_path
|
|
|
|
|
|
.as_deref()
|
|
|
|
|
|
.is_none_or(|current| {
|
|
|
|
|
|
!native_paths_match(current, &outcome.path)
|
|
|
|
|
|
});
|
2026-07-25 02:58:46 +03:00
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_output(&format!("Saved: {}", outcome.path.display()));
|
|
|
|
|
|
self.recent_thumbs.remove(&outcome.path);
|
|
|
|
|
|
if let Some(previous) = outcome.previous_autosave {
|
|
|
|
|
|
if previous != outcome.path {
|
|
|
|
|
|
let _ = std::fs::remove_file(previous);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if outcome.set_current_path {
|
|
|
|
|
|
self.tabs[i].current_path = Some(outcome.path.clone());
|
2026-07-27 22:37:47 +03:00
|
|
|
|
self.tabs[i].scene.document.version = outcome.version;
|
2026-07-25 02:58:46 +03:00
|
|
|
|
tasks.push(self.push_recent(outcome.path.clone()));
|
|
|
|
|
|
}
|
2026-07-27 13:13:58 +03:00
|
|
|
|
self.refresh_native_edit_guard_after_save(
|
|
|
|
|
|
i,
|
|
|
|
|
|
&outcome.path,
|
|
|
|
|
|
path_changed,
|
|
|
|
|
|
destination_lease,
|
|
|
|
|
|
);
|
2026-07-25 02:58:46 +03:00
|
|
|
|
if snapshot_is_current {
|
|
|
|
|
|
self.tabs[i].dirty = false;
|
|
|
|
|
|
let _ = std::fs::remove_file(outcome.path.with_extension("sv$"));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
match outcome.continuation {
|
|
|
|
|
|
crate::app::SaveContinuation::None => {}
|
|
|
|
|
|
crate::app::SaveContinuation::CloseTab if snapshot_is_current => {
|
|
|
|
|
|
self.pending_close = None;
|
|
|
|
|
|
tasks.push(self.close_unsaved_dialog_window());
|
|
|
|
|
|
tasks.push(self.update(Message::TabClose(i)));
|
2026-07-27 12:02:50 +03:00
|
|
|
|
tasks.push(self.continue_tab_close_queue());
|
2026-07-25 02:58:46 +03:00
|
|
|
|
}
|
|
|
|
|
|
crate::app::SaveContinuation::Quit if snapshot_is_current => {
|
|
|
|
|
|
self.pending_close = None;
|
|
|
|
|
|
if self.tabs.iter().any(|tab| tab.dirty) {
|
|
|
|
|
|
self.pending_close = Some(crate::app::PendingClose::Quit);
|
|
|
|
|
|
tasks.push(self.open_unsaved_dialog_window());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
tasks.push(self.close_unsaved_dialog_window());
|
|
|
|
|
|
tasks.push(self.exit_app());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
crate::app::SaveContinuation::CloseTab => {
|
|
|
|
|
|
self.pending_close = Some(crate::app::PendingClose::Tab(i));
|
|
|
|
|
|
tasks.push(self.open_unsaved_dialog_window());
|
|
|
|
|
|
}
|
|
|
|
|
|
crate::app::SaveContinuation::Quit => {
|
|
|
|
|
|
self.pending_close = Some(crate::app::PendingClose::Quit);
|
|
|
|
|
|
tasks.push(self.open_unsaved_dialog_window());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Task::batch(tasks)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-27 13:13:58 +03:00
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
fn restore_failed_save_continuation(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
continuation: crate::app::SaveContinuation,
|
|
|
|
|
|
tab_idx: usize,
|
|
|
|
|
|
) {
|
|
|
|
|
|
self.pending_close = match continuation {
|
|
|
|
|
|
crate::app::SaveContinuation::None => None,
|
|
|
|
|
|
crate::app::SaveContinuation::CloseTab => {
|
|
|
|
|
|
Some(crate::app::PendingClose::Tab(tab_idx))
|
|
|
|
|
|
}
|
|
|
|
|
|
crate::app::SaveContinuation::Quit => Some(crate::app::PendingClose::Quit),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub(super) fn on_save_file_in_use_retry(&mut self) -> Task<Message> {
|
|
|
|
|
|
let Some(mut failure) = self.pending_save_failure.take() else {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
let Some(i) = self.tabs.iter().position(|tab| tab.id == failure.tab_id) else {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
if self.tabs[i].edit_lock_conflict {
|
|
|
|
|
|
if let Err(error) = self.retry_native_edit_guard(i, &failure.path) {
|
|
|
|
|
|
let continuation = failure.continuation;
|
|
|
|
|
|
failure.error = error.clone();
|
|
|
|
|
|
self.pending_save_failure = Some(failure);
|
|
|
|
|
|
self.restore_failed_save_continuation(continuation, i);
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error_once(&format!("Unable to acquire edit lock: {error}"));
|
|
|
|
|
|
self.active_modal = Some(crate::app::ModalKind::FileInUse);
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
self.restore_failed_save_continuation(failure.continuation, i);
|
|
|
|
|
|
self.active_tab = i;
|
|
|
|
|
|
self.prepare_native_save(i);
|
|
|
|
|
|
self.queue_native_save(
|
|
|
|
|
|
i,
|
|
|
|
|
|
failure.path,
|
|
|
|
|
|
failure.version,
|
|
|
|
|
|
failure.purpose,
|
|
|
|
|
|
failure.continuation,
|
|
|
|
|
|
failure.set_current_path,
|
|
|
|
|
|
true,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub(super) fn on_save_file_in_use_save_as(&mut self) -> Task<Message> {
|
|
|
|
|
|
let Some(failure) = self.pending_save_failure.take() else {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
let Some(i) = self.tabs.iter().position(|tab| tab.id == failure.tab_id) else {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
self.restore_failed_save_continuation(failure.continuation, i);
|
|
|
|
|
|
self.active_tab = i;
|
|
|
|
|
|
self.save_dialog_for_unsaved =
|
|
|
|
|
|
failure.continuation != crate::app::SaveContinuation::None;
|
|
|
|
|
|
self.open_save_dialog_window(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub(super) fn on_external_change_overwrite(&mut self) -> Task<Message> {
|
|
|
|
|
|
let Some(conflict) = self.pending_external_change.take() else {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
let Some(i) = self.tabs.iter().position(|tab| tab.id == conflict.tab_id) else {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
self.restore_failed_save_continuation(conflict.continuation, i);
|
|
|
|
|
|
self.active_tab = i;
|
|
|
|
|
|
self.prepare_native_save(i);
|
|
|
|
|
|
self.queue_native_save(
|
|
|
|
|
|
i,
|
|
|
|
|
|
conflict.path,
|
|
|
|
|
|
conflict.version,
|
|
|
|
|
|
conflict.purpose,
|
|
|
|
|
|
conflict.continuation,
|
|
|
|
|
|
conflict.set_current_path,
|
|
|
|
|
|
false,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub(super) fn on_external_change_save_as(&mut self) -> Task<Message> {
|
|
|
|
|
|
let Some(conflict) = self.pending_external_change.take() else {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
let Some(i) = self.tabs.iter().position(|tab| tab.id == conflict.tab_id) else {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
self.restore_failed_save_continuation(conflict.continuation, i);
|
|
|
|
|
|
self.active_tab = i;
|
|
|
|
|
|
self.save_dialog_for_unsaved =
|
|
|
|
|
|
conflict.continuation != crate::app::SaveContinuation::None;
|
|
|
|
|
|
self.open_save_dialog_window(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub(super) fn on_external_change_reload(&mut self) -> Task<Message> {
|
|
|
|
|
|
let Some(conflict) = self.pending_external_change.take() else {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
let Some(i) = self.tabs.iter().position(|tab| tab.id == conflict.tab_id) else {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
let metadata = match std::fs::metadata(&conflict.path) {
|
|
|
|
|
|
Ok(metadata) => metadata,
|
|
|
|
|
|
Err(error) => {
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error(&format!("Reload failed: {error}"));
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
self.pending_close = None;
|
|
|
|
|
|
self.tab_counter += 1;
|
|
|
|
|
|
self.tabs[i] = crate::app::document::DocumentTab::new_drawing(self.tab_counter);
|
|
|
|
|
|
self.active_tab = i;
|
|
|
|
|
|
self.apply_bg_default(i);
|
|
|
|
|
|
self.update(Message::OpenPathPicked(Some((
|
|
|
|
|
|
conflict.path,
|
|
|
|
|
|
metadata.len(),
|
|
|
|
|
|
))))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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_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;
|
2026-07-25 02:58:46 +03:00
|
|
|
|
// Web serializes immediately below. Native preparation happens
|
|
|
|
|
|
// once after the destination/version is known.
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
{
|
|
|
|
|
|
self.sync_vport_display(i);
|
|
|
|
|
|
self.stamp_header_sysvars(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: 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() {
|
2026-06-28 20:31:30 +03:00
|
|
|
|
// A direct Save preserves the document's current version.
|
2026-07-23 14:09:38 +03:00
|
|
|
|
let ver = self.tabs[i].scene.document.version;
|
2026-07-25 02:58:46 +03:00
|
|
|
|
self.prepare_native_save(i);
|
|
|
|
|
|
return self.queue_native_save(
|
|
|
|
|
|
i,
|
|
|
|
|
|
path,
|
|
|
|
|
|
ver,
|
|
|
|
|
|
crate::app::SavePurpose::Manual,
|
|
|
|
|
|
crate::app::SaveContinuation::None,
|
|
|
|
|
|
false,
|
2026-07-27 13:13:58 +03:00
|
|
|
|
true,
|
2026-07-25 02:58:46 +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.save_dialog_for_unsaved = false;
|
2026-07-27 22:37:47 +03:00
|
|
|
|
self.save_with_default_format(i)
|
2026-07-11 09:12:36 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-27 22:37:47 +03:00
|
|
|
|
/// Save without the version picker: use the configured default and go straight to
|
2026-07-11 09:12:36 +03:00
|
|
|
|
/// the native destination dialog (native) or the browser download (web).
|
|
|
|
|
|
/// Used by plain Save (QSAVE) on an as-yet-unsaved drawing and by the
|
|
|
|
|
|
/// save-before-close flow — the version picker is reserved for Save As.
|
2026-07-27 22:37:47 +03:00
|
|
|
|
pub(in crate::app) fn save_with_default_format(&mut self, tab_idx: usize) -> Task<Message> {
|
2026-07-11 09:12:36 +03:00
|
|
|
|
self.active_tab = tab_idx;
|
2026-07-27 22:37:47 +03:00
|
|
|
|
self.save_dialog_format = self.default_save_format.clone();
|
|
|
|
|
|
let (ext, _) = crate::io::parse_save_format(&self.save_dialog_format);
|
2026-07-11 09:12:36 +03:00
|
|
|
|
self.save_dialog_filename = self.tabs[tab_idx]
|
|
|
|
|
|
.current_path
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.and_then(|p| p.file_name())
|
|
|
|
|
|
.map(|n| n.to_string_lossy().into_owned())
|
2026-07-27 22:37:47 +03:00
|
|
|
|
.unwrap_or_else(|| format!("{}.{ext}", self.tabs[tab_idx].tab_display_name()));
|
2026-07-11 09:12:36 +03:00
|
|
|
|
self.aec_drop_acknowledged = false;
|
|
|
|
|
|
self.on_save_dialog_confirm()
|
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_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();
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
|
2026-07-11 09:12:36 +03:00
|
|
|
|
// Native: hand the destination choice to the OS save dialog —
|
|
|
|
|
|
// it provides folder browsing and overwrite confirmation. The
|
|
|
|
|
|
// chosen version rides in `save_dialog_format` and the write
|
|
|
|
|
|
// happens in `on_save_dialog_path_picked` once a path returns.
|
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
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
{
|
2026-07-11 09:12:36 +03:00
|
|
|
|
let seed_dir = self.tabs[i]
|
|
|
|
|
|
.current_path
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.and_then(|p| p.parent().map(|d| d.to_path_buf()));
|
|
|
|
|
|
let default_name = filename;
|
|
|
|
|
|
let (filter_label, filter_ext): (&str, &str) =
|
|
|
|
|
|
if ext.eq_ignore_ascii_case("dxf") {
|
|
|
|
|
|
("DXF Files", "dxf")
|
|
|
|
|
|
} else {
|
|
|
|
|
|
("DWG Files", "dwg")
|
|
|
|
|
|
};
|
|
|
|
|
|
let close = self.close_save_dialog_window();
|
|
|
|
|
|
let pick = Task::perform(
|
|
|
|
|
|
async move {
|
2026-07-21 09:27:44 +03:00
|
|
|
|
let mut dlg = crate::sys::file_dialog()
|
2026-07-11 09:12:36 +03:00
|
|
|
|
.set_title("Save Drawing As")
|
|
|
|
|
|
.set_file_name(default_name)
|
|
|
|
|
|
.add_filter(filter_label, &[filter_ext]);
|
|
|
|
|
|
if let Some(dir) = seed_dir {
|
|
|
|
|
|
dlg = dlg.set_directory(dir);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
2026-07-11 09:12:36 +03:00
|
|
|
|
dlg.save_file().await.map(|h| crate::sys::handle_path(&h))
|
|
|
|
|
|
},
|
|
|
|
|
|
Message::SaveDialogPathPicked,
|
|
|
|
|
|
);
|
|
|
|
|
|
Task::batch([close, pick])
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
2026-07-11 09:12:36 +03:00
|
|
|
|
// Web: no filesystem — serialize and hand the browser a download.
|
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
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
|
|
{
|
2026-07-11 09:12:36 +03:00
|
|
|
|
let close = self.close_save_dialog_window();
|
|
|
|
|
|
sync_annotation_scale_header(&mut self.tabs[i].scene);
|
|
|
|
|
|
self.stamp_header_sysvars(i);
|
|
|
|
|
|
self.sync_truck_solids_to_acis(i);
|
2026-07-23 14:09:38 +03:00
|
|
|
|
self.stamp_thumbnail(i, version);
|
2026-07-29 00:41:51 +03:00
|
|
|
|
let mut recent_task = Task::none();
|
|
|
|
|
|
let saved = match crate::io::save_to_bytes(
|
|
|
|
|
|
&self.tabs[i].scene.document,
|
|
|
|
|
|
ext,
|
|
|
|
|
|
version,
|
|
|
|
|
|
) {
|
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(bytes) => {
|
|
|
|
|
|
crate::sys::download_bytes(&filename, &bytes);
|
2026-07-29 00:41:51 +03:00
|
|
|
|
let cache_name = std::path::Path::new(&filename)
|
|
|
|
|
|
.file_name()
|
|
|
|
|
|
.map(|name| name.to_string_lossy().into_owned())
|
|
|
|
|
|
.unwrap_or_else(|| filename.clone());
|
|
|
|
|
|
let path = std::path::PathBuf::from(cache_name);
|
|
|
|
|
|
self.tabs[i].current_path = Some(path.clone());
|
2026-07-27 22:37:47 +03:00
|
|
|
|
self.tabs[i].scene.document.version = version;
|
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].dirty = false;
|
2026-07-29 00:41:51 +03:00
|
|
|
|
recent_task = Task::perform(
|
|
|
|
|
|
async move {
|
|
|
|
|
|
crate::io::web_recent::store(
|
|
|
|
|
|
&path.to_string_lossy(),
|
|
|
|
|
|
&bytes,
|
|
|
|
|
|
)
|
|
|
|
|
|
.await
|
|
|
|
|
|
.map(|_| path)
|
|
|
|
|
|
},
|
|
|
|
|
|
Message::WebRecentStored,
|
|
|
|
|
|
);
|
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!("Saved: {filename}"));
|
2026-07-27 12:02:50 +03:00
|
|
|
|
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
|
|
|
|
}
|
2026-07-27 12:02:50 +03:00
|
|
|
|
Err(e) => {
|
|
|
|
|
|
self.command_line.push_error(&format!("Save failed: {e}"));
|
|
|
|
|
|
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
|
|
|
|
// Continue a pending tab close.
|
|
|
|
|
|
if self.save_dialog_for_unsaved {
|
2026-07-27 12:02:50 +03:00
|
|
|
|
if saved {
|
|
|
|
|
|
if let Some(crate::app::PendingClose::Tab(idx)) =
|
|
|
|
|
|
self.pending_close.take()
|
|
|
|
|
|
{
|
|
|
|
|
|
let cont = self.update(Message::TabClose(idx));
|
|
|
|
|
|
let rest = self.continue_tab_close_queue();
|
2026-07-29 00:41:51 +03:00
|
|
|
|
return Task::batch([close, recent_task, cont, rest]);
|
2026-07-27 12:02:50 +03:00
|
|
|
|
}
|
|
|
|
|
|
} else if self.pending_close.is_some() {
|
|
|
|
|
|
let retry = self.open_unsaved_dialog_window();
|
2026-07-29 00:41:51 +03:00
|
|
|
|
return Task::batch([close, recent_task, retry]);
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-29 00:41:51 +03:00
|
|
|
|
Task::batch([close, recent_task])
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 09:12:36 +03:00
|
|
|
|
/// Native: a destination path came back from the OS save dialog — write the
|
|
|
|
|
|
/// file there in the chosen version. `None` means the user cancelled.
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
|
|
pub(super) fn on_save_dialog_path_picked(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
picked: Option<std::path::PathBuf>,
|
|
|
|
|
|
) -> Task<Message> {
|
|
|
|
|
|
let Some(path) = picked else {
|
|
|
|
|
|
// Cancelled the OS dialog — if this came from the unsaved-changes →
|
|
|
|
|
|
// save → close flow, re-show the confirmation so the tab isn't lost.
|
|
|
|
|
|
if self.save_dialog_for_unsaved && self.pending_close.is_some() {
|
|
|
|
|
|
return self.open_unsaved_dialog_window();
|
|
|
|
|
|
}
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
let (_ext, version) = crate::io::parse_save_format(&self.save_dialog_format);
|
|
|
|
|
|
let i = self.active_tab;
|
2026-07-25 02:58:46 +03:00
|
|
|
|
self.prepare_native_save(i);
|
|
|
|
|
|
let continuation = if self.save_dialog_for_unsaved {
|
|
|
|
|
|
match self.pending_close {
|
|
|
|
|
|
Some(crate::app::PendingClose::Tab(_)) => crate::app::SaveContinuation::CloseTab,
|
|
|
|
|
|
Some(crate::app::PendingClose::Quit) => crate::app::SaveContinuation::Quit,
|
|
|
|
|
|
None => crate::app::SaveContinuation::None,
|
2026-07-11 09:12:36 +03:00
|
|
|
|
}
|
2026-07-25 02:58:46 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
crate::app::SaveContinuation::None
|
|
|
|
|
|
};
|
|
|
|
|
|
self.queue_native_save(
|
|
|
|
|
|
i,
|
|
|
|
|
|
path,
|
|
|
|
|
|
version,
|
|
|
|
|
|
crate::app::SavePurpose::SaveAs,
|
|
|
|
|
|
continuation,
|
|
|
|
|
|
true,
|
2026-07-27 13:13:58 +03:00
|
|
|
|
true,
|
2026-07-25 02:58:46 +03:00
|
|
|
|
)
|
2026-07-11 09:12:36 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
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
|
2026-07-27 22:37:47 +03:00
|
|
|
|
/// document's source type and version, then save.
|
2026-06-29 11:47:37 +03:00
|
|
|
|
pub(super) fn on_aec_drop_same_version(&mut self) -> Task<Message> {
|
2026-07-27 22:37:47 +03:00
|
|
|
|
let tab = &self.tabs[self.active_tab];
|
|
|
|
|
|
let is_dxf = tab
|
|
|
|
|
|
.current_path
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.and_then(|path| path.extension())
|
|
|
|
|
|
.and_then(|extension| extension.to_str())
|
|
|
|
|
|
.map(|extension| extension.eq_ignore_ascii_case("dxf"))
|
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
let document = &tab.scene.document;
|
|
|
|
|
|
let src = if is_dxf {
|
|
|
|
|
|
document.version
|
|
|
|
|
|
} else {
|
|
|
|
|
|
document.dwg_source_version.unwrap_or(document.version)
|
|
|
|
|
|
};
|
|
|
|
|
|
self.save_dialog_format = crate::io::format_for_version(src, is_dxf);
|
|
|
|
|
|
// Strip the old extension so the confirm path appends the source type.
|
2026-06-29 11:47:37 +03:00
|
|
|
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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> {
|
2026-07-25 02:58:46 +03:00
|
|
|
|
let mut tasks = Vec::new();
|
2026-06-29 13:17:40 +03:00
|
|
|
|
for i in 0..self.tabs.len() {
|
2026-07-25 02:58:46 +03:00
|
|
|
|
if !self.tabs[i].dirty
|
|
|
|
|
|
|| self.active_save_jobs.contains_key(&self.tabs[i].id)
|
|
|
|
|
|
{
|
2026-06-29 13:17:40 +03:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
let version = self.tabs[i].scene.document.version;
|
2026-07-25 02:58:46 +03:00
|
|
|
|
let target = self.autosave_target(i);
|
|
|
|
|
|
tasks.push(self.queue_native_save(
|
|
|
|
|
|
i,
|
|
|
|
|
|
target,
|
|
|
|
|
|
version,
|
|
|
|
|
|
crate::app::SavePurpose::Autosave,
|
|
|
|
|
|
crate::app::SaveContinuation::None,
|
|
|
|
|
|
false,
|
2026-07-27 13:13:58 +03:00
|
|
|
|
false,
|
2026-07-25 02:58:46 +03:00
|
|
|
|
));
|
2026-06-29 13:17:40 +03:00
|
|
|
|
}
|
2026-07-25 02:58:46 +03:00
|
|
|
|
Task::batch(tasks)
|
2026-06-29 13:17:40 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[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,
|
2026-07-31 11:16:14 +03:00
|
|
|
|
plot_area: &str,
|
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
|
|
|
|
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();
|
2026-07-31 11:16:14 +03:00
|
|
|
|
let plot_window = self.plot_window;
|
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 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);
|
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;
|
2026-07-31 11:16:14 +03:00
|
|
|
|
ps.plot_type = match plot_area {
|
|
|
|
|
|
"Window" => PlotType::Window,
|
|
|
|
|
|
"Display" => PlotType::LastScreenDisplay,
|
|
|
|
|
|
"Extents" => PlotType::Extents,
|
|
|
|
|
|
_ => PlotType::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
|
|
|
|
};
|
2026-07-31 11:16:14 +03:00
|
|
|
|
if plot_area == "Window" {
|
|
|
|
|
|
if let Some((x0, y0, x1, y1)) = plot_window {
|
|
|
|
|
|
ps.set_plot_window(x0, y0, x1, y1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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
|
|
|
|
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;
|
2026-07-28 17:16:27 +03:00
|
|
|
|
// The paper sheet fill is cached by epoch, while document
|
|
|
|
|
|
// entity tessellation is unaffected by the paper size.
|
|
|
|
|
|
self.tabs[i].scene.bump_geometry_no_blocks();
|
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_info(&format!(
|
|
|
|
|
|
"Page setup: {w:.1}×{h:.1} mm area={plot_area} \
|
|
|
|
|
|
center={center} rot={rotation}°"
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-14 13:38:16 -07:00
|
|
|
|
pub(in crate::app) fn on_plot_export_path_some(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
path: std::path::PathBuf,
|
|
|
|
|
|
) -> Task<Message> {
|
2026-07-31 11:16:14 +03:00
|
|
|
|
let (wires, hatches, wipeouts, page_w, page_h, ox, oy, rotation, scale, clip) =
|
|
|
|
|
|
self.layout_plot_params();
|
|
|
|
|
|
let plot_style = if self.plot_dialog.with_styles {
|
|
|
|
|
|
self.active_plot_style.clone()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
2026-07-25 18:49:18 +03:00
|
|
|
|
let worker_path = path.clone();
|
|
|
|
|
|
background_task(
|
|
|
|
|
|
move || {
|
|
|
|
|
|
crate::io::pdf_export::export_pdf(
|
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
|
|
|
|
&wires,
|
2026-07-31 11:16:14 +03:00
|
|
|
|
&hatches,
|
|
|
|
|
|
&wipeouts,
|
|
|
|
|
|
page_w,
|
|
|
|
|
|
page_h,
|
|
|
|
|
|
ox,
|
|
|
|
|
|
oy,
|
|
|
|
|
|
rotation,
|
|
|
|
|
|
scale,
|
|
|
|
|
|
clip,
|
2026-07-25 18:49:18 +03:00
|
|
|
|
&worker_path,
|
|
|
|
|
|
plot_style.as_ref(),
|
|
|
|
|
|
)
|
|
|
|
|
|
.map(|_| format!("Exported: {}", worker_path.display()))
|
|
|
|
|
|
.map_err(|e| format!("Export failed: {e}"))
|
|
|
|
|
|
},
|
|
|
|
|
|
|result| Message::BackgroundIoFinished(result, false),
|
|
|
|
|
|
)
|
2026-07-05 23:12:52 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-31 11:16:14 +03:00
|
|
|
|
/// Export the pending Extents/Window/Display area using the same clipped
|
|
|
|
|
|
/// render path in model space and paper space.
|
|
|
|
|
|
pub(super) fn on_plot_window_export_path_some(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
path: std::path::PathBuf,
|
2026-07-25 18:49:18 +03:00
|
|
|
|
) -> Task<Message> {
|
2026-07-31 11:16:14 +03:00
|
|
|
|
let job = match self.plot_dialog.area.as_str() {
|
|
|
|
|
|
"Display" => self.display_plot_job(),
|
|
|
|
|
|
"Extents" => self.extents_plot_job(),
|
|
|
|
|
|
_ => self.window_plot_job(),
|
|
|
|
|
|
};
|
|
|
|
|
|
let Some((wires, hatches, wipeouts, page_w, page_h, ox, oy, rotation, scale, clip)) =
|
|
|
|
|
|
job
|
|
|
|
|
|
else {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error("Plot area is empty. Pick a larger window.");
|
|
|
|
|
|
return Task::none();
|
|
|
|
|
|
};
|
|
|
|
|
|
let plot_style = if self.plot_dialog.with_styles {
|
|
|
|
|
|
self.active_plot_style.clone()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
2026-07-25 18:49:18 +03:00
|
|
|
|
let worker_path = path.clone();
|
|
|
|
|
|
self.close_active_modal();
|
|
|
|
|
|
background_task(
|
|
|
|
|
|
move || {
|
|
|
|
|
|
crate::io::pdf_export::export_pdf(
|
2026-07-05 23:12:52 +02:00
|
|
|
|
&wires,
|
2026-07-31 11:16:14 +03:00
|
|
|
|
&hatches,
|
|
|
|
|
|
&wipeouts,
|
|
|
|
|
|
page_w,
|
|
|
|
|
|
page_h,
|
|
|
|
|
|
ox,
|
|
|
|
|
|
oy,
|
|
|
|
|
|
rotation,
|
|
|
|
|
|
scale,
|
2026-07-05 23:12:52 +02:00
|
|
|
|
clip,
|
2026-07-25 18:49:18 +03:00
|
|
|
|
&worker_path,
|
|
|
|
|
|
plot_style.as_ref(),
|
|
|
|
|
|
)
|
|
|
|
|
|
.map(|_| {format!(
|
2026-07-05 23:12:52 +02:00
|
|
|
|
"Plotted window to {}",
|
2026-07-25 18:49:18 +03:00
|
|
|
|
worker_path
|
|
|
|
|
|
.file_name().unwrap_or_default().to_string_lossy()
|
|
|
|
|
|
)
|
|
|
|
|
|
}).map_err(|e| format!("Plot failed: {e}"))
|
|
|
|
|
|
},
|
|
|
|
|
|
|result| Message::BackgroundIoFinished(result, 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
|
|
|
|
}
|
|
|
|
|
|
|
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,
|
2026-07-31 11:16:14 +03:00
|
|
|
|
/// draw-origin offset, rotation, unit scale, and optional clip. Shared by
|
|
|
|
|
|
/// PDF export, preview, and printer output so all three render identically.
|
|
|
|
|
|
pub(super) fn layout_plot_params(&self) -> LayoutPlotParams {
|
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 i = self.active_tab;
|
|
|
|
|
|
let scene = &self.tabs[i].scene;
|
2026-07-31 11:16:14 +03:00
|
|
|
|
let paper_space = scene.current_layout != "Model";
|
|
|
|
|
|
let (source_wires, hatches, wipeouts) = plot_scene_content(scene);
|
|
|
|
|
|
// The printable-area rectangle is an on-screen guide, not drawing
|
|
|
|
|
|
// content. It used to leak into every paper-space PDF/preview/print.
|
|
|
|
|
|
let wires = if paper_space {
|
|
|
|
|
|
std::sync::Arc::new(
|
|
|
|
|
|
source_wires
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.filter(|wire| wire.name != "paper_printable_area")
|
|
|
|
|
|
.cloned()
|
|
|
|
|
|
.collect(),
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
source_wires
|
|
|
|
|
|
};
|
|
|
|
|
|
if let Some(((x0, y0), (x1, y1))) = scene.paper_limits() {
|
|
|
|
|
|
// Paper geometry is stored in layout units; PDF pages use mm.
|
|
|
|
|
|
// Scale both geometry and offsets by the same unit conversion.
|
|
|
|
|
|
// The old path converted only page width/height, so inch/scaled
|
|
|
|
|
|
// layouts placed tiny or 25.4×-shifted content on the page.
|
|
|
|
|
|
let units_per_mm = scene.paper_space_unit_factor().max(1e-9);
|
|
|
|
|
|
let mm_per_unit = 1.0 / units_per_mm;
|
|
|
|
|
|
let paper_w = ((x1 - x0) * mm_per_unit).max(1.0);
|
|
|
|
|
|
let paper_h = ((y1 - y0) * mm_per_unit).max(1.0);
|
|
|
|
|
|
// Preview/print is a read-only operation unless "Save to layout" is
|
|
|
|
|
|
// checked. Therefore its transient choices must come from the dialog,
|
|
|
|
|
|
// not the layout's previously persisted PlotSettings.
|
|
|
|
|
|
let dialog_rotation = self
|
|
|
|
|
|
.plot_dialog
|
|
|
|
|
|
.rotation
|
|
|
|
|
|
.trim_end_matches('°')
|
|
|
|
|
|
.parse::<i32>()
|
|
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
|
let plot_extents = self.plot_dialog.area == "Extents";
|
|
|
|
|
|
let plot_layout = self.plot_dialog.area == "Layout";
|
|
|
|
|
|
// `paper_limits()` already returns the current layout's final,
|
|
|
|
|
|
// rotation-adjusted sheet bounds. Applying its persisted plot
|
|
|
|
|
|
// rotation again swaps the page and rotates the printable clip out
|
|
|
|
|
|
// of view, producing a blank PDF for rotated custom sheets.
|
|
|
|
|
|
let rotation = if plot_layout { 0 } else { dialog_rotation };
|
|
|
|
|
|
let centered = self.plot_dialog.center;
|
|
|
|
|
|
let origin_x = self
|
|
|
|
|
|
.plot_dialog
|
|
|
|
|
|
.offset_x
|
|
|
|
|
|
.parse::<f64>()
|
|
|
|
|
|
.unwrap_or(0.0);
|
|
|
|
|
|
let origin_y = self
|
|
|
|
|
|
.plot_dialog
|
|
|
|
|
|
.offset_y
|
|
|
|
|
|
.parse::<f64>()
|
|
|
|
|
|
.unwrap_or(0.0);
|
|
|
|
|
|
|
|
|
|
|
|
let (scale, offset_x, offset_y) = if plot_extents {
|
|
|
|
|
|
let (min_x, min_y, max_x, max_y) =
|
|
|
|
|
|
plot_content_extents(&wires, &hatches, &wipeouts)
|
|
|
|
|
|
.unwrap_or((x0, y0, x1, y1));
|
|
|
|
|
|
let content_w = (max_x - min_x).max(1e-9);
|
|
|
|
|
|
let content_h = (max_y - min_y).max(1e-9);
|
|
|
|
|
|
let scale = if self.plot_dialog.scale == "Fit" {
|
|
|
|
|
|
const MARGIN: f64 = 1.05;
|
|
|
|
|
|
((paper_w / MARGIN) / content_w)
|
|
|
|
|
|
.min((paper_h / MARGIN) / content_h)
|
|
|
|
|
|
.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
|
|
|
|
} else {
|
2026-07-31 11:16:14 +03:00
|
|
|
|
let (num, den) = parse_plot_scale(&self.plot_dialog.scale);
|
|
|
|
|
|
((num / den) * mm_per_unit).max(1e-9)
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
};
|
2026-07-31 11:16:14 +03:00
|
|
|
|
let target_x = if centered {
|
|
|
|
|
|
(paper_w - content_w * scale) * 0.5
|
|
|
|
|
|
} else {
|
|
|
|
|
|
origin_x
|
|
|
|
|
|
};
|
|
|
|
|
|
let target_y = if centered {
|
|
|
|
|
|
(paper_h - content_h * scale) * 0.5
|
|
|
|
|
|
} else {
|
|
|
|
|
|
origin_y
|
|
|
|
|
|
};
|
|
|
|
|
|
(
|
|
|
|
|
|
scale,
|
|
|
|
|
|
target_x / scale - min_x,
|
|
|
|
|
|
target_y / scale - min_y,
|
|
|
|
|
|
)
|
|
|
|
|
|
} else if plot_layout {
|
|
|
|
|
|
// Layout is the one Paper-only plot mode: reproduce the sheet
|
|
|
|
|
|
// exactly. Its lower-left paper bound maps to PDF (0, 0);
|
|
|
|
|
|
// persisted plot origins/centering are already represented by
|
|
|
|
|
|
// the layout geometry and must not shift the sheet again.
|
|
|
|
|
|
(mm_per_unit, -x0, -y0)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Legacy direct callers still get dialog positioning. Normal
|
|
|
|
|
|
// Display/Window paths use `area_plot_job` instead.
|
|
|
|
|
|
let scale = mm_per_unit;
|
|
|
|
|
|
let target_x = if centered {
|
|
|
|
|
|
(paper_w - (x1 - x0) * scale) * 0.5
|
|
|
|
|
|
} else {
|
|
|
|
|
|
origin_x
|
|
|
|
|
|
};
|
|
|
|
|
|
let target_y = if centered {
|
|
|
|
|
|
(paper_h - (y1 - y0) * scale) * 0.5
|
|
|
|
|
|
} else {
|
|
|
|
|
|
origin_y
|
|
|
|
|
|
};
|
|
|
|
|
|
(scale, target_x / scale - x0, target_y / scale - y0)
|
|
|
|
|
|
};
|
|
|
|
|
|
let (page_w, page_h) = match rotation {
|
|
|
|
|
|
90 | 270 => (paper_h, paper_w),
|
|
|
|
|
|
_ => (paper_w, paper_h),
|
|
|
|
|
|
};
|
|
|
|
|
|
// Layout plots keep the full physical sheet as the PDF page, but
|
|
|
|
|
|
// ink is restricted to the device's printable rectangle. Extents
|
|
|
|
|
|
// has its own fitted area and must not inherit this paper clip.
|
|
|
|
|
|
let clip = if plot_layout {
|
|
|
|
|
|
scene.printable_area_limits().map(|((px0, py0), (px1, py1))| {
|
|
|
|
|
|
(
|
|
|
|
|
|
(px0 + offset_x) as f32,
|
|
|
|
|
|
(py0 + offset_y) as f32,
|
|
|
|
|
|
(px1 - px0) as f32,
|
|
|
|
|
|
(py1 - py0) as f32,
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
|
|
|
wires,
|
|
|
|
|
|
hatches,
|
|
|
|
|
|
wipeouts,
|
|
|
|
|
|
page_w,
|
|
|
|
|
|
page_h,
|
|
|
|
|
|
offset_x,
|
|
|
|
|
|
offset_y,
|
|
|
|
|
|
rotation,
|
|
|
|
|
|
scale as f32,
|
|
|
|
|
|
clip,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Model space keeps its established extents + 5% margin behaviour.
|
|
|
|
|
|
let (page_w, page_h, offset_x, offset_y) =
|
|
|
|
|
|
if let Some((min, max)) = scene.model_space_extents() {
|
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 margin = 1.05_f64;
|
2026-07-31 11:16:14 +03:00
|
|
|
|
let width = ((max.x - min.x) as f64 * margin).max(1.0);
|
|
|
|
|
|
let height = ((max.y - min.y) as f64 * margin).max(1.0);
|
|
|
|
|
|
let pad_x = (width - (max.x - min.x) as f64) * 0.5;
|
|
|
|
|
|
let pad_y = (height - (max.y - min.y) as f64) * 0.5;
|
|
|
|
|
|
(
|
|
|
|
|
|
width,
|
|
|
|
|
|
height,
|
|
|
|
|
|
-(min.x as f64) + pad_x,
|
|
|
|
|
|
-(min.y as f64) + pad_y,
|
|
|
|
|
|
)
|
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
|
|
|
|
} else {
|
2026-07-31 11:16:14 +03:00
|
|
|
|
(297.0, 210.0, 0.0, 0.0)
|
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
|
|
|
|
};
|
|
|
|
|
|
(
|
|
|
|
|
|
wires,
|
|
|
|
|
|
hatches,
|
|
|
|
|
|
wipeouts,
|
2026-07-31 11:16:14 +03:00
|
|
|
|
page_w,
|
|
|
|
|
|
page_h,
|
|
|
|
|
|
offset_x,
|
|
|
|
|
|
offset_y,
|
|
|
|
|
|
0,
|
|
|
|
|
|
1.0,
|
|
|
|
|
|
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
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub(super) fn on_print_to_printer(&mut self) -> Task<Message> {
|
2026-07-31 11:16:14 +03:00
|
|
|
|
let (wires, hatches, wipeouts, page_w, page_h, ox, oy, rotation, scale, clip) =
|
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.layout_plot_params();
|
2026-07-31 11:16:14 +03:00
|
|
|
|
let plot_style = if self.plot_dialog.with_styles {
|
|
|
|
|
|
self.active_plot_style.clone()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
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
|
|
|
|
self.command_line.push_info("Sending to system printer…");
|
2026-07-25 18:49:18 +03:00
|
|
|
|
background_task(
|
|
|
|
|
|
move || {
|
|
|
|
|
|
iced::futures::executor::block_on(
|
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
|
|
|
|
crate::io::print_to_printer::print_wires(
|
2026-07-31 11:16:14 +03:00
|
|
|
|
wires, hatches, wipeouts, page_w, page_h, ox, oy, rotation, scale, clip,
|
|
|
|
|
|
plot_style,
|
2026-07-25 18:49:18 +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
|
|
|
|
},
|
|
|
|
|
|
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;
|
perf(scene): return the memoized wire set by Arc, not by deep copy
entity_wires() cloned the entire wire set — every point, fill, and snap
buffer of every entity — on each call, although the set is already
memoized behind an Arc and none of the 18 call sites mutates it. On
large mesh imports (#358: 42M faces, ~210 MB of wire content per 1M
triangles) that is a multi-GB allocation + memcpy on every pick, snap,
selection, or plot pass.
Return the Arc instead. Call sites iterate as before; the two
window-plot paths that build a culled owned subset now clone only the
wires inside the plot window, and the print pipeline moves the Arc into
its async task.
Measured (tests/mem_probe_meshent.rs, 1M-triangle MESH, release):
resident after entity_wires() drops 758 -> 547 MB (866 -> 646 B/face).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 12:56:36 -07:00
|
|
|
|
for w in scene.entity_wires().iter() {
|
2026-07-09 12:57:35 +03:00
|
|
|
|
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}*"));
|
2026-07-31 11:16:14 +03:00
|
|
|
|
if self.plot_dialog.area != "Layout" {
|
|
|
|
|
|
// A layout may persist Window/Extents/Display together with its
|
|
|
|
|
|
// sheet-only origin/rotation. Start common area modes with the
|
|
|
|
|
|
// same neutral transform as Model even when no Area-change event
|
|
|
|
|
|
// fires after opening the dialog.
|
|
|
|
|
|
self.normalize_common_plot_dialog();
|
|
|
|
|
|
}
|
2026-07-09 12:25:46 +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
|
|
|
|
self.active_modal = Some(crate::app::ModalKind::Plot);
|
|
|
|
|
|
Task::none()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-31 11:16:14 +03:00
|
|
|
|
fn normalize_common_plot_dialog(&mut self) {
|
|
|
|
|
|
let layout_rotation = self
|
|
|
|
|
|
.plot_dialog
|
|
|
|
|
|
.rotation
|
|
|
|
|
|
.trim_end_matches('°')
|
|
|
|
|
|
.parse::<i32>()
|
|
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
|
if matches!(layout_rotation, 90 | 270) {
|
|
|
|
|
|
self.plot_dialog.orientation = if self.plot_dialog.orientation == "Portrait" {
|
|
|
|
|
|
"Landscape"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
"Portrait"
|
|
|
|
|
|
}
|
|
|
|
|
|
.into();
|
|
|
|
|
|
}
|
|
|
|
|
|
self.plot_dialog.center = true;
|
|
|
|
|
|
self.plot_dialog.offset_x = "0.0".into();
|
|
|
|
|
|
self.plot_dialog.offset_y = "0.0".into();
|
|
|
|
|
|
self.plot_dialog.rotation = "0°".into();
|
|
|
|
|
|
self.plot_dialog.scale = "Fit".into();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
/// 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) => {
|
2026-07-31 11:16:14 +03:00
|
|
|
|
// A paper Layout page setup commonly carries physical-sheet
|
|
|
|
|
|
// rotation, origin and 1:1 scale. Those values are correct only
|
|
|
|
|
|
// for Layout; carrying them into Window/Extents/Display moves a
|
|
|
|
|
|
// large selected area off-page. Enter the three common area
|
|
|
|
|
|
// modes with the same neutral defaults used in Model space.
|
|
|
|
|
|
if self.plot_dialog.area == "Layout" && s != "Layout" {
|
|
|
|
|
|
self.normalize_common_plot_dialog();
|
|
|
|
|
|
}
|
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.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 => {
|
2026-07-31 11:16:14 +03:00
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
if self.tabs[i].scene.current_layout != "Model"
|
|
|
|
|
|
&& self.tabs[i].scene.active_viewport.is_some()
|
|
|
|
|
|
{
|
|
|
|
|
|
// A layout Window is selected in paper coordinates and may
|
|
|
|
|
|
// extend anywhere on the canvas, including outside the sheet.
|
|
|
|
|
|
// Leave MSPACE so the floating viewport cannot constrain the
|
|
|
|
|
|
// two picks to its model view.
|
|
|
|
|
|
self.tabs[i].scene.deselect_all();
|
|
|
|
|
|
self.tabs[i].scene.active_viewport = None;
|
|
|
|
|
|
self.adopt_view_display(i);
|
|
|
|
|
|
self.tabs[i].refresh_active_ucs();
|
|
|
|
|
|
self.refresh_properties();
|
|
|
|
|
|
self.sync_dyn_fields();
|
|
|
|
|
|
}
|
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.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,
|
2026-07-31 11:16:14 +03:00
|
|
|
|
"Display" => PlotType::LastScreenDisplay,
|
2026-07-09 12:25:46 +03:00
|
|
|
|
_ => PlotType::Extents,
|
|
|
|
|
|
};
|
2026-07-31 11:16:14 +03:00
|
|
|
|
if d.area == "Window" {
|
|
|
|
|
|
if let Some((x0, y0, x1, y1)) = self.plot_window {
|
|
|
|
|
|
ps.set_plot_window(x0, y0, x1, y1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-09 12:25:46 +03:00
|
|
|
|
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;
|
2026-07-31 11:16:14 +03:00
|
|
|
|
if matches!(ps.plot_type, PlotType::Window) && !ps.plot_window.is_empty() {
|
|
|
|
|
|
self.plot_window = Some((
|
|
|
|
|
|
ps.plot_window.lower_left_x,
|
|
|
|
|
|
ps.plot_window.lower_left_y,
|
|
|
|
|
|
ps.plot_window.upper_right_x,
|
|
|
|
|
|
ps.plot_window.upper_right_y,
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
2026-07-09 12:25:46 +03:00
|
|
|
|
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",
|
2026-07-31 11:16:14 +03:00
|
|
|
|
PlotType::LastScreenDisplay => "Display",
|
2026-07-09 12:25:46 +03:00
|
|
|
|
_ => "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()
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-31 11:16:14 +03:00
|
|
|
|
/// Copy transient paper/scale choices out of the dialog without changing
|
|
|
|
|
|
/// the active layout.
|
|
|
|
|
|
fn sync_dialog_plot_runtime(&mut self) {
|
|
|
|
|
|
use crate::io::paper_sizes::{Orientation, PaperSize};
|
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 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
|
|
|
|
|
|
};
|
|
|
|
|
|
self.plot_format = paper;
|
|
|
|
|
|
self.plot_orientation = orient;
|
|
|
|
|
|
self.plot_scale = d.scale.clone();
|
2026-07-31 11:16:14 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_dialog_to_layout(&mut self) {
|
|
|
|
|
|
use crate::io::paper_sizes::sheet_mm;
|
|
|
|
|
|
self.sync_dialog_plot_runtime();
|
|
|
|
|
|
let d = self.plot_dialog.clone();
|
|
|
|
|
|
let (sheet_w, sheet_h) = sheet_mm(self.plot_format, self.plot_orientation);
|
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 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);
|
|
|
|
|
|
self.apply_plot_page_settings(
|
2026-07-31 11:16:14 +03:00
|
|
|
|
sheet_w,
|
|
|
|
|
|
sheet_h,
|
|
|
|
|
|
&d.area,
|
|
|
|
|
|
d.center,
|
|
|
|
|
|
off_x,
|
|
|
|
|
|
off_y,
|
|
|
|
|
|
rotation,
|
|
|
|
|
|
&d.scale,
|
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
|
|
|
|
);
|
2026-07-09 12:25:46 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-31 11:16:14 +03:00
|
|
|
|
/// Open a preview PDF, export a PDF, or send the job to the chosen printer.
|
|
|
|
|
|
/// The active layout changes only when "Save to layout" is checked.
|
2026-07-09 12:25:46 +03:00
|
|
|
|
fn on_plot_dlg_commit(&mut self, preview: bool) -> Task<Message> {
|
|
|
|
|
|
// Remember the user's print preferences across sessions.
|
refactor(config): consolidate settings into one grouped settings.json
App preferences were spread across six flat plain-text files (settings.txt,
recent.txt, recent_limit.txt, statusbar.txt, ribbon.txt, plot.txt), each with a
bespoke line parser and no grouping. Replace them with a single grouped JSON,
serialized via serde (enabling the derive feature; serde_json was already a
dependency).
- New app/config.rs: AppConfig { settings, recent, statusbar, ribbon, plot } →
<config>/settings.json (pretty JSON), load()/save().
- Make UserSettings, StatusBarConfig/StatusPill, CollapseMode and
PlotDialogState serde-derived; #[serde(skip)] marks the plot dialog's runtime
fields so only print preferences persist. Their bespoke .txt load/save is
removed.
- App gains current_config/apply_config/save_config: one AppConfig::load at
startup distributes into live state; every settings-change site
(persist_settings_if_changed, recent add/remove/limit, status-bar pill
toggle, ribbon density, plot commit) routes through save_config, which diffs
against the last write to avoid thrashing.
- Clean start: the old flat files are ignored (no migration). ocad.pgp stays a
separate hand-editable file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:41:34 +03:00
|
|
|
|
self.save_config();
|
2026-07-09 12:25:46 +03:00
|
|
|
|
let d = self.plot_dialog.clone();
|
2026-07-31 11:16:14 +03:00
|
|
|
|
// Preview and normal printing must not resize/mutate the live layout.
|
|
|
|
|
|
// Persist only when explicitly requested; the runtime paper/scale
|
|
|
|
|
|
// choices still drive this one plot operation.
|
|
|
|
|
|
self.sync_dialog_plot_runtime();
|
|
|
|
|
|
if !preview && d.save_layout {
|
|
|
|
|
|
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
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-31 11:16:14 +03:00
|
|
|
|
// Extents, Window and Display use one plot path in both spaces. Only
|
|
|
|
|
|
// Paper-space Layout is special: it uses the physical sheet bounds.
|
|
|
|
|
|
if matches!(d.area.as_str(), "Extents" | "Window" | "Display") {
|
|
|
|
|
|
let job = match d.area.as_str() {
|
|
|
|
|
|
"Display" => self.display_plot_job(),
|
|
|
|
|
|
"Extents" => self.extents_plot_job(),
|
|
|
|
|
|
_ => self.window_plot_job(),
|
|
|
|
|
|
};
|
|
|
|
|
|
let Some((
|
|
|
|
|
|
w_wires,
|
|
|
|
|
|
w_hatches,
|
|
|
|
|
|
w_wipeouts,
|
|
|
|
|
|
sw,
|
|
|
|
|
|
sh,
|
|
|
|
|
|
wox,
|
|
|
|
|
|
woy,
|
|
|
|
|
|
wrotation,
|
|
|
|
|
|
wscale,
|
|
|
|
|
|
wclip,
|
|
|
|
|
|
)) = job
|
|
|
|
|
|
else {
|
|
|
|
|
|
self.command_line
|
|
|
|
|
|
.push_error("Plot area is empty. Pick a larger window.");
|
|
|
|
|
|
self.active_modal = Some(crate::app::ModalKind::Plot);
|
|
|
|
|
|
return 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
|
|
|
|
// Preview renders to a temp PDF and opens it — never a save dialog,
|
|
|
|
|
|
// whatever the output target is.
|
|
|
|
|
|
if preview {
|
|
|
|
|
|
let tmp = std::env::temp_dir().join("open_cad_studio_preview.pdf");
|
2026-07-25 18:49:18 +03:00
|
|
|
|
return background_task(
|
|
|
|
|
|
move || {
|
|
|
|
|
|
crate::io::pdf_export::export_pdf(
|
2026-07-31 11:16:14 +03:00
|
|
|
|
&w_wires, &w_hatches, &w_wipeouts, sw, sh, wox, woy, wrotation, wscale, wclip, &tmp,
|
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
|
|
|
|
plot_style.as_ref(),
|
2026-07-25 18:49:18 +03:00
|
|
|
|
).and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp))
|
|
|
|
|
|
.map(|_| "Opened plot preview.".to_string()).map_err(|e| format!("Preview failed: {e}"))
|
|
|
|
|
|
},
|
|
|
|
|
|
|result| Message::BackgroundIoFinished(result, true),
|
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
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if d.to_file {
|
|
|
|
|
|
// Tested clipped export (opens a save dialog).
|
|
|
|
|
|
return Task::done(Message::PlotWindowExport);
|
|
|
|
|
|
}
|
|
|
|
|
|
let tmp = std::env::temp_dir().join("open_cad_studio_print.pdf");
|
2026-07-25 18:49:18 +03:00
|
|
|
|
let opts = self.plot_print_options(&d);
|
|
|
|
|
|
return background_task(
|
|
|
|
|
|
move || {
|
|
|
|
|
|
crate::io::pdf_export::export_pdf(
|
2026-07-31 11:16:14 +03:00
|
|
|
|
&w_wires, &w_hatches, &w_wipeouts, sw, sh, wox, woy, wrotation, wscale, wclip, &tmp,
|
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
|
|
|
|
plot_style.as_ref(),
|
2026-07-25 18:49:18 +03:00
|
|
|
|
).and_then(|_| crate::io::print_to_printer::print_existing_pdf(&tmp, &opts))
|
|
|
|
|
|
.map(|printer| format!("Sent to printer: {printer}"))
|
|
|
|
|
|
.map_err(|e| format!("Print failed: {e}"))
|
|
|
|
|
|
},
|
|
|
|
|
|
|result| Message::BackgroundIoFinished(result, false),
|
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
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-31 11:16:14 +03:00
|
|
|
|
let (wires, hatches, wipeouts, page_w, page_h, ox, oy, rotation, scale, clip) =
|
|
|
|
|
|
self.layout_plot_params();
|
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
|
|
|
|
|
|
|
|
|
|
if preview {
|
|
|
|
|
|
let tmp = std::env::temp_dir().join("open_cad_studio_preview.pdf");
|
2026-07-25 18:49:18 +03:00
|
|
|
|
return background_task(
|
|
|
|
|
|
move || {
|
|
|
|
|
|
crate::io::pdf_export::export_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
|
|
|
|
&wires,
|
|
|
|
|
|
&hatches,
|
|
|
|
|
|
&wipeouts,
|
2026-07-31 11:16:14 +03:00
|
|
|
|
page_w,
|
|
|
|
|
|
page_h,
|
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
|
|
|
|
ox,
|
|
|
|
|
|
oy,
|
2026-07-31 11:16:14 +03:00
|
|
|
|
rotation,
|
|
|
|
|
|
scale,
|
|
|
|
|
|
clip,
|
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
|
|
|
|
&tmp,
|
|
|
|
|
|
plot_style.as_ref(),
|
2026-07-25 18:49:18 +03:00
|
|
|
|
).and_then(|_| crate::io::print_to_printer::open_in_viewer(&tmp))
|
|
|
|
|
|
.map(|_| "Opened plot preview.".to_string()).map_err(|e| format!("Preview failed: {e}"))
|
|
|
|
|
|
},
|
|
|
|
|
|
|result| Message::BackgroundIoFinished(result, true),
|
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
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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…");
|
2026-07-25 18:49:18 +03:00
|
|
|
|
background_task(
|
|
|
|
|
|
move || {
|
|
|
|
|
|
iced::futures::executor::block_on(
|
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
|
|
|
|
crate::io::print_to_printer::print_wires_with(
|
2026-07-31 11:16:14 +03:00
|
|
|
|
wires, hatches, wipeouts, page_w, page_h, ox, oy, rotation, scale, clip,
|
|
|
|
|
|
plot_style, opts,
|
2026-07-25 18:49:18 +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
|
|
|
|
},
|
|
|
|
|
|
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),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-31 11:16:14 +03:00
|
|
|
|
fn window_plot_job(&self) -> Option<ClippedPlotParams> {
|
|
|
|
|
|
self.area_plot_job(self.plot_window?)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn display_plot_job(&self) -> Option<ClippedPlotParams> {
|
|
|
|
|
|
self.area_plot_job(self.display_plot_window()?)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn extents_plot_job(&self) -> Option<ClippedPlotParams> {
|
|
|
|
|
|
let scene = &self.tabs[self.active_tab].scene;
|
|
|
|
|
|
if scene.current_layout == "Model" {
|
|
|
|
|
|
let (min, max) = scene.model_space_extents()?;
|
|
|
|
|
|
return self.area_plot_job((
|
|
|
|
|
|
min.x as f64,
|
|
|
|
|
|
min.y as f64,
|
|
|
|
|
|
max.x as f64,
|
|
|
|
|
|
max.y as f64,
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
let (wires, hatches, wipeouts) = plot_scene_content(scene);
|
|
|
|
|
|
let extents = plot_content_extents(
|
|
|
|
|
|
&wires
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.filter(|wire| wire.name != "paper_printable_area")
|
|
|
|
|
|
.cloned()
|
|
|
|
|
|
.collect::<Vec<_>>(),
|
|
|
|
|
|
&hatches,
|
|
|
|
|
|
&wipeouts,
|
|
|
|
|
|
)?;
|
|
|
|
|
|
self.area_plot_job(extents)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Current visible rectangle in the active space. The result is deliberately
|
|
|
|
|
|
/// not clamped to the paper sheet: Display and Window may include the grey
|
|
|
|
|
|
/// canvas outside the sheet, matching Model-space plotting.
|
|
|
|
|
|
fn display_plot_window(&self) -> Option<(f64, f64, f64, f64)> {
|
|
|
|
|
|
let scene = &self.tabs[self.active_tab].scene;
|
|
|
|
|
|
let (canvas_w, canvas_h) = scene.selection.borrow().vp_size;
|
|
|
|
|
|
let viewport = if scene.current_layout == "Model" {
|
|
|
|
|
|
scene.active_model_tile_bounds(canvas_w, canvas_h)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
iced::Rectangle {
|
|
|
|
|
|
x: 0.0,
|
|
|
|
|
|
y: 0.0,
|
|
|
|
|
|
width: canvas_w,
|
|
|
|
|
|
height: canvas_h,
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
if viewport.width < 1.0 || viewport.height < 1.0 {
|
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
|
|
|
|
return None;
|
|
|
|
|
|
}
|
2026-07-31 11:16:14 +03:00
|
|
|
|
let bounds = iced::Rectangle {
|
|
|
|
|
|
x: 0.0,
|
|
|
|
|
|
y: 0.0,
|
|
|
|
|
|
width: viewport.width,
|
|
|
|
|
|
height: viewport.height,
|
|
|
|
|
|
};
|
|
|
|
|
|
let camera = scene.camera.borrow();
|
|
|
|
|
|
let mut x0 = f64::INFINITY;
|
|
|
|
|
|
let mut y0 = f64::INFINITY;
|
|
|
|
|
|
let mut x1 = f64::NEG_INFINITY;
|
|
|
|
|
|
let mut y1 = f64::NEG_INFINITY;
|
|
|
|
|
|
for point in [
|
|
|
|
|
|
iced::Point::new(0.0, 0.0),
|
|
|
|
|
|
iced::Point::new(bounds.width, 0.0),
|
|
|
|
|
|
iced::Point::new(bounds.width, bounds.height),
|
|
|
|
|
|
iced::Point::new(0.0, bounds.height),
|
|
|
|
|
|
] {
|
|
|
|
|
|
let world = camera.unproject_on_plane(
|
|
|
|
|
|
point,
|
|
|
|
|
|
bounds,
|
|
|
|
|
|
glam::Vec3::Z,
|
|
|
|
|
|
glam::DVec3::ZERO,
|
|
|
|
|
|
);
|
|
|
|
|
|
x0 = x0.min(world.x);
|
|
|
|
|
|
y0 = y0.min(world.y);
|
|
|
|
|
|
x1 = x1.max(world.x);
|
|
|
|
|
|
y1 = y1.max(world.y);
|
|
|
|
|
|
}
|
|
|
|
|
|
(x0.is_finite()
|
|
|
|
|
|
&& y0.is_finite()
|
|
|
|
|
|
&& x1.is_finite()
|
|
|
|
|
|
&& y1.is_finite()
|
|
|
|
|
|
&& x1 - x0 > 1e-6
|
|
|
|
|
|
&& y1 - y0 > 1e-6)
|
|
|
|
|
|
.then_some((x0, y0, x1, y1))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Render a selected rectangle through one shared Model/Paper path. The
|
|
|
|
|
|
/// window may lie partly or wholly outside a paper sheet.
|
|
|
|
|
|
fn area_plot_job(&self, window: (f64, f64, f64, f64)) -> Option<ClippedPlotParams> {
|
|
|
|
|
|
use crate::io::paper_sizes::{sheet_mm, window_to_sheet, PlotScale};
|
|
|
|
|
|
let i = self.active_tab;
|
|
|
|
|
|
let (x0, y0, x1, y1) = window;
|
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
|
|
|
|
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 {
|
2026-07-31 11:16:14 +03:00
|
|
|
|
let mm_per_unit = if self.tabs[i].scene.current_layout == "Model" {
|
|
|
|
|
|
1.0
|
|
|
|
|
|
} else {
|
|
|
|
|
|
1.0 / self.tabs[i].scene.paper_space_unit_factor().max(1e-9)
|
|
|
|
|
|
};
|
|
|
|
|
|
PlotScale::Ratio((num / den) * mm_per_unit)
|
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
|
|
|
|
} else {
|
|
|
|
|
|
PlotScale::Fit
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-07-31 11:16:14 +03:00
|
|
|
|
let (scale, centered_x, centered_y) =
|
|
|
|
|
|
window_to_sheet((win_w, win_h), (sheet_w, sheet_h), scale_sel);
|
|
|
|
|
|
let target_x = if self.plot_dialog.center {
|
|
|
|
|
|
centered_x
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.plot_dialog.offset_x.parse::<f64>().unwrap_or(0.0)
|
|
|
|
|
|
};
|
|
|
|
|
|
let target_y = if self.plot_dialog.center {
|
|
|
|
|
|
centered_y
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.plot_dialog.offset_y.parse::<f64>().unwrap_or(0.0)
|
|
|
|
|
|
};
|
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 scene = &self.tabs[i].scene;
|
|
|
|
|
|
let (wx0, wy0, wx1, wy1) = (x0 as f32, y0 as f32, x1 as f32, y1 as f32);
|
2026-07-31 11:16:14 +03:00
|
|
|
|
let (all_wires, hatches, wipeouts) = plot_scene_content(scene);
|
|
|
|
|
|
let wires: Vec<_> = all_wires
|
perf(scene): return the memoized wire set by Arc, not by deep copy
entity_wires() cloned the entire wire set — every point, fill, and snap
buffer of every entity — on each call, although the set is already
memoized behind an Arc and none of the 18 call sites mutates it. On
large mesh imports (#358: 42M faces, ~210 MB of wire content per 1M
triangles) that is a multi-GB allocation + memcpy on every pick, snap,
selection, or plot pass.
Return the Arc instead. Call sites iterate as before; the two
window-plot paths that build a culled owned subset now clone only the
wires inside the plot window, and the print pipeline moves the Arc into
its async task.
Measured (tests/mem_probe_meshent.rs, 1M-triangle MESH, release):
resident after entity_wires() drops 758 -> 547 MB (866 -> 646 B/face).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 12:56:36 -07:00
|
|
|
|
.iter()
|
2026-07-31 11:16:14 +03:00
|
|
|
|
.filter(|w| w.name != "paper_printable_area")
|
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
|
|
|
|
.filter(|w| w.aabb[0] <= wx1 && w.aabb[2] >= wx0 && w.aabb[1] <= wy1 && w.aabb[3] >= wy0)
|
perf(scene): return the memoized wire set by Arc, not by deep copy
entity_wires() cloned the entire wire set — every point, fill, and snap
buffer of every entity — on each call, although the set is already
memoized behind an Arc and none of the 18 call sites mutates it. On
large mesh imports (#358: 42M faces, ~210 MB of wire content per 1M
triangles) that is a multi-GB allocation + memcpy on every pick, snap,
selection, or plot pass.
Return the Arc instead. Call sites iterate as before; the two
window-plot paths that build a culled owned subset now clone only the
wires inside the plot window, and the print pipeline moves the Arc into
its async task.
Measured (tests/mem_probe_meshent.rs, 1M-triangle MESH, release):
resident after entity_wires() drops 758 -> 547 MB (866 -> 646 B/face).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 12:56:36 -07:00
|
|
|
|
.cloned()
|
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
|
|
|
|
.collect();
|
2026-07-31 11:16:14 +03:00
|
|
|
|
let offset_x = (target_x / scale) - x0;
|
|
|
|
|
|
let offset_y = (target_y / scale) - y0;
|
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 clip = Some((
|
2026-07-31 11:16:14 +03:00
|
|
|
|
(target_x / scale) as f32,
|
|
|
|
|
|
(target_y / scale) as f32,
|
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
|
|
|
|
win_w as f32,
|
|
|
|
|
|
win_h as f32,
|
|
|
|
|
|
));
|
2026-07-31 11:16:14 +03:00
|
|
|
|
let rotation = self
|
|
|
|
|
|
.plot_dialog
|
|
|
|
|
|
.rotation
|
|
|
|
|
|
.trim_end_matches('°')
|
|
|
|
|
|
.parse::<i32>()
|
|
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
|
let (page_w, page_h) = match rotation {
|
|
|
|
|
|
90 | 270 => (sheet_h, sheet_w),
|
|
|
|
|
|
_ => (sheet_w, sheet_h),
|
|
|
|
|
|
};
|
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
|
|
|
|
Some((
|
2026-07-31 11:16:14 +03:00
|
|
|
|
wires,
|
|
|
|
|
|
hatches,
|
|
|
|
|
|
wipeouts,
|
|
|
|
|
|
page_w,
|
|
|
|
|
|
page_h,
|
|
|
|
|
|
offset_x,
|
|
|
|
|
|
offset_y,
|
|
|
|
|
|
rotation,
|
|
|
|
|
|
scale as f32,
|
|
|
|
|
|
clip,
|
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
|
|
|
|
))
|
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 {
|
2026-07-21 09:27:44 +03:00
|
|
|
|
crate::sys::file_dialog()
|
refactor: split four oversized files into submodule trees
Break up the four giant files (~29k lines total) into focused submodule
directories, one concern per file. Pure mechanical moves — behavior is
unchanged.
- app/update.rs (9650) -> app/update/{mod,viewport,command,style,file,
dynamic,dialog,util}. The 384-arm update_inner match stays in mod.rs
with thin delegating arms; the 66 fattest arm bodies move to topic
methods. Arm set preserved exactly (384 -> 384).
- scene/mod.rs (9017) -> scene/{mod,entity,tess,hittest,layout,paper,
mspace,project,selection,modify,group_layer,preview}. The impl Scene
body is split across files via inherent-impl-per-file; struct, ctor,
fields and load-time helpers stay in the root. fn count 123 -> 123.
- app/commands.rs (6082) -> app/commands/* grouped by command family.
The 233-arm dispatch match becomes source-ordered per-family
dispatch_* handlers, preserving first-match precedence (233 -> 233).
- app/view.rs (4686) -> app/view/{mod,overlay,modal,viewcube,controls}.
Largest file drops from 9650 to 3343. Builds clean; app launches and
loads plugins with no panic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:51:29 +03:00
|
|
|
|
.set_title("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,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|