How to implement a custom backend

You have a source of pose data — a sensor driver, a physics engine, an interpolation formula, a calibration table — and you want to expose it as a first-class coordinate system that participates in the kinematic tree alongside the built-in ones.

This guide is the short recipe. It assumes you know what a coordinate system is and have already used the built-in coordsys_child. If you are still learning the backend interface from the ground up, follow Tutorial 202 first — it walks through one full backend end to end. For the formal contract of the classes mentioned below, jump to coordsys_backend, coordsys_generic or coordsys_child.

The recipe

A custom coordinate system always comes in two layers. Write the backend, then choose a wrapper.

  1. Derive a backend from coordsys_backend<CT> and implement two virtuals:

    • offset(unsigned hop_limit) const override — return the transformation toward WCS;

    • clone() const override — return a shared_ptr to an independent copy.

  2. Choose a wrapper that exposes the backend as a coordsys<CT>:

    • coordsys_generic<BE> — when the backend is self-contained (no kinematic-tree parent, or it manages its own traversal);

    • coordsys_child<BE> — when the backend follows the parent-relative pattern and you derived it from coordsys_child_backend<CT> instead. The wrapper then inherits ready-made (parent, tf_data_to_parent) and (WCS, tf_data_to_parent) constructors for free.

The wrapper inherits from coordsys<CT>, so the resulting type is accepted anywhere a coordinate system is.

Step 1 — Write the backend

template <nin::coordsys_traits CT>
class my_backend : public nin::coordsys_backend<CT>
{
    nin::coordsys<CT> parent_cs;          // optional, stored as linked_copy()
    // ... whatever else the backend needs (sensor handle, calibration, …)

public:
    explicit my_backend(nin::coordsys<CT> const& parent)
     : parent_cs { parent.linked_copy() }
    { }

    CT::tf_data_type offset(unsigned hop_limit) const override
    {
        // Local transformation from *this* coordinate system to parent_cs.
        CT::tf_data_type local = /* … */;

        // Compose with the parent's transformation toward WCS. Forward
        // hop_limit unchanged: coordsys::tf_to_WCS() decrements and checks it.
        return CT::compose_tf(parent_cs.tf_to_WCS(hop_limit), local);
    }

    std::shared_ptr<nin::coordsys_backend<CT>> clone() const override
    {
        return std::make_shared<my_backend>(parent_cs);
    }
};

Three rules survive every backend you will ever write:

Store referenced coordinate systems as linked_copy()

A linked copy shares the backend pointer, so when the user moves the parent, your backend sees the new pose without anyone having to refresh the link.

Never throw hop_limit_exceeded_error yourself

The public coordsys::tf_to_WCS() already decrements hop_limit before every call into offset() and throws when the counter reaches zero. Your offset() only has to forward hop_limit to whatever further tf_to_WCS() calls it makes.

Make clone() a deep copy

Two coordinate systems that compare unequal must not share mutable state. The only way to legitimately share a backend is through linked_copy().

Step 2 — Pair it with a coordsys

If the backend needs no extra members on the coordinate system, a using alias is all you need:

template <nin::coordsys_traits CT>
using my_coordsys = nin::coordsys_generic<my_backend<CT>>;

If the coordinate system itself needs more API — for example lock()/unlock() to satisfy the Lockable named requirement — wrap it in a dedicated class:

template <nin::coordsys_traits CT>
class my_coordsys : public nin::coordsys_generic<my_backend<CT>>
{
    using Base = nin::coordsys_generic<my_backend<CT>>;
public:
    using Base::Base;   // inherits the perfect-forwarding constructor

    // domain-specific helpers, forwarding through operator-> if needed:
    void recalibrate() { this->operator->()->recalibrate(); }
};

For the parent-relative pattern, swap coordsys_generic for coordsys_child and derive the backend from coordsys_child_backend<CT> instead. The construction syntax your users see (my_coordsys cs {parent, tf}) then matches every other child coordinate system in the library.

Worked examples to copy from

  • Tutorial 202 — coordsys_cutoff, a backend that short-circuits tree traversal when locked, wrapped in a coordsys_generic that adds lock()/unlock().

  • In-tree: coordsys_mujoco_backend in src/pos/mujoco.c++ — reads pose from a MuJoCo simulation, used through coordsys_generic.