diff --git a/extern/glow-extras b/extern/glow-extras
index 3097d90d45c6778ed72911f64ce3e08f9f5c6eb0..f53a709b1a5926ce12dd00d3514d7d730c859748 160000
--- a/extern/glow-extras
+++ b/extern/glow-extras
@@ -1 +1 @@
-Subproject commit 3097d90d45c6778ed72911f64ce3e08f9f5c6eb0
+Subproject commit f53a709b1a5926ce12dd00d3514d7d730c859748
diff --git a/extern/polymesh b/extern/polymesh
index 65025e1af6e6abb89401e0337d49310b01ba8833..5719ed1860a475ddfcf3d7119027e4bd7910b0a9 160000
--- a/extern/polymesh
+++ b/extern/polymesh
@@ -1 +1 @@
-Subproject commit 65025e1af6e6abb89401e0337d49310b01ba8833
+Subproject commit 5719ed1860a475ddfcf3d7119027e4bd7910b0a9
diff --git a/extern/typed-geometry b/extern/typed-geometry
index f8a5e56373e3da67e7ea543424d0704950cbdfc1..23d12b23ad62e2c0bef0be2d569e100dca2ce846 160000
--- a/extern/typed-geometry
+++ b/extern/typed-geometry
@@ -1 +1 @@
-Subproject commit f8a5e56373e3da67e7ea543424d0704950cbdfc1
+Subproject commit 23d12b23ad62e2c0bef0be2d569e100dca2ce846
diff --git a/samples/03-triangle-properties/main.cc b/samples/03-triangle-properties/main.cc
new file mode 100644
index 0000000000000000000000000000000000000000..fce193d893fa5c62b908835fea043567480370b2
--- /dev/null
+++ b/samples/03-triangle-properties/main.cc
@@ -0,0 +1,41 @@
+#include <typed-geometry/tg.hh>
+
+#include <imgui/imgui.h>
+
+#include <glow-extras/glfw/GlfwContext.hh>
+#include <glow-extras/viewer/canvas.hh>
+#include <glow-extras/viewer/view.hh>
+
+int main()
+{
+    glow::glfw::GlfwContext ctx;
+
+    tg::triangle3 t = {{1, 0, -2}, {-1, 0, -2}, {0, 0, 1.5f}};
+
+    gv::interactive([&](float) {
+        ImGui::SliderFloat3("pos0", &t.pos0.x, -2, 2);
+        ImGui::SliderFloat3("pos1", &t.pos1.x, -2, 2);
+        ImGui::SliderFloat3("pos2", &t.pos2.x, -2, 2);
+
+        ImGui::Separator();
+        ImGui::Text("Area: %f", tg::area_of(t));
+        ImGui::Text("Perimeter: %f", tg::perimeter_of(t));
+        ImGui::Text("Circumradius: %f", tg::circumradius_of(t));
+        ImGui::Text("Inradius: %f", tg::inradius_of(t));
+
+        auto c = gv::canvas();
+        c.add_lines(t);
+        c.add_face(t);
+
+        c.add_point(tg::centroid_of(t), tg::color3::red);
+        c.add_label(tg::centroid_of(t), "centroid");
+
+        c.add_point(tg::circumcenter_of(t), tg::color3::blue);
+        c.add_lines(tg::circumcircle_of(t), tg::color3::blue);
+        c.add_label(tg::circumcenter_of(t), "circumcenter");
+
+        c.add_point(tg::incenter_of(t), tg::color3::green);
+        c.add_lines(tg::incircle_of(t), tg::color3::green);
+        c.add_label(tg::incenter_of(t), "incenter");
+    });
+}
diff --git a/tests/feature/objects/triangles.cc b/tests/feature/objects/triangles.cc
new file mode 100644
index 0000000000000000000000000000000000000000..4a36bf4a57845ad9ac43d8c45b321d48b1818d1d
--- /dev/null
+++ b/tests/feature/objects/triangles.cc
@@ -0,0 +1,53 @@
+#include "test.hh"
+
+TG_FUZZ_TEST(Triangle, Circles)
+{
+    auto const execute_test = [&](auto bb) {
+        auto const p0 = uniform(rng, bb);
+        auto const p1 = uniform(rng, bb);
+        auto const p2 = uniform(rng, bb);
+
+        auto const t = tg::triangle(p0, p1, p2);
+
+        auto const p = tg::perimeter_of(t);
+
+        CHECK(distance(p0, p1) + distance(p1, p2) + distance(p2, p0) == approx(p).epsilon(0.001f));
+
+        //
+        // circumcenter
+        //
+        if (tg::min_height_of(t) > 0.1f) // flat triangles have extreme/unstable circles
+        {
+            auto const cc = tg::circumcenter_of(t);
+            auto const cs = tg::circumcircle_of(t);
+            auto const cr = tg::circumradius_of(t);
+
+            CHECK(distance(p0, cc) == approx(distance(p1, cc)).epsilon(0.001f));
+            CHECK(distance(p1, cc) == approx(distance(p2, cc)).epsilon(0.001f));
+            CHECK(distance(p2, cc) == approx(distance(p0, cc)).epsilon(0.001f));
+
+            CHECK(cs.radius == approx(distance(p0, cc)).epsilon(0.001f));
+            CHECK(cs.center == approx(cc, 0.001f));
+            CHECK(cr == approx(cs.radius).epsilon(0.001f));
+        }
+
+        //
+        // incenter
+        //
+        if (tg::min_height_of(t) > 0.1f) // flat triangles have unstable circles
+        {
+            auto const ic = tg::incenter_of(t);
+            auto const is = tg::incircle_of(t);
+            auto const ir = tg::inradius_of(t);
+
+            for (auto e : tg::edges_of(t))
+                CHECK(distance(e, ic) == approx(ir).epsilon(0.001f));
+
+            CHECK(is.center == approx(ic, 0.001f));
+            CHECK(ir == approx(is.radius).epsilon(0.001f));
+        }
+    };
+
+    execute_test(tg::aabb2(-10, 10));
+    execute_test(tg::aabb3(-10, 10));
+}