From d8e998b5e8528191b963865acbfdb58940c0787c Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:22:12 +0300 Subject: [PATCH 1/2] Complete continued dimension workflow --- src/app/command_driver.rs | 12 +- src/app/commands/dim.rs | 16 +- src/app/properties.rs | 13 +- src/command.rs | 2 + src/modules/annotate/aligned_dim.rs | 1 + src/modules/annotate/angular_dim.rs | 1 + src/modules/annotate/arc_length_dim.rs | 1 + src/modules/annotate/diameter_dim.rs | 1 + src/modules/annotate/dim_continue.rs | 771 +++++++++++++++++----- src/modules/annotate/jogged_radius_dim.rs | 1 + src/modules/annotate/linear_dim.rs | 1 + src/modules/annotate/ordinate_dim.rs | 1 + src/modules/annotate/radius_dim.rs | 1 + src/scene/mod.rs | 8 +- 14 files changed, 653 insertions(+), 177 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index d33b27233..3e7e4bafb 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -1288,6 +1288,7 @@ impl OpenCADStudio { mut entity, association, preserve_base_style, + keep_active, } => { let label = self.history_label_from_active_cmd(i, "DIMENSION"); let association_mode = self.tabs[i] @@ -1414,9 +1415,16 @@ impl OpenCADStudio { }; self.tabs[i].dirty = true; self.tabs[i].scene.clear_preview_wire(); - self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; - self.restore_pre_cmd_tangent(); + if keep_active { + let prompt = self.tabs[i].active_cmd.as_ref().map(|command| command.prompt()); + if let Some(prompt) = prompt { + self.command_line.push_info(&prompt); + } + } else { + self.tabs[i].active_cmd = None; + self.restore_pre_cmd_tangent(); + } if let Some(pd) = pending { self.commit_undo_delta(i, pd); } diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs index 22ee0d96e..90da46a80 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -646,13 +646,15 @@ impl OpenCADStudio { "DIMCONTINUE" => { use crate::modules::annotate::dim_continue::DimContinueCommand; - let cmd = if let Some((p1, p2, dp, rot, trot)) = - find_last_linear_dim(&self.tabs[i].scene) - { - DimContinueCommand::from_base(p1, p2, dp, rot, trot) - } else { - DimContinueCommand::new() - }; + let scene = &self.tabs[i].scene; + let recent = scene + .last_created_dimension + .and_then(|handle| scene.document.get_entity(handle)) + .cloned(); + let cmd = DimContinueCommand::new( + recent, + self.dimension_continue_mode == 1, + ); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } diff --git a/src/app/properties.rs b/src/app/properties.rs index 6129dd8b8..302c561ce 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -2470,7 +2470,16 @@ impl OpenCADStudio { preserve_dimension_layer_and_style: bool, ) -> Option { let i = self.active_tab; - let is_dimension = matches!(&entity, acadrust::EntityType::Dimension(_)); + let tracks_continuation = matches!( + &entity, + acadrust::EntityType::Dimension( + acadrust::entities::Dimension::Linear(_) + | acadrust::entities::Dimension::Aligned(_) + | acadrust::entities::Dimension::Angular2Ln(_) + | acadrust::entities::Dimension::Angular3Pt(_) + | acadrust::entities::Dimension::Ordinate(_) + ) + ); let inherited_dimension = if preserve_dimension_layer_and_style { match &entity { acadrust::EntityType::Dimension(dimension) => Some(( @@ -2748,7 +2757,7 @@ impl OpenCADStudio { self.tabs[i].last_draw_anchor = Some(handle); } } - if is_dimension { + if tracks_continuation { self.tabs[i].scene.last_created_dimension = new_handle; } new_handle diff --git a/src/command.rs b/src/command.rs index e8bf675e8..968c9d29e 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1251,6 +1251,8 @@ pub enum CmdResult { /// Retain the source dimension's layer and style instead of replacing /// them with the current creation defaults (DIMCONTINUEMODE=1). preserve_base_style: bool, + /// Keep the interactive command active after this dimension is committed. + keep_active: bool, }, /// Commit a Model-tab 3D solid: the acadrust entity (for selection / /// persistence) plus its B-rep (cached for boolean ops + shaded diff --git a/src/modules/annotate/aligned_dim.rs b/src/modules/annotate/aligned_dim.rs index dafc2183b..5765e4759 100644 --- a/src/modules/annotate/aligned_dim.rs +++ b/src/modules/annotate/aligned_dim.rs @@ -136,6 +136,7 @@ impl CadCommand for AlignedDimensionCommand { entity: self.plane.place_entity(EntityType::Dimension(Dimension::Aligned(dim))), association: DimensionAssociationInput::Infer(self.source_handle), preserve_base_style: false, + keep_active: false, } } } diff --git a/src/modules/annotate/angular_dim.rs b/src/modules/annotate/angular_dim.rs index f1c00059f..9e7547b96 100644 --- a/src/modules/annotate/angular_dim.rs +++ b/src/modules/annotate/angular_dim.rs @@ -235,6 +235,7 @@ impl AngularDimensionCommand { entity, association: DimensionAssociationInput::Explicit(self.source_refs.clone()), preserve_base_style: false, + keep_active: false, } } diff --git a/src/modules/annotate/arc_length_dim.rs b/src/modules/annotate/arc_length_dim.rs index 434eabbe6..4ae6ea24d 100644 --- a/src/modules/annotate/arc_length_dim.rs +++ b/src/modules/annotate/arc_length_dim.rs @@ -266,6 +266,7 @@ impl ArcLengthDimensionCommand { entity: plane.place_entity(EntityType::Dimension(Dimension::Arc(dimension))), association, preserve_base_style: false, + keep_active: false, } } } diff --git a/src/modules/annotate/diameter_dim.rs b/src/modules/annotate/diameter_dim.rs index 946cde255..36286d340 100644 --- a/src/modules/annotate/diameter_dim.rs +++ b/src/modules/annotate/diameter_dim.rs @@ -114,6 +114,7 @@ impl CadCommand for DiameterDimensionCommand { .place_entity(EntityType::Dimension(Dimension::Diameter(dim))), association: DimensionAssociationInput::Infer(self.source_handle), preserve_base_style: false, + keep_active: false, } } } diff --git a/src/modules/annotate/dim_continue.rs b/src/modules/annotate/dim_continue.rs index b7eb336b7..4d4f99b7b 100644 --- a/src/modules/annotate/dim_continue.rs +++ b/src/modules/annotate/dim_continue.rs @@ -1,20 +1,16 @@ -// DIMCONTINUE command — chain linear/aligned dimensions end-to-end. -// -// Each new point becomes the second extension line origin of a new dimension, -// whose first extension line origin is the second extension line of the previous dim. -// The dimension line stays at the same perpendicular offset as the base dimension. -// -// Constructed from commands.rs after finding the last placed linear/aligned dimension. +//! Repeating continued dimensions from the latest session dimension or a picked base. -use acadrust::entities::{Dimension, DimensionLinear}; +use acadrust::entities::{ + Dimension, DimensionAngular2Ln, DimensionAngular3Pt, DimensionBase, DimensionLinear, + DimensionOrdinate, +}; use acadrust::types::Vector3; -use acadrust::EntityType; -use glam::{DVec3, Vec3}; +use acadrust::{EntityType, Handle}; +use glam::DVec3; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdOption, CmdResult, DimensionAssociationInput}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; -use crate::t; pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/dim_continue.svg")); @@ -27,74 +23,262 @@ pub fn tool() -> ToolDef { } } -pub struct DimContinueCommand { - /// Fixed first-extension-line origin for the current step (moves each iteration). - chain_p1: DVec3, - /// Direction along the dimension axis (0.0 = horizontal, PI/2 = vertical). - rotation: f64, - /// Text reading rotation inherited from the base dim so a UCS-aligned chain - /// keeps its text consistent with the originating DIMLINEAR. 0 = natural. +#[derive(Clone)] +struct SourceStyle { + layer: String, + style_name: String, + normal: Vector3, text_rotation: f64, - /// Absolute perpendicular coordinate (dot with `perp`) of the base - /// dimension line. Each continued dim line is projected onto this exact - /// coordinate so the whole chain stays collinear — even when the extension - /// origins sit at different perpendicular positions (extension lines of - /// different lengths). (#151) - dim_line_perp: f64, - /// Direction of "up" perpendicular to the dim axis (points toward the dim line). - perp: DVec3, - /// True once we have a base dimension loaded. - ready: bool, + horizontal_direction: f64, } -impl DimContinueCommand { - /// No base dim found — will show an error prompt and cancel immediately. - pub fn new() -> Self { +impl SourceStyle { + fn from_base(base: &DimensionBase) -> Self { Self { - chain_p1: DVec3::ZERO, - rotation: 0.0, - text_rotation: 0.0, - dim_line_perp: 0.0, - perp: DVec3::Y, - ready: false, + layer: base.common.layer.clone(), + style_name: base.style_name.clone(), + normal: base.normal, + text_rotation: base.text_rotation, + horizontal_direction: base.horizontal_direction, } } - /// Build from the last placed dimension. - /// - /// `p2` — second extension line origin of the base dim (the chain starts - /// here). `p1` is unused — the dim line position comes from - /// `definition_point` alone — but kept for signature parity with the - /// shared `find_last_linear_dim` tuple (DIMBASELINE uses p1). - /// `definition_point` — where the dim line was placed; its perpendicular - /// coordinate is the line the whole chain stays collinear with. - /// `rotation` — 0.0 = horizontal dim, PI/2 = vertical dim. - pub fn from_base( - _p1: Vec3, - p2: Vec3, - definition_point: Vec3, + fn apply(&self, base: &mut DimensionBase) { + base.common.layer = self.layer.clone(); + base.style_name = self.style_name.clone(); + base.normal = self.normal; + base.text_rotation = self.text_rotation; + base.horizontal_direction = self.horizontal_direction; + } +} + +#[derive(Clone)] +enum ContinueKind { + Linear { + current: DVec3, rotation: f64, - text_rotation: f64, - ) -> Self { - // Widen the base-dim points to f64 so the committed chain math runs in - // full precision. (The base points arrive as f32 from the entity, but - // every derived committed coordinate must stay f64 from here on.) - let p2 = p2.as_dvec3(); - let definition_point = definition_point.as_dvec3(); - // Axis unit vector along the measurement direction — the base dim's - // rotation angle (any angle, incl. a UCS-aligned one), not a world H/V. - let axis = DVec3::new(rotation.cos(), rotation.sin(), 0.0); - // Perpendicular unit vector toward the dim line. - let perp = DVec3::new(-axis.y, axis.x, 0.0); - let dim_line_perp = definition_point.dot(perp); - Self { - chain_p1: p2, - rotation, - text_rotation, - dim_line_perp, - perp, - ready: true, + perpendicular: DVec3, + line_coordinate: f64, + }, + Angular2Ln { + source: DimensionAngular2Ln, + vertex: DVec3, + current: DVec3, + radius: f64, + }, + Angular3Pt { + source: DimensionAngular3Pt, + vertex: DVec3, + current: DVec3, + radius: f64, + }, + Ordinate { + source: DimensionOrdinate, + }, +} + +#[derive(Clone)] +struct ContinueState { + kind: ContinueKind, + style: SourceStyle, +} + +pub struct DimContinueCommand { + base: Option, + injected: Option, + history: Vec, + preserve_base_style: bool, +} + +impl DimContinueCommand { + pub fn new(recent: Option, preserve_base_style: bool) -> Self { + let mut command = Self { + base: None, + injected: None, + history: Vec::new(), + preserve_base_style, + }; + if let Some(entity) = recent { + let _ = command.install_base(&entity, None); } + command + } + + fn install_base(&mut self, entity: &EntityType, pick: Option) -> bool { + let EntityType::Dimension(dimension) = entity else { + return false; + }; + let style = SourceStyle::from_base(dimension.base()); + let kind = match dimension { + Dimension::Linear(source) => { + let rotation = source.rotation; + let perpendicular = DVec3::new(-rotation.sin(), rotation.cos(), 0.0); + let current = selected_linear_end( + dv(source.first_point), + dv(source.second_point), + pick, + ); + ContinueKind::Linear { + current, + rotation, + perpendicular, + line_coordinate: dv(source.base.definition_point).dot(perpendicular), + } + } + Dimension::Aligned(source) => { + let first = dv(source.first_point); + let second = dv(source.second_point); + let delta = second - first; + if delta.length_squared() <= 1.0e-18 { + return false; + } + let rotation = delta.y.atan2(delta.x); + let perpendicular = DVec3::new(-rotation.sin(), rotation.cos(), 0.0); + ContinueKind::Linear { + current: selected_linear_end(first, second, pick), + rotation, + perpendicular, + line_coordinate: dv(source.base.definition_point).dot(perpendicular), + } + } + Dimension::Angular2Ln(source) => { + let vertex = angular2_vertex(source); + let first = dv(source.second_point); + let second = dv(source.definition_point); + ContinueKind::Angular2Ln { + source: source.clone(), + vertex, + current: selected_angular_end(first, second, pick), + radius: (dv(source.dimension_arc) - vertex).length().max(1.0e-9), + } + } + Dimension::Angular3Pt(source) => { + let vertex = dv(source.angle_vertex); + ContinueKind::Angular3Pt { + source: source.clone(), + vertex, + current: selected_angular_end( + dv(source.first_point), + dv(source.second_point), + pick, + ), + radius: (dv(source.definition_point) - vertex).length().max(1.0e-9), + } + } + Dimension::Ordinate(source) => ContinueKind::Ordinate { + source: source.clone(), + }, + _ => return false, + }; + self.base = Some(ContinueState { kind, style }); + true + } + + fn build_dimension(&self, point: DVec3) -> Option { + let state = self.base.as_ref()?; + let mut dimension = match &state.kind { + ContinueKind::Linear { + current, + rotation, + perpendicular, + line_coordinate, + } => build_linear( + *current, + point, + *rotation, + *perpendicular, + *line_coordinate, + ), + ContinueKind::Angular2Ln { + source, + vertex, + current, + radius, + } => { + let moving = point - *vertex; + let previous = *current - *vertex; + if moving.length_squared() <= 1.0e-18 || previous.length_squared() <= 1.0e-18 { + return None; + } + let mut result = source.clone(); + result.first_point = v3(*vertex); + result.second_point = v3(point); + result.angle_vertex = v3(*vertex); + result.definition_point = v3(*current); + let (definition, text) = + angular_definition_and_text(*vertex, previous, moving, *radius); + result.dimension_arc = v3(definition); + result.base.definition_point = result.dimension_arc; + result.base.text_middle_point = v3(text); + result.base.insertion_point = result.base.text_middle_point; + result.base.actual_measurement = result.measurement_degrees(); + Dimension::Angular2Ln(result) + } + ContinueKind::Angular3Pt { + source, + vertex, + current, + radius, + } => { + let moving = point - *vertex; + let previous = *current - *vertex; + if moving.length_squared() <= 1.0e-18 || previous.length_squared() <= 1.0e-18 { + return None; + } + let mut result = source.clone(); + result.angle_vertex = v3(*vertex); + result.first_point = v3(point); + result.second_point = v3(*current); + let (definition, text) = + angular_definition_and_text(*vertex, previous, moving, *radius); + result.definition_point = v3(definition); + result.base.definition_point = result.definition_point; + result.base.text_middle_point = v3(text); + result.base.insertion_point = result.base.text_middle_point; + result.base.actual_measurement = result.measurement_degrees(); + Dimension::Angular3Pt(result) + } + ContinueKind::Ordinate { source } => { + let source_leader = dv(source.leader_endpoint); + let leader = if source.is_ordinate_type_x { + DVec3::new(point.x, source_leader.y, source_leader.z) + } else { + DVec3::new(source_leader.x, point.y, source_leader.z) + }; + let mut result = + DimensionOrdinate::new(v3(point), v3(leader), source.is_ordinate_type_x); + result.definition_point = source.definition_point; + result.base.definition_point = source.base.definition_point; + result.base.text_middle_point = v3(leader); + result.base.insertion_point = result.base.text_middle_point; + result.base.actual_measurement = result.measurement(); + Dimension::Ordinate(result) + } + }; + state.style.apply(dimension.base_mut()); + dimension.base_mut().common.handle = Handle::NULL; + dimension.base_mut().block_name.clear(); + Some(dimension) + } + + fn advance(&mut self, point: DVec3) { + let Some(state) = self.base.as_mut() else { + return; + }; + match &mut state.kind { + ContinueKind::Linear { current, .. } + | ContinueKind::Angular2Ln { current, .. } + | ContinueKind::Angular3Pt { current, .. } => *current = point, + ContinueKind::Ordinate { .. } => {} + } + } + + fn undo_one(&mut self) -> CmdResult { + let Some(previous) = self.history.pop() else { + return CmdResult::NeedPoint; + }; + self.base = Some(previous); + CmdResult::UndoDocument } } @@ -104,110 +288,373 @@ impl CadCommand for DimContinueCommand { } fn prompt(&self) -> String { - if !self.ready { - t!("DIMCONTINUE No base dimension found. Place a dimension first.").into_owned() + if self.base.is_none() { + "DIMCONTINUE Select continued dimension:".to_string() + } else if self + .base + .as_ref() + .is_some_and(|base| matches!(&base.kind, ContinueKind::Ordinate { .. })) + { + "DIMCONTINUE Specify feature location [Undo/Select] :" + .to_string() } } - fn on_point(&mut self, pt: DVec3) -> CmdResult { - if !self.ready { - return CmdResult::Cancel; + fn options(&self) -> Vec { + self.base.as_ref().map_or_else(Vec::new, |base| { + if matches!(&base.kind, ContinueKind::Ordinate { .. }) { + vec![CmdOption::new("Undo", "U"), CmdOption::new("Select", "S")] + } else { + vec![CmdOption::new("Select", "S"), CmdOption::new("Undo", "U")] + } + }) + } + + fn needs_entity_pick(&self) -> bool { + self.base.is_none() + } + + fn entity_pick_highlights_hover(&self) -> bool { + self.base.is_none() + } + + fn inject_before_entity_pick(&self) -> bool { + true + } + + fn inject_picked_entity(&mut self, entity: EntityType) { + self.injected = Some(entity); + } + + fn on_entity_pick(&mut self, _handle: Handle, point: DVec3) -> CmdResult { + let Some(entity) = self.injected.take() else { + return CmdResult::NeedPoint; + }; + let _ = self.install_base(&entity, Some(point)); + CmdResult::NeedPoint + } + + fn on_point(&mut self, point: DVec3) -> CmdResult { + let Some(dimension) = self.build_dimension(point) else { + return CmdResult::NeedPoint; + }; + if let Some(base) = self.base.clone() { + self.history.push(base); } - let p1 = self.chain_p1; - let p2 = pt; - - // Build a new linear dimension. - let mut dim = DimensionLinear::new(v3(p1), v3(p2)); - dim.rotation = self.rotation; - if self.text_rotation.abs() > 1e-9 { - dim.base.text_rotation = self.text_rotation; + self.advance(point); + CmdResult::CommitDimension { + entity: EntityType::Dimension(dimension), + association: DimensionAssociationInput::Infer(None), + preserve_base_style: self.preserve_base_style, + keep_active: true, } - - // Dim line stays collinear with the base: project both extension - // origins onto the base dim line's absolute perpendicular coordinate. - // (A fixed offset from p1 drifts off the base line whenever p1 and p2 - // sit at different perpendicular positions.) (#151) - let d1 = p1 + self.perp * (self.dim_line_perp - p1.dot(self.perp)); - let d2 = p2 + self.perp * (self.dim_line_perp - p2.dot(self.perp)); - dim.definition_point = v3(d1); - dim.base.definition_point = v3(d1); - dim.base.text_middle_point = v3((d1 + d2) * 0.5); - dim.base.insertion_point = dim.base.text_middle_point; - dim.base.actual_measurement = dim.measurement(); - - // Advance chain: next dim's P1 = this dim's P2. - self.chain_p1 = p2; - - CmdResult::CommitEntity(EntityType::Dimension(Dimension::Linear(dim))) } fn on_enter(&mut self) -> CmdResult { - CmdResult::Cancel - } - - fn on_mouse_move(&mut self, pt: DVec3) -> Option { - if !self.ready { - return None; + if self.base.take().is_some() { + CmdResult::NeedPoint + } else { + CmdResult::Cancel } - // Preview / rubber-band geometry feeds an f32 GPU buffer, so drop to f32 - // at this screen-only boundary. - let pt = pt.as_vec3(); - let p1 = self.chain_p1.as_vec3(); - let perp = self.perp.as_vec3(); - let dim_line_perp = self.dim_line_perp as f32; - let dim_line_pt = p1 + perp * (dim_line_perp - p1.dot(perp)); - let dim_line_pt2 = pt + perp * (dim_line_perp - pt.dot(perp)); - Some(WireModel { - 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: "dimcont_preview".into(), - points: vec![ - [p1.x, p1.y, p1.z], - [dim_line_pt.x, dim_line_pt.y, dim_line_pt.z], - [f32::NAN, 0.0, 0.0], - [pt.x, pt.y, pt.z], - [dim_line_pt2.x, dim_line_pt2.y, dim_line_pt2.z], - [f32::NAN, 0.0, 0.0], - [dim_line_pt.x, dim_line_pt.y, dim_line_pt.z], - [dim_line_pt2.x, dim_line_pt2.y, dim_line_pt2.z], - ], - 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(), - }) + } + + fn wants_text_input(&self) -> bool { + self.base.is_some() + } + + fn point_step_accepts_keywords(&self) -> bool { + self.base.is_some() + } + + fn on_text_input(&mut self, text: &str) -> Option { + match text.trim().to_ascii_uppercase().as_str() { + "S" | "SELECT" => { + self.base = None; + Some(CmdResult::NeedPoint) + } + "U" | "UNDO" => Some(self.undo_one()), + _ => None, + } + } + + fn on_undo_step(&mut self) -> Option { + if self.history.is_empty() { + None + } else { + Some(self.undo_one()) + } + } + + fn on_mouse_move(&mut self, point: DVec3) -> Option { + self.build_dimension(point) + .map(|dimension| preview_for_dimension(&dimension)) } } -fn v3(p: DVec3) -> Vector3 { - Vector3::new(p.x, p.y, p.z) +fn selected_linear_end(first: DVec3, second: DVec3, pick: Option) -> DVec3 { + pick.map_or(second, |point| { + if point.distance_squared(first) <= point.distance_squared(second) { + first + } else { + second + } + }) } +fn selected_angular_end(first: DVec3, second: DVec3, pick: Option) -> DVec3 { + pick.map_or(first, |point| { + if point.distance_squared(first) <= point.distance_squared(second) { + first + } else { + second + } + }) +} -// ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["DIMCONTINUE"] }); // DimContinueCommand +fn build_linear( + first: DVec3, + second: DVec3, + rotation: f64, + perpendicular: DVec3, + line_coordinate: f64, +) -> Dimension { + let first_line = first + perpendicular * (line_coordinate - first.dot(perpendicular)); + let second_line = second + perpendicular * (line_coordinate - second.dot(perpendicular)); + let mut result = DimensionLinear::new(v3(first), v3(second)); + result.rotation = rotation; + result.definition_point = v3(second_line); + result.base.definition_point = v3(second_line); + result.base.text_middle_point = v3((first_line + second_line) * 0.5); + result.base.insertion_point = result.base.text_middle_point; + result.base.actual_measurement = result.measurement(); + Dimension::Linear(result) +} + +fn angular2_vertex(source: &DimensionAngular2Ln) -> DVec3 { + let first = dv(source.first_point); + let first_direction = dv(source.second_point) - first; + let second = dv(source.angle_vertex); + let second_direction = dv(source.definition_point) - second; + line_intersection(first, first_direction, second, second_direction).unwrap_or(second) +} + +fn line_intersection(a: DVec3, b: DVec3, c: DVec3, d: DVec3) -> Option { + let cross = b.x * d.y - b.y * d.x; + if cross.abs() <= 1.0e-12 { + return None; + } + let delta = c - a; + Some(a + b * ((delta.x * d.y - delta.y * d.x) / cross)) +} + +fn angular_definition_and_text( + vertex: DVec3, + previous: DVec3, + moving: DVec3, + radius: f64, +) -> (DVec3, DVec3) { + let start = previous.y.atan2(previous.x); + let mut sweep = moving.y.atan2(moving.x) - start; + while sweep <= -std::f64::consts::PI { + sweep += std::f64::consts::TAU; + } + while sweep > std::f64::consts::PI { + sweep -= std::f64::consts::TAU; + } + let point_at = |fraction: f64| { + let angle = start + sweep * fraction; + vertex + DVec3::new(angle.cos(), angle.sin(), 0.0) * radius + }; + (point_at(2.0 / 3.0), point_at(0.5)) +} + +fn preview_for_dimension(dimension: &Dimension) -> WireModel { + let mut points = Vec::<[f64; 3]>::new(); + match dimension { + Dimension::Linear(dimension) => linear_preview( + &mut points, + dv(dimension.first_point), + dv(dimension.second_point), + dimension.rotation, + dv(dimension.definition_point), + dv(dimension.base.text_middle_point), + dimension.base.actual_measurement, + ), + Dimension::Angular2Ln(dimension) => { + let vertex = angular2_vertex(dimension); + angular_preview( + &mut points, + vertex, + dv(dimension.second_point) - vertex, + dv(dimension.definition_point) - vertex, + dv(dimension.dimension_arc), + dv(dimension.base.text_middle_point), + dimension.base.actual_measurement, + ); + } + Dimension::Angular3Pt(dimension) => angular_preview( + &mut points, + dv(dimension.angle_vertex), + dv(dimension.first_point) - dv(dimension.angle_vertex), + dv(dimension.second_point) - dv(dimension.angle_vertex), + dv(dimension.definition_point), + dv(dimension.base.text_middle_point), + dimension.base.actual_measurement, + ), + Dimension::Ordinate(dimension) => ordinate_preview( + &mut points, + dv(dimension.feature_location), + dv(dimension.leader_endpoint), + dimension.base.actual_measurement, + ), + _ => {} + } + WireModel::solid_f64( + "dimcontinue_preview".to_string(), + points, + WireModel::CYAN, + false, + ) +} + +fn linear_preview( + points: &mut Vec<[f64; 3]>, + first: DVec3, + second: DVec3, + rotation: f64, + definition: DVec3, + text: DVec3, + measurement: f64, +) { + let axis = DVec3::new(rotation.cos(), rotation.sin(), 0.0); + let perpendicular = DVec3::new(-axis.y, axis.x, 0.0); + let coordinate = definition.dot(perpendicular); + let first_line = first + perpendicular * (coordinate - first.dot(perpendicular)); + let second_line = second + perpendicular * (coordinate - second.dot(perpendicular)); + segment(points, first, first_line); + segment(points, second, second_line); + segment(points, first_line, second_line); + arrow(points, first_line, axis, perpendicular, first_line.distance(second_line)); + arrow(points, second_line, -axis, perpendicular, first_line.distance(second_line)); + text_box(points, text, axis, perpendicular, measurement); +} + +fn angular_preview( + points: &mut Vec<[f64; 3]>, + vertex: DVec3, + first: DVec3, + second: DVec3, + arc_point: DVec3, + text: DVec3, + measurement: f64, +) { + let radius = (arc_point - vertex).length().max(1.0e-9); + let start = first.y.atan2(first.x); + let mut sweep = second.y.atan2(second.x) - start; + while sweep <= -std::f64::consts::PI { + sweep += std::f64::consts::TAU; + } + while sweep > std::f64::consts::PI { + sweep -= std::f64::consts::TAU; + } + let first_end = vertex + normalized_or(first, DVec3::X) * radius; + let second_end = vertex + normalized_or(second, DVec3::Y) * radius; + segment(points, vertex, first_end); + segment(points, vertex, second_end); + let arc = cadkernel::geom2d::tessellate::arc( + [vertex.x, vertex.y], + radius, + start, + start + sweep, + vertex.z, + cadkernel::geom2d::tessellate::DEFAULT_SEGMENTS_PER_RADIAN, + ); + points.extend(arc); + points.push([f64::NAN, f64::NAN, f64::NAN]); + let sign = sweep.signum(); + let start_tangent = DVec3::new(-start.sin(), start.cos(), 0.0) * sign; + let end_angle = start + sweep; + let end_tangent = DVec3::new(end_angle.sin(), -end_angle.cos(), 0.0) * sign; + arrow(points, first_end, start_tangent, normalized_or(first, DVec3::X), radius); + arrow(points, second_end, end_tangent, normalized_or(second, DVec3::Y), radius); + let middle = start + sweep * 0.5; + let text_axis = normalized_or(DVec3::new(-middle.sin(), middle.cos(), 0.0), DVec3::X); + let text_perpendicular = DVec3::new(-text_axis.y, text_axis.x, 0.0); + text_box(points, text, text_axis, text_perpendicular, measurement); +} + +fn ordinate_preview( + points: &mut Vec<[f64; 3]>, + feature: DVec3, + leader: DVec3, + measurement: f64, +) { + segment(points, feature, leader); + let axis = normalized_or(leader - feature, DVec3::Y); + let perpendicular = DVec3::new(-axis.y, axis.x, 0.0); + arrow(points, feature, axis, perpendicular, feature.distance(leader)); + text_box(points, leader, perpendicular, axis, measurement); +} + +fn arrow( + points: &mut Vec<[f64; 3]>, + tip: DVec3, + inward: DVec3, + perpendicular: DVec3, + span: f64, +) { + let inward = normalized_or(inward, DVec3::X); + let perpendicular = normalized_or(perpendicular, DVec3::Y); + let size = span.clamp(1.0, 100.0) * 0.035; + let back = tip + inward * size; + segment(points, tip, back + perpendicular * size * 0.4); + segment(points, tip, back - perpendicular * size * 0.4); +} + +fn text_box( + points: &mut Vec<[f64; 3]>, + center: DVec3, + axis: DVec3, + perpendicular: DVec3, + measurement: f64, +) { + let digits = if measurement.abs() < 1.0 { + 1.0 + } else { + measurement.abs().log10().floor() + 1.0 + }; + let half_width = (digits + 2.0) * 0.12; + let half_height = 0.16; + let a = center - axis * half_width - perpendicular * half_height; + let b = center + axis * half_width - perpendicular * half_height; + let c = center + axis * half_width + perpendicular * half_height; + let d = center - axis * half_width + perpendicular * half_height; + points.extend([a.to_array(), b.to_array(), c.to_array(), d.to_array(), a.to_array()]); + points.push([f64::NAN, f64::NAN, f64::NAN]); +} + +fn segment(points: &mut Vec<[f64; 3]>, first: DVec3, second: DVec3) { + points.push(first.to_array()); + points.push(second.to_array()); + points.push([f64::NAN, f64::NAN, f64::NAN]); +} + +fn normalized_or(value: DVec3, fallback: DVec3) -> DVec3 { + if value.length_squared() > 1.0e-18 { + value.normalize() + } else { + fallback + } +} + +fn dv(point: Vector3) -> DVec3 { + DVec3::new(point.x, point.y, point.z) +} + +fn v3(point: DVec3) -> Vector3 { + Vector3::new(point.x, point.y, point.z) +} + +inventory::submit!(crate::command::CommandRegistration { names: &["DIMCONTINUE"] }); diff --git a/src/modules/annotate/jogged_radius_dim.rs b/src/modules/annotate/jogged_radius_dim.rs index 2363082b1..4ce9f8d5d 100644 --- a/src/modules/annotate/jogged_radius_dim.rs +++ b/src/modules/annotate/jogged_radius_dim.rs @@ -140,6 +140,7 @@ impl JoggedRadiusDimensionCommand { ))), association, preserve_base_style: false, + keep_active: false, } } } diff --git a/src/modules/annotate/linear_dim.rs b/src/modules/annotate/linear_dim.rs index fd6d6d6c5..7036e0950 100644 --- a/src/modules/annotate/linear_dim.rs +++ b/src/modules/annotate/linear_dim.rs @@ -188,6 +188,7 @@ impl CadCommand for LinearDimensionCommand { entity, association: DimensionAssociationInput::Infer(self.source_handle), preserve_base_style: false, + keep_active: false, } } } diff --git a/src/modules/annotate/ordinate_dim.rs b/src/modules/annotate/ordinate_dim.rs index 221523c7f..9343249d9 100644 --- a/src/modules/annotate/ordinate_dim.rs +++ b/src/modules/annotate/ordinate_dim.rs @@ -117,6 +117,7 @@ impl CadCommand for OrdinateDimCommand { )), association: DimensionAssociationInput::Infer(None), preserve_base_style: false, + keep_active: false, } } } diff --git a/src/modules/annotate/radius_dim.rs b/src/modules/annotate/radius_dim.rs index 10681bad8..2fa565352 100644 --- a/src/modules/annotate/radius_dim.rs +++ b/src/modules/annotate/radius_dim.rs @@ -112,6 +112,7 @@ impl CadCommand for RadiusDimensionCommand { entity: source_plane.place_entity(EntityType::Dimension(Dimension::Radius(dim))), association: DimensionAssociationInput::Infer(self.source_handle), preserve_base_style: false, + keep_active: false, } } } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 161d2a454..6262a24ea 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1420,10 +1420,10 @@ pub struct Scene { pub selection: Rc>, /// The CAD document — single source of truth for all entities. pub document: CadDocument, - /// Last dimension created during this application session. This is - /// intentionally not reconstructed from file handles: continuation and - /// baseline commands must not treat an old, merely high-handle dimension - /// loaded from disk as the user's most recently created dimension. + /// Last linear, aligned, angular, or ordinate dimension created during this + /// application session. This is intentionally not reconstructed from file + /// handles: continuation commands must not treat an old, merely high-handle + /// dimension loaded from disk as the user's most recently created dimension. pub last_created_dimension: Option, /// File-open-prepared index for non-graphical semantic object lookups. pub(crate) object_data_cache: crate::entities::object_data::ObjectDataCache, From 38b02164e8b2327f9f05feceda21966cf341cef0 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 28 Aug 2026 18:52:57 +0300 Subject: [PATCH 2/2] fix(dimension): repair continued workflows --- src/app/command_driver.rs | 23 +- src/app/commands/dim.rs | 1 + src/command.rs | 4 +- src/modules/annotate/aligned_dim.rs | 2 +- src/modules/annotate/angular_dim.rs | 2 +- src/modules/annotate/arc_length_dim.rs | 2 +- src/modules/annotate/diameter_dim.rs | 2 +- src/modules/annotate/dim_continue.rs | 695 ++++++++++++++-------- src/modules/annotate/jogged_radius_dim.rs | 2 +- src/modules/annotate/linear_dim.rs | 2 +- src/modules/annotate/ordinate_dim.rs | 2 +- src/modules/annotate/radius_dim.rs | 2 +- src/scene/dimension_assoc.rs | 73 ++- 13 files changed, 510 insertions(+), 302 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 3e7e4bafb..cf4a960a3 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -856,7 +856,7 @@ impl OpenCADStudio { .infer_dimension_sources(handle); self.tabs[i] .scene - .attach_dimension_association(handle, sources.to_vec()); + .attach_dimension_association(handle, sources); } } self.tabs[i].dirty = true; @@ -1269,7 +1269,7 @@ impl OpenCADStudio { .infer_dimension_sources(handle); self.tabs[i] .scene - .attach_dimension_association(handle, sources.to_vec()); + .attach_dimension_association(handle, sources); } } self.tabs[i].dirty = true; @@ -1288,7 +1288,7 @@ impl OpenCADStudio { mut entity, association, preserve_base_style, - keep_active, + continue_command, } => { let label = self.history_label_from_active_cmd(i, "DIMENSION"); let association_mode = self.tabs[i] @@ -1379,8 +1379,6 @@ impl OpenCADStudio { self.tabs[i] .scene .infer_dimension_sources(handle) - .into_iter() - .collect() }, |source| { if single_source_dimension { @@ -1416,18 +1414,21 @@ impl OpenCADStudio { self.tabs[i].dirty = true; self.tabs[i].scene.clear_preview_wire(); self.tabs[i].snap_result = None; - if keep_active { - let prompt = self.tabs[i].active_cmd.as_ref().map(|command| command.prompt()); - if let Some(prompt) = prompt { + if let Some(pd) = pending { + self.commit_undo_delta(i, pd); + } + if continue_command { + if let Some(prompt) = self.tabs[i] + .active_cmd + .as_ref() + .map(|cmd| cmd.prompt()) + { self.command_line.push_info(&prompt); } } else { self.tabs[i].active_cmd = None; self.restore_pre_cmd_tangent(); } - if let Some(pd) = pending { - self.commit_undo_delta(i, pd); - } } CmdResult::CommitSolid { entity, diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs index 90da46a80..042140ed5 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -649,6 +649,7 @@ impl OpenCADStudio { let scene = &self.tabs[i].scene; let recent = scene .last_created_dimension + .filter(|handle| scene.entity_belongs_to_active_space(*handle)) .and_then(|handle| scene.document.get_entity(handle)) .cloned(); let cmd = DimContinueCommand::new( diff --git a/src/command.rs b/src/command.rs index 968c9d29e..cd7544513 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1251,8 +1251,8 @@ pub enum CmdResult { /// Retain the source dimension's layer and style instead of replacing /// them with the current creation defaults (DIMCONTINUEMODE=1). preserve_base_style: bool, - /// Keep the interactive command active after this dimension is committed. - keep_active: bool, + /// Keep collecting points after the dimension is committed. + continue_command: bool, }, /// Commit a Model-tab 3D solid: the acadrust entity (for selection / /// persistence) plus its B-rep (cached for boolean ops + shaded diff --git a/src/modules/annotate/aligned_dim.rs b/src/modules/annotate/aligned_dim.rs index 5765e4759..9ecfa7c70 100644 --- a/src/modules/annotate/aligned_dim.rs +++ b/src/modules/annotate/aligned_dim.rs @@ -136,7 +136,7 @@ impl CadCommand for AlignedDimensionCommand { entity: self.plane.place_entity(EntityType::Dimension(Dimension::Aligned(dim))), association: DimensionAssociationInput::Infer(self.source_handle), preserve_base_style: false, - keep_active: false, + continue_command: false, } } } diff --git a/src/modules/annotate/angular_dim.rs b/src/modules/annotate/angular_dim.rs index 9e7547b96..f23e3b555 100644 --- a/src/modules/annotate/angular_dim.rs +++ b/src/modules/annotate/angular_dim.rs @@ -235,7 +235,7 @@ impl AngularDimensionCommand { entity, association: DimensionAssociationInput::Explicit(self.source_refs.clone()), preserve_base_style: false, - keep_active: false, + continue_command: false, } } diff --git a/src/modules/annotate/arc_length_dim.rs b/src/modules/annotate/arc_length_dim.rs index 4ae6ea24d..bbdfaf412 100644 --- a/src/modules/annotate/arc_length_dim.rs +++ b/src/modules/annotate/arc_length_dim.rs @@ -266,7 +266,7 @@ impl ArcLengthDimensionCommand { entity: plane.place_entity(EntityType::Dimension(Dimension::Arc(dimension))), association, preserve_base_style: false, - keep_active: false, + continue_command: false, } } } diff --git a/src/modules/annotate/diameter_dim.rs b/src/modules/annotate/diameter_dim.rs index 36286d340..19dfcb1ca 100644 --- a/src/modules/annotate/diameter_dim.rs +++ b/src/modules/annotate/diameter_dim.rs @@ -114,7 +114,7 @@ impl CadCommand for DiameterDimensionCommand { .place_entity(EntityType::Dimension(Dimension::Diameter(dim))), association: DimensionAssociationInput::Infer(self.source_handle), preserve_base_style: false, - keep_active: false, + continue_command: false, } } } diff --git a/src/modules/annotate/dim_continue.rs b/src/modules/annotate/dim_continue.rs index 4d4f99b7b..45d69520c 100644 --- a/src/modules/annotate/dim_continue.rs +++ b/src/modules/annotate/dim_continue.rs @@ -1,11 +1,14 @@ -//! Repeating continued dimensions from the latest session dimension or a picked base. - use acadrust::entities::{ Dimension, DimensionAngular2Ln, DimensionAngular3Pt, DimensionBase, DimensionLinear, DimensionOrdinate, }; use acadrust::types::Vector3; use acadrust::{EntityType, Handle}; +use cadkernel::geom2d::{ + arc_span, closest_point, intersect, nearest_of, Arc, Curve, Ray, Tolerance, Transform, Vec2, + XLine, +}; +use cadkernel::space::Plane; use glam::DVec3; use crate::command::{CadCommand, CmdOption, CmdResult, DimensionAssociationInput}; @@ -55,25 +58,25 @@ impl SourceStyle { #[derive(Clone)] enum ContinueKind { Linear { - current: DVec3, + current: Vec2, rotation: f64, - perpendicular: DVec3, + perpendicular: Vec2, line_coordinate: f64, }, Angular2Ln { - source: DimensionAngular2Ln, - vertex: DVec3, - current: DVec3, + vertex: Vec2, + current: Vec2, radius: f64, }, Angular3Pt { - source: DimensionAngular3Pt, - vertex: DVec3, - current: DVec3, + vertex: Vec2, + current: Vec2, radius: f64, }, Ordinate { - source: DimensionOrdinate, + definition: Vec2, + leader: Vec2, + is_x: bool, }, } @@ -81,6 +84,7 @@ enum ContinueKind { struct ContinueState { kind: ContinueKind, style: SourceStyle, + plane: Plane, } pub struct DimContinueCommand { @@ -104,78 +108,96 @@ impl DimContinueCommand { command } - fn install_base(&mut self, entity: &EntityType, pick: Option) -> bool { + fn install_base(&mut self, entity: &EntityType, pick_world: Option) -> bool { let EntityType::Dimension(dimension) = entity else { return false; }; let style = SourceStyle::from_base(dimension.base()); + let plane = plane_from_normal(dimension.base().definition_point, style.normal); + let pick = pick_world.and_then(|point| plane.project(point.to_array()).map(Vec2::from)); let kind = match dimension { Dimension::Linear(source) => { let rotation = source.rotation; - let perpendicular = DVec3::new(-rotation.sin(), rotation.cos(), 0.0); - let current = selected_linear_end( - dv(source.first_point), - dv(source.second_point), - pick, - ); + let perpendicular = Vec2::new(-rotation.sin(), rotation.cos()); ContinueKind::Linear { - current, + current: selected_linear_end( + project(&plane, source.first_point), + project(&plane, source.second_point), + pick, + ), rotation, perpendicular, - line_coordinate: dv(source.base.definition_point).dot(perpendicular), + line_coordinate: project(&plane, source.base.definition_point) + .dot(perpendicular), } } Dimension::Aligned(source) => { - let first = dv(source.first_point); - let second = dv(source.second_point); - let delta = second - first; - if delta.length_squared() <= 1.0e-18 { + let first = project(&plane, source.first_point); + let second = project(&plane, source.second_point); + let Some(direction) = (second - first).normalize() else { return false; - } - let rotation = delta.y.atan2(delta.x); - let perpendicular = DVec3::new(-rotation.sin(), rotation.cos(), 0.0); + }; + let rotation = direction.angle(); + let perpendicular = direction.perpendicular(); ContinueKind::Linear { current: selected_linear_end(first, second, pick), rotation, perpendicular, - line_coordinate: dv(source.base.definition_point).dot(perpendicular), + line_coordinate: project(&plane, source.base.definition_point) + .dot(perpendicular), } } Dimension::Angular2Ln(source) => { - let vertex = angular2_vertex(source); - let first = dv(source.second_point); - let second = dv(source.definition_point); + let lines = [ + [ + project(&plane, source.first_point), + project(&plane, source.second_point), + ], + [ + project(&plane, source.angle_vertex), + project(&plane, source.definition_point), + ], + ]; + let vertex = angular2_vertex(lines).unwrap_or(lines[1][0]); ContinueKind::Angular2Ln { - source: source.clone(), vertex, - current: selected_angular_end(first, second, pick), - radius: (dv(source.dimension_arc) - vertex).length().max(1.0e-9), + current: selected_angular_end(vertex, [lines[0][1], lines[1][1]], pick), + radius: project(&plane, source.dimension_arc) + .distance(vertex) + .max(1.0e-9), } } Dimension::Angular3Pt(source) => { - let vertex = dv(source.angle_vertex); + let vertex = project(&plane, source.angle_vertex); ContinueKind::Angular3Pt { - source: source.clone(), vertex, current: selected_angular_end( - dv(source.first_point), - dv(source.second_point), + vertex, + [ + project(&plane, source.first_point), + project(&plane, source.second_point), + ], pick, ), - radius: (dv(source.definition_point) - vertex).length().max(1.0e-9), + radius: project(&plane, source.definition_point) + .distance(vertex) + .max(1.0e-9), } } Dimension::Ordinate(source) => ContinueKind::Ordinate { - source: source.clone(), + definition: project(&plane, source.definition_point), + leader: project(&plane, source.leader_endpoint), + is_x: source.is_ordinate_type_x, }, _ => return false, }; - self.base = Some(ContinueState { kind, style }); + self.base = Some(ContinueState { kind, style, plane }); true } - fn build_dimension(&self, point: DVec3) -> Option { + fn build_dimension(&self, point_world: DVec3) -> Option { let state = self.base.as_ref()?; + let point = state.plane.project(point_world.to_array()).map(Vec2::from)?; let mut dimension = match &state.kind { ContinueKind::Linear { current, @@ -183,6 +205,7 @@ impl DimContinueCommand { perpendicular, line_coordinate, } => build_linear( + state.plane, *current, point, *rotation, @@ -190,78 +213,88 @@ impl DimContinueCommand { *line_coordinate, ), ContinueKind::Angular2Ln { - source, vertex, current, radius, } => { - let moving = point - *vertex; let previous = *current - *vertex; - if moving.length_squared() <= 1.0e-18 || previous.length_squared() <= 1.0e-18 { - return None; - } - let mut result = source.clone(); - result.first_point = v3(*vertex); - result.second_point = v3(point); - result.angle_vertex = v3(*vertex); - result.definition_point = v3(*current); + let moving = point - *vertex; let (definition, text) = - angular_definition_and_text(*vertex, previous, moving, *radius); - result.dimension_arc = v3(definition); + angular_definition_and_text(*vertex, previous, moving, *radius)?; + let mut result = DimensionAngular2Ln::default(); + result.first_point = world(state.plane, *vertex); + result.second_point = world(state.plane, *current); + result.angle_vertex = world(state.plane, *vertex); + result.definition_point = world(state.plane, point); + result.dimension_arc = world(state.plane, definition); result.base.definition_point = result.dimension_arc; - result.base.text_middle_point = v3(text); + result.base.text_middle_point = world(state.plane, text); result.base.insertion_point = result.base.text_middle_point; - result.base.actual_measurement = result.measurement_degrees(); Dimension::Angular2Ln(result) } ContinueKind::Angular3Pt { - source, vertex, current, radius, } => { - let moving = point - *vertex; let previous = *current - *vertex; - if moving.length_squared() <= 1.0e-18 || previous.length_squared() <= 1.0e-18 { - return None; - } - let mut result = source.clone(); - result.angle_vertex = v3(*vertex); - result.first_point = v3(point); - result.second_point = v3(*current); + let moving = point - *vertex; let (definition, text) = - angular_definition_and_text(*vertex, previous, moving, *radius); - result.definition_point = v3(definition); + angular_definition_and_text(*vertex, previous, moving, *radius)?; + let mut result = DimensionAngular3Pt::new( + world(state.plane, *vertex), + world(state.plane, *current), + world(state.plane, point), + ); + result.definition_point = world(state.plane, definition); result.base.definition_point = result.definition_point; - result.base.text_middle_point = v3(text); + result.base.text_middle_point = world(state.plane, text); result.base.insertion_point = result.base.text_middle_point; - result.base.actual_measurement = result.measurement_degrees(); Dimension::Angular3Pt(result) } - ContinueKind::Ordinate { source } => { - let source_leader = dv(source.leader_endpoint); - let leader = if source.is_ordinate_type_x { - DVec3::new(point.x, source_leader.y, source_leader.z) + ContinueKind::Ordinate { + definition, + leader, + is_x, + } => { + let to_dimension = Transform::rotation(-state.style.horizontal_direction); + let from_dimension = Transform::rotation(state.style.horizontal_direction); + let feature = Vec2::from(to_dimension.apply_point(point.into())); + let source_leader = Vec2::from(to_dimension.apply_point((*leader).into())); + let leader = if *is_x { + Vec2::new(feature.x, source_leader.y) } else { - DVec3::new(source_leader.x, point.y, source_leader.z) + Vec2::new(source_leader.x, feature.y) }; - let mut result = - DimensionOrdinate::new(v3(point), v3(leader), source.is_ordinate_type_x); - result.definition_point = source.definition_point; - result.base.definition_point = source.base.definition_point; - result.base.text_middle_point = v3(leader); + let leader = Vec2::from(from_dimension.apply_point(leader.into())); + let mut result = DimensionOrdinate::new( + world(state.plane, point), + world(state.plane, leader), + *is_x, + ); + result.definition_point = world(state.plane, *definition); + result.base.definition_point = result.definition_point; + result.base.text_middle_point = result.leader_endpoint; result.base.insertion_point = result.base.text_middle_point; - result.base.actual_measurement = result.measurement(); Dimension::Ordinate(result) } }; state.style.apply(dimension.base_mut()); + refresh_measurement(&mut dimension, state.plane); dimension.base_mut().common.handle = Handle::NULL; dimension.base_mut().block_name.clear(); Some(dimension) } - fn advance(&mut self, point: DVec3) { + fn advance(&mut self, point_world: DVec3) { + let Some(point) = self + .base + .as_ref() + .and_then(|state| state.plane.project(point_world.to_array())) + .map(Vec2::from) + else { + return; + }; let Some(state) = self.base.as_mut() else { return; }; @@ -348,7 +381,7 @@ impl CadCommand for DimContinueCommand { entity: EntityType::Dimension(dimension), association: DimensionAssociationInput::Infer(None), preserve_base_style: self.preserve_base_style, - keep_active: true, + continue_command: true, } } @@ -393,7 +426,26 @@ impl CadCommand for DimContinueCommand { } } -fn selected_linear_end(first: DVec3, second: DVec3, pick: Option) -> DVec3 { +fn build_linear( + plane: Plane, + first: Vec2, + second: Vec2, + rotation: f64, + perpendicular: Vec2, + line_coordinate: f64, +) -> Dimension { + let first_line = project_to_line(first, perpendicular, line_coordinate); + let second_line = project_to_line(second, perpendicular, line_coordinate); + let mut result = DimensionLinear::new(world(plane, first), world(plane, second)); + result.rotation = rotation; + result.definition_point = world(plane, second_line); + result.base.definition_point = result.definition_point; + result.base.text_middle_point = world(plane, first_line.lerp(second_line, 0.5)); + result.base.insertion_point = result.base.text_middle_point; + Dimension::Linear(result) +} + +fn selected_linear_end(first: Vec2, second: Vec2, pick: Option) -> Vec2 { pick.map_or(second, |point| { if point.distance_squared(first) <= point.distance_squared(second) { first @@ -403,112 +455,157 @@ fn selected_linear_end(first: DVec3, second: DVec3, pick: Option) -> DVec }) } -fn selected_angular_end(first: DVec3, second: DVec3, pick: Option) -> DVec3 { - pick.map_or(first, |point| { - if point.distance_squared(first) <= point.distance_squared(second) { - first - } else { - second - } - }) +fn selected_angular_end(vertex: Vec2, ends: [Vec2; 2], pick: Option) -> Vec2 { + let Some(pick) = pick else { + return ends[0]; + }; + let rays = ends.map(|end| { + Curve::Ray(Ray { + origin: vertex.into(), + direction: (end - vertex).into(), + }) + }); + nearest_of(rays.iter(), pick.into()) + .map(|(index, _)| ends[index]) + .unwrap_or(ends[0]) } -fn build_linear( - first: DVec3, - second: DVec3, - rotation: f64, - perpendicular: DVec3, - line_coordinate: f64, -) -> Dimension { - let first_line = first + perpendicular * (line_coordinate - first.dot(perpendicular)); - let second_line = second + perpendicular * (line_coordinate - second.dot(perpendicular)); - let mut result = DimensionLinear::new(v3(first), v3(second)); - result.rotation = rotation; - result.definition_point = v3(second_line); - result.base.definition_point = v3(second_line); - result.base.text_middle_point = v3((first_line + second_line) * 0.5); - result.base.insertion_point = result.base.text_middle_point; - result.base.actual_measurement = result.measurement(); - Dimension::Linear(result) -} - -fn angular2_vertex(source: &DimensionAngular2Ln) -> DVec3 { - let first = dv(source.first_point); - let first_direction = dv(source.second_point) - first; - let second = dv(source.angle_vertex); - let second_direction = dv(source.definition_point) - second; - line_intersection(first, first_direction, second, second_direction).unwrap_or(second) -} - -fn line_intersection(a: DVec3, b: DVec3, c: DVec3, d: DVec3) -> Option { - let cross = b.x * d.y - b.y * d.x; - if cross.abs() <= 1.0e-12 { - return None; - } - let delta = c - a; - Some(a + b * ((delta.x * d.y - delta.y * d.x) / cross)) +fn angular2_vertex(lines: [[Vec2; 2]; 2]) -> Option { + let first = Curve::XLine(XLine { + base: lines[0][0].into(), + direction: (lines[0][1] - lines[0][0]).into(), + }); + let second = Curve::XLine(XLine { + base: lines[1][0].into(), + direction: (lines[1][1] - lines[1][0]).into(), + }); + let scale = (lines[0][1] - lines[0][0]) + .length() + .max((lines[1][1] - lines[1][0]).length()) + .max(1.0); + intersect(&first, &second, Tolerance::new(scale * 1.0e-9)) + .first() + .map(|crossing| Vec2::from(crossing.point)) } fn angular_definition_and_text( - vertex: DVec3, - previous: DVec3, - moving: DVec3, + vertex: Vec2, + previous: Vec2, + moving: Vec2, radius: f64, -) -> (DVec3, DVec3) { - let start = previous.y.atan2(previous.x); - let mut sweep = moving.y.atan2(moving.x) - start; - while sweep <= -std::f64::consts::PI { - sweep += std::f64::consts::TAU; +) -> Option<(Vec2, Vec2)> { + let arc = angular_arc(vertex, previous, moving, radius)?; + Some(( + Vec2::from(arc.point_at(2.0 / 3.0)), + Vec2::from(arc.point_at(0.5)), + )) +} + +fn angular_arc(vertex: Vec2, first: Vec2, second: Vec2, radius: f64) -> Option { + let first = first.normalize()?; + let second = second.normalize()?; + if first.distance_squared(second) <= 1.0e-18 { + return None; } - while sweep > std::f64::consts::PI { - sweep -= std::f64::consts::TAU; + let first_angle = first.angle(); + let second_angle = second.angle(); + let (start_angle, end_angle) = + if arc_span(first_angle, second_angle) <= std::f64::consts::PI { + (first_angle, second_angle) + } else { + (second_angle, first_angle) + }; + Some(Curve::Arc(Arc { + centre: vertex.into(), + radius, + start_angle, + end_angle, + })) +} + +fn refresh_measurement(dimension: &mut Dimension, plane: Plane) { + match dimension { + Dimension::Linear(value) => { + let first = project(&plane, value.first_point); + let second = project(&plane, value.second_point); + let axis = Vec2::new(value.rotation.cos(), value.rotation.sin()); + value.base.actual_measurement = (second - first).dot(axis).abs(); + } + Dimension::Angular2Ln(value) => { + value.base.actual_measurement = value.measurement_degrees() + } + Dimension::Angular3Pt(value) => { + value.base.actual_measurement = value.measurement_degrees() + } + Dimension::Ordinate(value) => value.refresh_measurement(), + _ => {} } - let point_at = |fraction: f64| { - let angle = start + sweep * fraction; - vertex + DVec3::new(angle.cos(), angle.sin(), 0.0) * radius - }; - (point_at(2.0 / 3.0), point_at(0.5)) } fn preview_for_dimension(dimension: &Dimension) -> WireModel { let mut points = Vec::<[f64; 3]>::new(); match dimension { - Dimension::Linear(dimension) => linear_preview( - &mut points, - dv(dimension.first_point), - dv(dimension.second_point), - dimension.rotation, - dv(dimension.definition_point), - dv(dimension.base.text_middle_point), - dimension.base.actual_measurement, - ), - Dimension::Angular2Ln(dimension) => { - let vertex = angular2_vertex(dimension); - angular_preview( + Dimension::Linear(value) => { + let plane = plane_from_normal(value.first_point, value.base.normal); + linear_preview( &mut points, + plane, + project(&plane, value.first_point), + project(&plane, value.second_point), + value.rotation, + project(&plane, value.definition_point), + project(&plane, value.base.text_middle_point), + value.base.actual_measurement, + ); + } + Dimension::Angular2Ln(value) => { + let plane = plane_from_normal(value.first_point, value.base.normal); + let lines = [ + [ + project(&plane, value.first_point), + project(&plane, value.second_point), + ], + [ + project(&plane, value.angle_vertex), + project(&plane, value.definition_point), + ], + ]; + let vertex = angular2_vertex(lines).unwrap_or(lines[1][0]); + angular_preview( + &mut points, + plane, vertex, - dv(dimension.second_point) - vertex, - dv(dimension.definition_point) - vertex, - dv(dimension.dimension_arc), - dv(dimension.base.text_middle_point), - dimension.base.actual_measurement, + lines[0][1] - vertex, + lines[1][1] - vertex, + project(&plane, value.dimension_arc), + project(&plane, value.base.text_middle_point), + value.base.actual_measurement, + ); + } + Dimension::Angular3Pt(value) => { + let plane = plane_from_normal(value.angle_vertex, value.base.normal); + let vertex = project(&plane, value.angle_vertex); + angular_preview( + &mut points, + plane, + vertex, + project(&plane, value.first_point) - vertex, + project(&plane, value.second_point) - vertex, + project(&plane, value.definition_point), + project(&plane, value.base.text_middle_point), + value.base.actual_measurement, + ); + } + Dimension::Ordinate(value) => { + let plane = plane_from_normal(value.definition_point, value.base.normal); + ordinate_preview( + &mut points, + plane, + project(&plane, value.feature_location), + project(&plane, value.leader_endpoint), + value.base.actual_measurement, ); } - Dimension::Angular3Pt(dimension) => angular_preview( - &mut points, - dv(dimension.angle_vertex), - dv(dimension.first_point) - dv(dimension.angle_vertex), - dv(dimension.second_point) - dv(dimension.angle_vertex), - dv(dimension.definition_point), - dv(dimension.base.text_middle_point), - dimension.base.actual_measurement, - ), - Dimension::Ordinate(dimension) => ordinate_preview( - &mut points, - dv(dimension.feature_location), - dv(dimension.leader_endpoint), - dimension.base.actual_measurement, - ), _ => {} } WireModel::solid_f64( @@ -521,103 +618,166 @@ fn preview_for_dimension(dimension: &Dimension) -> WireModel { fn linear_preview( points: &mut Vec<[f64; 3]>, - first: DVec3, - second: DVec3, + plane: Plane, + first: Vec2, + second: Vec2, rotation: f64, - definition: DVec3, - text: DVec3, + definition: Vec2, + text: Vec2, measurement: f64, ) { - let axis = DVec3::new(rotation.cos(), rotation.sin(), 0.0); - let perpendicular = DVec3::new(-axis.y, axis.x, 0.0); + let axis = Vec2::new(rotation.cos(), rotation.sin()); + let perpendicular = axis.perpendicular(); let coordinate = definition.dot(perpendicular); - let first_line = first + perpendicular * (coordinate - first.dot(perpendicular)); - let second_line = second + perpendicular * (coordinate - second.dot(perpendicular)); - segment(points, first, first_line); - segment(points, second, second_line); - segment(points, first_line, second_line); - arrow(points, first_line, axis, perpendicular, first_line.distance(second_line)); - arrow(points, second_line, -axis, perpendicular, first_line.distance(second_line)); - text_box(points, text, axis, perpendicular, measurement); + let first_line = project_to_line(first, perpendicular, coordinate); + let second_line = project_to_line(second, perpendicular, coordinate); + segment(points, plane, first, first_line); + segment(points, plane, second, second_line); + segment(points, plane, first_line, second_line); + arrow( + points, + plane, + first_line, + axis, + perpendicular, + first_line.distance(second_line), + ); + arrow( + points, + plane, + second_line, + -axis, + perpendicular, + first_line.distance(second_line), + ); + text_box(points, plane, text, axis, perpendicular, measurement); } fn angular_preview( points: &mut Vec<[f64; 3]>, - vertex: DVec3, - first: DVec3, - second: DVec3, - arc_point: DVec3, - text: DVec3, + plane: Plane, + vertex: Vec2, + first: Vec2, + second: Vec2, + arc_point: Vec2, + text: Vec2, measurement: f64, ) { - let radius = (arc_point - vertex).length().max(1.0e-9); - let start = first.y.atan2(first.x); - let mut sweep = second.y.atan2(second.x) - start; - while sweep <= -std::f64::consts::PI { - sweep += std::f64::consts::TAU; + let radius = arc_point.distance(vertex).max(1.0e-9); + let Some(first_direction) = first.normalize() else { + return; + }; + let Some(second_direction) = second.normalize() else { + return; + }; + let Some(arc) = angular_arc(vertex, first, second, radius) else { + return; + }; + segment(points, plane, vertex, vertex + first_direction * radius); + segment(points, plane, vertex, vertex + second_direction * radius); + for index in 0..=24 { + push_local(points, plane, Vec2::from(arc.point_at(index as f64 / 24.0))); } - while sweep > std::f64::consts::PI { - sweep -= std::f64::consts::TAU; - } - let first_end = vertex + normalized_or(first, DVec3::X) * radius; - let second_end = vertex + normalized_or(second, DVec3::Y) * radius; - segment(points, vertex, first_end); - segment(points, vertex, second_end); - let arc = cadkernel::geom2d::tessellate::arc( - [vertex.x, vertex.y], - radius, + points.push([f64::NAN; 3]); + let start = Vec2::from(arc.point_at(0.0)); + let start_next = Vec2::from(arc.point_at(1.0 / 1024.0)); + let end = Vec2::from(arc.point_at(1.0)); + let end_previous = Vec2::from(arc.point_at(1.0 - 1.0 / 1024.0)); + arrow( + points, + plane, start, - start + sweep, - vertex.z, - cadkernel::geom2d::tessellate::DEFAULT_SEGMENTS_PER_RADIAN, + (start_next - start).normalize().unwrap_or(first_direction), + first_direction, + radius, + ); + arrow( + points, + plane, + end, + (end_previous - end).normalize().unwrap_or(-second_direction), + second_direction, + radius, + ); + let before_middle = Vec2::from(arc.point_at(0.5 - 1.0 / 1024.0)); + let after_middle = Vec2::from(arc.point_at(0.5 + 1.0 / 1024.0)); + let text_axis = (after_middle - before_middle) + .normalize() + .unwrap_or(Vec2::new(1.0, 0.0)); + text_box( + points, + plane, + text, + text_axis, + text_axis.perpendicular(), + measurement, ); - points.extend(arc); - points.push([f64::NAN, f64::NAN, f64::NAN]); - let sign = sweep.signum(); - let start_tangent = DVec3::new(-start.sin(), start.cos(), 0.0) * sign; - let end_angle = start + sweep; - let end_tangent = DVec3::new(end_angle.sin(), -end_angle.cos(), 0.0) * sign; - arrow(points, first_end, start_tangent, normalized_or(first, DVec3::X), radius); - arrow(points, second_end, end_tangent, normalized_or(second, DVec3::Y), radius); - let middle = start + sweep * 0.5; - let text_axis = normalized_or(DVec3::new(-middle.sin(), middle.cos(), 0.0), DVec3::X); - let text_perpendicular = DVec3::new(-text_axis.y, text_axis.x, 0.0); - text_box(points, text, text_axis, text_perpendicular, measurement); } fn ordinate_preview( points: &mut Vec<[f64; 3]>, - feature: DVec3, - leader: DVec3, + plane: Plane, + feature: Vec2, + leader: Vec2, measurement: f64, ) { - segment(points, feature, leader); - let axis = normalized_or(leader - feature, DVec3::Y); - let perpendicular = DVec3::new(-axis.y, axis.x, 0.0); - arrow(points, feature, axis, perpendicular, feature.distance(leader)); - text_box(points, leader, perpendicular, axis, measurement); + segment(points, plane, feature, leader); + let axis = (leader - feature) + .normalize() + .unwrap_or(Vec2::new(0.0, 1.0)); + let perpendicular = axis.perpendicular(); + arrow( + points, + plane, + feature, + axis, + perpendicular, + feature.distance(leader), + ); + text_box( + points, + plane, + leader, + perpendicular, + axis, + measurement, + ); } fn arrow( points: &mut Vec<[f64; 3]>, - tip: DVec3, - inward: DVec3, - perpendicular: DVec3, + plane: Plane, + tip: Vec2, + inward: Vec2, + perpendicular: Vec2, span: f64, ) { - let inward = normalized_or(inward, DVec3::X); - let perpendicular = normalized_or(perpendicular, DVec3::Y); + let inward = inward.normalize().unwrap_or(Vec2::new(1.0, 0.0)); + let perpendicular = perpendicular + .normalize() + .unwrap_or(Vec2::new(0.0, 1.0)); let size = span.clamp(1.0, 100.0) * 0.035; let back = tip + inward * size; - segment(points, tip, back + perpendicular * size * 0.4); - segment(points, tip, back - perpendicular * size * 0.4); + segment( + points, + plane, + tip, + back + perpendicular * size * 0.4, + ); + segment( + points, + plane, + tip, + back - perpendicular * size * 0.4, + ); } fn text_box( points: &mut Vec<[f64; 3]>, - center: DVec3, - axis: DVec3, - perpendicular: DVec3, + plane: Plane, + center: Vec2, + axis: Vec2, + perpendicular: Vec2, measurement: f64, ) { let digits = if measurement.abs() < 1.0 { @@ -631,30 +791,51 @@ fn text_box( let b = center + axis * half_width - perpendicular * half_height; let c = center + axis * half_width + perpendicular * half_height; let d = center - axis * half_width + perpendicular * half_height; - points.extend([a.to_array(), b.to_array(), c.to_array(), d.to_array(), a.to_array()]); - points.push([f64::NAN, f64::NAN, f64::NAN]); -} - -fn segment(points: &mut Vec<[f64; 3]>, first: DVec3, second: DVec3) { - points.push(first.to_array()); - points.push(second.to_array()); - points.push([f64::NAN, f64::NAN, f64::NAN]); -} - -fn normalized_or(value: DVec3, fallback: DVec3) -> DVec3 { - if value.length_squared() > 1.0e-18 { - value.normalize() - } else { - fallback + for point in [a, b, c, d, a] { + push_local(points, plane, point); } + points.push([f64::NAN; 3]); } -fn dv(point: Vector3) -> DVec3 { - DVec3::new(point.x, point.y, point.z) +fn project_to_line(point: Vec2, perpendicular: Vec2, coordinate: f64) -> Vec2 { + let line = Curve::XLine(XLine { + base: (perpendicular * coordinate).into(), + direction: perpendicular.perpendicular().into(), + }); + Vec2::from(closest_point(&line, point.into()).point) } -fn v3(point: DVec3) -> Vector3 { - Vector3::new(point.x, point.y, point.z) +fn plane_from_normal(origin: Vector3, normal: Vector3) -> Plane { + let (x_axis, y_axis) = + crate::scene::view::transform::ocs_axes((normal.x, normal.y, normal.z)); + Plane::from_axes( + point(origin), + [x_axis.0, x_axis.1, x_axis.2], + [y_axis.0, y_axis.1, y_axis.2], + ) +} + +fn project(plane: &Plane, value: Vector3) -> Vec2 { + Vec2::from(plane.project(point(value)).unwrap_or([0.0; 2])) +} + +fn world(plane: Plane, value: Vec2) -> Vector3 { + let value = plane.point_at(value.into()); + Vector3::new(value[0], value[1], value[2]) +} + +fn point(value: Vector3) -> [f64; 3] { + [value.x, value.y, value.z] +} + +fn segment(points: &mut Vec<[f64; 3]>, plane: Plane, first: Vec2, second: Vec2) { + push_local(points, plane, first); + push_local(points, plane, second); + points.push([f64::NAN; 3]); +} + +fn push_local(points: &mut Vec<[f64; 3]>, plane: Plane, point: Vec2) { + points.push(plane.point_at(point.into())); } inventory::submit!(crate::command::CommandRegistration { names: &["DIMCONTINUE"] }); diff --git a/src/modules/annotate/jogged_radius_dim.rs b/src/modules/annotate/jogged_radius_dim.rs index 4ce9f8d5d..9a517c968 100644 --- a/src/modules/annotate/jogged_radius_dim.rs +++ b/src/modules/annotate/jogged_radius_dim.rs @@ -140,7 +140,7 @@ impl JoggedRadiusDimensionCommand { ))), association, preserve_base_style: false, - keep_active: false, + continue_command: false, } } } diff --git a/src/modules/annotate/linear_dim.rs b/src/modules/annotate/linear_dim.rs index 7036e0950..53d09b08e 100644 --- a/src/modules/annotate/linear_dim.rs +++ b/src/modules/annotate/linear_dim.rs @@ -188,7 +188,7 @@ impl CadCommand for LinearDimensionCommand { entity, association: DimensionAssociationInput::Infer(self.source_handle), preserve_base_style: false, - keep_active: false, + continue_command: false, } } } diff --git a/src/modules/annotate/ordinate_dim.rs b/src/modules/annotate/ordinate_dim.rs index 9343249d9..1437ba953 100644 --- a/src/modules/annotate/ordinate_dim.rs +++ b/src/modules/annotate/ordinate_dim.rs @@ -117,7 +117,7 @@ impl CadCommand for OrdinateDimCommand { )), association: DimensionAssociationInput::Infer(None), preserve_base_style: false, - keep_active: false, + continue_command: false, } } } diff --git a/src/modules/annotate/radius_dim.rs b/src/modules/annotate/radius_dim.rs index 2fa565352..4cb6acc9d 100644 --- a/src/modules/annotate/radius_dim.rs +++ b/src/modules/annotate/radius_dim.rs @@ -112,7 +112,7 @@ impl CadCommand for RadiusDimensionCommand { entity: source_plane.place_entity(EntityType::Dimension(Dimension::Radius(dim))), association: DimensionAssociationInput::Infer(self.source_handle), preserve_base_style: false, - keep_active: false, + continue_command: false, } } } diff --git a/src/scene/dimension_assoc.rs b/src/scene/dimension_assoc.rs index 9e752c68b..82359b26c 100644 --- a/src/scene/dimension_assoc.rs +++ b/src/scene/dimension_assoc.rs @@ -408,12 +408,23 @@ fn resolve_reference(scene: &Scene, reference: &AssocDimensionReference) -> Opti .copied() } -fn dimension_inference_points(dimension: &Dimension) -> Option<[Option; 2]> { +fn dimension_inference_points(dimension: &Dimension) -> Vec> { match dimension { - Dimension::Linear(linear) => Some([Some(linear.first_point), Some(linear.second_point)]), - Dimension::Aligned(aligned) => Some([Some(aligned.first_point), Some(aligned.second_point)]), - Dimension::Ordinate(ordinate) => Some([Some(ordinate.feature_location), None]), - _ => None, + Dimension::Linear(linear) => vec![Some(linear.first_point), Some(linear.second_point)], + Dimension::Aligned(aligned) => vec![Some(aligned.first_point), Some(aligned.second_point)], + Dimension::Angular3Pt(angular) => vec![ + Some(angular.angle_vertex), + Some(angular.first_point), + Some(angular.second_point), + ], + Dimension::Angular2Ln(angular) => vec![ + Some(angular.first_point), + Some(angular.second_point), + Some(angular.angle_vertex), + Some(angular.definition_point), + ], + Dimension::Ordinate(ordinate) => vec![Some(ordinate.feature_location)], + _ => Vec::new(), } } @@ -800,10 +811,10 @@ impl Scene { pub(crate) fn infer_dimension_sources( &self, dimension: Handle, - ) -> [Option; 2] { + ) -> Vec> { let Some(EntityType::Dimension(entity)) = self.document.get_entity(dimension) else { - return [None, None]; + return Vec::new(); }; let radial_data = match entity { Dimension::Radius(radius) => Some(( @@ -845,23 +856,25 @@ impl Scene { }) .min_by(|first, second| first.0.total_cmp(&second.0)) .map(|(_, handle)| handle); - return [source, None]; + return vec![source]; } - let Some(points) = dimension_inference_points(entity) else { - return [None, None]; - }; - points.map(|point| { - point.and_then(|point| self.document - .entities() - .filter(|entity| entity.common().handle != dimension) - .filter_map(|entity| { - source_distance_squared(entity, point) - .map(|distance| (distance, entity.common().handle)) + dimension_inference_points(entity) + .into_iter() + .map(|point| { + point.and_then(|point| { + self.document + .entities() + .filter(|entity| entity.common().handle != dimension) + .filter_map(|entity| { + source_distance_squared(entity, point) + .map(|distance| (distance, entity.common().handle)) + }) + .filter(|(distance, _)| *distance <= 1e-16) + .min_by(|first, second| first.0.total_cmp(&second.0)) + .map(|(_, handle)| handle) }) - .filter(|(distance, _)| *distance <= 1e-16) - .min_by(|first, second| first.0.total_cmp(&second.0)) - .map(|(_, handle)| handle)) - }) + }) + .collect() } pub(crate) fn refresh_associative_dimensions( @@ -970,7 +983,7 @@ impl Scene { if let Some(point) = resolved[3] { angular.definition_point = point; } - angular.base.definition_point = angular.definition_point; + angular.base.definition_point = angular.dimension_arc; } Dimension::Radius(radius) => { let Some((radial, angle)) = radial_source else { @@ -1137,9 +1150,21 @@ impl Scene { arc.base.actual_measurement = arc.measurement(); } } - dimension.base_mut().actual_measurement = dimension.measurement(); + let measurement = match &*dimension { + Dimension::Linear(linear) => linear_measurement(linear), + _ => dimension.measurement(), + }; + dimension.base_mut().actual_measurement = measurement; refreshed.push((association.dimension, ChangeKind::Modified)); } refreshed } } + +fn linear_measurement(linear: &acadrust::entities::DimensionLinear) -> f64 { + let plane = plane_from_normal(linear.first_point, linear.base.normal); + let first = plane.project(dpoint(linear.first_point)).unwrap_or([0.0; 2]); + let second = plane.project(dpoint(linear.second_point)).unwrap_or(first); + let axis = [linear.rotation.cos(), linear.rotation.sin()]; + ((second[0] - first[0]) * axis[0] + (second[1] - first[1]) * axis[1]).abs() +}