diff --git a/python/ur_simple_control/path_generation/starworlds b/python/ur_simple_control/path_generation/starworlds
deleted file mode 160000
index 2990b9ecdeb8d7dd421fc518b5a97b2a31d5e67e..0000000000000000000000000000000000000000
--- a/python/ur_simple_control/path_generation/starworlds
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 2990b9ecdeb8d7dd421fc518b5a97b2a31d5e67e
diff --git a/python/ur_simple_control/path_generation/starworlds/LICENSE b/python/ur_simple_control/path_generation/starworlds/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/python/ur_simple_control/path_generation/starworlds/README.md b/python/ur_simple_control/path_generation/starworlds/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/python/ur_simple_control/path_generation/starworlds/obstacles/__init__.py b/python/ur_simple_control/path_generation/starworlds/obstacles/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..52cb0934227437bf233de1e81164eac4f85fa2fe
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/obstacles/__init__.py
@@ -0,0 +1,6 @@
+from .obstacle import Obstacle, Frame
+from .starshaped_obstacle import StarshapedObstacle
+from .ellipse import Ellipse
+from .polygon import Polygon
+from .starshaped_polygon import StarshapedPolygon
+from .starshaped_primitive_combination import StarshapedPrimitiveCombination
\ No newline at end of file
diff --git a/python/ur_simple_control/path_generation/starworlds/obstacles/ellipse.py b/python/ur_simple_control/path_generation/starworlds/obstacles/ellipse.py
new file mode 100644
index 0000000000000000000000000000000000000000..17f216ea08bfd56748b3be73dbb0ffe8d9155481
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/obstacles/ellipse.py
@@ -0,0 +1,217 @@
+from obstacles import StarshapedObstacle, Frame
+import numpy as np
+import matplotlib.pyplot as plt
+import matplotlib.patches as patches
+from utils import is_ccw, tic, toc
+import shapely
+
+
+class Ellipse(StarshapedObstacle):
+    def __init__(self, a, xr=None, n_pol=20, **kwargs):
+        self._a = np.array(a, float)
+        self._a2 = np.square(self._a)
+        self._n_pol = n_pol
+        # self._area = np.pi * self._a[0] * self._a[1]
+        if xr is None:
+            xr = np.zeros(2)
+        super().__init__(xr=xr, **kwargs)
+        self.enclosing_ball_diameter = max(2*self._a[0], 2*self._a[1])
+
+    def copy(self, id, name):
+        if (id == 'duplicate' or id == 'd'):
+            id = self.id()
+        return Ellipse(id=id, name=name, a=self._a, xr=self._xr, n_pol=self._n_pol, motion_model=self._motion_model)
+
+    def dilated_obstacle(self, padding, id="new", name=None):
+        cp = self.copy(id, name)
+        cp._a += padding
+        cp._a2 = np.square(cp._a)
+        cp.enclosing_ball_diameter = max(2 * cp._a[0], 2 * cp._a[1])
+        if self._polygon is not None:
+            cp._polygon = self._polygon.buffer(padding, cap_style=1, join_style=1)
+            cp._polygon_global_pose = None
+            cp._polygon_global = None
+            cp._kernel = cp._polygon
+        return cp
+
+    def init_plot(self, ax=None, show_reference=True, show_name=False, **kwargs):
+        if ax is None:
+            _, ax = plt.subplots(subplot_kw={'aspect': 'equal'})
+        # Default facecolor
+        if "fc" not in kwargs and "facecolor" not in kwargs:
+            kwargs["fc"] = 'lightgrey'
+        line_handles = []
+        # Boundary
+        line_handles += [patches.Ellipse(xy=[0, 0], width=2*self._a[0], height=2*self._a[1], angle=0, **kwargs)]
+        ax.add_patch(line_handles[-1])
+        # Reference point
+        line_handles += ax.plot(*self._xr, '+', color='k') if show_reference else [None]
+        # Name
+        line_handles += [ax.text(*self._xr, self._name)] if show_name else [None]
+        return line_handles, ax
+
+    def update_plot(self, line_handles, frame=Frame.GLOBAL):
+        pos, rot = self.pos(frame), self.rot(frame)
+        line_handles[0].center = pos
+        line_handles[0].angle = np.rad2deg(rot)
+        if line_handles[1] is not None:
+            line_handles[1].set_data(*self.xr(frame))
+        if line_handles[2] is not None:
+            line_handles[2].set_position(self.xr(frame))
+
+    def draw(self, frame=Frame.GLOBAL, **kwargs):
+        line_handles, ax = self.init_plot(**kwargs)
+        self.update_plot(line_handles, frame)
+        return line_handles, ax
+
+    def boundary_mapping(self, x, input_frame=Frame.GLOBAL, output_frame=Frame.GLOBAL):
+        x_obstacle = self.transform(x, input_frame, Frame.OBSTACLE)
+
+        intersect_obstacle = self.line_intersection([self._xr, self._xr+1.1*self.enclosing_ball_diameter*self.reference_direction(x_obstacle, Frame.OBSTACLE, Frame.OBSTACLE)],
+                                                    input_frame=Frame.OBSTACLE, output_frame=Frame.OBSTACLE)
+        if not intersect_obstacle:
+            return None
+        if len(intersect_obstacle) == 1:
+            return self.transform(intersect_obstacle[0], Frame.OBSTACLE, output_frame)
+        else:
+            if np.linalg.norm(intersect_obstacle[0]-x_obstacle) < np.linalg.norm(intersect_obstacle[1]-x_obstacle):
+                return self.transform(intersect_obstacle[0], Frame.OBSTACLE, output_frame)
+            else:
+                return self.transform(intersect_obstacle[1], Frame.OBSTACLE, output_frame)
+
+    def normal(self, x, input_frame=Frame.GLOBAL, output_frame=Frame.GLOBAL, x_is_boundary=False):
+        b_obstacle = self.transform(x, input_frame, Frame.OBSTACLE) if x_is_boundary else self.boundary_mapping(x, input_frame, Frame.OBSTACLE)
+        if b_obstacle is None:
+            return None
+        n_obstacle = np.array([self._a2[1] * b_obstacle[0], self._a2[0] * b_obstacle[1]])
+        n_obstacle = n_obstacle / np.linalg.norm(n_obstacle)
+        return self.rotate(n_obstacle, Frame.OBSTACLE, output_frame)
+
+    def point_location(self, x, input_frame=Frame.GLOBAL):
+        x_obstacle = self.transform(x, input_frame, Frame.OBSTACLE)
+        loc = (x_obstacle[0] / self._a[0]) ** 2 + (x_obstacle[1] / self._a[1]) ** 2 - 1
+        return loc
+
+    def line_intersection(self, line, input_frame=Frame.GLOBAL, output_frame=Frame.GLOBAL):
+        # Transform line points to left/right points in obstacle ellipse coordinates
+        l0_obstacle = self.transform(line[0], input_frame, Frame.OBSTACLE)
+        l1_obstacle = self.transform(line[1], input_frame, Frame.OBSTACLE)
+        l_left_obstacle = l0_obstacle if l0_obstacle[0] < l1_obstacle[0] else l1_obstacle
+        l_right_obstacle = l1_obstacle if l0_obstacle[0] < l1_obstacle[0] else l0_obstacle
+
+
+        vertical_line = abs(l_right_obstacle[0]-l_left_obstacle[0]) < 1e-4
+        if not vertical_line:
+            # Line parameters
+            m = (l_right_obstacle[1] - l_left_obstacle[1]) / (l_right_obstacle[0] - l_left_obstacle[0])
+            c = l_left_obstacle[1] - m * l_left_obstacle[0]
+            vertical_line = abs(m) > 100
+
+        # Special case with vertical line
+        if vertical_line:
+            if l_right_obstacle[0] < -self._a[0] or l_right_obstacle[0] > self._a[0]:
+                return []
+
+            l_top_obstacle = l_right_obstacle if l_right_obstacle[1] > l_left_obstacle[1] else l_left_obstacle
+            l_bottom_obstacle = l_left_obstacle if l_right_obstacle[1] > l_left_obstacle[1] else l_right_obstacle
+            x_intersect_top_obstacle, x_intersect_bottom_obstacle = np.array([0, self._a[1]]), np.array([0, -self._a[1]])
+            x_intersect_top = self.transform(x_intersect_top_obstacle, Frame.OBSTACLE, output_frame)
+            x_intersect_bottom = self.transform(x_intersect_bottom_obstacle, Frame.OBSTACLE, output_frame)
+            if l_top_obstacle[1] >= self._a[1] and l_bottom_obstacle[1] <= -self._a[1]:
+                return [x_intersect_top, x_intersect_bottom]
+            elif l_top_obstacle[1] >= self._a[1] and l_bottom_obstacle[1] <= self._a[1]:
+                return [x_intersect_top]
+            elif l_top_obstacle[1] >= -self._a[1] and l_bottom_obstacle[1] <= -self._a[1]:
+                return [x_intersect_bottom]
+            else:
+                return []
+
+        # obstacle ellipse coefficients at intersection with line m*x+c
+        kx2 = self._a2[0] * m**2 + self._a2[1]
+        kx = 2 * self._a2[0] * m * c
+        k1 = self._a2[0] * (c**2 - self._a2[1])
+
+        # TODO: Stable fix for finding intersection
+        discriminant = self._a2[0] * m**2 + self._a2[1] - c**2
+        if discriminant < 0:
+            return []
+        elif discriminant == 0:
+            tmp_x = -kx / (2 * kx2)
+            x_intersect_obstacle = np.array([tmp_x, m * tmp_x + c])
+            return [self.transform(x_intersect_obstacle, Frame.OBSTACLE, output_frame)]
+        else:
+            in_sqrt = kx ** 2 / (4 * kx2 ** 2) - k1 / kx2
+            if np.isclose(in_sqrt, 0):
+                in_sqrt = 0
+            tmp_x = -kx / (2 * kx2) - np.sqrt(in_sqrt)
+            x_intersect_left_obstacle = np.array([tmp_x, m * tmp_x + c])
+            tmp_x = -kx / (2 * kx2) + np.sqrt(in_sqrt)
+            x_intersect_right_obstacle = np.array([tmp_x, m * tmp_x + c])
+            x_intersect_left = self.transform(x_intersect_left_obstacle, Frame.OBSTACLE, output_frame)
+            x_intersect_right = self.transform(x_intersect_right_obstacle, Frame.OBSTACLE, output_frame)
+
+            if l_right_obstacle[0] < x_intersect_left_obstacle[0] or x_intersect_right_obstacle[0] < l_left_obstacle[0] or \
+                    x_intersect_left_obstacle[0] < l_left_obstacle[0] < l_right_obstacle[0] < x_intersect_right_obstacle[0]:
+                return []
+            elif x_intersect_left_obstacle[0] < l_left_obstacle[0]:
+                return [x_intersect_right]
+            elif l_right_obstacle[0] < x_intersect_right_obstacle[0]:
+                return [x_intersect_left]
+            else:
+                return [x_intersect_left, x_intersect_right]
+
+    def tangent_points(self, x, input_frame=Frame.GLOBAL, output_frame=Frame.GLOBAL):
+        x_obstacle = self.transform(x, input_frame, Frame.OBSTACLE)
+        if not self.exterior_point(x_obstacle, Frame.OBSTACLE):
+            return []
+        px, py = x_obstacle
+
+        # Special case with vertical tangent
+        a2 = self._a**2
+        vertical_tangent = abs(a2[0] - px**2) < 1e-5
+        if vertical_tangent:
+            m2 = (py**2-a2[1]) / (2*px*py)
+            x2 = (px * m2 ** 2 - py * m2) / (a2[1] / a2[0] + m2 ** 2)
+            y2 = m2 * (x2 - px) + py
+            tp1_obstacle = np.array([px, 0])
+            tp2_obstacle = np.array([x2, y2])
+        else:
+            tmp = px**2 - a2[0]
+            c1 = (px * py) / tmp
+            c2 = (a2[1] - py**2) / tmp
+            tmp = np.sqrt(c1**2+c2)
+            m1, m2 = c1 + tmp, c1 - tmp
+            tmp = a2[1] / a2[0]
+            m1_sqr = m1**2
+            m2_sqr = m2**2
+            x1 = (px * m1_sqr - py * m1) / (tmp + m1_sqr)
+            x2 = (px * m2_sqr - py * m2) / (tmp + m2_sqr)
+            y1 = m1 * (x1 - px) + py
+            y2 = m2 * (x2 - px) + py
+            tp1_obstacle = np.array([x1, y1])
+            tp2_obstacle = np.array([x2, y2])
+
+        tp1 = self.transform(tp1_obstacle, Frame.OBSTACLE, output_frame)
+        tp2 = self.transform(tp2_obstacle, Frame.OBSTACLE, output_frame)
+
+        if is_ccw(x, tp1, tp2):
+            tp1, tp2 = tp2, tp1
+
+        return [tp1, tp2]
+
+    # def area(self):
+    #     return self._area
+
+    # ------------ Private methods ------------ #
+    def _check_convexity(self):
+        self._is_convex = True
+
+    def _compute_kernel(self):
+        self._kernel = self._polygon
+
+    def _compute_polygon_representation(self):
+        # logprint(str(self) + ": " + str(self._polygon), 0)
+        t = np.linspace(0, 2 * np.pi, self._n_pol, endpoint=False)
+        a = self._a + 1e-3 # Add offset to adjust for polygon approximation
+        polygon = np.vstack((a[0] * np.cos(t), a[1] * np.sin(t))).T
+        self._polygon = shapely.geometry.Polygon(polygon)
diff --git a/python/ur_simple_control/path_generation/starworlds/obstacles/motion_model.py b/python/ur_simple_control/path_generation/starworlds/obstacles/motion_model.py
new file mode 100644
index 0000000000000000000000000000000000000000..64e4746494cc9c7b3f8d43459f8dfdff44807bf3
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/obstacles/motion_model.py
@@ -0,0 +1,119 @@
+import numpy as np
+from abc import ABC, abstractmethod
+
+
+class MotionModel(ABC):
+
+    def __init__(self, pos=None, rot=0):
+        self._pos = np.array([0., 0.]) if pos is None else np.array(pos, dtype='float64')
+        self._rot = rot
+        self._t = 0.
+
+    def move(self, obs_self, dt):
+        xy_vel = np.array(self.lin_vel())
+        rot_vel = self.rot_vel()
+        prev_pos, prev_rot = self._pos.copy(), self._rot
+        self._pos += xy_vel * dt
+        self._rot += rot_vel * dt
+        self._t += dt
+
+    def set_pos(self, pos):
+        self._pos = pos
+
+    def set_rot(self, rot):
+        self._rot = rot
+
+    def pos(self):
+        return self._pos
+
+    def rot(self):
+        return self._rot
+
+    @abstractmethod
+    def lin_vel(self):
+        pass
+
+    @abstractmethod
+    def rot_vel(self):
+        pass
+
+
+class Static(MotionModel):
+
+    def lin_vel(self):
+        return np.zeros(2)
+
+    def rot_vel(self):
+        return 0.
+
+
+class SinusVelocity(MotionModel):
+
+    # If cartesian_coords: (x1,x2) vel are Cartesian (x,y) vel.
+    # Else:                (x1,x2) vel are linear and rotational (lin,ang) vel.
+    def __init__(self, pos=None, rot=0, cartesian_coords=True, x1_mag=0., x1_period=0, x2_mag=0., x2_period=0):
+        super().__init__(pos, rot)
+        self.cartesian_coords = cartesian_coords
+        self.x1_vel_mag = x1_mag
+        self.x1_vel_freq = 2 * np.pi / x1_period if x1_period else 0
+        self.x2_vel_mag = x2_mag
+        self.x2_vel_freq = 2 * np.pi / x2_period if x2_period else 0
+
+    def lin_vel(self):
+        if self.cartesian_coords:
+            return np.array([self.x1_vel_mag * np.cos(self.x1_vel_freq * self._t),
+                             self.x2_vel_mag * np.cos(self.x2_vel_freq * self._t)])
+        else:
+            rot_mat = np.array([[np.cos(self._rot), -np.sin(self._rot)], [np.sin(self._rot), np.cos(self._rot)]])
+            return rot_mat.dot([self.x1_vel_mag * np.cos(self.x1_vel_freq * self._t), 0])
+
+    def rot_vel(self):
+        if self.cartesian_coords:
+            return 0.
+        else:
+            return np.array(self.x2_vel_mag * np.cos(self.x2_vel_freq * self._t))
+
+
+class Interval(MotionModel):
+
+    def __init__(self, init_pos, time_pos):
+        self.pos_point = np.array([init_pos] + [p for _, p in time_pos])
+        self.time_point = np.cumsum([0] + [t for t, _ in time_pos])
+        super().__init__(init_pos, 0)
+
+    def lin_vel(self):
+        if self._t > self.time_point[-1]:
+            vel_norm = np.linalg.norm((self.pos_point[-1] - self.pos_point[-2]) / (self.time_point[-1] - self.time_point[-2]))
+            dir = self.pos_point[-1] - self.pos()
+            if np.linalg.norm(dir) > vel_norm:
+                dir /= np.linalg.norm(dir)
+            return vel_norm * dir
+        idx = np.argmax(self._t < self.time_point)
+        return (self.pos_point[idx] - self.pos_point[idx-1]) / (self.time_point[idx] - self.time_point[idx-1])
+
+    def rot_vel(self):
+        return 0.
+
+
+class Waypoints(MotionModel):
+
+    def __init__(self, init_pos, waypoints, vel, wp_thresh=0.2):
+        self.waypoints = np.array([init_pos] + waypoints)
+        self.vel = vel
+        self._wp_idx = 0
+        self.wp_thresh = wp_thresh
+        super().__init__(init_pos, 0)
+
+    def lin_vel(self):
+        if not self._wp_idx < len(self.waypoints):
+            return np.zeros(2)
+        dir = self.waypoints[self._wp_idx] - self.pos()
+        wp_dist = np.linalg.norm(dir)
+        if wp_dist < self.wp_thresh:
+            self._wp_idx += 1
+            return self.lin_vel()
+        dir /= wp_dist
+        return self.vel * dir
+
+    def rot_vel(self):
+        return 0.
diff --git a/python/ur_simple_control/path_generation/starworlds/obstacles/obstacle.py b/python/ur_simple_control/path_generation/starworlds/obstacles/obstacle.py
new file mode 100644
index 0000000000000000000000000000000000000000..7789611537fe4a9621377a6521a8c1d6ae239f7d
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/obstacles/obstacle.py
@@ -0,0 +1,201 @@
+from abc import ABC, abstractmethod
+import numpy as np
+import shapely
+from shapely import affinity as sh_affinity
+from utils import affine_transform
+from copy import deepcopy
+from enum import Enum
+
+
+class Frame(Enum):
+    GLOBAL = 1
+    OBSTACLE = 2
+
+    class InvalidFrameError(Exception):
+        pass
+
+
+class Obstacle(ABC):
+    """ Abstract base class of obstacles
+    """
+    id_counter = 0
+
+    # obs_id <0: temp object, obs_id=0: new object, obs_id>0: existing object with id #obs_id
+    def __init__(self, motion_model=None, is_convex=None, is_starshaped=None, id='new', name=None, compute_polygon=False):
+        self._id = None
+        self._is_convex = is_convex
+        self._is_starshaped = is_starshaped
+        # Pose of local frame in global frame
+        self._motion_model = motion_model  # if motion_model is not None else mm.Static([0., 0.], 0.)
+        # Initialize id and name
+        self._set_id_name(id, name)
+        self._polygon = None  # Polygon in obstacle frame
+        self._polygon_global = None  # Polygon in global frame if static obstacle
+        self._polygon_global_pose = None  # Obstacle pose corresponding to global polygon
+        if compute_polygon:
+            self._compute_global_polygon_representation()
+
+    def __str__(self): return self._name
+
+    def copy(self, id='temporary', name=None):
+        ob = deepcopy(self)
+        if not (id == 'duplicate' or id == 'd'):
+            ob._set_id_name(id, name)
+        return ob
+
+    def pos(self, output_frame=Frame.GLOBAL):
+        if output_frame == Frame.OBSTACLE or self._motion_model is None:
+            return np.zeros(2)
+        if output_frame == Frame.GLOBAL:
+            return self._motion_model.pos()
+
+    def rot(self, output_frame=Frame.GLOBAL):
+        if output_frame == Frame.OBSTACLE or self._motion_model is None:
+            return 0.
+        if output_frame == Frame.GLOBAL:
+            return self._motion_model.rot()
+
+    def interior_point(self, x, input_frame=Frame.GLOBAL):
+        return True if self.point_location(x, input_frame) < 0 else False
+
+    def exterior_point(self, x, input_frame=Frame.GLOBAL):
+        return True if self.point_location(x, input_frame) > 0 else False
+
+    def boundary_point(self, x, input_frame=Frame.GLOBAL):
+        return np.isclose(self.point_location(x, input_frame), 0.)
+
+    def move(self, dt):
+        if self._motion_model is not None:
+            self._motion_model.move(self, dt)
+
+    def id(self): return self._id
+
+    def polygon(self, output_frame=Frame.GLOBAL):
+        if self._polygon is None:
+            self._compute_polygon_representation()
+            # self._compute_global_polygon_representation()
+        if output_frame == Frame.OBSTACLE or self._motion_model is None:
+            return self._polygon
+        elif output_frame == Frame.GLOBAL:
+            current_pose = [*self._motion_model.pos(), self._motion_model.rot()]
+            if not current_pose == self._polygon_global_pose:
+                self._polygon_global_pose = current_pose
+                c, s = np.cos(current_pose[2]), np.sin(current_pose[2])
+                trans_matrix = np.array([[c, -s, current_pose[0]], [s, c, current_pose[1]], [0, 0, 1]])
+                affinity_matrix = [trans_matrix[0, 0], trans_matrix[0, 1], trans_matrix[1, 0], trans_matrix[1, 1], trans_matrix[0, 2], trans_matrix[1, 2]]
+                self._polygon_global = sh_affinity.affine_transform(self._polygon, affinity_matrix)
+            return self._polygon_global
+        else:
+            raise Frame.InvalidFrameError
+
+    def intersects(self, other):
+        return self.polygon().intersects(other.polygon())
+
+    def transform(self, x, input_frame, output_frame):
+        if input_frame == output_frame or self._motion_model is None:
+            return x
+        elif input_frame == Frame.OBSTACLE and output_frame == Frame.GLOBAL:
+            return self._transform_obstacle2global(x)
+        elif input_frame == Frame.GLOBAL and output_frame == Frame.OBSTACLE:
+            return self._transform_global2obstacle(x)
+        else:
+            raise Frame.InvalidFrameError
+
+    def rotate(self, x, input_frame, output_frame):
+        if input_frame == output_frame or self._motion_model is None:
+            return x
+        elif input_frame == Frame.OBSTACLE and output_frame == Frame.GLOBAL:
+            return self._rotate_obstacle2global(x)
+        elif input_frame == Frame.GLOBAL and output_frame == Frame.OBSTACLE:
+            return self._rotate_global2obstacle(x)
+        else:
+            raise Frame.InvalidFrameError
+
+    def is_convex(self):
+        # Check if convexity already has been computed
+        if self._is_convex is None:
+            self._check_convexity()
+        return self._is_convex
+
+    def is_starshaped(self):
+        if self._is_starshaped is None:
+            if self.is_convex():
+                self._is_starshaped = True
+            else:
+                # TODO: Add check for starshapedness. Currently default to not starshaped
+                self._is_starshaped = False
+        return self._is_starshaped
+
+    def set_motion_model(self, motion_model):
+        self._motion_model = motion_model
+
+    # ------------ Private methods ------------ #
+    def _set_id_name(self, id, name=None):
+        if id == 'new' or id == 'n':
+            Obstacle.id_counter += 1
+            self._id = Obstacle.id_counter
+        elif id == 'temporary' or id == 'temp' or id == 't':
+            self._id = None
+        elif isinstance(id, int) and 0 < id <= Obstacle.id_counter:
+            self._id = id
+        else:
+            print("Invalid id '" + str(id) + "' in set_id. Create temporary obstacle.")
+            self._id = None
+        self._name = name if name else str(self._id)
+
+    def _rotate_obstacle2global(self, x_obstacle):
+        rot = self._motion_model.rot()
+        return affine_transform(x_obstacle, rotation=rot, translation=[0, 0])
+
+    def _rotate_global2obstacle(self, x_global):
+        rot = self._motion_model.rot()
+        return affine_transform(x_global, rotation=rot, translation=[0, 0], inverse=True)
+
+    def _transform_obstacle2global(self, x_obstacle):
+        pos, rot = (self._motion_model.pos(), self._motion_model.rot())
+        return affine_transform(x_obstacle, rotation=rot, translation=pos)
+
+    def _transform_global2obstacle(self, x_global):
+        pos, rot = (self._motion_model.pos(), self._motion_model.rot())
+        return affine_transform(x_global, rotation=rot, translation=pos, inverse=True)
+
+    def _compute_global_polygon_representation(self):
+        self._compute_polygon_representation()
+        if self._motion_model is None:
+            self._polygon_global = self._polygon
+        if self._motion_model.__class__.__name__ == 'Static':
+            pos, rot = (self._motion_model.pos(), self._motion_model.rot())
+            c, s = np.cos(rot), np.sin(rot)
+            trans_matrix = np.array([[c, -s, pos[0]], [s, c, pos[1]], [0, 0, 1]])
+            affinity_matrix = [trans_matrix[0, 0], trans_matrix[0, 1], trans_matrix[1, 0], trans_matrix[1, 1],
+                               trans_matrix[0, 2], trans_matrix[1, 2]]
+            self._polygon_global = shapely.affinity.affine_transform(self._polygon, affinity_matrix)
+
+    # ------------ Abstract methods ------------ #
+    @abstractmethod
+    def draw(self):
+        pass
+
+    @abstractmethod
+    def point_location(self, x, input_frame=Frame.GLOBAL):
+        pass
+
+    @abstractmethod
+    def dilated_obstacle(self, padding, id="new", name=None):
+        pass
+
+    @abstractmethod
+    def line_intersection(self, line, input_frame=Frame.GLOBAL, output_frame=Frame.GLOBAL):
+        pass
+
+    @abstractmethod
+    def tangent_points(self, x, input_frame=Frame.GLOBAL, output_frame=Frame.GLOBAL):
+        pass
+
+    @abstractmethod
+    def _check_convexity(self):
+        pass
+
+    @abstractmethod
+    def _compute_polygon_representation(self):
+        pass
diff --git a/python/ur_simple_control/path_generation/starworlds/obstacles/polygon.py b/python/ur_simple_control/path_generation/starworlds/obstacles/polygon.py
new file mode 100644
index 0000000000000000000000000000000000000000..d8ec63a62b891136497967a50f588d08dd1b8343
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/obstacles/polygon.py
@@ -0,0 +1,158 @@
+from obstacles import Obstacle, Frame
+from utils import is_cw, is_ccw, is_collinear, tic, toc
+import shapely
+import numpy as np
+import matplotlib.pyplot as plt
+import matplotlib.patches as patches
+
+
+class Polygon(Obstacle):
+
+    def __init__(self, polygon, **kwargs):
+        super().__init__(**kwargs)
+        self._polygon = shapely.geometry.Polygon(polygon)
+        self._polygon = shapely.ops.orient(self._polygon)
+        self._pol_bounds = self._polygon.bounds
+        self._compute_global_polygon_representation()
+        self.vertices = np.array(self._polygon.exterior.coords[:-1])
+        self.circular_vertices = np.array(self._polygon.exterior.coords)
+
+    def init_plot(self, ax=None, show_name=False, **kwargs):
+        if ax is None:
+            _, ax = plt.subplots(subplot_kw={'aspect': 'equal'})
+        if "fc" not in kwargs and "facecolor" not in kwargs:
+            kwargs["fc"] = 'lightgrey'
+        if 'show_reference' in kwargs:
+            del kwargs['show_reference']
+        line_handles = []
+        # Boundary
+        line_handles += [patches.Polygon(np.random.rand(3, 2), **kwargs)]
+        ax.add_patch(line_handles[-1])
+        # Name
+        line_handles += [ax.text(0, 0, self._name)] if show_name else [None]
+        return line_handles, ax
+
+    def extreme_points(self, frame=Frame.GLOBAL):
+        vertices = np.asarray(self.polygon(frame).exterior.coords)[:-1, :]
+        return [vertices[i] for i in range(vertices.shape[0])]
+
+    def update_plot(self, line_handles, frame=Frame.GLOBAL):
+        polygon = self.polygon(frame)
+        boundary = np.vstack((polygon.exterior.xy[0], polygon.exterior.xy[1])).T
+        line_handles[0].set_xy(boundary)
+        if line_handles[1] is not None:
+            line_handles[1].set_position(self.pos(frame))
+
+    def draw(self, frame=Frame.GLOBAL, **kwargs):
+        line_handles, ax = self.init_plot(**kwargs)
+        self.update_plot(line_handles, frame)
+        return line_handles, ax
+
+    def dilated_obstacle(self, padding, id="new", name=None):
+        cp = self.copy(id, name)
+        cp._polygon = cp._polygon.buffer(padding, cap_style=1, join_style=1)
+        cp._pol_bounds = cp._polygon.bounds
+        cp.vertices = np.array(cp._polygon.exterior.coords[:-1])
+        cp.circular_vertices = np.array(cp._polygon.exterior.coords)
+        cp._polygon_global_pose = None
+        cp._polygon_global = None
+        return cp
+
+    def point_location(self, x, input_frame=Frame.GLOBAL):
+        x_obstacle = self.transform(x, input_frame, Frame.OBSTACLE)
+        xmin, ymin, xmax, ymax = self._pol_bounds
+        if not (xmin < x_obstacle[0] < xmax and ymin < x_obstacle[1] < ymax):
+            return 1
+        x_sh = shapely.geometry.Point(x_obstacle)
+        if self._polygon.contains(x_sh):
+            return -1
+        if self._polygon.exterior.contains(x_sh):
+            return 0
+        return 1
+
+    def line_intersection(self, line, input_frame=Frame.GLOBAL, output_frame=Frame.GLOBAL):
+        l0_obstacle = self.transform(line[0], input_frame, Frame.OBSTACLE)
+        l1_obstacle = self.transform(line[1], input_frame, Frame.OBSTACLE)
+        intersection_points_shapely = shapely.geometry.LineString([l0_obstacle, l1_obstacle]).intersection(self._polygon.exterior)
+        if intersection_points_shapely.is_empty:
+            return []
+        if intersection_points_shapely.geom_type == 'Point':
+            intersection_points_obstacle = [np.array([intersection_points_shapely.x, intersection_points_shapely.y])]
+        elif intersection_points_shapely.geom_type == 'MultiPoint':
+            intersection_points_obstacle = [np.array([p.x, p.y]) for p in intersection_points_shapely.geoms]
+        elif intersection_points_shapely.geom_type == 'LineString':
+            intersection_points_obstacle = [np.array([ip[0], ip[1]]) for ip in intersection_points_shapely.coords]
+        elif intersection_points_shapely.geom_type == 'MultiLineString':
+            intersection_points_obstacle = [np.array([ip[0], ip[1]]) for line in intersection_points_shapely.geoms for ip in line.coords]
+        else:
+            print(intersection_points_shapely)
+        return [self.transform(ip, Frame.OBSTACLE, output_frame) for ip in intersection_points_obstacle]
+
+    def tangent_points(self, x, input_frame=Frame.GLOBAL, output_frame=Frame.GLOBAL):
+
+        x_obstacle = self.transform(x, input_frame, Frame.OBSTACLE)
+        t0 = tic()
+        phi = np.arctan2(self.circular_vertices[:, 1] - x_obstacle[1], self.circular_vertices[:, 0] - x_obstacle[0])
+        phi[phi < 0] += 2 * np.pi
+        t1 = toc(t0)
+        t0 = tic()
+        phi_diff = np.diff(phi)
+        t2 = toc(t0)
+        t0 = tic()
+        phi_decrease_idcs = phi_diff > np.pi
+        phi_increase_idcs = phi_diff < -np.pi
+        t3 = toc(t0)
+        t0 = tic()
+        phi_decrease_idcs = np.flatnonzero(phi_decrease_idcs)
+        phi_increase_idcs = np.flatnonzero(phi_increase_idcs)
+        for i in phi_decrease_idcs:
+            phi[i+1:] -= 2*np.pi
+        for i in phi_increase_idcs:
+            phi[i+1:] += 2*np.pi
+        t4 = toc(t0)
+
+        t0 = tic()
+
+        i_min, i_max = np.argmin(phi), np.argmax(phi)
+
+        if abs(phi[0] - phi[-1]) > 0.00001:
+            # Interior point
+            return []
+        if (phi[i_max] - phi[i_min]) >= 2*np.pi:
+            # Blocked exterior point
+            return []
+        t5 = toc(t0)
+
+        t0 = tic()
+        tp1_obstacle = self.circular_vertices[i_max]
+        tp2_obstacle = self.circular_vertices[i_min]
+
+        tp1 = self.transform(tp1_obstacle, Frame.OBSTACLE, output_frame)
+        tp2 = self.transform(tp2_obstacle, Frame.OBSTACLE, output_frame)
+
+        tend = toc(t0)
+        # print(sum([t1*100,t2*100,t3*100,t4*100,t5*100,tend*100*0]))
+        return [tp1, tp2]
+
+    def area(self):
+        return self._polygon.area
+
+    # ------------ Private methods ------------ #
+    def _check_convexity(self):
+        v = np.asarray(self._polygon.exterior.coords)[:-1, :]
+        i = 0
+        N = v.shape[0]
+        # Make sure first vertice is not collinear
+        while is_collinear(v[i-1, :], v[i, :], v[(i+1) % N, :]):
+            i += 1
+            if i > N:
+                raise RuntimeError("Bad polygon shape. All vertices collinear")
+        # All vertices must be either cw or ccw when iterating through for convexity
+        if is_cw(v[i-1, :], v[i, :], v[i+1, :]):
+            self._is_convex = not any([is_ccw(v[j-1, :], v[j, :], v[(j+1) % N, :]) for j in range(v.shape[0])])
+        else:
+            self._is_convex = not any([is_cw(v[j-1, :], v[j, :], v[(j+1) % N, :]) for j in range(v.shape[0])])
+
+    # Not needed
+    def _compute_polygon_representation(self):
+        pass
diff --git a/python/ur_simple_control/path_generation/starworlds/obstacles/starshaped_obstacle.py b/python/ur_simple_control/path_generation/starworlds/obstacles/starshaped_obstacle.py
new file mode 100644
index 0000000000000000000000000000000000000000..122ab8f02c24f2b1c0d57754353c639f4d5a3fae
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/obstacles/starshaped_obstacle.py
@@ -0,0 +1,85 @@
+from abc import abstractmethod
+import numpy as np
+from obstacles import Obstacle, Frame
+from shapely import affinity as sh_affinity
+
+
+class StarshapedObstacle(Obstacle):
+
+    def __init__(self, xr, **kwargs):
+        super().__init__(is_starshaped=True, **kwargs)
+        self._xr = np.array(xr)  # Reference point in obstacle frame
+        self._kernel = None
+        self._kernel_global = None  # Kernel in global frame
+        self._kernel_global_pose = None  # Obstacle pose corresponding to global kernel
+
+    def xr(self, output_frame=Frame.GLOBAL):
+        return self.transform(self._xr, Frame.OBSTACLE, output_frame)
+
+    def set_xr(self, xr, input_frame=Frame.OBSTACLE, safe_set=False):
+        new_xr = self.transform(xr, input_frame, Frame.OBSTACLE)
+        if safe_set:
+            k = self.kernel()
+            if not k.exterior_point(new_xr, Frame.OBSTACLE):
+                self._xr = new_xr
+        else:
+            self._xr = new_xr
+
+    def reference_direction(self, x, input_frame=Frame.GLOBAL, output_frame=Frame.GLOBAL):
+        dir = self.transform(x, input_frame, output_frame) - self.transform(self._xr, Frame.OBSTACLE, output_frame)
+        if not np.any(dir):
+            print("reference_direction for xr is not defined")
+        return dir / np.linalg.norm(dir, axis=x.ndim - 1)
+
+    # interior point: <1. exterior point: >1. boundary point: 1.
+    def distance_function(self, x, input_frame=Frame.GLOBAL):
+        x_obstacle = self.transform(x, input_frame, Frame.OBSTACLE)
+        dist_func = (np.linalg.norm(x_obstacle - self._xr, axis=x.ndim - 1) / (
+            np.linalg.norm(self.boundary_mapping(x_obstacle, input_frame=Frame.OBSTACLE, output_frame=Frame.OBSTACLE)
+                           - self._xr, axis=x.ndim - 1))) ** 2
+        return dist_func
+
+    # interior point: <0. exterior point: >0. boundary point: 0.
+    def point_location(self, x, input_frame=Frame.GLOBAL):
+        return np.sign(self.distance_function(x, input_frame)-1.)
+
+    def kernel(self, output_frame=Frame.GLOBAL):
+        if self._kernel is None:
+            self._compute_kernel()
+        if output_frame == Frame.OBSTACLE or self._motion_model is None:
+            return self._kernel
+        elif output_frame == Frame.GLOBAL:
+            current_pose = [*self._motion_model.pos(), self._motion_model.rot()]
+            if not current_pose == self._kernel_global_pose:
+                self._kernel_global_pose = current_pose
+                c, s = np.cos(current_pose[2]), np.sin(current_pose[2])
+                trans_matrix = np.array([[c, -s, current_pose[0]], [s, c, current_pose[1]], [0, 0, 1]])
+                affinity_matrix = [trans_matrix[0, 0], trans_matrix[0, 1], trans_matrix[1, 0], trans_matrix[1, 1], trans_matrix[0, 2], trans_matrix[1, 2]]
+                self._kernel_global = sh_affinity.affine_transform(self._kernel, affinity_matrix)
+            return self._kernel_global
+        else:
+            raise Frame.InvalidFrameError
+
+    def vel_intertial_frame(self, x):
+        if self._motion_model is None:
+            return np.zeros(2)
+        omega = self._motion_model.rot_vel()
+        lin_vel_omega = np.cross(np.hstack(([0, 0], omega)), np.hstack((self.reference_direction(x), 0)))[:2]
+        return self._motion_model.lin_vel() + lin_vel_omega
+
+    # ------------ Abstract methods ------------ #
+    @abstractmethod
+    def normal(self, x, input_frame=Frame.GLOBAL, output_frame=Frame.GLOBAL, x_is_boundary=False):
+        pass
+
+    @abstractmethod
+    def boundary_mapping(self, x, input_frame=Frame.GLOBAL, output_frame=Frame.GLOBAL):
+        pass
+
+    # @abstractmethod
+    # def signed_boundary_distance(self, x, input_frame=Frame.GLOBAL):
+    #     pass
+
+    @abstractmethod
+    def _compute_kernel(self):
+        pass
diff --git a/python/ur_simple_control/path_generation/starworlds/obstacles/starshaped_polygon.py b/python/ur_simple_control/path_generation/starworlds/obstacles/starshaped_polygon.py
new file mode 100644
index 0000000000000000000000000000000000000000..85eba3ce218671de85d625c612a6fa9494377efe
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/obstacles/starshaped_polygon.py
@@ -0,0 +1,866 @@
+from obstacles import Frame, StarshapedObstacle, Polygon
+import matplotlib.pyplot as plt
+import numpy as np
+import shapely
+from utils import is_cw, is_ccw, is_collinear, orientation_val, get_intersection, is_between, tic, toc
+
+
+class StarshapedPolygon(Polygon, StarshapedObstacle):
+
+    def __init__(self, polygon, xr=None, **kwargs):
+        super().__init__(polygon, xr=xr, **kwargs)
+        if xr is None:
+            self._compute_kernel()
+            if self._kernel.contains(self._kernel.centroid):
+                self._xr = np.array(self._kernel.centroid.coords[0])
+            else:
+                self._xr = np.array(self._kernel.representative_point().coords[0])
+        else:
+            self._xr = np.array(xr)
+        self.vertex_angles = None
+        self._update_vertex_angles()
+        self.enclosing_ball_diameter = self._polygon.bounds[2]-self._polygon.bounds[0] + self._polygon.bounds[3]-self._polygon.bounds[1]
+
+    def copy(self, id, name):
+        if (id == 'duplicate' or id == 'd'):
+            id = self.id()
+        return StarshapedPolygon(id=id, name=name, polygon=self._polygon, xr=self._xr, motion_model=self._motion_model)
+
+    # Note: Does not recompute the kernel
+    def dilated_obstacle(self, padding, id="new", name=None):
+        cp = self.copy(id, name)
+        cp._polygon = cp._polygon.buffer(padding, cap_style=1, join_style=1)
+        cp._pol_bounds = cp._polygon.bounds
+        cp.enclosing_ball_diameter = max(cp._polygon.bounds[2]-cp._polygon.bounds[0], cp._polygon.bounds[3]-cp._polygon.bounds[1])
+        cp.vertices = np.array(cp._polygon.exterior.coords[:-1])
+        cp.circular_vertices = np.array(cp._polygon.exterior.coords)
+        cp._polygon_global_pose = None
+        cp._polygon_global = None
+        cp._update_vertex_angles()
+        return cp
+
+    def distance_function(self, x, input_frame=Frame.GLOBAL):
+        x_obstacle = self.transform(x, input_frame, Frame.OBSTACLE)
+        dist_center = np.linalg.norm(x_obstacle - self._xr, axis=x.ndim - 1)
+        local_radius = np.linalg.norm(self.boundary_mapping(x_obstacle, input_frame=Frame.OBSTACLE, output_frame=Frame.OBSTACLE)
+                           - self._xr, axis=x.ndim - 1)
+        if dist_center < local_radius:
+            # Return proportional inside to have -> [0, 1]
+            return (dist_center / local_radius) ** 2
+        else:
+            return ((dist_center - local_radius) + 1) ** 2
+
+
+    def boundary_mapping(self, x, input_frame=Frame.GLOBAL, output_frame=Frame.GLOBAL):
+        x_obstacle = self.transform(x, input_frame, Frame.OBSTACLE)
+        intersect_obstacle = self.line_intersection([self._xr, self._xr+1.1*self.enclosing_ball_diameter*self.reference_direction(x_obstacle, Frame.OBSTACLE, Frame.OBSTACLE)],
+                                                    input_frame=Frame.OBSTACLE, output_frame=Frame.OBSTACLE)
+        if not intersect_obstacle:
+            return None
+        if len(intersect_obstacle) == 1:
+            return self.transform(intersect_obstacle[0], Frame.OBSTACLE, output_frame)
+        else:
+            if np.linalg.norm(intersect_obstacle[0]-x_obstacle) < np.linalg.norm(intersect_obstacle[1]-x_obstacle):
+                return self.transform(intersect_obstacle[0], Frame.OBSTACLE, output_frame)
+            else:
+                return self.transform(intersect_obstacle[1], Frame.OBSTACLE, output_frame)
+
+    def normal(self, x, input_frame=Frame.GLOBAL, output_frame=Frame.GLOBAL, x_is_boundary=False, type='edge'):
+        x_obstacle = self.transform(x, input_frame, Frame.OBSTACLE)
+        angle = np.arctan2(x_obstacle[1]-self._xr[1], x_obstacle[0]-self._xr[0])
+        v_idx = np.argmax(self.vertex_angles > angle)
+
+        if v_idx == self.vertices.shape[0]:
+            v_idx = 0
+
+        if type == 'edge':
+            n_obstacle = np.array([self.vertices[v_idx, 1] - self.vertices[v_idx - 1, 1], self.vertices[v_idx - 1, 0] - self.vertices[v_idx, 0]])
+            n_obstacle /= np.linalg.norm(n_obstacle)
+        elif type == 'weighted_edges':
+            edge_neighbors = [(self.vertices[(v_idx - 2 + i) % self.vertices.shape[0]], self.vertices[(v_idx - 1 + i) % self.vertices.shape[0]]) for i in range(3)]
+            edge_neighbors_normal = np.array([[e[1][1] - e[0][1],
+                                              e[0][0] - e[1][0]] for e in edge_neighbors])
+            edge_closest = [shapely.ops.nearest_points(shapely.geometry.LineString(e),
+                                                       shapely.geometry.Point(x_obstacle))[0].coords[0] for e in edge_neighbors]
+
+            dist = [np.linalg.norm(np.array(e)-x_obstacle) for e in edge_closest]
+            w = np.array([1/(d+1e-6) for d in dist])
+            w /= sum(w)
+            n_obstacle = edge_neighbors_normal.T.dot(w)
+        return self.rotate(n_obstacle, Frame.OBSTACLE, output_frame)
+
+
+        #directional weighted mean (see Appendix A) of normal vectors of the surface tiles ni(ΞΎ), the weights wi,
+        # and with respect to the reference direction r
+        # n_vert = self.vertices.shape[0]
+        # r = self.reference_direction(x, input_frame=Frame.OBSTACLE, output_frame=Frame.OBSTACLE)
+        # B = np.array((r, (-r[1], r[0])))
+        # ws = np.zeros(n_vert)
+        # kappa = np.zeros(n_vert)
+        # p = 3
+        # vi_min = np.inf
+        #
+        # edge_r = np.zeros((n_vert, 2))
+        # _, ax = self.draw(frame=Frame.OBSTACLE)
+        # ax.plot(*x_obstacle, 'ko')
+        # for i in range(n_vert):
+        #     # surface_i = np.array([self.vertices[v_idcs[i] - 1, :], self.vertices[v_idcs[i], :]])
+        #     # edge_normal_i = np.array([-(surface_i[0, 1] - surface_i[1, 1]), surface_i[0, 0] - surface_i[1, 0]])
+        #     edge_normal_i = np.array([self.vertices[i, 1] - self.vertices[i - 1, 1], self.vertices[i - 1, 0] - self.vertices[i, 0]])
+        #     edge_normal_i /= np.linalg.norm(edge_normal_i)
+        #     # rotated_edge_normal_i = Rpn.T.dot(edge_normal_i)
+        #
+        #     pi = np.array(
+        #         shapely.ops.nearest_points(shapely.geometry.LineString([self.vertices[i-1, :], self.vertices[i, :]]),
+        #                                    shapely.geometry.Point(x_obstacle))[0].coords[0])
+        #     # ax.quiver(*pi, *edge_normals[i, :], color='k')
+        #     edge_r[i, :] = [np.mean([self.vertices[i, 0], self.vertices[i - 1, 0]]),
+        #                     np.mean([self.vertices[i, 1], self.vertices[i - 1, 1]])]
+        #     vi = x_obstacle - pi
+        #     ei = (vi - edge_normal_i.dot(vi)*edge_normal_i) * np.sign(vi.dot(x_obstacle-edge_r[i, :]))
+        #     phi = np.arccos(ei.dot(vi) / (np.linalg.norm(ei) * np.linalg.norm(vi))) * np.sign(edge_normal_i.dot(vi))
+        #
+        #     if phi > 0:
+        #         ws[i] = 0
+        #     else:
+        #         ws[i] = (np.pi / phi) ** p - 1
+        #
+        #     # if np.linalg.norm(vi) < vi_min:
+        #     #     ws = np.zeros(n_vert)
+        #     #     ws[i] = 1
+        #     #     vi_min = np.linalg.norm(vi)
+        #
+        #
+        #
+        #     # if np.all(np.isclose(pi, x_obstacle)):
+        #     #     ws[i] = -1
+        #     # else:
+        #     #     ws[i] = 1 / np.linalg.norm(x_obstacle - pi) ** 2
+        #     rotated_edge_normal_i = B.T.dot(edge_normal_i)
+        #
+        #     kappa[i] = 0 if rotated_edge_normal_i[0] == 1 else np.arccos(rotated_edge_normal_i[0]) * np.sign(
+        #         rotated_edge_normal_i[1])
+        #
+        #     # v_idx += 1
+        # # if np.any(ws < 0):
+        # #     ws[np.nonzero(ws > 0)] = 0
+        # #     ws[np.nonzero(ws)] = 1
+        # #
+        # ws = ws / np.sum(ws)
+        # # n_obstacle2 =
+        #
+        # kappa_bar = ws.dot(kappa)
+        # tmp_vec = [np.cos(abs(kappa_bar)), np.sin(abs(kappa_bar)) * np.sign(kappa_bar)]
+        # n_obstacle = B.dot(tmp_vec)
+        #
+        # return self.rotate(n_obstacle, Frame.OBSTACLE, output_frame)
+
+    def set_xr(self, xr, input_frame=Frame.OBSTACLE, safe_set=False):
+        super().set_xr(xr, input_frame, safe_set)
+        self._update_vertex_angles()
+    
+    def init_plot(self, ax=None, show_reference=True, show_name=False, **kwargs):
+        line_handles, ax = super().init_plot(ax=ax, show_name=show_name, **kwargs)
+        if ax is None:
+            _, ax = plt.subplots(subplot_kw={'aspect': 'equal'})
+        # Reference point
+        line_handles += ax.plot(0, 0, '+', color='k') if show_reference else [None]
+        return line_handles, ax
+
+    def update_plot(self, line_handles, frame=Frame.GLOBAL):
+        super().update_plot(line_handles, frame)
+        if line_handles[1] is not None:
+            line_handles[1].set_position(self.xr(frame))
+        if line_handles[2] is not None:
+            line_handles[2].set_data(*self.xr(frame))
+
+    def _update_vertex_angles(self):
+        self.vertex_angles = np.arctan2(self.vertices[:, 1] - self._xr[1], self.vertices[:, 0] - self._xr[0])
+        idcs = np.argsort(self.vertex_angles)
+        self.vertex_angles = self.vertex_angles[idcs]
+        self.vertices = self.vertices[idcs, :]
+        self.circular_vertices = np.vstack((self.vertices, self.vertices[0, :]))
+        self.vertex_angles = np.hstack((self.vertex_angles, self.vertex_angles[0] + 2 * np.pi))
+
+    def _compute_kernel(self, verbose=False, debug=False):
+        if self.is_convex():
+            self._kernel = self._polygon
+            return
+
+        # Returns true if vertex v[i, :] is reflex
+        def is_reflex(i):
+            return is_cw(v[i - 1, :], v[i, :], v[(i + 1) % v.shape[0], :])
+
+        # Show polygon
+        def draw(xk, F1_idx, L1_idx, i, xk_bounded):
+            v_ext = np.vstack((v, v[0, :]))
+            xk_ext = np.vstack((xk, xk[0, :])) if xk_bounded else xk
+            plt.plot(v_ext[:, 0], v_ext[:, 1], label='v', marker='.')
+            # plt.plot(v_ext[:, 0], v_ext[:, 1], label='v')
+            # plt.text(v_ext[0, 0], v_ext[0, 1], 'v0')
+            plt.text(xk[0, 0], xk[0, 1], 'xk0')
+            axes = plt.gca()
+            xlim, ylim = axes.get_xlim(), axes.get_ylim()
+            plt.plot(xk_ext[:, 0], xk_ext[:, 1], label='xk')
+            plt.plot(xk[F1_idx, 0], xk[F1_idx, 1], marker='o', c='c', label='F1')
+            end_point = v[i, :] + INF_VAL * (xk[F1_idx, :] - v[i, :]) / np.linalg.norm(xk[F1_idx, :] - v[i, :])
+            plt.plot([v[i, 0], end_point[0]], [v[i, 1], end_point[1]], 'c--')
+            plt.plot(xk[L1_idx, 0], xk[L1_idx, 1], marker='o', c='b', label='L1')
+            end_point = v[i, :] + INF_VAL * (xk[L1_idx, :] - v[i, :]) / np.linalg.norm(xk[L1_idx, :] - v[i, :])
+            plt.plot([v[i, 0], end_point[0]], [v[i, 1], end_point[1]], 'b--')
+            plt.plot(v[i, 0], v[i, 1], marker='x', c='r', ms=12, label='v{}'.format(i))
+            axes.set_xlim(xlim)
+            axes.set_ylim(ylim)
+
+        def point_left_of_line(point, line_head, line_tail):
+            return is_ccw(line_head, line_tail, point)
+
+        def point_right_of_line(point, line_head, line_tail):
+            return is_cw(line_head, line_tail, point)
+
+        pol = shapely.ops.orient(self._polygon)
+        v = np.asarray(pol.exterior.coords)[:-1, :]
+
+        # Remove points in line
+        v_idcs = np.arange(v.shape[0])
+        for i in range(v.shape[0]):
+            if is_collinear(v[v_idcs[i - 2], :], v[v_idcs[i - 1], :], v[v_idcs[i], :]):
+                v_idcs[i - 1] = v_idcs[i - 2]
+        v_idcs = np.unique(v_idcs)
+        v = v[v_idcs, :]
+        if is_collinear(v[-2, :], v[-1, :], v[0, :]):
+            v = v[:-1]
+
+        N = v.shape[0]
+        for i in range(N):
+            # Order v to have v[:, 0] as reflex vertex
+            if is_reflex(i):
+                v = np.vstack((v[i:, :], v[:i, :]))
+                break
+
+        INF_VAL = 300.
+        # Initial step
+        F1 = v[0, :] + INF_VAL * (v[0, :] - v[1, :]) / np.linalg.norm(v[0, :] - v[1, :])
+        L1 = v[0, :] + INF_VAL * (v[0, :] - v[-1, :]) / np.linalg.norm(v[0, :] - v[-1, :])
+        xk = np.vstack((F1, v[0, :], L1))
+        F1_idx = 0  # Index of point F1 in xk
+        L1_idx = 2  # Index of point L1 in xk
+        xk_bounded = False
+
+        if verbose:
+            print("------")
+            print(0)
+            print("F1:", end=" ")
+            print(F1)
+            print("L1:", end=" ")
+            print(L1)
+        if debug:
+            draw(xk, F1_idx, L1_idx, 0, xk_bounded)
+            plt.show()
+
+        for i in range(1, N):
+
+            if verbose:
+                print("------")
+                print(i)
+                print("F1 index: {}".format(F1_idx))
+                print("L1 index: {}".format(L1_idx))
+
+            L1 = xk[L1_idx, :]
+            F1 = xk[F1_idx, :]
+            vi = v[i, :]
+            vi_1 = v[(i + 1) % N, :]
+
+            # (1) vi is reflex
+            if is_reflex(i):
+                # (1.2) F1 lies to the left of line ->(vi->vi+1)->vi+1
+                start_point = vi - INF_VAL * (vi_1 - vi) / np.linalg.norm(vi_1 - vi)
+                if point_left_of_line(F1, start_point, vi_1):
+                    case = 2
+                    if debug:
+                        draw(xk, F1_idx, L1_idx, i, xk_bounded)
+                        plt.plot([start_point[0], vi_1[0]], [start_point[1], vi_1[1]], marker='*', color='y',
+                                 label="->(v{}->v{})->v{}".format(i, i + 1, i + 1))
+                        plt.title("F1 lies to the left of line ->(v{}->v{})->v{}".format(i, i + 1, i + 1))
+                        plt.show()
+                    if verbose:
+                        print("(1.2) - F1 ({},{}) lies to the left of line ->(v{}->v{})->v{}".format(F1[0], F1[0], i,
+                                                                                                     i + 1, i + 1))
+                # (1.1) F1 lies on or to the right of line ->(vi->vi+1)->vi+1
+                else:
+                    case = 1
+                    if verbose:
+                        print(
+                            "(1.1) - F1 ({},{}) lies on or to the right of line ->(v{}->v{})->v{}".format(F1[0], F1[0],
+                                                                                                          i, i + 1,
+                                                                                                          i + 1))
+                        print("Scan xk ccw from F1 until we reach edge intersecting line ->(v{}->v{})->v{}".format(i,
+                                                                                                                   i + 1,
+                                                                                                                   i + 1))
+                    # Scan xk ccw from F1 to L1 until we reach edge intersecting line ->(vi->vi+1)->vi+1
+                    w1 = None
+                    idx_offsets = range(1, xk.shape[0] + 1)
+                    for off in idx_offsets:
+                        t = (F1_idx + off) % xk.shape[0]
+                        if not xk_bounded and t == 0:
+                            break
+                        # Get intersection w1 of current edge and line ->(vi->vi+1)->vi+1
+                        wt_prev = xk[t - 1, :]
+                        wt = xk[t, :]
+                        w1 = get_intersection(wt_prev, wt, start_point, vi_1)
+                        if debug:
+                            draw(xk, F1_idx, L1_idx, i, xk_bounded)
+                            col = 'r' if w1 is None else 'g'
+                            plt.plot([wt_prev[0], wt[0]], [wt_prev[1], wt[1]], marker='*', color=col, label="edge")
+                            plt.plot([start_point[0], vi_1[0]], [start_point[1], vi_1[1]], marker='*', color='y',
+                                     label="->(v{}->v{})->v{}".format(i, i + 1, i + 1))
+                            plt.title(
+                                "Scan xk ccw from F1 until we reach edge intersecting line ->(v{}->v{})->v{}".format(i,
+                                                                                                                     i + 1,
+                                                                                                                     i + 1))
+                        if w1 is not None:
+                            if debug:
+                                plt.plot(w1[0], w1[1], 's', label="w1", color='g')
+                                plt.legend()
+                                plt.show()
+                            break
+                        if debug:
+                            plt.legend()
+                            plt.show()
+
+                        if t == L1_idx:
+                            break
+                    # If no intersecting line is reached no kernel exists
+                    if w1 is None:
+                        # if debug:
+                        draw(xk, F1_idx, L1_idx, i, xk_bounded)
+                        plt.title('No kernel found.. Polygon not starshaped.')
+                        plt.legend()
+                        plt.show()
+                        return False
+                    if verbose:
+                        print("Found intersection ({},{}) at line ->(v{}->v{})->v{}".format(w1[0], w1[1], i, i + 1,
+                                                                                            i + 1))
+                        print("Scan xk cw from F1 until we reach edge intersecting line ->(v{}->v{})->v{}".format(i,
+                                                                                                                  i + 1,
+                                                                                                                  i + 1))
+                    # Scan xk cw from F1 until we reach edge intersecting line ->(vi->vi+1)->vi+1
+                    w2 = None
+                    idcs_list = np.flip(np.roll(np.arange(xk.shape[0]), -F1_idx - 1)) if xk_bounded else range(F1_idx,
+                                                                                                               0, -1)
+                    # for s in range(F1_idx, 0, -1):
+                    for s in idcs_list:
+                        ws = xk[s, :]
+                        ws_prev = xk[s - 1, :]
+                        # Get intersection w2 of edge and line ->(vi->vi+1)->vi+1
+                        w2 = get_intersection(ws_prev, ws, start_point, vi_1)
+
+                        if debug:
+                            draw(xk, F1_idx, L1_idx, i, xk_bounded)
+                            plt.plot(w1[0], w1[1], 's', label="w1", color='g')
+                            col = 'r' if w2 is None else 'g'
+                            plt.plot([ws_prev[0], ws[0]], [ws_prev[1], ws[1]], marker='*', color=col, label="edge")
+                            plt.plot([start_point[0], vi_1[0]], [start_point[1], vi_1[1]], marker='*', color='y',
+                                     label="->(v{}->v{})->v{}".format(i, i + 1, i + 1))
+                            plt.title(
+                                "Scan xk cw from F1 until we reach edge intersecting line ->(v{}->v{})->v{}".format(i,
+                                                                                                                    i + 1,
+                                                                                                                    i + 1))
+
+                        if w2 is not None:
+                            if xk_bounded and s > t:
+                                alpha = xk[xk.shape[0]:, :]  # Empty array
+                                beta = xk[t:s, :]
+                                L1_idx = L1_idx - t + 2
+                            else:
+                                alpha = xk[:s, :]
+                                beta = xk[t:, :]
+                                L1_idx = L1_idx - t + s + 2
+                            if debug:
+                                print("xk: ", end=" ")
+                                print(xk)
+                                print("alpha: ", end=" ")
+                                print(alpha)
+                                print("beta: ", end=" ")
+                                print(beta)
+                                print("w1: ", end=" ")
+                                print(w1)
+                                print("w2: ", end=" ")
+                                print(w2)
+                                plt.plot(w1[0], w1[1], 's', label="w1", color='k')
+                                plt.plot(w2[0], w2[1], 's', label="w2", color='g')
+                                plt.plot(alpha[:, 0], alpha[:, 1], 'r--', label="alpha")
+                                plt.plot(beta[:, 0], beta[:, 1], 'k--', label="beta")
+                                plt.legend()
+                                plt.show()
+                            xk = np.vstack((alpha, w2, w1, beta))
+                            w1_idx = alpha.shape[0] + 1
+                            w2_idx = alpha.shape[0]
+                            ####### F1 index reassignment should not be necessary for this case
+                            F1_idx = w2_idx
+                            # L1_idx = L1_idx - t + s + 2
+
+                            if verbose:
+                                print("Update 1")
+                                print("Found intersection ({},{}) at line ->(v{}->v{})->v{}".format(w2[0], w2[1], i,
+                                                                                                    i + 1, i + 1))
+                            break
+                        if debug:
+                            plt.legend()
+                            plt.show()
+                    # If no intersecting line is reached
+                    if w2 is None:
+                        # Test if xk+1 is bounded
+                        # If slope ->(vi->vi+1)->vi+1 is comprised between the slopes of initial and final half lines of xk,
+                        if (orientation_val(xk[-2, :], xk[-1, :], start_point) * orientation_val(vi_1, start_point,
+                                                                                                 xk[0, :])) > 0:
+                            beta = xk[t:, :]
+                            if debug:
+                                draw(xk, F1_idx, L1_idx, i, xk_bounded)
+                                plt.plot(w1[0], w1[1], 's', label="w1", color='g')
+                                plt.plot(xk[:2, 0], xk[:2, 1], '--c', label="initial half line")
+                                plt.plot(xk[-2:, 0], xk[-2:, 1], '--m', label="final half line")
+                                plt.plot([start_point[0], vi_1[0]], [start_point[1], vi_1[1]], marker='*', color='y',
+                                         label="->(v{}->v{})->v{}".format(i, i + 1, i + 1))
+                                plt.title(
+                                    "->(v{}->v{})->v{} between initial and finial half lines of xk".format(
+                                        i, i + 1, i + 1))
+                                plt.plot(beta[:, 0], beta[:, 1], 'k--', label="beta")
+                                plt.legend()
+                                plt.show()
+                            if verbose:
+                                print("Update 2 - xk still unbounded")
+                            # then xk+1= ->(vi->vi+1)->w1->beta is also unbounded.
+                            xk = np.vstack((start_point, w1, beta))
+                            w1_idx = 1
+                            # xk_bounded = False
+                            F1_idx = 0
+                            L1_idx -= t - 1
+
+
+                        else:
+                            # otherwise scan xk cw from xk[-1,:] until we reach edge intersecting line ->(vi->vi+1)->vi+1
+                            if verbose:
+                                print(
+                                    "Scan xk cw from end until we reach edge intersecting line ->(v{}->v{})->v{}".format(
+                                        i, i + 1, i + 1))
+                            w2 = None
+                            if xk_bounded:
+                                r = xk.shape[0]
+                                w2 = get_intersection(xk[0, :], xk[r - 1, :], start_point, vi_1)
+                                if debug:
+                                    draw(xk, F1_idx, L1_idx, i, xk_bounded)
+                                    plt.plot(w1[0], w1[1], 's', label="w1", color='g')
+                                    col = 'r' if w2 is None else 'g'
+                                    plt.plot([xk[r - 1, 0], xk[0, 0]], [xk[r - 1, 1], xk[0, 1]], marker='*', color=col,
+                                             label="edge")
+                                    plt.plot([start_point[0], vi_1[0]], [start_point[1], vi_1[1]], marker='*',
+                                             color='y',
+                                             label="->(v{}->v{})->v{}".format(i, i + 1, i + 1))
+                                    plt.title(
+                                        "Scan xk cw from xk[-1, :] until we reach edge intersecting line ->(v{}->v{})->v{}".format(
+                                            i, i + 1, i + 1))
+                                    if w2 is not None:
+                                        delta = xk[t:r, :]
+                                        if debug:
+                                            print(t, r)
+                                            plt.plot(delta[:, 0], delta[:, 1], 'k--', label="delta")
+                                            plt.plot(w1[0], w1[1], 's', label="w1", color='k')
+                                            plt.plot(w2[0], w2[1], 's', label="w2", color='g')
+                                            plt.legend()
+                                            plt.show()
+                                    if debug:
+                                        plt.legend()
+                                        plt.show()
+                            if w2 is None:
+                                for r in range(xk.shape[0] - 1, 0, -1):
+                                    # Get intersection w2 of edge and line ->(vi->vi+1)->vi+1
+                                    w2 = get_intersection(xk[r, :], xk[r - 1, :], start_point, vi_1)
+                                    if debug:
+                                        draw(xk, F1_idx, L1_idx, i, xk_bounded)
+                                        plt.plot(w1[0], w1[1], 's', label="w1", color='g')
+                                        col = 'r' if w2 is None else 'g'
+                                        plt.plot([xk[r - 1, 0], xk[r, 0]], [xk[r - 1, 1], xk[r, 1]], marker='*',
+                                                 color=col, label="edge")
+                                        plt.plot([start_point[0], vi_1[0]], [start_point[1], vi_1[1]], marker='*',
+                                                 color='y', label="->(v{}->v{})->v{}".format(i, i + 1, i + 1))
+                                        plt.title(
+                                            "Scan xk cw from xk[-1, :] until we reach edge intersecting line ->(v{}->v{})->v{}".format(
+                                                i, i + 1, i + 1))
+                                    if w2 is not None:
+                                        delta = xk[t:r, :]
+                                        if debug:
+                                            print(t, r)
+                                            plt.plot(delta[:, 0], delta[:, 1], 'k--', label="delta")
+                                            plt.plot(w1[0], w1[1], 's', label="w1", color='k')
+                                            plt.plot(w2[0], w2[1], 's', label="w2", color='g')
+                                            plt.legend()
+                                            plt.show()
+                                        break
+                                    if debug:
+                                        plt.legend()
+                                        plt.show()
+                            if verbose:
+                                print("Update 3")
+                                print("Found intersection ({},{}) at line ->(v{}->v{})->v{}".format(w2[0], w2[1], i,
+                                                                                                    i + 1, i + 1))
+                            # Set xk as delta-w2-w1
+                            xk = np.vstack((delta, w2, w1))
+                            w1_idx = delta.shape[0] + 1
+                            w2_idx = delta.shape[0]
+                            xk_bounded = True
+
+                            F1_idx = 0
+                            L1_idx = min(L1_idx - t, xk.shape[0] - 1)
+
+                # F1 update
+                if case == 1:
+                    # If ->(vi->vi+1)->vi+1 has just one intersection with xk F1 = startpoint
+                    if w2 is None:
+                        F1_idx = 0
+                    # Otherwise F1 = w2
+                    else:
+                        F1_idx = w2_idx
+                if case == 2:
+                    # Scan xk ccw from F1 until find vertex wt s.t. wt+1 lies to the
+                    # right of vi+1->(vi+1->wt)->. Let F1 = wt.
+                    idx_offsets = range(xk.shape[0])
+                    for off in idx_offsets:
+                        t = (F1_idx + off) % xk.shape[0]
+                        w_next = xk[(t + 1) % xk.shape[0], :]
+                        line_end_point = vi_1 + INF_VAL * (xk[t, :] - vi_1)
+                        if point_right_of_line(w_next, vi_1, line_end_point):
+                            F1_idx = t
+                            break
+
+                # Check update of previous L1 index for new xk
+                # if not np.isclose(np.linalg.norm(L1 - xk[L1_idx, :]), 0):
+                # print("BAD L1 update [{},{}] -> [{},{}]".format(xk[L1_idx, 0], xk[L1_idx, 1], L1[0], L1[1]))
+                # plt.figure(2)
+                # draw(xk, F1_idx, L1_idx, i, xk_bounded)
+                # plt.show()
+
+                # L1 update
+                # scan xk ccw from L1 until find vertex wu s.t. wu+1 lies to the
+                # left of vi+1->(vi+1->wu)->. Let L1 = wu.
+                idx_offsets = range(xk.shape[0])
+                for off in idx_offsets:
+                    u = (L1_idx + off) % xk.shape[0]
+                    w_next = xk[(u + 1) % xk.shape[0], :]
+                    line_end_point = vi_1 + INF_VAL * (xk[u, :] - vi_1)
+                    if point_left_of_line(w_next, vi_1, line_end_point):
+                        L1_idx = u
+                        break
+
+            else:
+                if verbose:
+                    print("(2)")
+                # Endpoint for line vi->(vi->vi+1)->
+                end_point = vi + INF_VAL * (vi_1 - vi) / np.linalg.norm(vi_1 - vi)
+
+                # (2.2) L1 lies to the left of line vi->(vi->vi+1)->
+                if point_left_of_line(L1, vi, end_point):
+                    case = 2
+                    if verbose:
+                        print("(2.2) - L1 ({},{}) lies to the left of line v{}->(v{}->v{})->".format(L1[0], L1[1], i, i,
+                                                                                                     i + 1))
+                    # xk stays the same
+
+                    if debug:
+                        draw(xk, F1_idx, L1_idx, i, xk_bounded)
+                        plt.plot([end_point[0], vi[0]], [end_point[1], vi[1]], marker='*', color='y',
+                                 label="v{}->(v{}->v{})->".format(i, i, i + 1))
+                        plt.title("L1 lies to the left of line v{}->(v{}->v{})->".format(i, i, i + 1))
+                        plt.show()
+
+                # (2.1) L1 lies on or to the right of line vi->(vi->vi+1)->
+                else:
+                    case = 1
+                    if verbose:
+                        print(
+                            "(2.1) - L1 ({},{}) lies on or to the right of line v{}->(v{}->v{})->".format(L1[0], L1[1],
+                                                                                                          i, i, i + 1))
+                        print(
+                            "Scan xk cw from L1 until we reach F1 or an edge intersecting line v{}->(v{}->v{})->".format(
+                                i, i, i + 1))
+                    # Scan xk cw from L1 until we reach F1 or an edge intersecting line vi->(vi->vi+1)->
+                    idx_offsets = range(xk.shape[0])
+                    w1 = None
+                    for off in idx_offsets:
+                        t = L1_idx - off
+                        # If circular
+                        if t < 0:
+                            t += xk.shape[0]
+                        if t == F1_idx:
+                            break
+                        # Get intersection w1 of edge and line vi->(vi->vi+1)->
+                        w1 = get_intersection(xk[t, :], xk[t - 1, :], vi, end_point)
+                        if debug:
+                            draw(xk, F1_idx, L1_idx, i, xk_bounded)
+                            col = 'r' if w1 is None else 'g'
+                            plt.plot([xk[t - 1, 0], xk[t, 0]], [xk[t - 1, 1], xk[t, 1]], marker='*', color=col,
+                                     label="edge")
+                            plt.plot([end_point[0], v[i, 0]], [end_point[1], v[i, 1]], marker='*', color='y',
+                                     label="v{}->(v{}->v{})->".format(i, i, i + 1))
+                            plt.title(
+                                "Scan xk cw from L1 until we reach F1 or an edge intersecting line v{}->(v{}->v{})->".format(
+                                    i,
+                                    i,
+                                    i + 1))
+                        if w1 is not None:
+                            if debug:
+                                plt.plot(w1[0], w1[1], 's', label="w1", color='g')
+                                plt.legend()
+                                plt.show()
+                            break
+                        if debug:
+                            plt.legend()
+                            plt.show()
+                    # If no intersecting line is reached no kernel exists
+                    if w1 is None:
+                        # if debug:
+                        draw(xk, F1_idx, L1_idx, i, xk_bounded)
+                        plt.title('No kernel found. Polygon not starshaped.')
+                        plt.show()
+                        return False
+
+                    if verbose:
+                        print("Found intersection ({},{}) at line v{}->(v{}->v{})-> for edge ({},{})-({},{})".format(
+                            w1[0], w1[1], i, i, i + 1, xk[t - 1, 0], xk[t - 1, 1], xk[t, 0], xk[t, 1]))
+                        print("Scan xk ccw from L1 until we reach edge intersecting line v{}->(v{}->v{})->".format(i,
+                                                                                                                   i,
+                                                                                                                   i + 1))
+                    # Scan xk ccw from L1 until we reach edge w2 intersecting line vi->(vi->vi+1)->
+                    w2 = None
+                    idx_offsets = range(1, xk.shape[0] + 1)
+                    for off in idx_offsets:
+                        s = (L1_idx + off) % xk.shape[0]
+                        if not xk_bounded and s == 0:
+                            break
+                        # Get intersection w2 of edge and line vi->(vi->vi+1)->
+                        w2 = get_intersection(xk[s - 1, :], xk[s, :], vi, end_point)
+                        if debug:
+                            draw(xk, F1_idx, L1_idx, i, xk_bounded)
+                            col = 'r' if w2 is None else 'g'
+                            plt.plot([xk[s - 1, 0], xk[s, 0]], [xk[s - 1, 1], xk[s, 1]], marker='*', color=col,
+                                     label="edge")
+                            plt.plot([end_point[0], v[i, 0]], [end_point[1], v[i, 1]], marker='*', color='y',
+                                     label="v{}->(v{}->v{})->".format(i, i, i + 1))
+                            plt.title(
+                                "Scan xk ccw from L1 until we reach edge intersecting line v{}->(v{}->v{})->".format(i,
+                                                                                                                     i,
+                                                                                                                     i + 1))
+                        if w2 is not None:
+                            if xk_bounded and t > s:
+                                alpha = xk[xk.shape[0]:, :]  # Empty array
+                                beta = xk[s:t, :]
+                            else:
+                                alpha = xk[:t, :]
+                                beta = xk[s:, :]
+                            if debug:
+                                print("xk: ", end=" ")
+                                print(xk)
+                                print("alpha: ", end=" ")
+                                print(alpha)
+                                print("beta: ", end=" ")
+                                print(beta)
+                                print("w1: ", end=" ")
+                                print(w1)
+                                print("w2: ", end=" ")
+                                print(w2)
+                                plt.plot(w1[0], w1[1], 's', label="w1", color='k')
+                                plt.plot(w2[0], w2[1], 's', label="w2", color='g')
+                                plt.plot(alpha[:, 0], alpha[:, 1], 'r--', label="alpha")
+                                plt.plot(beta[:, 0], beta[:, 1], 'k--', label="beta")
+                                plt.legend()
+                                plt.show()
+                            if verbose:
+                                print("Update 1")
+                                print(
+                                    "Found intersection ({},{}) at line v{}->(v{}->v{})-> for edge ({},{})-({},{})".format(
+                                        w2[0], w2[1], i, i, i + 1, xk[s, 0], xk[s, 1], xk[(s + 1) % xk.shape[0], 0],
+                                        xk[(s + 1) % xk.shape[0], 1]))
+
+                            xk = np.vstack((alpha, w1, w2, beta))
+                            w1_idx = alpha.shape[0]
+                            w2_idx = alpha.shape[0] + 1
+                            break
+                        if debug:
+                            plt.legend()
+                            plt.show()
+                    # If no intersecting line is reached
+                    if w2 is None:
+                        # Test if xk+1 is bounded
+                        # If slope vi->(vi->vi+1)-> is comprised between the slopes of initial and final half lines of xk,
+                        if not xk_bounded and ((orientation_val(xk[-2, :], xk[-1, :], end_point) * orientation_val(vi,
+                                                                                                                   end_point,
+                                                                                                                   xk[0,
+                                                                                                                   :])) > 0):
+                            alpha = xk[:t, :]
+                            if debug:
+                                draw(xk, F1_idx, L1_idx, i, xk_bounded)
+                                plt.plot(w1[0], w1[1], 's', label="w1", color='g')
+                                plt.plot(xk[:2, 0], xk[:2, 1], '--c', label="initial half line")
+                                plt.plot(xk[-2:, 0], xk[-2:, 1], '--m', label="final half line")
+                                plt.plot([end_point[0], v[i, 0]], [end_point[1], v[i, 1]], marker='*', color='y',
+                                         label="v{}->(v{}->v{})->".format(i, i, i + 1))
+                                plt.title(
+                                    "v{}->(v{}->v{})-> between initial and final half lines of xk".format(
+                                        i, i, i + 1))
+                                plt.plot(alpha[:, 0], alpha[:, 1], 'r--', label="alpha")
+                                plt.legend()
+                                plt.show()
+                            if verbose:
+                                print("Update 2 - xk still unbounded")
+                            # then xk+1= alpha->w1->(vi->vi+1)-> is also unbounded.
+                            xk = np.vstack((alpha, w1, end_point))
+                            w1_idx = alpha.shape[0]
+                            L1_idx = xk.shape[0] - 1
+                            F1_idx = 0
+                        else:
+                            if verbose:
+                                print(
+                                    "Scan xk ccw from start until we reach edge intersecting line v{}->(v{}->v{})->".format(
+                                        i,
+                                        i,
+                                        i + 1))
+                            # otherwise scan xk ccw from xk[0,:] until we reach edge intersecting line vi->(vi->vi+1)->
+                            w2 = None
+                            for r in range(1, xk.shape[0] + 1):
+                                # Get intersection w2 of edge and line vi->(vi->vi+1)->
+                                w2 = get_intersection(xk[r - 1, :], xk[r % xk.shape[0], :], vi, end_point)
+                                if debug:
+                                    draw(xk, F1_idx, L1_idx, i, xk_bounded)
+                                    col = 'r' if w2 is None else 'g'
+                                    plt.plot([xk[r - 1, 0], xk[r % xk.shape[0], 0]],
+                                             [xk[r - 1, 1], xk[r % xk.shape[0], 1]], marker='*', color=col,
+                                             label="edge")
+                                    plt.plot([end_point[0], v[i, 0]], [end_point[1], v[i, 1]], marker='*', color='y',
+                                             label="v{}->(v{}->v{})->".format(i, i, i + 1))
+                                    plt.title(
+                                        "Scan xk ccw from xk[0,:] until we reach edge intersecting line v{}->(v{}->v{})->".format(
+                                            i,
+                                            i,
+                                            i + 1))
+                                if w2 is not None:
+                                    delta = xk[r:t, :]
+                                    if debug:
+                                        plt.plot(w2[0], w2[1], 's', label="w2", color='g')
+                                        plt.legend()
+                                        plt.show()
+                                    break
+                                if debug:
+                                    plt.legend()
+                                    plt.show()
+                            if verbose:
+                                print("Update 3")
+                                print("Found intersection ({},{}) at line v{}->(v{}->v{})->".format(w2[0], w2[1], i, i,
+                                                                                                    i + 1))
+                            # Set xk as delta-w1-vi-vi+1-w2
+                            xk = np.vstack((delta, w1, w2))
+                            w1_idx = delta.shape[0]
+                            w2_idx = delta.shape[0] + 1
+                            xk_bounded = True
+                            F1_idx = 0
+                            L1_idx = min(L1_idx - t, xk.shape[0] - 1)
+
+                # F1 update
+
+                # If vi+1 in vi->(vi->vi+1)->w1,
+                if case == 2 or is_between(vi, vi_1, w1):
+                    # scan xk ccw from F1 until find vertex wt s.t. wt+1 lies to the
+                    # right of vi+1->(vi+1->wt)->. Let F1 = wt.
+                    idx_offsets = range(xk.shape[0])
+                    for off in idx_offsets:
+                        t = (F1_idx + off) % xk.shape[0]
+                        w_next = xk[(t + 1) % xk.shape[0], :]
+                        line_end_point = vi_1 + INF_VAL * (xk[t, :] - vi_1)
+                        if debug:
+                            draw(xk, F1_idx, L1_idx, i, xk_bounded)
+                            plt.plot([vi_1[0], line_end_point[0]], [vi_1[1], line_end_point[1]], '--',
+                                     label='v{}->(v{}->wt)'.format(i + 1, i + 1))
+                            c = 'g' if point_right_of_line(w_next, vi_1, line_end_point) else 'r'
+                            plt.plot(w_next[0], w_next[1], color=c, marker='s', label='w_t+1')
+                            plt.title(
+                                "Update F1\n scan xk ccw from F1 until find vertex wt s.t. wt+1 lies to the right of v{}->(v{}->wt)->.".format(
+                                    i + 1, i + 1))
+                            plt.legend()
+                            plt.show()
+                        if point_right_of_line(w_next, vi_1, line_end_point):
+                            F1_idx = t
+                            break
+                else:
+                    F1_idx = w1_idx
+
+                # L1 update
+                if case == 1:
+                    if w2 is not None:
+                        # If vi+1 in vi->(vi->vi+1)->w2
+                        if is_between(vi, vi_1, w2):
+                            L1_idx = w2_idx
+                        else:
+                            # scan xk ccw from w2 until find vertex wu s.t. wu+1 lies to the
+                            # left of vi+1->(vi+1->wu)->. Let L1 = wu.
+                            idx_offsets = range(xk.shape[0])
+                            for off in idx_offsets:
+                                u = (w2_idx + off) % xk.shape[0]
+                                w_next = xk[(u + 1) % xk.shape[0], :]
+                                line_end_point = vi_1 + INF_VAL * (xk[u, :] - vi_1)
+                                if debug:
+                                    draw(xk, F1_idx, L1_idx, i, xk_bounded)
+                                    plt.plot([vi_1[0], line_end_point[0]], [vi_1[1], line_end_point[1]], '--',
+                                             label='v{}->(v{}->wt)'.format(i + 1, i + 1))
+                                    c = 'g' if point_left_of_line(w_next, vi_1, line_end_point) else 'r'
+                                    plt.plot(w_next[0], w_next[1], color=c, marker='s', label='w_t+1')
+                                    plt.title(
+                                        "Update L1\n scan xk ccw from w2 until find vertex wu s.t. wu+1 lies to the left of v{}->(v{}->wt)->.".format(
+                                            i + 1, i + 1))
+                                    plt.legend()
+                                    plt.show()
+                                if point_left_of_line(w_next, vi_1, line_end_point):
+                                    L1_idx = u
+                                    break
+
+                    # (2.1.2) line vi->(vi->vi+1)-> intersects xk just in w1
+                    else:
+                        L1_idx = xk.shape[0] - 1
+                if case == 2:
+                    # print("(2.1.2)")
+                    if xk_bounded:
+                        # scan xk ccw from w2 until find vertex wu s.t. wu+1 lies to the
+                        # left of vi+1->(vi+1->wu)->. Let L1 = wu.
+                        idcs_list = np.roll(np.arange(xk.shape[0]), -L1_idx) if xk_bounded else range(L1_idx,
+                                                                                                      xk.shape[0] - 1)
+                        for u in idcs_list:
+                            w_next = xk[(u + 1) % xk.shape[0], :]
+                            line_end_point = vi_1 + INF_VAL * (xk[u, :] - vi_1)
+                            if debug:
+                                draw(xk, F1_idx, L1_idx, i, xk_bounded)
+                                plt.plot([vi_1[0], line_end_point[0]], [vi_1[1], line_end_point[1]], '--',
+                                         label='v{}->(v{}->wt)'.format(i + 1, i + 1))
+                                c = 'g' if point_left_of_line(w_next, vi_1, line_end_point) else 'r'
+                                plt.plot(w_next[0], w_next[1], color=c, marker='s', label='w_t+1')
+                                plt.title(
+                                    "Update L1\n scan xk ccw from w2 until find vertex wu s.t. wu+1 lies to the left of v{}->(v{}->wt)->.".format(
+                                        i + 1, i + 1))
+                                plt.legend()
+                                plt.show()
+                            if point_left_of_line(w_next, vi_1, line_end_point):
+                                L1_idx = u
+                                break
+
+            if verbose:
+                print("F1:", end=" ")
+                print(F1)
+                print("L1:", end=" ")
+                print(L1)
+                print("xk:", end=" ")
+                print(xk)
+        if debug or verbose:
+            draw(xk, F1_idx, L1_idx, 0, xk_bounded)
+            plt.show()
+        _, idx = np.unique(xk, axis=0, return_index=True)
+        xk = xk[np.sort(idx), :]
+        self._kernel = shapely.geometry.Polygon(xk)
+
+
diff --git a/python/ur_simple_control/path_generation/starworlds/obstacles/starshaped_primitive_combination.py b/python/ur_simple_control/path_generation/starworlds/obstacles/starshaped_primitive_combination.py
new file mode 100644
index 0000000000000000000000000000000000000000..6fdf2cd7ba907711261851bd22c60a6ab5d1b95d
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/obstacles/starshaped_primitive_combination.py
@@ -0,0 +1,226 @@
+import shapely
+import numpy as np
+from obstacles import Frame, StarshapedObstacle, StarshapedPolygon
+from utils import is_ccw, is_cw, draw_shapely_polygon
+import matplotlib.pyplot as plt
+
+# Note: Local == Global frame
+class StarshapedPrimitiveCombination(StarshapedObstacle):
+
+    def __init__(self, obstacle_cluster, hull_cluster, xr, **kwargs):
+        self._obstacle_cluster = obstacle_cluster
+        self._hull_cluster = hull_cluster
+        super().__init__(xr=xr, **kwargs)
+        self.vertices = None
+        self.circular_vertices = None
+        self.vertex_angles = None
+
+    def obstacle_cluster(self):
+        return self._obstacle_cluster
+
+    def hull_cluster(self):
+        return self._hull_cluster
+
+    def dilated_obstacle(self, padding, id="new", name=None):
+        pass
+
+    def point_location(self, x, input_frame=Frame.GLOBAL):
+        locs = [obs.point_location(x, input_frame=Frame.GLOBAL) for obs in self._obstacle_cluster] + \
+               [self._hull_cluster_point_location(x)]
+        if any([l < 0 for l in locs]):
+            # Interior point
+            return -1
+        if any([l == 0 for l in locs]):
+            # Boundary point
+            return 0
+        # Exterior point
+        return 1
+
+    def line_intersection(self, line, input_frame=Frame.GLOBAL, output_frame=Frame.GLOBAL):
+        intersection_points = []
+        for o in self._obstacle_cluster:
+            intersection_points += o.line_intersection(line, Frame.GLOBAL, Frame.GLOBAL)
+        intersection_points += self._hull_cluster_line_intersections(line)
+        return intersection_points
+
+    # TODO: Fix if needed. Currently not considering hull.
+    def tangent_points(self, x, input_frame=Frame.GLOBAL, output_frame=Frame.GLOBAL):
+        tp, tp_candidates = [], []
+        for obs in self._obstacle_cluster:
+            tp_candidates += obs.tangent_points(x, Frame.GLOBAL, Frame.GLOBAL)
+        for i in range(len(tp_candidates)):
+            if all([is_ccw(x, tp_candidates[i], tp_candidates[j]) for j in range(len(tp_candidates)) if j is not i]) or \
+                    all([is_cw(x, tp_candidates[i], tp_candidates[j]) for j in range(len(tp_candidates)) if j is not i]):
+                tp += [tp_candidates[i]]
+        return tp
+
+    def _compute_kernel(self):
+        self._kernel = StarshapedPolygon(self.polygon(), xr=self.xr(), id="temp").kernel()
+
+    def _check_convexity(self):
+        self._is_convex = StarshapedPolygon(self.polygon(), xr=self.xr(), id="temp").is_convex()
+
+    def boundary_mapping(self, x, input_frame=Frame.GLOBAL, output_frame=Frame.GLOBAL):
+        # intersection_points = [p for ps in self.line_intersection([self._xr, self._xr+10*(x-self._xr)]) for p in ps]
+        intersection_points = self.line_intersection([self._xr, self._xr+10*(x-self._xr)])
+        if not intersection_points:
+            return None
+        dist_intersection_points = [np.linalg.norm(ip - self._xr) for ip in intersection_points]
+        return intersection_points[np.argmax(dist_intersection_points)]
+
+    def vel_intertial_frame(self, x):
+        boundary_obs_idx = 0
+        max_dist = -1
+        for i, ps in enumerate(self.line_intersection([self._xr, self._xr+10*(x-self._xr)])):
+            o_intersection_dist = max([np.linalg.norm(p-self._xr) for p in ps] + [-1])
+            if o_intersection_dist > max_dist:
+                boundary_obs_idx = i
+                max_dist = o_intersection_dist
+        if boundary_obs_idx >= len(self._obstacle_cluster):
+            boundary_obs_idx -= len(self._obstacle_cluster)
+        return self._obstacle_cluster[boundary_obs_idx].vel_intertial_frame(x)
+
+    def normal(self, x, input_frame=Frame.GLOBAL, output_frame=Frame.GLOBAL, x_is_boundary=False, type='weigthed_polygon_approx'):
+        if type == 'sub_normal':
+            boundary_obs_idx = 0
+            max_dist = -1
+            line = [self._xr, self._xr+10*(x-self._xr)]
+            for i, o in enumerate(self._obstacle_cluster):
+                intersection_points = o.line_intersection(line, Frame.GLOBAL, Frame.GLOBAL)
+                for p in intersection_points:
+                    p_dist = np.linalg.norm(p - self._xr)
+                    if p_dist > max_dist:
+                        max_dist = p_dist
+                        boundary_obs_idx = i
+
+            hull_ip = self._hull_cluster_line_intersections(line)
+            if hull_ip:
+                for p in hull_ip:
+                    p_dist = np.linalg.norm(p - self._xr)
+                    if p_dist > max_dist:
+                        max_dist = p_dist
+                        boundary_obs_idx = len(self._obstacle_cluster)
+            if boundary_obs_idx < len(self._obstacle_cluster):
+                return self._obstacle_cluster[boundary_obs_idx].normal(x, input_frame=Frame.GLOBAL, output_frame=Frame.GLOBAL)
+            else:
+                boundary_obs_idx -= len(self._obstacle_cluster)
+                hull_vertices = np.array(self._hull_cluster.exterior.coords[:-1])
+                vertex_angles = np.array([np.arctan2(v[1] - self._xr[1], v[0] - self._xr[0]) for v in hull_vertices]).flatten()
+                idcs = np.argsort(vertex_angles)
+                vertex_angles = vertex_angles[idcs]
+                hull_vertices = hull_vertices[idcs, :]
+                vertex_angles = np.hstack((vertex_angles, vertex_angles[0] + 2 * np.pi))
+                hull_vertices = np.vstack((hull_vertices, hull_vertices[0, :]))
+
+                angle = np.arctan2(x[1] - self._xr[1], x[0] - self._xr[0])
+                v_idx = np.argmax(vertex_angles > angle)
+                # Adjust for circular self.vertices (self.vertices[0] == self.vertices[-1])
+                if v_idx == 0:
+                    v_idx = -1
+                n = np.array([hull_vertices[v_idx, 1] - hull_vertices[v_idx - 1, 1],
+                              hull_vertices[v_idx - 1, 0] - hull_vertices[v_idx, 0]])
+                n /= np.linalg.norm(n)
+                return n
+        elif type == 'polygon_approx' or type == 'weigthed_polygon_approx':
+            if self.vertices is None:
+                self._update_vertex_angles()
+            angle = np.arctan2(x[1] - self._xr[1], x[0] - self._xr[0])
+            v_idx = np.argmax(self.vertex_angles > angle)
+
+            if v_idx == self.vertices.shape[0]:
+                v_idx = 0
+
+            if type == 'polygon_approx':
+                n = np.array([self.vertices[v_idx, 1] - self.vertices[v_idx - 1, 1],
+                              self.vertices[v_idx - 1, 0] - self.vertices[v_idx, 0]])
+            else:
+                edge_neighbors = [(self.vertices[(v_idx - 2 + i) % self.vertices.shape[0]],
+                                   self.vertices[(v_idx - 1 + i) % self.vertices.shape[0]]) for i in range(3)]
+                edge_neighbors_normal = np.array([[e[1][1] - e[0][1],
+                                                   e[0][0] - e[1][0]] for e in edge_neighbors])
+                edge_closest = [shapely.ops.nearest_points(shapely.geometry.LineString(e),
+                                                           shapely.geometry.Point(x))[0].coords[0] for e in
+                                edge_neighbors]
+
+                dist = [np.linalg.norm(np.array(e) - x) for e in edge_closest]
+                w = np.array([1 / (d + 1e-10) for d in dist])
+                w /= sum(w)
+                n = edge_neighbors_normal.T.dot(w)
+
+            n /= np.linalg.norm(n)
+            return n
+
+    def set_xr(self, xr, input_frame=Frame.OBSTACLE, safe_set=False):
+        super().set_xr(xr, input_frame, safe_set)
+        self._update_vertex_angles()
+        
+    def init_plot(self, ax=None, show_reference=True, show_name=False, **kwargs):
+        if ax is None:
+            _, ax = plt.subplots(subplot_kw={'aspect': 'equal'})
+        if "fc" not in kwargs and "facecolor" not in kwargs:
+            kwargs["fc"] = 'lightgrey'
+
+        line_handles = []
+
+        lh, _ = draw_shapely_polygon(self.polygon(), ax=ax, **kwargs)
+        line_handles += lh
+
+        # Reference point
+        line_handles += ax.plot(*self.xr(), '+', color='k') if show_reference else [None]
+        # Name
+        line_handles += [ax.text(*self.xr(), self._name)] if show_name else [None]
+        return line_handles, ax
+
+    def update_plot(self, line_handles):
+        pass
+
+    def draw(self, ax=None, show_reference=True, show_name=False, **kwargs):
+        line_handles, ax = self.init_plot(ax, show_reference, show_name, **kwargs)
+        self.update_plot(line_handles)
+        return line_handles, ax
+
+    def _hull_cluster_point_location(self, x):
+        if self._hull_cluster is None:
+            return 1
+        x_sh = shapely.geometry.Point(x)
+        if self._hull_cluster.contains(x_sh):
+            return -1
+        if self._hull_cluster.disjoint(x_sh):
+            return 1
+        return 0
+
+    def _hull_cluster_line_intersections(self, line):
+        if self._hull_cluster is None:
+            return []
+        line_sh = shapely.geometry.LineString(line)
+        intersection_points_shapely = line_sh.intersection(self._hull_cluster.exterior)
+        if intersection_points_shapely.is_empty:
+            return []
+        if intersection_points_shapely.geom_type == 'Point':
+            return [np.array([intersection_points_shapely.x, intersection_points_shapely.y])]
+        elif intersection_points_shapely.geom_type == 'MultiPoint':
+            return [np.array([p.x, p.y]) for p in intersection_points_shapely.geoms]
+        elif intersection_points_shapely.geom_type == 'LineString':
+            return [np.array([ip[0], ip[1]]) for ip in intersection_points_shapely.coords]
+        elif intersection_points_shapely.geom_type == 'MultiLineString':
+            return [np.array([l[0], l[1]]) for line in intersection_points_shapely.geoms for
+                                            l in line.coords]
+        else:
+            print("[_hull_cluster_line_intersections]: Shapely geom_type not covered!")
+            print(intersection_points_shapely)
+
+    def _update_vertex_angles(self):
+        self.vertices = np.array(self._polygon.exterior.coords[:-1])
+        self.circular_vertices = np.array(self._polygon.exterior.coords)
+        self.vertex_angles = np.arctan2(self.vertices[:, 1] - self._xr[1], self.vertices[:, 0] - self._xr[0])
+        idcs = np.argsort(self.vertex_angles)
+        self.vertex_angles = self.vertex_angles[idcs]
+        self.vertices = self.vertices[idcs, :]
+        self.circular_vertices = np.vstack((self.vertices, self.vertices[0, :]))
+        self.vertex_angles = np.hstack((self.vertex_angles, self.vertex_angles[0] + 2 * np.pi))
+
+    def _compute_polygon_representation(self):
+        obs_pol = [obs.polygon() for obs in self._obstacle_cluster]
+        if self._hull_cluster is not None:
+            obs_pol += [self._hull_cluster]
+        self._polygon = shapely.ops.unary_union(obs_pol)
diff --git a/python/ur_simple_control/path_generation/starworlds/requirements.txt b/python/ur_simple_control/path_generation/starworlds/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..3689a74ef0eb7796d52dc466d5aafaa6c624e28f
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/requirements.txt
@@ -0,0 +1,4 @@
+numpy
+scipy
+matplotlib
+shapely
\ No newline at end of file
diff --git a/python/ur_simple_control/path_generation/starworlds/setup.py b/python/ur_simple_control/path_generation/starworlds/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..67761811b674817cb9f0d2dea2db43c8dba03227
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/setup.py
@@ -0,0 +1,14 @@
+from setuptools import setup, find_packages
+
+setup(name='starworlds',
+      version='1.0',
+      packages=find_packages(),
+      install_requires=[
+          'pyyaml',
+          'numpy',
+          'scipy',
+          'matplotlib',
+          'shapely'
+      ]
+
+)
diff --git a/python/ur_simple_control/path_generation/starworlds/starshaped_hull/__init__.py b/python/ur_simple_control/path_generation/starworlds/starshaped_hull/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..45406b14bb497117f7b86c4941c8e4849f3160e8
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/starshaped_hull/__init__.py
@@ -0,0 +1,2 @@
+from .starshaped_hull import admissible_kernel, kernel_starshaped_hull
+from .cluster_and_starify import ObstacleCluster, get_intersection_clusters, cluster_and_starify, draw_clustering, draw_adm_ker, draw_star_hull
\ No newline at end of file
diff --git a/python/ur_simple_control/path_generation/starworlds/starshaped_hull/cluster_and_starify.py b/python/ur_simple_control/path_generation/starworlds/starshaped_hull/cluster_and_starify.py
new file mode 100644
index 0000000000000000000000000000000000000000..2e825e86581a3bbecb805d3ea1351a1a6a5c95ed
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/starshaped_hull/cluster_and_starify.py
@@ -0,0 +1,500 @@
+import shapely
+import numpy as np
+from obstacles import Frame, StarshapedPrimitiveCombination, Ellipse, StarshapedPolygon
+from utils import is_ccw, is_collinear, equilateral_triangle, Cone, tic, toc, draw_shapely_polygon
+from scipy.spatial import ConvexHull
+import starshaped_hull as sh
+import matplotlib.pyplot as plt
+
+
+class ObstacleCluster:
+    def __init__(self, obstacles):
+        self.name = '_'.join([str(o.id()) for o in obstacles])
+        self.obstacles = obstacles
+        self.cluster_obstacle = None if len(obstacles) > 1 else obstacles[0]
+        self.kernel_points = None
+        self.admissible_kernel = None
+        self._polygon = None
+        self._polygon_excluding_hull = None
+
+    def __str__(self):
+        return self.name
+
+    def polygon(self):
+        if self._polygon is None:
+            if self.cluster_obstacle is None:
+                print("[OBSTACLE CLUSTER]: WARNING, cluster_obstacle must be defined before accessing polygon.")
+            else:
+                self._polygon = self.cluster_obstacle.polygon()
+        return self._polygon
+
+    def polygon_excluding_hull(self):
+        if self._polygon_excluding_hull is None:
+            self._polygon_excluding_hull = shapely.ops.unary_union([o.polygon() for o in self.obstacles])
+        return self._polygon_excluding_hull
+
+    def draw(self, ax=None):
+        if self.cluster_obstacle is not None:
+            ax, _ = self.cluster_obstacle.draw(ax=ax, fc="green")
+        for obs in self.obstacles:
+            ax, _ = obs.draw(ax=ax)
+        return ax, _
+
+
+def get_intersection_clusters(clusters):
+    No = len(clusters)
+    intersection_idcs = []
+
+    # Use polygon approximations for intersection check
+    cluster_polygons = [cl.polygon() for cl in clusters]
+
+    # Find intersections
+    intersections_exist = False
+    for i in range(No):
+        intersection_idcs += [[i]]
+        for j in range(i + 1, No):
+            if cluster_polygons[i].intersects(cluster_polygons[j]):
+                intersection_idcs[i] += [j]
+                intersections_exist = True
+
+    if not intersections_exist:
+        return clusters, intersections_exist
+
+    # Cluster intersecting obstacles
+    for i in range(No - 1, 0, -1):
+        for j in range(i - 1, -1, -1):
+            found = False
+            for l_j in intersection_idcs[j]:
+                if l_j in intersection_idcs[i]:
+                    found = True
+                    break
+            if found:
+                intersection_idcs[j] = list(set(intersection_idcs[j] + intersection_idcs[i]))
+                intersection_idcs[i] = []
+                break
+
+    # Create obstacle clusters
+    cluster_obstacles = [cl.obstacles for cl in clusters]
+    new_clusters = []
+    for i in range(No):
+        if intersection_idcs[i]:
+            new_clusters += [ObstacleCluster([o for j in intersection_idcs[i] for o in cluster_obstacles[j]])]
+
+    return new_clusters, intersections_exist
+
+
+def compute_kernel_points(cl, x, xg, epsilon, cl_prev, workspace):
+    triangle_center_prev = np.mean(cl_prev.kernel_points, axis=0) if cl_prev else None
+
+    t0 = tic()
+    ts = {}
+    # Find triangle center selection set (TCSS)
+    tcss = cl.admissible_kernel
+    # If obstacle is in exterior of workspace limit TCSS to workspace exterior
+    if not cl.polygon_excluding_hull().within(workspace.polygon()):
+        tcss_tmp = tcss.difference(workspace.polygon())
+        tcss_tmp = tcss_tmp.intersection(cl.polygon_excluding_hull()) # NOTE: Added for exlcluding undesired extremas in other workspace exterior than close to obstacles
+        if tcss_tmp.area > 1e-6:
+            tcss = tcss_tmp
+    tcss_tmp = tcss
+    ts['ws check'] = toc(t0)
+    # Try to use intersection of all obstacle kernels in cluster if all starshaped
+    if all([o.is_starshaped() for o in cl.obstacles]):
+        for o in cl.obstacles:
+            tcss_tmp = tcss_tmp.intersection(o.kernel())
+    ts['kernel intersection'] = toc(t0)-list(ts.values())[-1]
+    # Else, try to use union of all obstacles in cluster
+    if tcss_tmp.area < 1e-6:
+        tcss_tmp = tcss.intersection(cl.polygon_excluding_hull())
+    ts['cluster intersection'] = toc(t0)-list(ts.values())[-1]
+    if tcss_tmp.area > 1e-6:
+        tcss = tcss_tmp
+
+    # If not tc from previous iteraion, use closest point to TCSS in ad ker as triangle center
+    if triangle_center_prev is None:
+        tc, _ = shapely.ops.nearest_points(cl.admissible_kernel, tcss.centroid)
+        triangle_center = np.array(tc.coords[0])
+        while is_collinear(x, xg, triangle_center):
+            triangle_center += np.random.uniform(-1e-4, 1e-4, 2)
+    # Else, use previous triangle center if still in selection set
+    elif tcss.contains(shapely.geometry.Point(triangle_center_prev))\
+            and not is_collinear(x, xg, triangle_center_prev):
+        triangle_center = triangle_center_prev
+    # Else, try to maintain triangle center on same side of l(x,xg) as previous time step
+    else:
+        x_xg_line = shapely.geometry.LineString([x, xg])
+        splitted_tcss = shapely.ops.split(tcss, x_xg_line).geoms
+        triangle_center_selection_set = splitted_tcss[0]
+        if len(splitted_tcss) > 1:
+            for i in range(1, len(splitted_tcss)):
+                if is_ccw(x, xg, splitted_tcss[i].centroid.coords[0]) == is_ccw(x, xg, triangle_center_prev):
+                    triangle_center_selection_set = splitted_tcss[i]
+                    break
+        tc, _ = shapely.ops.nearest_points(cl.admissible_kernel, triangle_center_selection_set.centroid)
+        triangle_center = np.array(tc.coords[0])
+
+    ts['tc selection'] = toc(t0)-list(ts.values())[-1]
+
+    # if cl.name == '5_6_7':
+    #     hs, _ = draw_shapely_polygon(tcss, plt.gca(), fc='g')
+    #     hs += plt.plot(*triangle_center, 'kd')
+    #     while not plt.waitforbuttonpress(): pass
+    #     [h.remove() for h in hs]
+
+    # Select kernel points as largest equilateral triangle in TCSS (with maximum side length epsilon)
+    if tcss.geom_type == 'Polygon':
+        dist = tcss.exterior.distance(shapely.geometry.Point(triangle_center))
+    else:
+        tc = shapely.geometry.Point(triangle_center)
+        dist = min([p.exterior.distance(tc) for p in tcss.geoms])
+    triangle_length = min(epsilon, 0.9 * dist)
+    kernel_points = equilateral_triangle(triangle_center, triangle_length)
+    ts['triangle generation'] = toc(t0)-list(ts.values())[-1]
+    tot_time = sum(ts.values())
+    for k in ts.keys():
+        ts[k] = int(ts[k] / tot_time * 100)
+    # print(ts)
+    return kernel_points
+
+def extract_cluster(cl, cl_list):
+    if cl_list is None:
+        return None
+    for cl_i in cl_list:
+        if cl.name == cl_i.name:
+            return cl_i
+    return None
+
+# Input: Convex obstacles, excluding points x and xg, kernel width epsilon
+def cluster_and_starify(obstacles, x, xg, epsilon, workspace=None, max_compute_time=np.inf, previous_clusters=None,
+                        make_convex=False, exclude_obstacles=False, max_iterations=np.inf, verbose=False,
+                        timing_verbose=False, return_history=False):
+    t0 = tic()
+
+    if workspace is None:
+        workspace = Ellipse([1e10, 1e10])
+
+    # Exit flags
+    INCOMPLETE = 0
+    COMPLETE = 1
+    MAX_COMPUTE_TIME_CONVEX_HULL = 2
+
+    # Variable initialization
+    kernel_time, hull_time, cluster_time, convex_time = [0.], [0.], [0.], 0.
+    adm_ker_robot_cones = {obs.id(): None for obs in obstacles}
+    adm_ker_goal_cones = {obs.id(): None for obs in obstacles}
+    adm_ker_obstacles = {obs.id(): {} for obs in obstacles}
+    cluster_iterations = []
+    n_iter = 0
+
+    def default_result():
+        if return_history:
+            cl_history = cluster_iterations if n_iter > 0 else [ObstacleCluster([o]) for o in obstacles]
+            return [ObstacleCluster([o]) for o in obstacles], [0.] * 4, INCOMPLETE, n_iter, cl_history
+        else:
+            return [ObstacleCluster([o]) for o in obstacles], [0.] * 4, INCOMPLETE, n_iter
+
+    # Compute admissible kernel for all obstacles
+    for o in obstacles:
+        if max_compute_time < toc(t0):
+            return default_result()
+        # Find admissible kernel
+        adm_ker_robot_cones[o.id()] = sh.admissible_kernel(o, x)
+        adm_ker_goal_cones[o.id()] = sh.admissible_kernel(o, xg)
+
+        if adm_ker_robot_cones[o.id()] is None:
+            if verbose:
+                print("[Cluster and Starify]: Robot position is not a free exterior point of obstacle " + str(o.id()))
+            return default_result()
+
+        if adm_ker_goal_cones[o.id()] is None:
+            if verbose:
+                print("[Cluster and Starify]: Goal position is not a free exterior point of obstacle " + str(o.id()))
+            return default_result()
+
+        # Admissible kernel when excluding points of other obstacles
+        if exclude_obstacles:
+            for o_ex in obstacles:
+                if o_ex.id() == o.id():
+                    continue
+                o_x_exclude = o_ex.extreme_points()
+                if not all([o.exterior_point(x_ex) for x_ex in o_x_exclude]):
+                    adm_ker_obstacles[o.id()][o_ex.id()] = None
+                    continue
+                adm_ker_obstacles[o.id()][o_ex.id()] = sh.admissible_kernel(o, o_x_exclude[0]).polygon()
+                for v in o_x_exclude[1:]:
+                    adm_ker_obstacles[o.id()][o_ex.id()] = adm_ker_obstacles[o.id()][o_ex.id()].intersection(
+                        sh.admissible_kernel(o, v).polygon())
+    init_kernel_time = toc(t0)
+
+    # -- First iteration -- #
+
+    ker_sel_time, hull_compute_time, star_obj_time = [0], [0], [0]
+    # Initialize clusters as single obstacles
+    clusters = []
+    for o in obstacles:
+        # Ensure not xr in l(x,xg)
+        # while is_collinear(x, o.xr(), xg):
+        #     o.set_xr(o.xr(output_frame=Frame.OBSTACLE) + np.random.normal(0, 0.01, 2))
+        cl = ObstacleCluster([o])
+
+        # New---
+        t1 = tic()
+        cl.admissible_kernel = adm_ker_robot_cones[o.id()].intersection(adm_ker_goal_cones[o.id()])
+        kernel_time[0] += toc(t1)
+
+        t1 = tic()
+        cl_prev = extract_cluster(cl, previous_clusters)
+        # if cl_prev is None:
+        #     cl_prev.kernel_points = equilateral_triangle(o.xr(), epsilon)
+        cl.kernel_points = compute_kernel_points(cl, x, xg, epsilon, cl_prev, workspace=workspace)
+        ker_sel_time[0] += toc(t1)
+        t1 = tic()
+
+        # -- Compute starshaped hull of cluster
+        cl_id = "new" if cl_prev is None else cl_prev.cluster_obstacle.id()
+        cluster_hull_extensions = sh.kernel_starshaped_hull(cl.obstacles, cl.kernel_points)
+        hull_compute_time[0] += toc(t1)
+        t1 = tic()
+        k_centroid = np.mean(cl.kernel_points, axis=0)
+        if cluster_hull_extensions is None:
+            cl.cluster_obstacle = o
+            cl.cluster_obstacle.set_xr(k_centroid, input_frame=Frame.GLOBAL)
+        else:
+        # Non-starshaped polygons are included in the cluster hull
+        # cl_obstacles = [o] if o.is_starshaped() else []
+            cl.cluster_obstacle = StarshapedPrimitiveCombination(cl.obstacles, cluster_hull_extensions, xr=k_centroid,
+                                                             id=cl_id)
+        star_obj_time[0] += toc(t1)
+        # cl.polygon()
+        # if o.is_starshaped and o.kernel().contains(shapely.geometry.Point(k_centroid)):
+        #     cl.cluster_obstacle
+        # cl.cluster_obstacle = StarshapedPolygon(cl._polygon, xr=k_centroid, id=o.id())
+        # hull_time[0] += toc(t1)
+
+
+        clusters += [cl]
+
+
+        # if not o.is_starshaped():
+        #     cl = clusters[-1]
+        #     t1 = tic()
+        #     cl.admissible_kernel = adm_ker_robot_cones[o.id()].intersection(adm_ker_goal_cones[o.id()])
+        #     kernel_time[0] += toc(t1)
+        #
+        #     t1 = tic()
+        #     cl_prev = None
+        #     if previous_clusters:
+        #         for p_cl in previous_clusters:
+        #             if cl.name == p_cl.name:
+        #                 cl_prev = p_cl
+        #     cl.kernel_points = compute_kernel_points(cl, x, xg, epsilon, cl_prev, workspace=workspace)
+        #     # -- Compute starshaped hull of cluster
+        #     k_centroid = np.mean(cl.kernel_points, axis=0)
+        #     cl._polygon = sh.kernel_starshaped_hull(o, cl.kernel_points)
+        #     cl.cluster_obstacle = StarshapedPolygon(cl._polygon, xr=k_centroid, id=o.id())
+        #     hull_time[0] += toc(t1)
+    hull_time[0] = ker_sel_time[0] + hull_compute_time[0] + star_obj_time[0]
+
+    # Set cluster history
+    cluster_history = {cl.name: cl for cl in clusters}
+    if return_history:
+        cluster_iterations += [clusters]
+
+    # -- Cluster obstacles such that no intersection exists
+    t1 = tic()
+    clusters, intersection_exists = get_intersection_clusters(clusters)
+    cluster_time[0] = toc(t1)
+
+    n_iter = 1
+    # -- End first iteration -- #
+
+    while intersection_exists:
+        kernel_time += [0.]
+        hull_time += [0.]
+        cluster_time += [0.]
+        ker_sel_time += [0.]
+        hull_compute_time += [0.]
+        star_obj_time += [0.]
+
+        # Check compute time
+        if max_compute_time < toc(t0):
+            if verbose:
+                print("[Cluster and Starify]: Max compute time.")
+            cluster_iterations += [clusters]
+            return default_result()
+
+        # Find starshaped obstacle representation for each cluster
+        for i, cl in enumerate(clusters):
+            # If cluster already calculated keep it
+            if cl.name in cluster_history:
+                clusters[i] = cluster_history[cl.name]
+                continue
+
+            # ----------- Admissible Kernel ----------- #
+            t1 = tic()
+
+            # If cluster is two convex obstacles
+            # if len(cl.obstacles) == 2 and cl.obstacles[0].is_convex() and cl.obstacles[1].is_convex():
+            #     cl.admissible_kernel = cl.obstacles[0].polygon().intersection(cl.obstacles[1].polygon()).buffer(0.01)
+            # else:
+            adm_ker_robot = Cone.list_intersection([adm_ker_robot_cones[o.id()] for o in cl.obstacles], same_apex=True)
+            adm_ker_goal = Cone.list_intersection([adm_ker_goal_cones[o.id()] for o in cl.obstacles], same_apex=True)
+            cl.admissible_kernel = adm_ker_robot.intersection(adm_ker_goal)
+
+            if cl.admissible_kernel.is_empty or cl.admissible_kernel.area < 1e-6:
+                if verbose:
+                    print("[Cluster and Starify]: Could not find disjoint starshaped obstacles. Admissible kernel empty for the cluster " + cl.name)
+                cluster_iterations += [clusters]
+                return default_result()
+
+            # Exclude other obstacles
+            adm_ker_o_ex = cl.admissible_kernel
+            if exclude_obstacles:
+                for o in cl.obstacles:
+                    for o_ex in obstacles:
+                        if o_ex.id() == o.id() or adm_ker_obstacles[o.id()][o_ex.id()] is None:
+                            continue
+                        adm_ker_o_ex = adm_ker_o_ex.intersection(adm_ker_obstacles[o.id()][o_ex.id()])
+                if not (adm_ker_o_ex.is_empty or adm_ker_o_ex.area < 1e-6):
+                    cl.admissible_kernel = adm_ker_o_ex
+
+            kernel_time[n_iter] += toc(t1)
+            # ----------- End Admissible Kernel ----------- #
+
+            # ----------- Starshaped Hull ----------- #
+            t1 = tic()
+            # Check if cluster exist in previous cluster
+            cl_prev = None
+            if previous_clusters:
+                for p_cl in previous_clusters:
+                    if cl.name == p_cl.name:
+                        cl_prev = p_cl
+
+            # -- Kernel points selection
+            cl.kernel_points = compute_kernel_points(cl, x, xg, epsilon, cl_prev, workspace=workspace)
+            ker_sel_time[n_iter] += toc(t1)
+            t1 = tic()
+
+            # -- Compute starshaped hull of cluster
+            cl_id = "new" if cl_prev is None else cl_prev.cluster_obstacle.id()
+            cluster_hull_extensions = sh.kernel_starshaped_hull(cl.obstacles, cl.kernel_points)
+            hull_compute_time[n_iter] += toc(t1)
+
+            t1 = tic()
+            k_centroid = np.mean(cl.kernel_points, axis=0)
+            # Non-starshaped polygons are included in the cluster hull
+            cl_obstacles = [o for o in cl.obstacles if o.is_starshaped()]
+            cl.cluster_obstacle = StarshapedPrimitiveCombination(cl_obstacles, cluster_hull_extensions, xr=k_centroid, id=cl_id)
+            cl.polygon()
+
+            star_obj_time[n_iter] += toc(t1)
+            # ----------- End Starshaped Hull ----------- #
+
+            # -- Add cluster to history
+            cluster_history[cl.name] = cl
+
+        hull_time[n_iter] = ker_sel_time[n_iter] + hull_compute_time[n_iter] + star_obj_time[n_iter]
+
+        if return_history:
+            cluster_iterations += [clusters]
+
+        # ----------- Clustering ----------- #
+        t1 = tic()
+        # -- Cluster star obstacles such that no intersection exists
+        clusters, intersection_exists = get_intersection_clusters(clusters)
+        cluster_time[n_iter] = toc(t1)
+        # ----------- End Clustering ----------- #
+
+        n_iter += 1
+
+        if n_iter >= max_iterations:
+            break
+
+    # ----------- Make Convex ----------- #
+
+    if make_convex:
+        # Make convex if no intersection occurs
+        t1 = tic()
+        for j, cl in enumerate(clusters):
+            if not cl.cluster_obstacle.is_convex():
+                # Check compute time
+                if max_compute_time < toc(t0):
+                    if verbose:
+                        print("[Cluster and Starify]: Max compute time in convex hull.")
+                    return clusters, [sum(cluster_time), init_kernel_time+sum(kernel_time), sum(hull_time), toc(t1)], MAX_COMPUTE_TIME_CONVEX_HULL, n_iter
+
+                v = np.array(cl.polygon().exterior.coords)[:-1, :]
+                hull = ConvexHull(v)
+                hull_polygon = shapely.geometry.Polygon(v[hull.vertices, :])
+                if not any([hull_polygon.contains(shapely.geometry.Point(x_ex)) for x_ex in [x, xg]]) and not any(
+                        [hull_polygon.intersects(clusters[k].polygon()) for k in range(len(clusters)) if k != j]):
+                    # clusters[j].cluster_obstacle = StarshapedPrimitiveCombination(cl.obstacles, hull_polygon, cl.cluster_obstacle.xr(Frame.GLOBAL))
+                    clusters[j].cluster_obstacle = StarshapedPolygon(hull_polygon, xr=cl.cluster_obstacle.xr(Frame.GLOBAL))
+
+        convex_time = toc(t1)
+    # ----------- End Make Convex ----------- #
+
+    timing_vec = [sum(cluster_time), init_kernel_time+sum(kernel_time), sum(hull_time), convex_time]
+    if timing_verbose:
+
+        print("------------\nTotal timing (Cluster/AdmKer/StHull/ConvHull): {:.1f} [{:.1f}, {:.1f}, {:.1f}, {:.1f}]".format(sum(timing_vec), *timing_vec))
+        print("Init kernel timing: {:.1f}".format(init_kernel_time))
+        for i in range(n_iter):
+            print("Iteration {} timing (Cluster/AdmKer/StHull): [{:.1f}, {:.1f}, {:.1f}]".format(i, cluster_time[i], kernel_time[i], hull_time[i]))
+            print("\t Hull timing divide: (KerSel/Hull/Obj) calculation [{:.1f}, {:.1f}, {:.1f}]".format(ker_sel_time[i], hull_compute_time[i], star_obj_time[i]))
+
+    if return_history:
+        return clusters, timing_vec, COMPLETE, n_iter, cluster_iterations
+    else:
+        return clusters, timing_vec, COMPLETE, n_iter
+
+
+def draw_clustering(clusters, p, pg, xlim=None, ylim=None):
+    color = plt.cm.rainbow(np.linspace(0, 1, len(clusters)))
+    fig, ax = plt.subplots()
+    for i, cl in enumerate(clusters):
+        [o.draw(ax=ax, fc=color[i], show_reference=False, ec='k', alpha=0.8) for o in cl.obstacles]
+    ax.plot(*p, 'ko')
+    ax.plot(*pg, 'k*')
+    if xlim is not None:
+        ax.set_xlim(xlim)
+    if ylim is not None:
+        ax.set_ylim(ylim)
+    return fig, ax
+
+
+def draw_star_hull(clusters, p, pg, xlim=None, ylim=None):
+    fig, ax = plt.subplots()
+    for cl in clusters:
+        if cl.cluster_obstacle is not None:
+            cl.cluster_obstacle.draw(ax=ax, fc='g', alpha=0.8)
+            draw_shapely_polygon(cl.polygon_excluding_hull(), ax=ax, fc='lightgrey', ec='k')
+        else:
+            draw_shapely_polygon(cl.polygon_excluding_hull(), ax=ax, fc='r', ec='k')
+    ax.plot(*p, 'ko')
+    ax.plot(*pg, 'k*')
+    if xlim is not None:
+        ax.set_xlim(xlim)
+    if ylim is not None:
+        ax.set_ylim(ylim)
+    return fig, ax
+
+
+def draw_adm_ker(clusters, p, pg, xlim=None, ylim=None):
+    valid_adm_ker_cls = [cl for cl in clusters if not (cl.admissible_kernel is None or cl.admissible_kernel.is_empty)]
+    n_col = int(np.sqrt(len(valid_adm_ker_cls))) + 1
+    fig, axs = plt.subplots(n_col, n_col)
+    for i, cl in enumerate(valid_adm_ker_cls):
+        ax_i = axs[i//n_col, i%n_col]
+        [o.draw(ax=ax_i, show_name=1, show_reference=0, fc='lightgrey', ec='k', alpha=0.8) for o in cl.obstacles]
+        if not cl.admissible_kernel.geom_type == 'Point':
+            draw_shapely_polygon(cl.admissible_kernel, ax=ax_i, fc='y', alpha=0.3)
+            if cl.kernel_points is not None:
+                draw_shapely_polygon(shapely.geometry.Polygon(cl.kernel_points), ax=ax_i, fc='g', alpha=0.6)
+        ax_i.plot(*p, 'ko')
+        ax_i.plot(*pg, 'k*')
+        if xlim is not None:
+            ax_i.set_xlim(xlim)
+        if ylim is not None:
+            ax_i.set_ylim(ylim)
+    return fig, axs
diff --git a/python/ur_simple_control/path_generation/starworlds/starshaped_hull/starshaped_hull.py b/python/ur_simple_control/path_generation/starworlds/starshaped_hull/starshaped_hull.py
new file mode 100644
index 0000000000000000000000000000000000000000..8fde482f9edaf717ffcb82f170b9bacc158c3714
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/starshaped_hull/starshaped_hull.py
@@ -0,0 +1,207 @@
+import shapely
+import numpy as np
+from obstacles import Polygon
+from utils import is_ccw, is_cw, line, ray, Cone, convex_hull
+import matplotlib.pyplot as plt
+
+
+def admissible_kernel(obstacle, x):
+    # Find tangents of obstacle through x
+    tps = obstacle.tangent_points(x)
+    if not tps:
+        # Interior point
+        return None
+    return Cone(x, x-tps[0], x-tps[1])
+
+
+# Computes the starshaped hull of a list of obstacles for specified kernel points
+def kernel_starshaped_hull(obstacles, kernel_points):
+    if not type(obstacles) is list:
+        if obstacles.is_convex():
+            return convex_kernel_starshaped_hull(obstacles, kernel_points)
+        if issubclass(obstacles.__class__, Polygon):
+            return polygon_kernel_starshaped_hull(obstacles.polygon(), kernel_points)
+        else:
+            print("[kernel_starshaped_hull]: Bad obstacle class.")
+            print(obstacles)
+
+    sub_pols = [kernel_starshaped_hull(o, kernel_points) for o in obstacles]
+    hull_polygon = shapely.ops.unary_union(sub_pols)
+    if hull_polygon.is_empty:
+        return None
+    return hull_polygon
+
+
+def convex_kernel_starshaped_hull(convex_obstacle, kernel_points):
+    tps = []
+    for k in kernel_points:
+        tps += convex_obstacle.tangent_points(k)
+    if not tps:
+        return shapely.geometry.Polygon([])
+
+    tps = np.unique(tps,axis=0)
+    ch_points = np.vstack((tps, kernel_points))
+    pol = convex_hull(ch_points)
+    return shapely.geometry.Polygon(pol)
+
+
+# TODO: Improve computational consideration
+def polygon_kernel_starshaped_hull(polygon, kernel_points, debug=0):
+    kernel_points = kernel_points.reshape((kernel_points.size//2, 2))
+
+    if kernel_points.shape[0] > 2:
+        # NOTE: Assumes kernel points convex
+        # convex_kernel_subset = shapely.geometry.Polygon(kernel_points[ConvexHull(kernel_points).vertices, :])
+        convex_kernel_subset = shapely.geometry.Polygon(kernel_points)
+
+    # polygon_sh = polygon.polygon()  # Shapely represenation of polygon
+    # vertices = np.asarray(polygon_sh.exterior.coords)[:-1, :]  # Vertices of polygon
+    vertices = np.asarray(polygon.exterior.coords)[:-1, :]  # Vertices of polygon
+    star_vertices = []  # Vertices of starshaped hull polygon
+    v_bar = kernel_points[0].copy() # Last vertex of starshaped hull polygon
+    e1_idx = 0
+    e2_idx = 0
+    k_centroid = np.mean(kernel_points, axis=0)
+    k_included = [False] * kernel_points.shape[0]
+
+    # Arrange vertices such that v_1 is the one with largest x-value and vendv1v2 is CCW, (assumes no collinear vertices in P)
+    start_idx = np.argmax(vertices[:, 0])
+    vertices = np.roll(vertices, -start_idx, axis=0)
+    if is_cw(vertices[-1], vertices[0], vertices[1]):
+        vertices = np.flip(vertices, axis=0)
+        vertices = np.roll(vertices, 1, axis=0)
+    # print("Initial sort: {:.1f}".format(toc(t0)))
+
+    # Iterate through all vertices
+    for v_idx, v in enumerate(vertices):
+        adjust_e1 = False
+        # Check if no ray r(v,kv) intersects with interior of polygon
+        if all([ray(v, k, v).disjoint(polygon) for k in kernel_points]):
+            # Add current vertex
+            if kernel_points.shape[0] < 3 or not convex_kernel_subset.contains(shapely.geometry.Point(v)):
+                star_vertices += [v]
+            if star_vertices:
+                # Intersections of lines l(k,v) and l(e1,e2)
+                e1, e2 = star_vertices[e1_idx], star_vertices[e2_idx]
+                e1_e2 = line(e1, e2)
+
+                for k in kernel_points:
+                    kv_e1e2_intersect = line(k,v).intersection(e1_e2)
+                    # Adjust to closest intersection to e2
+                    if not kv_e1e2_intersect.is_empty:
+                        adjust_e1 = True
+                        e1_candidate = np.array([kv_e1e2_intersect.x, kv_e1e2_intersect.y])
+                        if np.linalg.norm(e2 - e1_candidate) < np.linalg.norm(e2 - star_vertices[e1_idx]):
+                            star_vertices[e1_idx] = e1_candidate
+
+            if not adjust_e1:
+                for k_idx, k in enumerate(kernel_points):
+                    kps = [kp for kp in kernel_points if not np.array_equal(kp, k)]
+                    kv_P_intersect = line(k, v).intersection(polygon)
+
+                    # If l(k,v) intersects interior of P
+                    if not kv_P_intersect.is_empty:
+                        # Find last intersection of l(k,v) and polygon boundary
+                        if kv_P_intersect.geom_type == 'LineString':
+                            intersection_points = [np.array([ip[0], ip[1]]) for ip in kv_P_intersect.coords]
+                        elif kv_P_intersect.geom_type == 'MultiLineString':
+                            intersection_points = [np.array([ip[0], ip[1]]) for l in kv_P_intersect.geoms for ip in
+                                                   l.coords]
+                        elif kv_P_intersect.geom_type == 'GeometryCollection':
+                            intersection_points = []
+                            for g in kv_P_intersect.geoms:
+                                if g.geom_type == 'Point':
+                                    intersection_points += [np.array(g.coords[0])]
+                                if kv_P_intersect.geom_type == 'LineString':
+                                    intersection_points += [np.array([ip[0], ip[1]]) for ip in g.coords]
+                        else:
+                            intersection_points = []
+
+                        u = None
+                        u_v = None
+                        for u_candidate in intersection_points:
+                            u_v = line(u_candidate, v)
+                            if u_v.disjoint(polygon):
+                                u = u_candidate
+                                break
+                        if u is None:
+                            continue
+
+                        # If no ray r(u,k'v) intersect with interior of polygon
+                        if not any([ray(u, kp, v).intersects(polygon) for kp in kps]):
+                            # Adjust u if l(k',v_bar) intersects l(u,v)
+                            for kp in kps:
+                                kpvb_uv_intersect = line(kp, v_bar).intersection(u_v)
+                                if not kpvb_uv_intersect.is_empty:
+                                    u = np.array([kpvb_uv_intersect.x, kpvb_uv_intersect.y])
+                            # Append u to P*
+                            star_vertices += [u]
+                            # Update last augmented edge
+                            e1_idx, e2_idx = len(star_vertices)-1, len(star_vertices)-2
+                            # Swap last vertices if not CCW
+                            if is_ccw(u, v, vertices[v_idx-1]):
+                            # if is_ccw(v_bar, v, u):
+                            # if is_cw(k_centroid, v, u):
+                                star_vertices[-2], star_vertices[-1] = star_vertices[-1], star_vertices[-2]
+                                e1_idx, e2_idx = e2_idx, e1_idx
+                            adjust_e1 = True
+                    else:
+                        # Check if no ray r(k,k'v) intersect with interior of polygon
+                        if (not k_included[k_idx]) and (not any([ray(k, kp, v).intersects(polygon) for kp in kps])):
+                            k_included[k_idx] = True
+                            # Append k to P*
+                            star_vertices += [k]
+                            # Update last augmented edge
+                            e1_idx, e2_idx = len(star_vertices)-1, len(star_vertices)-2
+                            # Swap last vertices if not CCW
+                            if is_ccw(k, v, vertices[v_idx-1]):
+                            # if is_cw(k_centroid, v, k):
+                                star_vertices[-2], star_vertices[-1] = star_vertices[-1], star_vertices[-2]
+                                e1_idx, e2_idx = e2_idx, e1_idx
+                            adjust_e1 = True
+            # Update v_bar
+            v_bar = star_vertices[-1]
+
+            # Visualize debug information
+            if debug == 1:
+                plt.plot(*k_centroid, 'ko')
+                plt.plot(*polygon.exterior.xy, 'k')
+                plt.plot([p[0] for p in star_vertices], [p[1] for p in star_vertices], 'g-o', linewidth=2)
+                [plt.plot(*k, 'kx') for k in kernel_points]
+                [plt.plot(*line(k,v).xy, 'k--') for k in kernel_points]
+                if adjust_e1:
+                    plt.plot(*star_vertices[e1_idx], 'ys')
+                plt.show()
+
+    # Check not added kernel points if they should be included
+    for j in range(len(star_vertices)):
+        v, vp = star_vertices[j - 1], star_vertices[j]
+        for k_idx, k in enumerate(kernel_points):
+            if (not k_included[k_idx]) and is_cw(k, v, vp):
+                k_included[k_idx] = True
+                # Insert k
+                star_vertices = star_vertices[:j] + [k] + star_vertices[j:]
+                # Visualize debug information
+                if debug == 1:
+                    plt.plot(*k_centroid, 'ko')
+                    plt.plot(*polygon.exterior.xy, 'k')
+                    plt.plot([p[0] for p in star_vertices], [p[1] for p in star_vertices], 'g-o', linewidth=2)
+                    [plt.plot(*ki, 'kx') for ki in kernel_points]
+                    plt.plot(*line(k, v).xy, 'r--*')
+                    plt.plot(*line(k, vp).xy, 'r--*')
+                    plt.plot(*k, 'go')
+                    plt.show()
+    # print("Final kernel check: {:.1f}".format(toc(t0)))
+
+    if debug:
+        ax = plt.gca()
+        ax.plot(*polygon.exterior.xy, 'k')
+        ax.plot([p[0] for p in star_vertices] + [star_vertices[0][0]],
+                [p[1] for p in star_vertices] + [star_vertices[0][1]], 'g-o', linewidth=2)
+        # [ax.plot(star_vertices[i][0], star_vertices[i][1], 'r*') for i in augmented_vertex_idcs]
+        [ax.plot(*zip(k, sv), 'y--') for sv in star_vertices for k in kernel_points]
+        ax.plot(*k_centroid, 'bs')
+        plt.show()
+
+    return shapely.geometry.Polygon(star_vertices)
+
diff --git a/python/ur_simple_control/path_generation/starworlds/starworlds.egg-info/PKG-INFO b/python/ur_simple_control/path_generation/starworlds/starworlds.egg-info/PKG-INFO
new file mode 100644
index 0000000000000000000000000000000000000000..4dee13fc6c070e01a3bca7b804984f6d9645b9f5
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/starworlds.egg-info/PKG-INFO
@@ -0,0 +1,9 @@
+Metadata-Version: 2.1
+Name: starworlds
+Version: 1.0
+License-File: LICENSE
+Requires-Dist: pyyaml
+Requires-Dist: numpy
+Requires-Dist: scipy
+Requires-Dist: matplotlib
+Requires-Dist: shapely
diff --git a/python/ur_simple_control/path_generation/starworlds/starworlds.egg-info/SOURCES.txt b/python/ur_simple_control/path_generation/starworlds/starworlds.egg-info/SOURCES.txt
new file mode 100644
index 0000000000000000000000000000000000000000..853123cb2e00b78f079beab8e29ceb9251e378d7
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/starworlds.egg-info/SOURCES.txt
@@ -0,0 +1,29 @@
+.gitignore
+LICENSE
+README.md
+requirements.txt
+setup.py
+obstacles/__init__.py
+obstacles/ellipse.py
+obstacles/motion_model.py
+obstacles/obstacle.py
+obstacles/polygon.py
+obstacles/starshaped_obstacle.py
+obstacles/starshaped_polygon.py
+obstacles/starshaped_primitive_combination.py
+starshaped_hull/__init__.py
+starshaped_hull/cluster_and_starify.py
+starshaped_hull/starshaped_hull.py
+starworlds.egg-info/PKG-INFO
+starworlds.egg-info/SOURCES.txt
+starworlds.egg-info/dependency_links.txt
+starworlds.egg-info/requires.txt
+starworlds.egg-info/top_level.txt
+tests/test_cluster_and_starify.py
+tests/test_obstacles.py
+tests/test_starshaped_hull.py
+tests/test_timing.py
+tests/test_utils.py
+utils/__init__.py
+utils/cg.py
+utils/misc.py
\ No newline at end of file
diff --git a/python/ur_simple_control/path_generation/starworlds/starworlds.egg-info/dependency_links.txt b/python/ur_simple_control/path_generation/starworlds/starworlds.egg-info/dependency_links.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/starworlds.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/python/ur_simple_control/path_generation/starworlds/starworlds.egg-info/requires.txt b/python/ur_simple_control/path_generation/starworlds/starworlds.egg-info/requires.txt
new file mode 100644
index 0000000000000000000000000000000000000000..f849890421afe2baa719e8822ed041b4c0ea68da
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/starworlds.egg-info/requires.txt
@@ -0,0 +1,5 @@
+pyyaml
+numpy
+scipy
+matplotlib
+shapely
diff --git a/python/ur_simple_control/path_generation/starworlds/starworlds.egg-info/top_level.txt b/python/ur_simple_control/path_generation/starworlds/starworlds.egg-info/top_level.txt
new file mode 100644
index 0000000000000000000000000000000000000000..35cb7de772c37f5866607798fbfd9e7e835bff72
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/starworlds.egg-info/top_level.txt
@@ -0,0 +1,3 @@
+obstacles
+starshaped_hull
+utils
diff --git a/python/ur_simple_control/path_generation/starworlds/tests/test_cluster_and_starify.py b/python/ur_simple_control/path_generation/starworlds/tests/test_cluster_and_starify.py
new file mode 100644
index 0000000000000000000000000000000000000000..0461e20f4aaa1b680a8ce87348f9b1700086117f
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/tests/test_cluster_and_starify.py
@@ -0,0 +1,129 @@
+import numpy as np
+import matplotlib.pyplot as plt
+from obstacles import Ellipse, StarshapedPolygon
+from obstacles import motion_model as mm
+from utils import generate_convex_polygon, draw_shapely_polygon
+from starshaped_hull import cluster_and_starify, draw_clustering, draw_adm_ker, draw_star_hull
+import shapely
+
+
+def test_cluster_and_starify():
+    n_obstacles = 40
+    ellipse_fraction = .5
+    ell_radius_mean, ell_radius_std = 1, 0.2
+    n_vertices, pol_box = 8, [2, 2]
+    target_scene_coverage = 0.3
+    epsilon = 0.2
+
+    np.random.seed(0)
+
+    def random_scene_point(scene_width):
+        return np.random.rand(2) * scene_width
+
+    def select_x_xg(scene_width, obstacles):
+        x = random_scene_point(scene_width)
+        while any([o.interior_point(x) for o in obstacles]):
+            x = random_scene_point(scene_width)
+        xg = random_scene_point(scene_width)
+        while any([o.interior_point(xg) for o in obstacles]):
+            xg = random_scene_point(scene_width)
+        return x, xg
+
+    # Generate obstacles
+    Nell = int(n_obstacles * ellipse_fraction)
+    Npol = n_obstacles - Nell
+    obstacles = [Ellipse(a=np.random.normal(ell_radius_mean, ell_radius_std, 2), n_pol=10) for j in range(Nell)]
+    obstacles += [StarshapedPolygon(generate_convex_polygon(n_vertices, pol_box), xr=[0, 0], is_convex=True) for j in range(Npol)]
+
+    # Compute area data
+    obstacle_area = sum([o.area() for o in obstacles])
+
+    # Setup scene
+    scene_width = np.sqrt(obstacle_area / target_scene_coverage * 0.9)
+
+    for j in range(Nell):
+        # obstacles[j].set_motion_model(ob.Static(random_scene_point(res['scene_width'][i])))
+        obstacles[j].set_motion_model(mm.Static(random_scene_point(scene_width - 2 * ell_radius_mean) + ell_radius_mean))
+    for j in range(Npol):
+        # obstacles[Nell+j].set_motion_model(ob.Static(random_scene_point(res['scene_width'][i])))
+        obstacles[Nell + j].set_motion_model(mm.Static(random_scene_point(scene_width - pol_box[0]) + pol_box[0] / 2))
+    [obs.polygon() for obs in obstacles]
+
+    # Select collision free robot and goal positions
+    x, xg = select_x_xg(scene_width, obstacles)
+    # Cluster and starify
+    clusters, timing, flag, n_iter, cluster_history = cluster_and_starify(obstacles, x, xg, epsilon, make_convex=0,
+                                                                          verbose=1, return_history=1, timing_verbose=1)
+
+    # Draw iteration steps
+    for i, clusters_i in enumerate(cluster_history[1:]):
+        _, ax = draw_clustering(clusters_i, x, xg, xlim=[0, scene_width], ylim=[0, scene_width])
+        ax.set_title("Clustering, Iteration {}/{}".format(i+1, len(cluster_history)))
+        fig, axs = draw_adm_ker(clusters_i, x, xg, xlim=[-0.2*scene_width, 1.2*scene_width], ylim=[-0.2*scene_width, 1.2*scene_width])
+        fig.suptitle("Admissible Kernel, Iteration {}/{}".format(i+1, len(cluster_history)))
+        _, ax = draw_star_hull(clusters_i, x, xg, xlim=[0, scene_width], ylim=[0, scene_width])
+        ax.set_title("Starshaped Hull, Iteration {}/{}".format(i+1, len(cluster_history)))
+
+
+def test_moving_cluster():
+    obstacles = [
+        Ellipse([1, 1], motion_model=mm.Static([-1, 0.3])),
+        Ellipse([1, 1], motion_model=mm.Static([0., 0.3])),
+        Ellipse([1, 1], motion_model=mm.Static([1, 0.3]))
+    ]
+    p0 = np.array([0.1, -2.5])
+    pg = np.array([0.1, 2.5])
+    epsilon = 0.2
+    xlim = [-3, 3]
+    ylim = [-3, 3]
+
+    clusters, timing, flag, n_iter = cluster_and_starify(obstacles, p0, pg, epsilon)
+
+    fig, axs = plt.subplots(1, 3)
+    [o.draw(ax=axs[0], show_reference=0, fc='lightgrey', ec='k', alpha=0.8) for o in obstacles]
+    axs[0].plot(*p0, 'ko')
+    axs[0].plot(*pg, 'k*')
+    axs[0].plot(*clusters[0].cluster_obstacle.xr(), 'gd')
+    axs[0].plot(*zip(p0, pg), '--')
+    axs[0].set_xlim(xlim)
+    axs[0].set_ylim(ylim)
+
+    xr_prev = clusters[0].cluster_obstacle.xr()
+    obstacles[0].set_motion_model(mm.Static([-1, -0.3]))
+    obstacles[1].set_motion_model(mm.Static([0., -0.3]))
+    obstacles[2].set_motion_model(mm.Static([1, -0.3]))
+    clusters, timing, flag, n_iter = cluster_and_starify(obstacles, p0, pg, epsilon, previous_clusters=clusters)
+
+    [o.draw(ax=axs[1], show_reference=0, fc='lightgrey', ec='k', alpha=0.8) for o in obstacles]
+    axs[1].plot(*p0, 'ko')
+    axs[1].plot(*pg, 'k*')
+    axs[1].plot(*xr_prev, 'sk')
+    axs[1].plot(*clusters[0].cluster_obstacle.xr(), 'gd')
+    axs[1].plot(*zip(p0, pg), '--')
+    axs[1].set_xlim(xlim)
+    axs[1].set_ylim(ylim)
+
+    xr_prev = clusters[0].cluster_obstacle.xr()
+    obstacles[0].set_motion_model(mm.Static([-1, -1]))
+    obstacles[1].set_motion_model(mm.Static([0., -1]))
+    obstacles[2].set_motion_model(mm.Static([1, -1]))
+    clusters, timing, flag, n_iter = cluster_and_starify(obstacles, p0, pg, epsilon, previous_clusters=clusters)
+
+    x_xg_line = shapely.geometry.LineString([p0, pg])
+    kernel_selection_set = shapely.ops.split(clusters[0].cluster_obstacle.polygon(), x_xg_line).geoms[0]
+
+    [o.draw(ax=axs[2], show_reference=0, fc='lightgrey', ec='k', alpha=0.8) for o in obstacles]
+    draw_shapely_polygon(kernel_selection_set, ax=axs[2], hatch='///', fill=False, linestyle='None')
+    axs[2].plot(*p0, 'ko')
+    axs[2].plot(*pg, 'k*')
+    axs[2].plot(*xr_prev, 'sk')
+    axs[2].plot(*clusters[0].cluster_obstacle.xr(), 'gd')
+    axs[2].plot(*zip(p0, pg), '--')
+    axs[2].set_xlim(xlim)
+    axs[2].set_ylim(ylim)
+
+
+if (__name__) == "__main__":
+    test_cluster_and_starify()
+    # test_moving_cluster()
+    plt.show()
\ No newline at end of file
diff --git a/python/ur_simple_control/path_generation/starworlds/tests/test_obstacles.py b/python/ur_simple_control/path_generation/starworlds/tests/test_obstacles.py
new file mode 100644
index 0000000000000000000000000000000000000000..2839f4036d8d58afac21a42b6d0c7daf42e4c8b4
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/tests/test_obstacles.py
@@ -0,0 +1,147 @@
+import numpy as np
+import matplotlib.pyplot as plt
+from obstacles import Ellipse, Polygon, StarshapedPolygon, StarshapedPrimitiveCombination, Frame
+from obstacles import motion_model as mm
+from utils import generate_convex_polygon, draw_shapely_polygon, generate_star_polygon
+import shapely.geometry
+
+
+def test_ellipse():
+    ell_axes = [2, 1]
+    ell_pos = [0, 0.5]
+    xlim = [ell_pos[0] - 2 * ell_axes[0], ell_pos[0] + 2 * ell_axes[0]]
+    ylim = [ell_pos[1] - 2 * ell_axes[1], ell_pos[1] + 2 * ell_axes[1]]
+    ell = Ellipse(ell_axes, xr=[0, .9], motion_model=mm.Static(ell_pos, 1))
+    while True:
+        x = np.array([np.random.uniform(*xlim),np.random.uniform(*ylim)])
+        if ell.exterior_point(x):
+            break
+    b = ell.boundary_mapping(x)
+    n = ell.normal(x)
+    tp = ell.tangent_points(x)
+    dir = ell.reference_direction(x)
+
+    _, ax = ell.draw()
+    ax.plot(*zip(ell.xr(Frame.GLOBAL), x), 'k--o')
+    if b is not None:
+        ax.plot(*b, 'y+')
+        ax.quiver(*b, *n)
+    if tp:
+        ax.plot(*zip(x, tp[0]), 'g:')
+        ax.plot(*zip(x, tp[1]), 'g:')
+    ax.quiver(*ell.xr(Frame.GLOBAL), *dir, color='c', zorder=3)
+    ax.set_xlim(xlim)
+    ax.set_ylim(ylim)
+
+
+def test_nonstar_polygon():
+    pass
+
+
+def test_star_polygon():
+    avg_radius = 1
+    xlim = [-2*avg_radius, 2*avg_radius]
+    ylim = xlim
+    pol = StarshapedPolygon(generate_star_polygon([0, 0], avg_radius, irregularity=0.3, spikiness=0.5, num_vertices=10))
+
+    while True:
+        x = np.array([np.random.uniform(*xlim), np.random.uniform(*ylim)])
+        if pol.exterior_point(x):
+            break
+    b = pol.boundary_mapping(x)
+    n = pol.normal(x)
+    tp = pol.tangent_points(x)
+    dir = pol.reference_direction(x)
+
+    _, ax = pol.draw()
+    ax.plot(*zip(pol.xr(Frame.GLOBAL), x), 'k--o')
+    if b is not None:
+        ax.plot(*b, 'y+')
+        ax.quiver(*b, *n)
+    if tp:
+        ax.plot(*zip(x, tp[0]), 'g:')
+        ax.plot(*zip(x, tp[1]), 'g:')
+    ax.quiver(*pol.xr(Frame.GLOBAL), *dir, color='c', zorder=3)
+
+    for i in np.linspace(0, 2 * np.pi, 100):
+        x = pol.xr() + 100*np.array([np.cos(i), np.sin(i)])
+        b = pol.boundary_mapping(x)
+        n = pol.normal(b)
+        ax.quiver(*b, *n)
+    ax.set_xlim(xlim)
+    ax.set_ylim(ylim)
+    print("es")
+
+def test_star_primitive_combination():
+    n_ellipses = 3
+    n_polygons = 2
+    n_vertices = 6
+    box_width = 2
+
+    ell_axes = [0.7, 0.4]
+    pol_box = [box_width, box_width]
+
+    xlim = [-box_width, box_width]
+    ylim = [-box_width, box_width]
+    ellipses = [Ellipse(ell_axes) for i in range(n_ellipses)]
+    polygons = [StarshapedPolygon(generate_convex_polygon(n_vertices, pol_box), is_convex=True)
+                for i in range(n_polygons)]
+    obstacles = ellipses + polygons
+    while True:
+        # Generate new positions
+        for obs in obstacles:
+            obs.set_motion_model(mm.Static(np.random.uniform(-0.2*box_width, 0.2*box_width, 2), np.random.uniform(0, 2*np.pi)))
+
+        # Identify if all obstacle form a single cluster
+        kernel = obstacles[0].polygon()
+        for obs in obstacles[1:]:
+            kernel = kernel.intersection(obs.polygon())
+
+        if not kernel.is_empty:
+            break
+        else:
+            _, ax = plt.subplots()
+            [obs.draw(ax=ax, fc='r', alpha=0.2, ec='k') for obs in obstacles]
+            ax.set_xlim(xlim)
+            ax.set_ylim(ylim)
+            plt.show()
+
+    xr = np.array(kernel.representative_point().coords[0])
+    star_obs = StarshapedPrimitiveCombination(obstacles, shapely.geometry.Polygon([]), xr)
+
+    while True:
+        x = np.array([np.random.uniform(*xlim), np.random.uniform(*ylim)])
+        if star_obs.exterior_point(x):
+            break
+    b = star_obs.boundary_mapping(x)
+    n = star_obs.normal(x)
+    tp = star_obs.tangent_points(x)
+    dir = star_obs.reference_direction(x)
+
+    _, ax = star_obs.draw()
+    draw_shapely_polygon(kernel, ax=ax, fc='g')
+    ax.plot(*zip(star_obs.xr(Frame.GLOBAL), x), 'k--o')
+    if b is not None:
+        ax.plot(*b, 'y+')
+        ax.quiver(*b, *n)
+    if tp:
+        ax.plot(*zip(x, tp[0]), 'g:')
+        ax.plot(*zip(x, tp[1]), 'g:')
+    ax.quiver(*star_obs.xr(Frame.GLOBAL), *dir, color='c', zorder=3)
+
+    for i in np.linspace(0, 2 * np.pi, 100):
+        x = star_obs.xr() + np.array([np.cos(i), np.sin(i)])
+        b = star_obs.boundary_mapping(x)
+        n = star_obs.normal(b)
+        # ax.quiver(*b, *n)
+
+    ax.set_xlim(xlim)
+    ax.set_ylim(ylim)
+
+
+if (__name__) == "__main__":
+    # test_ellipse()
+    # test_nonstar_polygon()
+    # test_star_polygon()
+    test_star_primitive_combination()
+    plt.show()
\ No newline at end of file
diff --git a/python/ur_simple_control/path_generation/starworlds/tests/test_starshaped_hull.py b/python/ur_simple_control/path_generation/starworlds/tests/test_starshaped_hull.py
new file mode 100644
index 0000000000000000000000000000000000000000..670a63616cdea33d59d825bb4067a08765a0b00d
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/tests/test_starshaped_hull.py
@@ -0,0 +1,53 @@
+import numpy as np
+import matplotlib.pyplot as plt
+from starshaped_hull import admissible_kernel
+from utils import draw_shapely_polygon, Cone
+from obstacles import Polygon, Ellipse
+from obstacles import motion_model as mm
+
+def test_admissible_kernel():
+    pol = Polygon([[0, 0], [1, 0], [2, 2], [2, 4], [-0.75, 1], [-1, 4], [-2, 2], [-2, 1], [-1.5, 0], [-2, -1]])
+
+    x_exclude = np.array([0, 2])
+
+    ad_ker = admissible_kernel(pol, x_exclude)
+
+    _, ax = pol.draw()
+    draw_shapely_polygon(ad_ker.polygon(), ax=ax, fc='y', alpha=0.6)
+    ax.plot(x_exclude[0], x_exclude[1], 'ro')
+    ax.set_xlim([-3, 6])
+    ax.set_ylim([-1, 5])
+
+
+def test_adm_ker_ellipse():
+    ell = Ellipse([1.08877265, 1.06673487], motion_model=mm.Static([4.61242385, 1.87941425]))
+    p = np.array([3.641344, 4.87955125])
+    pg = [0.73012219, 8.95180958]
+    adm_ker_p = admissible_kernel(ell, p)# Cone.list_intersection([adm_ker_robot_cones[o.id()] for o in cl.obstacles])
+    adm_ker_pg = admissible_kernel(ell, pg)
+    ad_ker = adm_ker_p.intersection(adm_ker_pg)
+
+    _, ax = ell.draw()
+    draw_shapely_polygon(ad_ker, ax=ax, fc='y', alpha=0.6)
+    draw_shapely_polygon(adm_ker_p.polygon(), ax=ax, fc='None', ec='k')
+    draw_shapely_polygon(adm_ker_pg.polygon(), ax=ax, fc='None', ec='k')
+    ax.plot(*p, 'rx')
+    ax.plot(*pg, 'rx')
+    ax.set_xlim(0, 10)
+    ax.set_ylim(0, 10)
+
+
+def test_kernel_starshaped_hull_single():
+    pass
+
+
+def test_kernel_starshaped_hull_cluster():
+    pass
+
+
+if (__name__) == "__main__":
+    test_admissible_kernel()
+    test_adm_ker_ellipse()
+    test_kernel_starshaped_hull_single()
+    test_kernel_starshaped_hull_cluster()
+    plt.show()
\ No newline at end of file
diff --git a/python/ur_simple_control/path_generation/starworlds/tests/test_timing.py b/python/ur_simple_control/path_generation/starworlds/tests/test_timing.py
new file mode 100644
index 0000000000000000000000000000000000000000..068ff3bcb7db0fa8077c7586bf29ec78c75f1d8a
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/tests/test_timing.py
@@ -0,0 +1,173 @@
+import numpy as np
+import matplotlib.pyplot as plt
+from obstacles import Ellipse, StarshapedPolygon
+from obstacles import motion_model as mm
+from utils import generate_convex_polygon
+from starshaped_hull import cluster_and_starify, draw_clustering, draw_adm_ker, draw_star_hull
+import shapely
+
+
+def test_cluster_and_starify_compute():
+    par = {'N_samples': 1000, 'pol_Nvert': 10, 'ell_n_pol': 30, 'ell_fraction': 0.5, 'pol_box': [2, 2],
+           'ell_radius_mean': 1., 'ell_radius_std': 0.2, 'target_scene_coverage': 0.25,
+           'No_min': 5, 'No_max': 50, 'rd_seed': 0, 'epsilon': 0.1}
+
+    plot_fail = 0
+    #  ----- Data generation  ------ #
+    np.random.seed(par['rd_seed'])
+    # Result data
+    res = {'compute_time': np.zeros((par['N_samples'], 4)), 'n_iter': np.zeros(par['N_samples'], dtype=np.int32),
+           'No': np.zeros(par['N_samples'], dtype=np.int32), 'Ncl': np.zeros(par['N_samples'], dtype=np.int32),
+           'obstacle_area': np.zeros(par['N_samples']), 'obstacle_coverage': np.zeros(par['N_samples']),
+           'scene_width': np.zeros(par['N_samples']), 'scene_coverage': np.zeros(par['N_samples'])}
+
+    def random_scene_point(scene_width):
+        return np.random.rand(2) * scene_width
+
+    def select_x_xg(scene_width, obstacles):
+        x = random_scene_point(scene_width)
+        while any([o.interior_point(x) for o in obstacles]):
+            x = random_scene_point(scene_width)
+        xg = random_scene_point(scene_width)
+        while any([o.interior_point(xg) for o in obstacles]):
+            xg = random_scene_point(scene_width)
+        return x, xg
+
+    for i in range(par['N_samples']):
+        # Generate obstacles
+        res['No'][i] = np.random.randint(par['No_min'], par['No_max'] + 1)
+        Nell = int(res['No'][i] * par['ell_fraction'])
+        Npol = res['No'][i] - Nell
+        obstacles = [
+            Ellipse(a=np.random.normal(par['ell_radius_mean'], par['ell_radius_std'], 2), n_pol=par['ell_n_pol'])
+            for j in range(Nell)]
+        obstacles += [StarshapedPolygon(generate_convex_polygon(par['pol_Nvert'], par['pol_box']), xr=[0, 0],
+                                           is_convex=True) for j in range(Npol)]
+
+        # Compute area data
+        res['obstacle_area'][i] = sum([o.area() for o in obstacles])
+
+        # Setup scene
+        res['scene_width'][i] = np.sqrt(res['obstacle_area'][i] / par['target_scene_coverage'] * 0.85)
+
+        for j in range(Nell):
+            # obstacles[j].set_motion_model(ob.Static(random_scene_point(res['scene_width'][i])))
+            obstacles[j].set_motion_model(mm.Static(
+                random_scene_point(res['scene_width'][i] - 2 * par['ell_radius_mean']) + par['ell_radius_mean']))
+        for j in range(Npol):
+            # obstacles[Nell+j].set_motion_model(ob.Static(random_scene_point(res['scene_width'][i])))
+            obstacles[Nell + j].set_motion_model(
+                mm.Static(random_scene_point(res['scene_width'][i] - par['pol_box'][0]) + par['pol_box'][0] / 2))
+
+        # Compute coverage data
+        res['obstacle_coverage'][i] = shapely.ops.unary_union([o.polygon() for o in obstacles]).area
+        res['scene_coverage'][i] = res['obstacle_coverage'][i] / (res['scene_width'][i] ** 2)
+
+        flag = 0
+        n_failures = -1
+        while flag == 0:
+            n_failures += 1
+            # Select collision free robot and goal positions
+            x, xg = select_x_xg(res['scene_width'][i], obstacles)
+            # Cluster and starify
+            clusters, timing, flag, res['n_iter'][i] = cluster_and_starify(obstacles, x, xg, par['epsilon'],
+                                                                           exclude_obstacles=0, make_convex=0,
+                                                                           max_iterations=100,
+                                                                           verbose=0)
+
+            star_obstacles = [cl.cluster_obstacle for cl in clusters]
+            res['compute_time'][i, :] = timing
+            res['Ncl'][i] = len(clusters)
+
+            if plot_fail and flag == 0:
+                _, ax_i = plt.subplots()
+                [o.draw(ax=ax_i, fc='g', alpha=0.8) for o in star_obstacles]
+                [o.draw(ax=ax_i, show_name=1, show_reference=0, ec='k', linestyle='--') for o in obstacles]
+                ax_i.plot(*x, 'rx', markersize=16)
+                ax_i.plot(*xg, 'rx', markersize=16)
+                ax_i.set_xlim([0, res['scene_width'][i]])
+                ax_i.set_ylim([0, res['scene_width'][i]])
+                if flag == 0:
+                    ax_i.set_title(
+                        'Fail\nSample: {}, #O: {}, #Cl: {}, Time: {:.1f}, It: {}, Scene coverage: {:.2f}({:.2f})'.format(
+                            i, res['No'][i], res['Ncl'][i], sum(res['compute_time'][i, :]), res['n_iter'][i],
+                            res['scene_coverage'][i], par['target_scene_coverage']))
+                else:
+                    ax_i.set_title(
+                        'Sample: {}, #O: {}, #Cl: {}, Time: {:.1f}, It: {}, Scene coverage: {:.2f}({:.2f})'.format(
+                            i, res['No'][i], res['Ncl'][i], sum(res['compute_time'][i, :]), res['n_iter'][i],
+                            res['scene_coverage'][i], par['target_scene_coverage']))
+                plt.show()
+
+            if n_failures == 5:
+                break
+
+    #  ----- Postprocessing ------ #
+    total_compute_time = np.sum(res['compute_time'], axis=1)
+    binc_niter = np.bincount(res['n_iter'])[min(res['n_iter']):]
+    print(np.vstack((np.arange(min(res['n_iter']), min(res['n_iter']) + len(binc_niter)), binc_niter)))
+
+    cl = ['r', 'g', 'b', 'y', 'k', 'c']
+    mk = ['o', '+', '^', 'x', 'd', '8']
+    tct_it = [None] * len(binc_niter)
+    No_it = [None] * len(binc_niter)
+    ct_it = [[]] * len(binc_niter)
+    scene_coverage_it = [None] * len(binc_niter)
+    for i in range(len(binc_niter)):
+        scene_coverage_it[i] = np.ma.masked_where(res['n_iter'] != min(res['n_iter']) + i, res['scene_coverage'])
+        tct_it[i] = np.ma.masked_where(res['n_iter'] != min(res['n_iter']) + i, total_compute_time)
+        No_it[i] = np.ma.masked_where(res['n_iter'] != min(res['n_iter']) + i, res['No'])
+        for j in range(3):
+            ma = np.ma.masked_where(res['n_iter'] != min(res['n_iter']) + i, res['compute_time'][:, j])
+            ct_it[i] += [ma.data]
+
+    timename = ['Clustering', 'Adm Ker', 'St Hull']
+    if max(res['No']) - min(res['No']) > 0:
+        _, axs = plt.subplots(3)
+        for j in range(3):
+            for i in range(len(binc_niter)):
+                axs[j].scatter(No_it[i], ct_it[i][j], c=cl[i], marker=mk[i])
+            axs[j].set_xlabel('No'), axs[j].set_ylabel('Time [ms]')
+            axs[j].set_title(timename[j])
+    plt.tight_layout()
+
+    _, axs = plt.subplots(1, 2)
+    # ax.scatter(res['No'], total_compute_time)
+    for i in range(len(binc_niter)):
+        axs[0].scatter(No_it[i], tct_it[i], c=cl[i], marker=mk[i], s=np.square(scene_coverage_it[i] * 20))
+        axs[1].scatter(scene_coverage_it[i], tct_it[i], c=cl[i], marker=mk[i], s=No_it[i] * 2)
+
+    _, ax = plt.subplots()
+    ax.scatter(res['No'], np.divide(res['compute_time'][:, 0], total_compute_time), color='r')
+    ax.scatter(res['No'], np.divide(res['compute_time'][:, 1], total_compute_time), color='g')
+    ax.scatter(res['No'], np.divide(res['compute_time'][:, 2], total_compute_time), color='b')
+    ax.set_title("Clustering fraction of total"), ax.set_xlabel('No'), ax.set_ylabel('Cl time / Tot time')
+
+    _, axs = plt.subplots(2, 2)
+    axs[0, 0].scatter(res['No'], res['obstacle_area'])
+    axs[0, 0].scatter(res['No'], res['obstacle_coverage']), axs[0, 0].set_title('Obstacle area')
+    axs[0, 0].set_xlabel('#Obstacles'), axs[1, 0].set_ylabel('Obstacle area')
+    axs[1, 0].scatter(res['scene_coverage'], res['n_iter']), axs[1, 0].set_title('#Iterations')
+    axs[1, 0].set_xlabel('Scene coverage'), axs[1, 0].set_ylabel('#Iterations')
+    axs[0, 1].scatter(res['No'], np.divide(res['Ncl'], res['No'])), axs[0, 1].set_title('#Clusters')
+    axs[0, 1].set_xlabel('#Obstacles'), axs[0, 1].set_ylabel('#Clusters/#Obstacles')
+    axs[1, 1].scatter(res['scene_coverage'], np.divide(res['Ncl'], res['No'])), axs[1, 1].set_title('#Clusters/#No')
+    axs[1, 1].set_xlabel('Scene coverage'), axs[1, 1].set_ylabel('#Clusters/#Obstacles')
+    plt.tight_layout()
+
+    scene_coverage = res['scene_coverage'] * 100
+    cov_span = max(scene_coverage) - min(scene_coverage)
+    binwidth = 1
+    _, ax = plt.subplots()
+    ax.hist(scene_coverage, bins=int(cov_span / binwidth),
+            color='blue', edgecolor='black')
+    ymin, ymax = ax.get_ylim()
+    ax.plot([par['target_scene_coverage'] * 100, par['target_scene_coverage'] * 100], [ymin, ymax], 'r--')
+    ax.set_title('Histogram with Binwidth = %d' % binwidth, size=30)
+    ax.set_xlabel('Coverage', size=22)
+    ax.set_ylabel('Samples', size=22)
+
+
+if (__name__) == "__main__":
+    test_cluster_and_starify_compute()
+    plt.show()
\ No newline at end of file
diff --git a/python/ur_simple_control/path_generation/starworlds/tests/test_utils.py b/python/ur_simple_control/path_generation/starworlds/tests/test_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..54cac6a5e2d79d03452cf847b41117d1674dd59b
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/tests/test_utils.py
@@ -0,0 +1,106 @@
+import numpy as np
+import matplotlib.pyplot as plt
+from utils import Cone, draw_shapely_polygon
+
+
+def test_affine_transformation():
+    pass
+
+
+def test_point_in_triangle():
+    pass
+
+
+def test_orientation():
+    pass
+
+
+def test_equilateral_triangle():
+    pass
+
+
+def test_convex_hull():
+    pass
+
+
+def test_generate_convex_polygon():
+    pass
+
+
+def test_generate_star_polygon():
+    pass
+
+
+def test_cone():
+    n_cones_sqrt = 4
+    n_cones_same_apex = 4
+    n_points = 8
+    n_several_cone_intersection = 2
+    boxlim = [-1, 1]
+
+    fig_int_sev, axs_int_sev = plt.subplots(n_cones_sqrt, n_cones_sqrt)
+    fig_int, axs_int = plt.subplots(n_cones_sqrt, n_cones_sqrt)
+    fig_p, axs_p = plt.subplots(n_cones_sqrt, n_cones_sqrt)
+    for i in range(n_cones_sqrt ** 2):
+        # Test cone intersection
+        c_params = np.random.uniform(*boxlim, (2, 3))
+        c1 = Cone(c_params[:, 0], c_params[:, 1] - c_params[:, 0], c_params[:, 2] - c_params[:, 0])
+        c2_params = np.random.uniform(*boxlim, (2, 3))
+        c2_apex = c_params[:, 0] if i < n_cones_same_apex else c2_params[:, 0]
+        c2 = Cone(c2_apex, c2_params[:, 1] - c2_apex, c2_params[:, 2] - c2_apex)
+        c_several_intersect = [c1, c2]
+        for j in range(1, n_several_cone_intersection):
+            c3_params = np.random.uniform(*boxlim, (2, 3))
+            c3_apex = c_params[:, 0] if i < n_cones_same_apex else c3_params[:, 0]
+            c_several_intersect += [Cone(c3_apex, c3_params[:, 1] - c3_apex, c3_params[:, 2] - c3_apex)]
+
+        two_cones_intersection = c1.intersection(c2)
+        several_cones_intersection = Cone.list_intersection(c_several_intersect, same_apex=i<n_cones_same_apex)
+
+        # Two cones intersection plot
+        fig_int.suptitle("Intersection of two cones")
+        ax_int_i = axs_int[i//n_cones_sqrt, i%n_cones_sqrt]
+        c1.draw(ax=ax_int_i, color='b', alpha=0.2, ray_color='b')
+        c2.draw(ax=ax_int_i, color='r', alpha=0.2, ray_color='r')
+        if not two_cones_intersection.is_empty and not two_cones_intersection.geom_type == 'Point':
+            draw_shapely_polygon(two_cones_intersection, ax=ax_int_i, color='k', alpha=0.2, hatch='///')
+        ax_int_i.set_xlim(1.2 * np.array(boxlim))
+        ax_int_i.set_ylim(1.2 * np.array(boxlim))
+
+        # Several cones intersection plot
+        fig_int_sev.suptitle("Intersection of several cones")
+        color = plt.cm.rainbow(np.linspace(0, 1, 2*n_several_cone_intersection))
+        ax_int_sev_i = axs_int_sev[i//n_cones_sqrt, i%n_cones_sqrt]
+        for j, c in enumerate(c_several_intersect):
+            c.draw(ax=ax_int_sev_i, color=color[j], alpha=0.2, ray_color=color[j])
+        if not several_cones_intersection.is_empty:
+            draw_shapely_polygon(several_cones_intersection, ax=ax_int_sev_i, color='k', alpha=0.2, hatch='///')
+        ax_int_sev_i.set_xlim(1.2 * np.array(boxlim))
+        ax_int_sev_i.set_ylim(1.2 * np.array(boxlim))
+
+        # Test point in cone
+        fig_p.suptitle("Point in cone")
+        ax_p_i = axs_p[i//n_cones_sqrt, i%n_cones_sqrt]
+        xs = np.linspace(*boxlim, n_points)
+        ys = np.linspace(*boxlim, n_points)
+        XS, YS = np.meshgrid(xs, ys)
+        c1.draw(ax=ax_p_i, color='b', alpha=0.2, ray_color='b')
+        for j in range(n_points):
+            for k in range(n_points):
+                col = 'g' if c1.point_in_cone([XS[j, k], YS[j, k]]) else 'r'
+                ax_p_i.plot(XS[j, k], YS[j, k], marker='.', color=col)
+        ax_p_i.set_xlim(1.2 * np.array(boxlim))
+        ax_p_i.set_ylim(1.2 * np.array(boxlim))
+
+
+if (__name__) == "__main__":
+    test_convex_hull()
+    test_orientation()
+    test_affine_transformation()
+    test_equilateral_triangle()
+    test_orientation()
+    test_generate_convex_polygon()
+    test_generate_star_polygon()
+    test_point_in_triangle()
+    test_cone()
+    plt.show()
diff --git a/python/ur_simple_control/path_generation/starworlds/utils/__init__.py b/python/ur_simple_control/path_generation/starworlds/utils/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..428e2f7fa52d681e0fec32febc1c69117e92bbb4
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/utils/__init__.py
@@ -0,0 +1,2 @@
+from .misc import *
+from .cg import *
diff --git a/python/ur_simple_control/path_generation/starworlds/utils/cg.py b/python/ur_simple_control/path_generation/starworlds/utils/cg.py
new file mode 100644
index 0000000000000000000000000000000000000000..bb7525d45bbaef9404d59fb15fa2c32d9e666f12
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/utils/cg.py
@@ -0,0 +1,593 @@
+import numpy as np
+import shapely.geometry
+import shapely.ops
+import matplotlib.pyplot as plt
+from typing import List, Tuple
+from utils import draw_shapely_polygon
+
+DEFAULT_RAY_INFINITY_LENGTH = 100000.
+COLLINEAR_THRESHOLD = 1e-10
+
+
+def affine_transform(x, rotation, translation, inverse=False):
+    if inverse:
+        x_t = [x[0] - translation[0], x[1] - translation[1]]
+        if rotation == 0:
+            return np.array(x_t)
+        c, s = np.cos(rotation), np.sin(rotation)
+        return np.array([c*x_t[0] + s*x_t[1], -s*x_t[0] + c*x_t[1]])
+    else:
+        if rotation != 0:
+            c, s = np.cos(rotation), np.sin(rotation)
+            return np.array([c*x[0]-s*x[1]+translation[0], s*x[0]+c*x[1]+translation[1]])
+        else:
+            return np.array([x[0]+translation[0], x[1]+translation[1]])
+
+
+# Line segment from a to b, excluding a and b
+def line(a, b):
+    return shapely.geometry.LineString([a + .0001 * (b - a), a + .9999 * (b - a)])
+
+
+# Random point on the triangle with vertices a, b and c
+def point_in_triangle(a, b, c):
+    """
+    """
+    x, y = np.random.rand(2)
+    q = abs(x - y)
+    s, t, u = q, 0.5 * (x + y - q), 1 - 0.5 * (q + x + y)
+    return s * a[0] + t * b[0] + u * c[0], s * a[1] + t * b[1] + u * c[1]
+
+# TODO: Dynamic length of ray
+# Ray emanating from a in direction of b->c
+def ray(a, b, c, ray_inf_length=DEFAULT_RAY_INFINITY_LENGTH):
+    return shapely.geometry.LineString([a + .0001 * (c - b), a + ray_inf_length * (c - b)])
+
+
+def np_orientation_val(a, b, c):
+    return (b[:, 1] - a[:, 1]) * (c[:, 0] - b[:, 0]) - (b[:, 0] - a[:, 0]) * (c[:, 1] - b[:, 1])
+
+
+def orientation_val(a, b, c):
+    return (b[1] - a[1]) * (c[0] - b[0]) - (b[0] - a[0]) * (c[1] - b[1])
+
+
+def is_collinear(a, b, c):
+    return abs(orientation_val(a, b, c)) < COLLINEAR_THRESHOLD
+
+
+def is_cw(a, b, c):
+    return orientation_val(a, b, c) > COLLINEAR_THRESHOLD
+
+
+def is_ccw(a, b, c):
+    return orientation_val(a, b, c) < -COLLINEAR_THRESHOLD
+
+
+# TODO: Use [NOT is_cw and NOT is_ccw] instead?
+# Returns true if point b is between point and b
+def is_between(a, b, c):
+    return np.isclose(np.linalg.norm(a-b) + np.linalg.norm(c-b), np.linalg.norm(a-c), rtol=1e-7)
+
+
+def intersect(a1, a2, b1, b2):
+    return (is_ccw(a1, b1, b2) != is_ccw(a2, b1, b2) and is_ccw(a1, a2, b1) != is_ccw(a1, a2, b2))
+
+
+class Point:
+
+    def __init__(self, x: float, y: float):
+        self.x = x
+        self.y = y
+        self.xy = [x, y]
+
+    def __str__(self):
+        return "Point {:s}".format(str(self.xy))
+
+    def __iter__(self):
+        return iter(self.xy)
+
+    def __getitem__(self, item):
+        return self.xy[item]
+
+    def draw(self, ax=None, marker='o', **kwargs):
+        if ax is None:
+            _, ax = plt.subplots(subplot_kw={'aspect': 'equal'})
+        handles = ax.plot(self.x, self.y, marker=marker, **kwargs)
+        return handles, ax
+
+
+class Line:
+
+    def __init__(self, p1: Point, p2: Point):
+        self.p1 = p1
+        self.p2 = p2
+
+    def __str__(self):
+        return "Line --({:.2f},{:.2f})--({:.2f},{:.2f})--".format(self.p1.x, self.p1.y, self.p2.x, self.p2.y)
+
+    def line_intersection(self, other: 'Line') -> Point:
+        self_dx = self.p1.x - self.p2.x
+        other_dx = other.p1.x - other.p2.x
+        self_dy = self.p1.y - self.p2.y
+        other_dy = other.p1.y - other.p2.y
+        den = self_dx * other_dy - self_dy * other_dx
+        if abs(den) < 1e-10:
+            # Parallel or coincident lines
+            return None
+        tmp1 = self.p1.x * self.p2.y - self.p1.y * self.p2.x
+        tmp2 = other.p1.x * other.p2.y - other.p1.y * other.p2.x
+        ip_x = (tmp1 * other_dx - self_dx * tmp2) / den
+        ip_y = (tmp1 * other_dy - self_dy * tmp2) / den
+        return Point(ip_x, ip_y)
+
+    def intersects(self, other):
+        return (is_ccw(self.p1, other.p1, other.p1) != is_ccw(self.p2, other.p1, other.p1) and is_ccw(self.p1, self.p2, other.p1) != is_ccw(self.p1, self.p2, other.p2))
+
+
+class LineSegment(Line):
+
+    def __str__(self):
+        return "Line segment ({:.2f},{:.2f})--({:.2f},{:.2f})".format(self.p1.x, self.p1.y, self.p2.x, self.p2.y)
+
+    def line_segment_intersection(self, other: 'LineSegment') -> Point:
+        self_dx = self.p1.x - self.p2.x
+        other_dx = other.p1.x - other.p2.x
+        self_dy = self.p1.y - self.p2.y
+        other_dy = other.p1.y - other.p2.y
+        p1_dx = self.p1.x - other.p1.x
+        p1_dy = self.p1.y - other.p1.y
+        den = self_dx * other_dy - self_dy * other_dx
+        if abs(den) < 1e-10:
+            # Parallel or coincident lines
+            return None
+        t = (p1_dx * other_dy - p1_dy * other_dx) / den
+        u = (p1_dx * self_dy - p1_dy * self_dx) / den
+        if t < 0 or t > 1 or u < 0 or u > 1:
+            return None
+        ip_x = self.p1.x - t * self_dx
+        ip_y = self.p1.y - t * self_dy
+        return Point(ip_x, ip_y)
+
+class Ray(Line):
+
+    def __str__(self):
+        return "Ray ({:.2f},{:.2f})--({:.2f},{:.2f})--".format(self.p1.x, self.p1.y, self.p2.x, self.p2.y)
+
+    def ray_intersection(self, other: 'Ray') -> Point:
+        self_dx = self.p1.x - self.p2.x
+        other_dx = other.p1.x - other.p2.x
+        self_dy = self.p1.y - self.p2.y
+        other_dy = other.p1.y - other.p2.y
+        p1_dx = self.p1.x - other.p1.x
+        p1_dy = self.p1.y - other.p1.y
+        den = self_dx * other_dy - self_dy * other_dx
+        if abs(den) < 1e-10:
+            # Parallel or coincident lines
+            return None
+        t = (p1_dx * other_dy - p1_dy * other_dx) / den
+        u = (p1_dx * self_dy - p1_dy * self_dx) / den
+        if t < 0 or u < 0:
+            return None
+        ip_x = self.p1.x - t * self_dx
+        ip_y = self.p1.y - t * self_dy
+        return Point(ip_x, ip_y)
+
+    def draw(self, ax=None, linestyle='--', color='k', markersize=16, **kwargs):
+        if ax is None:
+            _, ax = plt.subplots(subplot_kw={'aspect': 'equal'})
+        handles = ax.plot(*zip(self.p1, self.p2), linestyle=linestyle, color=color, **kwargs)
+        orient = np.rad2deg(np.arctan2(self.p2.y-self.p1.y, self.p2.x-self.p1.x))
+        handles += ax.plot(*self.p2, marker=(3, 0, orient-90), markersize=markersize, linestyle='None', color=color)
+        return handles, ax
+
+
+# Get intersection of line a and line b
+def get_intersection(a1, a2, b1, b2):
+    if not intersect(a1, a2, b1, b2):
+        if is_between(b1, a1, b2):
+            return a1
+        if is_between(b1, a2, b2):
+            return a2
+        if is_between(a1, b1, a2):
+            return b1
+        if is_between(a1, b2, a2):
+            return b2
+        return None
+    da = a2 - a1
+    db = b2 - b1
+    dp = a1 - b1
+    dap = np.array([-da[1], da[0]])
+    denom = np.dot(dap, db)
+    num = np.dot(dap, dp)
+    return (num / denom.astype(float)) * db + b1
+
+
+def equilateral_triangle(centroid, side_length, rot=0):
+    triangle = np.array(centroid) + np.array([[0, 1 / np.sqrt(3) * side_length],
+                                               [1 / 2 * side_length, -1 / (2*np.sqrt(3)) * side_length],
+                                               [-1 / 2 * side_length, -1 / (2*np.sqrt(3)) * side_length]])
+    if not rot:
+        return triangle
+    c,s = np.cos(rot), np.sin(rot)
+    return np.array([[c * triangle[0, 0] - s * triangle[0, 1], s * triangle[0, 0] + c * triangle[0, 1]],
+                     [c * triangle[1, 0] - s * triangle[1, 1], s * triangle[1, 0] + c * triangle[1, 1]],
+                     [c * triangle[2, 0] - s * triangle[2, 1], s * triangle[2, 0] + c * triangle[2, 1]]])
+
+
+def convex_hull(points):
+    n = len(points)
+
+    # Find the leftmost point
+    l = 0
+    for i in range(1, n):
+        if points[i][0] < points[l][0]:
+            l = i
+        elif points[i][0] == points[l][0]:
+            if points[i][1] > points[l][1]:
+                l = i
+
+    hull = []
+    '''
+    Start from leftmost point, keep moving counterclockwise
+    until reach the start point again. This loop runs O(h)
+    times where h is number of points in result or output.
+    '''
+    p = l
+    while (True):
+        # Add current point to result
+        hull.append([points[p][0], points[p][1]])
+
+        '''
+        Search for a point 'q' such that orientation(p, q,
+        x) is counterclockwise for all points 'x'. The idea
+        is to keep track of last visited most counterclock-
+        wise point in q. If any point 'i' is more counterclock-
+        wise than q, then update q.
+        '''
+        q = (p + 1) % n
+
+        for i in range(n):
+
+            # If i is more counterclockwise
+            # than current q, then update q
+            if is_ccw(points[p], points[i], points[q]):
+                q = i
+        '''
+        Now q is the most counterclockwise with respect to p
+        Set p as q for next iteration, so that q is added to
+        result 'hull'
+        '''
+        p = q
+
+        # While we don't come to first point
+        if (p == l):
+            break
+
+    return hull
+
+
+class Cone:
+    bb_width = 1e6
+    bottom_right = Point(bb_width, -bb_width)
+    top_right = Point(bb_width, bb_width)
+    bottom_left = Point(-bb_width, -bb_width)
+    top_left = Point(-bb_width, bb_width)
+    right_line = Line(bottom_right, top_right)
+    top_line = Line(top_left, top_right)
+    left_line = Line(top_left, bottom_left)
+    bottom_line = Line(bottom_left, bottom_right)
+    bb_edges = [bottom_line, right_line, top_line, left_line]
+    bb_corners = [bottom_right.xy, top_right.xy, top_left.xy, bottom_left.xy]
+    bb_corner_angles = np.pi / 4 * np.array([1, 3, 5, 7])
+
+    def __init__(self, apex, dir1, dir2):
+        self.apex = np.array(apex)
+        self.dir1 = np.array(dir1)
+        self.dir2 = np.array(dir2)
+        self.is_convex = is_ccw([0, 0], self.dir1, self.dir2)
+        self.ray1 = Ray(Point(*self.apex), Point(*(self.apex+self.dir1)))
+        self.ray2 = Ray(Point(*self.apex), Point(*(self.apex+self.dir2)))
+
+    def __str__(self):
+        return "Cone: ({:s}, {:s}, {:s})".format(str(self.apex), str(self.dir1), str(self.dir2))
+
+    def polygon(self) -> shapely.geometry.Polygon:
+
+        angle1 = np.arctan2(self.dir1[1], self.dir1[0]) + np.pi
+        angle2 = np.arctan2(self.dir2[1], self.dir2[0]) + np.pi
+
+        if Cone.bb_corner_angles[0] <= angle1 < Cone.bb_corner_angles[1]:
+            ray1_intersection_idx = 0
+        elif  Cone.bb_corner_angles[1] <= angle1 < Cone.bb_corner_angles[2]:
+            ray1_intersection_idx = 1
+        elif  Cone.bb_corner_angles[2] <= angle1 < Cone.bb_corner_angles[3]:
+            ray1_intersection_idx = 2
+        else:
+            ray1_intersection_idx = 3
+
+        if Cone.bb_corner_angles[0] <= angle2 < Cone.bb_corner_angles[1]:
+            ray2_intersection_idx = 0
+        elif  Cone.bb_corner_angles[1] <= angle2 < Cone.bb_corner_angles[2]:
+            ray2_intersection_idx = 1
+        elif  Cone.bb_corner_angles[2] <= angle2 < Cone.bb_corner_angles[3]:
+            ray2_intersection_idx = 2
+        else:
+            ray2_intersection_idx = 3
+
+        r1_border = self.ray1.ray_intersection(Cone.bb_edges[ray1_intersection_idx])
+        r2_border = self.ray2.ray_intersection(Cone.bb_edges[ray2_intersection_idx])
+        if r1_border is None:
+            ray1_intersection_idx = (ray1_intersection_idx + 1) % 4
+            r1_border = self.ray1.ray_intersection(Cone.bb_edges[ray1_intersection_idx])
+            if r1_border is None:
+                ray1_intersection_idx = (ray1_intersection_idx + 2) % 4
+                r1_border = self.ray1.ray_intersection(Cone.bb_edges[ray1_intersection_idx])
+                if r1_border is None:
+                    print("SOMETHING WRONG!")
+        if r2_border is None:
+            ray2_intersection_idx = (ray2_intersection_idx + 1) % 4
+            r2_border = self.ray2.ray_intersection(Cone.bb_edges[ray2_intersection_idx])
+            if r2_border is None:
+                ray2_intersection_idx = (ray2_intersection_idx + 2) % 4
+                r2_border = self.ray2.ray_intersection(Cone.bb_edges[ray2_intersection_idx])
+                if r2_border is None:
+                    print("SOMETHING WRONG!")
+
+        vertices = [self.apex, r1_border.xy]
+        c_idx = ray1_intersection_idx
+        if not (self.is_convex and (ray1_intersection_idx == ray2_intersection_idx)):
+            while True:
+                vertices += [Cone.bb_corners[c_idx]]
+                c_idx = (c_idx + 1) % 4
+                if c_idx == ray2_intersection_idx:
+                    break
+        vertices += [r2_border.xy]
+
+        return shapely.geometry.Polygon(vertices)
+
+    def point_in_cone(self, x):
+        if self.is_convex:
+            return is_ccw(self.apex, self.ray1.p2, x) and is_cw(self.apex, self.ray2.p2, x)
+        else:
+            return not (is_ccw(self.apex, self.ray2.p2, x) and is_cw(self.apex, self.ray1.p2, x))
+
+    def intersection(self, other: 'Cone', same_apex: bool=False) -> shapely.geometry.Polygon:
+        if same_apex:
+            intersection_list = self.intersection_same_apex(other)
+            if len(intersection_list) == 1:
+                return intersection_list[0].polygon()
+            else:
+                return shapely.ops.unary_union([i.polygon() for i in intersection_list])
+        else:
+            return self.polygon().intersection(other.polygon())
+
+    def intersection_same_apex(self, other: 'Cone') -> List['Cone']:
+        other_dir1_in_self = self.point_in_cone(other.apex+other.dir1)
+        other_dir2_in_self = self.point_in_cone(other.apex+other.dir2)
+        self_dir1_in_other = other.point_in_cone(self.apex+self.dir1)
+
+        if other_dir1_in_self and other_dir2_in_self:
+            # Check several cones
+            if self_dir1_in_other:
+                return [Cone(self.apex, self.dir1, other.dir2),
+                        Cone(self.apex, other.dir1, self.dir2)]
+            else:
+                return [other]
+        elif other_dir1_in_self:
+            return [Cone(self.apex, other.dir1, self.dir2)]
+        elif other_dir2_in_self:
+            return [Cone(self.apex, self.dir1, other.dir2)]
+        elif self_dir1_in_other:
+            return [self]
+        else:
+            return []
+
+    @staticmethod
+    def list_intersection(cones: List['Cone'], same_apex=False) -> shapely.geometry.Polygon:
+        if not same_apex:
+            intersection = cones[0].polygon()
+            for c in cones[1:]:
+                intersection = intersection.intersection(c.polygon())
+            return intersection
+
+        # List of cones
+        cones_intersect = [cones[0]]
+        for c in cones[1:]:
+            # Find intersection of current cones and next in list
+            cones_intersect_new = []
+            for i, ci in enumerate(cones_intersect):
+                cones_intersect_new += ci.intersection_same_apex(c)
+            cones_intersect = cones_intersect_new
+
+            # If empty intersection
+            if not cones_intersect:
+                return shapely.geometry.Polygon([])
+
+        if len(cones_intersect) == 1:
+            ret = cones_intersect[0].polygon()
+            return ret
+        else:
+            return shapely.ops.unary_union([c.polygon() for c in cones_intersect])
+
+    def draw(self, ax=None, ray_color='k', **kwargs):
+        handles, ax = draw_shapely_polygon(self.polygon(), ax=ax, **kwargs)
+        if ray_color is not None:
+            handles += ax.plot(*zip(self.apex, self.apex + Cone.bb_width * self.dir1), linestyle='--', color=ray_color)
+            handles += ax.plot(*zip(self.apex, self.apex + Cone.bb_width * self.dir2), linestyle='-', color=ray_color)
+        return handles, ax
+
+
+def generate_convex_polygon(n, box=None):
+    if box is None:
+        box = [1, 1]
+
+    # Generate two lists of random X and Y coordinates
+    xPool = [box[0]*np.random.uniform() for i in range(n)]
+    yPool = [box[0]*np.random.uniform() for i in range(n)]
+
+    # Sort them
+    xPool = np.sort(xPool)
+    yPool = np.sort(yPool)
+
+    # Isolate the extreme points
+    minX = xPool[0]
+    maxX = xPool[-1]
+    minY = yPool[0]
+    maxY = yPool[-1]
+
+    # Divide the interior points into two chains & Extract the vector components
+    lastTop = minX
+    lastBot = minX
+
+    xVec, yVec = [], []
+    for i in range(1, n-1):
+        if np.random.randint(2):
+            xVec += [xPool[i] - lastTop]
+            lastTop = xPool[i]
+        else:
+            xVec += [lastBot - xPool[i]]
+            lastBot = xPool[i]
+
+    xVec += [maxX - lastTop]
+    xVec += [lastBot - maxX]
+
+    lastLeft = minY
+    lastRight = minY
+
+    for i in range(1, n - 1):
+        if np.random.randint(2):
+            yVec += [yPool[i] - lastLeft]
+            lastLeft = yPool[i]
+        else:
+            yVec += [lastRight - yPool[i]]
+            lastRight = yPool[i]
+
+    yVec += [maxY - lastLeft]
+    yVec += [lastRight - maxY]
+
+    # Randomly pair up the X- and Y-components
+    np.random.shuffle(yVec)
+
+
+    # Combine the paired up components into vectors
+    # vec = [[xVec[i], yVec[i]] for i in range(n)]
+
+    # Sort the vectors by angle
+    angle = [np.arctan2(yVec[i], xVec[i]) for i in range(n)]
+    xVec = [x for _, x in sorted(zip(angle, xVec))]
+    yVec = [y for _, y in sorted(zip(angle, yVec))]
+
+    # Lay them end-to-end
+    x, y = 0, 0
+    minPolygonX, minPolygonY = 0, 0
+    points = []
+
+    for i in range(n):
+        points += [[x, y]]
+        x += xVec[i]
+        y += yVec[i]
+
+        minPolygonX = min(minPolygonX, x)
+        minPolygonY = min(minPolygonY, y)
+
+    xShift = minX - minPolygonX
+    yShift = minY - minPolygonY
+
+
+    # Move the polygon to have center in origin
+    for i in range(n):
+        points[i][0] += xShift - box[0]/2
+        points[i][1] += yShift - box[1]/2
+
+    xr = np.mean(points, axis=0)
+    for i in range(n):
+        points[i][0] -= xr[0]
+        points[i][1] -= xr[1]
+
+    if not shapely.geometry.box(-box[0],-box[1],box[0],box[1],).contains(shapely.geometry.Polygon(points)):
+        _, ax = plt.subplots()
+        draw_shapely_polygon(shapely.geometry.Polygon(points), ax=ax)
+        ax.set_xlim([-2*box[0], 2*box[0]])
+        ax.set_ylim([-2*box[1], 2*box[1]])
+        plt.show()
+
+    return points
+
+def generate_star_polygon(center: Tuple[float, float], avg_radius: float,
+                     irregularity: float, spikiness: float,
+                     num_vertices: int) -> List[Tuple[float, float]]:
+    """
+    Start with the center of the polygon at center, then creates the
+    polygon by sampling points on a circle around the center.
+    Random noise is added by varying the angular spacing between
+    sequential points, and by varying the radial distance of each
+    point from the centre.
+
+    Args:
+        center (Tuple[float, float]):
+            a pair representing the center of the circumference used
+            to generate the polygon.
+        avg_radius (float):
+            the average radius (distance of each generated vertex to
+            the center of the circumference) used to generate points
+            with a normal distribution.
+        irregularity (float):
+            variance of the spacing of the angles between consecutive
+            vertices.
+        spikiness (float):
+            variance of the distance of each vertex to the center of
+            the circumference.
+        num_vertices (int):
+            the number of vertices of the polygon.
+    Returns:
+        List[Tuple[float, float]]: list of vertices, in CCW order.
+    """
+    # Parameter check
+    if irregularity < 0 or irregularity > 1:
+        raise ValueError("Irregularity must be between 0 and 1.")
+    if spikiness < 0 or spikiness > 1:
+        raise ValueError("Spikiness must be between 0 and 1.")
+
+    irregularity *= 2 * np.pi / num_vertices
+    spikiness *= avg_radius
+    angle_steps = random_angle_steps(num_vertices, irregularity)
+
+    # now generate the points
+    points = []
+    angle = np.random.uniform(0, 2 * np.pi)
+    for i in range(num_vertices):
+        radius = np.clip(np.random.normal(avg_radius, spikiness), 0, 2 * avg_radius)
+        point = (center[0] + radius * np.cos(angle),
+                 center[1] + radius * np.sin(angle))
+        points.append(point)
+        angle += angle_steps[i]
+
+    return points
+
+def random_angle_steps(steps: int, irregularity: float) -> List[float]:
+    """Generates the division of a circumference in random angles.
+
+    Args:
+        steps (int):
+            the number of angles to generate.
+        irregularity (float):
+            variance of the spacing of the angles between consecutive vertices.
+    Returns:
+        List[float]: the list of the random angles.
+    """
+    # generate n angle steps
+    angles = []
+    lower = (2 * np.pi / steps) - irregularity
+    upper = (2 * np.pi / steps) + irregularity
+    cumsum = 0
+    for i in range(steps):
+        angle = np.random.uniform(lower, upper)
+        angles.append(angle)
+        cumsum += angle
+
+    # normalize the steps so that point 0 and point n+1 are the same
+    cumsum /= (2 * np.pi)
+    for i in range(steps):
+        angles[i] /= cumsum
+    return angles
diff --git a/python/ur_simple_control/path_generation/starworlds/utils/misc.py b/python/ur_simple_control/path_generation/starworlds/utils/misc.py
new file mode 100644
index 0000000000000000000000000000000000000000..05e09b0376a01818bd92cee24da50dcb64d46f8c
--- /dev/null
+++ b/python/ur_simple_control/path_generation/starworlds/utils/misc.py
@@ -0,0 +1,51 @@
+import matplotlib.pyplot as plt
+import matplotlib.patches as patches
+import time
+import inspect
+import traceback
+import shapely
+import numpy as np
+
+def tic():
+    return time.time()
+
+
+def toc(t0):
+    return (time.time()-t0) * 1000
+
+
+def draw_shapely_polygon(pol, ax=None, xlim=None, ylim=None, **kwargs):
+    if ax is None:
+        _, ax = plt.subplots(subplot_kw={'aspect': 'equal'})
+    pol_list = []
+    handles = []
+    if pol.geom_type == 'Polygon':
+        pol_list += [pol]
+    else:
+        for p in pol.geoms:
+            if p.geom_type == 'Polygon':
+                pol_list += [p]
+    for p in pol_list:
+        if xlim is not None and ylim is not None:
+            pol_plot = p.intersection(shapely.geometry.box(xlim[0] - 1, ylim[0] - 1, xlim[1] + 1, ylim[1] + 1))
+        else:
+            pol_plot = p
+        handles += [patches.Polygon(xy=np.vstack((pol_plot.exterior.xy[0], pol_plot.exterior.xy[1])).T, **kwargs)]
+        ax.add_patch(handles[-1])
+        if xlim is not None:
+            ax.set_xlim(xlim)
+        if ylim is not None:
+            ax.set_ylim(ylim)
+    return handles, ax
+
+
+def logprint(message=None, print_stack=0):
+  callerframerecord = inspect.stack()[1]    # 0 represents this line
+                                            # 1 represents line at caller
+  frame = callerframerecord[0]
+  info = inspect.getframeinfo(frame)
+  if print_stack:
+    traceback.print_stack()
+  print(info.function + ", line: " + str(info.lineno))
+  if message:
+      print(message)