forked from 1b-agent/opencadstudio-upstream
Merge pull request #1413 from ramox81/codex/linear-constraint-parity
Linear dimensional constraints: point flow, dynamic dimension, driving parameter, persistence # Conflicts: # src/scene/parametric_constraints.rs
This commit is contained in:
commit
0ee8b15511
19 files changed
+2204
-61
No files matched your search
@@ -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 {
|
||||
@@ -2674,6 +2810,9 @@ impl Scene {
|
||||
|
||||
pub(crate) fn load_parametric_constraints_from_document(&mut self) {
|
||||
self.parametric_constraints.clear();
|
||||
// Dynamic dimensions read from the file get their screen size on the
|
||||
// first tick after the view is known.
|
||||
self.dynamic_dimension_camera_gen = None;
|
||||
let owners: Vec<_> = self
|
||||
.document
|
||||
.block_records
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2295,6 +2295,15 @@ pub struct Scene {
|
||||
has_associative_centers: std::cell::Cell<Option<bool>>,
|
||||
/// Runtime parametric constraint sets decoded from standard graph scopes.
|
||||
pub(crate) parametric_constraints: Vec<parametric_constraints::ParametricConstraintSet>,
|
||||
/// 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>,
|
||||
/// The camera generation the dynamic dimensions were last scaled for.
|
||||
dynamic_dimension_camera_gen: Option<u64>,
|
||||
/// Session-only visibility overrides for constraint glyphs.
|
||||
hidden_parametric_constraints: HashSet<(
|
||||
parametric_constraints::ParametricScope,
|
||||
@@ -2625,6 +2634,10 @@ impl Scene {
|
||||
dependency_index_cache: RefCell::new(None),
|
||||
associative_hatch_source_cache: RefCell::new(None),
|
||||
parametric_constraints: Vec::new(),
|
||||
constraint_name_format: 2,
|
||||
dynamic_constraint_display: true,
|
||||
hidden_dynamic_dimensions: HashSet::default(),
|
||||
dynamic_dimension_camera_gen: None,
|
||||
hidden_parametric_constraints: HashSet::default(),
|
||||
shown_parametric_constraints: HashSet::default(),
|
||||
named_parameters: named_parameters::ParameterTable::new(),
|
||||
@@ -3319,6 +3332,7 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
if !self.parametric_constraints.is_empty() || !self.named_parameters.is_empty() {
|
||||
self.refresh_dynamic_dimension_texts();
|
||||
self.sync_native_parametric_graph();
|
||||
}
|
||||
if !changes.is_empty() {
|
||||
@@ -5533,6 +5547,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
|
||||
|
||||
@@ -351,6 +351,57 @@ pub(crate) fn equal_size_follower(
|
||||
Some(entity)
|
||||
}
|
||||
|
||||
/// The first free `d1`, `d2`, … name a new dimensional constraint takes.
|
||||
pub(crate) fn next_dimensional_parameter_name(
|
||||
table: &super::named_parameters::ParameterTable,
|
||||
) -> String {
|
||||
(1..)
|
||||
.map(|n| format!("d{n}"))
|
||||
.find(|name| !table.contains(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 on-screen text height of a dynamic dimension, in pixels.
|
||||
pub const DYNAMIC_DIMENSION_TEXT_PX: f32 = 12.0;
|
||||
|
||||
/// The xdata application that marks an annotational constraint dimension.
|
||||
pub const ANNOTATIONAL_DIMENSION_APP: &str = "OCS_CONSTRAINT_FORM";
|
||||
|
||||
/// The text a dynamic dimension shows: CONSTRAINTNAMEFORMAT 0 = name,
|
||||
/// 1 = value, 2 = name=value. A plain number shows as typed (`d1=100`); an
|
||||
/// expression shows the expression under the reference's `fx:` prefix
|
||||
/// (`fx: d2=d1*2`).
|
||||
pub(crate) fn dynamic_dimension_text(name: &str, source: &str, value: f64, format: u8) -> String {
|
||||
let source = source.trim();
|
||||
let literal = source.parse::<f64>().is_ok();
|
||||
let shown = if literal {
|
||||
source.to_string()
|
||||
} else {
|
||||
measured_expression(value)
|
||||
};
|
||||
match format {
|
||||
0 => name.to_string(),
|
||||
1 if literal => shown,
|
||||
1 => format!("fx: {shown}"),
|
||||
_ if literal => format!("{name}={shown}"),
|
||||
_ => format!("fx: {name}={source}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The expression a measured value seeds a parameter with (`100`, `111.8034`).
|
||||
pub(crate) fn measured_expression(value: f64) -> String {
|
||||
let text = format!("{value:.4}");
|
||||
let text = text.trim_end_matches('0').trim_end_matches('.');
|
||||
if text.is_empty() || text == "-" {
|
||||
"0".to_string()
|
||||
} else {
|
||||
text.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Grabbed points are exact kernel inputs; the solver anchors the remaining
|
||||
/// endpoint coordinates according to the line's directional constraints.
|
||||
pub(crate) fn grip_solve_anchor_refs(
|
||||
@@ -585,6 +636,8 @@ pub struct ParametricConstraintSet {
|
||||
/// parameter names may legitimately occur in different blocks.
|
||||
pub(crate) local_parameters: super::named_parameters::ParameterTable,
|
||||
pub(crate) retained_standard_groups: Vec<Handle>,
|
||||
/// The dynamic dimension that shows a dimensional constraint, by id.
|
||||
pub(crate) dimensions: std::collections::HashMap<ConstraintId, Handle>,
|
||||
/// Cached total remaining degrees of freedom, summed across every
|
||||
/// independent solve partition in this scope — updated by
|
||||
/// `parametric_solve::solve_scope` each time this set is resolved. `None`
|
||||
@@ -611,6 +664,7 @@ impl ParametricConstraintSet {
|
||||
next_id: 0,
|
||||
local_parameters: super::named_parameters::ParameterTable::new(),
|
||||
retained_standard_groups: Vec::new(),
|
||||
dimensions: std::collections::HashMap::new(),
|
||||
dof: None,
|
||||
conflicts: Vec::new(),
|
||||
}
|
||||
@@ -702,6 +756,7 @@ impl ParametricConstraintSet {
|
||||
|
||||
/// Removes a constraint by id. Returns whether one was actually removed.
|
||||
pub fn remove(&mut self, id: ConstraintId) -> bool {
|
||||
self.dimensions.remove(&id);
|
||||
let before = self.constraints.len();
|
||||
self.constraints.retain(|c| c.id != id);
|
||||
self.constraints.len() != before
|
||||
@@ -1420,6 +1475,66 @@ impl super::Scene {
|
||||
!self.hidden_parametric_constraints.contains(&(scope, id))
|
||||
}
|
||||
|
||||
/// Rewrites every dynamic dimension's text from its constraint's
|
||||
/// parameter name and current value, in the CONSTRAINTNAMEFORMAT
|
||||
/// reading `constraint_name_format` selects.
|
||||
pub(crate) fn refresh_dynamic_dimension_texts(&mut self) {
|
||||
let format = self.constraint_name_format;
|
||||
let mut updates: Vec<(Handle, String)> = Vec::new();
|
||||
for set in &self.parametric_constraints {
|
||||
let table = if set.local_parameters.is_empty() {
|
||||
&self.named_parameters
|
||||
} else {
|
||||
&set.local_parameters
|
||||
};
|
||||
for (id, dimension) in &set.dimensions {
|
||||
let Some(constraint) = set.get(*id) else {
|
||||
continue;
|
||||
};
|
||||
let (name, source, value) = match &constraint.driving_param {
|
||||
Some(DrivingValue::Named(name)) => (
|
||||
name.clone(),
|
||||
table
|
||||
.get(name)
|
||||
.map(|parameter| parameter.source.clone())
|
||||
.unwrap_or_default(),
|
||||
table.resolve(name).unwrap_or(f64::NAN),
|
||||
),
|
||||
Some(DrivingValue::Literal(value)) => {
|
||||
(String::new(), measured_expression(*value), *value)
|
||||
}
|
||||
None => continue,
|
||||
};
|
||||
// An annotational dimension shows the value at dimension
|
||||
// precision, as the reference does.
|
||||
let source = if self.dimension_is_annotational(*dimension) {
|
||||
format!("{value:.4}")
|
||||
} else {
|
||||
source
|
||||
};
|
||||
updates.push((
|
||||
*dimension,
|
||||
dynamic_dimension_text(&name, &source, value, format),
|
||||
));
|
||||
}
|
||||
}
|
||||
for (handle, text) in updates {
|
||||
let Some(acadrust::EntityType::Dimension(mut dimension)) =
|
||||
self.document.get_entity(handle).cloned()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if dimension.base().user_text.as_deref() == Some(text.as_str()) {
|
||||
continue;
|
||||
}
|
||||
crate::entities::dimension::set_dimension_text_override(
|
||||
dimension.base_mut(),
|
||||
Some(text),
|
||||
);
|
||||
self.update_entity(acadrust::EntityType::Dimension(dimension));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn should_display_parametric_constraint(
|
||||
&self,
|
||||
scope: ParametricScope,
|
||||
@@ -1483,9 +1598,162 @@ impl super::Scene {
|
||||
self.shown_parametric_constraints.remove(&(scope, *id));
|
||||
}
|
||||
}
|
||||
self.refresh_hidden_dynamic_dimensions();
|
||||
ids.len()
|
||||
}
|
||||
|
||||
/// True for a dynamic dimension of a dimensional constraint (not an
|
||||
/// annotational one, which is an ordinary plotted dimension).
|
||||
pub(crate) fn is_dynamic_dimension(&self, handle: Handle) -> bool {
|
||||
self.parametric_constraints
|
||||
.iter()
|
||||
.any(|set| set.dimensions.values().any(|dimension| *dimension == handle))
|
||||
&& !self.dimension_is_annotational(handle)
|
||||
}
|
||||
|
||||
/// True when a dimensional constraint's dimension uses the annotational
|
||||
/// form: drawn with its style, plotted, never rescaled to the screen.
|
||||
pub(crate) fn dimension_is_annotational(&self, handle: Handle) -> bool {
|
||||
self.document.get_entity(handle).is_some_and(|entity| {
|
||||
entity
|
||||
.common()
|
||||
.extended_data
|
||||
.get_record(ANNOTATIONAL_DIMENSION_APP)
|
||||
.is_some()
|
||||
})
|
||||
}
|
||||
|
||||
/// Marks a constraint dimension as annotational (DCFORM Annotational).
|
||||
pub(crate) fn mark_dimension_annotational(&mut self, handle: Handle) {
|
||||
crate::scene::view::dispatch::set_entity_xdata(
|
||||
&mut self.document,
|
||||
handle,
|
||||
ANNOTATIONAL_DIMENSION_APP,
|
||||
Some(vec![acadrust::xdata::XDataValue::String(
|
||||
"ANNOTATIONAL".to_string(),
|
||||
)]),
|
||||
);
|
||||
}
|
||||
|
||||
/// The document as written to a file: a dynamic dimension's screen-size
|
||||
/// overrides are a display matter and stay out of the file.
|
||||
pub(crate) fn document_for_save(&self) -> acadrust::CadDocument {
|
||||
use crate::entities::dim_override as ov;
|
||||
let mut document = self.document.clone();
|
||||
for set in &self.parametric_constraints {
|
||||
for handle in set.dimensions.values().copied() {
|
||||
if self.dimension_is_annotational(handle) {
|
||||
continue;
|
||||
}
|
||||
for code in [ov::DIMSCALE, ov::DIMTIH, ov::DIMTOH] {
|
||||
ov::set(&mut document, handle, code, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
document
|
||||
}
|
||||
|
||||
/// Gives every dynamic dimension a DIMSCALE override that keeps its
|
||||
/// text, arrows and offsets at a screen size (the reference draws them
|
||||
/// at a constant pixel size whatever the zoom). Runs whenever the camera
|
||||
/// changed; `force` re-applies after a dimension was created or loaded.
|
||||
pub fn refresh_dynamic_dimension_scales(&mut self, force: bool) {
|
||||
let Some(wpp) = self.world_per_pixel() else {
|
||||
return;
|
||||
};
|
||||
if !force && self.dynamic_dimension_camera_gen == Some(self.camera_generation) {
|
||||
return;
|
||||
}
|
||||
self.dynamic_dimension_camera_gen = Some(self.camera_generation);
|
||||
let handles: Vec<Handle> = self
|
||||
.parametric_constraints
|
||||
.iter()
|
||||
.flat_map(|set| set.dimensions.values().copied())
|
||||
.collect();
|
||||
let mut changes = Vec::new();
|
||||
for handle in handles {
|
||||
let Some(acadrust::EntityType::Dimension(dimension)) = self.document.get_entity(handle)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
// An annotational dimension keeps its style's size.
|
||||
if self.dimension_is_annotational(handle) {
|
||||
continue;
|
||||
}
|
||||
let style_name = dimension.base().style_name.clone();
|
||||
let text_height = self
|
||||
.document
|
||||
.dim_styles
|
||||
.iter()
|
||||
.find(|style| {
|
||||
style.name.eq_ignore_ascii_case(&style_name)
|
||||
|| (style_name.trim().is_empty()
|
||||
&& style.name.eq_ignore_ascii_case("Standard"))
|
||||
})
|
||||
.map(|style| style.dimtxt)
|
||||
.filter(|height| *height > 1e-9)
|
||||
.unwrap_or(0.18);
|
||||
let scale = f64::from(wpp) * f64::from(DYNAMIC_DIMENSION_TEXT_PX) / text_height;
|
||||
use crate::entities::dim_override as ov;
|
||||
let xdata = &dimension.base().common.extended_data;
|
||||
let current = ov::real(xdata, ov::DIMSCALE);
|
||||
// The reference draws dynamic dimension text horizontally.
|
||||
let horizontal = ov::int(xdata, ov::DIMTIH) == Some(1) && ov::int(xdata, ov::DIMTOH) == Some(1);
|
||||
if current.is_some_and(|value| (value - scale).abs() < 1e-9) && horizontal {
|
||||
continue;
|
||||
}
|
||||
ov::set(
|
||||
&mut self.document,
|
||||
handle,
|
||||
ov::DIMSCALE,
|
||||
Some(acadrust::xdata::XDataValue::Real(scale)),
|
||||
);
|
||||
if !horizontal {
|
||||
for code in [ov::DIMTIH, ov::DIMTOH] {
|
||||
ov::set(
|
||||
&mut self.document,
|
||||
handle,
|
||||
code,
|
||||
Some(acadrust::xdata::XDataValue::Integer16(1)),
|
||||
);
|
||||
}
|
||||
}
|
||||
changes.push((handle, super::ChangeKind::Modified));
|
||||
}
|
||||
if !changes.is_empty() {
|
||||
self.bump_entities(&changes);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.dimension_is_annotational(*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,
|
||||
@@ -1862,7 +2130,8 @@ impl super::Scene {
|
||||
let mut placements = set
|
||||
.constraints
|
||||
.iter()
|
||||
.filter(|c| c.enabled)
|
||||
// A dynamic dimension is its constraint's whole display.
|
||||
.filter(|c| c.enabled && !set.dimensions.contains_key(&c.id))
|
||||
.filter(|c| {
|
||||
let selected = c
|
||||
.refs
|
||||
@@ -1971,6 +2240,57 @@ impl super::Scene {
|
||||
equal_anchors.push(*point);
|
||||
true
|
||||
});
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
@@ -2443,7 +2443,32 @@ fn solve_scope(
|
||||
})
|
||||
})
|
||||
.flatten();
|
||||
let pinned = axis_pin.unwrap_or(*reference);
|
||||
// A transformed entity whose own end points a driving dimension
|
||||
// measures keeps only its second constraint point where the transform
|
||||
// put it; the dimension pulls the first end back to its value (a line
|
||||
// scaled 2x about its start keeps its d1 by moving that start), 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[1])
|
||||
})
|
||||
})
|
||||
.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() {
|
||||
@@ -2743,6 +2768,21 @@ fn solve_scope(
|
||||
constraint.refs.get(2).is_some_and(|axis| axis.entity == *handle)
|
||||
})
|
||||
|| equal_followers.iter().any(|follower| follower.entity == *handle)
|
||||
// A driving dimension between an entity's own ends is its size;
|
||||
// a retained length would contradict a new value.
|
||||
|| constraints.iter().any(|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 == *handle && r.marker.is_some())
|
||||
})
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user