forked from 1b-agent/opencadstudio-upstream
Complete parametric ribbon and constraint tools
This commit is contained in:
1 parent
f3716bb516
commit
7b4cef41f7
49 files changed
+1438
-103
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user