forked from 1b-agent/opencadstudio-upstream
Extend sketch constraints toward FreeCAD parity, staying DWG-native-compatible
Arcs are now full constraint participants (center/radius via the existing Circle-shaped solver path, plus real endpoints kept consistent by arc-rules CurveValue constraints); adds Diameter/DistanceX/DistanceY/ArcLength/Normal constraint kinds and initial Ellipse entity support (center-based constraints only). DWG-native class mappings and gaps (ArcLength, Ellipse) are grounded against real ObjectARX headers rather than guessed, so files saved with DWG-native constraints stay honest about what AutoCAD can actually represent. Also fixes a case-sensitivity bug where a lowercase named-parameter reference typed at a dimensional constraint's value prompt was rejected (the command line uppercases SingleToken input before the parameter-name lookup), and adds a "Used by" column to the Named Parameters panel. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
eb96e8b6ed
commit
3d0a41b69a
12 files changed
+1660
-60
No files matched your search
@@ -121,7 +121,20 @@ impl<'a> GroupBuilder<'a> {
|
||||
/// (well: `BoundedLine`, §4 — nothing we draw is an infinite
|
||||
/// construction line), `Circle`, or `Arc` node the first time this
|
||||
/// entity is referenced. Returns `None` for an entity kind this pass
|
||||
/// doesn't support as constraint geometry.
|
||||
/// doesn't support as constraint geometry — currently including
|
||||
/// `Ellipse` (constraint-parity Phase 5): the solver
|
||||
/// (`sketch_solve.rs`) and this app's own XRecord format both support
|
||||
/// ellipse-referencing constraints already, but `acadrust`'s
|
||||
/// `AssocConstraintNodeData::Ellipse`/`BoundedEllipse` variants have a
|
||||
/// different field shape from `Circle`/`Arc`'s here (`owner_id`/
|
||||
/// `is_implied`/`is_active` instead of `geometry_dependency`/
|
||||
/// `geometry_node_id`, i.e. they don't obviously look like the same
|
||||
/// "geometry dependency for an entity reference" role) — without
|
||||
/// confirming that against a real AutoCAD-written file, guessing would
|
||||
/// risk writing a wrong-but-plausible-looking object graph, which is
|
||||
/// worse than the honest "not persisted to DWG yet" gap this already
|
||||
/// degrades to (same contract as `ArcLength`, which has no native
|
||||
/// representation at all).
|
||||
fn geometry_node(&mut self, handle: Handle) -> Option<i32> {
|
||||
if let Some(existing) = self.entities.get(&handle) {
|
||||
if existing.geometry_node_id != 0 {
|
||||
@@ -269,6 +282,10 @@ fn geometrical_class_name(kind: ConstraintKind) -> Option<&'static str> {
|
||||
ConstraintKind::Vertical => Some("ACVERTICALCONSTRAINT"),
|
||||
ConstraintKind::Perpendicular => Some("ACPERPENDICULARCONSTRAINT"),
|
||||
ConstraintKind::Tangent => Some("ACTANGENTCONSTRAINT"),
|
||||
// AutoCAD's own `GeomConstraintType` enum (ObjectARX's
|
||||
// `AcGeomConstraint.h`) lists `kNormal` alongside `kPerpendicular`
|
||||
// as a distinct, real geometric-constraint kind.
|
||||
ConstraintKind::Normal => Some("ACNORMALCONSTRAINT"),
|
||||
ConstraintKind::Concentric => Some("ACCONCENTRICCONSTRAINT"),
|
||||
ConstraintKind::CenterPoint => Some("ACCENTERPOINTCONSTRAINT"),
|
||||
ConstraintKind::Colinear => Some("ACCOLINEARCONSTRAINT"),
|
||||
@@ -285,15 +302,29 @@ fn geometrical_class_name(kind: ConstraintKind) -> Option<&'static str> {
|
||||
// (design doc §5) needs the resolved entity types, not just the
|
||||
// ConstraintKind, so it's handled by the caller instead of here.
|
||||
ConstraintKind::Equal | ConstraintKind::Parallel | ConstraintKind::Distance
|
||||
| ConstraintKind::Angle | ConstraintKind::Radius => None,
|
||||
| ConstraintKind::Angle | ConstraintKind::Radius | ConstraintKind::Diameter
|
||||
| ConstraintKind::DistanceX | ConstraintKind::DistanceY => None,
|
||||
// No native DWG representation exists at all: neither
|
||||
// `AcExplicitConstr.h` (Distance/Angle/RadiusDiameter — no
|
||||
// ArcLength-shaped class) nor `acadrust`'s `AssocConstraintNodeData`
|
||||
// has anything for it. `constraint_node` below falls through to
|
||||
// its own `_ => None` for this kind, same "can't persist to DWG,
|
||||
// XRecord-only" gap the dependency-chain DXF omission already
|
||||
// documents.
|
||||
ConstraintKind::ArcLength => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `Diameter`/`DistanceX`/`DistanceY` reuse `Radius`'/`Distance`'s own DWG
|
||||
/// class — real AutoCAD represents them as the same
|
||||
/// `AcRadiusDiameterConstraint`/`AcDistanceConstraint` object with a
|
||||
/// different `RadiusDiameterConstrType`/`DirectionType` mode byte (set in
|
||||
/// `constraint_node` below), not a distinct native class.
|
||||
const fn dimensional_class_name(kind: ConstraintKind) -> &'static str {
|
||||
match kind {
|
||||
ConstraintKind::Distance => "ACDISTANCECONSTRAINT",
|
||||
ConstraintKind::Distance | ConstraintKind::DistanceX | ConstraintKind::DistanceY => "ACDISTANCECONSTRAINT",
|
||||
ConstraintKind::Angle => "ACANGLECONSTRAINT",
|
||||
ConstraintKind::Radius => "ACRADIUSDIAMETERCONSTRAINT",
|
||||
ConstraintKind::Radius | ConstraintKind::Diameter => "ACRADIUSDIAMETERCONSTRAINT",
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
@@ -361,9 +392,10 @@ fn constraint_node(
|
||||
builder.connect(node_id, nb);
|
||||
Some(node_id)
|
||||
}
|
||||
ConstraintKind::Distance | ConstraintKind::Angle | ConstraintKind::Radius => {
|
||||
ConstraintKind::Distance | ConstraintKind::DistanceX | ConstraintKind::DistanceY
|
||||
| ConstraintKind::Angle | ConstraintKind::Radius | ConstraintKind::Diameter => {
|
||||
let target_refs: Vec<i32> = match constraint.kind {
|
||||
ConstraintKind::Radius => vec![builder.ref_node(*refs.first()?)?],
|
||||
ConstraintKind::Radius | ConstraintKind::Diameter => vec![builder.ref_node(*refs.first()?)?],
|
||||
_ => {
|
||||
let [a, b] = refs else { return None };
|
||||
vec![builder.ref_node(*a)?, builder.ref_node(*b)?]
|
||||
@@ -372,6 +404,7 @@ fn constraint_node(
|
||||
let owner_id = *target_refs.first()?;
|
||||
let node_id = builder.alloc_node_id();
|
||||
let data = match constraint.kind {
|
||||
// Plain two-point distance: undirected (`kNotDirected`).
|
||||
ConstraintKind::Distance => AssocConstraintNodeData::Distance {
|
||||
owner_id,
|
||||
is_implied: false,
|
||||
@@ -381,6 +414,28 @@ fn constraint_node(
|
||||
direction_type: 0,
|
||||
distance: None,
|
||||
},
|
||||
// X-only/Y-only: `kFixedDirection` with the fixed unit
|
||||
// vector the distance is measured along — AutoCAD's own
|
||||
// representation of DistanceX/DistanceY, not a distinct
|
||||
// constraint class.
|
||||
ConstraintKind::DistanceX => AssocConstraintNodeData::Distance {
|
||||
owner_id,
|
||||
is_implied: false,
|
||||
is_active: true,
|
||||
value_dependency: Handle::NULL,
|
||||
dimension_dependency: Handle::NULL,
|
||||
direction_type: 1,
|
||||
distance: Some(Vector3::new(1.0, 0.0, 0.0)),
|
||||
},
|
||||
ConstraintKind::DistanceY => AssocConstraintNodeData::Distance {
|
||||
owner_id,
|
||||
is_implied: false,
|
||||
is_active: true,
|
||||
value_dependency: Handle::NULL,
|
||||
dimension_dependency: Handle::NULL,
|
||||
direction_type: 1,
|
||||
distance: Some(Vector3::new(0.0, 1.0, 0.0)),
|
||||
},
|
||||
ConstraintKind::Angle => AssocConstraintNodeData::Angle {
|
||||
owner_id,
|
||||
is_implied: false,
|
||||
@@ -397,6 +452,16 @@ fn constraint_node(
|
||||
dimension_dependency: Handle::NULL,
|
||||
mode: 0,
|
||||
},
|
||||
// `kCircleDiameter` — same class as Radius (`mode: 0` /
|
||||
// `kCircleRadius`), different mode byte.
|
||||
ConstraintKind::Diameter => AssocConstraintNodeData::RadiusDiameter {
|
||||
owner_id,
|
||||
is_implied: false,
|
||||
is_active: true,
|
||||
value_dependency: Handle::NULL,
|
||||
dimension_dependency: Handle::NULL,
|
||||
mode: 1,
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
builder.push_node(node_id, dimensional_class_name(constraint.kind), data);
|
||||
@@ -847,7 +912,7 @@ impl Scene {
|
||||
mod tests {
|
||||
use super::super::sketch_constraints::SketchScope;
|
||||
use super::*;
|
||||
use acadrust::entities::{Circle, Line};
|
||||
use acadrust::entities::{Arc, Circle, Line};
|
||||
|
||||
fn line_entity(scene: &mut Scene, start: (f64, f64), end: (f64, f64)) -> Handle {
|
||||
scene.add_entity(EntityType::Line(Line::from_points(
|
||||
@@ -860,6 +925,15 @@ mod tests {
|
||||
scene.add_entity(EntityType::Circle(Circle::from_center_radius(Vector3::new(center.0, center.1, 0.0), radius)))
|
||||
}
|
||||
|
||||
fn arc_entity(scene: &mut Scene, center: (f64, f64), radius: f64) -> Handle {
|
||||
scene.add_entity(EntityType::Arc(Arc::from_center_radius_angles(
|
||||
Vector3::new(center.0, center.1, 0.0),
|
||||
radius,
|
||||
0.0,
|
||||
std::f64::consts::PI,
|
||||
)))
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -1133,7 +1207,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The 8 new `ConstraintKind`s all reuse the same generic
|
||||
/// These `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
|
||||
@@ -1167,6 +1241,7 @@ mod tests {
|
||||
vec![SketchRef::center(circle_a), SketchRef::center(circle_b), SketchRef::whole(axis)],
|
||||
None,
|
||||
);
|
||||
set.add(ConstraintKind::Normal, vec![SketchRef::whole(circle_a), SketchRef::whole(line_a)], None);
|
||||
|
||||
scene.materialize_dwg_native_constraints_for_save(true);
|
||||
let owner = scene.document.header.model_space_block_handle;
|
||||
@@ -1178,6 +1253,7 @@ mod tests {
|
||||
|
||||
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",
|
||||
@@ -1187,9 +1263,258 @@ mod tests {
|
||||
"ACPOINTCURVECONSTRAINT",
|
||||
"ACEQUALDISTANCECONSTRAINT",
|
||||
"ACSYMMETRICCONSTRAINT",
|
||||
"ACNORMALCONSTRAINT",
|
||||
] {
|
||||
assert!(class_names.contains(&expected), "{ext}: missing {expected} node, got {class_names:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Constraint-parity Phase 1: an Arc registers in the solver as its own
|
||||
/// center/radius (`sketch_solve.rs`'s `EntityGeom::Circle` reuse), and
|
||||
/// this module already had `ACCONSTRAINEDARC` geometry-dependency
|
||||
/// support in place before that. This confirms the two sides actually
|
||||
/// meet: a Concentric/Tangent/Radius constraint referencing an arc
|
||||
/// still gets its expected class names AND its geometry node is the
|
||||
/// arc-specific one, through a real DWG/DXF byte round trip — not just
|
||||
/// the in-memory graph this test module builds directly.
|
||||
#[test]
|
||||
fn constraints_referencing_an_arc_round_trip_with_the_arc_geometry_node() {
|
||||
for ext in ["dxf", "dwg"] {
|
||||
let mut scene = Scene::new();
|
||||
let arc = arc_entity(&mut scene, (0.0, 0.0), 4.0);
|
||||
let circle = circle_entity(&mut scene, (10.0, -6.0), 2.0);
|
||||
|
||||
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
|
||||
set.add(ConstraintKind::Concentric, vec![SketchRef::center(arc), SketchRef::center(circle)], None);
|
||||
set.add(
|
||||
ConstraintKind::Radius,
|
||||
vec![SketchRef::whole(arc)],
|
||||
Some(crate::scene::named_parameters::DrivingValue::Literal(9.0)),
|
||||
);
|
||||
|
||||
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!("arc_ref.{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();
|
||||
assert!(class_names.contains(&"ACCONCENTRICCONSTRAINT"), "{ext}: missing ACCONCENTRICCONSTRAINT, got {class_names:?}");
|
||||
assert!(class_names.contains(&"ACRADIUSDIAMETERCONSTRAINT"), "{ext}: missing ACRADIUSDIAMETERCONSTRAINT, got {class_names:?}");
|
||||
assert!(
|
||||
group.nodes.iter().any(|n| n.class_name == "ACCONSTRAINEDARC"),
|
||||
"{ext}: the arc should have its own geometry node, got {class_names:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Constraint-parity Phase 4b: a Coincident constraint referencing an
|
||||
/// arc's actual *endpoint* (marker 0, not its center via `-3` or its
|
||||
/// whole curve) round-trips correctly too — `SketchRef`'s marker is
|
||||
/// serialized generically regardless of what entity type or point it
|
||||
/// addresses, so this needs no dedicated DWG-side wiring beyond what
|
||||
/// already existed; this test is here to actually confirm that rather
|
||||
/// than assume it.
|
||||
#[test]
|
||||
fn a_coincident_constraint_on_an_arcs_endpoint_round_trips() {
|
||||
for ext in ["dxf", "dwg"] {
|
||||
let mut scene = Scene::new();
|
||||
let arc = arc_entity(&mut scene, (0.0, 0.0), 4.0);
|
||||
let line = line_entity(&mut scene, (20.0, 20.0), (21.0, 20.0));
|
||||
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::Coincident,
|
||||
vec![SketchRef::point(arc, 0), SketchRef::point(line, 0)],
|
||||
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!("arc_endpoint_ref.{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();
|
||||
assert!(class_names.contains(&"ACPOINTCOINCIDENCECONSTRAINT"), "{ext}: missing ACPOINTCOINCIDENCECONSTRAINT, got {class_names:?}");
|
||||
assert!(
|
||||
group.nodes.iter().any(|n| n.class_name == "ACCONSTRAINEDARC"),
|
||||
"{ext}: the arc should still have its own geometry node, got {class_names:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Constraint-parity Phase 5: an ellipse isn't a supported geometry
|
||||
/// node type in `geometry_node` yet (see its own doc comment for why),
|
||||
/// so a Concentric constraint referencing one degrades to "not in the
|
||||
/// native graph" — the same "skip, don't error" contract every other
|
||||
/// unbuildable native-constraint case gets — while the other
|
||||
/// constraint in the same scope (Fixed, on a plain circle) still
|
||||
/// persists normally, and the ellipse constraint itself still survives
|
||||
/// in this app's own XRecord format.
|
||||
#[test]
|
||||
fn a_constraint_referencing_an_ellipse_is_absent_from_the_native_graph_but_not_from_xrecord() {
|
||||
for ext in ["dxf", "dwg"] {
|
||||
let mut scene = Scene::new();
|
||||
let ellipse = scene.add_entity(EntityType::Ellipse(acadrust::entities::Ellipse::from_center_axes(
|
||||
Vector3::new(0.0, 0.0, 0.0),
|
||||
Vector3::new(4.0, 0.0, 0.0),
|
||||
0.5,
|
||||
)));
|
||||
let circle = circle_entity(&mut scene, (10.0, 10.0), 2.0);
|
||||
|
||||
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
|
||||
set.add(ConstraintKind::Concentric, vec![SketchRef::center(ellipse), SketchRef::center(circle)], None);
|
||||
set.add(ConstraintKind::Fixed, vec![SketchRef::whole(circle)], None);
|
||||
|
||||
scene.materialize_sketch_constraints_for_save();
|
||||
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_doc =
|
||||
crate::io::load_bytes(&format!("ellipse_ref.{ext}"), bytes).unwrap_or_else(|e| panic!("reload {ext}: {e}"));
|
||||
|
||||
let group = native_group(&reloaded_doc, owner);
|
||||
let class_names: Vec<&str> = group.nodes.iter().map(|n| n.class_name.as_str()).collect();
|
||||
assert!(class_names.contains(&"ACFIXEDCONSTRAINT"), "{ext}: the other constraint should still persist, got {class_names:?}");
|
||||
assert!(
|
||||
!class_names.contains(&"ACCONCENTRICCONSTRAINT"),
|
||||
"{ext}: the ellipse-referencing Concentric constraint has no supported geometry node and must not appear, got {class_names:?}"
|
||||
);
|
||||
|
||||
let mut reloaded_scene = Scene::new();
|
||||
reloaded_scene.document = reloaded_doc;
|
||||
reloaded_scene.load_sketch_constraints_from_document();
|
||||
let restored = reloaded_scene
|
||||
.sketch_constraint_set(SketchScope::ModelSpace)
|
||||
.unwrap_or_else(|| panic!("{ext}: no ModelSpace constraint set survived the round trip"));
|
||||
let kinds: Vec<ConstraintKind> = restored.constraints.iter().map(|c| c.kind).collect();
|
||||
assert!(kinds.contains(&ConstraintKind::Concentric), "{ext}: the ellipse constraint should still round-trip via XRecord, got {kinds:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Constraint-parity Phase 4a: `ArcLength` has no ObjectARX class at
|
||||
/// all (confirmed against `AcExplicitConstr.h`) — this asserts the
|
||||
/// degradation is exactly as documented: the native DWG/DXF graph
|
||||
/// gets every *other* constraint on the arc (Radius, here) but no
|
||||
/// ArcLength-shaped node, while the app's own constraint set (what
|
||||
/// XRecord persistence actually serializes, per `sketch_persist.rs`'s
|
||||
/// fully-generic encode/decode) still carries it untouched.
|
||||
#[test]
|
||||
fn arc_length_has_no_native_class_but_survives_in_the_apps_own_constraint_set() {
|
||||
for ext in ["dxf", "dwg"] {
|
||||
let mut scene = Scene::new();
|
||||
let arc = arc_entity(&mut scene, (0.0, 0.0), 4.0);
|
||||
|
||||
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
|
||||
set.add(ConstraintKind::Radius, vec![SketchRef::whole(arc)], Some(DrivingValue::Literal(4.0)));
|
||||
set.add(ConstraintKind::ArcLength, vec![SketchRef::whole(arc)], Some(DrivingValue::Literal(6.0)));
|
||||
|
||||
// Both materializers, "called alongside, not instead of" each
|
||||
// other (`materialize_dwg_native_constraints_for_save`'s own
|
||||
// doc comment) — the native graph and this app's own XRecord
|
||||
// format are independent, additive persistence paths.
|
||||
scene.materialize_sketch_constraints_for_save();
|
||||
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_doc =
|
||||
crate::io::load_bytes(&format!("arc_length.{ext}"), bytes).unwrap_or_else(|e| panic!("reload {ext}: {e}"));
|
||||
|
||||
let group = native_group(&reloaded_doc, owner);
|
||||
let class_names: Vec<&str> = group.nodes.iter().map(|n| n.class_name.as_str()).collect();
|
||||
assert!(class_names.contains(&"ACRADIUSDIAMETERCONSTRAINT"), "{ext}: missing ACRADIUSDIAMETERCONSTRAINT, got {class_names:?}");
|
||||
assert!(
|
||||
!class_names.iter().any(|n| n.contains("ARCLENGTH")),
|
||||
"{ext}: ArcLength has no native DWG class and must not appear here, got {class_names:?}"
|
||||
);
|
||||
|
||||
// Still present in this app's own (XRecord-backed) constraint
|
||||
// set — reload into a fresh `Scene` and read it back the same
|
||||
// way `sketch_persist.rs`'s own round-trip tests do.
|
||||
let mut reloaded_scene = Scene::new();
|
||||
reloaded_scene.document = reloaded_doc;
|
||||
reloaded_scene.load_sketch_constraints_from_document();
|
||||
let restored = reloaded_scene
|
||||
.sketch_constraint_set(SketchScope::ModelSpace)
|
||||
.unwrap_or_else(|| panic!("{ext}: no ModelSpace constraint set survived the round trip"));
|
||||
let kinds: Vec<ConstraintKind> = restored.constraints.iter().map(|c| c.kind).collect();
|
||||
assert!(kinds.contains(&ConstraintKind::ArcLength), "{ext}: ArcLength should still round-trip via XRecord, got {kinds:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Constraint-parity Phase 2: `Diameter`/`DistanceX`/`DistanceY` reuse
|
||||
/// `Radius`'/`Distance`'s own DWG class with a different
|
||||
/// `RadiusDiameterConstrType`/`DirectionType` mode byte, matching real
|
||||
/// AutoCAD's representation (this module's own research, folded into
|
||||
/// `dimensional_class_name`'s doc comment). Confirms that byte actually
|
||||
/// survives a real DWG/DXF write+read, not just the in-memory node this
|
||||
/// module built.
|
||||
#[test]
|
||||
fn diameter_and_directed_distance_constraints_round_trip_with_their_mode_byte() {
|
||||
for ext in ["dxf", "dwg"] {
|
||||
let mut scene = Scene::new();
|
||||
let circle = circle_entity(&mut scene, (0.0, 0.0), 3.0);
|
||||
let line = line_entity(&mut scene, (0.0, 0.0), (10.0, 0.0));
|
||||
|
||||
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
|
||||
set.add(ConstraintKind::Diameter, vec![SketchRef::whole(circle)], Some(DrivingValue::Literal(16.0)));
|
||||
set.add(
|
||||
ConstraintKind::DistanceX,
|
||||
vec![SketchRef::point(line, 0), SketchRef::point(line, 1)],
|
||||
Some(DrivingValue::Literal(10.0)),
|
||||
);
|
||||
set.add(
|
||||
ConstraintKind::DistanceY,
|
||||
vec![SketchRef::point(line, 0), SketchRef::point(line, 1)],
|
||||
Some(DrivingValue::Literal(3.0)),
|
||||
);
|
||||
|
||||
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!("diameter_directed.{ext}"), bytes).unwrap_or_else(|e| panic!("reload {ext}: {e}"));
|
||||
|
||||
let group = native_group(&reloaded, owner);
|
||||
let radius_diameter_nodes: Vec<_> = group
|
||||
.nodes
|
||||
.iter()
|
||||
.filter_map(|n| match &n.data {
|
||||
AssocConstraintNodeData::RadiusDiameter { mode, .. } => Some(*mode),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(radius_diameter_nodes, vec![1], "{ext}: Diameter should round-trip as mode=1 (kCircleDiameter), got {radius_diameter_nodes:?}");
|
||||
|
||||
let distance_nodes: Vec<_> = group
|
||||
.nodes
|
||||
.iter()
|
||||
.filter_map(|n| match &n.data {
|
||||
AssocConstraintNodeData::Distance { direction_type, distance, .. } => Some((*direction_type, *distance)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(distance_nodes.len(), 2, "{ext}: expected DistanceX and DistanceY nodes, got {distance_nodes:?}");
|
||||
assert!(
|
||||
distance_nodes.contains(&(1, Some(Vector3::new(1.0, 0.0, 0.0)))),
|
||||
"{ext}: DistanceX should round-trip as direction_type=1 with a (1,0,0) direction, got {distance_nodes:?}"
|
||||
);
|
||||
assert!(
|
||||
distance_nodes.contains(&(1, Some(Vector3::new(0.0, 1.0, 0.0)))),
|
||||
"{ext}: DistanceY should round-trip as direction_type=1 with a (0,1,0) direction, got {distance_nodes:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,6 +114,42 @@ pub enum ConstraintKind {
|
||||
/// point-symmetry about a point, and symmetry between two lines, aren't
|
||||
/// modeled.
|
||||
Symmetric,
|
||||
/// A circle/arc's diameter (twice `Radius`'s target) — `refs`:
|
||||
/// `[whole(circle_or_arc)]`. Same DWG class as `Radius`
|
||||
/// (`ACRADIUSDIAMETERCONSTRAINT`), distinguished only by the
|
||||
/// `RadiusDiameterConstrType` mode byte
|
||||
/// (`dwg_native_constraints.rs`), matching how real AutoCAD represents
|
||||
/// it — not a distinct native object type.
|
||||
Diameter,
|
||||
/// The X-only (resp. Y-only) component of the distance between two
|
||||
/// points — `refs`: `[p1, p2]`, same shape as `Distance`. Same DWG
|
||||
/// class as `Distance` (`ACDISTANCECONSTRAINT`) with its
|
||||
/// `DirectionType` set to a fixed `(1,0,0)`/`(0,1,0)` direction, again
|
||||
/// matching AutoCAD's own representation rather than inventing a new
|
||||
/// class.
|
||||
DistanceX,
|
||||
DistanceY,
|
||||
/// A line perpendicular to a circle/arc's tangent at their point of
|
||||
/// contact — `refs`: `[whole(a), whole(b)]`, either order. For the
|
||||
/// Line/Circle-only entity model this is equivalent to "the line
|
||||
/// passes through the circle's center" (a circle's radius is always
|
||||
/// normal to its own tangent), so it solves via the same `PointOnLine`
|
||||
/// primitive `PointOnCurve` already uses. Distinct from
|
||||
/// `Perpendicular` (line-to-line only) — AutoCAD's own
|
||||
/// `GeomConstraintType` enum lists `kNormal` and `kPerpendicular`
|
||||
/// separately for exactly this reason. Line-Line has no meaning here
|
||||
/// (that's plain `Perpendicular`) and isn't buildable.
|
||||
Normal,
|
||||
/// An arc's arc length (`radius * sweep angle`) — `refs`:
|
||||
/// `[whole(arc)]`. Solves against the arc's own `start_angle`/
|
||||
/// `end_angle`, registered separately from its center/radius
|
||||
/// (`sketch_solve.rs`'s `arc_angles` cache) — no native DWG
|
||||
/// representation exists for this (neither `AcExplicitConstr.h` nor
|
||||
/// `acadrust`'s `AssocConstraintNodeData` has an ArcLength-shaped
|
||||
/// class/variant), so it persists in this app's own XRecord format
|
||||
/// only, the same "DWG can't carry everything" gap the dependency-chain
|
||||
/// DXF-omission precedent already documents.
|
||||
ArcLength,
|
||||
}
|
||||
|
||||
pub type ConstraintId = u32;
|
||||
@@ -388,6 +424,11 @@ impl ConstraintKind {
|
||||
ConstraintKind::PointOnCurve => "∈",
|
||||
ConstraintKind::EqualDistance => "≐",
|
||||
ConstraintKind::Symmetric => "S",
|
||||
ConstraintKind::Diameter => "⌀",
|
||||
ConstraintKind::DistanceX => "↔ₓ",
|
||||
ConstraintKind::DistanceY => "↔ᵧ",
|
||||
ConstraintKind::Normal => "⊾",
|
||||
ConstraintKind::ArcLength => "⌢",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -494,6 +535,40 @@ impl super::Scene {
|
||||
let changes: Vec<(Handle, super::ChangeKind)> = touched.into_iter().map(|h| (h, super::ChangeKind::Modified)).collect();
|
||||
self.bump_entities(&changes);
|
||||
}
|
||||
|
||||
/// Every persistent constraint, in any scope, currently driven by the
|
||||
/// named parameter `name` — what the Named Parameters panel's "used by"
|
||||
/// column shows. `entities` is the constraint's own referenced handles
|
||||
/// (deduplicated; a two-point constraint on the same entity's own two
|
||||
/// markers would otherwise list it twice), not resolved against the
|
||||
/// live document — a caller wanting an entity's current type/position
|
||||
/// still needs `Scene::document.get_entity`.
|
||||
pub fn parameter_usage(&self, name: &str) -> Vec<ParameterUsage> {
|
||||
let mut out = Vec::new();
|
||||
for set in &self.sketch_constraints {
|
||||
for c in &set.constraints {
|
||||
let Some(DrivingValue::Named(n)) = &c.driving_param else { continue };
|
||||
if n != name {
|
||||
continue;
|
||||
}
|
||||
let mut entities: Vec<Handle> = c.refs.iter().map(|r| r.entity).collect();
|
||||
entities.sort();
|
||||
entities.dedup();
|
||||
out.push(ParameterUsage { scope: set.scope, constraint_id: c.id, kind: c.kind, entities });
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// One persistent constraint driven by a named parameter — [`Scene::parameter_usage`]'s
|
||||
/// result type.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParameterUsage {
|
||||
pub scope: SketchScope,
|
||||
pub constraint_id: ConstraintId,
|
||||
pub kind: ConstraintKind,
|
||||
pub entities: Vec<Handle>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -586,4 +661,68 @@ mod tests {
|
||||
assert_eq!(restored.constraints[1].driving_param, Some(DrivingValue::Literal(12.5)));
|
||||
assert_eq!(restored.constraints[0].refs, set.constraints[0].refs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parameter_usage_finds_every_constraint_driven_by_the_named_parameter() {
|
||||
let mut scene = super::super::Scene::new();
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::Distance,
|
||||
vec![SketchRef::point(h(1), 0), SketchRef::point(h(1), 1)],
|
||||
Some(DrivingValue::Named("gap".to_string())),
|
||||
);
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::Radius,
|
||||
vec![SketchRef::whole(h(2))],
|
||||
Some(DrivingValue::Named("gap".to_string())),
|
||||
);
|
||||
// Unrelated: a literal-driven constraint and one driven by a
|
||||
// different name must not show up.
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::Radius,
|
||||
vec![SketchRef::whole(h(3))],
|
||||
Some(DrivingValue::Literal(5.0)),
|
||||
);
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::Distance,
|
||||
vec![SketchRef::point(h(4), 0), SketchRef::point(h(4), 1)],
|
||||
Some(DrivingValue::Named("other".to_string())),
|
||||
);
|
||||
|
||||
let usage = scene.parameter_usage("gap");
|
||||
assert_eq!(usage.len(), 2, "exactly the two constraints driven by 'gap', got {usage:?}");
|
||||
assert!(usage.iter().any(|u| u.kind == ConstraintKind::Distance && u.entities == vec![h(1)]));
|
||||
assert!(usage.iter().any(|u| u.kind == ConstraintKind::Radius && u.entities == vec![h(2)]));
|
||||
|
||||
assert_eq!(scene.parameter_usage("nonexistent").len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parameter_usage_searches_every_scope_not_just_model_space() {
|
||||
let mut scene = super::super::Scene::new();
|
||||
let block = h(99);
|
||||
scene.sketch_constraint_set_mut(SketchScope::Block(block)).add(
|
||||
ConstraintKind::Radius,
|
||||
vec![SketchRef::whole(h(1))],
|
||||
Some(DrivingValue::Named("r".to_string())),
|
||||
);
|
||||
let usage = scene.parameter_usage("r");
|
||||
assert_eq!(usage.len(), 1);
|
||||
assert_eq!(usage[0].scope, SketchScope::Block(block));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parameter_usage_deduplicates_an_entity_referenced_by_two_markers() {
|
||||
let mut scene = super::super::Scene::new();
|
||||
// A Distance constraint whose two points are both on the same
|
||||
// entity (e.g. a line's own start and end) must list that entity
|
||||
// once, not twice.
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::Distance,
|
||||
vec![SketchRef::point(h(1), 0), SketchRef::point(h(1), 1)],
|
||||
Some(DrivingValue::Named("len".to_string())),
|
||||
);
|
||||
let usage = scene.parameter_usage("len");
|
||||
assert_eq!(usage.len(), 1);
|
||||
assert_eq!(usage[0].entities, vec![h(1)]);
|
||||
}
|
||||
}
|
||||
+318
-21
@@ -13,14 +13,43 @@
|
||||
//! Maps every [`ConstraintKind`]. `Tangent` covers Line-Circle
|
||||
//! (`C2LDistance` with a driven zero-distance target aliased to the
|
||||
//! circle's own radius via `internal: false`) and Circle-Circle
|
||||
//! (`TangentCircumf`) — Line-Line has no meaning and Arc isn't a supported
|
||||
//! [`EntityGeom`] yet (matches that type's own documented scope), so those
|
||||
//! ref shapes build nothing, same "skip, don't panic" contract every other
|
||||
//! unbuildable constraint already gets. `ccw`/`internal` are picked from
|
||||
//! the pair's *current* geometry (which side of the line the circle already
|
||||
//! sits on; whether the circles are already nested) so the very first solve
|
||||
//! doesn't have to cross a sign-flip singularity to reach the nearest valid
|
||||
//! (`TangentCircumf`) — Line-Line has no meaning, so that ref shape builds
|
||||
//! nothing, same "skip, don't panic" contract every other unbuildable
|
||||
//! constraint already gets. `ccw`/`internal` are picked from the pair's
|
||||
//! *current* geometry (which side of the line the circle already sits on;
|
||||
//! whether the circles are already nested) so the very first solve doesn't
|
||||
//! have to cross a sign-flip singularity to reach the nearest valid
|
||||
//! tangent configuration.
|
||||
//!
|
||||
//! An `Arc` registers as a full [`EntityGeom::Arc`]: center, radius,
|
||||
//! `start_angle`/`end_angle`, *and* real `start`/`end` points (marker `0`/
|
||||
//! `1`, matching `Line`'s own convention — see `sketch_constraints::
|
||||
//! resolve_point`'s read-side counterpart). Naively, `start`/`end` would be
|
||||
//! independent free params that drift away from "actually on the circle at
|
||||
//! that angle" the moment anything pulls on them — planegcs itself avoids
|
||||
//! that by adding "arc rules" constraints internally whenever an arc is
|
||||
//! created; `ocs_gcs` has no dedicated helper for that, but it turns out not
|
||||
//! to need one: `ocs_gcs::constraints::curve_generic::CurveValue` already
|
||||
//! ties a point's `x`/`y` to *any* `Curve`'s value at a parameter, and
|
||||
//! `geo::Arc` already implements `Curve` (its `value(u)` is exactly
|
||||
//! "point on the circle at angle `u`"). So `solve_scope` adds four
|
||||
//! `CurveValue` constraints per registered arc — `start.x`/`start.y` tied
|
||||
//! to the curve at `start_angle`, `end.x`/`end.y` tied to it at
|
||||
//! `end_angle` — right after building every `SketchConstraint`'s own
|
||||
//! system-level constraints, unconditionally, the same way planegcs's own
|
||||
//! arc rules apply regardless of which constraints actually reference the
|
||||
//! arc. That's what makes Coincident/PointOnCurve/Midpoint/EqualDistance/
|
||||
//! Symmetric on an arc's actual endpoint (as opposed to its whole curve or
|
||||
//! its center) work: they resolve through the ordinary `point_ref`/
|
||||
//! `point_for_marker` machinery once markers `0`/`1` return real,
|
||||
//! consistency-anchored points, no per-kind special-casing needed.
|
||||
//!
|
||||
//! Every whole-circle-shaped kind (Concentric, Tangent, Equal, CenterPoint,
|
||||
//! Radius, Diameter, Fixed, Normal) still works on an arc via its
|
||||
//! `.circle` field — `whole_circle` extracts it generically; `Tangent`/
|
||||
//! `Normal`'s own inline matches (which don't go through `whole_circle`)
|
||||
//! have explicit `EntityGeom::Arc` arms alongside their `Circle` ones for
|
||||
//! the same reason.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
@@ -29,13 +58,14 @@ use acadrust::entities::EntityType;
|
||||
use acadrust::types::Handle;
|
||||
|
||||
use ocs_gcs::constraints::angle_distance::L2LAngle;
|
||||
use ocs_gcs::constraints::circle_arc::{C2LDistance, P2CDistance, TangentCircumf};
|
||||
use ocs_gcs::constraints::circle_arc::{ArcLength, C2LDistance, P2CDistance, TangentCircumf};
|
||||
use ocs_gcs::constraints::curve_generic::CurveValue;
|
||||
use ocs_gcs::constraints::point_line::{
|
||||
CenterOfGravity, Equal, EqualLineLength, MidpointOnLine, Parallel as ParallelConstraint,
|
||||
CenterOfGravity, Difference, 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};
|
||||
use ocs_gcs::geo::{Arc as GArc, Circle as GCircle, Ellipse as GEllipse, Line as GLine, Point as GPoint};
|
||||
use ocs_gcs::solvers::dogleg::solve_dl;
|
||||
use ocs_gcs::system::System;
|
||||
|
||||
@@ -44,14 +74,22 @@ use super::sketch_constraints::{ConstraintId, ConstraintKind, SketchConstraint,
|
||||
use super::{ChangeKind, Scene};
|
||||
|
||||
/// One referenced entity's geometry, registered into an `ocs_gcs::System`'s
|
||||
/// parameter store. Only the two entity types the existing one-shot
|
||||
/// constraint commands already support (`crate::modules::draw::constrain`)
|
||||
/// — extending this to arcs/ellipses is future work, not a gap introduced
|
||||
/// here.
|
||||
/// parameter store.
|
||||
#[derive(Clone, Copy)]
|
||||
enum EntityGeom {
|
||||
Line(GLine),
|
||||
Circle(GCircle),
|
||||
/// Full `geo::Arc` — center/radius (via `.circle`), `start_angle`/
|
||||
/// `end_angle`, and real `start`/`end` points kept consistent with
|
||||
/// those by the arc-rules `CurveValue` constraints `solve_scope` adds
|
||||
/// for every registered arc. See this module's doc comment.
|
||||
Arc(GArc),
|
||||
/// Center-and-shape support only — see `register_entity`'s `Ellipse`
|
||||
/// arm for the acadrust-to-`ocs_gcs` parametrization conversion, and
|
||||
/// this module's doc comment for what isn't wired up yet (major/minor
|
||||
/// axis dimensional constraints, `PointOnEllipse`-based Coincident/
|
||||
/// Tangent).
|
||||
Ellipse(GEllipse),
|
||||
}
|
||||
|
||||
impl EntityGeom {
|
||||
@@ -64,11 +102,34 @@ impl EntityGeom {
|
||||
(EntityGeom::Line(l), 0) => Some(l.p1),
|
||||
(EntityGeom::Line(l), 1) => Some(l.p2),
|
||||
(EntityGeom::Circle(c), -3) => Some(c.center),
|
||||
(EntityGeom::Arc(a), 0) => Some(a.start),
|
||||
(EntityGeom::Arc(a), 1) => Some(a.end),
|
||||
(EntityGeom::Arc(a), -3) => Some(a.circle.center),
|
||||
(EntityGeom::Ellipse(e), -3) => Some(e.center),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `Tangent`/`Normal` only ever care about "a whole circle-shaped curve" or
|
||||
/// "a whole line" — an `Arc` collapses to its `.circle` here exactly like
|
||||
/// `whole_circle` does, so those two constraints' own inline matches don't
|
||||
/// need every Circle/Arc combination spelled out separately.
|
||||
enum CircleOrLine {
|
||||
Circle(GCircle),
|
||||
Line(GLine),
|
||||
Other,
|
||||
}
|
||||
|
||||
fn as_circle_or_line(g: EntityGeom) -> CircleOrLine {
|
||||
match g {
|
||||
EntityGeom::Circle(c) => CircleOrLine::Circle(c),
|
||||
EntityGeom::Arc(a) => CircleOrLine::Circle(a.circle),
|
||||
EntityGeom::Line(l) => CircleOrLine::Line(l),
|
||||
EntityGeom::Ellipse(_) => CircleOrLine::Other,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads `handle`'s live geometry and registers it as fresh, free (never
|
||||
/// `driven`) parameters in `sys` — every referenced entity is solved for,
|
||||
/// nothing is a priori fixed; per planegcs-style solvers, an under-
|
||||
@@ -91,6 +152,44 @@ fn register_entity(document: &acadrust::CadDocument, sys: &mut System, handle: H
|
||||
let rad = sys.add_param(c.radius, false);
|
||||
Some(EntityGeom::Circle(GCircle { center, rad }))
|
||||
}
|
||||
EntityType::Arc(a) => {
|
||||
// Full registration: center/radius, both angles, and real
|
||||
// start/end points seeded from acadrust's own `start_point`/
|
||||
// `end_point` (already consistent with center/radius/angle at
|
||||
// registration time) — `solve_scope` adds the arc-rules
|
||||
// constraints that keep them that way under solving. See this
|
||||
// module's doc comment.
|
||||
let center = GPoint::new(sys.add_param(a.center.x, false), sys.add_param(a.center.y, false));
|
||||
let rad = sys.add_param(a.radius, false);
|
||||
let start_angle = sys.add_param(a.start_angle, false);
|
||||
let end_angle = sys.add_param(a.end_angle, false);
|
||||
let start_seed = a.start_point();
|
||||
let end_seed = a.end_point();
|
||||
let start = GPoint::new(sys.add_param(start_seed.x, false), sys.add_param(start_seed.y, false));
|
||||
let end = GPoint::new(sys.add_param(end_seed.x, false), sys.add_param(end_seed.y, false));
|
||||
Some(EntityGeom::Arc(GArc { circle: GCircle { center, rad }, start, end, start_angle, end_angle }))
|
||||
}
|
||||
EntityType::Ellipse(el) => {
|
||||
// acadrust's `Ellipse` is center + major-axis vector (its length
|
||||
// is the major radius) + minor/major ratio; `ocs_gcs::geo::
|
||||
// Ellipse` is center + one focus + minor radius. Converting:
|
||||
// minor_radius = major_radius * ratio, then the focus distance
|
||||
// c follows from a² = b² + c² (standard ellipse identity), and
|
||||
// focus1 sits `c` along the major-axis direction from center.
|
||||
let major_radius = el.major_axis.length();
|
||||
let minor_radius = major_radius * el.minor_axis_ratio;
|
||||
let focus_dist = (major_radius * major_radius - minor_radius * minor_radius).max(0.0).sqrt();
|
||||
// Direction is meaningless once major_radius is ~0 (a
|
||||
// degenerate point-ellipse) — arbitrarily fall back to +X
|
||||
// rather than dividing by ~0 in `normalize`.
|
||||
let unit_major =
|
||||
if major_radius > 1e-9 { el.major_axis.normalize() } else { acadrust::types::Vector3::UNIT_X };
|
||||
let focus1_point = el.center + unit_major * focus_dist;
|
||||
let center = GPoint::new(sys.add_param(el.center.x, false), sys.add_param(el.center.y, false));
|
||||
let focus1 = GPoint::new(sys.add_param(focus1_point.x, false), sys.add_param(focus1_point.y, false));
|
||||
let radmin = sys.add_param(minor_radius, false);
|
||||
Some(EntityGeom::Ellipse(GEllipse { center, focus1, radmin }))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -142,11 +241,12 @@ fn build_constraint(
|
||||
|
||||
let whole_line = |sys: &mut System, cache: &mut HashMap<_, _>, r: SketchRef| match resolve_ref(document, sys, cache, r)? {
|
||||
EntityGeom::Line(l) => Some(l),
|
||||
EntityGeom::Circle(_) => None,
|
||||
EntityGeom::Circle(_) | EntityGeom::Arc(_) | EntityGeom::Ellipse(_) => None,
|
||||
};
|
||||
let whole_circle = |sys: &mut System, cache: &mut HashMap<_, _>, r: SketchRef| match resolve_ref(document, sys, cache, r)? {
|
||||
EntityGeom::Circle(circ) => Some(circ),
|
||||
EntityGeom::Line(_) => None,
|
||||
EntityGeom::Arc(a) => Some(a.circle),
|
||||
EntityGeom::Line(_) | EntityGeom::Ellipse(_) => None,
|
||||
};
|
||||
let point_ref = |sys: &mut System, cache: &mut HashMap<_, _>, r: SketchRef| {
|
||||
let marker = r.marker?;
|
||||
@@ -232,6 +332,18 @@ fn build_constraint(
|
||||
let zero = sys.add_param(0.0, true);
|
||||
vec![Rc::new(P2CDistance::new(circ, p, zero))]
|
||||
}
|
||||
// Same "distance to the underlying circle is zero" relation
|
||||
// as the `Circle` arm — doesn't restrict the point to
|
||||
// within the arc's own sweep, matching that same
|
||||
// pre-existing simplification for `Circle`.
|
||||
EntityGeom::Arc(a) => {
|
||||
let zero = sys.add_param(0.0, true);
|
||||
vec![Rc::new(P2CDistance::new(a.circle, p, zero))]
|
||||
}
|
||||
// `PointOnEllipse` (`ocs_gcs::constraints::conic`) isn't
|
||||
// wired up yet — deliberately deferred alongside
|
||||
// ellipse-tangency, same as this module's doc comment.
|
||||
EntityGeom::Ellipse(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
ConstraintKind::Symmetric => {
|
||||
@@ -276,6 +388,43 @@ fn build_constraint(
|
||||
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)),
|
||||
]
|
||||
}
|
||||
EntityGeom::Ellipse(el) => {
|
||||
let (cx, cy, fx, fy, b) = {
|
||||
let store = sys.store();
|
||||
(store.get(el.center.x), store.get(el.center.y), store.get(el.focus1.x), store.get(el.focus1.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.focus1.x, sys.add_param(fx, true), 1.0)),
|
||||
Rc::new(Equal::new(el.focus1.y, sys.add_param(fy, true), 1.0)),
|
||||
Rc::new(Equal::new(el.radmin, sys.add_param(b, true), 1.0)),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
ConstraintKind::Distance => {
|
||||
@@ -301,13 +450,50 @@ fn build_constraint(
|
||||
let target = sys.add_param(resolved, true);
|
||||
vec![Rc::new(Equal::new(circle.rad, target, 1.0))]
|
||||
}
|
||||
// Same math as `Radius`, just `radius = target / 2` instead of
|
||||
// `radius = target` — `Equal`'s `ratio` param already supports
|
||||
// this, matching how AutoCAD itself stores Diameter as the same
|
||||
// `ACRADIUSDIAMETERCONSTRAINT` class with a different mode byte
|
||||
// rather than a distinct one (`dwg_native_constraints.rs`).
|
||||
ConstraintKind::Diameter => {
|
||||
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, 0.5))]
|
||||
}
|
||||
// Signed X-only/Y-only component of the distance between two
|
||||
// points — `Difference::new(p1, p2, d)` is `p2 - p1 == d`, matching
|
||||
// AutoCAD's own `ACDISTANCECONSTRAINT` with a fixed-direction
|
||||
// vector rather than a distinct native class
|
||||
// (`dwg_native_constraints.rs`).
|
||||
ConstraintKind::DistanceX => {
|
||||
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(Difference::new(pa.x, pb.x, target))]
|
||||
}
|
||||
ConstraintKind::DistanceY => {
|
||||
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(Difference::new(pa.y, pb.y, target))]
|
||||
}
|
||||
ConstraintKind::Tangent => {
|
||||
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)) => {
|
||||
// An `Arc`'s `.circle` behaves identically to a plain `Circle`
|
||||
// for tangency math — normalize both refs down first so the
|
||||
// match below doesn't need every Circle/Arc combination
|
||||
// written out separately.
|
||||
match (as_circle_or_line(ga), as_circle_or_line(gb)) {
|
||||
(CircleOrLine::Circle(c1), CircleOrLine::Circle(c2)) => {
|
||||
let store = sys.store();
|
||||
let (x1, y1, r1) = (store.get(c1.center.x), store.get(c1.center.y), store.get(c1.rad));
|
||||
let (x2, y2, r2) = (store.get(c2.center.x), store.get(c2.center.y), store.get(c2.rad));
|
||||
@@ -319,7 +505,7 @@ fn build_constraint(
|
||||
let internal = center_dist < (r1 - r2).abs();
|
||||
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)) => {
|
||||
(CircleOrLine::Circle(circ), CircleOrLine::Line(line)) | (CircleOrLine::Line(line), CircleOrLine::Circle(circ)) => {
|
||||
let store = sys.store();
|
||||
let (cx, cy) = (store.get(circ.center.x), store.get(circ.center.y));
|
||||
let (x1, y1) = (store.get(line.p1.x), store.get(line.p1.y));
|
||||
@@ -336,11 +522,38 @@ fn build_constraint(
|
||||
let zero = sys.add_param(0.0, true);
|
||||
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).
|
||||
// Line-Line tangency has no meaning; either side being an
|
||||
// Ellipse falls here too (`PointOnEllipse`/tangency isn't
|
||||
// wired up yet).
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
// A circle's radius is always normal to its own tangent, so
|
||||
// "line normal to circle/arc" reduces to "line passes through the
|
||||
// circle's center" — the same `PointOnLine` primitive
|
||||
// `PointOnCurve` already uses for a point-on-line case.
|
||||
ConstraintKind::Normal => {
|
||||
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 (as_circle_or_line(ga), as_circle_or_line(gb)) {
|
||||
(CircleOrLine::Circle(circ), CircleOrLine::Line(line)) | (CircleOrLine::Line(line), CircleOrLine::Circle(circ)) => {
|
||||
vec![Rc::new(PointOnLine::new(circ.center, line))]
|
||||
}
|
||||
// Line-Line has no meaning here (that's `Perpendicular`);
|
||||
// Circle-Circle "normal" (orthogonal circles) needs a
|
||||
// different, not-yet-implemented relation.
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
ConstraintKind::ArcLength => {
|
||||
let Some(r) = c.refs.first() else { return Vec::new() };
|
||||
let Some(EntityGeom::Arc(arc)) = resolve_ref(document, 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(ArcLength::new(arc, target))]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,6 +599,22 @@ fn solve_scope(
|
||||
return None;
|
||||
}
|
||||
|
||||
// Arc rules (this module's doc comment): keep every registered arc's
|
||||
// `start`/`end` points consistent with its center/radius/angle,
|
||||
// unconditionally — planegcs itself adds these the moment an arc
|
||||
// exists, not only when some `SketchConstraint` happens to reference
|
||||
// its endpoint. Not tracked in `owner`: these are solver-internal
|
||||
// bookkeeping, never redundant with anything a user-facing constraint
|
||||
// could name, so there's no `ConstraintId` for them to report against.
|
||||
for geom in cache.values() {
|
||||
let EntityGeom::Arc(arc) = geom else { continue };
|
||||
let curve = Rc::new(*arc);
|
||||
sys.add_constraint(Rc::new(CurveValue::new(arc.start, arc.start.x, curve.clone(), arc.start_angle)));
|
||||
sys.add_constraint(Rc::new(CurveValue::new(arc.start, arc.start.y, curve.clone(), arc.start_angle)));
|
||||
sys.add_constraint(Rc::new(CurveValue::new(arc.end, arc.end.x, curve.clone(), arc.end_angle)));
|
||||
sys.add_constraint(Rc::new(CurveValue::new(arc.end, arc.end.y, curve, arc.end_angle)));
|
||||
}
|
||||
|
||||
let partitions = sys.partition();
|
||||
for sub in &partitions {
|
||||
solve_dl(sub, sys.store_mut());
|
||||
@@ -404,6 +633,15 @@ fn solve_scope(
|
||||
.map(|g| match g {
|
||||
EntityGeom::Line(_) => 4,
|
||||
EntityGeom::Circle(_) => 3,
|
||||
// Raw param count (center×2, rad, start×2, end×2, both
|
||||
// angles) — same "raw, not netted against its own
|
||||
// constraints" convention as every other arm here. The four
|
||||
// arc-rules `CurveValue` constraints (always present, added
|
||||
// just above) already reduce this to 5 *effective* DOF through
|
||||
// the normal `touched_free`/`diag.dof` accounting below, the
|
||||
// same way any other constraint would.
|
||||
EntityGeom::Arc(_) => 9,
|
||||
EntityGeom::Ellipse(_) => 5,
|
||||
})
|
||||
.sum();
|
||||
let touched_free: usize = partitions.iter().map(|s| s.p_size()).sum();
|
||||
@@ -458,6 +696,65 @@ fn solve_scope(
|
||||
results.push((handle, EntityType::Circle(updated)));
|
||||
}
|
||||
}
|
||||
(EntityType::Arc(a), EntityGeom::Arc(g)) => {
|
||||
// Center/radius/angles only — `start`/`end` are derived
|
||||
// (acadrust's `Arc` has no separate stored fields for them;
|
||||
// `start_point()`/`end_point()` compute them from these
|
||||
// same four), so nothing further needs writing back for
|
||||
// them specifically.
|
||||
let (cx, cy, r) = (store.get(g.circle.center.x), store.get(g.circle.center.y), store.get(g.circle.rad));
|
||||
let (new_start_angle, new_end_angle) = (store.get(g.start_angle), store.get(g.end_angle));
|
||||
if (cx - a.center.x).abs() > MOVE_EPS
|
||||
|| (cy - a.center.y).abs() > MOVE_EPS
|
||||
|| (r - a.radius).abs() > MOVE_EPS
|
||||
|| (new_start_angle - a.start_angle).abs() > MOVE_EPS
|
||||
|| (new_end_angle - a.end_angle).abs() > MOVE_EPS
|
||||
{
|
||||
let mut updated = a.clone();
|
||||
updated.center.x = cx;
|
||||
updated.center.y = cy;
|
||||
updated.radius = r;
|
||||
updated.start_angle = new_start_angle;
|
||||
updated.end_angle = new_end_angle;
|
||||
results.push((handle, EntityType::Arc(updated)));
|
||||
}
|
||||
}
|
||||
(EntityType::Ellipse(el), EntityGeom::Ellipse(g)) => {
|
||||
// Converting back to acadrust's center + major-axis-vector +
|
||||
// minor/major-ratio parametrization — the inverse of
|
||||
// `register_entity`'s `Ellipse` arm. `rad_maj_at` (already
|
||||
// provided by `ocs_gcs::geo::Ellipse`) does the a²=b²+c²
|
||||
// algebra; only the major-axis *direction* needs deriving
|
||||
// here, from the (possibly moved) focus relative to center.
|
||||
let (cx, cy) = (store.get(g.center.x), store.get(g.center.y));
|
||||
let (fx, fy) = (store.get(g.focus1.x), store.get(g.focus1.y));
|
||||
let minor_radius = store.get(g.radmin);
|
||||
let (major_radius, _) = g.rad_maj_at(store, None);
|
||||
let focus_dist = ((fx - cx).powi(2) + (fy - cy).powi(2)).sqrt();
|
||||
// Direction is ill-defined once the ellipse is (near)
|
||||
// circular — keep whatever direction it already had rather
|
||||
// than snapping to an arbitrary axis.
|
||||
let unit_major = if focus_dist > 1e-9 {
|
||||
acadrust::types::Vector3::new(fx - cx, fy - cy, 0.0).normalize()
|
||||
} else {
|
||||
el.major_axis.normalize()
|
||||
};
|
||||
let new_major_axis = unit_major * major_radius;
|
||||
let new_ratio = if major_radius > 1e-9 { minor_radius / major_radius } else { 0.0 };
|
||||
if (cx - el.center.x).abs() > MOVE_EPS
|
||||
|| (cy - el.center.y).abs() > MOVE_EPS
|
||||
|| (new_major_axis.x - el.major_axis.x).abs() > MOVE_EPS
|
||||
|| (new_major_axis.y - el.major_axis.y).abs() > MOVE_EPS
|
||||
|| (new_ratio - el.minor_axis_ratio).abs() > MOVE_EPS
|
||||
{
|
||||
let mut updated = el.clone();
|
||||
updated.center.x = cx;
|
||||
updated.center.y = cy;
|
||||
updated.major_axis = new_major_axis;
|
||||
updated.minor_axis_ratio = new_ratio;
|
||||
results.push((handle, EntityType::Ellipse(updated)));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user