{
  "measured_on": "2026-09-14",
  "question": "where a watertight model stops being watertight when it changes format",
  "object": "icosphere, subdivisions 3, scaled to 100 mm across, then separated so every triangle carries its own three corners with the coordinates still bit identical where triangles meet",
  "trimesh_version": "5.1.0",
  "numpy_version": "2.5.2",
  "float32_gap_at_model_scale_mm": 3.814697265625e-06,
  "gltf_spec_quotes": [
    {
      "quote": "Primitives correspond to the data required for GPU draw calls",
      "why_it_matters": "the unit being stored is a draw call's vertex buffer, not a solid"
    },
    {
      "quote": "All attribute accessors for a given primitive MUST have the same count",
      "why_it_matters": "a vertex is the tuple across every attribute, so a UV seam forces duplication"
    }
  ],
  "gltf_spec_source": {
    "spec": "glTF 2.0 Specification, Meshes",
    "url": "https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes",
    "retrieved": "2026-09-14"
  },
  "truth": {
    "vertices": 642,
    "faces": 1280,
    "boundary_edges": 0,
    "nonmanifold_edges": 0,
    "shells": 1,
    "watertight": true,
    "volume_mm3": 519092.6021
  },
  "split_source": {
    "vertices": 3840,
    "faces": 1280,
    "boundary_edges": 3840,
    "nonmanifold_edges": 0,
    "shells": 1280,
    "watertight": false
  },
  "welded_source_rows": [
    {
      "format": "stl",
      "stores_a_vertex_index": false,
      "vertices_stored_in_file": 3840,
      "file_evidence": "binary STL header declares 1280 triangles, 3 corners each, no index",
      "file_bytes": 64084,
      "vertices_after_reading": 642,
      "reader_merged_vertices": true,
      "boundary_edges_after_reading": 0,
      "shells_after_reading": 1,
      "watertight_after_reading": true,
      "reader_rule": "must rebuild: the file cannot express which corners are shared"
    },
    {
      "format": "obj",
      "stores_a_vertex_index": true,
      "vertices_stored_in_file": 642,
      "file_evidence": "642 lines beginning 'v '",
      "file_bytes": 42269,
      "vertices_after_reading": 642,
      "reader_merged_vertices": false,
      "boundary_edges_after_reading": 0,
      "shells_after_reading": 1,
      "watertight_after_reading": true,
      "reader_rule": "may rebuild: an index is present but duplicate positions are legal"
    },
    {
      "format": "ply",
      "stores_a_vertex_index": true,
      "vertices_stored_in_file": 642,
      "file_evidence": "header 'element vertex', format binary_little_endian",
      "file_bytes": 24560,
      "vertices_after_reading": 642,
      "reader_merged_vertices": false,
      "boundary_edges_after_reading": 0,
      "shells_after_reading": 1,
      "watertight_after_reading": true,
      "reader_rule": "may rebuild: an index is present but duplicate positions are legal"
    },
    {
      "format": "glb",
      "stores_a_vertex_index": true,
      "vertices_stored_in_file": 642,
      "file_evidence": "POSITION accessor count, componentType 5126 (5126 = float32); attributes present=['POSITION']",
      "file_bytes": 23764,
      "vertices_after_reading": 642,
      "reader_merged_vertices": false,
      "boundary_edges_after_reading": 0,
      "shells_after_reading": 1,
      "watertight_after_reading": true,
      "reader_rule": "must not merge by position: a vertex carries normals and UVs too"
    },
    {
      "format": "3mf",
      "stores_a_vertex_index": true,
      "vertices_stored_in_file": 642,
      "file_evidence": "642 <vertex> elements in 3D/3dmodel.model",
      "file_bytes": 13761,
      "vertices_after_reading": 642,
      "reader_merged_vertices": false,
      "boundary_edges_after_reading": 0,
      "shells_after_reading": 1,
      "watertight_after_reading": true,
      "reader_rule": "may rebuild: an index is present but duplicate positions are legal"
    }
  ],
  "split_source_rows": [
    {
      "format": "stl",
      "stores_a_vertex_index": false,
      "vertices_stored_in_file": 3840,
      "file_evidence": "binary STL header declares 1280 triangles, 3 corners each, no index",
      "file_bytes": 64084,
      "vertices_after_reading": 642,
      "reader_merged_vertices": true,
      "boundary_edges_after_reading": 0,
      "shells_after_reading": 1,
      "watertight_after_reading": true,
      "reader_rule": "must rebuild: the file cannot express which corners are shared"
    },
    {
      "format": "obj",
      "stores_a_vertex_index": true,
      "vertices_stored_in_file": 3840,
      "file_evidence": "3840 lines beginning 'v '",
      "file_bytes": 169868,
      "vertices_after_reading": 642,
      "reader_merged_vertices": true,
      "boundary_edges_after_reading": 0,
      "shells_after_reading": 1,
      "watertight_after_reading": true,
      "reader_rule": "may rebuild: an index is present but duplicate positions are legal"
    },
    {
      "format": "ply",
      "stores_a_vertex_index": true,
      "vertices_stored_in_file": 3840,
      "file_evidence": "header 'element vertex', format binary_little_endian",
      "file_bytes": 62937,
      "vertices_after_reading": 642,
      "reader_merged_vertices": true,
      "boundary_edges_after_reading": 0,
      "shells_after_reading": 1,
      "watertight_after_reading": true,
      "reader_rule": "may rebuild: an index is present but duplicate positions are legal"
    },
    {
      "format": "glb",
      "stores_a_vertex_index": true,
      "vertices_stored_in_file": 3840,
      "file_evidence": "POSITION accessor count, componentType 5126 (5126 = float32); attributes present=['POSITION']",
      "file_bytes": 62112,
      "vertices_after_reading": 3840,
      "reader_merged_vertices": false,
      "boundary_edges_after_reading": 3840,
      "shells_after_reading": 1280,
      "watertight_after_reading": false,
      "reader_rule": "must not merge by position: a vertex carries normals and UVs too"
    },
    {
      "format": "3mf",
      "stores_a_vertex_index": true,
      "vertices_stored_in_file": 3840,
      "file_evidence": "3840 <vertex> elements in 3D/3dmodel.model",
      "file_bytes": 28011,
      "vertices_after_reading": 642,
      "reader_merged_vertices": true,
      "boundary_edges_after_reading": 0,
      "shells_after_reading": 1,
      "watertight_after_reading": true,
      "reader_rule": "may rebuild: an index is present but duplicate positions are legal"
    }
  ],
  "epsilon_sweep": [
    {
      "epsilon_mm": 0.0,
      "below_float32_gap": true,
      "stl_vertices": 642,
      "stl_watertight": true,
      "stl_duplicates_resolved": 1.0,
      "obj_vertices": 642,
      "obj_watertight": true,
      "obj_duplicates_resolved": 1.0,
      "ply_vertices": 642,
      "ply_watertight": true,
      "ply_duplicates_resolved": 1.0,
      "glb_vertices": 3840,
      "glb_watertight": false,
      "glb_duplicates_resolved": 0.0,
      "3mf_vertices": 642,
      "3mf_watertight": true,
      "3mf_duplicates_resolved": 1.0
    },
    {
      "epsilon_mm": 1e-08,
      "below_float32_gap": true,
      "stl_vertices": 882,
      "stl_watertight": false,
      "stl_duplicates_resolved": 0.925,
      "obj_vertices": 3661,
      "obj_watertight": false,
      "obj_duplicates_resolved": 0.056,
      "ply_vertices": 882,
      "ply_watertight": false,
      "ply_duplicates_resolved": 0.925,
      "glb_vertices": 3840,
      "glb_watertight": false,
      "glb_duplicates_resolved": 0.0,
      "3mf_vertices": 3661,
      "3mf_watertight": false,
      "3mf_duplicates_resolved": 0.056
    },
    {
      "epsilon_mm": 1e-07,
      "below_float32_gap": true,
      "stl_vertices": 1455,
      "stl_watertight": false,
      "stl_duplicates_resolved": 0.7458,
      "obj_vertices": 3840,
      "obj_watertight": false,
      "obj_duplicates_resolved": 0.0,
      "ply_vertices": 1455,
      "ply_watertight": false,
      "ply_duplicates_resolved": 0.7458,
      "glb_vertices": 3840,
      "glb_watertight": false,
      "glb_duplicates_resolved": 0.0,
      "3mf_vertices": 3840,
      "3mf_watertight": false,
      "3mf_duplicates_resolved": 0.0
    },
    {
      "epsilon_mm": 1e-06,
      "below_float32_gap": true,
      "stl_vertices": 3272,
      "stl_watertight": false,
      "stl_duplicates_resolved": 0.1776,
      "obj_vertices": 3840,
      "obj_watertight": false,
      "obj_duplicates_resolved": 0.0,
      "ply_vertices": 3272,
      "ply_watertight": false,
      "ply_duplicates_resolved": 0.1776,
      "glb_vertices": 3840,
      "glb_watertight": false,
      "glb_duplicates_resolved": 0.0,
      "3mf_vertices": 3840,
      "3mf_watertight": false,
      "3mf_duplicates_resolved": 0.0
    },
    {
      "epsilon_mm": 1e-05,
      "below_float32_gap": false,
      "stl_vertices": 3839,
      "stl_watertight": false,
      "stl_duplicates_resolved": 0.0003,
      "obj_vertices": 3840,
      "obj_watertight": false,
      "obj_duplicates_resolved": 0.0,
      "ply_vertices": 3839,
      "ply_watertight": false,
      "ply_duplicates_resolved": 0.0003,
      "glb_vertices": 3840,
      "glb_watertight": false,
      "glb_duplicates_resolved": 0.0,
      "3mf_vertices": 3840,
      "3mf_watertight": false,
      "3mf_duplicates_resolved": 0.0
    },
    {
      "epsilon_mm": 0.0001,
      "below_float32_gap": false,
      "stl_vertices": 3840,
      "stl_watertight": false,
      "stl_duplicates_resolved": 0.0,
      "obj_vertices": 3840,
      "obj_watertight": false,
      "obj_duplicates_resolved": 0.0,
      "ply_vertices": 3840,
      "ply_watertight": false,
      "ply_duplicates_resolved": 0.0,
      "glb_vertices": 3840,
      "glb_watertight": false,
      "glb_duplicates_resolved": 0.0,
      "3mf_vertices": 3840,
      "3mf_watertight": false,
      "3mf_duplicates_resolved": 0.0
    }
  ],
  "not_measured_here": [
    "What any named application does on import. The reader behaviour recorded here is one library's, and the column that matters is the one before it: what the file stores. Blender, Cura, PrusaSlicer, MeshLab and every engine make their own merge decision, and the disagreement between them is the subject of the vertex welding benchmark rather than this one.",
    "Whether merging by position is the right thing to do. For a solid destined for a printer it usually is. For a model carrying UV seams or hard edges it destroys information that cannot be recovered, which is why the rendering format does not do it.",
    "Meshes that were never closed in the first place. Every case here starts from a sphere that is watertight by construction, so any open edge in the result was introduced on the way through."
  ]
}
