forked from 1b-agent/opencadstudio-upstream
Merge pull request #1332 from ramox81/codex/concentric-constraint-parity
# Conflicts: # src/entities/arc.rs # src/entities/ellipse.rs # src/modules/parametric/mod.rs # src/scene/parametric_solve.rs # src/ui/overlay.rs
This commit is contained in:
commit
16c417f81d
17 files changed
+692
-52
No files matched your search
@@ -454,11 +454,17 @@ impl<'a> GroupBuilder<'a> {
|
||||
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_midpoint {
|
||||
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,
|
||||
@@ -551,6 +557,9 @@ impl<'a> GroupBuilder<'a> {
|
||||
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);
|
||||
}
|
||||
@@ -2079,6 +2088,9 @@ pub(super) fn native_constraint_set(
|
||||
implicit_point_type::MID => {
|
||||
ParametricRef::segment_midpoint(curve.entity, segment)
|
||||
}
|
||||
implicit_point_type::CENTER => {
|
||||
ParametricRef::segment_center(curve.entity, segment)
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -14,8 +14,8 @@ use acadrust::types::{Handle, Vector3};
|
||||
/// ordered per-entity-type point list when non-negative (0/1 = a line's
|
||||
/// start/end, ...), or names a special case when negative (-3 = a
|
||||
/// circle/arc's center; -2 = a bounded curve's midpoint). Polyline segment
|
||||
/// and segment-midpoint references use private negative ranges so they stay
|
||||
/// distinct from vertex markers.
|
||||
/// segment-midpoint, and curved-segment-center references use private
|
||||
/// negative ranges so they stay distinct from vertex markers.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ParametricRef {
|
||||
pub entity: Handle,
|
||||
@@ -28,6 +28,7 @@ pub struct ParametricRef {
|
||||
|
||||
const POLYLINE_SEGMENT_MARKER_BASE: i32 = -1_000_000;
|
||||
const POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE: i32 = -2_000_000;
|
||||
const POLYLINE_SEGMENT_CENTER_MARKER_BASE: i32 = -3_000_000;
|
||||
const ELLIPSE_MAJOR_AXIS_MARKER: i32 = -4;
|
||||
const ELLIPSE_MINOR_AXIS_MARKER: i32 = -5;
|
||||
const TEXT_BASELINE_MARKER: i32 = -6;
|
||||
@@ -88,10 +89,25 @@ impl ParametricRef {
|
||||
|
||||
pub fn segment_midpoint_index(self) -> Option<usize> {
|
||||
let marker = self.marker?;
|
||||
(marker <= POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE)
|
||||
(marker <= POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE
|
||||
&& marker > POLYLINE_SEGMENT_CENTER_MARKER_BASE)
|
||||
.then(|| (POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE - marker) as usize)
|
||||
}
|
||||
|
||||
/// Select the center of one curved polyline segment.
|
||||
pub fn segment_center(entity: Handle, index: usize) -> Self {
|
||||
Self {
|
||||
entity,
|
||||
marker: Some(POLYLINE_SEGMENT_CENTER_MARKER_BASE - index as i32),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn segment_center_index(self) -> Option<usize> {
|
||||
let marker = self.marker?;
|
||||
(marker <= POLYLINE_SEGMENT_CENTER_MARKER_BASE)
|
||||
.then(|| (POLYLINE_SEGMENT_CENTER_MARKER_BASE - marker) as usize)
|
||||
}
|
||||
|
||||
/// Select the displayed baseline of a Text or MText entity.
|
||||
pub fn text_baseline(entity: Handle) -> Self {
|
||||
Self {
|
||||
@@ -503,7 +519,7 @@ impl ParametricConstraintSet {
|
||||
/// endpoint's equivalent of `dimension_assoc::resolve_reference`, restricted
|
||||
/// to the marker conventions constraint endpoints actually use (whole-entity
|
||||
/// `None`, an ordinary `source_points()` index, the `-3` center case, or a
|
||||
/// bounded curve/segment midpoint).
|
||||
/// bounded curve/segment midpoint, or a curved polyline-segment center).
|
||||
///
|
||||
/// Solver-side registration reads raw entity fields directly. This helper is
|
||||
/// for UI-side consumers that need the current world-space position.
|
||||
@@ -524,12 +540,20 @@ pub(crate) fn resolve_point(entity: &acadrust::EntityType, marker: i32) -> Optio
|
||||
let point = curve.point_at(0.5);
|
||||
return Some(Vector3::new(point[0], point[1], point[2]));
|
||||
}
|
||||
let segment = ParametricRef {
|
||||
let reference = ParametricRef {
|
||||
entity: Handle::NULL,
|
||||
marker: Some(marker),
|
||||
};
|
||||
if let Some(segment) = reference.segment_center_index() {
|
||||
let planar = crate::entities::curve::entity_curve(entity)?;
|
||||
let curve = planar.curve.segments().into_iter().nth(segment)?;
|
||||
let cadkernel::geom2d::Curve::Arc(arc) = curve else {
|
||||
return None;
|
||||
};
|
||||
let point = planar.plane.point_at(arc.centre);
|
||||
return Some(Vector3::new(point[0], point[1], point[2]));
|
||||
}
|
||||
.segment_midpoint_index();
|
||||
if let Some(segment) = segment {
|
||||
if let Some(segment) = reference.segment_midpoint_index() {
|
||||
let planar = crate::entities::curve::entity_curve(entity)?;
|
||||
let curve = planar.curve.segments().into_iter().nth(segment)?;
|
||||
let point = planar.plane.point_at(curve.point_at(0.5));
|
||||
@@ -831,6 +855,18 @@ fn glyph_placement_for_reference(
|
||||
let anchor = (start + end) * 0.5;
|
||||
return Some((anchor, segment_normal(start, end)));
|
||||
}
|
||||
if let Some(segment) = r.segment_center_index() {
|
||||
let planar = crate::entities::curve::entity_curve(entity)?;
|
||||
let curve = planar.curve.segments().into_iter().nth(segment)?;
|
||||
let on_arc = planar.plane.point_at(curve.point_at(0.5));
|
||||
let cadkernel::geom2d::Curve::Arc(arc) = curve else {
|
||||
return None;
|
||||
};
|
||||
let center = planar.plane.point_at(arc.centre);
|
||||
let center = Vector3::new(center[0], center[1], center[2]);
|
||||
let on_arc = Vector3::new(on_arc[0], on_arc[1], on_arc[2]);
|
||||
return Some((on_arc, on_arc - center));
|
||||
}
|
||||
if let Some(segment) = r.segment_index() {
|
||||
let anchor = resolve_point(
|
||||
entity,
|
||||
|
||||
@@ -513,6 +513,12 @@ fn resolve_constraint_point(
|
||||
if let Some(point) = geometry.point_for_marker(marker) {
|
||||
return Some(point);
|
||||
}
|
||||
if let Some(segment) = reference
|
||||
.segment_center_index()
|
||||
.and_then(|index| geometry.arc_segment(index))
|
||||
{
|
||||
return Some(segment.arc.circle.center);
|
||||
}
|
||||
|
||||
let midpoint_line = reference
|
||||
.segment_midpoint_index()
|
||||
@@ -794,6 +800,175 @@ fn refs_share_supported_plane(document: &acadrust::CadDocument, refs: &[Parametr
|
||||
plane_z.is_some()
|
||||
}
|
||||
|
||||
fn concentric_reference_geometry(
|
||||
document: &acadrust::CadDocument,
|
||||
reference: ParametricRef,
|
||||
) -> Option<(Vector3, cadkernel::space::Plane)> {
|
||||
let marker = reference.marker?;
|
||||
if marker != -3 && reference.segment_center_index().is_none() {
|
||||
return None;
|
||||
}
|
||||
let entity = document.get_entity(reference.entity)?;
|
||||
let center = super::parametric_constraints::resolve_point(entity, marker)?;
|
||||
let plane = crate::entities::curve::entity_curve(entity)?.plane;
|
||||
Some((center, plane))
|
||||
}
|
||||
|
||||
fn concentric_refs_share_plane(
|
||||
document: &acadrust::CadDocument,
|
||||
refs: &[ParametricRef],
|
||||
) -> bool {
|
||||
let [first, second] = refs else {
|
||||
return false;
|
||||
};
|
||||
if first == second {
|
||||
return false;
|
||||
}
|
||||
let Some((first_center, first_plane)) = concentric_reference_geometry(document, *first) else {
|
||||
return false;
|
||||
};
|
||||
let Some((second_center, second_plane)) = concentric_reference_geometry(document, *second)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let (Some(first_normal), Some(second_normal)) = (first_plane.normal(), second_plane.normal())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let parallel = (first_normal[0] * second_normal[0]
|
||||
+ first_normal[1] * second_normal[1]
|
||||
+ first_normal[2] * second_normal[2])
|
||||
.abs()
|
||||
>= 1.0 - 1.0e-9;
|
||||
let points = [
|
||||
first_plane.origin,
|
||||
second_plane.origin,
|
||||
[first_center.x, first_center.y, first_center.z],
|
||||
[second_center.x, second_center.y, second_center.z],
|
||||
];
|
||||
let tolerance = cadkernel::space::coplanarity_tolerance(&points);
|
||||
parallel
|
||||
&& first_plane
|
||||
.distance_to(second_plane.origin)
|
||||
.is_some_and(|distance| distance.abs() <= tolerance)
|
||||
&& first_plane
|
||||
.distance_to([second_center.x, second_center.y, second_center.z])
|
||||
.is_some_and(|distance| distance.abs() <= tolerance)
|
||||
}
|
||||
|
||||
fn ref_is_driven(driven: &[ParametricRef], reference: ParametricRef) -> bool {
|
||||
driven.iter().any(|candidate| {
|
||||
candidate.entity == reference.entity
|
||||
&& (candidate.marker == reference.marker || candidate.marker.is_none())
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_spatial_concentric_constraints(
|
||||
document: &acadrust::CadDocument,
|
||||
set: &ParametricConstraintSet,
|
||||
driven_refs: &[ParametricRef],
|
||||
initial_fixed_refs: &[ParametricRef],
|
||||
results: &mut Vec<(Handle, EntityType)>,
|
||||
) {
|
||||
let current_entity = |handle: Handle, results: &[(Handle, EntityType)]| {
|
||||
results
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|(candidate, _)| *candidate == handle)
|
||||
.map(|(_, entity)| entity.clone())
|
||||
.or_else(|| document.get_entity(handle).cloned())
|
||||
};
|
||||
|
||||
let mut handles = Vec::<Handle>::new();
|
||||
let mut relations = Vec::new();
|
||||
let mut fixed = Vec::new();
|
||||
let mut points = Vec::new();
|
||||
for constraint in set.constraints.iter().filter(|constraint| {
|
||||
constraint.enabled
|
||||
&& constraint.kind == ConstraintKind::Concentric
|
||||
&& !refs_share_supported_plane(document, &constraint.refs)
|
||||
&& concentric_refs_share_plane(document, &constraint.refs)
|
||||
}) {
|
||||
let [first, second] = constraint.refs.as_slice() else {
|
||||
continue;
|
||||
};
|
||||
if first.entity == second.entity {
|
||||
continue;
|
||||
}
|
||||
let (Some(first_entity), Some(second_entity)) = (
|
||||
current_entity(first.entity, results),
|
||||
current_entity(second.entity, results),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let (Some(first_marker), Some(second_marker)) = (first.marker, second.marker) else {
|
||||
continue;
|
||||
};
|
||||
let (Some(first_center), Some(second_center)) = (
|
||||
super::parametric_constraints::resolve_point(&first_entity, first_marker),
|
||||
super::parametric_constraints::resolve_point(&second_entity, second_marker),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let first_index = handles
|
||||
.iter()
|
||||
.position(|handle| *handle == first.entity)
|
||||
.unwrap_or_else(|| {
|
||||
handles.push(first.entity);
|
||||
handles.len() - 1
|
||||
});
|
||||
let second_index = handles
|
||||
.iter()
|
||||
.position(|handle| *handle == second.entity)
|
||||
.unwrap_or_else(|| {
|
||||
handles.push(second.entity);
|
||||
handles.len() - 1
|
||||
});
|
||||
let first_point = [first_center.x, first_center.y, first_center.z];
|
||||
let second_point = [second_center.x, second_center.y, second_center.z];
|
||||
relations.push((first_index, first_point, second_index, second_point));
|
||||
points.extend([first_point, second_point]);
|
||||
if ref_is_driven(driven_refs, *first) || ref_is_driven(initial_fixed_refs, *first) {
|
||||
fixed.push(first_index);
|
||||
}
|
||||
if ref_is_driven(driven_refs, *second) || ref_is_driven(initial_fixed_refs, *second) {
|
||||
fixed.push(second_index);
|
||||
}
|
||||
}
|
||||
if relations.is_empty() {
|
||||
return;
|
||||
}
|
||||
fixed.sort_unstable();
|
||||
fixed.dedup();
|
||||
let tolerance = cadkernel::space::coplanarity_tolerance(&points).max(MOVE_EPS);
|
||||
let Some(translations) = cadkernel::space::solve_rigid_point_coincidence(
|
||||
handles.len(),
|
||||
&relations,
|
||||
&fixed,
|
||||
tolerance,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
for (handle, translation) in handles.into_iter().zip(translations) {
|
||||
let translation = glam::DVec3::from_array(translation);
|
||||
if translation.length() <= tolerance {
|
||||
continue;
|
||||
}
|
||||
let Some(mut entity) = current_entity(handle, results) else {
|
||||
continue;
|
||||
};
|
||||
crate::scene::view::dispatch::apply_transform(
|
||||
&mut entity,
|
||||
&crate::command::EntityTransform::Translate(translation),
|
||||
);
|
||||
if let Some((_, current)) = results.iter_mut().find(|(candidate, _)| *candidate == handle) {
|
||||
*current = entity;
|
||||
} else {
|
||||
results.push((handle, entity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolved_target(params: &ParameterTable, constraint: &ParametricConstraint) -> Option<f64> {
|
||||
let value = constraint.driving_param.as_ref()?.resolve(params).ok()?;
|
||||
if !value.is_finite() {
|
||||
@@ -1982,6 +2157,7 @@ fn polyline_ref_order(
|
||||
}
|
||||
let position = if let Some(index) = reference.segment_index()
|
||||
.or_else(|| reference.segment_midpoint_index())
|
||||
.or_else(|| reference.segment_center_index())
|
||||
{
|
||||
2 * index + 1
|
||||
} else {
|
||||
@@ -2133,7 +2309,13 @@ fn solve_scope(
|
||||
|
||||
let has_smooth = constraints.iter()
|
||||
.any(|constraint| constraint.enabled && constraint.kind == ConstraintKind::Smooth);
|
||||
if cache.is_empty() && !has_smooth {
|
||||
let has_spatial_concentric = constraints.iter().any(|constraint| {
|
||||
constraint.enabled
|
||||
&& constraint.kind == ConstraintKind::Concentric
|
||||
&& !refs_share_supported_plane(document, &constraint.refs)
|
||||
&& concentric_refs_share_plane(document, &constraint.refs)
|
||||
});
|
||||
if cache.is_empty() && !has_smooth && !has_spatial_concentric {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -3013,6 +3195,13 @@ fn solve_scope(
|
||||
}
|
||||
}
|
||||
apply_smooth_constraints(document, set, &mut results);
|
||||
apply_spatial_concentric_constraints(
|
||||
document,
|
||||
set,
|
||||
driven_refs,
|
||||
initial_fixed_refs,
|
||||
&mut results,
|
||||
);
|
||||
Some((results, dof, conflicts))
|
||||
}
|
||||
|
||||
@@ -3064,6 +3253,21 @@ impl Scene {
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
if kind == ConstraintKind::Concentric {
|
||||
if !concentric_refs_share_plane(&self.document, refs) {
|
||||
return Err(
|
||||
"Concentric requires two supported circular curves on the same plane.",
|
||||
);
|
||||
}
|
||||
if !refs_share_supported_plane(&self.document, refs) {
|
||||
if refs[0].entity == refs[1].entity {
|
||||
return Err(
|
||||
"Concentric cannot move two curved segments of the same object on this plane.",
|
||||
);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
if !refs_share_supported_plane(&self.document, refs) {
|
||||
return Err(
|
||||
"Constraint references must be supported entities on the same world-XY plane.",
|
||||
|
||||
Reference in new issue
Block a user