Skip to content

Add native mesh file I/O (OBJ, STL, PLY)#1369

Open
dhairyashilRG wants to merge 12 commits into
NVIDIA:mainfrom
dhairyashilRG:feat/mesh-io-utilities
Open

Add native mesh file I/O (OBJ, STL, PLY)#1369
dhairyashilRG wants to merge 12 commits into
NVIDIA:mainfrom
dhairyashilRG:feat/mesh-io-utilities

Conversation

@dhairyashilRG

@dhairyashilRG dhairyashilRG commented Apr 15, 2026

Copy link
Copy Markdown

Description

This PR adds native, zero-dependency mesh I/O utilities (wp.load_mesh() and wp.save_mesh()) to Warp as per the plan

Currently, loading basic test meshes in Warp requires either pxr.UsdGeom (a massive dependency) or third-party packages like trimesh. This forces many basic geometry tests and examples to be gated behind USD_AVAILABLE.

This PR implements lightweight, pure-Python parsers for standard mesh formats (Wavefront OBJ, ASCII/Binary STL, and ASCII/Binary PLY) directly into Warp. This is intended to drop the OpenUSD requirement for testing simple geometries and improve onboarding, not to replace high-performance C++ pipelines for massive assets.

Checklist

  • New and existing tests cover these changes.
  • The CHANGELOG.md entry has been added.

Test plan

  • Added a comprehensive suite of unit tests in (warp/tests/io/test_mesh.py) for OBJ, STL (ASCII/Binary + vertex deduplication), and PLY formats.
  • Generated standard geometric test assets (cube.obj, cube.stl, etc.).
  • Verified locally on CPU and CUDA (T4, sm_75) via: uv run --extra dev -m warp.tests -s autodetect -k TestMeshIO
  • 23+ mesh tests pass.

New feature / enhancement

import warp as wp

# One-liner to load a mesh (format inferred automatically, BVH constructed)
mesh = wp.load_mesh("tests/io/assets/cube.obj")

# Example loading binary STL with simple vertex deduplication
mesh = wp.load_mesh("model.stl", stl_merge_tolerance=1e-5)

# Save back out
wp.save_mesh(mesh, "output.ply")

Out of scope for this PR: Material/texture loading (.mtl files), animation/sequence loading, and GPU-accelerated parsing (file I/O is CPU-bound).

Summary by CodeRabbit

  • New Features

    • Native mesh import/export for OBJ, STL (binary/ASCII), and PLY (binary/ASCII) with zero external dependencies; auto-detection, optional winding flip, and device-aware load/save with round-trip support.
  • Documentation

    • Changelog updated and a detailed mesh I/O testing/status document added.
  • Tests

    • New unit and validation tests, deterministic mesh assets, and a validation script covering parsing, formats, edge cases, and round-trips.
  • Examples

    • Example updated to show alternative mesh-loading via the new I/O.

Add native OBJ, STL, and PLY mesh file parsers to eliminate USD
dependency for simple test geometries and improve onboarding.

* feat(io): Add wp.load_mesh(), wp.save_mesh(), wp.read_mesh()
* feat(io): Add OBJ parser with quad triangulation
* feat(io): Add STL parser (binary/ASCII) with vertex deduplication
* feat(io): Add PLY parser (binary/ASCII, LE/BE)
* feat(io): Add MeshData container and to_warp_mesh() method
* test(io): Add 23 test cases across all formats
* test(io): Add mesh asset generator script

Resolves issue where users must install external packages (trimesh,
open3d) or use heavy OpenUSD dependency for basic mesh loading.

Signed-off-by: DhairyashilGhatage <dhairyashil25@gmail.com>
Address ruff findings from pre-commit checks. Non-functional
style fixes only.

* style(io): Remove unused imports (Path, warn)
* style(io): Add encoding='utf-8' to text-mode open() calls
* style(io): Add noqa for intentional lazy imports
* style(io): Fix exception chaining with 'from None'
* style(io): Replace legacy np.random with modern RNG API
* style(io): Replace 'assert False' with raise AssertionError

Signed-off-by: DhairyashilGhatage <dhairyashil25@gmail.com>
Kernel integration test was accessing mesh.points directly when
mesh is passed as wp.uint64 handle, causing WarpCodegenAttributeError.

Kernels must use mesh_query_point() for queries on mesh handles.
Direct attribute access (mesh.points, mesh.indices) only works on
Mesh objects in Python, not uint64 handles in kernel code.

Signed-off-by: DhairyashilGhatage <dhairyashil25@gmail.com>
Validation script had hardcoded CPU device check that failed on
CUDA systems. Use wp.get_device() for dynamic detection.

* test(io): Fix str(mesh.device) assertion for CUDA
* docs(io): Update implementation plan with CUDA test results
* docs(io): Document AWS T4 (sm_75) testing completed
* docs(io): Document all 69 existing mesh tests pass (no regressions)

Testing completed on macOS ARM (CPU), Linux x86_64 (CPU),
and Linux x86_64 + CUDA T4.

Signed-off-by: DhairyashilGhatage <dhairyashil25@gmail.com>
@copy-pr-bot

copy-pr-bot Bot commented Apr 15, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Apr 15, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds native mesh file I/O to Warp: OBJ, STL (binary/ASCII), and PLY (binary/ASCII) parsers/writers, a MeshData dataclass, and top-level wp.load_mesh(), wp.read_mesh(), and wp.save_mesh() with mesh-to-warp conversion, optional winding flips, and BVH construction hooks.

Changes

Cohort / File(s) Summary
Public API & Changelog
CHANGELOG.md, warp/__init__.py, warp/__init__.pyi
Changelog updated; new top-level re-exports added: load_mesh, read_mesh, save_mesh, and MeshData.
Core I/O module
warp/_src/io/__init__.py, warp/_src/io/mesh.py
New core I/O layer: MeshData dataclass, read_mesh/load_mesh/save_mesh, device resolution, format autodetect/dispatch, flip-winding helpers, and BVH construction options.
OBJ implementation
warp/_src/io/obj.py
Added OBJ reader/writer supporting v/vt/vn tokens, negative indices, polygon triangulation (fan), optional winding flip, and OBJ write formatting.
PLY implementation
warp/_src/io/ply.py
Added PLY header parser, ASCII and binary readers/writers, property handling (positions, normals, colors), and triangle fan triangulation for polygonal faces.
STL implementation
warp/_src/io/stl.py
Added STL auto-detection, binary and ASCII readers/writers, binary deduplication (merge tolerance), and optional winding flip.
Tests & validation
warp/tests/io/*, warp/tests/unittest_suites.py
New unit tests and validation scripts for OBJ/STL/PLY parsing, round-trips, device placement, edge cases, performance checks; tests added into default suite.
Test assets & generator
warp/tests/io/assets/*, warp/tests/io/assets/generate_mesh_assets.py
Added deterministic mesh assets (triangle/cube variants in OBJ/STL/PLY) and a generator script to produce test fixtures.
Examples & docs
warp/examples/core/example_mesh.py, warp/tests/io/TESTING_STATUS.md
Example updated with optional wp.load_mesh usage; testing status/limitations documentation added.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant LoadAPI as load_mesh()
    participant Device as Device<br/>Resolution
    participant ReadAPI as read_mesh()
    participant Parser as Format<br/>Parser (OBJ/STL/PLY)
    participant Converter as MeshData<br/>.to_warp_mesh()
    participant Warp as wp.Mesh

    App->>LoadAPI: load_mesh(filename, device=None, ...)
    LoadAPI->>Device: resolve device
    LoadAPI->>ReadAPI: read_mesh(filename, ...)
    ReadAPI->>Parser: detect & dispatch by format
    Parser-->>ReadAPI: return MeshData(points, indices, ...)
    ReadAPI->>ReadAPI: apply flip_winding if requested
    ReadAPI-->>LoadAPI: MeshData
    LoadAPI->>Converter: to_warp_mesh(device, bvh_kwargs)
    Converter->>Warp: construct wp.Mesh (arrays -> wp.array, build BVH)
    Warp-->>LoadAPI: wp.Mesh
    LoadAPI-->>App: wp.Mesh
Loading
sequenceDiagram
    participant App as Application
    participant SaveAPI as save_mesh()
    participant Mesh as wp.Mesh
    participant Converter as Extract<br/>Geometry (to NumPy)
    participant Writer as Format Writer<br/>(OBJ/STL/PLY)
    participant FS as File System

    App->>SaveAPI: save_mesh(mesh, filename, binary=True)
    SaveAPI->>SaveAPI: detect format from extension
    SaveAPI->>Mesh: read mesh.points, mesh.indices
    SaveAPI->>Converter: convert to NumPy arrays
    Converter-->>SaveAPI: (points, indices, normals?)
    SaveAPI->>Writer: dispatch to write_obj/write_stl/write_ply
    Writer->>FS: write file (ASCII or binary)
    FS-->>SaveAPI: success
    SaveAPI-->>App: file saved
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title "Add native mesh file I/O (OBJ, STL, PLY)" directly and clearly summarizes the main change: introducing native mesh file I/O support for three formats. It is specific, concise, and accurately reflects the primary objective of the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 95.31% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Apr 15, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds native, zero-dependency mesh I/O (wp.load_mesh(), wp.save_mesh(), wp.read_mesh(), wp.MeshData) supporting OBJ, STL (binary/ASCII), and PLY (binary/ASCII) formats. All previously identified P1 issues from prior review rounds have been resolved — including the PLY normals/colors list growth bug, the binary PLY struct format string, the STL face-normal shape mismatch, the OBJ negative-index off-by-one, and the MeshData public export.

Confidence Score: 5/5

Safe to merge; all remaining findings are non-blocking P2 style/consistency issues.

All prior P1 bugs are confirmed resolved. The two remaining code issues (missing divisibility guard in write_ply/write_obj, and missing face-count check in read_ply) are P2: they only trigger on malformed inputs that don't arise from the normal save_mesh→load_mesh path. Tests are correctly registered in default_suite and cover the primary user flows.

warp/_src/io/ply.py (write_ply missing guard + read_ply empty-face check); warp/_src/io/obj.py (write_obj missing guard); warp/tests/io/validate_mesh_io.py (bare asserts).

Important Files Changed

Filename Overview
warp/_src/io/mesh.py Core dispatcher: load_mesh/read_mesh/save_mesh/MeshData with format detection, size guard, and flip_winding. Clean design, well-documented.
warp/_src/io/obj.py OBJ parser handles negative indices, fan triangulation, and normals/UV length mismatch guard; write_obj missing divisibility-by-3 check before reshape.
warp/_src/io/stl.py STL parser with binary/ASCII auto-detection, spatial-hash deduplication, and consistent deduplication between ASCII and binary paths; write_stl has explicit index validation.
warp/_src/io/ply.py PLY parser with header-driven type mapping for faces and vertices; write_ply lacks divisibility-by-3 guard and read_ply doesn't validate empty face count.
warp/tests/io/test_mesh.py Proper unittest.TestCase covering OBJ/STL/PLY load, round-trip, device, error, and winding-order tests; registered in default_suite.
warp/tests/unittest_suites.py TestMeshIO and TestMeshIODevices correctly added to default_suite; validate_mesh_io.py intentionally excluded as a standalone script.
warp/init.py load_mesh, save_mesh, read_mesh, and MeshData all properly re-exported under the public wp.* namespace.
warp/tests/io/validate_mesh_io.py Informal validation script using bare assert statements (disabled with -O); not a unittest module and not registered in the test suite per AGENTS.md conventions.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["wp.load_mesh(filename)"] --> B["read_mesh()"]
    A2["wp.read_mesh(filename)"] --> B

    B --> C{Format detection}
    C -->|".obj"| D["read_obj()"]
    C -->|".stl"| E["read_stl()"]
    C -->|".ply"| F["read_ply()"]

    E --> G{_detect_stl_format}
    G -->|binary| H["_read_binary_stl()"]
    G -->|ascii| I["_read_ascii_stl()"]
    H --> J["_deduplicate_stl_vertices()"]
    I --> J

    F --> K{PLY format header}
    K -->|ascii| L["_read_ply_ascii()"]
    K -->|binary| M["_read_ply_binary()"]

    D & J & L & M --> N["MeshData\n(points, indices, normals?, uvs?, colors?)"]

    N -->|"load_mesh path"| O["MeshData.to_warp_mesh()"]
    O --> P["wp.Mesh (BVH built)"]

    P --> Q["wp.save_mesh(mesh, filename)"]
    Q --> R{ext}
    R -->|".obj"| S["write_obj()"]
    R -->|".stl"| T["write_stl()"]
    R -->|".ply"| U["write_ply()"]
Loading

Reviews (9): Last reviewed commit: "lint: Move struct import to top level in..." | Re-trigger Greptile

Comment thread warp/_src/io/ply.py Outdated
Comment thread warp/_src/io/ply.py Outdated
Comment thread warp/_src/io/stl.py
Comment thread warp/__init__.py
Comment thread warp/tests/io/test_mesh.py Outdated
Comment thread warp/tests/unittest_suites.py
Comment thread warp/_src/io/stl.py
Comment thread warp/_src/io/ply.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (6)
warp/_src/io/stl.py (1)

35-63: Consider logging or narrowing exceptions in format detection.

Static analysis flags the bare except Exception clauses. While this is acceptable for format detection (where any failure should fall back to the next format), narrowing to specific exceptions (ValueError, struct.error, UnicodeDecodeError) would be more precise.

📝 Optional: Narrow exception types
     if file_size < 84:
         # Must be ASCII
         try:
             _read_ascii_stl(filename)
             return "ascii"
-        except Exception:
+        except (ValueError, RuntimeError, UnicodeDecodeError):
             pass
     # Try ASCII
     try:
         _read_ascii_stl(filename)
         return "ascii"
-    except Exception:
+    except (ValueError, RuntimeError, UnicodeDecodeError):
         raise RuntimeError(f"Unable to parse STL file: '{filename}'") from None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/io/stl.py` around lines 35 - 63, Replace the broad except Exception
handlers in the STL format detection with narrower exception types: in the
initial ASCII probe catch only UnicodeDecodeError, ValueError and OSError when
calling _read_ascii_stl(filename); in the binary probe you already catch
(struct.error, OSError, EOFError) so keep those; and in the final ASCII fallback
catch the same narrow set (UnicodeDecodeError, ValueError, OSError) instead of
Exception before raising RuntimeError; reference the functions/variables
_read_ascii_stl, filename, file_size and the struct.error handling to locate the
three try/except blocks to update.
warp/_src/io/mesh.py (2)

68-79: Consider renaming format parameter to avoid shadowing the builtin.

Static analysis flags format as shadowing Python's builtin. While this works, renaming to file_format or mesh_format would avoid the warning and potential confusion.

📝 Optional: Rename parameter
 def load_mesh(
     filename: str,
     device: DeviceLike | None = None,
     *,
-    format: str | None = None,
+    file_format: str | None = None,
     flip_winding: bool = False,
     ...

Apply the same change to the function body and read_mesh().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/io/mesh.py` around lines 68 - 79, Rename the parameter named format
in functions load_mesh and read_mesh to a non-builtin name (e.g., file_format or
mesh_format) and update all uses inside their bodies and any callers in the diff
to the new name; ensure function signatures, default handling, type hints
(format: str | None -> file_format: str | None) and any keyword references
(e.g., when passing format=...) are updated consistently to eliminate builtin
shadowing warnings.

173-244: Code duplication with load_mesh() for validation and format detection.

The validation logic (file existence, size check, format detection) is duplicated between load_mesh() and read_mesh(). Consider extracting a shared helper or having load_mesh() call read_mesh() internally.

♻️ Optional: Refactor to reduce duplication
def load_mesh(
    filename: str,
    device: DeviceLike | None = None,
    *,
    format: str | None = None,
    flip_winding: bool = False,
    max_file_size_mb: float | None = 500.0,
    stl_merge_tolerance: float = 1e-6,
    support_winding_number: bool = False,
    bvh_constructor: BvhConstructor | str | None = None,
    bvh_leaf_size: int | None = None,
) -> wp.Mesh:
    # Resolve device
    if device is None:
        device = get_device()
    
    # Delegate to read_mesh for parsing
    data = read_mesh(
        filename,
        format=format,
        flip_winding=flip_winding,
        max_file_size_mb=max_file_size_mb,
        stl_merge_tolerance=stl_merge_tolerance,
    )
    
    return data.to_warp_mesh(
        device=device,
        support_winding_number=support_winding_number,
        bvh_constructor=bvh_constructor,
        bvh_leaf_size=bvh_leaf_size,
    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/io/mesh.py` around lines 173 - 244, The validation and
format-detection logic in read_mesh is duplicated in load_mesh; update load_mesh
to delegate parsing/validation to read_mesh by calling read_mesh(filename,
format=format, flip_winding=flip_winding, max_file_size_mb=max_file_size_mb,
stl_merge_tolerance=stl_merge_tolerance) and then convert the returned MeshData
to a wp.Mesh via MeshData.to_warp_mesh(...) (preserve load_mesh's device
resolution and parameters support_winding_number, bvh_constructor,
bvh_leaf_size), removing the duplicated file existence/size/extension checks
from load_mesh.
warp/tests/io/validate_mesh_io.py (2)

159-194: Hardcoded relative path may fail depending on working directory.

The path "warp/tests/io/assets/cube.obj" assumes the script is run from the repository root. Consider using Path(__file__).parent / "assets/cube.obj" for robustness.

🔧 Proposed fix: Use path relative to script
 def test_mesh_operations_integration():
     """Test integration with Warp mesh operations."""
     print("\nTesting Warp mesh operations integration...")

     # Load a test mesh
-    mesh = wp.load_mesh("warp/tests/io/assets/cube.obj")
+    cube_path = Path(__file__).parent / "assets" / "cube.obj"
+    mesh = wp.load_mesh(str(cube_path))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/tests/io/validate_mesh_io.py` around lines 159 - 194, The
test_mesh_operations_integration function uses a hardcoded relative path in the
wp.load_mesh call which can break depending on CWD; change the mesh load to
build the path relative to the test file (use Path(__file__).parent / "assets" /
"cube.obj") and pass its string (str(...) or .as_posix()) into wp.load_mesh, and
add an import for Path from pathlib at the top of the file if missing so mesh =
wp.load_mesh(...) uses the robust path.

24-38: Test assumes real_world_assets directory exists with OBJ files.

If this directory is empty or missing, the test passes silently (the glob returns nothing). Consider adding an assertion that at least one file was tested.

🛡️ Proposed fix: Ensure at least one file is tested
 def test_real_world_meshes():
     """Test loading real-world mesh files."""
     print("Testing real-world meshes...")

     real_world_dir = Path(__file__).parent / "real_world_assets"

-    for mesh_file in real_world_dir.glob("*.obj"):
+    mesh_files = list(real_world_dir.glob("*.obj"))
+    if not mesh_files:
+        print("  No real-world OBJ files found, skipping...")
+        return
+    
+    for mesh_file in mesh_files:
         print(f"  {mesh_file.name}...", end=" ")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/tests/io/validate_mesh_io.py` around lines 24 - 38, The
test_real_world_meshes function currently iterates real_world_dir.glob("*.obj")
which can yield zero files and make the test vacuously pass; fix it by first
collecting the matches (e.g., files = list(real_world_dir.glob("*.obj"))) and
asserting that files is non-empty (assert len(files) > 0, "No real-world .obj
test files found in real_world_assets") before the for loop, then iterate over
that files list (referencing test_real_world_meshes, real_world_dir, mesh_file).
warp/_src/io/obj.py (1)

111-163: Consider documenting that face indices assume per-vertex normals/UVs.

The writer uses the same vertex index for position, UV, and normal (face[0]+1/face[0]+1/face[0]+1), which assumes normals and UVs are per-vertex and aligned with the points array. This is a valid simplification for indexed meshes, but worth noting since the reader doesn't preserve this indexing relationship.

📝 Optional: Add a note to the docstring
 def write_obj(
     points: np.ndarray,
     indices: np.ndarray,
     filename: str,
     normals: np.ndarray | None = None,
     uvs: np.ndarray | None = None,
 ) -> None:
     """Write a mesh to an OBJ file.
 
     Args:
         points: Vertex positions, shape (N, 3).
         indices: Triangle indices, shape (M * 3,).
         filename: Output file path.
-        normals: Optional vertex normals, shape (N, 3).
-        uvs: Optional texture coordinates, shape (N, 2).
+        normals: Optional vertex normals, shape (N, 3). Must be per-vertex
+            (same count as points) if provided.
+        uvs: Optional texture coordinates, shape (N, 2). Must be per-vertex
+            (same count as points) if provided.
     """
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/io/obj.py` around lines 111 - 163, Update the write_obj docstring
to explicitly state that normals and uvs are assumed to be per-vertex and
aligned with the points array (i.e., the writer uses the same index for
position, texture coordinate and normal when emitting faces), and mention that
faces are written using those per-vertex indices (OBJ 1-based indexing); modify
the docstring in the write_obj function to include this note so callers know the
indexing assumption and limitations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@warp/_src/io/__init__.py`:
- Around line 6-7: Update the top-level docstring in this module to capitalize
the product name and format the API symbol as a code literal: change the plain
text "warp" to "Warp" and the plain text reference to wp.load_mesh to a
code-formatted ``wp.load_mesh`` in the module docstring so it follows docstring
conventions.

In `@warp/_src/io/stl.py`:
- Around line 145-193: The ASCII STL reader (_read_ascii_stl) currently returns
raw points/indices without applying vertex deduplication, causing format
inconsistency; after parsing points and indices but before returning MeshData,
run the same deduplication/merge logic used by the binary path (use the existing
merge_tolerance parameter or helper function used elsewhere in read_stl) to
merge nearby vertices and remap indices, then return MeshData with the
deduplicated points (np.float32) and updated indices (np.int32); ensure you
reuse the same dedupe helper or logic so ASCII and binary STL behave
identically.

In `@warp/tests/io/assets/generate_mesh_assets.py`:
- Around line 330-331: The two face writes f.write("f 1 7 2\n") and f.write("f 2
7 6\n") are wrong: vertex 1 is on x=0 so those lines do not reference the cube's
right face, and the triangle winding is not inverted as the docstring requires;
replace these two f.write calls with face definitions that use the four vertex
indices whose x==1 (the right-face vertices) and invert the winding by reversing
the vertex order for each triangle (i.e., ensure each triangle's indices are the
opposite order from the current orientation).

In `@warp/tests/io/validate_mesh_io.py`:
- Around line 52-58: Remove the two unused variables indices_u and indices_v
which are defined but never referenced; delete their declarations so only the
actual mesh generation variables (rng, points, indices) remain (look for the
indices_u and indices_v definitions in the test setup and remove those lines).

---

Nitpick comments:
In `@warp/_src/io/mesh.py`:
- Around line 68-79: Rename the parameter named format in functions load_mesh
and read_mesh to a non-builtin name (e.g., file_format or mesh_format) and
update all uses inside their bodies and any callers in the diff to the new name;
ensure function signatures, default handling, type hints (format: str | None ->
file_format: str | None) and any keyword references (e.g., when passing
format=...) are updated consistently to eliminate builtin shadowing warnings.
- Around line 173-244: The validation and format-detection logic in read_mesh is
duplicated in load_mesh; update load_mesh to delegate parsing/validation to
read_mesh by calling read_mesh(filename, format=format,
flip_winding=flip_winding, max_file_size_mb=max_file_size_mb,
stl_merge_tolerance=stl_merge_tolerance) and then convert the returned MeshData
to a wp.Mesh via MeshData.to_warp_mesh(...) (preserve load_mesh's device
resolution and parameters support_winding_number, bvh_constructor,
bvh_leaf_size), removing the duplicated file existence/size/extension checks
from load_mesh.

In `@warp/_src/io/obj.py`:
- Around line 111-163: Update the write_obj docstring to explicitly state that
normals and uvs are assumed to be per-vertex and aligned with the points array
(i.e., the writer uses the same index for position, texture coordinate and
normal when emitting faces), and mention that faces are written using those
per-vertex indices (OBJ 1-based indexing); modify the docstring in the write_obj
function to include this note so callers know the indexing assumption and
limitations.

In `@warp/_src/io/stl.py`:
- Around line 35-63: Replace the broad except Exception handlers in the STL
format detection with narrower exception types: in the initial ASCII probe catch
only UnicodeDecodeError, ValueError and OSError when calling
_read_ascii_stl(filename); in the binary probe you already catch (struct.error,
OSError, EOFError) so keep those; and in the final ASCII fallback catch the same
narrow set (UnicodeDecodeError, ValueError, OSError) instead of Exception before
raising RuntimeError; reference the functions/variables _read_ascii_stl,
filename, file_size and the struct.error handling to locate the three try/except
blocks to update.

In `@warp/tests/io/validate_mesh_io.py`:
- Around line 159-194: The test_mesh_operations_integration function uses a
hardcoded relative path in the wp.load_mesh call which can break depending on
CWD; change the mesh load to build the path relative to the test file (use
Path(__file__).parent / "assets" / "cube.obj") and pass its string (str(...) or
.as_posix()) into wp.load_mesh, and add an import for Path from pathlib at the
top of the file if missing so mesh = wp.load_mesh(...) uses the robust path.
- Around line 24-38: The test_real_world_meshes function currently iterates
real_world_dir.glob("*.obj") which can yield zero files and make the test
vacuously pass; fix it by first collecting the matches (e.g., files =
list(real_world_dir.glob("*.obj"))) and asserting that files is non-empty
(assert len(files) > 0, "No real-world .obj test files found in
real_world_assets") before the for loop, then iterate over that files list
(referencing test_real_world_meshes, real_world_dir, mesh_file).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 62f385ee-ad9c-4fa6-b8e8-badd39573813

📥 Commits

Reviewing files that changed from the base of the PR and between 20da233 and 7611f26.

⛔ Files ignored due to path filters (7)
  • warp/tests/io/assets/cube.obj is excluded by !**/*.obj
  • warp/tests/io/assets/cube_quads.obj is excluded by !**/*.obj
  • warp/tests/io/assets/inverted_cube.obj is excluded by !**/*.obj
  • warp/tests/io/assets/negative_indices.obj is excluded by !**/*.obj
  • warp/tests/io/assets/triangle.obj is excluded by !**/*.obj
  • warp/tests/io/real_world_assets/suzanne.obj is excluded by !**/*.obj
  • warp/tests/io/real_world_assets/utah_teapot.obj is excluded by !**/*.obj
📒 Files selected for processing (24)
  • CHANGELOG.md
  • warp/__init__.py
  • warp/_src/io/__init__.py
  • warp/_src/io/mesh.py
  • warp/_src/io/obj.py
  • warp/_src/io/ply.py
  • warp/_src/io/stl.py
  • warp/examples/core/example_mesh.py
  • warp/tests/io/TESTING_STATUS.md
  • warp/tests/io/__init__.py
  • warp/tests/io/assets/cube.ply
  • warp/tests/io/assets/cube.stl
  • warp/tests/io/assets/cube_ascii.ply
  • warp/tests/io/assets/cube_ascii.stl
  • warp/tests/io/assets/generate_mesh_assets.py
  • warp/tests/io/assets/triangle.ply
  • warp/tests/io/assets/triangle.stl
  • warp/tests/io/assets/triangle_ascii.ply
  • warp/tests/io/assets/triangle_ascii.stl
  • warp/tests/io/test_kernel_integration.py
  • warp/tests/io/test_mesh.py
  • warp/tests/io/test_mesh_queries.py
  • warp/tests/io/validate_mesh_io.py
  • warp/tests/unittest_suites.py
👮 Files not reviewed due to content moderation or server errors (8)
  • warp/init.py
  • warp/examples/core/example_mesh.py
  • CHANGELOG.md
  • warp/tests/io/test_kernel_integration.py
  • warp/tests/io/test_mesh_queries.py
  • warp/_src/io/ply.py
  • warp/tests/io/test_mesh.py
  • warp/tests/io/TESTING_STATUS.md

Comment thread warp/_src/io/__init__.py Outdated
Comment thread warp/_src/io/stl.py
Comment thread warp/tests/io/assets/generate_mesh_assets.py Outdated
Comment thread warp/tests/io/validate_mesh_io.py Outdated
Fix 4 critical bugs, 2 important issues, and 9 nitpicks identified
during code review. All 23 TestMeshIO tests pass.

Critical fixes (P1):
* ply(io): Fix ASCII normals/colors pre-allocation bug
  - Was allocating only 1 entry, now pre-allocates to vertex_count
* ply(io): Fix binary write struct format string
  - Was appending multiple byte-order prefixes, now uses single prefix
* stl(io): Remove per-face normals from MeshData
  - STL has per-face normals, not per-vertex; omit to avoid shape mismatch
* warp: Export MeshData to public namespace
  - Add wp.MeshData alongside load_mesh/read_mesh/save_mesh

Important fixes (P2):
* stl(io): Apply vertex deduplication to ASCII STL reader
  - ASCII and binary now produce equivalent results
* test(io): Use public wp.MeshData import instead of internal path

Code quality improvements:
* obj(io): Clarify docstring about per-vertex assumption for normals/uvs
* stl(io): Narrow exception types in format detection
* mesh(io): Rename 'format' parameter to 'file_format' (builtin shadow)
* mesh(io): Refactor load_mesh to delegate to read_mesh (reduce duplication)
* test(io): Fix hardcoded path to use Path(__file__).parent
* test(io): Add assertions for missing test files
* test(io): Remove unused variables indices_u/indices_v
* gen: Fix inverted cube right-face triangles (correct vertices & winding)
* io: Capitalize 'Warp' in module docstring

Test assets regenerated with fixes.

Signed-off-by: DhairyashilGhatage <dhairyashil25@gmail.com>
Comment thread warp/tests/io/test_mesh.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
warp/_src/io/stl.py (1)

271-279: Duplicated face normal computation.

The normal computation logic is duplicated between binary and ASCII branches. Consider extracting to a helper for maintainability.

♻️ Optional extraction
def _compute_face_normal(v0: np.ndarray, v1: np.ndarray, v2: np.ndarray) -> np.ndarray:
    """Compute unit normal for a triangle face."""
    edge1 = v1 - v0
    edge2 = v2 - v0
    normal = np.cross(edge1, edge2)
    norm_length = np.linalg.norm(normal)
    if norm_length > 0:
        return normal / norm_length
    return np.array([0.0, 0.0, 1.0])

Then use normal = _compute_face_normal(v0, v1, v2) in both branches.

Also applies to: 298-306

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/io/stl.py` around lines 271 - 279, The face normal computation in
warp/_src/io/stl.py is duplicated in the binary and ASCII parsing branches
(blocks around the current diff and lines 298-306); extract this logic into a
helper function (e.g., _compute_face_normal(v0, v1, v2)) that returns a unit
normal (fallback to np.array([0.0,0.0,1.0]) on zero length) and replace both
occurrences with calls to _compute_face_normal(v0, v1, v2) to remove duplication
and improve maintainability.
warp/_src/io/ply.py (2)

335-337: Consider validating indices length.

indices.reshape(-1, 3) will raise a confusing error if len(indices) isn't divisible by 3. A pre-check with a clear error message would improve debuggability.

🛡️ Optional validation
+    if len(indices) % 3 != 0:
+        raise ValueError(f"indices length ({len(indices)}) must be divisible by 3")
     indices_reshaped = indices.reshape(-1, 3)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/io/ply.py` around lines 335 - 337, Before calling
indices.reshape(-1, 3) in this block (where indices_reshaped, num_verts,
num_faces are computed), add a pre-check that len(indices) is divisible by 3 and
raise a clear ValueError (or use the module's logger) if not; include the length
of indices and a hint that faces must be triples so the error message pinpoints
the bad input and avoids the confusing reshape exception.

119-130: Misleading guard conditions.

The checks normals is not None and colors is not None are always True here since they're initialized as lists (empty or pre-allocated). The code works because the properties won't exist in the header if has_normals/has_colors is False, but the intent would be clearer using the boolean flags directly.

♻️ Suggested clarification
-                    if prop_name == "nx" and normals is not None:
+                    if prop_name == "nx" and has_normals:
                         normals[vertex_idx][0] = float(values[idx])
-                    elif prop_name == "ny" and normals is not None:
+                    elif prop_name == "ny" and has_normals:
                         normals[vertex_idx][1] = float(values[idx])
-                    elif prop_name == "nz" and normals is not None:
+                    elif prop_name == "nz" and has_normals:
                         normals[vertex_idx][2] = float(values[idx])
-                    elif prop_name == "red" and colors is not None:
+                    elif prop_name == "red" and has_colors:
                         colors[vertex_idx][0] = int(values[idx])
-                    elif prop_name == "green" and colors is not None:
+                    elif prop_name == "green" and has_colors:
                         colors[vertex_idx][1] = int(values[idx])
-                    elif prop_name == "blue" and colors is not None:
+                    elif prop_name == "blue" and has_colors:
                         colors[vertex_idx][2] = int(values[idx])
-                    elif prop_name == "alpha" and colors is not None:
+                    elif prop_name == "alpha" and has_colors:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/io/ply.py` around lines 119 - 130, The guard conditions inside the
PLY property parsing (checking `prop_name` and assigning to
`normals[vertex_idx]` or `colors[vertex_idx]`) use `normals is not None` /
`colors is not None`, which is misleading because those variables are always
lists; update the conditionals to use the boolean flags `has_normals` and
`has_colors` instead (e.g., replace `and normals is not None` with `and
has_normals`, and `and colors is not None` with `and has_colors`) so the intent
is clear when handling `prop_name` branches that assign to `normals`, `colors`,
and their components using `vertex_idx` and `values`.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@warp/_src/io/ply.py`:
- Around line 229-231: The code in _read_ply_binary directly indexes
prop_offsets["x"], ["y"], ["z"] when filling points, which will KeyError if
those vertex properties are missing; add a validation step before the vertex
loop that checks prop_offsets contains "x","y","z" and raise a clear,
descriptive exception (including the filename or other context) if any are
missing; update _read_ply_binary's signature to accept filename (or adjust the
error text) so the raised error can include file context and reference the
failing keys, and use that validation to avoid the unsafe indexing in the points
assignment block.
- Around line 257-263: The face-index reader currently hardcodes idx_format =
endian + "i" and misparses non-int32 PLYs; update the PLY header parser (the
code that reads property list lines) to capture both the count_type and
value_type for the face property (e.g., the tokens after "property list" such as
"uchar uint vertex_indices") and store them (e.g., face_count_type,
face_value_type). Add a small mapping from PLY types to struct format characters
(handling signed/unsigned and 8/16/32-bit variants) and use endian +
mapped_format when computing idx_format/idx_size and when reading the count
value; then read count using the mapped count format and loop to unpack that
many indices into face_verts using the mapped value format instead of
hard-coding "i". Ensure the code path that builds face_verts references the new
stored face_value_type and face_count_type.

In `@warp/_src/io/stl.py`:
- Around line 127-132: The code allocates and fills face_normals but never uses
it; remove the dead allocation and per-iteration assignment by deleting the
face_normals = np.empty(...) line and the face_normals[i] = normal assignment
inside the for i in range(num_tris) loop, and instead just advance the file
pointer to consume the 12-byte normal per triangle (e.g., f.read(12)) so the
loop still reads/skips the normal bytes without allocating memory.

---

Nitpick comments:
In `@warp/_src/io/ply.py`:
- Around line 335-337: Before calling indices.reshape(-1, 3) in this block
(where indices_reshaped, num_verts, num_faces are computed), add a pre-check
that len(indices) is divisible by 3 and raise a clear ValueError (or use the
module's logger) if not; include the length of indices and a hint that faces
must be triples so the error message pinpoints the bad input and avoids the
confusing reshape exception.
- Around line 119-130: The guard conditions inside the PLY property parsing
(checking `prop_name` and assigning to `normals[vertex_idx]` or
`colors[vertex_idx]`) use `normals is not None` / `colors is not None`, which is
misleading because those variables are always lists; update the conditionals to
use the boolean flags `has_normals` and `has_colors` instead (e.g., replace `and
normals is not None` with `and has_normals`, and `and colors is not None` with
`and has_colors`) so the intent is clear when handling `prop_name` branches that
assign to `normals`, `colors`, and their components using `vertex_idx` and
`values`.

In `@warp/_src/io/stl.py`:
- Around line 271-279: The face normal computation in warp/_src/io/stl.py is
duplicated in the binary and ASCII parsing branches (blocks around the current
diff and lines 298-306); extract this logic into a helper function (e.g.,
_compute_face_normal(v0, v1, v2)) that returns a unit normal (fallback to
np.array([0.0,0.0,1.0]) on zero length) and replace both occurrences with calls
to _compute_face_normal(v0, v1, v2) to remove duplication and improve
maintainability.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4140c9a8-1cc5-45cb-8cf0-07e1d92daa31

📥 Commits

Reviewing files that changed from the base of the PR and between 7611f26 and 39d3020.

📒 Files selected for processing (9)
  • warp/__init__.py
  • warp/_src/io/__init__.py
  • warp/_src/io/mesh.py
  • warp/_src/io/obj.py
  • warp/_src/io/ply.py
  • warp/_src/io/stl.py
  • warp/tests/io/assets/generate_mesh_assets.py
  • warp/tests/io/test_mesh.py
  • warp/tests/io/validate_mesh_io.py
✅ Files skipped from review due to trivial changes (3)
  • warp/_src/io/init.py
  • warp/tests/io/test_mesh.py
  • warp/tests/io/validate_mesh_io.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • warp/tests/io/assets/generate_mesh_assets.py
  • warp/_src/io/obj.py
  • warp/_src/io/mesh.py

Comment thread warp/_src/io/ply.py
Comment thread warp/_src/io/ply.py Outdated
Comment thread warp/_src/io/stl.py Outdated
- Fix test_flip_winding_order to use file_format parameter instead of format
- Remove unused face_normals allocation in _read_binary_stl
- Parse face index types from PLY header instead of hardcoding int32
- Add validation for required x, y, z vertex properties in PLY parser
- Delete test_kernel_integration.py and test_mesh_queries.py (not proper
  unittest.TestCase subclasses, functionality covered by existing tests)

These changes address reviewer feedback on robustness, consistency,
and adherence to project testing conventions.

Signed-off-by: DhairyashilGhatage <dhairyashil25@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
warp/_src/io/stl.py (4)

55-56: EOFError is not raised by struct.unpack or f.read().

f.read() returns an empty bytes object at EOF rather than raising EOFError. If the file is truncated, struct.unpack will raise struct.error when it receives fewer bytes than expected. Consider removing EOFError from the exception tuple for clarity, or keep it for defensive coding if you prefer.

♻️ Optional: Remove unreachable exception type
-    except (struct.error, OSError, EOFError):
+    except (struct.error, OSError):
         pass
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/io/stl.py` around lines 55 - 56, The except block currently catches
EOFError even though struct.unpack and f.read() do not raise EOFError; update
the exception tuple in the except clause inside warp/_src/io/stl.py to remove
EOFError and keep (struct.error, OSError) so only reachable exceptions are
caught (or explicitly document/leave EOFError only if you intentionally want
defensive, non-actionable catching). Locate the except handling around the
struct.unpack/f.read logic and edit the tuple on that except line accordingly.

251-251: Consider validating that indices length is divisible by 3.

If indices has a length not divisible by 3, reshape(-1, 3) will raise a cryptic ValueError. An explicit check with a clear error message would improve debuggability.

♻️ Optional: Add indices validation
+    if len(indices) % 3 != 0:
+        raise ValueError(f"Indices length ({len(indices)}) must be divisible by 3")
     indices_reshaped = indices.reshape(-1, 3)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/io/stl.py` at line 251, Before calling indices.reshape(-1, 3) in
warp/_src/io/stl.py, validate that len(indices) % 3 == 0 and raise a ValueError
with a clear message (e.g. "indices length must be divisible by 3, got
{len(indices)}") if not; then proceed to perform indices_reshaped =
indices.reshape(-1, 3). This check should be placed immediately before the
existing indices_reshaped assignment so any malformed input fails with an
explicit error instead of a cryptic reshape ValueError.

268-276: Face normal computation is duplicated.

The normal computation logic (cross product, normalization, zero-length handling) is repeated in both binary and ASCII write paths. This is a minor duplication; extracting a helper is optional.

♻️ Optional: Extract helper function
def _compute_face_normal(v0: np.ndarray, v1: np.ndarray, v2: np.ndarray) -> np.ndarray:
    """Compute normalized face normal, defaulting to [0, 0, 1] for degenerate triangles."""
    edge1 = v1 - v0
    edge2 = v2 - v0
    normal = np.cross(edge1, edge2)
    norm_length = np.linalg.norm(normal)
    if norm_length > 0:
        return normal / norm_length
    return np.array([0.0, 0.0, 1.0])

Also applies to: 295-303

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/io/stl.py` around lines 268 - 276, Extract the duplicated
face-normal logic into a single helper (e.g. _compute_face_normal(v0, v1, v2))
that computes the cross product, normalizes it, and returns a default
np.array([0.0,0.0,1.0]) for degenerate triangles, then replace the repeated
blocks in both the binary and ASCII STL write paths with calls to this helper
(look for the blocks computing edge1=edge..., edge2=..., np.cross,
np.linalg.norm and the zero-length branch). Ensure the helper is
imported/defined in the same module so both write branches call it.

181-186: Vertex count per facet is not validated.

A malformed ASCII STL with fewer or more than 3 vertices per facet would produce incorrect triangle indices. Consider tracking vertex count per facet and raising an error if it doesn't equal 3.

♻️ Optional: Add per-facet vertex count validation
     in_solid = False
     in_facet = False
+    facet_vertex_count = 0

     with open(filename, encoding="utf-8") as f:
         for line in f:
             ...
             elif keyword == "facet" and in_solid:
                 in_facet = True
+                facet_vertex_count = 0
             elif keyword == "endfacet":
+                if facet_vertex_count != 3:
+                    raise ValueError(f"Invalid facet: expected 3 vertices, got {facet_vertex_count}")
                 in_facet = False
             elif keyword == "vertex" and in_facet:
                 if len(parts) >= 4:
                     points.append([float(parts[1]), float(parts[2]), float(parts[3])])
+                    facet_vertex_count += 1
                     if len(points) % 3 == 0:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/io/stl.py` around lines 181 - 186, The parser currently appends
vertices for the "vertex" keyword but never validates that each facet has
exactly 3 vertices; add a per-facet counter (e.g., facet_vertex_count) that you
increment whenever you append to points inside the "vertex" && in_facet branch,
and validate when the facet ends (on the "endfacet" keyword or when in_facet
becomes False): if facet_vertex_count != 3 raise a ValueError (or a clear
parsing exception) and avoid updating indices/vertex_offset for malformed
facets; reset facet_vertex_count to 0 at the start of each facet. Ensure the
index-building logic that uses vertex_offset remains unchanged for valid facets
and that malformed facets do not corrupt indices.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@warp/_src/io/ply.py`:
- Around line 46-49: The loops that scan for "end_header" currently treat an
empty read (EOF) as a blank line and spin; change each loop that does line =
f.readline().decode("ascii").strip() to first read the raw bytes (raw =
f.readline()), check if raw == b"" and raise a parse/EOF error (e.g.,
ValueError("Unexpected EOF while scanning for end_header")) before decoding,
then proceed with line = raw.decode("ascii").strip(); apply this same guard to
the two later header-skip loops (the other places that read and strip lines
searching for end_header).
- Around line 72-73: Replace the current any(...) checks that set
header_info["has_colors"] and header_info["has_normals"] with strict existence
checks so colors are only exposed when red, green, and blue are all present and
normals only when nx, ny, and nz are all present; update all code paths that
allocate or populate arrays (where np.empty(...) is used) — including the ASCII
parsing branch and the binary parsing branches that reference
header_info["has_colors"]/["has_normals"] — to only allocate and fill color
arrays when the full RGB set is present and only allocate/fill normal arrays
when all three normal components exist, avoiding fabrication of zero/garbage
channels when only a single component (e.g., nx or alpha) was in the header.
- Around line 74-77: The code is recording face list types for every "property
list" in a "face" element, which can pick up unrelated list properties; update
the branch inside warp/_src/io/ply.py where current_element == "face" so it only
sets header_info["face_index_count_type"] and
header_info["face_index_value_type"] when the property name (parts[4]) is the
face index field (e.g., "vertex_indices" or "vertex_index"); only assign those
two header_info keys in that conditional to avoid mis-parsing other face list
properties.

---

Nitpick comments:
In `@warp/_src/io/stl.py`:
- Around line 55-56: The except block currently catches EOFError even though
struct.unpack and f.read() do not raise EOFError; update the exception tuple in
the except clause inside warp/_src/io/stl.py to remove EOFError and keep
(struct.error, OSError) so only reachable exceptions are caught (or explicitly
document/leave EOFError only if you intentionally want defensive, non-actionable
catching). Locate the except handling around the struct.unpack/f.read logic and
edit the tuple on that except line accordingly.
- Line 251: Before calling indices.reshape(-1, 3) in warp/_src/io/stl.py,
validate that len(indices) % 3 == 0 and raise a ValueError with a clear message
(e.g. "indices length must be divisible by 3, got {len(indices)}") if not; then
proceed to perform indices_reshaped = indices.reshape(-1, 3). This check should
be placed immediately before the existing indices_reshaped assignment so any
malformed input fails with an explicit error instead of a cryptic reshape
ValueError.
- Around line 268-276: Extract the duplicated face-normal logic into a single
helper (e.g. _compute_face_normal(v0, v1, v2)) that computes the cross product,
normalizes it, and returns a default np.array([0.0,0.0,1.0]) for degenerate
triangles, then replace the repeated blocks in both the binary and ASCII STL
write paths with calls to this helper (look for the blocks computing
edge1=edge..., edge2=..., np.cross, np.linalg.norm and the zero-length branch).
Ensure the helper is imported/defined in the same module so both write branches
call it.
- Around line 181-186: The parser currently appends vertices for the "vertex"
keyword but never validates that each facet has exactly 3 vertices; add a
per-facet counter (e.g., facet_vertex_count) that you increment whenever you
append to points inside the "vertex" && in_facet branch, and validate when the
facet ends (on the "endfacet" keyword or when in_facet becomes False): if
facet_vertex_count != 3 raise a ValueError (or a clear parsing exception) and
avoid updating indices/vertex_offset for malformed facets; reset
facet_vertex_count to 0 at the start of each facet. Ensure the index-building
logic that uses vertex_offset remains unchanged for valid facets and that
malformed facets do not corrupt indices.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9286e847-0c94-4e02-87ea-61b911d390d0

📥 Commits

Reviewing files that changed from the base of the PR and between 39d3020 and 0172eba.

📒 Files selected for processing (4)
  • warp/__init__.pyi
  • warp/_src/io/ply.py
  • warp/_src/io/stl.py
  • warp/tests/io/test_mesh.py
✅ Files skipped from review due to trivial changes (1)
  • warp/tests/io/test_mesh.py

Comment thread warp/_src/io/ply.py
Comment thread warp/_src/io/ply.py Outdated
Comment thread warp/_src/io/ply.py Outdated
- Add EOF detection in PLY header parsing loops to prevent infinite spin
  on truncated files
- Require complete normal/color component sets (all RGB, all xyz)
  instead of partial matches to avoid zero/garbage channels
- Only capture face index types for vertex_indices property to avoid
  mis-parsing unrelated face list properties
- Remove unreachable EOFError from STL exception tuple
- Add indices length validation before reshape in write_stl
- Extract duplicated face normal computation to _compute_face_normal
- Validate exactly 3 vertices per facet in ASCII STL parser

Signed-off-by: DhairyashilGhatage <dhairyashil25@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
warp/_src/io/stl.py (1)

19-27: Normalize the new docstrings to the repo's RST style.

These docstrings still use plain text or quoted literals for code elements like "binary", "ascii", merge_tolerance, and MeshData. Please switch those to repo-standard inline markup so Sphinx/Pylance render them consistently.

As per coding guidelines, "Use double backticks for code elements and parameter cross-references (RST syntax): ``data``, ``.nvdb``—not italics (*data*)."

Also applies to: 70-80, 112-120, 132-140, 171-177, 229-245, 274-283

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/io/stl.py` around lines 19 - 27, Update the docstrings in
warp/_src/io/stl.py to use RST inline code markup (double backticks) instead of
plain quotes or italics: replace occurrences like "binary", "ascii",
merge_tolerance, MeshData, and any `.nvdb` references with ``binary``,
``ascii``, ``merge_tolerance``, ``MeshData`` and ``.nvdb`` respectively in the
relevant docstrings (including the blocks around lines 19-27, 70-80, 112-120,
132-140, 171-177, 229-245, 274-283); ensure parameter cross-references and code
elements follow the repo guideline of using double backticks throughout so
Sphinx/Pylance render consistently and update any inline mentions in functions
such as detect_stl_format and related docstrings accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@warp/_src/io/stl.py`:
- Around line 219-225: Before returning MeshData, add a guard that rejects
unterminated ASCII facets: check the parser state variables (e.g., in_facet and
facet_vertex_count) and if in_facet is True or facet_vertex_count is not 0 (or
not equal to expected 3), raise a RuntimeError mentioning the truncated STL and
filename; do this validation right before constructing/returning
MeshData(points=..., indices=...) so partially parsed facets are not silently
accepted.

---

Nitpick comments:
In `@warp/_src/io/stl.py`:
- Around line 19-27: Update the docstrings in warp/_src/io/stl.py to use RST
inline code markup (double backticks) instead of plain quotes or italics:
replace occurrences like "binary", "ascii", merge_tolerance, MeshData, and any
`.nvdb` references with ``binary``, ``ascii``, ``merge_tolerance``, ``MeshData``
and ``.nvdb`` respectively in the relevant docstrings (including the blocks
around lines 19-27, 70-80, 112-120, 132-140, 171-177, 229-245, 274-283); ensure
parameter cross-references and code elements follow the repo guideline of using
double backticks throughout so Sphinx/Pylance render consistently and update any
inline mentions in functions such as detect_stl_format and related docstrings
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 462de8dd-1a5a-4049-8159-cbab354830d0

📥 Commits

Reviewing files that changed from the base of the PR and between 0172eba and 31b0367.

📒 Files selected for processing (2)
  • warp/_src/io/ply.py
  • warp/_src/io/stl.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • warp/_src/io/ply.py

Comment thread warp/_src/io/stl.py
Comment thread warp/_src/io/obj.py Outdated
@dhairyashilRG dhairyashilRG force-pushed the feat/mesh-io-utilities branch from 31b0367 to fd4bfdf Compare April 16, 2026 05:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
warp/_src/io/stl.py (2)

285-302: Consider validating index bounds before writing.

If indices contains values >= len(points), an unclear IndexError will occur during write. Adding bounds validation would provide a clearer error message.

🛡️ Proposed validation
     if len(indices) % 3 != 0:
         raise ValueError(f"indices length must be divisible by 3, got {len(indices)}")
+    if len(indices) > 0 and (indices.min() < 0 or indices.max() >= len(points)):
+        raise ValueError(
+            f"indices out of bounds: min={indices.min()}, max={indices.max()}, "
+            f"but points has {len(points)} vertices"
+        )
     indices_reshaped = indices.reshape(-1, 3)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/io/stl.py` around lines 285 - 302, Validate index bounds before
indexing points: check that all values in indices are >= 0 and < len(points)
(e.g. via np.any or np.max) right after indices_reshaped is created and before
the binary write loop, and raise a clear ValueError naming the offending max/min
index and the points length. Update the block that creates indices_reshaped and
the subsequent binary path (references: indices, indices_reshaped, points) to
perform this bounds check and raise a descriptive error instead of letting an
IndexError occur during v0/v1/v2 assignment.

142-156: Consider validating file size before reading triangles.

If the file is truncated or num_tris is corrupt, struct.unpack will raise an unclear error. Pre-validating the expected size would provide a clearer error message.

🛡️ Proposed validation
     with open(filename, "rb") as f:
-        f.read(80)  # Skip header
+        header = f.read(80)  # Skip header
+        if len(header) < 80:
+            raise RuntimeError(f"Truncated STL file (incomplete header): '{filename}'")
         num_tris = struct.unpack("<I", f.read(4))[0]

         if num_tris == 0:
             raise RuntimeError(f"No triangles found in STL file: '{filename}'")

+        # Validate file has enough data for all triangles
+        expected_size = 84 + num_tris * 50
+        f.seek(0, 2)  # Seek to end
+        actual_size = f.tell()
+        if actual_size < expected_size:
+            raise RuntimeError(
+                f"Truncated STL file: expected {expected_size} bytes for {num_tris} triangles, "
+                f"got {actual_size}. File: '{filename}'"
+            )
+        f.seek(84)  # Seek back to triangle data
+
         # Pre-allocate: worst case = 3 * num_tris unique vertices
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/io/stl.py` around lines 142 - 156, The code reads num_tris from the
STL header but doesn't validate that the file contains enough bytes for num_tris
triangles, so a truncated or corrupt file can cause unclear struct.unpack
errors; compute the expected_size = 80(header) + 4(num_tris) + num_tris*50(bytes
per triangle) (or use 84 + num_tris*50) and compare it to the actual file size
(via os.path.getsize(filename) or f.seek(0,2)/f.tell()) before looping; also
validate that num_tris is non-negative and not absurdly large, and if the check
fails raise a clear RuntimeError referencing filename and both expected and
actual sizes so callers get an explicit message (update the block that reads
num_tris, raw_points and the triangle loop accordingly).
warp/_src/io/ply.py (2)

78-87: Redundant condition on line 82.

len(parts) >= 5 is already verified on line 80, so the duplicate check on line 82 is unnecessary.

🧹 Remove duplicate condition
             elif (
                 current_element == "face"
                 and len(parts) >= 5
                 and parts[1] == "list"
-                and len(parts) >= 5
                 and parts[4] in ("vertex_indices", "vertex_index")
             ):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/io/ply.py` around lines 78 - 87, The conditional in the PLY header
parsing branch redundantly checks len(parts) >= 5 twice; update the if in the
block that examines current_element == "face" to remove the duplicate `and
len(parts) >= 5` so the condition reads something like: current_element ==
"face" and len(parts) >= 5 and parts[1] == "list" and parts[4] in
("vertex_indices", "vertex_index"); keep the existing assignments to
header_info["face_index_count_type"] and header_info["face_index_value_type"]
unchanged.

299-310: Face index type map also missing int16/uint16.

For consistency with the vertex type_map, add the missing aliases here as well.

🔧 Add missing aliases and consider deduplication
         ply_to_struct = {
             "char": "b",
             "uchar": "B",
             "short": "h",
             "ushort": "H",
             "int": "i",
             "int8": "b",
+            "int16": "h",
             "int32": "i",
             "uint": "I",
             "uint8": "B",
+            "uint16": "H",
             "uint32": "I",
         }

Alternatively, extract a shared constant at module level to avoid maintaining two near-identical mappings.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@warp/_src/io/ply.py` around lines 299 - 310, The face index type map
dictionary ply_to_struct in warp/_src/io/ply.py is missing the int16/uint16
aliases; add entries "int16": "h" and "uint16": "H" to match the vertex
type_map, and to avoid divergence consider deduplicating by extracting the
shared type mapping into a single module-level constant (e.g., TYPE_TO_STRUCT)
used by both vertex and face mapping code paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@warp/_src/io/ply.py`:
- Around line 143-146: In the loop that reads vertex lines (the block iterating
for vertex_idx in range(header["vertex_count"]) that reads line =
f.readline().decode("ascii").strip() and splits into values), validate that
len(values) >= 3 before converting to float; if not, raise a clear ValueError
(or custom exception) that includes the vertex_idx and the raw line contents so
malformed/truncated vertex lines produce a descriptive error instead of an
IndexError. Ensure the error message references the header["vertex_count"]
context and the offending line for easier debugging.
- Around line 152-163: The conditional checks use "normals is not None" /
"colors is not None" which are True for empty lists and cause IndexError when
partial components exist; update the conditions in the vertex parsing block (the
branches that handle "nx","ny","nz" and "red","green","blue") to reference the
existence flags has_normals and has_colors (or check truthiness/length) instead
of "is not None" so you only write into normals[vertex_idx][...] and
colors[vertex_idx][...] when those arrays actually contain allocated per-vertex
vectors.
- Around line 321-324: The loop that reads faces currently breaks silently when
count_bytes = f.read(count_size) returns empty, masking truncated/corrupt PLY
files; instead modify the face-reading loop in warp/_src/io/ply.py to raise a
descriptive exception (e.g., EOFError or ValueError) when count_bytes is empty
or shorter than count_size (include context such as the face index and expected
count_size), and similarly validate subsequent reads (like vertex index bytes)
to raise on short reads rather than breaking silently; update code paths that
relied on the break to handle the new exception or let it propagate.
- Around line 206-220: The type mappings are missing PLY aliases; update the
type_map dict and the ply_to_struct mapping to include "int16" -> "h", "uint16"
-> "H", and "float64" -> "d" so properties named int16/uint16/float64 are
handled the same as short/ushort/double; locate and modify the type_map
definition and the ply_to_struct variable in warp/_src/io/ply.py to add these
three aliases.

In `@warp/_src/io/stl.py`:
- Around line 82-87: The multiplication raw_points * scale can overflow int64
when tolerance is extremely small; before computing grid_points in the function
(the block that defines scale and grid_points), compute max_coord =
np.max(np.abs(raw_points)) and compare max_coord * scale against
np.iinfo(np.int64).max; if it would exceed int64 range, either reduce scale to
max_int64 / max_coord (so grid_points = np.round(raw_points * reduced_scale)) or
raise a clear ValueError instructing the caller to use a larger tolerance, and
update any error message to reference tolerance, scale, raw_points and
grid_points so the issue is discoverable.

---

Nitpick comments:
In `@warp/_src/io/ply.py`:
- Around line 78-87: The conditional in the PLY header parsing branch
redundantly checks len(parts) >= 5 twice; update the if in the block that
examines current_element == "face" to remove the duplicate `and len(parts) >= 5`
so the condition reads something like: current_element == "face" and len(parts)
>= 5 and parts[1] == "list" and parts[4] in ("vertex_indices", "vertex_index");
keep the existing assignments to header_info["face_index_count_type"] and
header_info["face_index_value_type"] unchanged.
- Around line 299-310: The face index type map dictionary ply_to_struct in
warp/_src/io/ply.py is missing the int16/uint16 aliases; add entries "int16":
"h" and "uint16": "H" to match the vertex type_map, and to avoid divergence
consider deduplicating by extracting the shared type mapping into a single
module-level constant (e.g., TYPE_TO_STRUCT) used by both vertex and face
mapping code paths.

In `@warp/_src/io/stl.py`:
- Around line 285-302: Validate index bounds before indexing points: check that
all values in indices are >= 0 and < len(points) (e.g. via np.any or np.max)
right after indices_reshaped is created and before the binary write loop, and
raise a clear ValueError naming the offending max/min index and the points
length. Update the block that creates indices_reshaped and the subsequent binary
path (references: indices, indices_reshaped, points) to perform this bounds
check and raise a descriptive error instead of letting an IndexError occur
during v0/v1/v2 assignment.
- Around line 142-156: The code reads num_tris from the STL header but doesn't
validate that the file contains enough bytes for num_tris triangles, so a
truncated or corrupt file can cause unclear struct.unpack errors; compute the
expected_size = 80(header) + 4(num_tris) + num_tris*50(bytes per triangle) (or
use 84 + num_tris*50) and compare it to the actual file size (via
os.path.getsize(filename) or f.seek(0,2)/f.tell()) before looping; also validate
that num_tris is non-negative and not absurdly large, and if the check fails
raise a clear RuntimeError referencing filename and both expected and actual
sizes so callers get an explicit message (update the block that reads num_tris,
raw_points and the triangle loop accordingly).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 72bd9e95-01be-4b40-9c24-f92616457c88

📥 Commits

Reviewing files that changed from the base of the PR and between 31b0367 and fd4bfdf.

📒 Files selected for processing (2)
  • warp/_src/io/ply.py
  • warp/_src/io/stl.py

Comment thread warp/_src/io/ply.py
Comment thread warp/_src/io/ply.py Outdated
Comment thread warp/_src/io/ply.py
Comment thread warp/_src/io/ply.py Outdated
Comment thread warp/_src/io/stl.py
- Add check for unterminated facets at EOF in ASCII STL parser
- Reset facet_vertex_count to 0 after endfacet to avoid false positive
- Update STL docstrings to use RST inline code markup (double backticks)
- Fix OBJ negative index off-by-one error: len(points) + v_idx instead of
  len(points) + v_idx + 1 (OBJ spec: -1 is last vertex, not out of bounds)

Signed-off-by: DhairyashilGhatage <dhairyashil25@gmail.com>
PLY fixes:
- Validate vertex line has at least 3 values with descriptive error
- Use has_normals/has_colors flags instead of 'is not None' to avoid
  IndexError when partial components exist
- Raise RuntimeError on EOF during face reading instead of silent break
- Add int16, uint16, float64 type aliases to type_map and ply_to_struct
- Remove duplicate len(parts) >= 5 check in header parser

STL fixes:
- Guard against int64 overflow in vertex deduplication with tiny tolerances
- Validate index bounds before writing to prevent cryptic IndexError
- Validate file size matches num_tris in binary reader for truncated files

Signed-off-by: DhairyashilGhatage <dhairyashil25@gmail.com>
Comment thread warp/_src/io/obj.py Outdated
OBJ files can have different vn (normal) count than v (vertex) count for
hard-edge meshes (e.g., 24 normals for 8 cube vertices). The current code
creates normals_array with vn count, violating MeshData.normals shape (N, 3)
where N is vertex count.

Only include normals when len(normals) == len(points) to ensure
per-vertex alignment. A complete fix would require parsing face //vn indices
and splitting vertices for non-manifold normals.

Signed-off-by: DhairyashilGhatage <dhairyashil25@gmail.com>
Comment thread warp/_src/io/obj.py Outdated
OBJ files can have different vt (UV) count than v (vertex) count.
Apply the same validation to uvs_array as normals_array to ensure
per-vertex data alignment. This prevents MeshData.uvs.shape[0] from
silently differing from MeshData.points.shape[0].

Signed-off-by: DhairyashilGhatage <dhairyashil25@gmail.com>
Fixes PLC0415 violations where 'import struct' was inside functions
instead of at module level. All imports now at top of file.

Signed-off-by: DhairyashilGhatage <dhairyashil25@gmail.com>
@dhairyashilRG

Copy link
Copy Markdown
Author

Greptile Summary

This PR adds native, zero-dependency mesh I/O (wp.load_mesh, wp.save_mesh, wp.read_mesh) for OBJ, binary/ASCII STL, and binary/ASCII PLY, cleanly structured behind a MeshData dataclass and correctly re-exported into the wp namespace. Most P1 issues from prior review rounds have been resolved; one new P1 remains:

Confidence Score: 4/5

Safe to merge after restoring the accidentally deleted CHANGELOG entry for GH-1327; all other remaining findings are P2 quality concerns.

One P1 remains: an unrelated CHANGELOG entry was removed in the diff (likely a rebase artifact). The parsers and public API are well-structured and the prior P1 bugs (PLY normal allocation, struct format string, STL shape mismatch, negative index off-by-one, MeshData not exported) are all resolved. P2 items (PLY unknown-type misalignment, STL triple-parse, write_stl empty-mesh crash) do not block correctness for the intended use cases.

CHANGELOG.md (deleted GH-1327 entry must be restored)

Important Files Changed

Filename Overview
CHANGELOG.md New mesh I/O entry added correctly; unrelated GH-1327 indexedarray entry was accidentally deleted, likely a rebase conflict artifact.
warp/_src/io/mesh.py New dispatch layer: MeshData dataclass, load_mesh/read_mesh/save_mesh/flip_winding helpers — clean structure, correct device handling, well-documented.
warp/_src/io/obj.py OBJ reader/writer with fan triangulation, negative index fix, normals length guard; UV array still lacks the same length guard as normals.
warp/_src/io/ply.py PLY ASCII/binary reader/writer; binary reader silently misaligns data if any vertex property type is not in the internal type_map.
warp/_src/io/stl.py STL binary/ASCII reader with spatial-hash deduplication; ASCII format is reparsed up to three times and write_stl crashes with a confusing error on empty meshes.
warp/tests/io/test_mesh.py Comprehensive unittest.TestCase-based suite covering OBJ/STL/PLY load, roundtrip, device, winding-order, and error cases; uses proper Path(file) relative paths.
warp/tests/unittest_suites.py TestMeshIO and TestMeshIODevices correctly added to default_suite as required by AGENTS.md.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["wp.load_mesh(filename, ...)"] --> B["read_mesh(filename, ...)"]
    C["wp.read_mesh(filename, ...)"] --> B
    B --> D{File exists?\nSize OK?}
    D -- No --> E[FileNotFoundError / ValueError]
    D -- Yes --> F{Detect format}
    F -- ext=obj --> G["read_obj()"]
    F -- ext=stl --> H["read_stl()"]
    F -- ext=ply --> I["read_ply()"]
    H --> J{Binary or ASCII?}
    J -- binary --> K["_read_binary_stl()"]
    J -- ascii --> L["_read_ascii_stl()"]
    K --> M["_deduplicate_stl_vertices()"]
    L --> N["_deduplicate_stl_vertices()"]
    I --> O{Format?}
    O -- ascii --> P["_read_ply_ascii()"]
    O -- binary --> Q["_read_ply_binary()"]
    G --> R[MeshData]
    M --> R
    N --> R
    P --> R
    Q --> R
    R --> S{flip_winding?}
    S -- Yes --> T["_apply_flip_winding()"]
    S -- No --> U[MeshData returned]
    T --> U
    U -- load_mesh path --> V["MeshData.to_warp_mesh(device)"]
    V --> W[wp.Mesh with BVH]
    X["wp.save_mesh(mesh, filename)"] --> Y{Detect ext}
    Y -- obj --> Z["write_obj()"]
    Y -- stl --> AA["write_stl()"]
    Y -- ply --> AB["write_ply()"]
Loading

Comments Outside Diff (1)

  1. CHANGELOG.md, line 34-36 (link)
    P1 Unrelated changelog entry accidentally removed
    The Support indexedarray fields in struct #1327 entry for wp.indexedarray support in @wp.struct was deleted in this diff. This is almost certainly a rebase/merge-conflict resolution artifact — the AGENTS.md guide specifically warns about this pattern ("After rebasing, diff CHANGELOG.md against the target branch to catch duplicate entries or other issues from clean but incorrect merges"). Restoring the dropped lines is needed to preserve the release notes for that feature.

Reviews (8): Last reviewed commit: "fix(io): Discard OBJ UVs when count mism..." | Re-trigger Greptile

Greptile Summary

This PR adds native, zero-dependency mesh I/O (wp.load_mesh, wp.save_mesh, wp.read_mesh) for OBJ, binary/ASCII STL, and binary/ASCII PLY, cleanly structured behind a MeshData dataclass and correctly re-exported into the wp namespace. Most P1 issues from prior review rounds have been resolved; one new P1 remains:

Confidence Score: 4/5

Safe to merge after restoring the accidentally deleted CHANGELOG entry for GH-1327; all other remaining findings are P2 quality concerns.

One P1 remains: an unrelated CHANGELOG entry was removed in the diff (likely a rebase artifact). The parsers and public API are well-structured and the prior P1 bugs (PLY normal allocation, struct format string, STL shape mismatch, negative index off-by-one, MeshData not exported) are all resolved. P2 items (PLY unknown-type misalignment, STL triple-parse, write_stl empty-mesh crash) do not block correctness for the intended use cases.

CHANGELOG.md (deleted GH-1327 entry must be restored)

Important Files Changed

Filename Overview
CHANGELOG.md New mesh I/O entry added correctly; unrelated GH-1327 indexedarray entry was accidentally deleted, likely a rebase conflict artifact.
warp/_src/io/mesh.py New dispatch layer: MeshData dataclass, load_mesh/read_mesh/save_mesh/flip_winding helpers — clean structure, correct device handling, well-documented.
warp/_src/io/obj.py OBJ reader/writer with fan triangulation, negative index fix, normals length guard; UV array still lacks the same length guard as normals.
warp/_src/io/ply.py PLY ASCII/binary reader/writer; binary reader silently misaligns data if any vertex property type is not in the internal type_map.
warp/_src/io/stl.py STL binary/ASCII reader with spatial-hash deduplication; ASCII format is reparsed up to three times and write_stl crashes with a confusing error on empty meshes.
warp/tests/io/test_mesh.py Comprehensive unittest.TestCase-based suite covering OBJ/STL/PLY load, roundtrip, device, winding-order, and error cases; uses proper Path(file) relative paths.
warp/tests/unittest_suites.py TestMeshIO and TestMeshIODevices correctly added to default_suite as required by AGENTS.md.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["wp.load_mesh(filename, ...)"] --> B["read_mesh(filename, ...)"]
    C["wp.read_mesh(filename, ...)"] --> B
    B --> D{File exists?\nSize OK?}
    D -- No --> E[FileNotFoundError / ValueError]
    D -- Yes --> F{Detect format}
    F -- ext=obj --> G["read_obj()"]
    F -- ext=stl --> H["read_stl()"]
    F -- ext=ply --> I["read_ply()"]
    H --> J{Binary or ASCII?}
    J -- binary --> K["_read_binary_stl()"]
    J -- ascii --> L["_read_ascii_stl()"]
    K --> M["_deduplicate_stl_vertices()"]
    L --> N["_deduplicate_stl_vertices()"]
    I --> O{Format?}
    O -- ascii --> P["_read_ply_ascii()"]
    O -- binary --> Q["_read_ply_binary()"]
    G --> R[MeshData]
    M --> R
    N --> R
    P --> R
    Q --> R
    R --> S{flip_winding?}
    S -- Yes --> T["_apply_flip_winding()"]
    S -- No --> U[MeshData returned]
    T --> U
    U -- load_mesh path --> V["MeshData.to_warp_mesh(device)"]
    V --> W[wp.Mesh with BVH]
    X["wp.save_mesh(mesh, filename)"] --> Y{Detect ext}
    Y -- obj --> Z["write_obj()"]
    Y -- stl --> AA["write_stl()"]
    Y -- ply --> AB["write_ply()"]
Loading

Comments Outside Diff (1)

  1. CHANGELOG.md, line 34-36 (link)
    P1 Unrelated changelog entry accidentally removed
    The Support indexedarray fields in struct #1327 entry for wp.indexedarray support in @wp.struct was deleted in this diff. This is almost certainly a rebase/merge-conflict resolution artifact — the AGENTS.md guide specifically warns about this pattern ("After rebasing, diff CHANGELOG.md against the target branch to catch duplicate entries or other issues from clean but incorrect merges"). Restoring the dropped lines is needed to preserve the release notes for that feature.

Reviews (8): Last reviewed commit: "fix(io): Discard OBJ UVs when count mism..." | Re-trigger Greptile

One aspect appears to be a false positive. The CHANGELOG diff shows only additions (mesh I/O entry) with no deletions. No GH-1327 entry exists in the main branch CHANGELOG.md to have been accidentally removed.

@dhairyashilRG dhairyashilRG marked this pull request as ready for review April 16, 2026 16:18
@dhairyashilRG

Copy link
Copy Markdown
Author

I've addressed the initial feedback from the automated bots (fixed the keyword arguments, resolved the OBJ normals shape mismatch, and updated the PLY parsing). All local tests are passing. Ready for a human review whenever someone has time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant