Assembly Origin Planes: - AssemblyObject::setupObject() relabels origin planes to Top (XY), Front (XZ), Right (YZ) on assembly creation - CommandCreateAssembly.py makes origin planes visible by default - AssemblyUtils.cpp getObjFromRef() resolves LocalCoordinateSystem to child datum elements for joint references to origin planes - TestAssemblyOriginPlanes.py: 9 integration tests covering structure, labels, grounding, reference resolution, solver, and save/load round-trip Solver Documentation: - docs/src/solver/: 7 new pages covering architecture overview, expression DAG, constraints, solving algorithms, diagnostics, assembly integration, and writing custom solvers - docs/src/SUMMARY.md: added Kindred Solver section
5.4 KiB
Constraints
Each constraint type maps to a class that produces residual expressions. The residuals equal zero when the constraint is satisfied. The number of residuals equals the number of degrees of freedom removed.
Source: mods/solver/kindred_solver/constraints.py, mods/solver/kindred_solver/geometry.py
Constraint vocabulary
Point constraints
| Type | DOF removed | Residuals |
|---|---|---|
| Coincident | 3 | p_i - p_j (world-frame marker origins coincide) |
| PointOnLine | 2 | Two components of (p_i - p_j) x z_j (point lies on line through p_j along z_j) |
| PointInPlane | 1 | (p_i - p_j) . z_j - offset (signed distance to plane) |
Orientation constraints
| Type | DOF removed | Residuals |
|---|---|---|
| Parallel | 2 | Two components of z_i x z_j (cross product of Z-axes is zero) |
| Perpendicular | 1 | z_i . z_j (dot product of Z-axes is zero) |
| Angle | 1 | z_i . z_j - cos(angle) |
Axis/surface constraints
| Type | DOF removed | Residuals |
|---|---|---|
| Concentric | 4 | Parallel Z-axes (2) + point-on-line (2) |
| Tangent | 1 | (p_i - p_j) . z_j (signed distance along normal) |
| Planar | 3 | Parallel normals (2) + point-in-plane (1) |
| LineInPlane | 2 | Point-in-plane (1) + z_i . n_j (line direction perpendicular to normal) (1) |
Kinematic joints
| Type | DOF removed | DOF remaining | Residuals |
|---|---|---|---|
| Fixed | 6 | 0 | Coincident origins (3) + quaternion error imaginary parts (3) |
| Ball | 3 | 3 | Coincident origins (same as Coincident) |
| Revolute | 5 | 1 (rotation about Z) | Coincident origins (3) + parallel Z-axes (2) |
| Cylindrical | 4 | 2 (rotation + slide) | Parallel Z-axes (2) + point-on-line (2) |
| Slider | 5 | 1 (slide along Z) | Parallel Z-axes (2) + point-on-line (2) + twist lock: x_i . y_j (1) |
| Screw | 5 | 1 (helical) | Cylindrical (4) + pitch coupling: axial - pitch * qz_rel / pi (1) |
| Universal | 4 | 2 (rotation about each Z) | Coincident origins (3) + perpendicular Z-axes (1) |
Mechanical elements
| Type | DOF removed | Residuals |
|---|---|---|
| Gear | 1 | r_i * qz_i + r_j * qz_j (coupled rotation via quaternion Z-components) |
| RackPinion | 1 | translation - 2 * pitch_radius * qz_i (rotation-translation coupling) |
| Cam | 0 | Stub (no residuals) |
| Slot | 0 | Stub (no residuals) |
Distance constraints
| Type | DOF removed | Residuals |
|---|---|---|
| DistancePointPoint | 1 | |p_i - p_j|^2 - d^2 (squared form avoids sqrt in Jacobian) |
| DistanceCylSph | 0 | Stub (geometry classification dependent) |
Marker convention
Every constraint references two parts (body_i, body_j) with local coordinate frames called markers. Each marker has a position (attachment point on the part) and a quaternion (orientation).
The marker Z-axis defines the constraint direction:
- Revolute: Z-axis = hinge axis
- Planar: Z-axis = face normal
- PointOnLine: Z-axis = line direction
- Slider: Z-axis = slide direction
The solver computes world-frame marker axes by composing the body quaternion with the marker quaternion: q_world = q_body * q_marker, then rotating unit vectors through the result.
Fixed constraint orientation
The Fixed constraint locks all 6 DOF using a quaternion error formulation:
- Compute total orientation:
q_i = q_body_i * q_marker_i,q_j = q_body_j * q_marker_j - Compute relative quaternion:
q_err = conj(q_i) * q_j - When orientations match,
q_erris the identity quaternion(1, 0, 0, 0) - Residuals are the three imaginary components of
q_err(should be zero)
The quaternion normalization constraint on each body provides the fourth equation needed to fully determine the quaternion.
Rotation proxies for mechanical constraints
Gear, RackPinion, and Screw constraints need to measure rotation angles. Rather than extracting Euler angles (which would introduce transcendentals), they use the Z-component of a relative quaternion as a proxy:
q_local = conj(q_marker) * q_body * q_marker
angle ~ 2 * qz_local (for small angles)
This is exact at the solution and has correct gradient direction, which is sufficient for Newton-Raphson convergence from a nearby initial guess.
Geometry helpers
The geometry.py module provides Expr-level vector operations used by constraint classes:
marker_z_axis(body, marker_quat)-- world-frame Z-axis viaquat_rotate(q_body * q_marker, [0,0,1])marker_x_axis(body, marker_quat)-- world-frame X-axis (used by Slider twist lock)marker_y_axis(body, marker_quat)-- world-frame Y-axis (used by Slider twist lock)dot3(a, b)-- dot product of Expr triplescross3(a, b)-- cross product of Expr triplespoint_plane_distance(point, origin, normal)-- signed distancepoint_line_perp_components(point, origin, dir)-- two perpendicular distance components
Writing a new constraint
To add a constraint type:
- Subclass
ConstraintBaseinconstraints.py - Implement
residuals()returning a list ofExprnodes - Add a case in
solver.py:_build_constraint()to instantiate it fromBaseJointKind - Add the
BaseJointKindvalue to_SUPPORTEDinsolver.py - Add the residual count to the tables in
decompose.py