|
|
|
|
@@ -0,0 +1,246 @@
|
|
|
|
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
|
|
|
// Import-only binding to the real IfcOpenShell parser and the supplied OCCT ABI.
|
|
|
|
|
#include <Python.h>
|
|
|
|
|
#include <App/Application.h>
|
|
|
|
|
#include <App/Document.h>
|
|
|
|
|
#include <App/DocumentObjectGroup.h>
|
|
|
|
|
#include <App/PropertyFile.h>
|
|
|
|
|
#include <App/PropertyStandard.h>
|
|
|
|
|
#include <Base/FileInfo.h>
|
|
|
|
|
#include <Mod/Part/App/PartFeature.h>
|
|
|
|
|
#include <BRepBuilderAPI_Transform.hxx>
|
|
|
|
|
#include <BRepCheck_Analyzer.hxx>
|
|
|
|
|
#include <BRep_Builder.hxx>
|
|
|
|
|
#include <ShapeFix_Shape.hxx>
|
|
|
|
|
#include <TopoDS_Compound.hxx>
|
|
|
|
|
#include <gp_Trsf.hxx>
|
|
|
|
|
#include <QJsonArray>
|
|
|
|
|
#include <QJsonDocument>
|
|
|
|
|
#include <QJsonObject>
|
|
|
|
|
#include <ifcparse/IfcFile.h>
|
|
|
|
|
#include <ifcgeom/Iterator.h>
|
|
|
|
|
#include <ifcgeom/kernels/opencascade/OpenCascadeKernel.h>
|
|
|
|
|
#include <ifcgeom/kernels/opencascade/OpenCascadeConversionResult.h>
|
|
|
|
|
#include <map>
|
|
|
|
|
#include <memory>
|
|
|
|
|
#include <set>
|
|
|
|
|
#include <sstream>
|
|
|
|
|
#include <iostream>
|
|
|
|
|
#include <functional>
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
using Entity = IfcUtil::IfcBaseEntity;
|
|
|
|
|
void stringProperty(App::DocumentObject* object, const char* name, const std::string& value)
|
|
|
|
|
{
|
|
|
|
|
auto* property = dynamic_cast<App::PropertyString*>(object->getPropertyByName(name));
|
|
|
|
|
if (!property) property = dynamic_cast<App::PropertyString*>(object->addDynamicProperty("App::PropertyString", name, "IFC"));
|
|
|
|
|
if (!property) throw std::runtime_error("IFC property type conflict");
|
|
|
|
|
property->setValue(value);
|
|
|
|
|
}
|
|
|
|
|
AttributeValue attribute(const Entity* entity, const char* name)
|
|
|
|
|
{
|
|
|
|
|
return entity->get_attribute_value(entity->declaration().as_entity()->attribute_index(name));
|
|
|
|
|
}
|
|
|
|
|
std::string stringAttribute(const Entity* entity, const char* name)
|
|
|
|
|
{
|
|
|
|
|
auto value = attribute(entity, name);
|
|
|
|
|
return value.isNull() ? std::string() : static_cast<std::string>(value);
|
|
|
|
|
}
|
|
|
|
|
QJsonValue jsonValue(const AttributeValue& value)
|
|
|
|
|
{
|
|
|
|
|
if (value.isNull()) return QJsonValue();
|
|
|
|
|
switch (value.type()) {
|
|
|
|
|
case IfcUtil::Argument_INT: return static_cast<int>(value);
|
|
|
|
|
case IfcUtil::Argument_BOOL: return static_cast<bool>(value);
|
|
|
|
|
case IfcUtil::Argument_DOUBLE: return static_cast<double>(value);
|
|
|
|
|
case IfcUtil::Argument_STRING: return QString::fromStdString(static_cast<std::string>(value));
|
|
|
|
|
case IfcUtil::Argument_ENTITY_INSTANCE: {
|
|
|
|
|
auto* item = static_cast<IfcUtil::IfcBaseClass*>(value);
|
|
|
|
|
std::ostringstream text;
|
|
|
|
|
item->toString(text);
|
|
|
|
|
return QString::fromStdString(text.str());
|
|
|
|
|
}
|
|
|
|
|
default: return QStringLiteral("See embedded original IFC");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
QJsonObject metadata(const Entity* entity)
|
|
|
|
|
{
|
|
|
|
|
QJsonObject data;
|
|
|
|
|
if (!entity || !entity->declaration().as_entity()) throw std::runtime_error("IFC metadata requires an entity declaration");
|
|
|
|
|
const auto attributes = entity->declaration().as_entity()->all_attributes();
|
|
|
|
|
for (size_t i = 0; i < attributes.size(); ++i)
|
|
|
|
|
data.insert(QString::fromStdString(attributes[i]->name()), jsonValue(entity->get_attribute_value(i)));
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
void identify(App::DocumentObject* object, const Entity* entity)
|
|
|
|
|
{
|
|
|
|
|
const auto name = stringAttribute(entity, "Name");
|
|
|
|
|
object->Label.setValue(name.empty() ? entity->declaration().name() : name);
|
|
|
|
|
stringProperty(object, "IfcGlobalId", stringAttribute(entity, "GlobalId"));
|
|
|
|
|
stringProperty(object, "IfcType", entity->declaration().name());
|
|
|
|
|
stringProperty(object, "IfcAttributes", QJsonDocument(metadata(entity)).toJson(QJsonDocument::Compact).toStdString());
|
|
|
|
|
auto* step = dynamic_cast<App::PropertyInteger*>(object->addDynamicProperty("App::PropertyInteger", "IfcStepId", "IFC"));
|
|
|
|
|
step->setValue(entity->id());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PyObject* openIfc(PyObject*, PyObject* args)
|
|
|
|
|
{
|
|
|
|
|
const char* path;
|
|
|
|
|
if (!PyArg_ParseTuple(args, "s:open", &path)) return nullptr;
|
|
|
|
|
App::Document* document = nullptr;
|
|
|
|
|
try {
|
|
|
|
|
IfcParse::IfcFile file(path);
|
|
|
|
|
if (!file.good()) throw std::runtime_error("IfcOpenShell rejected the IFC file or schema");
|
|
|
|
|
ifcopenshell::geometry::Settings settings;
|
|
|
|
|
settings.get<ifcopenshell::geometry::settings::UseWorldCoords>().value = true;
|
|
|
|
|
settings.get<ifcopenshell::geometry::settings::ConvertBackUnits>().value = false;
|
|
|
|
|
settings.get<ifcopenshell::geometry::settings::IteratorOutput>().value = ifcopenshell::geometry::settings::NATIVE;
|
|
|
|
|
auto kernel = std::make_unique<IfcGeom::OpenCascadeKernel>(settings);
|
|
|
|
|
IfcGeom::Iterator iterator(std::move(kernel), settings, &file, 1);
|
|
|
|
|
std::cerr << "IFC: initializing native geometry iterator" << std::endl;
|
|
|
|
|
if (!iterator.initialize()) throw std::runtime_error("IFC contains no convertible native geometry");
|
|
|
|
|
auto& application = App::GetApplication();
|
|
|
|
|
document = application.newDocument("IfcImport", "IFC Import", {false, true});
|
|
|
|
|
document->setUndoMode(1);
|
|
|
|
|
document->openTransaction("Import IFC with IfcOpenShell");
|
|
|
|
|
std::map<int, App::DocumentObject*> objects;
|
|
|
|
|
auto entities = file.instances_by_type("IfcProduct");
|
|
|
|
|
if (!entities) throw std::runtime_error("IFC has no products");
|
|
|
|
|
for (auto* item : *entities) {
|
|
|
|
|
auto* entity = item->as<Entity>();
|
|
|
|
|
const auto type = entity->declaration().name();
|
|
|
|
|
if (type != "IfcSite" && type != "IfcBuilding" && type != "IfcBuildingStorey" && type != "IfcSpace") continue;
|
|
|
|
|
auto* group = document->addObject("App::DocumentObjectGroup", ("Ifc_" + std::to_string(entity->id())).c_str());
|
|
|
|
|
identify(group, entity);
|
|
|
|
|
objects[entity->id()] = group;
|
|
|
|
|
}
|
|
|
|
|
std::size_t geometryCount = 0;
|
|
|
|
|
gp_Trsf scale;
|
|
|
|
|
scale.SetScale(gp_Pnt(0, 0, 0), 1000.0); // IfcOpenShell SI metres -> FreeCAD millimetres.
|
|
|
|
|
do {
|
|
|
|
|
auto* element = iterator.get_native();
|
|
|
|
|
if (!element) throw std::runtime_error("IfcOpenShell returned no native element");
|
|
|
|
|
std::cerr << "IFC: product #" << element->id() << " " << element->type() << " converted=" << geometryCount << std::endl;
|
|
|
|
|
std::unique_ptr<IfcGeom::ConversionResultShape> converted(element->geometry().as_compound(true));
|
|
|
|
|
auto* native = dynamic_cast<ifcopenshell::geometry::OpenCascadeShape*>(converted.get());
|
|
|
|
|
if (!native || native->shape().IsNull()) throw std::runtime_error("IFC conversion did not return OCCT geometry");
|
|
|
|
|
auto shape = BRepBuilderAPI_Transform(native->shape(), scale, true).Shape();
|
|
|
|
|
if (!BRepCheck_Analyzer(shape).IsValid()) {
|
|
|
|
|
std::cerr << "IFC: repairing #" << element->id() << std::endl;
|
|
|
|
|
ShapeFix_Shape repair(shape);
|
|
|
|
|
repair.Perform();
|
|
|
|
|
shape = repair.Shape();
|
|
|
|
|
if (!BRepCheck_Analyzer(shape).IsValid())
|
|
|
|
|
throw std::runtime_error("IFC product #" + std::to_string(element->id()) + " " + element->guid() + " has invalid OCCT geometry after repair");
|
|
|
|
|
}
|
|
|
|
|
auto* existing = objects[element->id()];
|
|
|
|
|
auto* feature = dynamic_cast<Part::Feature*>(existing);
|
|
|
|
|
if (existing && !feature) {
|
|
|
|
|
feature = dynamic_cast<Part::Feature*>(document->addObject("Part::Feature", ("IfcShape_"+std::to_string(element->id())).c_str()));
|
|
|
|
|
dynamic_cast<App::DocumentObjectGroup*>(existing)->addObject(feature);
|
|
|
|
|
} else if (!feature) {
|
|
|
|
|
feature = dynamic_cast<Part::Feature*>(document->addObject("Part::Feature", ("Ifc_"+std::to_string(element->id())).c_str()));
|
|
|
|
|
objects[element->id()] = feature;
|
|
|
|
|
}
|
|
|
|
|
if (!feature) throw std::runtime_error("FreeCAD could not create an IFC shape object");
|
|
|
|
|
if (!feature->getPropertyByName("IfcGlobalId")) identify(feature, element->product());
|
|
|
|
|
if (!feature->Shape.getValue().IsNull()) {
|
|
|
|
|
TopoDS_Compound compound; BRep_Builder builder; builder.MakeCompound(compound);
|
|
|
|
|
builder.Add(compound, feature->Shape.getValue()); builder.Add(compound, shape); shape = compound;
|
|
|
|
|
}
|
|
|
|
|
feature->Shape.setValue(shape);
|
|
|
|
|
if (element->type() == "IfcOpeningElement" || element->type() == "IfcSpace") {
|
|
|
|
|
// Retain analytical volumes without filling visible wall openings.
|
|
|
|
|
auto* property = feature->getPropertyByName("Visibility");
|
|
|
|
|
if (!property) property = feature->addDynamicProperty("App::PropertyBool", "Visibility", "IFC");
|
|
|
|
|
if (auto* visibility = dynamic_cast<App::PropertyBool*>(property)) visibility->setValue(false);
|
|
|
|
|
}
|
|
|
|
|
++geometryCount;
|
|
|
|
|
std::cerr << "IFC: completed #" << element->id() << std::endl;
|
|
|
|
|
} while (iterator.next());
|
|
|
|
|
if (iterator.had_error_processing_elements()) throw std::runtime_error("IFC geometry conversion was incomplete; document rolled back");
|
|
|
|
|
auto projects = file.instances_by_type("IfcProject");
|
|
|
|
|
if (projects) for (auto* item : *projects) {
|
|
|
|
|
auto* entity = item->as<Entity>();
|
|
|
|
|
auto* group = document->addObject("App::DocumentObjectGroup", ("Ifc_"+std::to_string(entity->id())).c_str());
|
|
|
|
|
identify(group, entity); objects[entity->id()] = group;
|
|
|
|
|
}
|
|
|
|
|
for (const auto& relationType : {"IfcRelAggregates", "IfcRelContainedInSpatialStructure"}) {
|
|
|
|
|
auto relations = file.instances_by_type(relationType);
|
|
|
|
|
if (!relations) continue;
|
|
|
|
|
for (auto* item : *relations) {
|
|
|
|
|
auto* relation = item->as<Entity>();
|
|
|
|
|
bool aggregates = std::string(relationType) == "IfcRelAggregates";
|
|
|
|
|
auto* parentEntity = static_cast<IfcUtil::IfcBaseClass*>(attribute(relation, aggregates ? "RelatingObject" : "RelatingStructure"))->as<Entity>();
|
|
|
|
|
auto found = objects.find(parentEntity->id());
|
|
|
|
|
auto* parent = found == objects.end() ? nullptr : dynamic_cast<App::DocumentObjectGroup*>(found->second);
|
|
|
|
|
if (!parent) continue;
|
|
|
|
|
aggregate_of_instance::ptr children = attribute(relation, aggregates ? "RelatedObjects" : "RelatedElements");
|
|
|
|
|
for (auto* child : *children) {
|
|
|
|
|
auto found = objects.find(child->as<Entity>()->id());
|
|
|
|
|
if (found != objects.end() && found->second != parent) parent->addObject(found->second);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
auto definitions = file.instances_by_type("IfcRelDefinesByProperties");
|
|
|
|
|
std::map<int, QJsonArray> propertySets;
|
|
|
|
|
if (definitions) for (auto* item : *definitions) {
|
|
|
|
|
auto* relation = item->as<Entity>();
|
|
|
|
|
auto definitionValue = attribute(relation, "RelatingPropertyDefinition");
|
|
|
|
|
if (definitionValue.isNull() || definitionValue.type() != IfcUtil::Argument_ENTITY_INSTANCE) continue;
|
|
|
|
|
QJsonArray data;
|
|
|
|
|
std::function<void(IfcUtil::IfcBaseClass*, unsigned)> appendDefinition = [&](IfcUtil::IfcBaseClass* definition, unsigned depth) {
|
|
|
|
|
if (!definition || depth > 4) throw std::runtime_error("Invalid IFC property set definition");
|
|
|
|
|
if (!definition->declaration().as_entity()) {
|
|
|
|
|
const auto aggregate = definition->get_attribute_value(0);
|
|
|
|
|
if (aggregate.type() != IfcUtil::Argument_AGGREGATE_OF_ENTITY_INSTANCE)
|
|
|
|
|
throw std::runtime_error("Unsupported IFC property set select");
|
|
|
|
|
aggregate_of_instance::ptr members = aggregate;
|
|
|
|
|
for (auto* member : *members) appendDefinition(member, depth + 1);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
auto* entity = definition->as<Entity>();
|
|
|
|
|
auto description = metadata(entity);
|
|
|
|
|
const auto type = definition->declaration().name();
|
|
|
|
|
if (type == "IfcPropertySet" || type == "IfcElementQuantity") {
|
|
|
|
|
aggregate_of_instance::ptr values = attribute(entity, type == "IfcPropertySet" ? "HasProperties" : "Quantities");
|
|
|
|
|
QJsonArray properties;
|
|
|
|
|
for (auto* value : *values) properties.append(metadata(value->as<Entity>()));
|
|
|
|
|
description.insert("Values", properties);
|
|
|
|
|
}
|
|
|
|
|
data.append(description);
|
|
|
|
|
};
|
|
|
|
|
appendDefinition(static_cast<IfcUtil::IfcBaseClass*>(definitionValue), 0);
|
|
|
|
|
aggregate_of_instance::ptr related = attribute(relation, "RelatedObjects");
|
|
|
|
|
for (auto* target : *related) for (const auto& definition : data)
|
|
|
|
|
propertySets[target->as<Entity>()->id()].append(definition);
|
|
|
|
|
}
|
|
|
|
|
for (const auto& entry : objects) {
|
|
|
|
|
if (propertySets.count(entry.first)) stringProperty(entry.second, "IfcPropertySets",
|
|
|
|
|
QJsonDocument(propertySets[entry.first]).toJson(QJsonDocument::Compact).toStdString());
|
|
|
|
|
}
|
|
|
|
|
auto* provenance = document->addObject("App::FeaturePython", "IfcSource");
|
|
|
|
|
auto* source = dynamic_cast<App::PropertyFileIncluded*>(provenance->addDynamicProperty("App::PropertyFileIncluded", "OriginalIFC", "IFC"));
|
|
|
|
|
source->setValue(path, "original.ifc");
|
|
|
|
|
stringProperty(provenance, "Importer", "IfcOpenShell 0.8.6 / native OpenCASCADE / IFC2X3,IFC4 / import-only");
|
|
|
|
|
stringProperty(provenance, "LengthUnit", "mm");
|
|
|
|
|
stringProperty(provenance, "GeometryCount", std::to_string(geometryCount));
|
|
|
|
|
document->FileName.setValue(path);
|
|
|
|
|
bool errors = false; document->recompute({}, false, &errors);
|
|
|
|
|
if (errors) throw std::runtime_error("FreeCAD recompute failed after IFC import");
|
|
|
|
|
document->commitTransaction();
|
|
|
|
|
application.setActiveDocument(document);
|
|
|
|
|
return document->getPyObject();
|
|
|
|
|
} catch (const std::exception& error) {
|
|
|
|
|
if (document) App::GetApplication().closeDocument(document);
|
|
|
|
|
PyErr_SetString(PyExc_RuntimeError, error.what());
|
|
|
|
|
} catch (...) {
|
|
|
|
|
if (document) App::GetApplication().closeDocument(document);
|
|
|
|
|
PyErr_SetString(PyExc_RuntimeError, "IFC import failed and the new document was rolled back");
|
|
|
|
|
}
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
PyMethodDef methods[] = {{"open", openIfc, METH_VARARGS, "Import real IFC2X3/IFC4 geometry into a new native FreeCAD document."}, {nullptr,nullptr,0,nullptr}};
|
|
|
|
|
PyModuleDef module = {PyModuleDef_HEAD_INIT, "IfcOpenShellNative", "Native import-only IfcOpenShell/OCCT binding, not the full Python API.", -1, methods, nullptr, nullptr, nullptr, nullptr};
|
|
|
|
|
}
|
|
|
|
|
extern "C" PyObject* PyInit_IfcOpenShellNative() { return PyModule_Create(&module); }
|