serializers: handle point/vertex-only representations

Addresses aothms's review comment on PR #8759: "I think serializers
will also need to be adapted. OBJ, Collada, etc."

Adds a proper "points" primitive to IfcGeom::Representation::
Triangulation (points_/points_item_ids_/points_material_ids_,
addPoint()), alongside the existing faces_/edges_, for vertices that
stand alone as a representation item (Vertex/Point/PointCloud, see
#134/#1409/#5218) rather than belonging to a face or edge.
points_material_ids_ is a separate array from the shared
faces+edges material_ids_, not folded into it: material_ids_ is a
single running sequence consumed in strict per-item emission order by
every writer below, and interleaving points into it would desync that
sequence whenever an item with points is triangulated before/after an
item with edges in the same file.

- WaveFrontOBJSerializer: emits "p" records for these points (the only
  way OBJ marks a vertex as a rendered primitive in its own right).
  Previously these vertices were written as bare "v" lines with no
  primitive referencing them at all.
- GltfSerializer: adds a POINTS (mode 0) primitive branch. Also fixes
  a latent bug: write() unconditionally returned early when
  material_ids() was empty, which is exactly the case for point-only
  geometry, silently dropping the whole node/mesh; and the later
  material-id iteration would have incremented an already-at-end
  iterator (UB) had that early return not caught it first.
- ColladaSerializer: COLLADA's schema has no points primitive (only
  lines/polygons/triangles), so there is nothing to write beyond the
  raw position source; logs a warning instead of silently producing an
  empty-but-successful mesh.
- IfcGeomWrapper.i: exposes points()/points_item_ids()/
  points_material_ids() (plus *_buffer variants) symmetrically with
  the existing edges_item_ids() etc.

Ported onto a clean v0.8.0 base (the original prototype was built on
top of Dion Moult's experimental ifcviewer-wgpu branch, PR #8759).
Fixes two build-time mismatches against that base's API: the logger
header is ifcparse/IfcLogger.h here, and Logger only exposes a
capitalized Warning(code_prefix, code_number, message, instance)
method, not a lowercase warning(); and material name sanitization is
IfcUtil::sanitate_material_name(), matching every other call site in
this file, not ifcopenshell::sanitate_material_name().

This contribution was produced with the assistance of an AI coding tool.
This commit is contained in:
Petru Conduraru
2026-07-21 23:07:31 +03:00
parent 99c514828d
commit c8f29bb843
5 changed files with 93 additions and 8 deletions
+17
View File
@@ -112,12 +112,20 @@ namespace IfcGeom {
std::vector<std::vector<std::vector<int>>> polyhedral_faces_with_holes_;
std::vector<int> edges_;
// Vertices that stand alone as a representation item in their own
// right (Vertex/Point/PointCloud, see #134/#1409/#5218), each
// entry an index into verts_, analogous to edges_/faces_.
std::vector<int> points_;
std::vector<double> normals_;
std::vector<double> uvs_;
std::vector<int> material_ids_;
std::vector<ifcopenshell::geometry::taxonomy::style::ptr> materials_;
std::vector<int> item_ids_;
std::vector<int> edges_item_ids_;
std::vector<int> points_item_ids_;
// Own array rather than material_ids_, to avoid desyncing the
// shared faces/edges running sequence when items are interleaved.
std::vector<int> points_material_ids_;
size_t weld_offset_;
VertexKeyMap welds;
@@ -132,6 +140,7 @@ namespace IfcGeom {
const std::vector<std::vector<int>>& polyhedral_faces_without_holes() const { return polyhedral_faces_without_holes_; }
const std::vector<std::vector<std::vector<int>>>& polyhedral_faces_with_holes() const { return polyhedral_faces_with_holes_; }
const std::vector<int>& edges() const { return edges_; }
const std::vector<int>& points() const { return points_; }
const std::vector<double>& normals() const { return normals_; }
const std::vector<double>& uvs() const { return uvs_; }
std::vector<double>& uvs_ref() { return uvs_; }
@@ -139,6 +148,8 @@ namespace IfcGeom {
const std::vector<ifcopenshell::geometry::taxonomy::style::ptr>& materials() const { return materials_; }
const std::vector<int>& item_ids() const { return item_ids_; }
const std::vector<int>& edges_item_ids() const { return edges_item_ids_; }
const std::vector<int>& points_item_ids() const { return points_item_ids_; }
const std::vector<int>& points_material_ids() const { return points_material_ids_; }
Triangulation(const BRep& shape_model);
@@ -222,6 +233,12 @@ namespace IfcGeom {
edges_item_ids_.push_back(item_id);
}
void addPoint(int item_id, int style, int vertex_index) {
points_.push_back(vertex_index);
points_item_ids_.push_back(item_id);
points_material_ids_.push_back(style);
}
void registerEdgeCount(int n1, int n2, std::map<std::pair<int, int>, int>& edgecount);
void resetWelds() {
+18
View File
@@ -670,10 +670,18 @@ struct ShapeRTTI : public boost::static_visitor<PyObject*>
return vector_to_buffer(self->edges());
}
std::pair<const char*, size_t> points_buffer() const {
return vector_to_buffer(self->points());
}
std::pair<const char*, size_t> material_ids_buffer() const {
return vector_to_buffer(self->material_ids());
}
std::pair<const char*, size_t> points_material_ids_buffer() const {
return vector_to_buffer(self->points_material_ids());
}
std::pair<const char*, size_t> item_ids_buffer() const {
return vector_to_buffer(self->item_ids());
}
@@ -682,6 +690,10 @@ struct ShapeRTTI : public boost::static_visitor<PyObject*>
return vector_to_buffer(self->edges_item_ids());
}
std::pair<const char*, size_t> points_item_ids_buffer() const {
return vector_to_buffer(self->points_item_ids());
}
std::pair<const char*, size_t> verts_buffer() const {
return vector_to_buffer(self->verts());
}
@@ -728,6 +740,7 @@ struct ShapeRTTI : public boost::static_visitor<PyObject*>
return self.polyhedral_faces_with_holes
faces = property(get_faces)
edges = property(edges)
points = property(points)
material_ids = property(material_ids)
materials = property(materials)
verts = property(verts)
@@ -735,12 +748,17 @@ struct ShapeRTTI : public boost::static_visitor<PyObject*>
item_ids = property(item_ids)
uvs = property(uvs)
edges_item_ids = property(edges_item_ids)
points_item_ids = property(points_item_ids)
points_material_ids = property(points_material_ids)
faces_buffer = property(faces_buffer)
edges_buffer = property(edges_buffer)
points_buffer = property(points_buffer)
material_ids_buffer = property(material_ids_buffer)
item_ids_buffer = property(item_ids_buffer)
edges_item_ids_buffer = property(edges_item_ids_buffer)
points_item_ids_buffer = property(points_item_ids_buffer)
points_material_ids_buffer = property(points_material_ids_buffer)
verts_buffer = property(verts_buffer)
normals_buffer = property(normals_buffer)
colors_buffer = property(colors_buffer)
+10 -1
View File
@@ -35,6 +35,7 @@
#include <cmath>
#include "../ifcparse/utils.h"
#include "../ifcparse/IfcLogger.h"
static std::string& collada_id(std::string& s)
{
@@ -70,7 +71,15 @@ void ColladaSerializer::ColladaExporter::ColladaGeometries::write(
const std::vector<double>& uvs, const std::vector<std::string>& material_references)
{
openMesh(mesh_id);
if (faces.empty() && edges.empty() && !positions.empty()) {
// Vertex/Point/PointCloud representation (#134/#1409/#5218). COLLADA
// has no native point primitive, only lines/polygons/triangles, so
// there is nothing meaningful to write beyond the raw position
// source below: the geometry will not be visible in the scene.
serializer->logger().Warning("GEO", 410, "Point/vertex-only geometry (" + mesh_id + ") has no COLLADA representation and will not be visible");
}
// The normals vector can be empty for example when the WELD_VERTICES setting is used.
// IfcOpenShell does not provide them with multiple face normals collapsed into a single vertex.
const bool has_normals = !normals.empty();
+25 -7
View File
@@ -182,7 +182,9 @@ size_t write_accessor(json& j, std::ofstream& ofs, It begin, It end, int bufferV
}
void GltfSerializer::write(const IfcGeom::TriangulationElement* o) {
if (o->geometry().material_ids().empty()) {
// material_ids() covers faces/edges only; points (#134/#1409/#5218) have
// their own points_material_ids().
if (o->geometry().material_ids().empty() && o->geometry().points_material_ids().empty()) {
return;
}
@@ -297,21 +299,37 @@ void GltfSerializer::write(const IfcGeom::TriangulationElement* o) {
auto it = meshes_.find(o->geometry().id());
if (it == meshes_.end()) {
auto mid1 = o->geometry().material_ids().begin();
auto mid0 = mid1;
std::vector<int>::const_iterator fid0;
int stride;
int primitive_type;
// Points have their own material id array, since material_ids() is a
// single running sequence shared between faces and edges only.
const std::vector<int>& material_ids = !o->geometry().faces().empty() || !o->geometry().edges().empty()
? o->geometry().material_ids()
: o->geometry().points_material_ids();
auto mid1 = material_ids.begin();
auto mid0 = mid1;
if (mid0 == material_ids.end()) {
// No faces, edges or points at all, nothing to write.
return;
}
if (!o->geometry().faces().empty()) {
stride = 3;
fid0 = o->geometry().faces().begin();
primitive_type = PRIM_TRIANGLES;
} else {
} else if (!o->geometry().edges().empty()) {
stride = 2;
fid0 = o->geometry().edges().begin();
primitive_type = PRIM_LINES;
} else {
// Vertex/Point/PointCloud representation with no owning face or
// edge, see #134/#1409/#5218.
stride = 1;
fid0 = o->geometry().points().begin();
primitive_type = PRIM_POINTS;
}
json mesh;
@@ -327,7 +345,7 @@ void GltfSerializer::write(const IfcGeom::TriangulationElement* o) {
// material.
mid1++;
if ((mid1 == o->geometry().material_ids().end()) || (*mid1 != *mid0)) {
if ((mid1 == material_ids.end()) || (*mid1 != *mid0)) {
auto n = std::distance(mid0, mid1);
auto fid1 = fid0 + n * stride;
@@ -362,7 +380,7 @@ void GltfSerializer::write(const IfcGeom::TriangulationElement* o) {
mesh["primitives"].push_back(primitive);
if (mid1 == o->geometry().material_ids().end()) {
if (mid1 == material_ids.end()) {
break;
}
@@ -194,6 +194,29 @@ void WaveFrontOBJSerializer::write(const IfcGeom::TriangulationElement* o)
obj_stream.stream << "l " << v1 << " " << v2 << "\n";
}
// Standalone points (Vertex/Point/PointCloud representations, no owning
// face or edge), see #134/#1409/#5218. OBJ's "p" element is the only way
// to mark a vertex as a rendered primitive in its own right. Points have
// their own material id array (not material_ids_/material_it above).
auto point_material_it = mesh.points_material_ids().begin();
for (int point_index : mesh.points()) {
const int material_id = *(point_material_it++);
if (material_id != previous_material_id) {
const ifcopenshell::geometry::taxonomy::style::ptr material = mesh.materials()[material_id];
std::string material_name = material->name;
IfcUtil::sanitate_material_name(material_name);
obj_stream.stream << "usemtl " << material_name << "\n";
if (materials.find(material_name) == materials.end()) {
writeMaterial(material);
materials.insert(material_name);
}
previous_material_id = material_id;
}
obj_stream.stream << "p " << (point_index + vcount_total) << "\n";
}
vcount_total += vcount;
ncount_total += ncount;
}