Merge pull request #1334 from ramox81/codex/horizontal-constraint-parity

This commit is contained in:
Hakan Seven committed 2026-09-17 17:39:44 +03:00
commit f9c59503f0
10 files changed
+710 -88

No files matched your search

+42 -22
View File
@@ -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,
+66
View File
@@ -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
View File
@@ -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();