Files
OneCAD-Draft/src/app/update/util.rs
T
Hakan SevenandClaude Opus 4.8 a1ee4d9269 feat(plot): page setup manager — named setups, layouts, <none>/<previous>
Restructure the plot dialog into a style-manager-style master/detail: a
left list of page setups plus a right settings editor. The list shows
<none> (defaults + PDF) and <previous> (the settings the dialog opened
with), then the drawing's layouts (*Model*, *Layout1*, …) and its
standalone named page setups.

- Named page setups live in the ACAD_PLOTSETTINGS dictionary
  (src/scene/page_setup.rs: names/get/save/delete/rename, creating the
  dictionary if absent); DWG round-trip verified.
- Toolbar New / Copy / Delete | Set current / Preview / Print. Selecting
  a row loads it into the editor; Set current writes to the active
  layout. Copy duplicates a layout or a setup into a new named setup;
  Delete/Rename apply only to named setups.
- Scene::plot_settings_for(name) generalizes effective_plot_settings so
  any layout's settings can be loaded; on open the settings are
  snapshotted for <previous>.
- Default selection: model space → <none>; a layout → its own setup.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 12:25:46 +03:00

105 lines
3.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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()
}
}
/// `<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())
}
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]
}