Skip to content

feat(data_load_3d): static PLY/PCD 3D loader (PointCloud + Mesh3D)#189

Merged
facontidavide merged 10 commits into
mainfrom
feat/data-load-3d
Jul 6, 2026
Merged

feat(data_load_3d): static PLY/PCD 3D loader (PointCloud + Mesh3D)#189
facontidavide merged 10 commits into
mainfrom
feat/data-load-3d

Conversation

@facontidavide

Copy link
Copy Markdown
Contributor

Summary

New data_load_3d DataSource plugin: loads a static .ply / .pcd file as a single 3D object at timestamp 0, plus a scalar summary. Not a port — a new plugin.

Input Object
.pcd (any encoding) sdk::PointCloud
.ply vertices-only sdk::PointCloud
.ply with faces sdk::Mesh3D (raw asset bytes; renderer/Assimp parses geometry)
  • PCD DATA: ascii, binary, binary_compressed (LZF, via vendored liblzf 3.6, BSD-2-Clause).
  • PLY format: ascii, binary_little_endian, binary_big_endian.
  • Emits an ObjectStore topic <stem> (tagged {"builtin_object_type":"kPointCloud"|"kMesh3D"} so PJ4 classifies + renders it) and a scalar topic <stem>/summary (num_points for clouds, num_faces for meshes, centroid/x,y,z). Degrades to summary-only when no object-write host is bound.

Architecture

loader_3d.cpp (FileSourceBase) dispatches by extension to two hand-rolled header parsers (pcd_reader, ply_reader) that funnel through one shared packing core (cloud_common). All returned objects are self-owning (own their bytes via anchor), so they are safe to copy/store past the reader call. The readers are total functions over untrusted input: malformed bytes return an error, never throw / read-or-write OOB / hang. Notable guards: from_chars numeric parsing (no throwing stoul), a kMaxCloudBytes (2 GiB) cap, a zero-stride-with-points reject, and a decompression-bomb guard (validate declared uncompressed size against width*height*point_step before allocating).

Testing

  • 4 unit-test suites (cloud_common, pcd_reader, ply_reader, loader_helpers), all green under the strict -Werror wall. Parsers are verified via serialize→deserialize round-trips through the real SDK codecs.
  • Coverage includes ascii/binary/binary_compressed PCD (incl. a mixed-field-width transpose), ascii/binary-LE/binary-BE PLY, faces→mesh routing, self-ownership survival past input destruction, and every hostile-input error path (truncated/corrupt/oversize headers, size-mismatch bombs, zero-stride, overflow, malformed tokens, CRLF, missing magic).
  • The builtin_object_type metadata JSON — the string PJ4 classifies on to render the object — is pinned by a test.
  • CI auto-discovers the plugin (has conanfile.py + manifest.json, no arrow/ dep) into the standard build/test matrix; no workflow edit needed.

Notes / follow-ups

  • End-to-end render in PJ4 was not exercised here (no pj_proto_app/GUI host in the per-plugin CI workspace) — recommended as a manual verification. The object-write seam and the metadata contract were verified against the actual SDK/PJ4 classifier headers.
  • The thin FileSourceBase glue (summary-schema branching, null-host fallback) is verified by inspection + the pinned metadata/path-helper tests; a FakeHost loader test is a reasonable future addition (matches toolbox_mosaico's pattern).
  • Vendored liblzf is unmodified (byte-identical to upstream 3.6, sha256 recorded), license-clean (BSD-2-Clause), compiled as isolated C static libs off the C++ warning wall.

🤖 Generated with Claude Code

@facontidavide facontidavide self-assigned this Jul 6, 2026
facontidavide and others added 10 commits July 6, 2026 16:34
Task 1 wired only the subdirectory-mode loader list; the self-contained
loaders (csv/mcap/parquet/ulog) are in both lists, so add data_load_3d to
the standalone-mode else() block too. Plan Step 5 updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dening

Address findings from spec/code-quality/Codex reviews of the packing core:
- Return a self-owning PJ::sdk::PointCloud (bytes held via BufferAnchor)
  instead of the copy-unsafe BuiltCloud wrapper whose data span dangled on
  copy; drop BuiltCloud entirely.
- Guard width*height*point_step against integer overflow and oversize
  allocation before resize() (kMaxCloudBytes cap), and reject a positive
  point count with zero stride (would spin the ascii loop). Prevents heap
  OOB writes / DoS on crafted headers.
- Overflow-safe computeLayout stride accumulation (uint64_t + uint32_t cap).
- Parse ascii with std::locale::classic() so dot-decimals survive a global
  comma-decimal locale (Qt may install one).
- Skip non-finite doubles when packing integer fields (cast would be UB).
- static_assert little-endian host; fix computeCentroid doc.
- Tests: overflow rejection, no-fields rejection, kUnknown/short-ascii
  errors, count>1 pack/byte-swap, and a copy-survives-destruction test
  that locks in the anchor ownership.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Parse .pcd header (FIELDS/SIZE/TYPE/COUNT/WIDTH/HEIGHT/DATA) into ParsedFields
and funnel through cloud_common::buildPointCloud. ascii + binary supported;
binary_compressed returns a clean error (deferred to a later task). Numeric
header fields parsed with std::from_chars so malformed input returns an error
instead of throwing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Parse the ascii .ply header; vertices-only files become a self-owning
PointCloud via cloud_common, files with faces become a self-owning Mesh3D
holding the raw .ply bytes (renderer parses geometry). Element counts parsed
with std::from_chars (no throwing); vertex-count uint32 narrowing guarded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dispatch by extension (.pcd/.ply) to the readers, push the serialized
PointCloud/Mesh3D to the ObjectStore at t=0 with builtin_object_type metadata
(kPointCloud/kMesh3D) so PJ4 classifies+renders it, and push a scalar summary
(num_points, num_faces for meshes, centroid x/y/z). Falls back to summary-only
when no object-write host is bound.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Vendor liblzf 3.6 (BSD-2-Clause) as separate C static libs (kept out of the
C++ -Werror wall). Decode PCD binary_compressed: parse the compressed/
uncompressed size header, LZF-decompress, and transpose the field-major (SoA)
buffer to packed AoS. The declared uncompressed size is validated against
width*height*point_step (<= kMaxCloudBytes) BEFORE allocating, guarding against
a decompression bomb.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document the loader: PLY/PCD routing (cloud vs mesh), supported encodings
(PCD ascii/binary/binary_compressed, PLY ascii/binary LE+BE), the t=0
single-snapshot behavior, the <stem> object topic + <stem>/summary scalars,
and the VIEWPOINT-ignored limitation. Note the new (non-port) plugin in
PORTING_PLAN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract builtinObjectMetadata/lowerExtension/fileStem into loader_helpers.hpp
and unit-test them (house style: sibling data_load plugins test pure helpers).
Locks the {"builtin_object_type":"kPointCloud"|"kMesh3D"} JSON — the string
PJ4 classifies on to render the object — plus the extension/stem/dotfile logic,
against regression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@facontidavide facontidavide merged commit c7abe46 into main Jul 6, 2026
38 checks passed
@facontidavide facontidavide deleted the feat/data-load-3d branch July 6, 2026 14:59
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