Buoyancy
The Gazebo Buoyancy plugin sums every collision in a model, which would make displacement depend on which accessories are fitted. Instead, the assembly owns displacement and parts own contact geometry.
<enable> is evaluated per link, not per model: a link that is not enabled is
never given Volume or CenterOfVolume components and contributes no
displacement. So the assembly carries a dedicated buoyancy link, and the world
enables buoyancy on that link alone:
<enable>blueboat::buoyancy_link</enable>
No part needs to know whether the vehicle floats, and nothing has to be subtracted to compensate for an accessory.
Displacement geometry is generated from the all-up-round (AUR) vehicle configuration rather than summed from components — modelling the overall buoyancy of the assembled vehicle is the normal approach for buoyant bodies:
USV pontoons — several boxes per side rather than one long box, so under graded buoyancy each short segment responds to its own local depth and pitch restores properly.
UUV hull — a single box whose height is solved so that
density × volume = mass × (1 + margin), giving near-neutral buoyancy.
Shape matters here. Graded buoyancy accepts <box> and <sphere> only;
anything else displaces nothing and warns once per process, which is easy to
miss. Uniform buoyancy also accepts cylinder and mesh. That restriction applies
to the buoyancy link, not to parts, so it never reaches the modeller.
One consequence to design around: displacement must live entirely on the enabled link. A genuinely buoyant part — a float, a syntactic block — cannot express that through its own collision.