diff --git a/benches/command_benchmarks.rs b/benches/command_benchmarks.rs index 3cd183a34..c65439c4a 100644 --- a/benches/command_benchmarks.rs +++ b/benches/command_benchmarks.rs @@ -16,9 +16,7 @@ use codec::types::{Vector2, Vector3}; use codec::{CadDocument, EntityType, Handle}; use harness::BenchmarkRunner; -use OpenCADStudio::modules::draw::modify::explode::{ - apply_explode_replacements, explode_batch, -}; +use OpenCADStudio::modules::draw::modify::explode::{apply_explode_replacements, explode_batch}; use OpenCADStudio::modules::draw::modify::flatten; use OpenCADStudio::scene::Scene; @@ -79,10 +77,8 @@ fn push_fixture_entities(doc: &mut CadDocument, quick: bool) -> Vec { let mut pl = LwPolyline::new(); pl.vertices = (0..VERTS_PER_POLYLINE) .map(|k| { - let mut v = LwVertex::new(Vector2::new( - base + k as f64 * 2.0, - (k % 5) as f64 * 0.75, - )); + let mut v = + LwVertex::new(Vector2::new(base + k as f64 * 2.0, (k % 5) as f64 * 0.75)); if k % 2 == 1 { v.bulge = 0.5; } @@ -101,13 +97,13 @@ fn push_fixture_entities(doc: &mut CadDocument, quick: bool) -> Vec { for i in 0..n_dim { let x = (i % 100) as f64 * 12.0; let y = (i / 100) as f64 * 12.0; - let mut d = DimensionLinear::new( - Vector3::new(x, y, 0.0), - Vector3::new(x + 10.0, y, 0.0), - ); + let mut d = DimensionLinear::new(Vector3::new(x, y, 0.0), Vector3::new(x + 10.0, y, 0.0)); d.definition_point = Vector3::new(x + 5.0, y + 3.0, 0.0); d.base.text_middle_point = Vector3::new(x + 5.0, y + 4.0, 0.0); - handles.push(doc.add_entity(EntityType::Dimension(Dimension::Linear(d))).unwrap()); + handles.push( + doc.add_entity(EntityType::Dimension(Dimension::Linear(d))) + .unwrap(), + ); } for i in 0..n_ml { @@ -121,7 +117,10 @@ fn push_fixture_entities(doc: &mut CadDocument, quick: bool) -> Vec { Vector3::new(x + 8.0, y + 8.0, 0.0), Vector3::new(x + 12.0, y + 8.0, 0.0), ]; - handles.push(doc.add_entity(EntityType::MLine(MLine::from_points(&pts))).unwrap()); + handles.push( + doc.add_entity(EntityType::MLine(MLine::from_points(&pts))) + .unwrap(), + ); } handles @@ -274,7 +273,8 @@ fn push_flatten_fixture(doc: &mut CadDocument, quick: bool) -> Vec { handles.push(doc.add_entity(EntityType::Line(line)).unwrap()); } for i in 0..n_circle { - let mut c = Circle::from_center_radius(Vector3::new(i as f64, 0.0, 1.0 + (i % 3) as f64), 2.0); + let mut c = + Circle::from_center_radius(Vector3::new(i as f64, 0.0, 1.0 + (i % 3) as f64), 2.0); if i % 2 == 1 { c.normal = Vector3::new(0.0, 0.6, 0.8); // tilted → ellipse } @@ -296,7 +296,8 @@ fn push_flatten_fixture(doc: &mut CadDocument, quick: bool) -> Vec { let mut pl = Polyline2D::new(); pl.elevation = 3.0 + (i % 5) as f64; for k in 0..6 { - pl.vertices.push(Vertex2D::new(Vector3::new(k as f64, i as f64, 0.0))); + pl.vertices + .push(Vertex2D::new(Vector3::new(k as f64, i as f64, 0.0))); } handles.push(doc.add_entity(EntityType::Polyline2D(pl)).unwrap()); } @@ -321,7 +322,10 @@ fn push_flatten_fixture(doc: &mut CadDocument, quick: bool) -> Vec { handles.push(doc.add_entity(EntityType::Solid(s)).unwrap()); } for i in 0..n_ray { - let r = Ray::new(Vector3::new(i as f64, 0.0, 5.0), Vector3::new(0.0, 0.0, 1.0)); + let r = Ray::new( + Vector3::new(i as f64, 0.0, 5.0), + Vector3::new(0.0, 0.0, 1.0), + ); handles.push(doc.add_entity(EntityType::Ray(r)).unwrap()); } for i in 0..n_flat { diff --git a/benches/performance_benchmarks.rs b/benches/performance_benchmarks.rs index af76ab508..4fc63a86e 100644 --- a/benches/performance_benchmarks.rs +++ b/benches/performance_benchmarks.rs @@ -59,8 +59,8 @@ use OpenCADStudio::scene::{ChangeKind, Scene}; use OpenCADStudio::snap::Snapper; use OpenCADStudio::ui::icons::{self, CHECK}; use OpenCADStudio::ui::overlay::{ - grid_segments, selection_overlay, should_reuse, CrosshairOptions, GridCanvasState, - GridKey, GridParams, GridStyle, GripMarker, NavCursor, OstTrackPoint, SelectionVisualOptions, + grid_segments, selection_overlay, should_reuse, CrosshairOptions, GridCanvasState, GridKey, + GridParams, GridStyle, GripMarker, NavCursor, OstTrackPoint, SelectionVisualOptions, UcsIconParams, }; use OpenCADStudio::ui::properties::LinetypeItem; @@ -914,10 +914,26 @@ fn bench_ui_grid_geometry(runner: &mut BenchmarkRunner) { if runner.should_run("ui_grid_geometry_uncached") { for _ in 0..10 { let _ = black_box(grid_segments( - view_rot1, eye1, bounds1, step1, step1, major_every, origin1, axes1, limits1, + view_rot1, + eye1, + bounds1, + step1, + step1, + major_every, + origin1, + axes1, + limits1, )); let _ = black_box(grid_segments( - view_rot2, eye2, bounds2, step2, step2, major_every, origin2, axes2, limits2, + view_rot2, + eye2, + bounds2, + step2, + step2, + major_every, + origin2, + axes2, + limits2, )); } @@ -928,8 +944,28 @@ fn bench_ui_grid_geometry(runner: &mut BenchmarkRunner) { for _ in 0..runs { let t0 = Instant::now(); for _ in 0..n { - let g1 = grid_segments(view_rot1, eye1, bounds1, step1, step1, major_every, origin1, axes1, limits1); - let g2 = grid_segments(view_rot2, eye2, bounds2, step2, step2, major_every, origin2, axes2, limits2); + let g1 = grid_segments( + view_rot1, + eye1, + bounds1, + step1, + step1, + major_every, + origin1, + axes1, + limits1, + ); + let g2 = grid_segments( + view_rot2, + eye2, + bounds2, + step2, + step2, + major_every, + origin2, + axes2, + limits2, + ); black_box(g1); black_box(g2); } @@ -1302,7 +1338,10 @@ fn bench_ui_grip_budget(runner: &mut BenchmarkRunner) { ), "µs", samples, - Some(((capped_grips.len() as f64) / (median_us / 1_000_000.0), "grips/s")), + Some(( + (capped_grips.len() as f64) / (median_us / 1_000_000.0), + "grips/s", + )), Some(500.0), // Target threshold < 500 µs (measured ~132 µs quick / ~147 µs full) ); @@ -1337,7 +1376,10 @@ fn bench_ui_grip_budget(runner: &mut BenchmarkRunner) { ), "µs", build_samples, - Some(((n_fixture_grips as f64) / (build_median_us / 1_000_000.0), "grips/s")), + Some(( + (n_fixture_grips as f64) / (build_median_us / 1_000_000.0), + "grips/s", + )), Some(7_000.0), // Target < 7 ms (selection-change hitch budget; measured ~2.36 ms full / ~0.30 ms quick, ~3x headroom) ); } @@ -1386,18 +1428,13 @@ fn bench_ui_constraint_glyphs(runner: &mut BenchmarkRunner) { let vp = (1920.0_f32, 1080.0_f32); // Prime the cache so every timed call below is a hit (same key, same Arc). - let primed = - scene.cached_glyph_placements(ParametricScope::ModelSpace, vp, true, 3, 4095); - assert!( - !primed.is_empty(), - "glyph fixture must yield placements" - ); + let primed = scene.cached_glyph_placements(ParametricScope::ModelSpace, vp, true, 3, 4095); + assert!(!primed.is_empty(), "glyph fixture must yield placements"); let n_glyphs = primed.len(); // Warm-up for allocator settling. for _ in 0..10 { - let hit = - scene.cached_glyph_placements(ParametricScope::ModelSpace, vp, true, 3, 4095); + let hit = scene.cached_glyph_placements(ParametricScope::ModelSpace, vp, true, 3, 4095); black_box(hit); } @@ -1499,7 +1536,12 @@ fn build_selection_overlay_element( .collect(); let ucs_icons = vec![UcsIconParams { view_proj: Mat4::IDENTITY, - bounds: Rectangle { x: 0.0, y: 0.0, width: 1920.0, height: 1080.0 }, + bounds: Rectangle { + x: 0.0, + y: 0.0, + width: 1920.0, + height: 1080.0, + }, axes: (Vec3::X, Vec3::Y, Vec3::Z), origin_screen: None, hover: false, @@ -1516,7 +1558,9 @@ fn build_selection_overlay_element( None, None, ucs_icons, - vec![OstTrackPoint { screen: Point::new(500.0, 500.0) }], + vec![OstTrackPoint { + screen: Point::new(500.0, 500.0), + }], vec![(Point::new(0.0, 0.0), Point::new(100.0, 100.0))], None, true, @@ -2237,4 +2281,3 @@ fn main() { runner.finish(); } - diff --git a/build.rs b/build.rs index 6c69b391a..50cd80b6e 100644 --- a/build.rs +++ b/build.rs @@ -26,8 +26,8 @@ fn main() { println!("cargo:rerun-if-changed=.git/{reference}"); } } - let revision = git_output(&["rev-parse", "--short=12", "HEAD"]) - .unwrap_or_else(|| "unknown".to_string()); + let revision = + git_output(&["rev-parse", "--short=12", "HEAD"]).unwrap_or_else(|| "unknown".to_string()); // diff-index exits 1 for differences and 128 when there is no repository, // so only a real difference marks the build dirty. let dirty = std::process::Command::new("git") @@ -59,8 +59,8 @@ fn main() { "cargo:rustc-env=OCS_TAG_DISTANCE={}", tag_distance.map_or_else(|| "unknown".to_string(), |n| n.to_string()) ); - let commit_date = git_output(&["log", "-1", "--format=%cs", "HEAD"]) - .unwrap_or_else(|| "unknown".to_string()); + let commit_date = + git_output(&["log", "-1", "--format=%cs", "HEAD"]).unwrap_or_else(|| "unknown".to_string()); println!("cargo:rustc-env=OCS_COMMIT_DATE={commit_date}"); // Semver-style build metadata: empty for a clean build on the release diff --git a/crates/dwg-thumbnailer-win/src/lib.rs b/crates/dwg-thumbnailer-win/src/lib.rs index 3d294726d..6c481d0c9 100644 --- a/crates/dwg-thumbnailer-win/src/lib.rs +++ b/crates/dwg-thumbnailer-win/src/lib.rs @@ -40,7 +40,7 @@ use windows::Win32::System::SystemServices::{DLL_PROCESS_ATTACH, DLL_PROCESS_DET use windows::Win32::UI::Shell::PropertiesSystem::{IInitializeWithFile, IInitializeWithFile_Impl}; use windows::Win32::UI::Shell::{ IThumbnailProvider, IThumbnailProvider_Impl, SHChangeNotify, SHCNE_ASSOCCHANGED, SHCNF_IDLIST, - WTS_ALPHATYPE, WTSAT_ARGB, + WTSAT_ARGB, WTS_ALPHATYPE, }; /// CLSID of this thumbnail provider (stable; used in the registry keys). @@ -72,7 +72,8 @@ struct DwgThumbProvider { impl IInitializeWithFile_Impl for DwgThumbProvider_Impl { fn Initialize(&self, pszfilepath: &PCWSTR, _grfmode: u32) -> windows::core::Result<()> { - let path = unsafe { pszfilepath.to_string() }.map_err(|_| windows::core::Error::from(E_INVALIDARG))?; + let path = unsafe { pszfilepath.to_string() } + .map_err(|_| windows::core::Error::from(E_INVALIDARG))?; *self.path.borrow_mut() = Some(path); Ok(()) } @@ -85,7 +86,11 @@ impl IThumbnailProvider_Impl for DwgThumbProvider_Impl { phbmp: *mut HBITMAP, pdwalpha: *mut WTS_ALPHATYPE, ) -> windows::core::Result<()> { - let path = self.path.borrow().clone().ok_or(windows::core::Error::from(E_FAIL))?; + let path = self + .path + .borrow() + .clone() + .ok_or(windows::core::Error::from(E_FAIL))?; let mut img = dwg_thumbnailer::extract(std::path::Path::new(&path), cx.max(1)) .ok_or(windows::core::Error::from(E_FAIL))?; dwg_thumbnailer::badge_dwg(&mut img); // full-width "DWG" band @@ -248,13 +253,14 @@ fn set_value(sub: &str, name: Option<&str>, value: &str) -> windows::core::Resul return Err(E_FAIL.into()); } let val_w = wide(value); - let bytes = - unsafe { std::slice::from_raw_parts(val_w.as_ptr() as *const u8, val_w.len() * 2) }; + let bytes = unsafe { std::slice::from_raw_parts(val_w.as_ptr() as *const u8, val_w.len() * 2) }; let name_w = name.map(wide); let rc = unsafe { RegSetValueExW( hkey, - name_w.as_ref().map_or(PCWSTR::null(), |n| PCWSTR(n.as_ptr())), + name_w + .as_ref() + .map_or(PCWSTR::null(), |n| PCWSTR(n.as_ptr())), 0, REG_SZ, Some(bytes), diff --git a/crates/dwg-thumbnailer/src/lib.rs b/crates/dwg-thumbnailer/src/lib.rs index 28deb0399..a94186049 100644 --- a/crates/dwg-thumbnailer/src/lib.rs +++ b/crates/dwg-thumbnailer/src/lib.rs @@ -74,14 +74,14 @@ pub fn badge_dwg(img: &mut RgbaImage) { let label = label.to_rgba8(); if label.width() > 0 && label.height() > 0 { let mut target_h = ((band_h as f32 * 0.72) as u32).max(1); - let mut target_w = ((label.width() as f32 * target_h as f32 / label.height() as f32) - as u32) - .max(1); + let mut target_w = + ((label.width() as f32 * target_h as f32 / label.height() as f32) as u32).max(1); // Don't let a wide wordmark spill past the thumbnail edges. let max_w = ((w as f32 * 0.90) as u32).max(1); if target_w > max_w { target_w = max_w; - target_h = ((label.height() as f32 * target_w as f32 / label.width() as f32) as u32) + target_h = ((label.height() as f32 * target_w as f32 / label.width() as f32) + as u32) .max(1); } let label = image::imageops::thumbnail(&label, target_w, target_h); @@ -197,7 +197,9 @@ fn parse_preview_container(buf: &[u8], base: u64) -> Option<(Fmt, &[u8])> { fn decode(format: Fmt, data: &[u8]) -> Option { let img = match format { Fmt::Png => image::load_from_memory_with_format(data, ImageFormat::Png).ok()?, - Fmt::Bmp => image::load_from_memory_with_format(&dib_to_bmp(data), ImageFormat::Bmp).ok()?, + Fmt::Bmp => { + image::load_from_memory_with_format(&dib_to_bmp(data), ImageFormat::Bmp).ok()? + } }; Some(img.to_rgba8()) } @@ -235,7 +237,9 @@ pub unsafe extern "C" fn dwg_thumbnail_png( return false; } let cstr = std::ffi::CStr::from_ptr(path_utf8); - let Ok(path) = cstr.to_str() else { return false }; + let Ok(path) = cstr.to_str() else { + return false; + }; let Some(mut img) = extract(Path::new(path), max_dim) else { return false; }; @@ -271,7 +275,11 @@ fn dib_to_bmp(dib: &[u8]) -> Vec { } 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()); diff --git a/crates/ocs_plugin_api/build.rs b/crates/ocs_plugin_api/build.rs index dda5ba997..0c1e31246 100644 --- a/crates/ocs_plugin_api/build.rs +++ b/crates/ocs_plugin_api/build.rs @@ -68,10 +68,7 @@ fn generate_type_registry(out_dir: &Path) { let types: Vec<(&str, TraceFn)> = vec![ ("EntityType", trace::), ("ObjectType", trace::), - ( - "HeaderVariables", - trace::, - ), + ("HeaderVariables", trace::), ("SummaryInfo", trace::), ("LineType", trace::), ("TextStyle", trace::), @@ -88,10 +85,7 @@ fn generate_type_registry(out_dir: &Path) { trace::, ), ("FieldDef", trace::), - ( - "DgnLsDefinition", - trace::, - ), + ("DgnLsDefinition", trace::), ("DgnLsComponent", trace::), ( "NotificationCollection", @@ -182,10 +176,7 @@ fn generate_type_registry(out_dir: &Path) { "AssocViewObjectActionParamKind", trace_simple::, ), - ( - "AcisVersion", - trace_simple::, - ), + ("AcisVersion", trace_simple::), ( "AssociativeData", trace_simple::, @@ -365,10 +356,7 @@ fn generate_type_registry(out_dir: &Path) { "PlotPaperUnits", trace_simple::, ), - ( - "PlotRotation", - trace_simple::, - ), + ("PlotRotation", trace_simple::), ("PlotType", trace_simple::), ( "PreviewFormat", @@ -403,14 +391,8 @@ fn generate_type_registry(out_dir: &Path) { "SolidHistoryOperation", trace_simple::, ), - ( - "SurfaceData", - trace_simple::, - ), - ( - "SurfaceKind", - trace_simple::, - ), + ("SurfaceData", trace_simple::), + ("SurfaceKind", trace_simple::), ( "SurfaceSmoothType", trace_simple::, @@ -525,7 +507,7 @@ fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) { use std::collections::HashSet; let policy: EntityCoveragePolicy = serde_json::from_str(include_str!("entity_coverage_policy.json")) - .expect("valid entity coverage policy"); + .expect("valid entity coverage policy"); let variants = ®istry.types[&TypeId::new("EntityType")].variants; let variant_names: HashSet<&str> = variants.iter().map(|v| v.name.as_str()).collect(); for name in policy @@ -814,7 +796,7 @@ fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) { | "vertices" ) ) { - "transaction_geometry" + "transaction_geometry" } else { "type_conversion_only" }; @@ -842,8 +824,17 @@ fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) { optional: false, is_sequence: false, snapshot_readable: true, - validation: if writable { "transaction_geometry" } else { "none" }.into(), - model_access: if writable { ModelAccess::ReadWrite } else { ModelAccess::ReadOnly }, + validation: if writable { + "transaction_geometry" + } else { + "none" + } + .into(), + model_access: if writable { + ModelAccess::ReadWrite + } else { + ModelAccess::ReadOnly + }, }); } assert!( @@ -864,7 +855,7 @@ fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) { out_dir.join("entity_coverage.json"), serde_json::to_string_pretty(&catalog).unwrap(), ) - .expect("write entity coverage"); + .expect("write entity coverage"); } fn trace(tracer: &mut Tracer, samples: &Samples) @@ -910,10 +901,7 @@ fn add_enum_samples(tracer: &mut Tracer, samples: &mut Samples) { samples, &codec::entities::SmoothSurfaceType::QuadraticBSpline, ); - let _ = tracer.trace_value( - samples, - &codec::entities::SmoothSurfaceType::CubicBSpline, - ); + let _ = tracer.trace_value(samples, &codec::entities::SmoothSurfaceType::CubicBSpline); let _ = tracer.trace_value(samples, &codec::entities::SmoothSurfaceType::Bezier); let _ = tracer.trace_value(samples, &codec::entities::AttachmentPoint::TopLeft); @@ -938,14 +926,8 @@ fn add_enum_samples(tracer: &mut Tracer, samples: &mut Samples) { samples, &codec::xdata::XDataValue::ControlString(String::new()), ); - let _ = tracer.trace_value( - samples, - &codec::xdata::XDataValue::LayerName(String::new()), - ); - let _ = tracer.trace_value( - samples, - &codec::xdata::XDataValue::BinaryData(Vec::new()), - ); + let _ = tracer.trace_value(samples, &codec::xdata::XDataValue::LayerName(String::new())); + let _ = tracer.trace_value(samples, &codec::xdata::XDataValue::BinaryData(Vec::new())); let _ = tracer.trace_value( samples, &codec::xdata::XDataValue::Handle(codec::Handle::default()), @@ -995,14 +977,8 @@ fn add_enum_samples(tracer: &mut Tracer, samples: &mut Samples) { // Proxy reference kinds pulled in by XRecord.object_references let _ = tracer.trace_value(samples, &codec::objects::ProxyReferenceKind::Undefined); - let _ = tracer.trace_value( - samples, - &codec::objects::ProxyReferenceKind::SoftOwnership, - ); - let _ = tracer.trace_value( - samples, - &codec::objects::ProxyReferenceKind::HardOwnership, - ); + let _ = tracer.trace_value(samples, &codec::objects::ProxyReferenceKind::SoftOwnership); + let _ = tracer.trace_value(samples, &codec::objects::ProxyReferenceKind::HardOwnership); let _ = tracer.trace_value(samples, &codec::objects::ProxyReferenceKind::SoftPointer); let _ = tracer.trace_value(samples, &codec::objects::ProxyReferenceKind::HardPointer); diff --git a/crates/ocs_plugin_api/src/entity_coverage.rs b/crates/ocs_plugin_api/src/entity_coverage.rs index b04b14ace..aa2901297 100644 --- a/crates/ocs_plugin_api/src/entity_coverage.rs +++ b/crates/ocs_plugin_api/src/entity_coverage.rs @@ -62,7 +62,9 @@ fn insert_scale(name: &str, value: f64) -> Result<(), String> { } #[cfg(feature = "host")] -fn validate_hatch_paths(paths: &[crate::host::codec::entities::BoundaryPath]) -> Result<(), String> { +fn validate_hatch_paths( + paths: &[crate::host::codec::entities::BoundaryPath], +) -> Result<(), String> { use crate::host::codec::entities::BoundaryEdge; use crate::host::codec::types::Vector2; let close = |a: Vector2, b: Vector2| (a.x - b.x).hypot(a.y - b.y) <= 1e-8; @@ -89,11 +91,19 @@ fn validate_hatch_paths(paths: &[crate::host::codec::entities::BoundaryPath]) -> for (ei, edge) in path.edges.iter().enumerate() { let pair = match edge { BoundaryEdge::Line(line) => { - if !line.start.x.is_finite() || !line.start.y.is_finite() || !line.end.x.is_finite() || !line.end.y.is_finite() { - return Err(format!("Hatch boundary path {pi} edge {ei} line has non-finite coordinates")); + if !line.start.x.is_finite() + || !line.start.y.is_finite() + || !line.end.x.is_finite() + || !line.end.y.is_finite() + { + return Err(format!( + "Hatch boundary path {pi} edge {ei} line has non-finite coordinates" + )); } if (line.start.x - line.end.x).hypot(line.start.y - line.end.y) < 1e-12 { - return Err(format!("Hatch boundary path {pi} edge {ei} line has zero length")); + return Err(format!( + "Hatch boundary path {pi} edge {ei} line has zero length" + )); } (line.start, line.end) } @@ -105,34 +115,65 @@ fn validate_hatch_paths(paths: &[crate::host::codec::entities::BoundaryPath]) -> return Err(format!("Hatch boundary path {pi} edge {ei} arc radius must be finite and positive")); } if !arc.start_angle.is_finite() || !arc.end_angle.is_finite() { - return Err(format!("Hatch boundary path {pi} edge {ei} arc angles must be finite")); + return Err(format!( + "Hatch boundary path {pi} edge {ei} arc angles must be finite" + )); } - let point = |angle: f64| Vector2::new( - arc.center.x + arc.radius * angle.cos(), - arc.center.y + arc.radius * angle.sin(), - ); + let point = |angle: f64| { + Vector2::new( + arc.center.x + arc.radius * angle.cos(), + arc.center.y + arc.radius * angle.sin(), + ) + }; let (start, end) = (point(arc.start_angle), point(arc.end_angle)); - if arc.counter_clockwise { (start, end) } else { (end, start) } + if arc.counter_clockwise { + (start, end) + } else { + (end, start) + } } BoundaryEdge::EllipticArc(arc) => { if !arc.center.x.is_finite() || !arc.center.y.is_finite() { return Err(format!("Hatch boundary path {pi} edge {ei} ellipse center has non-finite coordinates")); } - if !arc.major_axis_endpoint.x.is_finite() || !arc.major_axis_endpoint.y.is_finite() { + if !arc.major_axis_endpoint.x.is_finite() + || !arc.major_axis_endpoint.y.is_finite() + { return Err(format!("Hatch boundary path {pi} edge {ei} ellipse major axis has non-finite coordinates")); } if arc.major_axis_endpoint.x.hypot(arc.major_axis_endpoint.y) < 1e-12 { - return Err(format!("Hatch boundary path {pi} edge {ei} ellipse major axis has zero length")); + return Err(format!( + "Hatch boundary path {pi} edge {ei} ellipse major axis has zero length" + )); } - if !arc.minor_axis_ratio.is_finite() || !(0.0..=1.0).contains(&arc.minor_axis_ratio) || arc.minor_axis_ratio == 0.0 { + if !arc.minor_axis_ratio.is_finite() + || !(0.0..=1.0).contains(&arc.minor_axis_ratio) + || arc.minor_axis_ratio == 0.0 + { return Err(format!("Hatch boundary path {pi} edge {ei} ellipse ratio must be within (0, 1]")); } if !arc.start_angle.is_finite() || !arc.end_angle.is_finite() { - return Err(format!("Hatch boundary path {pi} edge {ei} ellipse angles must be finite")); + return Err(format!( + "Hatch boundary path {pi} edge {ei} ellipse angles must be finite" + )); + } + let start = ellipse_point( + arc.center, + arc.major_axis_endpoint, + arc.minor_axis_ratio, + arc.start_angle, + ); + let end = ellipse_point( + arc.center, + arc.major_axis_endpoint, + arc.minor_axis_ratio, + arc.end_angle, + ); + if arc.counter_clockwise { + (start, end) + } else { + (end, start) } - let start = ellipse_point(arc.center, arc.major_axis_endpoint, arc.minor_axis_ratio, arc.start_angle); - let end = ellipse_point(arc.center, arc.major_axis_endpoint, arc.minor_axis_ratio, arc.end_angle); - if arc.counter_clockwise { (start, end) } else { (end, start) } } BoundaryEdge::Polyline(poly) => { if poly.vertices.len() < 3 { @@ -144,14 +185,21 @@ fn validate_hatch_paths(paths: &[crate::host::codec::entities::BoundaryPath]) -> } } if !poly.is_closed { - return Err(format!("Hatch boundary path {pi} edge {ei} polyline is open")); + return Err(format!( + "Hatch boundary path {pi} edge {ei} polyline is open" + )); } let first = poly.vertices.first().unwrap(); - (Vector2::new(first.x, first.y), Vector2::new(first.x, first.y)) + ( + Vector2::new(first.x, first.y), + Vector2::new(first.x, first.y), + ) } BoundaryEdge::Spline(spline) => { if spline.degree < 1 { - return Err(format!("Hatch boundary path {pi} edge {ei} spline degree must be at least 1")); + return Err(format!( + "Hatch boundary path {pi} edge {ei} spline degree must be at least 1" + )); } if spline.control_points.len() < spline.degree as usize + 1 { return Err(format!("Hatch boundary path {pi} edge {ei} spline has too few control points for its degree")); @@ -171,7 +219,9 @@ fn validate_hatch_paths(paths: &[crate::host::codec::entities::BoundaryPath]) -> return Err(format!("Hatch boundary path {pi} edge {ei} fit point {fi} has non-finite coordinates")); } } - for (name, tangent) in [("start", spline.start_tangent), ("end", spline.end_tangent)] { + for (name, tangent) in + [("start", spline.start_tangent), ("end", spline.end_tangent)] + { if !tangent.x.is_finite() || !tangent.y.is_finite() { return Err(format!("Hatch boundary path {pi} edge {ei} spline {name} tangent is non-finite")); } @@ -179,10 +229,16 @@ fn validate_hatch_paths(paths: &[crate::host::codec::entities::BoundaryPath]) -> let points = if spline.fit_points.len() >= 2 { spline.fit_points.clone() } else { - spline.control_points.iter().map(|p| Vector2::new(p.x, p.y)).collect() + spline + .control_points + .iter() + .map(|p| Vector2::new(p.x, p.y)) + .collect() }; if points.len() < 2 { - return Err(format!("Hatch boundary path {pi} edge {ei} spline has too few points")); + return Err(format!( + "Hatch boundary path {pi} edge {ei} spline has too few points" + )); } (*points.first().unwrap(), *points.last().unwrap()) } @@ -192,7 +248,9 @@ fn validate_hatch_paths(paths: &[crate::host::codec::entities::BoundaryPath]) -> for ei in 0..endpoints.len() { let next = (ei + 1) % endpoints.len(); if !close(endpoints[ei].1, endpoints[next].0) { - return Err(format!("Hatch boundary path {pi} is not closed between edges {ei} and {next}")); + return Err(format!( + "Hatch boundary path {pi} is not closed between edges {ei} and {next}" + )); } } } @@ -218,14 +276,20 @@ fn validate_hatch_pattern(value: &crate::host::codec::entities::Hatch) -> Result } for (index, line) in value.pattern.lines.iter().enumerate() { if !line.angle.is_finite() - || !line.base_point.x.is_finite() || !line.base_point.y.is_finite() - || !line.offset.x.is_finite() || !line.offset.y.is_finite() + || !line.base_point.x.is_finite() + || !line.base_point.y.is_finite() + || !line.offset.x.is_finite() + || !line.offset.y.is_finite() || line.dash_lengths.iter().any(|dash| !dash.is_finite()) { - return Err(format!("Hatch.pattern.lines[{index}] contains non-finite values")); + return Err(format!( + "Hatch.pattern.lines[{index}] contains non-finite values" + )); } if line.offset.x.hypot(line.offset.y) < 1e-12 { - return Err(format!("Hatch.pattern.lines[{index}].offset must be nonzero")); + return Err(format!( + "Hatch.pattern.lines[{index}].offset must be nonzero" + )); } } Ok(()) @@ -303,7 +367,11 @@ fn validate_leader( { return Err(format!( "Leader.{name} must be finite and {}", - if name == "text_height" { "greater than zero" } else { "non-negative" } + if name == "text_height" { + "greater than zero" + } else { + "non-negative" + } )); } } @@ -346,7 +414,10 @@ fn validate_mline( } for (vi, vertex) in new.vertices.iter().enumerate() { finite_vector(&format!("MLine.vertices[{vi}].position"), &vertex.position)?; - finite_vector(&format!("MLine.vertices[{vi}].direction"), &vertex.direction)?; + finite_vector( + &format!("MLine.vertices[{vi}].direction"), + &vertex.direction, + )?; finite_vector(&format!("MLine.vertices[{vi}].miter"), &vertex.miter)?; for (si, segment) in vertex.segments.iter().enumerate() { if segment @@ -369,7 +440,9 @@ fn validate_mline( return Err("MLine.vertices must not all coincide".into()); } } - if new.flags.contains(crate::host::codec::entities::MLineFlags::CLOSED) + if new + .flags + .contains(crate::host::codec::entities::MLineFlags::CLOSED) && new.vertices.len() < 3 { return Err("closed MLine requires at least 3 vertices".into()); @@ -398,49 +471,103 @@ fn validate_dimension( if base.style_name.trim().is_empty() { return Err("Dimension.style_name is empty".into()); } - let (points, scalars, distinct): (Vec<(&str, &crate::host::codec::types::Vector3)>, Vec<(&str, f64)>, Vec<(&str, &str)>) = match new { + let (points, scalars, distinct): ( + Vec<(&str, &crate::host::codec::types::Vector3)>, + Vec<(&str, f64)>, + Vec<(&str, &str)>, + ) = match new { D::Aligned(d) => ( - vec![("definition_point", &d.definition_point), ("first_point", &d.first_point), ("second_point", &d.second_point)], + vec![ + ("definition_point", &d.definition_point), + ("first_point", &d.first_point), + ("second_point", &d.second_point), + ], vec![("ext_line_rotation", d.ext_line_rotation)], vec![("first_point", "second_point")], ), D::Linear(d) => ( - vec![("definition_point", &d.definition_point), ("first_point", &d.first_point), ("second_point", &d.second_point)], - vec![("rotation", d.rotation), ("ext_line_rotation", d.ext_line_rotation)], + vec![ + ("definition_point", &d.definition_point), + ("first_point", &d.first_point), + ("second_point", &d.second_point), + ], + vec![ + ("rotation", d.rotation), + ("ext_line_rotation", d.ext_line_rotation), + ], vec![("first_point", "second_point")], ), D::Radius(d) => ( - vec![("definition_point", &d.definition_point), ("angle_vertex", &d.angle_vertex)], + vec![ + ("definition_point", &d.definition_point), + ("angle_vertex", &d.angle_vertex), + ], vec![("leader_length", d.leader_length)], vec![("definition_point", "angle_vertex")], ), D::Diameter(d) => ( - vec![("definition_point", &d.definition_point), ("angle_vertex", &d.angle_vertex)], + vec![ + ("definition_point", &d.definition_point), + ("angle_vertex", &d.angle_vertex), + ], vec![("leader_length", d.leader_length)], vec![("definition_point", "angle_vertex")], ), D::Angular2Ln(d) => ( - vec![("definition_point", &d.definition_point), ("dimension_arc", &d.dimension_arc), ("first_point", &d.first_point), ("second_point", &d.second_point), ("angle_vertex", &d.angle_vertex)], + vec![ + ("definition_point", &d.definition_point), + ("dimension_arc", &d.dimension_arc), + ("first_point", &d.first_point), + ("second_point", &d.second_point), + ("angle_vertex", &d.angle_vertex), + ], vec![], vec![], ), D::Angular3Pt(d) => ( - vec![("definition_point", &d.definition_point), ("first_point", &d.first_point), ("second_point", &d.second_point), ("angle_vertex", &d.angle_vertex)], + vec![ + ("definition_point", &d.definition_point), + ("first_point", &d.first_point), + ("second_point", &d.second_point), + ("angle_vertex", &d.angle_vertex), + ], vec![], - vec![("first_point", "angle_vertex"), ("second_point", "angle_vertex")], + vec![ + ("first_point", "angle_vertex"), + ("second_point", "angle_vertex"), + ], ), D::Ordinate(d) => ( - vec![("definition_point", &d.definition_point), ("feature_location", &d.feature_location), ("leader_endpoint", &d.leader_endpoint)], + vec![ + ("definition_point", &d.definition_point), + ("feature_location", &d.feature_location), + ("leader_endpoint", &d.leader_endpoint), + ], vec![], vec![], ), D::Arc(d) => ( - vec![("definition_point", &d.definition_point), ("first_extension_point", &d.first_extension_point), ("second_extension_point", &d.second_extension_point), ("center_point", &d.center_point), ("first_leader_point", &d.first_leader_point), ("second_leader_point", &d.second_leader_point)], - vec![("arc_start_parameter", d.arc_start_parameter), ("arc_end_parameter", d.arc_end_parameter)], + vec![ + ("definition_point", &d.definition_point), + ("first_extension_point", &d.first_extension_point), + ("second_extension_point", &d.second_extension_point), + ("center_point", &d.center_point), + ("first_leader_point", &d.first_leader_point), + ("second_leader_point", &d.second_leader_point), + ], + vec![ + ("arc_start_parameter", d.arc_start_parameter), + ("arc_end_parameter", d.arc_end_parameter), + ], vec![("first_extension_point", "second_extension_point")], ), D::LargeRadial(d) => ( - vec![("definition_point", &d.definition_point), ("chord_point", &d.chord_point), ("override_center", &d.override_center), ("jog_point", &d.jog_point)], + vec![ + ("definition_point", &d.definition_point), + ("chord_point", &d.chord_point), + ("override_center", &d.override_center), + ("jog_point", &d.jog_point), + ], vec![("jog_angle", d.jog_angle)], vec![("definition_point", "chord_point")], ), @@ -484,7 +611,11 @@ fn validate_multileader( if !value.is_finite() || value < 0.0 || (!allow_zero && value == 0.0) { return Err(format!( "MultiLeader.{name} must be finite and {}", - if allow_zero { "non-negative" } else { "greater than zero" } + if allow_zero { + "non-negative" + } else { + "greater than zero" + } )); } } @@ -515,21 +646,38 @@ fn validate_multileader( ] { finite_vector(&format!("MultiLeader.context.{name}"), point)?; } - if context.transform_matrix.iter().any(|value| !value.is_finite()) { + if context + .transform_matrix + .iter() + .any(|value| !value.is_finite()) + { return Err("MultiLeader.context.transform_matrix must be finite".into()); } for (ri, root) in context.leader_roots.iter().enumerate() { - finite_vector(&format!("MultiLeader.context.leader_roots[{ri}].connection_point"), &root.connection_point)?; - finite_vector(&format!("MultiLeader.context.leader_roots[{ri}].direction"), &root.direction)?; + finite_vector( + &format!("MultiLeader.context.leader_roots[{ri}].connection_point"), + &root.connection_point, + )?; + finite_vector( + &format!("MultiLeader.context.leader_roots[{ri}].direction"), + &root.direction, + )?; if !root.landing_distance.is_finite() { - return Err(format!("MultiLeader.context.leader_roots[{ri}].landing_distance must be finite")); + return Err(format!( + "MultiLeader.context.leader_roots[{ri}].landing_distance must be finite" + )); } for (li, line) in root.lines.iter().enumerate() { if line.points.is_empty() { - return Err(format!("MultiLeader.context.leader_roots[{ri}].lines[{li}] has no points")); + return Err(format!( + "MultiLeader.context.leader_roots[{ri}].lines[{li}] has no points" + )); } for (pi, point) in line.points.iter().enumerate() { - finite_vector(&format!("MultiLeader.context.leader_roots[{ri}].lines[{li}].points[{pi}]"), point)?; + finite_vector( + &format!("MultiLeader.context.leader_roots[{ri}].lines[{li}].points[{pi}]"), + point, + )?; } } } @@ -568,12 +716,16 @@ fn validate_table( } for (ci, column) in new.columns.iter().enumerate() { if !column.width.is_finite() || column.width <= 0.0 { - return Err(format!("Table.columns[{ci}].width must be finite and greater than zero")); + return Err(format!( + "Table.columns[{ci}].width must be finite and greater than zero" + )); } } for (ri, row) in new.rows.iter().enumerate() { if !row.height.is_finite() || row.height <= 0.0 { - return Err(format!("Table.rows[{ri}].height must be finite and greater than zero")); + return Err(format!( + "Table.rows[{ri}].height must be finite and greater than zero" + )); } if row.cells.len() != new.columns.len() { return Err(format!( @@ -583,11 +735,20 @@ fn validate_table( )); } for (ci, cell) in row.cells.iter().enumerate() { - if !cell.rotation.is_finite() || !cell.geometry_scale.is_finite() || !cell.block_scale.is_finite() { - return Err(format!("Table.rows[{ri}].cells[{ci}] has non-finite rotation or scale")); + if !cell.rotation.is_finite() + || !cell.geometry_scale.is_finite() + || !cell.block_scale.is_finite() + { + return Err(format!( + "Table.rows[{ri}].cells[{ci}] has non-finite rotation or scale" + )); } for (ki, content) in cell.contents.iter().enumerate() { - if !content.rotation.is_finite() || !content.scale.is_finite() || !content.text_height.is_finite() || content.text_height < 0.0 { + if !content.rotation.is_finite() + || !content.scale.is_finite() + || !content.text_height.is_finite() + || content.text_height < 0.0 + { return Err(format!( "Table.rows[{ri}].cells[{ci}].contents[{ki}] has invalid rotation, scale or text height" )); @@ -607,7 +768,9 @@ fn validate_table( || range.bottom_row >= rows || range.right_col >= columns { - return Err(format!("Table.merged_ranges[{mi}] lies outside the {rows}x{columns} grid")); + return Err(format!( + "Table.merged_ranges[{mi}] lies outside the {rows}x{columns} grid" + )); } for (mj, other) in new.merged_ranges.iter().enumerate().skip(mi + 1) { let separate = range.bottom_row < other.top_row @@ -615,7 +778,9 @@ fn validate_table( || range.right_col < other.left_col || other.right_col < range.left_col; if !separate { - return Err(format!("Table.merged_ranges[{mi}] overlaps merged_ranges[{mj}]")); + return Err(format!( + "Table.merged_ranges[{mi}] overlaps merged_ranges[{mj}]" + )); } } } @@ -651,7 +816,10 @@ fn validate_polygon_mesh( } solid_normal("PolygonMesh.normal", &new.normal)?; for (index, vertex) in new.vertices.iter().enumerate() { - finite_vector(&format!("PolygonMesh.vertices[{index}].location"), &vertex.location)?; + finite_vector( + &format!("PolygonMesh.vertices[{index}].location"), + &vertex.location, + )?; } Ok(()) } @@ -666,7 +834,11 @@ fn validate_polyface_mesh( if old == Some(new) { return Ok(()); } - if !new.elevation.is_finite() || !new.thickness.is_finite() || !new.start_width.is_finite() || !new.end_width.is_finite() { + if !new.elevation.is_finite() + || !new.thickness.is_finite() + || !new.start_width.is_finite() + || !new.end_width.is_finite() + { return Err("PolyfaceMesh elevation, thickness and widths must be finite".into()); } solid_normal("PolyfaceMesh.normal", &new.normal)?; @@ -674,9 +846,18 @@ fn validate_polyface_mesh( return Err("PolyfaceMesh.vertices requires at least 3 vertices".into()); } for (index, vertex) in new.vertices.iter().enumerate() { - finite_vector(&format!("PolyfaceMesh.vertices[{index}].location"), &vertex.location)?; - if !vertex.bulge.is_finite() || !vertex.start_width.is_finite() || !vertex.end_width.is_finite() || !vertex.curve_tangent.is_finite() { - return Err(format!("PolyfaceMesh.vertices[{index}] has non-finite bulge, width or tangent")); + finite_vector( + &format!("PolyfaceMesh.vertices[{index}].location"), + &vertex.location, + )?; + if !vertex.bulge.is_finite() + || !vertex.start_width.is_finite() + || !vertex.end_width.is_finite() + || !vertex.curve_tangent.is_finite() + { + return Err(format!( + "PolyfaceMesh.vertices[{index}] has non-finite bulge, width or tangent" + )); } } if new.faces.is_empty() { @@ -694,7 +875,9 @@ fn validate_polyface_mesh( } } if indices.iter().filter(|value| **value != 0).count() < 3 { - return Err(format!("PolyfaceMesh.faces[{index}] needs at least 3 vertex indices")); + return Err(format!( + "PolyfaceMesh.faces[{index}] needs at least 3 vertex indices" + )); } } Ok(()) @@ -722,7 +905,11 @@ fn validate_mesh( if face.vertices.len() < 3 { return Err(format!("Mesh.faces[{index}] needs at least 3 vertices")); } - if let Some(bad) = face.vertices.iter().find(|vertex| **vertex >= new.vertices.len()) { + if let Some(bad) = face + .vertices + .iter() + .find(|vertex| **vertex >= new.vertices.len()) + { return Err(format!( "Mesh.faces[{index}] references vertex {bad} but only {} exist", new.vertices.len() @@ -731,13 +918,20 @@ fn validate_mesh( } for (index, edge) in new.edges.iter().enumerate() { if edge.start >= new.vertices.len() || edge.end >= new.vertices.len() { - return Err(format!("Mesh.edges[{index}] references a vertex out of range")); + return Err(format!( + "Mesh.edges[{index}] references a vertex out of range" + )); } if edge.start == edge.end { return Err(format!("Mesh.edges[{index}] has identical endpoints")); } - if edge.crease.is_some_and(|crease| !crease.is_finite() || crease < 0.0) { - return Err(format!("Mesh.edges[{index}].crease must be finite and non-negative")); + if edge + .crease + .is_some_and(|crease| !crease.is_finite() || crease < 0.0) + { + return Err(format!( + "Mesh.edges[{index}].crease must be finite and non-negative" + )); } } Ok(()) @@ -765,7 +959,11 @@ fn validate_helix( finite_vector("Helix.axis_base_point", &new.axis_base_point)?; finite_vector("Helix.start_point", &new.start_point)?; solid_normal("Helix.axis_vector", &new.axis_vector)?; - for (name, value) in [("radius", new.radius), ("turns", new.turns), ("turn_height", new.turn_height)] { + for (name, value) in [ + ("radius", new.radius), + ("turns", new.turns), + ("turn_height", new.turn_height), + ] { if !value.is_finite() || value <= 0.0 { return Err(format!("Helix.{name} must be finite and greater than zero")); } @@ -774,7 +972,9 @@ fn validate_helix( let length = axis.x.hypot(axis.y).hypot(axis.z); let delta = new.start_point - new.axis_base_point; let along = (delta.x * axis.x + delta.y * axis.y + delta.z * axis.z) / length; - let radial = ((delta.x * delta.x + delta.y * delta.y + delta.z * delta.z) - along * along).max(0.0).sqrt(); + let radial = ((delta.x * delta.x + delta.y * delta.y + delta.z * delta.z) - along * along) + .max(0.0) + .sqrt(); if radial <= 1.0e-9 { return Err("Helix.start_point must not lie on the axis".into()); } @@ -793,14 +993,19 @@ fn validate_raster_image( } if let Some(old) = old { if old.file_path != new.file_path { - return Err("RasterImage.file_path is fixed at creation; the image definition owns it".into()); + return Err( + "RasterImage.file_path is fixed at creation; the image definition owns it".into(), + ); } } else if new.file_path.trim().is_empty() { return Err("RasterImage.file_path is empty".into()); } else if new.definition_handle.is_none_or(|handle| handle.is_null()) && !std::path::Path::new(&new.file_path).is_file() { - return Err(format!("RasterImage.file_path {:?} is not a readable file", new.file_path)); + return Err(format!( + "RasterImage.file_path {:?} is not a readable file", + new.file_path + )); } finite_vector("RasterImage.insertion_point", &new.insertion_point)?; finite_vector("RasterImage.u_vector", &new.u_vector)?; @@ -820,7 +1025,9 @@ fn validate_raster_image( } for (index, vertex) in new.clip_boundary.vertices.iter().enumerate() { if !vertex.x.is_finite() || !vertex.y.is_finite() { - return Err(format!("RasterImage.clip_boundary.vertices[{index}] must be finite")); + return Err(format!( + "RasterImage.clip_boundary.vertices[{index}] must be finite" + )); } } if new.clipping_enabled && new.clip_boundary.vertices.len() < 2 { @@ -847,7 +1054,8 @@ fn validate_wipeout( if cross.x.hypot(cross.y).hypot(cross.z) <= 1.0e-12 { return Err("Wipeout.u_vector and v_vector must be nonzero and not parallel".into()); } - if !new.size.x.is_finite() || !new.size.y.is_finite() || new.size.x <= 0.0 || new.size.y <= 0.0 { + if !new.size.x.is_finite() || !new.size.y.is_finite() || new.size.x <= 0.0 || new.size.y <= 0.0 + { return Err("Wipeout.size must be finite and greater than zero".into()); } if new.brightness > 100 || new.contrast > 100 || new.fade > 100 { @@ -858,7 +1066,9 @@ fn validate_wipeout( } for (index, vertex) in new.clip_boundary_vertices.iter().enumerate() { if !vertex.x.is_finite() || !vertex.y.is_finite() { - return Err(format!("Wipeout.clip_boundary_vertices[{index}] must be finite")); + return Err(format!( + "Wipeout.clip_boundary_vertices[{index}] must be finite" + )); } } let count = new.clip_boundary_vertices.len(); @@ -894,7 +1104,11 @@ fn validate_underlay( } finite_vector("Underlay.insertion_point", &new.insertion_point)?; solid_normal("Underlay.normal", &new.normal)?; - for (name, value) in [("x_scale", new.x_scale), ("y_scale", new.y_scale), ("z_scale", new.z_scale)] { + for (name, value) in [ + ("x_scale", new.x_scale), + ("y_scale", new.y_scale), + ("z_scale", new.z_scale), + ] { if !value.is_finite() || value == 0.0 { return Err(format!("Underlay.{name} must be finite and nonzero")); } @@ -910,7 +1124,9 @@ fn validate_underlay( } for (index, vertex) in new.clip_boundary_vertices.iter().enumerate() { if !vertex.x.is_finite() || !vertex.y.is_finite() { - return Err(format!("Underlay.clip_boundary_vertices[{index}] must be finite")); + return Err(format!( + "Underlay.clip_boundary_vertices[{index}] must be finite" + )); } } if !new.clip_boundary_vertices.is_empty() && new.clip_boundary_vertices.len() < 2 { @@ -933,12 +1149,24 @@ fn validate_viewport( finite_vector("Viewport.view_center", &new.view_center)?; finite_vector("Viewport.view_target", &new.view_target)?; solid_normal("Viewport.view_direction", &new.view_direction)?; - for (name, value) in [("width", new.width), ("height", new.height), ("view_height", new.view_height), ("lens_length", new.lens_length), ("custom_scale", new.custom_scale)] { + for (name, value) in [ + ("width", new.width), + ("height", new.height), + ("view_height", new.view_height), + ("lens_length", new.lens_length), + ("custom_scale", new.custom_scale), + ] { if !value.is_finite() || value <= 0.0 { - return Err(format!("Viewport.{name} must be finite and greater than zero")); + return Err(format!( + "Viewport.{name} must be finite and greater than zero" + )); } } - for (name, value) in [("front_clip_z", new.front_clip_z), ("back_clip_z", new.back_clip_z), ("twist_angle", new.twist_angle)] { + for (name, value) in [ + ("front_clip_z", new.front_clip_z), + ("back_clip_z", new.back_clip_z), + ("twist_angle", new.twist_angle), + ] { if !value.is_finite() { return Err(format!("Viewport.{name} must be finite")); } @@ -1077,7 +1305,10 @@ fn basic_entity_errors(new: &crate::host::EntityType) -> Vec { EntityType::Ellipse(v) => { chk!(finite_vector("Ellipse.center", &v.center)); chk!(solid_normal("Ellipse.major_axis", &v.major_axis)); - if !v.minor_axis_ratio.is_finite() || v.minor_axis_ratio <= 0.0 || v.minor_axis_ratio > 1.0 { + if !v.minor_axis_ratio.is_finite() + || v.minor_axis_ratio <= 0.0 + || v.minor_axis_ratio > 1.0 + { errs.push("Ellipse.minor_axis_ratio must be within (0, 1]".into()); } chk!(finite("Ellipse.start_parameter", v.start_parameter)); @@ -1119,8 +1350,13 @@ fn basic_entity_errors(new: &crate::host::EntityType) -> Vec { if !vertex.location.x.is_finite() || !vertex.location.y.is_finite() { errs.push(format!("LwPolyline.vertices[{i}].location must be finite")); } - if !vertex.bulge.is_finite() || !vertex.start_width.is_finite() || !vertex.end_width.is_finite() { - errs.push(format!("LwPolyline.vertices[{i}] has a non-finite bulge or width")); + if !vertex.bulge.is_finite() + || !vertex.start_width.is_finite() + || !vertex.end_width.is_finite() + { + errs.push(format!( + "LwPolyline.vertices[{i}] has a non-finite bulge or width" + )); } } if !v.constant_width.is_finite() || v.constant_width < 0.0 { @@ -1135,12 +1371,25 @@ fn basic_entity_errors(new: &crate::host::EntityType) -> Vec { errs.push("Polyline2D.vertices requires at least 2 vertices".into()); } for (i, vertex) in v.vertices.iter().enumerate() { - chk!(finite_vector(&format!("Polyline2D.vertices[{i}].location"), &vertex.location)); - if !vertex.bulge.is_finite() || !vertex.start_width.is_finite() || !vertex.end_width.is_finite() { - errs.push(format!("Polyline2D.vertices[{i}] has a non-finite bulge or width")); + chk!(finite_vector( + &format!("Polyline2D.vertices[{i}].location"), + &vertex.location + )); + if !vertex.bulge.is_finite() + || !vertex.start_width.is_finite() + || !vertex.end_width.is_finite() + { + errs.push(format!( + "Polyline2D.vertices[{i}] has a non-finite bulge or width" + )); } } - for (name, value) in [("start_width", v.start_width), ("end_width", v.end_width), ("thickness", v.thickness), ("elevation", v.elevation)] { + for (name, value) in [ + ("start_width", v.start_width), + ("end_width", v.end_width), + ("thickness", v.thickness), + ("elevation", v.elevation), + ] { chk!(finite(&format!("Polyline2D.{name}"), value)); } chk!(solid_normal("Polyline2D.normal", &v.normal)); @@ -1150,7 +1399,10 @@ fn basic_entity_errors(new: &crate::host::EntityType) -> Vec { errs.push("Polyline.vertices requires at least 2 vertices".into()); } for (i, vertex) in v.vertices.iter().enumerate() { - chk!(finite_vector(&format!("Polyline.vertices[{i}].location"), &vertex.location)); + chk!(finite_vector( + &format!("Polyline.vertices[{i}].location"), + &vertex.location + )); } } EntityType::Polyline3D(v) => { @@ -1158,9 +1410,16 @@ fn basic_entity_errors(new: &crate::host::EntityType) -> Vec { errs.push("Polyline3D.vertices requires at least 2 vertices".into()); } for (i, vertex) in v.vertices.iter().enumerate() { - chk!(finite_vector(&format!("Polyline3D.vertices[{i}].position"), &vertex.position)); + chk!(finite_vector( + &format!("Polyline3D.vertices[{i}].position"), + &vertex.position + )); } - for (name, value) in [("default_start_width", v.default_start_width), ("default_end_width", v.default_end_width), ("elevation", v.elevation)] { + for (name, value) in [ + ("default_start_width", v.default_start_width), + ("default_end_width", v.default_end_width), + ("elevation", v.elevation), + ] { chk!(finite(&format!("Polyline3D.{name}"), value)); } chk!(solid_normal("Polyline3D.normal", &v.normal)); @@ -1255,7 +1514,10 @@ fn validate_surface( if old.is_some_and(|old| old.u_isolines == new.u_isolines && old.v_isolines == new.v_isolines) { return Ok(()); } - for (name, value) in [("u_isolines", new.u_isolines), ("v_isolines", new.v_isolines)] { + for (name, value) in [ + ("u_isolines", new.u_isolines), + ("v_isolines", new.v_isolines), + ] { if !(0..=200).contains(&value) { return Err(format!("Surface.{name} must be between 0 and 200")); } @@ -1271,7 +1533,9 @@ fn validate_section_symbol( old: Option<&crate::host::codec::entities::SectionSymbol>, new: &crate::host::codec::entities::SectionSymbol, ) -> Result<(), String> { - if old.is_some_and(|old| old.points == new.points && old.symbol_scale.to_bits() == new.symbol_scale.to_bits()) { + if old.is_some_and(|old| { + old.points == new.points && old.symbol_scale.to_bits() == new.symbol_scale.to_bits() + }) { return Ok(()); } if !new.symbol_scale.is_finite() || new.symbol_scale <= 0.0 { @@ -1281,10 +1545,18 @@ fn validate_section_symbol( return Err("SectionSymbol.points requires at least 2 points".into()); } for (index, point) in new.points.iter().enumerate() { - finite_vector(&format!("SectionSymbol.points[{index}].point"), &point.point)?; - finite_vector(&format!("SectionSymbol.points[{index}].label_offset"), &point.label_offset)?; + finite_vector( + &format!("SectionSymbol.points[{index}].point"), + &point.point, + )?; + finite_vector( + &format!("SectionSymbol.points[{index}].label_offset"), + &point.label_offset, + )?; if !point.bulge.is_finite() { - return Err(format!("SectionSymbol.points[{index}].bulge must be finite")); + return Err(format!( + "SectionSymbol.points[{index}].bulge must be finite" + )); } } Ok(()) @@ -1403,10 +1675,18 @@ pub fn validate_new_canvas_entity(entity: &crate::host::EntityType) -> Result<() } finite_vector("Insert.insert_point", &value.insert_point)?; solid_normal("Insert.normal", &value.normal)?; - for (name, scale) in [("x_scale", value.x_scale()), ("y_scale", value.y_scale()), ("z_scale", value.z_scale())] { + for (name, scale) in [ + ("x_scale", value.x_scale()), + ("y_scale", value.y_scale()), + ("z_scale", value.z_scale()), + ] { insert_scale(&format!("Insert.{name}"), scale)?; } - for (name, number) in [("rotation", value.rotation), ("column_spacing", value.column_spacing), ("row_spacing", value.row_spacing)] { + for (name, number) in [ + ("rotation", value.rotation), + ("column_spacing", value.column_spacing), + ("row_spacing", value.row_spacing), + ] { if !number.is_finite() { return Err(format!("Insert.{name} must be finite")); } @@ -1512,7 +1792,9 @@ pub fn validate_new_canvas_entity(entity: &crate::host::EntityType) -> Result<() } for (si, sp) in value.seed_points.iter().enumerate() { if !sp.x.is_finite() || !sp.y.is_finite() { - return Err(format!("Hatch.seed_points[{si}] has non-finite coordinates")); + return Err(format!( + "Hatch.seed_points[{si}] has non-finite coordinates" + )); } } validate_hatch_paths(&value.paths)?; @@ -1553,8 +1835,7 @@ pub fn validate_entity_mutation( Err(format!("{name} must contain finite coordinates")) } }; - let same3 = |a: &crate::host::codec::types::Vector3, - b: &crate::host::codec::types::Vector3| { + let same3 = |a: &crate::host::codec::types::Vector3, b: &crate::host::codec::types::Vector3| { a.x.to_bits() == b.x.to_bits() && a.y.to_bits() == b.y.to_bits() && a.z.to_bits() == b.z.to_bits() @@ -1773,18 +2054,34 @@ pub fn validate_entity_mutation( return Err("Shape.style_handle is read-only".into()); } } - (EntityType::Dimension(old), EntityType::Dimension(new)) => validate_dimension(Some(old), new)?, - (EntityType::MultiLeader(old), EntityType::MultiLeader(new)) => validate_multileader(Some(old), new)?, + (EntityType::Dimension(old), EntityType::Dimension(new)) => { + validate_dimension(Some(old), new)? + } + (EntityType::MultiLeader(old), EntityType::MultiLeader(new)) => { + validate_multileader(Some(old), new)? + } (EntityType::Table(old), EntityType::Table(new)) => validate_table(Some(old), new)?, - (EntityType::PolygonMesh(old), EntityType::PolygonMesh(new)) => validate_polygon_mesh(Some(old), new)?, - (EntityType::PolyfaceMesh(old), EntityType::PolyfaceMesh(new)) => validate_polyface_mesh(Some(old), new)?, + (EntityType::PolygonMesh(old), EntityType::PolygonMesh(new)) => { + validate_polygon_mesh(Some(old), new)? + } + (EntityType::PolyfaceMesh(old), EntityType::PolyfaceMesh(new)) => { + validate_polyface_mesh(Some(old), new)? + } (EntityType::Mesh(old), EntityType::Mesh(new)) => validate_mesh(Some(old), new)?, (EntityType::Helix(old), EntityType::Helix(new)) => validate_helix(Some(old), new)?, - (EntityType::RasterImage(old), EntityType::RasterImage(new)) => validate_raster_image(Some(old), new)?, + (EntityType::RasterImage(old), EntityType::RasterImage(new)) => { + validate_raster_image(Some(old), new)? + } (EntityType::Wipeout(old), EntityType::Wipeout(new)) => validate_wipeout(Some(old), new)?, - (EntityType::Underlay(old), EntityType::Underlay(new)) => validate_underlay(Some(old), new)?, - (EntityType::Viewport(old), EntityType::Viewport(new)) => validate_viewport(Some(old), new)?, - (EntityType::ViewBorder(old), EntityType::ViewBorder(new)) => validate_view_border(Some(old), new)?, + (EntityType::Underlay(old), EntityType::Underlay(new)) => { + validate_underlay(Some(old), new)? + } + (EntityType::Viewport(old), EntityType::Viewport(new)) => { + validate_viewport(Some(old), new)? + } + (EntityType::ViewBorder(old), EntityType::ViewBorder(new)) => { + validate_view_border(Some(old), new)? + } (EntityType::Light(old), EntityType::Light(new)) => validate_light(Some(old), new)?, (EntityType::Ole2Frame(old), EntityType::Ole2Frame(new)) => { validate_ole_frame(Some(old), new)?; @@ -1793,7 +2090,9 @@ pub fn validate_entity_mutation( } } (EntityType::Surface(old), EntityType::Surface(new)) => validate_surface(Some(old), new)?, - (EntityType::SectionSymbol(old), EntityType::SectionSymbol(new)) => validate_section_symbol(Some(old), new)?, + (EntityType::SectionSymbol(old), EntityType::SectionSymbol(new)) => { + validate_section_symbol(Some(old), new)? + } (EntityType::MLine(old), EntityType::MLine(new)) => validate_mline(Some(old), new)?, (EntityType::Leader(old), EntityType::Leader(new)) => validate_leader(Some(old), new)?, (EntityType::AttributeDefinition(old), EntityType::AttributeDefinition(new)) => { @@ -1948,7 +2247,9 @@ pub fn validate_entity_mutation( if old.seed_points != new.seed_points { for (si, sp) in new.seed_points.iter().enumerate() { if !sp.x.is_finite() || !sp.y.is_finite() { - return Err(format!("Hatch.seed_points[{si}] has non-finite coordinates")); + return Err(format!( + "Hatch.seed_points[{si}] has non-finite coordinates" + )); } } } @@ -2022,7 +2323,7 @@ pub fn validate_canvas_entity_references( .iter() .any(|style| style.handle == handle) }, - ); + ); if !found { return Err(format!( "Tolerance dimension style {:?} does not exist", @@ -2069,9 +2370,10 @@ pub fn validate_canvas_entity_references( _ => None, } { let name = value.block_name.trim(); - let record = document.block_records.get(name).ok_or_else(|| { - format!("Insert block {:?} does not exist", value.block_name) - })?; + let record = document + .block_records + .get(name) + .ok_or_else(|| format!("Insert block {:?} does not exist", value.block_name))?; if record.is_model_space() || record.is_paper_space() { return Err("Insert cannot reference a model-space or paper-space block".into()); } @@ -2122,19 +2424,30 @@ pub fn validate_canvas_entity_references( _ => false, }; if !matches { - return Err(format!("SectionSymbol {name} {handle:?} is not an existing {wanted} object")); + return Err(format!( + "SectionSymbol {name} {handle:?} is not an existing {wanted} object" + )); } } } if let EntityType::ViewBorder(value) = entity { - if !matches!(document.get_entity(value.active_viewport), Some(EntityType::Viewport(_))) { - return Err(format!("ViewBorder viewport {:?} does not exist", value.active_viewport)); + if !matches!( + document.get_entity(value.active_viewport), + Some(EntityType::Viewport(_)) + ) { + return Err(format!( + "ViewBorder viewport {:?} does not exist", + value.active_viewport + )); } if !matches!( document.objects.get(&value.scale_handle), Some(crate::host::codec::objects::ObjectType::Scale(_)) ) { - return Err(format!("ViewBorder scale {:?} does not exist", value.scale_handle)); + return Err(format!( + "ViewBorder scale {:?} does not exist", + value.scale_handle + )); } } if let EntityType::Viewport(value) = entity { @@ -2177,7 +2490,9 @@ pub fn validate_canvas_entity_references( document.objects.get(&handle), Some(crate::host::codec::objects::ObjectType::ImageDefinition(_)) ) { - return Err(format!("RasterImage image definition {handle:?} does not exist")); + return Err(format!( + "RasterImage image definition {handle:?} does not exist" + )); } } } @@ -2193,13 +2508,23 @@ pub fn validate_canvas_entity_references( for (ri, row) in value.rows.iter().enumerate() { for (ci, cell) in row.cells.iter().enumerate() { for content in &cell.contents { - if let Some(handle) = content.text_style_handle.filter(|handle| !handle.is_null()) { - if !document.text_styles.iter().any(|style| style.handle == handle) { + if let Some(handle) = + content.text_style_handle.filter(|handle| !handle.is_null()) + { + if !document + .text_styles + .iter() + .any(|style| style.handle == handle) + { return Err(format!("Table.rows[{ri}].cells[{ci}] text style handle {handle:?} does not exist")); } } if let Some(handle) = content.block_handle.filter(|handle| !handle.is_null()) { - if !document.block_records.iter().any(|record| record.handle == handle) { + if !document + .block_records + .iter() + .any(|record| record.handle == handle) + { return Err(format!("Table.rows[{ri}].cells[{ci}] block handle {handle:?} does not exist")); } } @@ -2209,34 +2534,63 @@ pub fn validate_canvas_entity_references( } if let EntityType::MultiLeader(value) = entity { use crate::host::codec::objects::ObjectType; - let handle_exists = |handle: Option, what: &str, ok: &dyn Fn(crate::host::codec::Handle) -> bool| { - match handle.filter(|handle| !handle.is_null()) { - Some(handle) if !ok(handle) => Err(format!("MultiLeader {what} handle {handle:?} does not exist")), - _ => Ok(()), - } + let handle_exists = + |handle: Option, + what: &str, + ok: &dyn Fn(crate::host::codec::Handle) -> bool| { + match handle.filter(|handle| !handle.is_null()) { + Some(handle) if !ok(handle) => Err(format!( + "MultiLeader {what} handle {handle:?} does not exist" + )), + _ => Ok(()), + } + }; + let is_block = |h| { + document + .block_records + .iter() + .any(|record| record.handle == h) }; - let is_block = |h| document.block_records.iter().any(|record| record.handle == h); let is_text_style = |h| document.text_styles.iter().any(|style| style.handle == h); let is_linetype = |h| document.line_types.iter().any(|ltype| ltype.handle == h); - let is_mleader_style = |h| matches!(document.objects.get(&h), Some(ObjectType::MultiLeaderStyle(_))); + let is_mleader_style = |h| { + matches!( + document.objects.get(&h), + Some(ObjectType::MultiLeaderStyle(_)) + ) + }; handle_exists(value.style_handle, "style", &is_mleader_style)?; handle_exists(value.text_style_handle, "text style", &is_text_style)?; - handle_exists(value.context.text_style_handle, "context text style", &is_text_style)?; + handle_exists( + value.context.text_style_handle, + "context text style", + &is_text_style, + )?; handle_exists(value.line_type_handle, "line type", &is_linetype)?; handle_exists(value.arrowhead_handle, "arrowhead block", &is_block)?; handle_exists(value.block_content_handle, "block content", &is_block)?; - handle_exists(value.context.block_content_handle, "context block content", &is_block)?; + handle_exists( + value.context.block_content_handle, + "context block content", + &is_block, + )?; } if let EntityType::Dimension(value) = entity { let name = value.base().style_name.trim(); - if !document.dim_styles.iter().any(|style| style.name.eq_ignore_ascii_case(name)) { - return Err(format!("Dimension style {:?} does not exist", value.base().style_name)); + if !document + .dim_styles + .iter() + .any(|style| style.name.eq_ignore_ascii_case(name)) + { + return Err(format!( + "Dimension style {:?} does not exist", + value.base().style_name + )); } } if let EntityType::MLine(value) = entity { - let style = resolve_mline_style(document, value).ok_or_else(|| { - format!("MLine style {:?} does not exist", value.style_name) - })?; + let style = resolve_mline_style(document, value) + .ok_or_else(|| format!("MLine style {:?} does not exist", value.style_name))?; if style.elements.is_empty() { return Err(format!("MLine style {:?} has no elements", style.name)); } @@ -2253,11 +2607,11 @@ pub fn validate_canvas_entity_references( } if let EntityType::Leader(value) = entity { use crate::host::codec::entities::LeaderCreationType; - if !document - .dim_styles - .iter() - .any(|style| style.name.eq_ignore_ascii_case(value.dimension_style.trim())) - { + if !document.dim_styles.iter().any(|style| { + style + .name + .eq_ignore_ascii_case(value.dimension_style.trim()) + }) { return Err(format!( "Leader dimension style {:?} does not exist", value.dimension_style @@ -2277,10 +2631,9 @@ pub fn validate_canvas_entity_references( ) })?; let expected = match value.creation_type { - LeaderCreationType::WithText => matches!( - annotation, - EntityType::Text(_) | EntityType::MText(_) - ), + LeaderCreationType::WithText => { + matches!(annotation, EntityType::Text(_) | EntityType::MText(_)) + } LeaderCreationType::WithTolerance => matches!(annotation, EntityType::Tolerance(_)), LeaderCreationType::WithBlock => matches!(annotation, EntityType::Insert(_)), LeaderCreationType::NoAnnotation => unreachable!(), @@ -2565,8 +2918,17 @@ mod tests { serde_json::from_str(get_embedded_entity_coverage_json()).unwrap(); for entry in &catalog.entity_kinds { let mut seen = std::collections::HashSet::new(); - for property in entry.properties.iter().filter(|p| p.model_access != ModelAccess::Unmapped) { - assert!(seen.insert(property.name.clone()), "{}: duplicate exposed name {}", entry.kind, property.name); + for property in entry + .properties + .iter() + .filter(|p| p.model_access != ModelAccess::Unmapped) + { + assert!( + seen.insert(property.name.clone()), + "{}: duplicate exposed name {}", + entry.kind, + property.name + ); } } } @@ -2648,7 +3010,7 @@ mod tests { entry .properties .iter() - .filter(|property| property.validation == "transaction_geometry") + .filter(|property| property.validation == "transaction_geometry") .map(|property| format!("{}.{}", entry.kind, property.name)) }) .collect(); @@ -2676,7 +3038,7 @@ mod tests { "Face3D.second_corner".to_owned(), "Face3D.third_corner".to_owned(), "Face3D.fourth_corner".to_owned(), - "Face3D.invisible_edges".to_owned(), + "Face3D.invisible_edges".to_owned(), "Insert.insert_point".to_owned(), "Insert.x_scale".to_owned(), "Insert.y_scale".to_owned(), @@ -2693,7 +3055,7 @@ mod tests { "Tolerance.text".to_owned(), "Tolerance.dimension_style_name".to_owned(), "Tolerance.text_height".to_owned(), - "Tolerance.dimension_gap".to_owned(), + "Tolerance.dimension_gap".to_owned(), "Shape.insertion_point".to_owned(), "Shape.size".to_owned(), "Shape.shape_name".to_owned(), @@ -2706,24 +3068,24 @@ mod tests { "Shape.style_name".to_owned(), "AttributeDefinition.tag".to_owned(), "AttributeDefinition.prompt".to_owned(), - "AttributeDefinition.default_value".to_owned(), - "AttributeDefinition.insertion_point".to_owned(), - "AttributeDefinition.alignment_point".to_owned(), + "AttributeDefinition.default_value".to_owned(), + "AttributeDefinition.insertion_point".to_owned(), + "AttributeDefinition.alignment_point".to_owned(), "AttributeDefinition.height".to_owned(), "AttributeDefinition.rotation".to_owned(), - "AttributeDefinition.width_factor".to_owned(), - "AttributeDefinition.oblique_angle".to_owned(), - "AttributeDefinition.text_style".to_owned(), - "AttributeDefinition.text_generation_flags".to_owned(), - "AttributeDefinition.horizontal_alignment".to_owned(), - "AttributeDefinition.vertical_alignment".to_owned(), + "AttributeDefinition.width_factor".to_owned(), + "AttributeDefinition.oblique_angle".to_owned(), + "AttributeDefinition.text_style".to_owned(), + "AttributeDefinition.text_generation_flags".to_owned(), + "AttributeDefinition.horizontal_alignment".to_owned(), + "AttributeDefinition.vertical_alignment".to_owned(), "AttributeDefinition.flags".to_owned(), "AttributeDefinition.field_length".to_owned(), "AttributeDefinition.normal".to_owned(), "AttributeDefinition.mtext_flag".to_owned(), "AttributeDefinition.is_multiline".to_owned(), "AttributeDefinition.line_count".to_owned(), - "AttributeDefinition.lock_position".to_owned(), + "AttributeDefinition.lock_position".to_owned(), "AttributeEntity.tag".to_owned(), "AttributeEntity.value".to_owned(), "AttributeEntity.insertion_point".to_owned(), @@ -2982,10 +3344,8 @@ mod tests { fn tolerance_geometry_and_style_references_are_validated() { use crate::host::codec::{self, types::Vector3}; let document = codec::CadDocument::new(); - let mut tolerance = codec::entities::Tolerance::with_text( - Vector3::new(1.0, 2.0, 0.0), - "{\\Fgdt;p}%%v0.1", - ); + let mut tolerance = + codec::entities::Tolerance::with_text(Vector3::new(1.0, 2.0, 0.0), "{\\Fgdt;p}%%v0.1"); let entity = codec::EntityType::Tolerance(tolerance.clone()); validate_new_canvas_entity(&entity).unwrap(); validate_canvas_entity_references(&document, &entity).unwrap(); @@ -3033,22 +3393,25 @@ mod tests { .contains("does not match")); let mut missing = leader.clone(); missing.annotation_handle = codec::Handle::new(0xdead); - assert!(validate_canvas_entity_references( - &document, - &codec::EntityType::Leader(missing) - ) - .unwrap_err() - .contains("does not exist")); + assert!( + validate_canvas_entity_references(&document, &codec::EntityType::Leader(missing)) + .unwrap_err() + .contains("does not exist") + ); let mut none_with_handle = leader.clone(); none_with_handle.creation_type = LeaderCreationType::NoAnnotation; - assert!(validate_new_canvas_entity(&codec::EntityType::Leader(none_with_handle)) - .unwrap_err() - .contains("no annotation")); + assert!( + validate_new_canvas_entity(&codec::EntityType::Leader(none_with_handle)) + .unwrap_err() + .contains("no annotation") + ); let mut one_point = leader.clone(); one_point.vertices.truncate(1); - assert!(validate_new_canvas_entity(&codec::EntityType::Leader(one_point)) - .unwrap_err() - .contains("at least 2")); + assert!( + validate_new_canvas_entity(&codec::EntityType::Leader(one_point)) + .unwrap_err() + .contains("at least 2") + ); let mut bad_style = leader.clone(); bad_style.dimension_style = "Missing".into(); assert!(validate_canvas_entity_references( @@ -3075,7 +3438,9 @@ mod tests { let document = codec::CadDocument::new(); let mut mline = codec::entities::MLine::new(); for x in [0.0, 5.0] { - mline.vertices.push(MLineVertex::new(Vector3::new(x, 0.0, 0.0))); + mline + .vertices + .push(MLineVertex::new(Vector3::new(x, 0.0, 0.0))); } let mut entity = codec::EntityType::MLine(mline.clone()); validate_new_canvas_entity(&entity).unwrap(); @@ -3084,21 +3449,36 @@ mod tests { let mut missing = mline.clone(); missing.style_name = "Missing".into(); - assert!(bind_canvas_entity_references(&document, &mut codec::EntityType::MLine(missing)) - .unwrap_err().contains("does not exist")); + assert!( + bind_canvas_entity_references(&document, &mut codec::EntityType::MLine(missing)) + .unwrap_err() + .contains("does not exist") + ); let mut wrong_segments = mline.clone(); wrong_segments.vertices[0].init_segments(3); - assert!(bind_canvas_entity_references(&document, &mut codec::EntityType::MLine(wrong_segments)) - .unwrap_err().contains("segments but style")); + assert!(bind_canvas_entity_references( + &document, + &mut codec::EntityType::MLine(wrong_segments) + ) + .unwrap_err() + .contains("segments but style")); let mut one = mline.clone(); one.vertices.truncate(1); - assert!(validate_new_canvas_entity(&codec::EntityType::MLine(one)).unwrap_err().contains("at least 2")); + assert!(validate_new_canvas_entity(&codec::EntityType::MLine(one)) + .unwrap_err() + .contains("at least 2")); let mut closed = mline.clone(); closed.flags |= codec::entities::MLineFlags::CLOSED; - assert!(validate_new_canvas_entity(&codec::EntityType::MLine(closed)).unwrap_err().contains("at least 3")); + assert!( + validate_new_canvas_entity(&codec::EntityType::MLine(closed)) + .unwrap_err() + .contains("at least 3") + ); let mut scale = mline.clone(); scale.scale_factor = 0.0; - assert!(validate_new_canvas_entity(&codec::EntityType::MLine(scale)).unwrap_err().contains("nonzero")); + assert!(validate_new_canvas_entity(&codec::EntityType::MLine(scale)) + .unwrap_err() + .contains("nonzero")); } #[cfg(feature = "host")] @@ -3129,11 +3509,9 @@ mod tests { unreachable!() }; bad.size = 0.0; - assert!( - validate_new_canvas_entity(&codec::EntityType::Shape(bad)) - .unwrap_err() - .contains("greater than zero") - ); + assert!(validate_new_canvas_entity(&codec::EntityType::Shape(bad)) + .unwrap_err() + .contains("greater than zero")); } #[test] @@ -3257,11 +3635,9 @@ mod tests { let mut xline = codec::entities::XLine::default(); xline.base_point.x = f64::NAN; - assert!( - validate_new_canvas_entity(&codec::EntityType::XLine(xline)) - .unwrap_err() - .contains("XLine.base_point") - ); + assert!(validate_new_canvas_entity(&codec::EntityType::XLine(xline)) + .unwrap_err() + .contains("XLine.base_point")); let solid = codec::entities::Solid::new( Vector3::ZERO, @@ -3438,35 +3814,42 @@ mod tests { let mut invalid = hatch.clone(); invalid.paths.clear(); - assert!(validate_new_canvas_entity(&codec::EntityType::Hatch(invalid)) - .unwrap_err() - .contains("at least one boundary path")); + assert!( + validate_new_canvas_entity(&codec::EntityType::Hatch(invalid)) + .unwrap_err() + .contains("at least one boundary path") + ); let mut invalid = hatch.clone(); invalid.is_solid = false; invalid.pattern_scale = 0.0; - assert!(validate_new_canvas_entity(&codec::EntityType::Hatch(invalid)) - .unwrap_err() - .contains("greater than zero")); + assert!( + validate_new_canvas_entity(&codec::EntityType::Hatch(invalid)) + .unwrap_err() + .contains("greater than zero") + ); let mut document = codec::CadDocument::new(); - let boundary = document.add_entity(codec::EntityType::Line( - codec::entities::Line::from_points( + let boundary = document + .add_entity(codec::EntityType::Line(codec::entities::Line::from_points( codec::types::Vector3::ZERO, codec::types::Vector3::UNIT_X, - ), - )).unwrap(); + ))) + .unwrap(); let mut associative = hatch.clone(); associative.is_associative = true; associative.paths[0].boundary_handles.push(boundary); validate_canvas_entity_references( &document, &codec::EntityType::Hatch(associative.clone()), - ).unwrap(); + ) + .unwrap(); associative.paths[0].boundary_handles[0] = codec::Handle::new(u64::MAX); assert!(validate_canvas_entity_references( &document, &codec::EntityType::Hatch(associative), - ).unwrap_err().contains("does not exist")); + ) + .unwrap_err() + .contains("does not exist")); } } diff --git a/crates/ocs_plugin_api/src/entity_coverage_types.rs b/crates/ocs_plugin_api/src/entity_coverage_types.rs index 5d101c694..49654d36a 100644 --- a/crates/ocs_plugin_api/src/entity_coverage_types.rs +++ b/crates/ocs_plugin_api/src/entity_coverage_types.rs @@ -1,5 +1,4 @@ /// Stable, language-neutral entity capability schema. - use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] diff --git a/crates/ocs_plugin_api/src/host.rs b/crates/ocs_plugin_api/src/host.rs index 5880a0335..ad9966ff1 100644 --- a/crates/ocs_plugin_api/src/host.rs +++ b/crates/ocs_plugin_api/src/host.rs @@ -65,15 +65,37 @@ pub enum LogLevel { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum SolidPrimitive { /// Axis-aligned box from its center and full extents. - Box { center: [f64; 3], size: [f64; 3] }, + Box { + center: [f64; 3], + size: [f64; 3], + }, /// Right triangular prism from its minimum corner and extents. - Wedge { origin: [f64; 3], size: [f64; 3] }, + Wedge { + origin: [f64; 3], + size: [f64; 3], + }, /// Cylinder standing on the plane through `center`. - Cylinder { center: [f64; 3], radius: f64, height: f64 }, - Sphere { center: [f64; 3], radius: f64 }, - Torus { center: [f64; 3], major: f64, minor: f64 }, + Cylinder { + center: [f64; 3], + radius: f64, + height: f64, + }, + Sphere { + center: [f64; 3], + radius: f64, + }, + Torus { + center: [f64; 3], + major: f64, + minor: f64, + }, /// Regular pyramid with `sides` base edges. - Pyramid { center: [f64; 3], radius: f64, height: f64, sides: u32 }, + Pyramid { + center: [f64; 3], + radius: f64, + height: f64, + sides: u32, + }, } /// How two solids combine. @@ -159,12 +181,20 @@ pub enum TableOperation { /// Create a dimension style, optionally copying an existing one first. /// `properties` is a JSON object of DimStyle fields (see the Python docs); /// handles and xref fields are managed by the host and refused. - DimStyleCreate { name: String, copy_from: Option, properties: String }, + DimStyleCreate { + name: String, + copy_from: Option, + properties: String, + }, /// Change only the DimStyle fields the JSON object `properties` names. DimStyleModify { name: String, properties: String }, /// Rename a text or dimension style; references to it follow. `Standard` /// is never renamed and a case-only change is refused. - StyleRename { kind: TableStyleKind, from: String, to: String }, + StyleRename { + kind: TableStyleKind, + from: String, + to: String, + }, /// Delete a text or dimension style that is neither current nor in use. /// `Standard` is never deleted. StyleDelete { kind: TableStyleKind, name: String }, @@ -200,9 +230,17 @@ pub enum TableOperation { /// Create a simple linetype. `pattern` is signed lengths in drawing units: /// positive is a dash, negative a gap, zero a dot (2-12 elements with at /// least one dash or dot and one gap). Returns the linetype's handle. - LinetypeCreate { name: String, description: String, pattern: Vec }, + LinetypeCreate { + name: String, + description: String, + pattern: Vec, + }, /// Change a simple linetype's description or pattern in place. - LinetypeModify { name: String, description: Option, pattern: Option> }, + LinetypeModify { + name: String, + description: Option, + pattern: Option>, + }, /// Rename a linetype; layers and entities that use it follow. LinetypeRename { from: String, to: String }, /// Delete a linetype that no layer, entity, dimension style or the current @@ -257,7 +295,10 @@ pub struct TextStyleConfig { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum SolidOperation { /// Build a new solid on `layer` (layer `0` when `None`). - Create { primitive: SolidPrimitive, layer: Option }, + Create { + primitive: SolidPrimitive, + layer: Option, + }, /// Apply a rigid transform, given as a column-major 4x4 matrix, to an /// existing solid. Scaling and shear are refused. Transform { handle: Handle, matrix: [f64; 16] }, @@ -265,12 +306,25 @@ pub enum SolidOperation { /// ellipse, closed polyline or spline, ...). The new region goes on /// `layer`, or on the source's layer when `None`; `delete_source` erases /// the profile afterwards, as the REGION command does. - RegionFromProfile { source: Handle, layer: Option, delete_source: bool }, + RegionFromProfile { + source: Handle, + layer: Option, + delete_source: bool, + }, /// Build a plane surface from one closed planar profile entity. - SurfaceFromProfile { source: Handle, layer: Option, delete_source: bool }, + SurfaceFromProfile { + source: Handle, + layer: Option, + delete_source: bool, + }, /// Extrude a planar profile along `direction`: a solid when the profile is /// closed, a surface when it is open. - Extrude { source: Handle, direction: [f64; 3], layer: Option, delete_source: bool }, + Extrude { + source: Handle, + direction: [f64; 3], + layer: Option, + delete_source: bool, + }, /// Combine two solids into a new one. Both operands are consumed unless /// `keep_operands`; the result goes on `layer` or the first operand's /// layer. The kernel refuses some cases (coincident faces, cuts it has no @@ -280,7 +334,12 @@ pub enum SolidOperation { /// `origin`, `width` drawing units wide, keeping the picture's aspect. /// PNG, JPEG and BMP are stored as read; other formats are re-encoded as /// PNG. The frame goes on `layer` (layer `0` when `None`). - EmbedPicture { path: String, origin: [f64; 3], width: f64, layer: Option }, + EmbedPicture { + path: String, + origin: [f64; 3], + width: f64, + layer: Option, + }, Boolean { first: Handle, second: Handle, @@ -302,21 +361,41 @@ pub enum HostSettingValue { #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum HostNotification { - InputLine { line: String }, + InputLine { + line: String, + }, Cancel, - DocumentChanged { version: u64 }, - SelectionChanged { handles: Vec }, + DocumentChanged { + version: u64, + }, + SelectionChanged { + handles: Vec, + }, Raw(Vec), /// V4 snapshot changed for a specific tab. Discriminant 5. - DocumentChangedV4 { tab_id: u64, version: u64 }, + DocumentChangedV4 { + tab_id: u64, + version: u64, + }, /// V4 tab closed notification. Discriminant 6. - DocumentTabClosed { tab_id: u64 }, + DocumentTabClosed { + tab_id: u64, + }, /// V4 selection changed for a specific tab. Discriminant 7. - SelectionChangedV4 { tab_id: u64, handles: Vec }, + SelectionChangedV4 { + tab_id: u64, + handles: Vec, + }, /// V7 active interactive-command state for a tab. Discriminant 8. - CommandStateChanged { tab_id: u64, command: Option }, + CommandStateChanged { + tab_id: u64, + command: Option, + }, /// V7 drawing epoch, broadcast even without an open shared view. Discriminant 9. - DrawingChanged { tab_id: u64, epoch: u64 }, + DrawingChanged { + tab_id: u64, + epoch: u64, + }, /// Fallback for notification variants added in future minor revisions. /// Carries the raw bincode payload so an older peer can ignore it without /// failing deserialization. @@ -329,57 +408,44 @@ impl Serialize for HostNotification { match self { HostNotification::InputLine { line } => { bytes.push(0); - bincode::serialize_into(&mut bytes, line) - .map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, line).map_err(serde::ser::Error::custom)?; } HostNotification::Cancel => bytes.push(1), HostNotification::DocumentChanged { version } => { bytes.push(2); - bincode::serialize_into(&mut bytes, version) - .map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, version).map_err(serde::ser::Error::custom)?; } HostNotification::SelectionChanged { handles } => { bytes.push(3); - bincode::serialize_into(&mut bytes, handles) - .map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, handles).map_err(serde::ser::Error::custom)?; } HostNotification::Raw(data) => { bytes.push(4); - bincode::serialize_into(&mut bytes, data) - .map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, data).map_err(serde::ser::Error::custom)?; } HostNotification::DocumentChangedV4 { tab_id, version } => { bytes.push(5); - bincode::serialize_into(&mut bytes, tab_id) - .map_err(serde::ser::Error::custom)?; - bincode::serialize_into(&mut bytes, version) - .map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, tab_id).map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, version).map_err(serde::ser::Error::custom)?; } HostNotification::DocumentTabClosed { tab_id } => { bytes.push(6); - bincode::serialize_into(&mut bytes, tab_id) - .map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, tab_id).map_err(serde::ser::Error::custom)?; } HostNotification::SelectionChangedV4 { tab_id, handles } => { bytes.push(7); - bincode::serialize_into(&mut bytes, tab_id) - .map_err(serde::ser::Error::custom)?; - bincode::serialize_into(&mut bytes, handles) - .map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, tab_id).map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, handles).map_err(serde::ser::Error::custom)?; } HostNotification::CommandStateChanged { tab_id, command } => { bytes.push(8); - bincode::serialize_into(&mut bytes, tab_id) - .map_err(serde::ser::Error::custom)?; - bincode::serialize_into(&mut bytes, command) - .map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, tab_id).map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, command).map_err(serde::ser::Error::custom)?; } HostNotification::DrawingChanged { tab_id, epoch } => { bytes.push(9); - bincode::serialize_into(&mut bytes, tab_id) - .map_err(serde::ser::Error::custom)?; - bincode::serialize_into(&mut bytes, epoch) - .map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, tab_id).map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, epoch).map_err(serde::ser::Error::custom)?; } HostNotification::Unknown(raw) => bytes.extend_from_slice(raw), } @@ -434,14 +500,28 @@ impl<'de> Deserialize<'de> for HostNotification { #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum PluginNotification { - Output { text: String }, - Error { text: String }, - Prompt { text: String }, - Progress { percent: u8 }, - Log { level: LogLevel, text: String }, + Output { + text: String, + }, + Error { + text: String, + }, + Prompt { + text: String, + }, + Progress { + percent: u8, + }, + Log { + level: LogLevel, + text: String, + }, Raw(Vec), /// V4 REPL status update. Discriminant 6. - ReplStatus { status: String, message: String }, + ReplStatus { + status: String, + message: String, + }, /// Fallback for notification variants added in future minor revisions. /// Carries the raw bincode payload so an older peer can ignore it without /// failing deserialization. @@ -454,42 +534,33 @@ impl Serialize for PluginNotification { match self { PluginNotification::Output { text } => { bytes.push(0); - bincode::serialize_into(&mut bytes, text) - .map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, text).map_err(serde::ser::Error::custom)?; } PluginNotification::Error { text } => { bytes.push(1); - bincode::serialize_into(&mut bytes, text) - .map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, text).map_err(serde::ser::Error::custom)?; } PluginNotification::Prompt { text } => { bytes.push(2); - bincode::serialize_into(&mut bytes, text) - .map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, text).map_err(serde::ser::Error::custom)?; } PluginNotification::Progress { percent } => { bytes.push(3); - bincode::serialize_into(&mut bytes, percent) - .map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, percent).map_err(serde::ser::Error::custom)?; } PluginNotification::Log { level, text } => { bytes.push(4); - bincode::serialize_into(&mut bytes, level) - .map_err(serde::ser::Error::custom)?; - bincode::serialize_into(&mut bytes, text) - .map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, level).map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, text).map_err(serde::ser::Error::custom)?; } PluginNotification::Raw(data) => { bytes.push(5); - bincode::serialize_into(&mut bytes, data) - .map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, data).map_err(serde::ser::Error::custom)?; } PluginNotification::ReplStatus { status, message } => { bytes.push(6); - bincode::serialize_into(&mut bytes, status) - .map_err(serde::ser::Error::custom)?; - bincode::serialize_into(&mut bytes, message) - .map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, status).map_err(serde::ser::Error::custom)?; + bincode::serialize_into(&mut bytes, message).map_err(serde::ser::Error::custom)?; } PluginNotification::Unknown(raw) => bytes.extend_from_slice(raw), } @@ -519,8 +590,8 @@ impl<'de> Deserialize<'de> for PluginNotification { .map(|percent| PluginNotification::Progress { percent }) .map_err(serde::de::Error::custom), 4 => { - let (level, text): (LogLevel, String) = bincode::deserialize(rest) - .map_err(serde::de::Error::custom)?; + let (level, text): (LogLevel, String) = + bincode::deserialize(rest).map_err(serde::de::Error::custom)?; Ok(PluginNotification::Log { level, text }) } 5 => bincode::deserialize(rest) @@ -641,7 +712,10 @@ pub enum PreviewWire { impl PreviewWire { /// Create a polyline preview wire with default host cyan color. pub fn new(points: Vec<[f64; 3]>) -> Self { - Self::Polyline { points, color: None } + Self::Polyline { + points, + color: None, + } } /// Optional RGBA color (0.0 to 1.0) specified for this preview wire. @@ -869,9 +943,7 @@ pub trait HostApi { /// Returns the optional `command_id` used to correlate the notification /// with a running command, and the notification payload. Long-running /// plugins should call this periodically to drain the bounded queue. - fn try_recv_notification( - &mut self, - ) -> Option<(Option, HostNotification)> { + fn try_recv_notification(&mut self) -> Option<(Option, HostNotification)> { None } @@ -949,7 +1021,9 @@ pub trait HostApi { } /// Current ordered selection in this session's tab (API v7). - fn selection(&self) -> Vec { Vec::new() } + fn selection(&self) -> Vec { + Vec::new() + } /// Replace this tab's selection with exactly these handles, in order. /// Missing or duplicate handles reject the request (API v7). diff --git a/crates/ocs_plugin_api/src/ipc/client.rs b/crates/ocs_plugin_api/src/ipc/client.rs index 1d10b1130..829a84255 100644 --- a/crates/ocs_plugin_api/src/ipc/client.rs +++ b/crates/ocs_plugin_api/src/ipc/client.rs @@ -16,7 +16,7 @@ use crate::ipc::protocol::{ HostResponse, HostToPlugin, PluginRequest, PluginResponse, PluginToHost, RunnerHandshake, }; use crate::ipc::transport::{recv, send}; -use crate::shm::{DocumentViewInfo, SharedDocumentReader, DocumentViewData}; +use crate::shm::{DocumentViewData, DocumentViewInfo, SharedDocumentReader}; /// Shared registry of active interactive commands, keyed by host-assigned id. pub type InteractiveRegistry = Rc>>>; @@ -60,7 +60,10 @@ impl IpcClient { &self, req: PluginRequest, ) -> Result { - send(&mut self.stream.borrow_mut(), &PluginToHost::Request(Box::new(req)))?; + send( + &mut self.stream.borrow_mut(), + &PluginToHost::Request(Box::new(req)), + )?; loop { match recv::(&mut self.stream.borrow_mut())? { HostToPlugin::Response(resp) => return Ok(*resp), @@ -389,16 +392,18 @@ impl HostApi for PluginHostApi { } } match self.doc_view.borrow().as_ref() { - Some(info) => match SharedDocumentReader::::open(Path::new(&info.path)) { - Ok(reader) => Box::new(reader), - Err(e) => { - eprintln!( - "[plugin] failed to open document view at {}: {e}", - info.path - ); - Box::new(EmptyDocumentReader) + Some(info) => { + match SharedDocumentReader::::open(Path::new(&info.path)) { + Ok(reader) => Box::new(reader), + Err(e) => { + eprintln!( + "[plugin] failed to open document view at {}: {e}", + info.path + ); + Box::new(EmptyDocumentReader) + } } - }, + } None => Box::new(EmptyDocumentReader), } } @@ -419,7 +424,9 @@ impl HostApi for PluginHostApi { } fn system_variable(&self, name: &str) -> Option { - match self.client.request(PluginRequest::GetSystemVariable { name: name.to_owned() }) { + match self.client.request(PluginRequest::GetSystemVariable { + name: name.to_owned(), + }) { Ok(PluginResponse::SystemVariable(value)) => value, _ => None, } @@ -451,11 +458,16 @@ impl HostApi for PluginHostApi { label: &str, entities: Vec, ) -> Result<(), String> { - match self.client.request(PluginRequest::UpdateEntitiesTransaction { - label: label.to_owned(), entities, - }) { + match self + .client + .request(PluginRequest::UpdateEntitiesTransaction { + label: label.to_owned(), + entities, + }) { Ok(PluginResponse::EntityTransactionResult(result)) => { - if result.is_ok() { self.document_cache = OnceCell::new(); } + if result.is_ok() { + self.document_cache = OnceCell::new(); + } result } Ok(PluginResponse::Error(error)) => Err(error), @@ -471,10 +483,18 @@ impl HostApi for PluginHostApi { } } - fn solid_operation(&mut self, operation: crate::host::SolidOperation) -> Result { - match self.client.request(PluginRequest::SolidOperation { operation }) { + fn solid_operation( + &mut self, + operation: crate::host::SolidOperation, + ) -> Result { + match self + .client + .request(PluginRequest::SolidOperation { operation }) + { Ok(PluginResponse::SolidResult(result)) => { - if result.is_ok() { self.document_cache = OnceCell::new(); } + if result.is_ok() { + self.document_cache = OnceCell::new(); + } result } Ok(PluginResponse::Error(error)) => Err(error), @@ -483,7 +503,10 @@ impl HostApi for PluginHostApi { } } - fn run_command(&mut self, request: crate::host::CommandRequest) -> Result { + fn run_command( + &mut self, + request: crate::host::CommandRequest, + ) -> Result { match self.client.request(PluginRequest::RunCommand { request }) { Ok(PluginResponse::CommandResult(result)) => { self.document_cache = OnceCell::new(); @@ -495,10 +518,18 @@ impl HostApi for PluginHostApi { } } - fn table_operation(&mut self, operation: crate::host::TableOperation) -> Result { - match self.client.request(PluginRequest::TableOperation { operation }) { + fn table_operation( + &mut self, + operation: crate::host::TableOperation, + ) -> Result { + match self + .client + .request(PluginRequest::TableOperation { operation }) + { Ok(PluginResponse::TableResult(result)) => { - if result.is_ok() { self.document_cache = OnceCell::new(); } + if result.is_ok() { + self.document_cache = OnceCell::new(); + } result } Ok(PluginResponse::Error(error)) => Err(error), @@ -508,7 +539,9 @@ impl HostApi for PluginHostApi { } fn set_selection(&mut self, handles: &[Handle]) -> Result<(), String> { - match self.client.request(PluginRequest::SetSelection { handles: handles.to_vec() }) { + match self.client.request(PluginRequest::SetSelection { + handles: handles.to_vec(), + }) { Ok(PluginResponse::SelectionResult(result)) => result, Ok(PluginResponse::Error(error)) => Err(error), Ok(other) => Err(format!("unexpected selection response: {other:?}")), @@ -555,7 +588,10 @@ impl HostApi for PluginHostApi { } fn execute_command(&mut self, cmd: &str) -> bool { - match self.client.request(PluginRequest::ExecuteCommand(cmd.to_string())) { + match self + .client + .request(PluginRequest::ExecuteCommand(cmd.to_string())) + { Ok(PluginResponse::Bool(b)) => b, Ok(other) => { eprintln!("[plugin] unexpected ExecuteCommand response: {other:?}"); @@ -662,7 +698,11 @@ mod tests { }, other => panic!("unexpected: {other:?}"), } - send(&mut peer, &HostToPlugin::Response(Box::new(PluginResponse::Ok))).unwrap(); + send( + &mut peer, + &HostToPlugin::Response(Box::new(PluginResponse::Ok)), + ) + .unwrap(); }); api.push_info("hello host"); peer_handle.join().unwrap(); @@ -719,7 +759,10 @@ mod tests { EntityType::Point(Point::new()), ]); peer_handle.join().unwrap(); - assert_eq!(handles, vec![Handle::new(10), Handle::new(11), Handle::new(12)]); + assert_eq!( + handles, + vec![Handle::new(10), Handle::new(11), Handle::new(12)] + ); } #[test] @@ -734,7 +777,11 @@ mod tests { }, other => panic!("unexpected: {other:?}"), } - send(&mut peer, &HostToPlugin::Response(Box::new(PluginResponse::Bool(true)))).unwrap(); + send( + &mut peer, + &HostToPlugin::Response(Box::new(PluginResponse::Bool(true))), + ) + .unwrap(); }); assert!(api.update_entity(EntityType::Point(Point::new()))); peer_handle.join().unwrap(); @@ -754,7 +801,11 @@ mod tests { }, other => panic!("unexpected: {other:?}"), } - send(&mut peer, &HostToPlugin::Response(Box::new(PluginResponse::Bool(true)))).unwrap(); + send( + &mut peer, + &HostToPlugin::Response(Box::new(PluginResponse::Bool(true))), + ) + .unwrap(); }); assert!(api.remove_entity(Handle::new(7))); peer_handle.join().unwrap(); diff --git a/crates/ocs_plugin_api/src/ipc/mod.rs b/crates/ocs_plugin_api/src/ipc/mod.rs index 971b47e9b..befb23aac 100644 --- a/crates/ocs_plugin_api/src/ipc/mod.rs +++ b/crates/ocs_plugin_api/src/ipc/mod.rs @@ -23,13 +23,13 @@ pub mod client; #[cfg(feature = "host")] pub mod protocol; #[cfg(feature = "host")] +pub mod proxy; +#[cfg(feature = "host")] pub mod server; #[cfg(feature = "host")] pub mod transport; #[cfg(feature = "host")] pub mod v4; -#[cfg(feature = "host")] -pub mod proxy; #[cfg(all(test, feature = "host"))] mod tests { diff --git a/crates/ocs_plugin_api/src/ipc/protocol.rs b/crates/ocs_plugin_api/src/ipc/protocol.rs index a7f282be7..3832140af 100644 --- a/crates/ocs_plugin_api/src/ipc/protocol.rs +++ b/crates/ocs_plugin_api/src/ipc/protocol.rs @@ -21,9 +21,9 @@ use crate::host::{CommandSource, CommandStep, HostSettingValue}; use crate::manifest::ApiVersion; use crate::ribbon::owned::{OwnedPluginManifest, OwnedRibbonGroup}; +pub use crate::host::PreviewWire; pub use codec::xdata::{ExtendedDataRecord, XDataValue}; pub use codec::{CadDocument, EntityType, Handle}; -pub use crate::host::PreviewWire; /// Events the host forwards to an active plugin `InteractiveCommand`. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -50,7 +50,10 @@ pub enum InteractiveEvent { #[derive(Debug, Serialize, Deserialize)] pub enum RunnerHandshake { Token(String), - TokenV4 { token: String, protocol_version: u32 }, + TokenV4 { + token: String, + protocol_version: u32, + }, } /// Environment variable through which the host passes the pre-shared @@ -83,7 +86,9 @@ pub enum HostRequest { tab_index: usize, }, /// V7: release an interactive command after completion or cancellation. - DropInteractive { command_id: u64 }, + DropInteractive { + command_id: u64, + }, /// V7: cursor move preview update during an interactive command. CursorMove { command_id: u64, @@ -145,13 +150,19 @@ pub enum PluginRequest { AddEntities(Vec), /// V4: ask the host to create/refresh a tab-keyed shared-memory document /// view and return the file path + current version. - OpenDocumentViewV4 { tab_id: u64 }, + OpenDocumentViewV4 { + tab_id: u64, + }, /// V4: ask the host to close the tab-keyed shared-memory document view. - CloseDocumentViewV4 { tab_id: u64 }, + CloseDocumentViewV4 { + tab_id: u64, + }, /// V4: ask the host for the stable tab identifier of the active tab. GetTabId, /// V5: ask the host for the filesystem path of the document in `tab_id`. - DocumentPath { tab_id: u64 }, + DocumentPath { + tab_id: u64, + }, /// Add a layer to the active document with full initial properties. AddLayer(crate::host::LayerConfig), /// Modify specified properties of an existing layer in the active document. @@ -159,20 +170,36 @@ pub enum PluginRequest { /// Run a command on the active document tab's command line (AutoLISP style). ExecuteCommand(String), /// Read a host-managed setting without nested command dispatch. - GetSystemVariable { name: String }, + GetSystemVariable { + name: String, + }, /// Change a host-managed setting without nested command dispatch. - SetSystemVariable { name: String, value: HostSettingValue }, + SetSystemVariable { + name: String, + value: HostSettingValue, + }, /// V7: validate and replace existing entities in a single undo step. - UpdateEntitiesTransaction { label: String, entities: Vec }, + UpdateEntitiesTransaction { + label: String, + entities: Vec, + }, /// V7: synchronous selection read/write for the dispatch tab. GetSelection, - SetSelection { handles: Vec }, + SetSelection { + handles: Vec, + }, /// V7 (additive): kernel-backed solid create or transform. - SolidOperation { operation: crate::host::SolidOperation }, + SolidOperation { + operation: crate::host::SolidOperation, + }, /// V7 (additive): drawing table record create/modify/rename/delete. - TableOperation { operation: crate::host::TableOperation }, + TableOperation { + operation: crate::host::TableOperation, + }, /// V7 (additive): drive an OCS command. - RunCommand { request: crate::host::CommandRequest }, + RunCommand { + request: crate::host::CommandRequest, + }, } /// Responses the host sends back for `PluginRequest`. diff --git a/crates/ocs_plugin_api/src/ipc/proxy.rs b/crates/ocs_plugin_api/src/ipc/proxy.rs index 6b5172b08..7816903d6 100644 --- a/crates/ocs_plugin_api/src/ipc/proxy.rs +++ b/crates/ocs_plugin_api/src/ipc/proxy.rs @@ -50,7 +50,10 @@ impl From for PluginRequestError { } /// Send a length-framed bincode message over a generic stream. -pub fn send_framed(writer: &mut W, msg: &impl serde::Serialize) -> Result<(), ProxyError> { +pub fn send_framed( + writer: &mut W, + msg: &impl serde::Serialize, +) -> Result<(), ProxyError> { let bytes = bincode::serialize(msg)?; if bytes.len() > MAX_MESSAGE_SIZE { return Err(ProxyError::TooLarge(bytes.len())); @@ -63,7 +66,9 @@ pub fn send_framed(writer: &mut W, msg: &impl serde::Serialize) -> Res } /// Receive a length-framed bincode message from a generic stream. -pub fn recv_framed(reader: &mut R) -> Result { +pub fn recv_framed( + reader: &mut R, +) -> Result { let mut len_buf = [0u8; 8]; reader.read_exact(&mut len_buf)?; let len = u64::from_le_bytes(len_buf) as usize; @@ -114,10 +119,12 @@ fn read_with_poll( let mut off = 0; while off < buf.len() { match reader.read(&mut buf[off..]) { - Ok(0) => return Err(PollError::Io(ProxyError::Io(std::io::Error::new( - std::io::ErrorKind::UnexpectedEof, - "proxy disconnected", - )))), + Ok(0) => { + return Err(PollError::Io(ProxyError::Io(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + "proxy disconnected", + )))) + } Ok(n) => off += n, Err(e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => { if let Err(err) = poll() { @@ -386,7 +393,8 @@ mod tests { run_request_proxy(listener, sender, token).ok(); }); - let sender = ProxyPluginRequestSender::connect_with_token("127.0.0.1", port, &token).unwrap(); + let sender = + ProxyPluginRequestSender::connect_with_token("127.0.0.1", port, &token).unwrap(); let resp = sender .request(PluginRequest::PushInfo("hello proxy".to_string())) .unwrap(); diff --git a/crates/ocs_plugin_api/src/ipc/server.rs b/crates/ocs_plugin_api/src/ipc/server.rs index 1941184d2..2856700a2 100644 --- a/crates/ocs_plugin_api/src/ipc/server.rs +++ b/crates/ocs_plugin_api/src/ipc/server.rs @@ -110,8 +110,12 @@ pub fn handle_plugin_request( ), GetSelection => PluginResponse::Selection(host.selection()), SetSelection { handles } => PluginResponse::SelectionResult(host.set_selection(&handles)), - SolidOperation { operation } => PluginResponse::SolidResult(host.solid_operation(operation)), - TableOperation { operation } => PluginResponse::TableResult(host.table_operation(operation)), + SolidOperation { operation } => { + PluginResponse::SolidResult(host.solid_operation(operation)) + } + TableOperation { operation } => { + PluginResponse::TableResult(host.table_operation(operation)) + } RunCommand { request } => PluginResponse::CommandResult(host.run_command(request)), } } diff --git a/crates/ocs_plugin_api/src/ipc/v4/client.rs b/crates/ocs_plugin_api/src/ipc/v4/client.rs index 41834fc05..22cea2f08 100644 --- a/crates/ocs_plugin_api/src/ipc/v4/client.rs +++ b/crates/ocs_plugin_api/src/ipc/v4/client.rs @@ -16,8 +16,8 @@ use interprocess::local_socket::{GenericNamespaced, Stream, ToNsName}; use interprocess::TryClone; use crate::host::{ - DocumentReader, ExecutionResult, HostApi, HostNotification, InteractiveCommand, - PluginNotification, PluginRequestSender, PluginRequestError, ReaderEntity, HostSettingValue, + DocumentReader, ExecutionResult, HostApi, HostNotification, HostSettingValue, + InteractiveCommand, PluginNotification, PluginRequestError, PluginRequestSender, ReaderEntity, }; use crate::ipc::protocol::{ HostRequest, HostResponse, PluginRequest, PluginResponse, RunnerHandshake, @@ -26,7 +26,7 @@ use crate::ipc::transport::{recv, send, TransportError}; use crate::ipc::v4::protocol::{ HostToPluginV4, NotificationEnvelope, PluginToHostV4, V4_PROTOCOL_VERSION, }; -use crate::shm::{DocumentViewInfo, SharedDocumentReader, DocumentViewData}; +use crate::shm::{DocumentViewData, DocumentViewInfo, SharedDocumentReader}; /// Default capacity of the bounded host→plugin notification queue. const DEFAULT_NOTIFY_QUEUE_SIZE: usize = 1024; @@ -89,8 +89,7 @@ impl V4Client { ))) })?; - let (notify_tx, notify_rx) = - mpsc::sync_channel(notify_queue_size()); + let (notify_tx, notify_rx) = mpsc::sync_channel(notify_queue_size()); let (runner_tx, runner_rx) = mpsc::channel(); let shared = Arc::new(Shared { @@ -101,14 +100,7 @@ impl V4Client { let shared_for_reader = Arc::clone(&shared); let notifications = Arc::new(Mutex::new(notify_rx)); - std::thread::spawn(move || { - reader_thread( - reader, - shared_for_reader, - notify_tx, - runner_tx, - ) - }); + std::thread::spawn(move || reader_thread(reader, shared_for_reader, notify_tx, runner_tx)); Ok(Self { shared, @@ -133,7 +125,11 @@ impl V4Client { /// Non-blocking poll for host-to-plugin notifications. pub fn try_recv_notification(&self) -> Option<(Option, HostNotification)> { - self.notifications.lock().unwrap_or_else(|e| e.into_inner()).try_recv().ok() + self.notifications + .lock() + .unwrap_or_else(|e| e.into_inner()) + .try_recv() + .ok() } /// Send a plugin response for a host request. @@ -143,18 +139,11 @@ impl V4Client { resp: HostResponse, ) -> Result<(), crate::ipc::transport::TransportError> { let mut writer = self.shared.writer.lock().unwrap_or_else(|e| e.into_inner()); - send( - &mut writer, - &PluginToHostV4::Response { id, payload: resp }, - ) + send(&mut writer, &PluginToHostV4::Response { id, payload: resp }) } /// Send a plugin-to-host notification. - pub fn notify_plugin( - &self, - command_id: Option, - notification: PluginNotification, - ) { + pub fn notify_plugin(&self, command_id: Option, notification: PluginNotification) { let envelope = NotificationEnvelope { command_id, payload: notification, @@ -168,10 +157,7 @@ impl V4Client { /// Build a callback that sends a delayed `CodeExecutionResult` response for /// the given request id. Used by `start_execute_code` implementations that /// run REPL code on a background thread. - pub fn execute_code_responder( - &self, - id: u64, - ) -> Box { + pub fn execute_code_responder(&self, id: u64) -> Box { let shared = Arc::clone(&self.shared); Box::new(move |result| { let mut writer = shared.writer.lock().unwrap_or_else(|e| e.into_inner()); @@ -235,7 +221,10 @@ fn reader_thread( } } Ok(HostToPluginV4::Notification(envelope)) => { - if notify_tx.try_send((envelope.command_id, envelope.payload)).is_err() { + if notify_tx + .try_send((envelope.command_id, envelope.payload)) + .is_err() + { let now = Instant::now(); let should_log = last_drop_log .map(|t| now.duration_since(t) >= DROP_LOG_INTERVAL) @@ -247,7 +236,10 @@ fn reader_thread( } } Ok(HostToPluginV4::Request { id, payload }) => { - if runner_tx.send(RunnerFrame::Request { id, payload }).is_err() { + if runner_tx + .send(RunnerFrame::Request { id, payload }) + .is_err() + { // Runner loop has exited; stop reading. break; } @@ -510,9 +502,7 @@ impl HostApi for V4PluginHostApi { fn write_record(&mut self, handle: Handle, record: ExtendedDataRecord) -> bool { let app = record.application_name.clone(); - match self - .request(PluginRequest::WriteRecord { handle, record }) - { + match self.request(PluginRequest::WriteRecord { handle, record }) { Ok(PluginResponse::Bool(b)) => { if b { self.record_cache.borrow_mut().remove(&(handle, app)); @@ -631,16 +621,18 @@ impl HostApi for V4PluginHostApi { } } match self.doc_view.borrow().as_ref() { - Some(info) => match SharedDocumentReader::::open(Path::new(&info.path)) { - Ok(reader) => Box::new(reader), - Err(e) => { - eprintln!( - "[plugin] failed to open document view at {}: {e}", - info.path - ); - Box::new(EmptyDocumentReader) + Some(info) => { + match SharedDocumentReader::::open(Path::new(&info.path)) { + Ok(reader) => Box::new(reader), + Err(e) => { + eprintln!( + "[plugin] failed to open document view at {}: {e}", + info.path + ); + Box::new(EmptyDocumentReader) + } } - }, + } None => Box::new(EmptyDocumentReader), } } @@ -657,7 +649,11 @@ impl HostApi for V4PluginHostApi { } fn try_recv_notification(&mut self) -> Option<(Option, HostNotification)> { - self.notifications.lock().unwrap_or_else(|e| e.into_inner()).try_recv().ok() + self.notifications + .lock() + .unwrap_or_else(|e| e.into_inner()) + .try_recv() + .ok() } fn plugin_request_sender(&self) -> Option> { @@ -734,7 +730,9 @@ impl HostApi for V4PluginHostApi { } fn system_variable(&self, name: &str) -> Option { - match self.request(PluginRequest::GetSystemVariable { name: name.to_owned() }) { + match self.request(PluginRequest::GetSystemVariable { + name: name.to_owned(), + }) { Ok(PluginResponse::SystemVariable(value)) => value, _ => None, } @@ -767,10 +765,13 @@ impl HostApi for V4PluginHostApi { entities: Vec, ) -> Result<(), String> { match self.request(PluginRequest::UpdateEntitiesTransaction { - label: label.to_owned(), entities, + label: label.to_owned(), + entities, }) { Ok(PluginResponse::EntityTransactionResult(result)) => { - if result.is_ok() { self.document_cache = OnceCell::new(); } + if result.is_ok() { + self.document_cache = OnceCell::new(); + } result } Ok(PluginResponse::Error(error)) => Err(error), @@ -786,10 +787,15 @@ impl HostApi for V4PluginHostApi { } } - fn solid_operation(&mut self, operation: crate::host::SolidOperation) -> Result { + fn solid_operation( + &mut self, + operation: crate::host::SolidOperation, + ) -> Result { match self.request(PluginRequest::SolidOperation { operation }) { Ok(PluginResponse::SolidResult(result)) => { - if result.is_ok() { self.document_cache = OnceCell::new(); } + if result.is_ok() { + self.document_cache = OnceCell::new(); + } result } Ok(PluginResponse::Error(error)) => Err(error), @@ -798,7 +804,10 @@ impl HostApi for V4PluginHostApi { } } - fn run_command(&mut self, request: crate::host::CommandRequest) -> Result { + fn run_command( + &mut self, + request: crate::host::CommandRequest, + ) -> Result { match self.request(PluginRequest::RunCommand { request }) { Ok(PluginResponse::CommandResult(result)) => { self.document_cache = OnceCell::new(); @@ -810,10 +819,15 @@ impl HostApi for V4PluginHostApi { } } - fn table_operation(&mut self, operation: crate::host::TableOperation) -> Result { + fn table_operation( + &mut self, + operation: crate::host::TableOperation, + ) -> Result { match self.request(PluginRequest::TableOperation { operation }) { Ok(PluginResponse::TableResult(result)) => { - if result.is_ok() { self.document_cache = OnceCell::new(); } + if result.is_ok() { + self.document_cache = OnceCell::new(); + } result } Ok(PluginResponse::Error(error)) => Err(error), @@ -823,7 +837,9 @@ impl HostApi for V4PluginHostApi { } fn set_selection(&mut self, handles: &[Handle]) -> Result<(), String> { - match self.request(PluginRequest::SetSelection { handles: handles.to_vec() }) { + match self.request(PluginRequest::SetSelection { + handles: handles.to_vec(), + }) { Ok(PluginResponse::SelectionResult(result)) => result, Ok(PluginResponse::Error(error)) => Err(error), Ok(other) => Err(format!("unexpected selection response: {other:?}")), @@ -899,9 +915,7 @@ impl V4Client { }); let shared_for_reader = Arc::clone(&shared); let notifications = Arc::new(Mutex::new(notify_rx)); - std::thread::spawn(move || { - reader_thread(reader, shared_for_reader, notify_tx, runner_tx) - }); + std::thread::spawn(move || reader_thread(reader, shared_for_reader, notify_tx, runner_tx)); Self { shared, notifications, @@ -1081,7 +1095,10 @@ mod tests { }); assert!(matches!(first.join().unwrap(), PluginResponse::Bool(true))); - assert!(matches!(second.join().unwrap(), PluginResponse::Bool(false))); + assert!(matches!( + second.join().unwrap(), + PluginResponse::Bool(false) + )); runner.join().unwrap(); } diff --git a/crates/ocs_plugin_api/src/ipc/v4/protocol.rs b/crates/ocs_plugin_api/src/ipc/v4/protocol.rs index 7f5822603..0b4e206d9 100644 --- a/crates/ocs_plugin_api/src/ipc/v4/protocol.rs +++ b/crates/ocs_plugin_api/src/ipc/v4/protocol.rs @@ -53,7 +53,10 @@ pub enum PluginToHostV4 { tab_id: Option, payload: PluginRequest, }, - Response { id: u64, payload: HostResponse }, + Response { + id: u64, + payload: HostResponse, + }, Notification(NotificationEnvelope), } @@ -95,11 +98,17 @@ mod tests { let bytes = bincode::serialize(¬ification).unwrap(); let decoded: HostNotification = bincode::deserialize(&bytes).unwrap(); assert_eq!(decoded, notification); - let stopped = HostNotification::CommandStateChanged { tab_id: 42, command: None }; + let stopped = HostNotification::CommandStateChanged { + tab_id: 42, + command: None, + }; let bytes = bincode::serialize(&stopped).unwrap(); let decoded: HostNotification = bincode::deserialize(&bytes).unwrap(); assert_eq!(decoded, stopped); - let changed = HostNotification::DrawingChanged { tab_id: 42, epoch: 7 }; + let changed = HostNotification::DrawingChanged { + tab_id: 42, + epoch: 7, + }; let bytes = bincode::serialize(&changed).unwrap(); let decoded: HostNotification = bincode::deserialize(&bytes).unwrap(); assert_eq!(decoded, changed); @@ -109,19 +118,33 @@ mod tests { fn command_request_and_outcome_roundtrip() { use crate::host::{CommandOutcome, CommandRequest}; for request in [ - CommandRequest::Run { line: "LINE 0,0 1,1".into() }, - CommandRequest::Start { name: "OFFSET".into() }, - CommandRequest::Point { point: [1.0, 2.0, 3.0] }, + CommandRequest::Run { + line: "LINE 0,0 1,1".into(), + }, + CommandRequest::Start { + name: "OFFSET".into(), + }, + CommandRequest::Point { + point: [1.0, 2.0, 3.0], + }, CommandRequest::Text { text: "2".into() }, CommandRequest::Token { text: "R".into() }, - CommandRequest::Entity { handle: codec::Handle::new(9), point: [0.0; 3] }, + CommandRequest::Entity { + handle: codec::Handle::new(9), + point: [0.0; 3], + }, CommandRequest::Selection, CommandRequest::Enter, CommandRequest::Cancel, ] { - let bytes = bincode::serialize(&PluginRequest::RunCommand { request: request.clone() }).unwrap(); - assert!(matches!(bincode::deserialize::(&bytes).unwrap(), - PluginRequest::RunCommand { request: decoded } if decoded == request)); + let bytes = bincode::serialize(&PluginRequest::RunCommand { + request: request.clone(), + }) + .unwrap(); + assert!( + matches!(bincode::deserialize::(&bytes).unwrap(), + PluginRequest::RunCommand { request: decoded } if decoded == request) + ); } let outcome = CommandOutcome { status: "waiting_input".into(), @@ -137,8 +160,10 @@ mod tests { }; for result in [Ok(outcome), Err("refused".to_owned())] { let bytes = bincode::serialize(&PluginResponse::CommandResult(result.clone())).unwrap(); - assert!(matches!(bincode::deserialize::(&bytes).unwrap(), - PluginResponse::CommandResult(decoded) if decoded == result)); + assert!( + matches!(bincode::deserialize::(&bytes).unwrap(), + PluginResponse::CommandResult(decoded) if decoded == result) + ); } } @@ -155,19 +180,60 @@ mod tests { ..Default::default() }, }, - TableOperation::LayerModify { config: LayerConfig { name: "Walls".into(), off: Some(true), ..Default::default() } }, - TableOperation::LayerRename { from: "A".into(), to: "B".into() }, - TableOperation::LayerDelete { name: "A".into(), erase_objects: true }, + TableOperation::LayerModify { + config: LayerConfig { + name: "Walls".into(), + off: Some(true), + ..Default::default() + }, + }, + TableOperation::LayerRename { + from: "A".into(), + to: "B".into(), + }, + TableOperation::LayerDelete { + name: "A".into(), + erase_objects: true, + }, TableOperation::LayerSetCurrent { name: "A".into() }, TableOperation::TextStyleCreate { - config: crate::host::TextStyleConfig { name: "T".into(), height: Some(2.5), oblique_angle: Some(0.2), backward: Some(true), ..Default::default() }, + config: crate::host::TextStyleConfig { + name: "T".into(), + height: Some(2.5), + oblique_angle: Some(0.2), + backward: Some(true), + ..Default::default() + }, + }, + TableOperation::TextStyleModify { + config: crate::host::TextStyleConfig { + name: "T".into(), + font_file: Some("romans".into()), + ..Default::default() + }, + }, + TableOperation::DimStyleCreate { + name: "D".into(), + copy_from: Some("Standard".into()), + properties: "{\"dimscale\":2}".into(), + }, + TableOperation::DimStyleModify { + name: "D".into(), + properties: "{}".into(), + }, + TableOperation::StyleRename { + kind: crate::host::TableStyleKind::Text, + from: "A".into(), + to: "B".into(), + }, + TableOperation::StyleDelete { + kind: crate::host::TableStyleKind::Dim, + name: "A".into(), + }, + TableOperation::StyleSetCurrent { + kind: crate::host::TableStyleKind::Dim, + name: "A".into(), }, - TableOperation::TextStyleModify { config: crate::host::TextStyleConfig { name: "T".into(), font_file: Some("romans".into()), ..Default::default() } }, - TableOperation::DimStyleCreate { name: "D".into(), copy_from: Some("Standard".into()), properties: "{\"dimscale\":2}".into() }, - TableOperation::DimStyleModify { name: "D".into(), properties: "{}".into() }, - TableOperation::StyleRename { kind: crate::host::TableStyleKind::Text, from: "A".into(), to: "B".into() }, - TableOperation::StyleDelete { kind: crate::host::TableStyleKind::Dim, name: "A".into() }, - TableOperation::StyleSetCurrent { kind: crate::host::TableStyleKind::Dim, name: "A".into() }, TableOperation::BlockCreate { name: "B".into(), entities: vec![codec::Handle::new(4), codec::Handle::new(5)], @@ -175,31 +241,67 @@ mod tests { erase_originals: true, description: Some("d".into()), }, - TableOperation::BlockModify { name: "B".into(), description: None, explodable: Some(false), scale_uniformly: Some(true) }, - TableOperation::BlockRename { from: "B".into(), to: "C".into() }, + TableOperation::BlockModify { + name: "B".into(), + description: None, + explodable: Some(false), + scale_uniformly: Some(true), + }, + TableOperation::BlockRename { + from: "B".into(), + to: "C".into(), + }, TableOperation::BlockDelete { name: "C".into() }, TableOperation::BlockEntityAdd { block: "C".into(), entity: codec::EntityType::Line(codec::entities::Line::new()), }, - TableOperation::LinetypeCreate { name: "L".into(), description: "d".into(), pattern: vec![1.0, -1.0] }, - TableOperation::LinetypeModify { name: "L".into(), description: None, pattern: Some(vec![2.0, -2.0]) }, - TableOperation::LinetypeRename { from: "L".into(), to: "M".into() }, + TableOperation::LinetypeCreate { + name: "L".into(), + description: "d".into(), + pattern: vec![1.0, -1.0], + }, + TableOperation::LinetypeModify { + name: "L".into(), + description: None, + pattern: Some(vec![2.0, -2.0]), + }, + TableOperation::LinetypeRename { + from: "L".into(), + to: "M".into(), + }, TableOperation::LinetypeDelete { name: "M".into() }, TableOperation::LayoutCreate { name: "S".into() }, - TableOperation::LayoutRename { from: "S".into(), to: "T".into() }, + TableOperation::LayoutRename { + from: "S".into(), + to: "T".into(), + }, TableOperation::LayoutDelete { name: "T".into() }, - TableOperation::LayoutSetCurrent { name: "Model".into() }, - TableOperation::LayoutSetPage { name: "S".into(), paper_size: Some([420.0, 297.0]), rotation: Some(90), scale: Some([1.0, 50.0]) }, + TableOperation::LayoutSetCurrent { + name: "Model".into(), + }, + TableOperation::LayoutSetPage { + name: "S".into(), + paper_size: Some([420.0, 297.0]), + rotation: Some(90), + scale: Some([1.0, 50.0]), + }, ] { - let bytes = bincode::serialize(&PluginRequest::TableOperation { operation: operation.clone() }).unwrap(); - assert!(matches!(bincode::deserialize::(&bytes).unwrap(), - PluginRequest::TableOperation { operation: decoded } if decoded == operation)); + let bytes = bincode::serialize(&PluginRequest::TableOperation { + operation: operation.clone(), + }) + .unwrap(); + assert!( + matches!(bincode::deserialize::(&bytes).unwrap(), + PluginRequest::TableOperation { operation: decoded } if decoded == operation) + ); } for result in [Ok(codec::Handle::new(9)), Err("refused".to_owned())] { let bytes = bincode::serialize(&PluginResponse::TableResult(result.clone())).unwrap(); - assert!(matches!(bincode::deserialize::(&bytes).unwrap(), - PluginResponse::TableResult(decoded) if decoded == result)); + assert!( + matches!(bincode::deserialize::(&bytes).unwrap(), + PluginResponse::TableResult(decoded) if decoded == result) + ); } } @@ -208,12 +310,29 @@ mod tests { use crate::host::{SolidOperation, SolidPrimitive}; for operation in [ SolidOperation::Create { - primitive: SolidPrimitive::Pyramid { center: [1.0, 2.0, 3.0], radius: 4.0, height: 5.0, sides: 6 }, + primitive: SolidPrimitive::Pyramid { + center: [1.0, 2.0, 3.0], + radius: 4.0, + height: 5.0, + sides: 6, + }, layer: Some("SOLIDS".into()), }, - SolidOperation::Transform { handle: codec::Handle::new(7), matrix: [1.0; 16] }, - SolidOperation::RegionFromProfile { source: codec::Handle::new(3), layer: None, delete_source: true }, - SolidOperation::EmbedPicture { path: "p.png".into(), origin: [0.0; 3], width: 5.0, layer: None }, + SolidOperation::Transform { + handle: codec::Handle::new(7), + matrix: [1.0; 16], + }, + SolidOperation::RegionFromProfile { + source: codec::Handle::new(3), + layer: None, + delete_source: true, + }, + SolidOperation::EmbedPicture { + path: "p.png".into(), + origin: [0.0; 3], + width: 5.0, + layer: None, + }, SolidOperation::Boolean { first: codec::Handle::new(1), second: codec::Handle::new(2), @@ -221,30 +340,52 @@ mod tests { layer: None, keep_operands: false, }, - SolidOperation::SurfaceFromProfile { source: codec::Handle::new(4), layer: Some("S".into()), delete_source: false }, - SolidOperation::Extrude { source: codec::Handle::new(5), direction: [0.0, 0.0, 2.0], layer: None, delete_source: true }, + SolidOperation::SurfaceFromProfile { + source: codec::Handle::new(4), + layer: Some("S".into()), + delete_source: false, + }, + SolidOperation::Extrude { + source: codec::Handle::new(5), + direction: [0.0, 0.0, 2.0], + layer: None, + delete_source: true, + }, ] { - let bytes = bincode::serialize(&PluginRequest::SolidOperation { operation: operation.clone() }).unwrap(); - assert!(matches!(bincode::deserialize::(&bytes).unwrap(), - PluginRequest::SolidOperation { operation: decoded } if decoded == operation)); + let bytes = bincode::serialize(&PluginRequest::SolidOperation { + operation: operation.clone(), + }) + .unwrap(); + assert!( + matches!(bincode::deserialize::(&bytes).unwrap(), + PluginRequest::SolidOperation { operation: decoded } if decoded == operation) + ); } for result in [Ok(codec::Handle::new(9)), Err("refused".to_owned())] { let bytes = bincode::serialize(&PluginResponse::SolidResult(result.clone())).unwrap(); - assert!(matches!(bincode::deserialize::(&bytes).unwrap(), - PluginResponse::SolidResult(decoded) if decoded == result)); + assert!( + matches!(bincode::deserialize::(&bytes).unwrap(), + PluginResponse::SolidResult(decoded) if decoded == result) + ); } } #[test] fn selection_request_and_response_roundtrip() { - let request = PluginRequest::SetSelection { handles: vec![codec::Handle::new(9)] }; + let request = PluginRequest::SetSelection { + handles: vec![codec::Handle::new(9)], + }; let bytes = bincode::serialize(&request).unwrap(); - assert!(matches!(bincode::deserialize::(&bytes).unwrap(), - PluginRequest::SetSelection { handles } if handles == vec![codec::Handle::new(9)])); + assert!( + matches!(bincode::deserialize::(&bytes).unwrap(), + PluginRequest::SetSelection { handles } if handles == vec![codec::Handle::new(9)]) + ); let response = PluginResponse::Selection(vec![codec::Handle::new(9)]); let bytes = bincode::serialize(&response).unwrap(); - assert!(matches!(bincode::deserialize::(&bytes).unwrap(), - PluginResponse::Selection(handles) if handles == vec![codec::Handle::new(9)])); + assert!( + matches!(bincode::deserialize::(&bytes).unwrap(), + PluginResponse::Selection(handles) if handles == vec![codec::Handle::new(9)]) + ); } #[test] diff --git a/crates/ocs_plugin_api/src/ipc/v4/server.rs b/crates/ocs_plugin_api/src/ipc/v4/server.rs index 08e38c669..d6fdea7d5 100644 --- a/crates/ocs_plugin_api/src/ipc/v4/server.rs +++ b/crates/ocs_plugin_api/src/ipc/v4/server.rs @@ -26,7 +26,10 @@ macro_rules! vlog { /// A frame delivered from the V4 reader thread to the host main thread. pub enum HostIncoming { - Response { id: u64, payload: crate::ipc::protocol::HostResponse }, + Response { + id: u64, + payload: crate::ipc::protocol::HostResponse, + }, Request { id: u64, tab_id: Option, diff --git a/crates/ocs_plugin_api/src/lib.rs b/crates/ocs_plugin_api/src/lib.rs index c36f23d49..e315972eb 100644 --- a/crates/ocs_plugin_api/src/lib.rs +++ b/crates/ocs_plugin_api/src/lib.rs @@ -32,12 +32,12 @@ //! - `runner` — child-process runner entry point. //! - `shm` — shared-memory document views. +pub mod entity_coverage; +pub mod entity_coverage_types; pub mod manifest; pub mod ribbon; pub mod type_registry; pub mod type_registry_types; -pub mod entity_coverage; -pub mod entity_coverage_types; pub mod version_info; /// Runtime host surface — only built with the `host` feature (pulls `opencadcodec`). @@ -66,17 +66,19 @@ pub mod shm; #[cfg(feature = "host")] pub mod runner; +pub use entity_coverage::{ + get_embedded_entity_coverage_json, EntityCoverageCatalog, EntityKindCoverage, EntityScope, + ModelAccess, PropertyCoverage, +}; pub use manifest::{ effective_max_api_version, host_accepts_plugin_version, ApiVersion, PluginManifest, API_VERSION, API_VERSION_MIN_SUPPORTED, MAX_API_VERSION_ENV, }; pub use ribbon::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, StyleKey, ToolDef}; pub use type_registry::{ - get_embedded_type_registry_json, EnumVariantInfo, FieldInfo, MethodInfo, ParameterInfo, - TypeId, TypeInfo, TypeKind, TypeRegistry, + get_embedded_type_registry_json, EnumVariantInfo, FieldInfo, MethodInfo, ParameterInfo, TypeId, + TypeInfo, TypeKind, TypeRegistry, }; -pub use entity_coverage::{get_embedded_entity_coverage_json, EntityCoverageCatalog, - EntityKindCoverage, EntityScope, ModelAccess, PropertyCoverage}; pub use version_info::get_embedded_version_info_json; #[cfg(test)] diff --git a/crates/ocs_plugin_api/src/process.rs b/crates/ocs_plugin_api/src/process.rs index 760f1353c..117ed330d 100644 --- a/crates/ocs_plugin_api/src/process.rs +++ b/crates/ocs_plugin_api/src/process.rs @@ -28,7 +28,9 @@ use std::time::{Duration, Instant}; use interprocess::local_socket::traits::Listener; use interprocess::local_socket::{GenericNamespaced, ListenerOptions, Stream, ToNsName}; -use crate::host::{CommandSource, CommandStep, ExecutionResult, HostApi, HostNotification, PluginNotification}; +use crate::host::{ + CommandSource, CommandStep, ExecutionResult, HostApi, HostNotification, PluginNotification, +}; use crate::ipc::protocol::{ CadDocument, EntityType, Handle, HostRequest, HostResponse, HostToPlugin, InteractiveEvent, PluginToHost, RunnerHandshake, PLUGIN_TOKEN_ENV, @@ -100,7 +102,11 @@ impl HostApi for NullHost { ) -> Option<&crate::ipc::protocol::ExtendedDataRecord> { None } - fn write_record(&mut self, _handle: Handle, _record: crate::ipc::protocol::ExtendedDataRecord) -> bool { + fn write_record( + &mut self, + _handle: Handle, + _record: crate::ipc::protocol::ExtendedDataRecord, + ) -> bool { false } fn remove_record(&mut self, _handle: Handle, _app_name: &str) -> bool { @@ -112,10 +118,7 @@ impl HostApi for NullHost { fn push_output(&mut self, _msg: &str) {} fn push_error(&mut self, _msg: &str) {} fn start_interactive(&mut self, _command: Box) {} - fn plugin_state_any( - &self, - _plugin_id: &str, - ) -> Option<&(dyn std::any::Any + Send + Sync)> { + fn plugin_state_any(&self, _plugin_id: &str) -> Option<&(dyn std::any::Any + Send + Sync)> { None } fn plugin_state_any_mut( @@ -419,7 +422,11 @@ impl PluginProcess { // the V4 reader thread. The notification handler receives an empty // plugin id until we read the manifest below; that brief window is // fine because the plugin has not yet been asked to do work. - let stream = stream.lock().unwrap_or_else(|e| e.into_inner()).take().expect("V4 stream"); + let stream = stream + .lock() + .unwrap_or_else(|e| e.into_inner()) + .take() + .expect("V4 stream"); let plugin_id_placeholder = Arc::new(std::sync::Mutex::new(String::new())); let handler_for_v4: Arc, PluginNotification) + Send + Sync> = { let plugin_id = Arc::clone(&plugin_id_placeholder); @@ -434,7 +441,9 @@ impl PluginProcess { HostResponse::Manifest(m) => m, other => return Err(PluginError::UnexpectedResponse(Box::new(other))), }; - *plugin_id_placeholder.lock().unwrap_or_else(|e| e.into_inner()) = manifest.id.clone(); + *plugin_id_placeholder + .lock() + .unwrap_or_else(|e| e.into_inner()) = manifest.id.clone(); ribbon = match v4_conn.call(host, HostRequest::GetRibbon, no_op)? { HostResponse::Ribbon(r) => r, other => return Err(PluginError::UnexpectedResponse(Box::new(other))), @@ -626,8 +635,14 @@ impl PluginProcess { /// Release a completed or abandoned V4 interactive command in the runner. pub fn drop_interactive(&self, command_id: u64) -> Result<(), PluginError> { - let Some(v4) = &self.v4 else { return Ok(()); }; - match v4.call(&mut NullHost, HostRequest::DropInteractive { command_id }, &mut |_| {})? { + let Some(v4) = &self.v4 else { + return Ok(()); + }; + match v4.call( + &mut NullHost, + HostRequest::DropInteractive { command_id }, + &mut |_| {}, + )? { HostResponse::Bool(true) => Ok(()), other => Err(PluginError::UnexpectedResponse(Box::new(other))), } @@ -886,10 +901,7 @@ fn format_exit_status(status: Option) -> String { /// Describe why a runner failed before/during connection, including any stderr /// it produced. -fn spawn_failure_status( - child: &Mutex>, - last_stderr: &Mutex, -) -> String { +fn spawn_failure_status(child: &Mutex>, last_stderr: &Mutex) -> String { let mut parts = vec![format_exit_status(child_status(child))]; if let Ok(line) = last_stderr.lock() { if !line.is_empty() { @@ -1259,7 +1271,10 @@ mod timeout_tests { fn push_undo(&mut self, _label: &str) {} fn set_dirty(&mut self) {} fn push_info(&mut self, msg: &str) { - self.push_info_messages.lock().unwrap().push(msg.to_string()); + self.push_info_messages + .lock() + .unwrap() + .push(msg.to_string()); } fn push_output(&mut self, _msg: &str) {} fn push_error(&mut self, _msg: &str) {} @@ -1433,7 +1448,9 @@ mod timeout_tests { ) .expect("send nested request"); let resp = recv::(&mut peer).expect("read nested response"); - assert!(matches!(resp, HostToPlugin::Response(ref r) if matches!(**r, PluginResponse::Ok))); + assert!( + matches!(resp, HostToPlugin::Response(ref r) if matches!(**r, PluginResponse::Ok)) + ); send(&mut peer, &PluginToHost::Response(HostResponse::Bool(true))) .expect("send final response"); }); @@ -1443,7 +1460,11 @@ mod timeout_tests { assert!(result.expect("dispatch succeeds")); assert!(process.is_alive(), "process should still be alive"); let infos = host.take_push_info(); - assert_eq!(infos, vec!["hello".to_string()], "push_info should be delivered to host"); + assert_eq!( + infos, + vec!["hello".to_string()], + "push_info should be delivered to host" + ); // Clean up the helper child so it does not outlive the test. if let Some(mut child) = process @@ -1482,7 +1503,10 @@ mod timeout_tests { RunnerHandshake::Token("correct-token".to_string()), "correct-token", ); - assert!(result.is_ok(), "expected authentication success, got {result:?}"); + assert!( + result.is_ok(), + "expected authentication success, got {result:?}" + ); assert!(!result.unwrap(), "V3 token should select V3 path"); } @@ -1497,7 +1521,10 @@ mod timeout_tests { }, "correct-token", ); - assert!(result.is_ok(), "expected authentication success, got {result:?}"); + assert!( + result.is_ok(), + "expected authentication success, got {result:?}" + ); assert!(result.unwrap(), "TokenV4 should select V4 path"); } diff --git a/crates/ocs_plugin_api/src/process/manager.rs b/crates/ocs_plugin_api/src/process/manager.rs index 90ac264d7..8d1578c9f 100644 --- a/crates/ocs_plugin_api/src/process/manager.rs +++ b/crates/ocs_plugin_api/src/process/manager.rs @@ -97,11 +97,7 @@ impl PluginManager { /// Fan out a host notification to every alive V4 plugin. Per-process errors /// are logged; this is best-effort. - pub fn broadcast_notification( - &self, - command_id: Option, - notification: HostNotification, - ) { + pub fn broadcast_notification(&self, command_id: Option, notification: HostNotification) { for p in &self.plugins { if !p.process.is_alive() { continue; diff --git a/crates/ocs_plugin_api/src/process/v4.rs b/crates/ocs_plugin_api/src/process/v4.rs index f7e46d02e..868bd2288 100644 --- a/crates/ocs_plugin_api/src/process/v4.rs +++ b/crates/ocs_plugin_api/src/process/v4.rs @@ -145,12 +145,7 @@ impl V4Connection { payload, }) => { if tab_id.is_none_or(|request_tab| request_tab == host.tab_id()) { - self.respond_to_plugin_request( - host, - rid, - payload, - on_start_interactive, - )?; + self.respond_to_plugin_request(host, rid, payload, on_start_interactive)?; } else { self.deferred .lock() @@ -167,9 +162,7 @@ impl V4Connection { } Err(mpsc::RecvTimeoutError::Disconnected) => { self.shared.alive.store(false, Ordering::SeqCst); - return Err(PluginError::Runner( - "V4 reader thread disconnected".into(), - )); + return Err(PluginError::Runner("V4 reader thread disconnected".into())); } } } @@ -217,12 +210,7 @@ impl V4Connection { payload, }) => { if tab_id.is_none_or(|request_tab| request_tab == current_tab_id) { - self.respond_to_plugin_request( - host, - id, - payload, - on_start_interactive, - )?; + self.respond_to_plugin_request(host, id, payload, on_start_interactive)?; } else { self.deferred .lock() @@ -238,9 +226,7 @@ impl V4Connection { Err(mpsc::TryRecvError::Empty) => return Ok(()), Err(mpsc::TryRecvError::Disconnected) => { self.shared.alive.store(false, Ordering::SeqCst); - return Err(PluginError::Runner( - "V4 reader thread disconnected".into(), - )); + return Err(PluginError::Runner("V4 reader thread disconnected".into())); } } } @@ -324,7 +310,11 @@ impl V4Connection { while self.shared.alive.load(Ordering::SeqCst) && Instant::now() < deadline { std::thread::sleep(Duration::from_millis(10)); } - let _ = self.reader_handle.lock().unwrap_or_else(|e| e.into_inner()).take(); + let _ = self + .reader_handle + .lock() + .unwrap_or_else(|e| e.into_inner()) + .take(); } } @@ -442,20 +432,23 @@ mod tests { label: &str, entities: Vec, ) -> Result<(), String> { - self.transactions.lock().unwrap().push((label.to_owned(), entities)); + self.transactions + .lock() + .unwrap() + .push((label.to_owned(), entities)); Ok(()) } fn set_dirty(&mut self) {} fn push_info(&mut self, msg: &str) { - self.push_info_messages.lock().unwrap().push(msg.to_string()); + self.push_info_messages + .lock() + .unwrap() + .push(msg.to_string()); } fn push_output(&mut self, _msg: &str) {} fn push_error(&mut self, _msg: &str) {} fn start_interactive(&mut self, _command: Box) {} - fn plugin_state_any( - &self, - _plugin_id: &str, - ) -> Option<&(dyn std::any::Any + Send + Sync)> { + fn plugin_state_any(&self, _plugin_id: &str) -> Option<&(dyn std::any::Any + Send + Sync)> { None } fn plugin_state_any_mut( @@ -537,7 +530,10 @@ mod tests { // Read the host's V4 request and respond. let req = recv::(&mut runner_stream).unwrap(); match req { - HostToPluginV4::Request { id, payload: HostRequest::Dispatch { cmd } } => { + HostToPluginV4::Request { + id, + payload: HostRequest::Dispatch { cmd }, + } => { assert_eq!(cmd, "HELLO"); send( &mut runner_stream, @@ -554,7 +550,13 @@ mod tests { let mut host = DummyHost::new(); let resp = conn - .call(&mut host, HostRequest::Dispatch { cmd: "HELLO".to_string() }, &mut |_| {}) + .call( + &mut host, + HostRequest::Dispatch { + cmd: "HELLO".to_string(), + }, + &mut |_| {}, + ) .unwrap(); assert!(matches!(resp, HostResponse::Bool(true))); runner.join().unwrap(); @@ -573,7 +575,10 @@ mod tests { let runner = thread::spawn(move || { let req = recv::(&mut runner_stream).unwrap(); match req { - HostToPluginV4::Request { id, payload: HostRequest::Dispatch { .. } } => { + HostToPluginV4::Request { + id, + payload: HostRequest::Dispatch { .. }, + } => { // Send a nested plugin request. send( &mut runner_stream, @@ -587,8 +592,10 @@ mod tests { // Read the response to the nested request. let resp = recv::(&mut runner_stream).unwrap(); match resp { - HostToPluginV4::Response { id: rid, payload: PluginResponse::Ok } - if rid == 99 => {} + HostToPluginV4::Response { + id: rid, + payload: PluginResponse::Ok, + } if rid == 99 => {} other => panic!("unexpected nested response: {other:?}"), } // Now respond to the original dispatch. @@ -607,11 +614,21 @@ mod tests { let mut host = DummyHost::new(); let resp = conn - .call(&mut host, HostRequest::Dispatch { cmd: "NESTED".to_string() }, &mut |_| {}) + .call( + &mut host, + HostRequest::Dispatch { + cmd: "NESTED".to_string(), + }, + &mut |_| {}, + ) .unwrap(); assert!(matches!(resp, HostResponse::Bool(true))); let infos = host.take_push_info(); - assert_eq!(infos, vec!["nested".to_string()], "push_info should be delivered to host"); + assert_eq!( + infos, + vec!["nested".to_string()], + "push_info should be delivered to host" + ); runner.join().unwrap(); restore_test_env(); } @@ -632,29 +649,56 @@ mod tests { let runner = thread::spawn(move || { let req = recv::(&mut runner_stream).unwrap(); - let HostToPluginV4::Request { id, payload: HostRequest::Dispatch { .. } } = req else { + let HostToPluginV4::Request { + id, + payload: HostRequest::Dispatch { .. }, + } = req + else { panic!("unexpected host request: {req:?}"); }; - send(&mut runner_stream, &PluginToHostV4::Request { - id: 99, - tab_id: Some(0), - payload: PluginRequest::UpdateEntitiesTransaction { - label: "Move point".into(), - entities: vec![sent], + send( + &mut runner_stream, + &PluginToHostV4::Request { + id: 99, + tab_id: Some(0), + payload: PluginRequest::UpdateEntitiesTransaction { + label: "Move point".into(), + entities: vec![sent], + }, }, - }).unwrap(); - assert!(matches!(recv::(&mut runner_stream).unwrap(), - HostToPluginV4::Response { id: 99, payload: PluginResponse::EntityTransactionResult(Ok(())) })); - send(&mut runner_stream, &PluginToHostV4::Response { - id, - payload: HostResponse::Bool(true), - }).unwrap(); + ) + .unwrap(); + assert!(matches!( + recv::(&mut runner_stream).unwrap(), + HostToPluginV4::Response { + id: 99, + payload: PluginResponse::EntityTransactionResult(Ok(())) + } + )); + send( + &mut runner_stream, + &PluginToHostV4::Response { + id, + payload: HostResponse::Bool(true), + }, + ) + .unwrap(); }); let mut host = DummyHost::new(); - assert!(matches!(conn.call(&mut host, HostRequest::Dispatch { cmd: "MOVE".into() }, &mut |_| {}).unwrap(), - HostResponse::Bool(true))); - assert_eq!(host.take_transactions(), vec![("Move point".into(), vec![expected])]); + assert!(matches!( + conn.call( + &mut host, + HostRequest::Dispatch { cmd: "MOVE".into() }, + &mut |_| {} + ) + .unwrap(), + HostResponse::Bool(true) + )); + assert_eq!( + host.take_transactions(), + vec![("Move point".into(), vec![expected])] + ); runner.join().unwrap(); restore_test_env(); } @@ -735,7 +779,11 @@ mod tests { std::thread::sleep(Duration::from_millis(200)); let got = received.lock().unwrap().clone(); - assert_eq!(got.len(), 1, "second notification should survive handler panic"); + assert_eq!( + got.len(), + 1, + "second notification should survive handler panic" + ); assert_eq!(got[0].0, Some(1)); runner.join().unwrap(); drop(conn); @@ -783,7 +831,10 @@ mod tests { count <= 3, "rate limiter should drop most notifications, got {count}" ); - assert!(conn.is_alive(), "connection should survive rate-limited flood"); + assert!( + conn.is_alive(), + "connection should survive rate-limited flood" + ); runner.join().unwrap(); drop(conn); std::env::remove_var("OCS_PLUGIN_NOTIFY_RATE_LIMIT"); @@ -816,7 +867,10 @@ mod tests { assert_eq!(command_id, 1); assert_eq!(source, CommandSource::Editor); assert_eq!(code, "1+1"); - assert_eq!(tab_index, 0, "ExecuteCode should be tied to the host's tab index"); + assert_eq!( + tab_index, 0, + "ExecuteCode should be tied to the host's tab index" + ); thread::sleep(Duration::from_millis(100)); send( &mut runner_stream, diff --git a/crates/ocs_plugin_api/src/ribbon/owned.rs b/crates/ocs_plugin_api/src/ribbon/owned.rs index 239d94026..4e055a7ce 100644 --- a/crates/ocs_plugin_api/src/ribbon/owned.rs +++ b/crates/ocs_plugin_api/src/ribbon/owned.rs @@ -110,7 +110,9 @@ fn collect_item_command_ids(item: &OwnedRibbonItem, out: &mut Vec) { } } OwnedRibbonItem::PropertiesGroup { match_prop } => push_tool_command(match_prop, out), - OwnedRibbonItem::StyleComboGroup { rows, manager_cmd, .. } => { + OwnedRibbonItem::StyleComboGroup { + rows, manager_cmd, .. + } => { for t in rows.iter().flatten() { push_tool_command(t, out); } @@ -214,17 +216,22 @@ impl From for OwnedRibbonItem { .collect(), default: default.to_string(), }, - RibbonItem::LabeledDropdown { id, label, icon, items, default } => { - OwnedRibbonItem::LabeledDropdown { - id: id.to_string(), - label: label.to_string(), - icon: icon.into(), - items: items.into_iter().map(|(a, b, i)| { - (a.to_string(), b.to_string(), i.into()) - }).collect(), - default: default.to_string(), - } - } + RibbonItem::LabeledDropdown { + id, + label, + icon, + items, + default, + } => OwnedRibbonItem::LabeledDropdown { + id: id.to_string(), + label: label.to_string(), + icon: icon.into(), + items: items + .into_iter() + .map(|(a, b, i)| (a.to_string(), b.to_string(), i.into())) + .collect(), + default: default.to_string(), + }, RibbonItem::LargeDropdown { id, label, @@ -242,9 +249,10 @@ impl From for OwnedRibbonItem { default: default.to_string(), }, RibbonItem::ToolGrid { columns } => OwnedRibbonItem::ToolGrid { - columns: columns.into_iter().map(|column| { - column.into_iter().map(Into::into).collect() - }).collect(), + columns: columns + .into_iter() + .map(|column| column.into_iter().map(Into::into).collect()) + .collect(), }, RibbonItem::LayerComboGroup { row2, row3 } => OwnedRibbonItem::LayerComboGroup { row2: row2.into_iter().map(Into::into).collect(), @@ -297,19 +305,28 @@ impl OwnedRibbonItem { .collect(), default: &*Box::leak(default.into_boxed_str()), }, - OwnedRibbonItem::LabeledDropdown { id, label, icon, items, default } => { - RibbonItem::LabeledDropdown { - id: &*Box::leak(id.into_boxed_str()), - label: &*Box::leak(label.into_boxed_str()), - icon: icon.to_static(), - items: items.into_iter().map(|(a, b, i)| ( - &*Box::leak(a.into_boxed_str()), - &*Box::leak(b.into_boxed_str()), - i.to_static(), - )).collect(), - default: &*Box::leak(default.into_boxed_str()), - } - } + OwnedRibbonItem::LabeledDropdown { + id, + label, + icon, + items, + default, + } => RibbonItem::LabeledDropdown { + id: &*Box::leak(id.into_boxed_str()), + label: &*Box::leak(label.into_boxed_str()), + icon: icon.to_static(), + items: items + .into_iter() + .map(|(a, b, i)| { + ( + &*Box::leak(a.into_boxed_str()), + &*Box::leak(b.into_boxed_str()), + i.to_static(), + ) + }) + .collect(), + default: &*Box::leak(default.into_boxed_str()), + }, OwnedRibbonItem::LargeDropdown { id, label, @@ -333,9 +350,10 @@ impl OwnedRibbonItem { default: &*Box::leak(default.into_boxed_str()), }, OwnedRibbonItem::ToolGrid { columns } => RibbonItem::ToolGrid { - columns: columns.into_iter().map(|column| { - column.into_iter().map(OwnedToolDef::to_static).collect() - }).collect(), + columns: columns + .into_iter() + .map(|column| column.into_iter().map(OwnedToolDef::to_static).collect()) + .collect(), }, OwnedRibbonItem::LayerComboGroup { row2, row3 } => RibbonItem::LayerComboGroup { row2: row2.into_iter().map(|t| t.to_static()).collect(), @@ -382,17 +400,22 @@ impl From<&RibbonItem> for OwnedRibbonItem { .collect(), default: default.to_string(), }, - RibbonItem::LabeledDropdown { id, label, icon, items, default } => { - OwnedRibbonItem::LabeledDropdown { - id: id.to_string(), - label: label.to_string(), - icon: icon.into(), - items: items.iter().map(|(a, b, i)| { - (a.to_string(), b.to_string(), i.into()) - }).collect(), - default: default.to_string(), - } - } + RibbonItem::LabeledDropdown { + id, + label, + icon, + items, + default, + } => OwnedRibbonItem::LabeledDropdown { + id: id.to_string(), + label: label.to_string(), + icon: icon.into(), + items: items + .iter() + .map(|(a, b, i)| (a.to_string(), b.to_string(), i.into())) + .collect(), + default: default.to_string(), + }, RibbonItem::LargeDropdown { id, label, @@ -410,9 +433,10 @@ impl From<&RibbonItem> for OwnedRibbonItem { default: default.to_string(), }, RibbonItem::ToolGrid { columns } => OwnedRibbonItem::ToolGrid { - columns: columns.iter().map(|column| { - column.iter().map(Into::into).collect() - }).collect(), + columns: columns + .iter() + .map(|column| column.iter().map(Into::into).collect()) + .collect(), }, RibbonItem::LayerComboGroup { row2, row3 } => OwnedRibbonItem::LayerComboGroup { row2: row2.iter().map(Into::into).collect(), @@ -622,7 +646,10 @@ mod tests { }; let ids = group.command_ids(); - assert_eq!(ids, ["SHOW_ALL", "SHOW_SELECTED", "PARALLEL", "PERPENDICULAR"]); + assert_eq!( + ids, + ["SHOW_ALL", "SHOW_SELECTED", "PARALLEL", "PERPENDICULAR"] + ); let round_trip: OwnedRibbonGroup = group.to_static().into(); assert_eq!(round_trip.command_ids(), ids); } diff --git a/crates/ocs_plugin_api/src/runner.rs b/crates/ocs_plugin_api/src/runner.rs index 237ddc982..e7b17c184 100644 --- a/crates/ocs_plugin_api/src/runner.rs +++ b/crates/ocs_plugin_api/src/runner.rs @@ -76,10 +76,8 @@ fn invoke_on_load( interactive: &InteractiveRegistry, ) -> Result<(), Box> { let mut proxy = client.plugin_host_api(0, interactive.clone()); - std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - plugin.on_load(&mut proxy) - })) - .map_err(|_| std::io::Error::other("plugin on_load() panicked"))?; + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| plugin.on_load(&mut proxy))) + .map_err(|_| std::io::Error::other("plugin on_load() panicked"))?; Ok(()) } @@ -121,7 +119,9 @@ fn run_v4( match client.recv_runner_frame_timeout(std::time::Duration::from_millis(50)) { Ok(RunnerFrame::Request { id, payload }) => { - if let Some(resp) = handle_host_request_v4(&mut *plugin, interactive, &client, id, payload) { + if let Some(resp) = + handle_host_request_v4(&mut *plugin, interactive, &client, id, payload) + { client.send_response(id, resp)?; } } @@ -283,7 +283,9 @@ fn handle_host_request_v4( HostRequest::GetManifest => { match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| plugin.manifest())) { Ok(m) => Some(HostResponse::Manifest(m.into())), - Err(_) => Some(HostResponse::Error("plugin manifest() panicked".to_string())), + Err(_) => Some(HostResponse::Error( + "plugin manifest() panicked".to_string(), + )), } } HostRequest::GetRibbon => { @@ -348,7 +350,9 @@ fn handle_host_request_v4( } match step { Ok(s) => Some(HostResponse::CommandStep(Box::new(s))), - Err(_) => Some(HostResponse::Error("interactive command panicked".to_string())), + Err(_) => Some(HostResponse::Error( + "interactive command panicked".to_string(), + )), } } HostRequest::DropInteractive { command_id } => { @@ -365,7 +369,9 @@ fn handle_host_request_v4( match result { Some(Ok(s)) => Some(HostResponse::Text(s)), Some(Err(_)) => Some(HostResponse::Error("prompt() panicked".to_string())), - None => Some(HostResponse::Error(format!("unknown interactive command {command_id}"))), + None => Some(HostResponse::Error(format!( + "unknown interactive command {command_id}" + ))), } } HostRequest::NeedsEntityPick { command_id } => { @@ -379,8 +385,12 @@ fn handle_host_request_v4( }; match result { Some(Ok(b)) => Some(HostResponse::Bool(b)), - Some(Err(_)) => Some(HostResponse::Error("needs_object_pick() panicked".to_string())), - None => Some(HostResponse::Error(format!("unknown interactive command {command_id}"))), + Some(Err(_)) => Some(HostResponse::Error( + "needs_object_pick() panicked".to_string(), + )), + None => Some(HostResponse::Error(format!( + "unknown interactive command {command_id}" + ))), } } HostRequest::CursorMove { command_id, pt } => { @@ -407,13 +417,7 @@ fn handle_host_request_v4( let mut proxy = client.plugin_host_api(tab_index, interactive.clone()); let respond = client.execute_code_responder(id); let started = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - plugin.start_execute_code( - &mut proxy, - command_id, - &code, - source, - respond, - ) + plugin.start_execute_code(&mut proxy, command_id, &code, source, respond) })); match started { Ok(true) => None, @@ -435,7 +439,9 @@ fn handle_host_request_v4( } } -unsafe fn load_plugin(path: &Path) -> Result<(u32, Box), Box> { +unsafe fn load_plugin( + path: &Path, +) -> Result<(u32, Box), Box> { let lib = libloading::Library::new(path)?; let version: libloading::Symbol u32> = lib diff --git a/crates/ocs_plugin_api/src/version_info.rs b/crates/ocs_plugin_api/src/version_info.rs index da14db317..27dceb773 100644 --- a/crates/ocs_plugin_api/src/version_info.rs +++ b/crates/ocs_plugin_api/src/version_info.rs @@ -147,14 +147,19 @@ mod tests { value["ocs_plugin_api_version"].as_str(), Some(env!("CARGO_PKG_VERSION")) ); - assert_eq!(value["api_version"].as_i64(), Some(crate::API_VERSION as i64)); + assert_eq!( + value["api_version"].as_i64(), + Some(crate::API_VERSION as i64) + ); assert_eq!( value["api_version_min_supported"].as_i64(), Some(crate::API_VERSION_MIN_SUPPORTED as i64) ); // acadrust_version should have a patch component (e.g. "0.4.0"). - let opencadcodec = value["acadrust_version"].as_str().expect("acadrust_version string"); + let opencadcodec = value["acadrust_version"] + .as_str() + .expect("acadrust_version string"); assert_eq!(opencadcodec.split('.').count(), 3); } @@ -223,10 +228,10 @@ mod tests { assert!(acadrust_source_hash("").is_none()); assert!(acadrust_source_hash("registry+https://crates.io").is_none()); assert!(acadrust_source_hash("git+https://github.com/foo/bar.git#short").is_none()); - assert!( - acadrust_source_hash("git+https://github.com/foo/bar.git#gggggggggggggggggggggggggggggggggggggggg") - .is_none() - ); + assert!(acadrust_source_hash( + "git+https://github.com/foo/bar.git#gggggggggggggggggggggggggggggggggggggggg" + ) + .is_none()); } #[test] diff --git a/crates/ocs_web_worker/src/lib.rs b/crates/ocs_web_worker/src/lib.rs index ed8c657a6..a2950d9a1 100644 --- a/crates/ocs_web_worker/src/lib.rs +++ b/crates/ocs_web_worker/src/lib.rs @@ -54,26 +54,24 @@ pub fn parse_document( "dwg" => { if recovery_mode { DwgReader::from_stream_with_options( - Cursor::new(Arc::clone(&bytes)), - DwgReadOptions::failsafe(), - ) - .read_with_stats() + Cursor::new(Arc::clone(&bytes)), + DwgReadOptions::failsafe(), + ) + .read_with_stats() } else { - DwgReader::from_stream(Cursor::new(Arc::clone(&bytes))) - .read_with_stats() + DwgReader::from_stream(Cursor::new(Arc::clone(&bytes))).read_with_stats() } } "dxf" => { if recovery_mode { - DxfReader::from_reader(Cursor::new(Arc::clone(&bytes))) - .and_then(|reader| { - reader - .with_configuration(DxfReaderConfiguration { - failsafe: true, - ..DxfReaderConfiguration::default() - }) - .read_with_stats() - }) + DxfReader::from_reader(Cursor::new(Arc::clone(&bytes))).and_then(|reader| { + reader + .with_configuration(DxfReaderConfiguration { + failsafe: true, + ..DxfReaderConfiguration::default() + }) + .read_with_stats() + }) } else { DxfReader::from_reader(Cursor::new(Arc::clone(&bytes))) .and_then(|reader| reader.read_with_stats()) @@ -96,9 +94,7 @@ pub fn parse_document( }; if !outcome.stats.has_usable_drawing_data() { let error = if recovery_mode { - format!( - "initial read failed: {initial_error}; recovery found no usable drawing data" - ) + format!("initial read failed: {initial_error}; recovery found no usable drawing data") } else { "initial read returned no source drawing records".to_string() }; @@ -134,8 +130,8 @@ pub fn parse_document( ); outcome.stats.recovered_errors = outcome.stats.recovered_errors.saturating_add(1); } - let source_sha256 = report_fingerprint_needed(&outcome.stats) - .then(|| sha256_document_bytes(&bytes)); + let source_sha256 = + report_fingerprint_needed(&outcome.stats).then(|| sha256_document_bytes(&bytes)); report_stage.call1(&JsValue::NULL, &JsValue::from_str("serialize document"))?; let encoded = encode_result(Ok(outcome), source_sha256, false, &bytes)?; report_stage.call1(&JsValue::NULL, &JsValue::from_str("copy output"))?; @@ -213,7 +209,7 @@ fn encode_result( recoverable_parse_error, runtime_fields, )) - .map_err(|error| worker_error(error.to_string(), bytes, true))?; + .map_err(|error| worker_error(error.to_string(), bytes, true))?; Ok(Uint8Array::from(encoded.as_slice())) } diff --git a/src/app/alias.rs b/src/app/alias.rs index 73b3156cb..7d11ba72e 100644 --- a/src/app/alias.rs +++ b/src/app/alias.rs @@ -243,23 +243,16 @@ pub(super) fn load_aliases() -> FxHashMap { // Only advance the version marker once the migrated table is // actually stored, so a failed set_item leaves the marker // behind and the merge is retried next launch. - if storage - .set_item(WEB_ALIAS_KEY, &to_pgp(&map)) - .is_ok() - { - let _ = storage.set_item( - WEB_ALIAS_VERSION_KEY, - &DEFAULT_ALIASES_VERSION.to_string(), - ); + if storage.set_item(WEB_ALIAS_KEY, &to_pgp(&map)).is_ok() { + let _ = storage + .set_item(WEB_ALIAS_VERSION_KEY, &DEFAULT_ALIASES_VERSION.to_string()); } } map } None => { - let _ = storage.set_item( - WEB_ALIAS_VERSION_KEY, - &DEFAULT_ALIASES_VERSION.to_string(), - ); + let _ = + storage.set_item(WEB_ALIAS_VERSION_KEY, &DEFAULT_ALIASES_VERSION.to_string()); default_map() } }; @@ -330,12 +323,7 @@ impl OpenCADStudio { .alias_editor_rows .iter() .filter(|(a, c)| !a.trim().is_empty() && !c.trim().is_empty()) - .map(|(a, c)| { - ( - a.trim().to_uppercase(), - c.trim().to_uppercase(), - ) - }) + .map(|(a, c)| (a.trim().to_uppercase(), c.trim().to_uppercase())) .collect(); self.set_command_aliases(map); } @@ -371,9 +359,8 @@ impl OpenCADStudio { ); } self.apply_alias_editor_rows(); - self.command_line.push_info( - crate::tf!("{} alias(es) applied.", self.command_aliases.len()).as_ref(), - ); + self.command_line + .push_info(crate::tf!("{} alias(es) applied.", self.command_aliases.len()).as_ref()); } /// Reset the working rows and live table to the shipped defaults. diff --git a/src/app/annotation_data.rs b/src/app/annotation_data.rs index dec24df2e..9724147ff 100644 --- a/src/app/annotation_data.rs +++ b/src/app/annotation_data.rs @@ -10,9 +10,9 @@ use iced::Task; use super::{Message, ModalKind, OpenCADStudio}; use crate::command::CadCommand; use crate::ui::window::annotation_data::{ - DataExtractionField, DataExtractionState, DataLinkChoice, DataLinkField, - ExtractionBegin, ExtractionPage, ExtractionSource, LinkPathType, LinkRange, - TableInsertField, TableInsertion, TableSource, + DataExtractionField, DataExtractionState, DataLinkChoice, DataLinkField, ExtractionBegin, + ExtractionPage, ExtractionSource, LinkPathType, LinkRange, TableInsertField, TableInsertion, + TableSource, }; #[derive(Clone)] @@ -132,7 +132,9 @@ fn column_index(text: &str) -> Option { break; } any = true; - value = value.checked_mul(26)?.checked_add((byte.to_ascii_uppercase() - b'A' + 1) as usize)?; + value = value + .checked_mul(26)? + .checked_add((byte.to_ascii_uppercase() - b'A' + 1) as usize)?; } any.then_some(value.saturating_sub(1)) } @@ -157,7 +159,8 @@ fn crop_range(rows: Vec>, range: &str) -> Result>, S // row are empty in every row, so clamp the window to the data width. let widest = rows.iter().map(Vec::len).max().unwrap_or(0); let c1 = c1.min(widest.saturating_sub(1)); - Ok(rows.into_iter() + Ok(rows + .into_iter() .skip(r0) .take(r1 - r0 + 1) .map(|row| { @@ -191,10 +194,7 @@ fn workbook_metadata(path: &Path) -> Result<(Vec, Vec), String> fn named_range_target(formula: &str) -> Option<(String, String)> { let formula = formula.trim().trim_start_matches('='); let (sheet, range) = formula.rsplit_once('!')?; - let sheet = sheet - .trim() - .trim_matches('\'') - .replace("''", "'"); + let sheet = sheet.trim().trim_matches('\'').replace("''", "'"); let range = range.replace('$', ""); (!sheet.is_empty() && !range.is_empty()).then_some((sheet, range)) } @@ -205,7 +205,11 @@ pub(crate) fn read_tabular_file( range_kind: LinkRange, range_value: Option<&str>, ) -> Result>, String> { - let ext = path.extension().and_then(|value| value.to_str()).unwrap_or("").to_ascii_lowercase(); + let ext = path + .extension() + .and_then(|value| value.to_str()) + .unwrap_or("") + .to_ascii_lowercase(); let (rows, cell_range) = if ext == "csv" || ext == "txt" { if range_kind == LinkRange::NamedRange { return Err(crate::t!("Named ranges require a spreadsheet file.").into_owned()); @@ -231,8 +235,9 @@ pub(crate) fn read_tabular_file( .find(|(candidate, _)| candidate.eq_ignore_ascii_case(name)) .map(|(_, formula)| formula.clone()) .ok_or_else(|| crate::t!("The named range was not found.").into_owned())?; - named_range_target(&formula) - .ok_or_else(|| crate::t!("The named range does not refer to a worksheet cell range.").into_owned())? + named_range_target(&formula).ok_or_else(|| { + crate::t!("The named range does not refer to a worksheet cell range.").into_owned() + })? } else { let selected_sheet = sheet .filter(|value| !value.trim().is_empty()) @@ -257,7 +262,10 @@ pub(crate) fn read_tabular_file( (!cell_range.is_empty()).then_some(cell_range), ) }; - let rows = match cell_range.as_deref().filter(|value| !value.trim().is_empty()) { + let rows = match cell_range + .as_deref() + .filter(|value| !value.trim().is_empty()) + { Some(value) => crop_range(rows, value)?, None => rows, }; @@ -269,13 +277,19 @@ pub(crate) fn read_tabular_file( } fn table_style_handle(doc: &CadDocument, name: &str) -> Option { - doc.objects.iter().find_map(|(handle, object)| match object { - ObjectType::TableStyle(style) if style.name.eq_ignore_ascii_case(name) => Some(*handle), - _ => None, - }) + doc.objects + .iter() + .find_map(|(handle, object)| match object { + ObjectType::TableStyle(style) if style.name.eq_ignore_ascii_case(name) => Some(*handle), + _ => None, + }) } -fn build_table(rows: &[Vec], style: Option, title: Option<&str>) -> codec::entities::Table { +fn build_table( + rows: &[Vec], + style: Option, + title: Option<&str>, +) -> codec::entities::Table { let columns = rows.iter().map(Vec::len).max().unwrap_or(1).max(1); let title_rows = usize::from(title.is_some_and(|value| !value.trim().is_empty())); let mut table = TableBuilder::new(rows.len().max(1) + title_rows, columns) @@ -336,7 +350,11 @@ fn link_custom_data(sheet: &str, kind: LinkRange, range: Option<&str>) -> Vec Result { +pub(crate) fn data_link_write_path(doc: &CadDocument, handle: Handle) -> Result { let link = data_link(doc, handle) .ok_or_else(|| crate::t!("The data link no longer exists.").into_owned())?; if link.option & 1 == 0 { @@ -381,21 +396,42 @@ pub(crate) fn data_link_write_path( let writable = path .extension() .and_then(|value| value.to_str()) - .is_some_and(|value| value.eq_ignore_ascii_case("csv") || value.eq_ignore_ascii_case("txt")); + .is_some_and(|value| { + value.eq_ignore_ascii_case("csv") || value.eq_ignore_ascii_case("txt") + }); if !writable { - return Err(crate::t!("Writing linked data is supported for CSV and text sources.").into_owned()); + return Err( + crate::t!("Writing linked data is supported for CSV and text sources.").into_owned(), + ); } Ok(path) } fn entity_details(entity: &EntityType) -> String { match entity { - EntityType::Line(value) => format!("({:.3},{:.3},{:.3})-({:.3},{:.3},{:.3})", value.start.x, value.start.y, value.start.z, value.end.x, value.end.y, value.end.z), - EntityType::Circle(value) => format!("C({:.3},{:.3},{:.3}) R={:.3}", value.center.x, value.center.y, value.center.z, value.radius), - EntityType::Arc(value) => format!("C({:.3},{:.3},{:.3}) R={:.3} {:.1}°-{:.1}°", value.center.x, value.center.y, value.center.z, value.radius, value.start_angle.to_degrees(), value.end_angle.to_degrees()), + EntityType::Line(value) => format!( + "({:.3},{:.3},{:.3})-({:.3},{:.3},{:.3})", + value.start.x, value.start.y, value.start.z, value.end.x, value.end.y, value.end.z + ), + EntityType::Circle(value) => format!( + "C({:.3},{:.3},{:.3}) R={:.3}", + value.center.x, value.center.y, value.center.z, value.radius + ), + EntityType::Arc(value) => format!( + "C({:.3},{:.3},{:.3}) R={:.3} {:.1}°-{:.1}°", + value.center.x, + value.center.y, + value.center.z, + value.radius, + value.start_angle.to_degrees(), + value.end_angle.to_degrees() + ), EntityType::Text(value) => value.value.clone(), EntityType::MText(value) => value.value.clone(), - EntityType::Insert(value) => format!("{} @({:.3},{:.3},{:.3})", value.block_name, value.insert_point.x, value.insert_point.y, value.insert_point.z), + EntityType::Insert(value) => format!( + "{} @({:.3},{:.3},{:.3})", + value.block_name, value.insert_point.x, value.insert_point.y, value.insert_point.z + ), EntityType::LwPolyline(value) => format!("{} vertices", value.vertices.len()), EntityType::Polyline(value) => format!("{} vertices", value.vertices.len()), EntityType::Polyline2D(value) => format!("{} vertices", value.vertices.len()), @@ -453,7 +489,9 @@ fn collect_drawing_paths( return Ok(()); } if !path.is_dir() { - return Err(crate::t!("Data source not found: %{path}", path = path.display()).into_owned()); + return Err( + crate::t!("Data source not found: %{path}", path = path.display()).into_owned(), + ); } for entry in std::fs::read_dir(path).map_err(|error| error.to_string())? { let entry = entry.map_err(|error| error.to_string())?; @@ -495,24 +533,45 @@ fn collect_type_counts( } } -fn extraction_preview(state: &DataExtractionState, current: &CadDocument) -> Result>, String> { - let allowed_types = state.objects.iter().filter(|value| value.checked).map(|value| value.name.clone()).collect::>(); - let selected = state.selection_handles.iter().copied().collect::>(); +fn extraction_preview( + state: &DataExtractionState, + current: &CadDocument, +) -> Result>, String> { + let allowed_types = state + .objects + .iter() + .filter(|value| value.checked) + .map(|value| value.name.clone()) + .collect::>(); + let selected = state + .selection_handles + .iter() + .copied() + .collect::>(); let mut records = Vec::new(); match state.source { - ExtractionSource::CurrentDrawing => collect_records(current, None, &allowed_types, &mut records), - ExtractionSource::CurrentSelection => collect_records(current, Some(&selected), &allowed_types, &mut records), + ExtractionSource::CurrentDrawing => { + collect_records(current, None, &allowed_types, &mut records) + } + ExtractionSource::CurrentSelection => { + collect_records(current, Some(&selected), &allowed_types, &mut records) + } ExtractionSource::DrawingsAndFolders => { if state.include_current { collect_records(current, None, &allowed_types, &mut records); } for source in extraction_drawing_paths(state)? { - let doc = crate::io::load_file(&source).map_err(|error| format!("{}: {error}", source.display()))?; + let doc = crate::io::load_file(&source) + .map_err(|error| format!("{}: {error}", source.display()))?; collect_records(&doc, None, &allowed_types, &mut records); } } } - let props = state.properties.iter().filter(|value| value.checked).collect::>(); + let props = state + .properties + .iter() + .filter(|value| value.checked) + .collect::>(); if props.is_empty() { return Err(crate::t!("Select at least one property.").into_owned()); } @@ -520,7 +579,12 @@ fn extraction_preview(state: &DataExtractionState, current: &CadDocument) -> Res if state.show_name { header.push("Name".to_string()); } - header.extend(props.iter().filter(|value| !(state.show_name && value.key == "type")).map(|value| value.name.clone())); + header.extend( + props + .iter() + .filter(|value| !(state.show_name && value.key == "type")) + .map(|value| value.name.clone()), + ); if state.show_count { header.push("Count".to_string()); } @@ -532,7 +596,12 @@ fn extraction_preview(state: &DataExtractionState, current: &CadDocument) -> Res if state.show_name { values.push(record.object_type.clone()); } - values.extend(props.iter().filter(|value| !(state.show_name && value.key == "type")).map(|value| record.value(&value.key))); + values.extend( + props + .iter() + .filter(|value| !(state.show_name && value.key == "type")) + .map(|value| record.value(&value.key)), + ); *grouped.entry(values).or_default() += 1; } for (mut values, count) in grouped { @@ -547,7 +616,12 @@ fn extraction_preview(state: &DataExtractionState, current: &CadDocument) -> Res if state.show_name { values.push(record.object_type.clone()); } - values.extend(props.iter().filter(|value| !(state.show_name && value.key == "type")).map(|value| record.value(&value.key))); + values.extend( + props + .iter() + .filter(|value| !(state.show_name && value.key == "type")) + .map(|value| record.value(&value.key)), + ); if state.show_count { values.push("1".into()); } @@ -570,7 +644,12 @@ fn csv_escape(value: &str) -> String { fn rows_to_csv(rows: &[Vec]) -> String { let mut csv = String::new(); for row in rows { - csv.push_str(&row.iter().map(|value| csv_escape(value)).collect::>().join(",")); + csv.push_str( + &row.iter() + .map(|value| csv_escape(value)) + .collect::>() + .join(","), + ); csv.push('\n'); } csv @@ -580,8 +659,18 @@ fn save_extraction_settings(state: &DataExtractionState) -> Result<(), String> { if state.settings_path.trim().is_empty() { return Ok(()); } - let object_types = state.objects.iter().filter(|value| value.checked).map(|value| value.name.clone()).collect::>(); - let properties = state.properties.iter().filter(|value| value.checked).map(|value| value.key.clone()).collect::>(); + let object_types = state + .objects + .iter() + .filter(|value| value.checked) + .map(|value| value.name.clone()) + .collect::>(); + let properties = state + .properties + .iter() + .filter(|value| value.checked) + .map(|value| value.key.clone()) + .collect::>(); let value = serde_json::json!({ "version": 1, "source": match state.source { @@ -609,36 +698,68 @@ fn save_extraction_settings(state: &DataExtractionState) -> Result<(), String> { fn load_extraction_settings(state: &mut DataExtractionState) -> Result<(), String> { let text = std::fs::read_to_string(&state.settings_path).map_err(|error| error.to_string())?; - let value: serde_json::Value = serde_json::from_str(&text).map_err(|error| error.to_string())?; + let value: serde_json::Value = + serde_json::from_str(&text).map_err(|error| error.to_string())?; state.source = match value["source"].as_str() { Some("currentSelection") => ExtractionSource::CurrentSelection, Some("drawingsAndFolders") => ExtractionSource::DrawingsAndFolders, _ => ExtractionSource::CurrentDrawing, }; - state.include_current = value["includeCurrent"].as_bool().unwrap_or(state.include_current); - state.include_subfolders = value["includeSubfolders"].as_bool().unwrap_or(state.include_subfolders); + state.include_current = value["includeCurrent"] + .as_bool() + .unwrap_or(state.include_current); + state.include_subfolders = value["includeSubfolders"] + .as_bool() + .unwrap_or(state.include_subfolders); if let Some(files) = value["sourceFiles"].as_array() { state.source_files = files .iter() .filter_map(|value| value.as_str().map(str::to_string)) .collect(); } - let object_types = value["objectTypes"].as_array().map(|values| values.iter().filter_map(|value| value.as_str()).collect::>()).unwrap_or_default(); + let object_types = value["objectTypes"] + .as_array() + .map(|values| { + values + .iter() + .filter_map(|value| value.as_str()) + .collect::>() + }) + .unwrap_or_default(); for item in &mut state.objects { item.checked = object_types.is_empty() || object_types.contains(item.name.as_str()); } - let properties = value["properties"].as_array().map(|values| values.iter().filter_map(|value| value.as_str()).collect::>()).unwrap_or_default(); + let properties = value["properties"] + .as_array() + .map(|values| { + values + .iter() + .filter_map(|value| value.as_str()) + .collect::>() + }) + .unwrap_or_default(); for item in &mut state.properties { item.checked = properties.is_empty() || properties.contains(item.key.as_str()); } - state.combine_identical = value["combineIdentical"].as_bool().unwrap_or(state.combine_identical); + state.combine_identical = value["combineIdentical"] + .as_bool() + .unwrap_or(state.combine_identical); state.show_count = value["showCount"].as_bool().unwrap_or(state.show_count); state.show_name = value["showName"].as_bool().unwrap_or(state.show_name); state.output_table = value["outputTable"].as_bool().unwrap_or(state.output_table); state.output_file = value["outputFile"].as_bool().unwrap_or(state.output_file); - state.output_path = value["outputPath"].as_str().unwrap_or(&state.output_path).to_string(); - state.table_style = value["tableStyle"].as_str().unwrap_or(&state.table_style).to_string(); - state.table_title = value["tableTitle"].as_str().unwrap_or(&state.table_title).to_string(); + state.output_path = value["outputPath"] + .as_str() + .unwrap_or(&state.output_path) + .to_string(); + state.table_style = value["tableStyle"] + .as_str() + .unwrap_or(&state.table_style) + .to_string(); + state.table_title = value["tableTitle"] + .as_str() + .unwrap_or(&state.table_title) + .to_string(); Ok(()) } @@ -675,7 +796,14 @@ impl OpenCADStudio { let links = data_link_choices(doc); let selected = selected .and_then(|handle| links.iter().find(|value| value.handle == handle).cloned()) - .or_else(|| self.data_link_manager.selected.as_ref().and_then(|old| links.iter().find(|value| value.handle == old.handle).cloned())) + .or_else(|| { + self.data_link_manager.selected.as_ref().and_then(|old| { + links + .iter() + .find(|value| value.handle == old.handle) + .cloned() + }) + }) .or_else(|| links.first().cloned()); let preview = selected .as_ref() @@ -690,16 +818,32 @@ impl OpenCADStudio { pub(super) fn open_data_extraction(&mut self) { let i = self.active_tab; let doc = &self.tabs[i].scene.document; - let selection_handles = self.tabs[i].scene.selected_entities().into_iter().map(|(handle, _)| handle).collect::>(); + let selection_handles = self.tabs[i] + .scene + .selected_entities() + .into_iter() + .map(|(handle, _)| handle) + .collect::>(); let mut counts = BTreeMap::::new(); for entity in doc.entities() { if !matches!(entity, EntityType::Block(_) | EntityType::BlockEnd(_)) { - *counts.entry(crate::entities::names::dxf_name(entity).to_string()).or_default() += 1; + *counts + .entry(crate::entities::names::dxf_name(entity).to_string()) + .or_default() += 1; } } let mut state = DataExtractionState::default(); state.selection_handles = selection_handles; - state.objects = counts.into_iter().map(|(name, count)| crate::ui::window::annotation_data::ExtractionObject { name, checked: true, count }).collect(); + state.objects = counts + .into_iter() + .map( + |(name, count)| crate::ui::window::annotation_data::ExtractionObject { + name, + checked: true, + count, + }, + ) + .collect(); state.table_styles = table_styles(doc); state.table_style = doc.header.current_table_style_name.clone(); self.data_extraction = state; @@ -743,7 +887,8 @@ impl OpenCADStudio { }; let doc = &self.tabs[i].scene.document; let Some(link) = data_link(doc, choice.handle) else { - self.table_insert.error = crate::t!("The selected data link no longer exists.").into_owned(); + self.table_insert.error = + crate::t!("The selected data link no longer exists.").into_owned(); return Task::none(); }; let rows = match read_link_rows(doc, link) { @@ -753,30 +898,57 @@ impl OpenCADStudio { return Task::none(); } }; - let table = build_table(&rows, table_style_handle(doc, &self.table_insert.style), None); - let command = crate::modules::annotate::data_link::DataLinkPlaceCommand::existing(table, choice.handle); + let table = build_table( + &rows, + table_style_handle(doc, &self.table_insert.style), + None, + ); + let command = crate::modules::annotate::data_link::DataLinkPlaceCommand::existing( + table, + choice.handle, + ); self.active_modal = None; self.reset_modal_geometry(); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); return Task::none(); } - let parse_usize = |value: &str| value.trim().parse::().ok().filter(|value| *value > 0); - let parse_number = |value: &str| value.trim().parse::().ok().filter(|value| value.is_finite() && *value > 0.0); + let parse_usize = |value: &str| { + value + .trim() + .parse::() + .ok() + .filter(|value| *value > 0) + }; + let parse_number = |value: &str| { + value + .trim() + .parse::() + .ok() + .filter(|value| value.is_finite() && *value > 0.0) + }; let (Some(columns), Some(data_rows), Some(column_width), Some(row_height)) = ( parse_usize(&self.table_insert.columns), parse_usize(&self.table_insert.data_rows), parse_number(&self.table_insert.column_width), parse_number(&self.table_insert.row_height), ) else { - self.table_insert.error = crate::t!("Columns, rows, width, and height must be positive values.").into_owned(); + self.table_insert.error = + crate::t!("Columns, rows, width, and height must be positive values.").into_owned(); return Task::none(); }; let doc = &self.tabs[i].scene.document; - let style = doc.objects.iter().find_map(|(handle, object)| match object { - ObjectType::TableStyle(style) if style.name.eq_ignore_ascii_case(&self.table_insert.style) => Some((*handle, style)), - _ => None, - }); + let style = doc + .objects + .iter() + .find_map(|(handle, object)| match object { + ObjectType::TableStyle(style) + if style.name.eq_ignore_ascii_case(&self.table_insert.style) => + { + Some((*handle, style)) + } + _ => None, + }); let multiplier = self.tabs[i].scene.creation_annotation_multiplier(); let command = crate::modules::annotate::table_cmd::TableCommand::configured( style, @@ -804,7 +976,12 @@ impl OpenCADStudio { let mut n = 1usize; loop { let name = format!("Data Link {n}"); - if !self.data_link_manager.links.iter().any(|link| link.name.eq_ignore_ascii_case(&name)) { + if !self + .data_link_manager + .links + .iter() + .any(|link| link.name.eq_ignore_ascii_case(&name)) + { self.data_link_manager.name = name; break; } @@ -837,7 +1014,8 @@ impl OpenCADStudio { let Some(choice) = self.data_link_manager.selected.clone() else { return Task::none(); }; - let Some(link) = data_link(&self.tabs[self.active_tab].scene.document, choice.handle) else { + let Some(link) = data_link(&self.tabs[self.active_tab].scene.document, choice.handle) + else { return Task::none(); }; self.data_link_manager.editing = true; @@ -845,13 +1023,22 @@ impl OpenCADStudio { self.data_link_manager.name = link_display_name(link); let path = resolve_link_path(&self.tabs[self.active_tab].scene.document, link); self.data_link_manager.path = path.to_string_lossy().into_owned(); - self.data_link_manager.path_type = match link.path_option { 2 => LinkPathType::Relative, 3 => LinkPathType::FileName, _ => LinkPathType::Full }; + self.data_link_manager.path_type = match link.path_option { + 2 => LinkPathType::Relative, + 3 => LinkPathType::FileName, + _ => LinkPathType::Full, + }; let (sheet, range_kind, range) = link_selection(link); let (sheets, named_ranges) = workbook_metadata(&path).unwrap_or_default(); self.data_link_manager.sheets = sheets; self.data_link_manager.named_ranges = named_ranges; self.data_link_manager.sheet = sheet - .filter(|value| self.data_link_manager.sheets.iter().any(|candidate| candidate == value)) + .filter(|value| { + self.data_link_manager + .sheets + .iter() + .any(|candidate| candidate == value) + }) .or_else(|| self.data_link_manager.sheets.first().cloned()) .unwrap_or_default(); self.data_link_manager.range = range.unwrap_or_default(); @@ -872,8 +1059,12 @@ impl OpenCADStudio { DataLinkField::Range(value) => self.data_link_manager.range = value, DataLinkField::Sheet(value) => self.data_link_manager.sheet = value, DataLinkField::AllowWrite(value) => self.data_link_manager.allow_write = value, - DataLinkField::UseSourceFormatting(value) => self.data_link_manager.use_source_formatting = value, - DataLinkField::UpdateSourceFormatting(value) => self.data_link_manager.update_source_formatting = value, + DataLinkField::UseSourceFormatting(value) => { + self.data_link_manager.use_source_formatting = value + } + DataLinkField::UpdateSourceFormatting(value) => { + self.data_link_manager.update_source_formatting = value + } DataLinkField::InsertTable(value) => self.data_link_manager.insert_table = value, DataLinkField::MoreOptions(value) => self.data_link_manager.more_options = value, } @@ -886,7 +1077,10 @@ impl OpenCADStudio { async { crate::sys::file_dialog() .set_title(crate::t!("Choose a Spreadsheet File").as_ref()) - .add_filter(crate::t!("Spreadsheet Files").as_ref(), &["csv", "txt", "xls", "xlsx", "xlsb", "ods"]) + .add_filter( + crate::t!("Spreadsheet Files").as_ref(), + &["csv", "txt", "xls", "xlsx", "xlsb", "ods"], + ) .add_filter(crate::t!("All Files").as_ref(), &["*"]) .pick_file() .await @@ -937,8 +1131,12 @@ impl OpenCADStudio { self.data_link_manager.status = crate::t!("Enter a data link name.").into_owned(); return Task::none(); } - if self.data_link_manager.links.iter().any(|link| link.name.eq_ignore_ascii_case(&name) && Some(link.handle) != self.data_link_manager.editing_handle) { - self.data_link_manager.status = crate::t!("A data link with that name already exists.").into_owned(); + if self.data_link_manager.links.iter().any(|link| { + link.name.eq_ignore_ascii_case(&name) + && Some(link.handle) != self.data_link_manager.editing_handle + }) { + self.data_link_manager.status = + crate::t!("A data link with that name already exists.").into_owned(); return Task::none(); } let range = (self.data_link_manager.range_kind != LinkRange::EntireSheet) @@ -959,8 +1157,20 @@ impl OpenCADStudio { }; let stored_path = match self.data_link_manager.path_type { LinkPathType::Full => std::fs::canonicalize(&input_path).unwrap_or(input_path.clone()), - LinkPathType::Relative => self.tabs[i].scene.document.source_path.as_deref().map(Path::new).and_then(Path::parent).and_then(|parent| input_path.strip_prefix(parent).ok()).map(Path::to_path_buf).unwrap_or(input_path.clone()), - LinkPathType::FileName => input_path.file_name().map(PathBuf::from).unwrap_or(input_path.clone()), + LinkPathType::Relative => self.tabs[i] + .scene + .document + .source_path + .as_deref() + .map(Path::new) + .and_then(Path::parent) + .and_then(|parent| input_path.strip_prefix(parent).ok()) + .map(Path::to_path_buf) + .unwrap_or(input_path.clone()), + LinkPathType::FileName => input_path + .file_name() + .map(PathBuf::from) + .unwrap_or(input_path.clone()), }; self.push_undo_snapshot(i, "DATALINK"); let handle = if let Some(handle) = self.data_link_manager.editing_handle { @@ -968,9 +1178,14 @@ impl OpenCADStudio { link.description = name; link.tooltip = input_path.to_string_lossy().into_owned(); link.connection_string = stored_path.to_string_lossy().into_owned(); - link.path_option = match self.data_link_manager.path_type { LinkPathType::Full => 1, LinkPathType::Relative => 2, LinkPathType::FileName => 3 }; + link.path_option = match self.data_link_manager.path_type { + LinkPathType::Full => 1, + LinkPathType::Relative => 2, + LinkPathType::FileName => 3, + }; link.option = i32::from(self.data_link_manager.allow_write); - link.flags = i32::from(self.data_link_manager.use_source_formatting) | (i32::from(self.data_link_manager.update_source_formatting) << 1); + link.flags = i32::from(self.data_link_manager.use_source_formatting) + | (i32::from(self.data_link_manager.update_source_formatting) << 1); link.status_flags = 1; link.update_status = "Linked".into(); link.custom_data = link_custom_data( @@ -983,13 +1198,27 @@ impl OpenCADStudio { } else { let handle = self.tabs[i].scene.document.allocate_handle(); let mut object = ClassObject::new(ClassObjectData::DataLink(DataLink { - data_adapter: if input_path.extension().and_then(|value| value.to_str()).is_some_and(|value| value.eq_ignore_ascii_case("csv") || value.eq_ignore_ascii_case("txt")) { "CSV".into() } else { "Spreadsheet".into() }, + data_adapter: if input_path + .extension() + .and_then(|value| value.to_str()) + .is_some_and(|value| { + value.eq_ignore_ascii_case("csv") || value.eq_ignore_ascii_case("txt") + }) { + "CSV".into() + } else { + "Spreadsheet".into() + }, description: name, tooltip: input_path.to_string_lossy().into_owned(), connection_string: stored_path.to_string_lossy().into_owned(), option: i32::from(self.data_link_manager.allow_write), - flags: i32::from(self.data_link_manager.use_source_formatting) | (i32::from(self.data_link_manager.update_source_formatting) << 1), - path_option: match self.data_link_manager.path_type { LinkPathType::Full => 1, LinkPathType::Relative => 2, LinkPathType::FileName => 3 }, + flags: i32::from(self.data_link_manager.use_source_formatting) + | (i32::from(self.data_link_manager.update_source_formatting) << 1), + path_option: match self.data_link_manager.path_type { + LinkPathType::Full => 1, + LinkPathType::Relative => 2, + LinkPathType::FileName => 3, + }, status_flags: 1, update_status: "Linked".into(), custom_data: link_custom_data( @@ -1000,7 +1229,11 @@ impl OpenCADStudio { ..DataLink::default() })); object.handle = handle; - self.tabs[i].scene.document.objects.insert(handle, ObjectType::ClassObject(object)); + self.tabs[i] + .scene + .document + .objects + .insert(handle, ObjectType::ClassObject(object)); handle }; self.tabs[i].dirty = true; @@ -1009,9 +1242,13 @@ impl OpenCADStudio { self.data_link_manager.preview = preview.clone(); self.refresh_data_link_manager(Some(handle)); if self.data_link_manager.insert_table { - let style = table_style_handle(&self.tabs[i].scene.document, &self.tabs[i].scene.document.header.current_table_style_name); + let style = table_style_handle( + &self.tabs[i].scene.document, + &self.tabs[i].scene.document.header.current_table_style_name, + ); let table = build_table(&preview, style, None); - let command = crate::modules::annotate::data_link::DataLinkPlaceCommand::existing(table, handle); + let command = + crate::modules::annotate::data_link::DataLinkPlaceCommand::existing(table, handle); self.active_modal = None; self.reset_modal_geometry(); self.command_line.push_info(&command.prompt()); @@ -1021,7 +1258,12 @@ impl OpenCADStudio { } pub(super) fn on_data_link_delete(&mut self) -> Task { - let Some(handle) = self.data_link_manager.selected.as_ref().map(|value| value.handle) else { + let Some(handle) = self + .data_link_manager + .selected + .as_ref() + .map(|value| value.handle) + else { return Task::none(); }; let i = self.active_tab; @@ -1035,7 +1277,11 @@ impl OpenCADStudio { cell.has_linked_data = false; cell.data_link_rows = 0; cell.data_link_columns = 0; - cell.state.remove(codec::entities::table::CellStateFlags::LINKED | codec::entities::table::CellStateFlags::CONTENT_LOCKED | codec::entities::table::CellStateFlags::FORMAT_LOCKED); + cell.state.remove( + codec::entities::table::CellStateFlags::LINKED + | codec::entities::table::CellStateFlags::CONTENT_LOCKED + | codec::entities::table::CellStateFlags::FORMAT_LOCKED, + ); } } } @@ -1045,7 +1291,8 @@ impl OpenCADStudio { self.tabs[i].dirty = true; self.data_link_manager.selected = None; self.refresh_data_link_manager(None); - self.data_link_manager.status = crate::t!("Data link deleted; existing table values were retained.").into_owned(); + self.data_link_manager.status = + crate::t!("Data link deleted; existing table values were retained.").into_owned(); Task::none() } @@ -1055,7 +1302,9 @@ impl OpenCADStudio { }; let i = self.active_tab; let doc = &self.tabs[i].scene.document; - let Some(link) = data_link(doc, choice.handle) else { return Task::none(); }; + let Some(link) = data_link(doc, choice.handle) else { + return Task::none(); + }; let rows = match read_link_rows(doc, link) { Ok(rows) => rows, Err(error) => { @@ -1064,7 +1313,10 @@ impl OpenCADStudio { } }; let style = table_style_handle(doc, &doc.header.current_table_style_name); - let command = crate::modules::annotate::data_link::DataLinkPlaceCommand::existing(build_table(&rows, style, None), choice.handle); + let command = crate::modules::annotate::data_link::DataLinkPlaceCommand::existing( + build_table(&rows, style, None), + choice.handle, + ); self.active_modal = None; self.reset_modal_geometry(); self.command_line.push_info(&command.prompt()); @@ -1092,12 +1344,32 @@ impl OpenCADStudio { DataExtractionField::Begin(value) => self.data_extraction.begin = value, DataExtractionField::SettingsPath(value) => self.data_extraction.settings_path = value, DataExtractionField::Source(value) => self.data_extraction.source = value, - DataExtractionField::IncludeCurrent(value) => self.data_extraction.include_current = value, - DataExtractionField::IncludeSubfolders(value) => self.data_extraction.include_subfolders = value, - DataExtractionField::Object(index, value) => if let Some(item) = self.data_extraction.objects.get_mut(index) { item.checked = value; }, - DataExtractionField::Property(index, value) => if let Some(item) = self.data_extraction.properties.get_mut(index) { item.checked = value; }, - DataExtractionField::Category(category, value) => if value { self.data_extraction.categories.insert(category); } else { self.data_extraction.categories.remove(&category); }, - DataExtractionField::CombineIdentical(value) => self.data_extraction.combine_identical = value, + DataExtractionField::IncludeCurrent(value) => { + self.data_extraction.include_current = value + } + DataExtractionField::IncludeSubfolders(value) => { + self.data_extraction.include_subfolders = value + } + DataExtractionField::Object(index, value) => { + if let Some(item) = self.data_extraction.objects.get_mut(index) { + item.checked = value; + } + } + DataExtractionField::Property(index, value) => { + if let Some(item) = self.data_extraction.properties.get_mut(index) { + item.checked = value; + } + } + DataExtractionField::Category(category, value) => { + if value { + self.data_extraction.categories.insert(category); + } else { + self.data_extraction.categories.remove(&category); + } + } + DataExtractionField::CombineIdentical(value) => { + self.data_extraction.combine_identical = value + } DataExtractionField::ShowCount(value) => self.data_extraction.show_count = value, DataExtractionField::ShowName(value) => self.data_extraction.show_name = value, DataExtractionField::OutputTable(value) => self.data_extraction.output_table = value, @@ -1146,20 +1418,25 @@ impl OpenCADStudio { .collect::>(); self.data_extraction.objects = counts .into_iter() - .map(|(name, count)| crate::ui::window::annotation_data::ExtractionObject { - checked: previous - .get(&name.to_ascii_lowercase()) - .copied() - .unwrap_or(true), - name, - count, - }) + .map( + |(name, count)| crate::ui::window::annotation_data::ExtractionObject { + checked: previous + .get(&name.to_ascii_lowercase()) + .copied() + .unwrap_or(true), + name, + count, + }, + ) .collect(); Ok(()) } fn update_extraction_preview(&mut self) { - match extraction_preview(&self.data_extraction, &self.tabs[self.active_tab].scene.document) { + match extraction_preview( + &self.data_extraction, + &self.tabs[self.active_tab].scene.document, + ) { Ok(rows) => { self.data_extraction.preview = rows; self.data_extraction.error.clear(); @@ -1177,7 +1454,9 @@ impl OpenCADStudio { ExtractionPage::Refine => ExtractionPage::Properties, ExtractionPage::Output => ExtractionPage::Refine, ExtractionPage::TableStyle => ExtractionPage::Output, - ExtractionPage::Finish if self.data_extraction.output_table => ExtractionPage::TableStyle, + ExtractionPage::Finish if self.data_extraction.output_table => { + ExtractionPage::TableStyle + } ExtractionPage::Finish => ExtractionPage::Output, }; self.data_extraction.error.clear(); @@ -1200,12 +1479,19 @@ impl OpenCADStudio { self.data_extraction.page = ExtractionPage::Source; } ExtractionPage::Source => { - if self.data_extraction.source == ExtractionSource::CurrentSelection && self.data_extraction.selection_handles.is_empty() { - self.data_extraction.error = crate::t!("No objects were selected when the wizard opened.").into_owned(); + if self.data_extraction.source == ExtractionSource::CurrentSelection + && self.data_extraction.selection_handles.is_empty() + { + self.data_extraction.error = + crate::t!("No objects were selected when the wizard opened.").into_owned(); return Task::none(); } - if self.data_extraction.source == ExtractionSource::DrawingsAndFolders && !self.data_extraction.include_current && self.data_extraction.source_files.is_empty() { - self.data_extraction.error = crate::t!("Add a drawing or include the current drawing.").into_owned(); + if self.data_extraction.source == ExtractionSource::DrawingsAndFolders + && !self.data_extraction.include_current + && self.data_extraction.source_files.is_empty() + { + self.data_extraction.error = + crate::t!("Add a drawing or include the current drawing.").into_owned(); return Task::none(); } if let Err(error) = self.refresh_extraction_objects() { @@ -1213,21 +1499,35 @@ impl OpenCADStudio { return Task::none(); } if self.data_extraction.objects.is_empty() { - self.data_extraction.error = crate::t!("No extractable objects were found in the selected source.").into_owned(); + self.data_extraction.error = + crate::t!("No extractable objects were found in the selected source.") + .into_owned(); return Task::none(); } self.data_extraction.page = ExtractionPage::Objects; } ExtractionPage::Objects => { - if !self.data_extraction.objects.iter().any(|value| value.checked) { - self.data_extraction.error = crate::t!("Select at least one object type.").into_owned(); + if !self + .data_extraction + .objects + .iter() + .any(|value| value.checked) + { + self.data_extraction.error = + crate::t!("Select at least one object type.").into_owned(); return Task::none(); } self.data_extraction.page = ExtractionPage::Properties; } ExtractionPage::Properties => { - if !self.data_extraction.properties.iter().any(|value| value.checked) { - self.data_extraction.error = crate::t!("Select at least one property.").into_owned(); + if !self + .data_extraction + .properties + .iter() + .any(|value| value.checked) + { + self.data_extraction.error = + crate::t!("Select at least one property.").into_owned(); return Task::none(); } self.data_extraction.page = ExtractionPage::Refine; @@ -1241,13 +1541,20 @@ impl OpenCADStudio { } ExtractionPage::Output => { if !self.data_extraction.output_table && !self.data_extraction.output_file { - self.data_extraction.error = crate::t!("Choose at least one output.").into_owned(); + self.data_extraction.error = + crate::t!("Choose at least one output.").into_owned(); return Task::none(); } - if self.data_extraction.output_file && self.data_extraction.output_path.trim().is_empty() { + if self.data_extraction.output_file + && self.data_extraction.output_path.trim().is_empty() + { return self.on_data_extraction_browse_output(); } - self.data_extraction.page = if self.data_extraction.output_table { ExtractionPage::TableStyle } else { ExtractionPage::Finish }; + self.data_extraction.page = if self.data_extraction.output_table { + ExtractionPage::TableStyle + } else { + ExtractionPage::Finish + }; } ExtractionPage::TableStyle => self.data_extraction.page = ExtractionPage::Finish, ExtractionPage::Finish => return self.on_data_extraction_finish(), @@ -1257,17 +1564,33 @@ impl OpenCADStudio { pub(super) fn on_data_extraction_browse_settings(&mut self) -> Task { let save = self.data_extraction.begin == ExtractionBegin::New; - Task::perform(async move { - let dialog = crate::sys::file_dialog().set_title(crate::t!("Data Extraction Settings").as_ref()).add_filter(crate::t!("Data Extraction Settings").as_ref(), &["dxex"]).add_filter(crate::t!("All Files").as_ref(), &["*"]); - if save { - dialog.set_file_name("extraction.dxex").save_file().await.map(|handle| crate::sys::handle_path(&handle)) - } else { - dialog.pick_file().await.map(|handle| crate::sys::handle_path(&handle)) - } - }, Message::DataExtractionBrowseSettingsResult) + Task::perform( + async move { + let dialog = crate::sys::file_dialog() + .set_title(crate::t!("Data Extraction Settings").as_ref()) + .add_filter(crate::t!("Data Extraction Settings").as_ref(), &["dxex"]) + .add_filter(crate::t!("All Files").as_ref(), &["*"]); + if save { + dialog + .set_file_name("extraction.dxex") + .save_file() + .await + .map(|handle| crate::sys::handle_path(&handle)) + } else { + dialog + .pick_file() + .await + .map(|handle| crate::sys::handle_path(&handle)) + } + }, + Message::DataExtractionBrowseSettingsResult, + ) } - pub(super) fn on_data_extraction_browse_settings_result(&mut self, path: Option) -> Task { + pub(super) fn on_data_extraction_browse_settings_result( + &mut self, + path: Option, + ) -> Task { if let Some(path) = path { self.data_extraction.settings_path = path.to_string_lossy().into_owned(); if self.data_extraction.begin != ExtractionBegin::New { @@ -1282,24 +1605,38 @@ impl OpenCADStudio { } pub(super) fn on_data_extraction_add_drawings(&mut self) -> Task { - Task::perform(async { - crate::sys::file_dialog() - .set_title(crate::t!("Add Drawings").as_ref()) - .add_filter(crate::t!("CAD Files").as_ref(), &["dwg", "dxf", "DWG", "DXF"]) - .add_filter(crate::t!("All Files").as_ref(), &["*"]) - .pick_files() - .await - .unwrap_or_default() - .into_iter() - .map(|handle| crate::sys::handle_path(&handle)) - .collect() - }, Message::DataExtractionAddDrawingsResult) + Task::perform( + async { + crate::sys::file_dialog() + .set_title(crate::t!("Add Drawings").as_ref()) + .add_filter( + crate::t!("CAD Files").as_ref(), + &["dwg", "dxf", "DWG", "DXF"], + ) + .add_filter(crate::t!("All Files").as_ref(), &["*"]) + .pick_files() + .await + .unwrap_or_default() + .into_iter() + .map(|handle| crate::sys::handle_path(&handle)) + .collect() + }, + Message::DataExtractionAddDrawingsResult, + ) } - pub(super) fn on_data_extraction_add_drawings_result(&mut self, paths: Vec) -> Task { + pub(super) fn on_data_extraction_add_drawings_result( + &mut self, + paths: Vec, + ) -> Task { for path in paths { let text = path.to_string_lossy().into_owned(); - if !self.data_extraction.source_files.iter().any(|value| value.eq_ignore_ascii_case(&text)) { + if !self + .data_extraction + .source_files + .iter() + .any(|value| value.eq_ignore_ascii_case(&text)) + { self.data_extraction.source_files.push(text); } } @@ -1344,25 +1681,35 @@ impl OpenCADStudio { } pub(super) fn on_data_extraction_browse_output(&mut self) -> Task { - Task::perform(async { - crate::sys::file_dialog() - .set_title(crate::t!("Save Data Extraction").as_ref()) - .set_file_name("extraction.csv") - .add_filter(crate::t!("CSV").as_ref(), &["csv"]) - .add_filter(crate::t!("Tab-separated text").as_ref(), &["txt"]) - .add_filter(crate::t!("All Files").as_ref(), &["*"]) - .save_file() - .await - .map(|handle| crate::sys::handle_path(&handle)) - }, Message::DataExtractionBrowseOutputResult) + Task::perform( + async { + crate::sys::file_dialog() + .set_title(crate::t!("Save Data Extraction").as_ref()) + .set_file_name("extraction.csv") + .add_filter(crate::t!("CSV").as_ref(), &["csv"]) + .add_filter(crate::t!("Tab-separated text").as_ref(), &["txt"]) + .add_filter(crate::t!("All Files").as_ref(), &["*"]) + .save_file() + .await + .map(|handle| crate::sys::handle_path(&handle)) + }, + Message::DataExtractionBrowseOutputResult, + ) } - pub(super) fn on_data_extraction_browse_output_result(&mut self, path: Option) -> Task { + pub(super) fn on_data_extraction_browse_output_result( + &mut self, + path: Option, + ) -> Task { if let Some(path) = path { self.data_extraction.output_path = path.to_string_lossy().into_owned(); self.data_extraction.output_file = true; if self.data_extraction.page == ExtractionPage::Output { - self.data_extraction.page = if self.data_extraction.output_table { ExtractionPage::TableStyle } else { ExtractionPage::Finish }; + self.data_extraction.page = if self.data_extraction.output_table { + ExtractionPage::TableStyle + } else { + ExtractionPage::Finish + }; } } Task::none() @@ -1380,8 +1727,16 @@ impl OpenCADStudio { let rows = self.data_extraction.preview.clone(); if self.data_extraction.output_file { let path = PathBuf::from(self.data_extraction.output_path.trim()); - let text = if path.extension().and_then(|value| value.to_str()).is_some_and(|value| value.eq_ignore_ascii_case("txt")) { - rows.iter().map(|row| row.join("\t")).collect::>().join("\n") + "\n" + let text = if path + .extension() + .and_then(|value| value.to_str()) + .is_some_and(|value| value.eq_ignore_ascii_case("txt")) + { + rows.iter() + .map(|row| row.join("\t")) + .collect::>() + .join("\n") + + "\n" } else { rows_to_csv(&rows) }; @@ -1392,9 +1747,13 @@ impl OpenCADStudio { } let i = self.active_tab; if self.data_extraction.output_table { - let style = table_style_handle(&self.tabs[i].scene.document, &self.data_extraction.table_style); + let style = table_style_handle( + &self.tabs[i].scene.document, + &self.data_extraction.table_style, + ); let table = build_table(&rows, style, Some(&self.data_extraction.table_title)); - let command = crate::modules::annotate::data_link::DataLinkPlaceCommand::unlinked(table); + let command = + crate::modules::annotate::data_link::DataLinkPlaceCommand::unlinked(table); self.active_modal = None; self.reset_modal_geometry(); self.command_line.push_info(&command.prompt()); @@ -1403,7 +1762,8 @@ impl OpenCADStudio { self.active_modal = None; self.reset_modal_geometry(); self.ribbon.deactivate_tool(); - self.command_line.push_output(crate::t!("DATAEXTRACTION: external file created.").as_ref()); + self.command_line + .push_output(crate::t!("DATAEXTRACTION: external file created.").as_ref()); } Task::none() } @@ -1415,10 +1775,7 @@ mod tests { #[test] fn cell_ranges_are_normalized_and_missing_cells_are_blank() { - let rows = vec![ - vec!["a".into(), "b".into(), "c".into()], - vec!["d".into()], - ]; + let rows = vec![vec!["a".into(), "b".into(), "c".into()], vec!["d".into()]]; assert_eq!( crop_range(rows, "C2:A1").unwrap(), vec![ diff --git a/src/app/automation.rs b/src/app/automation.rs index 088493ab7..856a7f9b2 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -16,9 +16,9 @@ #[cfg(not(target_arch = "wasm32"))] use crate::io::line_read::{lines_capped, MAX_LINE_BYTES}; +use std::collections::{BTreeMap, HashSet}; #[cfg(not(target_arch = "wasm32"))] use std::io::Write; -use std::collections::{BTreeMap, HashSet}; use std::path::PathBuf; use serde_json::{json, Value}; @@ -202,9 +202,7 @@ fn serve_socket_with_idle( /// The entities inside block definitions: owned by (or listed in) a block /// record that is not a layout's model or paper space. -fn definition_members( - document: &codec::CadDocument, -) -> std::collections::HashSet { +fn definition_members(document: &codec::CadDocument) -> std::collections::HashSet { let mut layouts: std::collections::HashSet = document .objects .values() @@ -297,7 +295,9 @@ pub(crate) fn entity_json(e: &codec::EntityType, detail: &str) -> Value { map.insert("value".into(), json!(t.value)); map.insert( "text".into(), - json!(crate::entities::text_support::resolve_dxf_special_chars(&t.value)), + json!(crate::entities::text_support::resolve_dxf_special_chars( + &t.value + )), ); map.insert("position".into(), v3(t.insertion_point)); map.insert("height".into(), json!(t.height)); @@ -306,8 +306,7 @@ pub(crate) fn entity_json(e: &codec::EntityType, detail: &str) -> Value { map.insert("value".into(), json!(t.value)); map.insert( "text".into(), - json!(codec::entities::mtext_format::parse_mtext(&t.value, true) - .to_plain_text()), + json!(codec::entities::mtext_format::parse_mtext(&t.value, true).to_plain_text()), ); map.insert("position".into(), v3(t.insertion_point)); map.insert("height".into(), json!(t.height)); @@ -341,13 +340,11 @@ pub(crate) fn entity_json(e: &codec::EntityType, detail: &str) -> Value { .edges .iter() .filter_map(|edge| match edge { - codec::entities::BoundaryEdge::Polyline(polyline) => Some( - json!(polyline - .vertices - .iter() - .map(|v| [v.x, v.y]) - .collect::>()), - ), + codec::entities::BoundaryEdge::Polyline(polyline) => Some(json!(polyline + .vertices + .iter() + .map(|v| [v.x, v.y]) + .collect::>())), _ => None, }) .collect(); @@ -435,7 +432,11 @@ pub(super) fn requested_save_target( let requested_format = match req["target_format"].as_str() { Some(value) if value.eq_ignore_ascii_case("dxf") => Some("dxf".to_string()), Some(value) if value.eq_ignore_ascii_case("dwg") => Some("dwg".to_string()), - Some(value) => return Err(format!("unsupported target format {value:?}; use dwg or dxf")), + Some(value) => { + return Err(format!( + "unsupported target format {value:?}; use dwg or dxf" + )) + } None => None, }; if let (Some(requested), Some(extension)) = (&requested_format, &path_format) { @@ -485,7 +486,9 @@ fn audit_ascii_dxf_references(path: &std::path::Path) -> Result { let text = String::from_utf8_lossy(&bytes).replace("\r\n", "\n"); let lines: Vec<&str> = text.lines().collect(); if lines.len() % 2 != 0 { - return Ok(json!({"ok":false,"malformed_pairs":true,"duplicate_handles":[],"dangling_handles":[]})); + return Ok( + json!({"ok":false,"malformed_pairs":true,"duplicate_handles":[],"dangling_handles":[]}), + ); } let mut declared = HashSet::from(["0".to_string()]); let mut duplicate = std::collections::BTreeSet::new(); @@ -495,7 +498,9 @@ fn audit_ascii_dxf_references(path: &std::path::Path) -> Result { let mut variable = None; for pair in lines.chunks_exact(2) { let Ok(code) = pair[0].trim().parse::() else { - return Ok(json!({"ok":false,"malformed_pairs":true,"duplicate_handles":[],"dangling_handles":[]})); + return Ok( + json!({"ok":false,"malformed_pairs":true,"duplicate_handles":[],"dangling_handles":[]}), + ); }; let value = pair[1].trim().to_ascii_uppercase(); pairs.push((code, value.clone())); @@ -983,23 +988,16 @@ impl OpenCADStudio { Ok(target) => target, Err(error) => return err(format!("save: {error}")), }; - let dropped = crate::io::dropped_on_save_count( - &self.tabs[i].scene.document, - version, - is_dxf, - ); + let dropped = + crate::io::dropped_on_save_count(&self.tabs[i].scene.document, version, is_dxf); if dropped > 0 && req["allow_lossy"].as_bool() != Some(true) { return err(format!( "save: conversion would drop {dropped} unsupported record(s); set allow_lossy=true to acknowledge" )); } #[cfg(not(target_arch = "wasm32"))] - let result = self.save_tab_synchronously_protected_as( - i, - path.clone(), - version, - true, - ); + let result = + self.save_tab_synchronously_protected_as(i, path.clone(), version, true); #[cfg(target_arch = "wasm32")] let result = crate::io::save(&self.tabs[i].scene.document, &path) .map_err(crate::io::SaveFailure::other); @@ -1167,8 +1165,18 @@ impl OpenCADStudio { let offset = req["offset"].as_u64().unwrap_or(0) as usize; const WHERE_OPS: &[&str] = &[ - "eq", "ne", "lt", "lte", "gt", "gte", "contains", - "starts_with", "ends_with", "in", "exists", "not_exists", + "eq", + "ne", + "lt", + "lte", + "gt", + "gte", + "contains", + "starts_with", + "ends_with", + "in", + "exists", + "not_exists", ]; if let Some(filters) = req["where"].as_array() { for filter in filters { @@ -1202,7 +1210,9 @@ impl OpenCADStudio { // Filters address with RFC 6901 pointers, the // same contract as the records op. let properties = serde_json::to_value(e).ok().and_then(|wrapper| { - wrapper.as_object().and_then(|object| object.values().next().cloned()) + wrapper + .as_object() + .and_then(|object| object.values().next().cloned()) }); let mut matches = true; for filter in filters { @@ -1210,7 +1220,9 @@ impl OpenCADStudio { let actual = if path.is_empty() { properties.as_ref() } else { - properties.as_ref().and_then(|properties| properties.pointer(path)) + properties + .as_ref() + .and_then(|properties| properties.pointer(path)) }; match crate::app::record_api::compare( actual, @@ -1230,7 +1242,8 @@ impl OpenCADStudio { } } if let Some(bounds) = bounds { - let (min, max) = crate::scene::convert::tess::entity_bounds_in(&tab.scene.document, e); + let (min, max) = + crate::scene::convert::tess::entity_bounds_in(&tab.scene.document, e); if max[0] < bounds[0] || max[1] < bounds[1] || min[0] > bounds[2] @@ -1312,15 +1325,11 @@ impl OpenCADStudio { .as_str() .map(std::path::Path::new) .or(tab.current_path.as_deref()); - let (target_version, target_is_dxf) = match requested_save_target( - req, - document.version, - source_is_dxf, - path, - ) { - Ok(target) => target, - Err(error) => return err(format!("audit: {error}")), - }; + let (target_version, target_is_dxf) = + match requested_save_target(req, document.version, source_is_dxf, path) { + Ok(target) => target, + Err(error) => return err(format!("audit: {error}")), + }; let declared_layers: HashSet = document .layers @@ -1382,14 +1391,12 @@ impl OpenCADStudio { } } - let dropped = crate::io::dropped_on_save_count( - document, - target_version, - target_is_dxf, - ); + let dropped = crate::io::dropped_on_save_count(document, target_version, target_is_dxf); let mut issues = Vec::new(); if !missing_layers.is_empty() { - issues.push(json!({"severity":"error","code":"undeclared_layer","values":missing_layers})); + issues.push( + json!({"severity":"error","code":"undeclared_layer","values":missing_layers}), + ); } if !missing_blocks.is_empty() { issues.push(json!({"severity":"error","code":"missing_block","values":missing_blocks})); @@ -1401,7 +1408,9 @@ impl OpenCADStudio { issues.push(json!({"severity":"error","code":"non_serializable_record","values":serialization_errors})); } if !non_finite_bounds.is_empty() { - issues.push(json!({"severity":"error","code":"non_finite_bounds","handles":non_finite_bounds})); + issues.push( + json!({"severity":"error","code":"non_finite_bounds","handles":non_finite_bounds}), + ); } if dropped > 0 { issues.push(json!({ @@ -1430,10 +1439,19 @@ impl OpenCADStudio { "details":source_dxf_structure.clone(), })); } - let errors = issues.iter().filter(|issue| issue["severity"] == "error").count(); - let warnings = issues.iter().filter(|issue| issue["severity"] == "warning").count(); + let errors = issues + .iter() + .filter(|issue| issue["severity"] == "error") + .count(); + let warnings = issues + .iter() + .filter(|issue| issue["severity"] == "warning") + .count(); #[cfg(not(target_arch = "wasm32"))] - let source_sha256 = tab.current_path.as_deref().filter(|path| path.is_file()) + let source_sha256 = tab + .current_path + .as_deref() + .filter(|path| path.is_file()) .and_then(|path| sha256_file(path).ok()); #[cfg(target_arch = "wasm32")] let source_sha256: Option = None; @@ -1466,31 +1484,37 @@ impl OpenCADStudio { pub(super) fn save_verified_request(&mut self, req: &Value) -> Result { let i = self.active_tab; let Some(raw_path) = req["path"].as_str() else { - return Err(json!({"ok":false,"status":"failed","code":"path_required","error":"save_verified requires an explicit absolute path"})); + return Err( + json!({"ok":false,"status":"failed","code":"path_required","error":"save_verified requires an explicit absolute path"}), + ); }; let path = std::path::PathBuf::from(raw_path); if !path.is_absolute() { - return Err(json!({"ok":false,"status":"failed","code":"absolute_path_required","error":"save_verified path must be absolute"})); + return Err( + json!({"ok":false,"status":"failed","code":"absolute_path_required","error":"save_verified path must be absolute"}), + ); } let extension = path .extension() .and_then(|value| value.to_str()) .map(str::to_ascii_lowercase); if !matches!(extension.as_deref(), Some("dwg" | "dxf")) { - return Err(json!({"ok":false,"status":"failed","code":"unsupported_format","error":"save_verified path must end in .dwg or .dxf"})); + return Err( + json!({"ok":false,"status":"failed","code":"unsupported_format","error":"save_verified path must end in .dwg or .dxf"}), + ); } if path.exists() && req["overwrite"].as_bool() != Some(true) { - return Err(json!({"ok":false,"status":"failed","code":"destination_exists","error":"destination exists; set overwrite=true to replace it"})); + return Err( + json!({"ok":false,"status":"failed","code":"destination_exists","error":"destination exists; set overwrite=true to replace it"}), + ); } let document = &self.tabs[i].scene.document; - let source_is_dxf = crate::io::source_is_dxf(self.tabs[i].current_path.as_deref(), document); - let (version, is_dxf) = requested_save_target( - req, - document.version, - source_is_dxf, - Some(&path), - ) - .map_err(|error| json!({"ok":false,"status":"failed","code":"invalid_target","error":error}))?; + let source_is_dxf = + crate::io::source_is_dxf(self.tabs[i].current_path.as_deref(), document); + let (version, is_dxf) = + requested_save_target(req, document.version, source_is_dxf, Some(&path)).map_err( + |error| json!({"ok":false,"status":"failed","code":"invalid_target","error":error}), + )?; let audit = self.document_audit(req); if audit["summary"]["errors"].as_u64().unwrap_or(0) > 0 { return Err(json!({ @@ -1518,19 +1542,24 @@ impl OpenCADStudio { let mut baked_view = snapshot.clone(); crate::modules::draw::modify::explode::bake_dimension_blocks(&mut baked_view); let before = document_manifest(&baked_view); - crate::io::save_as_version(&snapshot, &path, version) - .map_err(|error| json!({ + crate::io::save_as_version(&snapshot, &path, version).map_err(|error| { + json!({ "ok":false,"status":"failed","code":"save_failed","error":error, - }))?; - let sha256 = sha256_file(&path).map_err(|error| json!({ - "ok":false,"status":"failed","code":"hash_failed","error":error, - "saved":path, - }))?; + }) + })?; + let sha256 = sha256_file(&path).map_err(|error| { + json!({ + "ok":false,"status":"failed","code":"hash_failed","error":error, + "saved":path, + }) + })?; let dxf_structure = if is_dxf { - Some(audit_ascii_dxf_references(&path).map_err(|error| json!({ - "ok":false,"status":"failed","code":"dxf_audit_failed","error":error, - "saved":path,"sha256":sha256, - }))?) + Some(audit_ascii_dxf_references(&path).map_err(|error| { + json!({ + "ok":false,"status":"failed","code":"dxf_audit_failed","error":error, + "saved":path,"sha256":sha256, + }) + })?) } else { None }; @@ -1545,10 +1574,12 @@ impl OpenCADStudio { "target_version":format!("{version:?}"), })); } - let reopened = crate::io::load_file(&path).map_err(|error| json!({ - "ok":false,"status":"failed","code":"reopen_failed","error":error, - "saved":path,"sha256":sha256, - }))?; + let reopened = crate::io::load_file(&path).map_err(|error| { + json!({ + "ok":false,"status":"failed","code":"reopen_failed","error":error, + "saved":path,"sha256":sha256, + }) + })?; if reopened.version != version { return Err(json!({ "ok":false,"status":"failed","code":"version_mismatch", @@ -1653,7 +1684,8 @@ mod tests { .expect("line") .common_mut() .layer = "NOT_DECLARED".into(); - let audit = app.automation_op(r#"{"op":"audit","target_format":"dwg","target_version":"R14"}"#); + let audit = + app.automation_op(r#"{"op":"audit","target_format":"dwg","target_version":"R14"}"#); assert_eq!(audit["ok"], false, "{audit}"); assert_eq!(audit["issues"][0]["code"], "undeclared_layer", "{audit}"); } @@ -1744,10 +1776,8 @@ mod tests { #[test] fn raw_dxf_audit_detects_dangling_handle_references() { - let path = std::env::temp_dir().join(format!( - "ocs_dangling_handle_{}.dxf", - std::process::id() - )); + let path = + std::env::temp_dir().join(format!("ocs_dangling_handle_{}.dxf", std::process::id())); std::fs::write(&path, " 0\nLINE\n 5\n1\n330\n2\n").unwrap(); let audit = super::audit_ascii_dxf_references(&path).unwrap(); assert_eq!(audit["ok"], false, "{audit}"); @@ -1757,10 +1787,7 @@ mod tests { #[test] fn raw_dxf_audit_marks_binary_input_as_skipped() { - let path = std::env::temp_dir().join(format!( - "ocs_binary_dxf_{}.dxf", - std::process::id() - )); + let path = std::env::temp_dir().join(format!("ocs_binary_dxf_{}.dxf", std::process::id())); std::fs::write(&path, b"AutoCAD Binary DXF\r\n\x1a\0").unwrap(); let audit = super::audit_ascii_dxf_references(&path).unwrap(); assert_eq!(audit["ok"], true, "{audit}"); @@ -1770,10 +1797,8 @@ mod tests { #[test] fn raw_dxf_audit_detects_objects_orphaned_from_the_root_dictionary() { - let path = std::env::temp_dir().join(format!( - "ocs_orphaned_object_{}.dxf", - std::process::id() - )); + let path = + std::env::temp_dir().join(format!("ocs_orphaned_object_{}.dxf", std::process::id())); std::fs::write( &path, "0\nSECTION\n2\nOBJECTS\n0\nDICTIONARY\n5\nC\n330\n0\n0\nDICTIONARY\n5\n23\n330\nC\n0\nENDSEC\n0\nEOF\n", @@ -1863,9 +1888,8 @@ mod tests { let line = app.automation_op(r#"{"op":"run","cmd":"LINE 0,0 200,0"}"#); assert_eq!(line["added"], 1, "{line}"); - let text = app.automation_op( - r#"{"op":"run","cmd":"TEXT 100,135 8 0 OPEN CAD MCP ACCEPTANCE"}"#, - ); + let text = + app.automation_op(r#"{"op":"run","cmd":"TEXT 100,135 8 0 OPEN CAD MCP ACCEPTANCE"}"#); assert_eq!(text["added"], 1, "{text}"); app.tabs[i].scene.selection.borrow_mut().view.vp_size = (1600.0, 800.0); @@ -1963,7 +1987,11 @@ mod tests { _ => None, }) .collect(); - assert_eq!(texts.len(), 1, "expected exactly one Text entity, got {texts:?}"); + assert_eq!( + texts.len(), + 1, + "expected exactly one Text entity, got {texts:?}" + ); texts.into_iter().next().unwrap() } @@ -2108,8 +2136,7 @@ mod tests { .map(|e| format!("\"{}\"", e["handle"].as_str().unwrap())) .collect(); - let path = - std::env::temp_dir().join(format!("ocs_wblock_{}.dxf", std::process::id())); + let path = std::env::temp_dir().join(format!("ocs_wblock_{}.dxf", std::process::id())); let _ = std::fs::remove_file(&path); let p = path.to_string_lossy().replace('\\', "\\\\"); let r = app.automation_op(&format!( @@ -2141,7 +2168,11 @@ mod tests { let mut line = codec::entities::Line::new(); line.start = codec::types::Vector3::new(0.0, 0.0, 0.0); line.end = codec::types::Vector3::new(10.0, 0.0, 0.0); - let handle = app.tabs[i].scene.document.add_entity(codec::EntityType::Line(line)).unwrap(); + let handle = app.tabs[i] + .scene + .document + .add_entity(codec::EntityType::Line(line)) + .unwrap(); let mut br = codec::tables::BlockRecord::new("A_CPT"); br.handle = app.tabs[i].scene.document.allocate_handle(); br.entity_handles = vec![handle]; @@ -2149,7 +2180,8 @@ mod tests { let state = app.automation_op(r#"{"protocol":1,"op":"state"}"#); let doc = state["document_id"].as_u64().unwrap(); - let path = std::env::temp_dir().join(format!("ocs_wblock_block_{}.dxf", std::process::id())); + let path = + std::env::temp_dir().join(format!("ocs_wblock_block_{}.dxf", std::process::id())); let _ = std::fs::remove_file(&path); let p = path.to_string_lossy().replace('\\', "\\\\"); let r = app.automation_op(&format!( @@ -2214,7 +2246,10 @@ mod tests { app.automation_op(r#"{"op":"new"}"#); let state = app.automation_op(r#"{"protocol":1,"op":"state"}"#); let doc = state["document_id"].as_u64().unwrap(); - let p = std::env::temp_dir().join("ocs_plot_bad.pdf").to_string_lossy().replace('\\', "\\\\"); + let p = std::env::temp_dir() + .join("ocs_plot_bad.pdf") + .to_string_lossy() + .replace('\\', "\\\\"); let r = app.automation_op(&format!( r#"{{"protocol":1,"op":"plot","request_id":"plot-2","document_id":{doc},"path":"{p}","layout":"NOPE"}}"# )); @@ -2232,11 +2267,11 @@ mod tests { let mut app = OpenCADStudio::new_for_test(); app.automation_op(r#"{"op":"new"}"#); let i = app.active_tab; - let mtext = codec::MText::with_value( - "\\A1;10.5000", - codec::types::Vector3::new(10.0, 20.0, 0.0), - ); - app.tabs[i].scene.add_entity(codec::EntityType::MText(mtext)); + let mtext = + codec::MText::with_value("\\A1;10.5000", codec::types::Vector3::new(10.0, 20.0, 0.0)); + app.tabs[i] + .scene + .add_entity(codec::EntityType::MText(mtext)); let q = app.automation_op( r#"{"op":"query","type":"MTEXT","detail":"full","fields":["value","text","height","bounds"]}"#, @@ -2277,7 +2312,9 @@ mod tests { } fn count_type(app: &mut OpenCADStudio, kind: &str) -> u64 { - let q = app.automation_op(&format!(r#"{{"op":"query","type":"{kind}","detail":"summary"}}"#)); + let q = app.automation_op(&format!( + r#"{{"op":"query","type":"{kind}","detail":"summary"}}"# + )); q["count"].as_u64().unwrap() } @@ -2396,8 +2433,8 @@ mod tests { let mut app = OpenCADStudio::new_for_test(); app.automation_op(r#"{"op":"new"}"#); app.automation_op(r#"{"op":"run","cmd":"LINE 0,0 10,0"}"#); - let handle = app.automation_op(r#"{"op":"query","type":"Line","detail":"summary"}"#)["entities"][0] - ["handle"] + let handle = app.automation_op(r#"{"op":"query","type":"Line","detail":"summary"}"#) + ["entities"][0]["handle"] .as_str() .unwrap() .to_owned(); @@ -2426,7 +2463,12 @@ mod tests { .as_array() .unwrap() .iter() - .map(|e| (e["start"][0].as_f64().unwrap(), e["start"][1].as_f64().unwrap())) + .map(|e| { + ( + e["start"][0].as_f64().unwrap(), + e["start"][1].as_f64().unwrap(), + ) + }) .collect(); assert!(starts.contains(&(5.0, 0.0))); assert!(starts.contains(&(5.0, 10.0))); @@ -2437,8 +2479,8 @@ mod tests { let mut app = OpenCADStudio::new_for_test(); app.automation_op(r#"{"op":"new"}"#); app.automation_op(r#"{"op":"run","cmd":"LINE 10,0 20,0"}"#); - let handle = app.automation_op(r#"{"op":"query","type":"Line","detail":"summary"}"#)["entities"][0] - ["handle"] + let handle = app.automation_op(r#"{"op":"query","type":"Line","detail":"summary"}"#) + ["entities"][0]["handle"] .as_str() .unwrap() .to_owned(); @@ -2501,8 +2543,8 @@ mod tests { let mut app = OpenCADStudio::new_for_test(); app.automation_op(r#"{"op":"new"}"#); app.automation_op(r#"{"op":"run","cmd":"CIRCLE 0,0 1"}"#); - let handle = app.automation_op(r#"{"op":"query","type":"Circle","detail":"summary"}"#)["entities"][0] - ["handle"] + let handle = app.automation_op(r#"{"op":"query","type":"Circle","detail":"summary"}"#) + ["entities"][0]["handle"] .as_str() .unwrap() .to_owned(); @@ -2536,8 +2578,8 @@ mod tests { let mut app = OpenCADStudio::new_for_test(); app.automation_op(r#"{"op":"new"}"#); app.automation_op(r#"{"op":"run","cmd":"CIRCLE 5,5 2"}"#); - let handle = app.automation_op(r#"{"op":"query","type":"Circle","detail":"summary"}"#)["entities"][0] - ["handle"] + let handle = app.automation_op(r#"{"op":"query","type":"Circle","detail":"summary"}"#) + ["entities"][0]["handle"] .as_str() .unwrap() .to_owned(); @@ -2631,8 +2673,14 @@ mod tests { let path = std::env::temp_dir().join(format!("ocs_block_{}.dxf", std::process::id())); let _ = std::fs::remove_file(&path); let p = path.to_string_lossy().replace('\\', "\\\\"); - assert_eq!(app.automation_op(&format!(r#"{{"op":"save","path":"{p}"}}"#))["ok"], true); - assert_eq!(app.automation_op(&format!(r#"{{"op":"open","path":"{p}"}}"#))["ok"], true); + assert_eq!( + app.automation_op(&format!(r#"{{"op":"save","path":"{p}"}}"#))["ok"], + true + ); + assert_eq!( + app.automation_op(&format!(r#"{{"op":"open","path":"{p}"}}"#))["ok"], + true + ); assert_eq!(count_type(&mut app, "Insert"), 1); drop(app); let sidecar = path.with_file_name(format!( @@ -2648,8 +2696,8 @@ mod tests { let mut app = OpenCADStudio::new_for_test(); app.automation_op(r#"{"op":"new"}"#); app.automation_op(r#"{"op":"run","cmd":"CIRCLE 5,5 2"}"#); - let handle = app.automation_op(r#"{"op":"query","type":"Circle","detail":"summary"}"#)["entities"][0] - ["handle"] + let handle = app.automation_op(r#"{"op":"query","type":"Circle","detail":"summary"}"#) + ["entities"][0]["handle"] .as_str() .unwrap() .to_owned(); @@ -2930,16 +2978,24 @@ mod tests { } { let sel = app.tabs[i].scene.selection.borrow(); - assert!(sel.input.left_dragging, "drag must start (add={add} rect={rect})"); + assert!( + sel.input.left_dragging, + "drag must start (add={add} rect={rect})" + ); if rect { // Rectangle mode drives the box machinery, not the lasso. assert!( - sel.gesture.box_anchor.is_some() && sel.gesture.box_current.is_some() && !sel.gesture.poly_active, + sel.gesture.box_anchor.is_some() + && sel.gesture.box_current.is_some() + && !sel.gesture.poly_active, "rect marquee must arm the box (add={add})" ); } else { assert!(sel.gesture.poly_active, "lasso must start (add={add})"); - assert!(sel.gesture.poly_points.len() >= 3, "lasso points (add={add})"); + assert!( + sel.gesture.poly_points.len() >= 3, + "lasso points (add={add})" + ); } } let _ = app.update(Message::ViewportLeftRelease); @@ -3218,8 +3274,7 @@ mod tests { corrupt.center = codec::types::Vector3::new(1.0, 1.0, 0.0); // An absurd radius is rejected; a zero radius is valid. corrupt.radius = 1.0e11; - doc.add_entity(codec::EntityType::Circle(corrupt)) - .unwrap(); + doc.add_entity(codec::EntityType::Circle(corrupt)).unwrap(); let bytes = crate::io::save_to_bytes(&doc, "dxf", doc.version) .expect("save a document containing a corrupt entity"); std::fs::write(&path, bytes).unwrap(); @@ -3572,12 +3627,7 @@ mod tests { let listen_bound = bound.clone(); std::thread::spawn(move || { let mut app = super::OpenCADStudio::new_for_test(); - super::serve_socket_with_idle( - &mut app, - 0, - &listen_bound, - Duration::from_millis(300), - ); + super::serve_socket_with_idle(&mut app, 0, &listen_bound, Duration::from_millis(300)); }); let mut port = 0; for _ in 0..50 { @@ -3613,7 +3663,9 @@ mod tests { .expect("read timeout"); let mut reader2 = BufReader::new(second.try_clone().expect("clone")); let mut banner2 = String::new(); - reader2.read_line(&mut banner2).expect("second ready banner"); + reader2 + .read_line(&mut banner2) + .expect("second ready banner"); assert!(banner2.contains("ready"), "{banner2}"); } } diff --git a/src/app/command_driver/clipboard.rs b/src/app/command_driver/clipboard.rs index 383c69413..a794d200d 100644 --- a/src/app/command_driver/clipboard.rs +++ b/src/app/command_driver/clipboard.rs @@ -339,9 +339,7 @@ impl OpenCADStudio { } if matches!( self.tabs[i].scene.document.get_entity(handle), - Some( - codec::EntityType::Block(_) | codec::EntityType::BlockEnd(_) - ) + Some(codec::EntityType::Block(_) | codec::EntityType::BlockEnd(_)) ) { needs_geometry_bump = true; } else { @@ -373,9 +371,7 @@ impl OpenCADStudio { self.tabs[i].scene.document.get_entity(handle), Some(codec::EntityType::Leader(_)) ) { - let _ = self.tabs[i] - .scene - .sync_displayed_annotation_context(handle); + let _ = self.tabs[i].scene.sync_displayed_annotation_context(handle); } } // Recreate any group whose whole membership was copied, so a pasted @@ -385,7 +381,9 @@ impl OpenCADStudio { // clipboard clone to its new handle. Same shared `recreate_groups` // the in-drawing COPY path uses. (#440) if !deps.groups.is_empty() { - self.tabs[i].scene.recreate_groups(deps.groups.clone(), &handle_map); + self.tabs[i] + .scene + .recreate_groups(deps.groups.clone(), &handle_map); } // Constraints wholly within the pasted selection follow it. self.tabs[i] @@ -467,7 +465,10 @@ pub(crate) fn merge_table_records(doc: &mut codec::CadDocument, deps: &crate::ap /// document lacks. Shared by `merge_clipboard_blocks` and /// `paste_entities_kernel`. Returns how many definitions were created, so /// render-cache callers can bump once instead of per definition. -pub(crate) fn merge_block_defs(doc: &mut codec::CadDocument, blocks: &[crate::app::BlockDef]) -> usize { +pub(crate) fn merge_block_defs( + doc: &mut codec::CadDocument, + blocks: &[crate::app::BlockDef], +) -> usize { let mut created = 0; for def in blocks { let before = doc.block_records.len(); @@ -500,10 +501,7 @@ pub(crate) fn paste_entities_kernel( deps: &crate::app::ClipboardDeps, translate: Option<&crate::command::EntityTransform>, space: PasteSpace, -) -> ( - Vec, - rustc_hash::FxHashMap, -) { +) -> (Vec, rustc_hash::FxHashMap) { // Shared table-record merge (also used by `merge_dependencies`): recreate // missing records with fresh handles from the target document. merge_table_records(doc, deps); @@ -562,7 +560,11 @@ pub(crate) fn paste_entities_kernel( if let Some(e) = doc.get_entity_mut(new_entity) { e.common_mut().xdictionary_handle = Some(new_root); } - crate::scene::annotative::translate_annotation_contexts(doc, new_entity, annotation_delta); + crate::scene::annotative::translate_annotation_contexts( + doc, + new_entity, + annotation_delta, + ); } } @@ -1015,10 +1017,7 @@ mod clipboard_xdict_clone_tests { let new_root = recreate_ext_subtree(&mut dst, &cap, Some(dst_entity)) .expect("recreated xdictionary root"); assert!(!new_root.is_null()); - assert!( - !before.contains(&new_root), - "clone must use a fresh handle" - ); + assert!(!before.contains(&new_root), "clone must use a fresh handle"); assert_eq!(dst.objects.len(), before.len() + 2); let leaf = leaf_of(&dst, new_root); @@ -1052,8 +1051,7 @@ mod clipboard_xdict_clone_tests { let mut dst = codec::CadDocument::new(); let before_len = dst.objects.len(); - let root = - recreate_ext_subtree(&mut dst, &cap, None).expect("recreated xdictionary root"); + let root = recreate_ext_subtree(&mut dst, &cap, None).expect("recreated xdictionary root"); let leaf = leaf_of(&dst, root); match dst.objects.get(&leaf) { Some(ObjectType::XRecord(x)) => assert!( @@ -1182,9 +1180,7 @@ mod paste_entities_kernel_tests { codec::types::Vector3::new(10.0, 0.0, 0.0), ); seed_line.common.layer = "WALLS".to_string(); - let seed_handle = seed - .add_entity(codec::EntityType::Line(seed_line)) - .unwrap(); + let seed_handle = seed.add_entity(codec::EntityType::Line(seed_line)).unwrap(); let (entities, deps) = copy_to_clipboard_kernel(&seed, &[seed_handle]); let mut dst = codec::CadDocument::new(); @@ -1207,8 +1203,16 @@ mod paste_entities_kernel_tests { Some(codec::EntityType::Line(l)) => l, other => panic!("expected pasted Line, got {other:?}"), }; - assert!((line.start.x - 5.0).abs() < 1e-9, "start.x = {}", line.start.x); - assert!((line.start.y - 7.0).abs() < 1e-9, "start.y = {}", line.start.y); + assert!( + (line.start.x - 5.0).abs() < 1e-9, + "start.x = {}", + line.start.x + ); + assert!( + (line.start.y - 7.0).abs() < 1e-9, + "start.y = {}", + line.start.y + ); assert!((line.end.x - 15.0).abs() < 1e-9, "end.x = {}", line.end.x); // The layer dependency must have been recreated in the target document. assert!(dst.layers.contains("WALLS")); @@ -1223,9 +1227,7 @@ mod paste_entities_kernel_tests { .unwrap(); let mut leader = codec::entities::Leader::default(); leader.annotation_handle = note; - let leader_handle = seed - .add_entity(codec::EntityType::Leader(leader)) - .unwrap(); + let leader_handle = seed.add_entity(codec::EntityType::Leader(leader)).unwrap(); let (entities, deps) = copy_to_clipboard_kernel(&seed, &[leader_handle, note]); let mut dst = codec::CadDocument::new(); @@ -1264,10 +1266,8 @@ mod paste_entities_kernel_tests { let mut xrec = XRecord::new(); xrec.handle = src_leaf; xrec.owner = src_root; - xrec.entries.push(XRecordEntry::new( - 340, - XRecordValue::Handle(seed_entity), - )); + xrec.entries + .push(XRecordEntry::new(340, XRecordValue::Handle(seed_entity))); seed.objects.insert(src_root, ObjectType::Dictionary(dict)); seed.objects.insert(src_leaf, ObjectType::XRecord(xrec)); if let Some(e) = seed.get_entity_mut(seed_entity) { @@ -1323,7 +1323,7 @@ mod headless_reuse_proof_tests { // (`copy_to_clipboard_kernel`, `paste_entities_kernel`, // `match_layer_kernel`, `match_properties_kernel` + `MatchOpts`). use super::{copy_to_clipboard_kernel, paste_entities_kernel}; - use crate::entities::match_props::{MatchOpts, match_layer_kernel, match_properties_kernel}; + use crate::entities::match_props::{match_layer_kernel, match_properties_kernel, MatchOpts}; #[test] fn headless_copy_paste_then_match_kernels_share_one_path() { @@ -1347,8 +1347,7 @@ mod headless_reuse_proof_tests { assert_eq!(entities[0].common().layer, "WALLS"); let mut doc_b = codec::CadDocument::new(); - let translate = - crate::command::EntityTransform::Translate(glam::DVec3::new(5.0, 7.0, 0.0)); + let translate = crate::command::EntityTransform::Translate(glam::DVec3::new(5.0, 7.0, 0.0)); let (handles, handle_map) = paste_entities_kernel( &mut doc_b, &entities, @@ -1378,16 +1377,12 @@ mod headless_reuse_proof_tests { src_line.common.layer = "WALLS".to_string(); src_line.common.linetype_scale = 2.5; src_line.thickness = 1.25; - let src = doc_b - .add_entity(codec::EntityType::Line(src_line)) - .unwrap(); + let src = doc_b.add_entity(codec::EntityType::Line(src_line)).unwrap(); let mut dst_line = codec::entities::Line::new(); dst_line.common.layer = "0".to_string(); dst_line.common.linetype_scale = 1.0; dst_line.thickness = 0.0; - let dst = doc_b - .add_entity(codec::EntityType::Line(dst_line)) - .unwrap(); + let dst = doc_b.add_entity(codec::EntityType::Line(dst_line)).unwrap(); let count = match_layer_kernel(&mut doc_b, &[dst], src).expect("match layer"); assert_eq!(count, 1); @@ -1423,8 +1418,7 @@ mod headless_reuse_proof_tests { .add_entity(codec::EntityType::Line(line)) .expect("source line"); - let (entities, deps) = - copy_to_clipboard_kernel(&source, &[source_handle]); + let (entities, deps) = copy_to_clipboard_kernel(&source, &[source_handle]); let mut target = codec::CadDocument::new(); @@ -1448,9 +1442,7 @@ mod headless_reuse_proof_tests { let pasted = handles[0]; assert!(!pasted.is_null()); - let entity = target - .get_entity(pasted) - .expect("pasted entity must exist"); + let entity = target.get_entity(pasted).expect("pasted entity must exist"); assert_eq!( entity.common().owner_handle, @@ -1468,5 +1460,4 @@ mod headless_reuse_proof_tests { "pasted entity must be registered as a member of the edited block" ); } - } diff --git a/src/app/command_driver/constraint.rs b/src/app/command_driver/constraint.rs index 7a567540d..bc16e2b1e 100644 --- a/src/app/command_driver/constraint.rs +++ b/src/app/command_driver/constraint.rs @@ -11,7 +11,10 @@ impl OpenCADStudio { self.active_modal = Some(super::super::ModalKind::AutoConstrainSettings); } - pub(super) fn handle_add_parametric_constraint(&mut self, result: CmdResult) -> Option> { + pub(super) fn handle_add_parametric_constraint( + &mut self, + result: CmdResult, + ) -> Option> { let i = self.active_tab; let CmdResult::AddParametricConstraint { kind, @@ -28,19 +31,21 @@ impl OpenCADStudio { (kind == ConstraintKind::Smooth && command.name() == "GCSMOOTH") || (kind == ConstraintKind::Fixed && command.name() == "FXCONSTRAINT") }); - if let Err(message) = self.tabs[i].scene.validate_parametric_constraint( - kind, - &refs, - driving_param.as_ref(), - ) { + if let Err(message) = + self.tabs[i] + .scene + .validate_parametric_constraint(kind, &refs, driving_param.as_ref()) + { if !keep_command { self.tabs[i].active_cmd = None; } self.tabs[i].snap_result = None; self.command_line.push_error(message); if keep_command { - if let Some(prompt) = - self.tabs[i].active_cmd.as_ref().map(|command| command.prompt()) + if let Some(prompt) = self.tabs[i] + .active_cmd + .as_ref() + .map(|command| command.prompt()) { self.command_line.push_info(&prompt); } @@ -77,11 +82,11 @@ impl OpenCADStudio { .scene .record_undo_parametric_constraints_before(scope, constraints_before); let retain_size = self.constraint_solve_mode && driving_param.is_none(); - let id = self.tabs[i].scene.parametric_constraint_set_mut(scope).add( - kind, - refs, - driving_param, - ); + let id = + self.tabs[i] + .scene + .parametric_constraint_set_mut(scope) + .add(kind, refs, driving_param); self.tabs[i].scene.note_parametric_constraint_applied( scope, id, @@ -91,11 +96,9 @@ impl OpenCADStudio { .into_iter() .map(|h| (h, crate::scene::ChangeKind::Modified)) .collect(); - self.tabs[i].scene.bump_entities_with_parametric_policy( - &changes, - &[], - retain_size, - ); + self.tabs[i] + .scene + .bump_entities_with_parametric_policy(&changes, &[], retain_size); self.tabs[i].dirty = true; self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; @@ -107,7 +110,10 @@ impl OpenCADStudio { None } - pub(super) fn handle_add_equal_constraint(&mut self, result: CmdResult) -> Option> { + pub(super) fn handle_add_equal_constraint( + &mut self, + result: CmdResult, + ) -> Option> { let i = self.active_tab; let CmdResult::AddEqualConstraint { first, @@ -130,13 +136,11 @@ impl OpenCADStudio { for other in others { let refs = [first, other]; if other == first - || self - .tabs[i] + || self.tabs[i] .scene .validate_parametric_constraint(ConstraintKind::Equal, &refs, None) .is_err() - || equal_size_follower(&self.tabs[i].scene.document, first, other) - .is_none() + || equal_size_follower(&self.tabs[i].scene.document, first, other).is_none() { // The reference names the kind the first object // asks for. @@ -168,8 +172,10 @@ impl OpenCADStudio { // reference; Multiple keeps asking anyway. let keep = (multiple && !finishing) || (!multiple && followers.is_empty()); if keep { - if let Some(prompt) = - self.tabs[i].active_cmd.as_ref().map(|command| command.prompt()) + if let Some(prompt) = self.tabs[i] + .active_cmd + .as_ref() + .map(|command| command.prompt()) { self.command_line.push_info(&prompt); } @@ -214,10 +220,11 @@ impl OpenCADStudio { { self.tabs[i].scene.update_entity(resized); } - let id = self.tabs[i] - .scene - .parametric_constraint_set_mut(scope) - .add(ConstraintKind::Equal, vec![first, *follower], None); + let id = self.tabs[i].scene.parametric_constraint_set_mut(scope).add( + ConstraintKind::Equal, + vec![first, *follower], + None, + ); self.tabs[i].scene.note_parametric_constraint_applied( scope, id, @@ -252,12 +259,9 @@ impl OpenCADStudio { let scope = self.tabs[i].current_parametric_scope(); let resolved = { let document = &self.tabs[i].scene.document; - let world = - codec::types::Vector3::new(pick.point.x, pick.point.y, pick.point.z); + let world = codec::types::Vector3::new(pick.point.x, pick.point.y, pick.point.z); let reference = match pick.handle { - Some(handle) => { - nearest_parametric_point_on_entity(document, scope, handle, world) - } + Some(handle) => nearest_parametric_point_on_entity(document, scope, handle, world), None => nearest_parametric_point(document, scope, world, None), }; reference.map(|reference| { @@ -284,7 +288,10 @@ impl OpenCADStudio { } } - pub(super) fn handle_add_dimensional_constraint(&mut self, result: CmdResult) -> Option> { + pub(super) fn handle_add_dimensional_constraint( + &mut self, + result: CmdResult, + ) -> Option> { let i = self.active_tab; let CmdResult::AddDimensionalConstraint { kind, @@ -395,12 +402,7 @@ impl OpenCADStudio { None, ); let mut entity = if kind == ConstraintKind::Distance { - aligned_dim::aligned_dimension_entity( - first_point, - second_point, - location, - Some(text), - ) + aligned_dim::aligned_dimension_entity(first_point, second_point, location, Some(text)) } else { linear_dim::linear_dimension_entity( first_point, @@ -456,8 +458,7 @@ impl OpenCADStudio { let id = set.add(kind, refs, Some(DrivingValue::Named(name))); if direction.is_some() { if let Some(constraint) = set.constraints.iter_mut().find(|c| c.id == id) { - constraint.distance_direction_type = - distance_direction_type::PERPENDICULAR_TO_LINE; + constraint.distance_direction_type = distance_direction_type::PERPENDICULAR_TO_LINE; } } set.dimensions.insert(id, dimension); @@ -466,10 +467,9 @@ impl OpenCADStudio { id, self.constraint_bar_display, ); - self.tabs[i].scene.attach_dimension_association( - dimension, - vec![Some(first.entity), Some(second.entity)], - ); + self.tabs[i] + .scene + .attach_dimension_association(dimension, vec![Some(first.entity), Some(second.entity)]); let changes: Vec<(Handle, crate::scene::ChangeKind)> = touched .into_iter() .map(|handle| (handle, crate::scene::ChangeKind::Modified)) @@ -494,7 +494,10 @@ impl OpenCADStudio { None } - pub(super) fn handle_add_radial_constraint(&mut self, result: CmdResult) -> Option> { + pub(super) fn handle_add_radial_constraint( + &mut self, + result: CmdResult, + ) -> Option> { let i = self.active_tab; let CmdResult::AddRadialConstraint { circle, @@ -530,10 +533,7 @@ impl OpenCADStudio { .is_some_and(|set| { set.constraints.iter().any(|c| { c.enabled - && matches!( - c.kind, - ConstraintKind::Radius | ConstraintKind::Diameter - ) + && matches!(c.kind, ConstraintKind::Radius | ConstraintKind::Diameter) && c.refs.iter().any(|r| r.entity == circle.entity) }) }); @@ -672,7 +672,10 @@ impl OpenCADStudio { None } - pub(super) fn handle_add_angular_constraint(&mut self, result: CmdResult) -> Option> { + pub(super) fn handle_add_angular_constraint( + &mut self, + result: CmdResult, + ) -> Option> { let i = self.active_tab; let CmdResult::AddAngularConstraint { refs, @@ -771,11 +774,20 @@ impl OpenCADStudio { ); let entity = if kind == ConstraintKind::Angle { angular_dim::angular_two_line_entity( - points[0], points[1], points[2], points[3], location, Some(text), + points[0], + points[1], + points[2], + points[3], + location, + Some(text), ) } else { angular_dim::angular_three_point_entity( - points[0], points[1], points[2], location, Some(text), + points[0], + points[1], + points[2], + location, + Some(text), ) }; let Some(mut entity) = entity else { @@ -927,9 +939,7 @@ impl OpenCADStudio { .parametric_constraint_set(scope) .cloned() .unwrap_or_else(|| { - crate::scene::parametric_constraints::ParametricConstraintSet::new( - scope, - ) + crate::scene::parametric_constraints::ParametricConstraintSet::new(scope) }); let pending = self.begin_undo(i, "Parallel constraint", 1, true); if let Some(entity) = placed_entity { @@ -999,13 +1009,10 @@ impl OpenCADStudio { let scope = self.tabs[i].current_parametric_scope(); let document = &self.tabs[i].scene.document; - let world = - codec::types::Vector3::new(pick.point.x, pick.point.y, pick.point.z); + let world = codec::types::Vector3::new(pick.point.x, pick.point.y, pick.point.z); let resolved = match (pick.whole_curve, pick.handle) { - (true, Some(handle)) => { - parametric_curve_ref_for_pick(document, scope, handle, world) - .ok_or(FixConstraintCommand::INVALID_OBJECT) - } + (true, Some(handle)) => parametric_curve_ref_for_pick(document, scope, handle, world) + .ok_or(FixConstraintCommand::INVALID_OBJECT), (true, None) => Err(FixConstraintCommand::NO_OBJECT), (false, Some(handle)) => { nearest_parametric_point_on_entity(document, scope, handle, world) @@ -1020,11 +1027,7 @@ impl OpenCADStudio { let supported = !pick.whole_curve || self.tabs[i] .scene - .validate_parametric_constraint( - ConstraintKind::Fixed, - &[reference], - None, - ) + .validate_parametric_constraint(ConstraintKind::Fixed, &[reference], None) .is_ok(); supported .then_some(reference) @@ -1038,9 +1041,7 @@ impl OpenCADStudio { label: "Fixed constraint", }), // A miss re-prompts: `ReportError` keeps the command. - Err(message) => { - self.apply_cmd_result(CmdResult::ReportError(message.to_string())) - } + Err(message) => self.apply_cmd_result(CmdResult::ReportError(message.to_string())), } } @@ -1075,7 +1076,10 @@ impl OpenCADStudio { Task::none() } - pub(super) fn handle_add_horizontal_constraint(&mut self, result: CmdResult) -> Option> { + pub(super) fn handle_add_horizontal_constraint( + &mut self, + result: CmdResult, + ) -> Option> { let i = self.active_tab; let CmdResult::AddHorizontalConstraint { kind, @@ -1096,9 +1100,7 @@ impl OpenCADStudio { let vertical = kind == ConstraintKind::Vertical; let axis = if vertical { "Vertical" } else { "Horizontal" }; let scope = self.tabs[i].current_parametric_scope(); - let to_world = |point: glam::DVec3| { - codec::types::Vector3::new(point.x, point.y, point.z) - }; + let to_world = |point: glam::DVec3| codec::types::Vector3::new(point.x, point.y, point.z); let (refs, initial_fixed) = match selection { HorizontalConstraintSelection::Reference(reference) => { // The reference turns the object about its first @@ -1175,19 +1177,16 @@ impl OpenCADStudio { )); return Some(Task::none()); } - let direction = codec::types::Vector3::new( - direction.x / axis_length, - direction.y / axis_length, - 0.0, - ); - if let Err(message) = - self.tabs[i].scene.validate_parametric_constraint(kind, &refs, None) + let direction = + codec::types::Vector3::new(direction.x / axis_length, direction.y / axis_length, 0.0); + if let Err(message) = self.tabs[i] + .scene + .validate_parametric_constraint(kind, &refs, None) { self.command_line.push_error(message); return Some(Task::none()); } - if self - .tabs[i] + if self.tabs[i] .scene .parametric_constraint_set(scope) .is_some_and(|set| set.contains_axis_constraint(kind, &refs, direction)) @@ -1271,8 +1270,7 @@ impl OpenCADStudio { if length > 1.0e-9 && angle.abs() > 1.0e-9 { match vertex { Some(index) => { - let moved = - self.tabs[i].scene.document.get_entity(handle).cloned(); + let moved = self.tabs[i].scene.document.get_entity(handle).cloned(); if let Some(mut entity) = moved { if crate::scene::parametric_constraints::set_polyline_vertex( &mut entity, @@ -1336,9 +1334,7 @@ impl OpenCADStudio { use crate::scene::parametric_constraints::ConstraintKind; let scope = self.tabs[i].current_parametric_scope(); - let to_world = |point: glam::DVec3| { - codec::types::Vector3::new(point.x, point.y, point.z) - }; + let to_world = |point: glam::DVec3| codec::types::Vector3::new(point.x, point.y, point.z); let resolve_point = |pick: crate::command::CoincidentPick| { if let Some(handle) = pick.handle { crate::scene::parametric_constraints::nearest_parametric_point_on_entity( @@ -1359,8 +1355,7 @@ impl OpenCADStudio { let (first, second) = match selection { SymmetricConstraintSelection::Objects(first, second) => (first, second), SymmetricConstraintSelection::Points(first, second) => { - let (Some(first), Some(second)) = - (resolve_point(first), resolve_point(second)) + let (Some(first), Some(second)) = (resolve_point(first), resolve_point(second)) else { self.command_line.push_error( "Symmetric: select supported endpoints, centers, midpoints, or vertices.", @@ -1370,10 +1365,7 @@ impl OpenCADStudio { (first, second) } }; - if first == second - || axis.entity == first.entity - || axis.entity == second.entity - { + if first == second || axis.entity == first.entity || axis.entity == second.entity { self.command_line .push_error("Symmetric: select two different references and a separate line axis."); return Some(Task::none()); @@ -1438,11 +1430,9 @@ impl OpenCADStudio { .into_iter() .map(|handle| (handle, crate::scene::ChangeKind::Modified)) .collect::>(); - self.tabs[i].scene.bump_entities_with_initial_parametric_policy( - &changes, - &[first, axis], - false, - ); + self.tabs[i] + .scene + .bump_entities_with_initial_parametric_policy(&changes, &[first, axis], false); self.tabs[i].dirty = true; self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; @@ -1455,7 +1445,10 @@ impl OpenCADStudio { None } - pub(super) fn handle_add_perpendicular_constraint(&mut self, result: CmdResult) -> Option> { + pub(super) fn handle_add_perpendicular_constraint( + &mut self, + result: CmdResult, + ) -> Option> { let i = self.active_tab; let CmdResult::AddPerpendicularConstraint { first, @@ -1511,11 +1504,13 @@ impl OpenCADStudio { .into_iter() .map(|handle| (handle, crate::scene::ChangeKind::Modified)) .collect::>(); - self.tabs[i].scene.bump_entities_with_initial_parametric_policy( - &changes, - &[first_fixed, second_start], - true, - ); + self.tabs[i] + .scene + .bump_entities_with_initial_parametric_policy( + &changes, + &[first_fixed, second_start], + true, + ); self.tabs[i].dirty = true; self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; @@ -1537,11 +1532,11 @@ impl OpenCADStudio { use crate::scene::parametric_constraints::ConstraintKind; let refs = vec![first, second]; - if let Err(message) = self.tabs[i].scene.validate_parametric_constraint( - ConstraintKind::Tangent, - &refs, - None, - ) { + if let Err(message) = + self.tabs[i] + .scene + .validate_parametric_constraint(ConstraintKind::Tangent, &refs, None) + { self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.command_line.push_error(message); @@ -1578,11 +1573,9 @@ impl OpenCADStudio { .into_iter() .map(|handle| (handle, crate::scene::ChangeKind::Modified)) .collect::>(); - self.tabs[i].scene.bump_entities_with_initial_parametric_policy( - &changes, - &[first], - true, - ); + self.tabs[i] + .scene + .bump_entities_with_initial_parametric_policy(&changes, &[first], true); self.tabs[i].dirty = true; self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; @@ -1664,11 +1657,9 @@ impl OpenCADStudio { .into_iter() .map(|handle| (handle, crate::scene::ChangeKind::Modified)) .collect::>(); - self.tabs[i].scene.bump_entities_with_initial_parametric_policy( - &changes, - &[first], - true, - ); + self.tabs[i] + .scene + .bump_entities_with_initial_parametric_policy(&changes, &[first], true); self.tabs[i].dirty = true; self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; @@ -1680,7 +1671,10 @@ impl OpenCADStudio { None } - pub(super) fn handle_add_coincident_constraint(&mut self, result: CmdResult) -> Option> { + pub(super) fn handle_add_coincident_constraint( + &mut self, + result: CmdResult, + ) -> Option> { let i = self.active_tab; let CmdResult::AddCoincidentConstraint { first, @@ -1693,8 +1687,7 @@ impl OpenCADStudio { }; let scope = self.tabs[i].current_parametric_scope(); let to_world = |p: glam::DVec3| codec::types::Vector3::new(p.x, p.y, p.z); - let resolve = |pick: crate::command::CoincidentPick, - exclude: Option| { + let resolve = |pick: crate::command::CoincidentPick, exclude: Option| { if pick.whole_curve { pick.handle.and_then(|handle| { crate::scene::parametric_constraints::parametric_curve_ref_for_pick( @@ -1721,8 +1714,8 @@ impl OpenCADStudio { } }; let resolved_first = resolve(first, None); - let resolved_second = resolved_first - .and_then(|reference| resolve(second, Some(reference.entity))); + let resolved_second = + resolved_first.and_then(|reference| resolve(second, Some(reference.entity))); let (Some(first_ref), Some(second_ref)) = (resolved_first, resolved_second) else { self.command_line.push_error( "Coincident: select a supported endpoint, center, midpoint, vertex, or curve.", @@ -1791,7 +1784,8 @@ impl OpenCADStudio { if !multiple { self.tabs[i].active_cmd = None; } - self.command_line.push_output("Coincident constraint applied."); + self.command_line + .push_output("Coincident constraint applied."); if multiple { if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|cmd| cmd.prompt()) { self.command_line.push_info(&prompt); @@ -1825,19 +1819,18 @@ impl OpenCADStudio { .parametric_constraint_set(scope) .cloned() .unwrap_or_else(|| { - crate::scene::parametric_constraints::ParametricConstraintSet::new( - scope, - ) + crate::scene::parametric_constraints::ParametricConstraintSet::new(scope) }); let pending = self.begin_undo(i, "Coincident auto constrain", handles.len(), true); self.tabs[i] .scene .record_undo_parametric_constraints_before(scope, constraints_before); for refs in inferred { - let id = self.tabs[i] - .scene - .parametric_constraint_set_mut(scope) - .add(ConstraintKind::Coincident, refs, None); + let id = self.tabs[i].scene.parametric_constraint_set_mut(scope).add( + ConstraintKind::Coincident, + refs, + None, + ); self.tabs[i].scene.note_parametric_constraint_applied( scope, id, diff --git a/src/app/command_driver/dim_edit.rs b/src/app/command_driver/dim_edit.rs index a074c8285..906b4c636 100644 --- a/src/app/command_driver/dim_edit.rs +++ b/src/app/command_driver/dim_edit.rs @@ -29,13 +29,16 @@ impl OpenCADStudio { self.tabs[i].snap_result = None; } - pub(super) fn handle_edit_dimension_jog(&mut self, dimension: Handle, point: Option) { + pub(super) fn handle_edit_dimension_jog( + &mut self, + dimension: Handle, + point: Option, + ) { let i = self.active_tab; let valid = matches!( self.tabs[i].scene.document.get_entity(dimension), Some(codec::EntityType::Dimension( - codec::entities::Dimension::Linear(_) - | codec::entities::Dimension::Aligned(_) + codec::entities::Dimension::Linear(_) | codec::entities::Dimension::Aligned(_) )) ); if !valid { @@ -50,9 +53,7 @@ impl OpenCADStudio { XDataValue::Integer16(387), XDataValue::Integer16(3), XDataValue::Integer16(389), - XDataValue::Point3D(codec::types::Vector3::new( - point.x, point.y, point.z, - )), + XDataValue::Point3D(codec::types::Vector3::new(point.x, point.y, point.z)), ] }); crate::scene::view::dispatch::set_entity_xdata( @@ -79,7 +80,12 @@ impl OpenCADStudio { self.tabs[i].snap_result = None; } - pub(super) fn handle_space_dimensions(&mut self, base: Handle, others: Vec, spacing: Option) { + pub(super) fn handle_space_dimensions( + &mut self, + base: Handle, + others: Vec, + spacing: Option, + ) { let i = self.active_tab; self.push_undo_snapshot(i, "DIMSPACE"); if apply_dimspace(&mut self.tabs[i].scene, base, &others, spacing) { diff --git a/src/app/command_driver/entity_commit.rs b/src/app/command_driver/entity_commit.rs index 8caae44ad..2ff842393 100644 --- a/src/app/command_driver/entity_commit.rs +++ b/src/app/command_driver/entity_commit.rs @@ -41,12 +41,10 @@ impl OpenCADStudio { let is_associative_dimension = matches!( entity, codec::EntityType::Dimension( - codec::entities::Dimension::Linear(_) - | codec::entities::Dimension::Aligned(_) + codec::entities::Dimension::Linear(_) | codec::entities::Dimension::Aligned(_) ) ); - let association_enabled = - self.tabs[i].scene.document.header.dimension_associativity == 2; + let association_enabled = self.tabs[i].scene.document.header.dimension_associativity == 2; let committed = if preserve_commit_layer { self.commit_entity_handle_preserve_layer(entity) } else { @@ -99,8 +97,8 @@ impl OpenCADStudio { .as_ref() .and_then(|command| command.nested_copy_symbol_names()) .cloned(); - let delta_safe = symbol_names.is_none() - && entities.iter().all(|entity| self.delta_add_safe(i, entity)); + let delta_safe = + symbol_names.is_none() && entities.iter().all(|entity| self.delta_add_safe(i, entity)); let pending = self.begin_undo(i, label, entities.len(), delta_safe); if let Some(names) = symbol_names { let retained = self.tabs[i] @@ -162,8 +160,8 @@ impl OpenCADStudio { .as_ref() .and_then(|command| command.nested_copy_symbol_names()) .cloned(); - let delta_safe = symbol_names.is_none() - && entities.iter().all(|entity| self.delta_add_safe(i, entity)); + let delta_safe = + symbol_names.is_none() && entities.iter().all(|entity| self.delta_add_safe(i, entity)); let pending = self.begin_undo(i, label, entities.len(), delta_safe); if let Some(names) = symbol_names { let retained = self.tabs[i] @@ -223,24 +221,35 @@ impl OpenCADStudio { self.update_cont_anchor(&entity); // ATTDEF's height becomes the next text height (TEXTSIZE), as TEXT's does. if let codec::EntityType::AttributeDefinition(attribute) = &entity { - if self.tabs[i].active_cmd.as_ref().is_some_and(|cmd| cmd.name().ends_with("ATTDEF")) { + if self.tabs[i] + .active_cmd + .as_ref() + .is_some_and(|cmd| cmd.name().ends_with("ATTDEF")) + { self.tabs[i].scene.document.header.text_height = attribute.height; } } // A field the committed text shows (FIELD, an ATTDEF Default from the // Field dialog) is attached once the entity has its handle; the field // objects are not part of the add delta, so the step is a snapshot. - let text_field = self.tabs[i].active_cmd.as_ref().and_then(|cmd| cmd.text_field()); + let text_field = self.tabs[i] + .active_cmd + .as_ref() + .and_then(|cmd| cmd.text_field()); let label = self.history_label_from_active_cmd(i, "ENTITY"); // A block reference whose attribute definitions carry fields gets // them copied onto its attributes (field objects: snapshot step). let attribute_fields = match &entity { codec::EntityType::Insert(insert) if !insert.attributes.is_empty() => { - crate::entities::field::block_has_attribute_fields(&self.tabs[i].scene.document, &insert.block_name) + crate::entities::field::block_has_attribute_fields( + &self.tabs[i].scene.document, + &insert.block_name, + ) } _ => false, }; - let delta_safe = text_field.is_none() && !attribute_fields && self.delta_add_safe(i, &entity); + let delta_safe = + text_field.is_none() && !attribute_fields && self.delta_add_safe(i, &entity); let field_value = match &entity { codec::EntityType::MText(m) => m.value.clone(), codec::EntityType::AttributeDefinition(a) => a.default_value.clone(), @@ -250,12 +259,10 @@ impl OpenCADStudio { let is_associative_dimension = matches!( entity, codec::EntityType::Dimension( - codec::entities::Dimension::Linear(_) - | codec::entities::Dimension::Aligned(_) + codec::entities::Dimension::Linear(_) | codec::entities::Dimension::Aligned(_) ) ); - let association_enabled = - self.tabs[i].scene.document.header.dimension_associativity == 2; + let association_enabled = self.tabs[i].scene.document.header.dimension_associativity == 2; let committed = if preserve_commit_layer { self.commit_entity_handle_preserve_layer(entity) } else { @@ -278,8 +285,13 @@ impl OpenCADStudio { ); } if let (Some(handle), true) = (committed, attribute_fields) { - crate::entities::field::attach_attribute_fields(&mut self.tabs[i].scene.document, handle); - self.tabs[i].scene.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]); + crate::entities::field::attach_attribute_fields( + &mut self.tabs[i].scene.document, + handle, + ); + self.tabs[i] + .scene + .bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]); } if let Some(handle) = committed { self.apply_continuous_constraints(i, &[handle]); @@ -340,99 +352,99 @@ impl OpenCADStudio { } else { None }; - let pending = if association_mode == 0 { - let layer = self.tabs[i].active_layer.clone(); - if layer != "0" || entity.as_entity().layer().is_empty() { - entity.as_entity_mut().set_layer(layer); - } - crate::scene::view::dispatch::apply_color( - &mut entity, - self.ribbon.active_color, - ); - crate::scene::view::dispatch::apply_common_prop( - &mut entity, - "linetype", - &self.ribbon.active_linetype.clone(), - ); - crate::scene::view::dispatch::apply_line_weight( - &mut entity, - self.ribbon.active_lineweight, - ); - let celtscale = self.tabs[i] - .scene - .document - .header - .current_entity_linetype_scale; - if (celtscale - 1.0).abs() > 1e-9 && celtscale.abs() > 1e-9 { - entity.common_mut().linetype_scale = celtscale; - } - crate::scene::creation_style::apply_current_creation_styles( - &self.tabs[i].scene.document, - &mut entity, - ); - if let (Some((layer, style_name)), codec::EntityType::Dimension(dimension)) = - (inherited_dimension, &mut entity) - { - dimension.base_mut().common.layer = layer; - dimension.base_mut().style_name = style_name; - } - let pieces = crate::modules::draw::modify::explode::explode_entity( - &entity, - &self.tabs[i].scene.document, - ); - let delta_safe = pieces.iter().all(|piece| self.delta_add_safe(i, piece)); - let pending = self.begin_undo(i, label, pieces.len(), delta_safe); - for piece in pieces { - self.tabs[i].scene.add_entity(piece); - } - pending - } else { - let delta_safe = self.delta_add_safe(i, &entity); - let pending = self.begin_undo(i, label, 1, delta_safe); - if let Some(handle) = - self.commit_entity_handle_with_dimension_policy(entity, preserve_base_style) - { - if association_mode == 2 && association_allowed { - let mut changes = vec![(handle, crate::scene::ChangeKind::Modified)]; - match association { - crate::command::DimensionAssociationInput::Infer(source) => { - let sources: Vec<_> = source.map_or_else( - || self.infer_dimension_sources_guarded(i, handle), - |source| { - if single_source_dimension { - vec![Some(source)] - } else { - vec![Some(source), Some(source)] - } - }, - ); - self.tabs[i] - .scene - .attach_dimension_association(handle, sources.clone()); - changes.extend(sources.into_iter().flatten().map(|source| { - (source, crate::scene::ChangeKind::Modified) - })); - } - crate::command::DimensionAssociationInput::Explicit(sources) => { - self.tabs[i].scene.attach_dimension_association_sources( - handle, - sources.clone(), - ); - changes.extend(sources.into_iter().flatten().map(|source| { - (source.handle, crate::scene::ChangeKind::Modified) - })); - } - } - changes.sort_by_key(|(handle, _)| handle.value()); - changes.dedup_by_key(|(handle, _)| handle.value()); - self.tabs[i].scene.bump_entities(&changes); - } else if association_mode == 2 { - // Preserve the acquired viewport and source paths. - self.attach_viewport_dimension_association(i, handle); + let pending = + if association_mode == 0 { + let layer = self.tabs[i].active_layer.clone(); + if layer != "0" || entity.as_entity().layer().is_empty() { + entity.as_entity_mut().set_layer(layer); } - } - pending - }; + crate::scene::view::dispatch::apply_color(&mut entity, self.ribbon.active_color); + crate::scene::view::dispatch::apply_common_prop( + &mut entity, + "linetype", + &self.ribbon.active_linetype.clone(), + ); + crate::scene::view::dispatch::apply_line_weight( + &mut entity, + self.ribbon.active_lineweight, + ); + let celtscale = self.tabs[i] + .scene + .document + .header + .current_entity_linetype_scale; + if (celtscale - 1.0).abs() > 1e-9 && celtscale.abs() > 1e-9 { + entity.common_mut().linetype_scale = celtscale; + } + crate::scene::creation_style::apply_current_creation_styles( + &self.tabs[i].scene.document, + &mut entity, + ); + if let (Some((layer, style_name)), codec::EntityType::Dimension(dimension)) = + (inherited_dimension, &mut entity) + { + dimension.base_mut().common.layer = layer; + dimension.base_mut().style_name = style_name; + } + let pieces = crate::modules::draw::modify::explode::explode_entity( + &entity, + &self.tabs[i].scene.document, + ); + let delta_safe = pieces.iter().all(|piece| self.delta_add_safe(i, piece)); + let pending = self.begin_undo(i, label, pieces.len(), delta_safe); + for piece in pieces { + self.tabs[i].scene.add_entity(piece); + } + pending + } else { + let delta_safe = self.delta_add_safe(i, &entity); + let pending = self.begin_undo(i, label, 1, delta_safe); + if let Some(handle) = + self.commit_entity_handle_with_dimension_policy(entity, preserve_base_style) + { + if association_mode == 2 && association_allowed { + let mut changes = vec![(handle, crate::scene::ChangeKind::Modified)]; + match association { + crate::command::DimensionAssociationInput::Infer(source) => { + let sources: Vec<_> = source.map_or_else( + || self.infer_dimension_sources_guarded(i, handle), + |source| { + if single_source_dimension { + vec![Some(source)] + } else { + vec![Some(source), Some(source)] + } + }, + ); + self.tabs[i] + .scene + .attach_dimension_association(handle, sources.clone()); + changes.extend( + sources + .into_iter() + .flatten() + .map(|source| (source, crate::scene::ChangeKind::Modified)), + ); + } + crate::command::DimensionAssociationInput::Explicit(sources) => { + self.tabs[i] + .scene + .attach_dimension_association_sources(handle, sources.clone()); + changes.extend(sources.into_iter().flatten().map(|source| { + (source.handle, crate::scene::ChangeKind::Modified) + })); + } + } + changes.sort_by_key(|(handle, _)| handle.value()); + changes.dedup_by_key(|(handle, _)| handle.value()); + self.tabs[i].scene.bump_entities(&changes); + } else if association_mode == 2 { + // Preserve the acquired viewport and source paths. + self.attach_viewport_dimension_association(i, handle); + } + } + pending + }; self.tabs[i].dirty = true; self.tabs[i].scene.clear_preview_wire(); self.tabs[i].snap_result = None; @@ -452,10 +464,7 @@ impl OpenCADStudio { pub(super) fn handle_commit_dimensions_and_exit( &mut self, - dimensions: Vec<( - codec::EntityType, - crate::command::DimensionAssociationInput, - )>, + dimensions: Vec<(codec::EntityType, crate::command::DimensionAssociationInput)>, ) { let i = self.active_tab; let association_mode = self.tabs[i].scene.document.header.dimension_associativity; @@ -467,10 +476,7 @@ impl OpenCADStudio { if layer != "0" || entity.as_entity().layer().is_empty() { entity.as_entity_mut().set_layer(layer); } - crate::scene::view::dispatch::apply_color( - &mut entity, - self.ribbon.active_color, - ); + crate::scene::view::dispatch::apply_color(&mut entity, self.ribbon.active_color); crate::scene::view::dispatch::apply_common_prop( &mut entity, "linetype", @@ -521,30 +527,22 @@ impl OpenCADStudio { continue; } let sources = match association { - crate::command::DimensionAssociationInput::Infer(source) => { - source.map_or_else( - || { - self.infer_dimension_sources_guarded(i, handle) - .into_iter() - .map(|source| { - source.map( - crate::command::DimensionAssociationSource::inferred, - ) - }) - .collect() - }, - |source| { - vec![Some( - crate::command::DimensionAssociationSource::inferred( - source, - ), - )] - }, - ) - } - crate::command::DimensionAssociationInput::Explicit(sources) => { - sources - } + crate::command::DimensionAssociationInput::Infer(source) => source.map_or_else( + || { + self.infer_dimension_sources_guarded(i, handle) + .into_iter() + .map(|source| { + source.map(crate::command::DimensionAssociationSource::inferred) + }) + .collect() + }, + |source| { + vec![Some(crate::command::DimensionAssociationSource::inferred( + source, + ))] + }, + ), + crate::command::DimensionAssociationInput::Explicit(sources) => sources, }; self.tabs[i] .scene @@ -591,8 +589,7 @@ impl OpenCADStudio { let erase_source = erase_source.filter(|handle| { self.delete_objects != 0 && !self.tabs[i].scene.is_layer_locked(*handle) }); - let pending = - self.begin_undo(i, label, 1 + usize::from(erase_source.is_some()), true); + let pending = self.begin_undo(i, label, 1 + usize::from(erase_source.is_some()), true); let handle = self.add_solid_model(entity, *solid, history); if !handle.is_null() { if let Some(source) = erase_source { @@ -767,7 +764,9 @@ impl OpenCADStudio { mut options: Box, ) -> Option> { let i = self.active_tab; - options.handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle)); + options + .handles + .retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle)); if options.handles.is_empty() { self.command_line .push_info(crate::t!("No editable objects selected.").as_ref()); @@ -865,8 +864,7 @@ impl OpenCADStudio { sources.push(handles); } if let Some(paths) = hatch.boundary_paths.as_mut() { - for (path, handles) in std::sync::Arc::make_mut(paths).iter_mut().zip(&sources) - { + for (path, handles) in std::sync::Arc::make_mut(paths).iter_mut().zip(&sources) { path.boundary_handles = handles.clone(); path.flags.set_external(!handles.is_empty()); } @@ -942,9 +940,8 @@ impl OpenCADStudio { .bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]); self.tabs[i].dirty = true; } else { - self.command_line.push_error( - crate::t!("The selected object is no longer available.").as_ref(), - ); + self.command_line + .push_error(crate::t!("The selected object is no longer available.").as_ref()); } self.tabs[i].scene.clear_preview_wire(); self.tabs[i].active_cmd = None; @@ -969,13 +966,11 @@ impl OpenCADStudio { .reassociate_center_mark(target, source, point) { self.tabs[i].dirty = true; - self.command_line.push_output( - crate::t!("CENTERREASSOCIATE: center mark associated.").as_ref(), - ); + self.command_line + .push_output(crate::t!("CENTERREASSOCIATE: center mark associated.").as_ref()); } else { self.command_line.push_error( - crate::t!("CENTERREASSOCIATE: the selected source is not circular.") - .as_ref(), + crate::t!("CENTERREASSOCIATE: the selected source is not circular.").as_ref(), ); } self.tabs[i].active_cmd = None; @@ -995,8 +990,7 @@ impl OpenCADStudio { br.entity_handles .iter() .filter_map(|&h| { - if let Some(codec::EntityType::AttributeDefinition(ad)) = - doc.get_entity(h) + if let Some(codec::EntityType::AttributeDefinition(ad)) = doc.get_entity(h) { Some(ad.clone()) } else { @@ -1033,7 +1027,10 @@ impl OpenCADStudio { self.tabs[i].dirty = true; self.tabs[i].scene.clear_preview_wire(); // A repeating insertion keeps its command for the next block. - let keep = self.tabs[i].active_cmd.as_ref().is_some_and(|c| c.attreq_continue()); + let keep = self.tabs[i] + .active_cmd + .as_ref() + .is_some_and(|c| c.attreq_continue()); if keep { let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); if let Some(p) = prompt { @@ -1061,7 +1058,10 @@ impl OpenCADStudio { let name = ins.block_name.clone(); self.record_block_insert(&name); } - let keep = self.tabs[i].active_cmd.as_ref().is_some_and(|c| c.attreq_continue()); + let keep = self.tabs[i] + .active_cmd + .as_ref() + .is_some_and(|c| c.attreq_continue()); return Some(self.apply_cmd_result(if keep { CmdResult::CommitEntity(entity) } else { diff --git a/src/app/command_driver/mod.rs b/src/app/command_driver/mod.rs index 5b28a3327..b34b45038 100644 --- a/src/app/command_driver/mod.rs +++ b/src/app/command_driver/mod.rs @@ -80,19 +80,19 @@ use crate::command::{CmdResult, SelectionEntity, StepInput}; use codec::Handle; use iced::Task; -mod utilities; -mod parameters; -mod tests; -mod property_match; -mod entity_commit; -mod solid3d; -mod viewport; -mod transform; mod clipboard; -mod modify; -mod text_edit; mod constraint; mod dim_edit; +mod entity_commit; +mod modify; +mod parameters; +mod property_match; +mod solid3d; +mod tests; +mod text_edit; +mod transform; +mod utilities; +mod viewport; pub(crate) use clipboard::{ copy_to_clipboard_kernel, paste_entities_kernel, remap_ext_subtree_reference, PasteSpace, @@ -301,7 +301,11 @@ impl OpenCADStudio { /// Reports a refused command-line value and shows the prompt again. fn reprompt_active_command(&mut self, i: usize, message: &str) { self.command_line.push_error(message); - if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|command| command.prompt()) { + if let Some(prompt) = self.tabs[i] + .active_cmd + .as_ref() + .map(|command| command.prompt()) + { self.command_line.push_info(&prompt); } } @@ -408,7 +412,6 @@ impl OpenCADStudio { handles } - fn apply_cmd_result_inner(&mut self, result: CmdResult) -> Task { let i = self.active_tab; if let Some(bind) = self.tabs[i] @@ -481,7 +484,9 @@ impl OpenCADStudio { } => { if !target.is_null() { self.handle_vpclip(target, boundary, boundary_handle); - } else if let Some(task) = self.handle_mview_create_clipped(boundary, boundary_handle) { + } else if let Some(task) = + self.handle_mview_create_clipped(boundary, boundary_handle) + { return task; } } @@ -887,7 +892,11 @@ impl OpenCADStudio { result @ CmdResult::AlignSelected { .. } => { self.handle_align_selected(result); } - CmdResult::LengthenEntity { handle, pick_pt, mode } => { + CmdResult::LengthenEntity { + handle, + pick_pt, + mode, + } => { if let Some(task) = self.handle_lengthen_entity(handle, pick_pt, mode) { return task; } @@ -921,7 +930,11 @@ impl OpenCADStudio { CmdResult::StretchWindow { handles, windows } => { self.handle_stretch_window(handles, windows); } - CmdResult::StretchEntities { handles, windows, delta } => { + CmdResult::StretchEntities { + handles, + windows, + delta, + } => { if let Some(task) = self.handle_stretch_entities(handles, windows, delta) { return task; } @@ -942,7 +955,11 @@ impl OpenCADStudio { result @ CmdResult::PresspullPick { .. } => { self.handle_presspull_pick(result); } - CmdResult::PresspullApply { targets, distance, color: _ } => { + CmdResult::PresspullApply { + targets, + distance, + color: _, + } => { self.handle_presspull_apply(targets, distance); } @@ -964,15 +981,33 @@ impl OpenCADStudio { } } - result @ CmdResult::SolidEdgeBlend { .. } => return self.handle_solid_edge_blend(result), + result @ CmdResult::SolidEdgeBlend { .. } => { + return self.handle_solid_edge_blend(result) + } - CmdResult::SolidShell { handle, actions, distance } => return self.handle_solid_shell(handle, actions, distance), + CmdResult::SolidShell { + handle, + actions, + distance, + } => return self.handle_solid_shell(handle, actions, distance), - CmdResult::SolidSubtract { bases, cutters, convert_meshes } => return self.handle_solid_subtract(bases, cutters, convert_meshes), + CmdResult::SolidSubtract { + bases, + cutters, + convert_meshes, + } => return self.handle_solid_subtract(bases, cutters, convert_meshes), - CmdResult::SliceEntities { targets, plane, keep_point } => return self.handle_slice_entities(targets, plane, keep_point), + CmdResult::SliceEntities { + targets, + plane, + keep_point, + } => return self.handle_slice_entities(targets, plane, keep_point), - CmdResult::SliceSurfaceEntities { targets, cutter, keep_point } => return self.handle_slice_surface_entities(targets, cutter, keep_point), + CmdResult::SliceSurfaceEntities { + targets, + cutter, + keep_point, + } => return self.handle_slice_surface_entities(targets, cutter, keep_point), result @ CmdResult::HatcheditApply { .. } => { if let Some(task) = self.handle_hatchedit_apply(result) { @@ -1499,19 +1534,14 @@ impl OpenCADStudio { } // --- C2a handlers: property_match (thin-router targets, bodies moved verbatim) --- - // --- C2a handlers: viewport (thin-router targets, bodies moved verbatim) --- - // --- C2a handlers: text_edit (thin-router targets, bodies moved verbatim) --- - // --- C2b handlers: entity_commit (thin-router targets, bodies moved verbatim) --- - // --- C2b handlers: clipboard (thin-router targets, bodies moved verbatim) --- - /// Restore the tangent-snap / ortho state that was in effect before the command started. /// Recreate clipboard-dependency records (layer / linetype / text + dim /// style) in tab `i`'s document for any the copied entities reference but @@ -1526,13 +1556,10 @@ impl OpenCADStudio { clipboard::merge_table_records(&mut self.tabs[i].scene.document, deps); } - // --- C2d handlers: constraint (thin-router targets, bodies moved verbatim) --- - // --- C2d handlers: dim_edit (thin-router targets, bodies moved verbatim) --- - pub(super) fn restore_pre_cmd_tangent(&mut self) { if let Some(was_on) = self.pre_cmd_tangent.take() { if !was_on { @@ -1571,23 +1598,28 @@ fn entity_at_typed_point( extent = extent .max((bounds.max.x - bounds.min.x).abs()) .max((bounds.max.y - bounds.min.y).abs()); - let distance = match crate::scene::viewport_dimension_pick::planar_pick_distance(entity, point) { - Some(distance) => distance, - // A text or an ellipse answers by its extent, the way a click - // inside it picks it. - None if matches!( - entity, - codec::EntityType::Text(_) - | codec::EntityType::MText(_) - | codec::EntityType::Ellipse(_) - ) => - { - let dx = (bounds.min.x - point.x).max(point.x - bounds.max.x).max(0.0); - let dy = (bounds.min.y - point.y).max(point.y - bounds.max.y).max(0.0); - dx.hypot(dy) - } - None => continue, - }; + let distance = + match crate::scene::viewport_dimension_pick::planar_pick_distance(entity, point) { + Some(distance) => distance, + // A text or an ellipse answers by its extent, the way a click + // inside it picks it. + None if matches!( + entity, + codec::EntityType::Text(_) + | codec::EntityType::MText(_) + | codec::EntityType::Ellipse(_) + ) => + { + let dx = (bounds.min.x - point.x) + .max(point.x - bounds.max.x) + .max(0.0); + let dy = (bounds.min.y - point.y) + .max(point.y - bounds.max.y) + .max(0.0); + dx.hypot(dy) + } + None => continue, + }; if nearest.is_none_or(|(best, _)| distance < best) { nearest = Some((distance, common.handle)); } @@ -1598,10 +1630,8 @@ fn entity_at_typed_point( .map(|(_, handle)| handle) } - // ── DIMSPACE helper ─────────────────────────────────────────────────────────── - fn apply_mleader_align( scene: &mut crate::scene::Scene, handles: &[codec::Handle], @@ -1694,8 +1724,7 @@ fn apply_mleader_collect( } let px = point.x; let py = point.y; - let Some(codec::EntityType::MultiLeader(base)) = scene.document.get_entity(handles[0]) - else { + let Some(codec::EntityType::MultiLeader(base)) = scene.document.get_entity(handles[0]) else { return false; }; let base_block = base.block_content_handle; @@ -1746,5 +1775,3 @@ fn apply_mleader_collect( scene.bump_entities(&[(handles[0], crate::scene::ChangeKind::Modified)]); true } - - diff --git a/src/app/command_driver/modify.rs b/src/app/command_driver/modify.rs index 070d06c3b..c832155b1 100644 --- a/src/app/command_driver/modify.rs +++ b/src/app/command_driver/modify.rs @@ -1,7 +1,12 @@ use super::*; impl OpenCADStudio { - pub(super) fn handle_lengthen_entity(&mut self, handle: Handle, pick_pt: glam::DVec3, mode: crate::modules::draw::modify::lengthen::LenMode) -> Option> { + pub(super) fn handle_lengthen_entity( + &mut self, + handle: Handle, + pick_pt: glam::DVec3, + mode: crate::modules::draw::modify::lengthen::LenMode, + ) -> Option> { let i = self.active_tab; if self.reject_locked_edit(i, handle) { return Some(Task::none()); @@ -35,7 +40,12 @@ impl OpenCADStudio { None } - pub(super) fn handle_divide_entity(&mut self, handle: Handle, n: usize, marker: Option) { + pub(super) fn handle_divide_entity( + &mut self, + handle: Handle, + n: usize, + marker: Option, + ) { let i = self.active_tab; use crate::modules::draw::inquiry::divide::divide_entity; let pts = self.tabs[i] @@ -56,9 +66,8 @@ impl OpenCADStudio { self.command_line .push_output(crate::tf!("DIVIDE: {count} marker(s) placed.").as_ref()); } else { - self.command_line.push_error( - crate::t!("DIVIDE: entity type not supported or N < 2.").as_ref(), - ); + self.command_line + .push_error(crate::t!("DIVIDE: entity type not supported or N < 2.").as_ref()); } self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; @@ -105,12 +114,20 @@ impl OpenCADStudio { self.restore_pre_cmd_tangent(); } - pub(super) fn handle_pedit_op(&mut self, handle: Handle, op: crate::modules::draw::modify::pedit::PeditOp) -> Task { + pub(super) fn handle_pedit_op( + &mut self, + handle: Handle, + op: crate::modules::draw::modify::pedit::PeditOp, + ) -> Task { let i = self.active_tab; self.apply_pedit_result(i, handle, op) } - pub(super) fn handle_join_to_source(&mut self, source: Handle, handles: Vec) -> Option> { + pub(super) fn handle_join_to_source( + &mut self, + source: Handle, + handles: Vec, + ) -> Option> { let i = self.active_tab; if self.reject_locked_edit(i, source) { return Some(Task::none()); @@ -192,8 +209,7 @@ impl OpenCADStudio { self.tabs[i].snap_result = None; self.restore_pre_cmd_tangent(); self.command_line.push_output( - crate::tf!("JOIN: {count_in} object(s) joined into {count_out}.") - .as_ref(), + crate::tf!("JOIN: {count_in} object(s) joined into {count_out}.").as_ref(), ); self.refresh_properties(); } @@ -210,7 +226,12 @@ impl OpenCADStudio { None } - pub(super) fn handle_break_entity(&mut self, handle: Handle, p1: glam::DVec3, p2: glam::DVec3) -> Option> { + pub(super) fn handle_break_entity( + &mut self, + handle: Handle, + p1: glam::DVec3, + p2: glam::DVec3, + ) -> Option> { let i = self.active_tab; if self.reject_locked_edit(i, handle) { return Some(Task::none()); @@ -224,13 +245,15 @@ impl OpenCADStudio { match replacement { Some(frags) => { let unchanged = frags.len() == 1 - && self.tabs[i].scene.document.get_entity(handle).is_some_and( - |original| { + && self.tabs[i] + .scene + .document + .get_entity(handle) + .is_some_and(|original| { let mut fragment = frags[0].clone(); fragment.common_mut().handle = handle; &fragment == original - }, - ); + }); if unchanged { self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; @@ -282,7 +305,6 @@ impl OpenCADStudio { None } - pub(super) fn handle_hatchedit_apply(&mut self, result: CmdResult) -> Option> { let i = self.active_tab; let CmdResult::HatcheditApply { @@ -315,8 +337,7 @@ impl OpenCADStudio { } else { let plane = match self.tabs[i].scene.document.get_entity(handle) { Some(codec::EntityType::Hatch(h)) => { - let storage = - crate::entities::curve::ocs_plane(h.normal, h.elevation); + let storage = crate::entities::curve::ocs_plane(h.normal, h.elevation); crate::command::WorkingPlane::new( glam::DVec3::from_array(storage.origin), glam::DVec3::from_array(storage.x_axis), @@ -325,10 +346,12 @@ impl OpenCADStudio { } _ => self.tabs[i].ucs_xform().working_plane(), }; - let mut sources = - self.tabs[i].scene.boundary_sources_on_plane(plane, 1e-6); + let mut sources = self.tabs[i].scene.boundary_sources_on_plane(plane, 1e-6); sources.remove(&handle); - let command=crate::modules::draw::draw::hatchedit::HatcheditCommand::for_association(handle,name,scale,angle,plane,sources); + let command = + crate::modules::draw::draw::hatchedit::HatcheditCommand::for_association( + handle, name, scale, angle, plane, sources, + ); self.tabs[i].scene.deselect_all(); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); @@ -336,19 +359,16 @@ impl OpenCADStudio { return Some(Task::none()); } if let HatchEditOperation::DrawOrderBoundary { above } = &operation { - let references: Vec<_> = - match self.tabs[i].scene.document.get_entity(handle) { - Some(codec::EntityType::Hatch(h)) => h - .paths - .iter() - .flat_map(|p| p.boundary_handles.iter()) - .filter(|h| { - self.tabs[i].scene.document.get_entity(**h).is_some() - }) - .map(|h| format!("{:X}", h.value())) - .collect(), - _ => Vec::new(), - }; + let references: Vec<_> = match self.tabs[i].scene.document.get_entity(handle) { + Some(codec::EntityType::Hatch(h)) => h + .paths + .iter() + .flat_map(|p| p.boundary_handles.iter()) + .filter(|h| self.tabs[i].scene.document.get_entity(**h).is_some()) + .map(|h| format!("{:X}", h.value())) + .collect(), + _ => Vec::new(), + }; self.tabs[i].active_cmd = None; if references.is_empty() { self.command_line @@ -392,15 +412,15 @@ impl OpenCADStudio { name } }); - if layer.as_ref().is_some_and(|name| { - self.tabs[i].scene.document.layers.get(name).is_none() - }) { + if layer + .as_ref() + .is_some_and(|name| self.tabs[i].scene.document.layers.get(name).is_none()) + { self.discard_last_undo_entry(i); self.command_line.push_error("HATCHEDIT: layer not found."); return Some(Task::none()); } - if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) - { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { let common = entity.common_mut(); if let Some(value) = color { common.color = value; @@ -440,13 +460,9 @@ impl OpenCADStudio { self.tabs[i].scene.document.get_entity_mut(handle) { if !name.is_empty() && name != hatch.pattern.name { - if let Some(entry) = - crate::scene::model::hatch_patterns::find(&name) - { + if let Some(entry) = crate::scene::model::hatch_patterns::find(&name) { let mut pattern = - crate::scene::model::hatch_patterns::build_dxf_pattern( - entry, - ); + crate::scene::model::hatch_patterns::build_dxf_pattern(entry); crate::entities::hatch::scale_pattern_geometry( &mut pattern, scale.max(1.0e-6) as f64, @@ -466,8 +482,7 @@ impl OpenCADStudio { entry.gpu, crate::scene::model::hatch_model::HatchPattern::Solid ); - hatch.pattern_type = - codec::entities::HatchPatternType::Predefined; + hatch.pattern_type = codec::entities::HatchPatternType::Predefined; hatch.gradient_color.enabled = false; } } else { @@ -533,18 +548,13 @@ impl OpenCADStudio { || paths.iter().any(|path| { path.boundary_handles.is_empty() || path.boundary_handles.iter().any(|source| { - self.tabs[i] - .scene - .document - .get_entity(*source) - .is_none() + self.tabs[i].scene.document.get_entity(*source).is_none() }) }) { self.discard_last_undo_entry(i); - self.command_line.push_error( - "HATCHEDIT: associated boundary is no longer available.", - ); + self.command_line + .push_error("HATCHEDIT: associated boundary is no longer available."); return Some(Task::none()); } self.tabs[i].scene.replace_hatch_association(handle, paths); @@ -552,10 +562,8 @@ impl OpenCADStudio { HatchEditOperation::RecreateBoundary { associate, region } => { let source = self.tabs[i].scene.document.get_entity(handle).cloned(); if let Some(codec::EntityType::Hatch(source)) = source { - let storage = crate::entities::curve::ocs_plane( - source.normal, - source.elevation, - ); + let storage = + crate::entities::curve::ocs_plane(source.normal, source.elevation); let plane = crate::command::WorkingPlane::new( glam::DVec3::from_array(storage.origin), glam::DVec3::from_array(storage.x_axis), @@ -573,13 +581,11 @@ impl OpenCADStudio { }) .collect::>>(); let Some(loops) = loops.filter(|loops| { - !loops.is_empty() - && loops.iter().all(|ring| !ring.is_empty()) + !loops.is_empty() && loops.iter().all(|ring| !ring.is_empty()) }) else { self.discard_last_undo_entry(i); - self.command_line.push_error( - "HATCHEDIT: boundary cannot form a region.", - ); + self.command_line + .push_error("HATCHEDIT: boundary cannot form a region."); return Some(Task::none()); }; let contained = loops @@ -612,8 +618,7 @@ impl OpenCADStudio { .map(|(outer, depth)| { let mut indices = vec![outer]; indices.extend((0..loops.len()).filter(|inner| { - depths[*inner] == *depth + 1 - && contained[*inner][outer] + depths[*inner] == *depth + 1 && contained[*inner][outer] })); indices }) @@ -632,9 +637,8 @@ impl OpenCADStudio { .collect::>>(); let Some(entities) = entities else { self.discard_last_undo_entry(i); - self.command_line.push_error( - "HATCHEDIT: boundary cannot form a region.", - ); + self.command_line + .push_error("HATCHEDIT: boundary cannot form a region."); return Some(Task::none()); }; (entities, groups) @@ -642,21 +646,18 @@ impl OpenCADStudio { let rings = crate::scene::hatch_boundary_rings(&source); if rings.len() != source.paths.len() { self.discard_last_undo_entry(i); - self.command_line.push_error( - "HATCHEDIT: boundary cannot form a polyline.", - ); + self.command_line + .push_error("HATCHEDIT: boundary cannot form a polyline."); return Some(Task::none()); } let entities = crate::scene::boundary_entities(&rings, plane); if entities.len() != source.paths.len() { self.discard_last_undo_entry(i); - self.command_line.push_error( - "HATCHEDIT: boundary cannot form a polyline.", - ); + self.command_line + .push_error("HATCHEDIT: boundary cannot form a polyline."); return Some(Task::none()); } - let groups = - (0..entities.len()).map(|index| vec![index]).collect(); + let groups = (0..entities.len()).map(|index| vec![index]).collect(); (entities, groups) }; let mut path_handles = vec![None; source.paths.len()]; @@ -681,8 +682,7 @@ impl OpenCADStudio { path.flags.set_external(true); } } - hatch.is_associative = - path_handles.iter().all(Option::is_some); + hatch.is_associative = path_handles.iter().all(Option::is_some); } } self.tabs[i] diff --git a/src/app/command_driver/parameters.rs b/src/app/command_driver/parameters.rs index cab5fc6f7..5f71698e2 100644 --- a/src/app/command_driver/parameters.rs +++ b/src/app/command_driver/parameters.rs @@ -129,10 +129,7 @@ impl OpenCADStudio { self.command_line.push_error(&error.to_string()); return; } - if !table - .resolve(&target) - .is_ok_and(|value| value.is_finite()) - { + if !table.resolve(&target).is_ok_and(|value| value.is_finite()) { self.command_line.push_error("Invalid expression."); return; } @@ -171,7 +168,12 @@ impl OpenCADStudio { /// Renames a parameter everywhere: the table, the expressions that use /// it and the constraints it drives. - pub(crate) fn rename_parameter(&mut self, i: usize, old: &str, new: &str) -> Result<(), String> { + pub(crate) fn rename_parameter( + &mut self, + i: usize, + old: &str, + new: &str, + ) -> Result<(), String> { use crate::scene::named_parameters::{is_reserved_name, is_valid_name, DrivingValue}; let table = self.tabs[i].scene.named_parameters().clone(); let Some(source) = table.get(old).map(|parameter| parameter.source.clone()) else { @@ -181,7 +183,9 @@ impl OpenCADStudio { return Err(format!("Invalid parameter name {new}.")); } let mut renamed = table.clone(); - renamed.set(new, &source).map_err(|error| error.to_string())?; + renamed + .set(new, &source) + .map_err(|error| error.to_string())?; for parameter in table.iter() { if parameter.name == old { continue; @@ -221,7 +225,8 @@ impl OpenCADStudio { .record_undo_parametric_constraints_before(scope, before); let set = self.tabs[i].scene.parametric_constraint_set_mut(scope); for constraint in &mut set.constraints { - if matches!(&constraint.driving_param, Some(DrivingValue::Named(name)) if name == old) { + if matches!(&constraint.driving_param, Some(DrivingValue::Named(name)) if name == old) + { constraint.driving_param = Some(DrivingValue::Named(new.to_string())); } } @@ -902,22 +907,19 @@ impl OpenCADStudio { } pub(super) fn apply_continuous_constraints(&mut self, i: usize, new_handles: &[codec::Handle]) { - let supported_creation = self.tabs[i] - .active_cmd - .as_ref() - .is_some_and(|command| { - matches!( - command.name(), - "LINE" - | "PLINE" - | "POLYLINE" - | "RECTANG" - | "RECTANGLE" - | "POLYGON" - | "CIRCLE" - | "ARC" - ) - }); + let supported_creation = self.tabs[i].active_cmd.as_ref().is_some_and(|command| { + matches!( + command.name(), + "LINE" + | "PLINE" + | "POLYLINE" + | "RECTANG" + | "RECTANGLE" + | "POLYGON" + | "CIRCLE" + | "ARC" + ) + }); if supported_creation { self.apply_inferred_constraints(i, new_handles); } diff --git a/src/app/command_driver/property_match.rs b/src/app/command_driver/property_match.rs index fb41bc776..11601f17f 100644 --- a/src/app/command_driver/property_match.rs +++ b/src/app/command_driver/property_match.rs @@ -41,7 +41,6 @@ impl OpenCADStudio { } } - pub(super) fn handle_match_properties( &mut self, mut dest: Vec, @@ -163,5 +162,4 @@ impl OpenCADStudio { } None } - } diff --git a/src/app/command_driver/solid3d.rs b/src/app/command_driver/solid3d.rs index 5042fb036..758f7d864 100644 --- a/src/app/command_driver/solid3d.rs +++ b/src/app/command_driver/solid3d.rs @@ -45,8 +45,7 @@ impl OpenCADStudio { let mut consumed = Vec::new(); let mut failed = 0usize; for handle in &handles { - let Some(entity) = self.tabs[i].scene.document.get_entity(*handle).cloned() - else { + let Some(entity) = self.tabs[i].scene.document.get_entity(*handle).cloned() else { failed += 1; continue; }; @@ -56,11 +55,7 @@ impl OpenCADStudio { }; let direction = match extent { ExtrudeExtent::Height(height) => profile.plane.normal().map(|normal| { - glam::DVec3::new( - normal[0] * height, - normal[1] * height, - normal[2] * height, - ) + glam::DVec3::new(normal[0] * height, normal[1] * height, normal[2] * height) }), ExtrudeExtent::Direction(direction) => Some(direction), ExtrudeExtent::Path(_) => None, @@ -76,22 +71,14 @@ impl OpenCADStudio { (false, Some((_, path)), _) => { sweep_model::extruded_along_path(&entity, path, taper_angle) } - (false, None, Some(direction)) => sweep_model::extruded_direction( - &entity, - direction.to_array(), - taper_angle, - ), - (true, None, Some(direction)) => sweep_model::extruded_surface( - &entity, - direction.to_array(), - taper_angle, - ), + (false, None, Some(direction)) => { + sweep_model::extruded_direction(&entity, direction.to_array(), taper_angle) + } + (true, None, Some(direction)) => { + sweep_model::extruded_surface(&entity, direction.to_array(), taper_angle) + } (true, Some(_), _) => path_direction.and_then(|direction| { - sweep_model::extruded_surface( - &entity, - direction.to_array(), - taper_angle, - ) + sweep_model::extruded_surface(&entity, direction.to_array(), taper_angle) }), _ => None, }; @@ -126,9 +113,7 @@ impl OpenCADStudio { } } match history { - Some(history) => { - self.add_surface_model_with_history(surface, body, history) - } + Some(history) => self.add_surface_model_with_history(surface, body, history), None => self.add_surface_model(surface, body), } } else { @@ -136,12 +121,7 @@ impl OpenCADStudio { let history = path .as_ref() .and_then(|(_, path)| { - sweep_model::sweep_history( - &entity, - path, - taper_angle, - profile.plane.origin, - ) + sweep_model::sweep_history(&entity, path, taper_angle, profile.plane.origin) }) .or_else(|| { sweep_model::extrusion_history( @@ -187,7 +167,12 @@ impl OpenCADStudio { self.commit_undo_delta(i, pending); } } else { - self.command_line.push_error(crate::t!("EXTRUDE: no selected object could be extruded with the requested options.").as_ref()); + self.command_line.push_error( + crate::t!( + "EXTRUDE: no selected object could be extruded with the requested options." + ) + .as_ref(), + ); if let Some(pending) = pending { self.commit_undo_delta(i, pending); } @@ -200,7 +185,11 @@ impl OpenCADStudio { None } - pub(super) fn handle_thicken_entities(&mut self, handles: Vec, distance: f64) -> Option> { + pub(super) fn handle_thicken_entities( + &mut self, + handles: Vec, + distance: f64, + ) -> Option> { let i = self.active_tab; self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; @@ -233,20 +222,17 @@ impl OpenCADStudio { failed += 1; continue; }; - let kernel_distance = - if surface.kind == codec::entities::SurfaceKind::Revolved { - -distance - } else { - distance - }; + let kernel_distance = if surface.kind == codec::entities::SurfaceKind::Revolved { + -distance + } else { + distance + }; let body = self.tabs[i] .scene .solid_models .get(&handle) .cloned() - .or_else(|| { - crate::scene::convert::solid3d_tess::kernel_surface_body(surface) - }); + .or_else(|| crate::scene::convert::solid3d_tess::kernel_surface_body(surface)); let Some(body) = body else { failed += 1; continue; @@ -286,7 +272,8 @@ impl OpenCADStudio { if failed > 0 { self.command_line.push_output(&crate::tf!( "THICKEN: created {} solid(s); {} surface(s) could not be thickened exactly.", - created, failed + created, + failed )); } if let Some(pending) = pending { @@ -309,7 +296,11 @@ impl OpenCADStudio { self.presspull_pick(handle, point, offset); } - pub(super) fn handle_presspull_apply(&mut self, targets: Vec, distance: f64) { + pub(super) fn handle_presspull_apply( + &mut self, + targets: Vec, + distance: f64, + ) { self.presspull_apply(targets, distance); } @@ -357,8 +348,7 @@ impl OpenCADStudio { let mut created_handles = Vec::new(); let mut consumed = Vec::new(); for handle in &editable_handles { - let Some(entity) = self.tabs[i].scene.document.get_entity(*handle).cloned() - else { + let Some(entity) = self.tabs[i].scene.document.get_entity(*handle).cloned() else { failed += 1; continue; }; @@ -375,31 +365,22 @@ impl OpenCADStudio { continue; }; self.add_surface_model( - empty_revolved_surface( - &entity, - axis_start, - axis_end, - angle, - start_angle, - ), + empty_revolved_surface(&entity, axis_start, axis_end, angle, start_angle), body, ) } else { - let result = - sweep_model::revolve_history(&entity, from, to, angle, start_angle) - .and_then(|history| { - kernel::acis::rebuild_body(&history) - .ok() - .map(|solid| (solid, history)) + let result = sweep_model::revolve_history(&entity, from, to, angle, start_angle) + .and_then(|history| { + kernel::acis::rebuild_body(&history) + .ok() + .map(|solid| (solid, history)) + }) + .or_else(|| { + sweep_model::revolved(&entity, from, to, angle, start_angle).map(|solid| { + let history = crate::scene::model::solid_history::brep_op(&solid); + (solid, history) }) - .or_else(|| { - sweep_model::revolved(&entity, from, to, angle, start_angle) - .map(|solid| { - let history = - crate::scene::model::solid_history::brep_op(&solid); - (solid, history) - }) - }); + }); let Some((solid, history)) = result else { failed += 1; continue; @@ -436,8 +417,10 @@ impl OpenCADStudio { ); } else { self.command_line.push_error( - crate::t!("REVOLVE: no selected object could be revolved with the requested options.") - .as_ref(), + crate::t!( + "REVOLVE: no selected object could be revolved with the requested options." + ) + .as_ref(), ); } if let Some(pending) = pending { @@ -506,7 +489,8 @@ impl OpenCADStudio { let (path, options) = path.as_ref().zip(options).ok_or(None)?; if sweep_model::spatial_profile(&profile).is_some() { let record = sweep_model::spatial_sweep_record(&profile, path, options)?; - let body = kernel::acis::rebuild_sweep_with_mode(&record, true).map_err(|_| None)?; + let body = + kernel::acis::rebuild_sweep_with_mode(&record, true).map_err(|_| None)?; return Ok((body, record, true)); } let mut record = sweep_model::sweep_record(&profile, path, options).ok_or(None)?; @@ -517,18 +501,26 @@ impl OpenCADStudio { ) .map_err(|_| None)?; let surface = mode == ExtrudeMode::Surface || !closed; - let body = kernel::acis::rebuild_sweep_with_mode(&record, surface).map_err(|error| match error { - kernel::acis::HistoryRebuildError::Refused(why) => Some(sweep_model::sweep_refusal_code(why)), - _ => None, - })?; + let body = + kernel::acis::rebuild_sweep_with_mode(&record, surface).map_err(|error| { + match error { + kernel::acis::HistoryRebuildError::Refused(why) => { + Some(sweep_model::sweep_refusal_code(why)) + } + _ => None, + } + })?; Ok((body, record, surface)) })(); let (body, record, surface) = match result { Ok(value) => value, Err(code) => { if let Some(code) = code { - self.command_line.push_output(crate::t!("Modeling Operation Error:").as_ref()); - self.command_line.push_output(crate::tf!("Error Code Number is {code}", code = code).as_ref()); + self.command_line + .push_output(crate::t!("Modeling Operation Error:").as_ref()); + self.command_line.push_output( + crate::tf!("Error Code Number is {code}", code = code).as_ref(), + ); } failed += 1; continue; @@ -553,10 +545,20 @@ impl OpenCADStudio { if associative { // A picked base point, or the one several profiles share. let linked = crate::command::SweepOptions { - base_point: if picked_base || several { options.and_then(|o| o.base_point) } else { None }, + base_point: if picked_base || several { + options.and_then(|o| o.base_point) + } else { + None + }, ..picked }; - sweep_model::link_sweep_sources(&mut self.tabs[i].scene.document, created, handle, path_handle, &linked); + sweep_model::link_sweep_sources( + &mut self.tabs[i].scene.document, + created, + handle, + path_handle, + &linked, + ); } created } else { @@ -589,7 +591,13 @@ impl OpenCADStudio { self.tabs[i].dirty = true; } if failed > 0 { - self.command_line.push_output(crate::tf!("Unable to sweep {failed} selected objects.", failed = failed).as_ref()); + self.command_line.push_output( + crate::tf!( + "Unable to sweep {failed} selected objects.", + failed = failed + ) + .as_ref(), + ); } if let Some(pending) = pending { self.commit_undo_delta(i, pending); @@ -648,12 +656,14 @@ impl OpenCADStudio { }) .collect::>(); let result = if section_locked { - Err("LOFT: a source is on a locked layer; disable source deletion or unlock it.".to_string()) + Err( + "LOFT: a source is on a locked layer; disable source deletion or unlock it." + .to_string(), + ) } else { loft_command_model::record(§ions, &guides, path, &available, mode, options) .and_then(|record| { - kernel::acis::rebuild_loft_with_options(&record) - .map(|body| (body, record)) + kernel::acis::rebuild_loft_with_options(&record).map(|body| (body, record)) }) }; match result { @@ -679,10 +689,8 @@ impl OpenCADStudio { let dirty_before = self.tabs[i].dirty; let pending = self.begin_undo(i, "LOFT", 1, true); let created = if surface { - let handle = self.add_surface_model( - loft_command_model::surface_entity(&record), - body, - ); + let handle = + self.add_surface_model(loft_command_model::surface_entity(&record), body); if !handle.is_null() { self.tabs[i].scene.create_solid_history( handle, @@ -757,8 +765,7 @@ impl OpenCADStudio { else { unreachable!("router only routes the matching variant"); }; - let task = - self.solid_edge_blend(handle, &edges, base_face, value, other_value, fillet); + let task = self.solid_edge_blend(handle, &edges, base_face, value, other_value, fillet); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.tabs[i].scene.clear_preview_wire(); @@ -766,7 +773,12 @@ impl OpenCADStudio { task } - pub(super) fn handle_solid_shell(&mut self, handle: Handle, actions: Vec, distance: f64) -> Task { + pub(super) fn handle_solid_shell( + &mut self, + handle: Handle, + actions: Vec, + distance: f64, + ) -> Task { let i = self.active_tab; let task = self.solid_shell(handle, &actions, distance); self.tabs[i].active_cmd = None; @@ -776,7 +788,12 @@ impl OpenCADStudio { task } - pub(super) fn handle_solid_subtract(&mut self, bases: Vec, cutters: Vec, convert_meshes: bool) -> Task { + pub(super) fn handle_solid_subtract( + &mut self, + bases: Vec, + cutters: Vec, + convert_meshes: bool, + ) -> Task { let i = self.active_tab; let task = self.solid_subtract(&bases, &cutters, convert_meshes); self.tabs[i].active_cmd = None; @@ -786,7 +803,12 @@ impl OpenCADStudio { task } - pub(super) fn handle_slice_entities(&mut self, targets: Vec, plane: kernel::space::Plane, keep_point: Option) -> Task { + pub(super) fn handle_slice_entities( + &mut self, + targets: Vec, + plane: kernel::space::Plane, + keep_point: Option, + ) -> Task { let i = self.active_tab; let task = self.solid_slice(&targets, plane, keep_point); self.tabs[i].active_cmd = None; @@ -796,7 +818,12 @@ impl OpenCADStudio { task } - pub(super) fn handle_slice_surface_entities(&mut self, targets: Vec, cutter: Box, keep_point: Option) -> Task { + pub(super) fn handle_slice_surface_entities( + &mut self, + targets: Vec, + cutter: Box, + keep_point: Option, + ) -> Task { let i = self.active_tab; let task = self.solid_slice_surface(&targets, *cutter, keep_point); self.tabs[i].active_cmd = None; diff --git a/src/app/command_driver/tests.rs b/src/app/command_driver/tests.rs index 76ba84095..ef27ca475 100644 --- a/src/app/command_driver/tests.rs +++ b/src/app/command_driver/tests.rs @@ -94,12 +94,10 @@ mod parametric_constraint_undo_tests { fn add_line(app: &mut OpenCADStudio, x1: f64, y1: f64, x2: f64, y2: f64) -> Handle { app.tabs[app.active_tab] .scene - .add_entity(codec::EntityType::Line( - codec::entities::Line::from_points( - codec::types::Vector3::new(x1, y1, 0.0), - codec::types::Vector3::new(x2, y2, 0.0), - ), - )) + .add_entity(codec::EntityType::Line(codec::entities::Line::from_points( + codec::types::Vector3::new(x1, y1, 0.0), + codec::types::Vector3::new(x2, y2, 0.0), + ))) } fn line_angle_deg(app: &OpenCADStudio, handle: Handle) -> f64 { @@ -211,14 +209,11 @@ mod parametric_constraint_undo_tests { let mut app = OpenCADStudio::new_for_test(); let _ = app.automation_op(r#"{"op":"new"}"#); let handle = add_line(&mut app, 0.0, 0.0, 5.0, 2.0); - let (original_start, original_end, original_length) = match app.tabs[app.active_tab] - .scene - .document - .get_entity(handle) - { - Some(codec::EntityType::Line(line)) => (line.start, line.end, line.length()), - other => panic!("expected a Line, got {other:?}"), - }; + let (original_start, original_end, original_length) = + match app.tabs[app.active_tab].scene.document.get_entity(handle) { + Some(codec::EntityType::Line(line)) => (line.start, line.end, line.length()), + other => panic!("expected a Line, got {other:?}"), + }; let direction = codec::types::Vector3::new(3.0, 4.0, 0.0).normalize(); let _ = app.apply_cmd_result(CmdResult::AddHorizontalConstraint { @@ -264,7 +259,13 @@ mod parametric_constraint_undo_tests { Some(codec::EntityType::Line(line)) => line, other => panic!("expected a Line after redo, got {other:?}"), }; - assert!((line.end - line.start).normalize().cross(&direction).length() < 1.0e-7); + assert!( + (line.end - line.start) + .normalize() + .cross(&direction) + .length() + < 1.0e-7 + ); } #[test] @@ -832,9 +833,7 @@ mod parametric_constraint_undo_tests { else { panic!("expected second line"); }; - assert!( - (first.end - codec::types::Vector3::new(5.0, 0.0, 0.0)).length() < 1.0e-7 - ); + assert!((first.end - codec::types::Vector3::new(5.0, 0.0, 0.0)).length() < 1.0e-7); assert!((second.start - first.end).length() < 1.0e-7); assert!((second.length() - 5.0).abs() < 1.0e-7); } @@ -889,13 +888,12 @@ mod delobj_tests { let profile = app.tabs[i] .scene .add_entity(codec::EntityType::Circle(circle)); - let path = - app.tabs[i] - .scene - .add_entity(codec::EntityType::Line(codec::Line::from_points( - Vector3::new(0.0, 0.0, 0.0), - Vector3::new(0.0, 0.0, 5.0), - ))); + let path = app.tabs[i] + .scene + .add_entity(codec::EntityType::Line(codec::Line::from_points( + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(0.0, 0.0, 5.0), + ))); app.delete_objects = value; let _ = app.apply_cmd_result(CmdResult::SweepEntities { @@ -1211,9 +1209,9 @@ mod bedit_characterization_tests { fn add_circle_in_bedit(app: &mut OpenCADStudio) -> Handle { let tab = app.active_tab; - app.tabs[tab].scene.add_entity(EntityType::Circle(Circle::from_coords( - 0.0, 0.0, 0.0, 2.0, - ))) + app.tabs[tab] + .scene + .add_entity(EntityType::Circle(Circle::from_coords(0.0, 0.0, 0.0, 2.0))) } // ---- locked: must survive Phase 1 ---- @@ -1228,7 +1226,11 @@ mod bedit_characterization_tests { let _ = app.dispatch_command("BEDIT_SAVE"); assert!(app.tabs[app.active_tab].active_block_edit.is_none()); - assert_eq!(block_members(&app, "BLK").len(), 2, "save keeps the new entity"); + assert_eq!( + block_members(&app, "BLK").len(), + 2, + "save keeps the new entity" + ); } /// Locked: discard restores the definition to its entry state exactly @@ -1350,7 +1352,11 @@ mod bedit_characterization_tests { let _ = app.dispatch_command("BEDIT_DISCARD"); app.finish_pending_history(tab); - assert_eq!(definition_state(&app, "BLK"), entry, "discard restores entry"); + assert_eq!( + definition_state(&app, "BLK"), + entry, + "discard restores entry" + ); app.undo_steps(1); // Pinned: the state after one undo, observed on current code. @@ -1379,7 +1385,11 @@ mod bedit_characterization_tests { // its handle in the block record, so the definition holds a dangling // member. Expected to flip in Phase 1 (members should match the store). let members = block_members(&app, "BLK"); - assert_eq!(members.len(), 2, "documenting: block record keeps the undone handle"); + assert_eq!( + members.len(), + 2, + "documenting: block record keeps the undone handle" + ); let dangling = members .iter() .filter(|h| app.tabs[tab].scene.document.get_entity(**h).is_none()) diff --git a/src/app/command_driver/text_edit.rs b/src/app/command_driver/text_edit.rs index 1bc8f0f5f..86cb2b713 100644 --- a/src/app/command_driver/text_edit.rs +++ b/src/app/command_driver/text_edit.rs @@ -18,7 +18,12 @@ impl OpenCADStudio { self.open_mtext_editor(pos, handle, &initial, height, template.map(|m| *m)); } - pub(super) fn handle_suspend_for_mtext_input(&mut self, pos: glam::DVec3, initial: String, height: f64) { + pub(super) fn handle_suspend_for_mtext_input( + &mut self, + pos: glam::DVec3, + initial: String, + height: f64, + ) { let i = self.active_tab; self.tabs[i].suspended_cmd = self.tabs[i].active_cmd.take(); self.tabs[i].snap_result = None; @@ -185,7 +190,11 @@ impl OpenCADStudio { self.restore_pre_cmd_tangent(); } - pub(super) fn handle_ddedit_entity(&mut self, handle: Handle, new_text: String) -> Option> { + pub(super) fn handle_ddedit_entity( + &mut self, + handle: Handle, + new_text: String, + ) -> Option> { let i = self.active_tab; if self.reject_locked_edit(i, handle) { self.tabs[i].active_cmd = None; diff --git a/src/app/command_driver/transform.rs b/src/app/command_driver/transform.rs index c31d015fd..2a158a622 100644 --- a/src/app/command_driver/transform.rs +++ b/src/app/command_driver/transform.rs @@ -1,7 +1,11 @@ use super::*; impl OpenCADStudio { - pub(super) fn handle_transform_selected(&mut self, mut handles: Vec, transform: crate::command::EntityTransform) -> Option> { + pub(super) fn handle_transform_selected( + &mut self, + mut handles: Vec, + transform: crate::command::EntityTransform, + ) -> Option> { let i = self.active_tab; handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle)); if handles.is_empty() { @@ -34,7 +38,11 @@ impl OpenCADStudio { None } - pub(super) fn handle_copy_selected(&mut self, mut handles: Vec, transform: crate::command::EntityTransform) -> Option> { + pub(super) fn handle_copy_selected( + &mut self, + mut handles: Vec, + transform: crate::command::EntityTransform, + ) -> Option> { let i = self.active_tab; handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle)); if handles.is_empty() { @@ -88,10 +96,9 @@ impl OpenCADStudio { // it's delta-safe like a single transform. let pending = self.begin_undo(i, label, handles.len(), true); // Step 1: translate so src1 is at origin - self.tabs[i].scene.transform_entities( - &handles, - &crate::command::EntityTransform::Translate(-src1), - ); + self.tabs[i] + .scene + .transform_entities(&handles, &crate::command::EntityTransform::Translate(-src1)); // Step 2: uniform scale (only when != 1) if (scale - 1.0).abs() > 1e-4 { self.tabs[i].scene.transform_entities( @@ -114,10 +121,9 @@ impl OpenCADStudio { ); } // Step 4: translate to dst1 - self.tabs[i].scene.transform_entities( - &handles, - &crate::command::EntityTransform::Translate(dst1), - ); + self.tabs[i] + .scene + .transform_entities(&handles, &crate::command::EntityTransform::Translate(dst1)); self.tabs[i].dirty = true; self.tabs[i].scene.deselect_all(); for h in &handles { @@ -136,7 +142,11 @@ impl OpenCADStudio { } } - pub(super) fn handle_stretch_window(&mut self, mut handles: Vec, windows: Vec<(glam::DVec3, glam::DVec3)>) { + pub(super) fn handle_stretch_window( + &mut self, + mut handles: Vec, + windows: Vec<(glam::DVec3, glam::DVec3)>, + ) { let i = self.active_tab; // Accumulate every entity touched by any crossing window. Keep STRETCH // in its selection stage; Enter is what advances to the base point. @@ -249,7 +259,12 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(cmd)); } - pub(super) fn handle_stretch_entities(&mut self, mut handles: Vec, windows: Vec<(glam::DVec3, glam::DVec3)>, delta: glam::DVec3) -> Option> { + pub(super) fn handle_stretch_entities( + &mut self, + mut handles: Vec, + windows: Vec<(glam::DVec3, glam::DVec3)>, + delta: glam::DVec3, + ) -> Option> { let i = self.active_tab; handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle)); if handles.is_empty() { @@ -296,15 +311,10 @@ impl OpenCADStudio { } } codec::EntityType::LwPolyline(polyline) => { - if let Some(world) = - crate::entities::curve::lwpolyline_world_xy(polyline) - { + if let Some(world) = crate::entities::curve::lwpolyline_world_xy(polyline) { for (index, vertex) in world.vertices.iter().enumerate() { if in_win(vertex.location.x, vertex.location.y) { - driven_refs.push(ParametricRef::point( - *handle, - index as i32, - )); + driven_refs.push(ParametricRef::point(*handle, index as i32)); } } } @@ -312,10 +322,7 @@ impl OpenCADStudio { codec::EntityType::Polyline2D(polyline) => { for (index, vertex) in polyline.vertices.iter().enumerate() { if in_win(vertex.location.x, vertex.location.y) { - driven_refs.push(ParametricRef::point( - *handle, - index as i32, - )); + driven_refs.push(ParametricRef::point(*handle, index as i32)); } } } @@ -324,9 +331,7 @@ impl OpenCADStudio { { driven_refs.push(ParametricRef::center(*handle)); } - codec::EntityType::Arc(arc) - if in_win(arc.center.x, arc.center.y) => - { + codec::EntityType::Arc(arc) if in_win(arc.center.x, arc.center.y) => { driven_refs.push(ParametricRef::center(*handle)); } _ => {} @@ -354,8 +359,7 @@ impl OpenCADStudio { } } codec::EntityType::LwPolyline(p) => { - let Some(mut world) = crate::entities::curve::lwpolyline_world_xy(p) - else { + let Some(mut world) = crate::entities::curve::lwpolyline_world_xy(p) else { continue; }; for v in &mut world.vertices { diff --git a/src/app/command_driver/utilities.rs b/src/app/command_driver/utilities.rs index 84ee63d47..a06695e54 100644 --- a/src/app/command_driver/utilities.rs +++ b/src/app/command_driver/utilities.rs @@ -43,16 +43,10 @@ impl OpenCADStudio { return Some(glam::DVec3::new(line.end.x, line.end.y, line.end.z)); } codec::EntityType::Arc(_) => Some(2), - codec::EntityType::LwPolyline(polyline) => { - polyline.vertices.len().checked_sub(1) - } + codec::EntityType::LwPolyline(polyline) => polyline.vertices.len().checked_sub(1), codec::EntityType::Polyline(polyline) => polyline.vertices.len().checked_sub(1), - codec::EntityType::Polyline2D(polyline) => { - polyline.vertices.len().checked_sub(1) - } - codec::EntityType::Polyline3D(polyline) => { - polyline.vertices.len().checked_sub(1) - } + codec::EntityType::Polyline2D(polyline) => polyline.vertices.len().checked_sub(1), + codec::EntityType::Polyline3D(polyline) => polyline.vertices.len().checked_sub(1), _ => None, }?; crate::scene::view::dispatch::grips(entity) @@ -127,10 +121,16 @@ impl OpenCADStudio { ) { let touched: Vec<_> = grip.targets.iter().map(|target| target.handle).collect(); let connected = self.tabs[i].scene.parametric_connected_handles( - self.tabs[i].current_parametric_scope(), &touched, true, + self.tabs[i].current_parametric_scope(), + &touched, + true, ); for handle in connected { - if !self.grip_originals.iter().any(|(original, _)| *original == handle) { + if !self + .grip_originals + .iter() + .any(|(original, _)| *original == handle) + { if let Some(entity) = self.tabs[i].scene.document.get_entity(handle).cloned() { self.grip_originals.push((handle, entity)); } @@ -142,15 +142,29 @@ impl OpenCADStudio { } } } - let driven_refs: Vec<_> = grip.targets.iter().flat_map(|target| { - self.tabs[i].scene.document.get_entity(target.handle) - .map(|entity| crate::scene::parametric_constraints::grip_solve_anchor_refs( - entity, target.handle, target.grip_id, - )).unwrap_or_default() - }).collect(); + let driven_refs: Vec<_> = grip + .targets + .iter() + .flat_map(|target| { + self.tabs[i] + .scene + .document + .get_entity(target.handle) + .map(|entity| { + crate::scene::parametric_constraints::grip_solve_anchor_refs( + entity, + target.handle, + target.grip_id, + ) + }) + .unwrap_or_default() + }) + .collect(); let retain_size = self.constraint_solve_mode && !driven_refs.is_empty() - && driven_refs.iter().all(|reference| reference.marker.is_some()); + && driven_refs + .iter() + .all(|reference| reference.marker.is_some()); let solved = self.tabs[i].scene.solve_parametric_constraints_preview( &touched, &driven_refs, @@ -267,7 +281,9 @@ impl OpenCADStudio { .into_iter() .map(|handle| (handle, crate::scene::ChangeKind::Modified)) .collect(); - self.tabs[i].scene.bump_entities_after_parametric_solve(&changes); + self.tabs[i] + .scene + .bump_entities_after_parametric_solve(&changes); if let Some(dirty_before) = self.grip_dirty_before.take() { self.tabs[i].dirty = dirty_before; } @@ -535,7 +551,10 @@ impl OpenCADStudio { /// Only a `Text` token can come back unclaimed (`on_text_input` returning /// `None`), which is what lets the caller read it as something else — a /// typed distance, say — instead of guessing beforehand. - pub(in crate::app) fn feed_command_consumed(&mut self, input: StepInput) -> (Task, bool) { + pub(in crate::app) fn feed_command_consumed( + &mut self, + input: StepInput, + ) -> (Task, bool) { // A command that takes associative dimensions only keeps just those: // the rest leave the selection and the count of what went is reported, // as the reference does while gathering. @@ -769,7 +788,11 @@ impl OpenCADStudio { /// false: the line is one instalment and the tool stays at its next prompt /// for the following one. Only a plugin streaming a command feeds it that /// way; the command line and the automation feeder always finish. - pub(in crate::app) fn run_command_line_streaming(&mut self, cmd: &str, finish: bool) -> Task { + pub(in crate::app) fn run_command_line_streaming( + &mut self, + cmd: &str, + finish: bool, + ) -> Task { let i = self.active_tab; self.command_line.unconsumed.clear(); let tokens: Vec<&str> = cmd.split_whitespace().collect(); @@ -828,7 +851,11 @@ impl OpenCADStudio { /// silently dropped and `TEXT 0,0 5 0 hi` created nothing at all. /// * Anything still left over was never claimed by any prompt; it is /// recorded in [`CommandLine::unconsumed`] instead of vanishing. - pub(in crate::app) fn finish_active_command(&mut self, tokens: &[String], finish: bool) -> Task { + pub(in crate::app) fn finish_active_command( + &mut self, + tokens: &[String], + finish: bool, + ) -> Task { let i = self.active_tab; if self.tabs[i].active_cmd.is_none() { return Task::none(); diff --git a/src/app/command_driver/viewport.rs b/src/app/command_driver/viewport.rs index 8218d1a37..455960fd9 100644 --- a/src/app/command_driver/viewport.rs +++ b/src/app/command_driver/viewport.rs @@ -1,7 +1,11 @@ use super::*; impl OpenCADStudio { - pub(super) fn handle_mview_create(&mut self, viewport: codec::entities::Viewport, preserve_view: bool) { + pub(super) fn handle_mview_create( + &mut self, + viewport: codec::entities::Viewport, + preserve_view: bool, + ) { let i = self.active_tab; let saved_view = preserve_view.then(|| { ( @@ -58,21 +62,25 @@ impl OpenCADStudio { self.tabs[i].scene.clear_preview_wire(); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; - let Some(codec::EntityType::Viewport(vp)) = self.tabs[i].scene.document.get_entity(viewport).cloned() + let Some(codec::EntityType::Viewport(vp)) = + self.tabs[i].scene.document.get_entity(viewport).cloned() else { return; }; if boundary.is_none() && !boundary_handle.is_null() { let scene = &self.tabs[i].scene; let closed = scene.entity_belongs_to_current_layout(boundary_handle) - && scene.document.get_entity(boundary_handle).is_some_and(|entity| match entity { - codec::EntityType::Circle(_) => true, - codec::EntityType::Ellipse(ellipse) => ellipse.is_full(), - codec::EntityType::LwPolyline(polyline) => polyline.is_closed, - codec::EntityType::Polyline(polyline) => polyline.is_closed(), - codec::EntityType::Polyline2D(polyline) => polyline.is_closed(), - _ => false, - }); + && scene + .document + .get_entity(boundary_handle) + .is_some_and(|entity| match entity { + codec::EntityType::Circle(_) => true, + codec::EntityType::Ellipse(ellipse) => ellipse.is_full(), + codec::EntityType::LwPolyline(polyline) => polyline.is_closed, + codec::EntityType::Polyline(polyline) => polyline.is_closed(), + codec::EntityType::Polyline2D(polyline) => polyline.is_closed(), + _ => false, + }); if !closed { self.command_line .push_error(crate::t!("Object is not a closed curve.").as_ref()); @@ -100,13 +108,18 @@ impl OpenCADStudio { if created { scene.erase_entities(&[clip]); } - self.command_line - .push_error(crate::t!("MVIEW: the clipping boundary has no usable area.").as_ref()); + self.command_line.push_error( + crate::t!("MVIEW: the clipping boundary has no usable area.").as_ref(), + ); return; } // Model units per paper unit; the view centre follows the paper // centre so the model does not move on the sheet. - let scale = if vp.height.abs() > 1e-12 { vp.view_height / vp.height } else { 1.0 }; + let scale = if vp.height.abs() > 1e-12 { + vp.view_height / vp.height + } else { + 1.0 + }; let center = (lo + hi) * 0.5; let (dx, dy) = (center.x - vp.center.x, center.y - vp.center.y); let (s, c) = (-vp.twist_angle).sin_cos(); @@ -492,7 +505,10 @@ impl OpenCADStudio { /// Paper-space extents of a viewport clip boundary: exact for lines, arcs /// and circles (bulges included), else from its outline. -fn clip_boundary_extents(scene: &crate::scene::Scene, clip: Handle) -> Option<([f64; 2], [f64; 2])> { +fn clip_boundary_extents( + scene: &crate::scene::Scene, + clip: Handle, +) -> Option<([f64; 2], [f64; 2])> { let exact = scene .document .get_entity(clip) diff --git a/src/app/commands/attdef.rs b/src/app/commands/attdef.rs index 3fa2deef5..6126765ce 100644 --- a/src/app/commands/attdef.rs +++ b/src/app/commands/attdef.rs @@ -28,9 +28,16 @@ impl OpenCADStudio { Some(Task::none()) } "-ATTDEF" => { - let defaults = - crate::scene::creation_style::current_text_defaults(&self.tabs[i].scene.document); - let styles: Vec<_> = self.tabs[i].scene.document.text_styles.iter().cloned().collect(); + let defaults = crate::scene::creation_style::current_text_defaults( + &self.tabs[i].scene.document, + ); + let styles: Vec<_> = self.tabs[i] + .scene + .document + .text_styles + .iter() + .cloned() + .collect(); let command = AttdefCommand::new( "-ATTDEF", defaults.style_name, @@ -97,7 +104,8 @@ impl OpenCADStudio { Some(v) => match v.parse::().ok().filter(|v| *v <= 63) { Some(v) => attdef::session().aflags = v, None => { - self.command_line.push_error("Requires an integer between 0 and 63."); + self.command_line + .push_error("Requires an integer between 0 and 63."); self.command_line .push_output(&format!("Enter new value for AFLAGS <{current}>:")); self.pending_setvar = Some("AFLAGS".to_string()); @@ -145,7 +153,8 @@ impl OpenCADStudio { /// The Edit Attribute Definition dialog for `handle`. pub(in crate::app) fn open_attdef_edit(&mut self, handle: codec::Handle) { let i = self.active_tab; - let Some(codec::EntityType::AttributeDefinition(a)) = self.tabs[i].scene.document.get_entity(handle) + let Some(codec::EntityType::AttributeDefinition(a)) = + self.tabs[i].scene.document.get_entity(handle) else { return; }; @@ -175,11 +184,15 @@ impl OpenCADStudio { } AttdefDialogMsg::EditInsertField => { self.active_modal = None; - self.open_field_dialog(crate::ui::window::field_dialog::FieldTarget::AttdefEdit); + self.open_field_dialog( + crate::ui::window::field_dialog::FieldTarget::AttdefEdit, + ); } AttdefDialogMsg::Help => self.command_line.push_info( - crate::t!("Changes the tag, prompt and default value of an attribute definition.") - .as_ref(), + crate::t!( + "Changes the tag, prompt and default value of an attribute definition." + ) + .as_ref(), ), AttdefDialogMsg::EditOk => return self.attdef_edit_ok(i), AttdefDialogMsg::DismissError => edit.error = None, diff --git a/src/app/commands/blocks.rs b/src/app/commands/blocks.rs index 4c10b5e5f..a6739ab1e 100644 --- a/src/app/commands/blocks.rs +++ b/src/app/commands/blocks.rs @@ -38,8 +38,10 @@ impl OpenCADStudio { base: glam::DVec3, ) -> usize { // Clone + dep-capture live in the shared kernel; storage stays here. - let (entities, deps) = - super::super::command_driver::copy_to_clipboard_kernel(&self.tabs[i].scene.document, handles); + let (entities, deps) = super::super::command_driver::copy_to_clipboard_kernel( + &self.tabs[i].scene.document, + handles, + ); let count = entities.len(); self.clipboard_base = base; self.clipboard = entities; @@ -492,7 +494,8 @@ impl OpenCADStudio { // INSERT opens the Blocks palette on the drawing's blocks; // -INSERT runs on the command line. "INSERT" => { - self.command_line.push_output("*Insert a block from the Blocks palette"); + self.command_line + .push_output("*Insert a block from the Blocks palette"); self.open_blocks_palette(Some(crate::ui::window::block_palette::Tab::Current)); } @@ -519,7 +522,8 @@ impl OpenCADStudio { cmd if cmd.starts_with("_-INSERTFILE ") => { use crate::command::CadCommand; use crate::modules::insert::insert_block::InsertBlockCommand; - let path = std::path::PathBuf::from(cmd["_-INSERTFILE ".len()..].trim().trim_matches('"')); + let path = + std::path::PathBuf::from(cmd["_-INSERTFILE ".len()..].trim().trim_matches('"')); match self.import_file_as_block(path) { Ok(name) => { let units = self @@ -645,11 +649,10 @@ impl OpenCADStudio { .iter() .any(|a| a.tag.eq_ignore_ascii_case(tag)) { - ins.attributes - .push(codec::entities::AttributeEntity::new( - tag.clone(), - default.clone(), - )); + ins.attributes.push(codec::entities::AttributeEntity::new( + tag.clone(), + default.clone(), + )); } } synced += 1; @@ -702,7 +705,12 @@ impl OpenCADStudio { // A block picked in the ribbon gallery. cmd if cmd.starts_with("_BLOCKINSERT ") => { let name = cmd["_BLOCKINSERT ".len()..].trim().to_string(); - return Some(self.palette_insert(crate::ui::window::block_palette::Item::Current(name), None)); + return Some( + self.palette_insert( + crate::ui::window::block_palette::Item::Current(name), + None, + ), + ); } cmd if cmd.starts_with("_BLOCKSPALETTE ") => { use crate::ui::window::block_palette::Tab; @@ -737,10 +745,16 @@ impl OpenCADStudio { } // A file named after it skips the picker. cmd if cmd.starts_with("_PDFIMPORTFILE ") => { - let path = cmd["_PDFIMPORTFILE ".len()..].trim().trim_matches('"').to_string(); + let path = cmd["_PDFIMPORTFILE ".len()..] + .trim() + .trim_matches('"') + .to_string(); match std::fs::read(&path) { Ok(bytes) => { - crate::scene::model::pdf_raster::register_source(&path, std::sync::Arc::new(bytes)); + crate::scene::model::pdf_raster::register_source( + &path, + std::sync::Arc::new(bytes), + ); self.open_pdf_import_file(&path); } Err(_) => self.command_line.push_error(&format!("{path} not found.")), @@ -760,8 +774,11 @@ impl OpenCADStudio { "POINTCLOUDCROP" | "POINTCLOUDUNCROP" => { use crate::command::CadCommand; use crate::modules::insert::pc_crop::PointCloudCropCommand; - let command = - if cmd == "POINTCLOUDUNCROP" { PointCloudCropCommand::uncrop() } else { PointCloudCropCommand::new() }; + let command = if cmd == "POINTCLOUDUNCROP" { + PointCloudCropCommand::uncrop() + } else { + PointCloudCropCommand::new() + }; self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); } @@ -810,7 +827,10 @@ impl OpenCADStudio { // PDFATTACH picks the file; -PDFATTACH asks for it on the command // line. A path after either names the file at once. cmd if matches!( - cmd.split_whitespace().next().map(|v| v.to_ascii_uppercase()).as_deref(), + cmd.split_whitespace() + .next() + .map(|v| v.to_ascii_uppercase()) + .as_deref(), Some("PDFATTACH") | Some("-PDFATTACH") ) => { @@ -859,14 +879,12 @@ impl OpenCADStudio { } else { format!("{}/new_block.dwg", default_folder.replace('\\', "/")) }; - self.wblock = Some( - crate::ui::window::wblock::WblockState::new( - existing_blocks, - handles, - default_unit, - default_path, - ), - ); + self.wblock = Some(crate::ui::window::wblock::WblockState::new( + existing_blocks, + handles, + default_unit, + default_path, + )); self.active_modal = Some(crate::app::ModalKind::WriteBlock); } @@ -1079,8 +1097,7 @@ impl OpenCADStudio { self.tabs[i].xref_unloaded.remove(key); self.tabs[i].xref_stat_cache.remove(key); } - let handles: rustc_hash::FxHashSet = self.tabs - [i] + let handles: rustc_hash::FxHashSet = self.tabs[i] .scene .document .block_records @@ -1928,12 +1945,8 @@ mod tests { .objects .insert(h, ObjectType::ImageDefinition(def)); // Reference it so collect_entries lists it. - let mut img = codec::entities::RasterImage::new( - "img.png", - codec::types::Vector3::ZERO, - 8.0, - 8.0, - ); + let mut img = + codec::entities::RasterImage::new("img.png", codec::types::Vector3::ZERO, 8.0, 8.0); img.definition_handle = Some(h); app.tabs[i] .scene @@ -1963,12 +1976,8 @@ mod tests { .document .objects .insert(h, ObjectType::ImageDefinition(def)); - let mut img = codec::entities::RasterImage::new( - "img.png", - codec::types::Vector3::ZERO, - 8.0, - 8.0, - ); + let mut img = + codec::entities::RasterImage::new("img.png", codec::types::Vector3::ZERO, 8.0, 8.0); img.definition_handle = Some(h); app.tabs[i] .scene @@ -2069,9 +2078,7 @@ mod tests { .unwrap(); let mut line = codec::entities::Line::new(); line.common.layer = "WALLS".to_string(); - xref_doc - .add_entity(codec::EntityType::Line(line)) - .unwrap(); + xref_doc.add_entity(codec::EntityType::Line(line)).unwrap(); let bytes = crate::io::save_to_bytes(&xref_doc, "dwg", xref_doc.version).unwrap(); let xref_path = dir.join("plan.dwg"); std::fs::write(&xref_path, &bytes).unwrap(); @@ -2104,9 +2111,7 @@ mod tests { let mut line = codec::entities::Line::new(); line.common.plotstyle_flags = 0b11; line.common.plotstyle_handle = Some(xref_doc.allocate_handle()); - xref_doc - .add_entity(codec::EntityType::Line(line)) - .unwrap(); + xref_doc.add_entity(codec::EntityType::Line(line)).unwrap(); let bytes = crate::io::save_to_bytes(&xref_doc, "dwg", xref_doc.version).unwrap(); let xref_path = dir.join("plan.dwg"); std::fs::write(&xref_path, &bytes).unwrap(); @@ -2347,9 +2352,7 @@ mod tests { .unwrap(); let mut line = codec::entities::Line::new(); line.common.layer = "WALLS".to_string(); - xref_doc - .add_entity(codec::EntityType::Line(line)) - .unwrap(); + xref_doc.add_entity(codec::EntityType::Line(line)).unwrap(); let bytes = crate::io::save_to_bytes(&xref_doc, "dwg", xref_doc.version).unwrap(); let xref_path = dir.join("plan.dwg"); std::fs::write(&xref_path, &bytes).unwrap(); diff --git a/src/app/commands/blockvars.rs b/src/app/commands/blockvars.rs index fae4f4eae..8a19cde58 100644 --- a/src/app/commands/blockvars.rs +++ b/src/app/commands/blockvars.rs @@ -42,7 +42,8 @@ impl OpenCADStudio { match name.as_str() { "BLOCKSTATE" => { let state = self.tabs[i].refedit_session.is_some() as u8; - self.command_line.push_output(&format!("BLOCKSTATE = {state} (read only)")); + self.command_line + .push_output(&format!("BLOCKSTATE = {state} (read only)")); } "INSNAME" | "BLOCKNAVIGATE" => { let current = if name == "INSNAME" { @@ -52,7 +53,11 @@ impl OpenCADStudio { }; match value { Some(v) => { - let v = if v == "." { String::new() } else { v.trim_matches('"').to_string() }; + let v = if v == "." { + String::new() + } else { + v.trim_matches('"').to_string() + }; if name == "INSNAME" { self.insname = v; } else { diff --git a/src/app/commands/count.rs b/src/app/commands/count.rs index cdd4243a4..157ebfe42 100644 --- a/src/app/commands/count.rs +++ b/src/app/commands/count.rs @@ -6,14 +6,21 @@ use crate::app::{Message, OpenCADStudio}; use crate::command::CadCommand; use crate::modules::annotate::count_cmd::{CountCommand, CountTableCommand, UpdateFieldCommand}; use crate::modules::annotate::field_cmd::FieldPlaceCommand; -use crate::ui::window::count_palette::{aci_rgba, CountMode, CountMsg, CountTarget, RowAction, AREA_LAYER}; +use crate::ui::window::count_palette::{ + aci_rgba, CountMode, CountMsg, CountTarget, RowAction, AREA_LAYER, +}; use codec::count::{area_json, block_json, single_json, CountKey}; use codec::{EntityType, Handle}; use iced::Task; /// The variables this family answers for. -pub(super) const COUNT_SYSVARS: &[&str] = - &["COUNTNUMBER", "COUNTSERVICE", "COUNTPALETTESTATE", "COUNTCOLOR", "COUNTERRORCOLOR"]; +pub(super) const COUNT_SYSVARS: &[&str] = &[ + "COUNTNUMBER", + "COUNTSERVICE", + "COUNTPALETTESTATE", + "COUNTCOLOR", + "COUNTERRORCOLOR", +]; fn hex(h: Handle) -> String { format!("{:X}", h.value()) @@ -56,18 +63,27 @@ impl OpenCADStudio { match verb.as_str() { "COUNT" => { let preselected = self.tabs[i].scene.selected_handles_in_order(); - let current = matches!(rest.trim_start_matches('_').to_ascii_uppercase().as_str(), "C"); + let current = matches!( + rest.trim_start_matches('_').to_ascii_uppercase().as_str(), + "C" + ); if current && !preselected.is_empty() { // Count Selection: the selection is the target. let found = preselected.len(); self.command_line.push_output(&crate::tf!("{found} found")); let handles: Vec = preselected.iter().map(|h| hex(*h)).collect(); self.tabs[i].scene.deselect_all(); - return Some(self.dispatch_command(&format!("_COUNTRUN C T {}", handles.join(",")))); + return Some( + self.dispatch_command(&format!("_COUNTRUN C T {}", handles.join(","))), + ); } // Plain COUNT asks for its targets itself. self.tabs[i].scene.deselect_all(); - let command = if current { CountCommand::with_area("C") } else { CountCommand::new(false) }; + let command = if current { + CountCommand::with_area("C") + } else { + CountCommand::new(false) + }; Some(self.start_count_command(i, Box::new(command))) } "COUNTAREA" => Some(self.start_count_command(i, Box::new(CountCommand::new(true)))), @@ -92,9 +108,9 @@ impl OpenCADStudio { match self.count_field_code(i) { Some(code) => return Some(self.start_count_field(i, code)), None if self.tabs[i].count.is_some() => {} - None => self - .command_line - .push_error(&crate::t!("** COUNTFIELD command only available during Count. **")), + None => self.command_line.push_error(&crate::t!( + "** COUNTFIELD command only available during Count. **" + )), } Some(Task::none()) } @@ -150,7 +166,8 @@ impl OpenCADStudio { mode.cursor = None; self.tabs[i].count = Some(mode); self.apply_count_display(i); - self.command_line.push_output(&crate::t!("count area is active.")); + self.command_line + .push_output(&crate::t!("count area is active.")); } Some(Task::none()) } @@ -158,7 +175,11 @@ impl OpenCADStudio { let (point, names) = rest.split_once(' ').unwrap_or((rest, "")); let p: Vec = point.split(',').filter_map(|v| v.parse().ok()).collect(); if p.len() == 3 { - let names: Vec = names.split('|').filter(|n| !n.is_empty()).map(str::to_string).collect(); + let names: Vec = names + .split('|') + .filter(|n| !n.is_empty()) + .map(str::to_string) + .collect(); self.place_count_table(i, [p[0], p[1], p[2]], names); } Some(Task::none()) @@ -193,7 +214,8 @@ impl OpenCADStudio { _ => None, }; if let Some(v) = read_only { - self.command_line.push_output(&crate::tf!("{name} = {v} (read only)")); + self.command_line + .push_output(&crate::tf!("{name} = {v} (read only)")); return; } let (current, min, max) = match name { @@ -202,7 +224,8 @@ impl OpenCADStudio { _ => (self.count_palette.error_color as i32, 1, 255), }; let ask = |app: &mut Self| { - app.command_line.push_output(&crate::tf!("Enter new value for {name} <{current}>:")); + app.command_line + .push_output(&crate::tf!("Enter new value for {name} <{current}>:")); app.pending_setvar = Some(name.to_string()); }; let Some(value) = value else { @@ -237,7 +260,8 @@ impl OpenCADStudio { self.ribbon.set_count_palette(open); if open { if self.dock.location(id).is_none() { - self.dock.dock(id, crate::app::config::DockSide::Right, usize::MAX); + self.dock + .dock(id, crate::app::config::DockSide::Right, usize::MAX); } self.dock_expanded = Some(id); } else if self.dock_expanded == Some(id) { @@ -248,19 +272,26 @@ impl OpenCADStudio { /// Recolour the drawing for count mode: counted references, duplicates, /// the rest faded (nothing changes without a target). pub(in crate::app) fn apply_count_display(&mut self, i: usize) { - let computed = self.tabs[i].count.as_ref().map(|m| m.compute(&self.tabs[i].scene.document)); + let computed = self.tabs[i] + .count + .as_ref() + .map(|m| m.compute(&self.tabs[i].scene.document)); if let (Some(mode), Some(c)) = (self.tabs[i].count.as_mut(), computed) { mode.cache = Some(c); } - let display = self.tabs[i].count.as_ref().filter(|m| m.target.is_some()).map(|m| { - let r = m.result(&self.tabs[i].scene.document); - crate::scene::CountDisplay { - counted: r.counted.iter().copied().collect(), - errors: r.errors.iter().chain(&r.overlapped).copied().collect(), - color: aci_rgba(self.count_palette.color), - error_color: aci_rgba(self.count_palette.error_color), - } - }); + let display = self.tabs[i] + .count + .as_ref() + .filter(|m| m.target.is_some()) + .map(|m| { + let r = m.result(&self.tabs[i].scene.document); + crate::scene::CountDisplay { + counted: r.counted.iter().copied().collect(), + errors: r.errors.iter().chain(&r.overlapped).copied().collect(), + color: aci_rgba(self.count_palette.color), + error_color: aci_rgba(self.count_palette.error_color), + } + }); self.tabs[i].scene.set_count_display(display); let epoch = self.tabs[i].scene.geometry_epoch; if let Some(mode) = self.tabs[i].count.as_mut() { @@ -271,7 +302,11 @@ impl OpenCADStudio { /// After the drawing changes, count mode colours its references again. pub(in crate::app) fn refresh_count_if_stale(&mut self) { let i = self.active_tab; - let Some(mode) = self.tabs[i].count.as_ref().filter(|m| m.epoch != self.tabs[i].scene.geometry_epoch) else { + let Some(mode) = self.tabs[i] + .count + .as_ref() + .filter(|m| m.epoch != self.tabs[i].scene.geometry_epoch) + else { return; }; if let Some((h, _)) = mode.boundary { @@ -294,7 +329,11 @@ impl OpenCADStudio { } // An edited boundary moves the area with it. Some(EntityType::LwPolyline(pl)) => { - let ring: Vec<[f64; 2]> = pl.vertices.iter().map(|v| [v.location.x, v.location.y]).collect(); + let ring: Vec<[f64; 2]> = pl + .vertices + .iter() + .map(|v| [v.location.x, v.location.y]) + .collect(); if let Some(mode) = self.tabs[i].count.as_mut() { mode.area = Some(ring); } @@ -388,12 +427,20 @@ impl OpenCADStudio { tokens: &[&str], ) -> Option<(Option>, Option<(Handle, bool)>)> { let kind = tokens.first().copied().unwrap_or("E"); - let points = || tokens[1..].iter().map_while(|t| parse_xy(t)).collect::>(); + let points = || { + tokens[1..] + .iter() + .map_while(|t| parse_xy(t)) + .collect::>() + }; match kind { "E" => Some((None, None)), "K" => { let mode = self.tabs[i].count.as_ref(); - Some((mode.and_then(|m| m.area.clone()), mode.and_then(|m| m.boundary))) + Some(( + mode.and_then(|m| m.area.clone()), + mode.and_then(|m| m.boundary), + )) } // Current area counts all of model space (the view and an area // in use take no part). @@ -450,7 +497,10 @@ impl OpenCADStudio { /// counts the targets and enters count mode. fn count_run(&mut self, i: usize, rest: &str) { let tokens: Vec<&str> = rest.split_whitespace().collect(); - let split = tokens.iter().position(|t| *t == "L" || *t == "T").unwrap_or(tokens.len()); + let split = tokens + .iter() + .position(|t| *t == "L" || *t == "T") + .unwrap_or(tokens.len()); let (area_tokens, tail) = tokens.split_at(split); let targets: Vec = match tail { ["T", handles, ..] => handles @@ -479,9 +529,21 @@ impl OpenCADStudio { }) .collect(); let target = match blocks.first() { - Some(first) if blocks.len() == targets.len() && blocks.iter().all(|b| b.eq_ignore_ascii_case(first)) => { - let name = doc.block_records.get(first).map(|b| b.name.clone()).unwrap_or_else(|| first.to_string()); - Some(CountTarget::Block { name, reference: targets.first().copied(), matching: [false; 3], picked: true }) + Some(first) + if blocks.len() == targets.len() + && blocks.iter().all(|b| b.eq_ignore_ascii_case(first)) => + { + let name = doc + .block_records + .get(first) + .map(|b| b.name.clone()) + .unwrap_or_else(|| first.to_string()); + Some(CountTarget::Block { + name, + reference: targets.first().copied(), + matching: [false; 3], + picked: true, + }) } None if targets.is_empty() => None, _ => Some(CountTarget::Group(targets.clone())), @@ -492,10 +554,16 @@ impl OpenCADStudio { if let Some(old) = self.tabs[i].count.take().filter(|o| o.boundary != boundary) { self.drop_count_boundary(i, old.boundary); } - let mode = CountMode { area, boundary, target, ..CountMode::default() }; + let mode = CountMode { + area, + boundary, + target, + ..CountMode::default() + }; if let Some(name) = mode.target_name() { let n = mode.count(&self.tabs[i].scene.document); - self.command_line.push_output(&crate::tf!("{name} ...... {n}")); + self.command_line + .push_output(&crate::tf!("{name} ...... {n}")); } self.tabs[i].count = Some(mode); self.apply_count_display(i); @@ -508,11 +576,12 @@ impl OpenCADStudio { let mode = self.tabs[i].count.as_ref()?; match mode.target.as_ref()? { CountTarget::Group(handles) => Some(format!("\\AcCount {}", single_json(handles))), - CountTarget::Block { reference: Some(r), matching: [false, false, false], picked: true, .. } - if mode.boundary.is_none() => - { - Some(format!("\\AcCount {}", single_json(&[*r]))) - } + CountTarget::Block { + reference: Some(r), + matching: [false, false, false], + picked: true, + .. + } if mode.boundary.is_none() => Some(format!("\\AcCount {}", single_json(&[*r]))), CountTarget::Block { name, .. } => { let instances = mode.instances(&self.tabs[i].scene.document); Some(self.block_field_code(i, name, &mode.key(&instances))) @@ -531,13 +600,21 @@ impl OpenCADStudio { /// Place a count field as multi-line text (`Specify start point or [Height/Justify]:`). pub(in crate::app) fn start_count_field(&mut self, i: usize, code: String) -> Task { let doc = &self.tabs[i].scene.document; - let value = crate::entities::field::evaluate(doc, &code, &[], None).unwrap_or_else(|| "####".into()); + let value = crate::entities::field::evaluate(doc, &code, &[], None) + .unwrap_or_else(|| "####".into()); let defaults = crate::scene::creation_style::current_text_defaults(doc); self.command_line.push_output(&crate::tf!( "MTEXT Current text style: \"{}\" Text height: {:.4}", - defaults.style_name, defaults.height + defaults.style_name, + defaults.height )); - let command = FieldPlaceCommand::new(value, (code, Vec::new()), defaults.style_name, defaults.height).named("MTEXT"); + let command = FieldPlaceCommand::new( + value, + (code, Vec::new()), + defaults.style_name, + defaults.height, + ) + .named("MTEXT"); self.start_count_command(i, Box::new(command)) } @@ -563,7 +640,11 @@ impl OpenCADStudio { Some(EntityType::Table(t)) => t .block_record_handle .and_then(|r| doc.block_records.iter().find(|b| b.handle == r)) - .or_else(|| doc.block_records.get(&t.block_name).filter(|_| !t.block_name.is_empty())), + .or_else(|| { + doc.block_records + .get(&t.block_name) + .filter(|_| !t.block_name.is_empty()) + }), _ => None, }; if let Some(record) = record { @@ -573,7 +654,9 @@ impl OpenCADStudio { let attributes: Vec<&codec::entities::EntityCommon> = handles .iter() .filter_map(|h| match doc.get_entity(*h) { - Some(EntityType::Insert(insert)) => Some(insert.attributes.iter().map(|a| &a.common)), + Some(EntityType::Insert(insert)) => { + Some(insert.attributes.iter().map(|a| &a.common)) + } _ => None, }) .flatten() @@ -585,8 +668,10 @@ impl OpenCADStudio { }), }); if !hosting { - self.command_line.push_output(&crate::tf!("{found} field(s) found.", found = 0)); - self.command_line.push_output(&crate::tf!("{updated} field(s) updated.", updated = 0)); + self.command_line + .push_output(&crate::tf!("{found} field(s) found.", found = 0)); + self.command_line + .push_output(&crate::tf!("{updated} field(s) updated.", updated = 0)); return; } // The reference counts fields, not the texts holding them, and @@ -595,8 +680,10 @@ impl OpenCADStudio { if changed > 0 { self.tabs[i].dirty = true; } - self.command_line.push_output(&crate::tf!("{found} field(s) found.")); - self.command_line.push_output(&crate::tf!("{updated} field(s) updated.", updated = found)); + self.command_line + .push_output(&crate::tf!("{found} field(s) found.")); + self.command_line + .push_output(&crate::tf!("{updated} field(s) updated.", updated = found)); } /// A count table at `point`: `Item` | `Count`, one row per block (all @@ -607,11 +694,19 @@ impl OpenCADStudio { let area = self.tabs[i].count.as_ref().and_then(|m| m.area.clone()); let instances = codec::count::block_instances(doc, area.as_deref()); let mut names: Vec = if names.is_empty() { - codec::count::block_counts(&instances).into_iter().map(|c| c.name).collect() + codec::count::block_counts(&instances) + .into_iter() + .map(|c| c.name) + .collect() } else { names .iter() - .map(|n| doc.block_records.get(n).map(|b| b.name.clone()).unwrap_or_else(|| n.clone())) + .map(|n| { + doc.block_records + .get(n) + .map(|b| b.name.clone()) + .unwrap_or_else(|| n.clone()) + }) .collect() }; names.sort_by_key(|n| n.to_ascii_uppercase()); @@ -659,8 +754,10 @@ impl OpenCADStudio { } } } - let codes: Vec = - names.iter().map(|n| self.block_field_code(i, n, &CountKey::default())).collect(); + let codes: Vec = names + .iter() + .map(|n| self.block_field_code(i, n, &CountKey::default())) + .collect(); self.push_undo_snapshot(i, "COUNTTABLE"); let Some(handle) = self.commit_entity_handle(EntityType::Table(Box::new(table))) else { return; @@ -672,19 +769,29 @@ impl OpenCADStudio { .iter() .enumerate() .map(|(r, code)| { - let value = crate::entities::field::evaluate(doc, code, &[], None).unwrap_or_else(|| "####".into()); - (r + 1, 1, r"%<\_FldIdx 0>%".to_string(), vec![codec::fields::NewField::new(code.clone(), value)]) + let value = crate::entities::field::evaluate(doc, code, &[], None) + .unwrap_or_else(|| "####".into()); + ( + r + 1, + 1, + r"%<\_FldIdx 0>%".to_string(), + vec![codec::fields::NewField::new(code.clone(), value)], + ) }) .collect(); doc.build_table_block(handle, fields); - self.tabs[i].scene.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]); + self.tabs[i] + .scene + .bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]); self.tabs[i].dirty = true; } /// Zoom to a counted object (← / →, an error of the report). fn zoom_to_insert(&mut self, i: usize, h: Handle) { let doc = &self.tabs[i].scene.document; - let Some(entity) = doc.get_entity(h) else { return }; + let Some(entity) = doc.get_entity(h) else { + return; + }; let outline = codec::count::entity_outline(doc, entity); if outline.is_empty() { return; @@ -704,12 +811,20 @@ impl OpenCADStudio { let instances = mode.instances(&self.tabs[i].scene.document); let reference = instances .iter() - .find(|inst| inst.duplicate_of.is_none() && inst.name.eq_ignore_ascii_case(&name) && key.matches(inst)) + .find(|inst| { + inst.duplicate_of.is_none() + && inst.name.eq_ignore_ascii_case(&name) + && key.matches(inst) + }) .map(|inst| inst.handle); mode.target = Some(CountTarget::Block { name, reference, - matching: [key.layer.is_some(), key.scale.is_some(), key.mirror_state.is_some()], + matching: [ + key.layer.is_some(), + key.scale.is_some(), + key.mirror_state.is_some(), + ], picked: false, }); mode.cursor = None; @@ -721,9 +836,15 @@ impl OpenCADStudio { /// expression counts. pub(in crate::app) fn show_count_instances(&mut self, i: usize, json: &str) { match codec::count::parse_query(json) { - Some(codec::count::CountQuery::Block { name, key, boundary }) => { + Some(codec::count::CountQuery::Block { + name, + key, + boundary, + }) => { if let Some(b) = boundary { - if let Some(ring) = codec::count::boundary_polygon(&self.tabs[i].scene.document, b) { + if let Some(ring) = + codec::count::boundary_polygon(&self.tabs[i].scene.document, b) + { let mut mode = self.tabs[i].count.take().unwrap_or_default(); if mode.boundary.map(|(h, _)| h) != Some(b) { self.drop_count_boundary(i, mode.boundary); @@ -737,7 +858,11 @@ impl OpenCADStudio { } Some(codec::count::CountQuery::Single(handles)) if !handles.is_empty() => { let hexes: Vec = handles.iter().map(|h| hex(*h)).collect(); - let area = if self.tabs[i].count.is_some() { "K" } else { "E" }; + let area = if self.tabs[i].count.is_some() { + "K" + } else { + "E" + }; self.count_run(i, &format!("{area} T {}", hexes.join(","))); } _ => {} @@ -793,7 +918,10 @@ impl OpenCADStudio { CountMsg::TableAll(on) => { let names: Vec = if on { let area = self.tabs[i].count.as_ref().and_then(|m| m.area.clone()); - let instances = codec::count::block_instances(&self.tabs[i].scene.document, area.as_deref()); + let instances = codec::count::block_instances( + &self.tabs[i].scene.document, + area.as_deref(), + ); crate::ui::window::count_palette::rows(&instances, &self.count_palette) .into_iter() .map(|r| r.name.to_ascii_uppercase()) @@ -813,16 +941,26 @@ impl OpenCADStudio { } CountMsg::Back | CountMsg::Close => self.close_count(i), CountMsg::Match(k, on) => { - if let Some(CountMode { target: Some(CountTarget::Block { matching, .. }), .. }) = self.tabs[i].count.as_mut() { + if let Some(CountMode { + target: Some(CountTarget::Block { matching, .. }), + .. + }) = self.tabs[i].count.as_mut() + { matching[k] = on; } self.apply_count_display(i); } - CountMsg::ToggleDetails => self.count_palette.details_closed = !self.count_palette.details_closed, - CountMsg::ToggleErrors => self.count_palette.errors_closed = !self.count_palette.errors_closed, + CountMsg::ToggleDetails => { + self.count_palette.details_closed = !self.count_palette.details_closed + } + CountMsg::ToggleErrors => { + self.count_palette.errors_closed = !self.count_palette.errors_closed + } CountMsg::ShowError(h) => self.zoom_to_insert(i, h), CountMsg::Prev | CountMsg::Next => { - let Some(mode) = self.tabs[i].count.as_ref() else { return Task::none() }; + let Some(mode) = self.tabs[i].count.as_ref() else { + return Task::none(); + }; let result = mode.result(&self.tabs[i].scene.document); let n = result.counted.len(); if n == 0 { @@ -843,7 +981,11 @@ impl OpenCADStudio { CountMsg::Area => return self.dispatch_command("COUNTAREA"), CountMsg::Select => { self.tabs[i].scene.deselect_all(); - let command = CountCommand::with_area(if self.tabs[i].count.is_some() { "K" } else { "C" }); + let command = CountCommand::with_area(if self.tabs[i].count.is_some() { + "K" + } else { + "C" + }); return self.start_count_command(i, Box::new(command)); } CountMsg::Field => return self.dispatch_command("COUNTFIELD"), diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs index 8aa821cd5..112981216 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -30,7 +30,7 @@ fn solid_edge_sources( entity, codec::EntityType::Solid3D(_) | codec::EntityType::Surface(_) ) - .then_some(entity.common().handle) + .then_some(entity.common().handle) }) .collect::>(); app.tabs[tab].scene.restore_solid_models(&handles); @@ -199,8 +199,9 @@ impl OpenCADStudio { _ => None, }; if text_align.is_none() && mtext_ap.is_none() { - self.command_line - .push_error(crate::t!("JUSTIFYTEXT: unknown justification option.").as_ref()); + self.command_line.push_error( + crate::t!("JUSTIFYTEXT: unknown justification option.").as_ref(), + ); return Some(Task::none()); } self.push_undo_snapshot(i, "JUSTIFYTEXT"); @@ -358,10 +359,7 @@ impl OpenCADStudio { .selected_entities() .iter() .filter(|(_, e)| { - matches!( - e, - codec::EntityType::Text(_) | codec::EntityType::MText(_) - ) + matches!(e, codec::EntityType::Text(_) | codec::EntityType::MText(_)) }) .map(|(h, _)| *h) .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle)) @@ -421,8 +419,7 @@ impl OpenCADStudio { "TEXTFIT" => { use crate::command::SelectThenValueCommand; let has_sel = !self.tabs[i].scene.selected_entities().is_empty(); - let c = - SelectThenValueCommand::new("TEXTFIT", "TEXTFIT target width:", has_sel); + let c = SelectThenValueCommand::new("TEXTFIT", "TEXTFIT target width:", has_sel); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } @@ -485,9 +482,9 @@ impl OpenCADStudio { .map(|handle| (handle, crate::scene::ChangeKind::Modified)) .collect(); self.tabs[i].scene.bump_entities(&changes); - self.command_line.push_output(crate::tf!( - "TEXTFIT: fitted {n} text object(s) to width {target}." - ).as_ref()); + self.command_line.push_output( + crate::tf!("TEXTFIT: fitted {n} text object(s) to width {target}.").as_ref(), + ); } // (text sequential numbering) @@ -505,20 +502,11 @@ impl OpenCADStudio { cmd if cmd.starts_with("TCOUNT ") => { let mut args = cmd.split_whitespace().skip(1); - let start: i64 = args - .next() - .and_then(|s| s.parse().ok()) - .unwrap_or(1); + let start: i64 = args.next().and_then(|s| s.parse().ok()).unwrap_or(1); - let increment: i64 = args - .next() - .and_then(|s| s.parse().ok()) - .unwrap_or(1); + let increment: i64 = args.next().and_then(|s| s.parse().ok()).unwrap_or(1); - let placement = args - .next() - .unwrap_or("O") - .to_uppercase(); + let placement = args.next().unwrap_or("O").to_uppercase(); let placement = match placement.as_str() { "O" | "OVERWRITE" => "O", @@ -538,9 +526,7 @@ impl OpenCADStudio { .selected_entities() .iter() .filter_map(|(h, e)| match e { - codec::EntityType::Text(t) - if !self.tabs[i].scene.is_layer_locked(*h) => - { + codec::EntityType::Text(t) if !self.tabs[i].scene.is_layer_locked(*h) => { Some((*h, t.insertion_point.x, t.insertion_point.y)) } _ => None, @@ -558,10 +544,7 @@ impl OpenCADStudio { texts.sort_by(|a, b| { b.2.partial_cmp(&a.2) .unwrap_or(std::cmp::Ordering::Equal) - .then( - a.1.partial_cmp(&b.1) - .unwrap_or(std::cmp::Ordering::Equal), - ) + .then(a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)) }); self.push_undo_snapshot(i, "TCOUNT"); @@ -589,9 +572,7 @@ impl OpenCADStudio { let changes: Vec<_> = texts .iter() - .map(|(handle, _, _)| { - (*handle, crate::scene::ChangeKind::Modified) - }) + .map(|(handle, _, _)| (*handle, crate::scene::ChangeKind::Modified)) .collect(); self.tabs[i].scene.bump_entities(&changes); @@ -618,21 +599,22 @@ impl OpenCADStudio { .header .current_mleader_style_name .clone(); - let style = self.tabs[i] - .scene - .document - .objects - .iter() - .find_map(|(handle, object)| match object { - codec::objects::ObjectType::MultiLeaderStyle(style) - if style.name.eq_ignore_ascii_case(&name) => - { - let mut style = style.clone(); - style.handle = *handle; - Some(style) - } - _ => None, - }); + let style = + self.tabs[i] + .scene + .document + .objects + .iter() + .find_map(|(handle, object)| match object { + codec::objects::ObjectType::MultiLeaderStyle(style) + if style.name.eq_ignore_ascii_case(&name) => + { + let mut style = style.clone(); + style.handle = *handle; + Some(style) + } + _ => None, + }); let multiplier = self.tabs[i].scene.creation_annotation_multiplier(); let block_sources = self.tabs[i] .scene @@ -656,9 +638,11 @@ impl OpenCADStudio { .iter() .map(|style| (style.name.clone(), style.handle)) .collect(); - let new_cmd = style.map_or_else(MLeaderCommand::new, |style| { - MLeaderCommand::with_style(style, multiplier) - }).with_drawing_resources(block_sources, layers, text_styles); + let new_cmd = style + .map_or_else(MLeaderCommand::new, |style| { + MLeaderCommand::with_style(style, multiplier) + }) + .with_drawing_resources(block_sources, layers, text_styles); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } @@ -686,10 +670,7 @@ impl OpenCADStudio { .filter(|handle| scene.entity_belongs_to_active_space(*handle)) .and_then(|handle| scene.document.get_entity(handle)) .cloned(); - let cmd = DimContinueCommand::new( - recent, - self.dimension_continue_mode == 1, - ); + let cmd = DimContinueCommand::new(recent, self.dimension_continue_mode == 1); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } @@ -754,11 +735,8 @@ impl OpenCADStudio { surface_area: None, }) .collect(); - let cmd = QdimCommand::new( - selection, - dim_spacing, - self.quick_dimension_snap_priority, - ); + let cmd = + QdimCommand::new(selection, dim_spacing, self.quick_dimension_snap_priority); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } @@ -829,13 +807,15 @@ impl OpenCADStudio { "ZOOM EXTENTS ALL" | "ZOOM EXTENTS ALL VIEWPORTS" | "ZOOM EA" | "ZEA" => { self.tabs[i].scene.remember_current_view(); self.tabs[i].scene.fit_all_model_viewports(); - self.command_line.push_output(crate::t!("Zoom Extents — All Viewports").as_ref()); + self.command_line + .push_output(crate::t!("Zoom Extents — All Viewports").as_ref()); } "ZOOM EXTENTS" | "ZOOM E" | "ZOOMEXTENTS" | "ZE" => { self.tabs[i].scene.remember_current_view(); self.tabs[i].scene.fit_all(); - self.command_line.push_output(crate::t!("Zoom Extents").as_ref()); + self.command_line + .push_output(crate::t!("Zoom Extents").as_ref()); } "ZOOM IN" | "ZOOM I" | "ZI" => { @@ -847,19 +827,22 @@ impl OpenCADStudio { "ZOOM OUT" | "ZO" => { self.tabs[i].scene.remember_current_view(); self.tabs[i].scene.zoom_camera(1.5); - self.command_line.push_output(crate::t!("Zoom Out").as_ref()); + self.command_line + .push_output(crate::t!("Zoom Out").as_ref()); } // ZOOM ALL — fit the configured drawing limits. "ZOOM ALL" | "ZOOM A" | "ZA" => { self.tabs[i].scene.remember_current_view(); self.tabs[i].scene.fit_all_with_limits(); - self.command_line.push_output(crate::t!("Zoom All").as_ref()); + self.command_line + .push_output(crate::t!("Zoom All").as_ref()); } "ZOOM PREVIOUS" | "ZOOM P" | "ZP" => { if self.tabs[i].scene.restore_previous_view() { - self.command_line.push_output(crate::t!("Zoom Previous").as_ref()); + self.command_line + .push_output(crate::t!("Zoom Previous").as_ref()); } else { self.command_line .push_error(crate::t!("ZOOM: no previous view.").as_ref()); @@ -881,7 +864,8 @@ impl OpenCADStudio { } else { self.tabs[i].scene.remember_current_view(); if self.tabs[i].scene.zoom_to_entities(&handles) { - self.command_line.push_output(crate::t!("Zoom Object").as_ref()); + self.command_line + .push_output(crate::t!("Zoom Object").as_ref()); } else { self.command_line.push_error( crate::t!("ZOOM: selected objects have no visible bounds.").as_ref(), @@ -986,7 +970,11 @@ impl OpenCADStudio { }; (!handles.is_empty() && handles.iter().all(|h| { - self.tabs[i].scene.wire_models_for(&[*h]).iter().any(touches) + self.tabs[i] + .scene + .wire_models_for(&[*h]) + .iter() + .any(touches) })) .then(|| { ( @@ -1257,25 +1245,30 @@ impl OpenCADStudio { } else { // EXPLMODE 0 keeps references that are not uniformly and // positively scaled whole. - let selected: Vec<_> = if super::blockvars::explmode(&self.tabs[i].scene.document) == 0 { - selected - .into_iter() - .filter(|(_, entity)| match entity { - codec::EntityType::Insert(ins) => { - let (x, y, z) = (ins.x_scale(), ins.y_scale(), ins.z_scale()); - let uniform = x > 0.0 && (x - y).abs() <= 1e-10 && (x - z).abs() <= 1e-10; - if !uniform { - self.command_line - .push_output("The object is not uniformly and positively scaled."); + let selected: Vec<_> = + if super::blockvars::explmode(&self.tabs[i].scene.document) == 0 { + selected + .into_iter() + .filter(|(_, entity)| match entity { + codec::EntityType::Insert(ins) => { + let (x, y, z) = + (ins.x_scale(), ins.y_scale(), ins.z_scale()); + let uniform = x > 0.0 + && (x - y).abs() <= 1e-10 + && (x - z).abs() <= 1e-10; + if !uniform { + self.command_line.push_output( + "The object is not uniformly and positively scaled.", + ); + } + uniform } - uniform - } - _ => true, - }) - .collect() - } else { - selected - }; + _ => true, + }) + .collect() + } else { + selected + }; let replacements = plan_explode(&selected, &self.tabs[i].scene.document); let exploded = replacements.len(); if exploded > 0 { @@ -1288,8 +1281,9 @@ impl OpenCADStudio { self.command_line .push_output(crate::tf!("{exploded} object(s) exploded.").as_ref()); } else { - self.command_line - .push_info(crate::t!("EXPLODE: no explodable objects selected.").as_ref()); + self.command_line.push_info( + crate::t!("EXPLODE: no explodable objects selected.").as_ref(), + ); } } } @@ -1350,8 +1344,7 @@ impl OpenCADStudio { }) .collect(); let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect(); - let new_cmd = - TrimCommand::with_cutting_edges(all_entities, initial_edges); + let new_cmd = TrimCommand::with_cutting_edges(all_entities, initial_edges); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } @@ -1364,7 +1357,12 @@ impl OpenCADStudio { .iter() .filter_map(|w| { let h = Scene::handle_from_wire_name(&w.name)?; - self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e)) + self.tabs[i] + .scene + .document + .get_entity(h) + .cloned() + .map(|e| (h, e)) }) .collect(); let new_cmd = ExtrimCommand::new(all); @@ -1413,7 +1411,10 @@ impl OpenCADStudio { let text = cmd.strip_prefix("ARCTEXT").unwrap_or("").trim().to_string(); if text.is_empty() { self.command_line.push_info( - crate::t!("Usage: ARCTEXT (select an arc first; the text follows it)").as_ref(), + crate::t!( + "Usage: ARCTEXT (select an arc first; the text follows it)" + ) + .as_ref(), ); return None; } @@ -1433,7 +1434,8 @@ impl OpenCADStudio { }; let chars: Vec = text.chars().filter(|c| !c.is_control()).collect(); if chars.is_empty() { - self.command_line.push_error(crate::t!("ARCTEXT: no printable text.").as_ref()); + self.command_line + .push_error(crate::t!("ARCTEXT: no printable text.").as_ref()); return None; } let n = chars.len(); @@ -1456,8 +1458,9 @@ impl OpenCADStudio { self.tabs[i].scene.add_entity(codec::EntityType::Text(t)); } self.tabs[i].dirty = true; - self.command_line - .push_output(crate::tf!("ARCTEXT: placed {n} character(s) along the arc.").as_ref()); + self.command_line.push_output( + crate::tf!("ARCTEXT: placed {n} character(s) along the arc.").as_ref(), + ); } _ => return None, diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs index b194a71f9..16c4cbd07 100644 --- a/src/app/commands/display.rs +++ b/src/app/commands/display.rs @@ -8,8 +8,9 @@ impl OpenCADStudio { "PAN" => { self.tabs[i].pan_mode = true; self.clear_navigation_hover(i); - self.command_line - .push_output(crate::t!("PAN: drag with the left mouse button. Press Esc to exit.").as_ref()); + self.command_line.push_output( + crate::t!("PAN: drag with the left mouse button. Press Esc to exit.").as_ref(), + ); } // ── TABLE structure and cell editing ─────────────────────────────── @@ -33,12 +34,17 @@ impl OpenCADStudio { } let words: Vec<&str> = rest.split_whitespace().collect(); let integer = |index: usize| { - words.get(index).and_then(|value| value.parse::().ok()) + words + .get(index) + .and_then(|value| value.parse::().ok()) }; - let real = |index: usize| { - words.get(index).and_then(|value| value.parse::().ok()) - }; - let action = match words.first().map(|word| word.to_ascii_uppercase()).as_deref() { + let real = + |index: usize| words.get(index).and_then(|value| value.parse::().ok()); + let action = match words + .first() + .map(|word| word.to_ascii_uppercase()) + .as_deref() + { Some("CELL") => { let parts: Vec<&str> = rest.splitn(4, char::is_whitespace).collect(); match (integer(1), integer(2)) { @@ -117,11 +123,7 @@ impl OpenCADStudio { u64::from_str_radix(value.trim_start_matches("0x"), 16) .ok() .map(|handle| { - TableAction::Field( - row, - column, - codec::Handle::new(handle), - ) + TableAction::Field(row, column, codec::Handle::new(handle)) }) } _ => None, @@ -158,16 +160,19 @@ impl OpenCADStudio { None }; if matches!(action, TableAction::Block(_, _, _)) && block_handle.is_none() { - self.command_line.push_error( - crate::t!("TABLE: block definition not found.").as_ref(), - ); + self.command_line + .push_error(crate::t!("TABLE: block definition not found.").as_ref()); return Some(Task::none()); } if let TableAction::Field(_, _, field_handle) = &action { - if !self.tabs[i].scene.document.fields.contains_key(field_handle) { - self.command_line.push_error( - crate::t!("TABLE: field handle not found.").as_ref(), - ); + if !self.tabs[i] + .scene + .document + .fields + .contains_key(field_handle) + { + self.command_line + .push_error(crate::t!("TABLE: field handle not found.").as_ref()); return Some(Task::none()); } } @@ -337,10 +342,10 @@ impl OpenCADStudio { self.command_line .push_output(crate::t!("TABLE: edit applied.").as_ref()); } else { - self.command_line - .push_error(crate::t!( - "TABLE: row/column is out of range or the cell is locked." - ).as_ref()); + self.command_line.push_error( + crate::t!("TABLE: row/column is out of range or the cell is locked.") + .as_ref(), + ); } } @@ -388,9 +393,10 @@ impl OpenCADStudio { } } self.tabs[i].dirty = true; - self.command_line.push_output(crate::tf!( - "UCSICON {sub}: updated {count} viewport(s) + model space." - ).as_ref()); + self.command_line.push_output( + crate::tf!("UCSICON {sub}: updated {count} viewport(s) + model space.") + .as_ref(), + ); } "" => { // Bare UCSICON toggles visibility. @@ -405,11 +411,13 @@ impl OpenCADStudio { } self.tabs[i].dirty = true; let state = if visible { "ON" } else { "OFF" }; - self.command_line.push_output(crate::tf!("UCSICON {state}").as_ref()); + self.command_line + .push_output(crate::tf!("UCSICON {state}").as_ref()); } _ => { - self.command_line - .push_info(crate::t!("Usage: UCSICON ON | OFF | NOORIGIN | ORIGIN").as_ref()); + self.command_line.push_info( + crate::t!("Usage: UCSICON ON | OFF | NOORIGIN | ORIGIN").as_ref(), + ); } } } @@ -437,40 +445,52 @@ impl OpenCADStudio { self.tabs[i].scene.set_drawing_limit_check(enabled); self.tabs[i].dirty = true; } - self.command_line.push_output(crate::t!(if enabled { - "Limits checking ON." - } else { - "Limits checking OFF." - }).as_ref()); + self.command_line.push_output( + crate::t!(if enabled { + "Limits checking ON." + } else { + "Limits checking OFF." + }) + .as_ref(), + ); } cmd if cmd.starts_with("LIMITS SET ") => { let tokens: Vec<&str> = cmd["LIMITS SET ".len()..].split_whitespace().collect(); let values: Result, _> = tokens.iter().map(|value| value.parse()).collect(); let Ok(values) = values else { - self.command_line - .push_error(crate::t!("LIMITS: four numeric coordinates required.").as_ref()); + self.command_line.push_error( + crate::t!("LIMITS: four numeric coordinates required.").as_ref(), + ); return Some(Task::none()); }; if tokens.len() != 4 || !values.iter().all(|value| value.is_finite()) { - self.command_line - .push_error(crate::t!("LIMITS: four finite numeric coordinates required.").as_ref()); + self.command_line.push_error( + crate::t!("LIMITS: four finite numeric coordinates required.").as_ref(), + ); } else { let first = glam::DVec2::new(values[0], values[1]); let opposite = glam::DVec2::new(values[2], values[3]); let min = first.min(opposite); let max = first.max(opposite); if min.x == max.x || min.y == max.y { - self.command_line - .push_error(crate::t!("LIMITS: corners must define a non-zero area.").as_ref()); + self.command_line.push_error( + crate::t!("LIMITS: corners must define a non-zero area.").as_ref(), + ); } else { self.push_undo_snapshot(i, "LIMITS"); self.tabs[i].scene.set_current_drawing_limits(min, max); self.tabs[i].dirty = true; - self.command_line.push_output(crate::tf!( - "Drawing limits: {:.4},{:.4} to {:.4},{:.4}.", - min.x, min.y, max.x, max.y - ).as_ref()); + self.command_line.push_output( + crate::tf!( + "Drawing limits: {:.4},{:.4} to {:.4},{:.4}.", + min.x, + min.y, + max.x, + max.y + ) + .as_ref(), + ); } } } @@ -499,15 +519,21 @@ impl OpenCADStudio { // it only queues a per-viewport cache invalidation. "REDRAW" => { use crate::scene::ViewportRefreshScope; - self.tabs[i].scene.request_refresh(ViewportRefreshScope::Active); - self.command_line.push_output(crate::t!("REDRAW: viewport refreshed.").as_ref()); + self.tabs[i] + .scene + .request_refresh(ViewportRefreshScope::Active); + self.command_line + .push_output(crate::t!("REDRAW: viewport refreshed.").as_ref()); return Some(Task::none()); } // REDRAWALL — force re-rasterize of every generated viewport. "REDRAWALL" => { use crate::scene::ViewportRefreshScope; - self.tabs[i].scene.request_refresh(ViewportRefreshScope::All); - self.command_line.push_output(crate::t!("REDRAWALL: viewports refreshed.").as_ref()); + self.tabs[i] + .scene + .request_refresh(ViewportRefreshScope::All); + self.command_line + .push_output(crate::t!("REDRAWALL: viewports refreshed.").as_ref()); return Some(Task::none()); } // Rebuild tessellation caches without modifying the document. @@ -515,7 +541,8 @@ impl OpenCADStudio { // A regen is a field evaluation event (FIELDEVAL bit 16). self.tabs[i].scene.update_fields(16, None); self.tabs[i].scene.populate_meshes_from_document(); - self.command_line.push_output(crate::t!("REGEN: regenerated model.").as_ref()); + self.command_line + .push_output(crate::t!("REGEN: regenerated model.").as_ref()); return Some(Task::none()); } @@ -593,8 +620,9 @@ impl OpenCADStudio { // ── TOOLPALETTES — not yet implemented ─────────────────────────────── "TOOLPALETTES" => { - self.command_line - .push_info(crate::t!("TOOLPALETTES: Tool Palettes not yet implemented.").as_ref()); + self.command_line.push_info( + crate::t!("TOOLPALETTES: Tool Palettes not yet implemented.").as_ref(), + ); } // ── XDATA — read/write extended entity data ────────────────────────── @@ -635,19 +663,24 @@ impl OpenCADStudio { if let Some(entity) = self.tabs[i].scene.document.get_entity(*sh) { let xd = &entity.common().extended_data; if xd.is_empty() { - self.command_line - .push_output(crate::tf!(" {:x}: no xdata.", sh.value()).as_ref()); + self.command_line.push_output( + crate::tf!(" {:x}: no xdata.", sh.value()).as_ref(), + ); } else { for rec in xd.records() { - self.command_line.push_output(crate::tf!( - " {:x} [{}]: {} value(s)", - sh.value(), - rec.application_name, - rec.values.len() - ).as_ref()); + self.command_line.push_output( + crate::tf!( + " {:x} [{}]: {} value(s)", + sh.value(), + rec.application_name, + rec.values.len() + ) + .as_ref(), + ); for v in &rec.values { - self.command_line - .push_output(crate::tf!(" {:?}", v).as_ref()); + self.command_line.push_output( + crate::tf!(" {:?}", v).as_ref(), + ); } } } @@ -680,10 +713,13 @@ impl OpenCADStudio { } } self.tabs[i].dirty = true; - self.command_line.push_output(crate::tf!( - "XDATA: set [{app}] = \"{val}\" on {} entity/entities.", - editable.len() - ).as_ref()); + self.command_line.push_output( + crate::tf!( + "XDATA: set [{app}] = \"{val}\" on {} entity/entities.", + editable.len() + ) + .as_ref(), + ); } "CLEAR" => { let app_filter = parts.get(1).copied(); @@ -723,11 +759,14 @@ impl OpenCADStudio { } } self.tabs[i].dirty = true; - self.command_line.push_output(crate::t!("XDATA: cleared.").as_ref()); + self.command_line + .push_output(crate::t!("XDATA: cleared.").as_ref()); } _ => { - self.command_line - .push_info(crate::t!("Usage: XDATA LIST | SET | CLEAR [app]").as_ref()); + self.command_line.push_info( + crate::t!("Usage: XDATA LIST | SET | CLEAR [app]") + .as_ref(), + ); } } } @@ -786,7 +825,9 @@ impl OpenCADStudio { use crate::modules::insert::solid3d_cmds::PresspullCommand; let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer); let mut command = PresspullCommand::new(color); - command.set_isolines(crate::entities::solid3d::clamp_header_isolines(self.tabs[i].scene.document.header.isolines)); + command.set_isolines(crate::entities::solid3d::clamp_header_isolines( + self.tabs[i].scene.document.header.isolines, + )); command.set_preselection(self.presspull_preselection()); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); @@ -797,7 +838,9 @@ impl OpenCADStudio { use crate::modules::insert::solid3d_cmds::RevolveCommand; let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect(); let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer); - let isolines = crate::entities::solid3d::clamp_header_isolines(self.tabs[i].scene.document.header.isolines); + let isolines = crate::entities::solid3d::clamp_header_isolines( + self.tabs[i].scene.document.header.isolines, + ); let mut cmd = RevolveCommand::new(color, isolines); if !selected.is_empty() { cmd.set_preselection( @@ -815,13 +858,23 @@ impl OpenCADStudio { "SWEEP" => { use crate::modules::insert::solid3d_cmds::SweepCommand; let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer); - let isolines = crate::entities::solid3d::clamp_header_isolines(self.tabs[i].scene.document.header.isolines); + let isolines = crate::entities::solid3d::clamp_header_isolines( + self.tabs[i].scene.document.header.isolines, + ); let mut cmd = SweepCommand::new(color, isolines); cmd.set_parameters(self.tabs[i].scene.named_parameters().clone()); - let selected = self.tabs[i].scene.selected_handles_in_order() + let selected = self.tabs[i] + .scene + .selected_handles_in_order() .into_iter() - .filter_map(|handle| self.tabs[i].scene.document.get_entity(handle) - .cloned().map(|entity| (handle, entity))) + .filter_map(|handle| { + self.tabs[i] + .scene + .document + .get_entity(handle) + .cloned() + .map(|entity| (handle, entity)) + }) .collect::>(); if !selected.is_empty() { cmd.set_preselection(selected); @@ -834,13 +887,35 @@ impl OpenCADStudio { "LOFT" => { use crate::modules::insert::solid3d_cmds::LoftCommand; let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer); - let isolines = crate::entities::solid3d::clamp_header_isolines(self.tabs[i].scene.document.header.isolines); - let selected = self.tabs[i].scene.selected_handles_in_order().into_iter() - .filter_map(|handle| self.tabs[i].scene.document.get_entity(handle) - .cloned().map(|entity| (handle, entity))).collect(); - let available = self.tabs[i].scene.document.entities() - .map(|entity| (entity.common().handle, entity.clone())).collect(); - let cmd = LoftCommand::new(color, isolines, crate::command::ExtrudeMode::Solid, selected, available); + let isolines = crate::entities::solid3d::clamp_header_isolines( + self.tabs[i].scene.document.header.isolines, + ); + let selected = self.tabs[i] + .scene + .selected_handles_in_order() + .into_iter() + .filter_map(|handle| { + self.tabs[i] + .scene + .document + .get_entity(handle) + .cloned() + .map(|entity| (handle, entity)) + }) + .collect(); + let available = self.tabs[i] + .scene + .document + .entities() + .map(|entity| (entity.common().handle, entity.clone())) + .collect(); + let cmd = LoftCommand::new( + color, + isolines, + crate::command::ExtrudeMode::Solid, + selected, + available, + ); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } @@ -923,14 +998,18 @@ impl OpenCADStudio { "Plot style: {} ({} color overrides)", t.name, t.aci_entries.iter().filter(|e| e.color.is_some()).count() - ).into_owned() + ) + .into_owned() }) - .unwrap_or_else(|| crate::t!("No plot style table loaded.").into_owned()); + .unwrap_or_else(|| { + crate::t!("No plot style table loaded.").into_owned() + }); self.command_line.push_output(&msg); } _ => { - self.command_line - .push_error(crate::t!("Usage: PLOTSTYLE [LOAD | CLEAR | STATUS]").as_ref()); + self.command_line.push_error( + crate::t!("Usage: PLOTSTYLE [LOAD | CLEAR | STATUS]").as_ref(), + ); } } } @@ -948,17 +1027,21 @@ impl OpenCADStudio { .selected_entities() .iter() .filter(|(handle, _)| !self.tabs[i].scene.is_layer_locked(*handle)) - .filter_map(|(handle, _)| match self.tabs[i].scene.document.get_entity(*handle) { - Some(codec::EntityType::RasterImage(image)) => { - Some((*handle, image.flags.contains(ImageDisplayFlags::TRANSPARENCY_ON))) + .filter_map(|(handle, _)| { + match self.tabs[i].scene.document.get_entity(*handle) { + Some(codec::EntityType::RasterImage(image)) => Some(( + *handle, + image.flags.contains(ImageDisplayFlags::TRANSPARENCY_ON), + )), + _ => None, } - _ => None, }) .collect(); let Some(&(_, first_on)) = images.first() else { return Some(Task::none()); }; - let handles: Vec = images.iter().map(|(handle, _)| *handle).collect(); + let handles: Vec = + images.iter().map(|(handle, _)| *handle).collect(); if cmd == "TRANSPARENCY MODE" { let c = crate::modules::insert::image_transparency::TransparencyCommand::mode( handles, first_on, @@ -1022,8 +1105,9 @@ impl OpenCADStudio { .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle)) .collect(); if handles.is_empty() { - self.command_line - .push_error(crate::t!("UNDERLAY: select underlay entities first.").as_ref()); + self.command_line.push_error( + crate::t!("UNDERLAY: select underlay entities first.").as_ref(), + ); } else { let parts: Vec<&str> = sub.splitn(2, char::is_whitespace).collect(); let action = parts.first().copied().unwrap_or(""); @@ -1060,8 +1144,7 @@ impl OpenCADStudio { } "CLIP" => match arg { "ON" => { - ul.flags |= - codec::entities::UnderlayDisplayFlags::CLIPPING; + ul.flags |= codec::entities::UnderlayDisplayFlags::CLIPPING; changed += 1; } "OFF" => { @@ -1140,8 +1223,10 @@ impl OpenCADStudio { return Some(Task::none()); } let Some(scale) = self.tabs[i].scene.creation_annotation_scale_handle() else { - self.command_line - .push_error(crate::t!("OBJECTSCALE: the active annotation scale is unavailable.").as_ref()); + self.command_line.push_error( + crate::t!("OBJECTSCALE: the active annotation scale is unavailable.") + .as_ref(), + ); return Some(Task::none()); }; @@ -1156,11 +1241,9 @@ impl OpenCADStudio { let member = memberships.iter().any(|(_, current)| *current == scale); if adding { !member - && self.tabs[i] - .scene - .document - .get_entity(*handle) - .is_some_and(crate::scene::annotative::supports_annotation_context) + && self.tabs[i].scene.document.get_entity(*handle).is_some_and( + crate::scene::annotative::supports_annotation_context, + ) } else { member } @@ -1213,11 +1296,14 @@ impl OpenCADStudio { .collect(); self.tabs[i].scene.bump_entities(&changes); self.tabs[i].dirty = true; - self.command_line.push_output(crate::tf!( - "OBJECTSCALE: {} the current scale on {} object(s).", - if adding { "added" } else { "removed" }, - changed.len() - ).as_ref()); + self.command_line.push_output( + crate::tf!( + "OBJECTSCALE: {} the current scale on {} object(s).", + if adding { "added" } else { "removed" }, + changed.len() + ) + .as_ref(), + ); return Some(Task::none()); } @@ -1226,19 +1312,22 @@ impl OpenCADStudio { "HYPERLINK" => { use crate::command::SelectThenValueCommand; let has_sel = !self.tabs[i].scene.selected_entities().is_empty(); - let c = SelectThenValueCommand::new( - "HYPERLINK", - "HYPERLINK URL to attach:", - has_sel, - ); + let c = + SelectThenValueCommand::new("HYPERLINK", "HYPERLINK URL to attach:", has_sel); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } cmd if cmd.starts_with("HYPERLINK ") => { use codec::xdata::XDataValue; - let url = cmd.strip_prefix("HYPERLINK").unwrap_or("").trim().to_string(); + let url = cmd + .strip_prefix("HYPERLINK") + .unwrap_or("") + .trim() + .to_string(); if url.is_empty() { - self.command_line.push_info(crate::t!("Usage: HYPERLINK (select objects first)").as_ref()); + self.command_line.push_info( + crate::t!("Usage: HYPERLINK (select objects first)").as_ref(), + ); return Some(Task::none()); } let handles: Vec = self.tabs[i] @@ -1249,7 +1338,8 @@ impl OpenCADStudio { .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle)) .collect(); if handles.is_empty() { - self.command_line.push_error(crate::t!("HYPERLINK: select objects first.").as_ref()); + self.command_line + .push_error(crate::t!("HYPERLINK: select objects first.").as_ref()); return Some(Task::none()); } self.push_undo_snapshot(i, "HYPERLINK"); @@ -1282,7 +1372,11 @@ impl OpenCADStudio { "ADJUST", "ADJUST [Brightness / Contrast / Fade]:", vec![ - ("Brightness", "BRIGHTNESS", Some("ADJUST brightness 0-100:")), + ( + "Brightness", + "BRIGHTNESS", + Some("ADJUST brightness 0-100:"), + ), ("Contrast", "CONTRAST", Some("ADJUST contrast 0-100:")), ("Fade", "FADE", Some("ADJUST fade 0-100:")), ], @@ -1307,8 +1401,9 @@ impl OpenCADStudio { self.command_line .push_error(crate::t!("ADJUST: select raster image(s) first.").as_ref()); } else if action.is_empty() { - self.command_line - .push_info(crate::t!("Usage: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100>").as_ref()); + self.command_line.push_info( + crate::t!("Usage: ADJUST BRIGHTNESS|CONTRAST|FADE <0-100>").as_ref(), + ); } else if let Ok(v) = arg.parse::() { let v = v.min(100); self.push_undo_snapshot(i, "ADJUST"); @@ -1351,15 +1446,17 @@ impl OpenCADStudio { .map(|handle| (handle, crate::scene::ChangeKind::Modified)) .collect(); self.tabs[i].scene.bump_entities(&changes); - self.command_line - .push_output(crate::tf!("ADJUST: {action} = {v} on {changed} image(s).").as_ref()); + self.command_line.push_output( + crate::tf!("ADJUST: {action} = {v} on {changed} image(s).").as_ref(), + ); } else { self.command_line.push_error( crate::t!("ADJUST: no raster images selected, or unknown property (use BRIGHTNESS|CONTRAST|FADE).").as_ref(), ); } } else { - self.command_line.push_error(crate::t!("ADJUST: value must be 0-100.").as_ref()); + self.command_line + .push_error(crate::t!("ADJUST: value must be 0-100.").as_ref()); } } @@ -1377,10 +1474,8 @@ impl OpenCADStudio { } "ANNOALLVISIBLE" => { use crate::command::ValuePromptCommand; - let c = ValuePromptCommand::new( - "ANNOALLVISIBLE", - "ANNOALLVISIBLE new value [0/1]:", - ); + let c = + ValuePromptCommand::new("ANNOALLVISIBLE", "ANNOALLVISIBLE new value [0/1]:"); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } @@ -1402,10 +1497,8 @@ impl OpenCADStudio { } "ANNOAUTOSCALE" => { use crate::command::ValuePromptCommand; - let c = ValuePromptCommand::new( - "ANNOAUTOSCALE", - "ANNOAUTOSCALE new value [-4..4]:", - ); + let c = + ValuePromptCommand::new("ANNOAUTOSCALE", "ANNOAUTOSCALE new value [-4..4]:"); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } @@ -1427,8 +1520,9 @@ impl OpenCADStudio { .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle)) .collect(); if handles.is_empty() { - self.command_line - .push_error(crate::t!("ANNOUPDATE: select annotation objects first.").as_ref()); + self.command_line.push_error( + crate::t!("ANNOUPDATE: select annotation objects first.").as_ref(), + ); return Some(Task::none()); } self.push_undo_snapshot(i, "ANNOUPDATE"); @@ -1487,9 +1581,9 @@ impl OpenCADStudio { self.command_line .push_output(crate::tf!("Annotation scale: {arg}").as_ref()); } - None => self - .command_line - .push_error(crate::t!("Usage: ANNOSCALE e.g. 1:50, 2:1, or a factor").as_ref()), + None => self.command_line.push_error( + crate::t!("Usage: ANNOSCALE e.g. 1:50, 2:1, or a factor").as_ref(), + ), } } @@ -1503,8 +1597,16 @@ impl OpenCADStudio { "SCALELISTEDIT", "SCALELISTEDIT [Add / Delete]:", vec![ - ("Add", "ADD", Some("SCALELISTEDIT ADD new scale ratio (e.g. 1:50):")), - ("Delete", "DELETE", Some("SCALELISTEDIT DELETE scale ratio to remove:")), + ( + "Add", + "ADD", + Some("SCALELISTEDIT ADD new scale ratio (e.g. 1:50):"), + ), + ( + "Delete", + "DELETE", + Some("SCALELISTEDIT DELETE scale ratio to remove:"), + ), ], ); self.command_line.push_info(&c.prompt()); @@ -1522,20 +1624,22 @@ impl OpenCADStudio { self.push_undo_snapshot(i, "SCALELISTEDIT"); if self.tabs[i].scene.add_scale(arg, paper, drawing) { self.tabs[i].dirty = true; - self.command_line - .push_output(crate::tf!("Added annotation scale {arg}.").as_ref()); + self.command_line.push_output( + crate::tf!("Added annotation scale {arg}.").as_ref(), + ); } else { - self.command_line - .push_info(crate::tf!("Scale {arg} already exists.").as_ref()); + self.command_line.push_info( + crate::tf!("Scale {arg} already exists.").as_ref(), + ); } } - _ => self - .command_line - .push_error(crate::t!("SCALELISTEDIT ADD: use a ratio like 1:50.").as_ref()), + _ => self.command_line.push_error( + crate::t!("SCALELISTEDIT ADD: use a ratio like 1:50.").as_ref(), + ), }, - None => self - .command_line - .push_error(crate::t!("SCALELISTEDIT ADD: use a ratio like 1:50.").as_ref()), + None => self.command_line.push_error( + crate::t!("SCALELISTEDIT ADD: use a ratio like 1:50.").as_ref(), + ), }, "DELETE" | "REMOVE" => { let current = self.tabs[i] @@ -1545,20 +1649,25 @@ impl OpenCADStudio { .current_annotation_scale .clone(); if arg.is_empty() { - self.command_line.push_info(crate::t!("Usage: SCALELISTEDIT DELETE ").as_ref()); + self.command_line.push_info( + crate::t!("Usage: SCALELISTEDIT DELETE ").as_ref(), + ); } else if arg.eq_ignore_ascii_case(¤t) { - self.command_line.push_error(crate::tf!( - "Cannot delete the current annotation scale ({arg})." - ).as_ref()); + self.command_line.push_error( + crate::tf!("Cannot delete the current annotation scale ({arg}).") + .as_ref(), + ); } else { self.push_undo_snapshot(i, "SCALELISTEDIT"); if self.tabs[i].scene.remove_scale(arg) { self.tabs[i].dirty = true; - self.command_line - .push_output(crate::tf!("Removed annotation scale {arg}.").as_ref()); + self.command_line.push_output( + crate::tf!("Removed annotation scale {arg}.").as_ref(), + ); } else { - self.command_line - .push_info(crate::tf!("No annotation scale named {arg}.").as_ref()); + self.command_line.push_info( + crate::tf!("No annotation scale named {arg}.").as_ref(), + ); } } } @@ -1570,15 +1679,17 @@ impl OpenCADStudio { .map(|(n, _, _)| n) .collect(); if names.is_empty() { - self.command_line.push_info(crate::t!("No annotation scales defined.").as_ref()); - } else { self.command_line - .push_output(crate::tf!("Annotation scales: {}", names.join(", ")).as_ref()); + .push_info(crate::t!("No annotation scales defined.").as_ref()); + } else { + self.command_line.push_output( + crate::tf!("Annotation scales: {}", names.join(", ")).as_ref(), + ); } } - _ => self - .command_line - .push_info(crate::t!("Usage: SCALELISTEDIT [ADD 1:50 | DELETE 1:50]").as_ref()), + _ => self.command_line.push_info( + crate::t!("Usage: SCALELISTEDIT [ADD 1:50 | DELETE 1:50]").as_ref(), + ), } } @@ -1624,8 +1735,10 @@ impl OpenCADStudio { .collect(); if candidates.is_empty() { self.command_line.push_info( - crate::t!("ANNORESET: no selected object has alternate scale representations.") - .as_ref(), + crate::t!( + "ANNORESET: no selected object has alternate scale representations." + ) + .as_ref(), ); return Some(Task::none()); } @@ -1666,9 +1779,8 @@ impl OpenCADStudio { cmd if cmd.starts_with("DATALINK ") => { let path = cmd.trim_start_matches("DATALINK").trim(); if path.is_empty() { - self.command_line.push_info( - crate::t!("Usage: DATALINK ").as_ref(), - ); + self.command_line + .push_info(crate::t!("Usage: DATALINK ").as_ref()); return Some(Task::none()); } match std::fs::read_to_string(path) { @@ -1695,29 +1807,32 @@ impl OpenCADStudio { } let doc = &self.tabs[i].scene.document; let current_style = doc.header.current_table_style_name.clone(); - table.table_style_handle = doc.objects.iter().find_map(|(handle, object)| { - match object { - codec::objects::ObjectType::TableStyle(style) - if style.name.eq_ignore_ascii_case(¤t_style) => - { - Some(*handle) - } - _ => None, - } - }); + table.table_style_handle = + doc.objects + .iter() + .find_map(|(handle, object)| match object { + codec::objects::ObjectType::TableStyle(style) + if style.name.eq_ignore_ascii_case(¤t_style) => + { + Some(*handle) + } + _ => None, + }); let link_path = std::fs::canonicalize(path) .map(|value| value.to_string_lossy().into_owned()) .unwrap_or_else(|_| path.to_string()); - let command = crate::modules::annotate::data_link::DataLinkPlaceCommand::new( - table, &link_path, - ); + let command = + crate::modules::annotate::data_link::DataLinkPlaceCommand::new( + table, &link_path, + ); self.command_line .push_info(&crate::command::CadCommand::prompt(&command)); self.tabs[i].active_cmd = Some(Box::new(command)); } Err(e) => { - self.command_line - .push_error(crate::tf!("DATALINK: cannot read \"{path}\": {e}").as_ref()); + self.command_line.push_error( + crate::tf!("DATALINK: cannot read \"{path}\": {e}").as_ref(), + ); } } } @@ -1784,15 +1899,17 @@ impl OpenCADStudio { &self.tabs[i].scene.document, *link_handle, ) - .and_then(|path| std::fs::write(path, csv).map_err(|error| error.to_string())) - { + .and_then(|path| { + std::fs::write(path, csv).map_err(|error| error.to_string()) + }) { Ok(()) => written += 1, Err(error) => errors.push(error), } } if written > 0 { self.command_line.push_output( - crate::tf!("DATALINKUPDATE: wrote {} linked source(s).", written).as_ref(), + crate::tf!("DATALINKUPDATE: wrote {} linked source(s).", written) + .as_ref(), ); } for error in errors { @@ -1813,8 +1930,9 @@ impl OpenCADStudio { .filter(|(_, _, rows)| !rows.is_empty()) .collect(); if updates.is_empty() { - self.command_line - .push_error(crate::t!("DATALINKUPDATE: linked sources could not be read.").as_ref()); + self.command_line.push_error( + crate::t!("DATALINKUPDATE: linked sources could not be read.").as_ref(), + ); return Some(Task::none()); } self.push_undo_snapshot(i, "DATALINKUPDATE"); @@ -1834,7 +1952,11 @@ impl OpenCADStudio { table.remove_row(table.row_count() - 1); } while table.column_count() < columns { - let width = table.columns.last().map(|column| column.width).unwrap_or(2.0); + let width = table + .columns + .last() + .map(|column| column.width) + .unwrap_or(2.0); table.add_column(width); } while table.column_count() > columns { @@ -1864,8 +1986,11 @@ impl OpenCADStudio { self.tabs[i].dirty = true; self.refresh_properties(); self.command_line.push_output( - crate::tf!("DATALINKUPDATE: updated {} linked table(s).", changed_handles.len()) - .as_ref(), + crate::tf!( + "DATALINKUPDATE: updated {} linked table(s).", + changed_handles.len() + ) + .as_ref(), ); } @@ -1874,8 +1999,10 @@ impl OpenCADStudio { // (northing easting elevation) → Point at (easting, northing, elev). "LANDXMLIMPORT" => { use crate::command::ValuePromptCommand; - let c = - ValuePromptCommand::new("LANDXMLIMPORT", "LANDXMLIMPORT path to the .xml file:"); + let c = ValuePromptCommand::new( + "LANDXMLIMPORT", + "LANDXMLIMPORT path to the .xml file:", + ); self.command_line.push_info(&c.prompt()); self.tabs[i].active_cmd = Some(Box::new(c)); } @@ -1883,7 +2010,10 @@ impl OpenCADStudio { let path = cmd.trim_start_matches("LANDXMLIMPORT").trim(); if path.is_empty() { self.command_line.push_info( - crate::t!("Usage: LANDXMLIMPORT (imports CgPoint survey points)").as_ref(), + crate::t!( + "Usage: LANDXMLIMPORT (imports CgPoint survey points)" + ) + .as_ref(), ); return Some(Task::none()); } @@ -1891,8 +2021,10 @@ impl OpenCADStudio { Ok(xml) => { let pts = parse_landxml_cgpoints(&xml); if pts.is_empty() { - self.command_line - .push_info(crate::t!("LANDXMLIMPORT: no survey points found.").as_ref()); + self.command_line.push_info( + crate::t!("LANDXMLIMPORT: no survey points found.") + .as_ref(), + ); return Some(Task::none()); } self.push_undo_snapshot(i, "LANDXMLIMPORT"); @@ -1904,14 +2036,17 @@ impl OpenCADStudio { .add_entity_clone(codec::EntityType::Point(p)); } self.tabs[i].dirty = true; - self.command_line.push_output(crate::tf!( + self.command_line.push_output( + crate::tf!( "LANDXMLIMPORT: imported {} survey point(s). Use ZOOM EXTENTS to view.", pts.len() - ).as_ref()); + ) + .as_ref(), + ); } - Err(e) => self - .command_line - .push_error(crate::tf!("LANDXMLIMPORT: cannot read \"{path}\": {e}").as_ref()), + Err(e) => self.command_line.push_error( + crate::tf!("LANDXMLIMPORT: cannot read \"{path}\": {e}").as_ref(), + ), } } @@ -2046,8 +2181,14 @@ mod tests { app.tabs[i].scene.refresh_pending_any(), "REDRAW must leave a pending force request" ); - assert_eq!(app.tabs[i].scene.geometry_epoch, geom_before, "REDRAW must not regen"); - assert_eq!(app.tabs[i].scene.block_epoch, block_before, "REDRAW must not regen blocks"); + assert_eq!( + app.tabs[i].scene.geometry_epoch, geom_before, + "REDRAW must not regen" + ); + assert_eq!( + app.tabs[i].scene.block_epoch, block_before, + "REDRAW must not regen blocks" + ); } #[test] @@ -2057,7 +2198,10 @@ mod tests { let _ = full.run_command_line("REDRAW"); let _ = short.run_command_line("R"); let i = full.active_tab; - assert!(short.tabs[i].scene.refresh_pending_any(), "'R' must trigger REDRAW"); + assert!( + short.tabs[i].scene.refresh_pending_any(), + "'R' must trigger REDRAW" + ); assert_eq!( short.tabs[i].scene.refresh_pending_any(), full.tabs[i].scene.refresh_pending_any(), @@ -2070,7 +2214,10 @@ mod tests { let mut app = fresh_app(); let i = app.active_tab; let _ = app.run_command_line("REDRAWALL"); - assert!(app.tabs[i].scene.refresh_pending_any(), "REDRAWALL must leave a force request"); + assert!( + app.tabs[i].scene.refresh_pending_any(), + "REDRAWALL must leave a force request" + ); } #[test] @@ -2081,11 +2228,23 @@ mod tests { let block_before = app.tabs[i].scene.block_epoch; app.tabs[i].dirty = false; let _ = app.run_command_line("REGEN"); - assert_ne!(app.tabs[i].scene.geometry_epoch, geom_before, "REGEN must regenerate geometry"); - assert_ne!(app.tabs[i].scene.block_epoch, block_before, "REGEN must regenerate block epoch"); - assert!(!app.tabs[i].dirty, "REGEN must NOT mark the document as modified (no DB change)"); + assert_ne!( + app.tabs[i].scene.geometry_epoch, geom_before, + "REGEN must regenerate geometry" + ); + assert_ne!( + app.tabs[i].scene.block_epoch, block_before, + "REGEN must regenerate block epoch" + ); + assert!( + !app.tabs[i].dirty, + "REGEN must NOT mark the document as modified (no DB change)" + ); let _ = app.run_command_line("REGENALL"); - assert!(!app.tabs[i].dirty, "REGENALL must not dirty the document either"); + assert!( + !app.tabs[i].dirty, + "REGENALL must not dirty the document either" + ); } #[test] diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 72f9a5358..0dc6605db 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -438,9 +438,7 @@ impl OpenCADStudio { .document .entities() .filter_map(|entity| match entity { - codec::EntityType::Face3D(face) => { - Some((face.common.handle, face.clone())) - } + codec::EntityType::Face3D(face) => Some((face.common.handle, face.clone())), _ => None, }) .collect(); @@ -1030,9 +1028,8 @@ impl OpenCADStudio { false, true, ); - self.command_line.push_output( - format!("{} constraint bar(s) reset.", count).as_str(), - ); + self.command_line + .push_output(format!("{} constraint bar(s) reset.", count).as_str()); } } @@ -1145,20 +1142,15 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(sel)); } else { let scope = self.tabs[i].current_parametric_scope(); - let inferred = self.tabs[i] - .scene - .inferred_parametric_constraints( - scope, - &handles, - &self.auto_constrain_settings, - ); + let inferred = self.tabs[i].scene.inferred_parametric_constraints( + scope, + &handles, + &self.auto_constrain_settings, + ); if inferred.is_empty() { self.command_line.push_output( - format!( - "0 constraint(s) applied to {} object(s).", - handles.len() - ) - .as_str(), + format!("0 constraint(s) applied to {} object(s).", handles.len()) + .as_str(), ); self.tabs[i].scene.deselect_all(); self.refresh_selected_grips(); @@ -1281,8 +1273,7 @@ impl OpenCADStudio { use crate::scene::named_parameters::DrivingValue; use crate::scene::parametric_constraints::{ dynamic_dimension_text, next_angular_parameter_name, - next_dimensional_parameter_name, next_radial_parameter_name, - ConstraintKind, + next_dimensional_parameter_name, next_radial_parameter_name, ConstraintKind, }; let scope = self.tabs[i].current_parametric_scope(); @@ -1322,7 +1313,11 @@ impl OpenCADStudio { }; if self.tabs[i] .scene - .validate_parametric_constraint(kind, &refs, Some(&DrivingValue::Literal(measured))) + .validate_parametric_constraint( + kind, + &refs, + Some(&DrivingValue::Literal(measured)), + ) .is_err() { refused += 1; @@ -1451,10 +1446,12 @@ impl OpenCADStudio { self.tabs[i].scene.deselect_all(); self.tabs[i].dirty = true; self.refresh_properties(); - self.command_line - .push_output(crate::tf!("{converted} associative dimensions converted").as_ref()); self.command_line.push_output( - crate::tf!("{refused} associative dimension(s) could not be converted").as_ref(), + crate::tf!("{converted} associative dimensions converted").as_ref(), + ); + self.command_line.push_output( + crate::tf!("{refused} associative dimension(s) could not be converted") + .as_ref(), ); if let Some(pd) = pending { self.commit_undo_delta(i, pd); @@ -1472,7 +1469,11 @@ impl OpenCADStudio { let (kind, axis, label) = if vertical { (ConstraintKind::Vertical, "Vertical", "Vertical constraint") } else { - (ConstraintKind::Horizontal, "Horizontal", "Horizontal constraint") + ( + ConstraintKind::Horizontal, + "Horizontal", + "Horizontal constraint", + ) }; let new_command = || { if vertical { @@ -1491,13 +1492,14 @@ impl OpenCADStudio { .push_error(&format!("{axis}: select exactly one compatible object.")); } else { let handle = handles[0]; - let reference = self.tabs[i] - .scene - .document - .get_entity(handle) - .and_then(|entity| { - HorizontalConstraintCommand::preselected_reference(entity, handle) - }); + let reference = + self.tabs[i] + .scene + .document + .get_entity(handle) + .and_then(|entity| { + HorizontalConstraintCommand::preselected_reference(entity, handle) + }); if let Some(reference) = reference { let plane = self.tabs[i].ucs_xform().working_plane(); let direction = if vertical { plane.y } else { plane.x }; @@ -1532,13 +1534,14 @@ impl OpenCADStudio { // the reference's own point-or-object prompt. let handles = self.tabs[i].scene.selected_handles_in_order(); if let [handle] = handles.as_slice() { - let reference = self.tabs[i] - .scene - .document - .get_entity(*handle) - .and_then(|entity| { - FixConstraintCommand::preselected_reference(entity, *handle) - }); + let reference = + self.tabs[i] + .scene + .document + .get_entity(*handle) + .and_then(|entity| { + FixConstraintCommand::preselected_reference(entity, *handle) + }); if let Some(reference) = reference { return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint { kind: ConstraintKind::Fixed, @@ -1718,13 +1721,15 @@ impl OpenCADStudio { }) .collect::>(); if picks.len() == 2 && picks[0].reference != picks[1].reference { - return Some(self.apply_cmd_result(CmdResult::AddPerpendicularConstraint { - first: picks[0].reference, - second: picks[1].reference, - first_fixed: picks[0].fixed_reference, - second_start: picks[1].start_reference, - label: "Perpendicular constraint", - })); + return Some(self.apply_cmd_result( + CmdResult::AddPerpendicularConstraint { + first: picks[0].reference, + second: picks[1].reference, + first_fixed: picks[0].fixed_reference, + second_start: picks[1].start_reference, + label: "Perpendicular constraint", + }, + )); } self.tabs[i].scene.deselect_all(); self.command_line.push_error( @@ -1860,21 +1865,21 @@ impl OpenCADStudio { self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); } else { - use crate::command::{ - CmdResult, SymmetricConstraintSelection, - }; + use crate::command::{CmdResult, SymmetricConstraintSelection}; use crate::modules::parametric::SymmetricConstraintCommand; - let refs = (handles.len() == 3).then(|| { - let first = self.tabs[i].scene.document.get_entity(handles[0])?; - let second = self.tabs[i].scene.document.get_entity(handles[1])?; - let axis = self.tabs[i].scene.document.get_entity(handles[2])?; - SymmetricConstraintCommand::preselected_refs( - (first, handles[0]), - (second, handles[1]), - (axis, handles[2]), - ) - }).flatten(); + let refs = (handles.len() == 3) + .then(|| { + let first = self.tabs[i].scene.document.get_entity(handles[0])?; + let second = self.tabs[i].scene.document.get_entity(handles[1])?; + let axis = self.tabs[i].scene.document.get_entity(handles[2])?; + SymmetricConstraintCommand::preselected_refs( + (first, handles[0]), + (second, handles[1]), + (axis, handles[2]), + ) + }) + .flatten(); if let Some([first, second, axis]) = refs { return Some(self.apply_cmd_result(CmdResult::AddSymmetricConstraint { selection: SymmetricConstraintSelection::Objects(first, second), @@ -1962,10 +1967,8 @@ impl OpenCADStudio { // asked for this command. // The reference asks for the circle itself, whatever is // selected, so a pick-first set is left alone. - let name = next_radial_parameter_name( - self.tabs[i].scene.named_parameters(), - diameter, - ); + let name = + next_radial_parameter_name(self.tabs[i].scene.named_parameters(), diameter); let command = DimConstraintCommand::new( if diameter { DimConstraintAxis::Diameter @@ -2537,10 +2540,7 @@ impl OpenCADStudio { .filter(|handle| { matches!( scene.document.get_entity(*handle), - Some( - codec::EntityType::Solid3D(_) - | codec::EntityType::Surface(_) - ) + Some(codec::EntityType::Solid3D(_) | codec::EntityType::Surface(_)) ) }) .collect::>(); diff --git a/src/app/commands/field.rs b/src/app/commands/field.rs index 5c3bcee64..2b1e464c8 100644 --- a/src/app/commands/field.rs +++ b/src/app/commands/field.rs @@ -3,10 +3,12 @@ use crate::app::{Message, OpenCADStudio}; use crate::command::CadCommand; -use crate::modules::annotate::field_cmd::{FieldObjectPickCommand, FieldPlaceCommand, FieldTablePickCommand}; +use crate::modules::annotate::field_cmd::{ + FieldObjectPickCommand, FieldPlaceCommand, FieldTablePickCommand, +}; use crate::ui::window::field_dialog::{ - fields_of, nav_props, object_properties, FieldDialogMsg, FieldDialogState, FieldTarget, DATE_FORMATS, - NAMED_TYPES, + fields_of, nav_props, object_properties, FieldDialogMsg, FieldDialogState, FieldTarget, + DATE_FORMATS, NAMED_TYPES, }; use iced::Task; @@ -18,7 +20,10 @@ impl OpenCADStudio { Some(Task::none()) } // FIELDEVAL — the events that update fields, kept in the drawing. - cmd if cmd == "FIELDEVAL" || cmd.starts_with("FIELDEVAL ") || cmd.starts_with("SETVAR FIELDEVAL") => { + cmd if cmd == "FIELDEVAL" + || cmd.starts_with("FIELDEVAL ") + || cmd.starts_with("SETVAR FIELDEVAL") => + { let value = cmd .trim_start_matches("SETVAR ") .trim_start_matches("FIELDEVAL") @@ -44,8 +49,9 @@ impl OpenCADStudio { } _ => { let (min, max) = (0, 31); - self.command_line - .push_error(&crate::tf!("Requires an integer between {min} and {max}.")); + self.command_line.push_error(&crate::tf!( + "Requires an integer between {min} and {max}." + )); ask(self); } } @@ -58,9 +64,14 @@ impl OpenCADStudio { .and_then(|h| u64::from_str_radix(h, 16).ok()) .map(codec::Handle::new); if let Some(state) = self.field_dialog.as_mut() { - if let Some(entity) = handle.and_then(|h| self.tabs[i].scene.document.get_entity(h)) { + if let Some(entity) = + handle.and_then(|h| self.tabs[i].scene.document.get_entity(h)) + { let props = object_properties(entity); - state.object = Some((entity.common().handle, crate::t!(crate::entities::names::ui_name(entity)).into_owned())); + state.object = Some(( + entity.common().handle, + crate::t!(crate::entities::names::ui_name(entity)).into_owned(), + )); state.object_prop = props.first().copied(); state.object_props = props; } @@ -100,7 +111,11 @@ impl OpenCADStudio { let function = *parts.first()?; let cell = |at: &[&str]| -> Option<(codec::Handle, usize, usize)> { let handle = codec::Handle::new(u64::from_str_radix(at.first()?, 16).ok()?); - let point = glam::DVec3::new(at.get(1)?.parse().ok()?, at.get(2)?.parse().ok()?, at.get(3)?.parse().ok()?); + let point = glam::DVec3::new( + at.get(1)?.parse().ok()?, + at.get(2)?.parse().ok()?, + at.get(3)?.parse().ok()?, + ); let document = &self.tabs[i].scene.document; let codec::EntityType::Table(table) = document.get_entity(handle)? else { return None; @@ -111,7 +126,11 @@ impl OpenCADStudio { let name = |row: usize, column: usize| format!("{}{}", column_letters(column), row + 1); let (table, row, column) = cell(parts.get(1..5)?)?; if function == "Cell" { - return Some(format!("Table(%<\\_ObjId {:X}>%).{}", table.value(), name(row, column))); + return Some(format!( + "Table(%<\\_ObjId {:X}>%).{}", + table.value(), + name(row, column) + )); } let (second_table, row2, column2) = cell(parts.get(5..9)?)?; if second_table != table { @@ -139,7 +158,13 @@ impl OpenCADStudio { .collect(); if let Some(db) = self.sheet_set.db() { let custom = codec::sheet_set::custom_properties(db.sheet_set()); - let names = |flag: i32| custom.iter().filter(|(_, _, f)| f & flag != 0).map(|(n, _, _)| n.clone()).collect(); + let names = |flag: i32| { + custom + .iter() + .filter(|(_, _, f)| f & flag != 0) + .map(|(n, _, _)| n.clone()) + .collect() + }; state.ss_sheet_custom = names(codec::sheet_set::CUSTOM_SHEET_PROP); state.ss_set_custom = names(codec::sheet_set::CUSTOM_SHEET_SET_PROP); state.ss_set_name = db.name().to_string(); @@ -153,8 +178,13 @@ impl OpenCADStudio { } fn field_value(&self, code: &str, objects: &[codec::Handle]) -> String { - crate::entities::field::evaluate(&self.tabs[self.active_tab].scene.document, code, objects, None) - .unwrap_or_else(|| "----".to_string()) + crate::entities::field::evaluate( + &self.tabs[self.active_tab].scene.document, + code, + objects, + None, + ) + .unwrap_or_else(|| "----".to_string()) } fn refresh_field_preview(&mut self) { @@ -165,7 +195,8 @@ impl OpenCADStudio { let preview = if state.complete() { if state.name == "BlockPlaceholder" { // A placeholder shows its property name until a block reference resolves it. - let (label, _, _) = codec::fields::BLOCK_PLACEHOLDER_PROPERTIES[state.placeholder_property]; + let (label, _, _) = + codec::fields::BLOCK_PLACEHOLDER_PROPERTIES[state.placeholder_property]; text_case(label, state.text_case) } else { self.field_value(&code, &objects) @@ -191,16 +222,32 @@ impl OpenCADStudio { .filter(|b| !b.name.starts_with('*')) .map(|b| (b.name.clone(), b.handle)) .collect(), - "Dimstyle" => document.dim_styles.iter().map(|s| (s.name.clone(), s.handle)).collect(), - "Layer" => document.layers.iter().map(|l| (l.name.clone(), l.handle)).collect(), - "Linetype" => document.line_types.iter().map(|l| (l.name.clone(), l.handle)).collect(), + "Dimstyle" => document + .dim_styles + .iter() + .map(|s| (s.name.clone(), s.handle)) + .collect(), + "Layer" => document + .layers + .iter() + .map(|l| (l.name.clone(), l.handle)) + .collect(), + "Linetype" => document + .line_types + .iter() + .map(|l| (l.name.clone(), l.handle)) + .collect(), "Textstyle" => document .text_styles .iter() .filter(|s| !s.name.is_empty()) .map(|s| (s.name.clone(), s.handle)) .collect(), - "View" => document.views.iter().map(|v| (v.name.clone(), v.handle)).collect(), + "View" => document + .views + .iter() + .map(|v| (v.name.clone(), v.handle)) + .collect(), _ => Vec::new(), }; names.sort_by_key(|(n, _)| n.to_lowercase()); @@ -378,8 +425,9 @@ impl OpenCADStudio { match state.target { FieldTarget::NewText => { self.close_active_modal(); - let defaults = - crate::scene::creation_style::current_text_defaults(&self.tabs[i].scene.document); + let defaults = crate::scene::creation_style::current_text_defaults( + &self.tabs[i].scene.document, + ); let command = FieldPlaceCommand::new( value, field.expect("set above"), @@ -416,8 +464,12 @@ impl OpenCADStudio { let target = self.field_dialog.take().map(|s| s.target); self.close_active_modal(); self.active_modal = match target { - Some(FieldTarget::AttdefDefault) if self.attdef_dialog.is_some() => Some(crate::app::ModalKind::AttDef), - Some(FieldTarget::AttdefEdit) if self.attdef_edit.is_some() => Some(crate::app::ModalKind::AttDefEdit), + Some(FieldTarget::AttdefDefault) if self.attdef_dialog.is_some() => { + Some(crate::app::ModalKind::AttDef) + } + Some(FieldTarget::AttdefEdit) if self.attdef_edit.is_some() => { + Some(crate::app::ModalKind::AttDefEdit) + } _ => None, }; } @@ -445,14 +497,20 @@ fn text_case(value: &str, case: usize) -> String { 3 => { let lower = value.to_lowercase(); let mut chars = lower.chars(); - chars.next().map(|c| c.to_uppercase().chain(chars).collect()).unwrap_or_default() + chars + .next() + .map(|c| c.to_uppercase().chain(chars).collect()) + .unwrap_or_default() } 4 => value .split(' ') .map(|word| { let lower = word.to_lowercase(); let mut chars = lower.chars(); - chars.next().map(|c| c.to_uppercase().chain(chars).collect::()).unwrap_or_default() + chars + .next() + .map(|c| c.to_uppercase().chain(chars).collect::()) + .unwrap_or_default() }) .collect::>() .join(" "), diff --git a/src/app/commands/fileops.rs b/src/app/commands/fileops.rs index b5f69b303..cd448e0e5 100644 --- a/src/app/commands/fileops.rs +++ b/src/app/commands/fileops.rs @@ -28,8 +28,9 @@ impl OpenCADStudio { // Tabs without a path (never saved) are skipped with a note. "SAVEALL" => { if self.read_only { - self.command_line - .push_error(crate::t!("Read-only session (--read-only): saving is disabled.").as_ref()); + self.command_line.push_error( + crate::t!("Read-only session (--read-only): saving is disabled.").as_ref(), + ); return Some(Task::none()); } #[cfg(not(target_arch = "wasm32"))] @@ -41,47 +42,47 @@ impl OpenCADStudio { continue; } if let Some(path) = self.tabs[t].current_path.clone() { - if self - .active_save_jobs - .contains_key(&self.tabs[t].id) - { - self.command_line.push_info(crate::tf!( - "SAVEALL: {} already has a save running", - path.display() - ).as_ref()); + if self.active_save_jobs.contains_key(&self.tabs[t].id) { + self.command_line.push_info( + crate::tf!( + "SAVEALL: {} already has a save running", + path.display() + ) + .as_ref(), + ); skipped += 1; continue; } - match self.save_tab_synchronously_protected( - t, - path.clone(), - false, - ) { + match self.save_tab_synchronously_protected(t, path.clone(), false) { Ok(()) => { saved += 1; } - Err(e) => self.command_line.push_error(crate::tf!( - "SAVEALL: {} failed: {e}", - path.display() - ).as_ref()), + Err(e) => self.command_line.push_error( + crate::tf!("SAVEALL: {} failed: {e}", path.display()).as_ref(), + ), } } else { skipped += 1; } } - self.command_line.push_output(crate::tf!( - "SAVEALL: saved {saved} drawing(s){}.", - if skipped > 0 { - crate::tf!("; {skipped} need SAVEAS (no file path yet)").into_owned() - } else { - String::new() - } - ).as_ref()); + self.command_line.push_output( + crate::tf!( + "SAVEALL: saved {saved} drawing(s){}.", + if skipped > 0 { + crate::tf!("; {skipped} need SAVEAS (no file path yet)") + .into_owned() + } else { + String::new() + } + ) + .as_ref(), + ); } #[cfg(target_arch = "wasm32")] { - self.command_line - .push_info(crate::t!("SAVEALL: save each tab individually in the web build.").as_ref()); + self.command_line.push_info( + crate::t!("SAVEALL: save each tab individually in the web build.").as_ref(), + ); } return Some(Task::none()); } @@ -105,7 +106,8 @@ impl OpenCADStudio { // without undoing any work done since. "OOPS" => { if self.oops_cache.is_empty() { - self.command_line.push_info(crate::t!("OOPS: nothing to restore.").as_ref()); + self.command_line + .push_info(crate::t!("OOPS: nothing to restore.").as_ref()); } else { let restored = std::mem::take(&mut self.oops_cache); let pending = self.begin_undo(i, "OOPS", restored.len(), true); @@ -202,7 +204,11 @@ impl OpenCADStudio { } for d in deps { let dp = PathBuf::from(&d); - let resolved = if dp.is_absolute() { dp } else { parent.join(&dp) }; + let resolved = if dp.is_absolute() { + dp + } else { + parent.join(&dp) + }; if resolved.exists() { if let Some(fname) = resolved.file_name() { if std::fs::copy(&resolved, folder.join(fname)).is_ok() { @@ -211,18 +217,23 @@ impl OpenCADStudio { } } } - self.command_line.push_output(crate::tf!( - "{cmd}: packaged {copied} file(s) into {}", - folder.display() - ).as_ref()); + self.command_line.push_output( + crate::tf!( + "{cmd}: packaged {copied} file(s) into {}", + folder.display() + ) + .as_ref(), + ); } Err(e) => self .command_line .push_error(crate::tf!("{cmd}: cannot create folder ({e}).").as_ref()), } } else { - self.command_line - .push_error(crate::t!("ARCHIVE: save the drawing first (it has no file path yet).").as_ref()); + self.command_line.push_error( + crate::t!("ARCHIVE: save the drawing first (it has no file path yet).") + .as_ref(), + ); } } @@ -241,11 +252,14 @@ impl OpenCADStudio { "PERF" => { self.perf_hud = !self.perf_hud; crate::perf::set_ui_enabled(self.perf_hud); - self.command_line.push_info(crate::t!(if self.perf_hud { - "PERF panel on — tracing render and interaction costs" - } else { - "PERF panel off" - }).as_ref()); + self.command_line.push_info( + crate::t!(if self.perf_hud { + "PERF panel on — tracing render and interaction costs" + } else { + "PERF panel off" + }) + .as_ref(), + ); return Some(Task::none()); } @@ -277,14 +291,16 @@ impl OpenCADStudio { self.model_space.mode = crate::app::config::ModelSpaceMode::MatchTheme; self.sync_model_space_theme(true); self.persist_settings_if_changed(); - self.command_line - .push_output(crate::t!("Model space background set to Match Theme.").as_ref()); + self.command_line.push_output( + crate::t!("Model space background set to Match Theme.").as_ref(), + ); } else if first_arg == "CLASSIC" { self.model_space.mode = crate::app::config::ModelSpaceMode::ClassicDark; self.sync_model_space_theme(true); self.persist_settings_if_changed(); - self.command_line - .push_output(crate::t!("Model space background set to Classic CAD Dark.").as_ref()); + self.command_line.push_output( + crate::t!("Model space background set to Classic CAD Dark.").as_ref(), + ); } else if first_arg == "DEFAULT" || first_arg == "RESET" { if is_paper { self.model_space.custom_paper_bg = None; @@ -300,31 +316,48 @@ impl OpenCADStudio { .push_output(crate::t!("Background reset to default.").as_ref()); } else if first_arg == "DESK" { let sub_args = &args[1..]; - let sub_first = sub_args.first().map(|s| s.to_uppercase()).unwrap_or_default(); + let sub_first = sub_args + .first() + .map(|s| s.to_uppercase()) + .unwrap_or_default(); if sub_first == "DEFAULT" || sub_first == "RESET" { self.model_space.custom_desk_bg = None; self.sync_model_space_theme(false); self.persist_settings_if_changed(); - self.command_line - .push_output(crate::t!("Paper desk surround background reset to default.").as_ref()); + self.command_line.push_output( + crate::t!("Paper desk surround background reset to default.").as_ref(), + ); } else if let Some(rgba) = parse_background_color(sub_args) { let [r, g, b, _] = rgba; - let rgb = [(r * 255.0).round() as u8, (g * 255.0).round() as u8, (b * 255.0).round() as u8]; + let rgb = [ + (r * 255.0).round() as u8, + (g * 255.0).round() as u8, + (b * 255.0).round() as u8, + ]; self.model_space.custom_desk_bg = Some(rgb); self.sync_model_space_theme(false); self.persist_settings_if_changed(); - self.command_line.push_output(crate::tf!( - "Desk surround background: rgb({}, {}, {})", - rgb[0], - rgb[1], - rgb[2] - ).as_ref()); + self.command_line.push_output( + crate::tf!( + "Desk surround background: rgb({}, {}, {})", + rgb[0], + rgb[1], + rgb[2] + ) + .as_ref(), + ); } else { - self.command_line.push_info(crate::t!("Usage: BACKGROUND DESK | DEFAULT").as_ref()); + self.command_line.push_info( + crate::t!("Usage: BACKGROUND DESK | DEFAULT").as_ref(), + ); } } else if let Some(rgba) = parse_background_color(&args) { let [r, g, b, _] = rgba; - let rgb = [(r * 255.0).round() as u8, (g * 255.0).round() as u8, (b * 255.0).round() as u8]; + let rgb = [ + (r * 255.0).round() as u8, + (g * 255.0).round() as u8, + (b * 255.0).round() as u8, + ]; if is_paper { self.model_space.custom_paper_bg = Some(rgb); self.paper_bg_input = crate::app::config::rgb_to_hex(rgb); @@ -335,12 +368,9 @@ impl OpenCADStudio { } self.sync_model_space_theme(true); self.persist_settings_if_changed(); - self.command_line.push_output(crate::tf!( - "Background: rgb({}, {}, {})", - rgb[0], - rgb[1], - rgb[2] - ).as_ref()); + self.command_line.push_output( + crate::tf!("Background: rgb({}, {}, {})", rgb[0], rgb[1], rgb[2]).as_ref(), + ); } else { self.command_line.push_info( crate::t!("Usage: BACKGROUND (0–255) | THEME|CLASSIC|DEFAULT|DESK|BLACK|DARKGRAY|GRAY|LIGHTGRAY|WHITE").as_ref(), @@ -397,15 +427,19 @@ impl OpenCADStudio { .filter(|l| !l.starts_with('#') && !l.starts_with(';')) .map(|l| Task::done(Message::ScriptLine(l.to_string()))) .collect(); - self.command_line.push_output(crate::tf!( - "SCRIPT: running {} command(s) from {clean_path}.", - cmds.len() - ).as_ref()); + self.command_line.push_output( + crate::tf!( + "SCRIPT: running {} command(s) from {clean_path}.", + cmds.len() + ) + .as_ref(), + ); return Some(Task::batch(cmds)); } Err(e) => { - self.command_line - .push_error(crate::tf!("SCRIPT: cannot read {clean_path}: {e}").as_ref()); + self.command_line.push_error( + crate::tf!("SCRIPT: cannot read {clean_path}: {e}").as_ref(), + ); } } } @@ -480,17 +514,30 @@ mod tests { // Feed standard multi-line CAD script sequence let _ = app.update(Message::ScriptLine("LINE".into())); - assert!(app.tabs[i].active_cmd.is_some(), "LINE command should be active"); + assert!( + app.tabs[i].active_cmd.is_some(), + "LINE command should be active" + ); let _ = app.update(Message::ScriptLine("0,0".into())); - assert!(app.tabs[i].active_cmd.is_some(), "LINE command should stay active after first point"); + assert!( + app.tabs[i].active_cmd.is_some(), + "LINE command should stay active after first point" + ); let _ = app.update(Message::ScriptLine("10,10".into())); let _ = app.update(Message::ScriptLine("".into())); // Enter to finish let after_count = app.tabs[i].scene.document.entities().count(); - assert_eq!(after_count, before_count + 1, "Line entity should have been created"); - assert!(app.tabs[i].active_cmd.is_none(), "LINE command should be finished"); + assert_eq!( + after_count, + before_count + 1, + "Line entity should have been created" + ); + assert!( + app.tabs[i].active_cmd.is_none(), + "LINE command should be finished" + ); } #[test] @@ -502,8 +549,15 @@ mod tests { let _ = app.update(Message::ScriptLine("CIRCLE 5,5 2".into())); let after_count = app.tabs[i].scene.document.entities().count(); - assert_eq!(after_count, before_count + 1, "Circle entity should have been created"); - assert!(app.tabs[i].active_cmd.is_none(), "CIRCLE command should be finished"); + assert_eq!( + after_count, + before_count + 1, + "Circle entity should have been created" + ); + assert!( + app.tabs[i].active_cmd.is_none(), + "CIRCLE command should be finished" + ); } #[test] @@ -515,7 +569,11 @@ mod tests { let _ = app.update(Message::ScriptLine("_CIRCLE 10,10 3".into())); let after_count = app.tabs[i].scene.document.entities().count(); - assert_eq!(after_count, before_count + 1, "_CIRCLE should resolve to CIRCLE and create entity"); + assert_eq!( + after_count, + before_count + 1, + "_CIRCLE should resolve to CIRCLE and create entity" + ); } #[test] @@ -546,7 +604,11 @@ CIRCLE 50,50 15 ZOOM EXTENTS "#; - for line in SCRIPT.lines().map(str::trim).filter(|l| !l.starts_with('#') && !l.starts_with(';')) { + for line in SCRIPT + .lines() + .map(str::trim) + .filter(|l| !l.starts_with('#') && !l.starts_with(';')) + { let _ = app.update(Message::ScriptLine(line.to_string())); } // 4 square edges + 2 diagonal lines + 2 circles = 8 entities @@ -572,7 +634,10 @@ ZOOM EXTENTS // 2. Blank line when idle repeats LINE let _ = app.update(Message::ScriptLine("".into())); - assert!(app.tabs[i].active_cmd.is_some(), "Blank line should repeat LINE"); + assert!( + app.tabs[i].active_cmd.is_some(), + "Blank line should repeat LINE" + ); let _ = app.update(Message::ScriptLine("20,20".into())); let _ = app.update(Message::ScriptLine("30,30".into())); let _ = app.update(Message::ScriptLine("".into())); // finishes second LINE @@ -586,7 +651,10 @@ ZOOM EXTENTS // Blank line repeats CIRCLE let _ = app.update(Message::ScriptLine("".into())); - assert!(app.tabs[i].active_cmd.is_some(), "Blank line should repeat CIRCLE"); + assert!( + app.tabs[i].active_cmd.is_some(), + "Blank line should repeat CIRCLE" + ); let _ = app.update(Message::ScriptLine("15,15".into())); let _ = app.update(Message::ScriptLine("3".into())); assert_eq!(app.tabs[i].scene.document.entities().count(), 4); diff --git a/src/app/commands/inquiry.rs b/src/app/commands/inquiry.rs index d0a31134b..24bfd2a80 100644 --- a/src/app/commands/inquiry.rs +++ b/src/app/commands/inquiry.rs @@ -4,12 +4,15 @@ impl OpenCADStudio { /// Align every selected object or complete group by one edge or center. fn align_selected_bounds(&mut self, i: usize, command: &str) { use crate::command::EntityTransform; - use kernel::space::BoundsAlignment; use glam::DVec3; + use kernel::space::BoundsAlignment; let (alignment, label) = match command { "ALIGNLEFT" => (BoundsAlignment::Left, "Align Left"), - "ALIGNHCENTER" => (BoundsAlignment::HorizontalCenter, "Align Horizontal Centers"), + "ALIGNHCENTER" => ( + BoundsAlignment::HorizontalCenter, + "Align Horizontal Centers", + ), "ALIGNRIGHT" => (BoundsAlignment::Right, "Align Right"), "ALIGNTOP" => (BoundsAlignment::Top, "Align Top"), "ALIGNVCENTER" => (BoundsAlignment::VerticalCenter, "Align Vertical Centers"), @@ -62,19 +65,17 @@ impl OpenCADStudio { .expect("validated at least two finite bounds"); let pending = self.begin_undo(i, label, handles.len(), true); for ((unit, _), [dx, dy]) in bounds.iter().zip(offsets) { - self.tabs[i].scene.transform_entities( - unit, - &EntityTransform::Translate(DVec3::new(dx, dy, 0.0)), - ); + self.tabs[i] + .scene + .transform_entities(unit, &EntityTransform::Translate(DVec3::new(dx, dy, 0.0))); } self.tabs[i].dirty = true; self.refresh_properties(); if let Some(pending) = pending { self.commit_undo_delta(i, pending); } - self.command_line.push_output( - crate::tf!("{command}: aligned {} object(s).", bounds.len()).as_ref(), - ); + self.command_line + .push_output(crate::tf!("{command}: aligned {} object(s).", bounds.len()).as_ref()); } pub(super) fn dispatch_inquiry(&mut self, cmd: &str, i: usize) -> Option> { @@ -94,7 +95,8 @@ impl OpenCADStudio { "DESELECT" | "DESELALL" => { self.tabs[i].scene.deselect_all(); - self.command_line.push_output(crate::t!("Deselected.").as_ref()); + self.command_line + .push_output(crate::t!("Deselected.").as_ref()); self.refresh_properties(); } @@ -129,14 +131,21 @@ impl OpenCADStudio { || cmd.starts_with("QUICKCALC ") || cmd.starts_with("QC ") => { - let expr = cmd.splitn(2, char::is_whitespace).nth(1).unwrap_or("").trim(); + let expr = cmd + .splitn(2, char::is_whitespace) + .nth(1) + .unwrap_or("") + .trim(); if expr.is_empty() { - self.command_line - .push_info(crate::t!("Usage: CAL e.g. CAL (2+3)*4").as_ref()); + self.command_line.push_info( + crate::t!("Usage: CAL e.g. CAL (2+3)*4").as_ref(), + ); } else { match arith_eval(expr) { Ok(v) => self.command_line.push_output(crate::tf!("= {v}").as_ref()), - Err(e) => self.command_line.push_error(crate::tf!("CAL: {e}").as_ref()), + Err(e) => self + .command_line + .push_error(crate::tf!("CAL: {e}").as_ref()), } } } @@ -145,8 +154,9 @@ impl OpenCADStudio { "LIST" => { let selected: Vec<_> = self.tabs[i].scene.selected_entities(); if selected.is_empty() { - self.command_line - .push_error(crate::t!("LIST: no entities selected. Select entities first.").as_ref()); + self.command_line.push_error( + crate::t!("LIST: no entities selected. Select entities first.").as_ref(), + ); } else { for (handle, _) in &selected { if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) { @@ -165,18 +175,21 @@ impl OpenCADStudio { }; // Entity-specific details let details = entity_list_details(entity); - self.command_line.push_output(crate::tf!( - "{type_name} Handle:{:X} Layer:{} Color:{} LT:{}{}", - handle.value(), - common.layer, - color_str, - linetype, - if details.is_empty() { - String::new() - } else { - format!("\n {details}") - } - ).as_ref()); + self.command_line.push_output( + crate::tf!( + "{type_name} Handle:{:X} Layer:{} Color:{} LT:{}{}", + handle.value(), + common.layer, + color_str, + linetype, + if details.is_empty() { + String::new() + } else { + format!("\n {details}") + } + ) + .as_ref(), + ); } } } @@ -190,8 +203,10 @@ impl OpenCADStudio { let scene = &self.tabs[i].scene; let selected = scene.selected_entities(); if selected.is_empty() { - self.command_line - .push_error(crate::t!("SELHANDLES: no entities selected. Select entities first.").as_ref()); + self.command_line.push_error( + crate::t!("SELHANDLES: no entities selected. Select entities first.") + .as_ref(), + ); } else { use std::collections::BTreeMap; let space = if scene.current_layout == "Model" { @@ -212,18 +227,23 @@ impl OpenCADStudio { } } handles.sort_unstable(); - let types: Vec = - type_counts.iter().map(|(t, n)| format!("{t}×{n}")).collect(); + let types: Vec = type_counts + .iter() + .map(|(t, n)| format!("{t}×{n}")) + .collect(); let list: Vec = handles.iter().map(|h| format!("{:X}", h)).collect(); let mut msg = crate::tf!( "SELHANDLES: {} selected in {}\n Types: {}", handles.len(), space, types.join(", ") - ).into_owned(); + ) + .into_owned(); if !block_counts.is_empty() { - let blocks: Vec = - block_counts.iter().map(|(b, n)| format!("{b}×{n}")).collect(); + let blocks: Vec = block_counts + .iter() + .map(|(b, n)| format!("{b}×{n}")) + .collect(); msg.push_str(&crate::tf!("\n Blocks: {}", blocks.join(", ")).into_owned()); } msg.push_str(&crate::tf!("\n Handles: {}", list.join(",")).into_owned()); @@ -257,9 +277,12 @@ impl OpenCADStudio { let details = entity_list_details(entity); format!( "{type_name} {}:{:X} {}:{} {}:{} LT:{}{}", - crate::t!("Handle"), common.handle.value(), - crate::t!("Layer"), common.layer, - crate::t!("Color"), color_str, + crate::t!("Handle"), + common.handle.value(), + crate::t!("Layer"), + common.layer, + crate::t!("Color"), + color_str, linetype, if details.is_empty() { String::new() @@ -296,7 +319,9 @@ impl OpenCADStudio { } let mut cmd = JoinCommand::new(); if let Some(handle) = selected.first() { - if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle).cloned() { cmd = cmd.with_source(*handle, entity); } + if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle).cloned() { + cmd = cmd.with_source(*handle, entity); + } } self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); @@ -325,27 +350,26 @@ impl OpenCADStudio { .filter_map(|e| { let h = e.common().handle.value(); match e { - codec::EntityType::LwPolyline(_) - | codec::EntityType::Polyline2D(_) => Some(( - h, - PeditTarget { - is_poly: true, - convertible: false, - mesh_size: None, - mesh_closed: None, - }, - )), - codec::EntityType::Line(_) | codec::EntityType::Arc(_) => { + codec::EntityType::LwPolyline(_) | codec::EntityType::Polyline2D(_) => { Some(( h, PeditTarget { - is_poly: false, - convertible: true, + is_poly: true, + convertible: false, mesh_size: None, mesh_closed: None, }, )) } + codec::EntityType::Line(_) | codec::EntityType::Arc(_) => Some(( + h, + PeditTarget { + is_poly: false, + convertible: true, + mesh_size: None, + mesh_closed: None, + }, + )), codec::EntityType::PolygonMesh(mesh) => Some(( h, PeditTarget { @@ -380,9 +404,7 @@ impl OpenCADStudio { } "MLEDIT" => { - use crate::modules::draw::modify::mledit::{ - MlineEditCommand, MlineEditTarget, - }; + use crate::modules::draw::modify::mledit::{MlineEditCommand, MlineEditTarget}; let document = &self.tabs[i].scene.document; let targets = document .entities() @@ -409,13 +431,16 @@ impl OpenCADStudio { "SPLINEDIT" => { use crate::modules::draw::modify::splinedit::SplineditCommand; - let mut cmd_obj = SplineditCommand::new().with_delete_source(self.delete_objects != 0); + let mut cmd_obj = + SplineditCommand::new().with_delete_source(self.delete_objects != 0); let selected: Vec<_> = self.tabs[i].scene.selected.iter().copied().collect(); if let [handle] = selected.as_slice() { if let Some(entity @ codec::EntityType::Spline(_)) = self.tabs[i].scene.document.get_entity(*handle).cloned() { - if self.reject_locked_edit(i, *handle) { return Some(Task::none()); } + if self.reject_locked_edit(i, *handle) { + return Some(Task::none()); + } cmd_obj.inject_picked_entity(entity); cmd_obj.on_entity_pick(*handle, glam::DVec3::ZERO); } @@ -437,15 +462,16 @@ impl OpenCADStudio { use crate::modules::draw::modify::refedit::RefEditPickCommand; // If a session is already active, tell the user. if self.tabs[i].refedit_session.is_some() { - self.command_line - .push_error(crate::t!("REFEDIT: a session is already active. Use REFCLOSE first.").as_ref()); + self.command_line.push_error( + crate::t!("REFEDIT: a session is already active. Use REFCLOSE first.") + .as_ref(), + ); } else { // Check if a single INSERT is already selected. let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect(); if selected.len() == 1 { - if let Some(codec::EntityType::Insert(_)) = - selected.first().map(|(_, e)| e) + if let Some(codec::EntityType::Insert(_)) = selected.first().map(|(_, e)| e) { let handle = selected[0].0; // Skip pick phase — jump straight to begin. @@ -463,15 +489,15 @@ impl OpenCADStudio { "BEDIT" => { use crate::modules::draw::modify::block_edit::BlockEditPickCommand; if self.tabs[i].refedit_session.is_some() { - self.command_line - .push_error(crate::t!("BEDIT: finish the active REFEDIT (REFCLOSE) first.").as_ref()); + self.command_line.push_error( + crate::t!("BEDIT: finish the active REFEDIT (REFCLOSE) first.").as_ref(), + ); } else { // Jump straight to begin when a single INSERT is preselected. let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect(); if selected.len() == 1 { - if let Some(codec::EntityType::Insert(_)) = - selected.first().map(|(_, e)| e) + if let Some(codec::EntityType::Insert(_)) = selected.first().map(|(_, e)| e) { let handle = selected[0].0; let _ = @@ -498,31 +524,28 @@ impl OpenCADStudio { let block_name = match self.tabs[i].scene.document.get_entity(insert_handle) { Some(codec::EntityType::Insert(ins)) => ins.block_name.clone(), _ => { - self.command_line - .push_error(crate::t!("BEDIT: selected object is not a block reference.").as_ref()); + self.command_line.push_error( + crate::t!("BEDIT: selected object is not a block reference.").as_ref(), + ); return Some(Task::none()); } }; // Resolve the block record; reject external references (xrefs). - let (br_handle, is_xref) = match self.tabs[i] - .scene - .document - .block_records - .get(&block_name) - { - Some(br) => (br.handle, br.flags.is_xref), - None => { - self.command_line.push_error(crate::tf!( - "BEDIT: block \"{}\" not found.", - block_name - ).as_ref()); - return Some(Task::none()); - } - }; + let (br_handle, is_xref) = + match self.tabs[i].scene.document.block_records.get(&block_name) { + Some(br) => (br.handle, br.flags.is_xref), + None => { + self.command_line.push_error( + crate::tf!("BEDIT: block \"{}\" not found.", block_name).as_ref(), + ); + return Some(Task::none()); + } + }; if is_xref { - self.command_line - .push_error(crate::t!("BEDIT: cannot edit an external reference (xref).").as_ref()); + self.command_line.push_error( + crate::t!("BEDIT: cannot edit an external reference (xref).").as_ref(), + ); return Some(Task::none()); } if self.tabs[i] @@ -531,9 +554,7 @@ impl OpenCADStudio { .any(|session| session.br_handle == br_handle) { self.tabs[i].active_cmd = None; - return Some(Task::done(Message::BlockEditSwitch( - block_name.clone(), - ))); + return Some(Task::done(Message::BlockEditSwitch(block_name.clone()))); } // Snapshot the complete block definition so Discard can restore @@ -554,9 +575,7 @@ impl OpenCADStudio { .scene .block_definition_dependent_handles(br_handle) .into_iter() - .filter_map(|handle| { - self.tabs[i].scene.document.get_entity_arc(handle) - }) + .filter_map(|handle| self.tabs[i].scene.document.get_entity_arc(handle)) .collect(); let reference_attributes: Vec<_> = self.tabs[i] .scene @@ -567,10 +586,7 @@ impl OpenCADStudio { if reference.block_name.eq_ignore_ascii_case(&block_name) && !reference.attributes.is_empty() => { - Some(( - reference.common.handle, - reference.attributes.clone(), - )) + Some((reference.common.handle, reference.attributes.clone())) } _ => None, }) @@ -649,10 +665,13 @@ impl OpenCADStudio { self.refresh_properties(); self.tabs[i].active_cmd = None; self.tabs[i].dirty = true; - self.command_line.push_info(crate::tf!( - "BEDIT: Editing block \"{}\". Use Save Block or Discard to finish.", - block_name - ).as_ref()); + self.command_line.push_info( + crate::tf!( + "BEDIT: Editing block \"{}\". Use Save Block or Discard to finish.", + block_name + ) + .as_ref(), + ); } "BEDIT_SAVE" => { @@ -666,8 +685,9 @@ impl OpenCADStudio { return Some(Task::none()); } Some(_) => { - self.command_line - .push_error(crate::t!("BEDIT_SAVE: invalid block editor state.").as_ref()); + self.command_line.push_error( + crate::t!("BEDIT_SAVE: invalid block editor state.").as_ref(), + ); return Some(Task::none()); } }; @@ -679,10 +699,13 @@ impl OpenCADStudio { &session.return_camera, ); self.tabs[i].dirty = true; - self.command_line.push_output(crate::tf!( - "BEDIT: Block \"{}\" saved. All references updated.", - session.block_name - ).as_ref()); + self.command_line.push_output( + crate::tf!( + "BEDIT: Block \"{}\" saved. All references updated.", + session.block_name + ) + .as_ref(), + ); } "BEDIT_DISCARD" => { @@ -691,13 +714,15 @@ impl OpenCADStudio { self.tabs[i].block_edits.remove(index) } None => { - self.command_line - .push_error(crate::t!("BEDIT_DISCARD: no block editor is open.").as_ref()); + self.command_line.push_error( + crate::t!("BEDIT_DISCARD: no block editor is open.").as_ref(), + ); return Some(Task::none()); } Some(_) => { - self.command_line - .push_error(crate::t!("BEDIT_DISCARD: invalid block editor state.").as_ref()); + self.command_line.push_error( + crate::t!("BEDIT_DISCARD: invalid block editor state.").as_ref(), + ); return Some(Task::none()); } }; @@ -755,10 +780,9 @@ impl OpenCADStudio { &return_camera, ); self.tabs[i].dirty = true; - self.command_line.push_output(crate::tf!( - "BEDIT: Block \"{}\" edit discarded.", - block_name - ).as_ref()); + self.command_line.push_output( + crate::tf!("BEDIT: Block \"{}\" edit discarded.", block_name).as_ref(), + ); } cmd if cmd.starts_with("REFEDIT_BEGIN:") => { @@ -777,8 +801,9 @@ impl OpenCADStudio { let insert = match self.tabs[i].scene.document.get_entity(insert_handle) { Some(codec::EntityType::Insert(ins)) => ins.clone(), _ => { - self.command_line - .push_error(crate::t!("REFEDIT: selected object is not an INSERT.").as_ref()); + self.command_line.push_error( + crate::t!("REFEDIT: selected object is not an INSERT.").as_ref(), + ); return Some(Task::none()); } }; @@ -814,10 +839,10 @@ impl OpenCADStudio { { Some(br) => br.handle, None => { - self.command_line.push_error(crate::tf!( - "REFEDIT: block \"{}\" not found.", - insert.block_name - ).as_ref()); + self.command_line.push_error( + crate::tf!("REFEDIT: block \"{}\" not found.", insert.block_name) + .as_ref(), + ); return Some(Task::none()); } }; @@ -845,7 +870,8 @@ impl OpenCADStudio { }; if block_entities.is_empty() { - self.command_line.push_error(crate::t!("REFEDIT: block is empty.").as_ref()); + self.command_line + .push_error(crate::t!("REFEDIT: block is empty.").as_ref()); return Some(Task::none()); } @@ -856,8 +882,7 @@ impl OpenCADStudio { forward, inverse, // Everything allocated from here on is part of the session. - handle_watermark: self - .tabs[i] + handle_watermark: self.tabs[i] .scene .document .next_handle() @@ -999,10 +1024,13 @@ impl OpenCADStudio { } self.tabs[i].dirty = true; - self.command_line.push_output(crate::tf!( - "REFCLOSE: Block \"{}\" saved. All references updated.", - session.block_name - ).as_ref()); + self.command_line.push_output( + crate::tf!( + "REFCLOSE: Block \"{}\" saved. All references updated.", + session.block_name + ) + .as_ref(), + ); // End the edit fade before rebuilding, so the restored geometry // recolours bright. (#136) self.tabs[i].scene.set_refedit_keep(None); @@ -1077,7 +1105,8 @@ impl OpenCADStudio { "MEASURE" => { use crate::modules::draw::inquiry::divide::MeasureCommand; - let cmd = MeasureCommand::new().with_blocks(self.tabs[i].scene.custom_block_names()); + let cmd = + MeasureCommand::new().with_blocks(self.tabs[i].scene.custom_block_names()); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } @@ -1108,8 +1137,10 @@ impl OpenCADStudio { "MASSPROP" => { let selected = self.tabs[i].scene.selected_entities(); if selected.is_empty() { - self.command_line - .push_error(crate::t!("MASSPROP: no entities selected. Select entities first.").as_ref()); + self.command_line.push_error( + crate::t!("MASSPROP: no entities selected. Select entities first.") + .as_ref(), + ); } else { for (handle, _) in &selected { if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle) { @@ -1123,13 +1154,17 @@ impl OpenCADStudio { _ => None, }; if let Some(acis) = acis { - let properties = crate::scene::convert::solid3d_tess::kernel_acis_body(acis) - .and_then(|body| kernel::brep::mass_properties(&body)); + let properties = + crate::scene::convert::solid3d_tess::kernel_acis_body(acis) + .and_then(|body| kernel::brep::mass_properties(&body)); let Some(p) = properties else { - self.command_line.push_error(crate::tf!( - "MASSPROP: {} has no closed body to measure.", - crate::entities::names::dxf_name(entity), - ).as_ref()); + self.command_line.push_error( + crate::tf!( + "MASSPROP: {} has no closed body to measure.", + crate::entities::names::dxf_name(entity), + ) + .as_ref(), + ); continue; }; let [x, y, z] = p.centroid; @@ -1137,49 +1172,67 @@ impl OpenCADStudio { let [pxy, pyz, pzx] = p.product_of_inertia; let [rx, ry, rz] = p.radii_of_gyration; let [i1, i2, i3] = p.principal_moments; - self.command_line.push_output(crate::tf!( - "{} Volume={:.4} Centroid=({:.4},{:.4},{:.4})", - crate::entities::names::dxf_name(entity), - p.volume, - x, - y, - z, - ).as_ref()); - self.command_line.push_output(crate::tf!( - "Moments of inertia X={:.4} Y={:.4} Z={:.4}", - ix, - iy, - iz, - ).as_ref()); - self.command_line.push_output(crate::tf!( - "Products of inertia XY={:.4} YZ={:.4} ZX={:.4}", - pxy, - pyz, - pzx, - ).as_ref()); - self.command_line.push_output(crate::tf!( - "Radii of gyration X={:.4} Y={:.4} Z={:.4}", - rx, - ry, - rz, - ).as_ref()); - self.command_line.push_output(crate::tf!( + self.command_line.push_output( + crate::tf!( + "{} Volume={:.4} Centroid=({:.4},{:.4},{:.4})", + crate::entities::names::dxf_name(entity), + p.volume, + x, + y, + z, + ) + .as_ref(), + ); + self.command_line.push_output( + crate::tf!( + "Moments of inertia X={:.4} Y={:.4} Z={:.4}", + ix, + iy, + iz, + ) + .as_ref(), + ); + self.command_line.push_output( + crate::tf!( + "Products of inertia XY={:.4} YZ={:.4} ZX={:.4}", + pxy, + pyz, + pzx, + ) + .as_ref(), + ); + self.command_line.push_output( + crate::tf!( + "Radii of gyration X={:.4} Y={:.4} Z={:.4}", + rx, + ry, + rz, + ) + .as_ref(), + ); + self.command_line.push_output( + crate::tf!( "Principal moments about centroid I={:.4} J={:.4} K={:.4}", i1, i2, i3, - ).as_ref()); + ) + .as_ref(), + ); continue; } if let Some(props) = entity.mass_props() { - self.command_line.push_output(crate::tf!( - "{} Area={:.4} Perimeter={:.4} Centroid=({:.4},{:.4})", - crate::entities::names::dxf_name(entity), - props.area, - props.perimeter, - props.cx, - props.cy, - ).as_ref()); + self.command_line.push_output( + crate::tf!( + "{} Area={:.4} Perimeter={:.4} Centroid=({:.4},{:.4})", + crate::entities::names::dxf_name(entity), + props.area, + props.perimeter, + props.cx, + props.cy, + ) + .as_ref(), + ); } } } @@ -1189,10 +1242,10 @@ impl OpenCADStudio { // ── FLATTEN — move selected (or all) entities to Z=0 ───────────── "FLATTEN" => { let scene = &self.tabs[i].scene; - let handles = - crate::modules::draw::modify::flatten::collect_flatten_handles(scene); + let handles = crate::modules::draw::modify::flatten::collect_flatten_handles(scene); if handles.is_empty() { - self.command_line.push_error(crate::t!("FLATTEN: no entities.").as_ref()); + self.command_line + .push_error(crate::t!("FLATTEN: no entities.").as_ref()); } else { let candidate_count = handles.len(); let updates = crate::modules::draw::modify::flatten::plan_flatten( @@ -1216,11 +1269,14 @@ impl OpenCADStudio { self.tabs[i].dirty = true; self.refresh_properties(); } - self.command_line.push_output(crate::tf!( - "FLATTEN: {} entity(ies) moved to Z=0; {} unchanged or unsupported.", - moved, - candidate_count.saturating_sub(moved) - ).as_ref()); + self.command_line.push_output( + crate::tf!( + "FLATTEN: {} entity(ies) moved to Z=0; {} unchanged or unsupported.", + moved, + candidate_count.saturating_sub(moved) + ) + .as_ref(), + ); } } @@ -1257,8 +1313,9 @@ impl OpenCADStudio { .collect(); if prop.is_empty() { - self.command_line - .push_info(crate::t!("Usage: QSELECT TYPE|LAYER|COLOR|LINETYPE ").as_ref()); + self.command_line.push_info( + crate::t!("Usage: QSELECT TYPE|LAYER|COLOR|LINETYPE ").as_ref(), + ); } else if matched.is_empty() { self.command_line .push_output(crate::t!("QSELECT: no matching entities.").as_ref()); @@ -1267,8 +1324,9 @@ impl OpenCADStudio { for h in &matched { self.tabs[i].scene.select_entity(*h, false); } - self.command_line - .push_output(crate::tf!("QSELECT: {} entity(ies) selected.", matched.len()).as_ref()); + self.command_line.push_output( + crate::tf!("QSELECT: {} entity(ies) selected.", matched.len()).as_ref(), + ); self.refresh_properties(); } } @@ -1299,15 +1357,16 @@ impl OpenCADStudio { // Split at " REPLACE " keyword (case-insensitive). ASCII case // mapping keeps byte offsets valid for slicing `rest`. - let (search, replacement) = if let Some(pos) = rest.to_ascii_uppercase().find(" REPLACE ") - { - (&rest[..pos], Some(rest[pos + 9..].trim())) - } else { - (rest, None) - }; + let (search, replacement) = + if let Some(pos) = rest.to_ascii_uppercase().find(" REPLACE ") { + (&rest[..pos], Some(rest[pos + 9..].trim())) + } else { + (rest, None) + }; if search.is_empty() { - self.command_line.push_error(crate::t!("FIND: specify search text.").as_ref()); + self.command_line + .push_error(crate::t!("FIND: specify search text.").as_ref()); } else { let search_lc = search.to_lowercase(); let mut count = 0usize; @@ -1337,8 +1396,9 @@ impl OpenCADStudio { .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle)) .collect(); if targets.is_empty() { - self.command_line - .push_output(crate::tf!("FIND: \"{}\" not found.", search).as_ref()); + self.command_line.push_output( + crate::tf!("FIND: \"{}\" not found.", search).as_ref(), + ); } else { self.push_undo_snapshot(i, "FIND/REPLACE"); for h in &targets { @@ -1350,34 +1410,38 @@ impl OpenCADStudio { } } self.tabs[i].dirty = true; - self.command_line.push_output(crate::tf!( - "FIND/REPLACE: replaced {} occurrence(s) of \"{}\" → \"{}\".", - count, search, rep - ).as_ref()); + self.command_line.push_output( + crate::tf!( + "FIND/REPLACE: replaced {} occurrence(s) of \"{}\" → \"{}\".", + count, + search, + rep + ) + .as_ref(), + ); self.refresh_properties(); } } else { // List mode if handles.is_empty() { - self.command_line - .push_output(crate::tf!("FIND: \"{}\" not found.", search).as_ref()); + self.command_line.push_output( + crate::tf!("FIND: \"{}\" not found.", search).as_ref(), + ); } else { for h in &handles { if let Some(e) = self.tabs[i].scene.document.get_entity(*h) { use crate::entities::traits::EntityTypeOps; let txt = e.text_content().unwrap_or_default(); - self.command_line.push_output(crate::tf!( - " Handle {:X}: \"{}\"", - h.value(), - txt - ).as_ref()); + self.command_line.push_output( + crate::tf!(" Handle {:X}: \"{}\"", h.value(), txt) + .as_ref(), + ); } } - self.command_line.push_output(crate::tf!( - "FIND: {} match(es) for \"{}\".", - handles.len(), - search - ).as_ref()); + self.command_line.push_output( + crate::tf!("FIND: {} match(es) for \"{}\".", handles.len(), search) + .as_ref(), + ); } } } @@ -1475,15 +1539,26 @@ impl OpenCADStudio { }, ) }); - let Some((verb, kind, layer, color, transparency, linetype, lt_scale, lw, template_dimstyle)) = info + let Some(( + verb, + kind, + layer, + color, + transparency, + linetype, + lt_scale, + lw, + template_dimstyle, + )) = info else { self.command_line .push_error(crate::t!("ADDSELECTED: selected object not found.").as_ref()); return Task::none(); }; let Some(verb) = verb else { - self.command_line - .push_error(crate::tf!("ADDSELECTED: creating a new {kind} is not supported.").as_ref()); + self.command_line.push_error( + crate::tf!("ADDSELECTED: creating a new {kind} is not supported.").as_ref(), + ); return Task::none(); }; @@ -1496,15 +1571,34 @@ impl OpenCADStudio { // template's properties for the new object without permanently changing // CLAYER / CECOLOR / CELTYPE / CELWEIGHT (issue #239). let restore = crate::app::AddSelectedRestore { - layer_name: self.tabs[i].scene.document.header.current_layer_name.clone(), + layer_name: self.tabs[i] + .scene + .document + .header + .current_layer_name + .clone(), layer_handle: self.tabs[i].scene.document.header.current_layer_handle, color: self.tabs[i].scene.document.header.current_entity_color, transparency: self.tabs[i].scene.document.current_entity_transparency(), - linetype_name: self.tabs[i].scene.document.header.current_linetype_name.clone(), + linetype_name: self.tabs[i] + .scene + .document + .header + .current_linetype_name + .clone(), linetype_handle: self.tabs[i].scene.document.header.current_linetype_handle, line_weight: self.tabs[i].scene.document.header.current_line_weight, - lt_scale: self.tabs[i].scene.document.header.current_entity_linetype_scale, - dimstyle_name: self.tabs[i].scene.document.header.current_dimstyle_name.clone(), + lt_scale: self.tabs[i] + .scene + .document + .header + .current_entity_linetype_scale, + dimstyle_name: self.tabs[i] + .scene + .document + .header + .current_dimstyle_name + .clone(), dimstyle_handle: self.tabs[i].scene.document.header.current_dimstyle_handle, tab_active_layer: self.tabs[i].active_layer.clone(), tab_layers_current: self.tabs[i].layers.current_layer.clone(), @@ -1514,9 +1608,14 @@ impl OpenCADStudio { ribbon_lineweight: self.ribbon.active_lineweight, }; self.add_selected_restore = Some(restore); - if !self.tabs[i].scene.document.set_current_entity_transparency(transparency) { + if !self.tabs[i] + .scene + .document + .set_current_entity_transparency(transparency) + { self.add_selected_restore = None; - self.command_line.push_error("ADDSELECTED: template transparency cannot be adopted."); + self.command_line + .push_error("ADDSELECTED: template transparency cannot be adopted."); return Task::none(); } @@ -1574,9 +1673,9 @@ impl OpenCADStudio { self.ribbon.active_lineweight = lw; self.refresh_properties(); - self.command_line.push_output(crate::tf!( - "Add Selected: drawing a new {kind} on layer \"{layer}\"." - ).as_ref()); + self.command_line.push_output( + crate::tf!("Add Selected: drawing a new {kind} on layer \"{layer}\".").as_ref(), + ); // Launch the matching draw command (installs its interactive step). self.dispatch_command(verb) } @@ -1601,8 +1700,13 @@ impl OpenCADStudio { h.current_dimstyle_name = r.dimstyle_name; h.current_dimstyle_handle = r.dimstyle_handle; } - if !self.tabs[i].scene.document.set_current_entity_transparency(r.transparency) { - self.command_line.push_error("ADDSELECTED: current transparency could not be restored."); + if !self.tabs[i] + .scene + .document + .set_current_entity_transparency(r.transparency) + { + self.command_line + .push_error("ADDSELECTED: current transparency could not be restored."); } self.tabs[i].active_layer = r.tab_active_layer; self.tabs[i].layers.current_layer = r.tab_layers_current; @@ -1670,7 +1774,8 @@ fn entity_list_details(entity: &codec::EntityType) -> String { + (l.end.y - l.start.y).powi(2) + (l.end.z - l.start.z).powi(2)) .sqrt() - ).into_owned(), + ) + .into_owned(), codec::EntityType::Circle(c) => crate::tf!( "center ({:.4},{:.4},{:.4}) r={:.4} area={:.4}", c.center.x, @@ -1678,7 +1783,8 @@ fn entity_list_details(entity: &codec::EntityType) -> String { c.center.z, c.radius, PI * c.radius * c.radius - ).into_owned(), + ) + .into_owned(), codec::EntityType::Arc(a) => crate::tf!( "center ({:.4},{:.4},{:.4}) r={:.4} start={:.2}° end={:.2}°", a.center.x, @@ -1687,24 +1793,31 @@ fn entity_list_details(entity: &codec::EntityType) -> String { a.radius, a.start_angle.to_degrees(), a.end_angle.to_degrees() - ).into_owned(), + ) + .into_owned(), codec::EntityType::LwPolyline(p) => crate::tf!( "{} vertices closed={} elevation={:.4}", p.vertices.len(), p.is_closed, p.elevation - ).into_owned(), + ) + .into_owned(), codec::EntityType::Text(t) => crate::tf!( "\"{}\" h={:.4} at ({:.4},{:.4})", - t.value, t.height, t.insertion_point.x, t.insertion_point.y - ).into_owned(), + t.value, + t.height, + t.insertion_point.x, + t.insertion_point.y + ) + .into_owned(), codec::EntityType::MText(t) => crate::tf!( "\"{}\" h={:.4} at ({:.4},{:.4})", t.value.chars().take(40).collect::(), t.height, t.insertion_point.x, t.insertion_point.y - ).into_owned(), + ) + .into_owned(), codec::EntityType::Insert(ins) => crate::tf!( "block=\"{}\" at ({:.4},{:.4},{:.4}) scale=({:.4},{:.4},{:.4}) rot={:.2}°", ins.block_name, @@ -1715,25 +1828,27 @@ fn entity_list_details(entity: &codec::EntityType) -> String { ins.y_scale(), ins.z_scale(), ins.rotation.to_degrees() - ).into_owned(), + ) + .into_owned(), codec::EntityType::Spline(s) => crate::tf!( "{} ctrl pts degree={} closed={}", s.control_points.len(), s.degree, s.flags.closed - ).into_owned(), + ) + .into_owned(), codec::EntityType::Ellipse(e) => crate::tf!( "center ({:.4},{:.4}) major_len={:.4} ratio={:.4}", e.center.x, e.center.y, e.major_axis_length(), e.minor_axis_ratio - ).into_owned(), + ) + .into_owned(), _ => String::new(), } } - // ── CAL — arithmetic expression evaluator ────────────────────────────────── // A small recursive-descent parser for `+ - * /`, parentheses, unary signs and // decimal numbers. Self-contained (no external dependency). @@ -1863,11 +1978,11 @@ impl ArithParser { } } let s: String = self.chars[start..self.pos].iter().collect(); - s.parse::().map_err(|_| crate::tf!("bad number '{s}'").into_owned()) + s.parse::() + .map_err(|_| crate::tf!("bad number '{s}'").into_owned()) } } - #[cfg(test)] mod align_selected_bounds_tests { use super::*; @@ -1921,21 +2036,47 @@ mod align_selected_bounds_tests { let _ = app.automation_op(r#"{"op":"new"}"#); let left = add_line(&mut app, 0.0, 0.0, 1.0, 1.0); // leftmost & topmost already let right = add_line(&mut app, 5.0, -3.0, 8.0, -2.0); // further right and lower - app.tabs[app.active_tab].scene.select_entities(&[left, right]); + app.tabs[app.active_tab] + .scene + .select_entities(&[left, right]); let i = app.active_tab; app.dispatch_inquiry("ALIGNLEFT", i); - assert_eq!(line_start_x(&app, left), 0.0, "the already-leftmost object must not move"); - assert_eq!(line_start_x(&app, right), 0.0, "the other object's left edge should meet it"); + assert_eq!( + line_start_x(&app, left), + 0.0, + "the already-leftmost object must not move" + ); + assert_eq!( + line_start_x(&app, right), + 0.0, + "the other object's left edge should meet it" + ); app.dispatch_inquiry("ALIGNTOP", i); - assert_eq!(line_max_y(&app, left), 1.0, "the already-topmost object must not move"); - assert_eq!(line_max_y(&app, right), 1.0, "the other object's top edge should meet it"); + assert_eq!( + line_max_y(&app, left), + 1.0, + "the already-topmost object must not move" + ); + assert_eq!( + line_max_y(&app, right), + 1.0, + "the other object's top edge should meet it" + ); app.undo_steps(1); // undo ALIGNTOP - assert_eq!(line_max_y(&app, right), -2.0, "undo should restore the pre-ALIGNTOP position"); + assert_eq!( + line_max_y(&app, right), + -2.0, + "undo should restore the pre-ALIGNTOP position" + ); app.undo_steps(1); // undo ALIGNLEFT - assert_eq!(line_start_x(&app, right), 5.0, "undo should restore the original geometry"); + assert_eq!( + line_start_x(&app, right), + 5.0, + "undo should restore the original geometry" + ); } /// A complete, fully-selected group (`Scene::selected_object_units`) @@ -1948,8 +2089,12 @@ mod align_selected_bounds_tests { let a = add_line(&mut app, 5.0, 0.0, 6.0, 1.0); let b = add_line(&mut app, 7.0, 0.0, 8.0, 1.0); let other = add_line(&mut app, 0.0, 0.0, 1.0, 1.0); - app.tabs[app.active_tab].scene.create_group("pair".to_string(), vec![a, b]); - app.tabs[app.active_tab].scene.select_entities(&[a, b, other]); + app.tabs[app.active_tab] + .scene + .create_group("pair".to_string(), vec![a, b]); + app.tabs[app.active_tab] + .scene + .select_entities(&[a, b, other]); let i = app.active_tab; app.dispatch_inquiry("ALIGNLEFT", i); @@ -1958,7 +2103,11 @@ mod align_selected_bounds_tests { // transform — must shift by exactly the same amount, not collapse // onto `a`. assert_eq!(line_start_x(&app, a), 0.0); - assert_eq!(line_start_x(&app, b), 2.0, "b must keep its offset from a, not collapse onto it"); + assert_eq!( + line_start_x(&app, b), + 2.0, + "b must keep its offset from a, not collapse onto it" + ); } } @@ -1971,7 +2120,11 @@ mod find_replace_command_tests { /// taken from a case-mapped copy. #[test] fn find_replace_accepts_search_text_whose_case_mapping_changes_length() { - for cmd in ["FIND ı REPLACE x", "FINDALL fi REPLACE fi", "FIND ʼn replace n"] { + for cmd in [ + "FIND ı REPLACE x", + "FINDALL fi REPLACE fi", + "FIND ʼn replace n", + ] { let mut app = OpenCADStudio::new_for_test(); let i = app.active_tab; let _ = app.dispatch_inquiry(cmd, i); @@ -1999,18 +2152,37 @@ mod find_replace_command_tests { assert!(app.tabs[i].active_cmd.is_some()); // First point - let _ = app.tabs[i].active_cmd.as_mut().unwrap().on_point(glam::DVec3::new(0.0, 0.0, 0.0)); + let _ = app.tabs[i] + .active_cmd + .as_mut() + .unwrap() + .on_point(glam::DVec3::new(0.0, 0.0, 0.0)); // Second point - let res = app.tabs[i].active_cmd.as_mut().unwrap().on_point(glam::DVec3::new(10.0, 0.0, 0.0)); + let res = app.tabs[i] + .active_cmd + .as_mut() + .unwrap() + .on_point(glam::DVec3::new(10.0, 0.0, 0.0)); match res { crate::command::CmdResult::Measurement(msg) => { - assert!(msg.contains("Distance = 10"), "Expected 'Distance = 10', got: {msg}"); - assert!(msg.contains("Delta X = 10"), "Expected 'Delta X = 10', got: {msg}"); - assert!(msg.contains("Delta Y = 0"), "Expected 'Delta Y = 0', got: {msg}"); - assert!(msg.contains("Delta Z = 0"), "Expected 'Delta Z = 0', got: {msg}"); + assert!( + msg.contains("Distance = 10"), + "Expected 'Distance = 10', got: {msg}" + ); + assert!( + msg.contains("Delta X = 10"), + "Expected 'Delta X = 10', got: {msg}" + ); + assert!( + msg.contains("Delta Y = 0"), + "Expected 'Delta Y = 0', got: {msg}" + ); + assert!( + msg.contains("Delta Z = 0"), + "Expected 'Delta Z = 0', got: {msg}" + ); } _ => panic!("Expected CmdResult::Measurement"), } } } - diff --git a/src/app/commands/layerprops.rs b/src/app/commands/layerprops.rs index 6d49ab04d..464cf01fc 100644 --- a/src/app/commands/layerprops.rs +++ b/src/app/commands/layerprops.rs @@ -41,10 +41,12 @@ impl OpenCADStudio { "NEW" | "N" => { let name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string(); if name.is_empty() { - self.command_line.push_error(crate::t!("Usage: LAYER NEW ").as_ref()); - } else if self.tabs[i].scene.document.layers.contains(&name) { self.command_line - .push_error(crate::tf!("LAYER: '{}' already exists.", name).as_ref()); + .push_error(crate::t!("Usage: LAYER NEW ").as_ref()); + } else if self.tabs[i].scene.document.layers.contains(&name) { + self.command_line.push_error( + crate::tf!("LAYER: '{}' already exists.", name).as_ref(), + ); } else { let mut layer = Layer::new(&name); // Allocate a unique handle so the layer survives a @@ -66,7 +68,8 @@ impl OpenCADStudio { } self.push_undo_snapshot(i, "LAYER ON"); self.tabs[i].dirty = true; - self.command_line.push_output(crate::t!("LAYER: layers turned on.").as_ref()); + self.command_line + .push_output(crate::t!("LAYER: layers turned on.").as_ref()); } "OFF" => { for name in &parts[1..] { @@ -76,7 +79,8 @@ impl OpenCADStudio { } self.push_undo_snapshot(i, "LAYER OFF"); self.tabs[i].dirty = true; - self.command_line.push_output(crate::t!("LAYER: layers turned off.").as_ref()); + self.command_line + .push_output(crate::t!("LAYER: layers turned off.").as_ref()); } "FREEZE" | "FR" => { for name in &parts[1..] { @@ -86,7 +90,8 @@ impl OpenCADStudio { } self.push_undo_snapshot(i, "LAYER FREEZE"); self.tabs[i].dirty = true; - self.command_line.push_output(crate::t!("LAYER: layers frozen.").as_ref()); + self.command_line + .push_output(crate::t!("LAYER: layers frozen.").as_ref()); } "THAW" | "TH" => { for name in &parts[1..] { @@ -96,7 +101,8 @@ impl OpenCADStudio { } self.push_undo_snapshot(i, "LAYER THAW"); self.tabs[i].dirty = true; - self.command_line.push_output(crate::t!("LAYER: layers thawed.").as_ref()); + self.command_line + .push_output(crate::t!("LAYER: layers thawed.").as_ref()); } "LOCK" | "LO" => { for name in &parts[1..] { @@ -107,7 +113,8 @@ impl OpenCADStudio { self.push_undo_snapshot(i, "LAYER LOCK"); self.tabs[i].dirty = true; self.refresh_properties(); - self.command_line.push_output(crate::t!("LAYER: layers locked.").as_ref()); + self.command_line + .push_output(crate::t!("LAYER: layers locked.").as_ref()); } "UNLOCK" | "UL" => { for name in &parts[1..] { @@ -118,7 +125,8 @@ impl OpenCADStudio { self.push_undo_snapshot(i, "LAYER UNLOCK"); self.tabs[i].dirty = true; self.refresh_properties(); - self.command_line.push_output(crate::t!("LAYER: layers unlocked.").as_ref()); + self.command_line + .push_output(crate::t!("LAYER: layers unlocked.").as_ref()); } "COLOR" | "C" => { // LAYER COLOR @@ -136,30 +144,31 @@ impl OpenCADStudio { // this layer. The dependency index maps block // children back to their top-level INSERTs, so // unrelated entities stay warm. - self.tabs[i] - .scene - .invalidate_layer_dependencies(std::slice::from_ref( - &layer_name, - )); - self.command_line.push_output(crate::tf!( - "LAYER: '{}' color set to ACI {}.", - layer_name, idx - ).as_ref()); + self.tabs[i].scene.invalidate_layer_dependencies( + std::slice::from_ref(&layer_name), + ); + self.command_line.push_output( + crate::tf!("LAYER: '{}' color set to ACI {}.", layer_name, idx) + .as_ref(), + ); } else { - self.command_line - .push_error(crate::tf!("LAYER: '{}' not found.", layer_name).as_ref()); + self.command_line.push_error( + crate::tf!("LAYER: '{}' not found.", layer_name).as_ref(), + ); } } else { - self.command_line - .push_error(crate::t!("Usage: LAYER COLOR ").as_ref()); + self.command_line.push_error( + crate::t!("Usage: LAYER COLOR ").as_ref(), + ); } } "SET" | "S" | "CURRENT" => { let name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string(); if self.tabs[i].scene.document.layers.contains(&name) { self.tabs[i].layers.current_layer = name.clone(); - self.command_line - .push_output(crate::tf!("LAYER: current layer set to '{}'.", name).as_ref()); + self.command_line.push_output( + crate::tf!("LAYER: current layer set to '{}'.", name).as_ref(), + ); } else { self.command_line .push_error(crate::tf!("LAYER: '{}' not found.", name).as_ref()); @@ -206,22 +215,29 @@ impl OpenCADStudio { .map(|u| u.name.clone()) .collect(); if names.is_empty() { - self.command_line.push_output(crate::tf!( - "Active UCS: {} | No named UCSs defined.", - active_name - ).as_ref()); + self.command_line.push_output( + crate::tf!( + "Active UCS: {} | No named UCSs defined.", + active_name + ) + .as_ref(), + ); } else { - self.command_line.push_output(crate::tf!( - "Active UCS: {} | Named: {}", - active_name, - names.join(", ") - ).as_ref()); + self.command_line.push_output( + crate::tf!( + "Active UCS: {} | Named: {}", + active_name, + names.join(", ") + ) + .as_ref(), + ); } } "SAVE" | "S" => { let name = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string(); if name.is_empty() { - self.command_line.push_error(crate::t!("Usage: UCS SAVE ").as_ref()); + self.command_line + .push_error(crate::t!("Usage: UCS SAVE ").as_ref()); } else { // Save the current active UCS under this name. let mut ucs = match &self.tabs[i].active_ucs { @@ -239,14 +255,8 @@ impl OpenCADStudio { .get(&name) .map(|existing| existing.handle) .filter(|handle| !handle.is_null()) - .unwrap_or_else(|| { - self.tabs[i].scene.document.allocate_handle() - }); - self.tabs[i] - .scene - .document - .ucss - .add_or_replace(ucs.clone()); + .unwrap_or_else(|| self.tabs[i].scene.document.allocate_handle()); + self.tabs[i].scene.document.ucss.add_or_replace(ucs.clone()); self.tabs[i].scene.bump_ucs_epoch(); self.tabs[i].active_ucs = Some(ucs); active_changed = true; @@ -258,9 +268,9 @@ impl OpenCADStudio { "DELETE" | "DEL" | "D" => { let name = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string(); if name.is_empty() { - self.command_line.push_error(crate::t!("Usage: UCS DELETE ").as_ref()); - } else if let Some(removed) = - self.tabs[i].scene.document.ucss.remove(&name) + self.command_line + .push_error(crate::t!("Usage: UCS DELETE ").as_ref()); + } else if let Some(removed) = self.tabs[i].scene.document.ucss.remove(&name) { self.tabs[i].scene.bump_ucs_epoch(); let removed_handle = removed.handle; @@ -271,10 +281,11 @@ impl OpenCADStudio { } } } - let active_matches = self.tabs[i].active_ucs.as_ref().is_some_and(|ucs| { - (!removed_handle.is_null() && ucs.handle == removed_handle) - || ucs.name.eq_ignore_ascii_case(&name) - }); + let active_matches = + self.tabs[i].active_ucs.as_ref().is_some_and(|ucs| { + (!removed_handle.is_null() && ucs.handle == removed_handle) + || ucs.name.eq_ignore_ascii_case(&name) + }); if active_matches { if let Some(active) = self.tabs[i].active_ucs.as_mut() { active.name = "*ACTIVE*".to_string(); @@ -295,8 +306,9 @@ impl OpenCADStudio { "W" | "WORLD" => { self.tabs[i].active_ucs = None; active_changed = true; - self.command_line - .push_output(crate::t!("UCS reset to World Coordinate System.").as_ref()); + self.command_line.push_output( + crate::t!("UCS reset to World Coordinate System.").as_ref(), + ); } "VIEW" | "V" => { let rotation = self.tabs[i].scene.active_camera_rotation(); @@ -346,16 +358,23 @@ impl OpenCADStudio { } Some((handle, pick)) => { self.tabs[i].scene.restore_solid_models(&[handle]); - let built = self.tabs[i] - .scene - .solid_models - .get(&handle) - .and_then(|body| { - use crate::scene::model::solid_model; - let face = - solid_model::nearest_planar_face(body, pick.to_array())?; - solid_model::planar_face_ucs(body, face, pick.to_array()) - }); + let built = + self.tabs[i] + .scene + .solid_models + .get(&handle) + .and_then(|body| { + use crate::scene::model::solid_model; + let face = solid_model::nearest_planar_face( + body, + pick.to_array(), + )?; + solid_model::planar_face_ucs( + body, + face, + pick.to_array(), + ) + }); match built { Some(ucs) => { self.tabs[i].active_ucs = Some(ucs); @@ -395,7 +414,9 @@ impl OpenCADStudio { .scene .document .get_entity(handle) - .and_then(crate::scene::model::sweep_model::extrusion_profile_of) + .and_then( + crate::scene::model::sweep_model::extrusion_profile_of, + ) .and_then(|(profile, _)| { // The profile plane already carries the // entity's own in-plane axes, so adopt @@ -407,8 +428,7 @@ impl OpenCADStudio { let plane = profile.plane; let x = glam::DVec3::from_array(plane.x_axis); let y = glam::DVec3::from_array(plane.y_axis); - if x.length_squared() < 1e-12 - || y.length_squared() < 1e-12 + if x.length_squared() < 1e-12 || y.length_squared() < 1e-12 { return None; } @@ -418,10 +438,8 @@ impl OpenCADStudio { plane.origin[1], plane.origin[2], ); - ucs.x_axis = - Vector3::new(x.x, x.y, x.z); - ucs.y_axis = - Vector3::new(y.x, y.y, y.z); + ucs.x_axis = Vector3::new(x.x, x.y, x.z); + ucs.y_axis = Vector3::new(y.x, y.y, y.z); Some(ucs) }); match built { @@ -502,12 +520,18 @@ impl OpenCADStudio { wcs_origin.z as f64, ); active_changed = true; - self.command_line.push_output(crate::tf!( - "UCS origin set to ({:.4}, {:.4}, {:.4}).", - wcs_origin.x, wcs_origin.y, wcs_origin.z - ).as_ref()); + self.command_line.push_output( + crate::tf!( + "UCS origin set to ({:.4}, {:.4}, {:.4}).", + wcs_origin.x, + wcs_origin.y, + wcs_origin.z + ) + .as_ref(), + ); } else { - self.command_line.push_error(crate::t!("Usage: UCS ORIGIN x,y,z").as_ref()); + self.command_line + .push_error(crate::t!("Usage: UCS ORIGIN x,y,z").as_ref()); } } // UCS Z angle — rotate active UCS around its Z axis by degrees @@ -517,9 +541,7 @@ impl OpenCADStudio { let rad = angle_deg.to_radians(); let current = self.tabs[i].active_ucs.as_ref(); let origin = current - .map(|u| { - glam::DVec3::new(u.origin.x, u.origin.y, u.origin.z) - }) + .map(|u| glam::DVec3::new(u.origin.x, u.origin.y, u.origin.z)) .unwrap_or(glam::DVec3::ZERO); let mut new_ucs = ucs_rotated_z(origin, rad); // If already had axes, compose rotation on top @@ -545,10 +567,12 @@ impl OpenCADStudio { } self.tabs[i].active_ucs = Some(new_ucs); active_changed = true; - self.command_line - .push_output(crate::tf!("UCS rotated {:.2}° around Z.", angle_deg).as_ref()); + self.command_line.push_output( + crate::tf!("UCS rotated {:.2}° around Z.", angle_deg).as_ref(), + ); } else { - self.command_line.push_error(crate::t!("Usage: UCS Z ").as_ref()); + self.command_line + .push_error(crate::t!("Usage: UCS Z ").as_ref()); } } // UCS X angle — rotate around current UCS X axis @@ -573,10 +597,12 @@ impl OpenCADStudio { let ny = rot * old_y; ucs.y_axis = Vector3::new(ny.x as f64, ny.y as f64, ny.z as f64); active_changed = true; - self.command_line - .push_output(crate::tf!("UCS rotated {:.2}° around X.", angle_deg).as_ref()); + self.command_line.push_output( + crate::tf!("UCS rotated {:.2}° around X.", angle_deg).as_ref(), + ); } else { - self.command_line.push_error(crate::t!("Usage: UCS X ").as_ref()); + self.command_line + .push_error(crate::t!("Usage: UCS X ").as_ref()); } } // UCS Y angle — rotate around current UCS Y axis @@ -601,10 +627,12 @@ impl OpenCADStudio { let nx = rot * old_x; ucs.x_axis = Vector3::new(nx.x as f64, nx.y as f64, nx.z as f64); active_changed = true; - self.command_line - .push_output(crate::tf!("UCS rotated {:.2}° around Y.", angle_deg).as_ref()); + self.command_line.push_output( + crate::tf!("UCS rotated {:.2}° around Y.", angle_deg).as_ref(), + ); } else { - self.command_line.push_error(crate::t!("Usage: UCS Y ").as_ref()); + self.command_line + .push_error(crate::t!("Usage: UCS Y ").as_ref()); } } _ => { @@ -641,8 +669,7 @@ impl OpenCADStudio { cmd if cmd == "CREATESKETCH" || cmd.starts_with("CREATESKETCH ") => { if self.tabs[i].sketch_session.is_some() { self.command_line.push_error( - crate::t!("A sketch is already open — FINISHSKETCH closes it.") - .as_ref(), + crate::t!("A sketch is already open — FINISHSKETCH closes it.").as_ref(), ); return Some(Task::none()); } @@ -661,10 +688,8 @@ impl OpenCADStudio { self.commit_active_ucs_change(i, "CREATESKETCH"); } else if !arg.is_empty() { self.command_line.push_error( - crate::t!( - "Usage: CREATESKETCH [XY|XZ|YZ]; bare uses the current UCS." - ) - .as_ref(), + crate::t!("Usage: CREATESKETCH [XY|XZ|YZ]; bare uses the current UCS.") + .as_ref(), ); return Some(Task::none()); } @@ -720,16 +745,19 @@ impl OpenCADStudio { "" | "CURRENT" | "C" => { let r = self.tabs[i].scene.viewcube_ucs_mat(); self.plan_snap(i, r); - self.command_line.push_output(crate::t!("PLAN: current UCS plan view.").as_ref()); + self.command_line + .push_output(crate::t!("PLAN: current UCS plan view.").as_ref()); } "WORLD" | "W" => { self.plan_snap(i, glam::Mat4::IDENTITY); - self.command_line.push_output(crate::t!("PLAN: world plan view.").as_ref()); + self.command_line + .push_output(crate::t!("PLAN: world plan view.").as_ref()); } "UCS" | "U" => { let name = parts.get(2).map(|s| s.trim()).unwrap_or(""); if name.is_empty() { - self.command_line.push_info(crate::t!("Usage: PLAN UCS ").as_ref()); + self.command_line + .push_info(crate::t!("Usage: PLAN UCS ").as_ref()); } else { let ucs = self.tabs[i] .scene @@ -740,13 +768,12 @@ impl OpenCADStudio { .cloned(); match ucs { Some(u) => { - let r = crate::app::helpers::UcsXform::from_ucs(&u) - .rotation_mat(); + let r = + crate::app::helpers::UcsXform::from_ucs(&u).rotation_mat(); self.plan_snap(i, r); - self.command_line.push_output(crate::tf!( - "PLAN: plan view of UCS '{}'.", - u.name - ).as_ref()); + self.command_line.push_output( + crate::tf!("PLAN: plan view of UCS '{}'.", u.name).as_ref(), + ); } None => self .command_line @@ -754,9 +781,9 @@ impl OpenCADStudio { } } } - _ => self - .command_line - .push_info(crate::t!("Usage: PLAN [CURRENT / UCS / WORLD]").as_ref()), + _ => self.command_line.push_info( + crate::t!("Usage: PLAN [CURRENT / UCS / WORLD]").as_ref(), + ), } } @@ -788,16 +815,19 @@ impl OpenCADStudio { .map(|v| v.name.clone()) .collect(); if views.is_empty() { - self.command_line.push_output(crate::t!("No named views saved.").as_ref()); - } else { self.command_line - .push_output(crate::tf!("Named views: {}", views.join(", ")).as_ref()); + .push_output(crate::t!("No named views saved.").as_ref()); + } else { + self.command_line.push_output( + crate::tf!("Named views: {}", views.join(", ")).as_ref(), + ); } } "SAVE" | "S" => { let name = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string(); if name.is_empty() { - self.command_line.push_error(crate::t!("Usage: VIEW SAVE ").as_ref()); + self.command_line + .push_error(crate::t!("Usage: VIEW SAVE ").as_ref()); } else { let new_view = self.tabs[i].scene.current_as_named_view(&name); self.tabs[i].scene.document.views.add_or_replace(new_view); @@ -807,31 +837,51 @@ impl OpenCADStudio { } "WINDOW" | "W" => { // A plan view of the window between two corners. - let args: Vec<&str> = parts.get(2).map(|s| s.split_whitespace().collect()).unwrap_or_default(); + let args: Vec<&str> = parts + .get(2) + .map(|s| s.split_whitespace().collect()) + .unwrap_or_default(); let corner = |s: &str| { let mut c = s.split(','); - Some((c.next()?.trim().parse::().ok()?, c.next()?.trim().parse::().ok()?)) + Some(( + c.next()?.trim().parse::().ok()?, + c.next()?.trim().parse::().ok()?, + )) }; - match (args.first(), args.get(1).and_then(|s| corner(s)), args.get(2).and_then(|s| corner(s))) { + match ( + args.first(), + args.get(1).and_then(|s| corner(s)), + args.get(2).and_then(|s| corner(s)), + ) { (Some(name), Some(a), Some(b)) if a.0 != b.0 && a.1 != b.1 => { use codec::types::Vector3; let mut view = self.tabs[i].scene.current_as_named_view(name); - view.target = Vector3 { x: (a.0 + b.0) / 2.0, y: (a.1 + b.1) / 2.0, z: 0.0 }; - view.direction = Vector3 { x: 0.0, y: 0.0, z: 1.0 }; + view.target = Vector3 { + x: (a.0 + b.0) / 2.0, + y: (a.1 + b.1) / 2.0, + z: 0.0, + }; + view.direction = Vector3 { + x: 0.0, + y: 0.0, + z: 1.0, + }; view.width = (b.0 - a.0).abs(); view.height = (b.1 - a.1).abs(); self.tabs[i].scene.document.views.add_or_replace(view); - self.command_line.push_output(crate::tf!("View '{}' saved.", name).as_ref()); + self.command_line + .push_output(crate::tf!("View '{}' saved.", name).as_ref()); } - _ => self - .command_line - .push_error(crate::t!("Usage: VIEW WINDOW ").as_ref()), + _ => self.command_line.push_error( + crate::t!("Usage: VIEW WINDOW ").as_ref(), + ), } } "DELETE" | "DEL" | "D" => { let name = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string(); if name.is_empty() { - self.command_line.push_error(crate::t!("Usage: VIEW DELETE ").as_ref()); + self.command_line + .push_error(crate::t!("Usage: VIEW DELETE ").as_ref()); } else { if self.tabs[i].scene.document.views.remove(&name).is_some() { self.command_line @@ -845,13 +895,15 @@ impl OpenCADStudio { "RESTORE" | "R" => { let name = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string(); if name.is_empty() { - self.command_line.push_error(crate::t!("Usage: VIEW RESTORE ").as_ref()); + self.command_line + .push_error(crate::t!("Usage: VIEW RESTORE ").as_ref()); } else { let found = self.tabs[i].scene.document.views.get(&name).cloned(); if let Some(v) = found { self.tabs[i].scene.restore_named_view(&v); - self.command_line - .push_output(crate::tf!("View '{}' restored.", v.name).as_ref()); + self.command_line.push_output( + crate::tf!("View '{}' restored.", v.name).as_ref(), + ); } else { self.command_line .push_error(crate::tf!("View '{}' not found.", name).as_ref()); @@ -940,36 +992,42 @@ impl OpenCADStudio { .map(|s| { crate::tf!( "{} (h_margin:{:.2} v_margin:{:.2})", - s.name, s.horizontal_margin, s.vertical_margin - ).into_owned() + s.name, + s.horizontal_margin, + s.vertical_margin + ) + .into_owned() }) .collect(); if styles.is_empty() { - self.command_line.push_output(crate::t!("No table styles.").as_ref()); - } else { self.command_line - .push_output(crate::tf!("TableStyles:\n {}", styles.join("\n ")).as_ref()); + .push_output(crate::t!("No table styles.").as_ref()); + } else { + self.command_line.push_output( + crate::tf!("TableStyles:\n {}", styles.join("\n ")).as_ref(), + ); } } "NEW" | "N" => { let name = parts.get(1).copied().unwrap_or("").to_string(); if name.is_empty() { - self.command_line.push_error(crate::t!("Usage: TABLESTYLE NEW ").as_ref()); + self.command_line + .push_error(crate::t!("Usage: TABLESTYLE NEW ").as_ref()); } else { let doc = &self.tabs[i].scene.document; let exists = doc.objects.values().any(|o| { matches!(o, ObjectType::TableStyle(s) if s.name.eq_ignore_ascii_case(&name)) }); if exists { - self.command_line - .push_error(crate::tf!("TABLESTYLE: '{}' already exists.", name).as_ref()); + self.command_line.push_error( + crate::tf!("TABLESTYLE: '{}' already exists.", name).as_ref(), + ); } else { self.push_undo_snapshot(i, "TABLESTYLE NEW"); let mut style = TableStyle::standard(); style.name = name.clone(); - let nh = codec::Handle::new( - self.tabs[i].scene.document.next_handle(), - ); + let nh = + codec::Handle::new(self.tabs[i].scene.document.next_handle()); style.handle = nh; self.tabs[i] .scene @@ -977,8 +1035,9 @@ impl OpenCADStudio { .objects .insert(nh, ObjectType::TableStyle(style)); self.tabs[i].dirty = true; - self.command_line - .push_output(crate::tf!("TABLESTYLE: '{}' created.", name).as_ref()); + self.command_line.push_output( + crate::tf!("TABLESTYLE: '{}' created.", name).as_ref(), + ); } } } @@ -1024,31 +1083,34 @@ impl OpenCADStudio { }) .collect(); if styles.is_empty() { - self.command_line.push_output(crate::t!("No multiline styles.").as_ref()); - } else { self.command_line - .push_output(crate::tf!("MLineStyles:\n {}", styles.join("\n ")).as_ref()); + .push_output(crate::t!("No multiline styles.").as_ref()); + } else { + self.command_line.push_output( + crate::tf!("MLineStyles:\n {}", styles.join("\n ")).as_ref(), + ); } } "NEW" | "N" => { let name = parts.get(1).copied().unwrap_or("").to_string(); if name.is_empty() { - self.command_line.push_error(crate::t!("Usage: MLSTYLE NEW ").as_ref()); + self.command_line + .push_error(crate::t!("Usage: MLSTYLE NEW ").as_ref()); } else { let doc = &self.tabs[i].scene.document; let exists = doc.objects.values().any(|o| { matches!(o, ObjectType::MLineStyle(s) if s.name.eq_ignore_ascii_case(&name)) }); if exists { - self.command_line - .push_error(crate::tf!("MLSTYLE: '{}' already exists.", name).as_ref()); + self.command_line.push_error( + crate::tf!("MLSTYLE: '{}' already exists.", name).as_ref(), + ); } else { self.push_undo_snapshot(i, "MLSTYLE NEW"); let mut style = MLineStyle::standard(); style.name = name.clone(); - let nh = codec::Handle::new( - self.tabs[i].scene.document.next_handle(), - ); + let nh = + codec::Handle::new(self.tabs[i].scene.document.next_handle()); style.handle = nh; self.tabs[i] .scene @@ -1056,15 +1118,17 @@ impl OpenCADStudio { .objects .insert(nh, ObjectType::MLineStyle(style)); self.tabs[i].dirty = true; - self.command_line - .push_output(crate::tf!("MLSTYLE: '{}' created.", name).as_ref()); + self.command_line.push_output( + crate::tf!("MLSTYLE: '{}' created.", name).as_ref(), + ); } } } "SET" | "S" => { let name = parts.get(1).copied().unwrap_or("").to_string(); if name.is_empty() { - self.command_line.push_error(crate::t!("Usage: MLSTYLE SET ").as_ref()); + self.command_line + .push_error(crate::t!("Usage: MLSTYLE SET ").as_ref()); } else { let doc = &self.tabs[i].scene.document; let exists = doc.objects.values().any(|o| { @@ -1074,21 +1138,23 @@ impl OpenCADStudio { self.push_undo_snapshot(i, "MLSTYLE SET"); self.tabs[i].scene.document.header.multiline_style = name.clone(); self.tabs[i].dirty = true; - self.command_line.push_output(crate::tf!( - "MLSTYLE: current style set to '{}'.", - name - ).as_ref()); + self.command_line.push_output( + crate::tf!("MLSTYLE: current style set to '{}'.", name) + .as_ref(), + ); } else { - self.command_line - .push_error(crate::tf!("MLSTYLE: '{}' not found.", name).as_ref()); + self.command_line.push_error( + crate::tf!("MLSTYLE: '{}' not found.", name).as_ref(), + ); } } } "DEL" | "DELETE" => { let name = parts.get(1).copied().unwrap_or("").to_string(); if name.is_empty() || name.eq_ignore_ascii_case("Standard") { - self.command_line - .push_error(crate::t!("Cannot delete the Standard style.").as_ref()); + self.command_line.push_error( + crate::t!("Cannot delete the Standard style.").as_ref(), + ); } else { let doc = &self.tabs[i].scene.document; let handle = doc.objects.iter().find_map(|(&h, o)| { @@ -1106,17 +1172,21 @@ impl OpenCADStudio { self.push_undo_snapshot(i, "MLSTYLE DEL"); self.tabs[i].scene.document.objects.remove(&h); self.tabs[i].dirty = true; - self.command_line - .push_output(crate::tf!("MLSTYLE: '{}' deleted.", name).as_ref()); + self.command_line.push_output( + crate::tf!("MLSTYLE: '{}' deleted.", name).as_ref(), + ); } else { - self.command_line - .push_error(crate::tf!("MLSTYLE: '{}' not found.", name).as_ref()); + self.command_line.push_error( + crate::tf!("MLSTYLE: '{}' not found.", name).as_ref(), + ); } } } _ => { - self.command_line - .push_error(crate::t!("Usage: MLSTYLE [LIST|NEW |SET |DEL ]").as_ref()); + self.command_line.push_error( + crate::t!("Usage: MLSTYLE [LIST|NEW |SET |DEL ]") + .as_ref(), + ); } } } @@ -1141,22 +1211,29 @@ impl OpenCADStudio { .document .dim_styles .iter() - .map(|s| crate::tf!("{}(txt:{:.2} asz:{:.2})", s.name, s.dimtxt, s.dimasz).into_owned()) + .map(|s| { + crate::tf!("{}(txt:{:.2} asz:{:.2})", s.name, s.dimtxt, s.dimasz) + .into_owned() + }) .collect(); if styles.is_empty() { - self.command_line.push_output(crate::t!("No dim styles defined.").as_ref()); - } else { self.command_line - .push_output(crate::tf!("DimStyles: {}", styles.join(", ")).as_ref()); + .push_output(crate::t!("No dim styles defined.").as_ref()); + } else { + self.command_line.push_output( + crate::tf!("DimStyles: {}", styles.join(", ")).as_ref(), + ); } } "NEW" | "N" => { let name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string(); if name.is_empty() { - self.command_line.push_error(crate::t!("Usage: DIMSTYLE NEW ").as_ref()); - } else if self.tabs[i].scene.document.dim_styles.contains(&name) { self.command_line - .push_error(crate::tf!("DIMSTYLE: '{}' already exists.", name).as_ref()); + .push_error(crate::t!("Usage: DIMSTYLE NEW ").as_ref()); + } else if self.tabs[i].scene.document.dim_styles.contains(&name) { + self.command_line.push_error( + crate::tf!("DIMSTYLE: '{}' already exists.", name).as_ref(), + ); } else { let undo = self.begin_dim_style_undo( i, @@ -1234,24 +1311,31 @@ impl OpenCADStudio { } } self.tabs[i].dirty = true; - self.tabs[i].scene + self.tabs[i] + .scene .invalidate_dim_style_dependencies(&style_name); self.commit_dim_style_undo(i, undo); - self.command_line.push_output(crate::tf!( - "DIMSTYLE: '{style_name}'.{prop} = {val:.3}" - ).as_ref()); + self.command_line.push_output( + crate::tf!("DIMSTYLE: '{style_name}'.{prop} = {val:.3}") + .as_ref(), + ); } else { - self.command_line - .push_error(crate::tf!("DIMSTYLE: '{}' not found.", style_name).as_ref()); + self.command_line.push_error( + crate::tf!("DIMSTYLE: '{}' not found.", style_name).as_ref(), + ); } } else { - self.command_line - .push_error(crate::t!("Usage: DIMSTYLE SET ").as_ref()); + self.command_line.push_error( + crate::t!("Usage: DIMSTYLE SET ").as_ref(), + ); } } _ => { self.command_line.push_info( - crate::t!("Usage: DIMSTYLE LIST | NEW | SET ").as_ref(), + crate::t!( + "Usage: DIMSTYLE LIST | NEW | SET " + ) + .as_ref(), ); } } @@ -1275,10 +1359,15 @@ impl OpenCADStudio { .values() .filter_map(|o| { if let ObjectType::MultiLeaderStyle(s) = o { - Some(crate::tf!( - "{}(txt:{:.2} asz:{:.2})", - s.name, s.text_height, s.arrowhead_size - ).into_owned()) + Some( + crate::tf!( + "{}(txt:{:.2} asz:{:.2})", + s.name, + s.text_height, + s.arrowhead_size + ) + .into_owned(), + ) } else { None } @@ -1286,13 +1375,17 @@ impl OpenCADStudio { .collect(); let current = &self.tabs[i].active_mleader_style; if styles.is_empty() { - self.command_line - .push_output(crate::tf!("MLeader styles: (none) active: {current}").as_ref()); + self.command_line.push_output( + crate::tf!("MLeader styles: (none) active: {current}").as_ref(), + ); } else { - self.command_line.push_output(crate::tf!( - "MLeader styles: {} active: {current}", - styles.join(", ") - ).as_ref()); + self.command_line.push_output( + crate::tf!( + "MLeader styles: {} active: {current}", + styles.join(", ") + ) + .as_ref(), + ); } } "NEW" | "N" => { @@ -1305,10 +1398,9 @@ impl OpenCADStudio { |o| matches!(o, ObjectType::MultiLeaderStyle(s) if s.name == name), ); if already_exists { - self.command_line.push_error(crate::tf!( - "MLEADERSTYLE: '{}' already exists.", - name - ).as_ref()); + self.command_line.push_error( + crate::tf!("MLEADERSTYLE: '{}' already exists.", name).as_ref(), + ); } else { let handle = self.tabs[i].scene.document.allocate_handle(); let mut style = MultiLeaderStyle::new(&name); @@ -1320,8 +1412,9 @@ impl OpenCADStudio { .insert(handle, ObjectType::MultiLeaderStyle(style)); self.push_undo_snapshot(i, "MLEADERSTYLE NEW"); self.tabs[i].dirty = true; - self.command_line - .push_output(crate::tf!("MLEADERSTYLE: '{}' created.", name).as_ref()); + self.command_line.push_output( + crate::tf!("MLEADERSTYLE: '{}' created.", name).as_ref(), + ); } } } @@ -1371,38 +1464,46 @@ impl OpenCADStudio { } self.push_undo_snapshot(i, "MLEADERSTYLE SET"); self.tabs[i].dirty = true; - self.command_line.push_output(crate::tf!( - "MLEADERSTYLE: '{style_name}'.{prop} = {val:.3}" - ).as_ref()); + self.command_line.push_output( + crate::tf!("MLEADERSTYLE: '{style_name}'.{prop} = {val:.3}") + .as_ref(), + ); } else { - self.command_line.push_error(crate::tf!( - "MLEADERSTYLE: '{}' not found.", - style_name - ).as_ref()); + self.command_line.push_error( + crate::tf!("MLEADERSTYLE: '{}' not found.", style_name) + .as_ref(), + ); } } else { - self.command_line - .push_error(crate::t!("Usage: MLEADERSTYLE SET ").as_ref()); + self.command_line.push_error( + crate::t!("Usage: MLEADERSTYLE SET ") + .as_ref(), + ); } } "CURRENT" | "C" | "ACTIVE" => { let name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string(); if name.is_empty() { - self.command_line.push_output(crate::tf!( - "Current MLeader style: {}", - self.tabs[i].active_mleader_style - ).as_ref()); + self.command_line.push_output( + crate::tf!( + "Current MLeader style: {}", + self.tabs[i].active_mleader_style + ) + .as_ref(), + ); } else { let exists = name == "Standard" || self.tabs[i].scene.document.objects.values() .any(|o| matches!(o, ObjectType::MultiLeaderStyle(s) if s.name == name)); if exists { self.tabs[i].active_mleader_style = name.clone(); - self.command_line.push_output(crate::tf!( - "MLEADERSTYLE: current style set to '{name}'." - ).as_ref()); + self.command_line.push_output( + crate::tf!("MLEADERSTYLE: current style set to '{name}'.") + .as_ref(), + ); } else { - self.command_line - .push_error(crate::tf!("MLEADERSTYLE: '{}' not found.", name).as_ref()); + self.command_line.push_error( + crate::tf!("MLEADERSTYLE: '{}' not found.", name).as_ref(), + ); } } } @@ -1444,25 +1545,30 @@ impl OpenCADStudio { s.font_file, s.width_factor, s.oblique_angle.to_degrees() - ).into_owned() + ) + .into_owned() }) .collect(); if styles.is_empty() { - self.command_line.push_output(crate::t!("No text styles defined.").as_ref()); - } else { self.command_line - .push_output(crate::tf!("Text styles: {}", styles.join(" | ")).as_ref()); + .push_output(crate::t!("No text styles defined.").as_ref()); + } else { + self.command_line.push_output( + crate::tf!("Text styles: {}", styles.join(" | ")).as_ref(), + ); } } "SET" | "S" => { // STYLE SET — set active text style (for future text commands) let name = parts.get(1).map(|s| s.trim()).unwrap_or(""); if self.tabs[i].scene.document.text_styles.get(name).is_some() { - self.command_line - .push_output(crate::tf!("{prefix}: active style set to '{name}'.").as_ref()); + self.command_line.push_output( + crate::tf!("{prefix}: active style set to '{name}'.").as_ref(), + ); } else { - self.command_line - .push_error(crate::tf!("{prefix}: style '{name}' not found.").as_ref()); + self.command_line.push_error( + crate::tf!("{prefix}: style '{name}' not found.").as_ref(), + ); } } "NEW" | "N" => { @@ -1471,8 +1577,9 @@ impl OpenCADStudio { self.command_line .push_error(crate::tf!("Usage: {prefix} NEW ").as_ref()); } else if self.tabs[i].scene.document.text_styles.contains(&name) { - self.command_line - .push_error(crate::tf!("{prefix}: style '{name}' already exists.").as_ref()); + self.command_line.push_error( + crate::tf!("{prefix}: style '{name}' already exists.").as_ref(), + ); } else { let undo = self.begin_text_style_undo( i, @@ -1483,8 +1590,9 @@ impl OpenCADStudio { let _ = self.tabs[i].scene.document.text_styles.add(style); self.tabs[i].dirty = true; self.commit_text_style_undo(i, undo); - self.command_line - .push_output(crate::tf!("{prefix}: style '{name}' created.").as_ref()); + self.command_line.push_output( + crate::tf!("{prefix}: style '{name}' created.").as_ref(), + ); } } "FONT" | "F" => { @@ -1492,8 +1600,9 @@ impl OpenCADStudio { let style_name = parts.get(1).map(|s| s.trim()).unwrap_or("").to_string(); let font = parts.get(2).map(|s| s.trim()).unwrap_or("").to_string(); if style_name.is_empty() || font.is_empty() { - self.command_line - .push_error(crate::tf!("Usage: {prefix} FONT