mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user