diff --git a/src/app/automation.rs b/src/app/automation.rs index 951bb5c3..f3d6c3d0 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -549,10 +549,14 @@ mod tests { _ => None, }) .expect("CIRCLE should create one entity"); + let center = crate::scene::view::transform::ocs_point_to_wcs( + (circle.center.x, circle.center.y, circle.center.z), + (circle.normal.x, circle.normal.y, circle.normal.z), + ); let close = |a: f64, b: f64| (a - b).abs() < 1e-9; - assert!(close(circle.center.x, 2.0)); - assert!(close(circle.center.y, 0.0)); - assert!(close(circle.center.z, 3.0)); + assert!(close(center.0, 2.0)); + assert!(close(center.1, 0.0)); + assert!(close(center.2, 3.0)); assert!(close(circle.normal.x, 0.0)); assert!(close(circle.normal.y, -1.0)); assert!(close(circle.normal.z, 0.0)); @@ -612,6 +616,10 @@ mod tests { use crate::app::Message; use crate::modules::ModuleEvent; + let command_refusal = + crate::t!("No drawing open. Use NEW or OPEN to start a drawing."); + let tool_refusal = crate::t!("No drawing open — use New or Open first."); + // Fresh app = welcome tab, no drawing. let mut app = OpenCADStudio::new_for_test(); assert!( @@ -633,7 +641,7 @@ mod tests { .collect::>() .join("\n"); assert!( - !out.contains("No drawing open"), + !out.contains(command_refusal.as_ref()), "ABOUT needs no drawing and must not be refused on the welcome page: {out:?}" ); @@ -649,7 +657,7 @@ mod tests { .collect::>() .join("\n"); assert!( - out.contains("No drawing open"), + out.contains(command_refusal.as_ref()), "LINE must still be refused on the welcome page: {out:?}" ); assert!( @@ -669,7 +677,7 @@ mod tests { .collect::>() .join("\n"); assert!( - out.contains("No drawing open"), + out.contains(tool_refusal.as_ref()), "a scene-touching event must stay inert on the welcome page: {out:?}" ); diff --git a/src/app/mod.rs b/src/app/mod.rs index 8df8aafe..604d8e65 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1694,6 +1694,7 @@ pub enum Message { Tick(Instant), /// Periodic drain of plugin-to-host requests that arrived outside a host /// call (e.g. mutations from the Python REPL). + #[cfg(not(target_arch = "wasm32"))] DrainPluginRequests, /// Web: periodic check for per-script fonts a drawing needs but hasn't /// fetched yet (#141). Native: never emitted. @@ -3485,17 +3486,6 @@ impl OpenCADStudio { self.command_line.push_error(msg); } - #[cfg(test)] - pub(crate) fn command_history_info(&self) -> Vec { - use crate::ui::command_line::EntryKind; - self.command_line - .history - .iter() - .filter(|e| e.kind == EntryKind::Info) - .map(|e| e.text.clone()) - .collect() - } - /// Boot function for `iced::daemon`: returns initial state plus a task that /// opens the primary application window. Native only — the web build uses /// [`Self::boot_web`]. diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 67b79317..026b7203 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -111,6 +111,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { self.pending_close = Some(crate::app::PendingClose::Tab(idx)); return self.open_unsaved_dialog_window(); } + #[cfg(not(target_arch = "wasm32"))] let tab_id = self.tabs.get(idx).map(|t| t.id); // This tab is closing for good — drop its autosave recovery copy. #[cfg(not(target_arch = "wasm32"))] @@ -135,8 +136,8 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { self.sync_ribbon_layers(); self.sync_ribbon_styles(); self.sync_ribbon_from_selection(); + #[cfg(not(target_arch = "wasm32"))] if let Some(tab_id) = tab_id { - #[cfg(not(target_arch = "wasm32"))] v4_support::on_tab_closed(tab_id); } Task::none() diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 3c590187..7776ee7a 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -4202,15 +4202,16 @@ pub(super) fn on_open_file(&mut self) -> Task { .unwrap_or("export.ctb".into()); Task::perform( async move { - let mut dialog = crate::sys::file_dialog() + let dialog = crate::sys::file_dialog() .set_title("Save Plot Style Table") .set_file_name(&default_name) .add_filter("Plot Style Files", &["ctb", "CTB"]) .add_filter("All Files", &["*"]); #[cfg(not(target_arch = "wasm32"))] - if let Ok(dir) = crate::io::plot_style::ensure_plot_styles_dir() { - dialog = dialog.set_directory(dir); - } + let dialog = match crate::io::plot_style::ensure_plot_styles_dir() { + Ok(dir) => dialog.set_directory(dir), + Err(_) => dialog, + }; dialog.save_file().await .map(|h| crate::sys::handle_path(&h)) }, diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index f38dc4e9..1957288d 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -6635,16 +6635,17 @@ impl OpenCADStudio { Message::PlotStylePanelApply => self.on_plot_style_panel_apply(), - Message::PlotStylePanelSaveDirect => { - let Some(table) = self.active_plot_style.as_ref() else { + Message::PlotStylePanelSaveDirect => { + if self.active_plot_style.is_none() { self.command_line.push_error( crate::t!("No plot style table loaded. Load or create one first.").as_ref(), ); return Task::none(); - }; + } #[cfg(not(target_arch = "wasm32"))] { + let table = self.active_plot_style.as_ref().expect("checked above"); let table_name = table.name.clone(); let result = crate::io::plot_style::ensure_plot_styles_dir().and_then(|dir| { diff --git a/src/entities/curve.rs b/src/entities/curve.rs index a7e0133e..f30898a4 100644 --- a/src/entities/curve.rs +++ b/src/entities/curve.rs @@ -346,26 +346,7 @@ pub struct CurveSnap { pub key_vertices: Vec<[f64; 3]>, } -/// Every point the entity's own geometry offers to snap to. -/// -/// The point of routing this through [`entity_curve`] rather than reading the -/// fields per type: an arc's quadrants, an ellipse's axis ends and a spline's -/// midpoint are all the same question asked of different shapes, and asking -/// it once means the answer is exact everywhere. Previously an arc offered no -/// quadrants at all and a spline's "endpoints" were its control points, which -/// for a control-point spline are not on the curve. -/// -/// `None` for anything that is not a planar curve, which keeps the callers -/// that have their own snap sources — text, blocks, dimensions — untouched. -pub fn curve_snap(entity: &EntityType) -> Option { - Some(snap_from(&entity_curve(entity)?)) -} - -/// [`curve_snap`] for a caller that already has the curve. -/// -/// The per-type wire builders take a concrete entity rather than an -/// [`EntityType`], and wrapping one back up would mean cloning it on the -/// render path. +/// Snap candidates for a caller that already has the curve. pub fn snap_from(curve: &PlanarCurve) -> CurveSnap { // A chain of straight segments is what `key_vertices` means: the snap // engine joins consecutive entries and offers the midpoint of each. Only @@ -632,7 +613,7 @@ mod tests { arc.start_angle = 0.0; arc.end_angle = PI; // the upper half arc.normal = v3(0.0, 0.0, 1.0); - let snap = curve_snap(&EntityType::Arc(arc)).unwrap(); + let snap = snap_from(&entity_curve(&EntityType::Arc(arc)).unwrap()); let quadrants = hints(&snap, SnapHint::Quadrant); // 0° and 90° and 180° are on it; 270° is not. assert_eq!(quadrants.len(), 3, "{quadrants:?}"); @@ -655,7 +636,7 @@ mod tests { LwVertex::from_coords(10.0, 0.0), LwVertex::from_coords(10.0, 5.0), ]; - let snap = curve_snap(&EntityType::LwPolyline(polyline)).unwrap(); + let snap = snap_from(&entity_curve(&EntityType::LwPolyline(polyline)).unwrap()); assert_eq!(snap.key_vertices.len(), 3); // Midpoints are derived from those by the snap engine, so emitting // them here as well would offer every one of them twice. @@ -667,7 +648,7 @@ mod tests { let mut circle = CircleEnt::default(); circle.radius = 1.0; circle.normal = v3(0.0, 0.0, 1.0); - let snap = curve_snap(&EntityType::Circle(circle)).unwrap(); + let snap = snap_from(&entity_curve(&EntityType::Circle(circle)).unwrap()); assert!(snap.key_vertices.is_empty()); assert!(hints(&snap, SnapHint::Endpoint).is_empty()); assert!(hints(&snap, SnapHint::Midpoint).is_empty()); diff --git a/src/entities/tolerance.rs b/src/entities/tolerance.rs index 6172b0aa..f293e59f 100644 --- a/src/entities/tolerance.rs +++ b/src/entities/tolerance.rs @@ -840,7 +840,6 @@ mod tests { /// together inside correctly-sized boxes. #[test] fn cells_are_drawn_with_the_spacing_they_were_measured_with() { - use acadrust::EntityType; let doc = acadrust::CadDocument::new(); let mut tol = Tolerance::new(); tol.text = "ABC".into(); diff --git a/src/io/mod.rs b/src/io/mod.rs index 51a5443c..9a100c9f 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -71,6 +71,7 @@ impl OpenProgressState { self.phase.store(phase, Ordering::Release); } + #[cfg(not(target_arch = "wasm32"))] pub fn set_fraction(&self, phase: u8, base: u16, span: u16, completed: usize, total: usize) { let denominator = total.max(1) as u64; let value = base as u64 + (completed.min(total.max(1)) as u64 * span as u64 / denominator); @@ -1403,15 +1404,16 @@ mod save_failure_tests { /// Show a file-open dialog and load the selected CTB or STB file. pub async fn pick_plot_style() -> Option { - let mut dialog = crate::sys::file_dialog() + let dialog = crate::sys::file_dialog() .set_title("Load Plot Style Table") .add_filter("Plot Style Tables", &["ctb", "CTB"]) .add_filter("CTB Files", &["ctb", "CTB"]) .add_filter("All Files", &["*"]); #[cfg(not(target_arch = "wasm32"))] - if let Ok(dir) = plot_style::ensure_plot_styles_dir() { - dialog = dialog.set_directory(dir); - } + let dialog = match plot_style::ensure_plot_styles_dir() { + Ok(dir) => dialog.set_directory(dir), + Err(_) => dialog, + }; let handle = dialog.pick_file().await?; plot_style::PlotStyleTable::load(&crate::sys::handle_path(&handle)).ok() } diff --git a/src/io/plot_style.rs b/src/io/plot_style.rs index 95934069..a1e01360 100644 --- a/src/io/plot_style.rs +++ b/src/io/plot_style.rs @@ -10,7 +10,10 @@ use rustc_hash::FxHashMap as HashMap; use std::io::Read; -use std::path::{Component, Path, PathBuf}; +use std::path::{Component, Path}; + +#[cfg(not(target_arch = "wasm32"))] +use std::path::PathBuf; pub const DEFAULT_PLOT_STYLE: &str = "ocad.ctb"; pub const MONOCHROME_PLOT_STYLE: &str = "monochrome.ctb"; diff --git a/src/io/single_instance.rs b/src/io/single_instance.rs index feb6d25b..9b485452 100644 --- a/src/io/single_instance.rs +++ b/src/io/single_instance.rs @@ -400,7 +400,7 @@ mod tests { } let mut got = Vec::new(); - while let Ok(Some(p)) = rx.try_next() { + while let Ok(p) = rx.try_recv() { got.push(p.to_string_lossy().into_owned()); } got.sort(); diff --git a/src/modules/draw/modify/geom.rs b/src/modules/draw/modify/geom.rs index 7ea301d1..8b9d3e77 100644 --- a/src/modules/draw/modify/geom.rs +++ b/src/modules/draw/modify/geom.rs @@ -45,12 +45,6 @@ pub fn line_line( geom2d::line_line([px, py], [dx, dy], [qx, qy], [ex, ey]) } -/// Line parameters where `p + t·d` meets a circle: none, one when tangent, or -/// two ordered by increasing `t`. -pub fn line_circle(px: f64, py: f64, dx: f64, dy: f64, cx: f64, cy: f64, r: f64) -> Vec { - geom2d::line_circle([px, py], [dx, dy], [cx, cy], r) -} - pub fn line_points(start: [f64; 3], end: [f64; 3]) -> Vec<[f32; 3]> { narrow(vec![start, end]) } diff --git a/src/plugin/external.rs b/src/plugin/external.rs index 4407ae99..d7df8c48 100644 --- a/src/plugin/external.rs +++ b/src/plugin/external.rs @@ -267,7 +267,10 @@ fn parse_string_array(s: &str) -> Vec { pub(crate) use loader::{shutdown_plugins, with_manager}; #[cfg(all(not(target_arch = "wasm32"), not(test)))] -pub(crate) use loader::{load_at_startup, loaded_ids, remove_plugin}; +pub(crate) use loader::{load_at_startup, loaded_ids}; + +#[cfg(not(target_arch = "wasm32"))] +pub(crate) use loader::remove_plugin; #[cfg(not(target_arch = "wasm32"))] #[cfg_attr(test, allow(dead_code))] diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 93901d93..d3b1ae2d 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -117,6 +117,7 @@ impl Scene { /// Batch-add several entities, publishing geometry changes once at the end. /// This is the fast path used by plugin `add_entities` requests. + #[cfg(not(target_arch = "wasm32"))] pub fn add_entities(&mut self, entities: Vec) -> Vec { let mut handles = Vec::with_capacity(entities.len()); let mut changes = Vec::with_capacity(entities.len()); diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 4403fdbd..ad546ffb 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -596,6 +596,7 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches { /// /// The callback is UI-agnostic and may run from Rayon workers. Callers should /// keep it cheap, normally just updating atomics. +#[cfg(not(target_arch = "wasm32"))] pub fn build_derived_caches_with_progress( doc: &CadDocument, progress: &(dyn Fn(u16) + Sync), @@ -864,6 +865,7 @@ fn build_derived_caches_impl( /// the loader thread. The temporary `Scene` never crosses threads (it contains /// `Rc`/`RefCell` state); only its Send-safe document and immutable prepared /// geometry are returned. +#[cfg(not(target_arch = "wasm32"))] pub fn prepare_open_geometry( doc: CadDocument, caches: &DerivedCaches, diff --git a/tests/annotative_context_synthesis.rs b/tests/annotative_context_synthesis.rs index 4821eb20..74f705ff 100644 --- a/tests/annotative_context_synthesis.rs +++ b/tests/annotative_context_synthesis.rs @@ -23,7 +23,7 @@ fn empty_annotation_scales_is_not_annotative() { let ent = doc.add_entity(EntityType::MText(m)).unwrap(); // Build xdict -> "AcDbContextDataManager" -> "ACDB_ANNOTATIONSCALES" (empty). - let mut mk = |doc: &mut CadDocument, owner: Handle| -> Handle { + let mk = |doc: &mut CadDocument, owner: Handle| -> Handle { let h = doc.allocate_handle(); let mut d = Dictionary::new(); d.handle = h; diff --git a/tests/block_hatch_export.rs b/tests/block_hatch_export.rs index d394bec8..4aad9d9f 100644 --- a/tests/block_hatch_export.rs +++ b/tests/block_hatch_export.rs @@ -76,8 +76,9 @@ fn block_internal_hatch_reaches_export() { // A blue hatch, wrapped into a block and inserted in model space — the // minimal shape of "coloured fill nested in a block". let h = scene.add_entity(EntityType::Hatch(square_hatch(5))); + let identity = acadrust::types::Transform::identity(); scene - .create_block_from_entities(&[h], "LOGO", glam::DVec3::ZERO) + .create_block_from_entities(&[h], "LOGO", &identity, &identity) .expect("wrap hatch into a block + insert"); scene.populate_hatches_from_document(); @@ -239,17 +240,22 @@ fn app_created_hatch_roundtrips_catalog_spacing() { let mut scene = Scene::new(); let boundary: Vec<[f32; 2]> = vec![[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]]; let model = HatchModel { + render_instance: None, world_origin: [0.0, 0.0], boundary: Arc::new(boundary), boundary_wcs: None, + boundary_exterior: None, + boundary_sources: None, pattern: entry.gpu.clone(), name: "ANSI31".into(), color: [0.75, 0.75, 0.75, 0.85], + aci: 0, + line_weight_px: 1.0, angle_offset: 0.0, scale: 1.0, draw_depth: 0.0, }; - scene.add_hatch(model); + scene.add_hatch(model, None, None); scene.populate_hatches_from_document(); let hatches = scene.paper_canvas_hatches(); @@ -288,19 +294,24 @@ fn nested_hatch_serializes_only_outer_as_external() { let boundary_f32: Vec<[f32; 2]> = wcs.iter().map(|&[x, y]| [x as f32, y as f32]).collect(); let model = HatchModel { + render_instance: None, world_origin: [0.0, 0.0], boundary: Arc::new(boundary_f32), boundary_wcs: Some(Arc::new(wcs)), + boundary_exterior: None, + boundary_sources: None, pattern: HatchPattern::Solid, name: "SOLID".into(), color: [0.45, 0.45, 0.45, 0.60], + aci: 0, + line_weight_px: 1.0, angle_offset: 0.0, scale: 1.0, draw_depth: 0.0, }; let mut scene = Scene::new(); - scene.add_hatch(model); + scene.add_hatch(model, None, None); let dxf = scene .document diff --git a/tests/page_setup_root_repair.rs b/tests/page_setup_root_repair.rs index fdd2f5d0..74b7e350 100644 --- a/tests/page_setup_root_repair.rs +++ b/tests/page_setup_root_repair.rs @@ -2,7 +2,7 @@ // dictionary pointer unresolvable — it names a handle that never loaded (or a // non-dictionary), while the real named-object sub-dictionaries are owned by an // unrelated handle. Navigating that root then silently no-ops, so registering a -// new named-object entry (a page setup, the CTAB current-tab variable, an +// new named-object entry (a page setup, the variable dictionary, an // annotation scale) would vanish instead of persisting. // // `annotative::root_named_dict_handle` resolves the root robustly and, when it @@ -121,8 +121,19 @@ fn ctab_is_created_against_a_repaired_root() { else { panic!("root must be a dictionary"); }; + let variable_dictionary = root + .entries + .iter() + .find(|(key, _)| key == "AcDbVariableDictionary") + .map(|(_, handle)| *handle) + .expect("variable dictionary must be registered in the repaired root NOD"); + let Some(ObjectType::Dictionary(variable_dictionary)) = + scene.document.objects.get(&variable_dictionary) + else { + panic!("variable dictionary must resolve"); + }; assert!( - root.entries.iter().any(|(k, _)| k == "CTAB"), - "CTAB must be registered in the repaired root NOD" + variable_dictionary.entries.iter().any(|(key, _)| key == "CTAB"), + "CTAB must be registered in the variable dictionary" ); } diff --git a/tests/pdf_export_text_check.rs b/tests/pdf_export_text_check.rs index c47f186c..e8b1ac3b 100644 --- a/tests/pdf_export_text_check.rs +++ b/tests/pdf_export_text_check.rs @@ -11,7 +11,7 @@ use acadrust::entities::{Dimension, DimensionLinear, Text}; use acadrust::types::Vector3; use acadrust::EntityType; -use OpenCADStudio::io::pdf_export::export_pdf; +use OpenCADStudio::io::pdf_export::{export_pdf, PdfPlotOptions}; use OpenCADStudio::scene::Scene; #[test] @@ -69,6 +69,7 @@ fn text_and_dim_reach_pdf_export() { None, &p_text, None, + PdfPlotOptions::default(), ) .expect("export with text"); let with_text = std::fs::read(&p_text).expect("read pdf"); @@ -96,6 +97,7 @@ fn text_and_dim_reach_pdf_export() { None, &p_bare, None, + PdfPlotOptions::default(), ) .expect("export without text"); let no_text = std::fs::read(&p_bare).expect("read pdf"); diff --git a/tests/text_font_rendering.rs b/tests/text_font_rendering.rs index 5ec17819..b0287a78 100644 --- a/tests/text_font_rendering.rs +++ b/tests/text_font_rendering.rs @@ -49,12 +49,21 @@ fn expand_block_mtext( let ins = Insert::new("LABEL_BLOCK", insert_at); doc.add_entity(EntityType::Insert(ins.clone())).unwrap(); - let cache = BlockCache::build(&doc, 1.0, [0.0, 0.0, 0.0, 1.0], &Default::default()); + let cache = BlockCache::build( + &doc, + 1.0, + None, + true, + [0.0, 0.0, 0.0, 1.0], + None, + &Default::default(), + ); expand_insert( &cache, &ins, Handle::new(999), [1.0, 1.0, 1.0, 1.0], + 0, 0.0, [0.0; 8], 1.0, @@ -66,6 +75,7 @@ fn expand_block_mtext( pat: [0.0; 8], lw_px: 1.0, }, + 0, false, 1.0, None,