diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 94cee99e8..a5dd08d24 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -481,12 +481,12 @@ impl OpenCADStudio { if layer.starts_with("__DIMBREAK__") || layer.starts_with("__DIMBREAK_AUTO__") { - // DIMBREAK is a geometry operation we approximate by - // recording a note on the dimension; full intersection logic - // requires render geometry. For now, just undo-snapshot and log. - self.push_undo_snapshot(i, "DIMBREAK"); - self.command_line.push_output("DIMBREAK Break applied."); - self.tabs[i].dirty = true; + // DIMBREAK needs a break-gap field on the dimension + // model (not yet present) to store and render the gap. + // Report honestly rather than claiming success while + // changing nothing. (#181 / DIM-020) + self.command_line + .push_info("DIMBREAK: not yet implemented — nothing changed."); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; return Task::none(); @@ -503,10 +503,11 @@ impl OpenCADStudio { return Task::none(); } if layer.starts_with("__DIMJOG__") { - // Record jog position — visual rendering handled by scene. - self.push_undo_snapshot(i, "DIMJOGLINE"); - self.command_line.push_output("DIMJOGLINE Jog added."); - self.tabs[i].dirty = true; + // DIMJOGLINE needs a jog-point field on the dimension + // model (not yet present) to store and render the jog. + // Report honestly rather than faking success. (DIM-019) + self.command_line + .push_info("DIMJOGLINE: not yet implemented — nothing changed."); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; return Task::none(); @@ -2177,24 +2178,58 @@ fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) { .collect(); let spacing: f64 = parts[2].parse().unwrap_or(0.0); + use acadrust::entities::Dimension; let base_h = acadrust::Handle::from(base_val); - // Read base dimension's definition_point Z (perpendicular offset) - let base_z = if let Some(acadrust::EntityType::Dimension(d)) = scene.document.get_entity(base_h) - { - d.base().definition_point.z - } else { - return; + // 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) = match scene.document.get_entity(base_h) { + Some(acadrust::EntityType::Dimension(Dimension::Linear(d))) => { + let (s, c) = d.rotation.sin_cos(); + let perp = (-s, c); + let dp = d.definition_point; + (perp, dp.x * perp.0 + dp.y * perp.1) + } + Some(acadrust::EntityType::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; + (perp, dp.x * perp.0 + dp.y * perp.1) + } + _ => return, }; let effective_spacing = if spacing <= 0.0 { 10.0 } else { spacing }; for (idx, &hv) in other_vals.iter().enumerate() { let h = acadrust::Handle::from(hv); - let new_z = base_z + effective_spacing * (idx + 1) as f64; - if let Some(acadrust::EntityType::Dimension(d)) = scene.document.get_entity_mut(h) { - let dp = &mut d.base_mut().definition_point; - dp.z = new_z; + let target = base_coord + effective_spacing * (idx + 1) as f64; + 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; + }; + match dim { + Dimension::Linear(d) => { + slide(&mut d.definition_point); + d.base.definition_point = d.definition_point; + } + Dimension::Aligned(d) => { + slide(&mut d.definition_point); + d.base.definition_point = d.definition_point; + } + _ => {} + } } } + scene.bump_geometry(); } // ── MLEADERALIGN helper ─────────────────────────────────────────────────────── diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs index 2fad206a5..8b1c6200c 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -116,6 +116,88 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(cmd)); } + // QDIM second stage: the front-end relaunched with the picked + // dimension-line point and the gathered geometry now selected. Build + // a continuous chain of linear dimensions across the endpoints. + cmd if cmd.starts_with("QDIM_PLACE ") => { + let nums: Vec = cmd + .split_whitespace() + .skip(1) + .filter_map(|s| s.parse().ok()) + .collect(); + if nums.len() < 3 { + return Some(Task::none()); + } + let place = glam::Vec3::new(nums[0], nums[1], nums[2]); + let handles: Vec = self.tabs[i] + .scene + .selected_entities() + .iter() + .map(|(h, _)| *h) + .collect(); + let mut pts: Vec = Vec::new(); + for h in &handles { + if let Some(e) = self.tabs[i].scene.document.get_entity(*h) { + qdim_collect_points(e, &mut pts); + } + } + if pts.len() < 2 { + self.command_line + .push_error("QDIM: no dimensionable endpoints in the selection."); + return Some(Task::none()); + } + // Choose the dimension axis from the points' spread: a wider X + // span dimensions horizontally (ordered by X), else vertically. + let (minx, maxx) = pts.iter().fold((f32::MAX, f32::MIN), |(a, b), p| { + (a.min(p.x), b.max(p.x)) + }); + let (miny, maxy) = pts.iter().fold((f32::MAX, f32::MIN), |(a, b), p| { + (a.min(p.y), b.max(p.y)) + }); + let horizontal = (maxx - minx) >= (maxy - miny); + if horizontal { + pts.sort_by(|a, b| a.x.total_cmp(&b.x)); + pts.dedup_by(|a, b| (a.x - b.x).abs() < 1e-4); + } else { + pts.sort_by(|a, b| a.y.total_cmp(&b.y)); + pts.dedup_by(|a, b| (a.y - b.y).abs() < 1e-4); + } + if pts.len() < 2 { + self.command_line + .push_error("QDIM: endpoints collapse to a single position."); + return Some(Task::none()); + } + self.push_undo_snapshot(i, "QDIM"); + let v = |p: glam::Vec3| acadrust::types::Vector3::new(p.x as f64, p.y as f64, p.z as f64); + let mut made = 0usize; + for w in pts.windows(2) { + let (p1, p2) = (w[0], w[1]); + let mut dim = acadrust::entities::DimensionLinear::new(v(p1), v(p2)); + dim.rotation = if horizontal { + 0.0 + } else { + std::f64::consts::FRAC_PI_2 + }; + // Dim line passes through the picked perpendicular position. + let def = if horizontal { + glam::Vec3::new((p1.x + p2.x) * 0.5, place.y, 0.0) + } else { + glam::Vec3::new(place.x, (p1.y + p2.y) * 0.5, 0.0) + }; + dim.definition_point = v(def); + dim.base.definition_point = v(def); + dim.base.actual_measurement = dim.measurement(); + self.commit_entity(acadrust::EntityType::Dimension( + acadrust::entities::Dimension::Linear(dim), + )); + made += 1; + } + self.tabs[i].dirty = true; + self.command_line + .push_output(&format!("QDIM {made} dimensions created.")); + return Some(Task::none()); + } + "DIMEDIT" | "DED" => { use crate::modules::annotate::dimedit::DimEditCommand; let cmd = DimEditCommand::new(); @@ -565,3 +647,40 @@ fn find_last_linear_dim( } result } + +/// Collect candidate dimension endpoints from an entity for QDIM — line ends, +/// polyline vertices, arc endpoints — in world space. +fn qdim_collect_points(e: &acadrust::EntityType, out: &mut Vec) { + use acadrust::EntityType as ET; + let p = |v: &acadrust::types::Vector3| glam::Vec3::new(v.x as f32, v.y as f32, v.z as f32); + match e { + ET::Line(l) => { + out.push(p(&l.start)); + out.push(p(&l.end)); + } + ET::LwPolyline(pl) => { + for v in &pl.vertices { + out.push(glam::Vec3::new( + v.location.x as f32, + v.location.y as f32, + pl.elevation as f32, + )); + } + } + ET::Polyline(pl) => { + for v in &pl.vertices { + out.push(p(&v.location)); + } + } + ET::Arc(a) => { + for &ang in &[a.start_angle, a.end_angle] { + out.push(glam::Vec3::new( + (a.center.x + a.radius * ang.cos()) as f32, + (a.center.y + a.radius * ang.sin()) as f32, + a.center.z as f32, + )); + } + } + _ => {} + } +}