From f556a2b027e188a3265e745a301017847c439ba0 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 24 Aug 2026 12:10:29 +0300 Subject: [PATCH] Complete the linear dimension workflow --- src/modules/annotate/linear_dim.rs | 228 +++++++++++++++++++++++++++-- 1 file changed, 215 insertions(+), 13 deletions(-) diff --git a/src/modules/annotate/linear_dim.rs b/src/modules/annotate/linear_dim.rs index f2af91e0a..4429b9925 100644 --- a/src/modules/annotate/linear_dim.rs +++ b/src/modules/annotate/linear_dim.rs @@ -57,6 +57,14 @@ enum Step { DimensionLine { first: DVec3, second: DVec3 }, } +#[derive(Clone, Copy)] +enum AxisMode { + Automatic, + Horizontal, + Vertical, + Rotated(f64), +} + pub struct LinearDimensionCommand { step: Step, plane: WorkingPlane, @@ -68,6 +76,13 @@ pub struct LinearDimensionCommand { text_angle: Option, /// True while the next typed value is captured as the text angle. awaiting_angle: bool, + /// True while a rotation value for the Rotated option is being entered. + awaiting_rotation: bool, + axis_mode: AxisMode, + selecting_object: bool, + picked_entity: Option, + source_handle: Option, + mtext_override: bool, } impl LinearDimensionCommand { @@ -79,6 +94,12 @@ impl LinearDimensionCommand { awaiting_text: false, text_angle: None, awaiting_angle: false, + awaiting_rotation: false, + axis_mode: AxisMode::Automatic, + selecting_object: false, + picked_entity: None, + source_handle: None, + mtext_override: false, } } } @@ -94,18 +115,32 @@ impl CadCommand for LinearDimensionCommand { fn prompt(&self) -> String { if self.awaiting_text { - return t!("DIMLINEAR Enter dimension text (blank = measured value):").into_owned(); + return if self.mtext_override { + t!("DIMLINEAR Enter formatted dimension text (blank = measured value):") + .into_owned() + } else { + t!("DIMLINEAR Enter dimension text (blank = measured value):").into_owned() + }; } if self.awaiting_angle { return t!("DIMLINEAR Specify text angle (degrees):").into_owned(); } + if self.awaiting_rotation { + return t!("DIMLINEAR Specify dimension line angle (degrees):").into_owned(); + } + if self.selecting_object { + return t!("DIMLINEAR Select object to dimension:").into_owned(); + } match self.step { - Step::FirstPoint => t!("DIMLINEAR Specify first extension line origin:").into_owned(), + Step::FirstPoint => { + t!("DIMLINEAR Specify first extension line origin or press Enter to select object:") + .into_owned() + } Step::SecondPoint(_) => { - t!("DIMLINEAR Specify second extension line origin [Text/Angle]:").into_owned() + t!("DIMLINEAR Specify second extension line origin:").into_owned() } Step::DimensionLine { .. } => { - t!("DIMLINEAR Specify dimension line location [Text/Angle]:").into_owned() + t!("DIMLINEAR Specify dimension line location [Mtext/Text/Angle/Horizontal/Vertical/Rotated]:").into_owned() } } } @@ -117,6 +152,9 @@ impl CadCommand for LinearDimensionCommand { CmdResult::NeedPoint } Step::SecondPoint(first) => { + if pt.distance_squared(first) <= 1e-24 { + return CmdResult::NeedPoint; + } self.step = Step::DimensionLine { first, second: pt }; CmdResult::NeedPoint } @@ -125,21 +163,31 @@ impl CadCommand for LinearDimensionCommand { let second = self.plane.to_local(second); let pt = self.plane.to_local(pt); let mut dim = DimensionLinear::new(v3(first), v3(second)); - let axis = measure_axis(first, second, pt); + let axis = match self.axis_mode { + AxisMode::Automatic => measure_axis(first, second, pt), + AxisMode::Horizontal => DVec3::X, + AxisMode::Vertical => DVec3::Y, + AxisMode::Rotated(angle) => DVec3::new(angle.cos(), angle.sin(), 0.0), + }; dim.rotation = axis.y.atan2(axis.x); - dim.definition_point = v3(pt); - dim.base.definition_point = v3(pt); + dim.set_offset(dimension_line_offset(second, pt, axis)); + dim.base.definition_point = dim.definition_point; dim.base.text_middle_point = v3(linear_text_pos(first, second, pt, axis)); dim.base.insertion_point = dim.base.text_middle_point; dim.base.actual_measurement = dim.measurement(); - dim.base.user_text = self.text_override.clone(); + dim.base.set_text_override(self.text_override.clone()); // An explicit text angle overrides the UCS-derived rotation. if let Some(a) = self.text_angle { dim.base.text_rotation = a; } - CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Dimension( + let entity = self.plane.place_entity(EntityType::Dimension( Dimension::Linear(dim), - ))) + )); + if let Some(source) = self.source_handle { + CmdResult::CommitAssociativeDimension { entity, source } + } else { + CmdResult::CommitAndExit(entity) + } } } } @@ -154,6 +202,14 @@ impl CadCommand for LinearDimensionCommand { self.awaiting_angle = false; return CmdResult::NeedPoint; } + if self.awaiting_rotation { + self.awaiting_rotation = false; + return CmdResult::NeedPoint; + } + if matches!(self.step, Step::FirstPoint) { + self.selecting_object = true; + return CmdResult::NeedPoint; + } CmdResult::Cancel } @@ -168,7 +224,7 @@ impl CadCommand for LinearDimensionCommand { fn point_step_accepts_keywords(&self) -> bool { // While typing the override text or angle, route input as a value, not // a point pick / keyword. - !self.awaiting_text && !self.awaiting_angle + !self.awaiting_text && !self.awaiting_angle && !self.awaiting_rotation } fn wants_text_with_spaces(&self) -> bool { @@ -199,8 +255,24 @@ impl CadCommand for LinearDimensionCommand { self.awaiting_angle = false; return Some(CmdResult::NeedPoint); } + if self.awaiting_rotation { + if let Some(angle) = crate::entities::common::parse_typed_angle(text.trim()) { + self.axis_mode = AxisMode::Rotated(angle); + } + self.awaiting_rotation = false; + return Some(CmdResult::NeedPoint); + } + if !matches!(self.step, Step::DimensionLine { .. }) { + return None; + } match text.trim().to_uppercase().as_str() { - "T" | "TEXT" | "M" | "MTEXT" => { + "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) } @@ -208,10 +280,54 @@ impl CadCommand for LinearDimensionCommand { self.awaiting_angle = true; Some(CmdResult::NeedPoint) } + "H" | "HORIZONTAL" => { + self.axis_mode = AxisMode::Horizontal; + Some(CmdResult::NeedPoint) + } + "V" | "VERTICAL" => { + self.axis_mode = AxisMode::Vertical; + Some(CmdResult::NeedPoint) + } + "R" | "ROTATED" => { + self.awaiting_rotation = true; + 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: acadrust::Handle, point: DVec3) -> CmdResult { + let Some(entity) = self.picked_entity.take() else { + return CmdResult::NeedPoint; + }; + let Some((first, second)) = dimension_source_points(&entity, point) else { + return CmdResult::NeedPoint; + }; + if first.distance_squared(second) <= 1e-24 { + return CmdResult::NeedPoint; + } + self.source_handle = Some(handle); + self.selecting_object = false; + self.step = Step::DimensionLine { first, second }; + CmdResult::NeedPoint + } + fn on_mouse_move(&mut self, pt: DVec3) -> Option { match self.step { Step::FirstPoint => None, @@ -235,6 +351,76 @@ fn v3(pt: DVec3) -> Vector3 { Vector3::new(pt.x, pt.y, pt.z) } +fn dimension_line_offset(second: DVec3, point: DVec3, axis: DVec3) -> f64 { + let perpendicular = DVec3::new(-axis.y, axis.x, 0.0); + (point - second).dot(perpendicular) +} + +fn dimension_source_points(entity: &EntityType, click: DVec3) -> Option<(DVec3, DVec3)> { + let point = |p: Vector3| DVec3::new(p.x, p.y, p.z); + match entity { + EntityType::Line(line) => Some((point(line.start), point(line.end))), + EntityType::Arc(arc) => Some((point(arc.start_point()), point(arc.end_point()))), + EntityType::LwPolyline(polyline) => { + nearest_lw_segment(polyline, click) + } + EntityType::Polyline2D(polyline) => { + let vertices = crate::entities::polyline::drawn_vertices2d(polyline) + .unwrap_or_else(|| polyline.vertices.clone()); + nearest_segment( + vertices.iter().map(|vertex| { + DVec3::new(vertex.location.x, vertex.location.y, vertex.location.z) + }), + polyline.is_closed(), + click, + ) + } + _ => None, + } +} + +fn nearest_lw_segment( + polyline: &acadrust::entities::LwPolyline, + click: DVec3, +) -> Option<(DVec3, DVec3)> { + nearest_segment( + polyline.vertices.iter().map(|vertex| { + DVec3::new(vertex.location.x, vertex.location.y, polyline.elevation) + }), + polyline.is_closed, + click, + ) +} + +fn nearest_segment( + points: impl IntoIterator, + closed: bool, + click: DVec3, +) -> Option<(DVec3, DVec3)> { + let points: Vec<_> = points.into_iter().collect(); + if points.len() < 2 { + return None; + } + let count = if closed { points.len() } else { points.len() - 1 }; + (0..count) + .map(|index| { + let first = points[index]; + let second = points[(index + 1) % points.len()]; + let direction = second - first; + let length_squared = direction.length_squared(); + let t = if length_squared <= 1e-24 { + 0.0 + } else { + (click - first).dot(direction) / length_squared + } + .clamp(0.0, 1.0); + let distance = click.distance_squared(first + direction * t); + (distance, first, second) + }) + .min_by(|a, b| a.0.total_cmp(&b.0)) + .map(|(_, first, second)| (first, second)) +} + fn preview_wire(points: Vec) -> WireModel { WireModel { point_marker: None, @@ -289,7 +475,23 @@ fn dim_line_endpoints(first: DVec3, second: DVec3, def: DVec3, axis: DVec3) -> ( fn linear_dimension_preview(first: DVec3, second: DVec3, def: DVec3, axis: DVec3) -> Vec { let (d1, d2) = dim_line_endpoints(first, second, def, axis); let nan = DVec3::new(f64::NAN, f64::NAN, f64::NAN); - vec![first, d1, nan, second, d2, nan, d1, d2] + let arrow = 0.22; + let perp = DVec3::new(-axis.y, axis.x, 0.0); + let text = linear_text_pos(first, second, def, axis); + let half_width = ((second - first).length().log10().max(0.0) + 1.0) * 0.18; + let half_height = 0.16; + vec![ + first, d1, nan, second, d2, nan, d1, d2, nan, + d1, d1 + axis * arrow + perp * arrow * 0.45, nan, + d1, d1 + axis * arrow - perp * arrow * 0.45, nan, + d2, d2 - axis * arrow + perp * arrow * 0.45, nan, + d2, d2 - axis * arrow - perp * arrow * 0.45, nan, + text - axis * half_width - perp * half_height, + text + axis * half_width - perp * half_height, + text + axis * half_width + perp * half_height, + text - axis * half_width + perp * half_height, + text - axis * half_width - perp * half_height, + ] } fn linear_text_pos(first: DVec3, second: DVec3, def: DVec3, axis: DVec3) -> DVec3 {