Match Fix geometric constraint behavior

1) FXCONSTRAINT (and the new GCFIX name) prompts "Select point or [Object]
   <Object>:" — a pick fixes that constraint point; Enter or O asks for one
   line, polyline segment, circle, arc, ellipse or spline; misses and
   unsupported objects report the reference messages and re-prompt.
2) Point, block, text, MText, attribute and table insertion points are
   addressable constraint points.
3) Re-applying Fixed to the same point or object reports that the constraint
   already exists instead of adding a duplicate.
4) A persistent Fixed holds its pre-edit geometry through MOVE, ROTATE,
   SCALE, STRETCH, grip and Properties edits: fixed points stay, free points
   of a partly fixed entity follow the edit, and size retention skips fixed
   entities.
5) Object mode on a polyline fixes the picked segment (natively persisted)
   instead of the whole polyline; arc segments pin endpoints, center and
   radius.
6) Ribbon label "Fix" with a padlock icon.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ramox81andClaude Fable 5.1 committed 2026-09-18 15:38:33 +03:00
1 parent 2b38f798c0
commit f998b59193
9 files changed
+483 -206

No files matched your search

+3 -5
View File
@@ -1,7 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<line x1="4" y1="8" x2="20" y2="8" stroke="#6DB7ED" stroke-width="2.25" stroke-linecap="round"/>
<line x1="6" y1="8" x2="3" y2="13" stroke="#B4B6B9" stroke-width="1.6" stroke-linecap="round"/>
<line x1="11" y1="8" x2="8" y2="13" stroke="#B4B6B9" stroke-width="1.6" stroke-linecap="round"/>
<line x1="16" y1="8" x2="13" y2="13" stroke="#B4B6B9" stroke-width="1.6" stroke-linecap="round"/>
<line x1="21" y1="8" x2="18" y2="13" stroke="#B4B6B9" stroke-width="1.6" stroke-linecap="round"/>
<path d="M8 10.5V8a4 4 0 0 1 8 0v2.5" fill="none" stroke="#B4B6B9" stroke-width="2" stroke-linecap="round"/>
<rect x="5.5" y="10.5" width="13" height="9.5" rx="1.5" fill="none" stroke="#6DB7ED" stroke-width="2.25"/>
<circle cx="12" cy="15.25" r="1.25" fill="#6DB7ED"/>
</svg>

Before

Width:  |  Height:  |  Size: 587 B

After

Width:  |  Height:  |  Size: 366 B

+77 -8
View File
@@ -3177,23 +3177,23 @@ impl OpenCADStudio {
driving_param,
label,
} => {
use crate::scene::parametric_constraints::ConstraintKind;
// GCSMOOTH and FXCONSTRAINT re-prompt after a rejected pick.
let keep_command = self.tabs[i].active_cmd.as_ref().is_some_and(|command| {
(kind == ConstraintKind::Smooth && command.name() == "GCSMOOTH")
|| (kind == ConstraintKind::Fixed && command.name() == "FXCONSTRAINT")
});
if let Err(message) = self.tabs[i].scene.validate_parametric_constraint(
kind,
&refs,
driving_param.as_ref(),
) {
let keep_smooth_selection = kind
== crate::scene::parametric_constraints::ConstraintKind::Smooth
&& self.tabs[i]
.active_cmd
.as_ref()
.is_some_and(|command| command.name() == "GCSMOOTH");
if !keep_smooth_selection {
if !keep_command {
self.tabs[i].active_cmd = None;
}
self.tabs[i].snap_result = None;
self.command_line.push_error(message);
if keep_smooth_selection {
if keep_command {
if let Some(prompt) =
self.tabs[i].active_cmd.as_ref().map(|command| command.prompt())
{
@@ -3203,6 +3203,22 @@ impl OpenCADStudio {
return Task::none();
}
let scope = self.tabs[i].current_parametric_scope();
if kind == ConstraintKind::Fixed
&& self.tabs[i]
.scene
.parametric_constraint_set(scope)
.is_some_and(|set| {
set.constraints.iter().any(|existing| {
existing.enabled && existing.kind == kind && existing.refs == refs
})
})
{
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.command_line
.push_error("The constraint already exists on the selected objects.");
return Task::none();
}
let constraints_before = self.tabs[i]
.scene
.parametric_constraint_set(scope)
@@ -3244,6 +3260,59 @@ impl OpenCADStudio {
self.commit_undo_delta(i, pd);
}
}
CmdResult::AddFixedConstraint(pick) => {
use crate::modules::parametric::FixConstraintCommand;
use crate::scene::parametric_constraints::{
nearest_parametric_point, nearest_parametric_point_on_entity,
parametric_curve_ref_for_pick, ConstraintKind,
};
let scope = self.tabs[i].current_parametric_scope();
let document = &self.tabs[i].scene.document;
let world =
acadrust::types::Vector3::new(pick.point.x, pick.point.y, pick.point.z);
let resolved = match (pick.whole_curve, pick.handle) {
(true, Some(handle)) => {
parametric_curve_ref_for_pick(document, scope, handle, world)
.ok_or(FixConstraintCommand::INVALID_OBJECT)
}
(true, None) => Err(FixConstraintCommand::NO_OBJECT),
(false, Some(handle)) => {
nearest_parametric_point_on_entity(document, scope, handle, world)
.ok_or(FixConstraintCommand::INVALID_OBJECT)
}
(false, None) => nearest_parametric_point(document, scope, world, None)
.ok_or(FixConstraintCommand::NO_POINT),
};
// An off-plane or 3D curve is "not a valid object" to the
// reference, not a solver limitation.
let resolved = resolved.and_then(|reference| {
let supported = !pick.whole_curve
|| self.tabs[i]
.scene
.validate_parametric_constraint(
ConstraintKind::Fixed,
&[reference],
None,
)
.is_ok();
supported
.then_some(reference)
.ok_or(FixConstraintCommand::INVALID_OBJECT)
});
return match resolved {
Ok(reference) => self.apply_cmd_result(CmdResult::AddParametricConstraint {
kind: ConstraintKind::Fixed,
refs: vec![reference],
driving_param: None,
label: "Fixed constraint",
}),
// A miss re-prompts: `ReportError` keeps the command.
Err(message) => {
self.apply_cmd_result(CmdResult::ReportError(message.to_string()))
}
};
}
CmdResult::AddHorizontalConstraint {
selection,
direction,
+37 -8
View File
@@ -1420,7 +1420,7 @@ impl OpenCADStudio {
}
}
"VCONSTRAINT" | "FXCONSTRAINT" => {
"VCONSTRAINT" => {
let handles = self.tabs[i].scene.selected_handles_in_order();
if handles.is_empty() {
use crate::modules::draw::select::SelectObjectsCommand;
@@ -1433,20 +1433,49 @@ impl OpenCADStudio {
} else {
use crate::command::CmdResult;
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef};
let (kind, label) = if cmd == "VCONSTRAINT" {
(ConstraintKind::Vertical, "Vertical constraint")
} else {
(ConstraintKind::Fixed, "Fixed constraint")
};
return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint {
kind,
kind: ConstraintKind::Vertical,
refs: vec![ParametricRef::whole(handles[0])],
driving_param: None,
label,
label: "Vertical constraint",
}));
}
}
"FXCONSTRAINT" | "GCFIX" => {
use crate::command::CmdResult;
use crate::modules::parametric::FixConstraintCommand;
use crate::scene::parametric_constraints::ConstraintKind;
// One preselected whole curve applies at once (ribbon
// "select first, then click"); anything else goes through
// the reference's own point-or-object prompt.
let handles = self.tabs[i].scene.selected_handles_in_order();
if let [handle] = handles.as_slice() {
let reference = self.tabs[i]
.scene
.document
.get_entity(*handle)
.and_then(|entity| {
FixConstraintCommand::preselected_reference(entity, *handle)
});
if let Some(reference) = reference {
return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint {
kind: ConstraintKind::Fixed,
refs: vec![reference],
driving_param: None,
label: "Fixed constraint",
}));
}
}
if !handles.is_empty() {
self.tabs[i].scene.deselect_all();
}
let command = FixConstraintCommand::new();
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
"CCONSTRAINT" | "GCCOINCIDENT" => {
use crate::modules::parametric::CoincidentConstraintCommand;
let new_cmd = CoincidentConstraintCommand::new();
+6
View File
@@ -1570,6 +1570,11 @@ pub enum CmdResult {
kind: crate::scene::parametric_constraints::ConstraintKind,
label: &'static str,
},
/// Add a persistent `Fixed` constraint (`crate::modules::parametric::fixed`)
/// at one picked constraint point (`whole_curve == false`, resolved by the
/// host like `AddPointOnEntityConstraint`'s point) or on the whole curve /
/// polyline segment under the pick (`whole_curve == true`).
AddFixedConstraint(CoincidentPick),
/// Add a persistent `EqualDistance` constraint
/// (`crate::modules::parametric::equal_distance`): the distance
/// between `points[0]`/`points[1]` equals the distance between
@@ -2806,6 +2811,7 @@ mod constraint_registry_tests {
"NRCONSTRAINT",
"LCONSTRAINT",
"FXCONSTRAINT",
"GCFIX",
"SYCONSTRAINT",
"DCONSTRAINT",
"ACONSTRAINT",
+139
View File
@@ -0,0 +1,139 @@
//! GCFIX / FXCONSTRAINT — locks a constraint point, or a whole curve /
//! polyline segment, at its current position. Picks resolve on the host (a
//! `CadCommand` has no document access): `AddFixedConstraint` carries the
//! click, the driver maps it to a `ParametricRef` and re-prompts on a miss,
//! the way the reference does.
use acadrust::{EntityType, Handle};
use glam::DVec3;
use crate::command::{CadCommand, CmdOption, CmdResult, CoincidentPick};
use crate::scene::parametric_constraints::ParametricRef;
#[derive(Clone, Copy)]
enum Step {
PointOrObject,
Object,
}
pub struct FixConstraintCommand {
step: Step,
}
impl FixConstraintCommand {
pub const INVALID_OBJECT: &'static str =
"Invalid selection for Fixed. Select a line, polyline segment, circle, arc, ellipse or spline.";
pub const NO_POINT: &'static str = "No valid constraint point found.";
pub const NO_OBJECT: &'static str = "No object found.";
pub fn new() -> Self {
Self {
step: Step::PointOrObject,
}
}
/// The Object-mode reading of a single preselected entity: a whole
/// curve, or a one-segment polyline's segment. Anything else prompts.
pub fn preselected_reference(entity: &EntityType, handle: Handle) -> Option<ParametricRef> {
match entity {
EntityType::Line(_)
| EntityType::Circle(_)
| EntityType::Arc(_)
| EntityType::Ellipse(_)
| EntityType::Spline(_) => Some(ParametricRef::whole(handle)),
EntityType::LwPolyline(polyline) => (polyline.vertices.len() == 2
&& !polyline.is_closed)
.then(|| ParametricRef::segment(handle, 0)),
EntityType::Polyline2D(polyline) => (polyline.vertices.len() == 2
&& !polyline.is_closed())
.then(|| ParametricRef::segment(handle, 0)),
_ => None,
}
}
fn pick(&self, handle: Option<Handle>, point: DVec3) -> CmdResult {
CmdResult::AddFixedConstraint(CoincidentPick {
handle,
point,
whole_curve: matches!(self.step, Step::Object),
})
}
}
impl CadCommand for FixConstraintCommand {
fn name(&self) -> &'static str {
"FXCONSTRAINT"
}
fn prompt(&self) -> String {
match self.step {
Step::PointOrObject => "FXCONSTRAINT Select point or [Object] <Object>:".to_string(),
Step::Object => "FXCONSTRAINT Select object:".to_string(),
}
}
fn options(&self) -> Vec<CmdOption> {
matches!(self.step, Step::PointOrObject)
.then(|| vec![CmdOption::new("Object", "O")])
.unwrap_or_default()
}
fn wants_text_input(&self) -> bool {
true
}
fn point_step_accepts_keywords(&self) -> bool {
matches!(self.step, Step::PointOrObject)
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let keyword = text.trim().trim_start_matches('_').to_ascii_uppercase();
let to_object =
matches!(self.step, Step::PointOrObject) && matches!(keyword.as_str(), "O" | "OBJECT");
to_object.then(|| {
self.step = Step::Object;
CmdResult::NeedPoint
})
}
fn needs_entity_pick(&self) -> bool {
true
}
fn entity_pick_accepts_points(&self) -> bool {
true
}
fn entity_pick_highlights_hover(&self) -> bool {
true
}
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
if handle.is_null() {
return self.on_point(point);
}
self.pick(Some(handle), point)
}
fn on_point(&mut self, point: DVec3) -> CmdResult {
match self.step {
Step::PointOrObject => self.pick(None, point),
// A click on nothing at "Select object:".
Step::Object => CmdResult::ReportError(Self::NO_OBJECT.to_string()),
}
}
fn on_enter(&mut self) -> CmdResult {
match self.step {
Step::PointOrObject => {
self.step = Step::Object;
CmdResult::NeedPoint
}
Step::Object => CmdResult::Cancel,
}
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
+3
View File
@@ -6,6 +6,8 @@ mod coincident;
mod concentric;
mod constraint_bar;
mod equal_distance;
#[path = "fixed.rs"]
mod fixed_command;
mod geom_constraint;
#[path = "horizontal.rs"]
mod horizontal_command;
@@ -21,6 +23,7 @@ pub use coincident::{coincident_tool, CoincidentConstraintCommand};
pub use concentric::ConcentricConstraintCommand;
pub use constraint_bar::ConstraintBarOptionCommand;
pub use equal_distance::{equal_distance_tool, EqualDistanceConstraintCommand};
pub use fixed_command::FixConstraintCommand;
pub use geom_constraint::GeomConstraintCommand;
pub use horizontal_command::HorizontalConstraintCommand;
pub use perpendicular_command::{PerpendicularConstraintCommand, PerpendicularPick};
+2 -1
View File
@@ -136,7 +136,7 @@ pub mod fixed {
pub fn tool() -> ToolDef {
ToolDef {
id: "FXCONSTRAINT",
label: "Fixed",
label: "Fix",
icon: IconKind::Svg(include_bytes!("../../../assets/icons/constrain/fixed.svg")),
event: ModuleEvent::Command("FXCONSTRAINT".to_string()),
}
@@ -171,6 +171,7 @@ inventory::submit!(crate::command::CommandRegistration {
"NRCONSTRAINT",
"LCONSTRAINT",
"FXCONSTRAINT",
"GCFIX",
"SYCONSTRAINT",
]
});
+28
View File
@@ -587,7 +587,29 @@ impl ParametricConstraintSet {
///
/// Solver-side registration reads raw entity fields directly. This helper is
/// for UI-side consumers that need the current world-space position.
/// The single addressable point (marker `0`) of a point-like entity: the
/// node, insertion, or table origin the solver registers as its
/// `EntityGeom::Point` / text baseline start. `None` for curve entities,
/// whose marker `0` is a `source_points()` endpoint.
pub(crate) fn insertion_point(entity: &acadrust::EntityType) -> Option<Vector3> {
match entity {
acadrust::EntityType::Point(point) => Some(point.location),
acadrust::EntityType::Insert(insert) => Some(insert.insert_point),
acadrust::EntityType::Text(text) => Some(text.insertion_point),
acadrust::EntityType::MText(text) => Some(text.insertion_point),
acadrust::EntityType::AttributeDefinition(attribute) => Some(attribute.insertion_point),
acadrust::EntityType::AttributeEntity(attribute) => Some(attribute.insertion_point),
acadrust::EntityType::Table(table) => Some(table.insertion_point),
_ => None,
}
}
pub(crate) fn resolve_point(entity: &acadrust::EntityType, marker: i32) -> Option<Vector3> {
if marker == 0 {
if let Some(point) = insertion_point(entity) {
return Some(point);
}
}
if marker == -3 {
return match entity {
acadrust::EntityType::Circle(circle) => Some(circle.center_wcs()),
@@ -645,6 +667,12 @@ pub(crate) fn parametric_point_candidates(
.enumerate()
.map(|(marker, point)| (marker as i32, point))
.collect();
// A node / insertion point is a constraint point too (the reference
// fixes a POINT via NODe and TEXT via INSert), and these entities have
// no `source_points()` to collide with marker `0`.
if let Some(point) = insertion_point(entity) {
points.push((0, point));
}
match entity {
acadrust::EntityType::Circle(circle) => points.push((-3, circle.center_wcs())),
acadrust::EntityType::Arc(arc) => points.push((-3, arc.center_wcs())),
+188 -184
View File
@@ -28,6 +28,7 @@ use cadkernel_constraints::solvers::dogleg::solve_dl;
use cadkernel_constraints::solvers::lm::solve_lm;
use cadkernel_constraints::solvers::SolveStatus;
use cadkernel_constraints::system::System;
use cadkernel_constraints::util::ParamId;
use super::named_parameters::ParameterTable;
use super::parametric_constraints::{
@@ -184,6 +185,16 @@ fn register_entity(
handle: Handle,
) -> Option<EntityGeom> {
let entity = document.get_entity(handle)?;
register_entity_geom(sys, handle, entity)
}
/// `register_entity` for an entity value that isn't the document's — the
/// retained pre-edit original a persistent `Fixed` pins to.
fn register_entity_geom(
sys: &mut System,
handle: Handle,
entity: &EntityType,
) -> Option<EntityGeom> {
match entity {
EntityType::Point(point) => Some(EntityGeom::Point(GPoint::new(
sys.add_param(point.location.x, false),
@@ -1027,6 +1038,74 @@ fn retained_tangent_side(
Some(dx * (center[1] - y1) - dy * (center[0] - x1) >= 0.0)
}
/// The kernel parameters a `Fixed` constraint on `r` holds in place: a
/// polyline segment's endpoints (plus center/radius for an arc segment), one
/// addressable point, or every intrinsic parameter of a whole entity. Arc
/// pins its five intrinsic params only — `start`/`end` are already tied to
/// those by the arc-rules `CurveValue` constraints `solve_scope` adds;
/// Ellipse likewise pins center + radmin, `focus1` being tied to `center`
/// by the ellipse-rules `Difference` constraints.
fn fixed_pin_params(
document: &acadrust::CadDocument,
sys: &mut System,
cache: &mut HashMap<Handle, EntityGeom>,
r: ParametricRef,
) -> Vec<ParamId> {
if let Some(index) = r.segment_index() {
let Some(geometry) = resolve_ref(document, sys, cache, r) else {
return Vec::new();
};
if let Some(line) = geometry.line_segment(index) {
return vec![line.p1.x, line.p1.y, line.p2.x, line.p2.y];
}
return geometry
.arc_segment(index)
.map(|segment| {
let arc = segment.arc;
vec![
arc.start.x,
arc.start.y,
arc.end.x,
arc.end.y,
arc.circle.center.x,
arc.circle.center.y,
arc.circle.rad,
]
})
.unwrap_or_default();
}
if r.marker.is_some() {
return resolve_constraint_point(document, sys, cache, r)
.map(|point| vec![point.x, point.y])
.unwrap_or_default();
}
match resolve_ref(document, sys, cache, r) {
Some(EntityGeom::Point(point)) => vec![point.x, point.y],
Some(EntityGeom::Polyline { points, .. }) => {
points.iter().flat_map(|point| [point.x, point.y]).collect()
}
Some(EntityGeom::TextLine(line))
| Some(EntityGeom::Line(line))
| Some(EntityGeom::Ray(line))
| Some(EntityGeom::XLine(line)) => vec![line.p1.x, line.p1.y, line.p2.x, line.p2.y],
Some(EntityGeom::Circle(circle)) => vec![circle.center.x, circle.center.y, circle.rad],
Some(EntityGeom::Arc(arc)) => vec![
arc.circle.center.x,
arc.circle.center.y,
arc.circle.rad,
arc.start_angle,
arc.end_angle,
],
Some(EntityGeom::Ellipse(ellipse)) => {
vec![ellipse.center.x, ellipse.center.y, ellipse.radmin]
}
Some(EntityGeom::Spline { curve, .. }) => {
curve.poles.iter().flat_map(|point| [point.x, point.y]).collect()
}
None => Vec::new(),
}
}
/// Builds the kernel equations for one constraint. Unsupported references or
/// invalid dimensional targets produce no equations.
fn build_constraint(
@@ -1037,6 +1116,7 @@ fn build_constraint(
c: &ParametricConstraint,
tangent_side: Option<bool>,
tangent_point: Option<ParametricRef>,
originals: &HashMap<Handle, std::sync::Arc<EntityType>>,
) -> Vec<Rc<dyn Constraint>> {
if !c.enabled || !refs_share_supported_plane(document, &c.refs) {
return Vec::new();
@@ -1380,175 +1460,36 @@ fn build_constraint(
let Some(r) = c.refs.first() else {
return Vec::new();
};
if r.segment_index().is_some() {
let Some(line) = whole_line(sys, cache, *r) else {
return Vec::new();
};
let (x1, y1, x2, y2) = {
let store = sys.store();
(
store.get(line.p1.x),
store.get(line.p1.y),
store.get(line.p2.x),
store.get(line.p2.y),
)
};
return vec![
Rc::new(Equal::new(line.p1.x, sys.add_param(x1, true), 1.0)),
Rc::new(Equal::new(line.p1.y, sys.add_param(y1, true), 1.0)),
Rc::new(Equal::new(line.p2.x, sys.add_param(x2, true), 1.0)),
Rc::new(Equal::new(line.p2.y, sys.add_param(y2, true), 1.0)),
];
}
if r.marker.is_some() {
let Some(point) = point_ref(sys, cache, *r) else {
return Vec::new();
};
let (x, y) = {
let store = sys.store();
(store.get(point.x), store.get(point.y))
};
return vec![
Rc::new(Equal::new(point.x, sys.add_param(x, true), 1.0)),
Rc::new(Equal::new(point.y, sys.add_param(y, true), 1.0)),
];
}
let Some(geom) = resolve_ref(document, sys, cache, *r) else {
return Vec::new();
};
match geom {
EntityGeom::Point(point) => {
let (x, y) = {
let pinned = fixed_pin_params(document, sys, cache, *r);
// A persistent Fixed holds the *pre-edit* geometry: after MOVE /
// STRETCH / a grip drag the entity in `document` is already
// displaced, so the target comes from the caller's retained
// original when there is one — the entity's own live values
// otherwise (a freshly applied Fixed pins where it stands).
let mut targets: Option<Vec<f64>> = None;
if let Some(original) = originals.get(&r.entity) {
let mut original_cache = HashMap::new();
if let Some(geometry) = register_entity_geom(sys, r.entity, original.as_ref()) {
original_cache.insert(r.entity, geometry);
let params = fixed_pin_params(document, sys, &mut original_cache, *r);
if params.len() == pinned.len() {
let store = sys.store();
(store.get(point.x), store.get(point.y))
};
vec![
Rc::new(Equal::new(point.x, sys.add_param(x, true), 1.0)),
Rc::new(Equal::new(point.y, sys.add_param(y, true), 1.0)),
]
}
EntityGeom::Polyline { points, .. } => {
let values: Vec<_> = points
.iter()
.map(|point| {
let store = sys.store();
(point, store.get(point.x), store.get(point.y))
})
.collect();
values
.into_iter()
.flat_map(|(point, x, y)| {
[
Rc::new(Equal::new(point.x, sys.add_param(x, true), 1.0))
as Rc<dyn Constraint>,
Rc::new(Equal::new(point.y, sys.add_param(y, true), 1.0))
as Rc<dyn Constraint>,
]
})
.collect()
}
EntityGeom::TextLine(l)
| EntityGeom::Line(l)
| EntityGeom::Ray(l)
| EntityGeom::XLine(l) => {
let (x1, y1, x2, y2) = {
let store = sys.store();
(
store.get(l.p1.x),
store.get(l.p1.y),
store.get(l.p2.x),
store.get(l.p2.y),
)
};
vec![
Rc::new(Equal::new(l.p1.x, sys.add_param(x1, true), 1.0)),
Rc::new(Equal::new(l.p1.y, sys.add_param(y1, true), 1.0)),
Rc::new(Equal::new(l.p2.x, sys.add_param(x2, true), 1.0)),
Rc::new(Equal::new(l.p2.y, sys.add_param(y2, true), 1.0)),
]
}
EntityGeom::Circle(circ) => {
let (cx, cy, r) = {
let store = sys.store();
(
store.get(circ.center.x),
store.get(circ.center.y),
store.get(circ.rad),
)
};
vec![
Rc::new(Equal::new(circ.center.x, sys.add_param(cx, true), 1.0)),
Rc::new(Equal::new(circ.center.y, sys.add_param(cy, true), 1.0)),
Rc::new(Equal::new(circ.rad, sys.add_param(r, true), 1.0)),
]
}
// Pinning the 5 intrinsic params (center, radius, both
// angles) is enough — `start`/`end` are already tied to
// those via the arc-rules `CurveValue` constraints
// `solve_scope` adds for every arc, so pinning them too
// would just be redundant.
EntityGeom::Arc(a) => {
let (cx, cy, r, sa, ea) = {
let store = sys.store();
(
store.get(a.circle.center.x),
store.get(a.circle.center.y),
store.get(a.circle.rad),
store.get(a.start_angle),
store.get(a.end_angle),
)
};
vec![
Rc::new(Equal::new(a.circle.center.x, sys.add_param(cx, true), 1.0)),
Rc::new(Equal::new(a.circle.center.y, sys.add_param(cy, true), 1.0)),
Rc::new(Equal::new(a.circle.rad, sys.add_param(r, true), 1.0)),
Rc::new(Equal::new(a.start_angle, sys.add_param(sa, true), 1.0)),
Rc::new(Equal::new(a.end_angle, sys.add_param(ea, true), 1.0)),
]
}
// Pinning center + radmin is enough — `focus1` is already
// tied to `center` by the ellipse-rules `Difference`
// constraints `solve_scope` adds for every registered
// ellipse (see this module's doc comment), so with `center`
// pinned here too, pinning `focus1` again would just be
// redundant (same reasoning as Arc's `Fixed` arm above).
EntityGeom::Ellipse(el) => {
let (cx, cy, b) = {
let store = sys.store();
(
store.get(el.center.x),
store.get(el.center.y),
store.get(el.radmin),
)
};
vec![
Rc::new(Equal::new(el.center.x, sys.add_param(cx, true), 1.0)),
Rc::new(Equal::new(el.center.y, sys.add_param(cy, true), 1.0)),
Rc::new(Equal::new(el.radmin, sys.add_param(b, true), 1.0)),
]
}
EntityGeom::Spline { curve, .. } => {
let values: Vec<_> = curve
.poles
.iter()
.map(|point| {
let store = sys.store();
(point, store.get(point.x), store.get(point.y))
})
.collect();
values
.into_iter()
.flat_map(|(point, x, y)| {
[
Rc::new(Equal::new(point.x, sys.add_param(x, true), 1.0))
as Rc<dyn Constraint>,
Rc::new(Equal::new(point.y, sys.add_param(y, true), 1.0))
as Rc<dyn Constraint>,
]
})
.collect()
targets = Some(params.iter().map(|param| store.get(*param)).collect());
}
}
}
let targets = targets.unwrap_or_else(|| {
let store = sys.store();
pinned.iter().map(|param| store.get(*param)).collect()
});
pinned
.into_iter()
.zip(targets)
.map(|(param, value)| {
Rc::new(Equal::new(param, sys.add_param(value, true), 1.0))
as Rc<dyn Constraint>
})
.collect()
}
ConstraintKind::Distance => {
let [a, b] = c.refs.as_slice() else {
@@ -2252,6 +2193,7 @@ fn solve_scope(
retain_lengths: bool,
retained_before: &HashMap<Handle, std::sync::Arc<EntityType>>,
initial_fixed_refs: &[ParametricRef],
originals: &HashMap<Handle, std::sync::Arc<EntityType>>,
) -> Option<SolveResult> {
let params = if set.local_parameters.is_empty() {
drawing_params
@@ -2261,6 +2203,53 @@ fn solve_scope(
let mut sys = System::new();
let mut cache: HashMap<Handle, EntityGeom> = HashMap::new();
let constraints = constraints_in_drawing_order(document, set);
// A persistent Fixed outranks any edit: a point it holds is never a
// driven grip / STRETCH anchor, and a moved entity it holds gets no
// temporary whole-entity pin — the Fixed one, targeting the retained
// original, pulls it back instead (a MOVE leaves a fixed object, or the
// fixed points of a partly fixed one, where they were).
let fixed_refs: Vec<ParametricRef> = constraints
.iter()
.filter(|c| c.enabled && c.kind == ConstraintKind::Fixed)
.filter_map(|c| c.refs.first().copied())
.collect();
let held_by_fixed = |r: &ParametricRef| {
fixed_refs.iter().any(|f| {
f.entity == r.entity
&& (f.marker.is_none()
|| f.marker == r.marker
|| f.segment_index().is_some_and(|index| {
matches!(r.marker, Some(marker)
if marker == index as i32 || marker == index as i32 + 1)
}))
})
};
let mut driven_refs: Vec<ParametricRef> =
driven_refs.iter().copied().filter(|r| !held_by_fixed(r)).collect();
// A moved entity a Fixed holds only in part keeps its other primary
// points where the edit put them (driven), so the fixed ones pull just
// their own coordinates back — a MOVE on a line with one fixed end
// stretches the line instead of sliding it.
for reference in initial_fixed_refs {
if reference.marker.is_some() || !fixed_refs.iter().any(|f| f.entity == reference.entity)
{
continue;
}
let Some(entity) = document.get_entity(reference.entity) else {
continue;
};
for (marker, _) in super::parametric_constraints::parametric_point_candidates(entity) {
let point = ParametricRef::point(reference.entity, marker);
if (marker >= 0 || marker == -3)
&& !held_by_fixed(&point)
&& !driven_refs.contains(&point)
{
driven_refs.push(point);
}
}
}
let driven_refs = driven_refs.as_slice();
let fixed_touches = |handle: Handle| fixed_refs.iter().any(|f| f.entity == handle);
let line_axes = if retain_lengths { HashMap::new() } else { constrained_line_axes(&constraints) };
// Map partition-local kernel rows back to persistent constraint IDs by Rc identity.
let mut owner: Vec<(Rc<dyn Constraint>, ConstraintId)> = Vec::new();
@@ -2302,6 +2291,7 @@ fn solve_scope(
c,
tangent_side,
tangent_point,
originals,
) {
owner.push((constraint.clone(), c.id));
sys.add_constraint(constraint);
@@ -2313,6 +2303,9 @@ fn solve_scope(
// solve only; they are deliberately omitted from `owner` and from the
// persistent constraint set/native graph.
for reference in initial_fixed_refs {
if fixed_touches(reference.entity) {
continue;
}
let temporary = ParametricConstraint {
id: ConstraintId::MAX,
kind: ConstraintKind::Fixed,
@@ -2334,6 +2327,7 @@ fn solve_scope(
&temporary,
None,
None,
&HashMap::new(),
) {
sys.add_constraint(constraint);
}
@@ -2475,9 +2469,13 @@ fn solve_scope(
// endpoints. Other edits can retain line lengths as well.
if retain_size {
for (handle, geom) in &cache {
if initial_fixed_refs
.iter()
.any(|reference| reference.entity == *handle && reference.marker.is_none())
// A Fixed entity's size is whatever its pins say: whole → all
// pinned anyway, partial → the reference lets it resize (MOVE
// on a one-end-fixed line shortens it).
if fixed_touches(*handle)
|| initial_fixed_refs
.iter()
.any(|reference| reference.entity == *handle && reference.marker.is_none())
{
continue;
}
@@ -3346,6 +3344,7 @@ impl Scene {
&candidate,
None,
None,
&HashMap::new(),
)
.is_empty()
{
@@ -3443,22 +3442,25 @@ impl Scene {
}
}
// Pre-edit images of everything this transaction touched: what a
// persistent Fixed pins to, and (under CONSTRAINTSOLVEMODE) what
// sizes are retained from.
let mut originals: HashMap<Handle, std::sync::Arc<EntityType>> = self
.undo_recording
.as_ref()
.into_iter()
.flat_map(|recording| recording.before.iter())
.filter_map(|(handle, entity)| {
entity
.as_ref()
.map(|entity| (*handle, std::sync::Arc::clone(entity)))
})
.collect();
for (handle, entity) in retained_originals {
originals.insert(*handle, std::sync::Arc::new(entity.clone()));
}
let retained_before: HashMap<Handle, std::sync::Arc<EntityType>> = if retain_size {
let mut retained: HashMap<Handle, std::sync::Arc<EntityType>> = self
.undo_recording
.as_ref()
.into_iter()
.flat_map(|recording| recording.before.iter())
.filter_map(|(handle, entity)| {
entity
.as_ref()
.map(|entity| (*handle, std::sync::Arc::clone(entity)))
})
.collect();
for (handle, entity) in retained_originals {
retained.insert(*handle, std::sync::Arc::new(entity.clone()));
}
retained
originals.clone()
} else {
HashMap::new()
};
@@ -3482,6 +3484,7 @@ impl Scene {
true,
&retained_before,
initial_fixed_refs,
&originals,
) else {
continue;
};
@@ -3543,6 +3546,7 @@ impl Scene {
false,
&retained_before,
&[],
&retained_before,
)
else {
continue;