1
0

Compare commits

...

7 Commits

Author SHA1 Message Date
f10c2d4886 test: parse.underflow 2026-02-22 00:06:05 +02:00
114d0bc014 unit test: parse 2026-02-21 17:48:53 +02:00
306633d01e refactor parse + unit test 2026-02-21 15:44:33 +02:00
6a3a7541ea test: scan 2026-02-21 14:47:30 +02:00
0f40f89cbb move tests to tests/unit 2026-02-21 14:20:36 +02:00
753a6e0e1d unit tests: coverage, settings 2026-02-21 14:15:08 +02:00
ad55b1109d feature: write indexed data to file. 2026-02-20 22:20:10 +02:00
16 changed files with 1153 additions and 359 deletions

View File

@@ -7,5 +7,51 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-g -O3") set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-g -O3")
set(CMAKE_CXX_FLAGS "-fno-threadsafe-statics") set(CMAKE_CXX_FLAGS "-fno-threadsafe-statics")
file(GLOB SRC_FILES src/*.cpp) option(ENABLE_COVERAGE "Enable coverage instrumentation for tests" OFF)
file(GLOB SRC_FILES CONFIGURE_DEPENDS src/*.cpp)
set(APP_SOURCES ${SRC_FILES})
list(REMOVE_ITEM APP_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp)
add_executable(wavefront_parser ${SRC_FILES}) add_executable(wavefront_parser ${SRC_FILES})
target_include_directories(wavefront_parser PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
find_package(GTest REQUIRED)
enable_testing()
add_executable(wavefront_tests
tests/unit/trim_test.cpp
tests/unit/settings_test.cpp
tests/unit/scan_test.cpp
tests/unit/parse_test.cpp
${APP_SOURCES}
)
target_link_libraries(wavefront_tests PRIVATE
GTest::gtest
GTest::gmock
GTest::gtest_main
)
target_include_directories(wavefront_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
if(ENABLE_COVERAGE AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(wavefront_tests PRIVATE --coverage -O0 -g)
target_link_options(wavefront_tests PRIVATE --coverage)
endif()
include(GoogleTest)
gtest_discover_tests(wavefront_tests)
find_program(LCOV_EXECUTABLE lcov)
find_program(GENHTML_EXECUTABLE genhtml)
if(LCOV_EXECUTABLE AND GENHTML_EXECUTABLE)
add_custom_target(coverage
COMMAND ${LCOV_EXECUTABLE} --directory "${CMAKE_BINARY_DIR}" --capture --rc geninfo_unexecuted_blocks=1 --demangle-cpp --ignore-errors inconsistent --keep-going --output-file coverage.info
COMMAND ${LCOV_EXECUTABLE} --extract coverage.info "${CMAKE_SOURCE_DIR}/src/*" --output-file coverage.info
COMMAND ${GENHTML_EXECUTABLE} coverage.info --output-directory coverage
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
COMMENT "Generating lcov report at \"${CMAKE_BINARY_DIR}/coverage/index.html\""
VERBATIM
)
endif()

9
coverage.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
cmake -S . -B build/Coverage -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON
cmake --build build/Coverage --config Debug
ctest --test-dir build/Coverage --output-on-failure
cmake --build build/Coverage --target coverage
echo "Coverage report: file://$(pwd)/build/Coverage/coverage/index.html"

View File

@@ -88,12 +88,12 @@ namespace wavefront {
} else { } else {
target.push_back(0); target.push_back(0);
} }
}
std::copy( std::copy(
source.begin(), source.begin(),
source.end(), source.end(),
std::back_inserter(target) std::back_inserter(target)
); );
}
}, },
std::index_sequence<1, 2, 3>{} std::index_sequence<1, 2, 3>{}
); );
@@ -227,6 +227,7 @@ namespace wavefront {
std::array<IndexType, 3> triangle_data; std::array<IndexType, 3> triangle_data;
std::size_t triangle_vertex_index = 0; std::size_t triangle_vertex_index = 0;
for (const auto &vertex : triangle) { for (const auto &vertex : triangle) {
if constexpr (VertexDimCount > 1) {
std::array<IndexType, VertexDimCount> coordinate_indices; std::array<IndexType, VertexDimCount> coordinate_indices;
for (std::size_t i = 0; i < VertexDimCount; ++i) { for (std::size_t i = 0; i < VertexDimCount; ++i) {
const auto &wavefront_index = mapper[i].get_vertex_index(vertex); const auto &wavefront_index = mapper[i].get_vertex_index(vertex);
@@ -241,6 +242,13 @@ namespace wavefront {
assert(index != 0); assert(index != 0);
assert(triangle_vertex_index < 3); assert(triangle_vertex_index < 3);
triangle_data[triangle_vertex_index++] = index; triangle_data[triangle_vertex_index++] = index;
} else {
const auto &wavefront_index = mapper[0].get_vertex_index(vertex);
const auto &line_index = mapper[0].attribute_line_map.at(wavefront_index);
const auto &coordinate_index = mapper[0].line_coordinate_map.at(line_index);
assert(coordinate_index != 0);
triangle_data[triangle_vertex_index++] = coordinate_index;
}
} }
assert(triangle_vertex_index == 3); assert(triangle_vertex_index == 3);
triangle_data_set.emplace(triangle_data); triangle_data_set.emplace(triangle_data);

View File

@@ -8,6 +8,7 @@
#include <sstream> #include <sstream>
#include <vector> #include <vector>
#include <string> #include <string>
#include <tuple>
#include <memory> #include <memory>
#include <stdexcept> #include <stdexcept>
#include <variant> #include <variant>
@@ -16,6 +17,7 @@
#include "parse.hpp" #include "parse.hpp"
#include "compile.hpp" #include "compile.hpp"
#include "settings.hpp" #include "settings.hpp"
#include "output.hpp"
static void usage(const char* prog) { static void usage(const char* prog) {
std::cerr << "Usage: " << prog std::cerr << "Usage: " << prog
@@ -120,12 +122,12 @@ int main(int argc, char** argv) {
Settings settings = settings_builder.build(); Settings settings = settings_builder.build();
using FloatType = float; using FloatType = float;
using IndexType = std::size_t; using IndexType = std::uint32_t;
CATCH_AND_RETURN(scan_data, wavefront::scan_error, 1, wavefront::scan( auto scan_data = wavefront::scan(
settings.input(), settings.input(),
settings.selected_objects(), settings.selected_objects(),
settings.selected_groups()) settings.selected_groups()
); );
std::cerr << "Scanned " << scan_data.total_lines << " lines" << std::endl; std::cerr << "Scanned " << scan_data.total_lines << " lines" << std::endl;
@@ -173,32 +175,101 @@ int main(int argc, char** argv) {
std::cerr << "Compiled " << index_storage.triangles.size() << " triangles" << std::endl; std::cerr << "Compiled " << index_storage.triangles.size() << " triangles" << std::endl;
// TODO: We need to output: auto &output = settings.output();
// 1. Some kind of singature;
// 2. A version number; // 0:4: Signature
// 3. Flags describing the data: settings.output().write("\x7F" "3DG", 4);
// 3.1. position: required, always 3 coordinates (no flag needed) // 4:8: Version
// 3.2. normal: optional, always 3 coordinates (single flag needed) wavefront::write_number<std::uint32_t>(output, 1);
// 3.3. texcoord: optional, up to 3 coordinates (two flags needed, 0 = none, 1 = 1D, 2 = 2D, 3 = 3D)
// 3.4. type of number data: 0 = float, 1 = double std::uint32_t flags = 0;
// 3.5. vertices: 0 = never used, 1 = one attribute per vertex, 2 = two attributes per vertex, 3 = three attributes per vertex
// Flags: (0 = number type, 1 = normals present, 2-3 = texture_coordinate size, 4-7 = number of vertex attributes (up to 16), only 3 supported for now) if (coordinate_storage_index.normal_coordinate_index.size() > 0) {
// 4. Data order: up to attribute_count attributes, 1-byte each, specifying an index of map in file order. 0 is the number_list and therefore reserved. flags |= 1 << 1;
// 5. Sizes: }
// 5.1. Size of the number list (in items): 1 item = 1 float/double (depending on number_type)
// 5.2. Size of the position list (in items): 1 item = 3 indices into the float_list std::visit([&](const auto &texture_coordinates) {
// 5.3. Size of the normal list (in items, only if normal flag is set): 1 item = 3 indices into the float_list if constexpr (std::ranges::range<std::decay_t<decltype(texture_coordinates)>>) {
// 5.4. Size of the texcoord list (in items, only if texcoord_size is not 0): 1 item = 1-3 indices into the float_list (depending on texcoord_size) using mapped_type = typename std::decay_t<decltype(texture_coordinates)>::mapped_type;
// 5.5. Size of the vertex list (in items): 1 item = 1-attribute_count indices into different maps. if constexpr (std::ranges::range<mapped_type>) {
// 5.6. Size of the triangle list (in items): 1 item = 3 triangle vertex indices into the vertex list. flags |= std::tuple_size_v<mapped_type> << 2;
// Special references: number_list, triangle_list, vertex_list, position_list } else {
// All other references are additional attributes. flags |= 1 << 2;
// In order for quick load into GPU buffer we want: }
// - each map start file pos to be known }
// - each map size in bytes to be known }, coordinate_data.texture_coordinates);
// - each map type (float32, float64, int8, int16, int32, int64)
// - number of elements per item (or stride, i.e. number of bytes per item) // 8:12: flags<0: float_size, 1: has_normals, 2&3: texcoord_ndims (0 = no),
// - each map file pos aligned to 16-bytes (assuming file is loaded aligned to 16-bytes). Elements within the map do not need to be aligned. wavefront::write_number<std::uint32_t>(output, flags);
// 12:16: number_list item count
wavefront::write_number<std::uint32_t>(output, number_list.size());
// 16:20: 2D storage item count
wavefront::write_number<std::uint32_t>(output, std::get<2>(coordinate_storage).size());
// 20:24: 3D storage item count
wavefront::write_number<std::uint32_t>(output, std::get<3>(coordinate_storage).size());
// 24:28: Vertex storage item count
std::visit([&](const auto &vertex_data) {
using value_t = std::decay_t<decltype(vertex_data)>::value_type;
if constexpr (std::tuple_size_v<value_t> > 1) {
wavefront::write_number<std::uint32_t>(output, vertex_data.size());
} else {
wavefront::write_number<std::uint32_t>(output, 0);
}
}, index_storage.vertices);
wavefront::write_number<std::uint32_t>(output, index_storage.triangles.size());
output.write(reinterpret_cast<const char *>(number_list.data()), number_list.size() * sizeof(decltype(number_list)::value_type));
std::array<char, 16> padding;
padding.fill(0);
std::size_t required_padding;
required_padding = (16 - (number_list.size() * sizeof(decltype(number_list)::value_type) % 16)) % 16;
output.write(padding.data(), required_padding);
if (std::get<2>(coordinate_storage).size() > 0) {
const auto &value_list = std::get<2>(coordinate_storage);
using value_t = std::decay_t<decltype(value_list)>::value_type;
output.write(reinterpret_cast<const char *>(value_list.data()), value_list.size() * sizeof(value_t));
required_padding = (16 - (value_list.size() * sizeof(value_t) % 16)) % 16;
if (required_padding > 0) {
output.write(padding.data(), required_padding);
}
}
if (std::get<3>(coordinate_storage).size() > 0) [[likely]] {
const auto &value_list = std::get<3>(coordinate_storage);
using value_t = std::decay_t<decltype(value_list)>::value_type;
output.write(reinterpret_cast<const char *>(value_list.data()), value_list.size() * sizeof(value_t));
required_padding = (16 - (value_list.size() * sizeof(value_t) % 16)) % 16;
if (required_padding > 0) {
output.write(padding.data(), required_padding);
}
}
std::visit([&](const auto &vertex_data) {
using value_t = std::decay_t<decltype(vertex_data)>::value_type;
if constexpr (std::tuple_size_v<value_t> > 1) {
output.write(reinterpret_cast<const char *>(vertex_data.data()), vertex_data.size() * sizeof(value_t));
required_padding = (16 - (vertex_data.size() * sizeof(value_t) % 16)) % 16;
if (required_padding > 0) {
output.write(padding.data(), required_padding);
}
}
}, index_storage.vertices);
{
const auto &value_list = index_storage.triangles;
using value_t = std::decay_t<decltype(value_list)>::value_type;
output.write(reinterpret_cast<const char *>(value_list.data()), value_list.size() * sizeof(value_t));
required_padding = (16 - (value_list.size() * sizeof(value_t) % 16)) % 16;
if (required_padding > 0) {
output.write(padding.data(), required_padding);
}
}
return 0; return 0;
} }

9
src/output.hpp Normal file
View File

@@ -0,0 +1,9 @@
#include <array>
#include <ostream>
namespace wavefront {
template<typename NumericType>
void write_number(std::ostream &output, NumericType value) {
output.write(reinterpret_cast<char *>(&value), sizeof(NumericType));
}
}

View File

@@ -55,313 +55,15 @@ namespace wavefront {
wavefront_face_data_result_t parse_face_data(const scan_result &scan_data); wavefront_face_data_result_t parse_face_data(const scan_result &scan_data);
template<typename FloatType> template<typename FloatType>
coordinate_data_t<FloatType> parse_coordinate_data(const scan_result &scan_data, const wavefront_face_data_result_t &face_data) { coordinate_data_t<FloatType> parse_coordinate_data(const scan_result &scan_data, const wavefront_face_data_result_t &face_data);
using namespace std::string_literals;
coordinate_data_t<FloatType> result;
const auto &position_data = scan_data.category_map.at("v"s);
for (const auto &position_index : face_data.index_position_set) {
std::array<FloatType, 3> position_coordinates;
const auto &position_line_index = position_data.at(position_index);
const auto &position_line = scan_data.line_data.at(position_line_index);
decltype(position_line.find_first_of(' ')) number_begin_pos = 2, number_end_pos;
for (std::size_t position_coordinate_index = 0; position_coordinate_index < 3; position_coordinate_index++) {
if (number_begin_pos >= position_line.size()) {
throw parse_error(std::format(
"[{}]: {}",
position_line_index,
std::format("Line \"{}\" must contain at least {} numbers", "v", 3)
));
}
number_end_pos = std::min(position_line.find_first_of(' ', number_begin_pos), position_line.size());
auto [number_end, conversion_error] = std::from_chars(
position_line.data() + number_begin_pos,
position_line.data() + number_end_pos,
position_coordinates[position_coordinate_index],
std::chars_format::fixed
);
if (conversion_error != std::errc() || position_line.data() + number_end_pos != number_end) {
throw parse_error(std::format(
"[{}]: {}",
position_line_index,
std::format("Unable to parse \"{}\" floating point value", "v")
));
}
number_begin_pos = std::min(position_line.find_first_not_of(' ', number_end_pos), position_line.size());
}
if (number_begin_pos < position_line.size()) {
if (position_line.find_first_not_of("0123456789.-", number_begin_pos)) {
throw parse_error(std::format(
"[{}]: {}",
position_line_index,
std::format("Additional data in \"{}\" line: expected {} only", "v", "<x> <y> <z> [weight]")
));
}
}
result.position_coordinates[position_line_index] = position_coordinates;
}
if (face_data.index_normal_set.size() > 0 && scan_data.category_map.contains("vn"s)) {
const auto &normal_data = scan_data.category_map.at("vn"s);
for (const auto &normal_index : face_data.index_normal_set) {
std::array<FloatType, 3> normal_coordinates;
const auto &normal_line_index = normal_data.at(normal_index);
const auto &normal_line = scan_data.line_data.at(normal_line_index);
decltype(normal_line.find_first_of(' ')) number_begin_pos = 3, number_end_pos;
for (std::size_t normal_coordinate_index = 0; normal_coordinate_index < 3; normal_coordinate_index++) {
if (number_begin_pos >= normal_line.size()) {
throw parse_error(std::format(
"[{}]: {}",
normal_line_index,
std::format("Line \"{}\" must contain exactly {} numbers", "vn", 3)
));
}
number_end_pos = std::min(normal_line.find_first_of(' ', number_begin_pos), normal_line.size());
auto [number_end, conversion_error] = std::from_chars(
normal_line.data() + number_begin_pos,
normal_line.data() + number_end_pos,
normal_coordinates[normal_coordinate_index],
std::chars_format::fixed
);
if (conversion_error != std::errc() || normal_line.data() + number_end_pos != number_end) {
throw parse_error(std::format(
"[{}]: {}",
normal_line_index,
std::format("Unable to parse \"{}\" floating point value", "vn")
));
}
number_begin_pos = std::min(normal_line.find_first_not_of(' ', number_end_pos), normal_line.size());
}
if (number_end_pos != normal_line.size()) {
throw parse_error(std::format(
"[{}]: {}",
normal_line_index,
std::format("Additional data in \"{}\" line: expected {} only", "vn", "<x> <y> <z>")
));
}
result.normal_coordinates[normal_line_index] = normal_coordinates;
}
}
if (face_data.index_texcoord_set.size() > 0 && scan_data.category_map.contains("vt"s)) {
using insert_texcoord_data_t = std::function<void()>;
const auto &texcoord_data = scan_data.category_map.at("vt"s);
for (const auto &texcoord_index : face_data.index_texcoord_set) {
std::array<FloatType, 3> texcoord_coordinates;
const auto &texcoord_line_index = texcoord_data.at(texcoord_index);
const auto &texcoord_line = scan_data.line_data.at(texcoord_line_index);
decltype(texcoord_line.find_first_of(' ')) number_begin_pos = 3, number_end_pos;
std::size_t texcoord_coordinate_index = 0;
while (number_begin_pos < texcoord_line.size()) {
if (texcoord_coordinate_index >= 3) {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Line \"{}\" must contain maximum {} numbers", "vt", 3)
));
}
number_end_pos = std::min(texcoord_line.find_first_of(' ', number_begin_pos), texcoord_line.size());
auto [number_end, conversion_error] = std::from_chars(
texcoord_line.data() + number_begin_pos,
texcoord_line.data() + number_end_pos,
texcoord_coordinates[texcoord_coordinate_index++],
std::chars_format::fixed
);
if (conversion_error != std::errc() || texcoord_line.data() + number_end_pos != number_end) {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Unable to parse \"{}\" floating point value", "vt")
));
}
number_begin_pos = std::min(texcoord_line.find_first_not_of(' ', number_end_pos), texcoord_line.size());
}
if (texcoord_coordinate_index == 0) [[unlikely]] {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Unable to parse \"{}\" line: expected minimum one number", "vt")
));
}
if (result.texture_coordinates.valueless_by_exception() || result.texture_coordinates.index() == 0) [[unlikely]] {
if (texcoord_coordinate_index == 1) {
result.texture_coordinates = std::map<file_line_t, FloatType>{};
} else if (texcoord_coordinate_index == 2) {
result.texture_coordinates = std::map<file_line_t, std::array<FloatType, 2>>{};
} else if (texcoord_coordinate_index == 3) {
result.texture_coordinates = std::map<file_line_t, std::array<FloatType, 3>>{};
} else {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Line \"{}\" must contain maximum {} numbers", "vt", 3)
));
}
}
if (texcoord_coordinate_index == 0) [[unlikely]] {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Unable to parse \"{}\" line: expected minimum one number", "vt")
));
}
try {
if (texcoord_coordinate_index == 1) {
std::get<1>(result.texture_coordinates)[texcoord_line_index] = texcoord_coordinates[0];
} else if (texcoord_coordinate_index == 2) {
std::get<2>(result.texture_coordinates)[texcoord_line_index] = std::array<FloatType, 2>{texcoord_coordinates[0], texcoord_coordinates[1]};
} else if (texcoord_coordinate_index == 3) {
std::get<3>(result.texture_coordinates)[texcoord_line_index] = texcoord_coordinates;
}
} catch (std::bad_variant_access&) {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Line \"{}\": {}D texture coordinates expected, got {}D texture coordinates", "vt", result.texture_coordinates.index(), texcoord_coordinate_index)
));
}
}
}
return result;
}
template<typename FloatType> template<typename FloatType>
std::vector<FloatType> create_number_list(const coordinate_data_t<FloatType> &coordinate_data) { std::vector<FloatType> create_number_list(const coordinate_data_t<FloatType> &coordinate_data);
numset_t<FloatType> number_set;
for (const auto& [line_number, coordinates] : coordinate_data.position_coordinates) {
for (const auto &coordinate : coordinates) {
number_set.emplace(coordinate);
}
}
for (const auto& [line_number, coordinates] : coordinate_data.normal_coordinates) {
for (const auto &coordinate : coordinates) {
number_set.emplace(coordinate);
}
}
std::visit([&](const auto &texture_coordinate_map) {
if constexpr (std::ranges::range<std::decay_t<decltype(texture_coordinate_map)>>) {
for (const auto& [line_number, coordinates] : texture_coordinate_map) {
if constexpr (std::ranges::range<std::decay_t<decltype(coordinates)>>) {
for (const auto &coordinate : coordinates) {
number_set.emplace(coordinate);
}
} else {
number_set.emplace(coordinates);
}
}
}
}, coordinate_data.texture_coordinates);
number_set.emplace(std::numeric_limits<FloatType>::quiet_NaN());
std::vector<FloatType> number_list;
number_list.reserve(number_set.size());
std::copy(number_set.begin(), number_set.end(), std::back_inserter(number_list));
return number_list;
}
template<typename FloatType, typename IndexType> template<typename FloatType, typename IndexType>
coordinate_data_t<IndexType> create_coordinate_index(const coordinate_data_t<FloatType> &coordinate_data, const std::vector<FloatType> &float_list) { coordinate_data_t<IndexType> create_coordinate_index(const coordinate_data_t<FloatType> &coordinate_data, const std::vector<FloatType> &float_list);
coordinate_data_t<IndexType> coordinate_index_data;
static const auto float_compare = float_is_equal<FloatType>{};
static const auto float_less = less_with_nan_first_and_nearly_equal<FloatType>{};
for (const auto& [line_number, coordinates] : coordinate_data.position_coordinates) {
std::array<IndexType, coordinates.size()> position_coordinate_indices;
for (decltype(coordinates.size()) dim = 0; dim < coordinates.size(); ++dim) {
auto iterator = std::lower_bound(float_list.begin(), float_list.end(), coordinates[dim], float_less);
assert(iterator != float_list.end());
assert(float_compare(*iterator, coordinates[dim]));
auto index = std::distance(float_list.begin(), iterator);
assert(index != 0);
position_coordinate_indices[dim] = index;
}
coordinate_index_data.position_coordinates[line_number] = position_coordinate_indices;
}
for (const auto& [line_number, coordinates] : coordinate_data.normal_coordinates) {
std::array<IndexType, coordinates.size()> normal_coordinate_indices;
for (decltype(coordinates.size()) dim = 0; dim < coordinates.size(); ++dim) {
auto iterator = std::lower_bound(float_list.begin(), float_list.end(), coordinates[dim], float_less);
assert(iterator != float_list.end());
assert(float_compare(*iterator, coordinates[dim]));
auto index = std::distance(float_list.begin(), iterator);
assert(index != 0);
normal_coordinate_indices[dim] = index;
}
coordinate_index_data.normal_coordinates[line_number] = normal_coordinate_indices;
}
if (!coordinate_data.texture_coordinates.valueless_by_exception() && coordinate_data.texture_coordinates.index() > 0) {
std::visit([&](const auto &texture_coordinate_map) {
if constexpr (std::ranges::range<std::decay_t<decltype(texture_coordinate_map)>>) {
if constexpr (std::ranges::range<typename std::decay_t<decltype(texture_coordinate_map)>::mapped_type>) {
coordinate_index_data.texture_coordinates = std::map<
typename std::decay_t<decltype(texture_coordinate_map)>::key_type,
std::array<
IndexType,
std::tuple_size_v<typename std::decay_t<decltype(texture_coordinate_map)>::mapped_type>
>
>{};
} else {
coordinate_index_data.texture_coordinates = std::map<
typename std::decay_t<decltype(texture_coordinate_map)>::key_type,
IndexType
>{};
}
for (const auto& [line_number, coordinates] : texture_coordinate_map) {
if constexpr (std::ranges::range<std::decay_t<decltype(coordinates)>>) {
std::array<IndexType, coordinates.size()> texcoord_coordinate_indices;
for (std::size_t dim = 0; dim < coordinates.size(); ++dim) {
auto iterator = std::lower_bound(float_list.begin(), float_list.end(), coordinates[dim], float_less);
assert(iterator != float_list.end());
assert(float_compare(*iterator, coordinates[dim]));
auto index = std::distance(float_list.begin(), iterator);
assert(index != 0);
texcoord_coordinate_indices[dim] = index;
}
std::visit([&](auto &target_data) {
if constexpr (std::is_same_v<
std::map<
file_line_t,
std::array<
IndexType,
std::tuple_size_v<typename std::decay_t<decltype(texture_coordinate_map)>::mapped_type>
>
>,
typename std::decay_t<decltype(target_data)>
>) {
target_data[line_number] = texcoord_coordinate_indices;
}
}, coordinate_index_data.texture_coordinates);
} else {
auto iterator = std::lower_bound(float_list.begin(), float_list.end(), coordinates, float_less);
assert(iterator != float_list.end());
assert(float_compare(*iterator, coordinates));
auto coordinate = std::distance(float_list.begin(), iterator);
assert(coordinate != 0);
std::visit([&](auto &target_data) {
if constexpr (std::is_same_v<
std::map<file_line_t, IndexType>,
typename std::decay_t<decltype(target_data)>
>) {
target_data[line_number] = coordinate;
}
}, coordinate_index_data.texture_coordinates);
}
}
}
int j = 0;
}, coordinate_data.texture_coordinates);
} }
return coordinate_index_data; #include "parse.tpp"
}
}
#endif // __WAVEFRONT_PARSE_HPP__ #endif // __WAVEFRONT_PARSE_HPP__

323
src/parse.tpp Normal file
View File

@@ -0,0 +1,323 @@
#ifndef __WAVEFRONT_PARSE_TPP__
#define __WAVEFRONT_PARSE_TPP__
#include <algorithm>
#include <limits>
#include <ranges>
namespace wavefront {
template<typename FloatType>
coordinate_data_t<FloatType> parse_coordinate_data(const scan_result &scan_data, const wavefront_face_data_result_t &face_data) {
using namespace std::string_literals;
coordinate_data_t<FloatType> result;
if (!scan_data.category_map.contains("v"s)) {
throw parse_error("Missing required \"v\" data");
}
const auto &position_data = scan_data.category_map.at("v"s);
for (const auto &position_index : face_data.index_position_set) {
std::array<FloatType, 3> position_coordinates;
const auto &position_line_index = position_data.at(position_index);
const auto &position_line = scan_data.line_data.at(position_line_index);
decltype(position_line.find_first_of(' ')) number_begin_pos = 2, number_end_pos;
for (std::size_t position_coordinate_index = 0; position_coordinate_index < 3; position_coordinate_index++) {
if (number_begin_pos >= position_line.size()) {
throw parse_error(std::format(
"[{}]: {}",
position_line_index,
std::format("Line \"{}\" must contain at least {} numbers", "v", 3)
));
}
number_end_pos = std::min(position_line.find_first_of(' ', number_begin_pos), position_line.size());
auto [number_end, conversion_error] = std::from_chars(
position_line.data() + number_begin_pos,
position_line.data() + number_end_pos,
position_coordinates[position_coordinate_index],
std::chars_format::fixed
);
if (conversion_error != std::errc() || position_line.data() + number_end_pos != number_end) {
throw parse_error(std::format(
"[{}]: {}",
position_line_index,
std::format("Unable to parse \"{}\" floating point value", "v")
));
}
number_begin_pos = std::min(position_line.find_first_not_of(' ', number_end_pos), position_line.size());
}
if (number_begin_pos < position_line.size()) {
if (position_line.find_first_not_of("0123456789.-", number_begin_pos)) {
throw parse_error(std::format(
"[{}]: {}",
position_line_index,
std::format("Additional data in \"{}\" line: expected {} only", "v", "<x> <y> <z> [weight]")
));
}
}
result.position_coordinates[position_line_index] = position_coordinates;
}
if (face_data.index_normal_set.size() > 0 && scan_data.category_map.contains("vn"s)) {
const auto &normal_data = scan_data.category_map.at("vn"s);
for (const auto &normal_index : face_data.index_normal_set) {
std::array<FloatType, 3> normal_coordinates;
const auto &normal_line_index = normal_data.at(normal_index);
const auto &normal_line = scan_data.line_data.at(normal_line_index);
decltype(normal_line.find_first_of(' ')) number_begin_pos = 3, number_end_pos;
for (std::size_t normal_coordinate_index = 0; normal_coordinate_index < 3; normal_coordinate_index++) {
if (number_begin_pos >= normal_line.size()) {
throw parse_error(std::format(
"[{}]: {}",
normal_line_index,
std::format("Line \"{}\" must contain exactly {} numbers", "vn", 3)
));
}
number_end_pos = std::min(normal_line.find_first_of(' ', number_begin_pos), normal_line.size());
auto [number_end, conversion_error] = std::from_chars(
normal_line.data() + number_begin_pos,
normal_line.data() + number_end_pos,
normal_coordinates[normal_coordinate_index],
std::chars_format::fixed
);
if (conversion_error != std::errc() || normal_line.data() + number_end_pos != number_end) {
throw parse_error(std::format(
"[{}]: {}",
normal_line_index,
std::format("Unable to parse \"{}\" floating point value", "vn")
));
}
number_begin_pos = std::min(normal_line.find_first_not_of(' ', number_end_pos), normal_line.size());
}
if (number_end_pos != normal_line.size()) {
throw parse_error(std::format(
"[{}]: {}",
normal_line_index,
std::format("Additional data in \"{}\" line: expected {} only", "vn", "<x> <y> <z>")
));
}
result.normal_coordinates[normal_line_index] = normal_coordinates;
}
}
if (face_data.index_texcoord_set.size() > 0 && scan_data.category_map.contains("vt"s)) {
using insert_texcoord_data_t = std::function<void()>;
const auto &texcoord_data = scan_data.category_map.at("vt"s);
for (const auto &texcoord_index : face_data.index_texcoord_set) {
std::array<FloatType, 3> texcoord_coordinates;
const auto &texcoord_line_index = texcoord_data.at(texcoord_index);
const auto &texcoord_line = scan_data.line_data.at(texcoord_line_index);
decltype(texcoord_line.find_first_of(' ')) number_begin_pos = 3, number_end_pos;
std::size_t texcoord_coordinate_index = 0;
while (number_begin_pos < texcoord_line.size()) {
if (texcoord_coordinate_index >= 3) {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Line \"{}\" must contain maximum {} numbers", "vt", 3)
));
}
number_end_pos = std::min(texcoord_line.find_first_of(' ', number_begin_pos), texcoord_line.size());
auto [number_end, conversion_error] = std::from_chars(
texcoord_line.data() + number_begin_pos,
texcoord_line.data() + number_end_pos,
texcoord_coordinates[texcoord_coordinate_index++],
std::chars_format::fixed
);
if (conversion_error != std::errc() || texcoord_line.data() + number_end_pos != number_end) {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Unable to parse \"{}\" floating point value", "vt")
));
}
number_begin_pos = std::min(texcoord_line.find_first_not_of(' ', number_end_pos), texcoord_line.size());
}
if (texcoord_coordinate_index == 0) [[unlikely]] {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Unable to parse \"{}\" line: expected minimum one number", "vt")
));
}
if (result.texture_coordinates.valueless_by_exception() || result.texture_coordinates.index() == 0) [[unlikely]] {
if (texcoord_coordinate_index == 1) {
result.texture_coordinates = std::map<file_line_t, FloatType>{};
} else if (texcoord_coordinate_index == 2) {
result.texture_coordinates = std::map<file_line_t, std::array<FloatType, 2>>{};
} else if (texcoord_coordinate_index == 3) {
result.texture_coordinates = std::map<file_line_t, std::array<FloatType, 3>>{};
} else {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Line \"{}\" must contain maximum {} numbers", "vt", 3)
));
}
}
if (texcoord_coordinate_index == 0) [[unlikely]] {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Unable to parse \"{}\" line: expected minimum one number", "vt")
));
}
try {
if (texcoord_coordinate_index == 1) {
std::get<1>(result.texture_coordinates)[texcoord_line_index] = texcoord_coordinates[0];
} else if (texcoord_coordinate_index == 2) {
std::get<2>(result.texture_coordinates)[texcoord_line_index] = std::array<FloatType, 2>{texcoord_coordinates[0], texcoord_coordinates[1]};
} else if (texcoord_coordinate_index == 3) {
std::get<3>(result.texture_coordinates)[texcoord_line_index] = texcoord_coordinates;
}
} catch (std::bad_variant_access&) {
throw parse_error(std::format(
"[{}]: {}",
texcoord_line_index,
std::format("Line \"{}\": {}D texture coordinates expected, got {}D texture coordinates", "vt", result.texture_coordinates.index(), texcoord_coordinate_index)
));
}
}
}
return result;
}
template<typename FloatType>
std::vector<FloatType> create_number_list(const coordinate_data_t<FloatType> &coordinate_data) {
numset_t<FloatType> number_set;
for (const auto& [line_number, coordinates] : coordinate_data.position_coordinates) {
for (const auto &coordinate : coordinates) {
number_set.emplace(coordinate);
}
}
for (const auto& [line_number, coordinates] : coordinate_data.normal_coordinates) {
for (const auto &coordinate : coordinates) {
number_set.emplace(coordinate);
}
}
std::visit([&](const auto &texture_coordinate_map) {
if constexpr (std::ranges::range<std::decay_t<decltype(texture_coordinate_map)>>) {
for (const auto& [line_number, coordinates] : texture_coordinate_map) {
if constexpr (std::ranges::range<std::decay_t<decltype(coordinates)>>) {
for (const auto &coordinate : coordinates) {
number_set.emplace(coordinate);
}
} else {
number_set.emplace(coordinates);
}
}
}
}, coordinate_data.texture_coordinates);
number_set.emplace(std::numeric_limits<FloatType>::quiet_NaN());
std::vector<FloatType> number_list;
number_list.reserve(number_set.size());
std::copy(number_set.begin(), number_set.end(), std::back_inserter(number_list));
return number_list;
}
template<typename FloatType, typename IndexType>
coordinate_data_t<IndexType> create_coordinate_index(const coordinate_data_t<FloatType> &coordinate_data, const std::vector<FloatType> &float_list) {
coordinate_data_t<IndexType> coordinate_index_data;
static const auto float_compare = float_is_equal<FloatType>{};
static const auto float_less = less_with_nan_first_and_nearly_equal<FloatType>{};
for (const auto& [line_number, coordinates] : coordinate_data.position_coordinates) {
std::array<IndexType, coordinates.size()> position_coordinate_indices;
for (decltype(coordinates.size()) dim = 0; dim < coordinates.size(); ++dim) {
auto iterator = std::lower_bound(float_list.begin(), float_list.end(), coordinates[dim], float_less);
assert(iterator != float_list.end());
assert(float_compare(*iterator, coordinates[dim]));
auto index = std::distance(float_list.begin(), iterator);
assert(index != 0);
position_coordinate_indices[dim] = index;
}
coordinate_index_data.position_coordinates[line_number] = position_coordinate_indices;
}
for (const auto& [line_number, coordinates] : coordinate_data.normal_coordinates) {
std::array<IndexType, coordinates.size()> normal_coordinate_indices;
for (decltype(coordinates.size()) dim = 0; dim < coordinates.size(); ++dim) {
auto iterator = std::lower_bound(float_list.begin(), float_list.end(), coordinates[dim], float_less);
assert(iterator != float_list.end());
assert(float_compare(*iterator, coordinates[dim]));
auto index = std::distance(float_list.begin(), iterator);
assert(index != 0);
normal_coordinate_indices[dim] = index;
}
coordinate_index_data.normal_coordinates[line_number] = normal_coordinate_indices;
}
if (!coordinate_data.texture_coordinates.valueless_by_exception() && coordinate_data.texture_coordinates.index() > 0) {
std::visit([&](const auto &texture_coordinate_map) {
if constexpr (std::ranges::range<std::decay_t<decltype(texture_coordinate_map)>>) {
if constexpr (std::ranges::range<typename std::decay_t<decltype(texture_coordinate_map)>::mapped_type>) {
coordinate_index_data.texture_coordinates = std::map<
typename std::decay_t<decltype(texture_coordinate_map)>::key_type,
std::array<
IndexType,
std::tuple_size_v<typename std::decay_t<decltype(texture_coordinate_map)>::mapped_type>
>
>{};
} else {
coordinate_index_data.texture_coordinates = std::map<
typename std::decay_t<decltype(texture_coordinate_map)>::key_type,
IndexType
>{};
}
for (const auto& [line_number, coordinates] : texture_coordinate_map) {
if constexpr (std::ranges::range<std::decay_t<decltype(coordinates)>>) {
std::array<IndexType, coordinates.size()> texcoord_coordinate_indices;
for (std::size_t dim = 0; dim < coordinates.size(); ++dim) {
auto iterator = std::lower_bound(float_list.begin(), float_list.end(), coordinates[dim], float_less);
assert(iterator != float_list.end());
assert(float_compare(*iterator, coordinates[dim]));
auto index = std::distance(float_list.begin(), iterator);
assert(index != 0);
texcoord_coordinate_indices[dim] = index;
}
std::visit([&](auto &target_data) {
if constexpr (std::is_same_v<
std::map<
file_line_t,
std::array<
IndexType,
std::tuple_size_v<typename std::decay_t<decltype(texture_coordinate_map)>::mapped_type>
>
>,
typename std::decay_t<decltype(target_data)>
>) {
target_data[line_number] = texcoord_coordinate_indices;
}
}, coordinate_index_data.texture_coordinates);
} else {
auto iterator = std::lower_bound(float_list.begin(), float_list.end(), coordinates, float_less);
assert(iterator != float_list.end());
assert(float_compare(*iterator, coordinates));
auto coordinate = std::distance(float_list.begin(), iterator);
assert(coordinate != 0);
std::visit([&](auto &target_data) {
if constexpr (std::is_same_v<
std::map<file_line_t, IndexType>,
typename std::decay_t<decltype(target_data)>
>) {
target_data[line_number] = coordinate;
}
}, coordinate_index_data.texture_coordinates);
}
}
}
int j = 0;
}, coordinate_data.texture_coordinates);
}
return coordinate_index_data;
}
}
#endif // __WAVEFRONT_PARSE_TPP__

View File

@@ -11,8 +11,6 @@
#include "scan.hpp" #include "scan.hpp"
namespace wavefront { namespace wavefront {
scan_error::scan_error(const std::string &message) : std::runtime_error(message) {}
scan_result::scan_result() : scan_result::scan_result() :
total_lines(0), total_lines(0),
line_data(), line_data(),

View File

@@ -10,11 +10,6 @@
#include "settings.hpp" #include "settings.hpp"
namespace wavefront { namespace wavefront {
class scan_error : public std::runtime_error {
public:
explicit scan_error(const std::string &message);
};
struct scan_result { struct scan_result {
std::size_t total_lines; std::size_t total_lines;
std::map<std::size_t, std::string> line_data; std::map<std::size_t, std::string> line_data;

View File

@@ -5,10 +5,10 @@ namespace wavefront {
auto start = source.data(); auto start = source.data();
// size is the size of buffer, data() + size() is the first available byte after the string. // size is the size of buffer, data() + size() is the first available byte after the string.
auto end = source.data() + source.size() - 1; auto end = source.data() + source.size() - 1;
while (start < end && std::isspace(*start)) { while (start <= end && std::isspace(*start)) {
++start; ++start;
} }
while (start < end && (std::isspace(*end) || *end == 0)) { while (start <= end && (std::isspace(*end) || *end == 0)) {
--end; --end;
} }
return std::string_view(start, end - start + 1); return std::string_view(start, end - start + 1);

6
test.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
cmake -S . -B build/Debug -DCMAKE_BUILD_TYPE=Debug
cmake --build build/Debug --config Debug
ctest --test-dir build/Debug --output-on-failure

View File

@@ -0,0 +1,26 @@
# Command
v 0.1 0.2 0.3
v 0.2 0.3 0.4
v 0.3 0.4 0.5
v 1.1 1.2 1.3
v 1.2 1.3 1.4
v 1.3 1.4 1.5
v 2.1 2.2 2.3
v 2.2 2.3 2.4
v 2.3 2.4 2.5
vn 0.15 0.25 0.35
vn 0.25 0.35 0.45
vn 0.35 0.45 0.55
vt 0.9 0.8
vt 0.8 0.7
f 1/1/1 2/2/2 3/3/2
o test
f 4/3/2 5/2/1 6/1/2
g test
f 7/2/2 8/3/2 9/1/1
o
f 8/1/1 5/2/2 3/3/2
g
f 2/1/1 6/2/2 4/3/2
h this line is skipped

315
tests/unit/parse_test.cpp Normal file
View File

@@ -0,0 +1,315 @@
#include <string>
#include <variant>
#include <vector>
#include <gtest/gtest.h>
#include "parse.hpp"
#include "scan.hpp"
namespace {
#define ASSERT_COORDINATE_3D(container, line, x, y, z) \
{ \
const auto &coord = (container).at(line); \
EXPECT_FLOAT_EQ(coord[0], (x)); \
EXPECT_FLOAT_EQ(coord[1], (y)); \
EXPECT_FLOAT_EQ(coord[2], (z)); \
}
#define ASSERT_COORDINATE_2D(container, line, x, y) \
{ \
const auto &coord = (container).at(line); \
EXPECT_FLOAT_EQ(coord[0], (x)); \
EXPECT_FLOAT_EQ(coord[1], (y)); \
}
TEST(ParseFaceData, BuildsTrianglesAndIndexSets) {
wavefront::scan_result scan_result;
scan_result.total_lines = 26;
scan_result.line_data = {
{17u, "f 1/1/1 2/2/2 3/3/2"},
{19u, "f 4/3/2 5/2/1 6/1/2"},
{21u, "f 7/2/2 8/3/2 9/1/1"},
{23u, "f 8/1/1 5/2/2 3/3/2"},
{25u, "f 2/1/1 6/2/2 4/3/2"}
};
scan_result.category_map = {
{"f", {17u, 19u, 21u, 23u, 25u}}
};
const auto face_data = wavefront::parse_face_data(scan_result);
EXPECT_EQ(face_data.triangle_list.size(), 5u);
EXPECT_EQ(face_data.index_position_set.size(), 9u);
EXPECT_TRUE(face_data.index_position_set.contains(1));
EXPECT_TRUE(face_data.index_position_set.contains(9));
EXPECT_EQ(face_data.index_normal_set.size(), 2u);
EXPECT_TRUE(face_data.index_normal_set.contains(1));
EXPECT_TRUE(face_data.index_normal_set.contains(2));
EXPECT_EQ(face_data.index_texcoord_set.size(), 3u);
EXPECT_TRUE(face_data.index_texcoord_set.contains(1));
EXPECT_TRUE(face_data.index_texcoord_set.contains(3));
const auto &first_triangle = face_data.triangle_list.at(0);
EXPECT_EQ(first_triangle[0].face_line_number, 17u);
EXPECT_EQ(first_triangle[0].position_index, 1);
EXPECT_EQ(first_triangle[0].texcoord_index, 1);
EXPECT_EQ(first_triangle[0].normal_index, 1);
}
TEST(ParseCoordinateData, MapsCoordinatesWithoutParsingFaces) {
wavefront::scan_result scan_result;
scan_result.total_lines = 26;
scan_result.line_data = {
{3u, "v 0.1 0.2 0.3"},
{4u, "v 0.2 0.3 0.4"},
{5u, "v 0.3 0.4 0.5"},
{6u, "v 1.1 1.2 1.3"},
{7u, "v 1.2 1.3 1.4"},
{8u, "v 1.3 1.4 1.5"},
{9u, "v 2.1 2.2 2.3"},
{10u, "v 2.2 2.3 2.4"},
{11u, "v 2.3 2.4 2.5"},
{12u, "vn 0.15 0.25 0.35"},
{13u, "vn 0.25 0.35 0.45"},
{14u, "vn 0.35 0.45 0.55"},
{15u, "vt 0.9 0.8"},
{16u, "vt 0.8 0.7"},
{17u, "f 1/1/1 2/2/2 3/3/2"},
{19u, "f 4/3/2 5/2/1 6/1/2"},
{21u, "f 7/2/2 8/3/2 9/1/1"},
{23u, "f 8/1/1 5/2/2 3/3/2"},
{25u, "f 2/1/1 6/2/2 4/3/2"}
};
scan_result.category_map = {
{"v", {0u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u, 11u}},
{"vn", {0u, 12u, 13u, 14u}},
{"vt", {0u, 15u, 16u}},
{"f", {17u, 19u, 21u, 23u, 25u}}
};
wavefront::wavefront_face_data_result_t face_data;
face_data.index_position_set = {1, 2, 3, 4, 5, 6, 7, 8, 9};
face_data.index_normal_set = {1, 2, 3};
face_data.index_texcoord_set = {1, 2};
face_data.triangle_list = {
{{{17u, 1, 1, 1}, {17u, 2, 2, 2}, {17u, 3, 3, 2}}},
{{{19u, 4, 3, 2}, {19u, 5, 2, 1}, {19u, 6, 1, 2}}},
{{{21u, 7, 2, 2}, {21u, 8, 3, 2}, {21u, 9, 1, 1}}},
{{{23u, 8, 1, 1}, {23u, 5, 2, 2}, {23u, 3, 3, 2}}},
{{{25u, 2, 1, 1}, {25u, 6, 2, 2}, {25u, 4, 3, 2}}}
};
const auto coordinate_data = wavefront::parse_coordinate_data<float>(scan_result, face_data);
ASSERT_EQ(coordinate_data.position_coordinates.size(), 9u);
ASSERT_COORDINATE_3D(coordinate_data.position_coordinates, 3u, 0.1f, 0.2f, 0.3f);
ASSERT_COORDINATE_3D(coordinate_data.position_coordinates, 4u, 0.2f, 0.3f, 0.4f);
ASSERT_COORDINATE_3D(coordinate_data.position_coordinates, 5u, 0.3f, 0.4f, 0.5f);
ASSERT_COORDINATE_3D(coordinate_data.position_coordinates, 6u, 1.1f, 1.2f, 1.3f);
ASSERT_COORDINATE_3D(coordinate_data.position_coordinates, 7u, 1.2f, 1.3f, 1.4f);
ASSERT_COORDINATE_3D(coordinate_data.position_coordinates, 8u, 1.3f, 1.4f, 1.5f);
ASSERT_COORDINATE_3D(coordinate_data.position_coordinates, 9u, 2.1f, 2.2f, 2.3f);
ASSERT_COORDINATE_3D(coordinate_data.position_coordinates, 10u, 2.2f, 2.3f, 2.4f);
ASSERT_COORDINATE_3D(coordinate_data.position_coordinates, 11u, 2.3f, 2.4f, 2.5f);
ASSERT_EQ(coordinate_data.normal_coordinates.size(), 3u);
ASSERT_COORDINATE_3D(coordinate_data.normal_coordinates, 12u, 0.15f, 0.25f, 0.35f);
ASSERT_COORDINATE_3D(coordinate_data.normal_coordinates, 13u, 0.25f, 0.35f, 0.45f);
ASSERT_COORDINATE_3D(coordinate_data.normal_coordinates, 14u, 0.35f, 0.45f, 0.55f);
ASSERT_EQ(coordinate_data.texture_coordinates.index(), 2u);
ASSERT_EQ(std::get<2>(coordinate_data.texture_coordinates).size(), 2u);
ASSERT_COORDINATE_2D(std::get<2>(coordinate_data.texture_coordinates), 15u, 0.9f, 0.8f);
ASSERT_COORDINATE_2D(std::get<2>(coordinate_data.texture_coordinates), 16u, 0.8f, 0.7f);
}
struct InvalidNumberCase {
std::string line_type;
std::size_t invalid_index;
std::string expected_message;
};
class ParseCoordinateDataInvalidNumberTest
: public ::testing::TestWithParam<InvalidNumberCase> {
};
static std::string build_line_with_invalid_number(
const std::string &line_type,
std::size_t invalid_index
) {
std::vector<std::string> values;
if (line_type == "vt") {
values = {"0.9", "0.8", "0.7"};
} else {
values = {"0.1", "0.2", "0.3"};
}
if (invalid_index < values.size()) {
values[invalid_index] = "x";
}
std::string line = line_type;
for (const auto &value : values) {
line += " ";
line += value;
}
return line;
}
TEST_P(ParseCoordinateDataInvalidNumberTest, ThrowsOnInvalidNumber) {
const auto &param = GetParam();
wavefront::scan_result scan_result;
scan_result.total_lines = 3;
scan_result.line_data = {
{1u, "v 0.1 0.2 0.3"},
{3u, build_line_with_invalid_number(param.line_type, param.invalid_index)}
};
if (param.line_type == "v") {
scan_result.category_map = {
{"v", {0u, 1u, 3u}}
};
} else {
scan_result.category_map = {
{"v", {0u, 1u}},
{param.line_type, {0u, 3u}}
};
}
wavefront::wavefront_face_data_result_t face_data;
if (param.line_type == "v") {
face_data.index_position_set = {2};
} else if (param.line_type == "vn") {
face_data.index_normal_set = {1};
} else {
face_data.index_texcoord_set = {1};
}
try {
(void)wavefront::parse_coordinate_data<float>(scan_result, face_data);
FAIL() << "Expected parse_error";
} catch (const wavefront::parse_error &ex) {
EXPECT_EQ(std::string(ex.what()), param.expected_message);
}
}
INSTANTIATE_TEST_SUITE_P(
ParseCoordinateData,
ParseCoordinateDataInvalidNumberTest,
::testing::Values(
InvalidNumberCase{"v", 0u, "[3]: Unable to parse \"v\" floating point value"},
InvalidNumberCase{"v", 1u, "[3]: Unable to parse \"v\" floating point value"},
InvalidNumberCase{"v", 2u, "[3]: Unable to parse \"v\" floating point value"},
InvalidNumberCase{"vn", 0u, "[3]: Unable to parse \"vn\" floating point value"},
InvalidNumberCase{"vn", 1u, "[3]: Unable to parse \"vn\" floating point value"},
InvalidNumberCase{"vn", 2u, "[3]: Unable to parse \"vn\" floating point value"},
InvalidNumberCase{"vt", 0u, "[3]: Unable to parse \"vt\" floating point value"},
InvalidNumberCase{"vt", 1u, "[3]: Unable to parse \"vt\" floating point value"},
InvalidNumberCase{"vt", 2u, "[3]: Unable to parse \"vt\" floating point value"}
)
);
TEST(ParseCoordinateData, ThrowsWhenMissingVertexData) {
wavefront::scan_result scan_result;
scan_result.total_lines = 3;
scan_result.line_data = {
{3u, "vn 0.1 0.2 0.3"}
};
scan_result.category_map = {
{"vn", {0u, 3u}}
};
wavefront::wavefront_face_data_result_t face_data;
face_data.index_position_set = {1};
try {
(void)wavefront::parse_coordinate_data<float>(scan_result, face_data);
FAIL() << "Expected parse_error";
} catch (const wavefront::parse_error &ex) {
EXPECT_EQ(std::string(ex.what()), "Missing required \"v\" data");
}
}
struct UnderflowCase {
std::string line_type;
std::size_t number_count;
std::string expected_message;
};
class ParseCoordinateDataUnderflowTest
: public ::testing::TestWithParam<UnderflowCase> {
};
static std::string build_line_with_count(
const std::string &line_type,
std::size_t number_count
) {
std::vector<std::string> values;
if (line_type == "vt") {
values = {"0.9", "0.8", "0.7"};
} else {
values = {"0.1", "0.2", "0.3"};
}
std::string line = line_type;
for (std::size_t i = 0; i < number_count && i < values.size(); ++i) {
line += " ";
line += values[i];
}
return line;
}
TEST_P(ParseCoordinateDataUnderflowTest, ThrowsOnInsufficientNumbers) {
const auto &param = GetParam();
wavefront::scan_result scan_result;
scan_result.total_lines = 3;
scan_result.line_data = {
{1u, "v 0.1 0.2 0.3"},
{3u, build_line_with_count(param.line_type, param.number_count)}
};
if (param.line_type == "v") {
scan_result.category_map = {
{"v", {0u, 1u, 3u}}
};
} else {
scan_result.category_map = {
{"v", {0u, 1u}},
{param.line_type, {0u, 3u}}
};
}
wavefront::wavefront_face_data_result_t face_data;
if (param.line_type == "v") {
face_data.index_position_set = {2};
} else if (param.line_type == "vn") {
face_data.index_normal_set = {1};
} else {
face_data.index_texcoord_set = {1};
}
try {
(void)wavefront::parse_coordinate_data<float>(scan_result, face_data);
FAIL() << "Expected parse_error";
} catch (const wavefront::parse_error &ex) {
EXPECT_EQ(std::string(ex.what()), param.expected_message);
}
}
INSTANTIATE_TEST_SUITE_P(
ParseCoordinateData,
ParseCoordinateDataUnderflowTest,
::testing::Values(
UnderflowCase{"v", 0u, "[3]: Line \"v\" must contain at least 3 numbers"},
UnderflowCase{"v", 1u, "[3]: Line \"v\" must contain at least 3 numbers"},
UnderflowCase{"v", 2u, "[3]: Line \"v\" must contain at least 3 numbers"},
UnderflowCase{"vn", 0u, "[3]: Line \"vn\" must contain exactly 3 numbers"},
UnderflowCase{"vn", 1u, "[3]: Line \"vn\" must contain exactly 3 numbers"},
UnderflowCase{"vn", 2u, "[3]: Line \"vn\" must contain exactly 3 numbers"},
UnderflowCase{"vt", 0u, "[3]: Unable to parse \"vt\" line: expected minimum one number"}
)
);
}

64
tests/unit/scan_test.cpp Normal file
View File

@@ -0,0 +1,64 @@
#include <filesystem>
#include <fstream>
#include <gtest/gtest.h>
#include "scan.hpp"
namespace {
std::filesystem::path test_data_path(const std::string &filename) {
const auto here = std::filesystem::path(__FILE__).parent_path();
return here.parent_path() / "data" / filename;
}
TEST(Scan, ReadsAllSupportedTypes) {
std::ifstream input(test_data_path("test-scan-1.txt"));
ASSERT_TRUE(input.is_open());
const auto result = wavefront::scan(input);
EXPECT_EQ(result.total_lines, 26u);
EXPECT_EQ(result.line_data.size(), 21u);
EXPECT_TRUE(result.category_map.contains("v"));
EXPECT_TRUE(result.category_map.contains("vn"));
EXPECT_TRUE(result.category_map.contains("vt"));
EXPECT_TRUE(result.category_map.contains("f"));
EXPECT_EQ(result.category_map.at("v").size(), 10u);
EXPECT_EQ(result.category_map.at("vn").size(), 4u);
EXPECT_EQ(result.category_map.at("vt").size(), 3u);
EXPECT_EQ(result.category_map.at("f").size(), 5u);
}
TEST(Scan, FiltersByObject) {
std::ifstream input(test_data_path("test-scan-1.txt"));
ASSERT_TRUE(input.is_open());
const auto result = wavefront::scan(input, {"test"});
EXPECT_EQ(result.category_map.at("f").size(), 4u);
EXPECT_EQ(result.category_map.at("f")[0], 19u);
EXPECT_EQ(result.category_map.at("f")[1], 21u);
}
TEST(Scan, FiltersByGroup) {
std::ifstream input(test_data_path("test-scan-1.txt"));
ASSERT_TRUE(input.is_open());
const auto result = wavefront::scan(input, {}, {"test"});
EXPECT_EQ(result.category_map.at("f").size(), 3u);
EXPECT_EQ(result.category_map.at("f")[0], 21u);
}
TEST(Scan, FiltersByObjectAndGroup) {
std::ifstream input(test_data_path("test-scan-1.txt"));
ASSERT_TRUE(input.is_open());
const auto result = wavefront::scan(input, {"test"}, {"test"});
EXPECT_EQ(result.category_map.at("f").size(), 3u);
EXPECT_EQ(result.category_map.at("f")[0], 21u);
}
}

View File

@@ -0,0 +1,186 @@
#include <filesystem>
#include <fstream>
#include <random>
#include <sstream>
#include <string>
#include <gtest/gtest.h>
#include "settings.hpp"
namespace {
class ScopedFileCleanup {
public:
explicit ScopedFileCleanup(std::filesystem::path path) : path_(std::move(path)) {}
ScopedFileCleanup(const ScopedFileCleanup &) = delete;
ScopedFileCleanup &operator=(const ScopedFileCleanup &) = delete;
ScopedFileCleanup(ScopedFileCleanup &&) = delete;
ScopedFileCleanup &operator=(ScopedFileCleanup &&) = delete;
~ScopedFileCleanup() {
if (!path_.empty()) {
std::error_code error;
std::filesystem::remove(path_, error);
}
}
private:
std::filesystem::path path_;
};
std::string random_token(std::size_t length) {
static const char charset[] =
"0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
std::string result;
result.reserve(length);
std::mt19937 engine{std::random_device{}()};
std::uniform_int_distribution<std::size_t> dist(0, sizeof(charset) - 2);
for (std::size_t i = 0; i < length; ++i) {
result.push_back(charset[dist(engine)]);
}
return result;
}
TEST(SettingsBuilder, DefaultsToStdStreamsAndFlagsOff) {
wavefront::SettingsBuilder builder;
EXPECT_EQ(&builder.input(), &std::cin);
EXPECT_EQ(&builder.output(), &std::cout);
EXPECT_FALSE(builder.with_normals());
EXPECT_FALSE(builder.with_texcoords());
EXPECT_FALSE(builder.use_float64());
wavefront::Settings settings = builder.build();
EXPECT_EQ(&settings.input(), &std::cin);
EXPECT_EQ(&settings.output(), &std::cout);
EXPECT_FALSE(settings.extract_normals());
EXPECT_FALSE(settings.extract_texcoords());
EXPECT_FALSE(settings.use_float64());
EXPECT_TRUE(settings.selected_objects().empty());
EXPECT_TRUE(settings.selected_groups().empty());
}
TEST(SettingsBuilder, AppliesConfigurationAndSelection) {
std::istringstream input("dummy");
std::ostringstream output;
wavefront::SettingsBuilder builder;
builder.input(input)
.output(output)
.with_normals(true)
.with_texcoords(true)
.use_float64(true);
builder.selected_objects().push_back("object_a");
builder.selected_groups().push_back("group_a");
EXPECT_EQ(&builder.input(), &input);
EXPECT_EQ(&builder.output(), &output);
EXPECT_TRUE(builder.with_normals());
EXPECT_TRUE(builder.with_texcoords());
EXPECT_TRUE(builder.use_float64());
wavefront::Settings settings = builder.build();
EXPECT_EQ(&settings.input(), &input);
EXPECT_EQ(&settings.output(), &output);
EXPECT_TRUE(settings.extract_normals());
EXPECT_TRUE(settings.extract_texcoords());
EXPECT_TRUE(settings.use_float64());
ASSERT_EQ(settings.selected_objects().size(), 1u);
EXPECT_EQ(settings.selected_objects()[0], "object_a");
ASSERT_EQ(settings.selected_groups().size(), 1u);
EXPECT_EQ(settings.selected_groups()[0], "group_a");
}
TEST(SettingsBuilder, CanOpenFileFromString) {
const auto temp_dir = std::filesystem::temp_directory_path();
const auto token = random_token(12);
const auto input_path = temp_dir / std::filesystem::path("dragiyski-wavefront-parser-test-settings-input-" + token + ".txt");
const auto output_path = temp_dir / std::filesystem::path("dragiyski-wavefront-parser-test-settings-output-" + token + ".txt");
const ScopedFileCleanup input_cleanup(input_path);
const ScopedFileCleanup output_cleanup(output_path);
{
std::ofstream seed(input_path);
seed << token;
}
wavefront::SettingsBuilder builder;
builder.input(input_path.string())
.output(output_path.string());
wavefront::Settings settings = builder.build();
EXPECT_NE(&settings.input(), &std::cin);
EXPECT_NE(&settings.output(), &std::cout);
auto *input_stream = dynamic_cast<std::ifstream *>(&settings.input());
ASSERT_NE(input_stream, nullptr);
EXPECT_TRUE(input_stream->is_open());
std::string input_contents;
*input_stream >> input_contents;
EXPECT_EQ(input_contents, token);
auto *output_stream = dynamic_cast<std::ofstream *>(&settings.output());
ASSERT_NE(output_stream, nullptr);
EXPECT_TRUE(output_stream->is_open());
*output_stream << token;
output_stream->flush();
std::ifstream verify(output_path);
std::string output_contents;
verify >> output_contents;
EXPECT_EQ(output_contents, token);
}
TEST(SettingsBuilder, CanOpenFileFromMovedStreams) {
const auto temp_dir = std::filesystem::temp_directory_path();
const auto token = random_token(12);
const auto input_path = temp_dir / std::filesystem::path("dragiyski-wavefront-parser-test-settings-input-move-" + token + ".txt");
const auto output_path = temp_dir / std::filesystem::path("dragiyski-wavefront-parser-test-settings-output-move-" + token + ".txt");
const ScopedFileCleanup input_cleanup(input_path);
const ScopedFileCleanup output_cleanup(output_path);
{
std::ofstream seed(input_path);
seed << token;
}
std::ifstream input_stream(input_path);
std::ofstream output_stream(output_path);
wavefront::SettingsBuilder builder;
builder.input(std::move(input_stream))
.output(std::move(output_stream));
wavefront::Settings settings = builder.build();
EXPECT_NE(&settings.input(), &std::cin);
EXPECT_NE(&settings.output(), &std::cout);
auto *input_file = dynamic_cast<std::ifstream *>(&settings.input());
ASSERT_NE(input_file, nullptr);
EXPECT_TRUE(input_file->is_open());
std::string input_contents;
*input_file >> input_contents;
EXPECT_EQ(input_contents, token);
auto *output_file = dynamic_cast<std::ofstream *>(&settings.output());
ASSERT_NE(output_file, nullptr);
EXPECT_TRUE(output_file->is_open());
*output_file << token;
output_file->flush();
std::ifstream verify(output_path);
std::string output_contents;
verify >> output_contents;
EXPECT_EQ(output_contents, token);
}
}

36
tests/unit/trim_test.cpp Normal file
View File

@@ -0,0 +1,36 @@
#include <string>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "trim.hpp"
namespace {
using ::testing::StrEq;
struct TrimCase {
const char *source;
const char *expected;
};
class TrimTest : public ::testing::TestWithParam<TrimCase> {
};
TEST_P(TrimTest, ReturnsTrimmedString) {
const auto &param = GetParam();
std::string_view result = wavefront::trim(param.source);
EXPECT_THAT(std::string(result), StrEq(param.expected));
}
INSTANTIATE_TEST_SUITE_P(
TrimCases,
TrimTest,
::testing::Values(
TrimCase{" hello world ", "hello world"},
TrimCase{"\t\n spaced\t\n", "spaced"},
TrimCase{"trimmed", "trimmed"},
TrimCase{" ", ""},
TrimCase{"", ""}
)
);
}