diff --git a/src/ifcgeom/kernels/opencascade/OpenCascadeConversionResult.cpp b/src/ifcgeom/kernels/opencascade/OpenCascadeConversionResult.cpp index 6b6b403ba5..9d289e483d 100644 --- a/src/ifcgeom/kernels/opencascade/OpenCascadeConversionResult.cpp +++ b/src/ifcgeom/kernels/opencascade/OpenCascadeConversionResult.cpp @@ -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().get()) { diff --git a/src/ifcgeom/kernels/opencascade/OpenCascadeKernel.h b/src/ifcgeom/kernels/opencascade/OpenCascadeKernel.h index e4f3e299d2..b5f70b70ad 100644 --- a/src/ifcgeom/kernels/opencascade/OpenCascadeKernel.h +++ b/src/ifcgeom/kernels/opencascade/OpenCascadeKernel.h @@ -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>& openings, const IfcGeom::ConversionResults& entity_shapes, const ifcopenshell::geometry::taxonomy::matrix4& entity_trsf, IfcGeom::ConversionResults& cut_shapes); diff --git a/src/ifcgeom/kernels/opencascade/point.cpp b/src/ifcgeom/kernels/opencascade/point.cpp new file mode 100644 index 0000000000..645bf204a2 --- /dev/null +++ b/src/ifcgeom/kernels/opencascade/point.cpp @@ -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 . * + * * + ********************************************************************************/ + +// 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 +#include +#include + +using namespace ifcopenshell::geometry; +using namespace ifcopenshell::geometry::kernels; +using namespace IfcGeom; + +namespace { + TopoDS_Compound make_vertex_compound(const std::vector& 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(*point); + auto compound = make_vertex_compound({ p }); + + results.emplace_back(ConversionResult( + point->instance->as()->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 points; + points.reserve(collection->children.size()); + for (auto& c : collection->children) { + points.push_back(convert_xyz(*std::static_pointer_cast(c))); + } + + auto compound = make_vertex_compound(points); + + auto s = results.size(); + results.emplace_back(ConversionResult( + collection->instance->as()->id(), + new OpenCascadeShape(compound), + collection->surface_style + )); + if (collection->matrix) { + results[s].prepend(collection->matrix); + } + return true; +} diff --git a/src/ifcgeom/mapping/IfcCartesianPointList3D.cpp b/src/ifcgeom/mapping/IfcCartesianPointList3D.cpp new file mode 100644 index 0000000000..d07d7f6431 --- /dev/null +++ b/src/ifcgeom/mapping/IfcCartesianPointList3D.cpp @@ -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 . * + * * + ********************************************************************************/ + +// 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(); + c->children.reserve(coord_list.size()); + for (auto& coords : coord_list) { + auto p = taxonomy::make( + 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 diff --git a/src/ifcgeom/mapping/IfcVertexPoint.cpp b/src/ifcgeom/mapping/IfcVertexPoint.cpp new file mode 100644 index 0000000000..386783de34 --- /dev/null +++ b/src/ifcgeom/mapping/IfcVertexPoint.cpp @@ -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 . * + * * + ********************************************************************************/ + +// 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); +} diff --git a/src/ifcgeom/mapping/mapping.i b/src/ifcgeom/mapping/mapping.i index 8766238dc3..7d7d57ccce 100644 --- a/src/ifcgeom/mapping/mapping.i +++ b/src/ifcgeom/mapping/mapping.i @@ -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) diff --git a/src/ifcgeom/taxonomy.h b/src/ifcgeom/taxonomy.h index a1d0c89ad4..ae26d88312 100644 --- a/src/ifcgeom/taxonomy.h +++ b/src/ifcgeom/taxonomy.h @@ -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 - struct IFC_GEOM_API cartesian_base : public item, public eigen_base { + struct IFC_GEOM_API cartesian_base : public geom_item, public eigen_base { 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)) {} diff --git a/src/ifcopenshell-python/test/test_create_shape.py b/src/ifcopenshell-python/test/test_create_shape.py index e40570d161..c69f2d6488 100644 --- a/src/ifcopenshell-python/test/test_create_shape.py +++ b/src/ifcopenshell-python/test/test_create_shape.py @@ -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()