Rotation::evaluateVector() computes angle = 2*acos(w) which gives
values in [0, 2*pi]. When the relative quaternion has w < 0 (opposite
hemisphere), the angle exceeds pi even though q and -q represent the
same rotation. This caused the validator to report ~350 degree 'flips'
and reject valid solver output.
Fix: map the angle to [0, pi] before comparing against the 91-degree
threshold. This is the short-arc equivalent — the minimum rotation
angle between two orientations regardless of quaternion sign convention.
Points solver to fix/drag-quat-continuity (solver#40) which fixes
the planar constraint drift during interactive drag by enforcing
quaternion continuity on dragged parts and detecting branch jumps
beyond simple hemisphere negation.
Updates solver to include the fix for PlanarConstraint axial drift
when combined with CylindricalConstraint. The distance residual now
uses a world-frame reference normal (Const nodes) instead of the
body-attached normal that rotates with the body.
Updates mods/solver to include fix for the planar half-space correction
that caused 'flipped orientation' rejections when dragging a body
connected by a Cylindrical joint + distance=0 Planar constraint.
The solver's PlanarConstraint half-space tracker was reflecting the body
through the plane when the face normal dot product crossed zero during
legitimate rotation about the cylindrical axis. Now returns a tracking-
only HalfSpace (no correction) for on-plane constraints, matching the
pattern used by Cylindrical/Revolute/Concentric trackers.
See: kindred/solver#38
When parts with structured part numbers (e.g., P03-0001) are inserted
into an assembly multiple times, UniqueNameManager::decomposeName()
treats the trailing digits as an auto-generated suffix and increments
them (P03-0002, P03-0003), corrupting the part number.
Add a makeInstanceLabel() helper in AssemblyLink.cpp that appends -N
instance suffixes instead (P03-0001-1, P03-0001-2). All instances get
a suffix starting at -1 so the original part number is never modified.
Applied at all three Label.setValue() sites in
synchronizeComponents() (AssemblyLink, link group, and regular link
creation paths).
Also add a UniqueNameManager test documenting the trailing-digit
decomposition behavior for structured part numbers.
Closes#327
Three QSS issues caused headings to render with only the top ~60%
visible:
- QGroupBox: margin-top 12px was insufficient for the title rendered
in subcontrol-origin: margin. Increased to 16px and added 2px
vertical padding to the title.
- QDockWidget::title: min-height 18px conflicted with padding 8px 6px,
constraining the content area. Removed min-height to let Qt auto-size
from padding + font metrics.
- QSint--ActionGroup QToolButton: min-height 18px forced a height that
was then clipped by the C++ setFixedHeight(headerSize) calculation.
Set min-height to 0px so the C++ layout controls sizing.
Closes#325
The publish-release job was missing the target_commitish field in the
Gitea release creation API payload. Without it, Gitea cannot resolve
the tag to a git object and returns HTTP 500 with 'object does not
exist'.
Add COMMIT_SHA (from github.sha) to the job env and pass it as
target_commitish in the JSON payload.
Closes#326
The interactive drag section described the original naive implementation
(re-solve from scratch each step) and called the caching layer a
'planned future optimization'. In reality _DragCache is fully
implemented: pre_drag() builds the system, Jacobian, and compiled
evaluator once, and drag_step() reuses them.
Update code snippets, add _DragCache field table, and document the
single_equation_pass exclusion from the drag path.
Picks up fix/drag-orientation-stability (kindred/solver#36):
- Half-space tracking for all compound constraints with branch ambiguity
- Quaternion continuity enforcement during interactive drag
The datum plane detection in getDistanceType() only checked for
App::Plane (origin planes). This missed two other class hierarchies:
- PartDesign::Plane (inherits Part::Datum, NOT App::Plane)
- Part::Plane primitive referenced bare (no Face element)
Both produce empty element types (sub-name ends with ".") but failed
the isDerivedFrom<App::Plane>() check, falling through to
DistanceType::Other and the Planar fallback. This caused incorrect
constraint geometry, leading to conflicting/unsatisfiable constraints
and solver failures.
Add shape-based isDatumPlane/Line/Point helpers that cover all three
hierarchies by inspecting the actual OCCT geometry rather than relying
on class identity alone. Extend getDistanceType() to use these helpers
for all datum-vs-datum and datum-vs-element combinations.
Adds TestDatumClassification.py with tests for PartDesign::Plane,
Part::Plane (bare ref), and cross-hierarchy datum combinations.
The datum plane detection in getDistanceType() only checked for
App::Plane (origin planes). This missed two other class hierarchies:
- PartDesign::Plane (inherits Part::Datum, NOT App::Plane)
- Part::Plane primitive referenced bare (no Face element)
Both produce empty element types (sub-name ends with ".") but failed
the isDerivedFrom<App::Plane>() check, falling through to
DistanceType::Other and the Planar fallback. This caused incorrect
constraint geometry, leading to conflicting/unsatisfiable constraints
and solver failures.
Add shape-based isDatumPlane/Line/Point helpers that cover all three
hierarchies by inspecting the actual OCCT geometry rather than relying
on class identity alone. Extend getDistanceType() to use these helpers
for all datum-vs-datum and datum-vs-element combinations.
Adds TestDatumClassification.py with tests for PartDesign::Plane,
Part::Plane (bare ref), and cross-hierarchy datum combinations.
When a Distance joint references a datum plane (XY_Plane, XZ_Plane,
YZ_Plane), getDistanceType() failed to recognize it because datum
plane sub-names yield an empty element type. This caused the fallback
to DistanceType::Other → BaseJointKind::Planar, which adds spurious
parallel-normal residuals that overconstrain the system.
For example, three vertex-to-datum-plane Distance joints produced
10 residuals (3×Planar) with 6 mutually contradictory orientation
constraints, causing the solver to find garbage least-squares
solutions.
Add early detection of App::Plane datum objects before the main
geometry classification chain. Datum planes are now correctly mapped:
- Vertex + DatumPlane → PointPlane → PointInPlane (1 residual)
- Edge + DatumPlane → LinePlane → LineInPlane
- Face/DatumPlane + DatumPlane → PlanePlane → Planar
When a Distance joint references a datum plane (XY_Plane, XZ_Plane,
YZ_Plane), getDistanceType() failed to recognize it because datum
plane sub-names yield an empty element type. This caused the fallback
to DistanceType::Other → BaseJointKind::Planar, which adds spurious
parallel-normal residuals that overconstrain the system.
For example, three vertex-to-datum-plane Distance joints produced
10 residuals (3×Planar) with 6 mutually contradictory orientation
constraints, causing the solver to find garbage least-squares
solutions.
Add early detection of App::Plane datum objects before the main
geometry classification chain. Datum planes are now correctly mapped:
- Vertex + DatumPlane → PointPlane → PointInPlane (1 residual)
- Edge + DatumPlane → LinePlane → LineInPlane
- Face/DatumPlane + DatumPlane → PlanePlane → Planar
When a Distance joint references a datum plane (XY_Plane, XZ_Plane,
YZ_Plane), getDistanceType() failed to recognize it because datum
plane sub-names yield an empty element type. This caused the fallback
to DistanceType::Other → BaseJointKind::Planar, which adds spurious
parallel-normal residuals that overconstrain the system.
For example, three vertex-to-datum-plane Distance joints produced
10 residuals (3×Planar) with 6 mutually contradictory orientation
constraints, causing the solver to find garbage least-squares
solutions.
Add early detection of App::Plane datum objects before the main
geometry classification chain. Datum planes are now correctly mapped:
- Vertex + DatumPlane → PointPlane → PointInPlane (1 residual)
- Edge + DatumPlane → LinePlane → LineInPlane
- Face/DatumPlane + DatumPlane → PlanePlane → Planar
When a Distance joint references a datum plane (XY_Plane, XZ_Plane,
YZ_Plane), getDistanceType() failed to recognize it because datum
plane sub-names yield an empty element type. This caused the fallback
to DistanceType::Other → BaseJointKind::Planar, which adds spurious
parallel-normal residuals that overconstrain the system.
For example, three vertex-to-datum-plane Distance joints produced
10 residuals (3×Planar) with 6 mutually contradictory orientation
constraints, causing the solver to find garbage least-squares
solutions.
Add early detection of App::Plane datum objects before the main
geometry classification chain. Datum planes are now correctly mapped:
- Vertex + DatumPlane → PointPlane → PointInPlane (1 residual)
- Edge + DatumPlane → LinePlane → LineInPlane
- Face/DatumPlane + DatumPlane → PlanePlane → Planar
During drag operations, validateNewPlacements() compared each solver
result against the pre-drag positions saved once in preDrag(). As the
user dragged further, the cumulative rotation from that fixed baseline
easily exceeded the 91-degree threshold, causing valid intermediate
results to be rejected with 'flipped orientation' warnings and making
parts appear to explode.
Fix: call savePlacementsForUndo() after each accepted drag step so
that the flip check compares against the last accepted state rather
than the original pre-drag origin.
Two code paths were appending silo/manifest.json to the ZIP without
removing the previous entry, causing Python's zipfile module to warn
about duplicate names:
1. slotFinishSaveDocument() re-injected the cached manifest from
entries, then the modified_at update branch wrote a second copy.
2. update_manifest_fields() opened the ZIP in append mode and wrote
an updated manifest without removing the old one.
Fix slotFinishSaveDocument() by preparing the final manifest (with
updated modified_at) in the entries dict before writing, so only one
copy is written to the ZIP.
Fix update_manifest_fields() by rewriting the ZIP atomically via a
temp file, deduplicating any pre-existing duplicate entries in the
process.
updateSolveStatus() calls solve() when lastResult_.placements is empty,
but solve() calls updateSolveStatus() at the end. When an assembly has
zero constraints (all joints removed), the solver returns zero
placements, causing infinite recursion until stack overflow (segfault).
Add a static re-entrancy guard so the recursive solve() call is skipped
if updateSolveStatus() is already on the call stack.