Complete angular dimension creation and associations

This commit is contained in:
ramox81 2026-08-25 11:29:38 +03:00
commit e93815d0c5
5 changed files with 749 additions and 153 deletions

View file

@ -1229,17 +1229,24 @@ impl OpenCADStudio {
self.commit_undo_delta(i, pd);
}
}
CmdResult::CommitAssociativeDimension { entity, source } => {
let label = self.history_label_from_active_cmd(i, "DIMLINEAR");
CmdResult::CommitAssociativeDimension { entity, sources } => {
let label = self.history_label_from_active_cmd(i, "DIMENSION");
let pending = self.begin_undo(i, label, 1, false);
if let Some(handle) = self.commit_entity_handle(entity) {
self.tabs[i]
.scene
.attach_linear_dimension_association(handle, [Some(source), Some(source)]);
self.tabs[i].scene.bump_entities(&[
(handle, crate::scene::ChangeKind::Modified),
(source, crate::scene::ChangeKind::Modified),
]);
.attach_dimension_association(
handle,
sources.iter().copied().map(Some).collect(),
);
let mut changed = vec![(handle, crate::scene::ChangeKind::Modified)];
changed.extend(
sources
.iter()
.copied()
.map(|source| (source, crate::scene::ChangeKind::Modified)),
);
self.tabs[i].scene.bump_entities(&changed);
}
self.tabs[i].dirty = true;
self.tabs[i].scene.clear_preview_wire();

View file

@ -1212,10 +1212,10 @@ pub enum CmdResult {
CommitEntitiesAndExit(Vec<EntityType>),
/// Commit an acadrust entity to the document and end the command.
CommitAndExit(EntityType),
/// Commit an object-selected linear dimension and retain its source link.
/// Commit an object-selected dimension and retain its source links.
CommitAssociativeDimension {
entity: EntityType,
source: Handle,
sources: Vec<Handle>,
},
/// Commit a Model-tab 3D solid: the acadrust entity (for selection /
/// persistence) plus its B-rep (cached for boolean ops + shaded

View file

@ -1,12 +1,12 @@
use acadrust::entities::{Dimension, DimensionAngular3Pt};
use acadrust::types::Vector3;
use acadrust::entities::{Dimension, DimensionAngular2Ln, DimensionAngular3Pt};
use acadrust::types::{Handle, Vector3};
use acadrust::EntityType;
use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use glam::{DVec3, Vec3};
use crate::t;
use glam::DVec3;
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/dim_angular.svg"));
@ -22,20 +22,55 @@ pub fn tool() -> ToolDef {
enum Step {
Vertex,
FirstRay(DVec3),
SecondRay {
SecondRay { vertex: DVec3, first: DVec3 },
CircleSecondRay {
vertex: DVec3,
first: DVec3,
radius: f64,
source: Handle,
},
ArcPoint {
SecondLine {
first_start: DVec3,
first_end: DVec3,
first_source: Handle,
},
ArcPoint3 {
vertex: DVec3,
first: DVec3,
second: DVec3,
},
ArcPoint2 {
first_start: DVec3,
first_end: DVec3,
second_start: DVec3,
second_end: DVec3,
},
}
enum PickedCurve {
Line(DVec3, DVec3),
Arc {
center: DVec3,
first: DVec3,
second: DVec3,
},
Circle {
center: DVec3,
radius: f64,
},
}
pub struct AngularDimensionCommand {
step: Step,
plane: WorkingPlane,
selecting_object: bool,
picked_entity: Option<EntityType>,
source_handles: Vec<Handle>,
text_override: Option<String>,
awaiting_text: bool,
mtext_override: bool,
text_angle: Option<f64>,
awaiting_angle: bool,
}
impl AngularDimensionCommand {
@ -43,8 +78,92 @@ impl AngularDimensionCommand {
Self {
step: Step::Vertex,
plane: WorkingPlane::default(),
selecting_object: false,
picked_entity: None,
source_handles: Vec::new(),
text_override: None,
awaiting_text: false,
mtext_override: false,
text_angle: None,
awaiting_angle: false,
}
}
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 arc_point = self.plane.to_local(arc_point);
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;
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 arc_point = self.plane.to_local(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.dimension_arc;
dim.base.text_middle_point = dim.dimension_arc;
dim.base.insertion_point = dim.dimension_arc;
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));
if self.source_handles.is_empty() {
CmdResult::CommitAndExit(entity)
} else {
CmdResult::CommitAssociativeDimension {
entity,
sources: self.source_handles.clone(),
}
}
}
fn placement_step(&self) -> bool {
matches!(self.step, Step::ArcPoint3 { .. } | Step::ArcPoint2 { .. })
}
}
impl CadCommand for AngularDimensionCommand {
@ -57,59 +176,102 @@ impl CadCommand for AngularDimensionCommand {
}
fn prompt(&self) -> String {
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 t!("DIMANGULAR Specify text angle (degrees):").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(),
};
}
match self.step {
Step::Vertex => t!("DIMANGULAR Specify angle vertex:").into_owned(),
Step::Vertex => t!(
"DIMANGULAR Specify angle vertex or press Enter to select an object:"
)
.into_owned(),
Step::FirstRay(_) => {
t!("DIMANGULAR Specify first extension line point:").into_owned()
}
Step::SecondRay { .. } => {
Step::SecondRay { .. } | Step::CircleSecondRay { .. } => {
t!("DIMANGULAR Specify second extension line point:").into_owned()
}
Step::ArcPoint { .. } => t!("DIMANGULAR Specify dimension arc location:").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, pt: DVec3) -> CmdResult {
fn on_point(&mut self, point: DVec3) -> CmdResult {
match self.step {
Step::Vertex => {
self.step = Step::FirstRay(pt);
self.step = Step::FirstRay(point);
CmdResult::NeedPoint
}
Step::FirstRay(vertex) => {
self.step = Step::SecondRay { vertex, first: pt };
if point.distance_squared(vertex) <= 1.0e-24 {
return CmdResult::NeedPoint;
}
self.step = Step::SecondRay { vertex, first: point };
CmdResult::NeedPoint
}
Step::SecondRay { vertex, first } => {
self.step = Step::ArcPoint {
vertex,
first,
second: pt,
};
if point.distance_squared(vertex) <= 1.0e-24 {
return CmdResult::NeedPoint;
}
self.step = Step::ArcPoint3 { vertex, first, second: point };
CmdResult::NeedPoint
}
Step::ArcPoint {
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 pt = self.plane.to_local(pt);
let mut dim = DimensionAngular3Pt::new(v3(vertex), v3(first), v3(second));
dim.definition_point = v3(pt);
dim.base.definition_point = v3(pt);
dim.base.text_middle_point = v3(pt);
dim.base.insertion_point = v3(pt);
dim.base.actual_measurement = dim.measurement_degrees();
CmdResult::CommitAndExit(self.plane.place_entity(EntityType::Dimension(
Dimension::Angular3Pt(dim),
)))
Step::CircleSecondRay { vertex, first, radius, source } => {
let Some(second) = project_to_radius(vertex, point, radius) else {
return CmdResult::NeedPoint;
};
self.source_handles = vec![source, source, source];
self.step = Step::ArcPoint3 { vertex, first, second };
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_text {
self.awaiting_text = false;
return CmdResult::NeedPoint;
}
if self.awaiting_angle {
self.awaiting_angle = false;
return CmdResult::NeedPoint;
}
if matches!(self.step, Step::Vertex) {
self.selecting_object = true;
return CmdResult::NeedPoint;
}
CmdResult::Cancel
}
@ -117,49 +279,392 @@ impl CadCommand for AngularDimensionCommand {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
let pt = pt.as_vec3();
match self.step {
Step::Vertex => None,
Step::FirstRay(vertex) => Some(preview_wire(vec![vertex.as_vec3(), pt])),
Step::SecondRay { vertex, first } => Some(preview_wire(vec![
vertex.as_vec3(),
first.as_vec3(),
Vec3::new(f32::NAN, f32::NAN, f32::NAN),
vertex.as_vec3(),
pt,
])),
Step::ArcPoint {
vertex,
first,
second,
} => {
let points = angular_preview(
self.plane.to_local(vertex).as_vec3(),
self.plane.to_local(first).as_vec3(),
self.plane.to_local(second).as_vec3(),
self.plane.to_local(pt.as_dvec3()).as_vec3(),
)
.into_iter()
.map(|point| {
if point.x.is_nan() {
point
} else {
self.plane.to_world(point.as_dvec3()).as_vec3()
}
})
.collect();
Some(preview_wire(points))
}
fn wants_text_input(&self) -> bool {
true
}
fn wants_text_with_spaces(&self) -> bool {
self.awaiting_text
}
fn point_step_accepts_keywords(&self) -> bool {
self.placement_step() && !self.awaiting_text && !self.awaiting_angle
}
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;
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;
self.awaiting_text = true;
Some(CmdResult::NeedPoint)
}
"A" | "ANGLE" => {
self.awaiting_angle = true;
Some(CmdResult::NeedPoint)
}
"Q" | "QUADRANT" => Some(CmdResult::NeedPoint),
_ => None,
}
}
fn needs_entity_pick(&self) -> bool {
self.selecting_object
}
fn entity_pick_highlights_hover(&self) -> bool {
true
}
fn inject_before_entity_pick(&self) -> bool {
true
}
fn inject_picked_entity(&mut self, entity: EntityType) {
self.picked_entity = Some(entity);
}
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
if handle.is_null() {
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;
};
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];
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(first_start, first_end)) => {
self.step = Step::SecondLine {
first_start,
first_end,
first_source: handle,
};
CmdResult::NeedPoint
}
(Step::Vertex, PickedCurve::Arc { center, first, second }) => {
self.source_handles = vec![handle, handle, handle];
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 {
return CmdResult::NeedPoint;
};
self.selecting_object = false;
self.step = Step::CircleSecondRay {
vertex: center,
first,
radius,
source: handle,
};
CmdResult::NeedPoint
}
_ => CmdResult::NeedPoint,
}
}
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
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::ArcPoint3 { vertex, first, second } => {
angular_preview(vertex, first, second, point)
}
Step::ArcPoint2 {
first_start,
first_end,
second_start,
second_end,
} => two_line_preview(
first_start,
first_end,
second_start,
second_end,
point,
),
};
Some(preview_wire(points))
}
}
fn v3(pt: DVec3) -> Vector3 {
Vector3::new(pt.x, pt.y, pt.z)
fn picked_curve(entity: &EntityType, click: DVec3) -> Option<PickedCurve> {
let point = |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::Arc(arc) => Some(PickedCurve::Arc {
center: point(arc.center_wcs()),
first: point(arc.start_point_wcs()),
second: point(arc.end_point_wcs()),
}),
EntityType::Circle(circle) => Some(PickedCurve::Circle {
center: point(circle.center_wcs()),
radius: circle.radius,
}),
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 segment = nearest_segment_index(
polyline.vertices.iter().map(|vertex| [vertex.location.x, vertex.location.y]).collect(),
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,
)
}
EntityType::Polyline2D(polyline) => {
let vertices = crate::entities::polyline::drawn_vertices2d(polyline)
.unwrap_or_else(|| polyline.vertices.clone());
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 segment = nearest_segment_index(
vertices.iter().map(|vertex| [vertex.location.x, vertex.location.y]).collect(),
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,
)
}
_ => None,
}
}
fn preview_wire(points: Vec<Vec3>) -> WireModel {
fn nearest_segment_index(points: Vec<[f64; 2]>, 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(
points[*second],
points[(*second + 1) % points.len()],
click,
))
})
}
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];
let length_squared = dx * dx + dy * dy;
if length_squared <= 1.0e-24 {
return (point[0] - first[0]).powi(2) + (point[1] - first[1]).powi(2);
}
let t = (((point[0] - first[0]) * dx + (point[1] - first[1]) * dy) / length_squared)
.clamp(0.0, 1.0);
let x = first[0] + dx * t;
let y = first[1] + dy * t;
(point[0] - x).powi(2) + (point[1] - y).powi(2)
}
fn polyline_segment(
first: [f64; 2],
second: [f64; 2],
bulge: f64,
elevation: f64,
normal: Vector3,
) -> 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 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 {
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,
];
Some(PickedCurve::Arc {
center: to_world(center),
first: first_world,
second: second_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 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 denominator = first_direction.x * second_direction.y - first_direction.y * second_direction.x;
if denominator.abs()
<= 1.0e-12 * first_direction.length().max(second_direction.length()).max(1.0)
{
return None;
}
let offset = second_start - first_start;
let t = (offset.x * second_direction.y - offset.y * second_direction.x) / denominator;
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()];
let sweep = end - start;
let steps = ((sweep.abs() / std::f64::consts::PI * 90.0).ceil() as usize).clamp(8, 720);
for index in 0..=steps {
let angle = start + sweep * index as f64 / steps as f64;
points.push(vertex + DVec3::new(angle.cos() * radius, angle.sin() * radius, 0.0));
}
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 {
point_marker: None,
taper_widths: Vec::new(),
@ -173,11 +678,11 @@ fn preview_wire(points: Vec<Vec3>) -> WireModel {
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
dash_align_end: None,
text_verts: Vec::new(),
dash_from_start: false,
dash_align_end: None,
text_verts: Vec::new(),
name: "dimangular_preview".to_string(),
points: points.into_iter().map(|p| [p.x, p.y, p.z]).collect(),
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,
@ -195,38 +700,4 @@ fn preview_wire(points: Vec<Vec3>) -> WireModel {
}
}
fn angular_preview(vertex: Vec3, first: Vec3, second: Vec3, arc_pt: Vec3) -> Vec<Vec3> {
let mut points = vec![
vertex,
first,
Vec3::new(f32::NAN, f32::NAN, f32::NAN),
vertex,
second,
Vec3::new(f32::NAN, f32::NAN, f32::NAN),
];
let r = vertex.distance(arc_pt);
if r <= 1e-6 {
return points;
}
let a0 = (first.y - vertex.y).atan2(first.x - vertex.x);
let mut a1 = (second.y - vertex.y).atan2(second.x - vertex.x);
let mut delta = a1 - a0;
while delta <= 0.0 {
delta += std::f32::consts::TAU;
}
if delta > std::f32::consts::PI {
a1 -= std::f32::consts::TAU;
delta = a1 - a0;
}
let steps = 24;
for i in 0..=steps {
let t = i as f32 / steps as f32;
let a = a0 + delta * t;
points.push(vertex + Vec3::new(a.cos() * r, a.sin() * r, 0.0));
}
points
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["DIMANGULAR"] }); // AngularDimensionCommand
inventory::submit!(crate::command::CommandRegistration { names: &["DIMANGULAR"] });

View file

@ -194,7 +194,10 @@ impl CadCommand for LinearDimensionCommand {
Dimension::Linear(dim),
));
if let Some(source) = self.source_handle {
CmdResult::CommitAssociativeDimension { entity, source }
CmdResult::CommitAssociativeDimension {
entity,
sources: vec![source, source],
}
} else {
CmdResult::CommitAndExit(entity)
}

View file

@ -27,6 +27,7 @@ fn source_points(entity: &EntityType) -> Vec<Vector3> {
match entity {
EntityType::Line(line) => vec![line.start, line.end],
EntityType::Arc(arc) => vec![arc.start_point_wcs(), arc.end_point_wcs()],
EntityType::Circle(_) => Vec::new(),
EntityType::LwPolyline(polyline) => polyline
.vertices
.iter()
@ -55,6 +56,37 @@ fn source_points(entity: &EntityType) -> Vec<Vector3> {
}
}
fn source_reference(entity: &EntityType, point: Vector3) -> Option<(i32, f64)> {
let curve_parameter = match entity {
EntityType::Circle(circle) => {
let center = circle.center_wcs();
if point_distance_squared(center, point) <= 1e-16 {
return Some((-3, 0.0));
}
let (axis_x, axis_y) = circle.axes_wcs();
let offset = point - center;
Some(offset.dot(&axis_y).atan2(offset.dot(&axis_x)))
}
EntityType::Arc(arc) => {
let local = crate::scene::view::transform::wcs_point_to_ocs(
(point.x, point.y, point.z),
(arc.normal.x, arc.normal.y, arc.normal.z),
);
let dx = local.0 - arc.center.x;
let dy = local.1 - arc.center.y;
if dx * dx + dy * dy <= 1e-16 {
return Some((-3, 0.0));
}
Some(dy.atan2(dx))
}
_ => None,
};
if let Some(parameter) = curve_parameter {
return Some((-2, parameter));
}
source_marker(entity, point).map(|marker| (marker, 0.0))
}
fn source_marker(entity: &EntityType, point: Vector3) -> Option<i32> {
source_points(entity)
.into_iter()
@ -69,11 +101,45 @@ fn source_marker(entity: &EntityType, point: Vector3) -> Option<i32> {
fn resolve_reference(scene: &Scene, reference: &AssocDimensionReference) -> Option<Vector3> {
let source = *reference.xrefs.first()?;
let entity = scene.document.get_entity(source)?;
if reference.main_gs_marker == -3 {
return match entity {
EntityType::Circle(circle) => Some(circle.center_wcs()),
EntityType::Arc(arc) => Some(arc.center_wcs()),
_ => None,
};
}
if reference.main_gs_marker == -2 {
return match entity {
EntityType::Circle(circle) => {
Some(circle.point_at_angle_wcs(reference.osnap_distance))
}
EntityType::Arc(arc) => Some(arc.point_at_angle_wcs(reference.osnap_distance)),
_ => None,
};
}
source_points(entity)
.get(reference.main_gs_marker.max(0) as usize)
.copied()
}
fn dimension_reference_points(dimension: &Dimension) -> Vec<Vector3> {
match dimension {
Dimension::Linear(linear) => vec![linear.first_point, linear.second_point],
Dimension::Angular3Pt(angular) => vec![
angular.angle_vertex,
angular.first_point,
angular.second_point,
],
Dimension::Angular2Ln(angular) => vec![
angular.first_point,
angular.second_point,
angular.angle_vertex,
angular.definition_point,
],
_ => Vec::new(),
}
}
pub(crate) fn dimension_is_associative(
document: &acadrust::CadDocument,
dimension: Handle,
@ -97,34 +163,41 @@ pub(crate) fn dimension_is_associative(
}
impl Scene {
pub(crate) fn attach_linear_dimension_association(
pub(crate) fn attach_dimension_association(
&mut self,
dimension: Handle,
sources: [Option<Handle>; 2],
sources: Vec<Option<Handle>>,
) {
let Some(EntityType::Dimension(Dimension::Linear(linear))) =
let Some(EntityType::Dimension(dimension_entity)) =
self.document.get_entity(dimension)
else {
return;
};
let first_point = linear.first_point;
let second_point = linear.second_point;
let source_data = [first_point, second_point].map(|point| point);
let resolved: [Option<(Handle, i32)>; 2] = std::array::from_fn(|index| {
let source = sources[index]?;
let source_data = dimension_reference_points(dimension_entity);
if source_data.is_empty() {
return;
}
let resolved: Vec<Option<(Handle, i32, f64)>> = source_data
.iter()
.enumerate()
.map(|(index, point)| {
let source = sources.get(index).copied().flatten()?;
let entity = self.document.get_entity(source)?;
source_marker(entity, source_data[index]).map(|marker| (source, marker))
});
source_reference(entity, *point)
.map(|(marker, parameter)| (source, marker, parameter))
})
.collect();
if resolved.iter().all(Option::is_none) {
return;
}
let reference = |source: Handle, marker: i32, point: Vector3| AssocDimensionReference {
let reference = |source: Handle, marker: i32, parameter: f64, point: Vector3| AssocDimensionReference {
class_name: "AcDbOsnapPointRef".to_string(),
osnap_type: 1,
xrefs: vec![source],
main_subent_type: 1,
main_gs_marker: marker,
osnap_distance: parameter,
osnap_point: point,
..AssocDimensionReference::default()
};
@ -132,9 +205,12 @@ impl Scene {
std::array::from_fn(|_| Vec::new());
let mut associativity = 0;
for (index, resolved) in resolved.into_iter().enumerate() {
if let Some((source, marker)) = resolved {
if index >= references.len() {
break;
}
if let Some((source, marker, parameter)) = resolved {
associativity |= 1 << index;
references[index].push(reference(source, marker, source_data[index]));
references[index].push(reference(source, marker, parameter, source_data[index]));
}
}
@ -165,6 +241,14 @@ impl Scene {
}
}
pub(crate) fn attach_linear_dimension_association(
&mut self,
dimension: Handle,
sources: [Option<Handle>; 2],
) {
self.attach_dimension_association(dimension, sources.into_iter().collect());
}
pub(crate) fn infer_linear_dimension_sources(
&self,
dimension: Handle,
@ -222,26 +306,57 @@ impl Scene {
let mut refreshed = Vec::new();
for association in associations {
let first = association.references[0]
.first()
.and_then(|reference| resolve_reference(self, reference));
let second = association.references[1]
.first()
.and_then(|reference| resolve_reference(self, reference));
if first.is_none() && second.is_none() {
let resolved: [Option<Vector3>; 4] = std::array::from_fn(|index| {
association.references[index]
.first()
.and_then(|reference| resolve_reference(self, reference))
});
if resolved.iter().all(Option::is_none) {
continue;
}
if let Some(EntityType::Dimension(Dimension::Linear(linear))) =
if let Some(EntityType::Dimension(dimension)) =
self.document.get_entity_mut(association.dimension)
{
if let Some(first) = first {
linear.first_point = first;
match dimension {
Dimension::Linear(linear) => {
if let Some(point) = resolved[0] {
linear.first_point = point;
}
if let Some(point) = resolved[1] {
linear.second_point = point;
}
linear.base.definition_point = linear.definition_point;
}
Dimension::Angular3Pt(angular) => {
if let Some(point) = resolved[0] {
angular.angle_vertex = point;
}
if let Some(point) = resolved[1] {
angular.first_point = point;
}
if let Some(point) = resolved[2] {
angular.second_point = point;
}
angular.base.definition_point = angular.definition_point;
}
Dimension::Angular2Ln(angular) => {
if let Some(point) = resolved[0] {
angular.first_point = point;
}
if let Some(point) = resolved[1] {
angular.second_point = point;
}
if let Some(point) = resolved[2] {
angular.angle_vertex = point;
}
if let Some(point) = resolved[3] {
angular.definition_point = point;
}
angular.base.definition_point = angular.dimension_arc;
}
_ => continue,
}
if let Some(second) = second {
linear.second_point = second;
}
linear.base.actual_measurement = linear.measurement();
linear.base.definition_point = linear.definition_point;
dimension.base_mut().actual_measurement = dimension.measurement();
refreshed.push((association.dimension, ChangeKind::Modified));
}
}