From e572625de3af271a5e6c3ad7093bca6f10e2bbab Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 18 Sep 2026 18:58:35 +0300 Subject: [PATCH 1/3] Refuse an edit the constraints can only satisfy by collapsing the entity 9) When an edit contradicts a fixed point together with another constraint so that the only solution collapses the entity (a rotated line with a fixed start and a Vertical relation), the entity returns to its pre-edit shape instead of degenerating, as the reference does. Co-Authored-By: Claude Fable 5.1 --- src/scene/parametric_solve.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/scene/parametric_solve.rs b/src/scene/parametric_solve.rs index 39dd09618..4d52fe8b4 100644 --- a/src/scene/parametric_solve.rs +++ b/src/scene/parametric_solve.rs @@ -1038,6 +1038,18 @@ fn retained_tangent_side( Some(dx * (center[1] - y1) - dy * (center[0] - x1) >= 0.0) } +/// A solve result with no extent left — the only way the solver could +/// honour every constraint after an edit that contradicted them. +fn collapsed_by_solve(entity: &EntityType) -> bool { + const EPS: f64 = 1.0e-9; + match entity { + EntityType::Line(line) => (line.end - line.start).length() <= EPS, + EntityType::Circle(circle) => circle.radius <= EPS, + EntityType::Arc(arc) => arc.radius <= EPS, + _ => false, + } +} + /// 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 @@ -3613,6 +3625,19 @@ impl Scene { self.parametric_constraints[i].conflicts.clear(); } for (handle, new_entity) in solved { + // An edit the constraints can only satisfy by collapsing the + // entity (a rotated line whose start is fixed and direction + // constrained) is refused: the entity goes back to its + // pre-edit shape, as the reference does. + let new_entity = match originals.get(&handle) { + Some(original) + if collapsed_by_solve(&new_entity) + && !collapsed_by_solve(original.as_ref()) => + { + original.as_ref().clone() + } + _ => new_entity, + }; if let Some(before) = self.document.get_entity_arc(handle) { self.record_undo_before(handle, Some(before)); } From 5ac6dc279bd58a0cdb221acb8d4c9bc46386bd1a Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 18 Sep 2026 19:05:37 +0300 Subject: [PATCH 2/3] Keep only the end point of a transformed axis-constrained entity pinned 10) A transformed entity with a Horizontal or Vertical relation keeps only its end point where the transform put it; the rest re-aligns to the axis at the retained length, so a rotated vertical line stays vertical below its moved end instead of the whole-entity pin contradicting the axis and leaving the constraint unmet. Co-Authored-By: Claude Fable 5.1 --- src/scene/parametric_solve.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/scene/parametric_solve.rs b/src/scene/parametric_solve.rs index 4d52fe8b4..8d1ccddf8 100644 --- a/src/scene/parametric_solve.rs +++ b/src/scene/parametric_solve.rs @@ -2410,10 +2410,34 @@ fn solve_scope( if fixed_touches(reference.entity) { continue; } + // A transformed entity with a Horizontal/Vertical relation keeps + // only its end point where the transform put it; the rest re-aligns + // to the axis at the retained length (a rotated vertical line stays + // vertical below its moved end, as in the reference) instead of the + // whole-entity pin contradicting the axis and failing the solve. + let axis_pin = reference.marker.is_none().then(|| { + constraints.iter().find_map(|c| { + (c.enabled && matches!(c.kind, ConstraintKind::Horizontal | ConstraintKind::Vertical)) + .then(|| c.refs.iter().find(|r| r.entity == reference.entity)) + .flatten() + .map(|r| match r.directional_axis() { + Some(super::parametric_constraints::DirectionalAxis::TextBaseline) => { + ParametricRef::point(r.entity, 0) + } + Some(_) => ParametricRef::center(r.entity), + None => ParametricRef::point( + r.entity, + r.segment_index().map_or(1, |index| index as i32 + 1), + ), + }) + }) + }) + .flatten(); + let pinned = axis_pin.unwrap_or(*reference); let temporary = ParametricConstraint { id: ConstraintId::MAX, kind: ConstraintKind::Fixed, - refs: vec![*reference], + refs: vec![pinned], driving_param: None, enabled: true, native_origin: None, From b45e8bce1f8571eae0b0bf9d5c0a36a5729bb984 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 18 Sep 2026 19:06:57 +0300 Subject: [PATCH 3/3] Keep the transformed length when an axis-constrained entity re-aligns The temporary end-point pin of a transformed Horizontal/Vertical entity is paired with the length the transform left it, so a scaled vertical line stays scaled while a rotated one stays vertical at its rotated length. Co-Authored-By: Claude Fable 5.1 --- src/scene/parametric_solve.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/scene/parametric_solve.rs b/src/scene/parametric_solve.rs index 8d1ccddf8..fa0095096 100644 --- a/src/scene/parametric_solve.rs +++ b/src/scene/parametric_solve.rs @@ -2434,6 +2434,39 @@ fn solve_scope( }) .flatten(); let pinned = axis_pin.unwrap_or(*reference); + // The re-aligned entity keeps the length the transform gave it (a + // scaled vertical line stays scaled), not its pre-edit length. + if axis_pin.is_some() { + let segment = reference + .segment_index() + .or_else(|| { + constraints.iter().find_map(|c| { + c.refs + .iter() + .find(|r| r.entity == reference.entity) + .and_then(|r| r.segment_index()) + }) + }); + let line = resolve_ref(document, &mut sys, &mut cache, *reference).and_then(|geometry| { + match (&geometry, segment) { + (EntityGeom::Polyline { .. }, Some(index)) => geometry.line_segment(index), + (EntityGeom::Line(line) | EntityGeom::Ray(line) | EntityGeom::XLine(line), _) => { + Some(*line) + } + _ => None, + } + }); + if let Some(line) = line { + let length = { + let store = sys.store(); + let dx = store.get(line.p2.x) - store.get(line.p1.x); + let dy = store.get(line.p2.y) - store.get(line.p1.y); + (dx * dx + dy * dy).sqrt() + }; + let target = sys.add_param(length, true); + sys.add_constraint(Rc::new(P2PDistance::new(line.p1, line.p2, target))); + } + } let temporary = ParametricConstraint { id: ConstraintId::MAX, kind: ConstraintKind::Fixed,