From 58a84ade0deeae6569399c4cc7fd71e3e2eb9732 Mon Sep 17 00:00:00 2001 From: Plamen Dragiyski Date: Sat, 19 Jul 2025 17:30:42 +0300 Subject: [PATCH] initial parser and compiler --- .gitignore | 2 + CMakeLists.txt | 8 ++ src/helper.hpp | 15 +++ src/main.cpp | 139 +++++++++++++++++++++++ src/numset.hpp | 70 ++++++++++++ src/output.cpp | 93 ++++++++++++++++ src/output.hpp | 32 ++++++ src/wavefront.cpp | 278 ++++++++++++++++++++++++++++++++++++++++++++++ src/wavefront.hpp | 80 +++++++++++++ 9 files changed, 717 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 src/helper.hpp create mode 100644 src/main.cpp create mode 100644 src/numset.hpp create mode 100644 src/output.cpp create mode 100644 src/output.hpp create mode 100644 src/wavefront.cpp create mode 100644 src/wavefront.hpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6f31401 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +.vscode/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d311dee --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.16) +project(wavefront_parser LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +file(GLOB SRC_FILES src/*.cpp) +add_executable(wavefront_parser ${SRC_FILES}) diff --git a/src/helper.hpp b/src/helper.hpp new file mode 100644 index 0000000..dd34816 --- /dev/null +++ b/src/helper.hpp @@ -0,0 +1,15 @@ +#ifndef APP_HELPER_H +#define APP_HELPER_H + +#include +#include +#include + +template +constexpr std::array array_fill(const T& value) { + std::array array{}; + array.fill(value); + return array; +} + +#endif // APP_HELPER_H \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..6638a23 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,139 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wavefront.hpp" +#include "output.hpp" + +namespace fs = std::filesystem; + +void printUsage() { + std::cout + << "Usage: parse-wavefront [options] " << std::endl + << "Options:" << std::endl + << " --filter-object / -O Filter objects by name (can be used multiple times)" << std::endl + << " --filter-group / -G Filter groups by name (can be used multiple times)" << std::endl + << " --output-float-set / -f Output float set to file" << std::endl + << " --output-position / -p Output positions to file" << std::endl + << " --output-vertex / -v Output vertices to file" << std::endl + << " --output-mesh / -m Output mesh to file" << std::endl + << " --output-normal-file / -n Output normals to file" << std::endl + << " --output-texcoord-file / -t Output texture coordinates to file" << std::endl + << " -- Stop processing flags" << std::endl + ; + std::cout + << " Input Wavefront OBJ file" << std::endl + << "Examples:" << std::endl + << " parse-wavefront --float64 --output-float-set output.fset --output-position output.pos --filter-object Car,Wheel --filter-group Metal -- model.obj" << std::endl + ; +} + +[[noreturn]] +void exitWithError(const std::string& message) { + std::cerr << "Error: " << message << std::endl; + std::exit(1); +} + +int main(int argc, char *argv[]) { + std::vector selected_objects, selected_groups; + bool process_flags = true; + bool has_input_file = false; + bool has_output_float_set_file = false; + bool has_output_position_file = false; + bool has_output_mesh_file = false; + bool has_output_vertex_file = false; + std::string input_file_path; + for (decltype(argc) i = 1; i < argc; ++i) { + std::string arg = argv[i]; + if (!process_flags) { + if (has_input_file) { + std::cerr << "Unexpected argument after the input file: " << arg << std::endl; + printUsage(); + return 1; + } + has_input_file = true; + input_file_path = arg; + continue; + } + if (arg == "--filter-object" || arg == "-O") { + if (i + 1 < argc) { + selected_groups.push_back(argv[++i]); + } + } else if (arg == "--filter-group" || arg == "-G") { + if (i + 1 < argc) { + selected_groups.push_back(argv[++i]); + } + } else if (arg == "--") { + process_flags = false; + } else if (arg.starts_with("-")) { + std::cerr << "Unknown option: " << arg << std::endl; + printUsage(); + return 1; + } else { + input_file_path = arg; + has_input_file = true; + } + } + if (!has_input_file) { + std::cerr << "No input file specified." << std::endl; + printUsage(); + return 1; + } + // if (!has_output_float_set_file) { + // std::cerr << "No output float set file specified." << std::endl; + // printUsage(); + // return 1; + // } + // if (!has_output_vertex_file) { + // std::cerr << "No output vertex file specified." << std::endl; + // printUsage(); + // return 1; + // } + // if (!has_output_mesh_file) { + // std::cerr << "No output mesh file specified." << std::endl; + // printUsage(); + // return 1; + // } + // if (!has_output_position_file) { + // std::cerr << "No output position file specified." << std::endl; + // printUsage(); + // return 1; + // } + WaveFront::Settings wavefront_settings( + selected_objects.empty() ? std::nullopt : std::make_optional(selected_objects), + selected_groups.empty() ? std::nullopt : std::make_optional(selected_groups) + ); + WaveFront wavefront_parser(wavefront_settings); + + try { + if (input_file_path == "-") { + wavefront_parser.parse(std::cin); + } else { + std::ifstream input_file(input_file_path); + if (!input_file) { + std::cerr << "Failed to open input file: " << input_file_path << std::endl; + return 1; + } + wavefront_parser.parse(input_file); + } + } catch (const WaveFront::parse_error& e) { + std::cerr << input_file_path << ":" << e.line_number() << ": " << e.what() << std::endl; + return 1; + } + + Output output; + + output.compile(wavefront_parser); + + return 0; +} diff --git a/src/numset.hpp b/src/numset.hpp new file mode 100644 index 0000000..fe92750 --- /dev/null +++ b/src/numset.hpp @@ -0,0 +1,70 @@ +#ifndef APP_NUMSET_H +#define APP_NUMSET_H + +#include +#include +#include +#include + +template +struct float_is_equal { + bool operator()(T a, T b) const { + static const auto epsilon = std::numeric_limits::epsilon(); + static const auto min_value = std::numeric_limits::min(); + static const auto max_value = std::numeric_limits::max(); + // Nothing is equal to NaN (not even NaN) + if (std::isnan(a) || std::isnan(b)) { + return false; + } + // If both are exactly equal, they are equal, no further checks. + if (a == b) { + return true; + } + auto difference = std::abs(a - b); + auto finite_amplitude = std::min(std::abs(a) + std::abs(b), max_value); + auto min_difference = std::max(min_value, finite_amplitude * epsilon); + return difference < min_difference; + } +}; + +template +struct float_compare_less { + bool operator()(T a, T b) const { + static const auto is_equal_function = float_is_equal(); + if (!is_equal_function(a, b)) { + return a < b; + } + return false; + } +}; + +template +struct less_with_nan_first_and_nearly_equal { + bool operator()(T a, T b) const { + static const auto epsilon = std::numeric_limits::epsilon(); + static const auto min_value = std::numeric_limits::min(); + static const auto max_value = std::numeric_limits::max(); + + if (std::isnan(a)) { + return !std::isnan(b); + } + if (std::isnan(b)) { + return false; + } + if (a == b) { + return false; + } + auto difference = std::abs(a - b); + auto finite_amplitude = std::min(std::abs(a) + std::abs(b), max_value); + auto min_difference = std::max(min_value, finite_amplitude * epsilon); + if (difference < min_difference) { + return false; + } + return a < b; + } +}; + +template +using numset_t = std::set>; + +#endif // APP_NUMSET_H diff --git a/src/output.cpp b/src/output.cpp new file mode 100644 index 0000000..6f2688d --- /dev/null +++ b/src/output.cpp @@ -0,0 +1,93 @@ +#include +#include +#include +#include +#include +#include "output.hpp" +#include "helper.hpp" +#include "numset.hpp" + +Output::Output() { + float_data_.push_back(std::numeric_limits::quiet_NaN()); + vector_2d_data_.push_back(array_fill<2, std::uint32_t>(0)); + vector_3d_data_.push_back(array_fill<3, std::uint32_t>(0)); + triangle_data_.push_back(array_fill<3, uint32_t>(0)); +} + +void Output::compile(const WaveFront &input) { + compile_float_data(input); +} + +static const std::array attribute_name = { + "position", + "texture", + "normal" +}; + +template +void ensure_dims(std::size_t num_dimensions, std::uint32_t attribute_index) { + if (num_dimensions != 3) { + std::stringstream message; + message << "Unexpected unaligned " << attribute_name[attribute_index] << " size: expected exactly " << N << " dimensions, got " << num_dimensions; + throw std::runtime_error(message.str()); + } +} + +void Output::compile_float_data(const WaveFront &input) { + using get_data_reference_func_t = std::function> &()>; + static const std::array get_data_reference{{ + [&]() -> const std::vector> & { + return input.obj_v(); + }, + [&]() -> const std::vector> & { + return input.obj_vt(); + }, + [&]() -> const std::vector> & { + return input.obj_vn(); + }, + }}; + std::set> float_set; + std::size_t texture_size = 0; + + using check_attribute_dimensions_func_t = std::function; + std::array check_attribute_dimensions{{ + ensure_dims<3>, + [&](std::uint32_t num_dimensions, std::uint32_t attribute_index) { + texture_size = num_dimensions; + check_attribute_dimensions[1] = [&](std::uint32_t num_dimensions, std::uint32_t attribute_index) { + if (texture_size != num_dimensions) { + std::stringstream message; + message << "Unexpected unaligned texture size: expected exactly " << texture_size << " dimensions, got " << num_dimensions; + throw std::runtime_error(message.str()); + } + }; + }, + ensure_dims<3>, + }}; + for (const auto &triangle : input.obj_f()) { + for (const auto &vertex : triangle) { + for (std::uint32_t attribute_index = 0; attribute_index < 3; ++attribute_index) { + const auto &attribute_reference = vertex[attribute_index]; + if (attribute_reference == 0) { + continue; + } + const auto &container = get_data_reference[attribute_index](); + assert(attribute_reference < container.size()); + std::uint32_t dim_count = 0; + for (const auto &value : container.at(attribute_reference)) { + if (std::isnan(value)) { + break; + } + ++dim_count; + float_set.insert(value); + } + assert(dim_count > 0); + check_attribute_dimensions[attribute_index](dim_count, attribute_index); + } + } + } + + float_data_.insert(float_data_.end(), float_set.begin(), float_set.end()); + + int j = 0; +} diff --git a/src/output.hpp b/src/output.hpp new file mode 100644 index 0000000..5091807 --- /dev/null +++ b/src/output.hpp @@ -0,0 +1,32 @@ +#ifndef APP_MODEL_HPP +#define APP_MODEL_HPP + +#include +#include +#include + +#include "wavefront.hpp" + +class Output { + std::vector float_data_; + std::vector> vector_2d_data_; + std::vector> vector_3d_data_; + std::variant< + uint32_t, + std::array, + std::array + > vertex_data_; + std::vector> triangle_data_; + +public: + Output(); + ~Output() = default; + +private: + void compile_float_data(const WaveFront &input); + +public: + void compile(const WaveFront &input); +}; + +#endif // APP_MODEL_HPP diff --git a/src/wavefront.cpp b/src/wavefront.cpp new file mode 100644 index 0000000..275dc09 --- /dev/null +++ b/src/wavefront.cpp @@ -0,0 +1,278 @@ +#include "wavefront.hpp" +#include "helper.hpp" + +#include +#include + +WaveFront::Settings::Settings( + std::optional> selected_objects, + std::optional> selected_groups +) : +selected_objects_(selected_objects), +selected_groups_(selected_groups) +{ +} + +WaveFront::parse_error::parse_error(std::size_t line_number, const std::string& message) : +std::runtime_error(message), +line_number_(line_number) +{ +} + +std::size_t WaveFront::parse_error::line_number() const noexcept { + return line_number_; +} + +const std::map WaveFront::_parse_line_{ + { "v", &WaveFront::parse_v }, + { "vt", &WaveFront::parse_vt }, + { "vn", &WaveFront::parse_vn }, + { "f", &WaveFront::parse_f }, + { "o", &WaveFront::parse_o }, + { "g", &WaveFront::parse_g }, +}; + +WaveFront::WaveFront( + const Settings& settings +) : +settings_(settings), +num_attributes_(0), +num_texture_coordinates_(0), +current_line_(0) +{ + obj_v_.push_back(array_fill<3>(std::numeric_limits::quiet_NaN())); + obj_vt_.push_back(array_fill<3>(std::numeric_limits::quiet_NaN())); + obj_vn_.push_back(array_fill<3>(std::numeric_limits::quiet_NaN())); + obj_f_.push_back(array_fill<3>(array_fill<3>(uint32_t(0)))); +} + +void WaveFront::parse(std::istream &input_stream) { + std::string line; + + while(std::getline(input_stream, line)) { + ++current_line_; + + if (line.empty() || line[0] == '#') { + continue; // Skip empty lines and comments + } + + std::istringstream line_parser(line); + std::string token; + + std::getline(line_parser, token, ' '); + if (token.empty()) { + continue; + } + + auto token_parser_iterator = _parse_line_.find(token); + if (token_parser_iterator != _parse_line_.end()) { + (this->*(token_parser_iterator->second))(line_parser); + } + } +} + +void WaveFront::parse_o(std::istringstream &line_parser) { + std::string object_name; + std::getline(line_parser, object_name); + if (object_name.empty()) { + current_object_.reset(); + } else { + current_object_ = object_name; + } +} + +void WaveFront::parse_g(std::istringstream &line_parser) { + std::string group_name; + std::getline(line_parser, group_name); + if (group_name.empty()) { + current_group_.reset(); + } else { + current_group_ = group_name; + } +} + +void WaveFront::parse_v(std::istringstream &line_parser) { + std::array data = array_fill<3>(std::numeric_limits::quiet_NaN()); + std::string element; + std::size_t element_count = 0; + + while (std::getline(line_parser, element, ' ')) { + if (element_count >= 3) { + throw parse_error(current_line_, "Type \"v\" with more than 3 components is not supported."); + } + if (element.empty()) { + break; + } + try { + data[element_count++] = std::stof(element); + } catch (const std::invalid_argument&) { + throw parse_error(current_line_, "Invalid float value in vertex definition."); + } catch (const std::out_of_range&) { + throw parse_error(current_line_, "Float value out of range in vertex definition."); + } + } + if (element_count < 3) { + throw parse_error(current_line_, "Type \"v\" must have exactly 3 components."); + } + obj_v_.push_back(data); +} + +void WaveFront::parse_vn(std::istringstream &line_parser) { + std::array data = array_fill<3>(std::numeric_limits::quiet_NaN()); + std::string element; + std::size_t element_count = 0; + + while (std::getline(line_parser, element, ' ')) { + if (element_count >= 3) { + throw parse_error(current_line_, "Type \"vn\" with more than 3 components is not supported."); + } + if (element.empty()) { + break; + } + try { + data[element_count++] = std::stof(element); + } catch (const std::invalid_argument&) { + throw parse_error(current_line_, "Invalid float value in vertex normal definition."); + } catch (const std::out_of_range&) { + throw parse_error(current_line_, "Float value out of range in vertex normal definition."); + } + } + if (element_count < 3) { + throw parse_error(current_line_, "Type \"vn\" with have exactly 3 components."); + } + obj_vn_.push_back(data); +} + +void WaveFront::parse_vt(std::istringstream &line_parser) { + std::array data = array_fill<3>(std::numeric_limits::quiet_NaN()); + std::string element; + std::size_t element_count = 0; + + while (std::getline(line_parser, element, ' ')) { + if (element_count >= 3) { + throw parse_error(current_line_, "Type \"vt\" with more than 3 components is not supported."); + }; + if (element.empty()) { + break; + } + try { + data[element_count++] = std::stof(element); + } catch (const std::invalid_argument&) { + throw parse_error(current_line_, "Invalid float value in texture coordinate definition."); + } catch (const std::out_of_range&) { + throw parse_error(current_line_, "Float value out of range in texture coordinate definition."); + } + } + + if (element_count < 1) { + throw parse_error(current_line_, "Type \"vt\" must have at least 1 component."); + } + + obj_vt_.push_back(data); +} + +void WaveFront::parse_f(std::istringstream &line_parser) { + if (settings_.selected_objects_.has_value()) { + if (!current_object_.has_value()) { + return; + } + auto &selected_objects = settings_.selected_objects_.value(); + if (std::find(selected_objects.begin(), selected_objects.end(), current_object_.value()) == selected_objects.end()) { + return; + } + } + if (settings_.selected_groups_) { + if (!current_group_.has_value()) { + return; + } + auto selected_begin = settings_.selected_groups_->begin(); + auto selected_end = settings_.selected_groups_->end(); + auto current_value = current_group_.value(); + if (std::find(selected_begin, selected_end, current_value) == selected_end) { + return; + } + } + + std::size_t vertex_count = 0; + std::array, 3> triangle; + std::string token; + + while (std::getline(line_parser, token, ' ')) { + if (token.empty()) { + break; + } + auto vertex_indices = parse_vertex(token); + if (vertex_count < 3) { + triangle[vertex_count] = vertex_indices; + } else { + obj_f_.push_back(triangle); + triangle[0] = triangle[1]; + triangle[1] = triangle[2]; + triangle[2] = vertex_indices; + } + ++vertex_count; + } +} + +std::array WaveFront::parse_vertex(std::string &token) { + std::istringstream token_parser(token); + std::array vertex_result{0, 0, 0}; + std::string vertex_token; + std::size_t attribute_count = 0; + + static const std::array>, 3> data_reference = {obj_v_, obj_vt_, obj_vn_}; + + while (std::getline(token_parser, vertex_token, '/')) { + if (attribute_count >= 3) { + throw parse_error(current_line_, "Face vertex with more than 3 components is not supported."); + } + int64_t attribute_value; + if (vertex_token.empty()) { + attribute_value = 0; + } else { + try { + attribute_value = std::stoll(vertex_token); + } catch (const std::invalid_argument&) { + std::stringstream message; + message << "Invalid face vertex index: " << vertex_token; + throw parse_error(current_line_, message.str()); + } catch (const std::out_of_range&) { + std::stringstream message; + message << "Face vertex index out of range: " << vertex_token; + throw parse_error(current_line_, message.str()); + } + } + + if (attribute_value < 0) { + attribute_value = data_reference[attribute_count].size() + attribute_value; + if (attribute_value < 1 || attribute_value > data_reference[attribute_count].size()) { + std::stringstream message; + message << "Face vertex index out of range: " << vertex_token; + throw parse_error(current_line_, message.str()); + } + } else if (attribute_value >= static_cast(std::numeric_limits::max())) { + std::stringstream message; + message << "Face vertex index out of range: " << vertex_token; + throw parse_error(current_line_, message.str()); + } + vertex_result[attribute_count] = attribute_value; + ++attribute_count; + } + return vertex_result; +} + +const std::vector> &WaveFront::obj_v() const noexcept { + return obj_v_; +} + +const std::vector> &WaveFront::obj_vt() const noexcept { + return obj_vt_; +} + +const std::vector> &WaveFront::obj_vn() const noexcept { + return obj_vn_; +} + +const std::vector, 3>> &WaveFront::obj_f() const noexcept { + return obj_f_; +} diff --git a/src/wavefront.hpp b/src/wavefront.hpp new file mode 100644 index 0000000..a899001 --- /dev/null +++ b/src/wavefront.hpp @@ -0,0 +1,80 @@ +#ifndef APP_WAVEFRONT_HPP +#define APP_WAVEFRONT_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +class WaveFront; + +class WaveFront { +public: + enum class AttributeIndex : uint32_t { + POSITION = 0, + TEXTURE = 1, + NORMAL = 2 + }; +public: + class Settings { + friend class WaveFront; + private: + std::optional> selected_objects_; + std::optional> selected_groups_; + + public: + Settings() = default; + Settings( + std::optional> selected_objects, + std::optional> selected_groups + ); + Settings(const Settings&) = default; + Settings(Settings&&) = default; + ~Settings() = default; + }; + + class parse_error : public std::runtime_error { + public: + parse_error(std::size_t line_number, const std::string& message); + std::size_t line_number() const noexcept; + + private: + std::size_t line_number_; + }; +private: + using _parse_line_t = void (WaveFront::*)(std::istringstream&); + Settings settings_; + std::vector> obj_v_, obj_vt_, obj_vn_; + std::vector, 3>> obj_f_; + std::size_t num_attributes_, num_texture_coordinates_, current_line_; + std::optional current_object_, current_group_; + + static const std::map _parse_line_; + +private: + void parse_o(std::istringstream &line_parser); + void parse_g(std::istringstream &line_parser); + void parse_v(std::istringstream &line_parser); + void parse_vn(std::istringstream &line_parser); + void parse_vt(std::istringstream &line_parser); + void parse_f(std::istringstream &line_parser); + std::array parse_vertex(std::string &line_parser); + +public: + const std::vector> &obj_v() const noexcept; + const std::vector> &obj_vt() const noexcept; + const std::vector> &obj_vn() const noexcept; + const std::vector, 3>> &obj_f() const noexcept; + +public: + explicit WaveFront(const Settings &settings = Settings(std::nullopt, std::nullopt)); + ~WaveFront() = default; + + void parse(std::istream &input_string); +}; + +#endif // APP_WAVEFRONT_HPP