Skip to content

Add continuous collision checking - #13

Merged
rjoomen merged 117 commits into
tesseract-robotics:mainfrom
rjoomen:claude/update-review-md-8p8Ek
Apr 8, 2026
Merged

Add continuous collision checking#13
rjoomen merged 117 commits into
tesseract-robotics:mainfrom
rjoomen:claude/update-review-md-8p8Ek

Conversation

@rjoomen

@rjoomen rjoomen commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Implements discrete and continuous (swept/cast) collision detection using the Coal library
  • Architecture mirrors Tesseract's Bullet collision implementation
  • Includes comprehensive test coverage and benchmarks

Re-opened to preserve commit history (PR #12 was squash-merged by mistake).

🤖 Generated with Claude Code

rjoomen and others added 30 commits February 25, 2026 08:58
…nager

The first loop over link2cow_ was adding kinematic objects' regular COW
pointers to the dynamic broadphase update, but those objects are not
registered in the dynamic manager — only the cast COWs are. This could
cause undefined behavior in Coal's broadphase update. Now only static
regular COWs are added to static_update_; kinematic cast COWs are
handled exclusively by the second loop.

https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
The early-out compared only pose1 against the stored transform, so
changes to pose2 alone were silently ignored — the cast transform and
broadphase AABB were never updated. This matches the Bullet continuous
collision manager which unconditionally applies both poses.

https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
Replace linear std::find searches against broadphase manager object
lists with std::unordered_set lookups in removeCollisionObject
(discrete) and removeObjects (cast). This reduces the complexity of
unregistering N collision objects from two broadphase managers
containing M objects from O(N*M) to O(N+M).

https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
…update

extractVertices() calls toTriangleMesh() for curved shapes (Sphere,
Cylinder, Cone, Capsule), which is an expensive tessellation operation.
Previously this ran on every updateCastTransform() call. Now the base
vertices are extracted once at construction and cached in base_vertices_,
so computeSweptVertices() only needs to apply the transform to the
cached points.

https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
Previously, if an unrecognized shape type was passed to
computeBV<AABB, CastHullShape>, none of the dynamic_cast branches
would match and bv_original/bv_cast would remain uninitialized,
producing a garbage bounding volume (undefined behavior).

Add an else branch that builds the AABB from the pre-computed swept
vertices, which are available for any shape type. Also add a
getSweptVertices() accessor to CastHullShape.

https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
The CollisionCallback and DistanceCallback are shared between discrete
and continuous collision checking. When used by the continuous cast
manager (CoalCastBVHManager), the three continuous-collision fields on
ContactResult were never populated, leaving them at their defaults
(cc_time=-1, cc_type=CCType_None, cc_transform=Identity).

Add populateContinuousCollisionFields() which detects CastHullShape
geometry on each collision object and computes:
 - cc_transform: end-of-motion world pose (pose1 * castTransform)
 - cc_time: linear interpolation parameter by projecting the contact
   point onto the start-to-end motion trajectory
 - cc_type: Time0 / Between / Time1 classification

The function is a no-op for discrete checks (no CastHullShape present).

https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
…uousData

Replace the linear projection approach for cc_time computation with Bullet's
support-function-based approach. This uses GetAverageSupport to find the
shape's extreme points along the contact normal at t=0 and t=1, then
classifies collision time based on which pose has greater support projection.

Key changes:
- Add GetAverageSupport helper that averages support vertices for polyhedral
  shapes (Box, ConvexBase32) and falls back to coal::details::getSupport
  for smooth shapes
- Use average contact point (midpoint of nearest_points) matching Bullet
- Normal convention: from current object toward other (negate for obj 1)
- In Between case, update nearest_points_local to averaged support point
- Add tolerance constants matching Bullet (0.01 support, 0.001 length)

https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
Box is a smooth primitive in COAL with a unique support vertex per
direction, so vertex averaging is unnecessary. Only ConvexBase32 needs
explicit vertex iteration since multiple vertices can share a support plane.

https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
COAL's getSupport already handles ConvexBase32, so the manual vertex
iteration and averaging is unnecessary. The support *value* is identical;
only the Between-case cc_time could differ slightly (arbitrary vertex vs
face center), but the Time0/Time1/Between classification is unchanged.

Rename to GetSupport and remove unused COAL_EPSILON constant.

https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
updateCastTransform() was not calling computeLocalAABB(), so the
geometry's local AABB stayed at its initial (identity cast transform)
size. The broadphase then used a tiny AABB that didn't cover the swept
path, causing no candidate pairs to be generated and all CastBVH
collision tests to return empty results.

Also reorder setCollisionObjectsTransform(name, pose1, pose2) to update
the cast transform before setting the world transform, so that
updateAABB() picks up the correct aabb_local.

https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
CastHullShape previously extended coal::ShapeBase directly, which meant
it reported BV_UNKNOWN as its node type. COAL's collision dispatcher
doesn't recognize BV_UNKNOWN and throws:
  "Collision function between node type GEOM_BOX and node type
   BV_UNKNOWN is not yet supported."

By extending coal::ConvexBase32, CastHullShape:
- Reports GEOM_CONVEX as its node type
- Is recognized by COAL's GJK-based collision solver
- Uses ConvexBase32::points (set to swept vertices) for the support
  function, giving correct swept hull collision

The swept vertices are stored directly in the inherited `points` member,
removing the separate `swept_vertices_` field. A new updateConvexMembers()
helper keeps num_points and center in sync. clone() builds from scratch
to ensure the points alias is maintained.

https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
ConvexBaseTpl::clone() returns ConvexBaseTpl*, so the override must
return a covariant type (CastHullShape*), not ShapeBase*.

https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
…nvex hull

CastHullShape previously derived from ConvexBase32 and only stored vertices
without populating the neighbors data structure. This caused a crash in
coal::details::getShapeSupportLog because the GJK hill-climbing support
function requires neighbor adjacency data.

Now derives from ConvexTpl<Triangle32> and uses ConvexBase32::convexHull()
(which internally uses qhull) to compute a proper convex hull of the swept
vertices. The ConvexTpl constructor calls fillNeighbors() and
buildSupportWarmStart(), ensuring all data structures are properly populated.

Replaces the manual computeSweptVertices()/updateConvexMembers() methods
with a single buildConvexHull() method that produces a complete convex
polytope with points, triangles, neighbors, and support warm-start data.

https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
The convex hull approach uses vertex-based support (vertices at z=±0.125
for the box corners) whereas the exact Box support function can return
face points at z=0 for horizontal directions. Coal adjusts the contact
point by the security_margin (0.1), yielding z = -0.125 + 0.1 = -0.025
instead of the ideal z = 0.0.

Widen the Z-component tolerances from 0.001 to 0.03 to accommodate this
inherent limitation of vertex-based convex hull collision detection.

https://claude.ai/code/session_01JpfY1C2isv3cgud2grpKro
…t function

Bypass coal::ComputeCollision for CastHullShape collisions by injecting a
custom support function into MinkowskiDiff that implements the Schulman et al.
swept-shape support: max(support_start(d), support_end(d)). This runs GJK/EPA
directly, extracting witness points and penetration depth. Also adds explicit
template instantiations for getShapeSupport<CastHullShape> so they are
available from other translation units.

https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
…ata API

Use default constructor + member assignment instead of the parameterized
ContactTestData constructor, making this compatible regardless of whether
the 'active' parameter was removed (tesseract PR #1250) or not.

https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
Coal's getSupportTpl computes shape1's support in direction -dir (negated),
matching the Minkowski difference convention: w = s_S0(d) - s_S1(-d).
Our custom support function was passing dir (positive) for shape1, giving
an incorrect Minkowski difference and causing GJK to miss collisions.

https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
When both collision objects are CastHullShapes (e.g. sphere-sphere cast
tests), shape1 must also use the Schulman support function directly.
Dispatching a CastHullShape (ConvexBase32) through coal's generic
getSupport mishandles the 32-bit index type, causing incorrect support
points and missed collisions.

https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
GJK's distance_upper_bound (set to security_margin ≈ 0.1) caused
NoCollisionEarlyStopped for CastHullShape collisions. The swept volumes
extend much farther than the security_margin, so the Minkowski difference
support values easily exceeded the threshold, triggering early stopping
before the simplex could enclose the origin.

Set distance_upper_bound to max for CastHullShape to let GJK fully
converge. This fixes sphere-sphere and contact manager cast collision
tests where both shapes are CastHullShapes with large swept extents.

https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
The custom castHullCollide function with Schulman support injection had
two issues:
1. GJK distance_upper_bound (security_margin ≈ 0.1) triggered premature
   NoCollisionEarlyStopped for large swept volumes
2. GJK PolyakAcceleration + DualityGap convergence failed to detect
   collision for primitive sphere underlying shapes

Since CastHullShape IS a ConvexBase32 with correctly-built swept hull
vertices, coal's standard ComputeCollision functor handles collision
detection robustly via its built-in ConvexBase32 GJK support.

The continuous-collision fields (cc_time, cc_transform) are still
computed by populateContinuousCollisionFields using the underlying
shape's exact support function for precise time interpolation.

https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
The previous commit replaced the custom Schulman support GJK path with
coal's standard ComputeCollision, which treated CastHullShape as a plain
ConvexBase32. This caused accuracy regressions because the convex hull
mesh approximation of spheres is inscribed in the actual sphere, leading
to distance, witness point, and cc_time errors.

This restores the custom castHullCollide/castHullGetSupportFunc functions
that inject the Schulman support function (exact support of underlying
shape at t=0 and t=1) into GJK's MinkowskiDiff. The key fix is using
DefaultGJK (no acceleration) instead of PolyakAcceleration, whose
momentum-based acceleration interferes with the discontinuous Schulman
support function and caused convergence failures for shapes with
continuous support (Sphere, Cylinder, Capsule).

Also keeps distance_upper_bound=max for CastHullShape to prevent GJK
early stopping on large swept volumes.

https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
When the Schulman support function computes equal projections for start
and end support points (common for shapes with continuous support like
Sphere with pure translational sweep), always returning supportStart
caused GJK simplex degeneration - the same vertex was returned for
many nearby directions, preventing convergence.

Fix: return the average of start/end support points when projections
are within epsilon. This is geometrically valid (midpoint lies on the
convex hull boundary with the same max projection) and gives GJK
distinct vertices for different directions.

Also improve the initial GJK guess to use the center difference between
shapes instead of a hardcoded (1,0,0) axis.

https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
Add descriptive failure messages to all EXPECT_*/ASSERT_* calls in the
box-box and sphere-sphere cast collision test suites. When a test fails,
the output now explains:
- Which scenario is running (via SCOPED_TRACE with ContactTestType)
- Full contact result state dump (distance, normal, nearest_points,
  cc_time, cc_type) for context on any failure
- What each expected value represents geometrically (e.g., "static_box
  surface at x=-0.5", "collision at 25% of sweep")
- Which link is in which result slot when ordering varies

Also changes bare EXPECT_TRUE(empty) to ASSERT_FALSE(empty) with
messages explaining the geometry that should produce a collision,
so missing-contact failures abort early with actionable diagnostics
instead of crashing on result_vector[0] access.

https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
castHullCollide: Log GJK status (Valid/Failed/EarlyStopped/DidNotRun),
distance, shape types, and transforms when no collision is found. This
helps diagnose why primitive sphere-sphere CastHullShape pairs fail to
detect collisions.

populateContinuousCollisionFields: Replace Euclidean distance-based
cc_time interpolation with projection along the support point sweep
trajectory (pt_world0 → pt_world1). The projection is more robust
because it isolates motion along the sweep direction and avoids skew
from off-axis components of convex mesh support vertices.

https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
Coal's GJK::Status enum uses NoCollision (not Valid) for the
converged-but-separated case.

https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
Three fixes for CastHullShape continuous collision detection:

1. Use WithSweptSphere support mode in GJK and cc_time computation:
   Coal's getSupport<NoSweptSphere> returns zero for primitive shapes
   (Sphere, Capsule, etc.) because coal treats them as "point + swept
   sphere radius". Our custom GJK bypasses coal's swept-sphere handling,
   so we must use WithSweptSphere to include the shape's radius in the
   support point. This fixes primitive sphere-sphere returning zero
   contacts.

2. Use center-based cc_time interpolation:
   Previously cc_time was computed by projecting the contact point onto
   the support vertex trajectory. For tessellated meshes, the support
   vertex has off-axis components (e.g., (0.237, -0.077, 0.25) instead
   of ideal (0.25, 0, 0)), causing cc_time skew. Using the shape center
   trajectory instead eliminates this tessellation artifact.

3. Project nearest_points_local onto contact normal:
   The averaged support point used for nearest_points_local had off-axis
   components from mesh tessellation. Projecting the support distance
   onto the contact normal direction gives the correct surface point
   along the normal, matching Bullet's behavior.

https://claude.ai/code/session_01J4J9n5ZW4N7t2Nroj6Tk4W
claude and others added 28 commits March 18, 2026 13:22
…shape_id, normal norm

Add collision_cast_cctype_unit (shared Bullet+Coal) that exercises two
previously untested code paths in populateContinuousCollisionFields:

  CCType_Time1: moving sphere translates in +x into a static sphere.
    normal·sweep > 0 → CCType_Time1, cc_time=1.0.  Before the
    link-frame world-supports fix Coal always produced CCType_Between
    for pure translations; this test would have caught that.

  CCType_Time0: moving sphere starts overlapping and retreats in -x.
    normal·sweep < 0 → CCType_Time0, cc_time=0.0.  Also untested.

Both scenarios verify: cc_type, cc_time, transform, cc_transform,
nearest_points (world frame), nearest_points_local (exact round-trip
via transform and cc_transform), normal direction + unit-norm, and
type_id propagation (moving_sphere registered with type_id=7).

Additional changes:
- Add EXPECT_NEAR(cr.normal.norm(), 1.0, 1e-4) to collision_box_box_cast,
  collision_sphere_sphere_cast (all four scenarios), and
  collision_multi_shape_cast.
- Add EXPECT_EQ(cr.shape_id[arm_idx], 0) to collision_multi_shape_cast
  to verify that the collision is attributed to sub-shape A (index 0)
  not sub-shape B, exercising the multi-shape shape_id code path.
- Update REVIEW.md test-coverage-gaps section to mark all addressed items.

https://claude.ai/code/session_01UHMKKdBePu1Bm1X2HuK21h
coal_utils.cpp: CCType_Time1 branch now uses shape_tf1 (end-pose transform)
when computing nearest_points_local, matching Bullet's calculateContinuousData.
This ensures transform[ki] * nearest_points_local[ki] == nearest_points[ki]
(the world-frame contact point at t=1). Previously shape_tf0 was used, giving
a link-local offset inconsistent with Bullet and producing wrong round-trip
values in tests.

collision_cast_cctype_unit.hpp:
- Update CCType_Time1 round-trip expectations: p_at_start ≈ (0.15,0,0)
  (= world contact), p_at_end ≈ (2.05,0,0) (sphere surface at end pose).
- Update CCType_Time0 round-trip comments to clarify shape_tf0 usage.
- Relax nearest_points y,z tolerances to 0.05 (Bullet GJK on CastHullShape
  capsule has up to ±0.03 numerical noise in off-axis components).
- Update file-level docblock with corrected formulas for both CCType branches.

REVIEW.md: note that CCType_Time1 uses shape_tf1 for nearest_points_local.

https://claude.ai/code/session_01UHMKKdBePu1Bm1X2HuK21h
Bullet's GJK on CastHullShape (capsule geometry) produces up to ~0.003 error
in the x-component and ~0.03 in the y,z components of nearest_points for both
the kinematic and static objects.  Apply consistent 0.005/0.05 tolerances for
x/yz to all four nearest_points checks in both CCType_Time0 and CCType_Time1
scenarios.

https://claude.ai/code/session_01UHMKKdBePu1Bm1X2HuK21h
Three bug fixes for Coal's continuous collision detection:

1. COW::clone() was not copying world_pose_, causing octree voxels to be
   placed at local coordinates instead of world coordinates when the cast
   manager was cloned (e.g. via getContinuousContactManager). This was
   the root cause of Coal finding far fewer octree contacts than Bullet.

2. GJK cache entries were not invalidated when cast transforms changed
   between trajectory steps, potentially causing stale separating-axis
   hints to make GJK miss collisions.

3. cc_time formula replaced with the distance-ratio formula from
   Schulman et al. IJRR 2014 Eq. (17): l0c/(l0c+l1c), matching Bullet.
   The previous projection formula was equivalent for pure translations
   but diverges for rotational sweeps.

Test refactoring:

- Promote coal_cast_manager_branches_unit and coal_vs_bullet_contact_fields
  from Coal-specific tests to shared test suites that run for both Bullet
  and Coal (collision_cast_disable_during_sweep_unit, collision_cast_scenario
  _unit). 7 scenarios now validate ContactResult self-consistency per
  backend independently; 2 cross-backend comparison tests retained.

- Replace backend-specific expected values in sphere-sphere convex hull
  cast test with geometric invariants (cc_time range, distance consistency,
  coordinate checks) so the test passes for any GJK implementation without
  an is_coal parameter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move non-runtime deps (bullet, fcl, yaml-cpp, cereal, tesseract_support)
  to <test_depend> in package.xml
- Extract applyCollisionFilterMask() helper replacing 3 copies of filter
  mask assignment logic
- Define kTransformEpsilon constant and transformChanged() helper replacing
  hardcoded 1e-8 checks across both managers
- Extract collectTransformUpdate() and flushBatchUpdate() private methods
  to deduplicate setCollisionObjectsTransform overloads

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The CoalCollisionGeometryCache uses weak_ptr entries that expire when
geometries are destroyed, but stale entries were never cleaned up.
Call prune() in both discrete and cast manager clone() methods to
prevent unbounded cache growth across manager lifecycles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ckends

The -gjk_tolerance threshold caused Coal to miss near-zero-distance
contacts (e.g. swept volume grazing) that Bullet and FCL report.
Other backends do not apply this correction, so Coal shouldn't either.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the static octree voxel expansion workaround now that Coal has
native GEOM_CUSTOM <-> GEOM_OCTREE narrowphase support. Static octrees
register as raw OcTree in the static broadphase instead of thousands of
CastHullShape-wrapped boxes. This eliminates hasNonShapeBaseGeometry(),
the dual-path routing, and the identity-transform reset on demotion.

Change the default m_collisionFilterGroup from KinematicFilter to
StaticFilter so newly added objects go directly to the static broadphase
without a wasted round-trip through dynamic.

Add swapObjects() compensation for Coal's ShapeOcTreeCollisionTraversalNode
which internally swaps arguments without fixing Contact b1/b2 ordering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove CastHullShape-specific GJK branching in CollisionCallback::collide()
and use NesterovAcceleration uniformly. This eliminates the dynamic_cast
detection, the separate CachedGuess path, and the unbounded distance_upper_bound
override — all collision pairs now use BoundingVolumeGuess with DualityGap
convergence. NesterovAcceleration produces contact points matching Bullet on
tessellated convex hulls.

Add coal_vs_bullet_convex_cast_unit test to validate Coal cast results against
the Bullet backend.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract removeObjects and collectCastTransformUpdate helpers to align
discrete and cast manager structure. Replace collision_objects_raw_
vector with appendCollisionObjectsRaw() to eliminate redundant parallel
storage. Use invalidateCacheFor() helper instead of inline cache
iteration. Move removeObjects from public to private in cast manager.
Remove unused CollisionObjectConstPtr and Link2ConstCOW type aliases and
sameObject() function. Fix broadphase manager doxygen comments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ects

Move function bodies from coal_utils.h to coal_utils.cpp to reduce header
weight and compilation coupling. Refactor removeObjects in both managers to
use direct filter-group dispatch instead of constructing unordered_sets from
broadphase contents. In the cast manager, replace the (filter_group, is_cast)
parameters with a BroadPhaseCollisionManager reference, guarding at the call
site. Pass pre-computed inverse transforms into populateContinuousCollisionFields
to avoid redundant computation across contacts. Fix "CompundMesh" typo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Unify NesterovAcceleration + DualityGap/Relative for both cast and discrete
pairs (previously discrete used Absolute). Enable GJK warm-start caching for
all pairs, not just cast. Add cache invalidation in enable/disableCollisionObject
to prevent stale guesses after object state changes. Extract shared
setCollisionObjectEnabled helper in both managers.

Also adds coal_cast_gradient_quality_unit test and uses dynamic_cast<CastHullShape>
instead of GEOM_CUSTOM check for future-proofing against other custom shapes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…CastHullShape

CastHullShape now explicitly overrides getNodeType() to return GEOM_CUSTOM
(previously relied on a ShapeBase default that has been removed) and delegates
needNesterovNormalizeHeuristic() to the underlying shape via Coal's
getNormalizeSupportDirection, using shape_traits as the single source of truth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace full cache erasure with lazy GJK re-seeding for enable/disable
and transform updates. Cache entries now use a CollisionCacheEntry struct
with a gjk_guess_valid flag that is cleared on invalidation and re-seeded
in collide() where actual transforms are available. Full erasure
(invalidateCacheFor) is reserved for object removal.

This preserves the ComputeCollision functor and solver settings across
transform updates, avoiding repeated dynamic_cast and functor
construction. Multi-link batch updates accumulate affected pointers into
a CollisionObjectPtrSet and scan the cache once, replacing the previous
collision_cache.clear().

Other changes:
- CastHullShape::computeLocalAABB uses computeBV<AABB, ShapeBase>
- CastHullShape cached members (shapeAABB_, shapeCenter_,
  shapeHalfExtents_) removed in favor of Coal utility
- invalidateCacheFor deduplicated via CollisionObjectPtrSet overload
- Transform3s::inverse() now callable on const (Coal fork change)
- CLAUDE.md updated to reflect lazy re-seeding architecture

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…s Coal cast collision checking is now fully functional.
The old approach scanned the entire collision cache to mark entries
invalid on every transform update or enable/disable — O(N*M) per batch.
Replace with a per-COW gjk_generation_ counter: collide() compares
cached generation stamps and re-seeds only on mismatch. This eliminates
all invalidateCachedGJKGuessFor() functions and the CollisionObjectPtrSet
type.

Also makes the GJK guess threshold configurable via plugin YAML
(gjk_guess_threshold key, default 5mm) instead of hardcoded, and
adds a unit test for threshold configuration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Static octrees no longer eagerly expand into per-voxel CastHullShape
boxes in makeCastCollisionObject. Instead the raw OcTree is kept in
link2castcow_ and expanded only when promoted to active (kinematic).
Once expanded the cast COW is cached so re-promotion is free.

Also optimizes the expansion loop: pre-reserves vectors using
tree->size() and uses the leaf iterator API directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add configurable arc-sagitta swept-sphere inflation for rotational
motions in continuous collision detection. When enabled via the
d_arc_compensation YAML config key, CastHullShape swept sphere radii
are inflated to account for the sagitta of rotational arcs, preventing
missed collisions on fast-rotating links.

Hoist ContactResultMap lookup out of the per-contact loop in
CollisionCallback::collide — the find only needs to run once per
collision pair rather than once per contact.

Update CLAUDE.md with expanded architecture documentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adopt the Bullet pattern: store a persistent ContactTestDataWrapper
member on both Coal managers instead of constructing a fresh one per
contactTest() call. This avoids copying CollisionMarginData (which
contains an unordered_map of per-pair margins) on every call — measured
at 1.8% CPU reduction in hvr_planning_se perf profiling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CastHullShape::computeShapeSupport called getSupport(shape, dir, hint)
which created a fresh ShapeSupportData every call, forcing
getShapeSupportLog to reallocate its visited vector on every support
query. Store two persistent ShapeSupportData members (one per pose) and
pass them to the new getSupport overload. The visited vector is now
allocated once and reused via std::fill on subsequent calls.

Measured: getShapeSupportLog self-time dropped from 2.65% to 2.04%.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When half or more of the dynamic collision objects changed, use
Coal's no-arg update() (O(n) bottom-up refit) instead of per-object
remove+reinsert (O(k*log n)). In trajectory optimization nearly all
kinematic objects move each step, making the refit path faster.

Measured: insertLeaf+removeLeaf dropped from 1.50% to 0.75% CPU.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ates

Remove redundant per-COW setCollisionObjectsTransform and setContactDistanceThreshold calls in CoalCastBVHManager::clone(). COW::clone() already copies transforms and thresholds, and setCollisionMarginData called afterwards re-applies them.

Add full-refit threshold to static_manager_ in flushBatchUpdate for both discrete and cast managers, matching the existing dynamic_manager_ logic. When most static objects changed (e.g. margin update), a full refit is O(n) vs O(k*log n) for per-object remove+reinsert.
Preserve clean rotation matrices by avoiding pose * local multiplication when the local rotation is identity so axis alignment stays exact.
Bulk-add collision objects via registerObjects() in clone() to build balanced broadphase trees instead of inserting one-by-one. Extract updateBroadphaseAndCache() to consolidate repeated update+reserve logic. Skip redundant margin threshold updates when value unchanged.
@rjoomen
rjoomen merged commit 7e58e8b into tesseract-robotics:main Apr 8, 2026
12 checks passed
@rjoomen
rjoomen deleted the claude/update-review-md-8p8Ek branch April 8, 2026 20:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants