use acadrust::entities::{Dimension, DimensionAngular2Ln, DimensionAngular3Pt}; use acadrust::types::{Handle, Vector3}; use acadrust::EntityType; use cadkernel::geom2d::{ closest_point, line_line, Arc as KernelArc, BulgeArc, Curve as KernelCurve, Line as KernelLine, }; use crate::command::{ CadCommand, CmdOption, CmdResult, DimensionAssociationInput, DimensionAssociationSource, DimensionPreview, InputKind, WorkingPlane, }; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; use crate::t; use glam::DVec3; pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/dim_angular.svg")); pub fn tool() -> ToolDef { ToolDef { id: "DIMANGULAR", label: "Angular", icon: ICON, event: ModuleEvent::Command("DIMANGULAR".to_string()), } } enum Step { Vertex, FirstRay(DVec3), SecondRay { vertex: DVec3, first: DVec3 }, CircleSecondRay { vertex: DVec3, first: DVec3, source: Handle, }, SecondLine { first_start: DVec3, first_end: DVec3, first_source: Handle, first_refs: [SourceLocator; 2], }, ArcPoint3 { vertex: DVec3, first: DVec3, second: DVec3, }, ArcPoint2 { first_start: DVec3, first_end: DVec3, second_start: DVec3, second_end: DVec3, }, } #[derive(Clone, Copy, Debug, PartialEq)] struct SourceLocator { marker: Option, parameter: f64, } impl SourceLocator { const INFERRED: Self = Self { marker: None, parameter: 0.0 }; const fn explicit(marker: i32, parameter: f64) -> Self { Self { marker: Some(marker), parameter } } fn bind(self, handle: Handle) -> DimensionAssociationSource { match self.marker { Some(marker) => DimensionAssociationSource::explicit(handle, marker, self.parameter), None => DimensionAssociationSource::inferred(handle), } } } enum PickedCurve { Line { start: DVec3, end: DVec3, refs: [SourceLocator; 2], normal: Option, }, Arc { center: DVec3, first: DVec3, second: DVec3, refs: [SourceLocator; 3], normal: DVec3, }, Circle { center: DVec3, normal: DVec3, }, } pub struct AngularDimensionCommand { step: Step, plane: WorkingPlane, selecting_object: bool, picked_entity: Option, source_refs: Vec>, text_override: Option, awaiting_text: bool, mtext_override: bool, text_angle: Option, awaiting_angle: bool, angle_origin: Option, awaiting_quadrant: bool, quadrant_lock: Option<(f64, f64)>, } impl AngularDimensionCommand { pub fn new() -> Self { Self { step: Step::Vertex, plane: WorkingPlane::default(), selecting_object: true, picked_entity: None, source_refs: Vec::new(), text_override: None, awaiting_text: false, mtext_override: false, text_angle: None, awaiting_angle: false, angle_origin: None, awaiting_quadrant: false, quadrant_lock: None, } } 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 picked_text = self.plane.to_local(arc_point); let arc_point = locked_arc_point(vertex, picked_text, self.quadrant_lock); if two_line_frame(vertex, first, vertex, second, arc_point).is_none() { return CmdResult::NeedPoint; } 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; if self.quadrant_lock.is_some_and(|frame| { !point_angle_in_frame(vertex, picked_text, frame) }) { dim.base.text_middle_point = v3(picked_text); dim.base.insertion_point = dim.base.text_middle_point; dim.base.text_user_positioned = true; } 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 picked_text = self.plane.to_local(arc_point); let Some((vertex, _, _)) = two_line_frame( first_start, first_end, second_start, second_end, picked_text, ) else { return CmdResult::NeedPoint; }; let arc_point = locked_arc_point(vertex, picked_text, self.quadrant_lock); if two_line_frame( first_start, first_end, second_start, second_end, arc_point, ) .is_none() { return CmdResult::NeedPoint; } 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.definition_point; dim.base.text_middle_point = dim.dimension_arc; dim.base.insertion_point = dim.dimension_arc; if self.quadrant_lock.is_some_and(|frame| { !point_angle_in_frame(vertex, picked_text, frame) }) { dim.base.text_middle_point = v3(picked_text); dim.base.insertion_point = dim.base.text_middle_point; dim.base.text_user_positioned = true; } 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)); CmdResult::CommitDimension { entity, association: DimensionAssociationInput::Explicit(self.source_refs.clone()), preserve_base_style: false, continue_command: false, } } fn placement_step(&self) -> bool { matches!(self.step, Step::ArcPoint3 { .. } | Step::ArcPoint2 { .. }) } fn editor_anchor(&self) -> DVec3 { match self.step { Step::ArcPoint3 { vertex, first, second } => { let radius = vertex.distance(first).max(vertex.distance(second)).max(1.0); let first_angle = self.plane.angle(vertex, first).unwrap_or(0.0); let second_angle = self.plane.angle(vertex, second).unwrap_or(first_angle); let sweep = (second_angle - first_angle).rem_euclid(std::f64::consts::TAU); let angle = first_angle + sweep * 0.5; self.plane.to_world( self.plane.to_local(vertex) + DVec3::new(angle.cos() * radius, angle.sin() * radius, 0.0), ) } Step::ArcPoint2 { first_start, first_end, second_start, second_end, } => { 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); two_line_frame( first_start, first_end, second_start, second_end, (first_start + second_start) * 0.5, ) .map(|(vertex, start, end)| { self.plane.to_world(vertex + DVec3::new( ((start + end) * 0.5).cos(), ((start + end) * 0.5).sin(), 0.0, )) }) .unwrap_or_else(|| self.plane.to_world(first_start)) } _ => self.plane.origin, } } fn set_quadrant_from_point(&mut self, point: DVec3) -> bool { let point = self.plane.to_local(point); let frame = match self.step { Step::ArcPoint3 { vertex, first, second } => two_line_frame( self.plane.to_local(vertex), self.plane.to_local(first), self.plane.to_local(vertex), self.plane.to_local(second), point, ), Step::ArcPoint2 { first_start, first_end, second_start, second_end, } => two_line_frame( self.plane.to_local(first_start), self.plane.to_local(first_end), self.plane.to_local(second_start), self.plane.to_local(second_end), point, ), _ => None, }; if let Some((_, start, end)) = frame { self.quadrant_lock = Some((start, end)); true } else { false } } } impl CadCommand for AngularDimensionCommand { fn set_working_plane(&mut self, plane: WorkingPlane) { self.plane = plane; } fn name(&self) -> &'static str { "DIMANGULAR" } fn prompt(&self) -> String { if self.awaiting_quadrant { return t!("DIMANGULAR Specify quadrant:").into_owned(); } 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 if self.angle_origin.is_some() { t!("DIMANGULAR Specify second point for text angle:").into_owned() } else { t!("DIMANGULAR Specify text angle or first point:").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 specify an angle vertex:" ) .into_owned(), }; } match self.step { Step::Vertex => t!( "DIMANGULAR Specify angle vertex:" ) .into_owned(), Step::FirstRay(_) => { t!("DIMANGULAR Specify first extension line point:").into_owned() } Step::SecondRay { .. } | Step::CircleSecondRay { .. } => { t!("DIMANGULAR Specify second extension line point:").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, point: DVec3) -> CmdResult { if self.awaiting_quadrant { if self.set_quadrant_from_point(point) { self.awaiting_quadrant = false; } return CmdResult::NeedPoint; } if self.awaiting_angle { if let Some(origin) = self.angle_origin { if let Some(angle) = self.plane.angle(origin, point) { self.text_angle = Some(angle); self.awaiting_angle = false; self.angle_origin = None; } } else { self.angle_origin = Some(point); } return CmdResult::NeedPoint; } if self.selecting_object && matches!(self.step, Step::Vertex) { self.selecting_object = false; self.step = Step::FirstRay(point); return CmdResult::NeedPoint; } match self.step { Step::Vertex => { self.step = Step::FirstRay(point); CmdResult::NeedPoint } Step::FirstRay(vertex) => { if point.distance_squared(vertex) <= 1.0e-24 { return CmdResult::NeedPoint; } self.step = Step::SecondRay { vertex, first: point }; CmdResult::NeedPoint } Step::SecondRay { vertex, first } => { if point.distance_squared(vertex) <= 1.0e-24 { return CmdResult::NeedPoint; } let first_local = self.plane.vector_to_local(first - vertex); let second_local = self.plane.vector_to_local(point - vertex); if line_line( [0.0, 0.0], [first_local.x, first_local.y], [0.0, 0.0], [second_local.x, second_local.y], ) .is_none() { return CmdResult::NeedPoint; } self.step = Step::ArcPoint3 { vertex, first, second: point }; CmdResult::NeedPoint } Step::CircleSecondRay { vertex, first, source } => { if point.distance_squared(vertex) <= 1.0e-24 { return CmdResult::NeedPoint; } self.source_refs = vec![ Some(DimensionAssociationSource::inferred(source)), Some(DimensionAssociationSource::inferred(source)), None, ]; self.step = Step::ArcPoint3 { vertex, first, second: point }; 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_quadrant { self.awaiting_quadrant = false; return CmdResult::NeedPoint; } if self.awaiting_text { self.awaiting_text = false; return CmdResult::NeedPoint; } if self.awaiting_angle { self.awaiting_angle = false; self.angle_origin = None; return CmdResult::NeedPoint; } if matches!(self.step, Step::Vertex) { self.selecting_object = !self.selecting_object; return CmdResult::NeedPoint; } CmdResult::Cancel } /// Points and object picks may come through a paper-space viewport; /// the committed dimension then reports the model measurement. fn measures_through_viewports(&self) -> bool { true } fn dimension_acquired_points(&self) -> Vec { match self.step { Step::Vertex => vec![], Step::FirstRay(p) => vec![p], Step::SecondRay { vertex, first } | Step::CircleSecondRay { vertex, first, .. } => { vec![vertex, first] } Step::SecondLine { first_start, first_end, .. } => vec![first_start, first_end], Step::ArcPoint3 { vertex, first, second, } => vec![vertex, first, second], Step::ArcPoint2 { first_start, first_end, second_start, second_end, } => vec![first_start, first_end, second_start, second_end], } } fn dimension_placement_pending(&self) -> bool { matches!(self.step, Step::ArcPoint3 { .. } | Step::ArcPoint2 { .. }) } fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } fn input_kind(&self) -> InputKind { if self.awaiting_text { InputKind::FreeText } else if self.awaiting_angle || self.awaiting_quadrant { InputKind::SingleToken } else { InputKind::Point } } fn options(&self) -> Vec { if self.placement_step() && !self.awaiting_text && !self.awaiting_angle && !self.awaiting_quadrant { vec![ CmdOption::new("MText", "MTEXT"), CmdOption::new("Text", "TEXT"), CmdOption::new("Angle", "ANGLE"), CmdOption::new("Quadrant", "QUADRANT"), ] } else { Vec::new() } } fn point_step_accepts_keywords(&self) -> bool { self.placement_step() && !self.awaiting_text && !self.awaiting_angle && !self.awaiting_quadrant } 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; self.angle_origin = None; 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; Some(CmdResult::SuspendForMTextInput { pos: self.editor_anchor(), initial: self.text_override.clone().unwrap_or_default(), height: 2.5, }) } "A" | "ANGLE" => { self.awaiting_angle = true; self.angle_origin = None; Some(CmdResult::NeedPoint) } "Q" | "QUADRANT" => { self.awaiting_quadrant = true; Some(CmdResult::NeedPoint) } _ => None, } } fn on_editor_text(&mut self, value: String) { let value = value.trim(); self.text_override = if value.is_empty() || value == "<>" { None } else { Some(value.to_string()) }; } fn on_editor_closed(&mut self, _committed: bool) -> CmdResult { CmdResult::NeedPoint } fn needs_entity_pick(&self) -> bool { self.selecting_object } fn entity_pick_accepts_points(&self) -> bool { self.selecting_object && matches!(self.step, Step::Vertex) } 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() { if matches!(self.step, Step::Vertex) { self.selecting_object = false; self.step = Step::FirstRay(point); } 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; }; if !curve_is_coplanar(&curve, self.plane) { return CmdResult::NeedPoint; } match (&self.step, curve) { ( Step::SecondLine { first_start, first_end, first_source, first_refs, }, PickedCurve::Line { start: second_start, end: second_end, refs: second_refs, .. }, ) => { if (*first_source == handle && *first_refs == second_refs) || lines_parallel_in_plane( self.plane, *first_start, *first_end, second_start, second_end, ) { return CmdResult::NeedPoint; } self.source_refs = vec![ Some(first_refs[0].bind(*first_source)), Some(first_refs[1].bind(*first_source)), Some(second_refs[0].bind(handle)), Some(second_refs[1].bind(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 { start: first_start, end: first_end, refs, .. }, ) => { self.step = Step::SecondLine { first_start, first_end, first_source: handle, first_refs: refs, }; CmdResult::NeedPoint } ( Step::Vertex, PickedCurve::Arc { center, first, second, refs, .. }, ) => { self.source_refs = refs .into_iter() .map(|reference| Some(reference.bind(handle))) .collect(); self.selecting_object = false; self.step = Step::ArcPoint3 { vertex: center, first, second }; CmdResult::NeedPoint } (Step::Vertex, PickedCurve::Circle { center, .. }) => { if point.distance_squared(center) <= 1.0e-24 { return CmdResult::NeedPoint; } self.selecting_object = false; self.step = Step::CircleSecondRay { vertex: center, first: point, source: handle, }; CmdResult::NeedPoint } _ => CmdResult::NeedPoint, } } fn on_mouse_move(&mut self, point: DVec3) -> Option { if self.awaiting_angle { return self .angle_origin .map(|origin| preview_wire(vec![origin, point])); } 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, .. } => { vec![vertex, first, nan(), vertex, point] } Step::ArcPoint3 { 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 point = locked_arc_point( vertex, self.plane.to_local(point), self.quadrant_lock, ); angular_preview(vertex, first, second, point) .into_iter() .map(|value| if value.is_nan() { value } else { self.plane.to_world(value) }) .collect() } Step::ArcPoint2 { first_start, first_end, second_start, second_end, } => { 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 point = self.plane.to_local(point); let point = two_line_frame( first_start, first_end, second_start, second_end, point, ) .map(|(vertex, _, _)| locked_arc_point(vertex, point, self.quadrant_lock)) .unwrap_or(point); two_line_preview( first_start, first_end, second_start, second_end, point, ) .into_iter() .map(|value| if value.is_nan() { value } else { self.plane.to_world(value) }) .collect() } }; Some(preview_wire(points)) } fn dimension_preview(&self, cursor: DVec3) -> Option> { if self.selecting_object { return None; } let result = match self.step { Step::ArcPoint3 { vertex, first, second, } => self.finish_three_point(vertex, first, second, cursor), Step::ArcPoint2 { first_start, first_end, second_start, second_end, } => self.finish_two_line(first_start, first_end, second_start, second_end, cursor), _ => return None, }; match result { CmdResult::CommitDimension { entity, .. } => { Some(vec![DimensionPreview::current_style(entity)]) } _ => Some(Vec::new()), } } } fn picked_curve(entity: &EntityType, click: DVec3) -> Option { let point = |value: Vector3| DVec3::new(value.x, value.y, value.z); let normal = |value: Vector3| DVec3::new(value.x, value.y, value.z); match entity { EntityType::Line(line) => { let start = point(line.start); let end = point(line.end); (start.distance_squared(end) > 1.0e-24).then_some(PickedCurve::Line { start, end, refs: [SourceLocator::INFERRED; 2], normal: None, }) } EntityType::Arc(arc) => Some(PickedCurve::Arc { center: point(arc.center_wcs()), first: point(arc.start_point_wcs()), second: point(arc.end_point_wcs()), refs: [SourceLocator::INFERRED; 3], normal: normal(arc.normal), }), EntityType::Circle(circle) => Some(PickedCurve::Circle { center: point(circle.center_wcs()), normal: normal(circle.normal), }), 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 points: Vec<_> = polyline .vertices .iter() .map(|vertex| [vertex.location.x, vertex.location.y]) .collect(); let bulges: Vec<_> = polyline.vertices.iter().map(|vertex| vertex.bulge).collect(); let segment = nearest_segment_index( &points, &bulges, 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, segment, polyline.vertices.len(), ) } EntityType::Polyline2D(polyline) => { let vertices = &polyline.vertices; 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 points: Vec<_> = vertices .iter() .map(|vertex| [vertex.location.x, vertex.location.y]) .collect(); let bulges: Vec<_> = vertices.iter().map(|vertex| vertex.bulge).collect(); let segment = nearest_segment_index( &points, &bulges, 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, segment, vertices.len(), ) } _ => None, } } fn nearest_segment_index( points: &[[f64; 2]], bulges: &[f64], 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| { polyline_segment_distance_squared( points[*first], points[(*first + 1) % points.len()], bulges.get(*first).copied().unwrap_or(0.0), click, ) .total_cmp(&polyline_segment_distance_squared( points[*second], points[(*second + 1) % points.len()], bulges.get(*second).copied().unwrap_or(0.0), click, )) }) } fn polyline_segment_curve(first: [f64; 2], second: [f64; 2], bulge: f64) -> KernelCurve { if let Some(arc) = BulgeArc::from_bulge(first, second, bulge) { let (start_angle, end_angle) = if arc.sweep >= 0.0 { (arc.start_angle, arc.end_angle) } else { (arc.end_angle, arc.start_angle) }; KernelCurve::Arc(KernelArc { centre: arc.center, radius: arc.radius, start_angle, end_angle, }) } else { KernelCurve::Line(KernelLine { start: first, end: second }) } } fn polyline_segment_distance_squared( first: [f64; 2], second: [f64; 2], bulge: f64, point: [f64; 2], ) -> f64 { closest_point(&polyline_segment_curve(first, second, bulge), point) .distance .powi(2) } fn polyline_segment( first: [f64; 2], second: [f64; 2], bulge: f64, elevation: f64, normal: Vector3, segment: usize, vertex_count: usize, ) -> 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 first_world.distance_squared(second_world) <= 1.0e-24 { return None; } let refs = [ SourceLocator::explicit(segment as i32, 0.0), SourceLocator::explicit(((segment + 1) % vertex_count) as i32, 0.0), ]; let normal_world = DVec3::new(normal.x, normal.y, normal.z); if bulge.abs() <= 1.0e-12 { return Some(PickedCurve::Line { start: first_world, end: second_world, refs, normal: Some(normal_world), }); } let center = BulgeArc::from_bulge(first, second, bulge)?.center; Some(PickedCurve::Arc { center: to_world(center), first: first_world, second: second_world, refs: [ SourceLocator::explicit(-4, segment as f64), refs[0], refs[1], ], normal: normal_world, }) } fn curve_is_coplanar(curve: &PickedCurve, plane: WorkingPlane) -> bool { let (points, normal): (Vec, Option) = match curve { PickedCurve::Line { start, end, normal, .. } => (vec![*start, *end], *normal), PickedCurve::Arc { center, first, second, normal, .. } => { (vec![*center, *first, *second], Some(*normal)) } PickedCurve::Circle { center, normal } => (vec![*center], Some(*normal)), }; if let Some(normal) = normal { let alignment = normal.normalize_or_zero().dot(plane.z).abs(); if alignment < 1.0 - 1.0e-7 { return false; } } let local: Vec<_> = points.into_iter().map(|point| plane.to_local(point)).collect(); let scale = local .iter() .map(|point| point.x.abs().max(point.y.abs()).max(1.0)) .fold(1.0, f64::max); local.iter().all(|point| point.z.abs() <= 1.0e-9 * scale) } fn lines_parallel_in_plane( plane: WorkingPlane, first_start: DVec3, first_end: DVec3, second_start: DVec3, second_end: DVec3, ) -> bool { let first_start = plane.to_local(first_start); let first_end = plane.to_local(first_end); let second_start = plane.to_local(second_start); let second_end = plane.to_local(second_end); let first = first_end - first_start; let second = second_end - second_start; line_line( [first_start.x, first_start.y], [first.x, first.y], [second_start.x, second_start.y], [second.x, second.y], ) .is_none() } fn locked_arc_point( vertex: DVec3, point: DVec3, frame: Option<(f64, f64)>, ) -> DVec3 { let Some((start, end)) = frame else { return point }; let radius = ((point.x - vertex.x).powi(2) + (point.y - vertex.y).powi(2)).sqrt(); if radius <= 1.0e-12 { return point; } let angle = (start + end) * 0.5; DVec3::new( vertex.x + angle.cos() * radius, vertex.y + angle.sin() * radius, vertex.z, ) } fn point_angle_in_frame(vertex: DVec3, point: DVec3, frame: (f64, f64)) -> bool { let angle = (point.y - vertex.y).atan2(point.x - vertex.x); let sweep = frame.1 - frame.0; (angle - frame.0).rem_euclid(std::f64::consts::TAU) <= sweep + 1.0e-9 } 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 (t, _) = line_line( [first_start.x, first_start.y], [first_direction.x, first_direction.y], [second_start.x, second_start.y], [second_direction.x, second_direction.y], )?; 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()]; points.extend( cadkernel::geom2d::tessellate::arc( [vertex.x, vertex.y], radius, start, end, vertex.z, cadkernel::geom2d::tessellate::DEFAULT_SEGMENTS_PER_RADIAN, ) .into_iter() .map(DVec3::from_array), ); 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 { bg_adapt: None, point_marker: None, taper_widths: Vec::new(), pattern_stations: Vec::new(), world_width: 0.0, depth_override: None, display_visible: true, plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), name: "dimangular_preview".to_string(), 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, pattern_length: 0.0, pattern: [0.0; 8], line_weight_px: 1.0, snap_pts: vec![], tangent_geoms: vec![], aci: 0, key_vertices: vec![], aabb: WireModel::UNBOUNDED_AABB, plinegen: true, fill_tris: vec![], fill_tris_low: Vec::new(), } } inventory::submit!(crate::command::CommandRegistration { names: &["DIMANGULAR"] });