diff --git a/CMakeLists.txt b/CMakeLists.txt index ecc31ee..e107e59 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,9 @@ target_include_directories(core $ ) target_compile_features(core PUBLIC cxx_std_17) +if(EDUCE_CORE_CHARCONV_DEFS) + target_compile_definitions(core PUBLIC ${EDUCE_CORE_CHARCONV_DEFS}) +endif() set_target_properties(core PROPERTIES PUBLIC_HEADER "${public_hdrs}" diff --git a/README.md b/README.md index 59c0bda..ba4c432 100644 --- a/README.md +++ b/README.md @@ -346,17 +346,24 @@ examples. 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: +CMake probes for both at configure time via `CheckCharconvFP.cmake` and reports +the results as compile definitions (only emitted when the corresponding +function+type combination is absent): + +- **`from_chars` fallbacks** — `to_numeric` falls back to `std::stof` / + `std::stod` / `std::stold` for any type whose definition is set: + - `EDUCE_CORE_NEED_FROM_CHARS_FLOAT` + - `EDUCE_CORE_NEED_FROM_CHARS_DOUBLE` + - `EDUCE_CORE_NEED_FROM_CHARS_LONG_DOUBLE` +- **`to_chars` fallbacks** — `to_string` / `to_string_view` fall back to + `std::snprintf` for any type whose definition is set: + - `EDUCE_CORE_NEED_TO_CHARS_FLOAT` + - `EDUCE_CORE_NEED_TO_CHARS_DOUBLE` + - `EDUCE_CORE_NEED_TO_CHARS_LONG_DOUBLE` + +These definitions are attached to the `educelab::core` target as `PUBLIC` +compile definitions, so they propagate automatically to any target that links +against it: ```cmake # Import libcore @@ -368,17 +375,18 @@ FetchContent_Declare( ) FetchContent_MakeAvailable(libcore) -# Add an executable which has access to the libcore headers +# Definitions are propagated automatically — no extra step needed. add_executable(foo foo.cpp) -target_include_directories(foo - PUBLIC - $ -) +target_link_libraries(foo PRIVATE educelab::core) +``` + +If using the headers without linking against the CMake target, check the CMake +cache variables and set the needed definitions manually: -# Propagate the to_numeric fallback definition if needed -if(EDUCE_CORE_NEED_CHARCONV_FP) - target_compile_definitions(foo PRIVATE EDUCE_CORE_NEED_CHARCONV_FP) -endif() +```cmake +foreach(_def IN LISTS EDUCE_CORE_CHARCONV_DEFS) + target_compile_definitions(foo PRIVATE ${_def}) +endforeach() ``` ### Data caching diff --git a/cmake/CheckCharconvFP.cmake b/cmake/CheckCharconvFP.cmake index 235894e..1da3556 100644 --- a/cmake/CheckCharconvFP.cmake +++ b/cmake/CheckCharconvFP.cmake @@ -6,11 +6,12 @@ # in macOS 13.3; long double availability differs further. # # For each (function, type) combination a separate compile-time probe is run. -# When unavailable, a per-type preprocessor definition is set so String.hpp -# can activate the appropriate fallback specialisation without over-disabling -# types that are actually supported. +# When unavailable, a per-type preprocessor definition is appended to the +# EDUCE_CORE_CHARCONV_DEFS list so the caller can attach it to the library +# target via target_compile_definitions. # -# Definitions emitted (only when the corresponding function+type is absent): +# Definitions collected into EDUCE_CORE_CHARCONV_DEFS (only when the +# corresponding function+type is absent): # EDUCE_CORE_NEED_FROM_CHARS_FLOAT # EDUCE_CORE_NEED_FROM_CHARS_DOUBLE # EDUCE_CORE_NEED_FROM_CHARS_LONG_DOUBLE @@ -47,7 +48,7 @@ macro(_charconv_probe _direction _typename _snippet _defname) ") check_cxx_source_compiles("${_probe_code}" _CXX_CHARCONV_${_defname}) if(NOT _CXX_CHARCONV_${_defname}) - add_compile_definitions(EDUCE_CORE_${_defname}) + list(APPEND EDUCE_CORE_CHARCONV_DEFS EDUCE_CORE_${_defname}) endif() endmacro() diff --git a/conductor/product.md b/conductor/product.md index 94700d1..83cb441 100644 --- a/conductor/product.md +++ b/conductor/product.md @@ -21,8 +21,7 @@ EduceLab developers building C++ applications and libraries. 1. Provide a stable, reusable foundation for all EduceLab C++ projects 2. Offer well-tested, header-friendly types (Vec, Mat, Image, Mesh, UVMap, etc.) 3. Minimize external dependencies while maximizing utility -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`) +4. Provide standard Mesh IO (OBJ and PLY read/write) including UV maps and texture paths ## Current Version diff --git a/include/educelab/core/io/MeshIO_PLY.hpp b/include/educelab/core/io/MeshIO_PLY.hpp index 081ec02..95058f4 100644 --- a/include/educelab/core/io/MeshIO_PLY.hpp +++ b/include/educelab/core/io/MeshIO_PLY.hpp @@ -128,13 +128,42 @@ enum class PropRole { Texcoord, }; +/** @brief Scalar type of a PLY property, parsed once from the header so inner + * loops can switch on an integer instead of comparing strings. */ +enum class PLYType { + Unknown, + Float, ///< IEEE 754 single-precision (4 bytes) + Double, ///< IEEE 754 double-precision (8 bytes) + Int, ///< Signed 32-bit integer + UInt, ///< Unsigned 32-bit integer + Short, ///< Signed 16-bit integer + UShort, ///< Unsigned 16-bit integer + Char, ///< Signed 8-bit integer + UChar, ///< Unsigned 8-bit integer +}; + +/** @brief Parse a PLY type token into a @c PLYType enum value */ +inline auto parse_ply_type(std::string_view t) -> PLYType +{ + if (t == "float") return PLYType::Float; + if (t == "double") return PLYType::Double; + if (t == "int") return PLYType::Int; + if (t == "uint") return PLYType::UInt; + if (t == "short") return PLYType::Short; + if (t == "ushort") return PLYType::UShort; + if (t == "char") return PLYType::Char; + if (t == "uchar") return PLYType::UChar; + throw std::runtime_error( + "read_ply: unrecognized property type '" + std::string(t) + "'"); +} + /** @brief Parsed representation of a single PLY property declaration */ struct PLYProp { - std::string name; ///< Property name (e.g., "x", "nx", "red") - std::string type; ///< PLY scalar type string (e.g., "float", "uchar") - bool is_list{false}; ///< True when this is a list property - std::string list_count_type; ///< Type of the list-length prefix (when @c is_list) - PropRole role{PropRole::Unknown}; ///< Pre-computed semantic role for dispatch + std::string name; ///< Property name (e.g., "x", "nx", "red") + PLYType type{PLYType::Unknown}; ///< Scalar type of the property value + bool is_list{false}; ///< True when this is a list property + PLYType list_count_type{PLYType::Unknown}; ///< Type of the list-length prefix (when @c is_list) + PropRole role{PropRole::Unknown}; ///< Pre-computed semantic role for dispatch }; /** @brief A single element block from the PLY header */ @@ -157,16 +186,21 @@ struct PLYHeader { std::vector elements; ///< Element blocks in declaration order }; -/** @brief Return the byte width of a named PLY scalar type */ -inline auto ply_type_bytes(const std::string& t) -> std::size_t +/** @brief Return the byte width of a PLY scalar type */ +inline auto ply_type_bytes(PLYType t) -> std::size_t { - if (t == "double") return 8; - if (t == "float" || t == "int" || t == "uint") - return 4; - if (t == "short" || t == "ushort") return 2; - if (t == "char" || t == "uchar") return 1; - throw std::runtime_error( - "read_ply: unrecognized property type '" + t + "'"); + switch (t) { + case PLYType::Double: return 8; + case PLYType::Float: + case PLYType::Int: + case PLYType::UInt: return 4; + case PLYType::Short: + case PLYType::UShort: return 2; + case PLYType::Char: + case PLYType::UChar: return 1; + default: + throw std::runtime_error("read_ply: unrecognized property type"); + } } /** @brief Parse a PLY header; leaves @p file positioned at first data byte */ @@ -230,12 +264,12 @@ inline auto parse_ply_header(std::istream& file) -> PLYHeader PLYProp p; if (tokens[1] == "list" && tokens.size() >= 5) { p.is_list = true; - p.list_count_type = std::string(tokens[2]); - p.type = std::string(tokens[3]); + p.list_count_type = parse_ply_type(tokens[2]); + p.type = parse_ply_type(tokens[3]); p.name = std::string(tokens[4]); } else { p.name = std::string(tokens[2]); - p.type = std::string(tokens[1]); + p.type = parse_ply_type(tokens[1]); } // Assign semantic role once so inner read loops switch on an // integer rather than comparing strings per vertex / face. @@ -268,62 +302,66 @@ inline auto parse_ply_header(std::istream& file) -> PLYHeader /** @brief Read a single binary little-endian PLY scalar property from @p f */ template -auto read_ply_binary_prop(std::istream& f, const std::string& type) -> DestT +auto read_ply_binary_prop(std::istream& f, PLYType type) -> DestT { - if (type == "float") { - float v{}; - f.read(reinterpret_cast(&v), 4); - if (!f) { throw std::runtime_error("read_ply: unexpected end of binary data"); } - return static_cast(v); - } - if (type == "double") { - double v{}; - f.read(reinterpret_cast(&v), 8); - if (!f) { throw std::runtime_error("read_ply: unexpected end of binary data"); } - return static_cast(v); - } - if (type == "int") { - int32_t v{}; - f.read(reinterpret_cast(&v), 4); - if (!f) { throw std::runtime_error("read_ply: unexpected end of binary data"); } - return static_cast(v); - } - if (type == "uint") { - uint32_t v{}; - f.read(reinterpret_cast(&v), 4); - if (!f) { throw std::runtime_error("read_ply: unexpected end of binary data"); } - return static_cast(v); - } - if (type == "short") { - int16_t v{}; - f.read(reinterpret_cast(&v), 2); - if (!f) { throw std::runtime_error("read_ply: unexpected end of binary data"); } - return static_cast(v); - } - if (type == "ushort") { - uint16_t v{}; - f.read(reinterpret_cast(&v), 2); - if (!f) { throw std::runtime_error("read_ply: unexpected end of binary data"); } - return static_cast(v); - } - if (type == "char") { - int8_t v{}; - f.read(reinterpret_cast(&v), 1); - if (!f) { - throw std::runtime_error("read_ply: unexpected end of binary data"); + const auto err = []() { + throw std::runtime_error("read_ply: unexpected end of binary data"); + }; + switch (type) { + case PLYType::Float: { + float v{}; + f.read(reinterpret_cast(&v), 4); + if (!f) err(); + return static_cast(v); } - return static_cast(v); - } - if (type == "uchar") { - uint8_t v{}; - f.read(reinterpret_cast(&v), 1); - if (!f) { throw std::runtime_error("read_ply: unexpected end of binary data"); } - return static_cast(v); + case PLYType::Double: { + double v{}; + f.read(reinterpret_cast(&v), 8); + if (!f) err(); + return static_cast(v); + } + case PLYType::Int: { + int32_t v{}; + f.read(reinterpret_cast(&v), 4); + if (!f) err(); + return static_cast(v); + } + case PLYType::UInt: { + uint32_t v{}; + f.read(reinterpret_cast(&v), 4); + if (!f) err(); + return static_cast(v); + } + case PLYType::Short: { + int16_t v{}; + f.read(reinterpret_cast(&v), 2); + if (!f) err(); + return static_cast(v); + } + case PLYType::UShort: { + uint16_t v{}; + f.read(reinterpret_cast(&v), 2); + if (!f) err(); + return static_cast(v); + } + case PLYType::Char: { + int8_t v{}; + f.read(reinterpret_cast(&v), 1); + if (!f) err(); + return static_cast(v); + } + case PLYType::UChar: { + uint8_t v{}; + f.read(reinterpret_cast(&v), 1); + if (!f) err(); + return static_cast(v); + } + default: + // The header parser rejects unrecognized types before we reach + // here, but guard defensively so the function is correct in + // isolation. + throw std::runtime_error("read_ply: unrecognized property type"); } - // The header parser rejects unrecognized types before we reach here, - // but guard defensively so the function is correct in isolation. - throw std::runtime_error( - "read_ply: unrecognized property type '" + type + "'"); } /** @brief Extract a typed value from a raw byte buffer using memcpy. @@ -334,34 +372,52 @@ auto read_ply_binary_prop(std::istream& f, const std::string& type) -> DestT * istream::read calls. */ template -auto read_ply_prop_from_buf(const char* buf, const std::string& type) -> DestT +auto read_ply_prop_from_buf(const char* buf, PLYType type) -> DestT { - if (type == "float") { - float v; std::memcpy(&v, buf, 4); return static_cast(v); - } - if (type == "double") { - double v; std::memcpy(&v, buf, 8); return static_cast(v); - } - if (type == "int") { - int32_t v; std::memcpy(&v, buf, 4); return static_cast(v); - } - if (type == "uint") { - uint32_t v; std::memcpy(&v, buf, 4); return static_cast(v); - } - if (type == "short") { - int16_t v; std::memcpy(&v, buf, 2); return static_cast(v); - } - if (type == "ushort") { - uint16_t v; std::memcpy(&v, buf, 2); return static_cast(v); - } - if (type == "char") { - int8_t v; std::memcpy(&v, buf, 1); return static_cast(v); - } - if (type == "uchar") { - uint8_t v; std::memcpy(&v, buf, 1); return static_cast(v); + switch (type) { + case PLYType::Float: { + float v; + std::memcpy(&v, buf, 4); + return static_cast(v); + } + case PLYType::Double: { + double v; + std::memcpy(&v, buf, 8); + return static_cast(v); + } + case PLYType::Int: { + int32_t v; + std::memcpy(&v, buf, 4); + return static_cast(v); + } + case PLYType::UInt: { + uint32_t v; + std::memcpy(&v, buf, 4); + return static_cast(v); + } + case PLYType::Short: { + int16_t v; + std::memcpy(&v, buf, 2); + return static_cast(v); + } + case PLYType::UShort: { + uint16_t v; + std::memcpy(&v, buf, 2); + return static_cast(v); + } + case PLYType::Char: { + int8_t v; + std::memcpy(&v, buf, 1); + return static_cast(v); + } + case PLYType::UChar: { + uint8_t v; + std::memcpy(&v, buf, 1); + return static_cast(v); + } + default: + throw std::runtime_error("read_ply: unrecognized property type"); } - throw std::runtime_error( - "read_ply: unrecognized property type '" + type + "'"); } // ------------------------------------------------------------------------- @@ -598,7 +654,7 @@ void read_ply_impl( // Declared PLY type of the red property (taken as canonical for r/g/b). // Drives which Color variant is stored — uchar/ushort/float preserve the // file's native representation rather than forcing a lossy conversion. - std::string color_type; + PLYType color_type{PLYType::Unknown}; if (vert_elem) { for (const auto& p : vert_elem->props) { switch (p.role) { @@ -657,8 +713,8 @@ void read_ply_impl( while (std::getline(file, skip_line)) { // Trim trailing whitespace (incl. '\r' on Windows) then skip // '#'-comment lines; break on the first real data line. - trim_right_in_place(skip_line); - if (!skip_line.empty() && skip_line.front() != '#') + const auto sv = trim_right(skip_line); + if (!sv.empty() && sv.front() != '#') break; } }; @@ -738,12 +794,13 @@ void read_ply_impl( } } else { // ASCII: skip '#'-comment lines. + std::string_view sv; while (std::getline(file, line)) { - trim_right_in_place(line); - if (!line.empty() && line.front() != '#') + sv = trim_right(line); + if (!sv.empty() && sv.front() != '#') break; } - split(std::string_view(line), tokens); + split(sv, tokens); for (std::size_t pi = 0; pi < elem.props.size() && pi < tokens.size(); ++pi) { switch (elem.props[pi].role) { @@ -788,13 +845,12 @@ void read_ply_impl( // read through a float intermediate, which is lossless // for uchar (0-255) and ushort (0-65535) since both // ranges fit exactly in float. - if (color_type == "uchar" || color_type == "char") { + if (color_type == PLYType::UChar) { mesh.vertex(new_vi).color = Color::U8C3{ static_cast(r), static_cast(g), static_cast(b)}; - } else if ( - color_type == "ushort" || color_type == "short") { + } else if (color_type == PLYType::UShort) { mesh.vertex(new_vi).color = Color::U16C3{ static_cast(r), static_cast(g), @@ -829,12 +885,13 @@ void read_ply_impl( file, elem, n_vertices, load_texcoords, face_indices, texcoords); } else { + std::string_view sv; while (std::getline(file, fline)) { - trim_right_in_place(fline); - if (!fline.empty() && fline.front() != '#') + sv = trim_right(fline); + if (!sv.empty() && sv.front() != '#') break; } - split(std::string_view(fline), tokens); + split(sv, tokens); if (tokens.empty()) continue; read_ply_face_ascii( diff --git a/include/educelab/core/utils/Math.hpp b/include/educelab/core/utils/Math.hpp index 7e643b5..33c2b68 100644 --- a/include/educelab/core/utils/Math.hpp +++ b/include/educelab/core/utils/Math.hpp @@ -57,7 +57,7 @@ template auto cross(const T1& a, const T2& b) -> T1 { if (std::size(a) != 3 or std::size(b) != 3) { - throw std::invalid_argument("Inputs have mismatched dimensions"); + throw std::invalid_argument("Inputs must be 3-dimensional"); } T1 c; c[0] = a[1] * b[2] - a[2] * b[1]; @@ -74,7 +74,7 @@ template < auto cross(const T1& a, const std::initializer_list& b) -> T1 { if (std::size(a) != 3 or std::size(b) != 3) { - throw std::invalid_argument("Inputs have mismatched dimensions"); + throw std::invalid_argument("Inputs must be 3-dimensional"); } auto* bp = b.begin(); T1 c; diff --git a/include/educelab/core/utils/String.hpp b/include/educelab/core/utils/String.hpp index 0bd2fc3..aae5d1e 100644 --- a/include/educelab/core/utils/String.hpp +++ b/include/educelab/core/utils/String.hpp @@ -263,6 +263,8 @@ static auto split( chars.push_back(d[0]); } split(s, tokens, [&chars](char c) { + // std::is_invocable_r_v selects the predicate + // overload rather than re-entering this variadic template. return std::find(chars.begin(), chars.end(), c) != chars.end(); }); return;