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,