Files
OneCAD-Draft/src/scene/dwg_native_constraints.rs
T
ramox81andClaude Fable 5.1 5fab97a7d0 Aligned constraint: axis lines, zero collapse, Properties form/reference/description, screen-sized text
Point & line and 2Lines take a text baseline or an ellipse axis as the
line; a zero value folds the points onto the first one; 2Lines turns the
second line about its start. The Properties Constraint section edits
Constraint Form (Dynamic on the *ADSK_CONSTRAINTS layer, Annotational on
the current layer), Reference (driven constraint in parentheses whose
parameter follows the measurement) and Description (stored with the
parameter and the native variable). Dynamic dimensions keep a 12 px text
and 8 px arrows at any zoom, drop trailing zeros and show formulas as
`fx: len=d1*2`; the hidden constraints layer stays out of the layer lists.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 16:04:26 +03:00

4530 lines
174 KiB
Rust

//! Maps parametric constraints to the drawing format's associative object graph.
use acadrust::objects::{
Assoc2dConstraintGroup, AssocAction, AssocActionDependency, AssocConstraintNode,
AssocConstraintNodeData, AssocDependency, AssocDimDependencyBody, AssocEvalValue,
AssocEvalVariant,
AssocGeomDependency, AssocNetwork, AssocPersistentSubentId, AssocValueDependency,
AssocVariable, AssociativeData, AssociativeObject, ObjectType,
};
use acadrust::types::{Handle, Vector3};
use acadrust::{CadDocument, EntityType};
use rustc_hash::FxHashMap;
use super::named_parameters::{DrivingValue, ParameterTable};
use super::parametric_constraints::{
distance_direction_type, ConstraintKind, NativeConstraintOrigin, ParametricConstraint,
ParametricConstraintSet, ParametricRef, ParametricScope,
};
use super::Scene;
/// Default sub-dictionary key used for an associative network.
const NETWORK_DICTIONARY_KEY: &str = "ACAD_ASSOCNETWORK";
/// `AcConstraintGroupNode::GroupNodeId::kNullGroupNodeId` — node id 0 is
/// reserved as "no node", so real node ids start at 1.
const FIRST_NODE_ID: i32 = 1;
/// Native implicit-point type values.
mod implicit_point_type {
pub const START: u8 = 0;
pub const END: u8 = 1;
#[allow(dead_code)] // reserved for ParametricRef marker -2, not wired up yet
pub const MID: u8 = 2;
pub const CENTER: u8 = 3;
pub const DEFINE: u8 = 4;
}
/// One entity's constraint-group presence: its geometry node's id, plus
/// whichever `ImplicitPoint` nodes have been created for it so far, keyed
/// by our own `ParametricRef::marker` convention so a second constraint
/// referencing the same point reuses the same node instead of duplicating it.
#[derive(Default)]
struct EntityNodes {
geometry_node_id: i32,
points: FxHashMap<i32, i32>,
segments: FxHashMap<usize, i32>,
axes: FxHashMap<i32, i32>,
}
/// Builds one scope's `Assoc2dConstraintGroup` graph incrementally,
/// allocating node ids and deduplicating geometry/point nodes per entity.
struct GroupBuilder<'a> {
document: &'a CadDocument,
nodes: Vec<AssocConstraintNode>,
next_node_id: i32,
entities: FxHashMap<Handle, EntityNodes>,
/// Real entity handles that ended up with a geometry node — what
/// `AssocGeomDependency` objects get created for, in first-touch order.
referenced_entities: Vec<Handle>,
}
impl<'a> GroupBuilder<'a> {
fn new(document: &'a CadDocument) -> Self {
Self {
document,
nodes: Vec::new(),
next_node_id: FIRST_NODE_ID,
entities: FxHashMap::default(),
referenced_entities: Vec::new(),
}
}
fn alloc_node_id(&mut self) -> i32 {
let id = self.next_node_id;
self.next_node_id += 1;
id
}
fn push_node(&mut self, node_id: i32, class_name: &'static str, data: AssocConstraintNodeData) {
let status = u8::from(matches!(
&data,
AssocConstraintNodeData::ImplicitPoint { .. }
| AssocConstraintNodeData::Point { .. }
| AssocConstraintNodeData::Line { .. }
| AssocConstraintNodeData::BoundedLine { .. }
| AssocConstraintNodeData::Circle { .. }
| AssocConstraintNodeData::Arc { .. }
| AssocConstraintNodeData::Ellipse { .. }
| AssocConstraintNodeData::BoundedEllipse { .. }
| AssocConstraintNodeData::Spline { .. }
));
self.nodes.push(AssocConstraintNode {
node_id,
status,
connections: Vec::new(),
class_name: class_name.to_string(),
registry_flag: false,
data,
});
}
/// Returns the whole-geometry node for an entity, creating it once.
fn geometry_node(&mut self, handle: Handle) -> Option<i32> {
if let Some(existing) = self.entities.get(&handle) {
if existing.geometry_node_id != 0 {
return Some(existing.geometry_node_id);
}
}
let entity = self.document.get_entity(handle)?;
let node_id = self.alloc_node_id();
match entity {
EntityType::Point(point) => {
self.push_node(
node_id,
"AcConstrainedPoint",
AssocConstraintNodeData::Point {
geometry_dependency: Handle::NULL,
geometry_node_id: node_id,
point: Some(point.location),
},
);
}
EntityType::Insert(insert) => self.push_node(
node_id,
"AcConstrainedPoint",
AssocConstraintNodeData::Point {
geometry_dependency: Handle::NULL,
geometry_node_id: node_id,
point: Some(insert.insert_point),
},
),
EntityType::Text(text) => self.push_node(
node_id,
"AcConstrainedPoint",
AssocConstraintNodeData::Point {
geometry_dependency: Handle::NULL,
geometry_node_id: node_id,
point: Some(text.insertion_point),
},
),
EntityType::MText(text) => self.push_node(
node_id,
"AcConstrainedPoint",
AssocConstraintNodeData::Point {
geometry_dependency: Handle::NULL,
geometry_node_id: node_id,
point: Some(text.insertion_point),
},
),
EntityType::AttributeDefinition(attribute) => self.push_node(
node_id,
"AcConstrainedPoint",
AssocConstraintNodeData::Point {
geometry_dependency: Handle::NULL,
geometry_node_id: node_id,
point: Some(attribute.insertion_point),
},
),
EntityType::AttributeEntity(attribute) => self.push_node(
node_id,
"AcConstrainedPoint",
AssocConstraintNodeData::Point {
geometry_dependency: Handle::NULL,
geometry_node_id: node_id,
point: Some(attribute.insertion_point),
},
),
EntityType::Table(table) => self.push_node(
node_id,
"AcConstrainedPoint",
AssocConstraintNodeData::Point {
geometry_dependency: Handle::NULL,
geometry_node_id: node_id,
point: Some(table.insertion_point),
},
),
EntityType::Line(l) => {
let point = l.start;
let direction = (l.end - l.start).normalize();
self.push_node(
node_id,
"AcDbAssocGeomDependency", // placeholder, overwritten below
AssocConstraintNodeData::None,
);
let last = self.nodes.last_mut().expect("just pushed");
last.class_name = "AcConstrainedBoundedLine".to_string();
last.status = 1;
last.data = AssocConstraintNodeData::BoundedLine {
geometry_dependency: Handle::NULL,
geometry_node_id: node_id,
point,
direction,
is_ray: false,
start_point: l.start,
end_point: l.end,
};
}
EntityType::Ray(ray) => {
self.push_node(
node_id,
"AcConstrainedBoundedLine",
AssocConstraintNodeData::BoundedLine {
geometry_dependency: Handle::NULL,
geometry_node_id: node_id,
point: ray.base_point,
direction: ray.direction,
is_ray: true,
start_point: ray.base_point,
end_point: ray.base_point + ray.direction,
},
);
}
EntityType::XLine(line) => {
self.push_node(
node_id,
"AcConstrainedLine",
AssocConstraintNodeData::Line {
geometry_dependency: Handle::NULL,
geometry_node_id: node_id,
point: line.base_point,
direction: line.direction,
},
);
}
EntityType::Circle(c) => {
let (axis_x, _axis_y) =
crate::scene::view::transform::ocs_axes((c.normal.x, c.normal.y, c.normal.z));
self.push_node(
node_id,
"AcConstrainedCircle",
AssocConstraintNodeData::Circle {
geometry_dependency: Handle::NULL,
geometry_node_id: node_id,
center: c.center,
normal: c.normal,
direction: Vector3::new(axis_x.0, axis_x.1, axis_x.2),
radius: c.radius,
start_parameter: 0.0,
end_parameter: std::f64::consts::TAU,
reserved: 0.0,
},
);
}
EntityType::Arc(a) => {
let (axis_x, _axis_y) =
crate::scene::view::transform::ocs_axes((a.normal.x, a.normal.y, a.normal.z));
let direction = Vector3::new(axis_x.0, axis_x.1, axis_x.2);
let start_point = a.center
+ Vector3::new(axis_x.0, axis_x.1, axis_x.2) * (a.radius * a.start_angle.cos())
+ Vector3::new(-axis_x.1, axis_x.0, 0.0) * (a.radius * a.start_angle.sin());
let end_point = a.center
+ Vector3::new(axis_x.0, axis_x.1, axis_x.2) * (a.radius * a.end_angle.cos())
+ Vector3::new(-axis_x.1, axis_x.0, 0.0) * (a.radius * a.end_angle.sin());
self.push_node(
node_id,
"AcConstrainedArc",
AssocConstraintNodeData::Arc {
geometry_dependency: Handle::NULL,
geometry_node_id: node_id,
center: a.center,
normal: a.normal,
direction,
radius: a.radius,
start_parameter: a.start_angle,
end_parameter: a.end_angle,
reserved: 0.0,
start_point,
end_point,
},
);
}
EntityType::Ellipse(e) => {
let major_length = e.major_axis.length();
let major = if major_length > f64::EPSILON {
e.major_axis / major_length
} else {
Vector3::UNIT_X
};
let normal = e.normal.normalize();
let minor = Vector3::new(
normal.y * major.z - normal.z * major.y,
normal.z * major.x - normal.x * major.z,
normal.x * major.y - normal.y * major.x,
);
let point_at = |parameter: f64| {
e.center
+ e.major_axis * parameter.cos()
+ minor * (major_length * e.minor_axis_ratio * parameter.sin())
};
let data = if e.is_full() {
AssocConstraintNodeData::Ellipse {
geometry_dependency: Handle::NULL,
geometry_node_id: node_id,
center: e.center,
major_axis: e.major_axis,
axis_ratio: e.minor_axis_ratio,
}
} else {
AssocConstraintNodeData::BoundedEllipse {
geometry_dependency: Handle::NULL,
geometry_node_id: node_id,
center: e.center,
major_axis: e.major_axis,
axis_ratio: e.minor_axis_ratio,
start_point: point_at(e.start_parameter),
end_point: point_at(e.end_parameter),
}
};
let class_name = if e.is_full() {
"AcConstrainedEllipse"
} else {
"AcConstrainedBoundedEllipse"
};
self.push_node(node_id, class_name, data);
}
EntityType::Spline(spline) => {
let curve = crate::entities::spline::nurbs3(spline)?;
let weights = curve.weights();
let rational = spline.flags.rational
|| weights.iter().any(|weight| (*weight - 1.0).abs() > 1.0e-12);
let control_points: Vec<_> = curve
.control_points()
.iter()
.map(|point| Vector3::new(point[0], point[1], point[2]))
.collect();
let mut implicit_point_ids = Vec::with_capacity(control_points.len());
self.push_node(
node_id,
"AcConstrainedSpline",
AssocConstraintNodeData::Spline {
geometry_dependency: Handle::NULL,
geometry_node_id: node_id,
rational,
periodic: spline.flags.periodic,
degree: curve.degree() as i32,
knot_tolerance: spline.knot_tolerance.max(1.0e-10),
knot_physical_length: curve.knots().len() as i32,
knot_grow_length: 8,
knots: curve.knots().to_vec(),
weight_physical_length: if rational { weights.len() as i32 } else { 0 },
weight_grow_length: 8,
weights: if rational {
weights.to_vec()
} else {
Vec::new()
},
control_point_physical_length: control_points.len() as i32,
control_point_grow_length: 8,
control_points,
implicit_point_ids: Vec::new(),
},
);
for point_index in 0..curve.control_points().len() {
let point_id = self.alloc_node_id();
self.push_node(
point_id,
"AcConstrainedImplicitPoint",
AssocConstraintNodeData::ImplicitPoint {
geometry_dependency: Handle::NULL,
geometry_node_id: point_id,
point: None,
point_type: implicit_point_type::DEFINE,
point_index: point_index as i32,
curve_id: node_id,
},
);
implicit_point_ids.push(point_id);
}
if let Some(AssocConstraintNode {
data:
AssocConstraintNodeData::Spline {
implicit_point_ids: ids,
..
},
..
}) = self.nodes.iter_mut().find(|node| node.node_id == node_id)
{
*ids = implicit_point_ids.clone();
}
let points = &mut self.entities.entry(handle).or_default().points;
if let Some(first) = implicit_point_ids.first() {
points.insert(0, *first);
}
if let Some(last) = implicit_point_ids.last() {
points.insert(1, *last);
}
}
_ => return None,
}
self.entities.entry(handle).or_default().geometry_node_id = node_id;
if !self.referenced_entities.contains(&handle) {
self.referenced_entities.push(handle);
}
Some(node_id)
}
/// Returns a derived line node for a text baseline or ellipse axis. These
/// are real native curve nodes tied to the source entity dependency, so
/// the selected direction survives save/reopen instead of collapsing to
/// the entity insertion/center point.
fn directional_axis_node(&mut self, reference: ParametricRef) -> Option<i32> {
let marker = reference.marker?;
reference.directional_axis()?;
if let Some(node_id) = self
.entities
.get(&reference.entity)
.and_then(|entity| entity.axes.get(&marker))
{
return Some(*node_id);
}
let entity = self.document.get_entity(reference.entity)?;
let [start, end] =
super::parametric_constraints::directional_axis_endpoints(entity, reference)?;
let delta = end - start;
if delta.length_squared() <= 1.0e-24 {
return None;
}
let node_id = self.alloc_node_id();
self.push_node(
node_id,
"AcConstrainedBoundedLine",
AssocConstraintNodeData::BoundedLine {
geometry_dependency: Handle::NULL,
geometry_node_id: node_id,
point: start,
direction: delta.normalize(),
is_ray: false,
start_point: start,
end_point: end,
},
);
self.entities
.entry(reference.entity)
.or_default()
.axes
.insert(marker, node_id);
if !self.referenced_entities.contains(&reference.entity) {
self.referenced_entities.push(reference.entity);
}
Some(node_id)
}
/// Returns the point-node id for a line endpoint or curve center,
/// creating it the first time the marker is referenced.
fn point_node(&mut self, handle: Handle, marker: i32) -> Option<i32> {
if let Some(existing) = self
.entities
.get(&handle)
.and_then(|e| e.points.get(&marker))
{
return Some(*existing);
}
let segment_midpoint = ParametricRef::point(handle, marker).segment_midpoint_index();
let segment_center = ParametricRef::point(handle, marker).segment_center_index();
let polyline = matches!(
self.document.get_entity(handle),
Some(EntityType::LwPolyline(_) | EntityType::Polyline2D(_))
);
let (curve_id, point_type) = if let Some(segment) = segment_center {
(
self.segment_node(handle, segment)?,
implicit_point_type::CENTER,
)
} else if let Some(segment) = segment_midpoint {
(
self.segment_node(handle, segment)?,
implicit_point_type::MID,
)
} else if polyline && marker >= 0 {
let entity = self.document.get_entity(handle)?;
let points = super::dimension_assoc::source_points(entity);
let closed = match entity {
EntityType::LwPolyline(polyline) => polyline.is_closed,
EntityType::Polyline2D(polyline) => polyline.is_closed(),
_ => false,
};
let vertex = marker as usize;
if vertex >= points.len() {
return None;
}
let (segment, point_type) = if vertex + 1 < points.len() || closed {
(vertex, implicit_point_type::START)
} else {
(vertex.checked_sub(1)?, implicit_point_type::END)
};
(self.segment_node(handle, segment)?, point_type)
} else {
let curve_id = self.geometry_node(handle)?;
if let Some(existing) = self
.entities
.get(&handle)
.and_then(|entity| entity.points.get(&marker))
{
return Some(*existing);
}
let point_type = match marker {
0 => implicit_point_type::START,
1 => implicit_point_type::END,
-2 => implicit_point_type::MID,
-3 => implicit_point_type::CENTER,
_ => return None,
};
(curve_id, point_type)
};
let node_id = self.alloc_node_id();
self.push_node(
node_id,
"AcConstrainedImplicitPoint",
AssocConstraintNodeData::ImplicitPoint {
geometry_dependency: Handle::NULL,
geometry_node_id: node_id,
point: None,
point_type,
point_index: -1,
curve_id,
},
);
let mut relations = vec![if point_type == implicit_point_type::CENTER {
"AcCenterPointConstraint"
} else {
"AcPointCurveConstraint"
}];
if point_type == implicit_point_type::MID {
relations.push("AcMidPointConstraint");
}
for class_name in relations {
let relation_id = self.alloc_node_id();
self.push_node(
relation_id,
class_name,
AssocConstraintNodeData::Geometrical {
owner_id: 0,
is_implied: true,
is_active: true,
},
);
self.connect(relation_id, node_id);
self.connect(relation_id, curve_id);
}
self.entities
.entry(handle)
.or_default()
.points
.insert(marker, node_id);
Some(node_id)
}
/// The node id `ParametricRef` resolves to: a point node for a marked
/// reference, the whole geometry node otherwise.
fn ref_node(&mut self, r: ParametricRef) -> Option<i32> {
if r.directional_axis().is_some() {
return self.directional_axis_node(r);
}
if r.segment_midpoint_index().is_some() {
return self.point_node(r.entity, r.marker?);
}
if r.segment_center_index().is_some() {
return self.point_node(r.entity, r.marker?);
}
if let Some(segment) = r.segment_index() {
return self.segment_node(r.entity, segment);
}
if r.marker == Some(0)
&& matches!(
self.document.get_entity(r.entity),
Some(
EntityType::Point(_)
| EntityType::Insert(_)
| EntityType::Text(_)
| EntityType::MText(_)
| EntityType::AttributeDefinition(_)
| EntityType::AttributeEntity(_)
| EntityType::Table(_)
)
)
{
return self.geometry_node(r.entity);
}
match r.marker {
Some(marker) => self.point_node(r.entity, marker),
None => self.geometry_node(r.entity),
}
}
fn segment_node(&mut self, handle: Handle, index: usize) -> Option<i32> {
if let Some(node_id) = self
.entities
.get(&handle)
.and_then(|entity| entity.segments.get(&index))
{
return Some(*node_id);
}
let entity = self.document.get_entity(handle)?;
let points = super::dimension_assoc::source_points(entity);
let (closed, bulge) = match entity {
EntityType::LwPolyline(polyline) => {
(polyline.is_closed, polyline.vertices.get(index)?.bulge)
}
EntityType::Polyline2D(polyline) => {
(polyline.is_closed(), polyline.vertices.get(index)?.bulge)
}
_ => return None,
};
let start = *points.get(index)?;
let end = if index + 1 < points.len() {
points[index + 1]
} else if closed {
*points.first()?
} else {
return None;
};
let node_id = self.alloc_node_id();
if bulge.abs() <= 1e-9 {
self.push_node(
node_id,
"AcConstrainedBoundedLine",
AssocConstraintNodeData::BoundedLine {
geometry_dependency: Handle::NULL,
geometry_node_id: node_id,
point: start,
direction: (end - start).normalize(),
is_ray: false,
start_point: start,
end_point: end,
},
);
} else {
let arc = cadkernel::geom2d::BulgeArc::from_bulge(
[start.x, start.y],
[end.x, end.y],
bulge,
)?;
self.push_node(
node_id,
"AcConstrainedArc",
AssocConstraintNodeData::Arc {
geometry_dependency: Handle::NULL,
geometry_node_id: node_id,
center: Vector3::new(arc.center[0], arc.center[1], start.z),
normal: Vector3::UNIT_Z,
direction: Vector3::UNIT_X,
radius: arc.radius,
start_parameter: arc.start_angle,
end_parameter: arc.start_angle + arc.sweep,
reserved: 0.0,
start_point: start,
end_point: end,
},
);
}
self.entities
.entry(handle)
.or_default()
.segments
.insert(index, node_id);
if !self.referenced_entities.contains(&handle) {
self.referenced_entities.push(handle);
}
Some(node_id)
}
fn datum_line(&mut self, direction: Vector3) -> i32 {
let node_id = self.alloc_node_id();
self.push_node(
node_id,
"AcConstrainedDatumLine",
AssocConstraintNodeData::Line {
geometry_dependency: Handle::NULL,
geometry_node_id: node_id,
point: Vector3::ZERO,
direction: if direction.length_squared() > 1.0e-24 {
direction.normalize()
} else {
Vector3::UNIT_X
},
},
);
node_id
}
/// Records an undirected edge on both nodes.
fn connect(&mut self, a: i32, b: i32) {
if let Some(node) = self.nodes.iter_mut().find(|n| n.node_id == a) {
node.connections.push(b);
}
if let Some(node) = self.nodes.iter_mut().find(|n| n.node_id == b) {
node.connections.push(a);
}
}
}
/// Class name for a plain `Geometrical`-shaped constraint.
fn geometrical_class_name(kind: ConstraintKind) -> Option<&'static str> {
match kind {
ConstraintKind::Coincident => Some("AcPointCoincidenceConstraint"),
ConstraintKind::Perpendicular => Some("AcPerpendicularConstraint"),
ConstraintKind::Tangent => Some("AcTangentConstraint"),
// the native format's own `GeomConstraintType` enum (native SDK'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"),
ConstraintKind::Fixed => Some("AcFixedConstraint"),
ConstraintKind::Midpoint => Some("AcMidPointConstraint"),
ConstraintKind::PointOnCurve => Some("AcPointCurveConstraint"),
ConstraintKind::Symmetric => Some("AcSymmetricConstraint"),
// Also a plain `Geometrical`-shaped node in the native format'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` needs the resolved entity types, so the caller handles it.
ConstraintKind::Equal
| ConstraintKind::Smooth
| ConstraintKind::Horizontal
| ConstraintKind::Vertical
| ConstraintKind::Parallel
| ConstraintKind::Distance
| ConstraintKind::Angle
| ConstraintKind::Radius
| ConstraintKind::Diameter
| ConstraintKind::DistanceX
| ConstraintKind::DistanceY
| ConstraintKind::DistanceDirected
| ConstraintKind::Angle3Point
| ConstraintKind::RigidSet => None,
}
}
/// `Diameter`/`DistanceX`/`DistanceY` reuse `Radius`'/`Distance`'s own DWG
/// class — native implementations 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
| ConstraintKind::DistanceX
| ConstraintKind::DistanceY
| ConstraintKind::DistanceDirected => "AcDistanceConstraint",
ConstraintKind::Angle => "AcAngleConstraint",
ConstraintKind::Angle3Point => "Ac3PointAngleConstraint",
ConstraintKind::Radius | ConstraintKind::Diameter => "AcRadiusDiameterConstraint",
_ => "",
}
}
/// Builds one `AssocConstraintNode` for `constraint`, returning its node id
/// so the caller can track which ones still need a `value_dependency`
/// patched in once handles can be allocated (pass 2, `Allocator` — building
/// node *shape* only needs read access to resolve `Equal`'s line-vs-circle
/// split, `AssocGeomDependency`/`AssocValueDependency` handles need mutable
/// access to the same document, so this has to be two passes). `None` if
/// this constraint's refs don't resolve to nodes this pass supports
/// (mirrors `parametric_solve::build_constraint`'s own "skip, don't error"
/// contract for an unbuildable constraint).
fn constraint_node(
builder: &mut GroupBuilder,
document: &CadDocument,
constraint: &ParametricConstraint,
needs_value_dependency: &mut Vec<(i32, Option<DrivingValue>)>,
) -> Option<i32> {
let refs: &[ParametricRef] = &constraint.refs;
if constraint.kind == ConstraintKind::Smooth {
let (first, second, second_curve_ref) = match refs {
[first, second] => (*first, *second, ParametricRef::whole(second.entity)),
[first, second, second_curve] if second.entity == second_curve.entity => {
(*first, *second, *second_curve)
}
_ => return None,
};
let endpoint_parameter = |reference: ParametricRef| {
let entity = document.get_entity(reference.entity)?;
let EntityType::Spline(spline) = entity else {
return Some(None);
};
if spline.flags.closed || spline.flags.periodic {
return None;
}
let curve = crate::entities::spline::nurbs3(spline)?;
let value = match reference.marker {
Some(0) => curve.knots().get(curve.degree()).copied(),
Some(1) => curve
.knots()
.get(curve.knots().len().checked_sub(curve.degree() + 1)?)
.copied(),
_ => None,
}?;
Some(Some(value))
};
let first_parameter = endpoint_parameter(first)?;
let second_parameter = endpoint_parameter(second)?;
if first_parameter.is_none() && second_parameter.is_none() {
return None;
}
let first_curve = builder.geometry_node(first.entity)?;
let second_curve = builder.ref_node(second_curve_ref)?;
let first_point = builder.ref_node(first)?;
let second_point = builder.ref_node(second)?;
let coincidence = builder.alloc_node_id();
builder.push_node(
coincidence,
"AcPointCoincidenceConstraint",
AssocConstraintNodeData::Geometrical {
owner_id: 0,
is_implied: false,
is_active: constraint.enabled,
},
);
builder.connect(coincidence, first_point);
builder.connect(coincidence, second_point);
let add_help_pair = |builder: &mut GroupBuilder, constraint_class| {
let mut add_help = |value: Option<f64>| {
value.map(|value| {
let node_id = builder.alloc_node_id();
builder.push_node(
node_id,
"AcHelpParameter",
AssocConstraintNodeData::HelpParameter {
value,
reserved: false,
},
);
node_id
})
};
let first_help = add_help(first_parameter);
let second_help = add_help(second_parameter);
let child = builder.alloc_node_id();
builder.push_node(
child,
constraint_class,
AssocConstraintNodeData::Geometrical {
owner_id: 0,
is_implied: false,
is_active: constraint.enabled,
},
);
for target in [
Some(first_curve),
Some(second_curve),
first_help,
second_help,
]
.into_iter()
.flatten()
{
builder.connect(child, target);
}
if let Some(first_help) = first_help {
builder.connect(first_help, first_curve);
}
if let Some(second_help) = second_help {
builder.connect(second_help, second_curve);
}
child
};
let tangent = add_help_pair(builder, "AcTangentConstraint");
let equal_curvature = add_help_pair(builder, "AcEqualCurvatureConstraint");
let node_id = builder.alloc_node_id();
builder.push_node(
node_id,
"AcG2SmoothConstraint",
AssocConstraintNodeData::Composite {
owner_id: 0,
is_implied: false,
is_active: constraint.enabled,
owned_constraint_ids: vec![coincidence, tangent, equal_curvature],
},
);
for child_id in [coincidence, tangent, equal_curvature] {
if let Some(AssocConstraintNode {
data: AssocConstraintNodeData::Geometrical { owner_id, .. },
..
}) = builder
.nodes
.iter_mut()
.find(|node| node.node_id == child_id)
{
*owner_id = node_id;
}
}
return Some(node_id);
}
if let Some(class_name) = geometrical_class_name(constraint.kind) {
let target_refs: Vec<i32> = refs.iter().filter_map(|r| builder.ref_node(*r)).collect();
if target_refs.len() != refs.len() {
return None;
}
let node_id = builder.alloc_node_id();
builder.push_node(
node_id,
class_name,
AssocConstraintNodeData::Geometrical {
owner_id: 0,
is_implied: false,
is_active: constraint.enabled,
},
);
for target in &target_refs {
builder.connect(node_id, *target);
}
return Some(node_id);
}
match constraint.kind {
ConstraintKind::Horizontal | ConstraintKind::Vertical => {
if !matches!(refs.len(), 1 | 2) {
return None;
}
let targets: Vec<_> = refs
.iter()
.map(|reference| builder.ref_node(*reference))
.collect::<Option<_>>()?;
let horizontal = constraint.kind == ConstraintKind::Horizontal;
let fallback = if horizontal {
Vector3::UNIT_X
} else {
Vector3::UNIT_Y
};
let datum = builder.datum_line(constraint.axis_direction.unwrap_or(fallback));
let node_id = builder.alloc_node_id();
builder.push_node(
node_id,
if horizontal {
"AcHorizontalConstraint"
} else {
"AcVerticalConstraint"
},
AssocConstraintNodeData::Parallel {
owner_id: 0,
is_implied: false,
is_active: constraint.enabled,
datum_line_index: Some(datum),
},
);
for target in targets {
builder.connect(node_id, target);
}
Some(node_id)
}
ConstraintKind::Parallel => {
let [a, b] = refs else { return None };
let (na, nb) = (builder.ref_node(*a)?, builder.ref_node(*b)?);
let node_id = builder.alloc_node_id();
builder.push_node(
node_id,
"AcParallelConstraint",
AssocConstraintNodeData::Parallel {
owner_id: 0,
is_implied: false,
is_active: constraint.enabled,
datum_line_index: None,
},
);
builder.connect(node_id, na);
builder.connect(node_id, nb);
Some(node_id)
}
ConstraintKind::Equal => {
let [a, b] = refs else { return None };
let (na, nb) = (builder.ref_node(*a)?, builder.ref_node(*b)?);
let is_straight = |reference: &ParametricRef| {
matches!(
document.get_entity(reference.entity),
Some(EntityType::Line(_))
) || (reference.segment_index().is_some()
&& matches!(
document.get_entity(reference.entity),
Some(EntityType::LwPolyline(_) | EntityType::Polyline2D(_))
))
};
let both_lines = is_straight(a) && is_straight(b);
let class_name = if both_lines {
"AcEqualLengthConstraint"
} else {
"AcEqualRadiusConstraint"
};
let node_id = builder.alloc_node_id();
builder.push_node(
node_id,
class_name,
AssocConstraintNodeData::Geometrical {
owner_id: 0,
is_implied: false,
is_active: constraint.enabled,
},
);
builder.connect(node_id, na);
builder.connect(node_id, nb);
Some(node_id)
}
ConstraintKind::Distance
| ConstraintKind::DistanceX
| ConstraintKind::DistanceY
| ConstraintKind::DistanceDirected
| ConstraintKind::Angle
| ConstraintKind::Angle3Point
| ConstraintKind::Radius
| ConstraintKind::Diameter => {
let target_refs: Vec<i32> = match constraint.kind {
ConstraintKind::Radius | ConstraintKind::Diameter => {
vec![builder.ref_node(*refs.first()?)?]
}
ConstraintKind::Angle3Point => {
let [a, vertex, b] = refs else { return None };
vec![
builder.ref_node(*a)?,
builder.ref_node(*vertex)?,
builder.ref_node(*b)?,
]
}
ConstraintKind::DistanceDirected => {
if !matches!(refs.len(), 2 | 3) {
return None;
}
refs.iter()
.map(|reference| builder.ref_node(*reference))
.collect::<Option<_>>()?
}
_ => {
let [a, b] = refs else { return None };
vec![builder.ref_node(*a)?, builder.ref_node(*b)?]
}
};
let owner_id = 0;
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,
is_active: constraint.enabled,
value_dependency: Handle::NULL,
dimension_dependency: Handle::NULL,
direction_type: distance_direction_type::UNDIRECTED,
distance: None,
},
// X-only/Y-only: `kFixedDirection` with the fixed unit
// vector the distance is measured along — the native format's own
// representation of DistanceX/DistanceY, not a distinct
// constraint class.
ConstraintKind::DistanceX => AssocConstraintNodeData::Distance {
owner_id,
is_implied: false,
is_active: constraint.enabled,
value_dependency: Handle::NULL,
dimension_dependency: Handle::NULL,
direction_type: distance_direction_type::FIXED,
distance: Some(Vector3::new(1.0, 0.0, 0.0)),
},
ConstraintKind::DistanceY => AssocConstraintNodeData::Distance {
owner_id,
is_implied: false,
is_active: constraint.enabled,
value_dependency: Handle::NULL,
dimension_dependency: Handle::NULL,
direction_type: distance_direction_type::FIXED,
distance: Some(Vector3::new(0.0, 1.0, 0.0)),
},
ConstraintKind::DistanceDirected => AssocConstraintNodeData::Distance {
owner_id,
is_implied: false,
is_active: constraint.enabled,
value_dependency: Handle::NULL,
dimension_dependency: Handle::NULL,
direction_type: constraint.distance_direction_type,
distance: constraint.distance_direction,
},
ConstraintKind::Angle => AssocConstraintNodeData::Angle {
owner_id,
is_implied: false,
is_active: constraint.enabled,
value_dependency: Handle::NULL,
dimension_dependency: Handle::NULL,
sector_type: constraint.angle_sector,
},
ConstraintKind::Angle3Point => AssocConstraintNodeData::Angle {
owner_id,
is_implied: false,
is_active: constraint.enabled,
value_dependency: Handle::NULL,
dimension_dependency: Handle::NULL,
sector_type: constraint.angle_sector,
},
ConstraintKind::Radius => AssocConstraintNodeData::RadiusDiameter {
owner_id,
is_implied: false,
is_active: constraint.enabled,
value_dependency: Handle::NULL,
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: constraint.enabled,
value_dependency: Handle::NULL,
dimension_dependency: Handle::NULL,
mode: 1,
},
_ => unreachable!(),
};
builder.push_node(node_id, dimensional_class_name(constraint.kind), data);
for target in &target_refs {
builder.connect(node_id, *target);
}
needs_value_dependency.push((node_id, constraint.driving_param.clone()));
Some(node_id)
}
_ => None,
}
}
/// Everything a scope's native graph needs beyond the group's own object:
/// the `AssocGeomDependency` and `AssocValueDependency`/`AssocVariable`
/// objects it references by handle, and the network/dictionary wiring.
struct Allocator<'a> {
document: &'a mut CadDocument,
/// One shared variable per distinct parameter in this scope.
variables: FxHashMap<String, Handle>,
variable_actions: Vec<Handle>,
}
impl Allocator<'_> {
fn numeric_eval(value: f64) -> AssocEvalVariant {
if value.fract() == 0.0 && value >= i32::MIN as f64 && value <= i32::MAX as f64 {
AssocEvalVariant {
code: 90,
value: AssocEvalValue::Long(value as i32),
}
} else {
AssocEvalVariant {
code: 40,
value: AssocEvalValue::Real(value),
}
}
}
fn insert_associative(
&mut self,
owner: Handle,
dxf_name: &str,
cpp_class_name: &str,
data: AssociativeData,
) -> Handle {
let handle = self.document.allocate_handle();
self.insert_associative_at(handle, owner, dxf_name, cpp_class_name, data);
handle
}
/// Like [`Allocator::insert_associative`], but for a handle already
/// allocated up front — needed for the group/network pair, which
/// reference each other (`AssocAction::owning_network` /
/// `AssocNetwork::owned_actions`) and so must both have real handles
/// before either object is actually built.
fn insert_associative_at(
&mut self,
handle: Handle,
owner: Handle,
dxf_name: &str,
cpp_class_name: &str,
data: AssociativeData,
) {
let object = AssociativeObject {
handle,
owner,
dxf_name: dxf_name.to_string(),
cpp_class_name: cpp_class_name.to_string(),
data,
..Default::default()
};
self.document
.objects
.insert(handle, ObjectType::Associative(object));
}
fn geom_dependency(
&mut self,
group_handle: Handle,
entity: Handle,
dependency_id: i32,
) -> Handle {
self.insert_associative(
group_handle,
"ACDBASSOCGEOMDEPENDENCY",
"AcDbAssocGeomDependency",
AssociativeData::GeomDependency(AssocGeomDependency {
dependency: AssocDependency {
class_version: 2,
is_read_dependency: true,
is_write_dependency: true,
is_attached_to_object: true,
is_delegating_to_owning_action: true,
order: -10_000,
dependent_on: entity,
dependency_body_id: dependency_id,
..Default::default()
},
class_version: 0,
enabled: true,
// Point and curve addressing lives on constraint-node data.
persistent_subent: AssocPersistentSubentId::default(),
}),
)
}
/// Returns the `AssocVariable` handle for `name`, creating it once per scope.
fn variable(
&mut self,
network_handle: Handle,
name: &str,
formula: &str,
resolved: f64,
description: &str,
) -> Handle {
if let Some(&handle) = self.variables.get(name) {
if let Some(ObjectType::Associative(object)) = self.document.objects.get_mut(&handle) {
object.owner = network_handle;
if let AssociativeData::Variable(variable) = &mut object.data {
variable.action.owning_network = network_handle;
variable.expression = formula.to_string();
variable.value = Self::numeric_eval(resolved);
}
}
return handle;
}
let handle = self.insert_associative(
network_handle,
"ACDBASSOCVARIABLE",
"AcDbAssocVariable",
AssociativeData::Variable(AssocVariable {
action: AssocAction {
class_version: 2,
owning_network: network_handle,
action_index: self.variable_actions.len() as i32 + 1,
..Default::default()
},
class_version: 2,
name: name.to_string(),
expression: formula.to_string(),
evaluator: "AcDbCalc:1.0".to_string(),
description: description.to_string(),
value: Self::numeric_eval(resolved),
has_cached_value: false,
cached_value: String::new(),
flag: false,
reserved: 0,
}),
);
self.variables.insert(name.to_string(), handle);
self.variable_actions.push(handle);
handle
}
fn value_dependency(
&mut self,
group_handle: Handle,
variable_handle: Handle,
resolved: f64,
dependency_id: i32,
) -> Handle {
let value = Self::numeric_eval(resolved);
let handle = self.insert_associative(
group_handle,
"ACDBASSOCVALUEDEPENDENCY",
"AcDbAssocValueDependency",
AssociativeData::ValueDependency(AssocValueDependency {
dependency: AssocDependency {
class_version: 2,
is_read_dependency: true,
is_write_dependency: false,
is_attached_to_object: true,
is_delegating_to_owning_action: true,
dependent_on: variable_handle,
dependency_body_id: dependency_id,
..Default::default()
},
class_version: 0,
name: String::new(),
value,
}),
);
if let Some(ObjectType::Associative(variable)) =
self.document.objects.get_mut(&variable_handle)
{
variable.reactors.push(handle);
}
handle
}
/// The `AcDbAssocDependency` + `AcDbAssocDimDependencyBody` pair that
/// ties a dimensional constraint to the dynamic dimension showing it.
fn dimension_dependency(
&mut self,
group_handle: Handle,
dimension: Handle,
name: &str,
dependency_id: i32,
) -> Handle {
let handle = self.document.allocate_handle();
let body = self.insert_associative(
handle,
"ACDBASSOCDIMDEPENDENCYBODY",
"AcDbAssocDimDependencyBody",
AssociativeData::DimDependencyBody(AssocDimDependencyBody {
dependency_body_version: 1,
base_version: 1,
name: name.to_string(),
class_version: 1,
}),
);
self.insert_associative_at(
handle,
group_handle,
"ACDBASSOCDEPENDENCY",
"AcDbAssocDependency",
AssociativeData::Dependency(AssocDependency {
class_version: 2,
is_read_dependency: true,
is_write_dependency: true,
is_attached_to_object: true,
is_delegating_to_owning_action: true,
dependent_on: dimension,
dependency_body: body,
dependency_body_id: dependency_id,
..Default::default()
}),
);
// Earlier materializations' dependencies are gone by now; drop
// their reactor entries before adding this one.
let live: Vec<Handle> = self
.document
.get_entity(dimension)
.map(|entity| {
entity
.common()
.reactors
.iter()
.copied()
.filter(|reactor| self.document.objects.contains_key(reactor))
.collect()
})
.unwrap_or_default();
if let Some(entity) = self.document.get_entity_mut(dimension) {
let reactors = &mut entity.common_mut().reactors;
*reactors = live;
reactors.push(handle);
}
handle
}
}
/// Sets the dependency on whole-geometry nodes. Implicit points retain a
/// null dependency and refer to their owning geometry node by `curve_id`.
fn set_geometry_dependency(data: &mut AssocConstraintNodeData, handle: Handle) {
match data {
AssocConstraintNodeData::Point {
geometry_dependency,
..
}
| AssocConstraintNodeData::Line {
geometry_dependency,
..
}
| AssocConstraintNodeData::BoundedLine {
geometry_dependency,
..
}
| AssocConstraintNodeData::Circle {
geometry_dependency,
..
}
| AssocConstraintNodeData::Arc {
geometry_dependency,
..
}
| AssocConstraintNodeData::Ellipse {
geometry_dependency,
..
}
| AssocConstraintNodeData::BoundedEllipse {
geometry_dependency,
..
}
| AssocConstraintNodeData::Spline {
geometry_dependency,
..
} => {
*geometry_dependency = handle;
}
_ => {}
}
}
/// Patches a dimensional-constraint node's `value_dependency` field
/// (`Distance`/`Angle`/`RadiusDiameter` — the three kinds
/// [`constraint_node`] defers to `needs_value_dependency`).
fn set_value_dependency(data: &mut AssocConstraintNodeData, handle: Handle) {
match data {
AssocConstraintNodeData::Distance {
value_dependency, ..
}
| AssocConstraintNodeData::Angle {
value_dependency, ..
}
| AssocConstraintNodeData::RadiusDiameter {
value_dependency, ..
} => {
*value_dependency = handle;
}
_ => {}
}
}
/// Patches a dimensional-constraint node's `dimension_dependency` field.
fn set_dimension_dependency(data: &mut AssocConstraintNodeData, handle: Handle) {
match data {
AssocConstraintNodeData::Distance {
dimension_dependency,
..
}
| AssocConstraintNodeData::Angle {
dimension_dependency,
..
}
| AssocConstraintNodeData::RadiusDiameter {
dimension_dependency,
..
} => {
*dimension_dependency = handle;
}
_ => {}
}
}
/// Associative classes that must have real class numbers in DWG/DXF.
const ASSOC_CLASSES: &[(&str, &str, i32)] = &[
(
"ACDBASSOC2DCONSTRAINTGROUP",
"AcDbAssoc2dConstraintGroup",
45,
),
("ACDBASSOCNETWORK", "AcDbAssocNetwork", 45),
("ACDBASSOCVARIABLE", "AcDbAssocVariable", 45),
("ACDBASSOCGEOMDEPENDENCY", "AcDbAssocGeomDependency", 29),
("ACDBASSOCVALUEDEPENDENCY", "AcDbAssocValueDependency", 29),
("ACDBASSOCDEPENDENCY", "AcDbAssocDependency", 29),
("ACDBASSOCDIMDEPENDENCYBODY", "AcDbAssocDimDependencyBody", 45),
];
fn ensure_associative_classes_registered(document: &mut CadDocument) {
for (dxf_name, cpp_class_name, maintenance_version) in ASSOC_CLASSES {
if document.classes.get_by_name(dxf_name).is_some() {
continue;
}
document.classes.add_or_update(acadrust::classes::DxfClass {
dxf_name: dxf_name.to_string(),
cpp_class_name: cpp_class_name.to_string(),
application_name: "ObjectDBX Classes".to_string(),
proxy_flags: acadrust::classes::ProxyFlags(
acadrust::classes::ProxyFlags::ERASE_ALLOWED.0
| acadrust::classes::ProxyFlags::CLONING_ALLOWED.0
| acadrust::classes::ProxyFlags::DISABLES_PROXY_WARNING_DIALOG.0,
),
instance_count: 0,
was_zombie: false,
is_an_entity: false,
class_number: 0,
// 499 = "object" (non-entity) — every class here is one.
item_class_id: 0x1F3,
dwg_version: 27,
maintenance_version: *maintenance_version,
unknown1: 0,
unknown2: 0,
});
}
}
fn ensure_extension_dictionary(document: &mut CadDocument, owner: Handle) -> Handle {
document.ensure_extension_dictionary(owner)
}
/// Removes a dictionary entry and its owned object tree.
fn remove_dictionary_entry(document: &mut CadDocument, dictionary_handle: Handle, key: &str) {
let Some(ObjectType::Dictionary(dictionary)) = document.objects.get_mut(&dictionary_handle)
else {
return;
};
let Some(index) = dictionary
.entries
.iter()
.position(|(name, _)| name.eq_ignore_ascii_case(key))
else {
return;
};
let (_, target) = dictionary.entries.remove(index);
remove_owned_recursive(document, target);
}
fn set_dictionary_entry(
document: &mut CadDocument,
dictionary_handle: Handle,
key: &str,
target: Handle,
) {
let Some(ObjectType::Dictionary(dictionary)) = document.objects.get_mut(&dictionary_handle)
else {
return;
};
if let Some((_, handle)) = dictionary
.entries
.iter_mut()
.find(|(name, _)| name.eq_ignore_ascii_case(key))
{
*handle = target;
} else {
dictionary.add_entry(key, target);
}
}
fn ensure_global_network_dictionary(document: &mut CadDocument) -> Handle {
let root_handle = document.header.named_objects_dict_handle;
if let Some(ObjectType::Dictionary(root)) = document.objects.get(&root_handle) {
if let Some(handle) = root.get(NETWORK_DICTIONARY_KEY) {
if matches!(
document.objects.get(&handle),
Some(ObjectType::Dictionary(_))
) {
return handle;
}
}
}
remove_dictionary_entry(document, root_handle, NETWORK_DICTIONARY_KEY);
let dictionary_handle = document.allocate_handle();
let mut dictionary = acadrust::objects::Dictionary::new();
dictionary.handle = dictionary_handle;
dictionary.owner = root_handle;
dictionary.reactors.push(root_handle);
document
.objects
.insert(dictionary_handle, ObjectType::Dictionary(dictionary));
set_dictionary_entry(
document,
root_handle,
NETWORK_DICTIONARY_KEY,
dictionary_handle,
);
dictionary_handle
}
/// Removes `root` and every object transitively owned by it so rebuilding the
/// graph cannot accumulate orphaned dependency or variable objects.
fn remove_owned_recursive(document: &mut CadDocument, root: Handle) {
if root.is_null() {
return;
}
let children: Vec<Handle> = document
.objects
.keys()
.copied()
.filter(|&h| document.object_owner(h) == Some(root))
.collect();
for child in children {
remove_owned_recursive(document, child);
}
document.objects.remove(&root);
}
fn native_group_handles(document: &CadDocument, owner: Handle) -> Vec<Handle> {
let Some(network_handle) = native_scope_network_handle(document, owner) else {
return Vec::new();
};
let mut handles = Vec::new();
if let Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::Network(network),
..
})) = document.objects.get(&network_handle)
{
handles.extend(network.actions.iter().filter_map(|action| {
matches!(
document.objects.get(&action.dependency),
Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::ConstraintGroup(_),
..
}))
)
.then_some(action.dependency)
}));
}
for (handle, object) in &document.objects {
let ObjectType::Associative(object) = object else {
continue;
};
let AssociativeData::ConstraintGroup(group) = &object.data else {
continue;
};
if (object.owner == network_handle || group.action.owning_network == network_handle)
&& !handles.contains(handle)
{
handles.push(*handle);
}
}
handles
}
fn native_scope_network_handle(document: &CadDocument, owner: Handle) -> Option<Handle> {
let dictionary = document.extension_dictionary_handle(owner)?;
let ObjectType::Dictionary(dictionary) = document.objects.get(&dictionary)? else {
return None;
};
let mut network_handle = dictionary.get(NETWORK_DICTIONARY_KEY)?;
while let Some(ObjectType::Dictionary(dictionary)) = document.objects.get(&network_handle) {
network_handle = dictionary.get(NETWORK_DICTIONARY_KEY)?;
}
matches!(
document.objects.get(&network_handle),
Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::Network(_),
..
}))
)
.then_some(network_handle)
}
fn geometry_dependency(data: &AssocConstraintNodeData) -> Option<Handle> {
match data {
AssocConstraintNodeData::Point {
geometry_dependency,
..
}
| AssocConstraintNodeData::Line {
geometry_dependency,
..
}
| AssocConstraintNodeData::BoundedLine {
geometry_dependency,
..
}
| AssocConstraintNodeData::Circle {
geometry_dependency,
..
}
| AssocConstraintNodeData::Arc {
geometry_dependency,
..
}
| AssocConstraintNodeData::Ellipse {
geometry_dependency,
..
}
| AssocConstraintNodeData::BoundedEllipse {
geometry_dependency,
..
}
| AssocConstraintNodeData::Spline {
geometry_dependency,
..
} if !geometry_dependency.is_null() => Some(*geometry_dependency),
_ => None,
}
}
fn dependency_entity(
document: &CadDocument,
dependency: Handle,
geometry: &AssocConstraintNodeData,
) -> Option<Handle> {
let ObjectType::Associative(object) = document.objects.get(&dependency)? else {
return None;
};
let AssociativeData::GeomDependency(dependency) = &object.data else {
return None;
};
let entity = dependency.dependency.dependent_on;
let supported = match (document.get_entity(entity), geometry) {
(
Some(
EntityType::Point(_)
| EntityType::Insert(_)
| EntityType::Text(_)
| EntityType::MText(_)
| EntityType::AttributeDefinition(_)
| EntityType::AttributeEntity(_)
| EntityType::Table(_),
),
AssocConstraintNodeData::Point { .. },
) => true,
(
Some(EntityType::Text(_) | EntityType::MText(_) | EntityType::Ellipse(_)),
AssocConstraintNodeData::BoundedLine { .. },
) => true,
(Some(EntityType::Line(_)), AssocConstraintNodeData::BoundedLine { is_ray, .. }) => !is_ray,
(
Some(EntityType::LwPolyline(_) | EntityType::Polyline2D(_)),
AssocConstraintNodeData::BoundedLine { .. } | AssocConstraintNodeData::Arc { .. },
) => true,
(Some(EntityType::Ray(_)), AssocConstraintNodeData::BoundedLine { is_ray, .. }) => *is_ray,
(Some(EntityType::XLine(_)), AssocConstraintNodeData::Line { .. }) => true,
(Some(EntityType::Circle(_)), AssocConstraintNodeData::Circle { .. }) => true,
(Some(EntityType::Arc(_)), AssocConstraintNodeData::Arc { .. }) => true,
(
Some(EntityType::Ellipse(_)),
AssocConstraintNodeData::Ellipse { .. }
| AssocConstraintNodeData::BoundedEllipse { .. },
) => true,
(Some(EntityType::Spline(_)), AssocConstraintNodeData::Spline { .. }) => true,
_ => false,
};
supported.then_some(entity)
}
fn group_requires_preservation(document: &CadDocument, group: &Assoc2dConstraintGroup) -> bool {
group.nodes.iter().any(|node| {
if matches!(node.data, AssocConstraintNodeData::RigidSet { .. }) {
return true;
}
if let Some(dependency) = geometry_dependency(&node.data) {
if dependency_entity(document, dependency, &node.data).is_none() {
return true;
}
}
let class_name = node.class_name.to_ascii_uppercase();
class_name.ends_with("CONSTRAINT")
&& constraint_kind(node).is_none()
&& !matches!(
class_name.as_str(),
"ACEQUALCURVATURECONSTRAINT" | "ACEQUALHELPPARAMETERCONSTRAINT"
)
})
}
fn work_plane_vector(work_plane: &[Vector3; 3], local: Vector3) -> Option<Vector3> {
let [origin, axis_x, axis_y] = *work_plane;
let plane = cadkernel::space::Plane::from_axes(
[origin.x, origin.y, origin.z],
[axis_x.x, axis_x.y, axis_x.z],
[axis_y.x, axis_y.y, axis_y.z],
);
let mut world = cadkernel::space::Vec3::from(plane.vector_at([local.x, local.y]));
if local.z != 0.0 {
world = world + cadkernel::space::Vec3::from(plane.normal()?) * local.z;
}
let world = world.normalize()?;
Some(Vector3::new(world.x, world.y, world.z))
}
fn polyline_segment_reference(
document: &CadDocument,
entity: Handle,
data: &AssocConstraintNodeData,
work_plane: &[Vector3; 3],
) -> Option<ParametricRef> {
let (start_point, end_point) = match data {
AssocConstraintNodeData::BoundedLine {
start_point,
end_point,
..
}
| AssocConstraintNodeData::Arc {
start_point,
end_point,
..
} => (*start_point, *end_point),
_ => return None,
};
let [origin, axis_x, axis_y] = *work_plane;
let normal = Vector3::new(
axis_x.y * axis_y.z - axis_x.z * axis_y.y,
axis_x.z * axis_y.x - axis_x.x * axis_y.z,
axis_x.x * axis_y.y - axis_x.y * axis_y.x,
);
let to_world = |point: Vector3| origin + axis_x * point.x + axis_y * point.y + normal * point.z;
let start_point = to_world(start_point);
let end_point = to_world(end_point);
let entity_value = document.get_entity(entity)?;
let closed = match entity_value {
EntityType::LwPolyline(polyline) => polyline.is_closed,
EntityType::Polyline2D(polyline) => polyline.is_closed(),
_ => return None,
};
let points = super::dimension_assoc::source_points(entity_value);
let segment_count = points.len().saturating_sub(usize::from(!closed));
(0..segment_count)
.min_by(|first, second| {
let score = |index: usize| {
let a = points[index];
let b = points[(index + 1) % points.len()];
let forward = (a - start_point).length_squared() + (b - end_point).length_squared();
let reverse = (b - start_point).length_squared() + (a - end_point).length_squared();
forward.min(reverse)
};
score(*first).total_cmp(&score(*second))
})
.map(|index| ParametricRef::segment(entity, index))
}
fn directional_axis_reference(
document: &CadDocument,
entity: Handle,
data: &AssocConstraintNodeData,
work_plane: &[Vector3; 3],
) -> Option<ParametricRef> {
let AssocConstraintNodeData::BoundedLine {
start_point,
end_point,
..
} = data
else {
return None;
};
match document.get_entity(entity)? {
EntityType::Text(_) | EntityType::MText(_) => {
Some(ParametricRef::text_baseline(entity))
}
EntityType::Ellipse(ellipse) => {
let [origin, axis_x, axis_y] = *work_plane;
let normal = Vector3::new(
axis_x.y * axis_y.z - axis_x.z * axis_y.y,
axis_x.z * axis_y.x - axis_x.x * axis_y.z,
axis_x.x * axis_y.y - axis_x.y * axis_y.x,
);
let to_world = |point: Vector3| {
origin + axis_x * point.x + axis_y * point.y + normal * point.z
};
let direction = (to_world(*end_point) - to_world(*start_point)).normalize();
let major = ellipse.major_axis.normalize();
let ellipse_normal = ellipse.normal.normalize();
let minor = Vector3::new(
ellipse_normal.y * major.z - ellipse_normal.z * major.y,
ellipse_normal.z * major.x - ellipse_normal.x * major.z,
ellipse_normal.x * major.y - ellipse_normal.y * major.x,
);
if direction.dot(&minor).abs() > direction.dot(&major).abs() {
Some(ParametricRef::ellipse_minor_axis(entity))
} else {
Some(ParametricRef::ellipse_major_axis(entity))
}
}
_ => None,
}
}
fn numeric_value(value: &AssocEvalVariant) -> Option<f64> {
match value.value {
AssocEvalValue::Real(value) => Some(value),
AssocEvalValue::Long(value) => Some(value as f64),
AssocEvalValue::Short(value) => Some(value as f64),
AssocEvalValue::Byte(value) => Some(value as f64),
_ => None,
}
}
fn driving_value(
document: &CadDocument,
dependency: Handle,
parameters: &mut ParameterTable,
) -> Option<DrivingValue> {
let ObjectType::Associative(object) = document.objects.get(&dependency)? else {
return None;
};
let AssociativeData::ValueDependency(value_dependency) = &object.data else {
return None;
};
let literal = numeric_value(&value_dependency.value);
let ObjectType::Associative(variable) = document
.objects
.get(&value_dependency.dependency.dependent_on)?
else {
return literal.map(DrivingValue::Literal);
};
let AssociativeData::Variable(variable) = &variable.data else {
return literal.map(DrivingValue::Literal);
};
let source = if variable.expression.trim().is_empty() {
numeric_value(&variable.value)?.to_string()
} else {
variable.expression.clone()
};
if parameters.set(&variable.name, &source).is_ok() {
parameters.set_description(&variable.name, &variable.description);
Some(DrivingValue::Named(variable.name.clone()))
} else {
literal
.or_else(|| numeric_value(&variable.value))
.map(DrivingValue::Literal)
}
}
/// The dynamic dimension an `AcDbAssocDependency` ties to a dimensional
/// constraint node.
fn dependency_dimension(document: &CadDocument, dependency: Handle) -> Option<Handle> {
let ObjectType::Associative(object) = document.objects.get(&dependency)? else {
return None;
};
let AssociativeData::Dependency(dependency) = &object.data else {
return None;
};
let target = dependency.dependent_on;
matches!(document.get_entity(target)?, EntityType::Dimension(_)).then_some(target)
}
fn owning_composite(data: &AssocConstraintNodeData) -> Option<i32> {
match data {
AssocConstraintNodeData::Geometrical { owner_id, .. }
| AssocConstraintNodeData::Composite { owner_id, .. }
| AssocConstraintNodeData::Angle { owner_id, .. }
| AssocConstraintNodeData::Parallel { owner_id, .. }
| AssocConstraintNodeData::Distance { owner_id, .. }
| AssocConstraintNodeData::RadiusDiameter { owner_id, .. } => Some(*owner_id),
_ => None,
}
}
fn constraint_is_active(data: &AssocConstraintNodeData) -> bool {
match data {
AssocConstraintNodeData::Geometrical { is_active, .. }
| AssocConstraintNodeData::Composite { is_active, .. }
| AssocConstraintNodeData::Angle { is_active, .. }
| AssocConstraintNodeData::Parallel { is_active, .. }
| AssocConstraintNodeData::Distance { is_active, .. }
| AssocConstraintNodeData::RadiusDiameter { is_active, .. } => *is_active,
_ => true,
}
}
fn constraint_is_implied(data: &AssocConstraintNodeData) -> bool {
match data {
AssocConstraintNodeData::Geometrical { is_implied, .. }
| AssocConstraintNodeData::Composite { is_implied, .. }
| AssocConstraintNodeData::Angle { is_implied, .. }
| AssocConstraintNodeData::Parallel { is_implied, .. }
| AssocConstraintNodeData::Distance { is_implied, .. }
| AssocConstraintNodeData::RadiusDiameter { is_implied, .. } => *is_implied,
_ => false,
}
}
fn set_constraint_active(data: &mut AssocConstraintNodeData, active: bool) {
match data {
AssocConstraintNodeData::Geometrical { is_active, .. }
| AssocConstraintNodeData::Composite { is_active, .. }
| AssocConstraintNodeData::Angle { is_active, .. }
| AssocConstraintNodeData::Parallel { is_active, .. }
| AssocConstraintNodeData::Distance { is_active, .. }
| AssocConstraintNodeData::RadiusDiameter { is_active, .. } => *is_active = active,
_ => {}
}
}
fn clear_removed_geometry_owner(data: &mut AssocConstraintNodeData, removed: &[i32]) {
let owner = match data {
AssocConstraintNodeData::ImplicitPoint {
geometry_node_id, ..
}
| AssocConstraintNodeData::Point {
geometry_node_id, ..
}
| AssocConstraintNodeData::RigidSet {
geometry_node_id, ..
}
| AssocConstraintNodeData::Line {
geometry_node_id, ..
}
| AssocConstraintNodeData::BoundedLine {
geometry_node_id, ..
}
| AssocConstraintNodeData::Circle {
geometry_node_id, ..
}
| AssocConstraintNodeData::Arc {
geometry_node_id, ..
}
| AssocConstraintNodeData::Ellipse {
geometry_node_id, ..
}
| AssocConstraintNodeData::BoundedEllipse {
geometry_node_id, ..
}
| AssocConstraintNodeData::Spline {
geometry_node_id, ..
} => geometry_node_id,
_ => return,
};
if removed.contains(owner) {
*owner = 0;
}
}
fn constraint_kind(node: &AssocConstraintNode) -> Option<ConstraintKind> {
Some(match node.class_name.to_ascii_uppercase().as_str() {
"ACCONSTRAINEDRIGIDSET" => ConstraintKind::RigidSet,
"ACPOINTCOINCIDENCECONSTRAINT" => ConstraintKind::Coincident,
"ACHORIZONTALCONSTRAINT" => ConstraintKind::Horizontal,
"ACVERTICALCONSTRAINT" => ConstraintKind::Vertical,
"ACPARALLELCONSTRAINT" => ConstraintKind::Parallel,
"ACPERPENDICULARCONSTRAINT" => ConstraintKind::Perpendicular,
"ACEQUALLENGTHCONSTRAINT" | "ACEQUALRADIUSCONSTRAINT" => ConstraintKind::Equal,
"ACANGLECONSTRAINT" => ConstraintKind::Angle,
"AC3POINTANGLECONSTRAINT" => ConstraintKind::Angle3Point,
"ACTANGENTCONSTRAINT" => ConstraintKind::Tangent,
"ACG2SMOOTHCONSTRAINT" => ConstraintKind::Smooth,
"ACCONCENTRICCONSTRAINT" => ConstraintKind::Concentric,
"ACCENTERPOINTCONSTRAINT" => ConstraintKind::CenterPoint,
"ACCOLINEARCONSTRAINT" => ConstraintKind::Colinear,
"ACMIDPOINTCONSTRAINT" => ConstraintKind::Midpoint,
"ACFIXEDCONSTRAINT" => ConstraintKind::Fixed,
"ACPOINTCURVECONSTRAINT" => ConstraintKind::PointOnCurve,
"ACEQUALDISTANCECONSTRAINT" => ConstraintKind::EqualDistance,
"ACSYMMETRICCONSTRAINT" => ConstraintKind::Symmetric,
"ACNORMALCONSTRAINT" => ConstraintKind::Normal,
"ACRADIUSDIAMETERCONSTRAINT" => {
let AssocConstraintNodeData::RadiusDiameter { mode, .. } = node.data else {
return None;
};
if mode == 1 {
ConstraintKind::Diameter
} else {
ConstraintKind::Radius
}
}
"ACDISTANCECONSTRAINT" => {
let AssocConstraintNodeData::Distance {
direction_type,
distance,
..
} = &node.data
else {
return None;
};
if *direction_type == distance_direction_type::UNDIRECTED {
ConstraintKind::Distance
} else if *direction_type == distance_direction_type::FIXED
&& distance.is_some_and(|direction| {
let length = direction.x.hypot(direction.y);
length > f64::EPSILON
&& (direction.x / length - 1.0).abs() < 1.0e-9
&& (direction.y / length).abs() < 1.0e-9
})
{
ConstraintKind::DistanceX
} else if *direction_type == distance_direction_type::FIXED
&& distance.is_some_and(|direction| {
let length = direction.x.hypot(direction.y);
length > f64::EPSILON
&& (direction.x / length).abs() < 1.0e-9
&& (direction.y / length - 1.0).abs() < 1.0e-9
})
{
ConstraintKind::DistanceY
} else {
ConstraintKind::DistanceDirected
}
}
_ => return None,
})
}
fn rigid_set_references(
document: &CadDocument,
geometry_ids: &[i32],
refs: &FxHashMap<i32, ParametricRef>,
) -> (Vec<ParametricRef>, Vec<(ParametricRef, Vector3)>) {
let mut entities = Vec::new();
let mut points = Vec::new();
let mut add_point = |reference: ParametricRef, point: Vector3| {
if !points.iter().any(|(existing, _)| *existing == reference) {
points.push((reference, point));
}
};
for geometry_id in geometry_ids {
let Some(reference) = refs.get(geometry_id).copied() else {
continue;
};
if !entities.iter().any(|existing| *existing == reference) {
entities.push(reference);
}
let Some(entity) = document.get_entity(reference.entity) else {
continue;
};
if let Some(marker) = reference.marker {
if let Some(point) = super::parametric_constraints::resolve_point(entity, marker) {
add_point(reference, point);
}
continue;
}
for (marker, point) in super::dimension_assoc::source_points(entity)
.into_iter()
.enumerate()
{
add_point(ParametricRef::point(reference.entity, marker as i32), point);
}
match entity {
EntityType::Circle(circle) => {
add_point(ParametricRef::center(reference.entity), circle.center_wcs())
}
EntityType::Arc(arc) => {
add_point(ParametricRef::center(reference.entity), arc.center_wcs())
}
_ => {}
}
}
(entities, points)
}
pub(super) fn native_constraint_set(
document: &CadDocument,
owner: Handle,
parameters: &mut ParameterTable,
) -> Option<ParametricConstraintSet> {
let groups: Vec<_> = native_group_handles(document, owner)
.into_iter()
.filter_map(|handle| match document.objects.get(&handle) {
Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::ConstraintGroup(group),
..
})) => Some((handle, group, group_requires_preservation(document, group))),
_ => None,
})
.collect();
if groups.is_empty() {
return None;
}
let scope = if owner == document.header.model_space_block_handle {
ParametricScope::ModelSpace
} else {
ParametricScope::Block(owner)
};
let mut set = ParametricConstraintSet::new(scope);
for (group_handle, group, retained) in groups {
if retained {
set.retained_standard_groups.push(group_handle);
}
let mut refs = FxHashMap::default();
for node in &group.nodes {
let Some(dependency) = geometry_dependency(&node.data) else {
continue;
};
if let Some(entity) = dependency_entity(document, dependency, &node.data) {
let reference =
polyline_segment_reference(document, entity, &node.data, &group.work_plane)
.or_else(|| {
directional_axis_reference(
document,
entity,
&node.data,
&group.work_plane,
)
})
.unwrap_or_else(|| {
if matches!(node.data, AssocConstraintNodeData::Point { .. }) {
ParametricRef::point(entity, 0)
} else {
ParametricRef::whole(entity)
}
});
refs.insert(node.node_id, reference);
}
}
for node in &group.nodes {
let AssocConstraintNodeData::ImplicitPoint {
point_type,
point_index,
curve_id,
..
} = node.data
else {
continue;
};
let Some(curve) = refs.get(&curve_id).copied() else {
continue;
};
let reference = if let Some(segment) = curve.segment_index() {
let Some(entity) = document.get_entity(curve.entity) else {
continue;
};
let point_count = super::dimension_assoc::source_points(entity).len();
match point_type {
implicit_point_type::START => {
ParametricRef::point(curve.entity, segment as i32)
}
implicit_point_type::END if point_count > 0 => {
ParametricRef::point(curve.entity, ((segment + 1) % point_count) as i32)
}
implicit_point_type::MID => {
ParametricRef::segment_midpoint(curve.entity, segment)
}
implicit_point_type::CENTER => {
ParametricRef::segment_center(curve.entity, segment)
}
_ => continue,
}
} else {
let marker = match point_type {
implicit_point_type::START => 0,
implicit_point_type::END => 1,
implicit_point_type::MID => -2,
implicit_point_type::CENTER => -3,
implicit_point_type::DEFINE => {
let Some(point_count) = group.nodes.iter().find_map(|node| {
(node.node_id == curve_id)
.then_some(&node.data)
.and_then(|data| {
let AssocConstraintNodeData::Spline { control_points, .. } =
data
else {
return None;
};
Some(control_points.len())
})
}) else {
continue;
};
if point_index == 0 {
0
} else if usize::try_from(point_index).ok() == point_count.checked_sub(1) {
1
} else {
continue;
}
}
_ => continue,
};
ParametricRef::point(curve.entity, marker)
};
refs.insert(node.node_id, reference);
}
for node in &group.nodes {
let Some(kind) = constraint_kind(node) else {
continue;
};
if owning_composite(&node.data).is_some_and(|owner| owner != 0) {
continue;
}
if constraint_is_implied(&node.data) {
continue;
}
let enabled = constraint_is_active(&node.data);
let (targets, rigid_points) = match &node.data {
AssocConstraintNodeData::RigidSet { geometry_ids, .. } => {
rigid_set_references(document, geometry_ids, &refs)
}
AssocConstraintNodeData::Composite {
owned_constraint_ids,
..
} if kind == ConstraintKind::Smooth => {
let mut endpoints: Vec<ParametricRef> = owned_constraint_ids
.iter()
.filter_map(|id| group.nodes.iter().find(|child| child.node_id == *id))
.find(|child| {
child
.class_name
.eq_ignore_ascii_case("AcPointCoincidenceConstraint")
})
.map(|child| {
child
.connections
.iter()
.filter_map(|id| refs.get(id).copied())
.collect()
})
.unwrap_or_default();
if let Some(target) = endpoints.get(1).copied() {
let segment = owned_constraint_ids
.iter()
.filter_map(|id| group.nodes.iter().find(|child| child.node_id == *id))
.filter(|child| {
child.class_name.eq_ignore_ascii_case("AcTangentConstraint")
})
.flat_map(|child| child.connections.iter())
.filter_map(|id| refs.get(id).copied())
.find(|reference| {
reference.entity == target.entity
&& reference.segment_index().is_some()
});
if let Some(segment) = segment {
endpoints.push(segment);
}
}
(endpoints, Vec::new())
}
_ => (
node.connections
.iter()
.filter_map(|id| refs.get(id).copied())
.collect(),
Vec::new(),
),
};
if matches!(
kind,
ConstraintKind::PointOnCurve | ConstraintKind::CenterPoint
) && targets.len() == 2
&& targets[0].entity == targets[1].entity
{
continue;
}
let parameter_table = if matches!(scope, ParametricScope::Block(_)) {
&mut set.local_parameters
} else {
&mut *parameters
};
let driving = match &node.data {
AssocConstraintNodeData::Angle {
value_dependency, ..
}
| AssocConstraintNodeData::Distance {
value_dependency, ..
}
| AssocConstraintNodeData::RadiusDiameter {
value_dependency, ..
} => driving_value(document, *value_dependency, parameter_table),
_ => None,
};
let valid_target_count = match kind {
ConstraintKind::Horizontal | ConstraintKind::Vertical => {
matches!(targets.len(), 1 | 2)
}
ConstraintKind::Fixed | ConstraintKind::Radius | ConstraintKind::Diameter => {
targets.len() == 1
}
ConstraintKind::Symmetric | ConstraintKind::Angle3Point => targets.len() == 3,
ConstraintKind::DistanceDirected => matches!(targets.len(), 2 | 3),
ConstraintKind::EqualDistance => targets.len() == 4,
ConstraintKind::RigidSet => !targets.is_empty() && rigid_points.len() >= 2,
ConstraintKind::Smooth => matches!(targets.len(), 2 | 3),
_ => targets.len() == 2,
};
if !valid_target_count {
continue;
}
if matches!(
kind,
ConstraintKind::Distance
| ConstraintKind::DistanceX
| ConstraintKind::DistanceY
| ConstraintKind::DistanceDirected
| ConstraintKind::Angle
| ConstraintKind::Angle3Point
| ConstraintKind::Radius
| ConstraintKind::Diameter
) && driving.is_none()
{
continue;
}
let dimension = match &node.data {
AssocConstraintNodeData::Angle {
dimension_dependency,
..
}
| AssocConstraintNodeData::Distance {
dimension_dependency,
..
}
| AssocConstraintNodeData::RadiusDiameter {
dimension_dependency,
..
} => dependency_dimension(document, *dimension_dependency),
_ => None,
};
let id = set.add(kind, targets, driving);
if let Some(dimension) = dimension {
set.dimensions.insert(id, dimension);
}
if let Some(constraint) = set.constraints.last_mut() {
constraint.enabled = enabled;
constraint.rigid_points = rigid_points;
if matches!(kind, ConstraintKind::Horizontal | ConstraintKind::Vertical) {
let datum_id = match &node.data {
AssocConstraintNodeData::Parallel {
datum_line_index, ..
} => *datum_line_index,
_ => None,
};
let local_direction = datum_id.and_then(|datum_id| {
group.nodes.iter().find_map(|datum| {
(datum.node_id == datum_id).then_some(&datum.data).and_then(|data| {
match data {
AssocConstraintNodeData::Line { direction, .. } => {
Some(*direction)
}
_ => None,
}
})
})
});
if let Some(world) =
local_direction.and_then(|local| work_plane_vector(&group.work_plane, local))
{
constraint.axis_direction = Some(world);
}
}
if let AssocConstraintNodeData::Distance {
direction_type,
distance,
..
} = &node.data
{
constraint.distance_direction_type = *direction_type;
constraint.distance_direction = *distance;
}
if let AssocConstraintNodeData::Angle { sector_type, .. } = &node.data {
constraint.angle_sector = *sector_type;
}
if retained {
constraint.native_origin = Some(NativeConstraintOrigin {
group: group_handle,
node: node.node_id,
});
}
}
}
}
(!set.constraints.is_empty() || !set.retained_standard_groups.is_empty()).then_some(set)
}
fn sync_retained_group_constraints(document: &mut CadDocument, set: &ParametricConstraintSet) {
let retained: FxHashMap<(Handle, i32), bool> = set
.constraints
.iter()
.filter_map(|constraint| {
constraint
.native_origin
.map(|origin| ((origin.group, origin.node), constraint.enabled))
})
.collect();
for &group_handle in &set.retained_standard_groups {
let Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::ConstraintGroup(group),
..
})) = document.objects.get_mut(&group_handle)
else {
continue;
};
let mut removed = Vec::new();
for node in &mut group.nodes {
if constraint_kind(node).is_none() || constraint_is_implied(&node.data) {
continue;
}
match retained.get(&(group_handle, node.node_id)) {
Some(active) => set_constraint_active(&mut node.data, *active),
None => removed.push(node.node_id),
}
}
if removed.is_empty() {
continue;
}
group.nodes.retain(|node| !removed.contains(&node.node_id));
for node in &mut group.nodes {
node.connections.retain(|id| !removed.contains(id));
clear_removed_geometry_owner(&mut node.data, &removed);
}
}
}
fn detach_preserved_scope(
document: &mut CadDocument,
owner: Handle,
) -> (FxHashMap<String, Handle>, Vec<Handle>) {
let Some(network_handle) = native_scope_network_handle(document, owner) else {
return (FxHashMap::default(), Vec::new());
};
let preserved_groups: Vec<_> = native_group_handles(document, owner)
.into_iter()
.filter(|handle| {
matches!(
document.objects.get(handle),
Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::ConstraintGroup(group),
..
})) if group_requires_preservation(document, group)
)
})
.collect();
if preserved_groups.is_empty() {
return (FxHashMap::default(), Vec::new());
}
let mut variable_handles = Vec::new();
for group_handle in &preserved_groups {
let Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::ConstraintGroup(group),
..
})) = document.objects.get(group_handle)
else {
continue;
};
for dependency in &group.actions {
let Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::ValueDependency(value),
..
})) = document.objects.get(dependency)
else {
continue;
};
let handle = value.dependency.dependent_on;
if !handle.is_null() && !variable_handles.contains(&handle) {
variable_handles.push(handle);
}
}
}
let dictionary = document
.extension_dictionary_handle(owner)
.unwrap_or(Handle::NULL);
for handle in &preserved_groups {
if let Some(ObjectType::Associative(object)) = document.objects.get_mut(handle) {
object.owner = dictionary;
}
}
let mut variables = FxHashMap::default();
for handle in variable_handles {
let Some(ObjectType::Associative(object)) = document.objects.get_mut(&handle) else {
continue;
};
object.owner = dictionary;
if let AssociativeData::Variable(variable) = &object.data {
variables.insert(variable.name.clone(), handle);
}
}
debug_assert!(document.objects.contains_key(&network_handle));
(variables, preserved_groups)
}
/// Materializes one scope into the drawing's associative object graph.
fn materialize_scope(
document: &mut CadDocument,
owner: Handle,
set: &ParametricConstraintSet,
parameters: &ParameterTable,
root_network_handle: Handle,
action_index: i32,
existing_variables: FxHashMap<String, Handle>,
preserved_groups: Vec<Handle>,
materialize_all_parameters: bool,
) -> Option<Handle> {
let dictionary_handle = ensure_extension_dictionary(document, owner);
remove_dictionary_entry(document, dictionary_handle, NETWORK_DICTIONARY_KEY);
// Pass 1 (read-only): constraint-node shapes.
let mut needs_value_dependency = Vec::new();
let mut dimension_links = Vec::new();
let (mut nodes, entities, referenced_entities) = {
let mut builder = GroupBuilder::new(document);
for constraint in &set.constraints {
let node_id = constraint_node(
&mut builder,
document,
constraint,
&mut needs_value_dependency,
);
if let Some((node_id, dimension)) = node_id.zip(set.dimensions.get(&constraint.id)) {
if document.get_entity(*dimension).is_some() {
dimension_links.push((node_id, *dimension));
}
}
}
let GroupBuilder {
nodes,
entities,
referenced_entities,
..
} = builder;
(nodes, entities, referenced_entities)
};
if nodes.is_empty()
&& parameters.is_empty()
&& existing_variables.is_empty()
&& preserved_groups.is_empty()
{
return None;
}
if !nodes.is_empty() {
nodes.insert(
0,
AssocConstraintNode {
node_id: 0,
status: 0,
connections: Vec::new(),
class_name: String::new(),
registry_flag: false,
data: AssocConstraintNodeData::None,
},
);
}
// Pass 2 (mutable): allocate handles for everything the node shapes
// above still reference as `Handle::NULL`. The group and network
// reference each other, so both handles are reserved up front.
let group_handle = (!nodes.is_empty()).then(|| document.allocate_handle());
let network_handle = document.allocate_handle();
let mut allocator = Allocator {
document,
variable_actions: existing_variables.values().copied().collect(),
variables: existing_variables,
};
allocator.variable_actions.sort();
for (index, handle) in allocator.variable_actions.iter().copied().enumerate() {
if let Some(ObjectType::Associative(object)) = allocator.document.objects.get_mut(&handle) {
object.owner = network_handle;
if let AssociativeData::Variable(variable) = &mut object.data {
variable.action.owning_network = network_handle;
variable.action.action_index = index as i32 + 1;
if let Some(parameter) = parameters.get(&variable.name) {
variable.expression = parameter.source.clone();
variable.description = parameters.description(&parameter.name).to_string();
if let Ok(resolved) = parameters.resolve(&parameter.name) {
variable.value = Allocator::numeric_eval(resolved);
}
}
}
}
}
if materialize_all_parameters {
for parameter in parameters.iter() {
let resolved = parameters.resolve(&parameter.name).unwrap_or(0.0);
allocator.variable(
network_handle,
&parameter.name,
&parameter.source,
resolved,
parameters.description(&parameter.name),
);
}
}
let mut geometry_dependencies = Vec::with_capacity(referenced_entities.len());
for (index, entity_handle) in referenced_entities.iter().enumerate() {
let Some(entity_nodes) = entities.get(entity_handle) else {
continue;
};
if entity_nodes.geometry_node_id == 0 {
if entity_nodes.segments.is_empty() && entity_nodes.axes.is_empty() {
continue;
}
}
let dep_handle = allocator.geom_dependency(group_handle?, *entity_handle, index as i32 + 1);
geometry_dependencies.push(dep_handle);
for node_id in std::iter::once(entity_nodes.geometry_node_id)
.filter(|node_id| *node_id != 0)
.chain(entity_nodes.segments.values().copied())
.chain(entity_nodes.axes.values().copied())
{
if let Some(node) = nodes.iter_mut().find(|node| node.node_id == node_id) {
set_geometry_dependency(&mut node.data, dep_handle);
}
}
}
let mut group_dependencies = geometry_dependencies;
let mut node_names: FxHashMap<i32, String> = FxHashMap::default();
for (node_id, driving) in needs_value_dependency {
let Some(driving) = driving else { continue };
let Ok(resolved) = driving.resolve(parameters) else {
continue;
};
let (name, formula) = match &driving {
DrivingValue::Named(name) => {
let formula = parameters
.get(name)
.map(|p| p.source.clone())
.unwrap_or_default();
(name.clone(), formula)
}
DrivingValue::Literal(_) => (format!("d{node_id}"), resolved.to_string()),
};
node_names.insert(node_id, name.clone());
let variable_handle = allocator.variable(
network_handle,
&name,
&formula,
resolved,
parameters.description(&name),
);
let dependency_id = group_dependencies.len() as i32 + 1;
let dep_handle =
allocator.value_dependency(group_handle?, variable_handle, resolved, dependency_id);
group_dependencies.push(dep_handle);
if let Some(node) = nodes.iter_mut().find(|n| n.node_id == node_id) {
set_value_dependency(&mut node.data, dep_handle);
}
}
for (node_id, dimension) in dimension_links {
let Some(name) = node_names.get(&node_id) else {
continue;
};
let dependency_id = group_dependencies.len() as i32 + 1;
let dep_handle =
allocator.dimension_dependency(group_handle?, dimension, name, dependency_id);
group_dependencies.push(dep_handle);
if let Some(node) = nodes.iter_mut().find(|n| n.node_id == node_id) {
set_dimension_dependency(&mut node.data, dep_handle);
}
}
if let Some(group_handle) = group_handle {
let group = Assoc2dConstraintGroup {
action: AssocAction {
class_version: 2,
owning_network: network_handle,
action_index: allocator.variable_actions.len() as i32 + 1,
max_dependency_index: group_dependencies.len() as i32 + 1,
..Default::default()
},
version: 2,
flag: false,
work_plane: [Vector3::ZERO, Vector3::UNIT_X, Vector3::UNIT_Y],
dependency: Handle::NULL,
actions: group_dependencies,
nodes,
};
allocator.insert_associative_at(
group_handle,
network_handle,
"ASSOC2DCONSTRAINTGROUP",
"AcDbAssoc2dConstraintGroup",
AssociativeData::ConstraintGroup(group),
);
}
for (offset, handle) in preserved_groups.iter().copied().enumerate() {
if let Some(ObjectType::Associative(object)) = allocator.document.objects.get_mut(&handle) {
object.owner = network_handle;
if let AssociativeData::ConstraintGroup(group) = &mut object.data {
group.action.owning_network = network_handle;
group.action.action_index = allocator.variable_actions.len() as i32
+ i32::from(group_handle.is_some())
+ offset as i32
+ 1;
}
}
}
let network = AssocNetwork {
action: AssocAction {
class_version: 2,
owning_network: root_network_handle,
action_index,
..Default::default()
},
network_version: 0,
network_action_index: allocator.variable_actions.len() as i32
+ i32::from(group_handle.is_some())
+ preserved_groups.len() as i32,
actions: allocator
.variable_actions
.iter()
.copied()
.chain(group_handle)
.chain(preserved_groups)
.map(|dependency| AssocActionDependency {
is_owned: true,
dependency,
})
.collect(),
owned_actions: Vec::new(),
};
allocator.insert_associative_at(
network_handle,
dictionary_handle,
"ASSOCNETWORK",
"AcDbAssocNetwork",
AssociativeData::Network(network),
);
if let Some(ObjectType::Associative(network)) =
allocator.document.objects.get_mut(&network_handle)
{
network.reactors.push(dictionary_handle);
}
set_dictionary_entry(
allocator.document,
dictionary_handle,
NETWORK_DICTIONARY_KEY,
network_handle,
);
Some(network_handle)
}
impl Scene {
pub(crate) fn load_named_parameters_from_document(&mut self) {
self.named_parameters = ParameterTable::new();
let mut variables: Vec<_> = self
.document
.objects
.values()
.filter_map(|object| match object {
ObjectType::Associative(AssociativeObject {
handle,
data: AssociativeData::Variable(variable),
..
}) => Some((*handle, variable)),
_ => None,
})
.collect();
variables.sort_by_key(|(handle, _)| handle.value());
let model_network = native_scope_network_handle(
&self.document,
self.document.header.model_space_block_handle,
);
for (_, variable) in variables {
if model_network.is_none_or(|network| variable.action.owning_network != network) {
continue;
}
let source = if variable.expression.trim().is_empty() {
numeric_value(&variable.value).map(|value| value.to_string())
} else {
Some(variable.expression.clone())
};
if let Some(source) = source {
let _ = self.named_parameters.set(&variable.name, &source);
}
}
}
pub(crate) fn load_parametric_constraints_from_document(&mut self) {
self.parametric_constraints.clear();
let owners: Vec<_> = self
.document
.block_records
.iter()
.map(|record| record.handle)
.collect();
for owner in owners {
if let Some(set) =
native_constraint_set(&self.document, owner, &mut self.named_parameters)
{
self.parametric_constraints.push(set);
}
}
}
/// Makes the standard associative graph match the live command model.
pub(crate) fn sync_native_parametric_graph(&mut self) {
let mut owners: Vec<_> = self
.document
.block_records
.iter()
.map(|record| record.handle)
.collect();
for set in &self.parametric_constraints {
let owner = set.scope.owner_handle(&self.document);
if !owner.is_null() && !owners.contains(&owner) {
owners.push(owner);
}
}
let model_owner = self.document.header.model_space_block_handle;
if !owners.contains(&model_owner) {
owners.push(model_owner);
}
let mut scopes = Vec::new();
for owner in owners {
let existing = native_scope_network_handle(&self.document, owner).is_some();
let runtime = self
.parametric_constraints
.iter()
.find(|set| set.scope.owner_handle(&self.document) == owner)
.cloned();
let set = match runtime {
Some(set) => Some(set),
None if existing => {
native_constraint_set(&self.document, owner, &mut self.named_parameters)
}
None => None,
};
if set.is_none()
&& !existing
&& (owner != model_owner || self.named_parameters.is_empty())
{
continue;
}
if let Some(set) = &set {
sync_retained_group_constraints(&mut self.document, set);
}
let (variables, groups) = detach_preserved_scope(&mut self.document, owner);
let mut set = set.unwrap_or_else(|| {
if owner == model_owner {
ParametricConstraintSet::new(ParametricScope::ModelSpace)
} else {
ParametricConstraintSet::new(ParametricScope::Block(owner))
}
});
set.constraints.retain(|constraint| {
constraint
.native_origin
.is_none_or(|origin| !groups.contains(&origin.group))
});
for constraint in &mut set.constraints {
constraint.native_origin = None;
}
scopes.push((owner, set, variables, groups));
}
if scopes.is_empty() {
return;
}
ensure_associative_classes_registered(&mut self.document);
let global_dictionary = ensure_global_network_dictionary(&mut self.document);
remove_dictionary_entry(
&mut self.document,
global_dictionary,
NETWORK_DICTIONARY_KEY,
);
let root_network_handle = self.document.allocate_handle();
let mut child_networks = Vec::new();
for (index, (owner, set, variables, groups)) in scopes.into_iter().enumerate() {
let parameters = if set.local_parameters.is_empty() {
&self.named_parameters
} else {
&set.local_parameters
};
if let Some(handle) = materialize_scope(
&mut self.document,
owner,
&set,
parameters,
root_network_handle,
index as i32 + 1,
variables,
groups,
owner == model_owner,
) {
child_networks.push(handle);
}
}
if child_networks.is_empty() {
return;
}
let root_network = AssociativeObject {
handle: root_network_handle,
owner: global_dictionary,
reactors: vec![global_dictionary],
dxf_name: "ASSOCNETWORK".to_string(),
cpp_class_name: "AcDbAssocNetwork".to_string(),
data: AssociativeData::Network(AssocNetwork {
action: AssocAction {
class_version: 2,
..Default::default()
},
network_version: 0,
network_action_index: child_networks.len() as i32,
actions: child_networks
.into_iter()
.map(|dependency| AssocActionDependency {
is_owned: false,
dependency,
})
.collect(),
owned_actions: Vec::new(),
}),
..Default::default()
};
self.document
.objects
.insert(root_network_handle, ObjectType::Associative(root_network));
set_dictionary_entry(
&mut self.document,
global_dictionary,
NETWORK_DICTIONARY_KEY,
root_network_handle,
);
}
}
#[cfg(test)]
mod tests {
use super::super::parametric_constraints::{
angle_sector, distance_direction_type, ParametricScope,
};
use super::*;
use crate::scene::ChangeKind;
use acadrust::entities::{Arc, Circle, Insert, Line, LwPolyline, Ray, Spline, XLine};
use acadrust::tables::BlockRecord;
use acadrust::types::Vector2;
fn line_entity(scene: &mut Scene, start: (f64, f64), end: (f64, f64)) -> Handle {
scene.add_entity(EntityType::Line(Line::from_points(
Vector3::new(start.0, start.1, 0.0),
Vector3::new(end.0, end.1, 0.0),
)))
}
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,
)))
}
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,
)))
}
fn native_group_handle(document: &CadDocument, owner: Handle) -> Handle {
let dict = document
.extension_dictionary_handle(owner)
.expect("extension dictionary should exist");
let Some(ObjectType::Dictionary(dictionary)) = document.objects.get(&dict) else {
panic!("expected owner's extension dictionary object to exist");
};
let network_handle = dictionary
.get(NETWORK_DICTIONARY_KEY)
.expect("ACAD_ASSOCNETWORK entry should exist");
let Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::Network(network),
..
})) = document.objects.get(&network_handle)
else {
panic!("expected an AssocNetwork object at the ACAD_ASSOCNETWORK handle");
};
network
.actions
.iter()
.filter(|dependency| dependency.is_owned)
.map(|dependency| dependency.dependency)
.find(|handle| {
matches!(
document.objects.get(handle),
Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::ConstraintGroup(_),
..
}))
)
})
.expect("network should own the constraint group")
}
fn native_group(document: &CadDocument, owner: Handle) -> &Assoc2dConstraintGroup {
let group_handle = native_group_handle(document, owner);
let Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::ConstraintGroup(group),
..
})) = document.objects.get(&group_handle)
else {
panic!(
"expected an Assoc2dConstraintGroup object at the network's owned action handle"
);
};
group
}
#[test]
fn horizontal_and_vertical_constraints_round_trip_through_dwg_and_dxf() {
for ext in ["dwg", "dxf"] {
let mut scene = Scene::new();
let a = line_entity(&mut scene, (0.0, 0.0), (6.0, 8.0));
let b = line_entity(&mut scene, (10.0, 0.0), (10.0, 5.0));
let horizontal_direction = Vector3::new(0.6, 0.8, 0.0);
scene
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
.add_axis_constraint(
ConstraintKind::Horizontal,
vec![ParametricRef::whole(a)],
horizontal_direction,
);
scene
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
.add(
ConstraintKind::Vertical,
vec![ParametricRef::whole(b)],
None,
);
scene.sync_native_parametric_graph();
let owner = scene.document.header.model_space_block_handle;
// Root + two geometry nodes + two datums + two constraints.
let before = native_group(&scene.document, owner);
assert_eq!(
before.nodes.len(),
7,
"expected root + 2 geometry + 2 datums + 2 constraints, got {:#?}",
before.nodes
);
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!("dwg_native_poc.{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(&"AcHorizontalConstraint"),
"{ext}: missing horizontal constraint node, got {class_names:?}"
);
assert!(
class_names.contains(&"AcVerticalConstraint"),
"{ext}: missing vertical constraint node, got {class_names:?}"
);
assert_eq!(
class_names
.iter()
.filter(|&&n| n == "AcConstrainedBoundedLine")
.count(),
2,
"{ext}: expected both lines' geometry nodes to survive, got {class_names:?}"
);
let datums: Vec<i32> = group
.nodes
.iter()
.filter_map(|node| match node.data {
AssocConstraintNodeData::Parallel {
datum_line_index: Some(datum),
..
} if matches!(
node.class_name.as_str(),
"AcHorizontalConstraint" | "AcVerticalConstraint"
) =>
{
Some(datum)
}
_ => None,
})
.collect();
assert_eq!(datums.len(), 2, "{ext}: both constraints need a datum");
assert!(datums.iter().all(|datum| group.nodes.iter().any(|node| {
node.node_id == *datum && node.class_name == "AcConstrainedDatumLine"
})));
let mut restored = Scene::new();
restored.document = reloaded;
restored.load_parametric_constraints_from_document();
let horizontal = restored
.parametric_constraint_set(ParametricScope::ModelSpace)
.unwrap()
.constraints
.iter()
.find(|constraint| constraint.kind == ConstraintKind::Horizontal)
.unwrap();
assert!(
(horizontal.axis_direction.unwrap() - horizontal_direction).length() < 1.0e-12
);
}
}
#[test]
fn ray_and_construction_line_geometry_round_trip_through_the_standard_graph() {
for ext in ["dwg", "dxf"] {
let mut scene = Scene::new();
let ray = scene.add_entity(EntityType::Ray(Ray::new(
Vector3::ZERO,
Vector3::new(1.0, 1.0, 0.0),
)));
let xline = scene.add_entity(EntityType::XLine(XLine::new(
Vector3::new(4.0, 0.0, 0.0),
Vector3::UNIT_Y,
)));
let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace);
set.add(
ConstraintKind::Horizontal,
vec![ParametricRef::whole(ray)],
None,
);
set.add(
ConstraintKind::Parallel,
vec![ParametricRef::whole(ray), ParametricRef::whole(xline)],
None,
);
scene.sync_native_parametric_graph();
let bytes = crate::io::save_to_bytes(&scene.document, ext, scene.document.version)
.unwrap_or_else(|error| panic!("save {ext}: {error}"));
let mut restored = Scene::new();
restored.document = crate::io::load_bytes(&format!("line_kinds.{ext}"), bytes)
.unwrap_or_else(|error| panic!("reload {ext}: {error}"));
let owner = restored.document.header.model_space_block_handle;
let group = native_group(&restored.document, owner);
assert!(group.nodes.iter().any(|node| {
matches!(
node.data,
AssocConstraintNodeData::BoundedLine { is_ray: true, .. }
)
}));
assert!(group.nodes.iter().any(|node| {
node.class_name == "AcConstrainedLine"
&& matches!(node.data, AssocConstraintNodeData::Line { .. })
}));
restored.load_parametric_constraints_from_document();
let set = restored
.parametric_constraint_set(ParametricScope::ModelSpace)
.expect("constraints should import");
assert!(set
.constraints
.iter()
.any(|constraint| constraint.kind == ConstraintKind::Horizontal));
assert!(set
.constraints
.iter()
.any(|constraint| constraint.kind == ConstraintKind::Parallel));
}
}
#[test]
fn implicit_midpoint_round_trips_as_standard_implied_relations() {
for ext in ["dwg", "dxf"] {
let mut scene = Scene::new();
let carrier = line_entity(&mut scene, (0.0, 0.0), (10.0, 0.0));
let attached = line_entity(&mut scene, (5.0, 0.0), (5.0, 4.0));
scene
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
.add(
ConstraintKind::Coincident,
vec![
ParametricRef::point(carrier, -2),
ParametricRef::point(attached, 0),
],
None,
);
scene.sync_native_parametric_graph();
let bytes = crate::io::save_to_bytes(&scene.document, ext, scene.document.version)
.unwrap_or_else(|error| panic!("save {ext}: {error}"));
let mut restored = Scene::new();
restored.document = crate::io::load_bytes(&format!("midpoint.{ext}"), bytes)
.unwrap_or_else(|error| panic!("reload {ext}: {error}"));
let owner = restored.document.header.model_space_block_handle;
let group = native_group(&restored.document, owner);
assert!(group.nodes.iter().any(|node| matches!(
node.data,
AssocConstraintNodeData::ImplicitPoint {
point_type: implicit_point_type::MID,
..
}
)));
for class_name in ["AcPointCurveConstraint", "AcMidPointConstraint"] {
assert!(group.nodes.iter().any(|node| {
node.class_name == class_name
&& matches!(
node.data,
AssocConstraintNodeData::Geometrical {
is_implied: true,
is_active: true,
..
}
)
}));
}
restored.load_parametric_constraints_from_document();
let set = restored
.parametric_constraint_set(ParametricScope::ModelSpace)
.expect("coincidence should import");
assert_eq!(set.constraints.len(), 1);
assert_eq!(set.constraints[0].kind, ConstraintKind::Coincident);
assert!(set.constraints[0]
.refs
.iter()
.any(|reference| reference.entity == carrier && reference.marker == Some(-2)));
}
}
#[test]
fn spline_smoothness_round_trips_as_a_standard_composite_graph() {
for ext in ["dwg", "dxf"] {
let mut scene = Scene::new();
let first = scene.add_entity(EntityType::Spline(Spline::from_control_points(
3,
vec![
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(2.0, 0.0, 0.0),
Vector3::new(4.0, 2.0, 0.0),
Vector3::new(6.0, 2.0, 0.0),
],
)));
let second = line_entity(&mut scene, (6.0, 2.0), (12.0, 2.0));
scene
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
.add(
ConstraintKind::Smooth,
vec![
ParametricRef::point(first, 1),
ParametricRef::point(second, 0),
],
None,
);
scene.sync_native_parametric_graph();
let bytes = crate::io::save_to_bytes(&scene.document, ext, scene.document.version)
.unwrap_or_else(|error| panic!("save {ext}: {error}"));
let mut restored = Scene::new();
restored.document = crate::io::load_bytes(&format!("smooth.{ext}"), bytes)
.unwrap_or_else(|error| panic!("reload {ext}: {error}"));
let owner = restored.document.header.model_space_block_handle;
let group = native_group(&restored.document, owner);
let spline_nodes: Vec<_> = group
.nodes
.iter()
.filter_map(|node| match &node.data {
AssocConstraintNodeData::Spline {
implicit_point_ids, ..
} => Some(implicit_point_ids),
_ => None,
})
.collect();
assert_eq!(spline_nodes.len(), 1, "{ext}: spline graph node is missing");
assert!(spline_nodes.iter().all(|points| points.len() == 4));
assert_eq!(
group
.nodes
.iter()
.filter(|node| matches!(
node.data,
AssocConstraintNodeData::HelpParameter { .. }
))
.count(),
2,
"{ext}: the spline side needs one parameter for each child"
);
let composite = group
.nodes
.iter()
.find(|node| node.class_name == "AcG2SmoothConstraint")
.expect("smooth composite should exist");
let AssocConstraintNodeData::Composite {
owned_constraint_ids,
..
} = &composite.data
else {
panic!("{ext}: smooth node should carry composite data");
};
assert_eq!(owned_constraint_ids.len(), 3);
assert!(owned_constraint_ids.iter().all(|child_id| {
group.nodes.iter().any(|child| {
child.node_id == *child_id
&& owning_composite(&child.data) == Some(composite.node_id)
})
}));
restored.load_parametric_constraints_from_document();
let constraints = &restored
.parametric_constraint_set(ParametricScope::ModelSpace)
.expect("smooth constraint should import")
.constraints;
assert_eq!(
constraints.len(),
1,
"{ext}: composite children stay internal"
);
assert_eq!(constraints[0].kind, ConstraintKind::Smooth);
assert_eq!(
constraints[0].refs,
vec![
ParametricRef::point(first, 1),
ParametricRef::point(second, 0)
]
);
}
}
#[test]
fn native_only_constraints_and_parameters_are_imported() {
let mut scene = Scene::new();
let a = line_entity(&mut scene, (0.0, 0.0), (10.0, 0.0));
let b = line_entity(&mut scene, (0.0, 5.0), (10.0, 5.0));
scene.named_parameters.set("gap", "5").unwrap();
let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace);
set.add(
ConstraintKind::Horizontal,
vec![ParametricRef::whole(a)],
None,
);
set.add(
ConstraintKind::Parallel,
vec![ParametricRef::whole(a), ParametricRef::whole(b)],
None,
);
set.add(
ConstraintKind::DistanceY,
vec![ParametricRef::point(a, 0), ParametricRef::point(b, 0)],
Some(DrivingValue::Named("gap".to_string())),
);
scene.sync_native_parametric_graph();
let bytes = crate::io::save_to_bytes(&scene.document, "dwg", scene.document.version)
.expect("save native-only graph");
let mut restored = Scene::new();
restored.document = crate::io::load_bytes("native_only.dwg", bytes).unwrap();
restored.load_named_parameters_from_document();
restored.load_parametric_constraints_from_document();
let set = restored
.parametric_constraint_set(ParametricScope::ModelSpace)
.expect("native group should become an editable constraint set");
let kinds: Vec<_> = set
.constraints
.iter()
.map(|constraint| constraint.kind)
.collect();
assert_eq!(
kinds,
vec![
ConstraintKind::Horizontal,
ConstraintKind::Parallel,
ConstraintKind::DistanceY,
]
);
assert_eq!(restored.named_parameters.resolve("gap"), Ok(5.0));
assert_eq!(
set.constraints[2].driving_param,
Some(DrivingValue::Named("gap".to_string()))
);
}
#[test]
fn equal_parameter_names_remain_independent_across_block_scopes() {
let mut scene = Scene::new();
let mut block_handles = Vec::new();
let mut line_handles = Vec::new();
for (index, length) in [5.0, 9.0].into_iter().enumerate() {
let mut block = BlockRecord::new(format!("fixture_{index}"));
block.handle = scene.document.allocate_handle();
let block_handle = block.handle;
scene.document.block_records.add(block).unwrap();
let mut line = Line::from_points(Vector3::ZERO, Vector3::new(length, 0.0, 0.0));
line.common.owner_handle = block_handle;
let line_handle = scene.document.add_entity(EntityType::Line(line)).unwrap();
let set = scene.parametric_constraint_set_mut(ParametricScope::Block(block_handle));
set.local_parameters
.set("width", &length.to_string())
.unwrap();
set.add(
ConstraintKind::DistanceX,
vec![
ParametricRef::point(line_handle, 0),
ParametricRef::point(line_handle, 1),
],
Some(DrivingValue::Named("width".to_string())),
);
block_handles.push(block_handle);
line_handles.push(line_handle);
}
scene.sync_native_parametric_graph();
let bytes = crate::io::save_to_bytes(&scene.document, "dwg", scene.document.version)
.expect("save block parameter scopes");
let mut restored = Scene::new();
restored.document = crate::io::load_bytes("block_parameters.dwg", bytes).unwrap();
restored.load_named_parameters_from_document();
restored.load_parametric_constraints_from_document();
assert!(restored.named_parameters.is_empty());
for (index, block_handle) in block_handles.into_iter().enumerate() {
let set = restored
.parametric_constraint_set(ParametricScope::Block(block_handle))
.expect("block constraint scope should survive");
assert_eq!(set.local_parameters.resolve("width"), Ok([5.0, 9.0][index]));
assert_eq!(
set.constraints[0].driving_param,
Some(DrivingValue::Named("width".to_string()))
);
}
let changes: Vec<_> = line_handles
.into_iter()
.map(|handle| (handle, ChangeKind::Modified))
.collect();
restored.refresh_parametric_constraints(&changes);
}
#[test]
fn standalone_named_parameters_round_trip_as_standard_variables() {
for ext in ["dwg", "dxf"] {
let mut scene = Scene::new();
let xrecords_before = scene
.document
.objects
.values()
.filter(|object| matches!(object, ObjectType::XRecord(_)))
.count();
scene.named_parameters.set("width", "12").unwrap();
scene.named_parameters.set("height", "width / 2").unwrap();
scene.sync_native_parametric_graph();
assert_eq!(
scene
.document
.objects
.values()
.filter(|object| matches!(object, ObjectType::XRecord(_)))
.count(),
xrecords_before,
"standard parameters must not create application records"
);
assert!(has_variable_named(&scene.document, "width"));
assert!(has_variable_named(&scene.document, "height"));
let bytes = crate::io::save_to_bytes(&scene.document, ext, scene.document.version)
.unwrap_or_else(|e| panic!("save to {ext}: {e}"));
let mut restored = Scene::new();
restored.document = crate::io::load_bytes(&format!("parameters.{ext}"), bytes)
.unwrap_or_else(|e| panic!("reload {ext}: {e}"));
restored.load_named_parameters_from_document();
assert_eq!(restored.named_parameters.resolve("width"), Ok(12.0));
assert_eq!(restored.named_parameters.resolve("height"), Ok(6.0));
}
}
#[test]
fn command_style_add_and_remove_sync_the_standard_graph_immediately() {
let mut scene = Scene::new();
let line = line_entity(&mut scene, (0.0, 0.0), (10.0, 0.0));
let id = scene
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
.add(
ConstraintKind::Horizontal,
vec![ParametricRef::whole(line)],
None,
);
scene.bump_entities(&[(line, ChangeKind::Modified)]);
let owner = scene.document.header.model_space_block_handle;
assert!(native_group(&scene.document, owner)
.nodes
.iter()
.any(|node| node.class_name == "AcHorizontalConstraint"));
assert!(scene
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
.remove(id));
scene.bump_entities(&[(line, ChangeKind::Modified)]);
assert!(native_scope_network_handle(&scene.document, owner).is_none());
}
#[test]
fn inactive_constraints_round_trip_without_becoming_active() {
for ext in ["dwg", "dxf"] {
let mut scene = Scene::new();
let line = line_entity(&mut scene, (0.0, 0.0), (10.0, 0.0));
let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace);
set.add(
ConstraintKind::Horizontal,
vec![ParametricRef::whole(line)],
None,
);
set.constraints[0].enabled = false;
scene.bump_entities(&[(line, ChangeKind::Modified)]);
let bytes = crate::io::save_to_bytes(&scene.document, ext, scene.document.version)
.unwrap_or_else(|e| panic!("save to {ext}: {e}"));
let mut restored = Scene::new();
restored.document = crate::io::load_bytes(&format!("inactive.{ext}"), bytes)
.unwrap_or_else(|e| panic!("reload {ext}: {e}"));
restored.load_parametric_constraints_from_document();
let constraint = &restored
.parametric_constraint_set(ParametricScope::ModelSpace)
.expect("constraint set should survive")
.constraints[0];
assert_eq!(constraint.kind, ConstraintKind::Horizontal);
assert!(!constraint.enabled);
}
}
#[test]
fn point_and_two_point_horizontal_constraints_round_trip_natively() {
let mut scene = Scene::new();
let mut block = BlockRecord::new("fixture");
block.handle = scene.document.allocate_handle();
scene.document.block_records.add(block).unwrap();
let point = scene.add_entity(EntityType::Point(acadrust::entities::Point::at(
Vector3::new(1.0, 2.0, 0.0),
)));
let insert = scene.add_entity(EntityType::Insert(Insert::new(
"fixture",
Vector3::new(3.0, 4.0, 0.0),
)));
let line = line_entity(&mut scene, (5.0, 7.0), (10.0, 9.0));
let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace);
set.add(
ConstraintKind::Horizontal,
vec![
ParametricRef::point(point, 0),
ParametricRef::point(insert, 0),
],
None,
);
set.add(
ConstraintKind::Midpoint,
vec![ParametricRef::point(point, 0), ParametricRef::whole(line)],
None,
);
scene.sync_native_parametric_graph();
let bytes = crate::io::save_to_bytes(&scene.document, "dwg", scene.document.version)
.expect("save point graph");
let mut restored = Scene::new();
restored.document = crate::io::load_bytes("point_constraints.dwg", bytes).unwrap();
restored.load_parametric_constraints_from_document();
let constraints = &restored
.parametric_constraint_set(ParametricScope::ModelSpace)
.expect("native point constraints should be imported")
.constraints;
assert_eq!(constraints.len(), 2);
assert_eq!(constraints[0].kind, ConstraintKind::Horizontal);
assert_eq!(constraints[0].refs.len(), 2);
assert_eq!(constraints[1].kind, ConstraintKind::Midpoint);
}
#[test]
fn polyline_segment_constraints_round_trip_through_the_native_graph() {
let mut scene = Scene::new();
let polyline = scene.add_entity(EntityType::LwPolyline(LwPolyline::from_points(vec![
Vector2::new(0.0, 0.0),
Vector2::new(5.0, 2.0),
Vector2::new(10.0, 7.0),
])));
scene
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
.add(
ConstraintKind::Horizontal,
vec![ParametricRef::segment(polyline, 1)],
None,
);
scene.sync_native_parametric_graph();
let bytes = crate::io::save_to_bytes(&scene.document, "dwg", scene.document.version)
.expect("save polyline segment graph");
let mut restored = Scene::new();
restored.document = crate::io::load_bytes("polyline_segment.dwg", bytes).unwrap();
restored.load_parametric_constraints_from_document();
let set = restored
.parametric_constraint_set(ParametricScope::ModelSpace)
.expect("native segment constraint should be imported");
assert_eq!(set.constraints.len(), 1);
assert_eq!(set.constraints[0].kind, ConstraintKind::Horizontal);
assert_eq!(
set.constraints[0].refs,
vec![ParametricRef::segment(polyline, 1)]
);
}
#[test]
fn bulged_polyline_point_on_curve_round_trips_with_its_segment() {
for ext in ["dwg", "dxf"] {
let mut source = LwPolyline::new();
source.add_point(Vector2::new(0.0, 0.0));
source.add_point_with_bulge(Vector2::new(5.0, 0.0), 0.5);
source.add_point(Vector2::new(10.0, 5.0));
let mut scene = Scene::new();
let polyline = scene.add_entity(EntityType::LwPolyline(source));
let point = line_entity(&mut scene, (8.0, 2.0), (9.0, 2.0));
scene
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
.add(
ConstraintKind::PointOnCurve,
vec![
ParametricRef::point(point, 0),
ParametricRef::segment(polyline, 1),
],
None,
);
scene.sync_native_parametric_graph();
let group = native_group(
&scene.document,
scene.document.header.model_space_block_handle,
);
assert!(group
.nodes
.iter()
.any(|node| node.class_name == "AcConstrainedArc"));
let bytes = crate::io::save_to_bytes(&scene.document, ext, scene.document.version)
.unwrap_or_else(|error| panic!("save {ext}: {error}"));
let mut restored = Scene::new();
restored.document = crate::io::load_bytes(&format!("bulged_segment.{ext}"), bytes)
.unwrap_or_else(|error| panic!("reload {ext}: {error}"));
restored.load_parametric_constraints_from_document();
let set = restored
.parametric_constraint_set(ParametricScope::ModelSpace)
.expect("native point-on-curve constraint should be imported");
assert_eq!(set.constraints.len(), 1, "{ext}");
assert_eq!(set.constraints[0].kind, ConstraintKind::PointOnCurve);
assert_eq!(
set.constraints[0].refs,
vec![
ParametricRef::point(point, 0),
ParametricRef::segment(polyline, 1),
],
"{ext}"
);
}
}
fn distance_with_named_parameter_scene() -> (Scene, Handle) {
let mut scene = Scene::new();
let a = line_entity(&mut scene, (0.0, 0.0), (10.0, 0.0));
let b = line_entity(&mut scene, (20.0, 0.0), (30.0, 0.0));
scene
.named_parameters
.set("gap", "5")
.expect("defining the parameter should succeed");
scene
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
.add(
ConstraintKind::Distance,
vec![ParametricRef::point(a, 1), ParametricRef::point(b, 0)],
Some(DrivingValue::Named("gap".to_string())),
);
let owner = scene.document.header.model_space_block_handle;
(scene, owner)
}
fn has_variable_named(document: &CadDocument, name: &str) -> bool {
document.objects.values().any(|obj| {
matches!(
obj,
ObjectType::Associative(AssociativeObject { data: AssociativeData::Variable(v), .. })
if v.name == name
)
})
}
#[test]
fn a_dwg_save_keeps_the_full_dependency_chain_including_the_named_variable() {
let (mut scene, owner) = distance_with_named_parameter_scene();
scene.sync_native_parametric_graph();
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)
.unwrap_or_else(|e| panic!("save to dwg: {e}"));
let reloaded = crate::io::load_bytes("dwg_native_named_param.dwg", bytes)
.unwrap_or_else(|e| panic!("reload dwg: {e}"));
let group_handle = native_group_handle(&reloaded, owner);
let group = native_group(&reloaded, owner);
assert!(
group
.nodes
.iter()
.any(|n| n.class_name == "AcDistanceConstraint"),
"expected a distance constraint node, got {:?}",
group
.nodes
.iter()
.map(|n| &n.class_name)
.collect::<Vec<_>>()
);
assert!(
has_variable_named(&reloaded, "gap"),
"expected the AssocVariable \"gap\" to survive a DWG round trip"
);
assert_eq!(group.action.class_version, 2);
assert_eq!(group.action.action_index, 2);
assert_eq!(group.action.max_dependency_index, 4);
assert_eq!(group.actions.len(), 3);
let value_dependency = group
.nodes
.iter()
.find_map(|node| match &node.data {
AssocConstraintNodeData::Distance {
value_dependency, ..
} => Some(*value_dependency),
_ => None,
})
.expect("distance node should reference its value dependency");
let Some(ObjectType::Associative(AssociativeObject {
owner: value_owner,
data: AssociativeData::ValueDependency(value),
..
})) = reloaded.objects.get(&value_dependency)
else {
panic!("value dependency should exist");
};
assert_eq!(*value_owner, group_handle);
assert_eq!(value.dependency.dependency_body_id, 3);
assert!(value.dependency.is_read_dependency);
assert!(!value.dependency.is_write_dependency);
let variable_handle = value.dependency.dependent_on;
let Some(ObjectType::Associative(AssociativeObject {
owner: network_handle,
reactors,
data: AssociativeData::Variable(variable),
..
})) = reloaded.objects.get(&variable_handle)
else {
panic!("named variable should exist");
};
assert_eq!(group.action.owning_network, *network_handle);
assert_eq!(variable.action.owning_network, *network_handle);
assert_eq!(variable.action.action_index, 1);
assert!(reactors.contains(&value_dependency));
let Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::Network(network),
..
})) = reloaded.objects.get(network_handle)
else {
panic!("scope network should exist");
};
assert_eq!(network.network_action_index, 2);
assert_eq!(network.actions.len(), 2);
assert_eq!(network.actions[0].dependency, variable_handle);
assert_eq!(network.actions[1].dependency, group_handle);
assert!(network.actions.iter().all(|action| action.is_owned));
}
#[test]
fn a_dxf_save_keeps_the_full_constraint_dependency_chain() {
let (mut scene, owner) = distance_with_named_parameter_scene();
scene.sync_native_parametric_graph();
let bytes = crate::io::save_to_bytes(&scene.document, "dxf", scene.document.version)
.unwrap_or_else(|e| panic!("save to dxf: {e}"));
let reloaded = crate::io::load_bytes("dwg_native_named_param.dxf", bytes)
.unwrap_or_else(|e| panic!("reload dxf: {e}"));
let group = native_group(&reloaded, owner);
assert!(
group
.nodes
.iter()
.any(|n| n.class_name == "AcDistanceConstraint"),
"expected the distance constraint node to survive, got {:?}",
group
.nodes
.iter()
.map(|n| &n.class_name)
.collect::<Vec<_>>()
);
assert!(
has_variable_named(&reloaded, "gap"),
"expected the named variable to survive a DXF round trip"
);
}
#[test]
fn resaving_replaces_rather_than_accumulates_native_objects() {
let mut scene = Scene::new();
let a = line_entity(&mut scene, (0.0, 0.0), (10.0, 0.0));
scene
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
.add(
ConstraintKind::Horizontal,
vec![ParametricRef::whole(a)],
None,
);
scene.sync_native_parametric_graph();
let first_count = scene.document.objects.len();
scene.sync_native_parametric_graph();
let second_count = scene.document.objects.len();
assert_eq!(
first_count, second_count,
"a resave with the same constraints must not accumulate new objects"
);
}
#[test]
fn a_standard_rigid_set_and_its_constraints_remain_editable() {
let mut scene = Scene::new();
let a = line_entity(&mut scene, (0.0, 0.0), (10.0, 0.0));
scene
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
.add(
ConstraintKind::Horizontal,
vec![ParametricRef::whole(a)],
None,
);
scene.sync_native_parametric_graph();
let owner = scene.document.header.model_space_block_handle;
let group_handle = native_group_handle(&scene.document, owner);
let Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::ConstraintGroup(group),
..
})) = scene.document.objects.get_mut(&group_handle)
else {
panic!("expected a native constraint group");
};
let geometry_id = group
.nodes
.iter()
.find(|node| node.class_name == "AcConstrainedBoundedLine")
.map(|node| node.node_id)
.expect("line geometry should exist");
for node in &mut group.nodes {
if node.node_id == geometry_id {
if let AssocConstraintNodeData::BoundedLine {
geometry_node_id, ..
} = &mut node.data
{
*geometry_node_id = 99;
}
}
}
group.nodes.push(AssocConstraintNode {
node_id: 99,
class_name: "AcConstrainedRigidSet".to_string(),
data: AssocConstraintNodeData::RigidSet {
geometry_dependency: Handle::NULL,
geometry_node_id: 0,
reserved: false,
transform: [
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
],
geometry_ids: vec![geometry_id],
},
..Default::default()
});
scene.load_parametric_constraints_from_document();
let imported = scene
.parametric_constraint_set(ParametricScope::ModelSpace)
.expect("standard group should be imported");
assert!(imported
.constraints
.iter()
.any(|constraint| constraint.kind == ConstraintKind::Horizontal));
assert!(imported
.constraints
.iter()
.any(|constraint| constraint.kind == ConstraintKind::RigidSet));
scene
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
.add(
ConstraintKind::Vertical,
vec![ParametricRef::whole(a)],
None,
);
scene.sync_native_parametric_graph();
let groups: Vec<_> = native_group_handles(&scene.document, owner)
.into_iter()
.filter_map(|handle| match scene.document.objects.get(&handle) {
Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::ConstraintGroup(group),
..
})) => Some(group),
_ => None,
})
.collect();
assert!(groups.iter().any(|group| group
.nodes
.iter()
.any(|node| matches!(node.data, AssocConstraintNodeData::RigidSet { .. }))));
assert!(groups.iter().any(|group| group
.nodes
.iter()
.any(|node| node.class_name == "AcVerticalConstraint")));
let horizontal = scene
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
.constraints
.iter_mut()
.find(|constraint| constraint.kind == ConstraintKind::Horizontal)
.expect("imported horizontal constraint");
horizontal.enabled = false;
scene.sync_native_parametric_graph();
let Some(ObjectType::Associative(AssociativeObject {
data: AssociativeData::ConstraintGroup(group),
..
})) = scene.document.objects.get(&group_handle)
else {
panic!("retained group should exist");
};
assert!(group.nodes.iter().any(|node| {
node.class_name == "AcHorizontalConstraint" && !constraint_is_active(&node.data)
}));
}
#[test]
fn an_empty_constraint_set_leaves_no_native_network() {
let mut scene = Scene::new();
let _ = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace);
scene.sync_native_parametric_graph();
let owner = scene.document.header.model_space_block_handle;
let dict = scene.document.extension_dictionary_handle(owner);
if let Some(dict) = dict {
if let Some(ObjectType::Dictionary(dictionary)) = scene.document.objects.get(&dict) {
assert!(
dictionary.get(NETWORK_DICTIONARY_KEY).is_none(),
"an unconstrained scope should not materialize a native network"
);
}
}
}
fn model_space_has_native_network(scene: &Scene) -> bool {
native_scope_network_handle(
&scene.document,
scene.document.header.model_space_block_handle,
)
.is_some()
}
#[test]
fn sync_creates_the_standard_graph_for_a_new_constraint() {
let mut scene = Scene::new();
let a = line_entity(&mut scene, (0.0, 0.0), (10.0, 0.0));
scene
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
.add(
ConstraintKind::Horizontal,
vec![ParametricRef::whole(a)],
None,
);
scene.sync_native_parametric_graph();
assert!(
model_space_has_native_network(&scene),
"a new constraint should immediately create its standard graph"
);
}
/// 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
/// 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.parametric_constraint_set_mut(ParametricScope::ModelSpace);
set.add(
ConstraintKind::Concentric,
vec![
ParametricRef::center(circle_a),
ParametricRef::center(circle_b),
],
None,
);
set.add(
ConstraintKind::Colinear,
vec![ParametricRef::whole(line_a), ParametricRef::whole(line_b)],
None,
);
set.add(
ConstraintKind::Fixed,
vec![ParametricRef::whole(line_a)],
None,
);
set.add(
ConstraintKind::CenterPoint,
vec![
ParametricRef::point(marker, 0),
ParametricRef::center(circle_a),
],
None,
);
set.add(
ConstraintKind::Midpoint,
vec![
ParametricRef::point(marker, 1),
ParametricRef::whole(line_a),
],
None,
);
set.add(
ConstraintKind::PointOnCurve,
vec![
ParametricRef::point(marker, 0),
ParametricRef::whole(circle_a),
],
None,
);
set.add(
ConstraintKind::EqualDistance,
vec![
ParametricRef::point(line_b, 0),
ParametricRef::point(line_b, 1),
ParametricRef::point(line_a, 0),
ParametricRef::point(line_a, 1),
],
None,
);
set.add(
ConstraintKind::Symmetric,
vec![
ParametricRef::center(circle_a),
ParametricRef::center(circle_b),
ParametricRef::whole(axis),
],
None,
);
set.add(
ConstraintKind::Normal,
vec![ParametricRef::whole(circle_a), ParametricRef::whole(line_a)],
None,
);
scene.sync_native_parametric_graph();
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",
"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 (`parametric_solve.rs`'s `EntityGeom::Circle` reuse), and
/// this module already had constrained-arc 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.parametric_constraint_set_mut(ParametricScope::ModelSpace);
set.add(
ConstraintKind::Concentric,
vec![ParametricRef::center(arc), ParametricRef::center(circle)],
None,
);
set.add(
ConstraintKind::Radius,
vec![ParametricRef::whole(arc)],
Some(crate::scene::named_parameters::DrivingValue::Literal(9.0)),
);
scene.sync_native_parametric_graph();
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 concentric constraint, got {class_names:?}"
);
assert!(
class_names.contains(&"AcRadiusDiameterConstraint"),
"{ext}: missing radius/diameter constraint, 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 — `ParametricRef`'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
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
.add(
ConstraintKind::Coincident,
vec![ParametricRef::point(arc, 0), ParametricRef::point(line, 0)],
None,
);
scene.sync_native_parametric_graph();
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 point-coincidence constraint, 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:?}"
);
}
}
#[test]
fn an_ellipse_constraint_round_trips_in_the_native_graph() {
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.parametric_constraint_set_mut(ParametricScope::ModelSpace);
set.add(
ConstraintKind::Concentric,
vec![
ParametricRef::center(ellipse),
ParametricRef::center(circle),
],
None,
);
set.add(
ConstraintKind::Fixed,
vec![ParametricRef::whole(circle)],
None,
);
scene.sync_native_parametric_graph();
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}: missing ellipse-referencing constraint, got {class_names:?}"
);
assert!(
class_names.contains(&"AcConstrainedEllipse"),
"{ext}: missing ellipse geometry node, got {class_names:?}"
);
let mut reloaded_scene = Scene::new();
reloaded_scene.document = reloaded_doc;
reloaded_scene.load_parametric_constraints_from_document();
let restored = reloaded_scene
.parametric_constraint_set(ParametricScope::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 round-trip through the associative graph, 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
/// the native format'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.parametric_constraint_set_mut(ParametricScope::ModelSpace);
set.add(
ConstraintKind::Diameter,
vec![ParametricRef::whole(circle)],
Some(DrivingValue::Literal(16.0)),
);
set.add(
ConstraintKind::DistanceX,
vec![ParametricRef::point(line, 0), ParametricRef::point(line, 1)],
Some(DrivingValue::Literal(10.0)),
);
set.add(
ConstraintKind::DistanceY,
vec![ParametricRef::point(line, 0), ParametricRef::point(line, 1)],
Some(DrivingValue::Literal(3.0)),
);
scene.sync_native_parametric_graph();
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:?}"
);
}
}
#[test]
fn line_relative_distance_and_angle_sectors_round_trip() {
for ext in ["dxf", "dwg"] {
let mut scene = Scene::new();
let measured = line_entity(&mut scene, (0.0, 0.0), (3.0, 4.0));
let direction = line_entity(&mut scene, (-2.0, -2.0), (2.0, 2.0));
let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace);
let distance_id = set.add(
ConstraintKind::DistanceDirected,
vec![
ParametricRef::point(measured, 0),
ParametricRef::point(measured, 1),
ParametricRef::whole(direction),
],
Some(DrivingValue::Literal(7.0)),
);
let distance = set
.constraints
.iter_mut()
.find(|constraint| constraint.id == distance_id)
.unwrap();
distance.distance_direction_type = distance_direction_type::PARALLEL_TO_LINE;
distance.distance_direction = Some(Vector3::new(
std::f64::consts::FRAC_1_SQRT_2,
std::f64::consts::FRAC_1_SQRT_2,
0.0,
));
let angle_id = set.add(
ConstraintKind::Angle,
vec![
ParametricRef::whole(measured),
ParametricRef::whole(direction),
],
Some(DrivingValue::Literal(30.0)),
);
set.constraints
.iter_mut()
.find(|constraint| constraint.id == angle_id)
.unwrap()
.angle_sector = angle_sector::ANTIPARALLEL_COUNTERCLOCKWISE;
let angle3_id = set.add(
ConstraintKind::Angle3Point,
vec![
ParametricRef::point(measured, 0),
ParametricRef::point(measured, 1),
ParametricRef::point(direction, 1),
],
Some(DrivingValue::Literal(45.0)),
);
set.constraints
.iter_mut()
.find(|constraint| constraint.id == angle3_id)
.unwrap()
.angle_sector = angle_sector::ANTIPARALLEL_CLOCKWISE;
scene.sync_native_parametric_graph();
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(|error| panic!("save to {ext}: {error}"));
let document = crate::io::load_bytes(&format!("directed.{ext}"), bytes)
.unwrap_or_else(|error| panic!("reload {ext}: {error}"));
let group = native_group(&document, owner);
assert!(group.nodes.iter().any(|node| matches!(
node.data,
AssocConstraintNodeData::Distance {
direction_type: distance_direction_type::PARALLEL_TO_LINE,
..
}
)));
let mut sectors: Vec<_> = group
.nodes
.iter()
.filter_map(|node| match node.data {
AssocConstraintNodeData::Angle { sector_type, .. } => Some(sector_type),
_ => None,
})
.collect();
sectors.sort_unstable();
assert_eq!(
sectors,
vec![
angle_sector::ANTIPARALLEL_CLOCKWISE,
angle_sector::ANTIPARALLEL_COUNTERCLOCKWISE,
]
);
let mut restored = Scene::new();
restored.document = document;
restored.load_named_parameters_from_document();
restored.load_parametric_constraints_from_document();
let restored = restored
.parametric_constraint_set(ParametricScope::ModelSpace)
.unwrap();
let distance = restored
.constraints
.iter()
.find(|constraint| constraint.kind == ConstraintKind::DistanceDirected)
.unwrap();
assert_eq!(
distance.distance_direction_type,
distance_direction_type::PARALLEL_TO_LINE
);
assert_eq!(distance.refs.len(), 3);
let mut restored_sectors: Vec<_> = restored
.constraints
.iter()
.filter(|constraint| {
matches!(
constraint.kind,
ConstraintKind::Angle | ConstraintKind::Angle3Point
)
})
.map(|constraint| constraint.angle_sector)
.collect();
restored_sectors.sort_unstable();
assert_eq!(restored_sectors, sectors);
}
}
}