diff --git a/src/ifcconvert/IfcConvert.cpp b/src/ifcconvert/IfcConvert.cpp
index fac54d6025..c95a838584 100644
--- a/src/ifcconvert/IfcConvert.cpp
+++ b/src/ifcconvert/IfcConvert.cpp
@@ -39,6 +39,7 @@
#include "../serializers/XmlSerializer.h"
#include "../serializers/SvgSerializer.h"
#include "../serializers/USDSerializer.h"
+#include "../serializers/TtlWktSerializer.h"
#include "../ifcgeom/IfcGeomFilter.h"
#include "../ifcgeom/Iterator.h"
@@ -132,6 +133,7 @@ void print_usage(bool suggest_help = true)
#ifdef IFOPSH_WITH_CGAL
<< " .cityjson City JSON format for geospatial data\n"
#endif
+ << " .ttl TTL/WKT RDF Turtle with Well-Known-Text geometry\n"
<< " .ifc IFC-SPF Industry Foundation Classes\n"
<< "\n"
<< "If no output filename given, " << IfcUtil::path::from_utf8(DEFAULT_EXTENSION) << " will be used as the output file.\n";
@@ -647,7 +649,8 @@ int main(int argc, char** argv) {
IFC = IfcUtil::path::from_utf8(".ifc"),
USD = IfcUtil::path::from_utf8(".usd"),
USDA = IfcUtil::path::from_utf8(".usda"),
- USDC = IfcUtil::path::from_utf8(".usdc");
+ USDC = IfcUtil::path::from_utf8(".usdc"),
+ TTL = IfcUtil::path::from_utf8(".ttl");
// @todo clean up serializer selection
// @todo detect program options that conflict with the chosen serializer
@@ -826,6 +829,10 @@ int main(int argc, char** argv) {
geometry_settings.get().value = true;
}
+ if (output_extension == TTL) {
+ geometry_settings.get().value = ifcopenshell::geometry::settings::TriangulationMethod::POLYHEDRON_WITH_HOLES;
+ }
+
boost::shared_ptr serializer; /**< @todo use std::unique_ptr when possible */
if (output_extension == OBJ) {
// Do not use temp file for MTL as it's such a small file.
@@ -861,6 +868,8 @@ int main(int argc, char** argv) {
serializer = boost::make_shared(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings);
#endif
#endif
+ } else if (output_extension == TTL) {
+ serializer = boost::make_shared(IfcUtil::path::to_utf8(output_temp_filename), geometry_settings, serializer_settings);
} else {
cerr_ << "[Error] Unknown output filename extension '" << output_extension << "'\n";
write_log(!quiet);
diff --git a/src/ifcopenshell-python/ifcopenshell/geom/main.py b/src/ifcopenshell-python/ifcopenshell/geom/main.py
index 1773275bea..5beaa10342 100644
--- a/src/ifcopenshell-python/ifcopenshell/geom/main.py
+++ b/src/ifcopenshell-python/ifcopenshell/geom/main.py
@@ -570,3 +570,13 @@ class serializers:
hdf5 = ifcopenshell_wrapper.HdfSerializer
except:
pass
+
+ # ttl is always available since it doesn't depend on any C++ libraries,
+ # just people might be using an outdated binary
+ if hasattr(ifcopenshell_wrapper, 'TtlWktSerializer'):
+ @staticmethod
+ def ttl(
+ out_filename: Union[str, serializers.buffer], geometry_settings: settings, settings: serializer_settings
+ ) -> ifcopenshell_wrapper.SvgSerializer:
+ out_filename = transform_string(out_filename)
+ return ifcopenshell_wrapper.TtlWktSerializer(out_filename, geometry_settings, settings)
diff --git a/src/ifcwrap/IfcGeomWrapper.i b/src/ifcwrap/IfcGeomWrapper.i
index 9b1ab46395..002fd03aa0 100644
--- a/src/ifcwrap/IfcGeomWrapper.i
+++ b/src/ifcwrap/IfcGeomWrapper.i
@@ -253,6 +253,7 @@ namespace {
%include "../serializers/WavefrontObjSerializer.h"
%include "../serializers/XmlSerializer.h"
%include "../serializers/GltfSerializer.h"
+%include "../serializers/TtlWktSerializer.h"
%extend ifcopenshell::geometry::taxonomy::style {
size_t instance_id() const {
diff --git a/src/ifcwrap/IfcPython.i b/src/ifcwrap/IfcPython.i
index 72fe60f21d..a12457f092 100644
--- a/src/ifcwrap/IfcPython.i
+++ b/src/ifcwrap/IfcPython.i
@@ -193,7 +193,8 @@ constexpr bool is_std_vector_vector_v = is_std_vector_vector::value;
#include "../serializers/HdfSerializer.h"
#include "../serializers/XmlSerializer.h"
#include "../serializers/GltfSerializer.h"
-
+ #include "../serializers/TtlWktSerializer.h"
+
#ifdef HAS_SCHEMA_2x3
#include "../ifcparse/Ifc2x3.h"
#endif
diff --git a/src/serializers/TtlWktSerializer.cpp b/src/serializers/TtlWktSerializer.cpp
new file mode 100644
index 0000000000..7fd8962c01
--- /dev/null
+++ b/src/serializers/TtlWktSerializer.cpp
@@ -0,0 +1,316 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#include "TtlWktSerializer.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace {
+ const char* const LINESTRING = "LINESTRING";
+ const char* const POLYGON = "POLYGON";
+
+ void emit_polyhedral_surface(
+ std::ostream& os,
+ const std::vector& vertices,
+ const std::vector>>& faces)
+ {
+ os << "POLYHEDRALSURFACE Z(";
+
+ for (size_t i = 0; i < faces.size(); ++i) {
+ const auto& face = faces[i];
+ os << "(";
+ for (size_t j = 0; j < face.size(); ++j) {
+ const auto& loop = face[j];
+ os << "(";
+ for (size_t k = 0; k < loop.size(); ++k) {
+ int index = loop[k];
+ for (size_t l = 0; l < 3; ++l) {
+ os << vertices[index * 3 + l];
+ if (l != 2) {
+ os << " ";
+ }
+ }
+ if (k < loop.size() - 1) {
+ os << ", ";
+ }
+ }
+ os << ")";
+ if (j < face.size() - 1) {
+ os << ", ";
+ }
+ }
+ os << ")";
+ if (i < faces.size() - 1) {
+ os << ",";
+ }
+ }
+ os << ")";
+ }
+
+ void emit_line_component(
+ std::ostream& os,
+ const std::vector& vertices,
+ const std::vector& component,
+ bool force_2d = false,
+ const char* const wkt_geometry_type=LINESTRING)
+ {
+ os << wkt_geometry_type << " ";
+ if (!force_2d) {
+ os << "Z";
+ }
+ os << "(";
+ if (wkt_geometry_type == POLYGON) {
+ os << "(";
+ }
+ for (size_t i = 0; i < component.size() + (wkt_geometry_type == POLYGON ? 1 : 0); ++i) {
+ if (i != 0) {
+ os << ", ";
+ }
+ for (size_t l = 0; l < (force_2d ? 2 : 3); ++l) {
+ os << vertices[component[i % component.size()] * 3 + l];
+ if (l != (force_2d ? 1 : 2)) {
+ os << " ";
+ }
+ }
+ }
+ os << ")";
+ if (wkt_geometry_type == POLYGON) {
+ os << ")";
+ }
+ }
+
+ void emit_line_strings(
+ std::ostream& os,
+ const std::vector& vertices,
+ const std::vector& lines,
+ bool force_2d = false)
+ {
+ std::unordered_map> adjacencyList;
+ std::unordered_set visited;
+
+ for (size_t i = 0; i < lines.size(); i += 2) {
+ for (size_t j = 0; j < 2; ++j) {
+ const auto& p0 = lines[i + (j ? 1 : 0)];
+ const auto& p1 = lines[i + (j ? 0 : 1)];
+ adjacencyList[p0].push_back(p1);
+ }
+ }
+
+ auto traverseComponent = [&](int start) {
+ std::vector component; // To store the current component
+ std::queue toVisit;
+ toVisit.push(start);
+ visited.insert(start);
+
+ while (!toVisit.empty()) {
+ int current = toVisit.front();
+ toVisit.pop();
+ component.push_back(current);
+
+ for (int neighbor : adjacencyList[current]) {
+ if (visited.find(neighbor) == visited.end()) {
+ toVisit.push(neighbor);
+ visited.insert(neighbor);
+ }
+ }
+ }
+
+ return component;
+ };
+
+ std::vector> components;
+
+ for (auto it = lines.begin(); it != lines.end(); it += 2) {
+ if (visited.find(*it) == visited.end()) {
+ components.emplace_back(std::move(traverseComponent(*it)));
+ }
+ }
+
+ if (components.size() == 1) {
+ emit_line_component(os, vertices, components.front(), force_2d);
+ } else {
+ os << "GEOMETRYCOLLECTION(";
+ for (auto it = components.begin(); it != components.end(); ++it) {
+ if (it != components.begin()) {
+ os << ",";
+ }
+ emit_line_component(os, vertices, *it, force_2d);
+ }
+ os << ")";
+ }
+ }
+
+ std::string escape_for_turtle(const std::u32string& input) {
+ std::wostringstream escaped;
+ escaped << "\"";
+
+ for (auto& c : input) {
+ switch (c) {
+ case '\\':
+ escaped << "\\\\";
+ break;
+ case '\"':
+ escaped << "\\\"";
+ break;
+ case '\n':
+ escaped << "\\n";
+ break;
+ case '\r':
+ escaped << "\\r";
+ break;
+ case '\t':
+ escaped << "\\t";
+ break;
+ default:
+ if (c < 0x20 || c > 0x7E) {
+ escaped << "\\u"
+ << std::hex << std::setw(4) << std::setfill(L'0')
+ << (c & 0xFFFF);
+ } else {
+ escaped.put(c);
+ }
+ break;
+ }
+ }
+ escaped << "\"";
+ return IfcUtil::convert_utf8(escaped.str());
+ }
+
+ template
+ std::string capture_output(Fn fn, Ts... ts) {
+ std::ostringstream oss;
+ fn(oss, ts...);
+ return oss.str();
+ }
+}
+
+TtlWktSerializer::TtlWktSerializer(const stream_or_filename& filename, const ifcopenshell::geometry::Settings& geometry_settings, const ifcopenshell::geometry::SerializerSettings& settings)
+ : WriteOnlyGeometrySerializer(geometry_settings, settings)
+ , filename_(filename)
+{
+ const auto& tri_setting = geometry_settings.get().get();
+ if (tri_setting != ifcopenshell::geometry::settings::POLYHEDRON_WITH_HOLES) {
+ throw std::runtime_error("The RDF Turtle WKT serializer needs POLYHEDRON_WITH_HOLES triangulation output");
+ }
+ filename_.stream << std::setprecision(settings.get().get());
+}
+
+bool TtlWktSerializer::ready()
+{
+ return filename_.is_ready();
+}
+
+void TtlWktSerializer::writeHeader()
+{
+ filename_.stream << "# File generated by IfcOpenShell " << IFCOPENSHELL_VERSION << "\n";
+ filename_.stream << "@prefix geo: .\n";
+ filename_.stream << "@prefix ex: .\n";
+ filename_.stream << "@prefix dcterms: .\n";
+ filename_.stream << "@prefix rdfs: .\n\n\n";
+}
+
+void TtlWktSerializer::write(const IfcGeom::TriangulationElement* o)
+{
+ auto ttl_object_id = [&]() {
+ return boost::replace_all_copy(object_id(o), "-", "_");
+ };
+
+ filename_.stream << "ex:" << ttl_object_id() << " a geo:Feature ;\n";
+ filename_.stream << " dcterms:identifier " << escape_for_turtle(
+ IfcUtil::convert_utf8_to_utf32(o->guid())) << " ;\n";
+ filename_.stream << " rdfs:label " << escape_for_turtle(
+ IfcUtil::convert_utf8_to_utf32(o->name())
+ ) << " ;\n";
+ filename_.stream << " geo:hasGeometry ex:" << ttl_object_id() << "_geometry .\n\n";
+
+ if (!o->geometry().polyhedral_faces_with_holes().empty()) {
+ filename_.stream << "ex:" << ttl_object_id() << "_geometry a geo:Geometry ;\n";
+ filename_.stream << " geo:asWKT " << escape_for_turtle(
+ IfcUtil::convert_utf8_to_utf32(
+ capture_output(
+ emit_polyhedral_surface,
+ o->geometry().verts(),
+ o->geometry().polyhedral_faces_with_holes()))
+ ) << "^^geo:wktLiteral .\n\n";
+
+ Eigen::Map> vertex_map(o->geometry().verts().data(), 3, o->geometry().verts().size() / 3);
+
+ boost::optional>::const_iterator> lowest_face;
+ double lowest_z = std::numeric_limits::infinity();
+
+ for (const auto& f : o->geometry().polyhedral_faces_with_holes()) {
+ auto v0 = vertex_map.transpose().row(f[0][0]);
+ auto v1 = vertex_map.transpose().row(f[0][1]);
+ auto v2 = vertex_map.transpose().row(f[0][2]);
+ auto v1_v0 = v1 - v0;
+ auto v2_v0 = v2 - v0;
+
+ Eigen::Vector3d cross_product = v1_v0.cross(v2_v0);
+ cross_product.normalize();
+ // @nb we take abs because so that we can ignore face orientation and potential convatities rquire
+ if ((std::abs(cross_product.z()) + 1.e-9) >= 1.0 && v0.z() < lowest_z) {
+ lowest_face = f.begin();
+ }
+ }
+
+ if (lowest_face) {
+ filename_.stream << "ex:" << ttl_object_id() << " geo:hasGeometry ex:" << ttl_object_id() << "_footprint_geometry .\n\n";
+ filename_.stream << "ex:" << ttl_object_id() << "_footprint_geometry a geo:Geometry ;\n";
+ filename_.stream << " geo:asWKT " << escape_for_turtle(
+ IfcUtil::convert_utf8_to_utf32(
+ capture_output(
+ // @nb this is line_component, because this is the linestring
+ // from a faceboundary, not the edges as pairs of indices.
+ emit_line_component,
+ o->geometry().verts(),
+ **lowest_face,
+ true,
+ POLYGON))
+ ) << "^^geo:wktLiteral .\n\n";
+ }
+ } else {
+ filename_.stream << "ex:" << ttl_object_id() << "_geometry a geo:Geometry ;\n";
+ bool force_2d = true;
+ double z_value;
+ for (size_t i = 2; i < o->geometry().verts().size(); i += 3) {
+ const auto& cur = o->geometry().verts()[i];
+ if (i == 2) {
+ z_value = cur;
+ } else {
+ if (z_value != cur) {
+ force_2d = false;
+ break;
+ }
+ }
+ }
+ filename_.stream << " geo:asWKT " << escape_for_turtle(
+ IfcUtil::convert_utf8_to_utf32(
+ capture_output(
+ emit_line_strings,
+ o->geometry().verts(),
+ o->geometry().edges(),
+ force_2d))
+ ) << "^^geo:wktLiteral .\n\n";
+ }
+}
diff --git a/src/serializers/TtlWktSerializer.h b/src/serializers/TtlWktSerializer.h
new file mode 100644
index 0000000000..5e064b47ed
--- /dev/null
+++ b/src/serializers/TtlWktSerializer.h
@@ -0,0 +1,47 @@
+/********************************************************************************
+ * *
+ * This file is part of IfcOpenShell. *
+ * *
+ * IfcOpenShell is free software: you can redistribute it and/or modify *
+ * it under the terms of the Lesser GNU General Public License as published by *
+ * the Free Software Foundation, either version 3.0 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * IfcOpenShell is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * Lesser GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the Lesser GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ********************************************************************************/
+
+#ifndef TTLWKTSERIALIZER_H
+#define TTLWKTSERIALIZER_H
+
+#include
+#include
+#include
+
+#include "../serializers/serializers_api.h"
+#include "../ifcgeom/GeometrySerializer.h"
+
+ // http://people.sc.fsu.edu/~jburkardt/txt/obj_format.txt
+class SERIALIZERS_API TtlWktSerializer : public WriteOnlyGeometrySerializer {
+private:
+ stream_or_filename filename_;
+public:
+ TtlWktSerializer(const stream_or_filename& filename, const ifcopenshell::geometry::Settings& geometry_settings, const ifcopenshell::geometry::SerializerSettings& settings);
+ virtual ~TtlWktSerializer() {}
+ bool ready();
+ void writeHeader();
+ void write(const IfcGeom::TriangulationElement* o);
+ void write(const IfcGeom::BRepElement* /*o*/) {}
+ void finalize() {}
+ bool isTesselated() const { return true; }
+ void setUnitNameAndMagnitude(const std::string& /*name*/, float /*magnitude*/) {}
+ void setFile(IfcParse::IfcFile*) {}
+};
+
+#endif