diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 6a3584ab0..1c435fd22 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -3686,6 +3686,8 @@ impl OpenCADStudio { &[first], self.constraint_solve_mode, ); + // DYNCONSTRAINTDISPLAY 0 also keeps a new dynamic dimension off screen. + self.tabs[i].scene.refresh_hidden_dynamic_dimensions(); self.tabs[i].dirty = true; self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 79d66aa27..cf9d4686d 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -1506,6 +1506,16 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } + "DIMCONSTRAINT" => { + use crate::modules::parametric::DimConstraintMenuCommand; + // Only the dynamic form exists here. + self.command_line + .push_output("Current settings: Constraint form = Dynamic"); + let new_cmd = DimConstraintMenuCommand; + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + "EDCONSTRAINT" => { use crate::modules::parametric::EqualDistanceConstraintCommand; let new_cmd = EqualDistanceConstraintCommand::new(); diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index 0b5bc9b14..e997869cd 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -1052,6 +1052,8 @@ impl OpenCADStudio { | "CONSTRAINTINFER" | "CONSTRAINTBARDISPLAY" | "CONSTRAINTBARMODE" + | "CONSTRAINTNAMEFORMAT" + | "DYNCONSTRAINTDISPLAY" ) => { return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i); @@ -1075,7 +1077,7 @@ impl OpenCADStudio { if name.is_empty() || name == "?" { self.command_line.push_info(&super::plotvars::setvar_listing()); self.command_line.push_info( - crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY CONSTRAINTBARMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(), + crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY CONSTRAINTBARMODE CONSTRAINTNAMEFORMAT DYNCONSTRAINTDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(), ); } else { if name == "CETRANSPARENCY" { @@ -1275,17 +1277,26 @@ impl OpenCADStudio { | "CONSTRAINTINFER" | "CONSTRAINTBARDISPLAY" | "CONSTRAINTBARMODE" + | "CONSTRAINTNAMEFORMAT" + | "DYNCONSTRAINTDISPLAY" ) { let current = match name.as_str() { "CONSTRAINTSOLVEMODE" => i16::from(self.constraint_solve_mode), "CONSTRAINTINFER" => i16::from(self.constraint_infer), "CONSTRAINTBARDISPLAY" => self.constraint_bar_display, "CONSTRAINTBARMODE" => self.constraint_bar_mode, + "CONSTRAINTNAMEFORMAT" => { + i16::from(self.tabs[i].scene.constraint_name_format) + } + "DYNCONSTRAINTDISPLAY" => { + i16::from(self.tabs[i].scene.dynamic_constraint_display) + } _ => unreachable!(), }; let maximum = match name.as_str() { "CONSTRAINTBARDISPLAY" => 3, "CONSTRAINTBARMODE" => 4095, + "CONSTRAINTNAMEFORMAT" => 2, _ => 1, }; if let Some(value) = &value { @@ -1304,6 +1315,16 @@ impl OpenCADStudio { self.constraint_bar_display = mode } "CONSTRAINTBARMODE" => self.constraint_bar_mode = mode, + "CONSTRAINTNAMEFORMAT" => { + self.tabs[i].scene.constraint_name_format = mode as u8; + self.tabs[i].scene.refresh_dynamic_dimension_texts(); + self.tabs[i].dirty = true; + } + "DYNCONSTRAINTDISPLAY" => { + self.tabs[i].scene.dynamic_constraint_display = + mode != 0; + self.tabs[i].scene.refresh_hidden_dynamic_dimensions(); + } _ => unreachable!(), } self.persist_settings_if_changed(); diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index 6cf37e0a5..906dea36d 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -6295,6 +6295,45 @@ fn dimension_text_is_outside(dim: &Dimension, style: Option<&DimStyle>) -> bool } } +/// Where a dynamic dimension's lock mark sits: just before the text on its +/// baseline, with the direction that points away from the text. +pub(crate) fn dynamic_dimension_lock_anchor( + document: &CadDocument, + dim: &Dimension, + anno_scale: f64, +) -> Option<(Vector3, Vector3)> { + let style_name = &dim.base().style_name; + let source_style = document.dim_styles.iter().find(|s| { + s.name.eq_ignore_ascii_case(style_name) + || (style_name.trim().is_empty() && s.name.eq_ignore_ascii_case("Standard")) + }); + let effective_style = source_style.map(|style| resolved_dimension_style(style, dim, document)); + let style = effective_style.as_ref(); + let dim_scale = style + .map(|s| if s.dimscale > 1e-6 { s.dimscale } else { anno_scale }) + .unwrap_or(1.0); + let text_height = style.map(|s| s.dimtxt * dim_scale).unwrap_or(2.5); + let value = dimension_text_value(dim, style)?; + let stack_scale = style.map(dimtfac_or_one).unwrap_or(1.0); + let half_width = text_cells(&value, stack_scale) * text_height * CELL_WIDTH * 0.5; + let pos = dimension_text_pos_f64(dim, style, text_height, dim_scale); + let rot = if dim.base().text_rotation.abs() > 1e-9 { + dim.base().text_rotation + } else { + dimension_text_natural_rotation(dim) + }; + let (sr, cr) = rot.sin_cos(); + let outward = Vector3::new(-cr, -sr, 0.0); + Some(( + Vector3::new( + pos.x + outward.x * half_width, + pos.y + outward.y * half_width, + pos.z, + ), + outward, + )) +} + fn dimension_text_natural_rotation(dim: &Dimension) -> f64 { let angle = match dim { Dimension::Linear(d) => d.rotation, diff --git a/src/modules/parametric/dim_constraint.rs b/src/modules/parametric/dim_constraint.rs index a4ccb007d..522119203 100644 --- a/src/modules/parametric/dim_constraint.rs +++ b/src/modules/parametric/dim_constraint.rs @@ -390,6 +390,70 @@ impl CadCommand for DimConstraintCommand { } } +/// `DIMCONSTRAINT`: the reference's option front end for the dimensional +/// constraint family; each choice runs the focused command. +pub struct DimConstraintMenuCommand; + +impl DimConstraintMenuCommand { + fn dispatch(keyword: &str) -> Option<&'static str> { + Some(match keyword { + "L" | "LINEAR" => "DCLINEAR", + "H" | "HORIZONTAL" => "DCHORIZONTAL", + "V" | "VERTICAL" => "DCVERTICAL", + "A" | "ALIGNED" => "DCALIGNED", + "AN" | "ANGULAR" => "DCANGULAR", + "R" | "RADIAL" | "RADIUS" => "DCRADIUS", + "D" | "DIAMETER" => "DCDIAMETER", + "C" | "CONVERT" => "DCCONVERT", + _ => return None, + }) + } +} + +impl CadCommand for DimConstraintMenuCommand { + fn name(&self) -> &'static str { + "DIMCONSTRAINT" + } + + fn prompt(&self) -> String { + "DIMCONSTRAINT Enter dimensional constraint option [Linear/Horizontal/Vertical/Aligned/ANgular/Radial/Diameter/Convert] :".to_string() + } + + fn options(&self) -> Vec { + vec![ + CmdOption::new("Linear", "L"), + CmdOption::new("Horizontal", "H"), + CmdOption::new("Vertical", "V"), + CmdOption::new("Aligned", "A"), + CmdOption::new("ANgular", "AN"), + CmdOption::new("Radial", "R"), + CmdOption::new("Diameter", "D"), + CmdOption::new("Convert", "C"), + ] + } + + fn wants_text_input(&self) -> bool { + true + } + + fn on_text_input(&mut self, text: &str) -> Option { + let keyword = text.trim().trim_start_matches('_').to_ascii_uppercase(); + Self::dispatch(&keyword).map(|command| CmdResult::Dispatch(command.to_string())) + } + + fn on_point(&mut self, _point: DVec3) -> CmdResult { + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + CmdResult::Dispatch("DCALIGNED".to_string()) + } + + fn on_escape(&mut self) -> CmdResult { + CmdResult::Cancel + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/modules/parametric/mod.rs b/src/modules/parametric/mod.rs index 44ad59d44..6353d3672 100644 --- a/src/modules/parametric/mod.rs +++ b/src/modules/parametric/mod.rs @@ -26,7 +26,7 @@ mod tools; mod value; pub use coincident::{coincident_tool, CoincidentConstraintCommand}; pub use concentric::ConcentricConstraintCommand; -pub use dim_constraint::{DimConstraintAxis, DimConstraintCommand}; +pub use dim_constraint::{DimConstraintAxis, DimConstraintCommand, DimConstraintMenuCommand}; pub use constraint_bar::ConstraintBarOptionCommand; pub use equal_command::EqualConstraintCommand; pub use equal_distance::{equal_distance_tool, EqualDistanceConstraintCommand}; @@ -158,7 +158,7 @@ inventory::submit!(crate::command::CommandRegistration { names: &[ "AUTOCONSTRAIN", "CONSTRAINTSETTINGS", "GCSMOOTH", "GCSHOW", "GCHIDE", "GCRESET", "GCSHOWALL", "GCHIDEALL", "DCSHOW", "DCHIDE", "DCSHOWALL", "DCHIDEALL", - "DCCONVERT", "DELCONSTRAINT", + "DCCONVERT", "DELCONSTRAINT", "DIMCONSTRAINT", ] }); diff --git a/src/scene/dwg_native_constraints.rs b/src/scene/dwg_native_constraints.rs index e36ca0c6d..dc8174bf5 100644 --- a/src/scene/dwg_native_constraints.rs +++ b/src/scene/dwg_native_constraints.rs @@ -2,7 +2,8 @@ use acadrust::objects::{ Assoc2dConstraintGroup, AssocAction, AssocActionDependency, AssocConstraintNode, - AssocConstraintNodeData, AssocDependency, AssocEvalValue, AssocEvalVariant, + AssocConstraintNodeData, AssocDependency, AssocDimDependencyBody, AssocEvalValue, + AssocEvalVariant, AssocGeomDependency, AssocNetwork, AssocPersistentSubentId, AssocValueDependency, AssocVariable, AssociativeData, AssociativeObject, ObjectType, }; @@ -1291,6 +1292,67 @@ impl Allocator<'_> { } handle } + + /// The `AcDbAssocDependency` + `AcDbAssocDimDependencyBody` pair that + /// ties a dimensional constraint to the dynamic dimension showing it. + fn dimension_dependency( + &mut self, + group_handle: Handle, + dimension: Handle, + name: &str, + dependency_id: i32, + ) -> Handle { + let handle = self.document.allocate_handle(); + let body = self.insert_associative( + handle, + "ACDBASSOCDIMDEPENDENCYBODY", + "AcDbAssocDimDependencyBody", + AssociativeData::DimDependencyBody(AssocDimDependencyBody { + dependency_body_version: 1, + base_version: 1, + name: name.to_string(), + class_version: 1, + }), + ); + self.insert_associative_at( + handle, + group_handle, + "ACDBASSOCDEPENDENCY", + "AcDbAssocDependency", + AssociativeData::Dependency(AssocDependency { + class_version: 2, + is_read_dependency: true, + is_write_dependency: true, + is_attached_to_object: true, + is_delegating_to_owning_action: true, + dependent_on: dimension, + dependency_body: body, + dependency_body_id: dependency_id, + ..Default::default() + }), + ); + // Earlier materializations' dependencies are gone by now; drop + // their reactor entries before adding this one. + let live: Vec = self + .document + .get_entity(dimension) + .map(|entity| { + entity + .common() + .reactors + .iter() + .copied() + .filter(|reactor| self.document.objects.contains_key(reactor)) + .collect() + }) + .unwrap_or_default(); + if let Some(entity) = self.document.get_entity_mut(dimension) { + let reactors = &mut entity.common_mut().reactors; + *reactors = live; + reactors.push(handle); + } + handle + } } /// Sets the dependency on whole-geometry nodes. Implicit points retain a @@ -1355,6 +1417,27 @@ fn set_value_dependency(data: &mut AssocConstraintNodeData, handle: Handle) { } } +/// Patches a dimensional-constraint node's `dimension_dependency` field. +fn set_dimension_dependency(data: &mut AssocConstraintNodeData, handle: Handle) { + match data { + AssocConstraintNodeData::Distance { + dimension_dependency, + .. + } + | AssocConstraintNodeData::Angle { + dimension_dependency, + .. + } + | AssocConstraintNodeData::RadiusDiameter { + dimension_dependency, + .. + } => { + *dimension_dependency = handle; + } + _ => {} + } +} + /// Associative classes that must have real class numbers in DWG/DXF. const ASSOC_CLASSES: &[(&str, &str, i32)] = &[ ( @@ -1366,6 +1449,8 @@ const ASSOC_CLASSES: &[(&str, &str, i32)] = &[ ("ACDBASSOCVARIABLE", "AcDbAssocVariable", 45), ("ACDBASSOCGEOMDEPENDENCY", "AcDbAssocGeomDependency", 29), ("ACDBASSOCVALUEDEPENDENCY", "AcDbAssocValueDependency", 29), + ("ACDBASSOCDEPENDENCY", "AcDbAssocDependency", 29), + ("ACDBASSOCDIMDEPENDENCYBODY", "AcDbAssocDimDependencyBody", 45), ]; fn ensure_associative_classes_registered(document: &mut CadDocument) { @@ -1806,6 +1891,19 @@ fn driving_value( } } +/// The dynamic dimension an `AcDbAssocDependency` ties to a dimensional +/// constraint node. +fn dependency_dimension(document: &CadDocument, dependency: Handle) -> Option { + let ObjectType::Associative(object) = document.objects.get(&dependency)? else { + return None; + }; + let AssociativeData::Dependency(dependency) = &object.data else { + return None; + }; + let target = dependency.dependent_on; + matches!(document.get_entity(target)?, EntityType::Dimension(_)).then_some(target) +} + fn owning_composite(data: &AssocConstraintNodeData) -> Option { match data { AssocConstraintNodeData::Geometrical { owner_id, .. } @@ -2250,7 +2348,25 @@ pub(super) fn native_constraint_set( { continue; } - set.add(kind, targets, driving); + let dimension = match &node.data { + AssocConstraintNodeData::Angle { + dimension_dependency, + .. + } + | AssocConstraintNodeData::Distance { + dimension_dependency, + .. + } + | AssocConstraintNodeData::RadiusDiameter { + dimension_dependency, + .. + } => dependency_dimension(document, *dimension_dependency), + _ => None, + }; + let id = set.add(kind, targets, driving); + if let Some(dimension) = dimension { + set.dimensions.insert(id, dimension); + } if let Some(constraint) = set.constraints.last_mut() { constraint.enabled = enabled; constraint.rigid_points = rigid_points; @@ -2429,15 +2545,21 @@ fn materialize_scope( // Pass 1 (read-only): constraint-node shapes. let mut needs_value_dependency = Vec::new(); + let mut dimension_links = Vec::new(); let (mut nodes, entities, referenced_entities) = { let mut builder = GroupBuilder::new(document); for constraint in &set.constraints { - constraint_node( + let node_id = constraint_node( &mut builder, document, constraint, &mut needs_value_dependency, ); + if let Some((node_id, dimension)) = node_id.zip(set.dimensions.get(&constraint.id)) { + if document.get_entity(*dimension).is_some() { + dimension_links.push((node_id, *dimension)); + } + } } let GroupBuilder { nodes, @@ -2527,6 +2649,7 @@ fn materialize_scope( } let mut group_dependencies = geometry_dependencies; + let mut node_names: FxHashMap = FxHashMap::default(); for (node_id, driving) in needs_value_dependency { let Some(driving) = driving else { continue }; let Ok(resolved) = driving.resolve(parameters) else { @@ -2542,6 +2665,7 @@ fn materialize_scope( } DrivingValue::Literal(_) => (format!("d{node_id}"), resolved.to_string()), }; + node_names.insert(node_id, name.clone()); let variable_handle = allocator.variable(network_handle, &name, &formula, resolved); let dependency_id = group_dependencies.len() as i32 + 1; let dep_handle = @@ -2551,6 +2675,18 @@ fn materialize_scope( set_value_dependency(&mut node.data, dep_handle); } } + for (node_id, dimension) in dimension_links { + let Some(name) = node_names.get(&node_id) else { + continue; + }; + let dependency_id = group_dependencies.len() as i32 + 1; + let dep_handle = + allocator.dimension_dependency(group_handle?, dimension, name, dependency_id); + group_dependencies.push(dep_handle); + if let Some(node) = nodes.iter_mut().find(|n| n.node_id == node_id) { + set_dimension_dependency(&mut node.data, dep_handle); + } + } if let Some(group_handle) = group_handle { let group = Assoc2dConstraintGroup { diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 2c2d105df..0fdc9a53d 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -1402,6 +1402,9 @@ impl Scene { } if self.object_isolation.hides(common.handle) || (!include_preview_hidden && self.preview_hidden.contains(&common.handle)) + || self.hidden_dynamic_dimensions.contains(&common.handle) + // A dynamic dimension never plots. + || (plot_only && self.is_dynamic_dimension(common.handle)) { return false; } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 60821cba1..bf4dec17d 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2298,6 +2298,10 @@ pub struct Scene { /// CONSTRAINTNAMEFORMAT: what a dynamic dimension's text shows — /// 0 the parameter name, 1 the value, 2 `name=value`. pub constraint_name_format: u8, + /// DYNCONSTRAINTDISPLAY: whether dynamic dimensions draw at all. + pub dynamic_constraint_display: bool, + /// Dynamic dimensions DCHIDE or DYNCONSTRAINTDISPLAY 0 keep off screen. + hidden_dynamic_dimensions: HashSet, /// Session-only visibility overrides for constraint glyphs. hidden_parametric_constraints: HashSet<( parametric_constraints::ParametricScope, @@ -2629,6 +2633,8 @@ impl Scene { associative_hatch_source_cache: RefCell::new(None), parametric_constraints: Vec::new(), constraint_name_format: 2, + dynamic_constraint_display: true, + hidden_dynamic_dimensions: HashSet::default(), hidden_parametric_constraints: HashSet::default(), shown_parametric_constraints: HashSet::default(), named_parameters: named_parameters::ParameterTable::new(), @@ -5538,6 +5544,7 @@ impl Scene { self.object_isolation.hides(handle) || self.preview_hidden.contains(&handle) || self.command_preview_hidden.contains(&handle) + || self.hidden_dynamic_dimensions.contains(&handle) } /// Replace the command-owned source hide set and refresh only handles whose diff --git a/src/scene/parametric_constraints.rs b/src/scene/parametric_constraints.rs index 4f9317a75..eb8e2c0f6 100644 --- a/src/scene/parametric_constraints.rs +++ b/src/scene/parametric_constraints.rs @@ -361,6 +361,9 @@ pub(crate) fn next_dimensional_parameter_name( .unwrap_or_else(|| "d1".to_string()) } +/// The constraint-bar label that draws a dynamic dimension's lock mark. +pub const DYNAMIC_DIMENSION_GLYPH: &str = "\u{1F512}"; + /// The text a dynamic dimension shows: CONSTRAINTNAMEFORMAT 0 = name, /// 1 = value, 2 = name=value. pub(crate) fn dynamic_dimension_text(name: &str, value: f64, format: u8) -> String { @@ -1561,9 +1564,45 @@ impl super::Scene { self.shown_parametric_constraints.remove(&(scope, *id)); } } + self.refresh_hidden_dynamic_dimensions(); ids.len() } + /// True for a dimension that shows a dimensional constraint. + pub(crate) fn is_dynamic_dimension(&self, handle: Handle) -> bool { + self.parametric_constraints + .iter() + .any(|set| set.dimensions.values().any(|dimension| *dimension == handle)) + } + + /// Re-derives which dynamic dimensions stay off screen (DCHIDE, or + /// DYNCONSTRAINTDISPLAY 0 for all of them) and redraws the ones that + /// changed. + pub(crate) fn refresh_hidden_dynamic_dimensions(&mut self) { + let desired: rustc_hash::FxHashSet = self + .parametric_constraints + .iter() + .flat_map(|set| { + set.dimensions.iter().filter_map(|(id, dimension)| { + (!self.dynamic_constraint_display + || !self.is_parametric_constraint_visible(set.scope, *id)) + .then_some(*dimension) + }) + }) + .collect(); + if desired == self.hidden_dynamic_dimensions { + return; + } + let changes: Vec<_> = self + .hidden_dynamic_dimensions + .symmetric_difference(&desired) + .copied() + .map(|handle| (handle, super::ChangeKind::Modified)) + .collect(); + self.hidden_dynamic_dimensions = desired; + self.bump_entities(&changes); + } + /// Infers relations already present in the selected geometry. pub fn inferred_parametric_constraints( &self, @@ -1937,7 +1976,8 @@ impl super::Scene { let cam = self.camera.borrow(); (cam.view_proj_rte(bounds), cam.eye()) }; - set.constraints + let mut placements = set + .constraints .iter() // A dynamic dimension is its constraint's whole display. .filter(|c| c.enabled && !set.dimensions.contains_key(&c.id)) @@ -2031,7 +2071,59 @@ impl super::Scene { }) .collect::>() }) - .collect() + .collect::>(); + // A dynamic dimension carries the reference's lock mark at the + // start of its text instead of a constraint bar. + for (id, dimension) in &set.dimensions { + let Some(constraint) = set.get(*id) else { + continue; + }; + if !constraint.enabled || self.entity_temporarily_hidden(*dimension) { + continue; + } + let Some(acadrust::EntityType::Dimension(entity)) = self.document.get_entity(*dimension) + else { + continue; + }; + let Some((anchor, outward)) = crate::entities::dimension::dynamic_dimension_lock_anchor( + &self.document, + entity, + self.annotation_scale as f64, + ) else { + continue; + }; + let project = |point: Vector3| { + crate::scene::pick::grip::project_rte( + glam::DVec3::new(point.x, point.y, point.z), + view_rot, + eye, + bounds, + ) + }; + let (Some(screen), Some(outward_screen)) = ( + project(anchor), + project(Vector3::new( + anchor.x + outward.x, + anchor.y + outward.y, + anchor.z + outward.z, + )), + ) else { + continue; + }; + let direction = (outward_screen - screen).normalize_or(glam::Vec2::NEG_X); + let point = iced::Point::new(bounds.x + screen.x, bounds.y + screen.y); + if point.x.is_finite() && point.y.is_finite() { + placements.push(( + *id, + point, + direction.to_array(), + DYNAMIC_DIMENSION_GLYPH.to_string(), + false, + Vec::new(), + )); + } + } + placements } /// Hit-tests screen point `p` (same coordinate space as `p_full` in the diff --git a/src/scene/parametric_solve.rs b/src/scene/parametric_solve.rs index 59a5d30ca..c337bf2e7 100644 --- a/src/scene/parametric_solve.rs +++ b/src/scene/parametric_solve.rs @@ -2441,7 +2441,31 @@ fn solve_scope( }) }) .flatten(); - let pinned = axis_pin.unwrap_or(*reference); + // A transformed entity whose own end points a driving dimension + // measures keeps only the first constraint point where the transform + // put it; the dimension pulls the other end back to its value (a + // scaled line keeps its d1), as in the reference. + let dimension_pin = (reference.marker.is_none() && axis_pin.is_none()) + .then(|| { + constraints.iter().find_map(|c| { + (c.enabled + && c.driving_param.is_some() + && matches!( + c.kind, + ConstraintKind::Distance + | ConstraintKind::DistanceX + | ConstraintKind::DistanceY + | ConstraintKind::DistanceDirected + ) + && c.refs.len() >= 2 + && c.refs + .iter() + .all(|r| r.entity == reference.entity && r.marker.is_some())) + .then(|| c.refs[0]) + }) + }) + .flatten(); + let pinned = axis_pin.or(dimension_pin).unwrap_or(*reference); // The re-aligned entity keeps the length the transform gave it (a // scaled vertical line stays scaled), not its pre-edit length. if axis_pin.is_some() { diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 74a828a09..fbf49514c 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -39,6 +39,11 @@ fn is_fixed_glyph(label: &str) -> bool { label == "F" || label == crate::scene::parametric_constraints::FIXED_POINT_GLYPH } +/// The lock mark a dynamic dimension carries (`DYNAMIC_DIMENSION_GLYPH`). +fn is_dynamic_dimension_glyph(label: &str) -> bool { + label == crate::scene::parametric_constraints::DYNAMIC_DIMENSION_GLYPH +} + /// Vertical's two axis-mark labels (`parametric_constraints::vertical_glyph_label`). fn is_vertical_glyph(label: &str) -> bool { label == "│" || label == crate::scene::parametric_constraints::VERTICAL_POINTS_GLYPH @@ -48,7 +53,11 @@ fn constraint_glyph_size(label: &str) -> Size { if is_compact_coincident_glyph(label) { return Size::new(COINCIDENT_GLYPH_SIZE, COINCIDENT_GLYPH_SIZE); } - if label == "G²" || is_fixed_glyph(label) || is_vertical_glyph(label) { + if label == "G²" + || is_fixed_glyph(label) + || is_vertical_glyph(label) + || is_dynamic_dimension_glyph(label) + { let side = CONSTRAINT_GLYPH_SIZE + CONSTRAINT_GLYPH_PAD_Y * 2.0; return Size::new(side, side); } @@ -109,15 +118,10 @@ fn draw_concentric_constraint_glyph( fn draw_fixed_constraint_glyph( frame: &mut canvas::Frame, center: Point, - text: Color, + lock: Color, badge: Color, point_marker: bool, ) { - let lock = if point_marker { - text - } else { - Color::from_rgb8(214, 76, 76) - }; let shackle = canvas::Path::new(|builder| { let shackle_center = Point::new(center.x, center.y - 1.6); for step in 0..=12 { @@ -2114,7 +2118,10 @@ impl canvas::Program for SelectionCanvas { }) .into(), ); - frame.fill(&pill, bg); + // A dynamic dimension's lock sits bare on the drawing. + if !is_dynamic_dimension_glyph(label) { + frame.fill(&pill, bg); + } if *is_selected { frame.stroke( &pill, @@ -2132,14 +2139,18 @@ impl canvas::Program for SelectionCanvas { draw_smooth_constraint_glyph(&mut frame, glyph_center, fg); } else if label == "◎" { draw_concentric_constraint_glyph(&mut frame, glyph_center, fg); + } else if is_dynamic_dimension_glyph(label) { + draw_fixed_constraint_glyph(&mut frame, glyph_center, fg, bg, false); } else if is_fixed_glyph(label) { - draw_fixed_constraint_glyph( - &mut frame, - glyph_center, - fg, - bg, - label == crate::scene::parametric_constraints::FIXED_POINT_GLYPH, - ); + let point = + label == crate::scene::parametric_constraints::FIXED_POINT_GLYPH; + // The object-mode lock reads red, the point-mode one white. + let lock = if point { + fg + } else { + Color::from_rgb8(214, 76, 76) + }; + draw_fixed_constraint_glyph(&mut frame, glyph_center, lock, bg, point); } else if is_vertical_glyph(label) { draw_vertical_constraint_glyph( &mut frame,