style: apply cargo fmt across workspace

This commit is contained in:
Karim Jerbi committed 2026-10-11 03:14:29 +01:00
1 parent 85710e5454
commit 9cf9ff2c5c
531 files changed
+57740 -33906

No files matched your search

+1 -1
View File
@@ -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();
+31 -9
View File
@@ -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
View File
@@ -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
View File
@@ -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(&lt.name.to_ascii_uppercase());
if let Some(existing) = doc.line_types.get(&lt.name) {
let stripped = segments.is_some()
&& !existing.is_complex()
&& existing.elements.len() == lt.elements.len()
&& existing
.elements
.iter()
.zip(&lt.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(&lt.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, &lt.elements)) {
if let Some(elements) =
segments.and_then(|s| complex_elements(doc, s, &lt.elements))
{
if let Some(existing) = doc.line_types.get_mut(&lt.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
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+6 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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),
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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()
};
+1 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
);
}
}