diff --git a/extern/typed-geometry b/extern/typed-geometry
index a6c6371f1bac47f0f269032549adb1873d1f32e4..3f36234671dc3fbead5b0398912809e4c803df2c 160000
--- a/extern/typed-geometry
+++ b/extern/typed-geometry
@@ -1 +1 @@
-Subproject commit a6c6371f1bac47f0f269032549adb1873d1f32e4
+Subproject commit 3f36234671dc3fbead5b0398912809e4c803df2c
diff --git a/samples/04-object-properties/main.cc b/samples/04-object-properties/main.cc
new file mode 100644
index 0000000000000000000000000000000000000000..5119f84477f2b18e8c9504428a8645f91f616a7c
--- /dev/null
+++ b/samples/04-object-properties/main.cc
@@ -0,0 +1,291 @@
+#include <typed-geometry/tg.hh>
+
+#include <glow-extras/glfw/GlfwContext.hh>
+#include <glow-extras/viewer/view.hh>
+
+template <class Rng, class Obj>
+void test_obj(Rng& rng, Obj const& obj)
+{
+    auto const domainD = tg::object_traits<Obj>::domain_dimension;
+    auto const objectD = tg::object_traits<Obj>::object_dimension;
+    auto const solidD = [&] {
+        if constexpr (tg::object_traits<Obj>::is_boundary)
+            return objectD + 1;
+        return objectD;
+    }();
+    using TraitsT = typename tg::object_traits<Obj>::tag_t;
+
+    tg::aabb<domainD, float> aabbBigger;
+    auto nameString = tg::to_string("");
+
+    auto g = gv::grid();
+
+    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 (auto i = 0; i < size; ++i)
+            uniformPts.emplace_back(uniform(rng, obj));
+
+        auto const aabb = aabb_of(obj);
+        aabbBigger = tg::aabb(aabb.min - 0.25f, aabb.max + 0.25f);
+        tg::aabb3 aabb3;
+        if constexpr (domainD == 3)
+            aabb3 = aabb;
+        else
+            aabb3 = tg::aabb3(tg::pos3(aabb.min), tg::pos3(aabb.max));
+
+        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
+        gv::view(gv::lines(aabb3));
+
+        // normal_of
+        if constexpr (domainD == 3 && solidD == 2)
+            gv::view(tg::segment3(centroid, centroid + normal_of(obj)), tg::color4::green);
+    }
+    else // infinite objects
+    {
+        auto const p = any_point(obj);
+        aabbBigger = tg::aabb<domainD, float>(p - 10.f, p + 10.f);
+        nameString = " for " + tg::to_string(obj);
+    }
+
+    // common visualization for finite and infinite objects
+    {
+        auto pointsInside = std::vector<tg::pos3>();
+        auto pointsOutside = std::vector<tg::pos3>();
+        auto const numPts = tg::pow(20.f, domainD);
+        auto const tolerance = objectD < domainD ? 0.1f : 0.f;
+        for (auto i = 0; i < numPts; ++i)
+        {
+            auto const p = uniform(rng, aabbBigger);
+            if (contains(obj, p, tolerance))
+                pointsInside.emplace_back(p);
+            else
+                pointsOutside.emplace_back(p);
+        }
+
+        // contains
+        auto v = gv::view();
+        view(pointsInside, gv::maybe_empty, tg::color4::green, "contains (+-" + tg::to_string(tolerance) + ")" + nameString);
+        gv::view(gv::points(pointsOutside).point_size_px(5.f));
+    }
+
+    if constexpr (!std::is_same_v<Obj, tg::ellipse<solidD, float, domainD, TraitsT>>)
+    {
+        auto points = std::vector<tg::pos3>();
+        auto lines = std::vector<tg::segment3>();
+        auto const numPts = tg::pow(20.f, domainD);
+        for (auto i = 0; i < numPts; ++i)
+        {
+            auto const p = uniform(rng, aabbBigger);
+            auto const pProj = tg::pos3(project(p, obj));
+            points.emplace_back(pProj);
+            if (!contains(obj, p))
+                lines.emplace_back(tg::pos3(p), pProj);
+        }
+
+        // project
+        auto v = gv::view();
+        gv::view(points, "project");
+        gv::view(lines);
+    }
+
+    if constexpr (objectD >= domainD - 1)
+    {
+        auto linesGray = std::vector<tg::segment3>();
+        auto linesGreen = std::vector<tg::segment3>();
+        auto linesRed = std::vector<tg::segment3>();
+        auto pointsGray = std::vector<tg::pos3>();
+        auto pointsGreen = std::vector<tg::pos3>();
+        auto pointsRed = std::vector<tg::pos3>();
+        auto const numPts = tg::pow(10.f, domainD);
+        for (auto i = 0; i < numPts; ++i)
+        {
+            auto const p = uniform(rng, aabbBigger);
+            auto const d = tg::uniform<tg::dir<domainD, float>>(rng);
+            auto const r = tg::ray(p, d);
+            if (intersects(r, obj))
+            {
+                if constexpr (std::is_same_v<decltype(tg::intersection_parameter(r, obj)), tg::optional<tg::hit_interval<float>>>)
+                {
+                    auto const ts = tg::intersection_parameter(r, obj).value();
+                    if (ts.start > 0.f)
+                    {
+                        pointsGray.emplace_back(p);
+                        linesGray.emplace_back(tg::pos3(p), tg::pos3(r[ts.start]));
+                    }
+                    pointsGreen.emplace_back(r[ts.start]);
+                    if (ts.end == tg::max<float>())
+                        linesGreen.emplace_back(tg::pos3(r[ts.start]), tg::pos3(r[ts.start] + d));
+                    else
+                    {
+                        linesGreen.emplace_back(tg::pos3(r[ts.start]), tg::pos3(r[ts.end]));
+                        pointsGreen.emplace_back(r[ts.end]);
+                        linesGray.emplace_back(tg::pos3(r[ts.end]), tg::pos3(r[ts.end] + d));
+                    }
+                }
+                else
+                {
+                    auto const ts = tg::intersection_parameter(r, obj);
+                    pointsGray.emplace_back(p);
+                    for (auto const& t : ts)
+                        pointsGreen.emplace_back(r[t]);
+                    linesGray.emplace_back(tg::pos3(p), tg::pos3(r[ts.last()] + d));
+                }
+            }
+            else
+            {
+                pointsRed.emplace_back(p);
+                linesRed.emplace_back(tg::pos3(p), tg::pos3(p + d));
+            }
+        }
+
+        // ray intersection
+        auto v = gv::view();
+        gv::view(linesGray, "ray intersection", tg::aabb3(tg::pos3(aabbBigger.min), tg::pos3(aabbBigger.max)));
+        view(linesGreen, gv::maybe_empty, tg::color4::green);
+        gv::view(linesRed, tg::color4::red);
+        gv::view(pointsGray);
+        gv::view(pointsGreen, tg::color4::green);
+        gv::view(pointsRed, tg::color4::red);
+    }
+}
+
+int main()
+{
+    auto const test_obj_and_boundary = [](auto& rng, auto const& o) {
+        test_obj(rng, o);
+        test_obj(rng, boundary_of(o));
+    };
+
+    auto const test_obj_and_boundary_no_caps = [](auto& rng, auto const& o) {
+        test_obj(rng, o);
+        test_obj(rng, boundary_of(o));
+        test_obj(rng, 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 = uniform(rng, 5_deg, 180_deg); // sensible range for a convex inf_cone
+    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(rng, aabb_of(pos20, pos21));
+    test_obj_and_boundary(rng, aabb_of(pos30, pos31));
+    // box
+    test_obj_and_boundary(rng, tg::box2(pos20, m2));
+    test_obj_and_boundary(rng, tg::box3(pos30, m3));
+    test_obj_and_boundary(rng, tg::box2in3(pos30, m23));
+    // capsule
+    test_obj_and_boundary(rng, tg::capsule3(axis0, r));
+    // cylinder
+    test_obj_and_boundary_no_caps(rng, tg::cylinder3(axis0, r));
+    // ellipse
+    test_obj_and_boundary(rng, tg::ellipse2(pos20, m2));
+    test_obj_and_boundary(rng, tg::ellipse3(pos30, m3));
+    test_obj_and_boundary(rng, tg::ellipse2in3(pos30, m23));
+    // halfspace
+    test_obj(rng, tg::halfspace2(n2, h));
+    test_obj(rng, tg::halfspace3(n3, h));
+    // hemisphere
+    test_obj_and_boundary_no_caps(rng, tg::hemisphere2(pos20, r, n2));
+    test_obj_and_boundary_no_caps(rng, tg::hemisphere3(pos30, r, n3));
+    // inf_cone
+    test_obj_and_boundary(rng, tg::inf_cone2(pos20, n2, a));
+    test_obj_and_boundary(rng, tg::inf_cone3(pos30, n3, a));
+    // inf_cylinder
+    test_obj_and_boundary(rng, tg::inf_cylinder2(tg::line2(pos20, n2), r));
+    test_obj_and_boundary(rng, tg::inf_cylinder3(tg::line3(pos30, n3), r));
+    // line
+    test_obj(rng, tg::line2(pos20, n2));
+    test_obj(rng, tg::line3(pos30, n3));
+    // plane
+    test_obj(rng, tg::plane2(n2, h));
+    test_obj(rng, tg::plane3(n3, h));
+    // pyramid
+    test_obj_and_boundary_no_caps(rng, tg::pyramid<tg::box2in3>(tg::box2in3(pos30, m23), h));
+    test_obj_and_boundary_no_caps(rng, tg::pyramid<tg::sphere2in3>(disk0, h)); // == cone
+    test_obj_and_boundary_no_caps(rng, tg::pyramid<tg::triangle3>(tg::triangle3(pos30, pos31, pos32), h));
+    test_obj(rng, 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(rng, tg::quad2(pos20, pos21, pos22, pos23));
+    // test_obj(rng, tg::quad3(pos30, pos31, pos32, pos32 + (pos31 - pos30)));
+    // ray
+    test_obj(rng, tg::ray2(pos20, n2));
+    test_obj(rng, tg::ray3(pos30, n3));
+    // segment
+    test_obj(rng, tg::segment2(pos20, pos21));
+    test_obj(rng, tg::segment3(pos30, pos31));
+    // sphere
+    test_obj_and_boundary(rng, tg::sphere2(pos20, r));
+    test_obj_and_boundary(rng, tg::sphere3(pos30, r));
+    test_obj_and_boundary(rng, tg::sphere2in3(pos30, r, n3));
+    // triangle
+    test_obj(rng, tg::triangle2(pos20, pos21, pos22));
+    test_obj(rng, tg::triangle3(pos30, pos31, pos32));
+}