Save verified native IFC import and user validation evidence

Real IFC2X3/IFC4 import tested. Full BIM authoring/export remains out of scope; 349-second building import performance remains to optimize.
This commit is contained in:
2026-09-16 04:27:37 +08:00
parent 9a7c4f0335
commit 72152ef356
120 changed files with 3020296 additions and 114 deletions

View File

@@ -83,6 +83,22 @@ target_link_options(FreeCADWebPartBoxProbe PRIVATE
set_property(TARGET FreeCADWebPartBoxProbe APPEND PROPERTY LINK_DEPENDS
"${CMAKE_SOURCE_DIR}/src/Ext/freecad/project_utility.py")
set(FREECAD_WEB_IFCOPENSHELL_SOURCE "" CACHE PATH "Real IfcOpenShell source root")
set(FREECAD_WEB_IFCOPENSHELL_BUILD "" CACHE PATH "IfcOpenShell WASM static library build")
if(FREECAD_WEB_IFCOPENSHELL_SOURCE AND FREECAD_WEB_IFCOPENSHELL_BUILD)
foreach(_ifc_archive ifcparse/libIfcParse.a ifcgeom/libIfcGeom.a ifcgeom/kernels/libgeometry_kernel_opencascade.a)
if(NOT EXISTS "${FREECAD_WEB_IFCOPENSHELL_BUILD}/${_ifc_archive}")
message(FATAL_ERROR "Missing real IfcOpenShell archive: ${_ifc_archive}")
endif()
target_link_libraries(FreeCADWebPartBoxProbe PRIVATE "${FREECAD_WEB_IFCOPENSHELL_BUILD}/${_ifc_archive}")
endforeach()
target_sources(FreeCADWebPartBoxProbe PRIVATE IfcOpenShellImport.cpp)
target_include_directories(FreeCADWebPartBoxProbe SYSTEM PRIVATE
"${FREECAD_WEB_IFCOPENSHELL_SOURCE}/src"
"${FREECAD_WEB_EIGEN_ROOT}/include/eigen3")
target_compile_definitions(FreeCADWebPartBoxProbe PRIVATE FREECAD_WEB_HAS_IFCOPENSHELL=1 IFOPSH_WITH_OPENCASCADE)
endif()
add_executable(FreeCADWebPartDesktopCommandProbe PartDesktopCommandProbe.cpp PartDesktopCommands.cpp)
target_compile_features(FreeCADWebPartDesktopCommandProbe PRIVATE cxx_std_17)
target_compile_options(FreeCADWebPartDesktopCommandProbe PRIVATE "-O2" "-g0" "-fexceptions" "-pthread")

View File

@@ -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); }

View File

@@ -92,6 +92,9 @@
extern "C" PyObject* PyInit_Part();
extern "C" PyObject* PyInit_Mesh();
#ifdef FREECAD_WEB_HAS_IFCOPENSHELL
extern "C" PyObject* PyInit_IfcOpenShellNative();
#endif
extern "C" PyObject* PyInit_Materials();
namespace {
@@ -161,6 +164,9 @@ void ensurePartPythonRuntime()
addPythonModule("Materials", &PyInit_Materials);
addPythonModule("Part", &PyInit_Part);
addPythonModule("Mesh", &PyInit_Mesh);
#ifdef FREECAD_WEB_HAS_IFCOPENSHELL
addPythonModule("IfcOpenShellNative", &PyInit_IfcOpenShellNative);
#endif
const char* setup =
"import sys\n"
"sys.path.insert(0, '/freecad-python/stdlib')\n"

View File

@@ -28,6 +28,12 @@ try:
# Reuse the Part registrations from Mod/Part/Init.py. STEP with colors
# belongs to Import, not a geometry-only substitute in another module.
import Part
try:
import IfcOpenShellNative
if not App.getImportType('ifc'):
App.addImportType('Industry Foundation Classes (*.ifc *.IFC)', 'IfcOpenShellNative')
except ImportError:
pass
try:
import Mesh
if not App.getImportType('obj'): App.addImportType('Wavefront OBJ (*.obj *.OBJ)', 'Mesh')

View File

@@ -1837,7 +1837,13 @@ def _core_view_command(name, args, document_id):
raise ValueError('Selected native subelement does not exist')
selected.append({'documentId': doc.Name, 'object': obj.Name, 'subelements': subs})
shapes = {}
ifc_document = doc.getObject('IfcSource') is not None
display_deflection = 2.0 if ifc_document else 0.1
for obj in doc.Objects:
if ifc_document and obj.isDerivedFrom('App::DocumentObjectGroup'):
continue
if ifc_document:
print('IFC scene: shape', obj.Name, flush=True)
if hasattr(obj, 'Shape') and not obj.Shape.isNull():
shape = obj.Shape.copy()
if hasattr(obj, 'getGlobalPlacement'):
@@ -1850,7 +1856,7 @@ def _core_view_command(name, args, document_id):
if hasattr(obj, 'getGlobalPlacement'):
shape.Placement = obj.getGlobalPlacement()
shapes[obj.Name] = shape
rotation = list(state.ViewRotation) if state else [0., 0., 0., 1.]
rotation = list(state.ViewRotation) if state else ([0.424708, 0.175920, 0.339851, 0.820473] if ifc_document else [0., 0., 0., 1.])
style = state.DrawStyle if state else 0
visibility = json.loads(state.ObjectVisibility) if state and 'ObjectVisibility' in state.PropertiesList else {}
visibility = {obj.Name: bool(obj.Visibility) if 'Visibility' in obj.PropertiesList else visibility.get(obj.Name, True)
@@ -2015,10 +2021,14 @@ def _core_view_command(name, args, document_id):
for object_name, shape in shapes.items():
if not visibility.get(object_name, True):
continue
vertices, triangles = shape.tessellate(0.1) if shape.Faces else ([v.Point for v in shape.Vertexes], [])
if ifc_document:
print('IFC scene: tessellate', object_name, flush=True)
vertices, triangles = shape.tessellate(display_deflection) if shape.Faces else ([v.Point for v in shape.Vertexes], [])
if ifc_document:
print('IFC scene: edges', object_name, len(triangles), flush=True)
meshes.append({'object': object_name, 'positions': [[p.x, p.y, p.z] for p in vertices],
'triangles': [list(t) for t in triangles],
'edges': [[[p.x, p.y, p.z] for p in e.discretize(Deflection=0.1)] for e in shape.Edges]})
'edges': [[[p.x, p.y, p.z] for p in e.discretize(Deflection=display_deflection)] for e in shape.Edges]})
if name == 'Std_SceneInspector' or name == 'Std_DrawStyle' and args.get('action') == 'read':
return {'documentId': doc.Name, 'selection': selected,
'view': {'rotation': rotation, 'drawStyle': style, 'visibility': visibility},