Conversation
…ring Extends String.hpp with utilities needed for locale-independent mesh IO serialization and efficient whitespace parsing of OBJ/PLY files. - split(): add predicate overload (single O(n) pass); no-arg overload now splits on any whitespace via the predicate path; string-delimiter overload fast-paths single-char delimiters through the predicate overload - to_string_view(buf, val): caller-supplied buffer variant of to_chars with no heap allocation; intended for hot paths (e.g. per-vertex coordinate output in a write loop) - to_string(val): convenience wrapper over to_string_view for one-off conversions; allocates a std::string Replaces CheckToNumericFP.cmake + CheckToCharsFP.cmake with a unified CheckCharconvFP.cmake that probes from_chars and to_chars separately. EDUCE_CORE_NEED_CHARCONV_FP is now set only by the from_chars probe (the only path with a compile-time fallback). Updates README and conductor artifacts for the mesh-io_20260323 track. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace split_ws references with split() (no-arg whitespace form) - Update to_string_view usage to caller-provided buffer pattern: to_string_view(buf, val) instead of to_string_view(val) - Record preparatory String.hpp commit (2f67926) in plan overview - Bump updated date to 2026-03-25 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…h-io_20260323) Add compile-time detection traits has_normal<V>, has_color<V>, and has_chart<UVMapT> in detail/MeshTraits.hpp using the std::void_t detection idiom. IO functions use these via if constexpr to conditionally read/write normal, color, and chart data without extra overloads. Register TestMeshIO.cpp and add MeshTraits.hpp to installed headers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add read_obj / write_obj across all three tiers in MeshIO_OBJ.hpp. Write uses to_string_view(buf, val) for zero-allocation numeric output. Read uses split() + parse_face_ref() for locale-independent parsing. Detection traits (has_normal, has_color, has_chart) drive all conditional IO via if constexpr. Multi-chart write enforces has_chart via static_assert. Normals are written/read per-vertex; UVs are per-wedge. MTL texture paths are recovered from map_Kd in material-declaration order. Also adds Mesh::num_vertices() / num_faces() required for iteration. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…0260323) - Add MeshUtils.hpp with expand_at_seams: deduplicates (vertex, uv-pool) pairs across face corners, splitting seam vertices and returning a flat per-vertex UV array suitable for PLY output - Add MeshIO_PLY.hpp with write_ply (Tiers 1–3) and read_ply (Tiers 1–2); supports ASCII write, ASCII and binary-little-endian read, per-vertex s/t UV properties, and comment TextureFile header (MeshLab convention) - Add expand_at_seams and PLY round-trip tests (Tasks 3.1, 3.3) covering no-seam, single-seam, binary PLY, UV expansion, texture path round-trip, and missing-file error cases - Register MeshIO_PLY.hpp and MeshUtils.hpp as installed public headers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-io_20260323) Add MeshIO.hpp with read_mesh and write_mesh overloads (Tiers 1–3) that dispatch to OBJ or PLY IO by file extension; throw std::runtime_error for unsupported extensions. Add facade dispatch tests for both formats and all three tiers. Register MeshIO.hpp as an installed public header. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…20260323) Security: - parse_face_ref: throw on zero or negative OBJ face index - read_ply_impl: cap per-face vertex count at 256; validate face indices against n_vertices; throw on binary big-endian format Performance: - String::split(): thread_local static locale eliminates per-call mutex - Add split_into() overload; OBJ/PLY ASCII parsers reuse token vector - read_obj_impl: hoist face_verts/face_vts/face_vns outside loop - expand_at_seams: std::unordered_map with documented Fibonacci pair hash Architecture: - write_obj: extract write_obj_vertices/faces helpers; Tiers 1-3a share them - write_ply: extract write_ply_header/data helpers; all tiers share them - Add read_ply(path, mesh, uvmap) Tier 2 overload - read_mesh Tier 2 calls new read_ply Tier 2 directly (no throwaway vector) Testing: - Unique temp dirs via this-pointer hex suffix in all three fixtures - Full 3x3 position verification in all position round-trip tests - Verify all three vertex colors in color round-trip tests - Write-failure tests for write_obj, write_ply, write_mesh - PLY UV coordinate value verification - v/vt/vn combined face format test - PLY malformed header test - NCMesh (normals+colors) OBJ round-trip test - BinaryBE throw test - expand_at_seams empty-mesh test Platform: - CheckCharconvFP.cmake: add long double to_chars probe - String.hpp: to_string_view<long double> fallback via double cast when EDUCE_CORE_NEED_TO_CHARS_LONG_DOUBLE_FALLBACK is defined (macOS-26) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the single EDUCE_CORE_NEED_CHARCONV_FP flag with 6 independent
per-type probes (from_chars × {float,double,long double} and to_chars ×
{float,double,long double}). Each fallback specialisation is now gated
on its own definition, so a platform where only long double is missing
does not regress float/double paths.
to_string_view fallbacks use snprintf (float: %.9g, double: %.17g);
long double casts to double (same representation on Apple Silicon).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@copilot Review the PR implementation |
Co-authored-by: csparker247 <1434526+csparker247@users.noreply.github.com> Agent-Logs-Url: https://github.com/educelab/libcore/sessions/d7bca254-3f1d-4647-abdb-4eb0f9586a21
…_obj Tier 3b Co-authored-by: csparker247 <1434526+csparker247@users.noreply.github.com> Agent-Logs-Url: https://github.com/educelab/libcore/sessions/d7bca254-3f1d-4647-abdb-4eb0f9586a21
Reviewed the implementation and fixed issues found:
All 39 tests pass. |
…tive dispatch, and edge-case tests Co-authored-by: csparker247 <1434526+csparker247@users.noreply.github.com> Agent-Logs-Url: https://github.com/educelab/libcore/sessions/50bb342d-a00b-4bf5-9c2b-7c2af9957968
Co-authored-by: csparker247 <1434526+csparker247@users.noreply.github.com> Agent-Logs-Url: https://github.com/educelab/libcore/sessions/50bb342d-a00b-4bf5-9c2b-7c2af9957968
…h-io_20260323) Collapse the three identical `if (back=='\\r') pop_back()` + empty-check idioms in the ASCII read paths of MeshIO_PLY.hpp into a single `trim_right_in_place(line)` call from String.hpp. No behaviour change; trim_right already treats '\\r' as whitespace via std::isspace. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ispatch Pre-compute PropRole enum for every PLYProp in parse_ply_header based on element context (vertex vs face). Replace the if-else name-comparison chains in both binary and ASCII vertex/face reader inner loops with switch statements on the pre-computed role, eliminating O(properties) string comparisons per vertex/face. No behavior change; 17/17 tests pass. (mesh-io_20260323)
…reader Pre-compute per-property byte offsets from the PLY header so the binary vertex loop makes one istream::read() per vertex (O(vertices)) instead of one read per property per vertex (O(properties × vertices)). Adds read_ply_prop_from_buf<T> alongside the existing stream-based read_ply_binary_prop<T>; unknown/skipped properties are read into the buffer and their bytes are simply ignored, preserving correct stream positioning without extra istream::ignore() calls. Closes task 5.7 (mesh-io_20260323) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ly_impl The face-element branch of read_ply_impl contained ~80 lines of nested binary + ASCII parsing logic inside the element loop. Extract into two inline detail functions: - read_ply_face_binary: parses one binary face record from an istream - read_ply_face_ascii: parses one ASCII face record from a token vector Both take a reusable face_indices buffer and texcoords vector as out-params, clearing them before filling. read_ply_impl now delegates to these helpers and constructs the Face from the returned index buffer, reducing the nesting depth and isolating per-face parsing logic. Pre-compute load_texcoords = uvmap != nullptr && has_texcoord once outside the face loop; both helpers receive it as a plain bool. Closes task 5.8 (mesh-io_20260323) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PLYProp, PLYElement, PLYHeader and their members/enums: add @brief + inline ///< comments so EXTRACT_PRIVATE builds cleanly - ply_type_bytes, read_ply_binary_prop, read_ply_impl: add @brief - Fix broken \ref read_ply(path,mesh) (used param names, not types) - MeshIO_OBJ.hpp: escape <stem> as <stem> to suppress Doxygen xml/html tag warning
- product.md: add UVMap, IO, and trait detection to Key Goals - tech-stack.md: add UVMap, MeshUtils, MeshIO, MeshIO_OBJ, MeshIO_PLY to component table - README.md: add MeshIO header-only entries; add Mesh IO usage section
csparker247
left a comment
There was a problem hiding this comment.
@copilot Review the comments.
| 4. Provide standard Mesh IO (OBJ and PLY read/write) including UV maps, texture | ||
| paths, and compile-time vertex trait detection (`has_normal`, `has_color`) |
There was a problem hiding this comment.
| 4. Provide standard Mesh IO (OBJ and PLY read/write) including UV maps, texture | |
| paths, and compile-time vertex trait detection (`has_normal`, `has_color`) |
| auto cross(const T1& a, const std::initializer_list<T2>& b) -> T1 | ||
| { | ||
| if (std::size(a) != 3 or std::size(b) != 3) { | ||
| throw std::invalid_argument("Inputs have mismatched dimensions"); |
There was a problem hiding this comment.
| throw std::invalid_argument("Inputs have mismatched dimensions"); | |
| throw std::invalid_argument("Inputs must be 3-dimensional"); |
| chars.push_back(d[0]); | ||
| } | ||
| split(s, tokens, [&chars](char c) { | ||
| return std::find(chars.begin(), chars.end(), c) != chars.end(); |
| #### A note on `to_numeric` and `to_string` compilation | ||
|
|
||
| `to_numeric` uses `std::from_chars` for string-to-number conversion. | ||
| `to_string` and `to_string_view` use `std::to_chars` for number-to-string | ||
| conversion. Both were introduced in C++17 but floating-point support arrived | ||
| later and is gated on the runtime library version (e.g. macOS 13.3+). | ||
|
|
||
| CMake probes for both at configure time and reports the results: | ||
|
|
||
| - **`CXX_CHARCONV_FP_FROM_CHARS`** — whether `std::from_chars` supports | ||
| `float`. If not, `EDUCE_CORE_NEED_CHARCONV_FP` is defined and `to_numeric` | ||
| falls back to `std::stof` / `std::stod` / `std::stold`. | ||
| - **`CXX_CHARCONV_FP_TO_CHARS`** — whether `std::to_chars` supports `float`. | ||
| `to_string_view` requires this unconditionally; no fallback is provided. | ||
|
|
||
| When linking against the `educelab::core` CMake target, `EDUCE_CORE_NEED_CHARCONV_FP` | ||
| is propagated automatically. If using the library header-only, check the CMake | ||
| cache variable and set the definition manually: |
| # Conditionally add the to_numeric compiler definition | ||
| if(EDUCE_CORE_NEED_TO_NUMERIC_FP) | ||
| target_compile_definitions(foo PRIVATE EDUCE_CORE_NEED_TO_NUMERIC_FP) | ||
| # Propagate the to_numeric fallback definition if needed | ||
| if(EDUCE_CORE_NEED_CHARCONV_FP) | ||
| target_compile_definitions(foo PRIVATE EDUCE_CORE_NEED_CHARCONV_FP) |
| } else { | ||
| // ASCII: skip '#'-comment lines. | ||
| while (std::getline(file, line)) { | ||
| trim_right_in_place(line); |
There was a problem hiding this comment.
Why not stringview trim_right or trim? Especially since we're using a string_view for split later.
| if (color_type == "uchar" || color_type == "char") { | ||
| mesh.vertex(new_vi).color = Color::U8C3{ | ||
| static_cast<uint8_t>(r), | ||
| static_cast<uint8_t>(g), | ||
| static_cast<uint8_t>(b)}; | ||
| } else if ( | ||
| color_type == "ushort" || color_type == "short") { | ||
| mesh.vertex(new_vi).color = Color::U16C3{ | ||
| static_cast<uint16_t>(r), | ||
| static_cast<uint16_t>(g), | ||
| static_cast<uint16_t>(b)}; | ||
| } else { |
There was a problem hiding this comment.
| if (color_type == "uchar" || color_type == "char") { | |
| mesh.vertex(new_vi).color = Color::U8C3{ | |
| static_cast<uint8_t>(r), | |
| static_cast<uint8_t>(g), | |
| static_cast<uint8_t>(b)}; | |
| } else if ( | |
| color_type == "ushort" || color_type == "short") { | |
| mesh.vertex(new_vi).color = Color::U16C3{ | |
| static_cast<uint16_t>(r), | |
| static_cast<uint16_t>(g), | |
| static_cast<uint16_t>(b)}; | |
| } else { | |
| if (color_type == "uchar") { | |
| mesh.vertex(new_vi).color = Color::U8C3{ | |
| static_cast<uint8_t>(r), | |
| static_cast<uint8_t>(g), | |
| static_cast<uint8_t>(b)}; | |
| } else if ( | |
| color_type == "ushort") { | |
| mesh.vertex(new_vi).color = Color::U16C3{ | |
| static_cast<uint16_t>(r), | |
| static_cast<uint16_t>(g), | |
| static_cast<uint16_t>(b)}; | |
| } else { |
Don't include the signed types char and short in these conditions. That could be an incorrect cast if the values are negative.
| - **`CXX_CHARCONV_FP_FROM_CHARS`** — whether `std::from_chars` supports | ||
| `float`. If not, `EDUCE_CORE_NEED_CHARCONV_FP` is defined and `to_numeric` | ||
| falls back to `std::stof` / `std::stod` / `std::stold`. | ||
| - **`CXX_CHARCONV_FP_TO_CHARS`** — whether `std::to_chars` supports `float`. | ||
| `to_string_view` requires this unconditionally; no fallback is provided. | ||
|
|
||
| When linking against the `educelab::core` CMake target, `EDUCE_CORE_NEED_CHARCONV_FP` | ||
| is propagated automatically. If using the library header-only, check the CMake | ||
| cache variable and set the definition manually: |
There was a problem hiding this comment.
Is this still accurate? What compile definitions are added now? Update if this is wrong.
| # Propagate the to_numeric fallback definition if needed | ||
| if(EDUCE_CORE_NEED_CHARCONV_FP) | ||
| target_compile_definitions(foo PRIVATE EDUCE_CORE_NEED_CHARCONV_FP) |
There was a problem hiding this comment.
Same. Update if needed.
Addresses all unresolved review comments on the MeshIO implementation
PR.
## `MeshIO_PLY.hpp`
- **`PLYType` enum** replaces `std::string` for `PLYProp::type` and
`list_count_type`; `parse_ply_type()` converts header tokens at parse
time. `ply_type_bytes`, `read_ply_binary_prop`, and
`read_ply_prop_from_buf` now dispatch via `switch` instead of sequential
string comparisons
- **Color-type conditions** now match only `PLYType::UChar` /
`PLYType::UShort`; removed signed `char`/`short` cases that could
produce incorrect casts for negative values
- **`trim_right` (string_view)** replaces `trim_right_in_place` in all
three ASCII read loops; the trimmed `string_view` is passed directly to
`split`, avoiding a redundant cast
## CMake charconv probes
- `CheckCharconvFP.cmake` now appends needed definitions to
`EDUCE_CORE_CHARCONV_DEFS` instead of calling `add_compile_definitions`
- `CMakeLists.txt` applies them via `target_compile_definitions(core
PUBLIC ...)`, so they propagate automatically to any downstream target
via `target_link_libraries`
```cmake
# No manual propagation needed anymore
target_link_libraries(foo PRIVATE educelab::core)
```
## Miscellaneous
- `Math.hpp`: `cross()` error message → `"Inputs must be 3-dimensional"`
- `String.hpp`: comment at predicate-dispatch call site clarifying it
routes to the `is_invocable_r_v`-constrained overload, not back into the
variadic template
- `README.md`: charconv section updated with correct per-type definition
names (`EDUCE_CORE_NEED_FROM/TO_CHARS_{FLOAT,DOUBLE,LONG_DOUBLE}`) and
revised cmake example reflecting automatic propagation
- `conductor/product.md`: trimmed goal #4 per suggestion
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: csparker247 <1434526+csparker247@users.noreply.github.com>
Flip if/else blocks with a small false branch into early continue/return/skip, hoisting the main body out one indent level. - PLY: read_ply_face_ascii inner loop (matches read_ply_face_binary) - PLY: skip_binary_prop lambda - OBJ: read_obj_impl MTL parse block (early return) - OBJ: write_obj Tier 3b chart-grouping loop Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
This PR implements OBJ and PLY mesh IO for libcore — read and write, with three tiers (positions only; positions + UV map; positions + UV map + texture paths). A case-insensitive
read_mesh/write_meshfacade dispatches by file extension. The mesh IO track (mesh-io_20260323) is complete.MeshIO pipeline
io/MeshIO.hpp— facade with case-insensitive extension dispatch (.obj,.OBJ,.Plyall work)io/MeshIO_OBJ.hpp— read/write OBJ + MTL with per-wedge UVs, multi-chart support viausemtlio/MeshIO_PLY.hpp— read/write ASCII PLY, read binary little-endian PLY, per-wedge texcoord list + backward-compat per-vertex s/thas_normal,has_color) drive compile-time inclusion of normals and colors in both readers and writershas_chart) drive multi-chart OBJ outputSupporting additions
utils/MeshUtils.hpp—expand_at_seamsfor formats needing per-vertex UVsMesh:num_vertices,num_faces,clearString.hpp: zero-allocationsplit(string_view, vector&, pred)predicate overload,to_string/to_string_view(charconv-based, locale-independent, buffer-reusing), per-type charconv fallbackscmake/CheckCharconvFP.cmakereplacesCheckToNumericFP.cmake— probesfrom_charsandto_charsindependently for float, double, long double, emitting per-typeEDUCE_CORE_NEED_*definitionsHardening
insert_faceelement/propertylines throw instead of being silently skipped;propertybefore anyelementthrowsU8C3for uchar,U16C3for ushort,F32C3for float/double) instead of being forced through a lossy uchar castmtllibwith no argument now throws instead of dereferencing out of boundswrite_obj/write_plytier (catches silent I/O failures like disk full)Tests
55 cases in
tests/src/TestMeshIO.cppcovering round-trips (positions, normals, colors, UVs, textures, multi-chart, N-gons, empty meshes), facade dispatch, and all hardening cases: truncated binary PLY, out-of-bounds indices, excessive element / texcoord / list counts, malformed PLY headers, baremtllib, case-insensitive extensions, native-precision color storage.