forked from 1b-agent/opencadstudio-upstream
Merge pull request #1334 from ramox81/codex/horizontal-constraint-parity
This commit is contained in:
commit
f9c59503f0
10 files changed
+710
-88
No files matched your search
@@ -56,8 +56,6 @@ struct GroupBuilder<'a> {
|
||||
/// Real entity handles that ended up with a geometry node — what
|
||||
/// `AssocGeomDependency` objects get created for, in first-touch order.
|
||||
referenced_entities: Vec<Handle>,
|
||||
horizontal_datum: Option<i32>,
|
||||
vertical_datum: Option<i32>,
|
||||
}
|
||||
|
||||
impl<'a> GroupBuilder<'a> {
|
||||
@@ -68,8 +66,6 @@ impl<'a> GroupBuilder<'a> {
|
||||
next_node_id: FIRST_NODE_ID,
|
||||
entities: FxHashMap::default(),
|
||||
referenced_entities: Vec::new(),
|
||||
horizontal_datum: None,
|
||||
vertical_datum: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,15 +658,7 @@ impl<'a> GroupBuilder<'a> {
|
||||
Some(node_id)
|
||||
}
|
||||
|
||||
fn datum_line(&mut self, horizontal: bool) -> i32 {
|
||||
let cached = if horizontal {
|
||||
self.horizontal_datum
|
||||
} else {
|
||||
self.vertical_datum
|
||||
};
|
||||
if let Some(node_id) = cached {
|
||||
return node_id;
|
||||
}
|
||||
fn datum_line(&mut self, direction: Vector3) -> i32 {
|
||||
let node_id = self.alloc_node_id();
|
||||
self.push_node(
|
||||
node_id,
|
||||
@@ -679,18 +667,13 @@ impl<'a> GroupBuilder<'a> {
|
||||
geometry_dependency: Handle::NULL,
|
||||
geometry_node_id: node_id,
|
||||
point: Vector3::ZERO,
|
||||
direction: if horizontal {
|
||||
Vector3::UNIT_X
|
||||
direction: if direction.length_squared() > 1.0e-24 {
|
||||
direction.normalize()
|
||||
} else {
|
||||
Vector3::UNIT_Y
|
||||
Vector3::UNIT_X
|
||||
},
|
||||
},
|
||||
);
|
||||
if horizontal {
|
||||
self.horizontal_datum = Some(node_id);
|
||||
} else {
|
||||
self.vertical_datum = Some(node_id);
|
||||
}
|
||||
node_id
|
||||
}
|
||||
|
||||
@@ -932,7 +915,12 @@ fn constraint_node(
|
||||
.map(|reference| builder.ref_node(*reference))
|
||||
.collect::<Option<_>>()?;
|
||||
let horizontal = constraint.kind == ConstraintKind::Horizontal;
|
||||
let datum = builder.datum_line(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,
|
||||
@@ -2251,6 +2239,38 @@ pub(super) fn native_constraint_set(
|
||||
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(local) = local_direction {
|
||||
let [_, axis_x, axis_y] = group.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 world = axis_x * local.x + axis_y * local.y + normal * local.z;
|
||||
if world.length_squared() > 1.0e-24 {
|
||||
constraint.axis_direction = Some(world.normalize());
|
||||
}
|
||||
}
|
||||
}
|
||||
if let AssocConstraintNodeData::Distance {
|
||||
direction_type,
|
||||
distance,
|
||||
|
||||
@@ -389,6 +389,10 @@ pub struct ParametricConstraint {
|
||||
/// identifies the direction line for the line-relative modes.
|
||||
pub(crate) distance_direction_type: u8,
|
||||
pub(crate) distance_direction: Option<Vector3>,
|
||||
/// World-space datum direction captured for Horizontal/Vertical. Native
|
||||
/// files store the same vector on the connected constrained datum line.
|
||||
/// `None` keeps legacy world-X/world-Y behavior.
|
||||
pub(crate) axis_direction: Option<Vector3>,
|
||||
/// Which of the four directed sectors an angular constraint measures.
|
||||
pub(crate) angle_sector: u8,
|
||||
}
|
||||
@@ -476,11 +480,71 @@ impl ParametricConstraintSet {
|
||||
rigid_points: Vec::new(),
|
||||
distance_direction_type: 0,
|
||||
distance_direction: None,
|
||||
axis_direction: None,
|
||||
angle_sector: angle_sector::PARALLEL_COUNTERCLOCKWISE,
|
||||
});
|
||||
id
|
||||
}
|
||||
|
||||
/// Appends a Horizontal/Vertical constraint tied to an explicit datum
|
||||
/// direction rather than silently interpreting it in world coordinates.
|
||||
pub fn add_axis_constraint(
|
||||
&mut self,
|
||||
kind: ConstraintKind,
|
||||
refs: Vec<ParametricRef>,
|
||||
direction: Vector3,
|
||||
) -> ConstraintId {
|
||||
let fallback = if kind == ConstraintKind::Vertical {
|
||||
Vector3::UNIT_Y
|
||||
} else {
|
||||
Vector3::UNIT_X
|
||||
};
|
||||
let direction = if direction.length_squared() > 1.0e-24 {
|
||||
direction.normalize()
|
||||
} else {
|
||||
fallback
|
||||
};
|
||||
let id = self.add(kind, refs, None);
|
||||
if let Some(constraint) = self.constraints.last_mut() {
|
||||
constraint.axis_direction = Some(direction);
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
pub fn contains_axis_constraint(
|
||||
&self,
|
||||
kind: ConstraintKind,
|
||||
refs: &[ParametricRef],
|
||||
direction: Vector3,
|
||||
) -> bool {
|
||||
let fallback = if kind == ConstraintKind::Vertical {
|
||||
Vector3::UNIT_Y
|
||||
} else {
|
||||
Vector3::UNIT_X
|
||||
};
|
||||
let direction = if direction.length_squared() > 1.0e-24 {
|
||||
direction.normalize()
|
||||
} else {
|
||||
fallback
|
||||
};
|
||||
self.constraints.iter().any(|constraint| {
|
||||
if !constraint.enabled || constraint.kind != kind {
|
||||
return false;
|
||||
}
|
||||
let same_refs = constraint.refs == refs
|
||||
|| (refs.len() == 2
|
||||
&& constraint.refs.len() == 2
|
||||
&& constraint.refs[0] == refs[1]
|
||||
&& constraint.refs[1] == refs[0]);
|
||||
if !same_refs {
|
||||
return false;
|
||||
}
|
||||
let existing = constraint.axis_direction.unwrap_or(fallback);
|
||||
existing.length_squared() > 1.0e-24
|
||||
&& existing.normalize().dot(&direction).abs() >= 1.0 - 1.0e-10
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes a constraint by id. Returns whether one was actually removed.
|
||||
pub fn remove(&mut self, id: ConstraintId) -> bool {
|
||||
let before = self.constraints.len();
|
||||
@@ -1828,6 +1892,7 @@ mod tests {
|
||||
rigid_points: Vec::new(),
|
||||
distance_direction_type: 0,
|
||||
distance_direction: None,
|
||||
axis_direction: None,
|
||||
angle_sector: angle_sector::PARALLEL_COUNTERCLOCKWISE,
|
||||
};
|
||||
let (anchor, direction) =
|
||||
@@ -1863,6 +1928,7 @@ mod tests {
|
||||
rigid_points: Vec::new(),
|
||||
distance_direction_type: 0,
|
||||
distance_direction: None,
|
||||
axis_direction: None,
|
||||
angle_sector: angle_sector::PARALLEL_COUNTERCLOCKWISE,
|
||||
};
|
||||
let tangent = relation(
|
||||
|
||||
+102
-59
@@ -1076,6 +1076,37 @@ fn build_constraint(
|
||||
let point_ref = |sys: &mut System, cache: &mut HashMap<_, _>, r: ParametricRef| {
|
||||
resolve_constraint_point(document, sys, cache, r)
|
||||
};
|
||||
let directional_line =
|
||||
|sys: &mut System, cache: &mut HashMap<_, _>, r: ParametricRef| {
|
||||
let geometry = resolve_ref(document, sys, cache, r)?;
|
||||
let minor_axis = matches!(
|
||||
r.directional_axis(),
|
||||
Some(super::parametric_constraints::DirectionalAxis::EllipseMinor)
|
||||
);
|
||||
let line = match geometry {
|
||||
EntityGeom::TextLine(line)
|
||||
if matches!(
|
||||
r.directional_axis(),
|
||||
Some(super::parametric_constraints::DirectionalAxis::TextBaseline)
|
||||
) => line,
|
||||
EntityGeom::Line(line) | EntityGeom::Ray(line) | EntityGeom::XLine(line)
|
||||
if r.marker.is_none() => line,
|
||||
EntityGeom::Polyline { .. } => geometry.line_segment(r.segment_index()?)?,
|
||||
EntityGeom::Ellipse(ellipse)
|
||||
if matches!(
|
||||
r.directional_axis(),
|
||||
Some(
|
||||
super::parametric_constraints::DirectionalAxis::EllipseMajor
|
||||
| super::parametric_constraints::DirectionalAxis::EllipseMinor
|
||||
)
|
||||
) => GLine {
|
||||
p1: ellipse.center,
|
||||
p2: ellipse.focus1,
|
||||
},
|
||||
_ => return None,
|
||||
};
|
||||
Some((line, minor_axis))
|
||||
};
|
||||
|
||||
let point_on_bounded_arc = |sys: &mut System, point: GPoint, arc: GArc| {
|
||||
let initial = BoundedArcValue::initial_parameter(sys.store(), point, arc);
|
||||
@@ -1108,30 +1139,49 @@ fn build_constraint(
|
||||
ConstraintKind::Coincident | ConstraintKind::Concentric | ConstraintKind::CenterPoint => {
|
||||
point_pair_equal(sys, cache, &c.refs)
|
||||
}
|
||||
ConstraintKind::Horizontal => match c.refs.as_slice() {
|
||||
[r] => whole_line(sys, cache, *r)
|
||||
.map(|line| {
|
||||
vec![Rc::new(Equal::new(line.p1.y, line.p2.y, 1.0)) as Rc<dyn Constraint>]
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
[a, b] => match (point_ref(sys, cache, *a), point_ref(sys, cache, *b)) {
|
||||
(Some(a), Some(b)) => vec![Rc::new(Equal::new(a.y, b.y, 1.0))],
|
||||
ConstraintKind::Horizontal | ConstraintKind::Vertical => {
|
||||
let fallback = if c.kind == ConstraintKind::Vertical {
|
||||
Vector3::UNIT_Y
|
||||
} else {
|
||||
Vector3::UNIT_X
|
||||
};
|
||||
let direction = c.axis_direction.unwrap_or(fallback);
|
||||
let length = direction.x.hypot(direction.y);
|
||||
let (dx, dy) = if length > 1.0e-12 {
|
||||
(direction.x / length, direction.y / length)
|
||||
} else {
|
||||
(fallback.x, fallback.y)
|
||||
};
|
||||
let datum = GLine {
|
||||
p1: GPoint::new(sys.add_param(0.0, true), sys.add_param(0.0, true)),
|
||||
p2: GPoint::new(sys.add_param(dx, true), sys.add_param(dy, true)),
|
||||
};
|
||||
match c.refs.as_slice() {
|
||||
[reference] => directional_line(sys, cache, *reference)
|
||||
.map(|(line, minor_axis)| {
|
||||
if minor_axis {
|
||||
vec![Rc::new(PerpendicularConstraint::new(
|
||||
sys.store(),
|
||||
line,
|
||||
datum,
|
||||
)) as Rc<dyn Constraint>]
|
||||
} else {
|
||||
vec![Rc::new(ParallelConstraint::new(sys.store(), line, datum))
|
||||
as Rc<dyn Constraint>]
|
||||
}
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
[a, b] => match (point_ref(sys, cache, *a), point_ref(sys, cache, *b)) {
|
||||
(Some(a), Some(b)) => vec![Rc::new(ParallelConstraint::new(
|
||||
sys.store(),
|
||||
GLine { p1: a, p2: b },
|
||||
datum,
|
||||
))],
|
||||
_ => Vec::new(),
|
||||
},
|
||||
_ => Vec::new(),
|
||||
},
|
||||
_ => Vec::new(),
|
||||
},
|
||||
ConstraintKind::Vertical => match c.refs.as_slice() {
|
||||
[r] => whole_line(sys, cache, *r)
|
||||
.map(|line| {
|
||||
vec![Rc::new(Equal::new(line.p1.x, line.p2.x, 1.0)) as Rc<dyn Constraint>]
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
[a, b] => match (point_ref(sys, cache, *a), point_ref(sys, cache, *b)) {
|
||||
(Some(a), Some(b)) => vec![Rc::new(Equal::new(a.x, b.x, 1.0))],
|
||||
_ => Vec::new(),
|
||||
},
|
||||
_ => Vec::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
ConstraintKind::Parallel => {
|
||||
let [a, b] = c.refs.as_slice() else {
|
||||
return Vec::new();
|
||||
@@ -1147,39 +1197,6 @@ fn build_constraint(
|
||||
let [a, b] = c.refs.as_slice() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let directional_line =
|
||||
|sys: &mut System, cache: &mut HashMap<_, _>, r: ParametricRef| {
|
||||
let geometry = resolve_ref(document, sys, cache, r)?;
|
||||
let minor_axis = matches!(
|
||||
r.directional_axis(),
|
||||
Some(super::parametric_constraints::DirectionalAxis::EllipseMinor)
|
||||
);
|
||||
let line = match geometry {
|
||||
EntityGeom::TextLine(line)
|
||||
if matches!(
|
||||
r.directional_axis(),
|
||||
Some(super::parametric_constraints::DirectionalAxis::TextBaseline)
|
||||
) => line,
|
||||
EntityGeom::Line(line) | EntityGeom::Ray(line) | EntityGeom::XLine(line)
|
||||
if r.marker.is_none() => line,
|
||||
EntityGeom::Polyline { .. } => {
|
||||
geometry.line_segment(r.segment_index()?)?
|
||||
}
|
||||
EntityGeom::Ellipse(ellipse)
|
||||
if matches!(
|
||||
r.directional_axis(),
|
||||
Some(
|
||||
super::parametric_constraints::DirectionalAxis::EllipseMajor
|
||||
| super::parametric_constraints::DirectionalAxis::EllipseMinor
|
||||
)
|
||||
) => GLine {
|
||||
p1: ellipse.center,
|
||||
p2: ellipse.focus1,
|
||||
},
|
||||
_ => return None,
|
||||
};
|
||||
Some((line, minor_axis))
|
||||
};
|
||||
let (Some((fixed, fixed_minor)), Some((moving, moving_minor))) =
|
||||
(directional_line(sys, cache, *a), directional_line(sys, cache, *b))
|
||||
else {
|
||||
@@ -2184,7 +2201,21 @@ fn constrained_line_axes(constraints: &[&ParametricConstraint]) -> HashMap<Param
|
||||
for c in constraints.iter().filter(|c| c.enabled) {
|
||||
if let [reference] = c.refs.as_slice() {
|
||||
if matches!(c.kind, ConstraintKind::Horizontal | ConstraintKind::Vertical) {
|
||||
axes.insert(*reference, c.kind == ConstraintKind::Vertical);
|
||||
let fallback = if c.kind == ConstraintKind::Vertical {
|
||||
Vector3::UNIT_Y
|
||||
} else {
|
||||
Vector3::UNIT_X
|
||||
};
|
||||
let direction = c.axis_direction.unwrap_or(fallback);
|
||||
let length = direction.x.hypot(direction.y);
|
||||
if length > 1.0e-12 {
|
||||
let (x, y) = (direction.x.abs() / length, direction.y.abs() / length);
|
||||
if x >= 1.0 - 1.0e-10 {
|
||||
axes.insert(*reference, false);
|
||||
} else if y >= 1.0 - 1.0e-10 {
|
||||
axes.insert(*reference, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2292,6 +2323,7 @@ fn solve_scope(
|
||||
rigid_points: Vec::new(),
|
||||
distance_direction_type: distance_direction_type::UNDIRECTED,
|
||||
distance_direction: None,
|
||||
axis_direction: None,
|
||||
angle_sector: angle_sector::PARALLEL_COUNTERCLOCKWISE,
|
||||
};
|
||||
for constraint in build_constraint(
|
||||
@@ -2617,7 +2649,12 @@ fn solve_scope(
|
||||
};
|
||||
let directional = set.constraints.iter().any(|constraint| {
|
||||
constraint.enabled
|
||||
&& constraint.kind == ConstraintKind::Perpendicular
|
||||
&& matches!(
|
||||
constraint.kind,
|
||||
ConstraintKind::Horizontal
|
||||
| ConstraintKind::Vertical
|
||||
| ConstraintKind::Perpendicular
|
||||
)
|
||||
&& constraint.refs.iter().any(|reference| {
|
||||
reference.entity == *handle && reference.directional_axis().is_some()
|
||||
})
|
||||
@@ -2658,7 +2695,12 @@ fn solve_scope(
|
||||
};
|
||||
let directional = set.constraints.iter().any(|constraint| {
|
||||
constraint.enabled
|
||||
&& constraint.kind == ConstraintKind::Perpendicular
|
||||
&& matches!(
|
||||
constraint.kind,
|
||||
ConstraintKind::Horizontal
|
||||
| ConstraintKind::Vertical
|
||||
| ConstraintKind::Perpendicular
|
||||
)
|
||||
&& constraint.refs.iter().any(|reference| {
|
||||
reference.entity == *handle
|
||||
&& matches!(
|
||||
@@ -3291,6 +3333,7 @@ impl Scene {
|
||||
rigid_points: Vec::new(),
|
||||
distance_direction_type: 0,
|
||||
distance_direction: None,
|
||||
axis_direction: None,
|
||||
angle_sector: angle_sector::PARALLEL_COUNTERCLOCKWISE,
|
||||
};
|
||||
let mut system = System::new();
|
||||
|
||||
Reference in new issue
Block a user