diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs index 22ee0d96e..9d84edcb6 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -659,23 +659,30 @@ impl OpenCADStudio { "DIMBASELINE" => { use crate::modules::annotate::dim_baseline::DimBaselineCommand; - let cmd = if let Some((p1, p2, dp, rot, trot)) = - find_last_linear_dim(&self.tabs[i].scene) - { - let doc = &self.tabs[i].scene.document; - let dimdli = doc - .dim_styles - .iter() - .find(|s| { - s.name - .eq_ignore_ascii_case(&doc.header.current_dimstyle_name) - }) - .map(|s| s.dimdli as f32) - .unwrap_or(1.5); - DimBaselineCommand::from_base(p1, p2, dp, rot, trot, dimdli) + let scene = &self.tabs[i].scene; + let recent = scene + .last_created_dimension + .and_then(|handle| scene.document.get_entity(handle)) + .cloned(); + let dimdli_by_style = scene + .document + .dim_styles + .iter() + .map(|style| (style.name.to_ascii_lowercase(), style.dimdli)) + .collect(); + let fallback_dimdli = if scene.document.header.measurement == 1 { + 3.75 } else { - DimBaselineCommand::new() + 0.38 }; + let current_style_name = scene.document.header.current_dimstyle_name.clone(); + let cmd = DimBaselineCommand::new( + recent, + dimdli_by_style, + current_style_name, + fallback_dimdli, + 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/modules/annotate/dim_baseline.rs b/src/modules/annotate/dim_baseline.rs index 207e9c352..a2ae3df3a 100644 --- a/src/modules/annotate/dim_baseline.rs +++ b/src/modules/annotate/dim_baseline.rs @@ -1,20 +1,18 @@ -// DIMBASELINE command — stacked baseline dimensions all measured from the same origin. -// -// Each new point becomes the second extension line origin of a new dimension. -// The first extension line is always the same base origin point. -// Each new dimension line is placed further from the baseline by DIMDLI (increment). -// -// Constructed from commands.rs after finding the last placed linear/aligned dimension. +//! Repeating baseline dimensions from a session base or an explicitly picked base. -use acadrust::entities::{Dimension, DimensionLinear}; +use std::collections::HashMap; + +use acadrust::entities::{ + Dimension, DimensionAligned, 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_baseline.svg")); @@ -27,100 +25,302 @@ pub fn tool() -> ToolDef { } } -/// Fallback stacking increment (world units) when no DimStyle is available. -const DEFAULT_DIMDLI: f64 = 1.5; +#[derive(Clone)] +struct SourceStyle { + layer: String, + style_name: String, + normal: Vector3, + text_rotation: f64, + horizontal_direction: f64, +} + +impl SourceStyle { + fn from_base(base: &DimensionBase) -> Self { + Self { + layer: base.common.layer.clone(), + style_name: base.style_name.clone(), + normal: base.normal, + text_rotation: base.text_rotation, + horizontal_direction: base.horizontal_direction, + } + } + + 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; + } +} + +enum BaselineKind { + Linear { + fixed: DVec3, + rotation: f64, + aligned: bool, + perpendicular: DVec3, + next_offset: f64, + increment: f64, + }, + Angular2Ln { + source: DimensionAngular2Ln, + vertex: DVec3, + fixed_ray: DVec3, + next_radius: f64, + increment: f64, + }, + Angular3Pt { + source: DimensionAngular3Pt, + vertex: DVec3, + fixed_ray: DVec3, + next_radius: f64, + increment: f64, + }, + Ordinate { + source: DimensionOrdinate, + leader_delta: DVec3, + }, +} + +struct BaselineState { + kind: BaselineKind, + style: SourceStyle, +} pub struct DimBaselineCommand { - /// Fixed first-extension-line origin (never changes). - base_p1: DVec3, - /// Direction along the measurement direction (0.0 = horizontal, PI/2 = vertical). - rotation: f64, - /// Text reading rotation inherited from the base dim (keeps a UCS-aligned - /// chain's text consistent with the originating DIMLINEAR). 0 = natural. - text_rotation: f64, - /// Unit vector perpendicular to the dimension axis, pointing toward the dim line side. - perp: DVec3, - /// Perpendicular distance of the NEXT dimension line from the extension-line axis. - next_offset: f64, - /// Stacking increment from the active DimStyle (DIMDLI). - dimdli: f64, - /// True once we have a base dimension loaded. - ready: bool, + base: Option, + injected: Option, + dimdli_by_style: HashMap, + current_style_name: String, + fallback_dimdli: f64, + preserve_base_style: bool, + committed: usize, } impl DimBaselineCommand { - /// No base dim found — cancel immediately. - pub fn new() -> Self { - Self { - base_p1: DVec3::ZERO, - rotation: 0.0, - text_rotation: 0.0, - perp: DVec3::Y, - next_offset: DEFAULT_DIMDLI, - dimdli: DEFAULT_DIMDLI, - ready: false, - } - } - - /// Build from the last placed dimension. - /// - /// `p1` — first extension line origin (fixed baseline). - /// `p2` — second extension line origin of the base dim (unused for placement, kept for context). - /// `definition_point` — dim-line position of the base dim (defines perpendicular side). - /// `rotation` — 0.0 = horizontal, PI/2 = vertical. - /// `dimdli` — DimStyle stacking increment (use [`DEFAULT_DIMDLI`] when no style is active). - pub fn from_base( - p1: Vec3, - _p2: Vec3, - definition_point: Vec3, - rotation: f64, - text_rotation: f64, - dimdli: f32, + pub fn new( + recent: Option, + dimdli_by_style: HashMap, + current_style_name: String, + fallback_dimdli: f64, + preserve_base_style: bool, ) -> Self { - // The base dim's points enter as f32 from the caller; promote to f64 so - // all committed-coordinate math downstream stays exact. - let p1 = p1.as_dvec3(); - let definition_point = definition_point.as_dvec3(); - // Measurement axis = the base dim's rotation angle (any angle, incl. a - // UCS-aligned one), not a world horizontal/vertical. - let axis = DVec3::new(rotation.cos(), rotation.sin(), 0.0); - let perp = DVec3::new(-axis.y, axis.x, 0.0); - let base_offset = (definition_point - p1).dot(perp); - let dimdli = dimdli as f64; - let dimdli = if dimdli.abs() < 1e-6 { - DEFAULT_DIMDLI - } else { - dimdli.abs() + let mut command = Self { + base: None, + injected: None, + dimdli_by_style, + current_style_name, + fallback_dimdli, + preserve_base_style, + committed: 0, }; - // Stack each baseline outward — in the SIGN direction of the base dim's - // own offset. When the base dim sits on the -perp side (below / left of - // the points) a positive increment would march the stack back toward and - // across the points; the signed increment keeps it moving away. (#181) - let dir = if base_offset >= 0.0 { 1.0 } else { -1.0 }; - let dimdli = dimdli * dir; - let next_offset = base_offset + dimdli; - Self { - base_p1: p1, - rotation, - text_rotation, - perp, - next_offset, - dimdli, - ready: true, + if let Some(entity) = recent { + let _ = command.install_base(&entity, None); + } + command + } + + fn effective_increment(&self, style_name: &str) -> f64 { + let style_name = if self.preserve_base_style { + style_name + } else { + &self.current_style_name + }; + self.dimdli_by_style + .get(&style_name.to_ascii_lowercase()) + .copied() + .filter(|value| value.is_finite() && value.abs() > 1.0e-9) + .unwrap_or(self.fallback_dimdli) + .abs() + } + + 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 increment = self.effective_increment(&style.style_name); + let kind = match dimension { + Dimension::Linear(source) => { + let (fixed, _) = nearest_end(source.first_point, source.second_point, pick); + linear_base( + fixed, + source.rotation, + false, + dv(source.base.definition_point), + increment, + ) + } + Dimension::Aligned(source) => { + let (fixed, other) = nearest_end(source.first_point, source.second_point, pick); + linear_base( + fixed, + (other - fixed).y.atan2((other - fixed).x), + true, + dv(source.base.definition_point), + increment, + ) + } + Dimension::Angular2Ln(source) => { + let vertex = angular2_vertex(source); + let first = dv(source.second_point) - vertex; + let second = dv(source.definition_point) - vertex; + let fixed_ray = nearest_direction(first, second, vertex, pick); + let radius = (dv(source.dimension_arc) - vertex).length().max(1.0e-9); + BaselineKind::Angular2Ln { + source: source.clone(), + vertex, + fixed_ray, + next_radius: radius + increment, + increment, + } + } + Dimension::Angular3Pt(source) => { + let vertex = dv(source.angle_vertex); + let first = dv(source.first_point) - vertex; + let second = dv(source.second_point) - vertex; + let fixed_ray = nearest_direction(first, second, vertex, pick); + let radius = (dv(source.definition_point) - vertex).length().max(1.0e-9); + BaselineKind::Angular3Pt { + source: source.clone(), + vertex, + fixed_ray, + next_radius: radius + increment, + increment, + } + } + Dimension::Ordinate(source) => BaselineKind::Ordinate { + source: source.clone(), + leader_delta: dv(source.leader_endpoint) - dv(source.feature_location), + }, + _ => return false, + }; + self.base = Some(BaselineState { kind, style }); + self.committed = 0; + true + } + + fn build_dimension(&self, point: DVec3) -> Option { + let state = self.base.as_ref()?; + let mut dimension = match &state.kind { + BaselineKind::Linear { + fixed, + rotation, + aligned, + perpendicular, + next_offset, + .. + } => build_linear( + *fixed, + point, + *rotation, + *aligned, + *perpendicular, + *next_offset, + ), + BaselineKind::Angular2Ln { + source, + vertex, + fixed_ray, + next_radius, + .. + } => { + let moving = point - *vertex; + if moving.length_squared() <= 1.0e-18 { + return None; + } + let mut result = source.clone(); + result.first_point = v3(*vertex); + result.second_point = v3(*vertex + normalized_or(*fixed_ray, DVec3::X)); + result.angle_vertex = v3(*vertex); + result.definition_point = v3(point); + result.dimension_arc = v3( + *vertex + angular_bisector(*fixed_ray, moving) * *next_radius, + ); + result.base.definition_point = result.dimension_arc; + result.base.text_middle_point = result.dimension_arc; + result.base.insertion_point = result.dimension_arc; + result.base.actual_measurement = result.measurement_degrees(); + Dimension::Angular2Ln(result) + } + BaselineKind::Angular3Pt { + source, + vertex, + fixed_ray, + next_radius, + .. + } => { + let moving = point - *vertex; + if moving.length_squared() <= 1.0e-18 { + return None; + } + let mut result = source.clone(); + result.angle_vertex = v3(*vertex); + result.first_point = v3(*vertex + normalized_or(*fixed_ray, DVec3::X)); + result.second_point = v3(point); + result.definition_point = v3( + *vertex + angular_bisector(*fixed_ray, moving) * *next_radius, + ); + result.base.definition_point = result.definition_point; + result.base.text_middle_point = result.definition_point; + result.base.insertion_point = result.definition_point; + result.base.actual_measurement = result.measurement_degrees(); + Dimension::Angular3Pt(result) + } + BaselineKind::Ordinate { + source, + leader_delta, + } => { + let mut result = DimensionOrdinate::new( + v3(point), + v3(point + *leader_delta), + source.is_ordinate_type_x, + ); + result.definition_point = source.definition_point; + result.base.definition_point = source.base.definition_point; + result.base.text_middle_point = result.leader_endpoint; + result.base.insertion_point = result.leader_endpoint; + 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, direction: f64) { + let Some(state) = self.base.as_mut() else { + return; + }; + match &mut state.kind { + BaselineKind::Linear { + next_offset, + increment, + .. + } => *next_offset += *increment * direction, + BaselineKind::Angular2Ln { + next_radius, + increment, + .. + } + | BaselineKind::Angular3Pt { + next_radius, + increment, + .. + } => *next_radius = (*next_radius + *increment * direction).max(1.0e-9), + BaselineKind::Ordinate { .. } => {} } } -} -impl DimBaselineCommand { - /// The two dimension-line endpoints for the new baseline at `p2`: the fixed - /// base origin and `p2` each projected INDEPENDENTLY onto the dim line (at - /// the current stacking offset). Projecting both keeps the line straight - /// even when the two origins aren't level — a shared offset tilts it. (#181) - fn dim_line_pts(&self, p2: DVec3) -> (DVec3, DVec3) { - let target = self.base_p1.dot(self.perp) + self.next_offset; - let d1 = self.base_p1 + self.perp * (target - self.base_p1.dot(self.perp)); - let d2 = p2 + self.perp * (target - p2.dot(self.perp)); - (d1, d2) + fn undo_one(&mut self) -> Option { + if self.committed == 0 { + return Some(CmdResult::NeedPoint); + } + self.committed -= 1; + self.advance(-1.0); + Some(CmdResult::UndoDocument) } } @@ -130,104 +330,346 @@ impl CadCommand for DimBaselineCommand { } fn prompt(&self) -> String { - if !self.ready { - t!("DIMBASELINE No base dimension found. Place a dimension first.").into_owned() + if self.base.is_none() { + "DIMBASELINE Select base dimension (Enter to exit):".to_string() } else { - t!("DIMBASELINE Specify a second extension line origin (Enter to exit):").into_owned() + "DIMBASELINE Specify next extension origin [Select/Undo]