forked from 1b-agent/opencadstudio-upstream
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
971abe64a4
commit
6969c19746
8 files changed
+855
-64
No files matched your search
+213
-17
@@ -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::{
|
||||
|
||||
Reference in new issue
Block a user