// Copyright (c) 2015-2020 Geometry Factory // All rights reserved. // // This file is part of CGAL (www.cgal.org) // // $URL$ // $Id$ // SPDX-License-Identifier: LGPL-3.0-or-later OR LicenseRef-Commercial // // Author(s) : Lutz Kettner // Andreas Fabri // Maxime Gimeno #ifndef CGAL_IO_OBJ_H #define CGAL_IO_OBJ_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace CGAL { //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// // Read namespace IO { namespace internal { template bool read_OBJ(std::istream& is, PointRange& points, PolygonRange& polygons, VertexNormalOutputIterator, VertexTextureOutputIterator, const bool verbose = false) { if(!is.good()) { if(verbose) std::cerr<<"File doesn't exist."<::type Point; set_ascii_mode(is); // obj is ASCII only int mini(1), maxi(-1); std::string s; Point p; std::string line; bool tex_found(false), norm_found(false); while(getline(is, line)) { // get last non-whitespace, non-null character auto last = std::find_if(line.rbegin(), line.rend(), [](char c) { return c != '\0' && !std::isspace(c); }); if(last == line.rend()) continue; // line is empty or only whitespace // keep reading lines as long as the last non-whitespace, non-null character is a backslash while(last != line.rend() && *last == '\\') { // remove everything from the backslash (included) line = line.substr(0, line.size() - (last - line.rbegin()) - 1); std::string next_line; if(!getline(is, next_line)) break; line += next_line; last = std::find_if(line.rbegin(), line.rend(), [](char c) { return c != '\0' && !std::isspace(c); }); } CGAL_assertion(!line.empty()); std::istringstream iss(line); if(!(iss >> s)) continue; if(s == "v") { if(!(iss >> p)) { if(verbose) std::cerr << "error while reading OBJ vertex." << std::endl; return false; } points.push_back(p); } else if(s == "vt") { tex_found = true; } else if(s == "vn") { norm_found = true; } else if(s == "f") { int i; polygons.emplace_back(); while(iss >> i) { if(i < 1) { const std::size_t n = polygons.back().size(); ::CGAL::internal::resize(polygons.back(), n + 1); polygons.back()[n] = static_cast(points.size()) + i; // negative indices are relative references if(i < mini) mini = i; } else { const std::size_t n = polygons.back().size(); ::CGAL::internal::resize(polygons.back(), n + 1); polygons.back()[n] = i - 1; if(i-1 > maxi) maxi = i-1; } // the format can be "f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3 ..." and we only read vertex ids for now, // so skip to the next vertex, but be tolerant about which whitespace is used if (!std::isspace(iss.peek())) { std::string ignoreme; iss >> ignoreme; } } if(iss.bad()) { if(verbose) std::cerr << "error while reading OBJ face." << std::endl; return false; } } else if(s.front() == '#') { // this is a commented line, ignored } else if(s == "vp" || // Display s == "bevel" || s == "lod" || s == "ctech" || s == "c_interp" || s == "usemap" || s == "usemtl" || s == "stech" || s == "d_interp" || s == "mtllib" || s == "shadow_obj" || s == "trace_obj" || // groups s == "o" || s == "g" || s == "s" || // Free s == "p" || s == "cstype" || s == "deg" || s == "step" || s == "bmat" || s == "con" || s == "curv" || s == "curv2" || s == "surf" || s == "parm" || s == "trim" || s == "hole" || s == "scrv" || s == "sp" || s == "end" || s == "con" || s == "surf_1" || s == "q0_1" || s == "q1_1" || s == "curv2d_1" || s == "surf_2" || s == "q0_2" || s == "q1_2" || s == "curv2d_2" || // superseded statements s == "bsp" || s == "bzp" || s == "cdc" || s == "cdp" || s == "res") { // valid, but unsupported } else { if(verbose) std::cerr << "Error: unrecognized line: " << s << std::endl; return false; } } if(norm_found && verbose) std::cout << "NOTE: normals were found in this file, but were discarded." << std::endl; if(tex_found && verbose) std::cout << "NOTE: textures were found in this file, but were discarded." << std::endl; if(points.empty() || polygons.empty()) { if(verbose) std::cerr << "warning: empty file?" << std::endl; return false; } if(maxi > static_cast(points.size()) || mini < -static_cast(points.size())) { if(verbose) std::cerr << "error: invalid face index" << std::endl; return false; } return !is.bad(); } } // namespace internal /// \ingroup PkgStreamSupportIoFuncsOBJ /// /// \brief reads the content of `is` into `points` and `polygons`, using the \ref IOStreamOBJ. /// /// \attention The polygon soup is not cleared, and the data from the stream are appended. /// /// \tparam PointRange a model of the concepts `RandomAccessContainer` and `BackInsertionSequence` /// whose value type is the point type /// \tparam PolygonRange a model of the concepts `SequenceContainer` and `BackInsertionSequence` /// whose `value_type` is itself a model of the concepts `SequenceContainer` /// and `BackInsertionSequence` whose `value_type` is an unsigned integer type /// convertible to `std::size_t` /// \tparam NamedParameters a sequence of \ref bgl_namedparameters "Named Parameters" /// /// \param is the input stream /// \param points points of the soup of polygons /// \param polygons a range of polygons. Each element in it describes a polygon /// using the indices of the points in `points`. /// \param np optional \ref bgl_namedparameters "Named Parameters" described below /// /// \cgalNamedParamsBegin /// \cgalParamNBegin{verbose} /// \cgalParamDescription{indicates whether output warnings and error messages should be printed or not.} /// \cgalParamType{Boolean} /// \cgalParamDefault{`false`} /// \cgalParamNEnd /// \cgalNamedParamsEnd /// /// \returns `true` if the reading was successful, `false` otherwise. template bool read_OBJ(std::istream& is, PointRange& points, PolygonRange& polygons, const CGAL_NP_CLASS& np = parameters::default_values() #ifndef DOXYGEN_RUNNING , std::enable_if_t::value>* = nullptr #endif ) { const bool verbose = parameters::choose_parameter(parameters::get_parameter(np, internal_np::verbose), false); return internal::read_OBJ(is, points, polygons, CGAL::Emptyset_iterator(), CGAL::Emptyset_iterator(), verbose); } /// \ingroup PkgStreamSupportIoFuncsOBJ /// /// \brief reads the content of the file `fname` into `points` and `polygons`, using the \ref IOStreamOBJ. /// /// \attention The polygon soup is not cleared, and the data from the file are appended. /// /// \tparam PointRange a model of the concept `RandomAccessContainer` whose value type is the point type. /// \tparam PolygonRange a model of the concepts `SequenceContainer` and `BackInsertionSequence` /// whose `value_type` is itself a model of the concept `SequenceContainer` /// and `BackInsertionSequence` whose `value_type` is an unsigned integer type /// convertible to `std::size_t` /// \tparam NamedParameters a sequence of \ref bgl_namedparameters "Named Parameters" /// /// \param fname the path to the input file /// \param points points of the soup of polygons /// \param polygons a range of polygons. Each element in it describes a polygon /// using the indices of the points in `points`. /// \param np optional \ref bgl_namedparameters "Named Parameters" described below /// /// \cgalNamedParamsBegin /// \cgalParamNBegin{verbose} /// \cgalParamDescription{indicates whether output warnings and error messages should be printed or not.} /// \cgalParamType{Boolean} /// \cgalParamDefault{`false`} /// \cgalParamNEnd /// \cgalNamedParamsEnd /// /// \returns `true` if the reading was successful, `false` otherwise. template bool read_OBJ(const std::string& fname, PointRange& points, PolygonRange& polygons, const CGAL_NP_CLASS& np = parameters::default_values() #ifndef DOXYGEN_RUNNING , std::enable_if_t::value>* = nullptr #endif ) { std::ifstream is(fname); CGAL::IO::set_mode(is, CGAL::IO::ASCII); return read_OBJ(is, points, polygons, np); } //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// // Write /*! * \ingroup PkgStreamSupportIoFuncsOBJ * * \brief writes the content of `points` and `polygons` in `os`, using the \ref IOStreamOBJ. * * \tparam PointRange a model of the concept `RandomAccessContainer` whose value type is the point type * \tparam PolygonRange a model of the concept `SequenceContainer` * whose `value_type` is itself a model of the concept `SequenceContainer` * whose `value_type` is an unsigned integer type convertible to `std::size_t` * \tparam NamedParameters a sequence of \ref bgl_namedparameters "Named Parameters" * * \param os the output stream * \param points points of the soup of polygons * \param polygons a range of polygons. Each element in it describes a polygon * using the indices of the points in `points`. * \param np optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below * * \cgalNamedParamsBegin * \cgalParamNBegin{point_map} * \cgalParamDescription{a property map associating points to the elements of the range `points`} * \cgalParamType{a model of `ReadablePropertyMap` whose key type is the value type * of the iterator of `PointRange` and value type is a model of the concept `Point_3`} * \cgalParamDefault{`CGAL::Identity_property_map::value_type>`} * \cgalParamNEnd * \cgalParamNBegin{stream_precision} * \cgalParamDescription{a parameter used to set the precision (i.e. how many digits are generated) of the output stream} * \cgalParamType{int} * \cgalParamDefault{the precision of the stream `os`} * \cgalParamNEnd * \cgalNamedParamsEnd * * \return `true` if the writing was successful, `false` otherwise. */ template bool write_OBJ(std::ostream& os, const PointRange& points, const PolygonRange& polygons, const CGAL_NP_CLASS& np = parameters::default_values() #ifndef DOXYGEN_RUNNING , std::enable_if_t::value>* = nullptr #endif ) { set_ascii_mode(os); // obj is ASCII only Generic_writer writer(os); return writer(points, polygons, np); } /*! * \ingroup PkgStreamSupportIoFuncsOBJ * * \brief writes the content of `points` and `polygons` in a file named `fname`, using the \ref IOStreamOBJ. * * \tparam PointRange a model of the concept `RandomAccessContainer` whose value type is the point type * \tparam PolygonRange a model of the concept `SequenceContainer` * whose `value_type` is itself a model of the concept `SequenceContainer` * whose `value_type` is an unsigned integer type convertible to `std::size_t` * \tparam NamedParameters a sequence of \ref bgl_namedparameters "Named Parameters" * * \param fname the path to the output file * \param points points of the soup of polygons * \param polygons a range of polygons. Each element in it describes a polygon * using the indices of the points in `points`. * \param np optional sequence of \ref bgl_namedparameters "Named Parameters" among the ones listed below * * \cgalNamedParamsBegin * \cgalParamNBegin{point_map} * \cgalParamDescription{a property map associating points to the elements of the range `points`} * \cgalParamType{a model of `ReadablePropertyMap` whose key type is the value type * of the iterator of `PointRange` and value type is a model of the concept `Kernel::Point_3`} * \cgalParamDefault{`CGAL::Identity_property_map::value_type>`} * \cgalParamNEnd * \cgalParamNBegin{stream_precision} * \cgalParamDescription{a parameter used to set the precision (i.e. how many digits are generated) of the output stream} * \cgalParamType{int} * \cgalParamDefault{`6`} * \cgalParamNEnd * \cgalNamedParamsEnd * * \return `true` if the writing was successful, `false` otherwise. */ template bool write_OBJ(const std::string& fname, const PointRange& points, const PolygonRange& polygons, const CGAL_NP_CLASS& np = parameters::default_values() #ifndef DOXYGEN_RUNNING , std::enable_if_t::value>* = nullptr #endif ) { std::ofstream os(fname); CGAL::IO::set_mode(os, CGAL::IO::ASCII); return write_OBJ(os, points, polygons, np); } } // namespace IO } // namespace CGAL #endif // CGAL_IO_OBJ_H