forked from 1b-agent/opencadstudio-upstream
Add 8 geometric constraint kinds and an opt-in DWG-native persistence toggle
New ConstraintKind variants — Concentric, CenterPoint, Colinear, Midpoint, Fixed, PointOnCurve, EqualDistance, Symmetric — each built entirely from existing ocs_gcs (FreeCAD planegcs) primitives, no new solver math needed. build_constraint now returns Vec<Rc<dyn Constraint>> instead of Option<Rc<...>>, generalizing the old Coincident-only "needs a second system-level constraint" special case to every kind that needs one. Wired into the ribbon (7 new buttons), a new point+pre-selected-entity command for CenterPoint/Midpoint/PointOnCurve, a new 4-point command for EqualDistance, and the existing DWG-native class-name mapping (all 8 reuse the same generic "plain Geometrical node" path already built for Coincident/Horizontal/etc.). Deliberately not modeled: Normal and EqualCurvature, which for this app's Line/Circle-only entity model would just be Perpendicular and Equal wearing different DWG class names. Also makes DWG-native constraint persistence opt-in (Options > General, off by default) rather than unconditional: a scope that doesn't yet have the native graph is only given one when the setting is on, but a scope that already has one — from an earlier save, or a file that came from real AutoCAD — keeps getting it resynced regardless of the setting, so toggling it off never freezes a stale graph in place. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
cf33a1d019
commit
31b5ef4ec3
18 files changed
+1076
-106
No files matched your search
@@ -354,3 +354,212 @@ fn two_conflicting_distance_targets_are_reported_as_conflicting() {
|
||||
assert!(flagged_id == first || flagged_id == second, "the flagged id should be one of the two conflicting Distance constraints");
|
||||
assert_eq!(kind, RedundancyKind::Conflicting);
|
||||
}
|
||||
|
||||
fn set_circle_center(scene: &mut Scene, handle: Handle, center: Vector3) {
|
||||
if let Some(EntityType::Circle(c)) = scene.document.get_entity_mut(handle) {
|
||||
c.center = center;
|
||||
}
|
||||
}
|
||||
|
||||
fn set_line_start(scene: &mut Scene, handle: Handle, start: Vector3) {
|
||||
if let Some(EntityType::Line(l)) = scene.document.get_entity_mut(handle) {
|
||||
l.start = start;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concentric_constraint_pulls_the_second_circles_center_onto_the_firsts() {
|
||||
let mut scene = Scene::new();
|
||||
let a = add_circle(&mut scene, 0.0, 0.0, 3.0);
|
||||
let b = add_circle(&mut scene, 5.0, 5.0, 1.0);
|
||||
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::Concentric,
|
||||
vec![SketchRef::center(a), SketchRef::center(b)],
|
||||
None,
|
||||
);
|
||||
|
||||
set_circle_center(&mut scene, a, Vector3::new(2.0, -3.0, 0.0));
|
||||
scene.bump_entities(&[(a, ChangeKind::Modified)]);
|
||||
|
||||
let (ca, ra) = circle_geom(&scene, a);
|
||||
let (cb, rb) = circle_geom(&scene, b);
|
||||
assert!((ca.x - cb.x).abs() < 1e-6 && (ca.y - cb.y).abs() < 1e-6, "centers should coincide: a={ca:?} b={cb:?}");
|
||||
// Radii are untouched by Concentric — only centers move.
|
||||
assert!((ra - 3.0).abs() < 1e-6 && (rb - 1.0).abs() < 1e-6, "radii must not change: ra={ra} rb={rb}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn center_point_constraint_pulls_a_lines_endpoint_onto_a_circles_center() {
|
||||
let mut scene = Scene::new();
|
||||
let circle = add_circle(&mut scene, 0.0, 0.0, 4.0);
|
||||
let line = add_line(&mut scene, 10.0, 10.0, 20.0, 20.0);
|
||||
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::CenterPoint,
|
||||
vec![SketchRef::point(line, 0), SketchRef::center(circle)],
|
||||
None,
|
||||
);
|
||||
|
||||
set_circle_center(&mut scene, circle, Vector3::new(-6.0, 9.0, 0.0));
|
||||
scene.bump_entities(&[(circle, ChangeKind::Modified)]);
|
||||
|
||||
let (center, _) = circle_geom(&scene, circle);
|
||||
let (line_start, _) = line_endpoints(&scene, line);
|
||||
assert!(
|
||||
(center.x - line_start.x).abs() < 1e-6 && (center.y - line_start.y).abs() < 1e-6,
|
||||
"the line's start should sit exactly at the circle's center: center={center:?} start={line_start:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colinear_constraint_pulls_the_second_line_onto_the_firsts_infinite_line() {
|
||||
let mut scene = Scene::new();
|
||||
let a = add_line(&mut scene, 0.0, 0.0, 10.0, 0.0);
|
||||
let b = add_line(&mut scene, 3.0, 4.0, 7.0, 6.0); // off-axis, not on a's line
|
||||
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::Colinear,
|
||||
vec![SketchRef::whole(a), SketchRef::whole(b)],
|
||||
None,
|
||||
);
|
||||
|
||||
scene.bump_entities(&[(b, ChangeKind::Modified)]);
|
||||
|
||||
let (a1, a2) = line_endpoints(&scene, a);
|
||||
let (b1, b2) = line_endpoints(&scene, b);
|
||||
// Both of b's endpoints should land on a's infinite line: the signed
|
||||
// area of (a2-a1) × (bN-a1) is ~0 for a point exactly on that line.
|
||||
let area = |p: Vector3| (a2.x - a1.x) * (p.y - a1.y) - (a2.y - a1.y) * (p.x - a1.x);
|
||||
assert!(area(b1).abs() < 1e-5, "b.start should land on a's line, area={}", area(b1));
|
||||
assert!(area(b2).abs() < 1e-5, "b.end should land on a's line, area={}", area(b2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn midpoint_constraint_pulls_a_point_onto_a_lines_midpoint() {
|
||||
let mut scene = Scene::new();
|
||||
let base = add_line(&mut scene, 0.0, 0.0, 10.0, 0.0);
|
||||
let marker = add_line(&mut scene, 20.0, 20.0, 21.0, 21.0); // marker.start is the tracked point
|
||||
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::Midpoint,
|
||||
vec![SketchRef::point(marker, 0), SketchRef::whole(base)],
|
||||
None,
|
||||
);
|
||||
|
||||
set_line_end(&mut scene, base, Vector3::new(10.0, 8.0, 0.0));
|
||||
scene.bump_entities(&[(base, ChangeKind::Modified)]);
|
||||
|
||||
let (b1, b2) = line_endpoints(&scene, base);
|
||||
let (marker_start, _) = line_endpoints(&scene, marker);
|
||||
let expected = Vector3::new((b1.x + b2.x) / 2.0, (b1.y + b2.y) / 2.0, 0.0);
|
||||
assert!(
|
||||
(marker_start.x - expected.x).abs() < 1e-6 && (marker_start.y - expected.y).abs() < 1e-6,
|
||||
"marker point should sit at base's midpoint: expected={expected:?} got={marker_start:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_constraint_holds_an_entity_in_place_despite_a_connected_edit() {
|
||||
let mut scene = Scene::new();
|
||||
let fixed_line = add_line(&mut scene, 0.0, 0.0, 10.0, 0.0);
|
||||
let moving_line = add_line(&mut scene, 10.0, 0.0, 10.0, 10.0);
|
||||
|
||||
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
|
||||
set.add(ConstraintKind::Fixed, vec![SketchRef::whole(fixed_line)], None);
|
||||
set.add(ConstraintKind::Coincident, vec![SketchRef::point(fixed_line, 1), SketchRef::point(moving_line, 0)], None);
|
||||
|
||||
let (before_start, before_end) = line_endpoints(&scene, fixed_line);
|
||||
|
||||
// Drag the shared point — without Fixed this would pull fixed_line's
|
||||
// end along with it; Fixed should hold fixed_line exactly in place and
|
||||
// let moving_line's start follow back to it instead.
|
||||
set_line_start(&mut scene, moving_line, Vector3::new(15.0, 5.0, 0.0));
|
||||
scene.bump_entities(&[(moving_line, ChangeKind::Modified)]);
|
||||
|
||||
let (after_start, after_end) = line_endpoints(&scene, fixed_line);
|
||||
assert_eq!(before_start, after_start, "Fixed entity's start must not move");
|
||||
assert_eq!(before_end, after_end, "Fixed entity's end must not move");
|
||||
let (moving_start, _) = line_endpoints(&scene, moving_line);
|
||||
assert!(
|
||||
(moving_start.x - before_end.x).abs() < 1e-6 && (moving_start.y - before_end.y).abs() < 1e-6,
|
||||
"moving_line's start should have been pulled back to fixed_line's (unmoved) end"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_on_curve_constraint_pulls_a_point_onto_a_circles_circumference() {
|
||||
let mut scene = Scene::new();
|
||||
let circle = add_circle(&mut scene, 0.0, 0.0, 5.0);
|
||||
let marker = add_line(&mut scene, 100.0, 100.0, 101.0, 101.0); // marker.start is the tracked point
|
||||
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::PointOnCurve,
|
||||
vec![SketchRef::point(marker, 0), SketchRef::whole(circle)],
|
||||
None,
|
||||
);
|
||||
|
||||
scene.bump_entities(&[(marker, ChangeKind::Modified)]);
|
||||
|
||||
let (center, radius) = circle_geom(&scene, circle);
|
||||
let (marker_start, _) = line_endpoints(&scene, marker);
|
||||
let dist = ((marker_start.x - center.x).powi(2) + (marker_start.y - center.y).powi(2)).sqrt();
|
||||
assert!((dist - radius).abs() < 1e-5, "point should land on the circumference: dist={dist} radius={radius}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_distance_constraint_matches_a_second_point_pairs_separation() {
|
||||
let mut scene = Scene::new();
|
||||
// Reference pair: fixed 6 units apart.
|
||||
let a = add_line(&mut scene, 0.0, 0.0, 6.0, 0.0);
|
||||
// Tracked pair: starts at some other separation, should be pulled to 6.
|
||||
let b = add_line(&mut scene, 20.0, 20.0, 25.0, 20.0);
|
||||
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::EqualDistance,
|
||||
vec![SketchRef::point(b, 0), SketchRef::point(b, 1), SketchRef::point(a, 0), SketchRef::point(a, 1)],
|
||||
None,
|
||||
);
|
||||
|
||||
scene.bump_entities(&[(b, ChangeKind::Modified)]);
|
||||
|
||||
let (a1, a2) = line_endpoints(&scene, a);
|
||||
let (b1, b2) = line_endpoints(&scene, b);
|
||||
let dist_a = ((a2.x - a1.x).powi(2) + (a2.y - a1.y).powi(2)).sqrt();
|
||||
let dist_b = ((b2.x - b1.x).powi(2) + (b2.y - b1.y).powi(2)).sqrt();
|
||||
assert!((dist_a - dist_b).abs() < 1e-6, "the two pairs' separations should match: dist_a={dist_a} dist_b={dist_b}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symmetric_constraint_mirrors_one_circles_center_across_the_axis_line() {
|
||||
let mut scene = Scene::new();
|
||||
let axis = add_line(&mut scene, 0.0, 0.0, 0.0, 10.0); // the Y axis
|
||||
let a = add_circle(&mut scene, 3.0, 4.0, 1.0);
|
||||
// Starts well off the mirrored position (and not coincident with `a` —
|
||||
// a zero-length a/b segment would leave `Perpendicular`'s precomputed
|
||||
// scale dividing by zero at the very first solve iteration).
|
||||
let b = add_circle(&mut scene, 8.0, 9.0, 1.0);
|
||||
|
||||
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
|
||||
// Pin the axis and `a` in place — otherwise they're just as free to move
|
||||
// as `b`'s center, and with only 2 equations (MidpointOnLine +
|
||||
// Perpendicular) against that many more unknowns the solver is free to
|
||||
// converge on any of infinitely many valid configurations, not
|
||||
// necessarily "only b moves, to a's exact mirror" (which is the one
|
||||
// deterministic outcome this test actually wants to check).
|
||||
set.add(ConstraintKind::Fixed, vec![SketchRef::whole(axis)], None);
|
||||
set.add(ConstraintKind::Fixed, vec![SketchRef::whole(a)], None);
|
||||
set.add(ConstraintKind::Symmetric, vec![SketchRef::center(a), SketchRef::center(b), SketchRef::whole(axis)], None);
|
||||
|
||||
scene.bump_entities(&[(b, ChangeKind::Modified)]);
|
||||
|
||||
let (ca, _) = circle_geom(&scene, a);
|
||||
let (cb, _) = circle_geom(&scene, b);
|
||||
assert_eq!(ca, Vector3::new(3.0, 4.0, 0.0), "Fixed a's center must not have moved");
|
||||
// With both a and the axis pinned, b's center has a unique valid
|
||||
// position left: a's exact mirror across the Y axis, (-3, 4).
|
||||
assert!(
|
||||
(cb.x - -3.0).abs() < 1e-5 && (cb.y - 4.0).abs() < 1e-5,
|
||||
"b's center should have been pulled to a's mirror image across the axis: expected (-3, 4), got {cb:?}"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user