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
|
|
|
|
//! Small pure helpers split out of `update.rs`.
|
|
|
|
|
|
|
|
|
|
|
|
use crate::scene::Scene;
|
|
|
|
|
|
|
|
|
|
|
|
/// Parse a scale string like "1:50" or "2:1" into (numerator, denominator).
|
|
|
|
|
|
/// Returns (1.0, 1.0) for "Fit" or unknown formats.
|
|
|
|
|
|
/// Sync the model-space annotation scale into the standard CANNOSCALE /
|
|
|
|
|
|
/// CANNOSCALEVALUE header variables before a save, so the scale round-trips
|
|
|
|
|
|
/// through the file (and is read correctly by other CAD applications).
|
|
|
|
|
|
pub(super) fn sync_annotation_scale_header(scene: &mut Scene) {
|
|
|
|
|
|
let anno = scene.annotation_scale;
|
|
|
|
|
|
let value = if anno.abs() > 1e-9 {
|
|
|
|
|
|
1.0 / anno as f64
|
|
|
|
|
|
} else {
|
|
|
|
|
|
1.0
|
|
|
|
|
|
};
|
|
|
|
|
|
// Prefer the name of a matching scale already in the drawing's list;
|
|
|
|
|
|
// fall back to a formatted ratio when none matches.
|
|
|
|
|
|
let name = scene
|
|
|
|
|
|
.scale_list()
|
|
|
|
|
|
.into_iter()
|
|
|
|
|
|
.find(|(_, a, _)| (a - anno).abs() < 0.001 * anno.max(0.001))
|
|
|
|
|
|
.map(|(n, _, _)| n)
|
|
|
|
|
|
.unwrap_or_else(|| format_annotation_scale_name(anno));
|
|
|
|
|
|
let hdr = &mut scene.document.header;
|
|
|
|
|
|
hdr.current_annotation_scale = name;
|
|
|
|
|
|
hdr.annotation_scale_value = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Format an annotation-scale multiplier as a ratio name: 50.0 -> "1:50",
|
|
|
|
|
|
/// 0.5 -> "2:1", 1.0 -> "1:1".
|
|
|
|
|
|
fn format_annotation_scale_name(anno: f32) -> String {
|
|
|
|
|
|
if anno >= 1.0 {
|
|
|
|
|
|
format!("1:{}", anno.round() as i64)
|
|
|
|
|
|
} else if anno > 0.0 {
|
|
|
|
|
|
format!("{}:1", (1.0 / anno).round() as i64)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
"1:1".to_string()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-09 12:25:46 +03:00
|
|
|
|
/// `<none>` / `<previous>` are pseudo-entries, not real page setups.
|
|
|
|
|
|
pub(super) fn is_special_entry(s: &str) -> bool {
|
|
|
|
|
|
s == crate::ui::window::plot::SETUP_NONE || s == crate::ui::window::plot::SETUP_PREV
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// A page-setup list entry wrapped in `*…*` is a layout (its embedded page
|
|
|
|
|
|
/// setup), not a standalone named page setup.
|
|
|
|
|
|
pub(super) fn is_layout_entry(s: &str) -> bool {
|
|
|
|
|
|
s.len() >= 2 && s.starts_with('*') && s.ends_with('*')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// The layout name inside a `*name*` list entry.
|
|
|
|
|
|
pub(super) fn layout_entry_name(s: &str) -> &str {
|
|
|
|
|
|
s.trim_start_matches('*').trim_end_matches('*')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Infer the closest A-series paper label and orientation from sheet
|
|
|
|
|
|
/// dimensions (mm). Falls back to A4 when nothing is close.
|
|
|
|
|
|
pub(super) fn paper_label_from_dims(w: f64, h: f64) -> (String, String) {
|
|
|
|
|
|
use crate::io::paper_sizes::PaperSize;
|
|
|
|
|
|
let orient = if w >= h { "Landscape" } else { "Portrait" };
|
|
|
|
|
|
let (short, long) = if w <= h { (w, h) } else { (h, w) };
|
|
|
|
|
|
let mut best = ("A4".to_string(), f64::INFINITY);
|
|
|
|
|
|
for p in PaperSize::ALL {
|
|
|
|
|
|
let (pw, ph) = p.dimensions_mm(); // portrait: pw < ph
|
|
|
|
|
|
let err = (pw - short).abs() + (ph - long).abs();
|
|
|
|
|
|
if err < best.1 {
|
|
|
|
|
|
best = (p.label().to_string(), err);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
(best.0, orient.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
|
|
|
|
pub(super) fn parse_plot_scale(s: &str) -> (f64, f64) {
|
|
|
|
|
|
if s == "Fit" {
|
|
|
|
|
|
return (1.0, 1.0);
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some((a, b)) = s.split_once(':') {
|
|
|
|
|
|
let num: f64 = a.trim().parse().unwrap_or(1.0);
|
|
|
|
|
|
let den: f64 = b.trim().parse().unwrap_or(1.0);
|
|
|
|
|
|
if den > 0.0 {
|
|
|
|
|
|
return (num, den);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
(1.0, 1.0)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Convert an internal `[r,g,b,a]` colour (0.0–1.0) to a persisted 0–255 RGB
|
|
|
|
|
|
/// triplet, dropping alpha (backgrounds are always opaque).
|
|
|
|
|
|
pub(super) fn f4_to_u3([r, g, b, _]: [f32; 4]) -> [u8; 3] {
|
|
|
|
|
|
[
|
|
|
|
|
|
(r * 255.0).round().clamp(0.0, 255.0) as u8,
|
|
|
|
|
|
(g * 255.0).round().clamp(0.0, 255.0) as u8,
|
|
|
|
|
|
(b * 255.0).round().clamp(0.0, 255.0) as u8,
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Inverse of [`f4_to_u3`]: a persisted 0–255 RGB triplet back to an opaque
|
|
|
|
|
|
/// `[r,g,b,a]` colour.
|
|
|
|
|
|
pub(super) fn u3_to_f4([r, g, b]: [u8; 3]) -> [f32; 4] {
|
|
|
|
|
|
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0]
|
|
|
|
|
|
}
|
|
|
|
|
|
|