mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-21 04:32:23 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| df1ababbec | |||
| c8f29bb843 | |||
| 99c514828d | |||
| 8a5b1ab10d |
@@ -67,7 +67,60 @@ const Settings& ifcopenshell::geometry::kernels::AbstractKernel::settings() cons
|
||||
return settings_;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Homogeneous taxonomy::point3 collections (e.g. IfcCartesianPointList3D)
|
||||
// are reduced to a single result, see #134/#1409/#5218.
|
||||
bool is_reducible_point_collection(const ifcopenshell::geometry::taxonomy::collection::ptr& collection) {
|
||||
if (collection->children.empty()) {
|
||||
return false;
|
||||
}
|
||||
for (auto& c : collection->children) {
|
||||
if (c->kind() != ifcopenshell::geometry::taxonomy::POINT3) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool ifcopenshell::geometry::kernels::AbstractKernel::convert_impl(const taxonomy::collection::ptr collection, IfcGeom::ConversionResults& r) {
|
||||
if (collection->instance && is_reducible_point_collection(collection)) {
|
||||
// Reduce via the generic wrap_in_compound()/concat_many() API rather
|
||||
// than a per-kernel convert_impl(collection) override. concat_many()
|
||||
// combines everything in a single bulk call so kernels can implement
|
||||
// it in O(n): a loop calling concat() pairwise would either
|
||||
// re-classify an ever-growing accumulator (quadratic) or produce an
|
||||
// O(n)-deep nested shape (quadratic to traverse later).
|
||||
// Kept alive until concat_many() below: shapes[] holds raw pointers
|
||||
// into these ConversionResults' shared_ptr<ConversionResultShape>.
|
||||
IfcGeom::ConversionResults child_results;
|
||||
for (auto& c : collection->children) {
|
||||
if (!convert(c, child_results) && !partial_success_is_success) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (child_results.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<IfcGeom::ConversionResultShape*> shapes;
|
||||
shapes.reserve(child_results.size());
|
||||
for (auto& t : child_results) {
|
||||
shapes.push_back(t.Shape().get());
|
||||
}
|
||||
|
||||
auto* first = shapes.front();
|
||||
std::vector<IfcGeom::ConversionResultShape*> rest(shapes.begin() + 1, shapes.end());
|
||||
auto* accum = first->concat_many(rest);
|
||||
|
||||
r.emplace_back(IfcGeom::ConversionResult(collection->instance->as<IfcUtil::IfcBaseEntity>()->id(), accum, collection->surface_style));
|
||||
if (collection->matrix) {
|
||||
r.back().prepend(collection->matrix);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
auto s = r.size();
|
||||
for (auto& c : collection->children) {
|
||||
if (!convert(c, r) && !partial_success_is_success) {
|
||||
|
||||
@@ -492,6 +492,22 @@ namespace IfcGeom {
|
||||
virtual ConversionResultShape* intersect(ConversionResultShape*) = 0;
|
||||
virtual ConversionResultShape* concat(ConversionResultShape*) = 0;
|
||||
|
||||
// Bulk variant of concat(), combining `this` with every shape in
|
||||
// `others` into a single result. Used by AbstractKernel to reduce a
|
||||
// homogeneous collection (e.g. a point cloud) into one shape, see
|
||||
// #134/#1409/#5218. Default falls back to repeated concat() (correct
|
||||
// but potentially quadratic); kernels can override with a flat,
|
||||
// linear-time bulk implementation.
|
||||
virtual ConversionResultShape* concat_many(const std::vector<ConversionResultShape*>& others) {
|
||||
ConversionResultShape* result = wrap_in_compound();
|
||||
for (auto* other : others) {
|
||||
auto* next = result->concat(other);
|
||||
delete result;
|
||||
result = next;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
virtual std::size_t map(OpaqueCoordinate<4>& from, OpaqueCoordinate<4>& to) = 0;
|
||||
virtual std::size_t map(const std::vector<OpaqueCoordinate<4>>& from, const std::vector<OpaqueCoordinate<4>>& to) = 0;
|
||||
virtual ConversionResultShape* moved(ifcopenshell::geometry::taxonomy::matrix4::ptr) const = 0;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -343,6 +343,15 @@ 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);
|
||||
int idx = t->addVertex(item_id, surface_style_id, p.X(), p.Y(), p.Z());
|
||||
t->addPoint(item_id, surface_style_id, idx);
|
||||
}
|
||||
}
|
||||
|
||||
if (!settings.get<settings::OcctNoCleanTriangulation>().get()) {
|
||||
@@ -578,6 +587,22 @@ ConversionResultShape* ifcopenshell::geometry::OpenCascadeShape::concat(Conversi
|
||||
return new OpenCascadeShape(std::move(compound));
|
||||
}
|
||||
|
||||
ConversionResultShape* ifcopenshell::geometry::OpenCascadeShape::concat_many(const std::vector<ConversionResultShape*>& others)
|
||||
{
|
||||
// Unlike concat(), which is called pairwise and therefore re-classifies
|
||||
// its (potentially large) receiver on every call, this builds one flat
|
||||
// compound in a single O(n) pass: linear time and constant nesting depth
|
||||
// regardless of how many shapes are combined.
|
||||
TopoDS_Compound compound;
|
||||
BRep_Builder builder;
|
||||
builder.MakeCompound(compound);
|
||||
builder.Add(compound, shape_);
|
||||
for (auto* other : others) {
|
||||
builder.Add(compound, static_cast<OpenCascadeShape*>(other)->shape_);
|
||||
}
|
||||
return new OpenCascadeShape(std::move(compound));
|
||||
}
|
||||
|
||||
std::pair<OpaqueCoordinate<3>, OpaqueCoordinate<3>> ifcopenshell::geometry::OpenCascadeShape::bounding_box() const
|
||||
{
|
||||
throw std::runtime_error("Not implemented");
|
||||
|
||||
@@ -99,6 +99,7 @@ namespace ifcopenshell {
|
||||
virtual ConversionResultShape* subtract(ConversionResultShape*);
|
||||
virtual ConversionResultShape* intersect(ConversionResultShape*);
|
||||
virtual ConversionResultShape* concat(ConversionResultShape*);
|
||||
virtual ConversionResultShape* concat_many(const std::vector<ConversionResultShape*>& others);
|
||||
|
||||
virtual std::size_t map(OpaqueCoordinate<4>& from, OpaqueCoordinate<4>& to);
|
||||
virtual std::size_t map(const std::vector<OpaqueCoordinate<4>>& from, const std::vector<OpaqueCoordinate<4>>& to);
|
||||
|
||||
@@ -142,6 +142,10 @@ 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. The bulk
|
||||
// point-cloud fast path is generic infrastructure in
|
||||
// AbstractKernel::convert_impl(collection), not a per-kernel override.
|
||||
virtual bool convert_impl(const ifcopenshell::geometry::taxonomy::point3::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,63 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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). A point becomes a
|
||||
// TopoDS_Compound of TopoDS_Vertex. Point-cloud batching lives generically
|
||||
// in AbstractKernel::convert_impl(collection), see that file.
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
# IfcOpenShell - IFC toolkit and geometry engine
|
||||
# Copyright (C) 2026 IfcOpenShell contributors
|
||||
#
|
||||
# This file is part of IfcOpenShell.
|
||||
#
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 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
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
"""End-to-end coverage that mimics IfcConvert for Vertex/Point/PointCloud
|
||||
representations (#134, #1409, #5218), as requested by aothms on PR #8759:
|
||||
ifcopenshell.geom.create_shape() succeeding is not proof that the actual
|
||||
IfcConvert output pipeline (serializers writing real files) works. These
|
||||
tests invoke the real IfcConvert binary and inspect the resulting files.
|
||||
"""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import pytest
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.context
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.api.unit
|
||||
|
||||
IFCCONVERT = shutil.which("IfcConvert")
|
||||
|
||||
pytestmark = pytest.mark.skipif(IFCCONVERT is None, reason="Requires IfcConvert in path")
|
||||
|
||||
|
||||
def make_model(tmp_path):
|
||||
"""A single model with a Vertex, a Point, a PointCloud and (as a
|
||||
regression check) an ordinary extruded wall, so that IfcConvert has to
|
||||
process all four in one pass.
|
||||
"""
|
||||
f = ifcopenshell.file()
|
||||
ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject", name="Test")
|
||||
unit = ifcopenshell.api.unit.add_si_unit(f, unit_type="LENGTHUNIT")
|
||||
ifcopenshell.api.unit.assign_unit(f, units=[unit])
|
||||
context = ifcopenshell.api.context.add_context(f, context_type="Model")
|
||||
|
||||
vertex_xyz = (1.0, 2.0, 3.0)
|
||||
vertex_element = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuildingElementProxy", name="Vertex")
|
||||
vertex_point = f.createIfcVertexPoint(f.createIfcCartesianPoint(vertex_xyz))
|
||||
vertex_representation = f.createIfcTopologyRepresentation(context, "Body", "Vertex", [vertex_point])
|
||||
vertex_element.Representation = f.createIfcProductDefinitionShape(Representations=[vertex_representation])
|
||||
|
||||
point_xyz = (4.0, 5.0, 6.0)
|
||||
point_element = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuildingElementProxy", name="Point")
|
||||
point_representation = f.createIfcShapeRepresentation(
|
||||
context, "Body", "Point", [f.createIfcCartesianPoint(point_xyz)]
|
||||
)
|
||||
point_element.Representation = f.createIfcProductDefinitionShape(Representations=[point_representation])
|
||||
|
||||
cloud_coords = [(10.0, 11.0, 12.0), (13.0, 14.0, 15.0), (16.0, 17.0, 18.0), (-1.0, 0.5, 9.0)]
|
||||
cloud_element = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuildingElementProxy", name="PointCloud")
|
||||
point_list = f.createIfcCartesianPointList3D(cloud_coords)
|
||||
cloud_representation = f.createIfcShapeRepresentation(context, "Body", "PointCloud", [point_list])
|
||||
cloud_element.Representation = f.createIfcProductDefinitionShape(Representations=[cloud_representation])
|
||||
|
||||
wall = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="Wall")
|
||||
profile_points = [(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)]
|
||||
curve = f.createIfcPolyline([f.createIfcCartesianPoint(p) for p in profile_points])
|
||||
extrusion = f.createIfcExtrudedAreaSolid(
|
||||
f.createIfcArbitraryClosedProfileDef("AREA", None, curve),
|
||||
f.createIfcAxis2Placement3D(f.createIfcCartesianPoint((0.0, 0.0, 0.0))),
|
||||
f.createIfcDirection((0.0, 0.0, 1.0)),
|
||||
1.0,
|
||||
)
|
||||
wall_representation = f.createIfcShapeRepresentation(context, "Body", "SweptSolid", [extrusion])
|
||||
wall.Representation = f.createIfcProductDefinitionShape(Representations=[wall_representation])
|
||||
|
||||
fn = tmp_path / "point_representations.ifc"
|
||||
f.write(str(fn))
|
||||
return fn, {
|
||||
"Vertex": vertex_xyz,
|
||||
"Point": point_xyz,
|
||||
"PointCloud": cloud_coords,
|
||||
}
|
||||
|
||||
|
||||
def run_ifcconvert(input_fn, output_fn, *extra_args):
|
||||
# Point/vertex conversion (see kernels/opencascade/point.cpp) is only
|
||||
# implemented in the OpenCascade kernel, force it explicitly since a
|
||||
# build with CGAL/Manifold available would otherwise default to those.
|
||||
# --use-element-names makes the OBJ "g"/glTF node names predictable
|
||||
# (the IfcRoot.Name we set), instead of opaque unique IDs.
|
||||
args = [
|
||||
IFCCONVERT,
|
||||
"-yqv",
|
||||
"--kernel",
|
||||
"opencascade",
|
||||
"--use-element-names",
|
||||
str(input_fn),
|
||||
str(output_fn),
|
||||
*extra_args,
|
||||
]
|
||||
completed = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
return completed.returncode, completed.stdout.decode("utf-8", "replace")
|
||||
|
||||
|
||||
def parse_obj_groups(obj_text):
|
||||
"""Split a Wavefront OBJ into per-object groups of ("v"/"p"/"f" ...) lines."""
|
||||
groups = {}
|
||||
current = None
|
||||
for line in obj_text.splitlines():
|
||||
if line.startswith("g "):
|
||||
current = line[2:].strip()
|
||||
groups[current] = {"v": [], "p": [], "f": []}
|
||||
elif current is not None and line[:2] in ("v ", "p ", "f "):
|
||||
kind = line[0]
|
||||
groups[current][kind].append(line)
|
||||
return groups
|
||||
|
||||
|
||||
class TestIfcConvertObj:
|
||||
def test_vertex_point_and_point_cloud_produce_p_records(self, tmp_path):
|
||||
fn, expected = make_model(tmp_path)
|
||||
obj_fn = tmp_path / "out.obj"
|
||||
|
||||
returncode, log = run_ifcconvert(fn, obj_fn)
|
||||
assert returncode == 0, log
|
||||
assert obj_fn.exists()
|
||||
|
||||
groups = parse_obj_groups(obj_fn.read_text())
|
||||
|
||||
# Every representation-only object should produce exactly as many
|
||||
# "p" (point primitive) records as it has vertices, and no "f".
|
||||
for name, coords in [("Vertex", [expected["Vertex"]]), ("Point", [expected["Point"]])]:
|
||||
matches = [g for gid, g in groups.items() if gid.split("-")[0] == name or name in gid]
|
||||
assert matches, f"No OBJ group found for {name} (groups: {list(groups)})"
|
||||
g = matches[0]
|
||||
assert len(g["v"]) == len(coords)
|
||||
assert len(g["p"]) == len(coords)
|
||||
assert len(g["f"]) == 0
|
||||
for (x, y, z), vline in zip(coords, g["v"]):
|
||||
_, vx, vy, vz = vline.split()
|
||||
assert (float(vx), float(vy), float(vz)) == (x, y, z)
|
||||
|
||||
cloud_matches = [g for gid, g in groups.items() if "PointCloud" in gid]
|
||||
assert cloud_matches, f"No OBJ group found for PointCloud (groups: {list(groups)})"
|
||||
cloud = cloud_matches[0]
|
||||
assert len(cloud["v"]) == len(expected["PointCloud"])
|
||||
assert len(cloud["p"]) == len(expected["PointCloud"])
|
||||
assert len(cloud["f"]) == 0
|
||||
actual_coords = {tuple(float(c) for c in vline.split()[1:]) for vline in cloud["v"]}
|
||||
assert actual_coords == set(expected["PointCloud"])
|
||||
|
||||
# OBJ vertex/point indices are 1-based and cumulative across the
|
||||
# whole file (not reset per "g" group), matching how faces already
|
||||
# index into vcount_total. The point cloud's "p" indices must still
|
||||
# be distinct and contiguous, referencing exactly its own 4 "v" lines.
|
||||
p_indices = sorted(int(pline.split()[1]) for pline in cloud["p"])
|
||||
assert p_indices == list(range(p_indices[0], p_indices[0] + len(expected["PointCloud"])))
|
||||
|
||||
def test_ordinary_geometry_still_produces_faces(self, tmp_path):
|
||||
# Regression check: a normal extruded wall in the same file must
|
||||
# still triangulate to faces, unaffected by the point/vertex handling.
|
||||
fn, _ = make_model(tmp_path)
|
||||
obj_fn = tmp_path / "out.obj"
|
||||
|
||||
returncode, log = run_ifcconvert(fn, obj_fn)
|
||||
assert returncode == 0, log
|
||||
|
||||
groups = parse_obj_groups(obj_fn.read_text())
|
||||
wall_matches = [g for gid, g in groups.items() if "Wall" in gid]
|
||||
assert wall_matches, f"No OBJ group found for Wall (groups: {list(groups)})"
|
||||
wall = wall_matches[0]
|
||||
assert len(wall["f"]) > 0
|
||||
assert len(wall["p"]) == 0
|
||||
|
||||
|
||||
class TestIfcConvertGltf:
|
||||
def test_point_cloud_uses_points_primitive_mode(self, tmp_path):
|
||||
fn, expected = make_model(tmp_path)
|
||||
glb_fn = tmp_path / "out.glb"
|
||||
|
||||
returncode, log = run_ifcconvert(fn, glb_fn)
|
||||
assert returncode == 0, log
|
||||
assert glb_fn.exists()
|
||||
|
||||
data = glb_fn.read_bytes()
|
||||
magic, version, length = struct.unpack_from("<4sII", data, 0)
|
||||
assert magic == b"glTF"
|
||||
|
||||
offset = 12
|
||||
json_chunk = None
|
||||
while offset < length:
|
||||
chunk_length, chunk_type = struct.unpack_from("<I4s", data, offset)
|
||||
chunk_data = data[offset + 8 : offset + 8 + chunk_length]
|
||||
if chunk_type == b"JSON":
|
||||
json_chunk = json.loads(chunk_data.decode("utf-8"))
|
||||
break
|
||||
offset += 8 + chunk_length
|
||||
assert json_chunk is not None, "No JSON chunk found in .glb"
|
||||
|
||||
# Mode 0 is POINTS in glTF; some mesh, somewhere, must use it for
|
||||
# this file (the PointCloud/Point/Vertex elements), and none of the
|
||||
# 4 point-cloud vertices should have been silently dropped.
|
||||
point_primitives = [
|
||||
p for mesh in json_chunk.get("meshes", []) for p in mesh.get("primitives", []) if p.get("mode") == 0
|
||||
]
|
||||
assert point_primitives, "Expected at least one glTF primitive with mode=POINTS (0)"
|
||||
|
||||
accessors = json_chunk["accessors"]
|
||||
vertex_counts = {accessors[p["attributes"]["POSITION"]]["count"] for p in point_primitives}
|
||||
assert len(expected["PointCloud"]) in vertex_counts or 1 in vertex_counts
|
||||
|
||||
|
||||
class TestIfcConvertCollada:
|
||||
def test_point_only_geometry_does_not_crash(self, tmp_path):
|
||||
# COLLADA has no native point primitive (see ColladaSerializer.cpp:
|
||||
# ColladaGeometries::write logs a "has no COLLADA representation"
|
||||
# warning for this, but geometry serializer plugins don't currently
|
||||
# receive IfcConvert's actual logger instance - a pre-existing gap
|
||||
# unrelated to #134/#1409/#5218, so the warning isn't observable
|
||||
# from here). What matters end-to-end: IfcConvert must not crash,
|
||||
# and the position data must still be written even though there is
|
||||
# no visible primitive referencing it.
|
||||
fn, expected = make_model(tmp_path)
|
||||
dae_fn = tmp_path / "out.dae"
|
||||
|
||||
returncode, log = run_ifcconvert(fn, dae_fn)
|
||||
assert returncode == 0, log
|
||||
assert dae_fn.exists()
|
||||
|
||||
xml = dae_fn.read_text()
|
||||
tree = ET.fromstring(xml)
|
||||
ns = {"c": "http://www.collada.org/2005/11/COLLADASchema"}
|
||||
geometries = tree.findall(".//c:library_geometries/c:geometry", ns)
|
||||
assert geometries, "Expected at least one <geometry> in the .dae"
|
||||
|
||||
point_cloud_floats = " ".join(f"{c:g}" for coords in expected["PointCloud"] for c in coords)
|
||||
found_point_cloud_positions = False
|
||||
for geometry in geometries:
|
||||
float_array = geometry.find(".//c:float_array", ns)
|
||||
if (
|
||||
float_array is not None
|
||||
and float_array.text
|
||||
and point_cloud_floats in " ".join(float_array.text.split())
|
||||
):
|
||||
found_point_cloud_positions = True
|
||||
# No triangles/lines/polylist: nothing meaningful to draw.
|
||||
assert geometry.find(".//c:triangles", ns) is None
|
||||
assert geometry.find(".//c:lines", ns) is None
|
||||
assert found_point_cloud_positions, "PointCloud coordinates missing from .dae output"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import pytest
|
||||
|
||||
pytest.main(["-vvsx", __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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user