style: rustfmt the tree so the CI lint check passes

This commit is contained in:
Karim Jerbi committed 2026-10-15 04:10:06 +01:00
1 parent 1317a7b4fd
commit 6025d8be44
128 files changed
+4097 -1676

No files matched your search

+40 -8
View File
@@ -160,7 +160,11 @@ fn mesh_nodes(mesh: &MeshModel, index: usize) -> Option<([Node; 3], [Node; 3])>
continue;
}
// The last corner of a polygon is stored as its bitwise complement.
polygon_indices.extend([triangle[0] as i32, triangle[1] as i32, !(triangle[2] as i32)]);
polygon_indices.extend([
triangle[0] as i32,
triangle[1] as i32,
!(triangle[2] as i32),
]);
for _ in 0..3 {
normals.extend(n.map(|value| value / length));
}
@@ -173,10 +177,18 @@ fn mesh_nodes(mesh: &MeshModel, index: usize) -> Option<([Node; 3], [Node; 3])>
let name = format!("Mesh{}", index + 1);
let geometry = node(
"Geometry",
vec![Prop::I64(geometry_id), s(&format!("{name}\x00\x01Geometry")), s("Mesh")],
vec![
Prop::I64(geometry_id),
s(&format!("{name}\x00\x01Geometry")),
s("Mesh"),
],
vec![
node("Vertices", vec![Prop::F64s(points.concat())], Vec::new()),
node("PolygonVertexIndex", vec![Prop::I32s(polygon_indices)], Vec::new()),
node(
"PolygonVertexIndex",
vec![Prop::I32s(polygon_indices)],
Vec::new(),
),
node("GeometryVersion", vec![Prop::I32(124)], Vec::new()),
node(
"LayerElementNormal",
@@ -184,7 +196,11 @@ fn mesh_nodes(mesh: &MeshModel, index: usize) -> Option<([Node; 3], [Node; 3])>
vec![
node("Version", vec![Prop::I32(101)], Vec::new()),
node("Name", vec![s("")], Vec::new()),
node("MappingInformationType", vec![s("ByPolygonVertex")], Vec::new()),
node(
"MappingInformationType",
vec![s("ByPolygonVertex")],
Vec::new(),
),
node("ReferenceInformationType", vec![s("Direct")], Vec::new()),
node("Normals", vec![Prop::F64s(normals)], Vec::new()),
],
@@ -196,7 +212,11 @@ fn mesh_nodes(mesh: &MeshModel, index: usize) -> Option<([Node; 3], [Node; 3])>
node("Version", vec![Prop::I32(101)], Vec::new()),
node("Name", vec![s("")], Vec::new()),
node("MappingInformationType", vec![s("AllSame")], Vec::new()),
node("ReferenceInformationType", vec![s("IndexToDirect")], Vec::new()),
node(
"ReferenceInformationType",
vec![s("IndexToDirect")],
Vec::new(),
),
node("Materials", vec![Prop::I32s(vec![0])], Vec::new()),
],
),
@@ -227,7 +247,11 @@ fn mesh_nodes(mesh: &MeshModel, index: usize) -> Option<([Node; 3], [Node; 3])>
);
let model = node(
"Model",
vec![Prop::I64(model_id), s(&format!("{name}\x00\x01Model")), s("Mesh")],
vec![
Prop::I64(model_id),
s(&format!("{name}\x00\x01Model")),
s("Mesh"),
],
vec![
node("Version", vec![Prop::I32(232)], Vec::new()),
node("Properties70", Vec::new(), Vec::new()),
@@ -328,7 +352,12 @@ pub fn build_fbx(meshes: &[&MeshModel], unit_cm: f64) -> Option<Vec<u8>> {
axis("FrontAxisSign", -1),
axis("CoordAxis", 0),
axis("CoordAxisSign", 1),
p("UnitScaleFactor", "double", "Number", vec![Prop::F64(unit_cm)]),
p(
"UnitScaleFactor",
"double",
"Number",
vec![Prop::F64(unit_cm)],
),
],
),
],
@@ -437,7 +466,10 @@ mod tests {
#[test]
fn a_triangle_writes_a_well_formed_file() {
let triangle = mesh(vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], vec![0, 1, 2]);
let triangle = mesh(
vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
vec![0, 1, 2],
);
let bytes = build_fbx(&[&triangle], 0.1).expect("fbx");
assert!(bytes.starts_with(HEADER));
assert_eq!(
+4 -1
View File
@@ -243,7 +243,10 @@ mod windows_impl {
"ms-settings:defaultapps?registeredAppUser={}",
APP_REGISTRY_NAME.replace(' ', "%20")
);
std::process::Command::new("explorer.exe").arg(uri).spawn().ok()?;
std::process::Command::new("explorer.exe")
.arg(uri)
.spawn()
.ok()?;
Some(
"Opened Settings → Default apps — choose Open CAD Studio for .dwg / .dxf there."
.to_string(),
+4 -1
View File
@@ -25,7 +25,10 @@ static FONTS_FOLDER: std::sync::RwLock<Option<PathBuf>> = std::sync::RwLock::new
/// such as a share on the office network.
#[cfg(not(target_arch = "wasm32"))]
pub fn fonts_folder() -> Option<PathBuf> {
FONTS_FOLDER.read().unwrap_or_else(|e| e.into_inner()).clone()
FONTS_FOLDER
.read()
.unwrap_or_else(|e| e.into_inner())
.clone()
}
#[cfg(not(target_arch = "wasm32"))]
+56 -20
View File
@@ -178,7 +178,10 @@ mod browser {
/// inside the picker) — what save bookkeeping should use.
pub fn handle_name(tab_id: u64) -> Option<String> {
SAVE_HANDLES.with(|handles| {
handles.borrow().get(&tab_id).map(|handle| handle.name.clone())
handles
.borrow()
.get(&tab_id)
.map(|handle| handle.name.clone())
})
}
@@ -197,10 +200,8 @@ mod browser {
/// the bytes against the retained handle (`write_or_download`).
pub fn begin_web_save(tab_id: u64, ext: &str) {
PENDING_SAVE_EXTS.with(|exts| {
exts.borrow_mut().insert(
tab_id,
ext.trim_start_matches('.').to_ascii_lowercase(),
);
exts.borrow_mut()
.insert(tab_id, ext.trim_start_matches('.').to_ascii_lowercase());
});
WEB_SAVE_IN_FLIGHT.with(|in_flight| in_flight.borrow_mut().replace(tab_id));
}
@@ -264,10 +265,11 @@ mod browser {
let window = web_sys::window().ok_or_else(|| "browser window unavailable".to_string())?;
// Dynamic call — see the module docs for why the typed web-sys
// binding (`show_save_file_picker_with_options`) is not used.
let picker = js_sys::Reflect::get(window.as_ref(), &JsValue::from_str("showSaveFilePicker"))
.ok()
.and_then(|picker| picker.dyn_into::<js_sys::Function>().ok())
.ok_or_else(|| "showSaveFilePicker unavailable".to_string())?;
let picker =
js_sys::Reflect::get(window.as_ref(), &JsValue::from_str("showSaveFilePicker"))
.ok()
.and_then(|picker| picker.dyn_into::<js_sys::Function>().ok())
.ok_or_else(|| "showSaveFilePicker unavailable".to_string())?;
let options = picker_options(suggested_name, ext);
let promise = picker
.call1(window.as_ref(), &options)
@@ -315,7 +317,10 @@ mod browser {
/// the same way for the rest of the session.
pub async fn write_handle(tab_id: u64, bytes: &[u8]) -> Result<(), String> {
let handle = SAVE_HANDLES.with(|handles| {
handles.borrow().get(&tab_id).map(|save| save.handle.clone())
handles
.borrow()
.get(&tab_id)
.map(|save| save.handle.clone())
});
let Some(handle) = handle else {
return Err("no retained save destination for this drawing".to_string());
@@ -362,7 +367,10 @@ mod browser {
};
let accept = js_sys::Object::new();
let extensions = js_sys::Array::new();
extensions.push(&JsValue::from_str(&format!(".{}", ext.trim_start_matches('.'))));
extensions.push(&JsValue::from_str(&format!(
".{}",
ext.trim_start_matches('.')
)));
set(&accept, super::accept_mime(ext), &extensions);
let type_entry = js_sys::Object::new();
set(
@@ -378,7 +386,11 @@ mod browser {
let types = js_sys::Array::new();
types.push(&type_entry);
let options = js_sys::Object::new();
set(&options, "suggestedName", &JsValue::from_str(suggested_name));
set(
&options,
"suggestedName",
&JsValue::from_str(suggested_name),
);
set(&options, "types", &types);
options
}
@@ -398,7 +410,10 @@ mod tests {
/// whatever handle state the tab happens to hold.
#[test]
fn unsupported_browsers_always_download() {
assert_eq!(save_route(false, true, Some("dwg"), "dwg"), SaveRoute::Download);
assert_eq!(
save_route(false, true, Some("dwg"), "dwg"),
SaveRoute::Download
);
assert_eq!(save_route(false, false, None, "dxf"), SaveRoute::Download);
}
@@ -406,30 +421,51 @@ mod tests {
/// in place — no picker, no download.
#[test]
fn plain_save_with_matching_handle_writes_in_place() {
assert_eq!(save_route(true, true, Some("dwg"), "dwg"), SaveRoute::WriteInPlace);
assert_eq!(save_route(true, true, Some("dxf"), "dxf"), SaveRoute::WriteInPlace);
assert_eq!(
save_route(true, true, Some("dwg"), "dwg"),
SaveRoute::WriteInPlace
);
assert_eq!(
save_route(true, true, Some("dxf"), "dxf"),
SaveRoute::WriteInPlace
);
}
/// Extension comparison is case-insensitive and dot-tolerant — the
/// registry stores the normalized form, the format table's is bare.
#[test]
fn handle_ext_match_ignores_case_and_dot() {
assert_eq!(save_route(true, true, Some(".DWG"), "dwg"), SaveRoute::WriteInPlace);
assert_eq!(save_route(true, true, Some("dwg"), ".DWG"), SaveRoute::WriteInPlace);
assert_eq!(
save_route(true, true, Some(".DWG"), "dwg"),
SaveRoute::WriteInPlace
);
assert_eq!(
save_route(true, true, Some("dwg"), ".DWG"),
SaveRoute::WriteInPlace
);
}
/// A plain Save targeting the other format must re-pick: writing DXF
/// bytes through a .dwg handle would corrupt the file's identity.
#[test]
fn mismatched_format_re_picks() {
assert_eq!(save_route(true, true, Some("dwg"), "dxf"), SaveRoute::PickHandle);
assert_eq!(save_route(true, true, Some("dxf"), "dwg"), SaveRoute::PickHandle);
assert_eq!(
save_route(true, true, Some("dwg"), "dxf"),
SaveRoute::PickHandle
);
assert_eq!(
save_route(true, true, Some("dxf"), "dwg"),
SaveRoute::PickHandle
);
}
/// Save As never reuses the handle — the user asked for a destination.
#[test]
fn save_as_with_existing_handle_re_picks() {
assert_eq!(save_route(true, false, Some("dwg"), "dwg"), SaveRoute::PickHandle);
assert_eq!(
save_route(true, false, Some("dwg"), "dwg"),
SaveRoute::PickHandle
);
}
/// No handle yet (first save, or the previous write dropped it): pick.
+4 -1
View File
@@ -236,7 +236,10 @@ const LIN_SOURCE: &str = include_str!("../../assets/linetypes/OpenCADStudio.lin"
fn lin_source() -> &'static str {
static SOURCE: OnceLock<String> = OnceLock::new();
SOURCE.get_or_init(|| {
format!("{LIN_SOURCE}\n{}", crate::io::patterns::user_definitions("linetypes", "lin"))
format!(
"{LIN_SOURCE}\n{}",
crate::io::patterns::user_definitions("linetypes", "lin")
)
})
}
+24 -23
View File
@@ -8,9 +8,10 @@ pub mod edit_lock;
// Same split as web_session/web_autosave: the save-routing decision (and
// its tests) is pure and compiles everywhere; the showSaveFilePicker half
// inside is wasm-only.
pub(crate) mod fs_access;
pub mod fbx;
pub mod file_association;
pub mod font_repo;
pub(crate) mod fs_access;
pub mod line_read;
pub mod linetypes;
pub mod obj;
@@ -25,7 +26,6 @@ pub mod print_to_printer;
pub mod recovery;
#[cfg(not(target_arch = "wasm32"))]
pub mod single_instance;
pub mod fbx;
pub mod step;
pub mod stl;
pub mod svg_export;
@@ -38,9 +38,9 @@ pub(crate) mod web_recent;
// the native host; the OPFS half inside is wasm-only.
pub(crate) mod web_autosave;
#[cfg(target_arch = "wasm32")]
mod web_worker;
#[cfg(target_arch = "wasm32")]
pub(crate) mod web_drop;
#[cfg(target_arch = "wasm32")]
mod web_worker;
// Same split as web_autosave: the ?file= deep-link parser (and its tests) are
// pure and compile everywhere; the fetch/open half is wasm-only.
pub(crate) mod web_link;
@@ -57,9 +57,9 @@ use codec::entities::EntityType;
// parse worker / the xref bytes registry instead.
#[cfg(not(target_arch = "wasm32"))]
use codec::io::dwg::DwgReader;
use codec::{CadDocument, DwgWriter, DxfWriter};
#[cfg(not(target_arch = "wasm32"))]
use codec::{DwgReadOptions, DxfReader, DxfReaderConfiguration};
use codec::{CadDocument, DwgWriter, DxfWriter};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU16, AtomicU32, AtomicU8, Ordering};
use std::sync::Arc;
@@ -293,7 +293,10 @@ pub async fn pick_linetype_file_web() -> Option<(String, String)> {
pub async fn pick_datalink_csv_web() -> Option<(String, Vec<u8>)> {
let Some(handle) = crate::sys::file_dialog()
.set_title(crate::t!("Link a CSV data source").as_ref())
.add_filter(crate::t!("CSV Files").as_ref(), &["csv", "txt", "CSV", "TXT"])
.add_filter(
crate::t!("CSV Files").as_ref(),
&["csv", "txt", "CSV", "TXT"],
)
.add_filter(crate::t!("All Files").as_ref(), &["*"])
.pick_file()
.await
@@ -2003,8 +2006,7 @@ pub async fn pick_plot_style() -> Result<Option<plot_style::PlotStyleTable>, Str
{
let path = crate::sys::handle_path(&handle);
let table = plot_style::PlotStyleTable::load(&path)?;
if let (Ok(dir), Some(file_name)) =
(plot_style::ensure_plot_styles_dir(), path.file_name())
if let (Ok(dir), Some(file_name)) = (plot_style::ensure_plot_styles_dir(), path.file_name())
{
let destination = dir.join(file_name);
let same_place = path.parent().is_some_and(|parent| {
@@ -3534,10 +3536,8 @@ mod prepare_save_tests {
fn prepare_bakes_missing_dimension_blocks() {
let mut doc = CadDocument::new();
crate::io::linetypes::populate_document(&mut doc);
let mut dim = DimensionLinear::new(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(10.0, 0.0, 0.0),
);
let mut dim =
DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
dim.definition_point = Vector3::new(5.0, 7.0, 0.0);
dim.base.definition_point = Vector3::ZERO;
dim.base.common.handle = doc.allocate_handle();
@@ -3546,8 +3546,7 @@ mod prepare_save_tests {
.unwrap();
let prepared = prepare_document_for_save(doc.clone(), doc.version);
let EntityType::Dimension(Dimension::Linear(linear)) =
prepared.get_entity(handle).unwrap()
let EntityType::Dimension(Dimension::Linear(linear)) = prepared.get_entity(handle).unwrap()
else {
panic!("dimension missing from prepared clone");
};
@@ -3556,13 +3555,14 @@ mod prepare_save_tests {
"anonymous dimension block not baked: {}",
linear.base.block_name
);
assert!(prepared.block_records.get(&linear.base.block_name).is_some());
assert!(prepared
.block_records
.get(&linear.base.block_name)
.is_some());
assert_eq!(linear.base.definition_point, linear.definition_point);
// The live document is untouched: no block name, stale group-10.
let EntityType::Dimension(Dimension::Linear(live)) =
doc.get_entity(handle).unwrap()
else {
let EntityType::Dimension(Dimension::Linear(live)) = doc.get_entity(handle).unwrap() else {
panic!("dimension missing from live document");
};
assert!(live.base.block_name.is_empty());
@@ -3576,10 +3576,8 @@ mod prepare_save_tests {
fn save_to_bytes_equals_prepare_then_shared_serialize() {
let mut doc = CadDocument::new();
crate::io::linetypes::populate_document(&mut doc);
let mut dim = DimensionLinear::new(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(10.0, 0.0, 0.0),
);
let mut dim =
DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
dim.base.common.handle = doc.allocate_handle();
doc.add_entity(EntityType::Dimension(Dimension::Linear(dim)))
.unwrap();
@@ -3587,7 +3585,10 @@ mod prepare_save_tests {
let inline = save_to_bytes(&doc, ext, codec::DxfVersion::AC1032).expect("inline");
let prepared = prepare_document_for_save(doc.clone(), codec::DxfVersion::AC1032);
let shared = ocs_save::serialize_document(&prepared, ext).expect("shared");
assert_eq!(inline, shared, "inline and shared serialize diverge for {ext}");
assert_eq!(
inline, shared,
"inline and shared serialize diverge for {ext}"
);
}
}
}
+10 -2
View File
@@ -42,7 +42,12 @@ static CATALOG: OnceLock<Vec<PatternDef>> = OnceLock::new();
/// All parsed patterns, in file order: the bundled catalog, then the user's
/// own `.pat` files.
pub fn catalog() -> &'static [PatternDef] {
CATALOG.get_or_init(|| parse(&format!("{PAT_SRC}\n{}", user_definitions("patterns", "pat"))))
CATALOG.get_or_init(|| {
parse(&format!(
"{PAT_SRC}\n{}",
user_definitions("patterns", "pat")
))
})
}
/// The text of every `*.<ext>` file in `<config>/<folder>`, where users drop
@@ -59,7 +64,10 @@ pub(crate) fn user_definitions(folder: &str, ext: &str) -> String {
let mut paths: Vec<_> = entries
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| path.extension().is_some_and(|e| e.eq_ignore_ascii_case(ext)))
.filter(|path| {
path.extension()
.is_some_and(|e| e.eq_ignore_ascii_case(ext))
})
.collect();
paths.sort();
paths
+18 -4
View File
@@ -185,7 +185,8 @@ fn emit_image(
let mut height = image.height;
let mut pixels: std::borrow::Cow<[u8]> = std::borrow::Cow::Borrowed(image.pixels.as_slice());
if let Some(target) = options.quality.target_dpi() {
let dist = |a: [f32; 2], b: [f32; 2]| ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt();
let dist =
|a: [f32; 2], b: [f32; 2]| ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt();
let width_edge = dist(corners[0], corners[1]);
let height_edge = dist(corners[0], corners[3]);
// A degenerate (zero-area) placement keeps the native pixels.
@@ -208,8 +209,12 @@ fn emit_image(
if let Some(buffer) =
image::RgbaImage::from_raw(width, height, image.pixels.as_ref().clone())
{
let resized =
image::imageops::resize(&buffer, w, h, image::imageops::FilterType::Triangle);
let resized = image::imageops::resize(
&buffer,
w,
h,
image::imageops::FilterType::Triangle,
);
width = w;
height = h;
pixels = std::borrow::Cow::Owned(resized.into_raw());
@@ -751,7 +756,16 @@ fn append_pdf_page(
continue;
}
DrawItem::Image(image) => {
emit_image(doc, image_resources, &mut ops, image, ox, oy, scale, options)?;
emit_image(
doc,
image_resources,
&mut ops,
image,
ox,
oy,
scale,
options,
)?;
continue;
}
DrawItem::Wire(wire) => wire,
+1 -1
View File
@@ -7,9 +7,9 @@
// the historical `Pdf*` names as aliases so existing call sites keep working.
use crate::io::plot_style::PlotStyleTable;
use crate::scene::WireModel;
use crate::scene::model::hatch_model::HatchModel;
use crate::scene::model::image_model::ImageModel;
use crate::scene::WireModel;
#[derive(Clone, Debug)]
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
+7 -2
View File
@@ -41,12 +41,17 @@ static PLOT_STYLES_FOLDER: std::sync::RwLock<Option<PathBuf>> = std::sync::RwLoc
/// default one, such as a share on the office network.
#[cfg(not(target_arch = "wasm32"))]
pub fn plot_styles_folder() -> Option<PathBuf> {
PLOT_STYLES_FOLDER.read().unwrap_or_else(|e| e.into_inner()).clone()
PLOT_STYLES_FOLDER
.read()
.unwrap_or_else(|e| e.into_inner())
.clone()
}
#[cfg(not(target_arch = "wasm32"))]
pub fn set_plot_styles_folder(folder: Option<PathBuf>) {
*PLOT_STYLES_FOLDER.write().unwrap_or_else(|e| e.into_inner()) = folder;
*PLOT_STYLES_FOLDER
.write()
.unwrap_or_else(|e| e.into_inner()) = folder;
}
#[cfg(not(target_arch = "wasm32"))]
+145 -34
View File
@@ -63,8 +63,7 @@ pub fn export_svg_pages(
.unwrap_or_else(|| "drawing".to_string());
let parent = path.parent().unwrap_or_else(|| Path::new("."));
for (index, page) in pages.iter().enumerate() {
let svg = build_svg(page, plot_style)
.map_err(|e| format!("Page {}: {e}", index + 1))?;
let svg = build_svg(page, plot_style).map_err(|e| format!("Page {}: {e}", index + 1))?;
let numbered = parent.join(format!("{stem}-{}.svg", index + 1));
write_svg_atomically(&numbered, svg.as_bytes())
.map_err(|e| format!("Page {}: {e}", index + 1))?;
@@ -128,7 +127,11 @@ pub fn build_svg(
));
out.push_str(">\n<title>Open CAD Studio plot</title>\n");
out.push_str("<rect x=\"0\" y=\"0\"");
out.push_str(&format!(" width=\"{}\" height=\"{}\" fill=\"#ffffff\"/>\n", fmt(paper_w), fmt(paper_h)));
out.push_str(&format!(
" width=\"{}\" height=\"{}\" fill=\"#ffffff\"/>\n",
fmt(paper_w),
fmt(paper_h)
));
let mut clip_id: Option<String> = None;
if let Some((cx, cy, cw, ch)) = page.clip {
@@ -140,14 +143,25 @@ pub fn build_svg(
map.apply((cx + cw) as f64, (cy + ch) as f64),
map.apply(cx as f64, (cy + ch) as f64),
];
let (mut x0, mut y0, mut x1, mut y1) = (f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
let (mut x0, mut y0, mut x1, mut y1) = (
f64::INFINITY,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NEG_INFINITY,
);
for (x, y) in corners {
x0 = x0.min(x);
y0 = y0.min(y);
x1 = x1.max(x);
y1 = y1.max(y);
}
if x1 > x0 && y1 > y0 && x0.is_finite() && y0.is_finite() && x1.is_finite() && y1.is_finite() {
if x1 > x0
&& y1 > y0
&& x0.is_finite()
&& y0.is_finite()
&& x1.is_finite()
&& y1.is_finite()
{
out.push_str(&format!(
"<clipPath id=\"plot-clip\"><rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\"/></clipPath>\n",
fmt(x0), fmt(y0), fmt(x1 - x0), fmt(y1 - y0)
@@ -238,7 +252,8 @@ fn emit_page_content(
(first_wires, first_hatches, first_wipeouts, first_images),
(second_wires, second_hatches, second_wipeouts, second_images),
] {
let mut items = Vec::with_capacity(wires.len() * 2 + hatches.len() + wipeouts.len() + images.len());
let mut items =
Vec::with_capacity(wires.len() * 2 + hatches.len() + wipeouts.len() + images.len());
let mut sequence = 0usize;
for wire in wires {
if !wire.fill_tris.is_empty() {
@@ -257,7 +272,12 @@ fn emit_page_content(
sequence += 1;
}
for image in images {
items.push((image.image.draw_depth, 1u8, sequence, DrawItem::Image(image)));
items.push((
image.image.draw_depth,
1u8,
sequence,
DrawItem::Image(image),
));
sequence += 1;
}
items.sort_by(|a, b| {
@@ -267,10 +287,16 @@ fn emit_page_content(
});
for (_, _, _, item) in items {
match item {
DrawItem::WireFill(wire) => emit_wire_fills(out, &wire.wire, ox, oy, plot_style, options, map),
DrawItem::WireFill(wire) => {
emit_wire_fills(out, &wire.wire, ox, oy, plot_style, options, map)
}
DrawItem::Hatch(hatch) => emit_hatch(out, hatch, ox, oy, plot_style, options, map),
DrawItem::Text(wire) => emit_searchable_text(out, &wire.wire, ox, oy, plot_style, options, map),
DrawItem::Wire(wire) => emit_wire(out, &wire.wire, ox, oy, plot_style, options, map),
DrawItem::Text(wire) => {
emit_searchable_text(out, &wire.wire, ox, oy, plot_style, options, map)
}
DrawItem::Wire(wire) => {
emit_wire(out, &wire.wire, ox, oy, plot_style, options, map)
}
DrawItem::Image(image) => emit_image(out, image, ox, oy, options, map)?,
}
}
@@ -294,7 +320,10 @@ fn emit_wire(
if a < 0.01 {
return;
}
if matches!(wire.name.as_str(), "__paper_boundary__" | "paper_printable_area") {
if matches!(
wire.name.as_str(),
"__paper_boundary__" | "paper_printable_area"
) {
return;
}
let mut lw_override_mm: Option<f64> = None;
@@ -368,7 +397,12 @@ fn emit_wire(
pen_down = false;
continue;
}
d.push_str(&format!("{} {} {} ", if pen_down { 'L' } else { 'M' }, fmt(sx), fmt(sy)));
d.push_str(&format!(
"{} {} {} ",
if pen_down { 'L' } else { 'M' },
fmt(sx),
fmt(sy)
));
pen_down = true;
any = true;
}
@@ -400,7 +434,11 @@ fn emit_wire_fills(
if a < 0.01 {
return;
}
if wire.bg_adapt.as_deref().is_some_and(|adapt| adapt.canvas_color) {
if wire
.bg_adapt
.as_deref()
.is_some_and(|adapt| adapt.canvas_color)
{
[r, g, b] = [1.0, 1.0, 1.0];
} else {
let mut screening = 1.0;
@@ -424,7 +462,11 @@ fn emit_wire_fills(
let mut pts = Vec::with_capacity(3);
let mut ok = true;
for (pi, &[x, y, _]) in tri.iter().enumerate() {
let low = wire.fill_tris_low.get(ti * 3 + pi).copied().unwrap_or([0.0; 3]);
let low = wire
.fill_tris_low
.get(ti * 3 + pi)
.copied()
.unwrap_or([0.0; 3]);
let (sx, sy) = map.apply(x as f64 + low[0] as f64 + ox, y as f64 + low[1] as f64 + oy);
if !sx.is_finite() || !sy.is_finite() {
ok = false;
@@ -437,9 +479,12 @@ fn emit_wire_fills(
}
d.push_str(&format!(
"M {} {} L {} {} L {} {} Z ",
fmt(pts[0].0), fmt(pts[0].1),
fmt(pts[1].0), fmt(pts[1].1),
fmt(pts[2].0), fmt(pts[2].1),
fmt(pts[0].0),
fmt(pts[0].1),
fmt(pts[1].0),
fmt(pts[1].1),
fmt(pts[2].0),
fmt(pts[2].1),
));
}
if d.is_empty() {
@@ -490,7 +535,10 @@ fn emit_hatch(
}
if is_wipeout {
[r, g, b] = [1.0, 1.0, 1.0];
} else if !color_overridden && hatch.aci != 0 && !(hatch.aci == 7 && matches!(hatch.pattern, HatchPattern::Solid)) {
} else if !color_overridden
&& hatch.aci != 0
&& !(hatch.aci == 7 && matches!(hatch.pattern, HatchPattern::Solid))
{
let is_light = r > 0.80
&& g > 0.80
&& b > 0.80
@@ -514,7 +562,11 @@ fn emit_hatch(
0.1 * PT_TO_MM
}
.max(0.1 * PT_TO_MM);
let width_mm = if options.scale_lineweights { physical * s } else { physical };
let width_mm = if options.scale_lineweights {
physical * s
} else {
physical
};
let segments = hatch.pattern_segments_for_plot();
if segments.is_empty() {
return;
@@ -531,7 +583,13 @@ fn emit_hatch(
let diam = fmt(width_mm.max(0.04));
d.push_str(&format!("M {x} {y} m -{rad} 0 a {rad} {rad} 0 1 0 {diam} 0 a {rad} {rad} 0 1 0 -{diam} 0 ", x = fmt(ax), y = fmt(ay)));
} else {
d.push_str(&format!("M {} {} L {} {} ", fmt(ax), fmt(ay), fmt(bx), fmt(by)));
d.push_str(&format!(
"M {} {} L {} {} ",
fmt(ax),
fmt(ay),
fmt(bx),
fmt(by)
));
}
}
if d.is_empty() {
@@ -554,7 +612,11 @@ fn emit_hatch(
screening,
options,
);
[r, g, b] = [(r + second[0]) * 0.5, (g + second[1]) * 0.5, (b + second[2]) * 0.5];
[r, g, b] = [
(r + second[0]) * 0.5,
(g + second[1]) * 0.5,
(b + second[2]) * 0.5,
];
}
let mut d = String::new();
@@ -572,7 +634,12 @@ fn emit_hatch(
if !sx.is_finite() || !sy.is_finite() {
continue;
}
d.push_str(&format!("{} {} {} ", if current_empty { 'M' } else { 'L' }, fmt(sx), fmt(sy)));
d.push_str(&format!(
"{} {} {} ",
if current_empty { 'M' } else { 'L' },
fmt(sx),
fmt(sy)
));
current_empty = false;
any = true;
}
@@ -624,11 +691,20 @@ fn emit_searchable_text(
let size_mm = (run.height as f64 * map.scale / HELVETICA_CAP_RATIO).max(0.1);
let deg = -(run.rotation as f64).to_degrees();
let rotate = if deg.abs() > 1e-6 {
format!(" transform=\"rotate({} {} {})\"", fmt(deg), fmt(sx), fmt(sy))
format!(
" transform=\"rotate({} {} {})\"",
fmt(deg),
fmt(sx),
fmt(sy)
)
} else {
String::new()
};
let weight = if run.bold { " font-weight=\"bold\"" } else { "" };
let weight = if run.bold {
" font-weight=\"bold\""
} else {
""
};
out.push_str(&format!(
"<text x=\"{}\" y=\"{}\" font-size=\"{}\"{rotate}{weight} fill=\"{}\" font-family=\"{}\"{opacity}>{text}</text>\n",
fmt(sx),
@@ -678,10 +754,15 @@ fn emit_image(
return Ok(());
}
let to_svg = |high: [f32; 3], low: [f32; 3]| {
map.apply(high[0] as f64 + low[0] as f64 + ox, high[1] as f64 + low[1] as f64 + oy)
map.apply(
high[0] as f64 + low[0] as f64 + ox,
high[1] as f64 + low[1] as f64 + oy,
)
};
let corners: [[f64; 2]; 4] =
std::array::from_fn(|i| { let (x, y) = to_svg(image.corners[i], image.corners_low[i]); [x, y] });
let corners: [[f64; 2]; 4] = std::array::from_fn(|i| {
let (x, y) = to_svg(image.corners[i], image.corners_low[i]);
[x, y]
});
if !corners.iter().flatten().all(|v| v.is_finite())
|| !image.verts.iter().all(|v| {
let (x, y) = to_svg(v.pos, v.pos_low);
@@ -739,12 +820,19 @@ fn emit_image(
let mut d = String::new();
for (i, p) in ring.iter().enumerate() {
let (x, y) = map.apply(p[0] + ox, p[1] + oy);
d.push_str(&format!("{} {} {} ", if i == 0 { 'M' } else { 'L' }, fmt(x), fmt(y)));
d.push_str(&format!(
"{} {} {} ",
if i == 0 { 'M' } else { 'L' },
fmt(x),
fmt(y)
));
}
d.push_str("Z ");
rings.push_str(&format!("<path d=\"{}\"/>\n", d.trim_end()));
}
out.push_str(&format!("<clipPath id=\"img-clip-{id}\">{rings}</clipPath>\n"));
out.push_str(&format!(
"<clipPath id=\"img-clip-{id}\">{rings}</clipPath>\n"
));
clip_open = format!("<g clip-path=\"url(#img-clip-{id})\">\n");
clip_close = "</g>\n";
}
@@ -907,7 +995,12 @@ mod tests {
#[test]
fn y_flip_puts_bottom_left_geometry_at_the_svg_bottom() {
// A wire point at paper y=0 (sheet bottom) must land at y=paper_h in SVG.
let map = PageMap { paper_w: 210.0, paper_h: 297.0, scale: 1.0, rotation_deg: 0 };
let map = PageMap {
paper_w: 210.0,
paper_h: 297.0,
scale: 1.0,
rotation_deg: 0,
};
assert_eq!(map.apply(10.0, 0.0), (10.0, 297.0));
assert_eq!(map.apply(0.0, 297.0), (0.0, 0.0));
}
@@ -915,12 +1008,22 @@ mod tests {
#[test]
fn rotation_matches_the_pdf_ctm_convention() {
// PDF 90°: x' = paper_w - s*y, y' = s*x, then SVG flips y.
let map = PageMap { paper_w: 297.0, paper_h: 210.0, scale: 1.0, rotation_deg: 90 };
let map = PageMap {
paper_w: 297.0,
paper_h: 210.0,
scale: 1.0,
rotation_deg: 90,
};
let (x, y) = map.apply(0.0, 0.0);
assert_eq!((x, y), (297.0, 210.0));
let (x, y) = map.apply(5.0, 7.0);
assert_eq!((x, y), (297.0 - 7.0, 210.0 - 5.0));
let map = PageMap { paper_w: 210.0, paper_h: 297.0, scale: 1.0, rotation_deg: 180 };
let map = PageMap {
paper_w: 210.0,
paper_h: 297.0,
scale: 1.0,
rotation_deg: 180,
};
let (x, y) = map.apply(0.0, 0.0);
assert_eq!((x, y), (210.0, 0.0));
}
@@ -950,7 +1053,12 @@ mod tests {
fn nan_splits_subpaths_and_helpers_stay_paper_coloured() {
let wire = WireModel::solid(
"L1".to_string(),
vec![[0.0, 0.0, 0.0], [f32::NAN, f32::NAN, 0.0], [5.0, 5.0, 0.0], [6.0, 5.0, 0.0]],
vec![
[0.0, 0.0, 0.0],
[f32::NAN, f32::NAN, 0.0],
[5.0, 5.0, 0.0],
[6.0, 5.0, 0.0],
],
[0.0, 0.0, 1.0, 1.0],
false,
);
@@ -1002,6 +1110,9 @@ mod tests {
let svg = build_svg(&page, None).expect("builds");
assert!(svg.contains("<clipPath id=\"plot-clip\">"), "{svg}");
// y-flipped: rect y = 297 - (20 + 50) = 227.
assert!(svg.contains("x=\"10\" y=\"227\" width=\"100\" height=\"50\""), "{svg}");
assert!(
svg.contains("x=\"10\" y=\"227\" width=\"100\" height=\"50\""),
"{svg}"
);
}
}
+28 -5
View File
@@ -10,7 +10,19 @@ use codec::types::Color;
/// (name, type, face lighting model, face lighting quality, face color
/// mode, face modifier, edge model, edge style, internal only, extended
/// lighting model, properties)
type Row = (&'static str, i16, i16, i16, i16, i32, i32, i32, bool, i16, &'static str);
type Row = (
&'static str,
i16,
i16,
i16,
i16,
i32,
i32,
i32,
bool,
i16,
&'static str,
);
const STYLES: &[Row] = &[
("2dWireframe", 4, 0, 2, 0, 0, 1, 4, false, 3,
@@ -103,7 +115,9 @@ pub fn populate_document(document: &mut codec::CadDocument) {
if present {
continue;
}
let Some(properties) = props.split(' ').map(property).collect::<Option<Vec<_>>>() else { continue };
let Some(properties) = props.split(' ').map(property).collect::<Option<Vec<_>>>() else {
continue;
};
let handle = document.allocate_handle();
let mut style = VisualStyle::new();
style.handle = handle;
@@ -120,7 +134,9 @@ pub fn populate_document(document: &mut codec::CadDocument) {
style.internal_use_only = internal;
style.extended_lighting_model = elm;
style.properties = properties;
document.objects.insert(handle, ObjectType::VisualStyle(style));
document
.objects
.insert(handle, ObjectType::VisualStyle(style));
if let Some(ObjectType::Dictionary(entries)) = document.objects.get_mut(&dictionary) {
entries.add_entry(name, handle);
}
@@ -130,7 +146,10 @@ pub fn populate_document(document: &mut codec::CadDocument) {
/// The visual styles offered by name (not the internal ones), as the
/// reference lists them: the 2D wireframe style shows as "2D Wireframe".
pub fn offered(document: &codec::CadDocument) -> Vec<(String, codec::types::Handle)> {
let Some(ObjectType::Dictionary(entries)) = document.objects.get(&document.header.acad_visualstyle_dict_handle) else {
let Some(ObjectType::Dictionary(entries)) = document
.objects
.get(&document.header.acad_visualstyle_dict_handle)
else {
return Vec::new();
};
entries
@@ -141,7 +160,11 @@ pub fn offered(document: &codec::CadDocument) -> Vec<(String, codec::types::Hand
_ => false,
})
.map(|(name, handle)| {
let shown = if name.eq_ignore_ascii_case("2dWireframe") { "2D Wireframe".to_string() } else { name.clone() };
let shown = if name.eq_ignore_ascii_case("2dWireframe") {
"2D Wireframe".to_string()
} else {
name.clone()
};
(shown, *handle)
})
.collect()
+9 -11
View File
@@ -159,8 +159,6 @@ pub async fn snapshot(name: &str, bytes: &[u8], thumbnail: Option<&[u8]>) -> Res
Ok(())
}
#[cfg(target_arch = "wasm32")]
pub async fn read(name: &str) -> Result<Vec<u8>, String> {
let directory = autosave_directory(false).await?;
@@ -250,9 +248,7 @@ fn thumbnail_key(name: &str) -> String {
}
#[cfg(target_arch = "wasm32")]
async fn read_index(
directory: &web_sys::FileSystemDirectoryHandle,
) -> Result<Vec<String>, String> {
async fn read_index(directory: &web_sys::FileSystemDirectoryHandle) -> Result<Vec<String>, String> {
let bytes = match super::web_recent::read_entry(directory, INDEX_KEY).await {
Ok(bytes) => bytes,
// A missing index is a fresh store.
@@ -317,7 +313,10 @@ mod tests {
// empty-stem fallback reinstated the unsanitized input, breaking
// idempotence across crash cycles.
let once = recovered_name("a.b/c");
assert_eq!(once, "c (recovered).dxf", "final path component, no separator survives");
assert_eq!(
once, "c (recovered).dxf",
"final path component, no separator survives"
);
assert_eq!(recovered_name(&once), once, "idempotent");
let dotfile = recovered_name("/.dwg");
assert_eq!(dotfile, ".dwg (recovered).dwg");
@@ -355,10 +354,7 @@ mod tests {
"plan (recovered).dwg"
);
// Extensionless stacked names de-stack too, then gain ".dxf".
assert_eq!(
recovered_name("plan (recovered)"),
"plan (recovered).dxf"
);
assert_eq!(recovered_name("plan (recovered)"), "plan (recovered).dxf");
// A marker embedded in the middle (not a suffix) is left alone.
assert_eq!(
recovered_name("(recovered) plan.dwg"),
@@ -407,7 +403,9 @@ mod tests {
];
let plan = restore_all_plan(&entries);
assert_eq!(
plan.iter().map(|(name, _)| name.as_str()).collect::<Vec<_>>(),
plan.iter()
.map(|(name, _)| name.as_str())
.collect::<Vec<_>>(),
[
"new (recovered).dwg",
"mid (recovered).dwg",
+11 -9
View File
@@ -174,9 +174,8 @@ fn percent_decode(value: &str) -> String {
let mut out = Vec::with_capacity(bytes.len());
let mut index = 0;
while index < bytes.len() {
let escape = (bytes[index] == b'%' && index + 2 < bytes.len()).then(|| {
(hex(bytes[index + 1]), hex(bytes[index + 2]))
});
let escape = (bytes[index] == b'%' && index + 2 < bytes.len())
.then(|| (hex(bytes[index + 1]), hex(bytes[index + 2])));
match escape {
Some((Some(high), Some(low))) => {
out.push(high * 16 + low);
@@ -248,7 +247,10 @@ fn enqueue_or_full(name: String, bytes: Vec<u8>) -> LinkOutcome {
/// built inline in the poll arm and mention images); the classification
/// itself is shared via `RemoteFetchError`.
#[cfg(target_arch = "wasm32")]
fn fetch_error_text(url: &str, error: crate::scene::model::image_model::RemoteFetchError) -> String {
fn fetch_error_text(
url: &str,
error: crate::scene::model::image_model::RemoteFetchError,
) -> String {
use crate::scene::model::image_model::RemoteFetchError;
match error {
RemoteFetchError::Network => format!(
@@ -402,7 +404,10 @@ mod tests {
"?file=https%3A%2F%2Fexample.com%2F..dwgx",
] {
assert!(
matches!(parse_link(search), ParsedLink::Ignored(LinkRefusal::NotDrawing, _)),
matches!(
parse_link(search),
ParsedLink::Ignored(LinkRefusal::NotDrawing, _)
),
"{search} must be refused as not a drawing"
);
}
@@ -461,10 +466,7 @@ mod tests {
fn scheme_prefix_matches_rfc3986_shape_only() {
assert_eq!(scheme_prefix("https://x"), Some("https".to_string()));
assert_eq!(scheme_prefix("c:\\x"), Some("c".to_string()));
assert_eq!(
scheme_prefix("x1+2-3.4:rest"),
Some("x1+2-3.4".to_string())
);
assert_eq!(scheme_prefix("x1+2-3.4:rest"), Some("x1+2-3.4".to_string()));
// A colon after a non-scheme character (space, slash, digit start)
// is not a scheme.
assert_eq!(scheme_prefix("plan.dwg"), None);
+9 -17
View File
@@ -53,7 +53,8 @@ pub fn persist(tabs: &[String], active: usize) {
return;
}
let session = WebSession::new(tabs.to_vec(), active);
if let Some(storage) = web_sys::window().and_then(|window| window.local_storage().ok().flatten())
if let Some(storage) =
web_sys::window().and_then(|window| window.local_storage().ok().flatten())
{
let _ = storage.set_item(SESSION_KEY, &session.to_json());
}
@@ -76,10 +77,7 @@ mod tests {
#[test]
fn a_session_round_trips_through_json() {
let session = WebSession::new(
vec!["plan.dwg".to_string(), "sketch.dxf".to_string()],
1,
);
let session = WebSession::new(vec!["plan.dwg".to_string(), "sketch.dxf".to_string()], 1);
let restored = WebSession::from_json(&session.to_json()).expect("must parse");
assert_eq!(restored, session);
assert_eq!(restored.tabs, ["plan.dwg", "sketch.dxf"]);
@@ -91,8 +89,8 @@ mod tests {
// Construction clamps…
assert_eq!(WebSession::new(vec!["a.dwg".to_string()], 9).active, 0);
// …and so does a parse of stale or hostile JSON, both directions.
let over = WebSession::from_json(r#"{"tabs":["a.dwg","b.dwg"],"active":99}"#)
.expect("must parse");
let over =
WebSession::from_json(r#"{"tabs":["a.dwg","b.dwg"],"active":99}"#).expect("must parse");
assert_eq!(over.active, 1);
let under = WebSession::from_json(r#"{"tabs":["a.dwg"],"active":0}"#).unwrap();
assert_eq!(under.active, 0);
@@ -103,8 +101,8 @@ mod tests {
assert!(WebSession::from_json("").is_none());
assert!(WebSession::from_json("not json").is_none());
assert!(WebSession::from_json("{}").is_none()); // missing `tabs` field
// A session whose every tab closed is still valid memory (it says
// the crash lost nothing that was visibly open).
// A session whose every tab closed is still valid memory (it says
// the crash lost nothing that was visibly open).
let empty = WebSession::from_json(r#"{"tabs":[],"active":0}"#).unwrap();
assert!(empty.tabs.is_empty());
assert_eq!(empty.active, 0);
@@ -114,13 +112,7 @@ mod tests {
fn duplicate_names_are_kept_as_is() {
// Two tabs may share a display name on the web; the memory list
// records the list as it was, de-duplication is not its job.
let session = WebSession::new(
vec!["a.dwg".to_string(), "a.dwg".to_string()],
1,
);
assert_eq!(
WebSession::from_json(&session.to_json()).unwrap(),
session
);
let session = WebSession::new(vec!["a.dwg".to_string(), "a.dwg".to_string()], 1);
assert_eq!(WebSession::from_json(&session.to_json()).unwrap(), session);
}
}
+2 -4
View File
@@ -212,11 +212,9 @@ pub(super) async fn serialize_document(encoded_request: Vec<u8>) -> Result<Vec<u
async fn serialize_document_via_worker(encoded_request: &[u8]) -> Result<Vec<u8>, String> {
let options = WorkerOptions::new();
options.set_type(WorkerType::Module);
let worker =
Worker::new_with_options(WORKER_URL, &options).map_err(|error| js_error(error))?;
let worker = Worker::new_with_options(WORKER_URL, &options).map_err(|error| js_error(error))?;
let (sender, receiver) =
iced::futures::channel::oneshot::channel::<Result<Vec<u8>, String>>();
let (sender, receiver) = iced::futures::channel::oneshot::channel::<Result<Vec<u8>, String>>();
let sender = Rc::new(RefCell::new(Some(sender)));
let message_sender = sender.clone();
let on_message = Closure::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| {