2026-03-31 08:05:17 +03:00
|
|
|
// I/O module — open, save, and export CAD documents.
|
2026-03-22 14:56:27 +03:00
|
|
|
//
|
|
|
|
|
// All file reading/writing goes through acadrust.
|
|
|
|
|
// Default save format: DWG (AC1032 / R2018+).
|
|
|
|
|
|
2026-04-08 12:59:27 +03:00
|
|
|
pub mod obj;
|
2026-03-31 08:05:17 +03:00
|
|
|
pub mod pdf_export;
|
2026-04-06 14:37:23 +03:00
|
|
|
pub mod plot_style;
|
2026-04-08 22:21:50 +03:00
|
|
|
pub mod print_to_printer;
|
2026-04-08 22:23:58 +03:00
|
|
|
pub mod step;
|
2026-04-08 00:46:05 +03:00
|
|
|
pub mod stl;
|
2026-04-08 00:02:38 +03:00
|
|
|
pub mod xref;
|
2026-03-31 08:05:17 +03:00
|
|
|
|
2026-04-26 16:57:23 +03:00
|
|
|
use acadrust::entities::{Dimension, EntityType};
|
2026-03-22 14:56:27 +03:00
|
|
|
use acadrust::io::dwg::DwgReader;
|
|
|
|
|
use acadrust::{CadDocument, DwgWriter, DxfReader, DxfWriter};
|
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
|
|
|
|
|
// ── Open ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// Show a file-open dialog and load the selected DWG or DXF file.
|
|
|
|
|
/// Returns `(filename, path, document)` or an error string.
|
|
|
|
|
pub async fn pick_and_open() -> Result<(String, PathBuf, CadDocument), String> {
|
|
|
|
|
let handle = rfd::AsyncFileDialog::new()
|
|
|
|
|
.set_title("Open CAD file")
|
|
|
|
|
.add_filter("CAD Files", &["dwg", "dxf", "DWG", "DXF"])
|
|
|
|
|
.add_filter("DWG Files", &["dwg", "DWG"])
|
|
|
|
|
.add_filter("DXF Files", &["dxf", "DXF"])
|
|
|
|
|
.add_filter("All Files", &["*"])
|
|
|
|
|
.pick_file()
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let handle = match handle {
|
|
|
|
|
Some(h) => h,
|
|
|
|
|
None => return Err("Cancelled".into()),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let path = handle.path().to_path_buf();
|
|
|
|
|
open_path(path).await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Load a CAD file from a known path (used by recent files).
|
|
|
|
|
pub async fn open_path(path: PathBuf) -> Result<(String, PathBuf, CadDocument), String> {
|
|
|
|
|
let name = path
|
|
|
|
|
.file_name()
|
|
|
|
|
.map(|n| n.to_string_lossy().into_owned())
|
|
|
|
|
.unwrap_or_else(|| "unknown".into());
|
|
|
|
|
let doc = load_file(&path)?;
|
|
|
|
|
Ok((name, path, doc))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Load a DWG or DXF file directly from a path (auto-detect by extension).
|
|
|
|
|
pub fn load_file(path: &Path) -> Result<CadDocument, String> {
|
|
|
|
|
let ext = path
|
|
|
|
|
.extension()
|
|
|
|
|
.map(|e| e.to_string_lossy().to_lowercase())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
match ext.as_str() {
|
|
|
|
|
"dwg" => {
|
|
|
|
|
let mut reader = DwgReader::from_file(path).map_err(|e| e.to_string())?;
|
|
|
|
|
reader.read().map_err(|e| e.to_string())
|
|
|
|
|
}
|
2026-04-26 16:57:23 +03:00
|
|
|
"dxf" => {
|
|
|
|
|
let mut doc = DxfReader::from_file(path)
|
|
|
|
|
.map_err(|e| e.to_string())?
|
|
|
|
|
.read()
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
fix_dxf_dimension_rotations(&mut doc);
|
|
|
|
|
Ok(doc)
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
_ => Err(format!("Unsupported file format: .{ext}")),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Save dialog ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// Show a save-file dialog listing all DWG and DXF version filters.
|
|
|
|
|
/// DWG versions appear first; format is auto-detected from the returned extension.
|
|
|
|
|
pub async fn pick_save_path() -> Option<PathBuf> {
|
|
|
|
|
let dwg_filters: &[(&str, &[&str])] = &[
|
|
|
|
|
("DWG Files (2018)", &["dwg"]),
|
|
|
|
|
("DWG Files (2013)", &["dwg"]),
|
|
|
|
|
("DWG Files (2010)", &["dwg"]),
|
|
|
|
|
("DWG Files (2007)", &["dwg"]),
|
|
|
|
|
("DWG Files (2004)", &["dwg"]),
|
|
|
|
|
("DWG Files (2000)", &["dwg"]),
|
|
|
|
|
("DWG Files (R14)", &["dwg"]),
|
|
|
|
|
("DWG Files (R13)", &["dwg"]),
|
|
|
|
|
];
|
|
|
|
|
let dxf_filters: &[(&str, &[&str])] = &[
|
|
|
|
|
("DXF Files (2018)", &["dxf"]),
|
|
|
|
|
("DXF Files (2013)", &["dxf"]),
|
|
|
|
|
("DXF Files (2010)", &["dxf"]),
|
|
|
|
|
("DXF Files (2007)", &["dxf"]),
|
|
|
|
|
("DXF Files (2004)", &["dxf"]),
|
|
|
|
|
("DXF Files (2000)", &["dxf"]),
|
|
|
|
|
("DXF Files (R14)", &["dxf"]),
|
|
|
|
|
("DXF Files (R13)", &["dxf"]),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
let mut dlg = rfd::AsyncFileDialog::new()
|
|
|
|
|
.set_title("Save As")
|
|
|
|
|
.set_file_name("drawing.dwg");
|
|
|
|
|
for (label, exts) in dwg_filters.iter().chain(dxf_filters.iter()) {
|
|
|
|
|
dlg = dlg.add_filter(*label, *exts);
|
|
|
|
|
}
|
|
|
|
|
dlg.save_file().await.map(|h| h.path().to_path_buf())
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 14:37:23 +03:00
|
|
|
// ── Plot Style Table ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// Show a file-open dialog and load the selected CTB or STB file.
|
|
|
|
|
pub async fn pick_plot_style() -> Option<plot_style::PlotStyleTable> {
|
|
|
|
|
let handle = rfd::AsyncFileDialog::new()
|
|
|
|
|
.set_title("Load Plot Style Table")
|
|
|
|
|
.add_filter("Plot Style Tables", &["ctb", "stb", "CTB", "STB"])
|
|
|
|
|
.add_filter("CTB Files", &["ctb", "CTB"])
|
|
|
|
|
.add_filter("STB Files", &["stb", "STB"])
|
|
|
|
|
.add_filter("All Files", &["*"])
|
|
|
|
|
.pick_file()
|
|
|
|
|
.await?;
|
|
|
|
|
plot_style::PlotStyleTable::load(handle.path()).ok()
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 23:19:08 +03:00
|
|
|
// ── Image file picker ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// Show a file-open dialog for raster images and decode the selected file.
|
|
|
|
|
/// Returns `(path, pixel_width, pixel_height)` or an error string.
|
|
|
|
|
pub async fn pick_image_file() -> Result<(PathBuf, u32, u32), String> {
|
|
|
|
|
let handle = rfd::AsyncFileDialog::new()
|
|
|
|
|
.set_title("Select Image File")
|
|
|
|
|
.add_filter("Images", &["png", "jpg", "jpeg", "bmp", "tiff", "tif"])
|
|
|
|
|
.add_filter("PNG", &["png"])
|
|
|
|
|
.add_filter("JPEG", &["jpg", "jpeg"])
|
|
|
|
|
.add_filter("All Files", &["*"])
|
|
|
|
|
.pick_file()
|
|
|
|
|
.await
|
|
|
|
|
.ok_or_else(|| "Cancelled".to_string())?;
|
|
|
|
|
let path = handle.path().to_path_buf();
|
|
|
|
|
let img = image::open(&path).map_err(|e| e.to_string())?;
|
|
|
|
|
let (w, h) = image::GenericImageView::dimensions(&img);
|
|
|
|
|
Ok((path, w, h))
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 14:56:27 +03:00
|
|
|
// ── Save ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// Save the document to the given path.
|
|
|
|
|
/// Format is auto-detected from the extension (dwg / dxf).
|
|
|
|
|
pub fn save(doc: &CadDocument, path: &Path) -> Result<(), String> {
|
|
|
|
|
let ext = path
|
|
|
|
|
.extension()
|
|
|
|
|
.map(|e| e.to_string_lossy().to_lowercase())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
match ext.as_str() {
|
|
|
|
|
"dxf" => save_dxf(doc, path),
|
|
|
|
|
_ => save_dwg(doc, path),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn save_dwg(doc: &CadDocument, path: &Path) -> Result<(), String> {
|
|
|
|
|
DwgWriter::write_to_file(path, doc).map_err(|e| e.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn save_dxf(doc: &CadDocument, path: &Path) -> Result<(), String> {
|
|
|
|
|
DxfWriter::new(doc)
|
|
|
|
|
.write_to_file(path)
|
|
|
|
|
.map_err(|e| e.to_string())
|
|
|
|
|
}
|
2026-04-26 16:57:23 +03:00
|
|
|
|
|
|
|
|
// ── DXF post-load fixups ──────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// The acadrust DXF reader stores LinearDimension.rotation directly from DXF
|
|
|
|
|
/// group code 50, which is in degrees. DWG and our own dimension-creation code
|
|
|
|
|
/// store it in radians. Convert so tessellation can call cos/sin uniformly.
|
|
|
|
|
fn fix_dxf_dimension_rotations(doc: &mut CadDocument) {
|
|
|
|
|
for entity in doc.entities_mut() {
|
|
|
|
|
if let EntityType::Dimension(Dimension::Linear(d)) = entity {
|
|
|
|
|
d.rotation = d.rotation.to_radians();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|