ifcgeom: prototype OpenCascade kernel support for point/vertex representations

Exploratory proof of concept for #134, #1409 and #5218: IfcVertexPoint,
IfcCartesianPoint and IfcCartesianPointList3D used as top-level
representation items ("Vertex"/"Point"/"PointCloud") currently raise
"Failed to process shape" because AbstractKernel::convert_impl for
taxonomy::point3 is never implemented and point3 cannot appear as a
child of the generic items collection.

This makes point3/direction3 derive from geom_item instead of plain
item, so a lone point3 can stand in as a representation item, and adds:

- OpenCascadeKernel::convert_impl(point3): a single point becomes a
  TopoDS_Vertex wrapped in a TopoDS_Compound, as aothms suggested in
  #5218.
- OpenCascadeKernel::convert_impl(collection): a bulk fast path for a
  collection made up entirely of point3 children (e.g. a whole
  IfcCartesianPointList3D) that builds ONE compound with all vertices
  in a single pass, instead of paying the generic per-item conversion
  overhead (cache lookup, heap allocation, a separate Triangulate()
  call) once per point.
- mapping for IfcVertexPoint (as a top-level item) and
  IfcCartesianPointList3D.
- loose-vertex emission in OpenCascadeShape::Triangulate, since a
  vertex-only shape previously triangulated to nothing.

This directly tests aothms's "the overhead is enormous" concern from
#5218. Benchmarked on this machine (Apple M-series, Release build):

- Normal (non-point) geometry is unaffected: a 5178-shape real model
  processes in 4.48s before this change and 4.49s after (~0.3%, noise).
- The bulk fast path scales linearly and cheaply: ~0.5-0.9 us/point for
  an IfcCartesianPointList3D from 1k to 100k points (100k points in
  ~93ms total).
- With the fast path disabled (pure per-item conversion, i.e. the naive
  reading of "a TopoDS_Compound of TopoDS_Vertex" with no batching),
  scaling is still linear, not quadratic, but ~4-7x slower per point
  (~3.5-4 us/point at the same scale, 100k points in ~380ms).

So aothms's concern is real as a constant-factor tax from going through
full OCCT BRep objects (TopoDS_Vertex/Compound, shared_ptr taxonomy
nodes, per-item caching) rather than flat coordinate arrays, but it is
not the asymptotic blowup "enormous overhead" might suggest, and a
reasonably-scoped batching fast path narrows the gap substantially.
Given Bonsai already has a working, accepted Python-side bypass for
this (create_point_cloud_mesh / create_structural_point_connection_mesh
in bonsai/tool/loader.py and geometry.py), this is offered as a
proof of concept for evaluation, not a claim that it should override
the prior "something for 0.9" call.

IfcCartesianPointList2D ("PointCloud" in 2D) is intentionally out of
scope for this prototype.

Adds pytest coverage (no Catch2/C++ test harness exists in this
codebase) for all three representation types plus a 1000-point
round-trip/timing sanity check.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Petru Conduraru
2026-07-19 12:49:53 +03:00
parent e52e5e2e58
commit 8a5b1ab10d
8 changed files with 280 additions and 1 deletions
@@ -343,6 +343,14 @@ void ifcopenshell::geometry::OpenCascadeShape::Triangulate(ifcopenshell::geometr
previous = current;
}
}
// Emit vertices with no owning edge (point.cpp's Vertex/Point/PointCloud
// compounds), see #134 / #1409 / #5218.
for (TopExp_Explorer texp(shape_, TopAbs_VERTEX, TopAbs_EDGE); texp.More(); texp.Next()) {
gp_XYZ p = BRep_Tool::Pnt(TopoDS::Vertex(texp.Current())).XYZ();
taxonomy_transform(place.components_, p);
t->addVertex(item_id, surface_style_id, p.X(), p.Y(), p.Z());
}
}
if (!settings.get<settings::OcctNoCleanTriangulation>().get()) {
@@ -142,6 +142,9 @@ public:
virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::boolean_result::ptr, IfcGeom::ConversionResults&);
virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::loft::ptr, IfcGeom::ConversionResults&);
virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::sweep_along_curve::ptr, IfcGeom::ConversionResults&);
// Prototype for issue #134 / #1409 / #5218, see point.cpp.
virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::point3::ptr, IfcGeom::ConversionResults&);
virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::collection::ptr, IfcGeom::ConversionResults&);
virtual bool convert_openings(const IfcUtil::IfcBaseEntity* entity, const std::vector<std::pair<ifcopenshell::geometry::taxonomy::ptr, ifcopenshell::geometry::taxonomy::matrix4>>& openings,
const IfcGeom::ConversionResults& entity_shapes, const ifcopenshell::geometry::taxonomy::matrix4& entity_trsf, IfcGeom::ConversionResults& cut_shapes);
+102
View File
@@ -0,0 +1,102 @@
/********************************************************************************
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
// This file was generated with the assistance of an AI coding tool.
// Prototype kernel-level support for single-vertex / point-cloud
// representations (issues #134, #1409, #5218). Points become a
// TopoDS_Compound of TopoDS_Vertex, per aothms's suggested approach.
// convert_impl(point3) handles a lone point; convert_impl(collection) takes a
// bulk fast path for a collection made up entirely of point3 children (e.g. a
// whole IfcCartesianPointList3D "PointCloud"), converting it to a single
// compound in one pass instead of once per point. See commit message for the
// overhead/benchmark discussion.
#include "OpenCascadeKernel.h"
#include <BRepBuilderAPI_MakeVertex.hxx>
#include <BRep_Builder.hxx>
#include <TopoDS_Compound.hxx>
using namespace ifcopenshell::geometry;
using namespace ifcopenshell::geometry::kernels;
using namespace IfcGeom;
namespace {
TopoDS_Compound make_vertex_compound(const std::vector<gp_Pnt>& points) {
TopoDS_Compound compound;
BRep_Builder builder;
builder.MakeCompound(compound);
for (auto& p : points) {
builder.Add(compound, BRepBuilderAPI_MakeVertex(p).Vertex());
}
return compound;
}
}
bool OpenCascadeKernel::convert_impl(const taxonomy::point3::ptr point, IfcGeom::ConversionResults& results) {
if (!point->instance) {
return false;
}
auto p = convert_xyz<gp_Pnt>(*point);
auto compound = make_vertex_compound({ p });
results.emplace_back(ConversionResult(
point->instance->as<IfcUtil::IfcBaseEntity>()->id(),
new OpenCascadeShape(compound),
point->surface_style
));
return true;
}
bool OpenCascadeKernel::convert_impl(const taxonomy::collection::ptr collection, IfcGeom::ConversionResults& results) {
bool all_points = !collection->children.empty();
for (auto& c : collection->children) {
if (c->kind() != taxonomy::POINT3) {
all_points = false;
break;
}
}
if (!all_points || !collection->instance) {
// Not a homogeneous point cloud (or has no entity to attribute the
// resulting shape to), fall back to the generic per-child conversion.
return ifcopenshell::geometry::kernels::AbstractKernel::convert_impl(collection, results);
}
std::vector<gp_Pnt> points;
points.reserve(collection->children.size());
for (auto& c : collection->children) {
points.push_back(convert_xyz<gp_Pnt>(*std::static_pointer_cast<taxonomy::point3>(c)));
}
auto compound = make_vertex_compound(points);
auto s = results.size();
results.emplace_back(ConversionResult(
collection->instance->as<IfcUtil::IfcBaseEntity>()->id(),
new OpenCascadeShape(compound),
collection->surface_style
));
if (collection->matrix) {
results[s].prepend(collection->matrix);
}
return true;
}
@@ -0,0 +1,53 @@
/********************************************************************************
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
// This file was generated with the assistance of an AI coding tool.
// Prototype for issue #5218: maps a "PointCloud" IfcCartesianPointList3D item
// to a taxonomy::collection of point3, read straight from CoordList (see
// kernels/opencascade/point.cpp for the bulk conversion fast path).
#include "mapping.h"
#define mapping POSTFIX_SCHEMA(mapping)
using namespace ifcopenshell::geometry;
#ifdef SCHEMA_HAS_IfcCartesianPointList3D
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcCartesianPointList3D* inst) {
auto coord_list = inst->CoordList();
if (coord_list.empty()) {
return nullptr;
}
auto c = taxonomy::make<taxonomy::collection>();
c->children.reserve(coord_list.size());
for (auto& coords : coord_list) {
auto p = taxonomy::make<taxonomy::point3>(
coords.size() < 1 ? 0. : coords[0] * length_unit_,
coords.size() < 2 ? 0. : coords[1] * length_unit_,
coords.size() < 3 ? 0. : coords[2] * length_unit_);
// No entity per point, so id() lookups in the per-child fallback path
// stay safe by sharing the list's own instance.
p->instance = inst;
c->children.push_back(p);
}
return c;
}
#endif
+36
View File
@@ -0,0 +1,36 @@
/********************************************************************************
* *
* 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 <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
// This file was generated with the assistance of an AI coding tool.
// Prototype for issue #134 / #1409: maps IfcVertexPoint as a top-level
// representation item, not just as an IfcEdge's EdgeStart/-End (IfcEdge.cpp).
#include "mapping.h"
#define mapping POSTFIX_SCHEMA(mapping)
using namespace ifcopenshell::geometry;
taxonomy::ptr mapping::map_impl(const IfcSchema::IfcVertexPoint* inst) {
IfcSchema::IfcPoint* pnt = inst->VertexGeometry();
if (!pnt->declaration().is(IfcSchema::IfcCartesianPoint::Class())) {
logger_.Message(Logger::LOG_ERROR, "GEO", 257, "Only IfcCartesianPoints are supported for VertexGeometry", inst);
return nullptr;
}
return map(pnt);
}
+6
View File
@@ -161,6 +161,12 @@ BIND(IfcCartesianPoint);
#ifdef SCHEMA_HAS_IfcPointByDistanceExpression
BIND(IfcPointByDistanceExpression)
#endif
// IfcVertexPoint as a top-level "Vertex" representation item, see #134.
BIND(IfcVertexPoint);
#ifdef SCHEMA_HAS_IfcCartesianPointList3D
// IfcCartesianPointList3D as a top-level "PointCloud" item, see #5218.
BIND(IfcCartesianPointList3D);
#endif
BIND(IfcDirection);
#ifdef SCHEMA_HAS_IfcAxis2PlacementLinear
BIND(IfcAxis2PlacementLinear)
+2 -1
View File
@@ -637,8 +637,9 @@ typedef item const* ptr;
};
// @todo make 4d for easier multiplication
// geom_item base (not item) so point3 can be a collection child in its own right, see #134.
template <size_t N>
struct IFC_GEOM_API cartesian_base : public item, public eigen_base<Eigen::Vector3d> {
struct IFC_GEOM_API cartesian_base : public geom_item, public eigen_base<Eigen::Vector3d> {
cartesian_base() : eigen_base() {}
cartesian_base(const Eigen::Vector3d& c) : eigen_base(c) {}
cartesian_base(double x, double y, double z = 0.) : eigen_base(Eigen::Vector3d(x, y, z)) {}
@@ -135,6 +135,76 @@ class TestTriangulationAttributes(test.bootstrap.IFC4):
assert len(edges_item_ids) == len(edges)
class TestPointAndVertexRepresentations:
"""Prototype kernel-level support for point/vertex-only representations.
See https://github.com/IfcOpenShell/IfcOpenShell/issues/134,
https://github.com/IfcOpenShell/IfcOpenShell/issues/1409 and
https://github.com/IfcOpenShell/IfcOpenShell/issues/5218.
"""
def make_context(self):
ifc_file = ifcopenshell.file()
ifcopenshell.api.root.create_entity(ifc_file, ifc_class="IfcProject", name="Test")
context = ifcopenshell.api.context.add_context(ifc_file, context_type="Model")
return ifc_file, context
def test_vertex_representation(self):
ifc_file, context = self.make_context()
point = ifc_file.createIfcCartesianPoint((1.0, 2.0, 3.0))
vertex_point = ifc_file.createIfcVertexPoint(point)
representation = ifc_file.createIfcTopologyRepresentation(context, "Body", "Vertex", [vertex_point])
settings = ifcopenshell.geom.settings()
shape = ifcopenshell.geom.create_shape(settings, representation)
verts = ifcopenshell.util.shape.get_vertices(shape)
assert len(verts) == 1
assert tuple(verts[0]) == (1.0, 2.0, 3.0)
def test_point_representation(self):
ifc_file, context = self.make_context()
point = ifc_file.createIfcCartesianPoint((4.0, 5.0, 6.0))
representation = ifc_file.createIfcShapeRepresentation(context, "Body", "Point", [point])
settings = ifcopenshell.geom.settings()
shape = ifcopenshell.geom.create_shape(settings, representation)
verts = ifcopenshell.util.shape.get_vertices(shape)
assert len(verts) == 1
assert tuple(verts[0]) == (4.0, 5.0, 6.0)
def test_point_cloud_representation(self):
ifc_file, context = self.make_context()
coords = [(1.0, 1.0, 1.0), (2.0, 2.0, 2.0), (3.0, 3.0, 3.0), (-1.0, 0.5, 9.0)]
point_list = ifc_file.createIfcCartesianPointList3D(coords)
representation = ifc_file.createIfcShapeRepresentation(context, "Body", "PointCloud", [point_list])
settings = ifcopenshell.geom.settings()
shape = ifcopenshell.geom.create_shape(settings, representation)
verts = ifcopenshell.util.shape.get_vertices(shape)
assert {tuple(v) for v in verts} == set(coords)
def test_point_cloud_of_1000_points_round_trips_and_is_linear_time(self):
import random
import time
ifc_file, context = self.make_context()
random.seed(1)
coords = [(random.random(), random.random(), random.random()) for _ in range(1000)]
point_list = ifc_file.createIfcCartesianPointList3D(coords)
representation = ifc_file.createIfcShapeRepresentation(context, "Body", "PointCloud", [point_list])
settings = ifcopenshell.geom.settings()
t0 = time.perf_counter()
shape = ifcopenshell.geom.create_shape(settings, representation)
dt = time.perf_counter() - t0
verts = ifcopenshell.util.shape.get_vertices(shape)
assert {tuple(v) for v in verts} == set(coords)
# Generous ceiling: on a slow CI box a thousand-point cloud should
# still take well under a second (see the #5218 overhead discussion).
assert dt < 5.0
class TestAssignObject:
def test_no_welding_on_distinct_items(self):
self.file = ifcopenshell.api.project.create_file()