Repository: elalish/manifold (language: python)

Issue:
MeshGL64 tests, bindings, and deprecate Mesh
We should get this done before v3.0.


Discussion / hints:
@elalish We need to somehow get vertex pseudonormals in the meshgl output for our tests. I think `CalculateNormals` is actually giving us the face normal (duplicated property vertices?) for sharp edges, which is not very useful for our tests.
Also, I'm thinking if we should make the indices 64 bit, so we can slowly use 64-bit integers internally.
Let me look into those tests - I'll do that next. I'd guess some just need to be rethought. 

Are you thinking 64 bit ints just for `MeshGL64`?
> Are you thinking 64 bit ints just for `MeshGL64`?

Yes.
@elalish assigned you for the changes in tests to deprecate `Mesh`.

Fix patch:
diff --git a/bindings/wasm/helpers.cpp b/bindings/wasm/helpers.cpp
index fe3f36c24..8b2c82bac 100644
--- a/bindings/wasm/helpers.cpp
+++ b/bindings/wasm/helpers.cpp
@@ -223,7 +223,10 @@ std::vector<Manifold> SplitByPlane(Manifold& m, vec3 normal,
 }
 
 void CollectVertices(std::vector<vec3>& verts, const Manifold& manifold) {
-  Mesh mesh = manifold.GetMesh();
-  verts.insert(verts.end(), mesh.vertPos.begin(), mesh.vertPos.end());
+  const MeshGL64 mesh = manifold.GetMeshGL64();
+  const auto numVert = mesh.NumVert();
+  for (size_t v = 0; v < numVert; ++v) {
+    verts.push_back(mesh.GetVertPos(v));
+  }
 }
 }  // namespace man_js
diff --git a/meshIO/src/meshIO.cpp b/meshIO/src/meshIO.cpp
index aaa26ad08..d026c77ef 100644
--- a/meshIO/src/meshIO.cpp
+++ b/meshIO/src/meshIO.cpp
@@ -258,76 +258,4 @@ void ExportMesh(const std::string& filename, const MeshGL& mesh,
 
   ExportScene(scene, filename);
 }
-
-/**
- * Saves the Mesh to the desired file type, determined from the extension
- * specified. In the case of .glb/.gltf, this will save in version 2.0.
- *
- * This is a very simple export function and is intended primarily as a
- * demonstration. Generally users of this library will need to modify this to
- * write all the important properties for their application and read any custom
- * data structures.
- *
- * @param filename The file extension must be one that Assimp supports for
- * export. GLB & 3MF are recommended.
- * @param mesh The mesh to export, likely from Manifold.GetMesh().
- * @param options The options currently only affect an exported GLB's material.
- * Pass {} for defaults.
- */
-void ExportMesh(const std::string& filename, const Mesh& mesh,
-                const ExportOptions& options) {
-  if (mesh.triVerts.size() == 0) {
-    std::cout << filename << " was not saved because the input mesh was empty."
-              << std::endl;
-    return;
-  }
-
-  std::string type = GetType(filename);
-  const bool isYup = type == "glb2" || type == "gltf2";
-
-  aiScene* scene = CreateScene(options);
-  aiMesh* mesh_out = scene->mMeshes[0];
-
-  mesh_out->mNumVertices = mesh.vertPos.size();
-  mesh_out->mVertices = new aiVector3D[mesh_out->mNumVertices];
-  if (!options.faceted) {
-    DEBUG_ASSERT(
-        mesh.vertNormal.size() == mesh.vertPos.size(), userErr,
-        "vertNormal must be the same length as vertPos when faceted is false.");
-    mesh_out->mNormals = new aiVector3D[mesh_out->mNumVertices];
-  }
-  if (!options.mat.vertColor.empty()) {
-    DEBUG_ASSERT(mesh.vertPos.size() == options.mat.vertColor.size(), userErr,
-                 "If present, vertColor must be the same length as vertPos.");
-    mesh_out->mColors[0] = new aiColor4D[mesh_out->mNumVertices];
-  }
-
-  for (size_t i = 0; i < mesh_out->mNumVertices; ++i) {
-    const vec3& v = mesh.vertPos[i];
-    mesh_out->mVertices[i] =
-        isYup ? aiVector3D(v.y, v.z, v.x) : aiVector3D(v.x, v.y, v.z);
-    if (!options.faceted) {
-      const vec3& n = mesh.vertNormal[i];
-      mesh_out->mNormals[i] =
-          isYup ? aiVector3D(n.y, n.z, n.x) : aiVector3D(n.x, n.y, n.z);
-    }
-    if (!options.mat.vertColor.empty()) {
-      const vec4& c = options.mat.vertColor[i];
-      mesh_out->mColors[0][i] = aiColor4D(c.r, c.g, c.b, c.a);
-    }
-  }
-
-  mesh_out->mNumFaces = mesh.triVerts.size();
-  mesh_out->mFaces = new aiFace[mesh_out->mNumFaces];
-
-  for (size_t i = 0; i < mesh_out->mNumFaces; ++i) {
-    aiFace& face = mesh_out->mFaces[i];
-    face.mNumIndices = 3;
-    face.mIndices = new glm::uint[face.mNumIndices];
-    for (int j : {0, 1, 2}) face.mIndices[j] = mesh.triVerts[i][j];
-  }
-
-  ExportScene(scene, filename);
-}
-
 }  // namespace manifold
diff --git a/src/manifold/include/manifold/manifold.h b/src/manifold/include/manifold/manifold.h
index 9f04423d4..9cc48e801 100644
--- a/src/manifold/include/manifold/manifold.h
+++ b/src/manifold/include/manifold/manifold.h
@@ -102,10 +102,11 @@ struct MeshGLP {
 
   bool Merge();
 
-  vec3 GetVertPos(size_t i) const {
+  glm::vec<3, Precision> GetVertPos(size_t i) const {
     size_t offset = i * numProp;
-    return vec3(vertProperties[offset], vertProperties[offset + 1],
-                vertProperties[offset + 2]);
+    return glm::vec<3, Precision>(vertProperties[offset],
+                                  vertProperties[offset + 1],
+                                  vertProperties[offset + 2]);
   }
 
   glm::vec<3, I> GetTriVerts(size_t i) const {
@@ -113,6 +114,13 @@ struct MeshGLP {
     return glm::vec<3, I>(triVerts[offset], triVerts[offset + 1],
                           triVerts[offset + 2]);
   }
+
+  glm::vec<4, Precision> GetTangent(size_t i) const {
+    size_t offset = 4 * i;
+    return glm::vec<4, Precision>(
+        halfedgeTangent[offset], halfedgeTangent[offset + 1],
+        halfedgeTangent[offset + 2], halfedgeTangent[offset + 3]);
+  }
 };
 
 /**
@@ -168,7 +176,6 @@ class Manifold {
   Manifold& operator=(Manifold&&) noexcept;
 
   Manifold(const MeshGL&);
-  Manifold(const Mesh&);
   Manifold(const MeshGL64&);
 
   static Manifold Smooth(const MeshGL&,
@@ -204,7 +211,6 @@ class Manifold {
    *  Details of the manifold
    */
   ///@{
-  Mesh GetMesh() const;
   MeshGL GetMeshGL(ivec3 normalIdx = ivec3(0)) const;
   MeshGL64 GetMeshGL64(ivec3 normalIdx = ivec3(0)) const;
   bool IsEmpty() const;
diff --git a/src/manifold/src/impl.cpp b/src/manifold/src/impl.cpp
index 225eae704..9cc23cd6b 100644
--- a/src/manifold/src/impl.cpp
+++ b/src/manifold/src/impl.cpp
@@ -251,56 +251,6 @@ uint32_t Manifold::Impl::ReserveIDs(uint32_t n) {
   return Manifold::Impl::meshIDCounter_.fetch_add(n, std::memory_order_relaxed);
 }
 
-Manifold::Impl::Impl(const Mesh& mesh, const MeshRelationD& relation,
-                     const std::vector<double>& propertyTolerance,
-                     bool hasFaceIDs)
-    : vertPos_(mesh.vertPos), halfedgeTangent_(mesh.halfedgeTangent) {
-  meshRelation_ = {relation.originalID, relation.numProp, relation.properties,
-                   relation.meshIDtransform};
-
-  Vec<ivec3> triVerts;
-  for (size_t i = 0; i < mesh.triVerts.size(); ++i) {
-    const ivec3 tri = mesh.triVerts[i];
-    // Remove topological degenerates
-    if (tri[0] != tri[1] && tri[1] != tri[2] && tri[2] != tri[0]) {
-      triVerts.push_back(tri);
-      if (relation.triRef.size() > 0) {
-        meshRelation_.triRef.push_back(relation.triRef[i]);
-      }
-      if (relation.triProperties.size() > 0) {
-        meshRelation_.triProperties.push_back(relation.triProperties[i]);
-      }
-    }
-  }
-
-  if (!IsIndexInBounds(triVerts)) {
-    MarkFailure(Error::VertexOutOfBounds);
-    return;
-  }
-
-  CreateHalfedges(triVerts);
-  if (!IsManifold()) {
-    MarkFailure(Error::NotManifold);
-    return;
-  }
-
-  CalculateBBox();
-  if (!IsFinite()) {
-    MarkFailure(Error::NonFiniteVertex);
-    return;
-  }
-  SetPrecision(mesh.precision);
-
-  SplitPinchedVerts();
-
-  CalculateNormals();
-
-  InitializeOriginal();
-
-  SimplifyTopology();
-  Finish();
-}
-
 /**
  * Create either a unit tetrahedron, cube or octahedron. The cube is in the
  * first octant, while the others are symmetric about the origin.
diff --git a/src/manifold/src/impl.h b/src/manifold/src/impl.h
index 131321a96..eb8f9b6c4 100644
--- a/src/manifold/src/impl.h
+++ b/src/manifold/src/impl.h
@@ -219,10 +219,6 @@ struct Manifold::Impl {
     meshRelation_.originalID = -1;
   }
 
-  Impl(const Mesh&, const MeshRelationD& relation,
-       const std::vector<double>& propertyTolerance = {},
-       bool hasFaceIDs = false);
-
   inline void ForVert(int halfedge, std::function<void(int halfedge)> func) {
     int current = halfedge;
     do {
diff --git a/src/manifold/src/manifold.cpp b/src/manifold/src/manifold.cpp
index 004af1112..ce8a38aa3 100644
--- a/src/manifold/src/manifold.cpp
+++ b/src/manifold/src/manifold.cpp
@@ -293,50 +293,6 @@ Manifold::Manifold(const MeshGL& meshGL)
 Manifold::Manifold(const MeshGL64& meshGL64)
     : pNode_(std::make_shared<CsgLeafNode>(std::make_shared<Impl>(meshGL64))) {}
 
-/**
- * Convert a Mesh into a Manifold. Will return an empty Manifold
- * and set an Error Status if the Mesh is not an oriented 2-manifold. Will
- * collapse degenerate triangles and unnecessary vertices.
- *
- * @param mesh The input Mesh.
- */
-Manifold::Manifold(const Mesh& mesh) {
-  Impl::MeshRelationD relation;
-  pNode_ =
-      std::make_shared<CsgLeafNode>(std::make_shared<Impl>(mesh, relation));
-}
-
-/**
- * This returns a Mesh of simple vectors of vertices and triangles suitable for
- * saving or other operations outside of the context of this library.
- */
-Mesh Manifold::GetMesh() const {
-  ZoneScoped;
-  const Impl& impl = *GetCsgLeafNode().GetImpl();
-
-  Mesh result;
-  result.precision = Precision();
-  result.vertPos.insert(result.vertPos.end(), impl.vertPos_.begin(),
-                        impl.vertPos_.end());
-  result.vertNormal.insert(result.vertNormal.end(), impl.vertNormal_.begin(),
-                           impl.vertNormal_.end());
-  result.halfedgeTangent.insert(result.halfedgeTangent.end(),
-                                impl.halfedgeTangent_.begin(),
-                                impl.halfedgeTangent_.end());
-
-  result.triVerts.resize(NumTri());
-  auto& triVerts = result.triVerts;
-  const auto& halfedges = impl.halfedge_;
-  for_each_n(autoPolicy(NumTri(), 1e5), countAt(0), NumTri(),
-             [&triVerts, &halfedges](const int tri) {
-               for (int i : {0, 1, 2}) {
-                 triVerts[tri][i] = halfedges[3 * tri + i].startVert;
-               }
-             });
-
-  return result;
-}
-
 /**
  * The most complete output of this library, returning a MeshGL that is designed
  * to easily push into a renderer, including all interleaved vertex properties
diff --git a/src/utilities/include/manifold/common.h b/src/utilities/include/manifold/common.h
index 256def20c..f05a4c79b 100644
--- a/src/utilities/include/manifold/common.h
+++ b/src/utilities/include/manifold/common.h
@@ -152,31 +152,6 @@ using SimplePolygon = std::vector<vec2>;
  */
 using Polygons = std::vector<SimplePolygon>;
 
-/**
- * The triangle-mesh input and output of this library.
- */
-struct Mesh {
-  /// Required: The X-Y-Z positions of all vertices.
-  std::vector<vec3> vertPos;
-  /// Required: The vertex indices of the three triangle corners in CCW (from
-  /// the outside) order, for each triangle.
-  std::vector<ivec3> triVerts;
-  /// Optional: The X-Y-Z normal vectors of each vertex. If non-empty, must have
-  /// the same length as vertPos. If empty, these will be calculated
-  /// automatically.
-  std::vector<vec3> vertNormal;
-  /// Optional: The X-Y-Z-W weighted tangent vectors for smooth Refine(). If
-  /// non-empty, must be exactly three times as long as Mesh.triVerts. Indexed
-  /// as 3 * tri + i, representing the tangent from Mesh.triVerts[tri][i] along
-  /// the CCW edge. If empty, mesh is faceted.
-  std::vector<vec4> halfedgeTangent;
-  /// The absolute precision of the vertex positions, based on accrued rounding
-  /// errors. When creating a Manifold, the precision used will be the maximum
-  /// of this and a baseline precision from the size of the bounding box. Any
-  /// edge shorter than precision may be collapsed.
-  double precision = 0;
-};
-
 /**
  * Defines which edges to sharpen and how much for the Manifold.Smooth()
  * constructor.


Test patch:
diff --git a/test/boolean_complex_test.cpp b/test/boolean_complex_test.cpp
index b25f5eabf..7c5674cd0 100644
--- a/test/boolean_complex_test.cpp
+++ b/test/boolean_complex_test.cpp
@@ -168,31 +168,64 @@ TEST(BooleanComplex, Cylinders) {
 }
 
 TEST(BooleanComplex, Subtract) {
-  Mesh firstMesh;
-  firstMesh.vertPos = {{0, 0, 0},           {1540, 0, 0},
-                       {1540, 70, 0},       {0, 70, 0},
-                       {0, 0, -278.282},    {1540, 70, -278.282},
-                       {1540, 0, -278.282}, {0, 70, -278.282}};
+  MeshGL firstMesh;
+  firstMesh.vertProperties = {
+      0,    0,  0,         //
+      1540, 0,  0,         //
+      1540, 70, 0,         //
+      0,    70, 0,         //
+      0,    0,  -278.282,  //
+      1540, 70, -278.282,  //
+      1540, 0,  -278.282,  //
+      0,    70, -278.282   //
+  };
   firstMesh.triVerts = {
-      {0, 1, 2}, {2, 3, 0}, {4, 5, 6}, {5, 4, 7}, {6, 2, 1}, {6, 5, 2},
-      {5, 3, 2}, {5, 7, 3}, {7, 0, 3}, {7, 4, 0}, {4, 1, 0}, {4, 6, 1},
+      0, 1, 2,  //
+      2, 3, 0,  //
+      4, 5, 6,  //
+      5, 4, 7,  //
+      6, 2, 1,  //
+      6, 5, 2,  //
+      5, 3, 2,  //
+      5, 7, 3,  //
+      7, 0, 3,  //
+      7, 4, 0,  //
+      4, 1, 0,  //
+      4, 6, 1,  //
   };
 
-  Mesh secondMesh;
-  secondMesh.vertPos = {
-      {2.04636e-12, 70, 50000},       {2.04636e-12, -1.27898e-13, 50000},
-      {1470, -1.27898e-13, 50000},    {1540, 70, 50000},
-      {2.04636e-12, 70, -28.2818},    {1470, -1.27898e-13, 0},
-      {2.04636e-12, -1.27898e-13, 0}, {1540, 70, -28.2818}};
-  secondMesh.triVerts = {{0, 1, 2}, {2, 3, 0}, {4, 5, 6}, {5, 4, 7},
-                         {6, 2, 1}, {6, 5, 2}, {5, 3, 2}, {5, 7, 3},
-                         {7, 0, 3}, {7, 4, 0}, {4, 1, 0}, {4, 6, 1}};
+  MeshGL secondMesh;
+  secondMesh.vertProperties = {
+      2.04636e-12, 70,           50000,     //
+      2.04636e-12, -1.27898e-13, 50000,     //
+      1470,        -1.27898e-13, 50000,     //
+      1540,        70,           50000,     //
+      2.04636e-12, 70,           -28.2818,  //
+      1470,        -1.27898e-13, 0,         //
+      2.04636e-12, -1.27898e-13, 0,         //
+      1540,        70,           -28.2818   //
+  };
+  secondMesh.triVerts = {
+      0, 1, 2,  //
+      2, 3, 0,  //
+      4, 5, 6,  //
+      5, 4, 7,  //
+      6, 2, 1,  //
+      6, 5, 2,  //
+      5, 3, 2,  //
+      5, 7, 3,  //
+      7, 0, 3,  //
+      7, 4, 0,  //
+      4, 1, 0,  //
+      4, 6, 1   //
+  };
 
   Manifold first(firstMesh);
   Manifold second(secondMesh);
 
   first -= second;
   first.GetMeshGL();
+  EXPECT_EQ(first.Status(), Manifold::Error::NoError);
 }
 
 TEST(BooleanComplex, Close) {
diff --git a/test/boolean_test.cpp b/test/boolean_test.cpp
index 1c1c3a214..bf553aa92 100644
--- a/test/boolean_test.cpp
+++ b/test/boolean_test.cpp
@@ -61,8 +61,8 @@ TEST(Boolean, Normals) {
   MeshGL cubeGL = CubeSTL();
   cubeGL.Merge();
   const Manifold cube(cubeGL);
-  const MeshGL sphereGL = WithNormals(Manifold::Sphere(60));
-  const Manifold sphere(sphereGL);
+  const Manifold sphere = Manifold::Sphere(60).CalculateNormals(0);
+  const MeshGL sphereGL = sphere.GetMeshGL();
 
   Manifold result =
       cube.Scale(vec3(100)) -
@@ -218,10 +218,19 @@ TEST(Boolean, SelfSubtract) {
 }
 
 TEST(Boolean, Perturb) {
-  Mesh tmp;
-  tmp.vertPos = {
-      {0.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {1.0, 0.0, 0.0}, {0.0, 0.0, 1.0}};
-  tmp.triVerts = {{2, 0, 1}, {0, 3, 1}, {2, 3, 0}, {3, 2, 1}};
+  MeshGL tmp;
+  tmp.vertProperties = {
+      0.0, 0.0, 0.0,  //
+      0.0, 1.0, 0.0,  //
+      1.0, 0.0, 0.0,  //
+      0.0, 0.0, 1.0   //
+  };
+  tmp.triVerts = {
+      2, 0, 1,  //
+      0, 3, 1,  //
+      2, 3, 0,  //
+      3, 2, 1   //
+  };
   Manifold corner(tmp);
   Manifold empty = corner - corner;
   EXPECT_TRUE(empty.IsEmpty());
diff --git a/test/hull_test.cpp b/test/hull_test.cpp
index af97cf41b..b0bd6801b 100644
--- a/test/hull_test.cpp
+++ b/test/hull_test.cpp
@@ -22,29 +22,32 @@
 using namespace manifold;
 
 // Check if the mesh remains convex after adding new faces
-bool isMeshConvex(manifold::Manifold hullManifold, double epsilon = 0.0000001) {
+bool isMeshConvex(Manifold hullManifold, double epsilon = 0.0000001) {
   // Get the mesh from the manifold
-  manifold::Mesh mesh = hullManifold.GetMesh();
+  MeshGL64 mesh = hullManifold.GetMeshGL64();
 
-  const auto &vertPos = mesh.vertPos;
+  const auto numTri = mesh.NumTri();
+  const auto numVert = mesh.NumVert();
+  const auto numProp = mesh.numProp;
 
   // Iterate over each triangle
-  for (const auto &tri : mesh.triVerts) {
+  for (size_t t = 0; t < numTri; ++t) {
     // Get the vertices of the triangle
-    vec3 v0 = vertPos[tri[0]];
-    vec3 v1 = vertPos[tri[1]];
-    vec3 v2 = vertPos[tri[2]];
+    auto tri = mesh.GetTriVerts(t);
+    vec3 v0 = mesh.GetVertPos(tri[0]);
+    vec3 v1 = mesh.GetVertPos(tri[1]);
+    vec3 v2 = mesh.GetVertPos(tri[2]);
 
     // Compute the normal of the triangle
     vec3 normal = glm::normalize(glm::cross(v1 - v0, v2 - v0));
 
     // Check all other vertices
-    for (int i = 0; i < (int)vertPos.size(); ++i) {
+    for (size_t i = 0; i < numVert; ++i) {
       if (i == tri[0] || i == tri[1] || i == tri[2])
         continue;  // Skip vertices of the current triangle
 
       // Get the vertex
-      vec3 v = vertPos[i];
+      vec3 v = mesh.GetVertPos(i);
 
       // Compute the signed distance from the plane
       double distance = glm::dot(normal, v - v0);
diff --git a/test/manifold_test.cpp b/test/manifold_test.cpp
index 203b88648..18a2ab95c 100644
--- a/test/manifold_test.cpp
+++ b/test/manifold_test.cpp
@@ -51,7 +51,7 @@ TEST(Manifold, GetMeshGL) {
 }
 
 TEST(Manifold, Empty) {
-  Mesh emptyMesh;
+  MeshGL emptyMesh;
   Manifold empty(emptyMesh);
 
   EXPECT_TRUE(empty.IsEmpty());
@@ -178,18 +178,6 @@ TEST(Manifold, Cylinder) {
   EXPECT_EQ(cylinder.NumTri(), 4 * n - 4);
 }
 
-TEST(Manifold, Normals) {
-  Mesh cube = Manifold::Cube(vec3(1), true).GetMesh();
-  const int nVert = cube.vertPos.size();
-  for (int i = 0; i < nVert; ++i) {
-    vec3 v = glm::normalize(cube.vertPos[i]);
-    vec3& n = cube.vertNormal[i];
-    EXPECT_FLOAT_EQ(v.x, n.x);
-    EXPECT_FLOAT_EQ(v.y, n.y);
-    EXPECT_FLOAT_EQ(v.z, n.z);
-  }
-}
-
 TEST(Manifold, Extrude) {
   Polygons polys = SquareHole();
   Manifold donut = Manifold::Extrude(polys, 1.0, 3);
diff --git a/test/samples_test.cpp b/test/samples_test.cpp
index 800c88ec0..112f0d54c 100644
--- a/test/samples_test.cpp
+++ b/test/samples_test.cpp
@@ -22,14 +22,14 @@
 
 using namespace manifold;
 
-std::vector<int> EdgePairs(const Mesh in) {
-  const int numHalfedge = 3 * in.triVerts.size();
+std::vector<int> EdgePairs(const MeshGL in) {
+  const int numHalfedge = 3 * in.NumTri();
   std::vector<int> edgePair(numHalfedge);
 
   std::map<std::pair<int, int>, int> halfedgeLink;
   for (int i = 0; i < numHalfedge; ++i) {
-    std::pair<int, int> key = std::make_pair(in.triVerts[i / 3][i % 3],
-                                             in.triVerts[i / 3][(i + 1) % 3]);
+    std::pair<int, int> key = std::make_pair(
+        in.triVerts[i], in.triVerts[(i + 1) % 3 == 0 ? i - 2 : i + 1]);
     if (key.first > key.second) std::swap(key.first, key.second);
     const auto result = halfedgeLink.emplace(std::make_pair(key, i));
     if (!result.second) {
@@ -49,7 +49,6 @@ TEST(Samples, Knot13) {
 #ifdef MANIFOLD_EXPORT
   if (options.exportModels) ExportMesh("knot13.glb", knot13.GetMeshGL(), {});
 #endif
-  CheckNormals(knot13);
   EXPECT_EQ(knot13.Genus(), 1);
   auto prop = knot13.GetProperties();
   EXPECT_NEAR(prop.volume, 20786, 1);
@@ -63,7 +62,6 @@ TEST(Samples, Knot42) {
 #ifdef MANIFOLD_EXPORT
   if (options.exportModels) ExportMesh("knot42.glb", knot42.GetMeshGL(), {});
 #endif
-  CheckNormals(knot42);
   std::vector<Manifold> knots = knot42.Decompose();
   ASSERT_EQ(knots.size(), 2);
   EXPECT_EQ(knots[0].Genus(), 1);
@@ -81,22 +79,28 @@ TEST(Samples, Scallop) {
 
 #ifdef MANIFOLD_EXPORT
   if (options.exportModels) {
-    Mesh in = scallop.GetMesh();
+    MeshGL in = scallop.GetMeshGL();
     std::vector<int> edgePair = EdgePairs(in);
 
     ExportOptions options;
     const int numVert = scallop.NumVert();
     const int numHalfedge = 3 * scallop.NumTri();
+    const int numProp = in.numProp;
     for (size_t i = 0; i < scallop.NumVert(); ++i) {
       options.mat.vertColor.push_back({0, 0, 1, 1});
     }
     for (int i = 0; i < numHalfedge; ++i) {
-      const int vert = in.triVerts[i / 3][i % 3];
-      in.vertPos.push_back(in.vertPos[vert] + vec3(in.halfedgeTangent[i]) *
-                                                  in.halfedgeTangent[i].w);
+      const int vert = in.triVerts[i];
+      for (int j : {0, 1, 2}) {
+        in.vertProperties.push_back(in.vertProperties[numProp * vert + j] +
+                                    in.halfedgeTangent[4 * i + j] *
+                                        in.halfedgeTangent[4 * i + 3]);
+      }
       options.mat.vertColor.push_back({0.5, 0.5, 0, 1});
       const int j = edgePair[i % 3 == 0 ? i + 2 : i - 1];
-      in.triVerts.push_back({vert, numVert + i, numVert + j});
+      in.triVerts.push_back(vert);
+      in.triVerts.push_back(numVert + i);
+      in.triVerts.push_back(numVert + j);
     }
     options.faceted = true;
     options.mat.roughness = 0.5;
@@ -117,7 +121,6 @@ TEST(Samples, Scallop) {
 
   scallop = scallop.Refine(50).CalculateCurvature(-1, 0).SetProperties(
       3, colorCurvature);
-  CheckNormals(scallop);
   auto prop = scallop.GetProperties();
   EXPECT_NEAR(prop.volume, 39.9, 0.1);
   EXPECT_NEAR(prop.surfaceArea, 79.3, 0.1);
@@ -138,7 +141,6 @@ TEST(Samples, Scallop) {
 
 TEST(Samples, TetPuzzle) {
   Manifold puzzle = TetPuzzle(50, 0.2, 50);
-  CheckNormals(puzzle);
   EXPECT_LE(puzzle.NumDegenerateTris(), 2);
   CheckGL(puzzle);
 
@@ -152,7 +154,6 @@ TEST(Samples, TetPuzzle) {
 
 TEST(Samples, FrameReduced) {
   Manifold frame = RoundedFrame(100, 10, 4);
-  CheckNormals(frame);
   EXPECT_EQ(frame.NumDegenerateTris(), 0);
   EXPECT_EQ(frame.Genus(), 5);
   auto prop = frame.GetProperties();
@@ -167,7 +168,6 @@ TEST(Samples, FrameReduced) {
 
 TEST(Samples, Frame) {
   Manifold frame = RoundedFrame(100, 10);
-  CheckNormals(frame);
   EXPECT_EQ(frame.NumDegenerateTris(), 0);
   EXPECT_EQ(frame.Genus(), 5);
   CheckGL(frame);
@@ -182,7 +182,6 @@ TEST(Samples, Frame) {
 #ifdef MANIFOLD_CROSS_SECTION
 TEST(Samples, Bracelet) {
   Manifold bracelet = StretchyBracelet();
-  CheckNormals(bracelet);
   EXPECT_EQ(bracelet.NumDegenerateTris(), 0);
   EXPECT_EQ(bracelet.Genus(), 1);
   CheckGL(bracelet);
@@ -216,7 +215,6 @@ TEST(Samples, Bracelet) {
 TEST(Samples, GyroidModule) {
   const double size = 20;
   Manifold gyroid = GyroidModule(size);
-  CheckNormals(gyroid);
   EXPECT_LE(gyroid.NumDegenerateTris(), 4);
   EXPECT_EQ(gyroid.Genus(), 15);
   CheckGL(gyroid);
@@ -241,7 +239,6 @@ TEST(Samples, GyroidModule) {
 
 TEST(Samples, Sponge1) {
   Manifold sponge = MengerSponge(1);
-  CheckNormals(sponge);
   EXPECT_EQ(sponge.NumDegenerateTris(), 0);
   EXPECT_EQ(sponge.NumVert(), 40);
   EXPECT_EQ(sponge.Genus(), 5);
@@ -260,7 +257,6 @@ TEST(Samples, Sponge1) {
 #ifdef MANIFOLD_CROSS_SECTION
 TEST(Samples, Sponge4) {
   Manifold sponge = MengerSponge(4);
-  CheckNormals(sponge);
   EXPECT_LE(sponge.NumDegenerateTris(), 8);
   EXPECT_EQ(sponge.Genus(), 26433);  // should be 1:5, 2:81, 3:1409, 4:26433
   CheckGL(sponge);
diff --git a/test/smooth_test.cpp b/test/smooth_test.cpp
index a3e6206bd..3404373f2 100644
--- a/test/smooth_test.cpp
+++ b/test/smooth_test.cpp
@@ -140,15 +140,19 @@ TEST(Smooth, Sphere) {
     Manifold smoothed = Manifold::Smooth(sphere.GetMeshGL()).Refine(6);
     // Refine(3*x) puts a center point in the triangle, which is the worst
     // case.
-    Mesh out = smoothed.GetMesh();
-    auto bounds = std::minmax_element(out.vertPos.begin(), out.vertPos.end(),
-                                      [](const vec3& a, const vec3& b) {
-                                        return glm::dot(a, a) < glm::dot(b, b);
-                                      });
-    double min = glm::length(*bounds.first);
-    double max = glm::length(*bounds.second);
-    EXPECT_NEAR(min, 1, precision[i]);
-    EXPECT_NEAR(max, 1, precision[i]);
+    MeshGL64 out = smoothed.GetMeshGL64();
+    const int numVert = out.NumVert();
+    const int numProp = out.numProp;
+    double maxR2 = 0;
+    double minR2 = 2;
+    for (int v = 0; v < numVert; ++v) {
+      const vec3 a = out.GetVertPos(v);
+      const double r2 = dot(a, a);
+      maxR2 = std::max(maxR2, r2);
+      minR2 = std::min(minR2, r2);
+    }
+    EXPECT_NEAR(std::sqrt(minR2), 1, precision[i]);
+    EXPECT_NEAR(std::sqrt(maxR2), 1, precision[i]);
   }
 }
 
@@ -171,12 +175,12 @@ TEST(Smooth, Normals) {
 TEST(Smooth, Manual) {
   // Unit Octahedron
   const auto oct = Manifold::Sphere(1, 4).GetMeshGL();
-  Mesh smooth = Manifold::Smooth(oct).GetMesh();
+  MeshGL smooth = Manifold::Smooth(oct).GetMeshGL();
   // Sharpen the edge from vert 4 to 5
-  smooth.halfedgeTangent[6].w = 0;
-  smooth.halfedgeTangent[22].w = 0;
-  smooth.halfedgeTangent[16].w = 0;
-  smooth.halfedgeTangent[18].w = 0;
+  smooth.halfedgeTangent[4 * 6 + 3] = 0;
+  smooth.halfedgeTangent[4 * 22 + 3] = 0;
+  smooth.halfedgeTangent[4 * 16 + 3] = 0;
+  smooth.halfedgeTangent[4 * 18 + 3] = 0;
   Manifold interp(smooth);
   interp = interp.Refine(100);
 
@@ -269,21 +273,23 @@ vec4 CircularTangent(const vec3& tangent, const vec3& edgeVec) {
 
 #ifdef MANIFOLD_CROSS_SECTION
 TEST(Smooth, Torus) {
-  Mesh torusMesh =
+  MeshGL64 torusMesh =
       Manifold::Revolve(
           CrossSection::Circle(1, 8).Translate({2, 0}).ToPolygons(), 6)
-          .GetMesh();
-  const int numTri = torusMesh.triVerts.size();
+          .GetMeshGL64();
+  const int numTri = torusMesh.NumTri();
+  const int numProp = torusMesh.numProp;
 
   // Create correct toroidal halfedge tangents - SmoothOut() is too generic to
   // do this perfectly.
-  torusMesh.halfedgeTangent.resize(3 * numTri);
+  torusMesh.halfedgeTangent.resize(4 * 3 * numTri);
   for (int tri = 0; tri < numTri; ++tri) {
+    const auto triVerts = torusMesh.GetTriVerts(tri);
     for (const int i : {0, 1, 2}) {
-      vec4& tangent = torusMesh.halfedgeTangent[3 * tri + i];
-      const vec3 v = torusMesh.vertPos[torusMesh.triVerts[tri][i]];
-      const vec3 edge =
-          torusMesh.vertPos[torusMesh.triVerts[tri][(i + 1) % 3]] - v;
+      vec4 tangent;
+      const vec3 v = torusMesh.GetVertPos(triVerts[i]);
+      const vec3 v1 = torusMesh.GetVertPos(triVerts[(i + 1) % 3]);
+      const vec3 edge = v1 - v;
       if (edge.z == 0) {
         vec3 tan(v.y, -v.x, 0);
         tan *= glm::sign(glm::dot(tan, edge));
@@ -300,6 +306,9 @@ TEST(Smooth, Torus) {
       } else {
         tangent = {0, 0, 0, -1};
       }
+      const int e = 3 * tri + i;
+      for (const int j : {0, 1, 2, 3})
+        torusMesh.halfedgeTangent[4 * e + j] = tangent[j];
     }
   }
 
diff --git a/test/test.h b/test/test.h
index 889dc3c42..b6c1f64bf 100644
--- a/test/test.h
+++ b/test/test.h
@@ -53,7 +53,6 @@ MeshGL CubeSTL();
 MeshGL CubeUV();
 MeshGL WithIndexColors(const MeshGL& in);
 MeshGL WithPositionColors(const Manifold& in);
-MeshGL WithNormals(const Manifold& in);
 float GetMaxProperty(const MeshGL& mesh, int channel);
 float GetMinProperty(const MeshGL& mesh, int channel);
 void CheckFinite(const MeshGL& mesh);
@@ -62,7 +61,6 @@ void RelatedGL(const Manifold& out, const std::vector<MeshGL>& originals,
                bool checkNormals = false, bool updateNormals = false);
 void ExpectMeshes(const Manifold& manifold,
                   const std::vector<MeshSize>& meshSize);
-void CheckNormals(const Manifold& manifold);
 void CheckStrictly(const Manifold& manifold);
 void CheckGL(const Manifold& manifold);
 #ifdef MANIFOLD_EXPORT
diff --git a/test/test_main.cpp b/test/test_main.cpp
index 72bb1e6f0..cd8a4a37d 100644
--- a/test/test_main.cpp
+++ b/test/test_main.cpp
@@ -233,25 +233,6 @@ MeshGL WithPositionColors(const Manifold& in) {
   return out.GetMeshGL();
 }
 
-MeshGL WithNormals(const Manifold& in) {
-  const Mesh mesh = in.GetMesh();
-  MeshGL out;
-  out.runOriginalID = {Manifold::ReserveIDs(1)};
-  out.numProp = 6;
-  out.vertProperties.resize(out.numProp * mesh.vertPos.size());
-  for (size_t i = 0; i < mesh.vertPos.size(); ++i) {
-    for (int j : {0, 1, 2}) {
-      out.vertProperties[6 * i + j] = mesh.vertPos[i][j];
-      out.vertProperties[6 * i + 3 + j] = mesh.vertNormal[i][j];
-    }
-  }
-  out.triVerts.resize(3 * mesh.triVerts.size());
-  for (size_t i = 0; i < mesh.triVerts.size(); ++i) {
-    for (int j : {0, 1, 2}) out.triVerts[3 * i + j] = mesh.triVerts[i][j];
-  }
-  return out;
-}
-
 MeshGL CubeUV() {
   MeshGL mgl;
   mgl.numProp = 5;
@@ -442,22 +423,10 @@ void ExpectMeshes(const Manifold& manifold,
     EXPECT_EQ(meshGL.mergeFromVert.size(), meshGL.mergeToVert.size());
     EXPECT_EQ(meshGL.mergeFromVert.size(),
               meshGL.NumVert() - manifolds[i].NumVert());
-    const Mesh mesh = manifolds[i].GetMesh();
-    for (const vec3& normal : mesh.vertNormal) {
-      ASSERT_NEAR(glm::length(normal), 1, 0.0001);
-    }
-  }
-}
-
-void CheckNormals(const Manifold& manifold) {
-  EXPECT_TRUE(manifold.MatchesTriNormals());
-  for (const vec3& normal : manifold.GetMesh().vertNormal) {
-    ASSERT_NEAR(glm::length(normal), 1, 0.0001);
   }
 }
 
 void CheckStrictly(const Manifold& manifold) {
-  CheckNormals(manifold);
   EXPECT_EQ(manifold.NumDegenerateTris(), 0);
 }