forked from 1b-agent/opencadstudio-upstream
style: rustfmt the tree so the CI lint check passes
This commit is contained in:
1 parent
1317a7b4fd
commit
6025d8be44
128 files changed
+4097
-1676
No files matched your search
+40
-8
@@ -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!(
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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))]
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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| {
|
||||
|
||||
Reference in new issue
Block a user