Complete angular dimension interaction and associations

This commit is contained in:
ramox81 committed 2026-08-25 12:23:21 +03:00
1 parent a8f05716cf
commit 24b344b2b2
7 files changed
+777 -75

No files matched your search

+549 -71
View File
@@ -2,7 +2,9 @@ use acadrust::entities::{Dimension, DimensionAngular2Ln, DimensionAngular3Pt};
use acadrust::types::{Handle, Vector3};
use acadrust::EntityType;
use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::command::{
CadCommand, CmdOption, CmdResult, DimensionAssociationSource, WorkingPlane,
};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use crate::t;
@@ -26,13 +28,13 @@ enum Step {
CircleSecondRay {
vertex: DVec3,
first: DVec3,
radius: f64,
source: Handle,
},
SecondLine {
first_start: DVec3,
first_end: DVec3,
first_source: Handle,
first_refs: [SourceLocator; 2],
},
ArcPoint3 {
vertex: DVec3,
@@ -47,16 +49,44 @@ enum Step {
},
}
#[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(DVec3, DVec3),
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,
radius: f64,
normal: DVec3,
},
}
@@ -65,12 +95,15 @@ pub struct AngularDimensionCommand {
plane: WorkingPlane,
selecting_object: bool,
picked_entity: Option<EntityType>,
source_handles: Vec<Handle>,
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 {
@@ -78,14 +111,17 @@ impl AngularDimensionCommand {
Self {
step: Step::Vertex,
plane: WorkingPlane::default(),
selecting_object: false,
selecting_object: true,
picked_entity: None,
source_handles: Vec::new(),
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,
}
}
@@ -99,12 +135,23 @@ impl AngularDimensionCommand {
let vertex = self.plane.to_local(vertex);
let first = self.plane.to_local(first);
let second = self.plane.to_local(second);
let arc_point = self.plane.to_local(arc_point);
let 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,
@@ -128,7 +175,28 @@ impl AngularDimensionCommand {
let first_end = self.plane.to_local(first_end);
let second_start = self.plane.to_local(second_start);
let second_end = self.plane.to_local(second_end);
let arc_point = self.plane.to_local(arc_point);
let 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);
@@ -138,6 +206,13 @@ impl AngularDimensionCommand {
dim.base.definition_point = dim.dimension_arc;
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,
@@ -151,12 +226,12 @@ impl AngularDimensionCommand {
fn commit(&self, dimension: Dimension) -> CmdResult {
let entity = self.plane.place_entity(EntityType::Dimension(dimension));
if self.source_handles.is_empty() {
if self.source_refs.iter().all(Option::is_none) {
CmdResult::CommitAndExit(entity)
} else {
CmdResult::CommitAssociativeDimension {
CmdResult::CommitAssociativeDimensionDetailed {
entity,
sources: self.source_handles.clone(),
sources: self.source_refs.clone(),
}
}
}
@@ -164,6 +239,81 @@ impl AngularDimensionCommand {
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 {
@@ -176,6 +326,9 @@ impl CadCommand for AngularDimensionCommand {
}
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):")
@@ -185,17 +338,24 @@ impl CadCommand for AngularDimensionCommand {
};
}
if self.awaiting_angle {
return t!("DIMANGULAR Specify text angle (degrees):").into_owned();
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 polyline arc:").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 or press Enter to select an object:"
"DIMANGULAR Specify angle vertex:"
)
.into_owned(),
Step::FirstRay(_) => {
@@ -213,6 +373,29 @@ impl CadCommand for AngularDimensionCommand {
}
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);
@@ -229,15 +412,26 @@ impl CadCommand for AngularDimensionCommand {
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 (first_local.x * second_local.y - first_local.y * second_local.x).abs()
<= 1.0e-12 * first_local.length().max(second_local.length()).max(1.0)
{
return CmdResult::NeedPoint;
}
self.step = Step::ArcPoint3 { vertex, first, second: point };
CmdResult::NeedPoint
}
Step::CircleSecondRay { vertex, first, radius, source } => {
let Some(second) = project_to_radius(vertex, point, radius) else {
Step::CircleSecondRay { vertex, first, source } => {
if point.distance_squared(vertex) <= 1.0e-24 {
return CmdResult::NeedPoint;
};
self.source_handles = vec![source, source, source];
self.step = Step::ArcPoint3 { vertex, first, second };
}
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,
@@ -260,16 +454,21 @@ impl CadCommand for AngularDimensionCommand {
}
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 = true;
self.selecting_object = !self.selecting_object;
return CmdResult::NeedPoint;
}
CmdResult::Cancel
@@ -287,8 +486,24 @@ impl CadCommand for AngularDimensionCommand {
self.awaiting_text
}
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.placement_step()
&& !self.awaiting_text
&& !self.awaiting_angle
&& !self.awaiting_quadrant
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
@@ -309,6 +524,7 @@ impl CadCommand for AngularDimensionCommand {
crate::entities::common::parse_typed_angle(text.trim())
};
self.awaiting_angle = false;
self.angle_origin = None;
return Some(CmdResult::NeedPoint);
}
if !self.placement_step() {
@@ -322,22 +538,46 @@ impl CadCommand for AngularDimensionCommand {
}
"M" | "MTEXT" => {
self.mtext_override = true;
self.awaiting_text = true;
Some(CmdResult::NeedPoint)
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)
}
"Q" | "QUADRANT" => 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
}
@@ -352,6 +592,10 @@ impl CadCommand for AngularDimensionCommand {
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 {
@@ -360,13 +604,42 @@ impl CadCommand for AngularDimensionCommand {
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 },
PickedCurve::Line(second_start, second_end),
) if *first_source != handle => {
self.source_handles = vec![*first_source, *first_source, handle, handle];
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,
@@ -376,29 +649,49 @@ impl CadCommand for AngularDimensionCommand {
};
CmdResult::NeedPoint
}
(Step::Vertex, PickedCurve::Line(first_start, first_end)) => {
(
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 }) => {
self.source_handles = vec![handle, handle, handle];
(
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, radius }) => {
let Some(first) = project_to_radius(center, point, radius) else {
(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,
radius,
first: point,
source: handle,
};
CmdResult::NeedPoint
@@ -408,29 +701,63 @@ impl CadCommand for AngularDimensionCommand {
}
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, radius, .. } => {
let second = project_to_radius(vertex, point, radius).unwrap_or(point);
vec![vertex, first, nan(), vertex, second]
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,
} => two_line_preview(
first_start,
first_end,
second_start,
second_end,
point,
),
} => {
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))
}
@@ -438,24 +765,43 @@ impl CadCommand for AngularDimensionCommand {
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) => Some(PickedCurve::Line(point(line.start), point(line.end))),
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()),
radius: circle.radius,
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(
polyline.vertices.iter().map(|vertex| [vertex.location.x, vertex.location.y]).collect(),
&points,
&bulges,
polyline.is_closed,
[click.0, click.1],
)?;
@@ -467,17 +813,24 @@ fn picked_curve(entity: &EntityType, click: DVec3) -> Option<PickedCurve> {
first.bulge,
polyline.elevation,
polyline.normal,
segment,
polyline.vertices.len(),
)
}
EntityType::Polyline2D(polyline) => {
let vertices = crate::entities::polyline::drawn_vertices2d(polyline)
.unwrap_or_else(|| polyline.vertices.clone());
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(
vertices.iter().map(|vertex| [vertex.location.x, vertex.location.y]).collect(),
&points,
&bulges,
polyline.is_closed(),
[click.0, click.1],
)?;
@@ -489,27 +842,86 @@ fn picked_curve(entity: &EntityType, click: DVec3) -> Option<PickedCurve> {
first.bulge,
polyline.elevation,
polyline.normal,
segment,
vertices.len(),
)
}
_ => None,
}
}
fn nearest_segment_index(points: Vec<[f64; 2]>, closed: bool, click: [f64; 2]) -> Option<usize> {
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| {
segment_distance_squared(points[*first], points[(*first + 1) % points.len()], click)
.total_cmp(&segment_distance_squared(
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 bulge_center(first: [f64; 2], second: [f64; 2], bulge: f64) -> Option<[f64; 2]> {
if bulge.abs() <= 1.0e-12 {
return None;
}
let dx = second[0] - first[0];
let dy = second[1] - first[1];
let chord = dx.hypot(dy);
if chord <= 1.0e-12 {
return None;
}
let midpoint = [(first[0] + second[0]) * 0.5, (first[1] + second[1]) * 0.5];
let offset = chord * (1.0 - bulge * bulge) / (4.0 * bulge);
Some([
midpoint[0] - dy / chord * offset,
midpoint[1] + dx / chord * offset,
])
}
fn polyline_segment_distance_squared(
first: [f64; 2],
second: [f64; 2],
bulge: f64,
point: [f64; 2],
) -> f64 {
let Some(center) = bulge_center(first, second, bulge) else {
return segment_distance_squared(first, second, point);
};
let radius = (first[0] - center[0]).hypot(first[1] - center[1]);
let start = (first[1] - center[1]).atan2(first[0] - center[0]);
let target = (point[1] - center[1]).atan2(point[0] - center[0]);
let sweep = 4.0 * bulge.atan();
let into = if sweep >= 0.0 {
(target - start).rem_euclid(std::f64::consts::TAU)
} else {
(start - target).rem_euclid(std::f64::consts::TAU)
};
if into <= sweep.abs() + 1.0e-12 {
let radial = (point[0] - center[0]).hypot(point[1] - center[1]);
(radial - radius).powi(2)
} else {
((point[0] - first[0]).powi(2) + (point[1] - first[1]).powi(2)).min(
(point[0] - second[0]).powi(2) + (point[1] - second[1]).powi(2),
)
}
}
fn segment_distance_squared(first: [f64; 2], second: [f64; 2], point: [f64; 2]) -> f64 {
let dx = second[0] - first[0];
let dy = second[1] - first[1];
@@ -530,6 +942,8 @@ fn polyline_segment(
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(
@@ -540,31 +954,95 @@ fn polyline_segment(
};
let first_world = to_world(first);
let second_world = to_world(second);
if bulge.abs() <= 1.0e-12 {
return Some(PickedCurve::Line(first_world, second_world));
}
let dx = second[0] - first[0];
let dy = second[1] - first[1];
let chord = (dx * dx + dy * dy).sqrt();
if chord <= 1.0e-12 {
if first_world.distance_squared(second_world) <= 1.0e-24 {
return None;
}
let midpoint = [(first[0] + second[0]) * 0.5, (first[1] + second[1]) * 0.5];
let center_offset = chord * (1.0 - bulge * bulge) / (4.0 * bulge);
let center = [
midpoint[0] - dy / chord * center_offset,
midpoint[1] + dx / chord * center_offset,
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 = bulge_center(first, second, bulge)?;
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 project_to_radius(center: DVec3, point: DVec3, radius: f64) -> Option<DVec3> {
let direction = (point - center).normalize_or_zero();
(direction.length_squared() > 0.0).then_some(center + direction * radius)
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 min_z = local.iter().map(|point| point.z).fold(f64::INFINITY, f64::min);
let max_z = local.iter().map(|point| point.z).fold(f64::NEG_INFINITY, f64::max);
let scale = local
.iter()
.map(|point| point.x.abs().max(point.y.abs()).max(1.0))
.fold(1.0, f64::max);
max_z - min_z <= 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 = plane.vector_to_local(first_end - first_start);
let second = plane.vector_to_local(second_end - second_start);
let denominator = first.x * second.y - first.y * second.x;
denominator.abs() <= 1.0e-12 * first.length().max(second.length()).max(1.0)
}
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(