Aligned dimensional constraint: Point & line, 2Lines, value and property parity

DCALIGNED opens with `[Object/Point & line/2Lines] <Object>`. Point & line
asks `Specify constraint point or [Line] <Line>:` then `Select line:` (or the
line first and `Select constraint point:`), measures perpendicular to the
line from the constraint point to the line end nearest to it, and stores a
directed distance with the line as direction reference. 2Lines asks
`Select first line:` and `Select second line to make parallel:`, adds a
Parallel constraint solved with the first line pinned, then measures on the
solved geometry between the nearest ends. Unsupported line picks report the
reference's message; the same measurement again reports `The constraint
already exists on the selected objects.` and ends the command; an invalid
expression reports both reference lines and asks again.

The offered default and seeded expression carry twelve significant digits;
a typed value moves the second point (or the other object) while the first
point and its measured line stay, negative values keep their sign in the
parameter and use their magnitude, zero folds the points together, and the
dynamic dimension's origins follow every solve. The lock mark sits after the
text, a selected dynamic dimension shows triangle grips at its points and the
text square, and its Properties show the Constraint section (Name and
Expression editable) plus Text rotation.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
ramox81andClaude Fable 5.1 committed 2026-09-21 14:52:37 +03:00
1 parent 971abe64a4
commit 6969c19746
8 files changed
+855 -64

No files matched your search

+213 -17
View File
@@ -1324,7 +1324,8 @@ impl OpenCADStudio {
/// table, so unlike that whole-table Apply this can cheaply scope the
/// re-solve to just the entities that actually reference it.
fn resolve_named_parameter_edit(&mut self, i: usize, name: &str) {
let touched: Vec<Handle> = self.tabs[i]
let readers: Vec<&crate::scene::parametric_constraints::ParametricConstraint> = self
.tabs[i]
.scene
.parametric_constraints
.iter()
@@ -1333,8 +1334,24 @@ impl OpenCADStudio {
c.enabled
&& matches!(&c.driving_param, Some(crate::scene::named_parameters::DrivingValue::Named(n)) if n == name)
})
.collect();
let touched: Vec<Handle> = readers
.iter()
.flat_map(|c| c.refs.iter().map(|r| r.entity))
.collect();
// A changed value moves a dimensional constraint's second point; its
// first point (and the line it measures perpendicular to) stays, as
// in the reference.
let anchors: Vec<crate::scene::parametric_constraints::ParametricRef> = readers
.iter()
.flat_map(|c| {
crate::scene::parametric_constraints::dimensional_anchor_refs(
&self.tabs[i].scene.document,
&c.refs,
)
})
.filter(|reference| reference.marker.is_some())
.collect();
self.tabs[i].dirty = true;
if touched.is_empty() {
self.tabs[i].scene.sync_native_parametric_graph();
@@ -1344,7 +1361,9 @@ impl OpenCADStudio {
.into_iter()
.map(|h| (h, crate::scene::ChangeKind::Modified))
.collect();
self.tabs[i].scene.bump_entities(&changes);
self.tabs[i]
.scene
.bump_entities_with_parametric_policy(&changes, &anchors, false);
}
/// Commits one field of one Parameters-section row (Properties panel) —
@@ -1481,6 +1500,52 @@ impl OpenCADStudio {
Task::none()
}
/// Commits a dynamic dimension's Name or Expression row: the typed text
/// goes to the Parameters-section row of the constraint's parameter.
pub(super) fn on_dynamic_dimension_field_commit(
&mut self,
field: &'static str,
param_field: crate::ui::window::named_parameters::ParamField,
) -> Task<Message> {
use crate::scene::named_parameters::DrivingValue;
use crate::scene::parametric_constraints::dynamic_dimension_constraint;
use crate::ui::properties::FieldKey;
let i = self.active_tab;
self.tabs[i].properties.active_field = None;
let Some(typed) = self.tabs[i]
.properties
.edit_buf
.remove(&FieldKey::Geom(field))
else {
return Task::none();
};
let handles = self.property_target_handles(i);
let name = handles.first().and_then(|handle| {
let (_, constraint) =
dynamic_dimension_constraint(&self.tabs[i].scene.parametric_constraints, *handle)?;
match &constraint.driving_param {
Some(DrivingValue::Named(name)) => Some(name.clone()),
_ => None,
}
});
let index = name.and_then(|name| {
self.tabs[i]
.scene
.named_parameters()
.iter()
.position(|parameter| parameter.name == name)
});
let Some(index) = index else {
self.refresh_properties();
return Task::none();
};
self.tabs[i]
.properties
.edit_buf
.insert(FieldKey::Param(index, param_field), typed);
self.on_prop_param_commit(index, param_field)
}
/// Removes Parameters-section row `index` immediately. Any
/// constraint that referenced it gets the same "undefined reference"
/// resolve-failure treatment a formula's own bad reference already gets
@@ -3572,26 +3637,62 @@ impl OpenCADStudio {
second_point,
location,
axis,
direction,
name,
expression,
label,
} => {
use crate::modules::annotate::{aligned_dim, linear_dim};
use crate::scene::named_parameters::{is_valid_name, DrivingValue};
use crate::scene::parametric_constraints::{dynamic_dimension_text, ConstraintKind};
use crate::scene::parametric_constraints::{
distance_direction_type, dynamic_dimension_text, ConstraintKind,
};
let scope = self.tabs[i].current_parametric_scope();
let refs = vec![first, second];
let mut refs = vec![first, second];
refs.extend(direction);
// The same measurement between the same references is
// refused after the value, and the command ends.
let duplicate = self.tabs[i]
.scene
.parametric_constraint_set(scope)
.is_some_and(|set| {
set.constraints.iter().any(|c| {
c.enabled
&& c.kind == kind
&& c.refs.len() == refs.len()
&& c.refs[..2].contains(&first)
&& c.refs[..2].contains(&second)
&& c.refs.get(2) == refs.get(2)
})
});
if duplicate {
self.command_line
.push_output("The constraint already exists on the selected objects.");
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
return Task::none();
}
// A refused value keeps the prompt open, as the reference's
// editor does.
// editor does. A directed distance validates as the plain
// two-point distance it also is.
let (validate_kind, validate_refs) = if kind == ConstraintKind::DistanceDirected {
(ConstraintKind::Distance, &refs[..2])
} else {
(kind, &refs[..])
};
if let Err(message) = self.tabs[i].scene.validate_parametric_constraint(
kind,
&refs,
validate_kind,
validate_refs,
Some(&DrivingValue::Literal(1.0)),
) {
self.reprompt_active_command(i, message);
return Task::none();
}
let anchors = crate::scene::parametric_constraints::dimensional_anchor_refs(
&self.tabs[i].scene.document,
&refs,
);
let name = name.trim().to_string();
if !is_valid_name(&name) {
self.reprompt_active_command(i, "Invalid parameter name.");
@@ -3605,7 +3706,11 @@ impl OpenCADStudio {
let value = match table.resolve(&name) {
Ok(value) if value.is_finite() => value,
_ => {
self.reprompt_active_command(i, "Invalid expression.");
self.command_line.push_error("Invalid expression.");
self.reprompt_active_command(
i,
"The parameter is used in an expression which results in an invalid value for a dimensional constraint.",
);
return Task::none();
}
};
@@ -3653,8 +3758,10 @@ impl OpenCADStudio {
crate::scene::parametric_constraints::ParametricConstraintSet::new(scope)
});
let mut touched = vec![first.entity];
if second.entity != first.entity {
touched.push(second.entity);
for reference in [Some(second), direction].into_iter().flatten() {
if !touched.contains(&reference.entity) {
touched.push(reference.entity);
}
}
let pending = self.begin_undo(i, label, touched.len() + 1, false);
self.tabs[i]
@@ -3665,6 +3772,12 @@ impl OpenCADStudio {
let dimension = self.tabs[i].scene.add_entity(entity);
let set = self.tabs[i].scene.parametric_constraint_set_mut(scope);
let id = set.add(kind, refs, Some(DrivingValue::Named(name)));
if direction.is_some() {
if let Some(constraint) = set.constraints.iter_mut().find(|c| c.id == id) {
constraint.distance_direction_type =
distance_direction_type::PERPENDICULAR_TO_LINE;
}
}
set.dimensions.insert(id, dimension);
self.tabs[i].scene.note_parametric_constraint_applied(
scope,
@@ -3679,13 +3792,13 @@ impl OpenCADStudio {
.into_iter()
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
.collect();
// The first point stays; a value other than the measured one
// moves the second, as in the reference.
self.tabs[i].scene.bump_entities_with_parametric_policy(
&changes,
&[first],
self.constraint_solve_mode,
);
// The first point (and a line it belongs to that the distance
// runs perpendicular to) stays; a value other than the
// measured one moves the second, as in the reference. Not
// `retain_size`: the typed value is the new length.
self.tabs[i]
.scene
.bump_entities_with_parametric_policy(&changes, &anchors, false);
// DYNCONSTRAINTDISPLAY 0 also keeps a new dynamic dimension off screen.
self.tabs[i].scene.refresh_hidden_dynamic_dimensions();
self.tabs[i].dirty = true;
@@ -3696,6 +3809,89 @@ impl OpenCADStudio {
self.commit_undo_delta(i, pd);
}
}
CmdResult::MakeParallel {
first_line,
first_ends,
second_line,
second_ends,
} => {
use crate::scene::parametric_constraints::{resolve_point, ConstraintKind};
let scope = self.tabs[i].current_parametric_scope();
let refs = [first_line, second_line];
if let Err(message) =
self.tabs[i]
.scene
.validate_parametric_constraint(ConstraintKind::Parallel, &refs, None)
{
return self.apply_cmd_result(CmdResult::ReportError(message.to_string()));
}
let already = self.tabs[i]
.scene
.parametric_constraint_set(scope)
.is_some_and(|set| {
set.constraints.iter().any(|c| {
c.enabled
&& c.kind == ConstraintKind::Parallel
&& c.refs.contains(&first_line)
&& c.refs.contains(&second_line)
})
});
if !already {
let constraints_before = self.tabs[i]
.scene
.parametric_constraint_set(scope)
.cloned()
.unwrap_or_else(|| {
crate::scene::parametric_constraints::ParametricConstraintSet::new(
scope,
)
});
let pending = self.begin_undo(i, "Parallel constraint", 1, true);
self.tabs[i]
.scene
.record_undo_parametric_constraints_before(scope, constraints_before);
let set = self.tabs[i].scene.parametric_constraint_set_mut(scope);
let id = set.add(ConstraintKind::Parallel, refs.to_vec(), None);
self.tabs[i].scene.note_parametric_constraint_applied(
scope,
id,
self.constraint_bar_display,
);
// The first line stays; the second turns parallel to it.
self.tabs[i].scene.bump_entities_with_parametric_policy(
&[(second_line.entity, crate::scene::ChangeKind::Modified)],
&first_ends,
self.constraint_solve_mode,
);
self.tabs[i].dirty = true;
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
let ends = {
let document = &self.tabs[i].scene.document;
document.get_entity(second_line.entity).and_then(|entity| {
let world = |reference: crate::scene::parametric_constraints::ParametricRef| {
let point = resolve_point(entity, reference.marker?)?;
Some((reference, glam::DVec3::new(point.x, point.y, point.z)))
};
Some([world(second_ends[0])?, world(second_ends[1])?])
})
};
let Some(ends) = ends else {
return self.apply_cmd_result(CmdResult::ReportError(
crate::modules::parametric::DimConstraintCommand::NO_OBJECT.to_string(),
));
};
let result = self.tabs[i]
.active_cmd
.as_mut()
.map(|command| command.accept_parallel_line(ends));
return match result {
Some(result) => self.apply_cmd_result(result),
None => Task::none(),
};
}
CmdResult::AddFixedConstraint(pick) => {
use crate::modules::parametric::FixConstraintCommand;
use crate::scene::parametric_constraints::{
+120
View File
@@ -2292,6 +2292,28 @@ impl OpenCADStudio {
}
_ => entity_type_label(entity),
};
// A dynamic dimension shows only its constraint and text
// rotation, as the reference does.
let title = match crate::scene::parametric_constraints::dynamic_dimension_constraint(
&self.tabs[i].scene.parametric_constraints,
handle,
) {
Some((set, constraint)) => {
sections = dynamic_dimension_sections(
&self.tabs[i].scene,
set,
constraint,
sections,
);
match entity {
acadrust::EntityType::Dimension(
acadrust::entities::Dimension::Aligned(_),
) => t!("Aligned Dimensional Constraint").into_owned(),
_ => t!("Linear Dimensional Constraint").into_owned(),
}
}
None => title,
};
ui::PropertiesPanel {
choice_combos: sections
.iter()
@@ -2739,6 +2761,26 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={}
}
}
}
if self.tabs[i].scene.is_dynamic_dimension(handle) {
// A dynamic dimension shows the reference's grips: a
// triangle at each constraint point aimed at the other
// one, and the text square — no dimension line grip.
let points: Vec<glam::DVec3> = entity_grips
.iter()
.filter(|grip| grip.id <= 1)
.map(|grip| grip.world)
.collect();
entity_grips.retain(|grip| grip.id != 2);
for grip in &mut entity_grips {
if grip.id <= 1 {
let other = points.get(1 - grip.id).copied().unwrap_or(grip.world);
let dir = (other - grip.world).normalize_or(glam::DVec3::X);
*grip = crate::entities::common::oriented_triangle_grip(
grip.id, grip.world, dir,
);
}
}
}
entity_grips.extend(crate::scene::model::solid_history::primitive_grips(
&self.tabs[i].scene.document,
handle,
@@ -3572,6 +3614,84 @@ fn set_row(sections: &mut [crate::scene::model::object::PropSection], field: &st
/// Replace a row's value with an arbitrary control (editable field, dropdown,
/// colour picker …) rather than plain read-only text.
/// A dynamic dimension's Properties as the reference shows them: the
/// Constraint rows, then only the text rotation of the dimension itself.
fn dynamic_dimension_sections(
scene: &crate::scene::Scene,
set: &crate::scene::parametric_constraints::ParametricConstraintSet,
constraint: &crate::scene::parametric_constraints::ParametricConstraint,
sections: Vec<crate::scene::model::object::PropSection>,
) -> Vec<crate::scene::model::object::PropSection> {
use crate::scene::model::object::{PropSection, PropValue, Property};
use crate::scene::named_parameters::DrivingValue;
let table = if set.local_parameters.is_empty() {
scene.named_parameters()
} else {
&set.local_parameters
};
let (name, expression, value) = match &constraint.driving_param {
Some(DrivingValue::Named(name)) => (
name.clone(),
table
.iter()
.find(|parameter| &parameter.name == name)
.map(|parameter| parameter.source.clone())
.unwrap_or_default(),
table.resolve(name).ok(),
),
Some(DrivingValue::Literal(value)) => (String::new(), format!("{value}"), Some(*value)),
None => (String::new(), String::new(), None),
};
let row = |label: &str, field: &'static str, value: PropValue| Property {
label: label.to_string(),
field,
value,
};
let text_rotation = sections
.iter()
.flat_map(|section| section.props.iter())
.find(|property| property.field == "text_rotation")
.cloned();
let mut result = vec![PropSection {
title: t!("Constraint").into_owned(),
props: vec![
row(
t!("Constraint Form").as_ref(),
"dyn_constraint_form",
PropValue::ReadOnly("Dynamic".to_string()),
),
row(
t!("Reference").as_ref(),
"dyn_constraint_reference",
PropValue::ReadOnly(t!("No").into_owned()),
),
row(t!("Name").as_ref(), "dyn_constraint_name", PropValue::EditText(name)),
row(
t!("Expression").as_ref(),
"dyn_constraint_expression",
PropValue::EditText(expression),
),
row(
t!("Value").as_ref(),
"dyn_constraint_value",
PropValue::ReadOnly(value.map(|v| format!("{v:.4}")).unwrap_or_default()),
),
row(
t!("Description").as_ref(),
"dyn_constraint_description",
PropValue::ReadOnly(String::new()),
),
],
}];
if let Some(text_rotation) = text_rotation {
result.push(PropSection {
title: t!("Text").into_owned(),
props: vec![text_rotation],
});
}
result
}
fn set_row_value(
sections: &mut [crate::scene::model::object::PropSection],
field: &str,
+10
View File
@@ -3259,6 +3259,16 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
}
pub(super) fn on_prop_geom_commit(&mut self, field: &'static str) -> Task<Message> {
use crate::ui::window::named_parameters::ParamField;
// A dynamic dimension's Name/Expression rows edit its parameter.
let dynamic_field = match field {
"dyn_constraint_name" => Some(ParamField::Name),
"dyn_constraint_expression" => Some(ParamField::Formula),
_ => None,
};
if let Some(param_field) = dynamic_field {
return self.on_dynamic_dimension_field_commit(field, param_field);
}
let i = self.active_tab;
self.tabs[i].properties.active_field = None;
let handles = self.property_target_handles(i);
+20
View File
@@ -1625,10 +1625,22 @@ pub enum CmdResult {
second_point: DVec3,
location: DVec3,
axis: DVec3,
/// Aligned's Point & line / 2Lines: the line the distance is
/// measured perpendicular to.
direction: Option<crate::scene::parametric_constraints::ParametricRef>,
name: String,
expression: String,
label: &'static str,
},
/// Aligned's 2Lines: makes `second_line` parallel to `first_line` (whose
/// ends stay put), then hands the second line's solved ends back through
/// `CadCommand::accept_parallel_line`.
MakeParallel {
first_line: crate::scene::parametric_constraints::ParametricRef,
first_ends: [crate::scene::parametric_constraints::ParametricRef; 2],
second_line: crate::scene::parametric_constraints::ParametricRef,
second_ends: [crate::scene::parametric_constraints::ParametricRef; 2],
},
/// Adds a point or object symmetry relation around a picked line. The
/// first reference and axis remain fixed during initial placement.
AddSymmetricConstraint {
@@ -2482,6 +2494,14 @@ pub trait CadCommand: Send {
CmdResult::NeedPoint
}
/// The second line's ends after a `MakeParallel` solve.
fn accept_parallel_line(
&mut self,
_ends: [(crate::scene::parametric_constraints::ParametricRef, DVec3); 2],
) -> CmdResult {
CmdResult::NeedPoint
}
/// Include filled hatch / DXF SOLID regions in the entity hit-test.
///
/// Most entity-pick commands operate on curve geometry and intentionally
+5 -7
View File
@@ -6295,7 +6295,7 @@ 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
/// Where a dynamic dimension's lock mark sits: just after the text on its
/// baseline, with the direction that points away from the text.
pub(crate) fn dynamic_dimension_lock_anchor(
document: &CadDocument,
@@ -6323,13 +6323,11 @@ pub(crate) fn dynamic_dimension_lock_anchor(
dimension_text_natural_rotation(dim)
};
let (sr, cr) = rot.sin_cos();
let outward = Vector3::new(-cr, -sr, 0.0);
let outward = Vector3::new(cr, sr, 0.0);
// A glyph-sized gap keeps the lock clear of the last digit.
let reach = half_width + text_height * 0.9;
Some((
Vector3::new(
pos.x + outward.x * half_width,
pos.y + outward.y * half_width,
pos.z,
),
Vector3::new(pos.x + outward.x * reach, pos.y + outward.y * reach, pos.z),
outward,
))
}
+308 -24
View File
@@ -16,6 +16,29 @@ pub enum DimConstraintAxis {
Aligned,
}
/// A picked line (a line entity or one polyline segment) an Aligned
/// constraint measures perpendicular to.
#[derive(Clone, Copy)]
struct LineTarget {
line: ParametricRef,
/// Unit direction from its start to its end.
dir: DVec3,
/// Its two ends and where they are.
ends: [(ParametricRef, DVec3); 2],
}
impl LineTarget {
/// The end nearest to `point` — the end the reference measures from.
fn nearest_end(&self, point: DVec3) -> (ParametricRef, DVec3) {
let [a, b] = self.ends;
if (b.1 - point).length_squared() < (a.1 - point).length_squared() {
b
} else {
a
}
}
}
#[derive(Clone, Copy)]
enum Step {
/// `Specify first constraint point or [Object] <Object>:`
@@ -27,12 +50,37 @@ enum Step {
first: ParametricRef,
first_point: DVec3,
},
/// Aligned's Point & line: `Specify constraint point or [Line] <Line>:`
PointLinePoint,
/// `Select line:` after the constraint point.
PointLineLine {
point: ParametricRef,
point_pos: DVec3,
},
/// `Select line:` first (the Line option).
LineFirst,
/// `Select constraint point:` after the line.
LinePoint { line: LineTarget },
/// Aligned's 2Lines: `Select first line:`
TwoLinesFirst,
/// `Select second line to make parallel:`
TwoLinesSecond { line: LineTarget },
/// The host is making the second line parallel; `accept_parallel_line`
/// brings its solved ends.
TwoLinesParallel {
first: LineTarget,
second: LineTarget,
/// Where the second line was picked.
pick: DVec3,
},
/// `Specify dimension line location:`
Location {
first: ParametricRef,
second: ParametricRef,
first_point: DVec3,
second_point: DVec3,
/// The line the distance is measured perpendicular to.
direction: Option<LineTarget>,
},
/// `Enter value or name and value <d1=100>:`
Value {
@@ -44,6 +92,7 @@ enum Step {
axis: DVec3,
kind: ConstraintKind,
measured: f64,
direction: Option<LineTarget>,
},
}
@@ -63,6 +112,7 @@ impl DimConstraintCommand {
pub const NO_POINT: &'static str = "No valid constraint point found.";
pub const SAME_POINT: &'static str =
"The object or point is already selected. Select a different object or constraint point.";
pub const INVALID_LINE: &'static str = "Invalid selection for Aligned. Select a line segment, polyline segment, text, MText, major or minor axis of ellipse or elliptical arc.";
pub fn new(axis: DimConstraintAxis, default_name: String) -> Self {
Self {
@@ -127,9 +177,72 @@ impl DimConstraintCommand {
Some(DVec3::new(point.x, point.y, point.z))
}
/// The line a Point & line or 2Lines pick measures against: a line
/// entity as a whole, or the picked polyline segment.
fn line_target(entity: &EntityType, handle: Handle, point: DVec3) -> Option<LineTarget> {
let (line, first, second) = match entity {
EntityType::Line(_) => (
ParametricRef::whole(handle),
ParametricRef::point(handle, 0),
ParametricRef::point(handle, 1),
),
EntityType::LwPolyline(_) | EntityType::Polyline2D(_) => {
let (source, _, _) =
crate::scene::centerline::picked_source(entity, handle, point)?;
let index = source.segment_index;
if index < 0 {
return None;
}
let end = if resolve_point(entity, index + 1).is_some() {
index + 1
} else {
0
};
(
ParametricRef::segment(handle, index as usize),
ParametricRef::point(handle, index),
ParametricRef::point(handle, end),
)
}
_ => return None,
};
let start = Self::world(entity, first)?;
let finish = Self::world(entity, second)?;
let dir = (finish - start).try_normalize()?;
Some(LineTarget {
line,
dir,
ends: [(first, start), (second, finish)],
})
}
/// A picked line for the current line step, or the message to show.
fn pick_line(&mut self, handle: Handle, point: DVec3) -> Result<LineTarget, &'static str> {
if handle.is_null() {
return Err(Self::NO_OBJECT);
}
let Some(entity) = self.picked_entity.take() else {
return Err("");
};
Self::line_target(&entity, handle, point).ok_or(Self::INVALID_LINE)
}
/// The measured kind and axis: Linear reads the dimension line location
/// the way a linear dimension does, the others are fixed.
fn decide(&self, first: DVec3, second: DVec3, location: DVec3) -> (ConstraintKind, DVec3) {
/// the way a linear dimension does, the others are fixed; a Point & line
/// or 2Lines distance runs perpendicular to its line.
fn decide(
&self,
first: DVec3,
second: DVec3,
location: DVec3,
direction: Option<LineTarget>,
) -> (ConstraintKind, DVec3) {
if let Some(line) = direction {
return (
ConstraintKind::DistanceDirected,
DVec3::new(line.dir.y, -line.dir.x, 0.0),
);
}
match self.axis {
DimConstraintAxis::Linear => {
let axis =
@@ -149,10 +262,11 @@ impl DimConstraintCommand {
}
}
fn measured(kind: ConstraintKind, first: DVec3, second: DVec3) -> f64 {
fn measured(kind: ConstraintKind, first: DVec3, second: DVec3, axis: DVec3) -> f64 {
match kind {
ConstraintKind::DistanceX => (second.x - first.x).abs(),
ConstraintKind::DistanceY => (second.y - first.y).abs(),
ConstraintKind::DistanceDirected => (second - first).dot(axis).abs(),
_ => (second - first).length(),
}
}
@@ -166,6 +280,7 @@ impl DimConstraintCommand {
location,
axis,
kind,
direction,
..
} = self.step
else {
@@ -182,6 +297,7 @@ impl DimConstraintCommand {
second_point,
location,
axis,
direction: direction.map(|line| line.line),
name,
expression,
label: match self.axis {
@@ -202,11 +318,23 @@ impl CadCommand for DimConstraintCommand {
fn prompt(&self) -> String {
let name = self.command_name();
match self.step {
Step::First if self.axis == DimConstraintAxis::Aligned => format!(
"{name} Specify first constraint point or [Object/Point & line/2Lines] <Object>:"
),
Step::First => {
format!("{name} Specify first constraint point or [Object] <Object>:")
}
Step::Object => format!("{name} Select object:"),
Step::Second { .. } => format!("{name} Specify second constraint point:"),
Step::PointLinePoint => {
format!("{name} Specify constraint point or [Line] <Line>:")
}
Step::PointLineLine { .. } | Step::LineFirst => format!("{name} Select line:"),
Step::LinePoint { .. } => format!("{name} Select constraint point:"),
Step::TwoLinesFirst => format!("{name} Select first line:"),
Step::TwoLinesSecond { .. } | Step::TwoLinesParallel { .. } => {
format!("{name} Select second line to make parallel:")
}
Step::Location { .. } => format!("{name} Specify dimension line location:"),
Step::Value { measured, .. } => format!(
"{name} Enter value or name and value <{}={}>:",
@@ -217,9 +345,16 @@ impl CadCommand for DimConstraintCommand {
}
fn options(&self) -> Vec<CmdOption> {
matches!(self.step, Step::First)
.then(|| vec![CmdOption::new("Object", "O")])
.unwrap_or_default()
match self.step {
Step::First if self.axis == DimConstraintAxis::Aligned => vec![
CmdOption::new("Object", "O"),
CmdOption::new("Point & line", "P"),
CmdOption::new("2Lines", "2L"),
],
Step::First => vec![CmdOption::new("Object", "O")],
Step::PointLinePoint => vec![CmdOption::new("Line", "L")],
_ => Vec::new(),
}
}
fn wants_text_input(&self) -> bool {
@@ -227,15 +362,31 @@ impl CadCommand for DimConstraintCommand {
}
fn point_step_accepts_keywords(&self) -> bool {
matches!(self.step, Step::First | Step::Value { .. })
matches!(
self.step,
Step::First | Step::PointLinePoint | Step::Value { .. }
)
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
match self.step {
Step::First => {
let keyword = text.trim().trim_start_matches('_').to_ascii_uppercase();
if matches!(keyword.as_str(), "O" | "OBJECT") {
self.step = Step::Object;
let aligned = self.axis == DimConstraintAxis::Aligned;
match keyword.as_str() {
"O" | "OBJECT" => self.step = Step::Object,
"P" | "POINT" | "POINT & LINE" | "POINT&LINE" if aligned => {
self.step = Step::PointLinePoint
}
"2L" | "2LINES" if aligned => self.step = Step::TwoLinesFirst,
_ => return None,
}
Some(CmdResult::NeedPoint)
}
Step::PointLinePoint => {
let keyword = text.trim().trim_start_matches('_').to_ascii_uppercase();
if matches!(keyword.as_str(), "L" | "LINE") {
self.step = Step::LineFirst;
return Some(CmdResult::NeedPoint);
}
None
@@ -253,7 +404,18 @@ impl CadCommand for DimConstraintCommand {
}
fn needs_entity_pick(&self) -> bool {
matches!(self.step, Step::First | Step::Object | Step::Second { .. })
matches!(
self.step,
Step::First
| Step::Object
| Step::Second { .. }
| Step::PointLinePoint
| Step::PointLineLine { .. }
| Step::LineFirst
| Step::LinePoint { .. }
| Step::TwoLinesFirst
| Step::TwoLinesSecond { .. }
)
}
fn entity_pick_accepts_points(&self) -> bool {
@@ -280,11 +442,72 @@ impl CadCommand for DimConstraintCommand {
match self.step {
// The host resolves the constraint point and hands it back
// through `accept_constraint_point`.
Step::First | Step::Second { .. } => CmdResult::CheckConstraintPoint(CoincidentPick {
handle: (!handle.is_null()).then_some(handle),
point,
whole_curve: false,
}),
Step::First | Step::Second { .. } | Step::PointLinePoint | Step::LinePoint { .. } => {
CmdResult::CheckConstraintPoint(CoincidentPick {
handle: (!handle.is_null()).then_some(handle),
point,
whole_curve: false,
})
}
Step::PointLineLine { point: first, point_pos } => {
match self.pick_line(handle, point) {
Ok(line) => {
if line.line.entity == first.entity {
return Self::report(Self::SAME_POINT);
}
let (second, second_point) = line.nearest_end(point_pos);
self.step = Step::Location {
first,
second,
first_point: point_pos,
second_point,
direction: Some(line),
};
CmdResult::NeedPoint
}
Err("") => CmdResult::NeedPoint,
Err(message) => Self::report(message),
}
}
Step::LineFirst => match self.pick_line(handle, point) {
Ok(line) => {
self.step = Step::LinePoint { line };
CmdResult::NeedPoint
}
Err("") => CmdResult::NeedPoint,
Err(message) => Self::report(message),
},
Step::TwoLinesFirst => match self.pick_line(handle, point) {
Ok(line) => {
self.step = Step::TwoLinesSecond { line };
CmdResult::NeedPoint
}
Err("") => CmdResult::NeedPoint,
Err(message) => Self::report(message),
},
Step::TwoLinesSecond { line: first_line } => match self.pick_line(handle, point) {
Ok(second_line) => {
if second_line.line == first_line.line {
return Self::report(Self::SAME_POINT);
}
// The host makes the second line parallel first; the
// distance is measured on the solved geometry.
self.step = Step::TwoLinesParallel {
first: first_line,
second: second_line,
pick: point,
};
CmdResult::MakeParallel {
first_line: first_line.line,
first_ends: [first_line.ends[0].0, first_line.ends[1].0],
second_line: second_line.line,
second_ends: [second_line.ends[0].0, second_line.ends[1].0],
}
}
Err("") => CmdResult::NeedPoint,
Err(message) => Self::report(message),
},
Step::TwoLinesParallel { .. } => CmdResult::NeedPoint,
Step::Object => {
if handle.is_null() {
return Self::report(Self::NO_OBJECT);
@@ -308,6 +531,7 @@ impl CadCommand for DimConstraintCommand {
second,
first_point,
second_point,
direction: None,
};
CmdResult::NeedPoint
}
@@ -315,6 +539,32 @@ impl CadCommand for DimConstraintCommand {
}
}
fn accept_parallel_line(&mut self, ends: [(ParametricRef, DVec3); 2]) -> CmdResult {
let Step::TwoLinesParallel { first, second, pick } = self.step else {
return CmdResult::NeedPoint;
};
let Some(dir) = (ends[1].1 - ends[0].1).try_normalize() else {
return Self::report(Self::NO_POINT);
};
let second = LineTarget {
line: second.line,
dir,
ends,
};
// The first line's end nearest the second pick, then the second
// line's end nearest that end.
let (first_ref, first_point) = first.nearest_end(pick);
let (second_ref, second_point) = second.nearest_end(first_point);
self.step = Step::Location {
first: first_ref,
second: second_ref,
first_point,
second_point,
direction: Some(first),
};
CmdResult::NeedPoint
}
fn accept_constraint_point(&mut self, reference: ParametricRef, point: DVec3) -> CmdResult {
match self.step {
Step::First => {
@@ -333,6 +583,28 @@ impl CadCommand for DimConstraintCommand {
second: reference,
first_point,
second_point: point,
direction: None,
};
CmdResult::NeedPoint
}
Step::PointLinePoint => {
self.step = Step::PointLineLine {
point: reference,
point_pos: point,
};
CmdResult::NeedPoint
}
Step::LinePoint { line } => {
if line.line.entity == reference.entity {
return Self::report(Self::SAME_POINT);
}
let (first, first_point) = line.nearest_end(point);
self.step = Step::Location {
first,
second: reference,
first_point,
second_point: point,
direction: Some(line),
};
CmdResult::NeedPoint
}
@@ -342,20 +614,27 @@ impl CadCommand for DimConstraintCommand {
fn on_point(&mut self, point: DVec3) -> CmdResult {
match self.step {
Step::First | Step::Second { .. } => CmdResult::CheckConstraintPoint(CoincidentPick {
handle: None,
point,
whole_curve: false,
}),
Step::Object => Self::report(Self::NO_OBJECT),
Step::First | Step::Second { .. } | Step::PointLinePoint | Step::LinePoint { .. } => {
CmdResult::CheckConstraintPoint(CoincidentPick {
handle: None,
point,
whole_curve: false,
})
}
Step::Object
| Step::PointLineLine { .. }
| Step::LineFirst
| Step::TwoLinesFirst
| Step::TwoLinesSecond { .. } => Self::report(Self::NO_OBJECT),
Step::Location {
first,
second,
first_point,
second_point,
direction,
} => {
let (kind, axis) = self.decide(first_point, second_point, point);
let measured = Self::measured(kind, first_point, second_point);
let (kind, axis) = self.decide(first_point, second_point, point, direction);
let measured = Self::measured(kind, first_point, second_point, axis);
self.step = Step::Value {
first,
second,
@@ -365,10 +644,11 @@ impl CadCommand for DimConstraintCommand {
axis,
kind,
measured,
direction,
};
CmdResult::ReportMeasurement(format!("Dimension text = {measured:.4}"))
}
Step::Value { .. } => CmdResult::NeedPoint,
Step::TwoLinesParallel { .. } | Step::Value { .. } => CmdResult::NeedPoint,
}
}
@@ -378,6 +658,10 @@ impl CadCommand for DimConstraintCommand {
self.step = Step::Object;
CmdResult::NeedPoint
}
Step::PointLinePoint => {
self.step = Step::LineFirst;
CmdResult::NeedPoint
}
Step::Value { measured, .. } => self
.build(self.default_name.clone(), measured_expression(measured))
.unwrap_or(CmdResult::Cancel),
+164 -15
View File
@@ -374,17 +374,145 @@ pub(crate) fn dynamic_dimension_text(name: &str, value: f64, format: u8) -> Stri
}
}
/// The expression a measured value seeds a parameter with (`100`, `111.8034`).
/// The expression a measured value seeds a parameter with: twelve
/// significant digits with the trailing zeros dropped (`100`,
/// `111.803398875`), as the reference stores it.
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 == "-" {
if !value.is_finite() {
return "0".to_string();
}
// Solver noise (24.9999999047) reads as the round value it means.
let snapped = (value * 1.0e4).round() / 1.0e4;
let value = if (value - snapped).abs() < 1.0e-6 {
snapped
} else {
value
};
let magnitude = value.abs();
let integer_digits = if magnitude < 1.0 {
1
} else {
magnitude.log10().floor() as i32 + 1
};
let decimals = (12 - integer_digits).clamp(0, 12) as usize;
let text = format!("{value:.decimals$}");
let text = if text.contains('.') {
text.trim_end_matches('0').trim_end_matches('.')
} else {
text.as_str()
};
if text.is_empty() || text == "-" || text == "-0" {
"0".to_string()
} else {
text.to_string()
}
}
/// Moves a dynamic dimension's extension origins to `first`/`second`,
/// keeping its dimension line where it was; true when anything moved.
fn dynamic_dimension_follow_points(
dimension: &mut acadrust::entities::Dimension,
first: Vector3,
second: Vector3,
) -> bool {
use acadrust::entities::Dimension;
let same = |a: Vector3, b: Vector3| (a - b).length_squared() < 1.0e-16;
let to_dvec = |p: Vector3| glam::DVec3::new(p.x, p.y, p.z);
let (current_first, current_second, definition, axis) = match dimension {
Dimension::Aligned(d) => (d.first_point, d.second_point, d.definition_point, None),
Dimension::Linear(d) => (
d.first_point,
d.second_point,
d.definition_point,
Some(glam::DVec3::new(d.rotation.cos(), d.rotation.sin(), 0.0)),
),
_ => return false,
};
if same(current_first, first) && same(current_second, second) {
return false;
}
let text = dimension.base().user_text.clone();
let rebuilt = match axis {
None => crate::modules::annotate::aligned_dim::aligned_dimension_entity(
to_dvec(first),
to_dvec(second),
to_dvec(definition),
text,
),
Some(axis) => crate::modules::annotate::linear_dim::linear_dimension_entity(
to_dvec(first),
to_dvec(second),
to_dvec(definition),
axis,
text,
),
};
let acadrust::EntityType::Dimension(mut rebuilt) = rebuilt else {
return false;
};
// Everything but the geometry stays: identity, layer, color, style, xdata.
let base = dimension.base();
let target = rebuilt.base_mut();
target.common = base.common.clone();
target.style_name = base.style_name.clone();
target.text_rotation = base.text_rotation;
target.attachment_point = base.attachment_point;
target.line_spacing_style = base.line_spacing_style;
target.line_spacing_factor = base.line_spacing_factor;
*dimension = rebuilt;
true
}
/// What stays put when a dimensional constraint's value changes: its first
/// point, and — when the distance runs perpendicular to a line that owns
/// that point (2Lines, line-first Point & line) — the whole line, so the
/// other object moves, as in the reference.
pub(crate) fn dimensional_anchor_refs(
document: &acadrust::CadDocument,
refs: &[ParametricRef],
) -> Vec<ParametricRef> {
let Some(&first) = refs.first() else {
return Vec::new();
};
let mut anchors = vec![first];
if let Some(&line) = refs.get(2).filter(|line| line.entity == first.entity) {
let ends = match line.segment_index() {
Some(index) => {
let index = index as i32;
let end = document
.get_entity(line.entity)
.and_then(|entity| resolve_point(entity, index + 1))
.map_or(0, |_| index + 1);
[
ParametricRef::point(line.entity, index),
ParametricRef::point(line.entity, end),
]
}
None => [
ParametricRef::point(line.entity, 0),
ParametricRef::point(line.entity, 1),
],
};
anchors.extend(ends.into_iter().filter(|end| *end != first));
}
anchors
}
/// The constraint a dynamic dimension shows, with the table its parameter
/// lives in.
pub(crate) fn dynamic_dimension_constraint(
sets: &[ParametricConstraintSet],
handle: Handle,
) -> Option<(&ParametricConstraintSet, &ParametricConstraint)> {
sets.iter().find_map(|set| {
let id = set
.dimensions
.iter()
.find_map(|(id, dimension)| (*dimension == handle).then_some(*id))?;
Some((set, set.get(id)?))
})
}
/// 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(
@@ -1460,10 +1588,11 @@ impl super::Scene {
/// Rewrites every dynamic dimension's text from its constraint's
/// parameter name and current value, in the CONSTRAINTNAMEFORMAT
/// reading `constraint_name_format` selects.
/// reading `constraint_name_format` selects, and moves its extension
/// origins to where the solve left the constraint points.
pub(crate) fn refresh_dynamic_dimension_texts(&mut self) {
let format = self.constraint_name_format;
let mut updates: Vec<(Handle, String)> = Vec::new();
let mut updates: Vec<(Handle, String, Option<(Vector3, Vector3)>)> = Vec::new();
for set in &self.parametric_constraints {
let table = if set.local_parameters.is_empty() {
&self.named_parameters
@@ -1481,23 +1610,43 @@ impl super::Scene {
Some(DrivingValue::Literal(value)) => (String::new(), *value),
None => continue,
};
updates.push((*dimension, dynamic_dimension_text(&name, value, format)));
let world = |reference: &ParametricRef| {
let entity = self.document.get_entity(reference.entity)?;
resolve_point(entity, reference.marker?)
};
let points = match constraint.refs.as_slice() {
[first, second, ..] => world(first).zip(world(second)),
_ => None,
};
updates.push((
*dimension,
dynamic_dimension_text(&name, value, format),
points,
));
}
}
for (handle, text) in updates {
for (handle, text, points) 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;
let mut changed = false;
if let Some((first, second)) = points {
if dynamic_dimension_follow_points(&mut dimension, first, second) {
changed = true;
}
}
if dimension.base().user_text.as_deref() != Some(text.as_str()) {
crate::entities::dimension::set_dimension_text_override(
dimension.base_mut(),
Some(text),
);
changed = true;
}
if changed {
self.update_entity(acadrust::EntityType::Dimension(dimension));
}
crate::entities::dimension::set_dimension_text_override(
dimension.base_mut(),
Some(text),
);
self.update_entity(acadrust::EntityType::Dimension(dimension));
}
}
+15 -1
View File
@@ -993,9 +993,15 @@ fn resolved_target(params: &ParameterTable, constraint: &ParametricConstraint) -
if !value.is_finite() {
return None;
}
// A dimensional distance keeps its sign in the parameter but measures
// its magnitude, as the reference does (`d1=-50` shortens the line to
// 50; `0` folds the points together).
if constraint.kind == ConstraintKind::Distance {
return Some(value.abs());
}
let must_be_positive = matches!(
constraint.kind,
ConstraintKind::Distance | ConstraintKind::Radius | ConstraintKind::Diameter
ConstraintKind::Radius | ConstraintKind::Diameter
);
(!must_be_positive || value > 0.0).then_some(value)
}
@@ -1590,6 +1596,14 @@ fn build_constraint(
let Some(resolved) = resolved_target(params, c) else {
return Vec::new();
};
// A zero distance folds the points together; the distance
// residual has no gradient there, the coordinate equalities do.
if resolved.abs() <= f64::EPSILON {
return vec![
Rc::new(Equal::new(pa.x, pb.x, 1.0)),
Rc::new(Equal::new(pa.y, pb.y, 1.0)),
];
}
let target = sys.add_param(resolved, true);
vec![Rc::new(P2PDistance::new(pa, pb, target))]
}