diff --git a/extern/typed-geometry b/extern/typed-geometry
index a6c6371f1bac47f0f269032549adb1873d1f32e4..0c9b77eeb3b1e78e2054f69f15c3859fae6f84b6 160000
--- a/extern/typed-geometry
+++ b/extern/typed-geometry
@@ -1 +1 @@
-Subproject commit a6c6371f1bac47f0f269032549adb1873d1f32e4
+Subproject commit 0c9b77eeb3b1e78e2054f69f15c3859fae6f84b6
diff --git a/samples/04-object-properties/main.cc b/samples/04-object-properties/main.cc
new file mode 100644
index 0000000000000000000000000000000000000000..3b56a04bd9f9068d4aae61e5aa5c4ecac319fe95
--- /dev/null
+++ b/samples/04-object-properties/main.cc
@@ -0,0 +1,169 @@
+#include <typed-geometry/tg.hh>
+
+#include <glow-extras/glfw/GlfwContext.hh>
+#include <glow-extras/viewer/view.hh>
+
+template <class Obj>
+void test_obj(Obj const& obj)
+{
+    tg::rng rng;
+    const int domainD = tg::object_traits<Obj>::domain_dimension;
+    const int objectD = tg::object_traits<Obj>::object_dimension;
+    if constexpr (tg::object_traits<Obj>::is_finite)
+    {
+        float size;
+        if constexpr (objectD == 3)
+            size = 1000.f + volume_of(obj);
+        else if constexpr (objectD == 2)
+            size = 500.f + area_of(obj);
+        else if constexpr (tg::object_traits<Obj>::is_boundary)
+            size = 100.f + perimeter_of(obj);
+        else
+            size = 100.f + length(obj);
+
+        auto uniformPts = std::vector<tg::pos3>();
+        for (tg::u64 i = 0; i < size; ++i)
+            uniformPts.emplace_back(uniform(rng, obj));
+
+        auto v = gv::view();
+
+        // uniform
+        gv::view(uniformPts, tg::to_string(obj));
+
+        // any_point
+        gv::view(gv::points(tg::pos3(any_point(obj))).point_size_px(12.f), tg::color4::red);
+
+        // centroid
+        auto const centroid = centroid_of(obj);
+        if (any_point(obj) == centroid)
+            gv::view(gv::points(tg::pos3(centroid)).point_size_px(13.f), tg::color4::yellow);
+        else
+            gv::view(gv::points(tg::pos3(centroid)).point_size_px(12.f), tg::color4::green);
+
+        // aabb_of
+        auto const aabb = aabb_of(obj);
+        if constexpr (domainD == 3)
+            gv::view(gv::lines(aabb));
+        else
+            gv::view(gv::lines(tg::aabb3(tg::pos3(aabb.min), tg::pos3(aabb.max))));
+
+        // normal_of
+        if constexpr (domainD == 3 && ((objectD == 2 && !tg::object_traits<Obj>::is_boundary) || (objectD == 1 && tg::object_traits<Obj>::is_boundary)))
+            gv::view(tg::segment3(centroid, centroid + normal_of(obj)), tg::color4::green);
+    }
+}
+
+int main()
+{
+    auto const test_obj_and_boundary = [](auto const& o) {
+        test_obj(o);
+        test_obj(boundary_of(o));
+    };
+
+    auto const test_obj_and_boundary_no_caps = [](auto const& o) {
+        test_obj(o);
+        test_obj(boundary_of(o));
+        test_obj(boundary_no_caps_of(o));
+    };
+
+    glow::glfw::GlfwContext ctx;
+    tg::rng rng;
+
+    auto const r = uniform(rng, 0.0f, 10.0f);
+    auto const h = uniform(rng, 0.0f, 10.0f);
+    auto const a = tg::uniform<tg::angle>(rng);
+    auto const n2 = tg::uniform<tg::dir2>(rng);
+    auto const n3 = tg::uniform<tg::dir3>(rng);
+
+    auto const range2 = tg::aabb2(tg::pos2(-10), tg::pos2(10));
+    auto const range3 = tg::aabb3(tg::pos3(-10), tg::pos3(10));
+
+    auto const pos20 = uniform(rng, range2);
+    auto const pos21 = uniform(rng, range2);
+    auto const pos22 = uniform(rng, range2);
+
+    auto const pos30 = uniform(rng, range3);
+    auto const pos31 = uniform(rng, range3);
+    auto const pos32 = uniform(rng, range3);
+
+    auto const axis0 = tg::segment3(pos30, pos31);
+    auto const disk0 = tg::sphere2in3(pos30, r, n3);
+
+    auto const d1 = tg::uniform<tg::dir1>(rng);
+    auto m1 = tg::mat1();
+    m1[0] = d1 * uniform(rng, 1.0f, 3.0f);
+
+    auto const d20 = tg::uniform<tg::dir2>(rng);
+    auto const d21 = perpendicular(d20);
+    auto m2 = tg::mat2();
+    m2[0] = d20 * uniform(rng, 1.0f, 3.0f);
+    m2[1] = d21 * uniform(rng, 1.0f, 3.0f);
+
+    auto const d30 = tg::uniform<tg::dir3>(rng);
+    auto const d31 = any_normal(d30);
+    auto const d32 = normalize(cross(d30, d31));
+    auto m3 = tg::mat3();
+    m3[0] = d30 * uniform(rng, 1.0f, 3.0f);
+    m3[1] = d31 * uniform(rng, 1.0f, 3.0f);
+    m3[2] = d32 * uniform(rng, 1.0f, 3.0f);
+
+    auto m23 = tg::mat2x3();
+    m23[0] = d30 * uniform(rng, 1.0f, 3.0f);
+    m23[1] = d31 * uniform(rng, 1.0f, 3.0f);
+
+    // aabb
+    test_obj_and_boundary(aabb_of(pos20, pos21));
+    test_obj_and_boundary(aabb_of(pos30, pos31));
+    // box
+    test_obj_and_boundary(tg::box2(pos20, m2));
+    test_obj_and_boundary(tg::box3(pos30, m3));
+    test_obj_and_boundary(tg::box2in3(pos30, m23));
+    // capsule
+    test_obj_and_boundary(tg::capsule3(axis0, r));
+    // cylinder
+    test_obj_and_boundary_no_caps(tg::cylinder3(axis0, r));
+    // ellipse
+    test_obj_and_boundary(tg::ellipse2(pos20, m2));
+    test_obj_and_boundary(tg::ellipse3(pos30, m3));
+    test_obj_and_boundary(tg::ellipse2in3(pos30, m23));
+    // halfspace
+    test_obj(tg::halfspace2(n2, h));
+    test_obj(tg::halfspace3(n3, h));
+    // hemisphere
+    test_obj_and_boundary_no_caps(tg::hemisphere2(pos20, r, n2));
+    test_obj_and_boundary_no_caps(tg::hemisphere3(pos30, r, n3));
+    // inf_cone
+    test_obj_and_boundary(tg::inf_cone2(pos20, n2, a));
+    test_obj_and_boundary(tg::inf_cone3(pos30, n3, a));
+    // inf_cylinder
+    test_obj_and_boundary(tg::inf_cylinder2(tg::line2(pos20, n2), r));
+    test_obj_and_boundary(tg::inf_cylinder3(tg::line3(pos30, n3), r));
+    // line
+    test_obj(tg::line2(pos20, n2));
+    test_obj(tg::line3(pos30, n3));
+    // plane
+    test_obj(tg::plane2(n2, h));
+    test_obj(tg::plane3(n3, h));
+    // pyramid
+    test_obj_and_boundary_no_caps(tg::pyramid<tg::box2in3>(tg::box2in3(pos30, m23), h));
+    test_obj_and_boundary_no_caps(tg::pyramid<tg::sphere2in3>(disk0, h)); // == cone
+    test_obj_and_boundary_no_caps(tg::pyramid<tg::triangle3>(tg::triangle3(pos30, pos31, pos32), h));
+    test_obj(tg::pyramid_boundary_no_caps<tg::quad3>(tg::quad3(pos30, pos31, pos32, pos32 + (pos30 - pos31)), h));
+    // test_obj_and_boundary_no_caps(tg::pyramid<tg::quad3>(tg::quad3(pos30, pos31, pos32, pos32 + (pos30 - pos31)), h));
+    // TODO: quad
+    // test_obj(tg::quad2(pos20, pos21, pos22, pos23));
+    // test_obj(tg::quad3(pos30, pos31, pos32, pos32 + (pos31 - pos30)));
+    // ray
+    test_obj(tg::ray2(pos20, n2));
+    test_obj(tg::ray3(pos30, n3));
+    // segment
+    test_obj(tg::segment2(pos20, pos21));
+    test_obj(tg::segment3(pos30, pos31));
+    // sphere
+    test_obj_and_boundary(tg::sphere2(pos20, r));
+    test_obj_and_boundary(tg::sphere3(pos30, r));
+    test_obj_and_boundary(tg::sphere2in3(pos30, r, n3));
+    // triangle
+    test_obj(tg::triangle2(pos20, pos21, pos22));
+    test_obj(tg::triangle3(pos30, pos31, pos32));
+}