Tutorial 5: Kinematic Trees

In this tutorial you will learn to work with coordsys_cutoff — a special node that lets you freeze, snapshot, or re-attach parts of a coordinate system hierarchy at runtime.

The Setup

A coordinate system tree can grow arbitrarily deep. Sometimes you need to temporarily isolate a subtree from the rest of the hierarchy, for example to capture a snapshot while the rest of the tree updates, or to re-root a kinematic chain onto a different parent. coordsys_cutoff provides exactly this capability.

The Code

#include <print>
import ninbot;

int main()
{
   using namespace nin;
   using namespace nin::R3;

   position_coordsys_child CS_A {WCS, { {2_m, 0_m, 0_m}, {} }};

   coordsys_cutoff<position_coordsys_traits> cutoff {CS_A};

   position_coordsys_child CS_D {cutoff, { {0_m, 0_m, 1_m}, {} }};

   point P = {CS_D, {1_m, 0_m, 0_m}};

   std::println("P in WCS (cutoff unlocked): {}", P.map_to(WCS));

   cutoff.lock();
   std::println("P in WCS (cutoff locked):   {}", P.map_to(WCS));
   cutoff.unlock();

   position_coordsys_child CS_E {WCS, { {10_m, 0_m, 0_m}, {} }};
   cutoff.set_parent(CS_E);
   std::println("P in WCS (cutoff at CS_E):  {}", P.map_to(WCS));

   return 0;
}

Output

P in WCS (cutoff unlocked): (3, 0, 1) (m)
P in WCS (cutoff locked):   (1, 0, 1) (m)
P in WCS (cutoff at CS_E):  (11, 0, 1) (m)

Building the code

Save the example as kinematic-trees.c++ alongside this CMakeLists.txt:

cmake_minimum_required(VERSION 4.0)
project(kinematic-trees LANGUAGES CXX)

find_package(Ninbot REQUIRED)

add_executable(kinematic-trees kinematic-trees.c++)
target_link_libraries(kinematic-trees PRIVATE ninbot::ninbot)

Then configure and build:

cmake -B build -G Ninja -DCMAKE_PREFIX_PATH=/path/to/ninbot/build
cmake --build build
./build/kinematic-trees

Walkthrough

Creating a cutoff node

position_coordsys_child CS_A {WCS, { {2_m, 0_m, 0_m}, {} }};

coordsys_cutoff<position_coordsys_traits> cutoff {CS_A};

coordsys_cutoff is a special node that introduces a re-attachable root in the coordinate tree. Here it is inserted as a child of CS_A. When unlocked (the default), it propagates transformations to its actual parent as usual.

Building a subtree below the cutoff

position_coordsys_child CS_D {cutoff, { {0_m, 0_m, 1_m}, {} }};

point P = {CS_D, {1_m, 0_m, 0_m}};

CS_D is a child of the cutoff node. P is a point in CS_D. While unlocked, the full transformation chain CS_D → cutoff → CS_A → WCS is used:

std::println("P in WCS (cutoff unlocked): {}", P.map_to(WCS));

Locking the cutoff

cutoff.lock();
std::println("P in WCS (cutoff locked):   {}", P.map_to(WCS));

Locking the cutoff freezes everything above it in the tree. The cutoff now acts as a local root — CS_D and all its children are expressed only relative to the cutoff position, ignoring CS_A and everything above it.

cutoff.unlock();

Unlocking restores the full chain.

Re-attaching a cutoff node

position_coordsys_child CS_E {WCS, { {10_m, 0_m, 0_m}, {} }};
cutoff.set_parent(CS_E);
std::println("P in WCS (cutoff at CS_E):  {}", P.map_to(WCS));

set_parent() changes the cutoff’s parent from CS_A to CS_E. This effectively moves the entire subtree rooted at the cutoff to a different location in the tree.