Files
OneCAD-Draft/src/modules/annotate/angular_dim.rs
T
Christopher Overbeck acbbb23297 feat(dimension): styled placement previews for creation commands
Show the styled dimension and its value while placing a dimension, using
the same builder the command commits with. Each creation command
(DIMLINEAR, DIMALIGNED, DIMANGULAR, DIMRADIUS, DIMDIAMETER, DIMARC,
DIMJOGGED, DIMORDINATE, DIMCONTINUE, DIMBASELINE, QDIM) reports the
dimensions it would commit at the current stage through
`CadCommand::dimension_preview`. The application applies the current
creation style, or the base dimension's style for DIMCONTINUEMODE=1,
and the same viewport measurement scale as commit, then tessellates the
transient entity through the normal dimension renderer.

While the second definition point is being acquired, a hovered model
snap supplies a provisional viewport scale; once both points are fixed,
hovering another viewport does not change the value. Angular dimensions
never receive length compensation. Degenerate geometry, an invalid
scale, or conflicting viewports fall back to the plain rubber-band
preview so no misleading value is shown.
2026-09-16 13:58:44 -05:00

1235 lines
41 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
}
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
}
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"] });