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:
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

+196 -9
View File
@@ -269,6 +269,18 @@ fn geometrical_class_name(kind: ConstraintKind) -> Option<&'static str> {
ConstraintKind::Vertical => Some("ACVERTICALCONSTRAINT"),
ConstraintKind::Perpendicular => Some("ACPERPENDICULARCONSTRAINT"),
ConstraintKind::Tangent => Some("ACTANGENTCONSTRAINT"),
ConstraintKind::Concentric => Some("ACCONCENTRICCONSTRAINT"),
ConstraintKind::CenterPoint => Some("ACCENTERPOINTCONSTRAINT"),
ConstraintKind::Colinear => Some("ACCOLINEARCONSTRAINT"),
ConstraintKind::Fixed => Some("ACFIXEDCONSTRAINT"),
ConstraintKind::Midpoint => Some("ACMIDPOINTCONSTRAINT"),
ConstraintKind::PointOnCurve => Some("ACPOINTCURVECONSTRAINT"),
ConstraintKind::Symmetric => Some("ACSYMMETRICCONSTRAINT"),
// Also a plain `Geometrical`-shaped node in AutoCAD's own class list
// (`is_plain_geometrical_constraint`) — unlike `Equal`, its class
// name never branches on entity type, so it belongs here rather
// than in the caller's per-entity-type dispatch.
ConstraintKind::EqualDistance => Some("ACEQUALDISTANCECONSTRAINT"),
// `Equal` isn't one native kind — the line/circle split below
// (design doc §5) needs the resolved entity types, not just the
// ConstraintKind, so it's handled by the caller instead of here.
@@ -664,6 +676,24 @@ fn remove_owned_recursive(document: &mut CadDocument, root: Handle) {
document.objects.remove(&root);
}
/// Whether `owner`'s scope already carries a materialized native graph —
/// from an earlier save with the setting on, or from a file that came from
/// real AutoCAD/BricsCAD. Drives the "sync-if-present, create-only-if-
/// enabled" rule in [`Scene::materialize_dwg_native_constraints_for_save`]:
/// a scope that already has this graph keeps getting it rebuilt on every
/// save regardless of the current setting, so turning the setting off never
/// leaves a stale, out-of-sync graph behind — only a scope that doesn't have
/// one yet is gated by the setting.
fn has_native_network(document: &CadDocument, owner: Handle) -> bool {
let Some(dictionary_handle) = document.extension_dictionary_handle(owner) else {
return false;
};
let Some(ObjectType::Dictionary(dictionary)) = document.objects.get(&dictionary_handle) else {
return false;
};
dictionary.get(NETWORK_DICTIONARY_KEY).is_some()
}
/// One scope's worth of
/// [`Scene::materialize_dwg_native_constraints_for_save`] — see that
/// method's doc comment for the save-time contract this implements.
@@ -784,14 +814,30 @@ impl Scene {
/// "materialize right before save, from scratch every time" convention
/// `sketch_persist.rs`'s `materialize_sketch_constraints_for_save` uses,
/// called alongside it, not instead of it (design doc §6: additive).
pub(crate) fn materialize_dwg_native_constraints_for_save(&mut self) {
ensure_associative_classes_registered(&mut self.document);
///
/// `enabled` is the app's `write_dwg_native_constraints` Options toggle
/// (off by default — this graph adds file weight every save, whether or
/// not AutoCAD/BricsCAD interop is wanted). It only gates *creating* the
/// graph in a scope that doesn't have one yet: a scope that already
/// carries one — from an earlier save with the setting on, or from a
/// file that came from real AutoCAD — keeps getting it rebuilt in sync
/// regardless of the current setting ([`has_native_network`]), so
/// toggling this off never leaves a stale graph sitting in the file.
pub(crate) fn materialize_dwg_native_constraints_for_save(&mut self, enabled: bool) {
let mut classes_registered = false;
for index in 0..self.sketch_constraints.len() {
let set = self.sketch_constraints[index].clone();
let owner = set.scope.owner_handle(&self.document);
if owner.is_null() {
continue;
}
if !enabled && !has_native_network(&self.document, owner) {
continue;
}
if !classes_registered {
ensure_associative_classes_registered(&mut self.document);
classes_registered = true;
}
materialize_scope(&mut self.document, owner, &set, &self.named_parameters);
}
}
@@ -801,7 +847,7 @@ impl Scene {
mod tests {
use super::super::sketch_constraints::SketchScope;
use super::*;
use acadrust::entities::Line;
use acadrust::entities::{Circle, Line};
fn line_entity(scene: &mut Scene, start: (f64, f64), end: (f64, f64)) -> Handle {
scene.add_entity(EntityType::Line(Line::from_points(
@@ -810,6 +856,10 @@ mod tests {
)))
}
fn circle_entity(scene: &mut Scene, center: (f64, f64), radius: f64) -> Handle {
scene.add_entity(EntityType::Circle(Circle::from_center_radius(Vector3::new(center.0, center.1, 0.0), radius)))
}
/// Digs a scope's materialized native graph back out of `document`:
/// `owner`'s extension dictionary -> `ACAD_ASSOCNETWORK` entry ->
/// its one owned action, which (for this module) is always the
@@ -854,7 +904,7 @@ mod tests {
None,
);
scene.materialize_dwg_native_constraints_for_save();
scene.materialize_dwg_native_constraints_for_save(true);
let owner = scene.document.header.model_space_block_handle;
// Sanity before the round trip even happens: root + 2 geometry
// nodes + 2 constraint nodes.
@@ -915,7 +965,7 @@ mod tests {
#[test]
fn a_dwg_save_keeps_the_full_dependency_chain_including_the_named_variable() {
let (mut scene, owner) = distance_with_named_parameter_scene();
scene.materialize_dwg_native_constraints_for_save();
scene.materialize_dwg_native_constraints_for_save(true);
assert!(has_variable_named(&scene.document, "gap"), "expected an AssocVariable BEFORE serialization");
let bytes = crate::io::save_to_bytes(&scene.document, "dwg", scene.document.version)
@@ -951,7 +1001,7 @@ mod tests {
#[test]
fn a_dxf_save_keeps_only_the_constraint_group_shell() {
let (mut scene, owner) = distance_with_named_parameter_scene();
scene.materialize_dwg_native_constraints_for_save();
scene.materialize_dwg_native_constraints_for_save(true);
let bytes = crate::io::save_to_bytes(&scene.document, "dxf", scene.document.version)
.unwrap_or_else(|e| panic!("save to dxf: {e}"));
@@ -981,9 +1031,9 @@ mod tests {
None,
);
scene.materialize_dwg_native_constraints_for_save();
scene.materialize_dwg_native_constraints_for_save(true);
let first_count = scene.document.objects.len();
scene.materialize_dwg_native_constraints_for_save();
scene.materialize_dwg_native_constraints_for_save(true);
let second_count = scene.document.objects.len();
assert_eq!(first_count, second_count, "a resave with the same constraints must not accumulate new objects");
}
@@ -992,7 +1042,7 @@ mod tests {
fn an_empty_constraint_set_leaves_no_native_network() {
let mut scene = Scene::new();
let _ = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
scene.materialize_dwg_native_constraints_for_save();
scene.materialize_dwg_native_constraints_for_save(true);
let owner = scene.document.header.model_space_block_handle;
let dict = scene.document.extension_dictionary_handle(owner);
@@ -1005,4 +1055,141 @@ mod tests {
}
}
}
fn model_space_has_native_network(scene: &Scene) -> bool {
has_native_network(&scene.document, scene.document.header.model_space_block_handle)
}
#[test]
fn the_setting_off_skips_a_scope_with_no_existing_native_graph() {
let mut scene = Scene::new();
let a = line_entity(&mut scene, (0.0, 0.0), (10.0, 0.0));
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
ConstraintKind::Horizontal,
vec![SketchRef::whole(a)],
None,
);
scene.materialize_dwg_native_constraints_for_save(false);
assert!(
!model_space_has_native_network(&scene),
"a scope with no prior native graph should stay untouched while the setting is off"
);
}
/// The "sync-if-present, create-only-if-enabled" rule from
/// `Scene::materialize_dwg_native_constraints_for_save`'s doc comment:
/// once a scope has the native graph (here, from an earlier save with
/// the setting on), turning the setting off must not freeze that graph
/// out of date — it should keep tracking the current constraints.
#[test]
fn the_setting_off_still_resyncs_a_scope_that_already_has_a_native_graph() {
let mut scene = Scene::new();
let a = line_entity(&mut scene, (0.0, 0.0), (10.0, 0.0));
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
ConstraintKind::Horizontal,
vec![SketchRef::whole(a)],
None,
);
scene.materialize_dwg_native_constraints_for_save(true);
assert!(model_space_has_native_network(&scene), "sanity: the graph should exist after the first, enabled save");
let b = line_entity(&mut scene, (0.0, 0.0), (0.0, 10.0));
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
ConstraintKind::Vertical,
vec![SketchRef::whole(b)],
None,
);
scene.materialize_dwg_native_constraints_for_save(false);
let owner = scene.document.header.model_space_block_handle;
let group = native_group(&scene.document, owner);
assert!(
group.nodes.iter().any(|n| n.class_name == "ACVERTICALCONSTRAINT"),
"the second, disabled-setting save should still pick up the new Vertical constraint, got {:?}",
group.nodes.iter().map(|n| &n.class_name).collect::<Vec<_>>()
);
}
/// The other half of the same rule: disabling the setting after a scope
/// already has the graph must not make that graph disappear either —
/// only remove it by clearing the scope's constraints (existing
/// `an_empty_constraint_set_leaves_no_native_network` behavior), not by
/// toggling the setting off.
#[test]
fn the_setting_off_does_not_remove_an_existing_native_graph() {
let mut scene = Scene::new();
let a = line_entity(&mut scene, (0.0, 0.0), (10.0, 0.0));
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
ConstraintKind::Horizontal,
vec![SketchRef::whole(a)],
None,
);
scene.materialize_dwg_native_constraints_for_save(true);
scene.materialize_dwg_native_constraints_for_save(false);
assert!(
model_space_has_native_network(&scene),
"an existing native graph must survive a disabled-setting resave, not be silently dropped"
);
}
/// The 8 new `ConstraintKind`s all reuse the same generic
/// `Geometrical`-shaped node path `geometrical_class_name` drives (see
/// its own doc comment) — one representative mix of ref shapes (2 whole
/// entities, an asymmetric point+whole pair, and a 3-ref case) is
/// enough to confirm that path handles them all, without one dedicated
/// test per kind.
#[test]
fn new_constraint_kinds_round_trip_with_their_own_dwg_native_class_names() {
for ext in ["dxf", "dwg"] {
let mut scene = Scene::new();
let circle_a = circle_entity(&mut scene, (0.0, 0.0), 3.0);
let circle_b = circle_entity(&mut scene, (5.0, 5.0), 1.0);
let line_a = line_entity(&mut scene, (0.0, 0.0), (10.0, 0.0));
let line_b = line_entity(&mut scene, (3.0, 4.0), (7.0, 6.0));
let marker = line_entity(&mut scene, (20.0, 20.0), (21.0, 21.0));
let axis = line_entity(&mut scene, (0.0, 0.0), (0.0, 10.0));
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
set.add(ConstraintKind::Concentric, vec![SketchRef::center(circle_a), SketchRef::center(circle_b)], None);
set.add(ConstraintKind::Colinear, vec![SketchRef::whole(line_a), SketchRef::whole(line_b)], None);
set.add(ConstraintKind::Fixed, vec![SketchRef::whole(line_a)], None);
set.add(ConstraintKind::CenterPoint, vec![SketchRef::point(marker, 0), SketchRef::center(circle_a)], None);
set.add(ConstraintKind::Midpoint, vec![SketchRef::point(marker, 1), SketchRef::whole(line_a)], None);
set.add(ConstraintKind::PointOnCurve, vec![SketchRef::point(marker, 0), SketchRef::whole(circle_a)], None);
set.add(
ConstraintKind::EqualDistance,
vec![SketchRef::point(line_b, 0), SketchRef::point(line_b, 1), SketchRef::point(line_a, 0), SketchRef::point(line_a, 1)],
None,
);
set.add(
ConstraintKind::Symmetric,
vec![SketchRef::center(circle_a), SketchRef::center(circle_b), SketchRef::whole(axis)],
None,
);
scene.materialize_dwg_native_constraints_for_save(true);
let owner = scene.document.header.model_space_block_handle;
let bytes = crate::io::save_to_bytes(&scene.document, ext, scene.document.version)
.unwrap_or_else(|e| panic!("save to {ext}: {e}"));
let reloaded = crate::io::load_bytes(&format!("new_kinds.{ext}"), bytes)
.unwrap_or_else(|e| panic!("reload {ext}: {e}"));
let group = native_group(&reloaded, owner);
let class_names: Vec<&str> = group.nodes.iter().map(|n| n.class_name.as_str()).collect();
for expected in [
"ACCONCENTRICCONSTRAINT",
"ACCOLINEARCONSTRAINT",
"ACFIXEDCONSTRAINT",
"ACCENTERPOINTCONSTRAINT",
"ACMIDPOINTCONSTRAINT",
"ACPOINTCURVECONSTRAINT",
"ACEQUALDISTANCECONSTRAINT",
"ACSYMMETRICCONSTRAINT",
] {
assert!(class_names.contains(&expected), "{ext}: missing {expected} node, got {class_names:?}");
}
}
}
}
+43
View File
@@ -79,6 +79,41 @@ pub enum ConstraintKind {
Angle,
Radius,
Tangent,
/// Two circles/arcs share a center — `refs`: `[center(a), center(b)]`.
/// Solves identically to `Coincident` (`sketch_solve.rs` broadens that
/// match arm rather than duplicating it) — only the DWG-native class
/// name (`ACCONCENTRICCONSTRAINT` vs `ACPOINTCOINCIDENCECONSTRAINT`)
/// and the UI entry point differ.
Concentric,
/// A point sits at a circle/arc's center — `refs`: `[point, center(circle)]`.
/// Same solver math as `Coincident`/`Concentric`, different DWG class
/// name (`ACCENTERPOINTCONSTRAINT`).
CenterPoint,
/// Two lines share the same infinite line — `refs`: `[whole(a), whole(b)]`.
Colinear,
/// A point sits at another line's midpoint — `refs`: `[point, whole(line)]`.
Midpoint,
/// Locks a whole entity at its current position — `refs`: `[whole(entity)]`.
/// No `driving_param`: the target is the entity's own live geometry at
/// solve time, not a typed value (see `sketch_solve.rs`'s `Fixed` arm).
Fixed,
/// A point lies anywhere along a line's or circle's curve (not
/// restricted to an endpoint/center) — `refs`: `[point, whole(entity)]`.
PointOnCurve,
/// The distance between one point pair equals the distance between
/// another — `refs`: `[p1, p2, p3, p4]` (`dist(p1,p2) == dist(p3,p4)`).
/// AutoCAD's fourth `Equal` sub-kind (`ACEQUALDISTANCECONSTRAINT`) —
/// see the design doc for why `EqualCurvature`, the other missing
/// sub-kind, isn't modeled: for the only entity types this system
/// solves (Line, Circle), it would be mathematically identical to this
/// `Equal`'s existing circle/circle (radius) branch.
EqualDistance,
/// Two circles/arcs are mirror images of each other across a line —
/// `refs`: `[center(a), center(b), whole(mirror_line)]`. Scoped to the
/// circle-center-pair case (unambiguous with whole-entity selection);
/// point-symmetry about a point, and symmetry between two lines, aren't
/// modeled.
Symmetric,
}
pub type ConstraintId = u32;
@@ -345,6 +380,14 @@ impl ConstraintKind {
ConstraintKind::Angle => "∠",
ConstraintKind::Radius => "R",
ConstraintKind::Tangent => "T",
ConstraintKind::Concentric => "◎",
ConstraintKind::CenterPoint => "⊕",
ConstraintKind::Colinear => "L",
ConstraintKind::Midpoint => "M",
ConstraintKind::Fixed => "F",
ConstraintKind::PointOnCurve => "∈",
ConstraintKind::EqualDistance => "≐",
ConstraintKind::Symmetric => "S",
}
}
}
+161 -76
View File
@@ -29,9 +29,10 @@ use acadrust::entities::EntityType;
use acadrust::types::Handle;
use ocs_gcs::constraints::angle_distance::L2LAngle;
use ocs_gcs::constraints::circle_arc::{C2LDistance, TangentCircumf};
use ocs_gcs::constraints::circle_arc::{C2LDistance, P2CDistance, TangentCircumf};
use ocs_gcs::constraints::point_line::{
Equal, EqualLineLength, Parallel as ParallelConstraint, Perpendicular as PerpendicularConstraint, P2PDistance,
CenterOfGravity, Equal, EqualLineLength, MidpointOnLine, Parallel as ParallelConstraint,
Perpendicular as PerpendicularConstraint, PointOnLine, P2PDistance,
};
use ocs_gcs::constraints::Constraint;
use ocs_gcs::geo::{Circle as GCircle, Line as GLine, Point as GPoint};
@@ -111,14 +112,18 @@ fn resolve_ref(
Some(*cache.get(&r.entity)?)
}
/// One [`SketchConstraint`]'s `ocs_gcs` construction, or `None` if it can't
/// be built (an unsupported/not-yet-mapped kind, a dangling ref, a ref
/// shape the kind doesn't expect — e.g. `Parallel` needs two whole-line
/// refs — or, for a dimensional kind, a `driving_param` that fails to
/// resolve: an undefined named-parameter reference or a division by zero in
/// its formula, design doc `named_parameters_design.md` stage 3). A
/// constraint that can't be built is simply skipped for this solve, not an
/// error: geometry it would have constrained is left alone, matching how a
/// One [`SketchConstraint`]'s `ocs_gcs` construction: every system-level
/// constraint it contributes (most kinds contribute exactly one; a few —
/// `Coincident`/`Concentric`/`CenterPoint`'s X+Y halves, `Colinear`'s two
/// endpoints, `Midpoint`'s X+Y, `Symmetric`'s midpoint+perpendicular pair,
/// `Fixed`'s per-coordinate pins — contribute more). Empty means it can't be
/// built (an unsupported/not-yet-mapped kind, a dangling ref, a ref shape
/// the kind doesn't expect — e.g. `Parallel` needs two whole-line refs — or,
/// for a dimensional kind, a `driving_param` that fails to resolve: an
/// undefined named-parameter reference or a division by zero in its
/// formula, design doc `named_parameters_design.md` stage 3). A constraint
/// that can't be built is simply skipped for this solve, not an error:
/// geometry it would have constrained is left alone, matching how a
/// dangling associative-dimension reference degrades today rather than
/// aborting the whole recompute. (A named-parameter *cycle* should be
/// unreachable here — `ParameterTable::set` already refuses to create one —
@@ -130,9 +135,9 @@ fn build_constraint(
cache: &mut HashMap<Handle, EntityGeom>,
params: &ParameterTable,
c: &SketchConstraint,
) -> Option<Rc<dyn Constraint>> {
) -> Vec<Rc<dyn Constraint>> {
if !c.enabled {
return None;
return Vec::new();
}
let whole_line = |sys: &mut System, cache: &mut HashMap<_, _>, r: SketchRef| match resolve_ref(document, sys, cache, r)? {
@@ -148,64 +153,159 @@ fn build_constraint(
resolve_ref(document, sys, cache, r)?.point_for_marker(marker)
};
// `Coincident`/`Concentric`/`CenterPoint` all solve identically — two
// points (an endpoint, a circle's center via the existing `-3` marker,
// or a plain point) held equal on both axes. Only the DWG-native class
// name and the UI entry point that produces their `refs` differ.
let point_pair_equal = |sys: &mut System, cache: &mut HashMap<_, _>, refs: &[SketchRef]| -> Vec<Rc<dyn Constraint>> {
let [a, b] = refs else { return Vec::new() };
let (Some(pa), Some(pb)) = (point_ref(sys, cache, *a), point_ref(sys, cache, *b)) else { return Vec::new() };
vec![Rc::new(Equal::new(pa.x, pb.x, 1.0)), Rc::new(Equal::new(pa.y, pb.y, 1.0))]
};
match c.kind {
ConstraintKind::Coincident => {
let [a, b] = c.refs.as_slice() else { return None };
let (pa, pb) = (point_ref(sys, cache, *a)?, point_ref(sys, cache, *b)?);
// Two coordinate-equal constraints packaged as one via a small
// local combinator isn't worth it for a single call site — the
// caller (`solve_scope`) already `extend`s a `Vec` per
// constraint, so this returns just the X constraint and Y is
// added as a second, independent `SketchConstraint`-less
// push — see `solve_scope`'s handling of this arm specifically.
Some(Rc::new(Equal::new(pa.x, pb.x, 1.0)))
ConstraintKind::Coincident | ConstraintKind::Concentric | ConstraintKind::CenterPoint => {
point_pair_equal(sys, cache, &c.refs)
}
ConstraintKind::Horizontal => {
let l = whole_line(sys, cache, *c.refs.first()?)?;
Some(Rc::new(Equal::new(l.p1.y, l.p2.y, 1.0)))
let Some(r) = c.refs.first() else { return Vec::new() };
let Some(l) = whole_line(sys, cache, *r) else { return Vec::new() };
vec![Rc::new(Equal::new(l.p1.y, l.p2.y, 1.0))]
}
ConstraintKind::Vertical => {
let l = whole_line(sys, cache, *c.refs.first()?)?;
Some(Rc::new(Equal::new(l.p1.x, l.p2.x, 1.0)))
let Some(r) = c.refs.first() else { return Vec::new() };
let Some(l) = whole_line(sys, cache, *r) else { return Vec::new() };
vec![Rc::new(Equal::new(l.p1.x, l.p2.x, 1.0))]
}
ConstraintKind::Parallel => {
let [a, b] = c.refs.as_slice() else { return None };
let (fixed, moving) = (whole_line(sys, cache, *a)?, whole_line(sys, cache, *b)?);
Some(Rc::new(ParallelConstraint::new(sys.store(), moving, fixed)))
let [a, b] = c.refs.as_slice() else { return Vec::new() };
let (Some(fixed), Some(moving)) = (whole_line(sys, cache, *a), whole_line(sys, cache, *b)) else { return Vec::new() };
vec![Rc::new(ParallelConstraint::new(sys.store(), moving, fixed))]
}
ConstraintKind::Perpendicular => {
let [a, b] = c.refs.as_slice() else { return None };
let (fixed, moving) = (whole_line(sys, cache, *a)?, whole_line(sys, cache, *b)?);
Some(Rc::new(PerpendicularConstraint::new(sys.store(), moving, fixed)))
let [a, b] = c.refs.as_slice() else { return Vec::new() };
let (Some(fixed), Some(moving)) = (whole_line(sys, cache, *a), whole_line(sys, cache, *b)) else { return Vec::new() };
vec![Rc::new(PerpendicularConstraint::new(sys.store(), moving, fixed))]
}
ConstraintKind::Equal => {
let [a, b] = c.refs.as_slice() else { return None };
let [a, b] = c.refs.as_slice() else { return Vec::new() };
if let (Some(la), Some(lb)) = (whole_line(sys, cache, *a), whole_line(sys, cache, *b)) {
return Some(Rc::new(EqualLineLength::new(lb, la)));
return vec![Rc::new(EqualLineLength::new(lb, la))];
}
let (Some(ca), Some(cb)) = (whole_circle(sys, cache, *a), whole_circle(sys, cache, *b)) else { return Vec::new() };
vec![Rc::new(Equal::new(cb.rad, ca.rad, 1.0))]
}
ConstraintKind::EqualDistance => {
let [a, b, c2, d] = c.refs.as_slice() else { return Vec::new() };
let (Some(pa), Some(pb), Some(pc), Some(pd)) =
(point_ref(sys, cache, *a), point_ref(sys, cache, *b), point_ref(sys, cache, *c2), point_ref(sys, cache, *d))
else {
return Vec::new();
};
vec![Rc::new(EqualLineLength::new(GLine { p1: pc, p2: pd }, GLine { p1: pa, p2: pb }))]
}
ConstraintKind::Colinear => {
let [a, b] = c.refs.as_slice() else { return Vec::new() };
let (Some(onto), Some(moving)) = (whole_line(sys, cache, *a), whole_line(sys, cache, *b)) else { return Vec::new() };
// Pinning both of `moving`'s endpoints onto `onto`'s infinite
// line forces the two to coincide (as long as `moving`'s own
// two points stay distinct) — no dedicated "colinear" primitive
// needed, `PointOnLine` applied twice does it.
vec![Rc::new(PointOnLine::new(moving.p1, onto)), Rc::new(PointOnLine::new(moving.p2, onto))]
}
ConstraintKind::Midpoint => {
let [a, b] = c.refs.as_slice() else { return Vec::new() };
let (Some(p), Some(l)) = (point_ref(sys, cache, *a), whole_line(sys, cache, *b)) else { return Vec::new() };
vec![
Rc::new(CenterOfGravity::new(p.x, vec![l.p1.x, l.p2.x], vec![0.5, 0.5])),
Rc::new(CenterOfGravity::new(p.y, vec![l.p1.y, l.p2.y], vec![0.5, 0.5])),
]
}
ConstraintKind::PointOnCurve => {
let [a, b] = c.refs.as_slice() else { return Vec::new() };
let (Some(p), Some(geom)) = (point_ref(sys, cache, *a), resolve_ref(document, sys, cache, *b)) else {
return Vec::new();
};
match geom {
EntityGeom::Line(l) => vec![Rc::new(PointOnLine::new(p, l))],
EntityGeom::Circle(circ) => {
let zero = sys.add_param(0.0, true);
vec![Rc::new(P2CDistance::new(circ, p, zero))]
}
}
}
ConstraintKind::Symmetric => {
let [a, b, m] = c.refs.as_slice() else { return Vec::new() };
let (Some(pa), Some(pb), Some(mirror)) =
(point_ref(sys, cache, *a), point_ref(sys, cache, *b), whole_line(sys, cache, *m))
else {
return Vec::new();
};
let pair = GLine { p1: pa, p2: pb };
vec![
Rc::new(MidpointOnLine::new(pair, mirror)),
Rc::new(PerpendicularConstraint::new(sys.store(), pair, mirror)),
]
}
ConstraintKind::Fixed => {
let Some(r) = c.refs.first() else { return Vec::new() };
let Some(geom) = resolve_ref(document, sys, cache, *r) else {
return Vec::new();
};
match geom {
EntityGeom::Line(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)),
]
}
}
let (ca, cb) = (whole_circle(sys, cache, *a)?, whole_circle(sys, cache, *b)?);
Some(Rc::new(Equal::new(cb.rad, ca.rad, 1.0)))
}
ConstraintKind::Distance => {
let [a, b] = c.refs.as_slice() else { return None };
let (pa, pb) = (point_ref(sys, cache, *a)?, point_ref(sys, cache, *b)?);
let target = sys.add_param(c.driving_param.as_ref()?.resolve(params).ok()?, true);
Some(Rc::new(P2PDistance::new(pa, pb, target)))
let [a, b] = c.refs.as_slice() else { return Vec::new() };
let (Some(pa), Some(pb)) = (point_ref(sys, cache, *a), point_ref(sys, cache, *b)) else { return Vec::new() };
let Some(Ok(resolved)) = c.driving_param.as_ref().map(|d| d.resolve(params)) else { return Vec::new() };
let target = sys.add_param(resolved, true);
vec![Rc::new(P2PDistance::new(pa, pb, target))]
}
ConstraintKind::Angle => {
let [a, b] = c.refs.as_slice() else { return None };
let (fixed, moving) = (whole_line(sys, cache, *a)?, whole_line(sys, cache, *b)?);
let angle = sys.add_param(c.driving_param.as_ref()?.resolve(params).ok()?.to_radians(), true);
Some(Rc::new(L2LAngle::new(fixed, moving, angle)))
let [a, b] = c.refs.as_slice() else { return Vec::new() };
let (Some(fixed), Some(moving)) = (whole_line(sys, cache, *a), whole_line(sys, cache, *b)) else { return Vec::new() };
let Some(Ok(resolved)) = c.driving_param.as_ref().map(|d| d.resolve(params)) else { return Vec::new() };
let angle = sys.add_param(resolved.to_radians(), true);
vec![Rc::new(L2LAngle::new(fixed, moving, angle))]
}
ConstraintKind::Radius => {
let circle = whole_circle(sys, cache, *c.refs.first()?)?;
let target = sys.add_param(c.driving_param.as_ref()?.resolve(params).ok()?, true);
Some(Rc::new(Equal::new(circle.rad, target, 1.0)))
let Some(r) = c.refs.first() else { return Vec::new() };
let Some(circle) = whole_circle(sys, cache, *r) else {
return Vec::new();
};
let Some(Ok(resolved)) = c.driving_param.as_ref().map(|d| d.resolve(params)) else { return Vec::new() };
let target = sys.add_param(resolved, true);
vec![Rc::new(Equal::new(circle.rad, target, 1.0))]
}
ConstraintKind::Tangent => {
let [a, b] = c.refs.as_slice() else { return None };
let (ga, gb) = (resolve_ref(document, sys, cache, *a)?, resolve_ref(document, sys, cache, *b)?);
let [a, b] = c.refs.as_slice() else { return Vec::new() };
let (Some(ga), Some(gb)) = (resolve_ref(document, sys, cache, *a), resolve_ref(document, sys, cache, *b)) else {
return Vec::new();
};
match (ga, gb) {
(EntityGeom::Circle(c1), EntityGeom::Circle(c2)) => {
let store = sys.store();
@@ -217,7 +317,7 @@ fn build_constraint(
// the first solve doesn't have to cross the singularity
// between the two tangency configurations.
let internal = center_dist < (r1 - r2).abs();
Some(Rc::new(TangentCircumf::new(c1.center, c2.center, c1.rad, c2.rad, internal)))
vec![Rc::new(TangentCircumf::new(c1.center, c2.center, c1.rad, c2.rad, internal))]
}
(EntityGeom::Circle(circ), EntityGeom::Line(line)) | (EntityGeom::Line(line), EntityGeom::Circle(circ)) => {
let store = sys.store();
@@ -234,11 +334,11 @@ fn build_constraint(
// (see its `error_grad`), i.e. plain tangency rather than
// an offset distance.
let zero = sys.add_param(0.0, true);
Some(Rc::new(C2LDistance::new(circ, line, zero, ccw, false)))
vec![Rc::new(C2LDistance::new(circ, line, zero, ccw, false))]
}
// Line-Line tangency has no meaning; Arc isn't a supported
// `EntityGeom` yet (see that type's own doc comment).
_ => None,
_ => Vec::new(),
}
}
}
@@ -265,35 +365,20 @@ fn solve_scope(
) -> Option<(Vec<(Handle, EntityType)>, usize, Vec<(ConstraintId, ocs_gcs::diagnosis::RedundancyKind)>)> {
let mut sys = System::new();
let mut cache: HashMap<Handle, EntityGeom> = HashMap::new();
// Tracks which system-level `ocs_gcs` constraint came from which
// Tracks which system-level `ocs_gcs` constraint(s) came from which
// `SketchConstraint` — a `SubSystem`'s redundant-row indices (from
// `ocs_gcs::diagnosis`) are local to that partition's own constraint
// list, not `set.constraints`' indices, and Coincident contributes two
// system-level constraints (X and Y halves) for one `SketchConstraint`.
// `Rc::ptr_eq` against this after partitioning resolves a row back to
// the `ConstraintId` the UI actually names.
// list, not `set.constraints`' indices, and several kinds contribute
// more than one system-level constraint per `SketchConstraint` (see
// `build_constraint`'s doc comment). `Rc::ptr_eq` against this after
// partitioning resolves a row back to the `ConstraintId` the UI
// actually names.
let mut owner: Vec<(Rc<dyn Constraint>, ConstraintId)> = Vec::new();
for c in &set.constraints {
let Some(constraint) = build_constraint(document, &mut sys, &mut cache, params, c) else { continue };
owner.push((constraint.clone(), c.id));
sys.add_constraint(constraint);
// Coincident needs both X and Y equal; `build_constraint` returns
// only the X half (see its doc comment there) since one
// `SketchConstraint` maps to one `Rc<dyn Constraint>` everywhere
// else — add the Y half here instead of complicating that
// one-constraint-per-kind contract for a single kind.
if c.kind == ConstraintKind::Coincident {
if let [a, b] = c.refs.as_slice() {
if let (Some(pa), Some(pb)) = (
a.marker.and_then(|m| cache.get(&a.entity).and_then(|g| g.point_for_marker(m))),
b.marker.and_then(|m| cache.get(&b.entity).and_then(|g| g.point_for_marker(m))),
) {
let y_half: Rc<dyn Constraint> = Rc::new(Equal::new(pa.y, pb.y, 1.0));
owner.push((y_half.clone(), c.id));
sys.add_constraint(y_half);
}
}
for constraint in build_constraint(document, &mut sys, &mut cache, params, c) {
owner.push((constraint.clone(), c.id));
sys.add_constraint(constraint);
}
}