forked from 1b-agent/opencadstudio-upstream
style: apply cargo fmt across workspace
This commit is contained in:
1 parent
85710e5454
commit
9cf9ff2c5c
531 files changed
+57740
-33906
No files matched your search
+1
-1
@@ -97,7 +97,7 @@ pub(crate) fn path_matches_file(path: &Path, file: &File) -> std::io::Result<boo
|
||||
fn windows_file_identity(file: &File) -> std::io::Result<(u32, u64)> {
|
||||
use std::os::windows::io::AsRawHandle;
|
||||
use windows_sys::Win32::Storage::FileSystem::{
|
||||
BY_HANDLE_FILE_INFORMATION, GetFileInformationByHandle,
|
||||
GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION,
|
||||
};
|
||||
|
||||
let mut info = BY_HANDLE_FILE_INFORMATION::default();
|
||||
|
||||
@@ -269,7 +269,10 @@ mod windows_impl {
|
||||
}
|
||||
|
||||
fn wide(s: &str) -> Vec<u16> {
|
||||
std::ffi::OsStr::new(s).encode_wide().chain(Some(0)).collect()
|
||||
std::ffi::OsStr::new(s)
|
||||
.encode_wide()
|
||||
.chain(Some(0))
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Create `subkey` under HKCU and write `data` into `value` (None = the key's
|
||||
@@ -298,9 +301,8 @@ mod windows_impl {
|
||||
let data_w = wide(data);
|
||||
// cbData counts bytes including the trailing NUL that `wide` appended.
|
||||
let cb = (data_w.len() * std::mem::size_of::<u16>()) as u32;
|
||||
let rc = unsafe {
|
||||
RegSetValueExW(hkey, name_ptr, 0, REG_SZ, data_w.as_ptr() as *const u8, cb)
|
||||
};
|
||||
let rc =
|
||||
unsafe { RegSetValueExW(hkey, name_ptr, 0, REG_SZ, data_w.as_ptr() as *const u8, cb) };
|
||||
unsafe { RegCloseKey(hkey) };
|
||||
if rc != 0 {
|
||||
return Err(format!("RegSetValueExW failed ({rc}) for {subkey}"));
|
||||
@@ -348,7 +350,11 @@ mod windows_impl {
|
||||
fn register_progid(exe: &str, progid: &str, description: &str) -> Result<(), String> {
|
||||
let base = format!(r"Software\Classes\{progid}");
|
||||
set_string(&base, None, description)?;
|
||||
set_string(&format!(r"{base}\DefaultIcon"), None, &format!("\"{exe}\",0"))?;
|
||||
set_string(
|
||||
&format!(r"{base}\DefaultIcon"),
|
||||
None,
|
||||
&format!("\"{exe}\",0"),
|
||||
)?;
|
||||
set_string(
|
||||
&format!(r"{base}\shell\open\command"),
|
||||
None,
|
||||
@@ -377,7 +383,11 @@ mod windows_impl {
|
||||
set_string(APP_BASE, Some("FriendlyAppName"), "Open CAD Studio")?;
|
||||
// DefaultIcon is what Windows uses to show the app icon in the
|
||||
// "Open with" context-menu list and the "Choose another app" picker.
|
||||
set_string(&format!(r"{APP_BASE}\DefaultIcon"), None, &format!("\"{exe}\",0"))?;
|
||||
set_string(
|
||||
&format!(r"{APP_BASE}\DefaultIcon"),
|
||||
None,
|
||||
&format!("\"{exe}\",0"),
|
||||
)?;
|
||||
// Listing the extensions under SupportedTypes is what makes the app
|
||||
// appear in the "Open with → Choose another app" picker for them.
|
||||
set_string(&format!(r"{APP_BASE}\SupportedTypes"), Some(".dwg"), "")?;
|
||||
@@ -420,9 +430,21 @@ mod windows_impl {
|
||||
Some("ApplicationDescription"),
|
||||
"2D/3D CAD application for DWG and DXF drawings.",
|
||||
)?;
|
||||
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(".bak"), "OpenCADStudio.BAK")?;
|
||||
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(".bak"),
|
||||
"OpenCADStudio.BAK",
|
||||
)?;
|
||||
set_string(
|
||||
r"Software\RegisteredApplications",
|
||||
Some("Open CAD Studio"),
|
||||
|
||||
+12
-8
@@ -14,8 +14,7 @@ use codec::CadDocument;
|
||||
|
||||
/// The community folder's GitHub contents API (lists name + download URL).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const REPO_API_URL: &str =
|
||||
"https://api.github.com/repos/HakanSeven12/OpenCADStudio/contents/fonts";
|
||||
const REPO_API_URL: &str = "https://api.github.com/repos/HakanSeven12/OpenCADStudio/contents/fonts";
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const MAX_FONT_BYTES: u64 = 16 * 1024 * 1024;
|
||||
|
||||
@@ -126,7 +125,10 @@ fn fetch_repo_fonts() -> Result<Vec<RepoFont>, String> {
|
||||
let agent = crate::network::agent(std::time::Duration::from_secs(15));
|
||||
let mut response = agent
|
||||
.get(REPO_API_URL)
|
||||
.header("User-Agent", concat!("OpenCADStudio/", env!("OCS_APP_VERSION")))
|
||||
.header(
|
||||
"User-Agent",
|
||||
concat!("OpenCADStudio/", env!("OCS_APP_VERSION")),
|
||||
)
|
||||
.header("Accept", "application/vnd.github+json")
|
||||
.call()
|
||||
.map_err(|e| format!("Could not reach the font repository: {e}"))?;
|
||||
@@ -175,7 +177,8 @@ pub fn download_fonts(
|
||||
FontSource::Custom(_) => None,
|
||||
};
|
||||
let dir = fonts_dir().ok_or_else(|| "Could not resolve the fonts folder.".to_string())?;
|
||||
std::fs::create_dir_all(&dir).map_err(|e| format!("Could not create {}: {e}", dir.display()))?;
|
||||
std::fs::create_dir_all(&dir)
|
||||
.map_err(|e| format!("Could not create {}: {e}", dir.display()))?;
|
||||
let mut downloaded = Vec::new();
|
||||
for want in missing {
|
||||
let file = want.rsplit(['/', '\\']).next().unwrap_or(want);
|
||||
@@ -189,16 +192,17 @@ pub fn download_fonts(
|
||||
}
|
||||
// Custom folder: the font is simply `{base}/{file_name}`; a miss
|
||||
// (404) is skipped without failing the batch.
|
||||
(FontSource::Custom(base), _) => {
|
||||
Some(format!("{base}/{}", percent_encode_path(file)))
|
||||
}
|
||||
(FontSource::Custom(base), _) => Some(format!("{base}/{}", percent_encode_path(file))),
|
||||
(FontSource::Community, None) => None,
|
||||
};
|
||||
let Some(url) = url else { continue };
|
||||
let agent = crate::network::agent(std::time::Duration::from_secs(30));
|
||||
let mut response = agent
|
||||
.get(&url)
|
||||
.header("User-Agent", concat!("OpenCADStudio/", env!("OCS_APP_VERSION")))
|
||||
.header(
|
||||
"User-Agent",
|
||||
concat!("OpenCADStudio/", env!("OCS_APP_VERSION")),
|
||||
)
|
||||
.call()
|
||||
.map_err(|e| format!("Could not download {file}: {e}"))?;
|
||||
if response.status() != 200 {
|
||||
|
||||
+38
-18
@@ -120,10 +120,9 @@ pub fn document_lt_segments(document: &CadDocument, name: &str) -> Option<Comple
|
||||
.find(|s| s.handle == c.style_handle)
|
||||
.map(|s| s.name.as_str())
|
||||
.unwrap_or("");
|
||||
let font = crate::entities::text_support::resolve_text_style(
|
||||
style_name, document,
|
||||
)
|
||||
.font_name;
|
||||
let font =
|
||||
crate::entities::text_support::resolve_text_style(style_name, document)
|
||||
.font_name;
|
||||
let scale = c.scale as f32;
|
||||
segments.push(LtSegment::Text {
|
||||
text: text.clone(),
|
||||
@@ -275,16 +274,17 @@ pub fn populate_document_from_source(doc: &mut CadDocument, source: &str) -> usi
|
||||
for mut lt in parse(source) {
|
||||
let segments = complex.get(<.name.to_ascii_uppercase());
|
||||
if let Some(existing) = doc.line_types.get(<.name) {
|
||||
let stripped = segments.is_some()
|
||||
&& !existing.is_complex()
|
||||
&& existing.elements.len() == lt.elements.len()
|
||||
&& existing
|
||||
.elements
|
||||
.iter()
|
||||
.zip(<.elements)
|
||||
.all(|(a, b)| (a.length - b.length).abs() <= 1e-9 * b.length.abs().max(1.0));
|
||||
let stripped =
|
||||
segments.is_some()
|
||||
&& !existing.is_complex()
|
||||
&& existing.elements.len() == lt.elements.len()
|
||||
&& existing.elements.iter().zip(<.elements).all(|(a, b)| {
|
||||
(a.length - b.length).abs() <= 1e-9 * b.length.abs().max(1.0)
|
||||
});
|
||||
if stripped {
|
||||
if let Some(elements) = segments.and_then(|s| complex_elements(doc, s, <.elements)) {
|
||||
if let Some(elements) =
|
||||
segments.and_then(|s| complex_elements(doc, s, <.elements))
|
||||
{
|
||||
if let Some(existing) = doc.line_types.get_mut(<.name) {
|
||||
existing.elements = elements;
|
||||
}
|
||||
@@ -338,7 +338,11 @@ fn shape_file_stem(file: &str) -> String {
|
||||
/// reads as `ltypeshp`).
|
||||
fn bundled_shape_name(file: &str, number: i16) -> Option<&'static str> {
|
||||
let stem = shape_file_stem(file);
|
||||
let stem = if stem.is_empty() { "ltypeshp".to_string() } else { stem };
|
||||
let stem = if stem.is_empty() {
|
||||
"ltypeshp".to_string()
|
||||
} else {
|
||||
stem
|
||||
};
|
||||
BUNDLED_SHAPES
|
||||
.iter()
|
||||
.find(|(file, _, n)| *file == stem && *n == number)
|
||||
@@ -392,14 +396,24 @@ fn complex_elements(
|
||||
out.push(lengths.next()?.clone());
|
||||
continue;
|
||||
}
|
||||
LtSegment::Shape { name, shx_file, x, y, scale, rot_deg, .. } => {
|
||||
LtSegment::Shape {
|
||||
name,
|
||||
shx_file,
|
||||
x,
|
||||
y,
|
||||
scale,
|
||||
rot_deg,
|
||||
..
|
||||
} => {
|
||||
let stem = shape_file_stem(shx_file);
|
||||
let number = BUNDLED_SHAPES
|
||||
.iter()
|
||||
.find(|(file, shape, _)| *file == stem && shape.eq_ignore_ascii_case(name))?
|
||||
.2;
|
||||
LineTypeComplexData {
|
||||
content: LineTypeComplexContent::Shape { shape_number: number },
|
||||
content: LineTypeComplexContent::Shape {
|
||||
shape_number: number,
|
||||
},
|
||||
style_handle: shape_file_style(doc, shx_file),
|
||||
scale: decimal(*scale),
|
||||
rotation: decimal(*rot_deg).to_radians(),
|
||||
@@ -407,7 +421,14 @@ fn complex_elements(
|
||||
offset: [decimal(*x), decimal(*y)],
|
||||
}
|
||||
}
|
||||
LtSegment::Text { text, style, x, y, scale, rot_deg } => LineTypeComplexData {
|
||||
LtSegment::Text {
|
||||
text,
|
||||
style,
|
||||
x,
|
||||
y,
|
||||
scale,
|
||||
rot_deg,
|
||||
} => LineTypeComplexData {
|
||||
content: LineTypeComplexContent::Text { text: text.clone() },
|
||||
style_handle: doc
|
||||
.text_styles
|
||||
@@ -425,7 +446,6 @@ fn complex_elements(
|
||||
(lengths.next().is_none()).then_some(out)
|
||||
}
|
||||
|
||||
|
||||
// ── Parser ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Parse a `.lin` file and return all simple linetypes found.
|
||||
|
||||
+278
-293
@@ -3,41 +3,39 @@
|
||||
// All file reading/writing goes through opencadcodec.
|
||||
// Default save format: DWG (AC1032 / R2018+).
|
||||
|
||||
pub mod file_association;
|
||||
pub mod font_repo;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod edit_lock;
|
||||
pub mod file_association;
|
||||
pub mod font_repo;
|
||||
pub mod line_read;
|
||||
pub mod linetypes;
|
||||
pub mod obj;
|
||||
pub mod ole_embed;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod single_instance;
|
||||
pub mod paper_catalog;
|
||||
pub mod patterns;
|
||||
pub mod pdf_export;
|
||||
pub mod plot_device;
|
||||
pub mod plot_style;
|
||||
pub mod print_to_printer;
|
||||
pub mod recovery;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod single_instance;
|
||||
pub mod step;
|
||||
pub mod stl;
|
||||
pub mod xref;
|
||||
pub mod xref_model;
|
||||
pub mod linetypes;
|
||||
pub mod line_read;
|
||||
pub mod patterns;
|
||||
pub mod update_check;
|
||||
pub mod paper_catalog;
|
||||
pub mod plot_device;
|
||||
pub mod windows_media;
|
||||
pub mod thumbnail;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod web_worker;
|
||||
pub mod update_check;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) mod web_recent;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod web_worker;
|
||||
pub mod windows_media;
|
||||
pub mod xref;
|
||||
pub mod xref_model;
|
||||
|
||||
use crate::scene::DerivedCaches;
|
||||
use codec::entities::EntityType;
|
||||
use codec::io::dwg::DwgReader;
|
||||
use codec::{
|
||||
CadDocument, DwgReadOptions, DwgWriter, DxfReader, DxfReaderConfiguration, DxfWriter,
|
||||
};
|
||||
use codec::{CadDocument, DwgReadOptions, DwgWriter, DxfReader, DxfReaderConfiguration, DxfWriter};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU16, AtomicU32, AtomicU8, Ordering};
|
||||
use std::sync::Arc;
|
||||
@@ -98,9 +96,7 @@ pub fn open_phase_name(phase: u8) -> &'static str {
|
||||
|
||||
fn recovery_fingerprint_needed(caches: &DerivedCaches) -> bool {
|
||||
let parser_issue = caches.read_stats.as_ref().is_some_and(|stats| {
|
||||
stats.recovered()
|
||||
|| stats.skipped_source_records > 0
|
||||
|| !stats.stream_completed
|
||||
stats.recovered() || stats.skipped_source_records > 0 || !stats.stream_completed
|
||||
});
|
||||
let reference_issue = caches.xrefs.iter().any(|item| {
|
||||
matches!(
|
||||
@@ -130,10 +126,7 @@ impl OpenLoadError {
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn recovery_prompt(
|
||||
message: impl Into<String>,
|
||||
read_stats: Option<codec::ReadStats>,
|
||||
) -> Self {
|
||||
fn recovery_prompt(message: impl Into<String>, read_stats: Option<codec::ReadStats>) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
source_sha256: None,
|
||||
@@ -170,11 +163,22 @@ impl From<&str> for OpenLoadError {
|
||||
pub async fn pick_open_path() -> Option<(PathBuf, u64)> {
|
||||
let handle = crate::sys::file_dialog()
|
||||
.set_title(crate::t!("Open CAD file").as_ref())
|
||||
.add_filter(crate::t!("CAD Files").as_ref(), &["dwg", "dxf", "dwt", "bak", "sv$", "DWG", "DXF", "DWT", "BAK"])
|
||||
.add_filter(
|
||||
crate::t!("CAD Files").as_ref(),
|
||||
&[
|
||||
"dwg", "dxf", "dwt", "bak", "sv$", "DWG", "DXF", "DWT", "BAK",
|
||||
],
|
||||
)
|
||||
.add_filter(crate::t!("DWG Files").as_ref(), &["dwg", "DWG"])
|
||||
.add_filter(crate::t!("DXF Files").as_ref(), &["dxf", "DXF"])
|
||||
.add_filter(crate::t!("Drawing Template (*.dwt)").as_ref(), &["dwt", "DWT"])
|
||||
.add_filter(crate::t!("Backup / Autosave").as_ref(), &["bak", "sv$", "BAK"])
|
||||
.add_filter(
|
||||
crate::t!("Drawing Template (*.dwt)").as_ref(),
|
||||
&["dwt", "DWT"],
|
||||
)
|
||||
.add_filter(
|
||||
crate::t!("Backup / Autosave").as_ref(),
|
||||
&["bak", "sv$", "BAK"],
|
||||
)
|
||||
.add_filter(crate::t!("All Files").as_ref(), &["*"])
|
||||
.pick_file()
|
||||
.await?;
|
||||
@@ -202,11 +206,14 @@ pub async fn pick_layer_standard_path() -> Option<PathBuf> {
|
||||
/// Pick where a set of layer mappings is written, or read back from.
|
||||
pub async fn pick_layer_mapping_path(save: bool) -> Option<PathBuf> {
|
||||
let dialog = crate::sys::file_dialog()
|
||||
.set_title(crate::t!(if save {
|
||||
"Save layer mappings"
|
||||
} else {
|
||||
"Load layer mappings"
|
||||
}).as_ref())
|
||||
.set_title(
|
||||
crate::t!(if save {
|
||||
"Save layer mappings"
|
||||
} else {
|
||||
"Load layer mappings"
|
||||
})
|
||||
.as_ref(),
|
||||
)
|
||||
.add_filter(crate::t!("Layer mappings").as_ref(), &["ocslmap"])
|
||||
.add_filter(crate::t!("All Files").as_ref(), &["*"]);
|
||||
let handle = if save {
|
||||
@@ -305,135 +312,133 @@ async fn open_path_with_phase_attempt(
|
||||
let initial_fingerprint = crate::io::edit_lock::FileFingerprint::capture(&path2).ok();
|
||||
let attempted = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
(|| -> Result<_, OpenAttemptFailure> {
|
||||
use iced::time::Instant;
|
||||
progress2.set(crate::app::OPEN_PHASE_PARSING, 200, 0, 1000);
|
||||
let t_parse = Instant::now();
|
||||
let parser_progress = {
|
||||
let progress = Arc::clone(&progress2);
|
||||
let callback: Arc<dyn Fn(u16) + Send + Sync> = Arc::new(move |value| {
|
||||
progress.set_fraction(
|
||||
crate::app::OPEN_PHASE_PARSING,
|
||||
200,
|
||||
5600,
|
||||
value as usize,
|
||||
1000,
|
||||
);
|
||||
});
|
||||
callback
|
||||
};
|
||||
std::fs::File::open(&path2).map_err(|error| {
|
||||
// 32 = ERROR_SHARING_VIOLATION, 33 = ERROR_LOCK_VIOLATION:
|
||||
// another program holds the drawing without read sharing,
|
||||
// so nothing on the machine can read the bytes until it
|
||||
// closes the file — not even read-only.
|
||||
let message = match error.raw_os_error() {
|
||||
Some(32) | Some(33) => format!(
|
||||
"\"{}\" is in use by another program. Close the file there and \
|
||||
reopen it here, or open a copy of the file.",
|
||||
path2.display()
|
||||
),
|
||||
_ => format!("failed to open drawing: {error}"),
|
||||
};
|
||||
OpenAttemptFailure {
|
||||
message,
|
||||
read_stats: None,
|
||||
recoverable: false,
|
||||
}
|
||||
})?;
|
||||
let outcome = load_file_for_open(&path2, Some(parser_progress), &attempt)?;
|
||||
let read_stats = outcome.stats;
|
||||
let mut doc = outcome.document;
|
||||
let parse_ms = t_parse.elapsed().as_millis() as u32;
|
||||
progress2.set(crate::app::OPEN_PHASE_PARSING, 5800, 1000, 1000);
|
||||
let t_purge = Instant::now();
|
||||
let dropped = purge_corrupt_entities(&mut doc);
|
||||
let purge_ms = t_purge.elapsed().as_millis() as u32;
|
||||
if matches!(attempt, OpenAttempt::Strict) && dropped > 0 {
|
||||
return Err(OpenAttemptFailure {
|
||||
message: format!(
|
||||
"normal read found {dropped} structurally invalid drawing records"
|
||||
),
|
||||
read_stats: Some(read_stats),
|
||||
recoverable: true,
|
||||
});
|
||||
}
|
||||
// Before the geometry is prepared below: completing the
|
||||
// drawing's catalog linetypes (and repairing stripped ones)
|
||||
// after it would leave the first view drawn without them.
|
||||
crate::io::linetypes::populate_document(&mut doc);
|
||||
progress2.set(crate::app::OPEN_PHASE_XREF, 6000, 0, 1);
|
||||
let t_xref = Instant::now();
|
||||
let (xref_infos, xref_dropped) = if let Some(base_dir) = path2.parent() {
|
||||
let xref_progress = {
|
||||
use iced::time::Instant;
|
||||
progress2.set(crate::app::OPEN_PHASE_PARSING, 200, 0, 1000);
|
||||
let t_parse = Instant::now();
|
||||
let parser_progress = {
|
||||
let progress = Arc::clone(&progress2);
|
||||
let callback: Arc<dyn Fn(usize, usize) + Send + Sync> =
|
||||
Arc::new(move |completed, total| {
|
||||
progress.set_fraction(
|
||||
crate::app::OPEN_PHASE_XREF,
|
||||
6000,
|
||||
1400,
|
||||
completed,
|
||||
total,
|
||||
);
|
||||
});
|
||||
let callback: Arc<dyn Fn(u16) + Send + Sync> = Arc::new(move |value| {
|
||||
progress.set_fraction(
|
||||
crate::app::OPEN_PHASE_PARSING,
|
||||
200,
|
||||
5600,
|
||||
value as usize,
|
||||
1000,
|
||||
);
|
||||
});
|
||||
callback
|
||||
};
|
||||
crate::io::xref::register_underlay_sources(&doc, base_dir);
|
||||
crate::io::xref::resolve_xrefs_with_progress(
|
||||
&mut doc,
|
||||
base_dir,
|
||||
Some(xref_progress),
|
||||
)
|
||||
} else {
|
||||
(Vec::new(), 0)
|
||||
};
|
||||
let xref_ms = t_xref.elapsed().as_millis() as u32;
|
||||
progress2.set(crate::app::OPEN_PHASE_CACHING, 7400, 0, 10000);
|
||||
let t_caches = Instant::now();
|
||||
let cache_progress = |value: u16| {
|
||||
progress2.set_fraction(
|
||||
crate::app::OPEN_PHASE_CACHING,
|
||||
7400,
|
||||
2200,
|
||||
value as usize,
|
||||
10000,
|
||||
std::fs::File::open(&path2).map_err(|error| {
|
||||
// 32 = ERROR_SHARING_VIOLATION, 33 = ERROR_LOCK_VIOLATION:
|
||||
// another program holds the drawing without read sharing,
|
||||
// so nothing on the machine can read the bytes until it
|
||||
// closes the file — not even read-only.
|
||||
let message = match error.raw_os_error() {
|
||||
Some(32) | Some(33) => format!(
|
||||
"\"{}\" is in use by another program. Close the file there and \
|
||||
reopen it here, or open a copy of the file.",
|
||||
path2.display()
|
||||
),
|
||||
_ => format!("failed to open drawing: {error}"),
|
||||
};
|
||||
OpenAttemptFailure {
|
||||
message,
|
||||
read_stats: None,
|
||||
recoverable: false,
|
||||
}
|
||||
})?;
|
||||
let outcome = load_file_for_open(&path2, Some(parser_progress), &attempt)?;
|
||||
let read_stats = outcome.stats;
|
||||
let mut doc = outcome.document;
|
||||
let parse_ms = t_parse.elapsed().as_millis() as u32;
|
||||
progress2.set(crate::app::OPEN_PHASE_PARSING, 5800, 1000, 1000);
|
||||
let t_purge = Instant::now();
|
||||
let dropped = purge_corrupt_entities(&mut doc);
|
||||
let purge_ms = t_purge.elapsed().as_millis() as u32;
|
||||
if matches!(attempt, OpenAttempt::Strict) && dropped > 0 {
|
||||
return Err(OpenAttemptFailure {
|
||||
message: format!(
|
||||
"normal read found {dropped} structurally invalid drawing records"
|
||||
),
|
||||
read_stats: Some(read_stats),
|
||||
recoverable: true,
|
||||
});
|
||||
}
|
||||
// Before the geometry is prepared below: completing the
|
||||
// drawing's catalog linetypes (and repairing stripped ones)
|
||||
// after it would leave the first view drawn without them.
|
||||
crate::io::linetypes::populate_document(&mut doc);
|
||||
progress2.set(crate::app::OPEN_PHASE_XREF, 6000, 0, 1);
|
||||
let t_xref = Instant::now();
|
||||
let (xref_infos, xref_dropped) = if let Some(base_dir) = path2.parent() {
|
||||
let xref_progress = {
|
||||
let progress = Arc::clone(&progress2);
|
||||
let callback: Arc<dyn Fn(usize, usize) + Send + Sync> =
|
||||
Arc::new(move |completed, total| {
|
||||
progress.set_fraction(
|
||||
crate::app::OPEN_PHASE_XREF,
|
||||
6000,
|
||||
1400,
|
||||
completed,
|
||||
total,
|
||||
);
|
||||
});
|
||||
callback
|
||||
};
|
||||
crate::io::xref::register_underlay_sources(&doc, base_dir);
|
||||
crate::io::xref::resolve_xrefs_with_progress(
|
||||
&mut doc,
|
||||
base_dir,
|
||||
Some(xref_progress),
|
||||
)
|
||||
} else {
|
||||
(Vec::new(), 0)
|
||||
};
|
||||
let xref_ms = t_xref.elapsed().as_millis() as u32;
|
||||
progress2.set(crate::app::OPEN_PHASE_CACHING, 7400, 0, 10000);
|
||||
let t_caches = Instant::now();
|
||||
let cache_progress = |value: u16| {
|
||||
progress2.set_fraction(
|
||||
crate::app::OPEN_PHASE_CACHING,
|
||||
7400,
|
||||
2200,
|
||||
value as usize,
|
||||
10000,
|
||||
);
|
||||
};
|
||||
let mut caches = crate::scene::build_derived_caches_with_progress(
|
||||
&doc,
|
||||
&cache_progress,
|
||||
path2.parent(),
|
||||
);
|
||||
};
|
||||
let mut caches = crate::scene::build_derived_caches_with_progress(
|
||||
&doc,
|
||||
&cache_progress,
|
||||
path2.parent(),
|
||||
);
|
||||
caches.timings = crate::scene::OpenTimings {
|
||||
parse_ms,
|
||||
purge_ms,
|
||||
caches_ms: t_caches.elapsed().as_millis() as u32,
|
||||
xref_ms,
|
||||
// Filled in after `prepare_open_geometry` below.
|
||||
finalize_ms: 0,
|
||||
};
|
||||
caches.corrupt_dropped = dropped;
|
||||
caches.read_stats = Some(read_stats);
|
||||
caches.xref_dropped = xref_dropped;
|
||||
caches.xrefs = xref_infos;
|
||||
if recovery_fingerprint_needed(&caches) {
|
||||
caches.source_sha256 = stable_sha256_file(
|
||||
&path2,
|
||||
initial_fingerprint.as_ref(),
|
||||
);
|
||||
}
|
||||
progress2.set(crate::app::OPEN_PHASE_FINALIZING, 9600, 0, 1);
|
||||
let t_finalize = Instant::now();
|
||||
let (prepared_doc, prepared_geometry) =
|
||||
crate::scene::prepare_open_geometry(doc, &caches, model_bg);
|
||||
caches.timings.finalize_ms = t_finalize.elapsed().as_millis() as u32;
|
||||
doc = prepared_doc;
|
||||
caches.prepared_geometry = Some(prepared_geometry);
|
||||
progress2.set(crate::app::OPEN_PHASE_FINALIZING, 9950, 1, 1);
|
||||
// The localized command-line summary predates `finalize_ms` and
|
||||
// is keyed on its format string, so the full breakdown goes out
|
||||
// here instead of changing that key across every locale.
|
||||
crate::perf_record!(
|
||||
caches.timings = crate::scene::OpenTimings {
|
||||
parse_ms,
|
||||
purge_ms,
|
||||
caches_ms: t_caches.elapsed().as_millis() as u32,
|
||||
xref_ms,
|
||||
// Filled in after `prepare_open_geometry` below.
|
||||
finalize_ms: 0,
|
||||
};
|
||||
caches.corrupt_dropped = dropped;
|
||||
caches.read_stats = Some(read_stats);
|
||||
caches.xref_dropped = xref_dropped;
|
||||
caches.xrefs = xref_infos;
|
||||
if recovery_fingerprint_needed(&caches) {
|
||||
caches.source_sha256 =
|
||||
stable_sha256_file(&path2, initial_fingerprint.as_ref());
|
||||
}
|
||||
progress2.set(crate::app::OPEN_PHASE_FINALIZING, 9600, 0, 1);
|
||||
let t_finalize = Instant::now();
|
||||
let (prepared_doc, prepared_geometry) =
|
||||
crate::scene::prepare_open_geometry(doc, &caches, model_bg);
|
||||
caches.timings.finalize_ms = t_finalize.elapsed().as_millis() as u32;
|
||||
doc = prepared_doc;
|
||||
caches.prepared_geometry = Some(prepared_geometry);
|
||||
progress2.set(crate::app::OPEN_PHASE_FINALIZING, 9950, 1, 1);
|
||||
// The localized command-line summary predates `finalize_ms` and
|
||||
// is keyed on its format string, so the full breakdown goes out
|
||||
// here instead of changing that key across every locale.
|
||||
crate::perf_record!(
|
||||
"[perf] open-phases parse={}ms purge={}ms xref={}ms caches={}ms finalize={}ms",
|
||||
caches.timings.parse_ms,
|
||||
caches.timings.purge_ms,
|
||||
@@ -451,10 +456,7 @@ async fn open_path_with_phase_attempt(
|
||||
} else {
|
||||
OpenLoadError {
|
||||
message: failure.message,
|
||||
source_sha256: stable_sha256_file(
|
||||
&path2,
|
||||
initial_fingerprint.as_ref(),
|
||||
),
|
||||
source_sha256: stable_sha256_file(&path2, initial_fingerprint.as_ref()),
|
||||
read_stats: failure.read_stats,
|
||||
recovery_available: false,
|
||||
}
|
||||
@@ -466,17 +468,14 @@ async fn open_path_with_phase_attempt(
|
||||
);
|
||||
Err(OpenLoadError {
|
||||
message,
|
||||
source_sha256: stable_sha256_file(
|
||||
&path2,
|
||||
initial_fingerprint.as_ref(),
|
||||
),
|
||||
source_sha256: stable_sha256_file(&path2, initial_fingerprint.as_ref()),
|
||||
read_stats: None,
|
||||
recovery_available: false,
|
||||
})
|
||||
}
|
||||
};
|
||||
let _ = sender.send(result);
|
||||
})
|
||||
})
|
||||
.map_err(|error| OpenLoadError::from(format!("failed to start parser thread: {error}")))?;
|
||||
let (doc, caches) = receiver
|
||||
.await
|
||||
@@ -526,13 +525,17 @@ pub struct WebOpenOutcome {
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn pick_and_load_web(
|
||||
progress: Arc<OpenProgressState>,
|
||||
) -> WebOpenOutcome {
|
||||
pub async fn pick_and_load_web(progress: Arc<OpenProgressState>) -> WebOpenOutcome {
|
||||
let Some(handle) = crate::sys::file_dialog()
|
||||
.set_title(crate::t!("Open CAD file").as_ref())
|
||||
.add_filter(crate::t!("CAD Files").as_ref(), &["dwg", "dxf", "dwt", "DWG", "DXF", "DWT"])
|
||||
.add_filter(crate::t!("Drawing Template (*.dwt)").as_ref(), &["dwt", "DWT"])
|
||||
.add_filter(
|
||||
crate::t!("CAD Files").as_ref(),
|
||||
&["dwg", "dxf", "dwt", "DWG", "DXF", "DWT"],
|
||||
)
|
||||
.add_filter(
|
||||
crate::t!("Drawing Template (*.dwt)").as_ref(),
|
||||
&["dwt", "DWT"],
|
||||
)
|
||||
.add_filter(crate::t!("All Files").as_ref(), &["*"])
|
||||
.pick_file()
|
||||
.await
|
||||
@@ -580,10 +583,7 @@ pub async fn open_bytes_web(
|
||||
|
||||
/// Reopen a browser-private recent copy without showing the file picker.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn open_recent_web(
|
||||
path: PathBuf,
|
||||
progress: Arc<OpenProgressState>,
|
||||
) -> WebOpenOutcome {
|
||||
pub async fn open_recent_web(path: PathBuf, progress: Arc<OpenProgressState>) -> WebOpenOutcome {
|
||||
open_recent_web_attempt(path, progress, false, String::new()).await
|
||||
}
|
||||
|
||||
@@ -596,15 +596,7 @@ pub async fn recover_web_bytes(
|
||||
initial_stats: Option<codec::ReadStats>,
|
||||
) -> WebOpenOutcome {
|
||||
let size_bytes = bytes.len() as u64;
|
||||
let result = load_web_bytes(
|
||||
&name,
|
||||
&bytes,
|
||||
progress,
|
||||
true,
|
||||
&initial_error,
|
||||
initial_stats,
|
||||
)
|
||||
.await;
|
||||
let result = load_web_bytes(&name, &bytes, progress, true, &initial_error, initial_stats).await;
|
||||
WebOpenOutcome {
|
||||
name,
|
||||
size_bytes,
|
||||
@@ -647,15 +639,7 @@ async fn open_recent_web_attempt(
|
||||
bytes.len(),
|
||||
bytes.len(),
|
||||
);
|
||||
let result = load_web_bytes(
|
||||
&name,
|
||||
&bytes,
|
||||
progress,
|
||||
recovery_mode,
|
||||
&initial_error,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
let result = load_web_bytes(&name, &bytes, progress, recovery_mode, &initial_error, None).await;
|
||||
let keep_for_recovery = result
|
||||
.as_ref()
|
||||
.err()
|
||||
@@ -681,38 +665,32 @@ async fn load_web_bytes(
|
||||
mut initial_stats: Option<codec::ReadStats>,
|
||||
) -> Result<(String, PathBuf, CadDocument, DerivedCaches), OpenLoadError> {
|
||||
progress.set(crate::app::OPEN_PHASE_PARSING, 1000, 0, 1);
|
||||
let (outcome, mut source_sha256) = match web_worker::parse_document(
|
||||
name,
|
||||
bytes,
|
||||
recovery_mode,
|
||||
initial_error,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
let recoverable_parse_error = error.recovery_available;
|
||||
let mut source_sha256 = error.source_sha256;
|
||||
if recovery_mode && source_sha256.is_none() {
|
||||
source_sha256 = web_worker::sha256_document(bytes).await.ok();
|
||||
}
|
||||
let read_stats = merge_read_stats(error.read_stats, initial_stats.take());
|
||||
let message = if recovery_mode && !error.message.contains("initial read failed:") {
|
||||
format!(
|
||||
"initial read failed: {initial_error}; recovery read failed: {}",
|
||||
let (outcome, mut source_sha256) =
|
||||
match web_worker::parse_document(name, bytes, recovery_mode, initial_error).await {
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
let recoverable_parse_error = error.recovery_available;
|
||||
let mut source_sha256 = error.source_sha256;
|
||||
if recovery_mode && source_sha256.is_none() {
|
||||
source_sha256 = web_worker::sha256_document(bytes).await.ok();
|
||||
}
|
||||
let read_stats = merge_read_stats(error.read_stats, initial_stats.take());
|
||||
let message = if recovery_mode && !error.message.contains("initial read failed:") {
|
||||
format!(
|
||||
"initial read failed: {initial_error}; recovery read failed: {}",
|
||||
error.message
|
||||
)
|
||||
} else {
|
||||
error.message
|
||||
)
|
||||
} else {
|
||||
error.message
|
||||
};
|
||||
return Err(OpenLoadError {
|
||||
message: format!("Web parser worker: {message}"),
|
||||
recovery_available: !recovery_mode && recoverable_parse_error,
|
||||
source_sha256,
|
||||
read_stats,
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
return Err(OpenLoadError {
|
||||
message: format!("Web parser worker: {message}"),
|
||||
recovery_available: !recovery_mode && recoverable_parse_error,
|
||||
source_sha256,
|
||||
read_stats,
|
||||
});
|
||||
}
|
||||
};
|
||||
let mut outcome = outcome;
|
||||
if let Some(initial_stats) = initial_stats.take() {
|
||||
merge_read_diagnostics(&mut outcome.stats, initial_stats);
|
||||
@@ -730,9 +708,7 @@ async fn load_web_bytes(
|
||||
let dropped = purge_corrupt_entities(&mut doc);
|
||||
if !recovery_mode && dropped > 0 {
|
||||
return Err(OpenLoadError {
|
||||
message: format!(
|
||||
"normal read found {dropped} structurally invalid drawing records"
|
||||
),
|
||||
message: format!("normal read found {dropped} structurally invalid drawing records"),
|
||||
source_sha256: None,
|
||||
read_stats: Some(outcome.stats),
|
||||
recovery_available: true,
|
||||
@@ -860,13 +836,12 @@ fn load_file_for_open(
|
||||
) -> Result<codec::ReadOutcome, OpenAttemptFailure> {
|
||||
let outcome = match attempt {
|
||||
OpenAttempt::Strict => {
|
||||
let outcome = read_file_attempt(path, progress, false).map_err(|failure| {
|
||||
OpenAttemptFailure {
|
||||
let outcome =
|
||||
read_file_attempt(path, progress, false).map_err(|failure| OpenAttemptFailure {
|
||||
message: failure.message,
|
||||
read_stats: None,
|
||||
recoverable: failure.recoverable,
|
||||
}
|
||||
})?;
|
||||
})?;
|
||||
if !outcome.stats.has_usable_drawing_data() {
|
||||
return Err(OpenAttemptFailure {
|
||||
message: "initial read returned no source drawing records".to_string(),
|
||||
@@ -895,16 +870,15 @@ fn load_file_for_open(
|
||||
outcome
|
||||
}
|
||||
OpenAttempt::Recovery(initial_error, initial_stats) => {
|
||||
let mut outcome = read_file_attempt(path, progress, true).map_err(|failure| {
|
||||
OpenAttemptFailure {
|
||||
let mut outcome =
|
||||
read_file_attempt(path, progress, true).map_err(|failure| OpenAttemptFailure {
|
||||
message: format!(
|
||||
"initial read failed: {initial_error}; recovery read failed: {}",
|
||||
failure.message
|
||||
),
|
||||
read_stats: initial_stats.clone(),
|
||||
recoverable: false,
|
||||
}
|
||||
})?;
|
||||
})?;
|
||||
if !outcome.stats.has_usable_drawing_data() {
|
||||
if let Some(initial_stats) = initial_stats.clone() {
|
||||
merge_read_diagnostics(&mut outcome.stats, initial_stats);
|
||||
@@ -982,10 +956,7 @@ fn recoverable_reader_error(error: &codec::DxfError) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
fn merge_read_diagnostics(
|
||||
target: &mut codec::ReadStats,
|
||||
source: codec::ReadStats,
|
||||
) {
|
||||
fn merge_read_diagnostics(target: &mut codec::ReadStats, source: codec::ReadStats) {
|
||||
for diagnostic in source.diagnostics {
|
||||
if !target.diagnostics.contains(&diagnostic) {
|
||||
codec::push_read_diagnostic(&mut target.diagnostics, diagnostic);
|
||||
@@ -1106,20 +1077,16 @@ fn read_dwg_path(
|
||||
if let Some(progress) = progress {
|
||||
reader.set_progress_callback(progress);
|
||||
}
|
||||
reader
|
||||
.read_with_stats()
|
||||
.map_err(ReaderFailure::from_reader)
|
||||
reader.read_with_stats().map_err(ReaderFailure::from_reader)
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let mut reader = DwgReader::from_file_with_options(path, options)
|
||||
.map_err(ReaderFailure::from_reader)?;
|
||||
let mut reader =
|
||||
DwgReader::from_file_with_options(path, options).map_err(ReaderFailure::from_reader)?;
|
||||
if let Some(progress) = progress {
|
||||
reader.set_progress_callback(progress);
|
||||
}
|
||||
reader
|
||||
.read_with_stats()
|
||||
.map_err(ReaderFailure::from_reader)
|
||||
reader.read_with_stats().map_err(ReaderFailure::from_reader)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1605,10 +1572,7 @@ impl SaveFailure {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn externally_modified(path: &Path) -> Self {
|
||||
Self {
|
||||
message: format!(
|
||||
"{} changed on disk after it was opened",
|
||||
path.display()
|
||||
),
|
||||
message: format!("{} changed on disk after it was opened", path.display()),
|
||||
file_in_use: false,
|
||||
externally_modified: true,
|
||||
}
|
||||
@@ -1687,12 +1651,8 @@ mod save_failure_tests {
|
||||
.as_nanos()
|
||||
));
|
||||
|
||||
let error = save_as_version(
|
||||
&codec::CadDocument::new(),
|
||||
&path,
|
||||
codec::DxfVersion::AC1032,
|
||||
)
|
||||
.unwrap_err();
|
||||
let error = save_as_version(&codec::CadDocument::new(), &path, codec::DxfVersion::AC1032)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
error,
|
||||
@@ -1824,7 +1784,10 @@ pub async fn pick_plot_style() -> Result<Option<plot_style::PlotStyleTable>, Str
|
||||
pub async fn pick_image_file() -> Result<(PathBuf, u32, u32), String> {
|
||||
let handle = crate::sys::file_dialog()
|
||||
.set_title(crate::t!("Select Image File").as_ref())
|
||||
.add_filter(crate::t!("Images").as_ref(), &["png", "jpg", "jpeg", "bmp", "tiff", "tif"])
|
||||
.add_filter(
|
||||
crate::t!("Images").as_ref(),
|
||||
&["png", "jpg", "jpeg", "bmp", "tiff", "tif"],
|
||||
)
|
||||
.add_filter(crate::t!("PNG").as_ref(), &["png"])
|
||||
.add_filter(crate::t!("JPEG").as_ref(), &["jpg", "jpeg"])
|
||||
.add_filter(crate::t!("All Files").as_ref(), &["*"])
|
||||
@@ -1842,7 +1805,10 @@ pub async fn pick_image_file() -> Result<(PathBuf, u32, u32), String> {
|
||||
pub async fn pick_embedded_image_file() -> Result<ole_embed::EmbeddedImage, String> {
|
||||
let handle = crate::sys::file_dialog()
|
||||
.set_title(crate::t!("Select Image File").as_ref())
|
||||
.add_filter(crate::t!("Images").as_ref(), &["png", "jpg", "jpeg", "bmp", "tiff", "tif"])
|
||||
.add_filter(
|
||||
crate::t!("Images").as_ref(),
|
||||
&["png", "jpg", "jpeg", "bmp", "tiff", "tif"],
|
||||
)
|
||||
.add_filter(crate::t!("PNG").as_ref(), &["png"])
|
||||
.add_filter(crate::t!("JPEG").as_ref(), &["jpg", "jpeg"])
|
||||
.add_filter(crate::t!("All Files").as_ref(), &["*"])
|
||||
@@ -2022,11 +1988,7 @@ fn replace_save_file_inner(temp_path: &Path, path: &Path) -> std::io::Result<()>
|
||||
}
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
let replaced: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
|
||||
let replacement: Vec<u16> = temp_path
|
||||
.as_os_str()
|
||||
.encode_wide()
|
||||
.chain(Some(0))
|
||||
.collect();
|
||||
let replacement: Vec<u16> = temp_path.as_os_str().encode_wide().chain(Some(0)).collect();
|
||||
let ok = unsafe {
|
||||
windows_sys::Win32::Storage::FileSystem::ReplaceFileW(
|
||||
replaced.as_ptr(),
|
||||
@@ -2074,7 +2036,9 @@ pub fn save_to_bytes(
|
||||
let dimensions_ms = dimensions_started.elapsed().as_secs_f64() * 1000.0;
|
||||
let write_started = iced::time::Instant::now();
|
||||
let result = match ext.to_lowercase().as_str() {
|
||||
"dxf" => DxfWriter::new(&doc).write_to_vec().map_err(|e| e.to_string()),
|
||||
"dxf" => DxfWriter::new(&doc)
|
||||
.write_to_vec()
|
||||
.map_err(|e| e.to_string()),
|
||||
_ => {
|
||||
let mut buf = std::io::Cursor::new(Vec::new());
|
||||
DwgWriter::write_to_writer(&mut buf, &doc).map_err(|e| e.to_string())?;
|
||||
@@ -2098,7 +2062,6 @@ pub fn save_to_bytes(
|
||||
result
|
||||
}
|
||||
|
||||
|
||||
// ── Post-load fixups ──────────────────────────────────────────────────────
|
||||
|
||||
// Resolve the current text / dimension / multiline style from the handle the
|
||||
@@ -2112,13 +2075,23 @@ fn fix_current_style_names(doc: &mut CadDocument) {
|
||||
|
||||
let h = doc.header.current_text_style_handle;
|
||||
if h.is_valid() {
|
||||
if let Some(name) = doc.text_styles.iter().find(|s| s.handle == h).map(|s| s.name.clone()) {
|
||||
if let Some(name) = doc
|
||||
.text_styles
|
||||
.iter()
|
||||
.find(|s| s.handle == h)
|
||||
.map(|s| s.name.clone())
|
||||
{
|
||||
doc.header.current_text_style_name = name;
|
||||
}
|
||||
}
|
||||
let h = doc.header.current_dimstyle_handle;
|
||||
if h.is_valid() {
|
||||
if let Some(name) = doc.dim_styles.iter().find(|s| s.handle == h).map(|s| s.name.clone()) {
|
||||
if let Some(name) = doc
|
||||
.dim_styles
|
||||
.iter()
|
||||
.find(|s| s.handle == h)
|
||||
.map(|s| s.name.clone())
|
||||
{
|
||||
doc.header.current_dimstyle_name = name;
|
||||
}
|
||||
}
|
||||
@@ -2222,9 +2195,7 @@ pub(crate) fn set_drawing_variable(doc: &mut CadDocument, name: &str, value: &st
|
||||
let root = crate::scene::annotative::root_named_dict_handle(doc);
|
||||
let variable_dictionary = crate::scene::annotative::as_dict(doc, root)
|
||||
.and_then(|dictionary| dictionary.get("AcDbVariableDictionary"))
|
||||
.filter(|handle| {
|
||||
matches!(doc.objects.get(handle), Some(ObjectType::Dictionary(_)))
|
||||
})
|
||||
.filter(|handle| matches!(doc.objects.get(handle), Some(ObjectType::Dictionary(_))))
|
||||
.unwrap_or_else(|| {
|
||||
let handle = doc.allocate_handle();
|
||||
let mut dictionary = Dictionary::new();
|
||||
@@ -2308,8 +2279,11 @@ fn sync_current_styles_on_save(doc: &mut CadDocument) {
|
||||
// The renderer uses a closed filled arrow when a referenced block is
|
||||
// missing. Persist that same default instead of a dangling hard pointer:
|
||||
// Rhino otherwise drops every dimension using the affected DIMSTYLE.
|
||||
let block_handles: rustc_hash::FxHashSet<_> =
|
||||
doc.block_records.iter().map(|record| record.handle).collect();
|
||||
let block_handles: rustc_hash::FxHashSet<_> = doc
|
||||
.block_records
|
||||
.iter()
|
||||
.map(|record| record.handle)
|
||||
.collect();
|
||||
for style in doc.dim_styles.iter_mut() {
|
||||
for handle in [
|
||||
&mut style.dimblk,
|
||||
@@ -2420,12 +2394,10 @@ pub(crate) fn is_entity_corrupt(e: &EntityType) -> bool {
|
||||
|| p.vertices.iter().any(|v| !finite_vec3(&v.location))
|
||||
}
|
||||
E::Polyline3D(p) => {
|
||||
p.vertices.len() >= MAX_VERTS
|
||||
|| p.vertices.iter().any(|v| !finite_vec3(&v.position))
|
||||
p.vertices.len() >= MAX_VERTS || p.vertices.iter().any(|v| !finite_vec3(&v.position))
|
||||
}
|
||||
E::Polyline(p) => {
|
||||
p.vertices.len() >= MAX_VERTS
|
||||
|| p.vertices.iter().any(|v| !finite_vec3(&v.location))
|
||||
p.vertices.len() >= MAX_VERTS || p.vertices.iter().any(|v| !finite_vec3(&v.location))
|
||||
}
|
||||
E::Line(l) => !finite_vec3(&l.start) || !finite_vec3(&l.end),
|
||||
// Zero-radius curves are valid and have bounded kernel tessellation.
|
||||
@@ -2779,7 +2751,10 @@ mod layer_roundtrip_tests {
|
||||
|
||||
#[test]
|
||||
fn dwg_preserves_new_layer() {
|
||||
assert!(roundtrip_layers("dwg", 1), "DWG dropped the new layer (issue #67)");
|
||||
assert!(
|
||||
roundtrip_layers("dwg", 1),
|
||||
"DWG dropped the new layer (issue #67)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2791,7 +2766,10 @@ mod layer_roundtrip_tests {
|
||||
// the last are dropped on a handle-based DWG save (issue #67).
|
||||
#[test]
|
||||
fn dwg_preserves_multiple_new_layers() {
|
||||
assert!(roundtrip_layers("dwg", 3), "DWG dropped colliding new layers (issue #67)");
|
||||
assert!(
|
||||
roundtrip_layers("dwg", 3),
|
||||
"DWG dropped colliding new layers (issue #67)"
|
||||
);
|
||||
}
|
||||
|
||||
// #252: an entity added (as a plugin does) on a layer that no LAYER command
|
||||
@@ -2822,7 +2800,11 @@ mod layer_roundtrip_tests {
|
||||
);
|
||||
let ent = loaded
|
||||
.get_entity(h)
|
||||
.or_else(|| loaded.entities().find(|e| matches!(e, EntityType::Point(_))))
|
||||
.or_else(|| {
|
||||
loaded
|
||||
.entities()
|
||||
.find(|e| matches!(e, EntityType::Point(_)))
|
||||
})
|
||||
.expect("point entity missing after round-trip");
|
||||
assert_eq!(
|
||||
ent.common().layer,
|
||||
@@ -2853,9 +2835,15 @@ mod corrupt_guard_tests {
|
||||
#[test]
|
||||
fn knotless_spline_with_too_few_points_is_lowered_and_saves_as_dwg() {
|
||||
let mut doc = CadDocument::new();
|
||||
doc.add_entity(EntityType::Spline(knotless_spline(3, 2))).unwrap();
|
||||
doc.add_entity(EntityType::Spline(knotless_spline(3, 1))).unwrap();
|
||||
assert_eq!(purge_corrupt_entities(&mut doc), 1, "the lone point is dropped");
|
||||
doc.add_entity(EntityType::Spline(knotless_spline(3, 2)))
|
||||
.unwrap();
|
||||
doc.add_entity(EntityType::Spline(knotless_spline(3, 1)))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
purge_corrupt_entities(&mut doc),
|
||||
1,
|
||||
"the lone point is dropped"
|
||||
);
|
||||
let spline = doc
|
||||
.entities()
|
||||
.find_map(|entity| match entity {
|
||||
@@ -2866,10 +2854,8 @@ mod corrupt_guard_tests {
|
||||
assert_eq!(spline.degree, 1);
|
||||
assert_eq!(spline.knots, vec![0.0, 0.0, 1.0, 1.0]);
|
||||
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"ocs_knotless_spline_{}.dwg",
|
||||
std::process::id()
|
||||
));
|
||||
let path =
|
||||
std::env::temp_dir().join(format!("ocs_knotless_spline_{}.dwg", std::process::id()));
|
||||
save_as_version(&doc, &path, codec::DxfVersion::AC1032).expect("save");
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
@@ -2878,7 +2864,8 @@ mod corrupt_guard_tests {
|
||||
// the spline repair has to happen when any load is finalized.
|
||||
#[test]
|
||||
fn knotless_spline_is_repaired_by_every_load_path() {
|
||||
const DXF: &str = "0\nSECTION\n2\nENTITIES\n0\nSPLINE\n8\n0\n70\n0\n71\n3\n72\n0\n73\n2\n74\n0\n\
|
||||
const DXF: &str =
|
||||
"0\nSECTION\n2\nENTITIES\n0\nSPLINE\n8\n0\n70\n0\n71\n3\n72\n0\n73\n2\n74\n0\n\
|
||||
10\n0\n20\n0\n30\n0\n10\n1\n20\n1\n30\n0\n0\nENDSEC\n0\nEOF\n";
|
||||
let dir = std::env::temp_dir();
|
||||
let source = dir.join(format!("ocs_knotless_load_{}.dxf", std::process::id()));
|
||||
@@ -3132,10 +3119,8 @@ mod pre_r2000_layer_plot_tests {
|
||||
let mut doc = drawing_with_layers();
|
||||
for (index, layer) in ["COARSE", "FINE"].into_iter().enumerate() {
|
||||
let x = index as f64 * 10.0;
|
||||
let mut line = Line::from_points(
|
||||
Vector3::new(x, 0.0, 0.0),
|
||||
Vector3::new(x + 5.0, 5.0, 0.0),
|
||||
);
|
||||
let mut line =
|
||||
Line::from_points(Vector3::new(x, 0.0, 0.0), Vector3::new(x + 5.0, 5.0, 0.0));
|
||||
line.common.layer = layer.to_string();
|
||||
doc.add_entity(EntityType::Line(line)).expect("add line");
|
||||
}
|
||||
|
||||
+464
-396
File diff suppressed because it is too large.
Load diff
@@ -202,7 +202,12 @@ fn capability_cache() -> &'static Mutex<CapabilityCache> {
|
||||
|
||||
/// The cached capabilities of `printer`, if they were queried already.
|
||||
pub fn cached_printer_capabilities(printer: &str) -> Option<Arc<PrinterCapabilities>> {
|
||||
capability_cache().lock().ok()?.get(printer).cloned().flatten()
|
||||
capability_cache()
|
||||
.lock()
|
||||
.ok()?
|
||||
.get(printer)
|
||||
.cloned()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Query `printer` for its media (blocking: a process spawn or a driver
|
||||
|
||||
+50
-31
@@ -104,8 +104,8 @@ pub fn available_ctb_names() -> Vec<String> {
|
||||
/// Lineweight table: index value → mm, matching the stored LWEIGHT codes.
|
||||
/// Index 0 = 0.00 mm (hairline), others follow the DXF lineweight enum.
|
||||
pub const LW_TABLE: &[f32] = &[
|
||||
0.00, 0.05, 0.09, 0.10, 0.13, 0.15, 0.18, 0.20, 0.25, 0.30, 0.35, 0.40, 0.45, 0.50,
|
||||
0.53, 0.60, 0.65, 0.70, 0.80, 0.90, 1.00, 1.06, 1.20, 1.40, 1.58, 2.00, 2.11,
|
||||
0.00, 0.05, 0.09, 0.10, 0.13, 0.15, 0.18, 0.20, 0.25, 0.30, 0.35, 0.40, 0.45, 0.50, 0.53, 0.60,
|
||||
0.65, 0.70, 0.80, 0.90, 1.00, 1.06, 1.20, 1.40, 1.58, 2.00, 2.11,
|
||||
];
|
||||
|
||||
// ── Per-color entry ───────────────────────────────────────────────────────────
|
||||
@@ -235,8 +235,7 @@ impl PlotStyleTable {
|
||||
let gray = if aci == 7 {
|
||||
0
|
||||
} else {
|
||||
(0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32)
|
||||
.round() as u8
|
||||
(0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32).round() as u8
|
||||
};
|
||||
table.aci_entries[aci as usize].color = Some([gray, gray, gray]);
|
||||
}
|
||||
@@ -274,13 +273,19 @@ impl PlotStyleTable {
|
||||
{
|
||||
return Err(format!("Invalid plot style name: {name}"));
|
||||
}
|
||||
if path.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("stb")) {
|
||||
return Err(crate::tf!(
|
||||
"Named plot style tables (.stb) are not supported yet: {name}"
|
||||
)
|
||||
.into_owned());
|
||||
if path
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("stb"))
|
||||
{
|
||||
return Err(
|
||||
crate::tf!("Named plot style tables (.stb) are not supported yet: {name}")
|
||||
.into_owned(),
|
||||
);
|
||||
}
|
||||
if !path.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("ctb")) {
|
||||
if !path
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("ctb"))
|
||||
{
|
||||
return Err(format!("Invalid plot style name: {name}"));
|
||||
}
|
||||
|
||||
@@ -332,9 +337,8 @@ impl PlotStyleTable {
|
||||
if aci == 7 {
|
||||
Some([0.0; 3])
|
||||
} else {
|
||||
codec::types::aci_to_rgb(aci).map(|(r, g, b)| {
|
||||
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0]
|
||||
})
|
||||
codec::types::aci_to_rgb(aci)
|
||||
.map(|(r, g, b)| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0])
|
||||
}
|
||||
})?;
|
||||
let gray = 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2];
|
||||
@@ -350,13 +354,16 @@ impl PlotStyleTable {
|
||||
if matches!(entry.lineweight, 0 | 255) {
|
||||
None
|
||||
} else {
|
||||
self.lineweights.get(entry.lineweight as usize).copied().map(|weight| {
|
||||
if self.apply_factor {
|
||||
weight * self.scale_factor.max(0.0)
|
||||
} else {
|
||||
weight
|
||||
}
|
||||
})
|
||||
self.lineweights
|
||||
.get(entry.lineweight as usize)
|
||||
.copied()
|
||||
.map(|weight| {
|
||||
if self.apply_factor {
|
||||
weight * self.scale_factor.max(0.0)
|
||||
} else {
|
||||
weight
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,7 +430,11 @@ impl PlotStyleTable {
|
||||
" linepattern_size={}\n linetype={}\n adaptive_linetype={}\n",
|
||||
entry.linepattern_size,
|
||||
entry.linetype,
|
||||
if entry.adaptive_linetype { "TRUE" } else { "FALSE" }
|
||||
if entry.adaptive_linetype {
|
||||
"TRUE"
|
||||
} else {
|
||||
"FALSE"
|
||||
}
|
||||
));
|
||||
s.push_str(&format!(" lineweight={}\n", entry.lineweight));
|
||||
s.push_str(&format!(
|
||||
@@ -756,12 +767,8 @@ fn parse_plot_style_text(text: &str, name: String, is_stb: bool) -> Result<PlotS
|
||||
"physical_pen_number" => {
|
||||
style.entry.physical_pen_number = value.parse().unwrap_or(0)
|
||||
}
|
||||
"virtual_pen_number" => {
|
||||
style.entry.virtual_pen_number = value.parse().unwrap_or(0)
|
||||
}
|
||||
"linepattern_size" => {
|
||||
style.entry.linepattern_size = value.parse().unwrap_or(0.5)
|
||||
}
|
||||
"virtual_pen_number" => style.entry.virtual_pen_number = value.parse().unwrap_or(0),
|
||||
"linepattern_size" => style.entry.linepattern_size = value.parse().unwrap_or(0.5),
|
||||
"linetype" => style.entry.linetype = value.parse().unwrap_or(31),
|
||||
"adaptive_linetype" => {
|
||||
style.entry.adaptive_linetype = value.eq_ignore_ascii_case("TRUE")
|
||||
@@ -974,14 +981,20 @@ mod lenient_loading_tests {
|
||||
bytes[52..56].copy_from_slice(&(text_len - 1).to_le_bytes());
|
||||
let table = PlotStyleTable::from_bytes("foreign.ctb", &bytes).expect("loads");
|
||||
assert_pens_kept(&table);
|
||||
assert!(table.load_warnings.iter().any(|w| w.contains("bytes of text")));
|
||||
assert!(table
|
||||
.load_warnings
|
||||
.iter()
|
||||
.any(|w| w.contains("bytes of text")));
|
||||
|
||||
let mut bytes = compressed(&table_with_description("ok"));
|
||||
let compressed_len = u32::from_le_bytes(bytes[56..60].try_into().unwrap());
|
||||
bytes[56..60].copy_from_slice(&(compressed_len + 100).to_le_bytes());
|
||||
let table = PlotStyleTable::from_bytes("foreign.ctb", &bytes).expect("loads");
|
||||
assert_pens_kept(&table);
|
||||
assert!(table.load_warnings.iter().any(|w| w.contains("compressed bytes")));
|
||||
assert!(table
|
||||
.load_warnings
|
||||
.iter()
|
||||
.any(|w| w.contains("compressed bytes")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -999,7 +1012,10 @@ mod lenient_loading_tests {
|
||||
let table = PlotStyleTable::from_bytes("foreign.ctb", &compressed).expect("loads");
|
||||
assert_pens_kept(&table);
|
||||
assert_eq!(table.description, "Plumas – señal");
|
||||
assert!(table.load_warnings.iter().any(|w| w.contains("Windows-1252")));
|
||||
assert!(table
|
||||
.load_warnings
|
||||
.iter()
|
||||
.any(|w| w.contains("Windows-1252")));
|
||||
// Clean UTF-8 gets no note.
|
||||
let (text, note) = decode_plot_style_text("Ελληνικά".as_bytes().to_vec());
|
||||
assert_eq!(text, "Ελληνικά");
|
||||
@@ -1094,7 +1110,10 @@ mod inflate_limit_tests {
|
||||
flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::default());
|
||||
encoder.write_all(&bomb_plaintext()).expect("compress");
|
||||
let payload = encoder.finish().expect("finish");
|
||||
assert_ne!(payload[0], 0x78, "raw deflate takes the legacy DeflateDecoder path");
|
||||
assert_ne!(
|
||||
payload[0], 0x78,
|
||||
"raw deflate takes the legacy DeflateDecoder path"
|
||||
);
|
||||
|
||||
let mut data = vec![b'\n'];
|
||||
data.extend(payload);
|
||||
|
||||
+249
-100
@@ -129,9 +129,8 @@ pub fn printer_report() -> Vec<String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
match pdf_printto_command() {
|
||||
Some(command) => lines.push(
|
||||
crate::tf!("Plots go through the PDF application: {command}").into_owned(),
|
||||
),
|
||||
Some(command) => lines
|
||||
.push(crate::tf!("Plots go through the PDF application: {command}").into_owned()),
|
||||
None => lines.push(
|
||||
crate::t!(
|
||||
"Plots go straight to the printer (no PDF application registers a print verb)."
|
||||
@@ -159,7 +158,11 @@ pub fn printer_media_report(printer: &str) -> Vec<String> {
|
||||
lines.push(crate::tf!("{printer}: {count} sheet(s) reported").into_owned());
|
||||
for media in &caps.media {
|
||||
let m = media.margins;
|
||||
let borderless = if media.borderless { " (borderless available)" } else { "" };
|
||||
let borderless = if media.borderless {
|
||||
" (borderless available)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
lines.push(format!(
|
||||
" {} — margins L {:.1} B {:.1} R {:.1} T {:.1} mm{borderless}",
|
||||
media.paper.display(),
|
||||
@@ -230,8 +233,12 @@ pub(crate) fn pdf_printto_command() -> Option<String> {
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use windows_sys::Win32::UI::Shell::{AssocQueryStringW, ASSOCF_NOTRUNCATE, ASSOCSTR_COMMAND};
|
||||
|
||||
let wide =
|
||||
|s: &str| -> Vec<u16> { std::ffi::OsStr::new(s).encode_wide().chain(Some(0)).collect() };
|
||||
let wide = |s: &str| -> Vec<u16> {
|
||||
std::ffi::OsStr::new(s)
|
||||
.encode_wide()
|
||||
.chain(Some(0))
|
||||
.collect()
|
||||
};
|
||||
let extension = wide(".pdf");
|
||||
let verb = wide("printto");
|
||||
let mut len = 0u32;
|
||||
@@ -320,15 +327,21 @@ mod print_route_tests {
|
||||
fn a_named_printer_is_kept_and_blank_means_the_default() {
|
||||
assert_eq!(
|
||||
plan_print_route(Some("Office Laser"), Some("Home Inkjet"), false),
|
||||
Ok(PrintRoute::Direct { printer: "Office Laser".into() })
|
||||
Ok(PrintRoute::Direct {
|
||||
printer: "Office Laser".into()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
plan_print_route(Some(" "), Some("Home Inkjet"), false),
|
||||
Ok(PrintRoute::Direct { printer: "Home Inkjet".into() })
|
||||
Ok(PrintRoute::Direct {
|
||||
printer: "Home Inkjet".into()
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
plan_print_route(None, Some(" Home Inkjet "), true),
|
||||
Ok(PrintRoute::PdfApplication { printer: "Home Inkjet".into() })
|
||||
Ok(PrintRoute::PdfApplication {
|
||||
printer: "Home Inkjet".into()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -500,9 +513,7 @@ mod printer_buffer_tests {
|
||||
/// Build the platform printer-properties command.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn printer_properties_command(printer: Option<&str>) -> (&'static str, Vec<String>) {
|
||||
let named = printer
|
||||
.map(str::trim)
|
||||
.filter(|name| !name.is_empty());
|
||||
let named = printer.map(str::trim).filter(|name| !name.is_empty());
|
||||
|
||||
// `/e` opens the driver's *printing preferences* (paper, tray, duplex,
|
||||
// quality — what a print job honours), not the device's admin properties
|
||||
@@ -672,7 +683,10 @@ fn with_printer_devmode(
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum DevmodeSheet {
|
||||
Id(u16),
|
||||
UserMm { width_tenths: i16, length_tenths: i16 },
|
||||
UserMm {
|
||||
width_tenths: i16,
|
||||
length_tenths: i16,
|
||||
},
|
||||
}
|
||||
|
||||
/// Fetch the printer driver's DEVMODE, flip its orientation to match the
|
||||
@@ -688,8 +702,8 @@ fn oriented_printer_devmode(
|
||||
sheet: Option<DevmodeSheet>,
|
||||
) -> Option<Vec<u8>> {
|
||||
use windows_sys::Win32::Graphics::Gdi::{
|
||||
DEVMODEW, DM_IN_BUFFER, DM_ORIENTATION, DM_OUT_BUFFER, DM_PAPERLENGTH, DM_PAPERSIZE,
|
||||
DM_PAPERWIDTH, DMORIENT_LANDSCAPE, DMORIENT_PORTRAIT,
|
||||
DEVMODEW, DMORIENT_LANDSCAPE, DMORIENT_PORTRAIT, DM_IN_BUFFER, DM_ORIENTATION,
|
||||
DM_OUT_BUFFER, DM_PAPERLENGTH, DM_PAPERSIZE, DM_PAPERWIDTH,
|
||||
};
|
||||
use windows_sys::Win32::Graphics::Printing::DocumentPropertiesW;
|
||||
let attempt = |sheet: Option<DevmodeSheet>| {
|
||||
@@ -707,7 +721,10 @@ fn oriented_printer_devmode(
|
||||
(*dm).dmFields &= !(DM_PAPERWIDTH | DM_PAPERLENGTH);
|
||||
(*dm).Anonymous1.Anonymous1.dmPaperSize = id as i16;
|
||||
}
|
||||
Some(DevmodeSheet::UserMm { width_tenths, length_tenths }) => {
|
||||
Some(DevmodeSheet::UserMm {
|
||||
width_tenths,
|
||||
length_tenths,
|
||||
}) => {
|
||||
(*dm).dmFields |= DM_PAPERSIZE | DM_PAPERWIDTH | DM_PAPERLENGTH;
|
||||
(*dm).Anonymous1.Anonymous1.dmPaperSize =
|
||||
crate::io::windows_media::DMPAPER_USER as i16;
|
||||
@@ -776,16 +793,20 @@ pub(crate) fn windows_printer_media(
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use windows_sys::Win32::Foundation::POINT;
|
||||
use windows_sys::Win32::Graphics::Gdi::{
|
||||
CreateDCW, DeleteDC, GetDeviceCaps, DEVMODEW, DM_PAPERLENGTH, DM_PAPERSIZE,
|
||||
DM_PAPERWIDTH, HORZRES, LOGPIXELSX, LOGPIXELSY, PHYSICALHEIGHT, PHYSICALOFFSETX,
|
||||
PHYSICALOFFSETY, PHYSICALWIDTH, VERTRES,
|
||||
CreateDCW, DeleteDC, GetDeviceCaps, DEVMODEW, DM_PAPERLENGTH, DM_PAPERSIZE, DM_PAPERWIDTH,
|
||||
HORZRES, LOGPIXELSX, LOGPIXELSY, PHYSICALHEIGHT, PHYSICALOFFSETX, PHYSICALOFFSETY,
|
||||
PHYSICALWIDTH, VERTRES,
|
||||
};
|
||||
use windows_sys::Win32::Storage::Xps::{
|
||||
DeviceCapabilitiesW, DC_PAPERNAMES, DC_PAPERS, DC_PAPERSIZE,
|
||||
};
|
||||
|
||||
let wide =
|
||||
|s: &str| -> Vec<u16> { std::ffi::OsStr::new(s).encode_wide().chain(Some(0)).collect() };
|
||||
let wide = |s: &str| -> Vec<u16> {
|
||||
std::ffi::OsStr::new(s)
|
||||
.encode_wide()
|
||||
.chain(Some(0))
|
||||
.collect()
|
||||
};
|
||||
let device_wide = wide(printer.trim());
|
||||
// SAFETY: the device name is NUL-terminated; a null output buffer asks
|
||||
// for the count only.
|
||||
@@ -849,8 +870,10 @@ pub(crate) fn windows_printer_media(
|
||||
let fields = dm.dmFields;
|
||||
let paper = unsafe { dm.Anonymous1.Anonymous1 };
|
||||
let id = (fields & DM_PAPERSIZE != 0).then_some(paper.dmPaperSize as u16);
|
||||
let size = (fields & DM_PAPERWIDTH != 0 && fields & DM_PAPERLENGTH != 0)
|
||||
.then_some((i32::from(paper.dmPaperWidth), i32::from(paper.dmPaperLength)));
|
||||
let size = (fields & DM_PAPERWIDTH != 0 && fields & DM_PAPERLENGTH != 0).then_some((
|
||||
i32::from(paper.dmPaperWidth),
|
||||
i32::from(paper.dmPaperLength),
|
||||
));
|
||||
(id, size)
|
||||
})
|
||||
.unwrap_or((None, None));
|
||||
@@ -861,7 +884,14 @@ pub(crate) fn windows_printer_media(
|
||||
.map(|b| b.as_ptr().cast::<DEVMODEW>())
|
||||
.unwrap_or(std::ptr::null());
|
||||
// SAFETY: the strings are NUL-terminated and `devmode` outlives the DC.
|
||||
let hdc = unsafe { CreateDCW(winspool.as_ptr(), device_wide.as_ptr(), std::ptr::null(), init) };
|
||||
let hdc = unsafe {
|
||||
CreateDCW(
|
||||
winspool.as_ptr(),
|
||||
device_wide.as_ptr(),
|
||||
std::ptr::null(),
|
||||
init,
|
||||
)
|
||||
};
|
||||
let margins = if hdc.is_null() {
|
||||
None
|
||||
} else {
|
||||
@@ -901,14 +931,19 @@ fn gdi_raster_print(
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use windows_sys::Win32::Graphics::Gdi::{
|
||||
CreateDCW, DeleteDC, GetDeviceCaps, SetStretchBltMode, StretchDIBits, BITMAPINFO,
|
||||
BITMAPINFOHEADER, BI_RGB, COLORONCOLOR, DIB_RGB_COLORS, DEVMODEW, HORZRES, SRCCOPY,
|
||||
BITMAPINFOHEADER, BI_RGB, COLORONCOLOR, DEVMODEW, DIB_RGB_COLORS, HORZRES, SRCCOPY,
|
||||
VERTRES,
|
||||
};
|
||||
// windows-sys 0.61 groups the spooler-document calls (StartDoc/StartPage/
|
||||
// EndPage/EndDoc/DOCINFOW) under Storage::Xps, not Graphics::Gdi.
|
||||
use windows_sys::Win32::Storage::Xps::{EndDoc, EndPage, StartDocW, StartPage, DOCINFOW};
|
||||
|
||||
let wide = |s: &str| -> Vec<u16> { std::ffi::OsStr::new(s).encode_wide().chain(Some(0)).collect() };
|
||||
let wide = |s: &str| -> Vec<u16> {
|
||||
std::ffi::OsStr::new(s)
|
||||
.encode_wide()
|
||||
.chain(Some(0))
|
||||
.collect()
|
||||
};
|
||||
|
||||
// The device is resolved by the caller (`plan_print_route`): a name the
|
||||
// dialog chose or the system default looked up by name.
|
||||
@@ -944,7 +979,12 @@ fn gdi_raster_print(
|
||||
// SAFETY: all pointers are NUL-terminated / valid for the call; the DC is
|
||||
// released below and dm_buf outlives it.
|
||||
let hdc = unsafe {
|
||||
CreateDCW(winspool.as_ptr(), device_wide.as_ptr(), std::ptr::null(), init)
|
||||
CreateDCW(
|
||||
winspool.as_ptr(),
|
||||
device_wide.as_ptr(),
|
||||
std::ptr::null(),
|
||||
init,
|
||||
)
|
||||
};
|
||||
if hdc.is_null() {
|
||||
return Err(format!("Could not open printer \"{device}\" for printing."));
|
||||
@@ -952,8 +992,12 @@ fn gdi_raster_print(
|
||||
|
||||
let result = (|| -> Result<(), String> {
|
||||
// SAFETY: hdc is a valid printer DC; each capability is a documented index.
|
||||
let (page_w, page_h) =
|
||||
unsafe { (GetDeviceCaps(hdc, HORZRES as i32), GetDeviceCaps(hdc, VERTRES as i32)) };
|
||||
let (page_w, page_h) = unsafe {
|
||||
(
|
||||
GetDeviceCaps(hdc, HORZRES as i32),
|
||||
GetDeviceCaps(hdc, VERTRES as i32),
|
||||
)
|
||||
};
|
||||
if page_w <= 0 || page_h <= 0 {
|
||||
return Err("Printer reported no printable area.".into());
|
||||
}
|
||||
@@ -1001,10 +1045,9 @@ fn gdi_raster_print(
|
||||
};
|
||||
// SAFETY: info and its strings outlive every StartDoc/EndDoc call here.
|
||||
if unsafe { StartDocW(hdc, &info) } <= 0 {
|
||||
return Err(format!(
|
||||
"Print job could not start (code {}).",
|
||||
unsafe { windows_sys::Win32::Foundation::GetLastError() }
|
||||
));
|
||||
return Err(format!("Print job could not start (code {}).", unsafe {
|
||||
windows_sys::Win32::Foundation::GetLastError()
|
||||
}));
|
||||
}
|
||||
unsafe { SetStretchBltMode(hdc, COLORONCOLOR) };
|
||||
let mut bmi = BITMAPINFO {
|
||||
@@ -1023,10 +1066,9 @@ fn gdi_raster_print(
|
||||
};
|
||||
for _ in 0..copies.max(1) {
|
||||
if unsafe { StartPage(hdc) } <= 0 {
|
||||
return Err(format!(
|
||||
"Could not start a print page (code {}).",
|
||||
unsafe { windows_sys::Win32::Foundation::GetLastError() }
|
||||
));
|
||||
return Err(format!("Could not start a print page (code {}).", unsafe {
|
||||
windows_sys::Win32::Foundation::GetLastError()
|
||||
}));
|
||||
}
|
||||
// StartPage resets DC attributes, so the stretch mode is set per page.
|
||||
unsafe { SetStretchBltMode(hdc, COLORONCOLOR) };
|
||||
@@ -1063,10 +1105,9 @@ fn gdi_raster_print(
|
||||
}
|
||||
}
|
||||
if unsafe { EndDoc(hdc) } <= 0 {
|
||||
return Err(format!(
|
||||
"Print job could not complete (code {}).",
|
||||
unsafe { windows_sys::Win32::Foundation::GetLastError() }
|
||||
));
|
||||
return Err(format!("Print job could not complete (code {}).", unsafe {
|
||||
windows_sys::Win32::Foundation::GetLastError()
|
||||
}));
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
@@ -1083,8 +1124,7 @@ pub fn open_in_viewer(path: &std::path::Path) -> Result<(), String> {
|
||||
// the user's PDF application or leave generated previews in the temp
|
||||
// directory. Production viewers need the file after this call returns;
|
||||
// the test path has no external consumer, so it can be removed now.
|
||||
std::fs::remove_file(path)
|
||||
.map_err(|error| format!("Could not consume test preview: {error}"))
|
||||
std::fs::remove_file(path).map_err(|error| format!("Could not consume test preview: {error}"))
|
||||
}
|
||||
|
||||
/// Open a file with the OS default application (used for print preview).
|
||||
@@ -1123,7 +1163,11 @@ mod preview_isolation_tests {
|
||||
|
||||
super::open_in_viewer(&path).unwrap();
|
||||
|
||||
assert!(!path.exists(), "test preview was not cleaned up: {}", path.display());
|
||||
assert!(
|
||||
!path.exists(),
|
||||
"test preview was not cleaned up: {}",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1136,7 +1180,7 @@ fn shell_print_to(path: &std::path::Path, printer: &str, copies: u32) -> Result<
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use windows_sys::Win32::Foundation::{GetLastError, ERROR_NO_ASSOCIATION};
|
||||
use windows_sys::Win32::UI::Shell::{
|
||||
ShellExecuteExW, SHELLEXECUTEINFOW, SEE_MASK_FLAG_NO_UI, SEE_MASK_NOASYNC, SE_ERR_NOASSOC,
|
||||
ShellExecuteExW, SEE_MASK_FLAG_NO_UI, SEE_MASK_NOASYNC, SE_ERR_NOASSOC, SHELLEXECUTEINFOW,
|
||||
};
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::SW_HIDE;
|
||||
|
||||
@@ -1173,10 +1217,7 @@ fn shell_print_to(path: &std::path::Path, printer: &str, copies: u32) -> Result<
|
||||
|
||||
/// Dispatch a PDF to a specific printer with [`PrintOptions`].
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn dispatch_to_printer_opts(
|
||||
path: &std::path::Path,
|
||||
opts: &PrintOptions,
|
||||
) -> Result<String, String> {
|
||||
fn dispatch_to_printer_opts(path: &std::path::Path, opts: &PrintOptions) -> Result<String, String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let default_printer = windows_default_printer().ok();
|
||||
@@ -1244,20 +1285,17 @@ fn dispatch_to_printer_opts(
|
||||
for (key, value) in &opts.driver_options {
|
||||
cmd.arg("-o").arg(format!("{key}={value}"));
|
||||
}
|
||||
let lp_result = cmd
|
||||
.arg("--")
|
||||
.arg(path_str.as_ref())
|
||||
.output();
|
||||
let lp_result = cmd.arg("--").arg(path_str.as_ref()).output();
|
||||
if let Ok(out) = &lp_result {
|
||||
if !out.status.success() {
|
||||
// Continue to the lpr fallback below.
|
||||
} else {
|
||||
let msg = String::from_utf8_lossy(&out.stdout);
|
||||
let printer = msg
|
||||
.split_whitespace()
|
||||
.find(|w| w.contains('-'))
|
||||
.unwrap_or("printer")
|
||||
.to_string();
|
||||
let msg = String::from_utf8_lossy(&out.stdout);
|
||||
let printer = msg
|
||||
.split_whitespace()
|
||||
.find(|w| w.contains('-'))
|
||||
.unwrap_or("printer")
|
||||
.to_string();
|
||||
return Ok(printer);
|
||||
}
|
||||
}
|
||||
@@ -1324,7 +1362,10 @@ mod printer_properties_tests {
|
||||
// 210mm at 300 DPI ≈ 2480 px, aspect 210:148.
|
||||
assert!((page.width as f64 - 210.0 / 25.4 * 300.0).abs() < 2.0);
|
||||
assert!((page.height as f64 - 148.0 / 25.4 * 300.0).abs() < 2.0);
|
||||
assert_eq!(page.pixels.len(), page.width as usize * page.height as usize * 4);
|
||||
assert_eq!(
|
||||
page.pixels.len(),
|
||||
page.width as usize * page.height as usize * 4
|
||||
);
|
||||
}
|
||||
|
||||
/// Opening the printer DC and reading its printable area must work with
|
||||
@@ -1338,20 +1379,36 @@ mod printer_properties_tests {
|
||||
};
|
||||
|
||||
let wide = |s: &str| -> Vec<u16> {
|
||||
std::ffi::OsStr::new(s).encode_wide().chain(Some(0)).collect()
|
||||
std::ffi::OsStr::new(s)
|
||||
.encode_wide()
|
||||
.chain(Some(0))
|
||||
.collect()
|
||||
};
|
||||
let winspool = wide("WINSPOOL");
|
||||
let device = wide("Microsoft Print to PDF");
|
||||
// SAFETY: NUL-terminated strings; the DC is released before returning.
|
||||
let hdc = unsafe {
|
||||
CreateDCW(winspool.as_ptr(), device.as_ptr(), std::ptr::null(), std::ptr::null())
|
||||
CreateDCW(
|
||||
winspool.as_ptr(),
|
||||
device.as_ptr(),
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
)
|
||||
};
|
||||
assert!(!hdc.is_null(), "printer DC should open");
|
||||
// SAFETY: capability indexes on a valid DC.
|
||||
let (w, h) = unsafe { (GetDeviceCaps(hdc, HORZRES as i32), GetDeviceCaps(hdc, VERTRES as i32)) };
|
||||
let (w, h) = unsafe {
|
||||
(
|
||||
GetDeviceCaps(hdc, HORZRES as i32),
|
||||
GetDeviceCaps(hdc, VERTRES as i32),
|
||||
)
|
||||
};
|
||||
// SAFETY: releasing the DC opened above.
|
||||
unsafe { DeleteDC(hdc) };
|
||||
assert!(w > 0 && h > 0, "printable area should be reported, got {w}x{h}");
|
||||
assert!(
|
||||
w > 0 && h > 0,
|
||||
"printable area should be reported, got {w}x{h}"
|
||||
);
|
||||
}
|
||||
|
||||
/// End to end through the real spooler: plot a page with black ink, run
|
||||
@@ -1368,7 +1425,12 @@ mod printer_properties_tests {
|
||||
let mut ops = Vec::new();
|
||||
// A big black block in the middle of an A4 page.
|
||||
ops.push(printpdf::Op::SetFillColor {
|
||||
col: Color::Rgb(Rgb { r: 0.0, g: 0.0, b: 0.0, icc_profile: None }),
|
||||
col: Color::Rgb(Rgb {
|
||||
r: 0.0,
|
||||
g: 0.0,
|
||||
b: 0.0,
|
||||
icc_profile: None,
|
||||
}),
|
||||
});
|
||||
// printpdf 0.9's DrawRectangle never paints (its serializer always
|
||||
// ends the path with `n`), so the ink must be a filled polygon — the
|
||||
@@ -1377,10 +1439,34 @@ mod printer_properties_tests {
|
||||
polygon: printpdf::Polygon {
|
||||
rings: vec![printpdf::PolygonRing {
|
||||
points: vec![
|
||||
printpdf::LinePoint { p: printpdf::Point { x: Mm(60.0).into(), y: Mm(60.0).into() }, bezier: false },
|
||||
printpdf::LinePoint { p: printpdf::Point { x: Mm(150.0).into(), y: Mm(60.0).into() }, bezier: false },
|
||||
printpdf::LinePoint { p: printpdf::Point { x: Mm(150.0).into(), y: Mm(100.0).into() }, bezier: false },
|
||||
printpdf::LinePoint { p: printpdf::Point { x: Mm(60.0).into(), y: Mm(100.0).into() }, bezier: false },
|
||||
printpdf::LinePoint {
|
||||
p: printpdf::Point {
|
||||
x: Mm(60.0).into(),
|
||||
y: Mm(60.0).into(),
|
||||
},
|
||||
bezier: false,
|
||||
},
|
||||
printpdf::LinePoint {
|
||||
p: printpdf::Point {
|
||||
x: Mm(150.0).into(),
|
||||
y: Mm(60.0).into(),
|
||||
},
|
||||
bezier: false,
|
||||
},
|
||||
printpdf::LinePoint {
|
||||
p: printpdf::Point {
|
||||
x: Mm(150.0).into(),
|
||||
y: Mm(100.0).into(),
|
||||
},
|
||||
bezier: false,
|
||||
},
|
||||
printpdf::LinePoint {
|
||||
p: printpdf::Point {
|
||||
x: Mm(60.0).into(),
|
||||
y: Mm(100.0).into(),
|
||||
},
|
||||
bezier: false,
|
||||
},
|
||||
],
|
||||
}],
|
||||
mode: printpdf::PaintMode::Fill,
|
||||
@@ -1402,11 +1488,8 @@ mod printer_properties_tests {
|
||||
|
||||
// The spooled PDF must contain the black block: rasterise it and
|
||||
// count clearly-dark pixels.
|
||||
let page = crate::scene::model::pdf_raster::rasterize_page(
|
||||
&printed.to_string_lossy(),
|
||||
"1",
|
||||
)
|
||||
.expect("spooled output should rasterise");
|
||||
let page = crate::scene::model::pdf_raster::rasterize_page(&printed.to_string_lossy(), "1")
|
||||
.expect("spooled output should rasterise");
|
||||
let dark = page
|
||||
.pixels
|
||||
.chunks_exact(4)
|
||||
@@ -1432,16 +1515,45 @@ mod printer_properties_tests {
|
||||
let mut document = PdfDocument::new("gdi orientation test");
|
||||
let mut ops = Vec::new();
|
||||
ops.push(printpdf::Op::SetFillColor {
|
||||
col: Color::Rgb(Rgb { r: 0.0, g: 0.0, b: 0.0, icc_profile: None }),
|
||||
col: Color::Rgb(Rgb {
|
||||
r: 0.0,
|
||||
g: 0.0,
|
||||
b: 0.0,
|
||||
icc_profile: None,
|
||||
}),
|
||||
});
|
||||
ops.push(printpdf::Op::DrawPolygon {
|
||||
polygon: printpdf::Polygon {
|
||||
rings: vec![printpdf::PolygonRing {
|
||||
points: vec![
|
||||
printpdf::LinePoint { p: printpdf::Point { x: Mm(20.0).into(), y: Mm(20.0).into() }, bezier: false },
|
||||
printpdf::LinePoint { p: printpdf::Point { x: Mm(270.0).into(), y: Mm(20.0).into() }, bezier: false },
|
||||
printpdf::LinePoint { p: printpdf::Point { x: Mm(270.0).into(), y: Mm(45.0).into() }, bezier: false },
|
||||
printpdf::LinePoint { p: printpdf::Point { x: Mm(20.0).into(), y: Mm(45.0).into() }, bezier: false },
|
||||
printpdf::LinePoint {
|
||||
p: printpdf::Point {
|
||||
x: Mm(20.0).into(),
|
||||
y: Mm(20.0).into(),
|
||||
},
|
||||
bezier: false,
|
||||
},
|
||||
printpdf::LinePoint {
|
||||
p: printpdf::Point {
|
||||
x: Mm(270.0).into(),
|
||||
y: Mm(20.0).into(),
|
||||
},
|
||||
bezier: false,
|
||||
},
|
||||
printpdf::LinePoint {
|
||||
p: printpdf::Point {
|
||||
x: Mm(270.0).into(),
|
||||
y: Mm(45.0).into(),
|
||||
},
|
||||
bezier: false,
|
||||
},
|
||||
printpdf::LinePoint {
|
||||
p: printpdf::Point {
|
||||
x: Mm(20.0).into(),
|
||||
y: Mm(45.0).into(),
|
||||
},
|
||||
bezier: false,
|
||||
},
|
||||
],
|
||||
}],
|
||||
mode: printpdf::PaintMode::Fill,
|
||||
@@ -1449,7 +1561,9 @@ mod printer_properties_tests {
|
||||
},
|
||||
});
|
||||
// Landscape A4: 297 × 210 mm.
|
||||
document.pages.push(OutputPage::new(Mm(297.0), Mm(210.0), ops));
|
||||
document
|
||||
.pages
|
||||
.push(OutputPage::new(Mm(297.0), Mm(210.0), ops));
|
||||
let bytes = document.save(&PdfSaveOptions::default(), &mut Vec::new());
|
||||
let plot = std::env::temp_dir().join("ocs_gdi_orientation_source.pdf");
|
||||
std::fs::write(&plot, &bytes).unwrap();
|
||||
@@ -1459,11 +1573,8 @@ mod printer_properties_tests {
|
||||
let result = super::gdi_raster_print(&plot, "Microsoft Print to PDF", 1, Some(&printed));
|
||||
assert!(result.is_ok(), "GDI print failed: {result:?}");
|
||||
|
||||
let page = crate::scene::model::pdf_raster::rasterize_page(
|
||||
&printed.to_string_lossy(),
|
||||
"1",
|
||||
)
|
||||
.expect("spooled output should rasterise");
|
||||
let page = crate::scene::model::pdf_raster::rasterize_page(&printed.to_string_lossy(), "1")
|
||||
.expect("spooled output should rasterise");
|
||||
std::fs::remove_file(&plot).ok();
|
||||
std::fs::remove_file(&printed).ok();
|
||||
|
||||
@@ -1477,7 +1588,8 @@ mod printer_properties_tests {
|
||||
let mut dark = 0usize;
|
||||
for y in 0..page.height {
|
||||
for x in 0..page.width {
|
||||
let px = &page.pixels[((y * page.width + x) * 4) as usize..((y * page.width + x) * 4 + 3) as usize];
|
||||
let px = &page.pixels
|
||||
[((y * page.width + x) * 4) as usize..((y * page.width + x) * 4 + 3) as usize];
|
||||
if px.iter().all(|&c| c < 100) {
|
||||
dark += 1;
|
||||
min_x = min_x.min(x);
|
||||
@@ -1487,7 +1599,10 @@ mod printer_properties_tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(dark > 1_000, "printed page must carry ink; {dark} dark pixels");
|
||||
assert!(
|
||||
dark > 1_000,
|
||||
"printed page must carry ink; {dark} dark pixels"
|
||||
);
|
||||
let ink_w = (max_x - min_x + 1) as f64;
|
||||
let ink_h = (max_y - min_y + 1) as f64;
|
||||
assert_eq!(
|
||||
@@ -1547,7 +1662,10 @@ mod printer_properties_tests {
|
||||
);
|
||||
// SAFETY: the buffer holds at least a public DEVMODEW, just checked.
|
||||
let named = unsafe { (*(devmode.as_ptr() as *const DEVMODEW)).dmSize } as usize;
|
||||
assert!(named > 0 && named <= devmode.len(), "dmSize {named} outside the buffer");
|
||||
assert!(
|
||||
named > 0 && named <= devmode.len(),
|
||||
"dmSize {named} outside the buffer"
|
||||
);
|
||||
|
||||
// And the symptom the lost DEVMODE produced: the driver's default
|
||||
// sheet is one of the sheets it lists, so the report can name it.
|
||||
@@ -1589,7 +1707,10 @@ mod printer_properties_tests {
|
||||
);
|
||||
// SAFETY: the printer branch of the DEVMODE union.
|
||||
let got = unsafe { dm.Anonymous1.Anonymous1.dmOrientation };
|
||||
assert_eq!(got, want as i16, "landscape={landscape}: wrong dmOrientation");
|
||||
assert_eq!(
|
||||
got, want as i16,
|
||||
"landscape={landscape}: wrong dmOrientation"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1601,13 +1722,30 @@ mod printer_properties_tests {
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn windows_preferences_keep_the_printer_name_in_one_argument() {
|
||||
for name in ["Office Printer", "Wide Format Plotter 1200", "Εκτυπωτής γραφείου"] {
|
||||
for name in [
|
||||
"Office Printer",
|
||||
"Wide Format Plotter 1200",
|
||||
"Εκτυπωτής γραφείου",
|
||||
] {
|
||||
let (program, args) = printer_properties_command(Some(name));
|
||||
assert_eq!(program, "rundll32.exe");
|
||||
assert!(args.contains(&"/e".to_string()), "{args:?} must ask for preferences");
|
||||
assert!(!args.contains(&"/p".to_string()), "{args:?} must not ask for properties");
|
||||
let flag = args.iter().position(|arg| arg == "/n").expect("/n introduces the name");
|
||||
assert_eq!(args.get(flag + 1).map(String::as_str), Some(name), "{args:?}");
|
||||
assert!(
|
||||
args.contains(&"/e".to_string()),
|
||||
"{args:?} must ask for preferences"
|
||||
);
|
||||
assert!(
|
||||
!args.contains(&"/p".to_string()),
|
||||
"{args:?} must not ask for properties"
|
||||
);
|
||||
let flag = args
|
||||
.iter()
|
||||
.position(|arg| arg == "/n")
|
||||
.expect("/n introduces the name");
|
||||
assert_eq!(
|
||||
args.get(flag + 1).map(String::as_str),
|
||||
Some(name),
|
||||
"{args:?}"
|
||||
);
|
||||
assert_eq!(args.iter().filter(|arg| arg.as_str() == name).count(), 1);
|
||||
}
|
||||
}
|
||||
@@ -1617,9 +1755,7 @@ mod printer_properties_tests {
|
||||
fn macos_opens_print_settings() {
|
||||
let expected = (
|
||||
"open",
|
||||
vec![
|
||||
"x-apple.systempreferences:com.apple.Print-Scan-Settings.extension".to_string(),
|
||||
],
|
||||
vec!["x-apple.systempreferences:com.apple.Print-Scan-Settings.extension".to_string()],
|
||||
);
|
||||
assert_eq!(
|
||||
printer_properties_command(Some(" Office LaserJet ")),
|
||||
@@ -1751,7 +1887,11 @@ pub fn parse_lpoptions(text: &str) -> Vec<PrinterOption> {
|
||||
}
|
||||
options.push(PrinterOption {
|
||||
key: key.to_string(),
|
||||
label: if label.is_empty() { key.to_string() } else { label.to_string() },
|
||||
label: if label.is_empty() {
|
||||
key.to_string()
|
||||
} else {
|
||||
label.to_string()
|
||||
},
|
||||
default,
|
||||
choices,
|
||||
});
|
||||
@@ -1835,7 +1975,13 @@ Broken line without colon\n";
|
||||
// PageSize is the plot dialog's business; everything else is listed.
|
||||
assert_eq!(
|
||||
keys,
|
||||
["MediaType", "ColorModel", "Duplex", "cupsPrintQuality", "print-scaling"]
|
||||
[
|
||||
"MediaType",
|
||||
"ColorModel",
|
||||
"Duplex",
|
||||
"cupsPrintQuality",
|
||||
"print-scaling"
|
||||
]
|
||||
);
|
||||
let media = &options[0];
|
||||
assert_eq!(media.label, "Media Type");
|
||||
@@ -1855,7 +2001,10 @@ Broken line without colon\n";
|
||||
assert_eq!(humanize_keyword("AutoGray"), "Auto Gray");
|
||||
assert_eq!(humanize_keyword("ProcessGray"), "Process Gray");
|
||||
assert_eq!(humanize_keyword("auto-fit"), "auto-fit");
|
||||
assert_eq!(humanize_keyword("PhotographicSemiGloss"), "Photographic Semi Gloss");
|
||||
assert_eq!(
|
||||
humanize_keyword("PhotographicSemiGloss"),
|
||||
"Photographic Semi Gloss"
|
||||
);
|
||||
assert_eq!(humanize_keyword("A4"), "A4");
|
||||
}
|
||||
}
|
||||
+28
-25
@@ -107,11 +107,7 @@ impl RecoveryReport {
|
||||
.filter_map(|(index, item)| {
|
||||
let mut stats = item.read_stats.clone()?;
|
||||
for diagnostic in &mut stats.diagnostics {
|
||||
diagnostic.message = redact_known_paths(
|
||||
&diagnostic.message,
|
||||
path,
|
||||
references,
|
||||
);
|
||||
diagnostic.message = redact_known_paths(&diagnostic.message, path, references);
|
||||
}
|
||||
Some((format!("reference-{:02}", index + 1), stats))
|
||||
})
|
||||
@@ -123,8 +119,7 @@ impl RecoveryReport {
|
||||
notifications
|
||||
.iter()
|
||||
.filter(|item| {
|
||||
item.notification_type
|
||||
== codec::notification::NotificationType::Error
|
||||
item.notification_type == codec::notification::NotificationType::Error
|
||||
})
|
||||
.count()
|
||||
});
|
||||
@@ -138,13 +133,16 @@ impl RecoveryReport {
|
||||
})
|
||||
.collect();
|
||||
if notifications.omitted_count() > 0 {
|
||||
diagnostics.insert(0, (
|
||||
"truncated".to_string(),
|
||||
format!(
|
||||
"{} additional parser notifications were omitted after the safety limit",
|
||||
notifications.omitted_count()
|
||||
diagnostics.insert(
|
||||
0,
|
||||
(
|
||||
"truncated".to_string(),
|
||||
format!(
|
||||
"{} additional parser notifications were omitted after the safety limit",
|
||||
notifications.omitted_count()
|
||||
),
|
||||
),
|
||||
));
|
||||
);
|
||||
}
|
||||
for (index, reference) in references.iter().enumerate() {
|
||||
diagnostics.extend(reference.diagnostics.iter().map(|message| {
|
||||
@@ -262,7 +260,10 @@ impl RecoveryReport {
|
||||
lines.push(format!("Reader version: {}", codec::VERSION));
|
||||
lines.push(format!("Reader revision: {}", reader_revision()));
|
||||
lines.push(format!("Platform: {}", diagnostic_platform()));
|
||||
lines.push(format!("Created (Unix seconds): {}", self.created_unix_seconds));
|
||||
lines.push(format!(
|
||||
"Created (Unix seconds): {}",
|
||||
self.created_unix_seconds
|
||||
));
|
||||
lines.push(format!(
|
||||
"File: {}",
|
||||
private_file_label(&self.file_name, self.source_sha256.as_deref())
|
||||
@@ -290,7 +291,10 @@ impl RecoveryReport {
|
||||
.unwrap_or_else(|| "unknown".to_string())
|
||||
));
|
||||
lines.push(format!("Format version: {}", stats.source_version));
|
||||
lines.push(format!("Maintenance version: {}", stats.maintenance_version));
|
||||
lines.push(format!(
|
||||
"Maintenance version: {}",
|
||||
stats.maintenance_version
|
||||
));
|
||||
lines.push(format!("Recovery mode: {}", stats.recovery_mode));
|
||||
lines.push(format!("Stream completed: {}", stats.stream_completed));
|
||||
lines.push(format!("Source sections: {}", stats.source_sections));
|
||||
@@ -326,7 +330,10 @@ impl RecoveryReport {
|
||||
"References recovered: {}",
|
||||
self.references_recovered
|
||||
));
|
||||
lines.push(format!("References unavailable: {}", self.references_missing));
|
||||
lines.push(format!(
|
||||
"References unavailable: {}",
|
||||
self.references_missing
|
||||
));
|
||||
lines.push(format!("References failed: {}", self.references_failed));
|
||||
lines.push(format!("References skipped: {}", self.references_skipped));
|
||||
lines.push(format!(
|
||||
@@ -361,10 +368,7 @@ impl RecoveryReport {
|
||||
stats.skipped_source_records,
|
||||
));
|
||||
for diagnostic in stats.diagnostics.iter().take(50) {
|
||||
lines.push(format!(
|
||||
"{name} {}",
|
||||
structured_diagnostic_line(diagnostic)
|
||||
));
|
||||
lines.push(format!("{name} {}", structured_diagnostic_line(diagnostic)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -558,10 +562,7 @@ fn structured_diagnostic_line(diagnostic: &codec::ReadDiagnostic) -> String {
|
||||
.record_handle
|
||||
.map(|value| format!("0x{value:X}"))
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
let record_type = diagnostic
|
||||
.record_type
|
||||
.as_deref()
|
||||
.unwrap_or("-");
|
||||
let record_type = diagnostic.record_type.as_deref().unwrap_or("-");
|
||||
format!(
|
||||
"[code={} stage={} section={} offset={} offset-basis={} line={} handle={} type={}] {}",
|
||||
diagnostic.code,
|
||||
@@ -631,7 +632,9 @@ fn write_report(report: &RecoveryReport) -> Result<PathBuf, String> {
|
||||
|
||||
if let Some(directory) = crate::config::config_dir().map(|path| path.join("recovery_logs")) {
|
||||
if let Err(error) = std::fs::create_dir_all(&directory) {
|
||||
errors.push(format!("Could not create private recovery-log directory: {error}"));
|
||||
errors.push(format!(
|
||||
"Could not create private recovery-log directory: {error}"
|
||||
));
|
||||
} else {
|
||||
match write_unique(&directory, &file_name, &body) {
|
||||
Ok(path) => return Ok(path),
|
||||
|
||||
@@ -27,9 +27,9 @@
|
||||
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::net::{Ipv4Addr, SocketAddr, TcpListener, TcpStream};
|
||||
use std::path::PathBuf;
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
|
||||
+41
-13
@@ -27,9 +27,7 @@ pub fn from_screenshot(
|
||||
let height = screenshot.size.height as f32;
|
||||
let left = (bounds.x * scale).floor().clamp(0.0, width) as u32;
|
||||
let top = (bounds.y * scale).floor().clamp(0.0, height) as u32;
|
||||
let right = ((bounds.x + bounds.width) * scale)
|
||||
.ceil()
|
||||
.clamp(0.0, width) as u32;
|
||||
let right = ((bounds.x + bounds.width) * scale).ceil().clamp(0.0, width) as u32;
|
||||
let bottom = ((bounds.y + bounds.height) * scale)
|
||||
.ceil()
|
||||
.clamp(0.0, height) as u32;
|
||||
@@ -61,9 +59,15 @@ const MAX_DIM: u32 = 256;
|
||||
/// Canvas dimensions for an aspect ratio, longest edge = [`MAX_DIM`].
|
||||
fn canvas_dims(aspect: f64) -> (u32, u32) {
|
||||
if aspect >= 1.0 {
|
||||
(MAX_DIM, ((MAX_DIM as f64 / aspect).round() as u32).clamp(16, MAX_DIM))
|
||||
(
|
||||
MAX_DIM,
|
||||
((MAX_DIM as f64 / aspect).round() as u32).clamp(16, MAX_DIM),
|
||||
)
|
||||
} else {
|
||||
(((MAX_DIM as f64 * aspect).round() as u32).clamp(16, MAX_DIM), MAX_DIM)
|
||||
(
|
||||
((MAX_DIM as f64 * aspect).round() as u32).clamp(16, MAX_DIM),
|
||||
MAX_DIM,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,10 +77,16 @@ fn encode(img: RgbImage, png: bool) -> Option<Preview> {
|
||||
let mut buf = Cursor::new(Vec::new());
|
||||
img.write_to(&mut buf, ImageFormat::Png).ok()?;
|
||||
let data = buf.into_inner();
|
||||
return (!data.is_empty()).then_some(Preview { format: PreviewFormat::Png, data });
|
||||
return (!data.is_empty()).then_some(Preview {
|
||||
format: PreviewFormat::Png,
|
||||
data,
|
||||
});
|
||||
}
|
||||
let data = rle8_dib(&img).or_else(|| bmp24_dib(&img))?;
|
||||
Some(Preview { format: PreviewFormat::Bmp, data })
|
||||
Some(Preview {
|
||||
format: PreviewFormat::Bmp,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build an 8-bit palettised, `BI_RLE8`-compressed DIB. `None` when the image
|
||||
@@ -193,7 +203,11 @@ mod tests {
|
||||
fn dib_to_bmp(dib: &[u8]) -> Vec<u8> {
|
||||
let bi_size = u32::from_le_bytes([dib[0], dib[1], dib[2], dib[3]]) as usize;
|
||||
let bpp = u16::from_le_bytes([dib[14], dib[15]]) as usize;
|
||||
let palette = if (1..=8).contains(&bpp) { (1usize << bpp) * 4 } else { 0 };
|
||||
let palette = if (1..=8).contains(&bpp) {
|
||||
(1usize << bpp) * 4
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let mut v = Vec::with_capacity(14 + dib.len());
|
||||
v.extend_from_slice(b"BM");
|
||||
v.extend_from_slice(&((14 + dib.len()) as u32).to_le_bytes());
|
||||
@@ -219,18 +233,31 @@ mod tests {
|
||||
let bmp = encode(image, false).unwrap();
|
||||
assert_eq!(bmp.format, PreviewFormat::Bmp);
|
||||
// 8-bit, BI_RLE8.
|
||||
assert_eq!(u16::from_le_bytes([bmp.data[14], bmp.data[15]]), 8, "bitcount");
|
||||
assert_eq!(
|
||||
u16::from_le_bytes([bmp.data[14], bmp.data[15]]),
|
||||
8,
|
||||
"bitcount"
|
||||
);
|
||||
assert_eq!(
|
||||
u32::from_le_bytes([bmp.data[16], bmp.data[17], bmp.data[18], bmp.data[19]]),
|
||||
1,
|
||||
"compression = BI_RLE8"
|
||||
);
|
||||
// Far under a 24-bit DIB of the same canvas (256·256·3 = 196 608).
|
||||
assert!(bmp.data.len() < 196_608 / 10, "rle8 {} not << 24-bit", bmp.data.len());
|
||||
assert!(
|
||||
bmp.data.len() < 196_608 / 10,
|
||||
"rle8 {} not << 24-bit",
|
||||
bmp.data.len()
|
||||
);
|
||||
// Decodes through the exact path the reader uses, line preserved.
|
||||
let img = image::load_from_memory(&dib_to_bmp(&bmp.data)).expect("rle8 decodes").to_rgb8();
|
||||
let img = image::load_from_memory(&dib_to_bmp(&bmp.data))
|
||||
.expect("rle8 decodes")
|
||||
.to_rgb8();
|
||||
assert_eq!((img.width(), img.height()), (MAX_DIM, MAX_DIM));
|
||||
assert!(img.pixels().any(|px| px.0 == [0, 0, 0]), "black line missing");
|
||||
assert!(
|
||||
img.pixels().any(|px| px.0 == [0, 0, 0]),
|
||||
"black line missing"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -238,7 +265,8 @@ mod tests {
|
||||
let image = RgbImage::from_pixel(MAX_DIM, MAX_DIM, image::Rgb([255, 255, 255]));
|
||||
let p = encode(image, true).unwrap();
|
||||
assert_eq!(p.format, PreviewFormat::Png);
|
||||
let img = image::load_from_memory_with_format(&p.data, ImageFormat::Png).expect("png decodes");
|
||||
let img =
|
||||
image::load_from_memory_with_format(&p.data, ImageFormat::Png).expect("png decodes");
|
||||
assert_eq!((img.width(), img.height()), (MAX_DIM, MAX_DIM));
|
||||
}
|
||||
}
|
||||
+41
-16
@@ -3,8 +3,7 @@
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const RELEASES_API: &str =
|
||||
"https://api.github.com/repos/HakanSeven12/OpenCADStudio/releases/latest";
|
||||
pub const RELEASES_PAGE: &str =
|
||||
"https://github.com/HakanSeven12/OpenCADStudio/releases/latest";
|
||||
pub const RELEASES_PAGE: &str = "https://github.com/HakanSeven12/OpenCADStudio/releases/latest";
|
||||
|
||||
/// Give release assets time to propagate before offering an update.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -41,7 +40,10 @@ fn fetch_latest_if_outdated() -> Option<UpdateInfo> {
|
||||
let agent = crate::network::agent(std::time::Duration::from_secs(5));
|
||||
let body = agent
|
||||
.get(RELEASES_API)
|
||||
.header("User-Agent", concat!("OpenCADStudio/", env!("OCS_APP_VERSION")))
|
||||
.header(
|
||||
"User-Agent",
|
||||
concat!("OpenCADStudio/", env!("OCS_APP_VERSION")),
|
||||
)
|
||||
.header("Accept", "application/vnd.github+json")
|
||||
.call()
|
||||
.ok()?
|
||||
@@ -57,12 +59,21 @@ fn fetch_latest_if_outdated() -> Option<UpdateInfo> {
|
||||
"-linux-x86_64.AppImage"
|
||||
};
|
||||
if !metadata.get("assets")?.as_array()?.iter().any(|asset| {
|
||||
asset.get("name").and_then(|name| name.as_str()).is_some_and(|name| name.ends_with(suffix))
|
||||
&& asset.get("size").and_then(|size| size.as_u64()).unwrap_or(0) > 0
|
||||
asset
|
||||
.get("name")
|
||||
.and_then(|name| name.as_str())
|
||||
.is_some_and(|name| name.ends_with(suffix))
|
||||
&& asset
|
||||
.get("size")
|
||||
.and_then(|size| size.as_u64())
|
||||
.unwrap_or(0)
|
||||
> 0
|
||||
}) {
|
||||
return None;
|
||||
}
|
||||
let latest = metadata.get("tag_name")?.as_str()?
|
||||
let latest = metadata
|
||||
.get("tag_name")?
|
||||
.as_str()?
|
||||
.trim_start_matches('v')
|
||||
.to_string();
|
||||
if !is_newer(&latest, env!("OCS_APP_VERSION")) {
|
||||
@@ -70,7 +81,8 @@ fn fetch_latest_if_outdated() -> Option<UpdateInfo> {
|
||||
}
|
||||
// Suppress the notification until the release is old enough for the
|
||||
// Actions build to have published binaries.
|
||||
if let Some(published) = metadata.get("published_at")
|
||||
if let Some(published) = metadata
|
||||
.get("published_at")
|
||||
.and_then(|value| value.as_str())
|
||||
.and_then(parse_iso8601_utc)
|
||||
{
|
||||
@@ -83,8 +95,15 @@ fn fetch_latest_if_outdated() -> Option<UpdateInfo> {
|
||||
}
|
||||
}
|
||||
// Release notes are optional; treat missing as empty.
|
||||
let notes = metadata.get("body").and_then(|value| value.as_str()).unwrap_or_default().to_string();
|
||||
Some(UpdateInfo { version: latest, body: notes })
|
||||
let notes = metadata
|
||||
.get("body")
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
Some(UpdateInfo {
|
||||
version: latest,
|
||||
body: notes,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse a GitHub timestamp like `2026-05-29T12:34:56Z` into UNIX seconds.
|
||||
@@ -93,8 +112,13 @@ fn fetch_latest_if_outdated() -> Option<UpdateInfo> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn parse_iso8601_utc(s: &str) -> Option<u64> {
|
||||
let b = s.as_bytes();
|
||||
if b.len() != 20 || b[4] != b'-' || b[7] != b'-' || b[10] != b'T'
|
||||
|| b[13] != b':' || b[16] != b':' || b[19] != b'Z'
|
||||
if b.len() != 20
|
||||
|| b[4] != b'-'
|
||||
|| b[7] != b'-'
|
||||
|| b[10] != b'T'
|
||||
|| b[13] != b':'
|
||||
|| b[16] != b':'
|
||||
|| b[19] != b'Z'
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -126,10 +150,7 @@ fn parse_iso8601_utc(s: &str) -> Option<u64> {
|
||||
return None;
|
||||
}
|
||||
Some(
|
||||
days_since_epoch as u64 * 86_400
|
||||
+ hour as u64 * 3_600
|
||||
+ minute as u64 * 60
|
||||
+ second as u64,
|
||||
days_since_epoch as u64 * 86_400 + hour as u64 * 3_600 + minute as u64 * 60 + second as u64,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -141,7 +162,11 @@ fn is_newer(latest: &str, installed: &str) -> bool {
|
||||
let parts: Vec<&str> = version.split('.').collect();
|
||||
if (2..=3).contains(&parts.len()) && parts[0].len() == 4 && parts[0].starts_with("20") {
|
||||
let patch = parts.get(2).map_or(Some(0), |patch| patch.parse().ok())?;
|
||||
return Some(semver::Version::new(parts[0].parse().ok()?, parts[1].parse().ok()?, patch));
|
||||
return Some(semver::Version::new(
|
||||
parts[0].parse().ok()?,
|
||||
parts[1].parse().ok()?,
|
||||
patch,
|
||||
));
|
||||
}
|
||||
semver::Version::parse(version).ok()
|
||||
};
|
||||
|
||||
@@ -90,10 +90,7 @@ pub async fn store_open(
|
||||
return None;
|
||||
};
|
||||
if state.latest_id != pending_id {
|
||||
return Some((
|
||||
state.latest_id,
|
||||
std::sync::Arc::clone(&state.latest_bytes),
|
||||
));
|
||||
return Some((state.latest_id, std::sync::Arc::clone(&state.latest_bytes)));
|
||||
}
|
||||
state.active_writers = state.active_writers.saturating_sub(1);
|
||||
if state.active_writers == 0 {
|
||||
|
||||
+4
-11
@@ -55,10 +55,7 @@ pub(super) async fn parse_document(
|
||||
let bytes = Uint8Array::new(&value).to_vec();
|
||||
let payload: (
|
||||
u16,
|
||||
Result<
|
||||
codec::ReadOutcome,
|
||||
(String, Option<codec::ReadStats>),
|
||||
>,
|
||||
Result<codec::ReadOutcome, (String, Option<codec::ReadStats>)>,
|
||||
Option<String>,
|
||||
bool,
|
||||
Vec<EntityRuntimeFields>,
|
||||
@@ -72,10 +69,7 @@ pub(super) async fn parse_document(
|
||||
}
|
||||
match payload.1 {
|
||||
Ok(mut outcome) => {
|
||||
restore_entity_runtime_fields(
|
||||
&mut outcome.document,
|
||||
payload.4,
|
||||
);
|
||||
restore_entity_runtime_fields(&mut outcome.document, payload.4);
|
||||
Ok((outcome, payload.2))
|
||||
}
|
||||
Err((message, read_stats)) => Err(super::OpenLoadError {
|
||||
@@ -172,9 +166,8 @@ pub(super) async fn sha256_document(bytes: &[u8]) -> Result<String, super::OpenL
|
||||
let worker = Worker::new_with_options(WORKER_URL, &options)
|
||||
.map_err(|error| super::OpenLoadError::from(js_error(error)))?;
|
||||
|
||||
let (sender, receiver) = iced::futures::channel::oneshot::channel::<
|
||||
Result<String, super::OpenLoadError>,
|
||||
>();
|
||||
let (sender, receiver) =
|
||||
iced::futures::channel::oneshot::channel::<Result<String, super::OpenLoadError>>();
|
||||
let sender = Rc::new(RefCell::new(Some(sender)));
|
||||
let message_sender = sender.clone();
|
||||
let on_message = Closure::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| {
|
||||
|
||||
+46
-16
@@ -67,7 +67,10 @@ pub fn paper_names(names_utf16: &[u16], count: usize) -> Vec<String> {
|
||||
.chunks(PAPER_NAME_CHARS)
|
||||
.take(count)
|
||||
.map(|slot| {
|
||||
let end = slot.iter().position(|&unit| unit == 0).unwrap_or(slot.len());
|
||||
let end = slot
|
||||
.iter()
|
||||
.position(|&unit| unit == 0)
|
||||
.unwrap_or(slot.len());
|
||||
String::from_utf16_lossy(&slot[..end]).trim().to_string()
|
||||
})
|
||||
.collect()
|
||||
@@ -127,13 +130,15 @@ pub fn printer_media(raw: &RawPrinterMedia) -> Option<PrinterCapabilities> {
|
||||
sheet_of_id.push((raw.ids[index], position));
|
||||
continue;
|
||||
}
|
||||
let paper = paper_catalog::match_dimensions_mm(pw, ph).cloned().unwrap_or_else(|| {
|
||||
if name.is_empty() {
|
||||
PaperSize::custom(pw, ph, PaperUnits::Millimeters)
|
||||
} else {
|
||||
PaperSize::custom_named(&name, pw, ph, PaperUnits::Millimeters)
|
||||
}
|
||||
});
|
||||
let paper = paper_catalog::match_dimensions_mm(pw, ph)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
if name.is_empty() {
|
||||
PaperSize::custom(pw, ph, PaperUnits::Millimeters)
|
||||
} else {
|
||||
PaperSize::custom_named(&name, pw, ph, PaperUnits::Millimeters)
|
||||
}
|
||||
});
|
||||
sheet_of_id.push((raw.ids[index], media.len()));
|
||||
media.push(PrinterMedia {
|
||||
paper,
|
||||
@@ -229,7 +234,11 @@ mod tests {
|
||||
assert_eq!(names[0], "A4");
|
||||
assert_eq!(names[1].len(), PAPER_NAME_CHARS);
|
||||
assert!(names[2].starts_with("Letter"));
|
||||
assert_eq!(paper_names(&buffer, 1).len(), 1, "count caps the slots read");
|
||||
assert_eq!(
|
||||
paper_names(&buffer, 1).len(),
|
||||
1,
|
||||
"count caps the slots read"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -245,10 +254,22 @@ mod tests {
|
||||
.expect("usable sheets");
|
||||
assert_eq!(caps.media.len(), 3);
|
||||
assert_eq!(caps.media[0].paper.canonical, "ISO_A4_(210.00_x_297.00_MM)");
|
||||
assert!(caps.media[0].borderless, "the borderless twin marks the sheet");
|
||||
assert_eq!(caps.media[1].paper.canonical, "ANSI_A_(8.50_x_11.00_Inches)");
|
||||
assert_eq!(caps.media[2].paper.label, "Photo 10x15", "the driver's name is kept");
|
||||
assert_eq!(caps.media[2].margins.left, 3.0, "the device margins go on every sheet");
|
||||
assert!(
|
||||
caps.media[0].borderless,
|
||||
"the borderless twin marks the sheet"
|
||||
);
|
||||
assert_eq!(
|
||||
caps.media[1].paper.canonical,
|
||||
"ANSI_A_(8.50_x_11.00_Inches)"
|
||||
);
|
||||
assert_eq!(
|
||||
caps.media[2].paper.label, "Photo 10x15",
|
||||
"the driver's name is kept"
|
||||
);
|
||||
assert_eq!(
|
||||
caps.media[2].margins.left, 3.0,
|
||||
"the device margins go on every sheet"
|
||||
);
|
||||
assert!(caps.default_paper.is_none());
|
||||
}
|
||||
|
||||
@@ -275,7 +296,10 @@ mod tests {
|
||||
);
|
||||
media.default_id = Some(999);
|
||||
assert!(printer_media(&media).unwrap().default_paper.is_none());
|
||||
assert!(printer_media(&RawPrinterMedia::default()).is_none(), "nothing listed → catalogue");
|
||||
assert!(
|
||||
printer_media(&RawPrinterMedia::default()).is_none(),
|
||||
"nothing listed → catalogue"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -297,8 +321,14 @@ mod tests {
|
||||
assert!(close(margins.top, 100.0 / 600.0 * 25.4));
|
||||
assert!(close(margins.bottom, 200.0 / 600.0 * 25.4));
|
||||
assert!(device_margins_mm(&DeviceCapsSample::default()).is_none());
|
||||
let inverted = DeviceCapsSample { horz_res: 9000, ..sample };
|
||||
assert!(device_margins_mm(&inverted).is_none(), "printable area wider than the sheet");
|
||||
let inverted = DeviceCapsSample {
|
||||
horz_res: 9000,
|
||||
..sample
|
||||
};
|
||||
assert!(
|
||||
device_margins_mm(&inverted).is_none(),
|
||||
"printable area wider than the sheet"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+179
-181
@@ -128,9 +128,9 @@ fn resolve_xrefs_with_filter(
|
||||
.map(|(xref_index, (block_name, raw_path, br_handle))| {
|
||||
let resolved = resolve_path(&raw_path, base_dir);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let initial_fingerprint = resolved.as_ref().and_then(|path| {
|
||||
crate::io::edit_lock::FileFingerprint::capture(path).ok()
|
||||
});
|
||||
let initial_fingerprint = resolved
|
||||
.as_ref()
|
||||
.and_then(|path| crate::io::edit_lock::FileFingerprint::capture(path).ok());
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let initial_fingerprint = None;
|
||||
let units = std::sync::Arc::clone(&parse_units);
|
||||
@@ -217,8 +217,8 @@ fn resolve_xrefs_with_filter(
|
||||
|| outcome.stats.skipped_source_records > 0
|
||||
|| !outcome.stats.stream_completed
|
||||
|| outcome.document.notifications.iter().any(|item| {
|
||||
item.notification_type == codec::notification::NotificationType::Error
|
||||
});
|
||||
item.notification_type == codec::notification::NotificationType::Error
|
||||
});
|
||||
let mut diagnostics: Vec<String> = outcome
|
||||
.document
|
||||
.notifications
|
||||
@@ -247,34 +247,29 @@ fn resolve_xrefs_with_filter(
|
||||
if recovered || invalid > 0 {
|
||||
(XrefStatus::Failed, diagnostics, Some(outcome.stats))
|
||||
} else {
|
||||
// Reload is replacement, never append. Keep the prior merge
|
||||
// intact until parsing succeeded, then discard its generated
|
||||
// entities, sortents, and dependent symbols immediately before
|
||||
// importing the replacement content.
|
||||
//
|
||||
// VISRETAIN=1 (`header.retain_xref_visibility`): host-side
|
||||
// layer overrides survive the purge — snapshot them first and
|
||||
// re-apply onto the freshly merged layers below. Without this
|
||||
// every reload silently reset frozen/off/color tweaks the
|
||||
// user made on `name|*` layers back to file defaults.
|
||||
let visretain = doc.header.retain_xref_visibility;
|
||||
let kept_layers = if visretain {
|
||||
snapshot_dependent_layers(doc, &block_name)
|
||||
} else {
|
||||
HashMap::default()
|
||||
};
|
||||
let _ = unload_reference(doc, br_handle.value());
|
||||
remove_pipe_symbols(doc, &block_name);
|
||||
ensure_block_entities(doc, &block_name);
|
||||
dropped += merge_xref_into_block(
|
||||
doc,
|
||||
&block_name,
|
||||
br_handle,
|
||||
outcome.document,
|
||||
);
|
||||
if visretain {
|
||||
restore_dependent_layers(doc, &block_name, kept_layers);
|
||||
}
|
||||
// Reload is replacement, never append. Keep the prior merge
|
||||
// intact until parsing succeeded, then discard its generated
|
||||
// entities, sortents, and dependent symbols immediately before
|
||||
// importing the replacement content.
|
||||
//
|
||||
// VISRETAIN=1 (`header.retain_xref_visibility`): host-side
|
||||
// layer overrides survive the purge — snapshot them first and
|
||||
// re-apply onto the freshly merged layers below. Without this
|
||||
// every reload silently reset frozen/off/color tweaks the
|
||||
// user made on `name|*` layers back to file defaults.
|
||||
let visretain = doc.header.retain_xref_visibility;
|
||||
let kept_layers = if visretain {
|
||||
snapshot_dependent_layers(doc, &block_name)
|
||||
} else {
|
||||
HashMap::default()
|
||||
};
|
||||
let _ = unload_reference(doc, br_handle.value());
|
||||
remove_pipe_symbols(doc, &block_name);
|
||||
ensure_block_entities(doc, &block_name);
|
||||
dropped += merge_xref_into_block(doc, &block_name, br_handle, outcome.document);
|
||||
if visretain {
|
||||
restore_dependent_layers(doc, &block_name, kept_layers);
|
||||
}
|
||||
(XrefStatus::Loaded, diagnostics, Some(outcome.stats))
|
||||
}
|
||||
}
|
||||
@@ -333,7 +328,10 @@ pub fn register_underlay_sources(doc: &CadDocument, base_dir: &Path) {
|
||||
continue;
|
||||
};
|
||||
let stored = def.source_filename.trim();
|
||||
if let Some(found) = (!stored.is_empty()).then(|| resolve_path(stored, base_dir)).flatten() {
|
||||
if let Some(found) = (!stored.is_empty())
|
||||
.then(|| resolve_path(stored, base_dir))
|
||||
.flatten()
|
||||
{
|
||||
crate::scene::model::point_cloud::register_source(stored, found);
|
||||
}
|
||||
}
|
||||
@@ -368,7 +366,8 @@ impl LayerLook {
|
||||
|
||||
/// The source file's layers by upper-case name, read once per file version.
|
||||
fn source_layers(path: &Path) -> Option<HashMap<String, LayerLook>> {
|
||||
type Cache = std::sync::Mutex<HashMap<PathBuf, (std::time::SystemTime, HashMap<String, LayerLook>)>>;
|
||||
type Cache =
|
||||
std::sync::Mutex<HashMap<PathBuf, (std::time::SystemTime, HashMap<String, LayerLook>)>>;
|
||||
static CACHE: std::sync::OnceLock<Cache> = std::sync::OnceLock::new();
|
||||
let modified = std::fs::metadata(path).and_then(|m| m.modified()).ok()?;
|
||||
let cache = CACHE.get_or_init(Default::default);
|
||||
@@ -545,16 +544,9 @@ impl BindTaken {
|
||||
Self {
|
||||
layers: upper_names(doc.layers.names().map(|s| s.to_string()).collect()),
|
||||
linetypes: upper_names(doc.line_types.names().map(|s| s.to_string()).collect()),
|
||||
text_styles: upper_names(
|
||||
doc.text_styles.names().map(|s| s.to_string()).collect(),
|
||||
),
|
||||
text_styles: upper_names(doc.text_styles.names().map(|s| s.to_string()).collect()),
|
||||
dim_styles: upper_names(doc.dim_styles.names().map(|s| s.to_string()).collect()),
|
||||
blocks: upper_names(
|
||||
doc.block_records
|
||||
.iter()
|
||||
.map(|b| b.name.clone())
|
||||
.collect(),
|
||||
),
|
||||
blocks: upper_names(doc.block_records.iter().map(|b| b.name.clone()).collect()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -660,7 +652,9 @@ fn import_xref_symbols(
|
||||
let same_file = doc
|
||||
.text_styles
|
||||
.iter()
|
||||
.find(|host| host.is_shape_file && host.font_file.eq_ignore_ascii_case(&ts.font_file))
|
||||
.find(|host| {
|
||||
host.is_shape_file && host.font_file.eq_ignore_ascii_case(&ts.font_file)
|
||||
})
|
||||
.map(|host| host.handle);
|
||||
if let Some(host_handle) = same_file {
|
||||
maps.text_style_handles.insert(ts.handle, host_handle);
|
||||
@@ -798,11 +792,7 @@ fn null_handle(handle: &mut Option<Handle>) -> usize {
|
||||
/// Rewrite one entity's symbol references to the imported copies in place.
|
||||
/// Returns the count of unremappable foreign handles (see
|
||||
/// `bind_limitations` above) — ignored on the load path, reported per BIND.
|
||||
fn remap_xref_entity(
|
||||
host: &CadDocument,
|
||||
entity: &mut EntityType,
|
||||
maps: &XrefSymbolMaps,
|
||||
) -> usize {
|
||||
fn remap_xref_entity(host: &CadDocument, entity: &mut EntityType, maps: &XrefSymbolMaps) -> usize {
|
||||
let mut unremapped = 0usize;
|
||||
let rename = |map: &HashMap<String, String>, name: &mut String| {
|
||||
if let Some(new) = map.get(&name.to_uppercase()) {
|
||||
@@ -950,7 +940,8 @@ fn copy_xref_sortents(
|
||||
nt.add_entry(ne, ns);
|
||||
}
|
||||
if !nt.is_empty() {
|
||||
doc.objects.insert(nt.handle, ObjectType::SortEntitiesTable(nt));
|
||||
doc.objects
|
||||
.insert(nt.handle, ObjectType::SortEntitiesTable(nt));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1007,11 +998,16 @@ fn merge_xref_into_block(
|
||||
// ── Entities (shared helper) ────────────────────────────────────────
|
||||
// The unremapped-handle count is ignored on the load path (BIND reports
|
||||
// it per bind).
|
||||
let (dropped, _, entity_handle_map) =
|
||||
merge_source_entities(doc, &xref_doc, br_handle, &maps);
|
||||
let (dropped, _, entity_handle_map) = merge_source_entities(doc, &xref_doc, br_handle, &maps);
|
||||
|
||||
// ── Draw-order overrides (shared helper) ──────────────────────────────
|
||||
copy_xref_sortents(doc, &xref_doc, br_handle, &maps.br_handles, &entity_handle_map);
|
||||
copy_xref_sortents(
|
||||
doc,
|
||||
&xref_doc,
|
||||
br_handle,
|
||||
&maps.br_handles,
|
||||
&entity_handle_map,
|
||||
);
|
||||
dropped
|
||||
}
|
||||
|
||||
@@ -1152,11 +1148,11 @@ pub fn collect_entries_with_prev(
|
||||
unloaded: &std::collections::HashSet<crate::io::xref_model::UnloadKey>,
|
||||
prev: &std::collections::HashMap<u64, std::time::SystemTime>,
|
||||
) -> Vec<crate::io::xref_model::ReferenceEntry> {
|
||||
use codec::objects::ObjectType;
|
||||
use crate::io::xref_model::{
|
||||
child_key, decide_status, normalize_lexical, RefKind, RefStatus, RefType,
|
||||
ReferenceEntry, UnloadKey,
|
||||
child_key, decide_status, normalize_lexical, RefKind, RefStatus, RefType, ReferenceEntry,
|
||||
UnloadKey,
|
||||
};
|
||||
use codec::objects::ObjectType;
|
||||
|
||||
/// Resolve `raw` via `base_dir`; on success record `found_at`, stat
|
||||
/// size/mtime, and flip the entry to Loaded. Missing/empty stays NotFound.
|
||||
@@ -1218,12 +1214,8 @@ pub fn collect_entries_with_prev(
|
||||
resolve_only(&mut entry, &br.xref_path, base_dir);
|
||||
} else {
|
||||
stat_into(&mut entry, &br.xref_path, base_dir);
|
||||
entry.status = decide_status(
|
||||
entry.status,
|
||||
entry.modified,
|
||||
prev.get(&key).copied(),
|
||||
false,
|
||||
);
|
||||
entry.status =
|
||||
decide_status(entry.status, entry.modified, prev.get(&key).copied(), false);
|
||||
}
|
||||
entries.push(entry);
|
||||
}
|
||||
@@ -1253,12 +1245,8 @@ pub fn collect_entries_with_prev(
|
||||
resolve_only(&mut entry, &def.file_name, base_dir);
|
||||
} else {
|
||||
stat_into(&mut entry, &def.file_name, base_dir);
|
||||
entry.status = decide_status(
|
||||
entry.status,
|
||||
entry.modified,
|
||||
prev.get(&key).copied(),
|
||||
false,
|
||||
);
|
||||
entry.status =
|
||||
decide_status(entry.status, entry.modified, prev.get(&key).copied(), false);
|
||||
if is_unreferenced {
|
||||
entry.status = RefStatus::Unreferenced;
|
||||
}
|
||||
@@ -1295,12 +1283,8 @@ pub fn collect_entries_with_prev(
|
||||
resolve_only(&mut entry, &def.file_path, base_dir);
|
||||
} else {
|
||||
stat_into(&mut entry, &def.file_path, base_dir);
|
||||
entry.status = decide_status(
|
||||
entry.status,
|
||||
entry.modified,
|
||||
prev.get(&key).copied(),
|
||||
false,
|
||||
);
|
||||
entry.status =
|
||||
decide_status(entry.status, entry.modified, prev.get(&key).copied(), false);
|
||||
if !referenced_underlays.contains(handle) {
|
||||
entry.status = RefStatus::Unreferenced;
|
||||
}
|
||||
@@ -1326,7 +1310,12 @@ pub fn collect_entries_with_prev(
|
||||
ObjectType::Dictionary(dictionary) => Some(dictionary),
|
||||
_ => None,
|
||||
})
|
||||
.flat_map(|dictionary| dictionary.entries.iter().map(|(name, handle)| (*handle, name.clone())))
|
||||
.flat_map(|dictionary| {
|
||||
dictionary
|
||||
.entries
|
||||
.iter()
|
||||
.map(|(name, handle)| (*handle, name.clone()))
|
||||
})
|
||||
.collect();
|
||||
for (handle, obj) in doc.objects.iter() {
|
||||
let ObjectType::ClassObject(object) = obj else {
|
||||
@@ -1349,7 +1338,8 @@ pub fn collect_entries_with_prev(
|
||||
resolve_only(&mut entry, &def.source_filename, base_dir);
|
||||
} else {
|
||||
stat_into(&mut entry, &def.source_filename, base_dir);
|
||||
entry.status = decide_status(entry.status, entry.modified, prev.get(&key).copied(), false);
|
||||
entry.status =
|
||||
decide_status(entry.status, entry.modified, prev.get(&key).copied(), false);
|
||||
if !referenced_clouds.contains(handle) {
|
||||
entry.status = RefStatus::Unreferenced;
|
||||
}
|
||||
@@ -1395,7 +1385,11 @@ pub fn collect_entries_with_prev(
|
||||
let key = child_key(parent_key, &br.name, &br.xref_path);
|
||||
let mut child = ReferenceEntry::new(key, br.name.clone(), RefKind::DwgXref);
|
||||
child.parent_key = Some(parent_key);
|
||||
child.ref_type = if br.flags.is_xref_overlay { RefType::Overlay } else { RefType::Attach };
|
||||
child.ref_type = if br.flags.is_xref_overlay {
|
||||
RefType::Overlay
|
||||
} else {
|
||||
RefType::Attach
|
||||
};
|
||||
child.saved_path = br.xref_path.clone();
|
||||
if unloaded.contains(&UnloadKey::Nested(key)) {
|
||||
child.status = RefStatus::Unloaded;
|
||||
@@ -1404,7 +1398,8 @@ pub fn collect_entries_with_prev(
|
||||
// Nested baselines live in the same stat cache under the
|
||||
// child's high-bit-tagged key, which can never alias a host
|
||||
// handle (see `child_key`) — so the lookup below is safe.
|
||||
child.status = decide_status(child.status, child.modified, prev.get(&key).copied(), false);
|
||||
child.status =
|
||||
decide_status(child.status, child.modified, prev.get(&key).copied(), false);
|
||||
}
|
||||
if let Some(found) = &child.found_at {
|
||||
let resolved_id = normalize_lexical(found);
|
||||
@@ -1422,8 +1417,11 @@ pub fn collect_entries_with_prev(
|
||||
// reaches a parent, the parent already reflects its own subtree.
|
||||
// Unloaded/NotFound/Failed rows are never overridden.
|
||||
{
|
||||
let index: HashMap<u64, usize> =
|
||||
entries.iter().enumerate().map(|(i, e)| (e.key, i)).collect();
|
||||
let index: HashMap<u64, usize> = entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, e)| (e.key, i))
|
||||
.collect();
|
||||
for rev in (0..entries.len()).rev() {
|
||||
if entries[rev].status != RefStatus::Stale {
|
||||
continue;
|
||||
@@ -1472,7 +1470,10 @@ fn find_target(doc: &CadDocument, key: u64) -> Option<RefTarget> {
|
||||
}
|
||||
ObjectType::ClassObject(object) if handle.value() == key => {
|
||||
if let codec::objects::ClassObjectData::PointCloudDefinitionEx(def) = &object.data {
|
||||
return Some(RefTarget::PointCloud { handle: *handle, name: def.source_filename.clone() });
|
||||
return Some(RefTarget::PointCloud {
|
||||
handle: *handle,
|
||||
name: def.source_filename.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
ObjectType::UnderlayDefinition(def) if handle.value() == key => {
|
||||
@@ -1524,9 +1525,7 @@ pub fn unload_reference(doc: &mut CadDocument, key: u64) -> Result<String, Strin
|
||||
.objects
|
||||
.iter()
|
||||
.filter_map(|(h, o)| match o {
|
||||
ObjectType::SortEntitiesTable(t)
|
||||
if t.block_owner_handle == handle =>
|
||||
{
|
||||
ObjectType::SortEntitiesTable(t) if t.block_owner_handle == handle => {
|
||||
Some(*h)
|
||||
}
|
||||
_ => None,
|
||||
@@ -1585,9 +1584,7 @@ pub fn detach_reference(doc: &mut CadDocument, key: u64) -> Result<String, Strin
|
||||
let inserts: Vec<Handle> = doc
|
||||
.entities()
|
||||
.filter_map(|e| match e {
|
||||
EntityType::Insert(ins)
|
||||
if ins.block_name.eq_ignore_ascii_case(&name) =>
|
||||
{
|
||||
EntityType::Insert(ins) if ins.block_name.eq_ignore_ascii_case(&name) => {
|
||||
Some(e.common().handle)
|
||||
}
|
||||
_ => None,
|
||||
@@ -1614,9 +1611,7 @@ pub fn detach_reference(doc: &mut CadDocument, key: u64) -> Result<String, Strin
|
||||
.objects
|
||||
.iter()
|
||||
.filter_map(|(h, o)| match o {
|
||||
ObjectType::SortEntitiesTable(t)
|
||||
if t.block_owner_handle == handle =>
|
||||
{
|
||||
ObjectType::SortEntitiesTable(t) if t.block_owner_handle == handle => {
|
||||
Some(*h)
|
||||
}
|
||||
_ => None,
|
||||
@@ -1633,9 +1628,7 @@ pub fn detach_reference(doc: &mut CadDocument, key: u64) -> Result<String, Strin
|
||||
let refs: Vec<Handle> = doc
|
||||
.entities()
|
||||
.filter_map(|e| match e {
|
||||
EntityType::RasterImage(img)
|
||||
if img.definition_handle == Some(handle) =>
|
||||
{
|
||||
EntityType::RasterImage(img) if img.definition_handle == Some(handle) => {
|
||||
Some(e.common().handle)
|
||||
}
|
||||
_ => None,
|
||||
@@ -1671,7 +1664,9 @@ pub fn detach_reference(doc: &mut CadDocument, key: u64) -> Result<String, Strin
|
||||
.entities()
|
||||
.filter_map(|e| match e {
|
||||
EntityType::Extended(extended) => match &extended.data {
|
||||
ExtendedEntityData::PointCloudEx(data) if data.definition_handle == handle => {
|
||||
ExtendedEntityData::PointCloudEx(data)
|
||||
if data.definition_handle == handle =>
|
||||
{
|
||||
Some((e.common().handle, data.reactor_handle))
|
||||
}
|
||||
_ => None,
|
||||
@@ -1733,7 +1728,10 @@ pub fn bind_reference(
|
||||
RefTarget::DwgXref { handle, name } => bind_dwg(doc, handle, &name, base_dir, host_dir),
|
||||
RefTarget::Image { handle, name } => {
|
||||
collect_bind_images(doc, &name, base_dir, host_dir, Some(handle))?;
|
||||
Ok(BindOutcome { name, unremapped: 0 })
|
||||
Ok(BindOutcome {
|
||||
name,
|
||||
unremapped: 0,
|
||||
})
|
||||
}
|
||||
RefTarget::Pdf { .. } => Err(crate::t!(
|
||||
"XREF: PDF bind (vector import) is not available in this version."
|
||||
@@ -1811,14 +1809,14 @@ fn load_bind_source(ref_name: &str, found: &Path) -> Result<CadDocument, String>
|
||||
fn check_no_pdf_underlays(source: &CadDocument) -> Result<(), String> {
|
||||
use codec::entities::UnderlayType;
|
||||
use codec::objects::ObjectType;
|
||||
let has_pdf = source.objects.values().any(|o| {
|
||||
matches!(o, ObjectType::UnderlayDefinition(d) if d.underlay_type == UnderlayType::Pdf)
|
||||
});
|
||||
let has_pdf = source.objects.values().any(
|
||||
|o| matches!(o, ObjectType::UnderlayDefinition(d) if d.underlay_type == UnderlayType::Pdf),
|
||||
);
|
||||
if has_pdf {
|
||||
return Err(crate::t!(
|
||||
"XREF: PDF bind (vector import) is not available in this version."
|
||||
)
|
||||
.to_string());
|
||||
return Err(
|
||||
crate::t!("XREF: PDF bind (vector import) is not available in this version.")
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1845,8 +1843,7 @@ fn bind_nested_closure(
|
||||
let Some(child_found) = resolve_path(&child_saved, source_dir) else {
|
||||
return Err(bind_failed(&child_name, "file not found"));
|
||||
};
|
||||
let norm =
|
||||
crate::io::xref_model::normalize_lexical(&child_found.to_string_lossy());
|
||||
let norm = crate::io::xref_model::normalize_lexical(&child_found.to_string_lossy());
|
||||
if stack.contains(&norm) {
|
||||
continue;
|
||||
}
|
||||
@@ -1926,9 +1923,7 @@ fn collect_bind_images(
|
||||
.objects
|
||||
.iter()
|
||||
.filter_map(|(h, o)| match o {
|
||||
ObjectType::ImageDefinition(d)
|
||||
if only.is_none_or(|want| want == *h) =>
|
||||
{
|
||||
ObjectType::ImageDefinition(d) if only.is_none_or(|want| want == *h) => {
|
||||
Some((*h, d.file_name.clone()))
|
||||
}
|
||||
_ => None,
|
||||
@@ -2018,11 +2013,7 @@ pub const POINT_CLOUD_KEY: &str = "pointcloud:";
|
||||
|
||||
/// Make the named references' paths relative to `host` (their drawing's
|
||||
/// first save); a reference that cannot be relative keeps its full path.
|
||||
pub fn make_relative(
|
||||
doc: &mut CadDocument,
|
||||
names: &rustc_hash::FxHashSet<String>,
|
||||
host: &Path,
|
||||
) {
|
||||
pub fn make_relative(doc: &mut CadDocument, names: &rustc_hash::FxHashSet<String>, host: &Path) {
|
||||
for name in names {
|
||||
if let Some(handle) = name.strip_prefix(POINT_CLOUD_KEY) {
|
||||
if let Ok(handle) = u64::from_str_radix(handle, 16) {
|
||||
@@ -2232,10 +2223,7 @@ fn restore_dependent_layers(
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
for n in targets {
|
||||
let (Some(cur), Some(old)) = (
|
||||
doc.layers.get_mut(&n),
|
||||
kept.get(&n.to_uppercase()),
|
||||
) else {
|
||||
let (Some(cur), Some(old)) = (doc.layers.get_mut(&n), kept.get(&n.to_uppercase())) else {
|
||||
continue;
|
||||
};
|
||||
cur.flags = old.flags.clone();
|
||||
@@ -2251,11 +2239,7 @@ fn restore_dependent_layers(
|
||||
}
|
||||
|
||||
/// Store `new_raw` verbatim on the definition (no synthesis).
|
||||
pub fn set_ref_path(
|
||||
doc: &mut CadDocument,
|
||||
key: u64,
|
||||
new_raw: &str,
|
||||
) -> Result<String, String> {
|
||||
pub fn set_ref_path(doc: &mut CadDocument, key: u64, new_raw: &str) -> Result<String, String> {
|
||||
let Some(target) = find_target(doc, key) else {
|
||||
return Err(crate::t!("XREF: no loaded reference with that key.").to_string());
|
||||
};
|
||||
@@ -2359,9 +2343,10 @@ pub fn apply_pathtype(
|
||||
Err(crate::io::xref_model::PathtypeError::AcrossDrives) => {
|
||||
Err(crate::t!("XREF: cannot make path relative across drives.").to_string())
|
||||
}
|
||||
Err(crate::io::xref_model::PathtypeError::UnsavedHost) => Err(
|
||||
crate::t!("XREF Save the drawing first to resolve relative XREF paths.").to_string(),
|
||||
),
|
||||
Err(crate::io::xref_model::PathtypeError::UnsavedHost) => Err(crate::t!(
|
||||
"XREF Save the drawing first to resolve relative XREF paths."
|
||||
)
|
||||
.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2539,9 +2524,9 @@ mod tests {
|
||||
use super::collect_entries_with_prev;
|
||||
use super::HashSet;
|
||||
use super::{
|
||||
apply_pathtype, child_key, detach_reference, replace_path_prefix,
|
||||
resolve_xrefs_for_keys, restore_dependent_layers, set_ref_path,
|
||||
set_ref_type, snapshot_dependent_layers, unload_reference,
|
||||
apply_pathtype, child_key, detach_reference, replace_path_prefix, resolve_xrefs_for_keys,
|
||||
restore_dependent_layers, set_ref_path, set_ref_type, snapshot_dependent_layers,
|
||||
unload_reference,
|
||||
};
|
||||
use crate::io::xref_model::{Pathtype, RefKind, RefStatus, RefType};
|
||||
|
||||
@@ -2624,26 +2609,21 @@ mod tests {
|
||||
use std::time::UNIX_EPOCH;
|
||||
let dir = xref_tmpdir("staleprop");
|
||||
let (mid_path, inner_path) = write_nested_fixture(&dir);
|
||||
let (doc, mid_key) = xref_doc_with(
|
||||
"MID",
|
||||
&mid_path.to_string_lossy(),
|
||||
);
|
||||
let inner_key = child_key(
|
||||
mid_key,
|
||||
"INNER",
|
||||
&inner_path.to_string_lossy(),
|
||||
);
|
||||
let (doc, mid_key) = xref_doc_with("MID", &mid_path.to_string_lossy());
|
||||
let inner_key = child_key(mid_key, "INNER", &inner_path.to_string_lossy());
|
||||
let mut prev = std::collections::HashMap::new();
|
||||
prev.insert(inner_key, UNIX_EPOCH);
|
||||
let entries = collect_entries_with_prev(
|
||||
&doc,
|
||||
&dir,
|
||||
&std::collections::HashSet::new(),
|
||||
&prev,
|
||||
);
|
||||
let inner = entries.iter().find(|e| e.key == inner_key).expect("nested INNER listed");
|
||||
let entries =
|
||||
collect_entries_with_prev(&doc, &dir, &std::collections::HashSet::new(), &prev);
|
||||
let inner = entries
|
||||
.iter()
|
||||
.find(|e| e.key == inner_key)
|
||||
.expect("nested INNER listed");
|
||||
assert_eq!(inner.status, RefStatus::Stale);
|
||||
let mid = entries.iter().find(|e| e.key == mid_key).expect("MID listed");
|
||||
let mid = entries
|
||||
.iter()
|
||||
.find(|e| e.key == mid_key)
|
||||
.expect("MID listed");
|
||||
assert_eq!(mid.status, RefStatus::Stale, "parent reflects stale child");
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
@@ -2658,49 +2638,61 @@ mod tests {
|
||||
let dir = xref_tmpdir("e2e");
|
||||
let (mid_path, _) = write_nested_fixture(&dir);
|
||||
std::fs::write(dir.join("img.png"), b"fake-png").unwrap();
|
||||
let (mut doc, mid_key) =
|
||||
xref_doc_with("MID", &mid_path.to_string_lossy());
|
||||
let (mut doc, mid_key) = xref_doc_with("MID", &mid_path.to_string_lossy());
|
||||
// Referenced image → Loaded entry.
|
||||
let img_h = doc.allocate_handle();
|
||||
let mut img_def = ImageDefinition::with_dimensions("img.png", 8, 8);
|
||||
img_def.handle = img_h;
|
||||
doc.objects.insert(img_h, ObjectType::ImageDefinition(img_def));
|
||||
let mut img = codec::entities::RasterImage::new(
|
||||
"img.png",
|
||||
codec::types::Vector3::ZERO,
|
||||
8.0,
|
||||
8.0,
|
||||
);
|
||||
doc.objects
|
||||
.insert(img_h, ObjectType::ImageDefinition(img_def));
|
||||
let mut img =
|
||||
codec::entities::RasterImage::new("img.png", codec::types::Vector3::ZERO, 8.0, 8.0);
|
||||
img.definition_handle = Some(img_h);
|
||||
doc.add_entity(codec::EntityType::RasterImage(img)).unwrap();
|
||||
// Unreferenced PDF definition → listed, never merged.
|
||||
let pdf_h = doc.allocate_handle();
|
||||
let mut pdf_def = UnderlayDefinition::pdf("doc.pdf", "1");
|
||||
pdf_def.handle = pdf_h;
|
||||
doc.objects.insert(pdf_h, ObjectType::UnderlayDefinition(pdf_def));
|
||||
doc.objects
|
||||
.insert(pdf_h, ObjectType::UnderlayDefinition(pdf_def));
|
||||
|
||||
let empty = std::collections::HashSet::new();
|
||||
let no_prev = std::collections::HashMap::new();
|
||||
// 1. List: everything visible with the right statuses.
|
||||
let listed = collect_entries_with_prev(&doc, &dir, &empty, &no_prev);
|
||||
assert!(listed.iter().any(|e| e.name == "MID" && e.status == RefStatus::Loaded));
|
||||
assert!(listed.iter().any(|e| e.name == "INNER" && e.parent_key.is_some()));
|
||||
assert!(listed.iter().any(|e| e.name == "img.png" && e.status == RefStatus::Loaded));
|
||||
assert!(listed.iter().any(|e| e.name == "doc.pdf" && e.status == RefStatus::Unreferenced));
|
||||
assert!(listed
|
||||
.iter()
|
||||
.any(|e| e.name == "MID" && e.status == RefStatus::Loaded));
|
||||
assert!(listed
|
||||
.iter()
|
||||
.any(|e| e.name == "INNER" && e.parent_key.is_some()));
|
||||
assert!(listed
|
||||
.iter()
|
||||
.any(|e| e.name == "img.png" && e.status == RefStatus::Loaded));
|
||||
assert!(listed
|
||||
.iter()
|
||||
.any(|e| e.name == "doc.pdf" && e.status == RefStatus::Unreferenced));
|
||||
// 2. Unload: definition retained, content marked.
|
||||
unload_reference(&mut doc, mid_key).expect("unload");
|
||||
let mut unloaded = std::collections::HashSet::new();
|
||||
unloaded.insert(crate::io::xref_model::UnloadKey::Direct(mid_key));
|
||||
let listed = collect_entries_with_prev(&doc, &dir, &unloaded, &no_prev);
|
||||
assert!(listed.iter().any(|e| e.name == "MID" && e.status == RefStatus::Unloaded));
|
||||
assert!(doc.block_records.get("MID").is_some(), "definition retained");
|
||||
assert!(listed
|
||||
.iter()
|
||||
.any(|e| e.name == "MID" && e.status == RefStatus::Unloaded));
|
||||
assert!(
|
||||
doc.block_records.get("MID").is_some(),
|
||||
"definition retained"
|
||||
);
|
||||
// 3. Targeted reload: back to Loaded.
|
||||
let mut handles = HashSet::default();
|
||||
handles.insert(doc.block_records.get("MID").unwrap().handle);
|
||||
let (infos, _) = resolve_xrefs_for_keys(&mut doc, &dir, &handles);
|
||||
assert!(infos.iter().any(|i| i.name == "MID"));
|
||||
let listed = collect_entries_with_prev(&doc, &dir, &empty, &no_prev);
|
||||
assert!(listed.iter().any(|e| e.name == "MID" && e.status == RefStatus::Loaded));
|
||||
assert!(listed
|
||||
.iter()
|
||||
.any(|e| e.name == "MID" && e.status == RefStatus::Loaded));
|
||||
// 4. Detach: instances, definition, and nested rows all gone.
|
||||
detach_reference(&mut doc, mid_key).expect("detach");
|
||||
let listed = collect_entries_with_prev(&doc, &dir, &empty, &no_prev);
|
||||
@@ -2776,7 +2768,10 @@ mod tests {
|
||||
line.common.handle = codec::types::Handle::NULL;
|
||||
line.common.owner_handle = br_h;
|
||||
doc.add_entity(codec::EntityType::Line(line)).unwrap();
|
||||
assert_eq!(doc.block_records.get("PLAN").unwrap().entity_handles.len(), 1);
|
||||
assert_eq!(
|
||||
doc.block_records.get("PLAN").unwrap().entity_handles.len(),
|
||||
1
|
||||
);
|
||||
unload_reference(&mut doc, key).expect("unload");
|
||||
assert!(doc.block_records.get("PLAN").is_some());
|
||||
let owned: Vec<_> = doc
|
||||
@@ -2814,7 +2809,8 @@ mod tests {
|
||||
assert!(kept.contains_key("PLAN|WALLS"));
|
||||
// Simulate the purge + file-default re-merge.
|
||||
doc.layers.remove("PLAN|WALLS");
|
||||
doc.layers.add_or_replace(codec::tables::Layer::new("PLAN|WALLS"));
|
||||
doc.layers
|
||||
.add_or_replace(codec::tables::Layer::new("PLAN|WALLS"));
|
||||
assert!(!doc.layers.get("PLAN|WALLS").unwrap().flags.off);
|
||||
restore_dependent_layers(&mut doc, "PLAN", kept);
|
||||
let back = doc.layers.get("PLAN|WALLS").unwrap();
|
||||
@@ -2883,10 +2879,7 @@ mod tests {
|
||||
let (mut doc, _) = xref_doc_with("PLAN", "aé/x.dwg");
|
||||
let n = replace_path_prefix(&mut doc, "a/", "q/");
|
||||
assert_eq!(n, 0);
|
||||
assert_eq!(
|
||||
doc.block_records.get("PLAN").unwrap().xref_path,
|
||||
"aé/x.dwg"
|
||||
);
|
||||
assert_eq!(doc.block_records.get("PLAN").unwrap().xref_path, "aé/x.dwg");
|
||||
let (mut doc2, _) = xref_doc_with("PLAN", "répértoire/old/plan.dwg");
|
||||
let n2 = replace_path_prefix(&mut doc2, "répértoire/old", "répértoire/new");
|
||||
assert_eq!(n2, 1);
|
||||
@@ -3029,7 +3022,10 @@ mod tests {
|
||||
assert_eq!(outcome.name, "PLAN");
|
||||
assert!(host.layers.get("PLAN$0$DETAIL$0$WALLS").is_some());
|
||||
assert!(host.layers.get("PLAN$0$PLANLAYER").is_some());
|
||||
let child = host.block_records.get("PLAN$0$DETAIL").expect("bound child block");
|
||||
let child = host
|
||||
.block_records
|
||||
.get("PLAN$0$DETAIL")
|
||||
.expect("bound child block");
|
||||
assert!(!child.flags.is_xref && !child.flags.is_xref_overlay);
|
||||
assert!(!host
|
||||
.block_records
|
||||
@@ -3052,10 +3048,7 @@ mod tests {
|
||||
inner.common.layer = "WALLS".to_string();
|
||||
inner.common.owner_handle = door_h;
|
||||
doc.add_entity(codec::EntityType::Line(inner)).unwrap();
|
||||
let ins = codec::entities::Insert::new(
|
||||
"DOOR",
|
||||
codec::types::Vector3::ZERO,
|
||||
);
|
||||
let ins = codec::entities::Insert::new("DOOR", codec::types::Vector3::ZERO);
|
||||
doc.add_entity(codec::EntityType::Insert(ins)).unwrap();
|
||||
// Hatch (pattern names have no table — preserved verbatim).
|
||||
let mut hatch = codec::entities::Hatch::new();
|
||||
@@ -3091,8 +3084,7 @@ mod tests {
|
||||
let hatches: Vec<_> = host
|
||||
.entities()
|
||||
.filter(|e| {
|
||||
e.common().owner_handle == br.handle
|
||||
&& matches!(e, codec::EntityType::Hatch(_))
|
||||
e.common().owner_handle == br.handle && matches!(e, codec::EntityType::Hatch(_))
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(hatches.len(), 1);
|
||||
@@ -3146,8 +3138,7 @@ mod tests {
|
||||
let lines: Vec<_> = host
|
||||
.entities()
|
||||
.filter(|e| {
|
||||
e.common().owner_handle == br.handle
|
||||
&& matches!(e, codec::EntityType::Line(_))
|
||||
e.common().owner_handle == br.handle && matches!(e, codec::EntityType::Line(_))
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(lines.len(), 1);
|
||||
@@ -3238,9 +3229,13 @@ mod tests {
|
||||
let mut def = UnderlayDefinition::pdf("doc.pdf", "1");
|
||||
def.handle = h;
|
||||
host.objects.insert(h, ObjectType::UnderlayDefinition(def));
|
||||
let err =
|
||||
bind_reference(&mut host, h.value(), std::path::Path::new("."), std::path::Path::new("."))
|
||||
.unwrap_err();
|
||||
let err = bind_reference(
|
||||
&mut host,
|
||||
h.value(),
|
||||
std::path::Path::new("."),
|
||||
std::path::Path::new("."),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
err,
|
||||
"XREF: PDF bind (vector import) is not available in this version."
|
||||
@@ -3275,7 +3270,10 @@ fn relative_point_cloud_path(doc: &mut CadDocument, handle: Handle, host: &Path)
|
||||
};
|
||||
if let Ok(relative) = to_pathtype_result(&def.source_filename, host, Pathtype::Relative) {
|
||||
let relative = relative.replace('/', "\\");
|
||||
if let Some(found) = std::path::Path::new(&def.source_filename).is_file().then(|| def.source_filename.clone()) {
|
||||
if let Some(found) = std::path::Path::new(&def.source_filename)
|
||||
.is_file()
|
||||
.then(|| def.source_filename.clone())
|
||||
{
|
||||
crate::scene::model::point_cloud::register_source(&relative, found.into());
|
||||
}
|
||||
def.source_filename = relative;
|
||||
|
||||
+47
-30
@@ -17,25 +17,23 @@ fn normalize_core(raw: &str, fold_case: bool) -> String {
|
||||
|
||||
// 2. Prefix split: drive (`C:`), UNC (`//`), or none.
|
||||
let bytes = slashed.as_bytes();
|
||||
let (prefix, mut rest, unc) = if bytes.len() >= 2
|
||||
&& bytes[0].is_ascii_alphabetic()
|
||||
&& bytes[1] == b':'
|
||||
{
|
||||
let drive = &slashed[..2];
|
||||
(
|
||||
if fold_case {
|
||||
drive.to_ascii_lowercase()
|
||||
} else {
|
||||
drive.to_string()
|
||||
},
|
||||
slashed[2..].to_string(),
|
||||
false,
|
||||
)
|
||||
} else if let Some(stripped) = slashed.strip_prefix("//") {
|
||||
("//".to_string(), stripped.to_string(), true)
|
||||
} else {
|
||||
(String::new(), slashed, false)
|
||||
};
|
||||
let (prefix, mut rest, unc) =
|
||||
if bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' {
|
||||
let drive = &slashed[..2];
|
||||
(
|
||||
if fold_case {
|
||||
drive.to_ascii_lowercase()
|
||||
} else {
|
||||
drive.to_string()
|
||||
},
|
||||
slashed[2..].to_string(),
|
||||
false,
|
||||
)
|
||||
} else if let Some(stripped) = slashed.strip_prefix("//") {
|
||||
("//".to_string(), stripped.to_string(), true)
|
||||
} else {
|
||||
(String::new(), slashed, false)
|
||||
};
|
||||
if unc {
|
||||
// Collapse `///share` → `//share` so the rejoin can't triple-slash.
|
||||
rest = rest.trim_start_matches('/').to_string();
|
||||
@@ -147,9 +145,15 @@ fn comps_after_root(path_norm: &str) -> Vec<&str> {
|
||||
let is_drive = b.len() >= 2 && b[0].is_ascii_alphabetic() && b[1] == b':';
|
||||
let is_unc = path_norm.starts_with("//");
|
||||
if is_drive {
|
||||
path_norm[2..].split('/').filter(|s| !s.is_empty()).collect()
|
||||
path_norm[2..]
|
||||
.split('/')
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect()
|
||||
} else if is_unc {
|
||||
let all: Vec<&str> = path_norm[2..].split('/').filter(|s| !s.is_empty()).collect();
|
||||
let all: Vec<&str> = path_norm[2..]
|
||||
.split('/')
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
if all.len() >= 2 {
|
||||
all[2..].to_vec()
|
||||
} else {
|
||||
@@ -191,7 +195,10 @@ pub fn to_pathtype_result(
|
||||
if is_relative_path(&normalize_lexical(path)) && !normalize_lexical(path).is_empty() {
|
||||
if let Some(dir) = host.parent() {
|
||||
if !dir.as_os_str().is_empty() {
|
||||
let joined = dir.join(normalize_display(path)).to_string_lossy().into_owned();
|
||||
let joined = dir
|
||||
.join(normalize_display(path))
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
return Ok(normalize_display(&joined));
|
||||
}
|
||||
}
|
||||
@@ -468,7 +475,10 @@ pub fn decide_status(
|
||||
}
|
||||
if resolved == RefStatus::Loaded {
|
||||
if let (Some(l), Some(c)) = (live, cached) {
|
||||
let delta = l.duration_since(c).ok().or_else(|| c.duration_since(l).ok());
|
||||
let delta = l
|
||||
.duration_since(c)
|
||||
.ok()
|
||||
.or_else(|| c.duration_since(l).ok());
|
||||
if delta.is_some_and(|d| d > std::time::Duration::from_secs(1)) {
|
||||
return RefStatus::Stale;
|
||||
}
|
||||
@@ -561,9 +571,12 @@ impl RefStatCache {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{normalize_display, normalize_lexical, to_pathtype, to_pathtype_result, wildcard_match, Pathtype, PathtypeError};
|
||||
use super::{bind_symbol, bind_symbol_taken, RefKind, ReferenceEntry};
|
||||
use super::{child_key, decide_status, RefStatCache, RefStatus, UnloadSet};
|
||||
use super::{
|
||||
normalize_display, normalize_lexical, to_pathtype, to_pathtype_result, wildcard_match,
|
||||
Pathtype, PathtypeError,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn child_key_differs_from_colliding_host_key() {
|
||||
@@ -778,8 +791,7 @@ mod tests {
|
||||
|
||||
// Cell 2: raster image definition with an absolute file path.
|
||||
let img_handle = doc.allocate_handle();
|
||||
let mut img_def =
|
||||
ImageDefinition::with_dimensions(r"C:\Probe\IMG.png", 64u32, 64u32);
|
||||
let mut img_def = ImageDefinition::with_dimensions(r"C:\Probe\IMG.png", 64u32, 64u32);
|
||||
img_def.handle = img_handle;
|
||||
img_def.is_loaded = true;
|
||||
doc.objects
|
||||
@@ -795,8 +807,7 @@ mod tests {
|
||||
// Cell 4: the retain flag itself ($VISRETAIN).
|
||||
doc.header.retain_xref_visibility = true;
|
||||
|
||||
let bytes = crate::io::save_to_bytes(&doc, ext, doc.version)
|
||||
.expect("probe save");
|
||||
let bytes = crate::io::save_to_bytes(&doc, ext, doc.version).expect("probe save");
|
||||
std::fs::write(dir.join(format!("probe.{ext}")), &bytes).expect("probe write");
|
||||
|
||||
let back = crate::io::load_bytes(&format!("probe.{ext}"), bytes).expect("probe reload");
|
||||
@@ -880,7 +891,13 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn bind_chain_naming_transitive() {
|
||||
assert_eq!(bind_symbol("PLAN", "DETAIL", "WALLS"), "PLAN$0$DETAIL$0$WALLS");
|
||||
assert_eq!(bind_symbol_taken("PLAN", "WALLS", &["PLAN$0$WALLS"]), "PLAN$1$WALLS");
|
||||
assert_eq!(
|
||||
bind_symbol("PLAN", "DETAIL", "WALLS"),
|
||||
"PLAN$0$DETAIL$0$WALLS"
|
||||
);
|
||||
assert_eq!(
|
||||
bind_symbol_taken("PLAN", "WALLS", &["PLAN$0$WALLS"]),
|
||||
"PLAN$1$WALLS"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user