Dimensional constraints: dynamic dimension persistence, lock mark, display variables, DIMCONSTRAINT

The dynamic dimension of a dimensional constraint is now linked in the
drawing file (AcDbAssocDependency + AcDbAssocDimDependencyBody on the
constraint node), reread on load, hidden by DCHIDE/DYNCONSTRAINTDISPLAY
and never plotted; it carries a lock mark before its text. New
CONSTRAINTNAMEFORMAT and DYNCONSTRAINTDISPLAY variables and the
DIMCONSTRAINT option front end. A transformed entity whose own end points
a driving dimension measures keeps only the first constraint point, so a
scaled line returns to its dimension value.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ramox81andClaude Fable 5.1 committed 2026-09-21 12:13:36 +03:00
1 parent 72fd3fd35b
commit 971abe64a4
12 files changed
+433 -24

No files matched your search

+2
View File
@@ -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;
+10
View File
@@ -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();
+22 -1
View File
@@ -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();
+39
View File
@@ -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,
+64
View File
@@ -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] <Aligned>:".to_string()
}
fn options(&self) -> Vec<CmdOption> {
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<CmdResult> {
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::*;
+2 -2
View File
@@ -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",
]
});
+139 -3
View File
@@ -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<Handle> = 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<Handle> {
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<i32> {
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<i32, String> = 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 {
+3
View File
@@ -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;
}
+7
View File
@@ -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<Handle>,
/// 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
+94 -2
View File
@@ -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<Handle> = 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::<Vec<_>>()
})
.collect()
.collect::<Vec<_>>();
// 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
+25 -1
View File
@@ -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() {
+26 -15
View File
@@ -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<Message> 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<Message> 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,