Files
OneCAD-Draft/src/modules/annotate/angular_dim.rs
T
ramox81andClaude Fable 5.1 160930b99a Angular dimension drawing: ray-based three-point frame, text beside the arc
Three-point angular dimensions take their sweep from the two rays and the
arc point (extension lines follow their own rays, straight and reflex
angles draw), dynamic angular dimensions store no text point so DIMTAD
places the text beside an unbroken arc, and dynamic dimensions carry a
DIMGAP override of a quarter text height.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 10:41:52 +03:00

1281 lines
43 KiB
Rust

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<i32>,
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<DVec3>,
},
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<EntityType>,
source_refs: Vec<Option<DimensionAssociationSource>>,
text_override: Option<String>,
awaiting_text: bool,
mtext_override: bool,
text_angle: Option<f64>,
awaiting_angle: bool,
angle_origin: Option<DVec3>,
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<DVec3> {
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<CmdOption> {
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<CmdResult> {
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<WireModel> {
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<Vec<DimensionPreview>> {
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<PickedCurve> {
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<usize> {
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<PickedCurve> {
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<DVec3>, Option<DVec3>) = 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
}
pub(crate) 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<DVec3> {
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<DVec3> {
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<DVec3> {
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
}
/// A two-line angular dimension whose arc sits at `arc_point`, for a
/// dimensional constraint; `None` when the lines are parallel or the arc
/// point picks no sector.
pub(crate) fn angular_two_line_entity(
first_start: DVec3,
first_end: DVec3,
second_start: DVec3,
second_end: DVec3,
arc_point: DVec3,
text: Option<String>,
) -> Option<EntityType> {
two_line_frame(first_start, first_end, second_start, second_end, 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.definition_point;
// No stored text point: the style places the text (DIMTAD) beside the
// arc at render time, as the reference draws a dynamic dimension.
dim.base.insertion_point = dim.dimension_arc;
dim.base.actual_measurement = dim.measurement_degrees();
crate::entities::dimension::set_dimension_text_override(&mut dim.base, text);
Some(EntityType::Dimension(Dimension::Angular2Ln(dim)))
}
/// A three-point angular dimension (vertex, two points, arc point), for a
/// dimensional constraint.
pub(crate) fn angular_three_point_entity(
vertex: DVec3,
first: DVec3,
second: DVec3,
arc_point: DVec3,
text: Option<String>,
) -> Option<EntityType> {
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 = Vector3::new(0.0, 0.0, 0.0);
dim.base.insertion_point = dim.definition_point;
dim.base.actual_measurement = dim.measurement_degrees();
crate::entities::dimension::set_dimension_text_override(&mut dim.base, text);
Some(EntityType::Dimension(Dimension::Angular3Pt(dim)))
}
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<DVec3>) -> 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"] });