From c6f6c00220863b24e7151f3d2a58d79c4e49b057 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:49:32 +0300 Subject: [PATCH 1/4] Add dimension continuation creation policy --- src/app/command_driver.rs | 23 +++++++++++++++- src/app/commands/styleprops.rs | 25 +++++++++++++++++- src/app/mod.rs | 3 +++ src/app/properties.rs | 32 +++++++++++++++++++++++ src/app/settings.rs | 9 +++++++ src/app/update/file.rs | 2 ++ src/command.rs | 3 +++ 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/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 | 6 +++++ 16 files changed, 109 insertions(+), 2 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 09eb09718..d33b27233 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -1287,6 +1287,7 @@ impl OpenCADStudio { CmdResult::CommitDimension { mut entity, association, + preserve_base_style, } => { let label = self.history_label_from_active_cmd(i, "DIMENSION"); let association_mode = self.tabs[i] @@ -1300,6 +1301,17 @@ impl OpenCADStudio { acadrust::entities::Dimension::Ordinate(_) ) ); + let inherited_dimension = if preserve_base_style { + match &entity { + acadrust::EntityType::Dimension(dimension) => Some(( + dimension.base().common.layer.clone(), + dimension.base().style_name.clone(), + )), + _ => None, + } + } else { + None + }; let pending = if association_mode == 0 { let layer = self.tabs[i].active_layer.clone(); if layer != "0" || entity.as_entity().layer().is_empty() { @@ -1330,6 +1342,12 @@ impl OpenCADStudio { &self.tabs[i].scene.document, &mut entity, ); + if let (Some((layer, style_name)), acadrust::EntityType::Dimension(dimension)) = + (inherited_dimension, &mut entity) + { + dimension.base_mut().common.layer = layer; + dimension.base_mut().style_name = style_name; + } let pieces = crate::modules::draw::modify::explode::explode_entity( &entity, &self.tabs[i].scene.document, @@ -1345,7 +1363,10 @@ impl OpenCADStudio { } else { let delta_safe = self.delta_add_safe(i, &entity); let pending = self.begin_undo(i, label, 1, delta_safe); - if let Some(handle) = self.commit_entity_handle(entity) { + if let Some(handle) = self.commit_entity_handle_with_dimension_policy( + entity, + preserve_base_style, + ) { if association_mode == 2 { let mut changes = vec![ (handle, crate::scene::ChangeKind::Modified), diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index 39d4cd93c..31f72f161 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -931,6 +931,7 @@ impl OpenCADStudio { | "CENTERCROSSSIZE" | "CENTERCROSSGAP" | "CENTERMARKEXE" + | "DIMCONTINUEMODE" ) => { return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i); @@ -953,7 +954,7 @@ impl OpenCADStudio { let value = it.next().map(|s| s.trim().to_string()); if name.is_empty() || name == "?" { self.command_line.push_info( - "SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE | CLAYER CELTYPE TEXTSTYLE (read-only)", + "SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE | CLAYER CELTYPE TEXTSTYLE (read-only)", ); } else { let frame_kind = crate::scene::frame::kind_for_name(&name); @@ -1069,6 +1070,28 @@ impl OpenCADStudio { } return Some(self.finish_dispatch(cmd)); } + if name == "DIMCONTINUEMODE" { + let current = self.dimension_continue_mode; + if let Some(value) = &value { + match value.parse::() { + Ok(mode @ 0..=1) => { + self.dimension_continue_mode = mode; + self.persist_settings_if_changed(); + self.command_line + .push_output(&format!("DIMCONTINUEMODE = {mode}")); + } + _ => self.command_line.push_error( + "SETVAR: DIMCONTINUEMODE requires 0 or 1.", + ), + } + } else { + self.command_line.push_output(crate::tf!( + "Enter new value for {name} <{current}>:" + ).as_ref()); + self.pending_setvar = Some(name.clone()); + } + return Some(self.finish_dispatch(cmd)); + } if matches!(name.as_str(), "DONUTID" | "DONUTOD") { let current = if name == "DONUTID" { crate::modules::draw::defaults::get_donut_inner_diameter() diff --git a/src/app/mod.rs b/src/app/mod.rs index b18e53012..e4f0fcb84 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -465,6 +465,8 @@ pub(super) struct OpenCADStudio { options_tab: crate::ui::window::options::OptionsTab, /// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single). pub texteditmode: bool, + /// Creation-style policy used by continuing and baseline dimensions. + pub dimension_continue_mode: i16, /// When true (default), saving over an existing file first writes a `.bak` /// copy of it for recovery (#205). Toggle with the ISAVEBAK command. pub backup_on_save: bool, @@ -3210,6 +3212,7 @@ impl OpenCADStudio { dyn_input: true, options_tab: crate::ui::window::options::OptionsTab::General, texteditmode: false, + dimension_continue_mode: 1, backup_on_save: true, file_assoc_enabled: true, savetime_min: 10, diff --git a/src/app/properties.rs b/src/app/properties.rs index 006877cc4..6129dd8b8 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -2455,10 +2455,33 @@ impl OpenCADStudio { /// (or `None` if it could not be added). Lets callers follow up — e.g. /// open the in-place text editor on a freshly created MultiLeader. pub(super) fn commit_entity_handle( + &mut self, + entity: acadrust::EntityType, + ) -> Option { + self.commit_entity_handle_with_dimension_policy(entity, false) + } + + /// Commit an entity while optionally retaining a dimension's source layer + /// and dimension style. DIMCONTINUEMODE=1 uses this path; ordinary + /// creation continues to receive the current ribbon/style defaults. + pub(super) fn commit_entity_handle_with_dimension_policy( &mut self, mut entity: acadrust::EntityType, + preserve_dimension_layer_and_style: bool, ) -> Option { let i = self.active_tab; + let is_dimension = matches!(&entity, acadrust::EntityType::Dimension(_)); + let inherited_dimension = if preserve_dimension_layer_and_style { + match &entity { + acadrust::EntityType::Dimension(dimension) => Some(( + dimension.base().common.layer.clone(), + dimension.base().style_name.clone(), + )), + _ => None, + } + } else { + None + }; if let acadrust::EntityType::Table(table) = &mut entity { const PREFIX: &str = "__OPENCAD_LINK_PENDING__"; if let Some(path) = table.name.strip_prefix(PREFIX).map(str::to_string) { @@ -2560,6 +2583,12 @@ impl OpenCADStudio { &self.tabs[i].scene.document, &mut entity, ); + if let (Some((layer, style_name)), acadrust::EntityType::Dimension(dimension)) = + (inherited_dimension, &mut entity) + { + dimension.base_mut().common.layer = layer; + dimension.base_mut().style_name = style_name; + } // Smart centre objects carry their own drawing-level creation style. // Apply it after the generic ribbon style so ordinary LINE entities @@ -2719,6 +2748,9 @@ impl OpenCADStudio { self.tabs[i].last_draw_anchor = Some(handle); } } + if is_dimension { + self.tabs[i].scene.last_created_dimension = new_handle; + } new_handle } } diff --git a/src/app/settings.rs b/src/app/settings.rs index 8b65c73e5..8caa5f994 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -198,6 +198,10 @@ pub struct UserSettings { pub osmode: i32, /// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single). pub texteditmode: bool, + /// DIMCONTINUEMODE: 1 inherits the base dimension's layer/style; 0 uses + /// the current creation layer/style. Registry-style, app-level preference. + #[serde(default = "default_dimension_continue_mode")] + pub dimension_continue_mode: i16, /// TEXTFILL: fill TrueType glyphs (true) or draw them hollow (false). pub textfill: bool, /// When true, saving over an existing file first copies it to a sibling @@ -247,6 +251,10 @@ fn default_clipromptlines() -> i32 { 3 } +fn default_dimension_continue_mode() -> i16 { + 1 +} + fn deserialize_clipromptlines<'de, D>(de: D) -> Result where D: serde::Deserializer<'de>, @@ -284,6 +292,7 @@ impl Default for UserSettings { // off (suppress bit 16384). osmode: 575 | OSMODE_SUPPRESS, texteditmode: false, + dimension_continue_mode: 1, textfill: true, backup_on_save: true, file_assoc_enabled: true, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index bbf906eae..0e85da3ce 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -382,6 +382,7 @@ impl OpenCADStudio { self.snapper.snap_enabled, ), texteditmode: self.texteditmode, + dimension_continue_mode: self.dimension_continue_mode, textfill: crate::scene::text::sdf_atlas::textfill(), backup_on_save: self.backup_on_save, file_assoc_enabled: self.file_assoc_enabled, @@ -440,6 +441,7 @@ impl OpenCADStudio { self.snapper.enabled = modes.into_iter().collect(); self.snapper.snap_enabled = snap_enabled; self.texteditmode = s.texteditmode; + self.dimension_continue_mode = s.dimension_continue_mode.clamp(0, 1); crate::scene::text::sdf_atlas::set_textfill(s.textfill); self.backup_on_save = s.backup_on_save; self.file_assoc_enabled = s.file_assoc_enabled; diff --git a/src/command.rs b/src/command.rs index d4a2312b3..e8bf675e8 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1248,6 +1248,9 @@ pub enum CmdResult { CommitDimension { entity: EntityType, association: DimensionAssociationInput, + /// Retain the source dimension's layer and style instead of replacing + /// them with the current creation defaults (DIMCONTINUEMODE=1). + preserve_base_style: 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 2ec212d1e..dafc2183b 100644 --- a/src/modules/annotate/aligned_dim.rs +++ b/src/modules/annotate/aligned_dim.rs @@ -135,6 +135,7 @@ impl CadCommand for AlignedDimensionCommand { CmdResult::CommitDimension { entity: self.plane.place_entity(EntityType::Dimension(Dimension::Aligned(dim))), association: DimensionAssociationInput::Infer(self.source_handle), + preserve_base_style: false, } } } diff --git a/src/modules/annotate/angular_dim.rs b/src/modules/annotate/angular_dim.rs index 6bd3ef2e4..f1c00059f 100644 --- a/src/modules/annotate/angular_dim.rs +++ b/src/modules/annotate/angular_dim.rs @@ -234,6 +234,7 @@ impl AngularDimensionCommand { CmdResult::CommitDimension { entity, association: DimensionAssociationInput::Explicit(self.source_refs.clone()), + preserve_base_style: false, } } diff --git a/src/modules/annotate/arc_length_dim.rs b/src/modules/annotate/arc_length_dim.rs index f0225c1c9..434eabbe6 100644 --- a/src/modules/annotate/arc_length_dim.rs +++ b/src/modules/annotate/arc_length_dim.rs @@ -265,6 +265,7 @@ impl ArcLengthDimensionCommand { CmdResult::CommitDimension { entity: plane.place_entity(EntityType::Dimension(Dimension::Arc(dimension))), association, + preserve_base_style: false, } } } diff --git a/src/modules/annotate/diameter_dim.rs b/src/modules/annotate/diameter_dim.rs index 010434c71..946cde255 100644 --- a/src/modules/annotate/diameter_dim.rs +++ b/src/modules/annotate/diameter_dim.rs @@ -113,6 +113,7 @@ impl CadCommand for DiameterDimensionCommand { entity: source_plane .place_entity(EntityType::Dimension(Dimension::Diameter(dim))), association: DimensionAssociationInput::Infer(self.source_handle), + preserve_base_style: false, } } } diff --git a/src/modules/annotate/jogged_radius_dim.rs b/src/modules/annotate/jogged_radius_dim.rs index fdfc6097b..2363082b1 100644 --- a/src/modules/annotate/jogged_radius_dim.rs +++ b/src/modules/annotate/jogged_radius_dim.rs @@ -139,6 +139,7 @@ impl JoggedRadiusDimensionCommand { dimension, ))), association, + preserve_base_style: false, } } } diff --git a/src/modules/annotate/linear_dim.rs b/src/modules/annotate/linear_dim.rs index c2f927c80..fd6d6d6c5 100644 --- a/src/modules/annotate/linear_dim.rs +++ b/src/modules/annotate/linear_dim.rs @@ -187,6 +187,7 @@ impl CadCommand for LinearDimensionCommand { CmdResult::CommitDimension { entity, association: DimensionAssociationInput::Infer(self.source_handle), + preserve_base_style: false, } } } diff --git a/src/modules/annotate/ordinate_dim.rs b/src/modules/annotate/ordinate_dim.rs index 4ac1d8a17..221523c7f 100644 --- a/src/modules/annotate/ordinate_dim.rs +++ b/src/modules/annotate/ordinate_dim.rs @@ -116,6 +116,7 @@ impl CadCommand for OrdinateDimCommand { Dimension::Ordinate(dim), )), association: DimensionAssociationInput::Infer(None), + preserve_base_style: false, } } } diff --git a/src/modules/annotate/radius_dim.rs b/src/modules/annotate/radius_dim.rs index 62a833489..10681bad8 100644 --- a/src/modules/annotate/radius_dim.rs +++ b/src/modules/annotate/radius_dim.rs @@ -111,6 +111,7 @@ impl CadCommand for RadiusDimensionCommand { CmdResult::CommitDimension { entity: source_plane.place_entity(EntityType::Dimension(Dimension::Radius(dim))), association: DimensionAssociationInput::Infer(self.source_handle), + preserve_base_style: false, } } } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 0894fe12c..161d2a454 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1420,6 +1420,11 @@ 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. + 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, /// Native AcDbLight/Sun inputs, separated by rendered block and viewport @@ -1848,6 +1853,7 @@ impl Scene { model_pane_min_px: std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)), selection: Rc::new(RefCell::new(SelectionState::default())), document: CadDocument::new(), + last_created_dimension: None, object_data_cache: crate::entities::object_data::ObjectDataCache::default(), lighting_cache: RefCell::new(HashMap::default()), selected: HashSet::default(), From 980a7d611bf5d2bc88046ede30a2706e0374174d Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:49:38 +0300 Subject: [PATCH 2/4] Complete baseline dimension workflow --- src/app/commands/dim.rs | 37 +- src/modules/annotate/dim_baseline.rs | 806 +++++++++++++++++++++------ 2 files changed, 646 insertions(+), 197 deletions(-) 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] :".to_string() } else { - "DIMBASELINE Specify next extension origin [Select/Undo] :" + .to_string() } } fn options(&self) -> Vec { - self.base.as_ref().map_or_else(Vec::new, |_| { - vec![CmdOption::new("Select", "S"), CmdOption::new("Undo", "U")] + self.base.as_ref().map_or_else(Vec::new, |base| { + if matches!(&base.kind, BaselineKind::Ordinate { .. }) { + vec![CmdOption::new("Undo", "U"), CmdOption::new("Select", "S")] + } else { + vec![CmdOption::new("Select", "S"), CmdOption::new("Undo", "U")] + } }) } @@ -451,8 +463,8 @@ fn build_linear( let second_line = point + perpendicular * (target - point.dot(perpendicular)); if aligned { let mut result = DimensionAligned::new(v3(fixed), v3(point)); - result.definition_point = v3(first_line); - result.base.definition_point = v3(first_line); + 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(); @@ -460,8 +472,8 @@ fn build_linear( } else { let mut result = DimensionLinear::new(v3(fixed), v3(point)); result.rotation = rotation; - result.definition_point = v3(first_line); - result.base.definition_point = v3(first_line); + 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(); @@ -508,12 +520,25 @@ fn line_intersection(a: DVec3, b: DVec3, c: DVec3, d: DVec3) -> Option { Some(a + b * ((delta.x * d.y - delta.y * d.x) / cross)) } -fn angular_bisector(first: DVec3, second: DVec3) -> DVec3 { - let first = normalized_or(first, DVec3::X); - normalized_or( - first + normalized_or(second, DVec3::Y), - DVec3::new(-first.y, first.x, 0.0), - ) +fn angular_definition_and_text( + vertex: DVec3, + first: DVec3, + second: DVec3, + radius: f64, +) -> (DVec3, DVec3) { + 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 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 normalized_or(value: DVec3, fallback: DVec3) -> DVec3 { From 1d58082e0ac2a6cb1305875ca362726bcb9b6b5b Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 28 Aug 2026 08:01:44 +0300 Subject: [PATCH 4/4] fix(dimension): repair baseline workflows --- src/app/command_driver.rs | 21 +- src/app/commands/dim.rs | 87 +--- 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_baseline.rs | 591 ++++++++++++++-------- src/modules/annotate/dim_continue.rs | 308 ++++++----- 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/dimension_assoc.rs | 73 ++- 14 files changed, 640 insertions(+), 450 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index d33b27233..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,6 +1288,7 @@ impl OpenCADStudio { mut entity, association, preserve_base_style, + continue_command, } => { let label = self.history_label_from_active_cmd(i, "DIMENSION"); let association_mode = self.tabs[i] @@ -1378,8 +1379,6 @@ impl OpenCADStudio { self.tabs[i] .scene .infer_dimension_sources(handle) - .into_iter() - .collect() }, |source| { if single_source_dimension { @@ -1414,12 +1413,22 @@ 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 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(); + } } CmdResult::CommitSolid { entity, diff --git a/src/app/commands/dim.rs b/src/app/commands/dim.rs index 9d84edcb6..9c16bf312 100644 --- a/src/app/commands/dim.rs +++ b/src/app/commands/dim.rs @@ -646,13 +646,19 @@ 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 preserve_base_style = self.dimension_continue_mode == 1; + let cmd = scene + .last_created_dimension + .filter(|handle| scene.entity_belongs_to_active_space(*handle)) + .and_then(|handle| scene.document.get_entity(handle)) + .and_then(|entity| match entity { + acadrust::EntityType::Dimension(dimension) => Some( + DimContinueCommand::from_dimension(dimension, preserve_base_style), + ), + _ => None, + }) + .unwrap_or_else(|| DimContinueCommand::new(preserve_base_style)); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } @@ -662,6 +668,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 dimdli_by_style = scene @@ -1383,72 +1390,6 @@ impl OpenCADStudio { } } -/// Find the last placed linear or aligned dimension in the document. -/// Returns `(first_point, second_point, definition_point, rotation_rad)` in world-space. -fn find_last_linear_dim( - scene: &crate::scene::Scene, -) -> Option<(glam::Vec3, glam::Vec3, glam::Vec3, f64, f64)> { - use acadrust::entities::Dimension; - let mut best_handle: u64 = 0; - let mut result: Option<(glam::Vec3, glam::Vec3, glam::Vec3, f64, f64)> = None; - - for entity in scene.document.entities() { - if let acadrust::EntityType::Dimension(dim) = entity { - let h = entity.common().handle.value(); - if h <= best_handle { - continue; - } - let item = match dim { - Dimension::Linear(d) => { - let p1 = glam::Vec3::new( - d.first_point.x as f32, - d.first_point.y as f32, - d.first_point.z as f32, - ); - let p2 = glam::Vec3::new( - d.second_point.x as f32, - d.second_point.y as f32, - d.second_point.z as f32, - ); - let dp = glam::Vec3::new( - d.base.definition_point.x as f32, - d.base.definition_point.y as f32, - d.base.definition_point.z as f32, - ); - Some((p1, p2, dp, d.rotation, d.base.text_rotation)) - } - Dimension::Aligned(d) => { - let p1 = glam::Vec3::new( - d.first_point.x as f32, - d.first_point.y as f32, - d.first_point.z as f32, - ); - let p2 = glam::Vec3::new( - d.second_point.x as f32, - d.second_point.y as f32, - d.second_point.z as f32, - ); - let dp = glam::Vec3::new( - d.base.definition_point.x as f32, - d.base.definition_point.y as f32, - d.base.definition_point.z as f32, - ); - let dx = (d.second_point.x - d.first_point.x) as f32; - let dy = (d.second_point.y - d.first_point.y) as f32; - let rot = dy.atan2(dx) as f64; - Some((p1, p2, dp, rot, d.base.text_rotation)) - } - _ => None, - }; - if let Some(data) = item { - best_handle = h; - result = Some(data); - } - } - } - result -} - /// Collect candidate dimension endpoints from an entity for QDIM — line ends, /// polyline vertices, arc endpoints — in world space. fn qdim_collect_points(e: &acadrust::EntityType, out: &mut Vec) { diff --git a/src/command.rs b/src/command.rs index e8bf675e8..cd7544513 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 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 dafc2183b..9ecfa7c70 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, + continue_command: false, } } } diff --git a/src/modules/annotate/angular_dim.rs b/src/modules/annotate/angular_dim.rs index f1c00059f..f23e3b555 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, + continue_command: false, } } diff --git a/src/modules/annotate/arc_length_dim.rs b/src/modules/annotate/arc_length_dim.rs index 434eabbe6..bbdfaf412 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, + continue_command: false, } } } diff --git a/src/modules/annotate/diameter_dim.rs b/src/modules/annotate/diameter_dim.rs index 946cde255..19dfcb1ca 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, + continue_command: false, } } } diff --git a/src/modules/annotate/dim_baseline.rs b/src/modules/annotate/dim_baseline.rs index 1656432d8..834222a34 100644 --- a/src/modules/annotate/dim_baseline.rs +++ b/src/modules/annotate/dim_baseline.rs @@ -1,6 +1,5 @@ -//! Repeating baseline dimensions from a session base or an explicitly picked base. - use std::collections::HashMap; +use std::f64::consts::PI; use acadrust::entities::{ Dimension, DimensionAligned, DimensionAngular2Ln, DimensionAngular3Pt, DimensionBase, @@ -8,6 +7,10 @@ use acadrust::entities::{ }; use acadrust::types::Vector3; use acadrust::{EntityType, Handle}; +use cadkernel::geom2d::{ + arc_span, intersect, nearest_of, Arc, Curve, Line, Tolerance, Transform, Vec2, XLine, +}; +use cadkernel::space::Plane; use glam::DVec3; use crate::command::{CadCommand, CmdOption, CmdResult, DimensionAssociationInput}; @@ -56,35 +59,39 @@ impl SourceStyle { enum BaselineKind { Linear { - fixed: DVec3, + fixed: Vec2, rotation: f64, aligned: bool, - perpendicular: DVec3, + perpendicular: Vec2, next_offset: f64, increment: f64, }, Angular2Ln { - source: DimensionAngular2Ln, - vertex: DVec3, - fixed_ray: DVec3, + fixed_line: [Vec2; 2], + fixed_is_first: bool, + vertex: Vec2, + fixed_ray: Vec2, next_radius: f64, increment: f64, }, Angular3Pt { - source: DimensionAngular3Pt, - vertex: DVec3, - fixed_ray: DVec3, + vertex: Vec2, + fixed_point: Vec2, + fixed_ray: Vec2, next_radius: f64, increment: f64, }, Ordinate { - source: DimensionOrdinate, + definition: Vec2, + leader: Vec2, + is_x: bool, }, } struct BaselineState { kind: BaselineKind, style: SourceStyle, + plane: Plane, } pub struct DimBaselineCommand { @@ -140,35 +147,59 @@ impl DimBaselineCommand { }; let style = SourceStyle::from_base(dimension.base()); let increment = self.effective_increment(&style.style_name); + let plane = plane_from_normal(dimension.base().definition_point, style.normal); + let pick = pick.and_then(|point| plane.project(point.to_array()).map(Vec2::from)); let kind = match dimension { Dimension::Linear(source) => { - let (fixed, _) = nearest_end(source.first_point, source.second_point, pick); + let first = project(&plane, source.first_point); + let second = project(&plane, source.second_point); + let (fixed, _) = nearest_end(first, second, pick); linear_base( fixed, source.rotation, false, - dv(source.base.definition_point), + project(&plane, source.base.definition_point), increment, ) } Dimension::Aligned(source) => { - let (fixed, other) = nearest_end(source.first_point, source.second_point, pick); + let first = project(&plane, source.first_point); + let second = project(&plane, source.second_point); + let (fixed, other) = nearest_end(first, second, pick); linear_base( fixed, - (other - fixed).y.atan2((other - fixed).x), + (other - fixed).angle(), true, - dv(source.base.definition_point), + project(&plane, 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); + 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]); + let fixed_index = nearest_line(lines, pick); + let fixed_line = lines[fixed_index]; + let fixed_end = if fixed_index == 0 { + lines[0][1] + } else { + lines[1][1] + }; + let fixed_ray = fixed_end - vertex; + let radius = project(&plane, source.dimension_arc) + .distance(vertex) + .max(1.0e-9); BaselineKind::Angular2Ln { - source: source.clone(), + fixed_line, + fixed_is_first: fixed_index == 0, vertex, fixed_ray, next_radius: radius + increment, @@ -176,31 +207,42 @@ impl DimBaselineCommand { } } 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); + let vertex = project(&plane, source.angle_vertex); + let points = [ + project(&plane, source.first_point), + project(&plane, source.second_point), + ]; + let fixed_index = nearest_line( + [[vertex, points[0]], [vertex, points[1]]], + pick, + ); + let fixed_point = points[fixed_index]; + let radius = project(&plane, source.definition_point) + .distance(vertex) + .max(1.0e-9); BaselineKind::Angular3Pt { - source: source.clone(), vertex, - fixed_ray, + fixed_point, + fixed_ray: fixed_point - vertex, next_radius: radius + increment, increment, } } Dimension::Ordinate(source) => BaselineKind::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(BaselineState { kind, style }); + self.base = Some(BaselineState { kind, style, plane }); self.committed = 0; 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 { BaselineKind::Linear { fixed, @@ -210,82 +252,93 @@ impl DimBaselineCommand { next_offset, .. } => build_linear( + state.plane, *fixed, point, *rotation, *aligned, *perpendicular, *next_offset, - ), + )?, BaselineKind::Angular2Ln { - source, + fixed_line, + fixed_is_first, 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); let (definition, text) = - angular_definition_and_text(*vertex, *fixed_ray, moving, *next_radius); - result.dimension_arc = v3(definition); + angular_definition_and_text(*vertex, *fixed_ray, moving, *next_radius)?; + let mut result = DimensionAngular2Ln::default(); + if *fixed_is_first { + result.first_point = world(state.plane, fixed_line[0]); + result.second_point = world(state.plane, fixed_line[1]); + result.angle_vertex = world(state.plane, *vertex); + result.definition_point = world(state.plane, point); + } else { + result.first_point = world(state.plane, *vertex); + result.second_point = world(state.plane, point); + result.angle_vertex = world(state.plane, fixed_line[0]); + result.definition_point = world(state.plane, fixed_line[1]); + } + 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) } BaselineKind::Angular3Pt { - source, vertex, + fixed_point, 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); let (definition, text) = - angular_definition_and_text(*vertex, *fixed_ray, moving, *next_radius); - result.definition_point = v3(definition); + angular_definition_and_text(*vertex, *fixed_ray, moving, *next_radius)?; + let mut result = DimensionAngular3Pt::new( + world(state.plane, *vertex), + world(state.plane, *fixed_point), + 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) } - BaselineKind::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) + BaselineKind::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 leader = Vec2::from(from_dimension.apply_point(leader.into())); let mut result = DimensionOrdinate::new( - v3(point), - v3(leader), - source.is_ordinate_type_x, + world(state.plane, point), + world(state.plane, leader), + *is_x, ); - result.definition_point = source.definition_point; - result.base.definition_point = source.base.definition_point; - result.base.text_middle_point = v3(leader); + 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; 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) @@ -389,6 +442,7 @@ impl CadCommand for DimBaselineCommand { entity: EntityType::Dimension(dimension), association: DimensionAssociationInput::Infer(None), preserve_base_style: self.preserve_base_style, + continue_command: true, } } @@ -426,18 +480,19 @@ impl CadCommand for DimBaselineCommand { } fn on_mouse_move(&mut self, point: DVec3) -> Option { - self.build_dimension(point).map(|dimension| preview_for_dimension(&dimension)) + self.build_dimension(point) + .map(|dimension| preview_for_dimension(&dimension)) } } fn linear_base( - fixed: DVec3, + fixed: Vec2, rotation: f64, aligned: bool, - definition: DVec3, + definition: Vec2, increment: f64, ) -> BaselineKind { - let perpendicular = DVec3::new(-rotation.sin(), rotation.cos(), 0.0); + let perpendicular = Vec2::new(-rotation.sin(), rotation.cos()); let offset = (definition - fixed).dot(perpendicular); let increment = increment * if offset >= 0.0 { 1.0 } else { -1.0 }; BaselineKind::Linear { @@ -451,39 +506,48 @@ fn linear_base( } fn build_linear( - fixed: DVec3, - point: DVec3, + plane: Plane, + fixed: Vec2, + point: Vec2, rotation: f64, aligned: bool, - perpendicular: DVec3, + source_perpendicular: Vec2, offset: f64, -) -> Dimension { - let target = fixed.dot(perpendicular) + offset; - let first_line = fixed + perpendicular * (target - fixed.dot(perpendicular)); - let second_line = point + perpendicular * (target - point.dot(perpendicular)); - if aligned { - let mut result = DimensionAligned::new(v3(fixed), v3(point)); - 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::Aligned(result) +) -> Option { + let (first_line, second_line) = if aligned { + let mut perpendicular = (point - fixed).normalize()?.perpendicular(); + let source_side = source_perpendicular * offset.signum(); + if perpendicular.dot(source_side) < 0.0 { + perpendicular = -perpendicular; + } + let distance = offset.abs(); + (fixed + perpendicular * distance, point + perpendicular * distance) } else { - let mut result = DimensionLinear::new(v3(fixed), v3(point)); - 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); + let target = fixed.dot(source_perpendicular) + offset; + ( + project_to_line(fixed, source_perpendicular, target), + project_to_line(point, source_perpendicular, target), + ) + }; + if aligned { + let mut result = DimensionAligned::new(world(plane, fixed), world(plane, point)); + 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; - result.base.actual_measurement = result.measurement(); - Dimension::Linear(result) + Some(Dimension::Aligned(result)) + } else { + let mut result = DimensionLinear::new(world(plane, fixed), world(plane, point)); + 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; + Some(Dimension::Linear(result)) } } -fn nearest_end(first: Vector3, second: Vector3, pick: Option) -> (DVec3, DVec3) { - let first = dv(first); - let second = dv(second); +fn nearest_end(first: Vec2, second: Vec2, pick: Option) -> (Vec2, Vec2) { if pick.is_some_and(|point| point.distance_squared(second) < point.distance_squared(first)) { (second, first) } else { @@ -491,104 +555,156 @@ fn nearest_end(first: Vector3, second: Vector3, pick: Option) -> (DVec3, } } -fn nearest_direction(first: DVec3, second: DVec3, vertex: DVec3, pick: Option) -> DVec3 { - if let Some(point) = pick { - let direction = normalized_or(point - vertex, DVec3::X); - if normalized_or(second, DVec3::Y).dot(direction) - > normalized_or(first, DVec3::X).dot(direction) - { - return second; - } - } - first +fn nearest_line(lines: [[Vec2; 2]; 2], pick: Option) -> usize { + let Some(pick) = pick else { + return 0; + }; + let curves = lines.map(|line| { + Curve::Line(Line { + start: line[0].into(), + end: line[1].into(), + }) + }); + nearest_of(curves.iter(), pick.into()) + .map(|(index, _)| index) + .unwrap_or(0) } -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 + .into_iter() + .flatten() + .map(Vec2::length) + .fold(1.0, f64::max); + intersect(&first, &second, Tolerance::new(scale * 1.0e-9)) + .first() + .map(|crossing| Vec2::from(crossing.point)) } fn angular_definition_and_text( - vertex: DVec3, - first: DVec3, - second: DVec3, + vertex: Vec2, + first: Vec2, + second: Vec2, radius: f64, -) -> (DVec3, DVec3) { - 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 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)) +) -> Option<(Vec2, Vec2)> { + let curve = angular_arc(vertex, first, second, radius)?; + Some(( + Vec2::from(curve.point_at(2.0 / 3.0)), + Vec2::from(curve.point_at(0.5)), + )) } -fn normalized_or(value: DVec3, fallback: DVec3) -> DVec3 { - if value.length_squared() > 1.0e-18 { - value.normalize() +fn angular_arc(vertex: Vec2, first: Vec2, second: Vec2, radius: f64) -> Option { + let first = first.normalize()?; + let second = second.normalize()?; + let first_angle = first.angle(); + let second_angle = second.angle(); + let (start_angle, end_angle) = if arc_span(first_angle, second_angle) <= PI { + (first_angle, second_angle) } else { - fallback + (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::Aligned(value) => value.base.actual_measurement = value.measurement(), + 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(), + _ => {} } } fn preview_for_dimension(dimension: &Dimension) -> WireModel { let mut points = Vec::<[f32; 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), - ), - Dimension::Aligned(dimension) => { - let first = dv(dimension.first_point); - let second = dv(dimension.second_point); - let delta = second - first; + Dimension::Linear(value) => { + let plane = plane_from_normal(value.first_point, value.base.normal); linear_preview( &mut points, - first, - second, - delta.y.atan2(delta.x), - dv(dimension.definition_point), + plane, + project(&plane, value.first_point), + project(&plane, value.second_point), + value.rotation, + project(&plane, value.definition_point), ); } - Dimension::Angular2Ln(dimension) => angular_preview( + Dimension::Aligned(value) => { + let plane = plane_from_normal(value.first_point, value.base.normal); + let first = project(&plane, value.first_point); + let second = project(&plane, value.second_point); + linear_preview( + &mut points, + plane, + first, + second, + (second - first).angle(), + project(&plane, value.definition_point), + ); + } + 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, + lines[0][1] - vertex, + lines[1][1] - vertex, + project(&plane, value.dimension_arc), + ); + } + 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), + ); + } + Dimension::Ordinate(value) => segment_world( &mut points, - angular2_vertex(dimension), - dv(dimension.second_point) - angular2_vertex(dimension), - dv(dimension.definition_point) - angular2_vertex(dimension), - dv(dimension.dimension_arc), - ), - 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), - ), - Dimension::Ordinate(dimension) => segment( - &mut points, - dv(dimension.feature_location), - dv(dimension.leader_endpoint), + point(value.feature_location), + point(value.leader_endpoint), ), _ => {} } @@ -629,72 +745,103 @@ fn preview_for_dimension(dimension: &Dimension) -> WireModel { fn linear_preview( points: &mut Vec<[f32; 3]>, - first: DVec3, - second: DVec3, + plane: Plane, + first: Vec2, + second: Vec2, rotation: f64, - definition: DVec3, + definition: Vec2, ) { - let perpendicular = DVec3::new(-rotation.sin(), rotation.cos(), 0.0); + let perpendicular = Vec2::new(-rotation.sin(), rotation.cos()); let target = definition.dot(perpendicular); - let first_line = first + perpendicular * (target - first.dot(perpendicular)); - let second_line = second + perpendicular * (target - second.dot(perpendicular)); - segment(points, first, first_line); - segment(points, second, second_line); - segment(points, first_line, second_line); - append_arrows(points, first_line, second_line); + let first_line = project_to_line(first, perpendicular, target); + let second_line = project_to_line(second, perpendicular, target); + segment_local(points, plane, first, first_line); + segment_local(points, plane, second, second_line); + segment_local(points, plane, first_line, second_line); + append_arrows(points, plane, first_line, second_line); } -fn segment(points: &mut Vec<[f32; 3]>, first: DVec3, second: DVec3) { - points.push(first.as_vec3().to_array()); - points.push(second.as_vec3().to_array()); - points.push([f32::NAN, 0.0, 0.0]); -} - -fn append_arrows(points: &mut Vec<[f32; 3]>, first: DVec3, second: DVec3) { - let axis = normalized_or(second - first, DVec3::X); - let normal = DVec3::new(-axis.y, axis.x, 0.0); +fn append_arrows(points: &mut Vec<[f32; 3]>, plane: Plane, first: Vec2, second: Vec2) { + let Some(axis) = (second - first).normalize() else { + return; + }; + let normal = axis.perpendicular(); let size = first.distance(second).clamp(1.0, 100.0) * 0.035; for (tip, inward) in [(first, axis), (second, -axis)] { let back = tip + inward * size; - segment(points, tip, back + normal * size * 0.4); - segment(points, tip, back - normal * size * 0.4); + segment_local(points, plane, tip, back + normal * size * 0.4); + segment_local(points, plane, tip, back - normal * size * 0.4); } } fn angular_preview( points: &mut Vec<[f32; 3]>, - vertex: DVec3, - first: DVec3, - second: DVec3, - arc_point: DVec3, + plane: Plane, + vertex: Vec2, + first: Vec2, + second: Vec2, + arc_point: Vec2, ) { - 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; - } - segment(points, vertex, vertex + normalized_or(first, DVec3::X) * radius); - segment(points, vertex, vertex + normalized_or(second, DVec3::Y) * radius); + 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_local(points, plane, vertex, vertex + first_direction * radius); + segment_local(points, plane, vertex, vertex + second_direction * radius); for index in 0..=24 { - let angle = start + sweep * index as f64 / 24.0; - points.push( - (vertex + DVec3::new(angle.cos(), angle.sin(), 0.0) * radius) - .as_vec3() - .to_array(), - ); + push_local(points, plane, Vec2::from(arc.point_at(index as f64 / 24.0))); } } -fn dv(point: Vector3) -> DVec3 { - DVec3::new(point.x, point.y, point.z) +fn project_to_line(point: Vec2, perpendicular: Vec2, offset: f64) -> Vec2 { + point + perpendicular * (offset - point.dot(perpendicular)) } -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_local(points: &mut Vec<[f32; 3]>, plane: Plane, first: Vec2, second: Vec2) { + segment_world(points, plane.point_at(first.into()), plane.point_at(second.into())); +} + +fn segment_world(points: &mut Vec<[f32; 3]>, first: [f64; 3], second: [f64; 3]) { + points.push(float3(first)); + points.push(float3(second)); + points.push([f32::NAN, 0.0, 0.0]); +} + +fn push_local(points: &mut Vec<[f32; 3]>, plane: Plane, point: Vec2) { + points.push(float3(plane.point_at(point.into()))); +} + +fn float3(value: [f64; 3]) -> [f32; 3] { + [value[0] as f32, value[1] as f32, value[2] as f32] } inventory::submit!(crate::command::CommandRegistration { names: &["DIMBASELINE"] }); diff --git a/src/modules/annotate/dim_continue.rs b/src/modules/annotate/dim_continue.rs index b7eb336b7..fe5a255d9 100644 --- a/src/modules/annotate/dim_continue.rs +++ b/src/modules/annotate/dim_continue.rs @@ -1,17 +1,10 @@ -// 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. - -use acadrust::entities::{Dimension, DimensionLinear}; +use acadrust::entities::{Dimension, DimensionBase, DimensionLinear}; use acadrust::types::Vector3; use acadrust::EntityType; -use glam::{DVec3, Vec3}; +use cadkernel::space::Plane; +use glam::DVec3; -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, DimensionAssociationInput}; use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::scene::model::wire_model::WireModel; use crate::t; @@ -27,75 +20,112 @@ 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, - 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); + 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; + } +} + +struct ContinueState { + chain_p1: [f64; 2], + rotation: f64, + dim_line_perp: f64, + perpendicular: [f64; 2], + plane: Plane, + style: SourceStyle, +} + +pub struct DimContinueCommand { + state: Option, + preserve_base_style: bool, +} + +impl DimContinueCommand { + pub fn new(preserve_base_style: bool) -> Self { Self { - chain_p1: p2, - rotation, - text_rotation, - dim_line_perp, - perp, - ready: true, + state: None, + preserve_base_style, } } + + pub fn from_dimension(dimension: &Dimension, preserve_base_style: bool) -> Self { + let (first, second, definition, rotation) = match dimension { + Dimension::Linear(source) => ( + source.first_point, + source.second_point, + source.base.definition_point, + source.rotation, + ), + Dimension::Aligned(source) => { + let plane = plane_from_normal(source.first_point, source.base.normal); + let first = plane.project(point(source.first_point)).unwrap_or([0.0; 2]); + let second = plane.project(point(source.second_point)).unwrap_or(first); + let delta = [second[0] - first[0], second[1] - first[1]]; + ( + source.first_point, + source.second_point, + source.base.definition_point, + delta[1].atan2(delta[0]), + ) + } + _ => return Self::new(preserve_base_style), + }; + let plane = plane_from_normal(first, dimension.base().normal); + let second = plane.project(point(second)).unwrap_or([0.0; 2]); + let definition = plane.project(point(definition)).unwrap_or(second); + let perpendicular = [-rotation.sin(), rotation.cos()]; + let dim_line_perp = dot(definition, perpendicular); + Self { + state: Some(ContinueState { + chain_p1: second, + rotation, + dim_line_perp, + perpendicular, + plane, + style: SourceStyle::from_base(dimension.base()), + }), + preserve_base_style, + } + } + + fn placement(&self, point_world: DVec3) -> Option<([f64; 2], [f64; 2], [f64; 2])> { + let state = self.state.as_ref()?; + let second = state.plane.project(point_world.to_array())?; + let first_line = project_to_line( + state.chain_p1, + state.perpendicular, + state.dim_line_perp, + ); + let second_line = project_to_line( + second, + state.perpendicular, + state.dim_line_perp, + ); + Some((second, first_line, second_line)) + } } impl CadCommand for DimContinueCommand { @@ -104,61 +134,61 @@ impl CadCommand for DimContinueCommand { } fn prompt(&self) -> String { - if !self.ready { + if self.state.is_none() { t!("DIMCONTINUE No base dimension found. Place a dimension first.").into_owned() } else { t!("DIMCONTINUE Specify a second extension line origin (Enter to exit):").into_owned() } } - fn on_point(&mut self, pt: DVec3) -> CmdResult { - if !self.ready { + fn on_point(&mut self, point_world: DVec3) -> CmdResult { + let Some((second, first_line, second_line)) = self.placement(point_world) else { return CmdResult::Cancel; + }; + let state = self.state.as_mut().expect("continue state"); + let mut dimension = DimensionLinear::new( + vector3(state.plane.point_at(state.chain_p1)), + vector3(state.plane.point_at(second)), + ); + dimension.rotation = state.rotation; + state.style.apply(&mut dimension.base); + dimension.definition_point = vector3(state.plane.point_at(first_line)); + dimension.base.definition_point = dimension.definition_point; + dimension.base.text_middle_point = vector3(state.plane.point_at([ + (first_line[0] + second_line[0]) * 0.5, + (first_line[1] + second_line[1]) * 0.5, + ])); + dimension.base.insertion_point = dimension.base.text_middle_point; + let axis = [state.rotation.cos(), state.rotation.sin()]; + dimension.base.actual_measurement = dot( + [ + second[0] - state.chain_p1[0], + second[1] - state.chain_p1[1], + ], + axis, + ) + .abs(); + state.chain_p1 = second; + + CmdResult::CommitDimension { + entity: EntityType::Dimension(Dimension::Linear(dimension)), + association: DimensionAssociationInput::Infer(None), + preserve_base_style: self.preserve_base_style, + continue_command: true, } - 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; - } - - // 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; - } - // 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)); + fn on_mouse_move(&mut self, point_world: DVec3) -> Option { + let (second, first_line, second_line) = self.placement(point_world)?; + let state = self.state.as_ref()?; + let first = state.plane.point_at(state.chain_p1); + let second = state.plane.point_at(second); + let first_line = state.plane.point_at(first_line); + let second_line = state.plane.point_at(second_line); Some(WireModel { point_marker: None, taper_widths: Vec::new(), @@ -177,14 +207,14 @@ impl CadCommand for DimContinueCommand { 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], + float3(first), + float3(first_line), [f32::NAN, 0.0, 0.0], - [pt.x, pt.y, pt.z], - [dim_line_pt2.x, dim_line_pt2.y, dim_line_pt2.z], + float3(second), + float3(second_line), [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], + float3(first_line), + float3(second_line), ], points_low: Vec::new(), color: WireModel::CYAN, @@ -192,22 +222,50 @@ impl CadCommand for DimContinueCommand { pattern_length: 0.0, pattern: [0.0; 8], line_weight_px: 1.0, - snap_pts: vec![], - tangent_geoms: vec![], + snap_pts: Vec::new(), + tangent_geoms: Vec::new(), aci: 0, - key_vertices: vec![], + key_vertices: Vec::new(), aabb: WireModel::UNBOUNDED_AABB, plinegen: true, - fill_tris: vec![], + fill_tris: Vec::new(), fill_tris_low: Vec::new(), }) } } -fn v3(p: DVec3) -> Vector3 { - Vector3::new(p.x, p.y, p.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_to_line(point: [f64; 2], perpendicular: [f64; 2], offset: f64) -> [f64; 2] { + let distance = offset - dot(point, perpendicular); + [ + point[0] + perpendicular[0] * distance, + point[1] + perpendicular[1] * distance, + ] +} -// ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["DIMCONTINUE"] }); // DimContinueCommand +fn dot(first: [f64; 2], second: [f64; 2]) -> f64 { + first[0] * second[0] + first[1] * second[1] +} + +fn point(value: Vector3) -> [f64; 3] { + [value.x, value.y, value.z] +} + +fn vector3(value: [f64; 3]) -> Vector3 { + Vector3::new(value[0], value[1], value[2]) +} + +fn float3(value: [f64; 3]) -> [f32; 3] { + [value[0] as f32, value[1] as f32, value[2] as f32] +} + +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..9a517c968 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, + continue_command: false, } } } diff --git a/src/modules/annotate/linear_dim.rs b/src/modules/annotate/linear_dim.rs index fd6d6d6c5..53d09b08e 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, + continue_command: false, } } } diff --git a/src/modules/annotate/ordinate_dim.rs b/src/modules/annotate/ordinate_dim.rs index 221523c7f..1437ba953 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, + continue_command: false, } } } diff --git a/src/modules/annotate/radius_dim.rs b/src/modules/annotate/radius_dim.rs index 10681bad8..4cb6acc9d 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, + 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() +}