diff --git a/data/armarx_navigation/controller_config/PlatformTrajectory/default.json b/data/armarx_navigation/controller_config/PlatformTrajectory/default.json
index 4cd38813e9ad112a708650aeb00912e426037425..e69bb8299d15eab5f3de0eb5219308b88f9cd51c 100644
--- a/data/armarx_navigation/controller_config/PlatformTrajectory/default.json
+++ b/data/armarx_navigation/controller_config/PlatformTrajectory/default.json
@@ -1,18 +1,18 @@
 {
   "params": {
     "pidPos": {
-      "Kp": 1,
+      "Kp": 0.1,
       "Ki": 0,
       "Kd": 0
     },
     "pidOri": {
-      "Kp": 1,
+      "Kp": 0.1,
       "Ki": 0,
       "Kd": 0
     },
     "limits": {
-      "linear": 300,
-      "angular": 0.2
+      "linear": 500,
+      "angular": 0.5
     }
   },
   "targets": {
diff --git a/source/armarx/navigation/core/CMakeLists.txt b/source/armarx/navigation/core/CMakeLists.txt
index 4ee7e461279c24d83a67e7739b2df3f17ae1a822..d95cd0ed7c210e76675b2a9f286ccdd0854f6401 100644
--- a/source/armarx/navigation/core/CMakeLists.txt
+++ b/source/armarx/navigation/core/CMakeLists.txt
@@ -13,6 +13,7 @@ armarx_add_library(core
     SOURCES
         StaticScene.cpp
         Trajectory.cpp
+        basic_types.cpp
         types.cpp
         Graph.cpp
         aron_conversions.cpp
diff --git a/source/armarx/navigation/core/Trajectory.cpp b/source/armarx/navigation/core/Trajectory.cpp
index ce659b7c5995d447265d4c7cf081a8d563cdedca..fc5c9b0ba2d7d2efc9516e1979ee04c62115517a 100644
--- a/source/armarx/navigation/core/Trajectory.cpp
+++ b/source/armarx/navigation/core/Trajectory.cpp
@@ -6,18 +6,6 @@
 #include <iterator>
 #include <limits>
 
-#include <range/v3/action/insert.hpp>
-#include <range/v3/numeric/accumulate.hpp>
-#include <range/v3/range/conversion.hpp>
-#include <range/v3/view/all.hpp>
-#include <range/v3/view/concat.hpp>
-#include <range/v3/view/for_each.hpp>
-#include <range/v3/view/group_by.hpp>
-#include <range/v3/view/reverse.hpp>
-#include <range/v3/view/sliding.hpp>
-#include <range/v3/view/transform.hpp>
-#include <range/v3/view/zip.hpp>
-
 #include <Eigen/Core>
 #include <Eigen/Geometry>
 
@@ -34,6 +22,17 @@
 #include <armarx/navigation/conversions/eigen.h>
 #include <armarx/navigation/core/basic_types.h>
 #include <armarx/navigation/core/types.h>
+#include <range/v3/action/insert.hpp>
+#include <range/v3/numeric/accumulate.hpp>
+#include <range/v3/range/conversion.hpp>
+#include <range/v3/view/all.hpp>
+#include <range/v3/view/concat.hpp>
+#include <range/v3/view/for_each.hpp>
+#include <range/v3/view/group_by.hpp>
+#include <range/v3/view/reverse.hpp>
+#include <range/v3/view/sliding.hpp>
+#include <range/v3/view/transform.hpp>
+#include <range/v3/view/zip.hpp>
 
 // FIXME move to simox
 
@@ -158,13 +157,14 @@ namespace armarx::navigation::core
 
     } // namespace conv
 
-    Projection
-    GlobalTrajectory::getProjection(const Position& point,
-                              const VelocityInterpolation& velocityInterpolation) const
+
+    GlobalTrajectory::InternalProjection
+    GlobalTrajectory::projectInternal(const Position& point,
+                                      const VelocityInterpolation& velocityInterpolation) const
     {
         float distance = std::numeric_limits<float>::max();
 
-        Projection bestProj;
+        InternalProjection bestProj;
 
         for (size_t i = 0; i < pts.size() - 1; i++)
         {
@@ -198,26 +198,9 @@ namespace armarx::navigation::core
                 math::LinearInterpolatedPose ip(
                     wpBefore.waypoint.pose.matrix(), wpAfter.waypoint.pose.matrix(), 0, 1, true);
 
-                bestProj.wayPointBefore = wpBefore;
-                bestProj.wayPointAfter = wpAfter;
+                bestProj.indexBefore = i;
                 bestProj.projection.waypoint.pose = ip.Get(t);
 
-                bestProj.segment = [&]
-                {
-                    if (i == 0)
-                    {
-                        return Projection::Segment::FIRST;
-                    }
-
-                    if (i == (pts.size() - 2))
-                    {
-                        return Projection::Segment::FINAL;
-                    }
-
-                    return Projection::Segment::INTERMEDIATE;
-                }();
-
-
                 switch (velocityInterpolation)
                 {
                     case VelocityInterpolation::LinearInterpolation:
@@ -236,6 +219,82 @@ namespace armarx::navigation::core
         return bestProj;
     }
 
+    Projection
+    GlobalTrajectory::getProjection(const Position& point,
+                                    const VelocityInterpolation& velocityInterpolation) const
+    {
+        InternalProjection intProj = projectInternal(point, velocityInterpolation);
+
+        const auto& wpBefore = pts.at(intProj.indexBefore);
+        const auto& wpAfter = pts.at(intProj.indexBefore + 1);
+
+        Projection bestProj;
+
+        bestProj.wayPointBefore = wpBefore;
+        bestProj.wayPointAfter = wpAfter;
+        bestProj.projection = intProj.projection;
+
+        bestProj.segment = [&]
+        {
+            if (intProj.indexBefore == 0)
+            {
+                return Projection::Segment::FIRST;
+            }
+
+            if (intProj.indexBefore == (pts.size() - 2))
+            {
+                return Projection::Segment::FINAL;
+            }
+
+            return Projection::Segment::INTERMEDIATE;
+        }();
+
+        return bestProj;
+    }
+
+    std::pair<GlobalTrajectory, bool>
+    GlobalTrajectory::getSubTrajectory(const Position& point, float distance) const
+    {
+        const InternalProjection intProj =
+            projectInternal(point, VelocityInterpolation::LinearInterpolation);
+
+        GlobalTrajectory subTraj{};
+        subTraj.mutablePoints().push_back(intProj.projection);
+
+        float remainingDistance = distance;
+        GlobalTrajectoryPoint lastWp = intProj.projection;
+        for (size_t i = intProj.indexBefore + 1; i < pts.size(); i++)
+        {
+            const auto& nextWp = pts.at(i);
+
+            const float segmentDistance =
+                (nextWp.waypoint.pose.translation() - lastWp.waypoint.pose.translation()).norm();
+
+            if (segmentDistance < remainingDistance)
+            {
+                subTraj.mutablePoints().push_back(nextWp);
+                lastWp = nextWp;
+                remainingDistance -= segmentDistance;
+            }
+            else
+            {
+                // fraction of distance between segment end points
+                const float t = remainingDistance / segmentDistance;
+
+                math::LinearInterpolatedPose ip(
+                    lastWp.waypoint.pose.matrix(), nextWp.waypoint.pose.matrix(), 0, 1, true);
+                const float velocity = lastWp.velocity + (nextWp.velocity - lastWp.velocity) * t;
+
+                subTraj.mutablePoints().push_back({{static_cast<Pose>(ip.Get(t))}, velocity});
+
+                return {subTraj, false};
+            }
+        }
+
+        // remaining trajectory is shorter than the specified distance
+        return {subTraj, true};
+    }
+
     std::vector<Eigen::Vector3f>
     GlobalTrajectory::positions() const noexcept
     {
@@ -273,9 +332,9 @@ namespace armarx::navigation::core
 
     GlobalTrajectory
     GlobalTrajectory::FromPath(const Pose& start,
-                         const Positions& waypoints,
-                         const Pose& goal,
-                         const float velocity)
+                               const Positions& waypoints,
+                               const Pose& goal,
+                               const float velocity)
     {
         // currently, only 2D version
 
@@ -331,7 +390,8 @@ namespace armarx::navigation::core
     {
         core::Positions resampledPath;
 
-        const auto toPoint = [](const GlobalTrajectoryPoint& wp) -> Pose { return wp.waypoint.pose; };
+        const auto toPoint = [](const GlobalTrajectoryPoint& wp) -> Pose
+        { return wp.waypoint.pose; };
 
         const core::Path originalPath = pts | ranges::views::transform(toPoint) | ranges::to_vector;
 
@@ -601,7 +661,7 @@ namespace armarx::navigation::core
 
             constexpr int nSamples = 100;
 
-            for(int j = 0; j < nSamples; j++)
+            for (int j = 0; j < nSamples; j++)
             {
                 const float progress = static_cast<float>(j) / nSamples;
 
@@ -675,7 +735,6 @@ namespace armarx::navigation::core
         return indices;
     }
 
-
     const std::vector<LocalTrajectoryPoint>&
     LocalTrajectory::points() const
     {
@@ -689,7 +748,7 @@ namespace armarx::navigation::core
     }
 
     Evaluation
-    LocalTrajectory::evaluate(armarx::core::time::DateTime timestamp) const
+    LocalTrajectory::evaluate(const armarx::core::time::DateTime& timestamp) const
     {
         const auto cmp = [](const core::LocalTrajectoryPoint& lhs,
                             const DateTime& timestamp) -> bool
@@ -700,20 +759,43 @@ namespace armarx::navigation::core
         if (lower == pts.end() - 1)
         {
             // if we arrived at the last point, the velocity is 0
-            return {lower->pose, 0};
+            return {lower->pose, core::Twist::Zero()};
         }
 
         const Duration d1 = timestamp - lower->timestamp;
         const Duration dT = lower[1].timestamp - lower->timestamp;
 
+        // the waypoint before this timestamp
+        const auto& global_T_wp_before = lower->pose;
+
+        // the waypoint after this timestamp
+        const auto& global_T_wp_after = lower[1].pose;
+
         // fraction of time between segment end points
         const float t = d1 / dT;
-        math::LinearInterpolatedPose ip(lower->pose.matrix(), lower[1].pose.matrix(), 0, 1, true);
+        math::LinearInterpolatedPose ip(
+            global_T_wp_before.matrix(), global_T_wp_after.matrix(), 0, 1, true);
+
+        const core::Pose wp_before_T_wp_after = global_T_wp_before.inverse() * global_T_wp_after;
 
+        const Eigen::Matrix3f& global_R_wp_before = global_T_wp_before.linear();
+
+        // the movement direction in the global frame
+        const Eigen::Vector3f global_V_linear_movement_direction =
+            global_R_wp_before * wp_before_T_wp_after.translation().normalized();
         const Eigen::Vector3f distance = lower[1].pose.translation() - lower->pose.translation();
-        const float speed = distance.norm() / dT.toSecondsDouble();
+        const float linearVelocity = static_cast<float>(distance.norm()) / dT.toSecondsDouble();
+
+
+        // angular velocity
+
+        Eigen::AngleAxisf angleAx(wp_before_T_wp_after.linear());
+
+        const core::Twist feedforwardTwist{
+            .linear = linearVelocity * global_V_linear_movement_direction,
+            .angular = angleAx.axis() * angleAx.angle() / dT.toSecondsDouble()};
 
-        return {static_cast<core::Pose>(ip.Get(t)), speed};
+        return {static_cast<core::Pose>(ip.Get(t)), feedforwardTwist};
     }
 
 
diff --git a/source/armarx/navigation/core/Trajectory.h b/source/armarx/navigation/core/Trajectory.h
index 19745b6d0eb83d76248436053e2884cbd29bc973..20da0ba1f577865ab3e7938a1dddb4f3e565275a 100644
--- a/source/armarx/navigation/core/Trajectory.h
+++ b/source/armarx/navigation/core/Trajectory.h
@@ -24,6 +24,7 @@
 
 #include <cstddef>
 #include <memory>
+
 #include "ArmarXCore/core/time/DateTime.h"
 
 #include <armarx/navigation/core/basic_types.h>
@@ -76,6 +77,15 @@ namespace armarx::navigation::core
         Projection getProjection(const Position& point,
                                  const VelocityInterpolation& velocityInterpolation) const;
 
+        /**
+         * @brief Project point onto the trajectory and return a new trajectory along the old one
+         *        from that point for the specified distance.
+         * @return The subtrajectory and whether the subtrajectory ends at the same point,
+         *         as this trajectory
+         */
+        std::pair<GlobalTrajectory, bool> getSubTrajectory(const Position& point,
+                                                           const float distance) const;
+
         [[nodiscard]] std::vector<Position> positions() const noexcept;
 
         [[nodiscard]] std::vector<Pose> poses() const noexcept;
@@ -117,6 +127,16 @@ namespace armarx::navigation::core
          */
         Indices allConnectedPointsInRange(std::size_t idx, float radius) const;
 
+    private:
+        struct InternalProjection
+        {
+            GlobalTrajectoryPoint projection;
+            size_t indexBefore;
+        };
+        InternalProjection
+        projectInternal(const Position& point,
+                        const VelocityInterpolation& velocityInterpolation) const;
+
 
     private:
         std::vector<GlobalTrajectoryPoint> pts;
@@ -134,7 +154,7 @@ namespace armarx::navigation::core
     struct Evaluation
     {
         core::Pose pose;
-        float velocity; // [mm/s]
+        core::Twist feedforwardTwist; // [mm/s]
     };
 
 
@@ -151,7 +171,7 @@ namespace armarx::navigation::core
 
         std::vector<LocalTrajectoryPoint>& mutablePoints();
 
-        Evaluation evaluate(DateTime timestamp) const;
+        Evaluation evaluate(const DateTime& timestamp) const;
 
 
     private:
diff --git a/source/armarx/navigation/core/basic_types.cpp b/source/armarx/navigation/core/basic_types.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..460c560bc8905f63a3b3eb168193c6faf5d11bef
--- /dev/null
+++ b/source/armarx/navigation/core/basic_types.cpp
@@ -0,0 +1,18 @@
+#include "basic_types.h"
+
+namespace armarx::navigation::core
+{
+    core::Pose
+    Twist::poseDiff(const float dt) const
+    {
+        return core::Pose(Eigen::Translation3f(linear * dt)) *
+               core::Pose(Eigen::AngleAxisf(angular.norm() * dt, angular.normalized()));
+    }
+
+    Twist
+    Twist::Zero()
+    {
+        static const core::Twist zero{Eigen::Vector3f::Zero(), Eigen::Vector3f::Zero()};
+        return zero;
+    }
+} // namespace armarx::navigation::core
diff --git a/source/armarx/navigation/core/basic_types.h b/source/armarx/navigation/core/basic_types.h
index 0917270df14fcf9568f56f05ddb4a7492cc9e70e..6121a739f2359abe352a61ff82d765cf21c7c306 100644
--- a/source/armarx/navigation/core/basic_types.h
+++ b/source/armarx/navigation/core/basic_types.h
@@ -50,4 +50,14 @@ namespace armarx::navigation::core
         Pose pose;
     };
 
+    struct Twist
+    {
+        LinearVelocity linear;
+        AngularVelocity angular;
+
+        Pose poseDiff(float dt) const;
+
+        static Twist Zero();
+    };
+
 } // namespace armarx::navigation::core
diff --git a/source/armarx/navigation/core/types.cpp b/source/armarx/navigation/core/types.cpp
index 346cd4de2e7bc71ef3b0ec7ac7acc3f22a6e995c..7a25d9285728277b8121613da84fe939a7d3d4f1 100644
--- a/source/armarx/navigation/core/types.cpp
+++ b/source/armarx/navigation/core/types.cpp
@@ -2,17 +2,17 @@
 
 namespace armarx::navigation::core
 {
-    core::Pose
-    Twist::poseDiff(const float dt) const
-    {
-        return core::Pose(Eigen::Translation3f(linear * dt)) *
-               core::Pose(Eigen::AngleAxisf(angular.norm() * dt, angular.normalized()));
-    }
+    // core::Pose
+    // Twist::poseDiff(const float dt) const
+    // {
+    //     return core::Pose(Eigen::Translation3f(linear * dt)) *
+    //            core::Pose(Eigen::AngleAxisf(angular.norm() * dt, angular.normalized()));
+    // }
 
-    Twist
-    Twist::Zero()
-    {
-        static const core::Twist zero{Eigen::Vector3f::Zero(), Eigen::Vector3f::Zero()};
-        return zero;
-    }
+    // Twist
+    // Twist::Zero()
+    // {
+    //     static const core::Twist zero{Eigen::Vector3f::Zero(), Eigen::Vector3f::Zero()};
+    //     return zero;
+    // }
 } // namespace armarx::navigation::core
diff --git a/source/armarx/navigation/core/types.h b/source/armarx/navigation/core/types.h
index 9ce943acd75f34d153eda8c06974f6b5bfcc64d5..ade2630f76f818af1928cf77365246c79b923560 100644
--- a/source/armarx/navigation/core/types.h
+++ b/source/armarx/navigation/core/types.h
@@ -58,15 +58,7 @@ namespace armarx::navigation::core
     };
 
 
-    struct Twist
-    {
-        LinearVelocity linear;
-        AngularVelocity angular;
-
-        Pose poseDiff(float dt) const;
-
-        static Twist Zero();
-    };
+    
 
 
     class TimeServerInterface;
diff --git a/source/armarx/navigation/local_planning/TimedElasticBands.cpp b/source/armarx/navigation/local_planning/TimedElasticBands.cpp
index 0d61b9ed53fb3b164a1d8de17f8ad5b5fd384914..35881b6672026a908a9c3e6c6564b81da181f40a 100644
--- a/source/armarx/navigation/local_planning/TimedElasticBands.cpp
+++ b/source/armarx/navigation/local_planning/TimedElasticBands.cpp
@@ -101,16 +101,20 @@ namespace armarx::navigation::local_planning
     std::optional<LocalPlannerResult>
     TimedElasticBands::plan(const core::GlobalTrajectory& goal)
     {
-        const teb_local_planner::TimedElasticBand globalPath = conv::toRos(goal);
+        const core::Pose currentPose{scene.robot->getGlobalPose()};
+
+        // TODO (SALt): put distance parameter into config (note: distance is in mm)
+        // prune global trajectory
+        const auto& [prunedGoal, planToDest] =
+            goal.getSubTrajectory(currentPose.translation(), 2000);
+
+        const teb_local_planner::TimedElasticBand globalPath = conv::toRos(prunedGoal);
         teb_globalPath = globalPath.poses();
         hcp_->setGlobalPath(&teb_globalPath);
 
-        const core::Pose currentPose{scene.robot->getGlobalPose()};
-
         const teb_local_planner::PoseSE2 start = conv::toRos(currentPose);
-
-        const FindTargetResult target = findTarget(currentPose, goal);
-        const teb_local_planner::PoseSE2 end = conv::toRos(target.target);
+        const teb_local_planner::PoseSE2 end =
+            conv::toRos(prunedGoal.points().back().waypoint.pose);
 
         geometry_msgs::Twist velocity_start = conv::toRos(scene.platformVelocity);
 
@@ -119,7 +123,8 @@ namespace armarx::navigation::local_planning
 
         try
         {
-            hcp_->plan(start, end, &velocity_start, !target.planToDestination);
+            // TODO (SALt): free goal velocity is not respected inside homotopy planner
+            hcp_->plan(start, end, &velocity_start, !planToDest);
         }
         catch (std::exception& e)
         {
@@ -135,20 +140,6 @@ namespace armarx::navigation::local_planning
     }
 
 
-    TimedElasticBands::FindTargetResult
-    TimedElasticBands::findTarget(const core::Pose& currentPose,
-                                  const core::GlobalTrajectory& globalPath)
-    {
-        // TODO (SALt): implement
-
-        const core::Projection projection = globalPath.getProjection(
-            currentPose.translation(), core::VelocityInterpolation::LinearInterpolation);
-
-        return {conv::to2D(projection.projection.waypoint.pose),
-                conv::to2D(globalPath.points().back().waypoint.pose),
-                true};
-    }
-
     void
     TimedElasticBands::fillObstacles()
     {
diff --git a/source/armarx/navigation/local_planning/TimedElasticBands.h b/source/armarx/navigation/local_planning/TimedElasticBands.h
index caf2c832a94559ef91ae886d3b25a0c0e055fa36..6bded06728b9b26664c57065b20b641884df8693 100644
--- a/source/armarx/navigation/local_planning/TimedElasticBands.h
+++ b/source/armarx/navigation/local_planning/TimedElasticBands.h
@@ -59,16 +59,7 @@ namespace armarx::navigation::local_planning
         std::optional<LocalPlannerResult> plan(const core::GlobalTrajectory& goal) override;
 
     private:
-        struct FindTargetResult
-        {
-            core::Pose2D projection;
-            core::Pose2D target;
-            bool planToDestination;
-        };
-
         void initDefaultConfig();
-        FindTargetResult findTarget(const core::Pose& currentPose,
-                                    const core::GlobalTrajectory& globalPath);
         void fillObstacles();
 
     protected:
diff --git a/source/armarx/navigation/trajectory_control/local/TrajectoryFollowingController.cpp b/source/armarx/navigation/trajectory_control/local/TrajectoryFollowingController.cpp
index e4e2cbc4192cb0972852c8a50f7ed9f86b856305..a16cb65692e8cc6e5934d84f82bc76ef24cf32f3 100644
--- a/source/armarx/navigation/trajectory_control/local/TrajectoryFollowingController.cpp
+++ b/source/armarx/navigation/trajectory_control/local/TrajectoryFollowingController.cpp
@@ -154,9 +154,18 @@ namespace armarx::navigation::traj_ctrl::local
         pidPos.update(mat4f_to_pos(currentPose.matrix()), mat4f_to_pos(target.pose.matrix()));
         pidOri.update(mat4f_to_rpy(currentPose.matrix()), mat4f_to_rpy(target.pose.matrix()));
 
-        const core::Twist twist{.linear = pidPos.getControlValue(),
+
+        const core::Twist& twistFeedForward = target.feedforwardTwist;
+
+        const core::Twist twistError{.linear = pidPos.getControlValue(),
                                 .angular = pidOri.getControlValue()};
 
+        const core::Twist twist
+        {
+            .linear = twistFeedForward.linear + twistError.linear,
+            .angular = twistFeedForward.angular + twistError.angular
+        };
+
         const auto twistLimited = applyTwistLimits(twist);
         ARMARX_VERBOSE << deactivateSpam(1) << "Twist limited " << twistLimited.linear.transpose();
         ARMARX_VERBOSE << deactivateSpam(1) << "Twist angular " << twistLimited.angular.transpose();