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 <noreply@anthropic.com>
This commit is contained in:
Felix RiestraandClaude Sonnet 5 committed 2026-09-09 19:38:31 +02:00
1 parent cf33a1d019
commit 31b5ef4ec3
18 files changed
+1076 -106

No files matched your search

+56
View File
@@ -2375,6 +2375,62 @@ impl OpenCADStudio {
}
}
}
CmdResult::AddPointOnEntityConstraint { point, target, kind, label } => {
let scope = self.tabs[i].current_sketch_scope();
let to_world = |p: glam::DVec3| acadrust::types::Vector3::new(p.x, p.y, p.z);
let resolved = crate::scene::sketch_constraints::nearest_sketch_point(&self.tabs[i].scene.document, scope, to_world(point), Some(target));
match resolved {
Some(point_ref) => {
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
// `CenterPoint` addresses its whole-entity side via
// the center marker (same solve as Coincident /
// Concentric — `ConstraintKind::CenterPoint`'s own
// doc comment); `Midpoint`/`PointOnCurve` address it
// as a whole entity.
let target_ref =
if kind == ConstraintKind::CenterPoint { SketchRef::center(target) } else { SketchRef::whole(target) };
return self.apply_cmd_result(CmdResult::AddSketchConstraint {
kind,
refs: vec![point_ref, target_ref],
driving_param: None,
label,
});
}
None => {
self.command_line.push_error(
"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;
}
}
}
CmdResult::AddEqualDistanceConstraint { points, label } => {
let scope = self.tabs[i].current_sketch_scope();
let to_world = |p: glam::DVec3| acadrust::types::Vector3::new(p.x, p.y, p.z);
let mut refs = Vec::with_capacity(4);
for p in points {
let Some(r) = crate::scene::sketch_constraints::nearest_sketch_point(&self.tabs[i].scene.document, scope, to_world(p), None)
else {
refs.clear();
break;
};
refs.push(r);
}
if refs.len() == 4 {
return self.apply_cmd_result(CmdResult::AddSketchConstraint {
kind: crate::scene::sketch_constraints::ConstraintKind::EqualDistance,
refs,
driving_param: None,
label,
});
}
self.command_line.push_error(
"Equal Distance: a 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;
}
CmdResult::ReassociateCenterMark { target, source, point } => {
if self.reject_locked_edit(i, target) {
return Task::none();