feat(io): .bak backup-on-save + open .bak as a supported type (#205)
- Save: overwriting an existing file first copies it to a sibling <name>.bak (default on; toggle with the ISAVEBAK command, persisted in settings). - Open: .bak is a first-class type — the Open dialog lists it and the loader detects DWG vs DXF from the file's leading magic bytes (the extension hides the real format). - File association registers .bak too, every launch (so users who associated earlier get it automatically): Windows via the registry; Linux via a high-priority shared-mime-info magic that resolves DWG/DXF content under the .bak extension, with the generic *.bak glob claimed only once the app is the default DWG handler — so it never hijacks other apps' backups. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b77187e672
commit
962268e16e
7 changed files with 148 additions and 4 deletions
|
|
@ -1449,6 +1449,25 @@ impl OpenCADStudio {
|
||||||
.push_error("Requires 0 OR 1 OR MULTIPLE OR SINGLE");
|
.push_error("Requires 0 OR 1 OR MULTIPLE OR SINGLE");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"ISAVEBAK" => {
|
||||||
|
let v = if self.backup_on_save { 1 } else { 0 };
|
||||||
|
self.command_line.push_output(&format!("ISAVEBAK = {v}"));
|
||||||
|
}
|
||||||
|
cmd if cmd.starts_with("ISAVEBAK ") => {
|
||||||
|
match cmd.trim_start_matches("ISAVEBAK").trim() {
|
||||||
|
"0" => {
|
||||||
|
self.backup_on_save = false;
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
self.command_line.push_output("ISAVEBAK set to 0");
|
||||||
|
}
|
||||||
|
"1" => {
|
||||||
|
self.backup_on_save = true;
|
||||||
|
self.persist_settings_if_changed();
|
||||||
|
self.command_line.push_output("ISAVEBAK set to 1");
|
||||||
|
}
|
||||||
|
_ => self.command_line.push_error("Requires 0 or 1"),
|
||||||
|
}
|
||||||
|
}
|
||||||
"PDSIZE" => {
|
"PDSIZE" => {
|
||||||
use crate::command::ValuePromptCommand;
|
use crate::command::ValuePromptCommand;
|
||||||
let c = ValuePromptCommand::new(
|
let c = ValuePromptCommand::new(
|
||||||
|
|
|
||||||
|
|
@ -247,6 +247,9 @@ pub(super) struct OpenCADStudio {
|
||||||
dyn_input: bool,
|
dyn_input: bool,
|
||||||
/// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
|
/// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
|
||||||
pub texteditmode: bool,
|
pub texteditmode: bool,
|
||||||
|
/// When true (default), saving over an existing file first writes a `.bak`
|
||||||
|
/// copy of it for recovery (#205). Toggle with the ISAVEBAK command.
|
||||||
|
pub backup_on_save: bool,
|
||||||
/// Persisted default viewport background, restored from settings and applied
|
/// Persisted default viewport background, restored from settings and applied
|
||||||
/// to every drawing tab (new and opened) so a chosen background survives
|
/// to every drawing tab (new and opened) so a chosen background survives
|
||||||
/// restarts (#188). `None` = the built-in dark-grey / off-white defaults.
|
/// restarts (#188). `None` = the built-in dark-grey / off-white defaults.
|
||||||
|
|
@ -1862,6 +1865,7 @@ impl OpenCADStudio {
|
||||||
show_grid: false,
|
show_grid: false,
|
||||||
dyn_input: true,
|
dyn_input: true,
|
||||||
texteditmode: false,
|
texteditmode: false,
|
||||||
|
backup_on_save: true,
|
||||||
default_bg_color: None,
|
default_bg_color: None,
|
||||||
default_paper_bg_color: None,
|
default_paper_bg_color: None,
|
||||||
awaiting_vports: false,
|
awaiting_vports: false,
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,9 @@ pub struct UserSettings {
|
||||||
pub plugin_repos: Vec<String>,
|
pub plugin_repos: Vec<String>,
|
||||||
/// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
|
/// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
|
||||||
pub texteditmode: bool,
|
pub texteditmode: bool,
|
||||||
|
/// When true, saving over an existing file first copies it to a sibling
|
||||||
|
/// `<name>.bak` so a faulty or accidental save can be recovered (#205).
|
||||||
|
pub backup_on_save: bool,
|
||||||
/// Persisted viewport background colours (0–255 RGB); `None` = app default
|
/// Persisted viewport background colours (0–255 RGB); `None` = app default
|
||||||
/// (dark grey model / off-white paper). Applied to every drawing tab on
|
/// (dark grey model / off-white paper). Applied to every drawing tab on
|
||||||
/// launch and to tabs opened later, so a chosen background survives restarts
|
/// launch and to tabs opened later, so a chosen background survives restarts
|
||||||
|
|
@ -133,6 +136,7 @@ impl Default for UserSettings {
|
||||||
disabled_plugins: Vec::new(),
|
disabled_plugins: Vec::new(),
|
||||||
plugin_repos: Vec::new(),
|
plugin_repos: Vec::new(),
|
||||||
texteditmode: false,
|
texteditmode: false,
|
||||||
|
backup_on_save: true,
|
||||||
bg_color: None,
|
bg_color: None,
|
||||||
paper_bg_color: None,
|
paper_bg_color: None,
|
||||||
}
|
}
|
||||||
|
|
@ -177,6 +181,7 @@ impl UserSettings {
|
||||||
s.texteditmode = v;
|
s.texteditmode = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"backup_on_save" => s.backup_on_save = val == "1",
|
||||||
"disabled_plugins" => {
|
"disabled_plugins" => {
|
||||||
s.disabled_plugins = val
|
s.disabled_plugins = val
|
||||||
.split(',')
|
.split(',')
|
||||||
|
|
@ -218,7 +223,7 @@ impl UserSettings {
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(",");
|
.join(",");
|
||||||
let body = format!(
|
let body = format!(
|
||||||
"dyn={}\northo={}\npolar={}\npolar_increment_deg={}\nosnap={}\notrack={}\ndefault_assoc_prompted={}\nsnap_modes={}\ndisabled_plugins={}\nplugin_repos={}\ntexteditmode={}\nbg_color={}\npaper_bg_color={}\n",
|
"dyn={}\northo={}\npolar={}\npolar_increment_deg={}\nosnap={}\notrack={}\ndefault_assoc_prompted={}\nsnap_modes={}\ndisabled_plugins={}\nplugin_repos={}\ntexteditmode={}\nbackup_on_save={}\nbg_color={}\npaper_bg_color={}\n",
|
||||||
b(self.dyn_input),
|
b(self.dyn_input),
|
||||||
b(self.ortho),
|
b(self.ortho),
|
||||||
b(self.polar),
|
b(self.polar),
|
||||||
|
|
@ -230,6 +235,7 @@ impl UserSettings {
|
||||||
self.disabled_plugins.join(","),
|
self.disabled_plugins.join(","),
|
||||||
self.plugin_repos.join(","),
|
self.plugin_repos.join(","),
|
||||||
self.texteditmode,
|
self.texteditmode,
|
||||||
|
b(self.backup_on_save),
|
||||||
rgb_to_str(self.bg_color),
|
rgb_to_str(self.bg_color),
|
||||||
rgb_to_str(self.paper_bg_color),
|
rgb_to_str(self.paper_bg_color),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ impl OpenCADStudio {
|
||||||
},
|
},
|
||||||
plugin_repos: self.plugin_repos.clone(),
|
plugin_repos: self.plugin_repos.clone(),
|
||||||
texteditmode: self.texteditmode,
|
texteditmode: self.texteditmode,
|
||||||
|
backup_on_save: self.backup_on_save,
|
||||||
bg_color: self.default_bg_color.map(f4_to_u3),
|
bg_color: self.default_bg_color.map(f4_to_u3),
|
||||||
paper_bg_color: self.default_paper_bg_color.map(f4_to_u3),
|
paper_bg_color: self.default_paper_bg_color.map(f4_to_u3),
|
||||||
}
|
}
|
||||||
|
|
@ -58,6 +59,7 @@ impl OpenCADStudio {
|
||||||
self.disabled_plugins = s.disabled_plugins.iter().cloned().collect();
|
self.disabled_plugins = s.disabled_plugins.iter().cloned().collect();
|
||||||
self.plugin_repos = s.plugin_repos.clone();
|
self.plugin_repos = s.plugin_repos.clone();
|
||||||
self.texteditmode = s.texteditmode;
|
self.texteditmode = s.texteditmode;
|
||||||
|
self.backup_on_save = s.backup_on_save;
|
||||||
self.default_bg_color = s.bg_color.map(u3_to_f4);
|
self.default_bg_color = s.bg_color.map(u3_to_f4);
|
||||||
self.default_paper_bg_color = s.paper_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
|
// Push the restored background onto every drawing tab that exists now
|
||||||
|
|
@ -536,6 +538,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
self.tabs[i].scene.document.header.user_real1 =
|
self.tabs[i].scene.document.header.user_real1 =
|
||||||
self.tabs[i].scene.annotation_scale as f64;
|
self.tabs[i].scene.annotation_scale as f64;
|
||||||
// A direct Save preserves the document's current version.
|
// A direct Save preserves the document's current version.
|
||||||
|
if self.backup_on_save {
|
||||||
|
crate::io::write_backup(&path);
|
||||||
|
}
|
||||||
match crate::io::save(&self.tabs[i].scene.document, &path) {
|
match crate::io::save(&self.tabs[i].scene.document, &path) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.command_line
|
self.command_line
|
||||||
|
|
@ -597,6 +602,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
{
|
{
|
||||||
let path = self.save_dialog_folder.join(&filename);
|
let path = self.save_dialog_folder.join(&filename);
|
||||||
|
if self.backup_on_save {
|
||||||
|
crate::io::write_backup(&path);
|
||||||
|
}
|
||||||
match crate::io::save_as_version(&self.tabs[i].scene.document, &path, version) {
|
match crate::io::save_as_version(&self.tabs[i].scene.document, &path, version) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.command_line
|
self.command_line
|
||||||
|
|
|
||||||
|
|
@ -315,9 +315,10 @@ impl OpenCADStudio {
|
||||||
async {
|
async {
|
||||||
let handle = rfd::AsyncFileDialog::new()
|
let handle = rfd::AsyncFileDialog::new()
|
||||||
.set_title("Select External Reference File")
|
.set_title("Select External Reference File")
|
||||||
.add_filter("CAD Files", &["dwg", "dxf", "DWG", "DXF"])
|
.add_filter("CAD Files", &["dwg", "dxf", "bak", "DWG", "DXF", "BAK"])
|
||||||
.add_filter("DWG Files", &["dwg", "DWG"])
|
.add_filter("DWG Files", &["dwg", "DWG"])
|
||||||
.add_filter("DXF Files", &["dxf", "DXF"])
|
.add_filter("DXF Files", &["dxf", "DXF"])
|
||||||
|
.add_filter("Backup Files", &["bak", "BAK"])
|
||||||
.pick_file()
|
.pick_file()
|
||||||
.await;
|
.await;
|
||||||
match handle {
|
match handle {
|
||||||
|
|
|
||||||
|
|
@ -272,12 +272,15 @@ mod windows_impl {
|
||||||
// appear in the "Open with → Choose another app" picker for them.
|
// appear in the "Open with → Choose another app" picker for them.
|
||||||
set_string(&format!(r"{APP_BASE}\SupportedTypes"), Some(".dwg"), "")?;
|
set_string(&format!(r"{APP_BASE}\SupportedTypes"), Some(".dwg"), "")?;
|
||||||
set_string(&format!(r"{APP_BASE}\SupportedTypes"), Some(".dxf"), "")?;
|
set_string(&format!(r"{APP_BASE}\SupportedTypes"), Some(".dxf"), "")?;
|
||||||
|
// `.bak` copies hold verbatim DWG/DXF content and open the same way.
|
||||||
|
set_string(&format!(r"{APP_BASE}\SupportedTypes"), Some(".bak"), "")?;
|
||||||
|
|
||||||
// ── ProgIDs (per-user mirror of the MSI's) ──────────────────────────
|
// ── ProgIDs (per-user mirror of the MSI's) ──────────────────────────
|
||||||
// The Capabilities entries below point at these, and they must resolve
|
// The Capabilities entries below point at these, and they must resolve
|
||||||
// to a real open command for "set as default" to apply them.
|
// to a real open command for "set as default" to apply them.
|
||||||
register_progid(&exe, "OpenCADStudio.DWG", "DWG Drawing")?;
|
register_progid(&exe, "OpenCADStudio.DWG", "DWG Drawing")?;
|
||||||
register_progid(&exe, "OpenCADStudio.DXF", "DXF Drawing")?;
|
register_progid(&exe, "OpenCADStudio.DXF", "DXF Drawing")?;
|
||||||
|
register_progid(&exe, "OpenCADStudio.BAK", "CAD Backup")?;
|
||||||
// Also offer the ProgIDs in each extension's Open-with list.
|
// Also offer the ProgIDs in each extension's Open-with list.
|
||||||
set_string(
|
set_string(
|
||||||
r"Software\Classes\.dwg\OpenWithProgids",
|
r"Software\Classes\.dwg\OpenWithProgids",
|
||||||
|
|
@ -289,6 +292,11 @@ mod windows_impl {
|
||||||
Some("OpenCADStudio.DXF"),
|
Some("OpenCADStudio.DXF"),
|
||||||
"",
|
"",
|
||||||
)?;
|
)?;
|
||||||
|
set_string(
|
||||||
|
r"Software\Classes\.bak\OpenWithProgids",
|
||||||
|
Some("OpenCADStudio.BAK"),
|
||||||
|
"",
|
||||||
|
)?;
|
||||||
|
|
||||||
// ── Capabilities + RegisteredApplications ───────────────────────────
|
// ── Capabilities + RegisteredApplications ───────────────────────────
|
||||||
// Mirrors the MSI's DefaultPrograms component, but per-user, so the
|
// Mirrors the MSI's DefaultPrograms component, but per-user, so the
|
||||||
|
|
@ -304,6 +312,7 @@ mod windows_impl {
|
||||||
)?;
|
)?;
|
||||||
set_string(&format!(r"{CAP}\FileAssociations"), Some(".dwg"), "OpenCADStudio.DWG")?;
|
set_string(&format!(r"{CAP}\FileAssociations"), Some(".dwg"), "OpenCADStudio.DWG")?;
|
||||||
set_string(&format!(r"{CAP}\FileAssociations"), Some(".dxf"), "OpenCADStudio.DXF")?;
|
set_string(&format!(r"{CAP}\FileAssociations"), Some(".dxf"), "OpenCADStudio.DXF")?;
|
||||||
|
set_string(&format!(r"{CAP}\FileAssociations"), Some(".bak"), "OpenCADStudio.BAK")?;
|
||||||
set_string(
|
set_string(
|
||||||
r"Software\RegisteredApplications",
|
r"Software\RegisteredApplications",
|
||||||
Some("Open CAD Studio"),
|
Some("Open CAD Studio"),
|
||||||
|
|
@ -325,6 +334,11 @@ mod linux_impl {
|
||||||
/// Skipped when a system package already registered the app, so we don't
|
/// Skipped when a system package already registered the app, so we don't
|
||||||
/// shadow a distro-provided desktop entry.
|
/// shadow a distro-provided desktop entry.
|
||||||
pub(super) fn register_handler() -> Result<(), String> {
|
pub(super) fn register_handler() -> Result<(), String> {
|
||||||
|
// High-priority CAD magic so DWG/DXF content (including `.bak` copies)
|
||||||
|
// resolves to image/vnd.dwg / image/vnd.dxf even though the freedesktop
|
||||||
|
// `*.bak` glob would otherwise tag it application/x-trash (#205).
|
||||||
|
install_cad_mime();
|
||||||
|
|
||||||
// A system install already lists us — leave it alone.
|
// A system install already lists us — leave it alone.
|
||||||
let data_dirs = std::env::var("XDG_DATA_DIRS")
|
let data_dirs = std::env::var("XDG_DATA_DIRS")
|
||||||
.unwrap_or_else(|_| "/usr/local/share:/usr/share".to_string());
|
.unwrap_or_else(|_| "/usr/local/share:/usr/share".to_string());
|
||||||
|
|
@ -381,6 +395,65 @@ mod linux_impl {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Install a high-priority shared-mime-info magic rule so DWG/DXF *content*
|
||||||
|
/// is recognised regardless of extension. Without it a `.bak` copy matches
|
||||||
|
/// the freedesktop `*.bak` glob (application/x-trash) and never reaches us;
|
||||||
|
/// the magic (priority 90) beats that glob so DWG/DXF backups open with the
|
||||||
|
/// app. Best-effort + idempotent (skips the slow DB refresh when unchanged).
|
||||||
|
fn install_cad_mime() {
|
||||||
|
let Ok(home) = data_home() else { return };
|
||||||
|
let pkgs = home.join("mime/packages");
|
||||||
|
let xml_path = pkgs.join(format!("{APP_ID}-cad.xml"));
|
||||||
|
// The `*.bak` glob is what makes file managers (GIO/Nautilus key off the
|
||||||
|
// extension, not content) open DWG/DXF backups with us — but it would
|
||||||
|
// mislabel every OTHER app's `.bak` files too. So only claim it once the
|
||||||
|
// user has actually made us the default for DWG (i.e. granted the file
|
||||||
|
// association). The content magic below is always safe.
|
||||||
|
let bak_glob = if is_default_for_dwg() {
|
||||||
|
" <glob pattern=\"*.bak\" weight=\"60\"/>\n"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
let xml = format!(
|
||||||
|
r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
|
||||||
|
<mime-type type="image/vnd.dwg">
|
||||||
|
{bak_glob} <magic priority="100"><match value="AC10" type="string" offset="0"/></magic>
|
||||||
|
</mime-type>
|
||||||
|
<mime-type type="image/vnd.dxf">
|
||||||
|
{bak_glob} <magic priority="100"><match value="AutoCAD Binary DXF" type="string" offset="0"/></magic>
|
||||||
|
</mime-type>
|
||||||
|
</mime-info>
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
if std::fs::read_to_string(&xml_path).ok().as_deref() == Some(xml.as_str()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if std::fs::create_dir_all(&pkgs).is_ok() && std::fs::write(&xml_path, &xml).is_ok() {
|
||||||
|
let _ = std::process::Command::new("update-mime-database")
|
||||||
|
.arg(home.join("mime"))
|
||||||
|
.status();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the user has already made us the default app for DWG drawings
|
||||||
|
/// (i.e. granted the file association). Used to decide whether to claim the
|
||||||
|
/// generic `*.bak` extension for DWG/DXF backups (#205).
|
||||||
|
fn is_default_for_dwg() -> bool {
|
||||||
|
let cfg = std::env::var_os("XDG_CONFIG_HOME")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")));
|
||||||
|
let Some(cfg) = cfg else { return false };
|
||||||
|
let Ok(body) = std::fs::read_to_string(cfg.join("mimeapps.list")) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let target = format!("{APP_ID}.desktop");
|
||||||
|
body.lines().any(|l| {
|
||||||
|
let l = l.trim();
|
||||||
|
l.starts_with("image/vnd.dwg=") && l.contains(&target)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Write the app SVG icon into the user's hicolor icon theme so that the
|
/// Write the app SVG icon into the user's hicolor icon theme so that the
|
||||||
/// named `Icon={APP_ID}` entry in the .desktop file resolves correctly in
|
/// named `Icon={APP_ID}` entry in the .desktop file resolves correctly in
|
||||||
/// file managers and the "Open with" picker. Skipped when the icon is
|
/// file managers and the "Open with" picker. Skipped when the icon is
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,10 @@ const PHASE_FINALIZING: u8 = 3;
|
||||||
pub async fn pick_open_path() -> Option<(PathBuf, u64)> {
|
pub async fn pick_open_path() -> Option<(PathBuf, u64)> {
|
||||||
let handle = rfd::AsyncFileDialog::new()
|
let handle = rfd::AsyncFileDialog::new()
|
||||||
.set_title("Open CAD file")
|
.set_title("Open CAD file")
|
||||||
.add_filter("CAD Files", &["dwg", "dxf", "DWG", "DXF"])
|
.add_filter("CAD Files", &["dwg", "dxf", "bak", "DWG", "DXF", "BAK"])
|
||||||
.add_filter("DWG Files", &["dwg", "DWG"])
|
.add_filter("DWG Files", &["dwg", "DWG"])
|
||||||
.add_filter("DXF Files", &["dxf", "DXF"])
|
.add_filter("DXF Files", &["dxf", "DXF"])
|
||||||
|
.add_filter("Backup Files", &["bak", "BAK"])
|
||||||
.add_filter("All Files", &["*"])
|
.add_filter("All Files", &["*"])
|
||||||
.pick_file()
|
.pick_file()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -147,13 +148,36 @@ pub fn load_bytes(name: &str, bytes: Vec<u8>) -> Result<CadDocument, String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load a DWG or DXF file directly from a path (auto-detect by extension).
|
/// Load a DWG or DXF file directly from a path (auto-detect by extension).
|
||||||
|
/// Peek at a file's leading bytes to tell a DWG (version tag "AC10xx") from a
|
||||||
|
/// DXF. Used for `.bak` copies, whose extension hides the real format.
|
||||||
|
fn sniff_dwg_or_dxf(path: &Path) -> String {
|
||||||
|
use std::io::Read;
|
||||||
|
let mut buf = [0u8; 6];
|
||||||
|
if let Ok(mut f) = std::fs::File::open(path) {
|
||||||
|
let _ = f.read(&mut buf);
|
||||||
|
}
|
||||||
|
if buf.starts_with(b"AC10") {
|
||||||
|
"dwg".to_string()
|
||||||
|
} else {
|
||||||
|
"dxf".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load_file(path: &Path) -> Result<CadDocument, String> {
|
pub fn load_file(path: &Path) -> Result<CadDocument, String> {
|
||||||
let ext = path
|
let ext = path
|
||||||
.extension()
|
.extension()
|
||||||
.map(|e| e.to_string_lossy().to_lowercase())
|
.map(|e| e.to_string_lossy().to_lowercase())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
match ext.as_str() {
|
// A `.bak` backup holds a verbatim DWG or DXF copy — detect the real format
|
||||||
|
// from the file's leading bytes rather than the `.bak` extension.
|
||||||
|
let effective = if ext == "bak" {
|
||||||
|
sniff_dwg_or_dxf(path)
|
||||||
|
} else {
|
||||||
|
ext.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
match effective.as_str() {
|
||||||
"dwg" => {
|
"dwg" => {
|
||||||
let mut doc = DwgReader::from_file(path)
|
let mut doc = DwgReader::from_file(path)
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
|
|
@ -473,6 +497,15 @@ pub fn dropped_on_save_count(
|
||||||
n
|
n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Before overwriting `path`, copy the existing file to a sibling `<name>.bak`
|
||||||
|
/// so a faulty or accidental save can be recovered (#205). Best-effort: a
|
||||||
|
/// failed backup never blocks the save itself.
|
||||||
|
pub fn write_backup(path: &std::path::Path) {
|
||||||
|
if path.exists() {
|
||||||
|
let _ = std::fs::copy(path, path.with_extension("bak"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Plot Style Table ──────────────────────────────────────────────────────
|
// ── Plot Style Table ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Show a file-open dialog and load the selected CTB or STB file.
|
/// Show a file-open dialog and load the selected CTB or STB file.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue