diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index bf03b591..21061fd0 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -801,19 +801,24 @@ impl OpenCADStudio { // the structure snapshot fallback. let delta_safe = self.delta_add_safe(i, &entity); let pending = self.begin_undo(i, label, 1, delta_safe); - let is_linear_dimension = matches!( + let is_associative_dimension = matches!( entity, - acadrust::EntityType::Dimension(acadrust::entities::Dimension::Linear(_)) + acadrust::EntityType::Dimension( + acadrust::entities::Dimension::Linear(_) + | acadrust::entities::Dimension::Aligned(_) + ) ); + let association_enabled = + self.tabs[i].scene.document.header.dimension_associativity == 2; let committed = self.commit_entity_handle(entity); - if is_linear_dimension { + if is_associative_dimension && association_enabled { if let Some(handle) = committed { let sources = self.tabs[i] .scene - .infer_linear_dimension_sources(handle); + .infer_dimension_sources(handle); self.tabs[i] .scene - .attach_linear_dimension_association(handle, sources); + .attach_dimension_association(handle, sources); } } self.tabs[i].dirty = true; @@ -1205,19 +1210,24 @@ impl OpenCADStudio { let label = self.history_label_from_active_cmd(i, "ENTITY"); let delta_safe = self.delta_add_safe(i, &entity); let pending = self.begin_undo(i, label, 1, delta_safe); - let is_linear_dimension = matches!( + let is_associative_dimension = matches!( entity, - acadrust::EntityType::Dimension(acadrust::entities::Dimension::Linear(_)) + acadrust::EntityType::Dimension( + acadrust::entities::Dimension::Linear(_) + | acadrust::entities::Dimension::Aligned(_) + ) ); + let association_enabled = + self.tabs[i].scene.document.header.dimension_associativity == 2; let committed = self.commit_entity_handle(entity); - if is_linear_dimension { + if is_associative_dimension && association_enabled { if let Some(handle) = committed { let sources = self.tabs[i] .scene - .infer_linear_dimension_sources(handle); + .infer_dimension_sources(handle); self.tabs[i] .scene - .attach_linear_dimension_association(handle, sources); + .attach_dimension_association(handle, sources); } } self.tabs[i].dirty = true; @@ -1229,25 +1239,62 @@ impl OpenCADStudio { self.commit_undo_delta(i, pd); } } - CmdResult::CommitAssociativeDimension { entity, source } => { - let label = self.history_label_from_active_cmd(i, "DIMLINEAR"); - let pending = self.begin_undo(i, label, 1, false); - if let Some(handle) = self.commit_entity_handle(entity) { - self.tabs[i] - .scene - .attach_linear_dimension_association(handle, [Some(source), Some(source)]); - self.tabs[i].scene.bump_entities(&[ - (handle, crate::scene::ChangeKind::Modified), - (source, crate::scene::ChangeKind::Modified), - ]); - } + CmdResult::CommitDimension { mut entity, source } => { + let label = self.history_label_from_active_cmd(i, "DIMENSION"); + let association_mode = self.tabs[i] + .scene + .document + .header + .dimension_associativity; + let pending = if association_mode == 0 { + crate::scene::creation_style::apply_current_creation_styles( + &self.tabs[i].scene.document, + &mut entity, + ); + let pieces = crate::modules::draw::modify::explode::explode_entity( + &entity, + &self.tabs[i].scene.document, + ); + let delta_safe = pieces + .iter() + .all(|piece| self.delta_add_safe(i, piece)); + let pending = self.begin_undo(i, label, pieces.len(), delta_safe); + for piece in pieces { + self.commit_entity(piece); + } + pending + } 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 association_mode == 2 { + let sources = source.map_or_else( + || self.tabs[i].scene.infer_dimension_sources(handle), + |source| [Some(source), Some(source)], + ); + self.tabs[i] + .scene + .attach_dimension_association(handle, sources); + let mut changes = vec![ + (handle, crate::scene::ChangeKind::Modified), + ]; + changes.extend(sources.into_iter().flatten().map(|source| { + (source, crate::scene::ChangeKind::Modified) + })); + changes.sort_by_key(|(handle, _)| handle.value()); + changes.dedup_by_key(|(handle, _)| handle.value()); + self.tabs[i].scene.bump_entities(&changes); + } + } + pending + }; 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(pending) = pending { - self.commit_undo_delta(i, pending); + if let Some(pd) = pending { + self.commit_undo_delta(i, pd); } } CmdResult::CommitSolid { diff --git a/src/command.rs b/src/command.rs index 89b8d038..ea235802 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1212,10 +1212,12 @@ pub enum CmdResult { CommitEntitiesAndExit(Vec), /// Commit an acadrust entity to the document and end the command. CommitAndExit(EntityType), - /// Commit an object-selected linear dimension and retain its source link. - CommitAssociativeDimension { + /// Commit a linear/aligned dimension while honoring the drawing's + /// association mode. `source` is present for object-selection workflows; + /// point-created dimensions resolve sources from their snapped endpoints. + CommitDimension { entity: EntityType, - source: Handle, + source: Option, }, /// 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/linear_dim.rs b/src/modules/annotate/linear_dim.rs index a3768011..f88b593d 100644 --- a/src/modules/annotate/linear_dim.rs +++ b/src/modules/annotate/linear_dim.rs @@ -193,10 +193,9 @@ impl CadCommand for LinearDimensionCommand { let entity = self.plane.place_entity(EntityType::Dimension( Dimension::Linear(dim), )); - if let Some(source) = self.source_handle { - CmdResult::CommitAssociativeDimension { entity, source } - } else { - CmdResult::CommitAndExit(entity) + CmdResult::CommitDimension { + entity, + source: self.source_handle, } } } @@ -366,11 +365,42 @@ fn dimension_line_offset(second: DVec3, point: DVec3, axis: DVec3) -> f64 { (point - second).dot(perpendicular) } -fn dimension_source_points(entity: &EntityType, click: DVec3) -> Option<(DVec3, DVec3)> { +pub(crate) fn dimension_source_points( + entity: &EntityType, + click: DVec3, +) -> Option<(DVec3, DVec3)> { let point = |p: Vector3| DVec3::new(p.x, p.y, p.z); match entity { EntityType::Line(line) => Some((point(line.start), point(line.end))), EntityType::Arc(arc) => Some((point(arc.start_point_wcs()), point(arc.end_point_wcs()))), + EntityType::Circle(circle) => { + let click = crate::scene::view::transform::wcs_point_to_ocs( + (click.x, click.y, click.z), + (circle.normal.x, circle.normal.y, circle.normal.z), + ); + let delta = DVec3::new( + click.0 - circle.center.x, + click.1 - circle.center.y, + 0.0, + ); + let direction = if delta.length_squared() > 1e-24 { + delta.normalize() + } else { + DVec3::X + }; + let first = [ + circle.center.x + direction.x * circle.radius, + circle.center.y + direction.y * circle.radius, + ]; + let second = [ + circle.center.x - direction.x * circle.radius, + circle.center.y - direction.y * circle.radius, + ]; + Some(( + ocs_point(first, circle.center.z, circle.normal), + ocs_point(second, circle.center.z, circle.normal), + )) + } EntityType::LwPolyline(polyline) => nearest_planar_source( polyline .vertices diff --git a/src/scene/dimension_assoc.rs b/src/scene/dimension_assoc.rs index bda8f168..49d9d70d 100644 --- a/src/scene/dimension_assoc.rs +++ b/src/scene/dimension_assoc.rs @@ -56,6 +56,9 @@ fn source_points(entity: &EntityType) -> Vec { } fn source_marker(entity: &EntityType, point: Vector3) -> Option { + if matches!(entity, EntityType::Circle(_)) { + return Some(0); + } source_points(entity) .into_iter() .enumerate() @@ -69,11 +72,82 @@ fn source_marker(entity: &EntityType, point: Vector3) -> Option { fn resolve_reference(scene: &Scene, reference: &AssocDimensionReference) -> Option { let source = *reference.xrefs.first()?; let entity = scene.document.get_entity(source)?; + if let EntityType::Circle(circle) = entity { + let stored = crate::scene::view::transform::wcs_point_to_ocs( + ( + reference.osnap_point.x, + reference.osnap_point.y, + reference.osnap_point.z, + ), + (circle.normal.x, circle.normal.y, circle.normal.z), + ); + let dx = stored.0 - circle.center.x; + let dy = stored.1 - circle.center.y; + let length = dx.hypot(dy); + let stored_angle = dy.atan2(dx); + let angle = if reference.osnap_distance.abs() > 1e-12 + || stored_angle.abs() <= 1e-12 + { + reference.osnap_distance + } else { + stored_angle + }; + let (sin, cos) = angle.sin_cos(); + let (ux, uy) = if angle.is_finite() { + (cos, sin) + } else if length > 1e-12 { + (dx / length, dy / length) + } else { + (1.0, 0.0) + }; + return Some(ocs_point( + circle.center.x + ux * circle.radius, + circle.center.y + uy * circle.radius, + circle.center.z, + circle.normal, + )); + } source_points(entity) .get(reference.main_gs_marker.max(0) as usize) .copied() } +fn dimension_points(dimension: &Dimension) -> Option<[Vector3; 2]> { + match dimension { + Dimension::Linear(linear) => Some([linear.first_point, linear.second_point]), + Dimension::Aligned(aligned) => Some([aligned.first_point, aligned.second_point]), + _ => None, + } +} + +fn source_distance_squared(entity: &EntityType, point: Vector3) -> Option { + if let EntityType::Circle(circle) = entity { + let point = crate::scene::view::transform::wcs_point_to_ocs( + (point.x, point.y, point.z), + (circle.normal.x, circle.normal.y, circle.normal.z), + ); + let radial_error = + (point.0 - circle.center.x).hypot(point.1 - circle.center.y) - circle.radius; + let plane_error = point.2 - circle.center.z; + return Some(radial_error * radial_error + plane_error * plane_error); + } + source_points(entity) + .into_iter() + .map(|candidate| point_distance_squared(candidate, point)) + .min_by(f64::total_cmp) +} + +fn source_parameter(entity: &EntityType, point: Vector3) -> f64 { + let EntityType::Circle(circle) = entity else { + return 0.0; + }; + let point = crate::scene::view::transform::wcs_point_to_ocs( + (point.x, point.y, point.z), + (circle.normal.x, circle.normal.y, circle.normal.z), + ); + (point.1 - circle.center.y).atan2(point.0 - circle.center.x) +} + pub(crate) fn dimension_is_associative( document: &acadrust::CadDocument, dimension: Handle, @@ -97,34 +171,43 @@ pub(crate) fn dimension_is_associative( } impl Scene { - pub(crate) fn attach_linear_dimension_association( + pub(crate) fn attach_dimension_association( &mut self, dimension: Handle, sources: [Option; 2], ) { - let Some(EntityType::Dimension(Dimension::Linear(linear))) = - self.document.get_entity(dimension) + let Some(EntityType::Dimension(entity)) = self.document.get_entity(dimension) else { return; }; - let first_point = linear.first_point; - let second_point = linear.second_point; - let source_data = [first_point, second_point].map(|point| point); - let resolved: [Option<(Handle, i32)>; 2] = std::array::from_fn(|index| { + let Some(source_data) = dimension_points(entity) else { + return; + }; + let resolved: [Option<(Handle, i32, f64)>; 2] = std::array::from_fn(|index| { let source = sources[index]?; let entity = self.document.get_entity(source)?; - source_marker(entity, source_data[index]).map(|marker| (source, marker)) + source_marker(entity, source_data[index]).map(|marker| { + ( + source, + marker, + source_parameter(entity, source_data[index]), + ) + }) }); if resolved.iter().all(Option::is_none) { return; } - let reference = |source: Handle, marker: i32, point: Vector3| AssocDimensionReference { + let reference = |source: Handle, + marker: i32, + parameter: f64, + point: Vector3| AssocDimensionReference { class_name: "AcDbOsnapPointRef".to_string(), osnap_type: 1, xrefs: vec![source], main_subent_type: 1, main_gs_marker: marker, + osnap_distance: parameter, osnap_point: point, ..AssocDimensionReference::default() }; @@ -132,9 +215,14 @@ impl Scene { std::array::from_fn(|_| Vec::new()); let mut associativity = 0; for (index, resolved) in resolved.into_iter().enumerate() { - if let Some((source, marker)) = resolved { + if let Some((source, marker, parameter)) = resolved { associativity |= 1 << index; - references[index].push(reference(source, marker, source_data[index])); + references[index].push(reference( + source, + marker, + parameter, + source_data[index], + )); } } @@ -165,24 +253,23 @@ impl Scene { } } - pub(crate) fn infer_linear_dimension_sources( + pub(crate) fn infer_dimension_sources( &self, dimension: Handle, ) -> [Option; 2] { - let Some(EntityType::Dimension(Dimension::Linear(linear))) = - self.document.get_entity(dimension) + let Some(EntityType::Dimension(entity)) = self.document.get_entity(dimension) else { return [None, None]; }; - [linear.first_point, linear.second_point].map(|point| { + let Some(points) = dimension_points(entity) else { + return [None, None]; + }; + points.map(|point| { self.document .entities() .filter(|entity| entity.common().handle != dimension) .filter_map(|entity| { - source_points(entity) - .into_iter() - .map(|candidate| point_distance_squared(candidate, point)) - .min_by(f64::total_cmp) + source_distance_squared(entity, point) .map(|distance| (distance, entity.common().handle)) }) .filter(|(distance, _)| *distance <= 1e-16) @@ -231,19 +318,35 @@ impl Scene { if first.is_none() && second.is_none() { continue; } - if let Some(EntityType::Dimension(Dimension::Linear(linear))) = + let Some(EntityType::Dimension(dimension)) = self.document.get_entity_mut(association.dimension) - { - if let Some(first) = first { - linear.first_point = first; + else { + continue; + }; + match dimension { + Dimension::Linear(linear) => { + if let Some(first) = first { + linear.first_point = first; + } + if let Some(second) = second { + linear.second_point = second; + } + linear.base.actual_measurement = linear.measurement(); + linear.base.definition_point = linear.definition_point; } - if let Some(second) = second { - linear.second_point = second; + Dimension::Aligned(aligned) => { + if let Some(first) = first { + aligned.first_point = first; + } + if let Some(second) = second { + aligned.second_point = second; + } + aligned.base.actual_measurement = aligned.measurement(); + aligned.base.definition_point = aligned.definition_point; } - linear.base.actual_measurement = linear.measurement(); - linear.base.definition_point = linear.definition_point; - refreshed.push((association.dimension, ChangeKind::Modified)); + _ => continue, } + refreshed.push((association.dimension, ChangeKind::Modified)); } refreshed }