Skip to content

Export mesh layers when the field is empty (#662) - #686

Merged
leonardoaraujosantos merged 1 commit into
mainfrom
fix/662-mesh-only-export
Oct 4, 2026
Merged

leonardoaraujosantos merged 1 commit into
mainfrom
fix/662-mesh-only-export

Conversation

@leonardoaraujosantos

Copy link
Copy Markdown
Contributor

Problem

clay_document_mesh_combined meshed the field first, through clay_document_mesh, and returned that call's error before it considered any mesh layer. clay_document_mesh refuses an empty tape with CLAY_ERROR_INVALID_ARGUMENT ("empty document") and a field that the hidden-group drop leaves with no triangles with CLAY_ERROR_BACKEND ("meshing produced no triangles"). So a document whose only visible geometry is mesh layers could not be exported through the one call that exists to export mesh layers:

  • every SDF layer hidden,
  • no SDF item at all (a document that is only an imported reference),
  • every surface group hidden.

ClaySpaceDesktop worked around it by transforming and concatenating each visible mesh layer itself (CyberdyneCorp/ClaySpaceDesktop#213).

Root cause and design

The combined call was implemented as "field, then layers", with the field's refusals treated as the call's. Those refusals are right for clay_document_mesh, which was asked for a surface and has none to give, and wrong for the combined call when the mesh layers are the surface.

  • The visible mesh layers are placed first (place_visible_mesh_layers / place_mesh_layer, pulled out of the call unchanged), because whether there are any decides what an empty field means.
  • No visible mesh layer: the call delegates to clay_document_mesh, so the header's "returns exactly what clay_document_mesh would" stays literally true, refusals included. A document with nothing visible at all is still CLAY_ERROR_INVALID_ARGUMENT.
  • Some visible: the field goes through the shared mesh_tape with a new EmptyField::ContributesNothing mode. An empty tape, or a mesh the hidden-group drop leaves with no triangles, yields no field part rather than an error. The standalone calls (clay_document_mesh, clay_document_mesh_sdf_layer) keep the default Refuse.
  • Rejected: calling clay_document_mesh and reading its error as "empty". CLAY_ERROR_INVALID_ARGUMENT also means malformed params or an over-fine voxel size, and CLAY_ERROR_BACKEND also means a mesher failure; swallowing either would turn a real error into a silent export of the mesh layers alone. Those are all still returned, and clay_mesh_params is validated whether or not the field is empty.
  • mesh_tape's voxel-size pricing (caller's size or extent over resolution, the finite check, the grid-sample ceiling) moves verbatim into price_mesh_voxel. The two empty checks alone took mesh_tape from cognitive complexity 15 to 19; it is now 14. clay_document_mesh_combined goes from 22 to 7.

OpenSpec: openspec/changes/export-mesh-layers-without-a-field modifies the c-abi requirement "Exporting a document with its imported meshes is explicit", with two new scenarios (mesh-only document exports; nothing visible is still refused). The header comment above clay_document_mesh_combined and the docs/08-mesh-readback.md table row say the same.

Tests

tests/unit/test_c_mesh_layer.cpp, four new cases. Each export case places a tetrahedron mesh layer at x = 5 and asserts the result is that tetrahedron alone (4 vertices, 12 indices, bounds entirely past x = 4), and asserts clay_document_mesh alone still refuses:

  • a hidden SDF layer beside the mesh layer,
  • a document with no SDF item,
  • every surface group hidden (the second refusal path, from a non-empty tape),
  • nothing visible at all is still refused like the field call and writes a null mesh; a malformed clay_mesh_params (struct_size = 0) is refused even with a mesh layer visible.

Proof they fail on the unfixed code (the test file over main's clay_c.cpp):

c mesh combine: a hidden SDF layer leaves the mesh layers to export   REQUIRE( 1 == 0 )
c mesh combine: a document with no SDF item exports its mesh layer   REQUIRE( 1 == 0 )
c mesh combine: a field that meshes to nothing contributes nothing   REQUIRE( 8 == 0 )
test cases: 21 | 18 passed | 3 failed

The fourth case passes either way by design: it guards that the refusals that should survive still do.

Verification

  • ctest --preset cpu-only: 10/10 passed (all four unit shards).
  • clay_unit_tests '-sf=*test_c_mesh*': 86 cases, 247275 assertions, all pass.
  • npx @fission-ai/openspec@1.12.0 validate --all --strict: 78 passed.
  • GCC 16 -Wall -Wextra -Wpedantic -Wshadow -Werror -fsyntax-only on bindings/c/clay_c.cpp and the test file: clean.
  • tools/release_check.py --skip-slow: version, configure, build, tests (11/11), cpu parity (48 cases), layering, dialect, licenses, task-symbols, bindings (imported pyclay), kernels, c-abi, openspec all PASS. The device and four hardware/* rows fail on main too: they compare the tree against the last recorded device/hardware run, and main has moved since (include/clay/eval/bake_volume.h in A full brick build disagrees with its own raw field inside the band -- up to 0.263 on random documents, and a Move leaves those bricks stale #649, 48 engine files since the device gate). This change touches neither.

ABI

ABI 0.124.0 -> 0.124.0 (unchanged). No signature, struct or entry point moves; a call that used to refuse now succeeds on documents with an empty field and a visible mesh layer. Hosts keeping a transform-and-concat fallback on refusal can drop it; keeping it is harmless.

Closes #662

clay_document_mesh_combined meshed the field through clay_document_mesh
and returned its refusal before looking at a mesh layer, so a document
whose only visible geometry is mesh layers -- every SDF layer hidden, no
SDF item at all, or every surface group hidden -- could not be exported.

The visible mesh layers are now placed first. With none visible the call
is still exactly clay_document_mesh, refusals included. With some, the
field goes through mesh_tape in an EmptyField::ContributesNothing mode:
an empty tape or a mesh the hidden-group drop leaves without triangles
yields no field part instead of an error, while every other failure
(malformed params, unbounded scene, over-fine voxel, mesher error) is
still returned.

The voxel-size pricing moves unchanged out of mesh_tape into
price_mesh_voxel to keep it within the complexity target.
@leonardoaraujosantos
leonardoaraujosantos merged commit 3a1a95d into main Oct 4, 2026
16 checks passed
@leonardoaraujosantos
leonardoaraujosantos deleted the fix/662-mesh-only-export branch October 4, 2026 11:02
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.

clay_document_mesh_combined refuses a document whose visible geometry is only mesh layers

1 participant