diff --git a/src/polymesh/algorithms/cache-optimization.hh b/src/polymesh/algorithms/cache-optimization.hh
index 9e9f04c8a5d50ed8e9881bf49f023c559acf59bb..fa6e5991a0c1ac44c2b62f851f4e682e9d438b28 100644
--- a/src/polymesh/algorithms/cache-optimization.hh
+++ b/src/polymesh/algorithms/cache-optimization.hh
@@ -2,7 +2,7 @@
 
 #include <vector>
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 
 namespace polymesh
 {
diff --git a/src/polymesh/algorithms/components.hh b/src/polymesh/algorithms/components.hh
index 03aa1c23deb9e79d033cac18952cc5f3b9c9cd74..57e984ccdb59bb6178c78ccff5cced9a17563771 100644
--- a/src/polymesh/algorithms/components.hh
+++ b/src/polymesh/algorithms/components.hh
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 #include "../detail/primitive_set.hh"
 #include "../detail/topology_iterator.hh"
 
diff --git a/src/polymesh/algorithms/interpolation.hh b/src/polymesh/algorithms/interpolation.hh
index 6263237d56b7c74fdce00e1548c975a564cc68d7..e41d69176c8a8d78102c23e79480df9e8b69db2a 100644
--- a/src/polymesh/algorithms/interpolation.hh
+++ b/src/polymesh/algorithms/interpolation.hh
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 
 namespace polymesh
 {
diff --git a/src/polymesh/algorithms/normalize.hh b/src/polymesh/algorithms/normalize.hh
index 970d524aa736a9820a7ba26b1baad0c95257b74e..da713a8282483677658bf17e379d0a45001a646f 100644
--- a/src/polymesh/algorithms/normalize.hh
+++ b/src/polymesh/algorithms/normalize.hh
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 #include "../fields.hh"
 
 namespace polymesh
diff --git a/src/polymesh/algorithms/topology.hh b/src/polymesh/algorithms/topology.hh
index 9be358bafd6f3b6ea410bdebc38240e92b5373e9..6d3c53b79c46decdf5778e88e55d0abe3c95382c 100644
--- a/src/polymesh/algorithms/topology.hh
+++ b/src/polymesh/algorithms/topology.hh
@@ -2,7 +2,7 @@
 
 #include <vector>
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 
 namespace polymesh
 {
diff --git a/src/polymesh/algorithms/tracing.hh b/src/polymesh/algorithms/tracing.hh
index d757a17af723e8927a6763e2d8da7a5370080c15..66c74c70377c8ad2ea2769258231faf14eef391b 100644
--- a/src/polymesh/algorithms/tracing.hh
+++ b/src/polymesh/algorithms/tracing.hh
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 #include "../fields.hh"
 
 namespace polymesh
diff --git a/src/polymesh/attributes.hh b/src/polymesh/attributes.hh
index cf3635e4ac607415e6beeef947a0d95d6a50be30..d30c5b815477cd39b72f3440aaa121311bfee87a 100644
--- a/src/polymesh/attributes.hh
+++ b/src/polymesh/attributes.hh
@@ -73,6 +73,8 @@ public:
 
     int size() const;
     int capacity() const;
+    size_t byte_size() const override { return size() * sizeof(AttrT); }
+    size_t allocated_byte_size() const override { return capacity() * sizeof(AttrT); }
 
     attribute_iterator<primitive_attribute> begin() { return {0, size(), *this}; }
     attribute_iterator<primitive_attribute const&> begin() const { return {0, size(), *this}; }
@@ -146,8 +148,6 @@ protected:
 protected:
     void resize_from(int old_size) override;
     void clear_with_default() override;
-    size_t byte_size() const override { return size() * sizeof(AttrT); }
-    size_t allocated_byte_size() const override { return capacity() * sizeof(AttrT); }
 
     void apply_remapping(std::vector<int> const& map) override;
     void apply_transpositions(std::vector<std::pair<int, int>> const& ts) override;
@@ -231,6 +231,13 @@ struct halfedge_attribute final : primitive_attribute<halfedge_tag, AttrT>
     friend struct smart_collection;
 };
 
+/**
+ * creates a new attribute from a primitive collection.
+ *
+ * Usage:
+ *
+ *   auto pos = attribute(m.vertices(), tg::pos3::zero);
+ */
 template <class AttrT, class Collection>
 auto attribute(Collection const& c, AttrT const& defaultValue = {}) -> decltype(c.make_attribute(defaultValue))
 {
diff --git a/src/polymesh/attributes/flags.hh b/src/polymesh/attributes/flags.hh
index ea87558e307f20d953b9b73471dd4c082415ccf3..8f182ff050ef25b656005380607c5ddb8c622603 100644
--- a/src/polymesh/attributes/flags.hh
+++ b/src/polymesh/attributes/flags.hh
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "../attributes.hh"
+#include <polymesh/attributes.hh>
 
 namespace polymesh
 {
diff --git a/src/polymesh/attributes/partitioning.hh b/src/polymesh/attributes/partitioning.hh
index e38947261f3f6aefbe2fe7693ed7ca99a985abee..0b674482e7b34de0278b8f8cde008e6004f4656d 100644
--- a/src/polymesh/attributes/partitioning.hh
+++ b/src/polymesh/attributes/partitioning.hh
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 
 namespace polymesh
 {
diff --git a/src/polymesh/attributes/raw_attribute.hh b/src/polymesh/attributes/raw_attribute.hh
new file mode 100644
index 0000000000000000000000000000000000000000..ca5a1243632126f581b69037b2ea118bd200b8d5
--- /dev/null
+++ b/src/polymesh/attributes/raw_attribute.hh
@@ -0,0 +1,275 @@
+#pragma once
+
+#include <cstring>
+
+#include <polymesh/Mesh.hh>
+#include <polymesh/attributes.hh>
+#include <polymesh/span.hh>
+
+namespace polymesh
+{
+/**
+ * Raw attributes are attributes that store a fixed (but runtime determined) number of bytes per primitive
+ * The number of bytes per primitive is called stride
+ *
+ * NOTE: they are always zero-initialized
+ *
+ * TODO: iteration
+ * TODO: smart range
+ */
+template <class tag>
+struct raw_primitive_attribute : primitive_attribute_base<tag>
+{
+    template <class A>
+    using attribute = typename primitive<tag>::template attribute<A>;
+    using index_t = typename primitive<tag>::index;
+    using handle_t = typename primitive<tag>::handle;
+    using tag_t = tag;
+
+    // data access
+public:
+    span<std::byte> operator[](handle_t h)
+    {
+        POLYMESH_ASSERT(this->mMesh == h.mesh && "Handle belongs to a different mesh");
+        POLYMESH_ASSERT(0 <= h.idx.value && h.idx.value < this->size() && "out of bounds");
+        return {this->mData.get() + h.idx.value * mStride, mStride};
+    }
+    span<std::byte const> operator[](handle_t h) const
+    {
+        POLYMESH_ASSERT(this->mMesh == h.mesh && "Handle belongs to a different mesh");
+        POLYMESH_ASSERT(0 <= h.idx.value && h.idx.value < this->size() && "out of bounds");
+        return {this->mData.get() + h.idx.value * mStride, mStride};
+    }
+    span<std::byte> operator[](index_t h)
+    {
+        POLYMESH_ASSERT(h.is_valid());
+        return {this->mData.get() + h.value * mStride, mStride};
+    }
+    span<std::byte const> operator[](index_t h) const
+    {
+        POLYMESH_ASSERT(h.is_valid());
+        return {this->mData.get() + h.value * mStride, mStride};
+    }
+
+    span<std::byte> operator()(handle_t h)
+    {
+        POLYMESH_ASSERT(this->mMesh == h.mesh && "Handle belongs to a different mesh");
+        POLYMESH_ASSERT(0 <= h.idx.value && h.idx.value < this->size() && "out of bounds");
+        return {this->mData.get() + h.idx.value * mStride, mStride};
+    }
+    span<std::byte const> operator()(handle_t h) const
+    {
+        POLYMESH_ASSERT(this->mMesh == h.mesh && "Handle belongs to a different mesh");
+        POLYMESH_ASSERT(0 <= h.idx.value && h.idx.value < this->size() && "out of bounds");
+        return {this->mData.get() + h.idx.value * mStride, mStride};
+    }
+    span<std::byte> operator()(index_t h)
+    {
+        POLYMESH_ASSERT(h.is_valid());
+        return {this->mData.get() + h.value * mStride, mStride};
+    }
+
+    span<std::byte const> operator()(index_t h) const
+    {
+        POLYMESH_ASSERT(h.is_valid());
+        return {this->mData.get() + h.value * mStride, mStride};
+    }
+
+    std::byte* data() { return mData.get(); }
+    std::byte const* data() const { return mData.get(); }
+
+    int size() const { return primitive<tag>::all_size(*this->mMesh); }
+    int stride() const { return mStride; }
+    int capacity() const { return primitive<tag>::capacity(*this->mMesh); }
+    size_t byte_size() const override { return size() * mStride; }
+    size_t allocated_byte_size() const override { return capacity() * mStride; }
+
+    /// true iff this attribute is still attached to a mesh
+    /// do not use the attribute if not valid
+    bool is_valid() const { return this->mMesh != nullptr; }
+
+    // methods
+public:
+    /// sets all attribute values to zero
+    void clear()
+    {
+        if (byte_size() > 0)
+            std::memset(this->mData.get(), 0, byte_size());
+    }
+
+    /// Saves ALL data into a vector (includes possibly removed ones)
+    std::vector<std::byte> to_raw_vector() const
+    {
+        auto r = std::vector<std::byte>(byte_size());
+        if (r.size() > 0)
+            std::memcpy(r.data(), this->mData.get(), byte_size());
+        return r;
+    }
+
+    /// returns a new attribute where all elements were bitcast to the given type
+    /// NOTE: requires trivially copyable T with sizeof(T) == stride()
+    template <class T>
+    auto to() const -> attribute<T>
+    {
+        static_assert(std::is_trivially_copyable_v<T>, "only works for trivially copyable types");
+        POLYMESH_ASSERT(sizeof(T) == mStride && "stride must match exactly with type size");
+        auto a = attribute<T>(this->mesh());
+        if (byte_size() > 0)
+            std::memcpy(a.data(), this->mData.get(), byte_size());
+        return a;
+    }
+
+    // public ctor
+public:
+    raw_primitive_attribute() = default;
+    raw_primitive_attribute(Mesh const& mesh, int stride) : primitive_attribute_base<tag>(&mesh), mStride(stride)
+    {
+        POLYMESH_ASSERT(mStride > 0 && "only positive stride supported");
+
+        // register
+        this->register_attr();
+
+        // alloc data (zero-init)
+        this->mData.reset(new std::byte[this->capacity() * mStride]());
+    }
+
+    // members
+protected:
+    unique_array<std::byte> mData;
+    size_t mStride = 0; ///< number of bytes per element
+
+protected:
+    void resize_from(int old_size) override
+    {
+        // mesh is already resized, thus capacity() and size() return new values
+        // old_size is size before resize
+
+        auto new_capacity = this->capacity();
+        auto shared_size = std::min(this->size(), old_size);
+        POLYMESH_ASSERT(shared_size <= new_capacity && "size cannot exceed capacity");
+
+        // alloc new data
+        auto new_data = new_capacity > 0 ? new std::byte[new_capacity * mStride]() : nullptr;
+
+        // copy shared region to new data
+        if (shared_size > 0)
+            std::memcpy(new_data, this->mData.get(), shared_size * mStride);
+
+        // replace old data
+        this->mData.reset(new_data);
+    }
+    void clear_with_default() override { std::memset(this->mData.get(), 0, byte_size()); }
+
+    void apply_remapping(std::vector<int> const& map) override
+    {
+        // TODO: could be made faster by special casing a few stride sizes
+        for (auto i = 0u; i < map.size(); ++i)
+            std::memcpy(&this->mData[i], &this->mData[map[i]], mStride);
+    }
+    void apply_transpositions(std::vector<std::pair<int, int>> const& ts) override
+    {
+        for (auto t : ts)
+            for (auto i = 0u; i < mStride; ++i)
+                swap(this->mData[t.first * mStride + i], this->mData[t.second * mStride + i]);
+    }
+
+    template <class MeshT>
+    friend struct low_level_attribute_api;
+
+    // move & copy
+public:
+    raw_primitive_attribute(raw_primitive_attribute const& rhs) noexcept // copy
+      : primitive_attribute_base<tag>(rhs.mMesh), mStride(rhs.mStride)
+    {
+        // register attr
+        this->register_attr();
+
+        // alloc data
+        this->mData.reset(new std::byte[this->capacity() * mStride]());
+
+        // copy valid data
+        std::memcpy(this->mData.get(), rhs.mData.get(), rhs.byte_size());
+    }
+    raw_primitive_attribute(raw_primitive_attribute&& rhs) noexcept // move
+      : primitive_attribute_base<tag>(rhs.mMesh), mStride(rhs.mStride)
+    {
+        // take data from rhs
+        this->mData = std::move(rhs.mData);
+
+        // deregister rhs
+        rhs.deregister_attr();
+
+        // register lhs
+        this->register_attr();
+    }
+    raw_primitive_attribute& operator=(raw_primitive_attribute const& rhs) noexcept // copy assign
+    {
+        if (this == &rhs) // prevent self-copy
+            return *this;
+
+        // save old capacity for no-realloc path
+        auto old_capacity_bytes = is_valid() ? this->capacity() * mStride : 0;
+
+        // deregister from old mesh
+        this->deregister_attr();
+
+        // register into new mesh
+        this->mMesh = rhs.mMesh;
+        this->mStride = rhs.mStride;
+        this->register_attr();
+
+        // realloc if new capacity
+        auto new_capacity_bytes = this->capacity() * mStride;
+        if (old_capacity_bytes != new_capacity_bytes)
+        {
+            if (new_capacity_bytes == 0)
+                this->mData.reset();
+            else
+                this->mData.reset(new std::byte[new_capacity_bytes]());
+        }
+
+        // copy valid AND defaulted data
+        std::memcpy(this->mData.get(), rhs.mData.get(), new_capacity_bytes);
+
+        return *this;
+    }
+    raw_primitive_attribute& operator=(raw_primitive_attribute&& rhs) noexcept // move assign
+    {
+        if (this == &rhs) // prevent self-move
+            return *this;
+
+        // deregister from old mesh
+        this->deregister_attr();
+
+        // register into new mesh
+        this->mMesh = rhs.mMesh;
+        this->register_attr();
+
+        // take data of rhs
+        this->mStride = rhs.mStride;
+        this->mData = std::move(rhs.mData);
+
+        // deregister rhs
+        rhs.deregister_attr();
+
+        return *this;
+    }
+};
+
+struct raw_vertex_attribute final : raw_primitive_attribute<vertex_tag>
+{
+    using raw_primitive_attribute<vertex_tag>::raw_primitive_attribute;
+};
+struct raw_face_attribute final : raw_primitive_attribute<face_tag>
+{
+    using raw_primitive_attribute<face_tag>::raw_primitive_attribute;
+};
+struct raw_edge_attribute final : raw_primitive_attribute<edge_tag>
+{
+    using raw_primitive_attribute<edge_tag>::raw_primitive_attribute;
+};
+struct raw_halfedge_attribute final : raw_primitive_attribute<halfedge_tag>
+{
+    using raw_primitive_attribute<halfedge_tag>::raw_primitive_attribute;
+};
+}
diff --git a/src/polymesh/detail/topology_iterator.hh b/src/polymesh/detail/topology_iterator.hh
index 7dbeb6bb27ce35cb801f4e3673700ec6262fcb6f..90bafd0e0905a4b0c3b6e16b5da0688929a3b371 100644
--- a/src/polymesh/detail/topology_iterator.hh
+++ b/src/polymesh/detail/topology_iterator.hh
@@ -2,7 +2,7 @@
 
 #include <queue>
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 #include "primitive_set.hh"
 
 /// CAUTION: these iterators do NOT work like normal iterators where you can make copies!
diff --git a/src/polymesh/detail/unique_array.hh b/src/polymesh/detail/unique_array.hh
index ff82441de80384d0d998c74f08b277dffa17caf7..1d316b82dc16df7a8f10a42bfc9d5be28bd71bc5 100644
--- a/src/polymesh/detail/unique_array.hh
+++ b/src/polymesh/detail/unique_array.hh
@@ -11,7 +11,7 @@ struct unique_array
     using element_type = T;
 
     unique_array() = default;
-    explicit unique_array(int size) { ptr = new T[size]; }
+    explicit unique_array(int size) { ptr = new T[size](); }
     ~unique_array()
     {
         delete[] ptr;
diff --git a/src/polymesh/formats/obj.hh b/src/polymesh/formats/obj.hh
index d61ecbecd08c866fc31fddabaf190b47fe9a5a31..eb56a424e91ddef715847e6a624a6d2d3ab665ea 100644
--- a/src/polymesh/formats/obj.hh
+++ b/src/polymesh/formats/obj.hh
@@ -3,7 +3,7 @@
 #include <iosfwd>
 #include <string>
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 
 namespace polymesh
 {
diff --git a/src/polymesh/formats/off.hh b/src/polymesh/formats/off.hh
index 7847a862392c8cb88134817936905f6a40b49073..18ea1a42ad03aa031c29d919c3886fe143037161 100644
--- a/src/polymesh/formats/off.hh
+++ b/src/polymesh/formats/off.hh
@@ -4,7 +4,7 @@
 #include <iosfwd>
 #include <string>
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 
 namespace polymesh
 {
diff --git a/src/polymesh/formats/ply.hh b/src/polymesh/formats/ply.hh
index 9f890e2b26468cd70ec2e768ed8868d4a23bd6be..76b0b1b944a9acef19ba4bdd146f23ebd41ce408 100644
--- a/src/polymesh/formats/ply.hh
+++ b/src/polymesh/formats/ply.hh
@@ -3,7 +3,7 @@
 #include <iosfwd>
 #include <string>
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 
 namespace polymesh
 {
diff --git a/src/polymesh/formats/stl.hh b/src/polymesh/formats/stl.hh
index 347318acd92d7d758c71637a0fb1f45ffba84f7a..0c5a364697a1f6e2e0a500c3e540253b5f2289bc 100644
--- a/src/polymesh/formats/stl.hh
+++ b/src/polymesh/formats/stl.hh
@@ -4,7 +4,7 @@
 #include <iosfwd>
 #include <string>
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 
 namespace polymesh
 {
diff --git a/src/polymesh/impl/impl_attributes.hh b/src/polymesh/impl/impl_attributes.hh
index e17c5d5e9a107912bea02a8c970ecc7e3d5cf000..b8aa5a8eb200ee0e8b0f151d19d60b37ed1ec826 100644
--- a/src/polymesh/impl/impl_attributes.hh
+++ b/src/polymesh/impl/impl_attributes.hh
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 
 namespace polymesh
 {
@@ -189,7 +189,7 @@ primitive_attribute<tag, AttrT>& primitive_attribute<tag, AttrT>::operator=(prim
         if (new_capacity == 0)
             this->mData.reset();
         else
-            this->mData.reset(new AttrT[new_capacity]);
+            this->mData.reset(new AttrT[new_capacity]());
     }
 
     // copy ALL data (valid and defaulted)
@@ -234,7 +234,7 @@ void primitive_attribute<tag, AttrT>::resize_from(int old_size)
     POLYMESH_ASSERT(shared_size <= new_capacity && "size cannot exceed capacity");
 
     // alloc new data
-    auto new_data = new_capacity > 0 ? new AttrT[new_capacity] : nullptr;
+    auto new_data = new_capacity > 0 ? new AttrT[new_capacity]() : nullptr;
 
     // copy shared region to new data
     std::copy_n(this->mData.get(), shared_size, new_data);
diff --git a/src/polymesh/impl/impl_cursors.hh b/src/polymesh/impl/impl_cursors.hh
index 541efe5bffb51a725d4834f134ad8529c7708137..78ee003203aacce0236f5cc58701156f996a5d9d 100644
--- a/src/polymesh/impl/impl_cursors.hh
+++ b/src/polymesh/impl/impl_cursors.hh
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 
 namespace polymesh
 {
diff --git a/src/polymesh/impl/impl_low_level_api_base.hh b/src/polymesh/impl/impl_low_level_api_base.hh
index 7bdd5f93d135eb79c568a15a1eba86f0ac8da142..a9665a0f97fb42fb8d1542aee23691ad3f1b2474 100644
--- a/src/polymesh/impl/impl_low_level_api_base.hh
+++ b/src/polymesh/impl/impl_low_level_api_base.hh
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 
 namespace polymesh
 {
diff --git a/src/polymesh/impl/impl_low_level_api_mutable.hh b/src/polymesh/impl/impl_low_level_api_mutable.hh
index 369c92833f1c5cd1ca5aea9fe705fbe4b14308a9..b0987e39701d3e57f86ba5ab1592aa49f2156b13 100644
--- a/src/polymesh/impl/impl_low_level_api_mutable.hh
+++ b/src/polymesh/impl/impl_low_level_api_mutable.hh
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 
 namespace polymesh
 {
diff --git a/src/polymesh/impl/impl_mesh.hh b/src/polymesh/impl/impl_mesh.hh
index 892f15ab4dcfa052ba27ae6c6b0d5ee049b93bf3..5af5703acc73fa35bc73673c8222375c413f317c 100644
--- a/src/polymesh/impl/impl_mesh.hh
+++ b/src/polymesh/impl/impl_mesh.hh
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 #include "../assert.hh"
 #include "../detail/split_vector.hh"
 
diff --git a/src/polymesh/impl/impl_primitive.hh b/src/polymesh/impl/impl_primitive.hh
index 17d3e0437b3d608c4a31277b2999e5cd7dea9c7c..6aec17e96661f1ee968d2148f056cbdbaffc9a97 100644
--- a/src/polymesh/impl/impl_primitive.hh
+++ b/src/polymesh/impl/impl_primitive.hh
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 
 namespace polymesh
 {
diff --git a/src/polymesh/impl/impl_ranges.hh b/src/polymesh/impl/impl_ranges.hh
index e834348c13a56e48558b8128f9577bbc3289b2d2..5725bce6b4591302f47189d0c1f1accf65ce1aae 100644
--- a/src/polymesh/impl/impl_ranges.hh
+++ b/src/polymesh/impl/impl_ranges.hh
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 
 namespace polymesh
 {
diff --git a/src/polymesh/objects/cone.hh b/src/polymesh/objects/cone.hh
index 4e17284dc8d70d644be19a851a8af275dbc5d1e0..c2244eedf0063086fade2bf605e6db7f9d3b2efb 100644
--- a/src/polymesh/objects/cone.hh
+++ b/src/polymesh/objects/cone.hh
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 
 namespace polymesh::objects
 {
diff --git a/src/polymesh/objects/cube.hh b/src/polymesh/objects/cube.hh
index 5389783171a870d99edf581a006d69b37faf360f..5d0c1e026678c6efb86c24b2c2edc97c81a12747 100644
--- a/src/polymesh/objects/cube.hh
+++ b/src/polymesh/objects/cube.hh
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 #include "../fields.hh"
 
 namespace polymesh
diff --git a/src/polymesh/objects/cylinder.hh b/src/polymesh/objects/cylinder.hh
index 7e576b6cb9a717c3f0184d84deb1d45fd037e168..86dfa33614209fed0d25890449717b1a893eb76e 100644
--- a/src/polymesh/objects/cylinder.hh
+++ b/src/polymesh/objects/cylinder.hh
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 
 namespace polymesh::objects
 {
diff --git a/src/polymesh/objects/quad.hh b/src/polymesh/objects/quad.hh
index 229eeab9a0f4cea066c7c9ee5e9caa658e3c01b4..85433cbed95a36df28156fa459e156deebcc9239 100644
--- a/src/polymesh/objects/quad.hh
+++ b/src/polymesh/objects/quad.hh
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 
 namespace polymesh
 {
diff --git a/src/polymesh/objects/uv_sphere.hh b/src/polymesh/objects/uv_sphere.hh
index a44b2d9e2b3e47ae57d43a40a4392201a5118eb2..fbbea26ab815f9326f6390171ea392a8a0e9e462 100644
--- a/src/polymesh/objects/uv_sphere.hh
+++ b/src/polymesh/objects/uv_sphere.hh
@@ -1,6 +1,6 @@
 #pragma once
 
-#include "../Mesh.hh"
+#include <polymesh/Mesh.hh>
 
 namespace polymesh::objects
 {
diff --git a/src/polymesh/span.hh b/src/polymesh/span.hh
new file mode 100644
index 0000000000000000000000000000000000000000..db18e1a76196c5cd71055be3cf0102b2089fa532
--- /dev/null
+++ b/src/polymesh/span.hh
@@ -0,0 +1,80 @@
+#pragma once
+
+#include <cstddef>
+#include <type_traits>
+
+#include <polymesh/assert.hh>
+#include <polymesh/tmp.hh>
+
+namespace polymesh
+{
+// a non-owning view of a contiguous array of Ts
+// can be read and write (span<const T> vs span<T>)
+// is trivially copyable (and cheap)
+// NOTE: is range-checked via POLYMESH_ASSERT
+template <class T>
+struct span
+{
+    // ctors
+public:
+    constexpr span() = default;
+    constexpr span(T* data, size_t size) : _data(data), _size(size) {}
+    constexpr span(T* d_begin, T* d_end) : _data(d_begin), _size(d_end - d_begin) {}
+    template <size_t N>
+    constexpr span(T (&data)[N]) : _data(data), _size(N)
+    {
+    }
+    template <class Container, std::enable_if_t<tmp::is_contiguous_range<Container, T>, int> = 0>
+    constexpr span(Container&& c) : _data(c.data()), _size(c.size())
+    {
+    }
+
+    // container
+public:
+    constexpr T* begin() const { return _data; }
+    constexpr T* end() const { return _data + _size; }
+
+    constexpr T* data() const { return _data; }
+    constexpr size_t size() const { return _size; }
+    constexpr bool empty() const { return _size == 0; }
+
+    constexpr T& operator[](size_t i) const
+    {
+        POLYMESH_ASSERT(i < _size);
+        return _data[i];
+    }
+
+    constexpr T& front() const
+    {
+        POLYMESH_ASSERT(_size > 0);
+        return _data[0];
+    }
+    constexpr T& back() const
+    {
+        POLYMESH_ASSERT(_size > 0);
+        return _data[_size - 1];
+    }
+
+    // subviews
+public:
+    constexpr span first(size_t n) const
+    {
+        POLYMESH_ASSERT(n <= _size);
+        return {_data, n};
+    }
+    constexpr span last(size_t n) const
+    {
+        POLYMESH_ASSERT(n <= _size);
+        return {_data + (_size - n), n};
+    }
+    constexpr span subspan(size_t offset, size_t count) const
+    {
+        POLYMESH_ASSERT(offset + count <= _size);
+        return {_data + offset, count};
+    }
+
+private:
+    T* _data = nullptr;
+    size_t _size = 0;
+};
+}
diff --git a/src/polymesh/tmp.hh b/src/polymesh/tmp.hh
index 527247df3d7ea4b16e1aa031cbf5008e2a39696d..7801d2caf52d4440e8d35f6d3bea9baf80da20a5 100644
--- a/src/polymesh/tmp.hh
+++ b/src/polymesh/tmp.hh
@@ -1,5 +1,6 @@
 #pragma once
 
+#include <cstddef>
 #include <utility>
 
 namespace polymesh
@@ -99,5 +100,49 @@ struct constant_rational
     }
 };
 
+namespace detail
+{
+template <class Container, class ElementT>
+auto contiguous_range_test(Container* c) -> decltype(static_cast<ElementT*>(c->data()), static_cast<size_t>(c->size()), 0);
+template <class Container, class ElementT>
+char contiguous_range_test(...);
+
+template <class Container, class ElementT, class = void>
+struct is_range_t : std::false_type
+{
+};
+template <class ElementT, size_t N>
+struct is_range_t<ElementT[N], ElementT> : std::true_type
+{
+};
+template <class ElementT, size_t N>
+struct is_range_t<ElementT[N], ElementT const> : std::true_type
+{
+};
+template <class ElementT, size_t N>
+struct is_range_t<ElementT (&)[N], ElementT> : std::true_type
+{
+};
+template <class ElementT, size_t N>
+struct is_range_t<ElementT (&)[N], ElementT const> : std::true_type
+{
+};
+template <class Container, class ElementT>
+struct is_range_t<Container,
+                  ElementT,
+                  std::void_t<                                                              //
+                      decltype(static_cast<ElementT&>(*std::declval<Container>().begin())), //
+                      decltype(std::declval<Container>().end())                             //
+                      >> : std::true_type
+{
+};
+}
+
+template <class Container, class ElementT>
+static constexpr bool is_contiguous_range = sizeof(detail::contiguous_range_test<Container, ElementT>(nullptr)) == sizeof(int);
+
+template <class Container, class ElementT>
+static constexpr bool is_range = detail::is_range_t<Container, ElementT>::value;
+
 } // namespace tmp
 } // namespace polymesh