forked from 1b-agent/opencadstudio-upstream
Merge pull request #1289 from ramox81/fix/coincident-standard-command
# Conflicts: # src/command.rs # src/scene/mod.rs # src/scene/parametric_solve.rs
This commit is contained in:
commit
acde01cfc3
12 files changed
+1273
-228
No files matched your search
+200
-37
@@ -2186,10 +2186,17 @@ impl OpenCADStudio {
|
||||
self.tabs[i].active_cmd = None;
|
||||
return Task::none();
|
||||
}
|
||||
if matches!(&transform, crate::command::EntityTransform::Translate(_)) {
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
handles = self.tabs[i]
|
||||
.scene
|
||||
.parametric_connected_handles(scope, &handles);
|
||||
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
|
||||
}
|
||||
let label = self.history_label_from_active_cmd(i, "MOVE");
|
||||
// A move/rotate/scale/mirror mutates only the selected entities
|
||||
// (and their baked dimension sub-entities) through
|
||||
// transform_entities — always delta-safe.
|
||||
// A translation carries its complete connected constraint
|
||||
// component so each member keeps its own dimensions. Other
|
||||
// transforms retain their selected-object behavior.
|
||||
let pending = self.begin_undo(i, label, handles.len(), true);
|
||||
self.tabs[i].scene.transform_entities(&handles, &transform);
|
||||
self.apply_inferred_constraints(i, &handles);
|
||||
@@ -3068,44 +3075,167 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
CmdResult::AddCoincidentConstraint {
|
||||
point_a,
|
||||
point_b,
|
||||
first,
|
||||
second,
|
||||
multiple,
|
||||
label,
|
||||
} => {
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
let to_world = |p: glam::DVec3| acadrust::types::Vector3::new(p.x, p.y, p.z);
|
||||
let resolved_a = crate::scene::parametric_constraints::nearest_parametric_point(
|
||||
&self.tabs[i].scene.document,
|
||||
scope,
|
||||
to_world(point_a),
|
||||
None,
|
||||
);
|
||||
let resolved_b = resolved_a.and_then(|ref_a| {
|
||||
crate::scene::parametric_constraints::nearest_parametric_point(
|
||||
&self.tabs[i].scene.document,
|
||||
scope,
|
||||
to_world(point_b),
|
||||
None,
|
||||
)
|
||||
.filter(|ref_b| *ref_b != ref_a)
|
||||
});
|
||||
match (resolved_a, resolved_b) {
|
||||
(Some(ref_a), Some(ref_b)) => {
|
||||
return self.apply_cmd_result(CmdResult::AddParametricConstraint {
|
||||
kind: crate::scene::parametric_constraints::ConstraintKind::Coincident,
|
||||
refs: vec![ref_a, ref_b],
|
||||
driving_param: None,
|
||||
label,
|
||||
});
|
||||
let resolve = |pick: crate::command::CoincidentPick,
|
||||
exclude: Option<Handle>| {
|
||||
if pick.whole_curve {
|
||||
pick.handle.and_then(|handle| {
|
||||
crate::scene::parametric_constraints::parametric_curve_ref_for_pick(
|
||||
&self.tabs[i].scene.document,
|
||||
scope,
|
||||
handle,
|
||||
to_world(pick.point),
|
||||
)
|
||||
})
|
||||
} else if let Some(handle) = pick.handle {
|
||||
crate::scene::parametric_constraints::nearest_parametric_point_on_entity(
|
||||
&self.tabs[i].scene.document,
|
||||
scope,
|
||||
handle,
|
||||
to_world(pick.point),
|
||||
)
|
||||
} else {
|
||||
crate::scene::parametric_constraints::nearest_parametric_point(
|
||||
&self.tabs[i].scene.document,
|
||||
scope,
|
||||
to_world(pick.point),
|
||||
exclude,
|
||||
)
|
||||
}
|
||||
};
|
||||
let resolved_first = resolve(first, None);
|
||||
let resolved_second = resolved_first
|
||||
.and_then(|reference| resolve(second, Some(reference.entity)));
|
||||
let (Some(first_ref), Some(second_ref)) = (resolved_first, resolved_second) else {
|
||||
self.command_line.push_error(
|
||||
"Coincident: select a supported endpoint, center, midpoint, vertex, or curve.",
|
||||
);
|
||||
return Task::none();
|
||||
};
|
||||
let (kind, refs) = match (first.whole_curve, second.whole_curve) {
|
||||
(false, false) if first_ref != second_ref => (
|
||||
crate::scene::parametric_constraints::ConstraintKind::Coincident,
|
||||
vec![first_ref, second_ref],
|
||||
),
|
||||
(true, false) => (
|
||||
crate::scene::parametric_constraints::ConstraintKind::PointOnCurve,
|
||||
vec![second_ref, first_ref],
|
||||
),
|
||||
(false, true) => (
|
||||
crate::scene::parametric_constraints::ConstraintKind::PointOnCurve,
|
||||
vec![first_ref, second_ref],
|
||||
),
|
||||
_ => {
|
||||
self.command_line.push_error(
|
||||
"Coincident: pick didn't land on a point (endpoint, center, or vertex) — enable an Endpoint/Center object snap and try again.",
|
||||
);
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.command_line
|
||||
.push_error("Coincident: select one point and one curve, or two points.");
|
||||
return Task::none();
|
||||
}
|
||||
};
|
||||
if let Err(error) = self.tabs[i]
|
||||
.scene
|
||||
.validate_parametric_constraint(kind, &refs, None)
|
||||
{
|
||||
self.command_line.push_error(error);
|
||||
return Task::none();
|
||||
}
|
||||
let touched: Vec<_> = refs.iter().map(|reference| reference.entity).collect();
|
||||
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, label, touched.len(), true);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_parametric_constraints_before(scope, constraints_before);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set_mut(scope)
|
||||
.add(kind, refs, None);
|
||||
let changes: Vec<_> = touched
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i].scene.bump_entities_with_parametric_policy(
|
||||
&changes,
|
||||
&[first_ref],
|
||||
self.constraint_solve_mode,
|
||||
);
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].snap_result = None;
|
||||
if !multiple {
|
||||
self.tabs[i].active_cmd = None;
|
||||
}
|
||||
self.command_line.push_output("Coincident constraint applied.");
|
||||
if multiple {
|
||||
if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|cmd| cmd.prompt()) {
|
||||
self.command_line.push_info(&prompt);
|
||||
}
|
||||
self.command_line.set_step_options(
|
||||
self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|cmd| cmd.options())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
self.refresh_properties();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
CmdResult::AddAutoCoincidentConstraints { handles } => {
|
||||
use crate::scene::parametric_constraints::ConstraintKind;
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
let inferred = self.tabs[i]
|
||||
.scene
|
||||
.inferred_coincident_constraints(scope, &handles);
|
||||
let count = inferred.len();
|
||||
if count > 0 {
|
||||
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, "Coincident auto constrain", handles.len(), true);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_parametric_constraints_before(scope, constraints_before);
|
||||
for refs in inferred {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set_mut(scope)
|
||||
.add(ConstraintKind::Coincident, refs, None);
|
||||
}
|
||||
let changes: Vec<_> = handles
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.command_line
|
||||
.push_output(format!("{} coincident constraint(s) applied.", count).as_str());
|
||||
}
|
||||
CmdResult::AddPointOnEntityConstraint {
|
||||
point,
|
||||
@@ -8443,8 +8573,17 @@ mod parametric_constraint_undo_tests {
|
||||
let b = add_line(&mut app, 20.0, 0.0, 20.0, 5.0); // deliberately far from `a`, so the two picks are unambiguous
|
||||
|
||||
let _ = app.apply_cmd_result(CmdResult::AddCoincidentConstraint {
|
||||
point_a: glam::DVec3::new(5.0, 0.0, 0.0), // a's end
|
||||
point_b: glam::DVec3::new(20.0, 0.0, 0.0), // b's start
|
||||
first: crate::command::CoincidentPick {
|
||||
handle: Some(a),
|
||||
point: glam::DVec3::new(5.0, 0.0, 0.0),
|
||||
whole_curve: false,
|
||||
},
|
||||
second: crate::command::CoincidentPick {
|
||||
handle: Some(b),
|
||||
point: glam::DVec3::new(20.0, 0.0, 0.0),
|
||||
whole_curve: false,
|
||||
},
|
||||
multiple: false,
|
||||
label: "Coincident constraint",
|
||||
});
|
||||
|
||||
@@ -8460,6 +8599,21 @@ mod parametric_constraint_undo_tests {
|
||||
entities.contains(&a) && entities.contains(&b),
|
||||
"the constraint should reference both lines"
|
||||
);
|
||||
let Some(acadrust::EntityType::Line(first)) =
|
||||
app.tabs[app.active_tab].scene.document.get_entity(a)
|
||||
else {
|
||||
panic!("expected first line");
|
||||
};
|
||||
let Some(acadrust::EntityType::Line(second)) =
|
||||
app.tabs[app.active_tab].scene.document.get_entity(b)
|
||||
else {
|
||||
panic!("expected second line");
|
||||
};
|
||||
assert!(
|
||||
(first.end - acadrust::types::Vector3::new(5.0, 0.0, 0.0)).length() < 1.0e-7
|
||||
);
|
||||
assert!((second.start - first.end).length() < 1.0e-7);
|
||||
assert!((second.length() - 5.0).abs() < 1.0e-7);
|
||||
}
|
||||
|
||||
/// A pick that doesn't land near any real point (no object snap match)
|
||||
@@ -8472,8 +8626,17 @@ mod parametric_constraint_undo_tests {
|
||||
let _a = add_line(&mut app, 0.0, 0.0, 5.0, 0.0);
|
||||
|
||||
let _ = app.apply_cmd_result(CmdResult::AddCoincidentConstraint {
|
||||
point_a: glam::DVec3::new(5.0, 0.0, 0.0), // a's end — a real point
|
||||
point_b: glam::DVec3::new(500.0, 500.0, 0.0), // nowhere near anything
|
||||
first: crate::command::CoincidentPick {
|
||||
handle: Some(_a),
|
||||
point: glam::DVec3::new(5.0, 0.0, 0.0),
|
||||
whole_curve: false,
|
||||
},
|
||||
second: crate::command::CoincidentPick {
|
||||
handle: None,
|
||||
point: glam::DVec3::new(500.0, 500.0, 0.0),
|
||||
whole_curve: false,
|
||||
},
|
||||
multiple: false,
|
||||
label: "Coincident constraint",
|
||||
});
|
||||
|
||||
|
||||
@@ -1331,13 +1331,20 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
"CCONSTRAINT" => {
|
||||
"CCONSTRAINT" | "GCCOINCIDENT" => {
|
||||
use crate::modules::parametric::CoincidentConstraintCommand;
|
||||
let new_cmd = CoincidentConstraintCommand::new();
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
"GEOMCONSTRAINT" => {
|
||||
use crate::modules::parametric::GeomConstraintCommand;
|
||||
let new_cmd = GeomConstraintCommand::new();
|
||||
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();
|
||||
|
||||
+11
-71
@@ -882,12 +882,21 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
if !compact_solid {
|
||||
sections.extend(crate::entities::object_data::sections(
|
||||
let mut object_sections = crate::entities::object_data::sections(
|
||||
&self.tabs[i].scene.document,
|
||||
&self.tabs[i].scene.object_data_cache,
|
||||
handle,
|
||||
entity,
|
||||
));
|
||||
);
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
let constrained = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(scope)
|
||||
.is_some_and(|set| set.constraints_touching(handle).next().is_some());
|
||||
if constrained {
|
||||
object_sections.retain(|section| section.title != "Associative Data");
|
||||
}
|
||||
sections.extend(object_sections);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -2225,75 +2234,6 @@ impl OpenCADStudio {
|
||||
if compact_solid {
|
||||
retain_compact_solid_sections(&mut sections);
|
||||
}
|
||||
// Parametric-constraint list for this entity —
|
||||
// the Properties-panel analogue of the viewport's
|
||||
// constraint glyph pills: one row per constraint
|
||||
// touching `handle`, clickable to select every entity
|
||||
// it relates (PropConstraintLinkClick). Omitted
|
||||
// entirely when the entity has none — an unconstrained
|
||||
// entity (the common case) shouldn't gain empty
|
||||
// bookkeeping.
|
||||
{
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
let scene = &self.tabs[i].scene;
|
||||
if let Some(set) = scene.parametric_constraint_set(scope) {
|
||||
let parameters = if set.local_parameters.is_empty() {
|
||||
scene.named_parameters()
|
||||
} else {
|
||||
&set.local_parameters
|
||||
};
|
||||
let props: Vec<crate::scene::model::object::Property> = set
|
||||
.constraints_touching(handle)
|
||||
.map(|c| {
|
||||
let conflicting =
|
||||
set.conflicts.iter().any(|(id, _)| *id == c.id);
|
||||
let value_suffix = match &c.driving_param {
|
||||
Some(
|
||||
crate::scene::named_parameters::DrivingValue::Literal(
|
||||
v,
|
||||
),
|
||||
) => {
|
||||
format!(" = {v:.2}")
|
||||
}
|
||||
Some(
|
||||
crate::scene::named_parameters::DrivingValue::Named(
|
||||
name,
|
||||
),
|
||||
) => match parameters.resolve(name) {
|
||||
Ok(v) => format!(" = {name} ({v:.2})"),
|
||||
Err(_) => format!(" = {name} (?)"),
|
||||
},
|
||||
None => String::new(),
|
||||
};
|
||||
let label = format!(
|
||||
"{} {:?}{}",
|
||||
c.kind.glyph_symbol(),
|
||||
c.kind,
|
||||
value_suffix
|
||||
);
|
||||
let mut handles: Vec<acadrust::Handle> =
|
||||
c.refs.iter().map(|r| r.entity).collect();
|
||||
handles.sort_by_key(|h| h.value());
|
||||
handles.dedup();
|
||||
crate::scene::model::object::Property {
|
||||
label,
|
||||
field: "parametric_constraint",
|
||||
value: crate::scene::model::object::PropValue::EntityLink {
|
||||
id: c.id,
|
||||
handles,
|
||||
conflicting,
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if !props.is_empty() {
|
||||
sections.push(crate::scene::model::object::PropSection {
|
||||
title: t!("Constraints").into_owned(),
|
||||
props,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let title = match entity {
|
||||
acadrust::EntityType::Insert(ins) => {
|
||||
let is_xref = self.tabs[i]
|
||||
|
||||
Reference in new issue
Block a user