From e93815d0c52329c3a04dbfa675c75d2a2759b230 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Tue, 25 Aug 2026 11:29:38 +0300 Subject: [PATCH] Complete angular dimension creation and associations --- src/app/command_driver.rs | 21 +- src/command.rs | 4 +- src/modules/annotate/angular_dim.rs | 691 +++++++++++++++++++++++----- src/modules/annotate/linear_dim.rs | 5 +- src/scene/dimension_assoc.rs | 171 +++++-- 5 files changed, 744 insertions(+), 148 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index bf03b591f..1bff0d720 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -1229,17 +1229,24 @@ impl OpenCADStudio { self.commit_undo_delta(i, pd); } } - CmdResult::CommitAssociativeDimension { entity, source } => { - let label = self.history_label_from_active_cmd(i, "DIMLINEAR"); + CmdResult::CommitAssociativeDimension { entity, sources } => { + let label = self.history_label_from_active_cmd(i, "DIMENSION"); let pending = self.begin_undo(i, label, 1, false); if let Some(handle) = self.commit_entity_handle(entity) { self.tabs[i] .scene - .attach_linear_dimension_association(handle, [Some(source), Some(source)]); - self.tabs[i].scene.bump_entities(&[ - (handle, crate::scene::ChangeKind::Modified), - (source, crate::scene::ChangeKind::Modified), - ]); + .attach_dimension_association( + handle, + sources.iter().copied().map(Some).collect(), + ); + let mut changed = vec![(handle, crate::scene::ChangeKind::Modified)]; + changed.extend( + sources + .iter() + .copied() + .map(|source| (source, crate::scene::ChangeKind::Modified)), + ); + self.tabs[i].scene.bump_entities(&changed); } self.tabs[i].dirty = true; self.tabs[i].scene.clear_preview_wire(); diff --git a/src/command.rs b/src/command.rs index 89b8d0382..b8dbdb6fc 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1212,10 +1212,10 @@ pub enum CmdResult { CommitEntitiesAndExit(Vec), /// Commit an acadrust entity to the document and end the command. CommitAndExit(EntityType), - /// Commit an object-selected linear dimension and retain its source link. + /// Commit an object-selected dimension and retain its source links. CommitAssociativeDimension { entity: EntityType, - source: Handle, + sources: Vec, }, /// Commit a Model-tab 3D solid: the acadrust entity (for selection / /// persistence) plus its B-rep (cached for boolean ops + shaded diff --git a/src/modules/annotate/angular_dim.rs b/src/modules/annotate/angular_dim.rs index 36a917e45..d6e9edf97 100644 --- a/src/modules/annotate/angular_dim.rs +++ b/src/modules/annotate/angular_dim.rs @@ -1,12 +1,12 @@ -use acadrust::entities::{Dimension, DimensionAngular3Pt}; -use acadrust::types::Vector3; +use acadrust::entities::{Dimension, DimensionAngular2Ln, DimensionAngular3Pt}; +use acadrust::types::{Handle, Vector3}; use acadrust::EntityType; use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; -use glam::{DVec3, Vec3}; use crate::t; +use glam::DVec3; pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/dim_angular.svg")); @@ -22,20 +22,55 @@ pub fn tool() -> ToolDef { enum Step { Vertex, FirstRay(DVec3), - SecondRay { + SecondRay { vertex: DVec3, first: DVec3 }, + CircleSecondRay { vertex: DVec3, first: DVec3, + radius: f64, + source: Handle, }, - ArcPoint { + SecondLine { + first_start: DVec3, + first_end: DVec3, + first_source: Handle, + }, + ArcPoint3 { vertex: DVec3, first: DVec3, second: DVec3, }, + ArcPoint2 { + first_start: DVec3, + first_end: DVec3, + second_start: DVec3, + second_end: DVec3, + }, +} + +enum PickedCurve { + Line(DVec3, DVec3), + Arc { + center: DVec3, + first: DVec3, + second: DVec3, + }, + Circle { + center: DVec3, + radius: f64, + }, } pub struct AngularDimensionCommand { step: Step, plane: WorkingPlane, + selecting_object: bool, + picked_entity: Option, + source_handles: Vec, + text_override: Option, + awaiting_text: bool, + mtext_override: bool, + text_angle: Option, + awaiting_angle: bool, } impl AngularDimensionCommand { @@ -43,8 +78,92 @@ impl AngularDimensionCommand { Self { step: Step::Vertex, plane: WorkingPlane::default(), + selecting_object: false, + picked_entity: None, + source_handles: Vec::new(), + text_override: None, + awaiting_text: false, + mtext_override: false, + text_angle: None, + awaiting_angle: false, } } + + fn finish_three_point( + &self, + vertex: DVec3, + first: DVec3, + second: DVec3, + arc_point: DVec3, + ) -> CmdResult { + let vertex = self.plane.to_local(vertex); + let first = self.plane.to_local(first); + let second = self.plane.to_local(second); + let arc_point = self.plane.to_local(arc_point); + let mut dim = DimensionAngular3Pt::new(v3(vertex), v3(first), v3(second)); + dim.definition_point = v3(arc_point); + dim.base.definition_point = dim.definition_point; + dim.base.text_middle_point = dim.definition_point; + dim.base.insertion_point = dim.definition_point; + dim.base.actual_measurement = dim.measurement_degrees(); + crate::entities::dimension::set_dimension_text_override( + &mut dim.base, + self.text_override.clone(), + ); + if let Some(angle) = self.text_angle { + dim.base.text_rotation = angle; + } + self.commit(Dimension::Angular3Pt(dim)) + } + + fn finish_two_line( + &self, + first_start: DVec3, + first_end: DVec3, + second_start: DVec3, + second_end: DVec3, + arc_point: DVec3, + ) -> CmdResult { + let first_start = self.plane.to_local(first_start); + let first_end = self.plane.to_local(first_end); + let second_start = self.plane.to_local(second_start); + let second_end = self.plane.to_local(second_end); + let arc_point = self.plane.to_local(arc_point); + let mut dim = DimensionAngular2Ln::default(); + dim.first_point = v3(first_start); + dim.second_point = v3(first_end); + dim.angle_vertex = v3(second_start); + dim.definition_point = v3(second_end); + dim.dimension_arc = v3(arc_point); + dim.base.definition_point = dim.dimension_arc; + dim.base.text_middle_point = dim.dimension_arc; + dim.base.insertion_point = dim.dimension_arc; + dim.base.actual_measurement = dim.measurement_degrees(); + crate::entities::dimension::set_dimension_text_override( + &mut dim.base, + self.text_override.clone(), + ); + if let Some(angle) = self.text_angle { + dim.base.text_rotation = angle; + } + self.commit(Dimension::Angular2Ln(dim)) + } + + fn commit(&self, dimension: Dimension) -> CmdResult { + let entity = self.plane.place_entity(EntityType::Dimension(dimension)); + if self.source_handles.is_empty() { + CmdResult::CommitAndExit(entity) + } else { + CmdResult::CommitAssociativeDimension { + entity, + sources: self.source_handles.clone(), + } + } + } + + fn placement_step(&self) -> bool { + matches!(self.step, Step::ArcPoint3 { .. } | Step::ArcPoint2 { .. }) + } } impl CadCommand for AngularDimensionCommand { @@ -57,59 +176,102 @@ impl CadCommand for AngularDimensionCommand { } fn prompt(&self) -> String { + if self.awaiting_text { + return if self.mtext_override { + t!("DIMANGULAR Enter formatted dimension text (blank = measured value):") + .into_owned() + } else { + t!("DIMANGULAR Enter dimension text (blank = measured value):").into_owned() + }; + } + if self.awaiting_angle { + return t!("DIMANGULAR Specify text angle (degrees):").into_owned(); + } + if self.selecting_object { + return match self.step { + Step::SecondLine { .. } => t!("DIMANGULAR Select second line:").into_owned(), + _ => t!("DIMANGULAR Select arc, circle, line, or polyline arc:").into_owned(), + }; + } match self.step { - Step::Vertex => t!("DIMANGULAR Specify angle vertex:").into_owned(), + Step::Vertex => t!( + "DIMANGULAR Specify angle vertex or press Enter to select an object:" + ) + .into_owned(), Step::FirstRay(_) => { t!("DIMANGULAR Specify first extension line point:").into_owned() } - Step::SecondRay { .. } => { + Step::SecondRay { .. } | Step::CircleSecondRay { .. } => { t!("DIMANGULAR Specify second extension line point:").into_owned() } - Step::ArcPoint { .. } => t!("DIMANGULAR Specify dimension arc location:").into_owned(), + Step::SecondLine { .. } => t!("DIMANGULAR Select second line:").into_owned(), + Step::ArcPoint3 { .. } | Step::ArcPoint2 { .. } => t!( + "DIMANGULAR Specify dimension arc location [Mtext/Text/Angle/Quadrant]:" + ) + .into_owned(), } } - fn on_point(&mut self, pt: DVec3) -> CmdResult { + fn on_point(&mut self, point: DVec3) -> CmdResult { match self.step { Step::Vertex => { - self.step = Step::FirstRay(pt); + self.step = Step::FirstRay(point); CmdResult::NeedPoint } Step::FirstRay(vertex) => { - self.step = Step::SecondRay { vertex, first: pt }; + if point.distance_squared(vertex) <= 1.0e-24 { + return CmdResult::NeedPoint; + } + self.step = Step::SecondRay { vertex, first: point }; CmdResult::NeedPoint } Step::SecondRay { vertex, first } => { - self.step = Step::ArcPoint { - vertex, - first, - second: pt, - }; + if point.distance_squared(vertex) <= 1.0e-24 { + return CmdResult::NeedPoint; + } + self.step = Step::ArcPoint3 { vertex, first, second: point }; CmdResult::NeedPoint } - Step::ArcPoint { - vertex, - first, - second, - } => { - let vertex = self.plane.to_local(vertex); - let first = self.plane.to_local(first); - let second = self.plane.to_local(second); - let pt = self.plane.to_local(pt); - let mut dim = DimensionAngular3Pt::new(v3(vertex), v3(first), v3(second)); - dim.definition_point = v3(pt); - dim.base.definition_point = v3(pt); - dim.base.text_middle_point = v3(pt); - dim.base.insertion_point = v3(pt); - dim.base.actual_measurement = dim.measurement_degrees(); - CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Dimension( - Dimension::Angular3Pt(dim), - ))) + Step::CircleSecondRay { vertex, first, radius, source } => { + let Some(second) = project_to_radius(vertex, point, radius) else { + return CmdResult::NeedPoint; + }; + self.source_handles = vec![source, source, source]; + self.step = Step::ArcPoint3 { vertex, first, second }; + CmdResult::NeedPoint } + Step::SecondLine { .. } => CmdResult::NeedPoint, + Step::ArcPoint3 { vertex, first, second } => { + self.finish_three_point(vertex, first, second, point) + } + Step::ArcPoint2 { + first_start, + first_end, + second_start, + second_end, + } => self.finish_two_line( + first_start, + first_end, + second_start, + second_end, + point, + ), } } fn on_enter(&mut self) -> CmdResult { + if self.awaiting_text { + self.awaiting_text = false; + return CmdResult::NeedPoint; + } + if self.awaiting_angle { + self.awaiting_angle = false; + return CmdResult::NeedPoint; + } + if matches!(self.step, Step::Vertex) { + self.selecting_object = true; + return CmdResult::NeedPoint; + } CmdResult::Cancel } @@ -117,49 +279,392 @@ impl CadCommand for AngularDimensionCommand { CmdResult::Cancel } - fn on_mouse_move(&mut self, pt: DVec3) -> Option { - let pt = pt.as_vec3(); - match self.step { - Step::Vertex => None, - Step::FirstRay(vertex) => Some(preview_wire(vec![vertex.as_vec3(), pt])), - Step::SecondRay { vertex, first } => Some(preview_wire(vec![ - vertex.as_vec3(), - first.as_vec3(), - Vec3::new(f32::NAN, f32::NAN, f32::NAN), - vertex.as_vec3(), - pt, - ])), - Step::ArcPoint { - vertex, - first, - second, - } => { - let points = angular_preview( - self.plane.to_local(vertex).as_vec3(), - self.plane.to_local(first).as_vec3(), - self.plane.to_local(second).as_vec3(), - self.plane.to_local(pt.as_dvec3()).as_vec3(), - ) - .into_iter() - .map(|point| { - if point.x.is_nan() { - point - } else { - self.plane.to_world(point.as_dvec3()).as_vec3() - } - }) - .collect(); - Some(preview_wire(points)) - } + fn wants_text_input(&self) -> bool { + true + } + + fn wants_text_with_spaces(&self) -> bool { + self.awaiting_text + } + + fn point_step_accepts_keywords(&self) -> bool { + self.placement_step() && !self.awaiting_text && !self.awaiting_angle + } + + fn on_text_input(&mut self, text: &str) -> Option { + if self.awaiting_text { + let value = text.trim(); + self.text_override = if value.is_empty() || value == "<>" { + None + } else { + Some(value.to_string()) + }; + self.awaiting_text = false; + return Some(CmdResult::NeedPoint); } + if self.awaiting_angle { + self.text_angle = if text.trim().is_empty() { + None + } else { + crate::entities::common::parse_typed_angle(text.trim()) + }; + self.awaiting_angle = false; + return Some(CmdResult::NeedPoint); + } + if !self.placement_step() { + return None; + } + match text.trim().to_ascii_uppercase().as_str() { + "T" | "TEXT" => { + self.mtext_override = false; + self.awaiting_text = true; + Some(CmdResult::NeedPoint) + } + "M" | "MTEXT" => { + self.mtext_override = true; + self.awaiting_text = true; + Some(CmdResult::NeedPoint) + } + "A" | "ANGLE" => { + self.awaiting_angle = true; + Some(CmdResult::NeedPoint) + } + "Q" | "QUADRANT" => Some(CmdResult::NeedPoint), + _ => None, + } + } + + fn needs_entity_pick(&self) -> bool { + self.selecting_object + } + + fn entity_pick_highlights_hover(&self) -> bool { + true + } + + fn inject_before_entity_pick(&self) -> bool { + true + } + + fn inject_picked_entity(&mut self, entity: EntityType) { + self.picked_entity = Some(entity); + } + + fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult { + if handle.is_null() { + return CmdResult::NeedPoint; + } + let Some(entity) = self.picked_entity.take() else { + return CmdResult::NeedPoint; + }; + let Some(curve) = picked_curve(&entity, point) else { + return CmdResult::NeedPoint; + }; + + match (&self.step, curve) { + ( + Step::SecondLine { first_start, first_end, first_source }, + PickedCurve::Line(second_start, second_end), + ) if *first_source != handle => { + self.source_handles = vec![*first_source, *first_source, handle, handle]; + self.selecting_object = false; + self.step = Step::ArcPoint2 { + first_start: *first_start, + first_end: *first_end, + second_start, + second_end, + }; + CmdResult::NeedPoint + } + (Step::Vertex, PickedCurve::Line(first_start, first_end)) => { + self.step = Step::SecondLine { + first_start, + first_end, + first_source: handle, + }; + CmdResult::NeedPoint + } + (Step::Vertex, PickedCurve::Arc { center, first, second }) => { + self.source_handles = vec![handle, handle, handle]; + self.selecting_object = false; + self.step = Step::ArcPoint3 { vertex: center, first, second }; + CmdResult::NeedPoint + } + (Step::Vertex, PickedCurve::Circle { center, radius }) => { + let Some(first) = project_to_radius(center, point, radius) else { + return CmdResult::NeedPoint; + }; + self.selecting_object = false; + self.step = Step::CircleSecondRay { + vertex: center, + first, + radius, + source: handle, + }; + CmdResult::NeedPoint + } + _ => CmdResult::NeedPoint, + } + } + + fn on_mouse_move(&mut self, point: DVec3) -> Option { + let points = match self.step { + Step::Vertex | Step::SecondLine { .. } => return None, + Step::FirstRay(vertex) => vec![vertex, point], + Step::SecondRay { vertex, first } => vec![vertex, first, nan(), vertex, point], + Step::CircleSecondRay { vertex, first, radius, .. } => { + let second = project_to_radius(vertex, point, radius).unwrap_or(point); + vec![vertex, first, nan(), vertex, second] + } + Step::ArcPoint3 { vertex, first, second } => { + angular_preview(vertex, first, second, point) + } + Step::ArcPoint2 { + first_start, + first_end, + second_start, + second_end, + } => two_line_preview( + first_start, + first_end, + second_start, + second_end, + point, + ), + }; + Some(preview_wire(points)) } } -fn v3(pt: DVec3) -> Vector3 { - Vector3::new(pt.x, pt.y, pt.z) +fn picked_curve(entity: &EntityType, click: DVec3) -> Option { + let point = |value: Vector3| DVec3::new(value.x, value.y, value.z); + match entity { + EntityType::Line(line) => Some(PickedCurve::Line(point(line.start), point(line.end))), + EntityType::Arc(arc) => Some(PickedCurve::Arc { + center: point(arc.center_wcs()), + first: point(arc.start_point_wcs()), + second: point(arc.end_point_wcs()), + }), + EntityType::Circle(circle) => Some(PickedCurve::Circle { + center: point(circle.center_wcs()), + radius: circle.radius, + }), + EntityType::LwPolyline(polyline) => { + let click = crate::scene::view::transform::wcs_point_to_ocs( + (click.x, click.y, click.z), + (polyline.normal.x, polyline.normal.y, polyline.normal.z), + ); + let segment = nearest_segment_index( + polyline.vertices.iter().map(|vertex| [vertex.location.x, vertex.location.y]).collect(), + polyline.is_closed, + [click.0, click.1], + )?; + let first = polyline.vertices[segment]; + let second = polyline.vertices[(segment + 1) % polyline.vertices.len()]; + polyline_segment( + [first.location.x, first.location.y], + [second.location.x, second.location.y], + first.bulge, + polyline.elevation, + polyline.normal, + ) + } + EntityType::Polyline2D(polyline) => { + let vertices = crate::entities::polyline::drawn_vertices2d(polyline) + .unwrap_or_else(|| polyline.vertices.clone()); + let click = crate::scene::view::transform::wcs_point_to_ocs( + (click.x, click.y, click.z), + (polyline.normal.x, polyline.normal.y, polyline.normal.z), + ); + let segment = nearest_segment_index( + vertices.iter().map(|vertex| [vertex.location.x, vertex.location.y]).collect(), + polyline.is_closed(), + [click.0, click.1], + )?; + let first = &vertices[segment]; + let second = &vertices[(segment + 1) % vertices.len()]; + polyline_segment( + [first.location.x, first.location.y], + [second.location.x, second.location.y], + first.bulge, + polyline.elevation, + polyline.normal, + ) + } + _ => None, + } } -fn preview_wire(points: Vec) -> WireModel { +fn nearest_segment_index(points: Vec<[f64; 2]>, closed: bool, click: [f64; 2]) -> Option { + if points.len() < 2 { + return None; + } + let count = if closed { points.len() } else { points.len() - 1 }; + (0..count).min_by(|first, second| { + segment_distance_squared(points[*first], points[(*first + 1) % points.len()], click) + .total_cmp(&segment_distance_squared( + points[*second], + points[(*second + 1) % points.len()], + click, + )) + }) +} + +fn segment_distance_squared(first: [f64; 2], second: [f64; 2], point: [f64; 2]) -> f64 { + let dx = second[0] - first[0]; + let dy = second[1] - first[1]; + let length_squared = dx * dx + dy * dy; + if length_squared <= 1.0e-24 { + return (point[0] - first[0]).powi(2) + (point[1] - first[1]).powi(2); + } + let t = (((point[0] - first[0]) * dx + (point[1] - first[1]) * dy) / length_squared) + .clamp(0.0, 1.0); + let x = first[0] + dx * t; + let y = first[1] + dy * t; + (point[0] - x).powi(2) + (point[1] - y).powi(2) +} + +fn polyline_segment( + first: [f64; 2], + second: [f64; 2], + bulge: f64, + elevation: f64, + normal: Vector3, +) -> Option { + let to_world = |point: [f64; 2]| { + let value = crate::scene::view::transform::ocs_point_to_wcs( + (point[0], point[1], elevation), + (normal.x, normal.y, normal.z), + ); + DVec3::new(value.0, value.1, value.2) + }; + let first_world = to_world(first); + let second_world = to_world(second); + if bulge.abs() <= 1.0e-12 { + return Some(PickedCurve::Line(first_world, second_world)); + } + let dx = second[0] - first[0]; + let dy = second[1] - first[1]; + let chord = (dx * dx + dy * dy).sqrt(); + if chord <= 1.0e-12 { + return None; + } + let midpoint = [(first[0] + second[0]) * 0.5, (first[1] + second[1]) * 0.5]; + let center_offset = chord * (1.0 - bulge * bulge) / (4.0 * bulge); + let center = [ + midpoint[0] - dy / chord * center_offset, + midpoint[1] + dx / chord * center_offset, + ]; + Some(PickedCurve::Arc { + center: to_world(center), + first: first_world, + second: second_world, + }) +} + +fn project_to_radius(center: DVec3, point: DVec3, radius: f64) -> Option { + let direction = (point - center).normalize_or_zero(); + (direction.length_squared() > 0.0).then_some(center + direction * radius) +} + +fn two_line_frame( + first_start: DVec3, + first_end: DVec3, + second_start: DVec3, + second_end: DVec3, + arc_point: DVec3, +) -> Option<(DVec3, f64, f64)> { + let first_direction = first_end - first_start; + let second_direction = second_end - second_start; + let denominator = first_direction.x * second_direction.y - first_direction.y * second_direction.x; + if denominator.abs() + <= 1.0e-12 * first_direction.length().max(second_direction.length()).max(1.0) + { + return None; + } + let offset = second_start - first_start; + let t = (offset.x * second_direction.y - offset.y * second_direction.x) / denominator; + let vertex = first_start + first_direction * t; + let target = (arc_point.y - vertex.y).atan2(arc_point.x - vertex.x); + let mut best: Option<(f64, f64, f64)> = None; + for first_sign in [1.0, -1.0] { + for second_sign in [1.0, -1.0] { + let first = first_direction * first_sign; + let second = second_direction * second_sign; + for (start, end) in [ + (first.y.atan2(first.x), second.y.atan2(second.x)), + (second.y.atan2(second.x), first.y.atan2(first.x)), + ] { + let sweep = (end - start).rem_euclid(std::f64::consts::TAU); + if sweep <= 1.0e-9 || sweep > std::f64::consts::PI { + continue; + } + let selected = (target - start).rem_euclid(std::f64::consts::TAU); + if selected <= sweep + 1.0e-9 && best.is_none_or(|known| sweep < known.2) { + best = Some((start, start + sweep, sweep)); + } + } + } + } + best.map(|(start, end, _)| (vertex, start, end)) +} + +fn angular_preview(vertex: DVec3, first: DVec3, second: DVec3, arc_point: DVec3) -> Vec { + let Some((_, start, end)) = two_line_frame(vertex, first, vertex, second, arc_point) else { + return vec![vertex, first, nan(), vertex, second]; + }; + angular_preview_with_frame(vertex, first, second, arc_point, start, end) +} + +fn two_line_preview( + first_start: DVec3, + first_end: DVec3, + second_start: DVec3, + second_end: DVec3, + arc_point: DVec3, +) -> Vec { + let Some((vertex, start, end)) = two_line_frame( + first_start, + first_end, + second_start, + second_end, + arc_point, + ) else { + return vec![first_start, first_end, nan(), second_start, second_end]; + }; + angular_preview_with_frame(vertex, first_start, second_start, arc_point, start, end) +} + +fn angular_preview_with_frame( + vertex: DVec3, + first: DVec3, + second: DVec3, + arc_point: DVec3, + start: f64, + end: f64, +) -> Vec { + let radius = vertex.distance(arc_point); + let first_end = vertex + DVec3::new(start.cos(), start.sin(), 0.0) * radius; + let second_end = vertex + DVec3::new(end.cos(), end.sin(), 0.0) * radius; + let mut points = vec![first, first_end, nan(), second, second_end, nan()]; + let sweep = end - start; + let steps = ((sweep.abs() / std::f64::consts::PI * 90.0).ceil() as usize).clamp(8, 720); + for index in 0..=steps { + let angle = start + sweep * index as f64 / steps as f64; + points.push(vertex + DVec3::new(angle.cos() * radius, angle.sin() * radius, 0.0)); + } + points +} + +fn v3(point: DVec3) -> Vector3 { + Vector3::new(point.x, point.y, point.z) +} + +fn nan() -> DVec3 { + DVec3::splat(f64::NAN) +} + +fn preview_wire(points: Vec) -> WireModel { WireModel { point_marker: None, taper_widths: Vec::new(), @@ -173,11 +678,11 @@ fn preview_wire(points: Vec) -> WireModel { render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), - dash_from_start: false, - dash_align_end: None, - text_verts: Vec::new(), + dash_from_start: false, + dash_align_end: None, + text_verts: Vec::new(), name: "dimangular_preview".to_string(), - points: points.into_iter().map(|p| [p.x, p.y, p.z]).collect(), + points: points.into_iter().map(|point| [point.x as f32, point.y as f32, point.z as f32]).collect(), points_low: Vec::new(), color: WireModel::CYAN, selected: false, @@ -195,38 +700,4 @@ fn preview_wire(points: Vec) -> WireModel { } } -fn angular_preview(vertex: Vec3, first: Vec3, second: Vec3, arc_pt: Vec3) -> Vec { - let mut points = vec![ - vertex, - first, - Vec3::new(f32::NAN, f32::NAN, f32::NAN), - vertex, - second, - Vec3::new(f32::NAN, f32::NAN, f32::NAN), - ]; - let r = vertex.distance(arc_pt); - if r <= 1e-6 { - return points; - } - let a0 = (first.y - vertex.y).atan2(first.x - vertex.x); - let mut a1 = (second.y - vertex.y).atan2(second.x - vertex.x); - let mut delta = a1 - a0; - while delta <= 0.0 { - delta += std::f32::consts::TAU; - } - if delta > std::f32::consts::PI { - a1 -= std::f32::consts::TAU; - delta = a1 - a0; - } - let steps = 24; - for i in 0..=steps { - let t = i as f32 / steps as f32; - let a = a0 + delta * t; - points.push(vertex + Vec3::new(a.cos() * r, a.sin() * r, 0.0)); - } - points -} - - -// ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["DIMANGULAR"] }); // AngularDimensionCommand +inventory::submit!(crate::command::CommandRegistration { names: &["DIMANGULAR"] }); diff --git a/src/modules/annotate/linear_dim.rs b/src/modules/annotate/linear_dim.rs index a3768011c..4f0458441 100644 --- a/src/modules/annotate/linear_dim.rs +++ b/src/modules/annotate/linear_dim.rs @@ -194,7 +194,10 @@ impl CadCommand for LinearDimensionCommand { Dimension::Linear(dim), )); if let Some(source) = self.source_handle { - CmdResult::CommitAssociativeDimension { entity, source } + CmdResult::CommitAssociativeDimension { + entity, + sources: vec![source, source], + } } else { CmdResult::CommitAndExit(entity) } diff --git a/src/scene/dimension_assoc.rs b/src/scene/dimension_assoc.rs index bda8f168e..853d40092 100644 --- a/src/scene/dimension_assoc.rs +++ b/src/scene/dimension_assoc.rs @@ -27,6 +27,7 @@ fn source_points(entity: &EntityType) -> Vec { match entity { EntityType::Line(line) => vec![line.start, line.end], EntityType::Arc(arc) => vec![arc.start_point_wcs(), arc.end_point_wcs()], + EntityType::Circle(_) => Vec::new(), EntityType::LwPolyline(polyline) => polyline .vertices .iter() @@ -55,6 +56,37 @@ fn source_points(entity: &EntityType) -> Vec { } } +fn source_reference(entity: &EntityType, point: Vector3) -> Option<(i32, f64)> { + let curve_parameter = match entity { + EntityType::Circle(circle) => { + let center = circle.center_wcs(); + if point_distance_squared(center, point) <= 1e-16 { + return Some((-3, 0.0)); + } + let (axis_x, axis_y) = circle.axes_wcs(); + let offset = point - center; + Some(offset.dot(&axis_y).atan2(offset.dot(&axis_x))) + } + EntityType::Arc(arc) => { + let local = crate::scene::view::transform::wcs_point_to_ocs( + (point.x, point.y, point.z), + (arc.normal.x, arc.normal.y, arc.normal.z), + ); + let dx = local.0 - arc.center.x; + let dy = local.1 - arc.center.y; + if dx * dx + dy * dy <= 1e-16 { + return Some((-3, 0.0)); + } + Some(dy.atan2(dx)) + } + _ => None, + }; + if let Some(parameter) = curve_parameter { + return Some((-2, parameter)); + } + source_marker(entity, point).map(|marker| (marker, 0.0)) +} + fn source_marker(entity: &EntityType, point: Vector3) -> Option { source_points(entity) .into_iter() @@ -69,11 +101,45 @@ fn source_marker(entity: &EntityType, point: Vector3) -> Option { fn resolve_reference(scene: &Scene, reference: &AssocDimensionReference) -> Option { let source = *reference.xrefs.first()?; let entity = scene.document.get_entity(source)?; + if reference.main_gs_marker == -3 { + return match entity { + EntityType::Circle(circle) => Some(circle.center_wcs()), + EntityType::Arc(arc) => Some(arc.center_wcs()), + _ => None, + }; + } + if reference.main_gs_marker == -2 { + return match entity { + EntityType::Circle(circle) => { + Some(circle.point_at_angle_wcs(reference.osnap_distance)) + } + EntityType::Arc(arc) => Some(arc.point_at_angle_wcs(reference.osnap_distance)), + _ => None, + }; + } source_points(entity) .get(reference.main_gs_marker.max(0) as usize) .copied() } +fn dimension_reference_points(dimension: &Dimension) -> Vec { + match dimension { + Dimension::Linear(linear) => vec![linear.first_point, linear.second_point], + Dimension::Angular3Pt(angular) => vec![ + angular.angle_vertex, + angular.first_point, + angular.second_point, + ], + Dimension::Angular2Ln(angular) => vec![ + angular.first_point, + angular.second_point, + angular.angle_vertex, + angular.definition_point, + ], + _ => Vec::new(), + } +} + pub(crate) fn dimension_is_associative( document: &acadrust::CadDocument, dimension: Handle, @@ -97,34 +163,41 @@ pub(crate) fn dimension_is_associative( } impl Scene { - pub(crate) fn attach_linear_dimension_association( + pub(crate) fn attach_dimension_association( &mut self, dimension: Handle, - sources: [Option; 2], + sources: Vec>, ) { - let Some(EntityType::Dimension(Dimension::Linear(linear))) = + let Some(EntityType::Dimension(dimension_entity)) = self.document.get_entity(dimension) else { return; }; - let first_point = linear.first_point; - let second_point = linear.second_point; - let source_data = [first_point, second_point].map(|point| point); - let resolved: [Option<(Handle, i32)>; 2] = std::array::from_fn(|index| { - let source = sources[index]?; + let source_data = dimension_reference_points(dimension_entity); + if source_data.is_empty() { + return; + } + let resolved: Vec> = source_data + .iter() + .enumerate() + .map(|(index, point)| { + let source = sources.get(index).copied().flatten()?; let entity = self.document.get_entity(source)?; - source_marker(entity, source_data[index]).map(|marker| (source, marker)) - }); + source_reference(entity, *point) + .map(|(marker, parameter)| (source, marker, parameter)) + }) + .collect(); if resolved.iter().all(Option::is_none) { return; } - let reference = |source: Handle, marker: i32, point: Vector3| AssocDimensionReference { + let reference = |source: Handle, marker: i32, parameter: f64, point: Vector3| AssocDimensionReference { class_name: "AcDbOsnapPointRef".to_string(), osnap_type: 1, xrefs: vec![source], main_subent_type: 1, main_gs_marker: marker, + osnap_distance: parameter, osnap_point: point, ..AssocDimensionReference::default() }; @@ -132,9 +205,12 @@ impl Scene { std::array::from_fn(|_| Vec::new()); let mut associativity = 0; for (index, resolved) in resolved.into_iter().enumerate() { - if let Some((source, marker)) = resolved { + if index >= references.len() { + break; + } + if let Some((source, marker, parameter)) = resolved { associativity |= 1 << index; - references[index].push(reference(source, marker, source_data[index])); + references[index].push(reference(source, marker, parameter, source_data[index])); } } @@ -165,6 +241,14 @@ impl Scene { } } + pub(crate) fn attach_linear_dimension_association( + &mut self, + dimension: Handle, + sources: [Option; 2], + ) { + self.attach_dimension_association(dimension, sources.into_iter().collect()); + } + pub(crate) fn infer_linear_dimension_sources( &self, dimension: Handle, @@ -222,26 +306,57 @@ impl Scene { let mut refreshed = Vec::new(); for association in associations { - let first = association.references[0] - .first() - .and_then(|reference| resolve_reference(self, reference)); - let second = association.references[1] - .first() - .and_then(|reference| resolve_reference(self, reference)); - if first.is_none() && second.is_none() { + let resolved: [Option; 4] = std::array::from_fn(|index| { + association.references[index] + .first() + .and_then(|reference| resolve_reference(self, reference)) + }); + if resolved.iter().all(Option::is_none) { continue; } - if let Some(EntityType::Dimension(Dimension::Linear(linear))) = + if let Some(EntityType::Dimension(dimension)) = self.document.get_entity_mut(association.dimension) { - if let Some(first) = first { - linear.first_point = first; + match dimension { + Dimension::Linear(linear) => { + if let Some(point) = resolved[0] { + linear.first_point = point; + } + if let Some(point) = resolved[1] { + linear.second_point = point; + } + linear.base.definition_point = linear.definition_point; + } + Dimension::Angular3Pt(angular) => { + if let Some(point) = resolved[0] { + angular.angle_vertex = point; + } + if let Some(point) = resolved[1] { + angular.first_point = point; + } + if let Some(point) = resolved[2] { + angular.second_point = point; + } + angular.base.definition_point = angular.definition_point; + } + Dimension::Angular2Ln(angular) => { + if let Some(point) = resolved[0] { + angular.first_point = point; + } + if let Some(point) = resolved[1] { + angular.second_point = point; + } + if let Some(point) = resolved[2] { + angular.angle_vertex = point; + } + if let Some(point) = resolved[3] { + angular.definition_point = point; + } + angular.base.definition_point = angular.dimension_arc; + } + _ => continue, } - if let Some(second) = second { - linear.second_point = second; - } - linear.base.actual_measurement = linear.measurement(); - linear.base.definition_point = linear.definition_point; + dimension.base_mut().actual_measurement = dimension.measurement(); refreshed.push((association.dimension, ChangeKind::Modified)); } }