diff --git a/Cargo.lock b/Cargo.lock index b288dcaf8..49635a72c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -919,7 +919,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=1d81d7f9470b843feee06fb9136997d4a69ac553#1d81d7f9470b843feee06fb9136997d4a69ac553" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=50f020377c01fcebb5f31844b9900f046b05192f#50f020377c01fcebb5f31844b9900f046b05192f" dependencies = [ "acadrust", "cavalier_contours", @@ -930,7 +930,7 @@ dependencies = [ [[package]] name = "cadkernel-constraints" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=1d81d7f9470b843feee06fb9136997d4a69ac553#1d81d7f9470b843feee06fb9136997d4a69ac553" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=50f020377c01fcebb5f31844b9900f046b05192f#50f020377c01fcebb5f31844b9900f046b05192f" dependencies = [ "nalgebra", ] diff --git a/Cargo.toml b/Cargo.toml index 343915331..1467496a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,8 +33,8 @@ rfd = "0.17" clap = { version = "4", features = ["derive", "string"] } env_logger = "0.11" acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "4d7b359b0571f9b6e0f85a62a21c67508722b83e", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "1d81d7f9470b843feee06fb9136997d4a69ac553", features = ["acis", "offset"] } -cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "1d81d7f9470b843feee06fb9136997d4a69ac553" } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "50f020377c01fcebb5f31844b9900f046b05192f", features = ["acis", "offset"] } +cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "50f020377c01fcebb5f31844b9900f046b05192f" } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } diff --git a/src/app/annotation_data.rs b/src/app/annotation_data.rs index 0b415aa41..e21451319 100644 --- a/src/app/annotation_data.rs +++ b/src/app/annotation_data.rs @@ -1396,3 +1396,53 @@ impl OpenCADStudio { Task::none() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cell_ranges_are_normalized_and_missing_cells_are_blank() { + let rows = vec![ + vec!["a".into(), "b".into(), "c".into()], + vec!["d".into()], + ]; + assert_eq!( + crop_range(rows, "C2:A1").unwrap(), + vec![ + vec!["a".to_string(), "b".to_string(), "c".to_string()], + vec!["d".to_string(), String::new(), String::new()], + ] + ); + } + + #[test] + fn named_range_targets_accept_quoted_sheet_names() { + assert_eq!( + named_range_target("='Sheet One'!$A$1:$D$10"), + Some(("Sheet One".into(), "A1:D10".into())) + ); + } + + #[test] + fn csv_links_apply_the_selected_cell_range() { + let path = std::env::temp_dir().join(format!( + "ocs-data-link-{}-{}.csv", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::write(&path, "a,b,c\nd,e,f\n").unwrap(); + let rows = read_tabular_file(&path, None, LinkRange::CellRange, Some("B1:C2")); + let _ = std::fs::remove_file(path); + assert_eq!( + rows.unwrap(), + vec![ + vec!["b".to_string(), "c".to_string()], + vec!["e".to_string(), "f".to_string()], + ] + ); + } +} diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 31734995e..af720ea76 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -2949,6 +2949,93 @@ impl OpenCADStudio { self.tabs[i].scene.clear_preview_wire(); self.refresh_properties(); } + CmdResult::EditDimensionBreak { + dimensions, + operation, + } => { + self.push_undo_snapshot(i, "DIMBREAK"); + let result = apply_dimbreak(&mut self.tabs[i].scene, &dimensions, operation); + if result.changed.is_empty() { + self.command_line.push_info(&result.message); + } else { + for handle in &result.changed { + self.tabs[i].scene.invalidate_dim_block_recorded(*handle); + } + let changes = result + .changed + .iter() + .copied() + .map(|handle| (handle, crate::scene::ChangeKind::Modified)) + .collect::>(); + self.tabs[i].scene.bump_entities(&changes); + self.tabs[i].dirty = true; + self.command_line.push_output(&result.message); + } + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + } + CmdResult::EditDimensionJog { dimension, point } => { + let valid = matches!( + self.tabs[i].scene.document.get_entity(dimension), + Some(acadrust::EntityType::Dimension( + acadrust::entities::Dimension::Linear(_) + | acadrust::entities::Dimension::Aligned(_) + )) + ); + if !valid { + self.command_line.push_error( + crate::t!("DIMJOGLINE: select a linear or aligned dimension.").as_ref(), + ); + } else if !self.reject_locked_edit(i, dimension) { + self.push_undo_snapshot(i, "DIMJOGLINE"); + let values = point.map(|point| { + use acadrust::xdata::XDataValue; + vec![ + XDataValue::Integer16(387), + XDataValue::Integer16(3), + XDataValue::Integer16(389), + XDataValue::Point3D(acadrust::types::Vector3::new( + point.x, point.y, point.z, + )), + ] + }); + crate::scene::view::dispatch::set_entity_xdata( + &mut self.tabs[i].scene.document, + dimension, + "ACAD_DSTYLE_DIMJAG_POSITION", + values, + ); + self.tabs[i].scene.invalidate_dim_block_recorded(dimension); + self.tabs[i] + .scene + .bump_entities(&[(dimension, crate::scene::ChangeKind::Modified)]); + self.tabs[i].dirty = true; + self.command_line.push_output( + if point.is_some() { + crate::t!("DIMJOGLINE: jog added.") + } else { + crate::t!("DIMJOGLINE: jog removed.") + } + .as_ref(), + ); + } + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + } + CmdResult::SpaceDimensions { + base, + others, + spacing, + } => { + self.push_undo_snapshot(i, "DIMSPACE"); + if apply_dimspace(&mut self.tabs[i].scene, base, &others, spacing) { + self.tabs[i].dirty = true; + self.command_line + .push_output(crate::t!("DIMSPACE Spacing adjusted.").as_ref()); + } + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + } CmdResult::ReplaceEntity(handle, new_entities) => { if self.reject_locked_edit(i, handle) { return Task::none(); @@ -2974,122 +3061,6 @@ impl OpenCADStudio { } } } - // Detect DIMBREAK sentinel. - if new_entities.len() == 1 { - if let acadrust::EntityType::XLine(ref xl) = new_entities[0] { - let layer = xl.common.layer.clone(); - if layer.starts_with("__DIMBREAK_") { - self.push_undo_snapshot(i, "DIMBREAK"); - let result = apply_dimbreak(&mut self.tabs[i].scene, &layer); - if result.changed.is_empty() { - self.command_line.push_info(&result.message); - self.tabs[i].active_cmd = None; - self.tabs[i].snap_result = None; - return Task::none(); - } - for handle in &result.changed { - self.tabs[i].scene.invalidate_dim_block_recorded(*handle); - } - let changes: Vec<_> = result - .changed - .iter() - .copied() - .map(|handle| (handle, crate::scene::ChangeKind::Modified)) - .collect(); - self.tabs[i].scene.bump_entities(&changes); - self.tabs[i].dirty = true; - self.command_line.push_output(&result.message); - self.tabs[i].active_cmd = None; - self.tabs[i].snap_result = None; - return Task::none(); - } - if layer.starts_with("__DIMSPACE__") { - self.push_undo_snapshot(i, "DIMSPACE"); - if let Some(encoded) = layer.strip_prefix("__DIMSPACE__") { - apply_dimspace(&mut self.tabs[i].scene, encoded); - } - self.command_line.push_output(crate::t!("DIMSPACE Spacing adjusted.").as_ref()); - self.tabs[i].dirty = true; - self.tabs[i].active_cmd = None; - self.tabs[i].snap_result = None; - return Task::none(); - } - if layer.starts_with("__DIMJOG__") - || layer.starts_with("__DIMJOG_REMOVE__") - { - let remove = layer.starts_with("__DIMJOG_REMOVE__"); - let point = layer - .strip_prefix("__DIMJOG__") - .and_then(|encoded| encoded.split_once('|')) - .and_then(|(_, coordinates)| { - let mut values = coordinates - .split(',') - .filter_map(|value| value.parse::().ok()); - Some(acadrust::types::Vector3::new( - values.next()?, - values.next()?, - values.next()?, - )) - }); - let valid = matches!( - self.tabs[i].scene.document.get_entity(handle), - Some(acadrust::EntityType::Dimension( - acadrust::entities::Dimension::Linear(_) - | acadrust::entities::Dimension::Aligned(_) - )) - ); - if !valid || (!remove && point.is_none()) { - self.command_line.push_error( - crate::t!("DIMJOGLINE: select a linear or aligned dimension.") - .as_ref(), - ); - self.tabs[i].active_cmd = None; - self.tabs[i].snap_result = None; - return Task::none(); - } - self.push_undo_snapshot(i, "DIMJOGLINE"); - if remove { - crate::scene::view::dispatch::set_entity_xdata( - &mut self.tabs[i].scene.document, - handle, - "ACAD_DSTYLE_DIMJAG_POSITION", - None, - ); - } else if let Some(point) = point { - use acadrust::xdata::XDataValue; - crate::scene::view::dispatch::set_entity_xdata( - &mut self.tabs[i].scene.document, - handle, - "ACAD_DSTYLE_DIMJAG_POSITION", - Some(vec![ - XDataValue::Integer16(387), - XDataValue::Integer16(3), - XDataValue::Integer16(389), - XDataValue::Point3D(point), - ]), - ); - } - self.tabs[i].scene.invalidate_dim_block_recorded(handle); - self.tabs[i].scene.bump_entities(&[( - handle, - crate::scene::ChangeKind::Modified, - )]); - self.tabs[i].dirty = true; - self.command_line.push_output( - if remove { - crate::t!("DIMJOGLINE: jog removed.") - } else { - crate::t!("DIMJOGLINE: jog added.") - } - .as_ref(), - ); - self.tabs[i].active_cmd = None; - self.tabs[i].snap_result = None; - return Task::none(); - } - } - } - let label = self.history_label_from_active_cmd(i, "TRIM"); self.push_undo_snapshot(i, label); self.tabs[i].scene.erase_entities(&[handle]); @@ -6352,56 +6323,6 @@ struct DimBreakResult { message: String, } -#[derive(Clone, Copy)] -enum DimBreakOperation { - Auto, - Object(Handle), - Manual(glam::DVec3, glam::DVec3), - Remove, -} - -fn parse_dim_break_handles(encoded: &str) -> Vec { - encoded - .split(';') - .filter_map(|value| value.parse::().ok()) - .map(Handle::from) - .filter(|handle| !handle.is_null()) - .collect() -} - -fn parse_dim_break_point(encoded: &str) -> Option { - let mut values = encoded.split(',').map(str::parse::); - Some(glam::DVec3::new( - values.next()?.ok()?, - values.next()?.ok()?, - values.next()?.ok()?, - )) -} - -fn parse_dim_break(encoded: &str) -> Option<(Vec, DimBreakOperation)> { - if let Some(handles) = encoded.strip_prefix("__DIMBREAK_AUTO__") { - return Some((parse_dim_break_handles(handles), DimBreakOperation::Auto)); - } - if let Some(handles) = encoded.strip_prefix("__DIMBREAK_REMOVE__") { - return Some((parse_dim_break_handles(handles), DimBreakOperation::Remove)); - } - if let Some(rest) = encoded.strip_prefix("__DIMBREAK_OBJECT__") { - let (handles, crossing) = rest.split_once('|')?; - return Some(( - parse_dim_break_handles(handles), - DimBreakOperation::Object(Handle::from(crossing.parse::().ok()?)), - )); - } - if let Some(rest) = encoded.strip_prefix("__DIMBREAK_MANUAL__") { - let mut parts = rest.split('|'); - let handles = parse_dim_break_handles(parts.next()?); - let first = parse_dim_break_point(parts.next()?)?; - let second = parse_dim_break_point(parts.next()?)?; - return Some((handles, DimBreakOperation::Manual(first, second))); - } - None -} - fn wire_segments( models: &[crate::scene::model::wire_model::WireModel], ) -> Vec<(glam::DVec3, glam::DVec3)> { @@ -6435,33 +6356,12 @@ fn segment_intersection_xy( first: (glam::DVec3, glam::DVec3), second: (glam::DVec3, glam::DVec3), ) -> Option<(glam::DVec3, glam::DVec3)> { - let p = first.0; - let r = first.1 - first.0; - let q = second.0; - let s = second.1 - second.0; - let cross = |a: glam::DVec3, b: glam::DVec3| a.x * b.y - a.y * b.x; - let denominator = cross(r, s); - if denominator.abs() <= 1.0e-12 { - return None; - } - let qp = q - p; - let t = cross(qp, s) / denominator; - let u = cross(qp, r) / denominator; - if !(-1.0e-9..=1.0 + 1.0e-9).contains(&t) - || !(-1.0e-9..=1.0 + 1.0e-9).contains(&u) - { - return None; - } - let direction = r.normalize_or_zero(); - if direction.length_squared() <= 1.0e-18 { - return None; - } - let center = p + r * t.clamp(0.0, 1.0); - let half_gap = 0.25_f64.min(first.0.distance(first.1) * 0.2); - Some(( - center - direction * half_gap, - center + direction * half_gap, - )) + let gap = cadkernel::space::segment_break_gap_xy( + [first.0.to_array(), first.1.to_array()], + [second.0.to_array(), second.1.to_array()], + 0.25, + )?; + Some((glam::DVec3::from_array(gap[0]), glam::DVec3::from_array(gap[1]))) } fn break_object_handle(document: &acadrust::CadDocument, dimension: Handle) -> Option { @@ -6593,15 +6493,16 @@ fn break_reference( } } -fn apply_dimbreak(scene: &mut crate::scene::Scene, encoded: &str) -> DimBreakResult { - let Some((requested, operation)) = parse_dim_break(encoded) else { - return DimBreakResult { - changed: Vec::new(), - message: crate::t!("DIMBREAK: invalid operation data.").into_owned(), - }; - }; +fn apply_dimbreak( + scene: &mut crate::scene::Scene, + requested: &[Handle], + operation: crate::command::DimensionBreakOperation, +) -> DimBreakResult { + use crate::command::DimensionBreakOperation; + let dimensions: Vec<_> = requested - .into_iter() + .iter() + .copied() .filter(|handle| { matches!( scene.document.get_entity(*handle), @@ -6616,7 +6517,7 @@ fn apply_dimbreak(scene: &mut crate::scene::Scene, encoded: &str) -> DimBreakRes }; } - if matches!(operation, DimBreakOperation::Remove) { + if matches!(operation, DimensionBreakOperation::Remove) { let changed: Vec<_> = dimensions .into_iter() .filter(|handle| remove_dimension_break_data(&mut scene.document, *handle)) @@ -6629,8 +6530,8 @@ fn apply_dimbreak(scene: &mut crate::scene::Scene, encoded: &str) -> DimBreakRes } let crossing_handles: Vec = match operation { - DimBreakOperation::Object(handle) => vec![handle], - DimBreakOperation::Auto => scene + DimensionBreakOperation::Object(handle) => vec![handle], + DimensionBreakOperation::Auto => scene .document .entities() .map(|entity| entity.common().handle) @@ -6638,7 +6539,7 @@ fn apply_dimbreak(scene: &mut crate::scene::Scene, encoded: &str) -> DimBreakRes !dimensions.contains(handle) && scene.entity_belongs_to_active_space(*handle) }) .collect(), - DimBreakOperation::Manual(_, _) | DimBreakOperation::Remove => Vec::new(), + DimensionBreakOperation::Manual(_, _) | DimensionBreakOperation::Remove => Vec::new(), }; let crossing_segments: Vec<_> = crossing_handles .iter() @@ -6659,7 +6560,7 @@ fn apply_dimbreak(scene: &mut crate::scene::Scene, encoded: &str) -> DimBreakRes _ => None, }) .unwrap_or_default(); - let mut references = if matches!(operation, DimBreakOperation::Manual(_, _)) { + let mut references = if matches!(operation, DimensionBreakOperation::Manual(_, _)) { existing } else { existing @@ -6668,10 +6569,10 @@ fn apply_dimbreak(scene: &mut crate::scene::Scene, encoded: &str) -> DimBreakRes .collect() }; match operation { - DimBreakOperation::Manual(first, second) => { + DimensionBreakOperation::Manual(first, second) => { references.push(break_reference(references.len() as i32, 2, first, second)); } - DimBreakOperation::Auto | DimBreakOperation::Object(_) => { + DimensionBreakOperation::Auto | DimensionBreakOperation::Object(_) => { let dimension_segments = wire_segments(&scene.wire_models_for(&[*dimension])); let mut intersections = Vec::new(); for dim_segment in &dimension_segments { @@ -6697,13 +6598,13 @@ fn apply_dimbreak(scene: &mut crate::scene::Scene, encoded: &str) -> DimBreakRes }, )); } - DimBreakOperation::Remove => {} + DimensionBreakOperation::Remove => {} } pending.push((*dimension, references)); } let reserved = match operation { - DimBreakOperation::Object(handle) => handle, + DimensionBreakOperation::Object(handle) => handle, _ => Handle::NULL, }; let changed: Vec<_> = pending @@ -6723,65 +6624,55 @@ fn apply_dimbreak(scene: &mut crate::scene::Scene, encoded: &str) -> DimBreakRes } } -/// Parse `base_val,h1;h2;...;hN,spacing` and adjust parallel dimension positions. -fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) { - // Format: ",

;

;...;," - let parts: Vec<&str> = encoded.splitn(3, ',').collect(); - if parts.len() < 3 { - return; - } - let base_val: u64 = parts[0].parse().unwrap_or(0); - let other_vals: Vec = parts[1] - .split(';') - .filter_map(|s| s.parse::().ok()) - .collect(); - let requested_spacing = if parts[2].eq_ignore_ascii_case("AUTO") { - None - } else { - parts[2].parse::().ok() - }; - +fn apply_dimspace( + scene: &mut crate::scene::Scene, + base_h: Handle, + others: &[Handle], + requested_spacing: Option, +) -> bool { use acadrust::entities::Dimension; - let base_h = acadrust::Handle::from(base_val); - // Base dim: the perpendicular direction (from its rotation / axis) and the - // dim line's perpendicular coordinate. Spacing steps each parallel dim along - // this perp IN THE DRAWING PLANE — offsetting Z had no effect on the dim - // line, which is computed from def·perp with perp.z = 0. (#181 / DIM-021) - let (perp, base_coord, auto_spacing) = match scene.document.get_entity(base_h) { + let (axis, normal, definition, auto_spacing) = match scene.document.get_entity(base_h) { Some(acadrust::EntityType::Dimension(dimension @ Dimension::Linear(d))) => { - let (s, c) = d.rotation.sin_cos(); - let perp = (-s, c); - let dp = d.definition_point; let spacing = dimension_auto_spacing(&scene.document, dimension, scene.creation_annotation_multiplier()); - (perp, dp.x * perp.0 + dp.y * perp.1, spacing) + ( + [d.rotation.cos(), d.rotation.sin(), 0.0], + [d.base.normal.x, d.base.normal.y, d.base.normal.z], + [d.definition_point.x, d.definition_point.y, d.definition_point.z], + spacing, + ) } Some(acadrust::EntityType::Dimension(dimension @ Dimension::Aligned(d))) => { - let dx = d.second_point.x - d.first_point.x; - let dy = d.second_point.y - d.first_point.y; - let len = (dx * dx + dy * dy).sqrt().max(1e-12); - let perp = (-dy / len, dx / len); - let dp = d.definition_point; let spacing = dimension_auto_spacing(&scene.document, dimension, scene.creation_annotation_multiplier()); - (perp, dp.x * perp.0 + dp.y * perp.1, spacing) + ( + [ + d.second_point.x - d.first_point.x, + d.second_point.y - d.first_point.y, + d.second_point.z - d.first_point.z, + ], + [d.base.normal.x, d.base.normal.y, d.base.normal.z], + [d.definition_point.x, d.definition_point.y, d.definition_point.z], + spacing, + ) } - _ => return, + _ => return false, + }; + let Some(frame) = cadkernel::space::dimension_spacing_frame(axis, normal, definition) else { + return false; }; let effective_spacing = requested_spacing.unwrap_or(auto_spacing); let mut changes = Vec::new(); - for (idx, &hv) in other_vals.iter().enumerate() { - let h = acadrust::Handle::from(hv); - let target = base_coord + effective_spacing * (idx + 1) as f64; + for (idx, &h) in others.iter().enumerate() { + let target = frame.coordinate + effective_spacing * (idx + 1) as f64; let mut changed = false; if let Some(acadrust::EntityType::Dimension(dim)) = scene.document.get_entity_mut(h) { - // Slide this dim's definition point along perp so its perpendicular - // coordinate equals `target`; update both the struct field (render) - // and base (save). let slide = |p: &mut acadrust::types::Vector3| { - let cur = p.x * perp.0 + p.y * perp.1; - let delta = target - cur; - p.x += perp.0 * delta; - p.y += perp.1 * delta; + let point = cadkernel::space::move_to_dimension_spacing( + [p.x, p.y, p.z], + frame, + target, + ); + *p = acadrust::types::Vector3::new(point[0], point[1], point[2]); }; match dim { Dimension::Linear(d) => { @@ -6807,6 +6698,7 @@ fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) { if !changes.is_empty() { scene.bump_entities(&changes); } + !changes.is_empty() } fn dimension_auto_spacing( diff --git a/src/command.rs b/src/command.rs index 52b3f4f84..e5b43ea76 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1322,6 +1322,14 @@ pub enum DimensionEditOperation { Oblique(f64), } +#[derive(Clone, Copy)] +pub enum DimensionBreakOperation { + Auto, + Object(Handle), + Manual(DVec3, DVec3), + Remove, +} + /// Returned by every `CadCommand` method to tell main.rs what to do. #[allow(dead_code)] pub enum CmdResult { @@ -1488,6 +1496,19 @@ pub enum CmdResult { source: Handle, point: DVec3, }, + EditDimensionBreak { + dimensions: Vec, + operation: DimensionBreakOperation, + }, + EditDimensionJog { + dimension: Handle, + point: Option, + }, + SpaceDimensions { + base: Handle, + others: Vec, + spacing: Option, + }, /// Cancel: discard any preview and end the command. Cancel, /// End the command and begin in-place editing of the table cell under diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index 38f2fde3b..bc3fa9a2b 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -3252,64 +3252,63 @@ fn dimension_jog_point(dimension: &Dimension) -> Option { }) } -fn apply_dimension_jog(lines: &mut Vec<[f32; 3]>, requested: Vec3, size: f32, angle: f32) { +fn apply_dimension_jog( + lines: &mut Vec<[f32; 3]>, + requested: Vec3, + normal: Vec3, + size: f32, + angle: f32, +) { if lines.len() < 2 { return; } - let mut best: Option<(usize, Vec3, f32)> = None; - for (index, segment) in lines.windows(2).enumerate() { - if segment[0][0].is_nan() || segment[1][0].is_nan() { - continue; - } - let first = Vec3::from_array(segment[0]); - let second = Vec3::from_array(segment[1]); - let delta = second - first; - let length_squared = delta.length_squared(); - if length_squared <= 1.0e-12 { - continue; - } - let parameter = ((requested - first).dot(delta) / length_squared).clamp(0.0, 1.0); - let point = first + delta * parameter; - let distance = point.distance_squared(requested); - if best.is_none_or(|(_, _, current)| distance < current) { - best = Some((index, point, distance)); - } - } - let Some((target, center, _)) = best else { + let segments = lines + .windows(2) + .enumerate() + .filter(|(_, segment)| segment[0][0].is_finite() && segment[1][0].is_finite()) + .map(|(index, segment)| { + ( + index, + [segment[0].map(f64::from), segment[1].map(f64::from)], + ) + }) + .collect::>(); + let coordinates = segments.iter().map(|(_, segment)| *segment).collect::>(); + let Some((candidate, center)) = cadkernel::space::nearest_segment_point( + &coordinates, + requested.to_array().map(f64::from), + ) else { return; }; - let first = Vec3::from_array(lines[target]); - let second = Vec3::from_array(lines[target + 1]); - let Some(direction) = (second - first).try_normalize() else { + let target = segments[candidate].0; + let Some(jog) = cadkernel::space::dimension_jog_points( + coordinates[candidate], + center, + normal.to_array().map(f64::from), + size as f64, + angle as f64, + ) else { return; }; - let perpendicular = Vec3::new(-direction.y, direction.x, 0.0); - let half = size.max(1.0e-3).min(first.distance(second) * 0.2); - let amplitude = (half * (angle * 0.5).tan().abs()).clamp(half * 0.35, half * 1.5); - let jog_start = center - direction * half; - let jog_end = center + direction * half; - let jog = [ - jog_start, - center - direction * half * 0.25 + perpendicular * amplitude, - center + direction * half * 0.25 - perpendicular * amplitude, - jog_end, - ]; + let jog = jog.map(|point| point.map(|value| value as f32)); + let first = lines[target]; + let second = lines[target + 1]; let mut output = Vec::with_capacity(lines.len() + 4); output.extend_from_slice(&lines[..target]); if !output.is_empty() && !output.last().is_some_and(|point| point[0].is_nan()) { output.push([f32::NAN; 3]); } - if first.distance_squared(jog_start) > 1.0e-12 { - output.push(first.to_array()); - output.push(jog_start.to_array()); + if first != jog[0] { + output.push(first); + output.push(jog[0]); output.push([f32::NAN; 3]); } - output.extend(jog.into_iter().map(|point| point.to_array())); - if jog_end.distance_squared(second) > 1.0e-12 { + output.extend(jog); + if jog[3] != second { output.push([f32::NAN; 3]); - output.push(jog_end.to_array()); - output.push(second.to_array()); + output.push(jog[3]); + output.push(second); } if target + 2 < lines.len() { if !output.last().is_some_and(|point| point[0].is_nan()) @@ -3664,7 +3663,13 @@ fn tessellate_dimension_inner( let jog_angle = style .map(|style| style.dimjogang as f32) .unwrap_or(std::f32::consts::FRAC_PI_4); - apply_dimension_jog(&mut geom.dim_lines, point, dim_txt as f32 * 0.6, jog_angle); + apply_dimension_jog( + &mut geom.dim_lines, + point, + vec3_local(dim.base().normal), + dim_txt as f32 * 0.6, + jog_angle, + ); } apply_dimension_breaks(document, handle, &mut geom.dim_lines); apply_dimension_breaks(document, handle, &mut geom.ext_lines); diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index 7e253e373..f1fe5ac84 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -1195,6 +1195,7 @@ fn move_segment_parallel(polyline: &mut LwPolyline, segment: usize, offset: f64) }); } +#[cfg(test)] fn resize_arc_concentrically(polyline: &mut LwPolyline, segment: usize, offset: f64) { edit_polyline_geometry(polyline, |geometry| { geometry diff --git a/src/modules/annotate/dimbreak.rs b/src/modules/annotate/dimbreak.rs index 49011dea6..6512adb94 100644 --- a/src/modules/annotate/dimbreak.rs +++ b/src/modules/annotate/dimbreak.rs @@ -39,33 +39,18 @@ impl DimBreakCommand { } } - fn marker(layer: String, primary: Handle) -> CmdResult { - use acadrust::entities::XLine; - let mut marker = XLine::default(); - marker.common.layer = layer; - CmdResult::ReplaceEntity(primary, vec![EntityType::XLine(marker)]) - } - - fn handles(handles: &[Handle]) -> String { - handles - .iter() - .map(|handle| handle.value().to_string()) - .collect::>() - .join(";") - } - fn automatic(handles: &[Handle]) -> CmdResult { - Self::marker( - format!("__DIMBREAK_AUTO__{}", Self::handles(handles)), - handles[0], - ) + CmdResult::EditDimensionBreak { + dimensions: handles.to_vec(), + operation: crate::command::DimensionBreakOperation::Auto, + } } fn remove(handles: &[Handle]) -> CmdResult { - Self::marker( - format!("__DIMBREAK_REMOVE__{}", Self::handles(handles)), - handles[0], - ) + CmdResult::EditDimensionBreak { + dimensions: handles.to_vec(), + operation: crate::command::DimensionBreakOperation::Remove, + } } } @@ -155,14 +140,10 @@ impl CadCommand for DimBreakCommand { } CmdResult::NeedPoint } - Step::PickCrossing(handles) => Self::marker( - format!( - "__DIMBREAK_OBJECT__{}|{}", - Self::handles(handles), - handle.value() - ), - handles[0], - ), + Step::PickCrossing(handles) => CmdResult::EditDimensionBreak { + dimensions: handles.clone(), + operation: crate::command::DimensionBreakOperation::Object(handle), + }, Step::ManualFirst(_) | Step::ManualSecond(_, _) => CmdResult::NeedPoint, } } @@ -194,19 +175,10 @@ impl CadCommand for DimBreakCommand { self.step = Step::ManualSecond(handles.clone(), point); CmdResult::NeedPoint } - Step::ManualSecond(handles, first) => Self::marker( - format!( - "__DIMBREAK_MANUAL__{}|{:.15},{:.15},{:.15}|{:.15},{:.15},{:.15}", - Self::handles(handles), - first.x, - first.y, - first.z, - point.x, - point.y, - point.z - ), - handles[0], - ), + Step::ManualSecond(handles, first) => CmdResult::EditDimensionBreak { + dimensions: handles.clone(), + operation: crate::command::DimensionBreakOperation::Manual(*first, point), + }, _ => CmdResult::NeedPoint, } } @@ -224,3 +196,21 @@ impl CadCommand for DimBreakCommand { } inventory::submit!(crate::command::CommandRegistration { names: &["DIMBREAK"] }); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn automatic_break_returns_a_typed_edit() { + let dimensions = vec![Handle::from(7), Handle::from(9)]; + let CmdResult::EditDimensionBreak { + dimensions: actual, + operation: crate::command::DimensionBreakOperation::Auto, + } = DimBreakCommand::automatic(&dimensions) + else { + panic!("expected dimension break edit"); + }; + assert_eq!(actual, dimensions); + } +} diff --git a/src/modules/annotate/dimjogline.rs b/src/modules/annotate/dimjogline.rs index 43e166965..d2af762f9 100644 --- a/src/modules/annotate/dimjogline.rs +++ b/src/modules/annotate/dimjogline.rs @@ -2,7 +2,7 @@ use acadrust::entities::Dimension; use acadrust::{EntityType, Handle}; -use glam::{DVec3, Vec3}; +use glam::DVec3; use crate::command::{CadCommand, CmdOption, CmdResult, InputKind}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; @@ -43,16 +43,10 @@ impl DimJogLineCommand { } fn result(handle: Handle, point: Option) -> CmdResult { - use acadrust::entities::XLine; - let mut marker = XLine::default(); - marker.common.layer = match point { - Some(point) => format!( - "__DIMJOG__{}|{:.15},{:.15},{:.15}", - handle.value(), point.x, point.y, point.z - ), - None => format!("__DIMJOG_REMOVE__{}", handle.value()), - }; - CmdResult::ReplaceEntity(handle, vec![EntityType::XLine(marker)]) + CmdResult::EditDimensionJog { + dimension: handle, + point, + } } fn default_position(dimension: &Dimension) -> Option { @@ -75,29 +69,23 @@ impl DimJogLineCommand { } _ => return None, }; - let first = DVec3::new(first.x, first.y, first.z); - let second = DVec3::new(second.x, second.y, second.z); - let definition = DVec3::new(definition.x, definition.y, definition.z); - let axis = axis.try_normalize().unwrap_or(DVec3::X); - let perpendicular = DVec3::new(-axis.y, axis.x, 0.0); - let offset = definition.dot(perpendicular); - let first_on_line = first + perpendicular * (offset - first.dot(perpendicular)); - let second_on_line = second + perpendicular * (offset - second.dot(perpendicular)); - let midpoint = (first_on_line + second_on_line) * 0.5; - - let text = dimension.base().text_middle_point; - let text = DVec3::new(text.x, text.y, text.z); - let line = second_on_line - first_on_line; - let length_squared = line.length_squared(); - if length_squared <= 1.0e-18 { - return Some(midpoint); - } - let parameter = (text - first_on_line).dot(line) / length_squared; - if text.is_finite() && (0.0..=1.0).contains(¶meter) { - Some((first_on_line + text) * 0.5) + let base = dimension.base(); + let text = base.text_middle_point; + let normal = base.normal; + let text = if base.text_user_positioned { + [text.x, text.y, text.z] } else { - Some(midpoint) - } + [f64::NAN; 3] + }; + cadkernel::space::default_dimension_jog_position( + [first.x, first.y, first.z], + [second.x, second.y, second.z], + [definition.x, definition.y, definition.z], + axis.to_array(), + [normal.x, normal.y, normal.z], + text, + ) + .map(DVec3::from_array) } } @@ -198,34 +186,37 @@ impl CadCommand for DimJogLineCommand { if !matches!(self.step, Step::PickJogPos { .. }) { return None; } - let axis = self - .dimension - .as_ref() - .and_then(|dimension| match dimension { - Dimension::Linear(value) => { - Some(Vec3::new(value.rotation.cos() as f32, value.rotation.sin() as f32, 0.0)) - } - Dimension::Aligned(value) => Some(Vec3::new( - (value.second_point.x - value.first_point.x) as f32, - (value.second_point.y - value.first_point.y) as f32, - 0.0, - )), - _ => None, - }) - .and_then(Vec3::try_normalize) - .unwrap_or(Vec3::X); - let perpendicular = Vec3::new(-axis.y, axis.x, 0.0); - let center = point.as_vec3(); - let size = 0.3; - let points = vec![ - center - axis * size, - center - axis * size * 0.25 + perpendicular * size, - center + axis * size * 0.25 - perpendicular * size, - center + axis * size, - ]; + let dimension = self.dimension.as_ref()?; + let (segment, normal) = match dimension { + Dimension::Linear(value) => { + let axis = DVec3::new(value.rotation.cos(), value.rotation.sin(), 0.0); + ( + [(point - axis).to_array(), (point + axis).to_array()], + [value.base.normal.x, value.base.normal.y, value.base.normal.z], + ) + } + Dimension::Aligned(value) => ( + [ + [value.first_point.x, value.first_point.y, value.first_point.z], + [value.second_point.x, value.second_point.y, value.second_point.z], + ], + [value.base.normal.x, value.base.normal.y, value.base.normal.z], + ), + _ => return None, + }; + let points = cadkernel::space::dimension_jog_points( + segment, + point.to_array(), + normal, + 0.3, + std::f64::consts::FRAC_PI_2, + )?; let mut preview = WireModel::default(); preview.name = "dimjog_preview".into(); - preview.points = points.into_iter().map(|point| point.to_array()).collect(); + preview.points = points + .into_iter() + .map(|point| point.map(|value| value as f32)) + .collect(); preview.color = WireModel::CYAN; preview.line_weight_px = 1.2; Some(preview) @@ -233,3 +224,22 @@ impl CadCommand for DimJogLineCommand { } inventory::submit!(crate::command::CommandRegistration { names: &["DIMJOGLINE"] }); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn jog_result_keeps_the_selected_point() { + let point = DVec3::new(1.0, 2.0, 3.0); + let CmdResult::EditDimensionJog { + dimension, + point: actual, + } = DimJogLineCommand::result(Handle::from(7), Some(point)) + else { + panic!("expected dimension jog edit"); + }; + assert_eq!(dimension, Handle::from(7)); + assert_eq!(actual, Some(point)); + } +} diff --git a/src/modules/annotate/dimspace.rs b/src/modules/annotate/dimspace.rs index f52a748d9..1377be87c 100644 --- a/src/modules/annotate/dimspace.rs +++ b/src/modules/annotate/dimspace.rs @@ -164,7 +164,7 @@ impl CadCommand for DimSpaceCommand { if let Step::EnterSpacing { base, others } = &self.step { let token = text.trim(); let spacing = if matches!(token.to_ascii_uppercase().as_str(), "A" | "AUTO") { - "AUTO".to_string() + None } else { let Ok(value) = token.parse::() else { return Some(CmdResult::ReportError( @@ -176,24 +176,13 @@ impl CadCommand for DimSpaceCommand { t!("DIMSPACE: spacing value cannot be negative.").into_owned(), )); } - value.to_string() + Some(value) }; - let b = *base; - let o = others.clone(); - // Emit sentinel for commands.rs to handle - use acadrust::entities::XLine; - let mut xl = XLine::default(); - let handles_str: Vec = o.iter().map(|h| h.value().to_string()).collect(); - xl.common.layer = format!( - "__DIMSPACE__{},{},{}", - b.value(), - handles_str.join(";"), - spacing - ); - return Some(CmdResult::ReplaceEntity( - b, - vec![acadrust::EntityType::XLine(xl)], - )); + return Some(CmdResult::SpaceDimensions { + base: *base, + others: others.clone(), + spacing, + }); } None } @@ -211,7 +200,31 @@ impl CadCommand for DimSpaceCommand { CmdResult::Cancel } } +inventory::submit!(crate::command::CommandRegistration { names: &["DIMSPACE", "DSPACE"] }); +#[cfg(test)] +mod tests { + use super::*; -// ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["DIMSPACE", "DSPACE"] }); // DimSpaceCommand + #[test] + fn auto_spacing_returns_a_typed_edit() { + let mut command = DimSpaceCommand { + step: Step::EnterSpacing { + base: Handle::from(2), + others: vec![Handle::from(3)], + }, + picked_entity: None, + }; + let Some(CmdResult::SpaceDimensions { + base, + others, + spacing, + }) = command.on_text_input("Auto") + else { + panic!("expected dimension spacing edit"); + }; + assert_eq!(base, Handle::from(2)); + assert_eq!(others, vec![Handle::from(3)]); + assert_eq!(spacing, None); + } +}