Complete parametric ribbon and constraint tools

This commit is contained in:
Hakan Seven committed 2026-09-14 01:05:39 +03:00
1 parent f3716bb516
commit 7b4cef41f7
49 files changed
+1438 -103

No files matched your search

+59
View File
@@ -275,6 +275,12 @@ pub(crate) fn source_points(entity: &EntityType) -> Vec<Vector3> {
EntityType::Line(line) => vec![line.start, line.end],
EntityType::Arc(arc) => vec![arc.start_point_wcs(), arc.end_point_wcs()],
EntityType::Circle(_) => Vec::new(),
EntityType::Spline(spline) => crate::entities::spline::nurbs3(spline)
.map(|curve| {
let (start, end) = curve.domain();
[curve.point_at_knot(start), curve.point_at_knot(end)].into_iter()
.map(|point| Vector3::new(point[0], point[1], point[2])).collect()
}).unwrap_or_default(),
EntityType::LwPolyline(polyline) => polyline
.vertices
.iter()
@@ -564,6 +570,59 @@ pub(crate) fn dimension_is_associative(
})
}
pub(crate) fn constraint_from_associative_dimension(
document: &acadrust::CadDocument,
handle: Handle,
) -> Option<(
super::sketch_constraints::ConstraintKind,
Vec<super::sketch_constraints::SketchRef>,
super::named_parameters::DrivingValue,
)> {
use super::named_parameters::DrivingValue;
use super::sketch_constraints::{ConstraintKind, SketchRef};
let EntityType::Dimension(dimension) = document.get_entity(handle)? else { return None };
let association = document.objects.values().find_map(|object| {
let ObjectType::Associative(object) = object else { return None };
let AssociativeData::DimensionAssociation(association) = &object.data else { return None };
(association.dimension == handle && association.associativity != 0).then_some(association)
})?;
let point = |index: usize| {
let reference = association.references.get(index)?.first()?;
Some(SketchRef::point(*reference.xrefs.first()?, reference.main_gs_marker))
};
let whole = |index: usize| {
let reference = association.references.get(index)?.first()?;
Some(SketchRef::whole(*reference.xrefs.first()?))
};
let measurement = DrivingValue::Literal(dimension.measurement());
match dimension {
Dimension::Aligned(_) => Some((ConstraintKind::Distance, vec![point(0)?, point(1)?], measurement)),
Dimension::Linear(linear) => {
let direction = [linear.rotation.cos().abs(), linear.rotation.sin().abs()];
let kind = if direction[1] <= 1e-9 { ConstraintKind::DistanceX }
else if direction[0] <= 1e-9 { ConstraintKind::DistanceY }
else { return None };
Some((kind, vec![point(0)?, point(1)?], measurement))
}
Dimension::Radius(_) | Dimension::LargeRadial(_) => {
Some((ConstraintKind::Radius, vec![whole(0)?], measurement))
}
Dimension::Diameter(_) => Some((ConstraintKind::Diameter, vec![whole(0)?], measurement)),
Dimension::Angular2Ln(_) => {
let mut sources: Vec<Handle> = association.references.iter().flatten()
.filter_map(|reference| reference.xrefs.first().copied()).collect();
sources.sort_unstable();
sources.dedup();
let [first, second] = sources.as_slice() else { return None };
Some((ConstraintKind::Angle,
vec![SketchRef::whole(*first), SketchRef::whole(*second)], measurement))
}
Dimension::Arc(_) => Some((ConstraintKind::ArcLength, vec![whole(0)?], measurement)),
Dimension::Angular3Pt(_) | Dimension::Ordinate(_) => None,
}
}
pub(crate) fn radial_extension_points(
document: &acadrust::CadDocument,
dimension: Handle,
+1
View File
@@ -250,6 +250,7 @@ fn geometrical_class_name(kind: ConstraintKind) -> Option<&'static str> {
ConstraintKind::Vertical => Some("ACVERTICALCONSTRAINT"),
ConstraintKind::Perpendicular => Some("ACPERPENDICULARCONSTRAINT"),
ConstraintKind::Tangent => Some("ACTANGENTCONSTRAINT"),
ConstraintKind::Smooth => Some("ACG2SMOOTHCONSTRAINT"),
// the native format's own `GeomConstraintType` enum (native SDK's
// `AcGeomConstraint.h`) lists `kNormal` alongside `kPerpendicular`
// as a distinct, real geometric-constraint kind.
+6
View File
@@ -1927,6 +1927,11 @@ pub struct Scene {
/// Persistent parametric constraint sets, one per sketch scope.
/// Materialized into scoped XRecords during save and restored on open.
pub(crate) sketch_constraints: Vec<sketch_constraints::SketchConstraintSet>,
/// Session-only visibility overrides for constraint glyphs.
hidden_sketch_constraints: HashSet<(
sketch_constraints::SketchScope,
sketch_constraints::ConstraintId,
)>,
/// Document-wide named-parameter and expression table.
/// Persisted through a single drawing XRecord.
pub(crate) named_parameters: named_parameters::ParameterTable,
@@ -2244,6 +2249,7 @@ impl Scene {
dependency_index_cache: RefCell::new(None),
associative_hatch_source_cache: RefCell::new(None),
sketch_constraints: Vec::new(),
hidden_sketch_constraints: HashSet::default(),
named_parameters: named_parameters::ParameterTable::new(),
has_associative_centers: std::cell::Cell::new(None),
block_defn_cache: RefCell::new(HashMap::default()),
+205
View File
@@ -77,6 +77,8 @@ pub enum ConstraintKind {
Angle,
Radius,
Tangent,
/// An open spline endpoint remains curvature-continuous with another curve endpoint.
Smooth,
/// Two circles/arcs share a center — `refs`: `[center(a), center(b)]`.
/// Solves identically to `Coincident` (`sketch_solve.rs` broadens that
/// match arm rather than duplicating it) — only the DWG-native class
@@ -359,6 +361,7 @@ impl ConstraintKind {
ConstraintKind::Angle => "∠",
ConstraintKind::Radius => "R",
ConstraintKind::Tangent => "T",
ConstraintKind::Smooth => "G²",
ConstraintKind::Concentric => "◎",
ConstraintKind::CenterPoint => "⊕",
ConstraintKind::Colinear => "L",
@@ -467,6 +470,136 @@ pub(crate) fn glyph_placement(
}
impl super::Scene {
pub fn is_sketch_constraint_visible(&self, scope: SketchScope, id: ConstraintId) -> bool {
!self.hidden_sketch_constraints.contains(&(scope, id))
}
pub fn set_sketch_constraint_visibility(
&mut self,
scope: SketchScope,
handles: Option<&[Handle]>,
dimensional: bool,
visible: bool,
) -> usize {
let ids: Vec<_> = self.sketch_constraint_set(scope).into_iter()
.flat_map(|set| set.constraints.iter())
.filter(|constraint| constraint.driving_param.is_some() == dimensional)
.filter(|constraint| handles.is_none_or(|handles| constraint.refs.iter()
.any(|reference| handles.contains(&reference.entity))))
.map(|constraint| constraint.id).collect();
for id in &ids {
if visible { self.hidden_sketch_constraints.remove(&(scope, *id)); }
else { self.hidden_sketch_constraints.insert((scope, *id)); }
}
ids.len()
}
/// Infers relations already present in the selected geometry.
pub fn inferred_sketch_constraints(
&self,
scope: SketchScope,
handles: &[Handle],
) -> Vec<(ConstraintKind, Vec<SketchRef>)> {
use cadkernel::geom2d::{
infer_constraints, Arc, Circle, ConstraintEndpoint, InferredConstraint, Line,
SketchPrimitive, Tolerance,
};
let mut sources = Vec::new();
for handle in handles {
let primitive = match self.document.get_entity(*handle) {
Some(acadrust::EntityType::Line(line)) => SketchPrimitive::Line(Line {
start: [line.start.x, line.start.y], end: [line.end.x, line.end.y],
}),
Some(acadrust::EntityType::Circle(circle)) => SketchPrimitive::Circle(Circle {
centre: [circle.center.x, circle.center.y], radius: circle.radius,
}),
Some(acadrust::EntityType::Arc(arc)) => SketchPrimitive::Arc(Arc {
centre: [arc.center.x, arc.center.y], radius: arc.radius,
start_angle: arc.start_angle, end_angle: arc.end_angle,
}),
_ => continue,
};
sources.push((*handle, primitive));
}
let primitives: Vec<_> = sources.iter().map(|(_, primitive)| *primitive).collect();
let marker = |endpoint| match endpoint {
ConstraintEndpoint::Start => 0,
ConstraintEndpoint::End => 1,
};
let mut mapped: Vec<_> = infer_constraints(
&primitives, Tolerance::new(1e-6), 0.5_f64.to_radians(),
).into_iter().map(|relation| match relation {
InferredConstraint::Coincident { first, first_endpoint, second, second_endpoint } => (
ConstraintKind::Coincident,
vec![SketchRef::point(sources[first].0, marker(first_endpoint)),
SketchRef::point(sources[second].0, marker(second_endpoint))],
),
InferredConstraint::Collinear { first, second } => (
ConstraintKind::Colinear,
vec![SketchRef::whole(sources[first].0), SketchRef::whole(sources[second].0)],
),
InferredConstraint::Concentric { first, second } => (
ConstraintKind::Concentric,
vec![SketchRef::center(sources[first].0), SketchRef::center(sources[second].0)],
),
InferredConstraint::Parallel { first, second } => (
ConstraintKind::Parallel,
vec![SketchRef::whole(sources[first].0), SketchRef::whole(sources[second].0)],
),
InferredConstraint::Perpendicular { first, second } => (
ConstraintKind::Perpendicular,
vec![SketchRef::whole(sources[first].0), SketchRef::whole(sources[second].0)],
),
InferredConstraint::Horizontal { entity } => (
ConstraintKind::Horizontal, vec![SketchRef::whole(sources[entity].0)],
),
InferredConstraint::Vertical { entity } => (
ConstraintKind::Vertical, vec![SketchRef::whole(sources[entity].0)],
),
InferredConstraint::Tangent { first, second } => (
ConstraintKind::Tangent,
vec![SketchRef::whole(sources[first].0), SketchRef::whole(sources[second].0)],
),
}).collect();
if let Some(existing) = self.sketch_constraint_set(scope) {
mapped.retain(|(kind, refs)| !existing.constraints.iter().any(|constraint| {
constraint.kind == *kind && (constraint.refs == *refs
|| (constraint.refs.len() == 2 && refs.len() == 2
&& constraint.refs[0] == refs[1] && constraint.refs[1] == refs[0]))
}));
}
mapped.retain(|(kind, refs)| self.validate_sketch_constraint(*kind, refs, None).is_ok());
mapped
}
pub fn smooth_constraint_refs(&self, handles: &[Handle]) -> Option<Vec<SketchRef>> {
let [first, second] = handles else { return None };
let first_entity = self.document.get_entity(*first)?;
let second_entity = self.document.get_entity(*second)?;
let (spline_handle, spline, target_handle, target) = match (first_entity, second_entity) {
(acadrust::EntityType::Spline(spline), target) => (*first, spline, *second, target),
(target, acadrust::EntityType::Spline(spline)) => (*second, spline, *first, target),
_ => return None,
};
if spline.flags.closed || spline.flags.periodic { return None; }
let spline_points = super::dimension_assoc::source_points(
&acadrust::EntityType::Spline(spline.clone()));
let target_points = super::dimension_assoc::source_points(target);
let spline_ends = [*spline_points.first()?, *spline_points.last()?];
let target_ends = [*target_points.first()?, *target_points.last()?];
let mut best = (f64::INFINITY, 0, 0);
for (source_marker, source) in spline_ends.iter().enumerate() {
for (target_marker, target) in target_ends.iter().enumerate() {
let distance = (*source - *target).length_squared();
if distance < best.0 { best = (distance, source_marker, target_marker); }
}
}
Some(vec![
SketchRef::point(spline_handle, best.1 as i32),
SketchRef::point(target_handle, best.2 as i32),
])
}
/// The constraint set for `scope`, if one has been created.
pub fn sketch_constraint_set(&self, scope: SketchScope) -> Option<&SketchConstraintSet> {
self.sketch_constraints.iter().find(|s| s.scope == scope)
@@ -863,4 +996,76 @@ mod tests {
assert_eq!(usage.len(), 1);
assert_eq!(usage[0].entities, vec![h(1)]);
}
#[test]
fn automatic_inference_maps_relations_and_skips_existing_constraints() {
let mut scene = super::super::Scene::new();
let first = scene.add_entity(acadrust::EntityType::Line(
acadrust::entities::Line::from_points(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(5.0, 0.0, 0.0),
),
));
let second = scene.add_entity(acadrust::EntityType::Line(
acadrust::entities::Line::from_points(
Vector3::new(5.0, 0.0, 0.0),
Vector3::new(10.0, 0.0, 0.0),
),
));
scene
.sketch_constraint_set_mut(SketchScope::ModelSpace)
.add(
ConstraintKind::Horizontal,
vec![SketchRef::whole(first)],
None,
);
let inferred = scene.inferred_sketch_constraints(
SketchScope::ModelSpace,
&[first, second],
);
assert!(!inferred.iter().any(|(kind, refs)| {
*kind == ConstraintKind::Horizontal && *refs == [SketchRef::whole(first)]
}));
assert!(inferred.iter().any(|(kind, refs)| {
*kind == ConstraintKind::Horizontal && *refs == [SketchRef::whole(second)]
}));
assert!(inferred.iter().any(|(kind, _)| *kind == ConstraintKind::Coincident));
assert!(inferred.iter().any(|(kind, _)| *kind == ConstraintKind::Colinear));
}
#[test]
fn visibility_toggles_geometric_and_dimensional_independently() {
let mut scene = super::super::Scene::new();
let line = scene.add_entity(acadrust::EntityType::Line(
acadrust::entities::Line::from_points(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(5.0, 0.0, 0.0),
),
));
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
let geometric = set.add(
ConstraintKind::Horizontal,
vec![SketchRef::whole(line)],
None,
);
let dimensional = set.add(
ConstraintKind::Distance,
vec![SketchRef::point(line, 0), SketchRef::point(line, 1)],
Some(DrivingValue::Literal(5.0)),
);
assert_eq!(
scene.set_sketch_constraint_visibility(
SketchScope::ModelSpace,
None,
false,
false,
),
1
);
assert!(!scene.is_sketch_constraint_visible(SketchScope::ModelSpace, geometric));
assert!(scene.is_sketch_constraint_visible(SketchScope::ModelSpace, dimensional));
}
}
+204 -1
View File
@@ -233,6 +233,40 @@ fn entity_plane_z(entity: &EntityType) -> Option<f64> {
{
ellipse.center.z
}
EntityType::Spline(spline) if !spline.flags.closed && !spline.flags.periodic => {
let curve = crate::entities::spline::nurbs3(spline)?;
let first = *curve.control_points().first()?;
if !curve
.control_points()
.iter()
.all(|point| (point[2] - first[2]).abs() <= PLANE_EPS)
{
return None;
}
first[2]
}
EntityType::LwPolyline(polyline) if is_world_z(polyline.normal) => {
let points = super::dimension_assoc::source_points(entity);
let first = *points.first()?;
if !points
.iter()
.all(|point| (point.z - first.z).abs() <= PLANE_EPS)
{
return None;
}
first.z
}
EntityType::Polyline2D(polyline) if is_world_z(polyline.normal) => {
let points = super::dimension_assoc::source_points(entity);
let first = *points.first()?;
if !points
.iter()
.all(|point| (point.z - first.z).abs() <= PLANE_EPS)
{
return None;
}
first.z
}
_ => return None,
};
z.is_finite().then_some(z)
@@ -737,6 +771,9 @@ fn build_constraint(
_ => Vec::new(),
}
}
// Smooth edits the endpoint controls through cadkernel's spatial
// NURBS operation after the ordinary 2D system has settled.
ConstraintKind::Smooth => Vec::new(),
// A circle's radius is always normal to its own tangent, so
// "line normal to circle/arc" reduces to "line passes through the
// circle's center" — the same `PointOnLine` primitive
@@ -778,6 +815,85 @@ fn build_constraint(
}
}
fn smooth_target_jet(entity: &EntityType, marker: i32) -> Option<cadkernel::space::CurveJet> {
let parameter = match marker {
0 => 0.0,
1 => 1.0,
_ => return None,
};
let curve = crate::entities::curve::entity_curve(entity)?;
if curve.is_closed() {
return None;
}
let step = if marker == 0 { 1.0e-4 } else { -1.0e-4 };
let point = curve.point_at(parameter);
let tangent = curve.tangent_at(parameter);
let next = curve.point_at(parameter + step);
let third = curve.point_at(parameter + 2.0 * step);
Some(cadkernel::space::CurveJet {
point,
tangent,
curvature: cadkernel::space::curve::curvature_through(point, next, third),
})
}
fn apply_smooth_constraints(
document: &acadrust::CadDocument,
set: &SketchConstraintSet,
results: &mut Vec<(Handle, EntityType)>,
) {
for constraint in set
.constraints
.iter()
.filter(|constraint| constraint.enabled && constraint.kind == ConstraintKind::Smooth)
{
let [source_ref, target_ref] = constraint.refs.as_slice() else {
continue;
};
let source = results
.iter()
.find(|(handle, _)| *handle == source_ref.entity)
.map(|(_, entity)| entity.clone())
.or_else(|| document.get_entity(source_ref.entity).cloned());
let target = results
.iter()
.find(|(handle, _)| *handle == target_ref.entity)
.map(|(_, entity)| entity.clone())
.or_else(|| document.get_entity(target_ref.entity).cloned());
let (Some(EntityType::Spline(mut spline)), Some(target)) = (source, target) else {
continue;
};
let endpoint = match source_ref.marker {
Some(0) => cadkernel::space::SplineEnd::Start,
Some(1) => cadkernel::space::SplineEnd::End,
_ => continue,
};
let Some(target_marker) = target_ref.marker else {
continue;
};
let Some(target_jet) = smooth_target_jet(&target, target_marker) else {
continue;
};
let Some(curve) = crate::entities::spline::nurbs3(&spline) else {
continue;
};
let Some(smoothed) = cadkernel::space::smooth_nurbs_endpoint(&curve, endpoint, target_jet)
else {
continue;
};
crate::entities::spline::replace_with_nurbs(&mut spline, &smoothed);
let updated = EntityType::Spline(spline);
if let Some((_, entity)) = results
.iter_mut()
.find(|(handle, _)| *handle == source_ref.entity)
{
*entity = updated;
} else {
results.push((source_ref.entity, updated));
}
}
}
const MOVE_EPS: f64 = 1e-9;
type SolveResult = (
Vec<(Handle, EntityType)>,
@@ -823,7 +939,11 @@ fn solve_scope(
}
}
if cache.is_empty() {
let has_smooth = set
.constraints
.iter()
.any(|constraint| constraint.enabled && constraint.kind == ConstraintKind::Smooth);
if cache.is_empty() && !has_smooth {
return None;
}
@@ -1065,6 +1185,7 @@ fn solve_scope(
_ => {}
}
}
apply_smooth_constraints(document, set, &mut results);
Some((results, dof, conflicts))
}
@@ -1082,6 +1203,42 @@ impl Scene {
"Constraint references must be supported entities on the same world-XY plane.",
);
}
if kind == ConstraintKind::Smooth {
let [source_ref, target_ref] = refs else {
return Err("Smooth requires one spline endpoint and one target endpoint.");
};
if source_ref.entity == target_ref.entity {
return Err("Smooth requires two different entities.");
}
let Some(EntityType::Spline(spline)) = self.document.get_entity(source_ref.entity)
else {
return Err("Smooth requires an open spline as its first reference.");
};
if spline.flags.closed || spline.flags.periodic {
return Err("Smooth requires an open spline.");
}
let endpoint = match source_ref.marker {
Some(0) => cadkernel::space::SplineEnd::Start,
Some(1) => cadkernel::space::SplineEnd::End,
_ => return Err("Smooth requires a spline endpoint."),
};
let Some(target_marker) = target_ref.marker else {
return Err("Smooth requires a target endpoint.");
};
let Some(target) = self.document.get_entity(target_ref.entity) else {
return Err("Smooth target does not exist.");
};
let Some(target_jet) = smooth_target_jet(target, target_marker) else {
return Err("The selected target does not support endpoint smoothing.");
};
let Some(curve) = crate::entities::spline::nurbs3(spline) else {
return Err("The selected spline cannot be evaluated.");
};
if cadkernel::space::smooth_nurbs_endpoint(&curve, endpoint, target_jet).is_none() {
return Err("The selected spline cannot satisfy curvature continuity.");
}
return Ok(());
}
let validation_target = match driving_param {
Some(super::named_parameters::DrivingValue::Named(name))
if !self.named_parameters.contains(name) =>
@@ -1478,4 +1635,50 @@ mod tests {
"the previewed positions should be mutually parallel: dir_a={dir_a:?} dir_b={dir_b:?}"
);
}
#[test]
fn smooth_constraint_recomputes_the_spline_after_its_target_moves() {
let mut scene = Scene::new();
let target = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(5.0, 0.0, 0.0),
)));
let mut spline = acadrust::entities::Spline::new();
spline.degree = 3;
spline.control_points = vec![
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(1.0, 2.0, 0.0),
Vector3::new(3.0, 2.0, 0.0),
Vector3::new(5.0, 3.0, 0.0),
];
spline.knots = cadkernel::space::clamped_uniform_knots(3, 4);
let spline = scene.add_entity(EntityType::Spline(spline));
scene
.sketch_constraint_set_mut(SketchScope::ModelSpace)
.add(
ConstraintKind::Smooth,
vec![SketchRef::point(spline, 0), SketchRef::point(target, 0)],
None,
);
if let Some(EntityType::Line(line)) = scene.document.get_entity_mut(target) {
line.start.y = 1.0;
line.end.y = 1.0;
}
scene.bump_entities(&[(target, super::ChangeKind::Modified)]);
let EntityType::Spline(spline) = scene.document.get_entity(spline).unwrap() else {
panic!("expected spline");
};
let curve = crate::entities::spline::nurbs3(spline).unwrap();
let jet = cadkernel::space::CurveJet::from_nurbs(
&curve,
cadkernel::space::SplineEnd::Start,
)
.unwrap();
assert!((jet.point[0] - 0.0).abs() < 1e-9);
assert!((jet.point[1] - 1.0).abs() < 1e-9);
assert!(jet.tangent[1].abs() < 1e-9, "jet={jet:?}");
assert!(cadkernel::space::Vec3::from(jet.curvature).length() < 5e-3);
}
}